summaryrefslogtreecommitdiffstats
path: root/xbmc
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc')
-rw-r--r--xbmc/AutoSwitch.cpp232
-rw-r--r--xbmc/AutoSwitch.h30
-rw-r--r--xbmc/Autorun.cpp554
-rw-r--r--xbmc/Autorun.h74
-rw-r--r--xbmc/BackgroundInfoLoader.cpp151
-rw-r--r--xbmc/BackgroundInfoLoader.h62
-rw-r--r--xbmc/CMakeLists.txt91
-rw-r--r--xbmc/CompileInfo.cpp.in118
-rw-r--r--xbmc/CompileInfo.h37
-rw-r--r--xbmc/ContextMenuItem.cpp115
-rw-r--r--xbmc/ContextMenuItem.h90
-rw-r--r--xbmc/ContextMenuManager.cpp293
-rw-r--r--xbmc/ContextMenuManager.h80
-rw-r--r--xbmc/ContextMenus.cpp104
-rw-r--r--xbmc/ContextMenus.h40
-rw-r--r--xbmc/CueDocument.cpp490
-rw-r--r--xbmc/CueDocument.h68
-rw-r--r--xbmc/DatabaseManager.cpp232
-rw-r--r--xbmc/DatabaseManager.h65
-rw-r--r--xbmc/DbUrl.cpp136
-rw-r--r--xbmc/DbUrl.h57
-rw-r--r--xbmc/DllPaths.h18
-rw-r--r--xbmc/DllPaths_generated.h.in19
-rw-r--r--xbmc/DllPaths_generated_android.h.in24
-rw-r--r--xbmc/DllPaths_win32.h12
-rw-r--r--xbmc/DynamicDll.cpp81
-rw-r--r--xbmc/DynamicDll.h534
-rw-r--r--xbmc/FileItem.cpp4036
-rw-r--r--xbmc/FileItem.h884
-rw-r--r--xbmc/FileItemListModification.cpp54
-rw-r--r--xbmc/FileItemListModification.h31
-rw-r--r--xbmc/GUIInfoManager.cpp11456
-rw-r--r--xbmc/GUIInfoManager.h230
-rw-r--r--xbmc/GUILargeTextureManager.cpp247
-rw-r--r--xbmc/GUILargeTextureManager.h143
-rw-r--r--xbmc/GUIPassword.cpp636
-rw-r--r--xbmc/GUIPassword.h102
-rw-r--r--xbmc/GUIUserMessages.h145
-rw-r--r--xbmc/HDRStatus.h17
-rw-r--r--xbmc/IFileItemListModifier.h21
-rw-r--r--xbmc/IProgressCallback.h18
-rw-r--r--xbmc/InfoScanner.cpp30
-rw-r--r--xbmc/InfoScanner.h75
-rw-r--r--xbmc/LangInfo.cpp1528
-rw-r--r--xbmc/LangInfo.h325
-rw-r--r--xbmc/LockType.h20
-rw-r--r--xbmc/MediaSource.cpp112
-rw-r--r--xbmc/MediaSource.h109
-rw-r--r--xbmc/NfoFile.cpp190
-rw-r--r--xbmc/NfoFile.h69
-rw-r--r--xbmc/PartyModeManager.cpp575
-rw-r--r--xbmc/PartyModeManager.h82
-rw-r--r--xbmc/PasswordManager.cpp211
-rw-r--r--xbmc/PasswordManager.h105
-rw-r--r--xbmc/PlayListPlayer.cpp1060
-rw-r--r--xbmc/PlayListPlayer.h206
-rw-r--r--xbmc/SectionLoader.cpp130
-rw-r--r--xbmc/SectionLoader.h47
-rw-r--r--xbmc/SeekHandler.cpp438
-rw-r--r--xbmc/SeekHandler.h81
-rw-r--r--xbmc/ServiceBroker.cpp436
-rw-r--r--xbmc/ServiceBroker.h240
-rw-r--r--xbmc/ServiceManager.cpp442
-rw-r--r--xbmc/ServiceManager.h200
-rw-r--r--xbmc/SettingsLock.h25
-rw-r--r--xbmc/SortFileItem.h67
-rw-r--r--xbmc/SystemGlobals.cpp41
-rw-r--r--xbmc/TextureCache.cpp350
-rw-r--r--xbmc/TextureCache.h230
-rw-r--r--xbmc/TextureCacheJob.cpp293
-rw-r--r--xbmc/TextureCacheJob.h137
-rw-r--r--xbmc/TextureDatabase.cpp486
-rw-r--r--xbmc/TextureDatabase.h125
-rw-r--r--xbmc/ThumbLoader.cpp125
-rw-r--r--xbmc/ThumbLoader.h75
-rw-r--r--xbmc/URL.cpp804
-rw-r--r--xbmc/URL.h202
-rw-r--r--xbmc/Util.cpp2350
-rw-r--r--xbmc/Util.h256
-rw-r--r--xbmc/XBDateTime.cpp1580
-rw-r--r--xbmc/XBDateTime.h213
-rw-r--r--xbmc/addons/Addon.cpp679
-rw-r--r--xbmc/addons/Addon.h490
-rw-r--r--xbmc/addons/AddonBindings.cmake11
-rw-r--r--xbmc/addons/AddonBuilder.cpp121
-rw-r--r--xbmc/addons/AddonBuilder.h29
-rw-r--r--xbmc/addons/AddonDatabase.cpp1254
-rw-r--r--xbmc/addons/AddonDatabase.h247
-rw-r--r--xbmc/addons/AddonEvents.h126
-rw-r--r--xbmc/addons/AddonInstaller.cpp1286
-rw-r--r--xbmc/addons/AddonInstaller.h244
-rw-r--r--xbmc/addons/AddonManager.cpp1391
-rw-r--r--xbmc/addons/AddonManager.h696
-rw-r--r--xbmc/addons/AddonProvider.h40
-rw-r--r--xbmc/addons/AddonRepoInfo.h23
-rw-r--r--xbmc/addons/AddonRepos.cpp515
-rw-r--r--xbmc/addons/AddonRepos.h241
-rw-r--r--xbmc/addons/AddonStatusHandler.cpp131
-rw-r--r--xbmc/addons/AddonStatusHandler.h45
-rw-r--r--xbmc/addons/AddonSystemSettings.cpp157
-rw-r--r--xbmc/addons/AddonSystemSettings.h78
-rw-r--r--xbmc/addons/AddonUpdateRules.cpp104
-rw-r--r--xbmc/addons/AddonUpdateRules.h88
-rw-r--r--xbmc/addons/AddonVersion.cpp182
-rw-r--r--xbmc/addons/AddonVersion.h62
-rw-r--r--xbmc/addons/AudioDecoder.cpp258
-rw-r--r--xbmc/addons/AudioDecoder.h50
-rw-r--r--xbmc/addons/BinaryAddonCache.cpp133
-rw-r--r--xbmc/addons/BinaryAddonCache.h47
-rw-r--r--xbmc/addons/CMakeLists.txt74
-rw-r--r--xbmc/addons/ContextMenuAddon.cpp108
-rw-r--r--xbmc/addons/ContextMenuAddon.h36
-rw-r--r--xbmc/addons/ContextMenus.cpp100
-rw-r--r--xbmc/addons/ContextMenus.h54
-rw-r--r--xbmc/addons/ExtsMimeSupportList.cpp291
-rw-r--r--xbmc/addons/ExtsMimeSupportList.h187
-rw-r--r--xbmc/addons/FilesystemInstaller.cpp110
-rw-r--r--xbmc/addons/FilesystemInstaller.h36
-rw-r--r--xbmc/addons/FontResource.cpp51
-rw-r--r--xbmc/addons/FontResource.h35
-rw-r--r--xbmc/addons/GameResource.cpp20
-rw-r--r--xbmc/addons/GameResource.h28
-rw-r--r--xbmc/addons/IAddon.h159
-rw-r--r--xbmc/addons/IAddonManagerCallback.h32
-rw-r--r--xbmc/addons/IAddonSupportCheck.h46
-rw-r--r--xbmc/addons/IAddonSupportList.h78
-rw-r--r--xbmc/addons/ImageDecoder.cpp213
-rw-r--r--xbmc/addons/ImageDecoder.h73
-rw-r--r--xbmc/addons/ImageResource.cpp72
-rw-r--r--xbmc/addons/ImageResource.h40
-rw-r--r--xbmc/addons/LanguageResource.cpp151
-rw-r--r--xbmc/addons/LanguageResource.h59
-rw-r--r--xbmc/addons/PluginSource.cpp86
-rw-r--r--xbmc/addons/PluginSource.h55
-rw-r--r--xbmc/addons/Repository.cpp326
-rw-r--r--xbmc/addons/Repository.h76
-rw-r--r--xbmc/addons/RepositoryUpdater.cpp338
-rw-r--r--xbmc/addons/RepositoryUpdater.h98
-rw-r--r--xbmc/addons/Resource.h43
-rw-r--r--xbmc/addons/Scraper.cpp1470
-rw-r--r--xbmc/addons/Scraper.h182
-rw-r--r--xbmc/addons/ScreenSaver.cpp85
-rw-r--r--xbmc/addons/ScreenSaver.h34
-rw-r--r--xbmc/addons/Service.cpp131
-rw-r--r--xbmc/addons/Service.h60
-rw-r--r--xbmc/addons/Skin.cpp906
-rw-r--r--xbmc/addons/Skin.h297
-rw-r--r--xbmc/addons/UISoundsResource.cpp45
-rw-r--r--xbmc/addons/UISoundsResource.h26
-rw-r--r--xbmc/addons/VFSEntry.cpp770
-rw-r--r--xbmc/addons/VFSEntry.h311
-rw-r--r--xbmc/addons/Visualization.cpp234
-rw-r--r--xbmc/addons/Visualization.h59
-rw-r--r--xbmc/addons/Webinterface.cpp52
-rw-r--r--xbmc/addons/Webinterface.h38
-rw-r--r--xbmc/addons/addoninfo/AddonExtensions.cpp68
-rw-r--r--xbmc/addons/addoninfo/AddonExtensions.h76
-rw-r--r--xbmc/addons/addoninfo/AddonInfo.cpp312
-rw-r--r--xbmc/addons/addoninfo/AddonInfo.h309
-rw-r--r--xbmc/addons/addoninfo/AddonInfoBuilder.cpp874
-rw-r--r--xbmc/addons/addoninfo/AddonInfoBuilder.h109
-rw-r--r--xbmc/addons/addoninfo/AddonType.cpp59
-rw-r--r--xbmc/addons/addoninfo/AddonType.h125
-rw-r--r--xbmc/addons/addoninfo/CMakeLists.txt11
-rw-r--r--xbmc/addons/binary-addons/AddonDll.cpp553
-rw-r--r--xbmc/addons/binary-addons/AddonDll.h163
-rw-r--r--xbmc/addons/binary-addons/AddonInstanceHandler.cpp386
-rw-r--r--xbmc/addons/binary-addons/AddonInstanceHandler.h135
-rw-r--r--xbmc/addons/binary-addons/BinaryAddonBase.cpp108
-rw-r--r--xbmc/addons/binary-addons/BinaryAddonBase.h54
-rw-r--r--xbmc/addons/binary-addons/BinaryAddonManager.cpp88
-rw-r--r--xbmc/addons/binary-addons/BinaryAddonManager.h95
-rw-r--r--xbmc/addons/binary-addons/CMakeLists.txt12
-rw-r--r--xbmc/addons/binary-addons/DllAddon.h35
-rw-r--r--xbmc/addons/gui/CMakeLists.txt13
-rw-r--r--xbmc/addons/gui/GUIDialogAddonInfo.cpp940
-rw-r--r--xbmc/addons/gui/GUIDialogAddonInfo.h162
-rw-r--r--xbmc/addons/gui/GUIDialogAddonSettings.cpp484
-rw-r--r--xbmc/addons/gui/GUIDialogAddonSettings.h54
-rw-r--r--xbmc/addons/gui/GUIHelpers.cpp49
-rw-r--r--xbmc/addons/gui/GUIHelpers.h41
-rw-r--r--xbmc/addons/gui/GUIViewStateAddonBrowser.cpp64
-rw-r--r--xbmc/addons/gui/GUIViewStateAddonBrowser.h21
-rw-r--r--xbmc/addons/gui/GUIWindowAddonBrowser.cpp644
-rw-r--r--xbmc/addons/gui/GUIWindowAddonBrowser.h115
-rw-r--r--xbmc/addons/gui/skin/CMakeLists.txt7
-rw-r--r--xbmc/addons/gui/skin/SkinTimer.cpp97
-rw-r--r--xbmc/addons/gui/skin/SkinTimer.h110
-rw-r--r--xbmc/addons/gui/skin/SkinTimerManager.cpp222
-rw-r--r--xbmc/addons/gui/skin/SkinTimerManager.h77
-rw-r--r--xbmc/addons/gui/skin/SkinTimers.dox164
-rw-r--r--xbmc/addons/interfaces/AddonBase.cpp661
-rw-r--r--xbmc/addons/interfaces/AddonBase.h87
-rw-r--r--xbmc/addons/interfaces/AudioEngine.cpp761
-rw-r--r--xbmc/addons/interfaces/AudioEngine.h212
-rw-r--r--xbmc/addons/interfaces/CMakeLists.txt18
-rw-r--r--xbmc/addons/interfaces/Filesystem.cpp1181
-rw-r--r--xbmc/addons/interfaces/Filesystem.h131
-rw-r--r--xbmc/addons/interfaces/General.cpp468
-rw-r--r--xbmc/addons/interfaces/General.h76
-rw-r--r--xbmc/addons/interfaces/Network.cpp181
-rw-r--r--xbmc/addons/interfaces/Network.h54
-rw-r--r--xbmc/addons/interfaces/gui/CMakeLists.txt11
-rw-r--r--xbmc/addons/interfaces/gui/GUITranslator.cpp969
-rw-r--r--xbmc/addons/interfaces/gui/GUITranslator.h41
-rw-r--r--xbmc/addons/interfaces/gui/General.cpp252
-rw-r--r--xbmc/addons/interfaces/gui/General.h62
-rw-r--r--xbmc/addons/interfaces/gui/ListItem.cpp428
-rw-r--r--xbmc/addons/interfaces/gui/ListItem.h76
-rw-r--r--xbmc/addons/interfaces/gui/Window.cpp1499
-rw-r--r--xbmc/addons/interfaces/gui/Window.h278
-rw-r--r--xbmc/addons/interfaces/gui/controls/Button.cpp146
-rw-r--r--xbmc/addons/interfaces/gui/controls/Button.h56
-rw-r--r--xbmc/addons/interfaces/gui/controls/CMakeLists.txt27
-rw-r--r--xbmc/addons/interfaces/gui/controls/Edit.cpp239
-rw-r--r--xbmc/addons/interfaces/gui/controls/Edit.h67
-rw-r--r--xbmc/addons/interfaces/gui/controls/FadeLabel.cpp130
-rw-r--r--xbmc/addons/interfaces/gui/controls/FadeLabel.h57
-rw-r--r--xbmc/addons/interfaces/gui/controls/Image.cpp92
-rw-r--r--xbmc/addons/interfaces/gui/controls/Image.h57
-rw-r--r--xbmc/addons/interfaces/gui/controls/Label.cpp92
-rw-r--r--xbmc/addons/interfaces/gui/controls/Label.h53
-rw-r--r--xbmc/addons/interfaces/gui/controls/Progress.cpp88
-rw-r--r--xbmc/addons/interfaces/gui/controls/Progress.h53
-rw-r--r--xbmc/addons/interfaces/gui/controls/RadioButton.cpp146
-rw-r--r--xbmc/addons/interfaces/gui/controls/RadioButton.h57
-rw-r--r--xbmc/addons/interfaces/gui/controls/Rendering.cpp148
-rw-r--r--xbmc/addons/interfaces/gui/controls/Rendering.h89
-rw-r--r--xbmc/addons/interfaces/gui/controls/SettingsSlider.cpp311
-rw-r--r--xbmc/addons/interfaces/gui/controls/SettingsSlider.h77
-rw-r--r--xbmc/addons/interfaces/gui/controls/Slider.cpp305
-rw-r--r--xbmc/addons/interfaces/gui/controls/Slider.h78
-rw-r--r--xbmc/addons/interfaces/gui/controls/Spin.cpp346
-rw-r--r--xbmc/addons/interfaces/gui/controls/Spin.h86
-rw-r--r--xbmc/addons/interfaces/gui/controls/TextBox.cpp147
-rw-r--r--xbmc/addons/interfaces/gui/controls/TextBox.h57
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/CMakeLists.txt23
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/ContextMenu.cpp67
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/ContextMenu.h53
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/ExtendedProgressBar.cpp305
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/ExtendedProgressBar.h63
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/FileBrowser.cpp403
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/FileBrowser.h116
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/Keyboard.cpp319
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/Keyboard.h94
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/Numeric.cpp270
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/Numeric.h74
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/OK.cpp73
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/OK.h57
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/Progress.cpp328
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/Progress.h65
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/Select.cpp143
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/Select.h62
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/TextViewer.cpp64
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/TextViewer.h50
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/YesNo.cpp136
-rw-r--r--xbmc/addons/interfaces/gui/dialogs/YesNo.h72
-rw-r--r--xbmc/addons/interfaces/platform/android/System.cpp57
-rw-r--r--xbmc/addons/interfaces/platform/android/System.h26
-rw-r--r--xbmc/addons/kodi-dev-kit/.gitignore36
-rw-r--r--xbmc/addons/kodi-dev-kit/CMakeLists.txt4
-rw-r--r--xbmc/addons/kodi-dev-kit/cmake/test/abi-interface-test.cmake181
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Doxyfile2589
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/DoxygenLayout.xml207
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/General/DoxygenOnAddon.dox90
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/General/General.dox31
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/cpp_kodi_addon_vfs_protocol_1.pngbin0 -> 47796 bytes
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/cpp_kodi_addon_vfs_protocol_2.pngbin0 -> 36079 bytes
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/logo-cpp.pngbin0 -> 5153 bytes
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/logo-python.pngbin0 -> 5496 bytes
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp.dox165
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_gui.dox96
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral.dox414
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_Docs_ControllerInput_1.jpgbin0 -> 52077 bytes
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_Docs_Lifetime_9.pngbin0 -> 402251 bytes
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_1.dox294
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_10.dox149
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_2.dox139
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_3.dox138
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_4.dox94
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_5.dox113
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_6.dox185
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_7.dox130
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_8.dox86
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_9.dox215
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_general.dox8
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_python.dox299
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Skin/skin.dox69
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/kodi-dev.pngbin0 -> 1680 bytes
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/main.txt49
-rw-r--r--xbmc/addons/kodi-dev-kit/include/NOTE9
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/AddonBase.h1877
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/AudioEngine.h619
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/CMakeLists.txt15
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/Filesystem.h2379
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/General.h688
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/Network.h287
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/AudioDecoder.h728
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/AudioEncoder.h514
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/CMakeLists.txt20
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Game.h1432
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/ImageDecoder.h711
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Inputstream.h2050
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/PVR.h3568
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Peripheral.h889
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Screensaver.h424
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/VFS.h1211
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/VideoCodec.h446
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Visualization.h917
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/CMakeLists.txt14
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/DemuxPacket.h12
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamCodec.h11
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamConstants.h11
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamCrypto.h100
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/TimingConstants.h59
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/peripheral/CMakeLists.txt10
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/peripheral/PeripheralUtils.h1277
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/CMakeLists.txt19
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/ChannelGroups.h271
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Channels.h535
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/EDL.h90
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/EPG.h516
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/General.h536
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/MenuHook.h130
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Providers.h195
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Recordings.h545
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Stream.h330
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Timers.h896
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/CMakeLists.txt14
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/CMakeLists.txt20
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audiodecoder.h151
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audioencoder.h74
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/game.h1235
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/imagedecoder.h450
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream.h708
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/CMakeLists.txt14
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/demux_packet.h117
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_codec.h152
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_constants.h59
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_crypto.h138
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/timing_constants.h42
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/peripheral.h707
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr.h343
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/CMakeLists.txt20
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channel_groups.h57
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h108
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_defines.h77
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_edl.h65
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h657
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h300
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_menu_hook.h75
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h97
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_recordings.h148
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_stream.h156
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_timers.h410
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/screensaver.h60
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/vfs.h147
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/video_codec.h268
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/visualization.h139
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon_base.h404
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/audio_engine.h312
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/filesystem.h322
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/general.h124
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/CMakeLists.txt13
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/CMakeLists.txt21
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/button.h33
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/edit.h77
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/fade_label.h32
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/image.h35
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/label.h30
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/progress.h30
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/radio_button.h33
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/rendering.h36
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/settings_slider.h46
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/slider.h46
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/spin.h56
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/text_box.h34
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/definitions.h78
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/CMakeLists.txt19
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/context_menu.h31
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/extended_progress.h41
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/filebrowser.h75
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/keyboard.h72
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/numeric.h52
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/ok.h35
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/progress.h43
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/select.h40
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/text_viewer.h28
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/yes_no.h48
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/general.h50
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/input/CMakeLists.txt10
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/input/action_ids.h761
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/list_item.h52
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/window.h181
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/network.h46
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/CMakeLists.txt9
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/android/CMakeLists.txt10
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/android/system.h34
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/CMakeLists.txt13
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/General.h190
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/ListItem.h345
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/Window.h915
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Button.h166
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/CMakeLists.txt21
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Edit.h217
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/FadeLabel.h148
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Image.h112
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Label.h118
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Progress.h112
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/RadioButton.h214
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Rendering.h217
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/SettingsSlider.h314
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Slider.h326
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Spin.h416
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/TextBox.h164
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/CMakeLists.txt19
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/ContextMenu.h185
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/ExtendedProgress.h243
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/FileBrowser.h304
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Keyboard.h405
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Numeric.h347
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/OK.h101
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Progress.h244
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Select.h270
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/TextViewer.h109
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/YesNo.h188
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/CMakeLists.txt12
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/GL.h112
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/GLonDX.h395
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/Shader.h571
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/input/ActionIDs.h11
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/input/CMakeLists.txt10
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/gui/renderHelper.h82
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/platform/CMakeLists.txt9
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/platform/android/CMakeLists.txt10
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/platform/android/System.h109
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/tools/CMakeLists.txt14
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/tools/DllHelper.h211
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/tools/EndTime.h215
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/tools/StringUtils.h3086
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/tools/Thread.h399
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/tools/Timer.h315
-rw-r--r--xbmc/addons/kodi-dev-kit/include/kodi/versions.h517
-rwxr-xr-xxbmc/addons/kodi-dev-kit/tools/code-generator/code_generator.py76
-rw-r--r--xbmc/addons/kodi-dev-kit/tools/code-generator/src/commitChanges.py91
-rw-r--r--xbmc/addons/kodi-dev-kit/tools/code-generator/src/generateCMake__CMAKE_TREEDATA_COMMON_addon_dev_kit_txt.py56
-rw-r--r--xbmc/addons/kodi-dev-kit/tools/code-generator/src/generateCMake__XBMC_ADDONS_KODIDEVKIT_INCLUDE_KODI_allfiles.py134
-rw-r--r--xbmc/addons/kodi-dev-kit/tools/code-generator/src/helper_Log.py201
-rwxr-xr-xxbmc/addons/kodi-dev-kit/tools/debian-addon-package-test.sh141
-rwxr-xr-xxbmc/addons/kodi-dev-kit/tools/doxygen-header-class-list-creator.py190
-rw-r--r--xbmc/addons/settings/AddonSettings.cpp1726
-rw-r--r--xbmc/addons/settings/AddonSettings.h203
-rw-r--r--xbmc/addons/settings/CMakeLists.txt7
-rw-r--r--xbmc/addons/settings/SettingUrlEncodedString.cpp44
-rw-r--r--xbmc/addons/settings/SettingUrlEncodedString.h33
-rw-r--r--xbmc/addons/test/CMakeLists.txt6
-rw-r--r--xbmc/addons/test/TestAddonBuilder.cpp54
-rw-r--r--xbmc/addons/test/TestAddonDatabase.cpp80
-rw-r--r--xbmc/addons/test/TestAddonInfoBuilder.cpp179
-rw-r--r--xbmc/addons/test/TestAddonVersion.cpp271
-rw-r--r--xbmc/application/AppEnvironment.cpp32
-rw-r--r--xbmc/application/AppEnvironment.h20
-rw-r--r--xbmc/application/AppInboundProtocol.cpp31
-rw-r--r--xbmc/application/AppInboundProtocol.h24
-rw-r--r--xbmc/application/AppParamParser.cpp118
-rw-r--r--xbmc/application/AppParamParser.h34
-rw-r--r--xbmc/application/AppParams.cpp20
-rw-r--r--xbmc/application/AppParams.h89
-rw-r--r--xbmc/application/Application.cpp3693
-rw-r--r--xbmc/application/Application.h257
-rw-r--r--xbmc/application/ApplicationActionListeners.cpp48
-rw-r--r--xbmc/application/ApplicationActionListeners.h53
-rw-r--r--xbmc/application/ApplicationComponents.h15
-rw-r--r--xbmc/application/ApplicationEnums.h25
-rw-r--r--xbmc/application/ApplicationPlayer.cpp1061
-rw-r--r--xbmc/application/ApplicationPlayer.h205
-rw-r--r--xbmc/application/ApplicationPlayerCallback.cpp346
-rw-r--r--xbmc/application/ApplicationPlayerCallback.h43
-rw-r--r--xbmc/application/ApplicationPowerHandling.cpp598
-rw-r--r--xbmc/application/ApplicationPowerHandling.h116
-rw-r--r--xbmc/application/ApplicationSettingsHandling.cpp214
-rw-r--r--xbmc/application/ApplicationSettingsHandling.h34
-rw-r--r--xbmc/application/ApplicationSkinHandling.cpp514
-rw-r--r--xbmc/application/ApplicationSkinHandling.h53
-rw-r--r--xbmc/application/ApplicationStackHelper.cpp332
-rw-r--r--xbmc/application/ApplicationStackHelper.h213
-rw-r--r--xbmc/application/ApplicationVolumeHandling.cpp201
-rw-r--r--xbmc/application/ApplicationVolumeHandling.h71
-rw-r--r--xbmc/application/CMakeLists.txt29
-rw-r--r--xbmc/application/IApplicationComponent.h16
-rw-r--r--xbmc/cdrip/CDDARipJob.cpp259
-rw-r--r--xbmc/cdrip/CDDARipJob.h92
-rw-r--r--xbmc/cdrip/CDDARipper.cpp326
-rw-r--r--xbmc/cdrip/CDDARipper.h102
-rw-r--r--xbmc/cdrip/CMakeLists.txt17
-rw-r--r--xbmc/cdrip/Encoder.cpp159
-rw-r--r--xbmc/cdrip/Encoder.h67
-rw-r--r--xbmc/cdrip/EncoderAddon.cpp95
-rw-r--r--xbmc/cdrip/EncoderAddon.h43
-rw-r--r--xbmc/cdrip/EncoderFFmpeg.cpp413
-rw-r--r--xbmc/cdrip/EncoderFFmpeg.h74
-rw-r--r--xbmc/cdrip/IEncoder.h46
-rw-r--r--xbmc/commons/Buffer.h254
-rw-r--r--xbmc/commons/CMakeLists.txt7
-rw-r--r--xbmc/commons/Exception.cpp22
-rw-r--r--xbmc/commons/Exception.h113
-rw-r--r--xbmc/commons/ilog.h46
-rw-r--r--xbmc/contrib/kissfft/CMakeLists.txt10
-rw-r--r--xbmc/contrib/kissfft/COPYING11
-rw-r--r--xbmc/contrib/kissfft/_kiss_fft_guts.h158
-rw-r--r--xbmc/contrib/kissfft/kiss_fft.c401
-rw-r--r--xbmc/contrib/kissfft/kiss_fft.h132
-rw-r--r--xbmc/contrib/kissfft/kiss_fftr.c154
-rw-r--r--xbmc/contrib/kissfft/kiss_fftr.h54
-rw-r--r--xbmc/cores/AudioEngine/AEResampleFactory.cpp20
-rw-r--r--xbmc/cores/AudioEngine/AEResampleFactory.h33
-rw-r--r--xbmc/cores/AudioEngine/AESinkFactory.cpp114
-rw-r--r--xbmc/cores/AudioEngine/AESinkFactory.h58
-rw-r--r--xbmc/cores/AudioEngine/CMakeLists.txt166
-rw-r--r--xbmc/cores/AudioEngine/Encoders/AEEncoderFFmpeg.cpp322
-rw-r--r--xbmc/cores/AudioEngine/Encoders/AEEncoderFFmpeg.h55
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAE.cpp3498
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAE.h408
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.cpp703
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.h147
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEFilter.cpp351
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEFilter.h61
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEResampleFFMPEG.cpp277
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEResampleFFMPEG.h53
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESettings.cpp191
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESettings.h69
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESink.cpp1175
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESink.h158
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESound.cpp158
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESound.h71
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEStream.cpp797
-rw-r--r--xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEStream.h246
-rw-r--r--xbmc/cores/AudioEngine/Interfaces/AE.h323
-rw-r--r--xbmc/cores/AudioEngine/Interfaces/AEEncoder.h95
-rw-r--r--xbmc/cores/AudioEngine/Interfaces/AEResample.h34
-rw-r--r--xbmc/cores/AudioEngine/Interfaces/AESink.h85
-rw-r--r--xbmc/cores/AudioEngine/Interfaces/AESound.h44
-rw-r--r--xbmc/cores/AudioEngine/Interfaces/AEStream.h272
-rw-r--r--xbmc/cores/AudioEngine/Interfaces/IAudioCallback.h23
-rw-r--r--xbmc/cores/AudioEngine/Interfaces/ThreadedAE.h20
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkALSA.cpp1691
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkALSA.h100
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkAUDIOTRACK.cpp1286
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkAUDIOTRACK.h103
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkDARWINIOS.h56
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkDARWINIOS.mm766
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkDARWINOSX.cpp556
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkDARWINOSX.h62
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.h57
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.mm898
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkDirectSound.cpp758
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkDirectSound.h73
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkOSS.cpp544
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkOSS.h46
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkPULSE.cpp1297
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkPULSE.h81
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkSNDIO.cpp316
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkSNDIO.h49
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkWASAPI.cpp1002
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkWASAPI.h76
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkXAudio.cpp890
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkXAudio.h131
-rw-r--r--xbmc/cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.cpp128
-rw-r--r--xbmc/cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.h34
-rw-r--r--xbmc/cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.cpp168
-rw-r--r--xbmc/cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.h54
-rw-r--r--xbmc/cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.cpp92
-rw-r--r--xbmc/cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h17
-rw-r--r--xbmc/cores/AudioEngine/Sinks/osx/AEDeviceEnumerationOSX.cpp837
-rw-r--r--xbmc/cores/AudioEngine/Sinks/osx/AEDeviceEnumerationOSX.h246
-rw-r--r--xbmc/cores/AudioEngine/Sinks/osx/CoreAudioChannelLayout.cpp273
-rw-r--r--xbmc/cores/AudioEngine/Sinks/osx/CoreAudioChannelLayout.h75
-rw-r--r--xbmc/cores/AudioEngine/Sinks/osx/CoreAudioDevice.cpp938
-rw-r--r--xbmc/cores/AudioEngine/Sinks/osx/CoreAudioDevice.h96
-rw-r--r--xbmc/cores/AudioEngine/Sinks/osx/CoreAudioHardware.cpp286
-rw-r--r--xbmc/cores/AudioEngine/Sinks/osx/CoreAudioHardware.h28
-rw-r--r--xbmc/cores/AudioEngine/Sinks/osx/CoreAudioStream.cpp498
-rw-r--r--xbmc/cores/AudioEngine/Sinks/osx/CoreAudioStream.h57
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/AESinkPipewire.cpp492
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/AESinkPipewire.h48
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/Pipewire.cpp88
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/Pipewire.h77
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireContext.cpp34
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireContext.h42
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireCore.cpp70
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireCore.h56
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireNode.cpp208
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireNode.h80
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireProxy.cpp78
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireProxy.h53
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireRegistry.cpp111
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireRegistry.h57
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireStream.cpp161
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireStream.h72
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.cpp67
-rw-r--r--xbmc/cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.h51
-rw-r--r--xbmc/cores/AudioEngine/Sinks/test/CMakeLists.txt7
-rw-r--r--xbmc/cores/AudioEngine/Sinks/test/TestAESinkDARWINOSX.cpp384
-rw-r--r--xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin.cpp174
-rw-r--r--xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin.h218
-rw-r--r--xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin10.cpp245
-rw-r--r--xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin32.cpp313
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEAudioFormat.h69
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEBitstreamPacker.cpp439
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEBitstreamPacker.h66
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEChannelData.h111
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEChannelInfo.cpp385
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEChannelInfo.h55
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEDeviceInfo.cpp66
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEDeviceInfo.h52
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEELDParser.cpp198
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEELDParser.h20
-rw-r--r--xbmc/cores/AudioEngine/Utils/AELimiter.cpp77
-rw-r--r--xbmc/cores/AudioEngine/Utils/AELimiter.h43
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEPackIEC61937.cpp235
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEPackIEC61937.h76
-rw-r--r--xbmc/cores/AudioEngine/Utils/AERingBuffer.h284
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEStreamData.h19
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEStreamInfo.cpp865
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEStreamInfo.h102
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEUtil.cpp583
-rw-r--r--xbmc/cores/AudioEngine/Utils/AEUtil.h176
-rw-r--r--xbmc/cores/CMakeLists.txt14
-rw-r--r--xbmc/cores/DataCacheCore.cpp493
-rw-r--r--xbmc/cores/DataCacheCore.h335
-rw-r--r--xbmc/cores/DllLoader/CMakeLists.txt38
-rw-r--r--xbmc/cores/DllLoader/DllLoader-linux.cpp69
-rw-r--r--xbmc/cores/DllLoader/DllLoader.cpp808
-rw-r--r--xbmc/cores/DllLoader/DllLoader.h113
-rw-r--r--xbmc/cores/DllLoader/DllLoaderContainer.cpp344
-rw-r--r--xbmc/cores/DllLoader/DllLoaderContainer.h37
-rw-r--r--xbmc/cores/DllLoader/LibraryLoader.cpp62
-rw-r--r--xbmc/cores/DllLoader/LibraryLoader.h46
-rw-r--r--xbmc/cores/DllLoader/SoLoader.cpp101
-rw-r--r--xbmc/cores/DllLoader/SoLoader.h35
-rw-r--r--xbmc/cores/DllLoader/Win32DllLoader.cpp408
-rw-r--r--xbmc/cores/DllLoader/Win32DllLoader.h49
-rw-r--r--xbmc/cores/DllLoader/coff.cpp997
-rw-r--r--xbmc/cores/DllLoader/coff.h491
-rw-r--r--xbmc/cores/DllLoader/coffldr.h79
-rw-r--r--xbmc/cores/DllLoader/dll.cpp252
-rw-r--r--xbmc/cores/DllLoader/dll.h20
-rw-r--r--xbmc/cores/DllLoader/dll_tracker.cpp145
-rw-r--r--xbmc/cores/DllLoader/dll_tracker.h132
-rw-r--r--xbmc/cores/DllLoader/dll_tracker_file.cpp134
-rw-r--r--xbmc/cores/DllLoader/dll_tracker_file.h27
-rw-r--r--xbmc/cores/DllLoader/dll_tracker_library.cpp122
-rw-r--r--xbmc/cores/DllLoader/dll_tracker_library.h17
-rw-r--r--xbmc/cores/DllLoader/dll_util.cpp119
-rw-r--r--xbmc/cores/DllLoader/dll_util.h24
-rw-r--r--xbmc/cores/DllLoader/exports/CMakeLists.txt33
-rw-r--r--xbmc/cores/DllLoader/exports/emu_dummy.cpp20
-rw-r--r--xbmc/cores/DllLoader/exports/emu_dummy.h22
-rw-r--r--xbmc/cores/DllLoader/exports/emu_msvcrt.cpp2074
-rw-r--r--xbmc/cores/DllLoader/exports/emu_msvcrt.h167
-rwxr-xr-xxbmc/cores/DllLoader/exports/kernel32.def25
-rwxr-xr-xxbmc/cores/DllLoader/exports/msvcrt.def123
-rwxr-xr-xxbmc/cores/DllLoader/exports/pncrt.def114
-rw-r--r--xbmc/cores/DllLoader/exports/util/CMakeLists.txt5
-rw-r--r--xbmc/cores/DllLoader/exports/util/EmuFileWrapper.cpp232
-rw-r--r--xbmc/cores/DllLoader/exports/util/EmuFileWrapper.h73
-rwxr-xr-xxbmc/cores/DllLoader/exports/winMM.def6
-rw-r--r--xbmc/cores/DllLoader/exports/wrapper.c513
-rw-r--r--xbmc/cores/DllLoader/exports/wrapper_mach_alias62
-rwxr-xr-xxbmc/cores/DllLoader/exports/ws2_32.def25
-rw-r--r--xbmc/cores/DllLoader/ldt_keeper.c282
-rw-r--r--xbmc/cores/DllLoader/ldt_keeper.h38
-rw-r--r--xbmc/cores/DllLoader/mmap_anon.c75
-rw-r--r--xbmc/cores/DllLoader/mmap_anon.h14
-rw-r--r--xbmc/cores/EdlEdit.h31
-rw-r--r--xbmc/cores/ExternalPlayer/CMakeLists.txt5
-rw-r--r--xbmc/cores/ExternalPlayer/ExternalPlayer.cpp689
-rw-r--r--xbmc/cores/ExternalPlayer/ExternalPlayer.h89
-rw-r--r--xbmc/cores/FFmpeg.cpp119
-rw-r--r--xbmc/cores/FFmpeg.h53
-rw-r--r--xbmc/cores/GameSettings.h67
-rw-r--r--xbmc/cores/IPlayer.h277
-rw-r--r--xbmc/cores/IPlayerCallback.h37
-rw-r--r--xbmc/cores/MenuType.h22
-rw-r--r--xbmc/cores/RetroPlayer/CMakeLists.txt14
-rw-r--r--xbmc/cores/RetroPlayer/RetroPlayer.cpp698
-rw-r--r--xbmc/cores/RetroPlayer/RetroPlayer.h145
-rw-r--r--xbmc/cores/RetroPlayer/RetroPlayerAutoSave.cpp63
-rw-r--r--xbmc/cores/RetroPlayer/RetroPlayerAutoSave.h51
-rw-r--r--xbmc/cores/RetroPlayer/RetroPlayerDefines.h12
-rw-r--r--xbmc/cores/RetroPlayer/RetroPlayerInput.cpp73
-rw-r--r--xbmc/cores/RetroPlayer/RetroPlayerInput.h60
-rw-r--r--xbmc/cores/RetroPlayer/RetroPlayerTypes.h35
-rw-r--r--xbmc/cores/RetroPlayer/RetroPlayerUtils.cpp45
-rw-r--r--xbmc/cores/RetroPlayer/RetroPlayerUtils.h44
-rw-r--r--xbmc/cores/RetroPlayer/audio/AudioTranslator.cpp74
-rw-r--r--xbmc/cores/RetroPlayer/audio/AudioTranslator.h36
-rw-r--r--xbmc/cores/RetroPlayer/audio/CMakeLists.txt7
-rw-r--r--xbmc/cores/RetroPlayer/buffers/BaseRenderBuffer.cpp39
-rw-r--r--xbmc/cores/RetroPlayer/buffers/BaseRenderBuffer.h40
-rw-r--r--xbmc/cores/RetroPlayer/buffers/BaseRenderBufferPool.cpp156
-rw-r--r--xbmc/cores/RetroPlayer/buffers/BaseRenderBufferPool.h66
-rw-r--r--xbmc/cores/RetroPlayer/buffers/CMakeLists.txt34
-rw-r--r--xbmc/cores/RetroPlayer/buffers/IRenderBuffer.h70
-rw-r--r--xbmc/cores/RetroPlayer/buffers/IRenderBufferPool.h70
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferDMA.cpp125
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferDMA.h67
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferManager.cpp104
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferManager.h51
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGL.cpp76
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGL.h44
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGLES.cpp101
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGLES.h51
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferPoolDMA.cpp60
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferPoolDMA.h45
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGL.cpp64
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGL.h45
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGLES.cpp76
-rw-r--r--xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGLES.h47
-rw-r--r--xbmc/cores/RetroPlayer/buffers/video/CMakeLists.txt9
-rw-r--r--xbmc/cores/RetroPlayer/buffers/video/RenderBufferGuiTexture.cpp105
-rw-r--r--xbmc/cores/RetroPlayer/buffers/video/RenderBufferGuiTexture.h50
-rw-r--r--xbmc/cores/RetroPlayer/buffers/video/RenderBufferSysMem.cpp53
-rw-r--r--xbmc/cores/RetroPlayer/buffers/video/RenderBufferSysMem.h45
-rw-r--r--xbmc/cores/RetroPlayer/cheevos/CMakeLists.txt6
-rw-r--r--xbmc/cores/RetroPlayer/cheevos/Cheevos.cpp183
-rw-r--r--xbmc/cores/RetroPlayer/cheevos/Cheevos.h90
-rw-r--r--xbmc/cores/RetroPlayer/cheevos/RConsoleIDs.h85
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/CMakeLists.txt24
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIGameMessenger.cpp50
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIGameMessenger.h42
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIGameRenderManager.cpp310
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIGameRenderManager.h185
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIGameSettings.cpp70
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIGameSettings.h54
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIGameSettingsHandle.cpp59
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h91
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIGameVideoHandle.cpp39
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIGameVideoHandle.h34
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIRenderHandle.cpp62
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIRenderHandle.h76
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIRenderTarget.cpp66
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIRenderTarget.h94
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIRenderTargetFactory.cpp29
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/GUIRenderTargetFactory.h40
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/IGUIRenderSettings.h64
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/IGameCallback.h78
-rw-r--r--xbmc/cores/RetroPlayer/guibridge/IRenderCallback.h26
-rw-r--r--xbmc/cores/RetroPlayer/guicontrols/CMakeLists.txt9
-rw-r--r--xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.cpp190
-rw-r--r--xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.dox45
-rw-r--r--xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.h82
-rw-r--r--xbmc/cores/RetroPlayer/guicontrols/GUIRenderSettings.cpp103
-rw-r--r--xbmc/cores/RetroPlayer/guicontrols/GUIRenderSettings.h58
-rw-r--r--xbmc/cores/RetroPlayer/guiplayback/CMakeLists.txt7
-rw-r--r--xbmc/cores/RetroPlayer/guiplayback/GUIPlaybackControl.cpp155
-rw-r--r--xbmc/cores/RetroPlayer/guiplayback/GUIPlaybackControl.h52
-rw-r--r--xbmc/cores/RetroPlayer/guiwindows/CMakeLists.txt9
-rw-r--r--xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreen.cpp236
-rw-r--r--xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreen.h60
-rw-r--r--xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreenText.cpp128
-rw-r--r--xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreenText.h84
-rw-r--r--xbmc/cores/RetroPlayer/messages/CMakeLists.txt25
-rw-r--r--xbmc/cores/RetroPlayer/messages/savestate.fbs64
-rw-r--r--xbmc/cores/RetroPlayer/messages/video.fbs43
-rw-r--r--xbmc/cores/RetroPlayer/playback/CMakeLists.txt10
-rw-r--r--xbmc/cores/RetroPlayer/playback/GameLoop.cpp135
-rw-r--r--xbmc/cores/RetroPlayer/playback/GameLoop.h69
-rw-r--r--xbmc/cores/RetroPlayer/playback/IPlayback.h46
-rw-r--r--xbmc/cores/RetroPlayer/playback/IPlaybackControl.h52
-rw-r--r--xbmc/cores/RetroPlayer/playback/RealtimePlayback.h41
-rw-r--r--xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp443
-rw-r--r--xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h109
-rw-r--r--xbmc/cores/RetroPlayer/process/CMakeLists.txt7
-rw-r--r--xbmc/cores/RetroPlayer/process/RPProcessInfo.cpp232
-rw-r--r--xbmc/cores/RetroPlayer/process/RPProcessInfo.h201
-rw-r--r--xbmc/cores/RetroPlayer/process/X11/CMakeLists.txt5
-rw-r--r--xbmc/cores/RetroPlayer/process/X11/RPProcessInfoX11.cpp26
-rw-r--r--xbmc/cores/RetroPlayer/process/X11/RPProcessInfoX11.h26
-rw-r--r--xbmc/cores/RetroPlayer/process/android/CMakeLists.txt4
-rw-r--r--xbmc/cores/RetroPlayer/process/android/RPProcessInfoAndroid.cpp26
-rw-r--r--xbmc/cores/RetroPlayer/process/android/RPProcessInfoAndroid.h26
-rw-r--r--xbmc/cores/RetroPlayer/process/gbm/CMakeLists.txt4
-rw-r--r--xbmc/cores/RetroPlayer/process/gbm/RPProcessInfoGbm.cpp26
-rw-r--r--xbmc/cores/RetroPlayer/process/gbm/RPProcessInfoGbm.h26
-rw-r--r--xbmc/cores/RetroPlayer/process/ios/CMakeLists.txt5
-rw-r--r--xbmc/cores/RetroPlayer/process/ios/RPProcessInfoIOS.cpp26
-rw-r--r--xbmc/cores/RetroPlayer/process/ios/RPProcessInfoIOS.h28
-rw-r--r--xbmc/cores/RetroPlayer/process/osx/CMakeLists.txt5
-rw-r--r--xbmc/cores/RetroPlayer/process/osx/RPProcessInfoOSX.cpp26
-rw-r--r--xbmc/cores/RetroPlayer/process/osx/RPProcessInfoOSX.h26
-rw-r--r--xbmc/cores/RetroPlayer/process/wayland/CMakeLists.txt5
-rw-r--r--xbmc/cores/RetroPlayer/process/wayland/RPProcessInfoWayland.cpp26
-rw-r--r--xbmc/cores/RetroPlayer/process/wayland/RPProcessInfoWayland.h26
-rw-r--r--xbmc/cores/RetroPlayer/process/windows/CMakeLists.txt4
-rw-r--r--xbmc/cores/RetroPlayer/process/windows/RPProcessInfoWin.cpp28
-rw-r--r--xbmc/cores/RetroPlayer/process/windows/RPProcessInfoWin.h26
-rw-r--r--xbmc/cores/RetroPlayer/rendering/CMakeLists.txt18
-rw-r--r--xbmc/cores/RetroPlayer/rendering/IRenderManager.h56
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RPRenderManager.cpp1066
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RPRenderManager.h271
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RenderContext.cpp321
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RenderContext.h122
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RenderSettings.cpp32
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RenderSettings.h34
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RenderTranslator.cpp77
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RenderTranslator.h43
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RenderUtils.cpp222
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RenderUtils.h43
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RenderVideoSettings.cpp89
-rw-r--r--xbmc/cores/RetroPlayer/rendering/RenderVideoSettings.h60
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/CMakeLists.txt26
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPBaseRenderer.cpp238
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPBaseRenderer.h118
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.cpp134
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h53
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererGuiTexture.cpp286
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererGuiTexture.h66
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.cpp337
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h92
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.cpp317
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h81
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPWinRenderer.cpp305
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPWinRenderer.h132
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoShaders/CMakeLists.txt3
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/CMakeLists.txt5
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/RPWinOutputShader.cpp120
-rw-r--r--xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/RPWinOutputShader.h61
-rw-r--r--xbmc/cores/RetroPlayer/savestates/CMakeLists.txt19
-rw-r--r--xbmc/cores/RetroPlayer/savestates/ISavestate.h214
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp272
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h61
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.cpp637
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.h127
-rw-r--r--xbmc/cores/RetroPlayer/savestates/SavestateTypes.h28
-rw-r--r--xbmc/cores/RetroPlayer/streams/CMakeLists.txt15
-rw-r--r--xbmc/cores/RetroPlayer/streams/IRetroPlayerStream.h67
-rw-r--r--xbmc/cores/RetroPlayer/streams/IStreamManager.h41
-rw-r--r--xbmc/cores/RetroPlayer/streams/RPStreamManager.cpp65
-rw-r--r--xbmc/cores/RetroPlayer/streams/RPStreamManager.h42
-rw-r--r--xbmc/cores/RetroPlayer/streams/RetroPlayerAudio.cpp142
-rw-r--r--xbmc/cores/RetroPlayer/streams/RetroPlayerAudio.h67
-rw-r--r--xbmc/cores/RetroPlayer/streams/RetroPlayerStreamTypes.cpp19
-rw-r--r--xbmc/cores/RetroPlayer/streams/RetroPlayerStreamTypes.h86
-rw-r--r--xbmc/cores/RetroPlayer/streams/RetroPlayerVideo.cpp116
-rw-r--r--xbmc/cores/RetroPlayer/streams/RetroPlayerVideo.h112
-rw-r--r--xbmc/cores/RetroPlayer/streams/memory/BasicMemoryStream.cpp55
-rw-r--r--xbmc/cores/RetroPlayer/streams/memory/BasicMemoryStream.h48
-rw-r--r--xbmc/cores/RetroPlayer/streams/memory/CMakeLists.txt12
-rw-r--r--xbmc/cores/RetroPlayer/streams/memory/DeltaPairMemoryStream.cpp99
-rw-r--r--xbmc/cores/RetroPlayer/streams/memory/DeltaPairMemoryStream.h67
-rw-r--r--xbmc/cores/RetroPlayer/streams/memory/IMemoryStream.h144
-rw-r--r--xbmc/cores/RetroPlayer/streams/memory/LinearMemoryStream.cpp104
-rw-r--r--xbmc/cores/RetroPlayer/streams/memory/LinearMemoryStream.h69
-rw-r--r--xbmc/cores/VideoPlayer/AudioSinkAE.cpp387
-rw-r--r--xbmc/cores/VideoPlayer/AudioSinkAE.h85
-rw-r--r--xbmc/cores/VideoPlayer/Buffers/CMakeLists.txt13
-rw-r--r--xbmc/cores/VideoPlayer/Buffers/VideoBuffer.cpp466
-rw-r--r--xbmc/cores/VideoPlayer/Buffers/VideoBuffer.h197
-rw-r--r--xbmc/cores/VideoPlayer/Buffers/VideoBufferDMA.cpp187
-rw-r--r--xbmc/cores/VideoPlayer/Buffers/VideoBufferDMA.h61
-rw-r--r--xbmc/cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.cpp159
-rw-r--r--xbmc/cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.h100
-rw-r--r--xbmc/cores/VideoPlayer/Buffers/VideoBufferPoolDMA.cpp129
-rw-r--r--xbmc/cores/VideoPlayer/Buffers/VideoBufferPoolDMA.h44
-rw-r--r--xbmc/cores/VideoPlayer/CMakeLists.txt46
-rw-r--r--xbmc/cores/VideoPlayer/DVDClock.cpp309
-rw-r--r--xbmc/cores/VideoPlayer/DVDClock.h85
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Audio/CMakeLists.txt13
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodec.h123
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecAndroidMediaCodec.cpp807
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecAndroidMediaCodec.h75
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecFFmpeg.cpp402
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecFFmpeg.h64
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecPassthrough.cpp261
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecPassthrough.h56
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/CMakeLists.txt8
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecUtils.cpp77
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecUtils.h18
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecs.h27
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.cpp289
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h80
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/CMakeLists.txt27
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlay.h176
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodec.cpp40
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodec.h99
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecCCText.cpp165
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecCCText.h38
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecFFmpeg.cpp300
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecFFmpeg.h42
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecSSA.cpp141
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecSSA.h33
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecTX3G.cpp295
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecTX3G.h33
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecText.cpp150
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecText.h43
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayImage.h86
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayLibass.h35
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySSA.h28
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySpu.h95
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayText.h30
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/OverlayCodecWebVTT.cpp168
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/OverlayCodecWebVTT.h38
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder.c874
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder.h107
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder708.cpp1192
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder708.h304
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/AddonVideoCodec.cpp387
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/AddonVideoCodec.h54
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/CMakeLists.txt44
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.cpp83
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h267
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.cpp1745
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.h194
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.cpp647
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.h48
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecFFmpeg.cpp1380
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecFFmpeg.h117
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoPPFFmpeg.cpp130
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoPPFFmpeg.h42
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DXVA.cpp1573
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/DXVA.h262
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/VAAPI.cpp3242
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/VAAPI.h648
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/VDPAU.cpp3464
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/VDPAU.h661
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/VTB.cpp232
-rw-r--r--xbmc/cores/VideoPlayer/DVDCodecs/Video/VTB.h69
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxSPU.cpp660
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxSPU.h54
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/CMakeLists.txt23
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemux.cpp96
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemux.h381
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxBXA.cpp186
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxBXA.h69
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCC.cpp414
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCC.h58
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCDDA.cpp191
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCDDA.h48
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxClient.cpp764
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxClient.h66
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxFFmpeg.cpp2549
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxFFmpeg.h181
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.cpp111
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.h24
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxVobsub.cpp269
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxVobsub.h88
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDFactoryDemuxer.cpp80
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DVDFactoryDemuxer.h21
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DemuxMultiSource.cpp236
-rw-r--r--xbmc/cores/VideoPlayer/DVDDemuxers/DemuxMultiSource.h64
-rw-r--r--xbmc/cores/VideoPlayer/DVDFileInfo.cpp497
-rw-r--r--xbmc/cores/VideoPlayer/DVDFileInfo.h52
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/BlurayStateSerializer.cpp84
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/BlurayStateSerializer.h47
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/CMakeLists.txt38
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.cpp189
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.h24
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.cpp47
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.h215
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.cpp1307
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.h192
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFFmpeg.cpp127
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFFmpeg.h40
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFile.cpp162
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFile.h33
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamMemory.cpp96
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamMemory.h29
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.cpp1577
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.h194
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.cpp169
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.h46
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDStateSerializer.cpp127
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DVDStateSerializer.h67
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/DllDvdNav.h275
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamAddon.cpp777
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamAddon.h169
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiSource.cpp141
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiSource.h42
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiStreams.h31
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRBase.cpp369
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRBase.h83
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRChannel.cpp125
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRChannel.h33
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRRecording.cpp102
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRRecording.h28
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/config.h76
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvd_reader.h370
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvd_types.h282
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvdnav.h789
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvdnav_events.h226
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/ifo_types.h754
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/nav_types.h258
-rw-r--r--xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/version.h29
-rw-r--r--xbmc/cores/VideoPlayer/DVDMessage.cpp110
-rw-r--r--xbmc/cores/VideoPlayer/DVDMessage.h333
-rw-r--r--xbmc/cores/VideoPlayer/DVDMessageQueue.cpp351
-rw-r--r--xbmc/cores/VideoPlayer/DVDMessageQueue.h116
-rw-r--r--xbmc/cores/VideoPlayer/DVDOverlayContainer.cpp195
-rw-r--r--xbmc/cores/VideoPlayer/DVDOverlayContainer.h60
-rw-r--r--xbmc/cores/VideoPlayer/DVDResource.h37
-rw-r--r--xbmc/cores/VideoPlayer/DVDStreamInfo.cpp310
-rw-r--r--xbmc/cores/VideoPlayer/DVDStreamInfo.h123
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/CMakeLists.txt33
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDFactorySubtitle.cpp82
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDFactorySubtitle.h25
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleLineCollection.cpp111
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleLineCollection.h48
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParser.h88
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMPL2.cpp72
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMPL2.h27
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMicroDVD.cpp79
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMicroDVD.h28
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSSA.cpp57
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSSA.h27
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSami.cpp157
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSami.h26
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSubrip.cpp94
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSubrip.h25
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserVplayer.cpp89
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserVplayer.h28
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleStream.cpp138
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleStream.h65
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagMicroDVD.cpp165
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagMicroDVD.h33
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.cpp308
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.h65
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitlesLibass.cpp742
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitlesLibass.h169
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/SubtitleParserWebVTT.cpp84
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/SubtitleParserWebVTT.h24
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesAdapter.cpp96
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesAdapter.h91
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesStyle.h134
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/CMakeLists.txt7
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTHandler.cpp1166
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTHandler.h235
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTISOHandler.cpp150
-rw-r--r--xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTISOHandler.h40
-rw-r--r--xbmc/cores/VideoPlayer/Edl.cpp1040
-rw-r--r--xbmc/cores/VideoPlayer/Edl.h210
-rw-r--r--xbmc/cores/VideoPlayer/IVideoPlayer.h126
-rw-r--r--xbmc/cores/VideoPlayer/Interface/DemuxCrypto.h69
-rw-r--r--xbmc/cores/VideoPlayer/Interface/DemuxPacket.h50
-rw-r--r--xbmc/cores/VideoPlayer/Interface/InputStreamConstants.h11
-rw-r--r--xbmc/cores/VideoPlayer/Interface/StreamInfo.h83
-rw-r--r--xbmc/cores/VideoPlayer/Interface/TimingConstants.h24
-rw-r--r--xbmc/cores/VideoPlayer/PTSTracker.cpp323
-rw-r--r--xbmc/cores/VideoPlayer/PTSTracker.h59
-rw-r--r--xbmc/cores/VideoPlayer/Process/CMakeLists.txt4
-rw-r--r--xbmc/cores/VideoPlayer/Process/ProcessInfo.cpp696
-rw-r--r--xbmc/cores/VideoPlayer/Process/ProcessInfo.h180
-rw-r--r--xbmc/cores/VideoPlayer/Process/X11/CMakeLists.txt5
-rw-r--r--xbmc/cores/VideoPlayer/Process/X11/ProcessInfoX11.cpp59
-rw-r--r--xbmc/cores/VideoPlayer/Process/X11/ProcessInfoX11.h26
-rw-r--r--xbmc/cores/VideoPlayer/Process/android/CMakeLists.txt5
-rw-r--r--xbmc/cores/VideoPlayer/Process/android/ProcessInfoAndroid.cpp41
-rw-r--r--xbmc/cores/VideoPlayer/Process/android/ProcessInfoAndroid.h27
-rw-r--r--xbmc/cores/VideoPlayer/Process/gbm/CMakeLists.txt4
-rw-r--r--xbmc/cores/VideoPlayer/Process/gbm/ProcessInfoGBM.cpp37
-rw-r--r--xbmc/cores/VideoPlayer/Process/gbm/ProcessInfoGBM.h27
-rw-r--r--xbmc/cores/VideoPlayer/Process/ios/CMakeLists.txt5
-rw-r--r--xbmc/cores/VideoPlayer/Process/ios/ProcessInfoIOS.h25
-rw-r--r--xbmc/cores/VideoPlayer/Process/ios/ProcessInfoIos.cpp43
-rw-r--r--xbmc/cores/VideoPlayer/Process/osx/CMakeLists.txt5
-rw-r--r--xbmc/cores/VideoPlayer/Process/osx/ProcessInfoOSX.cpp58
-rw-r--r--xbmc/cores/VideoPlayer/Process/osx/ProcessInfoOSX.h27
-rw-r--r--xbmc/cores/VideoPlayer/Process/wayland/CMakeLists.txt5
-rw-r--r--xbmc/cores/VideoPlayer/Process/wayland/ProcessInfoWayland.cpp71
-rw-r--r--xbmc/cores/VideoPlayer/Process/wayland/ProcessInfoWayland.h27
-rw-r--r--xbmc/cores/VideoPlayer/Process/windows/CMakeLists.txt9
-rw-r--r--xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin.cpp41
-rw-r--r--xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin.h27
-rw-r--r--xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin10.cpp43
-rw-r--r--xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin10.h27
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayer.cpp5364
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayer.h567
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerAudio.cpp713
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerAudio.h123
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerAudioID3.cpp351
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerAudioID3.h58
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerRadioRDS.cpp1678
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerRadioRDS.h144
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerSubtitle.cpp215
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerSubtitle.h69
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerTeletext.cpp760
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerTeletext.h59
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerVideo.cpp1166
-rw-r--r--xbmc/cores/VideoPlayer/VideoPlayerVideo.h144
-rw-r--r--xbmc/cores/VideoPlayer/VideoReferenceClock.cpp280
-rw-r--r--xbmc/cores/VideoPlayer/VideoReferenceClock.h57
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/BaseRenderer.cpp522
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/BaseRenderer.h144
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/CMakeLists.txt58
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/ColorManager.cpp618
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/ColorManager.h190
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/DebugInfo.h33
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/DebugRenderer.cpp152
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/DebugRenderer.h53
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/FrameBufferObject.cpp127
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/FrameBufferObject.h76
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/CMakeLists.txt69
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DRMPRIMEEGL.cpp133
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DRMPRIMEEGL.h38
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DXVAHD.cpp631
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DXVAHD.h92
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIME.cpp264
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIME.h61
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIMEGLES.cpp417
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIMEGLES.h70
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodec.cpp243
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodec.h42
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodecSurface.cpp194
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodecSurface.h58
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.cpp314
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.h58
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.cpp266
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.h65
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVDPAU.cpp464
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVDPAU.h62
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.cpp236
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.h37
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.cpp268
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.h50
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VaapiEGL.cpp691
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VaapiEGL.h130
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VdpauGL.cpp222
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VdpauGL.h81
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VideoLayerBridgeDRMPRIME.cpp209
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VideoLayerBridgeDRMPRIME.h53
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.cpp2757
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h233
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.cpp1782
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h216
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/OverlayRenderer.cpp638
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/OverlayRenderer.h198
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererDX.cpp353
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererDX.h56
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererGL.cpp619
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererGL.h66
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererUtil.cpp290
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererUtil.h50
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderCapture.cpp24
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderCapture.h102
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureDX.cpp191
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureDX.h39
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGL.cpp186
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGL.h32
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGLES.cpp29
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGLES.h23
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderFactory.cpp57
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderFactory.h36
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderFlags.cpp89
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderFlags.h68
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderInfo.h40
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderManager.cpp1309
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/RenderManager.h247
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/CMakeLists.txt41
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConversionMatrix.cpp645
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConversionMatrix.h240
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConvolutionKernels.cpp298
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConvolutionKernels.h47
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/GLSLOutput.cpp172
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/GLSLOutput.h61
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ShaderFormats.h62
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGL.cpp266
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGL.h118
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGLES.cpp212
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGLES.h102
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/WinVideoFilter.cpp1228
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/WinVideoFilter.h240
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.cpp395
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.h164
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGLES.cpp295
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGLES.h138
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/dither.h74
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/test/CMakeLists.txt3
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/test/TestConversionMatrix.cpp183
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/WinRenderer.cpp353
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/WinRenderer.h62
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/windows/CMakeLists.txt13
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererBase.cpp726
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererBase.h191
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererDXVA.cpp383
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererDXVA.h64
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererHQ.cpp160
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererHQ.h33
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererShaders.cpp474
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererShaders.h71
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererSoftware.cpp204
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererSoftware.h54
-rw-r--r--xbmc/cores/VideoPlayer/test/edl/CMakeLists.txt3
-rw-r--r--xbmc/cores/VideoPlayer/test/edl/TestEdl.cpp461
-rw-r--r--xbmc/cores/VideoPlayer/test/edl/testdata/comskipversion1.txt5
-rw-r--r--xbmc/cores/VideoPlayer/test/edl/testdata/comskipversion2.txt5
-rw-r--r--xbmc/cores/VideoPlayer/test/edl/testdata/edlautowindautowait.edl7
-rw-r--r--xbmc/cores/VideoPlayer/test/edl/testdata/mplayerframebased.edl5
-rw-r--r--xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebased.edl6
-rw-r--r--xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebasedinterleavedcuts.edl4
-rw-r--r--xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebasedmixed.edl5
-rw-r--r--xbmc/cores/VideoPlayer/test/edl/testdata/snapstream.mkv.chapters.xml14
-rw-r--r--xbmc/cores/VideoPlayer/test/edl/testdata/videoredo.Vprj13
-rw-r--r--xbmc/cores/VideoSettings.cpp149
-rw-r--r--xbmc/cores/VideoSettings.h264
-rw-r--r--xbmc/cores/paplayer/AudioDecoder.cpp401
-rw-r--r--xbmc/cores/paplayer/AudioDecoder.h91
-rw-r--r--xbmc/cores/paplayer/CMakeLists.txt13
-rw-r--r--xbmc/cores/paplayer/CachingCodec.h18
-rw-r--r--xbmc/cores/paplayer/CodecFactory.cpp112
-rw-r--r--xbmc/cores/paplayer/CodecFactory.h23
-rw-r--r--xbmc/cores/paplayer/ICodec.h82
-rw-r--r--xbmc/cores/paplayer/PAPlayer.cpp1207
-rw-r--r--xbmc/cores/paplayer/PAPlayer.h154
-rw-r--r--xbmc/cores/paplayer/VideoPlayerCodec.cpp533
-rw-r--r--xbmc/cores/paplayer/VideoPlayerCodec.h65
-rw-r--r--xbmc/cores/playercorefactory/CMakeLists.txt9
-rw-r--r--xbmc/cores/playercorefactory/PlayerCoreConfig.cpp83
-rw-r--r--xbmc/cores/playercorefactory/PlayerCoreConfig.h56
-rw-r--r--xbmc/cores/playercorefactory/PlayerCoreFactory.cpp477
-rw-r--r--xbmc/cores/playercorefactory/PlayerCoreFactory.h69
-rw-r--r--xbmc/cores/playercorefactory/PlayerSelectionRule.cpp198
-rw-r--r--xbmc/cores/playercorefactory/PlayerSelectionRule.h62
-rw-r--r--xbmc/dbwrappers/CMakeLists.txt18
-rw-r--r--xbmc/dbwrappers/Database.cpp858
-rw-r--r--xbmc/dbwrappers/Database.h321
-rw-r--r--xbmc/dbwrappers/DatabaseQuery.cpp571
-rw-r--r--xbmc/dbwrappers/DatabaseQuery.h150
-rw-r--r--xbmc/dbwrappers/dataset.cpp625
-rw-r--r--xbmc/dbwrappers/dataset.h470
-rw-r--r--xbmc/dbwrappers/mysqldataset.cpp2099
-rw-r--r--xbmc/dbwrappers/mysqldataset.h167
-rw-r--r--xbmc/dbwrappers/qry_dat.cpp902
-rw-r--r--xbmc/dbwrappers/qry_dat.h265
-rw-r--r--xbmc/dbwrappers/sqlitedataset.cpp1063
-rw-r--r--xbmc/dbwrappers/sqlitedataset.h161
-rw-r--r--xbmc/dialogs/CMakeLists.txt69
-rw-r--r--xbmc/dialogs/GUIDialogBoxBase.cpp187
-rw-r--r--xbmc/dialogs/GUIDialogBoxBase.h61
-rw-r--r--xbmc/dialogs/GUIDialogBusy.cpp147
-rw-r--r--xbmc/dialogs/GUIDialogBusy.h51
-rw-r--r--xbmc/dialogs/GUIDialogBusyNoCancel.cpp47
-rw-r--r--xbmc/dialogs/GUIDialogBusyNoCancel.h24
-rw-r--r--xbmc/dialogs/GUIDialogButtonMenu.cpp46
-rw-r--r--xbmc/dialogs/GUIDialogButtonMenu.h21
-rw-r--r--xbmc/dialogs/GUIDialogCache.cpp178
-rw-r--r--xbmc/dialogs/GUIDialogCache.h48
-rw-r--r--xbmc/dialogs/GUIDialogColorPicker.cpp260
-rw-r--r--xbmc/dialogs/GUIDialogColorPicker.h64
-rw-r--r--xbmc/dialogs/GUIDialogContextMenu.cpp705
-rw-r--r--xbmc/dialogs/GUIDialogContextMenu.h152
-rw-r--r--xbmc/dialogs/GUIDialogExtendedProgressBar.cpp155
-rw-r--r--xbmc/dialogs/GUIDialogExtendedProgressBar.h63
-rw-r--r--xbmc/dialogs/GUIDialogFileBrowser.cpp1020
-rw-r--r--xbmc/dialogs/GUIDialogFileBrowser.h88
-rw-r--r--xbmc/dialogs/GUIDialogGamepad.cpp326
-rw-r--r--xbmc/dialogs/GUIDialogGamepad.h35
-rw-r--r--xbmc/dialogs/GUIDialogKaiToast.cpp186
-rw-r--r--xbmc/dialogs/GUIDialogKaiToast.h58
-rw-r--r--xbmc/dialogs/GUIDialogKeyboardGeneric.cpp843
-rw-r--r--xbmc/dialogs/GUIDialogKeyboardGeneric.h95
-rw-r--r--xbmc/dialogs/GUIDialogKeyboardTouch.cpp86
-rw-r--r--xbmc/dialogs/GUIDialogKeyboardTouch.h40
-rw-r--r--xbmc/dialogs/GUIDialogMediaFilter.cpp937
-rw-r--r--xbmc/dialogs/GUIDialogMediaFilter.h92
-rw-r--r--xbmc/dialogs/GUIDialogMediaSource.cpp620
-rw-r--r--xbmc/dialogs/GUIDialogMediaSource.h56
-rw-r--r--xbmc/dialogs/GUIDialogNumeric.cpp916
-rw-r--r--xbmc/dialogs/GUIDialogNumeric.h82
-rw-r--r--xbmc/dialogs/GUIDialogOK.cpp99
-rw-r--r--xbmc/dialogs/GUIDialogOK.h43
-rw-r--r--xbmc/dialogs/GUIDialogPlayEject.cpp101
-rw-r--r--xbmc/dialogs/GUIDialogPlayEject.h27
-rw-r--r--xbmc/dialogs/GUIDialogPlayerControls.cpp19
-rw-r--r--xbmc/dialogs/GUIDialogPlayerControls.h19
-rw-r--r--xbmc/dialogs/GUIDialogPlayerProcessInfo.cpp29
-rw-r--r--xbmc/dialogs/GUIDialogPlayerProcessInfo.h20
-rw-r--r--xbmc/dialogs/GUIDialogProgress.cpp292
-rw-r--r--xbmc/dialogs/GUIDialogProgress.h80
-rw-r--r--xbmc/dialogs/GUIDialogSeekBar.cpp130
-rw-r--r--xbmc/dialogs/GUIDialogSeekBar.h28
-rw-r--r--xbmc/dialogs/GUIDialogSelect.cpp405
-rw-r--r--xbmc/dialogs/GUIDialogSelect.h76
-rw-r--r--xbmc/dialogs/GUIDialogSimpleMenu.cpp154
-rw-r--r--xbmc/dialogs/GUIDialogSimpleMenu.h28
-rw-r--r--xbmc/dialogs/GUIDialogSlider.cpp125
-rw-r--r--xbmc/dialogs/GUIDialogSlider.h57
-rw-r--r--xbmc/dialogs/GUIDialogSmartPlaylistEditor.cpp648
-rw-r--r--xbmc/dialogs/GUIDialogSmartPlaylistEditor.h64
-rw-r--r--xbmc/dialogs/GUIDialogSmartPlaylistRule.cpp575
-rw-r--r--xbmc/dialogs/GUIDialogSmartPlaylistRule.h38
-rw-r--r--xbmc/dialogs/GUIDialogSubMenu.cpp30
-rw-r--r--xbmc/dialogs/GUIDialogSubMenu.h20
-rw-r--r--xbmc/dialogs/GUIDialogTextViewer.cpp132
-rw-r--r--xbmc/dialogs/GUIDialogTextViewer.h39
-rw-r--r--xbmc/dialogs/GUIDialogVolumeBar.cpp88
-rw-r--r--xbmc/dialogs/GUIDialogVolumeBar.h34
-rw-r--r--xbmc/dialogs/GUIDialogYesNo.cpp243
-rw-r--r--xbmc/dialogs/GUIDialogYesNo.h153
-rw-r--r--xbmc/dialogs/IGUIVolumeBarCallback.h28
-rw-r--r--xbmc/events/AddonEvent.cpp48
-rw-r--r--xbmc/events/AddonEvent.h39
-rw-r--r--xbmc/events/AddonManagementEvent.cpp74
-rw-r--r--xbmc/events/AddonManagementEvent.h43
-rw-r--r--xbmc/events/BaseEvent.cpp107
-rw-r--r--xbmc/events/BaseEvent.h53
-rw-r--r--xbmc/events/CMakeLists.txt18
-rw-r--r--xbmc/events/EventLog.cpp230
-rw-r--r--xbmc/events/EventLog.h62
-rw-r--r--xbmc/events/EventLogManager.cpp28
-rw-r--r--xbmc/events/EventLogManager.h26
-rw-r--r--xbmc/events/IEvent.h47
-rw-r--r--xbmc/events/MediaLibraryEvent.cpp116
-rw-r--r--xbmc/events/MediaLibraryEvent.h32
-rw-r--r--xbmc/events/NotificationEvent.h34
-rw-r--r--xbmc/events/UniqueEvent.h32
-rw-r--r--xbmc/events/windows/CMakeLists.txt7
-rw-r--r--xbmc/events/windows/GUIViewStateEventLog.cpp35
-rw-r--r--xbmc/events/windows/GUIViewStateEventLog.h29
-rw-r--r--xbmc/events/windows/GUIWindowEventLog.cpp307
-rw-r--r--xbmc/events/windows/GUIWindowEventLog.h37
-rw-r--r--xbmc/favourites/CMakeLists.txt17
-rw-r--r--xbmc/favourites/ContextMenus.cpp80
-rw-r--r--xbmc/favourites/ContextMenus.h76
-rw-r--r--xbmc/favourites/FavouritesService.cpp273
-rw-r--r--xbmc/favourites/FavouritesService.h56
-rw-r--r--xbmc/favourites/FavouritesURL.cpp205
-rw-r--r--xbmc/favourites/FavouritesURL.h66
-rw-r--r--xbmc/favourites/FavouritesUtils.cpp124
-rw-r--r--xbmc/favourites/FavouritesUtils.h24
-rw-r--r--xbmc/favourites/GUIDialogFavourites.cpp213
-rw-r--r--xbmc/favourites/GUIDialogFavourites.h43
-rw-r--r--xbmc/favourites/GUIViewStateFavourites.cpp28
-rw-r--r--xbmc/favourites/GUIViewStateFavourites.h24
-rw-r--r--xbmc/favourites/GUIWindowFavourites.cpp189
-rw-r--r--xbmc/favourites/GUIWindowFavourites.h33
-rw-r--r--xbmc/filesystem/AddonsDirectory.cpp953
-rw-r--r--xbmc/filesystem/AddonsDirectory.h72
-rw-r--r--xbmc/filesystem/AudioBookFileDirectory.cpp168
-rw-r--r--xbmc/filesystem/AudioBookFileDirectory.h30
-rw-r--r--xbmc/filesystem/BlurayCallback.cpp159
-rw-r--r--xbmc/filesystem/BlurayCallback.h31
-rw-r--r--xbmc/filesystem/BlurayDirectory.cpp344
-rw-r--r--xbmc/filesystem/BlurayDirectory.h56
-rw-r--r--xbmc/filesystem/BlurayFile.cpp35
-rw-r--r--xbmc/filesystem/BlurayFile.h25
-rw-r--r--xbmc/filesystem/CDDADirectory.cpp71
-rw-r--r--xbmc/filesystem/CDDADirectory.h24
-rw-r--r--xbmc/filesystem/CDDAFile.cpp227
-rw-r--r--xbmc/filesystem/CDDAFile.h44
-rw-r--r--xbmc/filesystem/CMakeLists.txt177
-rw-r--r--xbmc/filesystem/CacheStrategy.cpp447
-rw-r--r--xbmc/filesystem/CacheStrategy.h133
-rw-r--r--xbmc/filesystem/CircularCache.cpp281
-rw-r--r--xbmc/filesystem/CircularCache.h55
-rw-r--r--xbmc/filesystem/CurlFile.cpp2141
-rw-r--r--xbmc/filesystem/CurlFile.h202
-rw-r--r--xbmc/filesystem/DAVCommon.cpp73
-rw-r--r--xbmc/filesystem/DAVCommon.h21
-rw-r--r--xbmc/filesystem/DAVDirectory.cpp230
-rw-r--r--xbmc/filesystem/DAVDirectory.h33
-rw-r--r--xbmc/filesystem/DAVFile.cpp145
-rw-r--r--xbmc/filesystem/DAVFile.h31
-rw-r--r--xbmc/filesystem/Directorization.h139
-rw-r--r--xbmc/filesystem/Directory.cpp446
-rw-r--r--xbmc/filesystem/Directory.h82
-rw-r--r--xbmc/filesystem/DirectoryCache.cpp266
-rw-r--r--xbmc/filesystem/DirectoryCache.h73
-rw-r--r--xbmc/filesystem/DirectoryFactory.cpp197
-rw-r--r--xbmc/filesystem/DirectoryFactory.h39
-rw-r--r--xbmc/filesystem/DirectoryHistory.cpp156
-rw-r--r--xbmc/filesystem/DirectoryHistory.h67
-rw-r--r--xbmc/filesystem/DllLibCurl.cpp306
-rw-r--r--xbmc/filesystem/DllLibCurl.h106
-rw-r--r--xbmc/filesystem/EventsDirectory.cpp69
-rw-r--r--xbmc/filesystem/EventsDirectory.h40
-rw-r--r--xbmc/filesystem/FTPDirectory.cpp113
-rw-r--r--xbmc/filesystem/FTPDirectory.h25
-rw-r--r--xbmc/filesystem/FTPParse.cpp504
-rw-r--r--xbmc/filesystem/FTPParse.h33
-rw-r--r--xbmc/filesystem/FavouritesDirectory.cpp43
-rw-r--r--xbmc/filesystem/FavouritesDirectory.h28
-rw-r--r--xbmc/filesystem/File.cpp1224
-rw-r--r--xbmc/filesystem/File.h206
-rw-r--r--xbmc/filesystem/FileCache.cpp630
-rw-r--r--xbmc/filesystem/FileCache.h79
-rw-r--r--xbmc/filesystem/FileDirectoryFactory.cpp250
-rw-r--r--xbmc/filesystem/FileDirectoryFactory.h26
-rw-r--r--xbmc/filesystem/FileFactory.cpp180
-rw-r--r--xbmc/filesystem/FileFactory.h29
-rw-r--r--xbmc/filesystem/HTTPDirectory.cpp318
-rw-r--r--xbmc/filesystem/HTTPDirectory.h26
-rw-r--r--xbmc/filesystem/IDirectory.cpp177
-rw-r--r--xbmc/filesystem/IDirectory.h175
-rw-r--r--xbmc/filesystem/IFile.cpp91
-rw-r--r--xbmc/filesystem/IFile.h146
-rw-r--r--xbmc/filesystem/IFileDirectory.h22
-rw-r--r--xbmc/filesystem/IFileTypes.h108
-rw-r--r--xbmc/filesystem/ISO9660Directory.cpp81
-rw-r--r--xbmc/filesystem/ISO9660Directory.h25
-rw-r--r--xbmc/filesystem/ISO9660File.cpp127
-rw-r--r--xbmc/filesystem/ISO9660File.h49
-rw-r--r--xbmc/filesystem/ImageFile.cpp106
-rw-r--r--xbmc/filesystem/ImageFile.h34
-rw-r--r--xbmc/filesystem/LibraryDirectory.cpp182
-rw-r--r--xbmc/filesystem/LibraryDirectory.h40
-rw-r--r--xbmc/filesystem/MultiPathDirectory.cpp309
-rw-r--r--xbmc/filesystem/MultiPathDirectory.h42
-rw-r--r--xbmc/filesystem/MultiPathFile.cpp84
-rw-r--r--xbmc/filesystem/MultiPathFile.h27
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory.cpp344
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory.h37
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/CMakeLists.txt39
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.cpp284
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.h92
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.cpp52
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.h29
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.cpp65
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.h29
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.cpp33
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.h27
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.cpp65
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.h29
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.cpp33
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.h27
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.cpp63
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.h29
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.cpp33
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.h27
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.cpp55
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.h29
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.cpp57
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.h28
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.cpp61
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.h32
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.cpp87
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.h29
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.cpp22
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.h27
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.cpp32
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.h27
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.cpp37
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.h27
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.cpp33
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.h27
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.cpp57
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.h29
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/QueryParams.cpp58
-rw-r--r--xbmc/filesystem/MusicDatabaseDirectory/QueryParams.h43
-rw-r--r--xbmc/filesystem/MusicDatabaseFile.cpp90
-rw-r--r--xbmc/filesystem/MusicDatabaseFile.h35
-rw-r--r--xbmc/filesystem/MusicFileDirectory.cpp79
-rw-r--r--xbmc/filesystem/MusicFileDirectory.h33
-rw-r--r--xbmc/filesystem/MusicSearchDirectory.cpp52
-rw-r--r--xbmc/filesystem/MusicSearchDirectory.h24
-rw-r--r--xbmc/filesystem/NFSDirectory.cpp382
-rw-r--r--xbmc/filesystem/NFSDirectory.h34
-rw-r--r--xbmc/filesystem/NFSFile.cpp962
-rw-r--r--xbmc/filesystem/NFSFile.h148
-rw-r--r--xbmc/filesystem/NptXbmcFile.cpp534
-rw-r--r--xbmc/filesystem/OverrideDirectory.cpp41
-rw-r--r--xbmc/filesystem/OverrideDirectory.h28
-rw-r--r--xbmc/filesystem/OverrideFile.cpp116
-rw-r--r--xbmc/filesystem/OverrideFile.h43
-rw-r--r--xbmc/filesystem/PVRDirectory.cpp56
-rw-r--r--xbmc/filesystem/PVRDirectory.h35
-rw-r--r--xbmc/filesystem/PipeFile.cpp217
-rw-r--r--xbmc/filesystem/PipeFile.h77
-rw-r--r--xbmc/filesystem/PipesManager.cpp314
-rw-r--r--xbmc/filesystem/PipesManager.h120
-rw-r--r--xbmc/filesystem/PlaylistDirectory.cpp47
-rw-r--r--xbmc/filesystem/PlaylistDirectory.h23
-rw-r--r--xbmc/filesystem/PlaylistFileDirectory.cpp67
-rw-r--r--xbmc/filesystem/PlaylistFileDirectory.h25
-rw-r--r--xbmc/filesystem/PluginDirectory.cpp548
-rw-r--r--xbmc/filesystem/PluginDirectory.h89
-rw-r--r--xbmc/filesystem/PluginFile.cpp59
-rw-r--r--xbmc/filesystem/PluginFile.h31
-rw-r--r--xbmc/filesystem/RSSDirectory.cpp623
-rw-r--r--xbmc/filesystem/RSSDirectory.h39
-rw-r--r--xbmc/filesystem/ResourceDirectory.cpp53
-rw-r--r--xbmc/filesystem/ResourceDirectory.h26
-rw-r--r--xbmc/filesystem/ResourceFile.cpp71
-rw-r--r--xbmc/filesystem/ResourceFile.h27
-rw-r--r--xbmc/filesystem/ShoutcastFile.cpp362
-rw-r--r--xbmc/filesystem/ShoutcastFile.h76
-rw-r--r--xbmc/filesystem/SmartPlaylistDirectory.cpp359
-rw-r--r--xbmc/filesystem/SmartPlaylistDirectory.h33
-rw-r--r--xbmc/filesystem/SourcesDirectory.cpp106
-rw-r--r--xbmc/filesystem/SourcesDirectory.h30
-rw-r--r--xbmc/filesystem/SpecialProtocol.cpp304
-rw-r--r--xbmc/filesystem/SpecialProtocol.h85
-rw-r--r--xbmc/filesystem/SpecialProtocolDirectory.cpp44
-rw-r--r--xbmc/filesystem/SpecialProtocolDirectory.h25
-rw-r--r--xbmc/filesystem/SpecialProtocolFile.cpp25
-rw-r--r--xbmc/filesystem/SpecialProtocolFile.h24
-rw-r--r--xbmc/filesystem/StackDirectory.cpp232
-rw-r--r--xbmc/filesystem/StackDirectory.h33
-rw-r--r--xbmc/filesystem/UDFBlockInput.cpp73
-rw-r--r--xbmc/filesystem/UDFBlockInput.h43
-rw-r--r--xbmc/filesystem/UDFDirectory.cpp108
-rw-r--r--xbmc/filesystem/UDFDirectory.h29
-rw-r--r--xbmc/filesystem/UDFFile.cpp105
-rw-r--r--xbmc/filesystem/UDFFile.h48
-rw-r--r--xbmc/filesystem/UPnPDirectory.cpp358
-rw-r--r--xbmc/filesystem/UPnPDirectory.h37
-rw-r--r--xbmc/filesystem/UPnPFile.cpp71
-rw-r--r--xbmc/filesystem/UPnPFile.h30
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory.cpp377
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory.h36
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/CMakeLists.txt37
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.cpp271
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.h99
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.cpp52
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.h26
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.cpp127
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.h33
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.cpp49
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.h29
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.cpp80
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.h29
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.cpp75
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.h29
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.cpp106
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.h29
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.cpp36
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.h27
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.cpp36
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.h27
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.cpp37
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.h27
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.cpp22
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.h27
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.cpp81
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.h36
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.cpp44
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.h25
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.cpp43
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.h25
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.cpp56
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.h29
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.cpp74
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.h29
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/QueryParams.cpp97
-rw-r--r--xbmc/filesystem/VideoDatabaseDirectory/QueryParams.h59
-rw-r--r--xbmc/filesystem/VideoDatabaseFile.cpp99
-rw-r--r--xbmc/filesystem/VideoDatabaseFile.h31
-rw-r--r--xbmc/filesystem/VirtualDirectory.cpp204
-rw-r--r--xbmc/filesystem/VirtualDirectory.h65
-rw-r--r--xbmc/filesystem/XbtDirectory.cpp67
-rw-r--r--xbmc/filesystem/XbtDirectory.h33
-rw-r--r--xbmc/filesystem/XbtFile.cpp368
-rw-r--r--xbmc/filesystem/XbtFile.h67
-rw-r--r--xbmc/filesystem/XbtManager.cpp127
-rw-r--r--xbmc/filesystem/XbtManager.h58
-rw-r--r--xbmc/filesystem/ZeroconfDirectory.cpp232
-rw-r--r--xbmc/filesystem/ZeroconfDirectory.h28
-rw-r--r--xbmc/filesystem/ZipDirectory.cpp79
-rw-r--r--xbmc/filesystem/ZipDirectory.h24
-rw-r--r--xbmc/filesystem/ZipFile.cpp531
-rw-r--r--xbmc/filesystem/ZipFile.h61
-rw-r--r--xbmc/filesystem/ZipManager.cpp320
-rw-r--r--xbmc/filesystem/ZipManager.h82
-rw-r--r--xbmc/filesystem/test/CMakeLists.txt15
-rw-r--r--xbmc/filesystem/test/TestDirectory.cpp56
-rw-r--r--xbmc/filesystem/test/TestFile.cpp212
-rw-r--r--xbmc/filesystem/test/TestFileFactory.cpp161
-rw-r--r--xbmc/filesystem/test/TestHTTPDirectory.cpp301
-rw-r--r--xbmc/filesystem/test/TestNfsFile.cpp84
-rw-r--r--xbmc/filesystem/test/TestZipFile.cpp211
-rw-r--r--xbmc/filesystem/test/TestZipManager.cpp30
-rw-r--r--xbmc/filesystem/test/data/httpdirectory/apache-default.html15
-rw-r--r--xbmc/filesystem/test/data/httpdirectory/apache-fancy.html15
-rw-r--r--xbmc/filesystem/test/data/httpdirectory/apache-html.html19
-rw-r--r--xbmc/filesystem/test/data/httpdirectory/basic-multiline.html16
-rw-r--r--xbmc/filesystem/test/data/httpdirectory/basic.html13
-rw-r--r--xbmc/filesystem/test/data/httpdirectory/lighttp-default.html211
-rw-r--r--xbmc/filesystem/test/data/httpdirectory/nginx-default.html11
-rw-r--r--xbmc/filesystem/test/data/httpdirectory/nginx-fancyindex.html34
-rw-r--r--xbmc/filesystem/test/extendedlocalheader.zipbin0 -> 15352 bytes
-rw-r--r--xbmc/filesystem/test/refRARnormal.rarbin0 -> 3267 bytes
-rw-r--r--xbmc/filesystem/test/refRARstored.rarbin0 -> 5430 bytes
-rw-r--r--xbmc/filesystem/test/reffile.txt25
-rw-r--r--xbmc/filesystem/test/reffile.txt.rarbin0 -> 967 bytes
-rw-r--r--xbmc/filesystem/test/reffile.txt.zipbin0 -> 1023 bytes
-rw-r--r--xbmc/games/CMakeLists.txt10
-rw-r--r--xbmc/games/GameServices.cpp63
-rw-r--r--xbmc/games/GameServices.h72
-rw-r--r--xbmc/games/GameSettings.cpp242
-rw-r--r--xbmc/games/GameSettings.h55
-rw-r--r--xbmc/games/GameTypes.h32
-rw-r--r--xbmc/games/GameUtils.cpp314
-rw-r--r--xbmc/games/GameUtils.h105
-rw-r--r--xbmc/games/addons/CMakeLists.txt14
-rw-r--r--xbmc/games/addons/GameClient.cpp730
-rw-r--r--xbmc/games/addons/GameClient.h264
-rw-r--r--xbmc/games/addons/GameClientCallbacks.h38
-rw-r--r--xbmc/games/addons/GameClientInGameSaves.cpp164
-rw-r--r--xbmc/games/addons/GameClientInGameSaves.h66
-rw-r--r--xbmc/games/addons/GameClientProperties.cpp292
-rw-r--r--xbmc/games/addons/GameClientProperties.h93
-rw-r--r--xbmc/games/addons/GameClientSubsystem.cpp70
-rw-r--r--xbmc/games/addons/GameClientSubsystem.h81
-rw-r--r--xbmc/games/addons/GameClientTranslator.cpp256
-rw-r--r--xbmc/games/addons/GameClientTranslator.h115
-rw-r--r--xbmc/games/addons/cheevos/CMakeLists.txt5
-rw-r--r--xbmc/games/addons/cheevos/GameClientCheevos.cpp191
-rw-r--r--xbmc/games/addons/cheevos/GameClientCheevos.h57
-rw-r--r--xbmc/games/addons/input/CMakeLists.txt23
-rw-r--r--xbmc/games/addons/input/GameClientController.cpp138
-rw-r--r--xbmc/games/addons/input/GameClientController.h71
-rw-r--r--xbmc/games/addons/input/GameClientDevice.cpp73
-rw-r--r--xbmc/games/addons/input/GameClientDevice.h77
-rw-r--r--xbmc/games/addons/input/GameClientHardware.cpp25
-rw-r--r--xbmc/games/addons/input/GameClientHardware.h43
-rw-r--r--xbmc/games/addons/input/GameClientInput.cpp697
-rw-r--r--xbmc/games/addons/input/GameClientInput.h160
-rw-r--r--xbmc/games/addons/input/GameClientJoystick.cpp205
-rw-r--r--xbmc/games/addons/input/GameClientJoystick.h101
-rw-r--r--xbmc/games/addons/input/GameClientKeyboard.cpp101
-rw-r--r--xbmc/games/addons/input/GameClientKeyboard.h76
-rw-r--r--xbmc/games/addons/input/GameClientMouse.cpp106
-rw-r--r--xbmc/games/addons/input/GameClientMouse.h74
-rw-r--r--xbmc/games/addons/input/GameClientPort.cpp72
-rw-r--r--xbmc/games/addons/input/GameClientPort.h103
-rw-r--r--xbmc/games/addons/input/GameClientTopology.cpp110
-rw-r--r--xbmc/games/addons/input/GameClientTopology.h50
-rw-r--r--xbmc/games/addons/streams/CMakeLists.txt14
-rw-r--r--xbmc/games/addons/streams/GameClientStreamAudio.cpp105
-rw-r--r--xbmc/games/addons/streams/GameClientStreamAudio.h52
-rw-r--r--xbmc/games/addons/streams/GameClientStreamSwFramebuffer.cpp40
-rw-r--r--xbmc/games/addons/streams/GameClientStreamSwFramebuffer.h29
-rw-r--r--xbmc/games/addons/streams/GameClientStreamVideo.cpp108
-rw-r--r--xbmc/games/addons/streams/GameClientStreamVideo.h49
-rw-r--r--xbmc/games/addons/streams/GameClientStreams.cpp118
-rw-r--r--xbmc/games/addons/streams/GameClientStreams.h55
-rw-r--r--xbmc/games/addons/streams/IGameClientStream.h77
-rw-r--r--xbmc/games/agents/CMakeLists.txt7
-rw-r--r--xbmc/games/agents/GameAgentManager.cpp547
-rw-r--r--xbmc/games/agents/GameAgentManager.h171
-rw-r--r--xbmc/games/controllers/CMakeLists.txt18
-rw-r--r--xbmc/games/controllers/Controller.cpp157
-rw-r--r--xbmc/games/controllers/Controller.h120
-rw-r--r--xbmc/games/controllers/ControllerDefinitions.h55
-rw-r--r--xbmc/games/controllers/ControllerIDs.h15
-rw-r--r--xbmc/games/controllers/ControllerLayout.cpp159
-rw-r--r--xbmc/games/controllers/ControllerLayout.h92
-rw-r--r--xbmc/games/controllers/ControllerManager.cpp122
-rw-r--r--xbmc/games/controllers/ControllerManager.h93
-rw-r--r--xbmc/games/controllers/ControllerTranslator.cpp744
-rw-r--r--xbmc/games/controllers/ControllerTranslator.h53
-rw-r--r--xbmc/games/controllers/ControllerTypes.h33
-rw-r--r--xbmc/games/controllers/DefaultController.cpp34
-rw-r--r--xbmc/games/controllers/DefaultController.h50
-rw-r--r--xbmc/games/controllers/dialogs/CMakeLists.txt15
-rw-r--r--xbmc/games/controllers/dialogs/ControllerInstaller.cpp138
-rw-r--r--xbmc/games/controllers/dialogs/ControllerInstaller.h28
-rw-r--r--xbmc/games/controllers/dialogs/ControllerSelect.cpp133
-rw-r--r--xbmc/games/controllers/dialogs/ControllerSelect.h44
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp84
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h54
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogButtonCapture.cpp130
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h65
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp132
-rw-r--r--xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h47
-rw-r--r--xbmc/games/controllers/guicontrols/CMakeLists.txt28
-rw-r--r--xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp100
-rw-r--r--xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h49
-rw-r--r--xbmc/games/controllers/guicontrols/GUIControlTypes.h29
-rw-r--r--xbmc/games/controllers/guicontrols/GUIControllerButton.cpp26
-rw-r--r--xbmc/games/controllers/guicontrols/GUIControllerButton.h29
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp86
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureButton.h68
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp36
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureControls.h38
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp51
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureFactory.h37
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp39
-rw-r--r--xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h27
-rw-r--r--xbmc/games/controllers/guicontrols/GUIGameController.cpp64
-rw-r--r--xbmc/games/controllers/guicontrols/GUIGameController.h39
-rw-r--r--xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.cpp59
-rw-r--r--xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h42
-rw-r--r--xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp87
-rw-r--r--xbmc/games/controllers/guicontrols/GUISelectKeyButton.h51
-rw-r--r--xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp88
-rw-r--r--xbmc/games/controllers/guicontrols/GUIThrottleButton.h44
-rw-r--r--xbmc/games/controllers/guicontrols/GUIWheelButton.cpp88
-rw-r--r--xbmc/games/controllers/guicontrols/GUIWheelButton.h44
-rw-r--r--xbmc/games/controllers/input/CMakeLists.txt11
-rw-r--r--xbmc/games/controllers/input/InputSink.cpp67
-rw-r--r--xbmc/games/controllers/input/InputSink.h53
-rw-r--r--xbmc/games/controllers/input/PhysicalFeature.cpp162
-rw-r--r--xbmc/games/controllers/input/PhysicalFeature.h65
-rw-r--r--xbmc/games/controllers/input/PhysicalTopology.cpp56
-rw-r--r--xbmc/games/controllers/input/PhysicalTopology.h61
-rw-r--r--xbmc/games/controllers/types/CMakeLists.txt12
-rw-r--r--xbmc/games/controllers/types/ControllerGrid.cpp242
-rw-r--r--xbmc/games/controllers/types/ControllerGrid.h167
-rw-r--r--xbmc/games/controllers/types/ControllerHub.cpp109
-rw-r--r--xbmc/games/controllers/types/ControllerHub.h53
-rw-r--r--xbmc/games/controllers/types/ControllerNode.cpp136
-rw-r--r--xbmc/games/controllers/types/ControllerNode.h118
-rw-r--r--xbmc/games/controllers/types/ControllerTree.h30
-rw-r--r--xbmc/games/controllers/windows/CMakeLists.txt15
-rw-r--r--xbmc/games/controllers/windows/GUIConfigurationWizard.cpp494
-rw-r--r--xbmc/games/controllers/windows/GUIConfigurationWizard.h117
-rw-r--r--xbmc/games/controllers/windows/GUIControllerDefines.h45
-rw-r--r--xbmc/games/controllers/windows/GUIControllerList.cpp239
-rw-r--r--xbmc/games/controllers/windows/GUIControllerList.h62
-rw-r--r--xbmc/games/controllers/windows/GUIControllerWindow.cpp376
-rw-r--r--xbmc/games/controllers/windows/GUIControllerWindow.h68
-rw-r--r--xbmc/games/controllers/windows/GUIFeatureList.cpp297
-rw-r--r--xbmc/games/controllers/windows/GUIFeatureList.h77
-rw-r--r--xbmc/games/controllers/windows/IConfigurationWindow.h262
-rw-r--r--xbmc/games/dialogs/CMakeLists.txt10
-rw-r--r--xbmc/games/dialogs/DialogGameDefines.h27
-rw-r--r--xbmc/games/dialogs/GUIDialogSelectGameClient.cpp121
-rw-r--r--xbmc/games/dialogs/GUIDialogSelectGameClient.h61
-rw-r--r--xbmc/games/dialogs/GUIDialogSelectSavestate.cpp58
-rw-r--r--xbmc/games/dialogs/GUIDialogSelectSavestate.h28
-rw-r--r--xbmc/games/dialogs/osd/CMakeLists.txt25
-rw-r--r--xbmc/games/dialogs/osd/DialogGameAdvancedSettings.cpp54
-rw-r--r--xbmc/games/dialogs/osd/DialogGameAdvancedSettings.h27
-rw-r--r--xbmc/games/dialogs/osd/DialogGameOSD.cpp81
-rw-r--r--xbmc/games/dialogs/osd/DialogGameOSD.h54
-rw-r--r--xbmc/games/dialogs/osd/DialogGameOSDHelp.cpp71
-rw-r--r--xbmc/games/dialogs/osd/DialogGameOSDHelp.h40
-rw-r--r--xbmc/games/dialogs/osd/DialogGameSaves.cpp500
-rw-r--r--xbmc/games/dialogs/osd/DialogGameSaves.h113
-rw-r--r--xbmc/games/dialogs/osd/DialogGameStretchMode.cpp128
-rw-r--r--xbmc/games/dialogs/osd/DialogGameStretchMode.h51
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoFilter.cpp157
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoFilter.h47
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoRotation.cpp109
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoRotation.h44
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoSelect.cpp254
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVideoSelect.h84
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVolume.cpp149
-rw-r--r--xbmc/games/dialogs/osd/DialogGameVolume.h80
-rw-r--r--xbmc/games/dialogs/osd/DialogInGameSaves.cpp426
-rw-r--r--xbmc/games/dialogs/osd/DialogInGameSaves.h79
-rw-r--r--xbmc/games/ports/input/CMakeLists.txt11
-rw-r--r--xbmc/games/ports/input/PhysicalPort.cpp66
-rw-r--r--xbmc/games/ports/input/PhysicalPort.h64
-rw-r--r--xbmc/games/ports/input/PortInput.cpp129
-rw-r--r--xbmc/games/ports/input/PortInput.h77
-rw-r--r--xbmc/games/ports/input/PortManager.cpp347
-rw-r--r--xbmc/games/ports/input/PortManager.h81
-rw-r--r--xbmc/games/ports/types/CMakeLists.txt7
-rw-r--r--xbmc/games/ports/types/PortNode.cpp157
-rw-r--r--xbmc/games/ports/types/PortNode.h130
-rw-r--r--xbmc/games/ports/windows/CMakeLists.txt11
-rw-r--r--xbmc/games/ports/windows/GUIPortDefines.h22
-rw-r--r--xbmc/games/ports/windows/GUIPortList.cpp344
-rw-r--r--xbmc/games/ports/windows/GUIPortList.h79
-rw-r--r--xbmc/games/ports/windows/GUIPortWindow.cpp183
-rw-r--r--xbmc/games/ports/windows/GUIPortWindow.h56
-rw-r--r--xbmc/games/ports/windows/IPortList.h104
-rw-r--r--xbmc/games/tags/CMakeLists.txt5
-rw-r--r--xbmc/games/tags/GameInfoTag.cpp158
-rw-r--r--xbmc/games/tags/GameInfoTag.h112
-rw-r--r--xbmc/games/windows/CMakeLists.txt7
-rw-r--r--xbmc/games/windows/GUIViewStateWindowGames.cpp91
-rw-r--r--xbmc/games/windows/GUIViewStateWindowGames.h34
-rw-r--r--xbmc/games/windows/GUIWindowGames.cpp340
-rw-r--r--xbmc/games/windows/GUIWindowGames.h45
-rw-r--r--xbmc/guilib/CMakeLists.txt233
-rw-r--r--xbmc/guilib/D3DResource.cpp1244
-rw-r--r--xbmc/guilib/D3DResource.h293
-rw-r--r--xbmc/guilib/DDSImage.cpp148
-rw-r--r--xbmc/guilib/DDSImage.h102
-rw-r--r--xbmc/guilib/DirectXGraphics.cpp374
-rw-r--r--xbmc/guilib/DirectXGraphics.h159
-rw-r--r--xbmc/guilib/DirtyRegion.h27
-rw-r--r--xbmc/guilib/DirtyRegionSolvers.cpp73
-rw-r--r--xbmc/guilib/DirtyRegionSolvers.h39
-rw-r--r--xbmc/guilib/DirtyRegionTracker.cpp88
-rw-r--r--xbmc/guilib/DirtyRegionTracker.h35
-rw-r--r--xbmc/guilib/DispResource.h32
-rw-r--r--xbmc/guilib/FFmpegImage.cpp754
-rw-r--r--xbmc/guilib/FFmpegImage.h95
-rw-r--r--xbmc/guilib/GUIAction.cpp153
-rw-r--r--xbmc/guilib/GUIAction.h119
-rw-r--r--xbmc/guilib/GUIAudioManager.cpp399
-rw-r--r--xbmc/guilib/GUIAudioManager.h89
-rw-r--r--xbmc/guilib/GUIBaseContainer.cpp1510
-rw-r--r--xbmc/guilib/GUIBaseContainer.h238
-rw-r--r--xbmc/guilib/GUIBorderedImage.cpp86
-rw-r--r--xbmc/guilib/GUIBorderedImage.h37
-rw-r--r--xbmc/guilib/GUIButtonControl.cpp409
-rw-r--r--xbmc/guilib/GUIButtonControl.dox85
-rw-r--r--xbmc/guilib/GUIButtonControl.h112
-rw-r--r--xbmc/guilib/GUIColorButtonControl.cpp217
-rw-r--r--xbmc/guilib/GUIColorButtonControl.dox90
-rw-r--r--xbmc/guilib/GUIColorButtonControl.h71
-rw-r--r--xbmc/guilib/GUIColorManager.cpp144
-rw-r--r--xbmc/guilib/GUIColorManager.h56
-rw-r--r--xbmc/guilib/GUIComponent.cpp103
-rw-r--r--xbmc/guilib/GUIComponent.h49
-rw-r--r--xbmc/guilib/GUIControl.cpp979
-rw-r--r--xbmc/guilib/GUIControl.h383
-rw-r--r--xbmc/guilib/GUIControlFactory.cpp1588
-rw-r--r--xbmc/guilib/GUIControlFactory.h160
-rw-r--r--xbmc/guilib/GUIControlGroup.cpp537
-rw-r--r--xbmc/guilib/GUIControlGroup.dox108
-rw-r--r--xbmc/guilib/GUIControlGroup.h124
-rw-r--r--xbmc/guilib/GUIControlGroupList.cpp601
-rw-r--r--xbmc/guilib/GUIControlGroupList.h84
-rw-r--r--xbmc/guilib/GUIControlLookup.cpp113
-rw-r--r--xbmc/guilib/GUIControlLookup.h47
-rw-r--r--xbmc/guilib/GUIControlProfiler.cpp337
-rw-r--r--xbmc/guilib/GUIControlProfiler.h88
-rw-r--r--xbmc/guilib/GUIDialog.cpp239
-rw-r--r--xbmc/guilib/GUIDialog.h83
-rw-r--r--xbmc/guilib/GUIEditControl.cpp743
-rw-r--r--xbmc/guilib/GUIEditControl.dox60
-rw-r--r--xbmc/guilib/GUIEditControl.h131
-rw-r--r--xbmc/guilib/GUIFadeLabelControl.cpp267
-rw-r--r--xbmc/guilib/GUIFadeLabelControl.dox72
-rw-r--r--xbmc/guilib/GUIFadeLabelControl.h78
-rw-r--r--xbmc/guilib/GUIFixedListContainer.cpp308
-rw-r--r--xbmc/guilib/GUIFixedListContainer.dox142
-rw-r--r--xbmc/guilib/GUIFixedListContainer.h60
-rw-r--r--xbmc/guilib/GUIFont.cpp353
-rw-r--r--xbmc/guilib/GUIFont.h183
-rw-r--r--xbmc/guilib/GUIFontCache.cpp260
-rw-r--r--xbmc/guilib/GUIFontCache.h286
-rw-r--r--xbmc/guilib/GUIFontManager.cpp671
-rw-r--r--xbmc/guilib/GUIFontManager.h140
-rw-r--r--xbmc/guilib/GUIFontTTF.cpp1166
-rw-r--r--xbmc/guilib/GUIFontTTF.h279
-rw-r--r--xbmc/guilib/GUIFontTTFDX.cpp372
-rw-r--r--xbmc/guilib/GUIFontTTFDX.h67
-rw-r--r--xbmc/guilib/GUIFontTTFGL.cpp494
-rw-r--r--xbmc/guilib/GUIFontTTFGL.h58
-rw-r--r--xbmc/guilib/GUIImage.cpp417
-rw-r--r--xbmc/guilib/GUIImage.dox98
-rw-r--r--xbmc/guilib/GUIImage.h119
-rw-r--r--xbmc/guilib/GUIIncludes.cpp703
-rw-r--r--xbmc/guilib/GUIIncludes.h133
-rw-r--r--xbmc/guilib/GUIKeyboard.h84
-rw-r--r--xbmc/guilib/GUIKeyboardFactory.cpp239
-rw-r--r--xbmc/guilib/GUIKeyboardFactory.h50
-rw-r--r--xbmc/guilib/GUILabel.cpp250
-rw-r--r--xbmc/guilib/GUILabel.h240
-rw-r--r--xbmc/guilib/GUILabelControl.cpp293
-rw-r--r--xbmc/guilib/GUILabelControl.dox112
-rw-r--r--xbmc/guilib/GUILabelControl.h80
-rw-r--r--xbmc/guilib/GUIListContainer.cpp299
-rw-r--r--xbmc/guilib/GUIListContainer.dox138
-rw-r--r--xbmc/guilib/GUIListContainer.h55
-rw-r--r--xbmc/guilib/GUIListGroup.cpp238
-rw-r--r--xbmc/guilib/GUIListGroup.dox67
-rw-r--r--xbmc/guilib/GUIListGroup.h50
-rw-r--r--xbmc/guilib/GUIListItem.cpp441
-rw-r--r--xbmc/guilib/GUIListItem.h201
-rw-r--r--xbmc/guilib/GUIListItemLayout.cpp245
-rw-r--r--xbmc/guilib/GUIListItemLayout.h68
-rw-r--r--xbmc/guilib/GUIListLabel.cpp106
-rw-r--r--xbmc/guilib/GUIListLabel.h57
-rw-r--r--xbmc/guilib/GUIMessage.cpp146
-rw-r--r--xbmc/guilib/GUIMessage.h407
-rw-r--r--xbmc/guilib/GUIMoverControl.cpp254
-rw-r--r--xbmc/guilib/GUIMoverControl.dox92
-rw-r--r--xbmc/guilib/GUIMoverControl.h78
-rw-r--r--xbmc/guilib/GUIMultiImage.cpp323
-rw-r--r--xbmc/guilib/GUIMultiImage.dox78
-rw-r--r--xbmc/guilib/GUIMultiImage.h89
-rw-r--r--xbmc/guilib/GUIPanelContainer.cpp567
-rw-r--r--xbmc/guilib/GUIPanelContainer.dox126
-rw-r--r--xbmc/guilib/GUIPanelContainer.h62
-rw-r--r--xbmc/guilib/GUIProgressControl.cpp348
-rw-r--r--xbmc/guilib/GUIProgressControl.dox62
-rw-r--r--xbmc/guilib/GUIProgressControl.h71
-rw-r--r--xbmc/guilib/GUIRSSControl.cpp196
-rw-r--r--xbmc/guilib/GUIRSSControl.dox106
-rw-r--r--xbmc/guilib/GUIRSSControl.h72
-rw-r--r--xbmc/guilib/GUIRadioButtonControl.cpp260
-rw-r--r--xbmc/guilib/GUIRadioButtonControl.dox102
-rw-r--r--xbmc/guilib/GUIRadioButtonControl.h68
-rw-r--r--xbmc/guilib/GUIRangesControl.cpp414
-rw-r--r--xbmc/guilib/GUIRangesControl.dox56
-rw-r--r--xbmc/guilib/GUIRangesControl.h91
-rw-r--r--xbmc/guilib/GUIRenderingControl.cpp117
-rw-r--r--xbmc/guilib/GUIRenderingControl.dox34
-rw-r--r--xbmc/guilib/GUIRenderingControl.h33
-rw-r--r--xbmc/guilib/GUIResizeControl.cpp227
-rw-r--r--xbmc/guilib/GUIResizeControl.dox93
-rw-r--r--xbmc/guilib/GUIResizeControl.h70
-rw-r--r--xbmc/guilib/GUIScrollBarControl.cpp397
-rw-r--r--xbmc/guilib/GUIScrollBarControl.dox79
-rw-r--r--xbmc/guilib/GUIScrollBarControl.h72
-rw-r--r--xbmc/guilib/GUISettingsSliderControl.cpp169
-rw-r--r--xbmc/guilib/GUISettingsSliderControl.dox80
-rw-r--r--xbmc/guilib/GUISettingsSliderControl.h66
-rw-r--r--xbmc/guilib/GUIShaderDX.cpp423
-rw-r--r--xbmc/guilib/GUIShaderDX.h142
-rw-r--r--xbmc/guilib/GUISliderControl.cpp773
-rw-r--r--xbmc/guilib/GUISliderControl.dox71
-rw-r--r--xbmc/guilib/GUISliderControl.h129
-rw-r--r--xbmc/guilib/GUISpinControl.cpp1078
-rw-r--r--xbmc/guilib/GUISpinControl.dox83
-rw-r--r--xbmc/guilib/GUISpinControl.h133
-rw-r--r--xbmc/guilib/GUISpinControlEx.cpp161
-rw-r--r--xbmc/guilib/GUISpinControlEx.dox96
-rw-r--r--xbmc/guilib/GUISpinControlEx.h57
-rw-r--r--xbmc/guilib/GUIStaticItem.cpp121
-rw-r--r--xbmc/guilib/GUIStaticItem.h91
-rw-r--r--xbmc/guilib/GUITextBox.cpp451
-rw-r--r--xbmc/guilib/GUITextBox.dox63
-rw-r--r--xbmc/guilib/GUITextBox.h97
-rw-r--r--xbmc/guilib/GUITextLayout.cpp736
-rw-r--r--xbmc/guilib/GUITextLayout.h195
-rw-r--r--xbmc/guilib/GUITexture.cpp714
-rw-r--r--xbmc/guilib/GUITexture.h207
-rw-r--r--xbmc/guilib/GUITextureD3D.cpp156
-rw-r--r--xbmc/guilib/GUITextureD3D.h41
-rw-r--r--xbmc/guilib/GUITextureGL.cpp361
-rw-r--r--xbmc/guilib/GUITextureGL.h58
-rw-r--r--xbmc/guilib/GUITextureGLES.cpp304
-rw-r--r--xbmc/guilib/GUITextureGLES.h60
-rw-r--r--xbmc/guilib/GUIToggleButtonControl.cpp175
-rw-r--r--xbmc/guilib/GUIToggleButtonControl.dox91
-rw-r--r--xbmc/guilib/GUIToggleButtonControl.h56
-rw-r--r--xbmc/guilib/GUIVideoControl.cpp108
-rw-r--r--xbmc/guilib/GUIVideoControl.dox42
-rw-r--r--xbmc/guilib/GUIVideoControl.h37
-rw-r--r--xbmc/guilib/GUIVisualisationControl.cpp452
-rw-r--r--xbmc/guilib/GUIVisualisationControl.dox42
-rw-r--r--xbmc/guilib/GUIVisualisationControl.h103
-rw-r--r--xbmc/guilib/GUIWindow.cpp1089
-rw-r--r--xbmc/guilib/GUIWindow.h293
-rw-r--r--xbmc/guilib/GUIWindowManager.cpp1808
-rw-r--r--xbmc/guilib/GUIWindowManager.h285
-rw-r--r--xbmc/guilib/GUIWrappingListContainer.cpp240
-rw-r--r--xbmc/guilib/GUIWrappingListContainer.dox139
-rw-r--r--xbmc/guilib/GUIWrappingListContainer.h50
-rw-r--r--xbmc/guilib/IAudioDeviceChangedCallback.h18
-rw-r--r--xbmc/guilib/IDirtyRegionSolver.h25
-rw-r--r--xbmc/guilib/IGUIContainer.h43
-rw-r--r--xbmc/guilib/IMsgTargetCallback.h27
-rw-r--r--xbmc/guilib/IRenderingCallback.h19
-rw-r--r--xbmc/guilib/ISliderCallback.h36
-rw-r--r--xbmc/guilib/IWindowManagerCallback.cpp14
-rw-r--r--xbmc/guilib/IWindowManagerCallback.h30
-rw-r--r--xbmc/guilib/LocalizeStrings.cpp250
-rw-r--r--xbmc/guilib/LocalizeStrings.h72
-rw-r--r--xbmc/guilib/Shader.cpp384
-rw-r--r--xbmc/guilib/Shader.h174
-rw-r--r--xbmc/guilib/StereoscopicsManager.cpp636
-rw-r--r--xbmc/guilib/StereoscopicsManager.h99
-rw-r--r--xbmc/guilib/Texture.cpp495
-rw-r--r--xbmc/guilib/Texture.h144
-rw-r--r--xbmc/guilib/TextureBundle.cpp83
-rw-r--r--xbmc/guilib/TextureBundle.h76
-rw-r--r--xbmc/guilib/TextureBundleXBT.cpp301
-rw-r--r--xbmc/guilib/TextureBundleXBT.h69
-rw-r--r--xbmc/guilib/TextureDX.cpp193
-rw-r--r--xbmc/guilib/TextureDX.h41
-rw-r--r--xbmc/guilib/TextureFormats.h22
-rw-r--r--xbmc/guilib/TextureGL.cpp226
-rw-r--r--xbmc/guilib/TextureGL.h33
-rw-r--r--xbmc/guilib/TextureManager.cpp658
-rw-r--r--xbmc/guilib/TextureManager.h131
-rw-r--r--xbmc/guilib/Tween.h401
-rw-r--r--xbmc/guilib/VisibleEffect.cpp835
-rw-r--r--xbmc/guilib/VisibleEffect.h269
-rw-r--r--xbmc/guilib/WindowIDs.dox145
-rw-r--r--xbmc/guilib/WindowIDs.h193
-rw-r--r--xbmc/guilib/XBTF.cpp235
-rw-r--r--xbmc/guilib/XBTF.h107
-rw-r--r--xbmc/guilib/XBTFReader.cpp216
-rw-r--r--xbmc/guilib/XBTFReader.h37
-rw-r--r--xbmc/guilib/_Controls.dox40
-rw-r--r--xbmc/guilib/gui3d.h44
-rw-r--r--xbmc/guilib/guiinfo/AddonsGUIInfo.cpp279
-rw-r--r--xbmc/guilib/guiinfo/AddonsGUIInfo.h37
-rw-r--r--xbmc/guilib/guiinfo/CMakeLists.txt42
-rw-r--r--xbmc/guilib/guiinfo/GUIControlsGUIInfo.cpp814
-rw-r--r--xbmc/guilib/guiinfo/GUIControlsGUIInfo.h56
-rw-r--r--xbmc/guilib/guiinfo/GUIInfo.cpp33
-rw-r--r--xbmc/guilib/guiinfo/GUIInfo.h110
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoBool.cpp41
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoBool.h46
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoColor.cpp69
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoColor.h53
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoHelper.cpp205
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoHelper.h43
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoLabel.cpp337
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoLabel.h146
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoLabels.h985
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoProvider.h49
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoProviders.cpp122
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoProviders.h157
-rw-r--r--xbmc/guilib/guiinfo/GamesGUIInfo.cpp87
-rw-r--r--xbmc/guilib/guiinfo/GamesGUIInfo.h37
-rw-r--r--xbmc/guilib/guiinfo/IGUIInfoProvider.h97
-rw-r--r--xbmc/guilib/guiinfo/LibraryGUIInfo.cpp290
-rw-r--r--xbmc/guilib/guiinfo/LibraryGUIInfo.h59
-rw-r--r--xbmc/guilib/guiinfo/MusicGUIInfo.cpp713
-rw-r--r--xbmc/guilib/guiinfo/MusicGUIInfo.h46
-rw-r--r--xbmc/guilib/guiinfo/PicturesGUIInfo.cpp259
-rw-r--r--xbmc/guilib/guiinfo/PicturesGUIInfo.h45
-rw-r--r--xbmc/guilib/guiinfo/PlayerGUIInfo.cpp726
-rw-r--r--xbmc/guilib/guiinfo/PlayerGUIInfo.h93
-rw-r--r--xbmc/guilib/guiinfo/SkinGUIInfo.cpp140
-rw-r--r--xbmc/guilib/guiinfo/SkinGUIInfo.h37
-rw-r--r--xbmc/guilib/guiinfo/SystemGUIInfo.cpp727
-rw-r--r--xbmc/guilib/guiinfo/SystemGUIInfo.h55
-rw-r--r--xbmc/guilib/guiinfo/VideoGUIInfo.cpp818
-rw-r--r--xbmc/guilib/guiinfo/VideoGUIInfo.h53
-rw-r--r--xbmc/guilib/guiinfo/VisualisationGUIInfo.cpp115
-rw-r--r--xbmc/guilib/guiinfo/VisualisationGUIInfo.h37
-rw-r--r--xbmc/guilib/guiinfo/WeatherGUIInfo.cpp81
-rw-r--r--xbmc/guilib/guiinfo/WeatherGUIInfo.h37
-rw-r--r--xbmc/guilib/iimage.h74
-rw-r--r--xbmc/guilib/imagefactory.cpp59
-rw-r--r--xbmc/guilib/imagefactory.h27
-rw-r--r--xbmc/input/AppTranslator.cpp67
-rw-r--r--xbmc/input/AppTranslator.h18
-rw-r--r--xbmc/input/ButtonTranslator.cpp436
-rw-r--r--xbmc/input/ButtonTranslator.h93
-rw-r--r--xbmc/input/CMakeLists.txt59
-rw-r--r--xbmc/input/CustomControllerTranslator.cpp117
-rw-r--r--xbmc/input/CustomControllerTranslator.h48
-rw-r--r--xbmc/input/GamepadTranslator.cpp85
-rw-r--r--xbmc/input/GamepadTranslator.h18
-rw-r--r--xbmc/input/IButtonMapper.h24
-rw-r--r--xbmc/input/IKeymap.h87
-rw-r--r--xbmc/input/IKeymapEnvironment.h59
-rw-r--r--xbmc/input/IRTranslator.cpp312
-rw-r--r--xbmc/input/IRTranslator.h44
-rw-r--r--xbmc/input/InertialScrollingHandler.cpp235
-rw-r--r--xbmc/input/InertialScrollingHandler.h56
-rw-r--r--xbmc/input/InputCodingTable.h57
-rw-r--r--xbmc/input/InputCodingTableBasePY.cpp1314
-rw-r--r--xbmc/input/InputCodingTableBasePY.h28
-rw-r--r--xbmc/input/InputCodingTableFactory.cpp24
-rw-r--r--xbmc/input/InputCodingTableFactory.h21
-rw-r--r--xbmc/input/InputCodingTableKorean.cpp380
-rw-r--r--xbmc/input/InputCodingTableKorean.h37
-rw-r--r--xbmc/input/InputManager.cpp938
-rw-r--r--xbmc/input/InputManager.h310
-rw-r--r--xbmc/input/InputTranslator.cpp51
-rw-r--r--xbmc/input/InputTranslator.h55
-rw-r--r--xbmc/input/InputTypes.h46
-rw-r--r--xbmc/input/JoystickMapper.cpp157
-rw-r--r--xbmc/input/JoystickMapper.h49
-rw-r--r--xbmc/input/Key.cpp178
-rw-r--r--xbmc/input/Key.h215
-rw-r--r--xbmc/input/KeyboardLayout.cpp165
-rw-r--r--xbmc/input/KeyboardLayout.h52
-rw-r--r--xbmc/input/KeyboardLayoutManager.cpp148
-rw-r--r--xbmc/input/KeyboardLayoutManager.h45
-rw-r--r--xbmc/input/KeyboardStat.cpp255
-rw-r--r--xbmc/input/KeyboardStat.h53
-rw-r--r--xbmc/input/KeyboardTranslator.cpp98
-rw-r--r--xbmc/input/KeyboardTranslator.h21
-rw-r--r--xbmc/input/Keymap.cpp49
-rw-r--r--xbmc/input/Keymap.h31
-rw-r--r--xbmc/input/KeymapEnvironment.cpp16
-rw-r--r--xbmc/input/KeymapEnvironment.h27
-rw-r--r--xbmc/input/TouchTranslator.cpp193
-rw-r--r--xbmc/input/TouchTranslator.h58
-rw-r--r--xbmc/input/WindowKeymap.cpp54
-rw-r--r--xbmc/input/WindowKeymap.h41
-rw-r--r--xbmc/input/WindowTranslator.cpp353
-rw-r--r--xbmc/input/WindowTranslator.h72
-rw-r--r--xbmc/input/XBMC_keyboard.h45
-rw-r--r--xbmc/input/XBMC_keysym.h263
-rw-r--r--xbmc/input/XBMC_keytable.cpp354
-rw-r--r--xbmc/input/XBMC_keytable.h43
-rw-r--r--xbmc/input/XBMC_vkeys.h273
-rw-r--r--xbmc/input/actions/Action.cpp154
-rw-r--r--xbmc/input/actions/Action.h123
-rw-r--r--xbmc/input/actions/ActionIDs.h493
-rw-r--r--xbmc/input/actions/ActionTranslator.cpp311
-rw-r--r--xbmc/input/actions/ActionTranslator.h20
-rw-r--r--xbmc/input/actions/CMakeLists.txt10
-rw-r--r--xbmc/input/button/ButtonStat.cpp25
-rw-r--r--xbmc/input/button/ButtonStat.h30
-rw-r--r--xbmc/input/button/CMakeLists.txt5
-rw-r--r--xbmc/input/hardware/IHardwareInput.h32
-rw-r--r--xbmc/input/joysticks/CMakeLists.txt30
-rw-r--r--xbmc/input/joysticks/DeadzoneFilter.cpp96
-rw-r--r--xbmc/input/joysticks/DeadzoneFilter.h82
-rw-r--r--xbmc/input/joysticks/DriverPrimitive.cpp271
-rw-r--r--xbmc/input/joysticks/DriverPrimitive.h200
-rw-r--r--xbmc/input/joysticks/JoystickEasterEgg.cpp107
-rw-r--r--xbmc/input/joysticks/JoystickEasterEgg.h45
-rw-r--r--xbmc/input/joysticks/JoystickIDs.h13
-rw-r--r--xbmc/input/joysticks/JoystickMonitor.cpp111
-rw-r--r--xbmc/input/joysticks/JoystickMonitor.h58
-rw-r--r--xbmc/input/joysticks/JoystickTranslator.cpp175
-rw-r--r--xbmc/input/joysticks/JoystickTranslator.h125
-rw-r--r--xbmc/input/joysticks/JoystickTypes.h186
-rw-r--r--xbmc/input/joysticks/JoystickUtils.cpp103
-rw-r--r--xbmc/input/joysticks/JoystickUtils.h110
-rw-r--r--xbmc/input/joysticks/RumbleGenerator.cpp130
-rw-r--r--xbmc/input/joysticks/RumbleGenerator.h58
-rw-r--r--xbmc/input/joysticks/dialogs/CMakeLists.txt5
-rw-r--r--xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.cpp59
-rw-r--r--xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.h30
-rw-r--r--xbmc/input/joysticks/generic/ButtonMapping.cpp592
-rw-r--r--xbmc/input/joysticks/generic/ButtonMapping.h393
-rw-r--r--xbmc/input/joysticks/generic/CMakeLists.txt11
-rw-r--r--xbmc/input/joysticks/generic/DriverReceiving.cpp38
-rw-r--r--xbmc/input/joysticks/generic/DriverReceiving.h46
-rw-r--r--xbmc/input/joysticks/generic/FeatureHandling.cpp551
-rw-r--r--xbmc/input/joysticks/generic/FeatureHandling.h282
-rw-r--r--xbmc/input/joysticks/generic/InputHandling.cpp179
-rw-r--r--xbmc/input/joysticks/generic/InputHandling.h69
-rw-r--r--xbmc/input/joysticks/interfaces/IButtonMap.h343
-rw-r--r--xbmc/input/joysticks/interfaces/IButtonMapCallback.h47
-rw-r--r--xbmc/input/joysticks/interfaces/IButtonMapper.h124
-rw-r--r--xbmc/input/joysticks/interfaces/IButtonSequence.h31
-rw-r--r--xbmc/input/joysticks/interfaces/IDriverHandler.h77
-rw-r--r--xbmc/input/joysticks/interfaces/IDriverReceiver.h35
-rw-r--r--xbmc/input/joysticks/interfaces/IInputHandler.h169
-rw-r--r--xbmc/input/joysticks/interfaces/IInputProvider.h43
-rw-r--r--xbmc/input/joysticks/interfaces/IInputReceiver.h37
-rw-r--r--xbmc/input/joysticks/interfaces/IKeyHandler.h56
-rw-r--r--xbmc/input/joysticks/interfaces/IKeymapHandler.h52
-rw-r--r--xbmc/input/joysticks/keymaps/CMakeLists.txt11
-rw-r--r--xbmc/input/joysticks/keymaps/KeyHandler.cpp290
-rw-r--r--xbmc/input/joysticks/keymaps/KeyHandler.h114
-rw-r--r--xbmc/input/joysticks/keymaps/KeymapHandler.cpp278
-rw-r--r--xbmc/input/joysticks/keymaps/KeymapHandler.h108
-rw-r--r--xbmc/input/joysticks/keymaps/KeymapHandling.cpp104
-rw-r--r--xbmc/input/joysticks/keymaps/KeymapHandling.h76
-rw-r--r--xbmc/input/keyboard/CMakeLists.txt14
-rw-r--r--xbmc/input/keyboard/KeyboardEasterEgg.cpp45
-rw-r--r--xbmc/input/keyboard/KeyboardEasterEgg.h38
-rw-r--r--xbmc/input/keyboard/KeyboardTypes.h40
-rw-r--r--xbmc/input/keyboard/KeymapActionMap.cpp26
-rw-r--r--xbmc/input/keyboard/KeymapActionMap.h28
-rw-r--r--xbmc/input/keyboard/generic/CMakeLists.txt5
-rw-r--r--xbmc/input/keyboard/generic/KeyboardInputHandling.cpp51
-rw-r--r--xbmc/input/keyboard/generic/KeyboardInputHandling.h46
-rw-r--r--xbmc/input/keyboard/interfaces/IActionMap.h36
-rw-r--r--xbmc/input/keyboard/interfaces/IKeyboardDriverHandler.h43
-rw-r--r--xbmc/input/keyboard/interfaces/IKeyboardInputHandler.h69
-rw-r--r--xbmc/input/keyboard/interfaces/IKeyboardInputProvider.h43
-rw-r--r--xbmc/input/mouse/CMakeLists.txt13
-rw-r--r--xbmc/input/mouse/MouseStat.cpp378
-rw-r--r--xbmc/input/mouse/MouseStat.h227
-rw-r--r--xbmc/input/mouse/MouseTranslator.cpp135
-rw-r--r--xbmc/input/mouse/MouseTranslator.h34
-rw-r--r--xbmc/input/mouse/MouseTypes.h55
-rw-r--r--xbmc/input/mouse/generic/CMakeLists.txt5
-rw-r--r--xbmc/input/mouse/generic/MouseInputHandling.cpp352
-rw-r--r--xbmc/input/mouse/generic/MouseInputHandling.h66
-rw-r--r--xbmc/input/mouse/interfaces/IMouseDriverHandler.h56
-rw-r--r--xbmc/input/mouse/interfaces/IMouseInputHandler.h66
-rw-r--r--xbmc/input/mouse/interfaces/IMouseInputProvider.h43
-rw-r--r--xbmc/input/remote/IRRemote.h106
-rw-r--r--xbmc/input/touch/CMakeLists.txt8
-rw-r--r--xbmc/input/touch/ITouchActionHandler.h261
-rw-r--r--xbmc/input/touch/ITouchInputHandler.h104
-rw-r--r--xbmc/input/touch/ITouchInputHandling.cpp159
-rw-r--r--xbmc/input/touch/ITouchInputHandling.h90
-rw-r--r--xbmc/input/touch/TouchTypes.h112
-rw-r--r--xbmc/input/touch/generic/CMakeLists.txt14
-rw-r--r--xbmc/input/touch/generic/GenericTouchActionHandler.cpp212
-rw-r--r--xbmc/input/touch/generic/GenericTouchActionHandler.h94
-rw-r--r--xbmc/input/touch/generic/GenericTouchInputHandler.cpp397
-rw-r--r--xbmc/input/touch/generic/GenericTouchInputHandler.h98
-rw-r--r--xbmc/input/touch/generic/GenericTouchPinchDetector.cpp87
-rw-r--r--xbmc/input/touch/generic/GenericTouchPinchDetector.h32
-rw-r--r--xbmc/input/touch/generic/GenericTouchRotateDetector.cpp117
-rw-r--r--xbmc/input/touch/generic/GenericTouchRotateDetector.h36
-rw-r--r--xbmc/input/touch/generic/GenericTouchSwipeDetector.cpp176
-rw-r--r--xbmc/input/touch/generic/GenericTouchSwipeDetector.h49
-rw-r--r--xbmc/input/touch/generic/IGenericTouchGestureDetector.h91
-rw-r--r--xbmc/interfaces/AnnouncementManager.cpp342
-rw-r--r--xbmc/interfaces/AnnouncementManager.h95
-rw-r--r--xbmc/interfaces/CMakeLists.txt7
-rw-r--r--xbmc/interfaces/IActionListener.h19
-rw-r--r--xbmc/interfaces/IAnnouncer.h80
-rw-r--r--xbmc/interfaces/builtins/AddonBuiltins.cpp522
-rw-r--r--xbmc/interfaces/builtins/AddonBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/AndroidBuiltins.cpp73
-rw-r--r--xbmc/interfaces/builtins/AndroidBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/ApplicationBuiltins.cpp222
-rw-r--r--xbmc/interfaces/builtins/ApplicationBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/Builtins.cpp168
-rw-r--r--xbmc/interfaces/builtins/Builtins.h55
-rw-r--r--xbmc/interfaces/builtins/CECBuiltins.cpp82
-rw-r--r--xbmc/interfaces/builtins/CECBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/CMakeLists.txt41
-rw-r--r--xbmc/interfaces/builtins/GUIBuiltins.cpp601
-rw-r--r--xbmc/interfaces/builtins/GUIBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/GUIContainerBuiltins.cpp189
-rw-r--r--xbmc/interfaces/builtins/GUIContainerBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/GUIControlBuiltins.cpp238
-rw-r--r--xbmc/interfaces/builtins/GUIControlBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/LibraryBuiltins.cpp416
-rw-r--r--xbmc/interfaces/builtins/LibraryBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/OpticalBuiltins.cpp75
-rw-r--r--xbmc/interfaces/builtins/OpticalBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/PVRBuiltins.cpp224
-rw-r--r--xbmc/interfaces/builtins/PVRBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/PictureBuiltins.cpp135
-rw-r--r--xbmc/interfaces/builtins/PictureBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/PlayerBuiltins.cpp842
-rw-r--r--xbmc/interfaces/builtins/PlayerBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/ProfileBuiltins.cpp133
-rw-r--r--xbmc/interfaces/builtins/ProfileBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/SkinBuiltins.cpp687
-rw-r--r--xbmc/interfaces/builtins/SkinBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/SystemBuiltins.cpp267
-rw-r--r--xbmc/interfaces/builtins/SystemBuiltins.h19
-rw-r--r--xbmc/interfaces/builtins/WeatherBuiltins.cpp88
-rw-r--r--xbmc/interfaces/builtins/WeatherBuiltins.h19
-rw-r--r--xbmc/interfaces/generic/CMakeLists.txt15
-rw-r--r--xbmc/interfaces/generic/ILanguageInvocationHandler.h31
-rw-r--r--xbmc/interfaces/generic/ILanguageInvoker.cpp96
-rw-r--r--xbmc/interfaces/generic/ILanguageInvoker.h77
-rw-r--r--xbmc/interfaces/generic/LanguageInvokerThread.cpp133
-rw-r--r--xbmc/interfaces/generic/LanguageInvokerThread.h56
-rw-r--r--xbmc/interfaces/generic/RunningScriptObserver.cpp42
-rw-r--r--xbmc/interfaces/generic/RunningScriptObserver.h34
-rw-r--r--xbmc/interfaces/generic/RunningScriptsHandler.h104
-rw-r--r--xbmc/interfaces/generic/ScriptInvocationManager.cpp423
-rw-r--r--xbmc/interfaces/generic/ScriptInvocationManager.h156
-rw-r--r--xbmc/interfaces/generic/ScriptRunner.cpp177
-rw-r--r--xbmc/interfaces/generic/ScriptRunner.h49
-rw-r--r--xbmc/interfaces/info/CMakeLists.txt10
-rw-r--r--xbmc/interfaces/info/Info.h19
-rw-r--r--xbmc/interfaces/info/InfoBool.cpp25
-rw-r--r--xbmc/interfaces/info/InfoBool.h83
-rw-r--r--xbmc/interfaces/info/InfoExpression.cpp311
-rw-r--r--xbmc/interfaces/info/InfoExpression.h117
-rw-r--r--xbmc/interfaces/info/SkinVariable.cpp82
-rw-r--r--xbmc/interfaces/info/SkinVariable.h56
-rw-r--r--xbmc/interfaces/json-rpc/AddonsOperations.cpp321
-rw-r--r--xbmc/interfaces/json-rpc/AddonsOperations.h41
-rw-r--r--xbmc/interfaces/json-rpc/ApplicationOperations.cpp165
-rw-r--r--xbmc/interfaces/json-rpc/ApplicationOperations.h30
-rw-r--r--xbmc/interfaces/json-rpc/AudioLibrary.cpp1372
-rw-r--r--xbmc/interfaces/json-rpc/AudioLibrary.h82
-rw-r--r--xbmc/interfaces/json-rpc/CMakeLists.txt48
-rw-r--r--xbmc/interfaces/json-rpc/FavouritesOperations.cpp161
-rw-r--r--xbmc/interfaces/json-rpc/FavouritesOperations.h23
-rw-r--r--xbmc/interfaces/json-rpc/FileItemHandler.cpp554
-rw-r--r--xbmc/interfaces/json-rpc/FileItemHandler.h64
-rw-r--r--xbmc/interfaces/json-rpc/FileOperations.cpp418
-rw-r--r--xbmc/interfaces/json-rpc/FileOperations.h39
-rw-r--r--xbmc/interfaces/json-rpc/GUIOperations.cpp178
-rw-r--r--xbmc/interfaces/json-rpc/GUIOperations.h33
-rw-r--r--xbmc/interfaces/json-rpc/IClient.h21
-rw-r--r--xbmc/interfaces/json-rpc/IJSONRPCAnnouncer.h46
-rw-r--r--xbmc/interfaces/json-rpc/ITransportLayer.h35
-rw-r--r--xbmc/interfaces/json-rpc/InputOperations.cpp180
-rw-r--r--xbmc/interfaces/json-rpc/InputOperations.h50
-rw-r--r--xbmc/interfaces/json-rpc/JSONRPC.cpp392
-rw-r--r--xbmc/interfaces/json-rpc/JSONRPC.h72
-rw-r--r--xbmc/interfaces/json-rpc/JSONRPCUtils.h163
-rw-r--r--xbmc/interfaces/json-rpc/JSONServiceDescription.cpp2153
-rw-r--r--xbmc/interfaces/json-rpc/JSONServiceDescription.h442
-rw-r--r--xbmc/interfaces/json-rpc/JSONUtils.cpp38
-rw-r--r--xbmc/interfaces/json-rpc/JSONUtils.h490
-rw-r--r--xbmc/interfaces/json-rpc/PVROperations.cpp543
-rw-r--r--xbmc/interfaces/json-rpc/PVROperations.h58
-rw-r--r--xbmc/interfaces/json-rpc/PlayerOperations.cpp2158
-rw-r--r--xbmc/interfaces/json-rpc/PlayerOperations.h96
-rw-r--r--xbmc/interfaces/json-rpc/PlaylistOperations.cpp323
-rw-r--r--xbmc/interfaces/json-rpc/PlaylistOperations.h46
-rw-r--r--xbmc/interfaces/json-rpc/ProfilesOperations.cpp136
-rw-r--r--xbmc/interfaces/json-rpc/ProfilesOperations.h25
-rw-r--r--xbmc/interfaces/json-rpc/SettingsOperations.cpp905
-rw-r--r--xbmc/interfaces/json-rpc/SettingsOperations.h99
-rw-r--r--xbmc/interfaces/json-rpc/SystemOperations.cpp101
-rw-r--r--xbmc/interfaces/json-rpc/SystemOperations.h31
-rw-r--r--xbmc/interfaces/json-rpc/TextureOperations.cpp95
-rw-r--r--xbmc/interfaces/json-rpc/TextureOperations.h23
-rw-r--r--xbmc/interfaces/json-rpc/VideoLibrary.cpp1385
-rw-r--r--xbmc/interfaces/json-rpc/VideoLibrary.h95
-rw-r--r--xbmc/interfaces/json-rpc/XBMCOperations.cpp88
-rw-r--r--xbmc/interfaces/json-rpc/XBMCOperations.h23
-rw-r--r--xbmc/interfaces/json-rpc/schema/CMakeLists.txt30
-rw-r--r--xbmc/interfaces/json-rpc/schema/GenerateAddonXml.cmake6
-rw-r--r--xbmc/interfaces/json-rpc/schema/license.txt7
-rw-r--r--xbmc/interfaces/json-rpc/schema/methods.json2941
-rw-r--r--xbmc/interfaces/json-rpc/schema/notifications.json464
-rw-r--r--xbmc/interfaces/json-rpc/schema/types.json2081
-rw-r--r--xbmc/interfaces/json-rpc/schema/version.txt1
-rw-r--r--xbmc/interfaces/legacy/Addon.cpp243
-rw-r--r--xbmc/interfaces/legacy/Addon.h483
-rw-r--r--xbmc/interfaces/legacy/AddonCallback.cpp26
-rw-r--r--xbmc/interfaces/legacy/AddonCallback.h43
-rw-r--r--xbmc/interfaces/legacy/AddonClass.cpp90
-rw-r--r--xbmc/interfaces/legacy/AddonClass.h209
-rw-r--r--xbmc/interfaces/legacy/AddonString.h21
-rw-r--r--xbmc/interfaces/legacy/AddonUtils.cpp123
-rw-r--r--xbmc/interfaces/legacy/AddonUtils.h95
-rw-r--r--xbmc/interfaces/legacy/Alternative.h70
-rw-r--r--xbmc/interfaces/legacy/CMakeLists.txt76
-rw-r--r--xbmc/interfaces/legacy/CallbackFunction.cpp14
-rw-r--r--xbmc/interfaces/legacy/CallbackFunction.h171
-rw-r--r--xbmc/interfaces/legacy/CallbackHandler.cpp148
-rw-r--r--xbmc/interfaces/legacy/CallbackHandler.h58
-rw-r--r--xbmc/interfaces/legacy/Control.cpp1435
-rw-r--r--xbmc/interfaces/legacy/Control.h2960
-rw-r--r--xbmc/interfaces/legacy/Dialog.cpp622
-rw-r--r--xbmc/interfaces/legacy/Dialog.h965
-rw-r--r--xbmc/interfaces/legacy/Dictionary.h34
-rw-r--r--xbmc/interfaces/legacy/DrmCryptoSession.cpp108
-rw-r--r--xbmc/interfaces/legacy/DrmCryptoSession.h342
-rw-r--r--xbmc/interfaces/legacy/Exception.h55
-rw-r--r--xbmc/interfaces/legacy/File.cpp61
-rw-r--r--xbmc/interfaces/legacy/File.h326
-rw-r--r--xbmc/interfaces/legacy/InfoTagGame.cpp172
-rw-r--r--xbmc/interfaces/legacy/InfoTagGame.h383
-rw-r--r--xbmc/interfaces/legacy/InfoTagMusic.cpp468
-rw-r--r--xbmc/interfaces/legacy/InfoTagMusic.h1034
-rw-r--r--xbmc/interfaces/legacy/InfoTagPicture.cpp98
-rw-r--r--xbmc/interfaces/legacy/InfoTagPicture.h180
-rw-r--r--xbmc/interfaces/legacy/InfoTagRadioRDS.cpp234
-rw-r--r--xbmc/interfaces/legacy/InfoTagRadioRDS.h443
-rw-r--r--xbmc/interfaces/legacy/InfoTagVideo.cpp1063
-rw-r--r--xbmc/interfaces/legacy/InfoTagVideo.h2772
-rw-r--r--xbmc/interfaces/legacy/Keyboard.cpp66
-rw-r--r--xbmc/interfaces/legacy/Keyboard.h215
-rw-r--r--xbmc/interfaces/legacy/LanguageHook.cpp41
-rw-r--r--xbmc/interfaces/legacy/LanguageHook.h151
-rw-r--r--xbmc/interfaces/legacy/List.h20
-rw-r--r--xbmc/interfaces/legacy/ListItem.cpp1056
-rw-r--r--xbmc/interfaces/legacy/ListItem.h1291
-rw-r--r--xbmc/interfaces/legacy/ModuleXbmc.cpp600
-rw-r--r--xbmc/interfaces/legacy/ModuleXbmc.h898
-rw-r--r--xbmc/interfaces/legacy/ModuleXbmcgui.cpp58
-rw-r--r--xbmc/interfaces/legacy/ModuleXbmcgui.h149
-rw-r--r--xbmc/interfaces/legacy/ModuleXbmcplugin.cpp125
-rw-r--r--xbmc/interfaces/legacy/ModuleXbmcplugin.h489
-rw-r--r--xbmc/interfaces/legacy/ModuleXbmcvfs.cpp137
-rw-r--r--xbmc/interfaces/legacy/ModuleXbmcvfs.h334
-rw-r--r--xbmc/interfaces/legacy/Monitor.cpp82
-rw-r--r--xbmc/interfaces/legacy/Monitor.h311
-rw-r--r--xbmc/interfaces/legacy/PlayList.cpp144
-rw-r--r--xbmc/interfaces/legacy/PlayList.h212
-rw-r--r--xbmc/interfaces/legacy/Player.cpp607
-rw-r--r--xbmc/interfaces/legacy/Player.h824
-rw-r--r--xbmc/interfaces/legacy/RenderCapture.h206
-rw-r--r--xbmc/interfaces/legacy/Settings.cpp232
-rw-r--r--xbmc/interfaces/legacy/Settings.h500
-rw-r--r--xbmc/interfaces/legacy/Stat.h195
-rw-r--r--xbmc/interfaces/legacy/String.cpp14
-rw-r--r--xbmc/interfaces/legacy/Tuple.h98
-rw-r--r--xbmc/interfaces/legacy/Window.cpp767
-rw-r--r--xbmc/interfaces/legacy/Window.h878
-rw-r--r--xbmc/interfaces/legacy/WindowDialog.cpp76
-rw-r--r--xbmc/interfaces/legacy/WindowDialog.h88
-rw-r--r--xbmc/interfaces/legacy/WindowDialogMixin.cpp75
-rw-r--r--xbmc/interfaces/legacy/WindowDialogMixin.h43
-rw-r--r--xbmc/interfaces/legacy/WindowException.h21
-rw-r--r--xbmc/interfaces/legacy/WindowInterceptor.h210
-rw-r--r--xbmc/interfaces/legacy/WindowXML.cpp527
-rw-r--r--xbmc/interfaces/legacy/WindowXML.h559
-rw-r--r--xbmc/interfaces/legacy/aojsonrpc.h30
-rw-r--r--xbmc/interfaces/legacy/swighelper.h100
-rw-r--r--xbmc/interfaces/legacy/wsgi/CMakeLists.txt13
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiErrorStream.cpp63
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiErrorStream.h92
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiInputStream.cpp166
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiInputStream.h120
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiResponse.cpp92
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiResponse.h77
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiResponseBody.cpp29
-rw-r--r--xbmc/interfaces/legacy/wsgi/WsgiResponseBody.h50
-rw-r--r--xbmc/interfaces/python/AddonPythonInvoker.cpp131
-rw-r--r--xbmc/interfaces/python/AddonPythonInvoker.h23
-rw-r--r--xbmc/interfaces/python/CMakeLists.txt21
-rw-r--r--xbmc/interfaces/python/CallbackHandler.cpp65
-rw-r--r--xbmc/interfaces/python/CallbackHandler.h40
-rw-r--r--xbmc/interfaces/python/ContextItemAddonInvoker.cpp42
-rw-r--r--xbmc/interfaces/python/ContextItemAddonInvoker.h30
-rw-r--r--xbmc/interfaces/python/LanguageHook.cpp231
-rw-r--r--xbmc/interfaces/python/LanguageHook.h94
-rw-r--r--xbmc/interfaces/python/MethodType.groovy14
-rw-r--r--xbmc/interfaces/python/PyContext.cpp117
-rw-r--r--xbmc/interfaces/python/PyContext.h49
-rw-r--r--xbmc/interfaces/python/PythonInvoker.cpp724
-rw-r--r--xbmc/interfaces/python/PythonInvoker.h76
-rw-r--r--xbmc/interfaces/python/PythonSwig.cpp.template942
-rw-r--r--xbmc/interfaces/python/PythonTools.groovy140
-rw-r--r--xbmc/interfaces/python/XBPython.cpp627
-rw-r--r--xbmc/interfaces/python/XBPython.h123
-rw-r--r--xbmc/interfaces/python/preamble.h15
-rw-r--r--xbmc/interfaces/python/pythreadstate.h64
-rw-r--r--xbmc/interfaces/python/swig.cpp443
-rw-r--r--xbmc/interfaces/python/swig.h202
-rw-r--r--xbmc/interfaces/python/test/CMakeLists.txt5
-rw-r--r--xbmc/interfaces/python/test/TestSwig.cpp21
-rw-r--r--xbmc/interfaces/python/typemaps/python.Alternative.intm43
-rw-r--r--xbmc/interfaces/python/typemaps/python.Alternative.outtm34
-rw-r--r--xbmc/interfaces/python/typemaps/python.Tuple.intm35
-rw-r--r--xbmc/interfaces/python/typemaps/python.Tuple.outtm39
-rw-r--r--xbmc/interfaces/python/typemaps/python.buffer.intm36
-rw-r--r--xbmc/interfaces/python/typemaps/python.buffer.outtm11
-rw-r--r--xbmc/interfaces/python/typemaps/python.dict.intm23
-rw-r--r--xbmc/interfaces/python/typemaps/python.map.intm24
-rw-r--r--xbmc/interfaces/python/typemaps/python.smart_ptr.outtm14
-rw-r--r--xbmc/interfaces/python/typemaps/python.string.outtm11
-rw-r--r--xbmc/interfaces/python/typemaps/python.vector.intm36
-rw-r--r--xbmc/interfaces/python/typemaps/python.vector.outtm35
-rw-r--r--xbmc/interfaces/swig/AddonModuleXbmc.i63
-rw-r--r--xbmc/interfaces/swig/AddonModuleXbmcaddon.i37
-rw-r--r--xbmc/interfaces/swig/AddonModuleXbmcdrm.i30
-rw-r--r--xbmc/interfaces/swig/AddonModuleXbmcgui.i111
-rw-r--r--xbmc/interfaces/swig/AddonModuleXbmcplugin.i32
-rw-r--r--xbmc/interfaces/swig/AddonModuleXbmcvfs.i44
-rw-r--r--xbmc/interfaces/swig/AddonModuleXbmcwsgi.i52
-rw-r--r--xbmc/interfaces/swig/CMakeLists.txt63
-rw-r--r--xbmc/listproviders/CMakeLists.txt11
-rw-r--r--xbmc/listproviders/DirectoryProvider.cpp586
-rw-r--r--xbmc/listproviders/DirectoryProvider.h102
-rw-r--r--xbmc/listproviders/IListProvider.cpp40
-rw-r--r--xbmc/listproviders/IListProvider.h116
-rw-r--r--xbmc/listproviders/MultiProvider.cpp123
-rw-r--r--xbmc/listproviders/MultiProvider.h45
-rw-r--r--xbmc/listproviders/StaticProvider.cpp137
-rw-r--r--xbmc/listproviders/StaticProvider.h39
-rw-r--r--xbmc/media/CMakeLists.txt6
-rw-r--r--xbmc/media/MediaLockState.h16
-rw-r--r--xbmc/media/MediaType.cpp136
-rw-r--r--xbmc/media/MediaType.h71
-rw-r--r--xbmc/media/decoderfilter/CMakeLists.txt5
-rw-r--r--xbmc/media/decoderfilter/DecoderFilterManager.cpp177
-rw-r--r--xbmc/media/decoderfilter/DecoderFilterManager.h140
-rw-r--r--xbmc/media/drm/CMakeLists.txt5
-rw-r--r--xbmc/media/drm/CryptoSession.cpp27
-rw-r--r--xbmc/media/drm/CryptoSession.h51
-rw-r--r--xbmc/messaging/ApplicationMessenger.cpp293
-rw-r--r--xbmc/messaging/ApplicationMessenger.h438
-rw-r--r--xbmc/messaging/CMakeLists.txt7
-rw-r--r--xbmc/messaging/IMessageTarget.h57
-rw-r--r--xbmc/messaging/ThreadMessage.h142
-rw-r--r--xbmc/messaging/helpers/CMakeLists.txt7
-rw-r--r--xbmc/messaging/helpers/DialogHelper.cpp94
-rw-r--r--xbmc/messaging/helpers/DialogHelper.h112
-rw-r--r--xbmc/messaging/helpers/DialogOKHelper.cpp59
-rw-r--r--xbmc/messaging/helpers/DialogOKHelper.h77
-rw-r--r--xbmc/music/Album.cpp671
-rw-r--r--xbmc/music/Album.h183
-rw-r--r--xbmc/music/Artist.cpp239
-rw-r--r--xbmc/music/Artist.h222
-rw-r--r--xbmc/music/CMakeLists.txt25
-rw-r--r--xbmc/music/ContextMenus.cpp129
-rw-r--r--xbmc/music/ContextMenus.h74
-rw-r--r--xbmc/music/GUIViewStateMusic.cpp648
-rw-r--r--xbmc/music/GUIViewStateMusic.h85
-rw-r--r--xbmc/music/MusicDatabase.cpp13822
-rw-r--r--xbmc/music/MusicDatabase.h1157
-rw-r--r--xbmc/music/MusicDbUrl.cpp170
-rw-r--r--xbmc/music/MusicDbUrl.h26
-rw-r--r--xbmc/music/MusicInfoLoader.cpp314
-rw-r--r--xbmc/music/MusicInfoLoader.h46
-rw-r--r--xbmc/music/MusicLibraryQueue.cpp301
-rw-r--r--xbmc/music/MusicLibraryQueue.h135
-rw-r--r--xbmc/music/MusicThumbLoader.cpp385
-rw-r--r--xbmc/music/MusicThumbLoader.h60
-rw-r--r--xbmc/music/MusicUtils.cpp841
-rw-r--r--xbmc/music/MusicUtils.h120
-rw-r--r--xbmc/music/Song.cpp373
-rw-r--r--xbmc/music/Song.h224
-rw-r--r--xbmc/music/dialogs/CMakeLists.txt13
-rw-r--r--xbmc/music/dialogs/GUIDialogInfoProviderSettings.cpp479
-rw-r--r--xbmc/music/dialogs/GUIDialogInfoProviderSettings.h92
-rw-r--r--xbmc/music/dialogs/GUIDialogMusicInfo.cpp1043
-rw-r--r--xbmc/music/dialogs/GUIDialogMusicInfo.h80
-rw-r--r--xbmc/music/dialogs/GUIDialogMusicOSD.cpp91
-rw-r--r--xbmc/music/dialogs/GUIDialogMusicOSD.h22
-rw-r--r--xbmc/music/dialogs/GUIDialogSongInfo.cpp523
-rw-r--r--xbmc/music/dialogs/GUIDialogSongInfo.h54
-rw-r--r--xbmc/music/dialogs/GUIDialogVisualisationPresetList.cpp98
-rw-r--r--xbmc/music/dialogs/GUIDialogVisualisationPresetList.h32
-rw-r--r--xbmc/music/infoscanner/CMakeLists.txt11
-rw-r--r--xbmc/music/infoscanner/MusicAlbumInfo.cpp52
-rw-r--r--xbmc/music/infoscanner/MusicAlbumInfo.h50
-rw-r--r--xbmc/music/infoscanner/MusicArtistInfo.cpp34
-rw-r--r--xbmc/music/infoscanner/MusicArtistInfo.h39
-rw-r--r--xbmc/music/infoscanner/MusicInfoScanner.cpp2338
-rw-r--r--xbmc/music/infoscanner/MusicInfoScanner.h265
-rw-r--r--xbmc/music/infoscanner/MusicInfoScraper.cpp202
-rw-r--r--xbmc/music/infoscanner/MusicInfoScraper.h82
-rw-r--r--xbmc/music/jobs/CMakeLists.txt15
-rw-r--r--xbmc/music/jobs/MusicLibraryCleaningJob.cpp40
-rw-r--r--xbmc/music/jobs/MusicLibraryCleaningJob.h38
-rw-r--r--xbmc/music/jobs/MusicLibraryExportJob.cpp43
-rw-r--r--xbmc/music/jobs/MusicLibraryExportJob.h42
-rw-r--r--xbmc/music/jobs/MusicLibraryImportJob.cpp42
-rw-r--r--xbmc/music/jobs/MusicLibraryImportJob.h42
-rw-r--r--xbmc/music/jobs/MusicLibraryJob.cpp24
-rw-r--r--xbmc/music/jobs/MusicLibraryJob.h50
-rw-r--r--xbmc/music/jobs/MusicLibraryProgressJob.cpp24
-rw-r--r--xbmc/music/jobs/MusicLibraryProgressJob.h29
-rw-r--r--xbmc/music/jobs/MusicLibraryScanningJob.cpp58
-rw-r--r--xbmc/music/jobs/MusicLibraryScanningJob.h50
-rw-r--r--xbmc/music/tags/CMakeLists.txt22
-rw-r--r--xbmc/music/tags/ImusicInfoTagLoader.h26
-rw-r--r--xbmc/music/tags/MusicInfoTag.cpp1303
-rw-r--r--xbmc/music/tags/MusicInfoTag.h263
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderCDDA.cpp156
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderCDDA.h23
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderDatabase.cpp38
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderDatabase.h24
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderFFmpeg.cpp168
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderFFmpeg.h23
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderFactory.cpp90
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderFactory.h26
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderShn.cpp38
-rw-r--r--xbmc/music/tags/MusicInfoTagLoaderShn.h24
-rw-r--r--xbmc/music/tags/ReplayGain.cpp144
-rw-r--r--xbmc/music/tags/ReplayGain.h51
-rw-r--r--xbmc/music/tags/TagLibVFSStream.cpp321
-rw-r--r--xbmc/music/tags/TagLibVFSStream.h122
-rw-r--r--xbmc/music/tags/TagLoaderTagLib.cpp1371
-rw-r--r--xbmc/music/tags/TagLoaderTagLib.h54
-rw-r--r--xbmc/music/tags/test/CMakeLists.txt3
-rw-r--r--xbmc/music/tags/test/TestTagLoaderTagLib.cpp230
-rw-r--r--xbmc/music/windows/CMakeLists.txt15
-rw-r--r--xbmc/music/windows/GUIWindowMusicBase.cpp1097
-rw-r--r--xbmc/music/windows/GUIWindowMusicBase.h106
-rw-r--r--xbmc/music/windows/GUIWindowMusicNav.cpp944
-rw-r--r--xbmc/music/windows/GUIWindowMusicNav.h50
-rw-r--r--xbmc/music/windows/GUIWindowMusicPlaylist.cpp715
-rw-r--r--xbmc/music/windows/GUIWindowMusicPlaylist.h44
-rw-r--r--xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp450
-rw-r--r--xbmc/music/windows/GUIWindowMusicPlaylistEditor.h57
-rw-r--r--xbmc/music/windows/GUIWindowVisualisation.cpp233
-rw-r--r--xbmc/music/windows/GUIWindowVisualisation.h31
-rw-r--r--xbmc/music/windows/MusicFileItemListModifier.cpp114
-rw-r--r--xbmc/music/windows/MusicFileItemListModifier.h24
-rw-r--r--xbmc/network/AirPlayServer.cpp1225
-rw-r--r--xbmc/network/AirPlayServer.h108
-rw-r--r--xbmc/network/AirTunesServer.cpp753
-rw-r--r--xbmc/network/AirTunesServer.h101
-rw-r--r--xbmc/network/CMakeLists.txt60
-rw-r--r--xbmc/network/DNSNameCache.cpp114
-rw-r--r--xbmc/network/DNSNameCache.h34
-rw-r--r--xbmc/network/EventClient.cpp788
-rw-r--r--xbmc/network/EventClient.h260
-rw-r--r--xbmc/network/EventPacket.cpp80
-rw-r--r--xbmc/network/EventPacket.h227
-rw-r--r--xbmc/network/EventServer.cpp352
-rw-r--r--xbmc/network/EventServer.h81
-rw-r--r--xbmc/network/GUIDialogNetworkSetup.cpp471
-rw-r--r--xbmc/network/GUIDialogNetworkSetup.h74
-rw-r--r--xbmc/network/IWSDiscovery.h25
-rw-r--r--xbmc/network/Network.cpp525
-rw-r--r--xbmc/network/Network.h123
-rw-r--r--xbmc/network/NetworkServices.cpp1267
-rw-r--r--xbmc/network/NetworkServices.h130
-rw-r--r--xbmc/network/Socket.cpp329
-rw-r--r--xbmc/network/Socket.h253
-rw-r--r--xbmc/network/TCPServer.cpp756
-rw-r--r--xbmc/network/TCPServer.h118
-rw-r--r--xbmc/network/UdpClient.cpp262
-rw-r--r--xbmc/network/UdpClient.h69
-rw-r--r--xbmc/network/WakeOnAccess.cpp956
-rw-r--r--xbmc/network/WakeOnAccess.h81
-rw-r--r--xbmc/network/WebServer.cpp1409
-rw-r--r--xbmc/network/WebServer.h131
-rw-r--r--xbmc/network/Zeroconf.cpp187
-rw-r--r--xbmc/network/Zeroconf.h138
-rw-r--r--xbmc/network/ZeroconfBrowser.cpp245
-rw-r--r--xbmc/network/ZeroconfBrowser.h174
-rw-r--r--xbmc/network/cddb.cpp1083
-rw-r--r--xbmc/network/cddb.h125
-rw-r--r--xbmc/network/dacp/CMakeLists.txt5
-rw-r--r--xbmc/network/dacp/dacp.cpp97
-rw-r--r--xbmc/network/dacp/dacp.h38
-rw-r--r--xbmc/network/httprequesthandler/CMakeLists.txt30
-rw-r--r--xbmc/network/httprequesthandler/HTTPFileHandler.cpp103
-rw-r--r--xbmc/network/httprequesthandler/HTTPFileHandler.h48
-rw-r--r--xbmc/network/httprequesthandler/HTTPImageHandler.cpp51
-rw-r--r--xbmc/network/httprequesthandler/HTTPImageHandler.h29
-rw-r--r--xbmc/network/httprequesthandler/HTTPImageTransformationHandler.cpp170
-rw-r--r--xbmc/network/httprequesthandler/HTTPImageTransformationHandler.h46
-rw-r--r--xbmc/network/httprequesthandler/HTTPJsonRpcHandler.cpp183
-rw-r--r--xbmc/network/httprequesthandler/HTTPJsonRpcHandler.h71
-rw-r--r--xbmc/network/httprequesthandler/HTTPPythonHandler.cpp250
-rw-r--r--xbmc/network/httprequesthandler/HTTPPythonHandler.h51
-rw-r--r--xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.cpp85
-rw-r--r--xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.h30
-rw-r--r--xbmc/network/httprequesthandler/HTTPVfsHandler.cpp106
-rw-r--r--xbmc/network/httprequesthandler/HTTPVfsHandler.h28
-rw-r--r--xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.cpp60
-rw-r--r--xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h38
-rw-r--r--xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.cpp132
-rw-r--r--xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.h32
-rw-r--r--xbmc/network/httprequesthandler/IHTTPRequestHandler.cpp152
-rw-r--r--xbmc/network/httprequesthandler/IHTTPRequestHandler.h247
-rw-r--r--xbmc/network/httprequesthandler/python/CMakeLists.txt10
-rw-r--r--xbmc/network/httprequesthandler/python/HTTPPythonInvoker.cpp73
-rw-r--r--xbmc/network/httprequesthandler/python/HTTPPythonInvoker.h32
-rw-r--r--xbmc/network/httprequesthandler/python/HTTPPythonRequest.h42
-rw-r--r--xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.cpp458
-rw-r--r--xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.h47
-rw-r--r--xbmc/network/mdns/CMakeLists.txt9
-rw-r--r--xbmc/network/mdns/ZeroconfBrowserMDNS.cpp420
-rw-r--r--xbmc/network/mdns/ZeroconfBrowserMDNS.h92
-rw-r--r--xbmc/network/mdns/ZeroconfMDNS.cpp245
-rw-r--r--xbmc/network/mdns/ZeroconfMDNS.h71
-rw-r--r--xbmc/network/test/CMakeLists.txt5
-rw-r--r--xbmc/network/test/TestWebServer.cpp926
-rw-r--r--xbmc/network/test/data/webserver/test-ranges.txt1
-rw-r--r--xbmc/network/test/data/webserver/test.html1
-rw-r--r--xbmc/network/test/data/webserver/test.pngbin0 -> 634 bytes
-rw-r--r--xbmc/network/upnp/CMakeLists.txt18
-rw-r--r--xbmc/network/upnp/UPnP.cpp891
-rw-r--r--xbmc/network/upnp/UPnP.h108
-rw-r--r--xbmc/network/upnp/UPnPInternal.cpp1245
-rw-r--r--xbmc/network/upnp/UPnPInternal.h122
-rw-r--r--xbmc/network/upnp/UPnPPlayer.cpp625
-rw-r--r--xbmc/network/upnp/UPnPPlayer.h77
-rw-r--r--xbmc/network/upnp/UPnPRenderer.cpp758
-rw-r--r--xbmc/network/upnp/UPnPRenderer.h73
-rw-r--r--xbmc/network/upnp/UPnPServer.cpp1388
-rw-r--r--xbmc/network/upnp/UPnPServer.h159
-rw-r--r--xbmc/network/upnp/UPnPSettings.cpp110
-rw-r--r--xbmc/network/upnp/UPnPSettings.h56
-rw-r--r--xbmc/network/websocket/CMakeLists.txt11
-rw-r--r--xbmc/network/websocket/WebSocket.cpp430
-rw-r--r--xbmc/network/websocket/WebSocket.h136
-rw-r--r--xbmc/network/websocket/WebSocketManager.cpp77
-rw-r--r--xbmc/network/websocket/WebSocketManager.h19
-rw-r--r--xbmc/network/websocket/WebSocketV13.cpp154
-rw-r--r--xbmc/network/websocket/WebSocketV13.h22
-rw-r--r--xbmc/network/websocket/WebSocketV8.cpp189
-rw-r--r--xbmc/network/websocket/WebSocketV8.h33
-rw-r--r--xbmc/peripherals/CMakeLists.txt13
-rw-r--r--xbmc/peripherals/EventLockHandle.cpp20
-rw-r--r--xbmc/peripherals/EventLockHandle.h48
-rw-r--r--xbmc/peripherals/EventPollHandle.cpp35
-rw-r--r--xbmc/peripherals/EventPollHandle.h68
-rw-r--r--xbmc/peripherals/EventScanner.cpp174
-rw-r--r--xbmc/peripherals/EventScanner.h76
-rw-r--r--xbmc/peripherals/IEventScannerCallback.h20
-rw-r--r--xbmc/peripherals/PeripheralTypes.h370
-rw-r--r--xbmc/peripherals/Peripherals.cpp1030
-rw-r--r--xbmc/peripherals/Peripherals.h371
-rw-r--r--xbmc/peripherals/addons/AddonButtonMap.cpp734
-rw-r--r--xbmc/peripherals/addons/AddonButtonMap.h146
-rw-r--r--xbmc/peripherals/addons/AddonButtonMapping.cpp139
-rw-r--r--xbmc/peripherals/addons/AddonButtonMapping.h72
-rw-r--r--xbmc/peripherals/addons/AddonInputHandling.cpp192
-rw-r--r--xbmc/peripherals/addons/AddonInputHandling.h108
-rw-r--r--xbmc/peripherals/addons/CMakeLists.txt13
-rw-r--r--xbmc/peripherals/addons/PeripheralAddon.cpp990
-rw-r--r--xbmc/peripherals/addons/PeripheralAddon.h178
-rw-r--r--xbmc/peripherals/addons/PeripheralAddonTranslator.cpp449
-rw-r--r--xbmc/peripherals/addons/PeripheralAddonTranslator.h62
-rw-r--r--xbmc/peripherals/bus/CMakeLists.txt6
-rw-r--r--xbmc/peripherals/bus/PeripheralBus.cpp352
-rw-r--r--xbmc/peripherals/bus/PeripheralBus.h221
-rw-r--r--xbmc/peripherals/bus/PeripheralBusUSB.h29
-rw-r--r--xbmc/peripherals/bus/virtual/CMakeLists.txt12
-rw-r--r--xbmc/peripherals/bus/virtual/PeripheralBusAddon.cpp502
-rw-r--r--xbmc/peripherals/bus/virtual/PeripheralBusAddon.h94
-rw-r--r--xbmc/peripherals/bus/virtual/PeripheralBusApplication.cpp83
-rw-r--r--xbmc/peripherals/bus/virtual/PeripheralBusApplication.h40
-rw-r--r--xbmc/peripherals/bus/virtual/PeripheralBusCEC.cpp57
-rw-r--r--xbmc/peripherals/bus/virtual/PeripheralBusCEC.h43
-rw-r--r--xbmc/peripherals/devices/CMakeLists.txt30
-rw-r--r--xbmc/peripherals/devices/Peripheral.cpp771
-rw-r--r--xbmc/peripherals/devices/Peripheral.h306
-rw-r--r--xbmc/peripherals/devices/PeripheralBluetooth.cpp19
-rw-r--r--xbmc/peripherals/devices/PeripheralBluetooth.h23
-rw-r--r--xbmc/peripherals/devices/PeripheralCecAdapter.cpp1861
-rw-r--r--xbmc/peripherals/devices/PeripheralCecAdapter.h226
-rw-r--r--xbmc/peripherals/devices/PeripheralDisk.cpp23
-rw-r--r--xbmc/peripherals/devices/PeripheralDisk.h23
-rw-r--r--xbmc/peripherals/devices/PeripheralHID.cpp86
-rw-r--r--xbmc/peripherals/devices/PeripheralHID.h33
-rw-r--r--xbmc/peripherals/devices/PeripheralImon.cpp90
-rw-r--r--xbmc/peripherals/devices/PeripheralImon.h43
-rw-r--r--xbmc/peripherals/devices/PeripheralJoystick.cpp538
-rw-r--r--xbmc/peripherals/devices/PeripheralJoystick.h150
-rw-r--r--xbmc/peripherals/devices/PeripheralKeyboard.cpp125
-rw-r--r--xbmc/peripherals/devices/PeripheralKeyboard.h52
-rw-r--r--xbmc/peripherals/devices/PeripheralMouse.cpp150
-rw-r--r--xbmc/peripherals/devices/PeripheralMouse.h53
-rw-r--r--xbmc/peripherals/devices/PeripheralNIC.cpp23
-rw-r--r--xbmc/peripherals/devices/PeripheralNIC.h23
-rw-r--r--xbmc/peripherals/devices/PeripheralNyxboard.cpp55
-rw-r--r--xbmc/peripherals/devices/PeripheralNyxboard.h24
-rw-r--r--xbmc/peripherals/devices/PeripheralTuner.cpp19
-rw-r--r--xbmc/peripherals/devices/PeripheralTuner.h23
-rw-r--r--xbmc/peripherals/dialogs/CMakeLists.txt7
-rw-r--r--xbmc/peripherals/dialogs/GUIDialogPeripheralSettings.cpp308
-rw-r--r--xbmc/peripherals/dialogs/GUIDialogPeripheralSettings.h52
-rw-r--r--xbmc/peripherals/dialogs/GUIDialogPeripherals.cpp186
-rw-r--r--xbmc/peripherals/dialogs/GUIDialogPeripherals.h51
-rw-r--r--xbmc/pictures/CMakeLists.txt27
-rw-r--r--xbmc/pictures/ExifParse.cpp958
-rw-r--r--xbmc/pictures/ExifParse.h37
-rw-r--r--xbmc/pictures/GUIDialogPictureInfo.cpp114
-rw-r--r--xbmc/pictures/GUIDialogPictureInfo.h32
-rw-r--r--xbmc/pictures/GUIViewStatePictures.cpp86
-rw-r--r--xbmc/pictures/GUIViewStatePictures.h25
-rw-r--r--xbmc/pictures/GUIWindowPictures.cpp625
-rw-r--r--xbmc/pictures/GUIWindowPictures.h51
-rw-r--r--xbmc/pictures/GUIWindowSlideShow.cpp1382
-rw-r--r--xbmc/pictures/GUIWindowSlideShow.h155
-rw-r--r--xbmc/pictures/IptcParse.cpp212
-rw-r--r--xbmc/pictures/IptcParse.h10
-rw-r--r--xbmc/pictures/JpegParse.cpp316
-rw-r--r--xbmc/pictures/JpegParse.h63
-rw-r--r--xbmc/pictures/Picture.cpp588
-rw-r--r--xbmc/pictures/Picture.h134
-rw-r--r--xbmc/pictures/PictureInfoLoader.cpp99
-rw-r--r--xbmc/pictures/PictureInfoLoader.h34
-rw-r--r--xbmc/pictures/PictureInfoTag.cpp764
-rw-r--r--xbmc/pictures/PictureInfoTag.h161
-rw-r--r--xbmc/pictures/PictureScalingAlgorithm.cpp65
-rw-r--r--xbmc/pictures/PictureScalingAlgorithm.h51
-rw-r--r--xbmc/pictures/PictureThumbLoader.cpp249
-rw-r--r--xbmc/pictures/PictureThumbLoader.h40
-rw-r--r--xbmc/pictures/SlideShowPicture.cpp1016
-rw-r--r--xbmc/pictures/SlideShowPicture.h139
-rw-r--r--xbmc/pictures/libexif.cpp35
-rw-r--r--xbmc/pictures/libexif.h138
-rw-r--r--xbmc/platform/CMakeLists.txt10
-rw-r--r--xbmc/platform/Environment.cpp75
-rw-r--r--xbmc/platform/Environment.h98
-rw-r--r--xbmc/platform/Filesystem.h33
-rw-r--r--xbmc/platform/MessagePrinter.h43
-rw-r--r--xbmc/platform/Platform.h101
-rw-r--r--xbmc/platform/common/speech/CMakeLists.txt5
-rw-r--r--xbmc/platform/common/speech/SpeechRecognitionStub.cpp15
-rw-r--r--xbmc/platform/common/speech/SpeechRecognitionStub.h22
-rw-r--r--xbmc/platform/freebsd/CMakeLists.txt28
-rw-r--r--xbmc/platform/freebsd/CPUInfoFreebsd.cpp268
-rw-r--r--xbmc/platform/freebsd/CPUInfoFreebsd.h24
-rw-r--r--xbmc/platform/freebsd/MemUtils.cpp83
-rw-r--r--xbmc/platform/freebsd/OptionalsReg.cpp23
-rw-r--r--xbmc/platform/freebsd/OptionalsReg.h18
-rw-r--r--xbmc/platform/freebsd/PlatformFreebsd.cpp124
-rw-r--r--xbmc/platform/freebsd/PlatformFreebsd.h26
-rw-r--r--xbmc/platform/freebsd/network/CMakeLists.txt4
-rw-r--r--xbmc/platform/freebsd/network/NetworkFreebsd.cpp247
-rw-r--r--xbmc/platform/freebsd/network/NetworkFreebsd.h40
-rw-r--r--xbmc/platform/linux/AppParamParserLinux.cpp93
-rw-r--r--xbmc/platform/linux/AppParamParserLinux.h22
-rw-r--r--xbmc/platform/linux/CMakeLists.txt32
-rw-r--r--xbmc/platform/linux/CPUInfoLinux.cpp379
-rw-r--r--xbmc/platform/linux/CPUInfoLinux.h30
-rw-r--r--xbmc/platform/linux/DBusMessage.cpp198
-rw-r--r--xbmc/platform/linux/DBusMessage.h149
-rw-r--r--xbmc/platform/linux/DBusUtil.cpp296
-rw-r--r--xbmc/platform/linux/DBusUtil.h88
-rw-r--r--xbmc/platform/linux/FDEventMonitor.cpp243
-rw-r--r--xbmc/platform/linux/FDEventMonitor.h74
-rw-r--r--xbmc/platform/linux/MemUtils.cpp79
-rw-r--r--xbmc/platform/linux/OptionalsReg.cpp114
-rw-r--r--xbmc/platform/linux/OptionalsReg.h59
-rw-r--r--xbmc/platform/linux/PlatformLinux.cpp163
-rw-r--r--xbmc/platform/linux/PlatformLinux.h30
-rw-r--r--xbmc/platform/linux/SysfsPath.cpp47
-rw-r--r--xbmc/platform/linux/SysfsPath.h58
-rw-r--r--xbmc/platform/linux/TimeUtils.cpp14
-rw-r--r--xbmc/platform/linux/TimeUtils.h28
-rw-r--r--xbmc/platform/linux/input/CMakeLists.txt27
-rw-r--r--xbmc/platform/linux/input/LIRC.cpp209
-rw-r--r--xbmc/platform/linux/input/LIRC.h35
-rw-r--r--xbmc/platform/linux/input/LibInputHandler.cpp301
-rw-r--r--xbmc/platform/linux/input/LibInputHandler.h56
-rw-r--r--xbmc/platform/linux/input/LibInputKeyboard.cpp387
-rw-r--r--xbmc/platform/linux/input/LibInputKeyboard.h48
-rw-r--r--xbmc/platform/linux/input/LibInputPointer.cpp153
-rw-r--r--xbmc/platform/linux/input/LibInputPointer.h32
-rw-r--r--xbmc/platform/linux/input/LibInputSettings.cpp169
-rw-r--r--xbmc/platform/linux/input/LibInputSettings.h35
-rw-r--r--xbmc/platform/linux/input/LibInputTouch.cpp109
-rw-r--r--xbmc/platform/linux/input/LibInputTouch.h40
-rw-r--r--xbmc/platform/linux/network/CMakeLists.txt4
-rw-r--r--xbmc/platform/linux/network/NetworkLinux.cpp223
-rw-r--r--xbmc/platform/linux/network/NetworkLinux.h40
-rw-r--r--xbmc/platform/linux/network/zeroconf/CMakeLists.txt8
-rw-r--r--xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.cpp434
-rw-r--r--xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.h79
-rw-r--r--xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.cpp383
-rw-r--r--xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.h94
-rw-r--r--xbmc/platform/linux/peripherals/CMakeLists.txt11
-rw-r--r--xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.cpp78
-rw-r--r--xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.h34
-rw-r--r--xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.cpp254
-rw-r--r--xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.h43
-rw-r--r--xbmc/platform/linux/powermanagement/CMakeLists.txt17
-rw-r--r--xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.cpp50
-rw-r--r--xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.h23
-rw-r--r--xbmc/platform/linux/powermanagement/FallbackPowerSyscall.h26
-rw-r--r--xbmc/platform/linux/powermanagement/LinuxPowerSyscall.cpp65
-rw-r--r--xbmc/platform/linux/powermanagement/LinuxPowerSyscall.h18
-rw-r--r--xbmc/platform/linux/powermanagement/LogindUPowerSyscall.cpp342
-rw-r--r--xbmc/platform/linux/powermanagement/LogindUPowerSyscall.h52
-rw-r--r--xbmc/platform/linux/powermanagement/UPowerSyscall.cpp207
-rw-r--r--xbmc/platform/linux/powermanagement/UPowerSyscall.h61
-rw-r--r--xbmc/platform/linux/sse4/CMakeLists.txt8
-rw-r--r--xbmc/platform/linux/sse4/CopyFrame.cpp102
-rw-r--r--xbmc/platform/linux/sse4/DllLibSSE4.h32
-rw-r--r--xbmc/platform/linux/storage/CMakeLists.txt19
-rw-r--r--xbmc/platform/linux/storage/LinuxStorageProvider.cpp91
-rw-r--r--xbmc/platform/linux/storage/LinuxStorageProvider.h31
-rw-r--r--xbmc/platform/linux/storage/UDevProvider.cpp298
-rw-r--r--xbmc/platform/linux/storage/UDevProvider.h42
-rw-r--r--xbmc/platform/linux/storage/UDisks2Provider.cpp861
-rw-r--r--xbmc/platform/linux/storage/UDisks2Provider.h210
-rw-r--r--xbmc/platform/linux/storage/UDisksProvider.cpp412
-rw-r--r--xbmc/platform/linux/storage/UDisksProvider.h131
-rw-r--r--xbmc/platform/linux/test/CMakeLists.txt3
-rw-r--r--xbmc/platform/linux/test/TestSysfsPath.cpp91
-rw-r--r--xbmc/platform/linux/threads/CMakeLists.txt4
-rw-r--r--xbmc/platform/linux/threads/ThreadImplLinux.cpp171
-rw-r--r--xbmc/platform/linux/threads/ThreadImplLinux.h26
-rw-r--r--xbmc/platform/posix/CMakeLists.txt21
-rw-r--r--xbmc/platform/posix/CPUInfoPosix.cpp60
-rw-r--r--xbmc/platform/posix/CPUInfoPosix.h23
-rw-r--r--xbmc/platform/posix/ConvUtils.cpp23
-rw-r--r--xbmc/platform/posix/ConvUtils.h15
-rw-r--r--xbmc/platform/posix/Filesystem.cpp128
-rw-r--r--xbmc/platform/posix/MessagePrinter.cpp40
-rw-r--r--xbmc/platform/posix/PlatformDefs.h153
-rw-r--r--xbmc/platform/posix/PlatformPosix.cpp63
-rw-r--r--xbmc/platform/posix/PlatformPosix.h25
-rw-r--r--xbmc/platform/posix/PosixMountProvider.cpp144
-rw-r--r--xbmc/platform/posix/PosixMountProvider.h37
-rw-r--r--xbmc/platform/posix/PosixResourceCounter.cpp71
-rw-r--r--xbmc/platform/posix/PosixResourceCounter.h31
-rw-r--r--xbmc/platform/posix/PosixTimezone.cpp272
-rw-r--r--xbmc/platform/posix/PosixTimezone.h58
-rw-r--r--xbmc/platform/posix/XHandle.cpp138
-rw-r--r--xbmc/platform/posix/XHandle.h59
-rw-r--r--xbmc/platform/posix/XHandlePublic.h19
-rw-r--r--xbmc/platform/posix/XTimeUtils.cpp228
-rw-r--r--xbmc/platform/posix/filesystem/CMakeLists.txt18
-rw-r--r--xbmc/platform/posix/filesystem/PosixDirectory.cpp207
-rw-r--r--xbmc/platform/posix/filesystem/PosixDirectory.h29
-rw-r--r--xbmc/platform/posix/filesystem/PosixFile.cpp388
-rw-r--r--xbmc/platform/posix/filesystem/PosixFile.h47
-rw-r--r--xbmc/platform/posix/filesystem/SMBDirectory.cpp372
-rw-r--r--xbmc/platform/posix/filesystem/SMBDirectory.h33
-rw-r--r--xbmc/platform/posix/filesystem/SMBFile.cpp721
-rw-r--r--xbmc/platform/posix/filesystem/SMBFile.h93
-rw-r--r--xbmc/platform/posix/filesystem/SMBWSDiscovery.cpp126
-rw-r--r--xbmc/platform/posix/filesystem/SMBWSDiscovery.h107
-rw-r--r--xbmc/platform/posix/filesystem/SMBWSDiscoveryListener.cpp608
-rw-r--r--xbmc/platform/posix/filesystem/SMBWSDiscoveryListener.h128
-rw-r--r--xbmc/platform/posix/main.cpp75
-rw-r--r--xbmc/platform/posix/network/CMakeLists.txt5
-rw-r--r--xbmc/platform/posix/network/NetworkPosix.cpp150
-rw-r--r--xbmc/platform/posix/network/NetworkPosix.h61
-rw-r--r--xbmc/platform/posix/storage/discs/CMakeLists.txt7
-rw-r--r--xbmc/platform/posix/storage/discs/DiscDriveHandlerPosix.cpp152
-rw-r--r--xbmc/platform/posix/storage/discs/DiscDriveHandlerPosix.h58
-rw-r--r--xbmc/platform/posix/threads/CMakeLists.txt9
-rw-r--r--xbmc/platform/posix/threads/RecursiveMutex.cpp43
-rw-r--r--xbmc/platform/posix/threads/RecursiveMutex.h48
-rw-r--r--xbmc/platform/posix/threads/ThreadImplPosix.cpp35
-rw-r--r--xbmc/platform/posix/threads/ThreadImplPosix.h23
-rw-r--r--xbmc/platform/posix/utils/CMakeLists.txt10
-rw-r--r--xbmc/platform/posix/utils/FileHandle.h31
-rw-r--r--xbmc/platform/posix/utils/Mmap.cpp27
-rw-r--r--xbmc/platform/posix/utils/Mmap.h53
-rw-r--r--xbmc/platform/posix/utils/PosixInterfaceForCLog.cpp29
-rw-r--r--xbmc/platform/posix/utils/PosixInterfaceForCLog.h22
-rw-r--r--xbmc/platform/posix/utils/SharedMemory.cpp118
-rw-r--r--xbmc/platform/posix/utils/SharedMemory.h62
-rw-r--r--xbmc/platform/xbmc.cpp79
-rw-r--r--xbmc/platform/xbmc.h15
-rw-r--r--xbmc/playlists/CMakeLists.txt26
-rw-r--r--xbmc/playlists/PlayList.cpp513
-rw-r--r--xbmc/playlists/PlayList.h92
-rw-r--r--xbmc/playlists/PlayListB4S.cpp133
-rw-r--r--xbmc/playlists/PlayListB4S.h25
-rw-r--r--xbmc/playlists/PlayListFactory.cpp145
-rw-r--r--xbmc/playlists/PlayListFactory.h29
-rw-r--r--xbmc/playlists/PlayListM3U.cpp278
-rw-r--r--xbmc/playlists/PlayListM3U.h38
-rw-r--r--xbmc/playlists/PlayListPLS.cpp425
-rw-r--r--xbmc/playlists/PlayListPLS.h44
-rw-r--r--xbmc/playlists/PlayListTypes.h31
-rw-r--r--xbmc/playlists/PlayListURL.cpp69
-rw-r--r--xbmc/playlists/PlayListURL.h23
-rw-r--r--xbmc/playlists/PlayListWPL.cpp130
-rw-r--r--xbmc/playlists/PlayListWPL.h25
-rw-r--r--xbmc/playlists/PlayListXML.cpp197
-rw-r--r--xbmc/playlists/PlayListXML.h24
-rw-r--r--xbmc/playlists/PlayListXSPF.cpp132
-rw-r--r--xbmc/playlists/PlayListXSPF.h24
-rw-r--r--xbmc/playlists/SmartPlayList.cpp1610
-rw-r--r--xbmc/playlists/SmartPlayList.h186
-rw-r--r--xbmc/playlists/SmartPlaylistFileItemListModifier.cpp54
-rw-r--r--xbmc/playlists/SmartPlaylistFileItemListModifier.h26
-rw-r--r--xbmc/playlists/test/CMakeLists.txt4
-rw-r--r--xbmc/playlists/test/TestPlayListFactory.cpp38
-rw-r--r--xbmc/playlists/test/TestPlayListXSPF.cpp79
-rw-r--r--xbmc/playlists/test/test.xspf54
-rw-r--r--xbmc/powermanagement/CMakeLists.txt14
-rw-r--r--xbmc/powermanagement/DPMSSupport.cpp44
-rw-r--r--xbmc/powermanagement/DPMSSupport.h56
-rw-r--r--xbmc/powermanagement/IPowerSyscall.cpp24
-rw-r--r--xbmc/powermanagement/IPowerSyscall.h111
-rw-r--r--xbmc/powermanagement/PowerManager.cpp315
-rw-r--r--xbmc/powermanagement/PowerManager.h69
-rw-r--r--xbmc/powermanagement/PowerTypes.h21
-rw-r--r--xbmc/powermanagement/WinIdleTimer.h18
-rw-r--r--xbmc/profiles/CMakeLists.txt7
-rw-r--r--xbmc/profiles/Profile.cpp126
-rw-r--r--xbmc/profiles/Profile.h98
-rw-r--r--xbmc/profiles/ProfileManager.cpp747
-rw-r--r--xbmc/profiles/ProfileManager.h222
-rw-r--r--xbmc/profiles/dialogs/CMakeLists.txt7
-rw-r--r--xbmc/profiles/dialogs/GUIDialogLockSettings.cpp320
-rw-r--r--xbmc/profiles/dialogs/GUIDialogLockSettings.h53
-rw-r--r--xbmc/profiles/dialogs/GUIDialogProfileSettings.cpp397
-rw-r--r--xbmc/profiles/dialogs/GUIDialogProfileSettings.h62
-rw-r--r--xbmc/profiles/windows/CMakeLists.txt5
-rw-r--r--xbmc/profiles/windows/GUIWindowSettingsProfile.cpp269
-rw-r--r--xbmc/profiles/windows/GUIWindowSettingsProfile.h35
-rw-r--r--xbmc/programs/CMakeLists.txt7
-rw-r--r--xbmc/programs/GUIViewStatePrograms.cpp72
-rw-r--r--xbmc/programs/GUIViewStatePrograms.h24
-rw-r--r--xbmc/programs/GUIWindowPrograms.cpp185
-rw-r--r--xbmc/programs/GUIWindowPrograms.h35
-rw-r--r--xbmc/pvr/CMakeLists.txt30
-rw-r--r--xbmc/pvr/IPVRComponent.h18
-rw-r--r--xbmc/pvr/PVRCachedImage.cpp71
-rw-r--r--xbmc/pvr/PVRCachedImage.h43
-rw-r--r--xbmc/pvr/PVRCachedImages.cpp94
-rw-r--r--xbmc/pvr/PVRCachedImages.h49
-rw-r--r--xbmc/pvr/PVRChannelNumberInputHandler.cpp190
-rw-r--r--xbmc/pvr/PVRChannelNumberInputHandler.h118
-rw-r--r--xbmc/pvr/PVRComponentRegistration.cpp52
-rw-r--r--xbmc/pvr/PVRComponentRegistration.h22
-rw-r--r--xbmc/pvr/PVRContextMenus.cpp780
-rw-r--r--xbmc/pvr/PVRContextMenus.h65
-rw-r--r--xbmc/pvr/PVRDatabase.cpp1141
-rw-r--r--xbmc/pvr/PVRDatabase.h320
-rw-r--r--xbmc/pvr/PVREdl.cpp68
-rw-r--r--xbmc/pvr/PVREdl.h34
-rw-r--r--xbmc/pvr/PVREventLogJob.cpp56
-rw-r--r--xbmc/pvr/PVREventLogJob.h62
-rw-r--r--xbmc/pvr/PVRItem.cpp167
-rw-r--r--xbmc/pvr/PVRItem.h41
-rw-r--r--xbmc/pvr/PVRManager.cpp1029
-rw-r--r--xbmc/pvr/PVRManager.h482
-rw-r--r--xbmc/pvr/PVRPlaybackState.cpp566
-rw-r--r--xbmc/pvr/PVRPlaybackState.h284
-rw-r--r--xbmc/pvr/PVRStreamProperties.cpp40
-rw-r--r--xbmc/pvr/PVRStreamProperties.h43
-rw-r--r--xbmc/pvr/PVRThumbLoader.cpp136
-rw-r--r--xbmc/pvr/PVRThumbLoader.h41
-rw-r--r--xbmc/pvr/addons/CMakeLists.txt13
-rw-r--r--xbmc/pvr/addons/PVRClient.cpp2023
-rw-r--r--xbmc/pvr/addons/PVRClient.h1062
-rw-r--r--xbmc/pvr/addons/PVRClientCapabilities.cpp85
-rw-r--r--xbmc/pvr/addons/PVRClientCapabilities.h277
-rw-r--r--xbmc/pvr/addons/PVRClientMenuHooks.cpp185
-rw-r--r--xbmc/pvr/addons/PVRClientMenuHooks.h73
-rw-r--r--xbmc/pvr/addons/PVRClientUID.cpp33
-rw-r--r--xbmc/pvr/addons/PVRClientUID.h42
-rw-r--r--xbmc/pvr/addons/PVRClients.cpp977
-rw-r--r--xbmc/pvr/addons/PVRClients.h485
-rw-r--r--xbmc/pvr/channels/CMakeLists.txt23
-rw-r--r--xbmc/pvr/channels/PVRChannel.cpp843
-rw-r--r--xbmc/pvr/channels/PVRChannel.h550
-rw-r--r--xbmc/pvr/channels/PVRChannelGroup.cpp1176
-rw-r--r--xbmc/pvr/channels/PVRChannelGroup.h548
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupInternal.cpp245
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupInternal.h116
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupMember.cpp137
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupMember.h101
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupSettings.cpp120
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupSettings.h62
-rw-r--r--xbmc/pvr/channels/PVRChannelGroups.cpp621
-rw-r--r--xbmc/pvr/channels/PVRChannelGroups.h244
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupsContainer.cpp167
-rw-r--r--xbmc/pvr/channels/PVRChannelGroupsContainer.h175
-rw-r--r--xbmc/pvr/channels/PVRChannelNumber.cpp36
-rw-r--r--xbmc/pvr/channels/PVRChannelNumber.h88
-rw-r--r--xbmc/pvr/channels/PVRChannelsPath.cpp190
-rw-r--r--xbmc/pvr/channels/PVRChannelsPath.h73
-rw-r--r--xbmc/pvr/channels/PVRRadioRDSInfoTag.cpp736
-rw-r--r--xbmc/pvr/channels/PVRRadioRDSInfoTag.h208
-rw-r--r--xbmc/pvr/channels/test/CMakeLists.txt4
-rw-r--r--xbmc/pvr/channels/test/TestPVRChannelsPath.cpp404
-rw-r--r--xbmc/pvr/dialogs/CMakeLists.txt29
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.cpp76
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.h34
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelManager.cpp1076
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelManager.h95
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.cpp299
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.h61
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.cpp102
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.h41
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGroupManager.cpp546
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGroupManager.h75
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideControls.cpp19
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideControls.h21
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.cpp255
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.h44
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.cpp391
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.h64
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.cpp154
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.h47
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.cpp216
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.h60
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.cpp102
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.h37
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.cpp235
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.h58
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.cpp1513
-rw-r--r--xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.h196
-rw-r--r--xbmc/pvr/epg/CMakeLists.txt22
-rw-r--r--xbmc/pvr/epg/Epg.cpp554
-rw-r--r--xbmc/pvr/epg/Epg.h327
-rw-r--r--xbmc/pvr/epg/EpgChannelData.cpp107
-rw-r--r--xbmc/pvr/epg/EpgChannelData.h59
-rw-r--r--xbmc/pvr/epg/EpgContainer.cpp1028
-rw-r--r--xbmc/pvr/epg/EpgContainer.h360
-rw-r--r--xbmc/pvr/epg/EpgDatabase.cpp1494
-rw-r--r--xbmc/pvr/epg/EpgDatabase.h377
-rw-r--r--xbmc/pvr/epg/EpgInfoTag.cpp681
-rw-r--r--xbmc/pvr/epg/EpgInfoTag.h513
-rw-r--r--xbmc/pvr/epg/EpgSearchData.h44
-rw-r--r--xbmc/pvr/epg/EpgSearchFilter.cpp403
-rw-r--r--xbmc/pvr/epg/EpgSearchFilter.h171
-rw-r--r--xbmc/pvr/epg/EpgSearchPath.cpp64
-rw-r--r--xbmc/pvr/epg/EpgSearchPath.h49
-rw-r--r--xbmc/pvr/epg/EpgTagsCache.cpp179
-rw-r--r--xbmc/pvr/epg/EpgTagsCache.h61
-rw-r--r--xbmc/pvr/epg/EpgTagsContainer.cpp668
-rw-r--r--xbmc/pvr/epg/EpgTagsContainer.h227
-rw-r--r--xbmc/pvr/filesystem/CMakeLists.txt5
-rw-r--r--xbmc/pvr/filesystem/PVRGUIDirectory.cpp625
-rw-r--r--xbmc/pvr/filesystem/PVRGUIDirectory.h107
-rw-r--r--xbmc/pvr/guilib/CMakeLists.txt35
-rw-r--r--xbmc/pvr/guilib/GUIEPGGridContainer.cpp2549
-rw-r--r--xbmc/pvr/guilib/GUIEPGGridContainer.dox245
-rw-r--r--xbmc/pvr/guilib/GUIEPGGridContainer.h274
-rw-r--r--xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp723
-rw-r--r--xbmc/pvr/guilib/GUIEPGGridContainerModel.h178
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionListener.cpp420
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionListener.h46
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsChannels.cpp424
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsChannels.h182
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsClients.cpp88
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsClients.h38
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp341
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsDatabase.h40
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsEPG.cpp217
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsEPG.h84
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp74
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsParentalControl.h59
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp564
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsPlayback.h150
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp190
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h55
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp361
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsRecordings.h114
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsTimers.cpp1009
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsTimers.h234
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsUtils.cpp36
-rw-r--r--xbmc/pvr/guilib/PVRGUIActionsUtils.h40
-rw-r--r--xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp101
-rw-r--r--xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h46
-rw-r--r--xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp373
-rw-r--r--xbmc/pvr/guilib/PVRGUIChannelNavigator.h189
-rw-r--r--xbmc/pvr/guilib/PVRGUIProgressHandler.cpp97
-rw-r--r--xbmc/pvr/guilib/PVRGUIProgressHandler.h59
-rw-r--r--xbmc/pvr/guilib/guiinfo/CMakeLists.txt9
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp2017
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h217
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp270
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h111
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp424
-rw-r--r--xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h87
-rw-r--r--xbmc/pvr/providers/CMakeLists.txt7
-rw-r--r--xbmc/pvr/providers/PVRProvider.cpp393
-rw-r--r--xbmc/pvr/providers/PVRProvider.h245
-rw-r--r--xbmc/pvr/providers/PVRProviders.cpp380
-rw-r--r--xbmc/pvr/providers/PVRProviders.h139
-rw-r--r--xbmc/pvr/recordings/CMakeLists.txt9
-rw-r--r--xbmc/pvr/recordings/PVRRecording.cpp730
-rw-r--r--xbmc/pvr/recordings/PVRRecording.h541
-rw-r--r--xbmc/pvr/recordings/PVRRecordings.cpp361
-rw-r--r--xbmc/pvr/recordings/PVRRecordings.h154
-rw-r--r--xbmc/pvr/recordings/PVRRecordingsPath.cpp258
-rw-r--r--xbmc/pvr/recordings/PVRRecordingsPath.h68
-rw-r--r--xbmc/pvr/settings/CMakeLists.txt5
-rw-r--r--xbmc/pvr/settings/PVRSettings.cpp232
-rw-r--r--xbmc/pvr/settings/PVRSettings.h76
-rw-r--r--xbmc/pvr/timers/CMakeLists.txt13
-rw-r--r--xbmc/pvr/timers/PVRTimerInfoTag.cpp1389
-rw-r--r--xbmc/pvr/timers/PVRTimerInfoTag.h644
-rw-r--r--xbmc/pvr/timers/PVRTimerRuleMatcher.cpp193
-rw-r--r--xbmc/pvr/timers/PVRTimerRuleMatcher.h47
-rw-r--r--xbmc/pvr/timers/PVRTimerType.cpp418
-rw-r--r--xbmc/pvr/timers/PVRTimerType.h411
-rw-r--r--xbmc/pvr/timers/PVRTimers.cpp1338
-rw-r--r--xbmc/pvr/timers/PVRTimers.h331
-rw-r--r--xbmc/pvr/timers/PVRTimersPath.cpp82
-rw-r--r--xbmc/pvr/timers/PVRTimersPath.h50
-rw-r--r--xbmc/pvr/windows/CMakeLists.txt21
-rw-r--r--xbmc/pvr/windows/GUIViewStatePVR.cpp182
-rw-r--r--xbmc/pvr/windows/GUIViewStatePVR.h78
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRBase.cpp563
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRBase.h138
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRChannels.cpp414
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRChannels.h65
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRGuide.cpp973
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRGuide.h129
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRRecordings.cpp440
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRRecordings.h71
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRSearch.cpp544
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRSearch.h90
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimerRules.cpp47
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimerRules.h38
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimers.cpp47
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimers.h36
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimersBase.cpp204
-rw-r--r--xbmc/pvr/windows/GUIWindowPVRTimersBase.h36
-rw-r--r--xbmc/rendering/CMakeLists.txt27
-rw-r--r--xbmc/rendering/MatrixGL.cpp261
-rw-r--r--xbmc/rendering/MatrixGL.h85
-rw-r--r--xbmc/rendering/MatrixGL.neon.cpp47
-rw-r--r--xbmc/rendering/RenderSystem.cpp110
-rw-r--r--xbmc/rendering/RenderSystem.h98
-rw-r--r--xbmc/rendering/RenderSystemTypes.h35
-rw-r--r--xbmc/rendering/dx/CMakeLists.txt11
-rw-r--r--xbmc/rendering/dx/DeviceResources.cpp1361
-rw-r--r--xbmc/rendering/dx/DeviceResources.h187
-rw-r--r--xbmc/rendering/dx/DirectXHelper.h198
-rw-r--r--xbmc/rendering/dx/RenderContext.h31
-rw-r--r--xbmc/rendering/dx/RenderSystemDX.cpp706
-rw-r--r--xbmc/rendering/dx/RenderSystemDX.h106
-rw-r--r--xbmc/rendering/dx/ScreenshotSurfaceWindows.cpp110
-rw-r--r--xbmc/rendering/dx/ScreenshotSurfaceWindows.h22
-rw-r--r--xbmc/rendering/gl/CMakeLists.txt12
-rw-r--r--xbmc/rendering/gl/GLShader.cpp162
-rw-r--r--xbmc/rendering/gl/GLShader.h55
-rw-r--r--xbmc/rendering/gl/RenderSystemGL.cpp795
-rw-r--r--xbmc/rendering/gl/RenderSystemGL.h139
-rw-r--r--xbmc/rendering/gl/ScreenshotSurfaceGL.cpp65
-rw-r--r--xbmc/rendering/gl/ScreenshotSurfaceGL.h22
-rw-r--r--xbmc/rendering/gles/CMakeLists.txt11
-rw-r--r--xbmc/rendering/gles/GLESShader.cpp178
-rw-r--r--xbmc/rendering/gles/GLESShader.h65
-rw-r--r--xbmc/rendering/gles/RenderSystemGLES.cpp669
-rw-r--r--xbmc/rendering/gles/RenderSystemGLES.h143
-rw-r--r--xbmc/rendering/gles/ScreenshotSurfaceGLES.cpp72
-rw-r--r--xbmc/rendering/gles/ScreenshotSurfaceGLES.h22
-rw-r--r--xbmc/settings/AdvancedSettings.cpp1559
-rw-r--r--xbmc/settings/AdvancedSettings.h391
-rw-r--r--xbmc/settings/CMakeLists.txt49
-rw-r--r--xbmc/settings/DiscSettings.cpp62
-rw-r--r--xbmc/settings/DiscSettings.h34
-rw-r--r--xbmc/settings/DisplaySettings.cpp948
-rw-r--r--xbmc/settings/DisplaySettings.h173
-rw-r--r--xbmc/settings/GameSettings.cpp63
-rw-r--r--xbmc/settings/GameSettings.h44
-rw-r--r--xbmc/settings/ISubSettings.h41
-rw-r--r--xbmc/settings/LibExportSettings.cpp115
-rw-r--r--xbmc/settings/LibExportSettings.h68
-rw-r--r--xbmc/settings/MediaSettings.cpp415
-rw-r--r--xbmc/settings/MediaSettings.h120
-rw-r--r--xbmc/settings/MediaSourceSettings.cpp500
-rw-r--r--xbmc/settings/MediaSourceSettings.h66
-rw-r--r--xbmc/settings/SettingAddon.cpp84
-rw-r--r--xbmc/settings/SettingAddon.h38
-rw-r--r--xbmc/settings/SettingConditions.cpp494
-rw-r--r--xbmc/settings/SettingConditions.h40
-rw-r--r--xbmc/settings/SettingControl.cpp378
-rw-r--r--xbmc/settings/SettingControl.h314
-rw-r--r--xbmc/settings/SettingCreator.cpp28
-rw-r--r--xbmc/settings/SettingCreator.h22
-rw-r--r--xbmc/settings/SettingDateTime.cpp88
-rw-r--r--xbmc/settings/SettingDateTime.h45
-rw-r--r--xbmc/settings/SettingPath.cpp145
-rw-r--r--xbmc/settings/SettingPath.h47
-rw-r--r--xbmc/settings/SettingUtils.cpp140
-rw-r--r--xbmc/settings/SettingUtils.h52
-rw-r--r--xbmc/settings/Settings.cpp1088
-rw-r--r--xbmc/settings/Settings.h598
-rw-r--r--xbmc/settings/SettingsBase.cpp273
-rw-r--r--xbmc/settings/SettingsBase.h279
-rw-r--r--xbmc/settings/SettingsComponent.cpp400
-rw-r--r--xbmc/settings/SettingsComponent.h89
-rw-r--r--xbmc/settings/SettingsValueFlatJsonSerializer.cpp139
-rw-r--r--xbmc/settings/SettingsValueFlatJsonSerializer.h40
-rw-r--r--xbmc/settings/SettingsValueXmlSerializer.cpp101
-rw-r--r--xbmc/settings/SettingsValueXmlSerializer.h36
-rw-r--r--xbmc/settings/SkinSettings.cpp186
-rw-r--r--xbmc/settings/SkinSettings.h62
-rw-r--r--xbmc/settings/SubtitlesSettings.cpp192
-rw-r--r--xbmc/settings/SubtitlesSettings.h211
-rw-r--r--xbmc/settings/dialogs/CMakeLists.txt13
-rw-r--r--xbmc/settings/dialogs/GUIDialogContentSettings.cpp428
-rw-r--r--xbmc/settings/dialogs/GUIDialogContentSettings.h91
-rw-r--r--xbmc/settings/dialogs/GUIDialogLibExportSettings.cpp452
-rw-r--r--xbmc/settings/dialogs/GUIDialogLibExportSettings.h60
-rw-r--r--xbmc/settings/dialogs/GUIDialogSettingsBase.cpp972
-rw-r--r--xbmc/settings/dialogs/GUIDialogSettingsBase.h189
-rw-r--r--xbmc/settings/dialogs/GUIDialogSettingsManagerBase.cpp64
-rw-r--r--xbmc/settings/dialogs/GUIDialogSettingsManagerBase.h34
-rw-r--r--xbmc/settings/dialogs/GUIDialogSettingsManualBase.cpp1617
-rw-r--r--xbmc/settings/dialogs/GUIDialogSettingsManualBase.h650
-rw-r--r--xbmc/settings/lib/CMakeLists.txt31
-rw-r--r--xbmc/settings/lib/ISetting.cpp76
-rw-r--r--xbmc/settings/lib/ISetting.h139
-rw-r--r--xbmc/settings/lib/ISettingCallback.h88
-rw-r--r--xbmc/settings/lib/ISettingControl.cpp56
-rw-r--r--xbmc/settings/lib/ISettingControl.h36
-rw-r--r--xbmc/settings/lib/ISettingControlCreator.h32
-rw-r--r--xbmc/settings/lib/ISettingCreator.h35
-rw-r--r--xbmc/settings/lib/ISettingsHandler.h58
-rw-r--r--xbmc/settings/lib/ISettingsValueSerializer.h21
-rw-r--r--xbmc/settings/lib/Setting.cpp1690
-rw-r--r--xbmc/settings/lib/Setting.h536
-rw-r--r--xbmc/settings/lib/SettingCategoryAccess.cpp41
-rw-r--r--xbmc/settings/lib/SettingCategoryAccess.h47
-rw-r--r--xbmc/settings/lib/SettingConditions.cpp153
-rw-r--r--xbmc/settings/lib/SettingConditions.h105
-rw-r--r--xbmc/settings/lib/SettingDefinitions.h138
-rw-r--r--xbmc/settings/lib/SettingDependency.cpp421
-rw-r--r--xbmc/settings/lib/SettingDependency.h135
-rw-r--r--xbmc/settings/lib/SettingLevel.h21
-rw-r--r--xbmc/settings/lib/SettingRequirement.cpp37
-rw-r--r--xbmc/settings/lib/SettingRequirement.h47
-rw-r--r--xbmc/settings/lib/SettingSection.cpp359
-rw-r--r--xbmc/settings/lib/SettingSection.h186
-rw-r--r--xbmc/settings/lib/SettingType.h23
-rw-r--r--xbmc/settings/lib/SettingUpdate.cpp65
-rw-r--r--xbmc/settings/lib/SettingUpdate.h46
-rw-r--r--xbmc/settings/lib/SettingsManager.cpp1424
-rw-r--r--xbmc/settings/lib/SettingsManager.h544
-rw-r--r--xbmc/settings/windows/CMakeLists.txt11
-rw-r--r--xbmc/settings/windows/GUIControlSettings.cpp1798
-rw-r--r--xbmc/settings/windows/GUIControlSettings.h366
-rw-r--r--xbmc/settings/windows/GUIWindowSettings.cpp19
-rw-r--r--xbmc/settings/windows/GUIWindowSettings.h19
-rw-r--r--xbmc/settings/windows/GUIWindowSettingsCategory.cpp244
-rw-r--r--xbmc/settings/windows/GUIWindowSettingsCategory.h51
-rw-r--r--xbmc/settings/windows/GUIWindowSettingsScreenCalibration.cpp630
-rw-r--r--xbmc/settings/windows/GUIWindowSettingsScreenCalibration.h47
-rw-r--r--xbmc/speech/CMakeLists.txt7
-rw-r--r--xbmc/speech/ISpeechRecognition.h28
-rw-r--r--xbmc/speech/ISpeechRecognitionListener.h26
-rw-r--r--xbmc/speech/SpeechRecognitionErrors.h28
-rw-r--r--xbmc/storage/AutorunMediaJob.cpp75
-rw-r--r--xbmc/storage/AutorunMediaJob.h25
-rw-r--r--xbmc/storage/CMakeLists.txt16
-rw-r--r--xbmc/storage/DetectDVDType.cpp446
-rw-r--r--xbmc/storage/DetectDVDType.h88
-rw-r--r--xbmc/storage/IStorageProvider.h95
-rw-r--r--xbmc/storage/MediaManager.cpp821
-rw-r--r--xbmc/storage/MediaManager.h147
-rw-r--r--xbmc/storage/cdioSupport.cpp958
-rw-r--r--xbmc/storage/cdioSupport.h347
-rw-r--r--xbmc/storage/discs/IDiscDriveHandler.h93
-rw-r--r--xbmc/system_egl.h51
-rw-r--r--xbmc/system_gl.h37
-rw-r--r--xbmc/test/CMakeLists.txt13
-rw-r--r--xbmc/test/MtTestUtils.h45
-rw-r--r--xbmc/test/TestBasicEnvironment.cpp122
-rw-r--r--xbmc/test/TestBasicEnvironment.h25
-rw-r--r--xbmc/test/TestDateTime.cpp660
-rw-r--r--xbmc/test/TestDateTimeSpan.cpp80
-rw-r--r--xbmc/test/TestFileItem.cpp132
-rw-r--r--xbmc/test/TestTextureUtils.cpp51
-rw-r--r--xbmc/test/TestURL.cpp72
-rw-r--r--xbmc/test/TestUtil.cpp87
-rw-r--r--xbmc/test/TestUtils.cpp324
-rw-r--r--xbmc/test/TestUtils.h102
-rw-r--r--xbmc/test/xbmc-test.cpp32
-rw-r--r--xbmc/threads/CMakeLists.txt17
-rw-r--r--xbmc/threads/Condition.h108
-rw-r--r--xbmc/threads/CriticalSection.h27
-rw-r--r--xbmc/threads/Event.cpp95
-rw-r--r--xbmc/threads/Event.h239
-rw-r--r--xbmc/threads/IRunnable.h17
-rw-r--r--xbmc/threads/IThreadImpl.h43
-rw-r--r--xbmc/threads/Lockables.h107
-rw-r--r--xbmc/threads/SharedSection.h54
-rw-r--r--xbmc/threads/SingleLock.h32
-rw-r--r--xbmc/threads/SystemClock.h96
-rw-r--r--xbmc/threads/Thread.cpp282
-rw-r--r--xbmc/threads/Thread.h125
-rw-r--r--xbmc/threads/Timer.cpp109
-rw-r--r--xbmc/threads/Timer.h51
-rw-r--r--xbmc/threads/test/CMakeLists.txt7
-rw-r--r--xbmc/threads/test/TestEndTime.cpp77
-rw-r--r--xbmc/threads/test/TestEvent.cpp628
-rw-r--r--xbmc/threads/test/TestHelpers.h75
-rw-r--r--xbmc/threads/test/TestSharedSection.cpp215
-rw-r--r--xbmc/utils/ActorProtocol.cpp376
-rw-r--r--xbmc/utils/ActorProtocol.h117
-rw-r--r--xbmc/utils/AlarmClock.cpp161
-rw-r--r--xbmc/utils/AlarmClock.h69
-rw-r--r--xbmc/utils/AliasShortcutUtils.cpp93
-rw-r--r--xbmc/utils/AliasShortcutUtils.h14
-rw-r--r--xbmc/utils/Archive.cpp463
-rw-r--r--xbmc/utils/Archive.h185
-rw-r--r--xbmc/utils/Base64.cpp128
-rw-r--r--xbmc/utils/Base64.h27
-rw-r--r--xbmc/utils/BitstreamConverter.cpp1237
-rw-r--r--xbmc/utils/BitstreamConverter.h144
-rw-r--r--xbmc/utils/BitstreamReader.cpp96
-rw-r--r--xbmc/utils/BitstreamReader.h49
-rw-r--r--xbmc/utils/BitstreamStats.cpp70
-rw-r--r--xbmc/utils/BitstreamStats.h40
-rw-r--r--xbmc/utils/BitstreamWriter.cpp113
-rw-r--r--xbmc/utils/BitstreamWriter.h50
-rw-r--r--xbmc/utils/BooleanLogic.cpp122
-rw-r--r--xbmc/utils/BooleanLogic.h90
-rw-r--r--xbmc/utils/BufferObject.cpp61
-rw-r--r--xbmc/utils/BufferObject.h44
-rw-r--r--xbmc/utils/BufferObjectFactory.cpp42
-rw-r--r--xbmc/utils/BufferObjectFactory.h48
-rw-r--r--xbmc/utils/CMakeLists.txt248
-rw-r--r--xbmc/utils/CPUInfo.cpp64
-rw-r--r--xbmc/utils/CPUInfo.h121
-rw-r--r--xbmc/utils/CSSUtils.cpp151
-rw-r--r--xbmc/utils/CSSUtils.h24
-rw-r--r--xbmc/utils/CharArrayParser.cpp169
-rw-r--r--xbmc/utils/CharArrayParser.h118
-rw-r--r--xbmc/utils/CharsetConverter.cpp910
-rw-r--r--xbmc/utils/CharsetConverter.h207
-rw-r--r--xbmc/utils/CharsetDetection.cpp642
-rw-r--r--xbmc/utils/CharsetDetection.h94
-rw-r--r--xbmc/utils/ColorUtils.cpp150
-rw-r--r--xbmc/utils/ColorUtils.h167
-rw-r--r--xbmc/utils/ComponentContainer.h78
-rw-r--r--xbmc/utils/ContentUtils.cpp62
-rw-r--r--xbmc/utils/ContentUtils.h38
-rw-r--r--xbmc/utils/Crc32.cpp110
-rw-r--r--xbmc/utils/Crc32.h31
-rw-r--r--xbmc/utils/DMAHeapBufferObject.cpp190
-rw-r--r--xbmc/utils/DMAHeapBufferObject.h38
-rw-r--r--xbmc/utils/DRMHelpers.cpp60
-rw-r--r--xbmc/utils/DRMHelpers.h21
-rw-r--r--xbmc/utils/DatabaseUtils.cpp797
-rw-r--r--xbmc/utils/DatabaseUtils.h187
-rw-r--r--xbmc/utils/Digest.cpp169
-rw-r--r--xbmc/utils/Digest.h138
-rw-r--r--xbmc/utils/DiscsUtils.cpp68
-rw-r--r--xbmc/utils/DiscsUtils.h73
-rw-r--r--xbmc/utils/DumbBufferObject.cpp168
-rw-r--r--xbmc/utils/DumbBufferObject.h38
-rw-r--r--xbmc/utils/EGLFence.cpp70
-rw-r--r--xbmc/utils/EGLFence.h44
-rw-r--r--xbmc/utils/EGLImage.cpp318
-rw-r--r--xbmc/utils/EGLImage.h66
-rw-r--r--xbmc/utils/EGLUtils.cpp598
-rw-r--r--xbmc/utils/EGLUtils.h232
-rw-r--r--xbmc/utils/EmbeddedArt.cpp72
-rw-r--r--xbmc/utils/EmbeddedArt.h49
-rw-r--r--xbmc/utils/EndianSwap.cpp30
-rw-r--r--xbmc/utils/EndianSwap.h90
-rw-r--r--xbmc/utils/EventStream.h100
-rw-r--r--xbmc/utils/EventStreamDetail.h70
-rw-r--r--xbmc/utils/ExecString.cpp244
-rw-r--r--xbmc/utils/ExecString.h46
-rw-r--r--xbmc/utils/Fanart.cpp176
-rw-r--r--xbmc/utils/Fanart.h107
-rw-r--r--xbmc/utils/FileExtensionProvider.cpp263
-rw-r--r--xbmc/utils/FileExtensionProvider.h91
-rw-r--r--xbmc/utils/FileOperationJob.cpp358
-rw-r--r--xbmc/utils/FileOperationJob.h91
-rw-r--r--xbmc/utils/FileUtils.cpp361
-rw-r--r--xbmc/utils/FileUtils.h34
-rw-r--r--xbmc/utils/FontUtils.cpp167
-rw-r--r--xbmc/utils/FontUtils.h70
-rw-r--r--xbmc/utils/GBMBufferObject.cpp100
-rw-r--r--xbmc/utils/GBMBufferObject.h48
-rw-r--r--xbmc/utils/GLUtils.cpp280
-rw-r--r--xbmc/utils/GLUtils.h54
-rw-r--r--xbmc/utils/Geometry.h484
-rw-r--r--xbmc/utils/GlobalsHandling.h202
-rw-r--r--xbmc/utils/GroupUtils.cpp157
-rw-r--r--xbmc/utils/GroupUtils.h32
-rw-r--r--xbmc/utils/HDRCapabilities.h32
-rw-r--r--xbmc/utils/HTMLUtil.cpp229
-rw-r--r--xbmc/utils/HTMLUtil.h23
-rw-r--r--xbmc/utils/HttpHeader.cpp239
-rw-r--r--xbmc/utils/HttpHeader.h53
-rw-r--r--xbmc/utils/HttpParser.cpp236
-rw-r--r--xbmc/utils/HttpParser.h98
-rw-r--r--xbmc/utils/HttpRangeUtils.cpp424
-rw-r--r--xbmc/utils/HttpRangeUtils.h187
-rw-r--r--xbmc/utils/HttpResponse.cpp166
-rw-r--r--xbmc/utils/HttpResponse.h125
-rw-r--r--xbmc/utils/IArchivable.h22
-rw-r--r--xbmc/utils/IBufferObject.h131
-rw-r--r--xbmc/utils/ILocalizer.h23
-rw-r--r--xbmc/utils/IPlatformLog.h40
-rw-r--r--xbmc/utils/IRssObserver.h25
-rw-r--r--xbmc/utils/IScreenshotSurface.h36
-rw-r--r--xbmc/utils/ISerializable.h21
-rw-r--r--xbmc/utils/ISortable.h23
-rw-r--r--xbmc/utils/IXmlDeserializable.h19
-rw-r--r--xbmc/utils/InfoLoader.cpp60
-rw-r--r--xbmc/utils/InfoLoader.h33
-rw-r--r--xbmc/utils/JSONVariantParser.cpp219
-rw-r--r--xbmc/utils/JSONVariantParser.h22
-rw-r--r--xbmc/utils/JSONVariantWriter.cpp92
-rw-r--r--xbmc/utils/JSONVariantWriter.h21
-rw-r--r--xbmc/utils/Job.h179
-rw-r--r--xbmc/utils/JobManager.cpp440
-rw-r--r--xbmc/utils/JobManager.h381
-rw-r--r--xbmc/utils/LabelFormatter.cpp479
-rw-r--r--xbmc/utils/LabelFormatter.h76
-rw-r--r--xbmc/utils/LangCodeExpander.cpp1792
-rw-r--r--xbmc/utils/LangCodeExpander.h185
-rw-r--r--xbmc/utils/LegacyPathTranslation.cpp105
-rw-r--r--xbmc/utils/LegacyPathTranslation.h47
-rw-r--r--xbmc/utils/Literals.h29
-rw-r--r--xbmc/utils/Locale.cpp284
-rw-r--r--xbmc/utils/Locale.h161
-rw-r--r--xbmc/utils/Map.h102
-rw-r--r--xbmc/utils/MathUtils.h239
-rw-r--r--xbmc/utils/MemUtils.h30
-rw-r--r--xbmc/utils/Mime.cpp700
-rw-r--r--xbmc/utils/Mime.h46
-rw-r--r--xbmc/utils/MovingSpeed.cpp122
-rw-r--r--xbmc/utils/MovingSpeed.h152
-rw-r--r--xbmc/utils/Observer.cpp71
-rw-r--r--xbmc/utils/Observer.h95
-rw-r--r--xbmc/utils/POUtils.cpp313
-rw-r--r--xbmc/utils/POUtils.h162
-rw-r--r--xbmc/utils/PlayerUtils.cpp39
-rw-r--r--xbmc/utils/PlayerUtils.h17
-rw-r--r--xbmc/utils/ProgressJob.cpp185
-rw-r--r--xbmc/utils/ProgressJob.h163
-rw-r--r--xbmc/utils/Random.h26
-rw-r--r--xbmc/utils/RecentlyAddedJob.cpp396
-rw-r--r--xbmc/utils/RecentlyAddedJob.h30
-rw-r--r--xbmc/utils/RegExp.cpp651
-rw-r--r--xbmc/utils/RegExp.h165
-rw-r--r--xbmc/utils/RingBuffer.cpp245
-rw-r--r--xbmc/utils/RingBuffer.h40
-rw-r--r--xbmc/utils/RssManager.cpp199
-rw-r--r--xbmc/utils/RssManager.h68
-rw-r--r--xbmc/utils/RssReader.cpp415
-rw-r--r--xbmc/utils/RssReader.h67
-rw-r--r--xbmc/utils/SaveFileStateJob.cpp235
-rw-r--r--xbmc/utils/SaveFileStateJob.h21
-rw-r--r--xbmc/utils/ScopeGuard.h111
-rw-r--r--xbmc/utils/ScraperParser.cpp615
-rw-r--r--xbmc/utils/ScraperParser.h78
-rw-r--r--xbmc/utils/ScraperUrl.cpp434
-rw-r--r--xbmc/utils/ScraperUrl.h123
-rw-r--r--xbmc/utils/Screenshot.cpp117
-rw-r--r--xbmc/utils/Screenshot.h28
-rw-r--r--xbmc/utils/SortUtils.cpp1385
-rw-r--r--xbmc/utils/SortUtils.h233
-rw-r--r--xbmc/utils/Speed.cpp582
-rw-r--r--xbmc/utils/Speed.h116
-rw-r--r--xbmc/utils/Stopwatch.h111
-rw-r--r--xbmc/utils/StreamDetails.cpp674
-rw-r--r--xbmc/utils/StreamDetails.h142
-rw-r--r--xbmc/utils/StreamUtils.cpp32
-rw-r--r--xbmc/utils/StreamUtils.h34
-rw-r--r--xbmc/utils/StringUtils.cpp1900
-rw-r--r--xbmc/utils/StringUtils.h434
-rw-r--r--xbmc/utils/StringValidation.cpp49
-rw-r--r--xbmc/utils/StringValidation.h25
-rw-r--r--xbmc/utils/SystemInfo.cpp1469
-rw-r--r--xbmc/utils/SystemInfo.h165
-rw-r--r--xbmc/utils/Temperature.cpp481
-rw-r--r--xbmc/utils/Temperature.h103
-rw-r--r--xbmc/utils/TextSearch.cpp146
-rw-r--r--xbmc/utils/TextSearch.h37
-rw-r--r--xbmc/utils/TimeFormat.h67
-rw-r--r--xbmc/utils/TimeUtils.cpp108
-rw-r--r--xbmc/utils/TimeUtils.h46
-rw-r--r--xbmc/utils/TransformMatrix.h296
-rw-r--r--xbmc/utils/UDMABufferObject.cpp204
-rw-r--r--xbmc/utils/UDMABufferObject.h39
-rw-r--r--xbmc/utils/URIUtils.cpp1493
-rw-r--r--xbmc/utils/URIUtils.h241
-rw-r--r--xbmc/utils/UrlOptions.cpp170
-rw-r--r--xbmc/utils/UrlOptions.h46
-rw-r--r--xbmc/utils/Utf8Utils.cpp148
-rw-r--r--xbmc/utils/Utf8Utils.h42
-rw-r--r--xbmc/utils/VC1BitstreamParser.cpp149
-rw-r--r--xbmc/utils/VC1BitstreamParser.h31
-rw-r--r--xbmc/utils/Variant.cpp885
-rw-r--r--xbmc/utils/Variant.h170
-rw-r--r--xbmc/utils/Vector.cpp32
-rw-r--r--xbmc/utils/Vector.h39
-rw-r--r--xbmc/utils/XBMCTinyXML.cpp273
-rw-r--r--xbmc/utils/XBMCTinyXML.h59
-rw-r--r--xbmc/utils/XMLUtils.cpp345
-rw-r--r--xbmc/utils/XMLUtils.h95
-rw-r--r--xbmc/utils/XSLTUtils.cpp103
-rw-r--r--xbmc/utils/XSLTUtils.h51
-rw-r--r--xbmc/utils/XTimeUtils.h88
-rw-r--r--xbmc/utils/log.cpp302
-rw-r--r--xbmc/utils/log.h165
-rw-r--r--xbmc/utils/logtypes.h18
-rw-r--r--xbmc/utils/params_check_macros.h62
-rw-r--r--xbmc/utils/rfft.cpp73
-rw-r--r--xbmc/utils/rfft.h39
-rw-r--r--xbmc/utils/test/CMakeLists.txt51
-rw-r--r--xbmc/utils/test/CXBMCTinyXML-test.xml6
-rw-r--r--xbmc/utils/test/TestAlarmClock.cpp25
-rw-r--r--xbmc/utils/test/TestAliasShortcutUtils.cpp91
-rw-r--r--xbmc/utils/test/TestArchive.cpp411
-rw-r--r--xbmc/utils/test/TestBase64.cpp77
-rw-r--r--xbmc/utils/test/TestBitstreamStats.cpp60
-rw-r--r--xbmc/utils/test/TestCPUInfo.cpp72
-rw-r--r--xbmc/utils/test/TestCharsetConverter.cpp401
-rw-r--r--xbmc/utils/test/TestComponentContainer.cpp76
-rw-r--r--xbmc/utils/test/TestCrc32.cpp50
-rw-r--r--xbmc/utils/test/TestDatabaseUtils.cpp1377
-rw-r--r--xbmc/utils/test/TestDigest.cpp99
-rw-r--r--xbmc/utils/test/TestEndianSwap.cpp133
-rw-r--r--xbmc/utils/test/TestExecString.cpp118
-rw-r--r--xbmc/utils/test/TestFileOperationJob.cpp292
-rw-r--r--xbmc/utils/test/TestFileUtils.cpp44
-rw-r--r--xbmc/utils/test/TestGlobalsHandling.cpp25
-rw-r--r--xbmc/utils/test/TestGlobalsHandlingPattern1.h40
-rw-r--r--xbmc/utils/test/TestHTMLUtil.cpp36
-rw-r--r--xbmc/utils/test/TestHttpHeader.cpp505
-rw-r--r--xbmc/utils/test/TestHttpParser.cpp49
-rw-r--r--xbmc/utils/test/TestHttpRangeUtils.cpp887
-rw-r--r--xbmc/utils/test/TestHttpResponse.cpp43
-rw-r--r--xbmc/utils/test/TestJSONVariantParser.cpp189
-rw-r--r--xbmc/utils/test/TestJSONVariantWriter.cpp151
-rw-r--r--xbmc/utils/test/TestJobManager.cpp218
-rw-r--r--xbmc/utils/test/TestLabelFormatter.cpp69
-rw-r--r--xbmc/utils/test/TestLangCodeExpander.cpp29
-rw-r--r--xbmc/utils/test/TestLocale.cpp272
-rw-r--r--xbmc/utils/test/TestMathUtils.cpp59
-rw-r--r--xbmc/utils/test/TestMime.cpp29
-rw-r--r--xbmc/utils/test/TestPOUtils.cpp47
-rw-r--r--xbmc/utils/test/TestRegExp.cpp169
-rw-r--r--xbmc/utils/test/TestRingBuffer.cpp33
-rw-r--r--xbmc/utils/test/TestScraperParser.cpp24
-rw-r--r--xbmc/utils/test/TestScraperUrl.cpp34
-rw-r--r--xbmc/utils/test/TestSortUtils.cpp123
-rw-r--r--xbmc/utils/test/TestStopwatch.cpp66
-rw-r--r--xbmc/utils/test/TestStreamDetails.cpp76
-rw-r--r--xbmc/utils/test/TestStreamUtils.cpp23
-rw-r--r--xbmc/utils/test/TestStringUtils.cpp609
-rw-r--r--xbmc/utils/test/TestSystemInfo.cpp326
-rw-r--r--xbmc/utils/test/TestURIUtils.cpp585
-rw-r--r--xbmc/utils/test/TestUrlOptions.cpp193
-rw-r--r--xbmc/utils/test/TestVariant.cpp334
-rw-r--r--xbmc/utils/test/TestXBMCTinyXML.cpp58
-rw-r--r--xbmc/utils/test/TestXMLUtils.cpp356
-rw-r--r--xbmc/utils/test/Testlog.cpp96
-rw-r--r--xbmc/utils/test/Testrfft.cpp41
-rw-r--r--xbmc/utils/test/data/language/Spanish/strings.po26
-rw-r--r--xbmc/video/Bookmark.cpp39
-rw-r--r--xbmc/video/Bookmark.h53
-rw-r--r--xbmc/video/CMakeLists.txt33
-rw-r--r--xbmc/video/ContextMenus.cpp421
-rw-r--r--xbmc/video/ContextMenus.h114
-rw-r--r--xbmc/video/Episode.h51
-rw-r--r--xbmc/video/GUIViewStateVideo.cpp636
-rw-r--r--xbmc/video/GUIViewStateVideo.h90
-rw-r--r--xbmc/video/PlayerController.cpp582
-rw-r--r--xbmc/video/PlayerController.h58
-rw-r--r--xbmc/video/Teletext.cpp4163
-rw-r--r--xbmc/video/Teletext.h197
-rw-r--r--xbmc/video/TeletextDefines.h478
-rw-r--r--xbmc/video/VideoDatabase.cpp11356
-rw-r--r--xbmc/video/VideoDatabase.h1154
-rw-r--r--xbmc/video/VideoDbUrl.cpp208
-rw-r--r--xbmc/video/VideoDbUrl.h29
-rw-r--r--xbmc/video/VideoInfoDownloader.cpp263
-rw-r--r--xbmc/video/VideoInfoDownloader.h87
-rw-r--r--xbmc/video/VideoInfoScanner.cpp2247
-rw-r--r--xbmc/video/VideoInfoScanner.h260
-rw-r--r--xbmc/video/VideoInfoTag.cpp1791
-rw-r--r--xbmc/video/VideoInfoTag.h307
-rw-r--r--xbmc/video/VideoLibraryQueue.cpp255
-rw-r--r--xbmc/video/VideoLibraryQueue.h166
-rw-r--r--xbmc/video/VideoThumbLoader.cpp803
-rw-r--r--xbmc/video/VideoThumbLoader.h135
-rw-r--r--xbmc/video/VideoUtils.cpp702
-rw-r--r--xbmc/video/VideoUtils.h69
-rw-r--r--xbmc/video/ViewModeSettings.cpp102
-rw-r--r--xbmc/video/ViewModeSettings.h46
-rw-r--r--xbmc/video/dialogs/CMakeLists.txt26
-rw-r--r--xbmc/video/dialogs/GUIDialogAudioSettings.cpp437
-rw-r--r--xbmc/video/dialogs/GUIDialogAudioSettings.h82
-rw-r--r--xbmc/video/dialogs/GUIDialogCMSSettings.cpp233
-rw-r--r--xbmc/video/dialogs/GUIDialogCMSSettings.h39
-rw-r--r--xbmc/video/dialogs/GUIDialogFullScreenInfo.cpp30
-rw-r--r--xbmc/video/dialogs/GUIDialogFullScreenInfo.h21
-rw-r--r--xbmc/video/dialogs/GUIDialogSubtitleSettings.cpp426
-rw-r--r--xbmc/video/dialogs/GUIDialogSubtitleSettings.h70
-rw-r--r--xbmc/video/dialogs/GUIDialogSubtitles.cpp703
-rw-r--r--xbmc/video/dialogs/GUIDialogSubtitles.h72
-rw-r--r--xbmc/video/dialogs/GUIDialogTeletext.cpp197
-rw-r--r--xbmc/video/dialogs/GUIDialogTeletext.h39
-rw-r--r--xbmc/video/dialogs/GUIDialogVideoBookmarks.cpp590
-rw-r--r--xbmc/video/dialogs/GUIDialogVideoBookmarks.h76
-rw-r--r--xbmc/video/dialogs/GUIDialogVideoInfo.cpp2398
-rw-r--r--xbmc/video/dialogs/GUIDialogVideoInfo.h115
-rw-r--r--xbmc/video/dialogs/GUIDialogVideoOSD.cpp101
-rw-r--r--xbmc/video/dialogs/GUIDialogVideoOSD.h25
-rw-r--r--xbmc/video/dialogs/GUIDialogVideoSettings.cpp573
-rw-r--r--xbmc/video/dialogs/GUIDialogVideoSettings.h55
-rw-r--r--xbmc/video/jobs/CMakeLists.txt17
-rw-r--r--xbmc/video/jobs/VideoLibraryCleaningJob.cpp45
-rw-r--r--xbmc/video/jobs/VideoLibraryCleaningJob.h51
-rw-r--r--xbmc/video/jobs/VideoLibraryJob.cpp24
-rw-r--r--xbmc/video/jobs/VideoLibraryJob.h51
-rw-r--r--xbmc/video/jobs/VideoLibraryMarkWatchedJob.cpp118
-rw-r--r--xbmc/video/jobs/VideoLibraryMarkWatchedJob.h41
-rw-r--r--xbmc/video/jobs/VideoLibraryProgressJob.cpp24
-rw-r--r--xbmc/video/jobs/VideoLibraryProgressJob.h29
-rw-r--r--xbmc/video/jobs/VideoLibraryRefreshingJob.cpp380
-rw-r--r--xbmc/video/jobs/VideoLibraryRefreshingJob.h55
-rw-r--r--xbmc/video/jobs/VideoLibraryResetResumePointJob.cpp87
-rw-r--r--xbmc/video/jobs/VideoLibraryResetResumePointJob.h38
-rw-r--r--xbmc/video/jobs/VideoLibraryScanningJob.cpp50
-rw-r--r--xbmc/video/jobs/VideoLibraryScanningJob.h52
-rw-r--r--xbmc/video/tags/CMakeLists.txt12
-rw-r--r--xbmc/video/tags/IVideoInfoTagLoader.h61
-rw-r--r--xbmc/video/tags/VideoInfoTagLoaderFactory.cpp50
-rw-r--r--xbmc/video/tags/VideoInfoTagLoaderFactory.h34
-rw-r--r--xbmc/video/tags/VideoTagLoaderFFmpeg.cpp284
-rw-r--r--xbmc/video/tags/VideoTagLoaderFFmpeg.h62
-rw-r--r--xbmc/video/tags/VideoTagLoaderNFO.cpp204
-rw-r--r--xbmc/video/tags/VideoTagLoaderNFO.h41
-rw-r--r--xbmc/video/tags/VideoTagLoaderPlugin.cpp66
-rw-r--r--xbmc/video/tags/VideoTagLoaderPlugin.h43
-rw-r--r--xbmc/video/test/CMakeLists.txt4
-rw-r--r--xbmc/video/test/TestStacks.cpp74
-rw-r--r--xbmc/video/test/TestVideoInfoScanner.cpp74
-rw-r--r--xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)A.mp40
-rw-r--r--xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)B.mp40
-rw-r--r--xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd1.iso0
-rw-r--r--xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd2.iso0
-rw-r--r--xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part1.mkv0
-rw-r--r--xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part2.mkv0
-rw-r--r--xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part3.mkv0
-rw-r--r--xbmc/video/windows/CMakeLists.txt14
-rw-r--r--xbmc/video/windows/GUIWindowFullScreen.cpp437
-rw-r--r--xbmc/video/windows/GUIWindowFullScreen.h45
-rw-r--r--xbmc/video/windows/GUIWindowFullScreenDefines.h14
-rw-r--r--xbmc/video/windows/GUIWindowVideoBase.cpp1589
-rw-r--r--xbmc/video/windows/GUIWindowVideoBase.h148
-rw-r--r--xbmc/video/windows/GUIWindowVideoNav.cpp1171
-rw-r--r--xbmc/video/windows/GUIWindowVideoNav.h68
-rw-r--r--xbmc/video/windows/GUIWindowVideoPlaylist.cpp609
-rw-r--r--xbmc/video/windows/GUIWindowVideoPlaylist.h43
-rw-r--r--xbmc/video/windows/VideoFileItemListModifier.cpp135
-rw-r--r--xbmc/video/windows/VideoFileItemListModifier.h24
-rw-r--r--xbmc/view/CMakeLists.txt12
-rw-r--r--xbmc/view/GUIViewControl.cpp354
-rw-r--r--xbmc/view/GUIViewControl.h67
-rw-r--r--xbmc/view/GUIViewState.cpp637
-rw-r--r--xbmc/view/GUIViewState.h126
-rw-r--r--xbmc/view/ViewDatabase.cpp208
-rw-r--r--xbmc/view/ViewDatabase.h34
-rw-r--r--xbmc/view/ViewState.h40
-rw-r--r--xbmc/view/ViewStateSettings.cpp271
-rw-r--r--xbmc/view/ViewStateSettings.h62
-rw-r--r--xbmc/weather/CMakeLists.txt9
-rw-r--r--xbmc/weather/GUIWindowWeather.cpp299
-rw-r--r--xbmc/weather/GUIWindowWeather.h31
-rw-r--r--xbmc/weather/WeatherJob.cpp243
-rw-r--r--xbmc/weather/WeatherJob.h63
-rw-r--r--xbmc/weather/WeatherManager.cpp189
-rw-r--r--xbmc/weather/WeatherManager.h108
-rw-r--r--xbmc/windowing/CMakeLists.txt16
-rw-r--r--xbmc/windowing/GraphicContext.cpp1011
-rw-r--r--xbmc/windowing/GraphicContext.h232
-rw-r--r--xbmc/windowing/OSScreenSaver.cpp98
-rw-r--r--xbmc/windowing/OSScreenSaver.h118
-rw-r--r--xbmc/windowing/Resolution.cpp474
-rw-r--r--xbmc/windowing/Resolution.h107
-rw-r--r--xbmc/windowing/VideoSync.h31
-rw-r--r--xbmc/windowing/WinEvents.h19
-rw-r--r--xbmc/windowing/WinSystem.cpp278
-rw-r--r--xbmc/windowing/WinSystem.h203
-rw-r--r--xbmc/windowing/WindowSystemFactory.cpp42
-rw-r--r--xbmc/windowing/WindowSystemFactory.h38
-rw-r--r--xbmc/windowing/X11/CMakeLists.txt37
-rw-r--r--xbmc/windowing/X11/GLContext.cpp20
-rw-r--r--xbmc/windowing/X11/GLContext.h42
-rw-r--r--xbmc/windowing/X11/GLContextEGL.cpp542
-rw-r--r--xbmc/windowing/X11/GLContextEGL.h63
-rw-r--r--xbmc/windowing/X11/GLContextGLX.cpp293
-rw-r--r--xbmc/windowing/X11/GLContextGLX.h49
-rw-r--r--xbmc/windowing/X11/OSScreenSaverX11.cpp36
-rw-r--r--xbmc/windowing/X11/OSScreenSaverX11.h28
-rw-r--r--xbmc/windowing/X11/OptionalsReg.cpp256
-rw-r--r--xbmc/windowing/X11/OptionalsReg.h78
-rw-r--r--xbmc/windowing/X11/VideoSyncGLX.cpp277
-rw-r--r--xbmc/windowing/X11/VideoSyncGLX.h62
-rw-r--r--xbmc/windowing/X11/VideoSyncOML.cpp83
-rw-r--r--xbmc/windowing/X11/VideoSyncOML.h46
-rw-r--r--xbmc/windowing/X11/WinEventsX11.cpp673
-rw-r--r--xbmc/windowing/X11/WinEventsX11.h60
-rw-r--r--xbmc/windowing/X11/WinSystemX11.cpp1081
-rw-r--r--xbmc/windowing/X11/WinSystemX11.h117
-rw-r--r--xbmc/windowing/X11/WinSystemX11GLContext.cpp358
-rw-r--r--xbmc/windowing/X11/WinSystemX11GLContext.h79
-rw-r--r--xbmc/windowing/X11/WinSystemX11GLESContext.cpp293
-rw-r--r--xbmc/windowing/X11/WinSystemX11GLESContext.h62
-rw-r--r--xbmc/windowing/X11/X11DPMSSupport.cpp96
-rw-r--r--xbmc/windowing/X11/X11DPMSSupport.h20
-rw-r--r--xbmc/windowing/X11/XRandR.cpp518
-rw-r--r--xbmc/windowing/X11/XRandR.h109
-rw-r--r--xbmc/windowing/XBMC_events.h134
-rw-r--r--xbmc/windowing/android/AndroidUtils.cpp440
-rw-r--r--xbmc/windowing/android/AndroidUtils.h56
-rw-r--r--xbmc/windowing/android/CMakeLists.txt18
-rw-r--r--xbmc/windowing/android/OSScreenSaverAndroid.cpp21
-rw-r--r--xbmc/windowing/android/OSScreenSaverAndroid.h19
-rw-r--r--xbmc/windowing/android/VideoSyncAndroid.cpp78
-rw-r--r--xbmc/windowing/android/VideoSyncAndroid.h34
-rw-r--r--xbmc/windowing/android/WinEventsAndroid.cpp191
-rw-r--r--xbmc/windowing/android/WinEventsAndroid.h43
-rw-r--r--xbmc/windowing/android/WinSystemAndroid.cpp329
-rw-r--r--xbmc/windowing/android/WinSystemAndroid.h85
-rw-r--r--xbmc/windowing/android/WinSystemAndroidGLESContext.cpp270
-rw-r--r--xbmc/windowing/android/WinSystemAndroidGLESContext.h63
-rw-r--r--xbmc/windowing/gbm/CMakeLists.txt26
-rw-r--r--xbmc/windowing/gbm/GBMDPMSSupport.cpp43
-rw-r--r--xbmc/windowing/gbm/GBMDPMSSupport.h22
-rw-r--r--xbmc/windowing/gbm/GBMUtils.cpp107
-rw-r--r--xbmc/windowing/gbm/GBMUtils.h177
-rw-r--r--xbmc/windowing/gbm/OptionalsReg.cpp139
-rw-r--r--xbmc/windowing/gbm/OptionalsReg.h36
-rw-r--r--xbmc/windowing/gbm/VideoLayerBridge.h27
-rw-r--r--xbmc/windowing/gbm/VideoSyncGbm.cpp131
-rw-r--r--xbmc/windowing/gbm/VideoSyncGbm.h43
-rw-r--r--xbmc/windowing/gbm/WinSystemGbm.cpp445
-rw-r--r--xbmc/windowing/gbm/WinSystemGbm.h96
-rw-r--r--xbmc/windowing/gbm/WinSystemGbmEGLContext.cpp141
-rw-r--r--xbmc/windowing/gbm/WinSystemGbmEGLContext.h58
-rw-r--r--xbmc/windowing/gbm/WinSystemGbmGLContext.cpp174
-rw-r--r--xbmc/windowing/gbm/WinSystemGbmGLContext.h48
-rw-r--r--xbmc/windowing/gbm/WinSystemGbmGLESContext.cpp167
-rw-r--r--xbmc/windowing/gbm/WinSystemGbmGLESContext.h48
-rw-r--r--xbmc/windowing/gbm/drm/CMakeLists.txt21
-rw-r--r--xbmc/windowing/gbm/drm/DRMAtomic.cpp344
-rw-r--r--xbmc/windowing/gbm/drm/DRMAtomic.h81
-rw-r--r--xbmc/windowing/gbm/drm/DRMConnector.cpp99
-rw-r--r--xbmc/windowing/gbm/drm/DRMConnector.h53
-rw-r--r--xbmc/windowing/gbm/drm/DRMCrtc.cpp26
-rw-r--r--xbmc/windowing/gbm/drm/DRMCrtc.h46
-rw-r--r--xbmc/windowing/gbm/drm/DRMEncoder.cpp23
-rw-r--r--xbmc/windowing/gbm/drm/DRMEncoder.h43
-rw-r--r--xbmc/windowing/gbm/drm/DRMLegacy.cpp141
-rw-r--r--xbmc/windowing/gbm/drm/DRMLegacy.h39
-rw-r--r--xbmc/windowing/gbm/drm/DRMObject.cpp133
-rw-r--r--xbmc/windowing/gbm/drm/DRMObject.h71
-rw-r--r--xbmc/windowing/gbm/drm/DRMPlane.cpp118
-rw-r--r--xbmc/windowing/gbm/drm/DRMPlane.h58
-rw-r--r--xbmc/windowing/gbm/drm/DRMUtils.cpp740
-rw-r--r--xbmc/windowing/gbm/drm/DRMUtils.h103
-rw-r--r--xbmc/windowing/gbm/drm/OffScreenModeSetting.cpp52
-rw-r--r--xbmc/windowing/gbm/drm/OffScreenModeSetting.h38
-rw-r--r--xbmc/windowing/ios/CMakeLists.txt8
-rw-r--r--xbmc/windowing/ios/VideoSyncIos.cpp95
-rw-r--r--xbmc/windowing/ios/VideoSyncIos.h43
-rw-r--r--xbmc/windowing/ios/WinEventsIOS.h20
-rw-r--r--xbmc/windowing/ios/WinEventsIOS.mm55
-rw-r--r--xbmc/windowing/ios/WinSystemIOS.h97
-rw-r--r--xbmc/windowing/ios/WinSystemIOS.mm501
-rw-r--r--xbmc/windowing/linux/CMakeLists.txt16
-rw-r--r--xbmc/windowing/linux/OSScreenSaverFreedesktop.cpp78
-rw-r--r--xbmc/windowing/linux/OSScreenSaverFreedesktop.h40
-rw-r--r--xbmc/windowing/linux/WinSystemEGL.cpp36
-rw-r--r--xbmc/windowing/linux/WinSystemEGL.h37
-rw-r--r--xbmc/windowing/osx/CMakeLists.txt17
-rw-r--r--xbmc/windowing/osx/CocoaDPMSSupport.cpp55
-rw-r--r--xbmc/windowing/osx/CocoaDPMSSupport.h20
-rw-r--r--xbmc/windowing/osx/OSScreenSaverOSX.cpp31
-rw-r--r--xbmc/windowing/osx/OSScreenSaverOSX.h24
-rw-r--r--xbmc/windowing/osx/OpenGL/CMakeLists.txt14
-rw-r--r--xbmc/windowing/osx/OpenGL/OSXGLView.h18
-rw-r--r--xbmc/windowing/osx/OpenGL/OSXGLView.mm111
-rw-r--r--xbmc/windowing/osx/OpenGL/OSXGLWindow.h19
-rw-r--r--xbmc/windowing/osx/OpenGL/OSXGLWindow.mm245
-rw-r--r--xbmc/windowing/osx/OpenGL/WinSystemOSXGL.h35
-rw-r--r--xbmc/windowing/osx/OpenGL/WinSystemOSXGL.mm79
-rw-r--r--xbmc/windowing/osx/SDL/CMakeLists.txt10
-rw-r--r--xbmc/windowing/osx/SDL/WinEventsSDL.cpp242
-rw-r--r--xbmc/windowing/osx/SDL/WinEventsSDL.h24
-rw-r--r--xbmc/windowing/osx/SDL/WinSystemOSXSDL.h111
-rw-r--r--xbmc/windowing/osx/SDL/WinSystemOSXSDL.mm1852
-rw-r--r--xbmc/windowing/osx/VideoSyncOsx.h46
-rw-r--r--xbmc/windowing/osx/VideoSyncOsx.mm148
-rw-r--r--xbmc/windowing/osx/WinEventsOSX.h34
-rw-r--r--xbmc/windowing/osx/WinEventsOSX.mm54
-rw-r--r--xbmc/windowing/osx/WinEventsOSXImpl.h25
-rw-r--r--xbmc/windowing/osx/WinEventsOSXImpl.mm364
-rw-r--r--xbmc/windowing/osx/WinSystemOSX.h120
-rw-r--r--xbmc/windowing/osx/WinSystemOSX.mm1305
-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.h44
-rw-r--r--xbmc/windowing/tvos/WinEventsTVOS.h35
-rw-r--r--xbmc/windowing/tvos/WinEventsTVOS.mm76
-rw-r--r--xbmc/windowing/tvos/WinSystemTVOS.h107
-rw-r--r--xbmc/windowing/tvos/WinSystemTVOS.mm453
-rw-r--r--xbmc/windowing/wayland/CMakeLists.txt67
-rw-r--r--xbmc/windowing/wayland/Connection.cpp39
-rw-r--r--xbmc/windowing/wayland/Connection.h39
-rw-r--r--xbmc/windowing/wayland/InputProcessorKeyboard.cpp170
-rw-r--r--xbmc/windowing/wayland/InputProcessorKeyboard.h78
-rw-r--r--xbmc/windowing/wayland/InputProcessorPointer.cpp136
-rw-r--r--xbmc/windowing/wayland/InputProcessorPointer.h76
-rw-r--r--xbmc/windowing/wayland/InputProcessorTouch.cpp130
-rw-r--r--xbmc/windowing/wayland/InputProcessorTouch.h82
-rw-r--r--xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.cpp52
-rw-r--r--xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.h39
-rw-r--r--xbmc/windowing/wayland/OptionalsReg.cpp136
-rw-r--r--xbmc/windowing/wayland/OptionalsReg.h37
-rw-r--r--xbmc/windowing/wayland/Output.cpp145
-rw-r--r--xbmc/windowing/wayland/Output.h152
-rw-r--r--xbmc/windowing/wayland/Registry.cpp164
-rw-r--r--xbmc/windowing/wayland/Registry.h162
-rw-r--r--xbmc/windowing/wayland/Seat.cpp275
-rw-r--r--xbmc/windowing/wayland/Seat.h203
-rw-r--r--xbmc/windowing/wayland/SeatInputProcessing.cpp83
-rw-r--r--xbmc/windowing/wayland/SeatInputProcessing.h135
-rw-r--r--xbmc/windowing/wayland/SeatSelection.cpp199
-rw-r--r--xbmc/windowing/wayland/SeatSelection.h49
-rw-r--r--xbmc/windowing/wayland/ShellSurface.cpp35
-rw-r--r--xbmc/windowing/wayland/ShellSurface.h92
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceWlShell.cpp101
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceWlShell.h63
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceXdgShell.cpp151
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceXdgShell.h73
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp152
-rw-r--r--xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h77
-rw-r--r--xbmc/windowing/wayland/Signals.h151
-rw-r--r--xbmc/windowing/wayland/Util.cpp88
-rw-r--r--xbmc/windowing/wayland/Util.h54
-rw-r--r--xbmc/windowing/wayland/VideoSyncWpPresentation.cpp83
-rw-r--r--xbmc/windowing/wayland/VideoSyncWpPresentation.h47
-rw-r--r--xbmc/windowing/wayland/WinEventsWayland.cpp277
-rw-r--r--xbmc/windowing/wayland/WinEventsWayland.h49
-rw-r--r--xbmc/windowing/wayland/WinSystemWayland.cpp1568
-rw-r--r--xbmc/windowing/wayland/WinSystemWayland.h300
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContext.cpp150
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContext.h55
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.cpp125
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.h47
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.cpp114
-rw-r--r--xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.h47
-rw-r--r--xbmc/windowing/wayland/WindowDecorationHandler.h41
-rw-r--r--xbmc/windowing/wayland/WindowDecorator.cpp1043
-rw-r--r--xbmc/windowing/wayland/WindowDecorator.h262
-rw-r--r--xbmc/windowing/wayland/XkbcommonKeymap.cpp333
-rw-r--r--xbmc/windowing/wayland/XkbcommonKeymap.h140
-rw-r--r--xbmc/windowing/win10/CMakeLists.txt12
-rw-r--r--xbmc/windowing/win10/WinEventsWin10.cpp658
-rw-r--r--xbmc/windowing/win10/WinEventsWin10.h83
-rw-r--r--xbmc/windowing/win10/WinSystemWin10.cpp660
-rw-r--r--xbmc/windowing/win10/WinSystemWin10.h156
-rw-r--r--xbmc/windowing/win10/WinSystemWin10DX.cpp205
-rw-r--r--xbmc/windowing/win10/WinSystemWin10DX.h90
-rw-r--r--xbmc/windowing/windows/CMakeLists.txt14
-rw-r--r--xbmc/windowing/windows/VideoSyncD3D.cpp137
-rw-r--r--xbmc/windowing/windows/VideoSyncD3D.h37
-rw-r--r--xbmc/windowing/windows/Win32DPMSSupport.cpp52
-rw-r--r--xbmc/windowing/windows/Win32DPMSSupport.h20
-rw-r--r--xbmc/windowing/windows/WinEventsWin32.cpp1074
-rw-r--r--xbmc/windowing/windows/WinEventsWin32.h32
-rw-r--r--xbmc/windowing/windows/WinKeyMap.h181
-rw-r--r--xbmc/windowing/windows/WinSystemWin32.cpp1368
-rw-r--r--xbmc/windowing/windows/WinSystemWin32.h215
-rw-r--r--xbmc/windowing/windows/WinSystemWin32DX.cpp447
-rw-r--r--xbmc/windowing/windows/WinSystemWin32DX.h96
-rw-r--r--xbmc/windows/CMakeLists.txt25
-rw-r--r--xbmc/windows/GUIMediaWindow.cpp2314
-rw-r--r--xbmc/windows/GUIMediaWindow.h227
-rw-r--r--xbmc/windows/GUIWindowDebugInfo.cpp185
-rw-r--r--xbmc/windows/GUIWindowDebugInfo.h34
-rw-r--r--xbmc/windows/GUIWindowFileManager.cpp1331
-rw-r--r--xbmc/windows/GUIWindowFileManager.h105
-rw-r--r--xbmc/windows/GUIWindowHome.cpp183
-rw-r--r--xbmc/windows/GUIWindowHome.h41
-rw-r--r--xbmc/windows/GUIWindowLoginScreen.cpp269
-rw-r--r--xbmc/windows/GUIWindowLoginScreen.h43
-rw-r--r--xbmc/windows/GUIWindowPointer.cpp82
-rw-r--r--xbmc/windows/GUIWindowPointer.h27
-rw-r--r--xbmc/windows/GUIWindowScreensaver.cpp106
-rw-r--r--xbmc/windows/GUIWindowScreensaver.h42
-rw-r--r--xbmc/windows/GUIWindowScreensaverDim.cpp77
-rw-r--r--xbmc/windows/GUIWindowScreensaverDim.h28
-rw-r--r--xbmc/windows/GUIWindowSplash.cpp45
-rw-r--r--xbmc/windows/GUIWindowSplash.h29
-rw-r--r--xbmc/windows/GUIWindowStartup.cpp38
-rw-r--r--xbmc/windows/GUIWindowStartup.h23
-rw-r--r--xbmc/windows/GUIWindowSystemInfo.cpp260
-rw-r--r--xbmc/windows/GUIWindowSystemInfo.h29
3904 files changed, 835066 insertions, 0 deletions
diff --git a/xbmc/AutoSwitch.cpp b/xbmc/AutoSwitch.cpp
new file mode 100644
index 0000000..14adb8e
--- /dev/null
+++ b/xbmc/AutoSwitch.cpp
@@ -0,0 +1,232 @@
+/*
+ * 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 "AutoSwitch.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "view/ViewState.h"
+
+#define METHOD_BYFOLDERS 0
+#define METHOD_BYFILES 1
+#define METHOD_BYTHUMBPERCENT 2
+#define METHOD_BYFILECOUNT 3
+#define METHOD_BYFOLDERTHUMBS 4
+
+CAutoSwitch::CAutoSwitch(void) = default;
+
+CAutoSwitch::~CAutoSwitch(void) = default;
+
+/// \brief Generic function to add a layer of transparency to the calling window
+/// \param vecItems Vector of FileItems passed from the calling window
+int CAutoSwitch::GetView(const CFileItemList &vecItems)
+{
+ int iSortMethod = -1;
+ int iPercent = 0;
+ int iCurrentWindow = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ bool bHideParentFolderItems = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWPARENTDIRITEMS);
+
+ switch (iCurrentWindow)
+ {
+ case WINDOW_PICTURES:
+ {
+ iSortMethod = METHOD_BYFILECOUNT;
+ }
+ break;
+
+ case WINDOW_PROGRAMS:
+ {
+ iSortMethod = METHOD_BYTHUMBPERCENT;
+ iPercent = 50; // 50% of thumbs -> use thumbs.
+ }
+ break;
+
+ default:
+ {
+ if (MetadataPercentage(vecItems) > 0.25f)
+ return DEFAULT_VIEW_INFO;
+ else
+ return DEFAULT_VIEW_LIST;
+ }
+ break;
+ }
+
+ bool bThumbs = false;
+
+ switch (iSortMethod)
+ {
+ case METHOD_BYFOLDERS:
+ bThumbs = ByFolders(vecItems);
+ break;
+
+ case METHOD_BYFILES:
+ bThumbs = ByFiles(bHideParentFolderItems, vecItems);
+ break;
+
+ case METHOD_BYTHUMBPERCENT:
+ bThumbs = ByThumbPercent(bHideParentFolderItems, iPercent, vecItems);
+ break;
+ case METHOD_BYFILECOUNT:
+ bThumbs = ByFileCount(vecItems);
+ break;
+ case METHOD_BYFOLDERTHUMBS:
+ bThumbs = ByFolderThumbPercentage(bHideParentFolderItems, iPercent, vecItems);
+ break;
+ }
+
+ // the GUIViewControl object will default down to small icons if a big icon
+ // view is not available.
+ return bThumbs ? DEFAULT_VIEW_BIG_ICONS : DEFAULT_VIEW_LIST;
+}
+
+/// \brief Auto Switch method based on the current directory \e containing ALL folders and \e atleast one non-default thumb
+/// \param vecItems Vector of FileItems
+bool CAutoSwitch::ByFolders(const CFileItemList& vecItems)
+{
+ bool bThumbs = false;
+ // is the list all folders?
+ if (vecItems.GetFolderCount() == vecItems.Size())
+ {
+ // test for thumbs
+ for (int i = 0; i < vecItems.Size(); i++)
+ {
+ const CFileItemPtr pItem = vecItems[i];
+ if (pItem->HasArt("thumb"))
+ {
+ bThumbs = true;
+ break;
+ }
+ }
+ }
+ return bThumbs;
+}
+
+/// \brief Auto Switch method based on the current directory \e not containing ALL files and \e atleast one non-default thumb
+/// \param bHideParentDirItems - are we not counting the ".." item?
+/// \param vecItems Vector of FileItems
+bool CAutoSwitch::ByFiles(bool bHideParentDirItems, const CFileItemList& vecItems)
+{
+ bool bThumbs = false;
+ int iCompare = 0;
+
+ // parent directories are visible, increment
+ if (!bHideParentDirItems)
+ {
+ iCompare = 1;
+ }
+
+ // confirm the list is not just files and folderback
+ if (vecItems.GetFolderCount() > iCompare)
+ {
+ // test for thumbs
+ for (int i = 0; i < vecItems.Size(); i++)
+ {
+ const CFileItemPtr pItem = vecItems[i];
+ if (pItem->HasArt("thumb"))
+ {
+ bThumbs = true;
+ break;
+ }
+ }
+ }
+ return bThumbs;
+}
+
+
+/// \brief Auto Switch method based on the percentage of non-default thumbs \e in the current directory
+/// \param iPercent Percent of non-default thumbs to autoswitch on
+/// \param vecItems Vector of FileItems
+bool CAutoSwitch::ByThumbPercent(bool bHideParentDirItems, int iPercent, const CFileItemList& vecItems)
+{
+ bool bThumbs = false;
+ int iNumThumbs = 0;
+ int iNumItems = vecItems.Size();
+ if (!bHideParentDirItems)
+ {
+ iNumItems--;
+ }
+
+ if (iNumItems <= 0) return false;
+
+ for (int i = 0; i < vecItems.Size(); i++)
+ {
+ const CFileItemPtr pItem = vecItems[i];
+ if (pItem->HasArt("thumb"))
+ {
+ iNumThumbs++;
+ float fTempPercent = ( (float)iNumThumbs / (float)iNumItems ) * (float)100;
+ if (fTempPercent >= (float)iPercent)
+ {
+ bThumbs = true;
+ break;
+ }
+ }
+ }
+
+ return bThumbs;
+}
+
+/// \brief Auto Switch method based on whether there is more than 25% files.
+/// \param iPercent Percent of non-default thumbs to autoswitch on
+bool CAutoSwitch::ByFileCount(const CFileItemList& vecItems)
+{
+ if (vecItems.Size() == 0) return false;
+ float fPercent = (float)vecItems.GetFileCount() / vecItems.Size();
+ return (fPercent > 0.25f);
+}
+
+// returns true if:
+// 1. Have more than 75% folders and
+// 2. Have more than percent folders with thumbs
+bool CAutoSwitch::ByFolderThumbPercentage(bool hideParentDirItems, int percent, const CFileItemList &vecItems)
+{
+ int numItems = vecItems.Size();
+ if (!hideParentDirItems)
+ numItems--;
+ if (numItems <= 0) return false;
+
+ int fileCount = vecItems.GetFileCount();
+ if (fileCount > 0.25f * numItems) return false;
+
+ int numThumbs = 0;
+ for (int i = 0; i < vecItems.Size(); i++)
+ {
+ const CFileItemPtr item = vecItems[i];
+ if (item->m_bIsFolder && item->HasArt("thumb"))
+ {
+ numThumbs++;
+ if (numThumbs >= 0.01f * percent * (numItems - fileCount))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+float CAutoSwitch::MetadataPercentage(const CFileItemList &vecItems)
+{
+ int count = 0;
+ int total = vecItems.Size();
+ for (int i = 0; i < vecItems.Size(); i++)
+ {
+ const CFileItemPtr item = vecItems[i];
+ if(item->HasMusicInfoTag()
+ || item->HasVideoInfoTag()
+ || item->HasPictureInfoTag()
+ || item->HasProperty("Addon.ID"))
+ count++;
+ if(item->IsParentFolder())
+ total--;
+ }
+ return (total != 0) ? ((float)count / total) : 0.0f;
+}
diff --git a/xbmc/AutoSwitch.h b/xbmc/AutoSwitch.h
new file mode 100644
index 0000000..a1b2d5d
--- /dev/null
+++ b/xbmc/AutoSwitch.h
@@ -0,0 +1,30 @@
+/*
+ * 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
+
+class CFileItemList;
+
+class CAutoSwitch
+{
+public:
+
+ CAutoSwitch(void);
+ virtual ~CAutoSwitch(void);
+
+ static int GetView(const CFileItemList& vecItems);
+
+ static bool ByFolders(const CFileItemList& vecItems);
+ static bool ByFiles(bool bHideParentDirItems, const CFileItemList& vecItems);
+ static bool ByThumbPercent(bool bHideParentDirItems, int iPercent, const CFileItemList& vecItems);
+ static bool ByFileCount(const CFileItemList& vecItems);
+ static bool ByFolderThumbPercentage(bool hideParentDirItems, int percent, const CFileItemList &vecItems);
+ static float MetadataPercentage(const CFileItemList &vecItems);
+protected:
+
+};
diff --git a/xbmc/Autorun.cpp b/xbmc/Autorun.cpp
new file mode 100644
index 0000000..43f6c8d
--- /dev/null
+++ b/xbmc/Autorun.cpp
@@ -0,0 +1,554 @@
+/*
+ * 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 "Autorun.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "filesystem/Directory.h"
+#include "filesystem/DirectoryFactory.h"
+#include "filesystem/StackDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "playlists/PlayList.h"
+#include "profiles/ProfileManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+
+#include <stdlib.h>
+#ifndef TARGET_WINDOWS
+#include "storage/DetectDVDType.h"
+#endif
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#ifdef HAS_CDDA_RIPPER
+#include "cdrip/CDDARipper.h"
+#endif
+
+using namespace XFILE;
+using namespace MEDIA_DETECT;
+using namespace KODI::MESSAGING;
+using namespace std::chrono_literals;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+CAutorun::CAutorun()
+{
+ m_bEnable = true;
+}
+
+CAutorun::~CAutorun() = default;
+
+bool CAutorun::ExecuteAutorun(const std::string& path)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_LOGIN_SCREEN)
+ return false;
+
+ CCdInfo* pInfo = CServiceBroker::GetMediaManager().GetCdInfo(path);
+
+ if ( pInfo == NULL )
+ return false;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+ appPower->WakeUpScreenSaverAndDPMS(); // turn off the screensaver if it's active
+
+ bool success = false;
+
+#ifdef HAS_CDDA_RIPPER
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_AUDIOCDS_AUTOACTION) == AUTOCD_RIP &&
+ pInfo->IsAudio(1) && !CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetCurrentProfile().musicLocked())
+ {
+ success = KODI::CDRIP::CCDDARipper::GetInstance().RipCD();
+ }
+ else
+#endif
+
+ success = PlayDisc(path, false, false);
+ return success;
+}
+
+bool CAutorun::PlayDisc(const std::string& path, bool bypassSettings, bool startFromBeginning)
+{
+ if ( !bypassSettings && CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_AUDIOCDS_AUTOACTION) != AUTOCD_PLAY && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_DVDS_AUTORUN))
+ return false;
+
+ int nSize = CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC).size();
+ int nAddedToPlaylist = 0;
+
+ std::string mediaPath;
+
+ CCdInfo* pInfo = CServiceBroker::GetMediaManager().GetCdInfo(path);
+ if (pInfo == NULL)
+ return false;
+
+ if (pInfo->IsAudio(1))
+ mediaPath = "cdda://local/";
+
+ if (mediaPath.empty() && (pInfo->IsISOUDF(1) || pInfo->IsISOHFS(1) || pInfo->IsIso9660(1) || pInfo->IsIso9660Interactive(1)))
+ mediaPath = "iso9660://";
+
+ if (mediaPath.empty())
+ mediaPath = path;
+
+ if (mediaPath.empty() || mediaPath == "iso9660://")
+ mediaPath = CServiceBroker::GetMediaManager().GetDiscPath();
+
+ const CURL pathToUrl(mediaPath);
+ std::unique_ptr<IDirectory> pDir ( CDirectoryFactory::Create( pathToUrl ));
+ bool bPlaying = RunDisc(pDir.get(), mediaPath, nAddedToPlaylist, true, bypassSettings, startFromBeginning);
+
+ if ( !bPlaying && nAddedToPlaylist > 0 )
+ {
+ CGUIMessage msg( GUI_MSG_PLAYLIST_CHANGED, 0, 0 );
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage( msg );
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+ // Start playing the items we inserted
+ return CServiceBroker::GetPlaylistPlayer().Play(nSize, "");
+ }
+
+ return bPlaying;
+}
+
+/**
+ * This method tries to determine what type of disc is located in the given drive and starts to play the content appropriately.
+ */
+bool CAutorun::RunDisc(IDirectory* pDir, const std::string& strDrive, int& nAddedToPlaylist, bool bRoot, bool bypassSettings /* = false */, bool startFromBeginning /* = false */)
+{
+ if (!pDir)
+ {
+ CLog::Log(LOGDEBUG, "CAutorun::{}: cannot run disc. is it properly mounted?", __FUNCTION__);
+ return false;
+ }
+
+ bool bPlaying(false);
+ CFileItemList vecItems;
+
+ CURL pathToUrl{strDrive};
+ // if the url being requested is a generic "iso9660://" we need to expand it with the current drive device.
+ // use the hostname section to prepend the drive path
+ if (pathToUrl.GetRedacted() == "iso9660://")
+ {
+ pathToUrl.Reset();
+ pathToUrl.SetProtocol("iso9660");
+ pathToUrl.SetHostName(CServiceBroker::GetMediaManager().TranslateDevicePath(""));
+ }
+
+ if ( !pDir->GetDirectory( pathToUrl, vecItems ) )
+ {
+ return false;
+ }
+
+ // Sorting necessary for easier HDDVD handling
+ vecItems.Sort(SortByLabel, SortOrderAscending);
+
+ bool bAllowVideo = true;
+// bool bAllowPictures = true;
+ bool bAllowMusic = true;
+ if (!g_passwordManager.IsMasterLockUnlocked(false))
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ bAllowVideo = !profileManager->GetCurrentProfile().videoLocked();
+// bAllowPictures = !profileManager->GetCurrentProfile().picturesLocked();
+ bAllowMusic = !profileManager->GetCurrentProfile().musicLocked();
+ }
+
+ // is this a root folder we have to check the content to determine a disc type
+ if (bRoot)
+ {
+ std::string hddvdname = "";
+ CFileItemPtr phddvdItem;
+ bool bAutorunDVDs = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_DVDS_AUTORUN);
+
+ // check root folders next, for normal structured dvd's
+ for (const auto& pItem : vecItems)
+ {
+ // is the current item a (non system) folder?
+ if (pItem->m_bIsFolder && pItem->GetPath() != "." && pItem->GetPath() != "..")
+ {
+ std::string name = pItem->GetPath();
+ URIUtils::RemoveSlashAtEnd(name);
+ name = URIUtils::GetFileName(name);
+
+ // Check if the current foldername indicates a DVD structure (name is "VIDEO_TS")
+ if (StringUtils::EqualsNoCase(name, "VIDEO_TS") && bAllowVideo
+ && (bypassSettings || bAutorunDVDs))
+ {
+ std::string path = URIUtils::AddFileToFolder(pItem->GetPath(), "VIDEO_TS.IFO");
+ if (!CFileUtils::Exists(path))
+ path = URIUtils::AddFileToFolder(pItem->GetPath(), "video_ts.ifo");
+ CFileItemPtr item(new CFileItem(path, false));
+ item->SetLabel(CServiceBroker::GetMediaManager().GetDiskLabel(strDrive));
+ item->GetVideoInfoTag()->m_strFileNameAndPath =
+ CServiceBroker::GetMediaManager().GetDiskUniqueId(strDrive);
+
+ if (!startFromBeginning && !item->GetVideoInfoTag()->m_strFileNameAndPath.empty())
+ item->SetStartOffset(STARTOFFSET_RESUME);
+
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(PLAYLIST::TYPE_VIDEO, false);
+ CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_VIDEO, item);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().Play(0, "");
+ return true;
+ }
+
+ // Check if the current foldername indicates a Blu-Ray structure (default is "BDMV").
+ // A BR should also include an "AACS" folder for encryption, Sony-BRs can also include update folders for PS3 (PS3_UPDATE / PS3_VPRM).
+ //! @todo for the time being, the DVD autorun settings are used to determine if the BR should be started automatically.
+ if (StringUtils::EqualsNoCase(name, "BDMV") && bAllowVideo
+ && (bypassSettings || bAutorunDVDs))
+ {
+ CFileItemPtr item(new CFileItem(URIUtils::AddFileToFolder(pItem->GetPath(), "index.bdmv"), false));
+ item->SetLabel(CServiceBroker::GetMediaManager().GetDiskLabel(strDrive));
+ item->GetVideoInfoTag()->m_strFileNameAndPath =
+ CServiceBroker::GetMediaManager().GetDiskUniqueId(strDrive);
+
+ if (!startFromBeginning && !item->GetVideoInfoTag()->m_strFileNameAndPath.empty())
+ item->SetStartOffset(STARTOFFSET_RESUME);
+
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(PLAYLIST::TYPE_VIDEO, false);
+ CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_VIDEO, item);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().Play(0, "");
+ return true;
+ }
+
+ // Check if the current foldername indicates a HD DVD structure (default is "HVDVD_TS").
+ // Most HD DVD will also include an "ADV_OBJ" folder for advanced content. This folder should be handled first.
+ //! @todo for the time being, the DVD autorun settings are used to determine if the HD DVD should be started automatically.
+ CFileItemList items, sitems;
+
+ // Advanced Content HD DVD (most discs?)
+ if (StringUtils::EqualsNoCase(name, "ADV_OBJ"))
+ {
+ CLog::Log(LOGINFO,"HD DVD: Checking for playlist.");
+ // find playlist file
+ CDirectory::GetDirectory(pItem->GetPath(), items, "*.xpl", DIR_FLAG_DEFAULTS);
+ if (items.Size())
+ {
+ // HD DVD Standard says the highest numbered playlist has to be handled first.
+ CLog::Log(LOGINFO,"HD DVD: Playlist found. Set filetypes to *.xpl for external player.");
+ items.Sort(SortByLabel, SortOrderDescending);
+ phddvdItem = pItem;
+ hddvdname = URIUtils::GetFileName(items[0]->GetPath());
+ CLog::Log(LOGINFO, "HD DVD: {}", items[0]->GetPath());
+ }
+ }
+
+ // Standard Content HD DVD (few discs?)
+ if (StringUtils::EqualsNoCase(name, "HVDVD_TS") && bAllowVideo
+ && (bypassSettings || bAutorunDVDs))
+ {
+ if (hddvdname == "")
+ {
+ CLog::Log(LOGINFO,"HD DVD: Checking for ifo.");
+ // find Video Manager or Title Set Information
+ CDirectory::GetDirectory(pItem->GetPath(), items, "HV*.ifo", DIR_FLAG_DEFAULTS);
+ if (items.Size())
+ {
+ // HD DVD Standard says the lowest numbered ifo has to be handled first.
+ CLog::Log(LOGINFO,"HD DVD: IFO found. Set filename to HV* and filetypes to *.ifo for external player.");
+ items.Sort(SortByLabel, SortOrderAscending);
+ phddvdItem = pItem;
+ hddvdname = URIUtils::GetFileName(items[0]->GetPath());
+ CLog::Log(LOGINFO, "HD DVD: {}", items[0]->GetPath());
+ }
+ }
+ // Find and sort *.evo files for internal playback.
+ // While this algorithm works for all of my HD DVDs, it may fail on other discs. If there are very large extras which are
+ // alphabetically before the main movie they will be sorted to the top of the playlist and get played first.
+ CDirectory::GetDirectory(pItem->GetPath(), items, "*.evo", DIR_FLAG_DEFAULTS);
+ if (items.Size())
+ {
+ // Sort *.evo files in alphabetical order.
+ items.Sort(SortByLabel, SortOrderAscending);
+ int64_t asize = 0;
+ int ecount = 0;
+ // calculate average size of elements above 1gb
+ for (int j = 0; j < items.Size(); j++)
+ if (items[j]->m_dwSize > 1000000000)
+ {
+ ecount++;
+ asize = asize + items[j]->m_dwSize;
+ }
+ if (ecount > 0)
+ asize = asize / ecount;
+ // Put largest files in alphabetical order to top of new list.
+ for (int j = 0; j < items.Size(); j++)
+ if (items[j]->m_dwSize >= asize)
+ sitems.Add (items[j]);
+ // Sort *.evo files by size.
+ items.Sort(SortBySize, SortOrderDescending);
+ // Add other files with descending size to bottom of new list.
+ for (int j = 0; j < items.Size(); j++)
+ if (items[j]->m_dwSize < asize)
+ sitems.Add (items[j]);
+ // Replace list with optimized list.
+ items.Clear();
+ items.Copy (sitems);
+ sitems.Clear();
+ }
+ if (hddvdname != "")
+ {
+ CFileItem item(URIUtils::AddFileToFolder(phddvdItem->GetPath(), hddvdname), false);
+ item.SetLabel(CServiceBroker::GetMediaManager().GetDiskLabel(strDrive));
+ item.GetVideoInfoTag()->m_strFileNameAndPath =
+ CServiceBroker::GetMediaManager().GetDiskUniqueId(strDrive);
+
+ if (!startFromBeginning && !item.GetVideoInfoTag()->m_strFileNameAndPath.empty())
+ item.SetStartOffset(STARTOFFSET_RESUME);
+
+ // get playername
+ std::string hdVideoPlayer = CServiceBroker::GetPlayerCoreFactory().GetDefaultPlayer(item);
+
+ // Single *.xpl or *.ifo files require an external player to handle playback.
+ // If no matching rule was found, VideoPlayer will be default player.
+ if (hdVideoPlayer != "VideoPlayer")
+ {
+ CLog::Log(LOGINFO, "HD DVD: External singlefile playback initiated: {}", hddvdname);
+ g_application.PlayFile(item, hdVideoPlayer, false);
+ return true;
+ } else
+ CLog::Log(LOGINFO,"HD DVD: No external player found. Fallback to internal one.");
+ }
+
+ // internal *.evo playback.
+ CLog::Log(LOGINFO,"HD DVD: Internal multifile playback initiated.");
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(PLAYLIST::TYPE_VIDEO, false);
+ CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_VIDEO, items);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().Play(0, "");
+ return true;
+ }
+
+ // Video CDs can have multiple file formats. First we need to determine which one is used on the CD
+ std::string strExt;
+ if (StringUtils::EqualsNoCase(name, "MPEGAV"))
+ strExt = ".dat";
+ if (StringUtils::EqualsNoCase(name, "MPEG2"))
+ strExt = ".mpg";
+
+ // If a file format was extracted we are sure this is a VCD. Autoplay if settings indicate we should.
+ if (!strExt.empty() && bAllowVideo
+ && (bypassSettings || bAutorunDVDs))
+ {
+ CFileItemList items;
+ CDirectory::GetDirectory(pItem->GetPath(), items, strExt, DIR_FLAG_DEFAULTS);
+ if (items.Size())
+ {
+ items.Sort(SortByLabel, SortOrderAscending);
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_VIDEO, items);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().Play(0, "");
+ return true;
+ }
+ }
+ /* Probably want this if/when we add some automedia action dialog...
+ else if (pItem->GetPath().Find("PICTURES") != -1 && bAllowPictures
+ && (bypassSettings))
+ {
+ bPlaying = true;
+ std::string strExec = StringUtils::Format("RecursiveSlideShow({})", pItem->GetPath());
+ CBuiltins::Execute(strExec);
+ return true;
+ }
+ */
+ }
+ }
+ }
+
+ // check video first
+ if (!nAddedToPlaylist && !bPlaying && (bypassSettings || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_DVDS_AUTORUN)))
+ {
+ // stack video files
+ CFileItemList tempItems;
+ tempItems.Append(vecItems);
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_STACKVIDEOS))
+ tempItems.Stack();
+ CFileItemList itemlist;
+
+ for (int i = 0; i < tempItems.Size(); i++)
+ {
+ CFileItemPtr pItem = tempItems[i];
+ if (!pItem->m_bIsFolder && pItem->IsVideo())
+ {
+ bPlaying = true;
+ if (pItem->IsStack())
+ {
+ //! @todo remove this once the app/player is capable of handling stacks immediately
+ CStackDirectory dir;
+ CFileItemList items;
+ dir.GetDirectory(pItem->GetURL(), items);
+ itemlist.Append(items);
+ }
+ else
+ itemlist.Add(pItem);
+ }
+ }
+ if (itemlist.Size())
+ {
+ if (!bAllowVideo)
+ {
+ if (!bypassSettings)
+ return false;
+
+ if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return false;
+ }
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_VIDEO, itemlist);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().Play(0, "");
+ }
+ }
+ // then music
+ if (!bPlaying && (bypassSettings || CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_AUDIOCDS_AUTOACTION) == AUTOCD_PLAY) && bAllowMusic)
+ {
+ for (int i = 0; i < vecItems.Size(); i++)
+ {
+ CFileItemPtr pItem = vecItems[i];
+ if (!pItem->m_bIsFolder && pItem->IsAudio())
+ {
+ nAddedToPlaylist++;
+ CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_MUSIC, pItem);
+ }
+ }
+ }
+ /* Probably want this if/when we add some automedia action dialog...
+ // and finally pictures
+ if (!nAddedToPlaylist && !bPlaying && bypassSettings && bAllowPictures)
+ {
+ for (int i = 0; i < vecItems.Size(); i++)
+ {
+ CFileItemPtr pItem = vecItems[i];
+ if (!pItem->m_bIsFolder && pItem->IsPicture())
+ {
+ bPlaying = true;
+ std::string strExec = StringUtils::Format("RecursiveSlideShow({})", strDrive);
+ CBuiltins::Execute(strExec);
+ break;
+ }
+ }
+ }
+ */
+
+ // check subdirs if we are not playing yet
+ if (!bPlaying)
+ {
+ for (int i = 0; i < vecItems.Size(); i++)
+ {
+ CFileItemPtr pItem = vecItems[i];
+ if (pItem->m_bIsFolder)
+ {
+ if (pItem->GetPath() != "." && pItem->GetPath() != ".." )
+ {
+ if (RunDisc(pDir, pItem->GetPath(), nAddedToPlaylist, false, bypassSettings, startFromBeginning))
+ {
+ bPlaying = true;
+ break;
+ }
+ }
+ } // if (non system) folder
+ } // for all items in directory
+ } // if root folder
+
+ return bPlaying;
+}
+
+void CAutorun::HandleAutorun()
+{
+#if !defined(TARGET_WINDOWS) && defined(HAS_DVD_DRIVE)
+ const CDetectDVDMedia& mediadetect = CServiceBroker::GetDetectDVDMedia();
+
+ if (!m_bEnable)
+ {
+ mediadetect.m_evAutorun.Reset();
+ return ;
+ }
+
+ if (mediadetect.m_evAutorun.Wait(0ms))
+ {
+ if (!ExecuteAutorun(""))
+ CLog::Log(LOGDEBUG, "{}: Could not execute autorun", __func__);
+ mediadetect.m_evAutorun.Reset();
+ }
+#endif
+}
+
+void CAutorun::Enable()
+{
+ m_bEnable = true;
+}
+
+void CAutorun::Disable()
+{
+ m_bEnable = false;
+}
+
+bool CAutorun::IsEnabled() const
+{
+ return m_bEnable;
+}
+
+bool CAutorun::PlayDiscAskResume(const std::string& path)
+{
+ return PlayDisc(path, true,
+ !CanResumePlayDVD(path) ||
+ HELPERS::ShowYesNoDialogText(CVariant{341}, CVariant{""}, CVariant{13404},
+ CVariant{12021}) == DialogResponse::CHOICE_YES);
+}
+
+bool CAutorun::CanResumePlayDVD(const std::string& path)
+{
+ std::string strUniqueId = CServiceBroker::GetMediaManager().GetDiskUniqueId(path);
+ if (!strUniqueId.empty())
+ {
+ CVideoDatabase dbs;
+ dbs.Open();
+ CBookmark bookmark;
+ if (dbs.GetResumeBookMark(strUniqueId, bookmark))
+ return true;
+ }
+ return false;
+}
+
+void CAutorun::SettingOptionAudioCdActionsFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(16018), AUTOCD_NONE);
+ list.emplace_back(g_localizeStrings.Get(14098), AUTOCD_PLAY);
+#ifdef HAS_CDDA_RIPPER
+ list.emplace_back(g_localizeStrings.Get(14096), AUTOCD_RIP);
+#endif
+}
diff --git a/xbmc/Autorun.h b/xbmc/Autorun.h
new file mode 100644
index 0000000..1bd342b
--- /dev/null
+++ b/xbmc/Autorun.h
@@ -0,0 +1,74 @@
+/*
+ * 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
+
+// CAutorun - Autorun for different Cd Media
+// like DVD Movies or XBOX Games
+//
+// by Bobbin007 in 2003
+//
+//
+//
+
+#ifdef HAS_DVD_DRIVE
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+struct IntegerSettingOption;
+
+namespace XFILE
+{
+ class IDirectory;
+}
+
+class CSetting;
+
+enum AutoCDAction
+{
+ AUTOCD_NONE = 0,
+ AUTOCD_PLAY,
+ AUTOCD_RIP
+};
+
+namespace MEDIA_DETECT
+{
+class CAutorun
+{
+public:
+ CAutorun();
+ virtual ~CAutorun();
+ static bool CanResumePlayDVD(const std::string& path);
+ static bool PlayDisc(const std::string& path="", bool bypassSettings = false, bool startFromBeginning = false);
+ static bool PlayDiscAskResume(const std::string& path="");
+ bool IsEnabled() const;
+ void Enable();
+ void Disable();
+ void HandleAutorun();
+
+ /*! \brief Execute the autorun. Used for example to automatically rip cds or play optical discs
+ * @param path the path for the item (e.g. the disc path)
+ * @return true if some action was executed, false otherwise
+ */
+ static bool ExecuteAutorun(const std::string& path);
+
+ static void SettingOptionAudioCdActionsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+protected:
+ static bool RunDisc(XFILE::IDirectory* pDir, const std::string& strDrive, int& nAddedToPlaylist, bool bRoot, bool bypassSettings, bool startFromBeginning);
+ bool m_bEnable;
+};
+}
+
+#endif
diff --git a/xbmc/BackgroundInfoLoader.cpp b/xbmc/BackgroundInfoLoader.cpp
new file mode 100644
index 0000000..8235f81
--- /dev/null
+++ b/xbmc/BackgroundInfoLoader.cpp
@@ -0,0 +1,151 @@
+/*
+ * 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 "BackgroundInfoLoader.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "threads/Thread.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+CBackgroundInfoLoader::CBackgroundInfoLoader() : m_thread (NULL)
+{
+ m_bStop = true;
+ m_pObserver=NULL;
+ m_pProgressCallback=NULL;
+ m_pVecItems = NULL;
+ m_bIsLoading = false;
+}
+
+CBackgroundInfoLoader::~CBackgroundInfoLoader()
+{
+ StopThread();
+}
+
+void CBackgroundInfoLoader::Run()
+{
+ try
+ {
+ if (!m_vecItems.empty())
+ {
+ OnLoaderStart();
+
+ // Stage 1: All "fast" stuff we have already cached
+ for (std::vector<CFileItemPtr>::const_iterator iter = m_vecItems.begin(); iter != m_vecItems.end(); ++iter)
+ {
+ const CFileItemPtr& pItem = *iter;
+
+ // Ask the callback if we should abort
+ if ((m_pProgressCallback && m_pProgressCallback->Abort()) || m_bStop)
+ break;
+
+ try
+ {
+ if (LoadItemCached(pItem.get()) && m_pObserver)
+ m_pObserver->OnItemLoaded(pItem.get());
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,
+ "CBackgroundInfoLoader::LoadItemCached - Unhandled exception for item {}",
+ CURL::GetRedacted(pItem->GetPath()));
+ }
+ }
+
+ // Stage 2: All "slow" stuff that we need to lookup
+ for (std::vector<CFileItemPtr>::const_iterator iter = m_vecItems.begin(); iter != m_vecItems.end(); ++iter)
+ {
+ const CFileItemPtr& pItem = *iter;
+
+ // Ask the callback if we should abort
+ if ((m_pProgressCallback && m_pProgressCallback->Abort()) || m_bStop)
+ break;
+
+ try
+ {
+ if (LoadItemLookup(pItem.get()) && m_pObserver)
+ m_pObserver->OnItemLoaded(pItem.get());
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,
+ "CBackgroundInfoLoader::LoadItemLookup - Unhandled exception for item {}",
+ CURL::GetRedacted(pItem->GetPath()));
+ }
+ }
+ }
+
+ OnLoaderFinish();
+ m_bIsLoading = false;
+ }
+ catch (...)
+ {
+ m_bIsLoading = false;
+ CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__);
+ }
+}
+
+void CBackgroundInfoLoader::Load(CFileItemList& items)
+{
+ StopThread();
+
+ if (items.IsEmpty())
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ for (int nItem=0; nItem < items.Size(); nItem++)
+ m_vecItems.push_back(items[nItem]);
+
+ m_pVecItems = &items;
+ m_bStop = false;
+ m_bIsLoading = true;
+
+ m_thread = new CThread(this, "BackgroundLoader");
+ m_thread->Create();
+ m_thread->SetPriority(ThreadPriority::BELOW_NORMAL);
+}
+
+void CBackgroundInfoLoader::StopAsync()
+{
+ m_bStop = true;
+}
+
+
+void CBackgroundInfoLoader::StopThread()
+{
+ StopAsync();
+
+ if (m_thread)
+ {
+ m_thread->StopThread();
+ delete m_thread;
+ m_thread = NULL;
+ }
+ m_vecItems.clear();
+ m_pVecItems = NULL;
+ m_bIsLoading = false;
+}
+
+bool CBackgroundInfoLoader::IsLoading()
+{
+ return m_bIsLoading;
+}
+
+void CBackgroundInfoLoader::SetObserver(IBackgroundLoaderObserver* pObserver)
+{
+ m_pObserver = pObserver;
+}
+
+void CBackgroundInfoLoader::SetProgressCallback(IProgressCallback* pCallback)
+{
+ m_pProgressCallback = pCallback;
+}
+
diff --git a/xbmc/BackgroundInfoLoader.h b/xbmc/BackgroundInfoLoader.h
new file mode 100644
index 0000000..31de46c
--- /dev/null
+++ b/xbmc/BackgroundInfoLoader.h
@@ -0,0 +1,62 @@
+/*
+ * 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 "IProgressCallback.h"
+#include "threads/CriticalSection.h"
+#include "threads/IRunnable.h"
+
+#include <memory>
+#include <vector>
+
+class CFileItem; typedef std::shared_ptr<CFileItem> CFileItemPtr;
+class CFileItemList;
+class CThread;
+
+class IBackgroundLoaderObserver
+{
+public:
+ virtual ~IBackgroundLoaderObserver() = default;
+ virtual void OnItemLoaded(CFileItem* pItem) = 0;
+};
+
+class CBackgroundInfoLoader : public IRunnable
+{
+public:
+ CBackgroundInfoLoader();
+ ~CBackgroundInfoLoader() override;
+
+ void Load(CFileItemList& items);
+ bool IsLoading();
+ void Run() override;
+ void SetObserver(IBackgroundLoaderObserver* pObserver);
+ void SetProgressCallback(IProgressCallback* pCallback);
+ virtual bool LoadItem(CFileItem* pItem) { return false; }
+ virtual bool LoadItemCached(CFileItem* pItem) { return false; }
+ virtual bool LoadItemLookup(CFileItem* pItem) { return false; }
+
+ void StopThread(); // will actually stop the loader thread.
+ void StopAsync(); // will ask loader to stop as soon as possible, but not block
+
+protected:
+ virtual void OnLoaderStart() {}
+ virtual void OnLoaderFinish() {}
+
+ CFileItemList *m_pVecItems;
+ std::vector<CFileItemPtr> m_vecItems; // FileItemList would delete the items and we only want to keep a reference.
+ CCriticalSection m_lock;
+
+ volatile bool m_bIsLoading;
+ volatile bool m_bStop;
+ CThread *m_thread;
+
+ IBackgroundLoaderObserver* m_pObserver;
+ IProgressCallback* m_pProgressCallback;
+};
+
diff --git a/xbmc/CMakeLists.txt b/xbmc/CMakeLists.txt
new file mode 100644
index 0000000..4c69707
--- /dev/null
+++ b/xbmc/CMakeLists.txt
@@ -0,0 +1,91 @@
+set(SOURCES AutoSwitch.cpp
+ BackgroundInfoLoader.cpp
+ ContextMenuItem.cpp
+ ContextMenuManager.cpp
+ ContextMenus.cpp
+ CueDocument.cpp
+ DatabaseManager.cpp
+ DbUrl.cpp
+ DynamicDll.cpp
+ FileItem.cpp
+ FileItemListModification.cpp
+ GUIInfoManager.cpp
+ GUILargeTextureManager.cpp
+ GUIPassword.cpp
+ InfoScanner.cpp
+ LangInfo.cpp
+ MediaSource.cpp
+ NfoFile.cpp
+ PasswordManager.cpp
+ PlayListPlayer.cpp
+ PartyModeManager.cpp
+ SectionLoader.cpp
+ SeekHandler.cpp
+ ServiceBroker.cpp
+ ServiceManager.cpp
+ SystemGlobals.cpp
+ TextureCache.cpp
+ TextureCacheJob.cpp
+ TextureDatabase.cpp
+ ThumbLoader.cpp
+ URL.cpp
+ Util.cpp
+ XBDateTime.cpp)
+
+set(HEADERS AutoSwitch.h
+ BackgroundInfoLoader.h
+ CompileInfo.h
+ ContextMenuItem.h
+ ContextMenuManager.h
+ ContextMenus.h
+ CueDocument.h
+ DatabaseManager.h
+ DbUrl.h
+ DllPaths.h
+ DllPaths_win32.h
+ DynamicDll.h
+ FileItem.h
+ FileItemListModification.h
+ GUIInfoManager.h
+ GUILargeTextureManager.h
+ GUIPassword.h
+ GUIUserMessages.h
+ HDRStatus.h
+ IFileItemListModifier.h
+ IProgressCallback.h
+ InfoScanner.h
+ LangInfo.h
+ LockType.h
+ MediaSource.h
+ NfoFile.h
+ PartyModeManager.h
+ PasswordManager.h
+ PlayListPlayer.h
+ SectionLoader.h
+ SeekHandler.h
+ ServiceBroker.h
+ ServiceManager.h
+ SortFileItem.h
+ TextureCache.h
+ TextureCacheJob.h
+ TextureDatabase.h
+ ThumbLoader.h
+ URL.h
+ Util.h
+ XBDateTime.h
+ system_egl.h
+ system_gl.h)
+
+if(ENABLE_OPTICAL)
+ list(APPEND SOURCES Autorun.cpp)
+ list(APPEND HEADERS Autorun.h)
+endif()
+
+core_add_library(xbmc)
+if(CAP_FOUND)
+ target_compile_definitions(${CORE_LIBRARY} PRIVATE -DHAVE_LIBCAP=1)
+endif()
+
+if(CORE_SYSTEM_NAME STREQUAL windowsstore)
+ set_target_properties(${CORE_LIBRARY} PROPERTIES STATIC_LIBRARY_FLAGS "/ignore:4264")
+endif()
diff --git a/xbmc/CompileInfo.cpp.in b/xbmc/CompileInfo.cpp.in
new file mode 100644
index 0000000..131f519
--- /dev/null
+++ b/xbmc/CompileInfo.cpp.in
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2014-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 "CompileInfo.h"
+#include "addons/AddonRepoInfo.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <string>
+
+int CCompileInfo::GetMajor()
+{
+ return @APP_VERSION_MAJOR@;
+}
+
+int CCompileInfo::GetMinor()
+{
+ return @APP_VERSION_MINOR@;
+}
+
+const char* CCompileInfo::GetPackage()
+{
+ return "@APP_PACKAGE@";
+}
+
+const char* CCompileInfo::GetClass()
+{
+ static std::string s_classname;
+
+ if (s_classname.empty())
+ {
+ s_classname = CCompileInfo::GetPackage();
+ std::replace(s_classname.begin(), s_classname.end(), '.', '/');
+ }
+ return s_classname.c_str();
+}
+
+const char* CCompileInfo::GetAppName()
+{
+ return "@APP_NAME@";
+}
+
+const char* CCompileInfo::GetSuffix()
+{
+ return "@APP_VERSION_TAG@";
+}
+
+const char* CCompileInfo::GetSCMID()
+{
+ return "@APP_SCMID@";
+}
+
+std::string CCompileInfo::GetSharedLibrarySuffix()
+{
+ return "@APP_SHARED_LIBRARY_SUFFIX@";
+}
+
+const char* CCompileInfo::GetCopyrightYears()
+{
+ return "@APP_COPYRIGHT_YEARS@";
+}
+
+std::string CCompileInfo::GetBuildDate()
+{
+ const std::string bdate = "@APP_BUILD_DATE@";
+ if (!bdate.empty())
+ {
+ std::string datestamp = bdate.substr(0, 4) + "-" + bdate.substr(4, 2) + "-" + bdate.substr(6, 2);
+ return datestamp;
+ }
+ return "1970-01-01";
+}
+
+const char* CCompileInfo::GetVersionCode()
+{
+ return "@APP_VERSION_CODE@";
+}
+
+std::vector<ADDON::RepoInfo> CCompileInfo::LoadOfficialRepoInfos()
+{
+ const std::vector<std::string> officialAddonRepos =
+ StringUtils::Split("@APP_ADDON_REPOS@", ',');
+
+ std::vector<ADDON::RepoInfo> officialRepoInfos;
+ ADDON::RepoInfo newRepoInfo;
+
+ for (const auto& addonRepo : officialAddonRepos)
+ {
+ const std::vector<std::string> tmpRepoInfo = StringUtils::Split(addonRepo, '|');
+ newRepoInfo.m_repoId = tmpRepoInfo.front();
+ newRepoInfo.m_origin = tmpRepoInfo.back();
+ officialRepoInfos.emplace_back(newRepoInfo);
+ }
+
+ return officialRepoInfos;
+}
+
+std::vector<std::string> CCompileInfo::GetAvailableWindowSystems()
+{
+ return StringUtils::Split("@CORE_PLATFORM_NAME_LC@", ' ');
+}
+
+// Return version of python built against as format MAJOR.MINOR
+std::string CCompileInfo::GetPythonVersion()
+{
+ return "@PYTHON_VERSION@";
+}
+
+std::vector<std::string> CCompileInfo::GetWebserverExtraWhitelist()
+{
+ return StringUtils::Split("@KODI_WEBSERVER_EXTRA_WHITELIST@", ',');
+}
diff --git a/xbmc/CompileInfo.h b/xbmc/CompileInfo.h
new file mode 100644
index 0000000..4f22716
--- /dev/null
+++ b/xbmc/CompileInfo.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014-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 <string>
+#include <vector>
+
+namespace ADDON
+{
+struct RepoInfo;
+}
+
+class CCompileInfo
+{
+public:
+ static int GetMajor();
+ static int GetMinor();
+ static const char* GetPackage();
+ static const char* GetClass();
+ static const char* GetAppName();
+ static const char* GetSuffix(); // Git "Tag", e.g. alpha1
+ static const char* GetSCMID(); // Git Revision
+ static std::string GetSharedLibrarySuffix();
+ static const char* GetCopyrightYears();
+ static std::string GetBuildDate();
+ static const char* GetVersionCode();
+ static std::vector<std::string> GetAvailableWindowSystems();
+ static std::vector<ADDON::RepoInfo> LoadOfficialRepoInfos();
+ static std::string GetPythonVersion();
+ static std::vector<std::string> GetWebserverExtraWhitelist();
+};
diff --git a/xbmc/ContextMenuItem.cpp b/xbmc/ContextMenuItem.cpp
new file mode 100644
index 0000000..9191cb1
--- /dev/null
+++ b/xbmc/ContextMenuItem.cpp
@@ -0,0 +1,115 @@
+/*
+ * 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 "ContextMenuItem.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddon.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/LocalizeStrings.h"
+#ifdef HAS_PYTHON
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "interfaces/python/ContextItemAddonInvoker.h"
+#include "interfaces/python/XBPython.h"
+#endif
+#include "ServiceBroker.h"
+#include "utils/StringUtils.h"
+
+std::string CStaticContextMenuAction::GetLabel(const CFileItem& item) const
+{
+ return g_localizeStrings.Get(m_label);
+}
+
+bool CContextMenuItem::IsVisible(const CFileItem& item) const
+{
+ if (!m_infoBoolRegistered)
+ {
+ m_infoBool = CServiceBroker::GetGUI()->GetInfoManager().Register(m_visibilityCondition, 0);
+ m_infoBoolRegistered = true;
+ }
+ return IsGroup() || (m_infoBool && m_infoBool->Get(INFO::DEFAULT_CONTEXT, &item));
+}
+
+bool CContextMenuItem::IsParentOf(const CContextMenuItem& other) const
+{
+ return !m_groupId.empty() && (m_groupId == other.m_parent);
+}
+
+bool CContextMenuItem::IsGroup() const
+{
+ return !m_groupId.empty();
+}
+
+bool CContextMenuItem::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ if (!item || m_library.empty() || IsGroup())
+ return false;
+
+ ADDON::AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(m_addonId, addon, ADDON::OnlyEnabled::CHOICE_YES))
+ return false;
+
+ bool reuseLanguageInvoker = false;
+ if (addon->ExtraInfo().find("reuselanguageinvoker") != addon->ExtraInfo().end())
+ reuseLanguageInvoker = addon->ExtraInfo().at("reuselanguageinvoker") == "true";
+
+#ifdef HAS_PYTHON
+ LanguageInvokerPtr invoker(new CContextItemAddonInvoker(&CServiceBroker::GetXBPython(), item));
+ return (CScriptInvocationManager::GetInstance().ExecuteAsync(m_library, invoker, addon, m_args, reuseLanguageInvoker) != -1);
+#else
+ return false;
+#endif
+}
+
+bool CContextMenuItem::operator==(const CContextMenuItem& other) const
+{
+ if (IsGroup() && other.IsGroup())
+ return (m_groupId == other.m_groupId && m_parent == other.m_parent);
+
+ return (IsGroup() == other.IsGroup())
+ && (m_parent == other.m_parent)
+ && (m_library == other.m_library)
+ && (m_addonId == other.m_addonId)
+ && (m_args == other.m_args);
+}
+
+std::string CContextMenuItem::ToString() const
+{
+ if (IsGroup())
+ return StringUtils::Format("CContextMenuItem[group, id={}, parent={}, addon={}]", m_groupId,
+ m_parent, m_addonId);
+ else
+ return StringUtils::Format("CContextMenuItem[item, parent={}, library={}, addon={}]", m_parent,
+ m_library, m_addonId);
+}
+
+CContextMenuItem CContextMenuItem::CreateGroup(const std::string& label, const std::string& parent,
+ const std::string& groupId, const std::string& addonId)
+{
+ CContextMenuItem menuItem;
+ menuItem.m_label = label;
+ menuItem.m_parent = parent;
+ menuItem.m_groupId = groupId;
+ menuItem.m_addonId = addonId;
+ return menuItem;
+}
+
+CContextMenuItem CContextMenuItem::CreateItem(const std::string& label, const std::string& parent,
+ const std::string& library, const std::string& condition, const std::string& addonId, const std::vector<std::string>& args)
+{
+ CContextMenuItem menuItem;
+ menuItem.m_label = label;
+ menuItem.m_parent = parent;
+ menuItem.m_library = library;
+ menuItem.m_visibilityCondition = condition;
+ menuItem.m_addonId = addonId;
+ menuItem.m_args = args;
+ return menuItem;
+}
diff --git a/xbmc/ContextMenuItem.h b/xbmc/ContextMenuItem.h
new file mode 100644
index 0000000..4e04a06
--- /dev/null
+++ b/xbmc/ContextMenuItem.h
@@ -0,0 +1,90 @@
+/*
+ * 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 <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+
+namespace ADDON
+{
+ class CContextMenuAddon;
+}
+
+namespace INFO
+{
+class InfoBool;
+}
+
+class IContextMenuItem
+{
+public:
+ virtual ~IContextMenuItem() = default;
+ virtual bool IsVisible(const CFileItem& item) const = 0;
+ virtual bool Execute(const std::shared_ptr<CFileItem>& item) const = 0;
+ virtual std::string GetLabel(const CFileItem& item) const = 0;
+ virtual bool IsGroup() const { return false; }
+};
+
+
+class CStaticContextMenuAction : public IContextMenuItem
+{
+public:
+ explicit CStaticContextMenuAction(uint32_t label) : m_label(label) {}
+ std::string GetLabel(const CFileItem& item) const final;
+ bool IsGroup() const final { return false; }
+private:
+ const uint32_t m_label;
+};
+
+
+class CContextMenuItem : public IContextMenuItem
+{
+public:
+ CContextMenuItem() = default;
+
+ std::string GetLabel(const CFileItem& item) const override { return m_label; }
+ bool IsVisible(const CFileItem& item) const override ;
+ bool IsParentOf(const CContextMenuItem& menuItem) const;
+ bool IsGroup() const override ;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+ bool operator==(const CContextMenuItem& other) const;
+ std::string ToString() const;
+
+ static CContextMenuItem CreateGroup(
+ const std::string& label,
+ const std::string& parent,
+ const std::string& groupId,
+ const std::string& addonId);
+
+ static CContextMenuItem CreateItem(
+ const std::string& label,
+ const std::string& parent,
+ const std::string& library,
+ const std::string& condition,
+ const std::string& addonId,
+ const std::vector<std::string>& args = std::vector<std::string>());
+
+ friend class ADDON::CContextMenuAddon;
+
+private:
+ std::string m_label;
+ std::string m_parent;
+ std::string m_groupId;
+ std::string m_library;
+ std::string m_addonId; // The owner of this menu item
+ std::vector<std::string> m_args;
+
+ std::string m_visibilityCondition;
+ mutable std::shared_ptr<INFO::InfoBool> m_infoBool;
+ mutable bool m_infoBoolRegistered{false};
+};
diff --git a/xbmc/ContextMenuManager.cpp b/xbmc/ContextMenuManager.cpp
new file mode 100644
index 0000000..0efff74
--- /dev/null
+++ b/xbmc/ContextMenuManager.cpp
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2013-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 "ContextMenuManager.h"
+
+#include "ContextMenuItem.h"
+#include "ContextMenus.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonManager.h"
+#include "addons/ContextMenuAddon.h"
+#include "addons/ContextMenus.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "favourites/ContextMenus.h"
+#include "messaging/ApplicationMessenger.h"
+#include "music/ContextMenus.h"
+#include "pvr/PVRContextMenus.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "video/ContextMenus.h"
+
+#include <iterator>
+#include <mutex>
+
+using namespace ADDON;
+using namespace PVR;
+
+const CContextMenuItem CContextMenuManager::MAIN = CContextMenuItem::CreateGroup("", "", "kodi.core.main", "");
+const CContextMenuItem CContextMenuManager::MANAGE = CContextMenuItem::CreateGroup("", "", "kodi.core.manage", "");
+
+
+CContextMenuManager::CContextMenuManager(CAddonMgr& addonMgr)
+ : m_addonMgr(addonMgr) {}
+
+CContextMenuManager::~CContextMenuManager()
+{
+ Deinit();
+}
+
+void CContextMenuManager::Deinit()
+{
+ CPVRContextMenuManager::GetInstance().Events().Unsubscribe(this);
+ m_addonMgr.Events().Unsubscribe(this);
+ m_items.clear();
+}
+
+void CContextMenuManager::Init()
+{
+ m_addonMgr.Events().Subscribe(this, &CContextMenuManager::OnEvent);
+ CPVRContextMenuManager::GetInstance().Events().Subscribe(this, &CContextMenuManager::OnPVREvent);
+
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ m_items = {
+ std::make_shared<CONTEXTMENU::CVideoBrowse>(),
+ std::make_shared<CONTEXTMENU::CVideoResume>(),
+ std::make_shared<CONTEXTMENU::CVideoPlay>(),
+ std::make_shared<CONTEXTMENU::CVideoPlayAndQueue>(),
+ std::make_shared<CONTEXTMENU::CVideoPlayNext>(),
+ std::make_shared<CONTEXTMENU::CVideoQueue>(),
+ std::make_shared<CONTEXTMENU::CMusicBrowse>(),
+ std::make_shared<CONTEXTMENU::CMusicPlay>(),
+ std::make_shared<CONTEXTMENU::CMusicPlayNext>(),
+ std::make_shared<CONTEXTMENU::CMusicQueue>(),
+ std::make_shared<CONTEXTMENU::CAddonInfo>(),
+ std::make_shared<CONTEXTMENU::CEnableAddon>(),
+ std::make_shared<CONTEXTMENU::CDisableAddon>(),
+ std::make_shared<CONTEXTMENU::CAddonSettings>(),
+ std::make_shared<CONTEXTMENU::CCheckForUpdates>(),
+ std::make_shared<CONTEXTMENU::CEpisodeInfo>(),
+ std::make_shared<CONTEXTMENU::CMovieInfo>(),
+ std::make_shared<CONTEXTMENU::CMusicVideoInfo>(),
+ std::make_shared<CONTEXTMENU::CTVShowInfo>(),
+ std::make_shared<CONTEXTMENU::CAlbumInfo>(),
+ std::make_shared<CONTEXTMENU::CArtistInfo>(),
+ std::make_shared<CONTEXTMENU::CSongInfo>(),
+ std::make_shared<CONTEXTMENU::CVideoMarkWatched>(),
+ std::make_shared<CONTEXTMENU::CVideoMarkUnWatched>(),
+ std::make_shared<CONTEXTMENU::CVideoRemoveResumePoint>(),
+ std::make_shared<CONTEXTMENU::CEjectDisk>(),
+ std::make_shared<CONTEXTMENU::CEjectDrive>(),
+ std::make_shared<CONTEXTMENU::CMoveUpFavourite>(),
+ std::make_shared<CONTEXTMENU::CMoveDownFavourite>(),
+ std::make_shared<CONTEXTMENU::CChooseThumbnailForFavourite>(),
+ std::make_shared<CONTEXTMENU::CRenameFavourite>(),
+ std::make_shared<CONTEXTMENU::CRemoveFavourite>(),
+ std::make_shared<CONTEXTMENU::CAddRemoveFavourite>(),
+ };
+
+ ReloadAddonItems();
+
+ const std::vector<std::shared_ptr<IContextMenuItem>> pvrItems(CPVRContextMenuManager::GetInstance().GetMenuItems());
+ for (const auto &item : pvrItems)
+ m_items.emplace_back(item);
+}
+
+void CContextMenuManager::ReloadAddonItems()
+{
+ VECADDONS addons;
+ m_addonMgr.GetAddons(addons, AddonType::CONTEXTMENU_ITEM);
+
+ std::vector<CContextMenuItem> addonItems;
+ for (const auto& addon : addons)
+ {
+ auto items = std::static_pointer_cast<CContextMenuAddon>(addon)->GetItems();
+ for (auto& item : items)
+ {
+ auto it = std::find(addonItems.begin(), addonItems.end(), item);
+ if (it == addonItems.end())
+ addonItems.push_back(item);
+ }
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ m_addonItems = std::move(addonItems);
+
+ CLog::Log(LOGDEBUG, "ContextMenuManager: addon menus reloaded.");
+}
+
+void CContextMenuManager::OnEvent(const ADDON::AddonEvent& event)
+{
+ if (typeid(event) == typeid(AddonEvents::ReInstalled) ||
+ typeid(event) == typeid(AddonEvents::UnInstalled))
+ {
+ ReloadAddonItems();
+ }
+ else if (typeid(event) == typeid(AddonEvents::Enabled))
+ {
+ AddonPtr addon;
+ if (m_addonMgr.GetAddon(event.addonId, addon, AddonType::CONTEXTMENU_ITEM,
+ OnlyEnabled::CHOICE_YES))
+ {
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ auto items = std::static_pointer_cast<CContextMenuAddon>(addon)->GetItems();
+ for (auto& item : items)
+ {
+ auto it = std::find(m_addonItems.begin(), m_addonItems.end(), item);
+ if (it == m_addonItems.end())
+ m_addonItems.push_back(item);
+ }
+ CLog::Log(LOGDEBUG, "ContextMenuManager: loaded {}.", event.addonId);
+ }
+ }
+ else if (typeid(event) == typeid(AddonEvents::Disabled))
+ {
+ if (m_addonMgr.HasType(event.addonId, AddonType::CONTEXTMENU_ITEM))
+ {
+ ReloadAddonItems();
+ }
+ }
+}
+
+void CContextMenuManager::OnPVREvent(const PVRContextMenuEvent& event)
+{
+ switch (event.action)
+ {
+ case PVRContextMenuEventAction::ADD_ITEM:
+ {
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ m_items.emplace_back(event.item);
+ break;
+ }
+ case PVRContextMenuEventAction::REMOVE_ITEM:
+ {
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ auto it = std::find(m_items.begin(), m_items.end(), event.item);
+ if (it != m_items.end())
+ m_items.erase(it);
+ break;
+ }
+
+ default:
+ break;
+ }
+}
+
+bool CContextMenuManager::IsVisible(
+ const CContextMenuItem& menuItem, const CContextMenuItem& root, const CFileItem& fileItem) const
+{
+ if (menuItem.GetLabel(fileItem).empty() || !root.IsParentOf(menuItem))
+ return false;
+
+ if (menuItem.IsGroup())
+ {
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ return std::any_of(m_addonItems.begin(), m_addonItems.end(),
+ [&](const CContextMenuItem& other){ return menuItem.IsParentOf(other) && other.IsVisible(fileItem); });
+ }
+
+ return menuItem.IsVisible(fileItem);
+}
+
+ContextMenuView CContextMenuManager::GetItems(const CFileItem& fileItem, const CContextMenuItem& root /*= MAIN*/) const
+{
+ ContextMenuView result;
+ //! @todo implement group support
+ if (&root == &MAIN)
+ {
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ std::copy_if(m_items.begin(), m_items.end(), std::back_inserter(result),
+ [&](const std::shared_ptr<IContextMenuItem>& menu){ return menu->IsVisible(fileItem); });
+ }
+ return result;
+}
+
+ContextMenuView CContextMenuManager::GetAddonItems(const CFileItem& fileItem, const CContextMenuItem& root /*= MAIN*/) const
+{
+ ContextMenuView result;
+ {
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ for (const auto& menu : m_addonItems)
+ if (IsVisible(menu, root, fileItem))
+ result.emplace_back(new CContextMenuItem(menu));
+ }
+
+ if (&root == &MANAGE)
+ {
+ std::sort(result.begin(), result.end(),
+ [&](const ContextMenuView::value_type& lhs, const ContextMenuView::value_type& rhs)
+ {
+ return lhs->GetLabel(fileItem) < rhs->GetLabel(fileItem);
+ }
+ );
+ }
+ return result;
+}
+
+bool CONTEXTMENU::ShowFor(const std::shared_ptr<CFileItem>& fileItem, const CContextMenuItem& root)
+{
+ if (!fileItem)
+ return false;
+
+ const CContextMenuManager &contextMenuManager = CServiceBroker::GetContextMenuManager();
+
+ auto menuItems = contextMenuManager.GetItems(*fileItem, root);
+ for (auto&& item : contextMenuManager.GetAddonItems(*fileItem, root))
+ menuItems.emplace_back(std::move(item));
+
+ CContextButtons buttons;
+ // compute fileitem property-based contextmenu items
+ {
+ int i = 0;
+ while (fileItem->HasProperty(StringUtils::Format("contextmenulabel({})", i)))
+ {
+ buttons.emplace_back(
+ ~buttons.size(),
+ fileItem->GetProperty(StringUtils::Format("contextmenulabel({})", i)).asString());
+ ++i;
+ }
+ }
+ const int propertyMenuSize = buttons.size();
+
+ if (menuItems.empty() && propertyMenuSize == 0)
+ return true;
+
+ buttons.reserve(menuItems.size());
+ for (size_t i = 0; i < menuItems.size(); ++i)
+ buttons.Add(i, menuItems[i]->GetLabel(*fileItem));
+
+ int selected = CGUIDialogContextMenu::Show(buttons);
+ if (selected < 0 || selected >= static_cast<int>(buttons.size()))
+ return false;
+
+ if (selected < propertyMenuSize)
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ fileItem->GetProperty(StringUtils::Format("contextmenuaction({})", selected)).asString());
+ return true;
+ }
+
+ return menuItems[selected - propertyMenuSize]->IsGroup()
+ ? ShowFor(fileItem, static_cast<const CContextMenuItem&>(
+ *menuItems[selected - propertyMenuSize]))
+ : menuItems[selected - propertyMenuSize]->Execute(fileItem);
+}
+
+bool CONTEXTMENU::LoopFrom(const IContextMenuItem& menu, const std::shared_ptr<CFileItem>& fileItem)
+{
+ if (!fileItem)
+ return false;
+ if (menu.IsGroup())
+ return ShowFor(fileItem, static_cast<const CContextMenuItem&>(menu));
+ return menu.Execute(fileItem);
+}
diff --git a/xbmc/ContextMenuManager.h b/xbmc/ContextMenuManager.h
new file mode 100644
index 0000000..13d92bd
--- /dev/null
+++ b/xbmc/ContextMenuManager.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2013-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 "ContextMenuItem.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+namespace ADDON
+{
+struct AddonEvent;
+class CAddonMgr;
+} // namespace ADDON
+
+namespace PVR
+{
+ struct PVRContextMenuEvent;
+}
+
+using ContextMenuView = std::vector<std::shared_ptr<const IContextMenuItem>>;
+
+class CContextMenuManager
+{
+public:
+ static const CContextMenuItem MAIN;
+ static const CContextMenuItem MANAGE;
+
+ explicit CContextMenuManager(ADDON::CAddonMgr& addonMgr);
+ ~CContextMenuManager();
+
+ void Init();
+ void Deinit();
+
+ ContextMenuView GetItems(const CFileItem& item, const CContextMenuItem& root = MAIN) const;
+
+ ContextMenuView GetAddonItems(const CFileItem& item, const CContextMenuItem& root = MAIN) const;
+
+private:
+ CContextMenuManager(const CContextMenuManager&) = delete;
+ CContextMenuManager& operator=(CContextMenuManager const&) = delete;
+
+ bool IsVisible(
+ const CContextMenuItem& menuItem,
+ const CContextMenuItem& root,
+ const CFileItem& fileItem) const;
+
+ void ReloadAddonItems();
+ void OnEvent(const ADDON::AddonEvent& event);
+
+ void OnPVREvent(const PVR::PVRContextMenuEvent& event);
+
+ ADDON::CAddonMgr& m_addonMgr;
+
+ mutable CCriticalSection m_criticalSection;
+ std::vector<CContextMenuItem> m_addonItems;
+ std::vector<std::shared_ptr<IContextMenuItem>> m_items;
+};
+
+namespace CONTEXTMENU
+{
+ /*!
+ * Starts the context menu loop for a file item.
+ * */
+bool ShowFor(const std::shared_ptr<CFileItem>& fileItem,
+ const CContextMenuItem& root = CContextMenuManager::MAIN);
+
+/*!
+ * Shortcut for continuing the context menu loop from an existing menu item.
+ */
+bool LoopFrom(const IContextMenuItem& menu, const std::shared_ptr<CFileItem>& fileItem);
+}
diff --git a/xbmc/ContextMenus.cpp b/xbmc/ContextMenus.cpp
new file mode 100644
index 0000000..4dacfda
--- /dev/null
+++ b/xbmc/ContextMenus.cpp
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenus.h"
+
+#include "ServiceBroker.h"
+#include "favourites/FavouritesService.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/WindowTranslator.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+namespace CONTEXTMENU
+{
+
+ bool CEjectDisk::IsVisible(const CFileItem& item) const
+ {
+#ifdef HAS_DVD_DRIVE
+ return item.IsRemovable() && (item.IsDVD() || item.IsCDDA());
+#else
+ return false;
+#endif
+ }
+
+ bool CEjectDisk::Execute(const std::shared_ptr<CFileItem>& item) const
+ {
+#ifdef HAS_DVD_DRIVE
+ CServiceBroker::GetMediaManager().ToggleTray(
+ CServiceBroker::GetMediaManager().TranslateDevicePath(item->GetPath())[0]);
+#endif
+ return true;
+ }
+
+ bool CEjectDrive::IsVisible(const CFileItem& item) const
+ {
+ // Must be HDD
+ return item.IsRemovable() && !item.IsDVD() && !item.IsCDDA();
+ }
+
+ bool CEjectDrive::Execute(const std::shared_ptr<CFileItem>& item) const
+ {
+ return CServiceBroker::GetMediaManager().Eject(item->GetPath());
+ }
+
+namespace
+{
+
+int GetTargetWindowID(const CFileItem& item)
+{
+ int iTargetWindow = WINDOW_INVALID;
+
+ const std::string targetWindow = item.GetProperty("targetwindow").asString();
+ if (targetWindow.empty())
+ iTargetWindow = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ else
+ iTargetWindow = CWindowTranslator::TranslateWindow(targetWindow);
+
+ return iTargetWindow;
+}
+
+} // unnamed namespace
+
+std::string CAddRemoveFavourite::GetLabel(const CFileItem& item) const
+{
+ return g_localizeStrings.Get(CServiceBroker::GetFavouritesService().IsFavourited(item, GetTargetWindowID(item))
+ ? 14077 /* Remove from favourites */
+ : 14076); /* Add to favourites */
+}
+
+bool CAddRemoveFavourite::IsVisible(const CFileItem& item) const
+{
+ return (!item.GetPath().empty() && !item.IsParentFolder() && !item.IsPath("add") &&
+ !item.IsPath("newplaylist://") && !URIUtils::IsProtocol(item.GetPath(), "favourites") &&
+ !URIUtils::IsProtocol(item.GetPath(), "newsmartplaylist") &&
+ !URIUtils::IsProtocol(item.GetPath(), "newtag") &&
+ !URIUtils::IsProtocol(item.GetPath(), "musicsearch") &&
+ // Hide this item for all PVR EPG/timers/search except EPG/timer/timer rules/search root
+ // folders.
+ !StringUtils::StartsWith(item.GetPath(), "pvr://guide/") &&
+ !StringUtils::StartsWith(item.GetPath(), "pvr://timers/") &&
+ !StringUtils::StartsWith(item.GetPath(), "pvr://search/")) ||
+ item.GetPath() == "pvr://guide/tv/" || item.GetPath() == "pvr://guide/radio/" ||
+ item.GetPath() == "pvr://timers/tv/timers/" ||
+ item.GetPath() == "pvr://timers/radio/timers/" ||
+ item.GetPath() == "pvr://timers/tv/rules/" ||
+ item.GetPath() == "pvr://timers/radio/rules/" || item.GetPath() == "pvr://search/tv/" ||
+ item.GetPath() == "pvr://search/radio/";
+}
+
+bool CAddRemoveFavourite::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetFavouritesService().AddOrRemove(*item.get(), GetTargetWindowID(*item));
+}
+
+} // namespace CONTEXTMENU
diff --git a/xbmc/ContextMenus.h b/xbmc/ContextMenus.h
new file mode 100644
index 0000000..1f4dba0
--- /dev/null
+++ b/xbmc/ContextMenus.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenuItem.h"
+
+#include <memory>
+
+namespace CONTEXTMENU
+{
+
+struct CEjectDisk : CStaticContextMenuAction
+{
+ CEjectDisk() : CStaticContextMenuAction(13391) {} // Eject/Load CD/DVD!
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CEjectDrive : CStaticContextMenuAction
+{
+ CEjectDrive() : CStaticContextMenuAction(13420) {} // Eject Removable HDD!
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CAddRemoveFavourite : IContextMenuItem
+{
+ CAddRemoveFavourite() = default;
+ std::string GetLabel(const CFileItem& item) const override;
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+}
diff --git a/xbmc/CueDocument.cpp b/xbmc/CueDocument.cpp
new file mode 100644
index 0000000..c128874
--- /dev/null
+++ b/xbmc/CueDocument.cpp
@@ -0,0 +1,490 @@
+/*
+ * 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.
+ */
+
+////////////////////////////////////////////////////////////////////////////////////
+// Class: CueDocument
+// This class handles the .cue file format. This is produced by programs such as
+// EAC and CDRwin when one extracts audio data from a CD as a continuous .WAV
+// containing all the audio tracks in one big file. The .cue file contains all the
+// track and timing information. An example file is:
+//
+// PERFORMER "Pink Floyd"
+// TITLE "The Dark Side Of The Moon"
+// FILE "The Dark Side Of The Moon.mp3" WAVE
+// TRACK 01 AUDIO
+// TITLE "Speak To Me / Breathe"
+// PERFORMER "Pink Floyd"
+// INDEX 00 00:00:00
+// INDEX 01 00:00:32
+// TRACK 02 AUDIO
+// TITLE "On The Run"
+// PERFORMER "Pink Floyd"
+// INDEX 00 03:58:72
+// INDEX 01 04:00:72
+// TRACK 03 AUDIO
+// TITLE "Time"
+// PERFORMER "Pink Floyd"
+// INDEX 00 07:31:70
+// INDEX 01 07:33:70
+//
+// etc.
+//
+// The CCueDocument class member functions extract this information, and construct
+// the playlist items needed to seek to a track directly. This works best on CBR
+// compressed files - VBR files do not seek accurately enough for it to work well.
+//
+////////////////////////////////////////////////////////////////////////////////////
+
+#include "CueDocument.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <cstdlib>
+#include <set>
+
+using namespace XFILE;
+
+// Stuff for read CUE data from different sources.
+class CueReader
+{
+public:
+ virtual bool ready() const = 0;
+ virtual bool ReadLine(std::string &line) = 0;
+ virtual ~CueReader() = default;
+private:
+ std::string m_sourcePath;
+};
+
+class FileReader
+ : public CueReader
+{
+public:
+ explicit FileReader(const std::string &strFile) : m_szBuffer{}
+ {
+ m_opened = m_file.Open(strFile);
+ }
+ bool ReadLine(std::string &line) override
+ {
+ // Read the next line.
+ while (m_file.ReadString(m_szBuffer, 1023)) // Bigger than MAX_PATH_SIZE, for usage with relax!
+ {
+ // Remove the white space at the beginning and end of the line.
+ line = m_szBuffer;
+ StringUtils::Trim(line);
+ if (!line.empty())
+ return true;
+ // If we are here, we have an empty line so try the next line
+ }
+ return false;
+ }
+ bool ready() const override
+ {
+ return m_opened;
+ }
+ ~FileReader() override
+ {
+ if (m_opened)
+ m_file.Close();
+
+ }
+private:
+ CFile m_file;
+ bool m_opened;
+ char m_szBuffer[1024];
+};
+
+class BufferReader
+ : public CueReader
+{
+public:
+ explicit BufferReader(const std::string &strContent)
+ : m_data(strContent)
+ , m_pos(0)
+ {
+ }
+ bool ReadLine(std::string &line) override
+ {
+ // Read the next line.
+ line.clear();
+ while (m_pos < m_data.size())
+ {
+ // Remove the white space at the beginning of the line.
+ char ch = m_data.at(m_pos++);
+ if (ch == '\r' || ch == '\n') {
+ StringUtils::Trim(line);
+ if (!line.empty())
+ return true;
+ }
+ else
+ {
+ line.push_back(ch);
+ }
+ }
+
+ StringUtils::Trim(line);
+ return !line.empty();
+ }
+ bool ready() const override
+ {
+ return m_data.size() > 0;
+ }
+private:
+ std::string m_data;
+ size_t m_pos;
+};
+
+CCueDocument::~CCueDocument() = default;
+
+////////////////////////////////////////////////////////////////////////////////////
+// Function: ParseFile()
+// Opens the CUE file for reading, and constructs the track database information
+////////////////////////////////////////////////////////////////////////////////////
+bool CCueDocument::ParseFile(const std::string &strFilePath)
+{
+ FileReader reader(strFilePath);
+ return Parse(reader, strFilePath);
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// Function: ParseTag()
+// Reads CUE data from string buffer, and constructs the track database information
+////////////////////////////////////////////////////////////////////////////////////
+bool CCueDocument::ParseTag(const std::string &strContent)
+{
+ BufferReader reader(strContent);
+ return Parse(reader);
+}
+
+//////////////////////////////////////////////////////////////////////////////////
+// Function:GetSongs()
+// Store track information into songs list.
+//////////////////////////////////////////////////////////////////////////////////
+void CCueDocument::GetSongs(VECSONGS &songs)
+{
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ for (const auto& track : m_tracks)
+ {
+ CSong aSong;
+ //Pass artist to MusicInfoTag object by setting artist description string only.
+ //Artist credits not used during loading from cue sheet.
+ if (track.strArtist.empty() && !m_strArtist.empty())
+ aSong.strArtistDesc = m_strArtist;
+ else
+ aSong.strArtistDesc = track.strArtist;
+ //Pass album artist to MusicInfoTag object by setting album artist vector.
+ aSong.SetAlbumArtist(StringUtils::Split(m_strArtist, advancedSettings->m_musicItemSeparator));
+ aSong.strAlbum = m_strAlbum;
+ aSong.genre = StringUtils::Split(m_strGenre, advancedSettings->m_musicItemSeparator);
+ aSong.strReleaseDate = StringUtils::Format("{:04}", m_iYear);
+ aSong.iTrack = track.iTrackNumber;
+ if (m_iDiscNumber > 0)
+ aSong.iTrack |= (m_iDiscNumber << 16); // see CMusicInfoTag::GetDiscNumber()
+ if (track.strTitle.length() == 0) // No track information for this track!
+ aSong.strTitle = StringUtils::Format("Track {:2d}", track.iTrackNumber);
+ else
+ aSong.strTitle = track.strTitle;
+ aSong.strFileName = track.strFile;
+ aSong.iStartOffset = track.iStartTime;
+ aSong.iEndOffset = track.iEndTime;
+ if (aSong.iEndOffset)
+ // Convert offset in frames (75 per second) to duration in whole seconds with rounding
+ aSong.iDuration = CUtil::ConvertMilliSecsToSecsIntRounded(aSong.iEndOffset - aSong.iStartOffset);
+ else
+ aSong.iDuration = 0;
+
+ if (m_albumReplayGain.Valid())
+ aSong.replayGain.Set(ReplayGain::ALBUM, m_albumReplayGain);
+
+ if (track.replayGain.Valid())
+ aSong.replayGain.Set(ReplayGain::TRACK, track.replayGain);
+
+ songs.push_back(aSong);
+ }
+}
+
+void CCueDocument::UpdateMediaFile(const std::string& oldMediaFile, const std::string& mediaFile)
+{
+ for (Tracks::iterator it = m_tracks.begin(); it != m_tracks.end(); ++it)
+ {
+ if (it->strFile == oldMediaFile)
+ it->strFile = mediaFile;
+ }
+}
+
+void CCueDocument::GetMediaFiles(std::vector<std::string>& mediaFiles)
+{
+ typedef std::set<std::string> TSet;
+ TSet uniqueFiles;
+ for (Tracks::const_iterator it = m_tracks.begin(); it != m_tracks.end(); ++it)
+ uniqueFiles.insert(it->strFile);
+
+ for (TSet::const_iterator it = uniqueFiles.begin(); it != uniqueFiles.end(); ++it)
+ mediaFiles.push_back(*it);
+}
+
+std::string CCueDocument::GetMediaTitle()
+{
+ return m_strAlbum;
+}
+
+bool CCueDocument::IsLoaded() const
+{
+ return !m_tracks.empty();
+}
+
+bool CCueDocument::IsOneFilePerTrack() const
+{
+ return m_bOneFilePerTrack;
+}
+
+// Private Functions start here
+
+void CCueDocument::Clear()
+{
+ m_strArtist.clear();
+ m_strAlbum.clear();
+ m_strGenre.clear();
+ m_iYear = 0;
+ m_iTrack = 0;
+ m_iDiscNumber = 0;
+ m_albumReplayGain = ReplayGain::Info();
+ m_tracks.clear();
+}
+////////////////////////////////////////////////////////////////////////////////////
+// Function: Parse()
+// Constructs the track database information from CUE source
+////////////////////////////////////////////////////////////////////////////////////
+bool CCueDocument::Parse(CueReader& reader, const std::string& strFile)
+{
+ Clear();
+ if (!reader.ready())
+ return false;
+
+ std::string strLine;
+ std::string strCurrentFile = "";
+ bool bCurrentFileChanged = false;
+ int time;
+ int totalTracks = -1;
+ int numberFiles = -1;
+
+ // Run through the .CUE file and extract the tracks...
+ while (reader.ReadLine(strLine))
+ {
+ if (StringUtils::StartsWithNoCase(strLine, "INDEX 01"))
+ {
+ if (bCurrentFileChanged)
+ {
+ CLog::Log(LOGERROR, "Track split over multiple files, unsupported.");
+ return false;
+ }
+
+ // find the end of the number section
+ time = ExtractTimeFromIndex(strLine);
+ if (time == -1)
+ { // Error!
+ CLog::Log(LOGERROR, "Mangled Time in INDEX 0x tag in CUE file!");
+ return false;
+ }
+ if (totalTracks > 0 && m_tracks[totalTracks - 1].strFile == strCurrentFile) // Set the end time of the last track
+ m_tracks[totalTracks - 1].iEndTime = time;
+
+ if (totalTracks >= 0) // start time of the next track
+ m_tracks[totalTracks].iStartTime = time;
+ }
+ else if (StringUtils::StartsWithNoCase(strLine, "TITLE"))
+ {
+ if (totalTracks == -1) // No tracks yet
+ m_strAlbum = ExtractInfo(strLine.substr(5));
+ else
+ m_tracks[totalTracks].strTitle = ExtractInfo(strLine.substr(5));
+ }
+ else if (StringUtils::StartsWithNoCase(strLine, "PERFORMER"))
+ {
+ if (totalTracks == -1) // No tracks yet
+ m_strArtist = ExtractInfo(strLine.substr(9));
+ else // New Artist for this track
+ m_tracks[totalTracks].strArtist = ExtractInfo(strLine.substr(9));
+ }
+ else if (StringUtils::StartsWithNoCase(strLine, "TRACK"))
+ {
+ int iTrackNumber = ExtractNumericInfo(strLine.substr(5));
+
+ totalTracks++;
+
+ CCueTrack track;
+ m_tracks.push_back(track);
+ m_tracks[totalTracks].strFile = strCurrentFile;
+ if (iTrackNumber > 0)
+ m_tracks[totalTracks].iTrackNumber = iTrackNumber;
+ else
+ m_tracks[totalTracks].iTrackNumber = totalTracks + 1;
+
+ bCurrentFileChanged = false;
+ }
+ else if (StringUtils::StartsWithNoCase(strLine, "REM DISCNUMBER"))
+ {
+ int iDiscNumber = ExtractNumericInfo(strLine.substr(14));
+ if (iDiscNumber > 0)
+ m_iDiscNumber = iDiscNumber;
+ }
+ else if (StringUtils::StartsWithNoCase(strLine, "FILE"))
+ {
+ numberFiles++;
+ // already a file name? then the time computation will be changed
+ if (!strCurrentFile.empty())
+ bCurrentFileChanged = true;
+
+ strCurrentFile = ExtractInfo(strLine.substr(4));
+
+ // Resolve absolute paths (if needed).
+ if (!strFile.empty() && !strCurrentFile.empty())
+ ResolvePath(strCurrentFile, strFile);
+ }
+ else if (StringUtils::StartsWithNoCase(strLine, "REM DATE"))
+ {
+ int iYear = ExtractNumericInfo(strLine.substr(8));
+ if (iYear > 0)
+ m_iYear = iYear;
+ }
+ else if (StringUtils::StartsWithNoCase(strLine, "REM GENRE"))
+ {
+ m_strGenre = ExtractInfo(strLine.substr(9));
+ }
+ else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_ALBUM_GAIN"))
+ m_albumReplayGain.SetGain(strLine.substr(26));
+ else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_ALBUM_PEAK"))
+ m_albumReplayGain.SetPeak(strLine.substr(26));
+ else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_TRACK_GAIN") && totalTracks >= 0)
+ m_tracks[totalTracks].replayGain.SetGain(strLine.substr(26));
+ else if (StringUtils::StartsWithNoCase(strLine, "REM REPLAYGAIN_TRACK_PEAK") && totalTracks >= 0)
+ m_tracks[totalTracks].replayGain.SetPeak(strLine.substr(26));
+ }
+
+ // reset track counter to 0, and fill in the last tracks end time
+ m_iTrack = 0;
+ if (totalTracks >= 0)
+ m_tracks[totalTracks].iEndTime = 0;
+ else
+ CLog::Log(LOGERROR, "No INDEX 01 tags in CUE file!");
+
+ if ( totalTracks == numberFiles )
+ m_bOneFilePerTrack = true;
+
+ return (totalTracks >= 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// Function: ExtractInfo()
+// Extracts the information in quotes from the string line, returning it in quote
+////////////////////////////////////////////////////////////////////////////////////
+std::string CCueDocument::ExtractInfo(const std::string &line)
+{
+ size_t left = line.find('\"');
+ if (left != std::string::npos)
+ {
+ size_t right = line.find('\"', left + 1);
+ if (right != std::string::npos)
+ {
+ std::string text = line.substr(left + 1, right - left - 1);
+ g_charsetConverter.unknownToUTF8(text);
+ return text;
+ }
+ }
+ std::string text = line;
+ StringUtils::Trim(text);
+ g_charsetConverter.unknownToUTF8(text);
+ return text;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// Function: ExtractTimeFromIndex()
+// Extracts the time information from the index string index, returning it as a value in
+// milliseconds.
+// Assumed format is:
+// MM:SS:FF where MM is minutes, SS seconds, and FF frames (75 frames in a second)
+////////////////////////////////////////////////////////////////////////////////////
+int CCueDocument::ExtractTimeFromIndex(const std::string &index)
+{
+ // Get rid of the index number and any whitespace
+ std::string numberTime = index.substr(5);
+ StringUtils::TrimLeft(numberTime);
+ while (!numberTime.empty())
+ {
+ if (!StringUtils::isasciidigit(numberTime[0]))
+ break;
+ numberTime.erase(0, 1);
+ }
+ StringUtils::TrimLeft(numberTime);
+ // split the resulting string
+ std::vector<std::string> time = StringUtils::Split(numberTime, ":");
+ if (time.size() != 3)
+ return -1;
+
+ int mins = atoi(time[0].c_str());
+ int secs = atoi(time[1].c_str());
+ int frames = atoi(time[2].c_str());
+
+ return CUtil::ConvertSecsToMilliSecs(mins*60 + secs) + frames * 1000 / 75;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// Function: ExtractNumericInfo()
+// Extracts the numeric info from the string info, returning it as an integer value
+////////////////////////////////////////////////////////////////////////////////////
+int CCueDocument::ExtractNumericInfo(const std::string &info)
+{
+ std::string number(info);
+ StringUtils::TrimLeft(number);
+ if (number.empty() || !StringUtils::isasciidigit(number[0]))
+ return -1;
+ return atoi(number.c_str());
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// Function: ResolvePath()
+// Determines whether strPath is a relative path or not, and if so, converts it to an
+// absolute path using the path information in strBase
+////////////////////////////////////////////////////////////////////////////////////
+bool CCueDocument::ResolvePath(std::string &strPath, const std::string &strBase)
+{
+ std::string strDirectory = URIUtils::GetDirectory(strBase);
+ std::string strFilename = URIUtils::GetFileName(strPath);
+
+ strPath = URIUtils::AddFileToFolder(strDirectory, strFilename);
+
+ // i *hate* windows
+ if (!CFile::Exists(strPath))
+ {
+ CFileItemList items;
+ CDirectory::GetDirectory(strDirectory, items, "", DIR_FLAG_DEFAULTS);
+ for (int i=0;i<items.Size();++i)
+ {
+ if (items[i]->IsPath(strPath))
+ {
+ strPath = items[i]->GetPath();
+ return true;
+ }
+ }
+ CLog::Log(LOGERROR, "Could not find '{}' referenced in cue, case sensitivity issue?", strPath);
+ return false;
+ }
+
+ return true;
+}
+
diff --git a/xbmc/CueDocument.h b/xbmc/CueDocument.h
new file mode 100644
index 0000000..7218a9f
--- /dev/null
+++ b/xbmc/CueDocument.h
@@ -0,0 +1,68 @@
+/*
+ * 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 "music/Song.h"
+
+#include <string>
+#include <vector>
+
+#define MAX_PATH_SIZE 1024
+
+class CueReader;
+
+class CCueDocument
+{
+ class CCueTrack
+ {
+ public:
+ std::string strArtist;
+ std::string strTitle;
+ std::string strFile;
+ int iTrackNumber = 0;
+ int iStartTime = 0;
+ int iEndTime = 0;
+ ReplayGain::Info replayGain;
+ };
+public:
+ ~CCueDocument(void);
+ // USED
+ bool ParseFile(const std::string &strFilePath);
+ bool ParseTag(const std::string &strContent);
+ void GetSongs(VECSONGS &songs);
+ std::string GetMediaPath();
+ std::string GetMediaTitle();
+ void GetMediaFiles(std::vector<std::string>& mediaFiles);
+ void UpdateMediaFile(const std::string& oldMediaFile, const std::string& mediaFile);
+ bool IsOneFilePerTrack() const;
+ bool IsLoaded() const;
+private:
+ void Clear();
+ bool Parse(CueReader& reader, const std::string& strFile = std::string());
+
+ // Member variables
+ std::string m_strArtist; // album artist
+ std::string m_strAlbum; // album title
+ std::string m_strGenre; // album genre
+ int m_iYear = 0; //album year
+ int m_iTrack = 0; // current track
+ int m_iDiscNumber = 0; // Disc number
+ ReplayGain::Info m_albumReplayGain;
+
+ bool m_bOneFilePerTrack = false;
+
+ // cuetrack array
+ typedef std::vector<CCueTrack> Tracks;
+ Tracks m_tracks;
+
+ std::string ExtractInfo(const std::string &line);
+ int ExtractTimeFromIndex(const std::string &index);
+ int ExtractNumericInfo(const std::string &info);
+ bool ResolvePath(std::string &strPath, const std::string &strBase);
+};
diff --git a/xbmc/DatabaseManager.cpp b/xbmc/DatabaseManager.cpp
new file mode 100644
index 0000000..33f38e8
--- /dev/null
+++ b/xbmc/DatabaseManager.cpp
@@ -0,0 +1,232 @@
+/*
+ * 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 "DatabaseManager.h"
+
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "addons/AddonDatabase.h"
+#include "music/MusicDatabase.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "view/ViewDatabase.h"
+
+#include <mutex>
+
+using namespace PVR;
+
+CDatabaseManager::CDatabaseManager() :
+ m_bIsUpgrading(false)
+{
+ // Initialize the addon database (must be before the addon manager is init'd)
+ ADDON::CAddonDatabase db;
+ UpdateDatabase(db);
+}
+
+CDatabaseManager::~CDatabaseManager() = default;
+
+void CDatabaseManager::Initialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ m_dbStatus.clear();
+
+ CLog::Log(LOGDEBUG, "{}, updating databases...", __FUNCTION__);
+
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ // NOTE: Order here is important. In particular, CTextureDatabase has to be updated
+ // before CVideoDatabase.
+ {
+ ADDON::CAddonDatabase db;
+ UpdateDatabase(db);
+ }
+ { CViewDatabase db; UpdateDatabase(db); }
+ { CTextureDatabase db; UpdateDatabase(db); }
+ { CMusicDatabase db; UpdateDatabase(db, &advancedSettings->m_databaseMusic); }
+ { CVideoDatabase db; UpdateDatabase(db, &advancedSettings->m_databaseVideo); }
+ { CPVRDatabase db; UpdateDatabase(db, &advancedSettings->m_databaseTV); }
+ { CPVREpgDatabase db; UpdateDatabase(db, &advancedSettings->m_databaseEpg); }
+
+ CLog::Log(LOGDEBUG, "{}, updating databases... DONE", __FUNCTION__);
+
+ m_bIsUpgrading = false;
+}
+
+bool CDatabaseManager::CanOpen(const std::string &name)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ std::map<std::string, DB_STATUS>::const_iterator i = m_dbStatus.find(name);
+ if (i != m_dbStatus.end())
+ return i->second == DB_READY;
+ return false; // db isn't even attempted to update yet
+}
+
+void CDatabaseManager::UpdateDatabase(CDatabase &db, DatabaseSettings *settings)
+{
+ std::string name = db.GetBaseDBName();
+ UpdateStatus(name, DB_UPDATING);
+ if (Update(db, settings ? *settings : DatabaseSettings()))
+ UpdateStatus(name, DB_READY);
+ else
+ UpdateStatus(name, DB_FAILED);
+}
+
+bool CDatabaseManager::Update(CDatabase &db, const DatabaseSettings &settings)
+{
+ DatabaseSettings dbSettings = settings;
+ db.InitSettings(dbSettings);
+
+ int version = db.GetSchemaVersion();
+ std::string latestDb = dbSettings.name;
+ latestDb += std::to_string(version);
+
+ while (version >= db.GetMinSchemaVersion())
+ {
+ std::string dbName = dbSettings.name;
+ if (version)
+ dbName += std::to_string(version);
+
+ if (db.Connect(dbName, dbSettings, false))
+ {
+ // Database exists, take a copy for our current version (if needed) and reopen that one
+ if (version < db.GetSchemaVersion())
+ {
+ CLog::Log(LOGINFO, "Old database found - updating from version {} to {}", version,
+ db.GetSchemaVersion());
+ m_bIsUpgrading = true;
+
+ bool copy_fail = false;
+
+ try
+ {
+ db.CopyDB(latestDb);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Unable to copy old database {} to new version {}", dbName, latestDb);
+ copy_fail = true;
+ }
+
+ db.Close();
+
+ if (copy_fail)
+ return false;
+
+ if (!db.Connect(latestDb, dbSettings, false))
+ {
+ CLog::Log(LOGERROR, "Unable to open freshly copied database {}", latestDb);
+ return false;
+ }
+ }
+
+ // yay - we have a copy of our db, now do our worst with it
+ if (UpdateVersion(db, latestDb))
+ return true;
+
+ // update failed - loop around and see if we have another one available
+ db.Close();
+ }
+
+ // drop back to the previous version and try that
+ version--;
+ }
+ // try creating a new one
+ if (db.Connect(latestDb, dbSettings, true))
+ return true;
+
+ // failed to update or open the database
+ db.Close();
+ CLog::Log(LOGERROR, "Unable to create new database");
+ return false;
+}
+
+bool CDatabaseManager::UpdateVersion(CDatabase &db, const std::string &dbName)
+{
+ int version = db.GetDBVersion();
+ bool bReturn = false;
+
+ if (version < db.GetMinSchemaVersion())
+ {
+ CLog::Log(LOGERROR, "Can't update database {} from version {} - it's too old", dbName, version);
+ return false;
+ }
+ else if (version < db.GetSchemaVersion())
+ {
+ CLog::Log(LOGINFO, "Attempting to update the database {} from version {} to {}", dbName,
+ version, db.GetSchemaVersion());
+ bool success = true;
+ db.BeginTransaction();
+
+ try
+ {
+ // drop old analytics
+ db.DropAnalytics();
+ }
+ catch (...)
+ {
+ success = false;
+ }
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "Exception droping old analytics from {}", dbName);
+ db.RollbackTransaction();
+ return false;
+ }
+
+ db.CommitTransaction();
+ db.BeginTransaction();
+
+ try
+ {
+ // update table(s), recreate analytics, update version
+ db.UpdateTables(version);
+ db.CreateAnalytics();
+ db.UpdateVersionNumber();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception updating database {} from version {} to {}", dbName, version,
+ db.GetSchemaVersion());
+ success = false;
+ }
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "Error updating database {} from version {} to {}", dbName, version,
+ db.GetSchemaVersion());
+ db.RollbackTransaction();
+ return false;
+ }
+ bReturn = db.CommitTransaction();
+ CLog::Log(LOGINFO, "Update to version {} successful", db.GetSchemaVersion());
+ }
+ else if (version > db.GetSchemaVersion())
+ {
+ bReturn = false;
+ CLog::Log(LOGERROR,
+ "Can't open the database {} as it is a NEWER version than what we were expecting?",
+ dbName);
+ }
+ else
+ {
+ bReturn = true;
+ CLog::Log(LOGINFO, "Running database version {}", dbName);
+ }
+
+ return bReturn;
+}
+
+void CDatabaseManager::UpdateStatus(const std::string &name, DB_STATUS status)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_dbStatus[name] = status;
+}
diff --git a/xbmc/DatabaseManager.h b/xbmc/DatabaseManager.h
new file mode 100644
index 0000000..0fb10d2
--- /dev/null
+++ b/xbmc/DatabaseManager.h
@@ -0,0 +1,65 @@
+/*
+ * 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 <atomic>
+#include <map>
+#include <string>
+
+class CDatabase;
+class DatabaseSettings;
+
+/*!
+ \ingroup database
+ \brief Database manager class for handling database updating
+
+ Ensures that databases used in XBMC are up to date, and if a database can't be
+ opened, ensures we don't continuously try it.
+
+ */
+class CDatabaseManager
+{
+public:
+ CDatabaseManager();
+ CDatabaseManager(const CDatabaseManager&) = delete;
+ CDatabaseManager const& operator=(CDatabaseManager const&) = delete;
+ ~CDatabaseManager();
+
+ /*! \brief Initialize the database manager
+ Checks that all databases are up to date, otherwise updates them.
+ */
+ void Initialize();
+
+ /*! \brief Check whether we can open a database.
+
+ Checks whether the database has been updated correctly, if so returns true.
+ If the database update failed, returns false immediately.
+ If the database update is in progress, returns false.
+
+ \param name the name of the database to check.
+ \return true if the database can be opened, false otherwise.
+ */
+ bool CanOpen(const std::string &name);
+
+ bool IsUpgrading() const { return m_bIsUpgrading; }
+
+private:
+ std::atomic<bool> m_bIsUpgrading;
+
+ enum DB_STATUS { DB_CLOSED, DB_UPDATING, DB_READY, DB_FAILED };
+ void UpdateStatus(const std::string &name, DB_STATUS status);
+ void UpdateDatabase(CDatabase &db, DatabaseSettings *settings = NULL);
+ bool Update(CDatabase &db, const DatabaseSettings &settings);
+ bool UpdateVersion(CDatabase &db, const std::string &dbName);
+
+ CCriticalSection m_section; ///< Critical section protecting m_dbStatus.
+ std::map<std::string, DB_STATUS> m_dbStatus; ///< Our database status map.
+};
diff --git a/xbmc/DbUrl.cpp b/xbmc/DbUrl.cpp
new file mode 100644
index 0000000..170a736
--- /dev/null
+++ b/xbmc/DbUrl.cpp
@@ -0,0 +1,136 @@
+/*
+ * 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 "DbUrl.h"
+
+#include "utils/URIUtils.h"
+
+CDbUrl::CDbUrl()
+{
+ Reset();
+}
+
+CDbUrl::~CDbUrl() = default;
+
+void CDbUrl::Reset()
+{
+ m_valid = false;
+ m_type.clear();
+ m_url.Reset();
+ m_options.clear();
+}
+
+std::string CDbUrl::ToString() const
+{
+ if (!m_valid)
+ return "";
+
+ return m_url.Get();
+}
+
+bool CDbUrl::FromString(const std::string &dbUrl)
+{
+ Reset();
+
+ m_url.Parse(dbUrl);
+ m_valid = parse();
+
+ if (!m_valid)
+ Reset();
+
+ return m_valid;
+}
+
+void CDbUrl::AppendPath(const std::string &subPath)
+{
+ if (!m_valid || subPath.empty())
+ return;
+
+ m_url.SetFileName(URIUtils::AddFileToFolder(m_url.GetFileName(), subPath));
+}
+
+void CDbUrl::AddOption(const std::string &key, const char *value)
+{
+ if (!validateOption(key, value))
+ return;
+
+ CUrlOptions::AddOption(key, value);
+ updateOptions();
+}
+
+void CDbUrl::AddOption(const std::string &key, const std::string &value)
+{
+ if (!validateOption(key, value))
+ return;
+
+ CUrlOptions::AddOption(key, value);
+ updateOptions();
+}
+
+void CDbUrl::AddOption(const std::string &key, int value)
+{
+ if (!validateOption(key, value))
+ return;
+
+ CUrlOptions::AddOption(key, value);
+ updateOptions();
+}
+
+void CDbUrl::AddOption(const std::string &key, float value)
+{
+ if (!validateOption(key, value))
+ return;
+
+ CUrlOptions::AddOption(key, value);
+ updateOptions();
+}
+
+void CDbUrl::AddOption(const std::string &key, double value)
+{
+ if (!validateOption(key, value))
+ return;
+
+ CUrlOptions::AddOption(key, value);
+ updateOptions();
+}
+
+void CDbUrl::AddOption(const std::string &key, bool value)
+{
+ if (!validateOption(key, value))
+ return;
+
+ CUrlOptions::AddOption(key, value);
+ updateOptions();
+}
+
+void CDbUrl::AddOptions(const std::string &options)
+{
+ CUrlOptions::AddOptions(options);
+ updateOptions();
+}
+
+void CDbUrl::RemoveOption(const std::string &key)
+{
+ CUrlOptions::RemoveOption(key);
+ updateOptions();
+}
+
+bool CDbUrl::validateOption(const std::string &key, const CVariant &value)
+{
+ return !key.empty();
+}
+
+void CDbUrl::updateOptions()
+{
+ // Update the options string in the CURL object
+ std::string options = GetOptionsString();
+ if (!options.empty())
+ options = "?" + options;
+
+ m_url.SetOptions(options);
+}
diff --git a/xbmc/DbUrl.h b/xbmc/DbUrl.h
new file mode 100644
index 0000000..d307d7f
--- /dev/null
+++ b/xbmc/DbUrl.h
@@ -0,0 +1,57 @@
+/*
+ * 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 "URL.h"
+#include "utils/UrlOptions.h"
+
+#include <string>
+
+class CVariant;
+
+class CDbUrl : CUrlOptions
+{
+public:
+ CDbUrl();
+ ~CDbUrl() override;
+
+ bool IsValid() const { return m_valid; }
+ void Reset();
+
+ std::string ToString() const;
+ bool FromString(const std::string &dbUrl);
+
+ const std::string& GetType() const { return m_type; }
+ void AppendPath(const std::string &subPath);
+
+ using CUrlOptions::HasOption;
+ using CUrlOptions::GetOptions;
+ using CUrlOptions::GetOptionsString;
+
+ void AddOption(const std::string &key, const char *value) override;
+ void AddOption(const std::string &key, const std::string &value) override;
+ void AddOption(const std::string &key, int value) override;
+ void AddOption(const std::string &key, float value) override;
+ void AddOption(const std::string &key, double value) override;
+ void AddOption(const std::string &key, bool value) override;
+ void AddOptions(const std::string &options) override;
+ void RemoveOption(const std::string &key) override;
+
+protected:
+ virtual bool parse() = 0;
+ virtual bool validateOption(const std::string &key, const CVariant &value);
+
+ CURL m_url;
+ std::string m_type;
+
+private:
+ void updateOptions();
+
+ bool m_valid;
+};
diff --git a/xbmc/DllPaths.h b/xbmc/DllPaths.h
new file mode 100644
index 0000000..09bdd1b
--- /dev/null
+++ b/xbmc/DllPaths.h
@@ -0,0 +1,18 @@
+/*
+ * 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
+
+#ifdef TARGET_WINDOWS
+#include "DllPaths_win32.h"
+#elif defined (TARGET_ANDROID)
+#include "DllPaths_generated_android.h"
+#else
+#include "DllPaths_generated.h"
+#endif
+
diff --git a/xbmc/DllPaths_generated.h.in b/xbmc/DllPaths_generated.h.in
new file mode 100644
index 0000000..f29faa4
--- /dev/null
+++ b/xbmc/DllPaths_generated.h.in
@@ -0,0 +1,19 @@
+/*
+ * 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
+
+/* prefix install location */
+#define PREFIX_USR_PATH "@prefix@"
+
+/* VideoPlayer */
+#define DLL_PATH_LIBDVDNAV "special://xbmcbin/system/players/VideoPlayer/libdvdnav-@ARCH@.so"
+
+/* sse4 */
+#define DLL_PATH_LIBSSE4 "special://xbmcbin/system/libsse4-@ARCH@.so"
+
diff --git a/xbmc/DllPaths_generated_android.h.in b/xbmc/DllPaths_generated_android.h.in
new file mode 100644
index 0000000..d15988e
--- /dev/null
+++ b/xbmc/DllPaths_generated_android.h.in
@@ -0,0 +1,24 @@
+/*
+ * 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
+
+/* libraries */
+
+//Android rules:
+// - All solibs must be in the form ^lib*.so$
+// - All solibs must be in the same dir
+// - Arch need not be specified, each arch will get its own lib dir.
+// We only keep @ARCH@ here to retain the same structure as *nix.
+// * foo.so will be renamed libfoo.so in the packaging stage
+
+/* VideoPlayer */
+#define DLL_PATH_LIBDVDNAV "libdvdnav-@ARCH@.so"
+
+/* Android's libui for gralloc */
+#define DLL_PATH_LIBUI "libui.so"
diff --git a/xbmc/DllPaths_win32.h b/xbmc/DllPaths_win32.h
new file mode 100644
index 0000000..ea41058
--- /dev/null
+++ b/xbmc/DllPaths_win32.h
@@ -0,0 +1,12 @@
+/*
+ * 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
+
+/* VideoPlayer */
+#define DLL_PATH_LIBDVDNAV "special://xbmcbin/libdvdnav.dll"
diff --git a/xbmc/DynamicDll.cpp b/xbmc/DynamicDll.cpp
new file mode 100644
index 0000000..29538f9
--- /dev/null
+++ b/xbmc/DynamicDll.cpp
@@ -0,0 +1,81 @@
+/*
+ * 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 "DynamicDll.h"
+
+#include "SectionLoader.h"
+#include "utils/FileUtils.h"
+#include "utils/log.h"
+
+DllDynamic::DllDynamic()
+{
+ m_dll=NULL;
+ m_DelayUnload=true;
+}
+
+DllDynamic::DllDynamic(const std::string& strDllName):
+ m_strDllName(strDllName)
+{
+ m_dll=NULL;
+ m_DelayUnload=true;
+}
+
+DllDynamic::~DllDynamic()
+{
+ Unload();
+}
+
+bool DllDynamic::Load()
+{
+ if (m_dll)
+ return true;
+
+ if (!(m_dll=CSectionLoader::LoadDLL(m_strDllName, m_DelayUnload, LoadSymbols())))
+ return false;
+
+ if (!ResolveExports())
+ {
+ CLog::Log(LOGERROR, "Unable to resolve exports from dll {}", m_strDllName);
+ Unload();
+ return false;
+ }
+
+ return true;
+}
+
+void DllDynamic::Unload()
+{
+ if(m_dll)
+ CSectionLoader::UnloadDLL(m_strDllName);
+ m_dll=NULL;
+}
+
+bool DllDynamic::CanLoad()
+{
+ return CFileUtils::Exists(m_strDllName);
+}
+
+bool DllDynamic::EnableDelayedUnload(bool bOnOff)
+{
+ if (m_dll)
+ return false;
+
+ m_DelayUnload=bOnOff;
+
+ return true;
+}
+
+bool DllDynamic::SetFile(const std::string& strDllName)
+{
+ if (m_dll)
+ return false;
+
+ m_strDllName=strDllName;
+ return true;
+}
+
diff --git a/xbmc/DynamicDll.h b/xbmc/DynamicDll.h
new file mode 100644
index 0000000..8325af1
--- /dev/null
+++ b/xbmc/DynamicDll.h
@@ -0,0 +1,534 @@
+/*
+ * 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 "DllPaths.h"
+#include "cores/DllLoader/LibraryLoader.h"
+
+#include <string>
+
+///////////////////////////////////////////////////////////
+//
+// DECLARE_DLL_WRAPPER
+//
+// Declares the constructor of the wrapper class.
+// This must be followed by one or more
+// DEFINE_METHODX/DEFINE_METHOD_LINKAGEX and
+// one BEGIN_METHOD_RESOLVE/END_METHOD_RESOLVE block.
+//
+// classname: name of the wrapper class to construct
+// dllname: file including path of the dll to wrap
+
+#define DECLARE_DLL_WRAPPER(classname, dllname) \
+XDECLARE_DLL_WRAPPER(classname,dllname)
+
+#define XDECLARE_DLL_WRAPPER(classname, dllname) \
+public: \
+ classname () : DllDynamic( dllname ) {}
+
+///////////////////////////////////////////////////////////
+//
+// DECLARE_DLL_WRAPPER_TEMPLATE_BEGIN
+//
+// Declares the constructor of the wrapper class.
+// The method SetFile(strDllName) can be used to set the
+// dll of this wrapper.
+// This must be followed by one or more
+// DEFINE_METHODX/DEFINE_METHOD_LINKAGEX and
+// one BEGIN_METHOD_RESOLVE/END_METHOD_RESOLVE block.
+//
+// classname: name of the wrapper class to construct
+//
+#define DECLARE_DLL_WRAPPER_TEMPLATE(classname) \
+public: \
+ classname () {} \
+
+
+///////////////////////////////////////////////////////////
+//
+// LOAD_SYMBOLS
+//
+// Tells the dllloader to load Debug symbols when possible
+#define LOAD_SYMBOLS() \
+ protected: \
+ virtual bool LoadSymbols() { return true; }
+
+///////////////////////////////////////////////////////////
+//
+// DEFINE_GLOBAL
+//
+// Defines a global for export from the dll as well as
+// a function for accessing it (Get_name).
+//
+// type: The variables type.
+// name: Name of the variable.
+//
+
+#define DEFINE_GLOBAL_PTR(type, name) \
+ protected: \
+ union { \
+ type* m_##name; \
+ void* m_##name##_ptr; \
+ }; \
+ public: \
+ virtual type* Get_##name (void) \
+ { \
+ return m_##name; \
+ }
+
+#define DEFINE_GLOBAL(type, name) \
+ protected: \
+ union { \
+ type* m_##name; \
+ void* m_##name##_ptr; \
+ }; \
+ public: \
+ virtual type Get_##name (void) \
+ { \
+ return *m_##name; \
+ }
+
+///////////////////////////////////////////////////////////
+//
+// DEFINE_METHOD_LINKAGE
+//
+// Defines a function for an export from a dll, if the
+// calling convention is not __cdecl.
+// Use DEFINE_METHOD_LINKAGE for each function to be resolved.
+//
+// result: Result of the function
+// linkage: Calling convention of the function
+// name: Name of the function
+// args: Arguments of the function, enclosed in parentheses
+//
+#define DEFINE_METHOD_LINKAGE_FP(result, linkage, name, args) \
+ protected: \
+ typedef result (linkage * name##_METHOD) args; \
+ public: \
+ union { \
+ name##_METHOD m_##name; \
+ void* m_##name##_ptr; \
+ };
+
+#define DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, args2) \
+ protected: \
+ typedef result (linkage * name##_METHOD) args; \
+ union { \
+ name##_METHOD m_##name; \
+ void* m_##name##_ptr; \
+ }; \
+ public: \
+ virtual result name args override \
+ { \
+ return m_##name ? m_##name args2 : (result) 0; \
+ }
+
+#define DEFINE_METHOD_LINKAGE0(result, linkage, name) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, () , ())
+
+#define DEFINE_METHOD_LINKAGE1(result, linkage, name, args) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, (p1))
+
+#define DEFINE_METHOD_LINKAGE2(result, linkage, name, args) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, (p1, p2))
+
+#define DEFINE_METHOD_LINKAGE3(result, linkage, name, args) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, (p1, p2, p3))
+
+#define DEFINE_METHOD_LINKAGE4(result, linkage, name, args) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, (p1, p2, p3, p4))
+
+#define DEFINE_METHOD_LINKAGE5(result, linkage, name, args) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, (p1, p2, p3, p4, p5))
+
+#define DEFINE_METHOD_LINKAGE6(result, linkage, name, args) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, (p1, p2, p3, p4, p5, p6))
+
+#define DEFINE_METHOD_LINKAGE7(result, linkage, name, args) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, (p1, p2, p3, p4, p5, p6, p7))
+
+#define DEFINE_METHOD_LINKAGE8(result, linkage, name, args) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, (p1, p2, p3, p4, p5, p6, p7, p8))
+
+#define DEFINE_METHOD_LINKAGE9(result, linkage, name, args) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, (p1, p2, p3, p4, p5, p6, p7, p8, p9))
+
+#define DEFINE_METHOD_LINKAGE10(result, linkage, name, args) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, (p1, p2, p3, p4, p5, p6, p7, p8, p9, p10))
+
+#define DEFINE_METHOD_LINKAGE11(result, linkage, name, args) \
+ DEFINE_METHOD_LINKAGE_BASE(result, linkage, name, args, (p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11))
+
+///////////////////////////////////////////////////////////
+//
+// DEFINE_METHOD_FP
+//
+// Defines a function for an export from a dll as a function pointer.
+// Use DEFINE_METHOD_FP for each function to be resolved. Functions
+// defined like this are not listed by IntelliSence.
+//
+// result: Result of the function
+// name: Name of the function
+// args: Arguments of the function, enclosed in parentheses
+// The parameter names can be anything
+//
+#define DEFINE_METHOD_FP(result, name, args) DEFINE_METHOD_LINKAGE_FP(result, __cdecl, name, args)
+
+///////////////////////////////////////////////////////////
+//
+// DEFINE_METHODX
+//
+// Defines a function for an export from a dll.
+// Use DEFINE_METHODX for each function to be resolved.
+// where X is the number of parameter the function has.
+//
+// result: Result of the function
+// name: Name of the function
+// args: Arguments of the function, enclosed in parentheses
+// The parameter names have to be renamed to px, where
+// x is the number of the parameter
+//
+#define DEFINE_METHOD0(result, name) DEFINE_METHOD_LINKAGE0(result, __cdecl, name)
+#define DEFINE_METHOD1(result, name, args) DEFINE_METHOD_LINKAGE1(result, __cdecl, name, args)
+#define DEFINE_METHOD2(result, name, args) DEFINE_METHOD_LINKAGE2(result, __cdecl, name, args)
+#define DEFINE_METHOD3(result, name, args) DEFINE_METHOD_LINKAGE3(result, __cdecl, name, args)
+#define DEFINE_METHOD4(result, name, args) DEFINE_METHOD_LINKAGE4(result, __cdecl, name, args)
+#define DEFINE_METHOD5(result, name, args) DEFINE_METHOD_LINKAGE5(result, __cdecl, name, args)
+#define DEFINE_METHOD6(result, name, args) DEFINE_METHOD_LINKAGE6(result, __cdecl, name, args)
+#define DEFINE_METHOD7(result, name, args) DEFINE_METHOD_LINKAGE7(result, __cdecl, name, args)
+#define DEFINE_METHOD8(result, name, args) DEFINE_METHOD_LINKAGE8(result, __cdecl, name, args)
+#define DEFINE_METHOD9(result, name, args) DEFINE_METHOD_LINKAGE9(result, __cdecl, name, args)
+#define DEFINE_METHOD10(result, name, args) DEFINE_METHOD_LINKAGE10(result, __cdecl, name, args)
+#define DEFINE_METHOD11(result, name, args) DEFINE_METHOD_LINKAGE11(result, __cdecl, name, args)
+
+#ifdef TARGET_WINDOWS
+///////////////////////////////////////////////////////////
+//
+// DEFINE_FUNC_ALIGNED 0-X
+//
+// Defines a function for an export from a dll, which
+// requires an aligned stack on function call
+// Use DEFINE_FUNC_ALIGNED for each function to be resolved.
+//
+// result: Result of the function
+// linkage: Calling convention of the function
+// name: Name of the function
+// args: Argument types of the function
+//
+// Actual function call will expand to something like this
+// this will align the stack (esp) at the point of function
+// entry as required by gcc compiled dlls, it is a bit obfuscated
+// to allow for different sized variables
+//
+// int64_t test(int64_t p1, char p2, char p3)
+// {
+// int o,s = ((sizeof(p1)+3)&~3)+((sizeof(p2)+3)&~3)+((sizeof(p3)+3)&~3);
+// __asm mov [o],esp;
+// __asm sub esp, [s];
+// __asm and esp, ~15;
+// __asm add esp, [s]
+// m_test(p1, p2, p3); //return value will still be correct as long as we don't mess with it
+// __asm mov esp,[o];
+// };
+
+#define ALS(a) ((sizeof(a)+3)&~3)
+#define DEFINE_FUNC_PART1(result, linkage, name, args) \
+ private: \
+ typedef result (linkage * name##_type)##args; \
+ union { \
+ name##_type m_##name; \
+ void* m_##name##_ptr; \
+ }; \
+ public: \
+ virtual result name##args
+
+#define DEFINE_FUNC_PART2(size) \
+ { \
+ int o,s = size; \
+ __asm { \
+ __asm mov [o], esp \
+ __asm sub esp, [s] \
+ __asm and esp, ~15 \
+ __asm add esp, [s] \
+ }
+
+#define DEFINE_FUNC_PART3(name,args) \
+ m_##name##args; \
+ __asm { \
+ __asm mov esp,[o] \
+ } \
+ }
+
+#define DEFINE_FUNC_ALIGNED0(result, linkage, name) \
+ DEFINE_FUNC_PART1(result, linkage, name, ()) \
+ DEFINE_FUNC_PART2(0) \
+ DEFINE_FUNC_PART3(name,())
+
+#define DEFINE_FUNC_ALIGNED1(result, linkage, name, t1) \
+ DEFINE_FUNC_PART1(result, linkage, name, (t1 p1)) \
+ DEFINE_FUNC_PART2(ALS(p1)) \
+ DEFINE_FUNC_PART3(name,(p1))
+
+#define DEFINE_FUNC_ALIGNED2(result, linkage, name, t1, t2) \
+ DEFINE_FUNC_PART1(result, linkage, name, (t1 p1, t2 p2)) \
+ DEFINE_FUNC_PART2(ALS(p1)+ALS(p2)) \
+ DEFINE_FUNC_PART3(name,(p1, p2))
+
+#define DEFINE_FUNC_ALIGNED3(result, linkage, name, t1, t2, t3) \
+ DEFINE_FUNC_PART1(result, linkage, name, (t1 p1, t2 p2, t3 p3)) \
+ DEFINE_FUNC_PART2(ALS(p1)+ALS(p2)+ALS(p3)) \
+ DEFINE_FUNC_PART3(name,(p1, p2, p3))
+
+#define DEFINE_FUNC_ALIGNED4(result, linkage, name, t1, t2, t3, t4) \
+ DEFINE_FUNC_PART1(result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4)) \
+ DEFINE_FUNC_PART2(ALS(p1)+ALS(p2)+ALS(p3)+ALS(p4)) \
+ DEFINE_FUNC_PART3(name,(p1, p2, p3, p4))
+
+#define DEFINE_FUNC_ALIGNED5(result, linkage, name, t1, t2, t3, t4, t5) \
+ DEFINE_FUNC_PART1(result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5)) \
+ DEFINE_FUNC_PART2(ALS(p1)+ALS(p2)+ALS(p3)+ALS(p4)+ALS(p5)) \
+ DEFINE_FUNC_PART3(name,(p1, p2, p3, p4, p5))
+
+#define DEFINE_FUNC_ALIGNED6(result, linkage, name, t1, t2, t3, t4, t5, t6) \
+ DEFINE_FUNC_PART1(result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5, t6 p6)) \
+ DEFINE_FUNC_PART2(ALS(p1)+ALS(p2)+ALS(p3)+ALS(p4)+ALS(p5)+ALS(p6)) \
+ DEFINE_FUNC_PART3(name,(p1, p2, p3, p4, p5, p6))
+
+#define DEFINE_FUNC_ALIGNED7(result, linkage, name, t1, t2, t3, t4, t5, t6, t7) \
+ DEFINE_FUNC_PART1(result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5, t6 p6, t7 p7)) \
+ DEFINE_FUNC_PART2(ALS(p1)+ALS(p2)+ALS(p3)+ALS(p4)+ALS(p5)+ALS(p6)+ALS(p7)) \
+ DEFINE_FUNC_PART3(name,(p1, p2, p3, p4, p5, p6, p7))
+
+#define DEFINE_FUNC_ALIGNED8(result, linkage, name, t1, t2, t3, t4, t5, t6, t7, t8) \
+ DEFINE_FUNC_PART1(result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5, t6 p6, t7 p7, t8 p8)) \
+ DEFINE_FUNC_PART2(ALS(p1)+ALS(p2)+ALS(p3)+ALS(p4)+ALS(p5)+ALS(p6)+ALS(p7)+ALS(p8)) \
+ DEFINE_FUNC_PART3(name,(p1, p2, p3, p4, p5, p6, p7, p8))
+
+#define DEFINE_FUNC_ALIGNED9(result, linkage, name, t1, t2, t3, t4, t5, t6, t7, t8, t9) \
+ DEFINE_FUNC_PART1(result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5, t6 p6, t7 p7, t8 p8, t9 p9)) \
+ DEFINE_FUNC_PART2(ALS(p1)+ALS(p2)+ALS(p3)+ALS(p4)+ALS(p5)+ALS(p6)+ALS(p7)+ALS(p8)+ALS(p9)) \
+ DEFINE_FUNC_PART3(name,(p1, p2, p3, p4, p5, p6, p7, p8, p9))
+
+#else
+
+#define DEFINE_FUNC_ALIGNED0(result, linkage, name) DEFINE_METHOD_LINKAGE0 (result, linkage, name)
+#define DEFINE_FUNC_ALIGNED1(result, linkage, name, t1) DEFINE_METHOD_LINKAGE1 (result, linkage, name, (t1 p1) )
+#define DEFINE_FUNC_ALIGNED2(result, linkage, name, t1, t2) DEFINE_METHOD_LINKAGE2 (result, linkage, name, (t1 p1, t2 p2) )
+#define DEFINE_FUNC_ALIGNED3(result, linkage, name, t1, t2, t3) DEFINE_METHOD_LINKAGE3 (result, linkage, name, (t1 p1, t2 p2, t3 p3) )
+#define DEFINE_FUNC_ALIGNED4(result, linkage, name, t1, t2, t3, t4) DEFINE_METHOD_LINKAGE4 (result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4) )
+#define DEFINE_FUNC_ALIGNED5(result, linkage, name, t1, t2, t3, t4, t5) DEFINE_METHOD_LINKAGE5 (result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5) )
+#define DEFINE_FUNC_ALIGNED6(result, linkage, name, t1, t2, t3, t4, t5, t6) DEFINE_METHOD_LINKAGE6 (result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5, t6 p6) )
+#define DEFINE_FUNC_ALIGNED7(result, linkage, name, t1, t2, t3, t4, t5, t6, t7) DEFINE_METHOD_LINKAGE7 (result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5, t6 p6, t7 p7) )
+#define DEFINE_FUNC_ALIGNED8(result, linkage, name, t1, t2, t3, t4, t5, t6, t7, t8) DEFINE_METHOD_LINKAGE8 (result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5, t6 p6, t7 p7, t8 p8) )
+#define DEFINE_FUNC_ALIGNED9(result, linkage, name, t1, t2, t3, t4, t5, t6, t7, t8, t9) DEFINE_METHOD_LINKAGE9 (result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5, t6 p6, t7 p7, t8 p8, t9 p9) )
+#define DEFINE_FUNC_ALIGNED10(result, linkage, name, t1, t2, t3, t4, t5, t6, t7, t8, t10) DEFINE_METHOD_LINKAGE10(result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5, t6 p6, t7 p7, t8 p8, t9 p9, t10 p10) )
+#define DEFINE_FUNC_ALIGNED11(result, linkage, name, t1, t2, t3, t4, t5, t6, t7, t8, t10, t11) DEFINE_METHOD_LINKAGE11(result, linkage, name, (t1 p1, t2 p2, t3 p3, t4 p4, t5 p5, t6 p6, t7 p7, t8 p8, t9 p9, t10 p10, t11 p11) )
+
+#endif
+
+///////////////////////////////////////////////////////////
+//
+// BEGIN_METHOD_RESOLVE/END_METHOD_RESOLVE
+//
+// Defines a method that resolves the exported functions
+// defined with DEFINE_METHOD or DEFINE_METHOD_LINKAGE.
+// There must be a RESOLVE_METHOD or RESOLVE_METHOD_RENAME
+// for each DEFINE_METHOD or DEFINE_METHOD_LINKAGE within this
+// block. This block must be followed by an END_METHOD_RESOLVE.
+//
+#define BEGIN_METHOD_RESOLVE() \
+ protected: \
+ virtual bool ResolveExports() override \
+ {
+
+#define END_METHOD_RESOLVE() \
+ return true; \
+ }
+
+///////////////////////////////////////////////////////////
+//
+// RESOLVE_METHOD
+//
+// Resolves a method from a dll
+//
+// method: Name of the method defined with DEFINE_METHOD
+// or DEFINE_METHOD_LINKAGE
+//
+#define RESOLVE_METHOD(method) \
+ if (!m_dll->ResolveExport( #method , & m_##method##_ptr )) \
+ return false;
+
+#define RESOLVE_METHOD_FP(method) \
+ if (!m_dll->ResolveExport( #method , & m_##method##_ptr )) \
+ return false;
+
+
+///////////////////////////////////////////////////////////
+//
+// RESOLVE_METHOD_OPTIONAL
+//
+// Resolves a method from a dll. does not abort if the
+// method is missing
+//
+// method: Name of the method defined with DEFINE_METHOD
+// or DEFINE_METHOD_LINKAGE
+//
+
+#define RESOLVE_METHOD_OPTIONAL(method) \
+ m_##method##_ptr = nullptr; \
+ m_dll->ResolveExport( #method , & m_##method##_ptr, false );
+
+#define RESOLVE_METHOD_OPTIONAL_FP(method) \
+ m_##method##_ptr = NULL; \
+ m_dll->ResolveExport( #method , & m_##method##_ptr, false );
+
+
+
+///////////////////////////////////////////////////////////
+//
+// RESOLVE_METHOD_RENAME
+//
+// Resolves a method from a dll
+//
+// dllmethod: Name of the function exported from the dll
+// method: Name of the method defined with DEFINE_METHOD
+// or DEFINE_METHOD_LINKAGE
+//
+#define RESOLVE_METHOD_RENAME(dllmethod, method) \
+ if (!m_dll->ResolveExport( #dllmethod , & m_##method##_ptr )) \
+ return false;
+
+#define RESOLVE_METHOD_RENAME_OPTIONAL(dllmethod, method) \
+ m_##method##_ptr = nullptr; \
+ m_dll->ResolveExport( #dllmethod , & m_##method##_ptr, false );
+
+#define RESOLVE_METHOD_RENAME_FP(dllmethod, method) \
+ if (!m_dll->ResolveExport( #dllmethod , & method##_ptr )) \
+ return false;
+
+
+////////////////////////////////////////////////////////////////////
+//
+// Example declaration of a dll wrapper class
+//
+// 1. Define a class with pure virtual functions with all functions
+// exported from the dll. This is needed to use the IntelliSence
+// feature of the Visual Studio Editor.
+//
+// class DllExampleInterface
+// {
+// public:
+// virtual void foo (unsigned int type, char* szTest)=0;
+// virtual void bar (char* szTest, unsigned int type)=0;
+// };
+//
+// 2. Define a class, derived from DllDynamic and the previously defined
+// interface class. Define the constructor of the class using the
+// DECLARE_DLL_WRAPPER macro. Use the DEFINE_METHODX/DEFINE_METHOD_LINKAGEX
+// macros to define the functions from the interface above, where X is number of
+// parameters the function has. The function parameters
+// have to be enclosed in parentheses. The parameter names have to be changed to px
+// where x is the number on which position the parameter appears.
+// Use the RESOLVE_METHOD/RESOLVE_METHOD_RENAME to do the actually resolve the functions
+// from the dll when it's loaded. The RESOLVE_METHOD/RESOLVE_METHOD_RENAME have to
+// be between the BEGIN_METHOD_RESOLVE/END_METHOD_RESOLVE block.
+//
+// class DllExample : public DllDynamic, DllExampleInterface
+// {
+// DECLARE_DLL_WRAPPER(DllExample, special://xbmcbin/system/Example.dll)
+// LOAD_SYMBOLS() // add this if you want to load debug symbols for the dll
+// DEFINE_METHOD2(void, foo, (int p1, char* p2))
+// DEFINE_METHOD_LINKAGE2(void, __stdcall, bar, (char* p1, int p2))
+// DEFINE_METHOD_FP(void, foobar, (int type, char* szTest)) // No need to define this function in the
+// // interface class, as it's a function pointer.
+// // But its not recognised by IntelliSence
+// BEGIN_METHOD_RESOLVE()
+// RESOLVE_METHOD(foo)
+// RESOLVE_METHOD_RENAME("_bar@8", bar)
+// RESOLVE_METHOD_FP(foobar)
+// END_METHOD_RESOLVE()
+// };
+//
+// The above macros will expand to a class that will look like this
+//
+// class DllExample : public DllDynamic, DllExampleInterface
+// {
+// public:
+// DllExample() : DllDynamic( "special://xbmcbin/system/Example.dll" ) {}
+// protected:
+// virtual bool LoadSymbols() { return true; }
+// protected:
+// typedef void (* foo_METHOD) ( int p1, char* p2 );
+// foo_METHOD m_foo;
+// public:
+// virtual void foo( int p1, char* p2 )
+// {
+// return m_foo(p1, p2);
+// }
+// protected:
+// typedef void (__stdcall * bar_METHOD) ( char* p1, int p2 );
+// bar_METHOD m_bar;
+// public:
+// virtual void bar( char* p1, int p2 )
+// {
+// return m_bar(p1, p2);
+// }
+// protected:
+// typedef void (* foobar_METHOD) (int type, char* szTest);
+// public:
+// foobar_METHOD foobar;
+// protected:
+// virtual bool ResolveExports()
+// {
+// return (
+// m_dll->ResolveExport( "foo", (void**)& m_foo ) &&
+// m_dll->ResolveExport( "_bar@8", (void**)& m_bar ) &&
+// m_dll->ResolveExport( "foobar" , (void**)& foobar ) &&
+// 1
+// );
+// }
+// };
+//
+// Usage of the class
+//
+// DllExample dll;
+// dll.Load();
+// if (dll.IsLoaded())
+// {
+// dll.foo(1, "bar");
+// dll.Unload();
+// }
+//
+
+///////////////////////////////////////////////////////////
+//
+// Baseclass for a Dynamically loaded dll
+// use the above macros to create a dll wrapper
+//
+class DllDynamic
+{
+public:
+ DllDynamic();
+ explicit DllDynamic(const std::string& strDllName);
+ virtual ~DllDynamic();
+ virtual bool Load();
+ virtual void Unload();
+ virtual bool IsLoaded() const { return m_dll!=NULL; }
+ bool CanLoad();
+ bool EnableDelayedUnload(bool bOnOff);
+ bool SetFile(const std::string& strDllName);
+ const std::string &GetFile() const { return m_strDllName; }
+
+protected:
+ virtual bool ResolveExports()=0;
+ virtual bool LoadSymbols() { return false; }
+ bool m_DelayUnload;
+ LibraryLoader* m_dll;
+ std::string m_strDllName;
+};
diff --git a/xbmc/FileItem.cpp b/xbmc/FileItem.cpp
new file mode 100644
index 0000000..2eb5eb0
--- /dev/null
+++ b/xbmc/FileItem.cpp
@@ -0,0 +1,4036 @@
+/*
+ * Copyright (C) 2005-2020 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 "FileItem.h"
+
+#include "CueDocument.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "events/IEvent.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/StackDirectory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "filesystem/VideoDatabaseDirectory/QueryParams.h"
+#include "games/GameUtils.h"
+#include "games/tags/GameInfoTag.h"
+#include "guilib/LocalizeStrings.h"
+#include "media/MediaLockState.h"
+#include "music/Album.h"
+#include "music/Artist.h"
+#include "music/MusicDatabase.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/tags/MusicInfoTagLoaderFactory.h"
+#include "pictures/PictureInfoTag.h"
+#include "playlists/PlayListFactory.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingUtils.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/Archive.h"
+#include "utils/Crc32.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/Mime.h"
+#include "utils/Random.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/Bookmark.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoInfoTag.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <mutex>
+
+using namespace KODI;
+using namespace XFILE;
+using namespace PLAYLIST;
+using namespace MUSIC_INFO;
+using namespace PVR;
+using namespace GAME;
+
+CFileItem::CFileItem(const CSong& song)
+{
+ Initialize();
+ SetFromSong(song);
+}
+
+CFileItem::CFileItem(const CSong& song, const CMusicInfoTag& music)
+{
+ Initialize();
+ SetFromSong(song);
+ *GetMusicInfoTag() = music;
+}
+
+CFileItem::CFileItem(const CURL &url, const CAlbum& album)
+{
+ Initialize();
+
+ m_strPath = url.Get();
+ URIUtils::AddSlashAtEnd(m_strPath);
+ SetFromAlbum(album);
+}
+
+CFileItem::CFileItem(const std::string &path, const CAlbum& album)
+{
+ Initialize();
+
+ m_strPath = path;
+ URIUtils::AddSlashAtEnd(m_strPath);
+ SetFromAlbum(album);
+}
+
+CFileItem::CFileItem(const CMusicInfoTag& music)
+{
+ Initialize();
+ SetLabel(music.GetTitle());
+ m_strPath = music.GetURL();
+ m_bIsFolder = URIUtils::HasSlashAtEnd(m_strPath);
+ *GetMusicInfoTag() = music;
+ FillInDefaultIcon();
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(const CVideoInfoTag& movie)
+{
+ Initialize();
+ SetFromVideoInfoTag(movie);
+}
+
+namespace
+{
+ std::string GetEpgTagTitle(const std::shared_ptr<CPVREpgInfoTag>& epgTag)
+ {
+ if (CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ return g_localizeStrings.Get(19266); // Parental locked
+ else if (epgTag->Title().empty() &&
+ !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_EPG_HIDENOINFOAVAILABLE))
+ return g_localizeStrings.Get(19055); // no information available
+ else
+ return epgTag->Title();
+ }
+} // unnamed namespace
+
+void CFileItem::FillMusicInfoTag(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ CMusicInfoTag* musictag = GetMusicInfoTag(); // create (!) the music tag.
+
+ if (tag)
+ {
+ musictag->SetTitle(GetEpgTagTitle(tag));
+ musictag->SetGenre(tag->Genre());
+ musictag->SetDuration(tag->GetDuration());
+ musictag->SetURL(tag->Path());
+ }
+ else if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_EPG_HIDENOINFOAVAILABLE))
+ {
+ musictag->SetTitle(g_localizeStrings.Get(19055)); // no information available
+ }
+
+ musictag->SetLoaded(true);
+}
+
+CFileItem::CFileItem(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ Initialize();
+
+ m_bIsFolder = false;
+ m_epgInfoTag = tag;
+ m_strPath = tag->Path();
+ m_bCanQueue = false;
+ SetLabel(GetEpgTagTitle(tag));
+ m_dateTime = tag->StartAsLocalTime();
+
+ if (!tag->IconPath().empty())
+ {
+ SetArt("icon", tag->IconPath());
+ }
+ else
+ {
+ const std::string iconPath = tag->ChannelIconPath();
+ if (!iconPath.empty())
+ SetArt("icon", iconPath);
+ else if (tag->IsRadio())
+ SetArt("icon", "DefaultMusicSongs.png");
+ else
+ SetArt("icon", "DefaultTVShows.png");
+ }
+
+ // Speedup FillInDefaultIcon()
+ SetProperty("icon_never_overlay", true);
+
+ if (tag->IsRadio() && !HasMusicInfoTag())
+ FillMusicInfoTag(tag);
+
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(const std::shared_ptr<PVR::CPVREpgSearchFilter>& filter)
+{
+ Initialize();
+
+ m_bIsFolder = true;
+ m_epgSearchFilter = filter;
+ m_strPath = filter->GetPath();
+ m_bCanQueue = false;
+ SetLabel(filter->GetTitle());
+
+ const CDateTime lastExec = filter->GetLastExecutedDateTime();
+ if (lastExec.IsValid())
+ m_dateTime.SetFromUTCDateTime(lastExec);
+
+ SetArt("icon", "DefaultPVRSearch.png");
+
+ // Speedup FillInDefaultIcon()
+ SetProperty("icon_never_overlay", true);
+
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(const std::shared_ptr<CPVRChannelGroupMember>& channelGroupMember)
+{
+ Initialize();
+
+ const std::shared_ptr<CPVRChannel> channel = channelGroupMember->Channel();
+
+ m_pvrChannelGroupMemberInfoTag = channelGroupMember;
+
+ m_strPath = channelGroupMember->Path();
+ m_bIsFolder = false;
+ m_bCanQueue = false;
+ SetLabel(channel->ChannelName());
+
+ if (!channel->IconPath().empty())
+ SetArt("icon", channel->IconPath());
+ else if (channel->IsRadio())
+ SetArt("icon", "DefaultMusicSongs.png");
+ else
+ SetArt("icon", "DefaultTVShows.png");
+
+ SetProperty("channelid", channel->ChannelID());
+ SetProperty("path", channelGroupMember->Path());
+ SetArt("thumb", channel->IconPath());
+
+ // Speedup FillInDefaultIcon()
+ SetProperty("icon_never_overlay", true);
+
+ if (channel->IsRadio() && !HasMusicInfoTag())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgNow = channel->GetEPGNow();
+ FillMusicInfoTag(epgNow);
+ }
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(const std::shared_ptr<CPVRRecording>& record)
+{
+ Initialize();
+
+ m_bIsFolder = false;
+ m_pvrRecordingInfoTag = record;
+ m_strPath = record->m_strFileNameAndPath;
+ SetLabel(record->m_strTitle);
+ m_dateTime = record->RecordingTimeAsLocalTime();
+ m_dwSize = record->GetSizeInBytes();
+ m_bCanQueue = true;
+
+ // Set art
+ if (!record->IconPath().empty())
+ SetArt("icon", record->IconPath());
+ else
+ {
+ const std::shared_ptr<CPVRChannel> channel = record->Channel();
+ if (channel && !channel->IconPath().empty())
+ SetArt("icon", channel->IconPath());
+ else if (record->IsRadio())
+ SetArt("icon", "DefaultMusicSongs.png");
+ else
+ SetArt("icon", "DefaultTVShows.png");
+ }
+
+ if (!record->ThumbnailPath().empty())
+ SetArt("thumb", record->ThumbnailPath());
+
+ if (!record->FanartPath().empty())
+ SetArt("fanart", record->FanartPath());
+
+ // Speedup FillInDefaultIcon()
+ SetProperty("icon_never_overlay", true);
+
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(const std::shared_ptr<CPVRTimerInfoTag>& timer)
+{
+ Initialize();
+
+ m_bIsFolder = timer->IsTimerRule();
+ m_pvrTimerInfoTag = timer;
+ m_strPath = timer->Path();
+ SetLabel(timer->Title());
+ m_dateTime = timer->StartAsLocalTime();
+ m_bCanQueue = false;
+
+ if (!timer->ChannelIcon().empty())
+ SetArt("icon", timer->ChannelIcon());
+ else if (timer->IsRadio())
+ SetArt("icon", "DefaultMusicSongs.png");
+ else
+ SetArt("icon", "DefaultTVShows.png");
+
+ // Speedup FillInDefaultIcon()
+ SetProperty("icon_never_overlay", true);
+
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(const CArtist& artist)
+{
+ Initialize();
+ SetLabel(artist.strArtist);
+ m_strPath = artist.strArtist;
+ m_bIsFolder = true;
+ URIUtils::AddSlashAtEnd(m_strPath);
+ GetMusicInfoTag()->SetArtist(artist);
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(const CGenre& genre)
+{
+ Initialize();
+ SetLabel(genre.strGenre);
+ m_strPath = genre.strGenre;
+ m_bIsFolder = true;
+ URIUtils::AddSlashAtEnd(m_strPath);
+ GetMusicInfoTag()->SetGenre(genre.strGenre);
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(const CFileItem& item)
+ : CGUIListItem(item),
+ m_musicInfoTag(NULL),
+ m_videoInfoTag(NULL),
+ m_pictureInfoTag(NULL),
+ m_gameInfoTag(NULL)
+{
+ *this = item;
+}
+
+CFileItem::CFileItem(const CGUIListItem& item)
+{
+ Initialize();
+ // not particularly pretty, but it gets around the issue of Initialize() defaulting
+ // parameters in the CGUIListItem base class.
+ *static_cast<CGUIListItem*>(this) = item;
+
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(void)
+{
+ Initialize();
+}
+
+CFileItem::CFileItem(const std::string& strLabel)
+{
+ Initialize();
+ SetLabel(strLabel);
+}
+
+CFileItem::CFileItem(const char* strLabel)
+{
+ Initialize();
+ SetLabel(std::string(strLabel));
+}
+
+CFileItem::CFileItem(const CURL& path, bool bIsFolder)
+{
+ Initialize();
+ m_strPath = path.Get();
+ m_bIsFolder = bIsFolder;
+ if (m_bIsFolder && !m_strPath.empty() && !IsFileFolder())
+ URIUtils::AddSlashAtEnd(m_strPath);
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(const std::string& strPath, bool bIsFolder)
+{
+ Initialize();
+ m_strPath = strPath;
+ m_bIsFolder = bIsFolder;
+ if (m_bIsFolder && !m_strPath.empty() && !IsFileFolder())
+ URIUtils::AddSlashAtEnd(m_strPath);
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(const CMediaSource& share)
+{
+ Initialize();
+ m_bIsFolder = true;
+ m_bIsShareOrDrive = true;
+ m_strPath = share.strPath;
+ if (!IsRSS()) // no slash at end for rss feeds
+ URIUtils::AddSlashAtEnd(m_strPath);
+ std::string label = share.strName;
+ if (!share.strStatus.empty())
+ label = StringUtils::Format("{} ({})", share.strName, share.strStatus);
+ SetLabel(label);
+ m_iLockMode = share.m_iLockMode;
+ m_strLockCode = share.m_strLockCode;
+ m_iHasLock = share.m_iHasLock;
+ m_iBadPwdCount = share.m_iBadPwdCount;
+ m_iDriveType = share.m_iDriveType;
+ SetArt("thumb", share.m_strThumbnailImage);
+ SetLabelPreformatted(true);
+ if (IsDVD())
+ GetVideoInfoTag()->m_strFileNameAndPath = share.strDiskUniqueId; // share.strDiskUniqueId contains disc unique id
+ FillInMimeType(false);
+}
+
+CFileItem::CFileItem(std::shared_ptr<const ADDON::IAddon> addonInfo) : m_addonInfo(std::move(addonInfo))
+{
+ Initialize();
+}
+
+CFileItem::CFileItem(const EventPtr& eventLogEntry)
+{
+ Initialize();
+
+ m_eventLogEntry = eventLogEntry;
+ SetLabel(eventLogEntry->GetLabel());
+ m_dateTime = eventLogEntry->GetDateTime();
+ if (!eventLogEntry->GetIcon().empty())
+ SetArt("icon", eventLogEntry->GetIcon());
+}
+
+CFileItem::~CFileItem(void)
+{
+ delete m_musicInfoTag;
+ delete m_videoInfoTag;
+ delete m_pictureInfoTag;
+ delete m_gameInfoTag;
+
+ m_musicInfoTag = NULL;
+ m_videoInfoTag = NULL;
+ m_pictureInfoTag = NULL;
+ m_gameInfoTag = NULL;
+}
+
+CFileItem& CFileItem::operator=(const CFileItem& item)
+{
+ if (this == &item)
+ return *this;
+
+ CGUIListItem::operator=(item);
+ m_bLabelPreformatted=item.m_bLabelPreformatted;
+ FreeMemory();
+ m_strPath = item.m_strPath;
+ m_strDynPath = item.m_strDynPath;
+ m_bIsParentFolder = item.m_bIsParentFolder;
+ m_iDriveType = item.m_iDriveType;
+ m_bIsShareOrDrive = item.m_bIsShareOrDrive;
+ m_dateTime = item.m_dateTime;
+ m_dwSize = item.m_dwSize;
+
+ if (item.m_musicInfoTag)
+ {
+ if (m_musicInfoTag)
+ *m_musicInfoTag = *item.m_musicInfoTag;
+ else
+ m_musicInfoTag = new MUSIC_INFO::CMusicInfoTag(*item.m_musicInfoTag);
+ }
+ else
+ {
+ delete m_musicInfoTag;
+ m_musicInfoTag = NULL;
+ }
+
+ if (item.m_videoInfoTag)
+ {
+ if (m_videoInfoTag)
+ *m_videoInfoTag = *item.m_videoInfoTag;
+ else
+ m_videoInfoTag = new CVideoInfoTag(*item.m_videoInfoTag);
+ }
+ else
+ {
+ delete m_videoInfoTag;
+ m_videoInfoTag = NULL;
+ }
+
+ if (item.m_pictureInfoTag)
+ {
+ if (m_pictureInfoTag)
+ *m_pictureInfoTag = *item.m_pictureInfoTag;
+ else
+ m_pictureInfoTag = new CPictureInfoTag(*item.m_pictureInfoTag);
+ }
+ else
+ {
+ delete m_pictureInfoTag;
+ m_pictureInfoTag = NULL;
+ }
+
+ if (item.m_gameInfoTag)
+ {
+ if (m_gameInfoTag)
+ *m_gameInfoTag = *item.m_gameInfoTag;
+ else
+ m_gameInfoTag = new CGameInfoTag(*item.m_gameInfoTag);
+ }
+ else
+ {
+ delete m_gameInfoTag;
+ m_gameInfoTag = NULL;
+ }
+
+ m_epgInfoTag = item.m_epgInfoTag;
+ m_epgSearchFilter = item.m_epgSearchFilter;
+ m_pvrChannelGroupMemberInfoTag = item.m_pvrChannelGroupMemberInfoTag;
+ m_pvrRecordingInfoTag = item.m_pvrRecordingInfoTag;
+ m_pvrTimerInfoTag = item.m_pvrTimerInfoTag;
+ m_addonInfo = item.m_addonInfo;
+ m_eventLogEntry = item.m_eventLogEntry;
+
+ m_lStartOffset = item.m_lStartOffset;
+ m_lStartPartNumber = item.m_lStartPartNumber;
+ m_lEndOffset = item.m_lEndOffset;
+ m_strDVDLabel = item.m_strDVDLabel;
+ m_strTitle = item.m_strTitle;
+ m_iprogramCount = item.m_iprogramCount;
+ m_idepth = item.m_idepth;
+ m_iLockMode = item.m_iLockMode;
+ m_strLockCode = item.m_strLockCode;
+ m_iHasLock = item.m_iHasLock;
+ m_iBadPwdCount = item.m_iBadPwdCount;
+ m_bCanQueue=item.m_bCanQueue;
+ m_mimetype = item.m_mimetype;
+ m_extrainfo = item.m_extrainfo;
+ m_specialSort = item.m_specialSort;
+ m_bIsAlbum = item.m_bIsAlbum;
+ m_doContentLookup = item.m_doContentLookup;
+ return *this;
+}
+
+void CFileItem::Initialize()
+{
+ m_musicInfoTag = NULL;
+ m_videoInfoTag = NULL;
+ m_pictureInfoTag = NULL;
+ m_gameInfoTag = NULL;
+ m_bLabelPreformatted = false;
+ m_bIsAlbum = false;
+ m_dwSize = 0;
+ m_bIsParentFolder = false;
+ m_bIsShareOrDrive = false;
+ m_iDriveType = CMediaSource::SOURCE_TYPE_UNKNOWN;
+ m_lStartOffset = 0;
+ m_lStartPartNumber = 1;
+ m_lEndOffset = 0;
+ m_iprogramCount = 0;
+ m_idepth = 1;
+ m_iLockMode = LOCK_MODE_EVERYONE;
+ m_iBadPwdCount = 0;
+ m_iHasLock = LOCK_STATE_NO_LOCK;
+ m_bCanQueue = true;
+ m_specialSort = SortSpecialNone;
+ m_doContentLookup = true;
+}
+
+void CFileItem::Reset()
+{
+ // CGUIListItem members...
+ m_strLabel2.clear();
+ SetLabel("");
+ FreeIcons();
+ m_overlayIcon = ICON_OVERLAY_NONE;
+ m_bSelected = false;
+ m_bIsFolder = false;
+
+ m_strDVDLabel.clear();
+ m_strTitle.clear();
+ m_strPath.clear();
+ m_strDynPath.clear();
+ m_dateTime.Reset();
+ m_strLockCode.clear();
+ m_mimetype.clear();
+ delete m_musicInfoTag;
+ m_musicInfoTag=NULL;
+ delete m_videoInfoTag;
+ m_videoInfoTag=NULL;
+ m_epgInfoTag.reset();
+ m_epgSearchFilter.reset();
+ m_pvrChannelGroupMemberInfoTag.reset();
+ m_pvrRecordingInfoTag.reset();
+ m_pvrTimerInfoTag.reset();
+ delete m_pictureInfoTag;
+ m_pictureInfoTag=NULL;
+ delete m_gameInfoTag;
+ m_gameInfoTag = NULL;
+ m_extrainfo.clear();
+ ClearProperties();
+ m_eventLogEntry.reset();
+
+ Initialize();
+ SetInvalid();
+}
+
+// do not archive dynamic path
+void CFileItem::Archive(CArchive& ar)
+{
+ CGUIListItem::Archive(ar);
+
+ if (ar.IsStoring())
+ {
+ ar << m_bIsParentFolder;
+ ar << m_bLabelPreformatted;
+ ar << m_strPath;
+ ar << m_bIsShareOrDrive;
+ ar << m_iDriveType;
+ ar << m_dateTime;
+ ar << m_dwSize;
+ ar << m_strDVDLabel;
+ ar << m_strTitle;
+ ar << m_iprogramCount;
+ ar << m_idepth;
+ ar << m_lStartOffset;
+ ar << m_lStartPartNumber;
+ ar << m_lEndOffset;
+ ar << m_iLockMode;
+ ar << m_strLockCode;
+ ar << m_iBadPwdCount;
+
+ ar << m_bCanQueue;
+ ar << m_mimetype;
+ ar << m_extrainfo;
+ ar << m_specialSort;
+ ar << m_doContentLookup;
+
+ if (m_musicInfoTag)
+ {
+ ar << 1;
+ ar << *m_musicInfoTag;
+ }
+ else
+ ar << 0;
+ if (m_videoInfoTag)
+ {
+ ar << 1;
+ ar << *m_videoInfoTag;
+ }
+ else
+ ar << 0;
+ if (m_pictureInfoTag)
+ {
+ ar << 1;
+ ar << *m_pictureInfoTag;
+ }
+ else
+ ar << 0;
+ if (m_gameInfoTag)
+ {
+ ar << 1;
+ ar << *m_gameInfoTag;
+ }
+ else
+ ar << 0;
+ }
+ else
+ {
+ ar >> m_bIsParentFolder;
+ ar >> m_bLabelPreformatted;
+ ar >> m_strPath;
+ ar >> m_bIsShareOrDrive;
+ ar >> m_iDriveType;
+ ar >> m_dateTime;
+ ar >> m_dwSize;
+ ar >> m_strDVDLabel;
+ ar >> m_strTitle;
+ ar >> m_iprogramCount;
+ ar >> m_idepth;
+ ar >> m_lStartOffset;
+ ar >> m_lStartPartNumber;
+ ar >> m_lEndOffset;
+ int temp;
+ ar >> temp;
+ m_iLockMode = (LockType)temp;
+ ar >> m_strLockCode;
+ ar >> m_iBadPwdCount;
+
+ ar >> m_bCanQueue;
+ ar >> m_mimetype;
+ ar >> m_extrainfo;
+ ar >> temp;
+ m_specialSort = (SortSpecial)temp;
+ ar >> m_doContentLookup;
+
+ int iType;
+ ar >> iType;
+ if (iType == 1)
+ ar >> *GetMusicInfoTag();
+ ar >> iType;
+ if (iType == 1)
+ ar >> *GetVideoInfoTag();
+ ar >> iType;
+ if (iType == 1)
+ ar >> *GetPictureInfoTag();
+ ar >> iType;
+ if (iType == 1)
+ ar >> *GetGameInfoTag();
+
+ SetInvalid();
+ }
+}
+
+void CFileItem::Serialize(CVariant& value) const
+{
+ //CGUIListItem::Serialize(value["CGUIListItem"]);
+
+ value["strPath"] = m_strPath;
+ value["dateTime"] = (m_dateTime.IsValid()) ? m_dateTime.GetAsRFC1123DateTime() : "";
+ value["lastmodified"] = m_dateTime.IsValid() ? m_dateTime.GetAsDBDateTime() : "";
+ value["size"] = m_dwSize;
+ value["DVDLabel"] = m_strDVDLabel;
+ value["title"] = m_strTitle;
+ value["mimetype"] = m_mimetype;
+ value["extrainfo"] = m_extrainfo;
+
+ if (m_musicInfoTag)
+ (*m_musicInfoTag).Serialize(value["musicInfoTag"]);
+
+ if (m_videoInfoTag)
+ (*m_videoInfoTag).Serialize(value["videoInfoTag"]);
+
+ if (m_pictureInfoTag)
+ (*m_pictureInfoTag).Serialize(value["pictureInfoTag"]);
+
+ if (m_gameInfoTag)
+ (*m_gameInfoTag).Serialize(value["gameInfoTag"]);
+
+ if (!m_mapProperties.empty())
+ {
+ auto& customProperties = value["customproperties"];
+ for (const auto& prop : m_mapProperties)
+ customProperties[prop.first] = prop.second;
+ }
+}
+
+void CFileItem::ToSortable(SortItem &sortable, Field field) const
+{
+ switch (field)
+ {
+ case FieldPath:
+ sortable[FieldPath] = m_strPath;
+ break;
+ case FieldDate:
+ sortable[FieldDate] = (m_dateTime.IsValid()) ? m_dateTime.GetAsDBDateTime() : "";
+ break;
+ case FieldSize:
+ sortable[FieldSize] = m_dwSize;
+ break;
+ case FieldDriveType:
+ sortable[FieldDriveType] = m_iDriveType;
+ break;
+ case FieldStartOffset:
+ sortable[FieldStartOffset] = m_lStartOffset;
+ break;
+ case FieldEndOffset:
+ sortable[FieldEndOffset] = m_lEndOffset;
+ break;
+ case FieldProgramCount:
+ sortable[FieldProgramCount] = m_iprogramCount;
+ break;
+ case FieldBitrate:
+ sortable[FieldBitrate] = m_dwSize;
+ break;
+ case FieldTitle:
+ sortable[FieldTitle] = m_strTitle;
+ break;
+
+ // If there's ever a need to convert more properties from CGUIListItem it might be
+ // worth to make CGUIListItem implement ISortable as well and call it from here
+
+ default:
+ break;
+ }
+
+ if (HasMusicInfoTag())
+ GetMusicInfoTag()->ToSortable(sortable, field);
+
+ if (HasVideoInfoTag())
+ GetVideoInfoTag()->ToSortable(sortable, field);
+
+ if (HasPictureInfoTag())
+ GetPictureInfoTag()->ToSortable(sortable, field);
+
+ if (HasPVRChannelInfoTag())
+ GetPVRChannelInfoTag()->ToSortable(sortable, field);
+
+ if (HasPVRChannelGroupMemberInfoTag())
+ GetPVRChannelGroupMemberInfoTag()->ToSortable(sortable, field);
+
+ if (HasAddonInfo())
+ {
+ switch (field)
+ {
+ case FieldInstallDate:
+ sortable[FieldInstallDate] = GetAddonInfo()->InstallDate().GetAsDBDateTime();
+ break;
+ case FieldLastUpdated:
+ sortable[FieldLastUpdated] = GetAddonInfo()->LastUpdated().GetAsDBDateTime();
+ break;
+ case FieldLastUsed:
+ sortable[FieldLastUsed] = GetAddonInfo()->LastUsed().GetAsDBDateTime();
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (HasGameInfoTag())
+ GetGameInfoTag()->ToSortable(sortable, field);
+
+ if (m_eventLogEntry)
+ m_eventLogEntry->ToSortable(sortable, field);
+
+ if (IsFavourite())
+ {
+ if (field == FieldUserPreference)
+ sortable[FieldUserPreference] = GetProperty("favourite.index").asString();
+ }
+}
+
+void CFileItem::ToSortable(SortItem &sortable, const Fields &fields) const
+{
+ Fields::const_iterator it;
+ for (it = fields.begin(); it != fields.end(); ++it)
+ ToSortable(sortable, *it);
+
+ /* FieldLabel is used as a fallback by all sorters and therefore has to be present as well */
+ sortable[FieldLabel] = GetLabel();
+ /* FieldSortSpecial and FieldFolder are required in conjunction with all other sorters as well */
+ sortable[FieldSortSpecial] = m_specialSort;
+ sortable[FieldFolder] = m_bIsFolder;
+}
+
+bool CFileItem::Exists(bool bUseCache /* = true */) const
+{
+ if (m_strPath.empty()
+ || IsPath("add")
+ || IsInternetStream()
+ || IsParentFolder()
+ || IsVirtualDirectoryRoot()
+ || IsPlugin()
+ || IsPVR())
+ return true;
+
+ if (IsVideoDb() && HasVideoInfoTag())
+ {
+ CFileItem dbItem(m_bIsFolder ? GetVideoInfoTag()->m_strPath : GetVideoInfoTag()->m_strFileNameAndPath, m_bIsFolder);
+ return dbItem.Exists();
+ }
+
+ std::string strPath = m_strPath;
+
+ if (URIUtils::IsMultiPath(strPath))
+ strPath = CMultiPathDirectory::GetFirstPath(strPath);
+
+ if (URIUtils::IsStack(strPath))
+ strPath = CStackDirectory::GetFirstStackedFile(strPath);
+
+ if (m_bIsFolder)
+ return CDirectory::Exists(strPath, bUseCache);
+ else
+ return CFile::Exists(strPath, bUseCache);
+
+ return false;
+}
+
+bool CFileItem::IsVideo() const
+{
+ /* check preset mime type */
+ if(StringUtils::StartsWithNoCase(m_mimetype, "video/"))
+ return true;
+
+ if (HasVideoInfoTag())
+ return true;
+
+ if (HasGameInfoTag())
+ return false;
+
+ if (HasMusicInfoTag())
+ return false;
+
+ if (HasPictureInfoTag())
+ return false;
+
+ // only tv recordings are videos...
+ if (IsPVRRecording())
+ return !GetPVRRecordingInfoTag()->IsRadio();
+
+ // ... all other PVR items are not.
+ if (IsPVR())
+ return false;
+
+ if (URIUtils::IsDVD(m_strPath))
+ return true;
+
+ std::string extension;
+ if(StringUtils::StartsWithNoCase(m_mimetype, "application/"))
+ { /* check for some standard types */
+ extension = m_mimetype.substr(12);
+ if( StringUtils::EqualsNoCase(extension, "ogg")
+ || StringUtils::EqualsNoCase(extension, "mp4")
+ || StringUtils::EqualsNoCase(extension, "mxf") )
+ return true;
+ }
+
+ //! @todo If the file is a zip file, ask the game clients if any support this
+ // file before assuming it is video.
+
+ return URIUtils::HasExtension(m_strPath, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions());
+}
+
+bool CFileItem::IsEPG() const
+{
+ return HasEPGInfoTag();
+}
+
+bool CFileItem::IsPVRChannel() const
+{
+ return HasPVRChannelInfoTag();
+}
+
+bool CFileItem::IsPVRChannelGroup() const
+{
+ return URIUtils::IsPVRChannelGroup(m_strPath);
+}
+
+bool CFileItem::IsPVRRecording() const
+{
+ return HasPVRRecordingInfoTag();
+}
+
+bool CFileItem::IsUsablePVRRecording() const
+{
+ return (m_pvrRecordingInfoTag && !m_pvrRecordingInfoTag->IsDeleted());
+}
+
+bool CFileItem::IsDeletedPVRRecording() const
+{
+ return (m_pvrRecordingInfoTag && m_pvrRecordingInfoTag->IsDeleted());
+}
+
+bool CFileItem::IsInProgressPVRRecording() const
+{
+ return (m_pvrRecordingInfoTag && m_pvrRecordingInfoTag->IsInProgress());
+}
+
+bool CFileItem::IsPVRTimer() const
+{
+ return HasPVRTimerInfoTag();
+}
+
+bool CFileItem::IsDiscStub() const
+{
+ if (IsVideoDb() && HasVideoInfoTag())
+ {
+ CFileItem dbItem(m_bIsFolder ? GetVideoInfoTag()->m_strPath : GetVideoInfoTag()->m_strFileNameAndPath, m_bIsFolder);
+ return dbItem.IsDiscStub();
+ }
+
+ return URIUtils::HasExtension(m_strPath, CServiceBroker::GetFileExtensionProvider().GetDiscStubExtensions());
+}
+
+bool CFileItem::IsAudio() const
+{
+ /* check preset mime type */
+ if(StringUtils::StartsWithNoCase(m_mimetype, "audio/"))
+ return true;
+
+ if (HasMusicInfoTag())
+ return true;
+
+ if (HasVideoInfoTag())
+ return false;
+
+ if (HasPictureInfoTag())
+ return false;
+
+ if (HasGameInfoTag())
+ return false;
+
+ if (IsCDDA())
+ return true;
+
+ if(StringUtils::StartsWithNoCase(m_mimetype, "application/"))
+ { /* check for some standard types */
+ std::string extension = m_mimetype.substr(12);
+ if( StringUtils::EqualsNoCase(extension, "ogg")
+ || StringUtils::EqualsNoCase(extension, "mp4")
+ || StringUtils::EqualsNoCase(extension, "mxf") )
+ return true;
+ }
+
+ //! @todo If the file is a zip file, ask the game clients if any support this
+ // file before assuming it is audio
+
+ return URIUtils::HasExtension(m_strPath, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions());
+}
+
+bool CFileItem::IsDeleted() const
+{
+ if (HasPVRRecordingInfoTag())
+ return GetPVRRecordingInfoTag()->IsDeleted();
+
+ return false;
+}
+
+bool CFileItem::IsAudioBook() const
+{
+ return IsType(".m4b") || IsType(".mka");
+}
+
+bool CFileItem::IsGame() const
+{
+ if (HasGameInfoTag())
+ return true;
+
+ if (HasVideoInfoTag())
+ return false;
+
+ if (HasMusicInfoTag())
+ return false;
+
+ if (HasPictureInfoTag())
+ return false;
+
+ if (IsPVR())
+ return false;
+
+ if (HasAddonInfo())
+ return CGameUtils::IsStandaloneGame(std::const_pointer_cast<ADDON::IAddon>(GetAddonInfo()));
+
+ return CGameUtils::HasGameExtension(m_strPath);
+}
+
+bool CFileItem::IsPicture() const
+{
+ if (StringUtils::StartsWithNoCase(m_mimetype, "image/"))
+ return true;
+
+ if (HasPictureInfoTag())
+ return true;
+
+ if (HasGameInfoTag())
+ return false;
+
+ if (HasMusicInfoTag())
+ return false;
+
+ if (HasVideoInfoTag())
+ return false;
+
+ if (HasPVRTimerInfoTag() || HasPVRChannelInfoTag() || HasPVRChannelGroupMemberInfoTag() ||
+ HasPVRRecordingInfoTag() || HasEPGInfoTag() || HasEPGSearchFilter())
+ return false;
+
+ if (!m_strPath.empty())
+ return CUtil::IsPicture(m_strPath);
+
+ return false;
+}
+
+bool CFileItem::IsLyrics() const
+{
+ return URIUtils::HasExtension(m_strPath, ".cdg|.lrc");
+}
+
+bool CFileItem::IsSubtitle() const
+{
+ return URIUtils::HasExtension(m_strPath, CServiceBroker::GetFileExtensionProvider().GetSubtitleExtensions());
+}
+
+bool CFileItem::IsCUESheet() const
+{
+ return URIUtils::HasExtension(m_strPath, ".cue");
+}
+
+bool CFileItem::IsInternetStream(const bool bStrictCheck /* = false */) const
+{
+ if (HasProperty("IsHTTPDirectory"))
+ return bStrictCheck;
+
+ if (!m_strDynPath.empty())
+ return URIUtils::IsInternetStream(m_strDynPath, bStrictCheck);
+
+ return URIUtils::IsInternetStream(m_strPath, bStrictCheck);
+}
+
+bool CFileItem::IsStreamedFilesystem() const
+{
+ if (!m_strDynPath.empty())
+ return URIUtils::IsStreamedFilesystem(m_strDynPath);
+
+ return URIUtils::IsStreamedFilesystem(m_strPath);
+}
+
+bool CFileItem::IsFileFolder(EFileFolderType types) const
+{
+ EFileFolderType always_type = EFILEFOLDER_TYPE_ALWAYS;
+
+ /* internet streams are not directly expanded */
+ if(IsInternetStream())
+ always_type = EFILEFOLDER_TYPE_ONCLICK;
+
+ if(types & always_type)
+ {
+ if(IsSmartPlayList()
+ || (IsPlayList() && CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_playlistAsFolders)
+ || IsAPK()
+ || IsZIP()
+ || IsRAR()
+ || IsRSS()
+ || IsAudioBook()
+ || IsType(".ogg|.oga|.xbt")
+#if defined(TARGET_ANDROID)
+ || IsType(".apk")
+#endif
+ )
+ return true;
+ }
+
+ if (CServiceBroker::IsAddonInterfaceUp() &&
+ IsType(CServiceBroker::GetFileExtensionProvider().GetFileFolderExtensions().c_str()) &&
+ CServiceBroker::GetFileExtensionProvider().CanOperateExtension(m_strPath))
+ return true;
+
+ if(types & EFILEFOLDER_TYPE_ONBROWSE)
+ {
+ if((IsPlayList() && !CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_playlistAsFolders)
+ || IsDiscImage())
+ return true;
+ }
+
+ return false;
+}
+
+bool CFileItem::IsSmartPlayList() const
+{
+ if (HasProperty("library.smartplaylist") && GetProperty("library.smartplaylist").asBoolean())
+ return true;
+
+ return URIUtils::HasExtension(m_strPath, ".xsp");
+}
+
+bool CFileItem::IsLibraryFolder() const
+{
+ if (HasProperty("library.filter") && GetProperty("library.filter").asBoolean())
+ return true;
+
+ return URIUtils::IsLibraryFolder(m_strPath);
+}
+
+bool CFileItem::IsPlayList() const
+{
+ return CPlayListFactory::IsPlaylist(*this);
+}
+
+bool CFileItem::IsPythonScript() const
+{
+ return URIUtils::HasExtension(m_strPath, ".py");
+}
+
+bool CFileItem::IsType(const char *ext) const
+{
+ if (!m_strDynPath.empty())
+ return URIUtils::HasExtension(m_strDynPath, ext);
+
+ return URIUtils::HasExtension(m_strPath, ext);
+}
+
+bool CFileItem::IsNFO() const
+{
+ return URIUtils::HasExtension(m_strPath, ".nfo");
+}
+
+bool CFileItem::IsDiscImage() const
+{
+ return URIUtils::HasExtension(GetDynPath(), ".img|.iso|.nrg|.udf");
+}
+
+bool CFileItem::IsOpticalMediaFile() const
+{
+ if (IsDVDFile(false, true))
+ return true;
+
+ return IsBDFile();
+}
+
+bool CFileItem::IsDVDFile(bool bVobs /*= true*/, bool bIfos /*= true*/) const
+{
+ std::string strFileName = URIUtils::GetFileName(GetDynPath());
+ if (bIfos)
+ {
+ if (StringUtils::EqualsNoCase(strFileName, "video_ts.ifo"))
+ return true;
+ if (StringUtils::StartsWithNoCase(strFileName, "vts_") && StringUtils::EndsWithNoCase(strFileName, "_0.ifo") && strFileName.length() == 12)
+ return true;
+ }
+ if (bVobs)
+ {
+ if (StringUtils::EqualsNoCase(strFileName, "video_ts.vob"))
+ return true;
+ if (StringUtils::StartsWithNoCase(strFileName, "vts_") && StringUtils::EndsWithNoCase(strFileName, ".vob"))
+ return true;
+ }
+
+ return false;
+}
+
+bool CFileItem::IsBDFile() const
+{
+ std::string strFileName = URIUtils::GetFileName(GetDynPath());
+ return (StringUtils::EqualsNoCase(strFileName, "index.bdmv") || StringUtils::EqualsNoCase(strFileName, "MovieObject.bdmv")
+ || StringUtils::EqualsNoCase(strFileName, "INDEX.BDM") || StringUtils::EqualsNoCase(strFileName, "MOVIEOBJ.BDM"));
+}
+
+bool CFileItem::IsRAR() const
+{
+ return URIUtils::IsRAR(m_strPath);
+}
+
+bool CFileItem::IsAPK() const
+{
+ return URIUtils::IsAPK(m_strPath);
+}
+
+bool CFileItem::IsZIP() const
+{
+ return URIUtils::IsZIP(m_strPath);
+}
+
+bool CFileItem::IsCBZ() const
+{
+ return URIUtils::HasExtension(m_strPath, ".cbz");
+}
+
+bool CFileItem::IsCBR() const
+{
+ return URIUtils::HasExtension(m_strPath, ".cbr");
+}
+
+bool CFileItem::IsRSS() const
+{
+ return StringUtils::StartsWithNoCase(m_strPath, "rss://") || URIUtils::HasExtension(m_strPath, ".rss")
+ || StringUtils::StartsWithNoCase(m_strPath, "rsss://")
+ || m_mimetype == "application/rss+xml";
+}
+
+bool CFileItem::IsAndroidApp() const
+{
+ return URIUtils::IsAndroidApp(m_strPath);
+}
+
+bool CFileItem::IsStack() const
+{
+ return URIUtils::IsStack(m_strPath);
+}
+
+bool CFileItem::IsFavourite() const
+{
+ return URIUtils::IsFavourite(m_strPath);
+}
+
+bool CFileItem::IsPlugin() const
+{
+ return URIUtils::IsPlugin(m_strPath);
+}
+
+bool CFileItem::IsScript() const
+{
+ return URIUtils::IsScript(m_strPath);
+}
+
+bool CFileItem::IsAddonsPath() const
+{
+ return URIUtils::IsAddonsPath(m_strPath);
+}
+
+bool CFileItem::IsSourcesPath() const
+{
+ return URIUtils::IsSourcesPath(m_strPath);
+}
+
+bool CFileItem::IsMultiPath() const
+{
+ return URIUtils::IsMultiPath(m_strPath);
+}
+
+bool CFileItem::IsBluray() const
+{
+ if (URIUtils::IsBluray(m_strPath))
+ return true;
+
+ CFileItem item = CFileItem(GetOpticalMediaPath(), false);
+
+ return item.IsBDFile();
+}
+
+bool CFileItem::IsProtectedBlurayDisc() const
+{
+ std::string path;
+ path = URIUtils::AddFileToFolder(GetPath(), "AACS", "Unit_Key_RO.inf");
+ if (CFile::Exists(path))
+ return true;
+
+ return false;
+}
+
+bool CFileItem::IsCDDA() const
+{
+ return URIUtils::IsCDDA(m_strPath);
+}
+
+bool CFileItem::IsDVD() const
+{
+ return URIUtils::IsDVD(m_strPath) || m_iDriveType == CMediaSource::SOURCE_TYPE_DVD;
+}
+
+bool CFileItem::IsOnDVD() const
+{
+ return URIUtils::IsOnDVD(m_strPath) || m_iDriveType == CMediaSource::SOURCE_TYPE_DVD;
+}
+
+bool CFileItem::IsNfs() const
+{
+ return URIUtils::IsNfs(m_strPath);
+}
+
+bool CFileItem::IsOnLAN() const
+{
+ return URIUtils::IsOnLAN(m_strPath);
+}
+
+bool CFileItem::IsISO9660() const
+{
+ return URIUtils::IsISO9660(m_strPath);
+}
+
+bool CFileItem::IsRemote() const
+{
+ return URIUtils::IsRemote(m_strPath);
+}
+
+bool CFileItem::IsSmb() const
+{
+ return URIUtils::IsSmb(m_strPath);
+}
+
+bool CFileItem::IsURL() const
+{
+ return URIUtils::IsURL(m_strPath);
+}
+
+bool CFileItem::IsPVR() const
+{
+ return URIUtils::IsPVR(m_strPath);
+}
+
+bool CFileItem::IsLiveTV() const
+{
+ return URIUtils::IsLiveTV(m_strPath);
+}
+
+bool CFileItem::IsHD() const
+{
+ return URIUtils::IsHD(m_strPath);
+}
+
+bool CFileItem::IsMusicDb() const
+{
+ return URIUtils::IsMusicDb(m_strPath);
+}
+
+bool CFileItem::IsVideoDb() const
+{
+ return URIUtils::IsVideoDb(m_strPath);
+}
+
+bool CFileItem::IsVirtualDirectoryRoot() const
+{
+ return (m_bIsFolder && m_strPath.empty());
+}
+
+bool CFileItem::IsRemovable() const
+{
+ return IsOnDVD() || IsCDDA() || m_iDriveType == CMediaSource::SOURCE_TYPE_REMOVABLE;
+}
+
+bool CFileItem::IsReadOnly() const
+{
+ if (IsParentFolder())
+ return true;
+
+ if (m_bIsShareOrDrive)
+ return true;
+
+ return !CUtil::SupportsWriteFileOperations(m_strPath);
+}
+
+void CFileItem::FillInDefaultIcon()
+{
+ if (URIUtils::IsPVRGuideItem(m_strPath))
+ {
+ // epg items never have a default icon. no need to execute this expensive method.
+ // when filling epg grid window, easily tens of thousands of epg items are processed.
+ return;
+ }
+
+ //CLog::Log(LOGINFO, "FillInDefaultIcon({})", pItem->GetLabel());
+ // find the default icon for a file or folder item
+ // for files this can be the (depending on the file type)
+ // default picture for photo's
+ // default picture for songs
+ // default picture for videos
+ // default picture for shortcuts
+ // default picture for playlists
+ //
+ // for folders
+ // for .. folders the default picture for parent folder
+ // for other folders the defaultFolder.png
+
+ if (GetArt("icon").empty())
+ {
+ if (!m_bIsFolder)
+ {
+ /* To reduce the average runtime of this code, this list should
+ * be ordered with most frequently seen types first. Also bear
+ * in mind the complexity of the code behind the check in the
+ * case of IsWhatever() returns false.
+ */
+ if (IsPVRChannel())
+ {
+ if (GetPVRChannelInfoTag()->IsRadio())
+ SetArt("icon", "DefaultMusicSongs.png");
+ else
+ SetArt("icon", "DefaultTVShows.png");
+ }
+ else if ( IsLiveTV() )
+ {
+ // Live TV Channel
+ SetArt("icon", "DefaultTVShows.png");
+ }
+ else if ( URIUtils::IsArchive(m_strPath) )
+ { // archive
+ SetArt("icon", "DefaultFile.png");
+ }
+ else if ( IsUsablePVRRecording() )
+ {
+ // PVR recording
+ SetArt("icon", "DefaultVideo.png");
+ }
+ else if ( IsDeletedPVRRecording() )
+ {
+ // PVR deleted recording
+ SetArt("icon", "DefaultVideoDeleted.png");
+ }
+ else if ( IsAudio() )
+ {
+ // audio
+ SetArt("icon", "DefaultAudio.png");
+ }
+ else if ( IsVideo() )
+ {
+ // video
+ SetArt("icon", "DefaultVideo.png");
+ }
+ else if (IsPVRTimer())
+ {
+ SetArt("icon", "DefaultVideo.png");
+ }
+ else if ( IsPicture() )
+ {
+ // picture
+ SetArt("icon", "DefaultPicture.png");
+ }
+ else if ( IsPlayList() || IsSmartPlayList())
+ {
+ SetArt("icon", "DefaultPlaylist.png");
+ }
+ else if ( IsPythonScript() )
+ {
+ SetArt("icon", "DefaultScript.png");
+ }
+ else if (IsFavourite())
+ {
+ SetArt("icon", "DefaultFavourites.png");
+ }
+ else
+ {
+ // default icon for unknown file type
+ SetArt("icon", "DefaultFile.png");
+ }
+ }
+ else
+ {
+ if ( IsPlayList() || IsSmartPlayList())
+ {
+ SetArt("icon", "DefaultPlaylist.png");
+ }
+ else if (IsParentFolder())
+ {
+ SetArt("icon", "DefaultFolderBack.png");
+ }
+ else
+ {
+ SetArt("icon", "DefaultFolder.png");
+ }
+ }
+ }
+ // Set the icon overlays (if applicable)
+ if (!HasOverlay() && !HasProperty("icon_never_overlay"))
+ {
+ if (URIUtils::IsInRAR(m_strPath))
+ SetOverlayImage(CGUIListItem::ICON_OVERLAY_RAR);
+ else if (URIUtils::IsInZIP(m_strPath))
+ SetOverlayImage(CGUIListItem::ICON_OVERLAY_ZIP);
+ }
+}
+
+void CFileItem::RemoveExtension()
+{
+ if (m_bIsFolder)
+ return;
+
+ std::string strLabel = GetLabel();
+ URIUtils::RemoveExtension(strLabel);
+ SetLabel(strLabel);
+}
+
+void CFileItem::CleanString()
+{
+ if (IsLiveTV())
+ return;
+
+ std::string strLabel = GetLabel();
+ std::string strTitle, strTitleAndYear, strYear;
+ CUtil::CleanString(strLabel, strTitle, strTitleAndYear, strYear, true);
+ SetLabel(strTitleAndYear);
+}
+
+void CFileItem::SetLabel(const std::string &strLabel)
+{
+ if (strLabel == "..")
+ {
+ m_bIsParentFolder = true;
+ m_bIsFolder = true;
+ m_specialSort = SortSpecialOnTop;
+ SetLabelPreformatted(true);
+ }
+ CGUIListItem::SetLabel(strLabel);
+}
+
+void CFileItem::SetFileSizeLabel()
+{
+ if(m_bIsFolder && m_dwSize == 0)
+ SetLabel2("");
+ else
+ SetLabel2(StringUtils::SizeToString(m_dwSize));
+}
+
+bool CFileItem::CanQueue() const
+{
+ return m_bCanQueue;
+}
+
+void CFileItem::SetCanQueue(bool bYesNo)
+{
+ m_bCanQueue = bYesNo;
+}
+
+bool CFileItem::IsParentFolder() const
+{
+ return m_bIsParentFolder;
+}
+
+void CFileItem::FillInMimeType(bool lookup /*= true*/)
+{
+ //! @todo adapt this to use CMime::GetMimeType()
+ if (m_mimetype.empty())
+ {
+ if (m_bIsFolder)
+ m_mimetype = "x-directory/normal";
+ else if (HasPVRChannelInfoTag())
+ m_mimetype = GetPVRChannelInfoTag()->MimeType();
+ else if (StringUtils::StartsWithNoCase(GetDynPath(), "shout://") ||
+ StringUtils::StartsWithNoCase(GetDynPath(), "http://") ||
+ StringUtils::StartsWithNoCase(GetDynPath(), "https://"))
+ {
+ // If lookup is false, bail out early to leave mime type empty
+ if (!lookup)
+ return;
+
+ CCurlFile::GetMimeType(GetDynURL(), m_mimetype);
+
+ // try to get mime-type again but with an NSPlayer User-Agent
+ // in order for server to provide correct mime-type. Allows us
+ // to properly detect an MMS stream
+ if (StringUtils::StartsWithNoCase(m_mimetype, "video/x-ms-"))
+ CCurlFile::GetMimeType(GetDynURL(), m_mimetype, "NSPlayer/11.00.6001.7000");
+
+ // make sure there are no options set in mime-type
+ // mime-type can look like "video/x-ms-asf ; charset=utf8"
+ size_t i = m_mimetype.find(';');
+ if(i != std::string::npos)
+ m_mimetype.erase(i, m_mimetype.length() - i);
+ StringUtils::Trim(m_mimetype);
+ }
+ else
+ m_mimetype = CMime::GetMimeType(*this);
+
+ // if it's still empty set to an unknown type
+ if (m_mimetype.empty())
+ m_mimetype = "application/octet-stream";
+ }
+
+ // change protocol to mms for the following mime-type. Allows us to create proper FileMMS.
+ if(StringUtils::StartsWithNoCase(m_mimetype, "application/vnd.ms.wms-hdr.asfv1") ||
+ StringUtils::StartsWithNoCase(m_mimetype, "application/x-mms-framed"))
+ {
+ if (m_strDynPath.empty())
+ m_strDynPath = m_strPath;
+
+ StringUtils::Replace(m_strDynPath, "http:", "mms:");
+ }
+}
+
+void CFileItem::SetMimeTypeForInternetFile()
+{
+ if (m_doContentLookup && IsInternetStream())
+ {
+ SetMimeType("");
+ FillInMimeType(true);
+ }
+}
+
+bool CFileItem::IsSamePath(const CFileItem *item) const
+{
+ if (!item)
+ return false;
+
+ if (!m_strPath.empty() && item->GetPath() == m_strPath)
+ {
+ if (item->HasProperty("item_start") || HasProperty("item_start"))
+ return (item->GetProperty("item_start") == GetProperty("item_start"));
+ return true;
+ }
+ if (HasMusicInfoTag() && item->HasMusicInfoTag())
+ {
+ if (GetMusicInfoTag()->GetDatabaseId() != -1 && item->GetMusicInfoTag()->GetDatabaseId() != -1)
+ return ((GetMusicInfoTag()->GetDatabaseId() == item->GetMusicInfoTag()->GetDatabaseId()) &&
+ (GetMusicInfoTag()->GetType() == item->GetMusicInfoTag()->GetType()));
+ }
+ if (HasVideoInfoTag() && item->HasVideoInfoTag())
+ {
+ if (GetVideoInfoTag()->m_iDbId != -1 && item->GetVideoInfoTag()->m_iDbId != -1)
+ return ((GetVideoInfoTag()->m_iDbId == item->GetVideoInfoTag()->m_iDbId) &&
+ (GetVideoInfoTag()->m_type == item->GetVideoInfoTag()->m_type));
+ }
+ if (IsMusicDb() && HasMusicInfoTag())
+ {
+ CFileItem dbItem(m_musicInfoTag->GetURL(), false);
+ if (HasProperty("item_start"))
+ dbItem.SetProperty("item_start", GetProperty("item_start"));
+ return dbItem.IsSamePath(item);
+ }
+ if (IsVideoDb() && HasVideoInfoTag())
+ {
+ CFileItem dbItem(GetVideoInfoTag()->m_strFileNameAndPath, false);
+ if (HasProperty("item_start"))
+ dbItem.SetProperty("item_start", GetProperty("item_start"));
+ return dbItem.IsSamePath(item);
+ }
+ if (item->IsMusicDb() && item->HasMusicInfoTag())
+ {
+ CFileItem dbItem(item->m_musicInfoTag->GetURL(), false);
+ if (item->HasProperty("item_start"))
+ dbItem.SetProperty("item_start", item->GetProperty("item_start"));
+ return IsSamePath(&dbItem);
+ }
+ if (item->IsVideoDb() && item->HasVideoInfoTag())
+ {
+ CFileItem dbItem(item->GetVideoInfoTag()->m_strFileNameAndPath, false);
+ if (item->HasProperty("item_start"))
+ dbItem.SetProperty("item_start", item->GetProperty("item_start"));
+ return IsSamePath(&dbItem);
+ }
+ if (HasProperty("original_listitem_url"))
+ return (GetProperty("original_listitem_url") == item->GetPath());
+ return false;
+}
+
+bool CFileItem::IsAlbum() const
+{
+ return m_bIsAlbum;
+}
+
+void CFileItem::UpdateInfo(const CFileItem &item, bool replaceLabels /*=true*/)
+{
+ if (item.HasVideoInfoTag())
+ { // copy info across
+ //! @todo premiered info is normally stored in m_dateTime by the db
+
+ if (item.m_videoInfoTag)
+ {
+ if (m_videoInfoTag)
+ *m_videoInfoTag = *item.m_videoInfoTag;
+ else
+ m_videoInfoTag = new CVideoInfoTag(*item.m_videoInfoTag);
+ }
+ else
+ {
+ if (m_videoInfoTag)
+ delete m_videoInfoTag;
+
+ m_videoInfoTag = new CVideoInfoTag;
+ }
+
+ m_pvrRecordingInfoTag = item.m_pvrRecordingInfoTag;
+
+ SetOverlayImage(ICON_OVERLAY_UNWATCHED, GetVideoInfoTag()->GetPlayCount() > 0);
+ SetInvalid();
+ }
+ if (item.HasMusicInfoTag())
+ {
+ *GetMusicInfoTag() = *item.GetMusicInfoTag();
+ SetInvalid();
+ }
+ if (item.HasPictureInfoTag())
+ {
+ *GetPictureInfoTag() = *item.GetPictureInfoTag();
+ SetInvalid();
+ }
+ if (item.HasGameInfoTag())
+ {
+ *GetGameInfoTag() = *item.GetGameInfoTag();
+ SetInvalid();
+ }
+ SetDynPath(item.GetDynPath());
+ if (replaceLabels && !item.GetLabel().empty())
+ SetLabel(item.GetLabel());
+ if (replaceLabels && !item.GetLabel2().empty())
+ SetLabel2(item.GetLabel2());
+ if (!item.GetArt().empty())
+ SetArt(item.GetArt());
+ AppendProperties(item);
+}
+
+void CFileItem::MergeInfo(const CFileItem& item)
+{
+ // TODO: Currently merge the metadata/art info is implemented for video case only
+ if (item.HasVideoInfoTag())
+ {
+ if (item.m_videoInfoTag)
+ {
+ if (m_videoInfoTag)
+ m_videoInfoTag->Merge(*item.m_videoInfoTag);
+ else
+ m_videoInfoTag = new CVideoInfoTag(*item.m_videoInfoTag);
+ }
+
+ m_pvrRecordingInfoTag = item.m_pvrRecordingInfoTag;
+
+ SetOverlayImage(ICON_OVERLAY_UNWATCHED, GetVideoInfoTag()->GetPlayCount() > 0);
+ SetInvalid();
+ }
+ if (item.HasMusicInfoTag())
+ {
+ *GetMusicInfoTag() = *item.GetMusicInfoTag();
+ SetInvalid();
+ }
+ if (item.HasPictureInfoTag())
+ {
+ *GetPictureInfoTag() = *item.GetPictureInfoTag();
+ SetInvalid();
+ }
+ if (item.HasGameInfoTag())
+ {
+ *GetGameInfoTag() = *item.GetGameInfoTag();
+ SetInvalid();
+ }
+ SetDynPath(item.GetDynPath());
+ if (!item.GetLabel().empty())
+ SetLabel(item.GetLabel());
+ if (!item.GetLabel2().empty())
+ SetLabel2(item.GetLabel2());
+ if (!item.GetArt().empty())
+ {
+ if (item.IsVideo())
+ AppendArt(item.GetArt());
+ else
+ SetArt(item.GetArt());
+ }
+ AppendProperties(item);
+}
+
+void CFileItem::SetFromVideoInfoTag(const CVideoInfoTag &video)
+{
+ if (!video.m_strTitle.empty())
+ SetLabel(video.m_strTitle);
+ if (video.m_strFileNameAndPath.empty())
+ {
+ m_strPath = video.m_strPath;
+ URIUtils::AddSlashAtEnd(m_strPath);
+ m_bIsFolder = true;
+ }
+ else
+ {
+ m_strPath = video.m_strFileNameAndPath;
+ m_bIsFolder = false;
+ }
+
+ if (m_videoInfoTag)
+ *m_videoInfoTag = video;
+ else
+ m_videoInfoTag = new CVideoInfoTag(video);
+
+ if (video.m_iSeason == 0)
+ SetProperty("isspecial", "true");
+ FillInDefaultIcon();
+ FillInMimeType(false);
+}
+
+namespace
+{
+class CPropertySaveHelper
+{
+public:
+ CPropertySaveHelper(CFileItem& item, const std::string& property, const std::string& value)
+ : m_item(item), m_property(property), m_value(value)
+ {
+ }
+
+ bool NeedsSave() const { return !m_value.empty() || m_item.HasProperty(m_property); }
+
+ std::string GetValueToSave(const std::string& currentValue) const
+ {
+ std::string value;
+
+ if (!m_value.empty())
+ {
+ // Overwrite whatever we have; remember what we had originally.
+ if (!m_item.HasProperty(m_property))
+ m_item.SetProperty(m_property, currentValue);
+
+ value = m_value;
+ }
+ else if (m_item.HasProperty(m_property))
+ {
+ // Restore original value
+ value = m_item.GetProperty(m_property).asString();
+ m_item.ClearProperty(m_property);
+ }
+
+ return value;
+ }
+
+private:
+ CFileItem& m_item;
+ const std::string m_property;
+ const std::string m_value;
+};
+} // unnamed namespace
+
+void CFileItem::SetFromMusicInfoTag(const MUSIC_INFO::CMusicInfoTag& music)
+{
+ const std::string path = GetPath();
+ if (path.empty())
+ {
+ SetPath(music.GetURL());
+ }
+ else
+ {
+ const CPropertySaveHelper dynpath(*this, "OriginalDynPath", music.GetURL());
+ if (dynpath.NeedsSave())
+ SetDynPath(dynpath.GetValueToSave(m_strDynPath));
+ }
+
+ const CPropertySaveHelper label(*this, "OriginalLabel", music.GetTitle());
+ if (label.NeedsSave())
+ SetLabel(label.GetValueToSave(GetLabel()));
+
+ const CPropertySaveHelper thumb(*this, "OriginalThumb", music.GetStationArt());
+ if (thumb.NeedsSave())
+ SetArt("thumb", thumb.GetValueToSave(GetArt("thumb")));
+
+ *GetMusicInfoTag() = music;
+ FillInDefaultIcon();
+ FillInMimeType(false);
+}
+
+void CFileItem::SetFromAlbum(const CAlbum &album)
+{
+ if (!album.strAlbum.empty())
+ SetLabel(album.strAlbum);
+ m_bIsFolder = true;
+ m_strLabel2 = album.GetAlbumArtistString();
+ GetMusicInfoTag()->SetAlbum(album);
+
+ if (album.art.empty())
+ SetArt("icon", "DefaultAlbumCover.png");
+ else
+ SetArt(album.art);
+
+ m_bIsAlbum = true;
+ CMusicDatabase::SetPropertiesFromAlbum(*this,album);
+ FillInMimeType(false);
+}
+
+void CFileItem::SetFromSong(const CSong &song)
+{
+ if (!song.strTitle.empty())
+ SetLabel(song.strTitle);
+ if (song.idSong > 0)
+ {
+ std::string strExt = URIUtils::GetExtension(song.strFileName);
+ m_strPath = StringUtils::Format("musicdb://songs/{}{}", song.idSong, strExt);
+ }
+ else if (!song.strFileName.empty())
+ m_strPath = song.strFileName;
+ GetMusicInfoTag()->SetSong(song);
+ m_lStartOffset = song.iStartOffset;
+ m_lStartPartNumber = 1;
+ SetProperty("item_start", song.iStartOffset);
+ m_lEndOffset = song.iEndOffset;
+ if (!song.strThumb.empty())
+ SetArt("thumb", song.strThumb);
+ FillInMimeType(false);
+}
+
+std::string CFileItem::GetOpticalMediaPath() const
+{
+ std::string path;
+ path = URIUtils::AddFileToFolder(GetPath(), "VIDEO_TS.IFO");
+ if (CFile::Exists(path))
+ return path;
+
+ path = URIUtils::AddFileToFolder(GetPath(), "VIDEO_TS", "VIDEO_TS.IFO");
+ if (CFile::Exists(path))
+ return path;
+
+#ifdef HAVE_LIBBLURAY
+ path = URIUtils::AddFileToFolder(GetPath(), "index.bdmv");
+ if (CFile::Exists(path))
+ return path;
+
+ path = URIUtils::AddFileToFolder(GetPath(), "BDMV", "index.bdmv");
+ if (CFile::Exists(path))
+ return path;
+
+ path = URIUtils::AddFileToFolder(GetPath(), "INDEX.BDM");
+ if (CFile::Exists(path))
+ return path;
+
+ path = URIUtils::AddFileToFolder(GetPath(), "BDMV", "INDEX.BDM");
+ if (CFile::Exists(path))
+ return path;
+#endif
+ return std::string();
+}
+
+/**
+* @todo Ideally this (and SetPath) would not be available outside of construction
+* for CFileItem objects, or at least restricted to essentially be equivalent
+* to construction. This would require re-formulating a bunch of CFileItem
+* construction, and also allowing CFileItemList to have its own (public)
+* SetURL() function, so for now we give direct access.
+*/
+void CFileItem::SetURL(const CURL& url)
+{
+ m_strPath = url.Get();
+}
+
+const CURL CFileItem::GetURL() const
+{
+ CURL url(m_strPath);
+ return url;
+}
+
+bool CFileItem::IsURL(const CURL& url) const
+{
+ return IsPath(url.Get());
+}
+
+bool CFileItem::IsPath(const std::string& path, bool ignoreURLOptions /* = false */) const
+{
+ return URIUtils::PathEquals(m_strPath, path, false, ignoreURLOptions);
+}
+
+void CFileItem::SetDynURL(const CURL& url)
+{
+ m_strDynPath = url.Get();
+}
+
+const CURL CFileItem::GetDynURL() const
+{
+ if (!m_strDynPath.empty())
+ {
+ CURL url(m_strDynPath);
+ return url;
+ }
+ else
+ {
+ CURL url(m_strPath);
+ return url;
+ }
+}
+
+const std::string &CFileItem::GetDynPath() const
+{
+ if (!m_strDynPath.empty())
+ return m_strDynPath;
+ else
+ return m_strPath;
+}
+
+void CFileItem::SetDynPath(const std::string &path)
+{
+ m_strDynPath = path;
+}
+
+void CFileItem::SetCueDocument(const CCueDocumentPtr& cuePtr)
+{
+ m_cueDocument = cuePtr;
+}
+
+void CFileItem::LoadEmbeddedCue()
+{
+ CMusicInfoTag& tag = *GetMusicInfoTag();
+ if (!tag.Loaded())
+ return;
+
+ const std::string embeddedCue = tag.GetCueSheet();
+ if (!embeddedCue.empty())
+ {
+ CCueDocumentPtr cuesheet(new CCueDocument);
+ if (cuesheet->ParseTag(embeddedCue))
+ {
+ std::vector<std::string> MediaFileVec;
+ cuesheet->GetMediaFiles(MediaFileVec);
+ for (std::vector<std::string>::iterator itMedia = MediaFileVec.begin();
+ itMedia != MediaFileVec.end(); ++itMedia)
+ cuesheet->UpdateMediaFile(*itMedia, GetPath());
+ SetCueDocument(cuesheet);
+ }
+ // Clear cuesheet tag having added it to item
+ tag.SetCueSheet("");
+ }
+}
+
+bool CFileItem::HasCueDocument() const
+{
+ return (m_cueDocument.get() != nullptr);
+}
+
+bool CFileItem::LoadTracksFromCueDocument(CFileItemList& scannedItems)
+{
+ if (!m_cueDocument)
+ return false;
+
+ const CMusicInfoTag& tag = *GetMusicInfoTag();
+
+ VECSONGS tracks;
+ m_cueDocument->GetSongs(tracks);
+
+ bool oneFilePerTrack = m_cueDocument->IsOneFilePerTrack();
+ m_cueDocument.reset();
+
+ int tracksFound = 0;
+ for (VECSONGS::iterator it = tracks.begin(); it != tracks.end(); ++it)
+ {
+ CSong& song = *it;
+ if (song.strFileName == GetPath())
+ {
+ if (tag.Loaded())
+ {
+ if (song.strAlbum.empty() && !tag.GetAlbum().empty())
+ song.strAlbum = tag.GetAlbum();
+ //Pass album artist to final MusicInfoTag object via setting song album artist vector.
+ if (song.GetAlbumArtist().empty() && !tag.GetAlbumArtist().empty())
+ song.SetAlbumArtist(tag.GetAlbumArtist());
+ if (song.genre.empty() && !tag.GetGenre().empty())
+ song.genre = tag.GetGenre();
+ //Pass artist to final MusicInfoTag object via setting song artist description string only.
+ //Artist credits not used during loading from cue sheet.
+ if (song.strArtistDesc.empty() && !tag.GetArtistString().empty())
+ song.strArtistDesc = tag.GetArtistString();
+ if (tag.GetDiscNumber())
+ song.iTrack |= (tag.GetDiscNumber() << 16); // see CMusicInfoTag::GetDiscNumber()
+ if (!tag.GetCueSheet().empty())
+ song.strCueSheet = tag.GetCueSheet();
+
+ if (tag.GetYear())
+ song.strReleaseDate = tag.GetReleaseDate();
+ if (song.embeddedArt.Empty() && !tag.GetCoverArtInfo().Empty())
+ song.embeddedArt = tag.GetCoverArtInfo();
+ }
+
+ if (!song.iDuration && tag.GetDuration() > 0)
+ { // must be the last song
+ song.iDuration = CUtil::ConvertMilliSecsToSecsIntRounded(CUtil::ConvertSecsToMilliSecs(tag.GetDuration()) - song.iStartOffset);
+ }
+ if ( tag.Loaded() && oneFilePerTrack && ! ( tag.GetAlbum().empty() || tag.GetArtist().empty() || tag.GetTitle().empty() ) )
+ {
+ // If there are multiple files in a cue file, the tags from the files should be preferred if they exist.
+ scannedItems.Add(CFileItemPtr(new CFileItem(song, tag)));
+ }
+ else
+ {
+ scannedItems.Add(CFileItemPtr(new CFileItem(song)));
+ }
+ ++tracksFound;
+ }
+ }
+ return tracksFound != 0;
+}
+
+/////////////////////////////////////////////////////////////////////////////////
+/////
+///// CFileItemList
+/////
+//////////////////////////////////////////////////////////////////////////////////
+
+CFileItemList::CFileItemList()
+: CFileItem("", true)
+{
+}
+
+CFileItemList::CFileItemList(const std::string& strPath)
+: CFileItem(strPath, true)
+{
+}
+
+CFileItemList::~CFileItemList()
+{
+ Clear();
+}
+
+CFileItemPtr CFileItemList::operator[] (int iItem)
+{
+ return Get(iItem);
+}
+
+const CFileItemPtr CFileItemList::operator[] (int iItem) const
+{
+ return Get(iItem);
+}
+
+CFileItemPtr CFileItemList::operator[] (const std::string& strPath)
+{
+ return Get(strPath);
+}
+
+const CFileItemPtr CFileItemList::operator[] (const std::string& strPath) const
+{
+ return Get(strPath);
+}
+
+void CFileItemList::SetIgnoreURLOptions(bool ignoreURLOptions)
+{
+ m_ignoreURLOptions = ignoreURLOptions;
+
+ if (m_fastLookup)
+ {
+ m_fastLookup = false; // Force SetFastlookup to clear map
+ SetFastLookup(true); // and regenerate map
+ }
+}
+
+void CFileItemList::SetFastLookup(bool fastLookup)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ if (fastLookup && !m_fastLookup)
+ { // generate the map
+ m_map.clear();
+ for (unsigned int i=0; i < m_items.size(); i++)
+ {
+ CFileItemPtr pItem = m_items[i];
+ m_map.insert(MAPFILEITEMSPAIR(m_ignoreURLOptions ? CURL(pItem->GetPath()).GetWithoutOptions() : pItem->GetPath(), pItem));
+ }
+ }
+ if (!fastLookup && m_fastLookup)
+ m_map.clear();
+ m_fastLookup = fastLookup;
+}
+
+bool CFileItemList::Contains(const std::string& fileName) const
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ if (m_fastLookup)
+ return m_map.find(m_ignoreURLOptions ? CURL(fileName).GetWithoutOptions() : fileName) != m_map.end();
+
+ // slow method...
+ for (unsigned int i = 0; i < m_items.size(); i++)
+ {
+ const CFileItemPtr pItem = m_items[i];
+ if (pItem->IsPath(m_ignoreURLOptions ? CURL(fileName).GetWithoutOptions() : fileName))
+ return true;
+ }
+ return false;
+}
+
+void CFileItemList::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ ClearItems();
+ m_sortDescription.sortBy = SortByNone;
+ m_sortDescription.sortOrder = SortOrderNone;
+ m_sortDescription.sortAttributes = SortAttributeNone;
+ m_sortIgnoreFolders = false;
+ m_cacheToDisc = CACHE_IF_SLOW;
+ m_sortDetails.clear();
+ m_replaceListing = false;
+ m_content.clear();
+}
+
+void CFileItemList::ClearItems()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ // make sure we free the memory of the items (these are GUIControls which may have allocated resources)
+ FreeMemory();
+ for (unsigned int i = 0; i < m_items.size(); i++)
+ {
+ CFileItemPtr item = m_items[i];
+ item->FreeMemory();
+ }
+ m_items.clear();
+ m_map.clear();
+}
+
+void CFileItemList::Add(CFileItemPtr pItem)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ if (m_fastLookup)
+ m_map.insert(MAPFILEITEMSPAIR(m_ignoreURLOptions ? CURL(pItem->GetPath()).GetWithoutOptions() : pItem->GetPath(), pItem));
+ m_items.emplace_back(std::move(pItem));
+}
+
+void CFileItemList::Add(CFileItem&& item)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ auto ptr = std::make_shared<CFileItem>(std::move(item));
+ if (m_fastLookup)
+ m_map.insert(MAPFILEITEMSPAIR(m_ignoreURLOptions ? CURL(ptr->GetPath()).GetWithoutOptions() : ptr->GetPath(), ptr));
+ m_items.emplace_back(std::move(ptr));
+}
+
+void CFileItemList::AddFront(const CFileItemPtr &pItem, int itemPosition)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ if (itemPosition >= 0)
+ {
+ m_items.insert(m_items.begin()+itemPosition, pItem);
+ }
+ else
+ {
+ m_items.insert(m_items.begin()+(m_items.size()+itemPosition), pItem);
+ }
+ if (m_fastLookup)
+ {
+ m_map.insert(MAPFILEITEMSPAIR(m_ignoreURLOptions ? CURL(pItem->GetPath()).GetWithoutOptions() : pItem->GetPath(), pItem));
+ }
+}
+
+void CFileItemList::Remove(CFileItem* pItem)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ for (IVECFILEITEMS it = m_items.begin(); it != m_items.end(); ++it)
+ {
+ if (pItem == it->get())
+ {
+ m_items.erase(it);
+ if (m_fastLookup)
+ {
+ m_map.erase(m_ignoreURLOptions ? CURL(pItem->GetPath()).GetWithoutOptions() : pItem->GetPath());
+ }
+ break;
+ }
+ }
+}
+
+VECFILEITEMS::iterator CFileItemList::erase(VECFILEITEMS::iterator first,
+ VECFILEITEMS::iterator last)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ return m_items.erase(first, last);
+}
+
+void CFileItemList::Remove(int iItem)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ if (iItem >= 0 && iItem < Size())
+ {
+ CFileItemPtr pItem = *(m_items.begin() + iItem);
+ if (m_fastLookup)
+ {
+ m_map.erase(m_ignoreURLOptions ? CURL(pItem->GetPath()).GetWithoutOptions() : pItem->GetPath());
+ }
+ m_items.erase(m_items.begin() + iItem);
+ }
+}
+
+void CFileItemList::Append(const CFileItemList& itemlist)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ for (int i = 0; i < itemlist.Size(); ++i)
+ Add(itemlist[i]);
+}
+
+void CFileItemList::Assign(const CFileItemList& itemlist, bool append)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ if (!append)
+ Clear();
+ Append(itemlist);
+ SetPath(itemlist.GetPath());
+ SetLabel(itemlist.GetLabel());
+ m_sortDetails = itemlist.m_sortDetails;
+ m_sortDescription = itemlist.m_sortDescription;
+ m_replaceListing = itemlist.m_replaceListing;
+ m_content = itemlist.m_content;
+ m_mapProperties = itemlist.m_mapProperties;
+ m_cacheToDisc = itemlist.m_cacheToDisc;
+}
+
+bool CFileItemList::Copy(const CFileItemList& items, bool copyItems /* = true */)
+{
+ // assign all CFileItem parts
+ *static_cast<CFileItem*>(this) = static_cast<const CFileItem&>(items);
+
+ // assign the rest of the CFileItemList properties
+ m_replaceListing = items.m_replaceListing;
+ m_content = items.m_content;
+ m_mapProperties = items.m_mapProperties;
+ m_cacheToDisc = items.m_cacheToDisc;
+ m_sortDetails = items.m_sortDetails;
+ m_sortDescription = items.m_sortDescription;
+ m_sortIgnoreFolders = items.m_sortIgnoreFolders;
+
+ if (copyItems)
+ {
+ // make a copy of each item
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr newItem(new CFileItem(*items[i]));
+ Add(newItem);
+ }
+ }
+
+ return true;
+}
+
+CFileItemPtr CFileItemList::Get(int iItem) const
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ if (iItem > -1 && iItem < (int)m_items.size())
+ return m_items[iItem];
+
+ return CFileItemPtr();
+}
+
+CFileItemPtr CFileItemList::Get(const std::string& strPath) const
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ if (m_fastLookup)
+ {
+ MAPFILEITEMS::const_iterator it =
+ m_map.find(m_ignoreURLOptions ? CURL(strPath).GetWithoutOptions() : strPath);
+ if (it != m_map.end())
+ return it->second;
+
+ return CFileItemPtr();
+ }
+ // slow method...
+ for (unsigned int i = 0; i < m_items.size(); i++)
+ {
+ CFileItemPtr pItem = m_items[i];
+ if (pItem->IsPath(m_ignoreURLOptions ? CURL(strPath).GetWithoutOptions() : strPath))
+ return pItem;
+ }
+
+ return CFileItemPtr();
+}
+
+int CFileItemList::Size() const
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ return (int)m_items.size();
+}
+
+bool CFileItemList::IsEmpty() const
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ return m_items.empty();
+}
+
+void CFileItemList::Reserve(size_t iCount)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ m_items.reserve(iCount);
+}
+
+void CFileItemList::Sort(FILEITEMLISTCOMPARISONFUNC func)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ std::stable_sort(m_items.begin(), m_items.end(), func);
+}
+
+void CFileItemList::FillSortFields(FILEITEMFILLFUNC func)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ std::for_each(m_items.begin(), m_items.end(), func);
+}
+
+void CFileItemList::Sort(SortBy sortBy, SortOrder sortOrder, SortAttribute sortAttributes /* = SortAttributeNone */)
+{
+ if (sortBy == SortByNone ||
+ (m_sortDescription.sortBy == sortBy && m_sortDescription.sortOrder == sortOrder &&
+ m_sortDescription.sortAttributes == sortAttributes))
+ return;
+
+ SortDescription sorting;
+ sorting.sortBy = sortBy;
+ sorting.sortOrder = sortOrder;
+ sorting.sortAttributes = sortAttributes;
+
+ Sort(sorting);
+ m_sortDescription = sorting;
+}
+
+void CFileItemList::Sort(SortDescription sortDescription)
+{
+ if (sortDescription.sortBy == SortByFile || sortDescription.sortBy == SortBySortTitle ||
+ sortDescription.sortBy == SortByOriginalTitle || sortDescription.sortBy == SortByDateAdded ||
+ sortDescription.sortBy == SortByRating || sortDescription.sortBy == SortByYear ||
+ sortDescription.sortBy == SortByPlaylistOrder || sortDescription.sortBy == SortByLastPlayed ||
+ sortDescription.sortBy == SortByPlaycount)
+ sortDescription.sortAttributes = (SortAttribute)((int)sortDescription.sortAttributes | SortAttributeIgnoreFolders);
+
+ if (sortDescription.sortBy == SortByNone ||
+ (m_sortDescription.sortBy == sortDescription.sortBy && m_sortDescription.sortOrder == sortDescription.sortOrder &&
+ m_sortDescription.sortAttributes == sortDescription.sortAttributes))
+ return;
+
+ if (m_sortIgnoreFolders)
+ sortDescription.sortAttributes = (SortAttribute)((int)sortDescription.sortAttributes | SortAttributeIgnoreFolders);
+
+ const Fields fields = SortUtils::GetFieldsForSorting(sortDescription.sortBy);
+ SortItems sortItems((size_t)Size());
+ for (int index = 0; index < Size(); index++)
+ {
+ sortItems[index] = std::shared_ptr<SortItem>(new SortItem);
+ m_items[index]->ToSortable(*sortItems[index], fields);
+ (*sortItems[index])[FieldId] = index;
+ }
+
+ // do the sorting
+ SortUtils::Sort(sortDescription, sortItems);
+
+ // apply the new order to the existing CFileItems
+ VECFILEITEMS sortedFileItems;
+ sortedFileItems.reserve(Size());
+ for (SortItems::const_iterator it = sortItems.begin(); it != sortItems.end(); ++it)
+ {
+ CFileItemPtr item = m_items[(int)(*it)->at(FieldId).asInteger()];
+ // Set the sort label in the CFileItem
+ item->SetSortLabel((*it)->at(FieldSort).asWideString());
+
+ sortedFileItems.push_back(item);
+ }
+
+ // replace the current list with the re-ordered one
+ m_items = std::move(sortedFileItems);
+}
+
+void CFileItemList::Randomize()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ KODI::UTILS::RandomShuffle(m_items.begin(), m_items.end());
+}
+
+void CFileItemList::Archive(CArchive& ar)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ if (ar.IsStoring())
+ {
+ CFileItem::Archive(ar);
+
+ int i = 0;
+ if (!m_items.empty() && m_items[0]->IsParentFolder())
+ i = 1;
+
+ ar << (int)(m_items.size() - i);
+
+ ar << m_ignoreURLOptions;
+
+ ar << m_fastLookup;
+
+ ar << (int)m_sortDescription.sortBy;
+ ar << (int)m_sortDescription.sortOrder;
+ ar << (int)m_sortDescription.sortAttributes;
+ ar << m_sortIgnoreFolders;
+ ar << (int)m_cacheToDisc;
+
+ ar << (int)m_sortDetails.size();
+ for (unsigned int j = 0; j < m_sortDetails.size(); ++j)
+ {
+ const GUIViewSortDetails &details = m_sortDetails[j];
+ ar << (int)details.m_sortDescription.sortBy;
+ ar << (int)details.m_sortDescription.sortOrder;
+ ar << (int)details.m_sortDescription.sortAttributes;
+ ar << details.m_buttonLabel;
+ ar << details.m_labelMasks.m_strLabelFile;
+ ar << details.m_labelMasks.m_strLabelFolder;
+ ar << details.m_labelMasks.m_strLabel2File;
+ ar << details.m_labelMasks.m_strLabel2Folder;
+ }
+
+ ar << m_content;
+
+ for (; i < (int)m_items.size(); ++i)
+ {
+ CFileItemPtr pItem = m_items[i];
+ ar << *pItem;
+ }
+ }
+ else
+ {
+ CFileItemPtr pParent;
+ if (!IsEmpty())
+ {
+ CFileItemPtr pItem=m_items[0];
+ if (pItem->IsParentFolder())
+ pParent.reset(new CFileItem(*pItem));
+ }
+
+ SetIgnoreURLOptions(false);
+ SetFastLookup(false);
+ Clear();
+
+ CFileItem::Archive(ar);
+
+ int iSize = 0;
+ ar >> iSize;
+ if (iSize <= 0)
+ return ;
+
+ if (pParent)
+ {
+ m_items.reserve(iSize + 1);
+ m_items.push_back(pParent);
+ }
+ else
+ m_items.reserve(iSize);
+
+ bool ignoreURLOptions = false;
+ ar >> ignoreURLOptions;
+
+ bool fastLookup = false;
+ ar >> fastLookup;
+
+ int tempint;
+ ar >> tempint;
+ m_sortDescription.sortBy = (SortBy)tempint;
+ ar >> tempint;
+ m_sortDescription.sortOrder = (SortOrder)tempint;
+ ar >> tempint;
+ m_sortDescription.sortAttributes = (SortAttribute)tempint;
+ ar >> m_sortIgnoreFolders;
+ ar >> tempint;
+ m_cacheToDisc = CACHE_TYPE(tempint);
+
+ unsigned int detailSize = 0;
+ ar >> detailSize;
+ for (unsigned int j = 0; j < detailSize; ++j)
+ {
+ GUIViewSortDetails details;
+ ar >> tempint;
+ details.m_sortDescription.sortBy = (SortBy)tempint;
+ ar >> tempint;
+ details.m_sortDescription.sortOrder = (SortOrder)tempint;
+ ar >> tempint;
+ details.m_sortDescription.sortAttributes = (SortAttribute)tempint;
+ ar >> details.m_buttonLabel;
+ ar >> details.m_labelMasks.m_strLabelFile;
+ ar >> details.m_labelMasks.m_strLabelFolder;
+ ar >> details.m_labelMasks.m_strLabel2File;
+ ar >> details.m_labelMasks.m_strLabel2Folder;
+ m_sortDetails.push_back(details);
+ }
+
+ ar >> m_content;
+
+ for (int i = 0; i < iSize; ++i)
+ {
+ CFileItemPtr pItem(new CFileItem);
+ ar >> *pItem;
+ Add(pItem);
+ }
+
+ SetIgnoreURLOptions(ignoreURLOptions);
+ SetFastLookup(fastLookup);
+ }
+}
+
+void CFileItemList::FillInDefaultIcons()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ for (int i = 0; i < (int)m_items.size(); ++i)
+ {
+ CFileItemPtr pItem = m_items[i];
+ pItem->FillInDefaultIcon();
+ }
+}
+
+int CFileItemList::GetFolderCount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ int nFolderCount = 0;
+ for (int i = 0; i < (int)m_items.size(); i++)
+ {
+ CFileItemPtr pItem = m_items[i];
+ if (pItem->m_bIsFolder)
+ nFolderCount++;
+ }
+
+ return nFolderCount;
+}
+
+int CFileItemList::GetObjectCount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ int numObjects = (int)m_items.size();
+ if (numObjects && m_items[0]->IsParentFolder())
+ numObjects--;
+
+ return numObjects;
+}
+
+int CFileItemList::GetFileCount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ int nFileCount = 0;
+ for (int i = 0; i < (int)m_items.size(); i++)
+ {
+ CFileItemPtr pItem = m_items[i];
+ if (!pItem->m_bIsFolder)
+ nFileCount++;
+ }
+
+ return nFileCount;
+}
+
+int CFileItemList::GetSelectedCount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ int count = 0;
+ for (int i = 0; i < (int)m_items.size(); i++)
+ {
+ CFileItemPtr pItem = m_items[i];
+ if (pItem->IsSelected())
+ count++;
+ }
+
+ return count;
+}
+
+void CFileItemList::FilterCueItems()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ // Handle .CUE sheet files...
+ std::vector<std::string> itemstodelete;
+ for (int i = 0; i < (int)m_items.size(); i++)
+ {
+ CFileItemPtr pItem = m_items[i];
+ if (!pItem->m_bIsFolder)
+ { // see if it's a .CUE sheet
+ if (pItem->IsCUESheet())
+ {
+ CCueDocumentPtr cuesheet(new CCueDocument);
+ if (cuesheet->ParseFile(pItem->GetPath()))
+ {
+ std::vector<std::string> MediaFileVec;
+ cuesheet->GetMediaFiles(MediaFileVec);
+
+ // queue the cue sheet and the underlying media file for deletion
+ for (std::vector<std::string>::iterator itMedia = MediaFileVec.begin();
+ itMedia != MediaFileVec.end(); ++itMedia)
+ {
+ std::string strMediaFile = *itMedia;
+ std::string fileFromCue = strMediaFile; // save the file from the cue we're matching against,
+ // as we're going to search for others here...
+ bool bFoundMediaFile = CFile::Exists(strMediaFile);
+ if (!bFoundMediaFile)
+ {
+ // try file in same dir, not matching case...
+ if (Contains(strMediaFile))
+ {
+ bFoundMediaFile = true;
+ }
+ else
+ {
+ // try removing the .cue extension...
+ strMediaFile = pItem->GetPath();
+ URIUtils::RemoveExtension(strMediaFile);
+ CFileItem item(strMediaFile, false);
+ if (item.IsAudio() && Contains(strMediaFile))
+ {
+ bFoundMediaFile = true;
+ }
+ else
+ { // try replacing the extension with one of our allowed ones.
+ std::vector<std::string> extensions = StringUtils::Split(CServiceBroker::GetFileExtensionProvider().GetMusicExtensions(), "|");
+ for (std::vector<std::string>::const_iterator i = extensions.begin(); i != extensions.end(); ++i)
+ {
+ strMediaFile = URIUtils::ReplaceExtension(pItem->GetPath(), *i);
+ CFileItem item(strMediaFile, false);
+ if (!item.IsCUESheet() && !item.IsPlayList() && Contains(strMediaFile))
+ {
+ bFoundMediaFile = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (bFoundMediaFile)
+ {
+ cuesheet->UpdateMediaFile(fileFromCue, strMediaFile);
+ // apply CUE for later processing
+ for (int j = 0; j < (int)m_items.size(); j++)
+ {
+ CFileItemPtr pItem = m_items[j];
+ if (StringUtils::CompareNoCase(pItem->GetPath(), strMediaFile) == 0)
+ pItem->SetCueDocument(cuesheet);
+ }
+ }
+ }
+ }
+ itemstodelete.push_back(pItem->GetPath());
+ }
+ }
+ }
+ // now delete the .CUE files.
+ for (int i = 0; i < (int)itemstodelete.size(); i++)
+ {
+ for (int j = 0; j < (int)m_items.size(); j++)
+ {
+ CFileItemPtr pItem = m_items[j];
+ if (StringUtils::CompareNoCase(pItem->GetPath(), itemstodelete[i]) == 0)
+ { // delete this item
+ m_items.erase(m_items.begin() + j);
+ break;
+ }
+ }
+ }
+}
+
+// Remove the extensions from the filenames
+void CFileItemList::RemoveExtensions()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ for (int i = 0; i < Size(); ++i)
+ m_items[i]->RemoveExtension();
+}
+
+void CFileItemList::Stack(bool stackFiles /* = true */)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ // not allowed here
+ if (IsVirtualDirectoryRoot() ||
+ IsLiveTV() ||
+ IsSourcesPath() ||
+ IsLibraryFolder())
+ return;
+
+ SetProperty("isstacked", true);
+
+ // items needs to be sorted for stuff below to work properly
+ Sort(SortByLabel, SortOrderAscending);
+
+ StackFolders();
+
+ if (stackFiles)
+ StackFiles();
+}
+
+void CFileItemList::StackFolders()
+{
+ // Precompile our REs
+ VECCREGEXP folderRegExps;
+ CRegExp folderRegExp(true, CRegExp::autoUtf8);
+ const std::vector<std::string>& strFolderRegExps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_folderStackRegExps;
+
+ std::vector<std::string>::const_iterator strExpression = strFolderRegExps.begin();
+ while (strExpression != strFolderRegExps.end())
+ {
+ if (!folderRegExp.RegComp(*strExpression))
+ CLog::Log(LOGERROR, "{}: Invalid folder stack RegExp:'{}'", __FUNCTION__,
+ strExpression->c_str());
+ else
+ folderRegExps.push_back(folderRegExp);
+
+ ++strExpression;
+ }
+
+ if (!folderRegExp.IsCompiled())
+ {
+ CLog::Log(LOGDEBUG, "{}: No stack expressions available. Skipping folder stacking",
+ __FUNCTION__);
+ return;
+ }
+
+ // stack folders
+ for (int i = 0; i < Size(); i++)
+ {
+ CFileItemPtr item = Get(i);
+ // combined the folder checks
+ if (item->m_bIsFolder)
+ {
+ // only check known fast sources?
+ // NOTES:
+ // 1. rars and zips may be on slow sources? is this supposed to be allowed?
+ if( !item->IsRemote()
+ || item->IsSmb()
+ || item->IsNfs()
+ || URIUtils::IsInRAR(item->GetPath())
+ || URIUtils::IsInZIP(item->GetPath())
+ || URIUtils::IsOnLAN(item->GetPath())
+ )
+ {
+ // stack cd# folders if contains only a single video file
+
+ bool bMatch(false);
+
+ VECCREGEXP::iterator expr = folderRegExps.begin();
+ while (!bMatch && expr != folderRegExps.end())
+ {
+ //CLog::Log(LOGDEBUG,"{}: Running expression {} on {}", __FUNCTION__, expr->GetPattern(), item->GetLabel());
+ bMatch = (expr->RegFind(item->GetLabel().c_str()) != -1);
+ if (bMatch)
+ {
+ CFileItemList items;
+ CDirectory::GetDirectory(item->GetPath(), items,
+ CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
+ DIR_FLAG_DEFAULTS);
+ // optimized to only traverse listing once by checking for filecount
+ // and recording last file item for later use
+ int nFiles = 0;
+ int index = -1;
+ for (int j = 0; j < items.Size(); j++)
+ {
+ if (!items[j]->m_bIsFolder)
+ {
+ nFiles++;
+ index = j;
+ }
+
+ if (nFiles > 1)
+ break;
+ }
+
+ if (nFiles == 1)
+ *item = *items[index];
+ }
+ ++expr;
+ }
+
+ // check for dvd folders
+ if (!bMatch)
+ {
+ std::string dvdPath = item->GetOpticalMediaPath();
+
+ if (!dvdPath.empty())
+ {
+ // NOTE: should this be done for the CD# folders too?
+ item->m_bIsFolder = false;
+ item->SetPath(dvdPath);
+ item->SetLabel2("");
+ item->SetLabelPreformatted(true);
+ m_sortDescription.sortBy = SortByNone; /* sorting is now broken */
+ }
+ }
+ }
+ }
+ }
+}
+
+void CFileItemList::StackFiles()
+{
+ // Precompile our REs
+ VECCREGEXP stackRegExps;
+ CRegExp tmpRegExp(true, CRegExp::autoUtf8);
+ const std::vector<std::string>& strStackRegExps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoStackRegExps;
+ std::vector<std::string>::const_iterator strRegExp = strStackRegExps.begin();
+ while (strRegExp != strStackRegExps.end())
+ {
+ if (tmpRegExp.RegComp(*strRegExp))
+ {
+ if (tmpRegExp.GetCaptureTotal() == 4)
+ stackRegExps.push_back(tmpRegExp);
+ else
+ CLog::Log(LOGERROR, "Invalid video stack RE ({}). Must have 4 captures.",
+ strRegExp->c_str());
+ }
+ ++strRegExp;
+ }
+
+ // now stack the files, some of which may be from the previous stack iteration
+ int i = 0;
+ while (i < Size())
+ {
+ CFileItemPtr item1 = Get(i);
+
+ // skip folders, nfo files, playlists
+ if (item1->m_bIsFolder
+ || item1->IsParentFolder()
+ || item1->IsNFO()
+ || item1->IsPlayList()
+ )
+ {
+ // increment index
+ i++;
+ continue;
+ }
+
+ int64_t size = 0;
+ size_t offset = 0;
+ std::string stackName;
+ std::string file1;
+ std::string filePath;
+ std::vector<int> stack;
+ VECCREGEXP::iterator expr = stackRegExps.begin();
+
+ URIUtils::Split(item1->GetPath(), filePath, file1);
+ if (URIUtils::HasEncodedFilename(CURL(filePath)))
+ file1 = CURL::Decode(file1);
+
+ int j;
+ while (expr != stackRegExps.end())
+ {
+ if (expr->RegFind(file1, offset) != -1)
+ {
+ std::string Title1 = expr->GetMatch(1),
+ Volume1 = expr->GetMatch(2),
+ Ignore1 = expr->GetMatch(3),
+ Extension1 = expr->GetMatch(4);
+ if (offset)
+ Title1 = file1.substr(0, expr->GetSubStart(2));
+ j = i + 1;
+ while (j < Size())
+ {
+ CFileItemPtr item2 = Get(j);
+
+ // skip folders, nfo files, playlists
+ if (item2->m_bIsFolder
+ || item2->IsParentFolder()
+ || item2->IsNFO()
+ || item2->IsPlayList()
+ )
+ {
+ // increment index
+ j++;
+ continue;
+ }
+
+ std::string file2, filePath2;
+ URIUtils::Split(item2->GetPath(), filePath2, file2);
+ if (URIUtils::HasEncodedFilename(CURL(filePath2)) )
+ file2 = CURL::Decode(file2);
+
+ if (expr->RegFind(file2, offset) != -1)
+ {
+ std::string Title2 = expr->GetMatch(1),
+ Volume2 = expr->GetMatch(2),
+ Ignore2 = expr->GetMatch(3),
+ Extension2 = expr->GetMatch(4);
+ if (offset)
+ Title2 = file2.substr(0, expr->GetSubStart(2));
+ if (StringUtils::EqualsNoCase(Title1, Title2))
+ {
+ if (!StringUtils::EqualsNoCase(Volume1, Volume2))
+ {
+ if (StringUtils::EqualsNoCase(Ignore1, Ignore2) &&
+ StringUtils::EqualsNoCase(Extension1, Extension2))
+ {
+ if (stack.empty())
+ {
+ stackName = Title1 + Ignore1 + Extension1;
+ stack.push_back(i);
+ size += item1->m_dwSize;
+ }
+ stack.push_back(j);
+ size += item2->m_dwSize;
+ }
+ else // Sequel
+ {
+ offset = 0;
+ ++expr;
+ break;
+ }
+ }
+ else if (!StringUtils::EqualsNoCase(Ignore1, Ignore2)) // False positive, try again with offset
+ {
+ offset = expr->GetSubStart(3);
+ break;
+ }
+ else // Extension mismatch
+ {
+ offset = 0;
+ ++expr;
+ break;
+ }
+ }
+ else // Title mismatch
+ {
+ offset = 0;
+ ++expr;
+ break;
+ }
+ }
+ else // No match 2, next expression
+ {
+ offset = 0;
+ ++expr;
+ break;
+ }
+ j++;
+ }
+ if (j == Size())
+ expr = stackRegExps.end();
+ }
+ else // No match 1
+ {
+ offset = 0;
+ ++expr;
+ }
+ if (stack.size() > 1)
+ {
+ // have a stack, remove the items and add the stacked item
+ // dont actually stack a multipart rar set, just remove all items but the first
+ std::string stackPath;
+ if (Get(stack[0])->IsRAR())
+ stackPath = Get(stack[0])->GetPath();
+ else
+ {
+ CStackDirectory dir;
+ stackPath = dir.ConstructStackPath(*this, stack);
+ }
+ item1->SetPath(stackPath);
+ // clean up list
+ for (unsigned k = 1; k < stack.size(); k++)
+ Remove(i+1);
+ // item->m_bIsFolder = true; // don't treat stacked files as folders
+ // the label may be in a different char set from the filename (eg over smb
+ // the label is converted from utf8, but the filename is not)
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWEXTENSIONS))
+ URIUtils::RemoveExtension(stackName);
+
+ item1->SetLabel(stackName);
+ item1->m_dwSize = size;
+ break;
+ }
+ }
+ i++;
+ }
+}
+
+bool CFileItemList::Load(int windowID)
+{
+ CFile file;
+ auto path = GetDiscFileCache(windowID);
+ try
+ {
+ if (file.Open(path))
+ {
+ CArchive ar(&file, CArchive::load);
+ ar >> *this;
+ CLog::Log(LOGDEBUG, "Loading items: {}, directory: {} sort method: {}, ascending: {}", Size(),
+ CURL::GetRedacted(GetPath()), m_sortDescription.sortBy,
+ m_sortDescription.sortOrder == SortOrderAscending ? "true" : "false");
+ ar.Close();
+ file.Close();
+ return true;
+ }
+ }
+ catch(const std::out_of_range&)
+ {
+ CLog::Log(LOGERROR, "Corrupt archive: {}", CURL::GetRedacted(path));
+ }
+
+ return false;
+}
+
+bool CFileItemList::Save(int windowID)
+{
+ int iSize = Size();
+ if (iSize <= 0)
+ return false;
+
+ CLog::Log(LOGDEBUG, "Saving fileitems [{}]", CURL::GetRedacted(GetPath()));
+
+ CFile file;
+ std::string cachefile = GetDiscFileCache(windowID);
+ if (file.OpenForWrite(cachefile, true)) // overwrite always
+ {
+ // Before caching save simplified cache file name in every item so the cache file can be
+ // identifed and removed if the item is updated. List path and options (used for file
+ // name when list cached) can not be accurately derived from item path.
+ StringUtils::Replace(cachefile, "special://temp/archive_cache/", "");
+ StringUtils::Replace(cachefile, ".fi", "");
+ for (const auto& item : m_items)
+ item->SetProperty("cachefilename", cachefile);
+
+ CArchive ar(&file, CArchive::store);
+ ar << *this;
+ CLog::Log(LOGDEBUG, " -- items: {}, sort method: {}, ascending: {}", iSize,
+ m_sortDescription.sortBy,
+ m_sortDescription.sortOrder == SortOrderAscending ? "true" : "false");
+ ar.Close();
+ file.Close();
+ return true;
+ }
+
+ return false;
+}
+
+void CFileItemList::RemoveDiscCache(int windowID) const
+{
+ RemoveDiscCache(GetDiscFileCache(windowID));
+}
+
+void CFileItemList::RemoveDiscCache(const std::string& cacheFile) const
+{
+ if (CFile::Exists(cacheFile))
+ {
+ CLog::Log(LOGDEBUG, "Clearing cached fileitems [{}]", CURL::GetRedacted(GetPath()));
+ CFile::Delete(cacheFile);
+ }
+}
+
+void CFileItemList::RemoveDiscCacheCRC(const std::string& crc) const
+{
+ std::string cachefile = StringUtils::Format("special://temp/archive_cache/{}.fi", crc);
+ RemoveDiscCache(cachefile);
+}
+
+std::string CFileItemList::GetDiscFileCache(int windowID) const
+{
+ std::string strPath(GetPath());
+ URIUtils::RemoveSlashAtEnd(strPath);
+
+ uint32_t crc = Crc32::ComputeFromLowerCase(strPath);
+
+ if (IsCDDA() || IsOnDVD())
+ return StringUtils::Format("special://temp/archive_cache/r-{:08x}.fi", crc);
+
+ if (IsMusicDb())
+ return StringUtils::Format("special://temp/archive_cache/mdb-{:08x}.fi", crc);
+
+ if (IsVideoDb())
+ return StringUtils::Format("special://temp/archive_cache/vdb-{:08x}.fi", crc);
+
+ if (IsSmartPlayList())
+ return StringUtils::Format("special://temp/archive_cache/sp-{:08x}.fi", crc);
+
+ if (windowID)
+ return StringUtils::Format("special://temp/archive_cache/{}-{:08x}.fi", windowID, crc);
+
+ return StringUtils::Format("special://temp/archive_cache/{:08x}.fi", crc);
+}
+
+bool CFileItemList::AlwaysCache() const
+{
+ // some database folders are always cached
+ if (IsMusicDb())
+ return CMusicDatabaseDirectory::CanCache(GetPath());
+ if (IsVideoDb())
+ return CVideoDatabaseDirectory::CanCache(GetPath());
+ if (IsEPG())
+ return true; // always cache
+ return false;
+}
+
+std::string CFileItem::GetUserMusicThumb(bool alwaysCheckRemote /* = false */, bool fallbackToFolder /* = false */) const
+{
+ if (m_strPath.empty()
+ || StringUtils::StartsWithNoCase(m_strPath, "newsmartplaylist://")
+ || StringUtils::StartsWithNoCase(m_strPath, "newplaylist://")
+ || m_bIsShareOrDrive
+ || IsInternetStream()
+ || URIUtils::IsUPnP(m_strPath)
+ || (URIUtils::IsFTP(m_strPath) && !CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bFTPThumbs)
+ || IsPlugin()
+ || IsAddonsPath()
+ || IsLibraryFolder()
+ || IsParentFolder()
+ || IsMusicDb())
+ return "";
+
+ // we first check for <filename>.tbn or <foldername>.tbn
+ std::string fileThumb(GetTBNFile());
+ if (CFile::Exists(fileThumb))
+ return fileThumb;
+
+ // Fall back to folder thumb, if requested
+ if (!m_bIsFolder && fallbackToFolder)
+ {
+ CFileItem item(URIUtils::GetDirectory(m_strPath), true);
+ return item.GetUserMusicThumb(alwaysCheckRemote);
+ }
+
+ // if a folder, check for folder.jpg
+ if (m_bIsFolder && !IsFileFolder() && (!IsRemote() || alwaysCheckRemote || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICFILES_FINDREMOTETHUMBS)))
+ {
+ std::vector<CVariant> thumbs = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_MUSICLIBRARY_MUSICTHUMBS);
+ for (const auto& i : thumbs)
+ {
+ std::string strFileName = i.asString();
+ std::string folderThumb(GetFolderThumb(strFileName));
+ if (CFile::Exists(folderThumb)) // folder.jpg
+ return folderThumb;
+ size_t period = strFileName.find_last_of('.');
+ if (period != std::string::npos)
+ {
+ std::string ext;
+ std::string name = strFileName;
+ std::string folderThumb1 = folderThumb;
+ name.erase(period);
+ ext = strFileName.substr(period);
+ StringUtils::ToUpper(ext);
+ StringUtils::Replace(folderThumb1, strFileName, name + ext);
+ if (CFile::Exists(folderThumb1)) // folder.JPG
+ return folderThumb1;
+
+ folderThumb1 = folderThumb;
+ std::string firstletter = name.substr(0, 1);
+ StringUtils::ToUpper(firstletter);
+ name.replace(0, 1, firstletter);
+ StringUtils::Replace(folderThumb1, strFileName, name + ext);
+ if (CFile::Exists(folderThumb1)) // Folder.JPG
+ return folderThumb1;
+
+ folderThumb1 = folderThumb;
+ StringUtils::ToLower(ext);
+ StringUtils::Replace(folderThumb1, strFileName, name + ext);
+ if (CFile::Exists(folderThumb1)) // Folder.jpg
+ return folderThumb1;
+ }
+ }
+ }
+ // No thumb found
+ return "";
+}
+
+// Gets the .tbn filename from a file or folder name.
+// <filename>.ext -> <filename>.tbn
+// <foldername>/ -> <foldername>.tbn
+std::string CFileItem::GetTBNFile() const
+{
+ std::string thumbFile;
+ std::string strFile = m_strPath;
+
+ if (IsStack())
+ {
+ std::string strPath, strReturn;
+ URIUtils::GetParentPath(m_strPath,strPath);
+ CFileItem item(CStackDirectory::GetFirstStackedFile(strFile),false);
+ std::string strTBNFile = item.GetTBNFile();
+ strReturn = URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(strTBNFile));
+ if (CFile::Exists(strReturn))
+ return strReturn;
+
+ strFile = URIUtils::AddFileToFolder(strPath,URIUtils::GetFileName(CStackDirectory::GetStackedTitlePath(strFile)));
+ }
+
+ if (URIUtils::IsInRAR(strFile) || URIUtils::IsInZIP(strFile))
+ {
+ std::string strPath = URIUtils::GetDirectory(strFile);
+ std::string strParent;
+ URIUtils::GetParentPath(strPath,strParent);
+ strFile = URIUtils::AddFileToFolder(strParent, URIUtils::GetFileName(m_strPath));
+ }
+
+ CURL url(strFile);
+ strFile = url.GetFileName();
+
+ if (m_bIsFolder && !IsFileFolder())
+ URIUtils::RemoveSlashAtEnd(strFile);
+
+ if (!strFile.empty())
+ {
+ if (m_bIsFolder && !IsFileFolder())
+ thumbFile = strFile + ".tbn"; // folder, so just add ".tbn"
+ else
+ thumbFile = URIUtils::ReplaceExtension(strFile, ".tbn");
+ url.SetFileName(thumbFile);
+ thumbFile = url.Get();
+ }
+ return thumbFile;
+}
+
+bool CFileItem::SkipLocalArt() const
+{
+ return (m_strPath.empty()
+ || StringUtils::StartsWithNoCase(m_strPath, "newsmartplaylist://")
+ || StringUtils::StartsWithNoCase(m_strPath, "newplaylist://")
+ || m_bIsShareOrDrive
+ || IsInternetStream()
+ || URIUtils::IsUPnP(m_strPath)
+ || (URIUtils::IsFTP(m_strPath) && !CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bFTPThumbs)
+ || IsPlugin()
+ || IsAddonsPath()
+ || IsLibraryFolder()
+ || IsParentFolder()
+ || IsLiveTV()
+ || IsPVRRecording()
+ || IsDVD());
+}
+
+std::string CFileItem::GetThumbHideIfUnwatched(const CFileItem* item) const
+{
+ const std::shared_ptr<CSettingList> setting(std::dynamic_pointer_cast<CSettingList>(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(
+ CSettings::SETTING_VIDEOLIBRARY_SHOWUNWATCHEDPLOTS)));
+ if (setting && item->HasVideoInfoTag() && item->GetVideoInfoTag()->m_type == MediaTypeEpisode &&
+ item->GetVideoInfoTag()->GetPlayCount() == 0 &&
+ !CSettingUtils::FindIntInList(setting,
+ CSettings::VIDEOLIBRARY_THUMB_SHOW_UNWATCHED_EPISODE) &&
+ item->HasArt("thumb"))
+ {
+ std::string fanArt = item->GetArt("fanart");
+ if (fanArt.empty())
+ return "OverlaySpoiler.png";
+ else
+ return fanArt;
+ }
+
+ return item->GetArt("thumb");
+}
+
+std::string CFileItem::FindLocalArt(const std::string &artFile, bool useFolder) const
+{
+ if (SkipLocalArt())
+ return "";
+
+ std::string thumb;
+ if (!m_bIsFolder)
+ {
+ thumb = GetLocalArt(artFile, false);
+ if (!thumb.empty() && CFile::Exists(thumb))
+ return thumb;
+ }
+ if ((useFolder || (m_bIsFolder && !IsFileFolder())) && !artFile.empty())
+ {
+ std::string thumb2 = GetLocalArt(artFile, true);
+ if (!thumb2.empty() && thumb2 != thumb && CFile::Exists(thumb2))
+ return thumb2;
+ }
+ return "";
+}
+
+std::string CFileItem::GetLocalArtBaseFilename() const
+{
+ bool useFolder = false;
+ return GetLocalArtBaseFilename(useFolder);
+}
+
+std::string CFileItem::GetLocalArtBaseFilename(bool& useFolder) const
+{
+ std::string strFile = m_strPath;
+ if (IsStack())
+ {
+ std::string strPath;
+ URIUtils::GetParentPath(m_strPath,strPath);
+ strFile = URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(CStackDirectory::GetStackedTitlePath(strFile)));
+ }
+
+ if (URIUtils::IsInRAR(strFile) || URIUtils::IsInZIP(strFile))
+ {
+ std::string strPath = URIUtils::GetDirectory(strFile);
+ std::string strParent;
+ URIUtils::GetParentPath(strPath,strParent);
+ strFile = URIUtils::AddFileToFolder(strParent, URIUtils::GetFileName(strFile));
+ }
+
+ if (IsMultiPath())
+ strFile = CMultiPathDirectory::GetFirstPath(m_strPath);
+
+ if (IsOpticalMediaFile())
+ { // optical media files should be treated like folders
+ useFolder = true;
+ strFile = GetLocalMetadataPath();
+ }
+ else if (useFolder && !(m_bIsFolder && !IsFileFolder()))
+ strFile = URIUtils::GetDirectory(strFile);
+
+ return strFile;
+}
+
+std::string CFileItem::GetLocalArt(const std::string& artFile, bool useFolder) const
+{
+ // no retrieving of empty art files from folders
+ if (useFolder && artFile.empty())
+ return "";
+
+ std::string strFile = GetLocalArtBaseFilename(useFolder);
+ if (strFile.empty()) // empty filepath -> nothing to find
+ return "";
+
+ if (useFolder)
+ {
+ if (!artFile.empty())
+ return URIUtils::AddFileToFolder(strFile, artFile);
+ }
+ else
+ {
+ if (artFile.empty()) // old thumbnail matching
+ return URIUtils::ReplaceExtension(strFile, ".tbn");
+ else
+ return URIUtils::ReplaceExtension(strFile, "-" + artFile);
+ }
+ return "";
+}
+
+std::string CFileItem::GetFolderThumb(const std::string &folderJPG /* = "folder.jpg" */) const
+{
+ std::string strFolder = m_strPath;
+
+ if (IsStack() ||
+ URIUtils::IsInRAR(strFolder) ||
+ URIUtils::IsInZIP(strFolder))
+ {
+ URIUtils::GetParentPath(m_strPath,strFolder);
+ }
+
+ if (IsMultiPath())
+ strFolder = CMultiPathDirectory::GetFirstPath(m_strPath);
+
+ if (IsPlugin())
+ return "";
+
+ return URIUtils::AddFileToFolder(strFolder, folderJPG);
+}
+
+std::string CFileItem::GetMovieName(bool bUseFolderNames /* = false */) const
+{
+ if (IsPlugin() && HasVideoInfoTag() && !GetVideoInfoTag()->m_strTitle.empty())
+ return GetVideoInfoTag()->m_strTitle;
+
+ if (IsLabelPreformatted())
+ return GetLabel();
+
+ if (m_pvrRecordingInfoTag)
+ return m_pvrRecordingInfoTag->m_strTitle;
+ else if (URIUtils::IsPVRRecording(m_strPath))
+ {
+ std::string title = CPVRRecording::GetTitleFromURL(m_strPath);
+ if (!title.empty())
+ return title;
+ }
+
+ std::string strMovieName;
+ if (URIUtils::IsStack(m_strPath))
+ strMovieName = CStackDirectory::GetStackedTitlePath(m_strPath);
+ else
+ strMovieName = GetBaseMoviePath(bUseFolderNames);
+
+ URIUtils::RemoveSlashAtEnd(strMovieName);
+
+ return CURL::Decode(URIUtils::GetFileName(strMovieName));
+}
+
+std::string CFileItem::GetBaseMoviePath(bool bUseFolderNames) const
+{
+ std::string strMovieName = m_strPath;
+
+ if (IsMultiPath())
+ strMovieName = CMultiPathDirectory::GetFirstPath(m_strPath);
+
+ if (IsOpticalMediaFile())
+ return GetLocalMetadataPath();
+
+ if (bUseFolderNames &&
+ (!m_bIsFolder || URIUtils::IsInArchive(m_strPath) ||
+ (HasVideoInfoTag() && GetVideoInfoTag()->m_iDbId > 0 && !CMediaTypes::IsContainer(GetVideoInfoTag()->m_type))))
+ {
+ std::string name2(strMovieName);
+ URIUtils::GetParentPath(name2,strMovieName);
+ if (URIUtils::IsInArchive(m_strPath))
+ {
+ // Try to get archive itself, if empty take path before
+ name2 = CURL(m_strPath).GetHostName();
+ if (name2.empty())
+ name2 = strMovieName;
+
+ URIUtils::GetParentPath(name2, strMovieName);
+ }
+ }
+
+ return strMovieName;
+}
+
+std::string CFileItem::GetLocalFanart() const
+{
+ if (IsVideoDb())
+ {
+ if (!HasVideoInfoTag())
+ return ""; // nothing can be done
+ CFileItem dbItem(m_bIsFolder ? GetVideoInfoTag()->m_strPath : GetVideoInfoTag()->m_strFileNameAndPath, m_bIsFolder);
+ return dbItem.GetLocalFanart();
+ }
+
+ std::string strFile2;
+ std::string strFile = m_strPath;
+ if (IsStack())
+ {
+ std::string strPath;
+ URIUtils::GetParentPath(m_strPath,strPath);
+ CStackDirectory dir;
+ std::string strPath2;
+ strPath2 = dir.GetStackedTitlePath(strFile);
+ strFile = URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(strPath2));
+ CFileItem item(dir.GetFirstStackedFile(m_strPath),false);
+ std::string strTBNFile(URIUtils::ReplaceExtension(item.GetTBNFile(), "-fanart"));
+ strFile2 = URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(strTBNFile));
+ }
+ if (URIUtils::IsInRAR(strFile) || URIUtils::IsInZIP(strFile))
+ {
+ std::string strPath = URIUtils::GetDirectory(strFile);
+ std::string strParent;
+ URIUtils::GetParentPath(strPath,strParent);
+ strFile = URIUtils::AddFileToFolder(strParent, URIUtils::GetFileName(m_strPath));
+ }
+
+ // no local fanart available for these
+ if (IsInternetStream()
+ || URIUtils::IsUPnP(strFile)
+ || URIUtils::IsBluray(strFile)
+ || IsLiveTV()
+ || IsPlugin()
+ || IsAddonsPath()
+ || IsDVD()
+ || (URIUtils::IsFTP(strFile) && !CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bFTPThumbs)
+ || m_strPath.empty())
+ return "";
+
+ std::string strDir = URIUtils::GetDirectory(strFile);
+
+ if (strDir.empty())
+ return "";
+
+ CFileItemList items;
+ CDirectory::GetDirectory(strDir, items, CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
+ if (IsOpticalMediaFile())
+ { // grab from the optical media parent folder as well
+ CFileItemList moreItems;
+ CDirectory::GetDirectory(GetLocalMetadataPath(), moreItems, CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
+ items.Append(moreItems);
+ }
+
+ std::vector<std::string> fanarts = { "fanart" };
+
+ strFile = URIUtils::ReplaceExtension(strFile, "-fanart");
+ fanarts.insert(m_bIsFolder ? fanarts.end() : fanarts.begin(), URIUtils::GetFileName(strFile));
+
+ if (!strFile2.empty())
+ fanarts.insert(m_bIsFolder ? fanarts.end() : fanarts.begin(), URIUtils::GetFileName(strFile2));
+
+ for (std::vector<std::string>::const_iterator i = fanarts.begin(); i != fanarts.end(); ++i)
+ {
+ for (int j = 0; j < items.Size(); j++)
+ {
+ std::string strCandidate = URIUtils::GetFileName(items[j]->m_strPath);
+ URIUtils::RemoveExtension(strCandidate);
+ std::string strFanart = *i;
+ URIUtils::RemoveExtension(strFanart);
+ if (StringUtils::EqualsNoCase(strCandidate, strFanart))
+ return items[j]->m_strPath;
+ }
+ }
+
+ return "";
+}
+
+std::string CFileItem::GetLocalMetadataPath() const
+{
+ if (m_bIsFolder && !IsFileFolder())
+ return m_strPath;
+
+ std::string parent(URIUtils::GetParentPath(m_strPath));
+ std::string parentFolder(parent);
+ URIUtils::RemoveSlashAtEnd(parentFolder);
+ parentFolder = URIUtils::GetFileName(parentFolder);
+ if (StringUtils::EqualsNoCase(parentFolder, "VIDEO_TS") || StringUtils::EqualsNoCase(parentFolder, "BDMV"))
+ { // go back up another one
+ parent = URIUtils::GetParentPath(parent);
+ }
+ return parent;
+}
+
+bool CFileItem::LoadMusicTag()
+{
+ // not audio
+ if (!IsAudio())
+ return false;
+ // already loaded?
+ if (HasMusicInfoTag() && m_musicInfoTag->Loaded())
+ return true;
+ // check db
+ CMusicDatabase musicDatabase;
+ if (musicDatabase.Open())
+ {
+ CSong song;
+ if (musicDatabase.GetSongByFileName(m_strPath, song))
+ {
+ GetMusicInfoTag()->SetSong(song);
+ return true;
+ }
+ musicDatabase.Close();
+ }
+ // load tag from file
+ CLog::Log(LOGDEBUG, "{}: loading tag information for file: {}", __FUNCTION__, m_strPath);
+ CMusicInfoTagLoaderFactory factory;
+ std::unique_ptr<IMusicInfoTagLoader> pLoader (factory.CreateLoader(*this));
+ if (pLoader)
+ {
+ if (pLoader->Load(m_strPath, *GetMusicInfoTag()))
+ return true;
+ }
+ // no tag - try some other things
+ if (IsCDDA())
+ {
+ // we have the tracknumber...
+ int iTrack = GetMusicInfoTag()->GetTrackNumber();
+ if (iTrack >= 1)
+ {
+ std::string strText = g_localizeStrings.Get(554); // "Track"
+ if (!strText.empty() && strText[strText.size() - 1] != ' ')
+ strText += " ";
+ std::string strTrack = StringUtils::Format((strText + "{}"), iTrack);
+ GetMusicInfoTag()->SetTitle(strTrack);
+ GetMusicInfoTag()->SetLoaded(true);
+ return true;
+ }
+ }
+ else
+ {
+ std::string fileName = URIUtils::GetFileName(m_strPath);
+ URIUtils::RemoveExtension(fileName);
+ for (const std::string& fileFilter : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicTagsFromFileFilters)
+ {
+ CLabelFormatter formatter(fileFilter, "");
+ if (formatter.FillMusicTag(fileName, GetMusicInfoTag()))
+ {
+ GetMusicInfoTag()->SetLoaded(true);
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+bool CFileItem::LoadGameTag()
+{
+ // Already loaded?
+ if (HasGameInfoTag() && m_gameInfoTag->IsLoaded())
+ return true;
+
+ //! @todo
+ GetGameInfoTag();
+
+ m_gameInfoTag->SetLoaded(true);
+
+ return false;
+}
+
+bool CFileItem::LoadDetails()
+{
+ if (IsVideoDb())
+ {
+ if (HasVideoInfoTag())
+ return true;
+
+ CVideoDatabase db;
+ if (!db.Open())
+ return false;
+
+ VIDEODATABASEDIRECTORY::CQueryParams params;
+ VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(GetPath(), params);
+
+ if (params.GetMovieId() >= 0)
+ db.GetMovieInfo(GetPath(), *GetVideoInfoTag(), static_cast<int>(params.GetMovieId()));
+ else if (params.GetMVideoId() >= 0)
+ db.GetMusicVideoInfo(GetPath(), *GetVideoInfoTag(), static_cast<int>(params.GetMVideoId()));
+ else if (params.GetEpisodeId() >= 0)
+ db.GetEpisodeInfo(GetPath(), *GetVideoInfoTag(), static_cast<int>(params.GetEpisodeId()));
+ else if (params.GetSetId() >= 0) // movie set
+ db.GetSetInfo(static_cast<int>(params.GetSetId()), *GetVideoInfoTag(), this);
+ else if (params.GetTvShowId() >= 0)
+ {
+ if (params.GetSeason() >= 0)
+ {
+ const int idSeason = db.GetSeasonId(static_cast<int>(params.GetTvShowId()),
+ static_cast<int>(params.GetSeason()));
+ if (idSeason >= 0)
+ db.GetSeasonInfo(idSeason, *GetVideoInfoTag(), this);
+ }
+ else
+ db.GetTvShowInfo(GetPath(), *GetVideoInfoTag(), static_cast<int>(params.GetTvShowId()),
+ this);
+ }
+ else
+ {
+ db.Close();
+ return false;
+ }
+ db.Close();
+ return true;
+ }
+
+ if (m_bIsFolder && URIUtils::IsPVRRecordingFileOrFolder(GetPath()))
+ {
+ if (HasProperty("watchedepisodes") || HasProperty("watched"))
+ return true;
+
+ const std::string parentPath = URIUtils::GetParentPath(GetPath());
+
+ //! @todo optimize, find a way to set the details of the directory without loading its content.
+ CFileItemList items;
+ if (CDirectory::GetDirectory(parentPath, items, "", XFILE::DIR_FLAG_DEFAULTS))
+ {
+ const std::string path = GetPath();
+ const auto it = std::find_if(items.cbegin(), items.cend(),
+ [path](const auto& entry) { return entry->GetPath() == path; });
+ if (it != items.cend())
+ {
+ *this = *(*it);
+ return true;
+ }
+ }
+
+ CLog::LogF(LOGERROR, "Error filling item details (path={})", GetPath());
+ return false;
+ }
+
+ //! @todo add support for other types on demand.
+ CLog::LogF(LOGDEBUG, "Unsupported item type (path={})", GetPath());
+ return false;
+}
+
+void CFileItemList::Swap(unsigned int item1, unsigned int item2)
+{
+ if (item1 != item2 && item1 < m_items.size() && item2 < m_items.size())
+ std::swap(m_items[item1], m_items[item2]);
+}
+
+bool CFileItemList::UpdateItem(const CFileItem *item)
+{
+ if (!item)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ for (unsigned int i = 0; i < m_items.size(); i++)
+ {
+ CFileItemPtr pItem = m_items[i];
+ if (pItem->IsSamePath(item))
+ {
+ pItem->UpdateInfo(*item);
+ return true;
+ }
+ }
+ return false;
+}
+
+void CFileItemList::AddSortMethod(SortBy sortBy, int buttonLabel, const LABEL_MASKS &labelMasks, SortAttribute sortAttributes /* = SortAttributeNone */)
+{
+ AddSortMethod(sortBy, sortAttributes, buttonLabel, labelMasks);
+}
+
+void CFileItemList::AddSortMethod(SortBy sortBy, SortAttribute sortAttributes, int buttonLabel, const LABEL_MASKS &labelMasks)
+{
+ SortDescription sorting;
+ sorting.sortBy = sortBy;
+ sorting.sortAttributes = sortAttributes;
+
+ AddSortMethod(sorting, buttonLabel, labelMasks);
+}
+
+void CFileItemList::AddSortMethod(SortDescription sortDescription, int buttonLabel, const LABEL_MASKS &labelMasks)
+{
+ GUIViewSortDetails sort;
+ sort.m_sortDescription = sortDescription;
+ sort.m_buttonLabel = buttonLabel;
+ sort.m_labelMasks = labelMasks;
+
+ m_sortDetails.push_back(sort);
+}
+
+void CFileItemList::SetReplaceListing(bool replace)
+{
+ m_replaceListing = replace;
+}
+
+void CFileItemList::ClearSortState()
+{
+ m_sortDescription.sortBy = SortByNone;
+ m_sortDescription.sortOrder = SortOrderNone;
+ m_sortDescription.sortAttributes = SortAttributeNone;
+}
+
+bool CFileItem::HasVideoInfoTag() const
+{
+ // Note: CPVRRecording is derived from CVideoInfoTag
+ return m_pvrRecordingInfoTag.get() != nullptr || m_videoInfoTag != nullptr;
+}
+
+CVideoInfoTag* CFileItem::GetVideoInfoTag()
+{
+ // Note: CPVRRecording is derived from CVideoInfoTag
+ if (m_pvrRecordingInfoTag)
+ return m_pvrRecordingInfoTag.get();
+ else if (!m_videoInfoTag)
+ m_videoInfoTag = new CVideoInfoTag;
+
+ return m_videoInfoTag;
+}
+
+const CVideoInfoTag* CFileItem::GetVideoInfoTag() const
+{
+ // Note: CPVRRecording is derived from CVideoInfoTag
+ return m_pvrRecordingInfoTag ? m_pvrRecordingInfoTag.get() : m_videoInfoTag;
+}
+
+CPictureInfoTag* CFileItem::GetPictureInfoTag()
+{
+ if (!m_pictureInfoTag)
+ m_pictureInfoTag = new CPictureInfoTag;
+
+ return m_pictureInfoTag;
+}
+
+MUSIC_INFO::CMusicInfoTag* CFileItem::GetMusicInfoTag()
+{
+ if (!m_musicInfoTag)
+ m_musicInfoTag = new MUSIC_INFO::CMusicInfoTag;
+
+ return m_musicInfoTag;
+}
+
+CGameInfoTag* CFileItem::GetGameInfoTag()
+{
+ if (!m_gameInfoTag)
+ m_gameInfoTag = new CGameInfoTag;
+
+ return m_gameInfoTag;
+}
+
+bool CFileItem::HasPVRChannelInfoTag() const
+{
+ return m_pvrChannelGroupMemberInfoTag && m_pvrChannelGroupMemberInfoTag->Channel() != nullptr;
+}
+
+const std::shared_ptr<PVR::CPVRChannel> CFileItem::GetPVRChannelInfoTag() const
+{
+ return m_pvrChannelGroupMemberInfoTag ? m_pvrChannelGroupMemberInfoTag->Channel()
+ : std::shared_ptr<CPVRChannel>();
+}
+
+std::string CFileItem::FindTrailer() const
+{
+ std::string strFile2;
+ std::string strFile = m_strPath;
+ if (IsStack())
+ {
+ std::string strPath;
+ URIUtils::GetParentPath(m_strPath,strPath);
+ CStackDirectory dir;
+ std::string strPath2;
+ strPath2 = dir.GetStackedTitlePath(strFile);
+ strFile = URIUtils::AddFileToFolder(strPath,URIUtils::GetFileName(strPath2));
+ CFileItem item(dir.GetFirstStackedFile(m_strPath),false);
+ std::string strTBNFile(URIUtils::ReplaceExtension(item.GetTBNFile(), "-trailer"));
+ strFile2 = URIUtils::AddFileToFolder(strPath,URIUtils::GetFileName(strTBNFile));
+ }
+ if (URIUtils::IsInRAR(strFile) || URIUtils::IsInZIP(strFile))
+ {
+ std::string strPath = URIUtils::GetDirectory(strFile);
+ std::string strParent;
+ URIUtils::GetParentPath(strPath,strParent);
+ strFile = URIUtils::AddFileToFolder(strParent,URIUtils::GetFileName(m_strPath));
+ }
+
+ // no local trailer available for these
+ if (IsInternetStream()
+ || URIUtils::IsUPnP(strFile)
+ || URIUtils::IsBluray(strFile)
+ || IsLiveTV()
+ || IsPlugin()
+ || IsDVD())
+ return "";
+
+ std::string strDir = URIUtils::GetDirectory(strFile);
+ CFileItemList items;
+ CDirectory::GetDirectory(strDir, items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO | DIR_FLAG_NO_FILE_DIRS);
+ URIUtils::RemoveExtension(strFile);
+ strFile += "-trailer";
+ std::string strFile3 = URIUtils::AddFileToFolder(strDir, "movie-trailer");
+
+ // Precompile our REs
+ VECCREGEXP matchRegExps;
+ CRegExp tmpRegExp(true, CRegExp::autoUtf8);
+ const std::vector<std::string>& strMatchRegExps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_trailerMatchRegExps;
+
+ std::vector<std::string>::const_iterator strRegExp = strMatchRegExps.begin();
+ while (strRegExp != strMatchRegExps.end())
+ {
+ if (tmpRegExp.RegComp(*strRegExp))
+ {
+ matchRegExps.push_back(tmpRegExp);
+ }
+ ++strRegExp;
+ }
+
+ std::string strTrailer;
+ for (int i = 0; i < items.Size(); i++)
+ {
+ std::string strCandidate = items[i]->m_strPath;
+ URIUtils::RemoveExtension(strCandidate);
+ if (StringUtils::EqualsNoCase(strCandidate, strFile) ||
+ StringUtils::EqualsNoCase(strCandidate, strFile2) ||
+ StringUtils::EqualsNoCase(strCandidate, strFile3))
+ {
+ strTrailer = items[i]->m_strPath;
+ break;
+ }
+ else
+ {
+ VECCREGEXP::iterator expr = matchRegExps.begin();
+
+ while (expr != matchRegExps.end())
+ {
+ if (expr->RegFind(strCandidate) != -1)
+ {
+ strTrailer = items[i]->m_strPath;
+ i = items.Size();
+ break;
+ }
+ ++expr;
+ }
+ }
+ }
+
+ return strTrailer;
+}
+
+VideoDbContentType CFileItem::GetVideoContentType() const
+{
+ VideoDbContentType type = VideoDbContentType::MOVIES;
+ if (HasVideoInfoTag() && GetVideoInfoTag()->m_type == MediaTypeTvShow)
+ type = VideoDbContentType::TVSHOWS;
+ if (HasVideoInfoTag() && GetVideoInfoTag()->m_type == MediaTypeEpisode)
+ return VideoDbContentType::EPISODES;
+ if (HasVideoInfoTag() && GetVideoInfoTag()->m_type == MediaTypeMusicVideo)
+ return VideoDbContentType::MUSICVIDEOS;
+ if (HasVideoInfoTag() && GetVideoInfoTag()->m_type == MediaTypeAlbum)
+ return VideoDbContentType::MUSICALBUMS;
+
+ CVideoDatabaseDirectory dir;
+ VIDEODATABASEDIRECTORY::CQueryParams params;
+ dir.GetQueryParams(m_strPath, params);
+ if (params.GetSetId() != -1 && params.GetMovieId() == -1) // movie set
+ return VideoDbContentType::MOVIE_SETS;
+
+ return type;
+}
+
+CFileItem CFileItem::GetItemToPlay() const
+{
+ if (HasEPGInfoTag())
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(*this);
+ if (groupMember)
+ return CFileItem(groupMember);
+ }
+ return *this;
+}
+
+CBookmark CFileItem::GetResumePoint() const
+{
+ if (HasVideoInfoTag())
+ return GetVideoInfoTag()->GetResumePoint();
+ return CBookmark();
+}
+
+bool CFileItem::IsResumePointSet() const
+{
+ return GetResumePoint().IsSet();
+}
+
+double CFileItem::GetCurrentResumeTime() const
+{
+ return lrint(GetResumePoint().timeInSeconds);
+}
+
+bool CFileItem::GetCurrentResumeTimeAndPartNumber(int64_t& startOffset, int& partNumber) const
+{
+ CBookmark resumePoint(GetResumePoint());
+ if (resumePoint.IsSet())
+ {
+ startOffset = llrint(resumePoint.timeInSeconds);
+ partNumber = resumePoint.partNumber;
+ return true;
+ }
+ return false;
+}
+
+bool CFileItem::IsResumable() const
+{
+ return (!IsNFO() && !IsPlayList()) || IsType(".strm");
+}
diff --git a/xbmc/FileItem.h b/xbmc/FileItem.h
new file mode 100644
index 0000000..e0adc28
--- /dev/null
+++ b/xbmc/FileItem.h
@@ -0,0 +1,884 @@
+/*
+ * 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
+
+/*!
+ \file FileItem.h
+ \brief
+ */
+
+#include "LockType.h"
+#include "XBDateTime.h"
+#include "guilib/GUIListItem.h"
+#include "threads/CriticalSection.h"
+#include "utils/IArchivable.h"
+#include "utils/ISerializable.h"
+#include "utils/ISortable.h"
+#include "utils/SortUtils.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+enum class VideoDbContentType;
+
+namespace ADDON
+{
+class IAddon;
+}
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTag;
+}
+class CVideoInfoTag;
+class CPictureInfoTag;
+
+namespace KODI
+{
+namespace GAME
+{
+ class CGameInfoTag;
+}
+}
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRChannelGroupMember;
+class CPVREpgInfoTag;
+class CPVREpgSearchFilter;
+class CPVRRecording;
+class CPVRTimerInfoTag;
+}
+
+class CAlbum;
+class CArtist;
+class CSong;
+class CGenre;
+
+class CURL;
+class CVariant;
+
+class CFileItemList;
+class CCueDocument;
+typedef std::shared_ptr<CCueDocument> CCueDocumentPtr;
+
+class IEvent;
+typedef std::shared_ptr<const IEvent> EventPtr;
+
+/* special startoffset used to indicate that we wish to resume */
+#define STARTOFFSET_RESUME (-1)
+
+class CMediaSource;
+
+class CBookmark;
+
+enum EFileFolderType {
+ EFILEFOLDER_TYPE_ALWAYS = 1<<0,
+ EFILEFOLDER_TYPE_ONCLICK = 1<<1,
+ EFILEFOLDER_TYPE_ONBROWSE = 1<<2,
+
+ EFILEFOLDER_MASK_ALL = 0xff,
+ EFILEFOLDER_MASK_ONCLICK = EFILEFOLDER_TYPE_ALWAYS
+ | EFILEFOLDER_TYPE_ONCLICK,
+ EFILEFOLDER_MASK_ONBROWSE = EFILEFOLDER_TYPE_ALWAYS
+ | EFILEFOLDER_TYPE_ONCLICK
+ | EFILEFOLDER_TYPE_ONBROWSE,
+};
+
+/*!
+ \brief Represents a file on a share
+ \sa CFileItemList
+ */
+class CFileItem :
+ public CGUIListItem, public IArchivable, public ISerializable, public ISortable
+{
+public:
+ CFileItem(void);
+ CFileItem(const CFileItem& item);
+ explicit CFileItem(const CGUIListItem& item);
+ explicit CFileItem(const std::string& strLabel);
+ explicit CFileItem(const char* strLabel);
+ CFileItem(const CURL& path, bool bIsFolder);
+ CFileItem(const std::string& strPath, bool bIsFolder);
+ explicit CFileItem(const CSong& song);
+ CFileItem(const CSong& song, const MUSIC_INFO::CMusicInfoTag& music);
+ CFileItem(const CURL &path, const CAlbum& album);
+ CFileItem(const std::string &path, const CAlbum& album);
+ explicit CFileItem(const CArtist& artist);
+ explicit CFileItem(const CGenre& genre);
+ explicit CFileItem(const MUSIC_INFO::CMusicInfoTag& music);
+ explicit CFileItem(const CVideoInfoTag& movie);
+ explicit CFileItem(const std::shared_ptr<PVR::CPVREpgInfoTag>& tag);
+ explicit CFileItem(const std::shared_ptr<PVR::CPVREpgSearchFilter>& filter);
+ explicit CFileItem(const std::shared_ptr<PVR::CPVRChannelGroupMember>& channelGroupMember);
+ explicit CFileItem(const std::shared_ptr<PVR::CPVRRecording>& record);
+ explicit CFileItem(const std::shared_ptr<PVR::CPVRTimerInfoTag>& timer);
+ explicit CFileItem(const CMediaSource& share);
+ explicit CFileItem(std::shared_ptr<const ADDON::IAddon> addonInfo);
+ explicit CFileItem(const EventPtr& eventLogEntry);
+
+ ~CFileItem(void) override;
+ CGUIListItem* Clone() const override { return new CFileItem(*this); }
+
+ const CURL GetURL() const;
+ void SetURL(const CURL& url);
+ bool IsURL(const CURL& url) const;
+ const std::string& GetPath() const { return m_strPath; }
+ void SetPath(const std::string& path) { m_strPath = path; }
+ bool IsPath(const std::string& path, bool ignoreURLOptions = false) const;
+
+ const CURL GetDynURL() const;
+ void SetDynURL(const CURL& url);
+ const std::string &GetDynPath() const;
+ void SetDynPath(const std::string &path);
+
+ /*! \brief reset class to it's default values as per construction.
+ Free's all allocated memory.
+ \sa Initialize
+ */
+ void Reset();
+ CFileItem& operator=(const CFileItem& item);
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& value) const override;
+ void ToSortable(SortItem &sortable, Field field) const override;
+ void ToSortable(SortItem &sortable, const Fields &fields) const;
+ bool IsFileItem() const override { return true; }
+
+ bool Exists(bool bUseCache = true) const;
+
+ /*!
+ \brief Check whether an item is an optical media folder or its parent.
+ This will return the non-empty path to the playable entry point of the media
+ one or two levels down (VIDEO_TS.IFO for DVDs or index.bdmv for BDs).
+ The returned path will be empty if folder does not meet this criterion.
+ \return non-empty string if item is optical media folder, empty otherwise.
+ */
+ std::string GetOpticalMediaPath() const;
+ /*!
+ \brief Check whether an item is a video item. Note that this returns true for
+ anything with a video info tag, so that may include eg. folders.
+ \return true if item is video, false otherwise.
+ */
+ bool IsVideo() const;
+
+ bool IsDiscStub() const;
+
+ /*!
+ \brief Check whether an item is a picture item. Note that this returns true for
+ anything with a picture info tag, so that may include eg. folders.
+ \return true if item is picture, false otherwise.
+ */
+ bool IsPicture() const;
+ bool IsLyrics() const;
+ bool IsSubtitle() const;
+
+ /*!
+ \brief Check whether an item is an audio item. Note that this returns true for
+ anything with a music info tag, so that may include eg. folders.
+ \return true if item is audio, false otherwise.
+ */
+ bool IsAudio() const;
+
+ /*!
+ \brief Check whether an item is 'deleted' (for example, a trashed pvr recording).
+ \return true if item is 'deleted', false otherwise.
+ */
+ bool IsDeleted() const;
+
+ /*!
+ \brief Check whether an item is an audio book item.
+ \return true if item is audiobook, false otherwise.
+ */
+ bool IsAudioBook() const;
+
+ bool IsGame() const;
+ bool IsCUESheet() const;
+ bool IsInternetStream(const bool bStrictCheck = false) const;
+ bool IsStreamedFilesystem() const;
+ bool IsPlayList() const;
+ bool IsSmartPlayList() const;
+ bool IsLibraryFolder() const;
+ bool IsPythonScript() const;
+ bool IsPlugin() const;
+ bool IsScript() const;
+ bool IsAddonsPath() const;
+ bool IsSourcesPath() const;
+ bool IsNFO() const;
+ bool IsDiscImage() const;
+ bool IsOpticalMediaFile() const;
+ bool IsDVDFile(bool bVobs = true, bool bIfos = true) const;
+ bool IsBDFile() const;
+ bool IsBluray() const;
+ bool IsProtectedBlurayDisc() const;
+ bool IsRAR() const;
+ bool IsAPK() const;
+ bool IsZIP() const;
+ bool IsCBZ() const;
+ bool IsCBR() const;
+ bool IsISO9660() const;
+ bool IsCDDA() const;
+ bool IsDVD() const;
+ bool IsOnDVD() const;
+ bool IsOnLAN() const;
+ bool IsHD() const;
+ bool IsNfs() const;
+ bool IsRemote() const;
+ bool IsSmb() const;
+ bool IsURL() const;
+ bool IsStack() const;
+ bool IsFavourite() const;
+ bool IsMultiPath() const;
+ bool IsMusicDb() const;
+ bool IsVideoDb() const;
+ bool IsEPG() const;
+ bool IsPVRChannel() const;
+ bool IsPVRChannelGroup() const;
+ bool IsPVRRecording() const;
+ bool IsUsablePVRRecording() const;
+ bool IsDeletedPVRRecording() const;
+ bool IsInProgressPVRRecording() const;
+ bool IsPVRTimer() const;
+ bool IsType(const char *ext) const;
+ bool IsVirtualDirectoryRoot() const;
+ bool IsReadOnly() const;
+ bool CanQueue() const;
+ void SetCanQueue(bool bYesNo);
+ bool IsParentFolder() const;
+ bool IsFileFolder(EFileFolderType types = EFILEFOLDER_MASK_ALL) const;
+ bool IsRemovable() const;
+ bool IsPVR() const;
+ bool IsLiveTV() const;
+ bool IsRSS() const;
+ bool IsAndroidApp() const;
+
+ void RemoveExtension();
+ void CleanString();
+ void FillInDefaultIcon();
+ void SetFileSizeLabel();
+ void SetLabel(const std::string &strLabel) override;
+ VideoDbContentType GetVideoContentType() const;
+ bool IsLabelPreformatted() const { return m_bLabelPreformatted; }
+ void SetLabelPreformatted(bool bYesNo) { m_bLabelPreformatted=bYesNo; }
+ bool SortsOnTop() const { return m_specialSort == SortSpecialOnTop; }
+ bool SortsOnBottom() const { return m_specialSort == SortSpecialOnBottom; }
+ void SetSpecialSort(SortSpecial sort) { m_specialSort = sort; }
+
+ inline bool HasMusicInfoTag() const
+ {
+ return m_musicInfoTag != NULL;
+ }
+
+ MUSIC_INFO::CMusicInfoTag* GetMusicInfoTag();
+
+ inline const MUSIC_INFO::CMusicInfoTag* GetMusicInfoTag() const
+ {
+ return m_musicInfoTag;
+ }
+
+ bool HasVideoInfoTag() const;
+
+ CVideoInfoTag* GetVideoInfoTag();
+
+ const CVideoInfoTag* GetVideoInfoTag() const;
+
+ inline bool HasEPGInfoTag() const
+ {
+ return m_epgInfoTag.get() != NULL;
+ }
+
+ inline const std::shared_ptr<PVR::CPVREpgInfoTag> GetEPGInfoTag() const
+ {
+ return m_epgInfoTag;
+ }
+
+ bool HasEPGSearchFilter() const { return m_epgSearchFilter != nullptr; }
+
+ const std::shared_ptr<PVR::CPVREpgSearchFilter> GetEPGSearchFilter() const
+ {
+ return m_epgSearchFilter;
+ }
+
+ inline bool HasPVRChannelGroupMemberInfoTag() const
+ {
+ return m_pvrChannelGroupMemberInfoTag.get() != nullptr;
+ }
+
+ inline const std::shared_ptr<PVR::CPVRChannelGroupMember> GetPVRChannelGroupMemberInfoTag() const
+ {
+ return m_pvrChannelGroupMemberInfoTag;
+ }
+
+ bool HasPVRChannelInfoTag() const;
+ const std::shared_ptr<PVR::CPVRChannel> GetPVRChannelInfoTag() const;
+
+ inline bool HasPVRRecordingInfoTag() const
+ {
+ return m_pvrRecordingInfoTag.get() != NULL;
+ }
+
+ inline const std::shared_ptr<PVR::CPVRRecording> GetPVRRecordingInfoTag() const
+ {
+ return m_pvrRecordingInfoTag;
+ }
+
+ inline bool HasPVRTimerInfoTag() const
+ {
+ return m_pvrTimerInfoTag != NULL;
+ }
+
+ inline const std::shared_ptr<PVR::CPVRTimerInfoTag> GetPVRTimerInfoTag() const
+ {
+ return m_pvrTimerInfoTag;
+ }
+
+ /*!
+ \brief return the item to play. will be almost 'this', but can be different (e.g. "Play recording" from PVR EPG grid window)
+ \return the item to play
+ */
+ CFileItem GetItemToPlay() const;
+
+ /*!
+ \brief Test if this item has a valid resume point set.
+ \return True if this item has a resume point and it is set, false otherwise.
+ */
+ bool IsResumePointSet() const;
+
+ /*!
+ \brief Return the current resume time.
+ \return The time in seconds from the start to resume playing from.
+ */
+ double GetCurrentResumeTime() const;
+
+ /*!
+ \brief Return the current resume time and part.
+ \param startOffset will be filled with the resume time offset in seconds if item has a resume point set, is unchanged otherwise
+ \param partNumber will be filled with the part number if item has a resume point set, is unchanged otherwise
+ \return True if the item has a resume point set, false otherwise.
+ */
+ bool GetCurrentResumeTimeAndPartNumber(int64_t& startOffset, int& partNumber) const;
+
+ /*!
+ * \brief Test if this item type can be resumed.
+ * \return True if this item can be resumed, false otherwise.
+ */
+ bool IsResumable() const;
+
+ /*!
+ * \brief Get the offset where start the playback.
+ * \return The offset value as ms.
+ * Can return also special value -1, see define STARTOFFSET_RESUME.
+ */
+ int64_t GetStartOffset() const { return m_lStartOffset; }
+
+ /*!
+ * \brief Set the offset where start the playback.
+ * \param offset Set the offset value as ms,
+ or the special value STARTOFFSET_RESUME.
+ */
+ void SetStartOffset(const int64_t offset) { m_lStartOffset = offset; }
+
+ /*!
+ * \brief Get the end offset.
+ * \return The offset value as ms.
+ */
+ int64_t GetEndOffset() const { return m_lEndOffset; }
+
+ /*!
+ * \brief Set the end offset.
+ * \param offset Set the offset as ms.
+ */
+ void SetEndOffset(const int64_t offset) { m_lEndOffset = offset; }
+
+ inline bool HasPictureInfoTag() const
+ {
+ return m_pictureInfoTag != NULL;
+ }
+
+ inline const CPictureInfoTag* GetPictureInfoTag() const
+ {
+ return m_pictureInfoTag;
+ }
+
+ bool HasAddonInfo() const { return m_addonInfo != nullptr; }
+ const std::shared_ptr<const ADDON::IAddon> GetAddonInfo() const { return m_addonInfo; }
+
+ inline bool HasGameInfoTag() const
+ {
+ return m_gameInfoTag != NULL;
+ }
+
+ KODI::GAME::CGameInfoTag* GetGameInfoTag();
+
+ inline const KODI::GAME::CGameInfoTag* GetGameInfoTag() const
+ {
+ return m_gameInfoTag;
+ }
+
+ CPictureInfoTag* GetPictureInfoTag();
+
+ /*!
+ \brief Get the local fanart for this item if it exists
+ \return path to the local fanart for this item, or empty if none exists
+ \sa GetFolderThumb, GetTBNFile
+ */
+ std::string GetLocalFanart() const;
+
+ /*!
+ \brief Assemble the base filename of local artwork for an item,
+ accounting for archives, stacks and multi-paths, and BDMV/VIDEO_TS folders.
+ `useFolder` is set to false
+ \return the path to the base filename for artwork lookup.
+ \sa GetLocalArt
+ */
+ std::string GetLocalArtBaseFilename() const;
+ /*!
+ \brief Assemble the base filename of local artwork for an item,
+ accounting for archives, stacks and multi-paths, and BDMV/VIDEO_TS folders.
+ \param useFolder whether to look in the folder for the art file. Defaults to false.
+ \return the path to the base filename for artwork lookup.
+ \sa GetLocalArt
+ */
+ std::string GetLocalArtBaseFilename(bool& useFolder) const;
+
+ /*! \brief Assemble the filename of a particular piece of local artwork for an item.
+ No file existence check is typically performed.
+ \param artFile the art file to search for.
+ \param useFolder whether to look in the folder for the art file. Defaults to false.
+ \return the path to the local artwork.
+ \sa FindLocalArt
+ */
+ std::string GetLocalArt(const std::string& artFile, bool useFolder = false) const;
+
+ /*! \brief Assemble the filename of a particular piece of local artwork for an item,
+ and check for file existence.
+ \param artFile the art file to search for.
+ \param useFolder whether to look in the folder for the art file. Defaults to false.
+ \return the path to the local artwork if it exists, empty otherwise.
+ \sa GetLocalArt
+ */
+ std::string FindLocalArt(const std::string &artFile, bool useFolder) const;
+
+ /*! \brief Whether or not to skip searching for local art.
+ \return true if local art should be skipped for this item, false otherwise.
+ \sa GetLocalArt, FindLocalArt
+ */
+ bool SkipLocalArt() const;
+
+ /*! \brief Get the thumb for the item, but hide it to prevent spoilers if
+ the user has set 'Show information for unwatched items' appropriately.
+ \param item the item to get the thumb image for.
+ \return fanart or spoiler overlay if item is an unwatched episode, thumb art otherwise.
+ */
+ std::string GetThumbHideIfUnwatched(const CFileItem* item) const;
+
+ // Gets the .tbn file associated with this item
+ std::string GetTBNFile() const;
+ // Gets the folder image associated with this item (defaults to folder.jpg)
+ std::string GetFolderThumb(const std::string &folderJPG = "folder.jpg") const;
+ // Gets the correct movie title
+ std::string GetMovieName(bool bUseFolderNames = false) const;
+
+ /*! \brief Find the base movie path (i.e. the item the user expects us to use to lookup the movie)
+ For folder items, with "use foldernames for lookups" it returns the folder.
+ Regardless of settings, for VIDEO_TS/BDMV it returns the parent of the VIDEO_TS/BDMV folder (if present)
+
+ \param useFolderNames whether we're using foldernames for lookups
+ \return the base movie folder
+ */
+ std::string GetBaseMoviePath(bool useFolderNames) const;
+
+ // Gets the user thumb, if it exists
+ std::string GetUserMusicThumb(bool alwaysCheckRemote = false, bool fallbackToFolder = false) const;
+
+ /*! \brief Get the path where we expect local metadata to reside.
+ For a folder, this is just the existing path (eg tvshow folder)
+ For a file, this is the parent path, with exceptions made for VIDEO_TS and BDMV files
+
+ Three cases are handled:
+
+ /foo/bar/movie_name/file_name -> /foo/bar/movie_name/
+ /foo/bar/movie_name/VIDEO_TS/file_name -> /foo/bar/movie_name/
+ /foo/bar/movie_name/BDMV/file_name -> /foo/bar/movie_name/
+
+ \sa URIUtils::GetParentPath
+ */
+ std::string GetLocalMetadataPath() const;
+
+ // finds a matching local trailer file
+ std::string FindTrailer() const;
+
+ bool LoadMusicTag();
+ bool LoadGameTag();
+
+ /*! \brief Load detailed data for an item constructed with only a path and a folder flag
+ Fills item's video info tag, sets item properties.
+
+ \return true on success, false otherwise.
+ */
+ bool LoadDetails();
+
+ /* Returns the content type of this item if known */
+ const std::string& GetMimeType() const { return m_mimetype; }
+
+ /* sets the mime-type if known beforehand */
+ void SetMimeType(const std::string& mimetype) { m_mimetype = mimetype; } ;
+
+ /*! \brief Resolve the MIME type based on file extension or a web lookup
+ If m_mimetype is already set (non-empty), this function has no effect. For
+ http:// and shout:// streams, this will query the stream (blocking operation).
+ Set lookup=false to skip any internet lookups and always return immediately.
+ */
+ void FillInMimeType(bool lookup = true);
+
+ /*!
+ \brief Some sources do not support HTTP HEAD request to determine i.e. mime type
+ \return false if HEAD requests have to be avoided
+ */
+ bool ContentLookup() { return m_doContentLookup; }
+
+ /*!
+ \brief (Re)set the mime-type for internet files if allowed (m_doContentLookup)
+ Some sources do not support HTTP HEAD request to determine i.e. mime type
+ */
+ void SetMimeTypeForInternetFile();
+
+ /*!
+ *\brief Lookup via HTTP HEAD request might not be needed, use this setter to
+ * disable ContentLookup.
+ */
+ void SetContentLookup(bool enable) { m_doContentLookup = enable; }
+
+ /* general extra info about the contents of the item, not for display */
+ void SetExtraInfo(const std::string& info) { m_extrainfo = info; }
+ const std::string& GetExtraInfo() const { return m_extrainfo; }
+
+ /*! \brief Update an item with information from another item
+ We take metadata information from the given item and supplement the current item
+ with that info. If tags exist in the new item we use the entire tag information.
+ Properties are appended, and labels, thumbnail and icon are updated if non-empty
+ in the given item.
+ \param item the item used to supplement information
+ \param replaceLabels whether to replace labels (defaults to true)
+ */
+ void UpdateInfo(const CFileItem &item, bool replaceLabels = true);
+
+ /*! \brief Merge an item with information from another item
+ We take metadata/art information from the given item and supplement the current
+ item with that info. If tags exist in the new item we only merge the missing
+ tag information. Properties are appended, and labels are updated if non-empty
+ in the given item.
+ */
+ void MergeInfo(const CFileItem &item);
+
+ bool IsSamePath(const CFileItem *item) const;
+
+ bool IsAlbum() const;
+
+ /*! \brief Sets details using the information from the CVideoInfoTag object
+ Sets the videoinfotag and uses its information to set the label and path.
+ \param video video details to use and set
+ */
+ void SetFromVideoInfoTag(const CVideoInfoTag &video);
+
+ /*! \brief Sets details using the information from the CMusicInfoTag object
+ Sets the musicinfotag and uses its information to set the label and path.
+ \param music music details to use and set
+ */
+ void SetFromMusicInfoTag(const MUSIC_INFO::CMusicInfoTag &music);
+
+ /*! \brief Sets details using the information from the CAlbum object
+ Sets the album in the music info tag and uses its information to set the
+ label and album-specific properties.
+ \param album album details to use and set
+ */
+ void SetFromAlbum(const CAlbum &album);
+ /*! \brief Sets details using the information from the CSong object
+ Sets the song in the music info tag and uses its information to set the
+ label, path, song-specific properties and artwork.
+ \param song song details to use and set
+ */
+ void SetFromSong(const CSong &song);
+
+ bool m_bIsShareOrDrive; ///< is this a root share/drive
+ int m_iDriveType; ///< If \e m_bIsShareOrDrive is \e true, use to get the share type. Types see: CMediaSource::m_iDriveType
+ CDateTime m_dateTime; ///< file creation date & time
+ int64_t m_dwSize; ///< file size (0 for folders)
+ std::string m_strDVDLabel;
+ std::string m_strTitle;
+ int m_iprogramCount;
+ int m_idepth;
+ int m_lStartPartNumber;
+ LockType m_iLockMode;
+ std::string m_strLockCode;
+ int m_iHasLock; // 0 - no lock 1 - lock, but unlocked 2 - locked
+ int m_iBadPwdCount;
+
+ void SetCueDocument(const CCueDocumentPtr& cuePtr);
+ void LoadEmbeddedCue();
+ bool HasCueDocument() const;
+ bool LoadTracksFromCueDocument(CFileItemList& scannedItems);
+private:
+ /*! \brief initialize all members of this class (not CGUIListItem members) to default values.
+ Called from constructors, and from Reset()
+ \sa Reset, CGUIListItem
+ */
+ void Initialize();
+
+ /*!
+ \brief Return the current resume point for this item.
+ \return The resume point.
+ */
+ CBookmark GetResumePoint() const;
+
+ /*!
+ \brief Fill item's music tag from given epg tag.
+ */
+ void FillMusicInfoTag(const std::shared_ptr<PVR::CPVREpgInfoTag>& tag);
+
+ std::string m_strPath; ///< complete path to item
+ std::string m_strDynPath;
+
+ SortSpecial m_specialSort;
+ bool m_bIsParentFolder;
+ bool m_bCanQueue;
+ bool m_bLabelPreformatted;
+ std::string m_mimetype;
+ std::string m_extrainfo;
+ bool m_doContentLookup;
+ MUSIC_INFO::CMusicInfoTag* m_musicInfoTag;
+ CVideoInfoTag* m_videoInfoTag;
+ std::shared_ptr<PVR::CPVREpgInfoTag> m_epgInfoTag;
+ std::shared_ptr<PVR::CPVREpgSearchFilter> m_epgSearchFilter;
+ std::shared_ptr<PVR::CPVRRecording> m_pvrRecordingInfoTag;
+ std::shared_ptr<PVR::CPVRTimerInfoTag> m_pvrTimerInfoTag;
+ std::shared_ptr<PVR::CPVRChannelGroupMember> m_pvrChannelGroupMemberInfoTag;
+ CPictureInfoTag* m_pictureInfoTag;
+ std::shared_ptr<const ADDON::IAddon> m_addonInfo;
+ KODI::GAME::CGameInfoTag* m_gameInfoTag;
+ EventPtr m_eventLogEntry;
+ bool m_bIsAlbum;
+ int64_t m_lStartOffset;
+ int64_t m_lEndOffset;
+
+ CCueDocumentPtr m_cueDocument;
+};
+
+/*!
+ \brief A shared pointer to CFileItem
+ \sa CFileItem
+ */
+typedef std::shared_ptr<CFileItem> CFileItemPtr;
+
+/*!
+ \brief A vector of pointer to CFileItem
+ \sa CFileItem
+ */
+typedef std::vector< CFileItemPtr > VECFILEITEMS;
+
+/*!
+ \brief Iterator for VECFILEITEMS
+ \sa CFileItemList
+ */
+typedef std::vector< CFileItemPtr >::iterator IVECFILEITEMS;
+
+/*!
+ \brief A map of pointers to CFileItem
+ \sa CFileItem
+ */
+typedef std::map<std::string, CFileItemPtr > MAPFILEITEMS;
+
+/*!
+ \brief Pair for MAPFILEITEMS
+ \sa MAPFILEITEMS
+ */
+typedef std::pair<std::string, CFileItemPtr > MAPFILEITEMSPAIR;
+
+typedef bool (*FILEITEMLISTCOMPARISONFUNC) (const CFileItemPtr &pItem1, const CFileItemPtr &pItem2);
+typedef void (*FILEITEMFILLFUNC) (CFileItemPtr &item);
+
+/*!
+ \brief Represents a list of files
+ \sa CFileItemList, CFileItem
+ */
+class CFileItemList : public CFileItem
+{
+public:
+ enum CACHE_TYPE { CACHE_NEVER = 0, CACHE_IF_SLOW, CACHE_ALWAYS };
+
+ CFileItemList();
+ explicit CFileItemList(const std::string& strPath);
+ ~CFileItemList() override;
+ void Archive(CArchive& ar) override;
+ CFileItemPtr operator[] (int iItem);
+ const CFileItemPtr operator[] (int iItem) const;
+ CFileItemPtr operator[] (const std::string& strPath);
+ const CFileItemPtr operator[] (const std::string& strPath) const;
+ void Clear();
+ void ClearItems();
+ void Add(CFileItemPtr item);
+ void Add(CFileItem&& item);
+ void AddFront(const CFileItemPtr &pItem, int itemPosition);
+ void Remove(CFileItem* pItem);
+ void Remove(int iItem);
+ CFileItemPtr Get(int iItem) const;
+ const VECFILEITEMS& GetList() const { return m_items; }
+ CFileItemPtr Get(const std::string& strPath) const;
+ int Size() const;
+ bool IsEmpty() const;
+ void Append(const CFileItemList& itemlist);
+ void Assign(const CFileItemList& itemlist, bool append = false);
+ bool Copy (const CFileItemList& item, bool copyItems = true);
+ void Reserve(size_t iCount);
+ void Sort(SortBy sortBy, SortOrder sortOrder, SortAttribute sortAttributes = SortAttributeNone);
+ /* \brief Sorts the items based on the given sorting options
+
+ In contrast to Sort (see above) this does not change the internal
+ state by storing the sorting method and order used and therefore
+ will always execute the sorting even if the list of items has
+ already been sorted with the same options before.
+ */
+ void Sort(SortDescription sortDescription);
+ void Randomize();
+ void FillInDefaultIcons();
+ int GetFolderCount() const;
+ int GetFileCount() const;
+ int GetSelectedCount() const;
+ int GetObjectCount() const;
+ void FilterCueItems();
+ void RemoveExtensions();
+ void SetIgnoreURLOptions(bool ignoreURLOptions);
+ void SetFastLookup(bool fastLookup);
+ bool Contains(const std::string& fileName) const;
+ bool GetFastLookup() const { return m_fastLookup; }
+
+ /*! \brief stack a CFileItemList
+ By default we stack all items (files and folders) in a CFileItemList
+ \param stackFiles whether to stack all items or just collapse folders (defaults to true)
+ \sa StackFiles,StackFolders
+ */
+ void Stack(bool stackFiles = true);
+
+ SortOrder GetSortOrder() const { return m_sortDescription.sortOrder; }
+ SortBy GetSortMethod() const { return m_sortDescription.sortBy; }
+ void SetSortOrder(SortOrder sortOrder) { m_sortDescription.sortOrder = sortOrder; }
+ void SetSortMethod(SortBy sortBy) { m_sortDescription.sortBy = sortBy; }
+
+ /*! \brief load a CFileItemList out of the cache
+
+ The file list may be cached based on which window we're viewing in, as different
+ windows will be listing different portions of the same URL (eg viewing music files
+ versus viewing video files)
+
+ \param windowID id of the window that's loading this list (defaults to 0)
+ \return true if we loaded from the cache, false otherwise.
+ \sa Save,RemoveDiscCache
+ */
+ bool Load(int windowID = 0);
+
+ /*! \brief save a CFileItemList to the cache
+
+ The file list may be cached based on which window we're viewing in, as different
+ windows will be listing different portions of the same URL (eg viewing music files
+ versus viewing video files)
+
+ \param windowID id of the window that's saving this list (defaults to 0)
+ \return true if successful, false otherwise.
+ \sa Load,RemoveDiscCache
+ */
+ bool Save(int windowID = 0);
+ void SetCacheToDisc(CACHE_TYPE cacheToDisc) { m_cacheToDisc = cacheToDisc; }
+ bool CacheToDiscAlways() const { return m_cacheToDisc == CACHE_ALWAYS; }
+ bool CacheToDiscIfSlow() const { return m_cacheToDisc == CACHE_IF_SLOW; }
+ /*! \brief remove a previously cached CFileItemList from the cache
+
+ The file list may be cached based on which window we're viewing in, as different
+ windows will be listing different portions of the same URL (eg viewing music files
+ versus viewing video files)
+
+ \param windowID id of the window whose cache we which to remove (defaults to 0)
+ \sa Save,Load
+ */
+ void RemoveDiscCache(int windowID = 0) const;
+ void RemoveDiscCache(const std::string& cachefile) const;
+ void RemoveDiscCacheCRC(const std::string& crc) const;
+ bool AlwaysCache() const;
+
+ void Swap(unsigned int item1, unsigned int item2);
+
+ /*! \brief Update an item in the item list
+ \param item the new item, which we match based on path to an existing item in the list
+ \return true if the item exists in the list (and was thus updated), false otherwise.
+ */
+ bool UpdateItem(const CFileItem *item);
+
+ void AddSortMethod(SortBy sortBy, int buttonLabel, const LABEL_MASKS &labelMasks, SortAttribute sortAttributes = SortAttributeNone);
+ void AddSortMethod(SortBy sortBy, SortAttribute sortAttributes, int buttonLabel, const LABEL_MASKS &labelMasks);
+ void AddSortMethod(SortDescription sortDescription, int buttonLabel, const LABEL_MASKS &labelMasks);
+ bool HasSortDetails() const { return m_sortDetails.size() != 0; }
+ const std::vector<GUIViewSortDetails> &GetSortDetails() const { return m_sortDetails; }
+
+ /*! \brief Specify whether this list should be sorted with folders separate from files
+ By default we sort with folders listed (and sorted separately) except for those sort modes
+ which should be explicitly sorted with folders interleaved with files (eg SORT_METHOD_FILES).
+ With this set the folder state will be ignored, allowing folders and files to sort interleaved.
+ \param sort whether to ignore the folder state.
+ */
+ void SetSortIgnoreFolders(bool sort) { m_sortIgnoreFolders = sort; }
+ bool GetReplaceListing() const { return m_replaceListing; }
+ void SetReplaceListing(bool replace);
+ void SetContent(const std::string& content) { m_content = content; }
+ const std::string& GetContent() const { return m_content; }
+
+ void ClearSortState();
+
+ VECFILEITEMS::iterator begin() { return m_items.begin(); }
+ VECFILEITEMS::iterator end() { return m_items.end(); }
+ VECFILEITEMS::iterator erase(VECFILEITEMS::iterator first, VECFILEITEMS::iterator last);
+ VECFILEITEMS::const_iterator begin() const { return m_items.begin(); }
+ VECFILEITEMS::const_iterator end() const { return m_items.end(); }
+ VECFILEITEMS::const_iterator cbegin() const { return m_items.cbegin(); }
+ VECFILEITEMS::const_iterator cend() const { return m_items.cend(); }
+ std::reverse_iterator<VECFILEITEMS::const_iterator> rbegin() const { return m_items.rbegin(); }
+ std::reverse_iterator<VECFILEITEMS::const_iterator> rend() const { return m_items.rend(); }
+
+private:
+ void Sort(FILEITEMLISTCOMPARISONFUNC func);
+ void FillSortFields(FILEITEMFILLFUNC func);
+ std::string GetDiscFileCache(int windowID) const;
+
+ /*!
+ \brief stack files in a CFileItemList
+ \sa Stack
+ */
+ void StackFiles();
+
+ /*!
+ \brief stack folders in a CFileItemList
+ \sa Stack
+ */
+ void StackFolders();
+
+ VECFILEITEMS m_items;
+ MAPFILEITEMS m_map;
+ bool m_ignoreURLOptions = false;
+ bool m_fastLookup = false;
+ SortDescription m_sortDescription;
+ bool m_sortIgnoreFolders = false;
+ CACHE_TYPE m_cacheToDisc = CACHE_IF_SLOW;
+ bool m_replaceListing = false;
+ std::string m_content;
+
+ std::vector<GUIViewSortDetails> m_sortDetails;
+
+ mutable CCriticalSection m_lock;
+};
diff --git a/xbmc/FileItemListModification.cpp b/xbmc/FileItemListModification.cpp
new file mode 100644
index 0000000..8496ee9
--- /dev/null
+++ b/xbmc/FileItemListModification.cpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013-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 "FileItemListModification.h"
+
+#include "music/windows/MusicFileItemListModifier.h"
+#include "playlists/SmartPlaylistFileItemListModifier.h"
+#include "video/windows/VideoFileItemListModifier.h"
+
+CFileItemListModification::CFileItemListModification()
+{
+ m_modifiers.insert(new CSmartPlaylistFileItemListModifier());
+ m_modifiers.insert(new CMusicFileItemListModifier());
+ m_modifiers.insert(new CVideoFileItemListModifier());
+}
+
+CFileItemListModification::~CFileItemListModification()
+{
+ for (std::set<IFileItemListModifier*>::const_iterator modifier = m_modifiers.begin(); modifier != m_modifiers.end(); ++modifier)
+ delete *modifier;
+
+ m_modifiers.clear();
+}
+
+CFileItemListModification& CFileItemListModification::GetInstance()
+{
+ static CFileItemListModification instance;
+ return instance;
+}
+
+bool CFileItemListModification::CanModify(const CFileItemList &items) const
+{
+ for (std::set<IFileItemListModifier*>::const_iterator modifier = m_modifiers.begin(); modifier != m_modifiers.end(); ++modifier)
+ {
+ if ((*modifier)->CanModify(items))
+ return true;
+ }
+
+ return false;
+}
+
+bool CFileItemListModification::Modify(CFileItemList &items) const
+{
+ bool result = false;
+ for (std::set<IFileItemListModifier*>::const_iterator modifier = m_modifiers.begin(); modifier != m_modifiers.end(); ++modifier)
+ result |= (*modifier)->Modify(items);
+
+ return result;
+}
diff --git a/xbmc/FileItemListModification.h b/xbmc/FileItemListModification.h
new file mode 100644
index 0000000..2abb07f
--- /dev/null
+++ b/xbmc/FileItemListModification.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013-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 "IFileItemListModifier.h"
+
+#include <set>
+
+class CFileItemListModification : public IFileItemListModifier
+{
+public:
+ ~CFileItemListModification() override;
+
+ static CFileItemListModification& GetInstance();
+
+ bool CanModify(const CFileItemList &items) const override;
+ bool Modify(CFileItemList &items) const override;
+
+private:
+ CFileItemListModification();
+ CFileItemListModification(const CFileItemListModification&) = delete;
+ CFileItemListModification& operator=(CFileItemListModification const&) = delete;
+
+ std::set<IFileItemListModifier*> m_modifiers;
+};
diff --git a/xbmc/GUIInfoManager.cpp b/xbmc/GUIInfoManager.cpp
new file mode 100644
index 0000000..dc4b7a8
--- /dev/null
+++ b/xbmc/GUIInfoManager.cpp
@@ -0,0 +1,11456 @@
+/*
+ * 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 "GUIInfoManager.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/DataCacheCore.h"
+#include "filesystem/File.h"
+#include "games/tags/GameInfoTag.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoHelper.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/WindowTranslator.h"
+#include "interfaces/AnnouncementManager.h"
+#include "interfaces/info/InfoExpression.h"
+#include "messaging/ApplicationMessenger.h"
+#include "playlists/PlayListTypes.h"
+#include "settings/SkinSettings.h"
+#include "utils/CharsetConverter.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <array>
+#include <charconv>
+#include <cmath>
+#include <functional>
+#include <iterator>
+#include <memory>
+#include <mutex>
+
+using namespace KODI::GUILIB;
+using namespace KODI::GUILIB::GUIINFO;
+using namespace INFO;
+
+bool InfoBoolComparator(const InfoPtr &right, const InfoPtr &left)
+{
+ return *right < *left;
+}
+
+CGUIInfoManager::CGUIInfoManager(void)
+: m_currentFile(new CFileItem),
+ m_bools(&InfoBoolComparator)
+{
+}
+
+CGUIInfoManager::~CGUIInfoManager(void)
+{
+ delete m_currentFile;
+}
+
+void CGUIInfoManager::Initialize()
+{
+ CServiceBroker::GetAppMessenger()->RegisterReceiver(this);
+}
+
+/// \brief Translates a string as given by the skin into an int that we use for more
+/// efficient retrieval of data. Can handle combined strings on the form
+/// Player.Caching + VideoPlayer.IsFullscreen (Logical and)
+/// Player.HasVideo | Player.HasAudio (Logical or)
+int CGUIInfoManager::TranslateString(const std::string &condition)
+{
+ // translate $LOCALIZE as required
+ std::string strCondition(CGUIInfoLabel::ReplaceLocalize(condition));
+ return TranslateSingleString(strCondition);
+}
+
+typedef struct
+{
+ const char *str;
+ int val;
+} infomap;
+
+/// \page modules__infolabels_boolean_conditions Infolabels and Boolean conditions
+/// \tableofcontents
+///
+/// \section modules__infolabels_boolean_conditions_Description Description
+/// Skins can use boolean conditions with the <b>\<visible\></b> tag or with condition
+/// attributes. Scripts can read boolean conditions with
+/// <b>xbmc.getCondVisibility(condition)</b>.
+///
+/// Skins can use infolabels with <b>$INFO[infolabel]</b> or the <b>\<info\></b> tag. Scripts
+/// can read infolabels with <b>xbmc.getInfoLabel('infolabel')</b>.
+///
+/// @todo [docs] Improve the description and create links for functions
+/// @todo [docs] Separate boolean conditions from infolabels
+/// @todo [docs] Order items alphabetically within subsections for a better search experience
+/// @todo [docs] Order subsections alphabetically
+/// @todo [docs] Use links instead of bold values for infolabels/bools
+/// so we can use a link to point users when providing help
+///
+
+
+/// \page modules__infolabels_boolean_conditions
+/// \section modules_list_infolabels_booleans List of Infolabels and Boolean conditions
+/// \subsection modules__infolabels_boolean_conditions_GlobalBools Global
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`true`</b>,
+/// \anchor Global_True
+/// _boolean_,
+/// @return Always evaluates to **true**.
+/// <p>
+/// }
+/// \table_row3{ <b>`false`</b>,
+/// \anchor Global_False
+/// _boolean_,
+/// @return Always evaluates to **false**.
+/// <p>
+/// }
+/// \table_row3{ <b>`yes`</b>,
+/// \anchor Global_Yes
+/// _boolean_,
+/// @return same as \link Global_True `true` \endlink.
+/// <p>
+/// }
+/// \table_row3{ <b>`no`</b>,
+/// \anchor Global_No
+/// _boolean_,
+/// @return same as \link Global_False `false` \endlink.
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Addon Addon
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Addon.SettingStr(addon_id\,setting_id)`</b>,
+/// \anchor Addon_SettingString
+/// _string_,
+/// @return The string value of the setting `setting_id` belonging to the addon with the id `addon_id`.
+/// @param addon_id - the id of the addon
+/// @param setting_id - the addon setting
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link Addon_SettingString `Addon.SettingStr(addon_id\,setting_id)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Addon.SettingBool(addon_id\,setting_id)`</b>,
+/// \anchor Addon_SettingBool
+/// _boolean_,
+/// @return **True** if the setting `setting_id` belonging to the addon with the id `addon_id` is **True**\, **False** otherwise.
+/// @note The provided setting with `setting_id` must be a boolean setting type. Otherwise it will return the boolean info
+/// default value (which is **False**).
+/// @param addon_id - the id of the addon
+/// @param setting_id - the addon setting
+/// <p><hr>
+/// @skinning_v20 **[New Boolean Condition]** \link Addon_SettingBool `Addon.SettingBool(addon_id\,setting_id)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Addon.SettingInt(addon_id\,setting_id)`</b>,
+/// \anchor Addon_SettingInt
+/// _integer_,
+/// @return The integer value of the setting `setting_id` belong to the addon with the id `addon_id`.
+/// @note The provided setting with `setting_id` must be an integer setting type. Otherwise it will return the integer info
+/// default value (which is 0).
+/// @param addon_id - the id of the addon
+/// @param setting_id - the addon setting
+/// <p><hr>
+/// @skinning_v20 **[New Integer Info]** \link Addon_SettingInt `Addon.SettingInt(addon_id\,setting_id)`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap addons[] = {
+ {"settingstr", ADDON_SETTING_STRING},
+ {"settingbool", ADDON_SETTING_BOOL},
+ {"settingint", ADDON_SETTING_INT},
+};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_String String
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`String.IsEmpty(info)`</b>,
+/// \anchor String_IsEmpty
+/// _boolean_,
+/// @return **True** if the info is empty.
+/// @param info - infolabel
+/// @note **Example of info:** \link ListItem_Title `ListItem.Title` \endlink \,
+/// \link ListItem_Genre `ListItem.Genre` \endlink.
+/// Please note that string can also be a `$LOCALIZE[]`.
+/// Also note that in a panelview or similar this only works on the focused item
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link String_IsEmpty `String.IsEmpty(info)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`String.IsEqual(info\,string)`</b>,
+/// \anchor String_IsEqual
+/// _boolean_,
+/// @return **True** if the info is equal to the given string.
+/// @param info - infolabel
+/// @param string - comparison string
+/// @note **Example of info:** \link ListItem_Title `ListItem.Title` \endlink \,
+/// \link ListItem_Genre `ListItem.Genre` \endlink.
+/// Please note that string can also be a `$LOCALIZE[]`.
+/// Also note that in a panelview or similar this only works on the focused item
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link String_IsEqual `String.IsEqual(info\,string)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`String.StartsWith(info\,substring)`</b>,
+/// \anchor String_StartsWith
+/// _boolean_,
+/// @return **True** if the info starts with the given substring.
+/// @param info - infolabel
+/// @param substring - substring to check
+/// @note **Example of info:** \link ListItem_Title `ListItem.Title` \endlink \,
+/// \link ListItem_Genre `ListItem.Genre` \endlink.
+/// Please note that string can also be a `$LOCALIZE[]`.
+/// Also note that in a panelview or similar this only works on the focused item
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link String_StartsWith `String.StartsWith(info\,substring)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`String.EndsWith(info\,substring)`</b>,
+/// \anchor String_EndsWith
+/// _boolean_,
+/// @return **True** if the info ends with the given substring.
+/// @param info - infolabel
+/// @param substring - substring to check
+/// @note **Example of info:** \link ListItem_Title `ListItem.Title` \endlink \,
+/// \link ListItem_Genre `ListItem.Genre` \endlink.
+/// Please note that string can also be a `$LOCALIZE[]`.
+/// Also note that in a panelview or similar this only works on the focused item
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link String_EndsWith `String.EndsWith(info\,substring)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`String.Contains(info\,substring)`</b>,
+/// \anchor String_Contains
+/// _boolean_,
+/// @return **True** if the info contains the given substring.
+/// @param info - infolabel
+/// @param substring - substring to check
+/// @note **Example of info:** \link ListItem_Title `ListItem.Title` \endlink \,
+/// \link ListItem_Genre `ListItem.Genre` \endlink.
+/// Please note that string can also be a `$LOCALIZE[]`.
+/// Also note that in a panelview or similar this only works on the focused item
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link String_Contains `String.Contains(info\,substring)`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+
+
+const infomap string_bools[] = {{ "isempty", STRING_IS_EMPTY },
+ { "isequal", STRING_IS_EQUAL },
+ { "startswith", STRING_STARTS_WITH },
+ { "endswith", STRING_ENDS_WITH },
+ { "contains", STRING_CONTAINS }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Integer Integer
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Integer.ValueOf(number)`</b>,
+/// \anchor Integer_ValueOf
+/// _integer_,
+/// @return An integer info label that represents the provided number
+/// @param number - the number to compute
+/// @note **Example:** `Integer.ValueOf(4)` will be evaluated to 4.
+/// @note Will return -1 if not able to convert the provided value to an integer. **Example**: `Integer.ValueOf(some string)` will evaluate to -1
+/// as the provided argument is not an integer.
+/// <p><hr>
+/// @skinning_v20 **[New InfoLabel]** \link Integer_ValueOf `Integer.ValueOf(number)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Integer.IsEqual(info\,number)`</b>,
+/// \anchor Integer_IsEqual
+/// _boolean_,
+/// @return **True** if the value of the infolabel is equal to the supplied number.
+/// @param info - infolabel
+/// @param number - number or integer infolabel to compare
+/// @note **Example:** `Integer.IsEqual(ListItem.Year\,2000)`
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Integer_IsEqual `Integer.IsEqual(info\,number)`\endlink
+/// @skinning_v20 \link Integer_IsEqual `Integer.IsEqual(info\,number)`\endlink now supports comparisons against other integer infos
+/// and not just fixed number values.
+/// <p>
+/// }
+/// \table_row3{ <b>`Integer.IsGreater(info\,number)`</b>,
+/// \anchor Integer_IsGreater
+/// _boolean_,
+/// @return **True** if the value of the infolabel is greater than to the supplied number.
+/// @param info - infolabel
+/// @param number - number or integer infolabel to compare
+/// @note **Example:** `Integer.IsGreater(ListItem.Year\,2000)`
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Integer_IsGreater `Integer.IsGreater(info\,number)`\endlink
+/// @skinning_v20 \link Integer_IsGreater `Integer.IsGreater(info\,number)`\endlink now supports comparisons against other integer infos
+/// and not just fixed number values.
+/// <p>
+/// }
+/// \table_row3{ <b>`Integer.IsGreaterOrEqual(info\,number)`</b>,
+/// \anchor Integer_IsGreaterOrEqual
+/// _boolean_,
+/// @return **True** if the value of the infolabel is greater or equal to the supplied number.
+/// @param info - infolabel
+/// @param number - number or integer infolabel to compare
+/// @note **Example:** `Integer.IsGreaterOrEqual(ListItem.Year\,2000)`
+/// @note **Example2:** `Integer.IsGreaterOrEqual(Container(x).ListItem(1).Year\,Container(x).ListItem(2).Year)`
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Integer_IsGreaterOrEqual `Integer.IsGreaterOrEqual(info\,number)`\endlink
+/// @skinning_v20 \link Integer_IsGreaterOrEqual `Integer.IsGreaterOrEqual(info\,number)`\endlink now supports comparisons against other integer infos
+/// and not just fixed number values.
+/// <p>
+/// }
+/// \table_row3{ <b>`Integer.IsLess(info\,number)`</b>,
+/// \anchor Integer_IsLess
+/// _boolean_,
+/// @return **True** if the value of the infolabel is less than the supplied number.
+/// @param info - infolabel
+/// @param number - number or integer infolabel to compare
+/// @note **Example:** `Integer.IsLess(ListItem.Year\,2000)`
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Integer_IsLess `Integer.IsLess(info\,number)`\endlink
+/// @skinning_v20 \link Integer_IsLess `Integer.IsLess(info\,number)`\endlink now supports comparisons against other integer infos
+/// and not just fixed number values.
+/// <p>
+/// }
+/// \table_row3{ <b>`Integer.IsLessOrEqual(info\,number)`</b>,
+/// \anchor Integer_IsLessOrEqual
+/// _boolean_,
+/// @return **True** if the value of the infolabel is less or equal to the supplied number.
+/// @param info - infolabel
+/// @param number - number or integer infolabel to compare
+/// @note **Example:** `Integer.IsLessOrEqual(ListItem.Year\,2000)`
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Integer_IsLessOrEqual `Integer.IsLessOrEqual(info\,number)`\endlink
+/// @skinning_v20 \link Integer_IsLessOrEqual `Integer.IsLessOrEqual(info\,number)`\endlink now supports comparisons against other integer infos
+/// and not just fixed number values.
+/// <p>
+/// }
+/// \table_row3{ <b>`Integer.IsEven(info)`</b>,
+/// \anchor Integer_IsEven
+/// _boolean_,
+/// @return **True** if the value of the infolabel is odd
+/// @param info - infolabel
+/// @note **Example:** `Integer.IsEven(ListItem.CurrentItem)`
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link Integer_IsEven `Integer.IsEven(info)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Integer.IsOdd(info)`</b>,
+/// \anchor Integer_IsOdd
+/// _boolean_,
+/// @return **True** if the value of the infolabel is odd
+/// @param info - infolabel
+/// @note **Example:** `Integer.IsOdd(ListItem.CurrentItem)`
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link Integer_IsOdd `Integer.IsOdd(info)`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+
+const infomap integer_bools[] = {{ "isequal", INTEGER_IS_EQUAL },
+ { "isgreater", INTEGER_GREATER_THAN },
+ { "isgreaterorequal", INTEGER_GREATER_OR_EQUAL },
+ { "isless", INTEGER_LESS_THAN },
+ { "islessorequal", INTEGER_LESS_OR_EQUAL },
+ { "iseven", INTEGER_EVEN },
+ { "isodd", INTEGER_ODD }};
+
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Player Player
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Player.HasAudio`</b>,
+/// \anchor Player_HasAudio
+/// _boolean_,
+/// @return **True** if the player has an audio file.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.HasGame`</b>,
+/// \anchor Player_HasGame
+/// _boolean_,
+/// @return **True** if the player has a game file (RETROPLAYER).
+/// <p><hr>
+/// @skinning_v18 **[New Boolean Condition]** \link Player_HasGame `Player.HasGame`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.HasMedia`</b>,
+/// \anchor Player_HasMedia
+/// _boolean_,
+/// @return **True** if the player has an audio or video file.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.HasVideo`</b>,
+/// \anchor Player_HasVideo
+/// _boolean_,
+/// @return **True** if the player has a video file.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Paused`</b>,
+/// \anchor Player_Paused
+/// _boolean_,
+/// @return **True** if the player is paused.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Playing`</b>,
+/// \anchor Player_Playing
+/// _boolean_,
+/// @return **True** if the player is currently playing (i.e. not ffwding\,
+/// rewinding or paused.)
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Rewinding`</b>,
+/// \anchor Player_Rewinding
+/// _boolean_,
+/// @return **True** if the player is rewinding.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Rewinding2x`</b>,
+/// \anchor Player_Rewinding2x
+/// _boolean_,
+/// @return **True** if the player is rewinding at 2x.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Rewinding4x`</b>,
+/// \anchor Player_Rewinding4x
+/// _boolean_,
+/// @return **True** if the player is rewinding at 4x.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Rewinding8x`</b>,
+/// \anchor Player_Rewinding8x
+/// _boolean_,
+/// @return **True** if the player is rewinding at 8x.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Rewinding16x`</b>,
+/// \anchor Player_Rewinding16x
+/// _boolean_,
+/// @return **True** if the player is rewinding at 16x.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Rewinding32x`</b>,
+/// \anchor Player_Rewinding32x
+/// _boolean_,
+/// @return **True** if the player is rewinding at 32x.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Forwarding`</b>,
+/// \anchor Player_Forwarding
+/// _boolean_,
+/// @return **True** if the player is fast forwarding.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Forwarding2x`</b>,
+/// \anchor Player_Forwarding2x
+/// _boolean_,
+/// @return **True** if the player is fast forwarding at 2x.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Forwarding4x`</b>,
+/// \anchor Player_Forwarding4x
+/// _boolean_,
+/// @return **True** if the player is fast forwarding at 4x.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Forwarding8x`</b>,
+/// \anchor Player_Forwarding8x
+/// _boolean_,
+/// @return **True** if the player is fast forwarding at 8x.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Forwarding16x`</b>,
+/// \anchor Player_Forwarding16x
+/// _boolean_,
+/// @return **True** if the player is fast forwarding at 16x.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Forwarding32x`</b>,
+/// \anchor Player_Forwarding32x
+/// _boolean_,
+/// @return **True** if the player is fast forwarding at 32x.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Caching`</b>,
+/// \anchor Player_Caching
+/// _boolean_,
+/// @return **True** if the player is current re-caching data (internet based
+/// video playback).
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.DisplayAfterSeek`</b>,
+/// \anchor Player_DisplayAfterSeek
+/// _boolean_,
+/// @return **True** for the first 2.5 seconds after a seek.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Seekbar`</b>,
+/// \anchor Player_Seekbar
+/// _integer_,
+/// @return The percentage of one seek to other position.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Seeking`</b>,
+/// \anchor Player_Seeking
+/// _boolean_,
+/// @return **True** if a seek is in progress.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.ShowTime`</b>,
+/// \anchor Player_ShowTime
+/// _boolean_,
+/// @return **True** if the user has requested the time to show (occurs in video
+/// fullscreen).
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.ShowInfo`</b>,
+/// \anchor Player_ShowInfo
+/// _boolean_,
+/// @return **True** if the user has requested the song info to show (occurs in
+/// visualisation fullscreen and slideshow).
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Title`</b>,
+/// \anchor Player_Title
+/// _string_,
+/// @return The Musicplayer title for audio and the Videoplayer title for
+/// video.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.offset(number).Title`</b>,
+/// \anchor Player_Offset_Title
+/// _string_,
+/// @return The title of audio or video which has an offset `number` with respect to the currently playing item.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link Player_Offset_Title `Player.offset(number).Title`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.position(number).Title`</b>,
+/// \anchor Player_Position_Title
+/// _string_,
+/// @return The title of the audio or video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link Player_Position_Title `Player.position(number).Title`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Muted`</b>,
+/// \anchor Player_Muted
+/// _boolean_,
+/// @return **True** if the volume is muted.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.HasDuration`</b>,
+/// \anchor Player_HasDuration
+/// _boolean_,
+/// @return **True** if Media is not a true stream.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Passthrough`</b>,
+/// \anchor Player_Passthrough
+/// _boolean_,
+/// @return **True** if the player is using audio passthrough.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.CacheLevel`</b>,
+/// \anchor Player_CacheLevel
+/// _string_,
+/// @return The used cache level as a string with an integer number.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Progress`</b>,
+/// \anchor Player_Progress
+/// _integer_ / _string_,
+/// @return The progress position as percentage.
+/// <p><hr>
+/// @skinning_v19 \link Player_Progress `Player.Progress`\endlink infolabel
+/// also exposed as a string.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.ProgressCache`</b>,
+/// \anchor Player_ProgressCache
+/// _integer_ / _string_,
+/// @return How much of the file is cached above current play percentage
+/// <p><hr>
+/// @skinning_v19 \link Player_ProgressCache `Player.ProgressCache`\endlink
+/// infolabel also exposed as a string.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Volume`</b>,
+/// \anchor Player_Volume
+/// _string_,
+/// @return The current player volume with the format `%2.1f` dB
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.SubtitleDelay`</b>,
+/// \anchor Player_SubtitleDelay
+/// _string_,
+/// @return The used subtitle delay with the format `%2.3f` s
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.AudioDelay`</b>,
+/// \anchor Player_AudioDelay
+/// _string_,
+/// @return The used audio delay with the format `%2.3f` s
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Chapter`</b>,
+/// \anchor Player_Chapter
+/// _integer_,
+/// @return The current chapter of current playing media.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.ChapterCount`</b>,
+/// \anchor Player_ChapterCount
+/// _integer_,
+/// @return The total number of chapters of current playing media.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.ChapterName`</b>,
+/// \anchor Player_ChapterName
+/// _string_,
+/// @return The name of currently used chapter if available.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Folderpath`</b>,
+/// \anchor Player_Folderpath
+/// _string_,
+/// @return The full path of the currently playing song or movie
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.offset(number).Folderpath`</b>,
+/// \anchor Player_Offset_Folderpath
+/// _string_,
+/// @return The full path of the audio or video file which has an offset `number` with respect to the currently playing item.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link Player_Offset_Folderpath `Player.offset(number).Folderpath`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.position(number).Folderpath`</b>,
+/// \anchor Player_Position_Folderpath
+/// _string_,
+/// @return The full path of the audio or video file which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link Player_Position_Folderpath `Player.position(number).Folderpath`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.FilenameAndPath`</b>,
+/// \anchor Player_FilenameAndPath
+/// _string_,
+/// @return The full path with filename of the currently
+/// playing song or movie
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.offset(number).FilenameAndPath`</b>,
+/// \anchor Player_Offset_FilenameAndPath
+/// _string_,
+/// @return The full path with filename of audio or video file which has an offset `number` with respect to the currently playing item.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link Player_Offset_FilenameAndPath `Player.offset(number).FilenameAndPath`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.position(number).FilenameAndPath`</b>,
+/// \anchor Player_Position_FilenameAndPath
+/// _string_,
+/// @return The full path with filename of the audio or video file which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link Player_Position_FilenameAndPath `Player.position(number).FilenameAndPath`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Filename`</b>,
+/// \anchor Player_Filename
+/// _string_,
+/// @return The filename of the currently playing media.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Player_Filename `Player.Filename`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.offset(number).Filename`</b>,
+/// \anchor Player_Offset_Filename
+/// _string_,
+/// @return The filename of audio or video file which has an offset `number` with respect to the currently playing item.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link Player_Offset_Filename `Player.offset(number).Filename`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.position(number).Filename`</b>,
+/// \anchor Player_Position_Filename
+/// _string_,
+/// @return The filename of the audio or video file which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link Player_Position_Filename `Player.position(number).Filename`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.IsInternetStream`</b>,
+/// \anchor Player_IsInternetStream
+/// _boolean_,
+/// @return **True** if the player is playing an internet stream.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.PauseEnabled`</b>,
+/// \anchor Player_PauseEnabled
+/// _boolean_,
+/// @return **True** if played stream is paused.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.SeekEnabled`</b>,
+/// \anchor Player_SeekEnabled
+/// _boolean_,
+/// @return **True** if seek on playing is enabled.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.ChannelPreviewActive`</b>,
+/// \anchor Player_ChannelPreviewActive
+/// _boolean_,
+/// @return **True** if PVR channel preview is active (used
+/// channel tag different from played tag)
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.TempoEnabled`</b>,
+/// \anchor Player_TempoEnabled
+/// _boolean_,
+/// @return **True** if player supports tempo (i.e. speed up/down normal
+/// playback speed)
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Player_TempoEnabled `Player.TempoEnabled`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.IsTempo`</b>,
+/// \anchor Player_IsTempo
+/// _boolean_,
+/// @return **True** if player has tempo (i.e. is playing with a playback speed higher or
+/// lower than normal playback speed)
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Player_IsTempo `Player.IsTempo`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.PlaySpeed`</b>,
+/// \anchor Player_PlaySpeed
+/// _string_,
+/// @return The player playback speed with the format `%1.2f` (1.00 means normal
+/// playback speed).
+/// @note For Tempo\, the default range is 0.80 - 1.50 (it can be changed
+/// in advanced settings). If \ref Player_PlaySpeed "Player.PlaySpeed" returns a value different from 1.00
+/// and \ref Player_IsTempo "Player.IsTempo" is false it means the player is in ff/rw mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.HasResolutions`</b>,
+/// \anchor Player_HasResolutions
+/// _boolean_,
+/// @return **True** if the player is allowed to switch resolution and refresh rate
+/// (i.e. if whitelist modes are configured in Kodi's System/Display settings)
+/// <p><hr>
+/// @skinning_v18 **[New Boolean Condition]** \link Player_HasResolutions `Player.HasResolutions`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.HasPrograms`</b>,
+/// \anchor Player_HasPrograms
+/// _boolean_,
+/// @return **True** if the media file being played has programs\, i.e. groups of streams.
+/// @note Ex: if a media file has multiple streams (quality\, channels\, etc) a program represents
+/// a particular stream combo.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.FrameAdvance`</b>,
+/// \anchor Player_FrameAdvance
+/// _boolean_,
+/// @return **True** if player is in frame advance mode.
+/// @note Skins should hide seek bar in this mode
+/// <p><hr>
+/// @skinning_v18 **[New Boolean Condition]** \link Player_FrameAdvance `Player.FrameAdvance`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Icon`</b>,
+/// \anchor Player_Icon
+/// _string_,
+/// @return The thumbnail of the currently playing item. If no thumbnail image exists\,
+/// the icon will be returned\, if available.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link Player_Icon `Player.Icon`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Cutlist`</b>,
+/// \anchor Player_Cutlist
+/// _string_,
+/// @return The cutlist of the currently playing item as csv in the format start1\,end1\,start2\,end2\,...
+/// Tokens must have values in the range from 0.0 to 100.0. end token must be less or equal than start token.
+/// <p>
+/// @deprecated \link Player_Cutlist `Player.Cutlist`\endlink is deprecated and will be removed in the next version.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link Player_Cutlist `Player.Cutlist`\endlink
+/// @skinning_v20 \link Player_Cutlist `Player.Cutlist`\endlink is deprecated\, use \link Player_Editlist `Player.Editlist`\endlink instead
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Editlist`</b>,
+/// \anchor Player_Editlist
+/// _string_,
+/// @return The editlist of the currently playing item as csv in the format start1\,end1\,start2\,end2\,...
+/// Tokens must have values in the range from 0.0 to 100.0. end token must be less or equal than start token.
+/// @note This infolabel does not contain EDL cuts. Edits start and end times are ajusted according to cuts
+/// defined for the media item.
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link Player_Editlist `Player.Editlist`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Cuts`</b>,
+/// \anchor Player_Cuts
+/// _string_,
+/// @return The EDL cut markers of the currently playing item as csv in the format start1\,end1\,start2\,end2\,...
+/// Tokens must have values in the range from 0.0 to 100.0. end token must be less or equal than start token.
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link Player_Cuts `Player.Cuts`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.SceneMarkers`</b>,
+/// \anchor Player_SceneMarkers
+/// _string_,
+/// @return The EDL scene markers of the currently playing item as csv in the format start1\,end1\,start2\,end2\,...
+/// Tokens must have values in the range from 0.0 to 100.0. end token must be less or equal than start token.
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link Player_SceneMarkers `Player.SceneMarkers`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.HasSceneMarkers`</b>,
+/// \anchor Player_HasSceneMarkers
+/// _boolean_,
+/// @return **True** if the item being played has scene markers\, **False** otherwise
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link Player_HasSceneMarkers `Player.HasSceneMarkers`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Chapters`</b>,
+/// \anchor Player_Chapters
+/// _string_,
+/// @return The chapters of the currently playing item as csv in the format start1\,end1\,start2\,end2\,...
+/// Tokens must have values in the range from 0.0 to 100.0. end token must be less or equal than start token.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link Player_Chapters `Player.Chapters`\endlink
+/// <p>
+/// }
+const infomap player_labels[] = {{"hasmedia", PLAYER_HAS_MEDIA},
+ {"hasaudio", PLAYER_HAS_AUDIO},
+ {"hasvideo", PLAYER_HAS_VIDEO},
+ {"hasgame", PLAYER_HAS_GAME},
+ {"playing", PLAYER_PLAYING},
+ {"paused", PLAYER_PAUSED},
+ {"rewinding", PLAYER_REWINDING},
+ {"forwarding", PLAYER_FORWARDING},
+ {"rewinding2x", PLAYER_REWINDING_2x},
+ {"rewinding4x", PLAYER_REWINDING_4x},
+ {"rewinding8x", PLAYER_REWINDING_8x},
+ {"rewinding16x", PLAYER_REWINDING_16x},
+ {"rewinding32x", PLAYER_REWINDING_32x},
+ {"forwarding2x", PLAYER_FORWARDING_2x},
+ {"forwarding4x", PLAYER_FORWARDING_4x},
+ {"forwarding8x", PLAYER_FORWARDING_8x},
+ {"forwarding16x", PLAYER_FORWARDING_16x},
+ {"forwarding32x", PLAYER_FORWARDING_32x},
+ {"caching", PLAYER_CACHING},
+ {"seekbar", PLAYER_SEEKBAR},
+ {"seeking", PLAYER_SEEKING},
+ {"showtime", PLAYER_SHOWTIME},
+ {"showinfo", PLAYER_SHOWINFO},
+ {"muted", PLAYER_MUTED},
+ {"hasduration", PLAYER_HASDURATION},
+ {"passthrough", PLAYER_PASSTHROUGH},
+ {"cachelevel", PLAYER_CACHELEVEL},
+ {"title", PLAYER_TITLE},
+ {"progress", PLAYER_PROGRESS},
+ {"progresscache", PLAYER_PROGRESS_CACHE},
+ {"volume", PLAYER_VOLUME},
+ {"subtitledelay", PLAYER_SUBTITLE_DELAY},
+ {"audiodelay", PLAYER_AUDIO_DELAY},
+ {"chapter", PLAYER_CHAPTER},
+ {"chaptercount", PLAYER_CHAPTERCOUNT},
+ {"chaptername", PLAYER_CHAPTERNAME},
+ {"folderpath", PLAYER_PATH},
+ {"filenameandpath", PLAYER_FILEPATH},
+ {"filename", PLAYER_FILENAME},
+ {"isinternetstream", PLAYER_ISINTERNETSTREAM},
+ {"pauseenabled", PLAYER_CAN_PAUSE},
+ {"seekenabled", PLAYER_CAN_SEEK},
+ {"channelpreviewactive", PLAYER_IS_CHANNEL_PREVIEW_ACTIVE},
+ {"tempoenabled", PLAYER_SUPPORTS_TEMPO},
+ {"istempo", PLAYER_IS_TEMPO},
+ {"playspeed", PLAYER_PLAYSPEED},
+ {"hasprograms", PLAYER_HAS_PROGRAMS},
+ {"hasresolutions", PLAYER_HAS_RESOLUTIONS},
+ {"frameadvance", PLAYER_FRAMEADVANCE},
+ {"icon", PLAYER_ICON},
+ {"cutlist", PLAYER_CUTLIST},
+ {"editlist", PLAYER_EDITLIST},
+ {"cuts", PLAYER_CUTS},
+ {"scenemarkers", PLAYER_SCENE_MARKERS},
+ {"hasscenemarkers", PLAYER_HAS_SCENE_MARKERS},
+ {"chapters", PLAYER_CHAPTERS}};
+
+/// \page modules__infolabels_boolean_conditions
+/// \table_row3{ <b>`Player.Art(type)`</b>,
+/// \anchor Player_Art_type
+/// _string_,
+/// @return The Image for the defined art type for the current playing ListItem.
+/// @param type - The art type. The type is defined by scripts and scrappers and can have any value.
+/// Common example values for type are:
+/// - fanart
+/// - thumb
+/// - poster
+/// - banner
+/// - clearlogo
+/// - tvshow.poster
+/// - tvshow.banner
+/// - etc
+/// @todo get a way of centralize all random art strings used in core so we can point users to them
+/// while still making it clear they can have any value.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.HasPerformedSeek(interval)`</b>,
+/// \anchor Player_HasPerformedSeek
+/// _boolean_,
+/// @return **True** if the Player has performed a seek operation in the last provided second `interval`\, **False** otherwise.
+/// @param interval - the time interval (in seconds)
+/// <p><hr>
+/// @skinning_v20 **[New Boolean Condition]** \link Player_HasPerformedSeek `Player.HasPerformedSeek(interval)`\endlink
+/// <p>
+/// }
+
+const infomap player_param[] = {{"art", PLAYER_ITEM_ART},
+ {"hasperformedseek", PLAYER_HASPERFORMEDSEEK}};
+
+/// \page modules__infolabels_boolean_conditions
+/// \table_row3{ <b>`Player.SeekTime`</b>,
+/// \anchor Player_SeekTime
+/// _string_,
+/// @return The time to which the user is seeking.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.SeekOffset([format])`</b>,
+/// \anchor Player_SeekOffset_format
+/// _string_,
+/// @return The seek offset after a seek press in a given format.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// @note **Example:** user presses BigStepForward\, player.seekoffset returns +10:00
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.SeekStepSize`</b>,
+/// \anchor Player_SeekStepSize
+/// _string_,
+/// @return The seek step size.
+/// <p>
+/// <hr>
+/// @skinning_v15 **[New Infolabel]** \link Player_SeekStepSize `Player.SeekStepSize`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.TimeRemaining([format])`</b>,
+/// \anchor Player_TimeRemaining_format
+/// _string_,
+/// @return The remaining time of current playing media in a given format.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.TimeSpeed`</b>,
+/// \anchor Player_TimeSpeed
+/// _string_,
+/// @return The time and the playspeed formatted: "1:23 (2x)".
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Time([format])`</b>,
+/// \anchor Player_Time_format
+/// _string_,
+/// @return The elapsed time of current playing media in a given format.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Duration([format])`</b>,
+/// \anchor Player_Duration_format
+/// _string_,
+/// @return The total duration of the current playing media in a given format.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.FinishTime([format])`</b>,
+/// \anchor Player_FinishTime_format
+/// _string_,
+/// @return The time at which the playing media will end (in a specified format).
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.StartTime([format])`</b>,
+/// \anchor Player_StartTime_format
+/// _string_,
+/// @return The time at which the playing media began (in a specified format).
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.SeekNumeric([format])`</b>,
+/// \anchor Player_SeekNumeric_format
+/// _string_,
+/// @return The time at which the playing media began (in a specified format).
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+const infomap player_times[] = {{ "seektime", PLAYER_SEEKTIME },
+ { "seekoffset", PLAYER_SEEKOFFSET },
+ { "seekstepsize", PLAYER_SEEKSTEPSIZE },
+ { "timeremaining", PLAYER_TIME_REMAINING },
+ { "timespeed", PLAYER_TIME_SPEED },
+ { "time", PLAYER_TIME },
+ { "duration", PLAYER_DURATION },
+ { "finishtime", PLAYER_FINISH_TIME },
+ { "starttime", PLAYER_START_TIME },
+ { "seeknumeric", PLAYER_SEEKNUMERIC } };
+
+
+/// \page modules__infolabels_boolean_conditions
+/// \table_row3{ <b>`Player.Process(videohwdecoder)`</b>,
+/// \anchor Player_Process_videohwdecoder
+/// _boolean_,
+/// @return **True** if the currently playing video is decoded in hardware.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Player_Process_videohwdecoder `Player.Process(videohwdecoder)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(videodecoder)`</b>,
+/// \anchor Player_Process_videodecoder
+/// _string_,
+/// @return The videodecoder name of the currently playing video.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Player_Process_videodecoder `Player.Process(videodecoder)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(deintmethod)`</b>,
+/// \anchor Player_Process_deintmethod
+/// _string_,
+/// @return The deinterlace method of the currently playing video.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Player_Process_deintmethod `Player.Process(deintmethod)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(pixformat)`</b>,
+/// \anchor Player_Process_pixformat
+/// _string_,
+/// @return The pixel format of the currently playing video.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Player_Process_pixformat `Player.Process(pixformat)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(videowidth)`</b>,
+/// \anchor Player_Process_videowidth
+/// _string_,
+/// @return The width of the currently playing video.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Player_Process_videowidth `Player.Process(videowidth)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(videoheight)`</b>,
+/// \anchor Player_Process_videoheight
+/// _string_,
+/// @return The width of the currently playing video.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Player_Process_videoheight `Player.Process(videoheight)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(videoscantype)`</b>,
+/// \anchor Player_Process_videoscantype
+/// _string_,
+/// @return The scan type identifier of the currently playing video **p** (for progressive) or **i** (for interlaced).
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link Player_Process_videoscantype `Player.Process(videoscantype)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(videofps)`</b>,
+/// \anchor Player_Process_videofps
+/// _string_,
+/// @return The video framerate of the currently playing video.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Player_Process_videofps `Player.Process(videofps)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(videodar)`</b>,
+/// \anchor Player_Process_videodar
+/// _string_,
+/// @return The display aspect ratio of the currently playing video.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Player_Process_videodar `Player.Process(videodar)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(audiodecoder)`</b>,
+/// \anchor Player_Process_audiodecoder
+/// _string_,
+/// @return The audiodecoder name of the currently playing item.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Player_Process_videodar `Player.Process(audiodecoder)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(audiochannels)`</b>,
+/// \anchor Player_Process_audiochannels
+/// _string_,
+/// @return The audiodecoder name of the currently playing item.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Player_Process_audiochannels `Player.Process(audiochannels)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(audiosamplerate)`</b>,
+/// \anchor Player_Process_audiosamplerate
+/// _string_,
+/// @return The samplerate of the currently playing item.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Player_Process_audiosamplerate `Player.Process(audiosamplerate)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Player.Process(audiobitspersample)`</b>,
+/// \anchor Player_Process_audiobitspersample
+/// _string_,
+/// @return The bits per sample of the currently playing item.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Player_Process_audiobitspersample `Player.Process(audiobitspersample)`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+
+const infomap player_process[] = {{"videodecoder", PLAYER_PROCESS_VIDEODECODER},
+ {"deintmethod", PLAYER_PROCESS_DEINTMETHOD},
+ {"pixformat", PLAYER_PROCESS_PIXELFORMAT},
+ {"videowidth", PLAYER_PROCESS_VIDEOWIDTH},
+ {"videoheight", PLAYER_PROCESS_VIDEOHEIGHT},
+ {"videofps", PLAYER_PROCESS_VIDEOFPS},
+ {"videodar", PLAYER_PROCESS_VIDEODAR},
+ {"videohwdecoder", PLAYER_PROCESS_VIDEOHWDECODER},
+ {"audiodecoder", PLAYER_PROCESS_AUDIODECODER},
+ {"audiochannels", PLAYER_PROCESS_AUDIOCHANNELS},
+ {"audiosamplerate", PLAYER_PROCESS_AUDIOSAMPLERATE},
+ {"audiobitspersample", PLAYER_PROCESS_AUDIOBITSPERSAMPLE},
+ {"videoscantype", PLAYER_PROCESS_VIDEOSCANTYPE}};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Weather Weather
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Weather.IsFetched`</b>,
+/// \anchor Weather_IsFetched
+/// _boolean_,
+/// @return **True** if the weather data has been downloaded.
+/// <p>
+/// }
+/// \table_row3{ <b>`Weather.Conditions`</b>,
+/// \anchor Weather_Conditions
+/// _string_,
+/// @return The current weather conditions as textual description.
+/// @note This is looked up in a background process.
+/// <p>
+/// }
+/// \table_row3{ <b>`Weather.ConditionsIcon`</b>,
+/// \anchor Weather_ConditionsIcon
+/// _string_,
+/// @return The current weather conditions as an icon.
+/// @note This is looked up in a background process.
+/// <p>
+/// }
+/// \table_row3{ <b>`Weather.Temperature`</b>,
+/// \anchor Weather_Temperature
+/// _string_,
+/// @return The current weather temperature.
+/// <p>
+/// }
+/// \table_row3{ <b>`Weather.Location`</b>,
+/// \anchor Weather_Location
+/// _string_,
+/// @return The city/town which the above two items are for.
+/// <p>
+/// }
+/// \table_row3{ <b>`Weather.Fanartcode`</b>,
+/// \anchor Weather_fanartcode
+/// _string_,
+/// @return The current weather fanartcode.
+/// <p>
+/// }
+/// \table_row3{ <b>`Weather.Plugin`</b>,
+/// \anchor Weather_plugin
+/// _string_,
+/// @return The current weather plugin.
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap weather[] = {{ "isfetched", WEATHER_IS_FETCHED },
+ { "conditions", WEATHER_CONDITIONS_TEXT }, // labels from here
+ { "temperature", WEATHER_TEMPERATURE },
+ { "location", WEATHER_LOCATION },
+ { "fanartcode", WEATHER_FANART_CODE },
+ { "plugin", WEATHER_PLUGIN },
+ { "conditionsicon", WEATHER_CONDITIONS_ICON }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_System System
+/// @todo some values are hardcoded in the middle of the code - refactor to make it easier to track
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`System.AlarmLessOrEqual(alarmname\,seconds)`</b>,
+/// \anchor System_AlarmLessOrEqual
+/// _boolean_,
+/// @return **True** if the alarm with `alarmname` has less or equal to `seconds` left.
+/// @param alarmname - The name of the alarm. It can be one of the following:
+/// - shutdowntimer
+/// @param seconds - Time in seconds to compare with the alarm trigger event
+/// @note **Example:** `System.Alarmlessorequal(shutdowntimer\,119)`\,
+/// will return true when the shutdowntimer has less then 2 minutes
+/// left.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.HasNetwork`</b>,
+/// \anchor System_HasNetwork
+/// _boolean_,
+/// @return **True** if the Kodi host has a network available.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.HasMediadvd`</b>,
+/// \anchor System_HasMediadvd
+/// _boolean_,
+/// @return **True** if there is a CD or DVD in the DVD-ROM drive.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.HasMediaAudioCD`</b>,
+/// \anchor System_HasMediaAudioCD
+/// _boolean_,
+/// @return **True** if there is an audio CD in the optical drive. **False** if no drive
+/// available\, empty drive or other medium.
+/// <p><hr>
+/// @skinning_v18 **[New Boolean Condition]** \link System_HasMediaAudioCD
+/// `System.HasMediaAudioCD` \endlink <p>
+/// }
+/// \table_row3{ <b>`System.DVDReady`</b>,
+/// \anchor System_DVDReady
+/// _boolean_,
+/// @return **True** if the disc is ready to use.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.TrayOpen`</b>,
+/// \anchor System_TrayOpen
+/// _boolean_,
+/// @return **True** if the disc tray is open.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.HasLocks`</b>,
+/// \anchor System_HasLocks
+/// _boolean_,
+/// @return **True** if the system has an active lock mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.IsMaster`</b>,
+/// \anchor System_IsMaster
+/// _boolean_,
+/// @return **True** if the system is in master mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.ShowExitButton`</b>,
+/// \anchor System_ShowExitButton
+/// _boolean_,
+/// @return **True** if the exit button should be shown (configurable via advanced settings).
+/// <p>
+/// }
+/// \table_row3{ <b>`System.DPMSActive`</b>,
+/// \anchor System_DPMSActive
+/// _boolean_,
+/// @return **True** if DPMS (VESA Display Power Management Signaling) mode is active.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.IsStandalone`</b>,
+/// \anchor System_IsStandalone
+/// _boolean_,
+/// @return **True** if Kodi is running in standalone mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.IsFullscreen`</b>,
+/// \anchor System_IsFullscreen
+/// _boolean_,
+/// @return **True** if Kodi is running fullscreen.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.LoggedOn`</b>,
+/// \anchor System_LoggedOn
+/// _boolean_,
+/// @return **True** if a user is currently logged on under a profile.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.HasLoginScreen`</b>,
+/// \anchor System_HasLoginScreen
+/// _boolean_,
+/// @return **True** if the profile login screen is enabled.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.HasPVR`</b>,
+/// \anchor System_HasPVR
+/// _boolean_,
+/// @return **True** if PVR is supported from Kodi.
+/// @note normally always true
+///
+/// }
+/// \table_row3{ <b>`System.HasPVRAddon`</b>,
+/// \anchor System_HasPVRAddon
+/// _boolean_,
+/// @return **True** if at least one pvr client addon is installed and enabled.
+/// @param id - addon id of the PVR addon
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link System_HasPVRAddon
+/// `System.HasPVRAddon`\endlink <p>
+/// }
+/// \table_row3{ <b>`System.HasCMS`</b>,
+/// \anchor System_HasCMS
+/// _boolean_,
+/// @return **True** if colour management is supported from Kodi.
+/// @note currently only supported for OpenGL
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link System_HasCMS `System.HasCMS`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`System.HasActiveModalDialog`</b>,
+/// \anchor System_HasActiveModalDialog
+/// _boolean_,
+/// @return **True** if a modal dialog is active.
+/// <p><hr>
+/// @skinning_v18 **[New Boolean Condition]** \link System_HasActiveModalDialog
+/// `System.HasActiveModalDialog`\endlink <p>
+/// }
+/// \table_row3{ <b>`System.HasVisibleModalDialog`</b>,
+/// \anchor System_HasVisibleModalDialog
+/// _boolean_,
+/// @return **True** if a modal dialog is visible.
+/// <p><hr>
+/// @skinning_v18 **[New Boolean Condition]** \link System_HasVisibleModalDialog
+/// `System.HasVisibleModalDialog`\endlink <p>
+/// }
+/// \table_row3{ <b>`System.Platform.Linux`</b>,
+/// \anchor System_PlatformLinux
+/// _boolean_,
+/// @return **True** if Kodi is running on a linux/unix based computer.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Platform.Windows`</b>,
+/// \anchor System_PlatformWindows
+/// _boolean_,
+/// @return **True** if Kodi is running on a windows based computer.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Platform.UWP`</b>,
+/// \anchor System_PlatformUWP
+/// _boolean_,
+/// @return **True** if Kodi is running on Universal Windows Platform (UWP).
+/// <p><hr>
+/// @skinning_v18 **[New Boolean Condition]** \link System_PlatformUWP
+/// `System.Platform.UWP`\endlink <p>
+/// }
+/// \table_row3{ <b>`System.Platform.OSX`</b>,
+/// \anchor System_PlatformOSX
+/// _boolean_,
+/// @return **True** if Kodi is running on an OSX based computer.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Platform.IOS`</b>,
+/// \anchor System_PlatformIOS
+/// _boolean_,
+/// @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_,
+/// @return **True** if Kodi is running on an OSX or IOS system.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Platform.Android`</b>,
+/// \anchor System_PlatformAndroid
+/// _boolean_,
+/// @return **True** if Kodi is running on an android device.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.CanPowerDown`</b>,
+/// \anchor System_CanPowerDown
+/// _boolean_,
+/// @return **True** if Kodi can powerdown the system.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.CanSuspend`</b>,
+/// \anchor System_CanSuspend
+/// _boolean_,
+/// @return **True** if Kodi can suspend the system.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.CanHibernate`</b>,
+/// \anchor System_CanHibernate
+/// _boolean_,
+/// @return **True** if Kodi can hibernate the system.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.HasHiddenInput`</b>,
+/// \anchor System_HasHiddenInput
+/// _boolean_,
+/// @return **True** when to osd keyboard/numeric dialog requests a
+/// password/pincode.
+/// <p><hr>
+/// @skinning_v16 **[New Boolean Condition]** \link System_HasHiddenInput
+/// `System.HasHiddenInput`\endlink <p>
+/// }
+/// \table_row3{ <b>`System.CanReboot`</b>,
+/// \anchor System_CanReboot
+/// _boolean_,
+/// @return **True** if Kodi can reboot the system.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.ScreenSaverActive`</b>,
+/// \anchor System_ScreenSaverActive
+/// _boolean_,
+/// @return **True** if ScreenSaver is active.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.IdleShutdownInhibited`</b>,
+/// \anchor System_IdleShutdownInhibited
+/// _boolean_,
+/// @return **True** when shutdown on idle is disabled.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.HasShutdown`</b>,
+/// \anchor System_HasShutdown
+/// _boolean_,
+/// @return **True** if Kodi can shutdown the system.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Time`</b>,
+/// \anchor System_Time
+/// _string_,
+/// @return The current time.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Time(format)`</b>,
+/// \anchor System_Time_format
+/// _string_,
+/// @return The current time in a specified format.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Time(startTime[\,endTime])`</b>,
+/// \anchor System_Time
+/// _boolean_,
+/// @return **True** if the current system time is >= `startTime` and < `endTime` (if defined).
+/// @param startTime - Start time
+/// @param endTime - [opt] End time
+/// <p>
+/// @note Time must be specified in the format HH:mm\, using
+/// a 24 hour clock.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Date`</b>,
+/// \anchor System_Date
+/// _string_,
+/// @return The current date.
+/// <p><hr>
+/// @skinning_v16 **[Infolabel Updated]** \link System_Date `System.Date`\endlink
+/// will now return the full day and month names. old: sat\, jul 18 2015
+/// new: saturday\, july 18 2015
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Date(format)`</b>,
+/// \anchor System_Date_format
+/// _string_,
+/// @return The current date using a specified format.
+/// @param format - the format for the date. It can be one of the following
+/// values:
+/// - **d** - day of month (1-31)
+/// - **dd** - day of month (01-31)
+/// - **ddd** - short day of the week Mon-Sun
+/// - **DDD** - long day of the week Monday-Sunday
+/// - **m** - month (1-12)
+/// - **mm** - month (01-12)
+/// - **mmm** - short month name Jan-Dec
+/// - **MMM** - long month name January-December
+/// - **yy** - 2-digit year
+/// - **yyyy** - 4-digit year
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Date(startDate[\,endDate])`</b>,
+/// \anchor System_Date
+/// _boolean_,
+/// @return **True** if the current system date is >= `startDate` and < `endDate` (if defined).
+/// @param startDate - The start date
+/// @param endDate - [opt] The end date
+/// @note Date must be specified in the format MM-DD or YY-MM-DD.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.AlarmPos`</b>,
+/// \anchor System_AlarmPos
+/// _string_,
+/// @return The shutdown Timer position.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.BatteryLevel`</b>,
+/// \anchor System_BatteryLevel
+/// _string_,
+/// @return The remaining battery level in range 0-100.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.FreeSpace`</b>,
+/// \anchor System_FreeSpace
+/// _string_,
+/// @return The total Freespace on the drive.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.UsedSpace`</b>,
+/// \anchor System_UsedSpace
+/// _string_,
+/// @return The total Usedspace on the drive.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.TotalSpace`</b>,
+/// \anchor System_TotalSpace
+/// _string_,
+/// @return The total space on the drive.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.UsedSpacePercent`</b>,
+/// \anchor System_UsedSpacePercent
+/// _string_,
+/// @return The total Usedspace Percent on the drive.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.FreeSpacePercent`</b>,
+/// \anchor System_FreeSpacePercent
+/// _string_,
+/// @return The total Freespace Percent on the drive.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.CPUTemperature`</b>,
+/// \anchor System_CPUTemperature
+/// _string_,
+/// @return The current CPU temperature.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.CpuUsage`</b>,
+/// \anchor System_CpuUsage
+/// _string_,
+/// @return The the cpu usage for each individual cpu core.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.GPUTemperature`</b>,
+/// \anchor System_GPUTemperature
+/// _string_,
+/// @return The current GPU temperature.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.FanSpeed`</b>,
+/// \anchor System_FanSpeed
+/// _string_,
+/// @return The current fan speed.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.BuildVersion`</b>,
+/// \anchor System_BuildVersion
+/// _string_,
+/// @return The version of build.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.BuildVersionShort`</b>,
+/// \anchor System_BuildVersionShort
+/// _string_,
+/// @return The shorter string with version of build.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.BuildDate`</b>,
+/// \anchor System_BuildDate
+/// _string_,
+/// @return The date of build.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.BuildVersionCode`</b>,
+/// \anchor System_BuildVersionCode
+/// _string_,
+/// @return The version code of build.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.BuildVersionGit`</b>,
+/// \anchor System_BuildVersionGit
+/// _string_,
+/// @return The git version of build.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.FriendlyName`</b>,
+/// \anchor System_FriendlyName
+/// _string_,
+/// @return The Kodi instance name.
+/// @note It will auto append (%hostname%) in case
+/// the device name was not changed. eg. "Kodi (htpc)"
+/// <p>
+/// }
+/// \table_row3{ <b>`System.FPS`</b>,
+/// \anchor System_FPS
+/// _string_,
+/// @return The current rendering speed (frames per second).
+/// <p>
+/// }
+/// \table_row3{ <b>`System.FreeMemory`</b>,
+/// \anchor System_FreeMemory
+/// _string_,
+/// @return The amount of free memory in Mb.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.ScreenMode`</b>,
+/// \anchor System_ScreenMode
+/// _string_,
+/// @return The screenmode (eg windowed / fullscreen).
+/// <p>
+/// }
+/// \table_row3{ <b>`System.ScreenWidth`</b>,
+/// \anchor System_ScreenWidth
+/// _string_,
+/// @return The width of screen in pixels.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.ScreenHeight`</b>,
+/// \anchor System_ScreenHeight
+/// _string_,
+/// @return The height of screen in pixels.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.StartupWindow`</b>,
+/// \anchor System_StartupWindow
+/// _string_,
+/// @return The Window Kodi will load on startup.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link System_StartupWindow `System.StartupWindow`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`System.CurrentWindow`</b>,
+/// \anchor System_CurrentWindow
+/// _string_,
+/// @return The current Window in use.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.CurrentControl`</b>,
+/// \anchor System_CurrentControl
+/// _string_,
+/// @return The current focused control
+/// <p>
+/// }
+/// \table_row3{ <b>`System.CurrentControlId`</b>,
+/// \anchor System_CurrentControlId
+/// _string_,
+/// @return The ID of the currently focused control.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.DVDLabel`</b>,
+/// \anchor System_DVDLabel
+/// _string_,
+/// @return the label of the disk in the DVD-ROM drive.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.KernelVersion`</b>,
+/// \anchor System_KernelVersion
+/// _string_,
+/// @return The System kernel version.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.OSVersionInfo`</b>,
+/// \anchor System_OSVersionInfo
+/// _string_,
+/// @return The system name + kernel version.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Uptime`</b>,
+/// \anchor System_Uptime
+/// _string_,
+/// @return The system current uptime.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.TotalUptime`</b>,
+/// \anchor System_TotalUptime
+/// _string_,
+/// @return The system total uptime.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.CpuFrequency`</b>,
+/// \anchor System_CpuFrequency
+/// _string_,
+/// @return The system cpu frequency.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.ScreenResolution`</b>,
+/// \anchor System_ScreenResolution
+/// _string_,
+/// @return The screen resolution.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.VideoEncoderInfo`</b>,
+/// \anchor System_VideoEncoderInfo
+/// _string_,
+/// @return The video encoder info.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.InternetState`</b>,
+/// \anchor System_InternetState
+/// _string_,
+/// @return The internet state: connected or not connected.
+/// @warning Do not use to check status in a pythonscript since it is threaded.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Language`</b>,
+/// \anchor System_Language
+/// _string_,
+/// @return the current language.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.ProfileName`</b>,
+/// \anchor System_ProfileName
+/// _string_,
+/// @return The user name of the currently logged in Kodi user
+/// <p>
+/// }
+/// \table_row3{ <b>`System.ProfileThumb`</b>,
+/// \anchor System_ProfileThumb
+/// _string_,
+/// @return The thumbnail image of the currently logged in Kodi user
+/// <p>
+/// }
+/// \table_row3{ <b>`System.ProfileCount`</b>,
+/// \anchor System_ProfileCount
+/// _string_,
+/// @return The number of defined profiles.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.ProfileAutoLogin`</b>,
+/// \anchor System_ProfileAutoLogin
+/// _string_,
+/// @return The profile Kodi will auto login to.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link System_ProfileAutoLogin
+/// `System.ProfileAutoLogin`\endlink <p>
+/// }
+/// \table_row3{ <b>`System.StereoscopicMode`</b>,
+/// \anchor System_StereoscopicMode
+/// _string_,
+/// @return The preferred stereoscopic mode.
+/// @note Configured in settings > video > playback).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link System_StereoscopicMode
+/// `System.StereoscopicMode`\endlink <p>
+/// }
+/// \table_row3{ <b>`System.TemperatureUnits`</b>,
+/// \anchor System_TemperatureUnits
+/// _string_,
+/// @return the Celsius or the Fahrenheit symbol.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Progressbar`</b>,
+/// \anchor System_Progressbar
+/// _string_,
+/// @return The percentage of the currently active progress.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.GetBool(boolean)`</b>,
+/// \anchor System_GetBool
+/// _string_,
+/// @return The value of any standard system boolean setting.
+/// @note Will not work with settings in advancedsettings.xml
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Memory(type)`</b>,
+/// \anchor System_Memory
+/// _string_,
+/// @return The memory value depending on the requested type.
+/// @param type - Can be one of the following:
+/// - <b>free</b>
+/// - <b>free.percent</b>
+/// - <b>used</b>
+/// - <b>used.percent</b>
+/// - <b>total</b>
+/// <p>
+/// }
+/// \table_row3{ <b>`System.AddonTitle(id)`</b>,
+/// \anchor System_AddonTitle
+/// _string_,
+/// @return The title of the addon with the given id
+/// @param id - the addon id
+/// <p>
+/// }
+/// \table_row3{ <b>`System.AddonVersion(id)`</b>,
+/// \anchor System_AddonVersion
+/// _string_,
+/// @return The version of the addon with the given id.
+/// @param id - the addon id
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link System_AddonVersion
+/// `System.AddonVersion(id)`\endlink <p>
+/// }
+/// \table_row3{ <b>`System.AddonIcon(id)`</b>,
+/// \anchor System_AddonVersion
+/// _string_,
+/// @return The icon of the addon with the given id.
+/// @param id - the addon id
+/// <p>
+/// }
+/// \table_row3{ <b>`System.AddonUpdateCount`</b>,
+/// \anchor System_AddonUpdateCount
+/// _string_,
+/// @return The number of available addon updates.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link System_AddonUpdateCount `
+/// System.AddonUpdateCount`\endlink <p>
+/// }
+/// \table_row3{ <b>`System.IdleTime(time)`</b>,
+/// \anchor System_IdleTime
+/// _boolean_,
+/// @return **True** if Kodi has had no input for `time` amount of seconds.
+/// @param time - elapsed seconds to check for idle activity.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.PrivacyPolicy`</b>,
+/// \anchor System_PrivacyPolicy
+/// _string_,
+/// @return The official Kodi privacy policy.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link System_PrivacyPolicy `System.PrivacyPolicy`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`System.SupportsCPUUsage`</b>,
+/// \anchor System_SupportsCPUUsage
+/// _boolean_,
+/// @return **True** if the system can provide CPU usage information.
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link System_SupportsCPUUsage `
+/// System.SupportsCPUUsage`\endlink <p>
+/// }
+/// \table_row3{ <b>`System.SupportedHDRTypes`</b>,
+/// \anchor System_SupportedHDRTypes
+/// _string_,
+/// @return The display's supported HDR types.
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link System_SupportedHDRTypes `System.SupportedHDRTypes`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`System.IsScreensaverInhibited`</b>,
+/// \anchor System_IsScreensaverInhibited
+/// _boolean_,
+/// @return **True** when screensaver on idle is disabled.
+/// <p>
+/// }
+const infomap system_labels[] = {
+ {"hasnetwork", SYSTEM_ETHERNET_LINK_ACTIVE},
+ {"hasmediadvd", SYSTEM_MEDIA_DVD},
+ {"hasmediaaudiocd", SYSTEM_MEDIA_AUDIO_CD},
+ {"dvdready", SYSTEM_DVDREADY},
+ {"trayopen", SYSTEM_TRAYOPEN},
+ {"haslocks", SYSTEM_HASLOCKS},
+ {"hashiddeninput", SYSTEM_HAS_INPUT_HIDDEN},
+ {"hasloginscreen", SYSTEM_HAS_LOGINSCREEN},
+ {"hasactivemodaldialog", SYSTEM_HAS_ACTIVE_MODAL_DIALOG},
+ {"hasvisiblemodaldialog", SYSTEM_HAS_VISIBLE_MODAL_DIALOG},
+ {"ismaster", SYSTEM_ISMASTER},
+ {"isfullscreen", SYSTEM_ISFULLSCREEN},
+ {"isstandalone", SYSTEM_ISSTANDALONE},
+ {"loggedon", SYSTEM_LOGGEDON},
+ {"showexitbutton", SYSTEM_SHOW_EXIT_BUTTON},
+ {"canpowerdown", SYSTEM_CAN_POWERDOWN},
+ {"cansuspend", SYSTEM_CAN_SUSPEND},
+ {"canhibernate", SYSTEM_CAN_HIBERNATE},
+ {"canreboot", SYSTEM_CAN_REBOOT},
+ {"screensaveractive", SYSTEM_SCREENSAVER_ACTIVE},
+ {"dpmsactive", SYSTEM_DPMS_ACTIVE},
+ {"cputemperature", SYSTEM_CPU_TEMPERATURE}, // labels from here
+ {"cpuusage", SYSTEM_CPU_USAGE},
+ {"gputemperature", SYSTEM_GPU_TEMPERATURE},
+ {"fanspeed", SYSTEM_FAN_SPEED},
+ {"freespace", SYSTEM_FREE_SPACE},
+ {"usedspace", SYSTEM_USED_SPACE},
+ {"totalspace", SYSTEM_TOTAL_SPACE},
+ {"usedspacepercent", SYSTEM_USED_SPACE_PERCENT},
+ {"freespacepercent", SYSTEM_FREE_SPACE_PERCENT},
+ {"buildversion", SYSTEM_BUILD_VERSION},
+ {"buildversionshort", SYSTEM_BUILD_VERSION_SHORT},
+ {"buildversioncode", SYSTEM_BUILD_VERSION_CODE},
+ {"buildversiongit", SYSTEM_BUILD_VERSION_GIT},
+ {"builddate", SYSTEM_BUILD_DATE},
+ {"fps", SYSTEM_FPS},
+ {"freememory", SYSTEM_FREE_MEMORY},
+ {"language", SYSTEM_LANGUAGE},
+ {"temperatureunits", SYSTEM_TEMPERATURE_UNITS},
+ {"screenmode", SYSTEM_SCREEN_MODE},
+ {"screenwidth", SYSTEM_SCREEN_WIDTH},
+ {"screenheight", SYSTEM_SCREEN_HEIGHT},
+ {"currentwindow", SYSTEM_CURRENT_WINDOW},
+ {"currentcontrol", SYSTEM_CURRENT_CONTROL},
+ {"currentcontrolid", SYSTEM_CURRENT_CONTROL_ID},
+ {"dvdlabel", SYSTEM_DVD_LABEL},
+ {"internetstate", SYSTEM_INTERNET_STATE},
+ {"osversioninfo", SYSTEM_OS_VERSION_INFO},
+ {"kernelversion", SYSTEM_OS_VERSION_INFO}, // old, not correct name
+ {"uptime", SYSTEM_UPTIME},
+ {"totaluptime", SYSTEM_TOTALUPTIME},
+ {"cpufrequency", SYSTEM_CPUFREQUENCY},
+ {"screenresolution", SYSTEM_SCREEN_RESOLUTION},
+ {"videoencoderinfo", SYSTEM_VIDEO_ENCODER_INFO},
+ {"profilename", SYSTEM_PROFILENAME},
+ {"profilethumb", SYSTEM_PROFILETHUMB},
+ {"profilecount", SYSTEM_PROFILECOUNT},
+ {"profileautologin", SYSTEM_PROFILEAUTOLOGIN},
+ {"progressbar", SYSTEM_PROGRESS_BAR},
+ {"batterylevel", SYSTEM_BATTERY_LEVEL},
+ {"friendlyname", SYSTEM_FRIENDLY_NAME},
+ {"alarmpos", SYSTEM_ALARM_POS},
+ {"isinhibit",
+ SYSTEM_IDLE_SHUTDOWN_INHIBITED}, // Deprecated, replaced by "idleshutdowninhibited"
+ {"idleshutdowninhibited", SYSTEM_IDLE_SHUTDOWN_INHIBITED},
+ {"hasshutdown", SYSTEM_HAS_SHUTDOWN},
+ {"haspvr", SYSTEM_HAS_PVR},
+ {"startupwindow", SYSTEM_STARTUP_WINDOW},
+ {"stereoscopicmode", SYSTEM_STEREOSCOPIC_MODE},
+ {"hascms", SYSTEM_HAS_CMS},
+ {"privacypolicy", SYSTEM_PRIVACY_POLICY},
+ {"haspvraddon", SYSTEM_HAS_PVR_ADDON},
+ {"addonupdatecount", SYSTEM_ADDON_UPDATE_COUNT},
+ {"supportscpuusage", SYSTEM_SUPPORTS_CPU_USAGE},
+ {"supportedhdrtypes", SYSTEM_SUPPORTED_HDR_TYPES},
+ {"isscreensaverinhibited", SYSTEM_IS_SCREENSAVER_INHIBITED}};
+
+/// \page modules__infolabels_boolean_conditions
+/// \table_row3{ <b>`System.HasAddon(id)`</b>,
+/// \anchor System_HasAddon
+/// _boolean_,
+/// @return **True** if the specified addon is installed on the system.
+/// @param id - the addon id
+/// @skinning_v19 **[Boolean Condition Updated]** \link System_HasAddon `System.HasAddon(id)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`System.AddonIsEnabled(id)`</b>,
+/// \anchor System_AddonIsEnabled
+/// _boolean_,
+/// @return **True** if the specified addon is enabled on the system.
+/// @param id - The addon Id
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link System_AddonIsEnabled `System.AddonIsEnabled(id)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`System.HasCoreId(id)`</b>,
+/// \anchor System_HasCoreId
+/// _boolean_,
+/// @return **True** if the CPU core with the given 'id' exists.
+/// @param id - the id of the CPU core
+/// <p>
+/// }
+/// \table_row3{ <b>`System.HasAlarm(alarm)`</b>,
+/// \anchor System_HasAlarm
+/// _boolean_,
+/// @return **True** if the system has the `alarm` alarm set.
+/// @param alarm - the name of the alarm
+/// <p>
+/// }
+/// \table_row3{ <b>`System.CoreUsage(id)`</b>,
+/// \anchor System_CoreUsage
+/// _string_,
+/// @return the usage of the CPU core with the given 'id'
+/// @param id - the id of the CPU core
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Setting(hidewatched)`</b>,
+/// \anchor System_Setting
+/// _boolean_,
+/// @return **True** if 'hide watched items' is selected.
+/// <p>
+/// }
+/// \table_row3{ <b>`System.Setting(hideunwatchedepisodethumbs)`</b>,
+/// \anchor System_Setting_HideUnwatchedEpisodeThumbs
+/// _boolean_,
+/// @return **True** if 'hide unwatched episode setting is enabled'\, **False** otherwise.
+/// <p><hr>
+/// @skinning_v20 **[New Boolean Condition]** \link System_Setting_HideUnwatchedEpisodeThumbs `System.Setting(hideunwatchedepisodethumbs)`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap system_param[] = {{ "hasalarm", SYSTEM_HAS_ALARM },
+ { "hascoreid", SYSTEM_HAS_CORE_ID },
+ { "setting", SYSTEM_SETTING },
+ { "hasaddon", SYSTEM_HAS_ADDON },
+ { "addonisenabled", SYSTEM_ADDON_IS_ENABLED },
+ { "coreusage", SYSTEM_GET_CORE_USAGE }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Network Network
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Network.IsDHCP`</b>,
+/// \anchor Network_IsDHCP
+/// _boolean_,
+/// @return **True** if the network type is DHCP.
+/// @note Network type can be either DHCP or FIXED
+/// <p>
+/// }
+/// \table_row3{ <b>`Network.IPAddress`</b>,
+/// \anchor Network_IPAddress
+/// _string_,
+/// @return The system's IP Address. e.g. 192.168.1.15
+/// <p>
+/// }
+/// \table_row3{ <b>`Network.LinkState`</b>,
+/// \anchor Network_LinkState
+/// _string_,
+/// @return The network linkstate e.g. 10mbit/100mbit etc.
+/// <p>
+/// }
+/// \table_row3{ <b>`Network.MacAddress`</b>,
+/// \anchor Network_MacAddress
+/// _string_,
+/// @return The system's MAC address.
+/// <p>
+/// }
+/// \table_row3{ <b>`Network.SubnetMask`</b>,
+/// \anchor Network_SubnetMask
+/// _string_,
+/// @return The network subnet mask.
+/// <p>
+/// }
+/// \table_row3{ <b>`Network.GatewayAddress`</b>,
+/// \anchor Network_GatewayAddress
+/// _string_,
+/// @return The network gateway address.
+/// <p>
+/// }
+/// \table_row3{ <b>`Network.DNS1Address`</b>,
+/// \anchor Network_DNS1Address
+/// _string_,
+/// @return The network DNS 1 address.
+/// <p>
+/// }
+/// \table_row3{ <b>`Network.DNS2Address`</b>,
+/// \anchor Network_DNS2Address
+/// _string_,
+/// @return The network DNS 2 address.
+/// <p>
+/// }
+/// \table_row3{ <b>`Network.DHCPAddress`</b>,
+/// \anchor Network_DHCPAddress
+/// _string_,
+/// @return The DHCP IP address.
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap network_labels[] = {{ "isdhcp", NETWORK_IS_DHCP },
+ { "ipaddress", NETWORK_IP_ADDRESS }, //labels from here
+ { "linkstate", NETWORK_LINK_STATE },
+ { "macaddress", NETWORK_MAC_ADDRESS },
+ { "subnetmask", NETWORK_SUBNET_MASK },
+ { "gatewayaddress", NETWORK_GATEWAY_ADDRESS },
+ { "dns1address", NETWORK_DNS1_ADDRESS },
+ { "dns2address", NETWORK_DNS2_ADDRESS },
+ { "dhcpaddress", NETWORK_DHCP_ADDRESS }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_musicpartymode Music party mode
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`MusicPartyMode.Enabled`</b>,
+/// \anchor MusicPartyMode_Enabled
+/// _boolean_,
+/// @return **True** if Party Mode is enabled.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPartyMode.SongsPlayed`</b>,
+/// \anchor MusicPartyMode_SongsPlayed
+/// _string_,
+/// @return The number of songs played during Party Mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPartyMode.MatchingSongs`</b>,
+/// \anchor MusicPartyMode_MatchingSongs
+/// _string_,
+/// @return The number of songs available to Party Mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPartyMode.MatchingSongsPicked`</b>,
+/// \anchor MusicPartyMode_MatchingSongsPicked
+/// _string_,
+/// @return The number of songs picked already for Party Mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPartyMode.MatchingSongsLeft`</b>,
+/// \anchor MusicPartyMode_MatchingSongsLeft
+/// _string_,
+/// @return The number of songs left to be picked from for Party Mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPartyMode.RelaxedSongsPicked`</b>,
+/// \anchor MusicPartyMode_RelaxedSongsPicked
+/// _string_,
+/// @todo Not currently used
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPartyMode.RandomSongsPicked`</b>,
+/// \anchor MusicPartyMode_RandomSongsPicked
+/// _string_,
+/// @return The number of unique random songs picked during Party Mode.
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap musicpartymode[] = {{ "enabled", MUSICPM_ENABLED },
+ { "songsplayed", MUSICPM_SONGSPLAYED },
+ { "matchingsongs", MUSICPM_MATCHINGSONGS },
+ { "matchingsongspicked", MUSICPM_MATCHINGSONGSPICKED },
+ { "matchingsongsleft", MUSICPM_MATCHINGSONGSLEFT },
+ { "relaxedsongspicked", MUSICPM_RELAXEDSONGSPICKED },
+ { "randomsongspicked", MUSICPM_RANDOMSONGSPICKED }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_MusicPlayer Music player
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`MusicPlayer.Offset(number).Exists`</b>,
+/// \anchor MusicPlayer_Offset
+/// _boolean_,
+/// @return **True** if the music players playlist has a song queued in
+/// position (number).
+/// @param number - song position
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Title`</b>,
+/// \anchor MusicPlayer_Title
+/// _string_,
+/// @return The title of the currently playing song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.offset(number).Title`</b>,
+/// \anchor MusicPlayer_Offset_Title
+/// _string_,
+/// @return The title of the song which has an offset `number` with respect to the
+/// current playing song.
+/// @param number - the offset number with respect to the current playing song
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Position(number).Title`</b>,
+/// \anchor MusicPlayer_Position_Title
+/// _string_,
+/// @return The title of the song which as an offset `number` with respect to the
+/// start of the playlist.
+/// @param number - the offset number with respect to the start of the playlist
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Album`</b>,
+/// \anchor MusicPlayer_Album
+/// _string_,
+/// @return The album from which the current song is from.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.offset(number).Album`</b>,
+/// \anchor MusicPlayer_OffSet_Album
+/// _string_,
+/// @return The album from which the song with offset `number` with respect to
+/// the current song is from.
+/// @param number - the offset number with respect to the current playing song
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Position(number).Album`</b>,
+/// \anchor MusicPlayer_Position_Album
+/// _string_,
+/// @return The album from which the song with offset `number` with respect to
+/// the start of the playlist is from.
+/// @param number - the offset number with respect to the start of the playlist
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Album_Mood)`</b>,
+/// \anchor MusicPlayer_Property_Album_Mood
+/// _string_,
+/// @return The moods of the currently playing Album
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Role.Composer)`</b>,
+/// \anchor MusicPlayer_Property_Role_Composer
+/// _string_,
+/// @return The name of the person who composed the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Property_Role_Composer `MusicPlayer.Property(Role.Composer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Role.Conductor)`</b>,
+/// \anchor MusicPlayer_Property_Role_Conductor
+/// _string_,
+/// @return The name of the person who conducted the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Property_Role_Conductor `MusicPlayer.Property(Role.Conductor)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Role.Orchestra)`</b>,
+/// \anchor MusicPlayer_Property_Role_Orchestra
+/// _string_,
+/// @return The name of the orchestra performing the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Property_Role_Orchestra `MusicPlayer.Property(Role.Orchestra)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Role.Lyricist)`</b>,
+/// \anchor MusicPlayer_Property_Role_Lyricist
+/// _string_,
+/// @return The name of the person who wrote the lyrics of the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Property_Role_Lyricist `MusicPlayer.Property(Role.Lyricist)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Role.Remixer)`</b>,
+/// \anchor MusicPlayer_Property_Role_Remixer
+/// _string_,
+/// @return The name of the person who remixed the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Property_Role_Remixer `MusicPlayer.Property(Role.Remixer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Role.Arranger)`</b>,
+/// \anchor MusicPlayer_Property_Role_Arranger
+/// _string_,
+/// @return The name of the person who arranged the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Property_Role_Arranger `MusicPlayer.Property(Role.Arranger)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Role.Engineer)`</b>,
+/// \anchor MusicPlayer_Property_Role_Engineer
+/// _string_,
+/// @return The name of the person who was the engineer of the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Property_Role_Engineer `MusicPlayer.Property(Role.Engineer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Role.Producer)`</b>,
+/// \anchor MusicPlayer_Property_Role_Producer
+/// _string_,
+/// @return The name of the person who produced the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Property_Role_Producer `MusicPlayer.Property(Role.Producer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Role.DJMixer)`</b>,
+/// \anchor MusicPlayer_Property_Role_DJMixer
+/// _string_,
+/// @return The name of the dj who remixed the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Property_Role_DJMixer `MusicPlayer.Property(Role.DJMixer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Role.Mixer)`</b>,
+/// \anchor MusicPlayer_Property_Role_Mixer
+/// _string_,
+/// @return The name of the dj who remixed the selected song.
+/// @todo So maybe rather than a row each have one entry for Role.XXXXX with composer\, arranger etc. as listed values
+/// @note MusicPlayer.Property(Role.any_custom_role) also works\,
+/// where any_custom_role could be an instrument violin or some other production activity e.g. sound engineer.
+/// The roles listed (composer\, arranger etc.) are standard ones but there are many possible.
+/// Music file tagging allows for the musicians and all other people involved in the recording to be added\, Kodi
+/// will gathers and stores that data\, and it is available to GUI.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Property_Role_Mixer `MusicPlayer.Property(Role.Mixer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Album_Mood)`</b>,
+/// \anchor MusicPlayer_Property_Album_Mood
+/// _string_,
+/// @return the moods of the currently playing Album
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Album_Style)`</b>,
+/// \anchor MusicPlayer_Property_Album_Style
+/// _string_,
+/// @return the styles of the currently playing Album.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Album_Theme)`</b>,
+/// \anchor MusicPlayer_Property_Album_Theme
+/// _string_,
+/// @return The themes of the currently playing Album
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Album_Type)`</b>,
+/// \anchor MusicPlayer_Property_Album_Type
+/// _string_,
+/// @return The album type (e.g. compilation\, enhanced\, explicit lyrics) of the
+/// currently playing album.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Album_Label)`</b>,
+/// \anchor MusicPlayer_Property_Album_Label
+/// _string_,
+/// @return The record label of the currently playing album.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Album_Description)`</b>,
+/// \anchor MusicPlayer_Property_Album_Description
+/// _string_,
+/// @return A review of the currently playing album
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Artist`</b>,
+/// \anchor MusicPlayer_Artist
+/// _string_,
+/// @return Artist(s) of current song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.offset(number).Artist`</b>,
+/// \anchor MusicPlayer_Offset_Artist
+/// _string_,
+/// @return Artist(s) of the song which has an offset `number` with respect
+/// to the current playing song.
+/// @param number - the offset of the song with respect to the current
+/// playing song
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Position(number).Artist`</b>,
+/// \anchor MusicPlayer_Position_Artist
+/// _string_,
+/// @return Artist(s) of the song which has an offset `number` with respect
+/// to the start of the playlist.
+/// @param number - the offset of the song with respect to
+/// the start of the playlist
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.AlbumArtist`</b>,
+/// \anchor MusicPlayer_AlbumArtist
+/// _string_,
+/// @return The album artist of the currently playing song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Cover`</b>,
+/// \anchor MusicPlayer_Cover
+/// _string_,
+/// @return The album cover of currently playing song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Sortname)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Sortname
+/// _string_,
+/// @return The sortname of the currently playing Artist.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link MusicPlayer_Property_Artist_Sortname `MusicPlayer.Property(Artist_Sortname)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Type)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Type
+/// _string_,
+/// @return The type of the currently playing Artist - person\,
+/// group\, orchestra\, choir etc.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link MusicPlayer_Property_Artist_Type `MusicPlayer.Property(Artist_Type)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Gender)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Gender
+/// _string_,
+/// @return The gender of the currently playing Artist - male\,
+/// female\, other.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link MusicPlayer_Property_Artist_Gender `MusicPlayer.Property(Artist_Gender)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Disambiguation)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Disambiguation
+/// _string_,
+/// @return A brief description of the currently playing Artist that differentiates them
+/// from others with the same name.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link MusicPlayer_Property_Artist_Disambiguation `MusicPlayer.Property(Artist_Disambiguation)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Born)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Born
+/// _string_,
+/// @return The date of Birth of the currently playing Artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Died)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Died
+/// _string_,
+/// @return The date of Death of the currently playing Artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Formed)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Formed
+/// _string_,
+/// @return The Formation date of the currently playing Artist/Band.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Disbanded)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Disbanded
+/// _string_,
+/// @return The disbanding date of the currently playing Artist/Band.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_YearsActive)`</b>,
+/// \anchor MusicPlayer_Property_Artist_YearsActive
+/// _string_,
+/// @return The years the currently Playing artist has been active.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Instrument)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Instrument
+/// _string_,
+/// @return The instruments played by the currently playing artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Description)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Description
+/// _string_,
+/// @return A biography of the currently playing artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Mood)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Mood
+/// _string_,
+/// @return The moods of the currently playing artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Style)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Style
+/// _string_,
+/// @return The styles of the currently playing artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(Artist_Genre)`</b>,
+/// \anchor MusicPlayer_Property_Artist_Genre
+/// _string_,
+/// @return The genre of the currently playing artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Genre`</b>,
+/// \anchor MusicPlayer_Genre
+/// _string_,
+/// @return The genre(s) of current song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.offset(number).Genre`</b>,
+/// \anchor MusicPlayer_OffSet_Genre
+/// _string_,
+/// @return The genre(s) of the song with an offset `number` with respect
+/// to the current playing song.
+/// @param number - the offset song number with respect to the current playing
+/// song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Position(number).Genre`</b>,
+/// \anchor MusicPlayer_Position_Genre
+/// _string_,
+/// @return The genre(s) of the song with an offset `number` with respect
+/// to the start of the playlist.
+/// @param number - the offset song number with respect to the start of the
+/// playlist
+/// song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Lyrics`</b>,
+/// \anchor MusicPlayer_Lyrics
+/// _string_,
+/// @return The lyrics of current song stored in ID tag info.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Year`</b>,
+/// \anchor MusicPlayer_Year
+/// _string_,
+/// @return The year of release of current song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.offset(number).Year`</b>,
+/// \anchor MusicPlayer_Offset_Year
+/// _string_,
+/// @return The year of release of the song with an offset `number` with
+/// respect to the current playing song.
+/// @param number - the offset number with respect to the current song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Position(number).Year`</b>,
+/// \anchor MusicPlayer_Position_Year
+/// _string_,
+/// @return The year of release of the song with an offset `number` with
+/// respect to the start of the playlist.
+/// @param number - the offset number with respect to the start of the
+/// playlist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Rating`</b>,
+/// \anchor MusicPlayer_Rating
+/// _string_,
+/// @return The numeric Rating of current song (1-10).
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.offset(number).Rating`</b>,
+/// \anchor MusicPlayer_OffSet_Rating
+/// _string_,
+/// @return The numeric Rating of song with an offset `number` with
+/// respect to the current playing song.
+/// @param number - the offset with respect to the current playing song
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Position(number).Rating`</b>,
+/// \anchor MusicPlayer_Position_Rating
+/// _string_,
+/// @return The numeric Rating of song with an offset `number` with
+/// respect to the start of the playlist.
+/// @param number - the offset with respect to the start of the playlist
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.RatingAndVotes`</b>,
+/// \anchor MusicPlayer_RatingAndVotes
+/// _string_,
+/// @return The scraped rating and votes of currently playing song\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.UserRating`</b>,
+/// \anchor MusicPlayer_UserRating
+/// _string_,
+/// @return The scraped rating of the currently playing song (1-10).
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_UserRating `MusicPlayer.UserRating`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Votes`</b>,
+/// \anchor MusicPlayer_Votes
+/// _string_,
+/// @return The scraped votes of currently playing song\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.DiscNumber`</b>,
+/// \anchor MusicPlayer_DiscNumber
+/// _string_,
+/// @return The Disc Number of current song stored in ID tag info.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.offset(number).DiscNumber`</b>,
+/// \anchor MusicPlayer_Offset_DiscNumber
+/// _string_,
+/// @return The Disc Number of current song stored in ID tag info for the
+/// song with an offset `number` with respect to the playing song.
+/// @param number - The offset value for the song with respect to the
+/// playing song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Position(number).DiscNumber`</b>,
+/// \anchor MusicPlayer_Position_DiscNumber
+/// _string_,
+/// @return The Disc Number of current song stored in ID tag info for the
+/// song with an offset `number` with respect to the start of the playlist.
+/// @param number - The offset value for the song with respect to the
+/// start of the playlist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Comment`</b>,
+/// \anchor MusicPlayer_Comment
+/// _string_,
+/// @return The Comment of current song stored in ID tag info.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.offset(number).Comment`</b>,
+/// \anchor MusicPlayer_Offset_Comment
+/// _string_,
+/// @return The Comment of current song stored in ID tag info for the
+/// song with an offset `number` with respect to the playing song.
+/// @param number - The offset value for the song with respect to the
+/// playing song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Position(number).Comment`</b>,
+/// \anchor MusicPlayer_Position_Comment
+/// _string_,
+/// @return The Comment of current song stored in ID tag info for the
+/// song with an offset `number` with respect to the start of the playlist.
+/// @param number - The offset value for the song with respect to the
+/// start of the playlist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Contributors`</b>,
+/// \anchor MusicPlayer_Contributors
+/// _string_,
+/// @return The list of all people who've contributed to the currently playing song
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Contributors `MusicPlayer.Contributors`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.ContributorAndRole`</b>,
+/// \anchor MusicPlayer_ContributorAndRole
+/// _string_,
+/// @return The list of all people and their role who've contributed to the currently playing song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_ContributorAndRole `MusicPlayer.ContributorAndRole`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Mood`</b>,
+/// \anchor MusicPlayer_Mood
+/// _string_,
+/// @return The mood of the currently playing song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_Mood `MusicPlayer.Mood`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.PlaylistPlaying`</b>,
+/// \anchor MusicPlayer_PlaylistPlaying
+/// _boolean_,
+/// @return **True** if a playlist is currently playing.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Exists(relative\,position)`</b>,
+/// \anchor MusicPlayer_Exists
+/// _boolean_,
+/// @return **True** if the currently playing playlist has a song queued at the given position.
+/// @param relative - bool - If the position is relative
+/// @param position - int - The position of the song
+/// @note It is possible to define whether the position is relative or not\, default is false.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.HasPrevious`</b>,
+/// \anchor MusicPlayer_HasPrevious
+/// _boolean_,
+/// @return **True** if the music player has a a Previous Song in the Playlist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.HasNext`</b>,
+/// \anchor MusicPlayer_HasNext
+/// _boolean_,
+/// @return **True** if the music player has a next song queued in the Playlist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.PlayCount`</b>,
+/// \anchor MusicPlayer_PlayCount
+/// _integer_,
+/// @return The play count of currently playing song\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.LastPlayed`</b>,
+/// \anchor MusicPlayer_LastPlayed
+/// _string_,
+/// @return The last play date of currently playing song\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.TrackNumber`</b>,
+/// \anchor MusicPlayer_TrackNumber
+/// _string_,
+/// @return The track number of current song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.offset(number).TrackNumber`</b>,
+/// \anchor MusicPlayer_Offset_TrackNumber
+/// _string_,
+/// @return The track number of the song with an offset `number`
+/// with respect to the current playing song.
+/// @param number - The offset number of the song with respect to the
+/// playing song
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Position(number).TrackNumber`</b>,
+/// \anchor MusicPlayer_Position_TrackNumber
+/// _string_,
+/// @return The track number of the song with an offset `number`
+/// with respect to start of the playlist.
+/// @param number - The offset number of the song with respect
+/// to start of the playlist
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Duration`</b>,
+/// \anchor MusicPlayer_Duration
+/// _string_,
+/// @return The duration of the current song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.offset(number).Duration`</b>,
+/// \anchor MusicPlayer_Offset_Duration
+/// _string_,
+/// @return The duration of the song with an offset `number`
+/// with respect to the current playing song.
+/// @param number - the offset number of the song with respect
+/// to the current playing song
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Position(number).Duration`</b>,
+/// \anchor MusicPlayer_Position_Duration
+/// _string_,
+/// @return The duration of the song with an offset `number`
+/// with respect to the start of the playlist.
+/// @param number - the offset number of the song with respect
+/// to the start of the playlist
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.BitRate`</b>,
+/// \anchor MusicPlayer_BitRate
+/// _string_,
+/// @return The bitrate of current song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Channels`</b>,
+/// \anchor MusicPlayer_Channels
+/// _string_,
+/// @return The number of channels of current song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.BitsPerSample`</b>,
+/// \anchor MusicPlayer_BitsPerSample
+/// _string_,
+/// @return The number of bits per sample of current song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.SampleRate`</b>,
+/// \anchor MusicPlayer_SampleRate
+/// _string_,
+/// @return The samplerate of current playing song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Codec`</b>,
+/// \anchor MusicPlayer_Codec
+/// _string_,
+/// @return The codec of current playing song.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.PlaylistPosition`</b>,
+/// \anchor MusicPlayer_PlaylistPosition
+/// _string_,
+/// @return The position of the current song in the current music playlist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.PlaylistLength`</b>,
+/// \anchor MusicPlayer_PlaylistLength
+/// _string_,
+/// @return The total size of the current music playlist.
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.ChannelName`</b>,
+/// \anchor MusicPlayer_ChannelName
+/// _string_,
+/// @return The channel name of the radio programme that's currently playing (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.ChannelNumberLabel`</b>,
+/// \anchor MusicPlayer_ChannelNumberLabel
+/// _string_,
+/// @return The channel and subchannel number of the radio channel that's currently
+/// playing (PVR).
+/// <p><hr>
+/// @skinning_v14 **[New Infolabel]** \link MusicPlayer_ChannelNumberLabel `MusicPlayer.ChannelNumberLabel`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.ChannelGroup`</b>,
+/// \anchor MusicPlayer_ChannelGroup
+/// _string_,
+/// @return The channel group of the radio programme that's currently playing (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Property(propname)`</b>,
+/// \anchor MusicPlayer_Property_Propname
+/// _string_,
+/// @return The requested property value of the currently playing item.
+/// @param propname - The requested property
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.DBID`</b>,
+/// \anchor MusicPlayer_DBID
+/// _string_,
+/// @return The database id of the currently playing song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link MusicPlayer_DBID `MusicPlayer.DBID`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.DiscTitle`</b>,
+/// \anchor MusicPlayer_DiscTitle
+/// _string_,
+/// @return The title of the disc currently playing.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link MusicPlayer_DiscTitle `MusicPlayer.DiscTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.ReleaseDate`</b>,
+/// \anchor MusicPlayer_ReleaseDate
+/// _string_,
+/// @return The release date of the song currently playing.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link MusicPlayer_ReleaseDate `MusicPlayer.ReleaseDate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.OriginalDate`</b>,
+/// \anchor MusicPlayer_OriginalDate
+/// _string_,
+/// @return The original release date of the song currently playing.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link MusicPlayer_OriginalDate `MusicPlayer.OriginalDate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.BPM`</b>,
+/// \anchor MusicPlayer_BPM
+/// _string_,
+/// @return The bpm of the track currently playing.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link MusicPlayer_BPM `MusicPlayer.BPM`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.IsMultiDisc`</b>,
+/// \anchor MusicPlayer_IsMultiDisc
+/// _boolean_,
+/// @return Returns **true** if the album currently playing has more than one disc.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link MusicPlayer_IsMultiDisc `MusicPlayer.IsMultiDisc`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.TotalDiscs`</b>,
+/// \anchor MusicPlayer_TotalDiscs
+/// _string_,
+/// @return The number of discs associated with the currently playing album.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link MusicPlayer_TotalDiscs `MusicPlayer.TotalDiscs`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`MusicPlayer.Station`</b>,
+/// \anchor MusicPlayer_Station
+/// _string_,
+/// @return The name of the radio station currently playing (if available).
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link MusicPlayer_Station `MusicPlayer.Station`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap musicplayer[] = {{ "title", MUSICPLAYER_TITLE },
+ { "album", MUSICPLAYER_ALBUM },
+ { "artist", MUSICPLAYER_ARTIST },
+ { "albumartist", MUSICPLAYER_ALBUM_ARTIST },
+ { "year", MUSICPLAYER_YEAR },
+ { "genre", MUSICPLAYER_GENRE },
+ { "duration", MUSICPLAYER_DURATION },
+ { "tracknumber", MUSICPLAYER_TRACK_NUMBER },
+ { "cover", MUSICPLAYER_COVER },
+ { "bitrate", MUSICPLAYER_BITRATE },
+ { "playlistlength", MUSICPLAYER_PLAYLISTLEN },
+ { "playlistposition", MUSICPLAYER_PLAYLISTPOS },
+ { "channels", MUSICPLAYER_CHANNELS },
+ { "bitspersample", MUSICPLAYER_BITSPERSAMPLE },
+ { "samplerate", MUSICPLAYER_SAMPLERATE },
+ { "codec", MUSICPLAYER_CODEC },
+ { "discnumber", MUSICPLAYER_DISC_NUMBER },
+ { "disctitle", MUSICPLAYER_DISC_TITLE },
+ { "rating", MUSICPLAYER_RATING },
+ { "ratingandvotes", MUSICPLAYER_RATING_AND_VOTES },
+ { "userrating", MUSICPLAYER_USER_RATING },
+ { "votes", MUSICPLAYER_VOTES },
+ { "comment", MUSICPLAYER_COMMENT },
+ { "mood", MUSICPLAYER_MOOD },
+ { "contributors", MUSICPLAYER_CONTRIBUTORS },
+ { "contributorandrole", MUSICPLAYER_CONTRIBUTOR_AND_ROLE },
+ { "lyrics", MUSICPLAYER_LYRICS },
+ { "playlistplaying", MUSICPLAYER_PLAYLISTPLAYING },
+ { "exists", MUSICPLAYER_EXISTS },
+ { "hasprevious", MUSICPLAYER_HASPREVIOUS },
+ { "hasnext", MUSICPLAYER_HASNEXT },
+ { "playcount", MUSICPLAYER_PLAYCOUNT },
+ { "lastplayed", MUSICPLAYER_LASTPLAYED },
+ { "channelname", MUSICPLAYER_CHANNEL_NAME },
+ { "channelnumberlabel", MUSICPLAYER_CHANNEL_NUMBER },
+ { "channelgroup", MUSICPLAYER_CHANNEL_GROUP },
+ { "dbid", MUSICPLAYER_DBID },
+ { "property", MUSICPLAYER_PROPERTY },
+ { "releasedate", MUSICPLAYER_RELEASEDATE },
+ { "originaldate", MUSICPLAYER_ORIGINALDATE },
+ { "bpm", MUSICPLAYER_BPM },
+ { "ismultidisc", MUSICPLAYER_ISMULTIDISC },
+ { "totaldiscs", MUSICPLAYER_TOTALDISCS },
+ { "station", MUSICPLAYER_STATIONNAME }
+};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Videoplayer Video player
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`VideoPlayer.UsingOverlays`</b>,
+/// \anchor VideoPlayer_UsingOverlays
+/// _boolean_,
+/// @return **True** if the video player is using the hardware overlays render
+/// method.
+/// @note This is useful\, as with hardware overlays you have no alpha blending to
+/// the video image\, so shadows etc. need redoing\, or disabling.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.IsFullscreen`</b>,
+/// \anchor VideoPlayer_IsFullscreen
+/// _boolean_,
+/// @return **True** if the video player is in fullscreen mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.HasMenu`</b>,
+/// \anchor VideoPlayer_HasMenu
+/// _boolean_,
+/// @return **True** if the video player has a menu (ie is playing a DVD).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.HasInfo`</b>,
+/// \anchor VideoPlayer_HasInfo
+/// _boolean_,
+/// @return **True** if the current playing video has information from the
+/// library or from a plugin (eg director/plot etc.)
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Content(parameter)`</b>,
+/// \anchor VideoPlayer_Content
+/// _boolean_,
+/// @return **True** if the current Video you are playing is contained in
+/// corresponding Video Library sections. The following values are accepted:
+/// - <b>files</b>
+/// - <b>movies</b>
+/// - <b>episodes</b>
+/// - <b>musicvideos</b>
+/// - <b>livetv</b>
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.HasSubtitles`</b>,
+/// \anchor VideoPlayer_HasSubtitles
+/// _boolean_,
+/// @return **True** if there are subtitles available for video.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.HasTeletext`</b>,
+/// \anchor VideoPlayer_HasTeletext
+/// _boolean_,
+/// @return **True** if teletext is usable on played TV channel.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.IsStereoscopic`</b>,
+/// \anchor VideoPlayer_IsStereoscopic
+/// _boolean_,
+/// @return **True** when the currently playing video is a 3D (stereoscopic)
+/// video.
+/// <p><hr>
+/// @skinning_v13 **[New Boolean Condition]** \link VideoPlayer_IsStereoscopic `VideoPlayer.IsStereoscopic`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.SubtitlesEnabled`</b>,
+/// \anchor VideoPlayer_SubtitlesEnabled
+/// _boolean_,
+/// @return **True** if subtitles are turned on for video.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.HasEpg`</b>,
+/// \anchor VideoPlayer_HasEpg
+/// _boolean_,
+/// @return **True** if epg information is available for the currently playing
+/// programme (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.CanResumeLiveTV`</b>,
+/// \anchor VideoPlayer_CanResumeLiveTV
+/// _boolean_,
+/// @return **True** if a in-progress PVR recording is playing an the respective
+/// live TV channel is available.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Title`</b>,
+/// \anchor VideoPlayer_Title
+/// _string_,
+/// @return The title of currently playing video.
+/// @note If it's in the database it will return the database title\, else the filename.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Title`</b>,
+/// \anchor VideoPlayer_Offset_Title
+/// _string_,
+/// @return The title of video which has an offset `number` with respect to the currently playing video.
+/// @note If it's in the database it will return the database title\, else the filename.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Title `VideoPlayer.offset(number).Title`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Title`</b>,
+/// \anchor VideoPlayer_Position_Title
+/// _string_,
+/// @return The title of the video which has an offset `number` with respect to the start of the playlist.
+/// @note If it's in the database it will return the database title\, else the filename.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Title `VideoPlayer.position(number).Title`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.OriginalTitle`</b>,
+/// \anchor VideoPlayer_OriginalTitle
+/// _string_,
+/// @return The original title of currently playing video. If it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).OriginalTitle`</b>,
+/// \anchor VideoPlayer_Offset_OriginalTitle
+/// _string_,
+/// @return The original title of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_OriginalTitle `VideoPlayer.offset(number).OriginalTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).OriginalTitle`</b>,
+/// \anchor VideoPlayer_Position_OriginalTitle
+/// _string_,
+/// @return The original title of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_OriginalTitle `VideoPlayer.position(number).OriginalTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.TVShowTitle`</b>,
+/// \anchor VideoPlayer_TVShowTitle
+/// _string_,
+/// @return The title of currently playing episode's tvshow name.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).TVShowTitle`</b>,
+/// \anchor VideoPlayer_Offset_TVShowTitle
+/// _string_,
+/// @return The title of the episode's tvshow name which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_TVShowTitle `VideoPlayer.offset(number).TVShowTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).TVShowTitle`</b>,
+/// \anchor VideoPlayer_Position_TVShowTitle
+/// _string_,
+/// @return The title of the episode's tvshow name which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_TVShowTitle `VideoPlayer.position(number).TVShowTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Season`</b>,
+/// \anchor VideoPlayer_Season
+/// _string_,
+/// @return The season number of the currently playing episode\, if it's in the database.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link VideoPlayer_Season `VideoPlayer.Season`\endlink
+/// also supports EPG.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Season`</b>,
+/// \anchor VideoPlayer_Offset_Season
+/// _string_,
+/// @return The season number of the episode which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Season `VideoPlayer.offset(number).Season`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Season`</b>,
+/// \anchor VideoPlayer_Position_Season
+/// _string_,
+/// @return The season number of the episode which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Season `VideoPlayer.position(number).Season`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Episode`</b>,
+/// \anchor VideoPlayer_Episode
+/// _string_,
+/// @return The episode number of the currently playing episode.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link VideoPlayer_Episode `VideoPlayer.Episode`\endlink
+/// also supports EPG.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Episode`</b>,
+/// \anchor VideoPlayer_Offset_Episode
+/// _string_,
+/// @return The episode number of the episode which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Episode `VideoPlayer.offset(number).Episode`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Episode`</b>,
+/// \anchor VideoPlayer_Position_Episode
+/// _string_,
+/// @return The episode number of the episode which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Episode `VideoPlayer.position(number).Episode`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Genre`</b>,
+/// \anchor VideoPlayer_Genre
+/// _string_,
+/// @return The genre(s) of current movie\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Genre`</b>,
+/// \anchor VideoPlayer_Offset_Genre
+/// _string_,
+/// @return The genre(s) of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Genre `VideoPlayer.offset(number).Genre`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Genre`</b>,
+/// \anchor VideoPlayer_Position_Genre
+/// _string_,
+/// @return The genre(s) of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Genre `VideoPlayer.position(number).Genre`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Director`</b>,
+/// \anchor VideoPlayer_Director
+/// _string_,
+/// @return The director of current movie\, if it's in the database.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link VideoPlayer_Director `VideoPlayer.Director`\endlink
+/// also supports EPG.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Director`</b>,
+/// \anchor VideoPlayer_Offset_Director
+/// _string_,
+/// @return The director of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Director `VideoPlayer.offset(number).VideoPlayer_Offset_Director`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Director`</b>,
+/// \anchor VideoPlayer_Position_Director
+/// _string_,
+/// @return The director of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Director `VideoPlayer.position(number).Director`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Country`</b>,
+/// \anchor VideoPlayer_Country
+/// _string_,
+/// @return The production country of current movie\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Country`</b>,
+/// \anchor VideoPlayer_Offset_Country
+/// _string_,
+/// @return The production country of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Country `VideoPlayer.offset(number).Country`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Country`</b>,
+/// \anchor VideoPlayer_Position_Country
+/// _string_,
+/// @return The production country of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Country `VideoPlayer.position(number).Country`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Year`</b>,
+/// \anchor VideoPlayer_Year
+/// _string_,
+/// @return The year of release of current movie\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Year`</b>,
+/// \anchor VideoPlayer_Offset_Year
+/// _string_,
+/// @return The year of release of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Year `VideoPlayer.offset(number).Year`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Year`</b>,
+/// \anchor VideoPlayer_Position_Year
+/// _string_,
+/// @return The year of release of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Year `VideoPlayer.position(number).Year`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Cover`</b>,
+/// \anchor VideoPlayer_Cover
+/// _string_,
+/// @return The cover of currently playing movie.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Cover`</b>,
+/// \anchor VideoPlayer_Offset_Cover
+/// _string_,
+/// @return The cover of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Cover `VideoPlayer.offset(number).Cover`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Cover`</b>,
+/// \anchor VideoPlayer_Position_Cover
+/// _string_,
+/// @return The cover of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Cover `VideoPlayer.position(number).Cover`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Rating`</b>,
+/// \anchor VideoPlayer_Rating
+/// _string_,
+/// @return The scraped rating of current movie\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Rating`</b>,
+/// \anchor VideoPlayer_Offset_Rating
+/// _string_,
+/// @return The scraped rating of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Rating `VideoPlayer.offset(number).Rating`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Rating`</b>,
+/// \anchor VideoPlayer_Position_Rating
+/// _string_,
+/// @return The scraped rating of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Rating `VideoPlayer.position(number).Rating`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.UserRating`</b>,
+/// \anchor VideoPlayer_UserRating
+/// _string_,
+/// @return The user rating of the currently playing item.
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link VideoPlayer_UserRating `VideoPlayer.UserRating`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).UserRating`</b>,
+/// \anchor VideoPlayer_Offset_UserRating
+/// _string_,
+/// @return The user rating of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_UserRating `VideoPlayer.offset(number).UserRating`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).UserRating`</b>,
+/// \anchor VideoPlayer_Position_UserRating
+/// _string_,
+/// @return The user rating of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_UserRating `VideoPlayer.position(number).UserRating`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Votes`</b>,
+/// \anchor VideoPlayer_Votes
+/// _string_,
+/// @return The scraped votes of current movie\, if it's in the database.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link VideoPlayer_Votes `VideoPlayer.Votes`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Votes`</b>,
+/// \anchor VideoPlayer_Offset_Votes
+/// _string_,
+/// @return The scraped votes of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Votes `VideoPlayer.offset(number).Votes`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Votes`</b>,
+/// \anchor VideoPlayer_Position_Votes
+/// _string_,
+/// @return The scraped votes of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Votes `VideoPlayer.position(number).Votes`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.RatingAndVotes`</b>,
+/// \anchor VideoPlayer_RatingAndVotes
+/// _string_,
+/// @return The scraped rating and votes of current movie\, if it's in the database
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).RatingAndVotes`</b>,
+/// \anchor VideoPlayer_Offset_RatingAndVotes
+/// _string_,
+/// @return The scraped rating and votes of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_RatingAndVotes `VideoPlayer.offset(number).RatingAndVotes`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).RatingAndVotes`</b>,
+/// \anchor VideoPlayer_Position_RatingAndVotes
+/// _string_,
+/// @return The scraped rating and votes of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_RatingAndVotes `VideoPlayer.position(number).RatingAndVotes`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.mpaa`</b>,
+/// \anchor VideoPlayer_mpaa
+/// _string_,
+/// @return The MPAA rating of current movie\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).mpaa`</b>,
+/// \anchor VideoPlayer_Offset_mpaa
+/// _string_,
+/// @return The MPAA rating of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_mpaa `VideoPlayer.offset(number).mpaa`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).mpaa`</b>,
+/// \anchor VideoPlayer_Position_mpaa
+/// _string_,
+/// @return The MPAA rating of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_mpaa `VideoPlayer.position(number).mpaa`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Art(type)`</b>,
+/// \anchor VideoPlayer_art
+/// _string_,
+/// @return The art path for the requested arttype and for the currently playing video.
+/// @param type - can virtually be anything\, refers to the art type keyword in the art map (poster\, fanart\, banner\, thumb\, etc)
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link VideoPlayer_art `VideoPlayer.Art(type)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Art(type)`</b>,
+/// \anchor VideoPlayer_Offset_art
+/// _string_,
+/// @return The art path for the requested arttype and for the video which has an offset `number` with respect to the currently playing video.
+/// @param number - the offset with respect to the start of the playlist
+/// @param type - can virtually be anything\, refers to the art type keyword in the art map (poster\, fanart\, banner\, thumb\, etc)
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link VideoPlayer_Offset_mpaa `VideoPlayer.offset(number).Art(type)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Art(type)`</b>,
+/// \anchor VideoPlayer_position_art
+/// _string_,
+/// @return The art path for the requested arttype and for the video which has an offset `number` with respect to the start of the playlist.
+/// @param number - the offset with respect to the start of the playlist
+/// @param type - can virtually be anything\, refers to the art type keyword in the art map (poster\, fanart\, banner\, thumb\, etc)
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link VideoPlayer_position_art `VideoPlayer.position(number).Art(type)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.IMDBNumber`</b>,
+/// \anchor VideoPlayer_IMDBNumber
+/// _string_,
+/// @return The IMDb ID of the current movie\, if it's in the database.
+/// <p><hr>
+/// @skinning_v15 **[New Infolabel]** \link VideoPlayer_IMDBNumber `VideoPlayer.IMDBNumber`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).IMDBNumber`</b>,
+/// \anchor VideoPlayer_Offset_IMDBNumber
+/// _string_,
+/// @return The IMDb ID of the the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_IMDBNumber `VideoPlayer.offset(number).IMDBNumber`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).IMDBNumber`</b>,
+/// \anchor VideoPlayer_Position_IMDBNumber
+/// _string_,
+/// @return The IMDb ID of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_IMDBNumber `VideoPlayer.position(number).IMDBNumber`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Top250`</b>,
+/// \anchor VideoPlayer_Top250
+/// _string_,
+/// @return The IMDb Top250 position of the currently playing movie\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Top250`</b>,
+/// \anchor VideoPlayer_Offset_Top250
+/// _string_,
+/// @return The IMDb Top250 position of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Top250 `VideoPlayer.offset(number).Top250`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Top250`</b>,
+/// \anchor VideoPlayer_Position_Top250
+/// _string_,
+/// @return The IMDb Top250 position of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Top250 `VideoPlayer.position(number).Top250`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.EpisodeName`</b>,
+/// \anchor VideoPlayer_EpisodeName
+/// _string_,
+/// @return The name of the episode if the playing video is a TV Show\,
+/// if it's in the database (PVR).
+/// <p><hr>
+/// @skinning_v15 **[New Infolabel]** \link VideoPlayer_EpisodeName `VideoPlayer.EpisodeName`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.PlaylistPosition`</b>,
+/// \anchor VideoPlayer_PlaylistPosition
+/// _string_,
+/// @return The position of the current song in the current video playlist.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.PlaylistLength`</b>,
+/// \anchor VideoPlayer_PlaylistLength
+/// _string_,
+/// @return The total size of the current video playlist.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Cast`</b>,
+/// \anchor VideoPlayer_Cast
+/// _string_,
+/// @return A concatenated string of cast members of the current movie\, if it's in
+/// the database.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link VideoPlayer_Cast `VideoPlayer.Cast`\endlink
+/// also supports EPG.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.CastAndRole`</b>,
+/// \anchor VideoPlayer_CastAndRole
+/// _string_,
+/// @return A concatenated string of cast members and roles of the current movie\,
+/// if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Album`</b>,
+/// \anchor VideoPlayer_Album
+/// _string_,
+/// @return The album from which the current Music Video is from\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Album`</b>,
+/// \anchor VideoPlayer_Offset_Album
+/// _string_,
+/// @return The album from which the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Album `VideoPlayer.offset(number).Album`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Album`</b>,
+/// \anchor VideoPlayer_Position_Album
+/// _string_,
+/// @return The album from which the music video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Album `VideoPlayer.position(number).Album`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Artist`</b>,
+/// \anchor VideoPlayer_Artist
+/// _string_,
+/// @return The artist(s) of current Music Video\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Artist`</b>,
+/// \anchor VideoPlayer_Offset_Artist
+/// _string_,
+/// @return The artist(s) of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Artist `VideoPlayer.offset(number).Artist`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Artist`</b>,
+/// \anchor VideoPlayer_Position_Artist
+/// _string_,
+/// @return The artist(s) of the music video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Artist `VideoPlayer.position(number).Artist`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Studio`</b>,
+/// \anchor VideoPlayer_Studio
+/// _string_,
+/// @return The studio of current Music Video\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Studio`</b>,
+/// \anchor VideoPlayer_Offset_Studio
+/// _string_,
+/// @return The studio of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Studio `VideoPlayer.offset(number).Studio`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Studio`</b>,
+/// \anchor VideoPlayer_Position_Studio
+/// _string_,
+/// @return The studio of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Studio `VideoPlayer.position(number).Studio`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Writer`</b>,
+/// \anchor VideoPlayer_Writer
+/// _string_,
+/// @return The name of Writer of current playing Video\, if it's in the database.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link VideoPlayer_Writer `VideoPlayer.Writer`\endlink
+/// also supports EPG.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Writer`</b>,
+/// \anchor VideoPlayer_Offset_Writer
+/// _string_,
+/// @return The name of Writer of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Writer `VideoPlayer.offset(number).Writer`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Writer`</b>,
+/// \anchor VideoPlayer_Position_Writer
+/// _string_,
+/// @return The name of Writer of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Writer `VideoPlayer.position(number).Writer`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Tagline`</b>,
+/// \anchor VideoPlayer_Tagline
+/// _string_,
+/// @return The small Summary of current playing Video\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Tagline`</b>,
+/// \anchor VideoPlayer_Offset_Tagline
+/// _string_,
+/// @return The small Summary of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Tagline `VideoPlayer.offset(number).Tagline`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Tagline`</b>,
+/// \anchor VideoPlayer_Position_Tagline
+/// _string_,
+/// @return The small Summary of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Tagline `VideoPlayer.position(number).Tagline`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.PlotOutline`</b>,
+/// \anchor VideoPlayer_PlotOutline
+/// _string_,
+/// @return The small Summary of current playing Video\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).PlotOutline`</b>,
+/// \anchor VideoPlayer_Offset_PlotOutline
+/// _string_,
+/// @return The small Summary of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_PlotOutline `VideoPlayer.offset(number).PlotOutline`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).PlotOutline`</b>,
+/// \anchor VideoPlayer_Position_PlotOutline
+/// _string_,
+/// @return The small Summary of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_PlotOutline `VideoPlayer.position(number).PlotOutline`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Plot`</b>,
+/// \anchor VideoPlayer_Plot
+/// _string_,
+/// @return The complete Text Summary of current playing Video\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Plot`</b>,
+/// \anchor VideoPlayer_Offset_Plot
+/// _string_,
+/// @return The complete Text Summary of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Plot `VideoPlayer.offset(number).Plot`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Plot`</b>,
+/// \anchor VideoPlayer_Position_Plot
+/// _string_,
+/// @return The complete Text Summary of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Plot `VideoPlayer.position(number).Plot`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Premiered`</b>,
+/// \anchor VideoPlayer_Premiered
+/// _string_,
+/// @return The release or aired date of the currently playing episode\, show\, movie or EPG item\,
+/// if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Premiered`</b>,
+/// \anchor VideoPlayer_Offset_Premiered
+/// _string_,
+/// @return The release or aired date of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Premiered `VideoPlayer.offset(number).Premiered`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Premiered`</b>,
+/// \anchor VideoPlayer_Position_Premiered
+/// _string_,
+/// @return The release or aired date of the video which has an offset `number` with respect to the start of the playlist.
+/// if it's in the database.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Premiered `VideoPlayer.position(number).Premiered`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.Trailer`</b>,
+/// \anchor VideoPlayer_Trailer
+/// _string_,
+/// @return The path to the trailer of the currently playing movie\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).Trailer`</b>,
+/// \anchor VideoPlayer_Offset_Trailer
+/// _string_,
+/// @return The path to the trailer of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_Trailer `VideoPlayer.offset(number).Title`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).Trailer`</b>,
+/// \anchor VideoPlayer_Position_Trailer
+/// _string_,
+/// @return The path to the trailer of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_Trailer `VideoPlayer.position(number).Trailer`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.LastPlayed`</b>,
+/// \anchor VideoPlayer_LastPlayed
+/// _string_,
+/// @return The last play date of current playing Video\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).LastPlayed`</b>,
+/// \anchor VideoPlayer_Offset_LastPlayed
+/// _string_,
+/// @return The last play date of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_LastPlayed `VideoPlayer.offset(number).LastPlayed`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).LastPlayed`</b>,
+/// \anchor VideoPlayer_Position_LastPlayed
+/// _string_,
+/// @return The last play date of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_LastPlayed `VideoPlayer.position(number).LastPlayed`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.PlayCount`</b>,
+/// \anchor VideoPlayer_PlayCount
+/// _string_,
+/// @return The playcount of current playing Video\, if it's in the database.
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).PlayCount`</b>,
+/// \anchor VideoPlayer_Offset_PlayCount
+/// _string_,
+/// @return The playcount of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_PlayCount `VideoPlayer.offset(number).PlayCount`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).PlayCount`</b>,
+/// \anchor VideoPlayer_Position_PlayCount
+/// _string_,
+/// @return The playcount of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_PlayCount `VideoPlayer.position(number).PlayCount`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.VideoCodec`</b>,
+/// \anchor VideoPlayer_VideoCodec
+/// _string_,
+/// @return The video codec of the currently playing video (common values: see
+/// \ref ListItem_VideoCodec "ListItem.VideoCodec").
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.VideoResolution`</b>,
+/// \anchor VideoPlayer_VideoResolution
+/// _string_,
+/// @return The video resolution of the currently playing video (possible
+/// values: see \ref ListItem_VideoResolution "ListItem.VideoResolution").
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.VideoAspect`</b>,
+/// \anchor VideoPlayer_VideoAspect
+/// _string_,
+/// @return The aspect ratio of the currently playing video (possible values:
+/// see \ref ListItem_VideoAspect "ListItem.VideoAspect").
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.AudioCodec`</b>,
+/// \anchor VideoPlayer_AudioCodec
+/// _string_,
+/// @return The audio codec of the currently playing video\, optionally 'n'
+/// defines the number of the audiostream (common values: see
+/// \ref ListItem_AudioCodec "ListItem.AudioCodec").
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.AudioChannels`</b>,
+/// \anchor VideoPlayer_AudioChannels
+/// _string_,
+/// @return The number of audio channels of the currently playing video
+/// (possible values: see \ref ListItem_AudioChannels "ListItem.AudioChannels").
+/// <p><hr>
+/// @skinning_v16 **[Infolabel Updated]** \link VideoPlayer_AudioChannels `VideoPlayer.AudioChannels`\endlink
+/// if a video contains no audio\, these infolabels will now return empty.
+/// (they used to return 0)
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.AudioLanguage`</b>,
+/// \anchor VideoPlayer_AudioLanguage
+/// _string_,
+/// @return The language of the audio of the currently playing video(possible
+/// values: see \ref ListItem_AudioLanguage "ListItem.AudioLanguage").
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link VideoPlayer_AudioLanguage `VideoPlayer.AudioLanguage`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.SubtitlesLanguage`</b>,
+/// \anchor VideoPlayer_SubtitlesLanguage
+/// _string_,
+/// @return The language of the subtitle of the currently playing video
+/// (possible values: see \ref ListItem_SubtitleLanguage "ListItem.SubtitleLanguage").
+/// @note `VideoPlayer.SubtitlesLanguage` holds the language of the next available
+/// subtitle stream if subtitles are disabled in the player
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link VideoPlayer_SubtitlesLanguage `VideoPlayer.SubtitlesLanguage`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.StereoscopicMode`</b>,
+/// \anchor VideoPlayer_StereoscopicMode
+/// _string_,
+/// @return The stereoscopic mode of the currently playing video (possible
+/// values: see \ref ListItem_StereoscopicMode "ListItem.StereoscopicMode").
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link VideoPlayer_StereoscopicMode `VideoPlayer.StereoscopicMode`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.StartTime`</b>,
+/// \anchor VideoPlayer_StartTime
+/// _string_,
+/// @return The start date and time of the currently playing epg event or recording (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.EndTime`</b>,
+/// \anchor VideoPlayer_EndTime
+/// _string_,
+/// @return The end date and time of the currently playing epg event or recording (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.NextTitle`</b>,
+/// \anchor VideoPlayer_NextTitle
+/// _string_,
+/// @return The title of the programme that will be played next (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.NextGenre`</b>,
+/// \anchor VideoPlayer_NextGenre
+/// _string_,
+/// @return The genre of the programme that will be played next (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.NextPlot`</b>,
+/// \anchor VideoPlayer_NextPlot
+/// _string_,
+/// @return The plot of the programme that will be played next (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.NextPlotOutline`</b>,
+/// \anchor VideoPlayer_NextPlotOutline
+/// _string_,
+/// @return The plot outline of the programme that will be played next (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.NextStartTime`</b>,
+/// \anchor VideoPlayer_NextStartTime
+/// _string_,
+/// @return The start time of the programme that will be played next (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.NextEndTime`</b>,
+/// \anchor VideoPlayer_NextEndTime
+/// _string_,
+/// @return The end time of the programme that will be played next (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.NextDuration`</b>,
+/// \anchor VideoPlayer_NextDuration
+/// _string_,
+/// @return The duration of the programme that will be played next (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.ChannelName`</b>,
+/// \anchor VideoPlayer_ChannelName
+/// _string_,
+/// @return The name of the currently tuned channel (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.ChannelNumberLabel`</b>,
+/// \anchor VideoPlayer_ChannelNumberLabel
+/// _string_,
+/// @return The channel and subchannel number of the tv channel that's currently playing (PVR).
+/// <p><hr>
+/// @skinning_v14 **[New Infolabel]** \link VideoPlayer_ChannelNumberLabel `VideoPlayer.ChannelNumberLabel`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.ChannelGroup`</b>,
+/// \anchor VideoPlayer_ChannelGroup
+/// _string_,
+/// @return The group of the currently tuned channel (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.ParentalRating`</b>,
+/// \anchor VideoPlayer_ParentalRating
+/// _string_,
+/// @return The parental rating of the currently playing programme (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.DBID`</b>,
+/// \anchor VideoPlayer_DBID
+/// _string_,
+/// @return The database id of the currently playing video
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link VideoPlayer_DBID `VideoPlayer.DBID`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.offset(number).DBID`</b>,
+/// \anchor VideoPlayer_Offset_DBID
+/// _string_,
+/// @return The database id of the video which has an offset `number` with respect to the currently playing video.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Offset_DBID `VideoPlayer.offset(number).DBID`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.position(number).DBID`</b>,
+/// \anchor VideoPlayer_Position_DBID
+/// _string_,
+/// @return The database id of the video which has an offset `number` with respect to the start of the playlist.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_Position_DBID `VideoPlayer.position(number).DBID`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.UniqueID(name)`</b>,
+/// \anchor VideoPlayer_UniqueID
+/// _string_,
+/// @return The scraped metadata id of current movie\, if it's in the database.
+/// @param name - the name of the metadata provider.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_UniqueID `VideoPlayer.UniqueID(name)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.TvShowDBID`</b>,
+/// \anchor VideoPlayer_TvShowDBID
+/// _string_,
+/// @return The database id of the TvShow for the currently playing Episode
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link VideoPlayer_TvShowDBID `VideoPlayer.TvShowDBID`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.AudioStreamCount`</b>,
+/// \anchor VideoPlayer_AudioStreamCount
+/// _integer_,
+/// @return The number of audio streams of the currently playing video.
+/// @note If the video contains no audio streams it returns 0.
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link VideoPlayer_AudioStreamCount `VideoPlayer.AudioStreamCount`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`VideoPlayer.HdrType`</b>,
+/// \anchor VideoPlayer_HdrType
+/// _string_,
+/// @return String containing the name of the detected HDR type or empty if not HDR. See \ref StreamHdrType for the list of possible values.
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link VideoPlayer_HdrType `VideoPlayer.HdrType`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+// clang-format off
+const infomap videoplayer[] = {{ "title", VIDEOPLAYER_TITLE },
+ { "genre", VIDEOPLAYER_GENRE },
+ { "country", VIDEOPLAYER_COUNTRY },
+ { "originaltitle", VIDEOPLAYER_ORIGINALTITLE },
+ { "director", VIDEOPLAYER_DIRECTOR },
+ { "year", VIDEOPLAYER_YEAR },
+ { "cover", VIDEOPLAYER_COVER },
+ { "usingoverlays", VIDEOPLAYER_USING_OVERLAYS },
+ { "isfullscreen", VIDEOPLAYER_ISFULLSCREEN },
+ { "hasmenu", VIDEOPLAYER_HASMENU },
+ { "playlistlength", VIDEOPLAYER_PLAYLISTLEN },
+ { "playlistposition", VIDEOPLAYER_PLAYLISTPOS },
+ { "plot", VIDEOPLAYER_PLOT },
+ { "plotoutline", VIDEOPLAYER_PLOT_OUTLINE },
+ { "episode", VIDEOPLAYER_EPISODE },
+ { "season", VIDEOPLAYER_SEASON },
+ { "rating", VIDEOPLAYER_RATING },
+ { "ratingandvotes", VIDEOPLAYER_RATING_AND_VOTES },
+ { "userrating", VIDEOPLAYER_USER_RATING },
+ { "votes", VIDEOPLAYER_VOTES },
+ { "tvshowtitle", VIDEOPLAYER_TVSHOW },
+ { "premiered", VIDEOPLAYER_PREMIERED },
+ { "studio", VIDEOPLAYER_STUDIO },
+ { "mpaa", VIDEOPLAYER_MPAA },
+ { "top250", VIDEOPLAYER_TOP250 },
+ { "cast", VIDEOPLAYER_CAST },
+ { "castandrole", VIDEOPLAYER_CAST_AND_ROLE },
+ { "artist", VIDEOPLAYER_ARTIST },
+ { "album", VIDEOPLAYER_ALBUM },
+ { "writer", VIDEOPLAYER_WRITER },
+ { "tagline", VIDEOPLAYER_TAGLINE },
+ { "hasinfo", VIDEOPLAYER_HAS_INFO },
+ { "trailer", VIDEOPLAYER_TRAILER },
+ { "videocodec", VIDEOPLAYER_VIDEO_CODEC },
+ { "videoresolution", VIDEOPLAYER_VIDEO_RESOLUTION },
+ { "videoaspect", VIDEOPLAYER_VIDEO_ASPECT },
+ { "videobitrate", VIDEOPLAYER_VIDEO_BITRATE },
+ { "audiocodec", VIDEOPLAYER_AUDIO_CODEC },
+ { "audiochannels", VIDEOPLAYER_AUDIO_CHANNELS },
+ { "audiobitrate", VIDEOPLAYER_AUDIO_BITRATE },
+ { "audiolanguage", VIDEOPLAYER_AUDIO_LANG },
+ { "hasteletext", VIDEOPLAYER_HASTELETEXT },
+ { "lastplayed", VIDEOPLAYER_LASTPLAYED },
+ { "playcount", VIDEOPLAYER_PLAYCOUNT },
+ { "hassubtitles", VIDEOPLAYER_HASSUBTITLES },
+ { "subtitlesenabled", VIDEOPLAYER_SUBTITLESENABLED },
+ { "subtitleslanguage",VIDEOPLAYER_SUBTITLES_LANG },
+ { "starttime", VIDEOPLAYER_STARTTIME },
+ { "endtime", VIDEOPLAYER_ENDTIME },
+ { "nexttitle", VIDEOPLAYER_NEXT_TITLE },
+ { "nextgenre", VIDEOPLAYER_NEXT_GENRE },
+ { "nextplot", VIDEOPLAYER_NEXT_PLOT },
+ { "nextplotoutline", VIDEOPLAYER_NEXT_PLOT_OUTLINE },
+ { "nextstarttime", VIDEOPLAYER_NEXT_STARTTIME },
+ { "nextendtime", VIDEOPLAYER_NEXT_ENDTIME },
+ { "nextduration", VIDEOPLAYER_NEXT_DURATION },
+ { "channelname", VIDEOPLAYER_CHANNEL_NAME },
+ { "channelnumberlabel", VIDEOPLAYER_CHANNEL_NUMBER },
+ { "channelgroup", VIDEOPLAYER_CHANNEL_GROUP },
+ { "hasepg", VIDEOPLAYER_HAS_EPG },
+ { "parentalrating", VIDEOPLAYER_PARENTAL_RATING },
+ { "isstereoscopic", VIDEOPLAYER_IS_STEREOSCOPIC },
+ { "stereoscopicmode", VIDEOPLAYER_STEREOSCOPIC_MODE },
+ { "canresumelivetv", VIDEOPLAYER_CAN_RESUME_LIVE_TV },
+ { "imdbnumber", VIDEOPLAYER_IMDBNUMBER },
+ { "episodename", VIDEOPLAYER_EPISODENAME },
+ { "dbid", VIDEOPLAYER_DBID },
+ { "uniqueid", VIDEOPLAYER_UNIQUEID },
+ { "tvshowdbid", VIDEOPLAYER_TVSHOWDBID },
+ { "audiostreamcount", VIDEOPLAYER_AUDIOSTREAMCOUNT },
+ { "hdrtype", VIDEOPLAYER_HDR_TYPE },
+ { "art", VIDEOPLAYER_ART},
+};
+// clang-format on
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_RetroPlayer RetroPlayer
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`RetroPlayer.VideoFilter`</b>,
+/// \anchor RetroPlayer_VideoFilter
+/// _string_,
+/// @return The video filter of the currently-playing game.
+/// The following values are possible:
+/// - nearest (Nearest neighbor\, i.e. pixelate)
+/// - linear (Bilinear filtering\, i.e. smooth blur)
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link RetroPlayer_VideoFilter `RetroPlayer.VideoFilter`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RetroPlayer.StretchMode`</b>,
+/// \anchor RetroPlayer_StretchMode
+/// _string_,
+/// @return The stretch mode of the currently-playing game.
+/// The following values are possible:
+/// - normal (Show the game normally)
+/// - 4:3 (Stretch to a 4:3 aspect ratio)
+/// - fullscreen (Stretch to the full viewing area)
+/// - original (Shrink to the original resolution)
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link RetroPlayer_StretchMode `RetroPlayer.StretchMode`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RetroPlayer.VideoRotation`</b>,
+/// \anchor RetroPlayer_VideoRotation
+/// _integer_,
+/// @return The video rotation of the currently-playing game
+/// in degrees counter-clockwise.
+/// The following values are possible:
+/// - 0
+/// - 90 (Shown in the GUI as 270 degrees)
+/// - 180
+/// - 270 (Shown in the GUI as 90 degrees)
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link RetroPlayer_VideoRotation `RetroPlayer.VideoRotation`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap retroplayer[] =
+{
+ { "videofilter", RETROPLAYER_VIDEO_FILTER},
+ { "stretchmode", RETROPLAYER_STRETCH_MODE},
+ { "videorotation", RETROPLAYER_VIDEO_ROTATION},
+};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Container Container
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Container.HasFiles`</b>,
+/// \anchor Container_HasFiles
+/// _boolean_,
+/// @return **True** if the container contains files.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.HasFolders`</b>,
+/// \anchor Container_HasFolders
+/// _boolean_,
+/// @return **True** if the container contains folders.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.IsStacked`</b>,
+/// \anchor Container_IsStacked
+/// _boolean_,
+/// @return **True** if the container is currently in stacked mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.FolderPath`</b>,
+/// \anchor Container_FolderPath
+/// _string_,
+/// @return The complete path of currently displayed folder.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.FolderName`</b>,
+/// \anchor Container_FolderName
+/// _string_,
+/// @return The top most folder in currently displayed folder.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.PluginName`</b>,
+/// \anchor Container_PluginName
+/// _string_,
+/// @return The current plugins base folder name.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.PluginCategory`</b>,
+/// \anchor Container_PluginCategory
+/// _string_,
+/// @return The current plugins category (set by the scripter).
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Container_PluginCategory `Container.PluginCategory`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.Viewmode`</b>,
+/// \anchor Container_Viewmode
+/// _string_,
+/// @return The current viewmode (list\, icons etc).
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.ViewCount`</b>,
+/// \anchor Container_ViewCount
+/// _integer_,
+/// @return The number of available skin view modes for the current container listing.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Container_ViewCount `Container.ViewCount`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.Totaltime`</b>,
+/// \anchor Container_Totaltime
+/// _string_,
+/// @return The total time of all items in the current container.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.TotalWatched`</b>,
+/// \anchor Container_TotalWatched
+/// _string_,
+/// @return The number of watched items in the container.
+/// @param id - [opt] if not supplied the current container will be used.
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link Container_TotalWatched `Container(id).TotalWatched`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.TotalUnWatched`</b>,
+/// \anchor Container_TotalUnWatched
+/// _string_,
+/// @return The number of unwatched items in the container.
+/// @param id - [opt] if not supplied the current container will be used.
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link Container_TotalUnWatched `Container(id).TotalUnWatched`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.HasThumb`</b>,
+/// \anchor Container_HasThumb
+/// _boolean_,
+/// @return **True** if the current container you are in has a thumb assigned
+/// to it.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.SortOrder`</b>,
+/// \anchor Container_SortOrder
+/// _string_,
+/// @return The current sort order (Ascending/Descending).
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link Container_SortOrder `Container.SortOrder`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.CanFilter`</b>,
+/// \anchor Container_CanFilter
+/// _boolean_,
+/// @return **True** when the current container can be filtered.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.CanFilterAdvanced`</b>,
+/// \anchor Container_CanFilterAdvanced
+/// _boolean_,
+/// @return **True** when advanced filtering can be applied to the current container.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.Filtered`</b>,
+/// \anchor Container_Filtered
+/// _boolean_,
+/// @return **True** when a mediafilter is applied to the current container.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.ShowPlot`</b>,
+/// \anchor Container_ShowPlot
+/// _string_,
+/// @return The TV Show plot of the current container and can be used at
+/// season and episode level.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.ShowTitle`</b>,
+/// \anchor Container_ShowTitle
+/// _string_,
+/// @return The TV Show title of the current container and can be used at
+/// season and episode level.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Container_ShowTitle `Container.ShowTitle`\endlink
+/// <p>
+/// }
+const infomap mediacontainer[] = {{ "hasfiles", CONTAINER_HASFILES },
+ { "hasfolders", CONTAINER_HASFOLDERS },
+ { "isstacked", CONTAINER_STACKED },
+ { "folderpath", CONTAINER_FOLDERPATH },
+ { "foldername", CONTAINER_FOLDERNAME },
+ { "pluginname", CONTAINER_PLUGINNAME },
+ { "plugincategory", CONTAINER_PLUGINCATEGORY },
+ { "viewmode", CONTAINER_VIEWMODE },
+ { "viewcount", CONTAINER_VIEWCOUNT },
+ { "totaltime", CONTAINER_TOTALTIME },
+ { "totalwatched", CONTAINER_TOTALWATCHED },
+ { "totalunwatched", CONTAINER_TOTALUNWATCHED },
+ { "hasthumb", CONTAINER_HAS_THUMB },
+ { "sortorder", CONTAINER_SORT_ORDER },
+ { "canfilter", CONTAINER_CAN_FILTER },
+ { "canfilteradvanced",CONTAINER_CAN_FILTERADVANCED },
+ { "filtered", CONTAINER_FILTERED },
+ { "showplot", CONTAINER_SHOWPLOT },
+ { "showtitle", CONTAINER_SHOWTITLE }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \table_row3{ <b>`Container(id).OnNext`</b>,
+/// \anchor Container_OnNext
+/// _boolean_,
+/// @return **True** if the container with id (or current container if id is
+/// omitted) is moving to the next item. Allows views to be
+/// custom-designed (such as 3D coverviews etc.)
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).OnScrollNext`</b>,
+/// \anchor Container_OnScrollNext
+/// _boolean_,
+/// @return **True** if the container with id (or current container if id is
+/// omitted) is scrolling to the next item. Differs from \ref Container_OnNext "OnNext" in that
+/// \ref Container_OnNext "OnNext" triggers on movement even if there is no scroll involved.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).OnPrevious`</b>,
+/// \anchor Container_OnPrevious
+/// _boolean_,
+/// @return **True** if the container with id (or current container if id is
+/// omitted) is moving to the previous item. Allows views to be
+/// custom-designed (such as 3D coverviews etc).
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).OnScrollPrevious`</b>,
+/// \anchor Container_OnScrollPrevious
+/// _boolean_,
+/// @return **True** if the container with id (or current container if id is
+/// omitted) is scrolling to the previous item. Differs from \ref Container_OnPrevious "OnPrevious" in
+/// that \ref Container_OnPrevious "OnPrevious" triggers on movement even if there is no scroll involved.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).NumPages`</b>,
+/// \anchor Container_NumPages
+/// _integer_,
+/// @return The number of pages in the container with given id. If no id is specified it
+/// grabs the current container.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).NumItems`</b>,
+/// \anchor Container_NumItems
+/// _integer_,
+/// @return The number of items in the container or grouplist with given id excluding parent folder item.
+/// @note If no id is specified it grabs the current container.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).NumAllItems`</b>,
+/// \anchor Container_NumAllItems
+/// _integer_,
+/// @return The number of all items in the container or grouplist with given id including parent folder item.
+/// @note If no id is specified it grabs the current container.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link Container_NumAllItems `Container(id).NumAllItems`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).NumNonFolderItems`</b>,
+/// \anchor Container_NumNonFolderItems
+/// _integer_,
+/// @return The Number of items in the container or grouplist with given id excluding all folder items.
+/// @note **Example:** pvr recordings folders\, parent ".." folder).
+/// If no id is specified it grabs the current container.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link Container_NumNonFolderItems `Container(id).NumNonFolderItems`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).CurrentPage`</b>,
+/// \anchor Container_CurrentPage
+/// _string_,
+/// @return THe current page in the container with given id.
+/// @note If no id is specified it grabs the current container.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).CurrentItem`</b>,
+/// \anchor Container_CurrentItem
+/// _integer_,
+/// @return The current item in the container or grouplist with given id.
+/// @note If no id is specified it grabs the current container.
+/// <p><hr>
+/// @skinning_v15 **[New Infolabel]** \link Container_CurrentItem `Container(id).CurrentItem`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).Scrolling`</b>,
+/// \anchor Container_Scrolling
+/// _boolean_,
+/// @return **True** if the user is currently scrolling through the container
+/// with id (or current container if id is omitted).
+/// @note This is slightly delayed from the actual scroll start. Use
+/// \ref Container_OnScrollNext "Container(id).OnScrollNext" or
+/// \ref Container_OnScrollPrevious "Container(id).OnScrollPrevious" to trigger animations
+/// immediately on scroll.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).HasNext`</b>,
+/// \anchor Container_HasNext
+/// _boolean_,
+/// @return **True** if the container or textbox with id (id) has a next page.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).HasParent`</b>,
+/// \anchor Container_HasParent
+/// _boolean_,
+/// @return **True** when the container with given id contains a parent ('..') item.
+/// @note If no id is specified it grabs the current container.
+/// <p><hr>
+/// @skinning_v16 **[New Boolean Condition]** \link Container_HasParent `Container.HasParent`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).HasPrevious`</b>,
+/// \anchor Container_HasPrevious
+/// _boolean_,
+/// @return **True** if the container or textbox with id (id) has a previous page.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).IsUpdating`</b>,
+/// \anchor Container_IsUpdating
+/// _boolean_,
+/// @return **True** if the container with dynamic list content is currently updating.
+/// }
+const infomap container_bools[] ={{ "onnext", CONTAINER_MOVE_NEXT },
+ { "onprevious", CONTAINER_MOVE_PREVIOUS },
+ { "onscrollnext", CONTAINER_SCROLL_NEXT },
+ { "onscrollprevious", CONTAINER_SCROLL_PREVIOUS },
+ { "numpages", CONTAINER_NUM_PAGES },
+ { "numitems", CONTAINER_NUM_ITEMS },
+ { "numnonfolderitems", CONTAINER_NUM_NONFOLDER_ITEMS },
+ { "numallitems", CONTAINER_NUM_ALL_ITEMS },
+ { "currentpage", CONTAINER_CURRENT_PAGE },
+ { "currentitem", CONTAINER_CURRENT_ITEM },
+ { "scrolling", CONTAINER_SCROLLING },
+ { "hasnext", CONTAINER_HAS_NEXT },
+ { "hasparent", CONTAINER_HAS_PARENT_ITEM },
+ { "hasprevious", CONTAINER_HAS_PREVIOUS },
+ { "isupdating", CONTAINER_ISUPDATING }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \table_row3{ <b>`Container(id).Row`</b>,
+/// \anchor Container_Row
+/// _integer_,
+/// @return The row number of the focused position in a panel container.
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link Container_Row `Container(id).Row`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).Row(parameter)`</b>,
+/// \anchor Container_Row_parameter
+/// _boolean_,
+/// @return **True** if the row number of the focused position matches the specified parameter.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).Column`</b>,
+/// \anchor Container_Column
+/// _integer_,
+/// @return The column number of the focused position in a panel container.
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link Container_Column `Container(id).Column`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).Column(parameter)`</b>,
+/// \anchor Container_Column_parameter
+/// _boolean_,
+/// @return **True** if the column number of the focused position matches the specified parameter.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).Position`</b>,
+/// \anchor Container_Position
+/// _integer_,
+/// @return The current focused position of container / grouplist (id) as a
+/// numeric label.
+/// <p><hr>
+/// @skinning_v16 **[Infolabel Updated]** \link Container_Position `Container(id).Position`\endlink
+/// now also returns the position for items inside a grouplist.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).Position(parameter)`</b>,
+/// \anchor Container_Position_parameter
+/// _boolean_,
+/// @return **True** if the container with id (or current container if id is omitted) is focused on the specified position.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).SubItem(item_number)`</b>,
+/// \anchor Container_SubItem
+/// _boolean_,
+/// @return **True** if the container with id (or current container if id is omitted) is focused on the specified subitem.
+/// @note If no id is specified it grabs the current container.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).HasFocus(item_number)`</b>,
+/// \anchor Container_HasFocus
+/// _boolean_,
+/// @return **True** if the container with id (or current container if id is
+/// omitted) has static content and is focused on the item with id
+/// item_number.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.SortMethod`</b>,
+/// \anchor Container_SortMethod
+/// _string_,
+/// @return The current sort method (returns a localized value).
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.SortMethod(sortid)`</b>,
+/// \anchor Container_SortMethod_sortid
+/// _boolean_,
+/// @return **True** if the current sort method matches the specified SortID (see \ref List_of_sort_methods "SortUtils").
+/// <p>
+/// }
+const infomap container_ints[] = {{ "row", CONTAINER_ROW },
+ { "column", CONTAINER_COLUMN },
+ { "position", CONTAINER_POSITION },
+ { "subitem", CONTAINER_SUBITEM },
+ { "hasfocus", CONTAINER_HAS_FOCUS },
+ { "sortmethod", CONTAINER_SORT_METHOD },
+};
+
+/// \page modules__infolabels_boolean_conditions
+/// \table_row3{ <b>`Container.Property(addoncategory)`</b>,
+/// \anchor Container_Property_addoncategory
+/// _string_,
+/// @return The current add-on category.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.Property(reponame)`</b>,
+/// \anchor Container_Property_reponame
+/// _string_,
+/// @return The current add-on repository name.
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.Content`</b>,
+/// \anchor Container_Content
+/// _string_,
+/// @return The content of the current container.
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link Container_Content `Container.Content`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).ListItem(offset).Property`</b>,
+/// \anchor Container_ListItem_property
+/// _string_,
+/// @return the property of the ListItem with a given offset.
+/// @param offset - The offset for the listitem.
+/// @note `Property` has to be replaced with `Label`\, `Label2`\, `Icon` etc.
+/// @note **Example:** `Container(50).Listitem(2).Label `
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).ListItemNoWrap(offset).Property`</b>,
+/// \anchor Container_ListItemNoWrap
+/// _string_,
+/// @return the same as \link Container_ListItem_property `Container(id).ListItem(offset).Property` \endlink
+/// but it won't wrap.
+/// @param offset - The offset for the listitem.
+/// @note That means if the last item of a list is focused\, `ListItemNoWrap(1)`
+/// will be empty while `ListItem(1)` will return the first item of the list.
+/// `Property` has to be replaced with `Label`\, `Label2`\, `Icon` etc.
+/// @note **Example:** `Container(50).ListitemNoWrap(1).Plot`
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).ListItemPosition(x).[infolabel]`</b>,
+/// \anchor Container_ListItemPosition
+/// _string_,
+/// @return The infolabel for an item in a Container.
+/// @param x - the position in the container relative to the cursor position.
+/// @note **Example:** `Container(50).ListItemPosition(4).Genre`
+/// <p>
+/// }
+/// \table_row3{ <b>`Container(id).ListItemAbsolute(x).[infolabel]`</b>,
+/// \anchor Container_ListItemAbsolute
+/// _string_,
+/// @return The infolabel for an item in a Container.
+/// @param x - the absolute position in the container.
+/// @note **Example:** `Container(50).ListItemAbsolute(4).Genre`
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link Container_ListItemAbsolute `Container(id).ListItemAbsolute(x).[infolabel]`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.Content(parameter)`</b>,
+/// \anchor Container_Content_parameter
+/// _string_,
+/// @return **True** if the current container you are in contains the following:
+/// - <b>files</b>
+/// - <b>songs</b>
+/// - <b>artists</b>
+/// - <b>albums</b>
+/// - <b>movies</b>
+/// - <b>tvshows</b>
+/// - <b>seasons</b>
+/// - <b>episodes</b>
+/// - <b>musicvideos</b>
+/// - <b>genres</b>
+/// - <b>years</b>
+/// - <b>actors</b>
+/// - <b>playlists</b>
+/// - <b>plugins</b>
+/// - <b>studios</b>
+/// - <b>directors</b>
+/// - <b>sets</b>
+/// - <b>tags</b>
+/// @note These currently only work in the Video and Music
+/// Library or unless a Plugin has set the value) also available are
+/// Addons true when a list of add-ons is shown LiveTV true when a
+/// htsp (tvheadend) directory is shown
+/// <p>
+/// }
+/// \table_row3{ <b>`Container.Art(type)`</b>,
+/// \anchor Container_Art
+/// _string_,
+/// @return The path to the art image file for the given type of the current container.
+/// @param type - the art type to request.
+/// @todo List of all art types
+/// <p><hr>
+/// @skinning_v16 **[Infolabel Updated]** \link Container_Art `Container.Art(type)`\endlink
+/// <b>set.fanart</b> as possible type value.
+/// @skinning_v15 **[New Infolabel]** \link Container_Art `Container.Art(type)`\endlink
+/// <p>
+/// }
+///
+const infomap container_str[] = {{ "property", CONTAINER_PROPERTY },
+ { "content", CONTAINER_CONTENT },
+ { "art", CONTAINER_ART }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \table_row3{ <b>`Container.SortDirection(direction)`</b>,
+/// \anchor Container_SortDirection
+/// _boolean_,
+/// @return **True** if the sort direction of a container equals direction.
+/// @param direction - The direction to check. It can be:
+/// - <b>ascending</b>
+/// - <b>descending</b>
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_ListItem ListItem
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`ListItem.Thumb`</b>,
+/// \anchor ListItem_Thumb
+/// _string_,
+/// @return The thumbnail (if it exists) of the currently selected item
+/// in a list or thumb control.
+/// @deprecated but still available\, returns
+/// the same as \ref ListItem_Art_Type "ListItem.Art(thumb)"
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Icon`</b>,
+/// \anchor ListItem_Icon
+/// _string_,
+/// @return The thumbnail (if it exists) of the currently selected item in a list or thumb control.
+/// @note If no thumbnail image exists\, it will show the icon.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.ActualIcon`</b>,
+/// \anchor ListItem_ActualIcon
+/// _string_,
+/// @return The icon of the currently selected item in a list or thumb control.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Overlay`</b>,
+/// \anchor ListItem_Overlay
+/// _string_,
+/// @return The overlay icon status of the currently selected item in a list or thumb control.
+/// - compressed file -- OverlayRAR.png
+/// - watched -- OverlayWatched.png
+/// - unwatched -- OverlayUnwatched.png
+/// - locked -- OverlayLocked.png
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsFolder`</b>,
+/// \anchor ListItem_IsFolder
+/// _boolean_,
+/// @return **True** if the current ListItem is a folder.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsPlaying`</b>,
+/// \anchor ListItem_IsPlaying
+/// _boolean_,
+/// @return **True** if the current ListItem.* info labels and images are
+/// currently Playing media.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsResumable`</b>,
+/// \anchor ListItem_IsResumable
+/// _boolean_,
+/// @return **True** when the current ListItem has been partially played.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsCollection`</b>,
+/// \anchor ListItem_IsCollection
+/// _boolean_,
+/// @return **True** when the current ListItem is a movie set.
+/// <p><hr>
+/// @skinning_v15 **[New Boolean Condition]** \link ListItem_IsCollection `ListItem.IsCollection`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsSelected`</b>,
+/// \anchor ListItem_IsSelected
+/// _boolean_,
+/// @return **True** if the current ListItem is selected (f.e. currently playing
+/// in playlist window).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.HasEpg`</b>,
+/// \anchor ListItem_HasEpg
+/// _boolean_,
+/// @return **True** when the selected programme has epg info (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.HasTimer`</b>,
+/// \anchor ListItem_HasTimer
+/// _boolean_,
+/// @return **True** when a recording timer has been set for the selected
+/// programme (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsRecording`</b>,
+/// \anchor ListItem_IsRecording
+/// _boolean_,
+/// @return **True** when the selected programme is being recorded (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsPlayable`</b>,
+/// \anchor ListItem_IsPlayable
+/// _boolean_,
+/// @return **True** when the selected programme can be played (PVR)
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link ListItem_IsPlayable `ListItem.IsPlayable`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.HasArchive`</b>,
+/// \anchor ListItem_HasArchive
+/// _boolean_,
+/// @return **True** when the selected channel has a server-side back buffer (PVR)
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link ListItem_HasArchive `ListItem.HasArchive`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsEncrypted`</b>,
+/// \anchor ListItem_IsEncrypted
+/// _boolean_,
+/// @return **True** when the selected programme is encrypted (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsStereoscopic`</b>,
+/// \anchor ListItem_IsStereoscopic
+/// _boolean_,
+/// @return **True** when the selected video is a 3D (stereoscopic) video.
+/// <p><hr>
+/// @skinning_v13 **[New Boolean Condition]** \link ListItem_IsStereoscopic `ListItem.IsStereoscopic`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(IsSpecial)`</b>,
+/// \anchor ListItem_Property_IsSpecial
+/// _boolean_,
+/// @return **True** if the current Season/Episode is a Special.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(DateLabel)`</b>,
+/// \anchor ListItem_Property_DateLabel
+/// _boolean_,
+/// @return **True** if the item is a date label\, returns false if the item is a time label.
+/// @note Can be used in the rulerlayout of the epggrid control.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.IsEnabled)`</b>,
+/// \anchor ListItem_Property_AddonIsEnabled
+/// _boolean_,
+/// @return **True** when the selected addon is enabled (for use in the addon
+/// info dialog only).
+/// <p><hr>
+/// @skinning_v17 **[Boolean Condition Updated]** \link ListItem_Property_AddonIsEnabled `ListItem.Property(Addon.IsEnabled)`\endlink
+/// replaces `ListItem.Property(Addon.Enabled)`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.IsInstalled)`</b>,
+/// \anchor ListItem_Property_AddonIsInstalled
+/// _boolean_,
+/// @return **True** when the selected addon is installed (for use in the addon
+/// info dialog only).
+/// <p><hr>
+/// @skinning_v17 **[Boolean Condition Updated]** \link ListItem_Property_AddonIsInstalled `ListItem.Property(Addon.IsInstalled)`\endlink
+/// replaces `ListItem.Property(Addon.Installed)`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.HasUpdate)`</b>,
+/// \anchor ListItem_Property_AddonHasUpdate
+/// _boolean_,
+/// @return **True** when there's an update available for the selected addon.
+/// <p><hr>
+/// @skinning_v17 **[Boolean Condition Updated]** \link ListItem_Property_AddonHasUpdate `ListItem.Property(Addon.HasUpdate)`\endlink
+/// replaces `ListItem.Property(Addon.UpdateAvail)`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsAutoUpdateable`</b>,
+/// \anchor ListItem_IsAutoUpdateable
+/// _boolean_,
+/// @return **True** if this add-on can be updated automatically.
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link ListItem_IsAutoUpdateable `ListItem.IsAutoUpdateable`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.IsFromOfficialRepo)`</b>,
+/// \anchor ListItem_Property_AddonIsFromOfficialRepo
+/// _boolean_,
+/// @return **True** if this add-on is from an official repository.
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link ListItem_Property_AddonIsFromOfficialRepo `ListItem.Property(Addon.IsFromOfficialRepo)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.IsBinary)`</b>,
+/// \anchor ListItem_Property_AddonIsBinary
+/// _boolean_,
+/// @return **True** if this add-on is a binary addon.
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link ListItem_Property_AddonIsBinary `ListItem.Property(Addon.IsBinary)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.IsUpdate)`</b>,
+/// \anchor ListItem_Property_AddonIsUpdate
+/// _boolean_,
+/// @return **True** if this add-on is a valid update of an installed outdated add-on.
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link ListItem_Property_AddonIsUpdate `ListItem.Property(Addon.IsUpdate)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.ValidUpdateOrigin)`</b>,
+/// \anchor ListItem_Property_ValidUpdateOrigin
+/// _string_,
+/// @return The origin string of a valid update for the addon. Empty string if there is no valid update available.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_Property_ValidUpdateOrigin `ListItem.Property(Addon.ValidUpdateOrigin)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.ValidUpdateVersion)`</b>,
+/// \anchor ListItem_Property_ValidUpdateVersion
+/// _string_,
+/// @return The version string of a valid update for the addon. Empty string if there is no valid update available.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_Property_ValidUpdateVersion `ListItem.Property(Addon.ValidUpdateVersion)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Label`</b>,
+/// \anchor ListItem_Label
+/// _string_,
+/// @return The left label of the currently selected item in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Label2`</b>,
+/// \anchor ListItem_Label2
+/// _string_,
+/// @return The right label of the currently selected item in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Title`</b>,
+/// \anchor ListItem_Title
+/// _string_,
+/// @return The title of the currently selected song\, movie\, game in a container.
+/// <p><hr>
+/// @skinning_v18 **[Infolabel Updated]** \link ListItem_Title `ListItem.Title`\endlink extended
+/// to support games
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.OriginalTitle`</b>,
+/// \anchor ListItem_OriginalTitle
+/// _string_,
+/// @return The original title of the currently selected movie in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.SortLetter`</b>,
+/// \anchor ListItem_SortLetter
+/// _string_,
+/// @return The first letter of the current file in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.TrackNumber`</b>,
+/// \anchor ListItem_TrackNumber
+/// _string_,
+/// @return The track number of the currently selected song in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Artist`</b>,
+/// \anchor ListItem_Artist
+/// _string_,
+/// @return The artist of the currently selected song in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AlbumArtist`</b>,
+/// \anchor ListItem_AlbumArtist
+/// _string_,
+/// @return The artist of the currently selected album in a list.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Sortname)`</b>,
+/// \anchor ListItem_Property_Artist_Sortname
+/// _string_,
+/// @return The sortname of the currently selected Artist.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Property_Artist_Sortname `ListItem.Property(Artist_Sortname)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Type)`</b>,
+/// \anchor ListItem_Property_Artist_Type
+/// _string_,
+/// @return The type of the currently selected Artist - person\, group\, orchestra\, choir etc.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Property_Artist_Type `ListItem.Property(Artist_Type)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Gender)`</b>,
+/// \anchor ListItem_Property_Artist_Gender
+/// _string_,
+/// @return The Gender of the currently selected Artist - male\, female\, other.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Property_Artist_Gender `ListItem.Property(Artist_Gender)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Disambiguation)`</b>,
+/// \anchor ListItem_Property_Artist_Disambiguation
+/// _string_,
+/// @return A Brief description of the currently selected Artist that differentiates them
+/// from others with the same name.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Property_Artist_Disambiguation `ListItem.Property(Artist_Disambiguation)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Born)`</b>,
+/// \anchor ListItem_Property_Artist_Born
+/// _string_,
+/// @return The date of Birth of the currently selected Artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Died)`</b>,
+/// \anchor ListItem_Property_Artist_Died
+/// _string_,
+/// @return The date of Death of the currently selected Artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Formed)`</b>,
+/// \anchor ListItem_Property_Artist_Formed
+/// _string_,
+/// @return The formation date of the currently selected Band.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Disbanded)`</b>,
+/// \anchor ListItem_Property_Artist_Disbanded
+/// _string_,
+/// @return The disbanding date of the currently selected Band.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_YearsActive)`</b>,
+/// \anchor ListItem_Property_Artist_YearsActive
+/// _string_,
+/// @return The years the currently selected artist has been active.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Instrument)`</b>,
+/// \anchor ListItem_Property_Artist_Instrument
+/// _string_,
+/// @return The instruments played by the currently selected artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Description)`</b>,
+/// \anchor ListItem_Property_Artist_Description
+/// _string_,
+/// @return A biography of the currently selected artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Mood)`</b>,
+/// \anchor ListItem_Property_Artist_Mood
+/// _string_,
+/// @return The moods of the currently selected artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Style)`</b>,
+/// \anchor ListItem_Property_Artist_Style
+/// _string_,
+/// @return The styles of the currently selected artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Artist_Genre)`</b>,
+/// \anchor ListItem_Property_Artist_Genre
+/// _string_,
+/// @return The genre of the currently selected artist.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Album`</b>,
+/// \anchor ListItem_Album
+/// _string_,
+/// @return The album of the currently selected song in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Album_Mood)`</b>,
+/// \anchor ListItem_Property_Album_Mood
+/// _string_,
+/// @return The moods of the currently selected Album.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Album_Style)`</b>,
+/// \anchor ListItem_Property_Album_Style
+/// _string_,
+/// @return The styles of the currently selected Album.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Album_Theme)`</b>,
+/// \anchor ListItem_Property_Album_Theme
+/// _string_,
+/// @return The themes of the currently selected Album.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Album_Type)`</b>,
+/// \anchor ListItem_Property_Album_Type
+/// _string_,
+/// @return The Album Type (e.g. compilation\, enhanced\, explicit lyrics) of
+/// the currently selected Album.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Album_Label)`</b>,
+/// \anchor ListItem_Property_Album_Label
+/// _string_,
+/// @return The record label of the currently selected Album.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Album_Description)`</b>,
+/// \anchor ListItem_Property_Album_Description
+/// _string_,
+/// @return A review of the currently selected Album.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Album_Totaldiscs)`</b>,
+/// \anchor ListItem_Property_Album_Totaldiscs
+/// _string_,
+/// @return The total number of discs belonging to an album.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem.Property(Album_Totaldiscs) `ListItem.Property(Album_Totaldiscs)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Album_Isboxset)`</b>,
+/// \anchor ListItem_Property_Album_Isboxset
+/// _string_,
+/// @return **True** if the album is a boxset.
+/// <p><hr>
+/// @skinning_v19 **[New Infobool]** \link ListItem.Property(Album_Isboxset) `ListItem.Property(Album_Isboxset)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Album_Duration)`</b>,
+/// \anchor ListItem_Property_Album_Duration
+/// _string_,
+/// @return The duration of the album in HH:MM:SS.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_Property_Album_Duration `ListItem.Property(Album_Duration)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.DiscNumber`</b>,
+/// \anchor ListItem_DiscNumber
+/// _string_,
+/// @return The disc number of the currently selected song in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Year`</b>,
+/// \anchor ListItem_Year
+/// _string_,
+/// @return The year of the currently selected song\, album\, movie\, game in a
+/// container.
+/// <p><hr>
+/// @skinning_v18 **[Infolabel Updated]** \link ListItem_Title `ListItem.Title`\endlink extended
+/// to support games
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Premiered`</b>,
+/// \anchor ListItem_Premiered
+/// _string_,
+/// @return The release/aired date of the currently selected episode\, show\,
+/// movie or EPG item in a container.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link ListItem_Premiered `ListItem.Premiered`\endlink
+/// now also available for EPG items.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Genre`</b>,
+/// \anchor ListItem_Genre
+/// _string_,
+/// @return The genre of the currently selected song\, album or movie in a
+/// container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Contributors`</b>,
+/// \anchor ListItem_Contributors
+/// _string_,
+/// @return The list of all people who've contributed to the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Contributors `ListItem.Contributors`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.ContributorAndRole`</b>,
+/// \anchor ListItem_ContributorAndRole
+/// _string_,
+/// @return The list of all people and their role who've contributed to the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_ContributorAndRole `ListItem.ContributorAndRole`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Director`</b>,
+/// \anchor ListItem_Director
+/// _string_,
+/// @return The director of the currently selected movie in a container.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link ListItem_Director `ListItem.Director`\endlink
+/// also supports EPG.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Country`</b>,
+/// \anchor ListItem_Country
+/// _string_,
+/// @return The production country of the currently selected movie in a
+/// container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Episode`</b>,
+/// \anchor ListItem_Episode
+/// _string_,
+/// @return The episode number value for the currently selected episode. It
+/// also returns the number of total\, watched or unwatched episodes for the
+/// currently selected tvshow or season\, based on the the current watched
+/// filter.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link ListItem_Episode `ListItem.Episode`\endlink
+/// also supports EPG.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Season`</b>,
+/// \anchor ListItem_Season
+/// _string_,
+/// @return The season value for the currently selected tvshow.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link ListItem_Season `ListItem.Season`\endlink
+/// also supports EPG.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.TVShowTitle`</b>,
+/// \anchor ListItem_TVShowTitle
+/// _string_,
+/// @return The name value for the currently selected tvshow in the season and
+/// episode depth of the video library.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(TotalSeasons)`</b>,
+/// \anchor ListItem_Property_TotalSeasons
+/// _string_,
+/// @return The total number of seasons for the currently selected tvshow.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(TotalEpisodes)`</b>,
+/// \anchor ListItem_Property_TotalEpisodes
+/// _string_,
+/// @return the total number of episodes for the currently selected tvshow or
+/// season.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(WatchedEpisodes)`</b>,
+/// \anchor ListItem_Property_WatchedEpisodes
+/// _string_,
+/// @return The number of watched episodes for the currently selected tvshow
+/// or season.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(UnWatchedEpisodes)`</b>,
+/// \anchor ListItem_Property_UnWatchedEpisodes
+/// _string_,
+/// @return The number of unwatched episodes for the currently selected tvshow
+/// or season.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(NumEpisodes)`</b>,
+/// \anchor ListItem_Property_NumEpisodes
+/// _string_,
+/// @return The number of total\, watched or unwatched episodes for the
+/// currently selected tvshow or season\, based on the the current watched filter.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(WatchedEpisodePercent)`</b>,
+/// \anchor ListItem_Property_WatchedEpisodePercent
+/// _string_,
+/// @return The percentage of watched episodes in the tvshow (watched/total*100) or season.
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link ListItem_Property_WatchedEpisodePercent `ListItem.Property(WatchedEpisodePercent)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureAperture`</b>,
+/// \anchor ListItem_PictureAperture
+/// _string_,
+/// @return The F-stop used to take the selected picture.
+/// @note This is the value of the EXIF FNumber tag (hex code 0x829D).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureAuthor`</b>,
+/// \anchor ListItem_PictureAuthor
+/// _string_,
+/// @return The name of the person involved in writing about the selected picture.
+/// @note This is the value of the IPTC Writer tag (hex code 0x7A).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureAuthor `ListItem.PictureAuthor`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureByline`</b>,
+/// \anchor ListItem_PictureByline
+/// _string_,
+/// @return The name of the person who created the selected picture.
+/// @note This is the value of the IPTC Byline tag (hex code 0x50).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureByline `ListItem.PictureByline`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureBylineTitle`</b>,
+/// \anchor ListItem_PictureBylineTitle
+/// _string_,
+/// @return The title of the person who created the selected picture.
+/// @note This is the value of the IPTC BylineTitle tag (hex code 0x55).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureBylineTitle `ListItem.PictureBylineTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureCamMake`</b>,
+/// \anchor ListItem_PictureCamMake
+/// _string_,
+/// @return The manufacturer of the camera used to take the selected picture.
+/// @note This is the value of the EXIF Make tag (hex code 0x010F).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureCamModel`</b>,
+/// \anchor ListItem_PictureCamModel
+/// _string_,
+/// @return The manufacturer's model name or number of the camera used to take
+/// the selected picture.
+/// @note This is the value of the EXIF Model tag (hex code 0x0110).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureCaption`</b>,
+/// \anchor ListItem_PictureCaption
+/// _string_,
+/// @return A description of the selected picture.
+/// @note This is the value of the IPTC Caption tag (hex code 0x78).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureCategory`</b>,
+/// \anchor ListItem_PictureCategory
+/// _string_,
+/// @return The subject of the selected picture as a category code.
+/// @note This is the value of the IPTC Category tag (hex code 0x0F).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureCategory `ListItem.PictureCategory`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureCCDWidth`</b>,
+/// \anchor ListItem_PictureCCDWidth
+/// _string_,
+/// @return The width of the CCD in the camera used to take the selected
+/// picture.
+/// @note This is calculated from three EXIF tags (0xA002 * 0xA210 / 0xA20e).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureCCDWidth `ListItem.PictureCCDWidth`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureCity`</b>,
+/// \anchor ListItem_PictureCity
+/// _string_,
+/// @return The city where the selected picture was taken.
+/// @note This is the value of the IPTC City tag (hex code 0x5A).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureCity `ListItem.PictureCity`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureColour`</b>,
+/// \anchor ListItem_PictureColour
+/// _string_,
+/// @return Whether the selected picture is "Colour" or "Black and White".
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureColour `ListItem.PictureColour`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureComment`</b>,
+/// \anchor ListItem_PictureComment
+/// _string_,
+/// @return A description of the selected picture.
+/// @note This is the value of the
+/// EXIF User Comment tag (hex code 0x9286). This is the same value as
+/// \ref Slideshow_SlideComment "Slideshow.SlideComment".
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureCopyrightNotice`</b>,
+/// \anchor ListItem_PictureCopyrightNotice
+/// _string_,
+/// @return The copyright notice of the selected picture.
+/// @note This is the value of the IPTC Copyright tag (hex code 0x74).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureCopyrightNotice `ListItem.PictureCopyrightNotice`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureCountry`</b>,
+/// \anchor ListItem_PictureCountry
+/// _string_,
+/// @return The full name of the country where the selected picture was taken.
+/// @note This is the value of the IPTC CountryName tag (hex code 0x65).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureCountry `ListItem.PictureCountry`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureCountryCode`</b>,
+/// \anchor ListItem_PictureCountryCode
+/// _string_,
+/// @return The country code of the country where the selected picture was
+/// taken.
+/// @note This is the value of the IPTC CountryCode tag (hex code 0x64).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureCountryCode `ListItem.PictureCountryCode`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureCredit`</b>,
+/// \anchor ListItem_PictureCredit
+/// _string_,
+/// @return Who provided the selected picture.
+/// @note This is the value of the IPTC Credit tag (hex code 0x6E).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureCredit `ListItem.PictureCredit`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureDate`</b>,
+/// \anchor ListItem_PictureDate
+/// _string_,
+/// @return The localized date of the selected picture. The short form of the
+/// date is used.
+/// @note The value of the EXIF DateTimeOriginal tag (hex code 0x9003)
+/// is preferred. If the DateTimeOriginal tag is not found\, the value of
+/// DateTimeDigitized (hex code 0x9004) or of DateTime (hex code 0x0132) might
+/// be used.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureDate `ListItem.PictureDate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureDatetime`</b>,
+/// \anchor ListItem_PictureDatetime
+/// _string_,
+/// @return The date/timestamp of the selected picture. The localized short form
+/// of the date and time is used.
+/// @note The value of the EXIF DateTimeOriginal tag (hex code 0x9003) is preferred.
+/// If the DateTimeOriginal tag is not found\, the value of DateTimeDigitized
+/// (hex code 0x9004) or of DateTime (hex code 0x0132) might be used.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureDatetime `ListItem.PictureDatetime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureDesc`</b>,
+/// \anchor ListItem_PictureDesc
+/// _string_,
+/// @return A short description of the selected picture. The SlideComment\,
+/// EXIFComment\, or Caption values might contain a longer description.
+/// @note This is the value of the EXIF ImageDescription tag (hex code 0x010E).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureDigitalZoom`</b>,
+/// \anchor ListItem_PictureDigitalZoom
+/// _string_,
+/// @return The digital zoom ratio when the selected picture was taken.
+/// @note This is the value of the EXIF DigitalZoomRatio tag (hex code 0xA404).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureDigitalZoom `ListItem.PictureDigitalZoom`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureExpMode`</b>,
+/// \anchor ListItem_PictureExpMode
+/// _string_,
+/// @return The exposure mode of the selected picture.
+/// The possible values are:
+/// - <b>"Automatic"</b>
+/// - <b>"Manual"</b>
+/// - <b>"Auto bracketing"</b>
+/// @note This is the value of the EXIF ExposureMode tag (hex code 0xA402).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureExposure`</b>,
+/// \anchor ListItem_PictureExposure
+/// _string_,
+/// @return The class of the program used by the camera to set exposure when
+/// the selected picture was taken. Values include:
+/// - <b>"Manual"</b>
+/// - <b>"Program (Auto)"</b>
+/// - <b>"Aperture priority (Semi-Auto)"</b>
+/// - <b>"Shutter priority (semi-auto)"</b>
+/// - etc
+/// @note This is the value of the EXIF ExposureProgram tag (hex code 0x8822).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureExposure `ListItem.PictureExposure`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureExposureBias`</b>,
+/// \anchor ListItem_PictureExposureBias
+/// _string_,
+/// @return The exposure bias of the selected picture.
+/// Typically this is a number between -99.99 and 99.99.
+/// @note This is the value of the EXIF ExposureBiasValue tag (hex code 0x9204).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureExposureBias `ListItem.PictureExposureBias`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureExpTime`</b>,
+/// \anchor ListItem_PictureExpTime
+/// _string_,
+/// @return The exposure time of the selected picture\, in seconds.
+/// @note This is the value of the EXIF ExposureTime tag (hex code 0x829A).
+/// If the ExposureTime tag is not found\, the ShutterSpeedValue tag (hex code 0x9201)
+/// might be used.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureFlashUsed`</b>,
+/// \anchor ListItem_PictureFlashUsed
+/// _string_,
+/// @return The status of flash when the selected picture was taken. The value
+/// will be either "Yes" or "No"\, and might include additional information.
+/// @note This is the value of the EXIF Flash tag (hex code 0x9209).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureFlashUsed `ListItem.PictureFlashUsed`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureFocalLen`</b>,
+/// \anchor ListItem_PictureFocalLen
+/// _string_,
+/// @return The lens focal length of the selected picture.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureFocusDist`</b>,
+/// \anchor ListItem_PictureFocusDist
+/// _string_,
+/// @return The focal length of the lens\, in mm.
+/// @note This is the value of the EXIF FocalLength tag (hex code 0x920A).
+/// }
+/// \table_row3{ <b>`ListItem.PictureGPSLat`</b>,
+/// \anchor ListItem_PictureGPSLat
+/// _string_,
+/// @return The latitude where the selected picture was taken (degrees\,
+/// minutes\, seconds North or South).
+/// @note This is the value of the EXIF GPSInfo.GPSLatitude and GPSInfo.GPSLatitudeRef tags.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureGPSLon`</b>,
+/// \anchor ListItem_PictureGPSLon
+/// _string_,
+/// @return The longitude where the selected picture was taken (degrees\,
+/// minutes\, seconds East or West).
+/// @note This is the value of the EXIF GPSInfo.GPSLongitude and GPSInfo.GPSLongitudeRef tags.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureGPSAlt`</b>,
+/// \anchor ListItem_PictureGPSAlt
+/// _string_,
+/// @return The altitude in meters where the selected picture was taken.
+/// @note This is the value of the EXIF GPSInfo.GPSAltitude tag.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureHeadline`</b>,
+/// \anchor ListItem_PictureHeadline
+/// _string_,
+/// @return A synopsis of the contents of the selected picture.
+/// @note This is the value of the IPTC Headline tag (hex code 0x69).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureHeadline `ListItem.PictureHeadline`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureImageType`</b>,
+/// \anchor ListItem_PictureImageType
+/// _string_,
+/// @return The color components of the selected picture.
+/// @note This is the value of the IPTC ImageType tag (hex code 0x82).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureImageType `ListItem.PictureImageType`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureIPTCDate`</b>,
+/// \anchor ListItem_PictureIPTCDate
+/// _string_,
+/// @return The date when the intellectual content of the selected picture was
+/// created\, rather than when the picture was created.
+/// @note This is the value of the IPTC DateCreated tag (hex code 0x37).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureIPTCDate `ListItem.PictureIPTCDate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureIPTCTime`</b>,
+/// \anchor ListItem_PictureIPTCTime
+/// _string_,
+/// @return The time when the intellectual content of the selected picture was
+/// created\, rather than when the picture was created.
+/// @note This is the value of the IPTC TimeCreated tag (hex code 0x3C).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureIPTCTime `ListItem.PictureIPTCTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureISO`</b>,
+/// \anchor ListItem_PictureISO
+/// _string_,
+/// @return The ISO speed of the camera when the selected picture was taken.
+/// @note This is the value of the EXIF ISOSpeedRatings tag (hex code 0x8827).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureKeywords`</b>,
+/// \anchor ListItem_PictureKeywords
+/// _string_,
+/// @return The keywords assigned to the selected picture.
+/// @note This is the value of the IPTC Keywords tag (hex code 0x19).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureLightSource`</b>,
+/// \anchor ListItem_PictureLightSource
+/// _string_,
+/// @return The kind of light source when the picture was taken. Possible
+/// values include:
+/// - <b>"Daylight"</b>
+/// - <b>"Fluorescent"</b>
+/// - <b>"Incandescent"</b>
+/// - etc
+/// @note This is the value of the EXIF LightSource tag (hex code 0x9208).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureLightSource `ListItem.PictureLightSource`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureLongDate`</b>,
+/// \anchor ListItem_PictureLongDate
+/// _string_,
+/// @return Only the localized date of the selected picture. The long form of
+/// the date is used.
+/// @note The value of the EXIF DateTimeOriginal tag (hex code
+/// 0x9003) is preferred. If the DateTimeOriginal tag is not found\, the
+/// value of DateTimeDigitized (hex code 0x9004) or of DateTime (hex code
+/// 0x0132) might be used.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureLongDate `ListItem.PictureLongDate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureLongDatetime`</b>,
+/// \anchor ListItem_PictureLongDatetime
+/// _string_,
+/// @return The date/timestamp of the selected picture. The localized long
+/// form of the date and time is used.
+/// @note The value of the EXIF DateTimeOriginal
+/// tag (hex code 0x9003) is preferred. if the DateTimeOriginal tag is not
+/// found\, the value of DateTimeDigitized (hex code 0x9004) or of DateTime
+/// (hex code 0x0132) might be used.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureMeteringMode`</b>,
+/// \anchor ListItem_PictureMeteringMode
+/// _string_,
+/// @return The metering mode used when the selected picture was taken. The
+/// possible values are:
+/// - <b>"Center weight"</b>
+/// - <b>"Spot"</b>
+/// - <b>"Matrix"</b>
+/// @note This is the value of the EXIF MeteringMode tag (hex code 0x9207).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureMeteringMode `ListItem.PictureMeteringMode`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureObjectName`</b>,
+/// \anchor ListItem_PictureObjectName
+/// _string_,
+/// @return A shorthand reference for the selected picture.
+/// @note This is the value of the IPTC ObjectName tag (hex code 0x05).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureObjectName `ListItem.PictureObjectName`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureOrientation`</b>,
+/// \anchor ListItem_PictureOrientation
+/// _string_,
+/// @return The orientation of the selected picture. Possible values are:
+/// - <b>"Top Left"</b>
+/// - <b>"Top Right"</b>
+/// - <b>"Left Top"</b>
+/// - <b>"Right Bottom"</b>
+/// - etc
+/// @note This is the value of the EXIF Orientation tag (hex code 0x0112).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureOrientation `ListItem.PictureOrientation`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PicturePath`</b>,
+/// \anchor ListItem_PicturePath
+/// _string_,
+/// @return The filename and path of the selected picture.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureProcess`</b>,
+/// \anchor ListItem_PictureProcess
+/// _string_,
+/// @return The process used to compress the selected picture.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureProcess `ListItem.PictureProcess`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureReferenceService`</b>,
+/// \anchor ListItem_PictureReferenceService
+/// _string_,
+/// @return The Service Identifier of a prior envelope to which the selected
+/// picture refers.
+/// @note This is the value of the IPTC ReferenceService tag (hex code 0x2D).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureReferenceService `ListItem.PictureReferenceService`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureResolution`</b>,
+/// \anchor ListItem_PictureResolution
+/// _string_,
+/// @return The dimensions of the selected picture.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureSource`</b>,
+/// \anchor ListItem_PictureSource
+/// _string_,
+/// @return The original owner of the selected picture.
+/// @note This is the value of the IPTC Source tag (hex code 0x73).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureSource `ListItem.PictureSource`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureSpecialInstructions`</b>,
+/// \anchor ListItem_PictureSpecialInstructions
+/// _string_,
+/// @return Other editorial instructions concerning the use of the selected
+/// picture.
+/// @note This is the value of the IPTC SpecialInstructions tag (hex code 0x28).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureSpecialInstructions `ListItem.PictureSpecialInstructions`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureState`</b>,
+/// \anchor ListItem_PictureState
+/// _string_,
+/// @return The State/Province where the selected picture was taken.
+/// @note This is the value of the IPTC ProvinceState tag (hex code 0x5F).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureState `ListItem.PictureState`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureSublocation`</b>,
+/// \anchor ListItem_PictureSublocation
+/// _string_,
+/// @return The location within a city where the selected picture was taken -
+/// might indicate the nearest landmark.
+/// @note This is the value of the IPTC SubLocation tag (hex code 0x5C).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureSublocation `ListItem.PictureSublocation`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureSupplementalCategories`</b>,
+/// \anchor ListItem_PictureSupplementalCategories
+/// _string_,
+/// @return A supplemental category codes to further refine the subject of the
+/// selected picture.
+/// @note This is the value of the IPTC SuppCategory tag (hex code 0x14).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureSupplementalCategories `ListItem.PictureSupplementalCategories`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureTransmissionReference`</b>,
+/// \anchor ListItem_PictureTransmissionReference
+/// _string_,
+/// @return A code representing the location of original transmission of the
+/// selected picture.
+/// @note This is the value of the IPTC TransmissionReference tag (hex code 0x67).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureTransmissionReference `ListItem.PictureTransmissionReference`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureUrgency`</b>,
+/// \anchor ListItem_PictureUrgency
+/// _string_,
+/// @return The urgency of the selected picture. Values are 1-9.
+/// @note The "1" is most urgent. Some image management programs use urgency to indicate
+/// picture rating\, where urgency "1" is 5 stars and urgency "5" is 1 star.
+/// Urgencies 6-9 are not used for rating. This is the value of the IPTC
+/// Urgency tag (hex code 0x0A).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureUrgency `ListItem.PictureUrgency`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PictureWhiteBalance`</b>,
+/// \anchor ListItem_PictureWhiteBalance
+/// _string_,
+/// @return The white balance mode set when the selected picture was taken.
+/// The possible values are:
+/// - <b>"Manual"</b>
+/// - <b>"Auto"</b>
+/// @note This is the value of the EXIF WhiteBalance tag (hex code 0xA403).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_PictureWhiteBalance `ListItem.PictureWhiteBalance`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.FileName`</b>,
+/// \anchor ListItem_FileName
+/// _string_,
+/// @return The filename of the currently selected song or movie in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Path`</b>,
+/// \anchor ListItem_Path
+/// _string_,
+/// @return The complete path of the currently selected song or movie in a
+/// container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.FolderName`</b>,
+/// \anchor ListItem_FolderName
+/// _string_,
+/// @return The top most folder of the path of the currently selected song or
+/// movie in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.FolderPath`</b>,
+/// \anchor ListItem_FolderPath
+/// _string_,
+/// @return The complete path of the currently selected song or movie in a
+/// container (without user details).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.FileNameAndPath`</b>,
+/// \anchor ListItem_FileNameAndPath
+/// _string_,
+/// @return The full path with filename of the currently selected song or
+/// movie in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.FileExtension`</b>,
+/// \anchor ListItem_FileExtension
+/// _string_,
+/// @return The file extension (without leading dot) of the currently selected
+/// item in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.FileNameNoExtension`</b>,
+/// \anchor ListItem_FileName_No_Extension
+/// _string_,
+/// @return The filename without extension of the currently selected
+/// item in a container.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_FileName_No_Extension `ListItem.FileNameNoExtension`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Date`</b>,
+/// \anchor ListItem_Date
+/// _string_,
+/// @return The file date of the currently selected song or movie in a
+/// container / Aired date of an episode / Day\, start time and end time of
+/// current selected TV programme (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.DateTime`</b>,
+/// \anchor ListItem_DateTime
+/// _string_,
+/// @return The date and time a certain event happened (event log).
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link ListItem_DateTime `ListItem.DateTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.DateAdded`</b>,
+/// \anchor ListItem_DateAdded
+/// _string_,
+/// @return The date the currently selected item was added to the
+/// library / Date and time of an event in the EventLog window.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Size`</b>,
+/// \anchor ListItem_Size
+/// _string_,
+/// @return The file size of the currently selected song or movie in a
+/// container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Rating([name])`</b>,
+/// \anchor ListItem_Rating
+/// _string_,
+/// @return The scraped rating of the currently selected item in a container (1-10).
+/// @param name - [opt] you can specify the name of the scraper to retrieve a specific rating\,
+/// for use in dialogvideoinfo.xml.
+/// <p><hr>
+/// @skinning_v18 **[Infolabel Updated]** \link ListItem_Rating `ListItem.Rating([name])`\endlink replaces
+/// the old `ListItem.Ratings([name])` infolabel.
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Rating `ListItem.Ratings([name])`\endlink
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_Rating `ListItem.Ratings`\endlink
+/// for songs it's now the scraped rating.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Set`</b>,
+/// \anchor ListItem_Set
+/// _string_,
+/// @return The name of the set the movie is part of.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Set `ListItem.Set`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.SetId`</b>,
+/// \anchor ListItem_SetId
+/// _string_,
+/// @return The id of the set the movie is part of.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_SetId `ListItem.SetId`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Status`</b>,
+/// \anchor ListItem_Status
+/// _string_,
+/// @return One of the following status:
+/// - <b>"returning series"</b>
+/// - <b>"in production"</b>
+/// - <b>"planned"</b>
+/// - <b>"cancelled"</b>
+/// - <b>"ended"</b>
+/// <p>
+/// @note For use with tv shows.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Status `ListItem.Status`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.EndTimeResume`</b>,
+/// \anchor ListItem_EndTimeResume
+/// _string_,
+/// @return Returns the time a video will end if you resume it\, instead of playing it from the beginning.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_EndTimeResume `ListItem.EndTimeResume`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.UserRating`</b>,
+/// \anchor ListItem_UserRating
+/// _string_,
+/// @return The user rating of the currently selected item in a container (1-10).
+/// <p><hr>
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_UserRating `ListItem.UserRating`\endlink
+/// now available for albums/songs.
+/// @skinning_v16 **[New Infolabel]** \link ListItem_UserRating `ListItem.UserRating`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Votes([name])`</b>,
+/// \anchor ListItem_Votes
+/// _string_,
+/// @return The scraped votes of the currently selected movie in a container.
+/// @param name - [opt] you can specify the name of the scraper to retrieve specific votes\,
+/// for use in `dialogvideoinfo.xml`.
+/// <p><hr>
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_Votes `ListItem.Votes([name])`\endlink
+/// add optional param <b>name</b> to specify the scrapper.
+/// @skinning_v13 **[New Infolabel]** \link ListItem_Votes `ListItem.Votes`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.RatingAndVotes([name])`</b>,
+/// \anchor ListItem_RatingAndVotes
+/// _string_,
+/// @return The scraped rating and votes of the currently selected movie in a
+/// container (1-10).
+/// @param name - [opt] you can specify the name of the scraper to retrieve specific votes\,
+/// for use in `dialogvideoinfo.xml`.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_RatingAndVotes `ListItem.RatingAndVotes([name])`\endlink
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_RatingAndVotes `ListItem.RatingAndVotes`\endlink
+/// now available for albums/songs.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Mood`</b>,
+/// \anchor ListItem_Mood
+/// _string_,
+/// @return The mood of the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Mood `ListItem.Mood`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Mpaa`</b>,
+/// \anchor ListItem_Mpaa
+/// _string_,
+/// @return The MPAA rating of the currently selected movie in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.ProgramCount`</b>,
+/// \anchor ListItem_ProgramCount
+/// _string_,
+/// @return The number of times an xbe has been run from "my programs".
+/// @todo description might be outdated
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Duration`</b>,
+/// \anchor ListItem_Duration
+/// _string_,
+/// @return The duration of the currently selected item in a container
+/// in the format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p><hr>
+/// @skinning_v18 **[Infolabel Updated]** \link ListItem_Duration `ListItem.Duration`\endlink will
+/// return <b>hh:mm:ss</b> instead of the duration in minutes.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Duration(format)`</b>,
+/// \anchor ListItem_Duration_format
+/// _string_,
+/// @return The duration of the currently selected item in a container in
+/// different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.DBTYPE`</b>,
+/// \anchor ListItem_DBTYPE
+/// _string_,
+/// @return The database type of the \ref ListItem_DBID "ListItem.DBID" for videos (movie\, set\,
+/// genre\, actor\, tvshow\, season\, episode). It does not return any value
+/// for the music library.
+/// @note Beware with season\, the "*all seasons" entry does
+/// give a DBTYPE "season" and a DBID\, but you can't get the details of that
+/// entry since it's a virtual entry in the Video Library.
+/// <p><hr>
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_DBTYPE `ListItem.DBTYPE`\endlink
+/// now available in the music library.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.DBID`</b>,
+/// \anchor ListItem_DBID
+/// _string_,
+/// @return The database id of the currently selected listitem in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Appearances`</b>,
+/// \anchor ListItem_Appearances
+/// _string_,
+/// @return The number of movies featuring the selected actor / directed by the selected director.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Appearances `ListItem.Appearances`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Cast`</b>,
+/// \anchor ListItem_Cast
+/// _string_,
+/// @return A concatenated string of cast members of the currently selected
+/// movie\, for use in dialogvideoinfo.xml.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link ListItem_Cast `ListItem.Cast`\endlink
+/// also supports EPG.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.CastAndRole`</b>,
+/// \anchor ListItem_CastAndRole
+/// _string_,
+/// @return A concatenated string of cast members and roles of the currently
+/// selected movie\, for use in dialogvideoinfo.xml.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Studio`</b>,
+/// \anchor ListItem_Studio
+/// _string_,
+/// @return The studio of current selected Music Video in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Top250`</b>,
+/// \anchor ListItem_Top250
+/// _string_,
+/// @return The IMDb top250 position of the currently selected listitem in a
+/// container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Trailer`</b>,
+/// \anchor ListItem_Trailer
+/// _string_,
+/// @return The full trailer path with filename of the currently selected
+/// movie in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Writer`</b>,
+/// \anchor ListItem_Writer
+/// _string_,
+/// @return The name of Writer of current Video in a container.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link ListItem_Writer `ListItem.Writer`\endlink
+/// also supports EPG.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Tag`</b>,
+/// \anchor ListItem_Tag
+/// _string_,
+/// @return The summary of current Video in a container.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Tag `ListItem.Tag`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Tagline`</b>,
+/// \anchor ListItem_Tagline
+/// _string_,
+/// @return A Small Summary of current Video in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PlotOutline`</b>,
+/// \anchor ListItem_PlotOutline
+/// _string_,
+/// @return A small Summary of current Video in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Plot`</b>,
+/// \anchor ListItem_Plot
+/// _string_,
+/// @return The complete Text Summary of Video in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IMDBNumber`</b>,
+/// \anchor ListItem_IMDBNumber
+/// _string_,
+/// @return The IMDb ID of the selected Video in a container.
+/// <p><hr>
+/// @skinning_v15 **[New Infolabel]** \link ListItem_IMDBNumber `ListItem.IMDBNumber`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.EpisodeName`</b>,
+/// \anchor ListItem_EpisodeName
+/// _string_,
+/// @return The name of the episode if the selected EPG item is a TV Show (PVR).
+/// <p><hr>
+/// @skinning_v15 **[New Infolabel]** \link ListItem_EpisodeName `ListItem.EpisodeName`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PercentPlayed`</b>,
+/// \anchor ListItem_PercentPlayed
+/// _string_,
+/// @return The percentage value [0-100] of how far the selected video has been
+/// played.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.LastPlayed`</b>,
+/// \anchor ListItem_LastPlayed
+/// _string_,
+/// @return The last play date of Video in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.PlayCount`</b>,
+/// \anchor ListItem_PlayCount
+/// _string_,
+/// @return The playcount of Video in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.ChannelName`</b>,
+/// \anchor ListItem_ChannelName
+/// _string_,
+/// @return The name of current selected TV channel in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.VideoCodec`</b>,
+/// \anchor ListItem_VideoCodec
+/// _string_,
+/// @return The video codec of the currently selected video. Common values:
+/// - <b>3iv2</b>
+/// - <b>av1</b>
+/// - <b>avc1</b>
+/// - <b>div2</b>
+/// - <b>div3</b>
+/// - <b>divx</b>
+/// - <b>divx 4</b>
+/// - <b>dx50</b>
+/// - <b>flv</b>
+/// - <b>h264</b>
+/// - <b>microsoft</b>
+/// - <b>mp42</b>
+/// - <b>mp43</b>
+/// - <b>mp4v</b>
+/// - <b>mpeg1video</b>
+/// - <b>mpeg2video</b>
+/// - <b>mpg4</b>
+/// - <b>rv40</b>
+/// - <b>svq1</b>
+/// - <b>svq3</b>
+/// - <b>theora</b>
+/// - <b>vp6f</b>
+/// - <b>wmv2</b>
+/// - <b>wmv3</b>
+/// - <b>wvc1</b>
+/// - <b>xvid</b>
+/// - etc
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.VideoResolution`</b>,
+/// \anchor ListItem_VideoResolution
+/// _string_,
+/// @return The resolution of the currently selected video. Possible values:
+/// - <b>480</b>
+/// - <b>576</b>
+/// - <b>540</b>
+/// - <b>720</b>
+/// - <b>1080</b>
+/// - <b>4K</b>
+/// - <b>8K</b>
+/// @note 540 usually means a widescreen
+/// format (around 960x540) while 576 means PAL resolutions (normally
+/// 720x576)\, therefore 540 is actually better resolution than 576.
+/// <p><hr>
+/// @skinning_v18 **[Updated Infolabel]** \link ListItem_VideoResolution ListItem.VideoResolution\endlink
+/// added <b>8K</b> as a possible value.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.VideoAspect`</b>,
+/// \anchor ListItem_VideoAspect
+/// _string_,
+/// @return The aspect ratio of the currently selected video. Possible values:
+/// - <b>1.00</b>
+/// - <b>1.19</b>
+/// - <b>1.33</b>
+/// - <b>1.37</b>
+/// - <b>1.66</b>
+/// - <b>1.78</b>
+/// - <b>1.85</b>
+/// - <b>2.00</b>
+/// - <b>2.20</b>
+/// - <b>2.35</b>
+/// - <b>2.40</b>
+/// - <b>2.55</b>
+/// - <b>2.76</b>
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AudioCodec`</b>,
+/// \anchor ListItem_AudioCodec
+/// _string_,
+/// @return The audio codec of the currently selected video. Common values:
+/// - <b>aac</b>
+/// - <b>ac3</b>
+/// - <b>cook</b>
+/// - <b>dca</b>
+/// - <b>dtshd_hra</b>
+/// - <b>dtshd_ma</b>
+/// - <b>eac3</b>
+/// - <b>mp1</b>
+/// - <b>mp2</b>
+/// - <b>mp3</b>
+/// - <b>pcm_s16be</b>
+/// - <b>pcm_s16le</b>
+/// - <b>pcm_u8</b>
+/// - <b>truehd</b>
+/// - <b>vorbis</b>
+/// - <b>wmapro</b>
+/// - <b>wmav2</b>
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AudioChannels`</b>,
+/// \anchor ListItem_AudioChannels
+/// _string_,
+/// @return The number of audio channels of the currently selected video. Possible values:
+/// - <b>1</b>
+/// - <b>2</b>
+/// - <b>4</b>
+/// - <b>5</b>
+/// - <b>6</b>
+/// - <b>8</b>
+/// - <b>10</b>
+/// <p><hr>
+/// @skinning_v16 **[Infolabel Updated]** \link ListItem_AudioChannels `ListItem.AudioChannels`\endlink
+/// if a video contains no audio\, these infolabels will now return empty.
+/// (they used to return 0)
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AudioLanguage`</b>,
+/// \anchor ListItem_AudioLanguage
+/// _string_,
+/// @return The audio language of the currently selected video (an
+/// ISO 639-2 three character code: e.g. eng\, epo\, deu)
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.SubtitleLanguage`</b>,
+/// \anchor ListItem_SubtitleLanguage
+/// _string_,
+/// @return The subtitle language of the currently selected video (an
+/// ISO 639-2 three character code: e.g. eng\, epo\, deu)
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(AudioCodec.[n])`</b>,
+/// \anchor ListItem_Property_AudioCodec
+/// _string_,
+/// @return The audio codec of the currently selected video
+/// @param n - the number of the audiostream (values: see \ref ListItem_AudioCodec "ListItem.AudioCodec")
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link ListItem_Property_AudioCodec `ListItem.Property(AudioCodec.[n])`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(AudioChannels.[n])`</b>,
+/// \anchor ListItem_Property_AudioChannels
+/// _string_,
+/// @return The number of audio channels of the currently selected video
+/// @param n - the number of the audiostream (values: see
+/// \ref ListItem_AudioChannels "ListItem.AudioChannels")
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link ListItem_Property_AudioChannels `ListItem.Property(AudioChannels.[n])`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(AudioLanguage.[n])`</b>,
+/// \anchor ListItem_Property_AudioLanguage
+/// _string_,
+/// @return The audio language of the currently selected video
+/// @param n - the number of the audiostream (values: see \ref ListItem_AudioLanguage "ListItem.AudioLanguage")
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link ListItem_Property_AudioLanguage `ListItem.Property(AudioLanguage.[n])`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(SubtitleLanguage.[n])`</b>,
+/// \anchor ListItem_Property_SubtitleLanguage
+/// _string_,
+/// @return The subtitle language of the currently selected video
+/// @param n - the number of the subtitle (values: see \ref ListItem_SubtitleLanguage "ListItem.SubtitleLanguage")
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link ListItem_Property_SubtitleLanguage `ListItem.Property(SubtitleLanguage.[n])`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.Disclaimer)`</b>,
+/// \anchor ListItem_Property_AddonDisclaimer
+/// _string_,
+/// @return The disclaimer of the currently selected addon.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.Changelog)`</b>,
+/// \anchor ListItem_Property_AddonChangelog
+/// _string_,
+/// @return The changelog of the currently selected addon.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.ID)`</b>,
+/// \anchor ListItem_Property_AddonID
+/// _string_,
+/// @return The identifier of the currently selected addon.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.Status)`</b>,
+/// \anchor ListItem_Property_AddonStatus
+/// _string_,
+/// @return The status of the currently selected addon.
+/// @todo missing reference in GuiInfoManager.cpp making it hard to track.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.Orphaned)`</b>,
+/// \anchor ListItem_Property_AddonOrphaned
+/// _boolean_,
+/// @return **True** if the Addon is orphanad.
+/// @todo missing reference in GuiInfoManager.cpp making it hard to track.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link ListItem_Property_AddonOrphaned `ListItem.Property(Addon.Orphaned)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Addon.Path)`</b>,
+/// \anchor ListItem_Property_AddonPath
+/// _string_,
+/// @return The path of the currently selected addon.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.StartTime`</b>,
+/// \anchor ListItem_StartTime
+/// _string_,
+/// @return The start time of current selected TV programme in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.EndTime`</b>,
+/// \anchor ListItem_EndTime
+/// _string_,
+/// @return The end time of current selected TV programme in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.StartDate`</b>,
+/// \anchor ListItem_StartDate
+/// _string_,
+/// @return The start date of current selected TV programme in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.EndDate`</b>,
+/// \anchor ListItem_EndDate
+/// _string_,
+/// @return The end date of current selected TV programme in a container.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.NextTitle`</b>,
+/// \anchor ListItem_NextTitle
+/// _string_,
+/// @return The title of the next item (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.NextGenre`</b>,
+/// \anchor ListItem_NextGenre
+/// _string_,
+/// @return The genre of the next item (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.NextPlot`</b>,
+/// \anchor ListItem_NextPlot
+/// _string_,
+/// @return The plot of the next item (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.NextPlotOutline`</b>,
+/// \anchor ListItem_NextPlotOutline
+/// _string_,
+/// @return The plot outline of the next item (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.NextStartTime`</b>,
+/// \anchor ListItem_NextStartTime
+/// _string_,
+/// @return The start time of the next item (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.NextEndTime`</b>,
+/// \anchor ListItem_NextEndTime
+/// _string_,
+/// @return The end of the next item (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.NextStartDate`</b>,
+/// \anchor ListItem_NextStartDate
+/// _string_,
+/// @return The start date of the next item (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.NextEndDate`</b>,
+/// \anchor ListItem_NextEndDate
+/// _string_,
+/// @return The end date of the next item (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.NextDuration`</b>,
+/// \anchor ListItem_NextDuration
+/// _string_,
+/// @return The duration of the next item (PVR) in the format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_NextDuration `ListItem.NextDuration`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.NextDuration(format)`</b>,
+/// \anchor ListItem_NextDuration_format
+/// _string_,
+/// @return The duration of the next item (PVR) in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_NextDuration_format `ListItem.NextDuration(format)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.ChannelGroup`</b>,
+/// \anchor ListItem_ChannelGroup
+/// _string_,
+/// @return The channel group of the selected item (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.ChannelNumberLabel`</b>,
+/// \anchor ListItem_ChannelNumberLabel
+/// _string_,
+/// @return The channel and subchannel number of the currently selected channel that's
+/// currently playing (PVR).
+/// <p><hr>
+/// @skinning_v14 **[New Infolabel]** \link ListItem_ChannelNumberLabel `ListItem.ChannelNumberLabel`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Progress`</b>,
+/// \anchor ListItem_Progress
+/// _string_,
+/// @return The part of the programme that's been played (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.StereoscopicMode`</b>,
+/// \anchor ListItem_StereoscopicMode
+/// _string_,
+/// @return The stereomode of the selected video:
+/// - <b>mono</b>
+/// - <b>split_vertical</b>
+/// - <b>split_horizontal</b>
+/// - <b>row_interleaved</b>
+/// - <b>anaglyph_cyan_red</b>
+/// - <b>anaglyph_green_magenta</b>
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link ListItem_StereoscopicMode `ListItem.StereoscopicMode`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.HasTimerSchedule`</b>,
+/// \anchor ListItem_HasTimerSchedule
+/// _boolean_,
+/// @return **True** if the item was scheduled by a timer rule (PVR).
+/// <p><hr>
+/// @skinning_v16 **[New Boolean Condition]** \ref ListItem_HasTimerSchedule "ListItem.HasTimerSchedule"
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.HasReminder`</b>,
+/// \anchor ListItem_HasReminder
+/// _boolean_,
+/// @return **True** if the item has a reminder set (PVR).
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \ref ListItem_HasReminder "ListItem.HasReminder"
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.HasReminderRule`</b>,
+/// \anchor ListItem_ListItem.HasReminderRule
+/// _boolean_,
+/// @return **True** if the item was scheduled by a reminder timer rule (PVR).
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \ref ListItem_HasReminderRule "ListItem.HasReminderRule"
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.HasRecording`</b>,
+/// \anchor ListItem_HasRecording
+/// _boolean_,
+/// @return **True** if a given epg tag item currently gets recorded or has been recorded.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.TimerHasError`</b>,
+/// \anchor ListItem_TimerHasError
+/// _boolean_,
+/// @return **True** if the item has a timer and it won't be recorded because of an error (PVR).
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \ref ListItem_TimerHasError "ListItem.TimerHasError"
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.TimerHasConflict`</b>,
+/// \anchor ListItem_TimerHasConflict
+/// _boolean_,
+/// @return **True** if the item has a timer and it won't be recorded because of a conflict (PVR).
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \ref ListItem_TimerHasConflict "ListItem.TimerHasConflict"
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.TimerIsActive`</b>,
+/// \anchor ListItem_TimerIsActive
+/// _boolean_,
+/// @return **True** if the item has a timer that will be recorded\, i.e. the timer is enabled (PVR).
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \ref ListItem_TimerIsActive "ListItem.TimerIsActive"
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Comment`</b>,
+/// \anchor ListItem_Comment
+/// _string_,
+/// @return The comment assigned to the item (PVR/MUSIC).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.TimerType`</b>,
+/// \anchor ListItem_TimerType
+/// _string_,
+/// @return The type of the PVR timer / timer rule item as a human readable string.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.EpgEventTitle`</b>,
+/// \anchor ListItem_EpgEventTitle
+/// _string_,
+/// @return The title of the epg event associated with the item\, if any.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.EpgEventIcon`</b>,
+/// \anchor ListItem_EpgEventIcon
+/// _string_,
+/// @return The thumbnail for the EPG event associated with the item (if it exists).
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_EpgEventIcon `ListItem.EpgEventIcon`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.InProgress`</b>,
+/// \anchor ListItem_InProgress
+/// _boolean_,
+/// @return **True** if the EPG event item is currently active (time-wise).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsParentFolder`</b>,
+/// \anchor ListItem_IsParentFolder
+/// _boolean_,
+/// @return **True** if the current list item is the goto parent folder '..'.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link ListItem_IsParentFolder `ListItem.IsParentFolder`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonName`</b>,
+/// \anchor ListItem_AddonName
+/// _string_,
+/// @return The name of the currently selected addon.
+/// <p><hr>
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_AddonName `ListItem.AddonName`\endlink
+/// replaces `ListItem.Property(Addon.Name)`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonVersion`</b>,
+/// \anchor ListItem_AddonVersion
+/// _string_,
+/// @return The version of the currently selected addon.
+/// <p><hr>
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_AddonVersion `ListItem.AddonVersion`\endlink
+/// replaces `ListItem.Property(Addon.Version)`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonCreator`</b>,
+/// \anchor ListItem_AddonCreator
+/// _string_,
+/// @return The name of the author the currently selected addon.
+/// <p><hr>
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_AddonCreator `ListItem.AddonCreator`\endlink
+/// replaces `ListItem.Property(Addon.Creator)`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonSummary`</b>,
+/// \anchor ListItem_AddonSummary
+/// _string_,
+/// @return A short description of the currently selected addon.
+/// <p><hr>
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_AddonSummary `ListItem.AddonSummary`\endlink
+/// replaces `ListItem.Property(Addon.Summary)`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonDescription`</b>,
+/// \anchor ListItem_AddonDescription
+/// _string_,
+/// @return The full description of the currently selected addon.
+/// <p><hr>
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_AddonDescription `ListItem.AddonDescription`\endlink
+/// replaces `ListItem.Property(Addon.Description)`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonDisclaimer`</b>,
+/// \anchor ListItem_AddonDisclaimer
+/// _string_,
+/// @return The disclaimer of the currently selected addon.
+/// <p><hr>
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_AddonDisclaimer `ListItem.AddonDisclaimer`\endlink
+/// replaces `ListItem.Property(Addon.Disclaimer)`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonBroken`</b>,
+/// \anchor ListItem_AddonBroken
+/// _string_,
+/// @return A message when the addon is marked as broken in the repo.
+/// @deprecated but still available\, use \ref ListItem_AddonLifecycleDesc "ListItem.AddonLifecycleDesc"
+/// instead
+/// <p><hr>
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_AddonBroken `ListItem.AddonBroken`\endlink
+/// replaces `ListItem.Property(Addon.Broken)`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonLifecycleType`</b>,
+/// \anchor ListItem_AddonLifecycleType
+/// _string_,
+/// @return String name when the addon is marked as special condition in the repo.
+/// - <b>Label: 24169 (Normal)</b> - Used if an add-on has no special lifecycle state which is the default state
+/// - <b>Label: 24170 (Deprecated)</b> - The add-on should be marked as deprecated but is still usable
+/// - <b>Label: 24171 (Broken)</b> - The add-on should marked as broken in the repository
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_AddonLifecycleType `ListItem.AddonLifecycleType`\endlink
+/// replaces `ListItem.AddonBroken`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonLifecycleDesc`</b>,
+/// \anchor ListItem_AddonLifecycleDesc
+/// _string_,
+/// @return From addon defined message text when it is marked as special condition inside repository.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_AddonLifecycleDesc `ListItem.AddonLifecycleDesc``\endlink
+/// replaces `ListItem.AddonBroken`.
+/// <p>
+/// }
+
+/// \table_row3{ <b>`ListItem.AddonType`</b>,
+/// \anchor ListItem_AddonType
+/// _string_,
+/// @return The type (screensaver\, script\, skin\, etc...) of the currently selected addon.
+/// <p><hr>
+/// @skinning_v17 **[Infolabel Updated]** \link ListItem_AddonType `ListItem.AddonType`\endlink
+/// replaces `ListItem.Property(Addon.Type)`.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonInstallDate`</b>,
+/// \anchor ListItem_AddonInstallDate
+/// _string_,
+/// @return The date the addon was installed.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_AddonInstallDate `ListItem.AddonInstallDate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonLastUpdated`</b>,
+/// \anchor ListItem_AddonLastUpdated
+/// _string_,
+/// @return The date the addon was last updated.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_AddonLastUpdated `ListItem.AddonLastUpdated`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonLastUsed`</b>,
+/// \anchor ListItem_AddonLastUsed
+/// _string_,
+/// @return The date the addon was used last.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_AddonLastUsed `ListItem.AddonLastUsed`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonNews`</b>,
+/// \anchor ListItem_AddonNews
+/// _string_,
+/// @return A brief changelog\, taken from the addons' `addon.xml` file.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_AddonNews `ListItem.AddonNews`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonSize`</b>,
+/// \anchor ListItem_AddonSize
+/// _string_,
+/// @return The filesize of the addon.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_AddonSize `ListItem.AddonSize`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AddonOrigin`</b>,
+/// \anchor ListItem_AddonOrigin
+/// _string_,
+/// @return The name of the repository the add-on originates from.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.ExpirationDate`</b>,
+/// \anchor ListItem_ExpirationDate
+/// _string_,
+/// @return The expiration date of the selected item in a container\, empty string if not supported.
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.ExpirationTime`</b>,
+/// \anchor ListItem_ExpirationTime
+/// _string_,
+/// @return The expiration time of the selected item in a container\, empty string if not supported
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Art(type)`</b>,
+/// \anchor ListItem_Art_Type
+/// _string_,
+/// @return A particular art type for an item.
+/// @param type - the art type. It can be any value (set by scripts and scrappers). Common values:
+/// - <b>clearart</b> - the clearart (if it exists) of the currently selected movie or tv show.
+/// - <b>clearlogo</b> - the clearlogo (if it exists) of the currently selected movie or tv show.
+/// - <b>landscape</b> - the 16:9 landscape (if it exists) of the currently selected item.
+/// - <b>thumb</b> - the thumbnail of the currently selected item.
+/// - <b>poster</b> - the poster of the currently selected movie or tv show.
+/// - <b>banner</b> - the banner of the currently selected tv show.
+/// - <b>fanart</b> - the fanart image of the currently selected item.
+/// - <b>set.fanart</b> - the fanart image of the currently selected movieset.
+/// - <b>tvshow.poster</b> - the tv show poster of the parent container.
+/// - <b>tvshow.banner</b> - the tv show banner of the parent container.
+/// - <b>tvshow.clearlogo</b> - the tv show clearlogo (if it exists) of the parent container.
+/// - <b>tvshow.landscape</b> - the tv show landscape (if it exists) of the parent container.
+/// - <b>tvshow.clearart</b> - the tv show clearart (if it exists) of the parent container.
+/// - <b>season.poster</b> - the season poster of the currently selected season. (Only available in DialogVideoInfo.xml).
+/// - <b>season.banner</b> - the season banner of the currently selected season. (Only available in DialogVideoInfo.xml).
+/// - <b>season.fanart</b> - the fanart image of the currently selected season. (Only available in DialogVideoInfo.xml)
+/// - <b>artist.thumb</b> - the artist thumb of an album or song item.
+/// - <b>artist.fanart</b> - the artist fanart of an album or song item.
+/// - <b>album.thumb</b> - the album thumb (cover) of a song item.
+/// - <b>artist[n].*</b> - in case a song has multiple artists\, a digit is added to the art type for the 2nd artist onwards
+/// e.g `Listitem.Art(artist1.thumb)` gives the thumb of the 2nd artist of a song.
+/// - <b>albumartist[n].*</b> - n case a song has multiple album artists\, a digit is added to the art type for the 2nd artist
+/// onwards e.g `Listitem.Art(artist1.thumb)` gives the thumb of the 2nd artist of a song.
+/// <p>
+/// @todo Find a better way of finding the art types instead of manually defining them here.
+/// <p><hr>
+/// @skinning_v18 **[Infolabel Updated]** \link ListItem_Art_Type `ListItem.Art(type)`\endlink add <b>artist[n].*</b> and
+/// <b>albumartist[n].*</b> as possible targets for <b>type</b>
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Platform`</b>,
+/// \anchor ListItem_Platform
+/// _string_,
+/// @return The game platform (e.g. "Atari 2600") (RETROPLAYER).
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Platform `ListItem.Platform`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Genres`</b>,
+/// \anchor ListItem_Genres
+/// _string_,
+/// @return The game genres (e.g. "["Action"\,"Strategy"]") (RETROPLAYER).
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Genres `ListItem.Genres`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Publisher`</b>,
+/// \anchor ListItem_Publisher
+/// _string_,
+/// @return The game publisher (e.g. "Nintendo") (RETROPLAYER).
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Publisher `ListItem.Publisher`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Developer`</b>,
+/// \anchor ListItem_Developer
+/// _string_,
+/// @return The game developer (e.g. "Square") (RETROPLAYER).
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Developer `ListItem.Developer`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Overview`</b>,
+/// \anchor ListItem_Overview
+/// _string_,
+/// @return The game overview/summary (RETROPLAYER).
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Overview `ListItem.Overview`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.GameClient`</b>,
+/// \anchor ListItem_GameClient
+/// _string_,
+/// @return The add-on ID of the game client (a.k.a. emulator) to use for playing the game
+/// (e.g. game.libretro.fceumm) (RETROPLAYER).
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_GameClient `ListItem.GameClient`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(propname)`</b>,
+/// \anchor ListItem_Property_Propname
+/// _string_,
+/// @return The requested property of a ListItem.
+/// @param propname - the property requested
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Role.Composer)`</b>,
+/// \anchor ListItem_Property_Role_Composer
+/// _string_,
+/// @return The name of the person who composed the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Property_Role_Composer `ListItem.Property(Role.Composer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Role.Conductor)`</b>,
+/// \anchor ListItem_Property_Role_Conductor
+/// _string_,
+/// @return The name of the person who conducted the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Property_Role_Conductor `ListItem.Property(Role.Conductor)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Role.Orchestra)`</b>,
+/// \anchor ListItem_Property_Role_Orchestra
+/// _string_,
+/// @return The name of the orchestra performing the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Property_Role_Orchestra `ListItem.Property(Role.Orchestra)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Role.Lyricist)`</b>,
+/// \anchor ListItem_Property_Role_Lyricist
+/// _string_,
+/// @return The name of the person who wrote the lyrics of the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Property_Role_Lyricist `ListItem.Property(Role.Lyricist)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Role.Remixer)`</b>,
+/// \anchor ListItem_Property_Role_Remixer
+/// _string_,
+/// @return The name of the person who remixed the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Property_Role_Remixer `ListItem.Property(Role.Remixer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Role.Arranger)`</b>,
+/// \anchor ListItem_Property_Role_Arranger
+/// _string_,
+/// @return The name of the person who arranged the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Property_Role_Arranger `ListItem.Property(Role.Arranger)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Role.Engineer)`</b>,
+/// \anchor ListItem_Property_Role_Engineer
+/// _string_,
+/// @return The name of the person who was the engineer of the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Property_Role_Engineer `ListItem.Property(Role.Engineer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Role.Producer)`</b>,
+/// \anchor ListItem_Property_Role_Producer
+/// _string_,
+/// @return The name of the person who produced the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Property_Role_Producer `ListItem.Property(Role.Producer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Role.DJMixer)`</b>,
+/// \anchor ListItem_Property_Role_DJMixer
+/// _string_,
+/// @return The name of the dj who remixed the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Property_Role_DJMixer `ListItem.Property(Role.DJMixer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Role.Mixer)`</b>,
+/// \anchor ListItem_Property_Role_Mixer
+/// _string_,
+/// @return The name of the person who mixed the selected song.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link ListItem_Property_Role_DJMixer `ListItem.Property(Role.DJMixer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Game.VideoFilter)`</b>,
+/// \anchor ListItem_Property_Game_VideoFilter
+/// _string_,
+/// @return The video filter of the list item representing a
+/// gamewindow control (RETROPLAYER).
+/// See \link RetroPlayer_VideoFilter RetroPlayer.VideoFilter \endlink
+/// for the possible values.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Property_Game_VideoFilter `ListItem.Property(Game.VideoFilter)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Game.StretchMode)`</b>,
+/// \anchor ListItem_Property_Game_StretchMode
+/// _string_,
+/// @return The stretch mode of the list item representing a
+/// gamewindow control (RETROPLAYER).
+/// See \link RetroPlayer_StretchMode RetroPlayer.StretchMode \endlink
+/// for the possible values.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Property_Game_StretchMode `ListItem.Property(Game.StretchMode)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.Property(Game.VideoRotation)`</b>,
+/// \anchor ListItem_Property_Game_VideoRotation
+/// _integer_,
+/// @return The video rotation of the list item representing a
+/// gamewindow control (RETROPLAYER).
+/// See \link RetroPlayer_VideoRotation RetroPlayer.VideoRotation \endlink
+/// for the possible values.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link ListItem_Property_Game_VideoRotation `ListItem.Property(Game.VideoRotation)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.ParentalRating`</b>,
+/// \anchor ListItem_ParentalRating
+/// _string_,
+/// @return The parental rating of the list item (PVR).
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.CurrentItem`</b>,
+/// \anchor ListItem_CurrentItem
+/// _string_,
+/// @return The current index of the item in a container starting at 1.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_CurrentItem `ListItem.CurrentItem`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsNew`</b>,
+/// \anchor ListItem_IsNew
+/// _boolean_,
+/// @return **True** if the item is new (for example\, a Live TV show that will be first aired).
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_IsNew `ListItem.IsNew`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsPremiere`</b>,
+/// \anchor ListItem_IsPremiere
+/// _boolean_,
+/// @return **True** if the item is a premiere (for example\, a Movie first showing or season first on Live TV).
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_IsPremiere `ListItem.IsPremiere`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsFinale`</b>,
+/// \anchor ListItem_IsFinale
+/// _boolean_,
+/// @return **True** if the item is a finale (for example\, a season finale showing on Live TV).
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_IsFinale `ListItem.IsFinale`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsLive`</b>,
+/// \anchor ListItem_IsLive
+/// _boolean_,
+/// @return **True** if the item is live (for example\, a Live TV sports event).
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_IsLive `ListItem.IsLive`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.DiscTitle`</b>,
+/// \anchor ListItem_DiscTitle
+/// _string_,
+/// @return The disc title of the currently selected album or song.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_DiscTitle `ListItem.DiscTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.IsBoxset`</b>,
+/// \anchor ListItem_IsBoxset
+/// _boolean_,
+/// @return **True** if the item is part of a boxset album.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_IsBoxset `ListItem.IsBoxset`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.TotalDiscs`</b>,
+/// \anchor ListItem_TotalDiscs
+/// _boolean_,
+/// @return The total number of discs belonging to an album.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_TotalDiscs `ListItem.TotalDiscs`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.ReleaseDate`</b>,
+/// \anchor ListItem_ReleaseDate
+/// _string_,
+/// @return The release date of the item.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_ReleaseDate `ListItem.ReleaseDate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.OriginalDate`</b>,
+/// \anchor ListItem_OriginalDate
+/// _string_,
+/// @return The original release date of the item. Can be full or partial date.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_OriginalDate `ListItem.OriginalDate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.BPM`</b>,
+/// \anchor ListItem_BPM
+/// _string_,
+/// @return The BPM of a song.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_BPM `ListItem.BPM`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.UniqueID(name)`</b>,
+/// \anchor ListItem_UniqueID
+/// _string_,
+/// @return The scraped metadata id of the currently selected item in a container\,
+/// for use in dialogvideoinfo.xml.
+/// @param name - the name of the metadata provider.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_UniqueID `ListItem.UniqueID(name)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.BitRate`</b>,
+/// \anchor ListItem_BitRate
+/// _string_,
+/// @return The bitrate of a song. Actual rate for CBR\, average rate for VBR.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_BitRate `ListItem.BitRate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.SampleRate`</b>,
+/// \anchor ListItem_SampleRate
+/// _string_,
+/// @return The sample rate of a song / 1000.0 eg 44.1\, 48\, 96 etc.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_SampleRate `ListItem.SampleRate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.MusicChannels`</b>,
+/// \anchor ListItem_MusicChannels
+/// _string_,
+/// @return The number of audio channels of a song.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_No_Of_Channels `ListItem.NoOfChannels`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.TvShowDBID`</b>,
+/// \anchor ListItem_TvShowDBID
+/// _string_,
+/// @return The database id of the TvShow for the currently selected Season or Episode.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_TvShowDBID `ListItem.TvShowDBID`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`ListItem.AlbumStatus`</b>,
+/// \anchor ListItem_AlbumStatus
+/// _string_,
+/// @return The Musicbrainz release status of the album (official, bootleg, promotion etc)
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link ListItem_AlbumStatus `ListItem.AlbumStatus`\endlink
+/// }
+/// \table_row3{ <b>`ListItem.HdrType`</b>,
+/// \anchor ListItem_HdrType
+/// _string_,
+/// @return String containing the name of the detected HDR type or empty if not HDR. See \ref StreamHdrType for the list of possible values.
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link ListItem_HdrType `ListItem.HdrType`\endlink
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap listitem_labels[]= {{ "thumb", LISTITEM_THUMB },
+ { "icon", LISTITEM_ICON },
+ { "actualicon", LISTITEM_ACTUAL_ICON },
+ { "overlay", LISTITEM_OVERLAY },
+ { "label", LISTITEM_LABEL },
+ { "label2", LISTITEM_LABEL2 },
+ { "title", LISTITEM_TITLE },
+ { "tracknumber", LISTITEM_TRACKNUMBER },
+ { "artist", LISTITEM_ARTIST },
+ { "album", LISTITEM_ALBUM },
+ { "albumartist", LISTITEM_ALBUM_ARTIST },
+ { "year", LISTITEM_YEAR },
+ { "genre", LISTITEM_GENRE },
+ { "contributors", LISTITEM_CONTRIBUTORS },
+ { "contributorandrole", LISTITEM_CONTRIBUTOR_AND_ROLE },
+ { "director", LISTITEM_DIRECTOR },
+ { "disctitle", LISTITEM_DISC_TITLE },
+ { "filename", LISTITEM_FILENAME },
+ { "filenameandpath", LISTITEM_FILENAME_AND_PATH },
+ { "fileextension", LISTITEM_FILE_EXTENSION },
+ { "filenamenoextension", LISTITEM_FILENAME_NO_EXTENSION },
+ { "date", LISTITEM_DATE },
+ { "datetime", LISTITEM_DATETIME },
+ { "size", LISTITEM_SIZE },
+ { "rating", LISTITEM_RATING },
+ { "ratingandvotes", LISTITEM_RATING_AND_VOTES },
+ { "userrating", LISTITEM_USER_RATING },
+ { "votes", LISTITEM_VOTES },
+ { "mood", LISTITEM_MOOD },
+ { "programcount", LISTITEM_PROGRAM_COUNT },
+ { "duration", LISTITEM_DURATION },
+ { "isselected", LISTITEM_ISSELECTED },
+ { "isplaying", LISTITEM_ISPLAYING },
+ { "plot", LISTITEM_PLOT },
+ { "plotoutline", LISTITEM_PLOT_OUTLINE },
+ { "episode", LISTITEM_EPISODE },
+ { "season", LISTITEM_SEASON },
+ { "tvshowtitle", LISTITEM_TVSHOW },
+ { "premiered", LISTITEM_PREMIERED },
+ { "comment", LISTITEM_COMMENT },
+ { "path", LISTITEM_PATH },
+ { "foldername", LISTITEM_FOLDERNAME },
+ { "folderpath", LISTITEM_FOLDERPATH },
+ { "picturepath", LISTITEM_PICTURE_PATH },
+ { "pictureresolution",LISTITEM_PICTURE_RESOLUTION },
+ { "picturedatetime", LISTITEM_PICTURE_DATETIME },
+ { "picturedate", LISTITEM_PICTURE_DATE },
+ { "picturelongdatetime",LISTITEM_PICTURE_LONGDATETIME },
+ { "picturelongdate", LISTITEM_PICTURE_LONGDATE },
+ { "picturecomment", LISTITEM_PICTURE_COMMENT },
+ { "picturecaption", LISTITEM_PICTURE_CAPTION },
+ { "picturedesc", LISTITEM_PICTURE_DESC },
+ { "picturekeywords", LISTITEM_PICTURE_KEYWORDS },
+ { "picturecammake", LISTITEM_PICTURE_CAM_MAKE },
+ { "picturecammodel", LISTITEM_PICTURE_CAM_MODEL },
+ { "pictureaperture", LISTITEM_PICTURE_APERTURE },
+ { "picturefocallen", LISTITEM_PICTURE_FOCAL_LEN },
+ { "picturefocusdist", LISTITEM_PICTURE_FOCUS_DIST },
+ { "pictureexpmode", LISTITEM_PICTURE_EXP_MODE },
+ { "pictureexptime", LISTITEM_PICTURE_EXP_TIME },
+ { "pictureiso", LISTITEM_PICTURE_ISO },
+ { "pictureauthor", LISTITEM_PICTURE_AUTHOR },
+ { "picturebyline", LISTITEM_PICTURE_BYLINE },
+ { "picturebylinetitle", LISTITEM_PICTURE_BYLINE_TITLE },
+ { "picturecategory", LISTITEM_PICTURE_CATEGORY },
+ { "pictureccdwidth", LISTITEM_PICTURE_CCD_WIDTH },
+ { "picturecity", LISTITEM_PICTURE_CITY },
+ { "pictureurgency", LISTITEM_PICTURE_URGENCY },
+ { "picturecopyrightnotice", LISTITEM_PICTURE_COPYRIGHT_NOTICE },
+ { "picturecountry", LISTITEM_PICTURE_COUNTRY },
+ { "picturecountrycode", LISTITEM_PICTURE_COUNTRY_CODE },
+ { "picturecredit", LISTITEM_PICTURE_CREDIT },
+ { "pictureiptcdate", LISTITEM_PICTURE_IPTCDATE },
+ { "picturedigitalzoom", LISTITEM_PICTURE_DIGITAL_ZOOM },
+ { "pictureexposure", LISTITEM_PICTURE_EXPOSURE },
+ { "pictureexposurebias", LISTITEM_PICTURE_EXPOSURE_BIAS },
+ { "pictureflashused", LISTITEM_PICTURE_FLASH_USED },
+ { "pictureheadline", LISTITEM_PICTURE_HEADLINE },
+ { "picturecolour", LISTITEM_PICTURE_COLOUR },
+ { "picturelightsource", LISTITEM_PICTURE_LIGHT_SOURCE },
+ { "picturemeteringmode", LISTITEM_PICTURE_METERING_MODE },
+ { "pictureobjectname", LISTITEM_PICTURE_OBJECT_NAME },
+ { "pictureorientation", LISTITEM_PICTURE_ORIENTATION },
+ { "pictureprocess", LISTITEM_PICTURE_PROCESS },
+ { "picturereferenceservice", LISTITEM_PICTURE_REF_SERVICE },
+ { "picturesource", LISTITEM_PICTURE_SOURCE },
+ { "picturespecialinstructions", LISTITEM_PICTURE_SPEC_INSTR },
+ { "picturestate", LISTITEM_PICTURE_STATE },
+ { "picturesupplementalcategories", LISTITEM_PICTURE_SUP_CATEGORIES },
+ { "picturetransmissionreference", LISTITEM_PICTURE_TX_REFERENCE },
+ { "picturewhitebalance", LISTITEM_PICTURE_WHITE_BALANCE },
+ { "pictureimagetype", LISTITEM_PICTURE_IMAGETYPE },
+ { "picturesublocation", LISTITEM_PICTURE_SUBLOCATION },
+ { "pictureiptctime", LISTITEM_PICTURE_TIMECREATED },
+ { "picturegpslat", LISTITEM_PICTURE_GPS_LAT },
+ { "picturegpslon", LISTITEM_PICTURE_GPS_LON },
+ { "picturegpsalt", LISTITEM_PICTURE_GPS_ALT },
+ { "studio", LISTITEM_STUDIO },
+ { "country", LISTITEM_COUNTRY },
+ { "mpaa", LISTITEM_MPAA },
+ { "cast", LISTITEM_CAST },
+ { "castandrole", LISTITEM_CAST_AND_ROLE },
+ { "writer", LISTITEM_WRITER },
+ { "tagline", LISTITEM_TAGLINE },
+ { "status", LISTITEM_STATUS },
+ { "top250", LISTITEM_TOP250 },
+ { "trailer", LISTITEM_TRAILER },
+ { "sortletter", LISTITEM_SORT_LETTER },
+ { "tag", LISTITEM_TAG },
+ { "set", LISTITEM_SET },
+ { "setid", LISTITEM_SETID },
+ { "videocodec", LISTITEM_VIDEO_CODEC },
+ { "videoresolution", LISTITEM_VIDEO_RESOLUTION },
+ { "videoaspect", LISTITEM_VIDEO_ASPECT },
+ { "audiocodec", LISTITEM_AUDIO_CODEC },
+ { "audiochannels", LISTITEM_AUDIO_CHANNELS },
+ { "audiolanguage", LISTITEM_AUDIO_LANGUAGE },
+ { "subtitlelanguage", LISTITEM_SUBTITLE_LANGUAGE },
+ { "isresumable", LISTITEM_IS_RESUMABLE},
+ { "percentplayed", LISTITEM_PERCENT_PLAYED},
+ { "isfolder", LISTITEM_IS_FOLDER },
+ { "isparentfolder", LISTITEM_IS_PARENTFOLDER },
+ { "iscollection", LISTITEM_IS_COLLECTION },
+ { "originaltitle", LISTITEM_ORIGINALTITLE },
+ { "lastplayed", LISTITEM_LASTPLAYED },
+ { "playcount", LISTITEM_PLAYCOUNT },
+ { "discnumber", LISTITEM_DISC_NUMBER },
+ { "starttime", LISTITEM_STARTTIME },
+ { "endtime", LISTITEM_ENDTIME },
+ { "endtimeresume", LISTITEM_ENDTIME_RESUME },
+ { "startdate", LISTITEM_STARTDATE },
+ { "enddate", LISTITEM_ENDDATE },
+ { "nexttitle", LISTITEM_NEXT_TITLE },
+ { "nextgenre", LISTITEM_NEXT_GENRE },
+ { "nextplot", LISTITEM_NEXT_PLOT },
+ { "nextplotoutline", LISTITEM_NEXT_PLOT_OUTLINE },
+ { "nextstarttime", LISTITEM_NEXT_STARTTIME },
+ { "nextendtime", LISTITEM_NEXT_ENDTIME },
+ { "nextstartdate", LISTITEM_NEXT_STARTDATE },
+ { "nextenddate", LISTITEM_NEXT_ENDDATE },
+ { "nextduration", LISTITEM_NEXT_DURATION },
+ { "channelname", LISTITEM_CHANNEL_NAME },
+ { "channelnumberlabel", LISTITEM_CHANNEL_NUMBER },
+ { "channelgroup", LISTITEM_CHANNEL_GROUP },
+ { "hasepg", LISTITEM_HAS_EPG },
+ { "hastimer", LISTITEM_HASTIMER },
+ { "hastimerschedule", LISTITEM_HASTIMERSCHEDULE },
+ { "hasreminder", LISTITEM_HASREMINDER },
+ { "hasreminderrule", LISTITEM_HASREMINDERRULE },
+ { "hasrecording", LISTITEM_HASRECORDING },
+ { "isrecording", LISTITEM_ISRECORDING },
+ { "isplayable", LISTITEM_ISPLAYABLE },
+ { "hasarchive", LISTITEM_HASARCHIVE },
+ { "inprogress", LISTITEM_INPROGRESS },
+ { "isencrypted", LISTITEM_ISENCRYPTED },
+ { "progress", LISTITEM_PROGRESS },
+ { "dateadded", LISTITEM_DATE_ADDED },
+ { "dbtype", LISTITEM_DBTYPE },
+ { "dbid", LISTITEM_DBID },
+ { "appearances", LISTITEM_APPEARANCES },
+ { "stereoscopicmode", LISTITEM_STEREOSCOPIC_MODE },
+ { "isstereoscopic", LISTITEM_IS_STEREOSCOPIC },
+ { "imdbnumber", LISTITEM_IMDBNUMBER },
+ { "episodename", LISTITEM_EPISODENAME },
+ { "timertype", LISTITEM_TIMERTYPE },
+ { "epgeventtitle", LISTITEM_EPG_EVENT_TITLE },
+ { "epgeventicon", LISTITEM_EPG_EVENT_ICON },
+ { "timerisactive", LISTITEM_TIMERISACTIVE },
+ { "timerhaserror", LISTITEM_TIMERHASERROR },
+ { "timerhasconflict", LISTITEM_TIMERHASCONFLICT },
+ { "addonname", LISTITEM_ADDON_NAME },
+ { "addonversion", LISTITEM_ADDON_VERSION },
+ { "addoncreator", LISTITEM_ADDON_CREATOR },
+ { "addonsummary", LISTITEM_ADDON_SUMMARY },
+ { "addondescription", LISTITEM_ADDON_DESCRIPTION },
+ { "addondisclaimer", LISTITEM_ADDON_DISCLAIMER },
+ { "addonnews", LISTITEM_ADDON_NEWS },
+ { "addonbroken", LISTITEM_ADDON_BROKEN },
+ { "addonlifecycletype", LISTITEM_ADDON_LIFECYCLE_TYPE },
+ { "addonlifecycledesc", LISTITEM_ADDON_LIFECYCLE_DESC },
+ { "addontype", LISTITEM_ADDON_TYPE },
+ { "addoninstalldate", LISTITEM_ADDON_INSTALL_DATE },
+ { "addonlastupdated", LISTITEM_ADDON_LAST_UPDATED },
+ { "addonlastused", LISTITEM_ADDON_LAST_USED },
+ { "addonorigin", LISTITEM_ADDON_ORIGIN },
+ { "addonsize", LISTITEM_ADDON_SIZE },
+ { "expirationdate", LISTITEM_EXPIRATION_DATE },
+ { "expirationtime", LISTITEM_EXPIRATION_TIME },
+ { "art", LISTITEM_ART },
+ { "property", LISTITEM_PROPERTY },
+ { "parentalrating", LISTITEM_PARENTAL_RATING },
+ { "currentitem", LISTITEM_CURRENTITEM },
+ { "isnew", LISTITEM_IS_NEW },
+ { "isboxset", LISTITEM_IS_BOXSET },
+ { "totaldiscs", LISTITEM_TOTALDISCS },
+ { "releasedate", LISTITEM_RELEASEDATE },
+ { "originaldate", LISTITEM_ORIGINALDATE },
+ { "bpm", LISTITEM_BPM },
+ { "uniqueid", LISTITEM_UNIQUEID },
+ { "bitrate", LISTITEM_BITRATE },
+ { "samplerate", LISTITEM_SAMPLERATE },
+ { "musicchannels", LISTITEM_MUSICCHANNELS },
+ { "ispremiere", LISTITEM_IS_PREMIERE },
+ { "isfinale", LISTITEM_IS_FINALE },
+ { "islive", LISTITEM_IS_LIVE },
+ { "tvshowdbid", LISTITEM_TVSHOWDBID },
+ { "albumstatus", LISTITEM_ALBUMSTATUS },
+ { "isautoupdateable", LISTITEM_ISAUTOUPDATEABLE },
+ { "hdrtype", LISTITEM_VIDEO_HDR_TYPE },
+};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Visualisation Visualisation
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Visualisation.Enabled`</b>,
+/// \anchor Visualisation_Enabled
+/// _boolean_,
+/// @return **True** if any visualisation has been set in settings (so not None).
+/// <p>
+/// }
+/// \table_row3{ <b>`Visualisation.HasPresets`</b>,
+/// \anchor Visualisation_HasPresets
+/// _boolean_,
+/// @return **True** if the visualisation has built in presets.
+/// <p><hr>
+/// @skinning_v16 **[New Boolean Condition]** \link Visualisation_HasPresets `Visualisation.HasPresets`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Visualisation.Locked`</b>,
+/// \anchor Visualisation_Locked
+/// _boolean_,
+/// @return **True** if the current visualisation preset is locked (e.g. in Milkdrop).
+/// <p>
+/// }
+/// \table_row3{ <b>`Visualisation.Preset`</b>,
+/// \anchor Visualisation_Preset
+/// _string_,
+/// @return The current preset of the visualisation.
+/// <p>
+/// }
+/// \table_row3{ <b>`Visualisation.Name`</b>,
+/// \anchor Visualisation_Name
+/// _string_,
+/// @return the name of the visualisation.
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap visualisation[] = {{ "locked", VISUALISATION_LOCKED },
+ { "preset", VISUALISATION_PRESET },
+ { "haspresets", VISUALISATION_HAS_PRESETS },
+ { "name", VISUALISATION_NAME },
+ { "enabled", VISUALISATION_ENABLED }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Fanart Fanart
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Fanart.Color1`</b>,
+/// \anchor Fanart_Color1
+/// _string_,
+/// @return The first of three colors included in the currently selected
+/// Fanart theme for the parent TV Show.
+/// @note Colors are arranged Lightest to Darkest.
+/// <p>
+/// }
+/// \table_row3{ <b>`Fanart.Color2`</b>,
+/// \anchor Fanart_Color2
+/// _string_,
+/// @return The second of three colors included in the currently selected
+/// Fanart theme for the parent TV Show.
+/// @note Colors are arranged Lightest to Darkest.
+/// <p>
+/// }
+/// \table_row3{ <b>`Fanart.Color3`</b>,
+/// \anchor Fanart_Color3
+/// _string_,
+/// @return The third of three colors included in the currently selected
+/// Fanart theme for the parent TV Show.
+/// @note Colors are arranged Lightest to Darkest.
+/// <p>
+/// }
+/// \table_row3{ <b>`Fanart.Image`</b>,
+/// \anchor Fanart_Image
+/// _string_,
+/// @return The fanart image\, if any
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap fanart_labels[] = {{ "color1", FANART_COLOR1 },
+ { "color2", FANART_COLOR2 },
+ { "color3", FANART_COLOR3 },
+ { "image", FANART_IMAGE }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Skin Skin
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Skin.HasSetting(setting)`</b>,
+/// \anchor Skin_HasSetting
+/// _boolean_,
+/// @param setting - the requested skin setting
+/// @return **True** if the requested skin setting is true\, false otherwise.
+/// @sa \link Skin_SetBool `Skin.SetBool(setting[\,value])`
+/// <p>
+/// }
+/// \table_row3{ <b>`Skin.String(setting)`</b>,
+/// \anchor Skin_StringValue
+/// _string_,
+/// @param setting - the requested skin setting
+/// @return The value of the requested string setting (as a string)
+/// @sa \link Skin_SetString `Skin.SetString(setting[\,value])`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Skin.String(setting[\,value])`</b>,
+/// \anchor Skin_StringCompare
+/// _boolean_,
+/// @param setting - the requested skin setting
+/// @param value [opt] - the string value to compare the requested setting to
+/// @return **True** if the setting value equals the provided value\, false otherwise.
+/// @sa \link Skin_SetString `Skin.SetString(setting[\,value])`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Skin.HasTheme(theme)`</b>,
+/// \anchor Skin_HasTheme
+/// _boolean_,
+/// @param theme - the requested skin theme
+/// @return **True** if the requested theme is enabled\, false otherwise.
+/// @sa \link Skin_CycleTheme `Skin.Theme()`\endlink and \link Skin_CurrentTheme `Skin.CurrentTheme`\endlink.
+/// <p>
+/// }
+/// \table_row3{ <b>`Skin.CurrentTheme`</b>,
+/// \anchor Skin_CurrentTheme
+/// _string_,
+/// @return The current selected skin theme.
+/// <p>
+/// }
+/// \table_row3{ <b>`Skin.CurrentColourTheme`</b>,
+/// \anchor Skin_CurrentColourTheme
+/// _string_,
+/// @return the current selected colour theme of the skin.
+/// <p>
+/// }
+/// \table_row3{ <b>`Skin.AspectRatio`</b>,
+/// \anchor Skin_AspectRatio
+/// _string_,
+/// @return The closest aspect ratio match using the resolution info from the skin's `addon.xml` file.
+/// <p>
+/// }
+/// \table_row3{ <b>`Skin.Font`</b>,
+/// \anchor Skin_Font
+/// _string_,
+/// @return the current fontset from `Font.xml`.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link Skin_Font `Skin.Font`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Skin.Numeric(settingid)`</b>,
+/// \anchor Skin_Numeric
+/// _integer_,
+/// @return return the setting value as an integer/numeric value.
+/// @sa \link Skin_SetNumeric `Skin.SetNumeric(settingid)`\endlink
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link Skin_Numeric `Skin.Numeric(settingid)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Skin.TimerElapsedSecs(timer)`</b>,
+/// \anchor Skin_TimerElapsedSecs
+/// _integer_ \, _string_,
+/// @return The elapsed time in seconds for the provided `timer`.
+/// @param timer - the timer name
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link Skin_TimerElapsedSecs `Skin.TimerElapsedSecs(timer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Skin.TimerIsRunning(timer)`</b>,
+/// \anchor Skin_TimerIsRunning
+/// _boolean_,
+/// @return **True** if the given `timer` is active\, false otherwise.
+/// @param timer - the timer name
+/// <p><hr>
+/// @skinning_v20 **[New Infolabel]** \link Skin_TimerIsRunning `Skin.TimerIsRunning(timer)`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap skin_labels[] = {{ "currenttheme", SKIN_THEME },
+ { "currentcolourtheme",SKIN_COLOUR_THEME },
+ { "aspectratio", SKIN_ASPECT_RATIO},
+ { "font", SKIN_FONT}};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Window Window
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Window.IsMedia`</b>,
+/// \anchor Window_IsMedia
+/// _boolean_,
+/// @return **True** if this window is a media window (programs\, music\, video\,
+/// scripts\, pictures)
+/// <p>
+/// }
+/// \table_row3{ <b>`Window.Is(window)`</b>,
+/// \anchor Window_Is
+/// _boolean_,
+/// @return **True** if the window with the given name is the window which is currently rendered.
+/// @param window - the name of the window
+/// @note Useful in xml files that are shared between multiple windows or dialogs.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \ref Window_Is "Window.Is(window)"
+/// <p>
+/// }
+/// \table_row3{ <b>`Window.IsActive(window)`</b>,
+/// \anchor Window_IsActive
+/// _boolean_,
+/// @return **True** if the window with id or title _window_ is active
+/// @param window - the id or name of the window
+/// @note Excludes fade out time on dialogs
+/// <p>
+/// }
+/// \table_row3{ <b>`Window.IsVisible(window)`</b>,
+/// \anchor Window_IsVisible
+/// _boolean_,
+/// @return **True** if the window is visible
+/// @note Includes fade out time on dialogs
+/// <p>
+/// }
+/// \table_row3{ <b>`Window.IsTopmost(window)`</b>,
+/// \anchor Window_IsTopmost
+/// _boolean_,
+/// @return **True** if the window with id or title _window_ is on top of the
+/// window stack.
+/// @param window - the id or name of the window
+/// @note Excludes fade out time on dialogs
+/// @deprecated use \ref Window_IsDialogTopmost "Window.IsDialogTopmost(dialog)" instead
+/// <p>
+/// }
+/// \table_row3{ <b>`Window.IsDialogTopmost(dialog)`</b>,
+/// \anchor Window_IsDialogTopmost
+/// _boolean_,
+/// @return **True** if the dialog with id or title _dialog_ is on top of the
+/// dialog stack.
+/// @param window - the id or name of the window
+/// @note Excludes fade out time on dialogs
+/// <p>
+/// }
+/// \table_row3{ <b>`Window.IsModalDialogTopmost(dialog)`</b>,
+/// \anchor Window_IsModalDialogTopmost
+/// _boolean_,
+/// @return **True** if the dialog with id or title _dialog_ is on top of the
+/// modal dialog stack
+/// @note Excludes fade out time on dialogs
+/// <p>
+/// }
+/// \table_row3{ <b>`Window.Previous(window)`</b>,
+/// \anchor Window_Previous
+/// _boolean_,
+/// @return **True** if the window with id or title _window_ is being moved from.
+/// @param window - the window id or title
+/// @note Only valid while windows are changing.
+/// <p>
+/// }
+/// \table_row3{ <b>`Window.Next(window)`</b>,
+/// \anchor Window_Next
+/// _boolean_,
+/// @return **True** if the window with id or title _window_ is being moved to.
+/// @param window - the window id or title
+/// @note Only valid while windows are changing.
+/// <p>
+/// }
+/// \table_row3{ <b>`Window.Property(Addon.ID)`</b>,
+/// \anchor Window_Property_AddonId
+/// _string_,
+/// @return The id of the selected addon\, in `DialogAddonSettings.xml`.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link Window_Property_AddonId `Window.Property(Addon.ID)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Window.Property(IsRadio)`</b>,
+/// \anchor Window_Property_IsRadio
+/// _string_,
+/// @return "true" if the window is a radio window\, empty string otherwise (for use in the PVR windows).
+/// <p>
+/// }
+/// \table_row3{ <b>`Window([window]).Property(key)`</b>,
+/// \anchor Window_Window_Property_key
+/// _string_,
+/// @return A window property.
+/// @param window - [opt] window id or name.
+/// @param key - any value.
+/// <p>
+/// }
+/// \table_row3{ <b>`Window(AddonBrowser).Property(Updated)`</b>,
+/// \anchor Window_Addonbrowser_Property_Updated
+/// _string_,
+/// @return The date and time the addon repo was last checked for updates.
+/// @todo move to a future window document.
+/// <p><hr>
+/// @skinning_v15 **[New Infolabel]** \link Window_Addonbrowser_Property_Updated `Window(AddonBrowser).Property(Updated)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Window(Weather).Property(property)`</b>,
+/// \anchor Window_Weather_Property
+/// _string_,
+/// @return The property for the weather window.
+/// @param property - The requested property. The following are available:
+/// - Current.ConditionIcon
+/// - Day[0-6].OutlookIcon
+/// - Current.FanartCode
+/// - Day[0-6].FanartCode
+/// - WeatherProviderLogo
+/// - Daily.%i.OutlookIcon
+/// - 36Hour.%i.OutlookIcon
+/// - Weekend.%i.OutlookIcon
+/// - Hourly.%i.OutlookIcon
+/// @todo move to a future window document.
+/// <p><hr>
+/// @skinning_v16 **[Updated infolabel]** \link Window_Weather_Property `Window(Weather).Property(property)`\endlink
+/// For skins that support extended weather info\, the following infolabels have been changed:
+/// - Daily.%i.OutlookIcon
+/// - 36Hour.%i.OutlookIcon
+/// - Weekend.%i.OutlookIcon
+/// - Hourly.%i.OutlookIcon
+///
+/// previously the openweathermap addon would provide the full\, hardcoded path to the icon
+/// ie. `resource://resource.images.weathericons.default/28.png`
+/// to make it easier for skins to work with custom icon sets\, it now will return the filename only
+/// i.e. 28.png
+/// @skinning_v13 **[Infolabel Updated]** \link Window_Weather_Property `Window(Weather).Property(property)`\endlink
+/// added `WeatherProviderLogo` property - weather provider logo (for weather addons that support it).
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap window_bools[] = {{ "ismedia", WINDOW_IS_MEDIA },
+ { "is", WINDOW_IS },
+ { "isactive", WINDOW_IS_ACTIVE },
+ { "isvisible", WINDOW_IS_VISIBLE },
+ { "istopmost", WINDOW_IS_DIALOG_TOPMOST }, //! @deprecated, remove in v19
+ { "isdialogtopmost", WINDOW_IS_DIALOG_TOPMOST },
+ { "ismodaldialogtopmost", WINDOW_IS_MODAL_DIALOG_TOPMOST },
+ { "previous", WINDOW_PREVIOUS },
+ { "next", WINDOW_NEXT }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Control Control
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Control.HasFocus(id)`</b>,
+/// \anchor Control_HasFocus
+/// _boolean_,
+/// @return **True** if the currently focused control has id "id".
+/// @param id - The id of the control
+/// <p>
+/// }
+/// \table_row3{ <b>`Control.IsVisible(id)`</b>,
+/// \anchor Control_IsVisible
+/// _boolean_,
+/// @return **True** if the control with id "id" is visible.
+/// @param id - The id of the control
+/// <p>
+/// }
+/// \table_row3{ <b>`Control.IsEnabled(id)`</b>,
+/// \anchor Control_IsEnabled
+/// _boolean_,
+/// @return **True** if the control with id "id" is enabled.
+/// @param id - The id of the control
+/// <p>
+/// }
+/// \table_row3{ <b>`Control.GetLabel(id)[.index()]`</b>,
+/// \anchor Control_GetLabel
+/// _string_,
+/// @return The label value or texture name of the control with the given id.
+/// @param id - The id of the control
+/// @param index - [opt] Optionally you can specify index(1) to retrieve label2 from an Edit
+/// control.
+/// <p><hr>
+/// @skinning_v15 **[Infolabel Updated]** \link Control_GetLabel `Control.GetLabel(id)`\endlink
+/// added index parameter - allows skinner to retrieve label2 of a control. Only edit controls are supported.
+/// ** Example** : `Control.GetLabel(999).index(1)` where:
+/// - index(0) = label
+/// - index(1) = label2
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap control_labels[] = {{ "hasfocus", CONTROL_HAS_FOCUS },
+ { "isvisible", CONTROL_IS_VISIBLE },
+ { "isenabled", CONTROL_IS_ENABLED },
+ { "getlabel", CONTROL_GET_LABEL }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Playlist Playlist
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Playlist.Length(media)`</b>,
+/// \anchor Playlist_Length
+/// _integer_,
+/// @return The total size of the current playlist.
+/// @param media - [opt] mediatype with is either
+/// video or music.
+/// <p>
+/// }
+/// \table_row3{ <b>`Playlist.Position(media)`</b>,
+/// \anchor Playlist_Position
+/// _integer_,
+/// @return The position of the current item in the current playlist.
+/// @param media - [opt] mediatype with is either
+/// video or music.
+/// <p>
+/// }
+/// \table_row3{ <b>`Playlist.Random`</b>,
+/// \anchor Playlist_Random
+/// _integer_,
+/// @return String ID for the random mode:
+/// - **16041** (On)
+/// - **591** (Off)
+/// <p><hr>
+/// @skinning_v18 **[Infolabel Updated]** \link Playlist_Random `Playlist.Random`\endlink will
+/// now return **On/Off**
+/// <p>
+/// }
+/// \table_row3{ <b>`Playlist.Repeat`</b>,
+/// \anchor Playlist_Repeat
+/// _integer_,
+/// @return The String Id for the repeat mode. It can be one of the following
+/// values:
+/// - **592** (Repeat One)
+/// - **593** (Repeat All)
+/// - **594** (Repeat Off)
+/// <p>
+/// }
+/// \table_row3{ <b>`Playlist.IsRandom`</b>,
+/// \anchor Playlist_IsRandom
+/// _boolean_,
+/// @return **True** if the player is in random mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`Playlist.IsRepeat`</b>,
+/// \anchor Playlist_IsRepeat
+/// _boolean_,
+/// @return **True** if the player is in repeat all mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`Playlist.IsRepeatOne`</b>,
+/// \anchor Playlist_IsRepeatOne
+/// _boolean_,
+/// @return **True** if the player is in repeat one mode.
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap playlist[] = {{ "length", PLAYLIST_LENGTH },
+ { "position", PLAYLIST_POSITION },
+ { "random", PLAYLIST_RANDOM },
+ { "repeat", PLAYLIST_REPEAT },
+ { "israndom", PLAYLIST_ISRANDOM },
+ { "isrepeat", PLAYLIST_ISREPEAT },
+ { "isrepeatone", PLAYLIST_ISREPEATONE }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Pvr Pvr
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`PVR.IsRecording`</b>,
+/// \anchor PVR_IsRecording
+/// _boolean_,
+/// @return **True** when the system is recording a tv or radio programme.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.HasTimer`</b>,
+/// \anchor PVR_HasTimer
+/// _boolean_,
+/// @return **True** when a recording timer is active.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.HasTVChannels`</b>,
+/// \anchor PVR_HasTVChannels
+/// _boolean_,
+/// @return **True** if there are TV channels available.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.HasRadioChannels`</b>,
+/// \anchor PVR_HasRadioChannels
+/// _boolean_,
+/// @return **True** if there are radio channels available.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.HasNonRecordingTimer`</b>,
+/// \anchor PVR_HasNonRecordingTimer
+/// _boolean_,
+/// @return **True** if there are timers present who currently not do recording.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.BackendName`</b>,
+/// \anchor PVR_BackendName
+/// _string_,
+/// @return The name of the backend being used.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.BackendVersion`</b>,
+/// \anchor PVR_BackendVersion
+/// _string_,
+/// @return The version of the backend that's being used.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.BackendHost`</b>,
+/// \anchor PVR_BackendHost
+/// _string_,
+/// @return The backend hostname.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.BackendDiskSpace`</b>,
+/// \anchor PVR_BackendDiskSpace
+/// _string_,
+/// @return The available diskspace on the backend as string with size.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.BackendDiskSpaceProgr`</b>,
+/// \anchor PVR_BackendDiskSpaceProgr
+/// _integer_,
+/// @return The available diskspace on the backend as percent value.
+/// <p><hr>
+/// @skinning_v14 **[New Infolabel]** \link PVR_BackendDiskSpaceProgr `PVR.BackendDiskSpaceProgr`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.BackendChannels`</b>,
+/// \anchor PVR_BackendChannels
+/// _string (integer)_,
+/// @return The number of available channels the backend provides.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.BackendTimers`</b>,
+/// \anchor PVR_BackendTimers
+/// _string (integer)_,
+/// @return The number of timers set for the backend.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.BackendRecordings`</b>,
+/// \anchor PVR_BackendRecordings
+/// _string (integer)_,
+/// @return The number of recordings available on the backend.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.BackendDeletedRecordings`</b>,
+/// \anchor PVR_BackendDeletedRecordings
+/// _string (integer)_,
+/// @return The number of deleted recordings present on the backend.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.BackendNumber`</b>,
+/// \anchor PVR_BackendNumber
+/// _string_,
+/// @return The backend number.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TotalDiscSpace`</b>,
+/// \anchor PVR_TotalDiscSpace
+/// _string_,
+/// @return The total diskspace available for recordings.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.NextTimer`</b>,
+/// \anchor PVR_NextTimer
+/// _boolean_,
+/// @return The next timer date.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.IsPlayingTV`</b>,
+/// \anchor PVR_IsPlayingTV
+/// _boolean_,
+/// @return **True** when live tv is being watched.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.IsPlayingRadio`</b>,
+/// \anchor PVR_IsPlayingRadio
+/// _boolean_,
+/// @return **True** when live radio is being listened to.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.IsPlayingRecording`</b>,
+/// \anchor PVR_IsPlayingRecording
+/// _boolean_,
+/// @return **True** when a recording is being watched.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.IsPlayingEpgTag`</b>,
+/// \anchor PVR_IsPlayingEpgTag
+/// _boolean_,
+/// @return **True** when an epg tag is being watched.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.EpgEventProgress`</b>,
+/// \anchor PVR_EpgEventProgress
+/// _integer_,
+/// @return The percentage complete of the currently playing epg event.
+/// <p><hr>
+/// @skinning_v18 **[Infolabel Updated]** \link PVR_EpgEventProgress `PVR.EpgEventProgress`\endlink replaces
+/// the old `PVR.Progress` infolabel.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamClient`</b>,
+/// \anchor PVR_ActStreamClient
+/// _string_,
+/// @return The stream client name.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamDevice`</b>,
+/// \anchor PVR_ActStreamDevice
+/// _string_,
+/// @return The stream device name.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamStatus`</b>,
+/// \anchor PVR_ActStreamStatus
+/// _string_,
+/// @return The status of the stream.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamSignal`</b>,
+/// \anchor PVR_ActStreamSignal
+/// _string_,
+/// @return The signal quality of the stream.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamSnr`</b>,
+/// \anchor PVR_ActStreamSnr
+/// _string_,
+/// @return The signal to noise ratio of the stream.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamBer`</b>,
+/// \anchor PVR_ActStreamBer
+/// _string_,
+/// @return The bit error rate of the stream.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamUnc`</b>,
+/// \anchor PVR_ActStreamUnc
+/// _string_,
+/// @return The UNC value of the stream.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamProgrSignal`</b>,
+/// \anchor PVR_ActStreamProgrSignal
+/// _integer_,
+/// @return The signal quality of the programme.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamProgrSnr`</b>,
+/// \anchor PVR_ActStreamProgrSnr
+/// _integer_,
+/// @return The signal to noise ratio of the programme.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamIsEncrypted`</b>,
+/// \anchor PVR_ActStreamIsEncrypted
+/// _boolean_,
+/// @return **True** when channel is encrypted on source.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamEncryptionName`</b>,
+/// \anchor PVR_ActStreamEncryptionName
+/// _string_,
+/// @return The encryption used on the stream.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamServiceName`</b>,
+/// \anchor PVR_ActStreamServiceName
+/// _string_,
+/// @return The service name of played channel if available.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamMux`</b>,
+/// \anchor PVR_ActStreamMux
+/// _string_,
+/// @return The multiplex type of played channel if available.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ActStreamProviderName`</b>,
+/// \anchor PVR_ActStreamProviderName
+/// _string_,
+/// @return The provider name of the played channel if available.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.IsTimeShift`</b>,
+/// \anchor PVR_IsTimeShift
+/// _boolean_,
+/// @return **True** when for channel is timeshift available.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeShiftProgress`</b>,
+/// \anchor PVR_TimeShiftProgress
+/// _integer_,
+/// @return The position of currently timeshifted title on TV as integer.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeShiftSeekbar`</b>,
+/// \anchor PVR_TimeShiftSeekbar
+/// _integer_,
+/// @return The percentage we are seeking to in a timeshifted title.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link PVR_TimeShiftSeekbar `PVR.TimeShiftSeekbar`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.NowRecordingTitle`</b>,
+/// \anchor PVR_NowRecordingTitle
+/// _string_,
+/// @return The title of the programme being recorded.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.NowRecordingDateTime`</b>,
+/// \anchor PVR_NowRecordingDateTime
+/// _Date/Time string_,
+/// @return The start date and time of the current recording.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.NowRecordingChannel`</b>,
+/// \anchor PVR_NowRecordingChannel
+/// _string_,
+/// @return The channel name of the current recording.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.NowRecordingChannelIcon`</b>,
+/// \anchor PVR_NowRecordingChannelIcon
+/// _string_,
+/// @return The icon of the current recording channel.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.NextRecordingTitle`</b>,
+/// \anchor PVR_NextRecordingTitle
+/// _string_,
+/// @return The title of the next programme that will be recorded.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.NextRecordingDateTime`</b>,
+/// \anchor PVR_NextRecordingDateTime
+/// _Date/Time string_,
+/// @return The start date and time of the next recording.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.NextRecordingChannel`</b>,
+/// \anchor PVR_NextRecordingChannel
+/// _string_,
+/// @return The channel name of the next recording.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.NextRecordingChannelIcon`</b>,
+/// \anchor PVR_NextRecordingChannelIcon
+/// _string_,
+/// @return The icon of the next recording channel.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TVNowRecordingTitle`</b>,
+/// \anchor PVR_TVNowRecordingTitle
+/// _string_,
+/// @return The title of the tv programme being recorded.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_TVNowRecordingTitle `PVR.TVNowRecordingTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TVNowRecordingDateTime`</b>,
+/// \anchor PVR_TVNowRecordingDateTime
+/// _Date/Time string_,
+/// @return The start date and time of the current tv recording.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_TVNowRecordingDateTime `PVR.TVNowRecordingDateTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TVNowRecordingChannel`</b>,
+/// \anchor PVR_TVNowRecordingChannel
+/// _string_,
+/// @return The channel name of the current tv recording.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_TVNowRecordingChannel `PVR.TVNowRecordingChannel`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TVNowRecordingChannelIcon`</b>,
+/// \anchor PVR_TVNowRecordingChannelIcon
+/// _string_,
+/// @return The icon of the current recording TV channel.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_TVNowRecordingChannelIcon `PVR.TVNowRecordingChannelIcon`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TVNextRecordingTitle`</b>,
+/// \anchor PVR_TVNextRecordingTitle
+/// _string_,
+/// @return The title of the next tv programme that will be recorded.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_TVNextRecordingTitle `PVR.TVNextRecordingTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TVNextRecordingDateTime`</b>,
+/// \anchor PVR_TVNextRecordingDateTime
+/// _Date/Time string_,
+/// @return The start date and time of the next tv recording.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_TVNextRecordingDateTime `PVR.TVNextRecordingDateTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TVNextRecordingChannel`</b>,
+/// \anchor PVR_TVNextRecordingChannel
+/// _string_,
+/// @return The channel name of the next tv recording.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_TVNextRecordingChannel `PVR.TVNextRecordingChannel`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TVNextRecordingChannelIcon`</b>,
+/// \anchor PVR_TVNextRecordingChannelIcon
+/// _string_,
+/// @return The icon of the next recording tv channel.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_TVNextRecordingChannelIcon `PVR.TVNextRecordingChannelIcon`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.RadioNowRecordingTitle`</b>,
+/// \anchor PVR_RadioNowRecordingTitle
+/// _string_,
+/// @return The title of the radio programme being recorded.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_RadioNowRecordingTitle `PVR.RadioNowRecordingTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.RadioNowRecordingDateTime`</b>,
+/// \anchor PVR_RadioNowRecordingDateTime
+/// _Date/Time string_,
+/// @return The start date and time of the current radio recording.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_RadioNowRecordingDateTime `PVR.RadioNowRecordingDateTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.RadioNowRecordingChannel`</b>,
+/// \anchor PVR_RadioNowRecordingChannel
+/// _string_,
+/// @return The channel name of the current radio recording.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_RadioNowRecordingChannel `PVR.RadioNowRecordingChannel`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.RadioNowRecordingChannelIcon`</b>,
+/// \anchor PVR_RadioNowRecordingChannelIcon
+/// _string_,
+/// @return The icon of the current recording radio channel.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_RadioNowRecordingChannelIcon `PVR.RadioNowRecordingChannelIcon`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.RadioNextRecordingTitle`</b>,
+/// \anchor PVR_RadioNextRecordingTitle
+/// _string_,
+/// @return The title of the next radio programme that will be recorded.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_RadioNextRecordingTitle `PVR.RadioNextRecordingTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.RadioNextRecordingDateTime`</b>,
+/// \anchor PVR_RadioNextRecordingDateTime
+/// _Date/Time string_,
+/// @return The start date and time of the next radio recording.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_RadioNextRecordingDateTime `PVR.RadioNextRecordingDateTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.RadioNextRecordingChannel`</b>,
+/// \anchor PVR_RadioNextRecordingChannel
+/// _string_,
+/// @return The channel name of the next radio recording.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_RadioNextRecordingChannel `PVR.RadioNextRecordingChannel`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.RadioNextRecordingChannelIcon`</b>,
+/// \anchor PVR_RadioNextRecordingChannelIcon
+/// _string_,
+/// @return The icon of the next recording radio channel.
+/// <p><hr>
+/// @skinning_v17 **[New Infolabel]** \link PVR_RadioNextRecordingChannelIcon `PVR.RadioNextRecordingChannelIcon`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.IsRecordingTV`</b>,
+/// \anchor PVR_IsRecordingTV
+/// _boolean_,
+/// @return **True** when the system is recording a tv programme.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link PVR_IsRecordingTV `PVR.IsRecordingTV`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.HasTVTimer`</b>,
+/// \anchor PVR_HasTVTimer
+/// _boolean_,
+/// @return **True** if at least one tv timer is active.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link PVR_HasTVTimer `PVR.HasTVTimer`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.HasNonRecordingTVTimer`</b>,
+/// \anchor PVR_HasNonRecordingTVTimer
+/// _boolean_,
+/// @return **True** if there are tv timers present who currently not do recording.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link PVR_HasNonRecordingTVTimer `PVR.HasNonRecordingTVTimer`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.IsRecordingRadio`</b>,
+/// \anchor PVR_IsRecordingRadio
+/// _boolean_,
+/// @return **True** when the system is recording a radio programme.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link PVR_IsRecordingRadio `PVR.IsRecordingRadio`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.HasRadioTimer`</b>,
+/// \anchor PVR_HasRadioTimer
+/// _boolean_,
+/// @return **True** if at least one radio timer is active.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link PVR_HasRadioTimer `PVR.HasRadioTimer`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.HasNonRecordingRadioTimer`</b>,
+/// \anchor PVR_HasNonRecordingRadioTimer
+/// _boolean_,
+/// @return **True** if there are radio timers present who currently not do recording.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link PVR_HasNonRecordingRadioTimer `PVR.HasRadioTimer`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.ChannelNumberInput`</b>,
+/// \anchor PVR_ChannelNumberInput
+/// _string_,
+/// @return The currently entered channel number while in numeric channel input mode\, an empty string otherwise.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_ChannelNumberInput `PVR.ChannelNumberInput`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.CanRecordPlayingChannel`</b>,
+/// \anchor PVR_CanRecordPlayingChannel
+/// _boolean_,
+/// @return **True** if PVR is currently playing a channel and if this channel can be recorded.
+/// <p><hr>
+/// @skinning_v18 **[Infolabel Updated]** \link PVR_CanRecordPlayingChannel `PVR.CanRecordPlayingChannel`\endlink replaces
+/// the old `Player.CanRecord` infolabel.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.IsRecordingPlayingChannel`</b>,
+/// \anchor PVR_IsRecordingPlayingChannel
+/// _boolean_,
+/// @return **True** if PVR is currently playing a channel and if this channel is currently recorded.
+/// <p><hr>
+/// @skinning_v18 **[Infolabel Updated]** \link PVR_IsRecordingPlayingChannel `PVR.IsRecordingPlayingChannel`\endlink replaces
+/// the old `Player.Recording` infolabel.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.IsPlayingActiveRecording`</b>,
+/// \anchor PVR_IsPlayingActiveRecording
+/// _boolean_,
+/// @return **True** if PVR is currently playing an in progress recording.
+/// <p><hr>
+/// @skinning_v19 **[New Infolabel]** \link PVR_IsPlayingActiveRecording `PVR.IsPlayingActiveRecording`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeshiftProgressPlayPos`</b>,
+/// \anchor PVR_TimeshiftProgressPlayPos
+/// _integer_,
+/// @return The percentage of the current play position within the PVR timeshift progress.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_TimeshiftProgressPlayPos `PVR.TimeshiftProgressPlayPos`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeshiftProgressEpgStart`</b>,
+/// \anchor PVR_TimeshiftProgressEpgStart
+/// _integer_,
+/// @return The percentage of the start of the currently playing epg event within the PVR timeshift progress.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_TimeshiftProgressEpgStart `PVR.TimeshiftProgressEpgStart`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeshiftProgressEpgEnd`</b>,
+/// \anchor PVR_TimeshiftProgressEpgEnd
+/// _integer_,
+/// @return The percentage of the end of the currently playing epg event within the PVR timeshift progress.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_TimeshiftProgressEpgEnd `PVR.TimeshiftProgressEpgEnd`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeshiftProgressBufferStart`</b>,
+/// \anchor PVR_TimeshiftProgressBufferStart
+/// _integer_,
+/// @return The percentage of the start of the timeshift buffer within the PVR timeshift progress.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_TimeshiftProgressBufferStart `PVR.TimeshiftProgressBufferStart`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeshiftProgressBufferEnd`</b>,
+/// \anchor PVR_TimeshiftProgressBufferEnd
+/// _integer_,
+/// @return The percentage of the end of the timeshift buffer within the PVR timeshift progress.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_TimeshiftProgressBufferEnd `PVR.TimeshiftProgressBufferEnd`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.EpgEventIcon`</b>,
+/// \anchor PVR_EpgEventIcon
+/// _string_,
+/// @return The icon of the currently playing epg event\, if any.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_EpgEventIcon `PVR_EpgEventIcon`\endlink
+/// <p>
+/// }
+///
+const infomap pvr[] = {{ "isrecording", PVR_IS_RECORDING },
+ { "hastimer", PVR_HAS_TIMER },
+ { "hastvchannels", PVR_HAS_TV_CHANNELS },
+ { "hasradiochannels", PVR_HAS_RADIO_CHANNELS },
+ { "hasnonrecordingtimer", PVR_HAS_NONRECORDING_TIMER },
+ { "backendname", PVR_BACKEND_NAME },
+ { "backendversion", PVR_BACKEND_VERSION },
+ { "backendhost", PVR_BACKEND_HOST },
+ { "backenddiskspace", PVR_BACKEND_DISKSPACE },
+ { "backenddiskspaceprogr", PVR_BACKEND_DISKSPACE_PROGR },
+ { "backendchannels", PVR_BACKEND_CHANNELS },
+ { "backendtimers", PVR_BACKEND_TIMERS },
+ { "backendrecordings", PVR_BACKEND_RECORDINGS },
+ { "backenddeletedrecordings", PVR_BACKEND_DELETED_RECORDINGS },
+ { "backendnumber", PVR_BACKEND_NUMBER },
+ { "totaldiscspace", PVR_TOTAL_DISKSPACE },
+ { "nexttimer", PVR_NEXT_TIMER },
+ { "isplayingtv", PVR_IS_PLAYING_TV },
+ { "isplayingradio", PVR_IS_PLAYING_RADIO },
+ { "isplayingrecording", PVR_IS_PLAYING_RECORDING },
+ { "isplayingepgtag", PVR_IS_PLAYING_EPGTAG },
+ { "epgeventprogress", PVR_EPG_EVENT_PROGRESS },
+ { "actstreamclient", PVR_ACTUAL_STREAM_CLIENT },
+ { "actstreamdevice", PVR_ACTUAL_STREAM_DEVICE },
+ { "actstreamstatus", PVR_ACTUAL_STREAM_STATUS },
+ { "actstreamsignal", PVR_ACTUAL_STREAM_SIG },
+ { "actstreamsnr", PVR_ACTUAL_STREAM_SNR },
+ { "actstreamber", PVR_ACTUAL_STREAM_BER },
+ { "actstreamunc", PVR_ACTUAL_STREAM_UNC },
+ { "actstreamprogrsignal", PVR_ACTUAL_STREAM_SIG_PROGR },
+ { "actstreamprogrsnr", PVR_ACTUAL_STREAM_SNR_PROGR },
+ { "actstreamisencrypted", PVR_ACTUAL_STREAM_ENCRYPTED },
+ { "actstreamencryptionname", PVR_ACTUAL_STREAM_CRYPTION },
+ { "actstreamservicename", PVR_ACTUAL_STREAM_SERVICE },
+ { "actstreammux", PVR_ACTUAL_STREAM_MUX },
+ { "actstreamprovidername", PVR_ACTUAL_STREAM_PROVIDER },
+ { "istimeshift", PVR_IS_TIMESHIFTING },
+ { "timeshiftprogress", PVR_TIMESHIFT_PROGRESS },
+ { "timeshiftseekbar", PVR_TIMESHIFT_SEEKBAR },
+ { "nowrecordingtitle", PVR_NOW_RECORDING_TITLE },
+ { "nowrecordingdatetime", PVR_NOW_RECORDING_DATETIME },
+ { "nowrecordingchannel", PVR_NOW_RECORDING_CHANNEL },
+ { "nowrecordingchannelicon", PVR_NOW_RECORDING_CHAN_ICO },
+ { "nextrecordingtitle", PVR_NEXT_RECORDING_TITLE },
+ { "nextrecordingdatetime", PVR_NEXT_RECORDING_DATETIME },
+ { "nextrecordingchannel", PVR_NEXT_RECORDING_CHANNEL },
+ { "nextrecordingchannelicon", PVR_NEXT_RECORDING_CHAN_ICO },
+ { "tvnowrecordingtitle", PVR_TV_NOW_RECORDING_TITLE },
+ { "tvnowrecordingdatetime", PVR_TV_NOW_RECORDING_DATETIME },
+ { "tvnowrecordingchannel", PVR_TV_NOW_RECORDING_CHANNEL },
+ { "tvnowrecordingchannelicon", PVR_TV_NOW_RECORDING_CHAN_ICO },
+ { "tvnextrecordingtitle", PVR_TV_NEXT_RECORDING_TITLE },
+ { "tvnextrecordingdatetime", PVR_TV_NEXT_RECORDING_DATETIME },
+ { "tvnextrecordingchannel", PVR_TV_NEXT_RECORDING_CHANNEL },
+ { "tvnextrecordingchannelicon", PVR_TV_NEXT_RECORDING_CHAN_ICO },
+ { "radionowrecordingtitle", PVR_RADIO_NOW_RECORDING_TITLE },
+ { "radionowrecordingdatetime", PVR_RADIO_NOW_RECORDING_DATETIME },
+ { "radionowrecordingchannel", PVR_RADIO_NOW_RECORDING_CHANNEL },
+ { "radionowrecordingchannelicon", PVR_RADIO_NOW_RECORDING_CHAN_ICO },
+ { "radionextrecordingtitle", PVR_RADIO_NEXT_RECORDING_TITLE },
+ { "radionextrecordingdatetime", PVR_RADIO_NEXT_RECORDING_DATETIME },
+ { "radionextrecordingchannel", PVR_RADIO_NEXT_RECORDING_CHANNEL },
+ { "radionextrecordingchannelicon", PVR_RADIO_NEXT_RECORDING_CHAN_ICO },
+ { "isrecordingtv", PVR_IS_RECORDING_TV },
+ { "hastvtimer", PVR_HAS_TV_TIMER },
+ { "hasnonrecordingtvtimer", PVR_HAS_NONRECORDING_TV_TIMER },
+ { "isrecordingradio", PVR_IS_RECORDING_RADIO },
+ { "hasradiotimer", PVR_HAS_RADIO_TIMER },
+ { "hasnonrecordingradiotimer", PVR_HAS_NONRECORDING_RADIO_TIMER },
+ { "channelnumberinput", PVR_CHANNEL_NUMBER_INPUT },
+ { "canrecordplayingchannel", PVR_CAN_RECORD_PLAYING_CHANNEL },
+ { "isrecordingplayingchannel", PVR_IS_RECORDING_PLAYING_CHANNEL },
+ { "isplayingactiverecording", PVR_IS_PLAYING_ACTIVE_RECORDING },
+ { "timeshiftprogressplaypos", PVR_TIMESHIFT_PROGRESS_PLAY_POS },
+ { "timeshiftprogressepgstart", PVR_TIMESHIFT_PROGRESS_EPG_START },
+ { "timeshiftprogressepgend", PVR_TIMESHIFT_PROGRESS_EPG_END },
+ { "timeshiftprogressbufferstart", PVR_TIMESHIFT_PROGRESS_BUFFER_START },
+ { "timeshiftprogressbufferend", PVR_TIMESHIFT_PROGRESS_BUFFER_END },
+ { "epgeventicon", PVR_EPG_EVENT_ICON }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \table_row3{ <b>`PVR.EpgEventDuration`</b>,
+/// \anchor PVR_EpgEventDuration
+/// _string_,
+/// @return The duration of the currently playing epg event in the
+/// format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p><hr>
+/// @skinning_v18 **[Infolabel Updated]** \link PVR_EpgEventDuration `PVR.EpgEventDuration`\endlink replaces
+/// the old `PVR.Duration` infolabel.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.EpgEventDuration(format)`</b>,
+/// \anchor PVR_EpgEventDuration_format
+/// _string_,
+/// @return The duration of the currently playing EPG event in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.EpgEventElapsedTime`</b>,
+/// \anchor PVR_EpgEventElapsedTime
+/// _string_,
+/// @return the time of the current position of the currently playing epg event in the
+/// format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p><hr>
+/// @skinning_v18 **[Infolabel Updated]** \link PVR_EpgEventElapsedTime `PVR.EpgEventElapsedTime`\endlink replaces
+/// the old `PVR.Time` infolabel.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.EpgEventElapsedTime(format)`</b>,
+/// \anchor PVR_EpgEventElapsedTime_format
+/// _string_,
+/// @return The time of the current position of the currently playing epg event in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.EpgEventRemainingTime`</b>,
+/// \anchor PVR_EpgEventRemainingTime
+/// _string_,
+/// @return The remaining time for currently playing epg event in the
+/// format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_EpgEventRemainingTime `PVR.EpgEventRemainingTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.EpgEventRemainingTime(format)`</b>,
+/// \anchor PVR_EpgEventRemainingTime_format
+/// _string_,
+/// @return The remaining time for currently playing epg event in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.EpgEventSeekTime`</b>,
+/// \anchor PVR_EpgEventSeekTime
+/// _string_,
+/// @return The time the user is seeking within the currently playing epg event in the
+/// format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_EpgEventSeekTime `PVR.EpgEventSeekTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.EpgEventSeekTime(format)`</b>,
+/// \anchor PVR_EpgEventSeekTime_format
+/// _string_,
+/// @return The time the user is seeking within the currently playing epg event in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.EpgEventFinishTime`</b>,
+/// \anchor PVR_EpgEventFinishTime
+/// _string_,
+/// @return The time the currently playing epg event will end in the
+/// format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_EpgEventFinishTime `PVR.EpgEventFinishTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.EpgEventFinishTime(format)`</b>,
+/// \anchor PVR_EpgEventFinishTime_format
+/// _string_,
+/// Returns the time the currently playing epg event will end in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeShiftStart`</b>,
+/// \anchor PVR_TimeShiftStart
+/// _string_,
+/// @return The start time of the timeshift buffer in the
+/// format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeShiftStart(format)`</b>,
+/// \anchor PVR_TimeShiftStart_format
+/// _string_,
+/// Returns the start time of the timeshift buffer in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeShiftEnd`</b>,
+/// \anchor PVR_TimeShiftEnd
+/// _string_,
+/// @return The end time of the timeshift buffer in the
+/// format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeShiftEnd(format)`</b>,
+/// \anchor PVR_TimeShiftEnd_format
+/// _string_,
+/// @return The end time of the timeshift buffer in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeShiftCur`</b>,
+/// \anchor PVR_TimeShiftCur
+/// _string_,
+/// @return The current playback time within the timeshift buffer in the
+/// format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeShiftCur(format)`</b>,
+/// \anchor PVR_TimeShiftCur_format
+/// _string_,
+/// Returns the current playback time within the timeshift buffer in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeShiftOffset`</b>,
+/// \anchor PVR_TimeShiftOffset
+/// _string_,
+/// @return The delta of timeshifted time to actual time in the
+/// format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeShiftOffset(format)`</b>,
+/// \anchor PVR_TimeShiftOffset_format
+/// _string_,
+/// Returns the delta of timeshifted time to actual time in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeshiftProgressDuration`</b>,
+/// \anchor PVR_TimeshiftProgressDuration
+/// _string_,
+/// @return the duration of the PVR timeshift progress in the
+/// format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_TimeshiftProgressDuration `PVR.TimeshiftProgressDuration`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeshiftProgressDuration(format)`</b>,
+/// \anchor PVR_TimeshiftProgressDuration_format
+/// _string_,
+/// @return The duration of the PVR timeshift progress in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeshiftProgressStartTime`</b>,
+/// \anchor PVR_TimeshiftProgressStartTime
+/// _string_,
+/// @return The start time of the PVR timeshift progress in the
+/// format <b>hh:mm:ss</b>.
+/// @note <b>hh:</b> will be omitted if hours value is zero.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_TimeshiftProgressStartTime `PVR.TimeshiftProgressStartTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeshiftProgressStartTime(format)`</b>,
+/// \anchor PVR_TimeshiftProgressStartTime_format
+/// _string_,
+/// @return The start time of the PVR timeshift progress in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeshiftProgressEndTime`</b>,
+/// \anchor PVR_TimeshiftProgressEndTime
+/// _string_,
+/// @return The end time of the PVR timeshift progress in the
+/// format <b>hh:mm:ss</b>.
+/// @note hh: will be omitted if hours value is zero.
+/// <p><hr>
+/// @skinning_v18 **[New Infolabel]** \link PVR_TimeshiftProgressEndTime `PVR.TimeshiftProgressEndTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`PVR.TimeshiftProgressEndTime(format)`</b>,
+/// \anchor PVR_TimeshiftProgressEndTime_format
+/// _string_,
+/// @return The end time of the PVR timeshift progress in different formats.
+/// @param format [opt] The format of the return time value.
+/// See \ref TIME_FORMAT for the list of possible values.
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap pvr_times[] = {{ "epgeventduration", PVR_EPG_EVENT_DURATION },
+ { "epgeventelapsedtime", PVR_EPG_EVENT_ELAPSED_TIME },
+ { "epgeventremainingtime", PVR_EPG_EVENT_REMAINING_TIME },
+ { "epgeventfinishtime", PVR_EPG_EVENT_FINISH_TIME },
+ { "epgeventseektime", PVR_EPG_EVENT_SEEK_TIME },
+ { "timeshiftstart", PVR_TIMESHIFT_START_TIME },
+ { "timeshiftend", PVR_TIMESHIFT_END_TIME },
+ { "timeshiftcur", PVR_TIMESHIFT_PLAY_TIME },
+ { "timeshiftoffset", PVR_TIMESHIFT_OFFSET },
+ { "timeshiftprogressduration", PVR_TIMESHIFT_PROGRESS_DURATION },
+ { "timeshiftprogressstarttime", PVR_TIMESHIFT_PROGRESS_START_TIME },
+ { "timeshiftprogressendtime", PVR_TIMESHIFT_PROGRESS_END_TIME }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_RDS RDS
+/// @note Only supported if both the PVR backend and the Kodi client support RDS.
+///
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`RDS.HasRds`</b>,
+/// \anchor RDS_HasRds
+/// _boolean_,
+/// @return **True** if RDS is present.
+/// <p><hr>
+/// @skinning_v16 **[New Boolean Condition]** \link RDS_HasRds `RDS.HasRds`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.HasRadioText`</b>,
+/// \anchor RDS_HasRadioText
+/// _boolean_,
+/// @return **True** if RDS contains also RadioText.
+/// <p><hr>
+/// @skinning_v16 **[New Boolean Condition]** \link RDS_HasRadioText `RDS.HasRadioText`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.HasRadioTextPlus`</b>,
+/// \anchor RDS_HasRadioTextPlus
+/// _boolean_,
+/// @return **True** if RDS with RadioText contains also the plus information.
+/// <p><hr>
+/// @skinning_v16 **[New Boolean Condition]** \link RDS_HasRadioTextPlus `RDS.HasRadioTextPlus`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.HasHotline`</b>,
+/// \anchor RDS_HasHotline
+/// _boolean_,
+/// @return **True** if a hotline phone number is present.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Boolean Condition]** \link RDS_HasHotline `RDS.HasHotline`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.HasStudio`</b>,
+/// \anchor RDS_HasStudio
+/// _boolean_,
+/// @return **True** if a studio name is present.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Boolean Condition]** \link RDS_HasStudio `RDS.HasStudio`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.AudioLanguage`</b>,
+/// \anchor RDS_AudioLanguage
+/// _string_,
+/// @return The RDS reported audio language of the channel.
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_AudioLanguage `RDS.AudioLanguage`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.ChannelCountry`</b>,
+/// \anchor RDS_ChannelCountry
+/// _string_,
+/// @return The country where the radio channel is broadcasted.
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_ChannelCountry `RDS.ChannelCountry`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.GetLine(number)`</b>,
+/// \anchor RDS_GetLine
+/// _string_,
+/// @return The last sent RDS text messages on given number.
+/// @param number - given number for RDS\, 0 is the
+/// last and 4 rows are supported (0-3)
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_GetLine `RDS.GetLine(number)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.Title`</b>,
+/// \anchor RDS_Title
+/// _string_,
+/// @return The title of item; e.g. track title of an album.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_Title `RDS.Title`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.Artist`</b>,
+/// \anchor RDS_Artist
+/// _string_,
+/// @return A person or band/collective generally considered responsible for the work.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_Artist `RDS.Artist`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.Band`</b>,
+/// \anchor RDS_Band
+/// _string_,
+/// @return The band/orchestra/musician.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_Band `RDS.Band`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.Composer`</b>,
+/// \anchor RDS_Composer
+/// _string_,
+/// @return The name of the original composer/author.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_Composer `RDS.Composer`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.Conductor`</b>,
+/// \anchor RDS_Conductor
+/// _string_,
+/// @return The artist(s) who performed the work. In classical music this would be
+/// the conductor.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_Conductor `RDS.Conductor`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.Album`</b>,
+/// \anchor RDS_Album
+/// _string_,
+/// @return The album of the song.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_Album `RDS.Album`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.TrackNumber`</b>,
+/// \anchor RDS_TrackNumber
+/// _string_,
+/// @return The track number of the item on the album on which it was originally
+/// released.
+/// @note Only be available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_TrackNumber `RDS.TrackNumber`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.RadioStyle`</b>,
+/// \anchor RDS_RadioStyle
+/// _string_,
+/// @return The style of current played radio channel\, it is always
+/// updated once the style changes\, e.g "popmusic" to "news" or "weather"...
+/// | RDS | RBDS |
+/// |:------------------------|:------------------------|
+/// | none | none |
+/// | news | news |
+/// | currentaffairs | information |
+/// | information | sport |
+/// | sport | talk |
+/// | education | rockmusic |
+/// | drama | classicrockmusic |
+/// | cultures | adulthits |
+/// | science | softrock |
+/// | variedspeech | top40 |
+/// | popmusic | countrymusic |
+/// | rockmusic | oldiesmusic |
+/// | easylistening | softmusic |
+/// | lightclassics | nostalgia |
+/// | seriousclassics | jazzmusic |
+/// | othermusic | classical |
+/// | weather | randb |
+/// | finance | softrandb |
+/// | childrensprogs | language |
+/// | socialaffairs | religiousmusic |
+/// | religion | religioustalk |
+/// | phonein | personality |
+/// | travelandtouring | public |
+/// | leisureandhobby | college |
+/// | jazzmusic | spanishtalk |
+/// | countrymusic | spanishmusic |
+/// | nationalmusic | hiphop |
+/// | oldiesmusic | |
+/// | folkmusic | |
+/// | documentary | weather |
+/// | alarmtest | alarmtest |
+/// | alarm-alarm | alarm-alarm |
+/// @note "alarm-alarm" is normally not used from radio stations\, is thought
+/// to inform about horrible messages who are needed asap to all people.
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_RadioStyle `RDS.RadioStyle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.Comment`</b>,
+/// \anchor RDS_Comment
+/// _string_,
+/// @return The radio station comment string if available.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_Comment `RDS.Comment`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoNews`</b>,
+/// \anchor RDS_InfoNews
+/// _string_,
+/// @return The message / headline (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoNews `RDS.InfoNews`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoNewsLocal`</b>,
+/// \anchor RDS_InfoNewsLocal
+/// _string_,
+/// @return The local information news sended from radio channel (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoNewsLocal `RDS.InfoNewsLocal`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoStock`</b>,
+/// \anchor RDS_InfoStock
+/// _string_,
+/// @return The stock information; either as one part or as several distinct parts:
+/// "name 99latest value 99change 99high 99low 99volume" (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoStock `RDS.InfoStock`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoStockSize`</b>,
+/// \anchor RDS_InfoStockSize
+/// _string_,
+/// @return The number of rows present in stock information.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoStockSize `RDS.InfoStockSize`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoSport`</b>,
+/// \anchor RDS_InfoSport
+/// _string_,
+/// @return The result of a match; either as one part or as several distinct parts:
+/// "match 99result"\, e.g. "Bayern München : Borussia 995:5" (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoSport `RDS.InfoSport`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoSportSize`</b>,
+/// \anchor RDS_InfoSportSize
+/// _string_,
+/// @return The number of rows present in sport information.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoSportSize `RDS.InfoSportSize`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoLottery`</b>,
+/// \anchor RDS_InfoLottery
+/// _string_,
+/// @return The raffle / lottery: "key word 99values" (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoLottery `RDS.InfoLottery`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoLotterySize`</b>,
+/// \anchor RDS_InfoLotterySize
+/// _string_,
+/// @return The number of rows present in lottery information.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoLotterySize `RDS.InfoLotterySize`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoWeather`</b>,
+/// \anchor RDS_InfoWeather
+/// _string_,
+/// @return The weather information (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoWeather `RDS.InfoWeather`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoWeatherSize`</b>,
+/// \anchor RDS_InfoWeatherSize
+/// _string_,
+/// @return The number of rows present in weather information.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoWeatherSize `RDS.InfoWeatherSize`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoCinema`</b>,
+/// \anchor RDS_InfoCinema
+/// _string_,
+/// @return The information about movies in cinema (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoCinema `RDS.InfoCinema`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoCinemaSize`</b>,
+/// \anchor RDS_InfoCinemaSize
+/// _string_,
+/// @return The number of rows present in cinema information.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoCinemaSize `RDS.InfoCinemaSize`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoHoroscope`</b>,
+/// \anchor RDS_InfoHoroscope
+/// _string_,
+/// @return The horoscope; either as one part or as two distinct parts:
+/// "key word 99text"\, e.g. "sign of the zodiac 99blablabla" (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoHoroscope `RDS.InfoHoroscope`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoHoroscopeSize`</b>,
+/// \anchor RDS_InfoHoroscopeSize
+/// _string_,
+/// @return The Number of rows present in horoscope information.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoHoroscopeSize `RDS.InfoHoroscopeSize`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoOther`</b>,
+/// \anchor RDS_InfoOther
+/// _string_,
+/// @return Other information\, not especially specified: "key word 99info" (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoOther `RDS.InfoOther`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.InfoOtherSize`</b>,
+/// \anchor RDS_InfoOtherSize
+/// _string_,
+/// @return The number of rows present with other information.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_InfoOtherSize `RDS.InfoOtherSize`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.ProgStation`</b>,
+/// \anchor RDS_ProgStation
+/// _string_,
+/// @return The name of the radio channel.
+/// @note becomes also set from epg if it is not available from RDS
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_ProgStation `RDS.ProgStation`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.ProgNow`</b>,
+/// \anchor RDS_ProgNow
+/// _string_,
+/// @return The now playing program name.
+/// @note becomes also be set from epg if from RDS not available
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_ProgNow `RDS.ProgNow`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.ProgNext`</b>,
+/// \anchor RDS_ProgNext
+/// _string_,
+/// @return The next played program name (if available).
+/// @note becomes also be set from epg if from RDS not available
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_ProgNext `RDS.ProgNext`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.ProgHost`</b>,
+/// \anchor RDS_ProgHost
+/// _string_,
+/// @return The name of the host of the radio show.
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_ProgHost `RDS.ProgHost`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.ProgEditStaff`</b>,
+/// \anchor RDS_ProgEditStaff
+/// _string_,
+/// @return The name of the editorial staff; e.g. name of editorial journalist.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_ProgEditStaff `RDS.ProgEditStaff`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.ProgHomepage`</b>,
+/// \anchor RDS_ProgHomepage
+/// _string_,
+/// @return The Link to radio station homepage
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_ProgHomepage `RDS.ProgHomepage`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.ProgStyle`</b>,
+/// \anchor RDS_ProgStyle
+/// _string_,
+/// @return A human readable string about radiostyle defined from RDS or RBDS.
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_ProgStyle `RDS.ProgStyle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.PhoneHotline`</b>,
+/// \anchor RDS_PhoneHotline
+/// _string_,
+/// @return The telephone number of the radio station's hotline.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_PhoneHotline `RDS.PhoneHotline`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.PhoneStudio`</b>,
+/// \anchor RDS_PhoneStudio
+/// _string_,
+/// @return The telephone number of the radio station's studio.
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_PhoneStudio `RDS.PhoneStudio`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.SmsStudio`</b>,
+/// \anchor RDS_SmsStudio
+/// _string_,
+/// @return The sms number of the radio stations studio (to send directly a sms to
+/// the studio) (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_SmsStudio `RDS.SmsStudio`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.EmailHotline`</b>,
+/// \anchor RDS_EmailHotline
+/// _string_,
+/// @return The email address of the radio stations hotline (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_EmailHotline `RDS.EmailHotline`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`RDS.EmailStudio`</b>,
+/// \anchor RDS_EmailStudio
+/// _string_,
+/// @return The email address of the radio station's studio (if available).
+/// @note Only available on RadioText Plus
+/// <p><hr>
+/// @skinning_v16 **[New Infolabel]** \link RDS_EmailStudio `RDS.EmailStudio`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap rds[] = {{ "hasrds", RDS_HAS_RDS },
+ { "hasradiotext", RDS_HAS_RADIOTEXT },
+ { "hasradiotextplus", RDS_HAS_RADIOTEXT_PLUS },
+ { "audiolanguage", RDS_AUDIO_LANG },
+ { "channelcountry", RDS_CHANNEL_COUNTRY },
+ { "title", RDS_TITLE },
+ { "getline", RDS_GET_RADIOTEXT_LINE },
+ { "artist", RDS_ARTIST },
+ { "band", RDS_BAND },
+ { "composer", RDS_COMPOSER },
+ { "conductor", RDS_CONDUCTOR },
+ { "album", RDS_ALBUM },
+ { "tracknumber", RDS_ALBUM_TRACKNUMBER },
+ { "radiostyle", RDS_GET_RADIO_STYLE },
+ { "comment", RDS_COMMENT },
+ { "infonews", RDS_INFO_NEWS },
+ { "infonewslocal", RDS_INFO_NEWS_LOCAL },
+ { "infostock", RDS_INFO_STOCK },
+ { "infostocksize", RDS_INFO_STOCK_SIZE },
+ { "infosport", RDS_INFO_SPORT },
+ { "infosportsize", RDS_INFO_SPORT_SIZE },
+ { "infolottery", RDS_INFO_LOTTERY },
+ { "infolotterysize", RDS_INFO_LOTTERY_SIZE },
+ { "infoweather", RDS_INFO_WEATHER },
+ { "infoweathersize", RDS_INFO_WEATHER_SIZE },
+ { "infocinema", RDS_INFO_CINEMA },
+ { "infocinemasize", RDS_INFO_CINEMA_SIZE },
+ { "infohoroscope", RDS_INFO_HOROSCOPE },
+ { "infohoroscopesize", RDS_INFO_HOROSCOPE_SIZE },
+ { "infoother", RDS_INFO_OTHER },
+ { "infoothersize", RDS_INFO_OTHER_SIZE },
+ { "progstation", RDS_PROG_STATION },
+ { "prognow", RDS_PROG_NOW },
+ { "prognext", RDS_PROG_NEXT },
+ { "proghost", RDS_PROG_HOST },
+ { "progeditstaff", RDS_PROG_EDIT_STAFF },
+ { "proghomepage", RDS_PROG_HOMEPAGE },
+ { "progstyle", RDS_PROG_STYLE },
+ { "phonehotline", RDS_PHONE_HOTLINE },
+ { "phonestudio", RDS_PHONE_STUDIO },
+ { "smsstudio", RDS_SMS_STUDIO },
+ { "emailhotline", RDS_EMAIL_HOTLINE },
+ { "emailstudio", RDS_EMAIL_STUDIO },
+ { "hashotline", RDS_HAS_HOTLINE_DATA },
+ { "hasstudio", RDS_HAS_STUDIO_DATA }};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_slideshow Slideshow
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Slideshow.IsActive`</b>,
+/// \anchor Slideshow_IsActive
+/// _boolean_,
+/// @return **True** if the picture slideshow is running.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.IsPaused`</b>,
+/// \anchor Slideshow_IsPaused
+/// _boolean_,
+/// @return **True** if the picture slideshow is paused.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.IsRandom`</b>,
+/// \anchor Slideshow_IsRandom
+/// _boolean_,
+/// @return **True** if the picture slideshow is in random mode.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.IsVideo`</b>,
+/// \anchor Slideshow_IsVideo
+/// _boolean_,
+/// @return **True** if the picture slideshow is playing a video.
+/// <p><hr>
+/// @skinning_v13 **[New Boolean Condition]** \link Slideshow_IsVideo `Slideshow.IsVideo`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Altitude`</b>,
+/// \anchor Slideshow_Altitude
+/// _string_,
+/// @return The altitude in meters where the current picture was taken.
+/// @note This is the value of the EXIF GPSInfo.GPSAltitude tag.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Aperture`</b>,
+/// \anchor Slideshow_Aperture
+/// _string_,
+/// @return The F-stop used to take the current picture.
+/// @note This is the value of the EXIF FNumber tag (hex code 0x829D).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Author`</b>,
+/// \anchor Slideshow_Author
+/// _string_,
+/// @return The name of the person involved in writing about the current
+/// picture.
+/// @note This is the value of the IPTC Writer tag (hex code 0x7A).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Author `Slideshow.Author`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Byline`</b>,
+/// \anchor Slideshow_Byline
+/// _string_,
+/// @return The name of the person who created the current picture.
+/// @note This is the value of the IPTC Byline tag (hex code 0x50).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Byline `Slideshow.Byline`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.BylineTitle`</b>,
+/// \anchor Slideshow_BylineTitle
+/// _string_,
+/// @return The title of the person who created the current picture.
+/// @note This is the value of the IPTC BylineTitle tag (hex code 0x55).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_BylineTitle `Slideshow.BylineTitle`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.CameraMake`</b>,
+/// \anchor Slideshow_CameraMake
+/// _string_,
+/// @return The manufacturer of the camera used to take the current picture.
+/// @note This is the value of the EXIF Make tag (hex code 0x010F).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.CameraModel`</b>,
+/// \anchor Slideshow_CameraModel
+/// _string_,
+/// @return The manufacturer's model name or number of the camera used to take
+/// the current picture.
+/// @note This is the value of the EXIF Model tag (hex code 0x0110).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Caption`</b>,
+/// \anchor Slideshow_Caption
+/// _string_,
+/// @return A description of the current picture.
+/// @note This is the value of the IPTC Caption tag (hex code 0x78).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Category`</b>,
+/// \anchor Slideshow_Category
+/// _string_,
+/// @return The subject of the current picture as a category code.
+/// @note This is the value of the IPTC Category tag (hex code 0x0F).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Category `Slideshow.Category`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.CCDWidth`</b>,
+/// \anchor Slideshow_CCDWidth
+/// _string_,
+/// @return The width of the CCD in the camera used to take the current
+/// picture.
+/// @note This is calculated from three EXIF tags (0xA002 * 0xA210 / 0xA20e).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_CCDWidth `Slideshow.CCDWidth`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.City`</b>,
+/// \anchor Slideshow_City
+/// _string_,
+/// @return The city where the current picture was taken.
+/// @note This is the value of the IPTC City tag (hex code 0x5A).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_City `Slideshow.City`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Colour`</b>,
+/// \anchor Slideshow_Colour
+/// _string_,
+/// @return the colour of the picture. It can have one of the following values:
+/// - <b>"Colour"</b>
+/// - <b>"Black and White"</b>
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Colour `Slideshow.Colour`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.CopyrightNotice`</b>,
+/// \anchor Slideshow_CopyrightNotice
+/// _string_,
+/// @return The copyright notice of the current picture.
+/// @note This is the value of the IPTC Copyright tag (hex code 0x74).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_CopyrightNotice `Slideshow.CopyrightNotice`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Country`</b>,
+/// \anchor Slideshow_Country
+/// _string_,
+/// @return The full name of the country where the current picture was taken.
+/// @note This is the value of the IPTC CountryName tag (hex code 0x65).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Country `Slideshow.Country`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.CountryCode`</b>,
+/// \anchor Slideshow_CountryCode
+/// _string_,
+/// @return The country code of the country where the current picture was
+/// taken.
+/// @note This is the value of the IPTC CountryCode tag (hex code 0x64).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_CountryCode `Slideshow.CountryCode`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Credit`</b>,
+/// \anchor Slideshow_Credit
+/// _string_,
+/// @return Who provided the current picture.
+/// @note This is the value of the IPTC Credit tag (hex code 0x6E).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Credit `Slideshow.Credit`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.DigitalZoom`</b>,
+/// \anchor Slideshow_DigitalZoom
+/// _string_,
+/// @return The digital zoom ratio when the current picture was taken.
+/// @note This is the value of the EXIF .DigitalZoomRatio tag (hex code 0xA404).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_DigitalZoom `Slideshow.DigitalZoom`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.EXIFComment`</b>,
+/// \anchor Slideshow_EXIFComment
+/// _string_,
+/// @return A description of the current picture.
+/// @note This is the value of the EXIF User Comment tag (hex code 0x9286).
+/// This is the same value as \ref Slideshow_SlideComment "Slideshow.SlideComment".
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.EXIFDate`</b>,
+/// \anchor Slideshow_EXIFDate
+/// _string_,
+/// @return The localized date of the current picture. The short form of the
+/// date is used.
+/// @note The value of the EXIF DateTimeOriginal tag (hex code
+/// 0x9003) is preferred. If the DateTimeOriginal tag is not found\, the
+/// value of DateTimeDigitized (hex code 0x9004) or of DateTime (hex code
+/// 0x0132) might be used.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_EXIFDate `Slideshow.EXIFDate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.EXIFDescription`</b>,
+/// \anchor Slideshow_EXIFDescription
+/// _string_,
+/// @return A short description of the current picture. The SlideComment\,
+/// EXIFComment or Caption values might contain a longer description.
+/// @note This is the value of the EXIF ImageDescription tag (hex code 0x010E).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.EXIFSoftware`</b>,
+/// \anchor Slideshow_EXIFSoftware
+/// _string_,
+/// @return The name and version of the firmware used by the camera that took
+/// the current picture.
+/// @note This is the value of the EXIF Software tag (hex code 0x0131).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.EXIFTime`</b>,
+/// \anchor Slideshow_EXIFTime
+/// _string_,
+/// @return The date/timestamp of the current picture. The localized short
+/// form of the date and time is used.
+/// @note The value of the EXIF DateTimeOriginal tag (hex code 0x9003) is
+/// preferred. If the DateTimeOriginal tag is not found\, the value of
+/// DateTimeDigitized (hex code 0x9004) or of DateTime (hex code 0x0132)
+/// might be used.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Exposure`</b>,
+/// \anchor Slideshow_Exposure
+/// _string_,
+/// @return The class of the program used by the camera to set exposure when
+/// the current picture was taken. Values include:
+/// - <b>"Manual"</b>
+/// - <b>"Program (Auto)"</b>
+/// - <b>"Aperture priority (Semi-Auto)"</b>
+/// - <b>"Shutter priority (semi-auto)"</b>
+/// - etc...
+/// @note This is the value of the EXIF ExposureProgram tag
+/// (hex code 0x8822).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Exposure `Slideshow.Exposure`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.ExposureBias`</b>,
+/// \anchor Slideshow_ExposureBias
+/// _string_,
+/// @return The exposure bias of the current picture. Typically this is a
+/// number between -99.99 and 99.99.
+/// @note This is the value of the EXIF ExposureBiasValue tag (hex code 0x9204).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_ExposureBias `Slideshow.ExposureBias`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.ExposureMode`</b>,
+/// \anchor Slideshow_ExposureMode
+/// _string_,
+/// @return The exposure mode of the current picture. The possible values are:
+/// - <b>"Automatic"</b>
+/// - <b>"Manual"</b>
+/// - <b>"Auto bracketing"</b>
+/// @note This is the value of the EXIF ExposureMode tag (hex code 0xA402).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.ExposureTime`</b>,
+/// \anchor Slideshow_ExposureTime
+/// _string_,
+/// @return The exposure time of the current picture\, in seconds.
+/// @note This is the value of the EXIF ExposureTime tag (hex code 0x829A).
+/// If the ExposureTime tag is not found\, the ShutterSpeedValue tag (hex code
+/// 0x9201) might be used.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Filedate`</b>,
+/// \anchor Slideshow_Filedate
+/// _string_,
+/// @return The file date of the current picture.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Filename`</b>,
+/// \anchor Slideshow_Filename
+/// _string_,
+/// @return The file name of the current picture.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Filesize`</b>,
+/// \anchor Slideshow_Filesize
+/// _string_,
+/// @return The file size of the current picture.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.FlashUsed`</b>,
+/// \anchor Slideshow_FlashUsed
+/// _string_,
+/// @return The status of flash when the current picture was taken. The value
+/// will be either <b>"Yes"</b> or <b>"No"</b>\, and might include additional information.
+/// @note This is the value of the EXIF Flash tag (hex code 0x9209).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_FlashUsed `Slideshow.FlashUsed`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.FocalLength`</b>,
+/// \anchor Slideshow_FocalLength
+/// _string_,
+/// @return The focal length of the lens\, in mm.
+/// @note This is the value of the EXIF FocalLength tag (hex code 0x920A).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.FocusDistance`</b>,
+/// \anchor Slideshow_FocusDistance
+/// _string_,
+/// @return The distance to the subject\, in meters.
+/// @note This is the value of the EXIF SubjectDistance tag (hex code 0x9206).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Headline`</b>,
+/// \anchor Slideshow_Headline
+/// _string_,
+/// @return A synopsis of the contents of the current picture.
+/// @note This is the value of the IPTC Headline tag (hex code 0x69).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Headline `Slideshow.Headline`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.ImageType`</b>,
+/// \anchor Slideshow_ImageType
+/// _string_,
+/// @return The color components of the current picture.
+/// @note This is the value of the IPTC ImageType tag (hex code 0x82).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_ImageType `Slideshow.ImageType`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.IPTCDate`</b>,
+/// \anchor Slideshow_IPTCDate
+/// _string_,
+/// @return The date when the intellectual content of the current picture was
+/// created\, rather than when the picture was created.
+/// @note This is the value of the IPTC DateCreated tag (hex code 0x37).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.ISOEquivalence`</b>,
+/// \anchor Slideshow_ISOEquivalence
+/// _string_,
+/// @return The ISO speed of the camera when the current picture was taken.
+/// @note This is the value of the EXIF ISOSpeedRatings tag (hex code 0x8827).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Keywords`</b>,
+/// \anchor Slideshow_Keywords
+/// _string_,
+/// @return The keywords assigned to the current picture.
+/// @note This is the value of the IPTC Keywords tag (hex code 0x19).
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Latitude`</b>,
+/// \anchor Slideshow_Latitude
+/// _string_,
+/// @return The latitude where the current picture was taken (degrees\,
+/// minutes\, seconds North or South).
+/// @note This is the value of the EXIF GPSInfo.GPSLatitude and
+/// GPSInfo.GPSLatitudeRef tags.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.LightSource`</b>,
+/// \anchor Slideshow_LightSource
+/// _string_,
+/// @return The kind of light source when the picture was taken. Possible
+/// values include:
+/// - <b>"Daylight"</b>
+/// - <b>"Fluorescent"</b>
+/// - <b>"Incandescent"</b>
+/// - etc...
+/// @note This is the value of the EXIF LightSource tag (hex code 0x9208).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_LightSource `Slideshow.LightSource`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.LongEXIFDate`</b>,
+/// \anchor Slideshow_LongEXIFDate
+/// _string_,
+/// @return Only the localized date of the current picture. The long form of
+/// the date is used.
+/// @note The value of the EXIF DateTimeOriginal tag (hex code
+/// 0x9003) is preferred. If the DateTimeOriginal tag is not found\, the
+/// value of DateTimeDigitized (hex code 0x9004) or of DateTime (hex code
+/// 0x0132) might be used.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_LongEXIFDate `Slideshow.LongEXIFDate`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.LongEXIFTime`</b>,
+/// \anchor Slideshow_LongEXIFTime
+/// _string_,
+/// @return The date/timestamp of the current picture. The localized long form
+/// of the date and time is used.
+/// @note The value of the EXIF DateTimeOriginal tag
+/// (hex code 0x9003) is preferred. if the DateTimeOriginal tag is not found\,
+/// the value of DateTimeDigitized (hex code 0x9004) or of DateTime (hex
+/// code 0x0132) might be used.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_LongEXIFTime `Slideshow.LongEXIFTime`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Longitude`</b>,
+/// \anchor Slideshow_Longitude
+/// _string_,
+/// @return The longitude where the current picture was taken (degrees\,
+/// minutes\, seconds East or West).
+/// @note This is the value of the EXIF GPSInfo.GPSLongitude and
+/// GPSInfo.GPSLongitudeRef tags.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.MeteringMode`</b>,
+/// \anchor Slideshow_MeteringMode
+/// _string_,
+/// @return The metering mode used when the current picture was taken. The
+/// possible values are:
+/// - <b>"Center weight"</b>
+/// - <b>"Spot"</b>
+/// - <b>"Matrix"</b>
+/// @note This is the value of the EXIF MeteringMode tag (hex code 0x9207).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_MeteringMode `Slideshow.MeteringMode`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.ObjectName`</b>,
+/// \anchor Slideshow_ObjectName
+/// _string_,
+/// @return a shorthand reference for the current picture.
+/// @note This is the value of the IPTC ObjectName tag (hex code 0x05).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_ObjectName `Slideshow.ObjectName`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Orientation`</b>,
+/// \anchor Slideshow_Orientation
+/// _string_,
+/// @return The orientation of the current picture. Possible values are:
+/// - <b>"Top Left"</b>
+/// - <b>"Top Right"</b>
+/// - <b>"Left Top"</b>
+/// - <b>"Right Bottom"</b>
+/// - etc...
+/// @note This is the value of the EXIF Orientation tag (hex code 0x0112).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Orientation `Slideshow.Orientation`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Path`</b>,
+/// \anchor Slideshow_Path
+/// _string_,
+/// @return The file path of the current picture.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Process`</b>,
+/// \anchor Slideshow_Process
+/// _string_,
+/// @return The process used to compress the current picture.
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Process `Slideshow.Process`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.ReferenceService`</b>,
+/// \anchor Slideshow_ReferenceService
+/// _string_,
+/// @return The Service Identifier of a prior envelope to which the current
+/// picture refers.
+/// @note This is the value of the IPTC ReferenceService tag (hex code 0x2D).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_ReferenceService `Slideshow.ReferenceService`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Resolution`</b>,
+/// \anchor Slideshow_Resolution
+/// _string_,
+/// @return The dimensions of the current picture (Width x Height)
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.SlideComment`</b>,
+/// \anchor Slideshow_SlideComment
+/// _string_,
+/// @return A description of the current picture.
+/// @note This is the value of the EXIF User Comment tag (hex code 0x9286).
+/// This is the same value as \ref Slideshow_EXIFComment "Slideshow.EXIFComment".
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.SlideIndex`</b>,
+/// \anchor Slideshow_SlideIndex
+/// _string_,
+/// @return The slide index of the current picture.
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Source`</b>,
+/// \anchor Slideshow_Source
+/// _string_,
+/// @return The original owner of the current picture.
+/// @note This is the value of the IPTC Source tag (hex code 0x73).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Source `Slideshow.Source`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.SpecialInstructions`</b>,
+/// \anchor Slideshow_SpecialInstructions
+/// _string_,
+/// @return Other editorial instructions concerning the use of the current
+/// picture.
+/// @note This is the value of the IPTC SpecialInstructions tag (hex code 0x28).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_SpecialInstructions `Slideshow.SpecialInstructions`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.State`</b>,
+/// \anchor Slideshow_State
+/// _string_,
+/// @return The State/Province where the current picture was taken.
+/// @note This is the value of the IPTC ProvinceState tag (hex code 0x5F).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_State `Slideshow.State`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Sublocation`</b>,
+/// \anchor Slideshow_Sublocation
+/// _string_,
+/// @return The location within a city where the current picture was taken -
+/// might indicate the nearest landmark.
+/// @note This is the value of the IPTC SubLocation tag (hex code 0x5C).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Sublocation `Slideshow.Sublocation`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.SupplementalCategories`</b>,
+/// \anchor Slideshow_SupplementalCategories
+/// _string_,
+/// @return The supplemental category codes to further refine the subject of the
+/// current picture.
+/// @note This is the value of the IPTC SuppCategory tag (hex
+/// code 0x14).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_SupplementalCategories `Slideshow.SupplementalCategories`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.TimeCreated`</b>,
+/// \anchor Slideshow_TimeCreated
+/// _string_,
+/// @return The time when the intellectual content of the current picture was
+/// created\, rather than when the picture was created.
+/// @note This is the value of the IPTC TimeCreated tag (hex code 0x3C).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_TimeCreated `Slideshow.TimeCreated`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.TransmissionReference`</b>,
+/// \anchor Slideshow_TransmissionReference
+/// _string_,
+/// @return A code representing the location of original transmission of the
+/// current picture.
+/// @note This is the value of the IPTC TransmissionReference tag
+/// (hex code 0x67).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_TransmissionReference `Slideshow.TransmissionReference`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.Urgency`</b>,
+/// \anchor Slideshow_Urgency
+/// _string_,
+/// @return The urgency of the current picture. Values are 1-9. The 1 is most
+/// urgent.
+/// @note Some image management programs use urgency to indicate picture
+/// rating\, where urgency 1 is 5 stars and urgency 5 is 1 star. Urgencies
+/// 6-9 are not used for rating. This is the value of the IPTC Urgency tag
+/// (hex code 0x0A).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_Urgency `Slideshow.Urgency`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Slideshow.WhiteBalance`</b>,
+/// \anchor Slideshow_WhiteBalance
+/// _string_,
+/// @return The white balance mode set when the current picture was taken.
+/// The possible values are:
+/// - <b>"Manual"</b>
+/// - <b>"Auto"</b>
+/// <p>
+/// @note This is the value of the EXIF WhiteBalance tag (hex code 0xA403).
+/// <p><hr>
+/// @skinning_v13 **[New Infolabel]** \link Slideshow_WhiteBalance `Slideshow.WhiteBalance`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+const infomap slideshow[] = {{ "ispaused", SLIDESHOW_ISPAUSED },
+ { "isactive", SLIDESHOW_ISACTIVE },
+ { "isvideo", SLIDESHOW_ISVIDEO },
+ { "israndom", SLIDESHOW_ISRANDOM },
+ { "filename", SLIDESHOW_FILE_NAME },
+ { "path", SLIDESHOW_FILE_PATH },
+ { "filesize", SLIDESHOW_FILE_SIZE },
+ { "filedate", SLIDESHOW_FILE_DATE },
+ { "slideindex", SLIDESHOW_INDEX },
+ { "resolution", SLIDESHOW_RESOLUTION },
+ { "slidecomment", SLIDESHOW_COMMENT },
+ { "colour", SLIDESHOW_COLOUR },
+ { "process", SLIDESHOW_PROCESS },
+ { "exiftime", SLIDESHOW_EXIF_DATE_TIME },
+ { "exifdate", SLIDESHOW_EXIF_DATE },
+ { "longexiftime", SLIDESHOW_EXIF_LONG_DATE_TIME },
+ { "longexifdate", SLIDESHOW_EXIF_LONG_DATE },
+ { "exifdescription", SLIDESHOW_EXIF_DESCRIPTION },
+ { "cameramake", SLIDESHOW_EXIF_CAMERA_MAKE },
+ { "cameramodel", SLIDESHOW_EXIF_CAMERA_MODEL },
+ { "exifcomment", SLIDESHOW_EXIF_COMMENT },
+ { "exifsoftware", SLIDESHOW_EXIF_SOFTWARE },
+ { "aperture", SLIDESHOW_EXIF_APERTURE },
+ { "focallength", SLIDESHOW_EXIF_FOCAL_LENGTH },
+ { "focusdistance", SLIDESHOW_EXIF_FOCUS_DIST },
+ { "exposure", SLIDESHOW_EXIF_EXPOSURE },
+ { "exposuretime", SLIDESHOW_EXIF_EXPOSURE_TIME },
+ { "exposurebias", SLIDESHOW_EXIF_EXPOSURE_BIAS },
+ { "exposuremode", SLIDESHOW_EXIF_EXPOSURE_MODE },
+ { "flashused", SLIDESHOW_EXIF_FLASH_USED },
+ { "whitebalance", SLIDESHOW_EXIF_WHITE_BALANCE },
+ { "lightsource", SLIDESHOW_EXIF_LIGHT_SOURCE },
+ { "meteringmode", SLIDESHOW_EXIF_METERING_MODE },
+ { "isoequivalence", SLIDESHOW_EXIF_ISO_EQUIV },
+ { "digitalzoom", SLIDESHOW_EXIF_DIGITAL_ZOOM },
+ { "ccdwidth", SLIDESHOW_EXIF_CCD_WIDTH },
+ { "orientation", SLIDESHOW_EXIF_ORIENTATION },
+ { "supplementalcategories", SLIDESHOW_IPTC_SUP_CATEGORIES },
+ { "keywords", SLIDESHOW_IPTC_KEYWORDS },
+ { "caption", SLIDESHOW_IPTC_CAPTION },
+ { "author", SLIDESHOW_IPTC_AUTHOR },
+ { "headline", SLIDESHOW_IPTC_HEADLINE },
+ { "specialinstructions", SLIDESHOW_IPTC_SPEC_INSTR },
+ { "category", SLIDESHOW_IPTC_CATEGORY },
+ { "byline", SLIDESHOW_IPTC_BYLINE },
+ { "bylinetitle", SLIDESHOW_IPTC_BYLINE_TITLE },
+ { "credit", SLIDESHOW_IPTC_CREDIT },
+ { "source", SLIDESHOW_IPTC_SOURCE },
+ { "copyrightnotice", SLIDESHOW_IPTC_COPYRIGHT_NOTICE },
+ { "objectname", SLIDESHOW_IPTC_OBJECT_NAME },
+ { "city", SLIDESHOW_IPTC_CITY },
+ { "state", SLIDESHOW_IPTC_STATE },
+ { "country", SLIDESHOW_IPTC_COUNTRY },
+ { "transmissionreference", SLIDESHOW_IPTC_TX_REFERENCE },
+ { "iptcdate", SLIDESHOW_IPTC_DATE },
+ { "urgency", SLIDESHOW_IPTC_URGENCY },
+ { "countrycode", SLIDESHOW_IPTC_COUNTRY_CODE },
+ { "referenceservice", SLIDESHOW_IPTC_REF_SERVICE },
+ { "latitude", SLIDESHOW_EXIF_GPS_LATITUDE },
+ { "longitude", SLIDESHOW_EXIF_GPS_LONGITUDE },
+ { "altitude", SLIDESHOW_EXIF_GPS_ALTITUDE },
+ { "timecreated", SLIDESHOW_IPTC_TIMECREATED },
+ { "sublocation", SLIDESHOW_IPTC_SUBLOCATION },
+ { "imagetype", SLIDESHOW_IPTC_IMAGETYPE },
+};
+
+/// \page modules__infolabels_boolean_conditions
+/// \subsection modules__infolabels_boolean_conditions_Library Library
+/// @todo Make this annotate an array of infobools/labels to make it easier to track
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`Library.IsScanning`</b>,
+/// \anchor Library_IsScanning
+/// _boolean_,
+/// @return **True** if the library is being scanned.
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.IsScanningVideo`</b>,
+/// \anchor Library_IsScanningVideo
+/// _boolean_,
+/// @return **True** if the video library is being scanned.
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.IsScanningMusic`</b>,
+/// \anchor Library_IsScanningMusic
+/// _boolean_,
+/// @return **True** if the music library is being scanned.
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(music)`</b>,
+/// \anchor Library_HasContent_Music
+/// _boolean_,
+/// @return **True** if the library has music content.
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(video)`</b>,
+/// \anchor Library_HasContent_Video
+/// _boolean_,
+/// @return **True** if the library has video content.
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(movies)`</b>,
+/// \anchor Library_HasContent_Movies
+/// _boolean_,
+/// @return **True** if the library has movies.
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(tvshows)`</b>,
+/// \anchor Library_HasContent_TVShows
+/// _boolean_,
+/// @return **True** if the library has tvshows.
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(musicvideos)`</b>,
+/// \anchor Library_HasContent_MusicVideos
+/// _boolean_,
+/// @return **True** if the library has music videos.
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(moviesets)`</b>,
+/// \anchor Library_HasContent_MovieSets
+/// _boolean_,
+/// @return **True** if the library has movie sets.
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(singles)`</b>,
+/// \anchor Library_HasContent_Singles
+/// _boolean_,
+/// @return **True** if the library has singles.
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(compilations)`</b>,
+/// \anchor Library_HasContent_Compilations
+/// _boolean_,
+/// @return **True** if the library has compilations.
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(Role.Composer)`</b>,
+/// \anchor Library_HasContent_Role_Composer
+/// _boolean_,
+/// @return **True** if there are songs in the library which have composers.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Library_HasContent_Role_Composer `Library.HasContent(Role.Composer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(Role.Conductor)`</b>,
+/// \anchor Library_HasContent_Role_Conductor
+/// _boolean_,
+/// @return **True** if there are songs in the library which have a conductor.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Library_HasContent_Role_Conductor `Library.HasContent(Role.Conductor)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(Role.Orchestra)`</b>,
+/// \anchor Library_HasContent_Role_Orchestra
+/// _boolean_,
+/// @return **True** if there are songs in the library which have an orchestra.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Library_HasContent_Role_Orchestra `Library.HasContent(Role.Orchestra)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(Role.Lyricist)`</b>,
+/// \anchor Library_HasContent_Role_Lyricist
+/// _boolean_,
+/// @return **True** if there are songs in the library which have a lyricist.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Library_HasContent_Role_Lyricist `Library.HasContent(Role.Lyricist)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(Role.Remixer)`</b>,
+/// \anchor Library_HasContent_Role_Remixer
+/// _boolean_,
+/// @return **True** if there are songs in the library which have a remixer.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Library_HasContent_Role_Remixer `Library.HasContent(Role.Remixer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(Role.Arranger)`</b>,
+/// \anchor Library_HasContent_Role_Remixer
+/// _boolean_,
+/// @return **True** if there are songs in the library which have an arranger.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Library_HasContent_Role_Remixer `Library.HasContent(Role.Arranger)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(Role.Engineer)`</b>,
+/// \anchor Library_HasContent_Role_Engineer
+/// _boolean_,
+/// @return **True** if there are songs in the library which have an engineer.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Library_HasContent_Role_Engineer `Library.HasContent(Role.Engineer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(Role.Producer)`</b>,
+/// \anchor Library_HasContent_Role_Producer
+/// _boolean_,
+/// @return **True** if there are songs in the library which have an producer.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Library_HasContent_Role_Producer `Library.HasContent(Role.Producer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(Role.DJMixer)`</b>,
+/// \anchor Library_HasContent_Role_DJMixer
+/// _boolean_,
+/// @return **True** if there are songs in the library which have a DJMixer.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Library_HasContent_Role_DJMixer `Library.HasContent(Role.DJMixer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(Role.Mixer)`</b>,
+/// \anchor Library_HasContent_Role_Mixer
+/// _boolean_,
+/// @return **True** if there are songs in the library which have a mixer.
+/// <p><hr>
+/// @skinning_v17 **[New Boolean Condition]** \link Library_HasContent_Role_Mixer `Library.HasContent(Role.Mixer)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasContent(boxsets)`</b>,
+/// \anchor Library_HasContent_Boxsets
+/// _boolean_,
+/// @return **True** if there are albums in the library which are boxsets.
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link Library_HasContent_Boxsets `Library.HasContent(boxsets)`\endlink
+/// <p>
+/// }
+/// \table_row3{ <b>`Library.HasNode(path)`</b>,
+/// \anchor Library_HasNode
+/// _boolean_,
+/// @return **True** if there the node is present in the library.
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link Library_HasNode `Library.HasNode(path)`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+/// -----------------------------------------------------------------------------
+
+/// \page modules__infolabels_boolean_conditions
+/// \section modules_rm_infolabels_booleans Additional revision history for Infolabels and Boolean Conditions
+/// <hr>
+/// \subsection modules_rm_infolabels_booleans_v20 Kodi v20 (Nexus)
+/// @skinning_v20 **[Removed Boolean conditions]** The following boolean conditions have been removed:
+/// - `Player.DisplayAfterSeek` - use \link Player_HasPerformedSeek `Player.HasPerformedSeek(interval)`\endlink instead
+///
+/// <hr>
+/// \subsection modules_rm_infolabels_booleans_v19 Kodi v19 (Matrix)
+/// @skinning_v19 **[Removed Infolabels]** The following infolabels have been removed:
+/// - `System.Platform.Linux.RaspberryPi` - use \link System_Platform_Linux `System.Platform.Linux`\endlink instead
+///
+/// <hr>
+/// \subsection modules_rm_infolabels_booleans_v18 Kodi v18 (Leia)
+///
+/// @skinning_v18 **[Removed Infolabels]** The following infolabels have been removed:
+/// - `Listitem.Property(artistthumbs)`, `Listitem.Property(artistthumb)` - use
+/// \link ListItem_Art_Type `ListItem.Art(type)`\endlink with <b>albumartist[n].*</b> or <b>artist[n].*</b> as <b>type</b>
+/// - `ADSP.ActiveStreamType`
+/// - `ADSP.DetectedStreamType`
+/// - `ADSP.MasterName`
+/// - `ADSP.MasterInfo`
+/// - `ADSP.MasterOwnIcon`
+/// - `ADSP.MasterOverrideIcon`
+/// - `ListItem.ChannelNumber`, `ListItem.SubChannelNumber`, `MusicPlayer.ChannelNumber`,
+/// `MusicPlayer.SubChannelNumber`, `VideoPlayer.ChannelNumber`,
+/// `VideoPlayer.SubChannelNumber`. Please use the following alternatives
+/// \link ListItem_ChannelNumberLabel `ListItem.ChannelNumberLabel` \endlink,
+/// \link MusicPlayer_ChannelNumberLabel `MusicPlayer.ChannelNumberLabel` \endlink
+/// \link VideoPlayer_ChannelNumberLabel `VideoPlayer.ChannelNumberLabel` \endlink from now on.
+///
+/// @skinning_v18 **[Removed Boolean Conditions]** The following infobools have been removed:
+/// - `System.HasModalDialog` - use \link System_HasActiveModalDialog `System.HasActiveModalDialog` \endlink and
+/// \link System_HasVisibleModalDialog `System.HasVisibleModalDialog`\endlink instead
+/// - `StringCompare()` - use \link String_IsEqual `String.IsEqual(info,string)`\endlink instead
+/// - `SubString()` - use \link String_Contains `String.Contains(info,substring)`\endlink instead
+/// - `IntegerGreaterThan()` - use \link Integer_IsGreater `Integer.IsGreater(info,number)`\endlink instead
+/// - `IsEmpty()` - use \link String_IsEmpty `String.IsEmpty(info)`\endlink instead
+/// - `System.HasADSP`
+/// - `ADSP.IsActive`
+/// - `ADSP.HasInputResample`
+/// - `ADSP.HasPreProcess`
+/// - `ADSP.HasMasterProcess`
+/// - `ADSP.HasPostProcess`
+/// - `ADSP.HasOutputResample`
+/// - `ADSP.MasterActive`
+/// <hr>
+/// \subsection modules_rm_infolabels_booleans_v17 Kodi v17 (Krypton)
+/// @skinning_v17 **[Removed Infolabels]** The following infolabels have been removed:
+/// - `ListItem.StarRating` - use the other ratings instead.
+///
+/// @skinning_v17 **[Removed Boolean Conditions]** The following infobools have been removed:
+/// - `on` - use `true` instead
+/// - `off` - use `false` instead
+/// - `Player.ShowCodec`
+/// - `System.GetBool(pvrmanager.enabled)`
+/// <hr>
+/// \subsection modules_rm_infolabels_booleans_v16 Kodi v16 (Jarvis)
+/// @skinning_v16 **[New Boolean Conditions]** The following infobools were added:
+/// - `System.HasADSP`
+/// - `ADSP.IsActive`
+/// - `ADSP.HasInputResample`
+/// - `ADSP.HasPreProcess`
+/// - `ADSP.HasMasterProcess`
+/// - `ADSP.HasPostProcess`
+/// - `ADSP.HasOutputResample`
+/// - `ADSP.MasterActive`
+/// - `System.HasModalDialog`
+///
+/// @skinning_v16 **[New Infolabels]** The following infolabels were added:
+/// - `ADSP.ActiveStreamType`
+/// - `ADSP.DetectedStreamType`
+/// - `ADSP.MasterName`
+/// - `ADSP.MasterInfo`
+/// - `ADSP.MasterOwnIcon`
+/// - `ADSP.MasterOverrideIcon`
+///
+/// @skinning_v16 **[Removed Boolean Conditions]** The following infobols were removed:
+/// - `System.Platform.ATV2`
+
+/// <hr>
+/// \subsection modules_rm_infolabels_booleans_v15 Kodi v15 (Isengard)
+/// <hr>
+/// \subsection modules_rm_infolabels_booleans_v14 Kodi v14 (Helix)
+/// @skinning_v14 **[New Infolabels]** The following infolabels were added:
+/// - `ListItem.SubChannelNumber`
+/// - `MusicPlayer.SubChannelNumber`
+/// - `VideoPlayer.SubChannelNumber`
+///
+/// <hr>
+/// \subsection modules_rm_infolabels_booleans_v13 XBMC v13 (Gotham)
+/// @skinning_v13 **[Removed Infolabels]** The following infolabels were removed:
+/// - `Network.SubnetAddress`
+///
+/// <hr>
+// Crazy part, to use tableofcontents must it be on end
+/// \page modules__infolabels_boolean_conditions
+/// \tableofcontents
+
+CGUIInfoManager::Property::Property(const std::string &property, const std::string &parameters)
+: name(property)
+{
+ CUtil::SplitParams(parameters, params);
+}
+
+const std::string &CGUIInfoManager::Property::param(unsigned int n /* = 0 */) const
+{
+ if (n < params.size())
+ return params[n];
+ return StringUtils::Empty;
+}
+
+unsigned int CGUIInfoManager::Property::num_params() const
+{
+ return params.size();
+}
+
+void CGUIInfoManager::SplitInfoString(const std::string &infoString, std::vector<Property> &info)
+{
+ // our string is of the form:
+ // category[(params)][.info(params).info2(params)] ...
+ // so we need to split on . while taking into account of () pairs
+ unsigned int parentheses = 0;
+ std::string property;
+ std::string param;
+ for (size_t i = 0; i < infoString.size(); ++i)
+ {
+ if (infoString[i] == '(')
+ {
+ if (!parentheses++)
+ continue;
+ }
+ else if (infoString[i] == ')')
+ {
+ if (!parentheses)
+ CLog::Log(LOGERROR, "unmatched parentheses in {}", infoString);
+ else if (!--parentheses)
+ continue;
+ }
+ else if (infoString[i] == '.' && !parentheses)
+ {
+ if (!property.empty()) // add our property and parameters
+ {
+ StringUtils::ToLower(property);
+ info.emplace_back(Property(property, param));
+ }
+ property.clear();
+ param.clear();
+ continue;
+ }
+ if (parentheses)
+ param += infoString[i];
+ else
+ property += infoString[i];
+ }
+
+ if (parentheses)
+ CLog::Log(LOGERROR, "unmatched parentheses in {}", infoString);
+
+ if (!property.empty())
+ {
+ StringUtils::ToLower(property);
+ info.emplace_back(Property(property, param));
+ }
+}
+
+/// \brief Translates a string as given by the skin into an int that we use for more
+/// efficient retrieval of data.
+int CGUIInfoManager::TranslateSingleString(const std::string &strCondition)
+{
+ bool listItemDependent;
+ return TranslateSingleString(strCondition, listItemDependent);
+}
+
+int CGUIInfoManager::TranslateSingleString(const std::string &strCondition, bool &listItemDependent)
+{
+ /* We need to disable caching in INFO::InfoBool::Get if either of the following are true:
+ * 1. if condition is between LISTITEM_START and LISTITEM_END
+ * 2. if condition is string or integer the corresponding label is between LISTITEM_START and LISTITEM_END
+ * This is achieved by setting the bool pointed at by listItemDependent, either here or in a recursive call
+ */
+ // trim whitespaces
+ std::string strTest = strCondition;
+ StringUtils::Trim(strTest);
+
+ std::vector< Property> info;
+ SplitInfoString(strTest, info);
+
+ if (info.empty())
+ return 0;
+
+ const Property &cat = info[0];
+ if (info.size() == 1)
+ { // single category
+ if (cat.name == "false" || cat.name == "no")
+ return SYSTEM_ALWAYS_FALSE;
+ else if (cat.name == "true" || cat.name == "yes")
+ return SYSTEM_ALWAYS_TRUE;
+ }
+ else if (info.size() == 2)
+ {
+ const Property &prop = info[1];
+ if (cat.name == "string")
+ {
+ if (prop.name == "isempty")
+ {
+ return AddMultiInfo(CGUIInfo(STRING_IS_EMPTY, TranslateSingleString(prop.param(), listItemDependent)));
+ }
+ else if (prop.num_params() == 2)
+ {
+ for (const infomap& string_bool : string_bools)
+ {
+ if (prop.name == string_bool.str)
+ {
+ int data1 = TranslateSingleString(prop.param(0), listItemDependent);
+ // pipe our original string through the localize parsing then make it lowercase (picks up $LBRACKET etc.)
+ std::string label = CGUIInfoLabel::GetLabel(prop.param(1), INFO::DEFAULT_CONTEXT);
+ StringUtils::ToLower(label);
+ // 'true', 'false', 'yes', 'no' are valid strings, do not resolve them to SYSTEM_ALWAYS_TRUE or SYSTEM_ALWAYS_FALSE
+ if (label != "true" && label != "false" && label != "yes" && label != "no")
+ {
+ int data2 = TranslateSingleString(prop.param(1), listItemDependent);
+ if (data2 > 0)
+ return AddMultiInfo(CGUIInfo(string_bool.val, data1, -data2));
+ }
+ return AddMultiInfo(CGUIInfo(string_bool.val, data1, label));
+ }
+ }
+ }
+ }
+ if (cat.name == "integer")
+ {
+ if (prop.name == "valueof")
+ {
+ int value = -1;
+ std::from_chars(prop.param(0).data(), prop.param(0).data() + prop.param(0).size(), value);
+ return AddMultiInfo(CGUIInfo(INTEGER_VALUEOF, value));
+ }
+
+ for (const infomap& integer_bool : integer_bools)
+ {
+ if (prop.name == integer_bool.str)
+ {
+ std::array<int, 2> data = {-1, -1};
+ for (size_t i = 0; i < data.size(); i++)
+ {
+ std::from_chars_result result = std::from_chars(
+ prop.param(i).data(), prop.param(i).data() + prop.param(i).size(), data.at(i));
+ if (result.ec == std::errc::invalid_argument)
+ {
+ // could not translate provided value to int, translate the info string
+ data.at(i) = TranslateSingleString(prop.param(i), listItemDependent);
+ }
+ else
+ {
+ // conversion succeeded, integer value provided - translate it to an Integer.ValueOf() info.
+ data.at(i) = AddMultiInfo(CGUIInfo(INTEGER_VALUEOF, data.at(i)));
+ }
+ }
+ return AddMultiInfo(CGUIInfo(integer_bool.val, data.at(0), data.at(1)));
+ }
+ }
+ }
+ else if (cat.name == "player")
+ {
+ for (const infomap& player_label : player_labels)
+ {
+ if (prop.name == player_label.str)
+ return player_label.val;
+ }
+ for (const infomap& player_time : player_times)
+ {
+ if (prop.name == player_time.str)
+ return AddMultiInfo(CGUIInfo(player_time.val, TranslateTimeFormat(prop.param())));
+ }
+ if (prop.name == "process" && prop.num_params())
+ {
+ for (const infomap& player_proces : player_process)
+ {
+ if (StringUtils::EqualsNoCase(prop.param(), player_proces.str))
+ return player_proces.val;
+ }
+ }
+ if (prop.num_params() == 1)
+ {
+ for (const infomap& i : player_param)
+ {
+ if (prop.name == i.str)
+ return AddMultiInfo(CGUIInfo(i.val, prop.param()));
+ }
+ }
+ }
+ else if (cat.name == "addon")
+ {
+ for (const infomap& i : addons)
+ {
+ if (prop.name == i.str && prop.num_params() == 2)
+ return AddMultiInfo(CGUIInfo(i.val, prop.param(0), prop.param(1)));
+ }
+ }
+ else if (cat.name == "weather")
+ {
+ for (const infomap& i : weather)
+ {
+ if (prop.name == i.str)
+ return i.val;
+ }
+ }
+ else if (cat.name == "network")
+ {
+ for (const infomap& network_label : network_labels)
+ {
+ if (prop.name == network_label.str)
+ return network_label.val;
+ }
+ }
+ else if (cat.name == "musicpartymode")
+ {
+ for (const infomap& i : musicpartymode)
+ {
+ if (prop.name == i.str)
+ return i.val;
+ }
+ }
+ else if (cat.name == "system")
+ {
+ for (const infomap& system_label : system_labels)
+ {
+ if (prop.name == system_label.str)
+ return system_label.val;
+ }
+ if (prop.num_params() == 1)
+ {
+ const std::string &param = prop.param();
+ if (prop.name == "getbool")
+ {
+ std::string paramCopy = param;
+ StringUtils::ToLower(paramCopy);
+ return AddMultiInfo(CGUIInfo(SYSTEM_GET_BOOL, paramCopy));
+ }
+ for (const infomap& i : system_param)
+ {
+ if (prop.name == i.str)
+ return AddMultiInfo(CGUIInfo(i.val, param));
+ }
+ if (prop.name == "memory")
+ {
+ if (param == "free")
+ return SYSTEM_FREE_MEMORY;
+ else if (param == "free.percent")
+ return SYSTEM_FREE_MEMORY_PERCENT;
+ else if (param == "used")
+ return SYSTEM_USED_MEMORY;
+ else if (param == "used.percent")
+ return SYSTEM_USED_MEMORY_PERCENT;
+ else if (param == "total")
+ return SYSTEM_TOTAL_MEMORY;
+ }
+ else if (prop.name == "addontitle")
+ {
+ // Example: System.AddonTitle(Skin.String(HomeVideosButton1)) => skin string HomeVideosButton1 holds an addon identifier string
+ int infoLabel = TranslateSingleString(param, listItemDependent);
+ if (infoLabel > 0)
+ return AddMultiInfo(CGUIInfo(SYSTEM_ADDON_TITLE, infoLabel, 0));
+ std::string label = CGUIInfoLabel::GetLabel(param, INFO::DEFAULT_CONTEXT);
+ StringUtils::ToLower(label);
+ return AddMultiInfo(CGUIInfo(SYSTEM_ADDON_TITLE, label, 1));
+ }
+ else if (prop.name == "addonicon")
+ {
+ int infoLabel = TranslateSingleString(param, listItemDependent);
+ if (infoLabel > 0)
+ return AddMultiInfo(CGUIInfo(SYSTEM_ADDON_ICON, infoLabel, 0));
+ std::string label = CGUIInfoLabel::GetLabel(param, INFO::DEFAULT_CONTEXT);
+ StringUtils::ToLower(label);
+ return AddMultiInfo(CGUIInfo(SYSTEM_ADDON_ICON, label, 1));
+ }
+ else if (prop.name == "addonversion")
+ {
+ int infoLabel = TranslateSingleString(param, listItemDependent);
+ if (infoLabel > 0)
+ return AddMultiInfo(CGUIInfo(SYSTEM_ADDON_VERSION, infoLabel, 0));
+ std::string label = CGUIInfoLabel::GetLabel(param, INFO::DEFAULT_CONTEXT);
+ StringUtils::ToLower(label);
+ return AddMultiInfo(CGUIInfo(SYSTEM_ADDON_VERSION, label, 1));
+ }
+ else if (prop.name == "idletime")
+ return AddMultiInfo(CGUIInfo(SYSTEM_IDLE_TIME, atoi(param.c_str())));
+ }
+ if (prop.name == "alarmlessorequal" && prop.num_params() == 2)
+ return AddMultiInfo(CGUIInfo(SYSTEM_ALARM_LESS_OR_EQUAL, prop.param(0), atoi(prop.param(1).c_str())));
+ else if (prop.name == "date")
+ {
+ if (prop.num_params() == 2)
+ return AddMultiInfo(CGUIInfo(SYSTEM_DATE, StringUtils::DateStringToYYYYMMDD(prop.param(0)) % 10000, StringUtils::DateStringToYYYYMMDD(prop.param(1)) % 10000));
+ else if (prop.num_params() == 1)
+ {
+ int dateformat = StringUtils::DateStringToYYYYMMDD(prop.param(0));
+ if (dateformat <= 0) // not concrete date
+ return AddMultiInfo(CGUIInfo(SYSTEM_DATE, prop.param(0), -1));
+ else
+ return AddMultiInfo(CGUIInfo(SYSTEM_DATE, dateformat % 10000));
+ }
+ return SYSTEM_DATE;
+ }
+ else if (prop.name == "time")
+ {
+ if (prop.num_params() == 0)
+ return AddMultiInfo(CGUIInfo(SYSTEM_TIME, TIME_FORMAT_GUESS));
+ if (prop.num_params() == 1)
+ {
+ TIME_FORMAT timeFormat = TranslateTimeFormat(prop.param(0));
+ if (timeFormat == TIME_FORMAT_GUESS)
+ return AddMultiInfo(CGUIInfo(SYSTEM_TIME, StringUtils::TimeStringToSeconds(prop.param(0))));
+ return AddMultiInfo(CGUIInfo(SYSTEM_TIME, timeFormat));
+ }
+ else
+ return AddMultiInfo(CGUIInfo(SYSTEM_TIME, StringUtils::TimeStringToSeconds(prop.param(0)), StringUtils::TimeStringToSeconds(prop.param(1))));
+ }
+ }
+ else if (cat.name == "library")
+ {
+ if (prop.name == "isscanning")
+ return LIBRARY_IS_SCANNING;
+ else if (prop.name == "isscanningvideo")
+ return LIBRARY_IS_SCANNING_VIDEO; //! @todo change to IsScanning(Video)
+ else if (prop.name == "isscanningmusic")
+ return LIBRARY_IS_SCANNING_MUSIC;
+ else if (prop.name == "hascontent" && prop.num_params())
+ {
+ std::string cat = prop.param(0);
+ StringUtils::ToLower(cat);
+ if (cat == "music")
+ return LIBRARY_HAS_MUSIC;
+ else if (cat == "video")
+ return LIBRARY_HAS_VIDEO;
+ else if (cat == "movies")
+ return LIBRARY_HAS_MOVIES;
+ else if (cat == "tvshows")
+ return LIBRARY_HAS_TVSHOWS;
+ else if (cat == "musicvideos")
+ return LIBRARY_HAS_MUSICVIDEOS;
+ else if (cat == "moviesets")
+ return LIBRARY_HAS_MOVIE_SETS;
+ else if (cat == "singles")
+ return LIBRARY_HAS_SINGLES;
+ else if (cat == "compilations")
+ return LIBRARY_HAS_COMPILATIONS;
+ else if (cat == "boxsets")
+ return LIBRARY_HAS_BOXSETS;
+ else if (cat == "role" && prop.num_params() > 1)
+ return AddMultiInfo(CGUIInfo(LIBRARY_HAS_ROLE, prop.param(1), 0));
+ }
+ else if (prop.name == "hasnode" && prop.num_params())
+ {
+ std::string node = prop.param(0);
+ StringUtils::ToLower(node);
+ return AddMultiInfo(CGUIInfo(LIBRARY_HAS_NODE, prop.param(), 0));
+ }
+ }
+ else if (cat.name == "musicplayer")
+ {
+ for (const infomap& player_time : player_times) //! @todo remove these, they're repeats
+ {
+ if (prop.name == player_time.str)
+ return AddMultiInfo(CGUIInfo(player_time.val, TranslateTimeFormat(prop.param())));
+ }
+ if (prop.name == "content" && prop.num_params())
+ return AddMultiInfo(CGUIInfo(MUSICPLAYER_CONTENT, prop.param(), 0));
+ else if (prop.name == "property")
+ {
+ if (StringUtils::EqualsNoCase(prop.param(), "fanart_image"))
+ return AddMultiInfo(CGUIInfo(PLAYER_ITEM_ART, "fanart"));
+
+ return AddMultiInfo(CGUIInfo(MUSICPLAYER_PROPERTY, prop.param()));
+ }
+ return TranslateMusicPlayerString(prop.name);
+ }
+ else if (cat.name == "videoplayer")
+ {
+ if (prop.name != "starttime") // player.starttime is semantically different from videoplayer.starttime which has its own implementation!
+ {
+ for (const infomap& player_time : player_times) //! @todo remove these, they're repeats
+ {
+ if (prop.name == player_time.str)
+ return AddMultiInfo(CGUIInfo(player_time.val, TranslateTimeFormat(prop.param())));
+ }
+ }
+ if (prop.name == "content" && prop.num_params())
+ {
+ return AddMultiInfo(CGUIInfo(VIDEOPLAYER_CONTENT, prop.param(), 0));
+ }
+ if (prop.name == "uniqueid" && prop.num_params())
+ {
+ return AddMultiInfo(CGUIInfo(VIDEOPLAYER_UNIQUEID, prop.param(), 0));
+ }
+ if (prop.name == "art" && prop.num_params() > 0)
+ {
+ return AddMultiInfo(CGUIInfo(VIDEOPLAYER_ART, prop.param(), 0));
+ }
+ return TranslateVideoPlayerString(prop.name);
+ }
+ else if (cat.name == "retroplayer")
+ {
+ for (const infomap& i : retroplayer)
+ {
+ if (prop.name == i.str)
+ return i.val;
+ }
+ }
+ else if (cat.name == "slideshow")
+ {
+ for (const infomap& i : slideshow)
+ {
+ if (prop.name == i.str)
+ return i.val;
+ }
+ }
+ else if (cat.name == "container")
+ {
+ for (const infomap& i : mediacontainer) // these ones don't have or need an id
+ {
+ if (prop.name == i.str)
+ return i.val;
+ }
+ int id = atoi(cat.param().c_str());
+ for (const infomap& container_bool : container_bools) // these ones can have an id (but don't need to?)
+ {
+ if (prop.name == container_bool.str)
+ return id ? AddMultiInfo(CGUIInfo(container_bool.val, id)) : container_bool.val;
+ }
+ for (const infomap& container_int : container_ints) // these ones can have an int param on the property
+ {
+ if (prop.name == container_int.str)
+ return AddMultiInfo(CGUIInfo(container_int.val, id, atoi(prop.param().c_str())));
+ }
+ for (const infomap& i : container_str) // these ones have a string param on the property
+ {
+ if (prop.name == i.str)
+ return AddMultiInfo(CGUIInfo(i.val, id, prop.param()));
+ }
+ if (prop.name == "sortdirection")
+ {
+ SortOrder order = SortOrderNone;
+ if (StringUtils::EqualsNoCase(prop.param(), "ascending"))
+ order = SortOrderAscending;
+ else if (StringUtils::EqualsNoCase(prop.param(), "descending"))
+ order = SortOrderDescending;
+ return AddMultiInfo(CGUIInfo(CONTAINER_SORT_DIRECTION, order));
+ }
+ }
+ else if (cat.name == "listitem" ||
+ cat.name == "listitemposition" ||
+ cat.name == "listitemnowrap" ||
+ cat.name == "listitemabsolute")
+ {
+ int ret = TranslateListItem(cat, prop, 0, false);
+ if (ret)
+ listItemDependent = true;
+ return ret;
+ }
+ else if (cat.name == "visualisation")
+ {
+ for (const infomap& i : visualisation)
+ {
+ if (prop.name == i.str)
+ return i.val;
+ }
+ }
+ else if (cat.name == "fanart")
+ {
+ for (const infomap& fanart_label : fanart_labels)
+ {
+ if (prop.name == fanart_label.str)
+ return fanart_label.val;
+ }
+ }
+ else if (cat.name == "skin")
+ {
+ for (const infomap& skin_label : skin_labels)
+ {
+ if (prop.name == skin_label.str)
+ return skin_label.val;
+ }
+ if (prop.num_params())
+ {
+ if (prop.name == "string")
+ {
+ if (prop.num_params() == 2)
+ return AddMultiInfo(CGUIInfo(SKIN_STRING_IS_EQUAL, CSkinSettings::GetInstance().TranslateString(prop.param(0)), prop.param(1)));
+ else
+ return AddMultiInfo(CGUIInfo(SKIN_STRING, CSkinSettings::GetInstance().TranslateString(prop.param(0))));
+ }
+ else if (prop.name == "numeric")
+ {
+ return AddMultiInfo(
+ CGUIInfo(SKIN_INTEGER, CSkinSettings::GetInstance().TranslateString(prop.param(0))));
+ }
+ else if (prop.name == "hassetting")
+ return AddMultiInfo(CGUIInfo(SKIN_BOOL, CSkinSettings::GetInstance().TranslateBool(prop.param(0))));
+ else if (prop.name == "hastheme")
+ return AddMultiInfo(CGUIInfo(SKIN_HAS_THEME, prop.param(0)));
+ else if (prop.name == "timerisrunning")
+ return AddMultiInfo(CGUIInfo(SKIN_TIMER_IS_RUNNING, prop.param(0)));
+ else if (prop.name == "timerelapsedsecs")
+ return AddMultiInfo(CGUIInfo(SKIN_TIMER_ELAPSEDSECS, prop.param(0)));
+ }
+ }
+ else if (cat.name == "window")
+ {
+ if (prop.name == "property" && prop.num_params() == 1)
+ { //! @todo this doesn't support foo.xml
+ int winID = cat.param().empty() ? 0 : CWindowTranslator::TranslateWindow(cat.param());
+ if (winID != WINDOW_INVALID)
+ return AddMultiInfo(CGUIInfo(WINDOW_PROPERTY, winID, prop.param()));
+ }
+ for (const infomap& window_bool : window_bools)
+ {
+ if (prop.name == window_bool.str)
+ { //! @todo The parameter for these should really be on the first not the second property
+ if (prop.param().find("xml") != std::string::npos)
+ return AddMultiInfo(CGUIInfo(window_bool.val, 0, prop.param()));
+ int winID = prop.param().empty() ? WINDOW_INVALID : CWindowTranslator::TranslateWindow(prop.param());
+ return AddMultiInfo(CGUIInfo(window_bool.val, winID, 0));
+ }
+ }
+ }
+ else if (cat.name == "control")
+ {
+ for (const infomap& control_label : control_labels)
+ {
+ if (prop.name == control_label.str)
+ { //! @todo The parameter for these should really be on the first not the second property
+ int controlID = atoi(prop.param().c_str());
+ if (controlID)
+ return AddMultiInfo(CGUIInfo(control_label.val, controlID, 0));
+ return 0;
+ }
+ }
+ }
+ else if (cat.name == "controlgroup" && prop.name == "hasfocus")
+ {
+ int groupID = atoi(cat.param().c_str());
+ if (groupID)
+ return AddMultiInfo(CGUIInfo(CONTROL_GROUP_HAS_FOCUS, groupID, atoi(prop.param(0).c_str())));
+ }
+ else if (cat.name == "playlist")
+ {
+ int ret = -1;
+ for (const infomap& i : playlist)
+ {
+ if (prop.name == i.str)
+ {
+ ret = i.val;
+ break;
+ }
+ }
+ if (ret >= 0)
+ {
+ if (prop.num_params() <= 0)
+ return ret;
+ else
+ {
+ PLAYLIST::Id playlistid = PLAYLIST::TYPE_NONE;
+ if (StringUtils::EqualsNoCase(prop.param(), "video"))
+ playlistid = PLAYLIST::TYPE_VIDEO;
+ else if (StringUtils::EqualsNoCase(prop.param(), "music"))
+ playlistid = PLAYLIST::TYPE_MUSIC;
+
+ if (playlistid != PLAYLIST::TYPE_NONE)
+ return AddMultiInfo(CGUIInfo(ret, playlistid, 1));
+ }
+ }
+ }
+ else if (cat.name == "pvr")
+ {
+ for (const infomap& i : pvr)
+ {
+ if (prop.name == i.str)
+ return i.val;
+ }
+ for (const infomap& pvr_time : pvr_times)
+ {
+ if (prop.name == pvr_time.str)
+ return AddMultiInfo(CGUIInfo(pvr_time.val, TranslateTimeFormat(prop.param())));
+ }
+ }
+ else if (cat.name == "rds")
+ {
+ if (prop.name == "getline")
+ return AddMultiInfo(CGUIInfo(RDS_GET_RADIOTEXT_LINE, atoi(prop.param(0).c_str())));
+
+ for (const infomap& rd : rds)
+ {
+ if (prop.name == rd.str)
+ return rd.val;
+ }
+ }
+ }
+ else if (info.size() == 3 || info.size() == 4)
+ {
+ if (info[0].name == "system" && info[1].name == "platform")
+ { //! @todo replace with a single system.platform
+ std::string platform = info[2].name;
+ if (platform == "linux")
+ return SYSTEM_PLATFORM_LINUX;
+ else if (platform == "windows")
+ return SYSTEM_PLATFORM_WINDOWS;
+ else if (platform == "uwp")
+ return SYSTEM_PLATFORM_UWP;
+ else if (platform == "darwin")
+ return SYSTEM_PLATFORM_DARWIN;
+ else if (platform == "osx")
+ 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;
+ }
+ if (info[0].name == "musicplayer")
+ { //! @todo these two don't allow duration(foo) and also don't allow more than this number of levels...
+ if (info[1].name == "position")
+ {
+ int position = atoi(info[1].param().c_str());
+ int value = TranslateMusicPlayerString(info[2].name); // musicplayer.position(foo).bar
+ return AddMultiInfo(CGUIInfo(value, 2, position)); // 2 => absolute (0 used for not set)
+ }
+ else if (info[1].name == "offset")
+ {
+ int position = atoi(info[1].param().c_str());
+ int value = TranslateMusicPlayerString(info[2].name); // musicplayer.offset(foo).bar
+ return AddMultiInfo(CGUIInfo(value, 1, position)); // 1 => relative
+ }
+ }
+ else if (info[0].name == "videoplayer")
+ { //! @todo these two don't allow duration(foo) and also don't allow more than this number of levels...
+ if (info[1].name == "position")
+ {
+ int position = atoi(info[1].param().c_str());
+ int value = TranslateVideoPlayerString(info[2].name); // videoplayer.position(foo).bar
+ // additional param for the requested infolabel, e.g. VideoPlayer.Position(1).Art(poster): art is the value, poster is the param
+ const std::string& param = info[2].param();
+ return AddMultiInfo(
+ CGUIInfo(value, 2, position, param)); // 2 => absolute (0 used for not set)
+ }
+ else if (info[1].name == "offset")
+ {
+ int position = atoi(info[1].param().c_str());
+ int value = TranslateVideoPlayerString(info[2].name); // videoplayer.offset(foo).bar
+ // additional param for the requested infolabel, e.g. VideoPlayer.Offset(1).Art(poster): art is the value, poster is the param
+ const std::string& param = info[2].param();
+ return AddMultiInfo(CGUIInfo(value, 1, position, param)); // 1 => relative
+ }
+ }
+ else if (info[0].name == "player")
+ { //! @todo these two don't allow duration(foo) and also don't allow more than this number of levels...
+ if (info[1].name == "position")
+ {
+ int position = atoi(info[1].param().c_str());
+ int value = TranslatePlayerString(info[2].name); // player.position(foo).bar
+ return AddMultiInfo(CGUIInfo(value, 2, position)); // 2 => absolute (0 used for not set)
+ }
+ else if (info[1].name == "offset")
+ {
+ int position = atoi(info[1].param().c_str());
+ int value = TranslatePlayerString(info[2].name); // player.offset(foo).bar
+ return AddMultiInfo(CGUIInfo(value, 1, position)); // 1 => relative
+ }
+ }
+ else if (info[0].name == "container")
+ {
+ if (info[1].name == "listitem" ||
+ info[1].name == "listitemposition" ||
+ info[1].name == "listitemabsolute" ||
+ info[1].name == "listitemnowrap")
+ {
+ int id = atoi(info[0].param().c_str());
+ int ret = TranslateListItem(info[1], info[2], id, true);
+ if (ret)
+ listItemDependent = true;
+ return ret;
+ }
+ }
+ else if (info[0].name == "control")
+ {
+ const Property &prop = info[1];
+ for (const infomap& control_label : control_labels)
+ {
+ if (prop.name == control_label.str)
+ { //! @todo The parameter for these should really be on the first not the second property
+ int controlID = atoi(prop.param().c_str());
+ if (controlID)
+ return AddMultiInfo(CGUIInfo(control_label.val, controlID, atoi(info[2].param(0).c_str())));
+ return 0;
+ }
+ }
+ }
+ }
+
+ return 0;
+}
+
+int CGUIInfoManager::TranslateListItem(const Property& cat, const Property& prop, int id, bool container)
+{
+ int ret = 0;
+ std::string data3;
+ int data4 = 0;
+ if (prop.num_params() == 1)
+ {
+ // special case: map 'property(fanart_image)' to 'art(fanart)'
+ if (prop.name == "property" && StringUtils::EqualsNoCase(prop.param(), "fanart_image"))
+ {
+ ret = LISTITEM_ART;
+ data3 = "fanart";
+ }
+ else if (prop.name == "property" ||
+ prop.name == "art" ||
+ prop.name == "rating" ||
+ prop.name == "votes" ||
+ prop.name == "ratingandvotes" ||
+ prop.name == "uniqueid")
+ {
+ data3 = prop.param();
+ }
+ else if (prop.name == "duration" || prop.name == "nextduration")
+ {
+ data4 = TranslateTimeFormat(prop.param());
+ }
+ }
+
+ if (ret == 0)
+ {
+ for (const infomap& listitem_label : listitem_labels) // these ones don't have or need an id
+ {
+ if (prop.name == listitem_label.str)
+ {
+ ret = listitem_label.val;
+ break;
+ }
+ }
+ }
+
+ if (ret)
+ {
+ int offset = std::atoi(cat.param().c_str());
+
+ int flags = 0;
+ if (cat.name == "listitem")
+ flags = INFOFLAG_LISTITEM_WRAP;
+ else if (cat.name == "listitemposition")
+ flags = INFOFLAG_LISTITEM_POSITION;
+ else if (cat.name == "listitemabsolute")
+ flags = INFOFLAG_LISTITEM_ABSOLUTE;
+ else if (cat.name == "listitemnowrap")
+ flags = INFOFLAG_LISTITEM_NOWRAP;
+
+ if (container)
+ flags |= INFOFLAG_LISTITEM_CONTAINER;
+
+ return AddMultiInfo(CGUIInfo(ret, id, offset, flags, data3, data4));
+ }
+
+ return 0;
+}
+
+int CGUIInfoManager::TranslateMusicPlayerString(const std::string &info) const
+{
+ for (const infomap& i : musicplayer)
+ {
+ if (info == i.str)
+ return i.val;
+ }
+ return 0;
+}
+
+int CGUIInfoManager::TranslateVideoPlayerString(const std::string& info) const
+{
+ for (const infomap& i : videoplayer)
+ {
+ if (info == i.str)
+ return i.val;
+ }
+ return 0;
+}
+
+int CGUIInfoManager::TranslatePlayerString(const std::string& info) const
+{
+ for (const infomap& i : player_labels)
+ {
+ if (info == i.str)
+ return i.val;
+ }
+ return 0;
+}
+
+TIME_FORMAT CGUIInfoManager::TranslateTimeFormat(const std::string &format)
+{
+ if (format.empty())
+ return TIME_FORMAT_GUESS;
+ else if (StringUtils::EqualsNoCase(format, "hh"))
+ return TIME_FORMAT_HH;
+ else if (StringUtils::EqualsNoCase(format, "mm"))
+ return TIME_FORMAT_MM;
+ else if (StringUtils::EqualsNoCase(format, "ss"))
+ return TIME_FORMAT_SS;
+ else if (StringUtils::EqualsNoCase(format, "hh:mm"))
+ return TIME_FORMAT_HH_MM;
+ else if (StringUtils::EqualsNoCase(format, "mm:ss"))
+ return TIME_FORMAT_MM_SS;
+ else if (StringUtils::EqualsNoCase(format, "hh:mm:ss"))
+ return TIME_FORMAT_HH_MM_SS;
+ else if (StringUtils::EqualsNoCase(format, "hh:mm:ss xx"))
+ return TIME_FORMAT_HH_MM_SS_XX;
+ else if (StringUtils::EqualsNoCase(format, "h"))
+ return TIME_FORMAT_H;
+ else if (StringUtils::EqualsNoCase(format, "m"))
+ return TIME_FORMAT_M;
+ else if (StringUtils::EqualsNoCase(format, "h:mm:ss"))
+ return TIME_FORMAT_H_MM_SS;
+ else if (StringUtils::EqualsNoCase(format, "h:mm:ss xx"))
+ return TIME_FORMAT_H_MM_SS_XX;
+ else if (StringUtils::EqualsNoCase(format, "xx"))
+ return TIME_FORMAT_XX;
+ else if (StringUtils::EqualsNoCase(format, "secs"))
+ return TIME_FORMAT_SECS;
+ else if (StringUtils::EqualsNoCase(format, "mins"))
+ return TIME_FORMAT_MINS;
+ else if (StringUtils::EqualsNoCase(format, "hours"))
+ return TIME_FORMAT_HOURS;
+ return TIME_FORMAT_GUESS;
+}
+
+std::string CGUIInfoManager::GetLabel(int info, int contextWindow, std::string *fallback) const
+{
+ if (info >= CONDITIONAL_LABEL_START && info <= CONDITIONAL_LABEL_END)
+ {
+ return GetSkinVariableString(info, contextWindow, false);
+ }
+ else if (info >= MULTI_INFO_START && info <= MULTI_INFO_END)
+ {
+ return GetMultiInfoLabel(m_multiInfo[info - MULTI_INFO_START], contextWindow);
+ }
+ else if (info >= LISTITEM_START && info <= LISTITEM_END)
+ {
+ const CGUIListItemPtr item = GUIINFO::GetCurrentListItem(contextWindow);
+ if (item && item->IsFileItem())
+ return GetItemLabel(static_cast<CFileItem*>(item.get()), contextWindow, info, fallback);
+ }
+
+ std::string strLabel;
+ m_infoProviders.GetLabel(strLabel, m_currentFile, contextWindow, CGUIInfo(info), fallback);
+ return strLabel;
+}
+
+bool CGUIInfoManager::GetInt(int &value, int info, int contextWindow, const CGUIListItem *item /* = nullptr */) const
+{
+ if (info >= MULTI_INFO_START && info <= MULTI_INFO_END)
+ {
+ return GetMultiInfoInt(value, m_multiInfo[info - MULTI_INFO_START], contextWindow, item);
+ }
+ else if (info >= LISTITEM_START && info <= LISTITEM_END)
+ {
+ CGUIListItemPtr itemPtr;
+ if (!item)
+ {
+ itemPtr = GUIINFO::GetCurrentListItem(contextWindow);
+ item = itemPtr.get();
+ }
+ return GetItemInt(value, item, contextWindow, info);
+ }
+
+ value = 0;
+ return m_infoProviders.GetInt(value, m_currentFile, contextWindow, CGUIInfo(info));
+}
+
+INFO::InfoPtr CGUIInfoManager::Register(const std::string &expression, int context)
+{
+ std::string condition(CGUIInfoLabel::ReplaceLocalize(expression));
+ StringUtils::Trim(condition);
+
+ if (condition.empty())
+ return INFO::InfoPtr();
+
+ std::unique_lock<CCriticalSection> lock(m_critInfo);
+ std::pair<INFOBOOLTYPE::iterator, bool> res;
+
+ if (condition.find_first_of("|+[]!") != condition.npos)
+ res = m_bools.insert(std::make_shared<InfoExpression>(condition, context, m_refreshCounter));
+ else
+ res = m_bools.insert(std::make_shared<InfoSingle>(condition, context, m_refreshCounter));
+
+ if (res.second)
+ res.first->get()->Initialize();
+
+ return *(res.first);
+}
+
+void CGUIInfoManager::UnRegister(const INFO::InfoPtr& expression)
+{
+ std::unique_lock<CCriticalSection> lock(m_critInfo);
+ m_bools.erase(expression);
+}
+
+bool CGUIInfoManager::EvaluateBool(const std::string &expression, int contextWindow /* = 0 */, const CGUIListItemPtr &item /* = nullptr */)
+{
+ INFO::InfoPtr info = Register(expression, contextWindow);
+ if (info)
+ return info->Get(contextWindow, item.get());
+ return false;
+}
+
+bool CGUIInfoManager::GetBool(int condition1, int contextWindow, const CGUIListItem *item)
+{
+ bool bReturn = false;
+ int condition = std::abs(condition1);
+
+ if (condition >= LISTITEM_START && condition < LISTITEM_END)
+ {
+ CGUIListItemPtr itemPtr;
+ if (!item)
+ {
+ itemPtr = GUIINFO::GetCurrentListItem(contextWindow);
+ item = itemPtr.get();
+ }
+ bReturn = GetItemBool(item, contextWindow, condition);
+ }
+ else if (condition >= MULTI_INFO_START && condition <= MULTI_INFO_END)
+ {
+ bReturn = GetMultiInfoBool(m_multiInfo[condition - MULTI_INFO_START], contextWindow, item);
+ }
+ else if (!m_infoProviders.GetBool(bReturn, m_currentFile, contextWindow, CGUIInfo(condition)))
+ {
+ // default: use integer value different from 0 as true
+ int val;
+ bReturn = GetInt(val, condition, DEFAULT_CONTEXT) && val != 0;
+ }
+
+ return (condition1 < 0) ? !bReturn : bReturn;
+}
+
+bool CGUIInfoManager::GetMultiInfoBool(const CGUIInfo &info, int contextWindow, const CGUIListItem *item)
+{
+ bool bReturn = false;
+ int condition = std::abs(info.m_info);
+
+ if (condition >= LISTITEM_START && condition <= LISTITEM_END)
+ {
+ CGUIListItemPtr itemPtr;
+ if (!item)
+ {
+ itemPtr = GUIINFO::GetCurrentListItem(contextWindow, info.GetData1(), info.GetData2(), info.GetInfoFlag());
+ item = itemPtr.get();
+ }
+ if (item)
+ {
+ if (condition == LISTITEM_PROPERTY)
+ {
+ if (item->HasProperty(info.GetData3()))
+ bReturn = item->GetProperty(info.GetData3()).asBoolean();
+ }
+ else
+ bReturn = GetItemBool(item, contextWindow, condition);
+ }
+ else
+ {
+ bReturn = false;
+ }
+ }
+ else if (!m_infoProviders.GetBool(bReturn, m_currentFile, contextWindow, info))
+ {
+ switch (condition)
+ {
+ case STRING_IS_EMPTY:
+ // note: Get*Image() falls back to Get*Label(), so this should cover all of them
+ if (item && item->IsFileItem() && IsListItemInfo(info.GetData1()))
+ bReturn = GetItemImage(item, contextWindow, info.GetData1()).empty();
+ else
+ bReturn = GetImage(info.GetData1(), contextWindow).empty();
+ break;
+ case STRING_STARTS_WITH:
+ case STRING_ENDS_WITH:
+ case STRING_CONTAINS:
+ case STRING_IS_EQUAL:
+ {
+ std::string compare;
+ if (info.GetData2() < 0) // info labels are stored with negative numbers
+ {
+ int info2 = -info.GetData2();
+ CGUIListItemPtr item2;
+
+ if (IsListItemInfo(info2))
+ {
+ int iResolvedInfo2 = ResolveMultiInfo(info2);
+ if (iResolvedInfo2 != 0)
+ {
+ const GUIINFO::CGUIInfo& resolvedInfo2 = m_multiInfo[iResolvedInfo2 - MULTI_INFO_START];
+ if (resolvedInfo2.GetInfoFlag() & INFOFLAG_LISTITEM_CONTAINER)
+ item2 = GUIINFO::GetCurrentListItem(contextWindow, resolvedInfo2.GetData1()); // data1 contains the container id
+ }
+ }
+
+ if (item2 && item2->IsFileItem())
+ compare = GetItemImage(item2.get(), contextWindow, info2);
+ else if (item && item->IsFileItem())
+ compare = GetItemImage(item, contextWindow, info2);
+ else
+ compare = GetImage(info2, contextWindow);
+ }
+ else if (!info.GetData3().empty())
+ { // conditional string
+ compare = info.GetData3();
+ }
+ StringUtils::ToLower(compare);
+
+ std::string label;
+ if (item && item->IsFileItem() && IsListItemInfo(info.GetData1()))
+ label = GetItemImage(item, contextWindow, info.GetData1());
+ else
+ label = GetImage(info.GetData1(), contextWindow);
+ StringUtils::ToLower(label);
+
+ if (condition == STRING_STARTS_WITH)
+ bReturn = StringUtils::StartsWith(label, compare);
+ else if (condition == STRING_ENDS_WITH)
+ bReturn = StringUtils::EndsWith(label, compare);
+ else if (condition == STRING_CONTAINS)
+ bReturn = label.find(compare) != std::string::npos;
+ else
+ bReturn = StringUtils::EqualsNoCase(label, compare);
+ }
+ break;
+ case INTEGER_IS_EQUAL:
+ case INTEGER_GREATER_THAN:
+ case INTEGER_GREATER_OR_EQUAL:
+ case INTEGER_LESS_THAN:
+ case INTEGER_LESS_OR_EQUAL:
+ case INTEGER_EVEN:
+ case INTEGER_ODD:
+ {
+ auto getIntValue = [this, &item, &contextWindow](int infoNum) {
+ int intValue = 0;
+ if (!GetInt(intValue, infoNum, contextWindow, item))
+ {
+ std::string value;
+ if (item && item->IsFileItem() && IsListItemInfo(infoNum))
+ value = GetItemImage(item, contextWindow, infoNum);
+ else
+ value = GetImage(infoNum, contextWindow);
+
+ // Handle the case when a value contains time separator (:). This makes Integer.IsGreater
+ // useful for Player.Time* members without adding a separate set of members returning time in seconds
+ if (value.find_first_of(':') != value.npos)
+ intValue = StringUtils::TimeStringToSeconds(value);
+ else
+ std::from_chars(value.data(), value.data() + value.size(), intValue);
+ }
+ return intValue;
+ };
+
+ int leftIntValue = getIntValue(info.GetData1());
+ int rightIntValue = getIntValue(info.GetData2());
+
+ // compare
+ if (condition == INTEGER_IS_EQUAL)
+ bReturn = leftIntValue == rightIntValue;
+ else if (condition == INTEGER_GREATER_THAN)
+ bReturn = leftIntValue > rightIntValue;
+ else if (condition == INTEGER_GREATER_OR_EQUAL)
+ bReturn = leftIntValue >= rightIntValue;
+ else if (condition == INTEGER_LESS_THAN)
+ bReturn = leftIntValue < rightIntValue;
+ else if (condition == INTEGER_LESS_OR_EQUAL)
+ bReturn = leftIntValue <= rightIntValue;
+ else if (condition == INTEGER_EVEN)
+ bReturn = leftIntValue % 2 == 0;
+ else if (condition == INTEGER_ODD)
+ bReturn = leftIntValue % 2 != 0;
+ }
+ break;
+ }
+ }
+ return (info.m_info < 0) ? !bReturn : bReturn;
+}
+
+bool CGUIInfoManager::GetMultiInfoInt(int &value, const CGUIInfo &info, int contextWindow, const CGUIListItem *item) const
+{
+ if (info.m_info == INTEGER_VALUEOF)
+ {
+ value = info.GetData1();
+ return true;
+ }
+ else if (info.m_info >= LISTITEM_START && info.m_info <= LISTITEM_END)
+ {
+ CGUIListItemPtr itemPtr;
+ if (!item)
+ {
+ itemPtr = GUIINFO::GetCurrentListItem(contextWindow, info.GetData1(), info.GetData2(), info.GetInfoFlag());
+ item = itemPtr.get();
+ }
+ if (item)
+ {
+ if (info.m_info == LISTITEM_PROPERTY)
+ {
+ if (item->HasProperty(info.GetData3()))
+ {
+ value = item->GetProperty(info.GetData3()).asInteger();
+ return true;
+ }
+ return false;
+ }
+ else
+ return GetItemInt(value, item, contextWindow, info.m_info);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ return m_infoProviders.GetInt(value, m_currentFile, contextWindow, info);
+}
+
+std::string CGUIInfoManager::GetMultiInfoLabel(const CGUIInfo &constinfo, int contextWindow, std::string *fallback) const
+{
+ CGUIInfo info(constinfo);
+
+ if (info.m_info >= LISTITEM_START && info.m_info <= LISTITEM_END)
+ {
+ const CGUIListItemPtr item = GUIINFO::GetCurrentListItem(contextWindow, info.GetData1(), info.GetData2(), info.GetInfoFlag());
+ if (item)
+ {
+ // Image prioritizes images over labels (in the case of music item ratings for instance)
+ return GetMultiInfoItemImage(dynamic_cast<CFileItem*>(item.get()), contextWindow, info, fallback);
+ }
+ else
+ {
+ return std::string();
+ }
+ }
+ else if (info.m_info == SYSTEM_ADDON_TITLE ||
+ info.m_info == SYSTEM_ADDON_ICON ||
+ info.m_info == SYSTEM_ADDON_VERSION)
+ {
+ if (info.GetData2() == 0)
+ {
+ // resolve the addon id
+ const std::string addonId = GetLabel(info.GetData1(), contextWindow);
+ info = CGUIInfo(info.m_info, addonId);
+ }
+ }
+
+ std::string strValue;
+ m_infoProviders.GetLabel(strValue, m_currentFile, contextWindow, info, fallback);
+ return strValue;
+}
+
+/// \brief Obtains the filename of the image to show from whichever subsystem is needed
+std::string CGUIInfoManager::GetImage(int info, int contextWindow, std::string *fallback)
+{
+ if (info >= CONDITIONAL_LABEL_START && info <= CONDITIONAL_LABEL_END)
+ {
+ return GetSkinVariableString(info, contextWindow, true);
+ }
+ else if (info >= MULTI_INFO_START && info <= MULTI_INFO_END)
+ {
+ return GetMultiInfoLabel(m_multiInfo[info - MULTI_INFO_START], contextWindow, fallback);
+ }
+ else if (info == LISTITEM_THUMB ||
+ info == LISTITEM_ICON ||
+ info == LISTITEM_ACTUAL_ICON ||
+ info == LISTITEM_OVERLAY ||
+ info == LISTITEM_ART)
+ {
+ const CGUIListItemPtr item = GUIINFO::GetCurrentListItem(contextWindow);
+ if (item && item->IsFileItem())
+ return GetItemImage(item.get(), contextWindow, info, fallback);
+ }
+
+ return GetLabel(info, contextWindow, fallback);
+}
+
+void CGUIInfoManager::ResetCurrentItem()
+{
+ m_currentFile->Reset();
+ m_infoProviders.InitCurrentItem(nullptr);
+}
+
+void CGUIInfoManager::UpdateCurrentItem(const CFileItem &item)
+{
+ m_currentFile->UpdateInfo(item);
+}
+
+void CGUIInfoManager::SetCurrentItem(const CFileItem &item)
+{
+ *m_currentFile = item;
+ m_currentFile->FillInDefaultIcon();
+
+ m_infoProviders.InitCurrentItem(m_currentFile);
+
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Info, "OnChanged");
+}
+
+void CGUIInfoManager::SetCurrentAlbumThumb(const std::string &thumbFileName)
+{
+ if (CFileUtils::Exists(thumbFileName))
+ m_currentFile->SetArt("thumb", thumbFileName);
+ else
+ {
+ m_currentFile->SetArt("thumb", "");
+ m_currentFile->FillInDefaultIcon();
+ }
+}
+
+void CGUIInfoManager::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critInfo);
+ m_skinVariableStrings.clear();
+
+ /*
+ Erase any info bools that are unused. We do this repeatedly as each run
+ will remove those bools that are no longer dependencies of other bools
+ in the vector.
+ */
+ INFOBOOLTYPE swapList(&InfoBoolComparator);
+ do
+ {
+ swapList.clear();
+ for (auto &item : m_bools)
+ if (!item.unique())
+ swapList.insert(item);
+ m_bools.swap(swapList);
+ } while (swapList.size() != m_bools.size());
+
+ // log which ones are used - they should all be gone by now
+ for (INFOBOOLTYPE::const_iterator i = m_bools.begin(); i != m_bools.end(); ++i)
+ CLog::Log(LOGDEBUG, "Infobool '{}' still used by {} instances", (*i)->GetExpression(),
+ (unsigned int)i->use_count());
+}
+
+void CGUIInfoManager::UpdateAVInfo()
+{
+ if (CServiceBroker::GetDataCacheCore().HasAVInfoChanges())
+ {
+ VideoStreamInfo video;
+ AudioStreamInfo audio;
+ SubtitleStreamInfo subtitle;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->GetVideoStreamInfo(CURRENT_STREAM, video);
+ appPlayer->GetAudioStreamInfo(CURRENT_STREAM, audio);
+ appPlayer->GetSubtitleStreamInfo(CURRENT_STREAM, subtitle);
+
+ m_infoProviders.UpdateAVInfo(audio, video, subtitle);
+ }
+}
+
+int CGUIInfoManager::AddMultiInfo(const CGUIInfo &info)
+{
+ // check to see if we have this info already
+ for (unsigned int i = 0; i < m_multiInfo.size(); ++i)
+ if (m_multiInfo[i] == info)
+ return static_cast<int>(i) + MULTI_INFO_START;
+ // return the new offset
+ m_multiInfo.emplace_back(info);
+ int id = static_cast<int>(m_multiInfo.size()) + MULTI_INFO_START - 1;
+ if (id > MULTI_INFO_END)
+ CLog::Log(LOGERROR, "{} - too many multiinfo bool/labels in this skin", __FUNCTION__);
+ return id;
+}
+
+int CGUIInfoManager::ResolveMultiInfo(int info) const
+{
+ int iLastInfo = 0;
+
+ int iResolvedInfo = info;
+ while (iResolvedInfo >= MULTI_INFO_START && iResolvedInfo <= MULTI_INFO_END)
+ {
+ iLastInfo = iResolvedInfo;
+ iResolvedInfo = m_multiInfo[iResolvedInfo - MULTI_INFO_START].m_info;
+ }
+
+ return iLastInfo;
+}
+
+bool CGUIInfoManager::IsListItemInfo(int info) const
+{
+ int iResolvedInfo = info;
+ while (iResolvedInfo >= MULTI_INFO_START && iResolvedInfo <= MULTI_INFO_END)
+ iResolvedInfo = m_multiInfo[iResolvedInfo - MULTI_INFO_START].m_info;
+
+ return (iResolvedInfo >= LISTITEM_START && iResolvedInfo <= LISTITEM_END);
+}
+
+bool CGUIInfoManager::GetItemInt(int &value, const CGUIListItem *item, int contextWindow, int info) const
+{
+ value = 0;
+
+ if (!item)
+ return false;
+
+ return m_infoProviders.GetInt(value, item, contextWindow, CGUIInfo(info));
+}
+
+std::string CGUIInfoManager::GetItemLabel(const CFileItem *item, int contextWindow, int info, std::string *fallback /* = nullptr */) const
+{
+ return GetMultiInfoItemLabel(item, contextWindow, CGUIInfo(info), fallback);
+}
+
+std::string CGUIInfoManager::GetMultiInfoItemLabel(const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback /* = nullptr */) const
+{
+ if (!item)
+ return std::string();
+
+ std::string value;
+
+ if (info.m_info >= CONDITIONAL_LABEL_START && info.m_info <= CONDITIONAL_LABEL_END)
+ {
+ return GetSkinVariableString(info.m_info, contextWindow, false, item);
+ }
+ else if (info.m_info >= MULTI_INFO_START && info.m_info <= MULTI_INFO_END)
+ {
+ return GetMultiInfoItemLabel(item, contextWindow, m_multiInfo[info.m_info - MULTI_INFO_START], fallback);
+ }
+ else if (!m_infoProviders.GetLabel(value, item, contextWindow, info, fallback))
+ {
+ switch (info.m_info)
+ {
+ case LISTITEM_PROPERTY:
+ return item->GetProperty(info.GetData3()).asString();
+ case LISTITEM_LABEL:
+ return item->GetLabel();
+ case LISTITEM_LABEL2:
+ return item->GetLabel2();
+ case LISTITEM_FILENAME:
+ case LISTITEM_FILE_EXTENSION:
+ case LISTITEM_FILENAME_NO_EXTENSION:
+ {
+ std::string strFile = URIUtils::GetFileName(item->GetPath());
+ if (info.m_info == LISTITEM_FILE_EXTENSION)
+ {
+ std::string strExtension = URIUtils::GetExtension(strFile);
+ return StringUtils::TrimLeft(strExtension, ".");
+ }
+ else if (info.m_info == LISTITEM_FILENAME_NO_EXTENSION)
+ {
+ URIUtils::RemoveExtension(strFile);
+ }
+ return strFile;
+ }
+ case LISTITEM_DATE:
+ if (item->m_dateTime.IsValid())
+ return item->m_dateTime.GetAsLocalizedDate();
+ break;
+ case LISTITEM_DATETIME:
+ if (item->m_dateTime.IsValid())
+ return item->m_dateTime.GetAsLocalizedDateTime();
+ break;
+ case LISTITEM_SIZE:
+ if (!item->m_bIsFolder || item->m_dwSize)
+ return StringUtils::SizeToString(item->m_dwSize);
+ break;
+ case LISTITEM_PROGRAM_COUNT:
+ return std::to_string(item->m_iprogramCount);
+ case LISTITEM_ACTUAL_ICON:
+ return item->GetArt("icon");
+ case LISTITEM_ICON:
+ {
+ std::string strThumb = item->GetThumbHideIfUnwatched(item);
+ if (strThumb.empty())
+ strThumb = item->GetArt("icon");
+ if (fallback)
+ *fallback = item->GetArt("icon");
+ return strThumb;
+ }
+ case LISTITEM_ART:
+ return item->GetArt(info.GetData3());
+ case LISTITEM_OVERLAY:
+ return item->GetOverlayImage();
+ case LISTITEM_THUMB:
+ return item->GetThumbHideIfUnwatched(item);
+ case LISTITEM_FOLDERPATH:
+ return CURL(item->GetPath()).GetWithoutUserDetails();
+ case LISTITEM_FOLDERNAME:
+ case LISTITEM_PATH:
+ {
+ std::string path;
+ URIUtils::GetParentPath(item->GetPath(), path);
+ path = CURL(path).GetWithoutUserDetails();
+ if (info.m_info == LISTITEM_FOLDERNAME)
+ {
+ URIUtils::RemoveSlashAtEnd(path);
+ path = URIUtils::GetFileName(path);
+ }
+ return path;
+ }
+ case LISTITEM_FILENAME_AND_PATH:
+ {
+ std::string path = item->GetPath();
+ path = CURL(path).GetWithoutUserDetails();
+ return path;
+ }
+ case LISTITEM_SORT_LETTER:
+ {
+ std::string letter;
+ std::wstring character(1, item->GetSortLabel()[0]);
+ StringUtils::ToUpper(character);
+ g_charsetConverter.wToUTF8(character, letter);
+ return letter;
+ }
+ case LISTITEM_STARTTIME:
+ {
+ if (item->m_dateTime.IsValid())
+ return item->m_dateTime.GetAsLocalizedTime("", false);
+ break;
+ }
+ case LISTITEM_STARTDATE:
+ {
+ if (item->m_dateTime.IsValid())
+ return item->m_dateTime.GetAsLocalizedDate(true);
+ break;
+ }
+ case LISTITEM_CURRENTITEM:
+ return std::to_string(item->GetCurrentItem());
+ }
+ }
+
+ return value;
+}
+
+std::string CGUIInfoManager::GetItemImage(const CGUIListItem *item, int contextWindow, int info, std::string *fallback /*= nullptr*/) const
+{
+ if (!item || !item->IsFileItem())
+ return std::string();
+
+ return GetMultiInfoItemImage(static_cast<const CFileItem*>(item), contextWindow, CGUIInfo(info), fallback);
+}
+
+std::string CGUIInfoManager::GetMultiInfoItemImage(const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback /*= nullptr*/) const
+{
+ if (info.m_info >= CONDITIONAL_LABEL_START && info.m_info <= CONDITIONAL_LABEL_END)
+ {
+ return GetSkinVariableString(info.m_info, contextWindow, true, item);
+ }
+ else if (info.m_info >= MULTI_INFO_START && info.m_info <= MULTI_INFO_END)
+ {
+ return GetMultiInfoItemImage(item, contextWindow, m_multiInfo[info.m_info - MULTI_INFO_START], fallback);
+ }
+
+ return GetMultiInfoItemLabel(item, contextWindow, info, fallback);
+}
+
+bool CGUIInfoManager::GetItemBool(const CGUIListItem *item, int contextWindow, int condition) const
+{
+ if (!item)
+ return false;
+
+ bool value = false;
+ if (!m_infoProviders.GetBool(value, item, contextWindow, CGUIInfo(condition)))
+ {
+ switch (condition)
+ {
+ case LISTITEM_ISSELECTED:
+ return item->IsSelected();
+ case LISTITEM_IS_FOLDER:
+ return item->m_bIsFolder;
+ case LISTITEM_IS_PARENTFOLDER:
+ {
+ if (item->IsFileItem())
+ {
+ const CFileItem *pItem = static_cast<const CFileItem *>(item);
+ return pItem->IsParentFolder();
+ }
+ break;
+ }
+ }
+ }
+
+ return value;
+}
+
+void CGUIInfoManager::ResetCache()
+{
+ // mark our infobools as dirty
+ std::unique_lock<CCriticalSection> lock(m_critInfo);
+ ++m_refreshCounter;
+}
+
+void CGUIInfoManager::SetCurrentVideoTag(const CVideoInfoTag &tag)
+{
+ m_currentFile->SetFromVideoInfoTag(tag);
+ m_currentFile->SetStartOffset(0);
+}
+
+void CGUIInfoManager::SetCurrentSongTag(const MUSIC_INFO::CMusicInfoTag &tag)
+{
+ m_currentFile->SetFromMusicInfoTag(tag);
+ m_currentFile->SetStartOffset(0);
+}
+
+const MUSIC_INFO::CMusicInfoTag* CGUIInfoManager::GetCurrentSongTag() const
+{
+ if (m_currentFile->HasMusicInfoTag())
+ return m_currentFile->GetMusicInfoTag();
+
+ return nullptr;
+}
+
+const CVideoInfoTag* CGUIInfoManager::GetCurrentMovieTag() const
+{
+ if (m_currentFile->HasVideoInfoTag())
+ return m_currentFile->GetVideoInfoTag();
+
+ return nullptr;
+}
+
+const KODI::GAME::CGameInfoTag* CGUIInfoManager::GetCurrentGameTag() const
+{
+ if (m_currentFile->HasGameInfoTag())
+ return m_currentFile->GetGameInfoTag();
+
+ return nullptr;
+}
+
+int CGUIInfoManager::RegisterSkinVariableString(const CSkinVariableString* info)
+{
+ if (!info)
+ return 0;
+
+ std::unique_lock<CCriticalSection> lock(m_critInfo);
+ m_skinVariableStrings.emplace_back(*info);
+ delete info;
+ return CONDITIONAL_LABEL_START + m_skinVariableStrings.size() - 1;
+}
+
+int CGUIInfoManager::TranslateSkinVariableString(const std::string& name, int context)
+{
+ for (std::vector<CSkinVariableString>::const_iterator it = m_skinVariableStrings.begin();
+ it != m_skinVariableStrings.end(); ++it)
+ {
+ if (StringUtils::EqualsNoCase(it->GetName(), name) && it->GetContext() == context)
+ return it - m_skinVariableStrings.begin() + CONDITIONAL_LABEL_START;
+ }
+ return 0;
+}
+
+std::string CGUIInfoManager::GetSkinVariableString(int info,
+ int contextWindow,
+ bool preferImage /*= false*/,
+ const CGUIListItem* item /*= nullptr*/) const
+{
+ info -= CONDITIONAL_LABEL_START;
+ if (info >= 0 && info < static_cast<int>(m_skinVariableStrings.size()))
+ return m_skinVariableStrings[info].GetValue(contextWindow, preferImage, item);
+
+ return "";
+}
+
+bool CGUIInfoManager::ConditionsChangedValues(const std::map<INFO::InfoPtr, bool>& map)
+{
+ for (std::map<INFO::InfoPtr, bool>::const_iterator it = map.begin() ; it != map.end() ; ++it)
+ {
+ if (it->first->Get(INFO::DEFAULT_CONTEXT) != it->second)
+ return true;
+ }
+ return false;
+}
+
+int CGUIInfoManager::GetMessageMask()
+{
+ return TMSG_MASK_GUIINFOMANAGER;
+}
+
+void CGUIInfoManager::OnApplicationMessage(KODI::MESSAGING::ThreadMessage* pMsg)
+{
+ switch (pMsg->dwMessage)
+ {
+ case TMSG_GUI_INFOLABEL:
+ {
+ if (pMsg->lpVoid)
+ {
+ auto infoLabels = static_cast<std::vector<std::string>*>(pMsg->lpVoid);
+ for (auto& param : pMsg->params)
+ infoLabels->emplace_back(GetLabel(TranslateString(param), DEFAULT_CONTEXT));
+ }
+ }
+ break;
+
+ case TMSG_GUI_INFOBOOL:
+ {
+ if (pMsg->lpVoid)
+ {
+ auto infoLabels = static_cast<std::vector<bool>*>(pMsg->lpVoid);
+ for (auto& param : pMsg->params)
+ infoLabels->push_back(EvaluateBool(param, DEFAULT_CONTEXT));
+ }
+ }
+ break;
+
+ case TMSG_UPDATE_CURRENT_ITEM:
+ {
+ CFileItem* item = static_cast<CFileItem*>(pMsg->lpVoid);
+ if (!item)
+ return;
+
+ if (pMsg->param1 == 1 && item->HasMusicInfoTag()) // only grab music tag
+ SetCurrentSongTag(*item->GetMusicInfoTag());
+ else if (pMsg->param1 == 2 && item->HasVideoInfoTag()) // only grab video tag
+ SetCurrentVideoTag(*item->GetVideoInfoTag());
+ else
+ SetCurrentItem(*item);
+
+ delete item;
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+void CGUIInfoManager::RegisterInfoProvider(IGUIInfoProvider *provider)
+{
+ if (!CServiceBroker::GetWinSystem())
+ return;
+
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ m_infoProviders.RegisterProvider(provider, false);
+}
+
+void CGUIInfoManager::UnregisterInfoProvider(IGUIInfoProvider *provider)
+{
+ if (!CServiceBroker::GetWinSystem())
+ return;
+
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ m_infoProviders.UnregisterProvider(provider);
+}
diff --git a/xbmc/GUIInfoManager.h b/xbmc/GUIInfoManager.h
new file mode 100644
index 0000000..1098281
--- /dev/null
+++ b/xbmc/GUIInfoManager.h
@@ -0,0 +1,230 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProviders.h"
+#include "interfaces/info/InfoBool.h"
+#include "interfaces/info/SkinVariable.h"
+#include "messaging/IMessageTarget.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CVideoInfoTag;
+
+class CGUIListItem;
+typedef std::shared_ptr<CGUIListItem> CGUIListItemPtr;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameInfoTag;
+}
+namespace GUILIB
+{
+namespace GUIINFO
+{
+ class CGUIInfo;
+ class IGUIInfoProvider;
+}
+}
+}
+namespace INFO
+{
+ class InfoSingle;
+}
+namespace MUSIC_INFO
+{
+ class CMusicInfoTag;
+}
+
+/*!
+ \ingroup strings
+ \brief
+ */
+class CGUIInfoManager : public KODI::MESSAGING::IMessageTarget
+{
+public:
+ CGUIInfoManager(void);
+ ~CGUIInfoManager(void) override;
+
+ void Initialize();
+
+ void Clear();
+ void ResetCache();
+
+ // KODI::MESSAGING::IMessageTarget implementation
+ int GetMessageMask() override;
+ void OnApplicationMessage(KODI::MESSAGING::ThreadMessage* pMsg) override;
+
+ /*! \brief Register a boolean condition/expression
+ This routine allows controls or other clients of the info manager to register
+ to receive updates of particular expressions, in a particular context (currently windows).
+
+ In the future, it will allow clients to receive pushed callbacks when the expression changes.
+
+ \param expression the boolean condition or expression
+ \param context the context window
+ \return an identifier used to reference this expression
+ */
+ INFO::InfoPtr Register(const std::string &expression, int context = 0);
+
+ /*! \brief Unregister a boolean condition/expression
+ * This routine allows controls or other clients of the info manager to unregister a previously registered
+ * boolean condition/expression
+ \param expression the boolean condition or expression
+ */
+ void UnRegister(const INFO::InfoPtr& expression);
+
+ /// \brief iterates through boolean conditions and compares their stored values to current values. Returns true if any condition changed value.
+ bool ConditionsChangedValues(const std::map<INFO::InfoPtr, bool>& map);
+
+ /*! \brief Evaluate a boolean expression
+ \param expression the expression to evaluate
+ \param context the context in which to evaluate the expression (currently windows)
+ \return the value of the evaluated expression.
+ \sa Register
+ */
+ bool EvaluateBool(const std::string& expression,
+ int context,
+ const CGUIListItemPtr& item = nullptr);
+
+ int TranslateString(const std::string &strCondition);
+ int TranslateSingleString(const std::string &strCondition, bool &listItemDependent);
+
+ std::string GetLabel(int info, int contextWindow, std::string* fallback = nullptr) const;
+ std::string GetImage(int info, int contextWindow, std::string *fallback = nullptr);
+ bool GetInt(int& value, int info, int contextWindow, const CGUIListItem* item = nullptr) const;
+ bool GetBool(int condition, int contextWindow, const CGUIListItem* item = nullptr);
+
+ std::string GetItemLabel(const CFileItem *item, int contextWindow, int info, std::string *fallback = nullptr) const;
+ std::string GetItemImage(const CGUIListItem *item, int contextWindow, int info, std::string *fallback = nullptr) const;
+ /*! \brief Get integer value of info.
+ \param value int reference to pass value of given info
+ \param info id of info
+ \param context the context in which to evaluate the expression (currently windows)
+ \param item optional listitem if want to get listitem related int
+ \return true if given info was handled
+ \sa GetItemInt, GetMultiInfoInt
+ */
+ bool GetItemInt(int &value, const CGUIListItem *item, int contextWindow, int info) const;
+ bool GetItemBool(const CGUIListItem *item, int contextWindow, int condition) const;
+
+ /*! \brief Set currently playing file item
+ */
+ void SetCurrentItem(const CFileItem &item);
+ void ResetCurrentItem();
+ void UpdateCurrentItem(const CFileItem &item);
+
+ // Current song stuff
+ void SetCurrentAlbumThumb(const std::string &thumbFileName);
+ const MUSIC_INFO::CMusicInfoTag *GetCurrentSongTag() const;
+
+ // Current video stuff
+ const CVideoInfoTag* GetCurrentMovieTag() const;
+
+ // Current game stuff
+ const KODI::GAME::CGameInfoTag* GetCurrentGameTag() const;
+
+ void UpdateAVInfo();
+
+ int RegisterSkinVariableString(const INFO::CSkinVariableString* info);
+ int TranslateSkinVariableString(const std::string& name, int context);
+
+ /*! \brief register a guiinfo provider
+ \param the guiinfo provider to register
+ */
+ void RegisterInfoProvider(KODI::GUILIB::GUIINFO::IGUIInfoProvider *provider);
+
+ /*! \brief unregister a guiinfo provider
+ \param the guiinfo provider to unregister
+ */
+ void UnregisterInfoProvider(KODI::GUILIB::GUIINFO::IGUIInfoProvider *provider);
+
+ /*! \brief get access to the registered guiinfo providers
+ \return the guiinfo providers
+ */
+ KODI::GUILIB::GUIINFO::CGUIInfoProviders& GetInfoProviders() { return m_infoProviders; }
+
+private:
+ /*! \brief class for holding information on properties
+ */
+ class Property
+ {
+ public:
+ Property(const std::string &property, const std::string &parameters);
+
+ const std::string &param(unsigned int n = 0) const;
+ unsigned int num_params() const;
+
+ std::string name;
+ private:
+ std::vector<std::string> params;
+ };
+
+ /*! \brief Split an info string into it's constituent parts and parameters
+ Format is:
+
+ info1(params1).info2(params2).info3(params3) ...
+
+ where the parameters are an optional comma separated parameter list.
+
+ \param infoString the original string
+ \param info the resulting pairs of info and parameters.
+ */
+ void SplitInfoString(const std::string &infoString, std::vector<Property> &info);
+
+ int TranslateSingleString(const std::string &strCondition);
+ int TranslateListItem(const Property& cat, const Property& prop, int id, bool container);
+ int TranslateMusicPlayerString(const std::string &info) const;
+ int TranslateVideoPlayerString(const std::string& info) const;
+ int TranslatePlayerString(const std::string& info) const;
+ static TIME_FORMAT TranslateTimeFormat(const std::string &format);
+
+ std::string GetMultiInfoLabel(const KODI::GUILIB::GUIINFO::CGUIInfo &info, int contextWindow, std::string *fallback = nullptr) const;
+ bool GetMultiInfoInt(int &value, const KODI::GUILIB::GUIINFO::CGUIInfo &info, int contextWindow, const CGUIListItem *item) const;
+ bool GetMultiInfoBool(const KODI::GUILIB::GUIINFO::CGUIInfo &info, int contextWindow, const CGUIListItem *item);
+
+ std::string GetMultiInfoItemLabel(const CFileItem *item, int contextWindow, const KODI::GUILIB::GUIINFO::CGUIInfo &info, std::string *fallback = nullptr) const;
+ std::string GetMultiInfoItemImage(const CFileItem *item, int contextWindow, const KODI::GUILIB::GUIINFO::CGUIInfo &info, std::string *fallback = nullptr) const;
+
+ std::string GetSkinVariableString(int info,
+ int contextWindow,
+ bool preferImage = false,
+ const CGUIListItem* item = nullptr) const;
+
+ int AddMultiInfo(const KODI::GUILIB::GUIINFO::CGUIInfo &info);
+
+ int ResolveMultiInfo(int info) const;
+ bool IsListItemInfo(int info) const;
+
+ void SetCurrentSongTag(const MUSIC_INFO::CMusicInfoTag &tag);
+ void SetCurrentVideoTag(const CVideoInfoTag &tag);
+
+ // Vector of multiple information mapped to a single integer lookup
+ std::vector<KODI::GUILIB::GUIINFO::CGUIInfo> m_multiInfo;
+
+ // Current playing stuff
+ CFileItem* m_currentFile;
+
+ typedef std::set<INFO::InfoPtr, bool(*)(const INFO::InfoPtr&, const INFO::InfoPtr&)> INFOBOOLTYPE;
+ INFOBOOLTYPE m_bools;
+ unsigned int m_refreshCounter = 0;
+ std::vector<INFO::CSkinVariableString> m_skinVariableStrings;
+
+ CCriticalSection m_critInfo;
+
+ KODI::GUILIB::GUIINFO::CGUIInfoProviders m_infoProviders;
+};
diff --git a/xbmc/GUILargeTextureManager.cpp b/xbmc/GUILargeTextureManager.cpp
new file mode 100644
index 0000000..80b762b
--- /dev/null
+++ b/xbmc/GUILargeTextureManager.cpp
@@ -0,0 +1,247 @@
+/*
+ * 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 "GUILargeTextureManager.h"
+
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "commons/ilog.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/Texture.h"
+#include "utils/JobManager.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include <cassert>
+#include <chrono>
+#include <exception>
+#include <mutex>
+
+CImageLoader::CImageLoader(const std::string& path, const bool useCache)
+ : m_path(path), m_texture(nullptr)
+{
+ m_use_cache = useCache;
+}
+
+CImageLoader::~CImageLoader() = default;
+
+bool CImageLoader::DoWork()
+{
+ bool needsChecking = false;
+ std::string loadPath;
+
+ std::string texturePath = CServiceBroker::GetGUI()->GetTextureManager().GetTexturePath(m_path);
+ if (texturePath.empty())
+ return false;
+
+ if (m_use_cache)
+ loadPath = CServiceBroker::GetTextureCache()->CheckCachedImage(texturePath, needsChecking);
+ else
+ loadPath = texturePath;
+
+ if (!loadPath.empty())
+ {
+ // direct route - load the image
+ auto start = std::chrono::steady_clock::now();
+ m_texture =
+ CTexture::LoadFromFile(loadPath, CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(),
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight());
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ if (duration.count() > 100)
+ CLog::Log(LOGDEBUG, "{} - took {} ms to load {}", __FUNCTION__, duration.count(), loadPath);
+
+ if (m_texture)
+ {
+ if (needsChecking)
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(texturePath);
+
+ return true;
+ }
+
+ // Fallthrough on failure:
+ CLog::Log(LOGERROR, "{} - Direct texture file loading failed for {}", __FUNCTION__, loadPath);
+ }
+
+ if (!m_use_cache)
+ return false; // We're done
+
+ // not in our texture cache or it failed to load from it, so try and load directly and then cache the result
+ CServiceBroker::GetTextureCache()->CacheImage(texturePath, &m_texture);
+ return (m_texture != NULL);
+}
+
+CGUILargeTextureManager::CLargeTexture::CLargeTexture(const std::string &path):
+ m_path(path)
+{
+ m_refCount = 1;
+ m_timeToDelete = 0;
+}
+
+CGUILargeTextureManager::CLargeTexture::~CLargeTexture()
+{
+ assert(m_refCount == 0);
+ m_texture.Free();
+}
+
+void CGUILargeTextureManager::CLargeTexture::AddRef()
+{
+ m_refCount++;
+}
+
+bool CGUILargeTextureManager::CLargeTexture::DecrRef(bool deleteImmediately)
+{
+ assert(m_refCount);
+ m_refCount--;
+ if (m_refCount == 0)
+ {
+ if (deleteImmediately)
+ delete this;
+ else
+ m_timeToDelete = CTimeUtils::GetFrameTime() + TIME_TO_DELETE;
+ return true;
+ }
+ return false;
+}
+
+bool CGUILargeTextureManager::CLargeTexture::DeleteIfRequired(bool deleteImmediately)
+{
+ if (m_refCount == 0 && (deleteImmediately || m_timeToDelete < CTimeUtils::GetFrameTime()))
+ {
+ delete this;
+ return true;
+ }
+ return false;
+}
+
+void CGUILargeTextureManager::CLargeTexture::SetTexture(std::unique_ptr<CTexture> texture)
+{
+ assert(!m_texture.size());
+ if (texture)
+ {
+ const auto width = texture->GetWidth();
+ const auto height = texture->GetHeight();
+ m_texture.Set(std::move(texture), width, height);
+ }
+}
+
+CGUILargeTextureManager::CGUILargeTextureManager() = default;
+
+CGUILargeTextureManager::~CGUILargeTextureManager() = default;
+
+void CGUILargeTextureManager::CleanupUnusedImages(bool immediately)
+{
+ std::unique_lock<CCriticalSection> lock(m_listSection);
+ // check for items to remove from allocated list, and remove
+ listIterator it = m_allocated.begin();
+ while (it != m_allocated.end())
+ {
+ CLargeTexture *image = *it;
+ if (image->DeleteIfRequired(immediately))
+ it = m_allocated.erase(it);
+ else
+ ++it;
+ }
+}
+
+// if available, increment reference count, and return the image.
+// else, add to the queue list if appropriate.
+bool CGUILargeTextureManager::GetImage(const std::string &path, CTextureArray &texture, bool firstRequest, const bool useCache)
+{
+ std::unique_lock<CCriticalSection> lock(m_listSection);
+ for (listIterator it = m_allocated.begin(); it != m_allocated.end(); ++it)
+ {
+ CLargeTexture *image = *it;
+ if (image->GetPath() == path)
+ {
+ if (firstRequest)
+ image->AddRef();
+ texture = image->GetTexture();
+ return texture.size() > 0;
+ }
+ }
+
+ if (firstRequest)
+ QueueImage(path, useCache);
+
+ return true;
+}
+
+void CGUILargeTextureManager::ReleaseImage(const std::string &path, bool immediately)
+{
+ std::unique_lock<CCriticalSection> lock(m_listSection);
+ for (listIterator it = m_allocated.begin(); it != m_allocated.end(); ++it)
+ {
+ CLargeTexture *image = *it;
+ if (image->GetPath() == path)
+ {
+ if (image->DecrRef(immediately) && immediately)
+ m_allocated.erase(it);
+ return;
+ }
+ }
+ for (queueIterator it = m_queued.begin(); it != m_queued.end(); ++it)
+ {
+ unsigned int id = it->first;
+ CLargeTexture *image = it->second;
+ if (image->GetPath() == path && image->DecrRef(true))
+ {
+ // cancel this job
+ CServiceBroker::GetJobManager()->CancelJob(id);
+ m_queued.erase(it);
+ return;
+ }
+ }
+}
+
+// queue the image, and start the background loader if necessary
+void CGUILargeTextureManager::QueueImage(const std::string &path, bool useCache)
+{
+ if (path.empty())
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_listSection);
+ for (queueIterator it = m_queued.begin(); it != m_queued.end(); ++it)
+ {
+ CLargeTexture *image = it->second;
+ if (image->GetPath() == path)
+ {
+ image->AddRef();
+ return; // already queued
+ }
+ }
+
+ // queue the item
+ CLargeTexture *image = new CLargeTexture(path);
+ unsigned int jobID = CServiceBroker::GetJobManager()->AddJob(new CImageLoader(path, useCache),
+ this, CJob::PRIORITY_NORMAL);
+ m_queued.emplace_back(jobID, image);
+}
+
+void CGUILargeTextureManager::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ // see if we still have this job id
+ std::unique_lock<CCriticalSection> lock(m_listSection);
+ for (queueIterator it = m_queued.begin(); it != m_queued.end(); ++it)
+ {
+ if (it->first == jobID)
+ { // found our job
+ CImageLoader *loader = static_cast<CImageLoader*>(job);
+ CLargeTexture *image = it->second;
+ image->SetTexture(std::move(loader->m_texture));
+ loader->m_texture = NULL; // we want to keep the texture, and jobs are auto-deleted.
+ m_queued.erase(it);
+ m_allocated.push_back(image);
+ return;
+ }
+ }
+}
diff --git a/xbmc/GUILargeTextureManager.h b/xbmc/GUILargeTextureManager.h
new file mode 100644
index 0000000..ecb3b5e
--- /dev/null
+++ b/xbmc/GUILargeTextureManager.h
@@ -0,0 +1,143 @@
+/*
+ * 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 "guilib/TextureManager.h"
+#include "threads/CriticalSection.h"
+#include "utils/Job.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CTexture;
+
+/*!
+ \ingroup textures,jobs
+ \brief Image loader job class
+
+ Used by the CGUILargeTextureManager to perform asynchronous loading of textures.
+
+ \sa CGUILargeTextureManager and CJob
+ */
+class CImageLoader : public CJob
+{
+public:
+ CImageLoader(const std::string &path, const bool useCache);
+ ~CImageLoader() override;
+
+ /*!
+ \brief Work function that loads in a particular image.
+ */
+ bool DoWork() override;
+
+ bool m_use_cache; ///< Whether or not to use any caching with this image
+ std::string m_path; ///< path of image to load
+ std::unique_ptr<CTexture> m_texture; ///< Texture object to load the image into \sa CTexture.
+};
+
+/*!
+ \ingroup textures
+ \brief Background texture loading manager
+
+ Used to load textures for the user interface asynchronously, allowing fluid framerates
+ while background loading textures.
+
+ \sa IJobCallback, CGUITexture
+ */
+class CGUILargeTextureManager : public IJobCallback
+{
+public:
+ CGUILargeTextureManager();
+ ~CGUILargeTextureManager() override;
+
+ /*!
+ \brief Callback from CImageLoader on completion of a loaded image
+
+ Transfers texture information from the loading job to our allocated texture list.
+
+ \sa CImageLoader, IJobCallback
+ */
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ /*!
+ \brief Request a texture to be loaded in the background.
+
+ Loaded textures are reference counted, hence this call may immediately return with the texture
+ object filled if the texture has been previously loaded, else will return with an empty texture
+ object if it is being loaded.
+
+ \param path path of the image to load.
+ \param texture texture object to hold the resulting texture
+ \param orientation orientation of resulting texture
+ \param firstRequest true if this is the first time we are requesting this texture
+ \return true if the image exists, else false.
+ \sa CGUITextureArray and CGUITexture
+ */
+ bool GetImage(const std::string &path, CTextureArray &texture, bool firstRequest, bool useCache = true);
+
+ /*!
+ \brief Request a texture to be unloaded.
+
+ When textures are finished with, this function should be called. This decrements the texture's
+ reference count, and schedules it to be unloaded once the reference count reaches zero. If the
+ texture is still queued for loading, or is in the process of loading, the image load is cancelled.
+
+ \param path path of the image to release.
+ \param immediately if set true the image is immediately unloaded once its reference count reaches zero
+ rather than being unloaded after a delay.
+ */
+ void ReleaseImage(const std::string &path, bool immediately = false);
+
+ /*!
+ \brief Cleanup images that are no longer in use.
+
+ Loaded textures are reference counted, and upon reaching reference count 0 through ReleaseImage()
+ they are flagged as unused with the current time. After a delay they may be unloaded, hence
+ CleanupUnusedImages() should be called periodically to ensure this occurs.
+
+ \param immediately set to true to cleanup images regardless of whether the delay has passed
+ */
+ void CleanupUnusedImages(bool immediately = false);
+
+private:
+ class CLargeTexture
+ {
+ public:
+ explicit CLargeTexture(const std::string &path);
+ virtual ~CLargeTexture();
+
+ void AddRef();
+ bool DecrRef(bool deleteImmediately);
+ bool DeleteIfRequired(bool deleteImmediately = false);
+ void SetTexture(std::unique_ptr<CTexture> texture);
+
+ const std::string& GetPath() const { return m_path; }
+ const CTextureArray& GetTexture() const { return m_texture; }
+
+ private:
+ static const unsigned int TIME_TO_DELETE = 2000;
+
+ unsigned int m_refCount;
+ std::string m_path;
+ CTextureArray m_texture;
+ unsigned int m_timeToDelete;
+ };
+
+ void QueueImage(const std::string &path, bool useCache = true);
+
+ std::vector< std::pair<unsigned int, CLargeTexture *> > m_queued;
+ std::vector<CLargeTexture *> m_allocated;
+ typedef std::vector<CLargeTexture *>::iterator listIterator;
+ typedef std::vector< std::pair<unsigned int, CLargeTexture *> >::iterator queueIterator;
+
+ CCriticalSection m_listSection;
+};
+
diff --git a/xbmc/GUIPassword.cpp b/xbmc/GUIPassword.cpp
new file mode 100644
index 0000000..eaaa042
--- /dev/null
+++ b/xbmc/GUIPassword.cpp
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2005-2020 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 "GUIPassword.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dialogs/GUIDialogGamepad.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "favourites/FavouritesService.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "media/MediaLockState.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "profiles/ProfileManager.h"
+#include "profiles/dialogs/GUIDialogLockSettings.h"
+#include "profiles/dialogs/GUIDialogProfileSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "view/ViewStateSettings.h"
+
+#include <utility>
+
+using namespace KODI::MESSAGING;
+
+CGUIPassword::CGUIPassword(void)
+{
+ iMasterLockRetriesLeft = -1;
+ bMasterUser = false;
+}
+CGUIPassword::~CGUIPassword(void) = default;
+
+template<typename T>
+bool CGUIPassword::IsItemUnlocked(T pItem,
+ const std::string& strType,
+ const std::string& strLabel,
+ const std::string& strHeading)
+{
+ const std::shared_ptr<CProfileManager> profileManager =
+ CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ if (profileManager->GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE)
+ return true;
+
+ while (pItem->m_iHasLock > LOCK_STATE_LOCK_BUT_UNLOCKED)
+ {
+ const std::string strLockCode = pItem->m_strLockCode;
+ int iResult = 0; // init to user succeeded state, doing this to optimize switch statement below
+ if (!g_passwordManager.bMasterUser) // Check if we are the MasterUser!
+ {
+ if (0 != CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MASTERLOCK_MAXRETRIES) &&
+ pItem->m_iBadPwdCount >= CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MASTERLOCK_MAXRETRIES))
+ {
+ // user previously exhausted all retries, show access denied error
+ HELPERS::ShowOKDialogText(CVariant{12345}, CVariant{12346});
+ return false;
+ }
+ // show the appropriate lock dialog
+ iResult = VerifyPassword(pItem->m_iLockMode, strLockCode, strHeading);
+ }
+ switch (iResult)
+ {
+ case -1:
+ { // user canceled out
+ return false;
+ break;
+ }
+ case 0:
+ {
+ // password entry succeeded
+ pItem->m_iBadPwdCount = 0;
+ pItem->m_iHasLock = LOCK_STATE_LOCK_BUT_UNLOCKED;
+ g_passwordManager.LockSource(strType, strLabel, false);
+ CMediaSourceSettings::GetInstance().UpdateSource(strType, strLabel, "badpwdcount",
+ std::to_string(pItem->m_iBadPwdCount));
+ CMediaSourceSettings::GetInstance().Save();
+
+ // a mediasource has been unlocked successfully
+ // => refresh favourites due to possible visibility changes
+ CServiceBroker::GetFavouritesService().RefreshFavourites();
+ break;
+ }
+ case 1:
+ {
+ // password entry failed
+ if (0 != CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MASTERLOCK_MAXRETRIES))
+ pItem->m_iBadPwdCount++;
+ CMediaSourceSettings::GetInstance().UpdateSource(strType, strLabel, "badpwdcount",
+ std::to_string(pItem->m_iBadPwdCount));
+ CMediaSourceSettings::GetInstance().Save();
+ break;
+ }
+ default:
+ {
+ // this should never happen, but if it does, do nothing
+ return false;
+ break;
+ }
+ }
+ }
+ return true;
+}
+
+bool CGUIPassword::IsItemUnlocked(CFileItem* pItem, const std::string& strType)
+{
+ const std::string strLabel = pItem->GetLabel();
+ std::string strHeading;
+ if (pItem->m_bIsFolder)
+ strHeading = g_localizeStrings.Get(12325); // "Locked! Enter code..."
+ else
+ strHeading = g_localizeStrings.Get(12348); // "Item locked"
+
+ return IsItemUnlocked<CFileItem*>(pItem, strType, strLabel, strHeading);
+}
+
+bool CGUIPassword::IsItemUnlocked(CMediaSource* pItem, const std::string& strType)
+{
+ const std::string strLabel = pItem->strName;
+ const std::string& strHeading = g_localizeStrings.Get(12325); // "Locked! Enter code..."
+
+ return IsItemUnlocked<CMediaSource*>(pItem, strType, strLabel, strHeading);
+}
+
+bool CGUIPassword::CheckStartUpLock()
+{
+ // prompt user for mastercode if the mastercode was set b4 or by xml
+ int iVerifyPasswordResult = -1;
+
+ const std::string& strHeader = g_localizeStrings.Get(20075); // "Enter master lock code"
+
+ if (iMasterLockRetriesLeft == -1)
+ iMasterLockRetriesLeft = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MASTERLOCK_MAXRETRIES);
+
+ if (g_passwordManager.iMasterLockRetriesLeft == 0)
+ g_passwordManager.iMasterLockRetriesLeft = 1;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::string strPassword = profileManager->GetMasterProfile().getLockCode();
+
+ if (profileManager->GetMasterProfile().getLockMode() == 0)
+ iVerifyPasswordResult = 0;
+ else
+ {
+ for (int i=1; i <= g_passwordManager.iMasterLockRetriesLeft; i++)
+ {
+ iVerifyPasswordResult = VerifyPassword(profileManager->GetMasterProfile().getLockMode(), strPassword, strHeader);
+ if (iVerifyPasswordResult != 0 )
+ {
+ std::string strLabel1;
+ strLabel1 = g_localizeStrings.Get(12343); // "retries left"
+ int iLeft = g_passwordManager.iMasterLockRetriesLeft-i;
+ std::string strLabel = StringUtils::Format("{} {}", iLeft, strLabel1);
+
+ // PopUp OK and Display: MasterLock mode has changed but no new Mastercode has been set!
+ HELPERS::ShowOKDialogLines(CVariant{12360}, CVariant{12367}, CVariant{strLabel}, CVariant{""});
+ }
+ else
+ i=g_passwordManager.iMasterLockRetriesLeft;
+ }
+ }
+
+ if (iVerifyPasswordResult == 0)
+ {
+ g_passwordManager.iMasterLockRetriesLeft = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MASTERLOCK_MAXRETRIES);
+ return true; // OK The MasterCode Accepted! XBMC Can Run!
+ }
+ else
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SHUTDOWN); // Turn off the box
+ return false;
+ }
+}
+
+bool CGUIPassword::SetMasterLockMode(bool bDetails)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ CProfile* profile = profileManager->GetProfile(0);
+ if (profile)
+ {
+ CProfile::CLock locks = profile->GetLocks();
+ // prompt user for master lock
+ if (CGUIDialogLockSettings::ShowAndGetLock(locks, 12360, true, bDetails))
+ {
+ profile->SetLocks(locks);
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIPassword::IsProfileLockUnlocked(int iProfile)
+{
+ bool bDummy;
+ return IsProfileLockUnlocked(iProfile,bDummy,true);
+}
+
+bool CGUIPassword::IsProfileLockUnlocked(int iProfile, bool& bCanceled, bool prompt)
+{
+ if (g_passwordManager.bMasterUser)
+ return true;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ int iProfileToCheck = iProfile;
+ if (iProfile == -1)
+ iProfileToCheck = profileManager->GetCurrentProfileIndex();
+
+ if (iProfileToCheck == 0)
+ return IsMasterLockUnlocked(prompt,bCanceled);
+ else
+ {
+ const CProfile *profile = profileManager->GetProfile(iProfileToCheck);
+ if (!profile)
+ return false;
+
+ if (!prompt)
+ return (profile->getLockMode() == LOCK_MODE_EVERYONE);
+
+ if (profile->getDate().empty() &&
+ (profileManager->GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE ||
+ profile->getLockMode() == LOCK_MODE_EVERYONE))
+ {
+ // user hasn't set a password and this is the first time they've used this account
+ // so prompt for password/settings
+ if (CGUIDialogProfileSettings::ShowForProfile(iProfileToCheck, true))
+ return true;
+ }
+ else
+ {
+ if (profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE)
+ // prompt user for profile lock code
+ return CheckLock(profile->getLockMode(),profile->getLockCode(),20095,bCanceled);
+ }
+ }
+
+ return true;
+}
+
+bool CGUIPassword::IsMasterLockUnlocked(bool bPromptUser)
+{
+ bool bDummy;
+ return IsMasterLockUnlocked(bPromptUser,bDummy);
+}
+
+bool CGUIPassword::IsMasterLockUnlocked(bool bPromptUser, bool& bCanceled)
+{
+ bCanceled = false;
+
+ if (iMasterLockRetriesLeft == -1)
+ iMasterLockRetriesLeft = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MASTERLOCK_MAXRETRIES);
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if ((LOCK_MODE_EVERYONE < profileManager->GetMasterProfile().getLockMode() && !bMasterUser) && !bPromptUser)
+ {
+ // not unlocked, but calling code doesn't want to prompt user
+ return false;
+ }
+
+ if (g_passwordManager.bMasterUser || profileManager->GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE)
+ return true;
+
+ if (iMasterLockRetriesLeft == 0)
+ {
+ UpdateMasterLockRetryCount(false);
+ return false;
+ }
+
+ // no, unlock since we are allowed to prompt
+ const std::string& strHeading = g_localizeStrings.Get(20075);
+ std::string strPassword = profileManager->GetMasterProfile().getLockCode();
+
+ int iVerifyPasswordResult = VerifyPassword(profileManager->GetMasterProfile().getLockMode(), strPassword, strHeading);
+ if (1 == iVerifyPasswordResult)
+ UpdateMasterLockRetryCount(false);
+
+ if (0 != iVerifyPasswordResult)
+ {
+ bCanceled = true;
+ return false;
+ }
+
+ // user successfully entered mastercode
+ UpdateMasterLockRetryCount(true);
+ return true;
+}
+
+void CGUIPassword::UpdateMasterLockRetryCount(bool bResetCount)
+{
+ // \brief Updates Master Lock status.
+ // \param bResetCount masterlock retry counter is zeroed if true, or incremented and displays an Access Denied dialog if false.
+ if (!bResetCount)
+ {
+ // Bad mastercode entered
+ if (0 < CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MASTERLOCK_MAXRETRIES))
+ {
+ // We're keeping track of how many bad passwords are entered
+ if (1 < g_passwordManager.iMasterLockRetriesLeft)
+ {
+ // user still has at least one retry after decrementing
+ g_passwordManager.iMasterLockRetriesLeft--;
+ }
+ else
+ {
+ // user has run out of retry attempts
+ g_passwordManager.iMasterLockRetriesLeft = 0;
+ // Tell the user they ran out of retry attempts
+ HELPERS::ShowOKDialogText(CVariant{12345}, CVariant{12346});
+ return;
+ }
+ }
+ std::string dlgLine1 = "";
+ if (0 < g_passwordManager.iMasterLockRetriesLeft)
+ dlgLine1 = StringUtils::Format("{} {}", g_passwordManager.iMasterLockRetriesLeft,
+ g_localizeStrings.Get(12343)); // "retries left"
+ // prompt user for master lock code
+ HELPERS::ShowOKDialogLines(CVariant{20075}, CVariant{12345}, CVariant{std::move(dlgLine1)}, CVariant{0});
+ }
+ else
+ g_passwordManager.iMasterLockRetriesLeft = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MASTERLOCK_MAXRETRIES); // user entered correct mastercode, reset retries to max allowed
+}
+
+bool CGUIPassword::CheckLock(LockType btnType, const std::string& strPassword, int iHeading)
+{
+ bool bDummy;
+ return CheckLock(btnType,strPassword,iHeading,bDummy);
+}
+
+bool CGUIPassword::CheckLock(LockType btnType, const std::string& strPassword, int iHeading, bool& bCanceled)
+{
+ bCanceled = false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (btnType == LOCK_MODE_EVERYONE ||
+ strPassword == "-" ||
+ profileManager->GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE ||
+ g_passwordManager.bMasterUser)
+ {
+ return true;
+ }
+
+ const std::string& strHeading = g_localizeStrings.Get(iHeading);
+ int iVerifyPasswordResult = VerifyPassword(btnType, strPassword, strHeading);
+
+ if (iVerifyPasswordResult == -1)
+ bCanceled = true;
+
+ return (iVerifyPasswordResult==0);
+}
+
+bool CGUIPassword::CheckSettingLevelLock(const SettingLevel& level, bool enforce /*=false*/)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ LOCK_LEVEL::SETTINGS_LOCK lockLevel = profileManager->GetCurrentProfile().settingsLockLevel();
+
+ if (lockLevel == LOCK_LEVEL::NONE)
+ return true;
+
+ //check if we are already in settings and in an level that needs unlocking
+ int windowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ if ((int)lockLevel-1 <= (short)CViewStateSettings::GetInstance().GetSettingLevel() &&
+ (windowID == WINDOW_SETTINGS_MENU ||
+ (windowID >= WINDOW_SCREEN_CALIBRATION &&
+ windowID <= WINDOW_SETTINGS_MYPVR)))
+ return true; //Already unlocked
+
+ else if (lockLevel == LOCK_LEVEL::ALL)
+ return IsMasterLockUnlocked(true);
+ else if ((int)lockLevel-1 <= (short)level)
+ {
+ if (enforce)
+ return IsMasterLockUnlocked(true);
+ else if (!IsMasterLockUnlocked(false))
+ {
+ //Current Setting level is higher than our permission... so lower the viewing level
+ SettingLevel newLevel = (SettingLevel)(short)(lockLevel-2);
+ CViewStateSettings::GetInstance().SetSettingLevel(newLevel);
+ }
+ }
+ return true;
+
+}
+
+bool IsSettingsWindow(int iWindowID)
+{
+ return (iWindowID >= WINDOW_SCREEN_CALIBRATION && iWindowID <= WINDOW_SETTINGS_MYPVR)
+ || iWindowID == WINDOW_SKIN_SETTINGS;
+}
+
+bool CGUIPassword::CheckMenuLock(int iWindowID)
+{
+ bool bCheckPW = false;
+ int iSwitch = iWindowID;
+
+ // check if a settings subcategory was called from other than settings window
+ if (IsSettingsWindow(iWindowID))
+ {
+ int iCWindowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ if (iCWindowID != WINDOW_SETTINGS_MENU && !IsSettingsWindow(iCWindowID))
+ iSwitch = WINDOW_SETTINGS_MENU;
+ }
+
+ if (iWindowID == WINDOW_MUSIC_NAV)
+ {
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_HOME)
+ iSwitch = WINDOW_MUSIC_NAV;
+ }
+
+ if (iWindowID == WINDOW_VIDEO_NAV)
+ {
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_HOME)
+ iSwitch = WINDOW_VIDEO_NAV;
+ }
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ switch (iSwitch)
+ {
+ case WINDOW_SETTINGS_MENU: // Settings
+ return CheckSettingLevelLock(CViewStateSettings::GetInstance().GetSettingLevel());
+ break;
+ case WINDOW_ADDON_BROWSER: // Addons
+ bCheckPW = profileManager->GetCurrentProfile().addonmanagerLocked();
+ break;
+ case WINDOW_FILES: // Files
+ bCheckPW = profileManager->GetCurrentProfile().filesLocked();
+ break;
+ case WINDOW_PROGRAMS: // Programs
+ bCheckPW = profileManager->GetCurrentProfile().programsLocked();
+ break;
+ case WINDOW_MUSIC_NAV: // Music
+ bCheckPW = profileManager->GetCurrentProfile().musicLocked();
+ if (!bCheckPW && !m_strMediaSourcePath.empty()) // check mediasource by path
+ return g_passwordManager.IsMediaPathUnlocked(profileManager, "music");
+ break;
+ case WINDOW_VIDEO_NAV: // Video
+ bCheckPW = profileManager->GetCurrentProfile().videoLocked();
+ if (!bCheckPW && !m_strMediaSourcePath.empty()) // check mediasource by path
+ return g_passwordManager.IsMediaPathUnlocked(profileManager, "video");
+ break;
+ case WINDOW_PICTURES: // Pictures
+ bCheckPW = profileManager->GetCurrentProfile().picturesLocked();
+ break;
+ case WINDOW_GAMES: // Games
+ bCheckPW = profileManager->GetCurrentProfile().gamesLocked();
+ break;
+ case WINDOW_SETTINGS_PROFILES:
+ bCheckPW = true;
+ break;
+ default:
+ bCheckPW = false;
+ break;
+ }
+ if (bCheckPW)
+ return IsMasterLockUnlocked(true); //Now let's check the PW if we need!
+ else
+ return true;
+}
+
+bool CGUIPassword::LockSource(const std::string& strType, const std::string& strName, bool bState)
+{
+ VECSOURCES* pShares = CMediaSourceSettings::GetInstance().GetSources(strType);
+ bool bResult = false;
+ for (IVECSOURCES it=pShares->begin();it != pShares->end();++it)
+ {
+ if (it->strName == strName)
+ {
+ if (it->m_iHasLock > LOCK_STATE_NO_LOCK)
+ {
+ it->m_iHasLock = bState ? LOCK_STATE_LOCKED : LOCK_STATE_LOCK_BUT_UNLOCKED;
+ bResult = true;
+ }
+ break;
+ }
+ }
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_UPDATE_SOURCES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+
+ return bResult;
+}
+
+void CGUIPassword::LockSources(bool lock)
+{
+ // lock or unlock all sources (those with locks)
+ const char* strTypes[] = {"programs", "music", "video", "pictures", "files", "games"};
+ for (const char* const strType : strTypes)
+ {
+ VECSOURCES *shares = CMediaSourceSettings::GetInstance().GetSources(strType);
+ for (IVECSOURCES it=shares->begin();it != shares->end();++it)
+ if (it->m_iLockMode != LOCK_MODE_EVERYONE)
+ it->m_iHasLock = lock ? LOCK_STATE_LOCKED : LOCK_STATE_LOCK_BUT_UNLOCKED;
+ }
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_UPDATE_SOURCES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CGUIPassword::RemoveSourceLocks()
+{
+ // remove lock from all sources
+ const char* strTypes[] = {"programs", "music", "video", "pictures", "files", "games"};
+ for (const char* const strType : strTypes)
+ {
+ VECSOURCES *shares = CMediaSourceSettings::GetInstance().GetSources(strType);
+ for (IVECSOURCES it=shares->begin();it != shares->end();++it)
+ if (it->m_iLockMode != LOCK_MODE_EVERYONE) // remove old info
+ {
+ it->m_iHasLock = LOCK_STATE_NO_LOCK;
+ it->m_iLockMode = LOCK_MODE_EVERYONE;
+
+ // remove locks from xml
+ CMediaSourceSettings::GetInstance().UpdateSource(strType, it->strName, "lockmode", "0");
+ }
+ }
+ CMediaSourceSettings::GetInstance().Save();
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0, GUI_MSG_UPDATE_SOURCES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+bool CGUIPassword::IsDatabasePathUnlocked(const std::string& strPath, VECSOURCES& vecSources)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (g_passwordManager.bMasterUser || profileManager->GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE)
+ return true;
+
+ // try to find the best matching source
+ bool bName = false;
+ int iIndex = CUtil::GetMatchingSource(strPath, vecSources, bName);
+
+ if (iIndex > -1 && iIndex < static_cast<int>(vecSources.size()))
+ if (vecSources[iIndex].m_iHasLock < LOCK_STATE_LOCKED)
+ return true;
+
+ return false;
+}
+
+bool CGUIPassword::IsMediaPathUnlocked(const std::shared_ptr<CProfileManager>& profileManager,
+ const std::string& strType) const
+{
+ if (!StringUtils::StartsWithNoCase(m_strMediaSourcePath, "root") &&
+ !StringUtils::StartsWithNoCase(m_strMediaSourcePath, "library://"))
+ {
+ if (!g_passwordManager.bMasterUser &&
+ profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE)
+ {
+ VECSOURCES& vecSources = *CMediaSourceSettings::GetInstance().GetSources(strType);
+ bool bName = false;
+ int iIndex = CUtil::GetMatchingSource(m_strMediaSourcePath, vecSources, bName);
+ if (iIndex > -1 && iIndex < static_cast<int>(vecSources.size()))
+ {
+ return g_passwordManager.IsItemUnlocked(&vecSources[iIndex], strType);
+ }
+ }
+ }
+
+ return true;
+}
+
+bool CGUIPassword::IsMediaFileUnlocked(const std::string& type, const std::string& file) const
+{
+ std::vector<CMediaSource>* vecSources = CMediaSourceSettings::GetInstance().GetSources(type);
+
+ if (!vecSources)
+ {
+ CLog::Log(LOGERROR,
+ "{}: CMediaSourceSettings::GetInstance().GetSources(\"{}\") returned nullptr.",
+ __func__, type);
+ return true;
+ }
+
+ // try to find the best matching source for this file
+
+ bool isSourceName{false};
+ const std::string fileBasePath = URIUtils::GetBasePath(file);
+
+ int iIndex = CUtil::GetMatchingSource(fileBasePath, *vecSources, isSourceName);
+
+ if (iIndex > -1 && iIndex < static_cast<int>(vecSources->size()))
+ return (*vecSources)[iIndex].m_iHasLock < LOCK_STATE_LOCKED;
+
+ return true;
+}
+
+void CGUIPassword::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_MASTERLOCK_LOCKCODE)
+ SetMasterLockMode();
+}
+
+int CGUIPassword::VerifyPassword(LockType btnType, const std::string& strPassword, const std::string& strHeading)
+{
+ int iVerifyPasswordResult;
+ switch (btnType)
+ {
+ case LOCK_MODE_NUMERIC:
+ iVerifyPasswordResult = CGUIDialogNumeric::ShowAndVerifyPassword(const_cast<std::string&>(strPassword), strHeading, 0);
+ break;
+ case LOCK_MODE_GAMEPAD:
+ iVerifyPasswordResult = CGUIDialogGamepad::ShowAndVerifyPassword(const_cast<std::string&>(strPassword), strHeading, 0);
+ break;
+ case LOCK_MODE_QWERTY:
+ iVerifyPasswordResult = CGUIKeyboardFactory::ShowAndVerifyPassword(const_cast<std::string&>(strPassword), strHeading, 0);
+ break;
+ default: // must not be supported, treat as unlocked
+ iVerifyPasswordResult = 0;
+ break;
+ }
+
+ return iVerifyPasswordResult;
+}
+
diff --git a/xbmc/GUIPassword.h b/xbmc/GUIPassword.h
new file mode 100644
index 0000000..d278248
--- /dev/null
+++ b/xbmc/GUIPassword.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2005-2020 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 "LockType.h"
+#include "settings/lib/ISettingCallback.h"
+#include "settings/lib/SettingLevel.h"
+
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CMediaSource;
+class CProfileManager;
+
+typedef std::vector<CMediaSource> VECSOURCES;
+
+class CGUIPassword : public ISettingCallback
+{
+public:
+ CGUIPassword(void);
+ ~CGUIPassword(void) override;
+ template<typename T>
+ bool IsItemUnlocked(T pItem,
+ const std::string& strType,
+ const std::string& strLabel,
+ const std::string& strHeading);
+ /*! \brief Tests if the user is allowed to access the share folder
+ \param pItem The share folder item to access
+ \param strType The type of share being accessed, e.g. "music", "video", etc. See CSettings::UpdateSources()
+ \return If access is granted, returns \e true
+ */
+ bool IsItemUnlocked(CFileItem* pItem, const std::string &strType);
+ /*! \brief Tests if the user is allowed to access the Mediasource
+ \param pItem The share folder item to access
+ \param strType The type of share being accessed, e.g. "music", "video", etc. See CSettings::UpdateSources()
+ \return If access is granted, returns \e true
+ */
+ bool IsItemUnlocked(CMediaSource* pItem, const std::string &strType);
+ bool CheckLock(LockType btnType, const std::string& strPassword, int iHeading);
+ bool CheckLock(LockType btnType, const std::string& strPassword, int iHeading, bool& bCanceled);
+ bool IsProfileLockUnlocked(int iProfile=-1);
+ bool IsProfileLockUnlocked(int iProfile, bool& bCanceled, bool prompt = true);
+ bool IsMasterLockUnlocked(bool bPromptUser);
+ bool IsMasterLockUnlocked(bool bPromptUser, bool& bCanceled);
+
+ void UpdateMasterLockRetryCount(bool bResetCount);
+ bool CheckStartUpLock();
+ /*! \brief Checks if the current profile is allowed to access the given settings level
+ \param level - The level to check
+ \param enforce - If false, CheckSettingLevelLock is allowed to lower the current settings level
+ to a level we're allowed to access
+ \returns true if we're allowed to access the settings
+ */
+ bool CheckSettingLevelLock(const SettingLevel& level, bool enforce = false);
+ bool CheckMenuLock(int iWindowID);
+ bool SetMasterLockMode(bool bDetails=true);
+ bool LockSource(const std::string& strType, const std::string& strName, bool bState);
+ void LockSources(bool lock);
+ void RemoveSourceLocks();
+ bool IsDatabasePathUnlocked(const std::string& strPath, VECSOURCES& vecSources);
+
+ /*! \brief Helper function to test if a matching mediasource is currently unlocked
+ for a given media file
+ \note this function only returns the lock state. it does not provide unlock functionality
+ \param type The type of share being accessed, e.g. "music", "video", etc.
+ \param file The file to check lock state for
+ \return If access is granted, returns \e true
+ */
+ bool IsMediaFileUnlocked(const std::string& type, const std::string& file) const;
+
+ void SetMediaSourcePath(const std::string& strMediaSourcePath)
+ {
+ m_strMediaSourcePath = strMediaSourcePath;
+ }
+
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ bool bMasterUser;
+ int iMasterLockRetriesLeft;
+
+private:
+ /*! \brief Helper function to test if the user is allowed to access the path
+ by looking up the matching Mediasource. Used internally by CheckMenuLock.
+ \param profileManager instance passed by ref. see CGUIPassword::CheckMenuLock
+ \param strType The type of share being accessed, e.g. "music", "video", etc.
+ \return If access is granted, returns \e true
+ */
+ bool IsMediaPathUnlocked(const std::shared_ptr<CProfileManager>& profileManager,
+ const std::string& strType) const;
+
+ std::string m_strMediaSourcePath;
+ int VerifyPassword(LockType btnType, const std::string& strPassword, const std::string& strHeading);
+};
+
+extern CGUIPassword g_passwordManager;
diff --git a/xbmc/GUIUserMessages.h b/xbmc/GUIUserMessages.h
new file mode 100644
index 0000000..1e20380
--- /dev/null
+++ b/xbmc/GUIUserMessages.h
@@ -0,0 +1,145 @@
+/*
+ * 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
+
+// GUI messages outside GuiLib
+//
+
+#include "guilib/GUIMessage.h"
+
+// Source related messages
+constexpr const int GUI_MSG_REMOVED_MEDIA = GUI_MSG_USER + 1;
+constexpr const int GUI_MSG_UPDATE_SOURCES = GUI_MSG_USER + 2;
+
+// General playlist items changed
+constexpr const int GUI_MSG_PLAYLIST_CHANGED = GUI_MSG_USER + 3;
+
+// Start Slideshow in my pictures lpVoid = std::string
+// Param lpVoid: std::string* that points to the Directory
+// to start the slideshow in.
+constexpr const int GUI_MSG_START_SLIDESHOW = GUI_MSG_USER + 4;
+
+constexpr const int GUI_MSG_PLAYBACK_STARTED = GUI_MSG_USER + 5;
+constexpr const int GUI_MSG_PLAYBACK_ENDED = GUI_MSG_USER + 6;
+
+// Playback stopped by user
+constexpr const int GUI_MSG_PLAYBACK_STOPPED = GUI_MSG_USER + 7;
+
+// Message is send by the playlistplayer when it starts a playlist
+// Parameter:
+// dwParam1 = Current Playlist, can be PLAYLIST::TYPE_MUSIC or PLAYLIST::TYPE_VIDEO
+// dwParam2 = Item started in the playlist
+// lpVoid = Playlistitem started playing
+constexpr const int GUI_MSG_PLAYLISTPLAYER_STARTED = GUI_MSG_USER + 8;
+
+// Message is send by playlistplayer when next/previous item is started
+// Parameter:
+// dwParam1 = Current Playlist, can be PLAYLIST::TYPE_MUSIC or PLAYLIST::TYPE_VIDEO
+// dwParam2 = LOWORD Position of the current playlistitem
+// HIWORD Position of the previous playlistitem
+// lpVoid = Current Playlistitem
+constexpr const int GUI_MSG_PLAYLISTPLAYER_CHANGED = GUI_MSG_USER + 9;
+
+// Message is send by the playlistplayer when the last item to play ended
+// Parameter:
+// dwParam1 = Current Playlist, can be PLAYLIST::TYPE_MUSIC or PLAYLIST::TYPE_VIDEO
+// dwParam2 = Playlistitem played when stopping
+constexpr const int GUI_MSG_PLAYLISTPLAYER_STOPPED = GUI_MSG_USER + 10;
+
+constexpr const int GUI_MSG_LOAD_SKIN = GUI_MSG_USER + 11;
+
+// Message is send by the dialog scan music
+// Parameter:
+// StringParam = Directory last scanned
+constexpr const int GUI_MSG_DIRECTORY_SCANNED = GUI_MSG_USER + 12;
+
+constexpr const int GUI_MSG_SCAN_FINISHED = GUI_MSG_USER + 13;
+
+// Player has requested the next item for caching purposes (PAPlayer)
+constexpr const int GUI_MSG_QUEUE_NEXT_ITEM = GUI_MSG_USER + 16;
+
+// Playback request for the trailer of a given item
+constexpr const int GUI_MSG_PLAY_TRAILER = GUI_MSG_USER + 17;
+
+// Visualisation messages when loading/unloading
+constexpr const int GUI_MSG_VISUALISATION_UNLOADING = GUI_MSG_USER + 117; // sent by vis
+constexpr const int GUI_MSG_VISUALISATION_LOADED = GUI_MSG_USER + 118; // sent by vis
+constexpr const int GUI_MSG_GET_VISUALISATION = GUI_MSG_USER + 119; // request to vis for the visualisation object
+constexpr const int GUI_MSG_VISUALISATION_ACTION = GUI_MSG_USER + 120; // request the vis perform an action
+constexpr const int GUI_MSG_VISUALISATION_RELOAD = GUI_MSG_USER + 121; // request the vis to reload
+
+constexpr const int GUI_MSG_VIDEO_MENU_STARTED = GUI_MSG_USER + 21; // sent by VideoPlayer on entry to the menu
+
+// Message is sent by built-in function to alert the playlist window
+// that the user has initiated Random playback
+// dwParam1 = Current Playlist (PLAYLIST::TYPE_MUSIC or PLAYLIST::TYPE_VIDEO)
+// dwParam2 = 0 or 1 (Enabled or Disabled)
+constexpr const int GUI_MSG_PLAYLISTPLAYER_RANDOM = GUI_MSG_USER + 22;
+
+// Message is sent by built-in function to alert the playlist window
+// that the user has initiated Repeat playback
+// dwParam1 = Current Playlist (PLAYLIST::TYPE_MUSIC or PLAYLIST::TYPE_VIDEO)
+// dwParam2 = 0 or 1 or 2 (Off, Repeat All, Repeat One)
+constexpr const int GUI_MSG_PLAYLISTPLAYER_REPEAT = GUI_MSG_USER + 23;
+
+// Message is sent by the background info loader when it is finished with fetching a weather location.
+constexpr const int GUI_MSG_WEATHER_FETCHED = GUI_MSG_USER + 24;
+
+// Message is sent to the screensaver window to tell that it should check the lock
+constexpr const int GUI_MSG_CHECK_LOCK = GUI_MSG_USER + 25;
+
+// Message is sent to media windows to force a refresh
+constexpr const int GUI_MSG_UPDATE = GUI_MSG_USER + 26;
+
+// Message sent by filtering dialog to request a new filter be applied
+constexpr const int GUI_MSG_FILTER_ITEMS = GUI_MSG_USER + 27;
+
+// Message sent by search dialog to request a new search be applied
+constexpr const int GUI_MSG_SEARCH_UPDATE = GUI_MSG_USER + 28;
+
+// Message sent to tell the GUI to update a single item
+constexpr const int GUI_MSG_UPDATE_ITEM = GUI_MSG_USER + 29;
+
+// Flags for GUI_MSG_UPDATE_ITEM message
+constexpr int GUI_MSG_FLAG_UPDATE_LIST = 0x00000001;
+constexpr int GUI_MSG_FLAG_FORCE_UPDATE = 0x00000002;
+
+// Message sent to tell the GUI to change view mode
+constexpr const int GUI_MSG_CHANGE_VIEW_MODE = GUI_MSG_USER + 30;
+
+// Message sent to tell the GUI to change sort method/direction
+constexpr const int GUI_MSG_CHANGE_SORT_METHOD = GUI_MSG_USER + 31;
+constexpr const int GUI_MSG_CHANGE_SORT_DIRECTION = GUI_MSG_USER + 32;
+
+// Sent from filesystem if a path is known to have changed
+constexpr const int GUI_MSG_UPDATE_PATH = GUI_MSG_USER + 33;
+
+// Sent to tell window to initiate a search dialog
+constexpr const int GUI_MSG_SEARCH = GUI_MSG_USER + 34;
+
+// Sent to the AddonSetting dialogs from addons if they updated a setting
+constexpr const int GUI_MSG_SETTING_UPDATED = GUI_MSG_USER + 35;
+
+// Message sent to CGUIWindowSlideshow to show picture
+constexpr const int GUI_MSG_SHOW_PICTURE = GUI_MSG_USER + 36;
+
+// Sent to CGUIWindowEventLog
+constexpr const int GUI_MSG_EVENT_ADDED = GUI_MSG_USER + 39;
+constexpr const int GUI_MSG_EVENT_REMOVED = GUI_MSG_USER + 40;
+
+// Send to RDS RadioText handlers to inform about changed data
+constexpr const int GUI_MSG_UPDATE_RADIOTEXT = GUI_MSG_USER + 41;
+
+constexpr const int GUI_MSG_PLAYBACK_ERROR = GUI_MSG_USER + 42;
+constexpr const int GUI_MSG_PLAYBACK_AVCHANGE = GUI_MSG_USER + 43;
+constexpr const int GUI_MSG_PLAYBACK_AVSTARTED = GUI_MSG_USER + 44;
+
+// Sent to notify system sleep/wake
+constexpr const int GUI_MSG_SYSTEM_SLEEP = GUI_MSG_USER + 45;
+constexpr const int GUI_MSG_SYSTEM_WAKE = GUI_MSG_USER + 46;
diff --git a/xbmc/HDRStatus.h b/xbmc/HDRStatus.h
new file mode 100644
index 0000000..8fe5aae
--- /dev/null
+++ b/xbmc/HDRStatus.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) 2005-2020 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
+
+enum class HDR_STATUS
+{
+ HDR_TOGGLE_FAILED = -1,
+ HDR_UNSUPPORTED = 0,
+ HDR_OFF = 1,
+ HDR_ON = 2
+};
diff --git a/xbmc/IFileItemListModifier.h b/xbmc/IFileItemListModifier.h
new file mode 100644
index 0000000..00dd805
--- /dev/null
+++ b/xbmc/IFileItemListModifier.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013-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 CFileItemList;
+
+class IFileItemListModifier
+{
+public:
+ IFileItemListModifier() = default;
+ virtual ~IFileItemListModifier() = default;
+
+ virtual bool CanModify(const CFileItemList &items) const = 0;
+ virtual bool Modify(CFileItemList &items) const = 0;
+};
diff --git a/xbmc/IProgressCallback.h b/xbmc/IProgressCallback.h
new file mode 100644
index 0000000..7de790f
--- /dev/null
+++ b/xbmc/IProgressCallback.h
@@ -0,0 +1,18 @@
+/*
+ * 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
+
+class IProgressCallback
+{
+public:
+ virtual ~IProgressCallback() = default;
+ virtual void SetProgressMax(int max)=0;
+ virtual void SetProgressAdvance(int nSteps=1)=0;
+ virtual bool Abort()=0;
+};
diff --git a/xbmc/InfoScanner.cpp b/xbmc/InfoScanner.cpp
new file mode 100644
index 0000000..4db687b
--- /dev/null
+++ b/xbmc/InfoScanner.cpp
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016-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 "InfoScanner.h"
+
+#include "URL.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+bool CInfoScanner::HasNoMedia(const std::string &strDirectory) const
+{
+ std::string noMediaFile = URIUtils::AddFileToFolder(strDirectory, ".nomedia");
+
+ if (!URIUtils::IsPlugin(strDirectory) && CFileUtils::Exists(noMediaFile))
+ {
+ CLog::Log(LOGWARNING,
+ "Skipping item '{}' with '.nomedia' file in parent directory, it won't be added to "
+ "the library.",
+ CURL::GetRedacted(strDirectory));
+ return true;
+ }
+
+ return false;
+}
diff --git a/xbmc/InfoScanner.h b/xbmc/InfoScanner.h
new file mode 100644
index 0000000..208f914
--- /dev/null
+++ b/xbmc/InfoScanner.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016-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 <set>
+#include <string>
+#include <vector>
+
+class CGUIDialogProgressBarHandle;
+
+class CInfoScanner
+{
+public:
+ /*!
+ \brief Return values from the information lookup functions.
+ */
+ enum INFO_RET
+ {
+ INFO_CANCELLED,
+ INFO_ERROR,
+ INFO_NOT_NEEDED,
+ INFO_HAVE_ALREADY,
+ INFO_NOT_FOUND,
+ INFO_ADDED
+ };
+
+ /*
+ \brief Type of information returned from tag readers.
+ */
+
+ enum INFO_TYPE
+ {
+ NO_NFO = 0, //!< No info found
+ FULL_NFO = 1, //!< Full info specified
+ URL_NFO = 2, //!< A URL to grab info from was found
+ OVERRIDE_NFO = 3, //!< Override info was found
+ COMBINED_NFO = 4, //!< A URL to grab info from + override info was found
+ ERROR_NFO = 5, //!< Error processing info
+ TITLE_NFO = 6 //!< At least Title was read (and optionally the Year)
+ };
+
+ //! \brief Empty destructor.
+ virtual ~CInfoScanner() = default;
+
+ virtual bool DoScan(const std::string& strDirectory) = 0;
+
+ /*! \brief Check if the folder is excluded from scanning process
+ \param strDirectory Directory to scan
+ \return true if there is a .nomedia file
+ */
+ bool HasNoMedia(const std::string& strDirectory) const;
+
+ //! \brief Set whether or not to show a progress dialog.
+ void ShowDialog(bool show) { m_showDialog = show; }
+
+ //! \brief Returns whether or not a scan is in progress.
+ bool IsScanning() const { return m_bRunning; }
+
+protected:
+ //! \brief Protected constructor to only allow subclass instances.
+ CInfoScanner() = default;
+
+ std::set<std::string> m_pathsToScan; //!< Set of paths to scan
+ bool m_showDialog = false; //!< Whether or not to show progress bar dialog
+ CGUIDialogProgressBarHandle* m_handle = nullptr; //!< Progress bar handle
+ bool m_bRunning = false; //!< Whether or not scanner is running
+ bool m_bCanInterrupt = false; //!< Whether or not scanner is currently interruptible
+ bool m_bClean = false; //!< Whether or not to perform cleaning during scanning
+};
diff --git a/xbmc/LangInfo.cpp b/xbmc/LangInfo.cpp
new file mode 100644
index 0000000..db85aa5
--- /dev/null
+++ b/xbmc/LangInfo.cpp
@@ -0,0 +1,1528 @@
+/*
+ * 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 "LangInfo.h"
+
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/LanguageResource.h"
+#include "addons/RepositoryUpdater.h"
+#include "addons/addoninfo/AddonType.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "utils/CharsetConverter.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+#include "weather/WeatherManager.h"
+
+#include <algorithm>
+#include <stdexcept>
+
+using namespace PVR;
+
+static std::string shortDateFormats[] = {
+ // short date formats using "/"
+ "DD/MM/YYYY",
+ "MM/DD/YYYY",
+ "YYYY/MM/DD",
+ "D/M/YYYY",
+ // short date formats using "-"
+ "DD-MM-YYYY",
+ "MM-DD-YYYY",
+ "YYYY-MM-DD",
+ "YYYY-M-D",
+ // short date formats using "."
+ "DD.MM.YYYY",
+ "DD.M.YYYY",
+ "D.M.YYYY",
+ "D. M. YYYY",
+ "YYYY.MM.DD"
+};
+
+static std::string longDateFormats[] = {
+ "DDDD, D MMMM YYYY",
+ "DDDD, DD MMMM YYYY",
+ "DDDD, D. MMMM YYYY",
+ "DDDD, DD. MMMM YYYY",
+ "DDDD, MMMM D, YYYY",
+ "DDDD, MMMM DD, YYYY",
+ "DDDD D MMMM YYYY",
+ "DDDD DD MMMM YYYY",
+ "DDDD D. MMMM YYYY",
+ "DDDD DD. MMMM YYYY",
+ "D. MMMM YYYY",
+ "DD. MMMM YYYY",
+ "D. MMMM. YYYY",
+ "DD. MMMM. YYYY",
+ "YYYY. MMMM. D"
+};
+
+#define TIME_FORMAT_MM_SS ":mm:ss"
+#define TIME_FORMAT_SINGLE_12 "h" TIME_FORMAT_MM_SS
+#define TIME_FORMAT_DOUBLE_12 "hh" TIME_FORMAT_MM_SS
+#define TIME_FORMAT_SINGLE_24 "H" TIME_FORMAT_MM_SS
+#define TIME_FORMAT_DOUBLE_24 "HH" TIME_FORMAT_MM_SS
+
+#define TIME_FORMAT_12HOURS "12hours"
+#define TIME_FORMAT_24HOURS "24hours"
+
+typedef struct TemperatureInfo {
+ CTemperature::Unit unit;
+ std::string name;
+} TemperatureInfo;
+
+static TemperatureInfo temperatureInfo[] = {
+ { CTemperature::UnitFahrenheit, "f" },
+ { CTemperature::UnitKelvin, "k" },
+ { CTemperature::UnitCelsius, "c" },
+ { CTemperature::UnitReaumur, "re" },
+ { CTemperature::UnitRankine, "ra" },
+ { CTemperature::UnitRomer, "ro" },
+ { CTemperature::UnitDelisle, "de" },
+ { CTemperature::UnitNewton, "n" }
+};
+
+#define TEMP_UNIT_STRINGS 20027
+
+typedef struct SpeedInfo {
+ CSpeed::Unit unit;
+ std::string name;
+} SpeedInfo;
+
+static SpeedInfo speedInfo[] = {
+ { CSpeed::UnitKilometresPerHour, "kmh" },
+ { CSpeed::UnitMetresPerMinute, "mpmin" },
+ { CSpeed::UnitMetresPerSecond, "mps" },
+ { CSpeed::UnitFeetPerHour, "fth" },
+ { CSpeed::UnitFeetPerMinute, "ftm" },
+ { CSpeed::UnitFeetPerSecond, "fts" },
+ { CSpeed::UnitMilesPerHour, "mph" },
+ { CSpeed::UnitKnots, "kts" },
+ { CSpeed::UnitBeaufort, "beaufort" },
+ { CSpeed::UnitInchPerSecond, "inchs" },
+ { CSpeed::UnitYardPerSecond, "yards" },
+ { CSpeed::UnitFurlongPerFortnight, "fpf" }
+};
+
+#define SPEED_UNIT_STRINGS 20200
+
+#define SETTING_REGIONAL_DEFAULT "regional"
+
+static std::string ToTimeFormat(bool use24HourClock, bool singleHour, bool meridiem)
+{
+ if (use24HourClock)
+ return singleHour ? TIME_FORMAT_SINGLE_24 : TIME_FORMAT_DOUBLE_24;
+
+ if (!meridiem)
+ return singleHour ? TIME_FORMAT_SINGLE_12 : TIME_FORMAT_DOUBLE_12;
+
+ return StringUtils::Format(g_localizeStrings.Get(12382), ToTimeFormat(false, singleHour, false));
+}
+
+static std::string ToSettingTimeFormat(const CDateTime& time, const std::string& timeFormat)
+{
+ return StringUtils::Format(g_localizeStrings.Get(20036),
+ time.GetAsLocalizedTime(timeFormat, true), timeFormat);
+}
+
+static CTemperature::Unit StringToTemperatureUnit(const std::string& temperatureUnit)
+{
+ std::string unit(temperatureUnit);
+ StringUtils::ToLower(unit);
+
+ for (const TemperatureInfo& info : temperatureInfo)
+ {
+ if (info.name == unit)
+ return info.unit;
+ }
+
+ return CTemperature::UnitCelsius;
+}
+
+static CSpeed::Unit StringToSpeedUnit(const std::string& speedUnit)
+{
+ std::string unit(speedUnit);
+ StringUtils::ToLower(unit);
+
+ for (const SpeedInfo& info : speedInfo)
+ {
+ if (info.name == unit)
+ return info.unit;
+ }
+
+ return CSpeed::UnitKilometresPerHour;
+}
+
+struct SortLanguage
+{
+ bool operator()(const StringSettingOption &left, const StringSettingOption &right) const
+ {
+ std::string strLeft = left.label;
+ std::string strRight = right.label;
+ StringUtils::ToLower(strLeft);
+ StringUtils::ToLower(strRight);
+
+ return strLeft.compare(strRight) < 0;
+ }
+};
+
+CLangInfo::CRegion::CRegion()
+{
+ SetDefaults();
+}
+
+void CLangInfo::CRegion::SetDefaults()
+{
+ m_strName="N/A";
+ m_strLangLocaleName = "English";
+ m_strLangLocaleCodeTwoChar = "en";
+
+ m_strDateFormatShort="DD/MM/YYYY";
+ m_strDateFormatLong="DDDD, D MMMM YYYY";
+ m_strTimeFormat="HH:mm:ss";
+ m_tempUnit = CTemperature::UnitCelsius;
+ m_speedUnit = CSpeed::UnitKilometresPerHour;
+ m_strTimeZone.clear();
+}
+
+void CLangInfo::CRegion::SetTemperatureUnit(const std::string& strUnit)
+{
+ m_tempUnit = StringToTemperatureUnit(strUnit);
+}
+
+void CLangInfo::CRegion::SetSpeedUnit(const std::string& strUnit)
+{
+ m_speedUnit = StringToSpeedUnit(strUnit);
+}
+
+void CLangInfo::CRegion::SetTimeZone(const std::string& strTimeZone)
+{
+ m_strTimeZone = strTimeZone;
+}
+
+void CLangInfo::CRegion::SetGlobalLocale()
+{
+ std::string strLocale;
+ if (m_strRegionLocaleName.length() > 0)
+ {
+#ifdef TARGET_WINDOWS
+ std::string strLang, strRegion;
+ g_LangCodeExpander.ConvertToISO6391(m_strLangLocaleName, strLang);
+ g_LangCodeExpander.ConvertToISO6391(m_strRegionLocaleName, strRegion);
+ strLocale = strLang + "-" + strRegion;
+#else
+ strLocale = m_strLangLocaleName + "_" + m_strRegionLocaleName;
+#endif
+#ifdef TARGET_POSIX
+ strLocale += ".UTF-8";
+#endif
+ }
+ g_langInfo.m_originalLocale = std::locale(std::locale::classic(), new custom_numpunct(m_cDecimalSep, m_cThousandsSep, m_strGrouping));
+
+ CLog::Log(LOGDEBUG, "trying to set locale to {}", strLocale);
+
+ // We need to set the locale to only change the collate. Otherwise,
+ // decimal separator is changed depending of the current language
+ // (ie. "," in French or Dutch instead of "."). This breaks atof() and
+ // others similar functions.
+#if !(defined(TARGET_FREEBSD) || defined(TARGET_DARWIN_OSX) || defined(__UCLIBC__))
+ // on FreeBSD, darwin and uClibc-based systems libstdc++ is compiled with
+ // "generic" locale support
+ std::locale current_locale = std::locale::classic(); // C-Locale
+ try
+ {
+ std::locale lcl = std::locale(strLocale.c_str());
+ strLocale = lcl.name();
+ current_locale = current_locale.combine< std::collate<wchar_t> >(lcl);
+ current_locale = current_locale.combine< std::ctype<wchar_t> >(lcl);
+ current_locale = current_locale.combine< std::time_get<wchar_t> >(lcl);
+ current_locale = current_locale.combine< std::time_put<wchar_t> >(lcl);
+
+ assert(std::use_facet< std::numpunct<char> >(current_locale).decimal_point() == '.');
+
+ } catch(...) {
+ current_locale = std::locale::classic();
+ strLocale = "C";
+ }
+
+ g_langInfo.m_systemLocale = current_locale; //! @todo move to CLangInfo class
+ g_langInfo.m_collationtype = 0;
+ std::locale::global(current_locale);
+#endif
+
+#ifndef TARGET_WINDOWS
+ if (setlocale(LC_COLLATE, strLocale.c_str()) == NULL ||
+ setlocale(LC_CTYPE, strLocale.c_str()) == NULL ||
+ setlocale(LC_TIME, strLocale.c_str()) == NULL)
+ {
+ strLocale = "C";
+ setlocale(LC_COLLATE, strLocale.c_str());
+ setlocale(LC_CTYPE, strLocale.c_str());
+ setlocale(LC_TIME, strLocale.c_str());
+ }
+#else
+ std::wstring strLocaleW;
+ g_charsetConverter.utf8ToW(strLocale, strLocaleW);
+ if (_wsetlocale(LC_COLLATE, strLocaleW.c_str()) == NULL ||
+ _wsetlocale(LC_CTYPE, strLocaleW.c_str()) == NULL ||
+ _wsetlocale(LC_TIME, strLocaleW.c_str()) == NULL)
+ {
+ strLocale = "C";
+ strLocaleW = L"C";
+ _wsetlocale(LC_COLLATE, strLocaleW.c_str());
+ _wsetlocale(LC_CTYPE, strLocaleW.c_str());
+ _wsetlocale(LC_TIME, strLocaleW.c_str());
+ }
+#endif
+
+ g_charsetConverter.resetSystemCharset();
+ CLog::Log(LOGINFO, "global locale set to {}", strLocale);
+
+#ifdef TARGET_ANDROID
+ // Force UTF8 for, e.g., vsnprintf
+ setlocale(LC_ALL, "C.UTF-8");
+#endif
+}
+
+CLangInfo::CLangInfo()
+{
+ SetDefaults();
+ m_shortDateFormat = m_defaultRegion.m_strDateFormatShort;
+ m_longDateFormat = m_defaultRegion.m_strDateFormatLong;
+ m_timeFormat = m_defaultRegion.m_strTimeFormat;
+ m_use24HourClock = DetermineUse24HourClockFromTimeFormat(m_defaultRegion.m_strTimeFormat);
+ m_temperatureUnit = m_defaultRegion.m_tempUnit;
+ m_speedUnit = m_defaultRegion.m_speedUnit;
+ m_collationtype = 0;
+}
+
+CLangInfo::~CLangInfo() = default;
+
+void CLangInfo::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_LOCALE_AUDIOLANGUAGE)
+ SetAudioLanguage(std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+ else if (settingId == CSettings::SETTING_LOCALE_SUBTITLELANGUAGE)
+ SetSubtitleLanguage(std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+ else if (settingId == CSettings::SETTING_LOCALE_LANGUAGE)
+ {
+ if (!SetLanguage(std::static_pointer_cast<const CSettingString>(setting)->GetValue()))
+ {
+ auto langsetting = settings->GetSetting(CSettings::SETTING_LOCALE_LANGUAGE);
+ if (!langsetting)
+ {
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", CSettings::SETTING_LOCALE_LANGUAGE);
+ return;
+ }
+
+ std::static_pointer_cast<CSettingString>(langsetting)->Reset();
+ }
+ }
+ else if (settingId == CSettings::SETTING_LOCALE_COUNTRY)
+ SetCurrentRegion(std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+ else if (settingId == CSettings::SETTING_LOCALE_SHORTDATEFORMAT)
+ SetShortDateFormat(std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+ else if (settingId == CSettings::SETTING_LOCALE_LONGDATEFORMAT)
+ SetLongDateFormat(std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+ else if (settingId == CSettings::SETTING_LOCALE_TIMEFORMAT)
+ SetTimeFormat(std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+ else if (settingId == CSettings::SETTING_LOCALE_USE24HOURCLOCK)
+ {
+ Set24HourClock(std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+
+ // update the time format
+ settings->SetString(CSettings::SETTING_LOCALE_TIMEFORMAT,
+ PrepareTimeFormat(GetTimeFormat(), m_use24HourClock));
+ }
+ else if (settingId == CSettings::SETTING_LOCALE_TEMPERATUREUNIT)
+ SetTemperatureUnit(std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+ else if (settingId == CSettings::SETTING_LOCALE_SPEEDUNIT)
+ SetSpeedUnit(std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+}
+
+void CLangInfo::OnSettingsLoaded()
+{
+ // set the temperature and speed units based on the settings
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ SetShortDateFormat(settings->GetString(CSettings::SETTING_LOCALE_SHORTDATEFORMAT));
+ SetLongDateFormat(settings->GetString(CSettings::SETTING_LOCALE_LONGDATEFORMAT));
+ Set24HourClock(settings->GetString(CSettings::SETTING_LOCALE_USE24HOURCLOCK));
+ SetTimeFormat(settings->GetString(CSettings::SETTING_LOCALE_TIMEFORMAT));
+ SetTemperatureUnit(settings->GetString(CSettings::SETTING_LOCALE_TEMPERATUREUNIT));
+ SetSpeedUnit(settings->GetString(CSettings::SETTING_LOCALE_SPEEDUNIT));
+}
+
+bool CLangInfo::Load(const std::string& strLanguage)
+{
+ SetDefaults();
+
+ std::string strFileName = GetLanguageInfoPath(strLanguage);
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(strFileName))
+ {
+ CLog::Log(LOGERROR, "unable to load {}: {} at line {}", strFileName, xmlDoc.ErrorDesc(),
+ xmlDoc.ErrorRow());
+ return false;
+ }
+
+ // get the matching language addon
+ m_languageAddon = GetLanguageAddon(strLanguage);
+ if (m_languageAddon == NULL)
+ {
+ CLog::Log(LOGERROR, "Unknown language {}", strLanguage);
+ return false;
+ }
+
+ // get some language-specific information from the language addon
+ m_strGuiCharSet = m_languageAddon->GetGuiCharset();
+ m_forceUnicodeFont = m_languageAddon->ForceUnicodeFont();
+ m_strSubtitleCharSet = m_languageAddon->GetSubtitleCharset();
+ m_strDVDMenuLanguage = m_languageAddon->GetDvdMenuLanguage();
+ m_strDVDAudioLanguage = m_languageAddon->GetDvdAudioLanguage();
+ m_strDVDSubtitleLanguage = m_languageAddon->GetDvdSubtitleLanguage();
+ m_sortTokens = m_languageAddon->GetSortTokens();
+
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (pRootElement->ValueStr() != "language")
+ {
+ CLog::Log(LOGERROR, "{} Doesn't contain <language>", strFileName);
+ return false;
+ }
+
+ if (pRootElement->Attribute("locale"))
+ m_defaultRegion.m_strLangLocaleName = pRootElement->Attribute("locale");
+
+#ifdef TARGET_WINDOWS
+ // Windows need 3 chars isolang code
+ if (m_defaultRegion.m_strLangLocaleName.length() == 2)
+ {
+ if (!g_LangCodeExpander.ConvertISO6391ToISO6392B(m_defaultRegion.m_strLangLocaleName, m_defaultRegion.m_strLangLocaleName, true))
+ m_defaultRegion.m_strLangLocaleName = "";
+ }
+
+ if (!g_LangCodeExpander.ConvertWindowsLanguageCodeToISO6392B(m_defaultRegion.m_strLangLocaleName, m_languageCodeGeneral))
+ m_languageCodeGeneral = "";
+#else
+ if (m_defaultRegion.m_strLangLocaleName.length() != 3)
+ {
+ if (!g_LangCodeExpander.ConvertToISO6392B(m_defaultRegion.m_strLangLocaleName, m_languageCodeGeneral))
+ m_languageCodeGeneral = "";
+ }
+ else
+ m_languageCodeGeneral = m_defaultRegion.m_strLangLocaleName;
+#endif
+
+ std::string tmp;
+ if (g_LangCodeExpander.ConvertToISO6391(m_defaultRegion.m_strLangLocaleName, tmp))
+ m_defaultRegion.m_strLangLocaleCodeTwoChar = tmp;
+
+ const TiXmlNode *pRegions = pRootElement->FirstChild("regions");
+ if (pRegions && !pRegions->NoChildren())
+ {
+ const TiXmlElement *pRegion=pRegions->FirstChildElement("region");
+ while (pRegion)
+ {
+ CRegion region(m_defaultRegion);
+ region.m_strName = XMLUtils::GetAttribute(pRegion, "name");
+ if (region.m_strName.empty())
+ region.m_strName=g_localizeStrings.Get(10005); // Not available
+
+ if (pRegion->Attribute("locale"))
+ region.m_strRegionLocaleName = pRegion->Attribute("locale");
+
+#ifdef TARGET_WINDOWS
+ // Windows need 3 chars regions code
+ if (region.m_strRegionLocaleName.length() == 2)
+ {
+ if (!g_LangCodeExpander.ConvertISO31661Alpha2ToISO31661Alpha3(region.m_strRegionLocaleName, region.m_strRegionLocaleName))
+ region.m_strRegionLocaleName = "";
+ }
+#endif
+
+ const TiXmlNode *pDateLong=pRegion->FirstChild("datelong");
+ if (pDateLong && !pDateLong->NoChildren())
+ region.m_strDateFormatLong=pDateLong->FirstChild()->ValueStr();
+
+ const TiXmlNode *pDateShort=pRegion->FirstChild("dateshort");
+ if (pDateShort && !pDateShort->NoChildren())
+ region.m_strDateFormatShort=pDateShort->FirstChild()->ValueStr();
+
+ const TiXmlElement *pTime=pRegion->FirstChildElement("time");
+ if (pTime && !pTime->NoChildren())
+ {
+ region.m_strTimeFormat=pTime->FirstChild()->Value();
+ region.m_strMeridiemSymbols[MeridiemSymbolAM] = XMLUtils::GetAttribute(pTime, "symbolAM");
+ region.m_strMeridiemSymbols[MeridiemSymbolPM] = XMLUtils::GetAttribute(pTime, "symbolPM");
+ }
+
+ const TiXmlNode *pTempUnit=pRegion->FirstChild("tempunit");
+ if (pTempUnit && !pTempUnit->NoChildren())
+ region.SetTemperatureUnit(pTempUnit->FirstChild()->ValueStr());
+
+ const TiXmlNode *pSpeedUnit=pRegion->FirstChild("speedunit");
+ if (pSpeedUnit && !pSpeedUnit->NoChildren())
+ region.SetSpeedUnit(pSpeedUnit->FirstChild()->ValueStr());
+
+ const TiXmlNode *pTimeZone=pRegion->FirstChild("timezone");
+ if (pTimeZone && !pTimeZone->NoChildren())
+ region.SetTimeZone(pTimeZone->FirstChild()->ValueStr());
+
+ const TiXmlElement *pThousandsSep = pRegion->FirstChildElement("thousandsseparator");
+ if (pThousandsSep)
+ {
+ if (!pThousandsSep->NoChildren())
+ {
+ region.m_cThousandsSep = pThousandsSep->FirstChild()->Value()[0];
+ if (pThousandsSep->Attribute("groupingformat"))
+ region.m_strGrouping = StringUtils::BinaryStringToString(pThousandsSep->Attribute("groupingformat"));
+ else
+ region.m_strGrouping = "\3";
+ }
+ }
+ else
+ {
+ region.m_cThousandsSep = ',';
+ region.m_strGrouping = "\3";
+ }
+
+ const TiXmlElement *pDecimalSep = pRegion->FirstChildElement("decimalseparator");
+ if (pDecimalSep)
+ {
+ if (!pDecimalSep->NoChildren())
+ region.m_cDecimalSep = pDecimalSep->FirstChild()->Value()[0];
+ }
+ else
+ region.m_cDecimalSep = '.';
+
+ m_regions.insert(PAIR_REGIONS(region.m_strName, region));
+
+ pRegion=pRegion->NextSiblingElement("region");
+ }
+
+ const std::string& strName = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_COUNTRY);
+ SetCurrentRegion(strName);
+ }
+ g_charsetConverter.reinitCharsetsFromSettings();
+
+ return true;
+}
+
+std::string CLangInfo::GetLanguagePath(const std::string &language)
+{
+ if (language.empty())
+ return "";
+
+ std::string addonId = ADDON::CLanguageResource::GetAddonId(language);
+
+ std::string path = URIUtils::AddFileToFolder(GetLanguagePath(), addonId);
+ URIUtils::AddSlashAtEnd(path);
+
+ return path;
+}
+
+std::string CLangInfo::GetLanguageInfoPath(const std::string &language)
+{
+ if (language.empty())
+ return "";
+
+ return URIUtils::AddFileToFolder(GetLanguagePath(language), "langinfo.xml");
+}
+
+bool CLangInfo::UseLocaleCollation()
+{
+ if (m_collationtype == 0)
+ {
+ // Determine collation to use. When using MySQL/MariaDB or a platform that does not support
+ // locale language collation then use accent folding internal equivalent of utf8_general_ci
+ m_collationtype = 1;
+ if (!StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
+ "mysql") &&
+ !StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseVideo.type,
+ "mysql") &&
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_useLocaleCollation)
+ {
+ // Check that locale collation facet is implemented on the platform
+ const std::collate<wchar_t>& coll = std::use_facet<std::collate<wchar_t>>(m_systemLocale);
+ wchar_t lc = L'z';
+ wchar_t rc = 0x00E2; // Latin small letter a with circumflex
+ int comp_result = coll.compare(&lc, &lc + 1, &rc, &rc + 1);
+ if (comp_result > 0)
+ // Latin small letter a with circumflex put before z - collation works
+ m_collationtype = 2;
+ }
+ }
+ return m_collationtype == 2;
+}
+
+void CLangInfo::LoadTokens(const TiXmlNode* pTokens, std::set<std::string>& vecTokens)
+{
+ if (pTokens && !pTokens->NoChildren())
+ {
+ const TiXmlElement *pToken = pTokens->FirstChildElement("token");
+ while (pToken)
+ {
+ std::string strSep= " ._";
+ if (pToken->Attribute("separators"))
+ strSep = pToken->Attribute("separators");
+ if (pToken->FirstChild() && pToken->FirstChild()->Value())
+ {
+ if (strSep.empty())
+ vecTokens.insert(pToken->FirstChild()->ValueStr());
+ else
+ for (unsigned int i=0;i<strSep.size();++i)
+ vecTokens.insert(pToken->FirstChild()->ValueStr() + strSep[i]);
+ }
+ pToken = pToken->NextSiblingElement();
+ }
+ }
+}
+
+void CLangInfo::SetDefaults()
+{
+ m_regions.clear();
+
+ //Reset default region
+ m_defaultRegion.SetDefaults();
+
+ // Set the default region, we may be unable to load langinfo.xml
+ m_currentRegion = &m_defaultRegion;
+
+ m_systemLocale = std::locale::classic();
+
+ m_forceUnicodeFont = false;
+ m_strGuiCharSet = "CP1252";
+ m_strSubtitleCharSet = "CP1252";
+ m_strDVDMenuLanguage = "en";
+ m_strDVDAudioLanguage = "en";
+ m_strDVDSubtitleLanguage = "en";
+ m_sortTokens.clear();
+
+ m_languageCodeGeneral = "eng";
+}
+
+std::string CLangInfo::GetGuiCharSet() const
+{
+ std::shared_ptr<CSettingString> charsetSetting = std::static_pointer_cast<CSettingString>(CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(CSettings::SETTING_LOCALE_CHARSET));
+ if (charsetSetting == NULL || charsetSetting->IsDefault())
+ return m_strGuiCharSet;
+
+ return charsetSetting->GetValue();
+}
+
+std::string CLangInfo::GetSubtitleCharSet() const
+{
+ std::shared_ptr<CSettingString> charsetSetting = std::static_pointer_cast<CSettingString>(CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(CSettings::SETTING_SUBTITLES_CHARSET));
+ if (charsetSetting->IsDefault())
+ return m_strSubtitleCharSet;
+
+ return charsetSetting->GetValue();
+}
+
+void CLangInfo::GetAddonsLanguageCodes(std::map<std::string, std::string>& languages)
+{
+ ADDON::VECADDONS addons;
+ CServiceBroker::GetAddonMgr().GetAddons(addons, ADDON::AddonType::RESOURCE_LANGUAGE);
+ for (const auto& addon : addons)
+ {
+ const LanguageResourcePtr langAddon =
+ std::dynamic_pointer_cast<ADDON::CLanguageResource>(addon);
+ std::string langCode{langAddon->GetLocale().ToShortStringLC()};
+ StringUtils::Replace(langCode, '_', '-');
+ languages.emplace(langCode, addon->Name());
+ }
+}
+
+LanguageResourcePtr CLangInfo::GetLanguageAddon(const std::string& locale /* = "" */) const
+{
+ if (locale.empty() ||
+ (m_languageAddon != NULL && (locale.compare(m_languageAddon->ID()) == 0 || m_languageAddon->GetLocale().Equals(locale))))
+ return m_languageAddon;
+
+ std::string addonId = ADDON::CLanguageResource::GetAddonId(locale);
+ if (addonId.empty())
+ addonId = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_LANGUAGE);
+
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(addonId, addon, ADDON::AddonType::RESOURCE_LANGUAGE,
+ ADDON::OnlyEnabled::CHOICE_YES) &&
+ addon != NULL)
+ return std::dynamic_pointer_cast<ADDON::CLanguageResource>(addon);
+
+ return NULL;
+}
+
+std::string CLangInfo::ConvertEnglishNameToAddonLocale(const std::string& langName)
+{
+ ADDON::VECADDONS addons;
+ CServiceBroker::GetAddonMgr().GetAddons(addons, ADDON::AddonType::RESOURCE_LANGUAGE);
+ for (const auto& addon : addons)
+ {
+ if (StringUtils::CompareNoCase(addon->Name(), langName) == 0)
+ {
+ const LanguageResourcePtr langAddon =
+ std::dynamic_pointer_cast<ADDON::CLanguageResource>(addon);
+ std::string locale = langAddon->GetLocale().ToShortStringLC();
+ StringUtils::Replace(locale, '_', '-');
+ return locale;
+ }
+ }
+ return "";
+}
+
+std::string CLangInfo::GetEnglishLanguageName(const std::string& locale /* = "" */) const
+{
+ LanguageResourcePtr addon = GetLanguageAddon(locale);
+ if (addon == NULL)
+ return "";
+
+ return addon->Name();
+}
+
+bool CLangInfo::SetLanguage(std::string language /* = "" */, bool reloadServices /* = true */)
+{
+ if (language.empty())
+ language = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_LANGUAGE);
+
+ auto& addonMgr = CServiceBroker::GetAddonMgr();
+ ADDON::AddonPtr addon;
+
+ // Find the chosen language add-on if it's enabled
+ if (!addonMgr.GetAddon(language, addon, ADDON::AddonType::RESOURCE_LANGUAGE,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ if (!addonMgr.IsAddonInstalled(language) ||
+ (addonMgr.IsAddonDisabled(language) && !addonMgr.EnableAddon(language)))
+ {
+ CLog::Log(LOGWARNING,
+ "CLangInfo::{}: could not find or enable language add-on '{}', loading default...",
+ __func__, language);
+ language = std::static_pointer_cast<const CSettingString>(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(
+ CSettings::SETTING_LOCALE_LANGUAGE))
+ ->GetDefault();
+
+ if (!addonMgr.GetAddon(language, addon, ADDON::AddonType::RESOURCE_LANGUAGE,
+ ADDON::OnlyEnabled::CHOICE_NO))
+ {
+ CLog::Log(LOGFATAL, "CLangInfo::{}: could not find default language add-on '{}'", __func__,
+ language);
+ return false;
+ }
+ }
+ }
+
+ CLog::Log(LOGINFO, "CLangInfo: loading {} language information...", language);
+ if (!Load(language))
+ {
+ CLog::LogF(LOGFATAL, "CLangInfo: failed to load {} language information", language);
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CLangInfo: loading {} language strings...", language);
+ if (!g_localizeStrings.Load(GetLanguagePath(), language))
+ {
+ CLog::LogF(LOGFATAL, "CLangInfo: failed to load {} language strings", language);
+ return false;
+ }
+
+ ADDON::VECADDONS addons;
+ if (CServiceBroker::GetAddonMgr().GetInstalledAddons(addons))
+ {
+ const std::string locale = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_LANGUAGE);
+ for (const auto& addon : addons)
+ {
+ const std::string path = URIUtils::AddFileToFolder(addon->Path(), "resources", "language/");
+ g_localizeStrings.LoadAddonStrings(path, locale, addon->ID());
+ }
+ }
+
+ if (reloadServices)
+ {
+ // also tell our weather and skin to reload as these are localized
+ CServiceBroker::GetWeatherManager().Refresh();
+ CServiceBroker::GetPVRManager().LocalizationChanged();
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ "ReloadSkin");
+ }
+
+ return true;
+}
+
+// three char language code (not win32 specific)
+const std::string& CLangInfo::GetAudioLanguage() const
+{
+ if (!m_audioLanguage.empty())
+ return m_audioLanguage;
+
+ return m_languageCodeGeneral;
+}
+
+void CLangInfo::SetAudioLanguage(const std::string& language)
+{
+ if (language.empty()
+ || StringUtils::EqualsNoCase(language, "default")
+ || StringUtils::EqualsNoCase(language, "original")
+ || StringUtils::EqualsNoCase(language, "mediadefault")
+ || !g_LangCodeExpander.ConvertToISO6392B(language, m_audioLanguage))
+ m_audioLanguage.clear();
+}
+
+// three char language code (not win32 specific)
+const std::string& CLangInfo::GetSubtitleLanguage() const
+{
+ if (!m_subtitleLanguage.empty())
+ return m_subtitleLanguage;
+
+ return m_languageCodeGeneral;
+}
+
+void CLangInfo::SetSubtitleLanguage(const std::string& language)
+{
+ if (language.empty()
+ || StringUtils::EqualsNoCase(language, "default")
+ || StringUtils::EqualsNoCase(language, "original")
+ || !g_LangCodeExpander.ConvertToISO6392B(language, m_subtitleLanguage))
+ m_subtitleLanguage.clear();
+}
+
+// two character codes as defined in ISO639
+const std::string CLangInfo::GetDVDMenuLanguage() const
+{
+ std::string code;
+ if (!g_LangCodeExpander.ConvertToISO6391(m_currentRegion->m_strLangLocaleName, code))
+ code = m_strDVDMenuLanguage;
+
+ return code;
+}
+
+// two character codes as defined in ISO639
+const std::string CLangInfo::GetDVDAudioLanguage() const
+{
+ std::string code;
+ if (!g_LangCodeExpander.ConvertToISO6391(m_audioLanguage, code))
+ code = m_strDVDAudioLanguage;
+
+ return code;
+}
+
+// two character codes as defined in ISO639
+const std::string CLangInfo::GetDVDSubtitleLanguage() const
+{
+ std::string code;
+ if (!g_LangCodeExpander.ConvertToISO6391(m_subtitleLanguage, code))
+ code = m_strDVDSubtitleLanguage;
+
+ return code;
+}
+
+const CLocale& CLangInfo::GetLocale() const
+{
+ LanguageResourcePtr language = GetLanguageAddon();
+ if (language != NULL)
+ return language->GetLocale();
+
+ return CLocale::Empty;
+}
+
+const std::string& CLangInfo::GetRegionLocale() const
+{
+ return m_currentRegion->m_strRegionLocaleName;
+}
+
+const std::locale& CLangInfo::GetOriginalLocale() const
+{
+ return m_originalLocale;
+}
+
+// Returns the format string for the date of the current language
+const std::string& CLangInfo::GetDateFormat(bool bLongDate /* = false */) const
+{
+ if (bLongDate)
+ return GetLongDateFormat();
+
+ return GetShortDateFormat();
+}
+
+void CLangInfo::SetDateFormat(const std::string& dateFormat, bool bLongDate /* = false */)
+{
+ if (bLongDate)
+ SetLongDateFormat(dateFormat);
+ else
+ SetShortDateFormat(dateFormat);
+}
+
+const std::string& CLangInfo::GetShortDateFormat() const
+{
+ return m_shortDateFormat;
+}
+
+void CLangInfo::SetShortDateFormat(const std::string& shortDateFormat)
+{
+ std::string newShortDateFormat = shortDateFormat;
+ if (shortDateFormat == SETTING_REGIONAL_DEFAULT)
+ newShortDateFormat = m_currentRegion->m_strDateFormatShort;
+
+ m_shortDateFormat = newShortDateFormat;
+}
+
+const std::string& CLangInfo::GetLongDateFormat() const
+{
+ return m_longDateFormat;
+}
+
+void CLangInfo::SetLongDateFormat(const std::string& longDateFormat)
+{
+ std::string newLongDateFormat = longDateFormat;
+ if (longDateFormat == SETTING_REGIONAL_DEFAULT)
+ newLongDateFormat = m_currentRegion->m_strDateFormatShort;
+
+ m_longDateFormat = newLongDateFormat;
+}
+
+// Returns the format string for the time of the current language
+const std::string& CLangInfo::GetTimeFormat() const
+{
+ return m_timeFormat;
+}
+
+void CLangInfo::SetTimeFormat(const std::string& timeFormat)
+{
+ std::string newTimeFormat = timeFormat;
+ if (timeFormat == SETTING_REGIONAL_DEFAULT)
+ newTimeFormat = m_currentRegion->m_strTimeFormat;
+
+ m_timeFormat = PrepareTimeFormat(newTimeFormat, m_use24HourClock);
+}
+
+bool CLangInfo::Use24HourClock() const
+{
+ return m_use24HourClock;
+}
+
+void CLangInfo::Set24HourClock(bool use24HourClock)
+{
+ m_use24HourClock = use24HourClock;
+}
+
+void CLangInfo::Set24HourClock(const std::string& str24HourClock)
+{
+ bool use24HourClock = false;
+ if (str24HourClock == TIME_FORMAT_12HOURS)
+ use24HourClock = false;
+ else if (str24HourClock == TIME_FORMAT_24HOURS)
+ use24HourClock = true;
+ else if (str24HourClock == SETTING_REGIONAL_DEFAULT)
+ {
+ Set24HourClock(m_currentRegion->m_strTimeFormat);
+ return;
+ }
+ else
+ use24HourClock = DetermineUse24HourClockFromTimeFormat(str24HourClock);
+
+ if (m_use24HourClock == use24HourClock)
+ return;
+
+ m_use24HourClock = use24HourClock;
+}
+
+const std::string& CLangInfo::GetTimeZone() const
+{
+ return m_currentRegion->m_strTimeZone;
+}
+
+// Returns the AM/PM symbol of the current language
+const std::string& CLangInfo::GetMeridiemSymbol(MeridiemSymbol symbol) const
+{
+ // nothing to return if we use 24-hour clock
+ if (m_use24HourClock)
+ return StringUtils::Empty;
+
+ return MeridiemSymbolToString(symbol);
+}
+
+const std::string& CLangInfo::MeridiemSymbolToString(MeridiemSymbol symbol)
+{
+ switch (symbol)
+ {
+ case MeridiemSymbolAM:
+ return g_localizeStrings.Get(378);
+
+ case MeridiemSymbolPM:
+ return g_localizeStrings.Get(379);
+
+ default:
+ break;
+ }
+
+ return StringUtils::Empty;
+}
+
+// Fills the array with the region names available for this language
+void CLangInfo::GetRegionNames(std::vector<std::string>& array)
+{
+ for (const auto &region : m_regions)
+ {
+ std::string strName=region.first;
+ if (strName=="N/A")
+ strName=g_localizeStrings.Get(10005); // Not available
+ array.emplace_back(std::move(strName));
+ }
+}
+
+// Set the current region by its name, names from GetRegionNames() are valid.
+// If the region is not found the first available region is set.
+void CLangInfo::SetCurrentRegion(const std::string& strName)
+{
+ ITMAPREGIONS it=m_regions.find(strName);
+ if (it!=m_regions.end())
+ m_currentRegion=&it->second;
+ else if (!m_regions.empty())
+ m_currentRegion=&m_regions.begin()->second;
+ else
+ m_currentRegion=&m_defaultRegion;
+
+ m_currentRegion->SetGlobalLocale();
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetString(CSettings::SETTING_LOCALE_SHORTDATEFORMAT) == SETTING_REGIONAL_DEFAULT)
+ SetShortDateFormat(m_currentRegion->m_strDateFormatShort);
+ if (settings->GetString(CSettings::SETTING_LOCALE_LONGDATEFORMAT) == SETTING_REGIONAL_DEFAULT)
+ SetLongDateFormat(m_currentRegion->m_strDateFormatLong);
+ if (settings->GetString(CSettings::SETTING_LOCALE_USE24HOURCLOCK) == SETTING_REGIONAL_DEFAULT)
+ {
+ Set24HourClock(m_currentRegion->m_strTimeFormat);
+
+ // update the time format
+ SetTimeFormat(settings->GetString(CSettings::SETTING_LOCALE_TIMEFORMAT));
+ }
+ if (settings->GetString(CSettings::SETTING_LOCALE_TIMEFORMAT) == SETTING_REGIONAL_DEFAULT)
+ SetTimeFormat(m_currentRegion->m_strTimeFormat);
+ if (settings->GetString(CSettings::SETTING_LOCALE_TEMPERATUREUNIT) == SETTING_REGIONAL_DEFAULT)
+ SetTemperatureUnit(m_currentRegion->m_tempUnit);
+ if (settings->GetString(CSettings::SETTING_LOCALE_SPEEDUNIT) == SETTING_REGIONAL_DEFAULT)
+ SetSpeedUnit(m_currentRegion->m_speedUnit);
+}
+
+// Returns the current region set for this language
+const std::string& CLangInfo::GetCurrentRegion() const
+{
+ return m_currentRegion->m_strName;
+}
+
+CTemperature::Unit CLangInfo::GetTemperatureUnit() const
+{
+ return m_temperatureUnit;
+}
+
+void CLangInfo::SetTemperatureUnit(CTemperature::Unit temperatureUnit)
+{
+ if (m_temperatureUnit == temperatureUnit)
+ return;
+
+ m_temperatureUnit = temperatureUnit;
+
+ // refresh weather manager as temperatures need re-translating
+ // NOTE: this could be called before our service manager is up
+ if (CServiceBroker::IsServiceManagerUp())
+ CServiceBroker::GetWeatherManager().Refresh();
+}
+
+void CLangInfo::SetTemperatureUnit(const std::string& temperatureUnit)
+{
+ CTemperature::Unit unit = CTemperature::UnitCelsius;
+ if (temperatureUnit == SETTING_REGIONAL_DEFAULT)
+ unit = m_currentRegion->m_tempUnit;
+ else
+ unit = StringToTemperatureUnit(temperatureUnit);
+
+ SetTemperatureUnit(unit);
+}
+
+std::string CLangInfo::GetTemperatureAsString(const CTemperature& temperature) const
+{
+ if (!temperature.IsValid())
+ return g_localizeStrings.Get(13205); // "Unknown"
+
+ CTemperature::Unit temperatureUnit = GetTemperatureUnit();
+ return StringUtils::Format("{}{}", temperature.ToString(temperatureUnit),
+ GetTemperatureUnitString());
+}
+
+// Returns the temperature unit string for the current language
+const std::string& CLangInfo::GetTemperatureUnitString() const
+{
+ return GetTemperatureUnitString(m_temperatureUnit);
+}
+
+const std::string& CLangInfo::GetTemperatureUnitString(CTemperature::Unit temperatureUnit)
+{
+ return g_localizeStrings.Get(TEMP_UNIT_STRINGS + temperatureUnit);
+}
+
+void CLangInfo::SetSpeedUnit(CSpeed::Unit speedUnit)
+{
+ if (m_speedUnit == speedUnit)
+ return;
+
+ m_speedUnit = speedUnit;
+
+ // refresh weather manager as speeds need re-translating
+ // NOTE: this could be called before our service manager is up
+ if (CServiceBroker::IsServiceManagerUp())
+ CServiceBroker::GetWeatherManager().Refresh();
+}
+
+void CLangInfo::SetSpeedUnit(const std::string& speedUnit)
+{
+ CSpeed::Unit unit = CSpeed::UnitKilometresPerHour;
+ if (speedUnit == SETTING_REGIONAL_DEFAULT)
+ unit = m_currentRegion->m_speedUnit;
+ else
+ unit = StringToSpeedUnit(speedUnit);
+
+ SetSpeedUnit(unit);
+}
+
+CSpeed::Unit CLangInfo::GetSpeedUnit() const
+{
+ return m_speedUnit;
+}
+
+std::string CLangInfo::GetSpeedAsString(const CSpeed& speed) const
+{
+ if (!speed.IsValid())
+ return g_localizeStrings.Get(13205); // "Unknown"
+
+ return StringUtils::Format("{}{}", speed.ToString(GetSpeedUnit()), GetSpeedUnitString());
+}
+
+// Returns the speed unit string for the current language
+const std::string& CLangInfo::GetSpeedUnitString() const
+{
+ return GetSpeedUnitString(m_speedUnit);
+}
+
+const std::string& CLangInfo::GetSpeedUnitString(CSpeed::Unit speedUnit)
+{
+ return g_localizeStrings.Get(SPEED_UNIT_STRINGS + speedUnit);
+}
+
+std::set<std::string> CLangInfo::GetSortTokens() const
+{
+ std::set<std::string> sortTokens = m_sortTokens;
+ for (const auto& t : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_vecTokens)
+ sortTokens.insert(t);
+
+ return sortTokens;
+}
+
+bool CLangInfo::DetermineUse24HourClockFromTimeFormat(const std::string& timeFormat)
+{
+ // if the time format contains a "h" it's 12-hour and otherwise 24-hour clock format
+ return timeFormat.find('h') == std::string::npos;
+}
+
+bool CLangInfo::DetermineUseMeridiemFromTimeFormat(const std::string& timeFormat)
+{
+ // if the time format contains "xx" it's using meridiem
+ return timeFormat.find("xx") != std::string::npos;
+}
+
+std::string CLangInfo::PrepareTimeFormat(const std::string& timeFormat, bool use24HourClock)
+{
+ std::string preparedTimeFormat = timeFormat;
+ if (use24HourClock)
+ {
+ // replace all "h" with "H"
+ StringUtils::Replace(preparedTimeFormat, 'h', 'H');
+
+ // remove any "xx" for meridiem
+ StringUtils::Replace(preparedTimeFormat, "x", "");
+ }
+ else
+ // replace all "H" with "h"
+ StringUtils::Replace(preparedTimeFormat, 'H', 'h');
+
+ StringUtils::Trim(preparedTimeFormat);
+
+ return preparedTimeFormat;
+}
+
+void CLangInfo::SettingOptionsLanguageNamesFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ // find languages...
+ ADDON::VECADDONS addons;
+ if (!CServiceBroker::GetAddonMgr().GetAddons(addons, ADDON::AddonType::RESOURCE_LANGUAGE))
+ return;
+
+ for (const auto &addon : addons)
+ list.emplace_back(addon->Name(), addon->Name());
+
+ sort(list.begin(), list.end(), SortLanguage());
+}
+
+void CLangInfo::SettingOptionsISO6391LanguagesFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ std::vector<std::string> languages = g_LangCodeExpander.GetLanguageNames(
+ CLangCodeExpander::ISO_639_1, CLangCodeExpander::LANG_LIST::INCLUDE_USERDEFINED);
+
+ for (const auto &language : languages)
+ list.emplace_back(language, language);
+}
+
+void CLangInfo::SettingOptionsAudioStreamLanguagesFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(307), "mediadefault");
+ list.emplace_back(g_localizeStrings.Get(308), "original");
+ list.emplace_back(g_localizeStrings.Get(309), "default");
+
+ AddLanguages(list);
+}
+
+void CLangInfo::SettingOptionsSubtitleStreamLanguagesFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(231), "none");
+ list.emplace_back(g_localizeStrings.Get(13207), "forced_only");
+ list.emplace_back(g_localizeStrings.Get(308), "original");
+ list.emplace_back(g_localizeStrings.Get(309), "default");
+
+ AddLanguages(list);
+}
+
+void CLangInfo::SettingOptionsSubtitleDownloadlanguagesFiller(
+ const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(308), "original");
+ list.emplace_back(g_localizeStrings.Get(309), "default");
+
+ AddLanguages(list);
+}
+
+void CLangInfo::SettingOptionsRegionsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ std::vector<std::string> regions;
+ g_langInfo.GetRegionNames(regions);
+ std::sort(regions.begin(), regions.end(), sortstringbyname());
+
+ bool match = false;
+ for (unsigned int i = 0; i < regions.size(); ++i)
+ {
+ std::string region = regions[i];
+ list.emplace_back(region, region);
+
+ if (!match && region == std::static_pointer_cast<const CSettingString>(setting)->GetValue())
+ {
+ match = true;
+ current = region;
+ }
+ }
+
+ if (!match && !regions.empty())
+ current = regions[0];
+}
+
+void CLangInfo::SettingOptionsShortDateFormatsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ bool match = false;
+ const std::string& shortDateFormatSetting = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+
+ CDateTime now = CDateTime::GetCurrentDateTime();
+
+ list.emplace_back(
+ StringUtils::Format(g_localizeStrings.Get(20035),
+ now.GetAsLocalizedDate(g_langInfo.m_currentRegion->m_strDateFormatShort)),
+ SETTING_REGIONAL_DEFAULT);
+ if (shortDateFormatSetting == SETTING_REGIONAL_DEFAULT)
+ {
+ match = true;
+ current = SETTING_REGIONAL_DEFAULT;
+ }
+
+ for (const std::string& shortDateFormat : shortDateFormats)
+ {
+ list.emplace_back(now.GetAsLocalizedDate(shortDateFormat), shortDateFormat);
+
+ if (!match && shortDateFormatSetting == shortDateFormat)
+ {
+ match = true;
+ current = shortDateFormat;
+ }
+ }
+
+ if (!match && !list.empty())
+ current = list[0].value;
+}
+
+void CLangInfo::SettingOptionsLongDateFormatsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ bool match = false;
+ const std::string& longDateFormatSetting = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+
+ CDateTime now = CDateTime::GetCurrentDateTime();
+
+ list.emplace_back(
+ StringUtils::Format(g_localizeStrings.Get(20035),
+ now.GetAsLocalizedDate(g_langInfo.m_currentRegion->m_strDateFormatLong)),
+ SETTING_REGIONAL_DEFAULT);
+ if (longDateFormatSetting == SETTING_REGIONAL_DEFAULT)
+ {
+ match = true;
+ current = SETTING_REGIONAL_DEFAULT;
+ }
+
+ for (const std::string& longDateFormat : longDateFormats)
+ {
+ list.emplace_back(now.GetAsLocalizedDate(longDateFormat), longDateFormat);
+
+ if (!match && longDateFormatSetting == longDateFormat)
+ {
+ match = true;
+ current = longDateFormat;
+ }
+ }
+
+ if (!match && !list.empty())
+ current = list[0].value;
+}
+
+void CLangInfo::SettingOptionsTimeFormatsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ bool match = false;
+ const std::string& timeFormatSetting = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+
+ CDateTime now = CDateTime::GetCurrentDateTime();
+ bool use24hourFormat = g_langInfo.Use24HourClock();
+
+ list.emplace_back(
+ StringUtils::Format(g_localizeStrings.Get(20035),
+ ToSettingTimeFormat(now, g_langInfo.m_currentRegion->m_strTimeFormat)),
+ SETTING_REGIONAL_DEFAULT);
+ if (timeFormatSetting == SETTING_REGIONAL_DEFAULT)
+ {
+ match = true;
+ current = SETTING_REGIONAL_DEFAULT;
+ }
+
+ if (use24hourFormat)
+ {
+ list.emplace_back(ToSettingTimeFormat(now, TIME_FORMAT_SINGLE_24), TIME_FORMAT_SINGLE_24);
+ if (timeFormatSetting == TIME_FORMAT_SINGLE_24)
+ {
+ current = TIME_FORMAT_SINGLE_24;
+ match = true;
+ }
+
+ list.emplace_back(ToSettingTimeFormat(now, TIME_FORMAT_DOUBLE_24), TIME_FORMAT_DOUBLE_24);
+ if (timeFormatSetting == TIME_FORMAT_DOUBLE_24)
+ {
+ current = TIME_FORMAT_DOUBLE_24;
+ match = true;
+ }
+ }
+ else
+ {
+ list.emplace_back(ToSettingTimeFormat(now, TIME_FORMAT_SINGLE_12), TIME_FORMAT_SINGLE_12);
+ if (timeFormatSetting == TIME_FORMAT_SINGLE_12)
+ {
+ current = TIME_FORMAT_SINGLE_12;
+ match = true;
+ }
+
+ list.emplace_back(ToSettingTimeFormat(now, TIME_FORMAT_DOUBLE_12), TIME_FORMAT_DOUBLE_12);
+ if (timeFormatSetting == TIME_FORMAT_DOUBLE_12)
+ {
+ current = TIME_FORMAT_DOUBLE_12;
+ match = true;
+ }
+
+ std::string timeFormatSingle12Meridiem = ToTimeFormat(false, true, true);
+ list.emplace_back(ToSettingTimeFormat(now, timeFormatSingle12Meridiem), timeFormatSingle12Meridiem);
+ if (timeFormatSetting == timeFormatSingle12Meridiem)
+ {
+ current = timeFormatSingle12Meridiem;
+ match = true;
+ }
+
+ std::string timeFormatDouble12Meridiem = ToTimeFormat(false, false, true);
+ list.emplace_back(ToSettingTimeFormat(now, timeFormatDouble12Meridiem), timeFormatDouble12Meridiem);
+ if (timeFormatSetting == timeFormatDouble12Meridiem)
+ {
+ current = timeFormatDouble12Meridiem;
+ match = true;
+ }
+ }
+
+ if (!match && !list.empty())
+ current = list[0].value;
+}
+
+void CLangInfo::SettingOptions24HourClockFormatsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ bool match = false;
+ const std::string& clock24HourFormatSetting = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+
+ // determine the 24-hour clock format of the regional setting
+ int regionalClock24HourFormatLabel = DetermineUse24HourClockFromTimeFormat(g_langInfo.m_currentRegion->m_strTimeFormat) ? 12384 : 12383;
+ list.emplace_back(StringUtils::Format(g_localizeStrings.Get(20035),
+ g_localizeStrings.Get(regionalClock24HourFormatLabel)),
+ SETTING_REGIONAL_DEFAULT);
+ if (clock24HourFormatSetting == SETTING_REGIONAL_DEFAULT)
+ {
+ match = true;
+ current = SETTING_REGIONAL_DEFAULT;
+ }
+
+ list.emplace_back(g_localizeStrings.Get(12383), TIME_FORMAT_12HOURS);
+ if (clock24HourFormatSetting == TIME_FORMAT_12HOURS)
+ {
+ current = TIME_FORMAT_12HOURS;
+ match = true;
+ }
+
+ list.emplace_back(g_localizeStrings.Get(12384), TIME_FORMAT_24HOURS);
+ if (clock24HourFormatSetting == TIME_FORMAT_24HOURS)
+ {
+ current = TIME_FORMAT_24HOURS;
+ match = true;
+ }
+
+ if (!match && !list.empty())
+ current = list[0].value;
+}
+
+void CLangInfo::SettingOptionsTemperatureUnitsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ bool match = false;
+ const std::string& temperatureUnitSetting = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+
+ list.emplace_back(
+ StringUtils::Format(g_localizeStrings.Get(20035),
+ GetTemperatureUnitString(g_langInfo.m_currentRegion->m_tempUnit)),
+ SETTING_REGIONAL_DEFAULT);
+ if (temperatureUnitSetting == SETTING_REGIONAL_DEFAULT)
+ {
+ match = true;
+ current = SETTING_REGIONAL_DEFAULT;
+ }
+
+ for (const TemperatureInfo& info : temperatureInfo)
+ {
+ list.emplace_back(GetTemperatureUnitString(info.unit), info.name);
+
+ if (!match && temperatureUnitSetting == info.name)
+ {
+ match = true;
+ current = info.name;
+ }
+ }
+
+ if (!match && !list.empty())
+ current = list[0].value;
+}
+
+void CLangInfo::SettingOptionsSpeedUnitsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ bool match = false;
+ const std::string& speedUnitSetting = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+
+ list.emplace_back(
+ StringUtils::Format(g_localizeStrings.Get(20035),
+ GetSpeedUnitString(g_langInfo.m_currentRegion->m_speedUnit)),
+ SETTING_REGIONAL_DEFAULT);
+ if (speedUnitSetting == SETTING_REGIONAL_DEFAULT)
+ {
+ match = true;
+ current = SETTING_REGIONAL_DEFAULT;
+ }
+
+ for (const SpeedInfo& info : speedInfo)
+ {
+ list.emplace_back(GetSpeedUnitString(info.unit), info.name);
+
+ if (!match && speedUnitSetting == info.name)
+ {
+ match = true;
+ current = info.name;
+ }
+ }
+
+ if (!match && !list.empty())
+ current = list[0].value;
+}
+
+void CLangInfo::AddLanguages(std::vector<StringSettingOption> &list)
+{
+ std::vector<std::string> languages = g_LangCodeExpander.GetLanguageNames(
+ CLangCodeExpander::ISO_639_1, CLangCodeExpander::LANG_LIST::INCLUDE_ADDONS_USERDEFINED);
+
+ for (const auto& language : languages)
+ list.emplace_back(language, language);
+}
diff --git a/xbmc/LangInfo.h b/xbmc/LangInfo.h
new file mode 100644
index 0000000..1f6bc34
--- /dev/null
+++ b/xbmc/LangInfo.h
@@ -0,0 +1,325 @@
+/*
+ * 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 "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+#include "utils/GlobalsHandling.h"
+#include "utils/Locale.h"
+#include "utils/Speed.h"
+#include "utils/Temperature.h"
+
+#include <locale>
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#ifdef TARGET_WINDOWS
+#ifdef GetDateFormat
+#undef GetDateFormat
+#endif // GetDateFormat
+#ifdef GetTimeFormat
+#undef GetTimeFormat
+#endif // GetTimeFormat
+#endif // TARGET_WINDOWS
+
+class TiXmlNode;
+struct StringSettingOption;
+
+namespace ADDON
+{
+ class CLanguageResource;
+}
+typedef std::shared_ptr<ADDON::CLanguageResource> LanguageResourcePtr;
+
+typedef enum MeridiemSymbol
+{
+ MeridiemSymbolPM = 0,
+ MeridiemSymbolAM
+} MeridiemSymbol;
+
+class CLangInfo : public ISettingCallback, public ISettingsHandler
+{
+public:
+ CLangInfo();
+ ~CLangInfo() override;
+
+ // implementation of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ // implementation of ISettingsHandler
+ void OnSettingsLoaded() override;
+
+ /*
+ * \brief Get language codes list of the installed language addons.
+ * \param languages [OUT] The list of languages (language code, name).
+ */
+ static void GetAddonsLanguageCodes(std::map<std::string, std::string>& languages);
+
+ /*!
+ \brief Returns the language addon for the given locale (or the current one).
+
+ \param locale (optional) Locale of the language (current if empty)
+ \return Language addon for the given locale or NULL if the locale is invalid.
+ */
+ LanguageResourcePtr GetLanguageAddon(const std::string& locale = "") const;
+
+ std::string GetGuiCharSet() const;
+ std::string GetSubtitleCharSet() const;
+
+ // three char language code (not win32 specific)
+ const std::string& GetLanguageCode() const { return m_languageCodeGeneral; }
+
+ /*!
+ * \brief Convert an english language name to an addon locale,
+ * by searching in the installed language addons.
+ * \param langName [IN] The english language name
+ * \return The locale for the given english name, or empty if not found
+ */
+ static std::string ConvertEnglishNameToAddonLocale(const std::string& langName);
+
+ /*!
+ * \brief Get the english language name from given locale,
+ * by searching in the installed language addons.
+ * \param locale [OPT] Locale of the language (current if empty)
+ */
+ std::string GetEnglishLanguageName(const std::string& locale = "") const;
+
+ /*!
+ \brief Sets and loads the given (or configured) language, its details and strings.
+
+ \param strLanguage (optional) Language to be loaded.
+ \param reloadServices (optional) Whether to reload services relying on localization.
+ \return True if the language has been successfully loaded, false otherwise.
+ */
+ bool SetLanguage(std::string strLanguage = "", bool reloadServices = true);
+
+ const std::string& GetAudioLanguage() const;
+ // language can either be a two char language code as defined in ISO639
+ // or a three char language code
+ // or a language name in english (as used by XBMC)
+ void SetAudioLanguage(const std::string& language);
+
+ // three char language code (not win32 specific)
+ const std::string& GetSubtitleLanguage() const;
+ // language can either be a two char language code as defined in ISO639
+ // or a three char language code
+ // or a language name in english (as used by XBMC)
+ void SetSubtitleLanguage(const std::string& language);
+
+ const std::string GetDVDMenuLanguage() const;
+ const std::string GetDVDAudioLanguage() const;
+ const std::string GetDVDSubtitleLanguage() const;
+ const std::string& GetTimeZone() const;
+
+ const std::string& GetRegionLocale() const;
+
+ const std::locale& GetOriginalLocale() const;
+
+ /*!
+ \brief Returns the full locale of the current language.
+ */
+ const CLocale& GetLocale() const;
+
+ /*!
+ \brief Returns the system's current locale.
+ */
+ const std::locale& GetSystemLocale() const { return m_systemLocale; }
+
+ bool ForceUnicodeFont() const { return m_forceUnicodeFont; }
+
+ const std::string& GetDateFormat(bool bLongDate = false) const;
+ void SetDateFormat(const std::string& dateFormat, bool bLongDate = false);
+ const std::string& GetShortDateFormat() const;
+ void SetShortDateFormat(const std::string& shortDateFormat);
+ const std::string& GetLongDateFormat() const;
+ void SetLongDateFormat(const std::string& longDateFormat);
+
+ const std::string& GetTimeFormat() const;
+ void SetTimeFormat(const std::string& timeFormat);
+ bool Use24HourClock() const;
+ void Set24HourClock(bool use24HourClock);
+ void Set24HourClock(const std::string& str24HourClock);
+ const std::string& GetMeridiemSymbol(MeridiemSymbol symbol) const;
+ static const std::string& MeridiemSymbolToString(MeridiemSymbol symbol);
+
+ CTemperature::Unit GetTemperatureUnit() const;
+ void SetTemperatureUnit(CTemperature::Unit temperatureUnit);
+ void SetTemperatureUnit(const std::string& temperatureUnit);
+ const std::string& GetTemperatureUnitString() const;
+ static const std::string& GetTemperatureUnitString(CTemperature::Unit temperatureUnit);
+ std::string GetTemperatureAsString(const CTemperature& temperature) const;
+
+ CSpeed::Unit GetSpeedUnit() const;
+ void SetSpeedUnit(CSpeed::Unit speedUnit);
+ void SetSpeedUnit(const std::string& speedUnit);
+ const std::string& GetSpeedUnitString() const;
+ static const std::string& GetSpeedUnitString(CSpeed::Unit speedUnit);
+ std::string GetSpeedAsString(const CSpeed& speed) const;
+
+ void GetRegionNames(std::vector<std::string>& array);
+ void SetCurrentRegion(const std::string& strName);
+ const std::string& GetCurrentRegion() const;
+
+ std::set<std::string> GetSortTokens() const;
+
+ static std::string GetLanguagePath() { return "resource://"; }
+ static std::string GetLanguagePath(const std::string &language);
+ static std::string GetLanguageInfoPath(const std::string &language);
+ bool UseLocaleCollation();
+
+ static void LoadTokens(const TiXmlNode* pTokens, std::set<std::string>& vecTokens);
+
+ static void SettingOptionsLanguageNamesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsAudioStreamLanguagesFiller(
+ const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsSubtitleStreamLanguagesFiller(
+ const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsSubtitleDownloadlanguagesFiller(
+ const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsISO6391LanguagesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsRegionsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsShortDateFormatsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsLongDateFormatsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsTimeFormatsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptions24HourClockFormatsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsTemperatureUnitsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsSpeedUnitsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+
+protected:
+ void SetDefaults();
+ bool Load(const std::string& strLanguage);
+
+ static bool DetermineUse24HourClockFromTimeFormat(const std::string& timeFormat);
+ static bool DetermineUseMeridiemFromTimeFormat(const std::string& timeFormat);
+ static std::string PrepareTimeFormat(const std::string& timeFormat, bool use24HourClock);
+ static void AddLanguages(std::vector<StringSettingOption> &list);
+
+ class CRegion final
+ {
+ public:
+ CRegion();
+ void SetDefaults();
+ void SetTemperatureUnit(const std::string& strUnit);
+ void SetSpeedUnit(const std::string& strUnit);
+ void SetTimeZone(const std::string& strTimeZone);
+
+ class custom_numpunct : public std::numpunct<char>
+ {
+ public:
+ custom_numpunct(const char decimal_point, const char thousands_sep, const std::string& grouping)
+ : cDecimalPoint(decimal_point), cThousandsSep(thousands_sep), sGroup(grouping) {}
+ protected:
+ char do_decimal_point() const override { return cDecimalPoint; }
+ char do_thousands_sep() const override { return cThousandsSep; }
+ std::string do_grouping() const override { return sGroup; }
+ private:
+ const char cDecimalPoint;
+ const char cThousandsSep;
+ const std::string sGroup;
+ };
+
+ /*! \brief Set the locale associated with this region global.
+
+ Set the locale associated with this region global. This affects string
+ sorting & transformations.
+ */
+ void SetGlobalLocale();
+ std::string m_strLangLocaleName;
+ std::string m_strLangLocaleCodeTwoChar;
+ std::string m_strRegionLocaleName;
+ std::string m_strName;
+ std::string m_strDateFormatLong;
+ std::string m_strDateFormatShort;
+ std::string m_strTimeFormat;
+ std::string m_strMeridiemSymbols[2];
+ std::string m_strTimeZone;
+ std::string m_strGrouping;
+ char m_cDecimalSep;
+ char m_cThousandsSep;
+
+ CTemperature::Unit m_tempUnit;
+ CSpeed::Unit m_speedUnit;
+ };
+
+
+ typedef std::map<std::string, CRegion> MAPREGIONS;
+ typedef std::map<std::string, CRegion>::iterator ITMAPREGIONS;
+ typedef std::pair<std::string, CRegion> PAIR_REGIONS;
+ MAPREGIONS m_regions;
+ CRegion* m_currentRegion; // points to the current region
+ CRegion m_defaultRegion; // default, will be used if no region available via langinfo.xml
+ std::locale m_systemLocale; // current locale, matching GUI settings
+ std::locale m_originalLocale; // original locale, without changes of collate
+ int m_collationtype;
+ LanguageResourcePtr m_languageAddon;
+
+ std::string m_strGuiCharSet;
+ bool m_forceUnicodeFont;
+ std::string m_strSubtitleCharSet;
+ std::string m_strDVDMenuLanguage;
+ std::string m_strDVDAudioLanguage;
+ std::string m_strDVDSubtitleLanguage;
+ std::set<std::string> m_sortTokens;
+
+ std::string m_shortDateFormat;
+ std::string m_longDateFormat;
+ std::string m_timeFormat;
+ bool m_use24HourClock;
+ CTemperature::Unit m_temperatureUnit;
+ CSpeed::Unit m_speedUnit;
+
+ std::string m_audioLanguage;
+ std::string m_subtitleLanguage;
+ // this is the general (not win32-specific) three char language code
+ std::string m_languageCodeGeneral;
+};
+
+
+XBMC_GLOBAL_REF(CLangInfo, g_langInfo);
+#define g_langInfo XBMC_GLOBAL_USE(CLangInfo)
diff --git a/xbmc/LockType.h b/xbmc/LockType.h
new file mode 100644
index 0000000..9659046
--- /dev/null
+++ b/xbmc/LockType.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2005-2020 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
+
+typedef enum
+{
+ LOCK_MODE_UNKNOWN = -1,
+ LOCK_MODE_EVERYONE = 0,
+ LOCK_MODE_NUMERIC = 1,
+ LOCK_MODE_GAMEPAD = 2,
+ LOCK_MODE_QWERTY = 3,
+ LOCK_MODE_SAMBA = 4,
+ LOCK_MODE_EEPROM_PARENTAL = 5
+} LockType;
diff --git a/xbmc/MediaSource.cpp b/xbmc/MediaSource.cpp
new file mode 100644
index 0000000..6782ca3
--- /dev/null
+++ b/xbmc/MediaSource.cpp
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2005-2020 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 "MediaSource.h"
+
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "media/MediaLockState.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE;
+
+bool CMediaSource::IsWritable() const
+{
+ return CUtil::SupportsWriteFileOperations(strPath);
+}
+
+void CMediaSource::FromNameAndPaths(const std::string &category, const std::string &name, const std::vector<std::string> &paths)
+{
+ vecPaths = paths;
+ if (paths.empty())
+ { // no paths - return
+ strPath.clear();
+ }
+ else if (paths.size() == 1)
+ { // only one valid path? make it the strPath
+ strPath = paths[0];
+ }
+ else
+ { // multiple valid paths?
+ strPath = CMultiPathDirectory::ConstructMultiPath(vecPaths);
+ }
+
+ strName = name;
+ m_iLockMode = LOCK_MODE_EVERYONE;
+ m_strLockCode = "0";
+ m_iBadPwdCount = 0;
+ m_iHasLock = LOCK_STATE_NO_LOCK;
+ m_allowSharing = true;
+
+ if (URIUtils::IsMultiPath(strPath))
+ m_iDriveType = SOURCE_TYPE_VPATH;
+ else if (StringUtils::StartsWithNoCase(strPath, "udf:"))
+ {
+ m_iDriveType = SOURCE_TYPE_VIRTUAL_DVD;
+ strPath = "D:\\";
+ }
+ else if (URIUtils::IsISO9660(strPath))
+ m_iDriveType = SOURCE_TYPE_VIRTUAL_DVD;
+ else if (URIUtils::IsDVD(strPath))
+ m_iDriveType = SOURCE_TYPE_DVD;
+ else if (URIUtils::IsRemote(strPath))
+ m_iDriveType = SOURCE_TYPE_REMOTE;
+ else if (URIUtils::IsHD(strPath))
+ m_iDriveType = SOURCE_TYPE_LOCAL;
+ else
+ m_iDriveType = SOURCE_TYPE_UNKNOWN;
+ // check - convert to url and back again to make sure strPath is accurate
+ // in terms of what we expect
+ strPath = CURL(strPath).Get();
+}
+
+bool CMediaSource::operator==(const CMediaSource &share) const
+{
+ // NOTE: we may wish to filter this through CURL to enable better "fuzzy" matching
+ if (strPath != share.strPath)
+ return false;
+ if (strName != share.strName)
+ return false;
+ return true;
+}
+
+void AddOrReplace(VECSOURCES& sources, const VECSOURCES& extras)
+{
+ unsigned int i;
+ for( i=0;i<extras.size();++i )
+ {
+ unsigned int j;
+ for ( j=0;j<sources.size();++j)
+ {
+ if (StringUtils::EqualsNoCase(sources[j].strPath, extras[i].strPath))
+ {
+ sources[j] = extras[i];
+ break;
+ }
+ }
+ if (j == sources.size())
+ sources.push_back(extras[i]);
+ }
+}
+
+void AddOrReplace(VECSOURCES& sources, const CMediaSource& source)
+{
+ unsigned int i;
+ for( i=0;i<sources.size();++i )
+ {
+ if (StringUtils::EqualsNoCase(sources[i].strPath, source.strPath))
+ {
+ sources[i] = source;
+ break;
+ }
+ }
+ if (i == sources.size())
+ sources.push_back(source);
+}
diff --git a/xbmc/MediaSource.h b/xbmc/MediaSource.h
new file mode 100644
index 0000000..1afbea2
--- /dev/null
+++ b/xbmc/MediaSource.h
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2005-2020 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 "LockType.h"
+#include "media/MediaLockState.h"
+
+#include <string>
+#include <vector>
+
+/*!
+\ingroup windows
+\brief Represents a share.
+\sa VECMediaSource, IVECSOURCES
+*/
+class CMediaSource final
+{
+public:
+ enum SourceType
+ {
+ SOURCE_TYPE_UNKNOWN = 0,
+ SOURCE_TYPE_LOCAL = 1,
+ SOURCE_TYPE_DVD = 2,
+ SOURCE_TYPE_VIRTUAL_DVD = 3,
+ SOURCE_TYPE_REMOTE = 4,
+ SOURCE_TYPE_VPATH = 5,
+ SOURCE_TYPE_REMOVABLE = 6
+ };
+
+ bool operator==(const CMediaSource &right) const;
+
+ void FromNameAndPaths(const std::string &category, const std::string &name, const std::vector<std::string> &paths);
+ bool IsWritable() const;
+ std::string strName; ///< Name of the share, can be chosen freely.
+ std::string strStatus; ///< Status of the share (eg has disk etc.)
+ std::string strDiskUniqueId; ///< removable:// + DVD Label + DVD ID for resume point storage, if available
+ std::string strPath; ///< Path of the share, eg. iso9660:// or F:
+
+ /*!
+ \brief The type of the media source.
+
+ Value can be:
+ - SOURCE_TYPE_UNKNOWN \n
+ Unknown source, maybe a wrong path.
+ - SOURCE_TYPE_LOCAL \n
+ Harddisk source.
+ - SOURCE_TYPE_DVD \n
+ DVD-ROM source of the build in drive, strPath may vary.
+ - SOURCE_TYPE_VIRTUAL_DVD \n
+ DVD-ROM source, strPath is fix.
+ - SOURCE_TYPE_REMOTE \n
+ Network source.
+ */
+ SourceType m_iDriveType = SOURCE_TYPE_UNKNOWN;
+
+ /*!
+ \brief The type of Lock UI to show when accessing the media source.
+
+ Value can be:
+ - CMediaSource::LOCK_MODE_EVERYONE \n
+ Default value. No lock UI is shown, user can freely access the source.
+ - LOCK_MODE_NUMERIC \n
+ Lock code is entered via OSD numpad or IrDA remote buttons.
+ - LOCK_MODE_GAMEPAD \n
+ Lock code is entered via XBOX gamepad buttons.
+ - LOCK_MODE_QWERTY \n
+ Lock code is entered via OSD keyboard or PC USB keyboard.
+ - LOCK_MODE_SAMBA \n
+ Lock code is entered via OSD keyboard or PC USB keyboard and passed directly to SMB for authentication.
+ - LOCK_MODE_EEPROM_PARENTAL \n
+ Lock code is retrieved from XBOX EEPROM and entered via XBOX gamepad or remote.
+ - LOCK_MODE_UNKNOWN \n
+ Value is unknown or unspecified.
+ */
+ LockType m_iLockMode = LOCK_MODE_EVERYONE;
+ std::string m_strLockCode; ///< Input code for Lock UI to verify, can be chosen freely.
+ int m_iHasLock = LOCK_STATE_NO_LOCK;
+ int m_iBadPwdCount = 0; ///< Number of wrong passwords user has entered since share was last unlocked
+
+ std::string m_strThumbnailImage; ///< Path to a thumbnail image for the share, or blank for default
+
+ std::vector<std::string> vecPaths;
+ bool m_ignore = false; /// <Do not store in xml
+ bool m_allowSharing = true; /// <Allow browsing of source from UPnP / WebServer
+};
+
+/*!
+\ingroup windows
+\brief A vector to hold CMediaSource objects.
+\sa CMediaSource, IVECSOURCES
+*/
+typedef std::vector<CMediaSource> VECSOURCES;
+
+/*!
+\ingroup windows
+\brief Iterator of VECSOURCES.
+\sa CMediaSource, VECSOURCES
+*/
+typedef std::vector<CMediaSource>::iterator IVECSOURCES;
+typedef std::vector<CMediaSource>::const_iterator CIVECSOURCES;
+
+void AddOrReplace(VECSOURCES& sources, const VECSOURCES& extras);
+void AddOrReplace(VECSOURCES& sources, const CMediaSource& source);
diff --git a/xbmc/NfoFile.cpp b/xbmc/NfoFile.cpp
new file mode 100644
index 0000000..98a49b5
--- /dev/null
+++ b/xbmc/NfoFile.cpp
@@ -0,0 +1,190 @@
+/*
+ * 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.
+ */
+// NfoFile.cpp: implementation of the CNfoFile class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "NfoFile.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/File.h"
+#include "music/Album.h"
+#include "music/Artist.h"
+#include "video/VideoInfoDownloader.h"
+
+#include <string>
+#include <vector>
+
+using namespace XFILE;
+using namespace ADDON;
+
+CInfoScanner::INFO_TYPE CNfoFile::Create(const std::string& strPath,
+ const ScraperPtr& info, int episode)
+{
+ m_info = info; // assume we can use these settings
+ m_type = ScraperTypeFromContent(info->Content());
+ if (Load(strPath) != 0)
+ return CInfoScanner::NO_NFO;
+
+ CFileItemList items;
+ bool bNfo=false;
+
+ if (m_type == AddonType::SCRAPER_ALBUMS)
+ {
+ CAlbum album;
+ bNfo = GetDetails(album);
+ }
+ else if (m_type == AddonType::SCRAPER_ARTISTS)
+ {
+ CArtist artist;
+ bNfo = GetDetails(artist);
+ }
+ else if (m_type == AddonType::SCRAPER_TVSHOWS || m_type == AddonType::SCRAPER_MOVIES ||
+ m_type == AddonType::SCRAPER_MUSICVIDEOS)
+ {
+ // first check if it's an XML file with the info we need
+ CVideoInfoTag details;
+ bNfo = GetDetails(details);
+ if (episode > -1 && bNfo && m_type == AddonType::SCRAPER_TVSHOWS)
+ {
+ int infos=0;
+ while (m_headPos != std::string::npos && details.m_iEpisode != episode)
+ {
+ m_headPos = m_doc.find("<episodedetails", m_headPos + 1);
+ if (m_headPos == std::string::npos)
+ break;
+
+ bNfo = GetDetails(details);
+ infos++;
+ }
+ if (details.m_iEpisode != episode)
+ {
+ bNfo = false;
+ details.Reset();
+ m_headPos = 0;
+ if (infos == 1) // still allow differing nfo/file numbers for single ep nfo's
+ bNfo = GetDetails(details);
+ }
+ }
+ }
+
+ std::vector<ScraperPtr> vecScrapers = GetScrapers(m_type, m_info);
+
+ // search ..
+ int res = -1;
+ for (unsigned int i=0; i<vecScrapers.size(); ++i)
+ if ((res = Scrape(vecScrapers[i], m_scurl, m_doc)) == 0 || res == 2)
+ break;
+
+ if (res == 2)
+ return CInfoScanner::ERROR_NFO;
+ if (bNfo)
+ {
+ if (!m_scurl.HasUrls())
+ {
+ if (m_doc.find("[scrape url]") != std::string::npos)
+ return CInfoScanner::OVERRIDE_NFO;
+ else
+ return CInfoScanner::FULL_NFO;
+ }
+ else
+ return CInfoScanner::COMBINED_NFO;
+ }
+ return m_scurl.HasUrls() ? CInfoScanner::URL_NFO : CInfoScanner::NO_NFO;
+}
+
+// return value: 0 - success; 1 - no result; skip; 2 - error
+int CNfoFile::Scrape(ScraperPtr& scraper, CScraperUrl& url,
+ const std::string& content)
+{
+ if (scraper->IsNoop())
+ {
+ url = CScraperUrl();
+ return 0;
+ }
+
+ scraper->ClearCache();
+
+ try
+ {
+ url = scraper->NfoUrl(content);
+ }
+ catch (const CScraperError &sce)
+ {
+ CVideoInfoDownloader::ShowErrorDialog(sce);
+ if (!sce.FAborted())
+ return 2;
+ }
+
+ return url.HasUrls() ? 0 : 1;
+}
+
+int CNfoFile::Load(const std::string& strFile)
+{
+ Close();
+ XFILE::CFile file;
+ std::vector<uint8_t> buf;
+ if (file.LoadFile(strFile, buf) > 0)
+ {
+ m_doc.assign(reinterpret_cast<char*>(buf.data()), buf.size());
+ m_headPos = 0;
+ return 0;
+ }
+ m_doc.clear();
+ return 1;
+}
+
+void CNfoFile::Close()
+{
+ m_doc.clear();
+ m_headPos = 0;
+ m_scurl.Clear();
+}
+
+std::vector<ScraperPtr> CNfoFile::GetScrapers(AddonType type, const ScraperPtr& selectedScraper)
+{
+ AddonPtr addon;
+ ScraperPtr defaultScraper;
+ if (CAddonSystemSettings::GetInstance().GetActive(type, addon))
+ defaultScraper = std::dynamic_pointer_cast<CScraper>(addon);
+
+ std::vector<ScraperPtr> vecScrapers;
+
+ // add selected scraper - first priority
+ if (selectedScraper)
+ vecScrapers.push_back(selectedScraper);
+
+ // Add all scrapers except selected and default
+ VECADDONS addons;
+ CServiceBroker::GetAddonMgr().GetAddons(addons, type);
+
+ for (auto& addon : addons)
+ {
+ ScraperPtr scraper = std::dynamic_pointer_cast<CScraper>(addon);
+
+ // skip if scraper requires settings and there's nothing set yet
+ if (scraper->RequiresSettings() && !scraper->HasUserSettings())
+ continue;
+
+ if ((!selectedScraper || selectedScraper->ID() != scraper->ID()) &&
+ (!defaultScraper || defaultScraper->ID() != scraper->ID()))
+ vecScrapers.push_back(scraper);
+ }
+
+ // add default scraper - not user selectable so it's last priority
+ if (defaultScraper && (!selectedScraper ||
+ selectedScraper->ID() != defaultScraper->ID()) &&
+ (!defaultScraper->RequiresSettings() || defaultScraper->HasUserSettings()))
+ vecScrapers.push_back(defaultScraper);
+
+ return vecScrapers;
+}
diff --git a/xbmc/NfoFile.h b/xbmc/NfoFile.h
new file mode 100644
index 0000000..9b97f2a
--- /dev/null
+++ b/xbmc/NfoFile.h
@@ -0,0 +1,69 @@
+/*
+ * 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
+
+// NfoFile.h: interface for the CNfoFile class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "InfoScanner.h"
+#include "addons/Scraper.h"
+#include "utils/XBMCTinyXML.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace ADDON
+{
+enum class AddonType;
+}
+
+class CNfoFile
+{
+public:
+ virtual ~CNfoFile() { Close(); }
+
+ CInfoScanner::INFO_TYPE Create(const std::string&,
+ const ADDON::ScraperPtr&, int episode=-1);
+ template<class T>
+ bool GetDetails(T& details, const char* document=NULL,
+ bool prioritise=false)
+ {
+ CXBMCTinyXML doc;
+ if (document)
+ doc.Parse(document, TIXML_ENCODING_UNKNOWN);
+ else if (m_headPos < m_doc.size())
+ doc.Parse(m_doc.substr(m_headPos), TIXML_ENCODING_UNKNOWN);
+ else
+ return false;
+
+ return details.Load(doc.RootElement(), true, prioritise);
+ }
+
+ void Close();
+ void SetScraperInfo(ADDON::ScraperPtr info) { m_info = std::move(info); }
+ ADDON::ScraperPtr GetScraperInfo() { return m_info; }
+ const CScraperUrl &ScraperUrl() const { return m_scurl; }
+
+ static int Scrape(ADDON::ScraperPtr& scraper, CScraperUrl& url,
+ const std::string& content);
+
+ static std::vector<ADDON::ScraperPtr> GetScrapers(ADDON::AddonType type,
+ const ADDON::ScraperPtr& selectedScraper);
+
+private:
+ std::string m_doc;
+ size_t m_headPos = 0;
+ ADDON::ScraperPtr m_info;
+ ADDON::AddonType m_type{};
+ CScraperUrl m_scurl;
+
+ int Load(const std::string&);
+};
diff --git a/xbmc/PartyModeManager.cpp b/xbmc/PartyModeManager.cpp
new file mode 100644
index 0000000..2f02c23
--- /dev/null
+++ b/xbmc/PartyModeManager.cpp
@@ -0,0 +1,575 @@
+/*
+ * 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 "PartyModeManager.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicDatabase.h"
+#include "music/tags/MusicInfoTag.h"
+#include "playlists/PlayList.h"
+#include "playlists/SmartPlayList.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Random.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoInfoTag.h"
+
+#include <algorithm>
+
+using namespace KODI::MESSAGING;
+
+#define QUEUE_DEPTH 10
+
+CPartyModeManager::CPartyModeManager(void)
+{
+ m_bIsVideo = false;
+ m_bEnabled = false;
+ ClearState();
+}
+
+bool CPartyModeManager::Enable(PartyModeContext context /*= PARTYMODECONTEXT_MUSIC*/, const std::string& strXspPath /*= ""*/)
+{
+ // Filter using our PartyMode xml file
+ CSmartPlaylist playlist;
+ std::string partyModePath;
+ bool playlistLoaded;
+
+ m_bIsVideo = context == PARTYMODECONTEXT_VIDEO;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (!strXspPath.empty()) //if a path to a smartplaylist is supplied use it
+ partyModePath = strXspPath;
+ else if (m_bIsVideo)
+ partyModePath = profileManager->GetUserDataItem("PartyMode-Video.xsp");
+ else
+ partyModePath = profileManager->GetUserDataItem("PartyMode.xsp");
+
+ playlistLoaded=playlist.Load(partyModePath);
+
+ if (playlistLoaded)
+ {
+ m_type = playlist.GetType();
+ if (context == PARTYMODECONTEXT_UNKNOWN)
+ {
+ //get it from the xsp file
+ m_bIsVideo = (StringUtils::EqualsNoCase(m_type, "video") ||
+ StringUtils::EqualsNoCase(m_type, "musicvideos") ||
+ StringUtils::EqualsNoCase(m_type, "mixed"));
+ }
+ }
+ else if (m_bIsVideo)
+ m_type = "musicvideos";
+ else
+ m_type = "songs";
+
+ CGUIDialogProgress* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ int iHeading = (m_bIsVideo ? 20250 : 20121);
+ int iLine0 = (m_bIsVideo ? 20251 : 20123);
+ pDialog->SetHeading(CVariant{iHeading});
+ pDialog->SetLine(0, CVariant{iLine0});
+ pDialog->SetLine(1, CVariant{""});
+ pDialog->SetLine(2, CVariant{""});
+ pDialog->Open();
+
+ ClearState();
+ std::string strCurrentFilterMusic;
+ std::string strCurrentFilterVideo;
+ unsigned int songcount = 0;
+ unsigned int videocount = 0;
+ auto start = std::chrono::steady_clock::now();
+
+ if (StringUtils::EqualsNoCase(m_type, "songs") ||
+ StringUtils::EqualsNoCase(m_type, "mixed"))
+ {
+ CMusicDatabase db;
+ if (db.Open())
+ {
+ std::set<std::string> playlists;
+ if (playlistLoaded)
+ {
+ playlist.SetType("songs");
+ strCurrentFilterMusic = playlist.GetWhereClause(db, playlists);
+ }
+
+ CLog::Log(LOGINFO, "PARTY MODE MANAGER: Registering filter:[{}]", strCurrentFilterMusic);
+ songcount = db.GetRandomSongIDs(CDatabase::Filter(strCurrentFilterMusic), m_songIDCache);
+ m_iMatchingSongs = static_cast<int>(songcount);
+ if (m_iMatchingSongs < 1 && StringUtils::EqualsNoCase(m_type, "songs"))
+ {
+ pDialog->Close();
+ db.Close();
+ OnError(16031, "Party mode found no matching songs. Aborting.");
+ return false;
+ }
+ }
+ else
+ {
+ pDialog->Close();
+ OnError(16033, "Party mode could not open database. Aborting.");
+ return false;
+ }
+ db.Close();
+ }
+
+ if (StringUtils::EqualsNoCase(m_type, "musicvideos") ||
+ StringUtils::EqualsNoCase(m_type, "mixed"))
+ {
+ std::vector< std::pair<int,int> > songIDs2;
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ std::set<std::string> playlists;
+ if (playlistLoaded)
+ {
+ playlist.SetType("musicvideos");
+ strCurrentFilterVideo = playlist.GetWhereClause(db, playlists);
+ }
+
+ CLog::Log(LOGINFO, "PARTY MODE MANAGER: Registering filter:[{}]", strCurrentFilterVideo);
+ videocount = db.GetRandomMusicVideoIDs(strCurrentFilterVideo, songIDs2);
+ m_iMatchingSongs += static_cast<int>(videocount);
+ if (m_iMatchingSongs < 1)
+ {
+ pDialog->Close();
+ db.Close();
+ OnError(16031, "Party mode found no matching songs. Aborting.");
+ return false;
+ }
+ }
+ else
+ {
+ pDialog->Close();
+ OnError(16033, "Party mode could not open database. Aborting.");
+ return false;
+ }
+ db.Close();
+ m_songIDCache.insert(m_songIDCache.end(), songIDs2.begin(), songIDs2.end());
+ }
+
+ // Songs and music videos are random from query, but need mixing together when have both
+ if (songcount > 0 && videocount > 0 )
+ KODI::UTILS::RandomShuffle(m_songIDCache.begin(), m_songIDCache.end());
+
+ CLog::Log(LOGINFO,"PARTY MODE MANAGER: Matching songs = {0}", m_iMatchingSongs);
+ CLog::Log(LOGINFO,"PARTY MODE MANAGER: Party mode enabled!");
+
+ PLAYLIST::Id playlistId = GetPlaylistId();
+
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(playlistId);
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(playlistId, false);
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(playlistId, PLAYLIST::RepeatState::NONE);
+
+ pDialog->SetLine(0, CVariant{m_bIsVideo ? 20252 : 20124});
+ pDialog->Progress();
+ // add initial songs
+ if (!AddRandomSongs())
+ {
+ pDialog->Close();
+ return false;
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "{} time for song fetch: {} ms", __FUNCTION__, duration.count());
+
+ // start playing
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(playlistId);
+ Play(0);
+
+ pDialog->Close();
+ // open now playing window
+ if (StringUtils::EqualsNoCase(m_type, "songs"))
+ {
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_MUSIC_PLAYLIST)
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST);
+ }
+
+ // done
+ m_bEnabled = true;
+ Announce();
+ return true;
+}
+
+void CPartyModeManager::Disable()
+{
+ if (!IsEnabled())
+ return;
+ m_bEnabled = false;
+ Announce();
+ CLog::Log(LOGINFO,"PARTY MODE MANAGER: Party mode disabled.");
+}
+
+void CPartyModeManager::OnSongChange(bool bUpdatePlayed /* = false */)
+{
+ if (!IsEnabled())
+ return;
+ Process();
+ if (bUpdatePlayed)
+ m_iSongsPlayed++;
+}
+
+void CPartyModeManager::AddUserSongs(PLAYLIST::CPlayList& tempList, bool bPlay /* = false */)
+{
+ if (!IsEnabled())
+ return;
+
+ // where do we add?
+ int iAddAt = -1;
+ if (m_iLastUserSong < 0 || bPlay)
+ iAddAt = 1; // under the currently playing song
+ else
+ iAddAt = m_iLastUserSong + 1; // under the last user added song
+
+ int iNewUserSongs = tempList.size();
+ CLog::Log(LOGINFO, "PARTY MODE MANAGER: Adding {} user selected songs at {}", iNewUserSongs,
+ iAddAt);
+
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(GetPlaylistId()).Insert(tempList, iAddAt);
+
+ // update last user added song location
+ if (m_iLastUserSong < 0)
+ m_iLastUserSong = 0;
+ m_iLastUserSong += iNewUserSongs;
+
+ if (bPlay)
+ Play(1);
+}
+
+void CPartyModeManager::AddUserSongs(CFileItemList& tempList, bool bPlay /* = false */)
+{
+ if (!IsEnabled())
+ return;
+
+ // where do we add?
+ int iAddAt = -1;
+ if (m_iLastUserSong < 0 || bPlay)
+ iAddAt = 1; // under the currently playing song
+ else
+ iAddAt = m_iLastUserSong + 1; // under the last user added song
+
+ int iNewUserSongs = tempList.Size();
+ CLog::Log(LOGINFO, "PARTY MODE MANAGER: Adding {} user selected songs at {}", iNewUserSongs,
+ iAddAt);
+
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(GetPlaylistId()).Insert(tempList, iAddAt);
+
+ // update last user added song location
+ if (m_iLastUserSong < 0)
+ m_iLastUserSong = 0;
+ m_iLastUserSong += iNewUserSongs;
+
+ if (bPlay)
+ Play(1);
+}
+
+void CPartyModeManager::Process()
+{
+ ReapSongs();
+ MovePlaying();
+ AddRandomSongs();
+ UpdateStats();
+ SendUpdateMessage();
+}
+
+bool CPartyModeManager::AddRandomSongs()
+{
+ // All songs have been picked, no more to add
+ if (static_cast<int>(m_songIDCache.size()) == m_iMatchingSongsPicked)
+ return false;
+
+ PLAYLIST::CPlayList& playlist = CServiceBroker::GetPlaylistPlayer().GetPlaylist(GetPlaylistId());
+ int iMissingSongs = QUEUE_DEPTH - playlist.size();
+
+ if (iMissingSongs > 0)
+ {
+ // Limit songs fetched to remainder of songID cache
+ iMissingSongs = std::min(iMissingSongs, static_cast<int>(m_songIDCache.size()) - m_iMatchingSongsPicked);
+
+ // Pick iMissingSongs from remaining songID cache
+ std::string sqlWhereMusic = "songview.idSong IN (";
+ std::string sqlWhereVideo = "idMVideo IN (";
+
+ bool bSongs = false;
+ bool bMusicVideos = false;
+ for (int i = m_iMatchingSongsPicked; i < m_iMatchingSongsPicked + iMissingSongs; i++)
+ {
+ std::string song = StringUtils::Format("{},", m_songIDCache[i].second);
+ if (m_songIDCache[i].first == 1)
+ {
+ sqlWhereMusic += song;
+ bSongs = true;
+ }
+ else if (m_songIDCache[i].first == 2)
+ {
+ sqlWhereVideo += song;
+ bMusicVideos = true;
+ }
+ }
+ CFileItemList items;
+
+ if (bSongs)
+ {
+ sqlWhereMusic.back() = ')'; // replace the last comma with closing bracket
+ // Apply random sort (and limit) at db query for efficiency
+ SortDescription SortDescription;
+ SortDescription.sortBy = SortByRandom;
+ SortDescription.limitEnd = QUEUE_DEPTH;
+ CMusicDatabase database;
+ if (database.Open())
+ {
+ database.GetSongsFullByWhere("musicdb://songs/", CDatabase::Filter(sqlWhereMusic),
+ items, SortDescription, true);
+
+ // Get artist and album properties for songs
+ for (auto& item : items)
+ database.SetPropertiesForFileItem(*item);
+ database.Close();
+ }
+ else
+ {
+ OnError(16033, "Party mode could not open database. Aborting.");
+ return false;
+ }
+ }
+ if (bMusicVideos)
+ {
+ sqlWhereVideo.back() = ')'; // replace the last comma with closing bracket
+ CVideoDatabase database;
+ if (database.Open())
+ {
+ database.GetMusicVideosByWhere("videodb://musicvideos/titles/",
+ CDatabase::Filter(sqlWhereVideo), items);
+ database.Close();
+ }
+ else
+ {
+ OnError(16033, "Party mode could not open database. Aborting.");
+ return false;
+ }
+ }
+
+ // Randomize if the list has music videos or they will be in db order
+ // Songs only are already random.
+ if (bMusicVideos)
+ items.Randomize();
+ for (const auto& item : items)
+ {
+ // Update songID cache with order items in playlist
+ if (item->HasMusicInfoTag())
+ {
+ m_songIDCache[m_iMatchingSongsPicked].first = 1;
+ m_songIDCache[m_iMatchingSongsPicked].second = item->GetMusicInfoTag()->GetDatabaseId();
+ }
+ else if (item->HasVideoInfoTag())
+ {
+ m_songIDCache[m_iMatchingSongsPicked].first = 2;
+ m_songIDCache[m_iMatchingSongsPicked].second = item->GetVideoInfoTag()->m_iDbId;
+ }
+ CFileItemPtr pItem(item);
+ Add(pItem); // inc m_iMatchingSongsPicked
+ }
+ }
+ return true;
+}
+
+void CPartyModeManager::Add(CFileItemPtr &pItem)
+{
+ PLAYLIST::CPlayList& playlist = CServiceBroker::GetPlaylistPlayer().GetPlaylist(GetPlaylistId());
+ playlist.Add(pItem);
+ CLog::Log(LOGINFO, "PARTY MODE MANAGER: Adding randomly selected song at {}:[{}]",
+ playlist.size() - 1, pItem->GetPath());
+ m_iMatchingSongsPicked++;
+}
+
+bool CPartyModeManager::ReapSongs()
+{
+ const PLAYLIST::Id playlistId = GetPlaylistId();
+
+ // reap any played songs
+ int iCurrentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ int i=0;
+ while (i < CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId).size())
+ {
+ if (i < iCurrentSong)
+ {
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId).Remove(i);
+ iCurrentSong--;
+ if (i <= m_iLastUserSong)
+ m_iLastUserSong--;
+ }
+ else
+ i++;
+ }
+
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(iCurrentSong);
+ return true;
+}
+
+bool CPartyModeManager::MovePlaying()
+{
+ // move current song to the top if its not there
+ int iCurrentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+
+ if (iCurrentSong > 0)
+ {
+ CLog::Log(LOGINFO, "PARTY MODE MANAGER: Moving currently playing song from {} to 0",
+ iCurrentSong);
+ PLAYLIST::CPlayList& playlist =
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(GetPlaylistId());
+ PLAYLIST::CPlayList playlistTemp;
+ playlistTemp.Add(playlist[iCurrentSong]);
+ playlist.Remove(iCurrentSong);
+ for (int i=0; i<playlist.size(); i++)
+ playlistTemp.Add(playlist[i]);
+ playlist.Clear();
+ for (int i=0; i<playlistTemp.size(); i++)
+ playlist.Add(playlistTemp[i]);
+ }
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(0);
+ return true;
+}
+
+void CPartyModeManager::SendUpdateMessage()
+{
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CPartyModeManager::Play(int iPos)
+{
+ // Move current song to the top if its not there. Playlist filled up below by
+ // OnSongChange call from application GUI_MSG_PLAYBACK_STARTED processing
+ CServiceBroker::GetPlaylistPlayer().Play(iPos, "");
+ CLog::Log(LOGINFO, "PARTY MODE MANAGER: Playing song at {}", iPos);
+}
+
+void CPartyModeManager::OnError(int iError, const std::string& strLogMessage)
+{
+ // open error dialog
+ HELPERS::ShowOKDialogLines(CVariant{257}, CVariant{16030}, CVariant{iError}, CVariant{0});
+ CLog::Log(LOGERROR, "PARTY MODE MANAGER: {}", strLogMessage);
+ m_bEnabled = false;
+ SendUpdateMessage();
+}
+
+int CPartyModeManager::GetSongsPlayed()
+{
+ if (!IsEnabled())
+ return -1;
+ return m_iSongsPlayed;
+}
+
+int CPartyModeManager::GetMatchingSongs()
+{
+ if (!IsEnabled())
+ return -1;
+ return m_iMatchingSongs;
+}
+
+int CPartyModeManager::GetMatchingSongsPicked()
+{
+ if (!IsEnabled())
+ return -1;
+ return m_iMatchingSongsPicked;
+}
+
+int CPartyModeManager::GetMatchingSongsLeft()
+{
+ if (!IsEnabled())
+ return -1;
+ return m_iMatchingSongsLeft;
+}
+
+int CPartyModeManager::GetRelaxedSongs()
+{
+ if (!IsEnabled())
+ return -1;
+ return m_iRelaxedSongs;
+}
+
+int CPartyModeManager::GetRandomSongs()
+{
+ if (!IsEnabled())
+ return -1;
+ return m_iRandomSongs;
+}
+
+PartyModeContext CPartyModeManager::GetType() const
+{
+ if (!IsEnabled())
+ return PARTYMODECONTEXT_UNKNOWN;
+
+ if (m_bIsVideo)
+ return PARTYMODECONTEXT_VIDEO;
+
+ return PARTYMODECONTEXT_MUSIC;
+}
+
+void CPartyModeManager::ClearState()
+{
+ m_iLastUserSong = -1;
+ m_iSongsPlayed = 0;
+ m_iMatchingSongs = 0;
+ m_iMatchingSongsPicked = 0;
+ m_iMatchingSongsLeft = 0;
+ m_iRelaxedSongs = 0;
+ m_iRandomSongs = 0;
+
+ m_songIDCache.clear();
+}
+
+void CPartyModeManager::UpdateStats()
+{
+ m_iMatchingSongsLeft = m_iMatchingSongs - m_iMatchingSongsPicked;
+ m_iRandomSongs = m_iMatchingSongsPicked;
+ m_iRelaxedSongs = 0; // unsupported at this stage
+}
+
+bool CPartyModeManager::IsEnabled(PartyModeContext context /* = PARTYMODECONTEXT_UNKNOWN */) const
+{
+ if (!m_bEnabled) return false;
+ if (context == PARTYMODECONTEXT_VIDEO)
+ return m_bIsVideo;
+ if (context == PARTYMODECONTEXT_MUSIC)
+ return !m_bIsVideo;
+ return true; // unknown, but we're enabled
+}
+
+void CPartyModeManager::Announce()
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ {
+ CVariant data;
+
+ data["player"]["playerid"] = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+ data["property"]["partymode"] = m_bEnabled;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPropertyChanged",
+ data);
+ }
+}
+
+PLAYLIST::Id CPartyModeManager::GetPlaylistId() const
+{
+ return m_bIsVideo ? PLAYLIST::TYPE_VIDEO : PLAYLIST::TYPE_MUSIC;
+}
diff --git a/xbmc/PartyModeManager.h b/xbmc/PartyModeManager.h
new file mode 100644
index 0000000..4acb496
--- /dev/null
+++ b/xbmc/PartyModeManager.h
@@ -0,0 +1,82 @@
+/*
+ * 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 <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CFileItem; typedef std::shared_ptr<CFileItem> CFileItemPtr;
+class CFileItemList;
+namespace PLAYLIST
+{
+using Id = int;
+class CPlayList;
+}
+
+typedef enum
+{
+ PARTYMODECONTEXT_UNKNOWN,
+ PARTYMODECONTEXT_MUSIC,
+ PARTYMODECONTEXT_VIDEO
+} PartyModeContext;
+
+class CPartyModeManager final
+{
+public:
+ CPartyModeManager(void);
+
+ bool Enable(PartyModeContext context=PARTYMODECONTEXT_MUSIC, const std::string& strXspPath = "");
+ void Disable();
+ void Play(int iPos);
+ void OnSongChange(bool bUpdatePlayed = false);
+ void AddUserSongs(PLAYLIST::CPlayList& tempList, bool bPlay = false);
+ void AddUserSongs(CFileItemList& tempList, bool bPlay = false);
+ bool IsEnabled(PartyModeContext context=PARTYMODECONTEXT_UNKNOWN) const;
+ int GetSongsPlayed();
+ int GetMatchingSongs();
+ int GetMatchingSongsPicked();
+ int GetMatchingSongsLeft();
+ int GetRelaxedSongs();
+ int GetRandomSongs();
+ PartyModeContext GetType() const;
+
+private:
+ void Process();
+ bool AddRandomSongs();
+ void Add(CFileItemPtr &pItem);
+ bool ReapSongs();
+ bool MovePlaying();
+ void SendUpdateMessage();
+ void OnError(int iError, const std::string& strLogMessage);
+ void ClearState();
+ void UpdateStats();
+ void Announce();
+ PLAYLIST::Id GetPlaylistId() const;
+
+ // state
+ bool m_bEnabled;
+ bool m_bIsVideo;
+ int m_iLastUserSong;
+ std::string m_type;
+
+ // statistics
+ int m_iSongsPlayed;
+ int m_iMatchingSongs;
+ int m_iMatchingSongsPicked;
+ int m_iMatchingSongsLeft;
+ int m_iRelaxedSongs;
+ int m_iRandomSongs;
+
+ // history
+ std::vector<std::pair<int, int>> m_songIDCache;
+};
+
+extern CPartyModeManager g_partyModeManager;
diff --git a/xbmc/PasswordManager.cpp b/xbmc/PasswordManager.cpp
new file mode 100644
index 0000000..fdbb902
--- /dev/null
+++ b/xbmc/PasswordManager.cpp
@@ -0,0 +1,211 @@
+/*
+ * 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 "PasswordManager.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "profiles/ProfileManager.h"
+#include "profiles/dialogs/GUIDialogLockSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+CPasswordManager &CPasswordManager::GetInstance()
+{
+ static CPasswordManager sPasswordManager;
+ return sPasswordManager;
+}
+
+CPasswordManager::CPasswordManager()
+{
+ m_loaded = false;
+}
+
+bool CPasswordManager::AuthenticateURL(CURL &url)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_loaded)
+ Load();
+ std::string lookup(GetLookupPath(url));
+ std::map<std::string, std::string>::const_iterator it = m_temporaryCache.find(lookup);
+ if (it == m_temporaryCache.end())
+ { // second step, try something that doesn't quite match
+ it = m_temporaryCache.find(GetServerLookup(lookup));
+ }
+ if (it != m_temporaryCache.end())
+ {
+ CURL auth(it->second);
+ url.SetDomain(auth.GetDomain());
+ url.SetPassword(auth.GetPassWord());
+ url.SetUserName(auth.GetUserName());
+ return true;
+ }
+ return false;
+}
+
+bool CPasswordManager::PromptToAuthenticateURL(CURL &url)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ std::string passcode;
+ std::string username = url.GetUserName();
+ std::string domain = url.GetDomain();
+ if (!domain.empty())
+ username = domain + '\\' + username;
+
+ bool saveDetails = false;
+ if (!CGUIDialogLockSettings::ShowAndGetUserAndPassword(username, passcode, url.GetWithoutUserDetails(), &saveDetails))
+ return false;
+
+ // domain/name to domain\name
+ std::string name = username;
+ std::replace(name.begin(), name.end(), '/', '\\');
+
+ if (url.IsProtocol("smb") && name.find('\\') != std::string::npos)
+ {
+ auto pair = StringUtils::Split(name, '\\', 2);
+ url.SetDomain(pair[0]);
+ url.SetUserName(pair[1]);
+ }
+ else
+ {
+ url.SetDomain("");
+ url.SetUserName(username);
+ }
+
+ url.SetPassword(passcode);
+
+ // save the information for later
+ SaveAuthenticatedURL(url, saveDetails);
+ return true;
+}
+
+void CPasswordManager::SaveAuthenticatedURL(const CURL &url, bool saveToProfile)
+{
+ // don't store/save authenticated url if it doesn't contain username
+ if (url.GetUserName().empty())
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ std::string path = GetLookupPath(url);
+ std::string authenticatedPath = url.Get();
+
+ if (!m_loaded)
+ Load();
+
+ if (saveToProfile)
+ { // write to some random XML file...
+ m_permanentCache[path] = authenticatedPath;
+ Save();
+ }
+
+ // save for both this path and more generally the server as a whole.
+ m_temporaryCache[path] = authenticatedPath;
+ m_temporaryCache[GetServerLookup(path)] = authenticatedPath;
+}
+
+bool CPasswordManager::IsURLSupported(const CURL &url)
+{
+ return url.IsProtocol("smb")
+ || url.IsProtocol("nfs")
+ || url.IsProtocol("ftp")
+ || url.IsProtocol("ftps")
+ || url.IsProtocol("sftp")
+ || url.IsProtocol("http")
+ || url.IsProtocol("https")
+ || url.IsProtocol("dav")
+ || url.IsProtocol("davs");
+}
+
+void CPasswordManager::Clear()
+{
+ m_temporaryCache.clear();
+ m_permanentCache.clear();
+ m_loaded = false;
+}
+
+void CPasswordManager::Load()
+{
+ Clear();
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::string passwordsFile = profileManager->GetUserDataItem("passwords.xml");
+ if (CFileUtils::Exists(passwordsFile))
+ {
+ CXBMCTinyXML doc;
+ if (!doc.LoadFile(passwordsFile))
+ {
+ CLog::Log(LOGERROR, "{} - Unable to load: {}, Line {}\n{}", __FUNCTION__, passwordsFile,
+ doc.ErrorRow(), doc.ErrorDesc());
+ return;
+ }
+ const TiXmlElement *root = doc.RootElement();
+ if (root->ValueStr() != "passwords")
+ return;
+ // read in our passwords
+ const TiXmlElement *path = root->FirstChildElement("path");
+ while (path)
+ {
+ std::string from, to;
+ if (XMLUtils::GetPath(path, "from", from) && XMLUtils::GetPath(path, "to", to))
+ {
+ m_permanentCache[from] = to;
+ m_temporaryCache[from] = to;
+ m_temporaryCache[GetServerLookup(from)] = to;
+ }
+ path = path->NextSiblingElement("path");
+ }
+ }
+ m_loaded = true;
+}
+
+void CPasswordManager::Save() const
+{
+ if (m_permanentCache.empty())
+ return;
+
+ CXBMCTinyXML doc;
+ TiXmlElement rootElement("passwords");
+ TiXmlNode *root = doc.InsertEndChild(rootElement);
+ if (!root)
+ return;
+
+ for (std::map<std::string, std::string>::const_iterator i = m_permanentCache.begin(); i != m_permanentCache.end(); ++i)
+ {
+ TiXmlElement pathElement("path");
+ TiXmlNode *path = root->InsertEndChild(pathElement);
+ XMLUtils::SetPath(path, "from", i->first);
+ XMLUtils::SetPath(path, "to", i->second);
+ }
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ doc.SaveFile(profileManager->GetUserDataItem("passwords.xml"));
+}
+
+std::string CPasswordManager::GetLookupPath(const CURL &url) const
+{
+ if (url.IsProtocol("sftp"))
+ return GetServerLookup(url.Get());
+
+ return url.GetProtocol() + "://" + url.GetHostName() + "/" + url.GetShareName();
+}
+
+std::string CPasswordManager::GetServerLookup(const std::string &path) const
+{
+ CURL url(path);
+ return url.GetProtocol() + "://" + url.GetHostName() + "/";
+}
diff --git a/xbmc/PasswordManager.h b/xbmc/PasswordManager.h
new file mode 100644
index 0000000..c93dbd0
--- /dev/null
+++ b/xbmc/PasswordManager.h
@@ -0,0 +1,105 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+#include <map>
+#include <stdint.h>
+#include <string>
+
+class CURL;
+
+/*!
+ \ingroup filesystem
+ \brief Password Manager class for saving authentication details
+
+ Handles access to previously saved passwords for paths, translating normal URLs
+ into authenticated URLs if the user has details about the username and password
+ for a path previously saved. Should be accessed via CPasswordManager::GetInstance()
+ */
+class CPasswordManager
+{
+public:
+ /*!
+ \brief The only way through which the global instance of the CPasswordManager should be accessed.
+ \return the global instance.
+ */
+ static CPasswordManager &GetInstance();
+
+ /*!
+ \brief Authenticate a URL by looking the URL up in the temporary and permanent caches
+ First looks up based on host and share name. If that fails, it will try a match purely
+ on the host name (eg different shares on the same host with the same credentials)
+ \param url a CURL to authenticate
+ \return true if we have details in the cache, false otherwise.
+ \sa CURL
+ */
+ bool AuthenticateURL(CURL &url);
+
+ /*!
+ \brief Prompt for a username and password for the particular URL.
+
+ This routine pops up a dialog, requesting the user enter a username and password
+ to access the given URL. The user may optionally save these details. If saved
+ we write the details into the users profile. If not saved, the details are temporarily
+ stored so that further access no longer requires prompting for authentication.
+
+ \param url the URL to authenticate.
+ \return true if the user entered details, false if the user cancelled the dialog.
+ \sa CURL, SaveAuthenticatedURL
+ */
+ bool PromptToAuthenticateURL(CURL &url);
+
+ /*!
+ \brief Save an authenticated URL.
+
+ This routine stores an authenticated URL in the temporary cache, and optionally
+ saves these details into the users profile.
+
+ \param url the URL to authenticate.
+ \param saveToProfile whether to save in the users profile, defaults to true.
+ \sa CURL, PromptToAuthenticateURL
+ */
+ void SaveAuthenticatedURL(const CURL &url, bool saveToProfile = true);
+
+ /*!
+ \brief Is an URL is supported (by the manager)
+
+ This routine checks that an URL is supported by the manager
+
+ \param url the URL to check.
+ \return true if the URL is supported
+ \sa CURL, IsURLSupported
+ */
+ bool IsURLSupported(const CURL &url);
+
+ /*!
+ \brief Clear any previously cached passwords
+ */
+ void Clear();
+
+private:
+ // private construction, and no assignments; use the provided singleton methods
+ CPasswordManager();
+ CPasswordManager(const CPasswordManager&) = delete;
+ CPasswordManager& operator=(CPasswordManager const&) = delete;
+ ~CPasswordManager() = default;
+
+ void Load();
+ void Save() const;
+ std::string GetLookupPath(const CURL &url) const;
+ std::string GetServerLookup(const std::string &path) const;
+
+ std::map<std::string, std::string> m_temporaryCache;
+ std::map<std::string, std::string> m_permanentCache;
+ bool m_loaded;
+
+ CCriticalSection m_critSection;
+};
diff --git a/xbmc/PlayListPlayer.cpp b/xbmc/PlayListPlayer.cpp
new file mode 100644
index 0000000..b86e1cf
--- /dev/null
+++ b/xbmc/PlayListPlayer.cpp
@@ -0,0 +1,1060 @@
+/*
+ * 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 "PlayListPlayer.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "PartyModeManager.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationPowerHandling.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "filesystem/PluginDirectory.h"
+#include "filesystem/VideoDatabaseFile.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/tags/MusicInfoTag.h"
+#include "playlists/PlayList.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+using namespace PLAYLIST;
+using namespace KODI::MESSAGING;
+
+CPlayListPlayer::CPlayListPlayer(void)
+{
+ m_PlaylistMusic = new CPlayList(TYPE_MUSIC);
+ m_PlaylistVideo = new CPlayList(TYPE_VIDEO);
+ m_PlaylistEmpty = new CPlayList;
+ m_iCurrentSong = -1;
+ m_bPlayedFirstFile = false;
+ m_bPlaybackStarted = false;
+ m_iFailedSongs = 0;
+ m_failedSongsStart = std::chrono::steady_clock::now();
+}
+
+CPlayListPlayer::~CPlayListPlayer(void)
+{
+ Clear();
+ delete m_PlaylistMusic;
+ delete m_PlaylistVideo;
+ delete m_PlaylistEmpty;
+}
+
+bool CPlayListPlayer::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_PREV_ITEM && !IsSingleItemNonRepeatPlaylist())
+ {
+ PlayPrevious();
+ return true;
+ }
+ else if (action.GetID() == ACTION_NEXT_ITEM && !IsSingleItemNonRepeatPlaylist())
+ {
+ PlayNext();
+ return true;
+ }
+ else
+ return false;
+}
+
+bool CPlayListPlayer::OnMessage(CGUIMessage &message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_NOTIFY_ALL:
+ if (message.GetParam1() == GUI_MSG_UPDATE_ITEM && message.GetItem())
+ {
+ // update the items in our playlist(s) if necessary
+ for (Id playlistId : {TYPE_MUSIC, TYPE_VIDEO})
+ {
+ CPlayList& playlist = GetPlaylist(playlistId);
+ CFileItemPtr item = std::static_pointer_cast<CFileItem>(message.GetItem());
+ playlist.UpdateItem(item.get());
+ }
+ }
+ break;
+ case GUI_MSG_PLAYBACK_STOPPED:
+ {
+ if (m_iCurrentPlayList != TYPE_NONE && m_bPlaybackStarted)
+ {
+ CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_STOPPED, 0, 0, m_iCurrentPlayList, m_iCurrentSong);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ Reset();
+ m_iCurrentPlayList = TYPE_NONE;
+ return true;
+ }
+ }
+ break;
+ case GUI_MSG_PLAYBACK_STARTED:
+ {
+ m_bPlaybackStarted = true;
+ }
+ break;
+ }
+
+ return false;
+}
+
+int CPlayListPlayer::GetNextSong(int offset) const
+{
+ if (m_iCurrentPlayList == TYPE_NONE)
+ return -1;
+
+ const CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
+ if (playlist.size() <= 0)
+ return -1;
+
+ int song = m_iCurrentSong;
+
+ // party mode
+ if (g_partyModeManager.IsEnabled() && GetCurrentPlaylist() == TYPE_MUSIC)
+ return song + offset;
+
+ // wrap around in the case of repeating
+ if (RepeatedOne(m_iCurrentPlayList))
+ return song;
+
+ song += offset;
+ if (song >= playlist.size() && Repeated(m_iCurrentPlayList))
+ song %= playlist.size();
+
+ return song;
+}
+
+int CPlayListPlayer::GetNextSong()
+{
+ if (m_iCurrentPlayList == TYPE_NONE)
+ return -1;
+ CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
+ if (playlist.size() <= 0)
+ return -1;
+ int iSong = m_iCurrentSong;
+
+ // party mode
+ if (g_partyModeManager.IsEnabled() && GetCurrentPlaylist() == TYPE_MUSIC)
+ return iSong + 1;
+
+ // if repeat one, keep playing the current song if its valid
+ if (RepeatedOne(m_iCurrentPlayList))
+ {
+ // otherwise immediately abort playback
+ if (m_iCurrentSong >= 0 && m_iCurrentSong < playlist.size() && playlist[m_iCurrentSong]->GetProperty("unplayable").asBoolean())
+ {
+ CLog::Log(LOGERROR, "Playlist Player: RepeatOne stuck on unplayable item: {}, path [{}]",
+ m_iCurrentSong, playlist[m_iCurrentSong]->GetPath());
+ CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_STOPPED, 0, 0, m_iCurrentPlayList, m_iCurrentSong);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ Reset();
+ m_iCurrentPlayList = TYPE_NONE;
+ return -1;
+ }
+ return iSong;
+ }
+
+ // if we've gone beyond the playlist and repeat all is enabled,
+ // then we clear played status and wrap around
+ iSong++;
+ if (iSong >= playlist.size() && Repeated(m_iCurrentPlayList))
+ iSong = 0;
+
+ return iSong;
+}
+
+bool CPlayListPlayer::PlayNext(int offset, bool bAutoPlay)
+{
+ int iSong = GetNextSong(offset);
+ const CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
+
+ if ((iSong < 0) || (iSong >= playlist.size()) || (playlist.GetPlayable() <= 0))
+ {
+ if(!bAutoPlay)
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(559), g_localizeStrings.Get(34201));
+
+ CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_STOPPED, 0, 0, m_iCurrentPlayList, m_iCurrentSong);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ Reset();
+ m_iCurrentPlayList = TYPE_NONE;
+ return false;
+ }
+
+ return Play(iSong, "", false);
+}
+
+bool CPlayListPlayer::PlayPrevious()
+{
+ if (m_iCurrentPlayList == TYPE_NONE)
+ return false;
+
+ const CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
+ int iSong = m_iCurrentSong;
+
+ if (!RepeatedOne(m_iCurrentPlayList))
+ iSong--;
+
+ if (iSong < 0 && Repeated(m_iCurrentPlayList))
+ iSong = playlist.size() - 1;
+
+ if (iSong < 0 || playlist.size() <= 0)
+ {
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(559), g_localizeStrings.Get(34202));
+ return false;
+ }
+
+ return Play(iSong, "", false, true);
+}
+
+bool CPlayListPlayer::IsSingleItemNonRepeatPlaylist() const
+{
+ const CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
+ return (playlist.size() <= 1 && !RepeatedOne(m_iCurrentPlayList) && !Repeated(m_iCurrentPlayList));
+}
+
+bool CPlayListPlayer::Play()
+{
+ if (m_iCurrentPlayList == TYPE_NONE)
+ return false;
+
+ const CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
+ if (playlist.size() <= 0)
+ return false;
+
+ return Play(0, "");
+}
+
+bool CPlayListPlayer::PlaySongId(int songId)
+{
+ if (m_iCurrentPlayList == TYPE_NONE)
+ return false;
+
+ CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
+ if (playlist.size() <= 0)
+ return Play();
+
+ for (int i = 0; i < playlist.size(); i++)
+ {
+ if (playlist[i]->HasMusicInfoTag() && playlist[i]->GetMusicInfoTag()->GetDatabaseId() == songId)
+ return Play(i, "");
+ }
+ return Play();
+}
+
+bool CPlayListPlayer::Play(const CFileItemPtr& pItem, const std::string& player)
+{
+ Id playlistId;
+ bool isVideo{pItem->IsVideo()};
+ bool isAudio{pItem->IsAudio()};
+
+ if (isAudio && !isVideo)
+ playlistId = TYPE_MUSIC;
+ else if (isVideo && !isAudio)
+ playlistId = TYPE_VIDEO;
+ else if (pItem->HasProperty("playlist_type_hint"))
+ {
+ // There are two main cases that can fall here:
+ // - If an extension is set on both audio / video extension lists example .strm
+ // see GetFileExtensionProvider() -> GetVideoExtensions() / GetAudioExtensions()
+ // When you play the .strm containing single path, cause that
+ // IsVideo() and IsAudio() methods both return true
+ //
+ // - When you play a playlist (e.g. .m3u / .strm) containing multiple paths,
+ // and the path played is generic (e.g.without extension) and have no properties
+ // to detect the media type, IsVideo() / IsAudio() both return false
+ //
+ // for these cases the type is unknown so we rely on the hint
+ playlistId = pItem->GetProperty("playlist_type_hint").asInteger32(TYPE_NONE);
+ }
+ else
+ {
+ CLog::LogF(LOGWARNING, "ListItem type must be audio or video type. The type can be specified "
+ "by using ListItem::getVideoInfoTag or ListItem::getMusicInfoTag, in "
+ "the case of playlist entries by adding #KODIPROP mimetype value.");
+ return false;
+ }
+
+ ClearPlaylist(playlistId);
+ Reset();
+ SetCurrentPlaylist(playlistId);
+ Add(playlistId, pItem);
+
+ return Play(0, player);
+}
+
+bool CPlayListPlayer::Play(int iSong,
+ const std::string& player,
+ bool bAutoPlay /* = false */,
+ bool bPlayPrevious /* = false */)
+{
+ if (m_iCurrentPlayList == TYPE_NONE)
+ return false;
+
+ CPlayList& playlist = GetPlaylist(m_iCurrentPlayList);
+ if (playlist.size() <= 0)
+ return false;
+ if (iSong < 0)
+ iSong = 0;
+ if (iSong >= playlist.size())
+ iSong = playlist.size() - 1;
+
+ // check if the item itself is a playlist, and can be expanded
+ // only allow a few levels, this could end up in a loop
+ // if they refer to each other in a loop
+ for (int i=0; i<5; i++)
+ {
+ if(!playlist.Expand(iSong))
+ break;
+ }
+
+ m_iCurrentSong = iSong;
+ CFileItemPtr item = playlist[m_iCurrentSong];
+ if (item->IsVideoDb() && !item->HasVideoInfoTag())
+ *(item->GetVideoInfoTag()) = XFILE::CVideoDatabaseFile::GetVideoTag(CURL(item->GetDynPath()));
+
+ playlist.SetPlayed(true);
+
+ m_bPlaybackStarted = false;
+
+ const auto playAttempt = std::chrono::steady_clock::now();
+ bool ret = g_application.PlayFile(*item, player, bAutoPlay);
+ if (!ret)
+ {
+ CLog::Log(LOGERROR, "Playlist Player: skipping unplayable item: {}, path [{}]", m_iCurrentSong,
+ CURL::GetRedacted(item->GetDynPath()));
+ playlist.SetUnPlayable(m_iCurrentSong);
+
+ // abort on 100 failed CONSECUTIVE songs
+ if (!m_iFailedSongs)
+ m_failedSongsStart = playAttempt;
+ m_iFailedSongs++;
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ auto now = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_failedSongsStart);
+
+ if ((m_iFailedSongs >= advancedSettings->m_playlistRetries &&
+ advancedSettings->m_playlistRetries >= 0) ||
+ ((duration.count() >=
+ static_cast<unsigned int>(advancedSettings->m_playlistTimeout) * 1000) &&
+ advancedSettings->m_playlistTimeout))
+ {
+ CLog::Log(LOGDEBUG,"Playlist Player: one or more items failed to play... aborting playback");
+
+ // open error dialog
+ HELPERS::ShowOKDialogText(CVariant{16026}, CVariant{16027});
+
+ CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_STOPPED, 0, 0, m_iCurrentPlayList, m_iCurrentSong);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ Reset();
+ GetPlaylist(m_iCurrentPlayList).Clear();
+ m_iCurrentPlayList = TYPE_NONE;
+ m_iFailedSongs = 0;
+ m_failedSongsStart = std::chrono::steady_clock::now();
+ return false;
+ }
+
+ // how many playable items are in the playlist?
+ if (playlist.GetPlayable() > 0)
+ {
+ return bPlayPrevious ? PlayPrevious() : PlayNext();
+ }
+ // none? then abort playback
+ else
+ {
+ CLog::Log(LOGDEBUG,"Playlist Player: no more playable items... aborting playback");
+ CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_STOPPED, 0, 0, m_iCurrentPlayList, m_iCurrentSong);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ Reset();
+ m_iCurrentPlayList = TYPE_NONE;
+ return false;
+ }
+ }
+
+ // reset the start offset of this item
+ if (item->GetStartOffset() == STARTOFFSET_RESUME)
+ item->SetStartOffset(0);
+
+ //! @todo - move the above failure logic and the below success logic
+ //! to callbacks instead so we don't rely on the return value
+ //! of PlayFile()
+
+ // consecutive error counter so reset if the current item is playing
+ m_iFailedSongs = 0;
+ m_failedSongsStart = std::chrono::steady_clock::now();
+ m_bPlayedFirstFile = true;
+ return true;
+}
+
+void CPlayListPlayer::SetCurrentSong(int iSong)
+{
+ if (iSong >= -1 && iSong < GetPlaylist(m_iCurrentPlayList).size())
+ m_iCurrentSong = iSong;
+}
+
+int CPlayListPlayer::GetCurrentSong() const
+{
+ return m_iCurrentSong;
+}
+
+Id CPlayListPlayer::GetCurrentPlaylist() const
+{
+ return m_iCurrentPlayList;
+}
+
+void CPlayListPlayer::SetCurrentPlaylist(Id playlistId)
+{
+ if (playlistId == m_iCurrentPlayList)
+ return;
+
+ // changing the current playlist while party mode is on
+ // disables party mode
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Disable();
+
+ m_iCurrentPlayList = playlistId;
+ m_bPlayedFirstFile = false;
+}
+
+void CPlayListPlayer::ClearPlaylist(Id playlistId)
+{
+ // clear our applications playlist file
+ g_application.m_strPlayListFile.clear();
+
+ CPlayList& playlist = GetPlaylist(playlistId);
+ playlist.Clear();
+
+ // its likely that the playlist changed
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+}
+
+CPlayList& CPlayListPlayer::GetPlaylist(Id playlistId)
+{
+ switch (playlistId)
+ {
+ case TYPE_MUSIC:
+ return *m_PlaylistMusic;
+ break;
+ case TYPE_VIDEO:
+ return *m_PlaylistVideo;
+ break;
+ default:
+ m_PlaylistEmpty->Clear();
+ return *m_PlaylistEmpty;
+ break;
+ }
+}
+
+const CPlayList& CPlayListPlayer::GetPlaylist(Id playlistId) const
+{
+ switch (playlistId)
+ {
+ case TYPE_MUSIC:
+ return *m_PlaylistMusic;
+ break;
+ case TYPE_VIDEO:
+ return *m_PlaylistVideo;
+ break;
+ default:
+ // NOTE: This playlist may not be empty if the caller of the non-const version alters it!
+ return *m_PlaylistEmpty;
+ break;
+ }
+}
+
+int CPlayListPlayer::RemoveDVDItems()
+{
+ int nRemovedM = m_PlaylistMusic->RemoveDVDItems();
+ int nRemovedV = m_PlaylistVideo->RemoveDVDItems();
+
+ return nRemovedM + nRemovedV;
+}
+
+void CPlayListPlayer::Reset()
+{
+ m_iCurrentSong = -1;
+ m_bPlayedFirstFile = false;
+ m_bPlaybackStarted = false;
+
+ // its likely that the playlist changed
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+}
+
+bool CPlayListPlayer::HasPlayedFirstFile() const
+{
+ return m_bPlayedFirstFile;
+}
+
+bool CPlayListPlayer::Repeated(Id playlistId) const
+{
+ const auto repStatePos = m_repeatState.find(playlistId);
+ if (repStatePos != m_repeatState.end())
+ return repStatePos->second == RepeatState::ALL;
+ return false;
+}
+
+bool CPlayListPlayer::RepeatedOne(Id playlistId) const
+{
+ const auto repStatePos = m_repeatState.find(playlistId);
+ if (repStatePos != m_repeatState.end())
+ return (repStatePos->second == RepeatState::ONE);
+ return false;
+}
+
+void CPlayListPlayer::SetShuffle(Id playlistId, bool bYesNo, bool bNotify /* = false */)
+{
+ if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
+ return;
+
+ // disable shuffle in party mode
+ if (g_partyModeManager.IsEnabled() && playlistId == TYPE_MUSIC)
+ return;
+
+ // do we even need to do anything?
+ if (bYesNo != IsShuffled(playlistId))
+ {
+ // save the order value of the current song so we can use it find its new location later
+ int iOrder = -1;
+ CPlayList& playlist = GetPlaylist(playlistId);
+ if (m_iCurrentSong >= 0 && m_iCurrentSong < playlist.size())
+ iOrder = playlist[m_iCurrentSong]->m_iprogramCount;
+
+ // shuffle or unshuffle as necessary
+ if (bYesNo)
+ playlist.Shuffle();
+ else
+ playlist.UnShuffle();
+
+ if (bNotify)
+ {
+ std::string shuffleStr =
+ StringUtils::Format("{}: {}", g_localizeStrings.Get(191),
+ g_localizeStrings.Get(bYesNo ? 593 : 591)); // Shuffle: All/Off
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(559), shuffleStr);
+ }
+
+ // find the previous order value and fix the current song marker
+ if (iOrder >= 0)
+ {
+ int iIndex = playlist.FindOrder(iOrder);
+ if (iIndex >= 0)
+ m_iCurrentSong = iIndex;
+ // if iIndex < 0, something unexpected happened
+ // so dont do anything
+ }
+ }
+
+ // its likely that the playlist changed
+ if (CServiceBroker::GetGUI() != nullptr)
+ {
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+
+ AnnouncePropertyChanged(playlistId, "shuffled", IsShuffled(playlistId));
+}
+
+bool CPlayListPlayer::IsShuffled(Id playlistId) const
+{
+ // even if shuffled, party mode says its not
+ if (g_partyModeManager.IsEnabled() && playlistId == TYPE_MUSIC)
+ return false;
+
+ if (playlistId == TYPE_MUSIC || playlistId == TYPE_VIDEO)
+ return GetPlaylist(playlistId).IsShuffled();
+
+ return false;
+}
+
+void CPlayListPlayer::SetRepeat(Id playlistId, RepeatState state, bool bNotify /* = false */)
+{
+ if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
+ return;
+
+ // disable repeat in party mode
+ if (g_partyModeManager.IsEnabled() && playlistId == TYPE_MUSIC)
+ state = RepeatState::NONE;
+
+ // notify the user if there was a change in the repeat state
+ if (m_repeatState[playlistId] != state && bNotify)
+ {
+ int iLocalizedString;
+ if (state == RepeatState::NONE)
+ iLocalizedString = 595; // Repeat: Off
+ else if (state == RepeatState::ONE)
+ iLocalizedString = 596; // Repeat: One
+ else
+ iLocalizedString = 597; // Repeat: All
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(559), g_localizeStrings.Get(iLocalizedString));
+ }
+
+ m_repeatState[playlistId] = state;
+
+ CVariant data;
+ switch (state)
+ {
+ case RepeatState::ONE:
+ data = "one";
+ break;
+ case RepeatState::ALL:
+ data = "all";
+ break;
+ default:
+ data = "off";
+ break;
+ }
+
+ // its likely that the playlist changed
+ if (CServiceBroker::GetGUI() != nullptr)
+ {
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+
+ AnnouncePropertyChanged(playlistId, "repeat", data);
+}
+
+RepeatState CPlayListPlayer::GetRepeat(Id playlistId) const
+{
+ const auto repStatePos = m_repeatState.find(playlistId);
+ if (repStatePos != m_repeatState.end())
+ return repStatePos->second;
+ return RepeatState::NONE;
+}
+
+void CPlayListPlayer::ReShuffle(Id playlistId, int iPosition)
+{
+ // playlist has not played yet so shuffle the entire list
+ // (this only really works for new video playlists)
+ if (!GetPlaylist(playlistId).WasPlayed())
+ {
+ GetPlaylist(playlistId).Shuffle();
+ }
+ // we're trying to shuffle new items into the currently playing playlist
+ // so we shuffle starting at two positions below the current item
+ else if (playlistId == m_iCurrentPlayList)
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if ((appPlayer->IsPlayingAudio() && playlistId == TYPE_MUSIC) ||
+ (appPlayer->IsPlayingVideo() && playlistId == TYPE_VIDEO))
+ {
+ GetPlaylist(playlistId).Shuffle(m_iCurrentSong + 2);
+ }
+ }
+ // otherwise, shuffle from the passed position
+ // which is the position of the first new item added
+ else
+ {
+ GetPlaylist(playlistId).Shuffle(iPosition);
+ }
+}
+
+void CPlayListPlayer::Add(Id playlistId, const CPlayList& playlist)
+{
+ if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
+ return;
+ CPlayList& list = GetPlaylist(playlistId);
+ int iSize = list.size();
+ list.Add(playlist);
+ if (list.IsShuffled())
+ ReShuffle(playlistId, iSize);
+}
+
+void CPlayListPlayer::Add(Id playlistId, const CFileItemPtr& pItem)
+{
+ if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
+ return;
+ CPlayList& list = GetPlaylist(playlistId);
+ int iSize = list.size();
+ list.Add(pItem);
+ if (list.IsShuffled())
+ ReShuffle(playlistId, iSize);
+}
+
+void CPlayListPlayer::Add(Id playlistId, const CFileItemList& items)
+{
+ if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
+ return;
+ CPlayList& list = GetPlaylist(playlistId);
+ int iSize = list.size();
+ list.Add(items);
+ if (list.IsShuffled())
+ ReShuffle(playlistId, iSize);
+
+ // its likely that the playlist changed
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+}
+
+void CPlayListPlayer::Insert(Id playlistId, const CPlayList& playlist, int iIndex)
+{
+ if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
+ return;
+ CPlayList& list = GetPlaylist(playlistId);
+ int iSize = list.size();
+ list.Insert(playlist, iIndex);
+ if (list.IsShuffled())
+ ReShuffle(playlistId, iSize);
+ else if (m_iCurrentPlayList == playlistId && m_iCurrentSong >= iIndex)
+ m_iCurrentSong++;
+}
+
+void CPlayListPlayer::Insert(Id playlistId, const CFileItemPtr& pItem, int iIndex)
+{
+ if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
+ return;
+ CPlayList& list = GetPlaylist(playlistId);
+ int iSize = list.size();
+ list.Insert(pItem, iIndex);
+ if (list.IsShuffled())
+ ReShuffle(playlistId, iSize);
+ else if (m_iCurrentPlayList == playlistId && m_iCurrentSong >= iIndex)
+ m_iCurrentSong++;
+}
+
+void CPlayListPlayer::Insert(Id playlistId, const CFileItemList& items, int iIndex)
+{
+ if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
+ return;
+ CPlayList& list = GetPlaylist(playlistId);
+ int iSize = list.size();
+ list.Insert(items, iIndex);
+ if (list.IsShuffled())
+ ReShuffle(playlistId, iSize);
+ else if (m_iCurrentPlayList == playlistId && m_iCurrentSong >= iIndex)
+ m_iCurrentSong++;
+
+ // its likely that the playlist changed
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+}
+
+void CPlayListPlayer::Remove(Id playlistId, int iPosition)
+{
+ if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
+ return;
+ CPlayList& list = GetPlaylist(playlistId);
+ list.Remove(iPosition);
+ if (m_iCurrentPlayList == playlistId && m_iCurrentSong >= iPosition)
+ m_iCurrentSong--;
+
+ // its likely that the playlist changed
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+}
+
+void CPlayListPlayer::Clear()
+{
+ if (m_PlaylistMusic)
+ m_PlaylistMusic->Clear();
+ if (m_PlaylistVideo)
+ m_PlaylistVideo->Clear();
+ if (m_PlaylistEmpty)
+ m_PlaylistEmpty->Clear();
+}
+
+void CPlayListPlayer::Swap(Id playlistId, int indexItem1, int indexItem2)
+{
+ if (playlistId != TYPE_MUSIC && playlistId != TYPE_VIDEO)
+ return;
+
+ CPlayList& list = GetPlaylist(playlistId);
+ if (list.Swap(indexItem1, indexItem2) && playlistId == m_iCurrentPlayList)
+ {
+ if (m_iCurrentSong == indexItem1)
+ m_iCurrentSong = indexItem2;
+ else if (m_iCurrentSong == indexItem2)
+ m_iCurrentSong = indexItem1;
+ }
+
+ // its likely that the playlist changed
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+}
+
+void CPlayListPlayer::AnnouncePropertyChanged(Id playlistId,
+ const std::string& strProperty,
+ const CVariant& value)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ if (strProperty.empty() || value.isNull() ||
+ (playlistId == TYPE_VIDEO && !appPlayer->IsPlayingVideo()) ||
+ (playlistId == TYPE_MUSIC && !appPlayer->IsPlayingAudio()))
+ return;
+
+ CVariant data;
+ data["player"]["playerid"] = playlistId;
+ data["property"][strProperty] = value;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPropertyChanged",
+ data);
+}
+
+int PLAYLIST::CPlayListPlayer::GetMessageMask()
+{
+ return TMSG_MASK_PLAYLISTPLAYER;
+}
+
+void PLAYLIST::CPlayListPlayer::OnApplicationMessage(KODI::MESSAGING::ThreadMessage* pMsg)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ auto wakeScreensaver = []() {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+ appPower->WakeUpScreenSaverAndDPMS();
+ };
+
+ switch (pMsg->dwMessage)
+ {
+ case TMSG_PLAYLISTPLAYER_PLAY:
+ if (pMsg->param1 != -1)
+ Play(pMsg->param1, "");
+ else
+ Play();
+ break;
+
+ case TMSG_PLAYLISTPLAYER_PLAY_SONG_ID:
+ if (pMsg->param1 != -1)
+ {
+ bool *result = (bool*)pMsg->lpVoid;
+ *result = PlaySongId(pMsg->param1);
+ }
+ else
+ Play();
+ break;
+
+ case TMSG_PLAYLISTPLAYER_NEXT:
+ PlayNext();
+ break;
+
+ case TMSG_PLAYLISTPLAYER_PREV:
+ PlayPrevious();
+ break;
+
+ case TMSG_PLAYLISTPLAYER_ADD:
+ if (pMsg->lpVoid)
+ {
+ CFileItemList *list = static_cast<CFileItemList*>(pMsg->lpVoid);
+
+ Add(pMsg->param1, (*list));
+ delete list;
+ }
+ break;
+
+ case TMSG_PLAYLISTPLAYER_INSERT:
+ if (pMsg->lpVoid)
+ {
+ CFileItemList *list = static_cast<CFileItemList*>(pMsg->lpVoid);
+ Insert(pMsg->param1, (*list), pMsg->param2);
+ delete list;
+ }
+ break;
+
+ case TMSG_PLAYLISTPLAYER_REMOVE:
+ if (pMsg->param1 != -1)
+ Remove(pMsg->param1, pMsg->param2);
+ break;
+
+ case TMSG_PLAYLISTPLAYER_CLEAR:
+ ClearPlaylist(pMsg->param1);
+ break;
+
+ case TMSG_PLAYLISTPLAYER_SHUFFLE:
+ SetShuffle(pMsg->param1, pMsg->param2 > 0);
+ break;
+
+ case TMSG_PLAYLISTPLAYER_REPEAT:
+ SetRepeat(pMsg->param1, static_cast<RepeatState>(pMsg->param2));
+ break;
+
+ case TMSG_PLAYLISTPLAYER_GET_ITEMS:
+ if (pMsg->lpVoid)
+ {
+ PLAYLIST::CPlayList playlist = GetPlaylist(pMsg->param1);
+ CFileItemList *list = static_cast<CFileItemList*>(pMsg->lpVoid);
+
+ for (int i = 0; i < playlist.size(); i++)
+ list->Add(std::make_shared<CFileItem>(*playlist[i]));
+ }
+ break;
+
+ case TMSG_PLAYLISTPLAYER_SWAP:
+ if (pMsg->lpVoid)
+ {
+ auto indexes = static_cast<std::vector<int>*>(pMsg->lpVoid);
+ if (indexes->size() == 2)
+ Swap(pMsg->param1, indexes->at(0), indexes->at(1));
+ delete indexes;
+ }
+ break;
+
+ case TMSG_MEDIA_PLAY:
+ {
+ wakeScreensaver();
+
+ // first check if we were called from the PlayFile() function
+ if (pMsg->lpVoid && pMsg->param2 == 0)
+ {
+ // Discard the current playlist, if TMSG_MEDIA_PLAY gets posted with just a single item.
+ // Otherwise items may fail to play, when started while a playlist is playing.
+ Reset();
+
+ CFileItem *item = static_cast<CFileItem*>(pMsg->lpVoid);
+ g_application.PlayFile(*item, "", pMsg->param1 != 0);
+ delete item;
+ return;
+ }
+
+ //g_application.StopPlaying();
+ // play file
+ if (pMsg->lpVoid)
+ {
+ CFileItemList *list = static_cast<CFileItemList*>(pMsg->lpVoid);
+
+ if (list->Size() > 0)
+ {
+ Id playlistId = TYPE_MUSIC;
+ for (int i = 0; i < list->Size(); i++)
+ {
+ if ((*list)[i]->IsVideo())
+ {
+ playlistId = TYPE_VIDEO;
+ break;
+ }
+ }
+
+ ClearPlaylist(playlistId);
+ SetCurrentPlaylist(playlistId);
+ if (list->Size() == 1 && !(*list)[0]->IsPlayList())
+ {
+ CFileItemPtr item = (*list)[0];
+ // if the item is a plugin we need to resolve the URL to ensure the infotags are filled.
+ if (URIUtils::HasPluginPath(*item) &&
+ !XFILE::CPluginDirectory::GetResolvedPluginResult(*item))
+ {
+ return;
+ }
+ if (item->IsAudio() || item->IsVideo())
+ Play(item, pMsg->strParam);
+ else
+ g_application.PlayMedia(*item, pMsg->strParam, playlistId);
+ }
+ else
+ {
+ // Handle "shuffled" option if present
+ if (list->HasProperty("shuffled") && list->GetProperty("shuffled").isBoolean())
+ SetShuffle(playlistId, list->GetProperty("shuffled").asBoolean(), false);
+ // Handle "repeat" option if present
+ if (list->HasProperty("repeat") && list->GetProperty("repeat").isInteger())
+ SetRepeat(playlistId, static_cast<RepeatState>(list->GetProperty("repeat").asInteger()),
+ false);
+
+ Add(playlistId, (*list));
+ Play(pMsg->param1, pMsg->strParam);
+ }
+ }
+
+ delete list;
+ }
+ else if (pMsg->param1 == TYPE_MUSIC || pMsg->param1 == TYPE_VIDEO)
+ {
+ if (GetCurrentPlaylist() != pMsg->param1)
+ SetCurrentPlaylist(pMsg->param1);
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_PLAY, pMsg->param2);
+ }
+ }
+ break;
+
+ case TMSG_MEDIA_RESTART:
+ g_application.Restart(true);
+ break;
+
+ case TMSG_MEDIA_STOP:
+ {
+ // restore to previous window if needed
+ bool stopSlideshow = true;
+ bool stopVideo = true;
+ bool stopMusic = true;
+
+ Id playlistId = pMsg->param1;
+ if (playlistId != TYPE_NONE)
+ {
+ stopSlideshow = (playlistId == TYPE_PICTURE);
+ stopVideo = (playlistId == TYPE_VIDEO);
+ stopMusic = (playlistId == TYPE_MUSIC);
+ }
+
+ if ((stopSlideshow && CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) ||
+ (stopVideo && CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_VIDEO) ||
+ (stopVideo && CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_GAME) ||
+ (stopMusic && CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VISUALISATION))
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+
+ wakeScreensaver();
+
+ // stop playing file
+ if (appPlayer->IsPlaying())
+ g_application.StopPlaying();
+ }
+ break;
+
+ case TMSG_MEDIA_PAUSE:
+ if (appPlayer->HasPlayer())
+ {
+ wakeScreensaver();
+ appPlayer->Pause();
+ }
+ break;
+
+ case TMSG_MEDIA_UNPAUSE:
+ if (appPlayer->IsPausedPlayback())
+ {
+ wakeScreensaver();
+ appPlayer->Pause();
+ }
+ break;
+
+ case TMSG_MEDIA_PAUSE_IF_PLAYING:
+ if (appPlayer->IsPlaying() && !appPlayer->IsPaused())
+ {
+ wakeScreensaver();
+ appPlayer->Pause();
+ }
+ break;
+
+ case TMSG_MEDIA_SEEK_TIME:
+ {
+ if (appPlayer->IsPlaying() || appPlayer->IsPaused())
+ appPlayer->SeekTime(pMsg->param3);
+
+ break;
+ }
+ default:
+ break;
+ }
+}
diff --git a/xbmc/PlayListPlayer.h b/xbmc/PlayListPlayer.h
new file mode 100644
index 0000000..e394e29
--- /dev/null
+++ b/xbmc/PlayListPlayer.h
@@ -0,0 +1,206 @@
+/*
+ * 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 "guilib/IMsgTargetCallback.h"
+#include "messaging/IMessageTarget.h"
+#include "playlists/PlayListTypes.h"
+
+#include <chrono>
+#include <map>
+#include <memory>
+
+class CAction;
+class CFileItem; typedef std::shared_ptr<CFileItem> CFileItemPtr;
+class CFileItemList;
+
+class CVariant;
+
+namespace PLAYLIST
+{
+class CPlayList;
+
+class CPlayListPlayer : public IMsgTargetCallback,
+ public KODI::MESSAGING::IMessageTarget
+{
+
+public:
+ CPlayListPlayer(void);
+ ~CPlayListPlayer(void) override;
+ bool OnMessage(CGUIMessage &message) override;
+
+ int GetMessageMask() override;
+ void OnApplicationMessage(KODI::MESSAGING::ThreadMessage* pMsg) override;
+
+ /*! \brief Play the next (or another) entry in the current playlist
+ \param offset The offset from the current entry (defaults to 1, i.e. the next entry).
+ \param autoPlay Whether we should start playing if not already (defaults to false).
+ */
+ bool PlayNext(int offset = 1, bool autoPlay = false);
+
+ /*! \brief Play the previous entry in the current playlist
+ \sa PlayNext
+ */
+ bool PlayPrevious();
+ bool PlaySongId(int songId);
+ bool Play();
+
+ /*!
+ * \brief Creates a new playlist for an item and starts playing it
+ * \param pItem The item to play.
+ * \param player The player name.
+ * \return True if has success, otherwise false.
+ */
+ bool Play(const CFileItemPtr& pItem, const std::string& player);
+
+ /*! \brief Start playing a particular entry in the current playlist
+ \param index the index of the item to play. This value is modified to ensure it lies within the current playlist.
+ \param replace whether this item should replace the currently playing item. See CApplication::PlayFile (defaults to false).
+ \param playPreviousOnFail whether to go back to the previous item if playback fails (default to false)
+ */
+ bool Play(int index,
+ const std::string& player,
+ bool replace = false,
+ bool playPreviousOnFail = false);
+
+ /*! \brief Returns the index of the current item in active playlist.
+ \return Current item in the active playlist.
+ \sa SetCurrentSong
+ */
+ int GetCurrentSong() const;
+
+ /*! \brief Change the current item in the active playlist.
+ \param index item index in playlist. Set only if the index is within the range of the current playlist.
+ \sa GetCurrentSong
+ */
+ void SetCurrentSong(int index);
+
+ int GetNextSong();
+
+ /*! \brief Get the index in the playlist that is offset away from the current index in the current playlist.
+ Obeys any repeat settings (eg repeat one will return the current index regardless of offset)
+ \return the index of the entry, or -1 if there is no current playlist. There is no guarantee that the returned index is valid.
+ */
+ int GetNextSong(int offset) const;
+
+ /*! \brief Set the active playlist
+ \param id Values can be PLAYLIST::TYPE_NONE, PLAYLIST::TYPE_MUSIC or PLAYLIST::TYPE_VIDEO
+ \sa GetCurrentPlaylist
+ */
+ void SetCurrentPlaylist(PLAYLIST::Id playlistId);
+
+ /*! \brief Get the currently active playlist
+ \return PLAYLIST::TYPE_NONE, PLAYLIST::TYPE_MUSIC or PLAYLIST::TYPE_VIDEO
+ \sa SetCurrentPlaylist, GetPlaylist
+ */
+ PLAYLIST::Id GetCurrentPlaylist() const;
+
+ /*! \brief Get a particular playlist object
+ \param id Values can be PLAYLIST::TYPE_MUSIC or PLAYLIST::TYPE_VIDEO
+ \return A reference to the CPlayList object.
+ \sa GetCurrentPlaylist
+ */
+ CPlayList& GetPlaylist(PLAYLIST::Id playlistId);
+ const CPlayList& GetPlaylist(PLAYLIST::Id playlistId) const;
+
+ /*! \brief Removes any item from all playlists located on a removable share
+ \return Number of items removed from PLAYLIST::TYPE_MUSIC and PLAYLIST::TYPE_VIDEO
+ */
+ int RemoveDVDItems();
+
+ /*! \brief Resets the current song and unplayable counts.
+ Does not alter the active playlist.
+ */
+ void Reset();
+
+ void ClearPlaylist(PLAYLIST::Id playlistId);
+ void Clear();
+
+ /*! \brief Set shuffle state of a playlist.
+ If the shuffle state changes, the playlist is shuffled or unshuffled.
+ Has no effect if Party Mode is enabled.
+ \param playlist the playlist to (un)shuffle, PLAYLIST::TYPE_MUSIC or PLAYLIST::TYPE_VIDEO.
+ \param shuffle set true to shuffle, false to unshuffle.
+ \param notify notify the user with a Toast notification (defaults to false)
+ \sa IsShuffled
+ */
+ void SetShuffle(PLAYLIST::Id playlistId, bool shuffle, bool notify = false);
+
+ /*! \brief Return whether a playlist is shuffled.
+ If partymode is enabled, this always returns false.
+ \param playlist the playlist to query for shuffle state, PLAYLIST::TYPE_MUSIC or PLAYLIST::TYPE_VIDEO.
+ \return true if the given playlist is shuffled and party mode isn't enabled, false otherwise.
+ \sa SetShuffle
+ */
+ bool IsShuffled(PLAYLIST::Id playlistId) const;
+
+ /*! \brief Return whether or not something has been played yet from the current playlist.
+ \return true if something has been played, false otherwise.
+ */
+ bool HasPlayedFirstFile() const;
+
+ /*! \brief Set repeat state of a playlist.
+ If called while in Party Mode, repeat is disabled.
+ \param playlist the playlist to set repeat state for, PLAYLIST::TYPE_MUSIC or PLAYLIST::TYPE_VIDEO.
+ \param state set to RepeatState::NONE, RepeatState::ONE or RepeatState::ALL
+ \param notify notify the user with a Toast notification
+ \sa GetRepeat
+ */
+ void SetRepeat(PLAYLIST::Id playlistId, PLAYLIST::RepeatState state, bool notify = false);
+ PLAYLIST::RepeatState GetRepeat(PLAYLIST::Id playlistId) const;
+
+ // add items via the playlist player
+ void Add(PLAYLIST::Id playlistId, const CPlayList& playlist);
+ void Add(PLAYLIST::Id playlistId, const CFileItemPtr& pItem);
+ void Add(PLAYLIST::Id playlistId, const CFileItemList& items);
+ void Insert(PLAYLIST::Id playlistId, const CPlayList& playlist, int iIndex);
+ void Insert(PLAYLIST::Id playlistId, const CFileItemPtr& pItem, int iIndex);
+ void Insert(PLAYLIST::Id playlistId, const CFileItemList& items, int iIndex);
+ void Remove(PLAYLIST::Id playlistId, int iPosition);
+ void Swap(PLAYLIST::Id playlistId, int indexItem1, int indexItem2);
+
+ bool IsSingleItemNonRepeatPlaylist() const;
+
+ bool OnAction(const CAction &action);
+protected:
+ /*! \brief Returns true if the given is set to repeat all
+ \param playlist Playlist to be query
+ \return true if the given playlist is set to repeat all, false otherwise.
+ */
+ bool Repeated(PLAYLIST::Id playlistId) const;
+
+ /*! \brief Returns true if the given is set to repeat one
+ \param playlist Playlist to be query
+ \return true if the given playlist is set to repeat one, false otherwise.
+ */
+ bool RepeatedOne(PLAYLIST::Id playlistId) const;
+
+ void ReShuffle(PLAYLIST::Id playlistId, int iPosition);
+
+ void AnnouncePropertyChanged(PLAYLIST::Id playlistId,
+ const std::string& strProperty,
+ const CVariant& value);
+
+ bool m_bPlayedFirstFile;
+ bool m_bPlaybackStarted;
+ int m_iFailedSongs;
+ std::chrono::time_point<std::chrono::steady_clock> m_failedSongsStart;
+ int m_iCurrentSong;
+ PLAYLIST::Id m_iCurrentPlayList{PLAYLIST::TYPE_NONE};
+ CPlayList* m_PlaylistMusic;
+ CPlayList* m_PlaylistVideo;
+ CPlayList* m_PlaylistEmpty;
+ std::map<PLAYLIST::Id, PLAYLIST::RepeatState> m_repeatState{
+ {PLAYLIST::TYPE_MUSIC, PLAYLIST::RepeatState::NONE},
+ {PLAYLIST::TYPE_VIDEO, PLAYLIST::RepeatState::NONE},
+ {PLAYLIST::TYPE_PICTURE, PLAYLIST::RepeatState::NONE},
+ };
+};
+
+}
diff --git a/xbmc/SectionLoader.cpp b/xbmc/SectionLoader.cpp
new file mode 100644
index 0000000..5612512
--- /dev/null
+++ b/xbmc/SectionLoader.cpp
@@ -0,0 +1,130 @@
+/*
+ * 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 "SectionLoader.h"
+
+#include "cores/DllLoader/DllLoaderContainer.h"
+#include "utils/GlobalsHandling.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#define g_sectionLoader XBMC_GLOBAL_USE(CSectionLoader)
+
+// delay for unloading dll's
+#define UNLOAD_DELAY 30*1000 // 30 sec.
+
+//Define this to get logging on all calls to load/unload sections/dlls
+//#define LOGALL
+
+CSectionLoader::CSectionLoader(void) = default;
+
+CSectionLoader::~CSectionLoader(void)
+{
+ UnloadAll();
+}
+
+LibraryLoader *CSectionLoader::LoadDLL(const std::string &dllname, bool bDelayUnload /*=true*/, bool bLoadSymbols /*=false*/)
+{
+ std::unique_lock<CCriticalSection> lock(g_sectionLoader.m_critSection);
+
+ if (dllname.empty()) return NULL;
+ // check if it's already loaded, and increase the reference count if so
+ for (int i = 0; i < (int)g_sectionLoader.m_vecLoadedDLLs.size(); ++i)
+ {
+ CDll& dll = g_sectionLoader.m_vecLoadedDLLs[i];
+ if (StringUtils::EqualsNoCase(dll.m_strDllName, dllname))
+ {
+ dll.m_lReferenceCount++;
+ return dll.m_pDll;
+ }
+ }
+
+ // ok, now load the dll
+ CLog::Log(LOGDEBUG, "SECTION:LoadDLL({})", dllname);
+ LibraryLoader* pDll = DllLoaderContainer::LoadModule(dllname.c_str(), NULL, bLoadSymbols);
+ if (!pDll)
+ return NULL;
+
+ CDll newDLL;
+ newDLL.m_strDllName = dllname;
+ newDLL.m_lReferenceCount = 1;
+ newDLL.m_bDelayUnload=bDelayUnload;
+ newDLL.m_pDll=pDll;
+ g_sectionLoader.m_vecLoadedDLLs.push_back(newDLL);
+
+ return newDLL.m_pDll;
+}
+
+void CSectionLoader::UnloadDLL(const std::string &dllname)
+{
+ std::unique_lock<CCriticalSection> lock(g_sectionLoader.m_critSection);
+
+ if (dllname.empty()) return;
+ // check if it's already loaded, and decrease the reference count if so
+ for (int i = 0; i < (int)g_sectionLoader.m_vecLoadedDLLs.size(); ++i)
+ {
+ CDll& dll = g_sectionLoader.m_vecLoadedDLLs[i];
+ if (StringUtils::EqualsNoCase(dll.m_strDllName, dllname))
+ {
+ dll.m_lReferenceCount--;
+ if (0 == dll.m_lReferenceCount)
+ {
+ if (dll.m_bDelayUnload)
+ dll.m_unloadDelayStartTick = std::chrono::steady_clock::now();
+ else
+ {
+ CLog::Log(LOGDEBUG, "SECTION:UnloadDll({})", dllname);
+ if (dll.m_pDll)
+ DllLoaderContainer::ReleaseModule(dll.m_pDll);
+ g_sectionLoader.m_vecLoadedDLLs.erase(g_sectionLoader.m_vecLoadedDLLs.begin() + i);
+ }
+
+ return;
+ }
+ }
+ }
+}
+
+void CSectionLoader::UnloadDelayed()
+{
+ std::unique_lock<CCriticalSection> lock(g_sectionLoader.m_critSection);
+
+ // check if we can unload any unreferenced dlls
+ for (int i = 0; i < (int)g_sectionLoader.m_vecLoadedDLLs.size(); ++i)
+ {
+ CDll& dll = g_sectionLoader.m_vecLoadedDLLs[i];
+ auto now = std::chrono::steady_clock::now();
+ auto duration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - dll.m_unloadDelayStartTick);
+ if (dll.m_lReferenceCount == 0 && duration.count() > UNLOAD_DELAY)
+ {
+ CLog::Log(LOGDEBUG, "SECTION:UnloadDelayed(DLL: {})", dll.m_strDllName);
+
+ if (dll.m_pDll)
+ DllLoaderContainer::ReleaseModule(dll.m_pDll);
+ g_sectionLoader.m_vecLoadedDLLs.erase(g_sectionLoader.m_vecLoadedDLLs.begin() + i);
+ return;
+ }
+ }
+}
+
+void CSectionLoader::UnloadAll()
+{
+ // delete the dll's
+ std::unique_lock<CCriticalSection> lock(g_sectionLoader.m_critSection);
+ std::vector<CDll>::iterator it = g_sectionLoader.m_vecLoadedDLLs.begin();
+ while (it != g_sectionLoader.m_vecLoadedDLLs.end())
+ {
+ CDll& dll = *it;
+ if (dll.m_pDll)
+ DllLoaderContainer::ReleaseModule(dll.m_pDll);
+ it = g_sectionLoader.m_vecLoadedDLLs.erase(it);
+ }
+}
diff --git a/xbmc/SectionLoader.h b/xbmc/SectionLoader.h
new file mode 100644
index 0000000..20757c8
--- /dev/null
+++ b/xbmc/SectionLoader.h
@@ -0,0 +1,47 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+#include <chrono>
+#include <string>
+#include <vector>
+
+// forward
+class LibraryLoader;
+
+class CSectionLoader
+{
+public:
+ class CDll
+ {
+ public:
+ std::string m_strDllName;
+ long m_lReferenceCount;
+ LibraryLoader *m_pDll;
+ std::chrono::time_point<std::chrono::steady_clock> m_unloadDelayStartTick;
+ bool m_bDelayUnload;
+ };
+ CSectionLoader(void);
+ virtual ~CSectionLoader(void);
+
+ static LibraryLoader* LoadDLL(const std::string& strSection, bool bDelayUnload=true, bool bLoadSymbols=false);
+ static void UnloadDLL(const std::string& strSection);
+ static void UnloadDelayed();
+ void UnloadAll();
+
+protected:
+ std::vector<CDll> m_vecLoadedDLLs;
+ CCriticalSection m_critSection;
+
+};
+
+extern CSectionLoader g_sectionLoader;
+
diff --git a/xbmc/SeekHandler.cpp b/xbmc/SeekHandler.cpp
new file mode 100644
index 0000000..d686e28
--- /dev/null
+++ b/xbmc/SeekHandler.cpp
@@ -0,0 +1,438 @@
+/*
+ * 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 "SeekHandler.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/DataCacheCore.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "utils/MathUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <cmath>
+#include <mutex>
+#include <stdlib.h>
+
+CSeekHandler::~CSeekHandler()
+{
+ m_seekDelays.clear();
+ m_forwardSeekSteps.clear();
+ m_backwardSeekSteps.clear();
+}
+
+void CSeekHandler::Configure()
+{
+ Reset();
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ m_seekDelays.clear();
+ m_seekDelays.insert(std::make_pair(SEEK_TYPE_VIDEO, settings->GetInt(CSettings::SETTING_VIDEOPLAYER_SEEKDELAY)));
+ m_seekDelays.insert(std::make_pair(SEEK_TYPE_MUSIC, settings->GetInt(CSettings::SETTING_MUSICPLAYER_SEEKDELAY)));
+
+ m_forwardSeekSteps.clear();
+ m_backwardSeekSteps.clear();
+
+ std::map<SeekType, std::string> seekTypeSettingMap;
+ seekTypeSettingMap.insert(std::make_pair(SEEK_TYPE_VIDEO, CSettings::SETTING_VIDEOPLAYER_SEEKSTEPS));
+ seekTypeSettingMap.insert(std::make_pair(SEEK_TYPE_MUSIC, CSettings::SETTING_MUSICPLAYER_SEEKSTEPS));
+
+ for (std::map<SeekType, std::string>::iterator it = seekTypeSettingMap.begin(); it!=seekTypeSettingMap.end(); ++it)
+ {
+ std::vector<int> forwardSeekSteps;
+ std::vector<int> backwardSeekSteps;
+
+ std::vector<CVariant> seekSteps = settings->GetList(it->second);
+ for (std::vector<CVariant>::iterator it = seekSteps.begin(); it != seekSteps.end(); ++it)
+ {
+ int stepSeconds = static_cast<int>((*it).asInteger());
+ if (stepSeconds < 0)
+ backwardSeekSteps.insert(backwardSeekSteps.begin(), stepSeconds);
+ else
+ forwardSeekSteps.push_back(stepSeconds);
+ }
+
+ m_forwardSeekSteps.insert(std::make_pair(it->first, forwardSeekSteps));
+ m_backwardSeekSteps.insert(std::make_pair(it->first, backwardSeekSteps));
+ }
+}
+
+void CSeekHandler::Reset()
+{
+ m_requireSeek = false;
+ m_analogSeek = false;
+ m_seekStep = 0;
+ m_seekSize = 0;
+ m_timeCodePosition = 0;
+}
+
+int CSeekHandler::GetSeekStepSize(SeekType type, int step)
+{
+ if (step == 0)
+ return 0;
+
+ std::vector<int> seekSteps(step > 0 ? m_forwardSeekSteps.at(type) : m_backwardSeekSteps.at(type));
+
+ if (seekSteps.empty())
+ {
+ CLog::Log(LOGERROR, "SeekHandler - {} - No {} {} seek steps configured.", __FUNCTION__,
+ (type == SeekType::SEEK_TYPE_VIDEO ? "video" : "music"),
+ (step > 0 ? "forward" : "backward"));
+ return 0;
+ }
+
+ int seconds = 0;
+
+ // when exceeding the selected amount of steps repeat/sum up the last step size
+ if (static_cast<size_t>(abs(step)) <= seekSteps.size())
+ seconds = seekSteps.at(abs(step) - 1);
+ else
+ seconds = seekSteps.back() * (abs(step) - seekSteps.size() + 1);
+
+ return seconds;
+}
+
+void CSeekHandler::Seek(bool forward, float amount, float duration /* = 0 */, bool analogSeek /* = false */, SeekType type /* = SEEK_TYPE_VIDEO */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // not yet seeking
+ if (!m_requireSeek)
+ {
+ // use only the first step forward/backward for a seek without a delay
+ if (!analogSeek && m_seekDelays.at(type) == 0)
+ {
+ SeekSeconds(GetSeekStepSize(type, forward ? 1 : -1));
+ return;
+ }
+
+ m_requireSeek = true;
+ m_analogSeek = analogSeek;
+ m_seekDelay = analogSeek ? analogSeekDelay : m_seekDelays.at(type);
+ }
+
+ // calculate our seek amount
+ if (analogSeek)
+ {
+ //100% over 1 second.
+ float speed = 100.0f;
+ if( duration )
+ speed *= duration;
+ else
+ speed /= CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS();
+
+ double totalTime = g_application.GetTotalTime();
+ if (totalTime < 0)
+ totalTime = 0;
+
+ double seekSize = static_cast<double>(amount * amount * speed) * totalTime / 100.0;
+ if (forward)
+ SetSeekSize(m_seekSize + seekSize);
+ else
+ SetSeekSize(m_seekSize - seekSize);
+ }
+ else
+ {
+ m_seekStep += forward ? 1 : -1;
+ int seekSeconds = GetSeekStepSize(type, m_seekStep);
+ if (seekSeconds != 0)
+ {
+ SetSeekSize(seekSeconds);
+ }
+ else
+ {
+ // nothing to do, abort seeking
+ Reset();
+ }
+ }
+ m_seekChanged = true;
+ m_timer.StartZero();
+}
+
+void CSeekHandler::SeekSeconds(int seconds)
+{
+ // abort if we do not have a play time or already perform a seek
+ if (seconds == 0)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ SetSeekSize(seconds);
+
+ // perform relative seek
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->SeekTimeRelative(static_cast<int64_t>(seconds * 1000));
+
+ Reset();
+}
+
+int CSeekHandler::GetSeekSize() const
+{
+ return MathUtils::round_int(m_seekSize);
+}
+
+void CSeekHandler::SetSeekSize(double seekSize)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ int64_t playTime = appPlayer->GetTime();
+ double minSeekSize = (appPlayer->GetMinTime() - playTime) / 1000.0;
+ double maxSeekSize = (appPlayer->GetMaxTime() - playTime) / 1000.0;
+
+ m_seekSize = seekSize > 0
+ ? std::min(seekSize, maxSeekSize)
+ : std::max(seekSize, minSeekSize);
+}
+
+bool CSeekHandler::InProgress() const
+{
+ return m_requireSeek || CServiceBroker::GetDataCacheCore().IsSeeking();
+}
+
+void CSeekHandler::FrameMove()
+{
+ if (m_timer.GetElapsedMilliseconds() >= m_seekDelay && m_requireSeek)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // perform relative seek
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->SeekTimeRelative(static_cast<int64_t>(m_seekSize * 1000));
+
+ m_seekChanged = true;
+
+ Reset();
+ }
+
+ if (m_timeCodePosition > 0 && m_timerTimeCode.GetElapsedMilliseconds() >= 2500)
+ {
+ m_timeCodePosition = 0;
+ }
+
+ if (m_seekChanged)
+ {
+ m_seekChanged = false;
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_STATE_CHANGED);
+ }
+}
+
+void CSeekHandler::SettingOptionsSeekStepsFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ std::string label;
+ for (int seconds : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_seekSteps)
+ {
+ if (seconds > 60)
+ label = StringUtils::Format(g_localizeStrings.Get(14044), seconds / 60);
+ else
+ label = StringUtils::Format(g_localizeStrings.Get(14045), seconds);
+
+ list.insert(list.begin(), IntegerSettingOption("-" + label, seconds * -1));
+ list.emplace_back(label, seconds);
+ }
+}
+
+void CSeekHandler::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ if (setting->GetId() == CSettings::SETTING_VIDEOPLAYER_SEEKDELAY ||
+ setting->GetId() == CSettings::SETTING_VIDEOPLAYER_SEEKSTEPS ||
+ setting->GetId() == CSettings::SETTING_MUSICPLAYER_SEEKDELAY ||
+ setting->GetId() == CSettings::SETTING_MUSICPLAYER_SEEKSTEPS)
+ Configure();
+}
+
+bool CSeekHandler::OnAction(const CAction &action)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlaying() || !appPlayer->CanSeek())
+ return false;
+
+ SeekType type = g_application.CurrentFileItem().IsAudio() ? SEEK_TYPE_MUSIC : SEEK_TYPE_VIDEO;
+
+ if (SeekTimeCode(action))
+ return true;
+
+ switch (action.GetID())
+ {
+ case ACTION_SMALL_STEP_BACK:
+ case ACTION_STEP_BACK:
+ {
+ Seek(false, action.GetAmount(), action.GetRepeat(), false, type);
+ return true;
+ }
+ case ACTION_STEP_FORWARD:
+ {
+ Seek(true, action.GetAmount(), action.GetRepeat(), false, type);
+ return true;
+ }
+ case ACTION_BIG_STEP_BACK:
+ case ACTION_CHAPTER_OR_BIG_STEP_BACK:
+ {
+ appPlayer->Seek(false, true, action.GetID() == ACTION_CHAPTER_OR_BIG_STEP_BACK);
+ return true;
+ }
+ case ACTION_BIG_STEP_FORWARD:
+ case ACTION_CHAPTER_OR_BIG_STEP_FORWARD:
+ {
+ appPlayer->Seek(true, true, action.GetID() == ACTION_CHAPTER_OR_BIG_STEP_FORWARD);
+ return true;
+ }
+ case ACTION_NEXT_SCENE:
+ {
+ appPlayer->SeekScene(true);
+ return true;
+ }
+ case ACTION_PREV_SCENE:
+ {
+ appPlayer->SeekScene(false);
+ return true;
+ }
+ case ACTION_ANALOG_SEEK_FORWARD:
+ case ACTION_ANALOG_SEEK_BACK:
+ {
+ if (action.GetAmount())
+ Seek(action.GetID() == ACTION_ANALOG_SEEK_FORWARD, action.GetAmount(), action.GetRepeat(), true);
+ return true;
+ }
+ case REMOTE_0:
+ case REMOTE_1:
+ case REMOTE_2:
+ case REMOTE_3:
+ case REMOTE_4:
+ case REMOTE_5:
+ case REMOTE_6:
+ case REMOTE_7:
+ case REMOTE_8:
+ case REMOTE_9:
+ case ACTION_JUMP_SMS2:
+ case ACTION_JUMP_SMS3:
+ case ACTION_JUMP_SMS4:
+ case ACTION_JUMP_SMS5:
+ case ACTION_JUMP_SMS6:
+ case ACTION_JUMP_SMS7:
+ case ACTION_JUMP_SMS8:
+ case ACTION_JUMP_SMS9:
+ {
+ if (!g_application.CurrentFileItem().IsLiveTV())
+ {
+ ChangeTimeCode(action.GetID());
+ return true;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool CSeekHandler::SeekTimeCode(const CAction &action)
+{
+ if (m_timeCodePosition <= 0)
+ return false;
+
+ switch (action.GetID())
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_PLAYER_PLAY:
+ case ACTION_PAUSE:
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ g_application.SeekTime(GetTimeCodeSeconds());
+ Reset();
+ return true;
+ }
+ case ACTION_SMALL_STEP_BACK:
+ case ACTION_STEP_BACK:
+ case ACTION_BIG_STEP_BACK:
+ case ACTION_CHAPTER_OR_BIG_STEP_BACK:
+ case ACTION_MOVE_LEFT:
+ {
+ SeekSeconds(-GetTimeCodeSeconds());
+ return true;
+ }
+ case ACTION_STEP_FORWARD:
+ case ACTION_BIG_STEP_FORWARD:
+ case ACTION_CHAPTER_OR_BIG_STEP_FORWARD:
+ case ACTION_MOVE_RIGHT:
+ {
+ SeekSeconds(GetTimeCodeSeconds());
+ return true;
+ }
+ default:
+ break;
+ }
+ return false;
+}
+
+void CSeekHandler::ChangeTimeCode(int remote)
+{
+ if (remote >= ACTION_JUMP_SMS2 && remote <= ACTION_JUMP_SMS9)
+ {
+ // cast to REMOTE_X
+ remote -= (ACTION_JUMP_SMS2 - REMOTE_2);
+ }
+ if (remote >= REMOTE_0 && remote <= REMOTE_9)
+ {
+ m_timerTimeCode.StartZero();
+
+ if (m_timeCodePosition < 6)
+ m_timeCodeStamp[m_timeCodePosition++] = remote - REMOTE_0;
+ else
+ {
+ // rotate around
+ for (int i = 0; i < 5; i++)
+ m_timeCodeStamp[i] = m_timeCodeStamp[i + 1];
+ m_timeCodeStamp[5] = remote - REMOTE_0;
+ }
+ }
+ }
+
+int CSeekHandler::GetTimeCodeSeconds() const
+{
+ if (m_timeCodePosition > 0)
+ {
+ // Convert the timestamp into an integer
+ int tot = 0;
+ for (int i = 0; i < m_timeCodePosition; i++)
+ tot = tot * 10 + m_timeCodeStamp[i];
+
+ // Interpret result as HHMMSS
+ int s = tot % 100; tot /= 100;
+ int m = tot % 100; tot /= 100;
+ int h = tot % 100;
+
+ return h * 3600 + m * 60 + s;
+ }
+ return 0;
+}
diff --git a/xbmc/SeekHandler.h b/xbmc/SeekHandler.h
new file mode 100644
index 0000000..ac7161d
--- /dev/null
+++ b/xbmc/SeekHandler.h
@@ -0,0 +1,81 @@
+/*
+ * 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 "interfaces/IActionListener.h"
+#include "settings/lib/ISettingCallback.h"
+#include "threads/CriticalSection.h"
+#include "utils/Stopwatch.h"
+
+#include <map>
+#include <utility>
+#include <vector>
+
+struct IntegerSettingOption;
+
+enum SeekType
+{
+ SEEK_TYPE_VIDEO = 0,
+ SEEK_TYPE_MUSIC = 1
+};
+
+class CSeekHandler : public ISettingCallback, public IActionListener
+{
+public:
+ CSeekHandler() = default;
+ ~CSeekHandler() override;
+
+ static void SettingOptionsSeekStepsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ bool OnAction(const CAction &action) override;
+
+ void Seek(bool forward, float amount, float duration = 0, bool analogSeek = false, SeekType type = SEEK_TYPE_VIDEO);
+ void SeekSeconds(int seconds);
+ void FrameMove();
+ void Reset();
+ void Configure();
+
+ int GetSeekSize() const;
+ bool InProgress() const;
+
+ bool HasTimeCode() const { return m_timeCodePosition > 0; }
+ int GetTimeCodeSeconds() const;
+
+protected:
+ CSeekHandler(const CSeekHandler&) = delete;
+ CSeekHandler& operator=(CSeekHandler const&) = delete;
+ bool SeekTimeCode(const CAction &action);
+ void ChangeTimeCode(int remote);
+
+private:
+ static const int analogSeekDelay = 500;
+
+ void SetSeekSize(double seekSize);
+ int GetSeekStepSize(SeekType type, int step);
+
+ int m_seekDelay = 500;
+ std::map<SeekType, int > m_seekDelays;
+ bool m_requireSeek = false;
+ bool m_seekChanged = false;
+ bool m_analogSeek = false;
+ double m_seekSize = 0;
+ int m_seekStep = 0;
+ std::map<SeekType, std::vector<int> > m_forwardSeekSteps;
+ std::map<SeekType, std::vector<int> > m_backwardSeekSteps;
+ CStopWatch m_timer;
+ CStopWatch m_timerTimeCode;
+ int m_timeCodeStamp[6];
+ int m_timeCodePosition;
+
+ CCriticalSection m_critSection;
+};
diff --git a/xbmc/ServiceBroker.cpp b/xbmc/ServiceBroker.cpp
new file mode 100644
index 0000000..1d4a449
--- /dev/null
+++ b/xbmc/ServiceBroker.cpp
@@ -0,0 +1,436 @@
+/*
+ * 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 "ServiceBroker.h"
+
+#include "ServiceManager.h"
+#include "application/Application.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+
+#include <stdexcept>
+#include <utility>
+
+using namespace KODI;
+
+CServiceBroker::CServiceBroker()
+ : m_pGUI(nullptr), m_pWinSystem(nullptr), m_pActiveAE(nullptr), m_decoderFilterManager(nullptr)
+{
+}
+
+CServiceBroker::~CServiceBroker()
+{
+}
+
+std::shared_ptr<CAppParams> CServiceBroker::GetAppParams()
+{
+ if (!g_serviceBroker.m_appParams)
+ throw std::logic_error("AppParams not yet available / not available anymore.");
+
+ return g_serviceBroker.m_appParams;
+}
+
+void CServiceBroker::RegisterAppParams(const std::shared_ptr<CAppParams>& appParams)
+{
+ g_serviceBroker.m_appParams = appParams;
+}
+
+void CServiceBroker::UnregisterAppParams()
+{
+ g_serviceBroker.m_appParams.reset();
+}
+
+CLog& CServiceBroker::GetLogging()
+{
+ return *(g_serviceBroker.m_logging);
+}
+
+void CServiceBroker::CreateLogging()
+{
+ g_serviceBroker.m_logging = std::make_unique<CLog>();
+}
+
+void CServiceBroker::DestroyLogging()
+{
+ g_serviceBroker.m_logging.reset();
+}
+
+// announcement
+std::shared_ptr<ANNOUNCEMENT::CAnnouncementManager> CServiceBroker::GetAnnouncementManager()
+{
+ return g_serviceBroker.m_pAnnouncementManager;
+}
+void CServiceBroker::RegisterAnnouncementManager(
+ std::shared_ptr<ANNOUNCEMENT::CAnnouncementManager> port)
+{
+ g_serviceBroker.m_pAnnouncementManager = std::move(port);
+}
+
+void CServiceBroker::UnregisterAnnouncementManager()
+{
+ g_serviceBroker.m_pAnnouncementManager.reset();
+}
+
+ADDON::CAddonMgr& CServiceBroker::GetAddonMgr()
+{
+ return g_application.m_ServiceManager->GetAddonMgr();
+}
+
+ADDON::CBinaryAddonManager& CServiceBroker::GetBinaryAddonManager()
+{
+ return g_application.m_ServiceManager->GetBinaryAddonManager();
+}
+
+ADDON::CBinaryAddonCache& CServiceBroker::GetBinaryAddonCache()
+{
+ return g_application.m_ServiceManager->GetBinaryAddonCache();
+}
+
+ADDONS::CExtsMimeSupportList& CServiceBroker::GetExtsMimeSupportList()
+{
+ return g_application.m_ServiceManager->GetExtsMimeSupportList();
+}
+
+ADDON::CVFSAddonCache& CServiceBroker::GetVFSAddonCache()
+{
+ return g_application.m_ServiceManager->GetVFSAddonCache();
+}
+
+#ifdef HAS_PYTHON
+XBPython& CServiceBroker::GetXBPython()
+{
+ return g_application.m_ServiceManager->GetXBPython();
+}
+#endif
+
+#if defined(HAS_FILESYSTEM_SMB)
+WSDiscovery::IWSDiscovery& CServiceBroker::GetWSDiscovery()
+{
+ return g_application.m_ServiceManager->GetWSDiscovery();
+}
+#endif
+
+#if !defined(TARGET_WINDOWS) && defined(HAS_DVD_DRIVE)
+MEDIA_DETECT::CDetectDVDMedia& CServiceBroker::GetDetectDVDMedia()
+{
+ return g_application.m_ServiceManager->GetDetectDVDMedia();
+}
+#endif
+
+PVR::CPVRManager& CServiceBroker::GetPVRManager()
+{
+ return g_application.m_ServiceManager->GetPVRManager();
+}
+
+CContextMenuManager& CServiceBroker::GetContextMenuManager()
+{
+ return g_application.m_ServiceManager->GetContextMenuManager();
+}
+
+CDataCacheCore& CServiceBroker::GetDataCacheCore()
+{
+ return g_application.m_ServiceManager->GetDataCacheCore();
+}
+
+CPlatform& CServiceBroker::GetPlatform()
+{
+ return g_application.m_ServiceManager->GetPlatform();
+}
+
+PLAYLIST::CPlayListPlayer& CServiceBroker::GetPlaylistPlayer()
+{
+ return g_application.m_ServiceManager->GetPlaylistPlayer();
+}
+
+void CServiceBroker::RegisterSettingsComponent(const std::shared_ptr<CSettingsComponent>& settings)
+{
+ g_serviceBroker.m_pSettingsComponent = settings;
+}
+
+void CServiceBroker::UnregisterSettingsComponent()
+{
+ g_serviceBroker.m_pSettingsComponent.reset();
+}
+
+std::shared_ptr<CSettingsComponent> CServiceBroker::GetSettingsComponent()
+{
+ return g_serviceBroker.m_pSettingsComponent;
+}
+
+GAME::CControllerManager& CServiceBroker::GetGameControllerManager()
+{
+ return g_application.m_ServiceManager->GetGameControllerManager();
+}
+
+GAME::CGameServices& CServiceBroker::GetGameServices()
+{
+ return g_application.m_ServiceManager->GetGameServices();
+}
+
+KODI::RETRO::CGUIGameRenderManager& CServiceBroker::GetGameRenderManager()
+{
+ return g_application.m_ServiceManager->GetGameRenderManager();
+}
+
+PERIPHERALS::CPeripherals& CServiceBroker::GetPeripherals()
+{
+ return g_application.m_ServiceManager->GetPeripherals();
+}
+
+CFavouritesService& CServiceBroker::GetFavouritesService()
+{
+ return g_application.m_ServiceManager->GetFavouritesService();
+}
+
+ADDON::CServiceAddonManager& CServiceBroker::GetServiceAddons()
+{
+ return g_application.m_ServiceManager->GetServiceAddons();
+}
+
+ADDON::CRepositoryUpdater& CServiceBroker::GetRepositoryUpdater()
+{
+ return g_application.m_ServiceManager->GetRepositoryUpdater();
+}
+
+CInputManager& CServiceBroker::GetInputManager()
+{
+ return g_application.m_ServiceManager->GetInputManager();
+}
+
+CFileExtensionProvider& CServiceBroker::GetFileExtensionProvider()
+{
+ return g_application.m_ServiceManager->GetFileExtensionProvider();
+}
+
+CNetworkBase& CServiceBroker::GetNetwork()
+{
+ return g_application.m_ServiceManager->GetNetwork();
+}
+
+bool CServiceBroker::IsAddonInterfaceUp()
+{
+ return g_application.m_ServiceManager && g_application.m_ServiceManager->init_level > 1;
+}
+
+bool CServiceBroker::IsServiceManagerUp()
+{
+ return g_application.m_ServiceManager && g_application.m_ServiceManager->init_level == 3;
+}
+
+CWinSystemBase* CServiceBroker::GetWinSystem()
+{
+ return g_serviceBroker.m_pWinSystem;
+}
+
+void CServiceBroker::RegisterWinSystem(CWinSystemBase* winsystem)
+{
+ g_serviceBroker.m_pWinSystem = winsystem;
+}
+
+void CServiceBroker::UnregisterWinSystem()
+{
+ g_serviceBroker.m_pWinSystem = nullptr;
+}
+
+CRenderSystemBase* CServiceBroker::GetRenderSystem()
+{
+ if (g_serviceBroker.m_pWinSystem)
+ return g_serviceBroker.m_pWinSystem->GetRenderSystem();
+
+ return nullptr;
+}
+
+CPowerManager& CServiceBroker::GetPowerManager()
+{
+ return g_application.m_ServiceManager->GetPowerManager();
+}
+
+CWeatherManager& CServiceBroker::GetWeatherManager()
+{
+ return g_application.m_ServiceManager->GetWeatherManager();
+}
+
+CPlayerCoreFactory& CServiceBroker::GetPlayerCoreFactory()
+{
+ return g_application.m_ServiceManager->GetPlayerCoreFactory();
+}
+
+CDatabaseManager& CServiceBroker::GetDatabaseManager()
+{
+ return g_application.m_ServiceManager->GetDatabaseManager();
+}
+
+CEventLog* CServiceBroker::GetEventLog()
+{
+ if (!g_serviceBroker.m_pSettingsComponent)
+ return nullptr;
+
+ auto profileManager = g_serviceBroker.m_pSettingsComponent->GetProfileManager();
+ if (!profileManager)
+ return nullptr;
+
+ return &profileManager->GetEventLog();
+}
+
+CMediaManager& CServiceBroker::GetMediaManager()
+{
+ return g_application.m_ServiceManager->GetMediaManager();
+}
+
+CApplicationComponents& CServiceBroker::GetAppComponents()
+{
+ return g_application;
+}
+
+CGUIComponent* CServiceBroker::GetGUI()
+{
+ return g_serviceBroker.m_pGUI;
+}
+
+void CServiceBroker::RegisterGUI(CGUIComponent* gui)
+{
+ g_serviceBroker.m_pGUI = gui;
+}
+
+void CServiceBroker::UnregisterGUI()
+{
+ g_serviceBroker.m_pGUI = nullptr;
+}
+
+// audio
+IAE* CServiceBroker::GetActiveAE()
+{
+ return g_serviceBroker.m_pActiveAE;
+}
+void CServiceBroker::RegisterAE(IAE* ae)
+{
+ g_serviceBroker.m_pActiveAE = ae;
+}
+void CServiceBroker::UnregisterAE()
+{
+ g_serviceBroker.m_pActiveAE = nullptr;
+}
+
+// application
+std::shared_ptr<CAppInboundProtocol> CServiceBroker::GetAppPort()
+{
+ return g_serviceBroker.m_pAppPort;
+}
+void CServiceBroker::RegisterAppPort(std::shared_ptr<CAppInboundProtocol> port)
+{
+ g_serviceBroker.m_pAppPort = std::move(port);
+}
+void CServiceBroker::UnregisterAppPort()
+{
+ g_serviceBroker.m_pAppPort.reset();
+}
+
+void CServiceBroker::RegisterDecoderFilterManager(CDecoderFilterManager* manager)
+{
+ g_serviceBroker.m_decoderFilterManager = manager;
+}
+
+CDecoderFilterManager* CServiceBroker::GetDecoderFilterManager()
+{
+ return g_serviceBroker.m_decoderFilterManager;
+}
+
+std::shared_ptr<CCPUInfo> CServiceBroker::GetCPUInfo()
+{
+ return g_serviceBroker.m_cpuInfo;
+}
+
+void CServiceBroker::RegisterCPUInfo(std::shared_ptr<CCPUInfo> cpuInfo)
+{
+ g_serviceBroker.m_cpuInfo = std::move(cpuInfo);
+}
+
+void CServiceBroker::UnregisterCPUInfo()
+{
+ g_serviceBroker.m_cpuInfo.reset();
+}
+
+void CServiceBroker::RegisterTextureCache(const std::shared_ptr<CTextureCache>& cache)
+{
+ g_serviceBroker.m_textureCache = cache;
+}
+
+void CServiceBroker::UnregisterTextureCache()
+{
+ g_serviceBroker.m_textureCache.reset();
+}
+
+std::shared_ptr<CTextureCache> CServiceBroker::GetTextureCache()
+{
+ return g_serviceBroker.m_textureCache;
+}
+
+void CServiceBroker::RegisterJobManager(const std::shared_ptr<CJobManager>& jobManager)
+{
+ g_serviceBroker.m_jobManager = jobManager;
+}
+
+void CServiceBroker::UnregisterJobManager()
+{
+ g_serviceBroker.m_jobManager.reset();
+}
+
+std::shared_ptr<CJobManager> CServiceBroker::GetJobManager()
+{
+ return g_serviceBroker.m_jobManager;
+}
+
+void CServiceBroker::RegisterAppMessenger(
+ const std::shared_ptr<KODI::MESSAGING::CApplicationMessenger>& appMessenger)
+{
+ g_serviceBroker.m_appMessenger = appMessenger;
+}
+
+void CServiceBroker::UnregisterAppMessenger()
+{
+ g_serviceBroker.m_appMessenger.reset();
+}
+
+std::shared_ptr<KODI::MESSAGING::CApplicationMessenger> CServiceBroker::GetAppMessenger()
+{
+ return g_serviceBroker.m_appMessenger;
+}
+
+void CServiceBroker::RegisterKeyboardLayoutManager(
+ const std::shared_ptr<CKeyboardLayoutManager>& keyboardLayoutManager)
+{
+ g_serviceBroker.m_keyboardLayoutManager = keyboardLayoutManager;
+}
+
+void CServiceBroker::UnregisterKeyboardLayoutManager()
+{
+ g_serviceBroker.m_keyboardLayoutManager.reset();
+}
+
+std::shared_ptr<CKeyboardLayoutManager> CServiceBroker::GetKeyboardLayoutManager()
+{
+ return g_serviceBroker.m_keyboardLayoutManager;
+}
+
+void CServiceBroker::RegisterSpeechRecognition(
+ const std::shared_ptr<speech::ISpeechRecognition>& speechRecognition)
+{
+ g_serviceBroker.m_speechRecognition = speechRecognition;
+}
+
+void CServiceBroker::UnregisterSpeechRecognition()
+{
+ g_serviceBroker.m_speechRecognition.reset();
+}
+
+std::shared_ptr<speech::ISpeechRecognition> CServiceBroker::GetSpeechRecognition()
+{
+ return g_serviceBroker.m_speechRecognition;
+}
diff --git a/xbmc/ServiceBroker.h b/xbmc/ServiceBroker.h
new file mode 100644
index 0000000..dc502e4
--- /dev/null
+++ b/xbmc/ServiceBroker.h
@@ -0,0 +1,240 @@
+/*
+ * 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 "utils/GlobalsHandling.h"
+
+#include <memory>
+
+namespace ADDON
+{
+class CAddonMgr;
+class CBinaryAddonManager;
+class CBinaryAddonCache;
+class CVFSAddonCache;
+class CServiceAddonManager;
+class CRepositoryUpdater;
+} // namespace ADDON
+
+namespace ANNOUNCEMENT
+{
+class CAnnouncementManager;
+}
+
+namespace MEDIA_DETECT
+{
+class CDetectDVDMedia;
+}
+
+namespace PVR
+{
+class CPVRManager;
+}
+
+namespace PLAYLIST
+{
+class CPlayListPlayer;
+}
+
+namespace KODI
+{
+namespace MESSAGING
+{
+class CApplicationMessenger;
+}
+} // namespace KODI
+
+class CAppParams;
+template<class T>
+class CComponentContainer;
+class CContextMenuManager;
+class XBPython;
+class CDataCacheCore;
+class IAE;
+class IApplicationComponent;
+class CFavouritesService;
+class CInputManager;
+class CFileExtensionProvider;
+class CNetworkBase;
+class CWinSystemBase;
+class CRenderSystemBase;
+class CPowerManager;
+class CWeatherManager;
+class CPlayerCoreFactory;
+class CDatabaseManager;
+class CEventLog;
+class CGUIComponent;
+class CAppInboundProtocol;
+class CSettingsComponent;
+class CDecoderFilterManager;
+class CMediaManager;
+class CCPUInfo;
+class CLog;
+class CPlatform;
+class CTextureCache;
+class CJobManager;
+class CKeyboardLayoutManager;
+
+namespace WSDiscovery
+{
+class IWSDiscovery;
+}
+
+namespace KODI
+{
+namespace ADDONS
+{
+class CExtsMimeSupportList;
+}
+
+namespace GAME
+{
+class CControllerManager;
+class CGameServices;
+} // namespace GAME
+
+namespace RETRO
+{
+class CGUIGameRenderManager;
+}
+} // namespace KODI
+
+namespace PERIPHERALS
+{
+class CPeripherals;
+}
+
+namespace speech
+{
+class ISpeechRecognition;
+}
+
+class CServiceBroker
+{
+public:
+ CServiceBroker();
+ ~CServiceBroker();
+
+ static std::shared_ptr<CAppParams> GetAppParams();
+ static void RegisterAppParams(const std::shared_ptr<CAppParams>& appParams);
+ static void UnregisterAppParams();
+
+ static CLog& GetLogging();
+ static void CreateLogging();
+ static void DestroyLogging();
+
+ static std::shared_ptr<ANNOUNCEMENT::CAnnouncementManager> GetAnnouncementManager();
+ static void RegisterAnnouncementManager(
+ std::shared_ptr<ANNOUNCEMENT::CAnnouncementManager> announcementManager);
+ static void UnregisterAnnouncementManager();
+
+ static ADDON::CAddonMgr& GetAddonMgr();
+ static ADDON::CBinaryAddonManager& GetBinaryAddonManager();
+ static ADDON::CBinaryAddonCache& GetBinaryAddonCache();
+ static KODI::ADDONS::CExtsMimeSupportList& GetExtsMimeSupportList();
+ static ADDON::CVFSAddonCache& GetVFSAddonCache();
+ static XBPython& GetXBPython();
+ static WSDiscovery::IWSDiscovery& GetWSDiscovery();
+ static MEDIA_DETECT::CDetectDVDMedia& GetDetectDVDMedia();
+ static PVR::CPVRManager& GetPVRManager();
+ static CContextMenuManager& GetContextMenuManager();
+ static CDataCacheCore& GetDataCacheCore();
+ static CPlatform& GetPlatform();
+ static PLAYLIST::CPlayListPlayer& GetPlaylistPlayer();
+ static KODI::GAME::CControllerManager& GetGameControllerManager();
+ static KODI::GAME::CGameServices& GetGameServices();
+ static KODI::RETRO::CGUIGameRenderManager& GetGameRenderManager();
+ static PERIPHERALS::CPeripherals& GetPeripherals();
+ static CFavouritesService& GetFavouritesService();
+ static ADDON::CServiceAddonManager& GetServiceAddons();
+ static ADDON::CRepositoryUpdater& GetRepositoryUpdater();
+ static CInputManager& GetInputManager();
+ static CFileExtensionProvider& GetFileExtensionProvider();
+ static bool IsAddonInterfaceUp();
+ static bool IsServiceManagerUp();
+ static CNetworkBase& GetNetwork();
+ static CPowerManager& GetPowerManager();
+ static CWeatherManager& GetWeatherManager();
+ static CPlayerCoreFactory& GetPlayerCoreFactory();
+ static CDatabaseManager& GetDatabaseManager();
+ static CEventLog* GetEventLog();
+ static CMediaManager& GetMediaManager();
+ static CComponentContainer<IApplicationComponent>& GetAppComponents();
+
+ static CGUIComponent* GetGUI();
+ static void RegisterGUI(CGUIComponent* gui);
+ static void UnregisterGUI();
+
+ static void RegisterSettingsComponent(const std::shared_ptr<CSettingsComponent>& settings);
+ static void UnregisterSettingsComponent();
+ static std::shared_ptr<CSettingsComponent> GetSettingsComponent();
+
+ static void RegisterWinSystem(CWinSystemBase* winsystem);
+ static void UnregisterWinSystem();
+ static CWinSystemBase* GetWinSystem();
+ static CRenderSystemBase* GetRenderSystem();
+
+ static IAE* GetActiveAE();
+ static void RegisterAE(IAE* ae);
+ static void UnregisterAE();
+
+ static std::shared_ptr<CAppInboundProtocol> GetAppPort();
+ static void RegisterAppPort(std::shared_ptr<CAppInboundProtocol> port);
+ static void UnregisterAppPort();
+
+ static void RegisterDecoderFilterManager(CDecoderFilterManager* manager);
+ static CDecoderFilterManager* GetDecoderFilterManager();
+
+ static std::shared_ptr<CCPUInfo> GetCPUInfo();
+ static void RegisterCPUInfo(std::shared_ptr<CCPUInfo> cpuInfo);
+ static void UnregisterCPUInfo();
+
+ static void RegisterTextureCache(const std::shared_ptr<CTextureCache>& cache);
+ static void UnregisterTextureCache();
+ static std::shared_ptr<CTextureCache> GetTextureCache();
+
+ static void RegisterJobManager(const std::shared_ptr<CJobManager>& jobManager);
+ static void UnregisterJobManager();
+ static std::shared_ptr<CJobManager> GetJobManager();
+
+ static void RegisterAppMessenger(
+ const std::shared_ptr<KODI::MESSAGING::CApplicationMessenger>& appMessenger);
+ static void UnregisterAppMessenger();
+ static std::shared_ptr<KODI::MESSAGING::CApplicationMessenger> GetAppMessenger();
+
+ static void RegisterKeyboardLayoutManager(
+ const std::shared_ptr<CKeyboardLayoutManager>& keyboardLayoutManager);
+ static void UnregisterKeyboardLayoutManager();
+ static std::shared_ptr<CKeyboardLayoutManager> GetKeyboardLayoutManager();
+
+ static void RegisterSpeechRecognition(
+ const std::shared_ptr<speech::ISpeechRecognition>& speechRecognition);
+ static void UnregisterSpeechRecognition();
+ static std::shared_ptr<speech::ISpeechRecognition> GetSpeechRecognition();
+
+private:
+ std::shared_ptr<CAppParams> m_appParams;
+ std::unique_ptr<CLog> m_logging;
+ std::shared_ptr<ANNOUNCEMENT::CAnnouncementManager> m_pAnnouncementManager;
+ CGUIComponent* m_pGUI;
+ CWinSystemBase* m_pWinSystem;
+ IAE* m_pActiveAE;
+ std::shared_ptr<CAppInboundProtocol> m_pAppPort;
+ std::shared_ptr<CSettingsComponent> m_pSettingsComponent;
+ CDecoderFilterManager* m_decoderFilterManager;
+ std::shared_ptr<CCPUInfo> m_cpuInfo;
+ std::shared_ptr<CTextureCache> m_textureCache;
+ std::shared_ptr<CJobManager> m_jobManager;
+ std::shared_ptr<KODI::MESSAGING::CApplicationMessenger> m_appMessenger;
+ std::shared_ptr<CKeyboardLayoutManager> m_keyboardLayoutManager;
+ std::shared_ptr<speech::ISpeechRecognition> m_speechRecognition;
+};
+
+XBMC_GLOBAL_REF(CServiceBroker, g_serviceBroker);
+#define g_serviceBroker XBMC_GLOBAL_USE(CServiceBroker)
diff --git a/xbmc/ServiceManager.cpp b/xbmc/ServiceManager.cpp
new file mode 100644
index 0000000..02d9900
--- /dev/null
+++ b/xbmc/ServiceManager.cpp
@@ -0,0 +1,442 @@
+/*
+ * 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 "ServiceManager.h"
+
+#include "ContextMenuManager.h"
+#include "DatabaseManager.h"
+#include "PlayListPlayer.h"
+#include "addons/AddonManager.h"
+#include "addons/BinaryAddonCache.h"
+#include "addons/ExtsMimeSupportList.h"
+#include "addons/RepositoryUpdater.h"
+#include "addons/Service.h"
+#include "addons/VFSEntry.h"
+#include "addons/binary-addons/BinaryAddonManager.h"
+#include "cores/DataCacheCore.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "favourites/FavouritesService.h"
+#include "games/GameServices.h"
+#include "games/controllers/ControllerManager.h"
+#include "input/InputManager.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "interfaces/python/XBPython.h"
+#include "network/Network.h"
+#include "peripherals/Peripherals.h"
+#if defined(HAS_FILESYSTEM_SMB)
+#include "network/IWSDiscovery.h"
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/network/WSDiscoveryWin32.h"
+#else // !defined(TARGET_WINDOWS)
+#include "platform/posix/filesystem/SMBWSDiscovery.h"
+#endif // defined(TARGET_WINDOWS)
+#endif // HAS_FILESYSTEM_SMB
+#include "powermanagement/PowerManager.h"
+#include "profiles/ProfileManager.h"
+#include "pvr/PVRManager.h"
+#if !defined(TARGET_WINDOWS) && defined(HAS_DVD_DRIVE)
+#include "storage/DetectDVDType.h"
+#endif
+#include "storage/MediaManager.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/log.h"
+#include "weather/WeatherManager.h"
+
+using namespace KODI;
+
+CServiceManager::CServiceManager() = default;
+
+CServiceManager::~CServiceManager()
+{
+ if (init_level > 2)
+ DeinitStageThree();
+ if (init_level > 1)
+ DeinitStageTwo();
+ if (init_level > 0)
+ DeinitStageOne();
+}
+
+bool CServiceManager::InitForTesting()
+{
+ m_network = CNetworkBase::GetNetwork();
+
+ m_databaseManager.reset(new CDatabaseManager);
+
+ m_binaryAddonManager.reset(new ADDON::CBinaryAddonManager());
+ m_addonMgr.reset(new ADDON::CAddonMgr());
+ if (!m_addonMgr->Init())
+ {
+ CLog::Log(LOGFATAL, "CServiceManager::{}: Unable to start CAddonMgr", __FUNCTION__);
+ return false;
+ }
+
+ m_extsMimeSupportList.reset(new ADDONS::CExtsMimeSupportList(*m_addonMgr));
+ m_fileExtensionProvider.reset(new CFileExtensionProvider(*m_addonMgr));
+
+ init_level = 1;
+ return true;
+}
+
+void CServiceManager::DeinitTesting()
+{
+ init_level = 0;
+ m_fileExtensionProvider.reset();
+ m_extsMimeSupportList.reset();
+ m_binaryAddonManager.reset();
+ m_addonMgr.reset();
+ m_databaseManager.reset();
+ m_network.reset();
+}
+
+bool CServiceManager::InitStageOne()
+{
+ m_Platform.reset(CPlatform::CreateInstance());
+ if (!m_Platform->InitStageOne())
+ return false;
+
+#ifdef HAS_PYTHON
+ m_XBPython.reset(new XBPython());
+ CScriptInvocationManager::GetInstance().RegisterLanguageInvocationHandler(m_XBPython.get(),
+ ".py");
+#endif
+
+ m_playlistPlayer.reset(new PLAYLIST::CPlayListPlayer());
+
+ m_network = CNetworkBase::GetNetwork();
+
+ init_level = 1;
+ return true;
+}
+
+bool CServiceManager::InitStageTwo(const std::string& profilesUserDataFolder)
+{
+ // Initialize the addon database (must be before the addon manager is init'd)
+ m_databaseManager.reset(new CDatabaseManager);
+
+ m_binaryAddonManager.reset(
+ new ADDON::
+ CBinaryAddonManager()); /* Need to constructed before, GetRunningInstance() of binary CAddonDll need to call them */
+ m_addonMgr.reset(new ADDON::CAddonMgr());
+ if (!m_addonMgr->Init())
+ {
+ CLog::Log(LOGFATAL, "CServiceManager::{}: Unable to start CAddonMgr", __FUNCTION__);
+ return false;
+ }
+
+ m_repositoryUpdater.reset(new ADDON::CRepositoryUpdater(*m_addonMgr));
+
+ m_extsMimeSupportList.reset(new ADDONS::CExtsMimeSupportList(*m_addonMgr));
+
+ m_vfsAddonCache.reset(new ADDON::CVFSAddonCache());
+ m_vfsAddonCache->Init();
+
+ m_PVRManager.reset(new PVR::CPVRManager());
+
+ m_dataCacheCore.reset(new CDataCacheCore());
+
+ m_binaryAddonCache.reset(new ADDON::CBinaryAddonCache());
+ m_binaryAddonCache->Init();
+
+ m_favouritesService.reset(new CFavouritesService(profilesUserDataFolder));
+
+ m_serviceAddons.reset(new ADDON::CServiceAddonManager(*m_addonMgr));
+
+ m_contextMenuManager.reset(new CContextMenuManager(*m_addonMgr));
+
+ m_gameControllerManager = std::make_unique<GAME::CControllerManager>(*m_addonMgr);
+ m_inputManager.reset(new CInputManager());
+ m_inputManager->InitializeInputs();
+
+ m_peripherals.reset(new PERIPHERALS::CPeripherals(*m_inputManager, *m_gameControllerManager));
+
+ m_gameRenderManager.reset(new RETRO::CGUIGameRenderManager);
+
+ m_fileExtensionProvider.reset(new CFileExtensionProvider(*m_addonMgr));
+
+ m_powerManager.reset(new CPowerManager());
+ m_powerManager->Initialize();
+ m_powerManager->SetDefaults();
+
+ m_weatherManager.reset(new CWeatherManager());
+
+ m_mediaManager.reset(new CMediaManager());
+ m_mediaManager->Initialize();
+
+#if !defined(TARGET_WINDOWS) && defined(HAS_DVD_DRIVE)
+ m_DetectDVDType = std::make_unique<MEDIA_DETECT::CDetectDVDMedia>();
+#endif
+
+#if defined(HAS_FILESYSTEM_SMB)
+ m_WSDiscovery = WSDiscovery::IWSDiscovery::GetInstance();
+#endif
+
+ if (!m_Platform->InitStageTwo())
+ return false;
+
+ init_level = 2;
+ return true;
+}
+
+// stage 3 is called after successful initialization of WindowManager
+bool CServiceManager::InitStageThree(const std::shared_ptr<CProfileManager>& profileManager)
+{
+#if !defined(TARGET_WINDOWS) && defined(HAS_DVD_DRIVE)
+ // Start Thread for DVD Mediatype detection
+ CLog::Log(LOGINFO, "[Media Detection] starting service for optical media detection");
+ m_DetectDVDType->Create(false);
+#endif
+
+ // Peripherals depends on strings being loaded before stage 3
+ m_peripherals->Initialise();
+
+ m_gameServices =
+ std::make_unique<GAME::CGameServices>(*m_gameControllerManager, *m_gameRenderManager,
+ *m_peripherals, *profileManager, *m_inputManager);
+
+ m_contextMenuManager->Init();
+
+ // Init PVR manager after login, not already on login screen
+ if (!profileManager->UsingLoginScreen())
+ m_PVRManager->Init();
+
+ m_playerCoreFactory.reset(new CPlayerCoreFactory(*profileManager));
+
+ if (!m_Platform->InitStageThree())
+ return false;
+
+ init_level = 3;
+ return true;
+}
+
+void CServiceManager::DeinitStageThree()
+{
+ init_level = 2;
+#if !defined(TARGET_WINDOWS) && defined(HAS_DVD_DRIVE)
+ m_DetectDVDType->StopThread();
+ m_DetectDVDType.reset();
+#endif
+ m_playerCoreFactory.reset();
+ m_PVRManager->Deinit();
+ m_contextMenuManager->Deinit();
+ m_gameServices.reset();
+ m_peripherals->Clear();
+
+ m_Platform->DeinitStageThree();
+}
+
+void CServiceManager::DeinitStageTwo()
+{
+ init_level = 1;
+
+#if defined(HAS_FILESYSTEM_SMB)
+ m_WSDiscovery.reset();
+#endif
+
+ m_weatherManager.reset();
+ m_powerManager.reset();
+ m_fileExtensionProvider.reset();
+ m_gameRenderManager.reset();
+ m_peripherals.reset();
+ m_inputManager.reset();
+ m_gameControllerManager.reset();
+ m_contextMenuManager.reset();
+ m_serviceAddons.reset();
+ m_favouritesService.reset();
+ m_binaryAddonCache.reset();
+ m_dataCacheCore.reset();
+ m_PVRManager.reset();
+ m_extsMimeSupportList.reset();
+ m_vfsAddonCache.reset();
+ m_repositoryUpdater.reset();
+ m_binaryAddonManager.reset();
+ m_addonMgr.reset();
+ m_databaseManager.reset();
+
+ m_mediaManager->Stop();
+ m_mediaManager.reset();
+
+ m_Platform->DeinitStageTwo();
+}
+
+void CServiceManager::DeinitStageOne()
+{
+ init_level = 0;
+
+ m_network.reset();
+ m_playlistPlayer.reset();
+#ifdef HAS_PYTHON
+ CScriptInvocationManager::GetInstance().UnregisterLanguageInvocationHandler(m_XBPython.get());
+ m_XBPython.reset();
+#endif
+
+ m_Platform->DeinitStageOne();
+ m_Platform.reset();
+}
+
+#if defined(HAS_FILESYSTEM_SMB)
+WSDiscovery::IWSDiscovery& CServiceManager::GetWSDiscovery()
+{
+ return *m_WSDiscovery;
+}
+#endif
+
+ADDON::CAddonMgr& CServiceManager::GetAddonMgr()
+{
+ return *m_addonMgr;
+}
+
+ADDONS::CExtsMimeSupportList& CServiceManager::GetExtsMimeSupportList()
+{
+ return *m_extsMimeSupportList;
+}
+
+ADDON::CBinaryAddonCache& CServiceManager::GetBinaryAddonCache()
+{
+ return *m_binaryAddonCache;
+}
+
+ADDON::CBinaryAddonManager& CServiceManager::GetBinaryAddonManager()
+{
+ return *m_binaryAddonManager;
+}
+
+ADDON::CVFSAddonCache& CServiceManager::GetVFSAddonCache()
+{
+ return *m_vfsAddonCache;
+}
+
+ADDON::CServiceAddonManager& CServiceManager::GetServiceAddons()
+{
+ return *m_serviceAddons;
+}
+
+ADDON::CRepositoryUpdater& CServiceManager::GetRepositoryUpdater()
+{
+ return *m_repositoryUpdater;
+}
+
+#ifdef HAS_PYTHON
+XBPython& CServiceManager::GetXBPython()
+{
+ return *m_XBPython;
+}
+#endif
+
+#if !defined(TARGET_WINDOWS) && defined(HAS_DVD_DRIVE)
+MEDIA_DETECT::CDetectDVDMedia& CServiceManager::GetDetectDVDMedia()
+{
+ return *m_DetectDVDType;
+}
+#endif
+
+PVR::CPVRManager& CServiceManager::GetPVRManager()
+{
+ return *m_PVRManager;
+}
+
+CContextMenuManager& CServiceManager::GetContextMenuManager()
+{
+ return *m_contextMenuManager;
+}
+
+CDataCacheCore& CServiceManager::GetDataCacheCore()
+{
+ return *m_dataCacheCore;
+}
+
+CPlatform& CServiceManager::GetPlatform()
+{
+ return *m_Platform;
+}
+
+PLAYLIST::CPlayListPlayer& CServiceManager::GetPlaylistPlayer()
+{
+ return *m_playlistPlayer;
+}
+
+GAME::CControllerManager& CServiceManager::GetGameControllerManager()
+{
+ return *m_gameControllerManager;
+}
+
+GAME::CGameServices& CServiceManager::GetGameServices()
+{
+ return *m_gameServices;
+}
+
+KODI::RETRO::CGUIGameRenderManager& CServiceManager::GetGameRenderManager()
+{
+ return *m_gameRenderManager;
+}
+
+PERIPHERALS::CPeripherals& CServiceManager::GetPeripherals()
+{
+ return *m_peripherals;
+}
+
+CFavouritesService& CServiceManager::GetFavouritesService()
+{
+ return *m_favouritesService;
+}
+
+CInputManager& CServiceManager::GetInputManager()
+{
+ return *m_inputManager;
+}
+
+CFileExtensionProvider& CServiceManager::GetFileExtensionProvider()
+{
+ return *m_fileExtensionProvider;
+}
+
+CPowerManager& CServiceManager::GetPowerManager()
+{
+ return *m_powerManager;
+}
+
+// deleters for unique_ptr
+void CServiceManager::delete_dataCacheCore::operator()(CDataCacheCore* p) const
+{
+ delete p;
+}
+
+void CServiceManager::delete_contextMenuManager::operator()(CContextMenuManager* p) const
+{
+ delete p;
+}
+
+void CServiceManager::delete_favouritesService::operator()(CFavouritesService* p) const
+{
+ delete p;
+}
+
+CNetworkBase& CServiceManager::GetNetwork()
+{
+ return *m_network;
+}
+
+CWeatherManager& CServiceManager::GetWeatherManager()
+{
+ return *m_weatherManager;
+}
+
+CPlayerCoreFactory& CServiceManager::GetPlayerCoreFactory()
+{
+ return *m_playerCoreFactory;
+}
+
+CDatabaseManager& CServiceManager::GetDatabaseManager()
+{
+ return *m_databaseManager;
+}
+
+CMediaManager& CServiceManager::GetMediaManager()
+{
+ return *m_mediaManager;
+}
diff --git a/xbmc/ServiceManager.h b/xbmc/ServiceManager.h
new file mode 100644
index 0000000..9c99811
--- /dev/null
+++ b/xbmc/ServiceManager.h
@@ -0,0 +1,200 @@
+/*
+ * 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 "platform/Platform.h"
+
+#include <memory>
+
+namespace ADDON
+{
+class CAddonMgr;
+class CBinaryAddonManager;
+class CBinaryAddonCache;
+class CVFSAddonCache;
+class CServiceAddonManager;
+class CRepositoryUpdater;
+} // namespace ADDON
+
+namespace PVR
+{
+class CPVRManager;
+}
+
+namespace PLAYLIST
+{
+class CPlayListPlayer;
+}
+
+class CContextMenuManager;
+#ifdef HAS_PYTHON
+class XBPython;
+#endif
+#if defined(HAS_FILESYSTEM_SMB)
+namespace WSDiscovery
+{
+class IWSDiscovery;
+}
+#endif
+class CDataCacheCore;
+class CFavouritesService;
+class CNetworkBase;
+class CWinSystemBase;
+class CPowerManager;
+class CWeatherManager;
+
+namespace KODI
+{
+namespace ADDONS
+{
+class CExtsMimeSupportList;
+}
+
+namespace GAME
+{
+class CControllerManager;
+class CGameServices;
+} // namespace GAME
+
+namespace RETRO
+{
+class CGUIGameRenderManager;
+}
+} // namespace KODI
+
+namespace MEDIA_DETECT
+{
+class CDetectDVDMedia;
+}
+
+namespace PERIPHERALS
+{
+class CPeripherals;
+}
+
+class CInputManager;
+class CFileExtensionProvider;
+class CPlayerCoreFactory;
+class CDatabaseManager;
+class CProfileManager;
+class CEventLog;
+class CMediaManager;
+
+class CServiceManager
+{
+public:
+ CServiceManager();
+ ~CServiceManager();
+
+ bool InitForTesting();
+ bool InitStageOne();
+ bool InitStageTwo(const std::string& profilesUserDataFolder);
+ bool InitStageThree(const std::shared_ptr<CProfileManager>& profileManager);
+ void DeinitTesting();
+ void DeinitStageThree();
+ void DeinitStageTwo();
+ void DeinitStageOne();
+
+ ADDON::CAddonMgr& GetAddonMgr();
+ ADDON::CBinaryAddonManager& GetBinaryAddonManager();
+ ADDON::CBinaryAddonCache& GetBinaryAddonCache();
+ KODI::ADDONS::CExtsMimeSupportList& GetExtsMimeSupportList();
+ ADDON::CVFSAddonCache& GetVFSAddonCache();
+ ADDON::CServiceAddonManager& GetServiceAddons();
+ ADDON::CRepositoryUpdater& GetRepositoryUpdater();
+ CNetworkBase& GetNetwork();
+#ifdef HAS_PYTHON
+ XBPython& GetXBPython();
+#endif
+#if defined(HAS_FILESYSTEM_SMB)
+ WSDiscovery::IWSDiscovery& GetWSDiscovery();
+#endif
+ PVR::CPVRManager& GetPVRManager();
+ CContextMenuManager& GetContextMenuManager();
+ CDataCacheCore& GetDataCacheCore();
+ /**\brief Get the platform object. This is save to be called after Init1() was called
+ */
+ CPlatform& GetPlatform();
+ KODI::GAME::CControllerManager& GetGameControllerManager();
+ KODI::GAME::CGameServices& GetGameServices();
+ KODI::RETRO::CGUIGameRenderManager& GetGameRenderManager();
+ PERIPHERALS::CPeripherals& GetPeripherals();
+
+ PLAYLIST::CPlayListPlayer& GetPlaylistPlayer();
+ int init_level = 0;
+
+ CFavouritesService& GetFavouritesService();
+ CInputManager& GetInputManager();
+ CFileExtensionProvider& GetFileExtensionProvider();
+
+ CPowerManager& GetPowerManager();
+
+ CWeatherManager& GetWeatherManager();
+
+ CPlayerCoreFactory& GetPlayerCoreFactory();
+
+ CDatabaseManager& GetDatabaseManager();
+
+ CMediaManager& GetMediaManager();
+
+#if !defined(TARGET_WINDOWS) && defined(HAS_DVD_DRIVE)
+ MEDIA_DETECT::CDetectDVDMedia& GetDetectDVDMedia();
+#endif
+
+protected:
+ struct delete_dataCacheCore
+ {
+ void operator()(CDataCacheCore* p) const;
+ };
+
+ struct delete_contextMenuManager
+ {
+ void operator()(CContextMenuManager* p) const;
+ };
+
+ struct delete_favouritesService
+ {
+ void operator()(CFavouritesService* p) const;
+ };
+
+ std::unique_ptr<ADDON::CAddonMgr> m_addonMgr;
+ std::unique_ptr<ADDON::CBinaryAddonManager> m_binaryAddonManager;
+ std::unique_ptr<ADDON::CBinaryAddonCache> m_binaryAddonCache;
+ std::unique_ptr<KODI::ADDONS::CExtsMimeSupportList> m_extsMimeSupportList;
+ std::unique_ptr<ADDON::CVFSAddonCache> m_vfsAddonCache;
+ std::unique_ptr<ADDON::CServiceAddonManager> m_serviceAddons;
+ std::unique_ptr<ADDON::CRepositoryUpdater> m_repositoryUpdater;
+#if defined(HAS_FILESYSTEM_SMB)
+ std::unique_ptr<WSDiscovery::IWSDiscovery> m_WSDiscovery;
+#endif
+#ifdef HAS_PYTHON
+ std::unique_ptr<XBPython> m_XBPython;
+#endif
+ std::unique_ptr<PVR::CPVRManager> m_PVRManager;
+ std::unique_ptr<CContextMenuManager, delete_contextMenuManager> m_contextMenuManager;
+ std::unique_ptr<CDataCacheCore, delete_dataCacheCore> m_dataCacheCore;
+ std::unique_ptr<CPlatform> m_Platform;
+ std::unique_ptr<PLAYLIST::CPlayListPlayer> m_playlistPlayer;
+ std::unique_ptr<KODI::GAME::CControllerManager> m_gameControllerManager;
+ std::unique_ptr<KODI::GAME::CGameServices> m_gameServices;
+ std::unique_ptr<KODI::RETRO::CGUIGameRenderManager> m_gameRenderManager;
+ std::unique_ptr<PERIPHERALS::CPeripherals> m_peripherals;
+ std::unique_ptr<CFavouritesService, delete_favouritesService> m_favouritesService;
+ std::unique_ptr<CInputManager> m_inputManager;
+ std::unique_ptr<CFileExtensionProvider> m_fileExtensionProvider;
+ std::unique_ptr<CNetworkBase> m_network;
+ std::unique_ptr<CPowerManager> m_powerManager;
+ std::unique_ptr<CWeatherManager> m_weatherManager;
+ std::unique_ptr<CPlayerCoreFactory> m_playerCoreFactory;
+ std::unique_ptr<CDatabaseManager> m_databaseManager;
+ std::unique_ptr<CMediaManager> m_mediaManager;
+#if !defined(TARGET_WINDOWS) && defined(HAS_DVD_DRIVE)
+ std::unique_ptr<MEDIA_DETECT::CDetectDVDMedia> m_DetectDVDType;
+#endif
+};
diff --git a/xbmc/SettingsLock.h b/xbmc/SettingsLock.h
new file mode 100644
index 0000000..bc140f0
--- /dev/null
+++ b/xbmc/SettingsLock.h
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+namespace LOCK_LEVEL {
+ /**
+ Specifies, what Settings levels are locked for the user
+ **/
+ enum SETTINGS_LOCK
+ {
+ NONE, //settings are unlocked => user can access all settings levels
+ ALL, //all settings are locked => user always has to enter password, when entering the settings screen
+ STANDARD, //settings level standard and up are locked => user can still access the beginner levels
+ ADVANCED,
+ EXPERT
+ };
+}
+
+
diff --git a/xbmc/SortFileItem.h b/xbmc/SortFileItem.h
new file mode 100644
index 0000000..a5c29a5
--- /dev/null
+++ b/xbmc/SortFileItem.h
@@ -0,0 +1,67 @@
+/*
+ * 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
+
+typedef enum
+{
+ SORT_METHOD_NONE = 0,
+ SORT_METHOD_LABEL,
+ SORT_METHOD_LABEL_IGNORE_THE,
+ SORT_METHOD_DATE,
+ SORT_METHOD_SIZE,
+ SORT_METHOD_FILE,
+ SORT_METHOD_DRIVE_TYPE,
+ SORT_METHOD_TRACKNUM,
+ SORT_METHOD_DURATION,
+ SORT_METHOD_TITLE,
+ SORT_METHOD_TITLE_IGNORE_THE,
+ SORT_METHOD_ARTIST,
+ SORT_METHOD_ARTIST_AND_YEAR,
+ SORT_METHOD_ARTIST_IGNORE_THE,
+ SORT_METHOD_ALBUM,
+ SORT_METHOD_ALBUM_IGNORE_THE,
+ SORT_METHOD_GENRE,
+ SORT_METHOD_COUNTRY,
+ SORT_METHOD_YEAR,
+ SORT_METHOD_VIDEO_RATING,
+ SORT_METHOD_VIDEO_USER_RATING,
+ SORT_METHOD_DATEADDED,
+ SORT_METHOD_PROGRAM_COUNT,
+ SORT_METHOD_PLAYLIST_ORDER,
+ SORT_METHOD_EPISODE,
+ SORT_METHOD_VIDEO_TITLE,
+ SORT_METHOD_VIDEO_SORT_TITLE,
+ SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE,
+ SORT_METHOD_PRODUCTIONCODE,
+ SORT_METHOD_SONG_RATING,
+ SORT_METHOD_SONG_USER_RATING,
+ SORT_METHOD_MPAA_RATING,
+ SORT_METHOD_VIDEO_RUNTIME,
+ SORT_METHOD_STUDIO,
+ SORT_METHOD_STUDIO_IGNORE_THE,
+ SORT_METHOD_FULLPATH,
+ SORT_METHOD_LABEL_IGNORE_FOLDERS,
+ SORT_METHOD_LASTPLAYED,
+ SORT_METHOD_PLAYCOUNT,
+ SORT_METHOD_LISTENERS,
+ SORT_METHOD_UNSORTED,
+ SORT_METHOD_CHANNEL,
+ SORT_METHOD_CHANNEL_NUMBER,
+ SORT_METHOD_BITRATE,
+ SORT_METHOD_DATE_TAKEN,
+ SORT_METHOD_CLIENT_CHANNEL_ORDER,
+ SORT_METHOD_TOTAL_DISCS,
+ SORT_METHOD_ORIG_DATE,
+ SORT_METHOD_BPM,
+ SORT_METHOD_VIDEO_ORIGINAL_TITLE,
+ SORT_METHOD_VIDEO_ORIGINAL_TITLE_IGNORE_THE,
+ SORT_METHOD_PROVIDER,
+ SORT_METHOD_USER_PREFERENCE,
+ SORT_METHOD_MAX
+} SORT_METHOD;
diff --git a/xbmc/SystemGlobals.cpp b/xbmc/SystemGlobals.cpp
new file mode 100644
index 0000000..6bb854c
--- /dev/null
+++ b/xbmc/SystemGlobals.cpp
@@ -0,0 +1,41 @@
+/*
+ * 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 "SectionLoader.h"
+#include "utils/AlarmClock.h"
+#include "GUIInfoManager.h"
+#include "filesystem/DllLibCurl.h"
+#include "filesystem/DirectoryCache.h"
+#include "GUIPassword.h"
+#include "utils/LangCodeExpander.h"
+#include "PartyModeManager.h"
+#include "guilib/LocalizeStrings.h"
+#ifdef HAS_PYTHON
+#include "interfaces/python/XBPython.h"
+#endif
+
+// Guarantee that CSpecialProtocol is initialized before and uninitialized after ZipManager
+#include "filesystem/SpecialProtocol.h"
+std::map<std::string, std::string> CSpecialProtocol::m_pathMap;
+
+#include "filesystem/ZipManager.h"
+
+ CLangCodeExpander g_LangCodeExpander;
+ CLocalizeStrings g_localizeStrings;
+ CLocalizeStrings g_localizeStringsTemp;
+
+ XFILE::CDirectoryCache g_directoryCache;
+
+ CGUIPassword g_passwordManager;
+
+ XCURL::DllLibCurlGlobal g_curlInterface;
+ CPartyModeManager g_partyModeManager;
+
+ CAlarmClock g_alarmClock;
+ CSectionLoader g_sectionLoader;
+
+ CZipManager g_ZipManager;
diff --git a/xbmc/TextureCache.cpp b/xbmc/TextureCache.cpp
new file mode 100644
index 0000000..6eeb098
--- /dev/null
+++ b/xbmc/TextureCache.cpp
@@ -0,0 +1,350 @@
+/*
+ * 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 "TextureCache.h"
+
+#include "ServiceBroker.h"
+#include "TextureCacheJob.h"
+#include "URL.h"
+#include "commons/ilog.h"
+#include "filesystem/File.h"
+#include "filesystem/IFileTypes.h"
+#include "guilib/Texture.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Crc32.h"
+#include "utils/Job.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <chrono>
+#include <exception>
+#include <mutex>
+#include <string.h>
+
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+CTextureCache::CTextureCache() : CJobQueue(false, 1, CJob::PRIORITY_LOW_PAUSABLE)
+{
+}
+
+CTextureCache::~CTextureCache() = default;
+
+void CTextureCache::Initialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_databaseSection);
+ if (!m_database.IsOpen())
+ m_database.Open();
+}
+
+void CTextureCache::Deinitialize()
+{
+ CancelJobs();
+
+ std::unique_lock<CCriticalSection> lock(m_databaseSection);
+ m_database.Close();
+}
+
+bool CTextureCache::IsCachedImage(const std::string &url) const
+{
+ if (url.empty())
+ return false;
+
+ if (!CURL::IsFullPath(url))
+ return true;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ return URIUtils::PathHasParent(url, "special://skin", true) ||
+ URIUtils::PathHasParent(url, "special://temp", true) ||
+ URIUtils::PathHasParent(url, "resource://", true) ||
+ URIUtils::PathHasParent(url, "androidapp://", true) ||
+ URIUtils::PathHasParent(url, profileManager->GetThumbnailsFolder(), true);
+}
+
+bool CTextureCache::HasCachedImage(const std::string &url)
+{
+ CTextureDetails details;
+ std::string cachedImage(GetCachedImage(url, details));
+ return (!cachedImage.empty() && cachedImage != url);
+}
+
+std::string CTextureCache::GetCachedImage(const std::string &image, CTextureDetails &details, bool trackUsage)
+{
+ std::string url = CTextureUtils::UnwrapImageURL(image);
+ if (url.empty())
+ return "";
+ if (IsCachedImage(url))
+ return url;
+
+ // lookup the item in the database
+ if (GetCachedTexture(url, details))
+ {
+ if (trackUsage)
+ IncrementUseCount(details);
+ return GetCachedPath(details.file);
+ }
+ return "";
+}
+
+bool CTextureCache::CanCacheImageURL(const CURL &url)
+{
+ return url.GetUserName().empty() || url.GetUserName() == "music" ||
+ StringUtils::StartsWith(url.GetUserName(), "video_") ||
+ StringUtils::StartsWith(url.GetUserName(), "pvr") ||
+ StringUtils::StartsWith(url.GetUserName(), "epg");
+}
+
+std::string CTextureCache::CheckCachedImage(const std::string &url, bool &needsRecaching)
+{
+ CTextureDetails details;
+ std::string path(GetCachedImage(url, details, true));
+ needsRecaching = !details.hash.empty();
+ if (!path.empty())
+ return path;
+ return "";
+}
+
+void CTextureCache::BackgroundCacheImage(const std::string &url)
+{
+ if (url.empty())
+ return;
+
+ CTextureDetails details;
+ std::string path(GetCachedImage(url, details));
+ if (!path.empty() && details.hash.empty())
+ return; // image is already cached and doesn't need to be checked further
+
+ path = CTextureUtils::UnwrapImageURL(url);
+ if (path.empty())
+ return;
+
+ // needs (re)caching
+ AddJob(new CTextureCacheJob(path, details.hash));
+}
+
+bool CTextureCache::StartCacheImage(const std::string& image)
+{
+ std::unique_lock<CCriticalSection> lock(m_processingSection);
+ std::set<std::string>::iterator i = m_processinglist.find(image);
+ if (i == m_processinglist.end())
+ {
+ m_processinglist.insert(image);
+ return true;
+ }
+ return false;
+}
+
+std::string CTextureCache::CacheImage(const std::string& image,
+ std::unique_ptr<CTexture>* texture /*= nullptr*/,
+ CTextureDetails* details /*= nullptr*/)
+{
+ std::string url = CTextureUtils::UnwrapImageURL(image);
+ if (url.empty())
+ return "";
+
+ std::unique_lock<CCriticalSection> lock(m_processingSection);
+ if (m_processinglist.find(url) == m_processinglist.end())
+ {
+ m_processinglist.insert(url);
+ lock.unlock();
+ // cache the texture directly
+ CTextureCacheJob job(url);
+ bool success = job.CacheTexture(texture);
+ OnCachingComplete(success, &job);
+ if (success && details)
+ *details = job.m_details;
+ return success ? GetCachedPath(job.m_details.file) : "";
+ }
+ lock.unlock();
+
+ // wait for currently processing job to end.
+ while (true)
+ {
+ m_completeEvent.Wait(1000ms);
+ {
+ std::unique_lock<CCriticalSection> lock(m_processingSection);
+ if (m_processinglist.find(url) == m_processinglist.end())
+ break;
+ }
+ }
+ CTextureDetails tempDetails;
+ if (!details)
+ details = &tempDetails;
+
+ std::string cachedpath = GetCachedImage(url, *details, true);
+ if (!cachedpath.empty())
+ {
+ if (texture)
+ *texture = CTexture::LoadFromFile(cachedpath, 0, 0);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "CTextureCache::{} - Return NULL texture because cache is not ready",
+ __FUNCTION__);
+ }
+
+ return cachedpath;
+}
+
+bool CTextureCache::CacheImage(const std::string &image, CTextureDetails &details)
+{
+ std::string path = GetCachedImage(image, details);
+ if (path.empty()) // not cached
+ path = CacheImage(image, NULL, &details);
+
+ return !path.empty();
+}
+
+void CTextureCache::ClearCachedImage(const std::string& image, bool deleteSource /*= false */)
+{
+ //! @todo This can be removed when the texture cache covers everything.
+ const std::string url = CTextureUtils::UnwrapImageURL(image);
+ std::string path = deleteSource ? url : "";
+ std::string cachedFile;
+ if (ClearCachedTexture(url, cachedFile))
+ path = GetCachedPath(cachedFile);
+ if (CFile::Exists(path))
+ CFile::Delete(path);
+ path = URIUtils::ReplaceExtension(path, ".dds");
+ if (CFile::Exists(path))
+ CFile::Delete(path);
+}
+
+bool CTextureCache::ClearCachedImage(int id)
+{
+ std::string cachedFile;
+ if (ClearCachedTexture(id, cachedFile))
+ {
+ cachedFile = GetCachedPath(cachedFile);
+ if (CFile::Exists(cachedFile))
+ CFile::Delete(cachedFile);
+ cachedFile = URIUtils::ReplaceExtension(cachedFile, ".dds");
+ if (CFile::Exists(cachedFile))
+ CFile::Delete(cachedFile);
+ return true;
+ }
+ return false;
+}
+
+bool CTextureCache::GetCachedTexture(const std::string &url, CTextureDetails &details)
+{
+ std::unique_lock<CCriticalSection> lock(m_databaseSection);
+ return m_database.GetCachedTexture(url, details);
+}
+
+bool CTextureCache::AddCachedTexture(const std::string &url, const CTextureDetails &details)
+{
+ std::unique_lock<CCriticalSection> lock(m_databaseSection);
+ return m_database.AddCachedTexture(url, details);
+}
+
+void CTextureCache::IncrementUseCount(const CTextureDetails &details)
+{
+ static const size_t count_before_update = 100;
+ std::unique_lock<CCriticalSection> lock(m_useCountSection);
+ m_useCounts.reserve(count_before_update);
+ m_useCounts.push_back(details);
+ if (m_useCounts.size() >= count_before_update)
+ {
+ AddJob(new CTextureUseCountJob(m_useCounts));
+ m_useCounts.clear();
+ }
+}
+
+bool CTextureCache::SetCachedTextureValid(const std::string &url, bool updateable)
+{
+ std::unique_lock<CCriticalSection> lock(m_databaseSection);
+ return m_database.SetCachedTextureValid(url, updateable);
+}
+
+bool CTextureCache::ClearCachedTexture(const std::string &url, std::string &cachedURL)
+{
+ std::unique_lock<CCriticalSection> lock(m_databaseSection);
+ return m_database.ClearCachedTexture(url, cachedURL);
+}
+
+bool CTextureCache::ClearCachedTexture(int id, std::string &cachedURL)
+{
+ std::unique_lock<CCriticalSection> lock(m_databaseSection);
+ return m_database.ClearCachedTexture(id, cachedURL);
+}
+
+std::string CTextureCache::GetCacheFile(const std::string &url)
+{
+ auto crc = Crc32::ComputeFromLowerCase(url);
+ std::string hex = StringUtils::Format("{:08x}", crc);
+ std::string hash = StringUtils::Format("{}/{}", hex[0], hex.c_str());
+ return hash;
+}
+
+std::string CTextureCache::GetCachedPath(const std::string &file)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ return URIUtils::AddFileToFolder(profileManager->GetThumbnailsFolder(), file);
+}
+
+void CTextureCache::OnCachingComplete(bool success, CTextureCacheJob *job)
+{
+ if (success)
+ {
+ if (job->m_oldHash == job->m_details.hash)
+ SetCachedTextureValid(job->m_url, job->m_details.updateable);
+ else
+ AddCachedTexture(job->m_url, job->m_details);
+ }
+
+ { // remove from our processing list
+ std::unique_lock<CCriticalSection> lock(m_processingSection);
+ std::set<std::string>::iterator i = m_processinglist.find(job->m_url);
+ if (i != m_processinglist.end())
+ m_processinglist.erase(i);
+ }
+
+ m_completeEvent.Set();
+}
+
+void CTextureCache::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ if (strcmp(job->GetType(), kJobTypeCacheImage) == 0)
+ OnCachingComplete(success, static_cast<CTextureCacheJob*>(job));
+ return CJobQueue::OnJobComplete(jobID, success, job);
+}
+
+bool CTextureCache::Export(const std::string &image, const std::string &destination, bool overwrite)
+{
+ CTextureDetails details;
+ std::string cachedImage(GetCachedImage(image, details));
+ if (!cachedImage.empty())
+ {
+ std::string dest = destination + URIUtils::GetExtension(cachedImage);
+ if (overwrite || !CFile::Exists(dest))
+ {
+ if (CFile::Copy(cachedImage, dest))
+ return true;
+ CLog::Log(LOGERROR, "{} failed exporting '{}' to '{}'", __FUNCTION__, cachedImage, dest);
+ }
+ }
+ return false;
+}
+
+bool CTextureCache::Export(const std::string &image, const std::string &destination)
+{
+ CTextureDetails details;
+ std::string cachedImage(GetCachedImage(image, details));
+ if (!cachedImage.empty())
+ {
+ if (CFile::Copy(cachedImage, destination))
+ return true;
+ CLog::Log(LOGERROR, "{} failed exporting '{}' to '{}'", __FUNCTION__, cachedImage, destination);
+ }
+ return false;
+}
diff --git a/xbmc/TextureCache.h b/xbmc/TextureCache.h
new file mode 100644
index 0000000..dae043f
--- /dev/null
+++ b/xbmc/TextureCache.h
@@ -0,0 +1,230 @@
+/*
+ * 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 "TextureCacheJob.h"
+#include "TextureDatabase.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "utils/JobManager.h"
+
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+class CJob;
+class CURL;
+class CTexture;
+
+/*!
+ \ingroup textures
+ \brief Texture cache class for handling the caching of images.
+
+ Manages the caching of images for use as control textures. Images are cached
+ both as originals (direct copies) and as .dds textures for fast loading. Images
+ may be periodically checked for updates and may be purged from the cache if
+ unused for a set period of time.
+
+ */
+class CTextureCache : public CJobQueue
+{
+public:
+ CTextureCache();
+ ~CTextureCache() override;
+
+ /*! \brief Initialize the texture cache
+ */
+ void Initialize();
+
+ /*! \brief Deinitialize the texture cache
+ */
+ void Deinitialize();
+
+ /*! \brief Check whether we already have this image cached
+
+ Check and return URL to cached image if it exists; If not, return empty string.
+ If the image is cached, return URL (for original image or .dds version if requested)
+
+ \param image url of the image to check
+ \param needsRecaching [out] whether the image needs recaching.
+ \return cached url of this image
+ \sa GetCachedImage
+ */
+ std::string CheckCachedImage(const std::string &image, bool &needsRecaching);
+
+ /*! \brief Cache image (if required) using a background job
+
+ Checks firstly whether an image is already cached, and return URL if so [see CheckCacheImage]
+ If the image is not yet in the database, a background job is started to
+ cache the image and add to the database [see CTextureCacheJob]
+
+ \param image url of the image to cache
+ \sa CacheImage
+ */
+ void BackgroundCacheImage(const std::string &image);
+
+ /*! \brief Updates the in-process list.
+
+ Inserts the image url into the currently processing list
+ to avoid 2 jobs being processed at once
+
+ \param image url of the image to start processing
+ \return true if list updated, false otherwise
+ \sa CacheImage
+ */
+ bool StartCacheImage(const std::string& image);
+
+ /*! \brief Cache an image to image cache, optionally return the texture
+
+ Caches the given image, returning the texture if the caller wants it.
+
+ \param image url of the image to cache
+ \param texture [out] the loaded image
+ \param details [out] details of the cached image
+ \return cached url of this image
+ \sa CTextureCacheJob::CacheTexture
+ */
+ std::string CacheImage(const std::string& image,
+ std::unique_ptr<CTexture>* texture = nullptr,
+ CTextureDetails* details = nullptr);
+
+ /*! \brief Cache an image to image cache if not already cached, returning the image details.
+ \param image url of the image to cache.
+ \param details [out] the image details.
+ \return true if the image is in the cache, false otherwise.
+ \sa CTextureCacheJob::CacheTexture
+ */
+ bool CacheImage(const std::string &image, CTextureDetails &details);
+
+ /*! \brief Check whether an image is in the cache
+ Note: If the image url won't normally be cached (eg a skin image) this function will return false.
+ \param image url of the image
+ \return true if the image is cached, false otherwise
+ \sa ClearCachedImage
+ */
+ bool HasCachedImage(const std::string &image);
+
+ /*! \brief clear the cached version of the given image
+ \param image url of the image
+ \sa GetCachedImage
+ */
+ void ClearCachedImage(const std::string &image, bool deleteSource = false);
+
+ /*! \brief clear the cached version of the image with given id
+ \param database id of the image
+ \sa GetCachedImage
+ */
+ bool ClearCachedImage(int textureID);
+
+ /*! \brief retrieve a cache file (relative to the cache path) to associate with the given image, excluding extension
+ Use GetCachedPath(GetCacheFile(url)+extension) for the full path to the file.
+ \param url location of the image
+ \return a "unique" filename for the associated cache file, excluding extension
+ */
+ static std::string GetCacheFile(const std::string &url);
+
+ /*! \brief retrieve the full path of the given cached file
+ \param file name of the file
+ \return full path of the cached file
+ */
+ static std::string GetCachedPath(const std::string &file);
+
+ /*! \brief check whether an image:// URL may be cached
+ \param url the URL to the image
+ \return true if the given URL may be cached, false otherwise
+ */
+ static bool CanCacheImageURL(const CURL &url);
+
+ /*! \brief Add this image to the database
+ Thread-safe wrapper of CTextureDatabase::AddCachedTexture
+ \param image url of the original image
+ \param details the texture details to add
+ \return true if we successfully added to the database, false otherwise.
+ */
+ bool AddCachedTexture(const std::string &image, const CTextureDetails &details);
+
+ /*! \brief Export a (possibly) cached image to a file
+ \param image url of the original image
+ \param destination url of the destination image, excluding extension.
+ \param overwrite whether to overwrite the destination if it exists (TODO: Defaults to false)
+ \return true if we successfully exported the file, false otherwise.
+ */
+ bool Export(const std::string &image, const std::string &destination, bool overwrite);
+ bool Export(const std::string &image, const std::string &destination); //! @todo BACKWARD COMPATIBILITY FOR MUSIC THUMBS
+private:
+ // private construction, and no assignments; use the provided singleton methods
+ CTextureCache(const CTextureCache&) = delete;
+ CTextureCache const& operator=(CTextureCache const&) = delete;
+
+ /*! \brief Check if the given image is a cached image
+ \param image url of the image
+ \return true if this is a cached image, false otherwise.
+ */
+ bool IsCachedImage(const std::string &image) const;
+
+ /*! \brief retrieve the cached version of the given image (if it exists)
+ \param image url of the image
+ \param details [out] the details of the texture.
+ \param trackUsage whether this call should track usage of the image (defaults to false)
+ \return cached url of this image, empty if none exists
+ \sa ClearCachedImage, CTextureDetails
+ */
+ std::string GetCachedImage(const std::string &image, CTextureDetails &details, bool trackUsage = false);
+
+ /*! \brief Get an image from the database
+ Thread-safe wrapper of CTextureDatabase::GetCachedTexture
+ \param image url of the original image
+ \param details [out] texture details from the database (if available)
+ \return true if we have a cached version of this image, false otherwise.
+ */
+ bool GetCachedTexture(const std::string &url, CTextureDetails &details);
+
+ /*! \brief Clear an image from the database
+ Thread-safe wrapper of CTextureDatabase::ClearCachedTexture
+ \param image url of the original image
+ \param cacheFile [out] url of the cached original (if available)
+ \return true if we had a cached version of this image, false otherwise.
+ */
+ bool ClearCachedTexture(const std::string &url, std::string &cacheFile);
+ bool ClearCachedTexture(int textureID, std::string &cacheFile);
+
+ /*! \brief Increment the use count of a texture
+ Stores locally before calling CTextureDatabase::IncrementUseCount via a CUseCountJob
+ \sa CUseCountJob, CTextureDatabase::IncrementUseCount
+ */
+ void IncrementUseCount(const CTextureDetails &details);
+
+ /*! \brief Set a previously cached texture as valid in the database
+ Thread-safe wrapper of CTextureDatabase::SetCachedTextureValid
+ \param image url of the original image
+ \param updateable whether this image should be checked for updates
+ \return true if successful, false otherwise.
+ */
+ bool SetCachedTextureValid(const std::string &url, bool updateable);
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ /*! \brief Called when a caching job has completed.
+ Removes the job from our processing list, updates the database
+ and fires a DDS job if appropriate.
+ \param success whether the job was successful.
+ \param job the caching job.
+ */
+ void OnCachingComplete(bool success, CTextureCacheJob *job);
+
+ CCriticalSection m_databaseSection;
+ CTextureDatabase m_database;
+ std::set<std::string> m_processinglist; ///< currently processing list to avoid 2 jobs being processed at once
+ CCriticalSection m_processingSection;
+ CEvent m_completeEvent; ///< Set whenever a job has finished
+ std::vector<CTextureDetails> m_useCounts; ///< Use count tracking
+ CCriticalSection m_useCountSection;
+};
+
diff --git a/xbmc/TextureCacheJob.cpp b/xbmc/TextureCacheJob.cpp
new file mode 100644
index 0000000..02ef0f4
--- /dev/null
+++ b/xbmc/TextureCacheJob.cpp
@@ -0,0 +1,293 @@
+/*
+ * 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 "TextureCacheJob.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "TextureDatabase.h"
+#include "URL.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audiodecoder.h"
+#include "commons/ilog.h"
+#include "filesystem/File.h"
+#include "guilib/Texture.h"
+#include "music/MusicThumbLoader.h"
+#include "pictures/Picture.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/EmbeddedArt.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoThumbLoader.h"
+
+#include <cstdlib>
+#include <cstring>
+#include <exception>
+#include <utility>
+
+#include "PlatformDefs.h"
+
+CTextureCacheJob::CTextureCacheJob(const std::string &url, const std::string &oldHash):
+ m_url(url),
+ m_oldHash(oldHash),
+ m_cachePath(CTextureCache::GetCacheFile(m_url))
+{
+}
+
+CTextureCacheJob::~CTextureCacheJob() = default;
+
+bool CTextureCacheJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(),GetType()) == 0)
+ {
+ const CTextureCacheJob* cacheJob = dynamic_cast<const CTextureCacheJob*>(job);
+ if (cacheJob && cacheJob->m_cachePath == m_cachePath)
+ return true;
+ }
+ return false;
+}
+
+bool CTextureCacheJob::DoWork()
+{
+ if (ShouldCancel(0, 0))
+ return false;
+
+ // check whether we need cache the job anyway
+ bool needsRecaching = false;
+ std::string path(CServiceBroker::GetTextureCache()->CheckCachedImage(m_url, needsRecaching));
+ if (!path.empty() && !needsRecaching)
+ return false;
+ if (CServiceBroker::GetTextureCache()->StartCacheImage(m_url))
+ return CacheTexture();
+
+ return false;
+}
+
+bool CTextureCacheJob::CacheTexture(std::unique_ptr<CTexture>* out_texture)
+{
+ // unwrap the URL as required
+ std::string additional_info;
+ unsigned int width, height;
+ CPictureScalingAlgorithm::Algorithm scalingAlgorithm;
+ std::string image = DecodeImageURL(m_url, width, height, scalingAlgorithm, additional_info);
+
+ m_details.updateable = additional_info != "music" && UpdateableURL(image);
+
+ // generate the hash
+ m_details.hash = GetImageHash(image);
+ if (m_details.hash.empty())
+ return false;
+ else if (m_details.hash == m_oldHash)
+ return true;
+
+ std::unique_ptr<CTexture> texture = LoadImage(image, width, height, additional_info, true);
+ if (texture)
+ {
+ if (texture->HasAlpha())
+ m_details.file = m_cachePath + ".png";
+ else
+ m_details.file = m_cachePath + ".jpg";
+
+ CLog::Log(LOGDEBUG, "{} image '{}' to '{}':", m_oldHash.empty() ? "Caching" : "Recaching",
+ CURL::GetRedacted(image), m_details.file);
+
+ if (CPicture::CacheTexture(texture.get(), width, height,
+ CTextureCache::GetCachedPath(m_details.file), scalingAlgorithm))
+ {
+ m_details.width = width;
+ m_details.height = height;
+ if (out_texture) // caller wants the texture
+ *out_texture = std::move(texture);
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CTextureCacheJob::ResizeTexture(const std::string &url, uint8_t* &result, size_t &result_size)
+{
+ result = NULL;
+ result_size = 0;
+
+ if (url.empty())
+ return false;
+
+ // unwrap the URL as required
+ std::string additional_info;
+ unsigned int width, height;
+ CPictureScalingAlgorithm::Algorithm scalingAlgorithm;
+ std::string image = DecodeImageURL(url, width, height, scalingAlgorithm, additional_info);
+ if (image.empty())
+ return false;
+
+ std::unique_ptr<CTexture> texture = LoadImage(image, width, height, additional_info, true);
+ if (texture == NULL)
+ return false;
+
+ bool success = CPicture::ResizeTexture(image, texture.get(), width, height, result, result_size,
+ scalingAlgorithm);
+
+ return success;
+}
+
+std::string CTextureCacheJob::DecodeImageURL(const std::string &url, unsigned int &width, unsigned int &height, CPictureScalingAlgorithm::Algorithm& scalingAlgorithm, std::string &additional_info)
+{
+ // unwrap the URL as required
+ std::string image(url);
+ additional_info.clear();
+ width = height = 0;
+ scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm;
+ if (StringUtils::StartsWith(url, "image://"))
+ {
+ // format is image://[type@]<url_encoded_path>?options
+ CURL thumbURL(url);
+
+ if (!CTextureCache::CanCacheImageURL(thumbURL))
+ return "";
+ if (thumbURL.GetUserName() == "music")
+ additional_info = "music";
+ if (StringUtils::StartsWith(thumbURL.GetUserName(), "video_") ||
+ StringUtils::StartsWith(thumbURL.GetUserName(), "pvr") ||
+ StringUtils::StartsWith(thumbURL.GetUserName(), "epg"))
+ additional_info = thumbURL.GetUserName();
+
+ image = thumbURL.GetHostName();
+
+ if (thumbURL.HasOption("flipped"))
+ additional_info = "flipped";
+
+ if (thumbURL.GetOption("size") == "thumb")
+ width = height = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes;
+ else
+ {
+ if (thumbURL.HasOption("width") && StringUtils::IsInteger(thumbURL.GetOption("width")))
+ width = strtol(thumbURL.GetOption("width").c_str(), NULL, 0);
+ if (thumbURL.HasOption("height") && StringUtils::IsInteger(thumbURL.GetOption("height")))
+ height = strtol(thumbURL.GetOption("height").c_str(), NULL, 0);
+ }
+
+ if (thumbURL.HasOption("scaling_algorithm"))
+ scalingAlgorithm = CPictureScalingAlgorithm::FromString(thumbURL.GetOption("scaling_algorithm"));
+ }
+
+ // Handle special case about audiodecoder addon music files, e.g. SACD
+ if (StringUtils::EndsWith(URIUtils::GetExtension(image), KODI_ADDON_AUDIODECODER_TRACK_EXT))
+ {
+ std::string addonImageURL = URIUtils::GetDirectory(image);
+ URIUtils::RemoveSlashAtEnd(addonImageURL);
+ if (XFILE::CFile::Exists(addonImageURL))
+ image = addonImageURL;
+ }
+
+ return image;
+}
+
+std::unique_ptr<CTexture> CTextureCacheJob::LoadImage(const std::string& image,
+ unsigned int width,
+ unsigned int height,
+ const std::string& additional_info,
+ bool requirePixels)
+{
+ if (additional_info == "music")
+ { // special case for embedded music images
+ EmbeddedArt art;
+ if (CMusicThumbLoader::GetEmbeddedThumb(image, art))
+ return CTexture::LoadFromFileInMemory(art.m_data.data(), art.m_size, art.m_mime, width,
+ height);
+ }
+
+ if (StringUtils::StartsWith(additional_info, "video_"))
+ {
+ EmbeddedArt art;
+ if (CVideoThumbLoader::GetEmbeddedThumb(image, additional_info.substr(6), art))
+ return CTexture::LoadFromFileInMemory(art.m_data.data(), art.m_size, art.m_mime, width,
+ height);
+ }
+
+ // Validate file URL to see if it is an image
+ CFileItem file(image, false);
+ file.FillInMimeType();
+ if (!(file.IsPicture() && !(file.IsZIP() || file.IsRAR() || file.IsCBR() || file.IsCBZ() ))
+ && !StringUtils::StartsWithNoCase(file.GetMimeType(), "image/") && !StringUtils::EqualsNoCase(file.GetMimeType(), "application/octet-stream")) // ignore non-pictures
+ return NULL;
+
+ std::unique_ptr<CTexture> texture =
+ CTexture::LoadFromFile(image, width, height, requirePixels, file.GetMimeType());
+ if (!texture)
+ return NULL;
+
+ // EXIF bits are interpreted as: <flipXY><flipY*flipX><flipX>
+ // where to undo the operation we apply them in reverse order <flipX>*<flipY*flipX>*<flipXY>
+ // When flipped we have an additional <flipX> on the left, which is equivalent to toggling the last bit
+ if (additional_info == "flipped")
+ texture->SetOrientation(texture->GetOrientation() ^ 1);
+
+ return texture;
+}
+
+bool CTextureCacheJob::UpdateableURL(const std::string &url) const
+{
+ // we don't constantly check online images
+ return !(StringUtils::StartsWith(url, "http://") || StringUtils::StartsWith(url, "https://"));
+}
+
+std::string CTextureCacheJob::GetImageHash(const std::string &url)
+{
+ // silently ignore - we cannot stat these
+ // in the case of upnp thumbs are/should be provided when filling the directory list, there's no reason to stat all object ids
+ if (URIUtils::IsProtocol(url, "addons") || URIUtils::IsProtocol(url, "plugin") ||
+ URIUtils::IsProtocol(url, "upnp"))
+ return "";
+
+ struct __stat64 st;
+ if (XFILE::CFile::Stat(url, &st) == 0)
+ {
+ int64_t time = st.st_mtime;
+ if (!time)
+ time = st.st_ctime;
+ if (time || st.st_size)
+ return StringUtils::Format("d{}s{}", time, st.st_size);
+
+ // the image exists but we couldn't determine the mtime/ctime and/or size
+ // so set an obviously bad hash
+ return "BADHASH";
+ }
+
+ CLog::Log(LOGDEBUG, "{} - unable to stat url {}", __FUNCTION__, CURL::GetRedacted(url));
+ return "";
+}
+
+CTextureUseCountJob::CTextureUseCountJob(const std::vector<CTextureDetails> &textures) : m_textures(textures)
+{
+}
+
+bool CTextureUseCountJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(),GetType()) == 0)
+ {
+ const CTextureUseCountJob* useJob = dynamic_cast<const CTextureUseCountJob*>(job);
+ if (useJob && useJob->m_textures == m_textures)
+ return true;
+ }
+ return false;
+}
+
+bool CTextureUseCountJob::DoWork()
+{
+ CTextureDatabase db;
+ if (db.Open())
+ {
+ db.BeginTransaction();
+ for (std::vector<CTextureDetails>::const_iterator i = m_textures.begin(); i != m_textures.end(); ++i)
+ db.IncrementUseCount(*i);
+ db.CommitTransaction();
+ }
+ return true;
+}
diff --git a/xbmc/TextureCacheJob.h b/xbmc/TextureCacheJob.h
new file mode 100644
index 0000000..66775ac
--- /dev/null
+++ b/xbmc/TextureCacheJob.h
@@ -0,0 +1,137 @@
+/*
+ * 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 "pictures/PictureScalingAlgorithm.h"
+#include "utils/Job.h"
+
+#include <cstddef>
+#include <memory>
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+class CTexture;
+
+/*!
+ \ingroup textures
+ \brief Simple class for passing texture detail around
+ */
+class CTextureDetails
+{
+public:
+ CTextureDetails()
+ {
+ id = -1;
+ width = height = 0;
+ updateable = false;
+ };
+ bool operator==(const CTextureDetails &right) const
+ {
+ return (id == right.id &&
+ file == right.file &&
+ width == right.width );
+ };
+ int id;
+ std::string file;
+ std::string hash;
+ unsigned int width;
+ unsigned int height;
+ bool updateable;
+};
+
+/*!
+ \ingroup textures
+ \brief Job class for caching textures
+
+ Handles loading and caching of textures.
+ */
+class CTextureCacheJob : public CJob
+{
+public:
+ CTextureCacheJob(const std::string &url, const std::string &oldHash = "");
+ ~CTextureCacheJob() override;
+
+ const char* GetType() const override { return kJobTypeCacheImage; }
+ bool operator==(const CJob *job) const override;
+ bool DoWork() override;
+
+ /*! \brief retrieve a hash for the given image
+ Combines the size, ctime and mtime of the image file into a "unique" hash
+ \param url location of the image
+ \return a hash string for this image
+ */
+ bool CacheTexture(std::unique_ptr<CTexture>* texture = nullptr);
+
+ static bool ResizeTexture(const std::string &url, uint8_t* &result, size_t &result_size);
+
+ std::string m_url;
+ std::string m_oldHash;
+ CTextureDetails m_details;
+private:
+ /*! \brief retrieve a hash for the given image
+ Combines the size, ctime and mtime of the image file into a "unique" hash
+ \param url location of the image
+ \return a hash string for this image
+ */
+ static std::string GetImageHash(const std::string &url);
+
+ /*! \brief Check whether a given URL represents an image that can be updated
+ We currently don't check http:// and https:// URLs for updates, under the assumption that
+ a image URL is much more likely to be static and the actual image at the URL is unlikely
+ to change, so no point checking all the time.
+ \param url the url to check
+ \return true if the image given by the URL should be checked for updates, false otherwise
+ */
+ bool UpdateableURL(const std::string &url) const;
+
+ /*! \brief Decode an image URL to the underlying image, width, height and orientation
+ \param url wrapped URL of the image
+ \param width width derived from URL
+ \param height height derived from URL
+ \param scalingAlgorithm scaling algorithm derived from URL
+ \param additional_info additional information, such as "flipped" to flip horizontally
+ \return URL of the underlying image file.
+ */
+ static std::string DecodeImageURL(const std::string &url, unsigned int &width, unsigned int &height, CPictureScalingAlgorithm::Algorithm& scalingAlgorithm, std::string &additional_info);
+
+ /*! \brief Load an image at a given target size and orientation.
+
+ Doesn't necessarily load the image at the desired size - the loader *may* decide to load it slightly larger
+ or smaller than the desired size for speed reasons.
+
+ \param image the URL of the image file.
+ \param width the desired maximum width.
+ \param height the desired maximum height.
+ \param additional_info extra info for loading, such as whether to flip horizontally.
+ \return a pointer to a CTexture object, NULL if failed.
+ */
+ static std::unique_ptr<CTexture> LoadImage(const std::string& image,
+ unsigned int width,
+ unsigned int height,
+ const std::string& additional_info,
+ bool requirePixels = false);
+
+ std::string m_cachePath;
+};
+
+/* \brief Job class for storing the use count of textures
+ */
+class CTextureUseCountJob : public CJob
+{
+public:
+ explicit CTextureUseCountJob(const std::vector<CTextureDetails> &textures);
+
+ const char* GetType() const override { return "usecount"; }
+ bool operator==(const CJob *job) const override;
+ bool DoWork() override;
+
+private:
+ std::vector<CTextureDetails> m_textures;
+};
diff --git a/xbmc/TextureDatabase.cpp b/xbmc/TextureDatabase.cpp
new file mode 100644
index 0000000..9d7edff
--- /dev/null
+++ b/xbmc/TextureDatabase.cpp
@@ -0,0 +1,486 @@
+/*
+ * 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 "TextureDatabase.h"
+
+#include "URL.h"
+#include "XBDateTime.h"
+#include "dbwrappers/dataset.h"
+#include "utils/DatabaseUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+enum TextureField
+{
+ TF_None = 0,
+ TF_Id,
+ TF_Url,
+ TF_CachedUrl,
+ TF_LastHashCheck,
+ TF_ImageHash,
+ TF_Width,
+ TF_Height,
+ TF_UseCount,
+ TF_LastUsed,
+ TF_Max
+};
+
+typedef struct
+{
+ char string[14];
+ TextureField field;
+ CDatabaseQueryRule::FIELD_TYPE type;
+} translateField;
+
+static const translateField fields[] = {
+ { "none", TF_None, CDatabaseQueryRule::TEXT_FIELD },
+ { "textureid", TF_Id, CDatabaseQueryRule::REAL_FIELD },
+ { "url", TF_Url, CDatabaseQueryRule::TEXT_FIELD },
+ { "cachedurl", TF_CachedUrl, CDatabaseQueryRule::TEXT_FIELD },
+ { "lasthashcheck", TF_LastHashCheck, CDatabaseQueryRule::TEXT_FIELD },
+ { "imagehash", TF_ImageHash, CDatabaseQueryRule::TEXT_FIELD },
+ { "width", TF_Width, CDatabaseQueryRule::REAL_FIELD },
+ { "height", TF_Height, CDatabaseQueryRule::REAL_FIELD },
+ { "usecount", TF_UseCount, CDatabaseQueryRule::REAL_FIELD },
+ { "lastused", TF_LastUsed, CDatabaseQueryRule::TEXT_FIELD }
+};
+
+static const size_t NUM_FIELDS = sizeof(fields) / sizeof(translateField);
+
+int CTextureRule::TranslateField(const char *field) const
+{
+ for (const translateField& f : fields)
+ if (StringUtils::EqualsNoCase(field, f.string)) return f.field;
+ return FieldNone;
+}
+
+std::string CTextureRule::TranslateField(int field) const
+{
+ for (const translateField& f : fields)
+ if (field == f.field) return f.string;
+ return "none";
+}
+
+std::string CTextureRule::GetField(int field, const std::string &type) const
+{
+ if (field == TF_Id) return "texture.id";
+ else if (field == TF_Url) return "texture.url";
+ else if (field == TF_CachedUrl) return "texture.cachedurl";
+ else if (field == TF_LastHashCheck) return "texture.lasthashcheck";
+ else if (field == TF_ImageHash) return "texture.imagehash";
+ else if (field == TF_Width) return "sizes.width";
+ else if (field == TF_Height) return "sizes.height";
+ else if (field == TF_UseCount) return "sizes.usecount";
+ else if (field == TF_LastUsed) return "sizes.lastusetime";
+ return "";
+}
+
+CDatabaseQueryRule::FIELD_TYPE CTextureRule::GetFieldType(int field) const
+{
+ for (const translateField& f : fields)
+ if (field == f.field) return f.type;
+ return TEXT_FIELD;
+}
+
+std::string CTextureRule::FormatParameter(const std::string &operatorString,
+ const std::string &param,
+ const CDatabase &db,
+ const std::string &strType) const
+{
+ std::string parameter(param);
+ if (m_field == TF_Url)
+ parameter = CTextureUtils::UnwrapImageURL(param);
+ return CDatabaseQueryRule::FormatParameter(operatorString, parameter, db, strType);
+}
+
+void CTextureRule::GetAvailableFields(std::vector<std::string> &fieldList)
+{
+ // start at 1 to skip TF_None
+ for (unsigned int i = 1; i < NUM_FIELDS; i++)
+ fieldList.emplace_back(fields[i].string);
+}
+
+std::string CTextureUtils::GetWrappedImageURL(const std::string &image, const std::string &type, const std::string &options)
+{
+ if (StringUtils::StartsWith(image, "image://"))
+ return image; // already wrapped
+
+ CURL url;
+ url.SetProtocol("image");
+ url.SetUserName(type);
+ url.SetHostName(image);
+ if (!options.empty())
+ {
+ url.SetFileName("transform");
+ url.SetOptions("?" + options);
+ }
+ return url.Get();
+}
+
+std::string CTextureUtils::GetWrappedThumbURL(const std::string &image)
+{
+ return GetWrappedImageURL(image, "", "size=thumb");
+}
+
+std::string CTextureUtils::UnwrapImageURL(const std::string &image)
+{
+ if (StringUtils::StartsWith(image, "image://"))
+ {
+ CURL url(image);
+ if (url.GetUserName().empty() && url.GetOptions().empty())
+ return url.GetHostName();
+ }
+ return image;
+}
+
+CTextureDatabase::CTextureDatabase() = default;
+
+CTextureDatabase::~CTextureDatabase() = default;
+
+bool CTextureDatabase::Open()
+{
+ return CDatabase::Open();
+}
+
+void CTextureDatabase::CreateTables()
+{
+ CLog::Log(LOGINFO, "create texture table");
+ m_pDS->exec("CREATE TABLE texture (id integer primary key, url text, cachedurl text, imagehash text, lasthashcheck text)");
+
+ CLog::Log(LOGINFO, "create sizes table, index, and trigger");
+ m_pDS->exec("CREATE TABLE sizes (idtexture integer, size integer, width integer, height integer, usecount integer, lastusetime text)");
+
+ CLog::Log(LOGINFO, "create path table");
+ m_pDS->exec("CREATE TABLE path (id integer primary key, url text, type text, texture text)\n");
+}
+
+void CTextureDatabase::CreateAnalytics()
+{
+ CLog::Log(LOGINFO, "{} creating indices", __FUNCTION__);
+ m_pDS->exec("CREATE INDEX idxTexture ON texture(url)");
+ m_pDS->exec("CREATE INDEX idxSize ON sizes(idtexture, size)");
+ m_pDS->exec("CREATE INDEX idxSize2 ON sizes(idtexture, width, height)");
+ //! @todo Should the path index be a covering index? (we need only retrieve texture)
+ m_pDS->exec("CREATE INDEX idxPath ON path(url, type)");
+
+ CLog::Log(LOGINFO, "{} creating triggers", __FUNCTION__);
+ m_pDS->exec("CREATE TRIGGER textureDelete AFTER delete ON texture FOR EACH ROW BEGIN delete from sizes where sizes.idtexture=old.id; END");
+}
+
+void CTextureDatabase::UpdateTables(int version)
+{
+ if (version < 7)
+ { // update all old thumb://foo urls to image://foo?size=thumb
+ m_pDS->query("select id,texture from path where texture like 'thumb://%'");
+ while (!m_pDS->eof())
+ {
+ unsigned int id = m_pDS->fv(0).get_asInt();
+ CURL url(m_pDS->fv(1).get_asString());
+ m_pDS2->exec(PrepareSQL("update path set texture='image://%s?size=thumb' where id=%u", url.GetHostName().c_str(), id));
+ m_pDS->next();
+ }
+ m_pDS->query("select id, url from texture where url like 'thumb://%'");
+ while (!m_pDS->eof())
+ {
+ unsigned int id = m_pDS->fv(0).get_asInt();
+ CURL url(m_pDS->fv(1).get_asString());
+ m_pDS2->exec(PrepareSQL("update texture set url='image://%s?size=thumb', urlhash=0 where id=%u", url.GetHostName().c_str(), id));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ if (version < 8)
+ { // get rid of old cached thumbs as they were previously set to the cached thumb name instead of the source thumb
+ m_pDS->exec("delete from path");
+ }
+ if (version < 9)
+ { // get rid of the old path table and add the type column
+ m_pDS->exec("DROP TABLE IF EXISTS path");
+ m_pDS->exec("CREATE TABLE path (id integer primary key, urlhash integer, url text, type text, texture text)\n");
+ }
+ if (version < 10)
+ { // get rid of urlhash in both tables...
+ m_pDS->exec("DROP TABLE IF EXISTS path");
+ m_pDS->exec("CREATE TABLE path (id integer primary key, url text, type text, texture text)\n");
+
+ m_pDS->exec("CREATE TEMPORARY TABLE texture_backup(id,url,cachedurl,usecount,lastusetime,imagehash,lasthashcheck)");
+ m_pDS->exec("INSERT INTO texture_backup SELECT id,url,cachedurl,usecount,lastusetime,imagehash,lasthashcheck FROM texture");
+ m_pDS->exec("DROP TABLE texture");
+ m_pDS->exec("CREATE TABLE texture (id integer primary key, url text, cachedurl text, usecount integer, lastusetime text, imagehash text, lasthashcheck text)");
+ m_pDS->exec("INSERT INTO texture SELECT * FROM texture_backup");
+ m_pDS->exec("DROP TABLE texture_backup");
+ }
+ if (version < 11)
+ { // get rid of cached URLs that don't have the correct extension
+ m_pDS->exec("DELETE FROM texture WHERE SUBSTR(cachedUrl,-4,4) NOT IN ('.jpg', '.png')");
+ }
+ if (version < 12)
+ { // create new sizes table and move usecount info to it.
+ m_pDS->exec("DROP TABLE IF EXISTS texture");
+ m_pDS->exec("CREATE TABLE texture (id integer primary key, url text, cachedurl text, imagehash text, lasthashcheck text)");
+ m_pDS->exec("CREATE TABLE sizes (idtexture integer, size integer, width integer, height integer, usecount integer, lastusetime text)");
+ }
+}
+
+bool CTextureDatabase::IncrementUseCount(const CTextureDetails &details)
+{
+ std::string sql = PrepareSQL("UPDATE sizes SET usecount=usecount+1, lastusetime=CURRENT_TIMESTAMP WHERE idtexture=%u AND width=%u AND height=%u", details.id, details.width, details.height);
+ return ExecuteQuery(sql);
+}
+
+bool CTextureDatabase::GetCachedTexture(const std::string &url, CTextureDetails &details)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL("SELECT id, cachedurl, lasthashcheck, imagehash, width, height FROM texture JOIN sizes ON (texture.id=sizes.idtexture AND sizes.size=1) WHERE url='%s'", url.c_str());
+ m_pDS->query(sql);
+ if (!m_pDS->eof())
+ { // have some information
+ details.id = m_pDS->fv(0).get_asInt();
+ details.file = m_pDS->fv(1).get_asString();
+ CDateTime lastCheck;
+ lastCheck.SetFromDBDateTime(m_pDS->fv(2).get_asString());
+ if (lastCheck.IsValid() && lastCheck + CDateTimeSpan(1,0,0,0) < CDateTime::GetCurrentDateTime())
+ details.hash = m_pDS->fv(3).get_asString();
+ details.width = m_pDS->fv(4).get_asInt();
+ details.height = m_pDS->fv(5).get_asInt();
+ m_pDS->close();
+ return true;
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}, failed on url '{}'", __FUNCTION__, url);
+ }
+ return false;
+}
+
+bool CTextureDatabase::GetTextures(CVariant &items, const Filter &filter)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql = "SELECT %s FROM texture JOIN sizes ON (texture.id=sizes.idtexture AND sizes.size=1)";
+ std::string sqlFilter;
+ if (!CDatabase::BuildSQL("", filter, sqlFilter))
+ return false;
+
+ sql = PrepareSQL(sql, !filter.fields.empty() ? filter.fields.c_str() : "*") + sqlFilter;
+ if (!m_pDS->query(sql))
+ return false;
+
+ while (!m_pDS->eof())
+ {
+ CVariant texture;
+ texture["textureid"] = m_pDS->fv(0).get_asInt();
+ texture["url"] = m_pDS->fv(1).get_asString();
+ texture["cachedurl"] = m_pDS->fv(2).get_asString();
+ texture["imagehash"] = m_pDS->fv(3).get_asString();
+ texture["lasthashcheck"] = m_pDS->fv(4).get_asString();
+ CVariant size(CVariant::VariantTypeObject);
+ // 5 is sizes.idtexture
+ size["size"] = m_pDS->fv(6).get_asInt();
+ size["width"] = m_pDS->fv(7).get_asInt();
+ size["height"] = m_pDS->fv(8).get_asInt();
+ size["usecount"] = m_pDS->fv(9).get_asInt();
+ size["lastused"] = m_pDS->fv(10).get_asString();
+ texture["sizes"] = CVariant(CVariant::VariantTypeArray);
+ texture["sizes"].push_back(size);
+ items.push_back(texture);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}, failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CTextureDatabase::SetCachedTextureValid(const std::string &url, bool updateable)
+{
+ std::string date = updateable ? CDateTime::GetCurrentDateTime().GetAsDBDateTime() : "";
+ std::string sql = PrepareSQL("UPDATE texture SET lasthashcheck='%s' WHERE url='%s'", date.c_str(), url.c_str());
+ return ExecuteQuery(sql);
+}
+
+bool CTextureDatabase::AddCachedTexture(const std::string &url, const CTextureDetails &details)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL("DELETE FROM texture WHERE url='%s'", url.c_str());
+ m_pDS->exec(sql);
+
+ std::string date = details.updateable ? CDateTime::GetCurrentDateTime().GetAsDBDateTime() : "";
+ sql = PrepareSQL("INSERT INTO texture (id, url, cachedurl, imagehash, lasthashcheck) VALUES(NULL, '%s', '%s', '%s', '%s')", url.c_str(), details.file.c_str(), details.hash.c_str(), date.c_str());
+ m_pDS->exec(sql);
+ int textureID = (int)m_pDS->lastinsertid();
+
+ // set the size information
+ sql = PrepareSQL("INSERT INTO sizes (idtexture, size, usecount, lastusetime, width, height) VALUES(%u, 1, 1, CURRENT_TIMESTAMP, %u, %u)", textureID, details.width, details.height);
+ m_pDS->exec(sql);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on url '{}'", __FUNCTION__, url);
+ }
+ return true;
+}
+
+bool CTextureDatabase::ClearCachedTexture(const std::string &url, std::string &cacheFile)
+{
+ std::string id = GetSingleValue(PrepareSQL("select id from texture where url='%s'", url.c_str()));
+ return !id.empty() ? ClearCachedTexture(strtol(id.c_str(), NULL, 10), cacheFile) : false;
+}
+
+bool CTextureDatabase::ClearCachedTexture(int id, std::string &cacheFile)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL("select cachedurl from texture where id=%u", id);
+ m_pDS->query(sql);
+
+ if (!m_pDS->eof())
+ { // have some information
+ cacheFile = m_pDS->fv(0).get_asString();
+ m_pDS->close();
+ // remove it
+ sql = PrepareSQL("delete from texture where id=%u", id);
+ m_pDS->exec(sql);
+ return true;
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}, failed on texture id {}", __FUNCTION__, id);
+ }
+ return false;
+}
+
+bool CTextureDatabase::InvalidateCachedTexture(const std::string &url)
+{
+ std::string date = (CDateTime::GetCurrentDateTime() - CDateTimeSpan(2, 0, 0, 0)).GetAsDBDateTime();
+ std::string sql = PrepareSQL("UPDATE texture SET lasthashcheck='%s' WHERE url='%s'", date.c_str(), url.c_str());
+ return ExecuteQuery(sql);
+}
+
+std::string CTextureDatabase::GetTextureForPath(const std::string &url, const std::string &type)
+{
+ try
+ {
+ if (!m_pDB)
+ return "";
+ if (!m_pDS)
+ return "";
+
+ if (url.empty())
+ return "";
+
+ std::string sql = PrepareSQL("select texture from path where url='%s' and type='%s'", url.c_str(), type.c_str());
+ m_pDS->query(sql);
+
+ if (!m_pDS->eof())
+ { // have some information
+ std::string texture = m_pDS->fv(0).get_asString();
+ m_pDS->close();
+ return texture;
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}, failed on url '{}'", __FUNCTION__, url);
+ }
+ return "";
+}
+
+void CTextureDatabase::SetTextureForPath(const std::string &url, const std::string &type, const std::string &texture)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+
+ if (url.empty())
+ return;
+
+ std::string sql = PrepareSQL("select id from path where url='%s' and type='%s'", url.c_str(), type.c_str());
+ m_pDS->query(sql);
+ if (!m_pDS->eof())
+ { // update
+ int pathID = m_pDS->fv(0).get_asInt();
+ m_pDS->close();
+ sql = PrepareSQL("update path set texture='%s' where id=%u", texture.c_str(), pathID);
+ m_pDS->exec(sql);
+ }
+ else
+ { // add the texture
+ m_pDS->close();
+ sql = PrepareSQL("insert into path (id, url, type, texture) values(NULL, '%s', '%s', '%s')", url.c_str(), type.c_str(), texture.c_str());
+ m_pDS->exec(sql);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on url '{}'", __FUNCTION__, url);
+ }
+}
+
+void CTextureDatabase::ClearTextureForPath(const std::string &url, const std::string &type)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+
+ std::string sql = PrepareSQL("DELETE FROM path WHERE url='%s' and type='%s'", url.c_str(), type.c_str());
+ m_pDS->exec(sql);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on url '{}'", __FUNCTION__, url);
+ }
+}
+
+CDatabaseQueryRule *CTextureDatabase::CreateRule() const
+{
+ return new CTextureRule();
+}
+
+CDatabaseQueryRuleCombination *CTextureDatabase::CreateCombination() const
+{
+ return new CDatabaseQueryRuleCombination();
+}
diff --git a/xbmc/TextureDatabase.h b/xbmc/TextureDatabase.h
new file mode 100644
index 0000000..4965b9d
--- /dev/null
+++ b/xbmc/TextureDatabase.h
@@ -0,0 +1,125 @@
+/*
+ * 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 "TextureCacheJob.h"
+#include "dbwrappers/Database.h"
+#include "dbwrappers/DatabaseQuery.h"
+
+#include <string>
+#include <vector>
+
+class CVariant;
+
+class CTextureRule : public CDatabaseQueryRule
+{
+public:
+ CTextureRule() = default;
+ ~CTextureRule() override = default;
+
+ static void GetAvailableFields(std::vector<std::string> &fieldList);
+protected:
+ int TranslateField(const char *field) const override;
+ std::string TranslateField(int field) const override;
+ std::string GetField(int field, const std::string& type) const override;
+ FIELD_TYPE GetFieldType(int field) const override;
+ std::string FormatParameter(const std::string &negate,
+ const std::string &oper,
+ const CDatabase &db,
+ const std::string &type) const override;
+};
+
+class CTextureUtils
+{
+public:
+ /*! \brief retrieve a wrapped URL for a image file
+ \param image name of the file
+ \param type signifies a special type of image (eg embedded video thumb, picture folder thumb)
+ \param options which options we need (eg size=thumb)
+ \return full wrapped URL of the image file
+ */
+ static std::string GetWrappedImageURL(const std::string &image, const std::string &type = "", const std::string &options = "");
+ static std::string GetWrappedThumbURL(const std::string &image);
+
+ /*! \brief Unwrap an image://<url_encoded_path> style URL
+ Such urls are used for art over the webserver or other users of the VFS
+ \param image url of the image
+ \return the unwrapped URL, or the original URL if unwrapping is inappropriate.
+ */
+ static std::string UnwrapImageURL(const std::string &image);
+};
+
+class CTextureDatabase : public CDatabase, public IDatabaseQueryRuleFactory
+{
+public:
+ CTextureDatabase();
+ ~CTextureDatabase() override;
+ bool Open() override;
+
+ bool GetCachedTexture(const std::string &originalURL, CTextureDetails &details);
+ bool AddCachedTexture(const std::string &originalURL, const CTextureDetails &details);
+ bool SetCachedTextureValid(const std::string &originalURL, bool updateable);
+ bool ClearCachedTexture(const std::string &originalURL, std::string &cacheFile);
+ bool ClearCachedTexture(int textureID, std::string &cacheFile);
+ bool IncrementUseCount(const CTextureDetails &details);
+
+ /*! \brief Invalidate a previously cached texture
+ Invalidates the texture hash, and sets the texture update time to the current time so that
+ next texture load it will be re-cached.
+ \param url texture path
+ */
+ bool InvalidateCachedTexture(const std::string &originalURL);
+
+ /*! \brief Get a texture associated with the given path
+ Used for retrieval of previously discovered images to save
+ stat() on the filesystem all the time
+ \param url path that may be associated with a texture
+ \param type type of image to look for
+ \return URL of the texture associated with the given path
+ */
+ std::string GetTextureForPath(const std::string &url, const std::string &type);
+
+ /*! \brief Set a texture associated with the given path
+ Used for setting of previously discovered images to save
+ stat() on the filesystem all the time. Should be used to set
+ the actual image path, not the cached image path (the image will be
+ cached at load time.)
+ \param url path that was used to find the texture
+ \param type type of image to associate
+ \param texture URL of the texture to associate with the path
+ */
+ void SetTextureForPath(const std::string &url, const std::string &type, const std::string &texture);
+
+ /*! \brief Clear a texture associated with the given path
+ \param url path that was used to find the texture
+ \param type type of image to associate
+ \param texture URL of the texture to associate with the path
+ \sa GetTextureForPath, SetTextureForPath
+ */
+ void ClearTextureForPath(const std::string &url, const std::string &type);
+
+ bool GetTextures(CVariant &items, const Filter &filter);
+
+ // rule creation
+ CDatabaseQueryRule *CreateRule() const override;
+ CDatabaseQueryRuleCombination *CreateCombination() const override;
+protected:
+ /*! \brief retrieve a hash for the given url
+ Computes a hash of the current url to use for lookups in the database
+ \param url url to hash
+ \return a hash for this url
+ */
+ unsigned int GetURLHash(const std::string &url) const;
+
+ void CreateTables() override;
+ void CreateAnalytics() override;
+ void UpdateTables(int version) override;
+ int GetSchemaVersion() const override { return 13; }
+ const char* GetBaseDBName() const override { return "Textures"; }
+};
diff --git a/xbmc/ThumbLoader.cpp b/xbmc/ThumbLoader.cpp
new file mode 100644
index 0000000..3e1508c
--- /dev/null
+++ b/xbmc/ThumbLoader.cpp
@@ -0,0 +1,125 @@
+/*
+ * 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 "ThumbLoader.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "utils/FileUtils.h"
+
+CThumbLoader::CThumbLoader() :
+ CBackgroundInfoLoader()
+{
+ m_textureDatabase = new CTextureDatabase();
+}
+
+CThumbLoader::~CThumbLoader()
+{
+ delete m_textureDatabase;
+}
+
+void CThumbLoader::OnLoaderStart()
+{
+ m_textureDatabase->Open();
+}
+
+void CThumbLoader::OnLoaderFinish()
+{
+ m_textureDatabase->Close();
+}
+
+std::string CThumbLoader::GetCachedImage(const CFileItem &item, const std::string &type)
+{
+ if (!item.GetPath().empty() && m_textureDatabase->Open())
+ {
+ std::string image = m_textureDatabase->GetTextureForPath(item.GetPath(), type);
+ m_textureDatabase->Close();
+ return image;
+ }
+ return "";
+}
+
+void CThumbLoader::SetCachedImage(const CFileItem &item, const std::string &type, const std::string &image)
+{
+ if (!item.GetPath().empty() && m_textureDatabase->Open())
+ {
+ m_textureDatabase->SetTextureForPath(item.GetPath(), type, image);
+ m_textureDatabase->Close();
+ }
+}
+
+CProgramThumbLoader::CProgramThumbLoader() = default;
+
+CProgramThumbLoader::~CProgramThumbLoader() = default;
+
+bool CProgramThumbLoader::LoadItem(CFileItem *pItem)
+{
+ bool result = LoadItemCached(pItem);
+ result |= LoadItemLookup(pItem);
+
+ return result;
+}
+
+bool CProgramThumbLoader::LoadItemCached(CFileItem *pItem)
+{
+ if (pItem->IsParentFolder())
+ return false;
+
+ return FillThumb(*pItem);
+}
+
+bool CProgramThumbLoader::LoadItemLookup(CFileItem *pItem)
+{
+ return false;
+}
+
+bool CProgramThumbLoader::FillThumb(CFileItem &item)
+{
+ // no need to do anything if we already have a thumb set
+ std::string thumb = item.GetArt("thumb");
+
+ if (thumb.empty())
+ { // see whether we have a cached image for this item
+ thumb = GetCachedImage(item, "thumb");
+ if (thumb.empty())
+ {
+ thumb = GetLocalThumb(item);
+ if (!thumb.empty())
+ SetCachedImage(item, "thumb", thumb);
+ }
+ }
+
+ if (!thumb.empty())
+ {
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumb);
+ item.SetArt("thumb", thumb);
+ }
+ return true;
+}
+
+std::string CProgramThumbLoader::GetLocalThumb(const CFileItem &item)
+{
+ if (item.IsAddonsPath())
+ return "";
+
+ // look for the thumb
+ if (item.m_bIsFolder)
+ {
+ std::string folderThumb = item.GetFolderThumb();
+ if (CFileUtils::Exists(folderThumb))
+ return folderThumb;
+ }
+ else
+ {
+ std::string fileThumb(item.GetTBNFile());
+ if (CFileUtils::Exists(fileThumb))
+ return fileThumb;
+ }
+ return "";
+}
diff --git a/xbmc/ThumbLoader.h b/xbmc/ThumbLoader.h
new file mode 100644
index 0000000..7b163ea
--- /dev/null
+++ b/xbmc/ThumbLoader.h
@@ -0,0 +1,75 @@
+/*
+ * 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 "BackgroundInfoLoader.h"
+
+#include <string>
+
+class CTextureDatabase;
+
+class CThumbLoader : public CBackgroundInfoLoader
+{
+public:
+ CThumbLoader();
+ ~CThumbLoader() override;
+
+ void OnLoaderStart() override;
+ void OnLoaderFinish() override;
+
+ /*! \brief helper function to fill the art for a library item
+ \param item a CFileItem
+ \return true if we fill art, false otherwise
+ */
+ virtual bool FillLibraryArt(CFileItem &item) { return false; }
+
+ /*! \brief Checks whether the given item has an image listed in the texture database
+ \param item CFileItem to check
+ \param type the type of image to retrieve
+ \return the image associated with this item
+ */
+ virtual std::string GetCachedImage(const CFileItem &item, const std::string &type);
+
+ /*! \brief Associate an image with the given item in the texture database
+ \param item CFileItem to associate the image with
+ \param type the type of image
+ \param image the URL of the image
+ */
+ virtual void SetCachedImage(const CFileItem &item, const std::string &type, const std::string &image);
+
+protected:
+ CTextureDatabase *m_textureDatabase;
+};
+
+class CProgramThumbLoader : public CThumbLoader
+{
+public:
+ CProgramThumbLoader();
+ ~CProgramThumbLoader() override;
+ bool LoadItem(CFileItem* pItem) override;
+ bool LoadItemCached(CFileItem* pItem) override;
+ bool LoadItemLookup(CFileItem* pItem) override;
+
+ /*! \brief Fill the thumb of a programs item
+ First uses a cached thumb from a previous run, then checks for a local thumb
+ and caches it for the next run
+ \param item the CFileItem object to fill
+ \return true if we fill the thumb, false otherwise
+ \sa GetLocalThumb
+ */
+ virtual bool FillThumb(CFileItem &item);
+
+ /*! \brief Get a local thumb for a programs item
+ Shortcuts are checked, then we check for a file or folder thumb
+ \param item the CFileItem object to check
+ \return the local thumb (if it exists)
+ \sa FillThumb
+ */
+ static std::string GetLocalThumb(const CFileItem &item);
+};
diff --git a/xbmc/URL.cpp b/xbmc/URL.cpp
new file mode 100644
index 0000000..f4a66d0
--- /dev/null
+++ b/xbmc/URL.cpp
@@ -0,0 +1,804 @@
+/*
+ * 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 "URL.h"
+#include "utils/log.h"
+#include "utils/URIUtils.h"
+#include "utils/StringUtils.h"
+#include "Util.h"
+#include "filesystem/File.h"
+#include "FileItem.h"
+#include "filesystem/StackDirectory.h"
+#include "network/Network.h"
+#include "ServiceBroker.h"
+#ifndef TARGET_POSIX
+#include <sys\stat.h>
+#endif
+
+#include <string>
+#include <vector>
+
+using namespace ADDON;
+
+CURL::~CURL() = default;
+
+void CURL::Reset()
+{
+ m_strHostName.clear();
+ m_strDomain.clear();
+ m_strUserName.clear();
+ m_strPassword.clear();
+ m_strShareName.clear();
+ m_strFileName.clear();
+ m_strProtocol.clear();
+ m_strFileType.clear();
+ m_strOptions.clear();
+ m_strProtocolOptions.clear();
+ m_options.Clear();
+ m_protocolOptions.Clear();
+ m_iPort = 0;
+}
+
+void CURL::Parse(const std::string& strURL1)
+{
+ Reset();
+ // start by validating the path
+ std::string strURL = CUtil::ValidatePath(strURL1);
+
+ // strURL can be one of the following:
+ // format 1: protocol://[username:password]@hostname[:port]/directoryandfile
+ // format 2: protocol://file
+ // format 3: drive:directoryandfile
+ //
+ // first need 2 check if this is a protocol or just a normal drive & path
+ if (!strURL.size()) return ;
+ if (strURL == "?") return;
+
+ // form is format 1 or 2
+ // format 1: protocol://[domain;][username:password]@hostname[:port]/directoryandfile
+ // format 2: protocol://file
+
+ // decode protocol
+ size_t iPos = strURL.find("://");
+ if (iPos == std::string::npos)
+ {
+ // This is an ugly hack that needs some work.
+ // example: filename /foo/bar.zip/alice.rar/bob.avi
+ // This should turn into zip://rar:///foo/bar.zip/alice.rar/bob.avi
+ iPos = 0;
+ bool is_apk = (strURL.find(".apk/", iPos) != std::string::npos);
+ while (true)
+ {
+ if (is_apk)
+ iPos = strURL.find(".apk/", iPos);
+ else
+ iPos = strURL.find(".zip/", iPos);
+
+ int extLen = 3;
+ if (iPos == std::string::npos)
+ {
+ /* set filename and update extension*/
+ SetFileName(strURL);
+ return ;
+ }
+ iPos += extLen + 1;
+ std::string archiveName = strURL.substr(0, iPos);
+ struct __stat64 s;
+ if (XFILE::CFile::Stat(archiveName, &s) == 0)
+ {
+#ifdef TARGET_POSIX
+ if (!S_ISDIR(s.st_mode))
+#else
+ if (!(s.st_mode & S_IFDIR))
+#endif
+ {
+ archiveName = Encode(archiveName);
+ if (is_apk)
+ {
+ CURL c("apk://" + archiveName + "/" + strURL.substr(iPos + 1));
+ *this = c;
+ }
+ else
+ {
+ CURL c("zip://" + archiveName + "/" + strURL.substr(iPos + 1));
+ *this = c;
+ }
+ return;
+ }
+ }
+ }
+ }
+ else
+ {
+ SetProtocol(strURL.substr(0, iPos));
+ iPos += 3;
+ }
+
+ // virtual protocols
+ // why not handle all format 2 (protocol://file) style urls here?
+ // ones that come to mind are iso9660, cdda, musicdb, etc.
+ // they are all local protocols and have no server part, port number, special options, etc.
+ // this removes the need for special handling below.
+ if (
+ IsProtocol("stack") ||
+ IsProtocol("virtualpath") ||
+ IsProtocol("multipath") ||
+ IsProtocol("special") ||
+ IsProtocol("resource")
+ )
+ {
+ SetFileName(strURL.substr(iPos));
+ return;
+ }
+
+ if (IsProtocol("udf") || IsProtocol("iso9660"))
+ {
+ std::string lower(strURL);
+ StringUtils::ToLower(lower);
+ size_t isoPos = lower.find(".iso\\", iPos);
+ if (isoPos == std::string::npos)
+ isoPos = lower.find(".udf\\", iPos);
+ if (isoPos != std::string::npos)
+ {
+ strURL = strURL.replace(isoPos + 4, 1, "/");
+ }
+ }
+
+ // check for username/password - should occur before first /
+ if (iPos == std::string::npos) iPos = 0;
+
+ // for protocols supporting options, chop that part off here
+ // maybe we should invert this list instead?
+ size_t iEnd = strURL.length();
+ const char* sep = NULL;
+
+ //! @todo fix all Addon paths
+ std::string strProtocol2 = GetTranslatedProtocol();
+ if(IsProtocol("rss") ||
+ IsProtocol("rsss") ||
+ IsProtocol("rar") ||
+ IsProtocol("apk") ||
+ IsProtocol("xbt") ||
+ IsProtocol("zip") ||
+ IsProtocol("addons") ||
+ IsProtocol("image") ||
+ IsProtocol("videodb") ||
+ IsProtocol("musicdb") ||
+ IsProtocol("androidapp") ||
+ IsProtocol("pvr"))
+ sep = "?";
+ else
+ if( IsProtocolEqual(strProtocol2, "http")
+ || IsProtocolEqual(strProtocol2, "https")
+ || IsProtocolEqual(strProtocol2, "plugin")
+ || IsProtocolEqual(strProtocol2, "addons")
+ || IsProtocolEqual(strProtocol2, "rtsp"))
+ sep = "?;#|";
+ else if(IsProtocolEqual(strProtocol2, "ftp")
+ || IsProtocolEqual(strProtocol2, "ftps"))
+ sep = "?;|";
+
+ if(sep)
+ {
+ size_t iOptions = strURL.find_first_of(sep, iPos);
+ if (iOptions != std::string::npos)
+ {
+ // we keep the initial char as it can be any of the above
+ size_t iProto = strURL.find_first_of('|', iOptions);
+ if (iProto != std::string::npos)
+ {
+ SetProtocolOptions(strURL.substr(iProto+1));
+ SetOptions(strURL.substr(iOptions,iProto-iOptions));
+ }
+ else
+ SetOptions(strURL.substr(iOptions));
+ iEnd = iOptions;
+ }
+ }
+
+ size_t iSlash = strURL.find('/', iPos);
+ if(iSlash >= iEnd)
+ iSlash = std::string::npos; // was an invalid slash as it was contained in options
+
+ // also skip parsing username:password@ for udp/rtp as it not valid
+ // and conflicts with the following example: rtp://sourceip@multicastip
+ size_t iAlphaSign = strURL.find('@', iPos);
+ if (iAlphaSign != std::string::npos && iAlphaSign < iEnd &&
+ (iAlphaSign < iSlash || iSlash == std::string::npos) &&
+ !IsProtocol("udp") && !IsProtocol("rtp"))
+ {
+ // username/password found
+ std::string strUserNamePassword = strURL.substr(iPos, iAlphaSign - iPos);
+
+ // first extract domain, if protocol is smb
+ if (IsProtocol("smb"))
+ {
+ size_t iSemiColon = strUserNamePassword.find(';');
+
+ if (iSemiColon != std::string::npos)
+ {
+ m_strDomain = strUserNamePassword.substr(0, iSemiColon);
+ strUserNamePassword.erase(0, iSemiColon + 1);
+ }
+ }
+
+ // username:password
+ size_t iColon = strUserNamePassword.find(':');
+ if (iColon != std::string::npos)
+ {
+ m_strUserName = strUserNamePassword.substr(0, iColon);
+ m_strPassword = strUserNamePassword.substr(iColon + 1);
+ }
+ // username
+ else
+ {
+ m_strUserName = strUserNamePassword;
+ }
+
+ iPos = iAlphaSign + 1;
+ iSlash = strURL.find('/', iAlphaSign);
+
+ if (iSlash >= iEnd)
+ iSlash = std::string::npos;
+ }
+
+ std::string strHostNameAndPort = strURL.substr(iPos, (iSlash == std::string::npos) ? iEnd - iPos : iSlash - iPos);
+ // check for IPv6 numerical representation inside [].
+ // if [] found, let's store string inside as hostname
+ // and remove that parsed part from strHostNameAndPort
+ size_t iBrk = strHostNameAndPort.rfind(']');
+ if (iBrk != std::string::npos && strHostNameAndPort.find('[') == 0)
+ {
+ m_strHostName = strHostNameAndPort.substr(1, iBrk-1);
+ strHostNameAndPort.erase(0, iBrk+1);
+ }
+
+ // detect hostname:port/ or just :port/ if previous step found [IPv6] format
+ size_t iColon = strHostNameAndPort.rfind(':');
+ if (iColon != std::string::npos && iColon == strHostNameAndPort.find(':'))
+ {
+ if (m_strHostName.empty())
+ m_strHostName = strHostNameAndPort.substr(0, iColon);
+ m_iPort = atoi(strHostNameAndPort.substr(iColon + 1).c_str());
+ }
+
+ // if we still don't have hostname, the strHostNameAndPort substring
+ // is 'just' hostname without :port specification - so use it as is.
+ if (m_strHostName.empty())
+ m_strHostName = strHostNameAndPort;
+
+ if (iSlash != std::string::npos)
+ {
+ iPos = iSlash + 1;
+ if (iEnd > iPos)
+ m_strFileName = strURL.substr(iPos, iEnd - iPos);
+ }
+
+ if (IsProtocol("musicdb") || IsProtocol("videodb") || IsProtocol("sources") || IsProtocol("pvr"))
+ {
+ if (m_strHostName != "" && m_strFileName != "")
+ {
+ m_strFileName = StringUtils::Format("{}/{}", m_strHostName, m_strFileName);
+ m_strHostName = "";
+ }
+ else
+ {
+ if (!m_strHostName.empty() && strURL[iEnd-1]=='/')
+ m_strFileName = m_strHostName + "/";
+ else
+ m_strFileName = m_strHostName;
+ m_strHostName = "";
+ }
+ }
+
+ StringUtils::Replace(m_strFileName, '\\', '/');
+
+ /* update extension + sharename */
+ SetFileName(m_strFileName);
+
+ /* decode urlencoding on this stuff */
+ if(URIUtils::HasEncodedHostname(*this))
+ {
+ m_strHostName = Decode(m_strHostName);
+ SetHostName(m_strHostName);
+ }
+
+ m_strUserName = Decode(m_strUserName);
+ m_strPassword = Decode(m_strPassword);
+}
+
+void CURL::SetFileName(const std::string& strFileName)
+{
+ m_strFileName = strFileName;
+
+ size_t slash = m_strFileName.find_last_of(GetDirectorySeparator());
+ size_t period = m_strFileName.find_last_of('.');
+ if(period != std::string::npos && (slash == std::string::npos || period > slash))
+ m_strFileType = m_strFileName.substr(period+1);
+ else
+ m_strFileType = "";
+
+ slash = m_strFileName.find_first_of(GetDirectorySeparator());
+ if(slash == std::string::npos)
+ m_strShareName = m_strFileName;
+ else
+ m_strShareName = m_strFileName.substr(0, slash);
+
+ StringUtils::Trim(m_strFileType);
+ StringUtils::ToLower(m_strFileType);
+}
+
+void CURL::SetProtocol(const std::string& strProtocol)
+{
+ m_strProtocol = strProtocol;
+ StringUtils::ToLower(m_strProtocol);
+}
+
+void CURL::SetOptions(const std::string& strOptions)
+{
+ m_strOptions.clear();
+ m_options.Clear();
+ if( strOptions.length() > 0)
+ {
+ if(strOptions[0] == '?' ||
+ strOptions[0] == '#' ||
+ strOptions[0] == ';' ||
+ strOptions.find("xml") != std::string::npos)
+ {
+ m_strOptions = strOptions;
+ m_options.AddOptions(m_strOptions);
+ }
+ else
+ CLog::Log(LOGWARNING, "{} - Invalid options specified for url {}", __FUNCTION__, strOptions);
+ }
+}
+
+void CURL::SetProtocolOptions(const std::string& strOptions)
+{
+ m_strProtocolOptions.clear();
+ m_protocolOptions.Clear();
+ if (strOptions.length() > 0)
+ {
+ if (strOptions[0] == '|')
+ m_strProtocolOptions = strOptions.substr(1);
+ else
+ m_strProtocolOptions = strOptions;
+ m_protocolOptions.AddOptions(m_strProtocolOptions);
+ }
+}
+
+const std::string CURL::GetTranslatedProtocol() const
+{
+ if (IsProtocol("shout")
+ || IsProtocol("dav")
+ || IsProtocol("rss"))
+ return "http";
+
+ if (IsProtocol("davs")
+ || IsProtocol("rsss"))
+ return "https";
+
+ return GetProtocol();
+}
+
+const std::string CURL::GetFileNameWithoutPath() const
+{
+ // *.zip and *.rar store the actual zip/rar path in the hostname of the url
+ if ((IsProtocol("rar") ||
+ IsProtocol("zip") ||
+ IsProtocol("xbt") ||
+ IsProtocol("apk")) &&
+ m_strFileName.empty())
+ return URIUtils::GetFileName(m_strHostName);
+
+ // otherwise, we've already got the filepath, so just grab the filename portion
+ std::string file(m_strFileName);
+ URIUtils::RemoveSlashAtEnd(file);
+ return URIUtils::GetFileName(file);
+}
+
+inline
+void protectIPv6(std::string &hn)
+{
+ if (!hn.empty() && hn.find(':') != hn.rfind(':') && hn.find(':') != std::string::npos)
+ {
+ hn = '[' + hn + ']';
+ }
+}
+
+char CURL::GetDirectorySeparator() const
+{
+#ifndef TARGET_POSIX
+ //We don't want to use IsLocal here, it can return true
+ //for network protocols that matches localhost or hostname
+ //we only ever want to use \ for win32 local filesystem
+ if ( m_strProtocol.empty() )
+ return '\\';
+ else
+#endif
+ return '/';
+}
+
+std::string CURL::Get() const
+{
+ if (m_strProtocol.empty())
+ return m_strFileName;
+
+ unsigned int sizeneed = m_strProtocol.length()
+ + m_strDomain.length()
+ + m_strUserName.length()
+ + m_strPassword.length()
+ + m_strHostName.length()
+ + m_strFileName.length()
+ + m_strOptions.length()
+ + m_strProtocolOptions.length()
+ + 10;
+
+ std::string strURL;
+ strURL.reserve(sizeneed);
+
+ strURL = GetWithoutOptions();
+
+ if( !m_strOptions.empty() )
+ strURL += m_strOptions;
+
+ if (!m_strProtocolOptions.empty())
+ strURL += "|"+m_strProtocolOptions;
+
+ return strURL;
+}
+
+std::string CURL::GetWithoutOptions() const
+{
+ if (m_strProtocol.empty())
+ return m_strFileName;
+
+ std::string strGet = GetWithoutFilename();
+
+ // Prevent double slash when concatenating host part and filename part
+ if (m_strFileName.size() && (m_strFileName[0] == '/' || m_strFileName[0] == '\\') && URIUtils::HasSlashAtEnd(strGet))
+ URIUtils::RemoveSlashAtEnd(strGet);
+
+ return strGet + m_strFileName;
+}
+
+std::string CURL::GetWithoutUserDetails(bool redact) const
+{
+ std::string strURL;
+
+ if (IsProtocol("stack"))
+ {
+ CFileItemList items;
+ XFILE::CStackDirectory dir;
+ dir.GetDirectory(*this,items);
+ std::vector<std::string> newItems;
+ for (int i=0;i<items.Size();++i)
+ {
+ CURL url(items[i]->GetPath());
+ items[i]->SetPath(url.GetWithoutUserDetails(redact));
+ newItems.push_back(items[i]->GetPath());
+ }
+ dir.ConstructStackPath(newItems, strURL);
+ return strURL;
+ }
+
+ unsigned int sizeneed = m_strProtocol.length()
+ + m_strHostName.length()
+ + m_strFileName.length()
+ + m_strOptions.length()
+ + m_strProtocolOptions.length()
+ + 10;
+
+ if (redact && !m_strUserName.empty())
+ {
+ sizeneed += sizeof("USERNAME");
+ if (!m_strPassword.empty())
+ sizeneed += sizeof(":PASSWORD@");
+ if (!m_strDomain.empty())
+ sizeneed += sizeof("DOMAIN;");
+ }
+
+ strURL.reserve(sizeneed);
+
+ if (m_strProtocol.empty())
+ return m_strFileName;
+
+ strURL = m_strProtocol;
+ strURL += "://";
+
+ if (redact && !m_strUserName.empty())
+ {
+ if (!m_strDomain.empty())
+ strURL += "DOMAIN;";
+ strURL += "USERNAME";
+ if (!m_strPassword.empty())
+ strURL += ":PASSWORD";
+ strURL += "@";
+ }
+
+ if (!m_strHostName.empty())
+ {
+ std::string strHostName;
+
+ if (URIUtils::HasParentInHostname(*this))
+ strHostName = CURL(m_strHostName).GetWithoutUserDetails();
+ else
+ strHostName = m_strHostName;
+
+ if (URIUtils::HasEncodedHostname(*this))
+ strHostName = Encode(strHostName);
+
+ if ( HasPort() )
+ {
+ protectIPv6(strHostName);
+ strURL += strHostName + StringUtils::Format(":{}", m_iPort);
+ }
+ else
+ strURL += strHostName;
+
+ strURL += "/";
+ }
+ strURL += m_strFileName;
+
+ if( m_strOptions.length() > 0 )
+ strURL += m_strOptions;
+ if( m_strProtocolOptions.length() > 0 )
+ strURL += "|"+m_strProtocolOptions;
+
+ return strURL;
+}
+
+std::string CURL::GetWithoutFilename() const
+{
+ if (m_strProtocol.empty())
+ return "";
+
+ unsigned int sizeneed = m_strProtocol.length()
+ + m_strDomain.length()
+ + m_strUserName.length()
+ + m_strPassword.length()
+ + m_strHostName.length()
+ + 10;
+
+ std::string strURL;
+ strURL.reserve(sizeneed);
+
+ strURL = m_strProtocol;
+ strURL += "://";
+
+ if (!m_strUserName.empty())
+ {
+ if (!m_strDomain.empty())
+ {
+ strURL += Encode(m_strDomain);
+ strURL += ";";
+ }
+ strURL += Encode(m_strUserName);
+ if (!m_strPassword.empty())
+ {
+ strURL += ":";
+ strURL += Encode(m_strPassword);
+ }
+ strURL += "@";
+ }
+
+ if (!m_strHostName.empty())
+ {
+ std::string hostname;
+
+ if( URIUtils::HasEncodedHostname(*this) )
+ hostname = Encode(m_strHostName);
+ else
+ hostname = m_strHostName;
+
+ if (HasPort())
+ {
+ protectIPv6(hostname);
+ strURL += hostname + StringUtils::Format(":{}", m_iPort);
+ }
+ else
+ strURL += hostname;
+
+ strURL += "/";
+ }
+
+ return strURL;
+}
+
+std::string CURL::GetRedacted() const
+{
+ return GetWithoutUserDetails(true);
+}
+
+std::string CURL::GetRedacted(const std::string& path)
+{
+ return CURL(path).GetRedacted();
+}
+
+bool CURL::IsLocal() const
+{
+ return (m_strProtocol.empty() || IsLocalHost() || IsProtocol("win-lib"));
+}
+
+bool CURL::IsLocalHost() const
+{
+ return CServiceBroker::GetNetwork().IsLocalHost(m_strHostName);
+}
+
+bool CURL::IsFileOnly(const std::string &url)
+{
+ return url.find_first_of("/\\") == std::string::npos;
+}
+
+bool CURL::IsFullPath(const std::string &url)
+{
+ if (url.size() && url[0] == '/') return true; // /foo/bar.ext
+ if (url.find("://") != std::string::npos) return true; // foo://bar.ext
+ if (url.size() > 1 && url[1] == ':') return true; // c:\\foo\\bar\\bar.ext
+ if (StringUtils::StartsWith(url, "\\\\")) return true; // \\UNC\path\to\file
+ return false;
+}
+
+std::string CURL::Decode(const std::string& strURLData)
+//modified to be more accommodating - if a non hex value follows a % take the characters directly and don't raise an error.
+// However % characters should really be escaped like any other non safe character (www.rfc-editor.org/rfc/rfc1738.txt)
+{
+ std::string strResult;
+
+ /* result will always be less than source */
+ strResult.reserve( strURLData.length() );
+
+ for (unsigned int i = 0; i < strURLData.size(); ++i)
+ {
+ int kar = (unsigned char)strURLData[i];
+ if (kar == '+') strResult += ' ';
+ else if (kar == '%')
+ {
+ if (i < strURLData.size() - 2)
+ {
+ std::string strTmp;
+ strTmp.assign(strURLData.substr(i + 1, 2));
+ int dec_num=-1;
+ sscanf(strTmp.c_str(), "%x", (unsigned int *)&dec_num);
+ if (dec_num<0 || dec_num>255)
+ strResult += kar;
+ else
+ {
+ strResult += (char)dec_num;
+ i += 2;
+ }
+ }
+ else
+ strResult += kar;
+ }
+ else strResult += kar;
+ }
+
+ return strResult;
+}
+
+std::string CURL::Encode(const std::string& strURLData)
+{
+ std::string strResult;
+
+ /* wonder what a good value is here is, depends on how often it occurs */
+ strResult.reserve( strURLData.length() * 2 );
+
+ for (size_t i = 0; i < strURLData.size(); ++i)
+ {
+ const char kar = strURLData[i];
+
+ // Don't URL encode "-_.!()" according to RFC1738
+ //! @todo Update it to "-_.~" after Gotham according to RFC3986
+ if (StringUtils::isasciialphanum(kar) || kar == '-' || kar == '.' || kar == '_' || kar == '!' || kar == '(' || kar == ')')
+ strResult.push_back(kar);
+ else
+ strResult += StringUtils::Format("%{:02x}", (unsigned int)((unsigned char)kar));
+ }
+
+ return strResult;
+}
+
+bool CURL::IsProtocolEqual(const std::string &protocol, const char *type)
+{
+ /*
+ NOTE: We're currently using == here as m_strProtocol is assigned as lower-case in SetProtocol(),
+ and we've assumed all other callers are calling with protocol lower-case otherwise.
+ We possibly shouldn't do this (as CURL(foo).Get() != foo, though there are other reasons for this as well)
+ but it handles the requirements of RFC-1738 which allows the scheme to be case-insensitive.
+ */
+ if (type)
+ return protocol == type;
+ return false;
+}
+
+void CURL::GetOptions(std::map<std::string, std::string> &options) const
+{
+ CUrlOptions::UrlOptions optionsMap = m_options.GetOptions();
+ for (CUrlOptions::UrlOptions::const_iterator option = optionsMap.begin(); option != optionsMap.end(); option++)
+ options[option->first] = option->second.asString();
+}
+
+bool CURL::HasOption(const std::string &key) const
+{
+ return m_options.HasOption(key);
+}
+
+bool CURL::GetOption(const std::string &key, std::string &value) const
+{
+ CVariant valueObj;
+ if (!m_options.GetOption(key, valueObj))
+ return false;
+
+ value = valueObj.asString();
+ return true;
+}
+
+std::string CURL::GetOption(const std::string &key) const
+{
+ std::string value;
+ if (!GetOption(key, value))
+ return "";
+
+ return value;
+}
+
+void CURL::SetOption(const std::string &key, const std::string &value)
+{
+ m_options.AddOption(key, value);
+ SetOptions(m_options.GetOptionsString(true));
+}
+
+void CURL::RemoveOption(const std::string &key)
+{
+ m_options.RemoveOption(key);
+ SetOptions(m_options.GetOptionsString(true));
+}
+
+void CURL::GetProtocolOptions(std::map<std::string, std::string> &options) const
+{
+ CUrlOptions::UrlOptions optionsMap = m_protocolOptions.GetOptions();
+ for (CUrlOptions::UrlOptions::const_iterator option = optionsMap.begin(); option != optionsMap.end(); option++)
+ options[option->first] = option->second.asString();
+}
+
+bool CURL::HasProtocolOption(const std::string &key) const
+{
+ return m_protocolOptions.HasOption(key);
+}
+
+bool CURL::GetProtocolOption(const std::string &key, std::string &value) const
+{
+ CVariant valueObj;
+ if (!m_protocolOptions.GetOption(key, valueObj))
+ return false;
+
+ value = valueObj.asString();
+ return true;
+}
+
+std::string CURL::GetProtocolOption(const std::string &key) const
+{
+ std::string value;
+ if (!GetProtocolOption(key, value))
+ return "";
+
+ return value;
+}
+
+void CURL::SetProtocolOption(const std::string &key, const std::string &value)
+{
+ m_protocolOptions.AddOption(key, value);
+ m_strProtocolOptions = m_protocolOptions.GetOptionsString(false);
+}
+
+void CURL::RemoveProtocolOption(const std::string &key)
+{
+ m_protocolOptions.RemoveOption(key);
+ m_strProtocolOptions = m_protocolOptions.GetOptionsString(false);
+}
diff --git a/xbmc/URL.h b/xbmc/URL.h
new file mode 100644
index 0000000..77c12cc
--- /dev/null
+++ b/xbmc/URL.h
@@ -0,0 +1,202 @@
+/*
+ * 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 "utils/UrlOptions.h"
+
+#include <stdlib.h>
+#include <string>
+
+#ifdef TARGET_WINDOWS
+#undef SetPort // WIN32INCLUDES this is defined as SetPortA in WinSpool.h which is being included _somewhere_
+#endif
+
+class CURL
+{
+public:
+ explicit CURL(const std::string& strURL)
+ {
+ Parse(strURL);
+ }
+
+ CURL() = default;
+ virtual ~CURL(void);
+
+ // explicit equals operator for std::string comparison
+ bool operator==(const std::string &url) const { return Get() == url; }
+
+ void Reset();
+ void Parse(const std::string& strURL);
+ void SetFileName(const std::string& strFileName);
+ void SetHostName(const std::string& strHostName)
+ {
+ m_strHostName = strHostName;
+ }
+
+ void SetUserName(const std::string& strUserName)
+ {
+ m_strUserName = strUserName;
+ }
+
+ void SetDomain(const std::string& strDomain)
+ {
+ m_strDomain = strDomain;
+ }
+
+ void SetPassword(const std::string& strPassword)
+ {
+ m_strPassword = strPassword;
+ }
+
+ void SetProtocol(const std::string& strProtocol);
+ void SetOptions(const std::string& strOptions);
+ void SetProtocolOptions(const std::string& strOptions);
+ void SetPort(int port)
+ {
+ m_iPort = port;
+ }
+
+ bool HasPort() const
+ {
+ return (m_iPort != 0);
+ }
+
+ int GetPort() const
+ {
+ return m_iPort;
+ }
+
+ const std::string& GetHostName() const
+ {
+ return m_strHostName;
+ }
+
+ const std::string& GetDomain() const
+ {
+ return m_strDomain;
+ }
+
+ const std::string& GetUserName() const
+ {
+ return m_strUserName;
+ }
+
+ const std::string& GetPassWord() const
+ {
+ return m_strPassword;
+ }
+
+ const std::string& GetFileName() const
+ {
+ return m_strFileName;
+ }
+
+ const std::string& GetProtocol() const
+ {
+ return m_strProtocol;
+ }
+
+ const std::string GetTranslatedProtocol() const;
+
+ const std::string& GetFileType() const
+ {
+ return m_strFileType;
+ }
+
+ const std::string& GetShareName() const
+ {
+ return m_strShareName;
+ }
+
+ const std::string& GetOptions() const
+ {
+ return m_strOptions;
+ }
+
+ const std::string& GetProtocolOptions() const
+ {
+ return m_strProtocolOptions;
+ }
+
+ const std::string GetFileNameWithoutPath() const; /* return the filename excluding path */
+
+ char GetDirectorySeparator() const;
+
+ std::string Get() const;
+ std::string GetWithoutOptions() const;
+ std::string GetWithoutUserDetails(bool redact = false) const;
+ std::string GetWithoutFilename() const;
+ std::string GetRedacted() const;
+ static std::string GetRedacted(const std::string& path);
+ bool IsLocal() const;
+ bool IsLocalHost() const;
+ static bool IsFileOnly(const std::string &url); ///< return true if there are no directories in the url.
+ static bool IsFullPath(const std::string &url); ///< return true if the url includes the full path
+ static std::string Decode(const std::string& strURLData);
+ static std::string Encode(const std::string& strURLData);
+
+ /*! \brief Check whether a URL is a given URL scheme.
+ Comparison is case-insensitive as per RFC1738
+ \param type a lower-case scheme name, e.g. "smb".
+ \return true if the url is of the given scheme, false otherwise.
+ */
+ bool IsProtocol(const char *type) const
+ {
+ return IsProtocolEqual(m_strProtocol, type);
+ }
+
+ /*! \brief Check whether a URL protocol is a given URL scheme.
+ Both parameters MUST be lower-case. Typically this would be called using
+ the result of TranslateProtocol() which enforces this for protocol.
+ \param protocol a lower-case scheme name, e.g. "ftp"
+ \param type a lower-case scheme name, e.g. "smb".
+ \return true if the url is of the given scheme, false otherwise.
+ */
+ static bool IsProtocolEqual(const std::string& protocol, const char *type);
+
+ /*! \brief Check whether a URL is a given filetype.
+ Comparison is effectively case-insensitive as both the parameter
+ and m_strFileType are lower-case.
+ \param type a lower-case filetype, e.g. "mp3".
+ \return true if the url is of the given filetype, false otherwise.
+ */
+ bool IsFileType(const char *type) const
+ {
+ return m_strFileType == type;
+ }
+
+ void GetOptions(std::map<std::string, std::string> &options) const;
+ bool HasOption(const std::string &key) const;
+ bool GetOption(const std::string &key, std::string &value) const;
+ std::string GetOption(const std::string &key) const;
+ void SetOption(const std::string &key, const std::string &value);
+ void RemoveOption(const std::string &key);
+
+ void GetProtocolOptions(std::map<std::string, std::string> &options) const;
+ bool HasProtocolOption(const std::string &key) const;
+ bool GetProtocolOption(const std::string &key, std::string &value) const;
+ std::string GetProtocolOption(const std::string &key) const;
+ void SetProtocolOption(const std::string &key, const std::string &value);
+ void RemoveProtocolOption(const std::string &key);
+
+protected:
+ int m_iPort = 0;
+ std::string m_strHostName;
+ std::string m_strShareName;
+ std::string m_strDomain;
+ std::string m_strUserName;
+ std::string m_strPassword;
+ std::string m_strFileName;
+ std::string m_strProtocol;
+ std::string m_strFileType;
+ std::string m_strOptions;
+ std::string m_strProtocolOptions;
+ CUrlOptions m_options;
+ CUrlOptions m_protocolOptions;
+};
diff --git a/xbmc/Util.cpp b/xbmc/Util.cpp
new file mode 100644
index 0000000..50e7987
--- /dev/null
+++ b/xbmc/Util.cpp
@@ -0,0 +1,2350 @@
+/*
+ * 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 "network/Network.h"
+#if defined(TARGET_DARWIN)
+#include <sys/param.h>
+#include <mach-o/dyld.h>
+#endif
+
+#if defined(TARGET_FREEBSD)
+#include <sys/param.h>
+#include <sys/sysctl.h>
+#endif
+
+#ifdef TARGET_POSIX
+#include <sys/types.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <sys/wait.h>
+#endif
+#if defined(TARGET_ANDROID)
+#include <androidjni/ApplicationInfo.h>
+#include "platform/android/bionic_supplement/bionic_supplement.h"
+#include "platform/android/activity/XBMCApp.h"
+#include "CompileInfo.h"
+#endif
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "addons/VFSEntry.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "filesystem/PVRDirectory.h"
+#include "filesystem/RSSDirectory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "filesystem/StackDirectory.h"
+
+#include <algorithm>
+#include <array>
+#include <stdlib.h>
+#ifdef HAS_UPNP
+#include "filesystem/UPnPDirectory.h"
+#endif
+#include "profiles/ProfileManager.h"
+#include "utils/RegExp.h"
+#include "windowing/GraphicContext.h"
+#include "guilib/TextureManager.h"
+#include "storage/MediaManager.h"
+#ifdef TARGET_WINDOWS
+#include "utils/CharsetConverter.h"
+#include "WIN32Util.h"
+#endif
+#if defined(TARGET_DARWIN)
+#include "CompileInfo.h"
+#include "platform/darwin/DarwinUtils.h"
+#endif
+#include "URL.h"
+#include "cores/VideoPlayer/DVDSubtitles/DVDSubtitleStream.h"
+#include "cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.h"
+#include "filesystem/File.h"
+#include "guilib/LocalizeStrings.h"
+#include "platform/Environment.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FontUtils.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoInfoTag.h"
+#ifdef HAVE_LIBCAP
+ #include <sys/capability.h>
+#endif
+
+#include "cores/VideoPlayer/DVDDemuxers/DVDDemux.h"
+
+#include <fstrcmp.h>
+
+#ifdef HAS_DVD_DRIVE
+using namespace MEDIA_DETECT;
+#endif
+
+using namespace XFILE;
+using namespace PLAYLIST;
+using KODI::UTILITY::CDigest;
+
+#if !defined(TARGET_WINDOWS)
+unsigned int CUtil::s_randomSeed = time(NULL);
+#endif
+
+namespace
+{
+#ifdef TARGET_WINDOWS
+bool IsDirectoryValidRoot(std::wstring path)
+{
+ path += L"\\system\\settings\\settings.xml";
+#if defined(TARGET_WINDOWS_STORE)
+ auto h = CreateFile2(path.c_str(), GENERIC_READ, 0, OPEN_EXISTING, NULL);
+#else
+ auto h = CreateFileW(path.c_str(), GENERIC_READ, 0, nullptr,
+ OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
+#endif
+ if (h != INVALID_HANDLE_VALUE)
+ {
+ CloseHandle(h);
+ return true;
+ }
+
+ return false;
+}
+
+std::string GetHomePath(const std::string& strTarget, std::string strPath)
+{
+ std::wstring strPathW;
+
+ // Environment variable was set and we have a path
+ // Let's make sure it's not relative and test it
+ // so it's actually pointing to a directory containing
+ // our stuff
+ if (strPath.find("..") != std::string::npos)
+ {
+ //expand potential relative path to full path
+ g_charsetConverter.utf8ToW(strPath, strPathW, false);
+ CWIN32Util::AddExtraLongPathPrefix(strPathW);
+ auto bufSize = GetFullPathNameW(strPathW.c_str(), 0, nullptr, nullptr);
+ if (bufSize != 0)
+ {
+ auto buf = std::make_unique<wchar_t[]>(bufSize);
+ if (GetFullPathNameW(strPathW.c_str(), bufSize, buf.get(), nullptr) <= bufSize - 1)
+ {
+ strPathW = buf.get();
+ CWIN32Util::RemoveExtraLongPathPrefix(strPathW);
+
+ if (IsDirectoryValidRoot(strPathW))
+ {
+ g_charsetConverter.wToUTF8(strPathW, strPath);
+ return strPath;
+ }
+ }
+ }
+ }
+
+ // Okay se no environment variable is set, let's
+ // grab the executable path and check if it's being
+ // run from a directory containing our stuff
+ strPath = CUtil::ResolveExecutablePath();
+ auto last_sep = strPath.find_last_of(PATH_SEPARATOR_CHAR);
+ if (last_sep != std::string::npos)
+ strPath = strPath.substr(0, last_sep);
+
+ g_charsetConverter.utf8ToW(strPath, strPathW);
+ if (IsDirectoryValidRoot(strPathW))
+ return strPath;
+
+ // Still nothing, let's check the current working
+ // directory and see if it points to a directory
+ // with our stuff in it. This bit should never be
+ // needed when running on a users system, it's intended
+ // to make our dev environment easier.
+ auto bufSize = GetCurrentDirectoryW(0, nullptr);
+ if (bufSize > 0)
+ {
+ auto buf = std::make_unique<wchar_t[]>(bufSize);
+ if (0 != GetCurrentDirectoryW(bufSize, buf.get()))
+ {
+ std::string currentDirectory;
+ std::wstring currentDirectoryW(buf.get());
+ CWIN32Util::RemoveExtraLongPathPrefix(currentDirectoryW);
+
+ if (IsDirectoryValidRoot(currentDirectoryW))
+ {
+ g_charsetConverter.wToUTF8(currentDirectoryW, currentDirectory);
+ return currentDirectory;
+ }
+ }
+ }
+
+ // If we ended up here we're most likely screwed
+ // we will crash in a few seconds
+ return strPath;
+}
+#endif
+#if defined(TARGET_DARWIN)
+#if !defined(TARGET_DARWIN_EMBEDDED)
+bool IsDirectoryValidRoot(std::string path)
+{
+ path += "/system/settings/settings.xml";
+ return CFile::Exists(path);
+}
+#endif
+
+std::string GetHomePath(const std::string& strTarget, std::string strPath)
+{
+ if (strPath.empty())
+ {
+ auto strHomePath = CUtil::ResolveExecutablePath();
+ int result = -1;
+ char given_path[2 * MAXPATHLEN];
+ size_t path_size = 2 * MAXPATHLEN;
+
+ result = CDarwinUtils::GetExecutablePath(given_path, &path_size);
+ if (result == 0)
+ {
+ // Move backwards to last /.
+ for (int n = strlen(given_path) - 1; given_path[n] != '/'; n--)
+ given_path[n] = '\0';
+
+#if defined(TARGET_DARWIN_EMBEDDED)
+ strcat(given_path, "/AppData/AppHome/");
+#else
+ // Assume local path inside application bundle.
+ strcat(given_path, "../Resources/");
+ strcat(given_path, CCompileInfo::GetAppName());
+ strcat(given_path, "/");
+
+ // if this path doesn't exist we
+ // might not be started from the app bundle
+ // but from the debugger/xcode. Lets
+ // see if this assumption is valid
+ if (!CDirectory::Exists(given_path))
+ {
+ std::string given_path_stdstr = CUtil::ResolveExecutablePath();
+ // try to find the correct folder by going back
+ // in the executable path until settings.xml was found
+ bool validRoot = false;
+ do
+ {
+ given_path_stdstr = URIUtils::GetParentPath(given_path_stdstr);
+ validRoot = IsDirectoryValidRoot(given_path_stdstr);
+ }
+ while(given_path_stdstr.length() > 0 && !validRoot);
+ strncpy(given_path, given_path_stdstr.c_str(), sizeof(given_path)-1);
+ }
+
+#endif
+
+ // Convert to real path.
+ char real_path[2 * MAXPATHLEN];
+ if (realpath(given_path, real_path) != NULL)
+ {
+ strPath = real_path;
+ return strPath;
+ }
+ }
+ size_t last_sep = strHomePath.find_last_of(PATH_SEPARATOR_CHAR);
+ if (last_sep != std::string::npos)
+ strPath = strHomePath.substr(0, last_sep);
+ else
+ strPath = strHomePath;
+
+ }
+ return strPath;
+}
+#endif
+#if defined(TARGET_POSIX) && !defined(TARGET_DARWIN)
+std::string GetHomePath(const std::string& strTarget, std::string strPath)
+{
+ if (strPath.empty())
+ {
+ auto strHomePath = CUtil::ResolveExecutablePath();
+ size_t last_sep = strHomePath.find_last_of(PATH_SEPARATOR_CHAR);
+ if (last_sep != std::string::npos)
+ strPath = strHomePath.substr(0, last_sep);
+ else
+ strPath = strHomePath;
+ }
+ /* Change strPath accordingly when target is KODI_HOME and when INSTALL_PATH
+ * and BIN_INSTALL_PATH differ
+ */
+ std::string installPath = INSTALL_PATH;
+ std::string binInstallPath = BIN_INSTALL_PATH;
+
+ if (strTarget.empty() && installPath.compare(binInstallPath))
+ {
+ int pos = strPath.length() - binInstallPath.length();
+ std::string tmp = strPath;
+ tmp.erase(0, pos);
+ if (!tmp.compare(binInstallPath))
+ {
+ strPath.erase(pos, strPath.length());
+ strPath.append(installPath);
+ }
+ }
+
+ return strPath;
+}
+#endif
+}
+
+std::string CUtil::GetTitleFromPath(const std::string& strFileNameAndPath, bool bIsFolder /* = false */)
+{
+ CURL pathToUrl(strFileNameAndPath);
+ return GetTitleFromPath(pathToUrl, bIsFolder);
+}
+
+std::string CUtil::GetTitleFromPath(const CURL& url, bool bIsFolder /* = false */)
+{
+ // use above to get the filename
+ std::string path(url.Get());
+ URIUtils::RemoveSlashAtEnd(path);
+ std::string strFilename = URIUtils::GetFileName(path);
+
+#ifdef HAS_UPNP
+ // UPNP
+ if (url.IsProtocol("upnp"))
+ strFilename = CUPnPDirectory::GetFriendlyName(url);
+#endif
+
+ if (url.IsProtocol("rss") || url.IsProtocol("rsss"))
+ {
+ CRSSDirectory dir;
+ CFileItemList items;
+ if(dir.GetDirectory(url, items) && !items.m_strTitle.empty())
+ return items.m_strTitle;
+ }
+
+ // Shoutcast
+ else if (url.IsProtocol("shout"))
+ {
+ const std::string strFileNameAndPath = url.Get();
+ const size_t genre = strFileNameAndPath.find_first_of('=');
+ if(genre == std::string::npos)
+ strFilename = g_localizeStrings.Get(260);
+ else
+ strFilename = g_localizeStrings.Get(260) + " - " + strFileNameAndPath.substr(genre+1).c_str();
+ }
+
+ // Windows SMB Network (SMB)
+ else if (url.IsProtocol("smb") && strFilename.empty())
+ {
+ if (url.GetHostName().empty())
+ {
+ strFilename = g_localizeStrings.Get(20171);
+ }
+ else
+ {
+ strFilename = url.GetHostName();
+ }
+ }
+
+ // Root file views
+ else if (url.IsProtocol("sources"))
+ strFilename = g_localizeStrings.Get(744);
+
+ // Music Playlists
+ else if (StringUtils::StartsWith(path, "special://musicplaylists"))
+ strFilename = g_localizeStrings.Get(136);
+
+ // Video Playlists
+ else if (StringUtils::StartsWith(path, "special://videoplaylists"))
+ strFilename = g_localizeStrings.Get(136);
+
+ else if (URIUtils::HasParentInHostname(url) && strFilename.empty())
+ strFilename = URIUtils::GetFileName(url.GetHostName());
+
+ // now remove the extension if needed
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWEXTENSIONS) && !bIsFolder)
+ {
+ URIUtils::RemoveExtension(strFilename);
+ return strFilename;
+ }
+
+ // URLDecode since the original path may be an URL
+ strFilename = CURL::Decode(strFilename);
+ return strFilename;
+}
+
+void CUtil::CleanString(const std::string& strFileName,
+ std::string& strTitle,
+ std::string& strTitleAndYear,
+ std::string& strYear,
+ bool bRemoveExtension /* = false */,
+ bool bCleanChars /* = true */)
+{
+ strTitleAndYear = strFileName;
+
+ if (strFileName == "..")
+ return;
+
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+ const std::vector<std::string> &regexps = advancedSettings->m_videoCleanStringRegExps;
+
+ CRegExp reTags(true, CRegExp::autoUtf8);
+ CRegExp reYear(false, CRegExp::autoUtf8);
+
+ if (!reYear.RegComp(advancedSettings->m_videoCleanDateTimeRegExp))
+ {
+ CLog::Log(LOGERROR, "{}: Invalid datetime clean RegExp:'{}'", __FUNCTION__,
+ advancedSettings->m_videoCleanDateTimeRegExp);
+ }
+ else
+ {
+ if (reYear.RegFind(strTitleAndYear.c_str()) >= 0)
+ {
+ strTitleAndYear = reYear.GetMatch(1);
+ strYear = reYear.GetMatch(2);
+ }
+ }
+
+ URIUtils::RemoveExtension(strTitleAndYear);
+
+ for (const auto &regexp : regexps)
+ {
+ if (!reTags.RegComp(regexp.c_str()))
+ { // invalid regexp - complain in logs
+ CLog::Log(LOGERROR, "{}: Invalid string clean RegExp:'{}'", __FUNCTION__, regexp);
+ continue;
+ }
+ int j=0;
+ if ((j=reTags.RegFind(strTitleAndYear.c_str())) > 0)
+ strTitleAndYear = strTitleAndYear.substr(0, j);
+ }
+
+ // final cleanup - special characters used instead of spaces:
+ // all '_' tokens should be replaced by spaces
+ // if the file contains no spaces, all '.' tokens should be replaced by
+ // spaces - one possibility of a mistake here could be something like:
+ // "Dr..StrangeLove" - hopefully no one would have anything like this.
+ if (bCleanChars)
+ {
+ bool initialDots = true;
+ bool alreadyContainsSpace = (strTitleAndYear.find(' ') != std::string::npos);
+
+ for (char &c : strTitleAndYear)
+ {
+ if (c != '.')
+ initialDots = false;
+
+ if ((c == '_') || ((!alreadyContainsSpace) && !initialDots && (c == '.')))
+ {
+ c = ' ';
+ }
+ }
+ }
+
+ StringUtils::Trim(strTitleAndYear);
+ strTitle = strTitleAndYear;
+
+ // append year
+ if (!strYear.empty())
+ strTitleAndYear = strTitle + " (" + strYear + ")";
+
+ // restore extension if needed
+ if (!bRemoveExtension)
+ strTitleAndYear += URIUtils::GetExtension(strFileName);
+}
+
+void CUtil::GetQualifiedFilename(const std::string &strBasePath, std::string &strFilename)
+{
+ // Check if the filename is a fully qualified URL such as protocol://path/to/file
+ CURL plItemUrl(strFilename);
+ if (!plItemUrl.GetProtocol().empty())
+ return;
+
+ // If the filename starts "x:", "\\" or "/" it's already fully qualified so return
+ if (strFilename.size() > 1)
+#ifdef TARGET_POSIX
+ if ( (strFilename[1] == ':') || (strFilename[0] == '/') )
+#else
+ if ( strFilename[1] == ':' || (strFilename[0] == '\\' && strFilename[1] == '\\'))
+#endif
+ return;
+
+ // add to base path and then clean
+ strFilename = URIUtils::AddFileToFolder(strBasePath, strFilename);
+
+ // get rid of any /./ or \.\ that happen to be there
+ StringUtils::Replace(strFilename, "\\.\\", "\\");
+ StringUtils::Replace(strFilename, "/./", "/");
+
+ // now find any "\\..\\" and remove them via GetParentPath
+ size_t pos;
+ while ((pos = strFilename.find("/../")) != std::string::npos)
+ {
+ std::string basePath = strFilename.substr(0, pos + 1);
+ strFilename.erase(0, pos + 4);
+ basePath = URIUtils::GetParentPath(basePath);
+ strFilename = URIUtils::AddFileToFolder(basePath, strFilename);
+ }
+ while ((pos = strFilename.find("\\..\\")) != std::string::npos)
+ {
+ std::string basePath = strFilename.substr(0, pos + 1);
+ strFilename.erase(0, pos + 4);
+ basePath = URIUtils::GetParentPath(basePath);
+ strFilename = URIUtils::AddFileToFolder(basePath, strFilename);
+ }
+}
+
+void CUtil::RunShortcut(const char* szShortcutPath)
+{
+}
+
+std::string CUtil::GetHomePath(const std::string& strTarget)
+{
+ auto strPath = CEnvironment::getenv(strTarget);
+
+ return ::GetHomePath(strTarget, strPath);
+}
+
+bool CUtil::IsPicture(const std::string& strFile)
+{
+ return URIUtils::HasExtension(strFile,
+ CServiceBroker::GetFileExtensionProvider().GetPictureExtensions()+ "|.tbn|.dds");
+}
+
+std::string CUtil::GetSplashPath()
+{
+ std::array<std::string, 4> candidates {{ "special://home/media/splash.jpg", "special://home/media/splash.png", "special://xbmc/media/splash.jpg", "special://xbmc/media/splash.png" }};
+ auto it = std::find_if(candidates.begin(), candidates.end(), [](std::string const& file) { return XFILE::CFile::Exists(file); });
+ if (it == candidates.end())
+ throw std::runtime_error("No splash image found");
+ return CSpecialProtocol::TranslatePathConvertCase(*it);
+}
+
+bool CUtil::ExcludeFileOrFolder(const std::string& strFileOrFolder, const std::vector<std::string>& regexps)
+{
+ if (strFileOrFolder.empty())
+ return false;
+
+ CRegExp regExExcludes(true, CRegExp::autoUtf8); // case insensitive regex
+
+ for (const auto &regexp : regexps)
+ {
+ if (!regExExcludes.RegComp(regexp.c_str()))
+ { // invalid regexp - complain in logs
+ CLog::Log(LOGERROR, "{}: Invalid exclude RegExp:'{}'", __FUNCTION__, regexp);
+ continue;
+ }
+ if (regExExcludes.RegFind(strFileOrFolder) > -1)
+ {
+ CLog::LogF(LOGDEBUG, "File '{}' excluded. (Matches exclude rule RegExp: '{}')", CURL::GetRedacted(strFileOrFolder), regexp);
+ return true;
+ }
+ }
+ return false;
+}
+
+void CUtil::GetFileAndProtocol(const std::string& strURL, std::string& strDir)
+{
+ strDir = strURL;
+ if (!URIUtils::IsRemote(strURL)) return ;
+ if (URIUtils::IsDVD(strURL)) return ;
+
+ CURL url(strURL);
+ strDir = StringUtils::Format("{}://{}", url.GetProtocol(), url.GetFileName());
+}
+
+int CUtil::GetDVDIfoTitle(const std::string& strFile)
+{
+ std::string strFilename = URIUtils::GetFileName(strFile);
+ if (StringUtils::EqualsNoCase(strFilename, "video_ts.ifo")) return 0;
+ //VTS_[TITLE]_0.IFO
+ return atoi(strFilename.substr(4, 2).c_str());
+}
+
+std::string CUtil::GetFileDigest(const std::string& strPath, KODI::UTILITY::CDigest::Type type)
+{
+ CFile file;
+ std::string result;
+ if (file.Open(strPath))
+ {
+ CDigest digest{type};
+ char temp[1024];
+ while (true)
+ {
+ ssize_t read = file.Read(temp,1024);
+ if (read <= 0)
+ break;
+ digest.Update(temp,read);
+ }
+ result = digest.Finalize();
+ file.Close();
+ }
+
+ return result;
+}
+
+bool CUtil::GetDirectoryName(const std::string& strFileName, std::string& strDescription)
+{
+ std::string strFName = URIUtils::GetFileName(strFileName);
+ strDescription = URIUtils::GetDirectory(strFileName);
+ URIUtils::RemoveSlashAtEnd(strDescription);
+
+ size_t iPos = strDescription.find_last_of("/\\");
+ if (iPos != std::string::npos)
+ strDescription = strDescription.substr(iPos + 1);
+ else if (strDescription.size() <= 0)
+ strDescription = strFName;
+ return true;
+}
+
+void CUtil::GetDVDDriveIcon(const std::string& strPath, std::string& strIcon)
+{
+ if (!CServiceBroker::GetMediaManager().IsDiscInDrive(strPath))
+ {
+ strIcon = "DefaultDVDEmpty.png";
+ return ;
+ }
+
+ CFileItem item = CFileItem(strPath, false);
+
+ if (item.IsBluray())
+ {
+ strIcon = "DefaultBluray.png";
+ return;
+ }
+
+ if ( URIUtils::IsDVD(strPath) )
+ {
+ strIcon = "DefaultDVDFull.png";
+ return ;
+ }
+
+ if ( URIUtils::IsISO9660(strPath) )
+ {
+#ifdef HAS_DVD_DRIVE
+ CCdInfo* pInfo = CServiceBroker::GetMediaManager().GetCdInfo();
+ if ( pInfo != NULL && pInfo->IsVideoCd( 1 ) )
+ {
+ strIcon = "DefaultVCD.png";
+ return ;
+ }
+#endif
+ strIcon = "DefaultDVDRom.png";
+ return ;
+ }
+
+ if ( URIUtils::IsCDDA(strPath) )
+ {
+ strIcon = "DefaultCDDA.png";
+ return ;
+ }
+}
+
+void CUtil::RemoveTempFiles()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::string searchPath = profileManager->GetDatabaseFolder();
+ CFileItemList items;
+ if (!XFILE::CDirectory::GetDirectory(searchPath, items, ".tmp", DIR_FLAG_NO_FILE_DIRS))
+ return;
+
+ for (const auto &item : items)
+ {
+ if (item->m_bIsFolder)
+ continue;
+ XFILE::CFile::Delete(item->GetPath());
+ }
+}
+
+void CUtil::ClearSubtitles()
+{
+ //delete cached subs
+ CFileItemList items;
+ CDirectory::GetDirectory("special://temp/",items, "", DIR_FLAG_DEFAULTS);
+ for (const auto &item : items)
+ {
+ if (!item->m_bIsFolder)
+ {
+ if (item->GetPath().find("subtitle") != std::string::npos ||
+ item->GetPath().find("vobsub_queue") != std::string::npos)
+ {
+ CLog::Log(LOGDEBUG, "{} - Deleting temporary subtitle {}", __FUNCTION__, item->GetPath());
+ CFile::Delete(item->GetPath());
+ }
+ }
+ }
+}
+
+int64_t CUtil::ToInt64(uint32_t high, uint32_t low)
+{
+ int64_t n;
+ n = high;
+ n <<= 32;
+ n += low;
+ return n;
+}
+
+/*!
+ \brief Finds next unused filename that matches padded int format identifier provided
+ \param[in] fn_template filename template consisting of a padded int format identifier (eg screenshot%03d)
+ \param[in] max maximum number to search for available name
+ \return "" on failure, string next available name matching format identifier on success
+*/
+
+std::string CUtil::GetNextFilename(const std::string &fn_template, int max)
+{
+ std::string searchPath = URIUtils::GetDirectory(fn_template);
+ std::string mask = URIUtils::GetExtension(fn_template);
+ std::string name = StringUtils::Format(fn_template, 0);
+
+ CFileItemList items;
+ if (!CDirectory::GetDirectory(searchPath, items, mask, DIR_FLAG_NO_FILE_DIRS))
+ return name;
+
+ items.SetFastLookup(true);
+ for (int i = 0; i <= max; i++)
+ {
+ std::string name = StringUtils::Format(fn_template, i);
+ if (!items.Get(name))
+ return name;
+ }
+ return "";
+}
+
+std::string CUtil::GetNextPathname(const std::string &path_template, int max)
+{
+ if (path_template.find("%04d") == std::string::npos)
+ return "";
+
+ for (int i = 0; i <= max; i++)
+ {
+ std::string name = StringUtils::Format(path_template, i);
+ if (!CFile::Exists(name) && !CDirectory::Exists(name))
+ return name;
+ }
+ return "";
+}
+
+void CUtil::StatToStatI64(struct _stati64 *result, struct stat *stat)
+{
+ result->st_dev = stat->st_dev;
+ result->st_ino = stat->st_ino;
+ result->st_mode = stat->st_mode;
+ result->st_nlink = stat->st_nlink;
+ result->st_uid = stat->st_uid;
+ result->st_gid = stat->st_gid;
+ result->st_rdev = stat->st_rdev;
+ result->st_size = (int64_t)stat->st_size;
+
+#ifndef TARGET_POSIX
+ result->st_atime = (long)(stat->st_atime & 0xFFFFFFFF);
+ result->st_mtime = (long)(stat->st_mtime & 0xFFFFFFFF);
+ result->st_ctime = (long)(stat->st_ctime & 0xFFFFFFFF);
+#else
+ result->_st_atime = (long)(stat->st_atime & 0xFFFFFFFF);
+ result->_st_mtime = (long)(stat->st_mtime & 0xFFFFFFFF);
+ result->_st_ctime = (long)(stat->st_ctime & 0xFFFFFFFF);
+#endif
+}
+
+void CUtil::Stat64ToStatI64(struct _stati64 *result, struct __stat64 *stat)
+{
+ result->st_dev = stat->st_dev;
+ result->st_ino = stat->st_ino;
+ result->st_mode = stat->st_mode;
+ result->st_nlink = stat->st_nlink;
+ result->st_uid = stat->st_uid;
+ result->st_gid = stat->st_gid;
+ result->st_rdev = stat->st_rdev;
+ result->st_size = stat->st_size;
+#ifndef TARGET_POSIX
+ result->st_atime = (long)(stat->st_atime & 0xFFFFFFFF);
+ result->st_mtime = (long)(stat->st_mtime & 0xFFFFFFFF);
+ result->st_ctime = (long)(stat->st_ctime & 0xFFFFFFFF);
+#else
+ result->_st_atime = (long)(stat->st_atime & 0xFFFFFFFF);
+ result->_st_mtime = (long)(stat->st_mtime & 0xFFFFFFFF);
+ result->_st_ctime = (long)(stat->st_ctime & 0xFFFFFFFF);
+#endif
+}
+
+void CUtil::StatI64ToStat64(struct __stat64 *result, struct _stati64 *stat)
+{
+ result->st_dev = stat->st_dev;
+ result->st_ino = stat->st_ino;
+ result->st_mode = stat->st_mode;
+ result->st_nlink = stat->st_nlink;
+ result->st_uid = stat->st_uid;
+ result->st_gid = stat->st_gid;
+ result->st_rdev = stat->st_rdev;
+ result->st_size = stat->st_size;
+#ifndef TARGET_POSIX
+ result->st_atime = stat->st_atime;
+ result->st_mtime = stat->st_mtime;
+ result->st_ctime = stat->st_ctime;
+#else
+ result->st_atime = stat->_st_atime;
+ result->st_mtime = stat->_st_mtime;
+ result->st_ctime = stat->_st_ctime;
+#endif
+}
+
+void CUtil::StatToStat64(struct __stat64 *result, const struct stat *stat)
+{
+ memset(result, 0, sizeof(*result));
+ result->st_dev = stat->st_dev;
+ result->st_ino = stat->st_ino;
+ result->st_mode = stat->st_mode;
+ result->st_nlink = stat->st_nlink;
+ result->st_uid = stat->st_uid;
+ result->st_gid = stat->st_gid;
+ result->st_rdev = stat->st_rdev;
+ result->st_size = stat->st_size;
+ result->st_atime = stat->st_atime;
+ result->st_mtime = stat->st_mtime;
+ result->st_ctime = stat->st_ctime;
+}
+
+void CUtil::Stat64ToStat(struct stat *result, struct __stat64 *stat)
+{
+ result->st_dev = stat->st_dev;
+ result->st_ino = stat->st_ino;
+ result->st_mode = stat->st_mode;
+ result->st_nlink = stat->st_nlink;
+ result->st_uid = stat->st_uid;
+ result->st_gid = stat->st_gid;
+ result->st_rdev = stat->st_rdev;
+#ifndef TARGET_POSIX
+ if (stat->st_size <= LONG_MAX)
+ result->st_size = (_off_t)stat->st_size;
+#else
+ if (sizeof(stat->st_size) <= sizeof(result->st_size) )
+ result->st_size = stat->st_size;
+#endif
+ else
+ {
+ result->st_size = 0;
+ CLog::Log(LOGWARNING, "WARNING: File is larger than 32bit stat can handle, file size will be reported as 0 bytes");
+ }
+ result->st_atime = (time_t)(stat->st_atime & 0xFFFFFFFF);
+ result->st_mtime = (time_t)(stat->st_mtime & 0xFFFFFFFF);
+ result->st_ctime = (time_t)(stat->st_ctime & 0xFFFFFFFF);
+}
+
+#ifdef TARGET_WINDOWS
+void CUtil::Stat64ToStat64i32(struct _stat64i32 *result, struct __stat64 *stat)
+{
+ result->st_dev = stat->st_dev;
+ result->st_ino = stat->st_ino;
+ result->st_mode = stat->st_mode;
+ result->st_nlink = stat->st_nlink;
+ result->st_uid = stat->st_uid;
+ result->st_gid = stat->st_gid;
+ result->st_rdev = stat->st_rdev;
+#ifndef TARGET_POSIX
+ if (stat->st_size <= LONG_MAX)
+ result->st_size = (_off_t)stat->st_size;
+#else
+ if (sizeof(stat->st_size) <= sizeof(result->st_size) )
+ result->st_size = stat->st_size;
+#endif
+ else
+ {
+ result->st_size = 0;
+ CLog::Log(LOGWARNING, "WARNING: File is larger than 32bit stat can handle, file size will be reported as 0 bytes");
+ }
+#ifndef TARGET_POSIX
+ result->st_atime = stat->st_atime;
+ result->st_mtime = stat->st_mtime;
+ result->st_ctime = stat->st_ctime;
+#else
+ result->st_atime = stat->_st_atime;
+ result->st_mtime = stat->_st_mtime;
+ result->st_ctime = stat->_st_ctime;
+#endif
+}
+#endif
+
+bool CUtil::CreateDirectoryEx(const std::string& strPath)
+{
+ // Function to create all directories at once instead
+ // of calling CreateDirectory for every subdir.
+ // Creates the directory and subdirectories if needed.
+
+ // return true if directory already exist
+ if (CDirectory::Exists(strPath)) return true;
+
+ // we currently only allow HD and smb and nfs paths
+ if (!URIUtils::IsHD(strPath) && !URIUtils::IsSmb(strPath) && !URIUtils::IsNfs(strPath))
+ {
+ CLog::Log(LOGERROR, "{} called with an unsupported path: {}", __FUNCTION__, strPath);
+ return false;
+ }
+
+ std::vector<std::string> dirs = URIUtils::SplitPath(strPath);
+ if (dirs.empty())
+ return false;
+ std::string dir(dirs.front());
+ URIUtils::AddSlashAtEnd(dir);
+ for (std::vector<std::string>::const_iterator it = dirs.begin() + 1; it != dirs.end(); ++it)
+ {
+ dir = URIUtils::AddFileToFolder(dir, *it);
+ CDirectory::Create(dir);
+ }
+
+ // was the final destination directory successfully created ?
+ return CDirectory::Exists(strPath);
+}
+
+std::string CUtil::MakeLegalFileName(const std::string &strFile, int LegalType)
+{
+ std::string result = strFile;
+
+ StringUtils::Replace(result, '/', '_');
+ StringUtils::Replace(result, '\\', '_');
+ StringUtils::Replace(result, '?', '_');
+
+ if (LegalType == LEGAL_WIN32_COMPAT)
+ {
+ // just filter out some illegal characters on windows
+ StringUtils::Replace(result, ':', '_');
+ StringUtils::Replace(result, '*', '_');
+ StringUtils::Replace(result, '?', '_');
+ StringUtils::Replace(result, '\"', '_');
+ StringUtils::Replace(result, '<', '_');
+ StringUtils::Replace(result, '>', '_');
+ StringUtils::Replace(result, '|', '_');
+ StringUtils::TrimRight(result, ". ");
+ }
+ return result;
+}
+
+// legalize entire path
+std::string CUtil::MakeLegalPath(const std::string &strPathAndFile, int LegalType)
+{
+ if (URIUtils::IsStack(strPathAndFile))
+ return MakeLegalPath(CStackDirectory::GetFirstStackedFile(strPathAndFile));
+ if (URIUtils::IsMultiPath(strPathAndFile))
+ return MakeLegalPath(CMultiPathDirectory::GetFirstPath(strPathAndFile));
+ if (!URIUtils::IsHD(strPathAndFile) && !URIUtils::IsSmb(strPathAndFile) && !URIUtils::IsNfs(strPathAndFile))
+ return strPathAndFile; // we don't support writing anywhere except HD, SMB and NFS - no need to legalize path
+
+ bool trailingSlash = URIUtils::HasSlashAtEnd(strPathAndFile);
+ std::vector<std::string> dirs = URIUtils::SplitPath(strPathAndFile);
+ if (dirs.empty())
+ return strPathAndFile;
+ // we just add first token to path and don't legalize it - possible values:
+ // "X:" (local win32), "" (local unix - empty string before '/') or
+ // "protocol://domain"
+ std::string dir(dirs.front());
+ URIUtils::AddSlashAtEnd(dir);
+ for (std::vector<std::string>::const_iterator it = dirs.begin() + 1; it != dirs.end(); ++it)
+ dir = URIUtils::AddFileToFolder(dir, MakeLegalFileName(*it, LegalType));
+ if (trailingSlash) URIUtils::AddSlashAtEnd(dir);
+ return dir;
+}
+
+std::string CUtil::ValidatePath(const std::string &path, bool bFixDoubleSlashes /* = false */)
+{
+ std::string result = path;
+
+ // Don't do any stuff on URLs containing %-characters or protocols that embed
+ // filenames. NOTE: Don't use IsInZip or IsInRar here since it will infinitely
+ // recurse and crash XBMC
+ if (URIUtils::IsURL(path) &&
+ (path.find('%') != std::string::npos ||
+ StringUtils::StartsWithNoCase(path, "apk:") ||
+ StringUtils::StartsWithNoCase(path, "zip:") ||
+ StringUtils::StartsWithNoCase(path, "rar:") ||
+ StringUtils::StartsWithNoCase(path, "stack:") ||
+ StringUtils::StartsWithNoCase(path, "bluray:") ||
+ StringUtils::StartsWithNoCase(path, "multipath:") ))
+ return result;
+
+ // check the path for incorrect slashes
+#ifdef TARGET_WINDOWS
+ if (URIUtils::IsDOSPath(path))
+ {
+ StringUtils::Replace(result, '/', '\\');
+ /* The double slash correction should only be used when *absolutely*
+ necessary! This applies to certain DLLs or use from Python DLLs/scripts
+ that incorrectly generate double (back) slashes.
+ */
+ if (bFixDoubleSlashes && !result.empty())
+ {
+ // Fixup for double back slashes (but ignore the \\ of unc-paths)
+ for (size_t x = 1; x < result.size() - 1; x++)
+ {
+ if (result[x] == '\\' && result[x+1] == '\\')
+ result.erase(x, 1);
+ }
+ }
+ }
+ else if (path.find("://") != std::string::npos || path.find(":\\\\") != std::string::npos)
+#endif
+ {
+ StringUtils::Replace(result, '\\', '/');
+ /* The double slash correction should only be used when *absolutely*
+ necessary! This applies to certain DLLs or use from Python DLLs/scripts
+ that incorrectly generate double (back) slashes.
+ */
+ if (bFixDoubleSlashes && !result.empty())
+ {
+ // Fixup for double forward slashes(/) but don't touch the :// of URLs
+ for (size_t x = 2; x < result.size() - 1; x++)
+ {
+ if ( result[x] == '/' && result[x + 1] == '/' && !(result[x - 1] == ':' || (result[x - 1] == '/' && result[x - 2] == ':')) )
+ result.erase(x, 1);
+ }
+ }
+ }
+ return result;
+}
+
+void CUtil::SplitParams(const std::string &paramString, std::vector<std::string> &parameters)
+{
+ bool inQuotes = false;
+ bool lastEscaped = false; // only every second character can be escaped
+ int inFunction = 0;
+ size_t whiteSpacePos = 0;
+ std::string parameter;
+ parameters.clear();
+ for (size_t pos = 0; pos < paramString.size(); pos++)
+ {
+ char ch = paramString[pos];
+ bool escaped = (pos > 0 && paramString[pos - 1] == '\\' && !lastEscaped);
+ lastEscaped = escaped;
+ if (inQuotes)
+ { // if we're in a quote, we accept everything until the closing quote
+ if (ch == '"' && !escaped)
+ { // finished a quote - no need to add the end quote to our string
+ inQuotes = false;
+ }
+ }
+ else
+ { // not in a quote, so check if we should be starting one
+ if (ch == '"' && !escaped)
+ { // start of quote - no need to add the quote to our string
+ inQuotes = true;
+ }
+ if (inFunction && ch == ')')
+ { // end of a function
+ inFunction--;
+ }
+ if (ch == '(')
+ { // start of function
+ inFunction++;
+ }
+ if (!inFunction && ch == ',')
+ { // not in a function, so a comma signifies the end of this parameter
+ if (whiteSpacePos)
+ parameter = parameter.substr(0, whiteSpacePos);
+ // trim off start and end quotes
+ if (parameter.length() > 1 && parameter[0] == '"' && parameter[parameter.length() - 1] == '"')
+ parameter = parameter.substr(1, parameter.length() - 2);
+ else if (parameter.length() > 3 && parameter[parameter.length() - 1] == '"')
+ {
+ // check name="value" style param.
+ size_t quotaPos = parameter.find('"');
+ if (quotaPos > 1 && quotaPos < parameter.length() - 1 && parameter[quotaPos - 1] == '=')
+ {
+ parameter.erase(parameter.length() - 1);
+ parameter.erase(quotaPos);
+ }
+ }
+ parameters.push_back(parameter);
+ parameter.clear();
+ whiteSpacePos = 0;
+ continue;
+ }
+ }
+ if ((ch == '"' || ch == '\\') && escaped)
+ { // escaped quote or backslash
+ parameter[parameter.size()-1] = ch;
+ continue;
+ }
+ // whitespace handling - we skip any whitespace at the left or right of an unquoted parameter
+ if (ch == ' ' && !inQuotes)
+ {
+ if (parameter.empty()) // skip whitespace on left
+ continue;
+ if (!whiteSpacePos) // make a note of where whitespace starts on the right
+ whiteSpacePos = parameter.size();
+ }
+ else
+ whiteSpacePos = 0;
+ parameter += ch;
+ }
+ if (inFunction || inQuotes)
+ CLog::Log(LOGWARNING, "{}({}) - end of string while searching for ) or \"", __FUNCTION__,
+ paramString);
+ if (whiteSpacePos)
+ parameter.erase(whiteSpacePos);
+ // trim off start and end quotes
+ if (parameter.size() > 1 && parameter[0] == '"' && parameter[parameter.size() - 1] == '"')
+ parameter = parameter.substr(1,parameter.size() - 2);
+ else if (parameter.size() > 3 && parameter[parameter.size() - 1] == '"')
+ {
+ // check name="value" style param.
+ size_t quotaPos = parameter.find('"');
+ if (quotaPos > 1 && quotaPos < parameter.length() - 1 && parameter[quotaPos - 1] == '=')
+ {
+ parameter.erase(parameter.length() - 1);
+ parameter.erase(quotaPos);
+ }
+ }
+ if (!parameter.empty() || parameters.size())
+ parameters.push_back(parameter);
+}
+
+int CUtil::GetMatchingSource(const std::string& strPath1, VECSOURCES& VECSOURCES, bool& bIsSourceName)
+{
+ if (strPath1.empty())
+ return -1;
+
+ // copy as we may change strPath
+ std::string strPath = strPath1;
+
+ // Check for special protocols
+ CURL checkURL(strPath);
+
+ if (StringUtils::StartsWith(strPath, "special://skin/"))
+ return 1;
+
+ // do not return early if URL protocol is "plugin"
+ // since video- and/or audio-plugins can be configured as mediasource
+
+ // stack://
+ if (checkURL.IsProtocol("stack"))
+ strPath.erase(0, 8); // remove the stack protocol
+
+ if (checkURL.IsProtocol("shout"))
+ strPath = checkURL.GetHostName();
+
+ if (checkURL.IsProtocol("multipath"))
+ strPath = CMultiPathDirectory::GetFirstPath(strPath);
+
+ bIsSourceName = false;
+ int iIndex = -1;
+
+ // we first test the NAME of a source
+ for (int i = 0; i < (int)VECSOURCES.size(); ++i)
+ {
+ const CMediaSource &share = VECSOURCES[i];
+ std::string strName = share.strName;
+
+ // special cases for dvds
+ if (URIUtils::IsOnDVD(share.strPath))
+ {
+ if (URIUtils::IsOnDVD(strPath))
+ return i;
+
+ // not a path, so we need to modify the source name
+ // since we add the drive status and disc name to the source
+ // "Name (Drive Status/Disc Name)"
+ size_t iPos = strName.rfind('(');
+ if (iPos != std::string::npos && iPos > 1)
+ strName = strName.substr(0, iPos - 1);
+ }
+ if (StringUtils::EqualsNoCase(strPath, strName))
+ {
+ bIsSourceName = true;
+ return i;
+ }
+ }
+
+ // now test the paths
+
+ // remove user details, and ensure path only uses forward slashes
+ // and ends with a trailing slash so as not to match a substring
+ CURL urlDest(strPath);
+ urlDest.SetOptions("");
+ urlDest.SetProtocolOptions("");
+ std::string strDest = urlDest.GetWithoutUserDetails();
+ ForceForwardSlashes(strDest);
+ if (!URIUtils::HasSlashAtEnd(strDest))
+ strDest += "/";
+
+ size_t iLength = 0;
+ size_t iLenPath = strDest.size();
+ for (int i = 0; i < (int)VECSOURCES.size(); ++i)
+ {
+ const CMediaSource &share = VECSOURCES[i];
+
+ // does it match a source name?
+ if (share.strPath.substr(0,8) == "shout://")
+ {
+ CURL url(share.strPath);
+ if (strPath == url.GetHostName())
+ return i;
+ }
+
+ // doesn't match a name, so try the source path
+ std::vector<std::string> vecPaths;
+
+ // add any concatenated paths if they exist
+ if (!share.vecPaths.empty())
+ vecPaths = share.vecPaths;
+
+ // add the actual share path at the front of the vector
+ vecPaths.insert(vecPaths.begin(), share.strPath);
+
+ // test each path
+ for (const auto &path : vecPaths)
+ {
+ // remove user details, and ensure path only uses forward slashes
+ // and ends with a trailing slash so as not to match a substring
+ CURL urlShare(path);
+ urlShare.SetOptions("");
+ urlShare.SetProtocolOptions("");
+ std::string strShare = urlShare.GetWithoutUserDetails();
+ ForceForwardSlashes(strShare);
+ if (!URIUtils::HasSlashAtEnd(strShare))
+ strShare += "/";
+ size_t iLenShare = strShare.size();
+
+ if ((iLenPath >= iLenShare) && StringUtils::StartsWithNoCase(strDest, strShare) && (iLenShare > iLength))
+ {
+ // if exact match, return it immediately
+ if (iLenPath == iLenShare)
+ {
+ // if the path EXACTLY matches an item in a concatenated path
+ // set source name to true to load the full virtualpath
+ bIsSourceName = false;
+ if (vecPaths.size() > 1)
+ bIsSourceName = true;
+ return i;
+ }
+ iIndex = i;
+ iLength = iLenShare;
+ }
+ }
+ }
+
+ // return the index of the share with the longest match
+ if (iIndex == -1)
+ {
+
+ // rar:// and zip://
+ // if archive wasn't mounted, look for a matching share for the archive instead
+ if( StringUtils::StartsWithNoCase(strPath, "rar://") || StringUtils::StartsWithNoCase(strPath, "zip://") )
+ {
+ // get the hostname portion of the url since it contains the archive file
+ strPath = checkURL.GetHostName();
+
+ bIsSourceName = false;
+ bool bDummy;
+ return GetMatchingSource(strPath, VECSOURCES, bDummy);
+ }
+
+ CLog::Log(LOGDEBUG, "CUtil::GetMatchingSource: no matching source found for [{}]", strPath1);
+ }
+ return iIndex;
+}
+
+std::string CUtil::TranslateSpecialSource(const std::string &strSpecial)
+{
+ if (!strSpecial.empty() && strSpecial[0] == '$')
+ {
+ if (StringUtils::StartsWithNoCase(strSpecial, "$home"))
+ return URIUtils::AddFileToFolder("special://home/", strSpecial.substr(5));
+ else if (StringUtils::StartsWithNoCase(strSpecial, "$subtitles"))
+ return URIUtils::AddFileToFolder("special://subtitles/", strSpecial.substr(10));
+ else if (StringUtils::StartsWithNoCase(strSpecial, "$userdata"))
+ return URIUtils::AddFileToFolder("special://userdata/", strSpecial.substr(9));
+ else if (StringUtils::StartsWithNoCase(strSpecial, "$database"))
+ return URIUtils::AddFileToFolder("special://database/", strSpecial.substr(9));
+ else if (StringUtils::StartsWithNoCase(strSpecial, "$thumbnails"))
+ return URIUtils::AddFileToFolder("special://thumbnails/", strSpecial.substr(11));
+ else if (StringUtils::StartsWithNoCase(strSpecial, "$recordings"))
+ return URIUtils::AddFileToFolder("special://recordings/", strSpecial.substr(11));
+ else if (StringUtils::StartsWithNoCase(strSpecial, "$screenshots"))
+ return URIUtils::AddFileToFolder("special://screenshots/", strSpecial.substr(12));
+ else if (StringUtils::StartsWithNoCase(strSpecial, "$musicplaylists"))
+ return URIUtils::AddFileToFolder("special://musicplaylists/", strSpecial.substr(15));
+ else if (StringUtils::StartsWithNoCase(strSpecial, "$videoplaylists"))
+ return URIUtils::AddFileToFolder("special://videoplaylists/", strSpecial.substr(15));
+ else if (StringUtils::StartsWithNoCase(strSpecial, "$cdrips"))
+ return URIUtils::AddFileToFolder("special://cdrips/", strSpecial.substr(7));
+ // this one will be removed post 2.0
+ else if (StringUtils::StartsWithNoCase(strSpecial, "$playlists"))
+ return URIUtils::AddFileToFolder(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH), strSpecial.substr(10));
+ }
+ return strSpecial;
+}
+
+std::string CUtil::MusicPlaylistsLocation()
+{
+ const std::string path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH);
+ std::vector<std::string> vec;
+ vec.push_back(URIUtils::AddFileToFolder(path, "music"));
+ vec.push_back(URIUtils::AddFileToFolder(path, "mixed"));
+ return XFILE::CMultiPathDirectory::ConstructMultiPath(vec);
+}
+
+std::string CUtil::VideoPlaylistsLocation()
+{
+ const std::string path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH);
+ std::vector<std::string> vec;
+ vec.push_back(URIUtils::AddFileToFolder(path, "video"));
+ vec.push_back(URIUtils::AddFileToFolder(path, "mixed"));
+ return XFILE::CMultiPathDirectory::ConstructMultiPath(vec);
+}
+
+void CUtil::DeleteMusicDatabaseDirectoryCache()
+{
+ CUtil::DeleteDirectoryCache("mdb-");
+ CUtil::DeleteDirectoryCache("sp-"); // overkill as it will delete video smartplaylists, but as we can't differentiate based on URL...
+}
+
+void CUtil::DeleteVideoDatabaseDirectoryCache()
+{
+ CUtil::DeleteDirectoryCache("vdb-");
+ CUtil::DeleteDirectoryCache("sp-"); // overkill as it will delete music smartplaylists, but as we can't differentiate based on URL...
+}
+
+void CUtil::DeleteDirectoryCache(const std::string &prefix)
+{
+ std::string searchPath = "special://temp/";
+ CFileItemList items;
+ if (!XFILE::CDirectory::GetDirectory(searchPath, items, ".fi", DIR_FLAG_NO_FILE_DIRS))
+ return;
+
+ for (const auto &item : items)
+ {
+ if (item->m_bIsFolder)
+ continue;
+ std::string fileName = URIUtils::GetFileName(item->GetPath());
+ if (StringUtils::StartsWith(fileName, prefix))
+ XFILE::CFile::Delete(item->GetPath());
+ }
+}
+
+
+void CUtil::GetRecursiveListing(const std::string& strPath, CFileItemList& items, const std::string& strMask, unsigned int flags /* = DIR_FLAG_DEFAULTS */)
+{
+ CFileItemList myItems;
+ CDirectory::GetDirectory(strPath,myItems,strMask,flags);
+ for (const auto &item : myItems)
+ {
+ if (item->m_bIsFolder)
+ CUtil::GetRecursiveListing(item->GetPath(),items,strMask,flags);
+ else
+ items.Add(item);
+ }
+}
+
+void CUtil::GetRecursiveDirsListing(const std::string& strPath, CFileItemList& item, unsigned int flags /* = DIR_FLAG_DEFAULTS */)
+{
+ CFileItemList myItems;
+ CDirectory::GetDirectory(strPath,myItems,"",flags);
+ for (const auto &i : myItems)
+ {
+ if (i->m_bIsFolder && !i->IsPath(".."))
+ {
+ item.Add(i);
+ CUtil::GetRecursiveDirsListing(i->GetPath(),item,flags);
+ }
+ }
+}
+
+void CUtil::ForceForwardSlashes(std::string& strPath)
+{
+ size_t iPos = strPath.rfind('\\');
+ while (iPos != std::string::npos)
+ {
+ strPath.at(iPos) = '/';
+ iPos = strPath.rfind('\\');
+ }
+}
+
+double CUtil::AlbumRelevance(const std::string& strAlbumTemp1, const std::string& strAlbum1, const std::string& strArtistTemp1, const std::string& strArtist1)
+{
+ // case-insensitive fuzzy string comparison on the album and artist for relevance
+ // weighting is identical, both album and artist are 50% of the total relevance
+ // a missing artist means the maximum relevance can only be 0.50
+ std::string strAlbumTemp = strAlbumTemp1;
+ StringUtils::ToLower(strAlbumTemp);
+ std::string strAlbum = strAlbum1;
+ StringUtils::ToLower(strAlbum);
+ double fAlbumPercentage = fstrcmp(strAlbumTemp.c_str(), strAlbum.c_str());
+ double fArtistPercentage = 0.0;
+ if (!strArtist1.empty())
+ {
+ std::string strArtistTemp = strArtistTemp1;
+ StringUtils::ToLower(strArtistTemp);
+ std::string strArtist = strArtist1;
+ StringUtils::ToLower(strArtist);
+ fArtistPercentage = fstrcmp(strArtistTemp.c_str(), strArtist.c_str());
+ }
+ double fRelevance = fAlbumPercentage * 0.5 + fArtistPercentage * 0.5;
+ return fRelevance;
+}
+
+bool CUtil::MakeShortenPath(std::string StrInput, std::string& StrOutput, size_t iTextMaxLength)
+{
+ size_t iStrInputSize = StrInput.size();
+ if(iStrInputSize <= 0 || iTextMaxLength >= iStrInputSize)
+ {
+ StrOutput = StrInput;
+ return true;
+ }
+
+ char cDelim = '\0';
+ size_t nGreaterDelim, nPos;
+
+ nPos = StrInput.find_last_of( '\\' );
+ if (nPos != std::string::npos)
+ cDelim = '\\';
+ else
+ {
+ nPos = StrInput.find_last_of( '/' );
+ if (nPos != std::string::npos)
+ cDelim = '/';
+ }
+ if ( cDelim == '\0' )
+ return false;
+
+ if (nPos == StrInput.size() - 1)
+ {
+ StrInput.erase(StrInput.size() - 1);
+ nPos = StrInput.find_last_of(cDelim);
+ }
+ while( iTextMaxLength < iStrInputSize )
+ {
+ nPos = StrInput.find_last_of( cDelim, nPos );
+ nGreaterDelim = nPos;
+
+ if (nPos == std::string::npos || nPos == 0)
+ break;
+
+ nPos = StrInput.find_last_of( cDelim, nPos - 1 );
+
+ if ( nPos == std::string::npos )
+ break;
+ if ( nGreaterDelim > nPos )
+ StrInput.replace( nPos + 1, nGreaterDelim - nPos - 1, ".." );
+ iStrInputSize = StrInput.size();
+ }
+ // replace any additional /../../ with just /../ if necessary
+ std::string replaceDots = StringUtils::Format("..{}..", cDelim);
+ while (StrInput.size() > (unsigned int)iTextMaxLength)
+ if (!StringUtils::Replace(StrInput, replaceDots, ".."))
+ break;
+ // finally, truncate our string to force inside our max text length,
+ // replacing the last 2 characters with ".."
+
+ // eg end up with:
+ // "smb://../Playboy Swimsuit Cal.."
+ if (iTextMaxLength > 2 && StrInput.size() > (unsigned int)iTextMaxLength)
+ {
+ StrInput.erase(iTextMaxLength - 2);
+ StrInput += "..";
+ }
+ StrOutput = StrInput;
+ return true;
+}
+
+bool CUtil::SupportsWriteFileOperations(const std::string& strPath)
+{
+ // currently only hd, smb, nfs and dav support delete and rename
+ if (URIUtils::IsHD(strPath))
+ return true;
+ if (URIUtils::IsSmb(strPath))
+ return true;
+ if (URIUtils::IsPVRRecording(strPath))
+ return CPVRDirectory::SupportsWriteFileOperations(strPath);
+ if (URIUtils::IsNfs(strPath))
+ return true;
+ if (URIUtils::IsDAV(strPath))
+ return true;
+ if (URIUtils::IsStack(strPath))
+ return SupportsWriteFileOperations(CStackDirectory::GetFirstStackedFile(strPath));
+ if (URIUtils::IsMultiPath(strPath))
+ return CMultiPathDirectory::SupportsWriteFileOperations(strPath);
+
+ if (CServiceBroker::IsAddonInterfaceUp())
+ {
+ CURL url(strPath);
+ for (const auto& addon : CServiceBroker::GetVFSAddonCache().GetAddonInstances())
+ {
+ const auto& info = addon->GetProtocolInfo();
+ auto prots = StringUtils::Split(info.type, "|");
+ if (info.supportWrite &&
+ std::find(prots.begin(), prots.end(), url.GetProtocol()) != prots.end())
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CUtil::SupportsReadFileOperations(const std::string& strPath)
+{
+ return !URIUtils::IsVideoDb(strPath);
+}
+
+std::string CUtil::GetDefaultFolderThumb(const std::string &folderThumb)
+{
+ if (CServiceBroker::GetGUI()->GetTextureManager().HasTexture(folderThumb))
+ return folderThumb;
+ return "";
+}
+
+void CUtil::GetSkinThemes(std::vector<std::string>& vecTheme)
+{
+ static const std::string TexturesXbt = "Textures.xbt";
+
+ std::string strPath = URIUtils::AddFileToFolder(CServiceBroker::GetWinSystem()->GetGfxContext().GetMediaDir(), "media");
+ CFileItemList items;
+ CDirectory::GetDirectory(strPath, items, "", DIR_FLAG_DEFAULTS);
+ // Search for Themes in the Current skin!
+ for (const auto &pItem : items)
+ {
+ if (!pItem->m_bIsFolder)
+ {
+ std::string strExtension = URIUtils::GetExtension(pItem->GetPath());
+ std::string strLabel = pItem->GetLabel();
+ if ((strExtension == ".xbt" && !StringUtils::EqualsNoCase(strLabel, TexturesXbt)))
+ vecTheme.push_back(StringUtils::Left(strLabel, strLabel.size() - strExtension.size()));
+ }
+ else
+ {
+ // check if this is an xbt:// VFS path
+ CURL itemUrl(pItem->GetPath());
+ if (!itemUrl.IsProtocol("xbt") || !itemUrl.GetFileName().empty())
+ continue;
+
+ std::string strLabel = URIUtils::GetFileName(itemUrl.GetHostName());
+ if (!StringUtils::EqualsNoCase(strLabel, TexturesXbt))
+ vecTheme.push_back(StringUtils::Left(strLabel, strLabel.size() - URIUtils::GetExtension(strLabel).size()));
+ }
+ }
+ std::sort(vecTheme.begin(), vecTheme.end(), sortstringbyname());
+}
+
+void CUtil::InitRandomSeed()
+{
+ // Init random seed
+ auto now = std::chrono::steady_clock::now();
+ auto seed = now.time_since_epoch();
+
+ srand(static_cast<unsigned int>(seed.count()));
+}
+
+#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, ",");
+
+ // Strip quotes and whitespace around the arguments, or exec will fail.
+ // This allows the python invocation to be written more naturally with any amount of whitespace around the args.
+ // But it's still limited, for example quotes inside the strings are not expanded, etc.
+ //! @todo Maybe some python library routine can parse this more properly ?
+ for (std::vector<std::string>::iterator it = args.begin(); it != args.end(); ++it)
+ {
+ size_t pos;
+ pos = it->find_first_not_of(" \t\n\"'");
+ if (pos != std::string::npos)
+ {
+ it->erase(0, pos);
+ }
+
+ pos = it->find_last_not_of(" \t\n\"'"); // if it returns npos we'll end up with an empty string which is OK
+ {
+ it->erase(++pos, it->size());
+ }
+ }
+
+ return Command(args, waitExit);
+}
+
+bool CUtil::Command(const std::vector<std::string>& arrArgs, bool waitExit)
+{
+#ifdef _DEBUG
+ printf("Executing: ");
+ for (const auto &arg : arrArgs)
+ printf("%s ", arg.c_str());
+ printf("\n");
+#endif
+
+ pid_t child = fork();
+ int n = 0;
+ if (child == 0)
+ {
+ if (!waitExit)
+ {
+ // fork again in order not to leave a zombie process
+ child = fork();
+ if (child == -1)
+ _exit(2);
+ else if (child != 0)
+ _exit(0);
+ }
+ close(0);
+ close(1);
+ close(2);
+ if (!arrArgs.empty())
+ {
+ char **args = (char **)alloca(sizeof(char *) * (arrArgs.size() + 3));
+ memset(args, 0, (sizeof(char *) * (arrArgs.size() + 3)));
+ for (size_t i=0; i<arrArgs.size(); i++)
+ args[i] = const_cast<char *>(arrArgs[i].c_str());
+ execvp(args[0], args);
+ }
+ }
+ else
+ {
+ waitpid(child, &n, 0);
+ }
+
+ return (waitExit) ? (WEXITSTATUS(n) == 0) : true;
+}
+#endif
+
+int CUtil::LookupRomanDigit(char roman_digit)
+{
+ switch (roman_digit)
+ {
+ case 'i':
+ case 'I':
+ return 1;
+ case 'v':
+ case 'V':
+ return 5;
+ case 'x':
+ case 'X':
+ return 10;
+ case 'l':
+ case 'L':
+ return 50;
+ case 'c':
+ case 'C':
+ return 100;
+ case 'd':
+ case 'D':
+ return 500;
+ case 'm':
+ case 'M':
+ return 1000;
+ default:
+ return 0;
+ }
+}
+
+int CUtil::TranslateRomanNumeral(const char* roman_numeral)
+{
+
+ int decimal = -1;
+
+ if (roman_numeral && roman_numeral[0])
+ {
+ int temp_sum = 0,
+ last = 0,
+ repeat = 0,
+ trend = 1;
+ decimal = 0;
+ while (*roman_numeral)
+ {
+ int digit = CUtil::LookupRomanDigit(*roman_numeral);
+ int test = last;
+
+ // General sanity checks
+
+ // numeral not in LUT
+ if (!digit)
+ return -1;
+
+ while (test > 5)
+ test /= 10;
+
+ // N = 10^n may not precede (N+1) > 10^(N+1)
+ if (test == 1 && digit > last * 10)
+ return -1;
+
+ // N = 5*10^n may not precede (N+1) >= N
+ if (test == 5 && digit >= last)
+ return -1;
+
+ // End general sanity checks
+
+ if (last < digit)
+ {
+ // smaller numerals may not repeat before a larger one
+ if (repeat)
+ return -1;
+
+ temp_sum += digit;
+
+ repeat = 0;
+ trend = 0;
+ }
+ else if (last == digit)
+ {
+ temp_sum += digit;
+ repeat++;
+ trend = 1;
+ }
+ else
+ {
+ if (!repeat)
+ decimal += 2 * last - temp_sum;
+ else
+ decimal += temp_sum;
+
+ temp_sum = digit;
+
+ trend = 1;
+ repeat = 0;
+ }
+ // Post general sanity checks
+
+ // numerals may not repeat more than thrice
+ if (repeat == 3)
+ return -1;
+
+ last = digit;
+ roman_numeral++;
+ }
+
+ if (trend)
+ decimal += temp_sum;
+ else
+ decimal += 2 * last - temp_sum;
+ }
+ return decimal;
+}
+
+std::string CUtil::ResolveExecutablePath()
+{
+ std::string strExecutablePath;
+#ifdef TARGET_WINDOWS
+ static const size_t bufSize = MAX_PATH * 2;
+ wchar_t* buf = new wchar_t[bufSize];
+ buf[0] = 0;
+ ::GetModuleFileNameW(0, buf, bufSize);
+ buf[bufSize-1] = 0;
+ g_charsetConverter.wToUTF8(buf,strExecutablePath);
+ delete[] buf;
+#elif defined(TARGET_DARWIN)
+ char given_path[2*MAXPATHLEN];
+ size_t path_size =2*MAXPATHLEN;
+
+ CDarwinUtils::GetExecutablePath(given_path, &path_size);
+ strExecutablePath = given_path;
+#elif defined(TARGET_FREEBSD)
+ char buf[PATH_MAX];
+ size_t buflen;
+ int mib[4];
+
+ mib[0] = CTL_KERN;
+ mib[1] = KERN_PROC;
+ mib[2] = KERN_PROC_PATHNAME;
+ mib[3] = getpid();
+
+ buflen = sizeof(buf) - 1;
+ if(sysctl(mib, 4, buf, &buflen, NULL, 0) < 0)
+ strExecutablePath = "";
+ else
+ strExecutablePath = buf;
+#elif defined(TARGET_ANDROID)
+ strExecutablePath = CXBMCApp::getApplicationInfo().nativeLibraryDir;
+
+ std::string appName = CCompileInfo::GetAppName();
+ std::string libName = "lib" + appName + ".so";
+ StringUtils::ToLower(libName);
+ strExecutablePath += "/" + libName;
+#else
+ /* Get our PID and build the name of the link in /proc */
+ pid_t pid = getpid();
+ char linkname[64]; /* /proc/<pid>/exe */
+ snprintf(linkname, sizeof(linkname), "/proc/%i/exe", pid);
+
+ /* Now read the symbolic link */
+ char buf[PATH_MAX + 1];
+ buf[0] = 0;
+
+ int ret = readlink(linkname, buf, sizeof(buf) - 1);
+ if (ret != -1)
+ buf[ret] = 0;
+
+ strExecutablePath = buf;
+#endif
+ return strExecutablePath;
+}
+
+std::string CUtil::GetFrameworksPath(bool forPython)
+{
+ std::string strFrameworksPath;
+#if defined(TARGET_DARWIN)
+ strFrameworksPath = CDarwinUtils::GetFrameworkPath(forPython);
+#endif
+ return strFrameworksPath;
+}
+
+void CUtil::GetVideoBasePathAndFileName(const std::string& videoPath, std::string& basePath, std::string& videoFileName)
+{
+ CFileItem item(videoPath, false);
+ videoFileName = URIUtils::ReplaceExtension(URIUtils::GetFileName(videoPath), "");
+
+ if (item.HasVideoInfoTag())
+ basePath = item.GetVideoInfoTag()->m_basePath;
+
+ if (basePath.empty() && item.IsOpticalMediaFile())
+ basePath = item.GetLocalMetadataPath();
+
+ CURL url(videoPath);
+ if (basePath.empty() && url.IsProtocol("bluray"))
+ {
+ basePath = url.GetHostName();
+ videoFileName = URIUtils::ReplaceExtension(GetTitleFromPath(url.GetHostName()), "");
+
+ url = CURL(url.GetHostName());
+ if (url.IsProtocol("udf"))
+ basePath = URIUtils::GetParentPath(url.GetHostName());
+ }
+
+ if (basePath.empty())
+ basePath = URIUtils::GetBasePath(videoPath);
+}
+
+void CUtil::GetItemsToScan(const std::string& videoPath,
+ const std::string& item_exts,
+ const std::vector<std::string>& sub_dirs,
+ CFileItemList& items)
+{
+ int flags = DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO;
+
+ if (!videoPath.empty())
+ CDirectory::GetDirectory(videoPath, items, item_exts, flags);
+
+ std::vector<std::string> additionalPaths;
+ for (const auto &item : items)
+ {
+ for (const auto& subdir : sub_dirs)
+ {
+ if (StringUtils::EqualsNoCase(item->GetLabel(), subdir))
+ additionalPaths.push_back(item->GetPath());
+ }
+ }
+
+ for (std::vector<std::string>::const_iterator it = additionalPaths.begin(); it != additionalPaths.end(); ++it)
+ {
+ CFileItemList moreItems;
+ CDirectory::GetDirectory(*it, moreItems, item_exts, flags);
+ items.Append(moreItems);
+ }
+}
+
+
+void CUtil::ScanPathsForAssociatedItems(const std::string& videoName,
+ const CFileItemList& items,
+ const std::vector<std::string>& item_exts,
+ std::vector<std::string>& associatedFiles)
+{
+ for (const auto &pItem : items)
+ {
+ if (pItem->m_bIsFolder)
+ continue;
+
+ std::string strCandidate = URIUtils::GetFileName(pItem->GetPath());
+
+ // skip duplicates
+ if (std::find(associatedFiles.begin(), associatedFiles.end(), pItem->GetPath()) != associatedFiles.end())
+ continue;
+
+ URIUtils::RemoveExtension(strCandidate);
+ // NOTE: We don't know if one of videoName or strCandidate is URL-encoded and the other is not, so try both
+ if (StringUtils::StartsWithNoCase(strCandidate, videoName) || (StringUtils::StartsWithNoCase(strCandidate, CURL::Decode(videoName))))
+ {
+ if (URIUtils::IsRAR(pItem->GetPath()) || URIUtils::IsZIP(pItem->GetPath()))
+ CUtil::ScanArchiveForAssociatedItems(pItem->GetPath(), "", item_exts, associatedFiles);
+ else
+ {
+ associatedFiles.push_back(pItem->GetPath());
+ CLog::Log(LOGINFO, "{}: found associated file {}", __FUNCTION__,
+ CURL::GetRedacted(pItem->GetPath()));
+ }
+ }
+ else
+ {
+ if (URIUtils::IsRAR(pItem->GetPath()) || URIUtils::IsZIP(pItem->GetPath()))
+ CUtil::ScanArchiveForAssociatedItems(pItem->GetPath(), videoName, item_exts, associatedFiles);
+ }
+ }
+}
+
+int CUtil::ScanArchiveForAssociatedItems(const std::string& strArchivePath,
+ const std::string& videoNameNoExt,
+ const std::vector<std::string>& item_exts,
+ std::vector<std::string>& associatedFiles)
+{
+ CLog::LogF(LOGDEBUG, "Scanning archive {}", CURL::GetRedacted(strArchivePath));
+ int nItemsAdded = 0;
+ CFileItemList ItemList;
+
+ // zip only gets the root dir
+ if (URIUtils::HasExtension(strArchivePath, ".zip"))
+ {
+ CURL pathToUrl(strArchivePath);
+ CURL zipURL = URIUtils::CreateArchivePath("zip", pathToUrl, "");
+ if (!CDirectory::GetDirectory(zipURL, ItemList, "", DIR_FLAG_NO_FILE_DIRS))
+ return false;
+ }
+ else if (URIUtils::HasExtension(strArchivePath, ".rar"))
+ {
+ CURL pathToUrl(strArchivePath);
+ CURL rarURL = URIUtils::CreateArchivePath("rar", pathToUrl, "");
+ if (!CDirectory::GetDirectory(rarURL, ItemList, "", DIR_FLAG_NO_FILE_DIRS))
+ return false;
+ }
+ for (const auto &item : ItemList)
+ {
+ std::string strPathInRar = item->GetPath();
+ std::string strExt = URIUtils::GetExtension(strPathInRar);
+
+ // Check another archive in archive
+ if (strExt == ".zip" || strExt == ".rar")
+ {
+ nItemsAdded +=
+ ScanArchiveForAssociatedItems(strPathInRar, videoNameNoExt, item_exts, associatedFiles);
+ continue;
+ }
+
+ // check that the found filename matches the movie filename
+ size_t fnl = videoNameNoExt.size();
+ // NOTE: We don't know if videoNameNoExt is URL-encoded, so try both
+ if (fnl &&
+ !(StringUtils::StartsWithNoCase(URIUtils::GetFileName(strPathInRar), videoNameNoExt) ||
+ StringUtils::StartsWithNoCase(URIUtils::GetFileName(strPathInRar), CURL::Decode(videoNameNoExt))))
+ continue;
+
+ for (const auto& ext : item_exts)
+ {
+ if (StringUtils::EqualsNoCase(strExt, ext))
+ {
+ CLog::Log(LOGINFO, "{}: found associated file {}", __FUNCTION__,
+ CURL::GetRedacted(strPathInRar));
+ associatedFiles.push_back(strPathInRar);
+ nItemsAdded++;
+ break;
+ }
+ }
+ }
+
+ return nItemsAdded;
+}
+
+void CUtil::ScanForExternalSubtitles(const std::string& strMovie, std::vector<std::string>& vecSubtitles)
+{
+ auto start = std::chrono::steady_clock::now();
+
+ CFileItem item(strMovie, false);
+ if ((item.IsInternetStream() && !URIUtils::IsOnLAN(item.GetDynPath()))
+ || item.IsPlayList()
+ || item.IsLiveTV()
+ || !item.IsVideo())
+ return;
+
+ CLog::Log(LOGDEBUG, "{}: Searching for subtitles...", __FUNCTION__);
+
+ std::string strBasePath;
+ std::string strSubtitle;
+
+ GetVideoBasePathAndFileName(strMovie, strBasePath, strSubtitle);
+
+ CFileItemList items;
+ const std::vector<std::string> common_sub_dirs = { "subs", "subtitles", "vobsubs", "sub", "vobsub", "subtitle" };
+ const std::string subtitleExtensions = CServiceBroker::GetFileExtensionProvider().GetSubtitleExtensions();
+ GetItemsToScan(strBasePath, subtitleExtensions, common_sub_dirs, items);
+
+ const std::string customPath = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_CUSTOMPATH);
+
+ if (!CMediaSettings::GetInstance().GetAdditionalSubtitleDirectoryChecked() && !customPath.empty()) // to avoid checking non-existent directories (network) every time..
+ {
+ if (!CServiceBroker::GetNetwork().IsAvailable() && !URIUtils::IsHD(customPath))
+ {
+ CLog::Log(LOGINFO, "CUtil::CacheSubtitles: disabling alternate subtitle directory for this session, it's inaccessible");
+ CMediaSettings::GetInstance().SetAdditionalSubtitleDirectoryChecked(-1); // disabled
+ }
+ else if (!CDirectory::Exists(customPath))
+ {
+ CLog::Log(LOGINFO, "CUtil::CacheSubtitles: disabling alternate subtitle directory for this session, it's nonexistent");
+ CMediaSettings::GetInstance().SetAdditionalSubtitleDirectoryChecked(-1); // disabled
+ }
+
+ CMediaSettings::GetInstance().SetAdditionalSubtitleDirectoryChecked(1);
+ }
+
+ std::vector<std::string> strLookInPaths;
+ // this is last because we dont want to check any common subdirs or cd-dirs in the alternate <subtitles> dir.
+ if (CMediaSettings::GetInstance().GetAdditionalSubtitleDirectoryChecked() == 1)
+ {
+ std::string strPath2 = customPath;
+ URIUtils::AddSlashAtEnd(strPath2);
+ strLookInPaths.push_back(strPath2);
+ }
+
+ int flags = DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO;
+ for (const std::string& path : strLookInPaths)
+ {
+ CFileItemList moreItems;
+ CDirectory::GetDirectory(path, moreItems, subtitleExtensions, flags);
+ items.Append(moreItems);
+ }
+
+ std::vector<std::string> exts = StringUtils::Split(subtitleExtensions, '|');
+ exts.erase(std::remove(exts.begin(), exts.end(), ".zip"), exts.end());
+ exts.erase(std::remove(exts.begin(), exts.end(), ".rar"), exts.end());
+
+ ScanPathsForAssociatedItems(strSubtitle, items, exts, vecSubtitles);
+
+ size_t iSize = vecSubtitles.size();
+ for (size_t i = 0; i < iSize; i++)
+ {
+ if (URIUtils::HasExtension(vecSubtitles[i], ".smi"))
+ {
+ //Cache multi-language sami subtitle
+ CDVDSubtitleStream stream;
+ if (stream.Open(vecSubtitles[i]))
+ {
+ CDVDSubtitleTagSami TagConv;
+ TagConv.LoadHead(&stream);
+ if (TagConv.m_Langclass.size() >= 2)
+ {
+ for (const auto &lang : TagConv.m_Langclass)
+ {
+ std::string strDest =
+ StringUtils::Format("special://temp/subtitle.{}.{}.smi", lang.Name, i);
+ if (CFile::Copy(vecSubtitles[i], strDest))
+ {
+ CLog::Log(LOGINFO, " cached subtitle {}->{}", CURL::GetRedacted(vecSubtitles[i]),
+ strDest);
+ vecSubtitles.push_back(strDest);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "{}: END (total time: {} ms)", __FUNCTION__, duration.count());
+}
+
+ExternalStreamInfo CUtil::GetExternalStreamDetailsFromFilename(const std::string& videoPath, const std::string& associatedFile)
+{
+ ExternalStreamInfo info;
+
+ std::string videoBaseName = URIUtils::GetFileName(videoPath);
+ URIUtils::RemoveExtension(videoBaseName);
+
+ std::string toParse = URIUtils::GetFileName(associatedFile);
+ URIUtils::RemoveExtension(toParse);
+
+ // we check left part - if it's same as video base name - strip it
+ if (StringUtils::StartsWithNoCase(toParse, videoBaseName))
+ toParse = toParse.substr(videoBaseName.length());
+ else if (URIUtils::GetExtension(associatedFile) == ".sub" && URIUtils::IsInArchive(associatedFile))
+ {
+ // exclude parsing of vobsub file names that are embedded in an archive
+ CLog::Log(LOGDEBUG, "{} - skipping archived vobsub filename parsing: {}", __FUNCTION__,
+ CURL::GetRedacted(associatedFile));
+ toParse.clear();
+ }
+
+ // trim any non-alphanumeric char in the beginning
+ std::string::iterator result = std::find_if(toParse.begin(), toParse.end(), StringUtils::isasciialphanum);
+
+ std::string name;
+ if (result != toParse.end()) // if we have anything to parse
+ {
+ std::string inputString(result, toParse.end());
+ std::string delimiters(" .-");
+ std::vector<std::string> tokens;
+ StringUtils::Tokenize(inputString, tokens, delimiters);
+
+ for (auto it = tokens.rbegin(); it != tokens.rend(); ++it)
+ {
+ // try to recognize a flag
+ std::string flag_tmp(*it);
+ StringUtils::ToLower(flag_tmp);
+ if (!flag_tmp.compare("none"))
+ {
+ info.flag |= StreamFlags::FLAG_NONE;
+ continue;
+ }
+ else if (!flag_tmp.compare("default"))
+ {
+ info.flag |= StreamFlags::FLAG_DEFAULT;
+ continue;
+ }
+ else if (!flag_tmp.compare("forced"))
+ {
+ info.flag |= StreamFlags::FLAG_FORCED;
+ continue;
+ }
+
+ if (info.language.empty())
+ {
+ std::string langCode;
+ // try to recognize language
+ if (g_LangCodeExpander.ConvertToISO6392B(*it, langCode))
+ {
+ info.language = langCode;
+ continue;
+ }
+ }
+
+ name = (*it) + " " + name;
+ }
+ }
+ name += " ";
+ name += g_localizeStrings.Get(21602); // External
+ StringUtils::Trim(name);
+ info.name = StringUtils::RemoveDuplicatedSpacesAndTabs(name);
+ if (info.flag == 0)
+ info.flag = StreamFlags::FLAG_NONE;
+
+ CLog::Log(LOGDEBUG, "{} - Language = '{}' / Name = '{}' / Flag = '{}' from {}", __FUNCTION__,
+ info.language, info.name, info.flag, CURL::GetRedacted(associatedFile));
+
+ return info;
+}
+
+/*! \brief in a vector of subtitles finds the corresponding .sub file for a given .idx file
+ */
+bool CUtil::FindVobSubPair(const std::vector<std::string>& vecSubtitles, const std::string& strIdxPath, std::string& strSubPath)
+{
+ if (URIUtils::HasExtension(strIdxPath, ".idx"))
+ {
+ std::string strIdxFile;
+ std::string strIdxDirectory;
+ URIUtils::Split(strIdxPath, strIdxDirectory, strIdxFile);
+ for (const auto &subtitlePath : vecSubtitles)
+ {
+ std::string strSubFile;
+ std::string strSubDirectory;
+ URIUtils::Split(subtitlePath, strSubDirectory, strSubFile);
+ if (URIUtils::IsInArchive(subtitlePath))
+ strSubDirectory = CURL::Decode(strSubDirectory);
+ if (URIUtils::HasExtension(strSubFile, ".sub") &&
+ (URIUtils::PathEquals(URIUtils::ReplaceExtension(strIdxPath,""),
+ URIUtils::ReplaceExtension(subtitlePath,"")) ||
+ (strSubDirectory.size() >= 11 &&
+ StringUtils::EqualsNoCase(strSubDirectory.substr(6, strSubDirectory.length()-11), URIUtils::ReplaceExtension(strIdxPath,"")))))
+ {
+ strSubPath = subtitlePath;
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/*! \brief checks if in the vector of subtitles the given .sub file has a corresponding idx and hence is a vobsub file
+ */
+bool CUtil::IsVobSub(const std::vector<std::string>& vecSubtitles, const std::string& strSubPath)
+{
+ if (URIUtils::HasExtension(strSubPath, ".sub"))
+ {
+ std::string strSubFile;
+ std::string strSubDirectory;
+ URIUtils::Split(strSubPath, strSubDirectory, strSubFile);
+ if (URIUtils::IsInArchive(strSubPath))
+ strSubDirectory = CURL::Decode(strSubDirectory);
+ for (const auto &subtitlePath : vecSubtitles)
+ {
+ std::string strIdxFile;
+ std::string strIdxDirectory;
+ URIUtils::Split(subtitlePath, strIdxDirectory, strIdxFile);
+ if (URIUtils::HasExtension(strIdxFile, ".idx") &&
+ (URIUtils::PathEquals(URIUtils::ReplaceExtension(subtitlePath,""),
+ URIUtils::ReplaceExtension(strSubPath,"")) ||
+ (strSubDirectory.size() >= 11 &&
+ StringUtils::EqualsNoCase(strSubDirectory.substr(6, strSubDirectory.length()-11), URIUtils::ReplaceExtension(subtitlePath,"")))))
+ return true;
+ }
+ }
+ return false;
+}
+
+/*! \brief find a plain or archived vobsub .sub file corresponding to an .idx file
+ */
+std::string CUtil::GetVobSubSubFromIdx(const std::string& vobSubIdx)
+{
+ std::string vobSub = URIUtils::ReplaceExtension(vobSubIdx, ".sub");
+
+ // check if a .sub file exists in the same directory
+ if (CFile::Exists(vobSub))
+ {
+ return vobSub;
+ }
+
+ // look inside a .rar or .zip in the same directory
+ const std::string archTypes[] = { "rar", "zip" };
+ std::string vobSubFilename = URIUtils::GetFileName(vobSub);
+ for (const std::string& archType : archTypes)
+ {
+ vobSub = URIUtils::CreateArchivePath(archType,
+ CURL(URIUtils::ReplaceExtension(vobSubIdx, std::string(".") + archType)),
+ vobSubFilename).Get();
+ if (CFile::Exists(vobSub))
+ return vobSub;
+ }
+
+ return std::string();
+}
+
+/*! \brief find a .idx file from a path of a plain or archived vobsub .sub file
+ */
+std::string CUtil::GetVobSubIdxFromSub(const std::string& vobSub)
+{
+ std::string vobSubIdx = URIUtils::ReplaceExtension(vobSub, ".idx");
+
+ // check if a .idx file exists in the same directory
+ if (CFile::Exists(vobSubIdx))
+ {
+ return vobSubIdx;
+ }
+
+ // look outside archive (usually .rar) if the .sub is inside one
+ if (URIUtils::IsInArchive(vobSub))
+ {
+
+ std::string archiveFile = URIUtils::GetDirectory(vobSub);
+ std::string vobSubIdxDir = URIUtils::GetParentPath(archiveFile);
+
+ if (!vobSubIdxDir.empty())
+ {
+ std::string vobSubIdxFilename = URIUtils::GetFileName(vobSubIdx);
+ std::string vobSubIdx = URIUtils::AddFileToFolder(vobSubIdxDir, vobSubIdxFilename);
+
+ if (CFile::Exists(vobSubIdx))
+ return vobSubIdx;
+ }
+ }
+
+ return std::string();
+}
+
+void CUtil::ScanForExternalAudio(const std::string& videoPath, std::vector<std::string>& vecAudio)
+{
+ CFileItem item(videoPath, false);
+ if ( item.IsInternetStream()
+ || item.IsPlayList()
+ || item.IsLiveTV()
+ || item.IsPVR()
+ || !item.IsVideo())
+ return;
+
+ std::string strBasePath;
+ std::string strAudio;
+
+ GetVideoBasePathAndFileName(videoPath, strBasePath, strAudio);
+
+ CFileItemList items;
+ const std::vector<std::string> common_sub_dirs = { "audio", "tracks"};
+ GetItemsToScan(strBasePath, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions(), common_sub_dirs, items);
+
+ std::vector<std::string> exts = StringUtils::Split(CServiceBroker::GetFileExtensionProvider().GetMusicExtensions(), "|");
+
+ CVideoDatabase database;
+ database.Open();
+ bool useAllExternalAudio = database.GetUseAllExternalAudioForVideo(videoPath);
+
+ if (useAllExternalAudio)
+ {
+ for (const auto& audioItem : items.GetList())
+ {
+ vecAudio.push_back(audioItem.get()->GetPath());
+ }
+ }
+ else
+ ScanPathsForAssociatedItems(strAudio, items, exts, vecAudio);
+}
+
+bool CUtil::CanBindPrivileged()
+{
+#ifdef TARGET_POSIX
+
+ if (geteuid() == 0)
+ return true; //root user can always bind to privileged ports
+
+#ifdef HAVE_LIBCAP
+
+ //check if CAP_NET_BIND_SERVICE is enabled, this allows non-root users to bind to privileged ports
+ bool canbind = false;
+ cap_t capabilities = cap_get_proc();
+ if (capabilities)
+ {
+ cap_flag_value_t value;
+ if (cap_get_flag(capabilities, CAP_NET_BIND_SERVICE, CAP_EFFECTIVE, &value) == 0)
+ canbind = value;
+
+ cap_free(capabilities);
+ }
+
+ return canbind;
+
+#else //HAVE_LIBCAP
+
+ return false;
+
+#endif //HAVE_LIBCAP
+
+#else //TARGET_POSIX
+
+ return true;
+
+#endif //TARGET_POSIX
+}
+
+bool CUtil::ValidatePort(int port)
+{
+ // check that it's a valid port
+#ifdef TARGET_POSIX
+ if (!CUtil::CanBindPrivileged() && (port < 1024 || port > 65535))
+ return false;
+ else
+#endif
+ if (port <= 0 || port > 65535)
+ return false;
+
+ return true;
+}
+
+int CUtil::GetRandomNumber()
+{
+#if !defined(TARGET_WINDOWS)
+ return rand_r(&s_randomSeed);
+#else
+ unsigned int number;
+ if (rand_s(&number) == 0)
+ return (int)number;
+
+ return rand();
+#endif
+}
+
+void CUtil::CopyUserDataIfNeeded(const std::string& strPath,
+ const std::string& file,
+ const std::string& destname)
+{
+ std::string destPath;
+ if (destname.empty())
+ destPath = URIUtils::AddFileToFolder(strPath, file);
+ else
+ destPath = URIUtils::AddFileToFolder(strPath, destname);
+
+ if (!CFile::Exists(destPath))
+ {
+ // need to copy it across
+ std::string srcPath = URIUtils::AddFileToFolder("special://xbmc/userdata/", file);
+ CFile::Copy(srcPath, destPath);
+ }
+}
diff --git a/xbmc/Util.h b/xbmc/Util.h
new file mode 100644
index 0000000..fe67748
--- /dev/null
+++ b/xbmc/Util.h
@@ -0,0 +1,256 @@
+/*
+ * 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 "MediaSource.h" // Definition of VECSOURCES
+#include "utils/Digest.h"
+
+#include <climits>
+#include <cmath>
+#include <stdint.h>
+#include <string.h>
+#include <vector>
+
+// A list of filesystem types for LegalPath/FileName
+#define LEGAL_NONE 0
+#define LEGAL_WIN32_COMPAT 1
+#define LEGAL_FATX 2
+
+class CFileItem;
+class CFileItemList;
+class CURL;
+
+struct ExternalStreamInfo
+{
+ std::string name;
+ std::string language;
+ unsigned int flag = 0;
+};
+
+class CUtil
+{
+ CUtil() = delete;
+public:
+ static void CleanString(const std::string& strFileName,
+ std::string& strTitle,
+ std::string& strTitleAndYear,
+ std::string& strYear,
+ bool bRemoveExtension = false,
+ bool bCleanChars = true);
+ static std::string GetTitleFromPath(const CURL& url, bool bIsFolder = false);
+ static std::string GetTitleFromPath(const std::string& strFileNameAndPath, bool bIsFolder = false);
+ static void GetQualifiedFilename(const std::string &strBasePath, std::string &strFilename);
+ static void RunShortcut(const char* szPath);
+ static std::string GetHomePath(
+ const std::string& strTarget = "KODI_HOME"); // default target is "KODI_HOME"
+ static bool ExcludeFileOrFolder(const std::string& strFileOrFolder, const std::vector<std::string>& regexps);
+ static void GetFileAndProtocol(const std::string& strURL, std::string& strDir);
+ static int GetDVDIfoTitle(const std::string& strPathFile);
+
+ static bool IsPicture(const std::string& strFile);
+ /// Get resolved filesystem location of splash image
+ static std::string GetSplashPath();
+
+ /*! \brief retrieve MD5sum of a file
+ \param strPath - path to the file to MD5sum
+ \return md5 sum of the file
+ */
+ static std::string GetFileDigest(const std::string& strPath, KODI::UTILITY::CDigest::Type type);
+ static bool GetDirectoryName(const std::string& strFileName, std::string& strDescription);
+ static void GetDVDDriveIcon(const std::string& strPath, std::string& strIcon);
+ static void RemoveTempFiles();
+
+ static void ClearSubtitles();
+ static void ScanForExternalSubtitles(const std::string& strMovie, std::vector<std::string>& vecSubtitles );
+
+ /** \brief Retrieves stream info of external associated files, e.g., subtitles, for a given video.
+ * \param[in] videoPath The full path of the video file.
+ * \param[in] associatedFile A file that provides additional streams for the given video file.
+ * \return stream info for the given associatedFile
+ */
+ static ExternalStreamInfo GetExternalStreamDetailsFromFilename(const std::string& videoPath, const std::string& associatedFile);
+ static bool FindVobSubPair( const std::vector<std::string>& vecSubtitles, const std::string& strIdxPath, std::string& strSubPath );
+ static bool IsVobSub(const std::vector<std::string>& vecSubtitles, const std::string& strSubPath);
+ static std::string GetVobSubSubFromIdx(const std::string& vobSubIdx);
+ static std::string GetVobSubIdxFromSub(const std::string& vobSub);
+
+ /** \brief Retrieves paths of external audio files for a given video.
+ * \param[in] videoPath The full path of the video file.
+ * \param[out] vecAudio A vector containing the full paths of all found external audio files.
+ */
+ static void ScanForExternalAudio(const std::string& videoPath, std::vector<std::string>& vecAudio);
+ static int64_t ToInt64(uint32_t high, uint32_t low);
+ static std::string GetNextFilename(const std::string &fn_template, int max);
+ static std::string GetNextPathname(const std::string &path_template, int max);
+ static void StatToStatI64(struct _stati64 *result, struct stat *stat);
+ static void StatToStat64(struct __stat64 *result, const struct stat *stat);
+ static void Stat64ToStatI64(struct _stati64 *result, struct __stat64 *stat);
+ static void StatI64ToStat64(struct __stat64 *result, struct _stati64 *stat);
+ static void Stat64ToStat(struct stat *result, struct __stat64 *stat);
+#ifdef TARGET_WINDOWS
+ static void Stat64ToStat64i32(struct _stat64i32 *result, struct __stat64 *stat);
+#endif
+ static bool CreateDirectoryEx(const std::string& strPath);
+
+#ifdef TARGET_WINDOWS
+ static std::string MakeLegalFileName(const std::string &strFile, int LegalType=LEGAL_WIN32_COMPAT);
+ static std::string MakeLegalPath(const std::string &strPath, int LegalType=LEGAL_WIN32_COMPAT);
+#else
+ static std::string MakeLegalFileName(const std::string &strFile, int LegalType=LEGAL_NONE);
+ static std::string MakeLegalPath(const std::string &strPath, int LegalType=LEGAL_NONE);
+#endif
+ static std::string ValidatePath(const std::string &path, bool bFixDoubleSlashes = false); ///< return a validated path, with correct directory separators.
+
+ /*!
+ * \brief Check if a filename contains a supported font extension.
+ * \param filename The filename to check
+ * \return True if it is supported, otherwise false
+ */
+ static bool IsSupportedFontExtension(const std::string& fileName);
+
+ /*! \brief Split a comma separated parameter list into separate parameters.
+ Takes care of the case where we may have a quoted string containing commas, or we may
+ have a function (i.e. parentheses) with multiple parameters as a single parameter.
+
+ eg:
+
+ foo, bar(param1, param2), foo
+
+ will return:
+
+ "foo", "bar(param1, param2)", and "foo".
+
+ \param paramString the string to break up
+ \param parameters the returned parameters
+ */
+ static void SplitParams(const std::string& paramString, std::vector<std::string>& parameters);
+ static int GetMatchingSource(const std::string& strPath, VECSOURCES& VECSOURCES, bool& bIsSourceName);
+ static std::string TranslateSpecialSource(const std::string &strSpecial);
+ static void DeleteDirectoryCache(const std::string &prefix = "");
+ static void DeleteMusicDatabaseDirectoryCache();
+ static void DeleteVideoDatabaseDirectoryCache();
+ static std::string MusicPlaylistsLocation();
+ static std::string VideoPlaylistsLocation();
+
+ static void GetSkinThemes(std::vector<std::string>& vecTheme);
+ static void GetRecursiveListing(const std::string& strPath, CFileItemList& items, const std::string& strMask, unsigned int flags = 0 /* DIR_FLAG_DEFAULTS */);
+ static void GetRecursiveDirsListing(const std::string& strPath, CFileItemList& items, unsigned int flags = 0 /* DIR_FLAG_DEFAULTS */);
+ static void ForceForwardSlashes(std::string& strPath);
+
+ static double AlbumRelevance(const std::string& strAlbumTemp1, const std::string& strAlbum1, const std::string& strArtistTemp1, const std::string& strArtist1);
+ static bool MakeShortenPath(std::string StrInput, std::string& StrOutput, size_t iTextMaxLength);
+ /*! \brief Checks whether the supplied path supports Write file operations (e.g. Rename, Delete, ...)
+
+ \param strPath the path to be checked
+
+ \return true if Write file operations are supported, false otherwise
+ */
+ static bool SupportsWriteFileOperations(const std::string& strPath);
+ /*! \brief Checks whether the supplied path supports Read file operations (e.g. Copy, ...)
+
+ \param strPath the path to be checked
+
+ \return true if Read file operations are supported, false otherwise
+ */
+ static bool SupportsReadFileOperations(const std::string& strPath);
+ static std::string GetDefaultFolderThumb(const std::string &folderThumb);
+
+ static void InitRandomSeed();
+
+ // Get decimal integer representation of roman digit, ivxlcdm are valid
+ // return 0 for other chars;
+ static int LookupRomanDigit(char roman_digit);
+ // Translate a string of roman numerals to decimal a decimal integer
+ // return -1 on error, valid range is 1-3999
+ static int TranslateRomanNumeral(const char* roman_numeral);
+
+#if defined(TARGET_POSIX) && !defined(TARGET_DARWIN_TVOS)
+ //
+ // Forks to execute a shell command.
+ //
+ static bool Command(const std::vector<std::string>& arrArgs, bool waitExit = false);
+
+ //
+ // Forks to execute an unparsed shell command line.
+ //
+ static bool RunCommandLine(const std::string& cmdLine, bool waitExit = false);
+#endif
+ static std::string ResolveExecutablePath();
+ static std::string GetFrameworksPath(bool forPython = false);
+
+ static bool CanBindPrivileged();
+ static bool ValidatePort(int port);
+
+ /*!
+ * \brief Thread-safe random number generation
+ */
+ static int GetRandomNumber();
+
+ static int64_t ConvertSecsToMilliSecs(double secs) { return static_cast<int64_t>(secs * 1000); }
+ static double ConvertMilliSecsToSecs(int64_t offset) { return offset / 1000.0; }
+ static int64_t ConvertMilliSecsToSecsInt(int64_t offset) { return offset / 1000; }
+ static int64_t ConvertMilliSecsToSecsIntRounded(int64_t offset) { return ConvertMilliSecsToSecsInt(offset + 499); }
+
+ /** \brief Copy files from the application bundle over to the user data directory in Application Support/Kodi.
+ */
+ static void CopyUserDataIfNeeded(const std::string& strPath,
+ const std::string& file,
+ const std::string& destname = "");
+
+#if !defined(TARGET_WINDOWS)
+private:
+ static unsigned int s_randomSeed;
+#endif
+
+ protected:
+ /** \brief Retrieves the base path and the filename of a given video.
+ * \param[in] videoPath The full path of the video file.
+ * \param[out] basePath The base path of the given video.
+ * \param[out] videoFileName The file name of the given video..
+ */
+ static void GetVideoBasePathAndFileName(const std::string& videoPath,
+ std::string& basePath,
+ std::string& videoFileName);
+
+ /** \brief Retrieves FileItems that could contain associated files of a given video.
+ * \param[in] videoPath The full path of the video file.
+ * \param[in] item_exts A | separated string of extensions specifying the associated files.
+ * \param[in] sub_dirs A vector of sub directory names to look for.
+ * \param[out] items A List of FileItems to scan for associated files.
+ */
+ static void GetItemsToScan(const std::string& videoPath,
+ const std::string& item_exts,
+ const std::vector<std::string>& sub_dirs,
+ CFileItemList& items);
+
+ /** \brief Searches for associated files of a given video.
+ * \param[in] videoName The name of the video file.
+ * \param[in] items A List of FileItems to scan for associated files.
+ * \param[in] item_exts A vector of extensions specifying the associated files.
+ * \param[out] associatedFiles A vector containing the full paths of all found associated files.
+ */
+ static void ScanPathsForAssociatedItems(const std::string& videoName,
+ const CFileItemList& items,
+ const std::vector<std::string>& item_exts,
+ std::vector<std::string>& associatedFiles);
+
+ /** \brief Searches in an archive for associated files of a given video.
+ * \param[in] strArchivePath The full path of the archive.
+ * \param[in] videoNameNoExt The filename of the video without extension for which associated files should be retrieved.
+ * \param[in] item_exts A vector of extensions specifying the associated files.
+ * \param[out] associatedFiles A vector containing the full paths of all found associated files.
+ */
+ static int ScanArchiveForAssociatedItems(const std::string& strArchivePath,
+ const std::string& videoNameNoExt,
+ const std::vector<std::string>& item_exts,
+ std::vector<std::string>& associatedFiles);
+
+};
+
+
diff --git a/xbmc/XBDateTime.cpp b/xbmc/XBDateTime.cpp
new file mode 100644
index 0000000..85d5d46
--- /dev/null
+++ b/xbmc/XBDateTime.cpp
@@ -0,0 +1,1580 @@
+/*
+ * 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 "XBDateTime.h"
+
+#include "LangInfo.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/Archive.h"
+#include "utils/StringUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <cstdlib>
+
+#define SECONDS_PER_DAY 86400L
+#define SECONDS_PER_HOUR 3600L
+#define SECONDS_PER_MINUTE 60L
+#define SECONDS_TO_FILETIME 10000000L
+
+static const char *DAY_NAMES[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
+static const char *MONTH_NAMES[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
+
+/////////////////////////////////////////////////
+//
+// CDateTimeSpan
+//
+
+CDateTimeSpan::CDateTimeSpan()
+{
+ m_timeSpan.highDateTime = 0;
+ m_timeSpan.lowDateTime = 0;
+}
+
+CDateTimeSpan::CDateTimeSpan(const CDateTimeSpan& span)
+{
+ m_timeSpan.highDateTime = span.m_timeSpan.highDateTime;
+ m_timeSpan.lowDateTime = span.m_timeSpan.lowDateTime;
+}
+
+CDateTimeSpan::CDateTimeSpan(int day, int hour, int minute, int second)
+{
+ SetDateTimeSpan(day, hour, minute, second);
+}
+
+bool CDateTimeSpan::operator >(const CDateTimeSpan& right) const
+{
+ return KODI::TIME::CompareFileTime(&m_timeSpan, &right.m_timeSpan) > 0;
+}
+
+bool CDateTimeSpan::operator >=(const CDateTimeSpan& right) const
+{
+ return operator >(right) || operator ==(right);
+}
+
+bool CDateTimeSpan::operator <(const CDateTimeSpan& right) const
+{
+ return KODI::TIME::CompareFileTime(&m_timeSpan, &right.m_timeSpan) < 0;
+}
+
+bool CDateTimeSpan::operator <=(const CDateTimeSpan& right) const
+{
+ return operator <(right) || operator ==(right);
+}
+
+bool CDateTimeSpan::operator ==(const CDateTimeSpan& right) const
+{
+ return KODI::TIME::CompareFileTime(&m_timeSpan, &right.m_timeSpan) == 0;
+}
+
+bool CDateTimeSpan::operator !=(const CDateTimeSpan& right) const
+{
+ return !operator ==(right);
+}
+
+CDateTimeSpan CDateTimeSpan::operator +(const CDateTimeSpan& right) const
+{
+ CDateTimeSpan left(*this);
+
+ LARGE_INTEGER timeLeft;
+ left.ToLargeInt(timeLeft);
+
+ LARGE_INTEGER timeRight;
+ right.ToLargeInt(timeRight);
+
+ timeLeft.QuadPart+=timeRight.QuadPart;
+
+ left.FromLargeInt(timeLeft);
+
+ return left;
+}
+
+CDateTimeSpan CDateTimeSpan::operator -(const CDateTimeSpan& right) const
+{
+ CDateTimeSpan left(*this);
+
+ LARGE_INTEGER timeLeft;
+ left.ToLargeInt(timeLeft);
+
+ LARGE_INTEGER timeRight;
+ right.ToLargeInt(timeRight);
+
+ timeLeft.QuadPart-=timeRight.QuadPart;
+
+ left.FromLargeInt(timeLeft);
+
+ return left;
+}
+
+const CDateTimeSpan& CDateTimeSpan::operator +=(const CDateTimeSpan& right)
+{
+ LARGE_INTEGER timeThis;
+ ToLargeInt(timeThis);
+
+ LARGE_INTEGER timeRight;
+ right.ToLargeInt(timeRight);
+
+ timeThis.QuadPart+=timeRight.QuadPart;
+
+ FromLargeInt(timeThis);
+
+ return *this;
+}
+
+const CDateTimeSpan& CDateTimeSpan::operator -=(const CDateTimeSpan& right)
+{
+ LARGE_INTEGER timeThis;
+ ToLargeInt(timeThis);
+
+ LARGE_INTEGER timeRight;
+ right.ToLargeInt(timeRight);
+
+ timeThis.QuadPart-=timeRight.QuadPart;
+
+ FromLargeInt(timeThis);
+
+ return *this;
+}
+
+void CDateTimeSpan::ToLargeInt(LARGE_INTEGER& time) const
+{
+ time.u.HighPart = m_timeSpan.highDateTime;
+ time.u.LowPart = m_timeSpan.lowDateTime;
+}
+
+void CDateTimeSpan::FromLargeInt(const LARGE_INTEGER& time)
+{
+ m_timeSpan.highDateTime = time.u.HighPart;
+ m_timeSpan.lowDateTime = time.u.LowPart;
+}
+
+void CDateTimeSpan::SetDateTimeSpan(int day, int hour, int minute, int second)
+{
+ LARGE_INTEGER time;
+ ToLargeInt(time);
+
+ time.QuadPart= static_cast<long long>(day) *SECONDS_PER_DAY*SECONDS_TO_FILETIME;
+ time.QuadPart+= static_cast<long long>(hour) *SECONDS_PER_HOUR*SECONDS_TO_FILETIME;
+ time.QuadPart+= static_cast<long long>(minute) *SECONDS_PER_MINUTE*SECONDS_TO_FILETIME;
+ time.QuadPart+= static_cast<long long>(second) *SECONDS_TO_FILETIME;
+
+ FromLargeInt(time);
+}
+
+void CDateTimeSpan::SetFromTimeString(const std::string& time) // hh:mm
+{
+ if (time.size() >= 5 && time[2] == ':')
+ {
+ int hour = atoi(time.substr(0, 2).c_str());
+ int minutes = atoi(time.substr(3, 2).c_str());
+ SetDateTimeSpan(0,hour,minutes,0);
+ }
+}
+
+int CDateTimeSpan::GetDays() const
+{
+ LARGE_INTEGER time;
+ ToLargeInt(time);
+
+ return (int)(time.QuadPart/SECONDS_TO_FILETIME)/SECONDS_PER_DAY;
+}
+
+int CDateTimeSpan::GetHours() const
+{
+ LARGE_INTEGER time;
+ ToLargeInt(time);
+
+ return (int)((time.QuadPart/SECONDS_TO_FILETIME)%SECONDS_PER_DAY)/SECONDS_PER_HOUR;
+}
+
+int CDateTimeSpan::GetMinutes() const
+{
+ LARGE_INTEGER time;
+ ToLargeInt(time);
+
+ return (int)((time.QuadPart/SECONDS_TO_FILETIME%SECONDS_PER_DAY)%SECONDS_PER_HOUR)/SECONDS_PER_MINUTE;
+}
+
+int CDateTimeSpan::GetSeconds() const
+{
+ LARGE_INTEGER time;
+ ToLargeInt(time);
+
+ return (int)(((time.QuadPart/SECONDS_TO_FILETIME%SECONDS_PER_DAY)%SECONDS_PER_HOUR)%SECONDS_PER_MINUTE)%SECONDS_PER_MINUTE;
+}
+
+int CDateTimeSpan::GetSecondsTotal() const
+{
+ LARGE_INTEGER time;
+ ToLargeInt(time);
+
+ return (int)(time.QuadPart/SECONDS_TO_FILETIME);
+}
+
+void CDateTimeSpan::SetFromPeriod(const std::string &period)
+{
+ long days = atoi(period.c_str());
+ // find the first non-space and non-number
+ size_t pos = period.find_first_not_of("0123456789 ", 0);
+ if (pos != std::string::npos)
+ {
+ std::string units = period.substr(pos, 3);
+ if (StringUtils::EqualsNoCase(units, "wee"))
+ days *= 7;
+ else if (StringUtils::EqualsNoCase(units, "mon"))
+ days *= 31;
+ }
+
+ SetDateTimeSpan(days, 0, 0, 0);
+}
+
+/////////////////////////////////////////////////
+//
+// CDateTime
+//
+
+CDateTime::CDateTime()
+{
+ Reset();
+}
+
+CDateTime::CDateTime(const KODI::TIME::SystemTime& time)
+{
+ // we store internally as a FileTime
+ m_state = ToFileTime(time, m_time) ? valid : invalid;
+}
+
+CDateTime::CDateTime(const KODI::TIME::FileTime& time) : m_time(time)
+{
+ SetValid(true);
+}
+
+CDateTime::CDateTime(const CDateTime& time) : m_time(time.m_time)
+{
+ m_state=time.m_state;
+}
+
+CDateTime::CDateTime(const time_t& time)
+{
+ m_state = ToFileTime(time, m_time) ? valid : invalid;
+}
+
+CDateTime::CDateTime(const tm& time)
+{
+ m_state = ToFileTime(time, m_time) ? valid : invalid;
+}
+
+CDateTime::CDateTime(int year, int month, int day, int hour, int minute, int second)
+{
+ SetDateTime(year, month, day, hour, minute, second);
+}
+
+CDateTime CDateTime::GetCurrentDateTime()
+{
+ // get the current time
+ KODI::TIME::SystemTime time;
+ KODI::TIME::GetLocalTime(&time);
+
+ return CDateTime(time);
+}
+
+CDateTime CDateTime::GetUTCDateTime()
+{
+ CDateTime time(GetCurrentDateTime());
+ time += GetTimezoneBias();
+ return time;
+}
+
+const CDateTime& CDateTime::operator=(const KODI::TIME::SystemTime& right)
+{
+ m_state = ToFileTime(right, m_time) ? valid : invalid;
+
+ return *this;
+}
+
+const CDateTime& CDateTime::operator=(const KODI::TIME::FileTime& right)
+{
+ m_time=right;
+ SetValid(true);
+
+ return *this;
+}
+
+const CDateTime& CDateTime::operator =(const time_t& right)
+{
+ m_state = ToFileTime(right, m_time) ? valid : invalid;
+
+ return *this;
+}
+
+const CDateTime& CDateTime::operator =(const tm& right)
+{
+ m_state = ToFileTime(right, m_time) ? valid : invalid;
+
+ return *this;
+}
+
+bool CDateTime::operator >(const CDateTime& right) const
+{
+ return operator >(right.m_time);
+}
+
+bool CDateTime::operator >=(const CDateTime& right) const
+{
+ return operator >(right) || operator ==(right);
+}
+
+bool CDateTime::operator <(const CDateTime& right) const
+{
+ return operator <(right.m_time);
+}
+
+bool CDateTime::operator <=(const CDateTime& right) const
+{
+ return operator <(right) || operator ==(right);
+}
+
+bool CDateTime::operator ==(const CDateTime& right) const
+{
+ return operator ==(right.m_time);
+}
+
+bool CDateTime::operator !=(const CDateTime& right) const
+{
+ return !operator ==(right);
+}
+
+bool CDateTime::operator>(const KODI::TIME::FileTime& right) const
+{
+ return KODI::TIME::CompareFileTime(&m_time, &right) > 0;
+}
+
+bool CDateTime::operator>=(const KODI::TIME::FileTime& right) const
+{
+ return operator >(right) || operator ==(right);
+}
+
+bool CDateTime::operator<(const KODI::TIME::FileTime& right) const
+{
+ return KODI::TIME::CompareFileTime(&m_time, &right) < 0;
+}
+
+bool CDateTime::operator<=(const KODI::TIME::FileTime& right) const
+{
+ return operator <(right) || operator ==(right);
+}
+
+bool CDateTime::operator==(const KODI::TIME::FileTime& right) const
+{
+ return KODI::TIME::CompareFileTime(&m_time, &right) == 0;
+}
+
+bool CDateTime::operator!=(const KODI::TIME::FileTime& right) const
+{
+ return !operator ==(right);
+}
+
+bool CDateTime::operator>(const KODI::TIME::SystemTime& right) const
+{
+ KODI::TIME::FileTime time;
+ ToFileTime(right, time);
+
+ return operator >(time);
+}
+
+bool CDateTime::operator>=(const KODI::TIME::SystemTime& right) const
+{
+ return operator >(right) || operator ==(right);
+}
+
+bool CDateTime::operator<(const KODI::TIME::SystemTime& right) const
+{
+ KODI::TIME::FileTime time;
+ ToFileTime(right, time);
+
+ return operator <(time);
+}
+
+bool CDateTime::operator<=(const KODI::TIME::SystemTime& right) const
+{
+ return operator <(right) || operator ==(right);
+}
+
+bool CDateTime::operator==(const KODI::TIME::SystemTime& right) const
+{
+ KODI::TIME::FileTime time;
+ ToFileTime(right, time);
+
+ return operator ==(time);
+}
+
+bool CDateTime::operator!=(const KODI::TIME::SystemTime& right) const
+{
+ return !operator ==(right);
+}
+
+bool CDateTime::operator >(const time_t& right) const
+{
+ KODI::TIME::FileTime time;
+ ToFileTime(right, time);
+
+ return operator >(time);
+}
+
+bool CDateTime::operator >=(const time_t& right) const
+{
+ return operator >(right) || operator ==(right);
+}
+
+bool CDateTime::operator <(const time_t& right) const
+{
+ KODI::TIME::FileTime time;
+ ToFileTime(right, time);
+
+ return operator <(time);
+}
+
+bool CDateTime::operator <=(const time_t& right) const
+{
+ return operator <(right) || operator ==(right);
+}
+
+bool CDateTime::operator ==(const time_t& right) const
+{
+ KODI::TIME::FileTime time;
+ ToFileTime(right, time);
+
+ return operator ==(time);
+}
+
+bool CDateTime::operator !=(const time_t& right) const
+{
+ return !operator ==(right);
+}
+
+bool CDateTime::operator >(const tm& right) const
+{
+ KODI::TIME::FileTime time;
+ ToFileTime(right, time);
+
+ return operator >(time);
+}
+
+bool CDateTime::operator >=(const tm& right) const
+{
+ return operator >(right) || operator ==(right);
+}
+
+bool CDateTime::operator <(const tm& right) const
+{
+ KODI::TIME::FileTime time;
+ ToFileTime(right, time);
+
+ return operator <(time);
+}
+
+bool CDateTime::operator <=(const tm& right) const
+{
+ return operator <(right) || operator ==(right);
+}
+
+bool CDateTime::operator ==(const tm& right) const
+{
+ KODI::TIME::FileTime time;
+ ToFileTime(right, time);
+
+ return operator ==(time);
+}
+
+bool CDateTime::operator !=(const tm& right) const
+{
+ return !operator ==(right);
+}
+
+CDateTime CDateTime::operator +(const CDateTimeSpan& right) const
+{
+ CDateTime left(*this);
+
+ LARGE_INTEGER timeLeft;
+ left.ToLargeInt(timeLeft);
+
+ LARGE_INTEGER timeRight;
+ right.ToLargeInt(timeRight);
+
+ timeLeft.QuadPart+=timeRight.QuadPart;
+
+ left.FromLargeInt(timeLeft);
+
+ return left;
+}
+
+CDateTime CDateTime::operator -(const CDateTimeSpan& right) const
+{
+ CDateTime left(*this);
+
+ LARGE_INTEGER timeLeft;
+ left.ToLargeInt(timeLeft);
+
+ LARGE_INTEGER timeRight;
+ right.ToLargeInt(timeRight);
+
+ timeLeft.QuadPart-=timeRight.QuadPart;
+
+ left.FromLargeInt(timeLeft);
+
+ return left;
+}
+
+const CDateTime& CDateTime::operator +=(const CDateTimeSpan& right)
+{
+ LARGE_INTEGER timeThis;
+ ToLargeInt(timeThis);
+
+ LARGE_INTEGER timeRight;
+ right.ToLargeInt(timeRight);
+
+ timeThis.QuadPart+=timeRight.QuadPart;
+
+ FromLargeInt(timeThis);
+
+ return *this;
+}
+
+const CDateTime& CDateTime::operator -=(const CDateTimeSpan& right)
+{
+ LARGE_INTEGER timeThis;
+ ToLargeInt(timeThis);
+
+ LARGE_INTEGER timeRight;
+ right.ToLargeInt(timeRight);
+
+ timeThis.QuadPart-=timeRight.QuadPart;
+
+ FromLargeInt(timeThis);
+
+ return *this;
+}
+
+CDateTimeSpan CDateTime::operator -(const CDateTime& right) const
+{
+ CDateTimeSpan left;
+
+ LARGE_INTEGER timeLeft;
+ left.ToLargeInt(timeLeft);
+
+ LARGE_INTEGER timeThis;
+ ToLargeInt(timeThis);
+
+ LARGE_INTEGER timeRight;
+ right.ToLargeInt(timeRight);
+
+ timeLeft.QuadPart=timeThis.QuadPart-timeRight.QuadPart;
+
+ left.FromLargeInt(timeLeft);
+
+ return left;
+}
+
+CDateTime::operator KODI::TIME::FileTime() const
+{
+ return m_time;
+}
+
+void CDateTime::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar<<(int)m_state;
+ if (m_state==valid)
+ {
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+ ar<<st;
+ }
+ }
+ else
+ {
+ Reset();
+ int state;
+ ar >> state;
+ m_state = CDateTime::STATE(state);
+ if (m_state==valid)
+ {
+ KODI::TIME::SystemTime st;
+ ar>>st;
+ ToFileTime(st, m_time);
+ }
+ }
+}
+
+void CDateTime::Reset()
+{
+ SetDateTime(1601, 1, 1, 0, 0, 0);
+ SetValid(false);
+}
+
+void CDateTime::SetValid(bool yesNo)
+{
+ m_state=yesNo ? valid : invalid;
+}
+
+bool CDateTime::IsValid() const
+{
+ return m_state==valid;
+}
+
+bool CDateTime::ToFileTime(const KODI::TIME::SystemTime& time, KODI::TIME::FileTime& fileTime) const
+{
+ return KODI::TIME::SystemTimeToFileTime(&time, &fileTime) == 1 &&
+ (fileTime.lowDateTime > 0 || fileTime.highDateTime > 0);
+}
+
+bool CDateTime::ToFileTime(const time_t& time, KODI::TIME::FileTime& fileTime) const
+{
+ long long ll = time;
+ ll *= 10000000ll;
+ ll += 0x19DB1DED53E8000LL;
+
+ fileTime.lowDateTime = (DWORD)(ll & 0xFFFFFFFF);
+ fileTime.highDateTime = (DWORD)(ll >> 32);
+
+ return true;
+}
+
+bool CDateTime::ToFileTime(const tm& time, KODI::TIME::FileTime& fileTime) const
+{
+ KODI::TIME::SystemTime st = {};
+
+ st.year = time.tm_year + 1900;
+ st.month = time.tm_mon + 1;
+ st.dayOfWeek = time.tm_wday;
+ st.day = time.tm_mday;
+ st.hour = time.tm_hour;
+ st.minute = time.tm_min;
+ st.second = time.tm_sec;
+
+ return SystemTimeToFileTime(&st, &fileTime) == 1;
+}
+
+void CDateTime::ToLargeInt(LARGE_INTEGER& time) const
+{
+ time.u.HighPart = m_time.highDateTime;
+ time.u.LowPart = m_time.lowDateTime;
+}
+
+void CDateTime::FromLargeInt(const LARGE_INTEGER& time)
+{
+ m_time.highDateTime = time.u.HighPart;
+ m_time.lowDateTime = time.u.LowPart;
+}
+
+bool CDateTime::SetFromDateString(const std::string &date)
+{
+ //! @todo STRING_CLEANUP
+ if (date.empty())
+ {
+ SetValid(false);
+ return false;
+ }
+
+ if (SetFromDBDate(date))
+ return true;
+
+ const char* months[] = {"january","february","march","april","may","june","july","august","september","october","november","december",NULL};
+ int j=0;
+ size_t iDayPos = date.find("day");
+ size_t iPos = date.find(' ');
+ if (iDayPos < iPos && iDayPos != std::string::npos)
+ {
+ iDayPos = iPos + 1;
+ iPos = date.find(' ', iPos+1);
+ }
+ else
+ iDayPos = 0;
+
+ std::string strMonth = date.substr(iDayPos, iPos - iDayPos);
+ if (strMonth.empty())
+ return false;
+
+ size_t iPos2 = date.find(',');
+ std::string strDay = (date.size() >= iPos) ? date.substr(iPos, iPos2-iPos) : "";
+ std::string strYear = date.substr(date.find(' ', iPos2) + 1);
+ while (months[j] && StringUtils::CompareNoCase(strMonth, months[j]) != 0)
+ j++;
+ if (!months[j])
+ return false;
+
+ return SetDateTime(atol(strYear.c_str()),j+1,atol(strDay.c_str()),0,0,0);
+}
+
+int CDateTime::GetDay() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return st.day;
+}
+
+int CDateTime::GetMonth() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return st.month;
+}
+
+int CDateTime::GetYear() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return st.year;
+}
+
+int CDateTime::GetHour() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return st.hour;
+}
+
+int CDateTime::GetMinute() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return st.minute;
+}
+
+int CDateTime::GetSecond() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return st.second;
+}
+
+int CDateTime::GetDayOfWeek() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return st.dayOfWeek;
+}
+
+int CDateTime::GetMinuteOfDay() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+ return st.hour * 60 + st.minute;
+}
+
+bool CDateTime::SetDateTime(int year, int month, int day, int hour, int minute, int second)
+{
+ KODI::TIME::SystemTime st = {};
+
+ st.year = year;
+ st.month = month;
+ st.day = day;
+ st.hour = hour;
+ st.minute = minute;
+ st.second = second;
+
+ m_state = ToFileTime(st, m_time) ? valid : invalid;
+ return m_state == valid;
+}
+
+bool CDateTime::SetDate(int year, int month, int day)
+{
+ return SetDateTime(year, month, day, 0, 0, 0);
+}
+
+bool CDateTime::SetTime(int hour, int minute, int second)
+{
+ // 01.01.1601 00:00:00 is 0 as filetime
+ return SetDateTime(1601, 1, 1, hour, minute, second);
+}
+
+void CDateTime::GetAsSystemTime(KODI::TIME::SystemTime& time) const
+{
+ FileTimeToSystemTime(&m_time, &time);
+}
+
+#define UNIX_BASE_TIME 116444736000000000LL /* nanoseconds since epoch */
+void CDateTime::GetAsTime(time_t& time) const
+{
+ long long ll = (static_cast<long long>(m_time.highDateTime) << 32) + m_time.lowDateTime;
+ time=(time_t)((ll - UNIX_BASE_TIME) / 10000000);
+}
+
+void CDateTime::GetAsTm(tm& time) const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ time = {};
+ time.tm_year = st.year - 1900;
+ time.tm_mon = st.month - 1;
+ time.tm_wday = st.dayOfWeek;
+ time.tm_mday = st.day;
+ time.tm_hour = st.hour;
+ time.tm_min = st.minute;
+ time.tm_sec = st.second;
+ time.tm_isdst = -1;
+
+ mktime(&time);
+}
+
+void CDateTime::GetAsTimeStamp(KODI::TIME::FileTime& time) const
+{
+ KODI::TIME::LocalFileTimeToFileTime(&m_time, &time);
+}
+
+std::string CDateTime::GetAsDBDate() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return StringUtils::Format("{:04}-{:02}-{:02}", st.year, st.month, st.day);
+}
+
+std::string CDateTime::GetAsDBTime() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return StringUtils::Format("{:02}:{:02}:{:02}", st.hour, st.minute, st.second);
+}
+
+std::string CDateTime::GetAsDBDateTime() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return StringUtils::Format("{:04}-{:02}-{:02} {:02}:{:02}:{:02}", st.year, st.month, st.day,
+ st.hour, st.minute, st.second);
+}
+
+std::string CDateTime::GetAsSaveString() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return StringUtils::Format("{:04}{:02}{:02}_{:02}{:02}{:02}", st.year, st.month, st.day, st.hour,
+ st.minute, st.second);
+}
+
+bool CDateTime::SetFromUTCDateTime(const CDateTime &dateTime)
+{
+ CDateTime tmp(dateTime);
+ tmp -= GetTimezoneBias();
+
+ m_time = tmp.m_time;
+ m_state = tmp.m_state;
+ return m_state == valid;
+}
+
+static bool bGotTimezoneBias = false;
+
+void CDateTime::ResetTimezoneBias(void)
+{
+ bGotTimezoneBias = false;
+}
+
+CDateTimeSpan CDateTime::GetTimezoneBias(void)
+{
+ static CDateTimeSpan timezoneBias;
+
+ if (!bGotTimezoneBias)
+ {
+ bGotTimezoneBias = true;
+ KODI::TIME::TimeZoneInformation tz;
+ switch (KODI::TIME::GetTimeZoneInformation(&tz))
+ {
+ case KODI::TIME::KODI_TIME_ZONE_ID_DAYLIGHT:
+ timezoneBias = CDateTimeSpan(0, 0, tz.bias + tz.daylightBias, 0);
+ break;
+ case KODI::TIME::KODI_TIME_ZONE_ID_STANDARD:
+ timezoneBias = CDateTimeSpan(0, 0, tz.bias + tz.standardBias, 0);
+ break;
+ case KODI::TIME::KODI_TIME_ZONE_ID_UNKNOWN:
+ timezoneBias = CDateTimeSpan(0, 0, tz.bias, 0);
+ break;
+ }
+ }
+
+ return timezoneBias;
+}
+
+bool CDateTime::SetFromUTCDateTime(const time_t &dateTime)
+{
+ CDateTime tmp(dateTime);
+ return SetFromUTCDateTime(tmp);
+}
+
+bool CDateTime::SetFromW3CDate(const std::string &dateTime)
+{
+ std::string date;
+
+ size_t posT = dateTime.find('T');
+ if(posT != std::string::npos)
+ date = dateTime.substr(0, posT);
+ else
+ date = dateTime;
+
+ int year = 0, month = 1, day = 1;
+
+ if (date.size() >= 4)
+ year = atoi(date.substr(0, 4).c_str());
+
+ if (date.size() >= 10)
+ {
+ month = atoi(date.substr(5, 2).c_str());
+ day = atoi(date.substr(8, 2).c_str());
+ }
+
+ CDateTime tmpDateTime(year, month, day, 0, 0, 0);
+ if (tmpDateTime.IsValid())
+ *this = tmpDateTime;
+
+ return IsValid();
+}
+
+bool CDateTime::SetFromW3CDateTime(const std::string &dateTime, bool ignoreTimezone /* = false */)
+{
+ std::string date, time, zone;
+
+ size_t posT = dateTime.find('T');
+ if(posT != std::string::npos)
+ {
+ date = dateTime.substr(0, posT);
+ std::string::size_type posZ = dateTime.find_first_of("+-Z", posT);
+ if(posZ == std::string::npos)
+ time = dateTime.substr(posT + 1);
+ else
+ {
+ time = dateTime.substr(posT + 1, posZ - posT - 1);
+ zone = dateTime.substr(posZ);
+ }
+ }
+ else
+ date = dateTime;
+
+ int year = 0, month = 1, day = 1, hour = 0, min = 0, sec = 0;
+
+ if (date.size() >= 4)
+ year = atoi(date.substr(0, 4).c_str());
+
+ if (date.size() >= 10)
+ {
+ month = atoi(date.substr(5, 2).c_str());
+ day = atoi(date.substr(8, 2).c_str());
+ }
+
+ if (time.length() >= 5)
+ {
+ hour = atoi(time.substr(0, 2).c_str());
+ min = atoi(time.substr(3, 2).c_str());
+ }
+
+ if (time.length() >= 8)
+ sec = atoi(time.substr(6, 2).c_str());
+
+ CDateTime tmpDateTime(year, month, day, hour, min, sec);
+ if (!tmpDateTime.IsValid())
+ return false;
+
+ if (!ignoreTimezone && !zone.empty())
+ {
+ // check if the timezone is UTC
+ if (StringUtils::StartsWith(zone, "Z"))
+ return SetFromUTCDateTime(tmpDateTime);
+ else
+ {
+ // retrieve the timezone offset (ignoring the + or -)
+ CDateTimeSpan zoneSpan; zoneSpan.SetFromTimeString(zone.substr(1));
+ if (zoneSpan.GetSecondsTotal() != 0)
+ {
+ if (StringUtils::StartsWith(zone, "+"))
+ tmpDateTime -= zoneSpan;
+ else if (StringUtils::StartsWith(zone, "-"))
+ tmpDateTime += zoneSpan;
+ }
+ }
+ }
+
+ *this = tmpDateTime;
+ return IsValid();
+}
+
+bool CDateTime::SetFromDBDateTime(const std::string &dateTime)
+{
+ // assumes format YYYY-MM-DD HH:MM:SS
+ if (dateTime.size() == 19)
+ {
+ int year = atoi(dateTime.substr(0, 4).c_str());
+ int month = atoi(dateTime.substr(5, 2).c_str());
+ int day = atoi(dateTime.substr(8, 2).c_str());
+ int hour = atoi(dateTime.substr(11, 2).c_str());
+ int min = atoi(dateTime.substr(14, 2).c_str());
+ int sec = atoi(dateTime.substr(17, 2).c_str());
+ return SetDateTime(year, month, day, hour, min, sec);
+ }
+ return false;
+}
+
+bool CDateTime::SetFromDBDate(const std::string &date)
+{
+ if (date.size() < 10)
+ return false;
+ // assumes format:
+ // YYYY-MM-DD or DD-MM-YYYY
+ const static std::string sep_chars = "-./";
+ int year = 0, month = 0, day = 0;
+ if (sep_chars.find(date[2]) != std::string::npos)
+ {
+ day = atoi(date.substr(0, 2).c_str());
+ month = atoi(date.substr(3, 2).c_str());
+ year = atoi(date.substr(6, 4).c_str());
+ }
+ else if (sep_chars.find(date[4]) != std::string::npos)
+ {
+ year = atoi(date.substr(0, 4).c_str());
+ month = atoi(date.substr(5, 2).c_str());
+ day = atoi(date.substr(8, 2).c_str());
+ }
+ return SetDate(year, month, day);
+}
+
+bool CDateTime::SetFromDBTime(const std::string &time)
+{
+ if (time.size() < 5)
+ return false;
+
+ int hour;
+ int minute;
+
+ int second = 0;
+ // HH:MM or HH:MM:SS
+ hour = atoi(time.substr(0, 2).c_str());
+ minute = atoi(time.substr(3, 2).c_str());
+ // HH:MM:SS
+ if (time.size() == 8)
+ second = atoi(time.substr(6, 2).c_str());
+
+ return SetTime(hour, minute, second);
+}
+
+bool CDateTime::SetFromRFC1123DateTime(const std::string &dateTime)
+{
+ std::string date = dateTime;
+ StringUtils::Trim(date);
+
+ if (date.size() != 29)
+ return false;
+
+ int day = strtol(date.substr(5, 2).c_str(), NULL, 10);
+
+ std::string strMonth = date.substr(8, 3);
+ int month = 0;
+ for (unsigned int index = 0; index < 12; index++)
+ {
+ if (strMonth == MONTH_NAMES[index])
+ {
+ month = index + 1;
+ break;
+ }
+ }
+
+ if (month < 1)
+ return false;
+
+ int year = strtol(date.substr(12, 4).c_str(), NULL, 10);
+ int hour = strtol(date.substr(17, 2).c_str(), NULL, 10);
+ int min = strtol(date.substr(20, 2).c_str(), NULL, 10);
+ int sec = strtol(date.substr(23, 2).c_str(), NULL, 10);
+
+ return SetDateTime(year, month, day, hour, min, sec);
+}
+
+CDateTime CDateTime::FromDateString(const std::string &date)
+{
+ CDateTime dt;
+ dt.SetFromDateString(date);
+ return dt;
+}
+
+CDateTime CDateTime::FromDBDateTime(const std::string &dateTime)
+{
+ CDateTime dt;
+ dt.SetFromDBDateTime(dateTime);
+ return dt;
+}
+
+CDateTime CDateTime::FromDBDate(const std::string &date)
+{
+ CDateTime dt;
+ dt.SetFromDBDate(date);
+ return dt;
+}
+
+CDateTime CDateTime::FromDBTime(const std::string &time)
+{
+ CDateTime dt;
+ dt.SetFromDBTime(time);
+ return dt;
+}
+
+CDateTime CDateTime::FromW3CDate(const std::string &date)
+{
+ CDateTime dt;
+ dt.SetFromW3CDate(date);
+ return dt;
+}
+
+CDateTime CDateTime::FromW3CDateTime(const std::string &date, bool ignoreTimezone /* = false */)
+{
+ CDateTime dt;
+ dt.SetFromW3CDateTime(date, ignoreTimezone);
+ return dt;
+}
+
+CDateTime CDateTime::FromUTCDateTime(const CDateTime &dateTime)
+{
+ CDateTime dt;
+ dt.SetFromUTCDateTime(dateTime);
+ return dt;
+}
+
+CDateTime CDateTime::FromUTCDateTime(const time_t &dateTime)
+{
+ CDateTime dt;
+ dt.SetFromUTCDateTime(dateTime);
+ return dt;
+}
+
+CDateTime CDateTime::FromRFC1123DateTime(const std::string &dateTime)
+{
+ CDateTime dt;
+ dt.SetFromRFC1123DateTime(dateTime);
+ return dt;
+}
+
+std::string CDateTime::GetAsLocalizedTime(const std::string &format, bool withSeconds) const
+{
+ std::string strOut;
+ const std::string& strFormat = format.empty() ? g_langInfo.GetTimeFormat() : format;
+
+ KODI::TIME::SystemTime dateTime;
+ GetAsSystemTime(dateTime);
+
+ // Prefetch meridiem symbol
+ const std::string& strMeridiem =
+ CLangInfo::MeridiemSymbolToString(dateTime.hour > 11 ? MeridiemSymbolPM : MeridiemSymbolAM);
+
+ size_t length = strFormat.size();
+ for (size_t i=0; i < length; ++i)
+ {
+ char c=strFormat[i];
+ if (c=='\'')
+ {
+ // To be able to display a "'" in the string,
+ // find the last "'" that doesn't follow a "'"
+ size_t pos=i + 1;
+ while(((pos = strFormat.find(c, pos + 1)) != std::string::npos &&
+ pos<strFormat.size()) && strFormat[pos+1]=='\'') {}
+
+ std::string strPart;
+ if (pos != std::string::npos)
+ {
+ // Extract string between ' '
+ strPart=strFormat.substr(i + 1, pos - i - 1);
+ i=pos;
+ }
+ else
+ {
+ strPart=strFormat.substr(i + 1, length - i - 1);
+ i=length;
+ }
+
+ StringUtils::Replace(strPart, "''", "'");
+
+ strOut+=strPart;
+ }
+ else if (c=='h' || c=='H') // parse hour (H="24 hour clock")
+ {
+ int partLength=0;
+
+ int pos=strFormat.find_first_not_of(c,i+1);
+ if (pos>-1)
+ {
+ // Get length of the hour mask, eg. HH
+ partLength=pos-i;
+ i=pos-1;
+ }
+ else
+ {
+ // mask ends at the end of the string, extract it
+ partLength=length-i;
+ i=length;
+ }
+
+ int hour = dateTime.hour;
+ if (c=='h')
+ { // recalc to 12 hour clock
+ if (hour > 11)
+ hour -= (12 * (hour > 12));
+ else
+ hour += (12 * (hour < 1));
+ }
+
+ // Format hour string with the length of the mask
+ std::string str;
+ if (partLength==1)
+ str = std::to_string(hour);
+ else
+ str = StringUtils::Format("{:02}", hour);
+
+ strOut+=str;
+ }
+ else if (c=='m') // parse minutes
+ {
+ int partLength=0;
+
+ int pos=strFormat.find_first_not_of(c,i+1);
+ if (pos>-1)
+ {
+ // Get length of the minute mask, eg. mm
+ partLength=pos-i;
+ i=pos-1;
+ }
+ else
+ {
+ // mask ends at the end of the string, extract it
+ partLength=length-i;
+ i=length;
+ }
+
+ // Format minute string with the length of the mask
+ std::string str;
+ if (partLength==1)
+ str = std::to_string(dateTime.minute);
+ else
+ str = StringUtils::Format("{:02}", dateTime.minute);
+
+ strOut+=str;
+ }
+ else if (c=='s') // parse seconds
+ {
+ int partLength=0;
+
+ int pos=strFormat.find_first_not_of(c,i+1);
+ if (pos>-1)
+ {
+ // Get length of the seconds mask, eg. ss
+ partLength=pos-i;
+ i=pos-1;
+ }
+ else
+ {
+ // mask ends at the end of the string, extract it
+ partLength=length-i;
+ i=length;
+ }
+
+ if (withSeconds)
+ {
+ // Format seconds string with the length of the mask
+ std::string str;
+ if (partLength==1)
+ str = std::to_string(dateTime.second);
+ else
+ str = StringUtils::Format("{:02}", dateTime.second);
+
+ strOut+=str;
+ }
+ else
+ strOut.erase(strOut.size()-1,1);
+ }
+ else if (c=='x') // add meridiem symbol
+ {
+ int pos=strFormat.find_first_not_of(c,i+1);
+ if (pos>-1)
+ {
+ // Get length of the meridiem mask
+ i=pos-1;
+ }
+ else
+ {
+ // mask ends at the end of the string, extract it
+ i=length;
+ }
+
+ strOut+=strMeridiem;
+ }
+ else // everything else pass to output
+ strOut+=c;
+ }
+
+ return strOut;
+}
+
+std::string CDateTime::GetAsLocalizedDate(bool longDate/*=false*/) const
+{
+ return GetAsLocalizedDate(g_langInfo.GetDateFormat(longDate));
+}
+
+std::string CDateTime::GetAsLocalizedDate(const std::string &strFormat) const
+{
+ std::string strOut;
+
+ KODI::TIME::SystemTime dateTime;
+ GetAsSystemTime(dateTime);
+
+ size_t length = strFormat.size();
+ for (size_t i = 0; i < length; ++i)
+ {
+ char c=strFormat[i];
+ if (c=='\'')
+ {
+ // To be able to display a "'" in the string,
+ // find the last "'" that doesn't follow a "'"
+ size_t pos = i + 1;
+ while(((pos = strFormat.find(c, pos + 1)) != std::string::npos &&
+ pos < strFormat.size()) &&
+ strFormat[pos + 1] == '\'') {}
+
+ std::string strPart;
+ if (pos != std::string::npos)
+ {
+ // Extract string between ' '
+ strPart = strFormat.substr(i + 1, pos - i - 1);
+ i = pos;
+ }
+ else
+ {
+ strPart = strFormat.substr(i + 1, length - i - 1);
+ i = length;
+ }
+ StringUtils::Replace(strPart, "''", "'");
+ strOut+=strPart;
+ }
+ else if (c=='D' || c=='d') // parse days
+ {
+ size_t partLength=0;
+
+ size_t pos = strFormat.find_first_not_of(c, i+1);
+ if (pos != std::string::npos)
+ {
+ // Get length of the day mask, eg. DDDD
+ partLength=pos-i;
+ i=pos-1;
+ }
+ else
+ {
+ // mask ends at the end of the string, extract it
+ partLength=length-i;
+ i=length;
+ }
+
+ // Format string with the length of the mask
+ std::string str;
+ if (partLength==1) // single-digit number
+ str = std::to_string(dateTime.day);
+ else if (partLength==2) // two-digit number
+ str = StringUtils::Format("{:02}", dateTime.day);
+ else // Day of week string
+ {
+ int wday = dateTime.dayOfWeek;
+ if (wday < 1 || wday > 7) wday = 7;
+ str = g_localizeStrings.Get((c =='d' ? 40 : 10) + wday);
+ }
+ strOut+=str;
+ }
+ else if (c=='M' || c=='m') // parse month
+ {
+ size_t partLength=0;
+
+ size_t pos=strFormat.find_first_not_of(c,i+1);
+ if (pos != std::string::npos)
+ {
+ // Get length of the month mask, eg. MMMM
+ partLength=pos-i;
+ i=pos-1;
+ }
+ else
+ {
+ // mask ends at the end of the string, extract it
+ partLength=length-i;
+ i=length;
+ }
+
+ // Format string with the length of the mask
+ std::string str;
+ if (partLength==1) // single-digit number
+ str = std::to_string(dateTime.month);
+ else if (partLength==2) // two-digit number
+ str = StringUtils::Format("{:02}", dateTime.month);
+ else // Month string
+ {
+ int wmonth = dateTime.month;
+ if (wmonth < 1 || wmonth > 12) wmonth = 12;
+ str = g_localizeStrings.Get((c =='m' ? 50 : 20) + wmonth);
+ }
+ strOut+=str;
+ }
+ else if (c=='Y' || c =='y') // parse year
+ {
+ size_t partLength = 0;
+
+ size_t pos = strFormat.find_first_not_of(c,i+1);
+ if (pos != std::string::npos)
+ {
+ // Get length of the year mask, eg. YYYY
+ partLength=pos-i;
+ i=pos-1;
+ }
+ else
+ {
+ // mask ends at the end of the string, extract it
+ partLength=length-i;
+ i=length;
+ }
+
+ // Format string with the length of the mask
+ std::string str = std::to_string(dateTime.year); // four-digit number
+ if (partLength <= 2)
+ str.erase(0, 2); // two-digit number
+
+ strOut+=str;
+ }
+ else // everything else pass to output
+ strOut+=c;
+ }
+
+ return strOut;
+}
+
+std::string CDateTime::GetAsLocalizedDateTime(bool longDate/*=false*/, bool withSeconds/*=true*/) const
+{
+ return GetAsLocalizedDate(longDate) + ' ' + GetAsLocalizedTime("", withSeconds);
+}
+
+std::string CDateTime::GetAsLocalizedTime(TIME_FORMAT format, bool withSeconds /* = false */) const
+{
+ const std::string timeFormat = g_langInfo.GetTimeFormat();
+ bool use12hourclock = timeFormat.find('h') != std::string::npos;
+ switch (format)
+ {
+ case TIME_FORMAT_GUESS:
+ return GetAsLocalizedTime("", withSeconds);
+ case TIME_FORMAT_SS:
+ return GetAsLocalizedTime("ss", true);
+ case TIME_FORMAT_MM:
+ return GetAsLocalizedTime("mm", true);
+ case TIME_FORMAT_MM_SS:
+ return GetAsLocalizedTime("mm:ss", true);
+ case TIME_FORMAT_HH: // this forces it to a 12 hour clock
+ return GetAsLocalizedTime(use12hourclock ? "h" : "HH", false);
+ case TIME_FORMAT_HH_SS:
+ return GetAsLocalizedTime(use12hourclock ? "h:ss" : "HH:ss", true);
+ case TIME_FORMAT_HH_MM:
+ return GetAsLocalizedTime(use12hourclock ? "h:mm" : "HH:mm", false);
+ case TIME_FORMAT_HH_MM_XX:
+ return GetAsLocalizedTime(use12hourclock ? "h:mm xx" : "HH:mm", false);
+ case TIME_FORMAT_HH_MM_SS:
+ return GetAsLocalizedTime(use12hourclock ? "hh:mm:ss" : "HH:mm:ss", true);
+ case TIME_FORMAT_HH_MM_SS_XX:
+ return GetAsLocalizedTime(use12hourclock ? "hh:mm:ss xx" : "HH:mm:ss", true);
+ case TIME_FORMAT_H:
+ return GetAsLocalizedTime("h", false);
+ case TIME_FORMAT_M:
+ return GetAsLocalizedTime("m", false);
+ case TIME_FORMAT_H_MM_SS:
+ return GetAsLocalizedTime("h:mm:ss", true);
+ case TIME_FORMAT_H_MM_SS_XX:
+ return GetAsLocalizedTime("h:mm:ss xx", true);
+ case TIME_FORMAT_XX:
+ return use12hourclock ? GetAsLocalizedTime("xx", false) : "";
+ default:
+ break;
+ }
+ return GetAsLocalizedTime("", false);
+}
+
+CDateTime CDateTime::GetAsUTCDateTime() const
+{
+ CDateTime time(m_time);
+ time += GetTimezoneBias();
+ return time;
+}
+
+std::string CDateTime::GetAsRFC1123DateTime() const
+{
+ CDateTime time(GetAsUTCDateTime());
+
+ int weekDay = time.GetDayOfWeek();
+ if (weekDay < 0)
+ weekDay = 0;
+ else if (weekDay > 6)
+ weekDay = 6;
+ if (weekDay != time.GetDayOfWeek())
+ CLog::Log(LOGWARNING, "Invalid day of week {} in {}", time.GetDayOfWeek(),
+ time.GetAsDBDateTime());
+
+ int month = time.GetMonth();
+ if (month < 1)
+ month = 1;
+ else if (month > 12)
+ month = 12;
+ if (month != time.GetMonth())
+ CLog::Log(LOGWARNING, "Invalid month {} in {}", time.GetMonth(), time.GetAsDBDateTime());
+
+ return StringUtils::Format("{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT", DAY_NAMES[weekDay],
+ time.GetDay(), MONTH_NAMES[month - 1], time.GetYear(), time.GetHour(),
+ time.GetMinute(), time.GetSecond());
+}
+
+std::string CDateTime::GetAsW3CDate() const
+{
+ KODI::TIME::SystemTime st;
+ GetAsSystemTime(st);
+
+ return StringUtils::Format("{:04}-{:02}-{:02}", st.year, st.month, st.day);
+}
+
+std::string CDateTime::GetAsW3CDateTime(bool asUtc /* = false */) const
+{
+ CDateTime w3cDate = *this;
+ if (asUtc)
+ w3cDate = GetAsUTCDateTime();
+ KODI::TIME::SystemTime st;
+ w3cDate.GetAsSystemTime(st);
+
+ std::string result = StringUtils::Format("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", st.year, st.month,
+ st.day, st.hour, st.minute, st.second);
+ if (asUtc)
+ return result + "Z";
+
+ CDateTimeSpan bias = GetTimezoneBias();
+ return result + StringUtils::Format("{}{:02}:{:02}", (bias.GetSecondsTotal() >= 0 ? '+' : '-'),
+ abs(bias.GetHours()), abs(bias.GetMinutes()));
+}
+
+int CDateTime::MonthStringToMonthNum(const std::string& month)
+{
+ const char* months[] = {"january","february","march","april","may","june","july","august","september","october","november","december"};
+ const char* abr_months[] = {"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"};
+
+ int i = 0;
+ for (; i < 12 && !StringUtils::EqualsNoCase(month, months[i]) && !StringUtils::EqualsNoCase(month, abr_months[i]); i++);
+ i++;
+
+ return i;
+}
diff --git a/xbmc/XBDateTime.h b/xbmc/XBDateTime.h
new file mode 100644
index 0000000..fb18e7c
--- /dev/null
+++ b/xbmc/XBDateTime.h
@@ -0,0 +1,213 @@
+/*
+ * 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 "utils/IArchivable.h"
+#include "utils/TimeFormat.h"
+#include "utils/XTimeUtils.h"
+
+#include <string>
+
+#include "PlatformDefs.h"
+
+class CDateTime;
+
+class CDateTimeSpan
+{
+public:
+ CDateTimeSpan();
+ CDateTimeSpan(const CDateTimeSpan& span);
+ CDateTimeSpan& operator=(const CDateTimeSpan&) = default;
+ CDateTimeSpan(int day, int hour, int minute, int second);
+
+ bool operator >(const CDateTimeSpan& right) const;
+ bool operator >=(const CDateTimeSpan& right) const;
+ bool operator <(const CDateTimeSpan& right) const;
+ bool operator <=(const CDateTimeSpan& right) const;
+ bool operator ==(const CDateTimeSpan& right) const;
+ bool operator !=(const CDateTimeSpan& right) const;
+
+ CDateTimeSpan operator +(const CDateTimeSpan& right) const;
+ CDateTimeSpan operator -(const CDateTimeSpan& right) const;
+
+ const CDateTimeSpan& operator +=(const CDateTimeSpan& right);
+ const CDateTimeSpan& operator -=(const CDateTimeSpan& right);
+
+ void SetDateTimeSpan(int day, int hour, int minute, int second);
+ void SetFromPeriod(const std::string &period);
+ void SetFromTimeString(const std::string& time);
+
+ int GetDays() const;
+ int GetHours() const;
+ int GetMinutes() const;
+ int GetSeconds() const;
+ int GetSecondsTotal() const;
+
+private:
+ void ToLargeInt(LARGE_INTEGER& time) const;
+ void FromLargeInt(const LARGE_INTEGER& time);
+
+private:
+ KODI::TIME::FileTime m_timeSpan;
+
+ friend class CDateTime;
+};
+
+/// \brief DateTime class, which uses FileTime as it's base.
+class CDateTime final : public IArchivable
+{
+public:
+ CDateTime();
+ CDateTime(const CDateTime& time);
+ CDateTime& operator=(const CDateTime&) = default;
+ explicit CDateTime(const KODI::TIME::SystemTime& time);
+ explicit CDateTime(const KODI::TIME::FileTime& time);
+ explicit CDateTime(const time_t& time);
+ explicit CDateTime(const tm& time);
+ CDateTime(int year, int month, int day, int hour, int minute, int second);
+
+ static CDateTime GetCurrentDateTime();
+ static CDateTime GetUTCDateTime();
+ static int MonthStringToMonthNum(const std::string& month);
+
+ static CDateTime FromDBDateTime(const std::string &dateTime);
+ static CDateTime FromDateString(const std::string &date);
+ static CDateTime FromDBDate(const std::string &date);
+ static CDateTime FromDBTime(const std::string &time);
+ static CDateTime FromW3CDate(const std::string &date);
+ static CDateTime FromW3CDateTime(const std::string &date, bool ignoreTimezone = false);
+ static CDateTime FromUTCDateTime(const CDateTime &dateTime);
+ static CDateTime FromUTCDateTime(const time_t &dateTime);
+ static CDateTime FromRFC1123DateTime(const std::string &dateTime);
+
+ const CDateTime& operator=(const KODI::TIME::SystemTime& right);
+ const CDateTime& operator=(const KODI::TIME::FileTime& right);
+ const CDateTime& operator =(const time_t& right);
+ const CDateTime& operator =(const tm& right);
+
+ bool operator >(const CDateTime& right) const;
+ bool operator >=(const CDateTime& right) const;
+ bool operator <(const CDateTime& right) const;
+ bool operator <=(const CDateTime& right) const;
+ bool operator ==(const CDateTime& right) const;
+ bool operator !=(const CDateTime& right) const;
+
+ bool operator>(const KODI::TIME::FileTime& right) const;
+ bool operator>=(const KODI::TIME::FileTime& right) const;
+ bool operator<(const KODI::TIME::FileTime& right) const;
+ bool operator<=(const KODI::TIME::FileTime& right) const;
+ bool operator==(const KODI::TIME::FileTime& right) const;
+ bool operator!=(const KODI::TIME::FileTime& right) const;
+
+ bool operator>(const KODI::TIME::SystemTime& right) const;
+ bool operator>=(const KODI::TIME::SystemTime& right) const;
+ bool operator<(const KODI::TIME::SystemTime& right) const;
+ bool operator<=(const KODI::TIME::SystemTime& right) const;
+ bool operator==(const KODI::TIME::SystemTime& right) const;
+ bool operator!=(const KODI::TIME::SystemTime& right) const;
+
+ bool operator >(const time_t& right) const;
+ bool operator >=(const time_t& right) const;
+ bool operator <(const time_t& right) const;
+ bool operator <=(const time_t& right) const;
+ bool operator ==(const time_t& right) const;
+ bool operator !=(const time_t& right) const;
+
+ bool operator >(const tm& right) const;
+ bool operator >=(const tm& right) const;
+ bool operator <(const tm& right) const;
+ bool operator <=(const tm& right) const;
+ bool operator ==(const tm& right) const;
+ bool operator !=(const tm& right) const;
+
+ CDateTime operator +(const CDateTimeSpan& right) const;
+ CDateTime operator -(const CDateTimeSpan& right) const;
+
+ const CDateTime& operator +=(const CDateTimeSpan& right);
+ const CDateTime& operator -=(const CDateTimeSpan& right);
+
+ CDateTimeSpan operator -(const CDateTime& right) const;
+
+ operator KODI::TIME::FileTime() const;
+
+ void Archive(CArchive& ar) override;
+
+ void Reset();
+
+ int GetDay() const;
+ int GetMonth() const;
+ int GetYear() const;
+ int GetHour() const;
+ int GetMinute() const;
+ int GetSecond() const;
+ int GetDayOfWeek() const;
+ int GetMinuteOfDay() const;
+
+ bool SetDateTime(int year, int month, int day, int hour, int minute, int second);
+ bool SetDate(int year, int month, int day);
+ bool SetTime(int hour, int minute, int second);
+
+ bool SetFromDateString(const std::string &date);
+ bool SetFromDBDate(const std::string &date);
+ bool SetFromDBTime(const std::string &time);
+ bool SetFromW3CDate(const std::string &date);
+ bool SetFromW3CDateTime(const std::string &date, bool ignoreTimezone = false);
+ bool SetFromUTCDateTime(const CDateTime &dateTime);
+ bool SetFromUTCDateTime(const time_t &dateTime);
+ bool SetFromRFC1123DateTime(const std::string &dateTime);
+
+ /*! \brief set from a database datetime format YYYY-MM-DD HH:MM:SS
+ \sa GetAsDBDateTime()
+ */
+ bool SetFromDBDateTime(const std::string &dateTime);
+
+ void GetAsSystemTime(KODI::TIME::SystemTime& time) const;
+ void GetAsTime(time_t& time) const;
+ void GetAsTm(tm& time) const;
+ void GetAsTimeStamp(KODI::TIME::FileTime& time) const;
+
+ CDateTime GetAsUTCDateTime() const;
+ std::string GetAsSaveString() const;
+ std::string GetAsDBDateTime() const;
+ std::string GetAsDBDate() const;
+ std::string GetAsDBTime() const;
+ std::string GetAsLocalizedDate(bool longDate=false) const;
+ std::string GetAsLocalizedDate(const std::string &strFormat) const;
+ std::string GetAsLocalizedTime(const std::string &format, bool withSeconds=true) const;
+ std::string GetAsLocalizedDateTime(bool longDate=false, bool withSeconds=true) const;
+ std::string GetAsLocalizedTime(TIME_FORMAT format, bool withSeconds = false) const;
+ std::string GetAsRFC1123DateTime() const;
+ std::string GetAsW3CDate() const;
+ std::string GetAsW3CDateTime(bool asUtc = false) const;
+
+ void SetValid(bool yesNo);
+ bool IsValid() const;
+
+ static void ResetTimezoneBias(void);
+ static CDateTimeSpan GetTimezoneBias(void);
+
+private:
+ bool ToFileTime(const KODI::TIME::SystemTime& time, KODI::TIME::FileTime& fileTime) const;
+ bool ToFileTime(const time_t& time, KODI::TIME::FileTime& fileTime) const;
+ bool ToFileTime(const tm& time, KODI::TIME::FileTime& fileTime) const;
+
+ void ToLargeInt(LARGE_INTEGER& time) const;
+ void FromLargeInt(const LARGE_INTEGER& time);
+
+private:
+ KODI::TIME::FileTime m_time;
+
+ typedef enum _STATE
+ {
+ invalid=0,
+ valid
+ } STATE;
+
+ STATE m_state;
+};
diff --git a/xbmc/addons/Addon.cpp b/xbmc/addons/Addon.cpp
new file mode 100644
index 0000000..90669bb
--- /dev/null
+++ b/xbmc/addons/Addon.cpp
@@ -0,0 +1,679 @@
+/*
+ * 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 "Addon.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/RepositoryUpdater.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/settings/AddonSettings.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "settings/Settings.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <ostream>
+#include <string.h>
+#include <utility>
+#include <vector>
+
+#ifdef HAS_PYTHON
+#include "interfaces/python/XBPython.h"
+#endif
+
+using XFILE::CDirectory;
+using XFILE::CFile;
+
+namespace ADDON
+{
+
+CAddon::CAddon(const AddonInfoPtr& addonInfo, AddonType addonType)
+ : m_addonInfo(addonInfo),
+ m_type(addonType == AddonType::UNKNOWN ? addonInfo->MainType() : addonType)
+{
+}
+
+AddonType CAddon::MainType() const
+{
+ return m_addonInfo->MainType();
+}
+
+bool CAddon::HasType(AddonType type) const
+{
+ return m_addonInfo->HasType(type);
+}
+
+bool CAddon::HasMainType(AddonType type) const
+{
+ return m_addonInfo->HasType(type, true);
+}
+
+const CAddonType* CAddon::Type(AddonType type) const
+{
+ return m_addonInfo->Type(type);
+}
+
+std::string CAddon::ID() const
+{
+ return m_addonInfo->ID();
+}
+
+std::string CAddon::Name() const
+{
+ return m_addonInfo->Name();
+}
+
+bool CAddon::IsBinary() const
+{
+ return m_addonInfo->IsBinary();
+}
+
+CAddonVersion CAddon::Version() const
+{
+ return m_addonInfo->Version();
+}
+
+CAddonVersion CAddon::MinVersion() const
+{
+ return m_addonInfo->MinVersion();
+}
+
+std::string CAddon::Summary() const
+{
+ return m_addonInfo->Summary();
+}
+
+std::string CAddon::Description() const
+{
+ return m_addonInfo->Description();
+}
+
+std::string CAddon::Path() const
+{
+ return m_addonInfo->Path();
+}
+
+std::string CAddon::Profile() const
+{
+ return m_addonInfo->ProfilePath();
+}
+
+std::string CAddon::Author() const
+{
+ return m_addonInfo->Author();
+}
+
+std::string CAddon::ChangeLog() const
+{
+ return m_addonInfo->ChangeLog();
+}
+
+std::string CAddon::Icon() const
+{
+ return m_addonInfo->Icon();
+}
+
+ArtMap CAddon::Art() const
+{
+ return m_addonInfo->Art();
+}
+
+std::vector<std::string> CAddon::Screenshots() const
+{
+ return m_addonInfo->Screenshots();
+}
+
+std::string CAddon::Disclaimer() const
+{
+ return m_addonInfo->Disclaimer();
+}
+
+AddonLifecycleState CAddon::LifecycleState() const
+{
+ return m_addonInfo->LifecycleState();
+}
+
+std::string CAddon::LifecycleStateDescription() const
+{
+ return m_addonInfo->LifecycleStateDescription();
+}
+
+CDateTime CAddon::InstallDate() const
+{
+ return m_addonInfo->InstallDate();
+}
+
+CDateTime CAddon::LastUpdated() const
+{
+ return m_addonInfo->LastUpdated();
+}
+
+CDateTime CAddon::LastUsed() const
+{
+ return m_addonInfo->LastUsed();
+}
+
+std::string CAddon::Origin() const
+{
+ return m_addonInfo->Origin();
+}
+
+std::string CAddon::OriginName() const
+{
+ return m_addonInfo->OriginName();
+}
+
+uint64_t CAddon::PackageSize() const
+{
+ return m_addonInfo->PackageSize();
+}
+
+const InfoMap& CAddon::ExtraInfo() const
+{
+ return m_addonInfo->ExtraInfo();
+}
+
+const std::vector<DependencyInfo>& CAddon::GetDependencies() const
+{
+ return m_addonInfo->GetDependencies();
+}
+
+std::string CAddon::FanArt() const
+{
+ auto it = m_addonInfo->Art().find("fanart");
+ return it != m_addonInfo->Art().end() ? it->second : "";
+}
+
+bool CAddon::MeetsVersion(const CAddonVersion& versionMin, const CAddonVersion& version) const
+{
+ return m_addonInfo->MeetsVersion(versionMin, version);
+}
+
+/**
+ * Settings Handling
+ */
+
+std::vector<AddonInstanceId> CAddon::GetKnownInstanceIds() const
+{
+ return m_addonInfo->GetKnownInstanceIds();
+}
+
+bool CAddon::SupportsMultipleInstances() const
+{
+ return m_addonInfo->SupportsMultipleInstances();
+}
+
+AddonInstanceSupport CAddon::InstanceUseType() const
+{
+ return m_addonInfo->InstanceUseType();
+}
+
+bool CAddon::SupportsInstanceSettings() const
+{
+ return m_addonInfo->SupportsInstanceSettings();
+}
+
+bool CAddon::DeleteInstanceSettings(AddonInstanceId instance)
+{
+ if (instance == ADDON_SETTINGS_ID)
+ return false;
+
+ const auto itr = m_settings.find(instance);
+ if (itr == m_settings.end())
+ return false;
+
+ if (CFile::Exists(itr->second.m_userSettingsPath))
+ CFile::Delete(itr->second.m_userSettingsPath);
+
+ ResetSettings(instance);
+
+ return true;
+}
+
+bool CAddon::CanHaveAddonOrInstanceSettings()
+{
+ return HasSettings(ADDON_SETTINGS_ID) || SupportsInstanceSettings();
+}
+
+bool CAddon::HasSettings(AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ return LoadSettings(false, true, id) && m_settings[id].m_addonSettings->HasSettings();
+}
+
+bool CAddon::SettingsInitialized(AddonInstanceId id /* = ADDON_SETTINGS_ID */) const
+{
+ const auto addonSettings = FindInstanceSettings(id);
+ return addonSettings && addonSettings->IsInitialized();
+}
+
+bool CAddon::SettingsLoaded(AddonInstanceId id /* = ADDON_SETTINGS_ID */) const
+{
+ const auto addonSettings = FindInstanceSettings(id);
+ return addonSettings && addonSettings->IsLoaded();
+}
+
+bool CAddon::LoadSettings(bool bForce,
+ bool loadUserSettings,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ if (SettingsInitialized(id) && !bForce)
+ return true;
+
+ const auto itr = m_settings.find(id);
+ if (itr != m_settings.end())
+ {
+ if (itr->second.m_loadSettingsFailed)
+ return false;
+ }
+ else
+ {
+ InitSettings(id);
+ }
+
+ // assume loading settings fails
+ m_settings[id].m_loadSettingsFailed = true;
+
+ // reset the settings if we are forced to
+ if (SettingsInitialized(id) && bForce)
+ GetSettings(id)->Uninitialize();
+
+ // load the settings definition XML file
+ const auto addonSettingsDefinitionFile = m_settings[id].m_addonSettingsPath;
+ CXBMCTinyXML addonSettingsDefinitionDoc;
+ if (!addonSettingsDefinitionDoc.LoadFile(addonSettingsDefinitionFile))
+ {
+ if (CFile::Exists(addonSettingsDefinitionFile))
+ {
+ CLog::Log(LOGERROR, "CAddon[{}]: unable to load: {}, Line {}\n{}", ID(),
+ addonSettingsDefinitionFile, addonSettingsDefinitionDoc.ErrorRow(),
+ addonSettingsDefinitionDoc.ErrorDesc());
+ }
+
+ return false;
+ }
+
+ // initialize the settings definition
+ if (!GetSettings(id)->Initialize(addonSettingsDefinitionDoc))
+ {
+ CLog::Log(LOGERROR, "CAddon[{}]: failed to initialize addon settings", ID());
+ return false;
+ }
+
+ // loading settings didn't fail
+ m_settings[id].m_loadSettingsFailed = false;
+
+ // load user settings / values
+ if (loadUserSettings)
+ LoadUserSettings(id);
+
+ return true;
+}
+
+bool CAddon::HasUserSettings(AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ if (!LoadSettings(false, true, id))
+ return false;
+
+ return SettingsLoaded(id) && m_settings[id].m_hasUserSettings;
+}
+
+bool CAddon::ReloadSettings(AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ return LoadSettings(true, true, id);
+}
+
+void CAddon::ResetSettings(AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ m_settings.erase(id);
+}
+
+bool CAddon::LoadUserSettings(AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ if (!SettingsInitialized(id) && !InitSettings(id))
+ return false;
+
+ CSettingsData& data = m_settings[id];
+
+ data.m_hasUserSettings = false;
+
+ // there are no user settings
+ if (!CFile::Exists(data.m_userSettingsPath))
+ {
+ // mark the settings as loaded
+ GetSettings(id)->SetLoaded();
+ return true;
+ }
+
+ CXBMCTinyXML doc;
+ if (!doc.LoadFile(data.m_userSettingsPath))
+ {
+ CLog::Log(LOGERROR, "CAddon[{}]: failed to load addon settings from {}", ID(),
+ data.m_userSettingsPath);
+ return false;
+ }
+
+ return SettingsFromXML(doc, false, id);
+}
+
+bool CAddon::HasSettingsToSave(AddonInstanceId id /* = ADDON_SETTINGS_ID */) const
+{
+ return SettingsLoaded(id);
+}
+
+void CAddon::SaveSettings(AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ if (!HasSettingsToSave(id))
+ return; // no settings to save
+
+ CSettingsData& data = m_settings[id];
+
+ // break down the path into directories
+ const std::string strAddon = URIUtils::GetDirectory(data.m_userSettingsPath);
+ const std::string strRoot = URIUtils::GetDirectory(strAddon);
+
+ // create the individual folders
+ if (!CDirectory::Exists(strRoot))
+ CDirectory::Create(strRoot);
+ if (!CDirectory::Exists(strAddon))
+ CDirectory::Create(strAddon);
+
+ // create the XML file
+ CXBMCTinyXML doc;
+ if (SettingsToXML(doc, id))
+ doc.SaveFile(data.m_userSettingsPath);
+
+ data.m_hasUserSettings = true;
+
+ //push the settings changes to the running addon instance
+ CServiceBroker::GetAddonMgr().ReloadSettings(ID(), id);
+#ifdef HAS_PYTHON
+ CServiceBroker::GetXBPython().OnSettingsChanged(ID());
+#endif
+}
+
+std::string CAddon::GetSetting(const std::string& key, AddonInstanceId id)
+{
+ if (key.empty() || !LoadSettings(false, true, id))
+ return ""; // no settings available
+
+ auto setting = m_settings[id].m_addonSettings->GetSetting(key);
+ if (setting != nullptr)
+ return setting->ToString();
+
+ return "";
+}
+
+template<class TSetting>
+bool GetSettingValue(CAddon& addon,
+ AddonInstanceId instanceId,
+ const std::string& key,
+ typename TSetting::Value& value)
+{
+ if (key.empty() || !addon.HasSettings(instanceId))
+ return false;
+
+ auto setting = addon.GetSettings(instanceId)->GetSetting(key);
+ if (setting == nullptr || setting->GetType() != TSetting::Type())
+ return false;
+
+ value = std::static_pointer_cast<TSetting>(setting)->GetValue();
+ return true;
+}
+
+bool CAddon::GetSettingBool(const std::string& key,
+ bool& value,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ return GetSettingValue<CSettingBool>(*this, id, key, value);
+}
+
+bool CAddon::GetSettingInt(const std::string& key,
+ int& value,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ return GetSettingValue<CSettingInt>(*this, id, key, value);
+}
+
+bool CAddon::GetSettingNumber(const std::string& key,
+ double& value,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ return GetSettingValue<CSettingNumber>(*this, id, key, value);
+}
+
+bool CAddon::GetSettingString(const std::string& key,
+ std::string& value,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ return GetSettingValue<CSettingString>(*this, id, key, value);
+}
+
+void CAddon::UpdateSetting(const std::string& key,
+ const std::string& value,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ if (key.empty() || !LoadSettings(false, true, id))
+ return;
+
+ // try to get the setting
+ auto setting = m_settings[id].m_addonSettings->GetSetting(key);
+
+ // if the setting doesn't exist, try to add it
+ if (setting == nullptr)
+ {
+ setting = m_settings[id].m_addonSettings->AddSetting(key, value);
+ if (setting == nullptr)
+ {
+ CLog::Log(LOGERROR, "CAddon[{}]: failed to add undefined setting \"{}\"", ID(), key);
+ return;
+ }
+ }
+
+ setting->FromString(value);
+}
+
+template<class TSetting>
+bool UpdateSettingValue(CAddon& addon,
+ AddonInstanceId instanceId,
+ const std::string& key,
+ typename TSetting::Value value)
+{
+ if (key.empty() || !addon.HasSettings(instanceId))
+ return false;
+
+ // try to get the setting
+ auto setting = addon.GetSettings(instanceId)->GetSetting(key);
+
+ // if the setting doesn't exist, try to add it
+ if (setting == nullptr)
+ {
+ setting = addon.GetSettings(instanceId)->AddSetting(key, value);
+ if (setting == nullptr)
+ {
+ CLog::Log(LOGERROR, "CAddon[{}]: failed to add undefined setting \"{}\"", addon.ID(), key);
+ return false;
+ }
+ }
+
+ if (setting->GetType() != TSetting::Type())
+ return false;
+
+ return std::static_pointer_cast<TSetting>(setting)->SetValue(value);
+}
+
+bool CAddon::UpdateSettingBool(const std::string& key,
+ bool value,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ return UpdateSettingValue<CSettingBool>(*this, id, key, value);
+}
+
+bool CAddon::UpdateSettingInt(const std::string& key,
+ int value,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ return UpdateSettingValue<CSettingInt>(*this, id, key, value);
+}
+
+bool CAddon::UpdateSettingNumber(const std::string& key,
+ double value,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ return UpdateSettingValue<CSettingNumber>(*this, id, key, value);
+}
+
+bool CAddon::UpdateSettingString(const std::string& key,
+ const std::string& value,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ return UpdateSettingValue<CSettingString>(*this, id, key, value);
+}
+
+bool CAddon::SettingsFromXML(const CXBMCTinyXML& doc,
+ bool loadDefaults,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ if (doc.RootElement() == nullptr)
+ return false;
+
+ // if the settings haven't been initialized yet, try it from the given XML
+ if (!SettingsInitialized(id))
+ {
+ if (!GetSettings(id)->Initialize(doc))
+ {
+ CLog::Log(LOGERROR, "CAddon[{}]: failed to initialize addon settings", ID());
+ return false;
+ }
+ }
+
+ // reset all setting values to their default value
+ if (loadDefaults)
+ GetSettings(id)->SetDefaults();
+
+ // try to load the setting's values from the given XML
+ if (!GetSettings(id)->Load(doc))
+ {
+ CLog::Log(LOGERROR, "CAddon[{}]: failed to load user settings", ID());
+ return false;
+ }
+
+ m_settings[id].m_hasUserSettings = true;
+
+ return true;
+}
+
+bool CAddon::SettingsToXML(CXBMCTinyXML& doc, AddonInstanceId id /* = ADDON_SETTINGS_ID */) const
+{
+ if (!SettingsInitialized(id))
+ return false;
+
+ if (!m_settings[id].m_addonSettings->Save(doc))
+ {
+ CLog::Log(LOGERROR, "CAddon[{}]: failed to save addon settings", ID());
+ return false;
+ }
+
+ return true;
+}
+
+bool CAddon::InitSettings(AddonInstanceId id)
+{
+ // initialize addon settings if necessary
+ if (!FindInstanceSettings(id))
+ {
+ CSettingsData data;
+
+ data.m_addonSettings =
+ std::make_shared<CAddonSettings>(enable_shared_from_this::shared_from_this(), id);
+ if (id == ADDON_SETTINGS_ID)
+ {
+ data.m_addonSettingsPath =
+ URIUtils::AddFileToFolder(m_addonInfo->Path(), "resources", "settings.xml");
+ data.m_userSettingsPath = URIUtils::AddFileToFolder(Profile(), "settings.xml");
+ }
+ else
+ {
+ data.m_addonSettingsPath =
+ URIUtils::AddFileToFolder(m_addonInfo->Path(), "resources", "instance-settings.xml");
+ data.m_userSettingsPath =
+ URIUtils::AddFileToFolder(Profile(), StringUtils::Format("instance-settings-{}.xml", id));
+ }
+
+ m_settings[id] = std::move(data);
+ return true;
+ }
+
+ return false;
+}
+
+std::shared_ptr<CAddonSettings> CAddon::FindInstanceSettings(AddonInstanceId id) const
+{
+ const auto itr = m_settings.find(id);
+ if (itr == m_settings.end())
+ return nullptr;
+
+ return itr->second.m_addonSettings;
+}
+
+std::shared_ptr<CAddonSettings> CAddon::GetSettings(AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ if (InitSettings(id))
+ LoadSettings(false, true, id);
+
+ return m_settings[id].m_addonSettings;
+}
+
+std::string CAddon::LibPath() const
+{
+ // Get library related to given type on construction
+ std::string libName = m_addonInfo->Type(m_type)->LibName();
+ if (libName.empty())
+ {
+ // If not present fallback to master library
+ libName = m_addonInfo->LibName();
+ if (libName.empty())
+ return "";
+ }
+ return URIUtils::AddFileToFolder(m_addonInfo->Path(), libName);
+}
+
+CAddonVersion CAddon::GetDependencyVersion(const std::string& dependencyID) const
+{
+ return m_addonInfo->DependencyVersion(dependencyID);
+}
+
+void OnPreInstall(const AddonPtr& addon)
+{
+ //Fallback to the pre-install callback in the addon.
+ //! @bug If primary extension point have changed we're calling the wrong method.
+ addon->OnPreInstall();
+}
+
+void OnPostInstall(const AddonPtr& addon, bool update, bool modal)
+{
+ addon->OnPostInstall(update, modal);
+}
+
+void OnPreUnInstall(const AddonPtr& addon)
+{
+ addon->OnPreUnInstall();
+}
+
+void OnPostUnInstall(const AddonPtr& addon)
+{
+ addon->OnPostUnInstall();
+}
+
+} // namespace ADDON
diff --git a/xbmc/addons/Addon.h b/xbmc/addons/Addon.h
new file mode 100644
index 0000000..a09b8db
--- /dev/null
+++ b/xbmc/addons/Addon.h
@@ -0,0 +1,490 @@
+/*
+ * 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 "addons/IAddon.h"
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+class CXBMCTinyXML;
+
+namespace ADDON
+{
+
+enum class AddonType;
+class CAddonType;
+
+class CAddonInfo;
+using AddonInfoPtr = std::shared_ptr<CAddonInfo>;
+
+void OnPreInstall(const AddonPtr& addon);
+void OnPostInstall(const AddonPtr& addon, bool update, bool modal);
+void OnPreUnInstall(const AddonPtr& addon);
+void OnPostUnInstall(const AddonPtr& addon);
+
+class CAddon : public IAddon
+{
+public:
+ explicit CAddon(const AddonInfoPtr& addonInfo, AddonType addonType);
+ ~CAddon() override = default;
+
+ /**
+ * @brief To get the main type of this addon
+ *
+ * This is the first type defined in **addon.xml** and can be different to the
+ * on @ref Type() defined type.
+ *
+ * @return The used main type of addon
+ */
+ AddonType MainType() const override;
+
+ /**
+ * @brief To get the on this CAddon class processed addon type
+ *
+ * @return For this class used addon type
+ */
+ AddonType Type() const override { return m_type; }
+
+ /**
+ * @brief To check complete addon (not only this) contains a type
+ *
+ * @note This can be overridden by a child e.g. plugin to check for subtype
+ * e.g. video or music.
+ *
+ * @param[in] type The to checked type identifier
+ * @return true in case the wanted type is supported, false if not
+ */
+ bool HasType(AddonType type) const override;
+
+ /**
+ * @brief To check complete addon (not only this) has a specific type
+ * defined in its first extension point including the provided subcontent
+ * e.g. video or audio
+ *
+ * @param[in] type Type identifier to be checked
+ * @return true in case the wanted type is the main type, false if not
+ */
+ bool HasMainType(AddonType type) const override;
+
+ /**
+ * @brief The get for given addon type information and extension data
+ *
+ * @param[in] type The wanted type data
+ * @return addon type class with @ref CAddonExtensions as information
+ *
+ * @note This function return never a "nullptr", in case the wanted type is
+ * not supported, becomes a dummy of @ref CAddonType given.
+ *
+ * ------------------------------------------------------------------------
+ *
+ * **Example:**
+ * ~~~~~~~~~~~~~{.cpp}
+ * // To get e.g. <extension ... name="blablabla" /> from addon.xml
+ * std::string name = Type(ADDON_...)->GetValue("@name").asString();
+ * ~~~~~~~~~~~~~
+ *
+ */
+ const CAddonType* Type(AddonType type) const;
+
+ std::string ID() const override;
+ std::string Name() const override;
+ bool IsInUse() const override { return false; }
+ bool IsBinary() const override;
+ CAddonVersion Version() const override;
+ CAddonVersion MinVersion() const override;
+ std::string Summary() const override;
+ std::string Description() const override;
+ std::string Path() const override;
+ std::string Profile() const override;
+ std::string LibPath() const override;
+ std::string Author() const override;
+ std::string ChangeLog() const override;
+ std::string Icon() const override;
+ ArtMap Art() const override;
+ std::vector<std::string> Screenshots() const override;
+ std::string Disclaimer() const override;
+ AddonLifecycleState LifecycleState() const override;
+ std::string LifecycleStateDescription() const override;
+ CDateTime InstallDate() const override;
+ CDateTime LastUpdated() const override;
+ CDateTime LastUsed() const override;
+ std::string Origin() const override;
+ std::string OriginName() const override;
+ uint64_t PackageSize() const override;
+ const InfoMap& ExtraInfo() const override;
+ const std::vector<DependencyInfo>& GetDependencies() const override;
+ std::string FanArt() const override;
+
+ /*!
+ * \brief Check add-on for support from independent work instances.
+ *
+ * \return true if the add-on supports individual add-on instances, false otherwise
+ */
+ bool SupportsMultipleInstances() const override;
+
+ /*!
+ * \brief Return the used instance path type of the add-on type.
+ *
+ * \return The route used to instance handling, @ref AddonInstanceUse::NONE if not supported.
+ */
+ AddonInstanceSupport InstanceUseType() const override;
+
+ /*!
+ * \brief Gives active, independently working instance identifiers for this add-on.
+ *
+ * This function is supported if add-on type has defined
+ * @ref AddonInstanceUse::BY_SETTINGS and the associated settings
+ * are available.
+ *
+ * \return List of active instance identifiers.
+ */
+ std::vector<AddonInstanceId> GetKnownInstanceIds() const override;
+
+ /*!
+ * \brief Check whether the add-on supports individual settings per add-on instance.
+ *
+ * This function is supported if add-on type has defined
+ * @ref AddonInstanceUse::BY_SETTINGS
+ *
+ * \return true if the add-on supports individual settings per add-on instance, false otherwise
+ */
+ bool SupportsInstanceSettings() const override;
+
+ /*!
+ * \brief Delete selected instance settings from storage.
+ *
+ * The related instance-settings-[0-9...].xml file will be deleted by this method.
+ *
+ * \param[in] instance Instance identifier to use.
+ * \return true on success, false otherwise.
+ */
+ bool DeleteInstanceSettings(AddonInstanceId instance) override;
+
+ /*!
+ * \brief Check whether this add-on can be configured by the user.
+ *
+ * \return true if the add-on has settings, false otherwise
+ */
+ bool CanHaveAddonOrInstanceSettings() override;
+
+ /*!
+ * \brief Check whether this add-on can be configured by the user.
+ *
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if the add-on has settings, false otherwise
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasUserSettings, GetSetting, UpdateSetting
+ */
+ bool HasSettings(AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Check whether the user has configured this add-on or not.
+ *
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if previously saved settings are found, false otherwise
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasSettings, GetSetting, UpdateSetting
+ */
+ bool HasUserSettings(AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Save any user configured settings
+ *
+ * \param[in] instance Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ *
+ * \sa LoadSettings, LoadUserSettings, HasSettings, HasUserSettings, GetSetting, UpdateSetting
+ */
+ void SaveSettings(AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Update a user-configured setting with a new value.
+ *
+ * \param[in] key the id of the setting to update
+ * \param[in] value the value that the setting should take
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasSettings, HasUserSettings, GetSetting
+ */
+ void UpdateSetting(const std::string& key,
+ const std::string& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Update a user-configured setting with a new boolean value.
+ *
+ * \param[in] key the id of the setting to update
+ * \param[in] value the value that the setting should take
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasSettings, HasUserSettings, GetSetting
+ */
+ bool UpdateSettingBool(const std::string& key,
+ bool value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Update a user-configured setting with a new integer value.
+ *
+ * \param[in] key the id of the setting to update
+ * \param[in] value the value that the setting should take
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasSettings, HasUserSettings, GetSetting
+ */
+ bool UpdateSettingInt(const std::string& key,
+ int value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Update a user-configured setting with a new number value.
+ *
+ * \param[in] key the id of the setting to update
+ * \param[in] value the value that the setting should take
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasSettings, HasUserSettings, GetSetting
+ */
+ bool UpdateSettingNumber(const std::string& key,
+ double value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Update a user-configured setting with a new string value.
+ *
+ * \param[in] key the id of the setting to update
+ * \param[in] value the value that the setting should take
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasSettings, HasUserSettings, GetSetting
+ */
+ bool UpdateSettingString(const std::string& key,
+ const std::string& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Retrieve a particular settings value.
+ *
+ * If a previously configured user setting is available, we return it's value, else we return the default (if available).
+ *
+ * \param[in] key the id of the setting to retrieve
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return the current value of the setting, or the default if the setting has yet to be configured.
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasSettings, HasUserSettings, UpdateSetting
+ */
+ std::string GetSetting(const std::string& key, AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Retrieve a particular settings value as boolean.
+ *
+ * If a previously configured user setting is available, we return it's value, else we return the default (if available).
+ *
+ * \param[in] key the id of the setting to retrieve
+ * \param[out] value the current value of the setting, or the default if the setting has yet to be configured
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if the setting's value was retrieved, false otherwise.
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasSettings, HasUserSettings, UpdateSetting
+ */
+ bool GetSettingBool(const std::string& key,
+ bool& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Retrieve a particular settings value as integer.
+ *
+ * If a previously configured user setting is available, we return it's value, else we return the default (if available)
+ *
+ * \param[in] key the id of the setting to retrieve
+ * \param[out] value the current value of the setting, or the default if the setting has yet to be configured
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if the setting's value was retrieved, false otherwise.
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasSettings, HasUserSettings, UpdateSetting
+ */
+ bool GetSettingInt(const std::string& key,
+ int& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Retrieve a particular settings value as number.
+ *
+ * If a previously configured user setting is available, we return it's value, else we return the default (if available)
+ *
+ * \param[in] key the id of the setting to retrieve
+ * \param[out] value the current value of the setting, or the default if the setting has yet to be configured
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if the setting's value was retrieved, false otherwise.
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasSettings, HasUserSettings, UpdateSetting
+ */
+ bool GetSettingNumber(const std::string& key,
+ double& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*!
+ * \brief Retrieve a particular settings value as string
+ *
+ * If a previously configured user setting is available, we return it's value, else we return the default (if available)
+ *
+ * \param[in] key the id of the setting to retrieve
+ * \param[out] value the current value of the setting, or the default if the setting has yet to be configured
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if the setting's value was retrieved, false otherwise.
+ *
+ * \sa LoadSettings, LoadUserSettings, SaveSettings, HasSettings, HasUserSettings, UpdateSetting
+ */
+ bool GetSettingString(const std::string& key,
+ std::string& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ std::shared_ptr<CAddonSettings> GetSettings(AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*! \brief get the required version of a dependency.
+ \param dependencyID the addon ID of the dependency.
+ \return the version this addon requires.
+ */
+ CAddonVersion GetDependencyVersion(const std::string& dependencyID) const override;
+
+ /*! \brief return whether or not this addon satisfies the given version requirements
+ \param version the version to meet.
+ \return true if min_version <= version <= current_version, false otherwise.
+ */
+ bool MeetsVersion(const CAddonVersion& versionMin, const CAddonVersion& version) const override;
+
+ bool ReloadSettings(AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ void ResetSettings(AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ /*! \brief retrieve the running instance of an add-on if it persists while running.
+ */
+ AddonPtr GetRunningInstance() const override { return AddonPtr(); }
+
+ void OnPreInstall() override{};
+ void OnPostInstall(bool update, bool modal) override{};
+ void OnPreUnInstall() override{};
+ void OnPostUnInstall() override{};
+
+protected:
+ /*!
+ * \brief Whether or not the settings have been initialized.
+ *
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if settings initialize was successfull
+ */
+ virtual bool SettingsInitialized(AddonInstanceId id = ADDON_SETTINGS_ID) const;
+
+ /*!
+ * \brief Whether or not the settings have been loaded.
+ *
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if settings are loaded correct
+ */
+ virtual bool SettingsLoaded(AddonInstanceId id = ADDON_SETTINGS_ID) const;
+
+ /*!
+ * \brief Load the default settings and override these with any previously configured user settings
+ *
+ * \param[in] bForce force the load of settings even if they are already loaded (reload)
+ * \param[in] loadUserSettings whether or not to load user settings
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if settings exist, false otherwise
+ *
+ * \sa LoadUserSettings, SaveSettings, HasSettings, HasUserSettings, GetSetting, UpdateSetting
+ */
+ bool LoadSettings(bool bForce, bool loadUserSettings, AddonInstanceId id = ADDON_SETTINGS_ID);
+
+ /*!
+ * \brief Load the user settings
+ *
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if user settings exist, false otherwise
+ *
+ * \sa LoadSettings, SaveSettings, HasSettings, HasUserSettings, GetSetting, UpdateSetting
+ */
+ virtual bool LoadUserSettings(AddonInstanceId id = ADDON_SETTINGS_ID);
+
+ /*!
+ * \brief Whether there are settings to be saved
+ *
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if settings has to save
+ *
+ * \sa SaveSettings
+ */
+ virtual bool HasSettingsToSave(AddonInstanceId id = ADDON_SETTINGS_ID) const;
+
+ /*!
+ * \brief Parse settings from an XML document
+ *
+ * \param[in] doc XML document to parse for settings
+ * \param[in] loadDefaults if true, the default attribute is used and settings are reset prior to parsing, else the value attribute is used.
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if settings are loaded, false otherwise
+ *
+ * \sa SettingsToXML
+ */
+ virtual bool SettingsFromXML(const CXBMCTinyXML& doc,
+ bool loadDefaults,
+ AddonInstanceId id = ADDON_SETTINGS_ID);
+
+ /*!
+ * \brief Write settings into an XML document
+ *
+ * \param[out] doc XML document to receive the settings
+ * \param[in] id Instance identifier to use, use @ref ADDON_SETTINGS_ID
+ * to denote global add-on settings from settings.xml.
+ * \return true if settings are saved, false otherwise
+ *
+ * \sa SettingsFromXML
+ */
+ virtual bool SettingsToXML(CXBMCTinyXML& doc, AddonInstanceId id = ADDON_SETTINGS_ID) const;
+
+ const AddonInfoPtr m_addonInfo;
+
+private:
+ struct CSettingsData
+ {
+ bool m_loadSettingsFailed{false};
+ bool m_hasUserSettings{false};
+ std::string m_addonSettingsPath;
+ std::string m_userSettingsPath;
+ std::shared_ptr<CAddonSettings> m_addonSettings;
+ };
+
+ bool InitSettings(AddonInstanceId id);
+ std::shared_ptr<CAddonSettings> FindInstanceSettings(AddonInstanceId id) const;
+
+ mutable std::unordered_map<AddonInstanceId, CSettingsData> m_settings;
+ const AddonType m_type;
+};
+
+}; // namespace ADDON
diff --git a/xbmc/addons/AddonBindings.cmake b/xbmc/addons/AddonBindings.cmake
new file mode 100644
index 0000000..b89478e
--- /dev/null
+++ b/xbmc/addons/AddonBindings.cmake
@@ -0,0 +1,11 @@
+# List contains only add-on related headers not present in
+# ./addons/kodi-dev-kit/include/kodi
+#
+
+# Keep this in alphabetical order
+set(CORE_ADDON_BINDINGS_FILES
+)
+
+set(CORE_ADDON_BINDINGS_DIRS
+ ${CORE_SOURCE_DIR}/xbmc/addons/kodi-dev-kit/include/kodi/
+)
diff --git a/xbmc/addons/AddonBuilder.cpp b/xbmc/addons/AddonBuilder.cpp
new file mode 100644
index 0000000..7d52ee0
--- /dev/null
+++ b/xbmc/addons/AddonBuilder.cpp
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016-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 "addons/AddonBuilder.h"
+
+#include "ServiceBroker.h"
+#include "addons/ContextMenuAddon.h"
+#include "addons/FontResource.h"
+#include "addons/GameResource.h"
+#include "addons/ImageResource.h"
+#include "addons/LanguageResource.h"
+#include "addons/PluginSource.h"
+#include "addons/Repository.h"
+#include "addons/Scraper.h"
+#include "addons/Service.h"
+#include "addons/Skin.h"
+#include "addons/UISoundsResource.h"
+#include "addons/Webinterface.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "games/addons/GameClient.h"
+#include "games/controllers/Controller.h"
+#include "pvr/addons/PVRClient.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI;
+
+namespace ADDON
+{
+
+AddonPtr CAddonBuilder::Generate(const AddonInfoPtr& info, AddonType type)
+{
+ if (!info || info->ID().empty())
+ return AddonPtr();
+
+ if (type == AddonType::UNKNOWN)
+ type = info->MainType();
+ if (type == AddonType::UNKNOWN)
+ return std::make_shared<CAddon>(info, AddonType::UNKNOWN);
+
+ // Handle screensaver special cases
+ if (type == AddonType::SCREENSAVER)
+ {
+ // built in screensaver or python screensaver
+ if (StringUtils::StartsWithNoCase(info->ID(), "screensaver.xbmc.builtin.") ||
+ URIUtils::HasExtension(info->LibName(), ".py"))
+ return std::make_shared<CAddon>(info, type);
+ }
+
+ // Handle audio encoder special cases
+ if (type == AddonType::AUDIOENCODER)
+ {
+ // built in audio encoder
+ if (StringUtils::StartsWithNoCase(info->ID(), "audioencoder.kodi.builtin."))
+ return std::make_shared<CAddonDll>(info, type);
+ }
+
+ switch (type)
+ {
+ case AddonType::AUDIODECODER:
+ case AddonType::AUDIOENCODER:
+ case AddonType::IMAGEDECODER:
+ case AddonType::INPUTSTREAM:
+ case AddonType::PERIPHERALDLL:
+ case AddonType::PVRDLL:
+ case AddonType::VFS:
+ case AddonType::VISUALIZATION:
+ case AddonType::SCREENSAVER:
+ return std::make_shared<CAddonDll>(info, type);
+ case AddonType::GAMEDLL:
+ return std::make_shared<GAME::CGameClient>(info);
+ case AddonType::PLUGIN:
+ case AddonType::SCRIPT:
+ return std::make_shared<CPluginSource>(info, type);
+ case AddonType::SCRIPT_LIBRARY:
+ case AddonType::SCRIPT_LYRICS:
+ case AddonType::SCRIPT_MODULE:
+ case AddonType::SUBTITLE_MODULE:
+ case AddonType::SCRIPT_WEATHER:
+ return std::make_shared<CAddon>(info, type);
+ case AddonType::WEB_INTERFACE:
+ return std::make_shared<CWebinterface>(info);
+ case AddonType::SERVICE:
+ return std::make_shared<CService>(info);
+ case AddonType::SCRAPER_ALBUMS:
+ case AddonType::SCRAPER_ARTISTS:
+ case AddonType::SCRAPER_MOVIES:
+ case AddonType::SCRAPER_MUSICVIDEOS:
+ case AddonType::SCRAPER_TVSHOWS:
+ case AddonType::SCRAPER_LIBRARY:
+ return std::make_shared<CScraper>(info, type);
+ case AddonType::SKIN:
+ return std::make_shared<CSkinInfo>(info);
+ case AddonType::RESOURCE_FONT:
+ return std::make_shared<CFontResource>(info);
+ case AddonType::RESOURCE_IMAGES:
+ return std::make_shared<CImageResource>(info);
+ case AddonType::RESOURCE_GAMES:
+ return std::make_shared<CGameResource>(info);
+ case AddonType::RESOURCE_LANGUAGE:
+ return std::make_shared<CLanguageResource>(info);
+ case AddonType::RESOURCE_UISOUNDS:
+ return std::make_shared<CUISoundsResource>(info);
+ case AddonType::REPOSITORY:
+ return std::make_shared<CRepository>(info);
+ case AddonType::CONTEXTMENU_ITEM:
+ return std::make_shared<CContextMenuAddon>(info);
+ case AddonType::GAME_CONTROLLER:
+ return std::make_shared<GAME::CController>(info);
+ default:
+ break;
+ }
+ return AddonPtr();
+}
+
+}
diff --git a/xbmc/addons/AddonBuilder.h b/xbmc/addons/AddonBuilder.h
new file mode 100644
index 0000000..dfc4865
--- /dev/null
+++ b/xbmc/addons/AddonBuilder.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016-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 <memory>
+
+namespace ADDON
+{
+enum class AddonType;
+
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+
+class CAddonInfo;
+using AddonInfoPtr = std::shared_ptr<CAddonInfo>;
+
+class CAddonBuilder
+{
+public:
+ static AddonPtr Generate(const AddonInfoPtr& info, AddonType type);
+};
+
+} // namespace ADDON
diff --git a/xbmc/addons/AddonDatabase.cpp b/xbmc/addons/AddonDatabase.cpp
new file mode 100644
index 0000000..29837be
--- /dev/null
+++ b/xbmc/addons/AddonDatabase.cpp
@@ -0,0 +1,1254 @@
+/*
+ * 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 "AddonDatabase.h"
+
+#include "XBDateTime.h"
+#include "addons/AddonBuilder.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonInfoBuilder.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dbwrappers/dataset.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/JSONVariantWriter.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <utility>
+
+using namespace ADDON;
+
+std::string CAddonDatabaseSerializer::SerializeMetadata(const CAddonInfo& addon)
+{
+ CVariant variant;
+ variant["author"] = addon.Author();
+ variant["disclaimer"] = addon.Disclaimer();
+ variant["lifecycletype"] = static_cast<unsigned int>(addon.LifecycleState());
+ variant["lifecycledesc"] = addon.LifecycleStateDescription();
+ variant["size"] = addon.PackageSize();
+
+ variant["path"] = addon.Path();
+ variant["icon"] = addon.Icon();
+
+ variant["art"] = CVariant(CVariant::VariantTypeObject);
+ for (const auto& item : addon.Art())
+ variant["art"][item.first] = item.second;
+
+ variant["screenshots"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& item : addon.Screenshots())
+ variant["screenshots"].push_back(item);
+
+ variant["extensions"] = CVariant(CVariant::VariantTypeArray);
+ variant["extensions"].push_back(SerializeExtensions(*addon.Type(addon.MainType())));
+
+ variant["dependencies"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& dep : addon.GetDependencies())
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["addonId"] = dep.id;
+ info["version"] = dep.version.asString();
+ info["minversion"] = dep.versionMin.asString();
+ info["optional"] = dep.optional;
+ variant["dependencies"].push_back(std::move(info));
+ }
+
+ variant["extrainfo"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& kv : addon.ExtraInfo())
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["key"] = kv.first;
+ info["value"] = kv.second;
+ variant["extrainfo"].push_back(std::move(info));
+ }
+
+ std::string json;
+ CJSONVariantWriter::Write(variant, json, true);
+ return json;
+}
+
+CVariant CAddonDatabaseSerializer::SerializeExtensions(const CAddonExtensions& addonType)
+{
+ CVariant variant;
+ variant["type"] = addonType.m_point;
+
+ variant["values"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& value : addonType.m_values)
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["id"] = value.first;
+ info["content"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& content : value.second)
+ {
+ CVariant contentEntry(CVariant::VariantTypeObject);
+ contentEntry["key"] = content.first;
+ contentEntry["value"] = content.second.str;
+ info["content"].push_back(std::move(contentEntry));
+ }
+
+ variant["values"].push_back(std::move(info));
+ }
+
+ variant["children"] = CVariant(CVariant::VariantTypeArray);
+ for (auto& child : addonType.m_children)
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["id"] = child.first;
+ info["child"] = SerializeExtensions(child.second);
+ variant["children"].push_back(std::move(info));
+ }
+
+ return variant;
+}
+
+void CAddonDatabaseSerializer::DeserializeMetadata(const std::string& document,
+ CAddonInfoBuilderFromDB& builder)
+{
+ CVariant variant;
+ if (!CJSONVariantParser::Parse(document, variant))
+ return;
+
+ builder.SetAuthor(variant["author"].asString());
+ builder.SetDisclaimer(variant["disclaimer"].asString());
+ builder.SetLifecycleState(
+ static_cast<AddonLifecycleState>(variant["lifecycletype"].asUnsignedInteger()),
+ variant["lifecycledesc"].asString());
+ builder.SetPackageSize(variant["size"].asUnsignedInteger());
+
+ builder.SetPath(variant["path"].asString());
+ builder.SetIcon(variant["icon"].asString());
+
+ std::map<std::string, std::string> art;
+ for (auto it = variant["art"].begin_map(); it != variant["art"].end_map(); ++it)
+ art.emplace(it->first, it->second.asString());
+ builder.SetArt(std::move(art));
+
+ std::vector<std::string> screenshots;
+ for (auto it = variant["screenshots"].begin_array(); it != variant["screenshots"].end_array(); ++it)
+ screenshots.push_back(it->asString());
+ builder.SetScreenshots(std::move(screenshots));
+
+ CAddonType addonType;
+ DeserializeExtensions(variant["extensions"][0], addonType);
+ addonType.m_type = CAddonInfo::TranslateType(addonType.m_point);
+ builder.SetExtensions(std::move(addonType));
+
+ {
+ std::vector<DependencyInfo> deps;
+ for (auto it = variant["dependencies"].begin_array(); it != variant["dependencies"].end_array(); ++it)
+ {
+ deps.emplace_back((*it)["addonId"].asString(), CAddonVersion((*it)["minversion"].asString()),
+ CAddonVersion((*it)["version"].asString()), (*it)["optional"].asBoolean());
+ }
+ builder.SetDependencies(std::move(deps));
+ }
+
+ InfoMap extraInfo;
+ for (auto it = variant["extrainfo"].begin_array(); it != variant["extrainfo"].end_array(); ++it)
+ extraInfo.emplace((*it)["key"].asString(), (*it)["value"].asString());
+ builder.SetExtrainfo(std::move(extraInfo));
+}
+
+void CAddonDatabaseSerializer::DeserializeExtensions(const CVariant& variant,
+ CAddonExtensions& addonType)
+{
+ addonType.m_point = variant["type"].asString();
+
+ for (auto value = variant["values"].begin_array(); value != variant["values"].end_array();
+ ++value)
+ {
+ std::string id = (*value)["id"].asString();
+ std::vector<std::pair<std::string, SExtValue>> extValues;
+ for (auto content = (*value)["content"].begin_array();
+ content != (*value)["content"].end_array(); ++content)
+ {
+ extValues.emplace_back((*content)["key"].asString(), SExtValue{(*content)["value"].asString()});
+ }
+
+ addonType.m_values.emplace_back(id, extValues);
+ }
+
+ for (auto child = variant["children"].begin_array(); child != variant["children"].end_array();
+ ++child)
+ {
+ CAddonExtensions childExt;
+ DeserializeExtensions((*child)["child"], childExt);
+ std::string id = (*child)["id"].asString();
+ addonType.m_children.emplace_back(id, childExt);
+ }
+
+ return;
+}
+
+CAddonDatabase::CAddonDatabase() = default;
+
+CAddonDatabase::~CAddonDatabase() = default;
+
+bool CAddonDatabase::Open()
+{
+ return CDatabase::Open();
+}
+
+int CAddonDatabase::GetMinSchemaVersion() const
+{
+ return 21;
+}
+
+int CAddonDatabase::GetSchemaVersion() const
+{
+ return 33;
+}
+
+void CAddonDatabase::CreateTables()
+{
+ CLog::Log(LOGINFO, "create addons table");
+ m_pDS->exec("CREATE TABLE addons ("
+ "id INTEGER PRIMARY KEY,"
+ "metadata BLOB,"
+ "addonID TEXT NOT NULL,"
+ "version TEXT NOT NULL,"
+ "name TEXT NOT NULL,"
+ "summary TEXT NOT NULL,"
+ "news TEXT NOT NULL,"
+ "description TEXT NOT NULL)");
+
+ CLog::Log(LOGINFO, "create repo table");
+ m_pDS->exec("CREATE TABLE repo (id integer primary key, addonID text,"
+ "checksum text, lastcheck text, version text, nextcheck TEXT)\n");
+
+ CLog::Log(LOGINFO, "create addonlinkrepo table");
+ m_pDS->exec("CREATE TABLE addonlinkrepo (idRepo integer, idAddon integer)\n");
+
+ CLog::Log(LOGINFO, "create update_rules table");
+ m_pDS->exec(
+ "CREATE TABLE update_rules (id integer primary key, addonID TEXT, updateRule INTEGER)\n");
+
+ CLog::Log(LOGINFO, "create package table");
+ m_pDS->exec("CREATE TABLE package (id integer primary key, addonID text, filename text, hash text)\n");
+
+ CLog::Log(LOGINFO, "create installed table");
+ m_pDS->exec("CREATE TABLE installed (id INTEGER PRIMARY KEY, addonID TEXT UNIQUE, "
+ "enabled BOOLEAN, installDate TEXT, lastUpdated TEXT, lastUsed TEXT, "
+ "origin TEXT NOT NULL DEFAULT '', disabledReason INTEGER NOT NULL DEFAULT 0) \n");
+}
+
+void CAddonDatabase::CreateAnalytics()
+{
+ CLog::Log(LOGINFO, "{} creating indices", __FUNCTION__);
+ m_pDS->exec("CREATE INDEX idxAddons ON addons(addonID)");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_addonlinkrepo_1 ON addonlinkrepo ( idAddon, idRepo )\n");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_addonlinkrepo_2 ON addonlinkrepo ( idRepo, idAddon )\n");
+ m_pDS->exec("CREATE UNIQUE INDEX idxUpdate_rules ON update_rules(addonID, updateRule)");
+ m_pDS->exec("CREATE UNIQUE INDEX idxPackage ON package(filename)");
+}
+
+void CAddonDatabase::UpdateTables(int version)
+{
+ if (version < 22)
+ {
+ m_pDS->exec("DROP TABLE system");
+ }
+ if (version < 24)
+ {
+ m_pDS->exec("DELETE FROM addon");
+ m_pDS->exec("DELETE FROM addonextra");
+ m_pDS->exec("DELETE FROM dependencies");
+ m_pDS->exec("DELETE FROM addonlinkrepo");
+ m_pDS->exec("DELETE FROM repo");
+ }
+ if (version < 25)
+ {
+ m_pDS->exec("ALTER TABLE installed ADD origin TEXT NOT NULL DEFAULT ''");
+ }
+ if (version < 26)
+ {
+ m_pDS->exec("DROP TABLE addon");
+ m_pDS->exec("DROP TABLE addonextra");
+ m_pDS->exec("DROP TABLE dependencies");
+ m_pDS->exec("DELETE FROM addonlinkrepo");
+ m_pDS->exec("DELETE FROM repo");
+ m_pDS->exec("CREATE TABLE addons ("
+ "id INTEGER PRIMARY KEY,"
+ "metadata BLOB,"
+ "addonID TEXT NOT NULL,"
+ "version TEXT NOT NULL,"
+ "name TEXT NOT NULL,"
+ "summary TEXT NOT NULL,"
+ "description TEXT NOT NULL)");
+ }
+ if (version < 27)
+ {
+ m_pDS->exec("ALTER TABLE addons ADD news TEXT NOT NULL DEFAULT ''");
+ }
+ if (version < 28)
+ {
+ m_pDS->exec("ALTER TABLE installed ADD disabledReason INTEGER NOT NULL DEFAULT 0");
+ // On adding this field we will use user disabled as the default reason for any disabled addons
+ m_pDS->exec("UPDATE installed SET disabledReason=1 WHERE enabled=0");
+ }
+ if (version < 29)
+ {
+ m_pDS->exec("DROP TABLE broken");
+ }
+ if (version < 30)
+ {
+ m_pDS->exec("ALTER TABLE repo ADD nextcheck TEXT");
+ }
+ if (version < 31)
+ {
+ m_pDS->exec("UPDATE installed SET origin = addonID WHERE (origin='') AND "
+ "EXISTS (SELECT * FROM repo WHERE repo.addonID = installed.addonID)");
+ }
+ if (version < 32)
+ {
+ m_pDS->exec(
+ "CREATE TABLE update_rules (id integer primary key, addonID text, updateRule INTEGER)");
+ m_pDS->exec("INSERT INTO update_rules (addonID, updateRule) SELECT addonID, 1 updateRule FROM "
+ "blacklist");
+ m_pDS->exec("DROP INDEX IF EXISTS idxBlack");
+ m_pDS->exec("DROP TABLE blacklist");
+ }
+ if (version < 33)
+ {
+ m_pDS->query(PrepareSQL("SELECT * FROM addons"));
+ while (!m_pDS->eof())
+ {
+ const int id = m_pDS->fv("id").get_asInt();
+ const std::string metadata = m_pDS->fv("metadata").get_asString();
+
+ CVariant variant;
+ if (!CJSONVariantParser::Parse(metadata, variant))
+ continue;
+
+ // Replace obsolete "broken" with new in json text
+ if (variant.isMember("broken") && variant["broken"].asString().empty())
+ {
+ variant["lifecycletype"] = static_cast<unsigned int>(AddonLifecycleState::BROKEN);
+ variant["lifecycledesc"] = variant["broken"].asString();
+ variant.erase("broken");
+ }
+
+ // Fix wrong added change about "broken" to "lifecycle..." (request 18286)
+ // as there was every addon marked as broken. This checks his text and if
+ // them empty, becomes it declared as normal.
+ if (variant.isMember("lifecycledesc") && variant.isMember("lifecycletype") &&
+ variant["lifecycledesc"].asString().empty() &&
+ variant["lifecycletype"].asUnsignedInteger() !=
+ static_cast<unsigned int>(AddonLifecycleState::NORMAL))
+ {
+ variant["lifecycletype"] = static_cast<unsigned int>(AddonLifecycleState::NORMAL);
+ }
+
+ CVariant variantUpdate;
+ variantUpdate["type"] = variant["extensions"][0].asString();
+ variantUpdate["values"] = CVariant(CVariant::VariantTypeArray);
+ variantUpdate["children"] = CVariant(CVariant::VariantTypeArray);
+
+ for (auto it = variant["extrainfo"].begin_array(); it != variant["extrainfo"].end_array();
+ ++it)
+ {
+ if ((*it)["key"].asString() == "provides")
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["id"] = (*it)["key"].asString();
+ info["content"] = CVariant(CVariant::VariantTypeArray);
+
+ CVariant contentEntry(CVariant::VariantTypeObject);
+ contentEntry["key"] = (*it)["key"].asString();
+ contentEntry["value"] = (*it)["value"].asString();
+ info["content"].push_back(std::move(contentEntry));
+
+ variantUpdate["values"].push_back(std::move(info));
+ break;
+ }
+ }
+ variant["extensions"][0] = variantUpdate;
+
+ std::string json;
+ CJSONVariantWriter::Write(variant, json, true);
+ m_pDS->exec(PrepareSQL("UPDATE addons SET metadata='%s' WHERE id=%i", json.c_str(), id));
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+}
+
+void CAddonDatabase::SyncInstalled(const std::set<std::string>& ids,
+ const std::set<std::string>& system,
+ const std::set<std::string>& optional)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+
+ std::set<std::string> db;
+ m_pDS->query(PrepareSQL("SELECT addonID FROM installed"));
+ while (!m_pDS->eof())
+ {
+ db.insert(m_pDS->fv("addonID").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ std::set<std::string> added;
+ std::set<std::string> removed;
+ std::set_difference(ids.begin(), ids.end(), db.begin(), db.end(), std::inserter(added, added.end()));
+ std::set_difference(db.begin(), db.end(), ids.begin(), ids.end(), std::inserter(removed, removed.end()));
+
+ for (const auto& id : added)
+ CLog::Log(LOGDEBUG, "CAddonDatabase: {} has been installed.", id);
+
+ for (const auto& id : removed)
+ CLog::Log(LOGDEBUG, "CAddonDatabase: {} has been uninstalled.", id);
+
+ std::string now = CDateTime::GetCurrentDateTime().GetAsDBDateTime();
+ BeginTransaction();
+ for (const auto& id : added)
+ {
+ int enable = 0;
+
+ if (system.find(id) != system.end() || optional.find(id) != optional.end())
+ enable = 1;
+
+ m_pDS->exec(PrepareSQL("INSERT INTO installed(addonID, enabled, installDate) "
+ "VALUES('%s', %d, '%s')", id.c_str(), enable, now.c_str()));
+ }
+
+ for (const auto& id : removed)
+ {
+ m_pDS->exec(PrepareSQL("DELETE FROM installed WHERE addonID='%s'", id.c_str()));
+ RemoveAllUpdateRulesForAddon(id);
+ DeleteRepository(id);
+ }
+
+ for (const auto& id : system)
+ {
+ m_pDS->exec(PrepareSQL("UPDATE installed SET enabled=1 WHERE addonID='%s'", id.c_str()));
+ // Make sure system addons always have ORIGIN_SYSTEM origin
+ m_pDS->exec(PrepareSQL("UPDATE installed SET origin='%s' WHERE addonID='%s'", ORIGIN_SYSTEM,
+ id.c_str()));
+ }
+
+ for (const auto& id : optional)
+ {
+ // Make sure optional system addons always have ORIGIN_SYSTEM origin
+ m_pDS->exec(PrepareSQL("UPDATE installed SET origin='%s' WHERE addonID='%s'", ORIGIN_SYSTEM,
+ id.c_str()));
+ }
+
+ CommitTransaction();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ RollbackTransaction();
+ }
+}
+
+bool CAddonDatabase::SetLastUpdated(const std::string& addonId, const CDateTime& dateTime)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ m_pDS->exec(PrepareSQL("UPDATE installed SET lastUpdated='%s' WHERE addonID='%s'",
+ dateTime.GetAsDBDateTime().c_str(), addonId.c_str()));
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonId);
+ }
+ return false;
+}
+
+bool CAddonDatabase::SetOrigin(const std::string& addonId, const std::string& origin)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ m_pDS->exec(PrepareSQL("UPDATE installed SET origin='%s' WHERE addonID='%s'", origin.c_str(), addonId.c_str()));
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonId);
+ }
+ return false;
+}
+
+bool CAddonDatabase::SetLastUsed(const std::string& addonId, const CDateTime& dateTime)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ auto start = std::chrono::steady_clock::now();
+ m_pDS->exec(PrepareSQL("UPDATE installed SET lastUsed='%s' WHERE addonID='%s'",
+ dateTime.GetAsDBDateTime().c_str(), addonId.c_str()));
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "CAddonDatabase::SetLastUsed[{}] took {} ms", addonId, duration.count());
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonId);
+ }
+ return false;
+}
+
+bool CAddonDatabase::FindByAddonId(const std::string& addonId, ADDON::VECADDONS& result) const
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL(
+ "SELECT addons.version, addons.name, addons.summary, addons.description, addons.metadata, addons.news,"
+ "repo.addonID AS repoID FROM addons "
+ "JOIN addonlinkrepo ON addonlinkrepo.idAddon=addons.id "
+ "JOIN repo ON repo.id=addonlinkrepo.idRepo "
+ "WHERE "
+ "repo.checksum IS NOT NULL AND repo.checksum != '' "
+ "AND EXISTS (SELECT * FROM installed WHERE installed.addonID=repoID AND installed.enabled=1) "
+ "AND addons.addonID='%s'", addonId.c_str());
+
+ m_pDS->query(sql);
+
+ VECADDONS addons;
+ addons.reserve(m_pDS->num_rows());
+
+ while (!m_pDS->eof())
+ {
+ CAddonInfoBuilderFromDB builder;
+ builder.SetId(addonId);
+ builder.SetVersion(CAddonVersion(m_pDS->fv("version").get_asString()));
+ builder.SetName(m_pDS->fv("name").get_asString());
+ builder.SetSummary(m_pDS->fv("summary").get_asString());
+ builder.SetDescription(m_pDS->fv("description").get_asString());
+ CAddonDatabaseSerializer::DeserializeMetadata(m_pDS->fv("metadata").get_asString(), builder);
+ builder.SetChangelog(m_pDS->fv("news").get_asString());
+ builder.SetOrigin(m_pDS->fv("repoID").get_asString());
+
+ auto addon = CAddonBuilder::Generate(builder.get(), AddonType::UNKNOWN);
+ if (addon)
+ addons.push_back(std::move(addon));
+ else
+ CLog::Log(LOGERROR, "CAddonDatabase: failed to build {}", addonId);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ result = std::move(addons);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon {}", __FUNCTION__, addonId);
+ }
+ return false;
+}
+
+bool CAddonDatabase::GetAddon(const std::string& addonID,
+ const CAddonVersion& version,
+ const std::string& repoId,
+ AddonPtr& addon)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ const std::string sql =
+ PrepareSQL("SELECT addons.id FROM addons "
+ "JOIN addonlinkrepo ON addonlinkrepo.idAddon=addons.id "
+ "JOIN repo ON repo.id=addonlinkrepo.idRepo "
+ "WHERE addons.addonID='%s' AND addons.version='%s' AND repo.addonID='%s'",
+ addonID.c_str(), version.asString().c_str(), repoId.c_str());
+
+ m_pDS->query(sql);
+ if (m_pDS->eof())
+ return false;
+
+ return GetAddon(m_pDS->fv("id").get_asInt(), addon);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon {}", __FUNCTION__, addonID);
+ }
+ return false;
+
+}
+
+bool CAddonDatabase::GetAddon(int id, AddonPtr &addon)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS2)
+ return false;
+
+ m_pDS2->query(PrepareSQL("SELECT addons.*, repo.addonID as origin FROM addons "
+ "JOIN addonlinkrepo ON addonlinkrepo.idAddon=addons.id "
+ "JOIN repo ON repo.id=addonlinkrepo.idRepo "
+ "WHERE addons.id=%i",
+ id));
+ if (m_pDS2->eof())
+ return false;
+
+ CAddonInfoBuilderFromDB builder;
+ builder.SetId(m_pDS2->fv("addonID").get_asString());
+ builder.SetOrigin(m_pDS2->fv("origin").get_asString());
+ builder.SetVersion(CAddonVersion(m_pDS2->fv("version").get_asString()));
+ builder.SetName(m_pDS2->fv("name").get_asString());
+ builder.SetSummary(m_pDS2->fv("summary").get_asString());
+ builder.SetDescription(m_pDS2->fv("description").get_asString());
+ CAddonDatabaseSerializer::DeserializeMetadata(m_pDS2->fv("metadata").get_asString(), builder);
+
+ addon = CAddonBuilder::Generate(builder.get(), AddonType::UNKNOWN);
+ return addon != nullptr;
+
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon {}", __FUNCTION__, id);
+ }
+ return false;
+}
+
+bool CAddonDatabase::GetRepositoryContent(VECADDONS& addons) const
+{
+ return GetRepositoryContent("", addons);
+}
+
+bool CAddonDatabase::GetRepositoryContent(const std::string& id, VECADDONS& addons) const
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ auto start = std::chrono::steady_clock::now();
+
+ // Ensure that the repositories we fetch from are enabled and valid.
+ std::vector<std::string> repoIds;
+ {
+ std::string sql = PrepareSQL(
+ " SELECT repo.id FROM repo"
+ " WHERE repo.checksum IS NOT NULL AND repo.checksum != ''"
+ " AND EXISTS (SELECT * FROM installed WHERE installed.addonID=repo.addonID AND"
+ " installed.enabled=1)");
+
+ if (!id.empty())
+ sql += PrepareSQL(" AND repo.addonId='%s'", id.c_str());
+
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ repoIds.emplace_back(m_pDS->fv("id").get_asString());
+ m_pDS->next();
+ }
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "CAddonDatabase: SELECT repo.id FROM repo .. took {} ms", duration.count());
+
+ if (repoIds.empty())
+ {
+ if (id.empty())
+ {
+ CLog::Log(LOGDEBUG, "CAddonDatabase: no valid repository, continuing");
+ addons = {};
+ return true;
+ }
+
+ CLog::Log(LOGDEBUG, "CAddonDatabase: no valid repository matching '{}'", id);
+ return false;
+ }
+
+ {
+ std::string sql = PrepareSQL(" SELECT addons.*, repo.addonID AS repoID FROM addons"
+ " JOIN addonlinkrepo ON addons.id=addonlinkrepo.idAddon"
+ " JOIN repo ON repo.id=addonlinkrepo.idRepo"
+ " WHERE addonlinkrepo.idRepo IN (%s)"
+ " ORDER BY repo.addonID, addons.addonID",
+ StringUtils::Join(repoIds, ",").c_str());
+
+ start = std::chrono::steady_clock::now();
+ m_pDS->query(sql);
+
+ end = std::chrono::steady_clock::now();
+ duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "CAddonDatabase: query {} returned {} rows in {} ms", sql,
+ m_pDS->num_rows(), duration.count());
+ }
+
+ VECADDONS result;
+ result.reserve(m_pDS->num_rows());
+
+ while (!m_pDS->eof())
+ {
+ std::string addonId = m_pDS->fv("addonID").get_asString();
+ CAddonVersion version(m_pDS->fv("version").get_asString());
+
+ CAddonInfoBuilderFromDB builder;
+ builder.SetId(addonId);
+ builder.SetVersion(version);
+ builder.SetName(m_pDS->fv("name").get_asString());
+ builder.SetSummary(m_pDS->fv("summary").get_asString());
+ builder.SetDescription(m_pDS->fv("description").get_asString());
+ builder.SetOrigin(m_pDS->fv("repoID").get_asString());
+ CAddonDatabaseSerializer::DeserializeMetadata(m_pDS->fv("metadata").get_asString(), builder);
+
+ auto addon = CAddonBuilder::Generate(builder.get(), AddonType::UNKNOWN);
+ if (addon)
+ {
+ result.emplace_back(std::move(addon));
+ }
+ else
+ CLog::Log(LOGWARNING, "CAddonDatabase: failed to build {}", addonId);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ addons = std::move(result);
+
+ end = std::chrono::steady_clock::now();
+ duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "CAddonDatabase::GetAddons took {} ms", duration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+void CAddonDatabase::DeleteRepository(const std::string& id)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+
+ int idRepo = GetRepositoryId(id);
+ if (idRepo < 0)
+ return;
+
+ m_pDS->exec(PrepareSQL("DELETE FROM repo WHERE id=%i", idRepo));
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, id);
+ }
+}
+
+void CAddonDatabase::DeleteRepositoryContents(const std::string& id)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+
+ int idRepo = GetRepositoryId(id);
+ if (idRepo < 0)
+ return;
+
+ m_pDS->exec(PrepareSQL("DELETE FROM addons WHERE id IN (SELECT idAddon FROM addonlinkrepo WHERE idRepo=%i)", idRepo));
+ m_pDS->exec(PrepareSQL("DELETE FROM addonlinkrepo WHERE idRepo=%i", idRepo));
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, id);
+ }
+}
+
+int CAddonDatabase::GetRepositoryId(const std::string& addonId)
+{
+ if (!m_pDB)
+ return -1;
+ if (!m_pDS)
+ return -1;
+
+ m_pDS->query(PrepareSQL("SELECT id FROM repo WHERE addonID='%s'", addonId.c_str()));
+ if (m_pDS->eof())
+ return -1;
+
+ return m_pDS->fv("id").get_asInt();
+}
+
+bool CAddonDatabase::UpdateRepositoryContent(const std::string& repository,
+ const CAddonVersion& version,
+ const std::string& checksum,
+ const std::vector<AddonInfoPtr>& addons)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ DeleteRepositoryContents(repository);
+ int idRepo = GetRepositoryId(repository);
+ if (idRepo < 0)
+ return false;
+
+ assert(idRepo > 0);
+
+ m_pDB->start_transaction();
+ m_pDS->exec(
+ PrepareSQL("UPDATE repo SET checksum='%s' WHERE id='%i'", checksum.c_str(), idRepo));
+ for (const auto& addon : addons)
+ {
+ m_pDS->exec(PrepareSQL(
+ "INSERT INTO addons (id, metadata, addonID, version, name, summary, description, news) "
+ "VALUES (NULL, '%s', '%s', '%s', '%s','%s', '%s','%s')",
+ CAddonDatabaseSerializer::SerializeMetadata(*addon).c_str(), addon->ID().c_str(),
+ addon->Version().asString().c_str(), addon->Name().c_str(), addon->Summary().c_str(),
+ addon->Description().c_str(), addon->ChangeLog().c_str()));
+
+ int idAddon = static_cast<int>(m_pDS->lastinsertid());
+ if (idAddon <= 0)
+ {
+ CLog::Log(LOGERROR, "{} insert failed on addon '{}'", __FUNCTION__, addon->ID());
+ RollbackTransaction();
+ return false;
+ }
+
+ m_pDS->exec(PrepareSQL("INSERT INTO addonlinkrepo (idRepo, idAddon) VALUES (%i, %i)", idRepo, idAddon));
+ }
+
+ m_pDB->commit_transaction();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, repository);
+ RollbackTransaction();
+ }
+ return false;
+}
+
+int CAddonDatabase::GetRepoChecksum(const std::string& id, std::string& checksum)
+{
+ try
+ {
+ if (!m_pDB)
+ return -1;
+ if (!m_pDS)
+ return -1;
+
+ std::string strSQL = PrepareSQL("select * from repo where addonID='%s'",id.c_str());
+ m_pDS->query(strSQL);
+ if (!m_pDS->eof())
+ {
+ checksum = m_pDS->fv("checksum").get_asString();
+ return m_pDS->fv("id").get_asInt();
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, id);
+ }
+ checksum.clear();
+ return -1;
+}
+
+CAddonDatabase::RepoUpdateData CAddonDatabase::GetRepoUpdateData(const std::string& id)
+{
+ RepoUpdateData result{};
+ try
+ {
+ if (m_pDB && m_pDS)
+ {
+ std::string strSQL = PrepareSQL("select * from repo where addonID='%s'",id.c_str());
+ m_pDS->query(strSQL);
+ if (!m_pDS->eof())
+ {
+ result.lastCheckedAt.SetFromDBDateTime(m_pDS->fv("lastcheck").get_asString());
+ result.lastCheckedVersion = CAddonVersion(m_pDS->fv("version").get_asString());
+ result.nextCheckAt.SetFromDBDateTime(m_pDS->fv("nextcheck").get_asString());
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, id);
+ }
+ return result;
+}
+
+int CAddonDatabase::SetRepoUpdateData(const std::string& id, const RepoUpdateData& updateData)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ int retId = -1;
+ std::string sql = PrepareSQL("SELECT * FROM repo WHERE addonID='%s'", id.c_str());
+ m_pDS->query(sql);
+
+ if (m_pDS->eof())
+ {
+ sql = PrepareSQL("INSERT INTO repo (id, addonID, lastcheck, version, nextcheck) "
+ "VALUES (NULL, '%s', '%s', '%s', '%s')",
+ id.c_str(), updateData.lastCheckedAt.GetAsDBDateTime().c_str(),
+ updateData.lastCheckedVersion.asString().c_str(),
+ updateData.nextCheckAt.GetAsDBDateTime().c_str());
+ m_pDS->exec(sql);
+ retId = static_cast<int>(m_pDS->lastinsertid());
+ }
+ else
+ {
+ retId = m_pDS->fv("id").get_asInt();
+ sql = PrepareSQL(
+ "UPDATE repo SET lastcheck='%s', version='%s', nextcheck='%s' WHERE addonID='%s'",
+ updateData.lastCheckedAt.GetAsDBDateTime().c_str(),
+ updateData.lastCheckedVersion.asString().c_str(),
+ updateData.nextCheckAt.GetAsDBDateTime().c_str(), id.c_str());
+ m_pDS->exec(sql);
+ }
+
+ return retId;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on repo '{}'", __FUNCTION__, id);
+ }
+ return -1;
+}
+
+bool CAddonDatabase::Search(const std::string& search, VECADDONS& addons)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT id FROM addons WHERE name LIKE '%%%s%%' OR summary LIKE '%%%s%%' "
+ "OR description LIKE '%%%s%%'", search.c_str(), search.c_str(), search.c_str());
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (!m_pDS->query(strSQL)) return false;
+ if (m_pDS->num_rows() == 0) return false;
+
+ while (!m_pDS->eof())
+ {
+ AddonPtr addon;
+ GetAddon(m_pDS->fv("id").get_asInt(), addon);
+ if (static_cast<int>(addon->Type()) >= static_cast<int>(AddonType::UNKNOWN) + 1 &&
+ static_cast<int>(addon->Type()) < static_cast<int>(AddonType::SCRAPER_LIBRARY))
+ addons.push_back(addon);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CAddonDatabase::DisableAddon(const std::string& addonID, AddonDisabledReason disabledReason)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ const std::string sql =
+ PrepareSQL("UPDATE installed SET enabled=0, disabledReason=%d WHERE addonID='%s'",
+ static_cast<int>(disabledReason), addonID.c_str());
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonID);
+ }
+ return false;
+}
+
+bool CAddonDatabase::EnableAddon(const std::string& addonID)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ const std::string sql = PrepareSQL(
+ "UPDATE installed SET enabled=1, disabledReason=0 WHERE addonID='%s'", addonID.c_str());
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonID);
+ }
+ return false;
+}
+
+bool CAddonDatabase::GetDisabled(std::map<std::string, AddonDisabledReason>& addons)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ const std::string sql =
+ PrepareSQL("SELECT addonID, disabledReason FROM installed WHERE enabled=0");
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ addons.insert({m_pDS->fv("addonID").get_asString(),
+ static_cast<AddonDisabledReason>(m_pDS->fv("disabledReason").get_asInt())});
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CAddonDatabase::GetAddonUpdateRules(
+ std::map<std::string, std::vector<AddonUpdateRule>>& rulesMap) const
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL("SELECT * FROM update_rules");
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ rulesMap[m_pDS->fv("addonID").get_asString()].emplace_back(
+ static_cast<AddonUpdateRule>(m_pDS->fv("updateRule").get_asInt()));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CAddonDatabase::AddUpdateRuleForAddon(const std::string& addonID, AddonUpdateRule updateRule)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql =
+ PrepareSQL("INSERT INTO update_rules(id, addonID, updateRule) VALUES(NULL, '%s', %d)",
+ addonID.c_str(), static_cast<int>(updateRule));
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonID);
+ }
+ return false;
+}
+
+bool CAddonDatabase::RemoveAllUpdateRulesForAddon(const std::string& addonID)
+{
+ return RemoveUpdateRuleForAddon(addonID, AddonUpdateRule::ANY);
+}
+
+bool CAddonDatabase::RemoveUpdateRuleForAddon(const std::string& addonID,
+ AddonUpdateRule updateRule)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL("DELETE FROM update_rules WHERE addonID='%s'", addonID.c_str());
+
+ if (updateRule != AddonUpdateRule::ANY)
+ {
+ sql += PrepareSQL(" AND updateRule = %d", static_cast<int>(updateRule));
+ }
+
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon '{}'", __FUNCTION__, addonID);
+ }
+ return false;
+}
+
+bool CAddonDatabase::AddPackage(const std::string& addonID,
+ const std::string& packageFileName,
+ const std::string& hash)
+{
+ std::string sql = PrepareSQL("insert or ignore into package(id, addonID, filename, hash)"
+ "values(NULL, '%s', '%s', '%s')",
+ addonID.c_str(), packageFileName.c_str(), hash.c_str());
+ return ExecuteQuery(sql);
+}
+
+bool CAddonDatabase::GetPackageHash(const std::string& addonID,
+ const std::string& packageFileName,
+ std::string& hash)
+{
+ std::string where = PrepareSQL("addonID='%s' and filename='%s'",
+ addonID.c_str(), packageFileName.c_str());
+ hash = GetSingleValue("package", "hash", where);
+ return !hash.empty();
+}
+
+bool CAddonDatabase::RemovePackage(const std::string& packageFileName)
+{
+ std::string sql = PrepareSQL("delete from package where filename='%s'", packageFileName.c_str());
+ return ExecuteQuery(sql);
+}
+
+void CAddonDatabase::OnPostUnInstall(const std::string& addonId)
+{
+ RemoveAllUpdateRulesForAddon(addonId);
+ DeleteRepository(addonId);
+
+ //! @todo should be done before uninstall to avoid any race conditions
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+ m_pDS->exec(PrepareSQL("DELETE FROM installed WHERE addonID='%s'", addonId.c_str()));
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on addon {}", __FUNCTION__, addonId);
+ }
+}
+
+void CAddonDatabase::GetInstallData(const AddonInfoPtr& addon)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS)
+ return;
+
+ m_pDS->query(PrepareSQL("SELECT addonID, installDate, lastUpdated, lastUsed, "
+ "origin FROM installed WHERE addonID='%s'",
+ addon->ID().c_str()));
+ if (!m_pDS->eof())
+ {
+ CAddonInfoBuilder::SetInstallData(
+ addon, CDateTime::FromDBDateTime(m_pDS->fv("installDate").get_asString()),
+ CDateTime::FromDBDateTime(m_pDS->fv("lastUpdated").get_asString()),
+ CDateTime::FromDBDateTime(m_pDS->fv("lastUsed").get_asString()),
+ m_pDS->fv("origin").get_asString());
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CAddonDatabase::{}: failed", __FUNCTION__);
+ }
+}
+
+bool CAddonDatabase::AddInstalledAddon(const std::shared_ptr<CAddonInfo>& addon,
+ const std::string& origin)
+{
+ try
+ {
+ if (!m_pDB)
+ return false;
+ if (!m_pDS)
+ return false;
+
+ m_pDS->query(PrepareSQL("SELECT * FROM installed WHERE addonID='%s'", addon->ID().c_str()));
+
+ if (m_pDS->eof())
+ {
+ std::string now = CDateTime::GetCurrentDateTime().GetAsDBDateTime();
+
+ m_pDS->exec(PrepareSQL("INSERT INTO installed(addonID, enabled, installDate, origin) "
+ "VALUES('%s', 1, '%s', '%s')",
+ addon->ID().c_str(), now.c_str(), origin.c_str()));
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ return false;
+ }
+
+ return true;
+}
diff --git a/xbmc/addons/AddonDatabase.h b/xbmc/addons/AddonDatabase.h
new file mode 100644
index 0000000..72caa85
--- /dev/null
+++ b/xbmc/addons/AddonDatabase.h
@@ -0,0 +1,247 @@
+/*
+ * 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 "XBDateTime.h"
+#include "addons/AddonVersion.h"
+#include "dbwrappers/Database.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+class CVariant;
+
+namespace ADDON
+{
+
+enum class AddonDisabledReason;
+enum class AddonUpdateRule;
+
+class CAddonExtensions;
+class CAddonInfoBuilderFromDB;
+
+class CAddonInfo;
+using AddonInfoPtr = std::shared_ptr<CAddonInfo>;
+
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+using VECADDONS = std::vector<AddonPtr>;
+
+/*!
+ * @brief Addon content serializer/deserializer.
+ *
+ * Used to save data from the add-on in the database using json format.
+ * The corresponding field in SQL is "addons" for "metadata".
+ *
+ * @warning Changes in the json format need a way to update the addon database
+ * for users, otherwise problems may occur when reading the old content.
+ */
+class CAddonDatabaseSerializer
+{
+ CAddonDatabaseSerializer() = delete;
+
+public:
+ static std::string SerializeMetadata(const CAddonInfo& addon);
+ static void DeserializeMetadata(const std::string& document, CAddonInfoBuilderFromDB& builder);
+
+private:
+ static CVariant SerializeExtensions(const CAddonExtensions& addonType);
+ static void DeserializeExtensions(const CVariant& document, CAddonExtensions& addonType);
+};
+
+class CAddonDatabase : public CDatabase
+{
+public:
+ CAddonDatabase();
+ ~CAddonDatabase() override;
+ bool Open() override;
+
+ /*! \brief Get an addon with a specific version and repository. */
+ bool GetAddon(const std::string& addonID,
+ const ADDON::CAddonVersion& version,
+ const std::string& repoId,
+ ADDON::AddonPtr& addon);
+
+ /*! Get the addon IDs that have been set to disabled */
+ bool GetDisabled(std::map<std::string, ADDON::AddonDisabledReason>& addons);
+
+ /*! Returns all addons in the repositories with id `addonId`. */
+ bool FindByAddonId(const std::string& addonId, ADDON::VECADDONS& addons) const;
+
+ bool UpdateRepositoryContent(const std::string& repositoryId,
+ const ADDON::CAddonVersion& version,
+ const std::string& checksum,
+ const std::vector<AddonInfoPtr>& addons);
+
+ int GetRepoChecksum(const std::string& id, std::string& checksum);
+
+ /*!
+ \brief Get addons in repository `id`
+ \param id id of the repository
+ \returns true on success, false on error or if repository have never been synced.
+ */
+ bool GetRepositoryContent(const std::string& id, ADDON::VECADDONS& addons) const;
+
+ /*! Get addons across all repositories */
+ bool GetRepositoryContent(ADDON::VECADDONS& addons) const;
+
+ struct RepoUpdateData
+ {
+ /*! \brief last time the repo was checked, or invalid CDateTime if never checked */
+ CDateTime lastCheckedAt;
+ /*! \brief last version of the repo add-on that was checked, or empty if never checked */
+ ADDON::CAddonVersion lastCheckedVersion{""};
+ /*! \brief next time the repo should be checked, or invalid CDateTime if unknown */
+ CDateTime nextCheckAt;
+
+ RepoUpdateData() = default;
+
+ RepoUpdateData(const CDateTime& lastCheckedAt,
+ const ADDON::CAddonVersion& lastCheckedVersion,
+ const CDateTime& nextCheckAt)
+ : lastCheckedAt{lastCheckedAt},
+ lastCheckedVersion{lastCheckedVersion},
+ nextCheckAt{nextCheckAt}
+ {
+ }
+ };
+
+ /*!
+ \brief Set data concerning repository update (last/next date etc.), and create the repo if needed
+ \param id add-on id of the repository
+ \param updateData update data to set
+ \returns id of the repository, or -1 on error.
+ */
+ int SetRepoUpdateData(const std::string& id, const RepoUpdateData& updateData);
+
+ /*!
+ \brief Retrieve repository update data (last/next date etc.)
+ \param id add-on id of the repo
+ \return update data of the repository
+ */
+ RepoUpdateData GetRepoUpdateData(const std::string& id);
+
+ bool Search(const std::string& search, ADDON::VECADDONS& items);
+
+ /*!
+ * \brief Disable an addon.
+ * Sets a flag that this addon has been disabled. If disabled, it is usually still available on
+ * disk.
+ * \param addonID id of the addon to disable
+ * \param disabledReason the reason why the addon is being disabled
+ * \return true on success, false on failure
+ * \sa IsAddonDisabled, HasDisabledAddons, EnableAddon
+ */
+ bool DisableAddon(const std::string& addonID, ADDON::AddonDisabledReason disabledReason);
+
+ /*! \brief Enable an addon.
+ * Enables an addon that has previously been disabled
+ * \param addonID id of the addon to enable
+ * \return true on success, false on failure
+ * \sa DisableAddon, IsAddonDisabled, HasDisabledAddons
+ */
+ bool EnableAddon(const std::string& addonID);
+
+ /*!
+ * \brief Write dataset with addon-id and rule to the db
+ * \param addonID the addonID
+ * \param updateRule the rule value to be written
+ * \return true on success, false otherwise
+ */
+ bool AddUpdateRuleForAddon(const std::string& addonID, ADDON::AddonUpdateRule updateRule);
+
+ /*!
+ * \brief Remove all rule datasets for an addon-id from the db
+ * \param addonID the addonID
+ * \return true on success, false otherwise
+ */
+ bool RemoveAllUpdateRulesForAddon(const std::string& addonID);
+
+ /*!
+ * \brief Remove a single rule dataset for an addon-id from the db
+ * \note specifying AddonUpdateRule::ANY will remove all rules.
+ * use @ref RemoveAllUpdateRulesForAddon() instead
+ * \param addonID the addonID
+ * \param updateRule the rule to remove
+ * \return true on success, false otherwise
+ */
+ bool RemoveUpdateRuleForAddon(const std::string& addonID, AddonUpdateRule updateRule);
+
+ /*!
+ * \brief Retrieve all rule datasets from db and store them into map
+ * \param rulesMap target map
+ * \return true on success, false otherwise
+ */
+ bool GetAddonUpdateRules(std::map<std::string, std::vector<AddonUpdateRule>>& rulesMap) const;
+
+ /*! \brief Store an addon's package filename and that file's hash for future verification
+ \param addonID id of the addon we're adding a package for
+ \param packageFileName filename of the package
+ \param hash MD5 checksum of the package
+ \return Whether or not the info successfully made it into the DB.
+ \sa GetPackageHash, RemovePackage
+ */
+ bool AddPackage(const std::string& addonID,
+ const std::string& packageFileName,
+ const std::string& hash);
+ /*! \brief Query the MD5 checksum of the given given addon's given package
+ \param addonID id of the addon we're who's package we're querying
+ \param packageFileName filename of the package
+ \param hash return the MD5 checksum of the package
+ \return Whether or not we found a hash for the given addon's given package
+ \sa AddPackage, RemovePackage
+ */
+ bool GetPackageHash(const std::string& addonID,
+ const std::string& packageFileName,
+ std::string& hash);
+ /*! \brief Remove a package's info from the database
+ \param packageFileName filename of the package
+ \return Whether or not we succeeded in removing the package
+ \sa AddPackage, GetPackageHash
+ */
+ bool RemovePackage(const std::string& packageFileName);
+
+ /*! Clear internal fields that shouldn't be kept around indefinitely */
+ void OnPostUnInstall(const std::string& addonId);
+
+ void SyncInstalled(const std::set<std::string>& ids,
+ const std::set<std::string>& system,
+ const std::set<std::string>& optional);
+
+ bool SetLastUpdated(const std::string& addonId, const CDateTime& dateTime);
+ bool SetOrigin(const std::string& addonId, const std::string& origin);
+ bool SetLastUsed(const std::string& addonId, const CDateTime& dateTime);
+
+ void GetInstallData(const ADDON::AddonInfoPtr& addon);
+
+ /*! \brief Add dataset for a new installed addon to the database
+ * \param addon the addon to insert
+ * \param origin the origin it was installed from
+ * \return true on success, false otherwise
+ */
+ bool AddInstalledAddon(const std::shared_ptr<CAddonInfo>& addon, const std::string& origin);
+
+protected:
+ void CreateTables() override;
+ void CreateAnalytics() override;
+ void UpdateTables(int version) override;
+ int GetMinSchemaVersion() const override;
+ int GetSchemaVersion() const override;
+ const char *GetBaseDBName() const override { return "Addons"; }
+
+ bool GetAddon(int id, ADDON::AddonPtr& addon);
+ void DeleteRepository(const std::string& id);
+ void DeleteRepositoryContents(const std::string& id);
+ int GetRepositoryId(const std::string& addonId);
+};
+
+}; // namespace ADDON
diff --git a/xbmc/addons/AddonEvents.h b/xbmc/addons/AddonEvents.h
new file mode 100644
index 0000000..8db032c
--- /dev/null
+++ b/xbmc/addons/AddonEvents.h
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016-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 "addons/IAddon.h"
+
+#include <string>
+
+namespace ADDON
+{
+
+struct AddonEvent
+{
+ std::string addonId;
+ AddonInstanceId instanceId{ADDON_SINGLETON_INSTANCE_ID};
+
+ explicit AddonEvent(std::string addonId) : addonId(std::move(addonId)) {}
+ AddonEvent(std::string addonId, AddonInstanceId instanceId)
+ : addonId(std::move(addonId)), instanceId(instanceId)
+ {
+ }
+
+ // Note: Do not remove the virtual dtor. There are types derived from AddonEvent (see below)
+ // and there are several places where 'typeid' is used to determine the runtime type of
+ // AddonEvent references. And 'typeid' only works for polymorphic objects.
+ virtual ~AddonEvent() = default;
+};
+
+namespace AddonEvents
+{
+
+/**
+ * Emitted after the add-on has been enabled.
+ */
+struct Enabled : AddonEvent
+{
+ explicit Enabled(std::string addonId) : AddonEvent(std::move(addonId)) {}
+};
+
+/**
+ * Emitted after the add-on has been disabled.
+ */
+struct Disabled : AddonEvent
+{
+ explicit Disabled(std::string addonId) : AddonEvent(std::move(addonId)) {}
+};
+
+/**
+ * Emitted after a new usable add-on instance was added.
+ */
+struct InstanceAdded : AddonEvent
+{
+ InstanceAdded(std::string addonId, AddonInstanceId instanceId)
+ : AddonEvent(std::move(addonId), instanceId)
+ {
+ }
+};
+
+/**
+ * Emitted after an add-on instance was removed.
+ */
+struct InstanceRemoved : AddonEvent
+{
+ InstanceRemoved(std::string addonId, AddonInstanceId instanceId)
+ : AddonEvent(std::move(addonId), instanceId)
+ {
+ }
+};
+
+/**
+ * Emitted after the add-on's metadata has been changed.
+ */
+struct MetadataChanged : AddonEvent
+{
+ explicit MetadataChanged(std::string addonId) : AddonEvent(std::move(addonId)) {}
+};
+
+/**
+ * Emitted when a different version of the add-on has been installed
+ * to the file system and should be reloaded.
+ */
+struct ReInstalled : AddonEvent
+{
+ explicit ReInstalled(std::string addonId) : AddonEvent(std::move(addonId)) {}
+};
+
+/**
+ * Emitted after the add-on has been uninstalled.
+ */
+struct UnInstalled : AddonEvent
+{
+ explicit UnInstalled(std::string addonId) : AddonEvent(std::move(addonId)) {}
+};
+
+/**
+ * Emitted after the add-on has been loaded.
+ */
+struct Load : AddonEvent
+{
+ explicit Load(std::string addonId) : AddonEvent(std::move(addonId)) {}
+};
+
+/**
+ * Emitted after the add-on has been unloaded.
+ */
+struct Unload : AddonEvent
+{
+ explicit Unload(std::string addonId) : AddonEvent(std::move(addonId)) {}
+};
+
+/**
+ * Emitted after the auto-update state of the add-on has been changed.
+ */
+struct AutoUpdateStateChanged : AddonEvent
+{
+ explicit AutoUpdateStateChanged(std::string addonId) : AddonEvent(std::move(addonId)) {}
+};
+
+} // namespace AddonEvents
+} // namespace ADDON
diff --git a/xbmc/addons/AddonInstaller.cpp b/xbmc/addons/AddonInstaller.cpp
new file mode 100644
index 0000000..0a0d169
--- /dev/null
+++ b/xbmc/addons/AddonInstaller.cpp
@@ -0,0 +1,1286 @@
+/*
+ * Copyright (C) 2011-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 "AddonInstaller.h"
+
+#include "FileItem.h"
+#include "FilesystemInstaller.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h" // for callback
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonRepos.h"
+#include "addons/Repository.h"
+#include "addons/RepositoryUpdater.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "events/AddonManagementEvent.h"
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+#include "favourites/FavouritesService.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h" // for callback
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileOperationJob.h"
+#include "utils/FileUtils.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <functional>
+#include <mutex>
+
+using namespace XFILE;
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+
+using namespace std::chrono_literals;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+using KODI::UTILITY::TypedDigest;
+
+namespace
+{
+class CAddonInstallJob : public CFileOperationJob
+{
+public:
+ CAddonInstallJob(const ADDON::AddonPtr& addon,
+ const ADDON::RepositoryPtr& repo,
+ AutoUpdateJob isAutoUpdate);
+
+ bool DoWork() override;
+
+ static constexpr const char* TYPE_DOWNLOAD = "DOWNLOAD";
+ static constexpr const char* TYPE_INSTALL = "INSTALL";
+ /*!
+ * \brief Returns the current processing type in the installation job
+ *
+ * \return The current processing type as string, can be \ref TYPE_DOWNLOAD or
+ * \ref TYPE_INSTALL
+ */
+ const char* GetType() const override { return m_currentType; }
+
+ /*! \brief Find the add-on and its repository for the given add-on ID
+ * \param addonID ID of the add-on to find
+ * \param[out] repo the repository to use
+ * \param[out] addon Add-on with the given add-on ID
+ * \return True if the add-on and its repository were found, false otherwise.
+ */
+ static bool GetAddon(const std::string& addonID,
+ ADDON::RepositoryPtr& repo,
+ ADDON::AddonPtr& addon);
+
+ void SetDependsInstall(DependencyJob dependsInstall) { m_dependsInstall = dependsInstall; }
+ void SetAllowCheckForUpdates(AllowCheckForUpdates allowCheckForUpdates)
+ {
+ m_allowCheckForUpdates = allowCheckForUpdates;
+ };
+
+private:
+ void OnPreInstall();
+ void OnPostInstall();
+ bool Install(const std::string& installFrom,
+ const ADDON::RepositoryPtr& repo = ADDON::RepositoryPtr());
+ bool DownloadPackage(const std::string& path, const std::string& dest);
+
+ bool DoFileOperation(FileAction action,
+ CFileItemList& items,
+ const std::string& file,
+ bool useSameJob = true);
+
+ /*! \brief Queue a notification for addon installation/update failure
+ \param addonID - addon id
+ \param fileName - filename which is shown in case the addon id is unknown
+ \param message - error message to be displayed
+ */
+ void ReportInstallError(const std::string& addonID,
+ const std::string& fileName,
+ const std::string& message = "");
+
+ ADDON::AddonPtr m_addon;
+ ADDON::RepositoryPtr m_repo;
+ bool m_isUpdate;
+ AutoUpdateJob m_isAutoUpdate;
+ DependencyJob m_dependsInstall = DependencyJob::CHOICE_NO;
+ AllowCheckForUpdates m_allowCheckForUpdates = AllowCheckForUpdates::CHOICE_YES;
+ const char* m_currentType = TYPE_DOWNLOAD;
+};
+
+class CAddonUnInstallJob : public CFileOperationJob
+{
+public:
+ CAddonUnInstallJob(const ADDON::AddonPtr& addon, bool removeData);
+
+ bool DoWork() override;
+ void SetRecurseOrphaned(RecurseOrphaned recurseOrphaned) { m_recurseOrphaned = recurseOrphaned; };
+
+private:
+ void ClearFavourites();
+
+ ADDON::AddonPtr m_addon;
+ bool m_removeData;
+ RecurseOrphaned m_recurseOrphaned = RecurseOrphaned::CHOICE_YES;
+};
+
+} // unnamed namespace
+
+CAddonInstaller::CAddonInstaller() : m_idle(true)
+{ }
+
+CAddonInstaller::~CAddonInstaller() = default;
+
+CAddonInstaller &CAddonInstaller::GetInstance()
+{
+ static CAddonInstaller addonInstaller;
+ return addonInstaller;
+}
+
+void CAddonInstaller::OnJobComplete(unsigned int jobID, bool success, CJob* job)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ JobMap::iterator i = find_if(m_downloadJobs.begin(), m_downloadJobs.end(), [jobID](const std::pair<std::string, CDownloadJob>& p) {
+ return p.second.jobID == jobID;
+ });
+ if (i != m_downloadJobs.end())
+ m_downloadJobs.erase(i);
+ if (m_downloadJobs.empty())
+ m_idle.Set();
+ lock.unlock();
+ PrunePackageCache();
+
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CAddonInstaller::OnJobProgress(unsigned int jobID, unsigned int progress, unsigned int total, const CJob *job)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ JobMap::iterator i = find_if(m_downloadJobs.begin(), m_downloadJobs.end(), [jobID](const std::pair<std::string, CDownloadJob>& p) {
+ return p.second.jobID == jobID;
+ });
+ if (i != m_downloadJobs.end())
+ {
+ // update job progress
+ i->second.progress = 100 / total * progress;
+ i->second.downloadFinshed = std::string(job->GetType()) == CAddonInstallJob::TYPE_INSTALL;
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM);
+ msg.SetStringParam(i->first);
+ lock.unlock();
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+}
+
+bool CAddonInstaller::IsDownloading() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return !m_downloadJobs.empty();
+}
+
+void CAddonInstaller::GetInstallList(VECADDONS &addons) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::vector<std::string> addonIDs;
+ for (JobMap::const_iterator i = m_downloadJobs.begin(); i != m_downloadJobs.end(); ++i)
+ {
+ if (i->second.jobID)
+ addonIDs.push_back(i->first);
+ }
+ lock.unlock();
+
+ auto& addonMgr = CServiceBroker::GetAddonMgr();
+ for (const auto& addonId : addonIDs)
+ {
+ AddonPtr addon;
+ if (addonMgr.FindInstallableById(addonId, addon))
+ addons.emplace_back(std::move(addon));
+ }
+}
+
+bool CAddonInstaller::GetProgress(const std::string& addonID, unsigned int& percent, bool& downloadFinshed) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ JobMap::const_iterator i = m_downloadJobs.find(addonID);
+ if (i != m_downloadJobs.end())
+ {
+ percent = i->second.progress;
+ downloadFinshed = i->second.downloadFinshed;
+ return true;
+ }
+ return false;
+}
+
+bool CAddonInstaller::Cancel(const std::string &addonID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ JobMap::iterator i = m_downloadJobs.find(addonID);
+ if (i != m_downloadJobs.end())
+ {
+ CServiceBroker::GetJobManager()->CancelJob(i->second.jobID);
+ m_downloadJobs.erase(i);
+ if (m_downloadJobs.empty())
+ m_idle.Set();
+ return true;
+ }
+
+ return false;
+}
+
+bool CAddonInstaller::InstallModal(const std::string& addonID,
+ ADDON::AddonPtr& addon,
+ InstallModalPrompt promptForInstall)
+{
+ if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
+ return false;
+
+ // we assume that addons that are enabled don't get to this routine (i.e. that GetAddon() has been called)
+ if (CServiceBroker::GetAddonMgr().GetAddon(addonID, addon, OnlyEnabled::CHOICE_NO))
+ return false; // addon is installed but disabled, and the user has specifically activated something that needs
+ // the addon - should we enable it?
+
+ // check we have it available
+ if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon))
+ return false;
+
+ // if specified ask the user if he wants it installed
+ if (promptForInstall == InstallModalPrompt::CHOICE_YES)
+ {
+ if (HELPERS::ShowYesNoDialogLines(CVariant{24076}, CVariant{24100}, CVariant{addon->Name()},
+ CVariant{24101}) != DialogResponse::CHOICE_YES)
+ {
+ return false;
+ }
+ }
+
+ if (!InstallOrUpdate(addonID, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_YES))
+ return false;
+
+ return CServiceBroker::GetAddonMgr().GetAddon(addonID, addon, OnlyEnabled::CHOICE_YES);
+}
+
+
+bool CAddonInstaller::InstallOrUpdate(const std::string& addonID,
+ BackgroundJob background,
+ ModalJob modal)
+{
+ AddonPtr addon;
+ RepositoryPtr repo;
+ if (!CAddonInstallJob::GetAddon(addonID, repo, addon))
+ return false;
+
+ return DoInstall(addon, repo, background, modal, AutoUpdateJob::CHOICE_NO,
+ DependencyJob::CHOICE_NO, AllowCheckForUpdates::CHOICE_YES);
+}
+
+bool CAddonInstaller::InstallOrUpdateDependency(const ADDON::AddonPtr& dependsId,
+ const ADDON::RepositoryPtr& repo)
+{
+ return DoInstall(dependsId, repo, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_NO,
+ AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_YES,
+ AllowCheckForUpdates::CHOICE_YES);
+}
+
+bool CAddonInstaller::RemoveDependency(const std::shared_ptr<IAddon>& dependsId) const
+{
+ const bool removeData = CDirectory::Exists(dependsId->Profile());
+ CAddonUnInstallJob removeDependencyJob(dependsId, removeData);
+ removeDependencyJob.SetRecurseOrphaned(RecurseOrphaned::CHOICE_NO);
+
+ return removeDependencyJob.DoWork();
+}
+
+std::vector<std::string> CAddonInstaller::RemoveOrphanedDepsRecursively() const
+{
+ std::vector<std::string> removedItems;
+
+ auto toRemove = CServiceBroker::GetAddonMgr().GetOrphanedDependencies();
+ while (toRemove.size() > 0)
+ {
+ for (const auto& dep : toRemove)
+ {
+ if (RemoveDependency(dep))
+ {
+ removedItems.emplace_back(dep->Name()); // successfully removed
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "CAddonMgr::{}: failed to remove orphaned add-on/dependency: {}",
+ __func__, dep->Name());
+ }
+ }
+
+ toRemove = CServiceBroker::GetAddonMgr().GetOrphanedDependencies();
+ }
+
+ return removedItems;
+}
+
+bool CAddonInstaller::Install(const std::string& addonId,
+ const CAddonVersion& version,
+ const std::string& repoId)
+{
+ CLog::Log(LOGDEBUG, "CAddonInstaller: installing '{}' version '{}' from repository '{}'", addonId,
+ version.asString(), repoId);
+
+ AddonPtr addon;
+ CAddonDatabase database;
+
+ if (!database.Open() || !database.GetAddon(addonId, version, repoId, addon))
+ return false;
+
+ AddonPtr repo;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(repoId, repo, AddonType::REPOSITORY,
+ OnlyEnabled::CHOICE_YES))
+ return false;
+
+ return DoInstall(addon, std::static_pointer_cast<CRepository>(repo), BackgroundJob::CHOICE_YES,
+ ModalJob::CHOICE_NO, AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_NO,
+ AllowCheckForUpdates::CHOICE_YES);
+}
+
+bool CAddonInstaller::DoInstall(const AddonPtr& addon,
+ const RepositoryPtr& repo,
+ BackgroundJob background,
+ ModalJob modal,
+ AutoUpdateJob autoUpdate,
+ DependencyJob dependsInstall,
+ AllowCheckForUpdates allowCheckForUpdates)
+{
+ // check whether we already have the addon installing
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_downloadJobs.find(addon->ID()) != m_downloadJobs.end())
+ return false;
+
+ CAddonInstallJob* installJob = new CAddonInstallJob(addon, repo, autoUpdate);
+ if (background == BackgroundJob::CHOICE_YES)
+ {
+ // Workaround: because CAddonInstallJob is blocking waiting for other jobs, it needs to be run
+ // with priority dedicated.
+ unsigned int jobID =
+ CServiceBroker::GetJobManager()->AddJob(installJob, this, CJob::PRIORITY_DEDICATED);
+ m_downloadJobs.insert(make_pair(addon->ID(), CDownloadJob(jobID)));
+ m_idle.Reset();
+
+ return true;
+ }
+
+ m_downloadJobs.insert(make_pair(addon->ID(), CDownloadJob(0)));
+ m_idle.Reset();
+ lock.unlock();
+
+ installJob->SetDependsInstall(dependsInstall);
+ installJob->SetAllowCheckForUpdates(allowCheckForUpdates);
+
+ bool result = false;
+ if (modal == ModalJob::CHOICE_YES)
+ result = installJob->DoModal();
+ else
+ result = installJob->DoWork();
+ delete installJob;
+
+ lock.lock();
+ JobMap::iterator i = m_downloadJobs.find(addon->ID());
+ m_downloadJobs.erase(i);
+ if (m_downloadJobs.empty())
+ m_idle.Set();
+
+ return result;
+}
+
+bool CAddonInstaller::InstallFromZip(const std::string &path)
+{
+ if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
+ return false;
+
+ CLog::Log(LOGDEBUG, "CAddonInstaller: installing from zip '{}'", CURL::GetRedacted(path));
+
+ // grab the descriptive XML document from the zip, and read it in
+ CFileItemList items;
+ //! @bug some zip files return a single item (root folder) that we think is stored, so we don't use the zip:// protocol
+ CURL pathToUrl(path);
+ CURL zipDir = URIUtils::CreateArchivePath("zip", pathToUrl, "");
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (!CDirectory::GetDirectory(zipDir, items, "", DIR_FLAG_DEFAULTS) ||
+ items.Size() != 1 || !items[0]->m_bIsFolder)
+ {
+ if (eventLog)
+ eventLog->AddWithNotification(EventPtr(
+ new CNotificationEvent(24045, StringUtils::Format(g_localizeStrings.Get(24143), path),
+ "special://xbmc/media/icon256x256.png", EventLevel::Error)));
+
+ CLog::Log(
+ LOGERROR,
+ "CAddonInstaller: installing addon failed '{}' - itemsize: {}, first item is folder: {}",
+ CURL::GetRedacted(path), items.Size(), items[0]->m_bIsFolder);
+ return false;
+ }
+
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().LoadAddonDescription(items[0]->GetPath(), addon))
+ return DoInstall(addon, RepositoryPtr(), BackgroundJob::CHOICE_YES, ModalJob::CHOICE_NO,
+ AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_NO,
+ AllowCheckForUpdates::CHOICE_YES);
+
+ if (eventLog)
+ eventLog->AddWithNotification(EventPtr(
+ new CNotificationEvent(24045, StringUtils::Format(g_localizeStrings.Get(24143), path),
+ "special://xbmc/media/icon256x256.png", EventLevel::Error)));
+ return false;
+}
+
+bool CAddonInstaller::UnInstall(const AddonPtr& addon, bool removeData)
+{
+ CServiceBroker::GetJobManager()->AddJob(new CAddonUnInstallJob(addon, removeData), this);
+ return true;
+}
+
+bool CAddonInstaller::CheckDependencies(const AddonPtr& addon,
+ CAddonDatabase* database /* = nullptr */)
+{
+ std::pair<std::string, std::string> failedDep;
+ return CheckDependencies(addon, failedDep, database);
+}
+
+bool CAddonInstaller::CheckDependencies(const AddonPtr& addon,
+ std::pair<std::string, std::string>& failedDep,
+ CAddonDatabase* database /* = nullptr */)
+{
+ std::vector<std::string> preDeps;
+ preDeps.push_back(addon->ID());
+ CAddonDatabase localDB;
+ if (!database)
+ database = &localDB;
+
+ return CheckDependencies(addon, preDeps, *database, failedDep);
+}
+
+bool CAddonInstaller::CheckDependencies(const AddonPtr &addon,
+ std::vector<std::string>& preDeps, CAddonDatabase &database,
+ std::pair<std::string, std::string> &failedDep)
+{
+ if (addon == nullptr)
+ return true; // a nullptr addon has no dependencies
+
+ for (const auto& it : addon->GetDependencies())
+ {
+ const std::string &addonID = it.id;
+ const CAddonVersion& versionMin = it.versionMin;
+ const CAddonVersion& version = it.version;
+ bool optional = it.optional;
+ AddonPtr dep;
+ const bool haveInstalledAddon =
+ CServiceBroker::GetAddonMgr().GetAddon(addonID, dep, OnlyEnabled::CHOICE_NO);
+ if ((haveInstalledAddon && !dep->MeetsVersion(versionMin, version)) ||
+ (!haveInstalledAddon && !optional))
+ {
+ // we have it but our version isn't good enough, or we don't have it and we need it
+ if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, dep) ||
+ (dep && !dep->MeetsVersion(versionMin, version)))
+ {
+ // we don't have it in a repo, or we have it but the version isn't good enough, so dep isn't satisfied.
+ CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: requires {} version {} which is not available",
+ addon->ID(), addonID, version.asString());
+
+ // fill in the details of the failed dependency
+ failedDep.first = addonID;
+ failedDep.second = version.asString();
+
+ return false;
+ }
+ }
+
+ // need to enable the dependency
+ if (dep && CServiceBroker::GetAddonMgr().IsAddonDisabled(addonID) &&
+ !CServiceBroker::GetAddonMgr().EnableAddon(addonID))
+ {
+ return false;
+ }
+
+ // at this point we have our dep, or the dep is optional (and we don't have it) so check that it's OK as well
+ //! @todo should we assume that installed deps are OK?
+ if (dep && std::find(preDeps.begin(), preDeps.end(), dep->ID()) == preDeps.end())
+ {
+ preDeps.push_back(dep->ID());
+ if (!CheckDependencies(dep, preDeps, database, failedDep))
+ {
+ database.Close();
+ return false;
+ }
+ }
+ }
+ database.Close();
+
+ return true;
+}
+
+bool CAddonInstaller::HasJob(const std::string& ID) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_downloadJobs.find(ID) != m_downloadJobs.end();
+}
+
+void CAddonInstaller::PrunePackageCache()
+{
+ std::map<std::string, std::unique_ptr<CFileItemList>> packs;
+ int64_t size = EnumeratePackageFolder(packs);
+ int64_t limit = static_cast<int64_t>(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_addonPackageFolderSize) * 1024 * 1024;
+ if (size < limit)
+ return;
+
+ // Prune packages
+ // 1. Remove the largest packages, leaving at least 2 for each add-on
+ CFileItemList items;
+ CAddonDatabase db;
+ db.Open();
+ for (auto it = packs.begin(); it != packs.end(); ++it)
+ {
+ it->second->Sort(SortByLabel, SortOrderDescending);
+ for (int j = 2; j < it->second->Size(); j++)
+ items.Add(CFileItemPtr(new CFileItem(*it->second->Get(j))));
+ }
+
+ items.Sort(SortBySize, SortOrderDescending);
+ int i = 0;
+ while (size > limit && i < items.Size())
+ {
+ size -= items[i]->m_dwSize;
+ db.RemovePackage(items[i]->GetPath());
+ CFileUtils::DeleteItem(items[i++]);
+ }
+
+ if (size > limit)
+ {
+ // 2. Remove the oldest packages (leaving least 1 for each add-on)
+ items.Clear();
+ for (auto it = packs.begin(); it != packs.end(); ++it)
+ {
+ if (it->second->Size() > 1)
+ items.Add(CFileItemPtr(new CFileItem(*it->second->Get(1))));
+ }
+
+ items.Sort(SortByDate, SortOrderAscending);
+ i = 0;
+ while (size > limit && i < items.Size())
+ {
+ size -= items[i]->m_dwSize;
+ db.RemovePackage(items[i]->GetPath());
+ CFileUtils::DeleteItem(items[i++]);
+ }
+ }
+}
+
+void CAddonInstaller::InstallAddons(const VECADDONS& addons,
+ bool wait,
+ AllowCheckForUpdates allowCheckForUpdates)
+{
+ for (const auto& addon : addons)
+ {
+ AddonPtr toInstall;
+ RepositoryPtr repo;
+ if (CAddonInstallJob::GetAddon(addon->ID(), repo, toInstall))
+ DoInstall(toInstall, repo, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_NO,
+ AutoUpdateJob::CHOICE_YES, DependencyJob::CHOICE_NO, allowCheckForUpdates);
+ }
+ if (wait)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_downloadJobs.empty())
+ {
+ m_idle.Reset();
+ lock.unlock();
+ m_idle.Wait();
+ }
+ }
+}
+
+int64_t CAddonInstaller::EnumeratePackageFolder(
+ std::map<std::string, std::unique_ptr<CFileItemList>>& result)
+{
+ CFileItemList items;
+ CDirectory::GetDirectory("special://home/addons/packages/",items,".zip",DIR_FLAG_NO_FILE_DIRS);
+ int64_t size = 0;
+ for (int i = 0; i < items.Size(); i++)
+ {
+ if (items[i]->m_bIsFolder)
+ continue;
+
+ size += items[i]->m_dwSize;
+ std::string pack,dummy;
+ CAddonVersion::SplitFileName(pack, dummy, items[i]->GetLabel());
+ if (result.find(pack) == result.end())
+ result[pack] = std::make_unique<CFileItemList>();
+ result[pack]->Add(CFileItemPtr(new CFileItem(*items[i])));
+ }
+
+ return size;
+}
+
+CAddonInstallJob::CAddonInstallJob(const AddonPtr& addon,
+ const RepositoryPtr& repo,
+ AutoUpdateJob isAutoUpdate)
+ : m_addon(addon), m_repo(repo), m_isAutoUpdate(isAutoUpdate)
+{
+ AddonPtr dummy;
+ m_isUpdate = CServiceBroker::GetAddonMgr().GetAddon(addon->ID(), dummy, OnlyEnabled::CHOICE_NO);
+}
+
+bool CAddonInstallJob::GetAddon(const std::string& addonID, RepositoryPtr& repo,
+ ADDON::AddonPtr& addon)
+{
+ if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon))
+ return false;
+
+ AddonPtr tmp;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(addon->Origin(), tmp, AddonType::REPOSITORY,
+ OnlyEnabled::CHOICE_YES))
+ return false;
+
+ repo = std::static_pointer_cast<CRepository>(tmp);
+
+ return true;
+}
+
+bool CAddonInstallJob::DoWork()
+{
+ m_currentType = CAddonInstallJob::TYPE_DOWNLOAD;
+
+ SetTitle(StringUtils::Format(g_localizeStrings.Get(24057), m_addon->Name()));
+ SetProgress(0);
+
+ // check whether all the dependencies are available or not
+ SetText(g_localizeStrings.Get(24058));
+ std::pair<std::string, std::string> failedDep;
+ if (!CAddonInstaller::GetInstance().CheckDependencies(m_addon, failedDep))
+ {
+ std::string details =
+ StringUtils::Format(g_localizeStrings.Get(24142), failedDep.first, failedDep.second);
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: {}", m_addon->ID(), details);
+ ReportInstallError(m_addon->ID(), m_addon->ID(), details);
+ return false;
+ }
+
+ std::string installFrom;
+ {
+ // Addons are installed by downloading the .zip package on the server to the local
+ // packages folder, then extracting from the local .zip package into the addons folder
+ // Both these functions are achieved by "copying" using the vfs.
+
+ if (!m_repo && URIUtils::HasSlashAtEnd(m_addon->Path()))
+ { // passed in a folder - all we need do is copy it across
+ installFrom = m_addon->Path();
+ }
+ else
+ {
+ std::string path{m_addon->Path()};
+ TypedDigest hash;
+ if (m_repo)
+ {
+ CRepository::ResolveResult resolvedAddon = m_repo->ResolvePathAndHash(m_addon);
+ path = resolvedAddon.location;
+ hash = resolvedAddon.digest;
+ if (path.empty())
+ {
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to resolve addon install source path",
+ m_addon->ID());
+ ReportInstallError(m_addon->ID(), m_addon->ID());
+ return false;
+ }
+ }
+
+ CAddonDatabase db;
+ if (!db.Open())
+ {
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to open database", m_addon->ID());
+ ReportInstallError(m_addon->ID(), m_addon->ID());
+ return false;
+ }
+
+ std::string packageOriginalPath, packageFileName;
+ URIUtils::Split(path, packageOriginalPath, packageFileName);
+ // Use ChangeBasePath so the URL is decoded if necessary
+ const std::string packagePath = "special://home/addons/packages/";
+ //!@todo fix design flaw in file copying: We use CFileOperationJob to download the package from the internet
+ // to the local cache. It tries to be "smart" and decode the URL. But it never tells us what the result is,
+ // so if we try for example to download "http://localhost/a+b.zip" the result ends up in "a b.zip".
+ // First bug is that it actually decodes "+", which is not necessary except in query parts. Second bug
+ // is that we cannot know that it does this and what the result is so the package will not be found without
+ // using ChangeBasePath here (which is the same function the copying code uses and performs the translation).
+ std::string package = URIUtils::ChangeBasePath(packageOriginalPath, packageFileName, packagePath);
+
+ // check that we don't already have a valid copy
+ if (!hash.Empty())
+ {
+ std::string hashExisting;
+ if (db.GetPackageHash(m_addon->ID(), package, hashExisting) && hash.value != hashExisting)
+ {
+ db.RemovePackage(package);
+ }
+ if (CFile::Exists(package))
+ {
+ CFile::Delete(package);
+ }
+ }
+
+ // zip passed in - download + extract
+ if (!CFile::Exists(package))
+ {
+ if (!DownloadPackage(path, packagePath))
+ {
+ CFile::Delete(package);
+
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to download {}", m_addon->ID(),
+ package);
+ ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
+ return false;
+ }
+ }
+
+ // at this point we have the package - check that it is valid
+ SetText(g_localizeStrings.Get(24077));
+ if (!hash.Empty())
+ {
+ TypedDigest actualHash{hash.type, CUtil::GetFileDigest(package, hash.type)};
+ if (hash != actualHash)
+ {
+ CFile::Delete(package);
+
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: Hash mismatch after download. Expected {}, was {}",
+ m_addon->ID(), hash.value, actualHash.value);
+ ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
+ return false;
+ }
+
+ db.AddPackage(m_addon->ID(), package, hash.value);
+ }
+
+ // check if the archive is valid
+ CURL archive = URIUtils::CreateArchivePath("zip", CURL(package), "");
+
+ CFileItemList archivedFiles;
+ AddonPtr temp;
+ if (!CDirectory::GetDirectory(archive, archivedFiles, "", DIR_FLAG_DEFAULTS) ||
+ archivedFiles.Size() != 1 || !archivedFiles[0]->m_bIsFolder ||
+ !CServiceBroker::GetAddonMgr().LoadAddonDescription(archivedFiles[0]->GetPath(), temp))
+ {
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: invalid package {}", m_addon->ID(), package);
+ db.RemovePackage(package);
+ CFile::Delete(package);
+ ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
+ return false;
+ }
+
+ installFrom = package;
+ }
+ }
+
+ m_currentType = CAddonInstallJob::TYPE_INSTALL;
+
+ // run any pre-install functions
+ ADDON::OnPreInstall(m_addon);
+
+ if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon->ID()))
+ {
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to unload addon.", m_addon->ID());
+ return false;
+ }
+
+ // perform install
+ if (!Install(installFrom, m_repo))
+ return false;
+
+ // Load new installed and if successed replace defined m_addon here with new one
+ if (!CServiceBroker::GetAddonMgr().LoadAddon(m_addon->ID(), m_addon->Origin(),
+ m_addon->Version()) ||
+ !CServiceBroker::GetAddonMgr().GetAddon(m_addon->ID(), m_addon, OnlyEnabled::CHOICE_YES))
+ {
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to reload addon", m_addon->ID());
+ return false;
+ }
+
+ g_localizeStrings.LoadAddonStrings(URIUtils::AddFileToFolder(m_addon->Path(), "resources/language/"),
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_LANGUAGE), m_addon->ID());
+
+ ADDON::OnPostInstall(m_addon, m_isUpdate, IsModal());
+
+ // Write origin to database via addon manager, where this information is up-to-date.
+ // Needed to set origin correctly for new installed addons.
+
+ std::string origin;
+ if (m_addon->Origin() == ORIGIN_SYSTEM)
+ {
+ origin = ORIGIN_SYSTEM; // keep system add-on origin as ORIGIN_SYSTEM
+ }
+ else if (m_addon->HasMainType(AddonType::REPOSITORY))
+ {
+ origin = m_addon->ID(); // use own id as origin if repository
+
+ // if a repository is updated during the add-on migration process, we need to skip
+ // calling CheckForUpdates() on the repo to prevent deadlock issues during migration
+
+ if (m_allowCheckForUpdates == AllowCheckForUpdates::CHOICE_YES)
+ {
+ if (m_isUpdate)
+ {
+ CLog::Log(LOGDEBUG, "ADDONS: repository [{}] updated. now checking for content updates.",
+ m_addon->ID());
+ CServiceBroker::GetRepositoryUpdater().CheckForUpdates(
+ std::static_pointer_cast<CRepository>(m_addon), false);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "ADDONS: skipping CheckForUpdates() on repository [{}].", m_addon->ID());
+ }
+ }
+ else if (m_repo)
+ {
+ origin = m_repo->ID(); // use repo id as origin
+ }
+
+ CServiceBroker::GetAddonMgr().SetAddonOrigin(m_addon->ID(), origin, m_isUpdate);
+
+ if (m_dependsInstall == DependencyJob::CHOICE_YES)
+ {
+ CLog::Log(LOGDEBUG, "ADDONS: dependency [{}] will not be version checked and unpinned",
+ m_addon->ID());
+ }
+ else
+ {
+ // we only do pinning/unpinning for non-system add-ons
+ if (m_addon->Origin() != ORIGIN_SYSTEM)
+ {
+ // get all compatible versions of an addon-id regardless of their origin
+ // from all installed repositories
+ std::vector<std::shared_ptr<IAddon>> compatibleVersions =
+ CServiceBroker::GetAddonMgr().GetCompatibleVersions(m_addon->ID());
+
+ if (!m_addon->Origin().empty())
+ {
+ // handle add-ons that originate from a repository
+
+ // find the latest version for the origin we installed from
+ CAddonVersion latestVersion;
+ for (const auto& compatibleVersion : compatibleVersions)
+ {
+ if (compatibleVersion->Origin() == m_addon->Origin() &&
+ compatibleVersion->Version() > latestVersion)
+ {
+ latestVersion = compatibleVersion->Version();
+ }
+ }
+
+ if (m_addon->Version() == latestVersion)
+ {
+ // unpin the installed addon if it's the latest of its origin
+ CServiceBroker::GetAddonMgr().RemoveUpdateRuleFromList(m_addon->ID(),
+ AddonUpdateRule::PIN_OLD_VERSION);
+ CLog::Log(LOGDEBUG, "ADDONS: unpinned Addon: [{}] Origin: [{}] Version: [{}]",
+ m_addon->ID(), m_addon->Origin(), m_addon->Version().asString());
+ }
+ else
+ {
+ // pin if it is not the latest
+ CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_addon->ID(),
+ AddonUpdateRule::PIN_OLD_VERSION);
+ CLog::Log(LOGDEBUG, "ADDONS: pinned Addon: [{}] Origin: [{}] Version: [{}]",
+ m_addon->ID(), m_addon->Origin(), m_addon->Version().asString());
+ }
+ }
+ else
+ {
+ // handle manually installed add-ons
+
+ // find the latest version of any origin/repository
+ CAddonVersion latestVersion;
+ for (const auto& compatibleVersion : compatibleVersions)
+ {
+ if (compatibleVersion->Version() > latestVersion)
+ {
+ latestVersion = compatibleVersion->Version();
+ }
+ }
+
+ if (m_addon->Version() < latestVersion)
+ {
+ // pin zip version if it's lesser than latest from repo(s)
+ CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_addon->ID(),
+ AddonUpdateRule::PIN_ZIP_INSTALL);
+ CLog::Log(LOGDEBUG, "ADDONS: pinned zip installed Addon: [{}] Version: [{}]",
+ m_addon->ID(), m_addon->Version().asString());
+ }
+ else
+ {
+ // unpin zip version if it's >= the latest from repos
+ CServiceBroker::GetAddonMgr().RemoveUpdateRuleFromList(m_addon->ID(),
+ AddonUpdateRule::PIN_ZIP_INSTALL);
+ CLog::Log(LOGDEBUG, "ADDONS: unpinned zip installed Addon: [{}] Version: [{}]",
+ m_addon->ID(), m_addon->Version().asString());
+ }
+ }
+ }
+ }
+
+ bool notify = (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_ADDONS_NOTIFICATIONS) ||
+ m_isAutoUpdate == AutoUpdateJob::CHOICE_NO) &&
+ !IsModal() && m_dependsInstall == DependencyJob::CHOICE_NO;
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, m_isUpdate ? 24065 : 24084)), notify,
+ false);
+
+ if (m_isAutoUpdate == AutoUpdateJob::CHOICE_YES &&
+ m_addon->LifecycleState() == AddonLifecycleState::BROKEN)
+ {
+ CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: auto-disabling due to being marked as broken",
+ m_addon->ID());
+ CServiceBroker::GetAddonMgr().DisableAddon(m_addon->ID(), AddonDisabledReason::USER);
+ if (eventLog)
+ eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, 24094)), true, false);
+ }
+ else if (m_addon->LifecycleState() == AddonLifecycleState::DEPRECATED)
+ {
+ CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: installed addon marked as deprecated",
+ m_addon->ID());
+ std::string text =
+ StringUtils::Format(g_localizeStrings.Get(24168), m_addon->LifecycleStateDescription());
+ if (eventLog)
+ eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, text)), true, false);
+ }
+
+ // and we're done!
+ MarkFinished();
+ return true;
+}
+
+bool CAddonInstallJob::DownloadPackage(const std::string &path, const std::string &dest)
+{
+ if (ShouldCancel(0, 1))
+ return false;
+
+ SetText(g_localizeStrings.Get(24078));
+
+ // need to download/copy the package first
+ CFileItemList list;
+ list.Add(CFileItemPtr(new CFileItem(path, false)));
+ list[0]->Select(true);
+
+ return DoFileOperation(CFileOperationJob::ActionReplace, list, dest, true);
+}
+
+bool CAddonInstallJob::DoFileOperation(FileAction action, CFileItemList &items, const std::string &file, bool useSameJob /* = true */)
+{
+ bool result = false;
+ if (useSameJob)
+ {
+ SetFileOperation(action, items, file);
+
+ // temporarily disable auto-closing so not to close the current progress indicator
+ bool autoClose = GetAutoClose();
+ if (autoClose)
+ SetAutoClose(false);
+ // temporarily disable updating title or text
+ bool updateInformation = GetUpdateInformation();
+ if (updateInformation)
+ SetUpdateInformation(false);
+
+ result = CFileOperationJob::DoWork();
+
+ SetUpdateInformation(updateInformation);
+ SetAutoClose(autoClose);
+ }
+ else
+ {
+ CFileOperationJob job(action, items, file);
+
+ // pass our progress indicators to the temporary job and only allow it to
+ // show progress updates (no title or text changes)
+ job.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), GetUpdateProgress(), false);
+
+ result = job.DoWork();
+ }
+
+ return result;
+}
+
+bool CAddonInstallJob::Install(const std::string &installFrom, const RepositoryPtr& repo)
+{
+ const auto& deps = m_addon->GetDependencies();
+
+ if (!deps.empty() && m_addon->HasType(AddonType::REPOSITORY))
+ {
+ bool notSystemAddon = std::none_of(deps.begin(), deps.end(), [](const DependencyInfo& dep) {
+ return CServiceBroker::GetAddonMgr().IsSystemAddon(dep.id);
+ });
+
+ if (notSystemAddon)
+ {
+ CLog::Log(
+ LOGERROR,
+ "CAddonInstallJob::{}: failed to install repository [{}]. It has dependencies defined",
+ __func__, m_addon->ID());
+ ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24088));
+ return false;
+ }
+ }
+
+ SetText(g_localizeStrings.Get(24079));
+ unsigned int totalSteps = static_cast<unsigned int>(deps.size()) + 1;
+ if (ShouldCancel(0, totalSteps))
+ return false;
+
+ CAddonRepos addonRepos;
+ if (!addonRepos.IsValid())
+ return false;
+
+ // The first thing we do is install dependencies
+ for (auto it = deps.begin(); it != deps.end(); ++it)
+ {
+ if (it->id != "xbmc.metadata")
+ {
+ const std::string &addonID = it->id;
+ const CAddonVersion& versionMin = it->versionMin;
+ const CAddonVersion& version = it->version;
+ bool optional = it->optional;
+ AddonPtr dependency;
+ const bool haveInstalledAddon =
+ CServiceBroker::GetAddonMgr().GetAddon(addonID, dependency, OnlyEnabled::CHOICE_NO);
+ if ((haveInstalledAddon && !dependency->MeetsVersion(versionMin, version)) ||
+ (!haveInstalledAddon && !optional))
+ {
+ // we have it but our version isn't good enough, or we don't have it and we need it
+
+ // dependency is already queued up for install - ::Install will fail
+ // instead we wait until the Job has finished. note that we
+ // recall install on purpose in case prior installation failed
+ if (CAddonInstaller::GetInstance().HasJob(addonID))
+ {
+ while (CAddonInstaller::GetInstance().HasJob(addonID))
+ KODI::TIME::Sleep(50ms);
+
+ if (!CServiceBroker::GetAddonMgr().IsAddonInstalled(addonID))
+ {
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}",
+ m_addon->ID(), addonID);
+ ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
+ return false;
+ }
+ }
+ // don't have the addon or the addon isn't new enough - grab it (no new job for these)
+ else
+ {
+ RepositoryPtr repoForDep;
+ AddonPtr dependencyToInstall;
+
+ // origin of m_addon is empty at least if an addon is installed for the first time
+ // we need to override "parentRepoId" if the passed in repo is valid.
+
+ const std::string& parentRepoId =
+ m_addon->Origin().empty() && repo ? repo->ID() : m_addon->Origin();
+
+ if (!addonRepos.FindDependency(addonID, parentRepoId, dependencyToInstall, repoForDep))
+ {
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to find dependency {}", m_addon->ID(),
+ addonID);
+ ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
+ return false;
+ }
+ else
+ {
+ if (!dependencyToInstall->MeetsVersion(versionMin, version))
+ {
+ CLog::Log(LOGERROR,
+ "CAddonInstallJob[{}]: found dependency [{}/{}] doesn't meet minimum "
+ "version [{}]",
+ m_addon->ID(), addonID, dependencyToInstall->Version().asString(),
+ versionMin.asString());
+ ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
+ return false;
+ }
+ }
+
+ if (IsModal())
+ {
+ CAddonInstallJob dependencyJob(dependencyToInstall, repoForDep,
+ AutoUpdateJob::CHOICE_NO);
+ dependencyJob.SetDependsInstall(DependencyJob::CHOICE_YES);
+
+ // pass our progress indicators to the temporary job and don't allow it to
+ // show progress or information updates (no progress, title or text changes)
+ dependencyJob.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), false,
+ false);
+
+ if (!dependencyJob.DoModal())
+ {
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}",
+ m_addon->ID(), addonID);
+ ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
+ return false;
+ }
+ }
+ else if (!CAddonInstaller::GetInstance().InstallOrUpdateDependency(dependencyToInstall,
+ repoForDep))
+ {
+ CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}",
+ m_addon->ID(), dependencyToInstall->ID());
+ ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
+ return false;
+ }
+ }
+ }
+ }
+
+ if (ShouldCancel(std::distance(deps.begin(), it), totalSteps))
+ return false;
+ }
+
+ SetText(g_localizeStrings.Get(24086));
+ SetProgress(static_cast<unsigned int>(100.0 * (totalSteps - 1.0) / totalSteps));
+
+ CFilesystemInstaller fsInstaller;
+ if (!fsInstaller.InstallToFilesystem(installFrom, m_addon->ID()))
+ {
+ ReportInstallError(m_addon->ID(), m_addon->ID());
+ return false;
+ }
+
+ SetProgress(100);
+
+ return true;
+}
+
+void CAddonInstallJob::ReportInstallError(const std::string& addonID, const std::string& fileName, const std::string& message /* = "" */)
+{
+ AddonPtr addon;
+ CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon);
+
+ MarkFinished();
+
+ std::string msg = message;
+ EventPtr activity;
+ if (addon != nullptr)
+ {
+ AddonPtr addon2;
+ bool success = CServiceBroker::GetAddonMgr().GetAddon(addonID, addon2, OnlyEnabled::CHOICE_YES);
+ if (msg.empty())
+ {
+ msg = g_localizeStrings.Get(addon2 != nullptr && success ? 113 : 114);
+ }
+
+ activity = EventPtr(new CAddonManagementEvent(addon, EventLevel::Error, msg));
+ if (IsModal())
+ HELPERS::ShowOKDialogText(CVariant{m_addon->Name()}, CVariant{msg});
+ }
+ else
+ {
+ activity = EventPtr(new CNotificationEvent(
+ 24045, !msg.empty() ? msg : StringUtils::Format(g_localizeStrings.Get(24143), fileName),
+ EventLevel::Error));
+
+ if (IsModal())
+ HELPERS::ShowOKDialogText(CVariant{fileName}, CVariant{msg});
+ }
+
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(activity, !IsModal(), false);
+}
+
+CAddonUnInstallJob::CAddonUnInstallJob(const AddonPtr &addon, bool removeData)
+ : m_addon(addon), m_removeData(removeData)
+{ }
+
+bool CAddonUnInstallJob::DoWork()
+{
+ ADDON::OnPreUnInstall(m_addon);
+
+ //Unregister addon with the manager to ensure nothing tries
+ //to interact with it while we are uninstalling.
+ if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon->ID()))
+ {
+ CLog::Log(LOGERROR, "CAddonUnInstallJob[{}]: failed to unload addon.", m_addon->ID());
+ return false;
+ }
+
+ CFilesystemInstaller fsInstaller;
+ if (!fsInstaller.UnInstallFromFilesystem(m_addon->Path()))
+ {
+ CLog::Log(LOGERROR, "CAddonUnInstallJob[{}]: could not delete addon data.", m_addon->ID());
+ return false;
+ }
+
+ ClearFavourites();
+ if (m_removeData)
+ {
+ CFileUtils::DeleteItem(m_addon->Profile());
+ }
+
+ AddonPtr addon;
+
+ // try to get the addon object from the repository as the local one does not exist anymore
+ // if that doesn't work fall back to the local one
+ if (!CServiceBroker::GetAddonMgr().FindInstallableById(m_addon->ID(), addon) || addon == nullptr)
+ {
+ addon = m_addon;
+ }
+
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(new CAddonManagementEvent(addon, 24144))); // Add-on uninstalled
+
+ CServiceBroker::GetAddonMgr().OnPostUnInstall(m_addon->ID());
+
+ CAddonDatabase database;
+ if (database.Open())
+ database.OnPostUnInstall(m_addon->ID());
+
+ ADDON::OnPostUnInstall(m_addon);
+
+ if (m_recurseOrphaned == RecurseOrphaned::CHOICE_YES)
+ {
+ const auto removedItems = CAddonInstaller::GetInstance().RemoveOrphanedDepsRecursively();
+
+ if (removedItems.size() > 0)
+ {
+ CLog::Log(LOGINFO, "CAddonUnInstallJob[{}]: removed orphaned dependencies ({})",
+ m_addon->ID(), StringUtils::Join(removedItems, ", "));
+ }
+ }
+
+ return true;
+}
+
+void CAddonUnInstallJob::ClearFavourites()
+{
+ bool bSave = false;
+ CFileItemList items;
+ CServiceBroker::GetFavouritesService().GetAll(items);
+ for (int i = 0; i < items.Size(); i++)
+ {
+ if (items[i]->GetPath().find(m_addon->ID()) != std::string::npos)
+ {
+ items.Remove(items[i].get());
+ bSave = true;
+ }
+ }
+
+ if (bSave)
+ CServiceBroker::GetFavouritesService().Save(items);
+}
diff --git a/xbmc/addons/AddonInstaller.h b/xbmc/addons/AddonInstaller.h
new file mode 100644
index 0000000..727e9e9
--- /dev/null
+++ b/xbmc/addons/AddonInstaller.h
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2011-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/Event.h"
+#include "utils/Job.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CFileItemList;
+
+namespace ADDON
+{
+
+class CAddonVersion;
+
+class CAddonDatabase;
+
+class CRepository;
+using RepositoryPtr = std::shared_ptr<CRepository>;
+
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+using VECADDONS = std::vector<AddonPtr>;
+
+enum class BackgroundJob : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+enum class ModalJob : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+enum class AutoUpdateJob : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+enum class DependencyJob : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+enum class InstallModalPrompt : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+enum class AllowCheckForUpdates : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+enum class RecurseOrphaned : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+class CAddonInstaller : public IJobCallback
+{
+public:
+ static CAddonInstaller &GetInstance();
+
+ bool IsDownloading() const;
+ void GetInstallList(ADDON::VECADDONS &addons) const;
+ bool GetProgress(const std::string& addonID, unsigned int& percent, bool& downloadFinshed) const;
+ bool Cancel(const std::string &addonID);
+
+ /*! \brief Installs the addon while showing a modal progress dialog
+ \param addonID the addon ID of the item to install.
+ \param addon [out] the installed addon for later use.
+ \param promptForInstall Whether or not to prompt the user before installing the addon.
+ \return true on successful install, false otherwise.
+ \sa Install
+ */
+ bool InstallModal(const std::string& addonID,
+ ADDON::AddonPtr& addon,
+ InstallModalPrompt promptForInstall);
+
+ /*! \brief Install an addon if it is available in a repository
+ \param addonID the addon ID of the item to install
+ \param background whether to install in the background or not.
+ \param modal whether to show a modal dialog when not installing in background
+ \return true on successful install, false on failure.
+ \sa DoInstall
+ */
+ bool InstallOrUpdate(const std::string& addonID, BackgroundJob background, ModalJob modal);
+
+ /*! \brief Install a dependency from a specific repository
+ \param dependsId the dependency to install
+ \param repo the repository to install the addon from
+ \return true on successful install, false on failure.
+ \sa DoInstall
+ */
+ bool InstallOrUpdateDependency(const ADDON::AddonPtr& dependsId,
+ const ADDON::RepositoryPtr& repo);
+
+ /*! \brief Remove a single dependency from the system
+ \param dependsId the dependency to remove
+ \return true on successful uninstall, false on failure.
+ */
+ bool RemoveDependency(const std::shared_ptr<IAddon>& dependsId) const;
+
+ /*!
+ * \brief Removes all orphaned add-ons recursively. Removal may orphan further
+ * add-ons/dependencies, so loop until no orphaned is left on the system
+ * \return Names of add-ons that have effectively been removed
+ */
+ std::vector<std::string> RemoveOrphanedDepsRecursively() const;
+
+ /*! \brief Installs a vector of addons
+ * \param addons the list of addons to install
+ * \param wait if the method should wait for all the DoInstall jobs to finish or if it should return right away
+ * \param allowCheckForUpdates indicates if content update checks are allowed
+ * after installation of a repository addon from the vector
+ * \sa DoInstall
+ */
+ void InstallAddons(const ADDON::VECADDONS& addons,
+ bool wait,
+ AllowCheckForUpdates allowCheckForUpdates);
+
+ /*! \brief Install an addon from the given zip path
+ \param path the zip file to install from
+ \return true if successful, false otherwise
+ \sa DoInstall
+ */
+ bool InstallFromZip(const std::string &path);
+
+ /*! Install an addon with a specific version and repository */
+ bool Install(const std::string& addonId,
+ const ADDON::CAddonVersion& version,
+ const std::string& repoId);
+
+ /*! Uninstall an addon, remove addon data if requested */
+ bool UnInstall(const ADDON::AddonPtr& addon, bool removeData);
+
+ /*! \brief Check whether dependencies of an addon exist or are installable.
+ Iterates through the addon's dependencies, checking they're installed or installable.
+ Each dependency must also satisfies CheckDependencies in turn.
+ \param addon the addon to check
+ \param database the database instance to update. Defaults to NULL.
+ \return true if dependencies are available, false otherwise.
+ */
+ bool CheckDependencies(const ADDON::AddonPtr& addon, CAddonDatabase* database = nullptr);
+
+ /*! \brief Check whether dependencies of an addon exist or are installable.
+ Iterates through the addon's dependencies, checking they're installed or installable.
+ Each dependency must also satisfies CheckDependencies in turn.
+ \param addon the addon to check
+ \param failedDep Dependency addon that isn't available
+ \param database the database instance to update. Defaults to NULL.
+ \return true if dependencies are available, false otherwise.
+ */
+ bool CheckDependencies(const ADDON::AddonPtr& addon,
+ std::pair<std::string, std::string>& failedDep,
+ CAddonDatabase* database = nullptr);
+
+ /*! \brief Check if an installation job for a given add-on is already queued up
+ * \param ID The ID of the add-on
+ * \return true if a job exists, false otherwise
+ */
+ bool HasJob(const std::string& ID) const;
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob* job) override;
+ void OnJobProgress(unsigned int jobID, unsigned int progress, unsigned int total, const CJob *job) override;
+
+ class CDownloadJob
+ {
+ public:
+ explicit CDownloadJob(unsigned int id) : jobID(id) { }
+
+ unsigned int jobID;
+ unsigned int progress = 0;
+ bool downloadFinshed = false;
+ };
+
+ typedef std::map<std::string, CDownloadJob> JobMap;
+
+private:
+ // private construction, and no assignments; use the provided singleton methods
+ CAddonInstaller();
+ CAddonInstaller(const CAddonInstaller&) = delete;
+ CAddonInstaller const& operator=(CAddonInstaller const&) = delete;
+ ~CAddonInstaller() override;
+
+ /*! \brief Install an addon from a repository or zip
+ * \param addon the AddonPtr describing the addon
+ * \param repo the repository to install addon from
+ * \param background whether to install in the background or not.
+ * \param modal whether to install in modal mode or not.
+ * \param autoUpdate whether the addon is installed in auto update mode.
+ * (i.e. no notification)
+ * \param dependsInstall whether this is the installation of a dependency addon
+ * \param allowCheckForUpdates whether content update check after installation of
+ * a repository addon is allowed
+ * \return true on successful install, false on failure.
+ */
+ bool DoInstall(const ADDON::AddonPtr& addon,
+ const ADDON::RepositoryPtr& repo,
+ BackgroundJob background,
+ ModalJob modal,
+ AutoUpdateJob autoUpdate,
+ DependencyJob dependsInstall,
+ AllowCheckForUpdates allowCheckForUpdates);
+
+ /*! \brief Check whether dependencies of an addon exist or are installable.
+ Iterates through the addon's dependencies, checking they're installed or installable.
+ Each dependency must also satisfies CheckDependencies in turn.
+ \param addon the addon to check
+ \param preDeps previous dependencies encountered during recursion. aids in avoiding infinite recursion
+ \param database database instance to update
+ \param failedDep Dependency addon that isn't available
+ \return true if dependencies are available, false otherwise.
+ */
+ bool CheckDependencies(const ADDON::AddonPtr &addon, std::vector<std::string>& preDeps, CAddonDatabase &database, std::pair<std::string, std::string> &failedDep);
+
+ void PrunePackageCache();
+ int64_t EnumeratePackageFolder(std::map<std::string, std::unique_ptr<CFileItemList>>& result);
+
+ mutable CCriticalSection m_critSection;
+ JobMap m_downloadJobs;
+ CEvent m_idle;
+};
+
+}; // namespace ADDON
diff --git a/xbmc/addons/AddonManager.cpp b/xbmc/addons/AddonManager.cpp
new file mode 100644
index 0000000..06d7dcf
--- /dev/null
+++ b/xbmc/addons/AddonManager.cpp
@@ -0,0 +1,1391 @@
+/*
+ * 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 "AddonManager.h"
+
+#include "CompileInfo.h"
+#include "FileItem.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "addons/AddonBuilder.h"
+#include "addons/AddonDatabase.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonRepos.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/AddonUpdateRules.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonInfoBuilder.h"
+#include "addons/addoninfo/AddonType.h"
+#include "events/AddonManagementEvent.h"
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <array>
+#include <mutex>
+#include <set>
+#include <utility>
+
+using namespace XFILE;
+
+namespace ADDON
+{
+
+/**********************************************************
+ * CAddonMgr
+ *
+ */
+
+std::map<AddonType, IAddonMgrCallback*> CAddonMgr::m_managers;
+
+static bool LoadManifest(std::set<std::string>& system, std::set<std::string>& optional)
+{
+ CXBMCTinyXML doc;
+ if (!doc.LoadFile("special://xbmc/system/addon-manifest.xml"))
+ {
+ CLog::Log(LOGERROR, "ADDONS: manifest missing");
+ return false;
+ }
+
+ auto root = doc.RootElement();
+ if (!root || root->ValueStr() != "addons")
+ {
+ CLog::Log(LOGERROR, "ADDONS: malformed manifest");
+ return false;
+ }
+
+ auto elem = root->FirstChildElement("addon");
+ while (elem)
+ {
+ if (elem->FirstChild())
+ {
+ if (XMLUtils::GetAttribute(elem, "optional") == "true")
+ optional.insert(elem->FirstChild()->ValueStr());
+ else
+ system.insert(elem->FirstChild()->ValueStr());
+ }
+ elem = elem->NextSiblingElement("addon");
+ }
+ return true;
+}
+
+CAddonMgr::CAddonMgr()
+ : m_database(std::make_unique<CAddonDatabase>()),
+ m_updateRules(std::make_unique<CAddonUpdateRules>())
+{
+}
+
+CAddonMgr::~CAddonMgr()
+{
+ DeInit();
+}
+
+IAddonMgrCallback* CAddonMgr::GetCallbackForType(AddonType type)
+{
+ if (m_managers.find(type) == m_managers.end())
+ return nullptr;
+ else
+ return m_managers[type];
+}
+
+bool CAddonMgr::RegisterAddonMgrCallback(AddonType type, IAddonMgrCallback* cb)
+{
+ if (cb == nullptr)
+ return false;
+
+ m_managers.erase(type);
+ m_managers[type] = cb;
+
+ return true;
+}
+
+void CAddonMgr::UnregisterAddonMgrCallback(AddonType type)
+{
+ m_managers.erase(type);
+}
+
+bool CAddonMgr::Init()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!LoadManifest(m_systemAddons, m_optionalSystemAddons))
+ {
+ CLog::Log(LOGERROR, "ADDONS: Failed to read manifest");
+ return false;
+ }
+
+ if (!m_database->Open())
+ CLog::Log(LOGFATAL, "ADDONS: Failed to open database");
+
+ FindAddons();
+
+ //Ensure required add-ons are installed and enabled
+ for (const auto& id : m_systemAddons)
+ {
+ AddonPtr addon;
+ if (!GetAddon(id, addon, AddonType::UNKNOWN, OnlyEnabled::CHOICE_YES))
+ {
+ CLog::Log(LOGFATAL, "addon '{}' not installed or not enabled.", id);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void CAddonMgr::DeInit()
+{
+ m_database->Close();
+
+ /* If temporary directory was used from add-on, delete it */
+ if (XFILE::CDirectory::Exists(m_tempAddonBasePath))
+ XFILE::CDirectory::RemoveRecursive(CSpecialProtocol::TranslatePath(m_tempAddonBasePath));
+}
+
+bool CAddonMgr::HasAddons(AddonType type)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& addonInfo : m_installedAddons)
+ {
+ if (addonInfo.second->HasType(type) && !IsAddonDisabled(addonInfo.second->ID()))
+ return true;
+ }
+ return false;
+}
+
+bool CAddonMgr::HasInstalledAddons(AddonType type)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& addonInfo : m_installedAddons)
+ {
+ if (addonInfo.second->HasType(type))
+ return true;
+ }
+ return false;
+}
+
+void CAddonMgr::AddToUpdateableAddons(AddonPtr &pAddon)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_updateableAddons.push_back(pAddon);
+}
+
+void CAddonMgr::RemoveFromUpdateableAddons(AddonPtr &pAddon)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ VECADDONS::iterator it = std::find(m_updateableAddons.begin(), m_updateableAddons.end(), pAddon);
+
+ if(it != m_updateableAddons.end())
+ {
+ m_updateableAddons.erase(it);
+ }
+}
+
+struct AddonIdFinder
+{
+ explicit AddonIdFinder(const std::string& id)
+ : m_id(id)
+ {}
+
+ bool operator()(const AddonPtr& addon)
+ {
+ return m_id == addon->ID();
+ }
+ private:
+ std::string m_id;
+};
+
+bool CAddonMgr::ReloadSettings(const std::string& addonId, AddonInstanceId instanceId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ VECADDONS::iterator it =
+ std::find_if(m_updateableAddons.begin(), m_updateableAddons.end(), AddonIdFinder(addonId));
+
+ if( it != m_updateableAddons.end())
+ {
+ return (*it)->ReloadSettings(instanceId);
+ }
+ return false;
+}
+
+std::vector<std::shared_ptr<IAddon>> CAddonMgr::GetAvailableUpdates() const
+{
+ std::vector<std::shared_ptr<IAddon>> availableUpdates =
+ GetAvailableUpdatesOrOutdatedAddons(AddonCheckType::AVAILABLE_UPDATES);
+
+ std::lock_guard<std::mutex> lock(m_lastAvailableUpdatesCountMutex);
+ m_lastAvailableUpdatesCountAsString = std::to_string(availableUpdates.size());
+
+ return availableUpdates;
+}
+
+const std::string& CAddonMgr::GetLastAvailableUpdatesCountAsString() const
+{
+ std::lock_guard<std::mutex> lock(m_lastAvailableUpdatesCountMutex);
+ return m_lastAvailableUpdatesCountAsString;
+};
+
+std::vector<std::shared_ptr<IAddon>> CAddonMgr::GetOutdatedAddons() const
+{
+ return GetAvailableUpdatesOrOutdatedAddons(AddonCheckType::OUTDATED_ADDONS);
+}
+
+std::vector<std::shared_ptr<IAddon>> CAddonMgr::GetAvailableUpdatesOrOutdatedAddons(
+ AddonCheckType addonCheckType) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto start = std::chrono::steady_clock::now();
+
+ std::vector<std::shared_ptr<IAddon>> result;
+ std::vector<std::shared_ptr<IAddon>> installed;
+ CAddonRepos addonRepos;
+
+ if (addonRepos.IsValid())
+ {
+ GetAddonsForUpdate(installed);
+ addonRepos.BuildUpdateOrOutdatedList(installed, result, addonCheckType);
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "CAddonMgr::GetAvailableUpdatesOrOutdatedAddons took {} ms",
+ duration.count());
+
+ return result;
+}
+
+std::map<std::string, AddonWithUpdate> CAddonMgr::GetAddonsWithAvailableUpdate() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto start = std::chrono::steady_clock::now();
+
+ std::vector<std::shared_ptr<IAddon>> installed;
+ std::map<std::string, AddonWithUpdate> result;
+ CAddonRepos addonRepos;
+
+ if (addonRepos.IsValid())
+ {
+ GetAddonsForUpdate(installed);
+ addonRepos.BuildAddonsWithUpdateList(installed, result);
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "CAddonMgr::{} took {} ms", __func__, duration.count());
+
+ return result;
+}
+
+std::vector<std::shared_ptr<IAddon>> CAddonMgr::GetCompatibleVersions(
+ const std::string& addonId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto start = std::chrono::steady_clock::now();
+
+ CAddonRepos addonRepos(addonId);
+ std::vector<std::shared_ptr<IAddon>> result;
+
+ if (addonRepos.IsValid())
+ addonRepos.BuildCompatibleVersionsList(result);
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "CAddonMgr::{} took {} ms", __func__, duration.count());
+
+ return result;
+}
+
+bool CAddonMgr::HasAvailableUpdates()
+{
+ return !GetAvailableUpdates().empty();
+}
+
+std::vector<std::shared_ptr<IAddon>> CAddonMgr::GetOrphanedDependencies() const
+{
+ std::vector<std::shared_ptr<IAddon>> allAddons;
+ GetAddonsInternal(AddonType::UNKNOWN, allAddons, OnlyEnabled::CHOICE_NO,
+ CheckIncompatible::CHOICE_YES);
+
+ std::vector<std::shared_ptr<IAddon>> orphanedDependencies;
+ for (const auto& addon : allAddons)
+ {
+ if (IsOrphaned(addon, allAddons))
+ {
+ orphanedDependencies.emplace_back(addon);
+ }
+ }
+
+ return orphanedDependencies;
+}
+
+bool CAddonMgr::IsOrphaned(const std::shared_ptr<IAddon>& addon,
+ const std::vector<std::shared_ptr<IAddon>>& allAddons) const
+{
+ if (CServiceBroker::GetAddonMgr().IsSystemAddon(addon->ID()) ||
+ !CAddonType::IsDependencyType(addon->MainType()))
+ return false;
+
+ auto dependsOnCapturedAddon = [&addon](const std::shared_ptr<IAddon>& _) {
+ const auto& deps = _->GetDependencies();
+ return std::any_of(deps.begin(), deps.end(),
+ [&addon](const DependencyInfo& dep) { return dep.id == addon->ID(); });
+ };
+
+ return std::none_of(allAddons.begin(), allAddons.end(), dependsOnCapturedAddon);
+}
+
+bool CAddonMgr::GetAddonsForUpdate(VECADDONS& addons) const
+{
+ return GetAddonsInternal(AddonType::UNKNOWN, addons, OnlyEnabled::CHOICE_YES,
+ CheckIncompatible::CHOICE_YES);
+}
+
+bool CAddonMgr::GetAddons(VECADDONS& addons) const
+{
+ return GetAddonsInternal(AddonType::UNKNOWN, addons, OnlyEnabled::CHOICE_YES,
+ CheckIncompatible::CHOICE_NO);
+}
+
+bool CAddonMgr::GetAddons(VECADDONS& addons, AddonType type)
+{
+ return GetAddonsInternal(type, addons, OnlyEnabled::CHOICE_YES, CheckIncompatible::CHOICE_NO);
+}
+
+bool CAddonMgr::GetInstalledAddons(VECADDONS& addons)
+{
+ return GetAddonsInternal(AddonType::UNKNOWN, addons, OnlyEnabled::CHOICE_NO,
+ CheckIncompatible::CHOICE_NO);
+}
+
+bool CAddonMgr::GetInstalledAddons(VECADDONS& addons, AddonType type)
+{
+ return GetAddonsInternal(type, addons, OnlyEnabled::CHOICE_NO, CheckIncompatible::CHOICE_NO);
+}
+
+bool CAddonMgr::GetDisabledAddons(VECADDONS& addons)
+{
+ return CAddonMgr::GetDisabledAddons(addons, AddonType::UNKNOWN);
+}
+
+bool CAddonMgr::GetDisabledAddons(VECADDONS& addons, AddonType type)
+{
+ VECADDONS all;
+ if (GetInstalledAddons(all, type))
+ {
+ std::copy_if(all.begin(), all.end(), std::back_inserter(addons),
+ [this](const AddonPtr& addon){ return IsAddonDisabled(addon->ID()); });
+ return true;
+ }
+ return false;
+}
+
+bool CAddonMgr::GetInstallableAddons(VECADDONS& addons)
+{
+ return GetInstallableAddons(addons, AddonType::UNKNOWN);
+}
+
+bool CAddonMgr::GetInstallableAddons(VECADDONS& addons, AddonType type)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CAddonRepos addonRepos;
+
+ if (!addonRepos.IsValid())
+ return false;
+
+ // get all addons
+ addonRepos.GetLatestAddonVersions(addons);
+
+ // go through all addons and remove all that are already installed
+
+ addons.erase(std::remove_if(addons.begin(), addons.end(),
+ [this, type](const AddonPtr& addon)
+ {
+ bool bErase = false;
+
+ // check if the addon matches the provided addon type
+ if (type != AddonType::UNKNOWN && addon->Type() != type && !addon->HasType(type))
+ bErase = true;
+
+ if (!this->CanAddonBeInstalled(addon))
+ bErase = true;
+
+ return bErase;
+ }), addons.end());
+
+ return true;
+}
+
+bool CAddonMgr::FindInstallableById(const std::string& addonId, AddonPtr& result)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CAddonRepos addonRepos(addonId);
+ if (!addonRepos.IsValid())
+ return false;
+
+ AddonPtr addonToUpdate;
+
+ // check for an update if addon is installed already
+
+ if (GetAddon(addonId, addonToUpdate, AddonType::UNKNOWN, OnlyEnabled::CHOICE_NO))
+ {
+ if (addonRepos.DoAddonUpdateCheck(addonToUpdate, result))
+ return true;
+ }
+
+ // get the latest version from all repos if the
+ // addon is up-to-date or not installed yet
+
+ CLog::Log(LOGDEBUG,
+ "CAddonMgr::{}: addon {} is up-to-date or not installed. falling back to get latest "
+ "version from all repos",
+ __FUNCTION__, addonId);
+
+ return addonRepos.GetLatestAddonVersionFromAllRepos(addonId, result);
+}
+
+bool CAddonMgr::GetAddonsInternal(AddonType type,
+ VECADDONS& addons,
+ OnlyEnabled onlyEnabled,
+ CheckIncompatible checkIncompatible) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& addonInfo : m_installedAddons)
+ {
+ if (type != AddonType::UNKNOWN && !addonInfo.second->HasType(type))
+ continue;
+
+ if (onlyEnabled == OnlyEnabled::CHOICE_YES &&
+ ((checkIncompatible == CheckIncompatible::CHOICE_NO &&
+ IsAddonDisabled(addonInfo.second->ID())) ||
+ (checkIncompatible == CheckIncompatible::CHOICE_YES &&
+ IsAddonDisabledExcept(addonInfo.second->ID(), AddonDisabledReason::INCOMPATIBLE))))
+ continue;
+
+ //FIXME: hack for skipping special dependency addons (xbmc.python etc.).
+ //Will break if any extension point is added to them
+ if (addonInfo.second->MainType() == AddonType::UNKNOWN)
+ continue;
+
+ AddonPtr addon = CAddonBuilder::Generate(addonInfo.second, type);
+ if (addon)
+ {
+ // if the addon has a running instance, grab that
+ AddonPtr runningAddon = addon->GetRunningInstance();
+ if (runningAddon)
+ addon = runningAddon;
+ addons.emplace_back(std::move(addon));
+ }
+ }
+ return addons.size() > 0;
+}
+
+bool CAddonMgr::GetIncompatibleEnabledAddonInfos(std::vector<AddonInfoPtr>& incompatible) const
+{
+ return GetIncompatibleAddonInfos(incompatible, false);
+}
+
+bool CAddonMgr::GetIncompatibleAddonInfos(std::vector<AddonInfoPtr>& incompatible,
+ bool includeDisabled) const
+{
+ GetAddonInfos(incompatible, true, AddonType::UNKNOWN);
+ if (includeDisabled)
+ GetDisabledAddonInfos(incompatible, AddonType::UNKNOWN, AddonDisabledReason::INCOMPATIBLE);
+ incompatible.erase(std::remove_if(incompatible.begin(), incompatible.end(),
+ [this](const AddonInfoPtr& a) { return IsCompatible(a); }),
+ incompatible.end());
+ return !incompatible.empty();
+}
+
+std::vector<AddonInfoPtr> CAddonMgr::MigrateAddons()
+{
+ // install all addon updates
+ std::lock_guard<std::mutex> lock(m_installAddonsMutex);
+ CLog::Log(LOGINFO, "ADDON: waiting for add-ons to update...");
+ VECADDONS updates;
+ GetAddonUpdateCandidates(updates);
+ InstallAddonUpdates(updates, true, AllowCheckForUpdates::CHOICE_NO);
+
+ // get addons that became incompatible and disable them
+ std::vector<AddonInfoPtr> incompatible;
+ GetIncompatibleAddonInfos(incompatible, true);
+
+ return DisableIncompatibleAddons(incompatible);
+}
+
+std::vector<AddonInfoPtr> CAddonMgr::DisableIncompatibleAddons(
+ const std::vector<AddonInfoPtr>& incompatible)
+{
+ std::vector<AddonInfoPtr> changed;
+ for (const auto& addon : incompatible)
+ {
+ CLog::Log(LOGINFO, "ADDON: {} version {} is incompatible", addon->ID(),
+ addon->Version().asString());
+
+ if (!CAddonSystemSettings::GetInstance().UnsetActive(addon))
+ {
+ CLog::Log(LOGWARNING, "ADDON: failed to unset {}", addon->ID());
+ continue;
+ }
+ if (!DisableAddon(addon->ID(), AddonDisabledReason::INCOMPATIBLE))
+ {
+ CLog::Log(LOGWARNING, "ADDON: failed to disable {}", addon->ID());
+ }
+
+ changed.emplace_back(addon);
+ }
+
+ return changed;
+}
+
+void CAddonMgr::CheckAndInstallAddonUpdates(bool wait) const
+{
+ std::lock_guard<std::mutex> lock(m_installAddonsMutex);
+ VECADDONS updates;
+ GetAddonUpdateCandidates(updates);
+ InstallAddonUpdates(updates, wait, AllowCheckForUpdates::CHOICE_YES);
+}
+
+bool CAddonMgr::GetAddonUpdateCandidates(VECADDONS& updates) const
+{
+ // Get Addons in need of an update and remove all the blacklisted ones
+ updates = GetAvailableUpdates();
+ updates.erase(
+ std::remove_if(updates.begin(), updates.end(),
+ [this](const AddonPtr& addon) { return !IsAutoUpdateable(addon->ID()); }),
+ updates.end());
+ return updates.empty();
+}
+
+void CAddonMgr::SortByDependencies(VECADDONS& updates) const
+{
+ std::vector<std::shared_ptr<ADDON::IAddon>> sorted;
+ while (!updates.empty())
+ {
+ for (auto it = updates.begin(); it != updates.end();)
+ {
+ const auto& addon = *it;
+
+ const auto& dependencies = addon->GetDependencies();
+ bool addToSortedList = true;
+ // if the addon has dependencies we need to check for each dependency if it also has
+ // an update to be installed (and in that case, if it is already in the sorted vector).
+ // if all dependency match the said conditions, the addon doesn't depend on other addons
+ // waiting to be updated. Hence, the addon being processed can be installed (i.e. added to
+ // the end of the sorted vector of addon updates)
+ for (const auto& dep : dependencies)
+ {
+ auto comparator = [&dep](const std::shared_ptr<ADDON::IAddon>& addon) {
+ return addon->ID() == dep.id;
+ };
+
+ if ((std::any_of(updates.begin(), updates.end(), comparator)) &&
+ (!std::any_of(sorted.begin(), sorted.end(), comparator)))
+ {
+ addToSortedList = false;
+ break;
+ }
+ }
+
+ // add to the end of sorted list of addons
+ if (addToSortedList)
+ {
+ sorted.emplace_back(addon);
+ it = updates.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+ updates = sorted;
+}
+
+void CAddonMgr::InstallAddonUpdates(VECADDONS& updates,
+ bool wait,
+ AllowCheckForUpdates allowCheckForUpdates) const
+{
+ // sort addons by dependencies (ensure install order) and install all
+ SortByDependencies(updates);
+ CAddonInstaller::GetInstance().InstallAddons(updates, wait, allowCheckForUpdates);
+}
+
+bool CAddonMgr::GetAddon(const std::string& str,
+ AddonPtr& addon,
+ AddonType type,
+ OnlyEnabled onlyEnabled) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ AddonInfoPtr addonInfo = GetAddonInfo(str, type);
+ if (addonInfo)
+ {
+ addon = CAddonBuilder::Generate(addonInfo, type);
+ if (addon)
+ {
+ if (onlyEnabled == OnlyEnabled::CHOICE_YES && IsAddonDisabled(addonInfo->ID()))
+ return false;
+
+ // if the addon has a running instance, grab that
+ AddonPtr runningAddon = addon->GetRunningInstance();
+ if (runningAddon)
+ addon = runningAddon;
+ }
+ return nullptr != addon.get();
+ }
+
+ return false;
+}
+
+bool CAddonMgr::GetAddon(const std::string& str, AddonPtr& addon, OnlyEnabled onlyEnabled) const
+{
+ return GetAddon(str, addon, AddonType::UNKNOWN, onlyEnabled);
+}
+
+bool CAddonMgr::HasType(const std::string& id, AddonType type)
+{
+ AddonPtr addon;
+ return GetAddon(id, addon, type, OnlyEnabled::CHOICE_NO);
+}
+
+bool CAddonMgr::FindAddon(const std::string& addonId,
+ const std::string& origin,
+ const CAddonVersion& addonVersion)
+{
+ std::map<std::string, std::shared_ptr<CAddonInfo>> installedAddons;
+
+ FindAddons(installedAddons, "special://xbmcbin/addons");
+ // Confirm special://xbmcbin/addons and special://xbmc/addons are not the same
+ if (!CSpecialProtocol::ComparePath("special://xbmcbin/addons", "special://xbmc/addons"))
+ FindAddons(installedAddons, "special://xbmc/addons");
+ FindAddons(installedAddons, "special://home/addons");
+
+ const auto it = installedAddons.find(addonId);
+ if (it == installedAddons.cend() || it->second->Version() != addonVersion)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_database->GetInstallData(it->second);
+ CLog::Log(LOGINFO, "CAddonMgr::{}: {} v{} installed", __FUNCTION__, addonId,
+ addonVersion.asString());
+
+ m_installedAddons[addonId] = it->second; // insert/replace entry
+ m_database->AddInstalledAddon(it->second, origin);
+
+ // Reload caches
+ std::map<std::string, AddonDisabledReason> tmpDisabled;
+ m_database->GetDisabled(tmpDisabled);
+ m_disabled = std::move(tmpDisabled);
+
+ m_updateRules->RefreshRulesMap(*m_database);
+ return true;
+}
+
+bool CAddonMgr::FindAddons()
+{
+ ADDON_INFO_LIST installedAddons;
+
+ FindAddons(installedAddons, "special://xbmcbin/addons");
+ // Confirm special://xbmcbin/addons and special://xbmc/addons are not the same
+ if (!CSpecialProtocol::ComparePath("special://xbmcbin/addons", "special://xbmc/addons"))
+ FindAddons(installedAddons, "special://xbmc/addons");
+ FindAddons(installedAddons, "special://home/addons");
+
+ std::set<std::string> installed;
+ for (const auto& addon : installedAddons)
+ installed.insert(addon.second->ID());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // Sync with db
+ m_database->SyncInstalled(installed, m_systemAddons, m_optionalSystemAddons);
+ for (const auto& addon : installedAddons)
+ {
+ m_database->GetInstallData(addon.second);
+ CLog::Log(LOGINFO, "CAddonMgr::{}: {} v{} installed", __FUNCTION__, addon.second->ID(),
+ addon.second->Version().asString());
+ }
+
+ m_installedAddons = std::move(installedAddons);
+
+ // Reload caches
+ std::map<std::string, AddonDisabledReason> tmpDisabled;
+ m_database->GetDisabled(tmpDisabled);
+ m_disabled = std::move(tmpDisabled);
+
+ m_updateRules->RefreshRulesMap(*m_database);
+
+ return true;
+}
+
+bool CAddonMgr::UnloadAddon(const std::string& addonId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!IsAddonInstalled(addonId))
+ return true;
+
+ AddonPtr localAddon;
+ // can't unload an binary addon that is in use
+ if (GetAddon(addonId, localAddon, AddonType::UNKNOWN, OnlyEnabled::CHOICE_NO) &&
+ localAddon->IsBinary() && localAddon->IsInUse())
+ {
+ CLog::Log(LOGERROR, "CAddonMgr::{}: could not unload binary add-on {}, as is in use", __func__,
+ addonId);
+ return false;
+ }
+
+ m_installedAddons.erase(addonId);
+ CLog::Log(LOGDEBUG, "CAddonMgr::{}: {} unloaded", __func__, addonId);
+
+ lock.unlock();
+ AddonEvents::Unload event(addonId);
+ m_unloadEvents.HandleEvent(event);
+
+ return true;
+}
+
+bool CAddonMgr::LoadAddon(const std::string& addonId,
+ const std::string& origin,
+ const CAddonVersion& addonVersion)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ AddonPtr addon;
+ if (GetAddon(addonId, addon, AddonType::UNKNOWN, OnlyEnabled::CHOICE_NO))
+ {
+ return true;
+ }
+
+ if (!FindAddon(addonId, origin, addonVersion))
+ {
+ CLog::Log(LOGERROR, "CAddonMgr: could not reload add-on {}. FindAddon failed.", addonId);
+ return false;
+ }
+
+ if (!GetAddon(addonId, addon, AddonType::UNKNOWN, OnlyEnabled::CHOICE_NO))
+ {
+ CLog::Log(LOGERROR, "CAddonMgr: could not load add-on {}. No add-on with that ID is installed.",
+ addonId);
+ return false;
+ }
+
+ lock.unlock();
+
+ AddonEvents::Load event(addon->ID());
+ m_unloadEvents.HandleEvent(event);
+
+ if (IsAddonDisabled(addon->ID()))
+ {
+ EnableAddon(addon->ID());
+ return true;
+ }
+
+ m_events.Publish(AddonEvents::ReInstalled(addon->ID()));
+ CLog::Log(LOGDEBUG, "CAddonMgr: {} successfully loaded", addon->ID());
+ return true;
+}
+
+void CAddonMgr::OnPostUnInstall(const std::string& id)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_disabled.erase(id);
+ RemoveAllUpdateRulesFromList(id);
+ m_events.Publish(AddonEvents::UnInstalled(id));
+}
+
+void CAddonMgr::UpdateLastUsed(const std::string& id)
+{
+ auto time = CDateTime::GetCurrentDateTime();
+ CServiceBroker::GetJobManager()->Submit([this, id, time]() {
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_database->SetLastUsed(id, time);
+ auto addonInfo = GetAddonInfo(id, AddonType::UNKNOWN);
+ if (addonInfo)
+ addonInfo->SetLastUsed(time);
+ }
+ m_events.Publish(AddonEvents::MetadataChanged(id));
+ });
+}
+
+static void ResolveDependencies(const std::string& addonId, std::vector<std::string>& needed, std::vector<std::string>& missing)
+{
+ if (std::find(needed.begin(), needed.end(), addonId) != needed.end())
+ return;
+
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(addonId, addon, AddonType::UNKNOWN,
+ OnlyEnabled::CHOICE_NO))
+ missing.push_back(addonId);
+ else
+ {
+ needed.push_back(addonId);
+ for (const auto& dep : addon->GetDependencies())
+ if (!dep.optional)
+ ResolveDependencies(dep.id, needed, missing);
+ }
+}
+
+bool CAddonMgr::DisableAddon(const std::string& id, AddonDisabledReason disabledReason)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!CanAddonBeDisabled(id))
+ return false;
+ if (m_disabled.find(id) != m_disabled.end())
+ return true; //already disabled
+ if (!m_database->DisableAddon(id, disabledReason))
+ return false;
+ if (!m_disabled.emplace(id, disabledReason).second)
+ return false;
+
+ //success
+ CLog::Log(LOGDEBUG, "CAddonMgr: {} disabled", id);
+ AddonPtr addon;
+ if (GetAddon(id, addon, AddonType::UNKNOWN, OnlyEnabled::CHOICE_NO) && addon != nullptr)
+ {
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(new CAddonManagementEvent(addon, 24141)));
+ }
+
+ m_events.Publish(AddonEvents::Disabled(id));
+ return true;
+}
+
+bool CAddonMgr::UpdateDisabledReason(const std::string& id, AddonDisabledReason newDisabledReason)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!IsAddonDisabled(id))
+ return false;
+ if (!m_database->DisableAddon(id, newDisabledReason))
+ return false;
+
+ m_disabled[id] = newDisabledReason;
+
+ // success
+ CLog::Log(LOGDEBUG, "CAddonMgr: DisabledReason for {} updated to {}", id,
+ static_cast<int>(newDisabledReason));
+ return true;
+}
+
+bool CAddonMgr::EnableSingle(const std::string& id)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_disabled.find(id) == m_disabled.end())
+ return true; //already enabled
+
+ AddonPtr addon;
+ if (!GetAddon(id, addon, AddonType::UNKNOWN, OnlyEnabled::CHOICE_NO) || addon == nullptr)
+ return false;
+
+ auto eventLog = CServiceBroker::GetEventLog();
+
+ if (!IsCompatible(*addon))
+ {
+ CLog::Log(LOGERROR, "Add-on '{}' is not compatible with Kodi", addon->ID());
+ if (eventLog)
+ eventLog->AddWithNotification(
+ EventPtr(new CNotificationEvent(addon->Name(), 24152, EventLevel::Error)));
+ UpdateDisabledReason(addon->ID(), AddonDisabledReason::INCOMPATIBLE);
+ return false;
+ }
+
+ if (!m_database->EnableAddon(id))
+ return false;
+ m_disabled.erase(id);
+
+ // If enabling a repo add-on without an origin, set its origin to its own id
+ if (addon->HasType(AddonType::REPOSITORY) && addon->Origin().empty())
+ SetAddonOrigin(id, id, false);
+
+ if (eventLog)
+ eventLog->Add(EventPtr(new CAddonManagementEvent(addon, 24064)));
+
+ CLog::Log(LOGDEBUG, "CAddonMgr: enabled {}", addon->ID());
+ m_events.Publish(AddonEvents::Enabled(id));
+ return true;
+}
+
+bool CAddonMgr::EnableAddon(const std::string& id)
+{
+ if (id.empty() || !IsAddonInstalled(id))
+ return false;
+ std::vector<std::string> needed;
+ std::vector<std::string> missing;
+ ResolveDependencies(id, needed, missing);
+ for (const auto& dep : missing)
+ CLog::Log(LOGWARNING,
+ "CAddonMgr: '{}' required by '{}' is missing. Add-on may not function "
+ "correctly",
+ dep, id);
+ for (auto it = needed.rbegin(); it != needed.rend(); ++it)
+ EnableSingle(*it);
+
+ return true;
+}
+
+bool CAddonMgr::IsAddonDisabled(const std::string& ID) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_disabled.find(ID) != m_disabled.end();
+}
+
+bool CAddonMgr::IsAddonDisabledExcept(const std::string& ID,
+ AddonDisabledReason disabledReason) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto disabledAddon = m_disabled.find(ID);
+ return disabledAddon != m_disabled.end() && disabledAddon->second != disabledReason;
+}
+
+bool CAddonMgr::CanAddonBeDisabled(const std::string& ID)
+{
+ if (ID.empty())
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ // Required system add-ons can not be disabled
+ if (IsRequiredSystemAddon(ID))
+ return false;
+
+ AddonPtr localAddon;
+ // can't disable an addon that isn't installed
+ if (!GetAddon(ID, localAddon, AddonType::UNKNOWN, OnlyEnabled::CHOICE_NO))
+ return false;
+
+ // can't disable an addon that is in use
+ if (localAddon->IsInUse())
+ return false;
+
+ return true;
+}
+
+bool CAddonMgr::CanAddonBeEnabled(const std::string& id)
+{
+ return !id.empty() && IsAddonInstalled(id);
+}
+
+bool CAddonMgr::IsAddonInstalled(const std::string& ID)
+{
+ AddonPtr tmp;
+ return GetAddon(ID, tmp, AddonType::UNKNOWN, OnlyEnabled::CHOICE_NO);
+}
+
+bool CAddonMgr::IsAddonInstalled(const std::string& ID, const std::string& origin) const
+{
+ AddonPtr tmp;
+
+ if (GetAddon(ID, tmp, AddonType::UNKNOWN, OnlyEnabled::CHOICE_NO) && tmp)
+ {
+ if (tmp->Origin() == ORIGIN_SYSTEM)
+ {
+ return CAddonRepos::IsOfficialRepo(origin);
+ }
+ else
+ {
+ return tmp->Origin() == origin;
+ }
+ }
+ return false;
+}
+
+bool CAddonMgr::IsAddonInstalled(const std::string& ID,
+ const std::string& origin,
+ const CAddonVersion& version)
+{
+ AddonPtr tmp;
+
+ if (GetAddon(ID, tmp, AddonType::UNKNOWN, OnlyEnabled::CHOICE_NO) && tmp)
+ {
+ if (tmp->Origin() == ORIGIN_SYSTEM)
+ {
+ return CAddonRepos::IsOfficialRepo(origin) && tmp->Version() == version;
+ }
+ else
+ {
+ return tmp->Origin() == origin && tmp->Version() == version;
+ }
+ }
+ return false;
+}
+
+bool CAddonMgr::CanAddonBeInstalled(const AddonPtr& addon)
+{
+ return addon != nullptr && addon->LifecycleState() != AddonLifecycleState::BROKEN &&
+ !IsAddonInstalled(addon->ID());
+}
+
+bool CAddonMgr::CanUninstall(const AddonPtr& addon)
+{
+ return addon && CanAddonBeDisabled(addon->ID()) && !IsBundledAddon(addon->ID());
+}
+
+bool CAddonMgr::IsBundledAddon(const std::string& id)
+{
+ return XFILE::CDirectory::Exists(
+ CSpecialProtocol::TranslatePath("special://xbmc/addons/" + id + "/")) ||
+ XFILE::CDirectory::Exists(
+ CSpecialProtocol::TranslatePath("special://xbmcbin/addons/" + id + "/"));
+}
+
+bool CAddonMgr::IsSystemAddon(const std::string& id)
+{
+ return IsOptionalSystemAddon(id) || IsRequiredSystemAddon(id);
+}
+
+bool CAddonMgr::IsRequiredSystemAddon(const std::string& id)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::find(m_systemAddons.begin(), m_systemAddons.end(), id) != m_systemAddons.end();
+}
+
+bool CAddonMgr::IsOptionalSystemAddon(const std::string& id)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::find(m_optionalSystemAddons.begin(), m_optionalSystemAddons.end(), id) !=
+ m_optionalSystemAddons.end();
+}
+
+bool CAddonMgr::LoadAddonDescription(const std::string &directory, AddonPtr &addon)
+{
+ auto addonInfo = CAddonInfoBuilder::Generate(directory);
+ if (addonInfo)
+ addon = CAddonBuilder::Generate(addonInfo, AddonType::UNKNOWN);
+
+ return addon != nullptr;
+}
+
+bool CAddonMgr::AddUpdateRuleToList(const std::string& id, AddonUpdateRule updateRule)
+{
+ return m_updateRules->AddUpdateRuleToList(*m_database, id, updateRule);
+}
+
+bool CAddonMgr::RemoveAllUpdateRulesFromList(const std::string& id)
+{
+ return m_updateRules->RemoveAllUpdateRulesFromList(*m_database, id);
+}
+
+bool CAddonMgr::RemoveUpdateRuleFromList(const std::string& id, AddonUpdateRule updateRule)
+{
+ return m_updateRules->RemoveUpdateRuleFromList(*m_database, id, updateRule);
+}
+
+bool CAddonMgr::IsAutoUpdateable(const std::string& id) const
+{
+ return m_updateRules->IsAutoUpdateable(id);
+}
+
+void CAddonMgr::PublishEventAutoUpdateStateChanged(const std::string& id)
+{
+ m_events.Publish(AddonEvents::AutoUpdateStateChanged(id));
+}
+
+void CAddonMgr::PublishInstanceAdded(const std::string& addonId, AddonInstanceId instanceId)
+{
+ m_events.Publish(AddonEvents::InstanceAdded(addonId, instanceId));
+}
+
+void CAddonMgr::PublishInstanceRemoved(const std::string& addonId, AddonInstanceId instanceId)
+{
+ m_events.Publish(AddonEvents::InstanceRemoved(addonId, instanceId));
+}
+
+bool CAddonMgr::IsCompatible(const IAddon& addon) const
+{
+ for (const auto& dependency : addon.GetDependencies())
+ {
+ if (!dependency.optional)
+ {
+ // Intentionally only check the xbmc.* and kodi.* magic dependencies. Everything else will
+ // not be missing anyway, unless addon was installed in an unsupported way.
+ if (StringUtils::StartsWith(dependency.id, "xbmc.") ||
+ StringUtils::StartsWith(dependency.id, "kodi."))
+ {
+ AddonPtr addon;
+ bool haveAddon =
+ GetAddon(dependency.id, addon, AddonType::UNKNOWN, OnlyEnabled::CHOICE_YES);
+ if (!haveAddon || !addon->MeetsVersion(dependency.versionMin, dependency.version))
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool CAddonMgr::IsCompatible(const AddonInfoPtr& addonInfo) const
+{
+ for (const auto& dependency : addonInfo->GetDependencies())
+ {
+ if (!dependency.optional)
+ {
+ // Intentionally only check the xbmc.* and kodi.* magic dependencies. Everything else will
+ // not be missing anyway, unless addon was installed in an unsupported way.
+ if (StringUtils::StartsWith(dependency.id, "xbmc.") ||
+ StringUtils::StartsWith(dependency.id, "kodi."))
+ {
+ AddonInfoPtr addonInfo = GetAddonInfo(dependency.id, AddonType::UNKNOWN);
+ if (!addonInfo || !addonInfo->MeetsVersion(dependency.versionMin, dependency.version))
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+std::vector<DependencyInfo> CAddonMgr::GetDepsRecursive(const std::string& id,
+ OnlyEnabledRootAddon onlyEnabledRootAddon)
+{
+ std::vector<DependencyInfo> added;
+ AddonPtr root_addon;
+ if (!FindInstallableById(id, root_addon) &&
+ !GetAddon(id, root_addon, AddonType::UNKNOWN, static_cast<OnlyEnabled>(onlyEnabledRootAddon)))
+ {
+ return added;
+ }
+
+ std::vector<DependencyInfo> toProcess;
+ for (const auto& dep : root_addon->GetDependencies())
+ toProcess.push_back(dep);
+
+ while (!toProcess.empty())
+ {
+ auto current_dep = *toProcess.begin();
+ toProcess.erase(toProcess.begin());
+ if (StringUtils::StartsWith(current_dep.id, "xbmc.") ||
+ StringUtils::StartsWith(current_dep.id, "kodi."))
+ continue;
+
+ auto added_it = std::find_if(added.begin(), added.end(), [&](const DependencyInfo& d){ return d.id == current_dep.id;});
+ if (added_it != added.end())
+ {
+ if (current_dep.version < added_it->version)
+ continue;
+
+ bool aopt = added_it->optional;
+ added.erase(added_it);
+ added.push_back(current_dep);
+ if (!current_dep.optional && aopt)
+ continue;
+ }
+ else
+ added.push_back(current_dep);
+
+ AddonPtr current_addon;
+ if (FindInstallableById(current_dep.id, current_addon))
+ {
+ for (const auto& item : current_addon->GetDependencies())
+ toProcess.push_back(item);
+ }
+ }
+
+ return added;
+}
+
+bool CAddonMgr::GetAddonInfos(AddonInfos& addonInfos, bool onlyEnabled, AddonType type) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bool forUnknown = type == AddonType::UNKNOWN;
+ for (auto& info : m_installedAddons)
+ {
+ if (onlyEnabled && m_disabled.find(info.first) != m_disabled.end())
+ continue;
+
+ if (info.second->MainType() != AddonType::UNKNOWN && (forUnknown || info.second->HasType(type)))
+ addonInfos.push_back(info.second);
+ }
+
+ return !addonInfos.empty();
+}
+
+std::vector<AddonInfoPtr> CAddonMgr::GetAddonInfos(bool onlyEnabled,
+ const std::vector<AddonType>& types) const
+{
+ std::vector<AddonInfoPtr> infos;
+ if (types.empty())
+ return infos;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (auto& info : m_installedAddons)
+ {
+ if (onlyEnabled && m_disabled.find(info.first) != m_disabled.end())
+ continue;
+
+ if (info.second->MainType() == AddonType::UNKNOWN)
+ continue;
+
+ const auto it = std::find_if(types.begin(), types.end(),
+ [info](AddonType t) { return info.second->HasType(t); });
+ if (it != types.end())
+ infos.emplace_back(info.second);
+ }
+
+ return infos;
+}
+
+bool CAddonMgr::GetDisabledAddonInfos(std::vector<AddonInfoPtr>& addonInfos, AddonType type) const
+{
+ return GetDisabledAddonInfos(addonInfos, type, AddonDisabledReason::NONE);
+}
+
+bool CAddonMgr::GetDisabledAddonInfos(std::vector<AddonInfoPtr>& addonInfos,
+ AddonType type,
+ AddonDisabledReason disabledReason) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bool forUnknown = type == AddonType::UNKNOWN;
+ for (const auto& info : m_installedAddons)
+ {
+ const auto disabledAddon = m_disabled.find(info.first);
+ if (disabledAddon == m_disabled.end())
+ continue;
+
+ if (info.second->MainType() != AddonType::UNKNOWN &&
+ (forUnknown || info.second->HasType(type)) &&
+ (disabledReason == AddonDisabledReason::NONE || disabledReason == disabledAddon->second))
+ addonInfos.emplace_back(info.second);
+ }
+
+ return !addonInfos.empty();
+}
+
+const AddonInfoPtr CAddonMgr::GetAddonInfo(const std::string& id, AddonType type) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ auto addon = m_installedAddons.find(id);
+ if (addon != m_installedAddons.end())
+ if ((type == AddonType::UNKNOWN || addon->second->HasType(type)))
+ return addon->second;
+
+ return nullptr;
+}
+
+void CAddonMgr::FindAddons(ADDON_INFO_LIST& addonmap, const std::string& path)
+{
+ CFileItemList items;
+ if (XFILE::CDirectory::GetDirectory(path, items, "", XFILE::DIR_FLAG_NO_FILE_DIRS))
+ {
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ std::string path = items[i]->GetPath();
+ if (CFileUtils::Exists(path + "addon.xml"))
+ {
+ AddonInfoPtr addonInfo = CAddonInfoBuilder::Generate(path);
+ if (addonInfo)
+ {
+ const auto& it = addonmap.find(addonInfo->ID());
+ if (it != addonmap.end())
+ {
+ if (it->second->Version() > addonInfo->Version())
+ {
+ CLog::Log(LOGWARNING, "CAddonMgr::{}: Addon '{}' already present with higher version {} at '{}' - other version {} at '{}' will be ignored",
+ __FUNCTION__, addonInfo->ID(), it->second->Version().asString(), it->second->Path(), addonInfo->Version().asString(), addonInfo->Path());
+ continue;
+ }
+ CLog::Log(LOGDEBUG, "CAddonMgr::{}: Addon '{}' already present with version {} at '{}' replaced with version {} at '{}'",
+ __FUNCTION__, addonInfo->ID(), it->second->Version().asString(), it->second->Path(), addonInfo->Version().asString(), addonInfo->Path());
+ }
+
+ addonmap[addonInfo->ID()] = addonInfo;
+ }
+ }
+ }
+ }
+}
+
+AddonOriginType CAddonMgr::GetAddonOriginType(const AddonPtr& addon) const
+{
+ if (addon->Origin() == ORIGIN_SYSTEM)
+ return AddonOriginType::SYSTEM;
+ else if (!addon->Origin().empty())
+ return AddonOriginType::REPOSITORY;
+ else
+ return AddonOriginType::MANUAL;
+}
+
+bool CAddonMgr::IsAddonDisabledWithReason(const std::string& ID,
+ AddonDisabledReason disabledReason) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto& disabledAddon = m_disabled.find(ID);
+ return disabledAddon != m_disabled.end() && disabledAddon->second == disabledReason;
+}
+
+/*!
+ * @brief Addon update and install management.
+ */
+/*@{{{*/
+
+bool CAddonMgr::SetAddonOrigin(const std::string& addonId, const std::string& repoAddonId, bool isUpdate)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_database->SetOrigin(addonId, repoAddonId);
+ if (isUpdate)
+ m_database->SetLastUpdated(addonId, CDateTime::GetCurrentDateTime());
+
+ // If available in manager update
+ const AddonInfoPtr info = GetAddonInfo(addonId, AddonType::UNKNOWN);
+ if (info)
+ m_database->GetInstallData(info);
+ return true;
+}
+
+bool CAddonMgr::AddonsFromRepoXML(const RepositoryDirInfo& repo,
+ const std::string& xml,
+ std::vector<AddonInfoPtr>& addons)
+{
+ CXBMCTinyXML doc;
+ if (!doc.Parse(xml))
+ {
+ CLog::Log(LOGERROR, "CAddonMgr::{}: Failed to parse addons.xml", __func__);
+ return false;
+ }
+
+ if (doc.RootElement() == nullptr || doc.RootElement()->ValueStr() != "addons")
+ {
+ CLog::Log(LOGERROR, "CAddonMgr::{}: Failed to parse addons.xml. Malformed", __func__);
+ return false;
+ }
+
+ // each addon XML should have a UTF-8 declaration
+ auto element = doc.RootElement()->FirstChildElement("addon");
+ while (element)
+ {
+ auto addonInfo = CAddonInfoBuilder::Generate(element, repo);
+ if (addonInfo)
+ addons.emplace_back(addonInfo);
+
+ element = element->NextSiblingElement("addon");
+ }
+
+ return true;
+}
+
+/*@}}}*/
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/AddonManager.h b/xbmc/addons/AddonManager.h
new file mode 100644
index 0000000..cdce156
--- /dev/null
+++ b/xbmc/addons/AddonManager.h
@@ -0,0 +1,696 @@
+/*
+ * 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 "threads/CriticalSection.h"
+#include "utils/EventStream.h"
+
+#include <map>
+#include <memory>
+#include <mutex>
+#include <set>
+#include <string>
+#include <vector>
+
+namespace ADDON
+{
+enum class AddonDisabledReason;
+enum class AddonOriginType;
+enum class AddonType;
+enum class AddonUpdateRule;
+enum class AllowCheckForUpdates : bool;
+
+class CAddonDatabase;
+class CAddonUpdateRules;
+class CAddonVersion;
+class IAddonMgrCallback;
+
+class CAddonInfo;
+using AddonInfoPtr = std::shared_ptr<CAddonInfo>;
+using ADDON_INFO_LIST = std::map<std::string, AddonInfoPtr>;
+
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+using VECADDONS = std::vector<AddonPtr>;
+
+struct AddonEvent;
+struct AddonWithUpdate;
+struct DependencyInfo;
+struct RepositoryDirInfo;
+
+using AddonInstanceId = uint32_t;
+
+enum class AddonCheckType : bool
+{
+ OUTDATED_ADDONS,
+ AVAILABLE_UPDATES,
+};
+
+enum class OnlyEnabled : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+enum class OnlyEnabledRootAddon : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+enum class CheckIncompatible : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+/**
+ * Class - CAddonMgr
+ * Holds references to all addons, enabled or
+ * otherwise. Services the generic callbacks available
+ * to all addon variants.
+ */
+class CAddonMgr
+{
+public:
+ bool ReInit()
+ {
+ DeInit();
+ return Init();
+ }
+ bool Init();
+ void DeInit();
+
+ CAddonMgr();
+ CAddonMgr(const CAddonMgr&) = delete;
+ virtual ~CAddonMgr();
+
+ CEventStream<AddonEvent>& Events() { return m_events; }
+ CEventStream<AddonEvent>& UnloadEvents() { return m_unloadEvents; }
+
+ IAddonMgrCallback* GetCallbackForType(AddonType type);
+ bool RegisterAddonMgrCallback(AddonType type, IAddonMgrCallback* cb);
+ void UnregisterAddonMgrCallback(AddonType type);
+
+ /*! \brief Retrieve a specific addon (of a specific type)
+ \param id the id of the addon to retrieve.
+ \param addon[out] the retrieved addon pointer - only use if the function returns true.
+ \param type type of addon to retrieve - defaults to any type.
+ \param onlyEnabled whether we only want enabled addons - set to false to allow both enabled and disabled addons - defaults to true.
+ \return true if an addon matching the id of the given type is available and is enabled (if onlyEnabled is true).
+ */
+ bool GetAddon(const std::string& id,
+ AddonPtr& addon,
+ AddonType type,
+ OnlyEnabled onlyEnabled) const;
+
+ /*! \brief Retrieve a specific addon (of no specific type)
+ \param id the id of the addon to retrieve.
+ \param addon[out] the retrieved addon pointer - only use if the function returns true.
+ \param onlyEnabled whether we only want enabled addons - set to false to allow both enabled and disabled addons - defaults to true.
+ \return true if an addon matching the id of any type is available and is enabled (if onlyEnabled is true).
+ */
+ bool GetAddon(const std::string& id, AddonPtr& addon, OnlyEnabled onlyEnabled) const;
+
+ bool HasType(const std::string& id, AddonType type);
+
+ bool HasAddons(AddonType type);
+
+ bool HasInstalledAddons(AddonType type);
+
+ /*! Returns all installed, enabled and incompatible (and disabled) add-ons. */
+ bool GetAddonsForUpdate(VECADDONS& addons) const;
+
+ /*! Returns all installed, enabled add-ons. */
+ bool GetAddons(VECADDONS& addons) const;
+
+ /*! Returns enabled add-ons with given type. */
+ bool GetAddons(VECADDONS& addons, AddonType type);
+
+ /*! Returns all installed, including disabled. */
+ bool GetInstalledAddons(VECADDONS& addons);
+
+ /*! Returns installed add-ons, including disabled, with given type. */
+ bool GetInstalledAddons(VECADDONS& addons, AddonType type);
+
+ bool GetDisabledAddons(VECADDONS& addons);
+
+ bool GetDisabledAddons(VECADDONS& addons, AddonType type);
+
+ /*! Get all installable addons */
+ bool GetInstallableAddons(VECADDONS& addons);
+
+ bool GetInstallableAddons(VECADDONS& addons, AddonType type);
+
+ /*! \brief Get the installable addon depending on install rules
+ * or fall back to highest version.
+ * \note This function gets called in different contexts. If it's
+ * called for checking possible updates for already installed addons
+ * our update restriction rules apply.
+ * If it's called to (for example) populate an addon-select-dialog
+ * the addon is not installed yet, and we have to fall back to the
+ * highest version.
+ * \param addonId addon to check for update or installation
+ * \param addon[out] the retrieved addon pointer - only use if the function returns true.
+ * \return true if an addon matching the id is available.
+ */
+ bool FindInstallableById(const std::string& addonId, AddonPtr& addon);
+
+ void AddToUpdateableAddons(AddonPtr& pAddon);
+ void RemoveFromUpdateableAddons(AddonPtr& pAddon);
+ bool ReloadSettings(const std::string& addonId, AddonInstanceId instanceId);
+
+ /*! Get addons with available updates */
+ std::vector<std::shared_ptr<IAddon>> GetAvailableUpdates() const;
+
+ /*! Get addons that are outdated */
+ std::vector<std::shared_ptr<IAddon>> GetOutdatedAddons() const;
+
+ /*! Returns true if there is any addon with available updates, otherwise false */
+ bool HasAvailableUpdates();
+
+ /*!
+ * \brief Checks if the passed in addon is an orphaned dependency
+ * \param addon the add-on/dependency to check
+ * \param allAddons vector of all installed add-ons
+ * \return true or false
+ */
+ bool IsOrphaned(const std::shared_ptr<IAddon>& addon,
+ const std::vector<std::shared_ptr<IAddon>>& allAddons) const;
+
+ /*! \brief Checks for new / updated add-ons
+ \return True if everything went ok, false otherwise
+ */
+ bool FindAddons();
+
+ /*! \brief Checks whether given addon with given origin/version is installed
+ * \param addonId addon to check
+ * \param origin origin to check
+ * \param addonVersion version to check
+ * \return True if installed, false otherwise
+ */
+ bool FindAddon(const std::string& addonId,
+ const std::string& origin,
+ const CAddonVersion& addonVersion);
+
+ /*!
+ * @brief Fills the the provided vector with the list of incompatible
+ * enabled addons and returns if there's any.
+ *
+ * @param[out] incompatible List of incompatible addons
+ * @return true if there are incompatible addons
+ */
+ bool GetIncompatibleEnabledAddonInfos(std::vector<AddonInfoPtr>& incompatible) const;
+
+ /*!
+ * Migrate all the addons (updates all addons that have an update pending and disables those
+ * that got incompatible)
+ *
+ * @return list of all addons (infos) that were modified.
+ */
+ std::vector<AddonInfoPtr> MigrateAddons();
+
+ /*!
+ * @brief Try to disable addons in the given list.
+ *
+ * @param[in] incompatible List of incompatible addon infos
+ * @return list of all addon Infos that were disabled
+ */
+ std::vector<AddonInfoPtr> DisableIncompatibleAddons(
+ const std::vector<AddonInfoPtr>& incompatible);
+
+ /*!
+ * Install available addon updates, if any.
+ * @param wait If kodi should wait for all updates to download and install before returning
+ */
+ void CheckAndInstallAddonUpdates(bool wait) const;
+
+ /*!
+ * @note: should only be called by AddonInstaller
+ *
+ * Unload addon from the system. Returns true if it was unloaded, otherwise false.
+ */
+ bool UnloadAddon(const std::string& addonId);
+
+ /*!
+ * @note: should only be called by AddonInstaller
+ *
+ * Returns true if the addon was successfully loaded and enabled; otherwise false.
+ */
+ bool LoadAddon(const std::string& addonId,
+ const std::string& origin,
+ const CAddonVersion& addonVersion);
+
+ /*! @note: should only be called by AddonInstaller
+ *
+ * Hook for clearing internal state after uninstall.
+ */
+ void OnPostUnInstall(const std::string& id);
+
+ /*! \brief Disable an addon. Returns true on success, false on failure. */
+ bool DisableAddon(const std::string& ID, AddonDisabledReason disabledReason);
+
+ /*! \brief Updates reason for a disabled addon. Returns true on success, false on failure. */
+ bool UpdateDisabledReason(const std::string& id, AddonDisabledReason newDisabledReason);
+
+ /*! \brief Enable an addon. Returns true on success, false on failure. */
+ bool EnableAddon(const std::string& ID);
+
+ /* \brief Check whether an addon has been disabled via DisableAddon.
+ In case the disabled cache does not know about the current state the database routine will be used.
+ \param ID id of the addon
+ \sa DisableAddon
+ */
+ bool IsAddonDisabled(const std::string& ID) const;
+
+ /*!
+ * @brief Check whether an addon has been disabled via DisableAddon except for a particular
+ * reason In case the disabled cache does not know about the current state the database routine
+ * will be used.
+ * @param[in] ID id of the addon
+ * @param[in] disabledReason the reason that will be an exception to being disabled
+ * @return true if the addon was disabled except for the specified reason
+ * @sa DisableAddon
+ */
+ bool IsAddonDisabledExcept(const std::string& ID, AddonDisabledReason disabledReason) const;
+
+ /* \brief Checks whether an addon can be disabled via DisableAddon.
+ \param ID id of the addon
+ \sa DisableAddon
+ */
+ bool CanAddonBeDisabled(const std::string& ID);
+
+ bool CanAddonBeEnabled(const std::string& id);
+
+ /* \brief Checks whether an addon is installed.
+ \param ID id of the addon
+ */
+ bool IsAddonInstalled(const std::string& ID);
+
+ /* \brief Checks whether an addon is installed from a
+ * particular origin repo
+ * \note if checked for an origin defined as official (i.e. repository.xbmc.org)
+ * this function will return true even if the addon is a shipped system add-on
+ * \param ID id of the addon
+ * \param origin origin repository id
+ */
+ bool IsAddonInstalled(const std::string& ID, const std::string& origin) const;
+
+ /* \brief Checks whether an addon is installed from a
+ * particular origin repo and version
+ * \note if checked for an origin defined as official (i.e. repository.xbmc.org)
+ * this function will return true even if the addon is a shipped system add-on
+ * \param ID id of the addon
+ * \param origin origin repository id
+ * \param version the version of the addon
+ */
+ bool IsAddonInstalled(const std::string& ID,
+ const std::string& origin,
+ const CAddonVersion& version);
+
+ /* \brief Checks whether an addon can be installed. Broken addons can't be installed.
+ \param addon addon to be checked
+ */
+ bool CanAddonBeInstalled(const AddonPtr& addon);
+
+ bool CanUninstall(const AddonPtr& addon);
+
+ /*!
+ * @brief Checks whether an addon is a bundled addon
+ *
+ * @param[in] id id of the addon
+ * @return true if addon is bundled addon, false otherwise.
+ */
+ bool IsBundledAddon(const std::string& id);
+
+ /*!
+ * @brief Checks whether an addon is a system addon
+ *
+ * @param[in] id id of the addon
+ * @return true if addon is system addon, false otherwise.
+ */
+ bool IsSystemAddon(const std::string& id);
+
+ /*!
+ * @brief Checks whether an addon is a required system addon
+ *
+ * @param[in] id id of the addon
+ * @return true if addon is a required system addon, false otherwise.
+ */
+ bool IsRequiredSystemAddon(const std::string& id);
+
+ /*!
+ * @brief Checks whether an addon is an optional system addon
+ *
+ * @param[in] id id of the addon
+ * @return true if addon is an optional system addon, false otherwise.
+ */
+ bool IsOptionalSystemAddon(const std::string& id);
+
+ /*!
+ * @brief Addon update rules.
+ *
+ * member functions for handling and querying add-on update rules
+ *
+ * @warning This should be never used from other places outside of addon
+ * system directory.
+ *
+ */
+ /*@{{{*/
+
+ /* \brief Add a single update rule to the list for an addon
+ * \sa CAddonUpdateRules::AddUpdateRuleToList()
+ */
+ bool AddUpdateRuleToList(const std::string& id, AddonUpdateRule updateRule);
+
+ /* \brief Remove all rules from update rules list for an addon
+ * \sa CAddonUpdateRules::RemoveAllUpdateRulesFromList()
+ */
+ bool RemoveAllUpdateRulesFromList(const std::string& id);
+
+ /* \brief Remove a specific rule from update rules list for an addon
+ * \sa CAddonUpdateRules::RemoveUpdateRuleFromList()
+ */
+ bool RemoveUpdateRuleFromList(const std::string& id, AddonUpdateRule updateRule);
+
+ /* \brief Check if an addon version is auto-updateable
+ * \param id addon id to be checked
+ * \return true is addon is auto-updateable, false otherwise
+ * \sa CAddonUpdateRules::IsAutoUpdateable()
+ */
+ bool IsAutoUpdateable(const std::string& id) const;
+
+ /*@}}}*/
+
+ /* \brief Launches event AddonEvent::AutoUpdateStateChanged
+ * \param id addon id to pass through
+ * \sa CGUIDialogAddonInfo::OnToggleAutoUpdates()
+ */
+ void PublishEventAutoUpdateStateChanged(const std::string& id);
+ void UpdateLastUsed(const std::string& id);
+
+ /*!
+ * \brief Launches event @ref AddonEvent::InstanceAdded
+ *
+ * This is called when a new instance is added in add-on settings.
+ *
+ * \param[in] addonId Add-on id to pass through
+ * \param[in] instanceId Identifier of the add-on instance
+ */
+ void PublishInstanceAdded(const std::string& addonId, AddonInstanceId instanceId);
+
+ /*!
+ * \brief Launches event @ref AddonEvent::InstanceRemoved
+ *
+ * This is called when an instance is removed in add-on settings.
+ *
+ * \param[in] addonId Add-on id to pass through
+ * \param[in] instanceId Identifier of the add-on instance
+ */
+ void PublishInstanceRemoved(const std::string& addonId, AddonInstanceId instanceId);
+
+ /*! \brief Load the addon in the given path
+ This loads the addon using c-pluff which parses the addon descriptor file.
+ \param path folder that contains the addon.
+ \param addon [out] returned addon.
+ \return true if addon is set, false otherwise.
+ */
+ bool LoadAddonDescription(const std::string& path, AddonPtr& addon);
+
+ bool ServicesHasStarted() const;
+
+ /*!
+ * @deprecated This addon function should no more used and becomes replaced
+ * in future with the other below by his callers.
+ */
+ bool IsCompatible(const IAddon& addon) const;
+
+ /*!
+ * @brief Check given addon information is compatible with Kodi.
+ *
+ * @param[in] addonInfo Addon information to check
+ * @return true if compatible, false if not
+ */
+ bool IsCompatible(const AddonInfoPtr& addonInfo) const;
+
+ /*! \brief Recursively get dependencies for an add-on
+ * \param id the id of the root addon
+ * \param onlyEnabledRootAddon whether look for enabled root add-ons only
+ */
+ std::vector<DependencyInfo> GetDepsRecursive(const std::string& id,
+ OnlyEnabledRootAddon onlyEnabledRootAddon);
+
+ /*!
+ * @brief Get a list of add-on's with info's for the on system available
+ * ones.
+ *
+ * @param[out] addonInfos list where finded addon information becomes stored
+ * @param[in] onlyEnabled If true are only enabled ones given back,
+ * if false all on system available. Default is true.
+ * @param[in] type The requested type, with "ADDON_UNKNOWN" are all add-on
+ * types given back who match the case with value before.
+ * If a type id becomes added are only add-ons returned who
+ * match them. Default is for all types.
+ * @return true if the list contains entries
+ */
+ bool GetAddonInfos(std::vector<AddonInfoPtr>& addonInfos, bool onlyEnabled, AddonType type) const;
+
+ /*!
+ * @brief Get a list of add-on's with info's for the on system available
+ * ones.
+ *
+ * @param[in] onlyEnabled If true are only enabled ones given back,
+ * if false all on system available. Default is true.
+ * @param[in] types List about requested types.
+ * @return List where finded addon information becomes returned.
+ *
+ * @note @ref ADDON_UNKNOWN should not used for here!
+ */
+ std::vector<AddonInfoPtr> GetAddonInfos(bool onlyEnabled,
+ const std::vector<AddonType>& types) const;
+
+ /*!
+ * @brief Get a list of disabled add-on's with info's
+ *
+ * @param[out] addonInfos list where finded addon information becomes stored
+ * @param[in] type The requested type, with "ADDON_UNKNOWN"
+ * are all add-on types given back who match the case
+ * with value before.
+ * If a type id becomes added are only add-ons
+ * returned who match them. Default is for all types.
+ * @return true if the list contains entries
+ */
+ bool GetDisabledAddonInfos(std::vector<AddonInfoPtr>& addonInfos, AddonType type) const;
+
+ /*!
+ * @brief Get a list of disabled add-on's with info's for the on system
+ * available ones with a specific disabled reason.
+ *
+ * @param[out] addonInfos list where finded addon information becomes stored
+ * @param[in] type The requested type, with "ADDON_UNKNOWN"
+ * are all add-on types given back who match the case
+ * with value before.
+ * If a type id becomes added are only add-ons
+ * returned who match them. Default is for all types.
+ * @param[in] disabledReason To get all disabled addons use the value
+ * "AddonDiasbledReason::NONE". If any other value
+ * is supplied only addons with that reason will be
+ * returned.
+ * @return true if the list contains entries
+ */
+ bool GetDisabledAddonInfos(std::vector<AddonInfoPtr>& addonInfos,
+ AddonType type,
+ AddonDisabledReason disabledReason) const;
+
+ const AddonInfoPtr GetAddonInfo(const std::string& id, AddonType type) const;
+
+ /*!
+ * @brief Get the path where temporary add-on files are stored
+ *
+ * @return the base path used for temporary addon paths
+ *
+ * @warning the folder and its contents are deleted when Kodi is closed
+ */
+ const std::string& GetTempAddonBasePath() { return m_tempAddonBasePath; }
+
+ AddonOriginType GetAddonOriginType(const AddonPtr& addon) const;
+
+ /*!
+ * \brief Check whether an addon has been disabled with a special reason.
+ * \param ID id of the addon
+ * \param disabledReason reason we want to check for (NONE, USER, INCOMPATIBLE, PERMANENT_FAILURE)
+ * \return true or false
+ */
+ bool IsAddonDisabledWithReason(const std::string& ID, AddonDisabledReason disabledReason) const;
+
+ /*!
+ * @brief Addon update and install management.
+ *
+ * Parts inside here are used for changes about addon system.
+ *
+ * @warning This should be never used from other places outside of addon
+ * system directory.
+ */
+ /*@{{{*/
+
+ /*!
+ * @brief Update addon origin data.
+ *
+ * This becomes called from @ref CAddonInstallJob to set the source repo and
+ * if update, to set also the date.
+ *
+ * @note This must be called after the addon manager has inserted a new addon
+ * with @ref FindAddons() into database.
+ *
+ * @param[in] addonId Identifier of addon
+ * @param[in] repoAddonId Identifier of related repository addon
+ * @param[in] isUpdate If call becomes done on already installed addon and
+ * update only.
+ * @return True if successfully done, otherwise false
+ *
+ * Currently listed call sources:
+ * - @ref CAddonInstallJob::DoWork
+ */
+ bool SetAddonOrigin(const std::string& addonId, const std::string& repoAddonId, bool isUpdate);
+
+ /*!
+ * @brief Parse a repository XML file for addons and load their descriptors.
+ *
+ * A repository XML is essentially a concatenated list of addon descriptors.
+ *
+ * @param[in] repo The repository info.
+ * @param[in] xml The XML document from repository.
+ * @param[out] addons returned list of addons.
+ * @return true if the repository XML file is parsed, false otherwise.
+ *
+ * Currently listed call sources:
+ * - @ref CRepository::FetchIndex
+ */
+ bool AddonsFromRepoXML(const RepositoryDirInfo& repo,
+ const std::string& xml,
+ std::vector<AddonInfoPtr>& addons);
+
+ /*@}}}*/
+
+ /*!
+ * \brief Retrieves list of outdated addons as well as their related
+ * available updates and stores them into map.
+ * \return map of outdated addons with their update
+ */
+ std::map<std::string, AddonWithUpdate> GetAddonsWithAvailableUpdate() const;
+
+ /*!
+ * \brief Retrieves list of compatible addon versions of all origins
+ * \param[in] addonId addon to look up
+ * \return vector containing compatible addon versions
+ */
+ std::vector<std::shared_ptr<IAddon>> GetCompatibleVersions(const std::string& addonId) const;
+
+ /*!
+ * \brief Return number of available updates formatted as string
+ * this can be used as a lightweight method of retrieving the number of updates
+ * rather than using the expensive GetAvailableUpdates call
+ * \return number of available updates
+ */
+ const std::string& GetLastAvailableUpdatesCountAsString() const;
+
+ /*!
+ * \brief returns a vector with all found orphaned dependencies.
+ * \return the vector
+ */
+ std::vector<std::shared_ptr<IAddon>> GetOrphanedDependencies() const;
+
+private:
+ CAddonMgr& operator=(CAddonMgr const&) = delete;
+
+ VECADDONS m_updateableAddons;
+
+ /*!
+ * \brief returns a vector with either available updates or outdated addons.
+ * usually called by its wrappers GetAvailableUpdates() or
+ * GetOutdatedAddons()
+ * \param[in] true to return outdated addons, false to return available updates
+ * \return vector filled with either available updates or outdated addons
+ */
+ std::vector<std::shared_ptr<IAddon>> GetAvailableUpdatesOrOutdatedAddons(
+ AddonCheckType addonCheckType) const;
+
+ bool GetAddonsInternal(AddonType type,
+ VECADDONS& addons,
+ OnlyEnabled onlyEnabled,
+ CheckIncompatible checkIncompatible) const;
+
+ bool EnableSingle(const std::string& id);
+
+ void FindAddons(ADDON_INFO_LIST& addonmap, const std::string& path);
+
+ /*!
+ * @brief Fills the the provided vector with the list of incompatible
+ * addons and returns if there's any.
+ *
+ * @param[out] incompatible List of incompatible addons
+ * @param[in] whether or not to include incompatible addons that are disabled
+ * @return true if there are incompatible addons
+ */
+ bool GetIncompatibleAddonInfos(std::vector<AddonInfoPtr>& incompatible,
+ bool includeDisabled) const;
+
+ /*!
+ * Get the list of of available updates
+ * \param[in,out] updates the vector of addons to be filled with addons that need to be updated (not blacklisted)
+ * \return if there are any addons needing updates
+ */
+ bool GetAddonUpdateCandidates(VECADDONS& updates) const;
+
+ /*!\brief Sort a list of addons for installation, i.e., defines the order of installation depending
+ * of each addon dependencies.
+ * \param[in,out] updates the vector of addons to sort
+ */
+ void SortByDependencies(VECADDONS& updates) const;
+
+ /*!
+ * Install the list of addon updates via AddonInstaller
+ * \param[in,out] updates the vector of addons to install (will be sorted)
+ * \param wait if the process should wait for all addons to install
+ * \param allowCheckForUpdates indicates if content update checks are allowed
+ * after installation of a repository addon from the list
+ */
+ void InstallAddonUpdates(VECADDONS& updates,
+ bool wait,
+ AllowCheckForUpdates allowCheckForUpdates) const;
+
+ // This guards the addon installation process to make sure
+ // addon updates are not installed concurrently
+ // while the migration is running. Addon updates can be triggered
+ // as a result of a repository update event.
+ // (migration will install any available update anyway)
+ mutable std::mutex m_installAddonsMutex;
+
+ std::map<std::string, AddonDisabledReason> m_disabled;
+ static std::map<AddonType, IAddonMgrCallback*> m_managers;
+ mutable CCriticalSection m_critSection;
+ std::unique_ptr<CAddonDatabase> m_database;
+ std::unique_ptr<CAddonUpdateRules> m_updateRules;
+ CEventSource<AddonEvent> m_events;
+ CBlockingEventSource<AddonEvent> m_unloadEvents;
+ std::set<std::string> m_systemAddons;
+ std::set<std::string> m_optionalSystemAddons;
+ ADDON_INFO_LIST m_installedAddons;
+
+ // Temporary path given to add-ons, whose content is deleted when Kodi is stopped
+ const std::string m_tempAddonBasePath = "special://temp/addons";
+
+ /*!
+ * latest count of available updates
+ */
+ mutable std::string m_lastAvailableUpdatesCountAsString;
+ mutable std::mutex m_lastAvailableUpdatesCountMutex;
+};
+
+}; /* namespace ADDON */
diff --git a/xbmc/addons/AddonProvider.h b/xbmc/addons/AddonProvider.h
new file mode 100644
index 0000000..2a1e7af
--- /dev/null
+++ b/xbmc/addons/AddonProvider.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017-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 "addons/kodi-dev-kit/include/kodi/AddonBase.h"
+
+#include <memory>
+
+/*
+* CAddonProvider
+* IUnknown implementation to retrieve sub-addons from already active addons
+* See Inputstream.cpp/h for an explaric use case
+*/
+
+namespace ADDON
+{
+class CAddonInfo;
+typedef std::shared_ptr<CAddonInfo> AddonInfoPtr;
+
+class IAddonProvider
+{
+public:
+ virtual ~IAddonProvider() = default;
+ enum INSTANCE_TYPE
+ {
+ INSTANCE_INPUTSTREAM,
+ INSTANCE_VIDEOCODEC
+ };
+ virtual void GetAddonInstance(INSTANCE_TYPE instance_type,
+ ADDON::AddonInfoPtr& addonInfo,
+ KODI_HANDLE& parentInstance) = 0;
+};
+
+} // namespace ADDON
diff --git a/xbmc/addons/AddonRepoInfo.h b/xbmc/addons/AddonRepoInfo.h
new file mode 100644
index 0000000..c7b99c1
--- /dev/null
+++ b/xbmc/addons/AddonRepoInfo.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2005-2020 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
+
+namespace ADDON
+{
+
+/**
+ * Struct - RepoInfo
+ */
+struct RepoInfo
+{
+ std::string m_repoId;
+ std::string m_origin;
+};
+
+}; /* namespace ADDON */
diff --git a/xbmc/addons/AddonRepos.cpp b/xbmc/addons/AddonRepos.cpp
new file mode 100644
index 0000000..77ae837
--- /dev/null
+++ b/xbmc/addons/AddonRepos.cpp
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2005-2020 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 "AddonRepos.h"
+
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonRepoInfo.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/Repository.h"
+#include "addons/RepositoryUpdater.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <vector>
+
+namespace
+{
+constexpr auto ALL_ADDON_IDS = "";
+constexpr auto ALL_REPOSITORIES = nullptr;
+} // anonymous namespace
+
+using namespace ADDON;
+
+static std::vector<RepoInfo> officialRepoInfos = CCompileInfo::LoadOfficialRepoInfos();
+
+/**********************************************************
+ * CAddonRepos
+ *
+ */
+
+CAddonRepos::CAddonRepos() : m_addonMgr(CServiceBroker::GetAddonMgr())
+{
+ m_valid = m_addonDb.Open() && LoadAddonsFromDatabase(ALL_ADDON_IDS, ALL_REPOSITORIES);
+}
+
+CAddonRepos::CAddonRepos(const std::string& addonId) : m_addonMgr(CServiceBroker::GetAddonMgr())
+{
+ m_valid = m_addonDb.Open() && LoadAddonsFromDatabase(addonId, ALL_REPOSITORIES);
+}
+
+CAddonRepos::CAddonRepos(const std::shared_ptr<IAddon>& repoAddon)
+ : m_addonMgr(CServiceBroker::GetAddonMgr())
+{
+ m_valid = m_addonDb.Open() && LoadAddonsFromDatabase(ALL_ADDON_IDS, repoAddon);
+}
+
+bool CAddonRepos::IsFromOfficialRepo(const std::shared_ptr<IAddon>& addon,
+ CheckAddonPath checkAddonPath)
+{
+ auto comparator = [&](const RepoInfo& officialRepo) {
+ if (checkAddonPath == CheckAddonPath::CHOICE_YES)
+ {
+ return (addon->Origin() == officialRepo.m_repoId &&
+ StringUtils::StartsWithNoCase(addon->Path(), officialRepo.m_origin));
+ }
+
+ return addon->Origin() == officialRepo.m_repoId;
+ };
+
+ return addon->Origin() == ORIGIN_SYSTEM ||
+ std::any_of(officialRepoInfos.begin(), officialRepoInfos.end(), comparator);
+}
+
+bool CAddonRepos::IsOfficialRepo(const std::string& repoId)
+{
+ return repoId == ORIGIN_SYSTEM || std::any_of(officialRepoInfos.begin(), officialRepoInfos.end(),
+ [&repoId](const RepoInfo& officialRepo) {
+ return repoId == officialRepo.m_repoId;
+ });
+}
+
+bool CAddonRepos::LoadAddonsFromDatabase(const std::string& addonId,
+ const std::shared_ptr<IAddon>& repoAddon)
+{
+ if (repoAddon != ALL_REPOSITORIES)
+ {
+ if (!m_addonDb.GetRepositoryContent(repoAddon->ID(), m_allAddons))
+ {
+ // Repo content is invalid. Ask for update and wait.
+ CServiceBroker::GetRepositoryUpdater().CheckForUpdates(
+ std::static_pointer_cast<CRepository>(repoAddon));
+ CServiceBroker::GetRepositoryUpdater().Await();
+
+ if (!m_addonDb.GetRepositoryContent(repoAddon->ID(), m_allAddons))
+ {
+
+ // could not connect to repository
+ KODI::MESSAGING::HELPERS::ShowOKDialogText(CVariant{repoAddon->Name()}, CVariant{24991});
+ return false;
+ }
+ }
+ }
+ else if (addonId == ALL_ADDON_IDS)
+ {
+ // load full repository content
+ m_addonDb.GetRepositoryContent(m_allAddons);
+ if (m_allAddons.empty())
+ return true;
+ }
+ else
+ {
+ // load specific addonId only
+ m_addonDb.FindByAddonId(addonId, m_allAddons);
+ }
+
+ if (m_allAddons.empty())
+ return false;
+
+ for (const auto& addon : m_allAddons)
+ {
+ if (m_addonMgr.IsCompatible(*addon))
+ {
+ m_addonsByRepoMap[addon->Origin()].insert({addon->ID(), addon});
+ }
+ }
+
+ for (const auto& repo : m_addonsByRepoMap)
+ {
+ CLog::Log(LOGDEBUG, "ADDONS: {} - {} addon(s) loaded", repo.first, repo.second.size());
+
+ const auto& addonsPerRepo = repo.second;
+
+ for (const auto& addonMapEntry : addonsPerRepo)
+ {
+ const auto& addonToAdd = addonMapEntry.second;
+
+ if (IsFromOfficialRepo(addonToAdd, CheckAddonPath::CHOICE_YES))
+ {
+ AddAddonIfLatest(addonToAdd, m_latestOfficialVersions);
+ }
+ else
+ {
+ AddAddonIfLatest(addonToAdd, m_latestPrivateVersions);
+ }
+
+ // add to latestVersionsByRepo
+ AddAddonIfLatest(repo.first, addonToAdd, m_latestVersionsByRepo);
+ }
+ }
+
+ return true;
+}
+
+void CAddonRepos::AddAddonIfLatest(const std::shared_ptr<IAddon>& addonToAdd,
+ std::map<std::string, std::shared_ptr<IAddon>>& map) const
+{
+ const auto& latestKnown = map.find(addonToAdd->ID());
+ if (latestKnown == map.end() || addonToAdd->Version() > latestKnown->second->Version())
+ map[addonToAdd->ID()] = addonToAdd;
+}
+
+void CAddonRepos::AddAddonIfLatest(
+ const std::string& repoId,
+ const std::shared_ptr<IAddon>& addonToAdd,
+ std::map<std::string, std::map<std::string, std::shared_ptr<IAddon>>>& map) const
+{
+ const auto& latestVersionByRepo = map.find(repoId);
+
+ if (latestVersionByRepo == map.end()) // repo not found
+ {
+ map[repoId].insert({addonToAdd->ID(), addonToAdd});
+ }
+ else
+ {
+ const auto& latestVersionEntryByRepo = latestVersionByRepo->second;
+ const auto& latestKnown = latestVersionEntryByRepo.find(addonToAdd->ID());
+
+ if (latestKnown == latestVersionEntryByRepo.end() ||
+ addonToAdd->Version() > latestKnown->second->Version())
+ map[repoId][addonToAdd->ID()] = addonToAdd;
+ }
+}
+
+void CAddonRepos::BuildUpdateOrOutdatedList(const std::vector<std::shared_ptr<IAddon>>& installed,
+ std::vector<std::shared_ptr<IAddon>>& result,
+ AddonCheckType addonCheckType) const
+{
+ std::shared_ptr<IAddon> update;
+
+ CLog::Log(LOGDEBUG, "CAddonRepos::{}: Building {} list from installed add-ons", __func__,
+ addonCheckType == AddonCheckType::OUTDATED_ADDONS ? "outdated" : "update");
+
+ for (const auto& addon : installed)
+ {
+ if (DoAddonUpdateCheck(addon, update))
+ {
+ result.emplace_back(addonCheckType == AddonCheckType::OUTDATED_ADDONS ? addon : update);
+ }
+ }
+}
+
+void CAddonRepos::BuildAddonsWithUpdateList(
+ const std::vector<std::shared_ptr<IAddon>>& installed,
+ std::map<std::string, AddonWithUpdate>& addonsWithUpdate) const
+{
+ std::shared_ptr<IAddon> update;
+
+ CLog::Log(LOGDEBUG,
+ "CAddonRepos::{}: Building combined addons-with-update map from installed add-ons",
+ __func__);
+
+ for (const auto& addon : installed)
+ {
+ if (DoAddonUpdateCheck(addon, update))
+ {
+ addonsWithUpdate.insert({addon->ID(), {addon, update}});
+ }
+ }
+}
+
+bool CAddonRepos::DoAddonUpdateCheck(const std::shared_ptr<IAddon>& addon,
+ std::shared_ptr<IAddon>& update) const
+{
+ CLog::Log(LOGDEBUG, "ADDONS: update check: addonID = {} / Origin = {} / Version = {}",
+ addon->ID(), addon->Origin(), addon->Version().asString());
+
+ update.reset();
+
+ const AddonRepoUpdateMode updateMode =
+ CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode();
+
+ bool hasOfficialUpdate = FindAddonAndCheckForUpdate(addon, m_latestOfficialVersions, update);
+
+ // addons with an empty origin have at least been checked against official repositories
+ if (!addon->Origin().empty())
+ {
+ if (ORIGIN_SYSTEM != addon->Origin() && !hasOfficialUpdate) // not a system addon
+ {
+
+ // we didn't find an official update.
+ // either version is current or that add-on isn't contained in official repos
+ if (IsFromOfficialRepo(addon, CheckAddonPath::CHOICE_NO))
+ {
+
+ // check further if it IS contained in official repos
+ if (updateMode == AddonRepoUpdateMode::ANY_REPOSITORY)
+ {
+ if (!FindAddonAndCheckForUpdate(addon, m_latestPrivateVersions, update))
+ {
+ return false;
+ }
+ }
+ }
+ else
+ {
+ // ...we check for updates in the origin repo only
+ const auto& repoEntry = m_latestVersionsByRepo.find(addon->Origin());
+ if (repoEntry != m_latestVersionsByRepo.end())
+ {
+ if (!FindAddonAndCheckForUpdate(addon, repoEntry->second, update))
+ {
+ return false;
+ }
+ }
+ }
+ }
+ }
+
+ if (update != nullptr)
+ {
+ CLog::Log(LOGDEBUG, "ADDONS: -- found -->: addonID = {} / Origin = {} / Version = {}",
+ update->ID(), update->Origin(), update->Version().asString());
+ return true;
+ }
+
+ return false;
+}
+
+bool CAddonRepos::FindAddonAndCheckForUpdate(
+ const std::shared_ptr<IAddon>& addonToCheck,
+ const std::map<std::string, std::shared_ptr<IAddon>>& map,
+ std::shared_ptr<IAddon>& update) const
+{
+ const auto& remote = map.find(addonToCheck->ID());
+ if (remote != map.end()) // is addon in the desired map?
+ {
+ if ((remote->second->Version() > addonToCheck->Version()) ||
+ m_addonMgr.IsAddonDisabledWithReason(addonToCheck->ID(), AddonDisabledReason::INCOMPATIBLE))
+ {
+ // return addon update
+ update = remote->second;
+ return true; // update found
+ }
+ }
+
+ // either addon wasn't found or it's up to date
+ return false;
+}
+
+bool CAddonRepos::GetLatestVersionByMap(const std::string& addonId,
+ const std::map<std::string, std::shared_ptr<IAddon>>& map,
+ std::shared_ptr<IAddon>& addon) const
+{
+ const auto& remote = map.find(addonId);
+ if (remote != map.end()) // is addon in the desired map?
+ {
+ addon = remote->second;
+ return true;
+ }
+
+ return false;
+}
+
+bool CAddonRepos::GetLatestAddonVersionFromAllRepos(const std::string& addonId,
+ std::shared_ptr<IAddon>& addon) const
+{
+ const AddonRepoUpdateMode updateMode =
+ CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode();
+
+ bool hasOfficialVersion = GetLatestVersionByMap(addonId, m_latestOfficialVersions, addon);
+
+ if (hasOfficialVersion)
+ {
+ if (updateMode == AddonRepoUpdateMode::ANY_REPOSITORY)
+ {
+ std::shared_ptr<IAddon> thirdPartyAddon;
+
+ // only use this version if it's higher than the official one
+ if (GetLatestVersionByMap(addonId, m_latestPrivateVersions, thirdPartyAddon))
+ {
+ if (thirdPartyAddon->Version() > addon->Version())
+ addon = thirdPartyAddon;
+ }
+ }
+ }
+ else
+ {
+ if (!GetLatestVersionByMap(addonId, m_latestPrivateVersions, addon))
+ return false;
+ }
+
+ return true;
+}
+
+void CAddonRepos::GetLatestAddonVersions(std::vector<std::shared_ptr<IAddon>>& addonList) const
+{
+ const AddonRepoUpdateMode updateMode =
+ CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode();
+
+ addonList.clear();
+
+ // first we insert all official addon versions into the resulting vector
+
+ for (const auto& officialVersion : m_latestOfficialVersions)
+ addonList.emplace_back(officialVersion.second);
+
+ // then we insert private addon versions if they don't exist in the official map
+ // or installation from ANY_REPOSITORY is allowed and the private version is higher
+
+ for (const auto& privateVersion : m_latestPrivateVersions)
+ {
+ const auto& officialVersion = m_latestOfficialVersions.find(privateVersion.first);
+ if (officialVersion == m_latestOfficialVersions.end() ||
+ (updateMode == AddonRepoUpdateMode::ANY_REPOSITORY &&
+ privateVersion.second->Version() > officialVersion->second->Version()))
+ {
+ addonList.emplace_back(privateVersion.second);
+ }
+ }
+}
+
+void CAddonRepos::GetLatestAddonVersionsFromAllRepos(
+ std::vector<std::shared_ptr<IAddon>>& addonList) const
+{
+ const AddonRepoUpdateMode updateMode =
+ CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode();
+
+ addonList.clear();
+
+ // first we insert all official addon versions into the resulting vector
+
+ for (const auto& officialVersion : m_latestOfficialVersions)
+ addonList.emplace_back(officialVersion.second);
+
+ // then we insert latest version per addon and repository if they don't exist in the official map
+ // or installation from ANY_REPOSITORY is allowed and the private version is higher
+
+ for (const auto& repo : m_latestVersionsByRepo)
+ {
+ // content of official repos is stored in m_latestVersionsByRepo too
+ // so we need to filter them out
+
+ if (std::none_of(officialRepoInfos.begin(), officialRepoInfos.end(),
+ [&](const ADDON::RepoInfo& officialRepo) {
+ return repo.first == officialRepo.m_repoId;
+ }))
+ {
+ for (const auto& latestAddon : repo.second)
+ {
+ const auto& officialVersion = m_latestOfficialVersions.find(latestAddon.first);
+ if (officialVersion == m_latestOfficialVersions.end() ||
+ (updateMode == AddonRepoUpdateMode::ANY_REPOSITORY &&
+ latestAddon.second->Version() > officialVersion->second->Version()))
+ {
+ addonList.emplace_back(latestAddon.second);
+ }
+ }
+ }
+ }
+}
+
+bool CAddonRepos::FindDependency(const std::string& dependsId,
+ const std::string& parentRepoId,
+ std::shared_ptr<IAddon>& dependencyToInstall,
+ std::shared_ptr<CRepository>& repoForDep) const
+{
+ const AddonRepoUpdateMode updateMode =
+ CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode();
+
+ bool dependencyHasOfficialVersion =
+ GetLatestVersionByMap(dependsId, m_latestOfficialVersions, dependencyToInstall);
+
+ if (dependencyHasOfficialVersion)
+ {
+ if (updateMode == AddonRepoUpdateMode::ANY_REPOSITORY)
+ {
+ std::shared_ptr<IAddon> thirdPartyDependency;
+
+ // only use this version if it's higher than the official one
+ if (GetLatestVersionByMap(dependsId, m_latestPrivateVersions, thirdPartyDependency))
+ {
+ if (thirdPartyDependency->Version() > dependencyToInstall->Version())
+ dependencyToInstall = thirdPartyDependency;
+ }
+ }
+ }
+ else
+ {
+ // If we didn't find an official version of this dependency
+ // ...we check in the origin repo of the parent
+ if (!FindDependencyByParentRepo(dependsId, parentRepoId, dependencyToInstall))
+ return false;
+ }
+
+ // we got the dependency, so now get a repository-pointer to return
+
+ std::shared_ptr<IAddon> tmp;
+ if (!m_addonMgr.GetAddon(dependencyToInstall->Origin(), tmp, AddonType::REPOSITORY,
+ OnlyEnabled::CHOICE_YES))
+ return false;
+
+ repoForDep = std::static_pointer_cast<CRepository>(tmp);
+
+ CLog::Log(LOGDEBUG, "ADDONS: found dependency [{}] for install/update from repo [{}]",
+ dependencyToInstall->ID(), repoForDep->ID());
+
+ if (dependencyToInstall->HasType(AddonType::REPOSITORY))
+ {
+ CLog::Log(LOGDEBUG,
+ "ADDONS: dependency with id [{}] has type ADDON_REPOSITORY and will not install!",
+ dependencyToInstall->ID());
+
+ return false;
+ }
+
+ return true;
+}
+
+bool CAddonRepos::FindDependencyByParentRepo(const std::string& dependsId,
+ const std::string& parentRepoId,
+ std::shared_ptr<IAddon>& dependencyToInstall) const
+{
+ const auto& repoEntry = m_latestVersionsByRepo.find(parentRepoId);
+ if (repoEntry != m_latestVersionsByRepo.end())
+ {
+ if (GetLatestVersionByMap(dependsId, repoEntry->second, dependencyToInstall))
+ return true;
+ }
+
+ return false;
+}
+
+void CAddonRepos::BuildCompatibleVersionsList(
+ std::vector<std::shared_ptr<IAddon>>& compatibleVersions) const
+{
+ std::vector<std::shared_ptr<IAddon>> officialVersions;
+ std::vector<std::shared_ptr<IAddon>> privateVersions;
+
+ for (const auto& addon : m_allAddons)
+ {
+ if (m_addonMgr.IsCompatible(*addon))
+ {
+ if (IsFromOfficialRepo(addon, CheckAddonPath::CHOICE_YES))
+ {
+ officialVersions.emplace_back(addon);
+ }
+ else
+ {
+ privateVersions.emplace_back(addon);
+ }
+ }
+ }
+
+ auto comparator = [](const std::shared_ptr<IAddon>& a, const std::shared_ptr<IAddon>& b) {
+ return (a->Version() > b->Version());
+ };
+
+ std::sort(officialVersions.begin(), officialVersions.end(), comparator);
+ std::sort(privateVersions.begin(), privateVersions.end(), comparator);
+
+ compatibleVersions = std::move(officialVersions);
+ std::move(privateVersions.begin(), privateVersions.end(), std::back_inserter(compatibleVersions));
+}
diff --git a/xbmc/addons/AddonRepos.h b/xbmc/addons/AddonRepos.h
new file mode 100644
index 0000000..bb9be34
--- /dev/null
+++ b/xbmc/addons/AddonRepos.h
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2005-2020 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 "addons/AddonDatabase.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace ADDON
+{
+
+class CAddonVersion;
+class CAddonMgr;
+class CRepository;
+class IAddon;
+enum class AddonCheckType : bool;
+
+enum class CheckAddonPath
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+/**
+ * Struct - CAddonWithUpdate
+ */
+struct AddonWithUpdate
+{
+ std::shared_ptr<IAddon> m_installed;
+ std::shared_ptr<IAddon> m_update;
+};
+
+/**
+ * Class - CAddonRepos
+ * Reads information about installed official/third party repos and their contained add-ons from the database.
+ * Used to check for updates for installed add-ons and dependencies while obeying permission rules.
+ * Note that this class is not responsible for refreshing the repo data stored in the database.
+ */
+class CAddonRepos
+{
+public:
+ CAddonRepos(); // load all add-ons from all installed repositories
+ explicit CAddonRepos(const std::string& addonId); // load a specific add-on id only
+ explicit CAddonRepos(const std::shared_ptr<IAddon>& repoAddon); // load add-ons of a specific repo
+
+ /*!
+ * \brief Build the list of addons to be updated depending on defined rules
+ * or the list of outdated addons
+ * \param installed vector of all addons installed on the system that are
+ * checked for an update
+ * \param[in] addonCheckType build list of OUTDATED or UPDATES
+ * \param[out] result list of addon versions that are going to be installed
+ * or are outdated
+ */
+ void BuildUpdateOrOutdatedList(const std::vector<std::shared_ptr<IAddon>>& installed,
+ std::vector<std::shared_ptr<IAddon>>& result,
+ AddonCheckType addonCheckType) const;
+
+ /*!
+ * \brief Build the list of outdated addons and their available updates.
+ * \param installed vector of all addons installed on the system that are
+ * checked for an update
+ * \param[out] addonsWithUpdate target map
+ */
+ void BuildAddonsWithUpdateList(const std::vector<std::shared_ptr<IAddon>>& installed,
+ std::map<std::string, AddonWithUpdate>& addonsWithUpdate) const;
+
+ /*!
+ * \brief Checks if the origin-repository of a given addon is defined as official repo
+ * and can also verify if the origin-path (e.g. https://mirrors.kodi.tv ...)
+ * is matching
+ * \note if this function is called on locally installed add-ons, for instance when populating
+ * 'My add-ons', the local installation path is returned as origin.
+ * thus parameter CheckAddonPath::CHOICE_NO needs to be passed in such cases
+ * \param addon pointer to addon to be checked
+ * \param checkAddonPath also check origin path
+ * \return true if the repository id of a given addon is defined as official
+ * and the addons origin matches the defined official origin of the repo id
+ */
+ static bool IsFromOfficialRepo(const std::shared_ptr<IAddon>& addon,
+ CheckAddonPath checkAddonPath);
+
+ /*!
+ * \brief Checks if the passed in repository is defined as official repo
+ * which includes ORIGIN_SYSTEM
+ * \param repoId repository id to check
+ * \return true if the repository id is defined as official, false otherwise
+ */
+ static bool IsOfficialRepo(const std::string& repoId);
+
+ /*!
+ * \brief Check if an update is available for a single addon
+ * \param addon that is checked for an update
+ * \param[out] update pointer to the found update
+ * \return true if an installable update was found, false otherwise
+ */
+ bool DoAddonUpdateCheck(const std::shared_ptr<IAddon>& addon,
+ std::shared_ptr<IAddon>& update) const;
+
+ /*!
+ * \brief Retrieves the latest version of an addon from all installed repositories
+ * follows addon origin restriction rules
+ * \param addonId addon id we're looking the latest version for
+ * \param[out] addon pointer to the found addon
+ * \return true if a version was found, false otherwise
+ */
+ bool GetLatestAddonVersionFromAllRepos(const std::string& addonId,
+ std::shared_ptr<IAddon>& addon) const;
+
+ /*!
+ * \brief Retrieves the latest official versions of addons to vector.
+ * Private versions are added obeying updateMode.
+ * (either OFFICIAL_ONLY or ANY_REPOSITORY)
+ * \param[out] addonList retrieved addon list in a vector
+ */
+ void GetLatestAddonVersions(std::vector<std::shared_ptr<IAddon>>& addonList) const;
+
+ /*!
+ * \brief Retrieves the latest official versions of addons to vector.
+ * Private versions (latest per repository) are added obeying updateMode.
+ * (either OFFICIAL_ONLY or ANY_REPOSITORY)
+ * \param[out] addonList retrieved addon list in a vector
+ */
+ void GetLatestAddonVersionsFromAllRepos(std::vector<std::shared_ptr<IAddon>>& addonList) const;
+
+ /*!
+ * \brief Find a dependency to install during an addon install or update
+ * If the dependency cannot be found in official versions we look in the
+ * installing/updating addon's (the parent's) origin repository
+ * \param dependsId addon id of the dependency we're looking for
+ * \param parentRepoId origin repository of the dependee
+ * \param [out] dependencyToInstall pointer to the found dependency, only use
+ * if function returns true
+ * \param [out] repoForDep the repository that dependency will install from finally
+ * \return true if the dependency was found, false otherwise
+ */
+ bool FindDependency(const std::string& dependsId,
+ const std::string& parentRepoId,
+ std::shared_ptr<IAddon>& dependencyToInstall,
+ std::shared_ptr<CRepository>& repoForDep) const;
+
+ /*!
+ * \brief Find a dependency addon in the repository of its parent
+ * \param dependsId addon id of the dependency we're looking for
+ * \param parentRepoId origin repository of the dependee
+ * \param [out] dependencyToInstall pointer to the found dependency, only use
+ * if function returns true
+ * \return true if the dependency was found, false otherwise
+ */
+ bool FindDependencyByParentRepo(const std::string& dependsId,
+ const std::string& parentRepoId,
+ std::shared_ptr<IAddon>& dependencyToInstall) const;
+
+ /*!
+ * \brief Build compatible versions list based on the contents of m_allAddons
+ * \note content of m_allAddons depends on the preceding call to @ref LoadAddonsFromDatabase()
+ * \param[out] compatibleVersions target vector to be filled
+ */
+ void BuildCompatibleVersionsList(std::vector<std::shared_ptr<IAddon>>& compatibleVersions) const;
+
+ /*!
+ * \brief Return whether add-ons repo/version information was properly loaded after construction
+ * \return true on success, false otherwise
+ */
+ bool IsValid() const { return m_valid; }
+
+private:
+ /*!
+ * \brief Load and configure add-on maps
+ * \return true on success, false otherwise
+ */
+ bool LoadAddonsFromDatabase(const std::string& addonId, const std::shared_ptr<IAddon>& repoAddon);
+
+ /*!
+ * \brief Looks up an addon in a given repository map and
+ * checks if an update is available
+ * \param addonToCheck the addon we want to find and version check
+ * \param map the repository map we want to check against
+ * \param[out] pointer to the found update. if the addon is
+ * up-to-date on our system, this param will return 'nullptr'
+ * \return true if the addon was found in the desired map and
+ * its version is newer than our local version.
+ * false if the addon does NOT exist in the map or it is up to date.
+ */
+ bool FindAddonAndCheckForUpdate(const std::shared_ptr<IAddon>& addonToCheck,
+ const std::map<std::string, std::shared_ptr<IAddon>>& map,
+ std::shared_ptr<IAddon>& update) const;
+
+ /*!
+ * \brief Adds the latest version of an addon to the desired map
+ * \param addonToAdd the addon whose latest version should be added
+ * \param map target map, e.g. latestOfficialVersions or latestPrivateVersions
+ */
+ void AddAddonIfLatest(const std::shared_ptr<IAddon>& addonToAdd,
+ std::map<std::string, std::shared_ptr<IAddon>>& map) const;
+
+ /*!
+ * \brief Adds the latest version of an addon to the desired map per repository
+ * used to populate 'latestVersionsByRepo'
+ * \param repoId the repository that addon comes from
+ * \param addonToAdd the addon whose latest version should be added
+ * \param map target map, latestVersionsByRepo
+ */
+ void AddAddonIfLatest(
+ const std::string& repoId,
+ const std::shared_ptr<IAddon>& addonToAdd,
+ std::map<std::string, std::map<std::string, std::shared_ptr<IAddon>>>& map) const;
+
+ /*!
+ * \brief Looks up an addon entry in a specific map
+ * \param addonId addon we want to retrieve
+ * \param map the map we're looking into for the wanted addon
+ * \param[out] addon pointer to the found addon, only use when function returns true
+ * \return true if the addon was found in the map, false otherwise
+ */
+ bool GetLatestVersionByMap(const std::string& addonId,
+ const std::map<std::string, std::shared_ptr<IAddon>>& map,
+ std::shared_ptr<IAddon>& addon) const;
+
+ const CAddonMgr& m_addonMgr;
+ CAddonDatabase m_addonDb;
+ bool m_valid{false};
+
+ std::vector<std::shared_ptr<IAddon>> m_allAddons;
+
+ std::map<std::string, std::shared_ptr<IAddon>> m_latestOfficialVersions;
+ std::map<std::string, std::shared_ptr<IAddon>> m_latestPrivateVersions;
+ std::map<std::string, std::map<std::string, std::shared_ptr<IAddon>>> m_latestVersionsByRepo;
+ std::map<std::string, std::multimap<std::string, std::shared_ptr<IAddon>>> m_addonsByRepoMap;
+};
+
+}; /* namespace ADDON */
diff --git a/xbmc/addons/AddonStatusHandler.cpp b/xbmc/addons/AddonStatusHandler.cpp
new file mode 100644
index 0000000..3c6f478
--- /dev/null
+++ b/xbmc/addons/AddonStatusHandler.cpp
@@ -0,0 +1,131 @@
+/*
+ * 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 "AddonStatusHandler.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddonManagerCallback.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <utility>
+
+using namespace KODI::MESSAGING;
+
+namespace ADDON
+{
+
+/**********************************************************
+ * CAddonStatusHandler - AddOn Status Report Class
+ *
+ * Used to inform the user about occurred errors and
+ * changes inside Add-on's, and ask him what to do.
+ *
+ */
+
+CCriticalSection CAddonStatusHandler::m_critSection;
+
+CAddonStatusHandler::CAddonStatusHandler(const std::string& addonID,
+ AddonInstanceId instanceId,
+ ADDON_STATUS status,
+ bool sameThread)
+ : CThread(("AddonStatus " + std::to_string(instanceId) + "@" + addonID).c_str()),
+ m_instanceId(instanceId),
+ m_status(ADDON_STATUS_UNKNOWN)
+{
+ //! @todo The status handled CAddonStatusHandler by is related to the class, not the instance
+ //! having CAddonMgr construct an instance makes no sense
+ if (!CServiceBroker::GetAddonMgr().GetAddon(addonID, m_addon, OnlyEnabled::CHOICE_YES))
+ return;
+
+ CLog::Log(LOGINFO,
+ "Called Add-on status handler for '{}' of clientName:{}, clientID:{}, instanceID:{} "
+ "(same Thread={})",
+ status, m_addon->Name(), m_addon->ID(), m_instanceId, sameThread ? "yes" : "no");
+
+ m_status = status;
+
+ if (sameThread)
+ {
+ Process();
+ }
+ else
+ {
+ Create(true);
+ }
+}
+
+CAddonStatusHandler::~CAddonStatusHandler()
+{
+ StopThread();
+}
+
+void CAddonStatusHandler::OnStartup()
+{
+ SetPriority(ThreadPriority::LOWEST);
+}
+
+void CAddonStatusHandler::OnExit()
+{
+}
+
+void CAddonStatusHandler::Process()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ std::string heading = StringUtils::Format(
+ "{}: {}", CAddonInfo::TranslateType(m_addon->Type(), true), m_addon->Name());
+
+ /* Request to restart the AddOn and data structures need updated */
+ if (m_status == ADDON_STATUS_NEED_RESTART)
+ {
+ HELPERS::ShowOKDialogLines(CVariant{heading}, CVariant{24074});
+ CServiceBroker::GetAddonMgr()
+ .GetCallbackForType(m_addon->Type())
+ ->RequestRestart(m_addon->ID(), m_instanceId, true);
+ }
+ /* Some required settings are missing/invalid */
+ else if (m_status == ADDON_STATUS_NEED_SETTINGS)
+ {
+ CGUIDialogYesNo* pDialogYesNo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (!pDialogYesNo) return;
+
+ pDialogYesNo->SetHeading(CVariant{heading});
+ pDialogYesNo->SetLine(1, CVariant{24070});
+ pDialogYesNo->SetLine(2, CVariant{24072});
+ pDialogYesNo->Open();
+
+ if (!pDialogYesNo->IsConfirmed()) return;
+
+ if (!m_addon->HasSettings(m_instanceId))
+ return;
+
+ if (CGUIDialogAddonSettings::ShowForAddon(m_addon))
+ {
+ //! @todo Doesn't dialogaddonsettings save these automatically? It should do this.
+ m_addon->SaveSettings(m_instanceId);
+ CServiceBroker::GetAddonMgr()
+ .GetCallbackForType(m_addon->Type())
+ ->RequestRestart(m_addon->ID(), m_instanceId, true);
+ }
+ }
+}
+
+
+} /*namespace ADDON*/
+
diff --git a/xbmc/addons/AddonStatusHandler.h b/xbmc/addons/AddonStatusHandler.h
new file mode 100644
index 0000000..3254ea0
--- /dev/null
+++ b/xbmc/addons/AddonStatusHandler.h
@@ -0,0 +1,45 @@
+/*
+ * 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 "addons/IAddon.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon_base.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+namespace ADDON
+{
+ /**
+ * Class - CAddonStatusHandler
+ * Used to inform the user about occurred errors and
+ * changes inside Add-on's, and ask him what to do.
+ * It can executed in the same thread as the calling
+ * function or in a separate thread.
+ */
+ class CAddonStatusHandler : private CThread
+ {
+ public:
+ CAddonStatusHandler(const std::string& addonID,
+ AddonInstanceId instanceId,
+ ADDON_STATUS status,
+ bool sameThread = true);
+ ~CAddonStatusHandler() override;
+
+ /* Thread handling */
+ void Process() override;
+ void OnStartup() override;
+ void OnExit() override;
+
+ private:
+ static CCriticalSection m_critSection;
+ const uint32_t m_instanceId;
+ AddonPtr m_addon;
+ ADDON_STATUS m_status;
+ };
+}
diff --git a/xbmc/addons/AddonSystemSettings.cpp b/xbmc/addons/AddonSystemSettings.cpp
new file mode 100644
index 0000000..30f3aaf
--- /dev/null
+++ b/xbmc/addons/AddonSystemSettings.cpp
@@ -0,0 +1,157 @@
+/*
+ * 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 "AddonSystemSettings.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+CAddonSystemSettings::CAddonSystemSettings()
+ : m_activeSettings{
+ {AddonType::AUDIOENCODER, CSettings::SETTING_AUDIOCDS_ENCODER},
+ {AddonType::RESOURCE_LANGUAGE, CSettings::SETTING_LOCALE_LANGUAGE},
+ {AddonType::RESOURCE_UISOUNDS, CSettings::SETTING_LOOKANDFEEL_SOUNDSKIN},
+ {AddonType::SCRAPER_ALBUMS, CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER},
+ {AddonType::SCRAPER_ARTISTS, CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER},
+ {AddonType::SCRAPER_MOVIES, CSettings::SETTING_SCRAPERS_MOVIESDEFAULT},
+ {AddonType::SCRAPER_MUSICVIDEOS, CSettings::SETTING_SCRAPERS_MUSICVIDEOSDEFAULT},
+ {AddonType::SCRAPER_TVSHOWS, CSettings::SETTING_SCRAPERS_TVSHOWSDEFAULT},
+ {AddonType::SCREENSAVER, CSettings::SETTING_SCREENSAVER_MODE},
+ {AddonType::SCRIPT_WEATHER, CSettings::SETTING_WEATHER_ADDON},
+ {AddonType::SKIN, CSettings::SETTING_LOOKANDFEEL_SKIN},
+ {AddonType::WEB_INTERFACE, CSettings::SETTING_SERVICES_WEBSKIN},
+ {AddonType::VISUALIZATION, CSettings::SETTING_MUSICPLAYER_VISUALISATION},
+ }
+{}
+
+CAddonSystemSettings& CAddonSystemSettings::GetInstance()
+{
+ static CAddonSystemSettings inst;
+ return inst;
+}
+
+void CAddonSystemSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting->GetId() == CSettings::SETTING_ADDONS_MANAGE_DEPENDENCIES)
+ {
+ std::vector<std::string> params{"addons://dependencies/", "return"};
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_ADDON_BROWSER, params);
+ }
+ else if (setting->GetId() == CSettings::SETTING_ADDONS_SHOW_RUNNING)
+ {
+ std::vector<std::string> params{"addons://running/", "return"};
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_ADDON_BROWSER, params);
+ }
+ else if (setting->GetId() == CSettings::SETTING_ADDONS_REMOVE_ORPHANED_DEPENDENCIES)
+ {
+ using namespace KODI::MESSAGING::HELPERS;
+
+ const auto removedItems = CAddonInstaller::GetInstance().RemoveOrphanedDepsRecursively();
+ if (removedItems.size() > 0)
+ {
+ const auto message =
+ StringUtils::Format(g_localizeStrings.Get(36641), StringUtils::Join(removedItems, ", "));
+
+ ShowOKDialogText(CVariant{36640}, CVariant{message}); // "following orphaned were removed..."
+ }
+ else
+ {
+ ShowOKDialogText(CVariant{36640}, CVariant{36642}); // "no orphaned found / removed"
+ }
+ }
+}
+
+void CAddonSystemSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ using namespace KODI::MESSAGING::HELPERS;
+
+ if (setting->GetId() == CSettings::SETTING_ADDONS_ALLOW_UNKNOWN_SOURCES &&
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_ADDONS_ALLOW_UNKNOWN_SOURCES) &&
+ ShowYesNoDialogText(19098, 36618) != DialogResponse::CHOICE_YES)
+ {
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetBool(CSettings::SETTING_ADDONS_ALLOW_UNKNOWN_SOURCES, false);
+ }
+}
+
+bool CAddonSystemSettings::GetActive(AddonType type, AddonPtr& addon)
+{
+ auto it = m_activeSettings.find(type);
+ if (it != m_activeSettings.end())
+ {
+ auto settingValue = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(it->second);
+ return CServiceBroker::GetAddonMgr().GetAddon(settingValue, addon, type,
+ OnlyEnabled::CHOICE_YES);
+ }
+ return false;
+}
+
+bool CAddonSystemSettings::SetActive(AddonType type, const std::string& addonID)
+{
+ auto it = m_activeSettings.find(type);
+ if (it != m_activeSettings.end())
+ {
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(it->second, addonID);
+ return true;
+ }
+ return false;
+}
+
+bool CAddonSystemSettings::IsActive(const IAddon& addon)
+{
+ AddonPtr active;
+ return GetActive(addon.Type(), active) && active->ID() == addon.ID();
+}
+
+bool CAddonSystemSettings::UnsetActive(const AddonInfoPtr& addon)
+{
+ auto it = m_activeSettings.find(addon->MainType());
+ if (it == m_activeSettings.end())
+ return true;
+
+ auto setting = std::static_pointer_cast<CSettingString>(CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(it->second));
+ if (setting->GetValue() != addon->ID())
+ return true;
+
+ if (setting->GetDefault() == addon->ID())
+ return false; // Cant unset defaults
+
+ setting->Reset();
+ return true;
+}
+
+int CAddonSystemSettings::GetAddonAutoUpdateMode() const
+{
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_ADDONS_AUTOUPDATES);
+}
+
+AddonRepoUpdateMode CAddonSystemSettings::GetAddonRepoUpdateMode() const
+{
+ const int updateMode = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_ADDONS_UPDATEMODE);
+ return static_cast<AddonRepoUpdateMode>(updateMode);
+}
+}
diff --git a/xbmc/addons/AddonSystemSettings.h b/xbmc/addons/AddonSystemSettings.h
new file mode 100644
index 0000000..bf5ca38
--- /dev/null
+++ b/xbmc/addons/AddonSystemSettings.h
@@ -0,0 +1,78 @@
+/*
+ * 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 "settings/lib/ISettingCallback.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+namespace ADDON
+{
+
+const int AUTO_UPDATES_ON = 0;
+const int AUTO_UPDATES_NOTIFY = 1;
+const int AUTO_UPDATES_NEVER = 2;
+
+enum class AddonRepoUpdateMode
+{
+ OFFICIAL_ONLY = 0,
+ ANY_REPOSITORY = 1
+};
+
+enum class AddonType;
+
+class CAddonInfo;
+using AddonInfoPtr = std::shared_ptr<CAddonInfo>;
+
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+
+class CAddonSystemSettings : public ISettingCallback
+{
+public:
+ static CAddonSystemSettings& GetInstance();
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ bool GetActive(AddonType type, AddonPtr& addon);
+ bool SetActive(AddonType type, const std::string& addonID);
+ bool IsActive(const IAddon& addon);
+
+ /*!
+ * Gets Kodi addon auto update mode
+ *
+ * @return the autoupdate mode value
+ */
+ int GetAddonAutoUpdateMode() const;
+
+
+ /*!
+ * Gets Kodi preferred addon repository update mode
+ *
+ * @return the preferred mode value
+ */
+ AddonRepoUpdateMode GetAddonRepoUpdateMode() const;
+
+ /*!
+ * Attempt to unset addon as active. Returns true if addon is no longer active,
+ * false if it could not be unset (e.g. if the addon is the default)
+ */
+ bool UnsetActive(const AddonInfoPtr& addon);
+
+private:
+ CAddonSystemSettings();
+ CAddonSystemSettings(const CAddonSystemSettings&) = delete;
+ CAddonSystemSettings& operator=(const CAddonSystemSettings&) = delete;
+ ~CAddonSystemSettings() override = default;
+
+ const std::map<AddonType, std::string> m_activeSettings;
+};
+};
diff --git a/xbmc/addons/AddonUpdateRules.cpp b/xbmc/addons/AddonUpdateRules.cpp
new file mode 100644
index 0000000..7d9d43a
--- /dev/null
+++ b/xbmc/addons/AddonUpdateRules.cpp
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2005-2020 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 "AddonUpdateRules.h"
+
+#include "AddonDatabase.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace ADDON;
+
+bool CAddonUpdateRules::RefreshRulesMap(const CAddonDatabase& db)
+{
+ m_updateRules.clear();
+ db.GetAddonUpdateRules(m_updateRules);
+ return true;
+}
+
+bool CAddonUpdateRules::IsAutoUpdateable(const std::string& id) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_updateRules.find(id) == m_updateRules.end();
+}
+
+bool CAddonUpdateRules::IsUpdateableByRule(const std::string& id, AddonUpdateRule updateRule) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto& updateRulesEntry = m_updateRules.find(id);
+ return (updateRulesEntry == m_updateRules.end() ||
+ std::none_of(updateRulesEntry->second.begin(), updateRulesEntry->second.end(),
+ [updateRule](AddonUpdateRule rule) { return rule == updateRule; }));
+}
+
+bool CAddonUpdateRules::AddUpdateRuleToList(CAddonDatabase& db,
+ const std::string& id,
+ AddonUpdateRule updateRule)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!IsUpdateableByRule(id, updateRule))
+ {
+ return true;
+ }
+
+ if (db.AddUpdateRuleForAddon(id, updateRule))
+ {
+ m_updateRules[id].emplace_back(updateRule);
+ return true;
+ }
+ return false;
+}
+
+bool CAddonUpdateRules::RemoveUpdateRuleFromList(CAddonDatabase& db,
+ const std::string& id,
+ AddonUpdateRule updateRule)
+{
+ return (updateRule != AddonUpdateRule::ANY && RemoveFromUpdateRuleslist(db, id, updateRule));
+}
+
+bool CAddonUpdateRules::RemoveAllUpdateRulesFromList(CAddonDatabase& db, const std::string& id)
+{
+ return RemoveFromUpdateRuleslist(db, id, AddonUpdateRule::ANY);
+}
+
+bool CAddonUpdateRules::RemoveFromUpdateRuleslist(CAddonDatabase& db,
+ const std::string& id,
+ AddonUpdateRule updateRule)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const auto& updateRulesEntry = m_updateRules.find(id);
+ if (updateRulesEntry != m_updateRules.end())
+ {
+ bool onlySingleRule = (updateRulesEntry->second.size() == 1);
+
+ if (updateRule == AddonUpdateRule::ANY ||
+ (onlySingleRule && updateRulesEntry->second.front() == updateRule))
+ {
+ if (db.RemoveAllUpdateRulesForAddon(id))
+ {
+ m_updateRules.erase(id);
+ return true;
+ }
+ }
+ else if (!onlySingleRule)
+ {
+ const auto& position =
+ std::find(updateRulesEntry->second.begin(), updateRulesEntry->second.end(), updateRule);
+ if (position != updateRulesEntry->second.end() && db.RemoveUpdateRuleForAddon(id, updateRule))
+ {
+ updateRulesEntry->second.erase(position);
+ return true;
+ }
+ }
+ }
+ return false;
+}
diff --git a/xbmc/addons/AddonUpdateRules.h b/xbmc/addons/AddonUpdateRules.h
new file mode 100644
index 0000000..1538b74
--- /dev/null
+++ b/xbmc/addons/AddonUpdateRules.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2005-2020 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 <map>
+#include <string>
+#include <vector>
+
+namespace ADDON
+{
+
+class CAddonDatabase;
+
+enum class AddonUpdateRule;
+
+/**
+ * Class - CAddonUpdateRules
+ * Manages information about the updateability of addons by defining
+ * and handling certain rules.
+ */
+class CAddonUpdateRules
+{
+public:
+ /* \brief Refresh addon update rules map from the database
+ * \param db database connection
+ * \return true on success, false otherwise
+ */
+ bool RefreshRulesMap(const CAddonDatabase& db);
+
+ /* \brief Check if an addon version is auto updateable
+ * \param id addon id to be checked
+ * \return true is addon is auto updateable, false otherwise
+ */
+ bool IsAutoUpdateable(const std::string& id) const;
+
+ /* \brief Add a single rule to the update rules list for an addon
+ * \param db database connection
+ * \param id addon-id to set rule for
+ * \param the rule to set
+ * \return true on success, false otherwise
+ */
+ bool AddUpdateRuleToList(CAddonDatabase& db, const std::string& id, AddonUpdateRule updateRule);
+
+ /* \brief Remove a single rule from the update rules list for an addon
+ * \param db database connection
+ * \param id addon-id to remove rule for
+ * \param the rule to remove
+ * \return true on success, false otherwise
+ */
+ bool RemoveUpdateRuleFromList(CAddonDatabase& db,
+ const std::string& id,
+ AddonUpdateRule updateRule);
+
+ /* \brief Remove all rules from the update rules list for an addon
+ * \param db database connection
+ * \param id addon-id to remove rules for
+ * \return true on success, false otherwise
+ */
+ bool RemoveAllUpdateRulesFromList(CAddonDatabase& db, const std::string& id);
+
+private:
+ /* \brief Checks if an addon version is updateable with a specific rule
+ * \param id addon id to be checked
+ * \param updateRule the rule to check for
+ * \return true is addon is updateable by that updateRule, false otherwise
+ * \sa CAddonUpdateRules::IsAutoUpdateable()
+ */
+ bool IsUpdateableByRule(const std::string& id, AddonUpdateRule updateRule) const;
+
+ /* \brief Executor for @ref RemoveUpdateRuleFromList() and @ref RemoveAllUpdateRulesFromList()
+ */
+ bool RemoveFromUpdateRuleslist(CAddonDatabase& db,
+ const std::string& id,
+ AddonUpdateRule updateRule);
+
+ mutable CCriticalSection m_critSection;
+ std::map<std::string, std::vector<AddonUpdateRule>> m_updateRules;
+};
+
+}; /* namespace ADDON */
diff --git a/xbmc/addons/AddonVersion.cpp b/xbmc/addons/AddonVersion.cpp
new file mode 100644
index 0000000..10b314e
--- /dev/null
+++ b/xbmc/addons/AddonVersion.cpp
@@ -0,0 +1,182 @@
+/*
+ * 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 "AddonVersion.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+namespace
+{
+// Add-on versions are used e.g. in file names and should
+// not have too much freedom in their accepted characters
+// Things that should be allowed: e.g. 0.1.0~beta3+git010cab3
+// Note that all of these characters are url-safe
+const std::string VALID_ADDON_VERSION_CHARACTERS =
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.+_@~";
+} // namespace
+
+namespace ADDON
+{
+CAddonVersion::CAddonVersion(const std::string& version)
+ : mEpoch(0), mUpstream(version.empty() ? "0.0.0" : [&version] {
+ auto versionLowerCase = std::string(version);
+ StringUtils::ToLower(versionLowerCase);
+ return versionLowerCase;
+ }())
+{
+ size_t pos = mUpstream.find(':');
+ if (pos != std::string::npos)
+ {
+ mEpoch = strtol(mUpstream.c_str(), nullptr, 10);
+ mUpstream.erase(0, pos + 1);
+ }
+
+ pos = mUpstream.find('-');
+ if (pos != std::string::npos)
+ {
+ mRevision = mUpstream.substr(pos + 1);
+ if (mRevision.find_first_not_of(VALID_ADDON_VERSION_CHARACTERS) != std::string::npos)
+ {
+ CLog::Log(LOGERROR, "AddonVersion: {} is not a valid revision number", mRevision);
+ mRevision = "";
+ }
+ mUpstream.erase(pos);
+ }
+
+ if (mUpstream.find_first_not_of(VALID_ADDON_VERSION_CHARACTERS) != std::string::npos)
+ {
+ CLog::Log(LOGERROR, "AddonVersion: {} is not a valid version", mUpstream);
+ mUpstream = "0.0.0";
+ }
+}
+
+CAddonVersion::CAddonVersion(const char* version)
+ : CAddonVersion(std::string(version ? version : ""))
+{
+}
+
+/**Compare two components of a Debian-style version. Return -1, 0, or 1
+ * if a is less than, equal to, or greater than b, respectively.
+ */
+int CAddonVersion::CompareComponent(const char* a, const char* b)
+{
+ while (*a && *b)
+ {
+ while (*a && *b && !isdigit(*a) && !isdigit(*b))
+ {
+ if (*a != *b)
+ {
+ if (*a == '~')
+ return -1;
+ if (*b == '~')
+ return 1;
+ return *a < *b ? -1 : 1;
+ }
+ a++;
+ b++;
+ }
+ if (*a && *b && (!isdigit(*a) || !isdigit(*b)))
+ {
+ if (*a == '~')
+ return -1;
+ if (*b == '~')
+ return 1;
+ return isdigit(*a) ? -1 : 1;
+ }
+
+ char *next_a, *next_b;
+ long int num_a = strtol(a, &next_a, 10);
+ long int num_b = strtol(b, &next_b, 10);
+ if (num_a != num_b)
+ return num_a < num_b ? -1 : 1;
+
+ a = next_a;
+ b = next_b;
+ }
+ if (!*a && !*b)
+ return 0;
+ if (*a)
+ return *a == '~' ? -1 : 1;
+ else
+ return *b == '~' ? 1 : -1;
+}
+
+bool CAddonVersion::operator<(const CAddonVersion& other) const
+{
+ if (mEpoch != other.mEpoch)
+ return mEpoch < other.mEpoch;
+
+ int result = CompareComponent(mUpstream.c_str(), other.mUpstream.c_str());
+ if (result)
+ return (result < 0);
+
+ return (CompareComponent(mRevision.c_str(), other.mRevision.c_str()) < 0);
+}
+
+bool CAddonVersion::operator>(const CAddonVersion& other) const
+{
+ return !(*this <= other);
+}
+
+bool CAddonVersion::operator==(const CAddonVersion& other) const
+{
+ return mEpoch == other.mEpoch &&
+ CompareComponent(mUpstream.c_str(), other.mUpstream.c_str()) == 0 &&
+ CompareComponent(mRevision.c_str(), other.mRevision.c_str()) == 0;
+}
+
+bool CAddonVersion::operator!=(const CAddonVersion& other) const
+{
+ return !(*this == other);
+}
+
+bool CAddonVersion::operator<=(const CAddonVersion& other) const
+{
+ return *this < other || *this == other;
+}
+
+bool CAddonVersion::operator>=(const CAddonVersion& other) const
+{
+ return !(*this < other);
+}
+
+bool CAddonVersion::empty() const
+{
+ return mEpoch == 0 && mUpstream == "0.0.0" && mRevision.empty();
+}
+
+std::string CAddonVersion::asString() const
+{
+ std::string out;
+ if (mEpoch)
+ out = StringUtils::Format("{}:", mEpoch);
+ out += mUpstream;
+ if (!mRevision.empty())
+ out += "-" + mRevision;
+ return out;
+}
+
+bool CAddonVersion::SplitFileName(std::string& ID,
+ std::string& version,
+ const std::string& filename)
+{
+ size_t dpos = filename.rfind('-');
+ if (dpos == std::string::npos)
+ return false;
+ ID = filename.substr(0, dpos);
+ version = filename.substr(dpos + 1);
+ version = version.substr(0, version.size() - 4);
+
+ return true;
+}
+} // namespace ADDON
diff --git a/xbmc/addons/AddonVersion.h b/xbmc/addons/AddonVersion.h
new file mode 100644
index 0000000..78537a5
--- /dev/null
+++ b/xbmc/addons/AddonVersion.h
@@ -0,0 +1,62 @@
+/*
+ * 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 <string>
+
+namespace ADDON
+{
+/* \brief Addon versioning using the debian versioning scheme
+
+ CAddonVersion uses debian versioning, which means in the each section of the period
+ separated version string, numbers are compared numerically rather than lexicographically,
+ thus any preceding zeros are ignored.
+
+ i.e. 1.00 is considered the same as 1.0, and 1.01 is considered the same as 1.1.
+
+ Further, 1.0 < 1.0.0
+
+ See here for more info: http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
+ */
+class CAddonVersion
+{
+public:
+ explicit CAddonVersion(const char* version = nullptr);
+ explicit CAddonVersion(const std::string& version);
+
+ CAddonVersion(const CAddonVersion& other) = default;
+ CAddonVersion(CAddonVersion&& other) = default;
+ CAddonVersion& operator=(const CAddonVersion& other) = default;
+ CAddonVersion& operator=(CAddonVersion&& other) = default;
+
+ virtual ~CAddonVersion() = default;
+
+ int Epoch() const { return mEpoch; }
+ const std::string& Upstream() const { return mUpstream; }
+ const std::string& Revision() const { return mRevision; }
+
+ bool operator<(const CAddonVersion& other) const;
+ bool operator>(const CAddonVersion& other) const;
+ bool operator<=(const CAddonVersion& other) const;
+ bool operator>=(const CAddonVersion& other) const;
+ bool operator==(const CAddonVersion& other) const;
+ bool operator!=(const CAddonVersion& other) const;
+ std::string asString() const;
+ bool empty() const;
+
+ static bool SplitFileName(std::string& ID, std::string& version, const std::string& filename);
+
+protected:
+ int mEpoch;
+ std::string mUpstream;
+ std::string mRevision;
+
+ static int CompareComponent(const char* a, const char* b);
+};
+} // namespace ADDON
diff --git a/xbmc/addons/AudioDecoder.cpp b/xbmc/addons/AudioDecoder.cpp
new file mode 100644
index 0000000..f2531a1
--- /dev/null
+++ b/xbmc/addons/AudioDecoder.cpp
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2013 Arne Morten Kvarving
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "AudioDecoder.h"
+
+#include "FileItem.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/interfaces/AudioEngine.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "filesystem/File.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/tags/TagLoaderTagLib.h"
+#include "utils/Mime.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace ADDON;
+using namespace KODI::ADDONS;
+
+CAudioDecoder::CAudioDecoder(const AddonInfoPtr& addonInfo)
+ : IAddonInstanceHandler(ADDON_INSTANCE_AUDIODECODER, addonInfo)
+{
+ m_CodecName = addonInfo->Type(AddonType::AUDIODECODER)->GetValue("@name").asString();
+ m_strExt = m_CodecName + KODI_ADDON_AUDIODECODER_TRACK_EXT;
+ m_hasTags = addonInfo->Type(AddonType::AUDIODECODER)->GetValue("@tags").asBoolean();
+
+ // Create all interface parts independent to make API changes easier if
+ // something is added
+ m_ifc.audiodecoder = new AddonInstance_AudioDecoder;
+ m_ifc.audiodecoder->toAddon = new KodiToAddonFuncTable_AudioDecoder();
+ m_ifc.audiodecoder->toKodi = new AddonToKodiFuncTable_AudioDecoder();
+ m_ifc.audiodecoder->toKodi->kodiInstance = this;
+}
+
+CAudioDecoder::~CAudioDecoder()
+{
+ DestroyInstance();
+
+ delete m_ifc.audiodecoder->toKodi;
+ delete m_ifc.audiodecoder->toAddon;
+ delete m_ifc.audiodecoder;
+}
+
+bool CAudioDecoder::CreateDecoder()
+{
+ if (CreateInstance() != ADDON_STATUS_OK)
+ return false;
+
+ return true;
+}
+
+bool CAudioDecoder::SupportsFile(const std::string& filename)
+{
+ // Create in case not available, possible as this done by IAddonSupportCheck
+ if ((!m_ifc.hdl && !CreateDecoder()) || !m_ifc.audiodecoder->toAddon->supports_file)
+ return false;
+
+ return m_ifc.audiodecoder->toAddon->supports_file(m_ifc.hdl, filename.c_str());
+}
+
+bool CAudioDecoder::Init(const CFileItem& file, unsigned int filecache)
+{
+ if (!m_ifc.audiodecoder->toAddon->init)
+ return false;
+
+ /// for replaygain
+ /// @todo About audio decoder in most cases Kodi's one not work, add fallback
+ /// to use addon if this fails. Need API change about addons music info tag!
+ CTagLoaderTagLib tag;
+ tag.Load(file.GetDynPath(), XFILE::CMusicFileDirectory::m_tag, nullptr);
+
+ int channels = -1;
+ int sampleRate = -1;
+ AudioEngineDataFormat addonFormat = AUDIOENGINE_FMT_INVALID;
+ AudioEngineChannel channelList[AUDIOENGINE_CH_MAX] = {AUDIOENGINE_CH_NULL};
+
+ bool ret = m_ifc.audiodecoder->toAddon->init(m_ifc.hdl, file.GetDynPath().c_str(), filecache,
+ &channels, &sampleRate, &m_bitsPerSample,
+ &m_TotalTime, &m_bitRate, &addonFormat, channelList);
+ if (ret)
+ {
+ if (channels <= 0 || sampleRate <= 0 || addonFormat == AUDIOENGINE_FMT_INVALID)
+ {
+ CLog::Log(LOGERROR,
+ "CAudioDecoder::{} - Addon '{}' returned true without set of needed values",
+ __func__, ID());
+ return false;
+ }
+
+ m_format.m_dataFormat = Interface_AudioEngine::TranslateAEFormatToKodi(addonFormat);
+ m_format.m_sampleRate = sampleRate;
+ if (channelList[0] != AUDIOENGINE_CH_NULL)
+ {
+ CAEChannelInfo layout;
+ for (const auto& channel : channelList)
+ {
+ if (channel == AUDIOENGINE_CH_NULL)
+ break;
+ layout += Interface_AudioEngine::TranslateAEChannelToKodi(channel);
+ }
+
+ m_format.m_channelLayout = layout;
+ }
+ else
+ m_format.m_channelLayout = CAEUtil::GuessChLayout(channels);
+ }
+
+ return ret;
+}
+
+int CAudioDecoder::ReadPCM(uint8_t* buffer, size_t size, size_t* actualsize)
+{
+ if (!m_ifc.audiodecoder->toAddon->read_pcm)
+ return 0;
+
+ return m_ifc.audiodecoder->toAddon->read_pcm(m_ifc.hdl, buffer, size, actualsize);
+}
+
+bool CAudioDecoder::Seek(int64_t time)
+{
+ if (!m_ifc.audiodecoder->toAddon->seek)
+ return false;
+
+ m_ifc.audiodecoder->toAddon->seek(m_ifc.hdl, time);
+ return true;
+}
+
+bool CAudioDecoder::Load(const std::string& fileName,
+ MUSIC_INFO::CMusicInfoTag& tag,
+ EmbeddedArt* art)
+{
+ if (!m_ifc.audiodecoder->toAddon->read_tag)
+ return false;
+
+ KODI_ADDON_AUDIODECODER_INFO_TAG ifcTag = {};
+ bool ret = m_ifc.audiodecoder->toAddon->read_tag(m_ifc.hdl, fileName.c_str(), &ifcTag);
+ if (ret)
+ {
+ if (ifcTag.title)
+ {
+ tag.SetTitle(ifcTag.title);
+ free(ifcTag.title);
+ }
+ if (ifcTag.artist)
+ {
+ tag.SetArtist(ifcTag.artist);
+ free(ifcTag.artist);
+ }
+ if (ifcTag.album)
+ {
+ tag.SetAlbum(ifcTag.album);
+ free(ifcTag.album);
+ }
+ if (ifcTag.album_artist)
+ {
+ tag.SetAlbumArtist(ifcTag.album_artist);
+ free(ifcTag.album_artist);
+ }
+ if (ifcTag.media_type)
+ {
+ tag.SetType(ifcTag.media_type);
+ free(ifcTag.media_type);
+ }
+ if (ifcTag.genre)
+ {
+ tag.SetGenre(ifcTag.genre);
+ free(ifcTag.genre);
+ }
+ tag.SetDuration(ifcTag.duration);
+ tag.SetTrackNumber(ifcTag.track);
+ tag.SetDiscNumber(ifcTag.disc);
+ if (ifcTag.disc_subtitle)
+ {
+ tag.SetDiscSubtitle(ifcTag.disc_subtitle);
+ free(ifcTag.disc_subtitle);
+ }
+ tag.SetTotalDiscs(ifcTag.disc_total);
+ if (ifcTag.release_date)
+ {
+ tag.SetReleaseDate(ifcTag.release_date);
+ free(ifcTag.release_date);
+ }
+ if (ifcTag.lyrics)
+ {
+ tag.SetLyrics(ifcTag.lyrics);
+ free(ifcTag.lyrics);
+ }
+ tag.SetSampleRate(ifcTag.samplerate);
+ tag.SetNoOfChannels(ifcTag.channels);
+ tag.SetBitRate(ifcTag.bitrate);
+ if (ifcTag.comment)
+ {
+ tag.SetComment(ifcTag.comment);
+ free(ifcTag.comment);
+ }
+
+ if (ifcTag.cover_art_path)
+ {
+ const std::string mimetype =
+ CMime::GetMimeType(URIUtils::GetExtension(ifcTag.cover_art_path));
+ if (StringUtils::StartsWith(mimetype, "image/"))
+ {
+ XFILE::CFile file;
+ std::vector<uint8_t> buf;
+
+ if (file.LoadFile(ifcTag.cover_art_path, buf) > 0)
+ {
+ tag.SetCoverArtInfo(buf.size(), mimetype);
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t*>(buf.data()), buf.size(), mimetype);
+ }
+ }
+ free(ifcTag.cover_art_path);
+ }
+ else if (ifcTag.cover_art_mem_mimetype && ifcTag.cover_art_mem && ifcTag.cover_art_mem_size > 0)
+ {
+ tag.SetCoverArtInfo(ifcTag.cover_art_mem_size, ifcTag.cover_art_mem_mimetype);
+ if (art)
+ art->Set(ifcTag.cover_art_mem, ifcTag.cover_art_mem_size, ifcTag.cover_art_mem_mimetype);
+ }
+
+ if (ifcTag.cover_art_mem_mimetype)
+ free(ifcTag.cover_art_mem_mimetype);
+ if (ifcTag.cover_art_mem)
+ free(ifcTag.cover_art_mem);
+
+ tag.SetLoaded(true);
+ }
+
+ return ret;
+}
+
+int CAudioDecoder::GetTrackCount(const std::string& strPath)
+{
+ if (!m_ifc.audiodecoder->toAddon->track_count)
+ return 0;
+
+ int result = m_ifc.audiodecoder->toAddon->track_count(m_ifc.hdl, strPath.c_str());
+
+ if (result > 1)
+ {
+ if (m_hasTags)
+ {
+ if (!Load(strPath, XFILE::CMusicFileDirectory::m_tag, nullptr))
+ return 0;
+ }
+ else
+ XFILE::CMusicFileDirectory::m_tag.SetTitle(CURL(strPath).GetFileNameWithoutPath());
+ XFILE::CMusicFileDirectory::m_tag.SetLoaded(true);
+ }
+
+ return result;
+}
diff --git a/xbmc/addons/AudioDecoder.h b/xbmc/addons/AudioDecoder.h
new file mode 100644
index 0000000..7e2424e
--- /dev/null
+++ b/xbmc/addons/AudioDecoder.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2013 Arne Morten Kvarving
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/IAddonSupportCheck.h"
+#include "addons/IAddonSupportList.h"
+#include "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/AudioDecoder.h"
+#include "cores/paplayer/ICodec.h"
+#include "filesystem/MusicFileDirectory.h"
+#include "music/tags/ImusicInfoTagLoader.h"
+
+namespace KODI
+{
+namespace ADDONS
+{
+
+class CAudioDecoder : public ADDON::IAddonInstanceHandler,
+ public IAddonSupportCheck,
+ public ICodec,
+ public MUSIC_INFO::IMusicInfoTagLoader,
+ public XFILE::CMusicFileDirectory
+{
+public:
+ explicit CAudioDecoder(const ADDON::AddonInfoPtr& addonInfo);
+ ~CAudioDecoder() override;
+
+ // Things that MUST be supplied by the child classes
+ bool CreateDecoder();
+ bool Init(const CFileItem& file, unsigned int filecache) override;
+ int ReadPCM(uint8_t* buffer, size_t size, size_t* actualsize) override;
+ bool Seek(int64_t time) override;
+ bool CanInit() override { return true; }
+ bool Load(const std::string& strFileName,
+ MUSIC_INFO::CMusicInfoTag& tag,
+ EmbeddedArt* art = nullptr) override;
+ int GetTrackCount(const std::string& strPath) override;
+ bool SupportsFile(const std::string& filename) override;
+
+private:
+ bool m_hasTags;
+};
+
+} /* namespace ADDONS */
+} /* namespace KODI */
diff --git a/xbmc/addons/BinaryAddonCache.cpp b/xbmc/addons/BinaryAddonCache.cpp
new file mode 100644
index 0000000..3005d44
--- /dev/null
+++ b/xbmc/addons/BinaryAddonCache.cpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016-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 "BinaryAddonCache.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+
+#include <mutex>
+
+namespace ADDON
+{
+
+const std::vector<AddonType> ADDONS_TO_CACHE = {AddonType::GAMEDLL};
+
+CBinaryAddonCache::~CBinaryAddonCache()
+{
+ Deinit();
+}
+
+void CBinaryAddonCache::Init()
+{
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CBinaryAddonCache::OnEvent);
+ Update();
+}
+
+void CBinaryAddonCache::Deinit()
+{
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+}
+
+void CBinaryAddonCache::GetAddons(VECADDONS& addons, AddonType type)
+{
+ VECADDONS myAddons;
+ GetInstalledAddons(myAddons, type);
+
+ for (auto &addon : myAddons)
+ {
+ if (!CServiceBroker::GetAddonMgr().IsAddonDisabled(addon->ID()))
+ addons.emplace_back(std::move(addon));
+ }
+}
+
+void CBinaryAddonCache::GetDisabledAddons(VECADDONS& addons, AddonType type)
+{
+ VECADDONS myAddons;
+ GetInstalledAddons(myAddons, type);
+
+ for (auto &addon : myAddons)
+ {
+ if (CServiceBroker::GetAddonMgr().IsAddonDisabled(addon->ID()))
+ addons.emplace_back(std::move(addon));
+ }
+}
+
+void CBinaryAddonCache::GetInstalledAddons(VECADDONS& addons, AddonType type)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto it = m_addons.find(type);
+ if (it != m_addons.end())
+ addons = it->second;
+}
+
+AddonPtr CBinaryAddonCache::GetAddonInstance(const std::string& strId, AddonType type)
+{
+ AddonPtr addon;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ auto it = m_addons.find(type);
+ if (it != m_addons.end())
+ {
+ VECADDONS& addons = it->second;
+ auto itAddon = std::find_if(addons.begin(), addons.end(),
+ [&strId](const AddonPtr& addon)
+ {
+ return addon->ID() == strId;
+ });
+
+ if (itAddon != addons.end())
+ addon = *itAddon;
+ }
+
+ return addon;
+}
+
+void CBinaryAddonCache::OnEvent(const AddonEvent& event)
+{
+ if (typeid(event) == typeid(AddonEvents::Enabled) ||
+ typeid(event) == typeid(AddonEvents::Disabled) ||
+ typeid(event) == typeid(AddonEvents::ReInstalled))
+ {
+ for (auto &type : ADDONS_TO_CACHE)
+ {
+ if (CServiceBroker::GetAddonMgr().HasType(event.addonId, type))
+ {
+ Update();
+ break;
+ }
+ }
+ }
+ else if (typeid(event) == typeid(AddonEvents::UnInstalled))
+ {
+ Update();
+ }
+}
+
+void CBinaryAddonCache::Update()
+{
+ using AddonMap = std::multimap<AddonType, VECADDONS>;
+ AddonMap addonmap;
+
+ for (auto &addonType : ADDONS_TO_CACHE)
+ {
+ VECADDONS addons;
+ CServiceBroker::GetAddonMgr().GetInstalledAddons(addons, addonType);
+ addonmap.insert(AddonMap::value_type(addonType, addons));
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_addons = std::move(addonmap);
+ }
+}
+
+}
diff --git a/xbmc/addons/BinaryAddonCache.h b/xbmc/addons/BinaryAddonCache.h
new file mode 100644
index 0000000..5536c9f
--- /dev/null
+++ b/xbmc/addons/BinaryAddonCache.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016-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 <map>
+#include <memory>
+#include <vector>
+
+namespace ADDON
+{
+
+enum class AddonType;
+
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+using VECADDONS = std::vector<AddonPtr>;
+
+struct AddonEvent;
+
+class CBinaryAddonCache
+{
+public:
+ virtual ~CBinaryAddonCache();
+ void Init();
+ void Deinit();
+ void GetAddons(VECADDONS& addons, AddonType type);
+ void GetDisabledAddons(VECADDONS& addons, AddonType type);
+ void GetInstalledAddons(VECADDONS& addons, AddonType type);
+ AddonPtr GetAddonInstance(const std::string& strId, AddonType type);
+
+protected:
+ void Update();
+ void OnEvent(const AddonEvent& event);
+
+ CCriticalSection m_critSection;
+ std::multimap<AddonType, VECADDONS> m_addons;
+};
+
+} // namespace ADDON
diff --git a/xbmc/addons/CMakeLists.txt b/xbmc/addons/CMakeLists.txt
new file mode 100644
index 0000000..947a885
--- /dev/null
+++ b/xbmc/addons/CMakeLists.txt
@@ -0,0 +1,74 @@
+set(SOURCES Addon.cpp
+ AddonBuilder.cpp
+ BinaryAddonCache.cpp
+ AddonDatabase.cpp
+ AddonInstaller.cpp
+ AddonManager.cpp
+ AddonRepos.cpp
+ AddonStatusHandler.cpp
+ AddonSystemSettings.cpp
+ AddonUpdateRules.cpp
+ AddonVersion.cpp
+ AudioDecoder.cpp
+ ContextMenuAddon.cpp
+ ContextMenus.cpp
+ ExtsMimeSupportList.cpp
+ FontResource.cpp
+ FilesystemInstaller.cpp
+ GameResource.cpp
+ ImageDecoder.cpp
+ ImageResource.cpp
+ LanguageResource.cpp
+ PluginSource.cpp
+ Repository.cpp
+ RepositoryUpdater.cpp
+ Scraper.cpp
+ ScreenSaver.cpp
+ Service.cpp
+ Skin.cpp
+ UISoundsResource.cpp
+ VFSEntry.cpp
+ Visualization.cpp
+ Webinterface.cpp)
+
+set(HEADERS Addon.h
+ AddonBuilder.h
+ AddonEvents.h
+ AddonRepoInfo.h
+ BinaryAddonCache.h
+ AddonDatabase.h
+ AddonInstaller.h
+ AddonManager.h
+ AddonProvider.h
+ AddonRepos.h
+ AddonStatusHandler.h
+ AddonSystemSettings.h
+ AddonUpdateRules.h
+ AddonVersion.h
+ AudioDecoder.h
+ ContextMenuAddon.h
+ ContextMenus.h
+ ExtsMimeSupportList.h
+ FilesystemInstaller.h
+ FontResource.h
+ GameResource.h
+ IAddon.h
+ IAddonManagerCallback.h
+ IAddonSupportCheck.h
+ ImageDecoder.h
+ ImageResource.h
+ LanguageResource.h
+ PluginSource.h
+ Repository.h
+ RepositoryUpdater.h
+ Resource.h
+ Scraper.h
+ ScreenSaver.h
+ Service.h
+ Skin.h
+ UISoundsResource.h
+ VFSEntry.h
+ Visualization.h
+ Webinterface.h)
+
+core_add_library(addons)
diff --git a/xbmc/addons/ContextMenuAddon.cpp b/xbmc/addons/ContextMenuAddon.cpp
new file mode 100644
index 0000000..1717580
--- /dev/null
+++ b/xbmc/addons/ContextMenuAddon.cpp
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2013-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 "ContextMenuAddon.h"
+
+#include "ContextMenuItem.h"
+#include "ContextMenuManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <sstream>
+
+namespace ADDON
+{
+
+CContextMenuAddon::CContextMenuAddon(const AddonInfoPtr& addonInfo)
+ : CAddon(addonInfo, AddonType::CONTEXTMENU_ITEM)
+{
+ const CAddonExtensions* menu = Type(AddonType::CONTEXTMENU_ITEM)->GetElement("menu");
+ if (menu)
+ {
+ int tmp = 0;
+ ParseMenu(menu, "", tmp);
+ }
+ else
+ {
+ //backwards compatibility. add first item definition
+ const CAddonExtensions* elem = Type(AddonType::CONTEXTMENU_ITEM)->GetElement("item");
+ if (elem)
+ {
+ std::string visCondition = elem->GetValue("visible").asString();
+ if (visCondition.empty())
+ visCondition = "false";
+
+ std::string parent = elem->GetValue("parent").asString() == "kodi.core.manage"
+ ? CContextMenuManager::MANAGE.m_groupId : CContextMenuManager::MAIN.m_groupId;
+
+ auto label = elem->GetValue("label").asString();
+ if (StringUtils::IsNaturalNumber(label))
+ label = g_localizeStrings.GetAddonString(ID(), atoi(label.c_str()));
+
+ CContextMenuItem menuItem = CContextMenuItem::CreateItem(
+ label, parent,
+ URIUtils::AddFileToFolder(Path(), Type(AddonType::CONTEXTMENU_ITEM)->LibName()),
+ visCondition, ID());
+
+ m_items.push_back(menuItem);
+ }
+ }
+}
+
+CContextMenuAddon::~CContextMenuAddon() = default;
+
+void CContextMenuAddon::ParseMenu(
+ const CAddonExtensions* elem,
+ const std::string& parent,
+ int& anonGroupCount)
+{
+ auto menuId = elem->GetValue("@id").asString();
+ auto menuLabel = elem->GetValue("label").asString();
+ if (StringUtils::IsNaturalNumber(menuLabel))
+ menuLabel = g_localizeStrings.GetAddonString(ID(), std::stoi(menuLabel));
+
+ if (menuId.empty())
+ {
+ //anonymous group. create a new unique internal id.
+ std::stringstream ss;
+ ss << ID() << ++anonGroupCount;
+ menuId = ss.str();
+ }
+
+ m_items.push_back(CContextMenuItem::CreateGroup(menuLabel, parent, menuId, ID()));
+
+ for (const auto& subMenu : elem->GetElements("menu"))
+ ParseMenu(&subMenu.second, menuId, anonGroupCount);
+
+ for (const auto& element : elem->GetElements("item"))
+ {
+ std::string visCondition = element.second.GetValue("visible").asString();
+ std::string library = element.second.GetValue("@library").asString();
+ std::string label = element.second.GetValue("label").asString();
+ if (StringUtils::IsNaturalNumber(label))
+ label = g_localizeStrings.GetAddonString(ID(), atoi(label.c_str()));
+
+ std::vector<std::string> args;
+ args.push_back(ID());
+
+ std::string arg = element.second.GetValue("@args").asString();
+ if (!arg.empty())
+ args.push_back(arg);
+
+ if (!label.empty() && !library.empty() && !visCondition.empty())
+ {
+ auto menu = CContextMenuItem::CreateItem(label, menuId,
+ URIUtils::AddFileToFolder(Path(), library), visCondition, ID(), args);
+ m_items.push_back(menu);
+ }
+ }
+}
+
+}
diff --git a/xbmc/addons/ContextMenuAddon.h b/xbmc/addons/ContextMenuAddon.h
new file mode 100644
index 0000000..74221fd
--- /dev/null
+++ b/xbmc/addons/ContextMenuAddon.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013-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 "addons/Addon.h"
+
+#include <memory>
+#include <vector>
+
+class CContextMenuItem;
+
+namespace ADDON
+{
+class CAddonExtensions;
+class CAddonInfo;
+using AddonInfoPtr = std::shared_ptr<CAddonInfo>;
+
+class CContextMenuAddon : public CAddon
+{
+public:
+ explicit CContextMenuAddon(const AddonInfoPtr& addonInfo);
+ ~CContextMenuAddon() override;
+
+ const std::vector<CContextMenuItem>& GetItems() const { return m_items; }
+
+private:
+ void ParseMenu(const CAddonExtensions* elem, const std::string& parent, int& anonGroupCount);
+ std::vector<CContextMenuItem> m_items;
+};
+}
diff --git a/xbmc/addons/ContextMenus.cpp b/xbmc/addons/ContextMenus.cpp
new file mode 100644
index 0000000..23f5036
--- /dev/null
+++ b/xbmc/addons/ContextMenus.cpp
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenus.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/Repository.h"
+#include "addons/RepositoryUpdater.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonInfo.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "addons/gui/GUIHelpers.h"
+
+namespace CONTEXTMENU
+{
+
+using namespace ADDON;
+
+bool CAddonInfo::IsVisible(const CFileItem& item) const
+{
+ return item.HasAddonInfo();
+}
+
+bool CAddonInfo::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CGUIDialogAddonInfo::ShowForItem(item);
+}
+
+bool CAddonSettings::IsVisible(const CFileItem& item) const
+{
+ AddonPtr addon;
+ return item.HasAddonInfo() &&
+ CServiceBroker::GetAddonMgr().GetAddon(item.GetAddonInfo()->ID(), addon,
+ OnlyEnabled::CHOICE_NO) &&
+ addon->CanHaveAddonOrInstanceSettings();
+}
+
+bool CAddonSettings::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ AddonPtr addon;
+ return CServiceBroker::GetAddonMgr().GetAddon(item->GetAddonInfo()->ID(), addon,
+ OnlyEnabled::CHOICE_NO) &&
+ CGUIDialogAddonSettings::ShowForAddon(addon);
+}
+
+bool CCheckForUpdates::IsVisible(const CFileItem& item) const
+{
+ return item.HasAddonInfo() && item.GetAddonInfo()->Type() == AddonType::REPOSITORY;
+}
+
+bool CCheckForUpdates::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ AddonPtr addon;
+ if (item->HasAddonInfo() &&
+ CServiceBroker::GetAddonMgr().GetAddon(item->GetAddonInfo()->ID(), addon,
+ AddonType::REPOSITORY, OnlyEnabled::CHOICE_YES))
+ {
+ CServiceBroker::GetRepositoryUpdater().CheckForUpdates(std::static_pointer_cast<CRepository>(addon), true);
+ return true;
+ }
+ return false;
+}
+
+
+bool CEnableAddon::IsVisible(const CFileItem& item) const
+{
+ return item.HasAddonInfo() &&
+ CServiceBroker::GetAddonMgr().IsAddonDisabled(item.GetAddonInfo()->ID()) &&
+ CServiceBroker::GetAddonMgr().CanAddonBeEnabled(item.GetAddonInfo()->ID());
+}
+
+bool CEnableAddon::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ // Check user want to enable if lifecycle not normal
+ if (!ADDON::GUI::CHelpers::DialogAddonLifecycleUseAsk(item->GetAddonInfo()))
+ return false;
+
+ return CServiceBroker::GetAddonMgr().EnableAddon(item->GetAddonInfo()->ID());
+}
+
+bool CDisableAddon::IsVisible(const CFileItem& item) const
+{
+ return item.HasAddonInfo() &&
+ !CServiceBroker::GetAddonMgr().IsAddonDisabled(item.GetAddonInfo()->ID()) &&
+ CServiceBroker::GetAddonMgr().CanAddonBeDisabled(item.GetAddonInfo()->ID());
+}
+
+bool CDisableAddon::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetAddonMgr().DisableAddon(item->GetAddonInfo()->ID(),
+ AddonDisabledReason::USER);
+}
+}
diff --git a/xbmc/addons/ContextMenus.h b/xbmc/addons/ContextMenus.h
new file mode 100644
index 0000000..9a87c1d
--- /dev/null
+++ b/xbmc/addons/ContextMenus.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenuItem.h"
+
+#include <memory>
+
+class CFileItem;
+
+namespace CONTEXTMENU
+{
+
+struct CAddonInfo : CStaticContextMenuAction
+{
+ CAddonInfo() : CStaticContextMenuAction(19033) {}
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CAddonSettings : CStaticContextMenuAction
+{
+ CAddonSettings() : CStaticContextMenuAction(10004) {}
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CCheckForUpdates : CStaticContextMenuAction
+{
+ CCheckForUpdates() : CStaticContextMenuAction(24034) {}
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CEnableAddon : CStaticContextMenuAction
+{
+ CEnableAddon() : CStaticContextMenuAction(24022) {}
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CDisableAddon : CStaticContextMenuAction
+{
+ CDisableAddon() : CStaticContextMenuAction(24021) {}
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+}
diff --git a/xbmc/addons/ExtsMimeSupportList.cpp b/xbmc/addons/ExtsMimeSupportList.cpp
new file mode 100644
index 0000000..88c6d49
--- /dev/null
+++ b/xbmc/addons/ExtsMimeSupportList.cpp
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2013 Arne Morten Kvarving
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ExtsMimeSupportList.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/AudioDecoder.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace ADDON;
+using namespace KODI::ADDONS;
+
+CExtsMimeSupportList::CExtsMimeSupportList(CAddonMgr& addonMgr) : m_addonMgr(addonMgr)
+{
+ m_addonMgr.Events().Subscribe(this, &CExtsMimeSupportList::OnEvent);
+
+ // Load all available audio decoder addons during Kodi start
+ const std::vector<AddonType> types = {AddonType::AUDIODECODER, AddonType::IMAGEDECODER};
+ const auto addonInfos = m_addonMgr.GetAddonInfos(true, types);
+ for (const auto& addonInfo : addonInfos)
+ m_supportedList.emplace_back(ScanAddonProperties(addonInfo->MainType(), addonInfo));
+}
+
+CExtsMimeSupportList::~CExtsMimeSupportList()
+{
+ m_addonMgr.Events().Unsubscribe(this);
+}
+
+void CExtsMimeSupportList::OnEvent(const AddonEvent& event)
+{
+ if (typeid(event) == typeid(AddonEvents::Enabled) ||
+ typeid(event) == typeid(AddonEvents::Disabled) ||
+ typeid(event) == typeid(AddonEvents::ReInstalled))
+ {
+ if (m_addonMgr.HasType(event.addonId, AddonType::AUDIODECODER) ||
+ m_addonMgr.HasType(event.addonId, AddonType::IMAGEDECODER))
+ Update(event.addonId);
+ }
+ else if (typeid(event) == typeid(AddonEvents::UnInstalled))
+ {
+ Update(event.addonId);
+ }
+}
+
+void CExtsMimeSupportList::Update(const std::string& id)
+{
+ // Stop used instance if present, otherwise the new becomes created on already created addon base one.
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const auto itAddon =
+ std::find_if(m_supportedList.begin(), m_supportedList.end(),
+ [&id](const SupportValues& addon) { return addon.m_addonInfo->ID() == id; });
+
+ if (itAddon != m_supportedList.end())
+ {
+ m_supportedList.erase(itAddon);
+ }
+ }
+
+ // Create and init the new addon instance
+ std::shared_ptr<CAddonInfo> addonInfo = m_addonMgr.GetAddonInfo(id, AddonType::UNKNOWN);
+ if (addonInfo && !m_addonMgr.IsAddonDisabled(id))
+ {
+ if (addonInfo->HasType(AddonType::AUDIODECODER) || addonInfo->HasType(AddonType::IMAGEDECODER))
+ {
+ SupportValues values = ScanAddonProperties(addonInfo->MainType(), addonInfo);
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_supportedList.emplace_back(values);
+ }
+ }
+ }
+}
+
+CExtsMimeSupportList::SupportValues CExtsMimeSupportList::ScanAddonProperties(
+ AddonType type, const std::shared_ptr<CAddonInfo>& addonInfo)
+{
+ SupportValues values;
+
+ values.m_addonType = type;
+ values.m_addonInfo = addonInfo;
+ if (type == AddonType::AUDIODECODER)
+ {
+ values.m_codecName = addonInfo->Type(type)->GetValue("@name").asString();
+ values.m_hasTags = addonInfo->Type(type)->GetValue("@tags").asBoolean();
+ values.m_hasTracks = addonInfo->Type(type)->GetValue("@tracks").asBoolean();
+ }
+
+ const auto support = addonInfo->Type(type)->GetElement("support");
+ if (support)
+ {
+ // Scan here about complete defined xml groups with description and maybe icon
+ // e.g.
+ // `<extension name=".zwdsp">`
+ // ` <description>30246</description>`
+ // `</extension>`
+ for (const auto& i : support->GetElements())
+ {
+ std::string name = i.second.GetValue("@name").asString();
+ if (name.empty())
+ continue;
+
+ const int description = i.second.GetValue("description").asInteger();
+ const std::string icon =
+ !i.second.GetValue("icon").empty()
+ ? URIUtils::AddFileToFolder(addonInfo->Path(), i.second.GetValue("icon").asString())
+ : "";
+
+ if (i.first == "extension")
+ {
+ if (name[0] != '.')
+ name.insert(name.begin(), '.');
+ values.m_supportedExtensions.emplace(name, SupportValue(description, icon));
+ }
+ else if (i.first == "mimetype")
+ {
+ values.m_supportedMimetypes.emplace(name, SupportValue(description, icon));
+ const std::string extension = i.second.GetValue("extension").asString();
+ if (!extension.empty())
+ values.m_supportedExtensions.emplace(extension, SupportValue(description, icon));
+ }
+ }
+
+ // Scan here about small defined xml groups without anything
+ // e.g. `<extension name=".adp"/>`
+ for (const auto& i : support->GetValues())
+ {
+ for (const auto& j : i.second)
+ {
+ std::string name = j.second.asString();
+ if (j.first == "extension@name")
+ {
+ if (name[0] != '.')
+ name.insert(name.begin(), '.');
+ values.m_supportedExtensions.emplace(name, SupportValue(-1, ""));
+ }
+ else if (j.first == "mimetype@name")
+ values.m_supportedMimetypes.emplace(name, SupportValue(-1, ""));
+ }
+ }
+ }
+
+ // Check addons support tracks, if yes add extension about related entry names
+ // By them addon no more need to add itself on his addon.xml
+ if (values.m_hasTracks)
+ values.m_supportedExtensions.emplace(
+ "." + values.m_codecName + KODI_ADDON_AUDIODECODER_TRACK_EXT, SupportValue(-1, ""));
+
+
+ return values;
+}
+
+std::vector<CExtsMimeSupportList::SupportValues> CExtsMimeSupportList::GetSupportedAddonInfos(
+ FilterSelect select)
+{
+ std::vector<SupportValues> addonInfos;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& entry : m_supportedList)
+ {
+ if (select == FilterSelect::all || (select == FilterSelect::hasTags && entry.m_hasTags) ||
+ (select == FilterSelect::hasTracks && entry.m_hasTracks))
+ addonInfos.emplace_back(entry);
+ }
+
+ return addonInfos;
+}
+
+bool CExtsMimeSupportList::IsExtensionSupported(const std::string& ext)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& entry : m_supportedList)
+ {
+ const auto it = std::find_if(
+ entry.m_supportedExtensions.begin(), entry.m_supportedExtensions.end(),
+ [ext](const std::pair<std::string, SupportValue>& v) { return v.first == ext; });
+ if (it != entry.m_supportedExtensions.end())
+ return true;
+ }
+
+ return false;
+}
+
+std::vector<std::pair<AddonType, std::shared_ptr<ADDON::CAddonInfo>>> CExtsMimeSupportList::
+ GetExtensionSupportedAddonInfos(const std::string& ext, FilterSelect select)
+{
+ std::vector<std::pair<AddonType, std::shared_ptr<CAddonInfo>>> addonInfos;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& entry : m_supportedList)
+ {
+ const auto it = std::find_if(
+ entry.m_supportedExtensions.begin(), entry.m_supportedExtensions.end(),
+ [ext](const std::pair<std::string, SupportValue>& v) { return v.first == ext; });
+ if (it != entry.m_supportedExtensions.end() &&
+ (select == FilterSelect::all || (select == FilterSelect::hasTags && entry.m_hasTags) ||
+ (select == FilterSelect::hasTracks && entry.m_hasTracks)))
+ addonInfos.emplace_back(entry.m_addonType, entry.m_addonInfo);
+ }
+
+ return addonInfos;
+}
+
+bool CExtsMimeSupportList::IsMimetypeSupported(const std::string& mimetype)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& entry : m_supportedList)
+ {
+
+ const auto it = std::find_if(
+ entry.m_supportedMimetypes.begin(), entry.m_supportedMimetypes.end(),
+ [mimetype](const std::pair<std::string, SupportValue>& v) { return v.first == mimetype; });
+ if (it != entry.m_supportedMimetypes.end())
+ return true;
+ }
+
+ return false;
+}
+
+std::vector<std::pair<AddonType, std::shared_ptr<CAddonInfo>>> CExtsMimeSupportList::
+ GetMimetypeSupportedAddonInfos(const std::string& mimetype, FilterSelect select)
+{
+ std::vector<std::pair<AddonType, std::shared_ptr<CAddonInfo>>> addonInfos;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& entry : m_supportedList)
+ {
+ const auto it = std::find_if(
+ entry.m_supportedMimetypes.begin(), entry.m_supportedMimetypes.end(),
+ [mimetype](const std::pair<std::string, SupportValue>& v) { return v.first == mimetype; });
+ if (it != entry.m_supportedMimetypes.end() &&
+ (select == FilterSelect::all || (select == FilterSelect::hasTags && entry.m_hasTags) ||
+ (select == FilterSelect::hasTracks && entry.m_hasTracks)))
+ addonInfos.emplace_back(entry.m_addonType, entry.m_addonInfo);
+ }
+
+ return addonInfos;
+}
+
+std::vector<AddonSupportEntry> CExtsMimeSupportList::GetSupportedExtsAndMimeTypes(
+ const std::string& addonId)
+{
+ std::vector<AddonSupportEntry> list;
+
+ const auto it =
+ std::find_if(m_supportedList.begin(), m_supportedList.end(),
+ [addonId](const SupportValues& v) { return v.m_addonInfo->ID() == addonId; });
+ if (it == m_supportedList.end())
+ return list;
+
+ for (const auto& entry : it->m_supportedExtensions)
+ {
+ AddonSupportEntry supportEntry;
+ supportEntry.m_type = AddonSupportType::Extension;
+ supportEntry.m_name = entry.first;
+ supportEntry.m_description =
+ g_localizeStrings.GetAddonString(addonId, entry.second.m_description);
+ supportEntry.m_icon = entry.second.m_icon;
+ list.emplace_back(supportEntry);
+ }
+ for (const auto& entry : it->m_supportedMimetypes)
+ {
+ AddonSupportEntry supportEntry;
+ supportEntry.m_type = AddonSupportType::Mimetype;
+ supportEntry.m_name = entry.first;
+ supportEntry.m_description =
+ g_localizeStrings.GetAddonString(addonId, entry.second.m_description);
+ supportEntry.m_icon = entry.second.m_icon;
+ list.emplace_back(supportEntry);
+ }
+
+ return list;
+}
diff --git a/xbmc/addons/ExtsMimeSupportList.h b/xbmc/addons/ExtsMimeSupportList.h
new file mode 100644
index 0000000..f7e8d47
--- /dev/null
+++ b/xbmc/addons/ExtsMimeSupportList.h
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2005-2021 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 "addons/IAddonSupportList.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace ADDON
+{
+enum class AddonType;
+class CAddonMgr;
+class CAddonInfo;
+
+struct AddonEvent;
+}
+
+namespace KODI
+{
+namespace ADDONS
+{
+
+/*!
+ * @brief Class to manage all available and activated add-ons and
+ * to release their types to the outside for selection.
+ *
+ * @note It may also make sense in the future to expand this class and design it
+ * globally in Kodi so that other addons can also be managed with it (all which
+ * have mimetypes and file extensions, e.g. vfs, audioencoder).
+ */
+class CExtsMimeSupportList : public KODI::ADDONS::IAddonSupportList
+{
+public:
+ CExtsMimeSupportList(ADDON::CAddonMgr& addonMgr);
+ ~CExtsMimeSupportList();
+
+ /*!
+ * @brief Filter selection values.
+ */
+ enum class FilterSelect
+ {
+ /*! To select all available */
+ all,
+
+ /*! To select only them where support tags */
+ hasTags,
+
+ /*! Get only where support tracks within */
+ hasTracks
+ };
+
+ /*!
+ * @brief Structure to store information about supported part.
+ */
+ struct SupportValue
+ {
+ SupportValue(int description, std::string icon)
+ : m_description(description), m_icon(std::move(icon))
+ {
+ }
+
+ // Description text about supported part
+ int m_description;
+
+ // Own by addon defined icon path
+ std::string m_icon;
+ };
+
+ /*!
+ * @brief Structure to store available data for related addon.
+ */
+ struct SupportValues
+ {
+ // Type of stored addon to check on scan
+ ADDON::AddonType m_addonType{};
+
+ // Related addon info class
+ std::shared_ptr<ADDON::CAddonInfo> m_addonInfo;
+
+ // Addon his own codec identification name
+ std::string m_codecName;
+
+ // If as true support addons own song information tags
+ bool m_hasTags{false};
+
+ // To know addon includes several tracks inside one file, if set to true
+ bool m_hasTracks{false};
+
+ // List of supported file extensions by addon
+ // First: Extension name
+ // Second: With @ref SupportValue defined content
+ std::map<std::string, SupportValue> m_supportedExtensions;
+
+ // List of supported mimetypes by addon
+ // First: Mimetype name
+ // Second: With @ref SupportValue defined content
+ std::map<std::string, SupportValue> m_supportedMimetypes;
+ };
+
+ /*!
+ * @brief Get list of all by audiodecoder supported parts.
+ *
+ * Thought to use on planned new window about management about supported
+ * extensions and mimetypes in Kodi and to allow edit by user to enable or
+ * disable corresponding parts.
+ *
+ * This function is also used to notify Kodi supported formats and to allow
+ * playback.
+ *
+ * @param[in] select To filter the listed information by type
+ * @return List of the available types listed for the respective add-on
+ */
+ std::vector<SupportValues> GetSupportedAddonInfos(FilterSelect select);
+
+ /*!
+ * @brief To query the desired file extension is supported in it.
+ *
+ * @param[in] ext Extension name to check
+ * @return True if within supported, false if not
+ */
+ bool IsExtensionSupported(const std::string& ext);
+
+ /*!
+ * @brief To get a list of all compatible audio decoder add-ons for the file extension.
+ *
+ * @param[in] ext Extension name to check
+ * @param[in] select To filter the listed information by type
+ * @return List of @ref ADDON::CAddonInfo where support related extension
+ */
+ std::vector<std::pair<ADDON::AddonType, std::shared_ptr<ADDON::CAddonInfo>>>
+ GetExtensionSupportedAddonInfos(const std::string& ext, FilterSelect select);
+
+ /*!
+ * @brief To query the desired file mimetype is supported in it.
+ *
+ * @param[in] mimetype Mimetype name to check
+ * @return True if within supported, false if not
+ */
+ bool IsMimetypeSupported(const std::string& mimetype);
+
+ /*!
+ * @brief To get a list of all compatible audio decoder add-ons for the mimetype.
+ *
+ * @param[in] mimetype Mimetype name to check
+ * @param[in] select To filter the listed information by type
+ * @return List of @ref ADDON::CAddonInfo where support related mimetype
+ */
+ std::vector<std::pair<ADDON::AddonType, std::shared_ptr<ADDON::CAddonInfo>>>
+ GetMimetypeSupportedAddonInfos(const std::string& mimetype, FilterSelect select);
+
+ /*!
+ * @brief To give all file extensions and MIME types supported by the addon.
+ *
+ * @param[in] addonId Identifier about wanted addon
+ * @return List of all supported parts on selected addon
+ *
+ * @sa KODI::ADDONS::IAddonSupportList
+ */
+ std::vector<KODI::ADDONS::AddonSupportEntry> GetSupportedExtsAndMimeTypes(
+ const std::string& addonId) override;
+
+protected:
+ void Update(const std::string& id);
+ void OnEvent(const ADDON::AddonEvent& event);
+
+ static SupportValues ScanAddonProperties(ADDON::AddonType type,
+ const std::shared_ptr<ADDON::CAddonInfo>& addonInfo);
+
+ CCriticalSection m_critSection;
+
+ std::vector<SupportValues> m_supportedList;
+ ADDON::CAddonMgr& m_addonMgr;
+};
+
+} /* namespace ADDONS */
+} /* namespace KODI */
diff --git a/xbmc/addons/FilesystemInstaller.cpp b/xbmc/addons/FilesystemInstaller.cpp
new file mode 100644
index 0000000..0f7b32d
--- /dev/null
+++ b/xbmc/addons/FilesystemInstaller.cpp
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2016-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 "FilesystemInstaller.h"
+
+#include "FileItem.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/FileOperationJob.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+CFilesystemInstaller::CFilesystemInstaller()
+ : m_addonFolder(CSpecialProtocol::TranslatePath("special://home/addons/")),
+ m_tempFolder(CSpecialProtocol::TranslatePath("special://home/addons/temp/"))
+{
+}
+
+bool CFilesystemInstaller::InstallToFilesystem(const std::string& archive, const std::string& addonId)
+{
+ auto addonFolder = URIUtils::AddFileToFolder(m_addonFolder, addonId);
+ auto newAddonData = URIUtils::AddFileToFolder(m_tempFolder, StringUtils::CreateUUID());
+ auto oldAddonData = URIUtils::AddFileToFolder(m_tempFolder, StringUtils::CreateUUID());
+
+ if (!CDirectory::Create(newAddonData))
+ return false;
+
+ if (!UnpackArchive(archive, newAddonData))
+ {
+ CLog::Log(LOGERROR, "Failed to unpack archive '{}' to '{}'", archive, newAddonData);
+ return false;
+ }
+
+ bool hasOldData = CDirectory::Exists(addonFolder);
+ if (hasOldData)
+ {
+ if (!CFile::Rename(addonFolder, oldAddonData))
+ {
+ CLog::Log(LOGERROR, "Failed to move old addon files from '{}' to '{}'", addonFolder,
+ oldAddonData);
+ return false;
+ }
+ }
+
+ if (!CFile::Rename(newAddonData, addonFolder))
+ {
+ CLog::Log(LOGERROR, "Failed to move new addon files from '{}' to '{}'", newAddonData,
+ addonFolder);
+ return false;
+ }
+
+ if (hasOldData)
+ {
+ if (!CDirectory::RemoveRecursive(oldAddonData))
+ {
+ CLog::Log(LOGWARNING, "Failed to delete old addon files in '{}'", oldAddonData);
+ }
+ }
+ return true;
+}
+
+bool CFilesystemInstaller::UnInstallFromFilesystem(const std::string& addonFolder)
+{
+ auto tempFolder = URIUtils::AddFileToFolder(m_tempFolder, StringUtils::CreateUUID());
+ if (!CFile::Rename(addonFolder, tempFolder))
+ {
+ CLog::Log(LOGERROR, "Failed to move old addon files from '{}' to '{}'", addonFolder,
+ tempFolder);
+ return false;
+ }
+
+ if (!CDirectory::RemoveRecursive(tempFolder))
+ {
+ CLog::Log(LOGWARNING, "Failed to delete old addon files in '{}'", tempFolder);
+ }
+ return true;
+}
+
+bool CFilesystemInstaller::UnpackArchive(std::string path, const std::string& dest)
+{
+ if (!URIUtils::IsProtocol(path, "zip"))
+ path = URIUtils::CreateArchivePath("zip", CURL(path), "").Get();
+
+ CFileItemList files;
+ if (!CDirectory::GetDirectory(path, files, "", DIR_FLAG_DEFAULTS))
+ return false;
+
+ if (files.Size() == 1 && files[0]->m_bIsFolder)
+ {
+ path = files[0]->GetPath();
+ files.Clear();
+ if (!CDirectory::GetDirectory(path, files, "", DIR_FLAG_DEFAULTS))
+ return false;
+ }
+ CLog::Log(LOGDEBUG, "Unpacking {} to {}", path, dest);
+
+ for (auto i = 0; i < files.Size(); ++i)
+ files[i]->Select(true);
+
+ CFileOperationJob job(CFileOperationJob::ActionCopy, files, dest);
+ return job.DoWork();
+}
diff --git a/xbmc/addons/FilesystemInstaller.h b/xbmc/addons/FilesystemInstaller.h
new file mode 100644
index 0000000..d0bfdcd
--- /dev/null
+++ b/xbmc/addons/FilesystemInstaller.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016-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 <string>
+
+/*!
+ * Responsible for safely unpacking/copying/removing addon files from/to the addon folder.
+ */
+class CFilesystemInstaller
+{
+public:
+
+ CFilesystemInstaller();
+
+ /*!
+ * @param archive Absolute path to zip file to install.
+ * @param addonId
+ * @return true on success, otherwise false.
+ */
+ bool InstallToFilesystem(const std::string& archive, const std::string& addonId);
+
+ bool UnInstallFromFilesystem(const std::string& addonPath);
+
+private:
+ static bool UnpackArchive(std::string path, const std::string& dest);
+
+ std::string m_addonFolder;
+ std::string m_tempFolder;
+};
diff --git a/xbmc/addons/FontResource.cpp b/xbmc/addons/FontResource.cpp
new file mode 100644
index 0000000..0ed843b
--- /dev/null
+++ b/xbmc/addons/FontResource.cpp
@@ -0,0 +1,51 @@
+/*
+ * 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 "FontResource.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/SpecialProtocol.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+
+namespace ADDON
+{
+
+CFontResource::CFontResource(const AddonInfoPtr& addonInfo)
+ : CResource(addonInfo, AddonType::RESOURCE_FONT)
+{
+}
+
+void CFontResource::OnPostInstall(bool update, bool modal)
+{
+ std::string skin = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN);
+ const auto& deps =
+ CServiceBroker::GetAddonMgr().GetDepsRecursive(skin, OnlyEnabledRootAddon::CHOICE_YES);
+ for (const auto& it : deps)
+ if (it.id == ID())
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ "ReloadSkin");
+}
+
+bool CFontResource::GetFont(const std::string& file, std::string& path) const
+{
+ std::string result = CSpecialProtocol::TranslatePathConvertCase(Path()+"/resources/"+file);
+ if (CFileUtils::Exists(result))
+ {
+ path = result;
+ return true;
+ }
+
+ return false;
+}
+
+}
diff --git a/xbmc/addons/FontResource.h b/xbmc/addons/FontResource.h
new file mode 100644
index 0000000..350090d
--- /dev/null
+++ b/xbmc/addons/FontResource.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014-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 "addons/Resource.h"
+
+#include <memory>
+
+namespace ADDON
+{
+class CFontResource : public CResource
+{
+public:
+ explicit CFontResource(const AddonInfoPtr& addonInfo);
+
+ //! \brief Check whether file is allowed or not (no filters here).
+ bool IsAllowed(const std::string& file) const override { return true; }
+
+ //! \brief Get the font path if given font file is served by the add-on.
+ //! \param[in] file File name of font.
+ //! \param[out] path Full path to font if found.
+ //! \return True if font was found, false otherwise.
+ bool GetFont(const std::string& file, std::string& path) const;
+
+ //! \brief Callback executed after installation
+ void OnPostInstall(bool update, bool modal) override;
+};
+
+}
diff --git a/xbmc/addons/GameResource.cpp b/xbmc/addons/GameResource.cpp
new file mode 100644
index 0000000..257da96
--- /dev/null
+++ b/xbmc/addons/GameResource.cpp
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2016-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 "GameResource.h"
+
+#include "addons/addoninfo/AddonType.h"
+
+#include <utility>
+
+using namespace ADDON;
+
+CGameResource::CGameResource(const AddonInfoPtr& addonInfo)
+ : CResource(addonInfo, AddonType::RESOURCE_GAMES)
+{
+}
diff --git a/xbmc/addons/GameResource.h b/xbmc/addons/GameResource.h
new file mode 100644
index 0000000..6770ed5
--- /dev/null
+++ b/xbmc/addons/GameResource.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016-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 "addons/Resource.h"
+
+#include <memory>
+
+namespace ADDON
+{
+
+class CGameResource : public CResource
+{
+public:
+ explicit CGameResource(const AddonInfoPtr& addonInfo);
+ ~CGameResource() override = default;
+
+ // implementation of CResource
+ bool IsAllowed(const std::string& file) const override { return true; }
+};
+
+}
diff --git a/xbmc/addons/IAddon.h b/xbmc/addons/IAddon.h
new file mode 100644
index 0000000..6953b20
--- /dev/null
+++ b/xbmc/addons/IAddon.h
@@ -0,0 +1,159 @@
+/*
+ * 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 <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CDateTime;
+class TiXmlElement;
+
+namespace ADDON
+{
+enum class AddonInstanceSupport;
+enum class AddonLifecycleState;
+enum class AddonType;
+
+class CAddonMgr;
+class CAddonSettings;
+class CAddonVersion;
+
+struct DependencyInfo;
+
+using AddonInstanceId = uint32_t;
+
+constexpr const char* ADDON_SETTING_INSTANCE_GROUP = "kodi_addon_instance";
+constexpr const char* ADDON_SETTING_INSTANCE_NAME_VALUE = "kodi_addon_instance_name";
+constexpr const char* ADDON_SETTING_INSTANCE_ENABLED_VALUE = "kodi_addon_instance_enabled";
+
+/*!
+ * @brief Identifier denoting default add-on instance.
+ *
+ * All numbers greater than 0 denote add-ons with support for multiple instances.
+ */
+constexpr AddonInstanceId ADDON_SINGLETON_INSTANCE_ID = 0;
+
+/*!
+ * @brief Identifier denoting initial first add-on instance.
+ */
+constexpr AddonInstanceId ADDON_FIRST_INSTANCE_ID = 1;
+
+/*!
+ * @brief Identifier denoting add-on instance id as unused.
+ *
+ * @sa ADDON::IAddonInstanceHandler
+ */
+constexpr AddonInstanceId ADDON_INSTANCE_ID_UNUSED = ADDON_SINGLETON_INSTANCE_ID;
+
+/*!
+ * @brief Identifier denoting default add-on settings.xml.
+ *
+ * All numbers greater than 0 denote add-on instances with an individual set of settings.
+ */
+constexpr AddonInstanceId ADDON_SETTINGS_ID = ADDON_SINGLETON_INSTANCE_ID;
+
+constexpr char const* ORIGIN_SYSTEM = "b6a50484-93a0-4afb-a01c-8d17e059feda";
+
+class IAddon;
+typedef std::shared_ptr<IAddon> AddonPtr;
+typedef std::vector<AddonPtr> VECADDONS;
+
+using InfoMap = std::map<std::string, std::string>;
+using ArtMap = std::map<std::string, std::string>;
+
+class IAddon : public std::enable_shared_from_this<IAddon>
+{
+public:
+ virtual ~IAddon() = default;
+ virtual AddonType MainType() const = 0;
+ virtual AddonType Type() const = 0;
+ virtual bool HasType(AddonType type) const = 0;
+ virtual bool HasMainType(AddonType type) const = 0;
+ virtual std::string ID() const = 0;
+ virtual std::string Name() const = 0;
+ virtual bool IsInUse() const = 0;
+ virtual bool IsBinary() const = 0;
+ virtual CAddonVersion Version() const = 0;
+ virtual CAddonVersion MinVersion() const = 0;
+ virtual std::string Summary() const = 0;
+ virtual std::string Description() const = 0;
+ virtual std::string Path() const = 0;
+ virtual std::string Profile() const = 0;
+ virtual std::string LibPath() const = 0;
+ virtual std::string ChangeLog() const = 0;
+ virtual std::string FanArt() const = 0;
+ virtual ArtMap Art() const = 0;
+ virtual std::vector<std::string> Screenshots() const = 0;
+ virtual std::string Author() const = 0;
+ virtual std::string Icon() const = 0;
+ virtual std::string Disclaimer() const = 0;
+ virtual AddonLifecycleState LifecycleState() const = 0;
+ virtual std::string LifecycleStateDescription() const = 0;
+ virtual CDateTime InstallDate() const = 0;
+ virtual CDateTime LastUpdated() const = 0;
+ virtual CDateTime LastUsed() const = 0;
+ virtual std::string Origin() const = 0;
+ virtual std::string OriginName() const = 0;
+ virtual uint64_t PackageSize() const = 0;
+ virtual const InfoMap& ExtraInfo() const = 0;
+ virtual bool SupportsMultipleInstances() const = 0;
+ virtual AddonInstanceSupport InstanceUseType() const = 0;
+ virtual std::vector<AddonInstanceId> GetKnownInstanceIds() const = 0;
+ virtual bool SupportsInstanceSettings() const = 0;
+ virtual bool DeleteInstanceSettings(AddonInstanceId instance) = 0;
+ virtual bool CanHaveAddonOrInstanceSettings() = 0;
+ virtual bool HasSettings(AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual bool HasUserSettings(AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual void SaveSettings(AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual void UpdateSetting(const std::string& key,
+ const std::string& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual bool UpdateSettingBool(const std::string& key,
+ bool value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual bool UpdateSettingInt(const std::string& key,
+ int value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual bool UpdateSettingNumber(const std::string& key,
+ double value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual bool UpdateSettingString(const std::string& key,
+ const std::string& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual std::string GetSetting(const std::string& key,
+ AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual bool GetSettingBool(const std::string& key,
+ bool& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual bool GetSettingInt(const std::string& key,
+ int& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual bool GetSettingNumber(const std::string& key,
+ double& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual bool GetSettingString(const std::string& key,
+ std::string& value,
+ AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual std::shared_ptr<CAddonSettings> GetSettings(AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual const std::vector<DependencyInfo>& GetDependencies() const = 0;
+ virtual CAddonVersion GetDependencyVersion(const std::string& dependencyID) const = 0;
+ virtual bool MeetsVersion(const CAddonVersion& versionMin,
+ const CAddonVersion& version) const = 0;
+ virtual bool ReloadSettings(AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual void ResetSettings(AddonInstanceId id = ADDON_SETTINGS_ID) = 0;
+ virtual AddonPtr GetRunningInstance() const = 0;
+ virtual void OnPreInstall() = 0;
+ virtual void OnPostInstall(bool update, bool modal) = 0;
+ virtual void OnPreUnInstall() = 0;
+ virtual void OnPostUnInstall() = 0;
+};
+
+}; // namespace ADDON
diff --git a/xbmc/addons/IAddonManagerCallback.h b/xbmc/addons/IAddonManagerCallback.h
new file mode 100644
index 0000000..03391f9
--- /dev/null
+++ b/xbmc/addons/IAddonManagerCallback.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "addons/IAddon.h"
+
+#include <string>
+
+namespace ADDON
+{
+/**
+ * Class - IAddonMgrCallback
+ * This callback should be inherited by any class which manages
+ * specific addon types. Could be mostly used for Dll addon types to handle
+ * cleanup before restart/removal
+ */
+class IAddonMgrCallback
+{
+public:
+ virtual ~IAddonMgrCallback() = default;
+ virtual bool RequestRestart(const std::string& addonId,
+ AddonInstanceId instanceId,
+ bool datachanged) = 0;
+};
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/IAddonSupportCheck.h b/xbmc/addons/IAddonSupportCheck.h
new file mode 100644
index 0000000..463afab
--- /dev/null
+++ b/xbmc/addons/IAddonSupportCheck.h
@@ -0,0 +1,46 @@
+/*
+ * 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 <string>
+
+namespace KODI
+{
+namespace ADDONS
+{
+
+/*!
+ * @brief Parent class to ask addons for support.
+ *
+ * This one can be used as a parent on the respective Kodi-sided addon class,
+ * this makes it easier to query for the desired support without using the
+ * class's own function definition.
+ */
+class IAddonSupportCheck
+{
+public:
+ IAddonSupportCheck() = default;
+ virtual ~IAddonSupportCheck() = default;
+
+ /*!
+ * @brief Function to query the respective add-ons used for the support of
+ * the desired file.
+ *
+ * @param[in] filename File which is queried for addon support
+ * @return True if addon supports the desired file, false if not
+ *
+ * @note Is set here with true as default and not with "= 0" in order to have
+ * class expandable and perhaps to be able to insert other query functions in
+ * the future.
+ */
+ virtual bool SupportsFile(const std::string& filename) { return true; }
+};
+
+} /* namespace ADDONS */
+} /* namespace KODI */
diff --git a/xbmc/addons/IAddonSupportList.h b/xbmc/addons/IAddonSupportList.h
new file mode 100644
index 0000000..0390a4c
--- /dev/null
+++ b/xbmc/addons/IAddonSupportList.h
@@ -0,0 +1,78 @@
+/*
+ * 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 <string>
+#include <vector>
+
+namespace KODI
+{
+namespace ADDONS
+{
+
+/*!
+ * @brief To identify related type about entry
+ */
+enum class AddonSupportType
+{
+ /* To set if type relates to a file extension */
+ Extension,
+
+ /* Used if relates to a Internet Media Type (MIME-Type) */
+ Mimetype
+};
+
+/*!
+ * @brief Information structure with which a supported format of an addon can
+ * be stored.
+ */
+struct AddonSupportEntry
+{
+ /* Type what this is about */
+ AddonSupportType m_type;
+
+ /* The name used for processing */
+ std::string m_name;
+
+ /* User-friendly description of the name type stored here. */
+ std::string m_description;
+
+ /* Path to use own icon about. */
+ std::string m_icon;
+};
+
+/*!
+ * @brief Parent class to manage all available mimetypes and file extensions of
+ * the respective add-on and its types.
+ *
+ * This class should be integrated in the respective add-on type manager and in
+ * order to get an overview of all that are intended for file processing.
+ *
+ * @todo Extend this class with database usage and support to activate /
+ * deactivate the respective formats and thus to override add-ons if several
+ * support the same.
+ */
+class IAddonSupportList
+{
+public:
+ IAddonSupportList() = default;
+ virtual ~IAddonSupportList() = default;
+
+ /*!
+ * @brief To give all file extensions and MIME types supported by the addon.
+ *
+ * @param[in] addonId Identifier about wanted addon
+ * @return List of all supported parts on selected addon
+ */
+ virtual std::vector<AddonSupportEntry> GetSupportedExtsAndMimeTypes(
+ const std::string& addonId) = 0;
+};
+
+} /* namespace ADDONS */
+} /* namespace KODI */
diff --git a/xbmc/addons/ImageDecoder.cpp b/xbmc/addons/ImageDecoder.cpp
new file mode 100644
index 0000000..6a7d52d
--- /dev/null
+++ b/xbmc/addons/ImageDecoder.cpp
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2013 Arne Morten Kvarving
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ImageDecoder.h"
+
+#include "guilib/TextureFormats.h"
+#include "pictures/PictureInfoTag.h"
+#include "utils/StringUtils.h"
+
+using namespace ADDON;
+using namespace KODI::ADDONS;
+
+namespace
+{
+
+constexpr std::array<std::tuple<unsigned int, ADDON_IMG_FMT, size_t>, 4> KodiToAddonFormat = {
+ {{XB_FMT_A8R8G8B8, ADDON_IMG_FMT_A8R8G8B8, sizeof(uint8_t) * 4},
+ {XB_FMT_A8, ADDON_IMG_FMT_A8, sizeof(uint8_t) * 1},
+ {XB_FMT_RGBA8, ADDON_IMG_FMT_RGBA8, sizeof(uint8_t) * 4},
+ {XB_FMT_RGB8, ADDON_IMG_FMT_RGB8, sizeof(uint8_t) * 3}}};
+
+} /* namespace */
+
+CImageDecoder::CImageDecoder(const AddonInfoPtr& addonInfo, const std::string& mimetype)
+ : IAddonInstanceHandler(ADDON_INSTANCE_IMAGEDECODER, addonInfo), m_mimetype(mimetype)
+{
+ // Create all interface parts independent to make API changes easier if
+ // something is added
+ m_ifc.imagedecoder = new AddonInstance_ImageDecoder;
+ m_ifc.imagedecoder->toAddon = new KodiToAddonFuncTable_ImageDecoder();
+ m_ifc.imagedecoder->toKodi = new AddonToKodiFuncTable_ImageDecoder();
+
+ if (CreateInstance() != ADDON_STATUS_OK)
+ return;
+
+ m_created = true;
+}
+
+CImageDecoder::~CImageDecoder()
+{
+ DestroyInstance();
+
+ delete m_ifc.imagedecoder->toKodi;
+ delete m_ifc.imagedecoder->toAddon;
+ delete m_ifc.imagedecoder;
+}
+
+bool CImageDecoder::SupportsFile(const std::string& filename)
+{
+ // Create in case not available, possible as this done by IAddonSupportCheck
+ if (!m_created || !m_ifc.imagedecoder->toAddon->supports_file)
+ return false;
+
+ return m_ifc.imagedecoder->toAddon->supports_file(m_ifc.hdl, filename.c_str());
+}
+
+bool CImageDecoder::LoadInfoTag(const std::string& fileName, CPictureInfoTag* tag)
+{
+ if (!m_created || !m_ifc.imagedecoder->toAddon->read_tag || !tag)
+ return false;
+
+ KODI_ADDON_IMAGEDECODER_INFO_TAG ifcTag = {};
+ bool ret = m_ifc.imagedecoder->toAddon->read_tag(m_ifc.hdl, fileName.c_str(), &ifcTag);
+ if (ret)
+ {
+ /*!
+ * List of values currently not used on addon interface.
+ *
+ * struct ExifInfo:
+ * - int Process{};
+ * - float CCDWidth{};
+ * - int Whitebalance{};
+ * - int CommentsCharset{};
+ * - int XPCommentsCharset{};
+ * - std::string Comments;
+ * - std::string FileComment;
+ * - std::string XPComment;
+ * - unsigned ThumbnailOffset{};
+ * - unsigned ThumbnailSize{};
+ * - unsigned LargestExifOffset{};
+ * - char ThumbnailAtEnd{};
+ * - int ThumbnailSizeOffset{};
+ * - std::vector<int> DateTimeOffsets;
+ *
+ * struct IPTCInfo:
+ * - std::string RecordVersion;
+ * - std::string SupplementalCategories;
+ * - std::string Keywords;
+ * - std::string Caption;
+ * - std::string Headline;
+ * - std::string SpecialInstructions;
+ * - std::string Category;
+ * - std::string Byline;
+ * - std::string BylineTitle;
+ * - std::string Credit;
+ * - std::string Source;
+ * - std::string ObjectName;
+ * - std::string City;
+ * - std::string State;
+ * - std::string Country;
+ * - std::string TransmissionReference;
+ * - std::string Date;
+ * - std::string Urgency;
+ * - std::string ReferenceService;
+ * - std::string CountryCode;
+ * - std::string SubLocation;
+ * - std::string ImageType;
+ *
+ * @todo Rework @ref CPictureInfoTag to not limit on fixed structures ExifInfo & IPTCInfo.
+ */
+
+ tag->m_exifInfo.Width = ifcTag.width;
+ tag->m_exifInfo.Height = ifcTag.height;
+ tag->m_exifInfo.Distance = ifcTag.distance;
+ tag->m_exifInfo.Orientation = ifcTag.orientation;
+ tag->m_exifInfo.IsColor = ifcTag.color == ADDON_IMG_COLOR_COLORED ? 1 : 0;
+ tag->m_exifInfo.ApertureFNumber = ifcTag.aperture_f_number;
+ tag->m_exifInfo.FlashUsed = ifcTag.flash_used ? 1 : 0;
+ tag->m_exifInfo.LightSource = ifcTag.light_source;
+ tag->m_exifInfo.FocalLength = ifcTag.focal_length;
+ tag->m_exifInfo.FocalLength35mmEquiv = ifcTag.focal_length_in_35mm_format;
+ tag->m_exifInfo.MeteringMode = ifcTag.metering_mode;
+ tag->m_exifInfo.DigitalZoomRatio = ifcTag.digital_zoom_ratio;
+ tag->m_exifInfo.ExposureTime = ifcTag.exposure_time;
+ tag->m_exifInfo.ExposureBias = ifcTag.exposure_bias;
+ tag->m_exifInfo.ExposureProgram = ifcTag.exposure_program;
+ tag->m_exifInfo.ExposureMode = ifcTag.exposure_mode;
+ tag->m_exifInfo.ISOequivalent = static_cast<int>(ifcTag.iso_speed);
+ tag->m_iptcInfo.TimeCreated = CDateTime(ifcTag.time_created).GetAsLocalizedDateTime();
+ tag->m_exifInfo.GpsInfoPresent = ifcTag.gps_info_present;
+ if (tag->m_exifInfo.GpsInfoPresent)
+ {
+ tag->m_exifInfo.GpsLat =
+ StringUtils::Format("{}{:.0f}°{:.0f}'{:.2f}\"", ifcTag.latitude_ref, ifcTag.latitude[0],
+ ifcTag.latitude[1], ifcTag.latitude[2]);
+ tag->m_exifInfo.GpsLong =
+ StringUtils::Format("{}{:.0f}°{:.0f}'{:.2f}\"", ifcTag.longitude_ref, ifcTag.longitude[0],
+ ifcTag.longitude[1], ifcTag.longitude[2]);
+ tag->m_exifInfo.GpsAlt =
+ StringUtils::Format("{}{:.2f} m", ifcTag.altitude_ref ? '-' : '+', ifcTag.altitude);
+ }
+
+ if (ifcTag.camera_manufacturer)
+ {
+ tag->m_exifInfo.CameraMake = ifcTag.camera_manufacturer;
+ free(ifcTag.camera_manufacturer);
+ }
+ if (ifcTag.camera_model)
+ {
+ tag->m_exifInfo.CameraModel = ifcTag.camera_model;
+ free(ifcTag.camera_model);
+ }
+ if (ifcTag.author)
+ {
+ tag->m_iptcInfo.Author = ifcTag.author;
+ free(ifcTag.author);
+ }
+ if (ifcTag.description)
+ {
+ tag->m_exifInfo.Description = ifcTag.description;
+ free(ifcTag.description);
+ }
+ if (ifcTag.copyright)
+ {
+ tag->m_iptcInfo.CopyrightNotice = ifcTag.copyright;
+ free(ifcTag.copyright);
+ }
+ }
+
+ return ret;
+}
+
+bool CImageDecoder::LoadImageFromMemory(unsigned char* buffer,
+ unsigned int bufSize,
+ unsigned int width,
+ unsigned int height)
+{
+ if (!m_created || !m_ifc.imagedecoder->toAddon->load_image_from_memory)
+ return false;
+
+ m_width = width;
+ m_height = height;
+ return m_ifc.imagedecoder->toAddon->load_image_from_memory(m_ifc.hdl, m_mimetype.c_str(), buffer,
+ bufSize, &m_width, &m_height);
+}
+
+bool CImageDecoder::Decode(unsigned char* const pixels,
+ unsigned int width,
+ unsigned int height,
+ unsigned int pitch,
+ unsigned int format)
+{
+ if (!m_created || !m_ifc.imagedecoder->toAddon->decode)
+ return false;
+
+ const auto it = std::find_if(KodiToAddonFormat.begin(), KodiToAddonFormat.end(),
+ [format](auto& p) { return std::get<0>(p) == format; });
+ if (it == KodiToAddonFormat.end())
+ return false;
+
+ const ADDON_IMG_FMT addonFmt = std::get<1>(*it);
+ const size_t size = width * height * std::get<2>(*it);
+ const bool result =
+ m_ifc.imagedecoder->toAddon->decode(m_ifc.hdl, pixels, size, width, height, pitch, addonFmt);
+ m_width = width;
+ m_height = height;
+
+ return result;
+}
diff --git a/xbmc/addons/ImageDecoder.h b/xbmc/addons/ImageDecoder.h
new file mode 100644
index 0000000..038c601
--- /dev/null
+++ b/xbmc/addons/ImageDecoder.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 Arne Morten Kvarving
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "addons/IAddonSupportCheck.h"
+#include "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/ImageDecoder.h"
+#include "guilib/iimage.h"
+
+class CPictureInfoTag;
+
+namespace KODI
+{
+namespace ADDONS
+{
+
+class CImageDecoder : public ADDON::IAddonInstanceHandler,
+ public KODI::ADDONS::IAddonSupportCheck,
+ public IImage
+{
+public:
+ explicit CImageDecoder(const ADDON::AddonInfoPtr& addonInfo, const std::string& mimetype);
+ ~CImageDecoder() override;
+
+ bool IsCreated() const { return m_created; }
+
+ /*! @ref IImage related functions */
+ ///@{
+ bool CreateThumbnailFromSurface(unsigned char*,
+ unsigned int,
+ unsigned int,
+ unsigned int,
+ unsigned int,
+ const std::string&,
+ unsigned char*&,
+ unsigned int&) override
+ {
+ return false;
+ }
+ bool LoadImageFromMemory(unsigned char* buffer,
+ unsigned int bufSize,
+ unsigned int width,
+ unsigned int height) override;
+ bool Decode(unsigned char* const pixels,
+ unsigned int width,
+ unsigned int height,
+ unsigned int pitch,
+ unsigned int format) override;
+ ///@}
+
+ /*! From @ref CPictureInfoTag used function to get information from addon */
+ ///@{
+ bool LoadInfoTag(const std::string& fileName, CPictureInfoTag* tag);
+ ///@}
+
+ /*! @ref KODI::ADDONS::IAddonSupportCheck related function */
+ ///@{
+ bool SupportsFile(const std::string& filename) override;
+ ///@}
+
+private:
+ /*! @note m_mimetype not set in all cases, only available if @ref LoadImageFromMemory is used. */
+ const std::string m_mimetype;
+ bool m_created{false};
+};
+
+} /* namespace ADDONS */
+} /* namespace KODI */
diff --git a/xbmc/addons/ImageResource.cpp b/xbmc/addons/ImageResource.cpp
new file mode 100644
index 0000000..06eada3
--- /dev/null
+++ b/xbmc/addons/ImageResource.cpp
@@ -0,0 +1,72 @@
+/*
+ * 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 "ImageResource.h"
+
+#include "URL.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/XbtManager.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+namespace ADDON
+{
+
+CImageResource::CImageResource(const AddonInfoPtr& addonInfo)
+ : CResource(addonInfo, AddonType::RESOURCE_IMAGES)
+{
+ m_type = Type(AddonType::RESOURCE_IMAGES)->GetValue("@type").asString();
+}
+
+void CImageResource::OnPreUnInstall()
+{
+ CURL xbtUrl;
+ if (!HasXbt(xbtUrl))
+ return;
+
+ // if there's an XBT we need to remove it from the XBT manager
+ XFILE::CXbtManager::GetInstance().Release(xbtUrl);
+}
+
+bool CImageResource::IsAllowed(const std::string &file) const
+{
+ // check if the file path points to a directory
+ if (URIUtils::HasSlashAtEnd(file, true))
+ return true;
+
+ std::string ext = URIUtils::GetExtension(file);
+ return file.empty() ||
+ StringUtils::EqualsNoCase(ext, ".png") ||
+ StringUtils::EqualsNoCase(ext, ".jpg");
+}
+
+std::string CImageResource::GetFullPath(const std::string &filePath) const
+{
+ // check if there's an XBT file which might contain the file. if not just return the usual full path
+ CURL xbtUrl;
+ if (!HasXbt(xbtUrl))
+ return CResource::GetFullPath(filePath);
+
+ // append the file path to the xbt:// URL
+ return URIUtils::AddFileToFolder(xbtUrl.Get(), filePath);
+}
+
+bool CImageResource::HasXbt(CURL& xbtUrl) const
+{
+ std::string resourcePath = GetResourcePath();
+ std::string xbtPath = URIUtils::AddFileToFolder(resourcePath, "Textures.xbt");
+ if (!CFileUtils::Exists(xbtPath))
+ return false;
+
+ // translate it into a xbt:// URL
+ xbtUrl = URIUtils::CreateArchivePath("xbt", CURL(xbtPath));
+
+ return true;
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/ImageResource.h b/xbmc/addons/ImageResource.h
new file mode 100644
index 0000000..f49a072
--- /dev/null
+++ b/xbmc/addons/ImageResource.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014-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 "addons/Resource.h"
+
+#include <string>
+
+class CURL;
+
+namespace ADDON
+{
+
+//! \brief A collection of images. The collection can have a type.
+class CImageResource : public CResource
+{
+public:
+ explicit CImageResource(const AddonInfoPtr& addonInfo);
+
+ void OnPreUnInstall() override;
+
+ bool IsAllowed(const std::string &file) const override;
+ std::string GetFullPath(const std::string &filePath) const override;
+
+ //! \brief Returns type of image collection
+ const std::string& GetType() const { return m_type; }
+
+private:
+ bool HasXbt(CURL& xbtUrl) const;
+
+ std::string m_type; //!< Type of images
+};
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/LanguageResource.cpp b/xbmc/addons/LanguageResource.cpp
new file mode 100644
index 0000000..c9ec0c8
--- /dev/null
+++ b/xbmc/addons/LanguageResource.cpp
@@ -0,0 +1,151 @@
+/*
+ * 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 "LanguageResource.h"
+
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/Skin.h"
+#include "addons/addoninfo/AddonType.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+using namespace KODI::MESSAGING;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+#define LANGUAGE_ADDON_PREFIX "resource.language."
+
+namespace ADDON
+{
+
+CLanguageResource::CLanguageResource(const AddonInfoPtr& addonInfo)
+ : CResource(addonInfo, AddonType::RESOURCE_LANGUAGE)
+{
+ // parse <extension> attributes
+ m_locale =
+ CLocale::FromString(Type(AddonType::RESOURCE_LANGUAGE)->GetValue("@locale").asString());
+
+ // parse <charsets>
+ const CAddonExtensions* charsetsElement =
+ Type(AddonType::RESOURCE_LANGUAGE)->GetElement("charsets");
+ if (charsetsElement != nullptr)
+ {
+ m_charsetGui = charsetsElement->GetValue("gui").asString();
+ m_forceUnicodeFont = charsetsElement->GetValue("gui@unicodefont").asBoolean();
+ m_charsetSubtitle = charsetsElement->GetValue("subtitle").asString();
+ }
+
+ // parse <dvd>
+ const CAddonExtensions* dvdElement = Type(AddonType::RESOURCE_LANGUAGE)->GetElement("dvd");
+ if (dvdElement != nullptr)
+ {
+ m_dvdLanguageMenu = dvdElement->GetValue("menu").asString();
+ m_dvdLanguageAudio = dvdElement->GetValue("audio").asString();
+ m_dvdLanguageSubtitle = dvdElement->GetValue("subtitle").asString();
+ }
+ // fall back to the language of the addon if a DVD language is not defined
+ if (m_dvdLanguageMenu.empty())
+ m_dvdLanguageMenu = m_locale.GetLanguageCode();
+ if (m_dvdLanguageAudio.empty())
+ m_dvdLanguageAudio = m_locale.GetLanguageCode();
+ if (m_dvdLanguageSubtitle.empty())
+ m_dvdLanguageSubtitle = m_locale.GetLanguageCode();
+
+ // parse <sorttokens>
+ const CAddonExtensions* sorttokensElement =
+ Type(AddonType::RESOURCE_LANGUAGE)->GetElement("sorttokens");
+ if (sorttokensElement != nullptr)
+ {
+ /* First loop goes around rows e.g.
+ * <token separators="'">L</token>
+ * <token>Le</token>
+ * ...
+ */
+ for (const auto& values : sorttokensElement->GetValues())
+ {
+ /* Second loop goes around the row parts, e.g.
+ * separators = "'"
+ * token = Le
+ */
+ std::string token = values.second.GetValue("token").asString();
+ std::string separators = values.second.GetValue("token@separators").asString();
+ if (!token.empty())
+ {
+ if (separators.empty())
+ separators = " ._";
+
+ for (auto separator : separators)
+ m_sortTokens.insert(token + separator);
+ }
+ }
+ }
+}
+
+bool CLanguageResource::IsInUse() const
+{
+ return StringUtils::EqualsNoCase(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_LANGUAGE), ID());
+}
+
+void CLanguageResource::OnPostInstall(bool update, bool modal)
+{
+ if (!g_SkinInfo)
+ return;
+
+ if (IsInUse() || (!update && !modal &&
+ (HELPERS::ShowYesNoDialogText(CVariant{Name()}, CVariant{24132}) ==
+ DialogResponse::CHOICE_YES)))
+ {
+ if (IsInUse())
+ g_langInfo.SetLanguage(ID());
+ else
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_LOCALE_LANGUAGE, ID());
+ }
+}
+
+bool CLanguageResource::IsAllowed(const std::string &file) const
+{
+ return file.empty() ||
+ StringUtils::EqualsNoCase(file.c_str(), "langinfo.xml") ||
+ StringUtils::EqualsNoCase(file.c_str(), "strings.po");
+}
+
+std::string CLanguageResource::GetAddonId(const std::string& locale)
+{
+ if (locale.empty())
+ return "";
+
+ std::string addonId = locale;
+ if (!StringUtils::StartsWith(addonId, LANGUAGE_ADDON_PREFIX))
+ addonId = LANGUAGE_ADDON_PREFIX + locale;
+
+ StringUtils::ToLower(addonId);
+ return addonId;
+}
+
+bool CLanguageResource::FindLegacyLanguage(const std::string &locale, std::string &legacyLanguage)
+{
+ if (locale.empty())
+ return false;
+
+ std::string addonId = GetAddonId(locale);
+
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(addonId, addon, AddonType::RESOURCE_LANGUAGE,
+ OnlyEnabled::CHOICE_YES))
+ return false;
+
+ legacyLanguage = addon->Name();
+ return true;
+}
+
+}
diff --git a/xbmc/addons/LanguageResource.h b/xbmc/addons/LanguageResource.h
new file mode 100644
index 0000000..d9587d6
--- /dev/null
+++ b/xbmc/addons/LanguageResource.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2014-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 "addons/Resource.h"
+#include "utils/Locale.h"
+
+#include <set>
+
+namespace ADDON
+{
+class CLanguageResource : public CResource
+{
+public:
+ explicit CLanguageResource(const AddonInfoPtr& addonInfo);
+
+ bool IsInUse() const override;
+
+ void OnPostInstall(bool update, bool modal) override;
+
+ bool IsAllowed(const std::string &file) const override;
+
+ const CLocale& GetLocale() const { return m_locale; }
+
+ const std::string& GetGuiCharset() const { return m_charsetGui; }
+ bool ForceUnicodeFont() const { return m_forceUnicodeFont; }
+ const std::string& GetSubtitleCharset() const { return m_charsetSubtitle; }
+
+ const std::string& GetDvdMenuLanguage() const { return m_dvdLanguageMenu; }
+ const std::string& GetDvdAudioLanguage() const { return m_dvdLanguageAudio; }
+ const std::string& GetDvdSubtitleLanguage() const { return m_dvdLanguageSubtitle; }
+
+ const std::set<std::string>& GetSortTokens() const { return m_sortTokens; }
+
+ static std::string GetAddonId(const std::string& locale);
+
+ static bool FindLegacyLanguage(const std::string &locale, std::string &legacyLanguage);
+
+private:
+ CLocale m_locale;
+
+ std::string m_charsetGui;
+ bool m_forceUnicodeFont;
+ std::string m_charsetSubtitle;
+
+ std::string m_dvdLanguageMenu;
+ std::string m_dvdLanguageAudio;
+ std::string m_dvdLanguageSubtitle;
+
+ std::set<std::string> m_sortTokens;
+};
+
+}
diff --git a/xbmc/addons/PluginSource.cpp b/xbmc/addons/PluginSource.cpp
new file mode 100644
index 0000000..c61d7b1
--- /dev/null
+++ b/xbmc/addons/PluginSource.cpp
@@ -0,0 +1,86 @@
+/*
+ * 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 "PluginSource.h"
+
+#include "URL.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "utils/StringUtils.h"
+
+#include <utility>
+
+namespace ADDON
+{
+
+CPluginSource::CPluginSource(const AddonInfoPtr& addonInfo, AddonType addonType)
+ : CAddon(addonInfo, addonType)
+{
+ std::string provides = addonInfo->Type(addonType)->GetValue("provides").asString();
+
+ for (const auto& values : addonInfo->Type(addonType)->GetValues())
+ {
+ if (values.first != "medialibraryscanpath")
+ continue;
+
+ std::string url = "plugin://" + ID() + '/';
+ std::string content = values.second.GetValue("medialibraryscanpath@content").asString();
+ std::string path = values.second.GetValue("medialibraryscanpath").asString();
+ if (!path.empty() && path.front() == '/')
+ path.erase(0, 1);
+ if (path.compare(0, url.size(), url))
+ path.insert(0, url);
+ m_mediaLibraryScanPaths[content].push_back(CURL(path).GetFileName());
+ }
+
+ SetProvides(provides);
+}
+
+void CPluginSource::SetProvides(const std::string &content)
+{
+ if (!content.empty())
+ {
+ std::vector<std::string> provides = StringUtils::Split(content, ' ');
+ for (std::vector<std::string>::const_iterator i = provides.begin(); i != provides.end(); ++i)
+ {
+ Content content = Translate(*i);
+ if (content != UNKNOWN)
+ m_providedContent.insert(content);
+ }
+ }
+ if (Type() == AddonType::SCRIPT && m_providedContent.empty())
+ m_providedContent.insert(EXECUTABLE);
+}
+
+CPluginSource::Content CPluginSource::Translate(const std::string &content)
+{
+ if (content == "audio")
+ return CPluginSource::AUDIO;
+ else if (content == "image")
+ return CPluginSource::IMAGE;
+ else if (content == "executable")
+ return CPluginSource::EXECUTABLE;
+ else if (content == "video")
+ return CPluginSource::VIDEO;
+ else if (content == "game")
+ return CPluginSource::GAME;
+ else
+ return CPluginSource::UNKNOWN;
+}
+
+bool CPluginSource::HasType(AddonType type) const
+{
+ return ((type == AddonType::VIDEO && Provides(VIDEO)) ||
+ (type == AddonType::AUDIO && Provides(AUDIO)) ||
+ (type == AddonType::IMAGE && Provides(IMAGE)) ||
+ (type == AddonType::GAME && Provides(GAME)) ||
+ (type == AddonType::EXECUTABLE && Provides(EXECUTABLE)) || (type == CAddon::Type()));
+}
+
+} /*namespace ADDON*/
+
diff --git a/xbmc/addons/PluginSource.h b/xbmc/addons/PluginSource.h
new file mode 100644
index 0000000..722df8b
--- /dev/null
+++ b/xbmc/addons/PluginSource.h
@@ -0,0 +1,55 @@
+/*
+ * 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 "addons/Addon.h"
+
+#include <set>
+
+namespace ADDON
+{
+
+typedef std::map<std::string, std::vector<std::string>> ContentPathMap;
+
+class CPluginSource : public CAddon
+{
+public:
+
+ enum Content { UNKNOWN, AUDIO, IMAGE, EXECUTABLE, VIDEO, GAME };
+
+ explicit CPluginSource(const AddonInfoPtr& addonInfo, AddonType addonType);
+
+ bool HasType(AddonType type) const override;
+ bool Provides(const Content& content) const
+ {
+ return content == UNKNOWN ? false : m_providedContent.count(content) > 0;
+ }
+
+ bool ProvidesSeveral() const
+ {
+ return m_providedContent.size() > 1;
+ }
+
+ const ContentPathMap& MediaLibraryScanPaths() const
+ {
+ return m_mediaLibraryScanPaths;
+ }
+
+ static Content Translate(const std::string &content);
+private:
+ /*! \brief Set the provided content for this plugin
+ If no valid content types are passed in, we set the EXECUTABLE type
+ \param content a space-separated list of content types
+ */
+ void SetProvides(const std::string &content);
+ std::set<Content> m_providedContent;
+ ContentPathMap m_mediaLibraryScanPaths;
+};
+
+} /*namespace ADDON*/
diff --git a/xbmc/addons/Repository.cpp b/xbmc/addons/Repository.cpp
new file mode 100644
index 0000000..6708e71
--- /dev/null
+++ b/xbmc/addons/Repository.cpp
@@ -0,0 +1,326 @@
+/*
+ * 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 "Repository.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/AddonDatabase.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/RepositoryUpdater.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/File.h"
+#include "filesystem/ZipFile.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "utils/Base64.h"
+#include "utils/Digest.h"
+#include "utils/Mime.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <tuple>
+#include <utility>
+
+using namespace XFILE;
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+using KODI::UTILITY::CDigest;
+using KODI::UTILITY::TypedDigest;
+
+
+CRepository::ResolveResult CRepository::ResolvePathAndHash(const AddonPtr& addon) const
+{
+ std::string const& path = addon->Path();
+
+ auto dirIt = std::find_if(m_dirs.begin(), m_dirs.end(), [&path](RepositoryDirInfo const& dir) {
+ return URIUtils::PathHasParent(path, dir.datadir, true);
+ });
+ if (dirIt == m_dirs.end())
+ {
+ CLog::Log(LOGERROR, "Requested path {} not found in known repository directories", path);
+ return {};
+ }
+
+ if (dirIt->hashType == CDigest::Type::INVALID)
+ {
+ // We have a path, but need no hash
+ return {path, {}};
+ }
+
+ // Do not follow mirror redirect, we want the headers of the redirect response
+ CURL url{path};
+ url.SetProtocolOption("redirect-limit", "0");
+ CCurlFile file;
+ if (!file.Open(url))
+ {
+ CLog::Log(LOGERROR, "Could not fetch addon location and hash from {}", path);
+ return {};
+ }
+
+ std::string hashTypeStr = CDigest::TypeToString(dirIt->hashType);
+
+ // Return the location from the header so we don't have to look it up again
+ // (saves one request per addon install)
+ std::string location = file.GetRedirectURL();
+ // content-* headers are base64, convert to base16
+ TypedDigest hash{dirIt->hashType, StringUtils::ToHexadecimal(Base64::Decode(file.GetHttpHeader().GetValue(std::string("content-") + hashTypeStr)))};
+
+ if (hash.Empty())
+ {
+ int tmp;
+ // Expected hash, but none found -> fall back to old method
+ if (!FetchChecksum(path + "." + hashTypeStr, hash.value, tmp) || hash.Empty())
+ {
+ CLog::Log(LOGERROR, "Failed to find hash for {} from HTTP header and in separate file", path);
+ return {};
+ }
+ }
+ if (location.empty())
+ {
+ // Fall back to original URL if we do not get a redirect
+ location = path;
+ }
+
+ CLog::Log(LOGDEBUG, "Resolved addon path {} to {} hash {}", path, location, hash.value);
+
+ return {location, hash};
+}
+
+CRepository::CRepository(const AddonInfoPtr& addonInfo) : CAddon(addonInfo, AddonType::REPOSITORY)
+{
+ RepositoryDirList dirs;
+ CAddonVersion version;
+ AddonInfoPtr addonver =
+ CServiceBroker::GetAddonMgr().GetAddonInfo("xbmc.addon", AddonType::UNKNOWN);
+ if (addonver)
+ version = addonver->Version();
+
+ for (const auto& element : Type(AddonType::REPOSITORY)->GetElements("dir"))
+ {
+ RepositoryDirInfo dir = ParseDirConfiguration(element.second);
+ if ((dir.minversion.empty() || version >= dir.minversion) &&
+ (dir.maxversion.empty() || version <= dir.maxversion))
+ m_dirs.push_back(std::move(dir));
+ }
+
+ // old (dharma compatible) way of defining the addon repository structure, is no longer supported
+ // we error out so the user knows how to migrate. The <dir> way is supported since gotham.
+ //! @todo remove if block completely in v21
+ if (!Type(AddonType::REPOSITORY)->GetValue("info").empty())
+ {
+ CLog::Log(LOGERROR,
+ "Repository add-on {} uses old schema definition for the repository extension point! "
+ "This is no longer supported, please update your addon to use <dir> definitions.",
+ ID());
+ }
+
+ if (m_dirs.empty())
+ {
+ CLog::Log(LOGERROR,
+ "Repository add-on {} does not have any directory and won't be able to update/serve "
+ "addons! Please fix the addon.xml definition",
+ ID());
+ }
+
+ for (auto const& dir : m_dirs)
+ {
+ CURL datadir(dir.datadir);
+ if (datadir.IsProtocol("http"))
+ {
+ CLog::Log(LOGWARNING, "Repository add-on {} uses plain HTTP for add-on downloads in path {} - this is insecure and will make your Kodi installation vulnerable to attacks if enabled!", ID(), datadir.GetRedacted());
+ }
+ else if (datadir.IsProtocol("https") && datadir.HasProtocolOption("verifypeer") && datadir.GetProtocolOption("verifypeer") == "false")
+ {
+ CLog::Log(LOGWARNING, "Repository add-on {} disabled peer verification for add-on downloads in path {} - this is insecure and will make your Kodi installation vulnerable to attacks if enabled!", ID(), datadir.GetRedacted());
+ }
+ }
+}
+
+bool CRepository::FetchChecksum(const std::string& url,
+ std::string& checksum,
+ int& recheckAfter) noexcept
+{
+ CFile file;
+ if (!file.Open(url))
+ return false;
+
+ // we intentionally avoid using file.GetLength() for
+ // Transfer-Encoding: chunked servers.
+ std::stringstream ss;
+ char temp[1024];
+ int read;
+ while ((read = file.Read(temp, sizeof(temp))) > 0)
+ ss.write(temp, read);
+ if (read <= -1)
+ return false;
+ checksum = ss.str();
+ std::size_t pos = checksum.find_first_of(" \n");
+ if (pos != std::string::npos)
+ {
+ checksum = checksum.substr(0, pos);
+ }
+
+ // Determine update interval from (potential) HTTP response
+ // Default: 24 h
+ recheckAfter = 24 * 60 * 60;
+ // This special header is set by the Kodi mirror redirector to control client update frequency
+ // depending on the load on the mirrors
+ const std::string recheckAfterHeader{
+ file.GetProperty(FILE_PROPERTY_RESPONSE_HEADER, "X-Kodi-Recheck-After")};
+ if (!recheckAfterHeader.empty())
+ {
+ try
+ {
+ // Limit value range to sensible values (1 hour to 1 week)
+ recheckAfter =
+ std::max(std::min(std::stoi(recheckAfterHeader), 24 * 7 * 60 * 60), 1 * 60 * 60);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGWARNING, "Could not parse X-Kodi-Recheck-After header value '{}' from {}",
+ recheckAfterHeader, url);
+ }
+ }
+
+ return true;
+}
+
+bool CRepository::FetchIndex(const RepositoryDirInfo& repo,
+ std::string const& digest,
+ std::vector<AddonInfoPtr>& addons) noexcept
+{
+ XFILE::CCurlFile http;
+
+ std::string response;
+ if (!http.Get(repo.info, response))
+ {
+ CLog::Log(LOGERROR, "CRepository: failed to read {}", repo.info);
+ return false;
+ }
+
+ if (repo.checksumType != CDigest::Type::INVALID)
+ {
+ std::string actualDigest = CDigest::Calculate(repo.checksumType, response);
+ if (!StringUtils::EqualsNoCase(digest, actualDigest))
+ {
+ CLog::Log(LOGERROR, "CRepository: {} index has wrong digest {}, expected: {}", repo.info, actualDigest, digest);
+ return false;
+ }
+ }
+
+ if (URIUtils::HasExtension(repo.info, ".gz")
+ || CMime::GetFileTypeFromMime(http.GetProperty(XFILE::FILE_PROPERTY_MIME_TYPE)) == CMime::EFileType::FileTypeGZip)
+ {
+ CLog::Log(LOGDEBUG, "CRepository '{}' is gzip. decompressing", repo.info);
+ std::string buffer;
+ if (!CZipFile::DecompressGzip(response, buffer))
+ {
+ CLog::Log(LOGERROR, "CRepository: failed to decompress gzip from '{}'", repo.info);
+ return false;
+ }
+ response = std::move(buffer);
+ }
+
+ return CServiceBroker::GetAddonMgr().AddonsFromRepoXML(repo, response, addons);
+}
+
+CRepository::FetchStatus CRepository::FetchIfChanged(const std::string& oldChecksum,
+ std::string& checksum,
+ std::vector<AddonInfoPtr>& addons,
+ int& recheckAfter) const
+{
+ checksum = "";
+ std::vector<std::tuple<RepositoryDirInfo const&, std::string>> dirChecksums;
+ std::vector<int> recheckAfterTimes;
+
+ for (const auto& dir : m_dirs)
+ {
+ if (!dir.checksum.empty())
+ {
+ std::string part;
+ int recheckAfterThisDir;
+ if (!FetchChecksum(dir.checksum, part, recheckAfterThisDir))
+ {
+ recheckAfter = 1 * 60 * 60; // retry after 1 hour
+ CLog::Log(LOGERROR, "CRepository: failed read '{}'", dir.checksum);
+ return STATUS_ERROR;
+ }
+ dirChecksums.emplace_back(dir, part);
+ recheckAfterTimes.push_back(recheckAfterThisDir);
+ checksum += part;
+ }
+ }
+
+ // Default interval: 24 h
+ recheckAfter = 24 * 60 * 60;
+ if (dirChecksums.size() == m_dirs.size() && !dirChecksums.empty())
+ {
+ // Use smallest update interval out of all received (individual intervals per directory are
+ // not possible)
+ recheckAfter = *std::min_element(recheckAfterTimes.begin(), recheckAfterTimes.end());
+ // If all directories have checksums and they match the last one, nothing has changed
+ if (dirChecksums.size() == m_dirs.size() && oldChecksum == checksum)
+ return STATUS_NOT_MODIFIED;
+ }
+
+ for (const auto& dirTuple : dirChecksums)
+ {
+ std::vector<AddonInfoPtr> tmp;
+ if (!FetchIndex(std::get<0>(dirTuple), std::get<1>(dirTuple), tmp))
+ return STATUS_ERROR;
+ addons.insert(addons.end(), tmp.begin(), tmp.end());
+ }
+ return STATUS_OK;
+}
+
+RepositoryDirInfo CRepository::ParseDirConfiguration(const CAddonExtensions& configuration)
+{
+ RepositoryDirInfo dir;
+ dir.checksum = configuration.GetValue("checksum").asString();
+ std::string checksumStr = configuration.GetValue("checksum@verify").asString();
+ if (!checksumStr.empty())
+ {
+ dir.checksumType = CDigest::TypeFromString(checksumStr);
+ }
+ dir.info = configuration.GetValue("info").asString();
+ dir.datadir = configuration.GetValue("datadir").asString();
+ dir.artdir = configuration.GetValue("artdir").asString();
+ if (dir.artdir.empty())
+ {
+ dir.artdir = dir.datadir;
+ }
+
+ std::string hashStr = configuration.GetValue("hashes").asString();
+ StringUtils::ToLower(hashStr);
+ if (hashStr == "true")
+ {
+ // Deprecated alias
+ hashStr = "md5";
+ }
+ if (!hashStr.empty() && hashStr != "false")
+ {
+ dir.hashType = CDigest::TypeFromString(hashStr);
+ if (dir.hashType == CDigest::Type::MD5)
+ {
+ CLog::Log(LOGWARNING, "CRepository::{}: Repository has MD5 hashes enabled - this hash function is broken and will only guard against unintentional data corruption", __FUNCTION__);
+ }
+ }
+
+ dir.minversion = CAddonVersion{configuration.GetValue("@minversion").asString()};
+ dir.maxversion = CAddonVersion{configuration.GetValue("@maxversion").asString()};
+
+ return dir;
+}
diff --git a/xbmc/addons/Repository.h b/xbmc/addons/Repository.h
new file mode 100644
index 0000000..4c2ac32
--- /dev/null
+++ b/xbmc/addons/Repository.h
@@ -0,0 +1,76 @@
+/*
+ * 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 "addons/Addon.h"
+#include "addons/AddonVersion.h"
+#include "utils/Digest.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace ADDON
+{
+class CAddonExtensions;
+
+struct RepositoryDirInfo
+{
+ CAddonVersion minversion{""};
+ CAddonVersion maxversion{""};
+ std::string info;
+ std::string checksum;
+ KODI::UTILITY::CDigest::Type checksumType{KODI::UTILITY::CDigest::Type::INVALID};
+ std::string datadir;
+ std::string artdir;
+ KODI::UTILITY::CDigest::Type hashType{KODI::UTILITY::CDigest::Type::INVALID};
+};
+
+typedef std::vector<RepositoryDirInfo> RepositoryDirList;
+
+class CRepository : public CAddon
+{
+public:
+ explicit CRepository(const AddonInfoPtr& addonInfo);
+
+ enum FetchStatus
+ {
+ STATUS_OK,
+ STATUS_NOT_MODIFIED,
+ STATUS_ERROR
+ };
+
+ FetchStatus FetchIfChanged(const std::string& oldChecksum,
+ std::string& checksum,
+ std::vector<AddonInfoPtr>& addons,
+ int& recheckAfter) const;
+
+ struct ResolveResult
+ {
+ std::string location;
+ KODI::UTILITY::TypedDigest digest;
+ };
+ ResolveResult ResolvePathAndHash(AddonPtr const& addon) const;
+
+private:
+ static bool FetchChecksum(const std::string& url,
+ std::string& checksum,
+ int& recheckAfter) noexcept;
+ static bool FetchIndex(const RepositoryDirInfo& repo,
+ std::string const& digest,
+ std::vector<AddonInfoPtr>& addons) noexcept;
+
+ static RepositoryDirInfo ParseDirConfiguration(const CAddonExtensions& configuration);
+
+ RepositoryDirList m_dirs;
+};
+
+typedef std::shared_ptr<CRepository> RepositoryPtr;
+}
+
diff --git a/xbmc/addons/RepositoryUpdater.cpp b/xbmc/addons/RepositoryUpdater.cpp
new file mode 100644
index 0000000..3ea8b9c
--- /dev/null
+++ b/xbmc/addons/RepositoryUpdater.cpp
@@ -0,0 +1,338 @@
+/*
+ * 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 "RepositoryUpdater.h"
+
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "addons/AddonDatabase.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/Repository.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "events/AddonManagementEvent.h"
+#include "events/EventLog.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/JobManager.h"
+#include "utils/ProgressJob.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <mutex>
+#include <vector>
+
+using namespace std::chrono_literals;
+
+namespace ADDON
+{
+
+class CRepositoryUpdateJob : public CProgressJob
+{
+public:
+ explicit CRepositoryUpdateJob(const RepositoryPtr& repo) : m_repo(repo) {}
+ ~CRepositoryUpdateJob() override = default;
+ bool DoWork() override;
+ const RepositoryPtr& GetAddon() const { return m_repo; }
+
+private:
+ const RepositoryPtr m_repo;
+};
+
+bool CRepositoryUpdateJob::DoWork()
+{
+ CLog::Log(LOGDEBUG, "CRepositoryUpdateJob[{}] checking for updates.", m_repo->ID());
+ CAddonDatabase database;
+ database.Open();
+
+ std::string oldChecksum;
+ if (database.GetRepoChecksum(m_repo->ID(), oldChecksum) == -1)
+ oldChecksum = "";
+
+ const CAddonDatabase::RepoUpdateData updateData{database.GetRepoUpdateData(m_repo->ID())};
+ if (updateData.lastCheckedVersion != m_repo->Version())
+ oldChecksum = "";
+
+ std::string newChecksum;
+ std::vector<AddonInfoPtr> addons;
+ int recheckAfter;
+ auto status = m_repo->FetchIfChanged(oldChecksum, newChecksum, addons, recheckAfter);
+
+ database.SetRepoUpdateData(
+ m_repo->ID(), CAddonDatabase::RepoUpdateData(
+ CDateTime::GetCurrentDateTime(), m_repo->Version(),
+ CDateTime::GetCurrentDateTime() + CDateTimeSpan(0, 0, 0, recheckAfter)));
+
+ MarkFinished();
+
+ if (status == CRepository::STATUS_ERROR)
+ return false;
+
+ if (status == CRepository::STATUS_NOT_MODIFIED)
+ {
+ CLog::Log(LOGDEBUG, "CRepositoryUpdateJob[{}] checksum not changed.", m_repo->ID());
+ return true;
+ }
+
+ //Invalidate art.
+ {
+ CTextureDatabase textureDB;
+ textureDB.Open();
+ textureDB.BeginMultipleExecute();
+
+ for (const auto& addon : addons)
+ {
+ AddonPtr oldAddon;
+ if (CServiceBroker::GetAddonMgr().FindInstallableById(addon->ID(), oldAddon) && oldAddon &&
+ addon->Version() > oldAddon->Version())
+ {
+ if (!oldAddon->Icon().empty() || !oldAddon->Art().empty() ||
+ !oldAddon->Screenshots().empty())
+ CLog::Log(LOGDEBUG, "CRepository: invalidating cached art for '{}'", addon->ID());
+
+ if (!oldAddon->Icon().empty())
+ textureDB.InvalidateCachedTexture(oldAddon->Icon());
+
+ for (const auto& path : oldAddon->Screenshots())
+ textureDB.InvalidateCachedTexture(path);
+
+ for (const auto& art : oldAddon->Art())
+ textureDB.InvalidateCachedTexture(art.second);
+ }
+ }
+ textureDB.CommitMultipleExecute();
+ }
+
+ database.UpdateRepositoryContent(m_repo->ID(), m_repo->Version(), newChecksum, addons);
+ return true;
+}
+
+CRepositoryUpdater::CRepositoryUpdater(CAddonMgr& addonMgr) :
+ m_timer(this),
+ m_doneEvent(true),
+ m_addonMgr(addonMgr)
+{
+ // Register settings
+ std::set<std::string> settingSet;
+ settingSet.insert(CSettings::SETTING_ADDONS_AUTOUPDATES);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->RegisterCallback(this, settingSet);
+}
+
+void CRepositoryUpdater::Start()
+{
+ m_addonMgr.Events().Subscribe(this, &CRepositoryUpdater::OnEvent);
+ ScheduleUpdate();
+}
+
+CRepositoryUpdater::~CRepositoryUpdater()
+{
+ // Unregister settings
+ CServiceBroker::GetSettingsComponent()->GetSettings()->UnregisterCallback(this);
+
+ m_addonMgr.Events().Unsubscribe(this);
+}
+
+void CRepositoryUpdater::OnEvent(const ADDON::AddonEvent& event)
+{
+ if (typeid(event) == typeid(ADDON::AddonEvents::Enabled))
+ {
+ if (m_addonMgr.HasType(event.addonId, AddonType::REPOSITORY))
+ ScheduleUpdate();
+ }
+}
+
+void CRepositoryUpdater::OnJobComplete(unsigned int jobID, bool success, CJob* job)
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ m_jobs.erase(std::find(m_jobs.begin(), m_jobs.end(), job));
+ if (m_jobs.empty())
+ {
+ CLog::Log(LOGDEBUG, "CRepositoryUpdater: done.");
+ m_doneEvent.Set();
+
+ VECADDONS updates = m_addonMgr.GetAvailableUpdates();
+
+ if (CAddonSystemSettings::GetInstance().GetAddonAutoUpdateMode() == AUTO_UPDATES_NOTIFY)
+ {
+ if (!updates.empty())
+ {
+ if (updates.size() == 1)
+ CGUIDialogKaiToast::QueueNotification(
+ updates[0]->Icon(), updates[0]->Name(), g_localizeStrings.Get(24068),
+ TOAST_DISPLAY_TIME, false, TOAST_DISPLAY_TIME);
+ else
+ CGUIDialogKaiToast::QueueNotification(
+ "", g_localizeStrings.Get(24001), g_localizeStrings.Get(24061),
+ TOAST_DISPLAY_TIME, false, TOAST_DISPLAY_TIME);
+
+ auto eventLog = CServiceBroker::GetEventLog();
+ for (const auto &addon : updates)
+ {
+ if (eventLog)
+ eventLog->Add(EventPtr(new CAddonManagementEvent(addon, 24068)));
+ }
+ }
+ }
+
+ if (CAddonSystemSettings::GetInstance().GetAddonAutoUpdateMode() == AUTO_UPDATES_ON)
+ {
+ m_addonMgr.CheckAndInstallAddonUpdates(false);
+ }
+
+ ScheduleUpdate();
+
+ m_events.Publish(RepositoryUpdated{});
+ }
+}
+
+bool CRepositoryUpdater::CheckForUpdates(bool showProgress)
+{
+ VECADDONS addons;
+ if (m_addonMgr.GetAddons(addons, AddonType::REPOSITORY) && !addons.empty())
+ {
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ for (const auto& addon : addons)
+ CheckForUpdates(std::static_pointer_cast<ADDON::CRepository>(addon), showProgress);
+
+ return true;
+ }
+
+ return false;
+}
+
+static void SetProgressIndicator(CRepositoryUpdateJob* job)
+{
+ auto dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
+ if (dialog)
+ job->SetProgressIndicators(dialog->GetHandle(g_localizeStrings.Get(24092)), nullptr);
+}
+
+void CRepositoryUpdater::CheckForUpdates(const ADDON::RepositoryPtr& repo, bool showProgress)
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ auto job = std::find_if(m_jobs.begin(), m_jobs.end(),
+ [&](CRepositoryUpdateJob* job){ return job->GetAddon()->ID() == repo->ID(); });
+
+ if (job == m_jobs.end())
+ {
+ auto* job = new CRepositoryUpdateJob(repo);
+ m_jobs.push_back(job);
+ m_doneEvent.Reset();
+ if (showProgress)
+ SetProgressIndicator(job);
+ CServiceBroker::GetJobManager()->AddJob(job, this, CJob::PRIORITY_LOW);
+ }
+ else
+ {
+ if (showProgress && !(*job)->HasProgressIndicator())
+ SetProgressIndicator(*job);
+ }
+}
+
+void CRepositoryUpdater::Await()
+{
+ m_doneEvent.Wait();
+}
+
+void CRepositoryUpdater::OnTimeout()
+{
+ //workaround
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_VIDEO ||
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_GAME ||
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ {
+ CLog::Log(LOGDEBUG,"CRepositoryUpdater: busy playing. postponing scheduled update");
+ m_timer.RestartAsync(2min);
+ return;
+ }
+
+ CLog::Log(LOGDEBUG,"CRepositoryUpdater: running scheduled update");
+ CheckForUpdates();
+}
+
+void CRepositoryUpdater::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting->GetId() == CSettings::SETTING_ADDONS_AUTOUPDATES)
+ ScheduleUpdate();
+}
+
+CDateTime CRepositoryUpdater::LastUpdated() const
+{
+ VECADDONS repos;
+ if (!m_addonMgr.GetAddons(repos, AddonType::REPOSITORY) || repos.empty())
+ return CDateTime();
+
+ CAddonDatabase db;
+ db.Open();
+ std::vector<CDateTime> updateTimes;
+ std::transform(
+ repos.begin(), repos.end(), std::back_inserter(updateTimes), [&](const AddonPtr& repo) {
+ const auto updateData = db.GetRepoUpdateData(repo->ID());
+ if (updateData.lastCheckedAt.IsValid() && updateData.lastCheckedVersion == repo->Version())
+ return updateData.lastCheckedAt;
+ return CDateTime();
+ });
+
+ return *std::min_element(updateTimes.begin(), updateTimes.end());
+}
+
+CDateTime CRepositoryUpdater::ClosestNextCheck() const
+{
+ VECADDONS repos;
+ if (!m_addonMgr.GetAddons(repos, AddonType::REPOSITORY) || repos.empty())
+ return CDateTime();
+
+ CAddonDatabase db;
+ db.Open();
+ std::vector<CDateTime> nextCheckTimes;
+ std::transform(
+ repos.begin(), repos.end(), std::back_inserter(nextCheckTimes), [&](const AddonPtr& repo) {
+ const auto updateData = db.GetRepoUpdateData(repo->ID());
+ if (updateData.nextCheckAt.IsValid() && updateData.lastCheckedVersion == repo->Version())
+ return updateData.nextCheckAt;
+ return CDateTime();
+ });
+
+ return *std::min_element(nextCheckTimes.begin(), nextCheckTimes.end());
+}
+
+void CRepositoryUpdater::ScheduleUpdate()
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ m_timer.Stop(true);
+
+ if (CAddonSystemSettings::GetInstance().GetAddonAutoUpdateMode() == AUTO_UPDATES_NEVER)
+ return;
+
+ if (!m_addonMgr.HasAddons(AddonType::REPOSITORY))
+ return;
+
+ int delta{1};
+ const auto nextCheck = ClosestNextCheck();
+ if (nextCheck.IsValid())
+ {
+ // Repos were already checked once and we know when to check next
+ delta = std::max(1, (nextCheck - CDateTime::GetCurrentDateTime()).GetSecondsTotal() * 1000);
+ CLog::Log(LOGDEBUG, "CRepositoryUpdater: closest next update check at {} (in {} s)",
+ nextCheck.GetAsLocalizedDateTime(), delta / 1000);
+ }
+
+ if (!m_timer.Start(std::chrono::milliseconds(delta)))
+ CLog::Log(LOGERROR,"CRepositoryUpdater: failed to start timer");
+}
+}
diff --git a/xbmc/addons/RepositoryUpdater.h b/xbmc/addons/RepositoryUpdater.h
new file mode 100644
index 0000000..e5550c7
--- /dev/null
+++ b/xbmc/addons/RepositoryUpdater.h
@@ -0,0 +1,98 @@
+/*
+ * 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 "settings/lib/ISettingCallback.h"
+#include "threads/CriticalSection.h"
+#include "threads/Timer.h"
+#include "utils/EventStream.h"
+#include "utils/Job.h"
+
+#include <memory>
+#include <vector>
+
+class CDateTime;
+
+namespace ADDON
+{
+
+class CAddonMgr;
+
+class CRepository;
+using RepositoryPtr = std::shared_ptr<CRepository>;
+
+class CRepositoryUpdateJob;
+
+struct AddonEvent;
+
+class CRepositoryUpdater : private ITimerCallback, private IJobCallback, public ISettingCallback
+{
+public:
+ explicit CRepositoryUpdater(CAddonMgr& addonMgr);
+ ~CRepositoryUpdater() override;
+
+ void Start();
+
+ /**
+ * Check a single repository for updates.
+ */
+ void CheckForUpdates(const ADDON::RepositoryPtr& repo, bool showProgress=false);
+
+ /**
+ * Check all repositories for updates.
+ */
+ bool CheckForUpdates(bool showProgress=false);
+
+ /**
+ * Wait for any pending/in-progress updates to complete.
+ */
+ void Await();
+
+ /**
+ * Schedule an automatic update to run based on settings and previous update
+ * times. May be called when there are external changes this updater must know
+ * about. Any previously scheduled update will be cancelled.
+ */
+ void ScheduleUpdate();
+
+ /**
+ * Returns the time of the last check (oldest). Invalid time if never checked.
+ */
+ CDateTime LastUpdated() const;
+
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ struct RepositoryUpdated { };
+
+ CEventStream<RepositoryUpdated>& Events() { return m_events; }
+
+private:
+ CRepositoryUpdater(const CRepositoryUpdater&) = delete;
+ CRepositoryUpdater(CRepositoryUpdater&&) = delete;
+ CRepositoryUpdater& operator=(const CRepositoryUpdater&) = delete;
+ CRepositoryUpdater& operator=(CRepositoryUpdater&&) = delete;
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ void OnTimeout() override;
+
+ void OnEvent(const ADDON::AddonEvent& event);
+
+ CDateTime ClosestNextCheck() const;
+
+ CCriticalSection m_criticalSection;
+ CTimer m_timer;
+ CEvent m_doneEvent;
+ std::vector<CRepositoryUpdateJob*> m_jobs;
+ CAddonMgr& m_addonMgr;
+
+ CEventSource<RepositoryUpdated> m_events;
+};
+}
diff --git a/xbmc/addons/Resource.h b/xbmc/addons/Resource.h
new file mode 100644
index 0000000..d426c85
--- /dev/null
+++ b/xbmc/addons/Resource.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014-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 "addons/Addon.h"
+#include "utils/URIUtils.h"
+
+#include <memory>
+
+namespace ADDON
+{
+
+class CResource : public CAddon
+{
+public:
+ ~CResource() override = default;
+
+ virtual bool IsAllowed(const std::string &file) const = 0;
+
+ virtual std::string GetFullPath(const std::string &filePath) const
+ {
+ return URIUtils::AddFileToFolder(GetResourcePath(), filePath);
+ }
+
+protected:
+ explicit CResource(const AddonInfoPtr& addonInfo, AddonType addonType)
+ : CAddon(addonInfo, addonType)
+ {
+ }
+
+ std::string GetResourcePath() const
+ {
+ return URIUtils::AddFileToFolder(Path(), "resources");
+ }
+};
+
+}
diff --git a/xbmc/addons/Scraper.cpp b/xbmc/addons/Scraper.cpp
new file mode 100644
index 0000000..14c555f
--- /dev/null
+++ b/xbmc/addons/Scraper.cpp
@@ -0,0 +1,1470 @@
+/*
+ * 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 "Scraper.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/settings/AddonSettings.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/PluginDirectory.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/Album.h"
+#include "music/Artist.h"
+#include "music/MusicDatabase.h"
+#include "music/infoscanner/MusicAlbumInfo.h"
+#include "music/infoscanner/MusicArtistInfo.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SettingsValueFlatJsonSerializer.h"
+#include "utils/CharsetConverter.h"
+#include "utils/ScraperParser.h"
+#include "utils/ScraperUrl.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <algorithm>
+#include <sstream>
+
+#include <fstrcmp.h>
+
+using namespace XFILE;
+using namespace MUSIC_GRABBER;
+using namespace VIDEO;
+
+namespace ADDON
+{
+
+typedef struct
+{
+ const char *name;
+ CONTENT_TYPE type;
+ int pretty;
+} ContentMapping;
+
+static const ContentMapping content[] = {{"unknown", CONTENT_NONE, 231},
+ {"albums", CONTENT_ALBUMS, 132},
+ {"music", CONTENT_ALBUMS, 132},
+ {"artists", CONTENT_ARTISTS, 133},
+ {"movies", CONTENT_MOVIES, 20342},
+ {"tvshows", CONTENT_TVSHOWS, 20343},
+ {"musicvideos", CONTENT_MUSICVIDEOS, 20389}};
+
+std::string TranslateContent(const CONTENT_TYPE &type, bool pretty /*=false*/)
+{
+ for (const ContentMapping& map : content)
+ {
+ if (type == map.type)
+ {
+ if (pretty && map.pretty)
+ return g_localizeStrings.Get(map.pretty);
+ else
+ return map.name;
+ }
+ }
+ return "";
+}
+
+CONTENT_TYPE TranslateContent(const std::string &string)
+{
+ for (const ContentMapping& map : content)
+ {
+ if (string == map.name)
+ return map.type;
+ }
+ return CONTENT_NONE;
+}
+
+AddonType ScraperTypeFromContent(const CONTENT_TYPE& content)
+{
+ switch (content)
+ {
+ case CONTENT_ALBUMS:
+ return AddonType::SCRAPER_ALBUMS;
+ case CONTENT_ARTISTS:
+ return AddonType::SCRAPER_ARTISTS;
+ case CONTENT_MOVIES:
+ return AddonType::SCRAPER_MOVIES;
+ case CONTENT_MUSICVIDEOS:
+ return AddonType::SCRAPER_MUSICVIDEOS;
+ case CONTENT_TVSHOWS:
+ return AddonType::SCRAPER_TVSHOWS;
+ default:
+ return AddonType::UNKNOWN;
+ }
+}
+
+// if the XML root is <error>, throw CScraperError with enclosed <title>/<message> values
+static void CheckScraperError(const TiXmlElement *pxeRoot)
+{
+ if (!pxeRoot || StringUtils::CompareNoCase(pxeRoot->Value(), "error"))
+ return;
+ std::string sTitle;
+ std::string sMessage;
+ XMLUtils::GetString(pxeRoot, "title", sTitle);
+ XMLUtils::GetString(pxeRoot, "message", sMessage);
+ throw CScraperError(sTitle, sMessage);
+}
+
+CScraper::CScraper(const AddonInfoPtr& addonInfo, AddonType addonType)
+ : CAddon(addonInfo, addonType),
+ m_fLoaded(false),
+ m_requiressettings(false),
+ m_pathContent(CONTENT_NONE)
+{
+ m_requiressettings = addonInfo->Type(addonType)->GetValue("@requiressettings").asBoolean();
+
+ CDateTimeSpan persistence;
+ std::string tmp = addonInfo->Type(addonType)->GetValue("@cachepersistence").asString();
+ if (!tmp.empty())
+ m_persistence.SetFromTimeString(tmp);
+
+ switch (addonType)
+ {
+ case AddonType::SCRAPER_ALBUMS:
+ m_pathContent = CONTENT_ALBUMS;
+ break;
+ case AddonType::SCRAPER_ARTISTS:
+ m_pathContent = CONTENT_ARTISTS;
+ break;
+ case AddonType::SCRAPER_MOVIES:
+ m_pathContent = CONTENT_MOVIES;
+ break;
+ case AddonType::SCRAPER_MUSICVIDEOS:
+ m_pathContent = CONTENT_MUSICVIDEOS;
+ break;
+ case AddonType::SCRAPER_TVSHOWS:
+ m_pathContent = CONTENT_TVSHOWS;
+ break;
+ default:
+ break;
+ }
+
+ m_isPython = URIUtils::GetExtension(addonInfo->Type(addonType)->LibPath()) == ".py";
+}
+
+bool CScraper::Supports(const CONTENT_TYPE &content) const
+{
+ return Type() == ScraperTypeFromContent(content);
+}
+
+bool CScraper::SetPathSettings(CONTENT_TYPE content, const std::string &xml)
+{
+ m_pathContent = content;
+ if (!LoadSettings(false, false))
+ return false;
+
+ if (xml.empty())
+ return true;
+
+ CXBMCTinyXML doc;
+ doc.Parse(xml);
+ return SettingsFromXML(doc, false);
+}
+
+std::string CScraper::GetPathSettings()
+{
+ if (!LoadSettings(false, true))
+ return "";
+
+ std::stringstream stream;
+ CXBMCTinyXML doc;
+ SettingsToXML(doc);
+ if (doc.RootElement())
+ stream << *doc.RootElement();
+
+ return stream.str();
+}
+
+void CScraper::ClearCache()
+{
+ std::string strCachePath = URIUtils::AddFileToFolder(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cachePath, "scrapers");
+
+ // create scraper cache dir if needed
+ if (!CDirectory::Exists(strCachePath))
+ CDirectory::Create(strCachePath);
+
+ strCachePath = URIUtils::AddFileToFolder(strCachePath, ID());
+ URIUtils::AddSlashAtEnd(strCachePath);
+
+ if (CDirectory::Exists(strCachePath))
+ {
+ CFileItemList items;
+ CDirectory::GetDirectory(strCachePath, items, "", DIR_FLAG_DEFAULTS);
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ // wipe cache
+ if (items[i]->m_dateTime + m_persistence <= CDateTime::GetCurrentDateTime())
+ CFile::Delete(items[i]->GetDynPath());
+ }
+ }
+ else
+ CDirectory::Create(strCachePath);
+}
+
+// returns a vector of strings: the first is the XML output by the function; the rest
+// is XML output by chained functions, possibly recursively
+// the CCurlFile object is passed in so that URL fetches can be canceled from other threads
+// throws CScraperError abort on internal failures (e.g., parse errors)
+std::vector<std::string> CScraper::Run(const std::string &function,
+ const CScraperUrl &scrURL,
+ CCurlFile &http,
+ const std::vector<std::string> *extras)
+{
+ if (!Load())
+ throw CScraperError();
+
+ std::string strXML = InternalRun(function, scrURL, http, extras);
+ if (strXML.empty())
+ {
+ if (function != "NfoUrl" && function != "ResolveIDToUrl")
+ CLog::Log(LOGERROR, "{}: Unable to parse web site", __FUNCTION__);
+ throw CScraperError();
+ }
+
+ CLog::Log(LOGDEBUG, "scraper: {} returned {}", function, strXML);
+
+ CXBMCTinyXML doc;
+ /* all data was converted to UTF-8 before being processed by scraper */
+ doc.Parse(strXML, TIXML_ENCODING_UTF8);
+ if (!doc.RootElement())
+ {
+ CLog::Log(LOGERROR, "{}: Unable to parse XML", __FUNCTION__);
+ throw CScraperError();
+ }
+
+ std::vector<std::string> result;
+ result.push_back(strXML);
+ TiXmlElement *xchain = doc.RootElement()->FirstChildElement();
+ // skip children of the root element until <url> or <chain>
+ while (xchain && strcmp(xchain->Value(), "url") && strcmp(xchain->Value(), "chain"))
+ xchain = xchain->NextSiblingElement();
+ while (xchain)
+ {
+ // <chain|url function="...">param</>
+ const char *szFunction = xchain->Attribute("function");
+ if (szFunction)
+ {
+ CScraperUrl scrURL2;
+ std::vector<std::string> extras;
+ // for <chain>, pass the contained text as a parameter; for <url>, as URL content
+ if (strcmp(xchain->Value(), "chain") == 0)
+ {
+ if (xchain->FirstChild())
+ extras.emplace_back(xchain->FirstChild()->Value());
+ }
+ else
+ scrURL2.ParseAndAppendUrl(xchain);
+ // Fix for empty chains. $$1 would still contain the
+ // previous value as there is no child of the xml node.
+ // since $$1 will always either contain the data from an
+ // url or the parameters to a chain, we can safely clear it here
+ // to fix this issue
+ m_parser.m_param[0].clear();
+ std::vector<std::string> result2 = RunNoThrow(szFunction, scrURL2, http, &extras);
+ result.insert(result.end(), result2.begin(), result2.end());
+ }
+ xchain = xchain->NextSiblingElement();
+ // continue to skip past non-<url> or <chain> elements
+ while (xchain && strcmp(xchain->Value(), "url") && strcmp(xchain->Value(), "chain"))
+ xchain = xchain->NextSiblingElement();
+ }
+
+ return result;
+}
+
+// just like Run, but returns an empty list instead of throwing in case of error
+// don't use in new code; errors should be handled appropriately
+std::vector<std::string> CScraper::RunNoThrow(const std::string &function,
+ const CScraperUrl &url,
+ XFILE::CCurlFile &http,
+ const std::vector<std::string> *extras)
+{
+ std::vector<std::string> vcs;
+ try
+ {
+ vcs = Run(function, url, http, extras);
+ }
+ catch (const CScraperError &sce)
+ {
+ assert(sce.FAborted()); // the only kind we should get
+ }
+ return vcs;
+}
+
+std::string CScraper::InternalRun(const std::string &function,
+ const CScraperUrl &scrURL,
+ CCurlFile &http,
+ const std::vector<std::string> *extras)
+{
+ // walk the list of input URLs and fetch each into parser parameters
+ const auto& urls = scrURL.GetUrls();
+ size_t i;
+ for (i = 0; i < urls.size(); ++i)
+ {
+ if (!CScraperUrl::Get(urls[i], m_parser.m_param[i], http, ID()) ||
+ m_parser.m_param[i].empty())
+ return "";
+ }
+ // put the 'extra' parameters into the parser parameter list too
+ if (extras)
+ {
+ for (size_t j = 0; j < extras->size(); ++j)
+ m_parser.m_param[j + i] = (*extras)[j];
+ }
+
+ return m_parser.Parse(function, this);
+}
+
+std::string CScraper::GetPathSettingsAsJSON()
+{
+ static const std::string EmptyPathSettings = "{}";
+
+ if (!LoadSettings(false, true))
+ return EmptyPathSettings;
+
+ CSettingsValueFlatJsonSerializer jsonSerializer;
+ auto json = jsonSerializer.SerializeValues(GetSettings()->GetSettingsManager());
+ if (json.empty())
+ return EmptyPathSettings;
+
+ return json;
+}
+
+bool CScraper::Load()
+{
+ if (m_fLoaded || m_isPython)
+ return true;
+
+ bool result = m_parser.Load(LibPath());
+ if (result)
+ {
+ //! @todo this routine assumes that deps are a single level, and assumes the dep is installed.
+ //! 1. Does it make sense to have recursive dependencies?
+ //! 2. Should we be checking the dep versions or do we assume it is ok?
+ auto deps = GetDependencies();
+ auto itr = deps.begin();
+ while (itr != deps.end())
+ {
+ if (itr->id == "xbmc.metadata")
+ {
+ ++itr;
+ continue;
+ }
+ AddonPtr dep;
+
+ bool bOptional = itr->optional;
+
+ if (CServiceBroker::GetAddonMgr().GetAddon((*itr).id, dep, ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ CXBMCTinyXML doc;
+ if (dep->Type() == AddonType::SCRAPER_LIBRARY && doc.LoadFile(dep->LibPath()))
+ m_parser.AddDocument(&doc);
+ }
+ else
+ {
+ if (!bOptional)
+ {
+ result = false;
+ break;
+ }
+ }
+ ++itr;
+ }
+ }
+
+ if (!result)
+ CLog::Log(LOGWARNING, "failed to load scraper XML from {}", LibPath());
+ return m_fLoaded = result;
+}
+
+bool CScraper::IsInUse() const
+{
+ if (Supports(CONTENT_ALBUMS) || Supports(CONTENT_ARTISTS))
+ { // music scraper
+ CMusicDatabase db;
+ if (db.Open() && db.ScraperInUse(ID()))
+ return true;
+ }
+ else
+ { // video scraper
+ CVideoDatabase db;
+ if (db.Open() && db.ScraperInUse(ID()))
+ return true;
+ }
+ return false;
+}
+
+bool CScraper::IsNoop()
+{
+ if (!Load())
+ throw CScraperError();
+
+ return !m_isPython && m_parser.IsNoop();
+}
+
+// pass in contents of .nfo file; returns URL (possibly empty if none found)
+// and may populate strId, or throws CScraperError on error
+CScraperUrl CScraper::NfoUrl(const std::string &sNfoContent)
+{
+ CScraperUrl scurlRet;
+
+ if (IsNoop())
+ return scurlRet;
+
+ if (m_isPython)
+ {
+ std::stringstream str;
+ str << "plugin://" << ID() << "?action=NfoUrl&nfo=" << CURL::Encode(sNfoContent)
+ << "&pathSettings=" << CURL::Encode(GetPathSettingsAsJSON());
+
+ CFileItemList items;
+ if (!XFILE::CDirectory::GetDirectory(str.str(), items, "", DIR_FLAG_DEFAULTS))
+ return scurlRet;
+
+ if (items.Size() == 0)
+ return scurlRet;
+ if (items.Size() > 1)
+ CLog::Log(LOGWARNING, "{}: scraper returned multiple results; using first", __FUNCTION__);
+
+ CScraperUrl::SUrlEntry surl;
+ surl.m_type = CScraperUrl::UrlType::General;
+ surl.m_url = items[0]->GetDynPath();
+ scurlRet.AppendUrl(surl);
+ return scurlRet;
+ }
+
+ // scraper function takes contents of .nfo file, returns XML (see below)
+ std::vector<std::string> vcsIn;
+ vcsIn.push_back(sNfoContent);
+ CScraperUrl scurl;
+ CCurlFile fcurl;
+ std::vector<std::string> vcsOut = Run("NfoUrl", scurl, fcurl, &vcsIn);
+ if (vcsOut.empty() || vcsOut[0].empty())
+ return scurlRet;
+ if (vcsOut.size() > 1)
+ CLog::Log(LOGWARNING, "{}: scraper returned multiple results; using first", __FUNCTION__);
+
+ // parse returned XML: either <error> element on error, blank on failure,
+ // or <url>...</url> or <url>...</url><id>...</id> on success
+ for (size_t i = 0; i < vcsOut.size(); ++i)
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(vcsOut[i], TIXML_ENCODING_UTF8);
+ CheckScraperError(doc.RootElement());
+
+ if (doc.RootElement())
+ {
+ /*
+ NOTE: Scrapers might return invalid xml with some loose
+ elements (eg. '<url>http://some.url</url><id>123</id>').
+ Since XMLUtils::GetString() is assuming well formed xml
+ with start and end-tags we're not able to use it.
+ Check for the desired Elements instead.
+ */
+ TiXmlElement* pxeUrl = nullptr;
+ TiXmlElement* pId = nullptr;
+ if (!strcmp(doc.RootElement()->Value(), "details"))
+ {
+ pxeUrl = doc.RootElement()->FirstChildElement("url");
+ pId = doc.RootElement()->FirstChildElement("id");
+ }
+ else
+ {
+ pId = doc.FirstChildElement("id");
+ pxeUrl = doc.FirstChildElement("url");
+ }
+ if (pId && pId->FirstChild())
+ scurlRet.SetId(pId->FirstChild()->ValueStr());
+
+ if (pxeUrl && pxeUrl->Attribute("function"))
+ continue;
+
+ if (pxeUrl)
+ scurlRet.ParseAndAppendUrl(pxeUrl);
+ else if (!strcmp(doc.RootElement()->Value(), "url"))
+ scurlRet.ParseAndAppendUrl(doc.RootElement());
+ else
+ continue;
+ break;
+ }
+ }
+ return scurlRet;
+}
+
+CScraperUrl CScraper::ResolveIDToUrl(const std::string &externalID)
+{
+ CScraperUrl scurlRet;
+
+ if (m_isPython)
+ {
+ std::stringstream str;
+ str << "plugin://" << ID() << "?action=resolveid&key=" << CURL::Encode(externalID)
+ << "&pathSettings=" << CURL::Encode(GetPathSettingsAsJSON());
+
+ CFileItem item("resolve me", false);
+
+ if (XFILE::CPluginDirectory::GetPluginResult(str.str(), item, false))
+ scurlRet.ParseFromData(item.GetDynPath());
+
+ return scurlRet;
+ }
+
+ // scraper function takes an external ID, returns XML (see below)
+ std::vector<std::string> vcsIn;
+ vcsIn.push_back(externalID);
+ CScraperUrl scurl;
+ CCurlFile fcurl;
+ std::vector<std::string> vcsOut = Run("ResolveIDToUrl", scurl, fcurl, &vcsIn);
+ if (vcsOut.empty() || vcsOut[0].empty())
+ return scurlRet;
+ if (vcsOut.size() > 1)
+ CLog::Log(LOGWARNING, "{}: scraper returned multiple results; using first", __FUNCTION__);
+
+ // parse returned XML: either <error> element on error, blank on failure,
+ // or <url>...</url> or <url>...</url><id>...</id> on success
+ for (size_t i = 0; i < vcsOut.size(); ++i)
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(vcsOut[i], TIXML_ENCODING_UTF8);
+ CheckScraperError(doc.RootElement());
+
+ if (doc.RootElement())
+ {
+ /*
+ NOTE: Scrapers might return invalid xml with some loose
+ elements (eg. '<url>http://some.url</url><id>123</id>').
+ Since XMLUtils::GetString() is assuming well formed xml
+ with start and end-tags we're not able to use it.
+ Check for the desired Elements instead.
+ */
+ TiXmlElement* pxeUrl = nullptr;
+ TiXmlElement* pId = nullptr;
+ if (!strcmp(doc.RootElement()->Value(), "details"))
+ {
+ pxeUrl = doc.RootElement()->FirstChildElement("url");
+ pId = doc.RootElement()->FirstChildElement("id");
+ }
+ else
+ {
+ pId = doc.FirstChildElement("id");
+ pxeUrl = doc.FirstChildElement("url");
+ }
+ if (pId && pId->FirstChild())
+ scurlRet.SetId(pId->FirstChild()->ValueStr());
+
+ if (pxeUrl && pxeUrl->Attribute("function"))
+ continue;
+
+ if (pxeUrl)
+ scurlRet.ParseAndAppendUrl(pxeUrl);
+ else if (!strcmp(doc.RootElement()->Value(), "url"))
+ scurlRet.ParseAndAppendUrl(doc.RootElement());
+ else
+ continue;
+ break;
+ }
+ }
+ return scurlRet;
+}
+
+static bool RelevanceSortFunction(const CScraperUrl &left, const CScraperUrl &right)
+{
+ return left.GetRelevance() > right.GetRelevance();
+}
+
+template<class T>
+static T FromFileItem(const CFileItem &item);
+
+template<>
+CScraperUrl FromFileItem<CScraperUrl>(const CFileItem &item)
+{
+ CScraperUrl url;
+
+ url.SetTitle(item.GetLabel());
+ if (item.HasProperty("relevance"))
+ url.SetRelevance(item.GetProperty("relevance").asDouble());
+ CScraperUrl::SUrlEntry surl;
+ surl.m_type = CScraperUrl::UrlType::General;
+ surl.m_url = item.GetDynPath();
+ url.AppendUrl(surl);
+
+ return url;
+}
+
+template<>
+CMusicAlbumInfo FromFileItem<CMusicAlbumInfo>(const CFileItem &item)
+{
+ CMusicAlbumInfo info;
+ const std::string& sTitle = item.GetLabel();
+ std::string sArtist = item.GetProperty("album.artist").asString();
+ std::string sAlbumName;
+ if (!sArtist.empty())
+ sAlbumName = StringUtils::Format("{} - {}", sArtist, sTitle);
+ else
+ sAlbumName = sTitle;
+
+ CScraperUrl url;
+ url.AppendUrl(CScraperUrl::SUrlEntry(item.GetDynPath()));
+
+ info = CMusicAlbumInfo(sTitle, sArtist, sAlbumName, url);
+ if (item.HasProperty("relevance"))
+ info.SetRelevance(item.GetProperty("relevance").asFloat());
+
+ if (item.HasProperty("album.releasestatus"))
+ info.GetAlbum().strReleaseStatus = item.GetProperty("album.releasestatus").asString();
+ if (item.HasProperty("album.type"))
+ info.GetAlbum().strType = item.GetProperty("album.type").asString();
+ if (item.HasProperty("album.year"))
+ info.GetAlbum().strReleaseDate = item.GetProperty("album.year").asString();
+ if (item.HasProperty("album.label"))
+ info.GetAlbum().strLabel = item.GetProperty("album.label").asString();
+ info.GetAlbum().art = item.GetArt();
+
+ return info;
+}
+
+template<>
+CMusicArtistInfo FromFileItem<CMusicArtistInfo>(const CFileItem &item)
+{
+ CMusicArtistInfo info;
+ const std::string& sTitle = item.GetLabel();
+
+ CScraperUrl url;
+ url.AppendUrl(CScraperUrl::SUrlEntry(item.GetDynPath()));
+
+ info = CMusicArtistInfo(sTitle, url);
+ if (item.HasProperty("artist.genre"))
+ info.GetArtist().genre = StringUtils::Split(item.GetProperty("artist.genre").asString(),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ if (item.HasProperty("artist.disambiguation"))
+ info.GetArtist().strDisambiguation = item.GetProperty("artist.disambiguation").asString();
+ if (item.HasProperty("artist.type"))
+ info.GetArtist().strType = item.GetProperty("artist.type").asString();
+ if (item.HasProperty("artist.gender"))
+ info.GetArtist().strGender = item.GetProperty("artist.gender").asString();
+ if (item.HasProperty("artist.born"))
+ info.GetArtist().strBorn = item.GetProperty("artist.born").asString();
+
+ return info;
+}
+
+template<class T>
+static std::vector<T> PythonFind(const std::string &ID,
+ const std::map<std::string, std::string> &additionals)
+{
+ std::vector<T> result;
+ CFileItemList items;
+ std::stringstream str;
+ str << "plugin://" << ID << "?action=find";
+ for (const auto &it : additionals)
+ str << "&" << it.first << "=" << CURL::Encode(it.second);
+
+ if (XFILE::CDirectory::GetDirectory(str.str(), items, "", DIR_FLAG_DEFAULTS))
+ {
+ for (const auto& it : items)
+ result.emplace_back(std::move(FromFileItem<T>(*it)));
+ }
+
+ return result;
+}
+
+static std::string FromString(const CFileItem &item, const std::string &key)
+{
+ return item.GetProperty(key).asString();
+}
+
+static std::vector<std::string> FromArray(const CFileItem &item, const std::string &key, int sep)
+{
+ return StringUtils::Split(item.GetProperty(key).asString(),
+ sep ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator
+ : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+}
+
+static void ParseThumbs(CScraperUrl &scurl,
+ const CFileItem &item,
+ int nThumbs,
+ const std::string &tag)
+{
+ for (int i = 0; i < nThumbs; ++i)
+ {
+ std::stringstream prefix;
+ prefix << tag << i + 1;
+ std::string url = FromString(item, prefix.str() + ".url");
+ std::string aspect = FromString(item, prefix.str() + ".aspect");
+ std::string preview = FromString(item, prefix.str() + ".preview");
+ scurl.AddParsedUrl(url, aspect, preview);
+ }
+}
+
+static std::string ParseFanart(const CFileItem &item, int nFanart, const std::string &tag)
+{
+ std::string result;
+ TiXmlElement fanart("fanart");
+ for (int i = 0; i < nFanart; ++i)
+ {
+ std::stringstream prefix;
+ prefix << tag << i + 1;
+ std::string url = FromString(item, prefix.str() + ".url");
+ std::string preview = FromString(item, prefix.str() + ".preview");
+ TiXmlElement thumb("thumb");
+ thumb.SetAttribute("preview", preview);
+ TiXmlText text(url);
+ thumb.InsertEndChild(text);
+ fanart.InsertEndChild(thumb);
+ }
+ result << fanart;
+
+ return result;
+}
+
+template<class T>
+static void DetailsFromFileItem(const CFileItem &, T &);
+
+template<>
+void DetailsFromFileItem<CAlbum>(const CFileItem &item, CAlbum &album)
+{
+ album.strAlbum = item.GetLabel();
+ album.strMusicBrainzAlbumID = FromString(item, "album.musicbrainzid");
+ album.strReleaseGroupMBID = FromString(item, "album.releasegroupid");
+
+ int nArtists = item.GetProperty("album.artists").asInteger32();
+ album.artistCredits.reserve(nArtists);
+ for (int i = 0; i < nArtists; ++i)
+ {
+ std::stringstream prefix;
+ prefix << "album.artist" << i + 1;
+ CArtistCredit artistCredit;
+ artistCredit.SetArtist(FromString(item, prefix.str() + ".name"));
+ artistCredit.SetMusicBrainzArtistID(FromString(item, prefix.str() + ".musicbrainzid"));
+ album.artistCredits.push_back(artistCredit);
+ }
+
+ album.strArtistDesc = FromString(item, "album.artist_description");
+ album.genre = FromArray(item, "album.genre", 0);
+ album.styles = FromArray(item, "album.styles", 0);
+ album.moods = FromArray(item, "album.moods", 0);
+ album.themes = FromArray(item, "album.themes", 0);
+ album.bCompilation = item.GetProperty("album.compilation").asBoolean();
+ album.strReview = FromString(item, "album.review");
+ album.strReleaseDate = FromString(item, "album.releasedate");
+ if (album.strReleaseDate.empty())
+ album.strReleaseDate = FromString(item, "album.year");
+ album.strOrigReleaseDate = FromString(item, "album.originaldate");
+ album.strLabel = FromString(item, "album.label");
+ album.strType = FromString(item, "album.type");
+ album.strReleaseStatus = FromString(item, "album.releasestatus");
+ album.fRating = item.GetProperty("album.rating").asFloat();
+ album.iUserrating = item.GetProperty("album.user_rating").asInteger32();
+ album.iVotes = item.GetProperty("album.votes").asInteger32();
+
+ /* Scrapers fetch a list of possible art but do not set the current images used because art
+ selection depends on other preferences so is handled by CMusicInfoScanner
+ album.art = item.GetArt();
+ */
+
+ int nThumbs = item.GetProperty("album.thumbs").asInteger32();
+ ParseThumbs(album.thumbURL, item, nThumbs, "album.thumb");
+}
+
+template<>
+void DetailsFromFileItem<CArtist>(const CFileItem &item, CArtist &artist)
+{
+ artist.strArtist = item.GetLabel();
+ artist.strMusicBrainzArtistID = FromString(item, "artist.musicbrainzid");
+ artist.strDisambiguation = FromString(item, "artist.disambiguation");
+ artist.strType = FromString(item, "artist.type");
+ artist.strGender = FromString(item, "artist.gender");
+ artist.genre = FromArray(item, "artist.genre", 0);
+ artist.styles = FromArray(item, "artist.styles", 0);
+ artist.moods = FromArray(item, "artist.moods", 0);
+ artist.yearsActive = FromArray(item, "artist.years_active", 0);
+ artist.instruments = FromArray(item, "artist.instruments", 0);
+ artist.strBorn = FromString(item, "artist.born");
+ artist.strFormed = FromString(item, "artist.formed");
+ artist.strBiography = FromString(item, "artist.biography");
+ artist.strDied = FromString(item, "artist.died");
+ artist.strDisbanded = FromString(item, "artist.disbanded");
+
+ /* Scrapers fetch a list of possible art but do not set the current images used because art
+ selection depends on other preferences so is handled by CMusicInfoScanner
+ artist.art = item.GetArt();
+ */
+
+ int nAlbums = item.GetProperty("artist.albums").asInteger32();
+ artist.discography.reserve(nAlbums);
+ for (int i = 0; i < nAlbums; ++i)
+ {
+ std::stringstream prefix;
+ prefix << "artist.album" << i + 1;
+ CDiscoAlbum discoAlbum;
+ discoAlbum.strAlbum = FromString(item, prefix.str() + ".title");
+ discoAlbum.strYear = FromString(item, prefix.str() + ".year");
+ discoAlbum.strReleaseGroupMBID = FromString(item, prefix.str() + ".musicbrainzreleasegroupid");
+ artist.discography.emplace_back(discoAlbum);
+ }
+
+ int nThumbs = item.GetProperty("artist.thumbs").asInteger32();
+ ParseThumbs(artist.thumbURL, item, nThumbs, "artist.thumb");
+
+ // Support deprecated fanarts property, add to artist.thumbURL
+ int nFanart = item.GetProperty("artist.fanarts").asInteger32();
+ if (nFanart > 0)
+ {
+ CFanart fanart;
+ fanart.m_xml = ParseFanart(item, nFanart, "artist.fanart");
+ fanart.Unpack();
+ for (unsigned int i = 0; i < fanart.GetNumFanarts(); i++)
+ artist.thumbURL.AddParsedUrl(fanart.GetImageURL(i), "fanart", fanart.GetPreviewURL(i));
+ }
+}
+
+template<>
+void DetailsFromFileItem<CVideoInfoTag>(const CFileItem &item, CVideoInfoTag &tag)
+{
+ if (item.HasVideoInfoTag())
+ tag = *item.GetVideoInfoTag();
+}
+
+template<class T>
+static bool PythonDetails(const std::string &ID,
+ const std::string &key,
+ const std::string &url,
+ const std::string &action,
+ const std::string &pathSettings,
+ T &result)
+{
+ std::stringstream str;
+ str << "plugin://" << ID << "?action=" << action << "&" << key << "=" << CURL::Encode(url);
+ str << "&pathSettings=" << CURL::Encode(pathSettings);
+
+ CFileItem item(url, false);
+
+ if (!XFILE::CPluginDirectory::GetPluginResult(str.str(), item, false))
+ return false;
+
+ DetailsFromFileItem(item, result);
+ return true;
+}
+
+// fetch list of matching movies sorted by relevance (may be empty);
+// throws CScraperError on error; first called with fFirst set, then unset if first try fails
+std::vector<CScraperUrl> CScraper::FindMovie(XFILE::CCurlFile &fcurl,
+ const std::string &movieTitle, int movieYear,
+ bool fFirst)
+{
+ // prepare parameters for URL creation
+ std::string sTitle, sYear;
+ if (movieYear < 0)
+ {
+ std::string sTitleYear;
+ CUtil::CleanString(movieTitle, sTitle, sTitleYear, sYear, true /*fRemoveExt*/, fFirst);
+ }
+ else
+ {
+ sTitle = movieTitle;
+ sYear = std::to_string( movieYear );
+ }
+
+ CLog::Log(LOGDEBUG,
+ "{}: Searching for '{}' using {} scraper "
+ "(path: '{}', content: '{}', version: '{}')",
+ __FUNCTION__, sTitle, Name(), Path(), ADDON::TranslateContent(Content()),
+ Version().asString());
+
+ std::vector<CScraperUrl> vcscurl;
+ if (IsNoop())
+ return vcscurl;
+
+ if (!fFirst)
+ StringUtils::Replace(sTitle, '-', ' ');
+
+ if (m_isPython)
+ {
+ std::map<std::string, std::string> additionals{{"title", sTitle}};
+ if (!sYear.empty())
+ additionals.insert({"year", sYear});
+ additionals.emplace("pathSettings", GetPathSettingsAsJSON());
+ return PythonFind<CScraperUrl>(ID(), additionals);
+ }
+
+ std::vector<std::string> vcsIn(1);
+ g_charsetConverter.utf8To(SearchStringEncoding(), sTitle, vcsIn[0]);
+ vcsIn[0] = CURL::Encode(vcsIn[0]);
+ if (fFirst && !sYear.empty())
+ vcsIn.push_back(sYear);
+
+ // request a search URL from the title/filename/etc.
+ CScraperUrl scurl;
+ std::vector<std::string> vcsOut = Run("CreateSearchUrl", scurl, fcurl, &vcsIn);
+ if (vcsOut.empty())
+ {
+ CLog::Log(LOGDEBUG, "{}: CreateSearchUrl failed", __FUNCTION__);
+ throw CScraperError();
+ }
+ scurl.ParseFromData(vcsOut[0]);
+
+ // do the search, and parse the result into a list
+ vcsIn.clear();
+ vcsIn.push_back(scurl.GetFirstThumbUrl());
+ vcsOut = Run("GetSearchResults", scurl, fcurl, &vcsIn);
+
+ bool fSort(true);
+ std::set<std::string> stsDupeCheck;
+ bool fResults(false);
+ for (std::vector<std::string>::const_iterator i = vcsOut.begin(); i != vcsOut.end(); ++i)
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(*i, TIXML_ENCODING_UTF8);
+ if (!doc.RootElement())
+ {
+ CLog::Log(LOGERROR, "{}: Unable to parse XML", __FUNCTION__);
+ continue; // might have more valid results later
+ }
+
+ CheckScraperError(doc.RootElement());
+
+ TiXmlHandle xhDoc(&doc);
+ TiXmlHandle xhResults = xhDoc.FirstChild("results");
+ if (!xhResults.Element())
+ continue;
+ fResults = true; // even if empty
+
+ // we need to sort if returned results don't specify 'sorted="yes"'
+ if (fSort)
+ {
+ const char *sorted = xhResults.Element()->Attribute("sorted");
+ if (sorted != nullptr)
+ fSort = !StringUtils::EqualsNoCase(sorted, "yes");
+ }
+
+ for (TiXmlElement *pxeMovie = xhResults.FirstChild("entity").Element(); pxeMovie;
+ pxeMovie = pxeMovie->NextSiblingElement())
+ {
+ TiXmlNode *pxnTitle = pxeMovie->FirstChild("title");
+ TiXmlElement *pxeLink = pxeMovie->FirstChildElement("url");
+ if (pxnTitle && pxnTitle->FirstChild() && pxeLink && pxeLink->FirstChild())
+ {
+ CScraperUrl scurlMovie;
+ auto title = pxnTitle->FirstChild()->ValueStr();
+ std::string id;
+ if (XMLUtils::GetString(pxeMovie, "id", id))
+ scurlMovie.SetId(id);
+
+ for (; pxeLink && pxeLink->FirstChild(); pxeLink = pxeLink->NextSiblingElement("url"))
+ scurlMovie.ParseAndAppendUrl(pxeLink);
+
+ // calculate the relevance of this hit
+ std::string sCompareTitle = scurlMovie.GetTitle();
+ StringUtils::ToLower(sCompareTitle);
+ std::string sMatchTitle = sTitle;
+ StringUtils::ToLower(sMatchTitle);
+
+ /*
+ * Identify the best match by performing a fuzzy string compare on the search term and
+ * the result. Additionally, use the year (if available) to further refine the best match.
+ * An exact match scores 1, a match off by a year scores 0.5 (release dates can vary between
+ * countries), otherwise it scores 0.
+ */
+ std::string sCompareYear;
+ XMLUtils::GetString(pxeMovie, "year", sCompareYear);
+
+ double yearScore = 0;
+ if (!sYear.empty() && !sCompareYear.empty())
+ yearScore =
+ std::max(0.0, 1 - 0.5 * abs(atoi(sYear.c_str()) - atoi(sCompareYear.c_str())));
+
+ scurlMovie.SetRelevance(fstrcmp(sMatchTitle.c_str(), sCompareTitle.c_str()) + yearScore);
+
+ // reconstruct a title for the user
+ if (!sCompareYear.empty())
+ title += StringUtils::Format(" ({})", sCompareYear);
+
+ std::string sLanguage;
+ if (XMLUtils::GetString(pxeMovie, "language", sLanguage) && !sLanguage.empty())
+ title += StringUtils::Format(" ({})", sLanguage);
+
+ // filter for dupes from naughty scrapers
+ if (stsDupeCheck.insert(scurlMovie.GetFirstThumbUrl() + " " + title).second)
+ {
+ scurlMovie.SetTitle(title);
+ vcscurl.push_back(scurlMovie);
+ }
+ }
+ }
+ }
+
+ if (!fResults)
+ throw CScraperError(); // scraper aborted
+
+ if (fSort)
+ std::stable_sort(vcscurl.begin(), vcscurl.end(), RelevanceSortFunction);
+
+ return vcscurl;
+}
+
+// find album by artist, using fcurl for web fetches
+// returns a list of albums (empty if no match or failure)
+std::vector<CMusicAlbumInfo> CScraper::FindAlbum(CCurlFile &fcurl,
+ const std::string &sAlbum,
+ const std::string &sArtist)
+{
+ CLog::Log(LOGDEBUG,
+ "{}: Searching for '{} - {}' using {} scraper "
+ "(path: '{}', content: '{}', version: '{}')",
+ __FUNCTION__, sArtist, sAlbum, Name(), Path(), ADDON::TranslateContent(Content()),
+ Version().asString());
+
+ std::vector<CMusicAlbumInfo> vcali;
+ if (IsNoop())
+ return vcali;
+
+ if (m_isPython)
+ return PythonFind<CMusicAlbumInfo>(ID(),
+ {{"title", sAlbum}, {"artist", sArtist}, {"pathSettings", GetPathSettingsAsJSON()}});
+
+ // scraper function is given the album and artist as parameters and
+ // returns an XML <url> element parseable by CScraperUrl
+ std::vector<std::string> extras(2);
+ g_charsetConverter.utf8To(SearchStringEncoding(), sAlbum, extras[0]);
+ g_charsetConverter.utf8To(SearchStringEncoding(), sArtist, extras[1]);
+ extras[0] = CURL::Encode(extras[0]);
+ extras[1] = CURL::Encode(extras[1]);
+ CScraperUrl scurl;
+ std::vector<std::string> vcsOut = RunNoThrow("CreateAlbumSearchUrl", scurl, fcurl, &extras);
+ if (vcsOut.size() > 1)
+ CLog::Log(LOGWARNING, "{}: scraper returned multiple results; using first", __FUNCTION__);
+
+ if (vcsOut.empty() || vcsOut[0].empty())
+ return vcali;
+ scurl.ParseFromData(vcsOut[0]);
+
+ // the next function is passed the contents of the returned URL, and returns
+ // an empty string on failure; on success, returns XML matches in the form:
+ // <results>
+ // <entity>
+ // <title>...</title>
+ // <url>...</url> (with the usual CScraperUrl decorations like post or spoof)
+ // <artist>...</artist>
+ // <year>...</year>
+ // <relevance [scale="..."]>...</relevance> (scale defaults to 1; score is divided by it)
+ // </entity>
+ // ...
+ // </results>
+ vcsOut = RunNoThrow("GetAlbumSearchResults", scurl, fcurl);
+
+ // parse the returned XML into a vector of album objects
+ for (std::vector<std::string>::const_iterator i = vcsOut.begin(); i != vcsOut.end(); ++i)
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(*i, TIXML_ENCODING_UTF8);
+ TiXmlHandle xhDoc(&doc);
+
+ for (TiXmlElement *pxeAlbum = xhDoc.FirstChild("results").FirstChild("entity").Element();
+ pxeAlbum; pxeAlbum = pxeAlbum->NextSiblingElement())
+ {
+ std::string sTitle;
+ if (XMLUtils::GetString(pxeAlbum, "title", sTitle) && !sTitle.empty())
+ {
+ std::string sArtist;
+ std::string sAlbumName;
+ if (XMLUtils::GetString(pxeAlbum, "artist", sArtist) && !sArtist.empty())
+ sAlbumName = StringUtils::Format("{} - {}", sArtist, sTitle);
+ else
+ sAlbumName = sTitle;
+
+ std::string sYear;
+ if (XMLUtils::GetString(pxeAlbum, "year", sYear) && !sYear.empty())
+ sAlbumName = StringUtils::Format("{} ({})", sAlbumName, sYear);
+
+ // if no URL is provided, use the URL we got back from CreateAlbumSearchUrl
+ // (e.g., in case we only got one result back and were sent to the detail page)
+ TiXmlElement *pxeLink = pxeAlbum->FirstChildElement("url");
+ CScraperUrl scurlAlbum;
+ if (!pxeLink)
+ scurlAlbum.ParseFromData(scurl.GetData());
+ for (; pxeLink && pxeLink->FirstChild(); pxeLink = pxeLink->NextSiblingElement("url"))
+ scurlAlbum.ParseAndAppendUrl(pxeLink);
+
+ if (!scurlAlbum.HasUrls())
+ continue;
+
+ CMusicAlbumInfo ali(sTitle, sArtist, sAlbumName, scurlAlbum);
+
+ TiXmlElement *pxeRel = pxeAlbum->FirstChildElement("relevance");
+ if (pxeRel && pxeRel->FirstChild())
+ {
+ const char *szScale = pxeRel->Attribute("scale");
+ float flScale = szScale ? float(atof(szScale)) : 1;
+ ali.SetRelevance(float(atof(pxeRel->FirstChild()->Value())) / flScale);
+ }
+
+ vcali.push_back(ali);
+ }
+ }
+ }
+ return vcali;
+}
+
+// find artist, using fcurl for web fetches
+// returns a list of artists (empty if no match or failure)
+std::vector<CMusicArtistInfo> CScraper::FindArtist(CCurlFile &fcurl, const std::string &sArtist)
+{
+ CLog::Log(LOGDEBUG,
+ "{}: Searching for '{}' using {} scraper "
+ "(file: '{}', content: '{}', version: '{}')",
+ __FUNCTION__, sArtist, Name(), Path(), ADDON::TranslateContent(Content()),
+ Version().asString());
+
+ std::vector<CMusicArtistInfo> vcari;
+ if (IsNoop())
+ return vcari;
+
+ if (m_isPython)
+ return PythonFind<CMusicArtistInfo>(ID(),
+ {{"artist", sArtist}, {"pathSettings", GetPathSettingsAsJSON()}});
+
+ // scraper function is given the artist as parameter and
+ // returns an XML <url> element parseable by CScraperUrl
+ std::vector<std::string> extras(1);
+ g_charsetConverter.utf8To(SearchStringEncoding(), sArtist, extras[0]);
+ extras[0] = CURL::Encode(extras[0]);
+ CScraperUrl scurl;
+ std::vector<std::string> vcsOut = RunNoThrow("CreateArtistSearchUrl", scurl, fcurl, &extras);
+
+ if (vcsOut.empty() || vcsOut[0].empty())
+ return vcari;
+ scurl.ParseFromData(vcsOut[0]);
+
+ // the next function is passed the contents of the returned URL, and returns
+ // an empty string on failure; on success, returns XML matches in the form:
+ // <results>
+ // <entity>
+ // <title>...</title>
+ // <year>...</year>
+ // <genre>...</genre>
+ // <disambiguation>...</disambiguation>
+ // <url>...</url> (with the usual CScraperUrl decorations like post or spoof)
+ // </entity>
+ // ...
+ // </results>
+ vcsOut = RunNoThrow("GetArtistSearchResults", scurl, fcurl);
+
+ // parse the returned XML into a vector of artist objects
+ for (std::vector<std::string>::const_iterator i = vcsOut.begin(); i != vcsOut.end(); ++i)
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(*i, TIXML_ENCODING_UTF8);
+ if (!doc.RootElement())
+ {
+ CLog::Log(LOGERROR, "{}: Unable to parse XML", __FUNCTION__);
+ return vcari;
+ }
+ TiXmlHandle xhDoc(&doc);
+ for (TiXmlElement *pxeArtist = xhDoc.FirstChild("results").FirstChild("entity").Element();
+ pxeArtist; pxeArtist = pxeArtist->NextSiblingElement())
+ {
+ TiXmlNode *pxnTitle = pxeArtist->FirstChild("title");
+ if (pxnTitle && pxnTitle->FirstChild())
+ {
+ CScraperUrl scurlArtist;
+
+ TiXmlElement *pxeLink = pxeArtist->FirstChildElement("url");
+ if (!pxeLink)
+ scurlArtist.ParseFromData(scurl.GetData());
+ for (; pxeLink && pxeLink->FirstChild(); pxeLink = pxeLink->NextSiblingElement("url"))
+ scurlArtist.ParseAndAppendUrl(pxeLink);
+
+ if (!scurlArtist.HasUrls())
+ continue;
+
+ CMusicArtistInfo ari(pxnTitle->FirstChild()->Value(), scurlArtist);
+ std::string genre;
+ XMLUtils::GetString(pxeArtist, "genre", genre);
+ if (!genre.empty())
+ ari.GetArtist().genre =
+ StringUtils::Split(genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ XMLUtils::GetString(pxeArtist, "disambiguation", ari.GetArtist().strDisambiguation);
+ XMLUtils::GetString(pxeArtist, "year", ari.GetArtist().strBorn);
+
+ vcari.push_back(ari);
+ }
+ }
+ }
+ return vcari;
+}
+
+// fetch list of episodes from URL (from video database)
+EPISODELIST CScraper::GetEpisodeList(XFILE::CCurlFile &fcurl, const CScraperUrl &scurl)
+{
+ EPISODELIST vcep;
+ if (!scurl.HasUrls())
+ return vcep;
+
+ CLog::Log(LOGDEBUG,
+ "{}: Searching '{}' using {} scraper "
+ "(file: '{}', content: '{}', version: '{}')",
+ __FUNCTION__, scurl.GetFirstThumbUrl(), Name(), Path(),
+ ADDON::TranslateContent(Content()), Version().asString());
+
+ if (m_isPython)
+ {
+ std::stringstream str;
+ str << "plugin://" << ID()
+ << "?action=getepisodelist&url=" << CURL::Encode(scurl.GetFirstThumbUrl())
+ << "&pathSettings=" << CURL::Encode(GetPathSettingsAsJSON());
+
+ CFileItemList items;
+ if (!XFILE::CDirectory::GetDirectory(str.str(), items, "", DIR_FLAG_DEFAULTS))
+ return vcep;
+
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ EPISODE ep;
+ const auto& tag = *items[i]->GetVideoInfoTag();
+ ep.strTitle = tag.m_strTitle;
+ ep.iSeason = tag.m_iSeason;
+ ep.iEpisode = tag.m_iEpisode;
+ ep.cDate = tag.m_firstAired;
+ ep.iSubepisode = items[i]->GetProperty("video.sub_episode").asInteger();
+ CScraperUrl::SUrlEntry surl;
+ surl.m_type = CScraperUrl::UrlType::General;
+ surl.m_url = items[i]->GetURL().Get();
+ ep.cScraperUrl.AppendUrl(surl);
+ vcep.push_back(ep);
+ }
+
+ return vcep;
+ }
+
+ std::vector<std::string> vcsIn;
+ vcsIn.push_back(scurl.GetFirstThumbUrl());
+ std::vector<std::string> vcsOut = RunNoThrow("GetEpisodeList", scurl, fcurl, &vcsIn);
+
+ // parse the XML response
+ for (std::vector<std::string>::const_iterator i = vcsOut.begin(); i != vcsOut.end(); ++i)
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(*i);
+ if (!doc.RootElement())
+ {
+ CLog::Log(LOGERROR, "{}: Unable to parse XML", __FUNCTION__);
+ continue;
+ }
+
+ TiXmlHandle xhDoc(&doc);
+ for (TiXmlElement *pxeMovie = xhDoc.FirstChild("episodeguide").FirstChild("episode").Element();
+ pxeMovie; pxeMovie = pxeMovie->NextSiblingElement())
+ {
+ EPISODE ep;
+ TiXmlElement *pxeLink = pxeMovie->FirstChildElement("url");
+ std::string strEpNum;
+ if (pxeLink && XMLUtils::GetInt(pxeMovie, "season", ep.iSeason) &&
+ XMLUtils::GetString(pxeMovie, "epnum", strEpNum) && !strEpNum.empty())
+ {
+ CScraperUrl &scurlEp(ep.cScraperUrl);
+ size_t dot = strEpNum.find('.');
+ ep.iEpisode = atoi(strEpNum.c_str());
+ ep.iSubepisode = (dot != std::string::npos) ? atoi(strEpNum.substr(dot + 1).c_str()) : 0;
+ std::string title;
+ if (!XMLUtils::GetString(pxeMovie, "title", title) || title.empty())
+ title = g_localizeStrings.Get(10005); // Not available
+ scurlEp.SetTitle(title);
+ std::string id;
+ if (XMLUtils::GetString(pxeMovie, "id", id))
+ scurlEp.SetId(id);
+
+ for (; pxeLink && pxeLink->FirstChild(); pxeLink = pxeLink->NextSiblingElement("url"))
+ scurlEp.ParseAndAppendUrl(pxeLink);
+
+ // date must be the format of yyyy-mm-dd
+ ep.cDate.SetValid(false);
+ std::string sDate;
+ if (XMLUtils::GetString(pxeMovie, "aired", sDate) && sDate.length() == 10)
+ {
+ tm tm;
+ if (strptime(sDate.c_str(), "%Y-%m-%d", &tm))
+ ep.cDate.SetDate(1900 + tm.tm_year, tm.tm_mon + 1, tm.tm_mday);
+ }
+ vcep.push_back(ep);
+ }
+ }
+ }
+
+ return vcep;
+}
+
+// takes URL; returns true and populates video details on success, false otherwise
+bool CScraper::GetVideoDetails(XFILE::CCurlFile &fcurl,
+ const CScraperUrl &scurl,
+ bool fMovie /*else episode*/,
+ CVideoInfoTag &video)
+{
+ CLog::Log(LOGDEBUG,
+ "{}: Reading {} '{}' using {} scraper "
+ "(file: '{}', content: '{}', version: '{}')",
+ __FUNCTION__, fMovie ? MediaTypeMovie : MediaTypeEpisode, scurl.GetFirstThumbUrl(),
+ Name(), Path(), ADDON::TranslateContent(Content()), Version().asString());
+
+ video.Reset();
+
+ if (m_isPython)
+ return PythonDetails(ID(), "url", scurl.GetFirstThumbUrl(),
+ fMovie ? "getdetails" : "getepisodedetails", GetPathSettingsAsJSON(), video);
+
+ std::string sFunc = fMovie ? "GetDetails" : "GetEpisodeDetails";
+ std::vector<std::string> vcsIn;
+ vcsIn.push_back(scurl.GetId());
+ vcsIn.push_back(scurl.GetFirstThumbUrl());
+ std::vector<std::string> vcsOut = RunNoThrow(sFunc, scurl, fcurl, &vcsIn);
+
+ // parse XML output
+ bool fRet(false);
+ for (std::vector<std::string>::const_iterator i = vcsOut.begin(); i != vcsOut.end(); ++i)
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(*i, TIXML_ENCODING_UTF8);
+ if (!doc.RootElement())
+ {
+ CLog::Log(LOGERROR, "{}: Unable to parse XML", __FUNCTION__);
+ continue;
+ }
+
+ TiXmlHandle xhDoc(&doc);
+ TiXmlElement *pxeDetails = xhDoc.FirstChild("details").Element();
+ if (!pxeDetails)
+ {
+ CLog::Log(LOGERROR, "{}: Invalid XML file (want <details>)", __FUNCTION__);
+ continue;
+ }
+ video.Load(pxeDetails, true /*fChain*/);
+ fRet = true; // but don't exit in case of chaining
+ }
+ return fRet;
+}
+
+// takes a URL; returns true and populates album on success, false otherwise
+bool CScraper::GetAlbumDetails(CCurlFile &fcurl, const CScraperUrl &scurl, CAlbum &album)
+{
+ CLog::Log(LOGDEBUG,
+ "{}: Reading '{}' using {} scraper "
+ "(file: '{}', content: '{}', version: '{}')",
+ __FUNCTION__, scurl.GetFirstThumbUrl(), Name(), Path(),
+ ADDON::TranslateContent(Content()), Version().asString());
+
+ if (m_isPython)
+ return PythonDetails(ID(), "url", scurl.GetFirstThumbUrl(),
+ "getdetails", GetPathSettingsAsJSON(), album);
+
+ std::vector<std::string> vcsOut = RunNoThrow("GetAlbumDetails", scurl, fcurl);
+
+ // parse the returned XML into an album object (see CAlbum::Load for details)
+ bool fRet(false);
+ for (std::vector<std::string>::const_iterator i = vcsOut.begin(); i != vcsOut.end(); ++i)
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(*i, TIXML_ENCODING_UTF8);
+ if (!doc.RootElement())
+ {
+ CLog::Log(LOGERROR, "{}: Unable to parse XML", __FUNCTION__);
+ return false;
+ }
+ fRet = album.Load(doc.RootElement(), i != vcsOut.begin());
+ }
+ return fRet;
+}
+
+// takes a URL (one returned from FindArtist), the original search string, and
+// returns true and populates artist on success, false on failure
+bool CScraper::GetArtistDetails(CCurlFile &fcurl,
+ const CScraperUrl &scurl,
+ const std::string &sSearch,
+ CArtist &artist)
+{
+ if (!scurl.HasUrls())
+ return false;
+
+ CLog::Log(LOGDEBUG,
+ "{}: Reading '{}' ('{}') using {} scraper "
+ "(file: '{}', content: '{}', version: '{}')",
+ __FUNCTION__, scurl.GetFirstThumbUrl(), sSearch, Name(), Path(),
+ ADDON::TranslateContent(Content()), Version().asString());
+
+ if (m_isPython)
+ return PythonDetails(ID(), "url", scurl.GetFirstThumbUrl(),
+ "getdetails", GetPathSettingsAsJSON(), artist);
+
+ // pass in the original search string for chaining to search other sites
+ std::vector<std::string> vcIn;
+ vcIn.push_back(sSearch);
+ vcIn[0] = CURL::Encode(vcIn[0]);
+
+ std::vector<std::string> vcsOut = RunNoThrow("GetArtistDetails", scurl, fcurl, &vcIn);
+
+ // ok, now parse the xml file
+ bool fRet(false);
+ for (std::vector<std::string>::const_iterator i = vcsOut.begin(); i != vcsOut.end(); ++i)
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(*i, TIXML_ENCODING_UTF8);
+ if (!doc.RootElement())
+ {
+ CLog::Log(LOGERROR, "{}: Unable to parse XML", __FUNCTION__);
+ return false;
+ }
+
+ fRet = artist.Load(doc.RootElement(), i != vcsOut.begin());
+ }
+ return fRet;
+}
+
+bool CScraper::GetArtwork(XFILE::CCurlFile &fcurl, CVideoInfoTag &details)
+{
+ if (!details.HasUniqueID())
+ return false;
+
+ CLog::Log(LOGDEBUG,
+ "{}: Reading artwork for '{}' using {} scraper "
+ "(file: '{}', content: '{}', version: '{}')",
+ __FUNCTION__, details.GetUniqueID(), Name(), Path(), ADDON::TranslateContent(Content()),
+ Version().asString());
+
+ if (m_isPython)
+ return PythonDetails(ID(), "id", details.GetUniqueID(),
+ "getartwork", GetPathSettingsAsJSON(), details);
+
+ std::vector<std::string> vcsIn;
+ CScraperUrl scurl;
+ vcsIn.push_back(details.GetUniqueID());
+ std::vector<std::string> vcsOut = RunNoThrow("GetArt", scurl, fcurl, &vcsIn);
+
+ bool fRet(false);
+ for (std::vector<std::string>::const_iterator it = vcsOut.begin(); it != vcsOut.end(); ++it)
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(*it, TIXML_ENCODING_UTF8);
+ if (!doc.RootElement())
+ {
+ CLog::Log(LOGERROR, "{}: Unable to parse XML", __FUNCTION__);
+ return false;
+ }
+ fRet = details.Load(doc.RootElement(), it != vcsOut.begin());
+ }
+ return fRet;
+}
+}
diff --git a/xbmc/addons/Scraper.h b/xbmc/addons/Scraper.h
new file mode 100644
index 0000000..d9f7286
--- /dev/null
+++ b/xbmc/addons/Scraper.h
@@ -0,0 +1,182 @@
+/*
+ * 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 "XBDateTime.h"
+#include "addons/Addon.h"
+#include "utils/ScraperParser.h"
+#include "utils/ScraperUrl.h"
+#include "video/Episode.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CAlbum;
+class CArtist;
+class CVideoInfoTag;
+
+namespace MUSIC_GRABBER
+{
+class CMusicAlbumInfo;
+class CMusicArtistInfo;
+}
+
+typedef enum
+{
+ CONTENT_MOVIES,
+ CONTENT_TVSHOWS,
+ CONTENT_MUSICVIDEOS,
+ CONTENT_ALBUMS,
+ CONTENT_ARTISTS,
+ CONTENT_NONE,
+} CONTENT_TYPE;
+
+namespace XFILE
+{
+ class CCurlFile;
+}
+
+class CScraperUrl;
+
+namespace ADDON
+{
+class CScraper;
+typedef std::shared_ptr<CScraper> ScraperPtr;
+
+std::string TranslateContent(const CONTENT_TYPE &content, bool pretty=false);
+CONTENT_TYPE TranslateContent(const std::string &string);
+AddonType ScraperTypeFromContent(const CONTENT_TYPE& content);
+
+// thrown as exception to signal abort or show error dialog
+class CScraperError
+{
+public:
+ CScraperError() = default;
+ CScraperError(const std::string &sTitle, const std::string &sMessage) :
+ m_fAborted(false), m_sTitle(sTitle), m_sMessage(sMessage) {}
+
+ bool FAborted() const { return m_fAborted; }
+ const std::string &Title() const { return m_sTitle; }
+ const std::string &Message() const { return m_sMessage; }
+
+private:
+ bool m_fAborted = true;
+ std::string m_sTitle;
+ std::string m_sMessage;
+};
+
+class CScraper : public CAddon
+{
+public:
+ explicit CScraper(const AddonInfoPtr& addonInfo, AddonType addonType);
+
+ /*! \brief Set the scraper settings for a particular path from an XML string
+ Loads the default and user settings (if not already loaded) and, if the given XML string is non-empty,
+ overrides the user settings with the XML.
+ \param content Content type of the path
+ \param xml string of XML with the settings. If non-empty this overrides any saved user settings.
+ \return true if settings are available, false otherwise
+ \sa GetPathSettings
+ */
+ bool SetPathSettings(CONTENT_TYPE content, const std::string& xml);
+
+ /*! \brief Get the scraper settings for a particular path in the form of an XML string
+ Loads the default and user settings (if not already loaded) and returns the user settings in the
+ form or an XML string
+ \return a string containing the XML settings
+ \sa SetPathSettings
+ */
+ std::string GetPathSettings();
+
+ /*! \brief Clear any previously cached results for this scraper
+ Any previously cached files are cleared if they have been cached for longer than the specified
+ cachepersistence.
+ */
+ void ClearCache();
+
+ CONTENT_TYPE Content() const { return m_pathContent; }
+ bool RequiresSettings() const { return m_requiressettings; }
+ bool Supports(const CONTENT_TYPE &content) const;
+
+ bool IsInUse() const override;
+ bool IsNoop();
+ bool IsPython() const { return m_isPython; }
+
+ // scraper media functions
+ CScraperUrl NfoUrl(const std::string &sNfoContent);
+
+ /*! \brief Resolve an external ID (e.g. MusicBrainz IDs) to a URL using scrapers
+ If we have an ID in hand, e.g. MusicBrainz IDs or TheTVDB Season IDs
+ we can get directly to a URL instead of searching by name and choosing from
+ the search results. The correct scraper type should be used to get the right
+ URL for a given ID, so we can differentiate albums, artists, TV Seasons, etc.
+ \param externalID the external ID - e.g. MusicBrainzArtist/AlbumID
+ \return a populated URL pointing to the details page for the given ID or
+ an empty URL if we couldn't resolve the ID.
+ */
+ CScraperUrl ResolveIDToUrl(const std::string &externalID);
+
+ std::vector<CScraperUrl> FindMovie(XFILE::CCurlFile &fcurl,
+ const std::string &movieTitle, int movieYear, bool fFirst);
+ std::vector<MUSIC_GRABBER::CMusicAlbumInfo> FindAlbum(XFILE::CCurlFile &fcurl,
+ const std::string &sAlbum, const std::string &sArtist = "");
+ std::vector<MUSIC_GRABBER::CMusicArtistInfo> FindArtist(
+ XFILE::CCurlFile &fcurl, const std::string &sArtist);
+ VIDEO::EPISODELIST GetEpisodeList(XFILE::CCurlFile &fcurl, const CScraperUrl &scurl);
+
+ bool GetVideoDetails(XFILE::CCurlFile &fcurl, const CScraperUrl &scurl,
+ bool fMovie/*else episode*/, CVideoInfoTag &video);
+ bool GetAlbumDetails(XFILE::CCurlFile &fcurl, const CScraperUrl &scurl,
+ CAlbum &album);
+ bool GetArtistDetails(XFILE::CCurlFile &fcurl, const CScraperUrl &scurl,
+ const std::string &sSearch, CArtist &artist);
+ bool GetArtwork(XFILE::CCurlFile &fcurl, CVideoInfoTag &details);
+
+private:
+ CScraper(const CScraper &rhs) = delete;
+ CScraper& operator=(const CScraper&) = delete;
+ CScraper(CScraper&&) = delete;
+ CScraper& operator=(CScraper&&) = delete;
+
+ std::string SearchStringEncoding() const
+ { return m_parser.GetSearchStringEncoding(); }
+
+ /*! \brief Get the scraper settings for a particular path in the form of a JSON string
+ Loads the default and user settings (if not already loaded) and returns the user settings in the
+ form of an JSON string. It is used in Python scrapers.
+ \return a string containing the JSON settings
+ \sa SetPathSettings
+ */
+ std::string GetPathSettingsAsJSON();
+
+ bool Load();
+ std::vector<std::string> Run(const std::string& function,
+ const CScraperUrl& url,
+ XFILE::CCurlFile& http,
+ const std::vector<std::string>* extras = nullptr);
+ std::vector<std::string> RunNoThrow(const std::string& function,
+ const CScraperUrl& url,
+ XFILE::CCurlFile& http,
+ const std::vector<std::string>* extras = nullptr);
+ std::string InternalRun(const std::string& function,
+ const CScraperUrl& url,
+ XFILE::CCurlFile& http,
+ const std::vector<std::string>* extras);
+
+ bool m_fLoaded;
+ bool m_isPython = false;
+ bool m_requiressettings;
+ CDateTimeSpan m_persistence;
+ CONTENT_TYPE m_pathContent;
+ CScraperParser m_parser;
+};
+
+}
+
diff --git a/xbmc/addons/ScreenSaver.cpp b/xbmc/addons/ScreenSaver.cpp
new file mode 100644
index 0000000..06e1afd
--- /dev/null
+++ b/xbmc/addons/ScreenSaver.cpp
@@ -0,0 +1,85 @@
+/*
+ * 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 "ScreenSaver.h"
+
+#include "filesystem/SpecialProtocol.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+using namespace ADDON;
+using namespace KODI::ADDONS;
+
+namespace
+{
+void get_properties(const KODI_HANDLE hdl, struct KODI_ADDON_SCREENSAVER_PROPS* props)
+{
+ if (hdl)
+ static_cast<CScreenSaver*>(hdl)->GetProperties(props);
+}
+} // namespace
+
+CScreenSaver::CScreenSaver(const AddonInfoPtr& addonInfo)
+ : IAddonInstanceHandler(ADDON_INSTANCE_SCREENSAVER, addonInfo)
+{
+ m_ifc.screensaver = new AddonInstance_Screensaver;
+ m_ifc.screensaver->toAddon = new KodiToAddonFuncTable_Screensaver();
+ m_ifc.screensaver->toKodi = new AddonToKodiFuncTable_Screensaver();
+ m_ifc.screensaver->toKodi->get_properties = get_properties;
+
+ /* Open the class "kodi::addon::CInstanceScreensaver" on add-on side */
+ if (CreateInstance() != ADDON_STATUS_OK)
+ CLog::Log(LOGFATAL, "Screensaver: failed to create instance for '{}' and not usable!", ID());
+}
+
+CScreenSaver::~CScreenSaver()
+{
+ /* Destroy the class "kodi::addon::CInstanceScreensaver" on add-on side */
+ DestroyInstance();
+
+ delete m_ifc.screensaver->toAddon;
+ delete m_ifc.screensaver->toKodi;
+ delete m_ifc.screensaver;
+}
+
+bool CScreenSaver::Start()
+{
+ if (m_ifc.screensaver->toAddon->start)
+ return m_ifc.screensaver->toAddon->start(m_ifc.hdl);
+ return false;
+}
+
+void CScreenSaver::Stop()
+{
+ if (m_ifc.screensaver->toAddon->stop)
+ m_ifc.screensaver->toAddon->stop(m_ifc.hdl);
+}
+
+void CScreenSaver::Render()
+{
+ if (m_ifc.screensaver->toAddon->render)
+ m_ifc.screensaver->toAddon->render(m_ifc.hdl);
+}
+
+void CScreenSaver::GetProperties(struct KODI_ADDON_SCREENSAVER_PROPS* props)
+{
+ if (!props)
+ return;
+
+ const auto winSystem = CServiceBroker::GetWinSystem();
+ if (!winSystem)
+ return;
+
+ props->x = 0;
+ props->y = 0;
+ props->device = winSystem->GetHWContext();
+ props->width = winSystem->GetGfxContext().GetWidth();
+ props->height = winSystem->GetGfxContext().GetHeight();
+ props->pixelRatio = winSystem->GetGfxContext().GetResInfo().fPixelRatio;
+}
diff --git a/xbmc/addons/ScreenSaver.h b/xbmc/addons/ScreenSaver.h
new file mode 100644
index 0000000..616d3e8
--- /dev/null
+++ b/xbmc/addons/ScreenSaver.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Screensaver.h"
+
+namespace KODI
+{
+namespace ADDONS
+{
+
+class CScreenSaver : public ADDON::IAddonInstanceHandler
+{
+public:
+ explicit CScreenSaver(const ADDON::AddonInfoPtr& addonInfo);
+ ~CScreenSaver() override;
+
+ bool Start();
+ void Stop();
+ void Render();
+
+ // Addon callback functions
+ void GetProperties(struct KODI_ADDON_SCREENSAVER_PROPS* props);
+};
+
+} /* namespace ADDONS */
+} /* namespace KODI */
diff --git a/xbmc/addons/Service.cpp b/xbmc/addons/Service.cpp
new file mode 100644
index 0000000..e9b6b40
--- /dev/null
+++ b/xbmc/addons/Service.cpp
@@ -0,0 +1,131 @@
+/*
+ * 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 "Service.h"
+
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+
+namespace ADDON
+{
+
+CService::CService(const AddonInfoPtr& addonInfo) : CAddon(addonInfo, AddonType::SERVICE)
+{
+}
+
+CServiceAddonManager::CServiceAddonManager(CAddonMgr& addonMgr) :
+ m_addonMgr(addonMgr)
+{
+}
+
+CServiceAddonManager::~CServiceAddonManager()
+{
+ m_addonMgr.Events().Unsubscribe(this);
+ m_addonMgr.UnloadEvents().Unsubscribe(this);
+}
+
+void CServiceAddonManager::OnEvent(const ADDON::AddonEvent& event)
+{
+ if (typeid(event) == typeid(ADDON::AddonEvents::Enabled))
+ {
+ Start(event.addonId);
+ }
+ else if (typeid(event) == typeid(ADDON::AddonEvents::ReInstalled))
+ {
+ Stop(event.addonId);
+ Start(event.addonId);
+ }
+ else if (typeid(event) == typeid(ADDON::AddonEvents::Disabled) ||
+ typeid(event) == typeid(ADDON::AddonEvents::Unload))
+ {
+ Stop(event.addonId);
+ }
+}
+
+void CServiceAddonManager::Start()
+{
+ m_addonMgr.Events().Subscribe(this, &CServiceAddonManager::OnEvent);
+ m_addonMgr.UnloadEvents().Subscribe(this, &CServiceAddonManager::OnEvent);
+ VECADDONS addons;
+ if (m_addonMgr.GetAddons(addons, AddonType::SERVICE))
+ {
+ for (const auto& addon : addons)
+ {
+ Start(addon);
+ }
+ }
+}
+
+void CServiceAddonManager::Start(const std::string& addonId)
+{
+ AddonPtr addon;
+ if (m_addonMgr.GetAddon(addonId, addon, AddonType::SERVICE, OnlyEnabled::CHOICE_YES))
+ {
+ Start(addon);
+ }
+}
+
+void CServiceAddonManager::Start(const AddonPtr& addon)
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ if (m_services.find(addon->ID()) != m_services.end())
+ {
+ CLog::Log(LOGDEBUG, "CServiceAddonManager: {} already started.", addon->ID());
+ return;
+ }
+
+ if (StringUtils::EndsWith(addon->LibPath(), ".py"))
+ {
+ CLog::Log(LOGDEBUG, "CServiceAddonManager: starting {}", addon->ID());
+ auto handle = CScriptInvocationManager::GetInstance().ExecuteAsync(addon->LibPath(), addon);
+ if (handle == -1)
+ {
+ CLog::Log(LOGERROR, "CServiceAddonManager: {} failed to start", addon->ID());
+ return;
+ }
+ m_services[addon->ID()] = handle;
+ }
+}
+
+void CServiceAddonManager::Stop()
+{
+ m_addonMgr.Events().Unsubscribe(this);
+ m_addonMgr.UnloadEvents().Unsubscribe(this);
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ for (const auto& service : m_services)
+ {
+ Stop(service);
+ }
+ m_services.clear();
+}
+
+void CServiceAddonManager::Stop(const std::string& addonId)
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ auto it = m_services.find(addonId);
+ if (it != m_services.end())
+ {
+ Stop(*it);
+ m_services.erase(it);
+ }
+}
+
+void CServiceAddonManager::Stop(const std::map<std::string, int>::value_type& service)
+{
+ CLog::Log(LOGDEBUG, "CServiceAddonManager: stopping {}.", service.first);
+ if (!CScriptInvocationManager::GetInstance().Stop(service.second))
+ {
+ CLog::Log(LOGINFO, "CServiceAddonManager: failed to stop {} (may have ended)", service.first);
+ }
+}
+}
diff --git a/xbmc/addons/Service.h b/xbmc/addons/Service.h
new file mode 100644
index 0000000..38f1d1e
--- /dev/null
+++ b/xbmc/addons/Service.h
@@ -0,0 +1,60 @@
+/*
+ * 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 "addons/Addon.h"
+#include "addons/AddonEvents.h"
+#include "threads/CriticalSection.h"
+
+namespace ADDON
+{
+ class CService: public CAddon
+ {
+ public:
+ explicit CService(const AddonInfoPtr& addonInfo);
+ };
+
+ class CServiceAddonManager
+ {
+ public:
+ explicit CServiceAddonManager(CAddonMgr& addonMgr);
+ ~CServiceAddonManager();
+
+ /**
+ * Start all services.
+ */
+ void Start();
+
+ /**
+ * Start service by add-on id.
+ */
+ void Start(const AddonPtr& addon);
+ void Start(const std::string& addonId);
+
+ /**
+ * Stop all services.
+ */
+ void Stop();
+
+ /**
+ * Stop service by add-on id.
+ */
+ void Stop(const std::string& addonId);
+
+ private:
+ void OnEvent(const AddonEvent& event);
+
+ void Stop(const std::map<std::string, int>::value_type& service);
+
+ CAddonMgr& m_addonMgr;
+ CCriticalSection m_criticalSection;
+ /** add-on id -> script id */
+ std::map<std::string, int> m_services;
+ };
+}
diff --git a/xbmc/addons/Skin.cpp b/xbmc/addons/Skin.cpp
new file mode 100644
index 0000000..4450a90
--- /dev/null
+++ b/xbmc/addons/Skin.cpp
@@ -0,0 +1,906 @@
+/*
+ * 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 "Skin.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "threads/Timer.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <charconv>
+
+#define XML_SETTINGS "settings"
+#define XML_SETTING "setting"
+#define XML_ATTR_TYPE "type"
+#define XML_ATTR_NAME "name"
+#define XML_ATTR_ID "id"
+
+using namespace XFILE;
+using namespace KODI::MESSAGING;
+using namespace std::chrono_literals;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+std::shared_ptr<ADDON::CSkinInfo> g_SkinInfo;
+
+namespace
+{
+constexpr auto DELAY = 500ms;
+}
+
+namespace ADDON
+{
+
+class CSkinSettingUpdateHandler : private ITimerCallback
+{
+public:
+ CSkinSettingUpdateHandler(CAddon& addon)
+ : m_addon(addon), m_timer(this) {}
+ ~CSkinSettingUpdateHandler() override = default;
+
+ void OnTimeout() override;
+ void TriggerSave();
+private:
+ CAddon &m_addon;
+ CTimer m_timer;
+};
+
+bool CSkinSetting::Serialize(TiXmlElement* parent) const
+{
+ if (parent == nullptr)
+ return false;
+
+ TiXmlElement setting(XML_SETTING);
+ setting.SetAttribute(XML_ATTR_ID, name.c_str());
+ setting.SetAttribute(XML_ATTR_TYPE, GetType());
+
+ if (!SerializeSetting(&setting))
+ return false;
+
+ parent->InsertEndChild(setting);
+
+ return true;
+}
+
+bool CSkinSetting::Deserialize(const TiXmlElement* element)
+{
+ if (element == nullptr)
+ return false;
+
+ name = XMLUtils::GetAttribute(element, XML_ATTR_ID);
+
+ // backwards compatibility for guisettings.xml
+ if (name.empty())
+ name = XMLUtils::GetAttribute(element, XML_ATTR_NAME);
+
+ return true;
+}
+
+bool CSkinSettingString::Deserialize(const TiXmlElement* element)
+{
+ value.clear();
+
+ if (!CSkinSetting::Deserialize(element))
+ return false;
+
+ if (element->FirstChild() != nullptr)
+ value = element->FirstChild()->Value();
+
+ return true;
+}
+
+bool CSkinSettingString::SerializeSetting(TiXmlElement* element) const
+{
+ if (element == nullptr)
+ return false;
+
+ TiXmlText xmlValue(value);
+ element->InsertEndChild(xmlValue);
+
+ return true;
+}
+
+bool CSkinSettingBool::Deserialize(const TiXmlElement* element)
+{
+ value = false;
+
+ if (!CSkinSetting::Deserialize(element))
+ return false;
+
+ if (element->FirstChild() != nullptr)
+ value = StringUtils::EqualsNoCase(element->FirstChild()->ValueStr(), "true");
+
+ return true;
+}
+
+bool CSkinSettingBool::SerializeSetting(TiXmlElement* element) const
+{
+ if (element == nullptr)
+ return false;
+
+ TiXmlText xmlValue(value ? "true" : "false");
+ element->InsertEndChild(xmlValue);
+
+ return true;
+}
+
+CSkinInfo::CSkinInfo(const AddonInfoPtr& addonInfo,
+ const RESOLUTION_INFO& resolution /* = RESOLUTION_INFO() */)
+ : CAddon(addonInfo, AddonType::SKIN),
+ m_defaultRes(resolution),
+ m_effectsSlowDown(1.f),
+ m_debugging(false)
+{
+ m_settingsUpdateHandler.reset(new CSkinSettingUpdateHandler(*this));
+}
+
+CSkinInfo::CSkinInfo(const AddonInfoPtr& addonInfo) : CAddon(addonInfo, AddonType::SKIN)
+{
+ for (const auto& values : Type(AddonType::SKIN)->GetValues())
+ {
+ if (values.first != "res")
+ continue;
+
+ int width = values.second.GetValue("res@width").asInteger();
+ int height = values.second.GetValue("res@height").asInteger();
+ bool defRes = values.second.GetValue("res@default").asBoolean();
+ std::string folder = values.second.GetValue("res@folder").asString();
+ std::string strAspect = values.second.GetValue("res@aspect").asString();
+ float aspect = 0;
+
+ std::vector<std::string> fracs = StringUtils::Split(strAspect, ':');
+ if (fracs.size() == 2)
+ aspect = (float)(atof(fracs[0].c_str()) / atof(fracs[1].c_str()));
+ if (width > 0 && height > 0)
+ {
+ RESOLUTION_INFO res(width, height, aspect, folder);
+ res.strId = strAspect; // for skin usage, store aspect string in strId
+ if (defRes)
+ m_defaultRes = res;
+ m_resolutions.push_back(res);
+ }
+ }
+
+ m_effectsSlowDown = Type(AddonType::SKIN)->GetValue("@effectslowdown").asFloat();
+ if (m_effectsSlowDown == 0.0f)
+ m_effectsSlowDown = 1.f;
+
+ m_debugging = Type(AddonType::SKIN)->GetValue("@debugging").asBoolean();
+
+ m_settingsUpdateHandler.reset(new CSkinSettingUpdateHandler(*this));
+ LoadStartupWindows(addonInfo);
+}
+
+CSkinInfo::~CSkinInfo() = default;
+
+struct closestRes
+{
+ explicit closestRes(const RESOLUTION_INFO &target) : m_target(target) { };
+ bool operator()(const RESOLUTION_INFO &i, const RESOLUTION_INFO &j)
+ {
+ float diff = fabs(i.DisplayRatio() - m_target.DisplayRatio()) - fabs(j.DisplayRatio() - m_target.DisplayRatio());
+ if (diff < 0) return true;
+ if (diff > 0) return false;
+ diff = fabs((float)i.iHeight - m_target.iHeight) - fabs((float)j.iHeight - m_target.iHeight);
+ if (diff < 0) return true;
+ if (diff > 0) return false;
+ return fabs((float)i.iWidth - m_target.iWidth) < fabs((float)j.iWidth - m_target.iWidth);
+ }
+ RESOLUTION_INFO m_target;
+};
+
+void CSkinInfo::Start()
+{
+ if (!LoadUserSettings())
+ CLog::Log(LOGWARNING, "CSkinInfo: failed to load skin settings");
+
+ if (!m_resolutions.size())
+ { // try falling back to whatever resolutions exist in the directory
+ CFileItemList items;
+ CDirectory::GetDirectory(Path(), items, "", DIR_FLAG_NO_FILE_DIRS);
+ for (int i = 0; i < items.Size(); i++)
+ {
+ RESOLUTION_INFO res;
+ if (items[i]->m_bIsFolder && TranslateResolution(items[i]->GetLabel(), res))
+ m_resolutions.push_back(res);
+ }
+ }
+
+ if (!m_resolutions.empty())
+ {
+ // find the closest resolution
+ const RESOLUTION_INFO &target = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ RESOLUTION_INFO& res = *std::min_element(m_resolutions.begin(), m_resolutions.end(), closestRes(target));
+ m_currentAspect = res.strId;
+ }
+}
+
+std::string CSkinInfo::GetSkinPath(const std::string& strFile, RESOLUTION_INFO *res, const std::string& strBaseDir /* = "" */) const
+{
+ if (m_resolutions.empty())
+ return ""; // invalid skin
+
+ std::string strPathToUse = Path();
+ if (!strBaseDir.empty())
+ strPathToUse = strBaseDir;
+
+ // if the caller doesn't care about the resolution just use a temporary
+ RESOLUTION_INFO tempRes;
+ if (!res)
+ res = &tempRes;
+
+ // find the closest resolution
+ const RESOLUTION_INFO &target = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ *res = *std::min_element(m_resolutions.begin(), m_resolutions.end(), closestRes(target));
+
+ std::string strPath = URIUtils::AddFileToFolder(strPathToUse, res->strMode, strFile);
+ if (CFileUtils::Exists(strPath))
+ return strPath;
+
+ // use the default resolution
+ *res = m_defaultRes;
+
+ return URIUtils::AddFileToFolder(strPathToUse, res->strMode, strFile);
+}
+
+bool CSkinInfo::HasSkinFile(const std::string &strFile) const
+{
+ return CFileUtils::Exists(GetSkinPath(strFile));
+}
+
+void CSkinInfo::LoadIncludes()
+{
+ std::string includesPath =
+ CSpecialProtocol::TranslatePathConvertCase(GetSkinPath("Includes.xml"));
+ CLog::Log(LOGINFO, "Loading skin includes from {}", includesPath);
+ m_includes.Clear();
+ m_includes.Load(includesPath);
+}
+
+void CSkinInfo::LoadTimers()
+{
+ const std::string timersPath =
+ CSpecialProtocol::TranslatePathConvertCase(GetSkinPath("Timers.xml"));
+ CLog::LogF(LOGINFO, "Trying to load skin timers from {}", timersPath);
+ m_skinTimerManager.LoadTimers(timersPath);
+}
+
+void CSkinInfo::ProcessTimers()
+{
+ m_skinTimerManager.Process();
+}
+void CSkinInfo::ResolveIncludes(TiXmlElement* node,
+ std::map<INFO::InfoPtr, bool>* xmlIncludeConditions /* = nullptr */)
+{
+ if(xmlIncludeConditions)
+ xmlIncludeConditions->clear();
+
+ m_includes.Resolve(node, xmlIncludeConditions);
+}
+
+int CSkinInfo::GetStartWindow() const
+{
+ int windowID = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_LOOKANDFEEL_STARTUPWINDOW);
+ assert(m_startupWindows.size());
+ for (std::vector<CStartupWindow>::const_iterator it = m_startupWindows.begin(); it != m_startupWindows.end(); ++it)
+ {
+ if (windowID == (*it).m_id)
+ return windowID;
+ }
+ // return our first one
+ return m_startupWindows[0].m_id;
+}
+
+bool CSkinInfo::LoadStartupWindows(const AddonInfoPtr& addonInfo)
+{
+ m_startupWindows.clear();
+ m_startupWindows.emplace_back(WINDOW_HOME, "513");
+ m_startupWindows.emplace_back(WINDOW_TV_CHANNELS, "19180");
+ m_startupWindows.emplace_back(WINDOW_TV_GUIDE, "19273");
+ m_startupWindows.emplace_back(WINDOW_RADIO_CHANNELS, "19183");
+ m_startupWindows.emplace_back(WINDOW_RADIO_GUIDE, "19274");
+ m_startupWindows.emplace_back(WINDOW_PROGRAMS, "0");
+ m_startupWindows.emplace_back(WINDOW_PICTURES, "1");
+ m_startupWindows.emplace_back(WINDOW_MUSIC_NAV, "2");
+ m_startupWindows.emplace_back(WINDOW_VIDEO_NAV, "3");
+ m_startupWindows.emplace_back(WINDOW_FILES, "7");
+ m_startupWindows.emplace_back(WINDOW_SETTINGS_MENU, "5");
+ m_startupWindows.emplace_back(WINDOW_WEATHER, "8");
+ m_startupWindows.emplace_back(WINDOW_FAVOURITES, "1036");
+ return true;
+}
+
+void CSkinInfo::GetSkinPaths(std::vector<std::string> &paths) const
+{
+ RESOLUTION_INFO res;
+ GetSkinPath("Home.xml", &res);
+ if (!res.strMode.empty())
+ paths.push_back(URIUtils::AddFileToFolder(Path(), res.strMode));
+ if (res.strMode != m_defaultRes.strMode)
+ paths.push_back(URIUtils::AddFileToFolder(Path(), m_defaultRes.strMode));
+}
+
+bool CSkinInfo::TranslateResolution(const std::string &name, RESOLUTION_INFO &res)
+{
+ std::string lower(name); StringUtils::ToLower(lower);
+ if (lower == "pal")
+ res = RESOLUTION_INFO(720, 576, 4.0f/3, "pal");
+ else if (lower == "pal16x9")
+ res = RESOLUTION_INFO(720, 576, 16.0f/9, "pal16x9");
+ else if (lower == "ntsc")
+ res = RESOLUTION_INFO(720, 480, 4.0f/3, "ntsc");
+ else if (lower == "ntsc16x9")
+ res = RESOLUTION_INFO(720, 480, 16.0f/9, "ntsc16x9");
+ else if (lower == "720p")
+ res = RESOLUTION_INFO(1280, 720, 0, "720p");
+ else if (lower == "1080i")
+ res = RESOLUTION_INFO(1920, 1080, 0, "1080i");
+ else
+ return false;
+ return true;
+}
+
+int CSkinInfo::GetFirstWindow() const
+{
+ int startWindow = GetStartWindow();
+ if (HasSkinFile("Startup.xml"))
+ startWindow = WINDOW_STARTUP_ANIM;
+ return startWindow;
+}
+
+bool CSkinInfo::IsInUse() const
+{
+ // Could extend this to prompt for reverting to the standard skin perhaps
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN) == ID();
+}
+
+const INFO::CSkinVariableString* CSkinInfo::CreateSkinVariable(const std::string& name, int context)
+{
+ return m_includes.CreateSkinVariable(name, context);
+}
+
+void CSkinInfo::OnPreInstall()
+{
+ bool skinLoaded = g_SkinInfo != nullptr;
+ if (IsInUse() && skinLoaded)
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ "UnloadSkin");
+}
+
+void CSkinInfo::OnPostInstall(bool update, bool modal)
+{
+ if (!g_SkinInfo)
+ return;
+
+ if (IsInUse() || (!update && !modal &&
+ HELPERS::ShowYesNoDialogText(CVariant{Name()}, CVariant{24099}) ==
+ DialogResponse::CHOICE_YES))
+ {
+ CGUIDialogKaiToast *toast = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogKaiToast>(WINDOW_DIALOG_KAI_TOAST);
+ if (toast)
+ {
+ toast->ResetTimer();
+ toast->Close(true);
+ }
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN) == ID())
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ "ReloadSkin");
+ else
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_LOOKANDFEEL_SKIN, ID());
+ }
+}
+
+void CSkinInfo::Unload()
+{
+ m_skinTimerManager.Stop();
+}
+
+bool CSkinInfo::TimerIsRunning(const std::string& timer) const
+{
+ return m_skinTimerManager.TimerIsRunning(timer);
+}
+
+float CSkinInfo::GetTimerElapsedSeconds(const std::string& timer) const
+{
+ return m_skinTimerManager.GetTimerElapsedSeconds(timer);
+}
+
+void CSkinInfo::TimerStart(const std::string& timer) const
+{
+ m_skinTimerManager.TimerStart(timer);
+}
+
+void CSkinInfo::TimerStop(const std::string& timer) const
+{
+ m_skinTimerManager.TimerStop(timer);
+}
+
+void CSkinInfo::SettingOptionsSkinColorsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ if (!g_SkinInfo)
+ return;
+
+ std::string settingValue = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ // Remove the .xml extension from the Themes
+ if (URIUtils::HasExtension(settingValue, ".xml"))
+ URIUtils::RemoveExtension(settingValue);
+ current = "SKINDEFAULT";
+
+ // There is a default theme (just defaults.xml)
+ // any other *.xml files are additional color themes on top of this one.
+
+ // add the default label
+ list.emplace_back(g_localizeStrings.Get(15109), "SKINDEFAULT"); // the standard defaults.xml will be used!
+
+ // Search for colors in the Current skin!
+ std::vector<std::string> vecColors;
+ std::string strPath = URIUtils::AddFileToFolder(g_SkinInfo->Path(), "colors");
+
+ CFileItemList items;
+ CDirectory::GetDirectory(CSpecialProtocol::TranslatePathConvertCase(strPath), items, ".xml", DIR_FLAG_DEFAULTS);
+ // Search for Themes in the Current skin!
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr pItem = items[i];
+ if (!pItem->m_bIsFolder && !StringUtils::EqualsNoCase(pItem->GetLabel(), "defaults.xml"))
+ { // not the default one
+ vecColors.push_back(pItem->GetLabel().substr(0, pItem->GetLabel().size() - 4));
+ }
+ }
+ sort(vecColors.begin(), vecColors.end(), sortstringbyname());
+ for (int i = 0; i < (int) vecColors.size(); ++i)
+ list.emplace_back(vecColors[i], vecColors[i]);
+
+ // try to find the best matching value
+ for (const auto& elem : list)
+ {
+ if (StringUtils::EqualsNoCase(elem.value, settingValue))
+ current = settingValue;
+ }
+}
+
+void CSkinInfo::SettingOptionsSkinFontsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ if (!g_SkinInfo)
+ return;
+
+ std::string settingValue = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ bool currentValueSet = false;
+ std::string strPath = g_SkinInfo->GetSkinPath("Font.xml");
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(strPath))
+ {
+ CLog::Log(LOGERROR, "FillInSkinFonts: Couldn't load {}", strPath);
+ return;
+ }
+
+ const TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (!pRootElement || pRootElement->ValueStr() != "fonts")
+ {
+ CLog::Log(LOGERROR, "FillInSkinFonts: file {} doesn't start with <fonts>", strPath);
+ return;
+ }
+
+ const TiXmlElement *pChild = pRootElement->FirstChildElement("fontset");
+ while (pChild)
+ {
+ const char* idAttr = pChild->Attribute("id");
+ const char* idLocAttr = pChild->Attribute("idloc");
+ if (idAttr != nullptr)
+ {
+ if (idLocAttr)
+ list.emplace_back(g_localizeStrings.Get(atoi(idLocAttr)), idAttr);
+ else
+ list.emplace_back(idAttr, idAttr);
+
+ if (StringUtils::EqualsNoCase(idAttr, settingValue))
+ currentValueSet = true;
+ }
+ pChild = pChild->NextSiblingElement("fontset");
+ }
+
+ if (list.empty())
+ { // Since no fontset is defined, there is no selection of a fontset, so disable the component
+ list.emplace_back(g_localizeStrings.Get(13278), "");
+ current = "";
+ currentValueSet = true;
+ }
+
+ if (!currentValueSet)
+ current = list[0].value;
+}
+
+void CSkinInfo::SettingOptionsSkinThemesFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ // get the chosen theme and remove the extension from the current theme (backward compat)
+ std::string settingValue = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ URIUtils::RemoveExtension(settingValue);
+ current = "SKINDEFAULT";
+
+ // there is a default theme (just Textures.xbt)
+ // any other *.xbt files are additional themes on top of this one.
+
+ // add the default Label
+ list.emplace_back(g_localizeStrings.Get(15109), "SKINDEFAULT"); // the standard Textures.xbt will be used
+
+ // search for themes in the current skin!
+ std::vector<std::string> vecTheme;
+ CUtil::GetSkinThemes(vecTheme);
+
+ // sort the themes for GUI and list them
+ for (int i = 0; i < (int) vecTheme.size(); ++i)
+ list.emplace_back(vecTheme[i], vecTheme[i]);
+
+ // try to find the best matching value
+ for (const auto& elem : list)
+ {
+ if (StringUtils::EqualsNoCase(elem.value, settingValue))
+ current = settingValue;
+ }
+}
+
+void CSkinInfo::SettingOptionsStartupWindowsFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ if (!g_SkinInfo)
+ return;
+
+ int settingValue = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ current = -1;
+
+ const std::vector<CStartupWindow> &startupWindows = g_SkinInfo->GetStartupWindows();
+
+ for (std::vector<CStartupWindow>::const_iterator it = startupWindows.begin(); it != startupWindows.end(); ++it)
+ {
+ std::string windowName = it->m_name;
+ if (StringUtils::IsNaturalNumber(windowName))
+ windowName = g_localizeStrings.Get(atoi(windowName.c_str()));
+ int windowID = it->m_id;
+
+ list.emplace_back(windowName, windowID);
+
+ if (settingValue == windowID)
+ current = settingValue;
+ }
+
+ // if the current value hasn't been properly set, set it to the first window in the list
+ if (current < 0)
+ current = list[0].value;
+}
+
+void CSkinInfo::ToggleDebug()
+{
+ m_debugging = !m_debugging;
+}
+
+int CSkinInfo::TranslateString(const std::string &setting)
+{
+ // run through and see if we have this setting
+ for (const auto& it : m_strings)
+ {
+ if (StringUtils::EqualsNoCase(setting, it.second->name))
+ return it.first;
+ }
+
+ // didn't find it - insert it
+ CSkinSettingStringPtr skinString(new CSkinSettingString());
+ skinString->name = setting;
+
+ int number = m_bools.size() + m_strings.size();
+ m_strings.insert(std::pair<int, CSkinSettingStringPtr>(number, skinString));
+
+ return number;
+}
+
+int CSkinInfo::GetInt(int setting) const
+{
+ const std::string settingValue = GetString(setting);
+ if (settingValue.empty())
+ {
+ return -1;
+ }
+ int settingValueInt{-1};
+ std::from_chars(settingValue.data(), settingValue.data() + settingValue.size(), settingValueInt);
+ return settingValueInt;
+}
+
+const std::string& CSkinInfo::GetString(int setting) const
+{
+ const auto& it = m_strings.find(setting);
+ if (it != m_strings.end())
+ return it->second->value;
+
+ return StringUtils::Empty;
+}
+
+void CSkinInfo::SetString(int setting, const std::string &label)
+{
+ auto&& it = m_strings.find(setting);
+ if (it != m_strings.end())
+ {
+ it->second->value = label;
+ m_settingsUpdateHandler->TriggerSave();
+ return;
+ }
+
+ CLog::Log(LOGFATAL, "{}: unknown setting ({}) requested", __FUNCTION__, setting);
+ assert(false);
+}
+
+int CSkinInfo::TranslateBool(const std::string &setting)
+{
+ // run through and see if we have this setting
+ for (const auto& it : m_bools)
+ {
+ if (StringUtils::EqualsNoCase(setting, it.second->name))
+ return it.first;
+ }
+
+ // didn't find it - insert it
+ CSkinSettingBoolPtr skinBool(new CSkinSettingBool());
+ skinBool->name = setting;
+
+ int number = m_bools.size() + m_strings.size();
+ m_bools.insert(std::pair<int, CSkinSettingBoolPtr>(number, skinBool));
+ m_settingsUpdateHandler->TriggerSave();
+
+ return number;
+}
+
+bool CSkinInfo::GetBool(int setting) const
+{
+ const auto& it = m_bools.find(setting);
+ if (it != m_bools.end())
+ return it->second->value;
+
+ // default is to return false
+ return false;
+}
+
+void CSkinInfo::SetBool(int setting, bool set)
+{
+ auto&& it = m_bools.find(setting);
+ if (it != m_bools.end())
+ {
+ it->second->value = set;
+ m_settingsUpdateHandler->TriggerSave();
+ return;
+ }
+
+ CLog::Log(LOGFATAL, "{}: unknown setting ({}) requested", __FUNCTION__, setting);
+ assert(false);
+}
+
+std::set<CSkinSettingPtr> CSkinInfo::GetSkinSettings() const
+{
+ std::set<CSkinSettingPtr> settings;
+
+ for (const auto& setting : m_settings)
+ settings.insert(setting.second);
+
+ return settings;
+}
+
+CSkinSettingPtr CSkinInfo::GetSkinSetting(const std::string& settingId)
+{
+ const auto& it = m_settings.find(settingId);
+ if (it != m_settings.end())
+ return it->second;
+
+ return nullptr;
+}
+
+std::shared_ptr<const CSkinSetting> CSkinInfo::GetSkinSetting(const std::string& settingId) const
+{
+ const auto& it = m_settings.find(settingId);
+ if (it != m_settings.end())
+ return it->second;
+
+ return nullptr;
+}
+
+void CSkinInfo::Reset(const std::string &setting)
+{
+ // run through and see if we have this setting as a string
+ for (auto& it : m_strings)
+ {
+ if (StringUtils::EqualsNoCase(setting, it.second->name))
+ {
+ it.second->value.clear();
+ m_settingsUpdateHandler->TriggerSave();
+ return;
+ }
+ }
+
+ // and now check for the skin bool
+ for (auto& it : m_bools)
+ {
+ if (StringUtils::EqualsNoCase(setting, it.second->name))
+ {
+ it.second->value = false;
+ m_settingsUpdateHandler->TriggerSave();
+ return;
+ }
+ }
+}
+
+void CSkinInfo::Reset()
+{
+ // clear all the settings and strings from this skin.
+ for (auto& it : m_bools)
+ it.second->value = false;
+
+ for (auto& it : m_strings)
+ it.second->value.clear();
+
+ m_settingsUpdateHandler->TriggerSave();
+}
+
+std::set<CSkinSettingPtr> CSkinInfo::ParseSettings(const TiXmlElement* rootElement)
+{
+ std::set<CSkinSettingPtr> settings;
+ if (rootElement == nullptr)
+ return settings;
+
+ const TiXmlElement *settingElement = rootElement->FirstChildElement(XML_SETTING);
+ while (settingElement != nullptr)
+ {
+ CSkinSettingPtr setting = ParseSetting(settingElement);
+ if (setting != nullptr)
+ settings.insert(setting);
+
+ settingElement = settingElement->NextSiblingElement(XML_SETTING);
+ }
+
+ return settings;
+}
+
+CSkinSettingPtr CSkinInfo::ParseSetting(const TiXmlElement* element)
+{
+ if (element == nullptr)
+ return CSkinSettingPtr();
+
+ std::string settingType = XMLUtils::GetAttribute(element, XML_ATTR_TYPE);
+ CSkinSettingPtr setting;
+ if (settingType == "string")
+ setting = CSkinSettingPtr(new CSkinSettingString());
+ else if (settingType == "bool")
+ setting = CSkinSettingPtr(new CSkinSettingBool());
+ else
+ return CSkinSettingPtr();
+
+ if (setting == nullptr)
+ return CSkinSettingPtr();
+
+ if (!setting->Deserialize(element))
+ return CSkinSettingPtr();
+
+ return setting;
+}
+
+bool CSkinInfo::SettingsLoaded(AddonInstanceId id /* = ADDON_SETTINGS_ID */) const
+{
+ if (id != ADDON_SETTINGS_ID)
+ return false;
+
+ return !m_strings.empty() || !m_bools.empty();
+}
+
+bool CSkinInfo::SettingsFromXML(const CXBMCTinyXML& doc,
+ bool loadDefaults,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ const TiXmlElement *rootElement = doc.RootElement();
+ if (rootElement == nullptr || rootElement->ValueStr().compare(XML_SETTINGS) != 0)
+ {
+ CLog::Log(LOGWARNING, "CSkinInfo: no <settings> tag found");
+ return false;
+ }
+
+ m_settings.clear();
+ m_strings.clear();
+ m_bools.clear();
+
+ int number = 0;
+ std::set<CSkinSettingPtr> settings = ParseSettings(rootElement);
+ for (const auto& setting : settings)
+ {
+ if (setting->GetType() == "string")
+ {
+ m_settings.insert(std::make_pair(setting->name, setting));
+ m_strings.insert(
+ std::make_pair(number++, std::dynamic_pointer_cast<CSkinSettingString>(setting)));
+ }
+ else if (setting->GetType() == "bool")
+ {
+ m_settings.insert(std::make_pair(setting->name, setting));
+ m_bools.insert(
+ std::make_pair(number++, std::dynamic_pointer_cast<CSkinSettingBool>(setting)));
+ }
+ else
+ CLog::Log(LOGWARNING, "CSkinInfo: ignoring setting of unknown type \"{}\"",
+ setting->GetType());
+ }
+
+ return true;
+}
+
+bool CSkinInfo::SettingsToXML(CXBMCTinyXML& doc, AddonInstanceId id /* = ADDON_SETTINGS_ID */) const
+{
+ // add the <skinsettings> tag
+ TiXmlElement rootElement(XML_SETTINGS);
+ TiXmlNode *settingsNode = doc.InsertEndChild(rootElement);
+ if (settingsNode == nullptr)
+ {
+ CLog::Log(LOGWARNING, "CSkinInfo: could not create <settings> tag");
+ return false;
+ }
+
+ TiXmlElement* settingsElement = settingsNode->ToElement();
+ for (const auto& it : m_bools)
+ {
+ if (!it.second->Serialize(settingsElement))
+ CLog::Log(LOGWARNING, "CSkinInfo: failed to save string setting \"{}\"", it.second->name);
+ }
+
+ for (const auto& it : m_strings)
+ {
+ if (!it.second->Serialize(settingsElement))
+ CLog::Log(LOGWARNING, "CSkinInfo: failed to save bool setting \"{}\"", it.second->name);
+ }
+
+ return true;
+}
+
+void CSkinSettingUpdateHandler::OnTimeout()
+{
+ m_addon.SaveSettings();
+}
+
+void CSkinSettingUpdateHandler::TriggerSave()
+{
+ if (m_timer.IsRunning())
+ m_timer.Restart();
+ else
+ m_timer.Start(DELAY);
+}
+
+} /*namespace ADDON*/
diff --git a/xbmc/addons/Skin.h b/xbmc/addons/Skin.h
new file mode 100644
index 0000000..4126167
--- /dev/null
+++ b/xbmc/addons/Skin.h
@@ -0,0 +1,297 @@
+/*
+ * 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 "addons/Addon.h"
+#include "addons/gui/skin/SkinTimerManager.h"
+#include "guilib/GUIIncludes.h" // needed for the GUIInclude member
+#include "windowing/GraphicContext.h" // needed for the RESOLUTION members
+
+#include <map>
+#include <memory>
+#include <set>
+#include <utility>
+#include <vector>
+
+#define CREDIT_LINE_LENGTH 50
+
+class CSetting;
+struct IntegerSettingOption;
+struct StringSettingOption;
+
+namespace ADDON
+{
+
+class CSkinSettingUpdateHandler;
+
+class CSkinSetting
+{
+public:
+ virtual ~CSkinSetting() = default;
+
+ bool Serialize(TiXmlElement* parent) const;
+
+ virtual std::string GetType() const = 0;
+
+ virtual bool Deserialize(const TiXmlElement* element);
+
+ std::string name;
+
+protected:
+ virtual bool SerializeSetting(TiXmlElement* element) const = 0;
+};
+
+typedef std::shared_ptr<CSkinSetting> CSkinSettingPtr;
+
+class CSkinSettingString : public CSkinSetting
+{
+public:
+ ~CSkinSettingString() override = default;
+
+ std::string GetType() const override { return "string"; }
+
+ bool Deserialize(const TiXmlElement* element) override;
+
+ std::string value;
+
+protected:
+ bool SerializeSetting(TiXmlElement* element) const override;
+};
+
+typedef std::shared_ptr<CSkinSettingString> CSkinSettingStringPtr;
+
+class CSkinSettingBool : public CSkinSetting
+{
+public:
+ ~CSkinSettingBool() override = default;
+
+ std::string GetType() const override { return "bool"; }
+
+ bool Deserialize(const TiXmlElement* element) override;
+
+ bool value = false;
+
+protected:
+ bool SerializeSetting(TiXmlElement* element) const override;
+};
+
+typedef std::shared_ptr<CSkinSettingBool> CSkinSettingBoolPtr;
+
+class CSkinInfo : public CAddon
+{
+public:
+ class CStartupWindow
+ {
+ public:
+ CStartupWindow(int id, const char *name):
+ m_id(id), m_name(name)
+ {
+ };
+ int m_id;
+ std::string m_name;
+ };
+
+ explicit CSkinInfo(const AddonInfoPtr& addonInfo);
+ //FIXME: CAddonCallbacksGUI/WindowXML hack
+ explicit CSkinInfo(
+ const AddonInfoPtr& addonInfo,
+ const RESOLUTION_INFO& resolution);
+
+ CSkinInfo(
+ const AddonInfoPtr& addonInfo,
+ const RESOLUTION_INFO& resolution,
+ const std::vector<RESOLUTION_INFO>& resolutions,
+ float effectsSlowDown,
+ bool debugging);
+
+ ~CSkinInfo() override;
+
+ /*! \brief Load resolution information from directories in Path().
+ */
+ void Start();
+
+ bool HasSkinFile(const std::string &strFile) const;
+
+ /*! \brief Get the full path to the specified file in the skin
+ We search for XML files in the skin folder that best matches the current resolution.
+ \param file XML file to look for
+ \param res [out] If non-NULL, the resolution that the returned XML file is in is returned. Defaults to NULL.
+ \param baseDir [in] If non-empty, the given directory is searched instead of the skin's directory. Defaults to empty.
+ \return path to the XML file
+ */
+ std::string GetSkinPath(const std::string& file,
+ RESOLUTION_INFO* res = nullptr,
+ const std::string& baseDir = "") const;
+
+ /*! \brief Return whether skin debugging is enabled
+ \return true if skin debugging (set via <debugging>true</debugging> in addon.xml) is enabled.
+ */
+ bool IsDebugging() const { return m_debugging; }
+
+ /*! \brief Get the id of the first window to load
+ The first window is generally Startup.xml unless it doesn't exist or if the skinner
+ has specified which start windows they support and the user is going to somewhere other
+ than the home screen.
+ \return id of the first window to load
+ */
+ int GetFirstWindow() const;
+
+ /*! \brief Get the id of the window the user wants to start in after any skin animation
+ \return id of the start window
+ */
+ int GetStartWindow() const;
+
+ /*! \brief Translate a resolution string
+ \param name the string to translate
+ \param res [out] the resolution structure if name is valid
+ \return true if the resolution is valid, false otherwise
+ */
+ static bool TranslateResolution(const std::string &name, RESOLUTION_INFO &res);
+
+ void ResolveIncludes(TiXmlElement* node,
+ std::map<INFO::InfoPtr, bool>* xmlIncludeConditions = nullptr);
+
+ float GetEffectsSlowdown() const { return m_effectsSlowDown; }
+
+ const std::vector<CStartupWindow>& GetStartupWindows() const { return m_startupWindows; }
+
+ /*! \brief Retrieve the skin paths to search for skin XML files
+ \param paths [out] vector of paths to search, in order.
+ */
+ void GetSkinPaths(std::vector<std::string> &paths) const;
+
+ bool IsInUse() const override;
+
+ const std::string& GetCurrentAspect() const { return m_currentAspect; }
+
+ void LoadIncludes();
+
+ /*! \brief Load the defined skin timers
+ \details Skin timers are defined in Timers.xml \sa Skin_Timers
+ */
+ void LoadTimers();
+
+ /*! \brief Starts evaluating timers
+ */
+ void ProcessTimers();
+
+ /*! \brief Called when unloading a skin, allows to cleanup specific
+ * skin resources.
+ */
+ void Unload();
+
+ void ToggleDebug();
+ const INFO::CSkinVariableString* CreateSkinVariable(const std::string& name, int context);
+
+ static void SettingOptionsSkinColorsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsSkinFontsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsSkinThemesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsStartupWindowsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ /*! \brief Don't handle skin settings like normal addon settings
+ */
+ bool HasSettings(AddonInstanceId id = ADDON_SETTINGS_ID) override { return false; }
+ bool HasUserSettings(AddonInstanceId id = ADDON_SETTINGS_ID) override { return false; }
+
+ int TranslateString(const std::string &setting);
+ const std::string& GetString(int setting) const;
+ void SetString(int setting, const std::string &label);
+
+ int TranslateBool(const std::string &setting);
+ bool GetBool(int setting) const;
+ void SetBool(int setting, bool set);
+
+ /*! \brief Get the skin setting value as an integer value
+ * \param setting - the setting id
+ * \return the setting value as an integer, -1 if no conversion is possible
+ */
+ int GetInt(int setting) const;
+
+ std::set<CSkinSettingPtr> GetSkinSettings() const;
+ CSkinSettingPtr GetSkinSetting(const std::string& settingId);
+ std::shared_ptr<const CSkinSetting> GetSkinSetting(const std::string& settingId) const;
+
+ void Reset(const std::string &setting);
+ void Reset();
+
+ static std::set<CSkinSettingPtr> ParseSettings(const TiXmlElement* rootElement);
+
+ void OnPreInstall() override;
+ void OnPostInstall(bool update, bool modal) override;
+
+ // skin timer methods
+
+ /*! \brief Checks if the timer with name `timer` is running
+ \param timer the name of the skin timer
+ \return true if the given timer exists and is running, false otherwise
+ */
+ bool TimerIsRunning(const std::string& timer) const;
+
+ /*! \brief Get the elapsed seconds since the timer with name `timer` was started
+ \param timer the name of the skin timer
+ \return the elapsed time in seconds the given timer is running (0 if not running or if it does not exist)
+ */
+ float GetTimerElapsedSeconds(const std::string& timer) const;
+
+ /*! \brief Starts/Enables a given skin timer
+ \param timer the name of the skin timer
+ */
+ void TimerStart(const std::string& timer) const;
+
+ /*! \brief Stops/Disables a given skin timer
+ \param timer the name of the skin timer
+ */
+ void TimerStop(const std::string& timer) const;
+
+protected:
+ bool LoadStartupWindows(const AddonInfoPtr& addonInfo);
+
+ static CSkinSettingPtr ParseSetting(const TiXmlElement* element);
+
+ bool SettingsLoaded(AddonInstanceId id = ADDON_SETTINGS_ID) const override;
+ bool SettingsFromXML(const CXBMCTinyXML& doc,
+ bool loadDefaults,
+ AddonInstanceId id = ADDON_SETTINGS_ID) override;
+ bool SettingsToXML(CXBMCTinyXML& doc, AddonInstanceId id = ADDON_SETTINGS_ID) const override;
+
+ RESOLUTION_INFO m_defaultRes;
+ std::vector<RESOLUTION_INFO> m_resolutions;
+
+ float m_effectsSlowDown;
+ CGUIIncludes m_includes;
+ std::string m_currentAspect;
+
+ std::vector<CStartupWindow> m_startupWindows;
+ bool m_debugging;
+
+ /*! Manager/Owner of skin timers */
+ CSkinTimerManager m_skinTimerManager;
+
+private:
+ std::map<int, CSkinSettingStringPtr> m_strings;
+ std::map<int, CSkinSettingBoolPtr> m_bools;
+ std::map<std::string, CSkinSettingPtr> m_settings;
+ std::unique_ptr<CSkinSettingUpdateHandler> m_settingsUpdateHandler;
+};
+
+} /*namespace ADDON*/
+
+extern std::shared_ptr<ADDON::CSkinInfo> g_SkinInfo;
diff --git a/xbmc/addons/UISoundsResource.cpp b/xbmc/addons/UISoundsResource.cpp
new file mode 100644
index 0000000..6ca53dd
--- /dev/null
+++ b/xbmc/addons/UISoundsResource.cpp
@@ -0,0 +1,45 @@
+/*
+ * 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 "UISoundsResource.h"
+
+#include "ServiceBroker.h"
+#include "addons/addoninfo/AddonType.h"
+#include "guilib/GUIAudioManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+
+namespace ADDON
+{
+
+CUISoundsResource::CUISoundsResource(const AddonInfoPtr& addonInfo)
+ : CResource(addonInfo, AddonType::RESOURCE_UISOUNDS)
+{
+}
+
+bool CUISoundsResource::IsAllowed(const std::string& file) const
+{
+ return StringUtils::EqualsNoCase(file, "sounds.xml")
+ || URIUtils::HasExtension(file, ".wav");
+}
+
+bool CUISoundsResource::IsInUse() const
+{
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SOUNDSKIN) == ID();
+}
+
+void CUISoundsResource::OnPostInstall(bool update, bool modal)
+{
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (IsInUse() && gui)
+ gui->GetAudioManager().Load();
+}
+
+}
diff --git a/xbmc/addons/UISoundsResource.h b/xbmc/addons/UISoundsResource.h
new file mode 100644
index 0000000..0a07f11
--- /dev/null
+++ b/xbmc/addons/UISoundsResource.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "addons/Resource.h"
+
+namespace ADDON
+{
+
+class CUISoundsResource : public CResource
+{
+public:
+ explicit CUISoundsResource(const AddonInfoPtr& addonInfo);
+
+ bool IsAllowed(const std::string &file) const override;
+ bool IsInUse() const override;
+ void OnPostInstall(bool update, bool modal) override;
+};
+
+}
diff --git a/xbmc/addons/VFSEntry.cpp b/xbmc/addons/VFSEntry.cpp
new file mode 100644
index 0000000..063ba25
--- /dev/null
+++ b/xbmc/addons/VFSEntry.cpp
@@ -0,0 +1,770 @@
+/*
+ * Copyright (C) 2013 Arne Morten Kvarving
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "VFSEntry.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/interfaces/Filesystem.h"
+#include "network/ZeroconfBrowser.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <utility>
+
+#if defined(TARGET_WINDOWS)
+#ifndef S_IFLNK
+#define S_IFLNK 0120000
+#endif
+#ifndef S_IFBLK
+#define S_IFBLK 0
+#endif
+#ifndef S_IFSOCK
+#define S_IFSOCK 0
+#endif
+#ifndef S_IFREG
+#define S_IFREG _S_IFREG
+#endif
+#ifndef S_IFCHR
+#define S_IFCHR _S_IFCHR
+#endif
+#ifndef S_IFDIR
+#define S_IFDIR _S_IFDIR
+#endif
+#ifndef S_IFIFO
+#define S_IFIFO _S_IFIFO
+#endif
+#endif
+
+namespace ADDON
+{
+
+CVFSAddonCache::~CVFSAddonCache()
+{
+ Deinit();
+}
+
+void CVFSAddonCache::Init()
+{
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CVFSAddonCache::OnEvent);
+
+ // Load all available VFS addons during Kodi start
+ std::vector<AddonInfoPtr> addonInfos;
+ CServiceBroker::GetAddonMgr().GetAddonInfos(addonInfos, true, AddonType::VFS);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& addonInfo : addonInfos)
+ {
+ VFSEntryPtr vfs = std::make_shared<CVFSEntry>(addonInfo);
+ vfs->Addon()->RegisterInformer(this);
+
+ m_addonsInstances.emplace_back(vfs);
+
+ if (!vfs->GetZeroconfType().empty())
+ CZeroconfBrowser::GetInstance()->AddServiceType(vfs->GetZeroconfType());
+ }
+}
+
+void CVFSAddonCache::Deinit()
+{
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+}
+
+const std::vector<VFSEntryPtr> CVFSAddonCache::GetAddonInstances()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_addonsInstances;
+}
+
+VFSEntryPtr CVFSAddonCache::GetAddonInstance(const std::string& strId)
+{
+ VFSEntryPtr addon;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const auto& itAddon = std::find_if(m_addonsInstances.begin(), m_addonsInstances.end(),
+ [&strId](const VFSEntryPtr& addon)
+ {
+ return addon->ID() == strId;
+ });
+
+ if (itAddon != m_addonsInstances.end())
+ addon = *itAddon;
+
+ return addon;
+}
+
+void CVFSAddonCache::OnEvent(const AddonEvent& event)
+{
+ if (typeid(event) == typeid(AddonEvents::Disabled))
+ {
+ for (const auto& vfs : m_addonsInstances)
+ {
+ if (vfs->ID() == event.addonId && !vfs->GetZeroconfType().empty())
+ CZeroconfBrowser::GetInstance()->RemoveServiceType(vfs->GetZeroconfType());
+ }
+ }
+
+ if (typeid(event) == typeid(AddonEvents::Enabled) ||
+ typeid(event) == typeid(AddonEvents::Disabled) ||
+ typeid(event) == typeid(AddonEvents::ReInstalled))
+ {
+ if (CServiceBroker::GetAddonMgr().HasType(event.addonId, AddonType::VFS))
+ Update(event.addonId);
+ }
+ else if (typeid(event) == typeid(AddonEvents::UnInstalled))
+ {
+ Update(event.addonId);
+ }
+}
+
+bool CVFSAddonCache::IsInUse(const std::string& id)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const auto& itAddon = std::find_if(m_addonsInstances.begin(), m_addonsInstances.end(),
+ [&id](const VFSEntryPtr& addon) { return addon->ID() == id; });
+ if (itAddon != m_addonsInstances.end() && (*itAddon).use_count() > 1)
+ return true;
+ return false;
+}
+
+void CVFSAddonCache::Update(const std::string& id)
+{
+ std::vector<VFSEntryPtr> addonmap;
+
+ // Stop used instance if present, otherwise the new becomes created on already created addon base one.
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const auto& itAddon =
+ std::find_if(m_addonsInstances.begin(), m_addonsInstances.end(),
+ [&id](const VFSEntryPtr& addon) { return addon->ID() == id; });
+
+ if (itAddon != m_addonsInstances.end())
+ {
+ (*itAddon)->Addon()->RegisterInformer(nullptr);
+ m_addonsInstances.erase(itAddon);
+ }
+ }
+
+ // Create and init the new VFS addon instance
+ AddonInfoPtr addonInfo = CServiceBroker::GetAddonMgr().GetAddonInfo(id, AddonType::VFS);
+ if (addonInfo && !CServiceBroker::GetAddonMgr().IsAddonDisabled(id))
+ {
+ VFSEntryPtr vfs = std::make_shared<CVFSEntry>(addonInfo);
+
+ if (!vfs->GetZeroconfType().empty())
+ CZeroconfBrowser::GetInstance()->AddServiceType(vfs->GetZeroconfType());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_addonsInstances.emplace_back(vfs);
+ }
+}
+
+class CVFSURLWrapper
+{
+ public:
+ explicit CVFSURLWrapper(const CURL& url2)
+ {
+ m_strings.push_back(url2.Get());
+ m_strings.push_back(url2.GetDomain());
+ m_strings.push_back(url2.GetHostName());
+ m_strings.push_back(url2.GetFileName());
+ m_strings.push_back(url2.GetOptions());
+ m_strings.push_back(url2.GetUserName());
+ m_strings.push_back(url2.GetPassWord());
+ m_strings.push_back(url2.GetRedacted());
+ m_strings.push_back(url2.GetShareName());
+ m_strings.push_back(url2.GetProtocol());
+
+ url.url = m_strings[0].c_str();
+ url.domain = m_strings[1].c_str();
+ url.hostname = m_strings[2].c_str();
+ url.filename = m_strings[3].c_str();
+ url.port = url2.GetPort();
+ url.options = m_strings[4].c_str();
+ url.username = m_strings[5].c_str();
+ url.password = m_strings[6].c_str();
+ url.redacted = m_strings[7].c_str();
+ url.sharename = m_strings[8].c_str();
+ url.protocol = m_strings[9].c_str();
+ }
+
+ VFSURL url;
+ protected:
+ std::vector<std::string> m_strings;
+};
+
+CVFSEntry::ProtocolInfo::ProtocolInfo(const AddonInfoPtr& addonInfo)
+ : supportPath(addonInfo->Type(AddonType::VFS)->GetValue("@supportPath").asBoolean()),
+ supportUsername(addonInfo->Type(AddonType::VFS)->GetValue("@supportUsername").asBoolean()),
+ supportPassword(addonInfo->Type(AddonType::VFS)->GetValue("@supportPassword").asBoolean()),
+ supportPort(addonInfo->Type(AddonType::VFS)->GetValue("@supportPort").asBoolean()),
+ supportBrowsing(addonInfo->Type(AddonType::VFS)->GetValue("@supportBrowsing").asBoolean()),
+ supportWrite(addonInfo->Type(AddonType::VFS)->GetValue("@supportWrite").asBoolean()),
+ defaultPort(addonInfo->Type(AddonType::VFS)->GetValue("@defaultPort").asInteger()),
+ type(addonInfo->Type(AddonType::VFS)->GetValue("@protocols").asString()),
+ label(addonInfo->Type(AddonType::VFS)->GetValue("@label").asInteger())
+{
+}
+
+CVFSEntry::CVFSEntry(const AddonInfoPtr& addonInfo)
+ : IAddonInstanceHandler(ADDON_INSTANCE_VFS, addonInfo),
+ m_protocols(addonInfo->Type(AddonType::VFS)->GetValue("@protocols").asString()),
+ m_extensions(addonInfo->Type(AddonType::VFS)->GetValue("@extensions").asString()),
+ m_zeroconf(addonInfo->Type(AddonType::VFS)->GetValue("@zeroconf").asString()),
+ m_files(addonInfo->Type(AddonType::VFS)->GetValue("@files").asBoolean()),
+ m_directories(addonInfo->Type(AddonType::VFS)->GetValue("@directories").asBoolean()),
+ m_filedirectories(addonInfo->Type(AddonType::VFS)->GetValue("@filedirectories").asBoolean()),
+ m_protocolInfo(addonInfo)
+{
+ if (!addonInfo->Type(AddonType::VFS)->GetValue("@supportDialog").asBoolean())
+ m_protocolInfo.type.clear();
+
+ // Create "C" interface structures, used as own parts to prevent API problems on update
+ m_ifc.vfs = new AddonInstance_VFSEntry;
+ m_ifc.vfs->props = new AddonProps_VFSEntry();
+ m_ifc.vfs->toAddon = new KodiToAddonFuncTable_VFSEntry();
+ m_ifc.vfs->toKodi = new AddonToKodiFuncTable_VFSEntry();
+
+ m_ifc.vfs->toKodi->kodiInstance = this;
+ if (CreateInstance() != ADDON_STATUS_OK)
+ CLog::Log(LOGFATAL, "CVFSEntry - Couldn't create instance on add-on '{}'", addonInfo->Name());
+}
+
+CVFSEntry::~CVFSEntry()
+{
+ DestroyInstance();
+
+ // Delete "C" interface structures
+ delete m_ifc.vfs->toAddon;
+ delete m_ifc.vfs->toKodi;
+ delete m_ifc.vfs->props;
+ delete m_ifc.vfs;
+}
+
+void* CVFSEntry::Open(const CURL& url)
+{
+ if (!m_ifc.vfs->toAddon->open)
+ return nullptr;
+
+ CVFSURLWrapper url2(url);
+ return m_ifc.vfs->toAddon->open(m_ifc.vfs, &url2.url);
+}
+
+void* CVFSEntry::OpenForWrite(const CURL& url, bool bOverWrite)
+{
+ if (!m_ifc.vfs->toAddon->open_for_write)
+ return nullptr;
+
+ CVFSURLWrapper url2(url);
+ return m_ifc.vfs->toAddon->open_for_write(m_ifc.vfs, &url2.url, bOverWrite);
+}
+
+bool CVFSEntry::Exists(const CURL& url)
+{
+ if (!m_ifc.vfs->toAddon->exists)
+ return false;
+
+ CVFSURLWrapper url2(url);
+ return m_ifc.vfs->toAddon->exists(m_ifc.vfs, &url2.url);
+}
+
+int CVFSEntry::Stat(const CURL& url, struct __stat64* buffer)
+{
+ int ret = -1;
+ if (!m_ifc.vfs->toAddon->stat)
+ return ret;
+
+ CVFSURLWrapper url2(url);
+ STAT_STRUCTURE statBuffer = {};
+ ret = m_ifc.vfs->toAddon->stat(m_ifc.vfs, &url2.url, &statBuffer);
+
+ *buffer = {};
+ buffer->st_dev = statBuffer.deviceId;
+ buffer->st_ino = statBuffer.fileSerialNumber;
+ buffer->st_size = statBuffer.size;
+ buffer->st_atime = statBuffer.accessTime;
+ buffer->st_mtime = statBuffer.modificationTime;
+ buffer->st_ctime = statBuffer.statusTime;
+ buffer->st_mode = 0;
+ if (statBuffer.isDirectory)
+ buffer->st_mode |= S_IFDIR;
+ if (statBuffer.isSymLink)
+ buffer->st_mode |= S_IFLNK;
+ if (statBuffer.isBlock)
+ buffer->st_mode |= S_IFBLK;
+ if (statBuffer.isCharacter)
+ buffer->st_mode |= S_IFCHR;
+ if (statBuffer.isFifo)
+ buffer->st_mode |= S_IFIFO;
+ if (statBuffer.isRegular)
+ buffer->st_mode |= S_IFREG;
+ if (statBuffer.isSocket)
+ buffer->st_mode |= S_IFSOCK;
+
+ return ret;
+}
+
+ssize_t CVFSEntry::Read(void* ctx, void* lpBuf, size_t uiBufSize)
+{
+ if (!m_ifc.vfs->toAddon->read)
+ return 0;
+
+ return m_ifc.vfs->toAddon->read(m_ifc.vfs, ctx, static_cast<uint8_t*>(lpBuf), uiBufSize);
+}
+
+ssize_t CVFSEntry::Write(void* ctx, const void* lpBuf, size_t uiBufSize)
+{
+ if (!m_ifc.vfs->toAddon->write)
+ return 0;
+
+ return m_ifc.vfs->toAddon->write(m_ifc.vfs, ctx, static_cast<const uint8_t*>(lpBuf), uiBufSize);
+}
+
+int64_t CVFSEntry::Seek(void* ctx, int64_t position, int whence)
+{
+ if (!m_ifc.vfs->toAddon->seek)
+ return 0;
+
+ return m_ifc.vfs->toAddon->seek(m_ifc.vfs, ctx, position, whence);
+}
+
+int CVFSEntry::Truncate(void* ctx, int64_t size)
+{
+ if (!m_ifc.vfs->toAddon->truncate)
+ return 0;
+
+ return m_ifc.vfs->toAddon->truncate(m_ifc.vfs, ctx, size);
+}
+
+void CVFSEntry::Close(void* ctx)
+{
+ if (m_ifc.vfs->toAddon->close)
+ m_ifc.vfs->toAddon->close(m_ifc.vfs, ctx);
+}
+
+int64_t CVFSEntry::GetPosition(void* ctx)
+{
+ if (!m_ifc.vfs->toAddon->get_position)
+ return 0;
+
+ return m_ifc.vfs->toAddon->get_position(m_ifc.vfs, ctx);
+}
+
+int CVFSEntry::GetChunkSize(void* ctx)
+{
+ if (!m_ifc.vfs->toAddon->get_chunk_size)
+ return 0;
+
+ return m_ifc.vfs->toAddon->get_chunk_size(m_ifc.vfs, ctx);
+}
+
+int64_t CVFSEntry::GetLength(void* ctx)
+{
+ if (!m_ifc.vfs->toAddon->get_length)
+ return 0;
+
+ return m_ifc.vfs->toAddon->get_length(m_ifc.vfs, ctx);
+}
+
+int CVFSEntry::IoControl(void* ctx, XFILE::EIoControl request, void* param)
+{
+ switch (request)
+ {
+ case XFILE::EIoControl::IOCTRL_SEEK_POSSIBLE:
+ {
+ if (!m_ifc.vfs->toAddon->io_control_get_seek_possible)
+ return -1;
+ return m_ifc.vfs->toAddon->io_control_get_seek_possible(m_ifc.vfs, ctx) ? 1 : 0;
+ }
+ case XFILE::EIoControl::IOCTRL_CACHE_STATUS:
+ {
+ if (!m_ifc.vfs->toAddon->io_control_get_cache_status)
+ return -1;
+
+ XFILE::SCacheStatus* kodiData = static_cast<XFILE::SCacheStatus*>(param);
+ if (!kodiData)
+ return -1;
+
+ VFS_CACHE_STATUS_DATA status;
+ int ret = m_ifc.vfs->toAddon->io_control_get_cache_status(m_ifc.vfs, ctx, &status) ? 0 : -1;
+ if (ret >= 0)
+ {
+ kodiData->forward = status.forward;
+ kodiData->maxrate = status.maxrate;
+ kodiData->currate = status.currate;
+ kodiData->lowrate = status.lowrate;
+ }
+ return ret;
+ }
+ case XFILE::EIoControl::IOCTRL_CACHE_SETRATE:
+ {
+ if (!m_ifc.vfs->toAddon->io_control_set_cache_rate)
+ return -1;
+
+ uint32_t& iParam = *static_cast<uint32_t*>(param);
+ return m_ifc.vfs->toAddon->io_control_set_cache_rate(m_ifc.vfs, ctx, iParam) ? 1 : 0;
+ }
+ case XFILE::EIoControl::IOCTRL_SET_RETRY:
+ {
+ if (!m_ifc.vfs->toAddon->io_control_set_retry)
+ return -1;
+
+ bool& bParam = *static_cast<bool*>(param);
+ return m_ifc.vfs->toAddon->io_control_set_retry(m_ifc.vfs, ctx, bParam) ? 0 : -1;
+ }
+
+ // Not by addon supported io's
+ case XFILE::EIoControl::IOCTRL_SET_CACHE:
+ case XFILE::EIoControl::IOCTRL_NATIVE:
+ default:
+ break;
+ }
+
+ return -1;
+}
+
+bool CVFSEntry::Delete(const CURL& url)
+{
+ if (!m_ifc.vfs->toAddon->delete_it)
+ return false;
+
+ CVFSURLWrapper url2(url);
+ return m_ifc.vfs->toAddon->delete_it(m_ifc.vfs, &url2.url);
+}
+
+bool CVFSEntry::Rename(const CURL& url, const CURL& url2)
+{
+ if (!m_ifc.vfs->toAddon->rename)
+ return false;
+
+ CVFSURLWrapper url3(url);
+ CVFSURLWrapper url4(url2);
+ return m_ifc.vfs->toAddon->rename(m_ifc.vfs, &url3.url, &url4.url);
+}
+
+void CVFSEntry::ClearOutIdle()
+{
+ if (m_ifc.vfs->toAddon->clear_out_idle)
+ m_ifc.vfs->toAddon->clear_out_idle(m_ifc.vfs);
+}
+
+void CVFSEntry::DisconnectAll()
+{
+ if (m_ifc.vfs->toAddon->disconnect_all)
+ m_ifc.vfs->toAddon->disconnect_all(m_ifc.vfs);
+}
+
+bool CVFSEntry::DirectoryExists(const CURL& url)
+{
+ if (!m_ifc.vfs->toAddon->directory_exists)
+ return false;
+
+ CVFSURLWrapper url2(url);
+ return m_ifc.vfs->toAddon->directory_exists(m_ifc.vfs, &url2.url);
+}
+
+bool CVFSEntry::RemoveDirectory(const CURL& url)
+{
+ if (!m_ifc.vfs->toAddon->remove_directory)
+ return false;
+
+ CVFSURLWrapper url2(url);
+ return m_ifc.vfs->toAddon->remove_directory(m_ifc.vfs, &url2.url);
+}
+
+bool CVFSEntry::CreateDirectory(const CURL& url)
+{
+ if (!m_ifc.vfs->toAddon->create_directory)
+ return false;
+
+ CVFSURLWrapper url2(url);
+ return m_ifc.vfs->toAddon->create_directory(m_ifc.vfs, &url2.url);
+}
+
+static void VFSDirEntriesToCFileItemList(int num_entries,
+ VFSDirEntry* entries,
+ CFileItemList& items)
+{
+ for (int i=0;i<num_entries;++i)
+ {
+ CFileItemPtr item(new CFileItem());
+ item->SetLabel(entries[i].label);
+ item->SetPath(entries[i].path);
+ item->m_dwSize = entries[i].size;
+ item->m_dateTime = entries[i].date_time;
+ item->m_bIsFolder = entries[i].folder;
+ if (entries[i].title)
+ item->m_strTitle = entries[i].title;
+ for (unsigned int j=0;j<entries[i].num_props;++j)
+ {
+ if (StringUtils::CompareNoCase(entries[i].properties[j].name, "propmisusepreformatted") == 0)
+ {
+ if (StringUtils::CompareNoCase(entries[i].properties[j].name, "true") == 0)
+ item->SetLabelPreformatted(true);
+ else
+ item->SetLabelPreformatted(false);
+ } else
+ item->SetProperty(entries[i].properties[j].name,
+ entries[i].properties[j].val);
+ }
+ items.Add(item);
+ }
+}
+
+bool CVFSEntry::GetDirectory(const CURL& url, CFileItemList& items,
+ void* ctx)
+{
+ if (!m_ifc.vfs->toAddon->get_directory || !m_ifc.vfs->toAddon->free_directory)
+ return false;
+
+ VFSGetDirectoryCallbacks callbacks;
+ callbacks.ctx = ctx;
+ callbacks.get_keyboard_input = CVFSEntryIDirectoryWrapper::DoGetKeyboardInput;
+ callbacks.set_error_dialog = CVFSEntryIDirectoryWrapper::DoSetErrorDialog;
+ callbacks.require_authentication = CVFSEntryIDirectoryWrapper::DoRequireAuthentication;
+
+ VFSDirEntry* entries = nullptr;
+ int num_entries = 0;
+ CVFSURLWrapper url2(url);
+ bool ret =
+ m_ifc.vfs->toAddon->get_directory(m_ifc.vfs, &url2.url, &entries, &num_entries, &callbacks);
+ if (ret)
+ {
+ VFSDirEntriesToCFileItemList(num_entries, entries, items);
+ m_ifc.vfs->toAddon->free_directory(m_ifc.vfs, entries, num_entries);
+ }
+
+ return ret;
+}
+
+bool CVFSEntry::ContainsFiles(const CURL& url, CFileItemList& items)
+{
+ if (!m_ifc.vfs->toAddon->contains_files || !m_ifc.vfs->toAddon->free_directory)
+ return false;
+
+ VFSDirEntry* entries = nullptr;
+ int num_entries = 0;
+
+ CVFSURLWrapper url2(url);
+ char rootpath[ADDON_STANDARD_STRING_LENGTH];
+ rootpath[0] = 0;
+ bool ret =
+ m_ifc.vfs->toAddon->contains_files(m_ifc.vfs, &url2.url, &entries, &num_entries, rootpath);
+ if (!ret)
+ return false;
+
+ VFSDirEntriesToCFileItemList(num_entries, entries, items);
+ m_ifc.vfs->toAddon->free_directory(m_ifc.vfs, entries, num_entries);
+ if (strlen(rootpath))
+ items.SetPath(rootpath);
+
+ return true;
+}
+
+CVFSEntryIFileWrapper::CVFSEntryIFileWrapper(VFSEntryPtr ptr)
+ : m_context(nullptr), m_addon(std::move(ptr))
+{
+}
+
+CVFSEntryIFileWrapper::~CVFSEntryIFileWrapper()
+{
+ Close();
+}
+
+bool CVFSEntryIFileWrapper::Open(const CURL& url)
+{
+ m_context = m_addon->Open(url);
+ return m_context != nullptr;
+}
+
+bool CVFSEntryIFileWrapper::OpenForWrite(const CURL& url, bool bOverWrite)
+{
+ m_context = m_addon->OpenForWrite(url, bOverWrite);
+ return m_context != nullptr;
+}
+
+bool CVFSEntryIFileWrapper::Exists(const CURL& url)
+{
+ return m_addon->Exists(url);
+}
+
+int CVFSEntryIFileWrapper::Stat(const CURL& url, struct __stat64* buffer)
+{
+ return m_addon->Stat(url, buffer);
+}
+
+int CVFSEntryIFileWrapper::Truncate(int64_t size)
+{
+ return m_addon->Truncate(m_context, size);
+}
+
+ssize_t CVFSEntryIFileWrapper::Read(void* lpBuf, size_t uiBufSize)
+{
+ if (!m_context)
+ return 0;
+
+ return m_addon->Read(m_context, lpBuf, uiBufSize);
+}
+
+ssize_t CVFSEntryIFileWrapper::Write(const void* lpBuf, size_t uiBufSize)
+{
+ if (!m_context)
+ return 0;
+
+ return m_addon->Write(m_context, lpBuf, uiBufSize);
+}
+
+int64_t CVFSEntryIFileWrapper::Seek(int64_t iFilePosition, int whence)
+{
+ if (!m_context)
+ return 0;
+
+ return m_addon->Seek(m_context, iFilePosition, whence);
+}
+
+void CVFSEntryIFileWrapper::Close()
+{
+ if (m_context)
+ {
+ m_addon->Close(m_context);
+ m_context = nullptr;
+ }
+}
+
+int64_t CVFSEntryIFileWrapper::GetPosition()
+{
+ if (!m_context)
+ return 0;
+
+ return m_addon->GetPosition(m_context);
+}
+
+int CVFSEntryIFileWrapper::GetChunkSize()
+{
+ if (!m_context)
+ return 0;
+
+ return m_addon->GetChunkSize(m_context);
+}
+
+int64_t CVFSEntryIFileWrapper::GetLength()
+{
+ if (!m_context)
+ return 0;
+
+ return m_addon->GetLength(m_context);
+}
+
+int CVFSEntryIFileWrapper::IoControl(XFILE::EIoControl request, void* param)
+{
+ if (!m_context)
+ return 0;
+
+ return m_addon->IoControl(m_context, request, param);
+}
+
+bool CVFSEntryIFileWrapper::Delete(const CURL& url)
+{
+ return m_addon->Delete(url);
+}
+
+bool CVFSEntryIFileWrapper::Rename(const CURL& url, const CURL& url2)
+{
+ return m_addon->Rename(url, url2);
+}
+
+CVFSEntryIDirectoryWrapper::CVFSEntryIDirectoryWrapper(VFSEntryPtr ptr) : m_addon(std::move(ptr))
+{
+}
+
+bool CVFSEntryIDirectoryWrapper::Exists(const CURL& url)
+{
+ return m_addon->DirectoryExists(url);
+}
+
+bool CVFSEntryIDirectoryWrapper::Remove(const CURL& url)
+{
+ return m_addon->RemoveDirectory(url);
+}
+
+bool CVFSEntryIDirectoryWrapper::Create(const CURL& url)
+{
+ return m_addon->CreateDirectory(url);
+}
+
+bool CVFSEntryIDirectoryWrapper::GetDirectory(const CURL& url,
+ CFileItemList& items)
+{
+ return m_addon->GetDirectory(url, items, this);
+}
+
+bool CVFSEntryIDirectoryWrapper::DoGetKeyboardInput(void* ctx,
+ const char* heading,
+ char** input,
+ bool hidden_input)
+{
+ return static_cast<CVFSEntryIDirectoryWrapper*>(ctx)->GetKeyboardInput2(heading, input, hidden_input);
+}
+
+bool CVFSEntryIDirectoryWrapper::GetKeyboardInput2(const char* heading,
+ char** input,
+ bool hidden_input)
+{
+ std::string inp;
+ bool result;
+ if ((result=GetKeyboardInput(CVariant(std::string(heading)), inp, hidden_input)))
+ *input = strdup(inp.c_str());
+
+ return result;
+}
+
+void CVFSEntryIDirectoryWrapper::DoSetErrorDialog(void* ctx, const char* heading,
+ const char* line1,
+ const char* line2,
+ const char* line3)
+{
+ static_cast<CVFSEntryIDirectoryWrapper*>(ctx)->SetErrorDialog2(heading, line1,
+ line2, line3);
+}
+
+void CVFSEntryIDirectoryWrapper::SetErrorDialog2(const char* heading,
+ const char* line1,
+ const char* line2,
+ const char* line3)
+{
+ CVariant l2=0, l3=0;
+ if (line2)
+ l2 = std::string(line2);
+ if (line3)
+ l3 = std::string(line3);
+ if (m_flags & XFILE::DIR_FLAG_ALLOW_PROMPT)
+ SetErrorDialog(CVariant(std::string(heading)),
+ CVariant(std::string(line1)), l2, l3);
+}
+
+void CVFSEntryIDirectoryWrapper::DoRequireAuthentication(void* ctx,
+ const char* url)
+{
+ static_cast<CVFSEntryIDirectoryWrapper*>(ctx)->RequireAuthentication2(CURL(url));
+}
+
+void CVFSEntryIDirectoryWrapper::RequireAuthentication2(const CURL& url)
+{
+ if (m_flags & XFILE::DIR_FLAG_ALLOW_PROMPT)
+ RequireAuthentication(url);
+}
+
+} /*namespace ADDON*/
+
diff --git a/xbmc/addons/VFSEntry.h b/xbmc/addons/VFSEntry.h
new file mode 100644
index 0000000..f600ac1
--- /dev/null
+++ b/xbmc/addons/VFSEntry.h
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2013 Arne Morten Kvarving
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "FileItem.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/VFS.h"
+#include "filesystem/IDirectory.h"
+#include "filesystem/IFile.h"
+#include "filesystem/IFileDirectory.h"
+
+#include <utility>
+
+namespace ADDON
+{
+struct AddonEvent;
+
+class CVFSEntry;
+typedef std::shared_ptr<CVFSEntry> VFSEntryPtr;
+
+class CVFSAddonCache : public CAddonDllInformer
+{
+public:
+ virtual ~CVFSAddonCache();
+ void Init();
+ void Deinit();
+ const std::vector<VFSEntryPtr> GetAddonInstances();
+ VFSEntryPtr GetAddonInstance(const std::string& strId);
+
+protected:
+ void Update(const std::string& id);
+ void OnEvent(const AddonEvent& event);
+ bool IsInUse(const std::string& id) override;
+
+ CCriticalSection m_critSection;
+ std::vector<VFSEntryPtr> m_addonsInstances;
+};
+
+ //! \brief A virtual filesystem entry add-on.
+ class CVFSEntry : public IAddonInstanceHandler
+ {
+ public:
+ //! \brief A structure encapsulating properties of supplied protocol.
+ struct ProtocolInfo
+ {
+ bool supportPath; //!< Protocol has path in addition to server name
+ bool supportUsername; //!< Protocol uses logins
+ bool supportPassword; //!< Protocol supports passwords
+ bool supportPort; //!< Protocol supports port customization
+ bool supportBrowsing; //!< Protocol supports server browsing
+ bool supportWrite; //!< Protocol supports write operations
+ int defaultPort; //!< Default port to use for protocol
+ std::string type; //!< URL type for protocol
+ int label; //!< String ID to use as label in dialog
+
+ //! \brief The constructor reads the info from an add-on info structure.
+ ProtocolInfo(const AddonInfoPtr& addonInfo);
+ };
+
+ //! \brief Construct from add-on properties.
+ //! \param addonInfo General addon properties
+ explicit CVFSEntry(const AddonInfoPtr& addonInfo);
+ ~CVFSEntry() override;
+
+ // Things that MUST be supplied by the child classes
+ void* Open(const CURL& url);
+ void* OpenForWrite(const CURL& url, bool bOverWrite);
+ bool Exists(const CURL& url);
+ int Stat(const CURL& url, struct __stat64* buffer);
+ ssize_t Read(void* ctx, void* lpBuf, size_t uiBufSize);
+ ssize_t Write(void* ctx, const void* lpBuf, size_t uiBufSize);
+ int64_t Seek(void* ctx, int64_t iFilePosition, int iWhence = SEEK_SET);
+ int Truncate(void* ctx, int64_t size);
+ void Close(void* ctx);
+ int64_t GetPosition(void* ctx);
+ int64_t GetLength(void* ctx);
+ int GetChunkSize(void* ctx);
+ int IoControl(void* ctx, XFILE::EIoControl request, void* param);
+ bool Delete(const CURL& url);
+ bool Rename(const CURL& url, const CURL& url2);
+
+ bool GetDirectory(const CURL& url, CFileItemList& items, void* ctx);
+ bool DirectoryExists(const CURL& url);
+ bool RemoveDirectory(const CURL& url);
+ bool CreateDirectory(const CURL& url);
+ void ClearOutIdle();
+ void DisconnectAll();
+
+ bool ContainsFiles(const CURL& url, CFileItemList& items);
+
+ const std::string& GetProtocols() const { return m_protocols; }
+ const std::string& GetExtensions() const { return m_extensions; }
+ bool HasFiles() const { return m_files; }
+ bool HasDirectories() const { return m_directories; }
+ bool HasFileDirectories() const { return m_filedirectories; }
+ const std::string& GetZeroconfType() const { return m_zeroconf; }
+ const ProtocolInfo& GetProtocolInfo() const { return m_protocolInfo; }
+ protected:
+ std::string m_protocols; //!< Protocols for VFS entry.
+ std::string m_extensions; //!< Extensions for VFS entry.
+ std::string m_zeroconf; //!< Zero conf announce string for VFS protocol.
+ bool m_files; //!< Vfs entry can read files.
+ bool m_directories; //!< VFS entry can list directories.
+ bool m_filedirectories; //!< VFS entry contains file directories.
+ ProtocolInfo m_protocolInfo; //!< Info about protocol for network dialog.
+ };
+
+ //! \brief Wrapper equipping a CVFSEntry with an IFile interface.
+ //! \details Needed as CVFSEntry implements several VFS interfaces
+ //! with overlapping methods.
+ class CVFSEntryIFileWrapper : public XFILE::IFile
+ {
+ public:
+ //! \brief The constructor initializes the reference to the wrapped CVFSEntry.
+ //! \param ptr The CVFSEntry to wrap.
+ explicit CVFSEntryIFileWrapper(VFSEntryPtr ptr);
+
+ //! \brief Empty destructor.
+ ~CVFSEntryIFileWrapper() override;
+
+ //! \brief Open a file.
+ //! \param[in] url URL to open.
+ //! \returns True if file was opened, false otherwise.
+ bool Open(const CURL& url) override;
+
+ //! \brief Open a file for writing.
+ //! \param[in] url URL to open.
+ //! \param[in] bOverWrite If true, overwrite an existing file.
+ //! \returns True if file was opened, false otherwise.
+ bool OpenForWrite(const CURL& url, bool bOverWrite) override;
+
+ //! \brief Check for file existence.
+ //! \param[in] url URL of file.
+ bool Exists(const CURL& url) override;
+
+ //! \brief Stat a file.
+ //! \param[in] url URL of file.
+ //! \param[out] buffer The stat info.
+ //! \details Returns 0 on success, non-zero otherwise (see fstat() return values).
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+
+ //! \brief Read data from file:
+ //! \param lpBuf Buffer to read data into.
+ //! \param[in] uiBufSize Number of bytes to read.
+ //! \returns Number of bytes read.
+ ssize_t Read(void* lpBuf, size_t uiBufSize) override;
+
+ //! \brief Write data to file.
+ //! \param[in] lpBuf Data to write.
+ //! \param[in] uiBufSize Number of bytes to write.
+ //! \returns Number of bytes written.
+ ssize_t Write(const void* lpBuf, size_t uiBufSize) override;
+
+ //! \brief Seek in file.
+ //! \param[in] iFilePosition Position to seek to.
+ //! \param[in] whence Origin for position.
+ //! \returns New file position.
+ int64_t Seek(int64_t iFilePosition, int whence = SEEK_SET) override;
+
+ //! \brief Truncate a file.
+ //! \param[in] size Size of new file.
+ int Truncate(int64_t size) override;
+
+ //! \brief Close file.
+ void Close() override;
+
+ //! \brief Obtain current file position.
+ int64_t GetPosition() override;
+
+ //! \brief Obtain file size.
+ int64_t GetLength() override;
+
+ //! \brief Obtain chunksize of file.
+ int GetChunkSize() override;
+
+ //! \brief Perform I/O controls for file.
+ int IoControl(XFILE::EIoControl request, void* param) override;
+
+ //! \brief Delete a file.
+ //! \param[in] url URL of file to delete.
+ bool Delete(const CURL& url) override;
+
+ //! \brief Rename a file.
+ //! \param[in] url URL of file to rename.
+ //! \param[in] url2 New URL of file.
+ bool Rename(const CURL& url, const CURL& url2) override;
+ protected:
+ void* m_context; //!< Opaque add-on specific context for opened file.
+ VFSEntryPtr m_addon; //!< Pointer to wrapped CVFSEntry.
+ };
+
+ //! \brief Wrapper equpping a CVFSEntry with an IDirectory interface.
+ //! \details Needed as CVFSEntry implements several VFS interfaces
+ //! with overlapping methods.
+ class CVFSEntryIDirectoryWrapper : public XFILE::IDirectory
+ {
+ public:
+ //! \brief The constructor initializes the reference to the wrapped CVFSEntry.
+ //! \param ptr The CVFSEntry to wrap.
+ explicit CVFSEntryIDirectoryWrapper(VFSEntryPtr ptr);
+
+ //! \brief Empty destructor.
+ ~CVFSEntryIDirectoryWrapper() override = default;
+
+ //! \brief Return directory listing.
+ //! \param[in] url URL to file to list.
+ //! \param items List of items in file.
+ //! \return True if listing succeeded, false otherwise.
+ bool GetDirectory(const CURL& url, CFileItemList& items) override;
+
+ //! \brief Check if directory exists.
+ //! \param[in] url URL to check.
+ bool Exists(const CURL& url) override;
+
+ //! \brief Delete directory.
+ //! \param[in] url URL to delete.
+ bool Remove(const CURL& url) override;
+
+ //! \brief Create directory.
+ //! \param[in] url URL to delete.
+ bool Create(const CURL& url) override;
+
+ //! \brief Static helper for doing a keyboard callback.
+ static bool DoGetKeyboardInput(void* context, const char* heading,
+ char** input, bool hidden_input);
+
+ //! \brief Get keyboard input.
+ bool GetKeyboardInput2(const char* heading, char** input, bool hidden_input);
+
+ //! \brief Static helper for displaying an error dialog.
+ static void DoSetErrorDialog(void* ctx, const char* heading,
+ const char* line1, const char* line2,
+ const char* line3);
+
+ //! \brief Show an error dialog.
+ void SetErrorDialog2(const char* heading, const char* line1,
+ const char* line2, const char* line3);
+
+ //! \brief Static helper for requiring authentication.
+ static void DoRequireAuthentication(void* ctx, const char* url);
+
+ //! \brief Require authentication.
+ void RequireAuthentication2(const CURL& url);
+ protected:
+ VFSEntryPtr m_addon; //!< Pointer to wrapper CVFSEntry.
+ };
+
+ //! \brief Wrapper equpping a CVFSEntry with an IFileDirectory interface.
+ //! \details Needed as CVFSEntry implements several VFS interfaces
+ //! with overlapping methods.
+ class CVFSEntryIFileDirectoryWrapper : public XFILE::IFileDirectory,
+ public CVFSEntryIDirectoryWrapper
+ {
+ public:
+ //! \brief The constructor initializes the reference to the wrapped CVFSEntry.
+ //! \param ptr The CVFSEntry to wrap.
+ explicit CVFSEntryIFileDirectoryWrapper(VFSEntryPtr ptr)
+ : CVFSEntryIDirectoryWrapper(std::move(ptr))
+ {
+ }
+
+ //! \brief Empty destructor.
+ ~CVFSEntryIFileDirectoryWrapper() override = default;
+
+ //! \brief Check if the given file should be treated as a directory.
+ //! \param[in] url URL for file to probe.
+ bool ContainsFiles(const CURL& url) override
+ {
+ return m_addon->ContainsFiles(url, m_items);
+ }
+
+ //! \brief Return directory listing.
+ //! \param[in] url URL to file to list.
+ //! \param items List of items in file.
+ //! \return True if listing succeeded, false otherwise.
+ bool GetDirectory(const CURL& url, CFileItemList& items) override
+ {
+ return CVFSEntryIDirectoryWrapper::GetDirectory(url, items);
+ }
+
+ //! \brief Check if directory exists.
+ //! \param[in] url URL to check.
+ bool Exists(const CURL& url) override
+ {
+ return CVFSEntryIDirectoryWrapper::Exists(url);
+ }
+
+ //! \brief Delete directory.
+ //! \param[in] url URL to delete.
+ bool Remove(const CURL& url) override
+ {
+ return CVFSEntryIDirectoryWrapper::Remove(url);
+ }
+
+ //! \brief Create directory.
+ //! \param[in] url URL to delete.
+ bool Create(const CURL& url) override
+ {
+ return CVFSEntryIDirectoryWrapper::Create(url);
+ }
+
+ CFileItemList m_items; //!< Internal list of items, used for cache purposes.
+ };
+
+} /*namespace ADDON*/
diff --git a/xbmc/addons/Visualization.cpp b/xbmc/addons/Visualization.cpp
new file mode 100644
index 0000000..e519e30
--- /dev/null
+++ b/xbmc/addons/Visualization.cpp
@@ -0,0 +1,234 @@
+/*
+ * 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 "Visualization.h"
+
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+using namespace ADDON;
+using namespace KODI::ADDONS;
+
+namespace
+{
+
+void get_properties(const KODI_HANDLE hdl, struct KODI_ADDON_VISUALIZATION_PROPS* props)
+{
+ if (hdl)
+ static_cast<CVisualization*>(hdl)->GetProperties(props);
+}
+
+void transfer_preset(const KODI_HANDLE hdl, const char* preset)
+{
+ if (hdl && preset)
+ static_cast<CVisualization*>(hdl)->TransferPreset(preset);
+}
+
+void clear_presets(const KODI_HANDLE hdl)
+{
+ if (hdl)
+ static_cast<CVisualization*>(hdl)->ClearPresets();
+}
+
+} // namespace
+
+CVisualization::CVisualization(const AddonInfoPtr& addonInfo, float x, float y, float w, float h)
+ : IAddonInstanceHandler(ADDON_INSTANCE_VISUALIZATION, addonInfo),
+ m_x(static_cast<int>(x)),
+ m_y(static_cast<int>(y)),
+ m_width(static_cast<int>(w)),
+ m_height(static_cast<int>(h))
+{
+ // Setup new Visualization instance
+ m_ifc.visualization = new AddonInstance_Visualization;
+ m_ifc.visualization->toAddon = new KodiToAddonFuncTable_Visualization();
+ m_ifc.visualization->toKodi = new AddonToKodiFuncTable_Visualization();
+ m_ifc.visualization->toKodi->get_properties = get_properties;
+ m_ifc.visualization->toKodi->transfer_preset = transfer_preset;
+ m_ifc.visualization->toKodi->clear_presets = clear_presets;
+
+ /* Open the class "kodi::addon::CInstanceVisualization" on add-on side */
+ if (CreateInstance() != ADDON_STATUS_OK)
+ {
+ CLog::Log(LOGFATAL, "Visualization: failed to create instance for '{}' and not usable!", ID());
+ return;
+ }
+
+ /* presets becomes send with "transfer_preset" during call of function below */
+ if (m_ifc.visualization->toAddon->get_presets)
+ m_ifc.visualization->toAddon->get_presets(m_ifc.hdl);
+}
+
+CVisualization::~CVisualization()
+{
+ /* Destroy the class "kodi::addon::CInstanceVisualization" on add-on side */
+ DestroyInstance();
+
+ delete m_ifc.visualization->toAddon;
+ delete m_ifc.visualization->toKodi;
+ delete m_ifc.visualization;
+}
+
+bool CVisualization::Start(int channels,
+ int samplesPerSec,
+ int bitsPerSample,
+ const std::string& songName)
+{
+ if (m_ifc.visualization->toAddon->start)
+ return m_ifc.visualization->toAddon->start(m_ifc.hdl, channels, samplesPerSec, bitsPerSample,
+ songName.c_str());
+ return false;
+}
+
+void CVisualization::Stop()
+{
+ if (m_ifc.visualization->toAddon->stop)
+ m_ifc.visualization->toAddon->stop(m_ifc.hdl);
+}
+
+void CVisualization::AudioData(const float* audioData, int audioDataLength)
+{
+ if (m_ifc.visualization->toAddon->audio_data)
+ m_ifc.visualization->toAddon->audio_data(m_ifc.hdl, audioData, audioDataLength);
+}
+
+bool CVisualization::IsDirty()
+{
+ if (m_ifc.visualization->toAddon->is_dirty)
+ return m_ifc.visualization->toAddon->is_dirty(m_ifc.hdl);
+ return false;
+}
+
+void CVisualization::Render()
+{
+ if (m_ifc.visualization->toAddon->render)
+ m_ifc.visualization->toAddon->render(m_ifc.hdl);
+}
+
+int CVisualization::GetSyncDelay()
+{
+ if (m_ifc.visualization->toAddon->get_sync_delay)
+ m_ifc.visualization->toAddon->get_sync_delay(m_ifc.hdl);
+ return 0;
+}
+
+bool CVisualization::NextPreset()
+{
+ if (m_ifc.visualization->toAddon->next_preset)
+ return m_ifc.visualization->toAddon->next_preset(m_ifc.hdl);
+ return false;
+}
+
+bool CVisualization::PrevPreset()
+{
+ if (m_ifc.visualization->toAddon->prev_preset)
+ return m_ifc.visualization->toAddon->prev_preset(m_ifc.hdl);
+ return false;
+}
+
+bool CVisualization::LoadPreset(int select)
+{
+ if (m_ifc.visualization->toAddon->load_preset)
+ return m_ifc.visualization->toAddon->load_preset(m_ifc.hdl, select);
+ return false;
+}
+
+bool CVisualization::RandomPreset()
+{
+ if (m_ifc.visualization->toAddon->random_preset)
+ return m_ifc.visualization->toAddon->random_preset(m_ifc.hdl);
+ return false;
+}
+
+bool CVisualization::LockPreset()
+{
+ if (m_ifc.visualization->toAddon->lock_preset)
+ return m_ifc.visualization->toAddon->lock_preset(m_ifc.hdl);
+ return false;
+}
+
+bool CVisualization::RatePreset(bool plus_minus)
+{
+ if (m_ifc.visualization->toAddon->rate_preset)
+ return m_ifc.visualization->toAddon->rate_preset(m_ifc.hdl, plus_minus);
+ return false;
+}
+
+bool CVisualization::UpdateAlbumart(const char* albumart)
+{
+ if (m_ifc.visualization->toAddon->update_albumart)
+ return m_ifc.visualization->toAddon->update_albumart(m_ifc.hdl, albumart);
+ return false;
+}
+
+bool CVisualization::UpdateTrack(const KODI_ADDON_VISUALIZATION_TRACK* track)
+{
+ if (m_ifc.visualization->toAddon->update_track)
+ return m_ifc.visualization->toAddon->update_track(m_ifc.hdl, track);
+ return false;
+}
+
+bool CVisualization::HasPresets()
+{
+ return !m_presets.empty();
+}
+
+bool CVisualization::GetPresetList(std::vector<std::string>& vecpresets)
+{
+ vecpresets = m_presets;
+ return !m_presets.empty();
+}
+
+int CVisualization::GetActivePreset()
+{
+ if (m_ifc.visualization->toAddon->get_active_preset)
+ return m_ifc.visualization->toAddon->get_active_preset(m_ifc.hdl);
+ return -1;
+}
+
+std::string CVisualization::GetActivePresetName()
+{
+ if (!m_presets.empty())
+ return m_presets[GetActivePreset()];
+ return "";
+}
+
+bool CVisualization::IsLocked()
+{
+ if (m_ifc.visualization->toAddon->is_locked)
+ return m_ifc.visualization->toAddon->is_locked(m_ifc.hdl);
+ return false;
+}
+
+void CVisualization::TransferPreset(const std::string& preset)
+{
+ m_presets.emplace_back(preset);
+}
+
+void CVisualization::ClearPresets()
+{
+ m_presets.clear();
+}
+
+void CVisualization::GetProperties(struct KODI_ADDON_VISUALIZATION_PROPS* props)
+{
+ if (!props)
+ return;
+
+ const auto winSystem = CServiceBroker::GetWinSystem();
+ if (!winSystem)
+ return;
+
+ props->x = m_x;
+ props->y = m_y;
+ props->width = m_width;
+ props->height = m_height;
+ props->device = winSystem->GetHWContext();
+ props->pixelRatio = winSystem->GetGfxContext().GetResInfo().fPixelRatio;
+}
diff --git a/xbmc/addons/Visualization.h b/xbmc/addons/Visualization.h
new file mode 100644
index 0000000..a6eefa6
--- /dev/null
+++ b/xbmc/addons/Visualization.h
@@ -0,0 +1,59 @@
+/*
+ * 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 "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Visualization.h"
+
+namespace KODI
+{
+namespace ADDONS
+{
+
+class CVisualization : public ADDON::IAddonInstanceHandler
+{
+public:
+ CVisualization(const ADDON::AddonInfoPtr& addonInfo, float x, float y, float w, float h);
+ ~CVisualization() override;
+
+ bool Start(int channels, int samplesPerSec, int bitsPerSample, const std::string& songName);
+ void Stop();
+ void AudioData(const float* audioData, int audioDataLength);
+ bool IsDirty();
+ void Render();
+ int GetSyncDelay();
+ bool NextPreset();
+ bool PrevPreset();
+ bool LoadPreset(int select);
+ bool RandomPreset();
+ bool LockPreset();
+ bool RatePreset(bool plus_minus);
+ bool UpdateAlbumart(const char* albumart);
+ bool UpdateTrack(const KODI_ADDON_VISUALIZATION_TRACK* track);
+ bool HasPresets();
+ bool GetPresetList(std::vector<std::string>& vecpresets);
+ int GetActivePreset();
+ std::string GetActivePresetName();
+ bool IsLocked();
+
+ // Addon callback functions
+ void GetProperties(struct KODI_ADDON_VISUALIZATION_PROPS* props);
+ void TransferPreset(const std::string& preset);
+ void ClearPresets();
+
+private:
+ const int m_x;
+ const int m_y;
+ const int m_width;
+ const int m_height;
+ std::vector<std::string> m_presets; /*!< cached preset list */
+};
+
+} // namespace ADDONS
+} // namespace KODI
diff --git a/xbmc/addons/Webinterface.cpp b/xbmc/addons/Webinterface.cpp
new file mode 100644
index 0000000..75af3b2
--- /dev/null
+++ b/xbmc/addons/Webinterface.cpp
@@ -0,0 +1,52 @@
+/*
+ * 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 "Webinterface.h"
+
+#include "addons/addoninfo/AddonType.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace ADDON;
+
+CWebinterface::CWebinterface(const AddonInfoPtr& addonInfo)
+ : CAddon(addonInfo, AddonType::WEB_INTERFACE),
+ m_type(WebinterfaceTypeStatic),
+ m_entryPoint(WEBINTERFACE_DEFAULT_ENTRY_POINT)
+{
+ // determine the type of the webinterface
+ std::string webinterfaceType = Type(AddonType::WEB_INTERFACE)->GetValue("@type").asString();
+ if (StringUtils::EqualsNoCase(webinterfaceType, "wsgi"))
+ m_type = WebinterfaceTypeWsgi;
+ else if (!webinterfaceType.empty() && !StringUtils::EqualsNoCase(webinterfaceType, "static") && !StringUtils::EqualsNoCase(webinterfaceType, "html"))
+ CLog::Log(LOGWARNING,
+ "CWebinterface::{}: Addon \"{}\" has specified an unsupported type \"{}\"", __func__,
+ ID(), webinterfaceType);
+
+ // determine the entry point of the webinterface
+ std::string entry = Type(AddonType::WEB_INTERFACE)->GetValue("@entry").asString();
+ if (!entry.empty())
+ m_entryPoint = entry;
+}
+
+std::string CWebinterface::GetEntryPoint(const std::string &path) const
+{
+ if (m_type == WebinterfaceTypeWsgi)
+ return LibPath();
+
+ return URIUtils::AddFileToFolder(path, m_entryPoint);
+}
+
+std::string CWebinterface::GetBaseLocation() const
+{
+ if (m_type == WebinterfaceTypeWsgi)
+ return "/addons/" + ID();
+
+ return "";
+}
diff --git a/xbmc/addons/Webinterface.h b/xbmc/addons/Webinterface.h
new file mode 100644
index 0000000..7a0ea7c
--- /dev/null
+++ b/xbmc/addons/Webinterface.h
@@ -0,0 +1,38 @@
+/*
+ * 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 "addons/Addon.h"
+
+#define WEBINTERFACE_DEFAULT_ENTRY_POINT "index.html"
+
+namespace ADDON
+{
+ typedef enum WebinterfaceType
+ {
+ WebinterfaceTypeStatic = 0,
+ WebinterfaceTypeWsgi
+ } WebinterfaceType;
+
+ class CWebinterface : public CAddon
+ {
+ public:
+ explicit CWebinterface(const AddonInfoPtr& addonInfo);
+
+ WebinterfaceType GetType() const { return m_type; }
+ const std::string& EntryPoint() const { return m_entryPoint; }
+
+ std::string GetEntryPoint(const std::string &path) const;
+ std::string GetBaseLocation() const;
+
+ private:
+ WebinterfaceType m_type;
+ std::string m_entryPoint;
+ };
+}
diff --git a/xbmc/addons/addoninfo/AddonExtensions.cpp b/xbmc/addons/addoninfo/AddonExtensions.cpp
new file mode 100644
index 0000000..b0bc3c5
--- /dev/null
+++ b/xbmc/addons/addoninfo/AddonExtensions.cpp
@@ -0,0 +1,68 @@
+/*
+ * 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 "AddonExtensions.h"
+
+#include "utils/StringUtils.h"
+
+using namespace ADDON;
+
+bool SExtValue::asBoolean() const
+{
+ return StringUtils::EqualsNoCase(str, "true");
+}
+
+const SExtValue CAddonExtensions::GetValue(const std::string& id) const
+{
+ for (const auto& values : m_values)
+ {
+ for (const auto& value : values.second)
+ {
+ if (value.first == id)
+ return value.second;
+ }
+ }
+ return SExtValue("");
+}
+
+const EXT_VALUES& CAddonExtensions::GetValues() const
+{
+ return m_values;
+}
+
+const CAddonExtensions* CAddonExtensions::GetElement(const std::string& id) const
+{
+ for (const auto& child : m_children)
+ {
+ if (child.first == id)
+ return &child.second;
+ }
+
+ return nullptr;
+}
+
+const EXT_ELEMENTS CAddonExtensions::GetElements(const std::string& id) const
+{
+ if (id.empty())
+ return m_children;
+
+ EXT_ELEMENTS children;
+ for (const auto& child : m_children)
+ {
+ if (child.first == id)
+ children.push_back(std::make_pair(child.first, child.second));
+ }
+ return children;
+}
+
+void CAddonExtensions::Insert(const std::string& id, const std::string& value)
+{
+ EXT_VALUE extension;
+ extension.push_back(std::make_pair(id, SExtValue(value)));
+ m_values.push_back(std::make_pair(id, extension));
+}
diff --git a/xbmc/addons/addoninfo/AddonExtensions.h b/xbmc/addons/addoninfo/AddonExtensions.h
new file mode 100644
index 0000000..2ea2011
--- /dev/null
+++ b/xbmc/addons/addoninfo/AddonExtensions.h
@@ -0,0 +1,76 @@
+/*
+ * 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 <stdlib.h>
+#include <string>
+#include <vector>
+
+namespace ADDON
+{
+
+class CAddonInfoBuilder;
+class CAddonDatabaseSerializer;
+
+struct SExtValue
+{
+ explicit SExtValue(const std::string& strValue) : str(strValue) { }
+ const std::string& asString() const { return str; }
+ bool asBoolean() const;
+ int asInteger() const { return std::atoi(str.c_str()); }
+ float asFloat() const { return static_cast<float>(std::atof(str.c_str())); }
+ bool empty() const { return str.empty(); }
+ const std::string str;
+};
+
+class CExtValues;
+class CAddonExtensions;
+typedef std::vector<std::pair<std::string, CAddonExtensions>> EXT_ELEMENTS;
+typedef std::vector<std::pair<std::string, SExtValue>> EXT_VALUE;
+typedef std::vector<std::pair<std::string, CExtValues>> EXT_VALUES;
+
+class CExtValues : public EXT_VALUE
+{
+public:
+ CExtValues(const EXT_VALUE& values) : EXT_VALUE(values) { }
+
+ const SExtValue GetValue(const std::string& id) const
+ {
+ for (const auto& value : *this)
+ {
+ if (value.first == id)
+ return value.second;
+ }
+ return SExtValue("");
+ }
+};
+
+class CAddonExtensions
+{
+public:
+ CAddonExtensions() = default;
+ ~CAddonExtensions() = default;
+
+ const SExtValue GetValue(const std::string& id) const;
+ const EXT_VALUES& GetValues() const;
+ const CAddonExtensions* GetElement(const std::string& id) const;
+ const EXT_ELEMENTS GetElements(const std::string& id = "") const;
+
+ void Insert(const std::string& id, const std::string& value);
+
+private:
+ friend class CAddonInfoBuilder;
+ friend class CAddonDatabaseSerializer;
+
+ std::string m_point;
+ EXT_VALUES m_values;
+ EXT_ELEMENTS m_children;
+};
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/addoninfo/AddonInfo.cpp b/xbmc/addons/addoninfo/AddonInfo.cpp
new file mode 100644
index 0000000..61bc087
--- /dev/null
+++ b/xbmc/addons/addoninfo/AddonInfo.cpp
@@ -0,0 +1,312 @@
+/*
+ * 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 "AddonInfo.h"
+
+#include "FileItem.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/Directory.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <algorithm>
+#include <array>
+#include <string_view>
+
+namespace ADDON
+{
+
+typedef struct
+{
+ std::string_view name;
+ std::string_view old_name;
+ AddonType type;
+ int pretty;
+ AddonInstanceSupport instance_support;
+ std::string_view icon;
+} TypeMapping;
+
+// clang-format off
+static constexpr const std::array<TypeMapping, 40> types =
+ {{
+ {"unknown", "", AddonType::UNKNOWN, 0, AddonInstanceSupport::SUPPORT_NONE, "" },
+ {"xbmc.metadata.scraper.albums", "", AddonType::SCRAPER_ALBUMS, 24016, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonAlbumInfo.png" },
+ {"xbmc.metadata.scraper.artists", "", AddonType::SCRAPER_ARTISTS, 24017, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonArtistInfo.png" },
+ {"xbmc.metadata.scraper.movies", "", AddonType::SCRAPER_MOVIES, 24007, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonMovieInfo.png" },
+ {"xbmc.metadata.scraper.musicvideos", "", AddonType::SCRAPER_MUSICVIDEOS, 24015, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonMusicVideoInfo.png" },
+ {"xbmc.metadata.scraper.tvshows", "", AddonType::SCRAPER_TVSHOWS, 24014, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonTvInfo.png" },
+ {"xbmc.metadata.scraper.library", "", AddonType::SCRAPER_LIBRARY, 24083, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonInfoLibrary.png" },
+ {"xbmc.ui.screensaver", "", AddonType::SCREENSAVER, 24008, AddonInstanceSupport::SUPPORT_OPTIONAL, "DefaultAddonScreensaver.png" },
+ {"xbmc.player.musicviz", "", AddonType::VISUALIZATION, 24010, AddonInstanceSupport::SUPPORT_OPTIONAL, "DefaultAddonVisualization.png" },
+ {"xbmc.python.pluginsource", "", AddonType::PLUGIN, 24005, AddonInstanceSupport::SUPPORT_NONE, "" },
+ {"xbmc.python.script", "", AddonType::SCRIPT, 24009, AddonInstanceSupport::SUPPORT_NONE, "" },
+ {"xbmc.python.weather", "", AddonType::SCRIPT_WEATHER, 24027, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonWeather.png" },
+ {"xbmc.python.lyrics", "", AddonType::SCRIPT_LYRICS, 24013, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonLyrics.png" },
+ {"xbmc.python.library", "", AddonType::SCRIPT_LIBRARY, 24081, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonHelper.png" },
+ {"xbmc.python.module", "", AddonType::SCRIPT_MODULE, 24082, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonLibrary.png" },
+ {"xbmc.subtitle.module", "", AddonType::SUBTITLE_MODULE, 24012, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonSubtitles.png" },
+ {"kodi.context.item", "", AddonType::CONTEXTMENU_ITEM, 24025, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonContextItem.png" },
+ {"kodi.game.controller", "", AddonType::GAME_CONTROLLER, 35050, AddonInstanceSupport::SUPPORT_OPTIONAL, "DefaultAddonGame.png" },
+ {"xbmc.gui.skin", "", AddonType::SKIN, 166, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonSkin.png" },
+ {"xbmc.webinterface", "", AddonType::WEB_INTERFACE, 199, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonWebSkin.png" },
+ {"xbmc.addon.repository", "", AddonType::REPOSITORY, 24011, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonRepository.png" },
+ {"kodi.pvrclient", "xbmc.pvrclient", AddonType::PVRDLL, 24019, AddonInstanceSupport::SUPPORT_SETTINGS, "DefaultAddonPVRClient.png" },
+ {"kodi.gameclient", "", AddonType::GAMEDLL, 35049, AddonInstanceSupport::SUPPORT_OPTIONAL, "DefaultAddonGame.png" },
+ {"kodi.peripheral", "", AddonType::PERIPHERALDLL, 35010, AddonInstanceSupport::SUPPORT_MANDATORY, "DefaultAddonPeripheral.png" },
+ {"xbmc.addon.video", "", AddonType::VIDEO, 1037, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonVideo.png" },
+ {"xbmc.addon.audio", "", AddonType::AUDIO, 1038, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonMusic.png" },
+ {"xbmc.addon.image", "", AddonType::IMAGE, 1039, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonPicture.png" },
+ {"xbmc.addon.executable", "", AddonType::EXECUTABLE, 1043, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonProgram.png" },
+ {"kodi.addon.game", "", AddonType::GAME, 35049, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonGame.png" },
+ {"kodi.audioencoder", "", AddonType::AUDIOENCODER, 200, AddonInstanceSupport::SUPPORT_MANDATORY, "DefaultAddonAudioEncoder.png" },
+ {"kodi.audiodecoder", "", AddonType::AUDIODECODER, 201, AddonInstanceSupport::SUPPORT_MANDATORY, "DefaultAddonAudioDecoder.png" },
+ {"xbmc.service", "", AddonType::SERVICE, 24018, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonService.png" },
+ {"kodi.resource.images", "", AddonType::RESOURCE_IMAGES, 24035, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonImages.png" },
+ {"kodi.resource.language", "", AddonType::RESOURCE_LANGUAGE, 24026, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonLanguage.png" },
+ {"kodi.resource.uisounds", "", AddonType::RESOURCE_UISOUNDS, 24006, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonUISounds.png" },
+ {"kodi.resource.games", "", AddonType::RESOURCE_GAMES, 35209, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonGame.png" },
+ {"kodi.resource.font", "", AddonType::RESOURCE_FONT, 13303, AddonInstanceSupport::SUPPORT_NONE, "DefaultAddonFont.png" },
+ {"kodi.inputstream", "", AddonType::INPUTSTREAM, 24048, AddonInstanceSupport::SUPPORT_MANDATORY, "DefaultAddonInputstream.png" },
+ {"kodi.vfs", "", AddonType::VFS, 39013, AddonInstanceSupport::SUPPORT_MANDATORY, "DefaultAddonVfs.png" },
+ {"kodi.imagedecoder", "", AddonType::IMAGEDECODER, 39015, AddonInstanceSupport::SUPPORT_MANDATORY, "DefaultAddonImageDecoder.png" },
+ }};
+// clang-format on
+
+const std::string& CAddonInfo::OriginName() const
+{
+ if (!m_originName)
+ {
+ ADDON::AddonPtr origin;
+ if (CServiceBroker::GetAddonMgr().GetAddon(m_origin, origin, ADDON::OnlyEnabled::CHOICE_NO))
+ m_originName = std::make_unique<std::string>(origin->Name());
+ else
+ m_originName = std::make_unique<std::string>(); // remember we tried to fetch the name
+ }
+ return *m_originName;
+}
+
+/**
+ * static public helper functions
+ *
+ */
+
+std::string CAddonInfo::TranslateType(AddonType type, bool pretty /*= false*/)
+{
+ for (const TypeMapping& map : types)
+ {
+ if (type == map.type)
+ {
+ if (pretty && map.pretty)
+ return g_localizeStrings.Get(map.pretty);
+ else
+ return std::string(map.name.data(), map.name.size());
+ }
+ }
+ return "";
+}
+
+AddonType CAddonInfo::TranslateType(const std::string& string)
+{
+ for (const TypeMapping& map : types)
+ {
+ if (string == map.name || (!map.old_name.empty() && string == map.old_name))
+ return map.type;
+ }
+
+ return AddonType::UNKNOWN;
+}
+
+std::string CAddonInfo::TranslateIconType(AddonType type)
+{
+ for (const TypeMapping& map : types)
+ {
+ if (type == map.type)
+ return std::string(map.icon.data(), map.icon.size());
+ }
+ return "";
+}
+
+AddonType CAddonInfo::TranslateSubContent(const std::string& content)
+{
+ if (content == "audio")
+ return AddonType::AUDIO;
+ else if (content == "image")
+ return AddonType::IMAGE;
+ else if (content == "executable")
+ return AddonType::EXECUTABLE;
+ else if (content == "video")
+ return AddonType::VIDEO;
+ else if (content == "game")
+ return AddonType::GAME;
+ else
+ return AddonType::UNKNOWN;
+}
+
+AddonInstanceSupport CAddonInfo::InstanceSupportType(AddonType type)
+{
+ const auto it = std::find_if(types.begin(), types.end(),
+ [type](const TypeMapping& entry) { return entry.type == type; });
+ if (it != types.end())
+ return it->instance_support;
+
+ return AddonInstanceSupport::SUPPORT_NONE;
+}
+
+CAddonInfo::CAddonInfo(std::string id, AddonType type) : m_id(std::move(id)), m_mainType(type)
+{
+
+}
+
+const CAddonType* CAddonInfo::Type(AddonType type) const
+{
+ static CAddonType dummy;
+
+ if (!m_types.empty())
+ {
+ if (type == AddonType::UNKNOWN)
+ return &m_types[0];
+
+ for (auto& addonType : m_types)
+ {
+ if (addonType.Type() == type)
+ return &addonType;
+ }
+ }
+
+ return &dummy;
+}
+
+bool CAddonInfo::HasType(AddonType type, bool mainOnly /*= false*/) const
+{
+ return (m_mainType == type ||
+ ProvidesSubContent(type, mainOnly ? m_mainType : AddonType::UNKNOWN));
+}
+
+bool CAddonInfo::ProvidesSubContent(AddonType content, AddonType mainType) const
+{
+ if (content == AddonType::UNKNOWN)
+ return false;
+
+ for (const auto& addonType : m_types)
+ {
+ if ((mainType == AddonType::UNKNOWN || addonType.Type() == mainType) &&
+ addonType.ProvidesSubContent(content))
+ return true;
+ }
+
+ return false;
+}
+
+bool CAddonInfo::ProvidesSeveralSubContents() const
+{
+ int contents = 0;
+ for (const auto& addonType : m_types)
+ contents += addonType.ProvidedSubContents();
+ return contents > 0 ? true : false;
+}
+
+bool CAddonInfo::MeetsVersion(const CAddonVersion& versionMin, const CAddonVersion& version) const
+{
+ return !(versionMin > m_version || version < m_minversion);
+}
+
+const CAddonVersion& CAddonInfo::DependencyMinVersion(const std::string& dependencyID) const
+{
+ auto it = std::find_if(m_dependencies.begin(), m_dependencies.end(),
+ [&](const DependencyInfo& other) { return other.id == dependencyID; });
+
+ if (it != m_dependencies.end())
+ return it->versionMin;
+
+ static CAddonVersion emptyVersion;
+ return emptyVersion;
+}
+
+const CAddonVersion& CAddonInfo::DependencyVersion(const std::string& dependencyID) const
+{
+ auto it = std::find_if(m_dependencies.begin(), m_dependencies.end(), [&](const DependencyInfo& other) { return other.id == dependencyID; });
+
+ if (it != m_dependencies.end())
+ return it->version;
+
+ static CAddonVersion emptyVersion;
+ return emptyVersion;
+}
+
+const std::string& CAddonInfo::GetTranslatedText(const std::unordered_map<std::string, std::string>& locales) const
+{
+ if (locales.size() == 1)
+ return locales.begin()->second;
+ else if (locales.empty())
+ return StringUtils::Empty;
+
+ // find the language from the list that matches the current locale best
+ std::string matchingLanguage = g_langInfo.GetLocale().FindBestMatch(locales);
+ if (matchingLanguage.empty())
+ matchingLanguage = KODI_ADDON_DEFAULT_LANGUAGE_CODE;
+
+ auto const& translatedValue = locales.find(matchingLanguage);
+ if (translatedValue != locales.end())
+ return translatedValue->second;
+ return StringUtils::Empty;
+}
+
+bool CAddonInfo::SupportsMultipleInstances() const
+{
+ switch (m_addonInstanceSupportType)
+ {
+ case AddonInstanceSupport::SUPPORT_MANDATORY:
+ case AddonInstanceSupport::SUPPORT_OPTIONAL:
+ return true;
+ case AddonInstanceSupport::SUPPORT_SETTINGS:
+ return m_supportsInstanceSettings;
+ case AddonInstanceSupport::SUPPORT_NONE:
+ default:
+ return false;
+ }
+}
+
+std::vector<AddonInstanceId> CAddonInfo::GetKnownInstanceIds() const
+{
+ static const std::vector<AddonInstanceId> singletonInstance = {ADDON_SINGLETON_INSTANCE_ID};
+
+ if (!m_supportsInstanceSettings)
+ return singletonInstance;
+
+ const std::string searchPath = StringUtils::Format("special://profile/addon_data/{}/", m_id);
+ CFileItemList items;
+ XFILE::CDirectory::GetDirectory(searchPath, items, ".xml", XFILE::DIR_FLAG_NO_FILE_DIRS);
+
+ std::vector<AddonInstanceId> ret;
+
+ for (const auto& item : items)
+ {
+ const std::string startName = "instance-settings-";
+ std::string filename = URIUtils::GetFileName(item->GetPath());
+ if (StringUtils::StartsWithNoCase(URIUtils::GetFileName(item->GetPath()), startName))
+ {
+ URIUtils::RemoveExtension(filename);
+ const std::string uid = filename.substr(startName.length());
+ if (!uid.empty() && StringUtils::IsInteger(uid))
+ ret.emplace_back(std::atoi(uid.c_str()));
+ }
+ }
+
+ // If no instances are used, create first as default.
+ if (ret.empty())
+ ret.emplace_back(ADDON_FIRST_INSTANCE_ID);
+
+ return ret;
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/addoninfo/AddonInfo.h b/xbmc/addons/addoninfo/AddonInfo.h
new file mode 100644
index 0000000..5998e2d
--- /dev/null
+++ b/xbmc/addons/addoninfo/AddonInfo.h
@@ -0,0 +1,309 @@
+/*
+ * 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 "XBDateTime.h"
+#include "addons/AddonVersion.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace ADDON
+{
+
+enum class AddonType;
+
+class CAddonBuilder;
+class CAddonInfo;
+class CAddonType;
+typedef std::shared_ptr<CAddonInfo> AddonInfoPtr;
+typedef std::vector<AddonInfoPtr> AddonInfos;
+
+using AddonInstanceId = uint32_t;
+
+/*!
+ * Defines the default language code used as fallback in case the requested language is not
+ * available. Used, for instance, to handle content from addon.xml.
+ */
+constexpr const char* KODI_ADDON_DEFAULT_LANGUAGE_CODE = "en_GB";
+
+enum class AddonDisabledReason
+{
+ /// @brief Special reason for returning all disabled addons.
+ ///
+ /// Only used as an actual value when an addon is enabled.
+ NONE = 0,
+ USER = 1,
+ INCOMPATIBLE = 2,
+ PERMANENT_FAILURE = 3
+};
+
+enum class AddonOriginType
+{
+ /// @brief The type of the origin of an addon.
+ ///
+ /// Represents where an addon was installed from.
+ SYSTEM = 0, /// The addon is a system addon
+ REPOSITORY = 1, /// The addon origin is a repository
+ MANUAL = 2 /// The addon origin is a zip file, package or development build
+};
+
+//! @brief Reasons why an addon is not updateable
+enum class AddonUpdateRule
+{
+ ANY = 0, //!< used internally, not to be explicitly set
+ USER_DISABLED_AUTO_UPDATE = 1, //!< automatic updates disabled via AddonInfo dialog
+ PIN_OLD_VERSION = 2, //!< user downgraded to an older version
+ PIN_ZIP_INSTALL = 3, //!< user installed manually from zip
+};
+
+/*!
+ * @brief Independent add-on instance support.
+ *
+ * Used to be able to find out its instance path for the respective add-on types.
+ */
+enum class AddonInstanceSupport
+{
+ //! If add-on type does not support instances.
+ SUPPORT_NONE = 0,
+
+ //! If add-on type needs support for several instances individually.
+ SUPPORT_MANDATORY = 1,
+
+ //! If add-on type can support several instances individually.
+ SUPPORT_OPTIONAL = 2,
+
+ //! If add-on type supports multiple instances using independent settings.
+ SUPPORT_SETTINGS = 3,
+};
+
+/*!
+ * @brief Add-on state defined within addon.xml to report about the current addon
+ * lifecycle state.
+ *
+ * E.g. the add-on is broken and can no longer be used.
+ *
+ * XML examples:
+ * ~~~~~~~~~~~~~{.xml}
+ * <lifecyclestate type="broken" lang="en_GB">SOME TEXT</lifecyclestate>
+ * ~~~~~~~~~~~~~
+ */
+enum class AddonLifecycleState
+{
+ NORMAL = 0, //!< Used if an add-on has no special lifecycle state which is the default state
+ DEPRECATED = 1, //!< the add-on should be marked as deprecated but is still usable
+ BROKEN = 2, //!< the add-on should marked as broken in the repository
+};
+
+struct DependencyInfo
+{
+ std::string id;
+ CAddonVersion versionMin, version;
+ bool optional;
+ DependencyInfo(std::string id,
+ const CAddonVersion& versionMin,
+ const CAddonVersion& version,
+ bool optional)
+ : id(std::move(id)),
+ versionMin(versionMin.empty() ? version : versionMin),
+ version(version),
+ optional(optional)
+ {
+ }
+
+ bool operator==(const DependencyInfo& rhs) const
+ {
+ return id == rhs.id && versionMin == rhs.versionMin && version == rhs.version &&
+ optional == rhs.optional;
+ }
+
+ bool operator!=(const DependencyInfo& rhs) const
+ {
+ return !(rhs == *this);
+ }
+};
+
+typedef std::map<std::string, std::string> InfoMap;
+typedef std::map<std::string, std::string> ArtMap;
+
+class CAddonInfoBuilder;
+
+class CAddonInfo
+{
+public:
+ CAddonInfo() = default;
+ CAddonInfo(std::string id, AddonType type);
+
+ void SetMainType(AddonType type) { m_mainType = type; }
+ void SetBinary(bool isBinary) { m_isBinary = isBinary; }
+ void SetLibName(const std::string& libname) { m_libname = libname; }
+ void SetPath(const std::string& path) { m_path = path; }
+ void AddExtraInfo(const std::string& idName, const std::string& value) { m_extrainfo[idName] = value; }
+ void SetLastUsed(const CDateTime& dateTime) { m_lastUsed = dateTime; }
+
+ const std::string& ID() const { return m_id; }
+
+ /**
+ * @brief To get the main type of this addon
+ *
+ * This is the first type defined in addon.xml.
+ *
+ * @return The used main type of addon
+ */
+ AddonType MainType() const { return m_mainType; }
+
+ /**
+ * @brief To check addon contains a type
+ *
+ * @param[in] type The to checked type identifier
+ * @param[in] mainOnly to check only in first defined main addon inside addon.xml
+ * @return true in case the wanted type is supported, false if not
+ */
+ bool HasType(AddonType type, bool mainOnly = false) const;
+
+ /**
+ * @brief To get all available types inside the addon
+ *
+ * To have all `<extension point="..." />` defined in addon.xml inside a list.
+ *
+ * @return List of all supported types
+ */
+ const std::vector<CAddonType>& Types() const { return m_types; }
+
+ /**
+ * @brief The get for given addon type information and extension data
+ *
+ * @param[in] type The wanted type data
+ * @return addon type class with @ref CAddonExtensions as information
+ *
+ * @note This function return never a "nullptr", in case the wanted type is
+ * not supported, becomes a dummy of @ref CAddonType given.
+ *
+ * ------------------------------------------------------------------------
+ *
+ * **Example:**
+ * ~~~~~~~~~~~~~{.cpp}
+ * // To get <extension ... name="blablabla" /> from addon.xml
+ * std::string name = Type(ADDON_...)->GetValue("@name").asString();
+ * ~~~~~~~~~~~~~
+ *
+ */
+ const CAddonType* Type(AddonType type) const;
+
+ bool ProvidesSubContent(AddonType content, AddonType mainType) const;
+ bool ProvidesSeveralSubContents() const;
+
+ const CAddonVersion& Version() const { return m_version; }
+ const CAddonVersion& MinVersion() const { return m_minversion; }
+ bool IsBinary() const { return m_isBinary; }
+ const CAddonVersion& DependencyMinVersion(const std::string& dependencyID) const;
+ const CAddonVersion& DependencyVersion(const std::string& dependencyID) const;
+ const std::string& Name() const { return m_name; }
+ const std::string& License() const { return m_license; }
+ const std::string& Summary() const { return GetTranslatedText(m_summary); }
+ const std::string& Description() const { return GetTranslatedText(m_description); }
+ const std::string& LibName() const { return m_libname; }
+ const std::string& Author() const { return m_author; }
+ const std::string& Source() const { return m_source; }
+ const std::string& Website() const { return m_website; }
+ const std::string& Forum() const { return m_forum; }
+ const std::string& EMail() const { return m_email; }
+ const std::string& Path() const { return m_path; }
+ const std::string& ProfilePath() const { return m_profilePath; }
+ const std::string& ChangeLog() const { return GetTranslatedText(m_changelog); }
+ const std::string& Icon() const { return m_icon; }
+ const ArtMap& Art() const { return m_art; }
+ const std::vector<std::string>& Screenshots() const { return m_screenshots; }
+ const std::string& Disclaimer() const { return GetTranslatedText(m_disclaimer); }
+ const std::vector<DependencyInfo>& GetDependencies() const { return m_dependencies; }
+ AddonLifecycleState LifecycleState() const { return m_lifecycleState; }
+ const std::string& LifecycleStateDescription() const
+ {
+ return GetTranslatedText(m_lifecycleStateDescription);
+ }
+ const std::string& Origin() const { return m_origin; }
+ const std::string& OriginName() const;
+
+ const InfoMap& ExtraInfo() const { return m_extrainfo; }
+
+ bool MeetsVersion(const CAddonVersion& versionMin, const CAddonVersion& version) const;
+ uint64_t PackageSize() const { return m_packageSize; }
+ CDateTime InstallDate() const { return m_installDate; }
+ CDateTime LastUpdated() const { return m_lastUpdated; }
+ CDateTime LastUsed() const { return m_lastUsed; }
+
+ bool SupportsMultipleInstances() const;
+ AddonInstanceSupport InstanceUseType() const { return m_addonInstanceSupportType; }
+
+ bool SupportsAddonSettings() const { return m_supportsAddonSettings; }
+ bool SupportsInstanceSettings() const { return m_supportsInstanceSettings; }
+ std::vector<AddonInstanceId> GetKnownInstanceIds() const;
+
+ /*!
+ * @brief Utilities to translate add-on parts to his requested part.
+ */
+ //@{
+ static std::string TranslateType(AddonType type, bool pretty = false);
+ static std::string TranslateIconType(AddonType type);
+ static AddonType TranslateType(const std::string& string);
+ static AddonType TranslateSubContent(const std::string& content);
+ static AddonInstanceSupport InstanceSupportType(AddonType type);
+ //@}
+
+private:
+ friend class CAddonInfoBuilder;
+ friend class CAddonInfoBuilderFromDB;
+
+ std::string m_id;
+ AddonType m_mainType{};
+ std::vector<CAddonType> m_types;
+
+ CAddonVersion m_version;
+ CAddonVersion m_minversion;
+ bool m_isBinary = false;
+ std::string m_name;
+ std::string m_license;
+ std::unordered_map<std::string, std::string> m_summary;
+ std::unordered_map<std::string, std::string> m_description;
+ std::string m_author;
+ std::string m_source;
+ std::string m_website;
+ std::string m_forum;
+ std::string m_email;
+ std::string m_path;
+ std::string m_profilePath;
+ std::unordered_map<std::string, std::string> m_changelog;
+ std::string m_icon;
+ ArtMap m_art;
+ std::vector<std::string> m_screenshots;
+ std::unordered_map<std::string, std::string> m_disclaimer;
+ std::vector<DependencyInfo> m_dependencies;
+ AddonLifecycleState m_lifecycleState = AddonLifecycleState::NORMAL;
+ std::unordered_map<std::string, std::string> m_lifecycleStateDescription;
+ CDateTime m_installDate;
+ CDateTime m_lastUpdated;
+ CDateTime m_lastUsed;
+ std::string m_origin;
+ mutable std::unique_ptr<std::string> m_originName; // @todo use std::optional once we use c++17
+ uint64_t m_packageSize = 0;
+ std::string m_libname;
+ InfoMap m_extrainfo;
+ std::vector<std::string> m_platforms;
+ AddonInstanceSupport m_addonInstanceSupportType{AddonInstanceSupport::SUPPORT_NONE};
+ bool m_supportsAddonSettings{false};
+ bool m_supportsInstanceSettings{false};
+
+ const std::string& GetTranslatedText(const std::unordered_map<std::string, std::string>& locales) const;
+};
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/addoninfo/AddonInfoBuilder.cpp b/xbmc/addons/addoninfo/AddonInfoBuilder.cpp
new file mode 100644
index 0000000..268efa6
--- /dev/null
+++ b/xbmc/addons/addoninfo/AddonInfoBuilder.cpp
@@ -0,0 +1,874 @@
+/*
+ * 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 "AddonInfoBuilder.h"
+
+#include "CompileInfo.h"
+#include "LangInfo.h"
+#include "addons/Repository.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/JSONVariantWriter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+#include <regex>
+
+namespace
+{
+// Note that all of these characters are url-safe
+const std::string VALID_ADDON_IDENTIFIER_CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_@!$";
+}
+
+namespace ADDON
+{
+
+CAddonInfoBuilderFromDB::CAddonInfoBuilderFromDB() : m_addonInfo(std::make_shared<CAddonInfo>())
+{
+}
+
+void CAddonInfoBuilderFromDB::SetId(std::string id)
+{
+ m_addonInfo->m_id = std::move(id);
+}
+
+void CAddonInfoBuilderFromDB::SetName(std::string name)
+{
+ m_addonInfo->m_name = std::move(name);
+}
+
+void CAddonInfoBuilderFromDB::SetLicense(std::string license)
+{
+ m_addonInfo->m_license = std::move(license);
+}
+
+void CAddonInfoBuilderFromDB::SetSummary(std::string summary)
+{
+ m_addonInfo->m_summary.insert(std::pair<std::string, std::string>("unk", std::move(summary)));
+}
+
+void CAddonInfoBuilderFromDB::SetDescription(std::string description)
+{
+ m_addonInfo->m_description.insert(
+ std::pair<std::string, std::string>("unk", std::move(description)));
+}
+
+void CAddonInfoBuilderFromDB::SetDisclaimer(std::string disclaimer)
+{
+ m_addonInfo->m_disclaimer.insert(
+ std::pair<std::string, std::string>("unk", std::move(disclaimer)));
+}
+
+void CAddonInfoBuilderFromDB::SetAuthor(std::string author)
+{
+ m_addonInfo->m_author = std::move(author);
+}
+
+void CAddonInfoBuilderFromDB::SetSource(std::string source)
+{
+ m_addonInfo->m_source = std::move(source);
+}
+
+void CAddonInfoBuilderFromDB::SetWebsite(std::string website)
+{
+ m_addonInfo->m_website = std::move(website);
+}
+
+void CAddonInfoBuilderFromDB::SetForum(std::string forum)
+{
+ m_addonInfo->m_forum = std::move(forum);
+}
+
+void CAddonInfoBuilderFromDB::SetEMail(std::string email)
+{
+ m_addonInfo->m_email = std::move(email);
+}
+
+void CAddonInfoBuilderFromDB::SetIcon(std::string icon)
+{
+ m_addonInfo->m_icon = std::move(icon);
+}
+
+void CAddonInfoBuilderFromDB::SetArt(const std::string& type, std::string value)
+{
+ m_addonInfo->m_art[type] = std::move(value);
+}
+
+void CAddonInfoBuilderFromDB::SetArt(std::map<std::string, std::string> art)
+{
+ m_addonInfo->m_art = std::move(art);
+}
+
+void CAddonInfoBuilderFromDB::SetScreenshots(std::vector<std::string> screenshots)
+{
+ m_addonInfo->m_screenshots = std::move(screenshots);
+}
+
+void CAddonInfoBuilderFromDB::SetChangelog(std::string changelog)
+{
+ m_addonInfo->m_changelog.insert(std::pair<std::string, std::string>("unk", std::move(changelog)));
+}
+
+void CAddonInfoBuilderFromDB::SetLifecycleState(AddonLifecycleState state, std::string description)
+{
+ m_addonInfo->m_lifecycleState = state;
+ m_addonInfo->m_lifecycleStateDescription.emplace("unk", std::move(description));
+}
+
+void CAddonInfoBuilderFromDB::SetPath(std::string path)
+{
+ m_addonInfo->m_path = std::move(path);
+}
+
+void CAddonInfoBuilderFromDB::SetLibName(std::string libname)
+{
+ m_addonInfo->m_libname = std::move(libname);
+}
+
+void CAddonInfoBuilderFromDB::SetVersion(CAddonVersion version)
+{
+ m_addonInfo->m_version = std::move(version);
+}
+
+void CAddonInfoBuilderFromDB::SetDependencies(std::vector<DependencyInfo> dependencies)
+{
+ m_addonInfo->m_dependencies = std::move(dependencies);
+}
+
+void CAddonInfoBuilderFromDB::SetExtrainfo(InfoMap extrainfo)
+{
+ m_addonInfo->m_extrainfo = std::move(extrainfo);
+}
+
+void CAddonInfoBuilderFromDB::SetInstallDate(const CDateTime& installDate)
+{
+ m_addonInfo->m_installDate = installDate;
+}
+
+void CAddonInfoBuilderFromDB::SetLastUpdated(const CDateTime& lastUpdated)
+{
+ m_addonInfo->m_lastUpdated = lastUpdated;
+}
+
+void CAddonInfoBuilderFromDB::SetLastUsed(const CDateTime& lastUsed)
+{
+ m_addonInfo->m_lastUsed = lastUsed;
+}
+
+void CAddonInfoBuilderFromDB::SetOrigin(std::string origin)
+{
+ m_addonInfo->m_origin = std::move(origin);
+}
+
+void CAddonInfoBuilderFromDB::SetPackageSize(uint64_t size)
+{
+ m_addonInfo->m_packageSize = size;
+}
+
+void CAddonInfoBuilderFromDB::SetExtensions(CAddonType addonType)
+{
+ if (!addonType.GetValue("provides").empty())
+ addonType.SetProvides(addonType.GetValue("provides").asString());
+
+ m_addonInfo->m_types.push_back(std::move(addonType));
+ m_addonInfo->m_mainType = addonType.m_type;
+}
+
+AddonInfoPtr CAddonInfoBuilder::Generate(const std::string& id, AddonType type)
+{
+ // Check addon identifier for forbidden characters
+ // The identifier is used e.g. in URLs so we shouldn't allow just
+ // any character to go through.
+ if (id.empty() || id.find_first_not_of(VALID_ADDON_IDENTIFIER_CHARACTERS) != std::string::npos)
+ {
+ CLog::Log(LOGERROR, "CAddonInfoBuilder::{}: identifier '{}' is invalid", __FUNCTION__, id);
+ return nullptr;
+ }
+
+ AddonInfoPtr addon = std::make_shared<CAddonInfo>();
+ addon->m_id = id;
+ addon->m_mainType = type;
+ return addon;
+}
+
+AddonInfoPtr CAddonInfoBuilder::Generate(const std::string& addonPath, bool platformCheck /*= true*/)
+{
+ auto addonRealPath = CSpecialProtocol::TranslatePath(addonPath);
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(URIUtils::AddFileToFolder(addonRealPath, "addon.xml")))
+ {
+ CLog::Log(LOGERROR, "CAddonInfoBuilder::{}: Unable to load '{}', Line {}\n{}",
+ __FUNCTION__,
+ URIUtils::AddFileToFolder(addonRealPath, "addon.xml"),
+ xmlDoc.ErrorRow(),
+ xmlDoc.ErrorDesc());
+ return nullptr;
+ }
+
+ AddonInfoPtr addon = std::make_shared<CAddonInfo>();
+ if (!ParseXML(addon, xmlDoc.RootElement(), addonRealPath))
+ return nullptr;
+
+ if (!platformCheck || PlatformSupportsAddon(addon))
+ return addon;
+
+ return nullptr;
+}
+
+AddonInfoPtr CAddonInfoBuilder::Generate(const TiXmlElement* baseElement,
+ const RepositoryDirInfo& repo,
+ bool platformCheck /*= true*/)
+{
+ AddonInfoPtr addon = std::make_shared<CAddonInfo>();
+ if (!ParseXML(addon, baseElement, repo.datadir, repo))
+ return nullptr;
+
+ if (!platformCheck || PlatformSupportsAddon(addon))
+ return addon;
+
+ return nullptr;
+}
+
+void CAddonInfoBuilder::SetInstallData(const AddonInfoPtr& addon, const CDateTime& installDate, const CDateTime& lastUpdated,
+ const CDateTime& lastUsed, const std::string& origin)
+{
+ if (!addon)
+ return;
+
+ addon->m_installDate = installDate;
+ addon->m_lastUpdated = lastUpdated;
+ addon->m_lastUsed = lastUsed;
+ addon->m_origin = origin;
+}
+
+bool CAddonInfoBuilder::ParseXML(const AddonInfoPtr& addon,
+ const TiXmlElement* element,
+ const std::string& addonPath)
+{
+ return ParseXML(addon, element, addonPath, {});
+}
+
+bool CAddonInfoBuilder::ParseXML(const AddonInfoPtr& addon,
+ const TiXmlElement* element,
+ const std::string& addonPath,
+ const RepositoryDirInfo& repo)
+{
+ /*
+ * Following values currently not set from creator:
+ * - CDateTime installDate;
+ * - CDateTime lastUpdated;
+ * - CDateTime lastUsed;
+ * - std::string origin;
+ */
+
+ if (!StringUtils::EqualsNoCase(element->Value(), "addon"))
+ {
+ CLog::Log(LOGERROR, "CAddonInfoBuilder::{}: file from '{}' doesn't contain <addon>", __FUNCTION__, addonPath);
+ return false;
+ }
+
+ /*
+ * The function variable "repo" is only used when reading data stored on the internet.
+ * A boolean value is then set here for easier identification.
+ */
+ const bool isRepoXMLContent = !repo.datadir.empty();
+
+ /*
+ * Parse addon.xml:
+ * <addon id="???"
+ * name="???"
+ * version="???"
+ * provider-name="???">
+ */
+ addon->m_id = StringUtils::CreateFromCString(element->Attribute("id"));
+ addon->m_name = StringUtils::CreateFromCString(element->Attribute("name"));
+ addon->m_author = StringUtils::CreateFromCString(element->Attribute("provider-name"));
+
+ const std::string version = StringUtils::CreateFromCString(element->Attribute("version"));
+ addon->m_version = CAddonVersion(version);
+
+ if (addon->m_id.empty() || addon->m_version.empty())
+ {
+ CLog::Log(LOGERROR, "CAddonInfoBuilder::{}: file '{}' doesn't contain required values on <addon ... > id='{}', version='{}'",
+ __FUNCTION__,
+ addonPath,
+ addon->m_id.empty() ? "missing" : addon->m_id,
+ addon->m_version.empty() ? "missing" : addon->m_version.asString());
+ return false;
+ }
+
+ // Check addon identifier for forbidden characters
+ // The identifier is used e.g. in URLs so we shouldn't allow just
+ // any character to go through.
+ if (addon->m_id.find_first_not_of(VALID_ADDON_IDENTIFIER_CHARACTERS) != std::string::npos)
+ {
+ CLog::Log(LOGERROR, "CAddonInfoBuilder::{}: identifier {} is invalid", __FUNCTION__, addon->m_id);
+ return false;
+ }
+
+ /*
+ * Parse addon.xml:
+ * <backwards-compatibility abi="???"/>
+ */
+ const TiXmlElement* backwards = element->FirstChildElement("backwards-compatibility");
+ if (backwards)
+ {
+ const std::string minVersion = StringUtils::CreateFromCString(backwards->Attribute("abi"));
+ addon->m_minversion = CAddonVersion(minVersion);
+ }
+
+ /*
+ * Parse addon.xml:
+ * <requires>
+ * <import addon="???" minversion="???" version="???" optional="???"/>
+ * </requires>
+ */
+ const TiXmlElement* requires = element->FirstChildElement("requires");
+ if (requires)
+ {
+ for (const TiXmlElement* child = requires->FirstChildElement("import"); child != nullptr; child = child->NextSiblingElement("import"))
+ {
+ if (child->Attribute("addon"))
+ {
+ const std::string minVersion =
+ StringUtils::CreateFromCString(child->Attribute("minversion"));
+ const std::string version = StringUtils::CreateFromCString(child->Attribute("version"));
+
+ bool optional = false;
+ child->QueryBoolAttribute("optional", &optional);
+
+ addon->m_dependencies.emplace_back(child->Attribute("addon"), CAddonVersion(minVersion),
+ CAddonVersion(version), optional);
+ }
+ }
+ }
+
+ std::string assetBasePath;
+ if (!isRepoXMLContent && !addonPath.empty())
+ {
+ // Default for add-on information not loaded from repository
+ assetBasePath = addonPath;
+ addon->m_path = addonPath;
+ }
+ else
+ {
+ assetBasePath = URIUtils::AddFileToFolder(repo.artdir, addon->m_id);
+ addon->m_path = URIUtils::AddFileToFolder(repo.datadir, addon->m_id, StringUtils::Format("{}-{}.zip", addon->m_id, addon->m_version.asString()));
+ }
+
+ addon->m_profilePath = StringUtils::Format("special://profile/addon_data/{}/", addon->m_id);
+
+ /*
+ * Parse addon.xml:
+ * <extension>
+ * ...
+ * </extension>
+ */
+ for (const TiXmlElement* child = element->FirstChildElement("extension"); child != nullptr; child = child->NextSiblingElement("extension"))
+ {
+ const std::string point = StringUtils::CreateFromCString(child->Attribute("point"));
+
+ if (point == "kodi.addon.metadata" || point == "xbmc.addon.metadata")
+ {
+ /*
+ * Parse addon.xml "<path">...</path>" (special related to repository path),
+ * do first and if present override the default. Also set assetBasePath to
+ * find screenshots and icons.
+ */
+ element = child->FirstChildElement("path");
+ if (element && element->GetText() != nullptr && !repo.datadir.empty())
+ {
+ addon->m_path = URIUtils::AddFileToFolder(repo.datadir, element->GetText());
+ assetBasePath = URIUtils::GetDirectory(URIUtils::AddFileToFolder(repo.artdir, element->GetText()));
+ }
+
+ /*
+ * Parse addon.xml "<summary lang="..">...</summary>"
+ */
+ GetTextList(child, "summary", addon->m_summary);
+
+ /*
+ * Parse addon.xml "<description lang="..">...</description>"
+ */
+ GetTextList(child, "description", addon->m_description);
+
+ /*
+ * Parse addon.xml "<disclaimer lang="..">...</disclaimer>"
+ */
+ GetTextList(child, "disclaimer", addon->m_disclaimer);
+
+ /*
+ * Parse addon.xml "<assets>...</assets>"
+ */
+ const TiXmlElement* element = child->FirstChildElement("assets");
+ if (element)
+ {
+ for (const TiXmlElement* elementsAssets = element->FirstChildElement(); elementsAssets != nullptr; elementsAssets = elementsAssets->NextSiblingElement())
+ {
+ std::string value = elementsAssets->Value();
+ if (value == "icon")
+ {
+ if (elementsAssets->GetText() != nullptr)
+ addon->m_icon = URIUtils::AddFileToFolder(assetBasePath, elementsAssets->GetText());
+ }
+ else if (value == "screenshot")
+ {
+ if (elementsAssets->GetText() != nullptr)
+ addon->m_screenshots.emplace_back(URIUtils::AddFileToFolder(assetBasePath, elementsAssets->GetText()));
+ }
+ else if (value == "fanart")
+ {
+ if (elementsAssets->GetText() != nullptr)
+ addon->m_art[value] = URIUtils::AddFileToFolder(assetBasePath, elementsAssets->GetText());
+ }
+ else if (value == "banner")
+ {
+ if (elementsAssets->GetText() != nullptr)
+ addon->m_art[value] = URIUtils::AddFileToFolder(assetBasePath, elementsAssets->GetText());
+ }
+ else if (value == "clearlogo")
+ {
+ if (elementsAssets->GetText() != nullptr)
+ addon->m_art[value] = URIUtils::AddFileToFolder(assetBasePath, elementsAssets->GetText());
+ }
+ else if (value == "thumb")
+ {
+ if (elementsAssets->GetText() != nullptr)
+ addon->m_art[value] =
+ URIUtils::AddFileToFolder(assetBasePath, elementsAssets->GetText());
+ }
+ }
+ }
+
+ /* Parse addon.xml "<platform">...</platform>" */
+ element = child->FirstChildElement("platform");
+ if (element && element->GetText() != nullptr)
+ {
+ auto platforms = StringUtils::Split(element->GetText(),
+ {" ", "\t", "\n", "\r"});
+ platforms.erase(std::remove_if(platforms.begin(), platforms.end(),
+ [](const std::string& platform) { return platform.empty(); }),
+ platforms.cend());
+ addon->m_platforms = platforms;
+ }
+
+ /* Parse addon.xml "<license">...</license>" */
+ element = child->FirstChildElement("license");
+ if (element && element->GetText() != nullptr)
+ addon->m_license = element->GetText();
+
+ /* Parse addon.xml "<source">...</source>" */
+ element = child->FirstChildElement("source");
+ if (element && element->GetText() != nullptr)
+ addon->m_source = element->GetText();
+
+ /* Parse addon.xml "<email">...</email>" */
+ element = child->FirstChildElement("email");
+ if (element && element->GetText() != nullptr)
+ addon->m_email = element->GetText();
+
+ /* Parse addon.xml "<website">...</website>" */
+ element = child->FirstChildElement("website");
+ if (element && element->GetText() != nullptr)
+ addon->m_website = element->GetText();
+
+ /* Parse addon.xml "<forum">...</forum>" */
+ element = child->FirstChildElement("forum");
+ if (element && element->GetText() != nullptr)
+ addon->m_forum = element->GetText();
+
+ /* Parse addon.xml "<broken">...</broken>"
+ * NOTE: Replaced with <lifecyclestate>, available for backward compatibility */
+ element = child->FirstChildElement("broken");
+ if (element && element->GetText() != nullptr)
+ {
+ addon->m_lifecycleState = AddonLifecycleState::BROKEN;
+ addon->m_lifecycleStateDescription.emplace(KODI_ADDON_DEFAULT_LANGUAGE_CODE,
+ element->GetText());
+ }
+
+ /* Parse addon.xml "<lifecyclestate">...</lifecyclestate>" */
+ element = child->FirstChildElement("lifecyclestate");
+ if (element && element->GetText() != nullptr)
+ {
+ const char* lang = element->Attribute("type");
+ if (lang)
+ {
+ if (strcmp(lang, "broken") == 0)
+ addon->m_lifecycleState = AddonLifecycleState::BROKEN;
+ else if (strcmp(lang, "deprecated") == 0)
+ addon->m_lifecycleState = AddonLifecycleState::DEPRECATED;
+ else
+ addon->m_lifecycleState = AddonLifecycleState::NORMAL;
+
+ GetTextList(child, "lifecyclestate", addon->m_lifecycleStateDescription);
+ }
+ }
+
+ /* Parse addon.xml "<language">...</language>" */
+ element = child->FirstChildElement("language");
+ if (element && element->GetText() != nullptr)
+ addon->AddExtraInfo("language", element->GetText());
+
+ /* Parse addon.xml "<reuselanguageinvoker">...</reuselanguageinvoker>" */
+ element = child->FirstChildElement("reuselanguageinvoker");
+ if (element && element->GetText() != nullptr)
+ addon->AddExtraInfo("reuselanguageinvoker", element->GetText());
+
+ /* Parse addon.xml "<size">...</size>" */
+ element = child->FirstChildElement("size");
+ if (element && element->GetText() != nullptr)
+ addon->m_packageSize = StringUtils::ToUint64(element->GetText(), 0);
+
+ /* Parse addon.xml "<news lang="..">...</news>"
+ *
+ * In the event that the changelog (news) in addon.xml is empty, check
+ * whether it is an installed addon and read a changelog.txt as a
+ * replacement, if available. */
+ GetTextList(child, "news", addon->m_changelog);
+ if (addon->m_changelog.empty() && !isRepoXMLContent && !addonPath.empty())
+ {
+ using XFILE::CFile;
+
+ const std::string changelog = URIUtils::AddFileToFolder(addonPath, "changelog.txt");
+ if (CFile::Exists(changelog))
+ {
+ CFile file;
+ std::vector<uint8_t> buf;
+ if (file.LoadFile(changelog, buf) > 0)
+ addon->m_changelog[KODI_ADDON_DEFAULT_LANGUAGE_CODE].assign(
+ reinterpret_cast<char*>(buf.data()), buf.size());
+ }
+ }
+ }
+ else
+ {
+ AddonType type = CAddonInfo::TranslateType(point);
+ if (type == AddonType::UNKNOWN || type >= AddonType::MAX_TYPES)
+ {
+ CLog::Log(LOGERROR, "CAddonInfoBuilder::{}: file '{}' doesn't contain a valid add-on type name ({})", __FUNCTION__, addon->m_path, point);
+ return false;
+ }
+
+ CAddonType addonType(type);
+ if (ParseXMLTypes(addonType, addon, child))
+ addon->m_types.emplace_back(std::move(addonType));
+ }
+ }
+
+ /*
+ * If nothing is defined in addon.xml set addon as unknown to have minimum one
+ * instance type present.
+ */
+ if (addon->m_types.empty())
+ {
+ CAddonType addonType(AddonType::UNKNOWN);
+ addon->m_types.emplace_back(std::move(addonType));
+ }
+
+ addon->m_mainType = addon->m_types[0].Type();
+ addon->m_libname = addon->m_types[0].m_libname;
+ if (!addon->m_types[0].GetValue("provides").empty())
+ addon->AddExtraInfo("provides", addon->m_types[0].GetValue("provides").asString());
+
+ // Ensure binary types have a valid library for the platform
+ if (addon->m_mainType == AddonType::VISUALIZATION ||
+ addon->m_mainType == AddonType::SCREENSAVER || addon->m_mainType == AddonType::PVRDLL ||
+ addon->m_mainType == AddonType::AUDIOENCODER ||
+ addon->m_mainType == AddonType::AUDIODECODER || addon->m_mainType == AddonType::VFS ||
+ addon->m_mainType == AddonType::IMAGEDECODER || addon->m_mainType == AddonType::INPUTSTREAM ||
+ addon->m_mainType == AddonType::PERIPHERALDLL || addon->m_mainType == AddonType::GAMEDLL)
+ {
+ if (addon->m_libname.empty())
+ {
+ // Prevent log file entry if data is from repository, there normal on
+ // addons for other OS's
+ if (!isRepoXMLContent)
+ CLog::Log(LOGERROR, "CAddonInfoBuilder::{}: addon.xml from '{}' for binary type '{}' doesn't contain library and addon becomes ignored",
+ __FUNCTION__, addon->ID(), CAddonInfo::TranslateType(addon->m_mainType));
+ return false;
+ }
+ }
+
+ if (!isRepoXMLContent)
+ {
+ using XFILE::CFile;
+ if (CFile::Exists(URIUtils::AddFileToFolder(addonPath, "resources", "settings.xml")))
+ addon->m_supportsAddonSettings = true;
+ if (CFile::Exists(URIUtils::AddFileToFolder(addonPath, "resources", "instance-settings.xml")))
+ addon->m_supportsInstanceSettings = true;
+ }
+
+ addon->m_addonInstanceSupportType = CAddonInfo::InstanceSupportType(addon->m_mainType);
+
+ return true;
+}
+
+bool CAddonInfoBuilder::ParseXMLTypes(CAddonType& addonType,
+ const AddonInfoPtr& info,
+ const TiXmlElement* child)
+{
+ if (child)
+ {
+ addonType.m_path = info->Path();
+
+ // Get add-on library file name (if present)
+ const char* library = child->Attribute("library");
+ if (library == nullptr)
+ library = GetPlatformLibraryName(child);
+ if (library != nullptr)
+ {
+ addonType.m_libname = library;
+
+ try
+ {
+ // linux is different and has the version number after the suffix
+ static const std::regex libRegex("^.*" +
+ CCompileInfo::CCompileInfo::GetSharedLibrarySuffix() +
+ "\\.?[0-9]*\\.?[0-9]*\\.?[0-9]*$");
+ if (std::regex_match(library, libRegex))
+ {
+ info->SetBinary(true);
+ CLog::Log(LOGDEBUG, "CAddonInfoBuilder::{}: Binary addon found: {}", __func__,
+ info->ID());
+ }
+ }
+ catch (const std::regex_error& e)
+ {
+ CLog::Log(LOGERROR, "CAddonInfoBuilder::{}: Regex error caught: {}", __func__,
+ e.what());
+ }
+ }
+
+ if (!ParseXMLExtension(addonType, child))
+ {
+ CLog::Log(LOGERROR, "CAddonInfoBuilder::{}: addon.xml file doesn't contain a valid add-on extensions ({})", __FUNCTION__, info->ID());
+ return false;
+ }
+ if (!addonType.GetValue("provides").empty())
+ addonType.SetProvides(addonType.GetValue("provides").asString());
+ return true;
+ }
+ return false;
+}
+
+bool CAddonInfoBuilder::ParseXMLExtension(CAddonExtensions& addonExt, const TiXmlElement* element)
+{
+ addonExt.m_point = StringUtils::CreateFromCString(element->Attribute("point"));
+
+ EXT_VALUE extension;
+ const TiXmlAttribute* attribute = element->FirstAttribute();
+ while (attribute)
+ {
+ std::string name = attribute->Name();
+ if (name != "point")
+ {
+ const std::string value = StringUtils::CreateFromCString(attribute->Value());
+ if (!value.empty())
+ {
+ name = "@" + name;
+ extension.emplace_back(std::make_pair(name, SExtValue(value)));
+ }
+ }
+ attribute = attribute->Next();
+ }
+ if (!extension.empty())
+ addonExt.m_values.emplace_back(std::pair<std::string, EXT_VALUE>("", std::move(extension)));
+
+ const TiXmlElement* childElement = element->FirstChildElement();
+ while (childElement)
+ {
+ const std::string id = StringUtils::CreateFromCString(childElement->Value());
+ if (!id.empty())
+ {
+ EXT_VALUE extension;
+ const TiXmlAttribute* attribute = childElement->FirstAttribute();
+ while (attribute)
+ {
+ std::string name = attribute->Name();
+ if (name != "point")
+ {
+ const std::string value = StringUtils::CreateFromCString(attribute->Value());
+ if (!value.empty())
+ {
+ name = id + "@" + name;
+ extension.emplace_back(std::make_pair(name, SExtValue(value)));
+ }
+ }
+ attribute = attribute->Next();
+ }
+
+ const std::string childElementText = StringUtils::CreateFromCString(childElement->GetText());
+
+ if (!childElementText.empty())
+ {
+ extension.emplace_back(std::make_pair(id, SExtValue(childElementText)));
+ }
+
+ if (!extension.empty())
+ addonExt.m_values.emplace_back(std::make_pair(id, std::move(extension)));
+
+ if (childElementText.empty())
+ {
+ const TiXmlElement* childSubElement = childElement->FirstChildElement();
+ if (childSubElement)
+ {
+ CAddonExtensions subElement;
+ if (ParseXMLExtension(subElement, childElement))
+ addonExt.m_children.emplace_back(std::make_pair(id, std::move(subElement)));
+ }
+ }
+ }
+ childElement = childElement->NextSiblingElement();
+ }
+
+ return true;
+}
+
+bool CAddonInfoBuilder::GetTextList(const TiXmlElement* element, const std::string& tag, std::unordered_map<std::string, std::string>& translatedValues)
+{
+ if (!element)
+ return false;
+
+ translatedValues.clear();
+
+ for (const TiXmlElement* child = element->FirstChildElement(tag); child != nullptr; child = child->NextSiblingElement(tag))
+ {
+ const char* lang = child->Attribute("lang");
+ const char* text = child->GetText();
+ if (lang != nullptr)
+ {
+ if (strcmp(lang, "no") == 0)
+ translatedValues.insert(std::make_pair("nb_NO", text != nullptr ? text : ""));
+ else
+ translatedValues.insert(std::make_pair(lang, text != nullptr ? text : ""));
+ }
+ else
+ translatedValues.insert(
+ std::make_pair(KODI_ADDON_DEFAULT_LANGUAGE_CODE, text != nullptr ? text : ""));
+ }
+
+ return !translatedValues.empty();
+}
+
+const char* CAddonInfoBuilder::GetPlatformLibraryName(const TiXmlElement* element)
+{
+ const char* libraryName;
+#if defined(TARGET_ANDROID)
+ libraryName = element->Attribute("library_android");
+#elif defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
+#if defined(TARGET_FREEBSD)
+ libraryName = element->Attribute("library_freebsd");
+ if (libraryName == nullptr)
+#endif
+ libraryName = element->Attribute("library_linux");
+#elif defined(TARGET_WINDOWS_DESKTOP)
+ libraryName = element->Attribute("library_windx");
+ if (libraryName == nullptr)
+ libraryName = element->Attribute("library_windows");
+#elif defined(TARGET_WINDOWS_STORE)
+ libraryName = element->Attribute("library_windowsstore");
+#elif defined(TARGET_DARWIN)
+#if defined(TARGET_DARWIN_EMBEDDED)
+ libraryName = element->Attribute("library_darwin_embedded");
+#else
+ libraryName = element->Attribute("library_osx");
+#endif
+#endif
+
+ return libraryName;
+}
+
+bool CAddonInfoBuilder::PlatformSupportsAddon(const AddonInfoPtr& addon)
+{
+ if (addon->m_platforms.empty())
+ return true;
+
+ std::vector<std::string> supportedPlatforms = {
+ "all",
+#if defined(TARGET_ANDROID)
+ "android",
+#if defined(__ARM_ARCH_7A__)
+ "android-armv7",
+#elif defined(__aarch64__)
+ "android-aarch64",
+#elif defined(__i686__)
+ "android-i686",
+#elif defined(__x86_64__)
+ "android-x86_64",
+#else
+ #warning no architecture dependant platform tag
+#endif
+#elif defined(TARGET_FREEBSD)
+ "freebsd",
+#elif defined(TARGET_LINUX)
+ "linux",
+#if defined(__ARM_ARCH_7A__)
+ "linux-armv7",
+#elif defined(__aarch64__)
+ "linux-aarch64",
+#elif defined(__i686__)
+ "linux-i686",
+#elif defined(__x86_64__)
+ "linux-x86_64",
+#else
+ #warning no architecture dependant platform tag
+#endif
+#elif defined(TARGET_WINDOWS_DESKTOP)
+ "windx",
+ "windows",
+#if defined(_M_IX86)
+ "windows-i686",
+#elif defined(_M_AMD64)
+ "windows-x86_64",
+#else
+#error no architecture dependant platform tag
+#endif
+#elif defined(TARGET_WINDOWS_STORE)
+ "windowsstore",
+#elif defined(TARGET_DARWIN_EMBEDDED)
+ "darwin_embedded",
+#if defined(TARGET_DARWIN_IOS)
+ "ios",
+#if defined(__aarch64__)
+ "ios-aarch64",
+#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__)
+ "osx64",
+ "osx-x86_64",
+#elif defined(__aarch64__)
+ "osxarm64",
+ "osx-arm64",
+#else
+#warning no architecture dependant platform tag
+#endif
+#endif
+ };
+
+ return std::find_first_of(addon->m_platforms.begin(), addon->m_platforms.end(),
+ supportedPlatforms.begin(), supportedPlatforms.end()) != addon->m_platforms.end();
+}
+
+}
diff --git a/xbmc/addons/addoninfo/AddonInfoBuilder.h b/xbmc/addons/addoninfo/AddonInfoBuilder.h
new file mode 100644
index 0000000..adbbc84
--- /dev/null
+++ b/xbmc/addons/addoninfo/AddonInfoBuilder.h
@@ -0,0 +1,109 @@
+/*
+ * 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 "addons/IAddon.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+class CDateTime;
+class TiXmlElement;
+
+namespace ADDON
+{
+enum class AddonLifecycleState;
+enum class AddonType;
+
+class CAddonExtensions;
+class CAddonType;
+
+struct DependencyInfo;
+struct RepositoryDirInfo;
+
+class CAddonInfo;
+using AddonInfoPtr = std::shared_ptr<CAddonInfo>;
+
+class CAddonInfoBuilder
+{
+public:
+ static AddonInfoPtr Generate(const std::string& id, AddonType type);
+ static AddonInfoPtr Generate(const std::string& addonPath, bool platformCheck = true);
+ static AddonInfoPtr Generate(const TiXmlElement* baseElement,
+ const RepositoryDirInfo& repo,
+ bool platformCheck = true);
+
+ /*!
+ * @brief Parts used from CAddonDatabase
+ */
+ //@{
+ static void SetInstallData(const AddonInfoPtr& addon, const CDateTime& installDate,
+ const CDateTime& lastUpdated, const CDateTime& lastUsed, const std::string& origin);
+ //@}
+
+private:
+ static bool ParseXML(const AddonInfoPtr& addon,
+ const TiXmlElement* element,
+ const std::string& addonPath);
+ static bool ParseXML(const AddonInfoPtr& addon,
+ const TiXmlElement* element,
+ const std::string& addonPath,
+ const RepositoryDirInfo& repo);
+ static bool ParseXMLTypes(CAddonType& addonType,
+ const AddonInfoPtr& info,
+ const TiXmlElement* child);
+ static bool ParseXMLExtension(CAddonExtensions& addonExt, const TiXmlElement* element);
+ static bool GetTextList(const TiXmlElement* element, const std::string& tag, std::unordered_map<std::string, std::string>& translatedValues);
+ static const char* GetPlatformLibraryName(const TiXmlElement* element);
+ static bool PlatformSupportsAddon(const AddonInfoPtr& addon);
+};
+
+class CAddonInfoBuilderFromDB
+{
+public:
+ CAddonInfoBuilderFromDB();
+
+ void SetId(std::string id);
+ void SetName(std::string name);
+ void SetLicense(std::string license);
+ void SetSummary(std::string summary);
+ void SetDescription(std::string description);
+ void SetDisclaimer(std::string disclaimer);
+ void SetAuthor(std::string author);
+ void SetSource(std::string source);
+ void SetWebsite(std::string website);
+ void SetForum(std::string forum);
+ void SetEMail(std::string email);
+ void SetIcon(std::string icon);
+ void SetArt(const std::string& type, std::string value);
+ void SetArt(std::map<std::string, std::string> art);
+ void SetScreenshots(std::vector<std::string> screenshots);
+ void SetChangelog(std::string changelog);
+ void SetLifecycleState(AddonLifecycleState state, std::string description);
+ void SetPath(std::string path);
+ void SetLibName(std::string libname);
+ void SetVersion(CAddonVersion version);
+ void SetDependencies(std::vector<DependencyInfo> dependencies);
+ void SetExtrainfo(InfoMap extrainfo);
+ void SetInstallDate(const CDateTime& installDate);
+ void SetLastUpdated(const CDateTime& lastUpdated);
+ void SetLastUsed(const CDateTime& lastUsed);
+ void SetOrigin(std::string origin);
+ void SetPackageSize(uint64_t size);
+ void SetExtensions(CAddonType addonType);
+
+ const AddonInfoPtr& get() { return m_addonInfo; }
+
+private:
+ AddonInfoPtr m_addonInfo;
+};
+}
diff --git a/xbmc/addons/addoninfo/AddonType.cpp b/xbmc/addons/addoninfo/AddonType.cpp
new file mode 100644
index 0000000..875e66f
--- /dev/null
+++ b/xbmc/addons/addoninfo/AddonType.cpp
@@ -0,0 +1,59 @@
+/*
+ * 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 "AddonType.h"
+
+#include "addons/addoninfo/AddonInfo.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+namespace ADDON
+{
+static const std::set<AddonType> dependencyTypes = {
+ AddonType::SCRAPER_LIBRARY,
+ AddonType::SCRIPT_LIBRARY,
+ AddonType::SCRIPT_MODULE,
+};
+} /* namespace ADDON */
+
+using namespace ADDON;
+
+std::string CAddonType::LibPath() const
+{
+ if (m_libname.empty())
+ return "";
+ return URIUtils::AddFileToFolder(m_path, m_libname);
+}
+
+void CAddonType::SetProvides(const std::string& content)
+{
+ if (!content.empty())
+ {
+ /*
+ * Normally the "provides" becomes added from xml scan, but for add-ons
+ * stored in the database (e.g. repository contents) it might not be
+ * available. Since this information is available in add-on metadata for the
+ * main type (see extrainfo) we take the function contents and insert it if
+ * empty.
+ */
+ if (GetValue("provides").empty())
+ Insert("provides", content);
+
+ for (const auto& provide : StringUtils::Split(content, ' '))
+ {
+ AddonType content = CAddonInfo::TranslateSubContent(provide);
+ if (content != AddonType::UNKNOWN)
+ m_providedSubContent.insert(content);
+ }
+ }
+}
+
+bool CAddonType::IsDependencyType(AddonType type)
+{
+ return dependencyTypes.find(type) != dependencyTypes.end();
+}
diff --git a/xbmc/addons/addoninfo/AddonType.h b/xbmc/addons/addoninfo/AddonType.h
new file mode 100644
index 0000000..9a0ee26
--- /dev/null
+++ b/xbmc/addons/addoninfo/AddonType.h
@@ -0,0 +1,125 @@
+/*
+ * 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 "addons/addoninfo/AddonExtensions.h"
+
+#include <set>
+#include <string>
+
+class TiXmlElement;
+
+namespace ADDON
+{
+
+enum class AddonType
+{
+ UNKNOWN = 0,
+ VISUALIZATION,
+ SKIN,
+ PVRDLL,
+ INPUTSTREAM,
+ GAMEDLL,
+ PERIPHERALDLL,
+ SCRIPT,
+ SCRIPT_WEATHER,
+ SUBTITLE_MODULE,
+ SCRIPT_LYRICS,
+ SCRAPER_ALBUMS,
+ SCRAPER_ARTISTS,
+ SCRAPER_MOVIES,
+ SCRAPER_MUSICVIDEOS,
+ SCRAPER_TVSHOWS,
+ SCREENSAVER,
+ PLUGIN,
+ REPOSITORY,
+ WEB_INTERFACE,
+ SERVICE,
+ AUDIOENCODER,
+ CONTEXTMENU_ITEM,
+ AUDIODECODER,
+ RESOURCE_IMAGES,
+ RESOURCE_LANGUAGE,
+ RESOURCE_UISOUNDS,
+ RESOURCE_GAMES,
+ RESOURCE_FONT,
+ VFS,
+ IMAGEDECODER,
+ SCRAPER_LIBRARY,
+ SCRIPT_LIBRARY,
+ SCRIPT_MODULE,
+ GAME_CONTROLLER,
+ VIDEOCODEC,
+
+ /**
+ * @brief virtual addon types
+ */
+ //@{
+ VIDEO,
+ AUDIO,
+ IMAGE,
+ EXECUTABLE,
+ GAME,
+ //@}
+
+ MAX_TYPES
+};
+
+class CAddonInfoBuilder;
+class CAddonDatabaseSerializer;
+
+class CAddonType : public CAddonExtensions
+{
+public:
+ CAddonType(AddonType type = AddonType::UNKNOWN) : m_type(type) {}
+
+ AddonType Type() const { return m_type; }
+ std::string LibPath() const;
+ const std::string& LibName() const { return m_libname; }
+
+ bool ProvidesSubContent(const AddonType& content) const
+ {
+ return content == AddonType::UNKNOWN
+ ? false
+ : m_type == content || m_providedSubContent.count(content) > 0;
+ }
+
+ bool ProvidesSeveralSubContents() const
+ {
+ return m_providedSubContent.size() > 1;
+ }
+
+ size_t ProvidedSubContents() const
+ {
+ return m_providedSubContent.size();
+ }
+
+ /*!
+ * @brief Indicates whether a given type is a dependency type (e.g. addons which the main type is
+ * a script.module)
+ *
+ * @param[in] type the provided type
+ * @return true if type is one of the dependency types
+ */
+ static bool IsDependencyType(AddonType type);
+
+private:
+ friend class CAddonInfoBuilder;
+ friend class CAddonInfoBuilderFromDB;
+ friend class CAddonDatabaseSerializer;
+
+ void SetProvides(const std::string& content);
+
+ AddonType m_type;
+ std::string m_path;
+ std::string m_libname;
+ std::set<AddonType> m_providedSubContent;
+};
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/addoninfo/CMakeLists.txt b/xbmc/addons/addoninfo/CMakeLists.txt
new file mode 100644
index 0000000..eb66dd3
--- /dev/null
+++ b/xbmc/addons/addoninfo/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES AddonInfoBuilder.cpp
+ AddonExtensions.cpp
+ AddonInfo.cpp
+ AddonType.cpp)
+
+set(HEADERS AddonInfoBuilder.h
+ AddonExtensions.h
+ AddonInfo.h
+ AddonType.h)
+
+core_add_library(addons_addoninfo)
diff --git a/xbmc/addons/binary-addons/AddonDll.cpp b/xbmc/addons/binary-addons/AddonDll.cpp
new file mode 100644
index 0000000..4ff5254
--- /dev/null
+++ b/xbmc/addons/binary-addons/AddonDll.cpp
@@ -0,0 +1,553 @@
+/*
+ * 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 "AddonDll.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonStatusHandler.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/binary-addons/BinaryAddonBase.h"
+#include "addons/binary-addons/BinaryAddonManager.h"
+#include "addons/binary-addons/DllAddon.h"
+#include "addons/interfaces/AddonBase.h"
+#include "addons/kodi-dev-kit/include/kodi/versions.h"
+#include "addons/settings/AddonSettings.h"
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "settings/lib/SettingSection.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <utility>
+
+using namespace KODI::MESSAGING;
+
+namespace ADDON
+{
+
+CAddonDll::CAddonDll(const AddonInfoPtr& addonInfo, BinaryAddonBasePtr addonBase)
+ : CAddon(addonInfo, addonInfo->MainType()), m_binaryAddonBase(std::move(addonBase))
+{
+}
+
+CAddonDll::CAddonDll(const AddonInfoPtr& addonInfo, AddonType addonType)
+ : CAddon(addonInfo, addonType),
+ m_binaryAddonBase(CServiceBroker::GetBinaryAddonManager().GetRunningAddonBase(addonInfo->ID()))
+{
+}
+
+CAddonDll::~CAddonDll()
+{
+ if (m_initialized)
+ Destroy();
+}
+
+std::string CAddonDll::GetDllPath(const std::string &libPath)
+{
+ std::string strFileName = libPath;
+ std::string strLibName = URIUtils::GetFileName(strFileName);
+
+ if (strLibName.empty())
+ return "";
+
+ /* Check if lib being loaded exists, else check in XBMC binary location */
+#if defined(TARGET_ANDROID)
+ if (XFILE::CFile::Exists(strFileName))
+ {
+ bool doCopy = true;
+ std::string dstfile = URIUtils::AddFileToFolder(CSpecialProtocol::TranslatePath("special://xbmcaltbinaddons/"), strLibName);
+
+ struct __stat64 dstFileStat;
+ if (XFILE::CFile::Stat(dstfile, &dstFileStat) == 0)
+ {
+ struct __stat64 srcFileStat;
+ if (XFILE::CFile::Stat(strFileName, &srcFileStat) == 0)
+ {
+ if (dstFileStat.st_size == srcFileStat.st_size && dstFileStat.st_mtime > srcFileStat.st_mtime)
+ doCopy = false;
+ }
+ }
+
+ if (doCopy)
+ {
+ CLog::Log(LOGDEBUG, "ADDON: caching {} to {}", strFileName, dstfile);
+ XFILE::CFile::Copy(strFileName, dstfile);
+ }
+
+ strFileName = dstfile;
+ }
+ if (!XFILE::CFile::Exists(strFileName))
+ {
+ std::string tempbin = getenv("KODI_ANDROID_LIBS");
+ strFileName = tempbin + "/" + strLibName;
+ }
+#endif
+
+ if (!XFILE::CFile::Exists(strFileName))
+ {
+ std::string strAltFileName;
+
+ std::string altbin = CSpecialProtocol::TranslatePath("special://xbmcaltbinaddons/");
+ if (!altbin.empty())
+ {
+ strAltFileName = altbin + strLibName;
+ if (!XFILE::CFile::Exists(strAltFileName))
+ {
+ std::string temp = CSpecialProtocol::TranslatePath("special://xbmc/addons/");
+ strAltFileName = strFileName;
+ strAltFileName.erase(0, temp.size());
+ strAltFileName = altbin + strAltFileName;
+ }
+ CLog::Log(LOGDEBUG, "ADDON: Trying to load {}", strAltFileName);
+ }
+
+ if (XFILE::CFile::Exists(strAltFileName))
+ strFileName = strAltFileName;
+ else
+ {
+ std::string temp = CSpecialProtocol::TranslatePath("special://xbmc/");
+ std::string tempbin = CSpecialProtocol::TranslatePath("special://xbmcbin/");
+ strFileName.erase(0, temp.size());
+ strFileName = tempbin + strFileName;
+ if (!XFILE::CFile::Exists(strFileName))
+ {
+ CLog::Log(LOGERROR, "ADDON: Could not locate {}", strLibName);
+ strFileName.clear();
+ }
+ }
+ }
+
+ return strFileName;
+}
+
+std::string CAddonDll::LibPath() const
+{
+ return GetDllPath(CAddon::LibPath());
+}
+
+bool CAddonDll::LoadDll()
+{
+ if (m_pDll)
+ return true;
+
+ std::string strFileName = LibPath();
+ if (strFileName.empty())
+ return false;
+
+ /* Load the Dll */
+ m_pDll = new DllAddon;
+ m_pDll->SetFile(strFileName);
+ m_pDll->EnableDelayedUnload(false);
+ if (!m_pDll->Load())
+ {
+ delete m_pDll;
+ m_pDll = nullptr;
+
+ std::string heading =
+ StringUtils::Format("{}: {}", CAddonInfo::TranslateType(Type(), true), Name());
+ HELPERS::ShowOKDialogLines(CVariant{heading}, CVariant{24070}, CVariant{24071});
+
+ return false;
+ }
+
+ return true;
+}
+
+ADDON_STATUS CAddonDll::Create(KODI_ADDON_INSTANCE_STRUCT* firstKodiInstance)
+{
+ CLog::Log(LOGDEBUG, "ADDON: Dll Initializing - {}", Name());
+ m_initialized = false;
+
+ if (!LoadDll())
+ {
+ return ADDON_STATUS_PERMANENT_FAILURE;
+ }
+
+ /* Check versions about global parts on add-on (parts used on all types) */
+ for (unsigned int id = ADDON_GLOBAL_MAIN; id <= ADDON_GLOBAL_MAX; ++id)
+ {
+ if (!CheckAPIVersion(id))
+ return ADDON_STATUS_PERMANENT_FAILURE;
+ }
+
+ /* Allocate the helper function class to allow crosstalk over
+ helper add-on headers */
+ if (!Interface_Base::InitInterface(this, m_interface, firstKodiInstance))
+ return ADDON_STATUS_PERMANENT_FAILURE;
+
+ /* Call Create to make connections, initializing data or whatever is
+ needed to become the AddOn running */
+ ADDON_STATUS status = m_pDll->Create(&m_interface);
+
+ // "C" ABI related call, if on add-on used.
+ if (status == ADDON_STATUS_OK && m_interface.toAddon->create)
+ status = m_interface.toAddon->create(m_interface.firstKodiInstance, &m_interface.addonBase);
+
+ if (status == ADDON_STATUS_OK)
+ {
+ m_initialized = true;
+ }
+ else if (status == ADDON_STATUS_NEED_SETTINGS)
+ {
+ if ((status = TransferSettings(ADDON_SETTINGS_ID)) == ADDON_STATUS_OK)
+ m_initialized = true;
+ else
+ new CAddonStatusHandler(ID(), ADDON_SETTINGS_ID, status, false);
+ }
+ else
+ { // Addon failed initialization
+ CLog::Log(LOGERROR,
+ "ADDON: Dll {} - Client returned bad status ({}) from Create and is not usable",
+ Name(), status);
+
+ // @todo currently a copy and paste from other function and becomes improved.
+ std::string heading =
+ StringUtils::Format("{}: {}", CAddonInfo::TranslateType(Type(), true), Name());
+ HELPERS::ShowOKDialogLines(CVariant{ heading }, CVariant{ 24070 }, CVariant{ 24071 });
+ }
+
+ return status;
+}
+
+void CAddonDll::Destroy()
+{
+ /* Unload library file */
+ if (m_pDll)
+ {
+ if (m_interface.toAddon->destroy)
+ m_interface.toAddon->destroy(m_interface.addonBase);
+ m_pDll->Unload();
+ }
+
+ Interface_Base::DeInitInterface(m_interface);
+
+ if (m_pDll)
+ {
+ delete m_pDll;
+ m_pDll = nullptr;
+ CLog::Log(LOGINFO, "ADDON: Dll Destroyed - {}", Name());
+ }
+
+ ResetSettings(ADDON_SETTINGS_ID);
+
+ m_initialized = false;
+}
+
+ADDON_STATUS CAddonDll::CreateInstance(KODI_ADDON_INSTANCE_STRUCT* instance)
+{
+ assert(instance != nullptr);
+ assert(instance->functions != nullptr);
+ assert(instance->info != nullptr);
+ assert(instance->info->functions != nullptr);
+
+ ADDON_STATUS status = ADDON_STATUS_OK;
+
+ if (!m_initialized)
+ status = Create(instance);
+ if (status != ADDON_STATUS_OK)
+ return status;
+
+ /* Check version of requested instance type */
+ if (!CheckAPIVersion(instance->info->type))
+ return ADDON_STATUS_PERMANENT_FAILURE;
+
+ status = m_interface.toAddon->create_instance(m_interface.addonBase, instance);
+
+ if (instance->info)
+ {
+ m_usedInstances[instance->info->kodi] = instance;
+ }
+
+ return status;
+}
+
+void CAddonDll::DestroyInstance(KODI_ADDON_INSTANCE_STRUCT* instance)
+{
+ if (m_usedInstances.empty())
+ return;
+
+ auto it = m_usedInstances.find(instance->info->kodi);
+ if (it != m_usedInstances.end())
+ {
+ m_interface.toAddon->destroy_instance(m_interface.addonBase, it->second);
+ m_usedInstances.erase(it);
+ }
+
+ if (m_usedInstances.empty())
+ Destroy();
+}
+
+bool CAddonDll::IsInUse() const
+{
+ if (m_informer)
+ return m_informer->IsInUse(ID());
+ return false;
+}
+
+void CAddonDll::RegisterInformer(CAddonDllInformer* informer)
+{
+ m_informer = informer;
+}
+
+AddonPtr CAddonDll::GetRunningInstance() const
+{
+ if (CServiceBroker::IsAddonInterfaceUp())
+ return CServiceBroker::GetBinaryAddonManager().GetRunningAddon(ID());
+
+ return AddonPtr();
+}
+
+void CAddonDll::OnPreInstall()
+{
+ if (m_binaryAddonBase)
+ m_binaryAddonBase->OnPreInstall();
+}
+
+void CAddonDll::OnPostInstall(bool update, bool modal)
+{
+ if (m_binaryAddonBase)
+ m_binaryAddonBase->OnPostInstall(update, modal);
+}
+
+void CAddonDll::OnPreUnInstall()
+{
+ if (m_binaryAddonBase)
+ m_binaryAddonBase->OnPreUnInstall();
+}
+
+void CAddonDll::OnPostUnInstall()
+{
+ if (m_binaryAddonBase)
+ m_binaryAddonBase->OnPostUnInstall();
+}
+
+bool CAddonDll::DllLoaded(void) const
+{
+ return m_pDll != nullptr;
+}
+
+CAddonVersion CAddonDll::GetTypeVersionDll(int type) const
+{
+ return CAddonVersion(m_pDll ? m_pDll->GetAddonTypeVersion(type) : nullptr);
+}
+
+CAddonVersion CAddonDll::GetTypeMinVersionDll(int type) const
+{
+ return CAddonVersion(m_pDll ? m_pDll->GetAddonTypeMinVersion(type) : nullptr);
+}
+
+void CAddonDll::SaveSettings(AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ // must save first, as TransferSettings() reloads saved settings!
+ CAddon::SaveSettings(id);
+ if (m_initialized)
+ TransferSettings(id);
+}
+
+ADDON_STATUS CAddonDll::TransferSettings(AddonInstanceId instanceId)
+{
+ bool restart = false;
+ ADDON_STATUS reportStatus = ADDON_STATUS_OK;
+
+ CLog::Log(LOGDEBUG, "Calling TransferSettings for: {}", Name());
+
+ LoadSettings(false, true, instanceId);
+
+ auto settings = GetSettings(instanceId);
+ if (settings != nullptr)
+ {
+ KODI_ADDON_INSTANCE_FUNC* instanceTarget{nullptr};
+ KODI_ADDON_INSTANCE_HDL instanceHandle{nullptr};
+ if (instanceId != ADDON_SETTINGS_ID)
+ {
+ const auto it = std::find_if(
+ m_usedInstances.begin(), m_usedInstances.end(),
+ [instanceId](const auto& data) { return data.second->info->number == instanceId; });
+ if (it == m_usedInstances.end())
+ return ADDON_STATUS_UNKNOWN;
+
+ instanceTarget = it->second->functions;
+ instanceHandle = it->second->hdl;
+ }
+
+ for (const auto& section : settings->GetSections())
+ {
+ for (const auto& category : section->GetCategories())
+ {
+ for (const auto& group : category->GetGroups())
+ {
+ for (const auto& setting : group->GetSettings())
+ {
+ if (StringUtils::StartsWith(setting->GetId(), ADDON_SETTING_INSTANCE_GROUP))
+ continue; // skip internal settings
+
+ ADDON_STATUS status = ADDON_STATUS_OK;
+ const char* id = setting->GetId().c_str();
+
+ switch (setting->GetType())
+ {
+ case SettingType::Boolean:
+ {
+ bool tmp = std::static_pointer_cast<CSettingBool>(setting)->GetValue();
+ if (instanceId == ADDON_SETTINGS_ID)
+ {
+ if (m_interface.toAddon->setting_change_boolean)
+ status =
+ m_interface.toAddon->setting_change_boolean(m_interface.addonBase, id, tmp);
+ }
+ else if (instanceTarget && instanceHandle)
+ {
+ if (instanceTarget->instance_setting_change_boolean)
+ status =
+ instanceTarget->instance_setting_change_boolean(instanceHandle, id, tmp);
+ }
+ break;
+ }
+
+ case SettingType::Integer:
+ {
+ int tmp = std::static_pointer_cast<CSettingInt>(setting)->GetValue();
+ if (instanceId == ADDON_SETTINGS_ID)
+ {
+ if (m_interface.toAddon->setting_change_integer)
+ status =
+ m_interface.toAddon->setting_change_integer(m_interface.addonBase, id, tmp);
+ }
+ else if (instanceTarget && instanceHandle)
+ {
+ if (instanceTarget->instance_setting_change_integer)
+ status =
+ instanceTarget->instance_setting_change_integer(instanceHandle, id, tmp);
+ }
+ break;
+ }
+
+ case SettingType::Number:
+ {
+ float tmpf = static_cast<float>(std::static_pointer_cast<CSettingNumber>(setting)->GetValue());
+ if (instanceId == ADDON_SETTINGS_ID)
+ {
+ if (m_interface.toAddon->setting_change_float)
+ status =
+ m_interface.toAddon->setting_change_float(m_interface.addonBase, id, tmpf);
+ }
+ else if (instanceTarget && instanceHandle)
+ {
+ if (instanceTarget->instance_setting_change_float)
+ status =
+ instanceTarget->instance_setting_change_float(instanceHandle, id, tmpf);
+ }
+ break;
+ }
+
+ case SettingType::String:
+ {
+ if (instanceId == ADDON_SETTINGS_ID)
+ {
+ if (m_interface.toAddon->setting_change_string)
+ status = m_interface.toAddon->setting_change_string(
+ m_interface.addonBase, id,
+ std::static_pointer_cast<CSettingString>(setting)->GetValue().c_str());
+ }
+ else if (instanceTarget && instanceHandle)
+ {
+ if (instanceTarget->instance_setting_change_string)
+ status = instanceTarget->instance_setting_change_string(
+ instanceHandle, id,
+ std::static_pointer_cast<CSettingString>(setting)->GetValue().c_str());
+ }
+ break;
+ }
+
+ default:
+ {
+ // log unknowns as an error, but go ahead and transfer the string
+ CLog::Log(LOGERROR, "Unknown setting type of '{}' for {}", id, Name());
+ if (instanceId == ADDON_SETTINGS_ID)
+ {
+ if (m_interface.toAddon->setting_change_string)
+ status = m_interface.toAddon->setting_change_string(
+ m_interface.addonBase, id, setting->ToString().c_str());
+ }
+ else if (instanceTarget && instanceHandle)
+ {
+ if (instanceTarget->instance_setting_change_string)
+ status = instanceTarget->instance_setting_change_string(
+ instanceHandle, id, setting->ToString().c_str());
+ }
+ break;
+ }
+ }
+
+ if (status == ADDON_STATUS_NEED_RESTART)
+ restart = true;
+ else if (status != ADDON_STATUS_OK)
+ reportStatus = status;
+ }
+ }
+ }
+ }
+ }
+
+ if (restart || reportStatus != ADDON_STATUS_OK)
+ {
+ new CAddonStatusHandler(ID(), instanceId, restart ? ADDON_STATUS_NEED_RESTART : reportStatus,
+ true);
+ }
+
+ return ADDON_STATUS_OK;
+}
+
+bool CAddonDll::CheckAPIVersion(int type)
+{
+ /* check the API version */
+ CAddonVersion kodiMinVersion(kodi::addon::GetTypeMinVersion(type));
+ CAddonVersion addonVersion(m_pDll->GetAddonTypeVersion(type));
+ CAddonVersion addonMinVersion = m_pDll->GetAddonTypeMinVersion_available()
+ ? CAddonVersion(m_pDll->GetAddonTypeMinVersion(type))
+ : addonVersion;
+
+ /* Check the global usage from addon
+ * if not used from addon, empty version is returned
+ */
+ if (type <= ADDON_GLOBAL_MAX && addonVersion.empty())
+ return true;
+
+ /* If a instance (not global) version becomes checked must be the version
+ * present.
+ */
+ if (kodiMinVersion > addonVersion ||
+ addonMinVersion > CAddonVersion(kodi::addon::GetTypeVersion(type)))
+ {
+ CLog::Log(LOGERROR, "Add-on '{}' is using an incompatible API version for type '{}'. Kodi API min version = '{}/{}', add-on API version '{}/{}'",
+ Name(),
+ kodi::addon::GetTypeName(type),
+ kodi::addon::GetTypeVersion(type),
+ kodiMinVersion.asString(),
+ addonMinVersion.asString(),
+ addonVersion.asString());
+
+ if (CServiceBroker::GetGUI())
+ {
+ CEventLog* eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->AddWithNotification(
+ EventPtr(new CNotificationEvent(Name(), 24152, EventLevel::Error)));
+ }
+
+ return false;
+ }
+
+ return true;
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/binary-addons/AddonDll.h b/xbmc/addons/binary-addons/AddonDll.h
new file mode 100644
index 0000000..5f17a04
--- /dev/null
+++ b/xbmc/addons/binary-addons/AddonDll.h
@@ -0,0 +1,163 @@
+/*
+ * 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 "addons/Addon.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon_base.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+class DllAddon;
+
+namespace ADDON
+{
+
+class CBinaryAddonBase;
+using BinaryAddonBasePtr = std::shared_ptr<CBinaryAddonBase>;
+
+/*!
+ * Addon instance handler, used as identify for std::map to find related
+ * addon instance. This class itself not accessed here.
+ *
+ * @todo As long game addon system use CAddonDll itself and not
+ * IAddonInstanceHandler as parent, is the set of this as "void*" needed.
+ * After game system is changed should by this also changed to
+ * "const IAddonInstanceHandler*" or direct in map below.
+ */
+using ADDON_INSTANCE_HANDLER = void*;
+
+/*!
+ * @brief Information class for use on addon type managers.
+ *
+ * This to query via @ref CAddonDll the manager so that work can be performed.
+ * If there are multiple instances it be harder to be informed about any changes.
+ */
+class CAddonDllInformer
+{
+public:
+ virtual ~CAddonDllInformer() = default;
+
+ virtual bool IsInUse(const std::string& id) = 0;
+};
+
+class CAddonDll : public CAddon
+{
+public:
+ CAddonDll(const AddonInfoPtr& addonInfo, BinaryAddonBasePtr addonBase);
+ CAddonDll(const AddonInfoPtr& addonInfo, AddonType addonType);
+ ~CAddonDll() override;
+
+ // Implementation of IAddon via CAddon
+ std::string LibPath() const override;
+
+ // addon settings
+ void SaveSettings(AddonInstanceId id = ADDON_SETTINGS_ID) override;
+
+ bool DllLoaded(void) const;
+
+ /*!
+ * @brief Get api version of moduleType type
+ *
+ * @return The version of requested type, if dll is loaded and supported by addon.
+ * If one of both do not match, an empty version is returned.
+ *
+ * @note This should only be called if the associated dll is loaded.
+ * Otherwise use @ref CAddonInfo::DependencyVersion(...)
+ */
+ CAddonVersion GetTypeVersionDll(int type) const;
+
+ /*!
+ * @brief Get api min version of moduleType type
+ *
+ * @return The version of requested type, if dll is loaded and supported by addon.
+ * If one of both do not match, an empty version is returned.
+ *
+ * @note This should only be called if the associated dll is loaded.
+ * Otherwise use @ref CAddonInfo::DependencyMinVersion(...)
+ */
+ CAddonVersion GetTypeMinVersionDll(int type) const;
+
+ /*!
+ * @brief Function to create a addon instance class
+ *
+ * @param[in,out] instance The for addon used data structure for active instance
+ * @return The status of addon after the creation.
+ */
+ ADDON_STATUS CreateInstance(KODI_ADDON_INSTANCE_STRUCT* instance);
+
+ /*!
+ * @brief Function to destroy a on addon created instance class
+ *
+ * @param[in] instance The for addon used data structure for active instance
+ */
+ void DestroyInstance(KODI_ADDON_INSTANCE_STRUCT* instance);
+
+ bool IsInUse() const override;
+ void RegisterInformer(CAddonDllInformer* informer);
+ AddonPtr GetRunningInstance() const override;
+
+ void OnPreInstall() override;
+ void OnPostInstall(bool update, bool modal) override;
+ void OnPreUnInstall() override;
+ void OnPostUnInstall() override;
+
+ bool Initialized() const { return m_initialized; }
+
+protected:
+ static std::string GetDllPath(const std::string& strFileName);
+
+ std::string m_parentLib;
+
+private:
+ /*!
+ * @brief Main addon creation call function
+ *
+ * This becomes called only one time before a addon instance becomes created.
+ * If another instance becomes requested is this Create no more used. To see
+ * like a "int main()" on exe.
+ *
+ * @param[in] firstKodiInstance The first instance who becomes used.
+ * In case addon supports only one instance
+ * and not multiple together can addon use
+ * only one complete class for everything.
+ * This is used then to interact on interface.
+ * @return The status of addon after the creation.
+ */
+ ADDON_STATUS Create(KODI_ADDON_INSTANCE_STRUCT* firstKodiInstance);
+
+ /*!
+ * @brief Main addon destroying call function
+ *
+ * This becomes called only one time after the last addon instance becomes destroyed.
+ */
+ void Destroy();
+
+ bool CheckAPIVersion(int type);
+
+ BinaryAddonBasePtr m_binaryAddonBase = nullptr;
+ DllAddon* m_pDll = nullptr;
+ bool m_initialized = false;
+ bool LoadDll();
+ std::map<ADDON_INSTANCE_HANDLER, KODI_ADDON_INSTANCE_STRUCT*> m_usedInstances;
+ CAddonDllInformer* m_informer = nullptr;
+
+ virtual ADDON_STATUS TransferSettings(AddonInstanceId instanceId);
+
+ /*!
+ * This structure, which is fixed to the addon headers, makes use of the at
+ * least supposed parts for the interface.
+ * This structure is defined in:
+ * /xbmc/addons/kodi-dev-kit/include/kodi/AddonBase.h
+ */
+ AddonGlobalInterface m_interface = {};
+};
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/binary-addons/AddonInstanceHandler.cpp b/xbmc/addons/binary-addons/AddonInstanceHandler.cpp
new file mode 100644
index 0000000..34c303b
--- /dev/null
+++ b/xbmc/addons/binary-addons/AddonInstanceHandler.cpp
@@ -0,0 +1,386 @@
+/*
+ * 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 "AddonInstanceHandler.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonVersion.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/binary-addons/BinaryAddonManager.h"
+#include "addons/interfaces/AddonBase.h"
+#include "addons/kodi-dev-kit/include/kodi/AddonBase.h"
+#include "addons/settings/AddonSettings.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+namespace ADDON
+{
+
+CCriticalSection IAddonInstanceHandler::m_cdSec;
+
+IAddonInstanceHandler::IAddonInstanceHandler(
+ ADDON_TYPE type,
+ const AddonInfoPtr& addonInfo,
+ AddonInstanceId instanceId /* = ADDON_INSTANCE_ID_UNUSED */,
+ KODI_HANDLE parentInstance /* = nullptr*/,
+ const std::string& uniqueWorkID /* = ""*/)
+ : m_type(type), m_instanceId(instanceId), m_parentInstance(parentInstance), m_addonInfo(addonInfo)
+{
+ // if no special instance ID is given generate one from class pointer (is
+ // faster as unique id and also safe enough for them).
+ m_uniqueWorkID = !uniqueWorkID.empty() ? uniqueWorkID : StringUtils::Format("{}", fmt::ptr(this));
+ m_addonBase = CServiceBroker::GetBinaryAddonManager().GetAddonBase(addonInfo, this, m_addon);
+
+ KODI_ADDON_INSTANCE_INFO* info = new KODI_ADDON_INSTANCE_INFO();
+ info->number = m_instanceId; // @todo change within next big API change to "instance_id"
+ info->id = m_uniqueWorkID.c_str(); // @todo change within next big API change to "unique_work_id"
+ info->version = kodi::addon::GetTypeVersion(m_type);
+ info->type = m_type;
+ info->kodi = this;
+ info->parent = m_parentInstance;
+ info->first_instance = m_addon && !m_addon->Initialized();
+ info->functions = new KODI_ADDON_INSTANCE_FUNC_CB();
+ info->functions->get_instance_user_path = get_instance_user_path;
+ info->functions->is_instance_setting_using_default = is_instance_setting_using_default;
+ info->functions->get_instance_setting_bool = get_instance_setting_bool;
+ info->functions->get_instance_setting_int = get_instance_setting_int;
+ info->functions->get_instance_setting_float = get_instance_setting_float;
+ info->functions->get_instance_setting_string = get_instance_setting_string;
+ info->functions->set_instance_setting_bool = set_instance_setting_bool;
+ info->functions->set_instance_setting_int = set_instance_setting_int;
+ info->functions->set_instance_setting_float = set_instance_setting_float;
+ info->functions->set_instance_setting_string = set_instance_setting_string;
+ m_ifc.info = info;
+ m_ifc.functions = new KODI_ADDON_INSTANCE_FUNC();
+}
+
+IAddonInstanceHandler::~IAddonInstanceHandler()
+{
+ CServiceBroker::GetBinaryAddonManager().ReleaseAddonBase(m_addonBase, this);
+
+ delete m_ifc.functions;
+ if (m_ifc.info)
+ delete m_ifc.info->functions;
+ delete m_ifc.info;
+}
+
+std::string IAddonInstanceHandler::ID() const
+{
+ return m_addon ? m_addon->ID() : "";
+}
+
+std::string IAddonInstanceHandler::Name() const
+{
+ return m_addon ? m_addon->Name() : "";
+}
+
+std::string IAddonInstanceHandler::Author() const
+{
+ return m_addon ? m_addon->Author() : "";
+}
+
+std::string IAddonInstanceHandler::Icon() const
+{
+ return m_addon ? m_addon->Icon() : "";
+}
+
+std::string IAddonInstanceHandler::Path() const
+{
+ return m_addon ? m_addon->Path() : "";
+}
+
+std::string IAddonInstanceHandler::Profile() const
+{
+ return m_addon ? m_addon->Profile() : "";
+}
+
+CAddonVersion IAddonInstanceHandler::Version() const
+{
+ return m_addon ? m_addon->Version() : CAddonVersion();
+}
+
+ADDON_STATUS IAddonInstanceHandler::CreateInstance()
+{
+ if (!m_addon)
+ return ADDON_STATUS_UNKNOWN;
+
+ std::unique_lock<CCriticalSection> lock(m_cdSec);
+
+ ADDON_STATUS status = m_addon->CreateInstance(&m_ifc);
+ if (status != ADDON_STATUS_OK)
+ {
+ CLog::Log(LOGERROR,
+ "IAddonInstanceHandler::{}: {} returned bad status \"{}\" during instance creation",
+ __func__, m_addon->ID(), kodi::addon::TranslateAddonStatus(status));
+ }
+ return status;
+}
+
+void IAddonInstanceHandler::DestroyInstance()
+{
+ std::unique_lock<CCriticalSection> lock(m_cdSec);
+ if (m_addon)
+ m_addon->DestroyInstance(&m_ifc);
+}
+
+std::shared_ptr<CSetting> IAddonInstanceHandler::GetSetting(const std::string& setting)
+{
+ if (!m_addon->HasSettings(m_instanceId))
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - couldn't get settings for add-on '{}'",
+ __func__, Name());
+ return nullptr;
+ }
+
+ auto value = m_addon->GetSettings(m_instanceId)->GetSetting(setting);
+ if (value == nullptr)
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - can't find setting '{}' in '{}'", __func__,
+ setting, Name());
+ return nullptr;
+ }
+
+ return value;
+}
+
+char* IAddonInstanceHandler::get_instance_user_path(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl)
+{
+ IAddonInstanceHandler* instance = static_cast<IAddonInstanceHandler*>(hdl);
+ if (!instance)
+ return nullptr;
+
+ const std::string path = CSpecialProtocol::TranslatePath(instance->m_addon->Profile());
+
+ XFILE::CDirectory::Create(path);
+ return strdup(path.c_str());
+}
+
+bool IAddonInstanceHandler::is_instance_setting_using_default(
+ const KODI_ADDON_INSTANCE_BACKEND_HDL hdl, const char* id)
+{
+ IAddonInstanceHandler* instance = static_cast<IAddonInstanceHandler*>(hdl);
+ if (!instance || !id)
+ return false;
+
+ auto setting = instance->GetSetting(id);
+ if (setting == nullptr)
+ return false;
+
+ return setting->IsDefault();
+}
+
+bool IAddonInstanceHandler::get_instance_setting_bool(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ bool* value)
+{
+ IAddonInstanceHandler* instance = static_cast<IAddonInstanceHandler*>(hdl);
+ if (!instance || !id || !value)
+ return false;
+
+ auto setting = instance->GetSetting(id);
+ if (setting == nullptr)
+ return false;
+
+ if (setting->GetType() != SettingType::Boolean)
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - setting '{}' is not a boolean in '{}'",
+ __func__, id, instance->Name());
+ return false;
+ }
+
+ *value = std::static_pointer_cast<CSettingBool>(setting)->GetValue();
+ return true;
+}
+
+bool IAddonInstanceHandler::get_instance_setting_int(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ int* value)
+{
+ IAddonInstanceHandler* instance = static_cast<IAddonInstanceHandler*>(hdl);
+ if (!instance || !id || !value)
+ return false;
+
+ auto setting = instance->GetSetting(id);
+ if (setting == nullptr)
+ return false;
+
+ if (setting->GetType() != SettingType::Integer && setting->GetType() != SettingType::Number)
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - setting '{}' is not a integer in '{}'",
+ __func__, id, instance->Name());
+ return false;
+ }
+
+ if (setting->GetType() == SettingType::Integer)
+ *value = std::static_pointer_cast<CSettingInt>(setting)->GetValue();
+ else
+ *value = static_cast<int>(std::static_pointer_cast<CSettingNumber>(setting)->GetValue());
+ return true;
+}
+
+bool IAddonInstanceHandler::get_instance_setting_float(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ float* value)
+{
+ IAddonInstanceHandler* instance = static_cast<IAddonInstanceHandler*>(hdl);
+ if (!instance || !id || !value)
+ return false;
+
+ auto setting = instance->GetSetting(id);
+ if (setting == nullptr)
+ return false;
+
+ if (setting->GetType() != SettingType::Number)
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - setting '{}' is not a number in '{}'",
+ __func__, id, instance->Name());
+ return false;
+ }
+
+ *value = static_cast<float>(std::static_pointer_cast<CSettingNumber>(setting)->GetValue());
+ return true;
+}
+
+bool IAddonInstanceHandler::get_instance_setting_string(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ char** value)
+{
+ IAddonInstanceHandler* instance = static_cast<IAddonInstanceHandler*>(hdl);
+ if (!instance || !id || !value)
+ return false;
+
+ auto setting = instance->GetSetting(id);
+ if (setting == nullptr)
+ return false;
+
+ if (setting->GetType() != SettingType::String)
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - setting '{}' is not a string in '{}'",
+ __func__, id, instance->Name());
+ return false;
+ }
+
+ *value = strdup(std::static_pointer_cast<CSettingString>(setting)->GetValue().c_str());
+ return true;
+}
+
+bool IAddonInstanceHandler::set_instance_setting_bool(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ bool value)
+{
+ IAddonInstanceHandler* instance = static_cast<IAddonInstanceHandler*>(hdl);
+ if (!instance || !id)
+ return false;
+
+ if (Interface_Base::UpdateSettingInActiveDialog(instance->m_addon.get(), instance->m_instanceId,
+ id, value ? "true" : "false"))
+ return true;
+
+ if (!instance->m_addon->UpdateSettingBool(id, value, instance->m_instanceId))
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - invalid setting type", __func__);
+ return false;
+ }
+
+ instance->m_addon->SaveSettings(instance->m_instanceId);
+
+ return true;
+}
+
+bool IAddonInstanceHandler::set_instance_setting_int(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ int value)
+{
+ IAddonInstanceHandler* instance = static_cast<IAddonInstanceHandler*>(hdl);
+ if (!instance || !id)
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - invalid data (instance='{}', id='{}')",
+ __func__, hdl, static_cast<const void*>(id));
+
+ return false;
+ }
+
+ if (Interface_Base::UpdateSettingInActiveDialog(instance->m_addon.get(), instance->m_instanceId,
+ id, std::to_string(value)))
+ return true;
+
+ if (!instance->m_addon->UpdateSettingInt(id, value, instance->m_instanceId))
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - invalid setting type", __func__);
+ return false;
+ }
+
+ instance->m_addon->SaveSettings(instance->m_instanceId);
+
+ return true;
+}
+
+bool IAddonInstanceHandler::set_instance_setting_float(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ float value)
+{
+ IAddonInstanceHandler* instance = static_cast<IAddonInstanceHandler*>(hdl);
+ if (!instance || !id)
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - invalid data (instance='{}', id='{}')",
+ __func__, hdl, static_cast<const void*>(id));
+
+ return false;
+ }
+
+ if (Interface_Base::UpdateSettingInActiveDialog(instance->m_addon.get(), instance->m_instanceId,
+ id, StringUtils::Format("{:f}", value)))
+ return true;
+
+ if (!instance->m_addon->UpdateSettingNumber(id, static_cast<double>(value),
+ instance->m_instanceId))
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - invalid setting type", __func__);
+ return false;
+ }
+
+ instance->m_addon->SaveSettings(instance->m_instanceId);
+
+ return true;
+}
+
+bool IAddonInstanceHandler::set_instance_setting_string(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ const char* value)
+{
+ IAddonInstanceHandler* instance = static_cast<IAddonInstanceHandler*>(hdl);
+ if (!instance || !id || !value)
+ {
+ CLog::Log(LOGERROR,
+ "IAddonInstanceHandler::{} - invalid data (instance='{}', id='{}', value='{}')",
+ __func__, hdl, static_cast<const void*>(id), static_cast<const void*>(value));
+
+ return false;
+ }
+
+ if (Interface_Base::UpdateSettingInActiveDialog(instance->m_addon.get(), instance->m_instanceId,
+ id, value))
+ return true;
+
+ if (!instance->m_addon->UpdateSettingString(id, value, instance->m_instanceId))
+ {
+ CLog::Log(LOGERROR, "IAddonInstanceHandler::{} - invalid setting type", __func__);
+ return false;
+ }
+
+ instance->m_addon->SaveSettings(instance->m_instanceId);
+
+ return true;
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/binary-addons/AddonInstanceHandler.h b/xbmc/addons/binary-addons/AddonInstanceHandler.h
new file mode 100644
index 0000000..27ba95b
--- /dev/null
+++ b/xbmc/addons/binary-addons/AddonInstanceHandler.h
@@ -0,0 +1,135 @@
+/*
+ * 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 "addons/IAddon.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon_base.h"
+#include "addons/kodi-dev-kit/include/kodi/versions.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <string>
+
+class CSetting;
+
+namespace ADDON
+{
+
+class CAddonDll;
+using AddonDllPtr = std::shared_ptr<CAddonDll>;
+
+class CAddonInfo;
+using AddonInfoPtr = std::shared_ptr<CAddonInfo>;
+
+class CBinaryAddonBase;
+using BinaryAddonBasePtr = std::shared_ptr<CBinaryAddonBase>;
+
+class IAddonInstanceHandler
+{
+public:
+ /**
+ * @brief Class constructor for handling add-on instance processes, allowing
+ * an add-on to handle multiple work simultaneously and independently.
+ *
+ * @param[in] type The associated add-on type which is processed in the running
+ * instance.
+ * @param[in] addonInfo Class for querying available add-on information (e.g.
+ * content declared in addon.xml).
+ * @param[in] parentInstance *[opt]* Above running add-on instance which starts this
+ * instance. Used to have the associated class for
+ * work to open in the add-on.\n\n
+ * **Currently used values:**
+ * | Parent | Target | Description
+ * |--------|--------|-------------
+ * | @ref kodi::addon::CInstanceInputStream | @ref kodi::addon::CInstanceVideoCodec | In order to be able to access the overlying input stream instance in the video codec created by Kodi on the add-on.
+ * @param[in] uniqueWorkID *[opt]* Identification value intended to pass any special
+ * values to the instance to be opened.
+ * If not used, the IAddonInstanceHandler class pointer
+ * is used as a string.\n\n
+ * **Currently used values:**
+ * | Add-on instance type | Description
+ * |----------------------|---------------------
+ * | @ref kodi::addon::CInstanceInputStream "Inputstream" | To transfer special values to inputstream using the property @ref STREAM_PROPERTY_INPUTSTREAM_INSTANCE_ID from external space, for example PVR add-on which also supports inputstream can exchange special values with it, e.g. select the necessary add-on processing class, since it is not known at the start what is being executed ( live TV, radio, recordings...) and add-on may use different classes.
+ * | All other | The used class pointer of Kodi's @ref IAddonInstanceHandler is used as a value to have an individually different value.
+ */
+ IAddonInstanceHandler(ADDON_TYPE type,
+ const AddonInfoPtr& addonInfo,
+ AddonInstanceId instanceId = ADDON_INSTANCE_ID_UNUSED,
+ KODI_HANDLE parentInstance = nullptr,
+ const std::string& uniqueWorkID = "");
+ virtual ~IAddonInstanceHandler();
+
+ ADDON_TYPE UsedType() const { return m_type; }
+ AddonInstanceId InstanceId() const { return m_instanceId; }
+ const std::string& UniqueWorkID() { return m_uniqueWorkID; }
+
+ std::string ID() const;
+ std::string Name() const;
+ std::string Author() const;
+ std::string Icon() const;
+ std::string Path() const;
+ std::string Profile() const;
+ CAddonVersion Version() const;
+
+ ADDON_STATUS CreateInstance();
+ void DestroyInstance();
+ const AddonDllPtr& Addon() const { return m_addon; }
+ AddonInfoPtr GetAddonInfo() const { return m_addonInfo; }
+
+ virtual void OnPreInstall() {}
+ virtual void OnPostInstall(bool update, bool modal) {}
+ virtual void OnPreUnInstall() {}
+ virtual void OnPostUnInstall() {}
+
+protected:
+ KODI_ADDON_INSTANCE_INFO m_info{};
+ KODI_ADDON_INSTANCE_STRUCT m_ifc{};
+
+private:
+ std::shared_ptr<CSetting> GetSetting(const std::string& setting);
+
+ static char* get_instance_user_path(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl);
+ static bool is_instance_setting_using_default(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id);
+ static bool get_instance_setting_bool(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ bool* value);
+ static bool get_instance_setting_int(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ int* value);
+ static bool get_instance_setting_float(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ float* value);
+ static bool get_instance_setting_string(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ char** value);
+ static bool set_instance_setting_bool(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ bool value);
+ static bool set_instance_setting_int(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ int value);
+ static bool set_instance_setting_float(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ float value);
+ static bool set_instance_setting_string(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ const char* value);
+
+ const ADDON_TYPE m_type;
+ const AddonInstanceId m_instanceId;
+ std::string m_uniqueWorkID;
+ KODI_HANDLE m_parentInstance;
+ AddonInfoPtr m_addonInfo;
+ BinaryAddonBasePtr m_addonBase;
+ AddonDllPtr m_addon;
+ static CCriticalSection m_cdSec;
+};
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/binary-addons/BinaryAddonBase.cpp b/xbmc/addons/binary-addons/BinaryAddonBase.cpp
new file mode 100644
index 0000000..cea1586
--- /dev/null
+++ b/xbmc/addons/binary-addons/BinaryAddonBase.cpp
@@ -0,0 +1,108 @@
+/*
+ * 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 "BinaryAddonBase.h"
+
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/binary-addons/AddonInstanceHandler.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace ADDON;
+
+const std::string& CBinaryAddonBase::ID() const
+{
+ return m_addonInfo->ID();
+}
+
+AddonDllPtr CBinaryAddonBase::GetAddon(IAddonInstanceHandler* handler)
+{
+ if (handler == nullptr)
+ {
+ CLog::Log(LOGERROR, "CBinaryAddonBase::{}: for Id '{}' called with empty instance handler",
+ __FUNCTION__, ID());
+ return nullptr;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // If no 'm_activeAddon' is defined create it new.
+ if (m_activeAddon == nullptr)
+ m_activeAddon = std::make_shared<CAddonDll>(m_addonInfo, shared_from_this());
+
+ // add the instance handler to the info to know used amount on addon
+ m_activeAddonHandlers.insert(handler);
+
+ return m_activeAddon;
+}
+
+void CBinaryAddonBase::ReleaseAddon(IAddonInstanceHandler* handler)
+{
+ if (handler == nullptr)
+ {
+ CLog::Log(LOGERROR, "CBinaryAddonBase::{}: for Id '{}' called with empty instance handler",
+ __FUNCTION__, ID());
+ return;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ auto presentHandler = m_activeAddonHandlers.find(handler);
+ if (presentHandler == m_activeAddonHandlers.end())
+ return;
+
+ m_activeAddonHandlers.erase(presentHandler);
+
+ // if no handler is present anymore reset and delete the add-on class on information
+ if (m_activeAddonHandlers.empty())
+ {
+ m_activeAddon.reset();
+ }
+}
+
+size_t CBinaryAddonBase::UsedInstanceCount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_activeAddonHandlers.size();
+}
+
+AddonDllPtr CBinaryAddonBase::GetActiveAddon()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_activeAddon;
+}
+
+void CBinaryAddonBase::OnPreInstall()
+{
+ const std::unordered_set<IAddonInstanceHandler*> activeAddonHandlers = m_activeAddonHandlers;
+ for (const auto& instance : activeAddonHandlers)
+ instance->OnPreInstall();
+}
+
+void CBinaryAddonBase::OnPostInstall(bool update, bool modal)
+{
+ const std::unordered_set<IAddonInstanceHandler*> activeAddonHandlers = m_activeAddonHandlers;
+ for (const auto& instance : activeAddonHandlers)
+ instance->OnPostInstall(update, modal);
+}
+
+void CBinaryAddonBase::OnPreUnInstall()
+{
+ const std::unordered_set<IAddonInstanceHandler*> activeAddonHandlers = m_activeAddonHandlers;
+ for (const auto& instance : activeAddonHandlers)
+ instance->OnPreUnInstall();
+}
+
+void CBinaryAddonBase::OnPostUnInstall()
+{
+ const std::unordered_set<IAddonInstanceHandler*> activeAddonHandlers = m_activeAddonHandlers;
+ for (const auto& instance : activeAddonHandlers)
+ instance->OnPostUnInstall();
+}
diff --git a/xbmc/addons/binary-addons/BinaryAddonBase.h b/xbmc/addons/binary-addons/BinaryAddonBase.h
new file mode 100644
index 0000000..789b35a
--- /dev/null
+++ b/xbmc/addons/binary-addons/BinaryAddonBase.h
@@ -0,0 +1,54 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+#include <memory>
+#include <string>
+#include <unordered_set>
+
+namespace ADDON
+{
+
+ class IAddonInstanceHandler;
+
+ class CAddonInfo;
+ using AddonInfoPtr = std::shared_ptr<CAddonInfo>;
+
+ class CAddonDll;
+ typedef std::shared_ptr<CAddonDll> AddonDllPtr;
+
+ class CBinaryAddonBase : public std::enable_shared_from_this<CBinaryAddonBase>
+ {
+ public:
+ explicit CBinaryAddonBase(const AddonInfoPtr& addonInfo) : m_addonInfo(addonInfo) { }
+
+ const std::string& ID() const;
+
+ AddonDllPtr GetAddon(IAddonInstanceHandler* handler);
+ void ReleaseAddon(IAddonInstanceHandler* handler);
+ size_t UsedInstanceCount() const;
+
+ AddonDllPtr GetActiveAddon();
+
+ void OnPreInstall();
+ void OnPostInstall(bool update, bool modal);
+ void OnPreUnInstall();
+ void OnPostUnInstall();
+
+ private:
+ AddonInfoPtr m_addonInfo;
+
+ mutable CCriticalSection m_critSection;
+ AddonDllPtr m_activeAddon;
+ std::unordered_set<IAddonInstanceHandler*> m_activeAddonHandlers;
+ };
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/binary-addons/BinaryAddonManager.cpp b/xbmc/addons/binary-addons/BinaryAddonManager.cpp
new file mode 100644
index 0000000..699fc09
--- /dev/null
+++ b/xbmc/addons/binary-addons/BinaryAddonManager.cpp
@@ -0,0 +1,88 @@
+/*
+ * 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 "BinaryAddonManager.h"
+
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/binary-addons/BinaryAddonBase.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace ADDON;
+
+BinaryAddonBasePtr CBinaryAddonManager::GetAddonBase(const AddonInfoPtr& addonInfo,
+ IAddonInstanceHandler* handler,
+ AddonDllPtr& addon)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ BinaryAddonBasePtr addonBase;
+
+ const auto& addonInstances = m_runningAddons.find(addonInfo->ID());
+ if (addonInstances != m_runningAddons.end())
+ {
+ addonBase = addonInstances->second;
+ }
+ else
+ {
+ addonBase = std::make_shared<CBinaryAddonBase>(addonInfo);
+
+ m_runningAddons.emplace(addonInfo->ID(), addonBase);
+ }
+
+ if (addonBase)
+ {
+ addon = addonBase->GetAddon(handler);
+ }
+ if (!addon)
+ {
+ CLog::Log(LOGFATAL, "CBinaryAddonManager::{}: Tried to get add-on '{}' who not available!",
+ __func__, addonInfo->ID());
+ }
+
+ return addonBase;
+}
+
+void CBinaryAddonManager::ReleaseAddonBase(const BinaryAddonBasePtr& addonBase,
+ IAddonInstanceHandler* handler)
+{
+ const auto& addon = m_runningAddons.find(addonBase->ID());
+ if (addon == m_runningAddons.end())
+ return;
+
+ addonBase->ReleaseAddon(handler);
+
+ if (addonBase->UsedInstanceCount() > 0)
+ return;
+
+ m_runningAddons.erase(addon);
+}
+
+BinaryAddonBasePtr CBinaryAddonManager::GetRunningAddonBase(const std::string& addonId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const auto& addonInstances = m_runningAddons.find(addonId);
+ if (addonInstances != m_runningAddons.end())
+ return addonInstances->second;
+
+ return nullptr;
+}
+
+AddonPtr CBinaryAddonManager::GetRunningAddon(const std::string& addonId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const BinaryAddonBasePtr base = GetRunningAddonBase(addonId);
+ if (base)
+ return base->GetActiveAddon();
+
+ return nullptr;
+}
diff --git a/xbmc/addons/binary-addons/BinaryAddonManager.h b/xbmc/addons/binary-addons/BinaryAddonManager.h
new file mode 100644
index 0000000..d7f4a9f
--- /dev/null
+++ b/xbmc/addons/binary-addons/BinaryAddonManager.h
@@ -0,0 +1,95 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+
+namespace ADDON
+{
+
+ class IAddonInstanceHandler;
+
+ class IAddon;
+ using AddonPtr = std::shared_ptr<IAddon>;
+
+ class CAddonInfo;
+ using AddonInfoPtr = std::shared_ptr<CAddonInfo>;
+
+ class CAddonDll;
+ typedef std::shared_ptr<CAddonDll> AddonDllPtr;
+
+ class CBinaryAddonBase;
+ typedef std::shared_ptr<CBinaryAddonBase> BinaryAddonBasePtr;
+
+ class CBinaryAddonManager
+ {
+ public:
+ CBinaryAddonManager() = default;
+ CBinaryAddonManager(const CBinaryAddonManager&) = delete;
+ ~CBinaryAddonManager() = default;
+
+ /*!
+ * @brief Create or get available addon instance handle base.
+ *
+ * On first call the binary addon base class becomes created, on every next
+ * call of addon id, this becomes given again and a counter about in
+ * @ref CBinaryAddonBase increased.
+ *
+ * @param[in] addonBase related addon base to release
+ * @param[in] handler related instance handle class
+ *
+ * @warning This and @ref ReleaseAddonBase are only be called from
+ * @ref IAddonInstanceHandler, use nowhere else allowed!
+ *
+ */
+ BinaryAddonBasePtr GetAddonBase(const AddonInfoPtr& addonInfo,
+ IAddonInstanceHandler* handler,
+ AddonDllPtr& addon);
+
+ /*!
+ * @brief Release a running addon instance handle base.
+ *
+ * On last release call the here on map stored entry becomes
+ * removed and the dll unloaded.
+ *
+ * @param[in] addonBase related addon base to release
+ * @param[in] handler related instance handle class
+ *
+ */
+ void ReleaseAddonBase(const BinaryAddonBasePtr& addonBase, IAddonInstanceHandler* handler);
+
+ /*!
+ * @brief Get running addon base class for a given addon id.
+ *
+ * @param[in] addonId the addon id
+ * @return running addon base class if found, nullptr otherwise.
+ *
+ */
+ BinaryAddonBasePtr GetRunningAddonBase(const std::string& addonId) const;
+
+ /*!
+ * @brief Used from other addon manager to get active addon over a from him
+ * created CAddonDll.
+ *
+ * @param[in] addonId related addon id string
+ * @return if present the pointer to active one or nullptr if not present
+ *
+ */
+ AddonPtr GetRunningAddon(const std::string& addonId) const;
+
+ private:
+ mutable CCriticalSection m_critSection;
+
+ std::map<std::string, BinaryAddonBasePtr> m_runningAddons;
+ };
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/binary-addons/CMakeLists.txt b/xbmc/addons/binary-addons/CMakeLists.txt
new file mode 100644
index 0000000..33a72ce
--- /dev/null
+++ b/xbmc/addons/binary-addons/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(SOURCES BinaryAddonManager.cpp
+ AddonDll.cpp
+ AddonInstanceHandler.cpp
+ BinaryAddonBase.cpp)
+
+set(HEADERS BinaryAddonManager.h
+ AddonDll.h
+ AddonInstanceHandler.h
+ BinaryAddonBase.h
+ DllAddon.h)
+
+core_add_library(addons_binary-addons)
diff --git a/xbmc/addons/binary-addons/DllAddon.h b/xbmc/addons/binary-addons/DllAddon.h
new file mode 100644
index 0000000..c0399f7
--- /dev/null
+++ b/xbmc/addons/binary-addons/DllAddon.h
@@ -0,0 +1,35 @@
+/*
+ * 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 "DynamicDll.h"
+
+class DllAddonInterface
+{
+public:
+ virtual ~DllAddonInterface() = default;
+ virtual ADDON_STATUS Create(void* cb) = 0;
+ virtual const char* GetAddonTypeVersion(int type)=0;
+ virtual const char* GetAddonTypeMinVersion(int type) = 0;
+};
+
+class DllAddon : public DllDynamic, public DllAddonInterface
+{
+public:
+ DECLARE_DLL_WRAPPER_TEMPLATE(DllAddon)
+ DEFINE_METHOD1(ADDON_STATUS, Create, (void* p1))
+ DEFINE_METHOD1(const char*, GetAddonTypeVersion, (int p1))
+ DEFINE_METHOD1(const char*, GetAddonTypeMinVersion, (int p1))
+ bool GetAddonTypeMinVersion_available() { return m_GetAddonTypeMinVersion != nullptr; }
+ BEGIN_METHOD_RESOLVE()
+ RESOLVE_METHOD_RENAME(ADDON_Create, Create)
+ RESOLVE_METHOD_RENAME(ADDON_GetTypeVersion, GetAddonTypeVersion)
+ RESOLVE_METHOD_RENAME_OPTIONAL(ADDON_GetTypeMinVersion, GetAddonTypeMinVersion)
+ END_METHOD_RESOLVE()
+};
diff --git a/xbmc/addons/gui/CMakeLists.txt b/xbmc/addons/gui/CMakeLists.txt
new file mode 100644
index 0000000..2a9c463
--- /dev/null
+++ b/xbmc/addons/gui/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES GUIDialogAddonInfo.cpp
+ GUIDialogAddonSettings.cpp
+ GUIHelpers.cpp
+ GUIViewStateAddonBrowser.cpp
+ GUIWindowAddonBrowser.cpp)
+
+set(HEADERS GUIDialogAddonInfo.h
+ GUIDialogAddonSettings.h
+ GUIHelpers.h
+ GUIViewStateAddonBrowser.h
+ GUIWindowAddonBrowser.h)
+
+core_add_library(addons_gui)
diff --git a/xbmc/addons/gui/GUIDialogAddonInfo.cpp b/xbmc/addons/gui/GUIDialogAddonInfo.cpp
new file mode 100644
index 0000000..3acdbf5
--- /dev/null
+++ b/xbmc/addons/gui/GUIDialogAddonInfo.cpp
@@ -0,0 +1,940 @@
+/*
+ * 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 "GUIDialogAddonInfo.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "addons/AddonDatabase.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonRepos.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/AudioDecoder.h"
+#include "addons/ExtsMimeSupportList.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "addons/gui/GUIHelpers.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/Directory.h"
+#include "games/GameUtils.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/builtins/Builtins.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pictures/GUIWindowSlideShow.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <functional>
+#include <sstream>
+#include <utility>
+
+#define CONTROL_BTN_INSTALL 6
+#define CONTROL_BTN_ENABLE 7
+#define CONTROL_BTN_UPDATE 8
+#define CONTROL_BTN_SETTINGS 9
+#define CONTROL_BTN_DEPENDENCIES 10
+#define CONTROL_BTN_SELECT 12
+#define CONTROL_BTN_AUTOUPDATE 13
+#define CONTROL_BTN_VERSIONS 14
+#define CONTROL_LIST_SCREENSHOTS 50
+
+using namespace KODI;
+using namespace ADDON;
+using namespace KODI::ADDONS;
+using namespace XFILE;
+using namespace KODI::MESSAGING;
+
+CGUIDialogAddonInfo::CGUIDialogAddonInfo(void)
+ : CGUIDialog(WINDOW_DIALOG_ADDON_INFO, "DialogAddonInfo.xml")
+{
+ m_item = CFileItemPtr(new CFileItem);
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogAddonInfo::~CGUIDialogAddonInfo(void) = default;
+
+bool CGUIDialogAddonInfo::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTN_UPDATE)
+ {
+ OnUpdate();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_INSTALL)
+ {
+ const auto& itemAddonInfo = m_item->GetAddonInfo();
+ if (!CServiceBroker::GetAddonMgr().IsAddonInstalled(
+ itemAddonInfo->ID(), itemAddonInfo->Origin(), itemAddonInfo->Version()))
+ {
+ OnInstall();
+ return true;
+ }
+ else
+ {
+ m_silentUninstall = false;
+ OnUninstall();
+ return true;
+ }
+ }
+ else if (iControl == CONTROL_BTN_SELECT)
+ {
+ OnSelect();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_ENABLE)
+ {
+ OnEnableDisable();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_SETTINGS)
+ {
+ OnSettings();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_DEPENDENCIES)
+ {
+ ShowDependencyList(Reactivate::CHOICE_YES, EntryPoint::SHOW_DEPENDENCIES);
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_AUTOUPDATE)
+ {
+ OnToggleAutoUpdates();
+ return true;
+ }
+ else if (iControl == CONTROL_LIST_SCREENSHOTS)
+ {
+ if (message.GetParam1() == ACTION_SELECT_ITEM ||
+ message.GetParam1() == ACTION_MOUSE_LEFT_CLICK)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl);
+ OnMessage(msg);
+ int start = msg.GetParam1();
+ if (start >= 0 && start < static_cast<int>(m_item->GetAddonInfo()->Screenshots().size()))
+ CGUIWindowSlideShow::RunSlideShow(m_item->GetAddonInfo()->Screenshots(), start);
+ }
+ }
+ else if (iControl == CONTROL_BTN_VERSIONS)
+ {
+ OnSelectVersion();
+ return true;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogAddonInfo::OnAction(const CAction& action)
+{
+ if (action.GetID() == ACTION_SHOW_INFO)
+ {
+ Close();
+ return true;
+ }
+ return CGUIDialog::OnAction(action);
+}
+
+void CGUIDialogAddonInfo::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+ BuildDependencyList();
+ UpdateControls(PerformButtonFocus::CHOICE_YES);
+}
+
+void CGUIDialogAddonInfo::UpdateControls(PerformButtonFocus performButtonFocus)
+{
+ if (!m_item)
+ return;
+
+ const auto& itemAddonInfo = m_item->GetAddonInfo();
+ bool isInstalled = CServiceBroker::GetAddonMgr().IsAddonInstalled(
+ itemAddonInfo->ID(), itemAddonInfo->Origin(), itemAddonInfo->Version());
+ m_addonEnabled =
+ m_localAddon && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_localAddon->ID());
+ bool canDisable =
+ isInstalled && CServiceBroker::GetAddonMgr().CanAddonBeDisabled(m_localAddon->ID());
+ bool canInstall = !isInstalled && itemAddonInfo->LifecycleState() != AddonLifecycleState::BROKEN;
+ bool canUninstall = m_localAddon && CServiceBroker::GetAddonMgr().CanUninstall(m_localAddon);
+
+ bool isUpdate = (!isInstalled && CServiceBroker::GetAddonMgr().IsAddonInstalled(
+ itemAddonInfo->ID(), itemAddonInfo->Origin()));
+
+ bool showUpdateButton = m_localAddon &&
+ CServiceBroker::GetAddonMgr().IsAutoUpdateable(m_localAddon->ID()) &&
+ m_item->GetProperty("Addon.HasUpdate").asBoolean();
+
+ if (isInstalled)
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_INSTALL, 24037); // uninstall
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_INSTALL, canUninstall);
+ }
+ else
+ {
+ if (isUpdate)
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_INSTALL, 24138); // update
+ }
+ else
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_INSTALL, 24038); // install
+ }
+
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_INSTALL, canInstall);
+ if (canInstall && performButtonFocus == PerformButtonFocus::CHOICE_YES)
+ {
+ SET_CONTROL_FOCUS(CONTROL_BTN_INSTALL, 0);
+ }
+ }
+
+ if (showUpdateButton)
+ {
+ SET_CONTROL_VISIBLE(CONTROL_BTN_UPDATE);
+ SET_CONTROL_HIDDEN(CONTROL_BTN_VERSIONS);
+ }
+ else
+ {
+ SET_CONTROL_VISIBLE(CONTROL_BTN_VERSIONS);
+ SET_CONTROL_HIDDEN(CONTROL_BTN_UPDATE);
+ }
+
+ if (m_addonEnabled)
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_ENABLE, 24021);
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_ENABLE, canDisable);
+ }
+ else
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_ENABLE, 24022);
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_ENABLE, isInstalled);
+ }
+
+ bool autoUpdatesOn = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_ADDONS_AUTOUPDATES) == AUTO_UPDATES_ON;
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_AUTOUPDATE, isInstalled && autoUpdatesOn);
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_AUTOUPDATE,
+ isInstalled && autoUpdatesOn &&
+ CServiceBroker::GetAddonMgr().IsAutoUpdateable(m_localAddon->ID()));
+ SET_CONTROL_LABEL(CONTROL_BTN_AUTOUPDATE, 21340);
+
+ const bool active = m_localAddon && CAddonSystemSettings::GetInstance().IsActive(*m_localAddon);
+ CONTROL_ENABLE_ON_CONDITION(
+ CONTROL_BTN_SELECT,
+ m_addonEnabled && (CanShowSupportList() || CanOpen() || CanRun() || (CanUse() && !active)));
+
+ int label;
+ if (CanShowSupportList())
+ label = 21484;
+ else if (CanUse())
+ label = 21480;
+ else if (CanOpen())
+ label = 21478;
+ else
+ label = 21479;
+ SET_CONTROL_LABEL(CONTROL_BTN_SELECT, label);
+
+ const bool hasSettings = m_localAddon && m_localAddon->CanHaveAddonOrInstanceSettings();
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_SETTINGS, isInstalled && hasSettings);
+ if (isInstalled && hasSettings && performButtonFocus == PerformButtonFocus::CHOICE_YES)
+ {
+ SET_CONTROL_FOCUS(CONTROL_BTN_SETTINGS, 0);
+ }
+
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_DEPENDENCIES, !m_depsInstalledWithAvailable.empty());
+
+ CFileItemList items;
+ for (const auto& screenshot : m_item->GetAddonInfo()->Screenshots())
+ {
+ auto item = std::make_shared<CFileItem>("");
+ item->SetArt("thumb", screenshot);
+ items.Add(std::move(item));
+ }
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST_SCREENSHOTS, 0, 0, &items);
+ OnMessage(msg);
+}
+
+static const std::string LOCAL_CACHE =
+ "\\0_local_cache"; // \0 to give it the lowest priority when sorting
+
+int CGUIDialogAddonInfo::AskForVersion(std::vector<std::pair<CAddonVersion, std::string>>& versions)
+{
+ auto dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ dialog->Reset();
+ dialog->SetHeading(CVariant{21338});
+ dialog->SetUseDetails(true);
+
+ for (const auto& versionInfo : versions)
+ {
+ CFileItem item(StringUtils::Format(g_localizeStrings.Get(21339), versionInfo.first.asString()));
+ if (m_localAddon && m_localAddon->Version() == versionInfo.first &&
+ m_item->GetAddonInfo()->Origin() == versionInfo.second)
+ item.Select(true);
+
+ AddonPtr repo;
+ if (versionInfo.second == LOCAL_CACHE)
+ {
+ item.SetLabel2(g_localizeStrings.Get(24095));
+ item.SetArt("icon", "DefaultAddonRepository.png");
+ dialog->Add(item);
+ }
+ else if (CServiceBroker::GetAddonMgr().GetAddon(versionInfo.second, repo, AddonType::REPOSITORY,
+ OnlyEnabled::CHOICE_YES))
+ {
+ item.SetLabel2(repo->Name());
+ item.SetArt("icon", repo->Icon());
+ dialog->Add(item);
+ }
+ }
+
+ dialog->Open();
+ return dialog->IsConfirmed() ? dialog->GetSelectedItem() : -1;
+}
+
+void CGUIDialogAddonInfo::OnUpdate()
+{
+ const auto& itemAddonInfo = m_item->GetAddonInfo();
+ const std::string& addonId = itemAddonInfo->ID();
+ const std::string& origin = m_item->GetProperty("Addon.ValidUpdateOrigin").asString();
+ const CAddonVersion& version =
+ static_cast<CAddonVersion>(m_item->GetProperty("Addon.ValidUpdateVersion").asString());
+
+ Close();
+ if (!m_depsInstalledWithAvailable.empty() &&
+ !ShowDependencyList(Reactivate::CHOICE_NO, EntryPoint::UPDATE))
+ return;
+
+ CAddonInstaller::GetInstance().Install(addonId, version, origin);
+}
+
+void CGUIDialogAddonInfo::OnSelectVersion()
+{
+ if (!m_item->HasAddonInfo())
+ return;
+
+ const std::string& processAddonId = m_item->GetAddonInfo()->ID();
+ EntryPoint entryPoint = m_localAddon ? EntryPoint::UPDATE : EntryPoint::INSTALL;
+
+ // get all compatible versions of an addon-id regardless of their origin
+ std::vector<std::shared_ptr<IAddon>> compatibleVersions =
+ CServiceBroker::GetAddonMgr().GetCompatibleVersions(processAddonId);
+
+ std::vector<std::pair<CAddonVersion, std::string>> versions;
+ versions.reserve(compatibleVersions.size());
+
+ for (const auto& compatibleVersion : compatibleVersions)
+ versions.emplace_back(compatibleVersion->Version(), compatibleVersion->Origin());
+
+ CAddonDatabase database;
+ database.Open();
+
+ CFileItemList items;
+ if (XFILE::CDirectory::GetDirectory("special://home/addons/packages/", items, ".zip",
+ DIR_FLAG_NO_FILE_DIRS))
+ {
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ std::string packageId;
+ std::string versionString;
+ if (CAddonVersion::SplitFileName(packageId, versionString, items[i]->GetLabel()))
+ {
+ if (packageId == processAddonId)
+ {
+ std::string hash;
+ std::string path(items[i]->GetPath());
+ if (database.GetPackageHash(processAddonId, items[i]->GetPath(), hash))
+ {
+ std::string sha256 = CUtil::GetFileDigest(path, KODI::UTILITY::CDigest::Type::SHA256);
+
+ // don't offer locally cached packages that result in an invalid version.
+ // usually this happens when the package filename gets malformed on the fs
+ // e.g. downloading "http://localhost/a+b.zip" ends up in "a b.zip"
+ const CAddonVersion version(versionString);
+ if (StringUtils::EqualsNoCase(sha256, hash) && !version.empty())
+ versions.emplace_back(version, LOCAL_CACHE);
+ }
+ }
+ }
+ }
+ }
+
+ if (versions.empty())
+ HELPERS::ShowOKDialogText(CVariant{21341}, CVariant{21342});
+ else
+ {
+ int i = AskForVersion(versions);
+ if (i != -1)
+ {
+ Close();
+
+ if (versions[i].second == LOCAL_CACHE)
+ {
+ CAddonInstaller::GetInstance().InstallFromZip(
+ StringUtils::Format("special://home/addons/packages/{}-{}.zip", processAddonId,
+ versions[i].first.asString()));
+ }
+ else
+ {
+ if (!m_depsInstalledWithAvailable.empty() &&
+ !ShowDependencyList(Reactivate::CHOICE_NO, entryPoint))
+ return;
+ CAddonInstaller::GetInstance().Install(processAddonId, versions[i].first,
+ versions[i].second);
+ }
+ }
+ }
+}
+
+void CGUIDialogAddonInfo::OnToggleAutoUpdates()
+{
+ CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), CONTROL_BTN_AUTOUPDATE);
+ if (OnMessage(msg))
+ {
+ bool selected = msg.GetParam1() == 1;
+ if (selected)
+ CServiceBroker::GetAddonMgr().RemoveAllUpdateRulesFromList(m_localAddon->ID());
+ else
+ CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_localAddon->ID(),
+ AddonUpdateRule::USER_DISABLED_AUTO_UPDATE);
+
+ bool showUpdateButton = (selected && m_item->GetProperty("Addon.HasUpdate").asBoolean());
+
+ if (showUpdateButton)
+ {
+ SET_CONTROL_VISIBLE(CONTROL_BTN_UPDATE);
+ SET_CONTROL_HIDDEN(CONTROL_BTN_VERSIONS);
+ }
+ else
+ {
+ SET_CONTROL_VISIBLE(CONTROL_BTN_VERSIONS);
+ SET_CONTROL_HIDDEN(CONTROL_BTN_UPDATE);
+ }
+
+ CServiceBroker::GetAddonMgr().PublishEventAutoUpdateStateChanged(m_localAddon->ID());
+ }
+}
+
+void CGUIDialogAddonInfo::OnInstall()
+{
+ if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
+ return;
+
+ if (!m_item->HasAddonInfo())
+ return;
+
+ const auto& itemAddonInfo = m_item->GetAddonInfo();
+ const std::string& origin = itemAddonInfo->Origin();
+
+ if (m_localAddon && CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode() !=
+ AddonRepoUpdateMode::ANY_REPOSITORY)
+ {
+ if (m_localAddon->Origin() != origin && m_localAddon->Origin() != ORIGIN_SYSTEM)
+ {
+ const std::string& header = g_localizeStrings.Get(19098); // Warning!
+ const std::string origin =
+ !m_localAddon->Origin().empty() ? m_localAddon->Origin() : g_localizeStrings.Get(39029);
+ const std::string text =
+ StringUtils::Format(g_localizeStrings.Get(39028), m_localAddon->Name(), origin,
+ m_localAddon->Version().asString());
+
+ if (CGUIDialogYesNo::ShowAndGetInput(header, text))
+ {
+ m_silentUninstall = true;
+ OnUninstall();
+ }
+ else
+ {
+ return;
+ }
+ }
+ }
+
+ const std::string& addonId = itemAddonInfo->ID();
+ const CAddonVersion& version = itemAddonInfo->Version();
+
+ Close();
+ if (!m_depsInstalledWithAvailable.empty() &&
+ !ShowDependencyList(Reactivate::CHOICE_NO, EntryPoint::INSTALL))
+ return;
+
+ CAddonInstaller::GetInstance().Install(addonId, version, origin);
+}
+
+void CGUIDialogAddonInfo::OnSelect()
+{
+ if (!m_localAddon)
+ return;
+
+ if (CanShowSupportList())
+ {
+ ShowSupportList();
+ return;
+ }
+
+ Close();
+
+ if (CanOpen() || CanRun())
+ CBuiltins::GetInstance().Execute("RunAddon(" + m_localAddon->ID() + ")");
+ else if (CanUse())
+ CAddonSystemSettings::GetInstance().SetActive(m_localAddon->Type(), m_localAddon->ID());
+}
+
+bool CGUIDialogAddonInfo::CanOpen() const
+{
+ return m_localAddon && m_localAddon->Type() == AddonType::PLUGIN;
+}
+
+bool CGUIDialogAddonInfo::CanRun() const
+{
+ if (m_localAddon)
+ {
+ if (m_localAddon->Type() == AddonType::SCRIPT)
+ return true;
+
+ if (GAME::CGameUtils::IsStandaloneGame(m_localAddon))
+ return true;
+ }
+
+ return false;
+}
+
+bool CGUIDialogAddonInfo::CanUse() const
+{
+ return m_localAddon && (m_localAddon->Type() == AddonType::SKIN ||
+ m_localAddon->Type() == AddonType::SCREENSAVER ||
+ m_localAddon->Type() == AddonType::VISUALIZATION ||
+ m_localAddon->Type() == AddonType::SCRIPT_WEATHER ||
+ m_localAddon->Type() == AddonType::RESOURCE_LANGUAGE ||
+ m_localAddon->Type() == AddonType::RESOURCE_UISOUNDS ||
+ m_localAddon->Type() == AddonType::AUDIOENCODER);
+}
+
+bool CGUIDialogAddonInfo::CanShowSupportList() const
+{
+ return m_localAddon && (m_localAddon->Type() == AddonType::AUDIODECODER ||
+ m_localAddon->Type() == AddonType::IMAGEDECODER);
+}
+
+bool CGUIDialogAddonInfo::PromptIfDependency(int heading, int line2)
+{
+ if (!m_localAddon)
+ return false;
+
+ VECADDONS addons;
+ std::vector<std::string> deps;
+ CServiceBroker::GetAddonMgr().GetAddons(addons);
+ for (VECADDONS::const_iterator it = addons.begin(); it != addons.end(); ++it)
+ {
+ auto i =
+ std::find_if((*it)->GetDependencies().begin(), (*it)->GetDependencies().end(),
+ [&](const DependencyInfo& other) { return other.id == m_localAddon->ID(); });
+ if (i != (*it)->GetDependencies().end() && !i->optional) // non-optional dependency
+ deps.push_back((*it)->Name());
+ }
+
+ if (!deps.empty())
+ {
+ std::string line0 = StringUtils::Format(g_localizeStrings.Get(24046), m_localAddon->Name());
+ std::string line1 = StringUtils::Join(deps, ", ");
+ HELPERS::ShowOKDialogLines(CVariant{heading}, CVariant{std::move(line0)},
+ CVariant{std::move(line1)}, CVariant{line2});
+ return true;
+ }
+ return false;
+}
+
+void CGUIDialogAddonInfo::OnUninstall()
+{
+ if (!m_localAddon.get())
+ return;
+
+ if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
+ return;
+
+ // ensure the addon is not a dependency of other installed addons
+ if (PromptIfDependency(24037, 24047))
+ return;
+
+ // prompt user to be sure
+ if (!m_silentUninstall && !CGUIDialogYesNo::ShowAndGetInput(CVariant{24037}, CVariant{750}))
+ return;
+
+ bool removeData = false;
+ if (CDirectory::Exists(m_localAddon->Profile()))
+ removeData = CGUIDialogYesNo::ShowAndGetInput(CVariant{24037}, CVariant{39014});
+
+ CAddonInstaller::GetInstance().UnInstall(m_localAddon, removeData);
+
+ Close();
+}
+
+void CGUIDialogAddonInfo::OnEnableDisable()
+{
+ if (!m_localAddon)
+ return;
+
+ if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
+ return;
+
+ if (m_addonEnabled)
+ {
+ if (PromptIfDependency(24075, 24091))
+ return; //required. can't disable
+
+ CServiceBroker::GetAddonMgr().DisableAddon(m_localAddon->ID(), AddonDisabledReason::USER);
+ }
+ else
+ {
+ // Check user want to enable if lifecycle not normal
+ if (!ADDON::GUI::CHelpers::DialogAddonLifecycleUseAsk(m_localAddon))
+ return;
+
+ CServiceBroker::GetAddonMgr().EnableAddon(m_localAddon->ID());
+ }
+
+ UpdateControls(PerformButtonFocus::CHOICE_NO);
+}
+
+void CGUIDialogAddonInfo::OnSettings()
+{
+ CGUIDialogAddonSettings::ShowForAddon(m_localAddon);
+}
+
+bool CGUIDialogAddonInfo::ShowDependencyList(Reactivate reactivate, EntryPoint entryPoint)
+{
+ if (entryPoint != EntryPoint::INSTALL || m_showDepDialogOnInstall)
+ {
+ auto pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ CFileItemList items;
+
+ for (const auto& it : m_depsInstalledWithAvailable)
+ {
+ // All combinations of depAddon and localAddon validity are possible and information
+ // must be displayed even when there is no depAddon.
+ // info_addon is the add-on to take the information to display (name, icon) from. The
+ // version in the repository is preferred because it might contain more recent data.
+
+ std::shared_ptr<IAddon> infoAddon = it.m_available ? it.m_available : it.m_installed;
+
+ if (infoAddon)
+ {
+ if (entryPoint != EntryPoint::UPDATE || !it.IsInstalledUpToDate())
+ {
+ const CFileItemPtr item = std::make_shared<CFileItem>(infoAddon->Name());
+ int messageId = 24180; // minversion only
+
+ // dep not installed locally, but it is available from a repo!
+ // make sure only non-optional add-ons that meet versionMin are
+ // announced for installation
+
+ if (!it.m_installed)
+ {
+ if (entryPoint != EntryPoint::SHOW_DEPENDENCIES && !it.m_depInfo.optional)
+ {
+ if (it.m_depInfo.versionMin <= it.m_available->Version())
+ {
+ messageId = 24181; // => install
+ }
+ else
+ {
+ messageId = 24185; // => not available, only lower versions available in the repos
+ }
+ }
+ }
+ else // dep is installed locally
+ {
+ messageId = 24182; // => installed
+
+ if (!it.IsInstalledUpToDate())
+ {
+ messageId = 24183; // => update to
+ }
+ }
+
+ if (entryPoint == EntryPoint::SHOW_DEPENDENCIES ||
+ infoAddon->MainType() != AddonType::SCRIPT_MODULE ||
+ !CAddonRepos::IsFromOfficialRepo(infoAddon, CheckAddonPath::CHOICE_NO))
+ {
+ item->SetLabel2(StringUtils::Format(
+ g_localizeStrings.Get(messageId), it.m_depInfo.versionMin.asString(),
+ it.m_installed ? it.m_installed->Version().asString() : "",
+ it.m_available ? it.m_available->Version().asString() : "",
+ it.m_depInfo.optional ? g_localizeStrings.Get(24184) : ""));
+
+ item->SetArt("icon", infoAddon->Icon());
+ item->SetProperty("addon_id", it.m_depInfo.id);
+ items.Add(item);
+ }
+ }
+ }
+ else
+ {
+ const CFileItemPtr item = std::make_shared<CFileItem>(it.m_depInfo.id);
+ item->SetLabel2(g_localizeStrings.Get(10005)); // Not available
+ items.Add(item);
+ }
+ }
+
+ if (!items.IsEmpty())
+ {
+ CFileItemPtr backup_item = GetCurrentListItem();
+ while (true)
+ {
+ pDialog->Reset();
+ pDialog->SetHeading(reactivate == Reactivate::CHOICE_YES ? 39024 : 39020);
+ pDialog->SetUseDetails(true);
+ for (auto& it : items)
+ pDialog->Add(*it);
+ pDialog->EnableButton(reactivate == Reactivate::CHOICE_NO, 186);
+ pDialog->SetButtonFocus(true);
+ pDialog->Open();
+
+ if (pDialog->IsButtonPressed())
+ return true;
+
+ if (pDialog->IsConfirmed())
+ {
+ const CFileItemPtr& item = pDialog->GetSelectedFileItem();
+ std::string addon_id = item->GetProperty("addon_id").asString();
+ std::shared_ptr<IAddon> depAddon;
+ if (CServiceBroker::GetAddonMgr().FindInstallableById(addon_id, depAddon))
+ {
+ Close();
+ ShowForItem(std::make_shared<CFileItem>(depAddon));
+ }
+ }
+ else
+ break;
+ }
+ SetItem(backup_item);
+ if (reactivate == Reactivate::CHOICE_YES)
+ Open();
+
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void CGUIDialogAddonInfo::ShowSupportList()
+{
+ std::vector<KODI::ADDONS::AddonSupportEntry> list;
+ if (CanShowSupportList())
+ list =
+ CServiceBroker::GetExtsMimeSupportList().GetSupportedExtsAndMimeTypes(m_localAddon->ID());
+
+ auto pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ CFileItemList items;
+ for (const auto& entry : list)
+ {
+ // Ignore included extension about track support
+ if (StringUtils::EndsWith(entry.m_name, KODI_ADDON_AUDIODECODER_TRACK_EXT))
+ continue;
+
+ std::string label;
+ if (entry.m_type == AddonSupportType::Extension)
+ label = StringUtils::Format(g_localizeStrings.Get(21346), entry.m_name);
+ else if (entry.m_type == AddonSupportType::Mimetype)
+ label = StringUtils::Format(g_localizeStrings.Get(21347), entry.m_name);
+ else
+ label = entry.m_name;
+
+ const CFileItemPtr item = std::make_shared<CFileItem>(label);
+ item->SetLabel2(entry.m_description);
+ if (!entry.m_icon.empty())
+ item->SetArt("icon", entry.m_icon);
+ else if (entry.m_type == AddonSupportType::Extension)
+ item->SetArt("icon", "DefaultExtensionInfo.png");
+ else if (entry.m_type == AddonSupportType::Mimetype)
+ item->SetArt("icon", "DefaultMimetypeInfo.png");
+ item->SetProperty("addon_id", m_localAddon->ID());
+ items.Add(item);
+ }
+
+ pDialog->Reset();
+ pDialog->SetHeading(21485);
+ pDialog->SetUseDetails(true);
+ for (auto& it : items)
+ pDialog->Add(*it);
+ pDialog->SetButtonFocus(true);
+ pDialog->Open();
+}
+
+bool CGUIDialogAddonInfo::ShowForItem(const CFileItemPtr& item)
+{
+ if (!item)
+ return false;
+
+ CGUIDialogAddonInfo* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogAddonInfo>(
+ WINDOW_DIALOG_ADDON_INFO);
+ if (!dialog)
+ return false;
+ if (!dialog->SetItem(item))
+ return false;
+
+ dialog->Open();
+ return true;
+}
+
+bool CGUIDialogAddonInfo::SetItem(const CFileItemPtr& item)
+{
+ if (!item || !item->HasAddonInfo())
+ return false;
+
+ m_item = std::make_shared<CFileItem>(*item);
+ m_localAddon.reset();
+ if (CServiceBroker::GetAddonMgr().GetAddon(item->GetAddonInfo()->ID(), m_localAddon,
+ OnlyEnabled::CHOICE_NO))
+ {
+ CLog::Log(LOGDEBUG, "{} - Addon with id {} not found locally.", __FUNCTION__,
+ item->GetAddonInfo()->ID());
+ }
+ return true;
+}
+
+void CGUIDialogAddonInfo::BuildDependencyList()
+{
+ if (!m_item)
+ return;
+
+ m_showDepDialogOnInstall = false;
+ m_depsInstalledWithAvailable.clear();
+ m_deps = CServiceBroker::GetAddonMgr().GetDepsRecursive(m_item->GetAddonInfo()->ID(),
+ OnlyEnabledRootAddon::CHOICE_NO);
+
+ for (const auto& dep : m_deps)
+ {
+ std::shared_ptr<IAddon> addonInstalled;
+ std::shared_ptr<IAddon> addonAvailable;
+
+ // Find add-on in local installation
+ if (!CServiceBroker::GetAddonMgr().GetAddon(dep.id, addonInstalled, OnlyEnabled::CHOICE_YES))
+ {
+ addonInstalled = nullptr;
+ }
+
+ // Find add-on in repositories
+ if (!CServiceBroker::GetAddonMgr().FindInstallableById(dep.id, addonAvailable))
+ {
+ addonAvailable = nullptr;
+ }
+
+ if (!addonInstalled)
+ {
+
+ // after pushing the install button the dependency install dialog
+ // will be opened only if...
+ // - dependencies are unavailable (for informational purposes) OR
+ // - the dependency is not a script/module OR
+ // - the script/module is not available at an official repo
+ if (!addonAvailable || addonAvailable->MainType() != AddonType::SCRIPT_MODULE ||
+ !CAddonRepos::IsFromOfficialRepo(addonAvailable, CheckAddonPath::CHOICE_NO))
+ {
+ m_showDepDialogOnInstall = true;
+ }
+ }
+ else
+ {
+
+ // only display dialog if updates for already installed dependencies will install
+ if (addonAvailable && addonAvailable->Version() > addonInstalled->Version())
+ {
+ m_showDepDialogOnInstall = true;
+ }
+ }
+
+ m_depsInstalledWithAvailable.emplace_back(dep, addonInstalled, addonAvailable);
+ }
+
+ std::sort(m_depsInstalledWithAvailable.begin(), m_depsInstalledWithAvailable.end(),
+ [](const auto& a, const auto& b) {
+ // 1. "not installed/available" go to the bottom first
+ const bool depAInstalledOrAvailable =
+ a.m_installed != nullptr || a.m_available != nullptr;
+ const bool depBInstalledOrAvailable =
+ b.m_installed != nullptr || b.m_available != nullptr;
+
+ if (depAInstalledOrAvailable != depBInstalledOrAvailable)
+ {
+ return !depAInstalledOrAvailable;
+ }
+
+ // 2. then optional add-ons to top
+ if (a.m_depInfo.optional != b.m_depInfo.optional)
+ {
+ return a.m_depInfo.optional;
+ }
+
+ // 3. addon type asc, except scripts/modules at the bottom
+ const std::shared_ptr<IAddon>& depA = a.m_installed ? a.m_installed : a.m_available;
+ const std::shared_ptr<IAddon>& depB = b.m_installed ? b.m_installed : b.m_available;
+
+ if (depA && depB)
+ {
+ const AddonType typeA = depA->MainType();
+ const AddonType typeB = depB->MainType();
+ if (typeA != typeB)
+ {
+ if ((typeA == AddonType::SCRIPT_MODULE) == (typeB == AddonType::SCRIPT_MODULE))
+ {
+ // both are scripts/modules or neither one is => sort by addon type asc
+ return typeA < typeB;
+ }
+ else
+ {
+ // At this point, either:
+ // A is script/module and B is not, or A is not script/module and B is.
+ // the script/module goes to the bottom
+ return typeA != AddonType::SCRIPT_MODULE;
+ }
+ }
+ }
+
+ // 4. finally order by addon-id
+ return a.m_depInfo.id < b.m_depInfo.id;
+ });
+}
+
+bool CInstalledWithAvailable::IsInstalledUpToDate() const
+{
+ if (m_installed)
+ {
+ if (!m_available || m_available->Version() == m_installed->Version())
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/addons/gui/GUIDialogAddonInfo.h b/xbmc/addons/gui/GUIDialogAddonInfo.h
new file mode 100644
index 0000000..b28a858
--- /dev/null
+++ b/xbmc/addons/gui/GUIDialogAddonInfo.h
@@ -0,0 +1,162 @@
+/*
+ * 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 "addons/addoninfo/AddonInfo.h"
+#include "guilib/GUIDialog.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace ADDON
+{
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+
+} // namespace ADDON
+
+enum class Reactivate : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+enum class PerformButtonFocus : bool
+{
+ CHOICE_YES = true,
+ CHOICE_NO = false,
+};
+
+enum class EntryPoint : int
+{
+ INSTALL,
+ UPDATE,
+ SHOW_DEPENDENCIES,
+};
+
+struct CInstalledWithAvailable
+{
+ CInstalledWithAvailable(const ADDON::DependencyInfo& depInfo,
+ const std::shared_ptr<ADDON::IAddon>& installed,
+ const std::shared_ptr<ADDON::IAddon>& available)
+ : m_depInfo(depInfo), m_installed(installed), m_available(available)
+ {
+ }
+
+ /*!
+ * @brief Returns true if the currently installed dependency version is up to date
+ * or the dependency is not available from a repository
+ */
+ bool IsInstalledUpToDate() const;
+
+ ADDON::DependencyInfo m_depInfo;
+ std::shared_ptr<ADDON::IAddon> m_installed;
+ std::shared_ptr<ADDON::IAddon> m_available;
+};
+
+class CGUIDialogAddonInfo : public CGUIDialog
+{
+public:
+ CGUIDialogAddonInfo(void);
+ ~CGUIDialogAddonInfo(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+
+ CFileItemPtr GetCurrentListItem(int offset = 0) override { return m_item; }
+ bool HasListItems() const override { return true; }
+
+ static bool ShowForItem(const CFileItemPtr& item);
+
+private:
+ void OnInitWindow() override;
+
+ /*!
+ * @brief Set the item to display addon info on.
+ *
+ * @param[in] item to display
+ * @return true if we can display information, false otherwise
+ */
+ bool SetItem(const CFileItemPtr& item);
+ void UpdateControls(PerformButtonFocus performButtonFocus);
+
+ void OnUpdate();
+ void OnSelectVersion();
+ void OnInstall();
+ void OnUninstall();
+ void OnEnableDisable();
+ void OnSettings();
+ void OnSelect();
+ void OnToggleAutoUpdates();
+ int AskForVersion(std::vector<std::pair<ADDON::CAddonVersion, std::string>>& versions);
+
+ /*!
+ * @brief Returns true if current addon can be opened (i.e is a plugin)
+ */
+ bool CanOpen() const;
+
+ /*!
+ * @brief Returns true if current addon can be run (i.e is a script)
+ */
+ bool CanRun() const;
+
+ /*!
+ * @brief Returns true if current addon is of a type that can only have one active
+ * in use at a time and can be changed (e.g skins)
+ */
+ bool CanUse() const;
+
+ /*!
+ * @brief Returns true if current addon can be show list about supported parts
+ */
+ bool CanShowSupportList() const;
+
+ /*!
+ * @brief check if the add-on is a dependency of others, and if so prompt the user.
+ *
+ * @param[in] heading the label for the heading of the prompt dialog
+ * @param[in] line2 the action that could not be completed.
+ * @return true if prompted, false otherwise.
+ */
+ bool PromptIfDependency(int heading, int line2);
+
+ /*!
+ * @brief Show a dialog with the addon's dependencies.
+ *
+ * @param[in] reactivate If true, reactivate info dialog when done
+ * @param[in] entryPoint INSTALL, UPDATE or SHOW_DEPENDENCIES
+ * @return True if okay was selected, false otherwise
+ */
+ bool ShowDependencyList(Reactivate reactivate, EntryPoint entryPoint);
+
+ /*!
+ * @brief Show a dialog with the addon's supported extensions and mimetypes.
+ */
+ void ShowSupportList();
+
+ /*!
+ * @brief Used to build up the dependency list shown by @ref ShowDependencyList()
+ */
+ void BuildDependencyList();
+
+ CFileItemPtr m_item;
+ ADDON::AddonPtr m_localAddon;
+ bool m_addonEnabled = false;
+
+ /*!< a switch to force @ref OnUninstall() to proceed without user interaction.
+ * useful for cases like where another repo’s version of an addon must
+ * be removed before installing a new version.
+ */
+ bool m_silentUninstall = false;
+
+ bool m_showDepDialogOnInstall = false;
+ std::vector<ADDON::DependencyInfo> m_deps;
+ std::vector<CInstalledWithAvailable> m_depsInstalledWithAvailable;
+};
diff --git a/xbmc/addons/gui/GUIDialogAddonSettings.cpp b/xbmc/addons/gui/GUIDialogAddonSettings.cpp
new file mode 100644
index 0000000..bc794d4
--- /dev/null
+++ b/xbmc/addons/gui/GUIDialogAddonSettings.cpp
@@ -0,0 +1,484 @@
+/*
+ * 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 "GUIDialogAddonSettings.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/settings/AddonSettings.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "input/Key.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/SettingSection.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "view/ViewStateSettings.h"
+
+#define CONTROL_BTN_LEVELS 20
+
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+
+CGUIDialogAddonSettings::CGUIDialogAddonSettings()
+ : CGUIDialogSettingsManagerBase(WINDOW_DIALOG_ADDON_SETTINGS, "DialogAddonSettings.xml")
+{
+}
+
+bool CGUIDialogAddonSettings::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ if (message.GetSenderId() == CONTROL_SETTINGS_CUSTOM_BUTTON)
+ {
+ OnResetSettings();
+ return true;
+ }
+ break;
+ }
+
+ case GUI_MSG_SETTING_UPDATED:
+ {
+ const std::string& settingId = message.GetStringParam(0);
+ const std::string& settingValue = message.GetStringParam(1);
+ const ADDON::AddonInstanceId instanceId = message.GetParam1();
+
+ if (instanceId != m_instanceId)
+ {
+ CLog::Log(LOGERROR,
+ "CGUIDialogAddonSettings::{}: Set value \"{}\" from add-on \"{}\" called with "
+ "invalid instance id (given: {}, needed: {})",
+ __func__, m_addon->ID(), settingId, instanceId, m_instanceId);
+ break;
+ }
+
+ std::shared_ptr<CSetting> setting = GetSettingsManager()->GetSetting(settingId);
+ if (setting != nullptr)
+ {
+ setting->FromString(settingValue);
+ return true;
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return CGUIDialogSettingsManagerBase::OnMessage(message);
+}
+
+bool CGUIDialogAddonSettings::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_SETTINGS_LEVEL_CHANGE:
+ {
+ // Test if we can access the new level
+ if (!g_passwordManager.CheckSettingLevelLock(
+ CViewStateSettings::GetInstance().GetNextSettingLevel(), true))
+ return false;
+
+ CViewStateSettings::GetInstance().CycleSettingLevel();
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ // try to keep the current position
+ std::string oldCategory;
+ if (m_iCategory >= 0 && m_iCategory < static_cast<int>(m_categories.size()))
+ oldCategory = m_categories[m_iCategory]->GetId();
+
+ SET_CONTROL_LABEL(CONTROL_BTN_LEVELS,
+ 10036 +
+ static_cast<int>(CViewStateSettings::GetInstance().GetSettingLevel()));
+ // only re-create the categories, the settings will be created later
+ SetupControls(false);
+
+ m_iCategory = 0;
+ // try to find the category that was previously selected
+ if (!oldCategory.empty())
+ {
+ for (int i = 0; i < static_cast<int>(m_categories.size()); i++)
+ {
+ if (m_categories[i]->GetId() == oldCategory)
+ {
+ m_iCategory = i;
+ break;
+ }
+ }
+ }
+
+ CreateSettings();
+ return true;
+ }
+
+ default:
+ break;
+ }
+
+ return CGUIDialogSettingsManagerBase::OnAction(action);
+}
+
+bool CGUIDialogAddonSettings::ShowForAddon(const ADDON::AddonPtr& addon,
+ bool saveToDisk /* = true */)
+{
+ if (!addon)
+ {
+ CLog::LogF(LOGERROR, "No addon given!");
+ return false;
+ }
+
+ if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
+ return false;
+
+ if (addon->SupportsInstanceSettings())
+ return ShowForMultipleInstances(addon, saveToDisk);
+ else
+ return ShowForSingleInstance(addon, saveToDisk);
+}
+
+bool CGUIDialogAddonSettings::ShowForSingleInstance(
+ const ADDON::AddonPtr& addon,
+ bool saveToDisk,
+ ADDON::AddonInstanceId instanceId /* = ADDON::ADDON_SETTINGS_ID */)
+{
+ if (!addon->HasSettings(instanceId))
+ {
+ // addon does not support settings, inform user
+ HELPERS::ShowOKDialogText(CVariant{24000}, CVariant{24030});
+ return false;
+ }
+
+ // Create the dialog
+ CGUIDialogAddonSettings* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogAddonSettings>(
+ WINDOW_DIALOG_ADDON_SETTINGS);
+ if (!dialog)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_ADDON_SETTINGS instance!");
+ return false;
+ }
+
+ dialog->m_addon = addon;
+ dialog->m_instanceId = instanceId;
+ dialog->m_saveToDisk = saveToDisk;
+
+ dialog->Open();
+
+ if (!dialog->IsConfirmed())
+ {
+ addon->ReloadSettings(instanceId);
+ return false;
+ }
+
+ if (saveToDisk)
+ addon->SaveSettings(instanceId);
+
+ return true;
+}
+
+bool CGUIDialogAddonSettings::ShowForMultipleInstances(const ADDON::AddonPtr& addon,
+ bool saveToDisk)
+{
+ CGUIDialogSelect* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!dialog)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT instance!");
+ return false;
+ }
+
+ int lastSelected = -1;
+ while (true)
+ {
+ std::vector<ADDON::AddonInstanceId> ids = addon->GetKnownInstanceIds();
+ std::sort(ids.begin(), ids.end(), [](const auto& a, const auto& b) { return a < b; });
+
+ dialog->Reset();
+ dialog->SetHeading(10012); // Add-on configurations and settings
+ dialog->SetUseDetails(false);
+
+ CFileItemList itemsInstances;
+ ADDON::AddonInstanceId highestId = 0;
+ for (const auto& id : ids)
+ {
+ std::string name;
+ addon->GetSettingString(ADDON_SETTING_INSTANCE_NAME_VALUE, name, id);
+ if (name.empty())
+ name = g_localizeStrings.Get(13205); // Unknown
+
+ bool enabled = false;
+ addon->GetSettingBool(ADDON_SETTING_INSTANCE_ENABLED_VALUE, enabled, id);
+
+ const std::string label = StringUtils::Format(
+ g_localizeStrings.Get(10020), name,
+ g_localizeStrings.Get(enabled ? 305 : 13106)); // Edit "config name" [enabled state]
+
+ const CFileItemPtr item = std::make_shared<CFileItem>(label);
+ item->SetProperty("id", id);
+ item->SetProperty("name", name);
+ itemsInstances.Add(item);
+
+ if (id > highestId)
+ highestId = id;
+ }
+
+ CFileItemList itemsGeneral;
+
+ const ADDON::AddonInstanceId addInstanceId = highestId + 1;
+ const ADDON::AddonInstanceId removeInstanceId = highestId + 2;
+
+ CFileItemPtr item =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(10014)); // Add add-on configuration
+ item->SetProperty("id", addInstanceId);
+ itemsGeneral.Add(item);
+
+ if (ids.size() > 1) // Forbid removal of last instance
+ {
+ item =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(10015)); // Remove add-on configuration
+ item->SetProperty("id", removeInstanceId);
+ itemsGeneral.Add(item);
+ }
+
+ if (addon->HasSettings(ADDON_SETTINGS_ID))
+ {
+ item = std::make_shared<CFileItem>(g_localizeStrings.Get(10013)); // Edit Add-on settings
+ item->SetProperty("id", ADDON_SETTINGS_ID);
+ itemsGeneral.Add(item);
+ }
+
+ for (auto& it : itemsGeneral)
+ dialog->Add(*it);
+
+ for (auto& it : itemsInstances)
+ dialog->Add(*it);
+
+ // Select last selected item, first instance config item or first item
+ if (lastSelected >= 0)
+ dialog->SetSelected(lastSelected);
+ else
+ dialog->SetSelected(itemsInstances.Size() > 0 ? itemsGeneral.Size() : 0);
+
+ dialog->Open();
+
+ if (dialog->IsButtonPressed() || !dialog->IsConfirmed())
+ break;
+
+ lastSelected = dialog->GetSelectedItem();
+
+ item = dialog->GetSelectedFileItem();
+ ADDON::AddonInstanceId instanceId = item->GetProperty("id").asInteger();
+
+ if (instanceId == addInstanceId)
+ {
+ instanceId = highestId + 1;
+
+ addon->GetSettings(instanceId);
+ addon->UpdateSettingString(ADDON_SETTING_INSTANCE_NAME_VALUE, "", instanceId);
+ addon->UpdateSettingBool(ADDON_SETTING_INSTANCE_ENABLED_VALUE, true, instanceId);
+ addon->SaveSettings(instanceId);
+
+ if (ShowForSingleInstance(addon, saveToDisk, instanceId))
+ {
+ CServiceBroker::GetAddonMgr().PublishInstanceAdded(addon->ID(), instanceId);
+ }
+ else
+ {
+ // Remove instance settings if not succeeded (e.g. dialog cancelled)
+ addon->DeleteInstanceSettings(instanceId);
+ }
+ }
+ else if (instanceId == removeInstanceId)
+ {
+ dialog->Reset();
+ dialog->SetHeading(10010); // Select add-on configuration to remove
+ dialog->SetUseDetails(false);
+
+ for (auto& it : itemsInstances)
+ {
+ CFileItem item(*it);
+ item.SetLabel((*it).GetProperty("name").asString());
+ dialog->Add(item);
+ }
+
+ dialog->SetSelected(0);
+ dialog->Open();
+
+ if (!dialog->IsButtonPressed() && dialog->IsConfirmed())
+ {
+ item = dialog->GetSelectedFileItem();
+ const std::string label = StringUtils::Format(
+ g_localizeStrings.Get(10019),
+ item->GetProperty("name")
+ .asString()); // Do you want to remove the add-on configuration "config name"?
+
+ if (CGUIDialogYesNo::ShowAndGetInput(10009, // Confirm add-on configuration removal
+ label))
+ {
+ instanceId = item->GetProperty("id").asInteger();
+ addon->DeleteInstanceSettings(instanceId);
+ CServiceBroker::GetAddonMgr().PublishInstanceRemoved(addon->ID(), instanceId);
+ }
+ }
+ }
+ else
+ {
+ // edit instance settings or edit addon settings selected; open settings dialog
+
+ bool enabled = false;
+ addon->GetSettingBool(ADDON_SETTING_INSTANCE_ENABLED_VALUE, enabled, instanceId);
+
+ if (ShowForSingleInstance(addon, saveToDisk, instanceId) && instanceId != ADDON_SETTINGS_ID)
+ {
+ // Publish new/removed instance configuration and start the use of new instance
+ bool enabledNow = false;
+ addon->GetSettingBool(ADDON_SETTING_INSTANCE_ENABLED_VALUE, enabledNow, instanceId);
+ if (enabled != enabledNow)
+ {
+ if (enabledNow)
+ CServiceBroker::GetAddonMgr().PublishInstanceAdded(addon->ID(), instanceId);
+ else
+ CServiceBroker::GetAddonMgr().PublishInstanceRemoved(addon->ID(), instanceId);
+ }
+ }
+ }
+
+ // refresh selection dialog content...
+
+ } // while (true)
+
+ return true;
+}
+
+void CGUIDialogAddonSettings::SaveAndClose()
+{
+ if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
+ return;
+
+ // get the dialog
+ CGUIDialogAddonSettings* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogAddonSettings>(
+ WINDOW_DIALOG_ADDON_SETTINGS);
+ if (dialog == nullptr || !dialog->IsActive())
+ return;
+
+ // check if we need to save the settings
+ if (dialog->m_saveToDisk && dialog->m_addon != nullptr)
+ dialog->m_addon->SaveSettings(dialog->m_instanceId);
+
+ // close the dialog
+ dialog->Close();
+}
+
+std::string CGUIDialogAddonSettings::GetCurrentAddonID() const
+{
+ if (m_addon == nullptr)
+ return "";
+
+ return m_addon->ID();
+}
+
+void CGUIDialogAddonSettings::SetupView()
+{
+ if (m_addon == nullptr || m_addon->GetSettings(m_instanceId) == nullptr)
+ return;
+
+ auto settings = m_addon->GetSettings(m_instanceId);
+ if (!settings->IsLoaded())
+ return;
+
+ CGUIDialogSettingsManagerBase::SetupView();
+
+ // set addon id as window property
+ SetProperty("Addon.ID", m_addon->ID());
+
+ // set heading
+ SetHeading(StringUtils::Format("$LOCALIZE[10004] - {}",
+ m_addon->Name())); // "Settings - AddonName"
+
+ // set control labels
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CUSTOM_BUTTON, 409);
+ SET_CONTROL_LABEL(CONTROL_BTN_LEVELS,
+ 10036 + static_cast<int>(CViewStateSettings::GetInstance().GetSettingLevel()));
+}
+
+std::string CGUIDialogAddonSettings::GetLocalizedString(uint32_t labelId) const
+{
+ std::string label = g_localizeStrings.GetAddonString(m_addon->ID(), labelId);
+ if (!label.empty())
+ return label;
+
+ return CGUIDialogSettingsManagerBase::GetLocalizedString(labelId);
+}
+
+std::string CGUIDialogAddonSettings::GetSettingsLabel(const std::shared_ptr<ISetting>& setting)
+{
+ if (setting == nullptr)
+ return "";
+
+ std::string label = GetLocalizedString(setting->GetLabel());
+ if (!label.empty())
+ return label;
+
+ // try the addon settings
+ label = m_addon->GetSettings(m_instanceId)->GetSettingLabel(setting->GetLabel());
+ if (!label.empty())
+ return label;
+
+ return CGUIDialogSettingsManagerBase::GetSettingsLabel(setting);
+}
+
+int CGUIDialogAddonSettings::GetSettingLevel() const
+{
+ return static_cast<int>(CViewStateSettings::GetInstance().GetSettingLevel());
+}
+
+std::shared_ptr<CSettingSection> CGUIDialogAddonSettings::GetSection()
+{
+ const auto settingsManager = GetSettingsManager();
+ if (settingsManager == nullptr)
+ return nullptr;
+
+ const auto sections = settingsManager->GetSections();
+ if (!sections.empty())
+ return sections.front();
+
+ return nullptr;
+}
+
+CSettingsManager* CGUIDialogAddonSettings::GetSettingsManager() const
+{
+ if (m_addon == nullptr || m_addon->GetSettings(m_instanceId) == nullptr)
+ return nullptr;
+
+ return m_addon->GetSettings(m_instanceId)->GetSettingsManager();
+}
+
+void CGUIDialogAddonSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (m_addon == nullptr || m_addon->GetSettings(m_instanceId) == nullptr)
+ return;
+
+ m_addon->GetSettings(m_instanceId)->OnSettingAction(setting);
+}
diff --git a/xbmc/addons/gui/GUIDialogAddonSettings.h b/xbmc/addons/gui/GUIDialogAddonSettings.h
new file mode 100644
index 0000000..a3cf664
--- /dev/null
+++ b/xbmc/addons/gui/GUIDialogAddonSettings.h
@@ -0,0 +1,54 @@
+/*
+ * 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 "addons/IAddon.h"
+#include "settings/dialogs/GUIDialogSettingsManagerBase.h"
+
+class CGUIDialogAddonSettings : public CGUIDialogSettingsManagerBase
+{
+public:
+ CGUIDialogAddonSettings();
+ ~CGUIDialogAddonSettings() override = default;
+
+ // specializations of CGUIControl
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+
+ static bool ShowForAddon(const ADDON::AddonPtr& addon, bool saveToDisk = true);
+ static void SaveAndClose();
+
+ std::string GetCurrentAddonID() const;
+
+protected:
+ // implementation of CGUIDialogSettingsBase
+ void SetupView() override;
+ std::string GetLocalizedString(uint32_t labelId) const override;
+ std::string GetSettingsLabel(const std::shared_ptr<ISetting>& setting) override;
+ int GetSettingLevel() const override;
+ std::shared_ptr<CSettingSection> GetSection() override;
+
+ // implementation of CGUIDialogSettingsManagerBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override { return true; }
+ CSettingsManager* GetSettingsManager() const override;
+
+ // implementation of ISettingCallback
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+private:
+ static bool ShowForSingleInstance(const ADDON::AddonPtr& addon,
+ bool saveToDisk,
+ ADDON::AddonInstanceId instanceId = ADDON::ADDON_SETTINGS_ID);
+ static bool ShowForMultipleInstances(const ADDON::AddonPtr& addon, bool saveToDisk);
+
+ ADDON::AddonPtr m_addon;
+ ADDON::AddonInstanceId m_instanceId{ADDON::ADDON_SETTINGS_ID};
+ bool m_saveToDisk = false;
+};
diff --git a/xbmc/addons/gui/GUIHelpers.cpp b/xbmc/addons/gui/GUIHelpers.cpp
new file mode 100644
index 0000000..62fb9d3
--- /dev/null
+++ b/xbmc/addons/gui/GUIHelpers.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2005-2020 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 "GUIHelpers.h"
+
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+
+using namespace ADDON;
+using namespace ADDON::GUI;
+
+bool CHelpers::DialogAddonLifecycleUseAsk(const std::shared_ptr<const IAddon>& addon)
+{
+ int header_nr;
+ int text_nr;
+ switch (addon->LifecycleState())
+ {
+ case AddonLifecycleState::BROKEN:
+ header_nr = 24164;
+ text_nr = 24165;
+ break;
+ case AddonLifecycleState::DEPRECATED:
+ header_nr = 24166;
+ text_nr = 24167;
+ break;
+ default:
+ header_nr = 0;
+ text_nr = 0;
+ break;
+ }
+ if (header_nr > 0)
+ {
+ std::string header = StringUtils::Format(g_localizeStrings.Get(header_nr), addon->ID());
+ std::string text =
+ StringUtils::Format(g_localizeStrings.Get(text_nr), addon->LifecycleStateDescription());
+ if (!CGUIDialogYesNo::ShowAndGetInput(header, text))
+ return false;
+ }
+
+ return true;
+}
diff --git a/xbmc/addons/gui/GUIHelpers.h b/xbmc/addons/gui/GUIHelpers.h
new file mode 100644
index 0000000..63310ae
--- /dev/null
+++ b/xbmc/addons/gui/GUIHelpers.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2005-2020 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 <memory>
+
+namespace ADDON
+{
+
+class IAddon;
+
+namespace GUI
+{
+
+class CHelpers
+{
+public:
+ /*!
+ * @brief This shows an Yes/No dialog with information about the add-on if it is
+ * not in the normal status.
+ *
+ * This asks the user whether he really wants to use the add-on and informs with
+ * text why the other status is.
+ *
+ * @note The dialog is currently displayed for @ref AddonLifecycleState::BROKEN
+ * and @ref AddonLifecycleState::DEPRECATED.
+ *
+ * @param[in] addon Class of the add-on to be checked
+ * @return True if user activation is desired, false if not
+ */
+ static bool DialogAddonLifecycleUseAsk(const std::shared_ptr<const IAddon>& addon);
+};
+
+} /* namespace GUI */
+} /* namespace ADDON */
diff --git a/xbmc/addons/gui/GUIViewStateAddonBrowser.cpp b/xbmc/addons/gui/GUIViewStateAddonBrowser.cpp
new file mode 100644
index 0000000..5504f33
--- /dev/null
+++ b/xbmc/addons/gui/GUIViewStateAddonBrowser.cpp
@@ -0,0 +1,64 @@
+/*
+ * 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 "GUIViewStateAddonBrowser.h"
+
+#include "FileItem.h"
+#include "filesystem/File.h"
+#include "guilib/WindowIDs.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "view/ViewState.h"
+#include "windowing/GraphicContext.h"
+
+using namespace XFILE;
+using namespace ADDON;
+
+CGUIViewStateAddonBrowser::CGUIViewStateAddonBrowser(const CFileItemList& items)
+ : CGUIViewState(items)
+{
+ if (URIUtils::PathEquals(items.GetPath(), "addons://"))
+ {
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "", "%L", ""));
+ SetSortMethod(SortByNone);
+ }
+ else if (URIUtils::PathEquals(items.GetPath(), "addons://recently_updated/", true))
+ {
+ AddSortMethod(SortByLastUpdated, 12014, LABEL_MASKS("%L", "%v", "%L", "%v"),
+ SortAttributeIgnoreFolders, SortOrderDescending);
+ }
+ else
+ {
+ AddSortMethod(SortByLabel, SortAttributeIgnoreFolders, 551,
+ LABEL_MASKS("%L", "%s", "%L", "%s"));
+
+ if (StringUtils::StartsWith(items.GetPath(), "addons://sources/"))
+ AddSortMethod(SortByLastUsed, 12012, LABEL_MASKS("%L", "%u", "%L", "%u"),
+ SortAttributeIgnoreFolders, SortOrderDescending); //Label, Last used
+
+ if (StringUtils::StartsWith(items.GetPath(), "addons://user/") &&
+ items.GetContent() == "addons")
+ AddSortMethod(SortByInstallDate, 12013, LABEL_MASKS("%L", "%i", "%L", "%i"),
+ SortAttributeIgnoreFolders, SortOrderDescending);
+
+ SetSortMethod(SortByLabel);
+ }
+ SetViewAsControl(DEFAULT_VIEW_AUTO);
+
+ LoadViewState(items.GetPath(), WINDOW_ADDON_BROWSER);
+}
+
+void CGUIViewStateAddonBrowser::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_ADDON_BROWSER);
+}
+
+std::string CGUIViewStateAddonBrowser::GetExtensions()
+{
+ return "";
+}
diff --git a/xbmc/addons/gui/GUIViewStateAddonBrowser.h b/xbmc/addons/gui/GUIViewStateAddonBrowser.h
new file mode 100644
index 0000000..e7cc564
--- /dev/null
+++ b/xbmc/addons/gui/GUIViewStateAddonBrowser.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "view/GUIViewState.h"
+
+class CGUIViewStateAddonBrowser : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateAddonBrowser(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ std::string GetExtensions() override;
+};
diff --git a/xbmc/addons/gui/GUIWindowAddonBrowser.cpp b/xbmc/addons/gui/GUIWindowAddonBrowser.cpp
new file mode 100644
index 0000000..2f522e7
--- /dev/null
+++ b/xbmc/addons/gui/GUIWindowAddonBrowser.cpp
@@ -0,0 +1,644 @@
+/*
+ * 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 "GUIWindowAddonBrowser.h"
+
+#include "ContextMenuManager.h"
+#include "FileItem.h"
+#include "GUIDialogAddonInfo.h"
+#include "GUIUserMessages.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/IAddon.h"
+#include "addons/RepositoryUpdater.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/AddonsDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "platform/Platform.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "threads/IRunnable.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <utility>
+
+#define CONTROL_SETTINGS 5
+#define CONTROL_FOREIGNFILTER 7
+#define CONTROL_BROKENFILTER 8
+#define CONTROL_CHECK_FOR_UPDATES 9
+
+using namespace ADDON;
+using namespace XFILE;
+
+CGUIWindowAddonBrowser::CGUIWindowAddonBrowser(void)
+ : CGUIMediaWindow(WINDOW_ADDON_BROWSER, "AddonBrowser.xml")
+{
+}
+
+CGUIWindowAddonBrowser::~CGUIWindowAddonBrowser() = default;
+
+bool CGUIWindowAddonBrowser::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ CServiceBroker::GetRepositoryUpdater().Events().Unsubscribe(this);
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+ }
+ break;
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CServiceBroker::GetRepositoryUpdater().Events().Subscribe(this,
+ &CGUIWindowAddonBrowser::OnEvent);
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CGUIWindowAddonBrowser::OnEvent);
+
+ SetProperties();
+ }
+ break;
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_FOREIGNFILTER)
+ {
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->ToggleBool(CSettings::SETTING_GENERAL_ADDONFOREIGNFILTER);
+ settings->Save();
+ Refresh();
+ return true;
+ }
+ else if (iControl == CONTROL_BROKENFILTER)
+ {
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->ToggleBool(CSettings::SETTING_GENERAL_ADDONBROKENFILTER);
+ settings->Save();
+ Refresh();
+ return true;
+ }
+ else if (iControl == CONTROL_CHECK_FOR_UPDATES)
+ {
+ CServiceBroker::GetRepositoryUpdater().CheckForUpdates(true);
+ return true;
+ }
+ else if (iControl == CONTROL_SETTINGS)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SETTINGS_SYSTEM,
+ "addons");
+ return true;
+ }
+ else if (m_viewControl.HasControl(iControl)) // list/thumb control
+ {
+ // get selected item
+ int iItem = m_viewControl.GetSelectedItem();
+ int iAction = message.GetParam1();
+
+ // iItem is checked for validity inside these routines
+ if (iAction == ACTION_SHOW_INFO)
+ {
+ if (!m_vecItems->Get(iItem)->GetProperty("Addon.ID").empty())
+ return CGUIDialogAddonInfo::ShowForItem((*m_vecItems)[iItem]);
+ return false;
+ }
+ }
+ }
+ break;
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (message.GetParam1() == GUI_MSG_UPDATE_ITEM && IsActive() &&
+ message.GetNumStringParams() == 1)
+ { // update this item
+ for (int i = 0; i < m_vecItems->Size(); ++i)
+ {
+ CFileItemPtr item = m_vecItems->Get(i);
+ if (item->GetProperty("Addon.ID") == message.GetStringParam())
+ {
+ UpdateStatus(item);
+ FormatAndSort(*m_vecItems);
+ return true;
+ }
+ }
+ }
+ else if (message.GetParam1() == GUI_MSG_UPDATE && IsActive())
+ SetProperties();
+ }
+ break;
+ default:
+ break;
+ }
+ return CGUIMediaWindow::OnMessage(message);
+}
+
+void CGUIWindowAddonBrowser::SetProperties()
+{
+ auto lastUpdated = CServiceBroker::GetRepositoryUpdater().LastUpdated();
+ SetProperty("Updated", lastUpdated.IsValid() ? lastUpdated.GetAsLocalizedDateTime()
+ : g_localizeStrings.Get(21337));
+}
+
+class UpdateAddons : public IRunnable
+{
+ void Run() override
+ {
+ for (const auto& addon : CServiceBroker::GetAddonMgr().GetAvailableUpdates())
+ CAddonInstaller::GetInstance().InstallOrUpdate(addon->ID(), BackgroundJob::CHOICE_YES,
+ ModalJob::CHOICE_NO);
+ }
+};
+
+class UpdateAllowedAddons : public IRunnable
+{
+ void Run() override
+ {
+ for (const auto& addon : CServiceBroker::GetAddonMgr().GetAvailableUpdates())
+ if (CServiceBroker::GetAddonMgr().IsAutoUpdateable(addon->ID()))
+ CAddonInstaller::GetInstance().InstallOrUpdate(addon->ID(), BackgroundJob::CHOICE_YES,
+ ModalJob::CHOICE_NO);
+ }
+};
+
+void CGUIWindowAddonBrowser::OnEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event)
+{
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CGUIWindowAddonBrowser::OnEvent(const ADDON::AddonEvent& event)
+{
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CGUIWindowAddonBrowser::InstallFromZip()
+{
+ using namespace KODI::MESSAGING::HELPERS;
+
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_ADDONS_ALLOW_UNKNOWN_SOURCES))
+ {
+ if (ShowYesNoDialogText(13106, 36617, 186, 10004) == DialogResponse::CHOICE_YES)
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(
+ WINDOW_SETTINGS_SYSTEM, CSettings::SETTING_ADDONS_ALLOW_UNKNOWN_SOURCES);
+ }
+ else
+ {
+ // pop up filebrowser to grab an installed folder
+ VECSOURCES shares = *CMediaSourceSettings::GetInstance().GetSources("files");
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ CServiceBroker::GetMediaManager().GetNetworkLocations(shares);
+ std::string path;
+ if (CGUIDialogFileBrowser::ShowAndGetFile(shares, "*.zip", g_localizeStrings.Get(24041), path))
+ {
+ CAddonInstaller::GetInstance().InstallFromZip(path);
+ }
+ }
+}
+
+bool CGUIWindowAddonBrowser::OnClick(int iItem, const std::string& player)
+{
+ CFileItemPtr item = m_vecItems->Get(iItem);
+ if (item->GetPath() == "addons://install/")
+ {
+ InstallFromZip();
+ return true;
+ }
+ if (item->GetPath() == "addons://update_all/")
+ {
+ UpdateAddons updater;
+ CGUIDialogBusy::Wait(&updater, 100, true);
+ return true;
+ }
+ if (item->GetPath() == "addons://update_allowed/")
+ {
+ UpdateAllowedAddons updater;
+ CGUIDialogBusy::Wait(&updater, 100, true);
+ return true;
+ }
+ if (!item->m_bIsFolder)
+ {
+ // cancel a downloading job
+ if (item->HasProperty("Addon.Downloading"))
+ {
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{24000}, item->GetProperty("Addon.Name"),
+ CVariant{24066}, CVariant{""}))
+ {
+ if (CAddonInstaller::GetInstance().Cancel(item->GetProperty("Addon.ID").asString()))
+ Refresh();
+ }
+ return true;
+ }
+
+ CGUIDialogAddonInfo::ShowForItem(item);
+ return true;
+ }
+ if (item->IsPath("addons://search/"))
+ {
+ Update(item->GetPath());
+ return true;
+ }
+
+ return CGUIMediaWindow::OnClick(iItem, player);
+}
+
+void CGUIWindowAddonBrowser::UpdateButtons()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ SET_CONTROL_SELECTED(GetID(), CONTROL_FOREIGNFILTER,
+ settings->GetBool(CSettings::SETTING_GENERAL_ADDONFOREIGNFILTER));
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BROKENFILTER,
+ settings->GetBool(CSettings::SETTING_GENERAL_ADDONBROKENFILTER));
+ CONTROL_ENABLE(CONTROL_CHECK_FOR_UPDATES);
+ CONTROL_ENABLE(CONTROL_SETTINGS);
+
+ bool allowFilter = CAddonsDirectory::IsRepoDirectory(CURL(m_vecItems->GetPath()));
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_FOREIGNFILTER, allowFilter);
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BROKENFILTER, allowFilter);
+
+ CGUIMediaWindow::UpdateButtons();
+}
+
+static bool IsForeign(const std::string& languages)
+{
+ if (languages.empty())
+ return false;
+
+ for (const auto& lang : StringUtils::Split(languages, " "))
+ {
+ if (lang == "en" || lang == g_langInfo.GetLocale().GetLanguageCode() ||
+ lang == g_langInfo.GetLocale().ToShortString())
+ return false;
+
+ // for backwards compatibility
+ if (lang == "no" && g_langInfo.GetLocale().ToShortString() == "nb_NO")
+ return false;
+ }
+ return true;
+}
+
+bool CGUIWindowAddonBrowser::GetDirectory(const std::string& strDirectory, CFileItemList& items)
+{
+ bool result = CGUIMediaWindow::GetDirectory(strDirectory, items);
+
+ if (result && CAddonsDirectory::IsRepoDirectory(CURL(strDirectory)))
+ {
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_GENERAL_ADDONFOREIGNFILTER))
+ {
+ int i = 0;
+ while (i < items.Size())
+ {
+ auto prop = items[i]->GetProperty("Addon.Language");
+ if (!prop.isNull() && IsForeign(prop.asString()))
+ items.Remove(i);
+ else
+ ++i;
+ }
+ }
+ if (settings->GetBool(CSettings::SETTING_GENERAL_ADDONBROKENFILTER))
+ {
+ for (int i = items.Size() - 1; i >= 0; i--)
+ {
+ if (items[i]->GetAddonInfo() &&
+ items[i]->GetAddonInfo()->LifecycleState() == AddonLifecycleState::BROKEN)
+ {
+ //check if it's installed
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(items[i]->GetProperty("Addon.ID").asString(),
+ addon, OnlyEnabled::CHOICE_YES))
+ items.Remove(i);
+ }
+ }
+ }
+ }
+
+ for (int i = 0; i < items.Size(); ++i)
+ UpdateStatus(items[i]);
+
+ return result;
+}
+
+void CGUIWindowAddonBrowser::UpdateStatus(const CFileItemPtr& item)
+{
+ if (!item || item->m_bIsFolder)
+ return;
+
+ unsigned int percent;
+ bool downloadFinshed;
+ if (CAddonInstaller::GetInstance().GetProgress(item->GetProperty("Addon.ID").asString(), percent,
+ downloadFinshed))
+ {
+ std::string progress = StringUtils::Format(
+ !downloadFinshed ? g_localizeStrings.Get(24042) : g_localizeStrings.Get(24044), percent);
+ item->SetProperty("Addon.Status", progress);
+ item->SetProperty("Addon.Downloading", true);
+ }
+ else
+ item->ClearProperty("Addon.Downloading");
+}
+
+bool CGUIWindowAddonBrowser::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ if (!CGUIMediaWindow::Update(strDirectory, updateFilterPath))
+ return false;
+
+ m_thumbLoader.Load(*m_vecItems);
+
+ return true;
+}
+
+int CGUIWindowAddonBrowser::SelectAddonID(AddonType type,
+ std::string& addonID,
+ bool showNone /* = false */,
+ bool showDetails /* = true */,
+ bool showInstalled /* = true */,
+ bool showInstallable /*= false */,
+ bool showMore /* = true */)
+{
+ std::vector<AddonType> types;
+ types.push_back(type);
+ return SelectAddonID(types, addonID, showNone, showDetails, showInstalled, showInstallable,
+ showMore);
+}
+
+int CGUIWindowAddonBrowser::SelectAddonID(AddonType type,
+ std::vector<std::string>& addonIDs,
+ bool showNone /* = false */,
+ bool showDetails /* = true */,
+ bool multipleSelection /* = true */,
+ bool showInstalled /* = true */,
+ bool showInstallable /* = false */,
+ bool showMore /* = true */)
+{
+ std::vector<AddonType> types;
+ types.push_back(type);
+ return SelectAddonID(types, addonIDs, showNone, showDetails, multipleSelection, showInstalled,
+ showInstallable, showMore);
+}
+
+int CGUIWindowAddonBrowser::SelectAddonID(const std::vector<AddonType>& types,
+ std::string& addonID,
+ bool showNone /* = false */,
+ bool showDetails /* = true */,
+ bool showInstalled /* = true */,
+ bool showInstallable /* = false */,
+ bool showMore /* = true */)
+{
+ std::vector<std::string> addonIDs;
+ if (!addonID.empty())
+ addonIDs.push_back(addonID);
+ int retval = SelectAddonID(types, addonIDs, showNone, showDetails, false, showInstalled,
+ showInstallable, showMore);
+ if (!addonIDs.empty())
+ addonID = addonIDs.at(0);
+ else
+ addonID = "";
+ return retval;
+}
+
+int CGUIWindowAddonBrowser::SelectAddonID(const std::vector<AddonType>& types,
+ std::vector<std::string>& addonIDs,
+ bool showNone /* = false */,
+ bool showDetails /* = true */,
+ bool multipleSelection /* = true */,
+ bool showInstalled /* = true */,
+ bool showInstallable /* = false */,
+ bool showMore /* = true */)
+{
+ // if we shouldn't show neither installed nor installable addons the list will be empty
+ if (!showInstalled && !showInstallable)
+ return -1;
+
+ // can't show the "Get More" button if we already show installable addons
+ if (showInstallable)
+ showMore = false;
+
+ CGUIDialogSelect* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!dialog)
+ return -1;
+
+ // get rid of any invalid addon types
+ std::vector<AddonType> validTypes(types.size());
+ std::copy_if(types.begin(), types.end(), validTypes.begin(),
+ [](AddonType type) { return type != AddonType::UNKNOWN; });
+
+ if (validTypes.empty())
+ return -1;
+
+ // get all addons to show
+ VECADDONS addons;
+ if (showInstalled)
+ {
+ for (std::vector<AddonType>::const_iterator type = validTypes.begin(); type != validTypes.end();
+ ++type)
+ {
+ VECADDONS typeAddons;
+ if (*type == AddonType::AUDIO)
+ CAddonsDirectory::GetScriptsAndPlugins("audio", typeAddons);
+ else if (*type == AddonType::EXECUTABLE)
+ CAddonsDirectory::GetScriptsAndPlugins("executable", typeAddons);
+ else if (*type == AddonType::IMAGE)
+ CAddonsDirectory::GetScriptsAndPlugins("image", typeAddons);
+ else if (*type == AddonType::VIDEO)
+ CAddonsDirectory::GetScriptsAndPlugins("video", typeAddons);
+ else if (*type == AddonType::GAME)
+ CAddonsDirectory::GetScriptsAndPlugins("game", typeAddons);
+ else
+ CServiceBroker::GetAddonMgr().GetAddons(typeAddons, *type);
+
+ addons.insert(addons.end(), typeAddons.begin(), typeAddons.end());
+ }
+ }
+
+ if (showInstallable || showMore)
+ {
+ VECADDONS installableAddons;
+ if (CServiceBroker::GetAddonMgr().GetInstallableAddons(installableAddons))
+ {
+ for (auto addon = installableAddons.begin(); addon != installableAddons.end();)
+ {
+ AddonPtr pAddon = *addon;
+
+ // check if the addon matches one of the provided addon types
+ bool matchesType = false;
+ for (std::vector<AddonType>::const_iterator type = validTypes.begin();
+ type != validTypes.end(); ++type)
+ {
+ if (pAddon->HasType(*type))
+ {
+ matchesType = true;
+ break;
+ }
+ }
+
+ if (matchesType)
+ {
+ ++addon;
+ continue;
+ }
+
+ addon = installableAddons.erase(addon);
+ }
+
+ if (showInstallable)
+ addons.insert(addons.end(), installableAddons.begin(), installableAddons.end());
+ else if (showMore)
+ showMore = !installableAddons.empty();
+ }
+ }
+
+ if (addons.empty() && !showNone)
+ return -1;
+
+ // turn the addons into items
+ std::map<std::string, AddonPtr> addonMap;
+ CFileItemList items;
+ for (const auto& addon : addons)
+ {
+ const CFileItemPtr item(CAddonsDirectory::FileItemFromAddon(addon, addon->ID()));
+ item->SetLabel2(addon->Summary());
+ if (!items.Contains(item->GetPath()))
+ {
+ items.Add(item);
+ addonMap.insert(std::make_pair(item->GetPath(), addon));
+ }
+ }
+
+ if (items.IsEmpty() && !showNone)
+ return -1;
+
+ std::string heading;
+ for (std::vector<AddonType>::const_iterator type = validTypes.begin(); type != validTypes.end();
+ ++type)
+ {
+ if (!heading.empty())
+ heading += ", ";
+ heading += CAddonInfo::TranslateType(*type, true);
+ }
+
+ dialog->SetHeading(CVariant{std::move(heading)});
+ dialog->Reset();
+ dialog->SetUseDetails(showDetails);
+
+ if (multipleSelection)
+ {
+ showNone = false;
+ showMore = false;
+ dialog->EnableButton(true, 186);
+ }
+ else if (showMore)
+ dialog->EnableButton(true, 21452);
+
+ if (showNone)
+ {
+ CFileItemPtr item(new CFileItem("", false));
+ item->SetLabel(g_localizeStrings.Get(231));
+ item->SetLabel2(g_localizeStrings.Get(24040));
+ item->SetArt("icon", "DefaultAddonNone.png");
+ item->SetSpecialSort(SortSpecialOnTop);
+ items.Add(item);
+ }
+ items.Sort(SortByLabel, SortOrderAscending);
+
+ if (!addonIDs.empty())
+ {
+ for (std::vector<std::string>::const_iterator it = addonIDs.begin(); it != addonIDs.end(); ++it)
+ {
+ CFileItemPtr item = items.Get(*it);
+ if (item)
+ item->Select(true);
+ }
+ }
+ dialog->SetItems(items);
+ dialog->SetMultiSelection(multipleSelection);
+ dialog->Open();
+
+ // if the "Get More" button has been pressed and we haven't shown the
+ // installable addons so far show a list of installable addons
+ if (showMore && dialog->IsButtonPressed())
+ return SelectAddonID(types, addonIDs, showNone, showDetails, multipleSelection, false, true,
+ false);
+
+ if (!dialog->IsConfirmed())
+ return 0;
+
+ addonIDs.clear();
+ for (int i : dialog->GetSelectedItems())
+ {
+ const CFileItemPtr& item = items.Get(i);
+
+ // check if one of the selected addons needs to be installed
+ if (showInstallable)
+ {
+ std::map<std::string, AddonPtr>::const_iterator itAddon = addonMap.find(item->GetPath());
+ if (itAddon != addonMap.end())
+ {
+ const AddonPtr& addon = itAddon->second;
+
+ // if the addon isn't installed we need to install it
+ if (!CServiceBroker::GetAddonMgr().IsAddonInstalled(addon->ID()))
+ {
+ AddonPtr installedAddon;
+ if (!CAddonInstaller::GetInstance().InstallModal(addon->ID(), installedAddon,
+ InstallModalPrompt::CHOICE_NO))
+ continue;
+ }
+
+ // if the addon is disabled we need to enable it
+ if (CServiceBroker::GetAddonMgr().IsAddonDisabled(addon->ID()))
+ CServiceBroker::GetAddonMgr().EnableAddon(addon->ID());
+ }
+ }
+
+ addonIDs.push_back(item->GetPath());
+ }
+ return 1;
+}
+
+std::string CGUIWindowAddonBrowser::GetStartFolder(const std::string& dir)
+{
+ if (StringUtils::StartsWith(dir, "addons://"))
+ {
+ if (StringUtils::StartsWith(dir, "addons://default_binary_addons_source/"))
+ {
+ const bool all = CServiceBroker::GetPlatform().SupportsUserInstalledBinaryAddons();
+ std::string startDir = dir;
+ StringUtils::Replace(startDir, "/default_binary_addons_source/", all ? "/all/" : "/user/");
+ return startDir;
+ }
+ else
+ return dir;
+ }
+
+ return CGUIMediaWindow::GetStartFolder(dir);
+}
diff --git a/xbmc/addons/gui/GUIWindowAddonBrowser.h b/xbmc/addons/gui/GUIWindowAddonBrowser.h
new file mode 100644
index 0000000..7f605ee
--- /dev/null
+++ b/xbmc/addons/gui/GUIWindowAddonBrowser.h
@@ -0,0 +1,115 @@
+/*
+ * 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 "ThumbLoader.h"
+#include "addons/RepositoryUpdater.h"
+#include "windows/GUIMediaWindow.h"
+
+#include <string>
+#include <vector>
+
+class CFileItemList;
+
+namespace ADDON
+{
+enum class AddonType;
+struct AddonEvent;
+}
+
+class CGUIWindowAddonBrowser : public CGUIMediaWindow
+{
+public:
+ CGUIWindowAddonBrowser(void);
+ ~CGUIWindowAddonBrowser(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+
+ /*!
+ * @brief Popup a selection dialog with a list of addons of the given type
+ *
+ * @param[in] type the type of addon wanted
+ * @param[in] addonID [in/out] the addon ID of the (pre) selected item
+ * @param[in] showNone whether there should be a "None" item in the list (defaults to false)
+ * @param[in] showDetails whether to show details of the addons or not
+ * @param[in] showInstalled whether installed addons should be in the list
+ * @param[in] showInstallable whether installable addons should be in the list
+ * @param[in] showMore whether to show the "Get More" button (only makes sense
+ * if showInstalled is true and showInstallable is false)
+ * @return 1 if an addon was selected or multiple selection was specified, 2 if
+ * "Get More" was chosen, 0 if the selection process was cancelled or -1
+ * if an error occurred or
+ */
+ static int SelectAddonID(ADDON::AddonType type,
+ std::string& addonID,
+ bool showNone = false,
+ bool showDetails = true,
+ bool showInstalled = true,
+ bool showInstallable = false,
+ bool showMore = true);
+ static int SelectAddonID(const std::vector<ADDON::AddonType>& types,
+ std::string& addonID,
+ bool showNone = false,
+ bool showDetails = true,
+ bool showInstalled = true,
+ bool showInstallable = false,
+ bool showMore = true);
+ /*!
+ * @brief Popup a selection dialog with a list of addons of the given type
+ *
+ * @param[in] type the type of addon wanted
+ * @param[in] addonIDs [in/out] array of (pre) selected addon IDs
+ * @param[in] showNone whether there should be a "None" item in the list (defaults to false)
+ * @param[in] showDetails whether to show details of the addons or not
+ * @param[in] multipleSelection allow selection of multiple addons, if set to
+ * true showNone will automatically switch to false
+ * @param[in] showInstalled whether installed addons should be in the list
+ * @param[in] showInstallable whether installable addons should be in the list
+ * @param[in] showMore whether to show the "Get More" button (only makes sense
+ * if showInstalled is true and showInstallable is false)
+ * @return 1 if an addon was selected or multiple selection was specified, 2 if
+ * "Get More" was chosen, 0 if the selection process was cancelled or -1
+ * if an error occurred or
+ */
+ static int SelectAddonID(ADDON::AddonType type,
+ std::vector<std::string>& addonIDs,
+ bool showNone = false,
+ bool showDetails = true,
+ bool multipleSelection = true,
+ bool showInstalled = true,
+ bool showInstallable = false,
+ bool showMore = true);
+ static int SelectAddonID(const std::vector<ADDON::AddonType>& types,
+ std::vector<std::string>& addonIDs,
+ bool showNone = false,
+ bool showDetails = true,
+ bool multipleSelection = true,
+ bool showInstalled = true,
+ bool showInstallable = false,
+ bool showMore = true);
+
+ bool UseFileDirectories() override { return false; }
+
+ static void InstallFromZip();
+
+protected:
+ bool OnClick(int iItem, const std::string& player = "") override;
+ void UpdateButtons() override;
+ bool GetDirectory(const std::string& strDirectory, CFileItemList& items) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ std::string GetStartFolder(const std::string& dir) override;
+
+ std::string GetRootPath() const override { return "addons://"; }
+
+private:
+ void SetProperties();
+ void UpdateStatus(const CFileItemPtr& item);
+ void OnEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event);
+ void OnEvent(const ADDON::AddonEvent& event);
+ CProgramThumbLoader m_thumbLoader;
+};
diff --git a/xbmc/addons/gui/skin/CMakeLists.txt b/xbmc/addons/gui/skin/CMakeLists.txt
new file mode 100644
index 0000000..916cd94
--- /dev/null
+++ b/xbmc/addons/gui/skin/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES SkinTimer.cpp
+ SkinTimerManager.cpp)
+
+set(HEADERS SkinTimer.h
+ SkinTimerManager.h)
+
+core_add_library(addons_gui_skin)
diff --git a/xbmc/addons/gui/skin/SkinTimer.cpp b/xbmc/addons/gui/skin/SkinTimer.cpp
new file mode 100644
index 0000000..c4e88b7
--- /dev/null
+++ b/xbmc/addons/gui/skin/SkinTimer.cpp
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2022 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 "SkinTimer.h"
+
+#include "interfaces/info/Info.h"
+
+CSkinTimer::CSkinTimer(const std::string& name,
+ const INFO::InfoPtr& startCondition,
+ const INFO::InfoPtr& resetCondition,
+ const INFO::InfoPtr& stopCondition,
+ const CGUIAction& startActions,
+ const CGUIAction& stopActions,
+ bool resetOnStart)
+ : m_name{name},
+ m_startCondition{startCondition},
+ m_resetCondition{resetCondition},
+ m_stopCondition{stopCondition},
+ m_startActions{startActions},
+ m_stopActions{stopActions},
+ m_resetOnStart{resetOnStart}
+{
+}
+
+void CSkinTimer::Start()
+{
+ if (m_resetOnStart)
+ {
+ CStopWatch::StartZero();
+ }
+ else
+ {
+ CStopWatch::Start();
+ }
+ OnStart();
+}
+
+void CSkinTimer::Reset()
+{
+ CStopWatch::Reset();
+}
+
+void CSkinTimer::Stop()
+{
+ CStopWatch::Stop();
+ OnStop();
+}
+
+bool CSkinTimer::VerifyStartCondition() const
+{
+ return m_startCondition && m_startCondition->Get(INFO::DEFAULT_CONTEXT);
+}
+
+bool CSkinTimer::VerifyResetCondition() const
+{
+ return m_resetCondition && m_resetCondition->Get(INFO::DEFAULT_CONTEXT);
+}
+
+bool CSkinTimer::VerifyStopCondition() const
+{
+ return m_stopCondition && m_stopCondition->Get(INFO::DEFAULT_CONTEXT);
+}
+
+INFO::InfoPtr CSkinTimer::GetStartCondition() const
+{
+ return m_startCondition;
+}
+
+INFO::InfoPtr CSkinTimer::GetResetCondition() const
+{
+ return m_resetCondition;
+}
+
+INFO::InfoPtr CSkinTimer::GetStopCondition() const
+{
+ return m_stopCondition;
+}
+
+void CSkinTimer::OnStart()
+{
+ if (m_startActions.HasAnyActions())
+ {
+ m_startActions.ExecuteActions();
+ }
+}
+
+void CSkinTimer::OnStop()
+{
+ if (m_stopActions.HasAnyActions())
+ {
+ m_stopActions.ExecuteActions();
+ }
+}
diff --git a/xbmc/addons/gui/skin/SkinTimer.h b/xbmc/addons/gui/skin/SkinTimer.h
new file mode 100644
index 0000000..d838ef0
--- /dev/null
+++ b/xbmc/addons/gui/skin/SkinTimer.h
@@ -0,0 +1,110 @@
+/*
+ * 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 "guilib/GUIAction.h"
+#include "interfaces/info/InfoExpression.h"
+#include "utils/Stopwatch.h"
+
+#include <memory>
+#include <string>
+
+class TiXmlElement;
+
+/*! \brief Skin timers are skin objects that dependent on time and can be fully controlled from skins either using boolean
+ * conditions or builtin functions. This class represents the Skin Timer object.
+ * \sa Skin_Timers
+ */
+class CSkinTimer : public CStopWatch
+{
+public:
+ /*! \brief Skin timer constructor
+ * \param name - the name of the timer
+ * \param startCondition - the boolean info expression to start the timer (may be null)
+ * \param resetCondition - the boolean info expression to reset the timer (may be null)
+ * \param stopCondition - the boolean info expression to stop the timer (may be null)
+ * \param startActions - the builtin functions to execute on timer start (actions may be empty)
+ * \param stopActions - the builtin functions to execute on timer stop (actions may be empty)
+ * \param resetOnStart - if the timer should be reset when started (i.e. start from zero if true or resumed if false)
+ */
+ CSkinTimer(const std::string& name,
+ const INFO::InfoPtr& startCondition,
+ const INFO::InfoPtr& resetCondition,
+ const INFO::InfoPtr& stopCondition,
+ const CGUIAction& startActions,
+ const CGUIAction& stopActions,
+ bool resetOnStart);
+
+ /*! \brief Default skin timer destructor */
+ virtual ~CSkinTimer() = default;
+
+ /*! \brief Start the skin timer */
+ void Start();
+
+ /*! \brief Resets the skin timer so that the elapsed time of the timer is 0 */
+ void Reset();
+
+ /*! \brief stops the skin timer */
+ void Stop();
+
+ /*! \brief Getter for the timer start boolean condition/expression
+ * \return the start boolean condition/expression (may be null)
+ */
+ INFO::InfoPtr GetStartCondition() const;
+
+ /*! \brief Getter for the timer reset boolean condition/expression
+ * \return the reset boolean condition/expression (may be null)
+ */
+ INFO::InfoPtr GetResetCondition() const;
+
+ /*! \brief Getter for the timer start boolean condition/expression
+ * \return the start boolean condition/expression (may be null)
+ */
+ INFO::InfoPtr GetStopCondition() const;
+
+ /*! \brief Evaluates the timer start boolean info expression returning the respective result.
+ * \details Called from the skin timer manager to check if the timer should be started
+ * \return true if the condition is true, false otherwise
+ */
+ bool VerifyStartCondition() const;
+
+ /*! \brief Evaluates the timer reset boolean info expression returning the respective result.
+ * \details Called from the skin timer manager to check if the timer should be reset to 0
+ * \return true if the condition is true, false otherwise
+ */
+ bool VerifyResetCondition() const;
+
+ /*! \brief Evaluates the timer stop boolean info expression returning the respective result.
+ * \details Called from the skin timer manager to check if the timer should be stopped
+ * \return true if the condition is true, false otherwise
+ */
+ bool VerifyStopCondition() const;
+
+private:
+ /*! \brief Called when this timer is started */
+ void OnStart();
+
+ /*! \brief Called when this timer is stopped */
+ void OnStop();
+
+ /*! The name of the skin timer */
+ std::string m_name;
+ /*! The info boolean expression that automatically starts the timer if evaluated true */
+ INFO::InfoPtr m_startCondition;
+ /*! The info boolean expression that automatically resets the timer if evaluated true */
+ INFO::InfoPtr m_resetCondition;
+ /*! The info boolean expression that automatically stops the timer if evaluated true */
+ INFO::InfoPtr m_stopCondition;
+ /*! The builtin functions to be executed when the timer is started */
+ CGUIAction m_startActions;
+ /*! The builtin functions to be executed when the timer is stopped */
+ CGUIAction m_stopActions;
+ /*! if the timer should be reset on start (or just resumed) */
+ bool m_resetOnStart{false};
+};
diff --git a/xbmc/addons/gui/skin/SkinTimerManager.cpp b/xbmc/addons/gui/skin/SkinTimerManager.cpp
new file mode 100644
index 0000000..663f5aa
--- /dev/null
+++ b/xbmc/addons/gui/skin/SkinTimerManager.cpp
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2022 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 "SkinTimerManager.h"
+
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIAction.h"
+#include "guilib/GUIComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <chrono>
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+void CSkinTimerManager::LoadTimers(const std::string& path)
+{
+ CXBMCTinyXML doc;
+ if (!doc.LoadFile(path))
+ {
+ CLog::LogF(LOGWARNING, "Could not load timers file {}: {} (row: {}, col: {})", path,
+ doc.ErrorDesc(), doc.ErrorRow(), doc.ErrorCol());
+ return;
+ }
+
+ TiXmlElement* root = doc.RootElement();
+ if (!root || !StringUtils::EqualsNoCase(root->Value(), "timers"))
+ {
+ CLog::LogF(LOGERROR, "Error loading timers file {}: Root element <timers> required.", path);
+ return;
+ }
+
+ const TiXmlElement* timerNode = root->FirstChildElement("timer");
+ while (timerNode)
+ {
+ LoadTimerInternal(timerNode);
+ timerNode = timerNode->NextSiblingElement("timer");
+ }
+}
+
+void CSkinTimerManager::LoadTimerInternal(const TiXmlElement* node)
+{
+ if ((!node->FirstChild("name") || !node->FirstChild("name")->FirstChild() ||
+ node->FirstChild("name")->FirstChild()->ValueStr().empty()))
+ {
+ CLog::LogF(LOGERROR, "Missing required field name for valid skin. Ignoring timer.");
+ return;
+ }
+
+ std::string timerName = node->FirstChild("name")->FirstChild()->Value();
+ if (m_timers.count(timerName) > 0)
+ {
+ CLog::LogF(LOGWARNING,
+ "Ignoring timer with name {} - another timer with the same name already exists",
+ timerName);
+ return;
+ }
+
+ // timer start
+ INFO::InfoPtr startInfo{nullptr};
+ bool resetOnStart{false};
+ if (node->FirstChild("start") && node->FirstChild("start")->FirstChild() &&
+ !node->FirstChild("start")->FirstChild()->ValueStr().empty())
+ {
+ startInfo = CServiceBroker::GetGUI()->GetInfoManager().Register(
+ node->FirstChild("start")->FirstChild()->ValueStr());
+ // check if timer needs to be reset after start
+ if (node->FirstChildElement("start")->Attribute("reset") &&
+ StringUtils::EqualsNoCase(node->FirstChildElement("start")->Attribute("reset"), "true"))
+ {
+ resetOnStart = true;
+ }
+ }
+
+ // timer reset
+ INFO::InfoPtr resetInfo{nullptr};
+ if (node->FirstChild("reset") && node->FirstChild("reset")->FirstChild() &&
+ !node->FirstChild("reset")->FirstChild()->ValueStr().empty())
+ {
+ resetInfo = CServiceBroker::GetGUI()->GetInfoManager().Register(
+ node->FirstChild("reset")->FirstChild()->ValueStr());
+ }
+ // timer stop
+ INFO::InfoPtr stopInfo{nullptr};
+ if (node->FirstChild("stop") && node->FirstChild("stop")->FirstChild() &&
+ !node->FirstChild("stop")->FirstChild()->ValueStr().empty())
+ {
+ stopInfo = CServiceBroker::GetGUI()->GetInfoManager().Register(
+ node->FirstChild("stop")->FirstChild()->ValueStr());
+ }
+
+ // process onstart actions
+ CGUIAction startActions;
+ startActions.EnableSendThreadMessageMode();
+ const TiXmlElement* onStartElement = node->FirstChildElement("onstart");
+ while (onStartElement)
+ {
+ if (onStartElement->FirstChild())
+ {
+ const std::string conditionalActionAttribute =
+ onStartElement->Attribute("condition") != nullptr ? onStartElement->Attribute("condition")
+ : "";
+ startActions.Append(CGUIAction::CExecutableAction{conditionalActionAttribute,
+ onStartElement->FirstChild()->Value()});
+ }
+ onStartElement = onStartElement->NextSiblingElement("onstart");
+ }
+
+ // process onstop actions
+ CGUIAction stopActions;
+ stopActions.EnableSendThreadMessageMode();
+ const TiXmlElement* onStopElement = node->FirstChildElement("onstop");
+ while (onStopElement)
+ {
+ if (onStopElement->FirstChild())
+ {
+ const std::string conditionalActionAttribute =
+ onStopElement->Attribute("condition") != nullptr ? onStopElement->Attribute("condition")
+ : "";
+ stopActions.Append(CGUIAction::CExecutableAction{conditionalActionAttribute,
+ onStopElement->FirstChild()->Value()});
+ }
+ onStopElement = onStopElement->NextSiblingElement("onstop");
+ }
+
+ m_timers[timerName] = std::make_unique<CSkinTimer>(CSkinTimer(
+ timerName, startInfo, resetInfo, stopInfo, startActions, stopActions, resetOnStart));
+}
+
+bool CSkinTimerManager::TimerIsRunning(const std::string& timer) const
+{
+ if (m_timers.count(timer) == 0)
+ {
+ CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer);
+ return false;
+ }
+ return m_timers.at(timer)->IsRunning();
+}
+
+float CSkinTimerManager::GetTimerElapsedSeconds(const std::string& timer) const
+{
+ if (m_timers.count(timer) == 0)
+ {
+ CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer);
+ return 0;
+ }
+ return m_timers.at(timer)->GetElapsedSeconds();
+}
+
+void CSkinTimerManager::TimerStart(const std::string& timer) const
+{
+ if (m_timers.count(timer) == 0)
+ {
+ CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer);
+ return;
+ }
+ m_timers.at(timer)->Start();
+}
+
+void CSkinTimerManager::TimerStop(const std::string& timer) const
+{
+ if (m_timers.count(timer) == 0)
+ {
+ CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer);
+ return;
+ }
+ m_timers.at(timer)->Stop();
+}
+
+void CSkinTimerManager::Stop()
+{
+ // skintimers, as infomanager clients register info conditions/expressions in the infomanager.
+ // The infomanager is linked to skins, being initialized or cleared when
+ // skins are loaded (or unloaded). All the registered boolean conditions from
+ // skin timers will end up being removed when the skin is unloaded. However, to
+ // self-contain this component unregister them all here.
+ for (auto const& [key, val] : m_timers)
+ {
+ const std::unique_ptr<CSkinTimer>::pointer timer = val.get();
+ if (timer->GetStartCondition())
+ {
+ CServiceBroker::GetGUI()->GetInfoManager().UnRegister(timer->GetStartCondition());
+ }
+ if (timer->GetStopCondition())
+ {
+ CServiceBroker::GetGUI()->GetInfoManager().UnRegister(timer->GetStopCondition());
+ }
+ if (timer->GetResetCondition())
+ {
+ CServiceBroker::GetGUI()->GetInfoManager().UnRegister(timer->GetResetCondition());
+ }
+ }
+ m_timers.clear();
+}
+
+void CSkinTimerManager::Process()
+{
+ for (const auto& [key, val] : m_timers)
+ {
+ const std::unique_ptr<CSkinTimer>::pointer timer = val.get();
+ if (!timer->IsRunning() && timer->VerifyStartCondition())
+ {
+ timer->Start();
+ }
+ else if (timer->IsRunning() && timer->VerifyStopCondition())
+ {
+ timer->Stop();
+ }
+ if (timer->GetElapsedSeconds() > 0 && timer->VerifyResetCondition())
+ {
+ timer->Reset();
+ }
+ }
+}
diff --git a/xbmc/addons/gui/skin/SkinTimerManager.h b/xbmc/addons/gui/skin/SkinTimerManager.h
new file mode 100644
index 0000000..fdf44d1
--- /dev/null
+++ b/xbmc/addons/gui/skin/SkinTimerManager.h
@@ -0,0 +1,77 @@
+/*
+ * 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 "SkinTimer.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+/*! \brief CSkinTimerManager is the container and manager for Skin timers. Its role is that of
+ * checking if the timer boolean conditions are valid, start or stop timers and execute the respective
+ * builtin actions linked to the timer lifecycle
+ * \note This component should only be called by the main/rendering thread
+ * \sa Skin_Timers
+ * \sa CSkinTimer
+ */
+class CSkinTimerManager
+{
+public:
+ /*! \brief Skin timer manager constructor */
+ CSkinTimerManager() = default;
+
+ /*! \brief Default skin timer manager destructor */
+ ~CSkinTimerManager() = default;
+
+ /*! \brief Loads all the skin timers
+ * \param path - the path for the skin Timers.xml file
+ */
+ void LoadTimers(const std::string& path);
+
+ /*! \brief Stops the manager */
+ void Stop();
+
+ /*! \brief Checks if the timer with name `timer` is running
+ \param timer the name of the skin timer
+ \return true if the given timer exists and is running, false otherwise
+ */
+ bool TimerIsRunning(const std::string& timer) const;
+
+ /*! \brief Get the elapsed seconds since the timer with name `timer` was started
+ \param timer the name of the skin timer
+ \return the elapsed time in seconds the given timer is running (0 if not running or if it does not exist)
+ */
+ float GetTimerElapsedSeconds(const std::string& timer) const;
+
+ /*! \brief Starts/Enables a given skin timer
+ \param timer the name of the skin timer
+ */
+ void TimerStart(const std::string& timer) const;
+
+ /*! \brief Stops/Disables a given skin timer
+ \param timer the name of the skin timer
+ */
+ void TimerStop(const std::string& timer) const;
+
+ // CThread methods
+
+ /*! \brief Run the main manager processing loop */
+ void Process();
+
+private:
+ /*! \brief Loads a specific timer
+ * \note Called internally from LoadTimers
+ * \param node - the XML representation of a skin timer object
+ */
+ void LoadTimerInternal(const TiXmlElement* node);
+
+ /*! Container for the skin timers */
+ std::map<std::string, std::unique_ptr<CSkinTimer>> m_timers;
+};
diff --git a/xbmc/addons/gui/skin/SkinTimers.dox b/xbmc/addons/gui/skin/SkinTimers.dox
new file mode 100644
index 0000000..0d6f171
--- /dev/null
+++ b/xbmc/addons/gui/skin/SkinTimers.dox
@@ -0,0 +1,164 @@
+/*!
+
+\page Skin_Timers Skin Timers
+\brief **Programatic time-based resources for Skins**
+
+\tableofcontents
+
+--------------------------------------------------------------------------------
+\section Skin_Timers_sect1 Description
+
+Skin timers are skin resources that are dependent on time and can be fully controlled from skins either using
+\link page_List_of_built_in_functions **Builtin functions**\endlink or
+\link modules__infolabels_boolean_conditions **Infolabels and Boolean conditions**\endlink. One can see them
+as stopwatches that can be activated and deactivated automatically depending on the value of info expressions or simply activated/deactivated
+manually from builtins.
+The framework was created to allow skins to control the visibility of windows (and controls) depending on
+the elapsed time of timers the skin defines. Skin timers allow multiple use cases in skins, previously only available via the execution
+of python scripts:
+- Closing a specific window after x seconds have elapsed
+- Controlling the visibility of a group (or triggering an animation) depending on the elapsed time of a given timer
+- Defining a buffer time window that is kept activated for a short period of time (e.g. keep controls visible for x seconds after a player seek)
+- Executing timed actions (on timer stop or timer start)
+- etc
+
+Skin timers are defined in the `Timers.xml` file within the xml directory of the skin. The file has the following "schema":
+
+~~~~~~~~~~~~~{.xml}
+<timers>
+ <timer>...</timer>
+ <timer>...</timer>
+</timers>
+~~~~~~~~~~~~~
+
+see \link Skin_Timers_sect2 the examples section\endlink and \link Skin_Timers_sect3 the list of available tags\endlink for concrete details.
+
+\skinning_v20 Added skin timers
+
+--------------------------------------------------------------------------------
+\section Skin_Timers_sect2 Examples
+
+The following example illustrates the simplest possible skin timer. This timer is completely manual (it has to be manually started and stopped):
+
+~~~~~~~~~~~~~{.xml}
+<timer>
+ <name>mymanualtimer</name>
+ <description>100% manual timer</description>
+</timer>
+~~~~~~~~~~~~~
+
+This timer can be controlled from your skin by executing the \link Builtin_SkinStartTimer `Skin.TimerStart(mymanualtimer)` builtin\endlink or
+\link Builtin_SkinStopTimer `Skin.TimerStop(mymanualtimer)` builtin\endlink. You can define the visibility of skin elements based on the internal
+properties of the timer, such as the fact that the timer is active/running using \link Skin_TimerIsRunning `Skin.TimerIsRunning(mymanualtimer)` info\endlink
+or depending on the elapsed time (e.g. 5 seconds) using the \link Skin_TimerElapsedSecs Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(mymanualtimer),5) info\endlink.
+
+The following timer is a variation of the previous timer but with the added ability of being automatically stopped by the skinning engine after a maximum of elapsed
+5 seconds without having to issue the `Skin.TimerStop(mymanualtimer)` builtin:
+
+~~~~~~~~~~~~~{.xml}
+<timer>
+ <name>mymanualautocloseabletimer</name>
+ <description>100% manual autocloseable timer</description>
+ <stop>Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(mymanualautocloseabletimer),5)</stop>
+</timer>
+~~~~~~~~~~~~~
+
+This type of timer is particularly useful if you want to automatically close a specific window (or triggering a close animation) after x time has elapsed,
+while guaranteeing the timer is also stopped. See the example below:
+
+~~~~~~~~~~~~~{.xml}
+<?xml version="1.0" encoding="utf-8"?>
+<window type="dialog" id="1109">
+ <onload>Skin.TimerStart(mymanualautocloseabletimer)</onload>
+ ...
+ <controls>
+ <control type="group">
+ <animation effect="slide" start="0,0" end="0,-80" time="300" condition="Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(mymanualautocloseabletimer),5)">Conditional</animation>
+ ...
+ </control>
+ </controls>
+</window>
+~~~~~~~~~~~~~
+
+The following timer presents a notification (for 1 sec) whenever the timer is activated or deactivated:
+
+~~~~~~~~~~~~~{.xml}
+<timer>
+ <name>manualtimerwithactions</name>
+ <description>100% manual timer with actions</description>
+ <onstart>Notification(skintimer, My timer was started, 1000)</onstart>
+ <onstop>Notification(skintimer, My timer was stopped, 1000)</onstop>
+</timer>
+~~~~~~~~~~~~~
+
+The following timer is an example of a completely automatic timer. The timer is automatically activated or deactivated based on the value
+of boolean info expressions. In this particular example, the timer is automatically started whenever the Player is playing a file (if not already running). It is stopped if
+there is no file being played (and of course if previously running). Since the timer can be activated/deactivated multiple times, `reset="true"` ensures the timer is
+always reset to 0 on each start operation. Whenever the timer is started or stopped, notifications are issued.
+
+~~~~~~~~~~~~~{.xml}
+<timer>
+ <name>myautomatictimer</name>
+ <description>Player state checker</description>
+ <start reset="true">Player.Playing</start>
+ <stop>!Player.Playing</stop>
+ <onstart>Notification(skintimer, Player has started playing a file, 1000)</onstart>
+ <onstop>Notification(skintimer, Player is no longer playing a file, 1000)</onstop>
+</timer>
+~~~~~~~~~~~~~
+
+In certain situations you might want to reset your timer without having to stop and start. For instance, if you want to stop the timer after 5 seconds
+but have the timer resetting to 0 seconds if the user provides some input to Kodi. For such cases the `<reset/>` condition can be used:
+
+~~~~~~~~~~~~~{.xml}
+<timer>
+ <name>windowtimer</name>
+ <description>Reset on idle</description>
+ <start reset="true">Window.IsActive(mywindow)</start>
+ <reset>Window.IsActive(mywindow) + !System.IdleTime(1) + Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(windowtimer), 1)</reset>
+ <stop>!Window.IsActive(mywindow) + Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(windowtimer), 5)</stop>
+ <onstop>Dialog.Close(mywindow)</onstop>
+</timer>
+~~~~~~~~~~~~~
+
+Finer conditional granularity can also be applied to the `onstop` or `onstart` actions. This allows the skinner to create generic timers which respect a
+limited set of conditions but trigger different actions depending on a condition applied only to the action.
+The following timer plays the trailer of a given item when the user is in the videos window, the item has a trailer, the player is not playing and the
+global idle time is greater than 3 seconds.
+As you can see, the first action (notification) is triggered for any item. The actual playback, on the other hand, will only play if the focused
+item has the label "MyAwesomeMovie".
+
+~~~~~~~~~~~~~{.xml}
+<timer>
+ <name>trailer_autoplay_idle_timer</name>
+ <start reset="true">System.IdleTime(3) + Window.IsVisible(videos) + !Player.HasMedia + !String.IsEmpty(ListItem.Trailer)</start>
+ <onstart>Notification(skintimer try play, $INFO[ListItem.Trailer], 1000)</onstart>
+ <onstart condition="String.IsEqual(ListItem.Label,MyAwesomeMovie)">PlayMedia($INFO[ListItem.Trailer],1,noresume)</onstart>
+</timer>
+~~~~~~~~~~~~~
+
+--------------------------------------------------------------------------------
+\section Skin_Timers_sect3 Available tags
+
+Skin timers have the following available tags:
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| name | The unique name of the timer. The name is used as the id of the timer, hence needs to be unique. <b>(required)</b>
+| description | The description of the timer, a helper string. <b>(optional)</b>
+| start | An info bool expression that the skinning engine should use to automatically start the timer <b>(optional)</b>
+| reset | An info bool expression that the skinning engine should use to automatically reset the timer <b>(optional)</b>
+| stop | An info bool expression that the skinning engine should use to automatically stop the timer <b>(optional)</b>
+| onstart | A builtin function that the skinning engine should execute when the timer is started <b>(optional)</b><b>(can be repeated)</b>. Supports an additional `"condition"` as element attribute.
+| onstop | A builtin function that the skinning engine should execute when the timer is stopped <b>(optional)</b><b>(can be repeated)</b>. Supports an additional `"condition"` as element attribute.
+
+@note If multiple onstart or onstop actions exist, their execution is triggered sequentially.
+@note Both onstart and onstop actions support fine-grained conditional granularity by specifying a "condition" attribute (see the examples above).
+
+--------------------------------------------------------------------------------
+\section Skin_Timers_sect4 See also
+#### Development:
+
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/addons/interfaces/AddonBase.cpp b/xbmc/addons/interfaces/AddonBase.cpp
new file mode 100644
index 0000000..496ca92
--- /dev/null
+++ b/xbmc/addons/interfaces/AddonBase.cpp
@@ -0,0 +1,661 @@
+/*
+ * Copyright (C) 2005-2020 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 "AddonBase.h"
+
+#include "GUIUserMessages.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "addons/settings/AddonSettings.h"
+#include "application/Application.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+// "C" interface addon callback handle classes
+#include "AudioEngine.h"
+#include "Filesystem.h"
+#include "General.h"
+#include "Network.h"
+#include "gui/General.h"
+
+namespace ADDON
+{
+
+std::vector<ADDON_GET_INTERFACE_FN> Interface_Base::s_registeredInterfaces;
+
+bool Interface_Base::InitInterface(CAddonDll* addon,
+ AddonGlobalInterface& addonInterface,
+ KODI_ADDON_INSTANCE_STRUCT* firstKodiInstance)
+{
+ addonInterface = {};
+
+ addonInterface.addonBase = nullptr;
+ addonInterface.globalSingleInstance = nullptr;
+ addonInterface.firstKodiInstance = firstKodiInstance;
+
+ // Create function list from kodi to addon, generated with malloc to have
+ // compatible with other versions
+ addonInterface.toKodi = new AddonToKodiFuncTable_Addon();
+ addonInterface.toKodi->kodiBase = addon;
+ addonInterface.toKodi->addon_log_msg = addon_log_msg;
+ addonInterface.toKodi->free_string = free_string;
+ addonInterface.toKodi->free_string_array = free_string_array;
+
+ addonInterface.toKodi->kodi_addon = new AddonToKodiFuncTable_kodi_addon();
+ addonInterface.toKodi->kodi_addon->get_addon_path = get_addon_path;
+ addonInterface.toKodi->kodi_addon->get_lib_path = get_lib_path;
+ addonInterface.toKodi->kodi_addon->get_user_path = get_user_path;
+ addonInterface.toKodi->kodi_addon->get_temp_path = get_temp_path;
+ addonInterface.toKodi->kodi_addon->get_localized_string = get_localized_string;
+ addonInterface.toKodi->kodi_addon->open_settings_dialog = open_settings_dialog;
+ addonInterface.toKodi->kodi_addon->is_setting_using_default = is_setting_using_default;
+ addonInterface.toKodi->kodi_addon->get_setting_bool = get_setting_bool;
+ addonInterface.toKodi->kodi_addon->get_setting_int = get_setting_int;
+ addonInterface.toKodi->kodi_addon->get_setting_float = get_setting_float;
+ addonInterface.toKodi->kodi_addon->get_setting_string = get_setting_string;
+ addonInterface.toKodi->kodi_addon->set_setting_bool = set_setting_bool;
+ addonInterface.toKodi->kodi_addon->set_setting_int = set_setting_int;
+ addonInterface.toKodi->kodi_addon->set_setting_float = set_setting_float;
+ addonInterface.toKodi->kodi_addon->set_setting_string = set_setting_string;
+ addonInterface.toKodi->kodi_addon->get_addon_info = get_addon_info;
+ addonInterface.toKodi->kodi_addon->get_type_version = get_type_version;
+ addonInterface.toKodi->kodi_addon->get_interface = get_interface;
+
+ // Related parts becomes set from addon headers, make here to nullptr to allow
+ // checks for right set of them
+ addonInterface.toAddon = new KodiToAddonFuncTable_Addon();
+
+ // Init the other interfaces
+ Interface_General::Init(&addonInterface);
+ Interface_AudioEngine::Init(&addonInterface);
+ Interface_Filesystem::Init(&addonInterface);
+ Interface_Network::Init(&addonInterface);
+ Interface_GUIGeneral::Init(&addonInterface);
+
+ return true;
+}
+
+void Interface_Base::DeInitInterface(AddonGlobalInterface& addonInterface)
+{
+ Interface_GUIGeneral::DeInit(&addonInterface);
+ Interface_Network::DeInit(&addonInterface);
+ Interface_Filesystem::DeInit(&addonInterface);
+ Interface_AudioEngine::DeInit(&addonInterface);
+ Interface_General::DeInit(&addonInterface);
+
+ if (addonInterface.toKodi)
+ delete addonInterface.toKodi->kodi_addon;
+ delete addonInterface.toKodi;
+ delete addonInterface.toAddon;
+ addonInterface = {};
+}
+
+void Interface_Base::RegisterInterface(ADDON_GET_INTERFACE_FN fn)
+{
+ s_registeredInterfaces.push_back(fn);
+}
+
+bool Interface_Base::UpdateSettingInActiveDialog(CAddonDll* addon,
+ AddonInstanceId instanceId,
+ const char* id,
+ const std::string& value)
+{
+ if (!CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_ADDON_SETTINGS))
+ return false;
+
+ CGUIDialogAddonSettings* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogAddonSettings>(
+ WINDOW_DIALOG_ADDON_SETTINGS);
+ if (dialog->GetCurrentAddonID() != addon->ID())
+ return false;
+
+ CGUIMessage message(GUI_MSG_SETTING_UPDATED, 0, 0);
+ std::vector<std::string> params;
+ params.emplace_back(id);
+ params.push_back(value);
+ message.SetStringParams(params);
+ message.SetParam1(instanceId);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message,
+ WINDOW_DIALOG_ADDON_SETTINGS);
+
+ return true;
+}
+
+/*!
+ * @brief Addon to Kodi basic callbacks below
+ *
+ * The amount of functions here are hold so minimal as possible. Only parts
+ * where needed on nearly every add-on (e.g. addon_log_msg) are to add there.
+ *
+ * More specific parts like e.g. to open files should be added to a separate
+ * part.
+ */
+//@{
+
+void Interface_Base::addon_log_msg(const KODI_ADDON_BACKEND_HDL hdl,
+ const int addonLogLevel,
+ const char* strMessage)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr)
+ {
+ CLog::Log(LOGERROR, "addon_log_msg(...) called with empty kodi instance pointer");
+ return;
+ }
+
+ int logLevel = LOGNONE;
+ switch (addonLogLevel)
+ {
+ case ADDON_LOG_DEBUG:
+ logLevel = LOGDEBUG;
+ break;
+ case ADDON_LOG_INFO:
+ logLevel = LOGINFO;
+ break;
+ case ADDON_LOG_WARNING:
+ logLevel = LOGWARNING;
+ break;
+ case ADDON_LOG_ERROR:
+ logLevel = LOGERROR;
+ break;
+ case ADDON_LOG_FATAL:
+ logLevel = LOGFATAL;
+ break;
+ default:
+ logLevel = LOGDEBUG;
+ break;
+ }
+
+ CLog::Log(logLevel, "AddOnLog: {}: {}", addon->ID(), strMessage);
+}
+
+char* Interface_Base::get_type_version(const KODI_ADDON_BACKEND_HDL hdl, int type)
+{
+ return strdup(kodi::addon::GetTypeVersion(type));
+}
+
+char* Interface_Base::get_addon_path(const KODI_ADDON_BACKEND_HDL hdl)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr)
+ {
+ CLog::Log(LOGERROR, "get_addon_path(...) called with empty kodi instance pointer");
+ return nullptr;
+ }
+
+ return strdup(CSpecialProtocol::TranslatePath(addon->Path()).c_str());
+}
+
+char* Interface_Base::get_lib_path(const KODI_ADDON_BACKEND_HDL hdl)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr)
+ {
+ CLog::Log(LOGERROR, "get_lib_path(...) called with empty kodi instance pointer");
+ return nullptr;
+ }
+
+ return strdup(CSpecialProtocol::TranslatePath("special://xbmcbinaddons/" + addon->ID()).c_str());
+}
+
+char* Interface_Base::get_user_path(const KODI_ADDON_BACKEND_HDL hdl)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr)
+ {
+ CLog::Log(LOGERROR, "get_user_path(...) called with empty kodi instance pointer");
+ return nullptr;
+ }
+
+ return strdup(CSpecialProtocol::TranslatePath(addon->Profile()).c_str());
+}
+
+char* Interface_Base::get_temp_path(const KODI_ADDON_BACKEND_HDL hdl)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr)
+ {
+ CLog::Log(LOGERROR, "get_temp_path(...) called with empty kodi instance pointer");
+ return nullptr;
+ }
+
+ std::string tempPath =
+ URIUtils::AddFileToFolder(CServiceBroker::GetAddonMgr().GetTempAddonBasePath(), addon->ID());
+ tempPath += "-temp";
+ XFILE::CDirectory::Create(tempPath);
+
+ return strdup(CSpecialProtocol::TranslatePath(tempPath).c_str());
+}
+
+char* Interface_Base::get_localized_string(const KODI_ADDON_BACKEND_HDL hdl, long label_id)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "get_localized_string(...) called with empty kodi instance pointer");
+ return nullptr;
+ }
+
+ if (g_application.m_bStop)
+ return nullptr;
+
+ std::string label = g_localizeStrings.GetAddonString(addon->ID(), label_id);
+ if (label.empty())
+ label = g_localizeStrings.Get(label_id);
+ char* buffer = strdup(label.c_str());
+ return buffer;
+}
+
+char* Interface_Base::get_addon_info(const KODI_ADDON_BACKEND_HDL hdl, const char* id)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr || id == nullptr)
+ {
+ CLog::Log(LOGERROR, "get_addon_info(...) called with empty pointer");
+ return nullptr;
+ }
+
+ std::string str;
+ if (StringUtils::CompareNoCase(id, "author") == 0)
+ str = addon->Author();
+ else if (StringUtils::CompareNoCase(id, "changelog") == 0)
+ str = addon->ChangeLog();
+ else if (StringUtils::CompareNoCase(id, "description") == 0)
+ str = addon->Description();
+ else if (StringUtils::CompareNoCase(id, "disclaimer") == 0)
+ str = addon->Disclaimer();
+ else if (StringUtils::CompareNoCase(id, "fanart") == 0)
+ str = addon->FanArt();
+ else if (StringUtils::CompareNoCase(id, "icon") == 0)
+ str = addon->Icon();
+ else if (StringUtils::CompareNoCase(id, "id") == 0)
+ str = addon->ID();
+ else if (StringUtils::CompareNoCase(id, "name") == 0)
+ str = addon->Name();
+ else if (StringUtils::CompareNoCase(id, "path") == 0)
+ str = addon->Path();
+ else if (StringUtils::CompareNoCase(id, "profile") == 0)
+ str = addon->Profile();
+ else if (StringUtils::CompareNoCase(id, "summary") == 0)
+ str = addon->Summary();
+ else if (StringUtils::CompareNoCase(id, "type") == 0)
+ str = ADDON::CAddonInfo::TranslateType(addon->Type());
+ else if (StringUtils::CompareNoCase(id, "version") == 0)
+ str = addon->Version().asString();
+ else
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - add-on '{}' requests invalid id '{}'", __func__,
+ addon->Name(), id);
+ return nullptr;
+ }
+
+ char* buffer = strdup(str.c_str());
+ return buffer;
+}
+
+bool Interface_Base::open_settings_dialog(const KODI_ADDON_BACKEND_HDL hdl)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr)
+ {
+ CLog::Log(LOGERROR, "open_settings_dialog(...) called with empty kodi instance pointer");
+ return false;
+ }
+
+ // show settings dialog
+ AddonPtr addonInfo;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(addon->ID(), addonInfo, OnlyEnabled::CHOICE_YES))
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - Could not get addon information for '{}'", __func__,
+ addon->ID());
+ return false;
+ }
+
+ return CGUIDialogAddonSettings::ShowForAddon(addonInfo);
+}
+
+bool Interface_Base::is_setting_using_default(const KODI_ADDON_BACKEND_HDL hdl, const char* id)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr || id == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid data (addon='{}', id='{}')", __func__, hdl,
+ static_cast<const void*>(id));
+
+ return false;
+ }
+
+ if (!addon->HasSettings())
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - couldn't get settings for add-on '{}'", __func__,
+ addon->Name());
+ return false;
+ }
+
+ auto setting = addon->GetSettings()->GetSetting(id);
+ if (setting == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - can't find setting '{}' in '{}'", __func__, id,
+ addon->Name());
+ return false;
+ }
+
+ return setting->IsDefault();
+}
+
+bool Interface_Base::get_setting_bool(const KODI_ADDON_BACKEND_HDL hdl, const char* id, bool* value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr || id == nullptr || value == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid data (addon='{}', id='{}', value='{}')",
+ __func__, hdl, static_cast<const void*>(id), static_cast<void*>(value));
+
+ return false;
+ }
+
+ if (!addon->HasSettings())
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - couldn't get settings for add-on '{}'", __func__,
+ addon->Name());
+ return false;
+ }
+
+ auto setting = addon->GetSettings()->GetSetting(id);
+ if (setting == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - can't find setting '{}' in '{}'", __func__, id,
+ addon->Name());
+ return false;
+ }
+
+ if (setting->GetType() != SettingType::Boolean)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - setting '{}' is not a boolean in '{}'", __func__, id,
+ addon->Name());
+ return false;
+ }
+
+ *value = std::static_pointer_cast<CSettingBool>(setting)->GetValue();
+ return true;
+}
+
+bool Interface_Base::get_setting_int(const KODI_ADDON_BACKEND_HDL hdl, const char* id, int* value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr || id == nullptr || value == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid data (addon='{}', id='{}', value='{}')",
+ __func__, hdl, static_cast<const void*>(id), static_cast<void*>(value));
+
+ return false;
+ }
+
+ if (!addon->HasSettings())
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - couldn't get settings for add-on '{}'", __func__,
+ addon->Name());
+ return false;
+ }
+
+ auto setting = addon->GetSettings()->GetSetting(id);
+ if (setting == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - can't find setting '{}' in '{}'", __func__, id,
+ addon->Name());
+ return false;
+ }
+
+ if (setting->GetType() != SettingType::Integer && setting->GetType() != SettingType::Number)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - setting '{}' is not a integer in '{}'", __func__, id,
+ addon->Name());
+ return false;
+ }
+
+ if (setting->GetType() == SettingType::Integer)
+ *value = std::static_pointer_cast<CSettingInt>(setting)->GetValue();
+ else
+ *value = static_cast<int>(std::static_pointer_cast<CSettingNumber>(setting)->GetValue());
+ return true;
+}
+
+bool Interface_Base::get_setting_float(const KODI_ADDON_BACKEND_HDL hdl,
+ const char* id,
+ float* value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr || id == nullptr || value == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid data (addon='{}', id='{}', value='{}')",
+ __func__, hdl, static_cast<const void*>(id), static_cast<void*>(value));
+
+ return false;
+ }
+
+ if (!addon->HasSettings())
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - couldn't get settings for add-on '{}'", __func__,
+ addon->Name());
+ return false;
+ }
+
+ auto setting = addon->GetSettings()->GetSetting(id);
+ if (setting == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - can't find setting '{}' in '{}'", __func__, id,
+ addon->Name());
+ return false;
+ }
+
+ if (setting->GetType() != SettingType::Number)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - setting '{}' is not a number in '{}'", __func__, id,
+ addon->Name());
+ return false;
+ }
+
+ *value = static_cast<float>(std::static_pointer_cast<CSettingNumber>(setting)->GetValue());
+ return true;
+}
+
+bool Interface_Base::get_setting_string(const KODI_ADDON_BACKEND_HDL hdl,
+ const char* id,
+ char** value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr || id == nullptr || value == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid data (addon='{}', id='{}', value='{}')",
+ __func__, hdl, static_cast<const void*>(id), static_cast<void*>(value));
+
+ return false;
+ }
+
+ if (!addon->HasSettings())
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - couldn't get settings for add-on '{}'", __func__,
+ addon->Name());
+ return false;
+ }
+
+ auto setting = addon->GetSettings()->GetSetting(id);
+ if (setting == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - can't find setting '{}' in '{}'", __func__, id,
+ addon->Name());
+ return false;
+ }
+
+ if (setting->GetType() != SettingType::String)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - setting '{}' is not a string in '{}'", __func__, id,
+ addon->Name());
+ return false;
+ }
+
+ *value = strdup(std::static_pointer_cast<CSettingString>(setting)->GetValue().c_str());
+ return true;
+}
+
+bool Interface_Base::set_setting_bool(const KODI_ADDON_BACKEND_HDL hdl, const char* id, bool value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr || id == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid data (addon='{}', id='{}')", __func__, hdl,
+ static_cast<const void*>(id));
+
+ return false;
+ }
+
+ if (Interface_Base::UpdateSettingInActiveDialog(addon, ADDON_SETTINGS_ID, id,
+ value ? "true" : "false"))
+ return true;
+
+ if (!addon->UpdateSettingBool(id, value))
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid setting type", __func__);
+ return false;
+ }
+
+ addon->SaveSettings();
+
+ return true;
+}
+
+bool Interface_Base::set_setting_int(const KODI_ADDON_BACKEND_HDL hdl, const char* id, int value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr || id == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid data (addon='{}', id='{}')", __func__, hdl,
+ static_cast<const void*>(id));
+
+ return false;
+ }
+
+ if (Interface_Base::UpdateSettingInActiveDialog(addon, ADDON_SETTINGS_ID, id,
+ std::to_string(value)))
+ return true;
+
+ if (!addon->UpdateSettingInt(id, value))
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid setting type", __func__);
+ return false;
+ }
+
+ addon->SaveSettings();
+
+ return true;
+}
+
+bool Interface_Base::set_setting_float(const KODI_ADDON_BACKEND_HDL hdl,
+ const char* id,
+ float value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr || id == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid data (addon='{}', id='{}')", __func__, hdl,
+ static_cast<const void*>(id));
+
+ return false;
+ }
+
+ if (Interface_Base::UpdateSettingInActiveDialog(addon, ADDON_SETTINGS_ID, id,
+ StringUtils::Format("{:f}", value)))
+ return true;
+
+ if (!addon->UpdateSettingNumber(id, static_cast<double>(value)))
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid setting type", __func__);
+ return false;
+ }
+
+ addon->SaveSettings();
+
+ return true;
+}
+
+bool Interface_Base::set_setting_string(const KODI_ADDON_BACKEND_HDL hdl,
+ const char* id,
+ const char* value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(hdl);
+ if (addon == nullptr || id == nullptr || value == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid data (addon='{}', id='{}', value='{}')",
+ __func__, hdl, static_cast<const void*>(id), static_cast<const void*>(value));
+
+ return false;
+ }
+
+ if (Interface_Base::UpdateSettingInActiveDialog(addon, ADDON_SETTINGS_ID, id, value))
+ return true;
+
+ if (!addon->UpdateSettingString(id, value))
+ {
+ CLog::Log(LOGERROR, "Interface_Base::{} - invalid setting type", __func__);
+ return false;
+ }
+
+ addon->SaveSettings();
+
+ return true;
+}
+
+void Interface_Base::free_string(const KODI_ADDON_BACKEND_HDL hdl, char* str)
+{
+ if (str)
+ free(str);
+}
+
+void Interface_Base::free_string_array(const KODI_ADDON_BACKEND_HDL hdl,
+ char** arr,
+ int numElements)
+{
+ if (arr)
+ {
+ for (int i = 0; i < numElements; ++i)
+ {
+ free(arr[i]);
+ }
+ free(arr);
+ }
+}
+
+void* Interface_Base::get_interface(const KODI_ADDON_BACKEND_HDL hdl,
+ const char* name,
+ const char* version)
+{
+ if (!name || !version)
+ return nullptr;
+
+ void* retval(nullptr);
+
+ for (auto fn : s_registeredInterfaces)
+ if ((retval = fn(name, version)))
+ break;
+
+ return retval;
+}
+
+//@}
+
+} // namespace ADDON
diff --git a/xbmc/addons/interfaces/AddonBase.h b/xbmc/addons/interfaces/AddonBase.h
new file mode 100644
index 0000000..6dcc53c
--- /dev/null
+++ b/xbmc/addons/interfaces/AddonBase.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2005-2020 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 "addons/IAddon.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon_base.h"
+
+extern "C"
+{
+namespace ADDON
+{
+
+typedef void* (*ADDON_GET_INTERFACE_FN)(const std::string& name, const std::string& version);
+
+class CAddonDll;
+
+/*!
+ * @brief Global general Add-on to Kodi callback functions
+ *
+ * To hold general functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/General.h"
+ */
+struct Interface_Base
+{
+ static bool InitInterface(CAddonDll* addon,
+ AddonGlobalInterface& addonInterface,
+ KODI_ADDON_INSTANCE_STRUCT* firstKodiInstance);
+ static void DeInitInterface(AddonGlobalInterface& addonInterface);
+ static void RegisterInterface(ADDON_GET_INTERFACE_FN fn);
+ static bool UpdateSettingInActiveDialog(CAddonDll* addon,
+ AddonInstanceId instanceId,
+ const char* id,
+ const std::string& value);
+
+ static std::vector<ADDON_GET_INTERFACE_FN> s_registeredInterfaces;
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void addon_log_msg(const KODI_ADDON_BACKEND_HDL hdl,
+ const int addonLogLevel,
+ const char* strMessage);
+ static char* get_type_version(const KODI_ADDON_BACKEND_HDL hdl, int type);
+ static char* get_addon_path(const KODI_ADDON_BACKEND_HDL hdl);
+ static char* get_lib_path(const KODI_ADDON_BACKEND_HDL hdl);
+ static char* get_user_path(const KODI_ADDON_BACKEND_HDL hdl);
+ static char* get_temp_path(const KODI_ADDON_BACKEND_HDL hdl);
+ static char* get_localized_string(const KODI_ADDON_BACKEND_HDL hdl, long label_id);
+ static char* get_addon_info(const KODI_ADDON_BACKEND_HDL hdl, const char* id);
+ static bool open_settings_dialog(const KODI_ADDON_BACKEND_HDL hdl);
+ static bool is_setting_using_default(const KODI_ADDON_BACKEND_HDL hdl, const char* id);
+ static bool get_setting_bool(const KODI_ADDON_BACKEND_HDL hdl, const char* id, bool* value);
+ static bool get_setting_int(const KODI_ADDON_BACKEND_HDL hdl, const char* id, int* value);
+ static bool get_setting_float(const KODI_ADDON_BACKEND_HDL hdl, const char* id, float* value);
+ static bool get_setting_string(const KODI_ADDON_BACKEND_HDL hdl, const char* id, char** value);
+ static bool set_setting_bool(const KODI_ADDON_BACKEND_HDL hdl, const char* id, bool value);
+ static bool set_setting_int(const KODI_ADDON_BACKEND_HDL hdl, const char* id, int value);
+ static bool set_setting_float(const KODI_ADDON_BACKEND_HDL hdl, const char* id, float value);
+ static bool set_setting_string(const KODI_ADDON_BACKEND_HDL hdl,
+ const char* id,
+ const char* value);
+ static void free_string(const KODI_ADDON_BACKEND_HDL hdl, char* str);
+ static void free_string_array(const KODI_ADDON_BACKEND_HDL hdl, char** arr, int numElements);
+ static void* get_interface(const KODI_ADDON_BACKEND_HDL hdl,
+ const char* name,
+ const char* version);
+ //@}
+};
+
+} /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/AudioEngine.cpp b/xbmc/addons/interfaces/AudioEngine.cpp
new file mode 100644
index 0000000..0efcc07
--- /dev/null
+++ b/xbmc/addons/interfaces/AudioEngine.cpp
@@ -0,0 +1,761 @@
+/*
+ * 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 "AudioEngine.h"
+
+#include "ServiceBroker.h"
+#include "addons/kodi-dev-kit/include/kodi/AddonBase.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "cores/AudioEngine/Interfaces/AEStream.h"
+#include "cores/AudioEngine/Utils/AEStreamData.h"
+#include "utils/log.h"
+
+using namespace kodi; // addon-dev-kit namespace
+using namespace kodi::audioengine; // addon-dev-kit namespace
+
+namespace ADDON
+{
+
+void Interface_AudioEngine::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_audioengine = new AddonToKodiFuncTable_kodi_audioengine();
+
+ // write KODI audio DSP specific add-on function addresses to callback table
+ addonInterface->toKodi->kodi_audioengine->make_stream = audioengine_make_stream;
+ addonInterface->toKodi->kodi_audioengine->free_stream = audioengine_free_stream;
+ addonInterface->toKodi->kodi_audioengine->get_current_sink_format = get_current_sink_format;
+
+ // AEStream add-on function callback table
+ addonInterface->toKodi->kodi_audioengine->aestream_get_space = aestream_get_space;
+ addonInterface->toKodi->kodi_audioengine->aestream_add_data = aestream_add_data;
+ addonInterface->toKodi->kodi_audioengine->aestream_get_delay = aestream_get_delay;
+ addonInterface->toKodi->kodi_audioengine->aestream_is_buffering = aestream_is_buffering;
+ addonInterface->toKodi->kodi_audioengine->aestream_get_cache_time = aestream_get_cache_time;
+ addonInterface->toKodi->kodi_audioengine->aestream_get_cache_total = aestream_get_cache_total;
+ addonInterface->toKodi->kodi_audioengine->aestream_pause = aestream_pause;
+ addonInterface->toKodi->kodi_audioengine->aestream_resume = aestream_resume;
+ addonInterface->toKodi->kodi_audioengine->aestream_drain = aestream_drain;
+ addonInterface->toKodi->kodi_audioengine->aestream_is_draining = aestream_is_draining;
+ addonInterface->toKodi->kodi_audioengine->aestream_is_drained = aestream_is_drained;
+ addonInterface->toKodi->kodi_audioengine->aestream_flush = aestream_flush;
+ addonInterface->toKodi->kodi_audioengine->aestream_get_volume = aestream_get_volume;
+ addonInterface->toKodi->kodi_audioengine->aestream_set_volume = aestream_set_volume;
+ addonInterface->toKodi->kodi_audioengine->aestream_get_amplification = aestream_get_amplification;
+ addonInterface->toKodi->kodi_audioengine->aestream_set_amplification = aestream_set_amplification;
+ addonInterface->toKodi->kodi_audioengine->aestream_get_frame_size = aestream_get_frame_size;
+ addonInterface->toKodi->kodi_audioengine->aestream_get_channel_count = aestream_get_channel_count;
+ addonInterface->toKodi->kodi_audioengine->aestream_get_sample_rate = aestream_get_sample_rate;
+ addonInterface->toKodi->kodi_audioengine->aestream_get_data_format = aestream_get_data_format;
+ addonInterface->toKodi->kodi_audioengine->aestream_get_resample_ratio =
+ aestream_get_resample_ratio;
+ addonInterface->toKodi->kodi_audioengine->aestream_set_resample_ratio =
+ aestream_set_resample_ratio;
+}
+
+void Interface_AudioEngine::DeInit(AddonGlobalInterface* addonInterface)
+{
+ if (addonInterface->toKodi) /* <-- Safe check, needed so long old addon way is present */
+ {
+ delete addonInterface->toKodi->kodi_audioengine;
+ addonInterface->toKodi->kodi_audioengine = nullptr;
+ }
+}
+
+AEChannel Interface_AudioEngine::TranslateAEChannelToKodi(AudioEngineChannel channel)
+{
+ switch (channel)
+ {
+ case AUDIOENGINE_CH_RAW:
+ return AE_CH_RAW;
+ case AUDIOENGINE_CH_FL:
+ return AE_CH_FL;
+ case AUDIOENGINE_CH_FR:
+ return AE_CH_FR;
+ case AUDIOENGINE_CH_FC:
+ return AE_CH_FC;
+ case AUDIOENGINE_CH_LFE:
+ return AE_CH_LFE;
+ case AUDIOENGINE_CH_BL:
+ return AE_CH_BL;
+ case AUDIOENGINE_CH_BR:
+ return AE_CH_BR;
+ case AUDIOENGINE_CH_FLOC:
+ return AE_CH_FLOC;
+ case AUDIOENGINE_CH_FROC:
+ return AE_CH_FROC;
+ case AUDIOENGINE_CH_BC:
+ return AE_CH_BC;
+ case AUDIOENGINE_CH_SL:
+ return AE_CH_SL;
+ case AUDIOENGINE_CH_SR:
+ return AE_CH_SR;
+ case AUDIOENGINE_CH_TFL:
+ return AE_CH_TFL;
+ case AUDIOENGINE_CH_TFR:
+ return AE_CH_TFR;
+ case AUDIOENGINE_CH_TFC:
+ return AE_CH_TFC;
+ case AUDIOENGINE_CH_TC:
+ return AE_CH_TC;
+ case AUDIOENGINE_CH_TBL:
+ return AE_CH_TBL;
+ case AUDIOENGINE_CH_TBR:
+ return AE_CH_TBR;
+ case AUDIOENGINE_CH_TBC:
+ return AE_CH_TBC;
+ case AUDIOENGINE_CH_BLOC:
+ return AE_CH_BLOC;
+ case AUDIOENGINE_CH_BROC:
+ return AE_CH_BROC;
+ case AUDIOENGINE_CH_MAX:
+ return AE_CH_MAX;
+ case AUDIOENGINE_CH_NULL:
+ default:
+ return AE_CH_NULL;
+ }
+}
+
+AudioEngineChannel Interface_AudioEngine::TranslateAEChannelToAddon(AEChannel channel)
+{
+ switch (channel)
+ {
+ case AE_CH_RAW:
+ return AUDIOENGINE_CH_RAW;
+ case AE_CH_FL:
+ return AUDIOENGINE_CH_FL;
+ case AE_CH_FR:
+ return AUDIOENGINE_CH_FR;
+ case AE_CH_FC:
+ return AUDIOENGINE_CH_FC;
+ case AE_CH_LFE:
+ return AUDIOENGINE_CH_LFE;
+ case AE_CH_BL:
+ return AUDIOENGINE_CH_BL;
+ case AE_CH_BR:
+ return AUDIOENGINE_CH_BR;
+ case AE_CH_FLOC:
+ return AUDIOENGINE_CH_FLOC;
+ case AE_CH_FROC:
+ return AUDIOENGINE_CH_FROC;
+ case AE_CH_BC:
+ return AUDIOENGINE_CH_BC;
+ case AE_CH_SL:
+ return AUDIOENGINE_CH_SL;
+ case AE_CH_SR:
+ return AUDIOENGINE_CH_SR;
+ case AE_CH_TFL:
+ return AUDIOENGINE_CH_TFL;
+ case AE_CH_TFR:
+ return AUDIOENGINE_CH_TFR;
+ case AE_CH_TFC:
+ return AUDIOENGINE_CH_TFC;
+ case AE_CH_TC:
+ return AUDIOENGINE_CH_TC;
+ case AE_CH_TBL:
+ return AUDIOENGINE_CH_TBL;
+ case AE_CH_TBR:
+ return AUDIOENGINE_CH_TBR;
+ case AE_CH_TBC:
+ return AUDIOENGINE_CH_TBC;
+ case AE_CH_BLOC:
+ return AUDIOENGINE_CH_BLOC;
+ case AE_CH_BROC:
+ return AUDIOENGINE_CH_BROC;
+ case AE_CH_MAX:
+ return AUDIOENGINE_CH_MAX;
+ case AE_CH_NULL:
+ default:
+ return AUDIOENGINE_CH_NULL;
+ }
+}
+
+AEDataFormat Interface_AudioEngine::TranslateAEFormatToKodi(AudioEngineDataFormat format)
+{
+ switch (format)
+ {
+ case AUDIOENGINE_FMT_U8:
+ return AE_FMT_U8;
+ case AUDIOENGINE_FMT_S16BE:
+ return AE_FMT_S16BE;
+ case AUDIOENGINE_FMT_S16LE:
+ return AE_FMT_S16LE;
+ case AUDIOENGINE_FMT_S16NE:
+ return AE_FMT_S16NE;
+ case AUDIOENGINE_FMT_S32BE:
+ return AE_FMT_S32BE;
+ case AUDIOENGINE_FMT_S32LE:
+ return AE_FMT_S32LE;
+ case AUDIOENGINE_FMT_S32NE:
+ return AE_FMT_S32NE;
+ case AUDIOENGINE_FMT_S24BE4:
+ return AE_FMT_S24BE4;
+ case AUDIOENGINE_FMT_S24LE4:
+ return AE_FMT_S24LE4;
+ case AUDIOENGINE_FMT_S24NE4:
+ return AE_FMT_S24NE4;
+ case AUDIOENGINE_FMT_S24NE4MSB:
+ return AE_FMT_S24NE4MSB;
+ case AUDIOENGINE_FMT_S24BE3:
+ return AE_FMT_S24BE3;
+ case AUDIOENGINE_FMT_S24LE3:
+ return AE_FMT_S24LE3;
+ case AUDIOENGINE_FMT_S24NE3:
+ return AE_FMT_S24NE3;
+ case AUDIOENGINE_FMT_DOUBLE:
+ return AE_FMT_DOUBLE;
+ case AUDIOENGINE_FMT_FLOAT:
+ return AE_FMT_FLOAT;
+ case AUDIOENGINE_FMT_RAW:
+ return AE_FMT_RAW;
+ case AUDIOENGINE_FMT_U8P:
+ return AE_FMT_U8P;
+ case AUDIOENGINE_FMT_S16NEP:
+ return AE_FMT_S16NEP;
+ case AUDIOENGINE_FMT_S32NEP:
+ return AE_FMT_S32NEP;
+ case AUDIOENGINE_FMT_S24NE4P:
+ return AE_FMT_S24NE4P;
+ case AUDIOENGINE_FMT_S24NE4MSBP:
+ return AE_FMT_S24NE4MSBP;
+ case AUDIOENGINE_FMT_S24NE3P:
+ return AE_FMT_S24NE3P;
+ case AUDIOENGINE_FMT_DOUBLEP:
+ return AE_FMT_DOUBLEP;
+ case AUDIOENGINE_FMT_FLOATP:
+ return AE_FMT_FLOATP;
+ case AUDIOENGINE_FMT_MAX:
+ return AE_FMT_MAX;
+ case AUDIOENGINE_FMT_INVALID:
+ default:
+ return AE_FMT_INVALID;
+ }
+}
+
+AudioEngineDataFormat Interface_AudioEngine::TranslateAEFormatToAddon(AEDataFormat format)
+{
+ switch (format)
+ {
+ case AE_FMT_U8:
+ return AUDIOENGINE_FMT_U8;
+ case AE_FMT_S16BE:
+ return AUDIOENGINE_FMT_S16BE;
+ case AE_FMT_S16LE:
+ return AUDIOENGINE_FMT_S16LE;
+ case AE_FMT_S16NE:
+ return AUDIOENGINE_FMT_S16NE;
+ case AE_FMT_S32BE:
+ return AUDIOENGINE_FMT_S32BE;
+ case AE_FMT_S32LE:
+ return AUDIOENGINE_FMT_S32LE;
+ case AE_FMT_S32NE:
+ return AUDIOENGINE_FMT_S32NE;
+ case AE_FMT_S24BE4:
+ return AUDIOENGINE_FMT_S24BE4;
+ case AE_FMT_S24LE4:
+ return AUDIOENGINE_FMT_S24LE4;
+ case AE_FMT_S24NE4:
+ return AUDIOENGINE_FMT_S24NE4;
+ case AE_FMT_S24NE4MSB:
+ return AUDIOENGINE_FMT_S24NE4MSB;
+ case AE_FMT_S24BE3:
+ return AUDIOENGINE_FMT_S24BE3;
+ case AE_FMT_S24LE3:
+ return AUDIOENGINE_FMT_S24LE3;
+ case AE_FMT_S24NE3:
+ return AUDIOENGINE_FMT_S24NE3;
+ case AE_FMT_DOUBLE:
+ return AUDIOENGINE_FMT_DOUBLE;
+ case AE_FMT_FLOAT:
+ return AUDIOENGINE_FMT_FLOAT;
+ case AE_FMT_RAW:
+ return AUDIOENGINE_FMT_RAW;
+ case AE_FMT_U8P:
+ return AUDIOENGINE_FMT_U8P;
+ case AE_FMT_S16NEP:
+ return AUDIOENGINE_FMT_S16NEP;
+ case AE_FMT_S32NEP:
+ return AUDIOENGINE_FMT_S32NEP;
+ case AE_FMT_S24NE4P:
+ return AUDIOENGINE_FMT_S24NE4P;
+ case AE_FMT_S24NE4MSBP:
+ return AUDIOENGINE_FMT_S24NE4MSBP;
+ case AE_FMT_S24NE3P:
+ return AUDIOENGINE_FMT_S24NE3P;
+ case AE_FMT_DOUBLEP:
+ return AUDIOENGINE_FMT_DOUBLEP;
+ case AE_FMT_FLOATP:
+ return AUDIOENGINE_FMT_FLOATP;
+ case AE_FMT_MAX:
+ return AUDIOENGINE_FMT_MAX;
+ case AE_FMT_INVALID:
+ default:
+ return AUDIOENGINE_FMT_INVALID;
+ }
+}
+
+AEStreamHandle* Interface_AudioEngine::audioengine_make_stream(void* kodiBase,
+ AUDIO_ENGINE_FORMAT* streamFormat,
+ unsigned int options)
+{
+ if (!kodiBase || !streamFormat)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamFormat='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamFormat));
+ return nullptr;
+ }
+
+ IAE* engine = CServiceBroker::GetActiveAE();
+ if (!engine)
+ return nullptr;
+
+ CAEChannelInfo layout;
+ for (unsigned int ch = 0; ch < AUDIOENGINE_CH_MAX; ++ch)
+ {
+ if (streamFormat->m_channels[ch] == AUDIOENGINE_CH_NULL)
+ break;
+ layout += TranslateAEChannelToKodi(streamFormat->m_channels[ch]);
+ }
+
+ AEAudioFormat format;
+ format.m_channelLayout = layout;
+ format.m_dataFormat = TranslateAEFormatToKodi(streamFormat->m_dataFormat);
+ format.m_sampleRate = streamFormat->m_sampleRate;
+
+ /* Translate addon options to kodi's options */
+ int kodiOption = 0;
+ if (options & AUDIO_STREAM_FORCE_RESAMPLE)
+ kodiOption |= AESTREAM_FORCE_RESAMPLE;
+ if (options & AUDIO_STREAM_PAUSED)
+ kodiOption |= AESTREAM_PAUSED;
+ if (options & AUDIO_STREAM_AUTOSTART)
+ kodiOption |= AESTREAM_AUTOSTART;
+
+ return engine->MakeStream(format, kodiOption).release();
+}
+
+void Interface_AudioEngine::audioengine_free_stream(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return;
+ }
+
+ IAE* engine = CServiceBroker::GetActiveAE();
+ if (engine)
+ engine->FreeStream(static_cast<IAEStream*>(streamHandle), true);
+}
+
+bool Interface_AudioEngine::get_current_sink_format(void* kodiBase, AUDIO_ENGINE_FORMAT* format)
+{
+ if (!kodiBase || !format)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', format='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(format));
+ return false;
+ }
+
+ IAE* engine = CServiceBroker::GetActiveAE();
+ if (!engine)
+ return false;
+
+ AEAudioFormat sinkFormat;
+ if (!engine->GetCurrentSinkFormat(sinkFormat))
+ {
+ CLog::Log(LOGERROR, "Interface_AudioEngine::{} - failed to get current sink format from AE!",
+ __FUNCTION__);
+ return false;
+ }
+
+ format->m_dataFormat = TranslateAEFormatToAddon(sinkFormat.m_dataFormat);
+ format->m_sampleRate = sinkFormat.m_sampleRate;
+ format->m_frames = sinkFormat.m_frames;
+ format->m_frameSize = sinkFormat.m_frameSize;
+ format->m_channelCount = sinkFormat.m_channelLayout.Count();
+ for (unsigned int ch = 0; ch < format->m_channelCount && ch < AUDIOENGINE_CH_MAX; ++ch)
+ {
+ format->m_channels[ch] = TranslateAEChannelToAddon(sinkFormat.m_channelLayout[ch]);
+ }
+
+ return true;
+}
+
+unsigned int Interface_AudioEngine::aestream_get_space(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return 0;
+ }
+
+ return static_cast<IAEStream*>(streamHandle)->GetSpace();
+}
+
+unsigned int Interface_AudioEngine::aestream_add_data(void* kodiBase,
+ AEStreamHandle* streamHandle,
+ uint8_t* const* data,
+ unsigned int offset,
+ unsigned int frames,
+ double pts,
+ bool hasDownmix,
+ double centerMixLevel)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return 0;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return 0;
+
+ IAEStream::ExtData extData;
+ extData.pts = pts;
+ extData.hasDownmix = hasDownmix;
+ extData.centerMixLevel = centerMixLevel;
+ return static_cast<IAEStream*>(streamHandle)->AddData(data, offset, frames, &extData);
+}
+
+double Interface_AudioEngine::aestream_get_delay(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return -1.0;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return -1.0;
+
+ return static_cast<IAEStream*>(streamHandle)->GetDelay();
+}
+
+bool Interface_AudioEngine::aestream_is_buffering(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return false;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return false;
+
+ return static_cast<IAEStream*>(streamHandle)->IsBuffering();
+}
+
+double Interface_AudioEngine::aestream_get_cache_time(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return -1.0;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return -1.0;
+
+ return static_cast<IAEStream*>(streamHandle)->GetCacheTime();
+}
+
+double Interface_AudioEngine::aestream_get_cache_total(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return -1.0;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return -1.0;
+
+ return static_cast<IAEStream*>(streamHandle)->GetCacheTotal();
+}
+
+void Interface_AudioEngine::aestream_pause(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return;
+
+ static_cast<IAEStream*>(streamHandle)->Pause();
+}
+
+void Interface_AudioEngine::aestream_resume(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return;
+ }
+
+ static_cast<IAEStream*>(streamHandle)->Resume();
+}
+
+void Interface_AudioEngine::aestream_drain(void* kodiBase, AEStreamHandle* streamHandle, bool wait)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return;
+
+ static_cast<IAEStream*>(streamHandle)->Drain(wait);
+}
+
+bool Interface_AudioEngine::aestream_is_draining(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return false;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return false;
+
+ return static_cast<IAEStream*>(streamHandle)->IsDraining();
+}
+
+bool Interface_AudioEngine::aestream_is_drained(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return false;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return false;
+
+ return static_cast<IAEStream*>(streamHandle)->IsDrained();
+}
+
+void Interface_AudioEngine::aestream_flush(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return;
+
+ static_cast<IAEStream*>(streamHandle)->Flush();
+}
+
+float Interface_AudioEngine::aestream_get_volume(void* kodiBase, AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return -1.0f;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return -1.0f;
+
+ return static_cast<IAEStream*>(streamHandle)->GetVolume();
+}
+
+void Interface_AudioEngine::aestream_set_volume(void* kodiBase,
+ AEStreamHandle* streamHandle,
+ float volume)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return;
+
+ static_cast<IAEStream*>(streamHandle)->SetVolume(volume);
+}
+
+float Interface_AudioEngine::aestream_get_amplification(void* kodiBase,
+ AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return -1.0f;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return -1.0f;
+
+ return static_cast<IAEStream*>(streamHandle)->GetAmplification();
+}
+
+void Interface_AudioEngine::aestream_set_amplification(void* kodiBase,
+ AEStreamHandle* streamHandle,
+ float amplify)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return;
+
+ static_cast<IAEStream*>(streamHandle)->SetAmplification(amplify);
+}
+
+unsigned int Interface_AudioEngine::aestream_get_frame_size(void* kodiBase,
+ AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return 0;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return 0;
+
+ return static_cast<IAEStream*>(streamHandle)->GetFrameSize();
+}
+
+unsigned int Interface_AudioEngine::aestream_get_channel_count(void* kodiBase,
+ AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return 0;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return 0;
+
+ return static_cast<IAEStream*>(streamHandle)->GetChannelCount();
+}
+
+unsigned int Interface_AudioEngine::aestream_get_sample_rate(void* kodiBase,
+ AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return 0;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return 0;
+
+ return static_cast<IAEStream*>(streamHandle)->GetSampleRate();
+}
+
+AudioEngineDataFormat Interface_AudioEngine::aestream_get_data_format(void* kodiBase,
+ AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return AUDIOENGINE_FMT_INVALID;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return AUDIOENGINE_FMT_INVALID;
+
+ return TranslateAEFormatToAddon(static_cast<IAEStream*>(streamHandle)->GetDataFormat());
+}
+
+double Interface_AudioEngine::aestream_get_resample_ratio(void* kodiBase,
+ AEStreamHandle* streamHandle)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return -1.0;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return -1.0;
+
+ return static_cast<IAEStream*>(streamHandle)->GetResampleRatio();
+}
+
+void Interface_AudioEngine::aestream_set_resample_ratio(void* kodiBase,
+ AEStreamHandle* streamHandle,
+ double ratio)
+{
+ if (!kodiBase || !streamHandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_AudioEngine::{} - invalid stream data (kodiBase='{}', streamHandle='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(streamHandle));
+ return;
+ }
+
+ if (!CServiceBroker::GetActiveAE())
+ return;
+
+ static_cast<IAEStream*>(streamHandle)->SetResampleRatio(ratio);
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/AudioEngine.h b/xbmc/addons/interfaces/AudioEngine.h
new file mode 100644
index 0000000..3700931
--- /dev/null
+++ b/xbmc/addons/interfaces/AudioEngine.h
@@ -0,0 +1,212 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/AudioEngine.h"
+#include "cores/AudioEngine/Utils/AEChannelData.h"
+
+extern "C"
+{
+namespace ADDON
+{
+
+struct Interface_AudioEngine
+{
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /**
+ * @brief Translation functions to separate Kodi and addons
+ *
+ * This thought to make it more safe for cases as something changed inside
+ * Kodi, addons overseen and breaks API, further to have on addons a better
+ * documentation about this parts.
+ */
+ //@{
+ static AEChannel TranslateAEChannelToKodi(AudioEngineChannel channel);
+ static AudioEngineChannel TranslateAEChannelToAddon(AEChannel channel);
+ static AEDataFormat TranslateAEFormatToKodi(AudioEngineDataFormat format);
+ static AudioEngineDataFormat TranslateAEFormatToAddon(AEDataFormat format);
+ //@}
+
+ /**
+ * Creates and returns a new handle to an IAEStream in the format specified, this function should never fail
+ * @param[in] streamFormat Format to use for stream
+ * @param[in] options A bit field of stream options (see: enum AEStreamOptions)
+ * @return a new Handle to an IAEStream that will accept data in the requested format
+ */
+ static AEStreamHandle* audioengine_make_stream(void* kodiBase,
+ AUDIO_ENGINE_FORMAT* streamFormat,
+ unsigned int options);
+
+ /**
+ * This method will remove the specified stream from the engine.
+ * For OSX/IOS this is essential to reconfigure the audio output.
+ * @param[in] streamHandle The stream to be altered
+ */
+ static void audioengine_free_stream(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Get the current sink data format
+ *
+ * @param[in] sinkFormat sink data format. For more details see AUDIO_ENGINE_FORMAT.
+ * @return Returns true on success, else false.
+ */
+ static bool get_current_sink_format(void* kodiBase, AUDIO_ENGINE_FORMAT* sinkFormat);
+
+ /**
+ * Returns the amount of space available in the stream
+ * @return The number of bytes AddData will consume
+ */
+ static unsigned int aestream_get_space(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Add planar or interleaved PCM data to the stream
+ * @param[in] data array of pointers to the planes
+ * @param[in] offset to frame in frames
+ * @param[in] frames number of frames
+ * @return The number of frames consumed
+ */
+ static unsigned int aestream_add_data(void* kodiBase,
+ AEStreamHandle* streamHandle,
+ uint8_t* const* data,
+ unsigned int offset,
+ unsigned int frames,
+ double pts,
+ bool hasDownmix,
+ double centerMixLevel);
+
+ /**
+ * Returns the time in seconds that it will take
+ * for the next added packet to be heard from the speakers.
+ * @return seconds
+ */
+ static double aestream_get_delay(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Returns if the stream is buffering
+ * @return True if the stream is buffering
+ */
+ static bool aestream_is_buffering(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Returns the time in seconds that it will take
+ * to underrun the cache if no sample is added.
+ * @return seconds
+ */
+ static double aestream_get_cache_time(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Returns the total time in seconds of the cache
+ * @return seconds
+ */
+ static double aestream_get_cache_total(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Pauses the stream playback
+ */
+ static void aestream_pause(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Resumes the stream after pausing
+ */
+ static void aestream_resume(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Start draining the stream
+ * @note Once called AddData will not consume more data.
+ */
+ static void aestream_drain(void* kodiBase, AEStreamHandle* streamHandle, bool wait);
+
+ /**
+ * Returns true if the is stream draining
+ */
+ static bool aestream_is_draining(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Returns true if the is stream has finished draining
+ */
+ static bool aestream_is_drained(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Flush all buffers dropping the audio data
+ */
+ static void aestream_flush(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Return the stream's current volume level
+ * @return The volume level between 0.0 and 1.0
+ */
+ static float aestream_get_volume(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Set the stream's volume level
+ * @param volume The new volume level between 0.0 and 1.0
+ */
+ static void aestream_set_volume(void* kodiBase, AEStreamHandle* streamHandle, float volume);
+
+ /**
+ * Gets the stream's volume amplification in linear units.
+ * @return The volume amplification factor between 1.0 and 1000.0
+ */
+ static float aestream_get_amplification(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Sets the stream's volume amplification in linear units.
+ * @param amplify The volume amplification factor between 1.0 and 1000.0
+ */
+ static void aestream_set_amplification(void* kodiBase,
+ AEStreamHandle* streamHandle,
+ float amplify);
+
+ /**
+ * Returns the size of one audio frame in bytes (channelCount * resolution)
+ * @return The size in bytes of one frame
+ */
+ static unsigned int aestream_get_frame_size(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Returns the number of channels the stream is configured to accept
+ * @return The channel count
+ */
+ static unsigned int aestream_get_channel_count(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Returns the stream's sample rate, if the stream is using a dynamic sample
+ * rate, this value will NOT reflect any changes made by calls to SetResampleRatio()
+ * @return The stream's sample rate (eg, 48000)
+ */
+ static unsigned int aestream_get_sample_rate(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Return the data format the stream has been configured with
+ * @return The stream's data format (eg, AE_FMT_S16LE)
+ */
+ static AudioEngineDataFormat aestream_get_data_format(void* kodiBase,
+ AEStreamHandle* streamHandle);
+
+ /**
+ * Return the resample ratio
+ * @note This will return an undefined value if the stream is not resampling
+ * @return the current resample ratio or undefined if the stream is not resampling
+ */
+ static double aestream_get_resample_ratio(void* kodiBase, AEStreamHandle* streamHandle);
+
+ /**
+ * Sets the resample ratio
+ * @note This function may return false if the stream is not resampling, if you wish to use this be sure to set the AESTREAM_FORCE_RESAMPLE option
+ * @param[in] ratio the new sample rate ratio, calculated by ((double)desiredRate / (double)GetSampleRate())
+ */
+ static void aestream_set_resample_ratio(void* kodiBase,
+ AEStreamHandle* streamHandle,
+ double ratio);
+};
+
+} /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/CMakeLists.txt b/xbmc/addons/interfaces/CMakeLists.txt
new file mode 100644
index 0000000..8e58bb6
--- /dev/null
+++ b/xbmc/addons/interfaces/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(SOURCES AddonBase.cpp
+ AudioEngine.cpp
+ General.cpp
+ Filesystem.cpp
+ Network.cpp)
+
+set(HEADERS AddonBase.h
+ AudioEngine.h
+ General.h
+ Filesystem.h
+ Network.h)
+
+if(CORE_SYSTEM_NAME STREQUAL android)
+ list(APPEND SOURCES platform/android/System.cpp)
+ list(APPEND HEADERS platform/android/System.h)
+endif()
+
+core_add_library(addons_interfaces)
diff --git a/xbmc/addons/interfaces/Filesystem.cpp b/xbmc/addons/interfaces/Filesystem.cpp
new file mode 100644
index 0000000..c063b48
--- /dev/null
+++ b/xbmc/addons/interfaces/Filesystem.cpp
@@ -0,0 +1,1181 @@
+/*
+ * 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 "Filesystem.h"
+
+#include "FileItem.h"
+#include "Util.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "platform/Filesystem.h"
+#include "utils/Crc32.h"
+#include "utils/HttpHeader.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <vector>
+
+#if defined(TARGET_WINDOWS)
+#ifndef S_IFLNK
+#define S_IFLNK 0120000
+#endif
+#ifndef S_ISBLK
+#define S_ISBLK(m) (0)
+#endif
+#ifndef S_ISSOCK
+#define S_ISSOCK(m) (0)
+#endif
+#ifndef S_ISLNK
+#define S_ISLNK(m) ((m & S_IFLNK) != 0)
+#endif
+#ifndef S_ISCHR
+#define S_ISCHR(m) ((m & _S_IFCHR) != 0)
+#endif
+#ifndef S_ISDIR
+#define S_ISDIR(m) ((m & _S_IFDIR) != 0)
+#endif
+#ifndef S_ISFIFO
+#define S_ISFIFO(m) ((m & _S_IFIFO) != 0)
+#endif
+#ifndef S_ISREG
+#define S_ISREG(m) ((m & _S_IFREG) != 0)
+#endif
+#endif
+
+using namespace kodi; // addon-dev-kit namespace
+using namespace XFILE;
+
+extern "C"
+{
+namespace ADDON
+{
+
+void Interface_Filesystem::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_filesystem = new AddonToKodiFuncTable_kodi_filesystem();
+
+ addonInterface->toKodi->kodi_filesystem->can_open_directory = can_open_directory;
+ addonInterface->toKodi->kodi_filesystem->create_directory = create_directory;
+ addonInterface->toKodi->kodi_filesystem->directory_exists = directory_exists;
+ addonInterface->toKodi->kodi_filesystem->remove_directory = remove_directory;
+ addonInterface->toKodi->kodi_filesystem->remove_directory_recursive = remove_directory_recursive;
+ addonInterface->toKodi->kodi_filesystem->get_directory = get_directory;
+ addonInterface->toKodi->kodi_filesystem->free_directory = free_directory;
+
+ addonInterface->toKodi->kodi_filesystem->file_exists = file_exists;
+ addonInterface->toKodi->kodi_filesystem->stat_file = stat_file;
+ addonInterface->toKodi->kodi_filesystem->delete_file = delete_file;
+ addonInterface->toKodi->kodi_filesystem->rename_file = rename_file;
+ addonInterface->toKodi->kodi_filesystem->copy_file = copy_file;
+ addonInterface->toKodi->kodi_filesystem->get_file_md5 = get_file_md5;
+ addonInterface->toKodi->kodi_filesystem->get_cache_thumb_name = get_cache_thumb_name;
+ addonInterface->toKodi->kodi_filesystem->make_legal_filename = make_legal_filename;
+ addonInterface->toKodi->kodi_filesystem->make_legal_path = make_legal_path;
+ addonInterface->toKodi->kodi_filesystem->translate_special_protocol = translate_special_protocol;
+ addonInterface->toKodi->kodi_filesystem->get_disk_space = get_disk_space;
+ addonInterface->toKodi->kodi_filesystem->is_internet_stream = is_internet_stream;
+ addonInterface->toKodi->kodi_filesystem->is_on_lan = is_on_lan;
+ addonInterface->toKodi->kodi_filesystem->is_remote = is_remote;
+ addonInterface->toKodi->kodi_filesystem->is_local = is_local;
+ addonInterface->toKodi->kodi_filesystem->is_url = is_url;
+ addonInterface->toKodi->kodi_filesystem->get_http_header = get_http_header;
+ addonInterface->toKodi->kodi_filesystem->get_mime_type = get_mime_type;
+ addonInterface->toKodi->kodi_filesystem->get_content_type = get_content_type;
+ addonInterface->toKodi->kodi_filesystem->get_cookies = get_cookies;
+
+ addonInterface->toKodi->kodi_filesystem->http_header_create = http_header_create;
+ addonInterface->toKodi->kodi_filesystem->http_header_free = http_header_free;
+
+ addonInterface->toKodi->kodi_filesystem->open_file = open_file;
+ addonInterface->toKodi->kodi_filesystem->open_file_for_write = open_file_for_write;
+ addonInterface->toKodi->kodi_filesystem->read_file = read_file;
+ addonInterface->toKodi->kodi_filesystem->read_file_string = read_file_string;
+ addonInterface->toKodi->kodi_filesystem->write_file = write_file;
+ addonInterface->toKodi->kodi_filesystem->flush_file = flush_file;
+ addonInterface->toKodi->kodi_filesystem->seek_file = seek_file;
+ addonInterface->toKodi->kodi_filesystem->truncate_file = truncate_file;
+ addonInterface->toKodi->kodi_filesystem->get_file_position = get_file_position;
+ addonInterface->toKodi->kodi_filesystem->get_file_length = get_file_length;
+ addonInterface->toKodi->kodi_filesystem->get_file_download_speed = get_file_download_speed;
+ addonInterface->toKodi->kodi_filesystem->close_file = close_file;
+ addonInterface->toKodi->kodi_filesystem->get_file_chunk_size = get_file_chunk_size;
+ addonInterface->toKodi->kodi_filesystem->io_control_get_seek_possible =
+ io_control_get_seek_possible;
+ addonInterface->toKodi->kodi_filesystem->io_control_get_cache_status =
+ io_control_get_cache_status;
+ addonInterface->toKodi->kodi_filesystem->io_control_set_cache_rate = io_control_set_cache_rate;
+ addonInterface->toKodi->kodi_filesystem->io_control_set_retry = io_control_set_retry;
+ addonInterface->toKodi->kodi_filesystem->get_property_values = get_property_values;
+
+ addonInterface->toKodi->kodi_filesystem->curl_create = curl_create;
+ addonInterface->toKodi->kodi_filesystem->curl_add_option = curl_add_option;
+ addonInterface->toKodi->kodi_filesystem->curl_open = curl_open;
+}
+
+void Interface_Filesystem::DeInit(AddonGlobalInterface* addonInterface)
+{
+ if (addonInterface->toKodi) /* <-- Safe check, needed so long old addon way is present */
+ {
+ delete addonInterface->toKodi->kodi_filesystem;
+ addonInterface->toKodi->kodi_filesystem = nullptr;
+ }
+}
+
+unsigned int Interface_Filesystem::TranslateFileReadBitsToKodi(unsigned int addonFlags)
+{
+ unsigned int kodiFlags = 0;
+
+ if (addonFlags & ADDON_READ_TRUNCATED)
+ kodiFlags |= READ_TRUNCATED;
+ if (addonFlags & ADDON_READ_CHUNKED)
+ kodiFlags |= READ_CHUNKED;
+ if (addonFlags & ADDON_READ_CACHED)
+ kodiFlags |= READ_CACHED;
+ if (addonFlags & ADDON_READ_NO_CACHE)
+ kodiFlags |= READ_NO_CACHE;
+ if (addonFlags & ADDON_READ_BITRATE)
+ kodiFlags |= READ_BITRATE;
+ if (addonFlags & ADDON_READ_MULTI_STREAM)
+ kodiFlags |= READ_MULTI_STREAM;
+ if (addonFlags & ADDON_READ_AUDIO_VIDEO)
+ kodiFlags |= READ_AUDIO_VIDEO;
+ if (addonFlags & ADDON_READ_AFTER_WRITE)
+ kodiFlags |= READ_AFTER_WRITE;
+ if (addonFlags & READ_REOPEN)
+ kodiFlags |= READ_REOPEN;
+
+ return kodiFlags;
+}
+
+bool Interface_Filesystem::can_open_directory(void* kodiBase, const char* url)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || url == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', url='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(url));
+ return false;
+ }
+
+ CFileItemList items;
+ return CDirectory::GetDirectory(url, items, "", DIR_FLAG_DEFAULTS | DIR_FLAG_BYPASS_CACHE);
+}
+
+bool Interface_Filesystem::create_directory(void* kodiBase, const char* path)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', path='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path));
+ return false;
+ }
+
+ return CDirectory::Create(path);
+}
+
+bool Interface_Filesystem::directory_exists(void* kodiBase, const char* path)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', path='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path));
+ return false;
+ }
+
+ return CDirectory::Exists(path, false);
+}
+
+bool Interface_Filesystem::remove_directory(void* kodiBase, const char* path)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', path='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path));
+ return false;
+ }
+
+ // Empty directory
+ CFileItemList fileItems;
+ CDirectory::GetDirectory(path, fileItems, "", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_BYPASS_CACHE);
+ for (int i = 0; i < fileItems.Size(); ++i)
+ CFile::Delete(fileItems.Get(i)->GetPath());
+
+ return CDirectory::Remove(path);
+}
+
+bool Interface_Filesystem::remove_directory_recursive(void* kodiBase, const char* path)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', path='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path));
+ return false;
+ }
+
+ return CDirectory::RemoveRecursive(path);
+}
+
+static void CFileItemListToVFSDirEntries(VFSDirEntry* entries, const CFileItemList& items)
+{
+ for (unsigned int i = 0; i < static_cast<unsigned int>(items.Size()); ++i)
+ {
+ entries[i].label = strdup(items[i]->GetLabel().c_str());
+ entries[i].path = strdup(items[i]->GetPath().c_str());
+ entries[i].size = items[i]->m_dwSize;
+ entries[i].folder = items[i]->m_bIsFolder;
+ items[i]->m_dateTime.GetAsTime(entries[i].date_time);
+ }
+}
+
+bool Interface_Filesystem::get_directory(void* kodiBase,
+ const char* path,
+ const char* mask,
+ struct VFSDirEntry** items,
+ unsigned int* num_items)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr || mask == nullptr || items == nullptr ||
+ num_items == nullptr)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_Filesystem::{} - invalid data (addon='{}', path='{}', mask='{}', "
+ "items='{}', num_items='{}'",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path),
+ static_cast<const void*>(mask), static_cast<void*>(items),
+ static_cast<void*>(num_items));
+ return false;
+ }
+
+ CFileItemList fileItems;
+ if (!CDirectory::GetDirectory(path, fileItems, mask,
+ DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_BYPASS_CACHE))
+ return false;
+
+ if (fileItems.Size() > 0)
+ {
+ *num_items = static_cast<unsigned int>(fileItems.Size());
+ *items = new VFSDirEntry[fileItems.Size()];
+ CFileItemListToVFSDirEntries(*items, fileItems);
+ }
+ else
+ {
+ *num_items = 0;
+ *items = nullptr;
+ }
+
+ return true;
+}
+
+void Interface_Filesystem::free_directory(void* kodiBase,
+ struct VFSDirEntry* items,
+ unsigned int num_items)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || items == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', items='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(items));
+ return;
+ }
+
+ for (unsigned int i = 0; i < num_items; ++i)
+ {
+ free(items[i].label);
+ free(items[i].path);
+ }
+ delete[] items;
+}
+
+//------------------------------------------------------------------------------
+
+bool Interface_Filesystem::file_exists(void* kodiBase, const char* filename, bool useCache)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || filename == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', filename='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(filename));
+ return false;
+ }
+
+ return CFile::Exists(filename, useCache);
+}
+
+bool Interface_Filesystem::stat_file(void* kodiBase,
+ const char* filename,
+ struct STAT_STRUCTURE* buffer)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || filename == nullptr || buffer == nullptr)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_Filesystem::{} - invalid data (addon='{}', filename='{}', buffer='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(filename),
+ static_cast<void*>(buffer));
+ return false;
+ }
+
+ struct __stat64 statBuffer;
+ if (CFile::Stat(filename, &statBuffer) != 0)
+ return false;
+
+ buffer->deviceId = statBuffer.st_dev;
+ buffer->fileSerialNumber = statBuffer.st_ino;
+ buffer->size = statBuffer.st_size;
+ buffer->accessTime = statBuffer.st_atime;
+ buffer->modificationTime = statBuffer.st_mtime;
+ buffer->statusTime = statBuffer.st_ctime;
+ buffer->isDirectory = S_ISDIR(statBuffer.st_mode);
+ buffer->isSymLink = S_ISLNK(statBuffer.st_mode);
+ buffer->isBlock = S_ISBLK(statBuffer.st_mode);
+ buffer->isCharacter = S_ISCHR(statBuffer.st_mode);
+ buffer->isFifo = S_ISFIFO(statBuffer.st_mode);
+ buffer->isRegular = S_ISREG(statBuffer.st_mode);
+ buffer->isSocket = S_ISSOCK(statBuffer.st_mode);
+
+ return true;
+}
+
+bool Interface_Filesystem::delete_file(void* kodiBase, const char* filename)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || filename == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', filename='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(filename));
+ return false;
+ }
+
+ return CFile::Delete(filename);
+}
+
+bool Interface_Filesystem::rename_file(void* kodiBase,
+ const char* filename,
+ const char* newFileName)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || filename == nullptr || newFileName == nullptr)
+ {
+ CLog::Log(
+ LOGERROR,
+ "Interface_Filesystem::{} - invalid data (addon='{}', filename='{}', newFileName='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(filename),
+ static_cast<const void*>(newFileName));
+ return false;
+ }
+
+ return CFile::Rename(filename, newFileName);
+}
+
+bool Interface_Filesystem::copy_file(void* kodiBase, const char* filename, const char* dest)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || filename == nullptr || dest == nullptr)
+ {
+ CLog::Log(
+ LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', filename='{}', dest='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(filename), static_cast<const void*>(dest));
+ return false;
+ }
+
+ return CFile::Copy(filename, dest);
+}
+
+char* Interface_Filesystem::get_file_md5(void* kodiBase, const char* filename)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || filename == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', filename='{})",
+ __FUNCTION__, kodiBase, static_cast<const void*>(filename));
+ return nullptr;
+ }
+
+ std::string string = CUtil::GetFileDigest(filename, KODI::UTILITY::CDigest::Type::MD5);
+ char* buffer = strdup(string.c_str());
+ return buffer;
+}
+
+char* Interface_Filesystem::get_cache_thumb_name(void* kodiBase, const char* filename)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || filename == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', filename='{})",
+ __FUNCTION__, kodiBase, static_cast<const void*>(filename));
+ return nullptr;
+ }
+
+ const auto crc = Crc32::ComputeFromLowerCase(filename);
+ const auto hex = StringUtils::Format("{:08x}.tbn", crc);
+ char* buffer = strdup(hex.c_str());
+ return buffer;
+}
+
+char* Interface_Filesystem::make_legal_filename(void* kodiBase, const char* filename)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || filename == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', filename='{})",
+ __FUNCTION__, kodiBase, static_cast<const void*>(filename));
+ return nullptr;
+ }
+
+ std::string string = CUtil::MakeLegalFileName(filename);
+ char* buffer = strdup(string.c_str());
+ return buffer;
+}
+
+char* Interface_Filesystem::make_legal_path(void* kodiBase, const char* path)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', path='{})",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path));
+ return nullptr;
+ }
+
+ std::string string = CUtil::MakeLegalPath(path);
+ char* buffer = strdup(string.c_str());
+ return buffer;
+}
+
+char* Interface_Filesystem::translate_special_protocol(void* kodiBase, const char* strSource)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || strSource == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', strSource='{})",
+ __FUNCTION__, kodiBase, static_cast<const void*>(strSource));
+ return nullptr;
+ }
+
+ return strdup(CSpecialProtocol::TranslatePath(strSource).c_str());
+}
+
+bool Interface_Filesystem::get_disk_space(
+ void* kodiBase, const char* path, uint64_t* capacity, uint64_t* free, uint64_t* available)
+{
+ using namespace KODI::PLATFORM::FILESYSTEM;
+
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr || capacity == nullptr || free == nullptr ||
+ available == nullptr)
+ {
+ CLog::Log(
+ LOGERROR,
+ "Interface_Filesystem::{} - invalid data (addon='{}', path='{}, capacity='{}, free='{}, "
+ "available='{})",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path), static_cast<void*>(capacity),
+ static_cast<void*>(free), static_cast<void*>(available));
+ return false;
+ }
+
+ std::error_code ec;
+ auto freeSpace = space(CSpecialProtocol::TranslatePath(path), ec);
+ if (ec.value() != 0)
+ return false;
+
+ *capacity = freeSpace.capacity;
+ *free = freeSpace.free;
+ *available = freeSpace.available;
+ return true;
+}
+
+bool Interface_Filesystem::is_internet_stream(void* kodiBase, const char* path, bool strictCheck)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', path='{})",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path));
+ return false;
+ }
+
+ return URIUtils::IsInternetStream(path, strictCheck);
+}
+
+bool Interface_Filesystem::is_on_lan(void* kodiBase, const char* path)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', path='{})",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path));
+ return false;
+ }
+
+ return URIUtils::IsOnLAN(path);
+}
+
+bool Interface_Filesystem::is_remote(void* kodiBase, const char* path)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', path='{})",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path));
+ return false;
+ }
+
+ return URIUtils::IsRemote(path);
+}
+
+bool Interface_Filesystem::is_local(void* kodiBase, const char* path)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', path='{})",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path));
+ return false;
+ }
+
+ return CURL(path).IsLocal();
+}
+
+bool Interface_Filesystem::is_url(void* kodiBase, const char* path)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || path == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', path='{})",
+ __FUNCTION__, kodiBase, static_cast<const void*>(path));
+ return false;
+ }
+
+ return URIUtils::IsURL(path);
+}
+
+bool Interface_Filesystem::get_mime_type(void* kodiBase,
+ const char* url,
+ char** content,
+ const char* useragent)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || url == nullptr || content == nullptr || useragent == nullptr)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_Filesystem::{} - invalid data (addon='{}', url='{}', content='{}', "
+ "useragent='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(url),
+ static_cast<const void*>(content), static_cast<const void*>(useragent));
+ return false;
+ }
+
+ std::string kodiContent;
+ bool ret = XFILE::CCurlFile::GetMimeType(CURL(url), kodiContent, useragent);
+ if (ret && !kodiContent.empty())
+ {
+ *content = strdup(kodiContent.c_str());
+ }
+ return ret;
+}
+
+bool Interface_Filesystem::get_content_type(void* kodiBase,
+ const char* url,
+ char** content,
+ const char* useragent)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || url == nullptr || content == nullptr || useragent == nullptr)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_Filesystem::{} - invalid data (addon='{}', url='{}', content='{}', "
+ "useragent='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(url),
+ static_cast<const void*>(content), static_cast<const void*>(useragent));
+ return false;
+ }
+
+ std::string kodiContent;
+ bool ret = XFILE::CCurlFile::GetContentType(CURL(url), kodiContent, useragent);
+ if (ret && !kodiContent.empty())
+ {
+ *content = strdup(kodiContent.c_str());
+ }
+ return ret;
+}
+
+bool Interface_Filesystem::get_cookies(void* kodiBase, const char* url, char** cookies)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || url == nullptr || cookies == nullptr)
+ {
+ CLog::Log(
+ LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', url='{}', cookies='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(url), static_cast<const void*>(cookies));
+ return false;
+ }
+
+ std::string kodiCookies;
+ bool ret = XFILE::CCurlFile::GetCookies(CURL(url), kodiCookies);
+ if (ret && !kodiCookies.empty())
+ {
+ *cookies = strdup(kodiCookies.c_str());
+ }
+ return ret;
+}
+
+bool Interface_Filesystem::get_http_header(void* kodiBase,
+ const char* url,
+ struct KODI_HTTP_HEADER* headers)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || url == nullptr || headers == nullptr || headers->handle == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data pointer given", __func__);
+ return false;
+ }
+
+ CHttpHeader* httpHeader = static_cast<CHttpHeader*>(headers->handle);
+ return XFILE::CCurlFile::GetHttpHeader(CURL(url), *httpHeader);
+}
+
+//------------------------------------------------------------------------------
+
+bool Interface_Filesystem::http_header_create(void* kodiBase, struct KODI_HTTP_HEADER* headers)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || headers == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', headers='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(headers));
+ return false;
+ }
+
+ headers->handle = new CHttpHeader;
+ headers->get_value = http_header_get_value;
+ headers->get_values = http_header_get_values;
+ headers->get_header = http_header_get_header;
+ headers->get_mime_type = http_header_get_mime_type;
+ headers->get_charset = http_header_get_charset;
+ headers->get_proto_line = http_header_get_proto_line;
+
+ return true;
+}
+
+void Interface_Filesystem::http_header_free(void* kodiBase, struct KODI_HTTP_HEADER* headers)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || headers == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', headers='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(headers));
+ return;
+ }
+
+ delete static_cast<CHttpHeader*>(headers->handle);
+ headers->handle = nullptr;
+}
+
+char* Interface_Filesystem::http_header_get_value(void* kodiBase, void* handle, const char* param)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || handle == nullptr || param == nullptr)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_Filesystem::{} - invalid data (addon='{}', handle='{}', param='{}')",
+ __FUNCTION__, kodiBase, handle, static_cast<const void*>(param));
+ return nullptr;
+ }
+
+ std::string string = static_cast<CHttpHeader*>(handle)->GetValue(param);
+
+ char* buffer = nullptr;
+ if (!string.empty())
+ buffer = strdup(string.c_str());
+ return buffer;
+}
+
+char** Interface_Filesystem::http_header_get_values(void* kodiBase,
+ void* handle,
+ const char* param,
+ int* length)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || handle == nullptr || param == nullptr || length == nullptr)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_Filesystem::{} - invalid data (addon='{}', handle='{}', param='{}', "
+ "length='{}')",
+ __FUNCTION__, kodiBase, handle, static_cast<const void*>(param),
+ static_cast<const void*>(length));
+ return nullptr;
+ }
+
+
+ std::vector<std::string> values = static_cast<CHttpHeader*>(handle)->GetValues(param);
+ *length = values.size();
+ char** ret = static_cast<char**>(malloc(sizeof(char*) * values.size()));
+ for (int i = 0; i < *length; ++i)
+ {
+ ret[i] = strdup(values[i].c_str());
+ }
+ return ret;
+}
+
+char* Interface_Filesystem::http_header_get_header(void* kodiBase, void* handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || handle == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', handle='{}')",
+ __FUNCTION__, kodiBase, handle);
+ return nullptr;
+ }
+
+ std::string string = static_cast<CHttpHeader*>(handle)->GetHeader();
+
+ char* buffer = nullptr;
+ if (!string.empty())
+ buffer = strdup(string.c_str());
+ return buffer;
+}
+
+char* Interface_Filesystem::http_header_get_mime_type(void* kodiBase, void* handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || handle == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', handle='{}')",
+ __FUNCTION__, kodiBase, handle);
+ return nullptr;
+ }
+
+ std::string string = static_cast<CHttpHeader*>(handle)->GetMimeType();
+
+ char* buffer = nullptr;
+ if (!string.empty())
+ buffer = strdup(string.c_str());
+ return buffer;
+}
+
+char* Interface_Filesystem::http_header_get_charset(void* kodiBase, void* handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || handle == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', handle='{}')",
+ __FUNCTION__, kodiBase, handle);
+ return nullptr;
+ }
+
+ std::string string = static_cast<CHttpHeader*>(handle)->GetCharset();
+
+ char* buffer = nullptr;
+ if (!string.empty())
+ buffer = strdup(string.c_str());
+ return buffer;
+}
+
+char* Interface_Filesystem::http_header_get_proto_line(void* kodiBase, void* handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || handle == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', handle='{}')",
+ __FUNCTION__, kodiBase, handle);
+ return nullptr;
+ }
+
+ std::string string = static_cast<CHttpHeader*>(handle)->GetProtoLine();
+
+ char* buffer = nullptr;
+ if (!string.empty())
+ buffer = strdup(string.c_str());
+ return buffer;
+}
+
+//------------------------------------------------------------------------------
+
+void* Interface_Filesystem::open_file(void* kodiBase, const char* filename, unsigned int flags)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || filename == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', filename='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(filename));
+ return nullptr;
+ }
+
+ CFile* file = new CFile;
+ if (file->Open(filename, TranslateFileReadBitsToKodi(flags)))
+ return static_cast<void*>(file);
+
+ delete file;
+ return nullptr;
+}
+
+void* Interface_Filesystem::open_file_for_write(void* kodiBase,
+ const char* filename,
+ bool overwrite)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || filename == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', filename='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(filename));
+ return nullptr;
+ }
+
+ CFile* file = new CFile;
+ if (file->OpenForWrite(filename, overwrite))
+ return static_cast<void*>(file);
+
+ delete file;
+ return nullptr;
+}
+
+ssize_t Interface_Filesystem::read_file(void* kodiBase, void* file, void* ptr, size_t size)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr || ptr == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', file='{}', ptr='{}')",
+ __FUNCTION__, kodiBase, file, ptr);
+ return -1;
+ }
+
+ return static_cast<CFile*>(file)->Read(ptr, size);
+}
+
+bool Interface_Filesystem::read_file_string(void* kodiBase,
+ void* file,
+ char* szLine,
+ int lineLength)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr || szLine == nullptr)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_Filesystem::{} - invalid data (addon='{}', file='{}', szLine=='{}')",
+ __FUNCTION__, kodiBase, file, static_cast<void*>(szLine));
+ return false;
+ }
+
+ return static_cast<CFile*>(file)->ReadString(szLine, lineLength);
+}
+
+ssize_t Interface_Filesystem::write_file(void* kodiBase, void* file, const void* ptr, size_t size)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr || ptr == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', file='{}', ptr='{}')",
+ __FUNCTION__, kodiBase, file, ptr);
+ return -1;
+ }
+
+ return static_cast<CFile*>(file)->Write(ptr, size);
+}
+
+void Interface_Filesystem::flush_file(void* kodiBase, void* file)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', file='{}')",
+ __FUNCTION__, kodiBase, file);
+ return;
+ }
+
+ static_cast<CFile*>(file)->Flush();
+}
+
+int64_t Interface_Filesystem::seek_file(void* kodiBase, void* file, int64_t position, int whence)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', file='{}')",
+ __FUNCTION__, kodiBase, file);
+ return -1;
+ }
+
+ return static_cast<CFile*>(file)->Seek(position, whence);
+}
+
+int Interface_Filesystem::truncate_file(void* kodiBase, void* file, int64_t size)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', file='{}')",
+ __FUNCTION__, kodiBase, file);
+ return -1;
+ }
+
+ return static_cast<CFile*>(file)->Truncate(size);
+}
+
+int64_t Interface_Filesystem::get_file_position(void* kodiBase, void* file)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', file='{}')",
+ __FUNCTION__, kodiBase, file);
+ return -1;
+ }
+
+ return static_cast<CFile*>(file)->GetPosition();
+}
+
+int64_t Interface_Filesystem::get_file_length(void* kodiBase, void* file)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', file='{}')",
+ __FUNCTION__, kodiBase, file);
+ return -1;
+ }
+
+ return static_cast<CFile*>(file)->GetLength();
+}
+
+double Interface_Filesystem::get_file_download_speed(void* kodiBase, void* file)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', file='{}')",
+ __FUNCTION__, kodiBase, file);
+ return 0.0;
+ }
+
+ return static_cast<CFile*>(file)->GetDownloadSpeed();
+}
+
+void Interface_Filesystem::close_file(void* kodiBase, void* file)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', file='{}')",
+ __FUNCTION__, kodiBase, file);
+ return;
+ }
+
+ static_cast<CFile*>(file)->Close();
+ delete static_cast<CFile*>(file);
+}
+
+int Interface_Filesystem::get_file_chunk_size(void* kodiBase, void* file)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_VFS::{} - invalid data (addon='{}', file='{}')", __FUNCTION__,
+ kodiBase, file);
+ return -1;
+ }
+
+ return static_cast<CFile*>(file)->GetChunkSize();
+}
+
+bool Interface_Filesystem::io_control_get_seek_possible(void* kodiBase, void* file)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_VFS::{} - invalid data (addon='{}', file='{}')", __FUNCTION__,
+ kodiBase, file);
+ return false;
+ }
+
+ return static_cast<CFile*>(file)->IoControl(EIoControl::IOCTRL_SEEK_POSSIBLE, nullptr) != 0
+ ? true
+ : false;
+}
+
+bool Interface_Filesystem::io_control_get_cache_status(void* kodiBase,
+ void* file,
+ struct VFS_CACHE_STATUS_DATA* status)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr || status == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_VFS::{} - invalid data (addon='{}', file='{}, status='{}')",
+ __FUNCTION__, kodiBase, file, static_cast<const void*>(status));
+ return false;
+ }
+
+ SCacheStatus data = {};
+ int ret = static_cast<CFile*>(file)->IoControl(EIoControl::IOCTRL_CACHE_STATUS, &data);
+ if (ret >= 0)
+ {
+ status->forward = data.forward;
+ status->maxrate = data.maxrate;
+ status->currate = data.currate;
+ status->lowrate = data.lowrate;
+ return true;
+ }
+ return false;
+}
+
+bool Interface_Filesystem::io_control_set_cache_rate(void* kodiBase, void* file, uint32_t rate)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_VFS::{} - invalid data (addon='{}', file='{}')", __FUNCTION__,
+ kodiBase, file);
+ return false;
+ }
+
+ return static_cast<CFile*>(file)->IoControl(EIoControl::IOCTRL_CACHE_SETRATE, &rate) >= 0 ? true
+ : false;
+}
+
+bool Interface_Filesystem::io_control_set_retry(void* kodiBase, void* file, bool retry)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_VFS::{} - invalid data (addon='{}', file='{}')", __FUNCTION__,
+ kodiBase, file);
+ return false;
+ }
+
+ return static_cast<CFile*>(file)->IoControl(EIoControl::IOCTRL_SET_RETRY, &retry) >= 0 ? true
+ : false;
+}
+
+char** Interface_Filesystem::get_property_values(
+ void* kodiBase, void* file, int type, const char* name, int* numValues)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr || name == nullptr || numValues == nullptr)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_Filesystem::{} - invalid data (addon='{}', file='{}', name='{}', "
+ "numValues='{}')",
+ __FUNCTION__, kodiBase, file, static_cast<const void*>(name),
+ static_cast<void*>(numValues));
+ return nullptr;
+ }
+
+ XFILE::FileProperty internalType;
+ switch (type)
+ {
+ case ADDON_FILE_PROPERTY_RESPONSE_PROTOCOL:
+ internalType = XFILE::FILE_PROPERTY_RESPONSE_PROTOCOL;
+ break;
+ case ADDON_FILE_PROPERTY_RESPONSE_HEADER:
+ internalType = XFILE::FILE_PROPERTY_RESPONSE_HEADER;
+ break;
+ case ADDON_FILE_PROPERTY_CONTENT_TYPE:
+ internalType = XFILE::FILE_PROPERTY_CONTENT_TYPE;
+ break;
+ case ADDON_FILE_PROPERTY_CONTENT_CHARSET:
+ internalType = XFILE::FILE_PROPERTY_CONTENT_CHARSET;
+ break;
+ case ADDON_FILE_PROPERTY_MIME_TYPE:
+ internalType = XFILE::FILE_PROPERTY_MIME_TYPE;
+ break;
+ case ADDON_FILE_PROPERTY_EFFECTIVE_URL:
+ internalType = XFILE::FILE_PROPERTY_EFFECTIVE_URL;
+ break;
+ default:
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', file='{}')",
+ __FUNCTION__, kodiBase, file);
+ return nullptr;
+ };
+ std::vector<std::string> values =
+ static_cast<CFile*>(file)->GetPropertyValues(internalType, name);
+ *numValues = values.size();
+ char** ret = static_cast<char**>(malloc(sizeof(char*) * values.size()));
+ for (int i = 0; i < *numValues; ++i)
+ {
+ ret[i] = strdup(values[i].c_str());
+ }
+ return ret;
+}
+
+void* Interface_Filesystem::curl_create(void* kodiBase, const char* url)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || url == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', url='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(url));
+ return nullptr;
+ }
+
+ CFile* file = new CFile;
+ if (file->CURLCreate(url))
+ return static_cast<void*>(file);
+
+ delete file;
+ return nullptr;
+}
+
+bool Interface_Filesystem::curl_add_option(
+ void* kodiBase, void* file, int type, const char* name, const char* value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr || name == nullptr || value == nullptr)
+ {
+ CLog::Log(
+ LOGERROR,
+ "Interface_Filesystem::{} - invalid data (addon='{}', file='{}', name='{}', value='{}')",
+ __FUNCTION__, kodiBase, file, static_cast<const void*>(name),
+ static_cast<const void*>(value));
+ return false;
+ }
+
+ XFILE::CURLOPTIONTYPE internalType;
+ switch (type)
+ {
+ case ADDON_CURL_OPTION_OPTION:
+ internalType = XFILE::CURL_OPTION_OPTION;
+ break;
+ case ADDON_CURL_OPTION_PROTOCOL:
+ internalType = XFILE::CURL_OPTION_PROTOCOL;
+ break;
+ case ADDON_CURL_OPTION_CREDENTIALS:
+ internalType = XFILE::CURL_OPTION_CREDENTIALS;
+ break;
+ case ADDON_CURL_OPTION_HEADER:
+ internalType = XFILE::CURL_OPTION_HEADER;
+ break;
+ default:
+ throw std::logic_error("Interface_Filesystem::curl_add_option - invalid curl option type");
+ return false;
+ };
+
+ return static_cast<CFile*>(file)->CURLAddOption(internalType, name, value);
+}
+
+bool Interface_Filesystem::curl_open(void* kodiBase, void* file, unsigned int flags)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || file == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Filesystem::{} - invalid data (addon='{}', file='{}')",
+ __FUNCTION__, kodiBase, file);
+ return false;
+ }
+
+ return static_cast<CFile*>(file)->CURLOpen(TranslateFileReadBitsToKodi(flags));
+}
+
+} /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/Filesystem.h b/xbmc/addons/interfaces/Filesystem.h
new file mode 100644
index 0000000..55e90ce
--- /dev/null
+++ b/xbmc/addons/interfaces/Filesystem.h
@@ -0,0 +1,131 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/Filesystem.h"
+
+extern "C"
+{
+
+struct VFSDirEntry;
+struct AddonGlobalInterface;
+
+namespace ADDON
+{
+
+struct Interface_Filesystem
+{
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ static unsigned int TranslateFileReadBitsToKodi(unsigned int addonFlags);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note For add of new functions use the "_" style to identify direct a
+ * add-on callback function. Everything with CamelCase is only for the
+ * usage in Kodi only.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ ///@{
+ static bool can_open_directory(void* kodiBase, const char* url);
+ static bool create_directory(void* kodiBase, const char* path);
+ static bool directory_exists(void* kodiBase, const char* path);
+ static bool remove_directory(void* kodiBase, const char* path);
+ static bool remove_directory_recursive(void* kodiBase, const char* path);
+ static bool get_directory(void* kodiBase,
+ const char* path,
+ const char* mask,
+ struct VFSDirEntry** items,
+ unsigned int* num_items);
+ static void free_directory(void* kodiBase, struct VFSDirEntry* items, unsigned int num_items);
+
+ static bool file_exists(void* kodiBase, const char* filename, bool useCache);
+ static bool stat_file(void* kodiBase, const char* filename, struct STAT_STRUCTURE* buffer);
+ static bool delete_file(void* kodiBase, const char* filename);
+ static bool rename_file(void* kodiBase, const char* filename, const char* newFileName);
+ static bool copy_file(void* kodiBase, const char* filename, const char* dest);
+ static char* get_file_md5(void* kodiBase, const char* filename);
+ static char* get_cache_thumb_name(void* kodiBase, const char* filename);
+ static char* make_legal_filename(void* kodiBase, const char* filename);
+ static char* make_legal_path(void* kodiBase, const char* path);
+ static char* translate_special_protocol(void* kodiBase, const char* strSource);
+ static bool get_disk_space(
+ void* kodiBase, const char* path, uint64_t* capacity, uint64_t* free, uint64_t* available);
+ static bool is_internet_stream(void* kodiBase, const char* path, bool strictCheck);
+ static bool is_on_lan(void* kodiBase, const char* path);
+ static bool is_remote(void* kodiBase, const char* path);
+ static bool is_local(void* kodiBase, const char* path);
+ static bool is_url(void* kodiBase, const char* path);
+
+ static bool get_http_header(void* kodiBase, const char* url, struct KODI_HTTP_HEADER* headers);
+ static bool get_mime_type(void* kodiBase, const char* url, char** content, const char* useragent);
+ static bool get_content_type(void* kodiBase,
+ const char* url,
+ char** content,
+ const char* useragent);
+ static bool get_cookies(void* kodiBase, const char* url, char** cookies);
+
+ /*!
+ * @brief Callback functions addon class kodi::vfs::CFile
+ */
+ ///@{
+ static bool http_header_create(void* kodiBase, struct KODI_HTTP_HEADER* headers);
+ static void http_header_free(void* kodiBase, struct KODI_HTTP_HEADER* headers);
+
+ static char* http_header_get_value(void* kodiBase, void* handle, const char* param);
+ static char** http_header_get_values(void* kodiBase,
+ void* handle,
+ const char* param,
+ int* length);
+ static char* http_header_get_header(void* kodiBase, void* handle);
+ static char* http_header_get_mime_type(void* kodiBase, void* handle);
+ static char* http_header_get_charset(void* kodiBase, void* handle);
+ static char* http_header_get_proto_line(void* kodiBase, void* handle);
+ ///@}
+
+ /*!
+ * @brief Callback functions addon class kodi::vfs::CFile
+ */
+ ///@{
+ static void* open_file(void* kodiBase, const char* filename, unsigned int flags);
+ static void* open_file_for_write(void* kodiBase, const char* filename, bool overwrite);
+ static ssize_t read_file(void* kodiBase, void* file, void* ptr, size_t size);
+ static bool read_file_string(void* kodiBase, void* file, char* szLine, int lineLength);
+ static ssize_t write_file(void* kodiBase, void* file, const void* ptr, size_t size);
+ static void flush_file(void* kodiBase, void* file);
+ static int64_t seek_file(void* kodiBase, void* file, int64_t position, int whence);
+ static int truncate_file(void* kodiBase, void* file, int64_t size);
+ static int64_t get_file_position(void* kodiBase, void* file);
+ static int64_t get_file_length(void* kodiBase, void* file);
+ static double get_file_download_speed(void* kodiBase, void* file);
+ static void close_file(void* kodiBase, void* file);
+ static int get_file_chunk_size(void* kodiBase, void* file);
+ static bool io_control_get_seek_possible(void* kodiBase, void* file);
+ static bool io_control_get_cache_status(void* kodiBase,
+ void* file,
+ struct VFS_CACHE_STATUS_DATA* status);
+ static bool io_control_set_cache_rate(void* kodiBase, void* file, uint32_t rate);
+ static bool io_control_set_retry(void* kodiBase, void* file, bool retry);
+ static char** get_property_values(
+ void* kodiBase, void* file, int type, const char* name, int* numValues);
+
+ static void* curl_create(void* kodiBase, const char* url);
+ static bool curl_add_option(
+ void* kodiBase, void* file, int type, const char* name, const char* value);
+ static bool curl_open(void* kodiBase, void* file, unsigned int flags);
+ ///@}
+ ///@}
+};
+
+} /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/General.cpp b/xbmc/addons/interfaces/General.cpp
new file mode 100644
index 0000000..eef6ccd
--- /dev/null
+++ b/xbmc/addons/interfaces/General.cpp
@@ -0,0 +1,468 @@
+/*
+ * 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 "General.h"
+
+#include "CompileInfo.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonVersion.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/General.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "input/KeyboardLayout.h"
+#include "input/KeyboardLayoutManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CharsetConverter.h"
+#include "utils/Digest.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/MemUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <string.h>
+
+using namespace kodi; // addon-dev-kit namespace
+using KODI::UTILITY::CDigest;
+
+namespace ADDON
+{
+
+void Interface_General::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi = static_cast<AddonToKodiFuncTable_kodi*>(malloc(sizeof(AddonToKodiFuncTable_kodi)));
+
+ addonInterface->toKodi->kodi->unknown_to_utf8 = unknown_to_utf8;
+ addonInterface->toKodi->kodi->get_language = get_language;
+ addonInterface->toKodi->kodi->queue_notification = queue_notification;
+ addonInterface->toKodi->kodi->get_md5 = get_md5;
+ addonInterface->toKodi->kodi->get_region = get_region;
+ addonInterface->toKodi->kodi->get_free_mem = get_free_mem;
+ addonInterface->toKodi->kodi->get_global_idle_time = get_global_idle_time;
+ addonInterface->toKodi->kodi->is_addon_avilable = is_addon_avilable;
+ addonInterface->toKodi->kodi->kodi_version = kodi_version;
+ addonInterface->toKodi->kodi->get_current_skin_id = get_current_skin_id;
+ addonInterface->toKodi->kodi->get_keyboard_layout = get_keyboard_layout;
+ addonInterface->toKodi->kodi->change_keyboard_layout = change_keyboard_layout;
+}
+
+void Interface_General::DeInit(AddonGlobalInterface* addonInterface)
+{
+ if (addonInterface->toKodi && /* <-- needed as long as the old addon way is used */
+ addonInterface->toKodi->kodi)
+ {
+ free(addonInterface->toKodi->kodi);
+ addonInterface->toKodi->kodi = nullptr;
+ }
+}
+
+char* Interface_General::unknown_to_utf8(void* kodiBase, const char* source, bool* ret, bool failOnBadChar)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon || !source || !ret)
+ {
+ CLog::Log(LOGERROR, "Interface_General::{} - invalid data (addon='{}', source='{}', ret='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(source), static_cast<void*>(ret));
+ return nullptr;
+ }
+
+ std::string string;
+ *ret = g_charsetConverter.unknownToUTF8(source, string, failOnBadChar);
+ char* buffer = strdup(string.c_str());
+ return buffer;
+}
+
+char* Interface_General::get_language(void* kodiBase, int format, bool region)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_General::{} - invalid data (addon='{}')", __FUNCTION__,
+ kodiBase);
+ return nullptr;
+ }
+
+ std::string string = g_langInfo.GetEnglishLanguageName();
+ switch (format)
+ {
+ case LANG_FMT_ISO_639_1:
+ {
+ std::string langCode;
+ g_LangCodeExpander.ConvertToISO6391(string, langCode);
+ string = langCode;
+ if (region)
+ {
+ std::string region2Code;
+ g_LangCodeExpander.ConvertToISO6391(g_langInfo.GetRegionLocale(), region2Code);
+ if (!region2Code.empty())
+ string += "-" + region2Code;
+ }
+ break;
+ }
+ case LANG_FMT_ISO_639_2:
+ {
+ std::string langCode;
+ g_LangCodeExpander.ConvertToISO6392B(string, langCode);
+ string = langCode;
+ if (region)
+ {
+ std::string region3Code;
+ g_LangCodeExpander.ConvertToISO6392B(g_langInfo.GetRegionLocale(), region3Code);
+ if (!region3Code.empty())
+ string += "-" + region3Code;
+ }
+ break;
+ }
+ case LANG_FMT_ENGLISH_NAME:
+ default:
+ {
+ if (region)
+ string += "-" + g_langInfo.GetCurrentRegion();
+ break;
+ }
+ }
+
+ char* buffer = strdup(string.c_str());
+ return buffer;
+}
+
+bool Interface_General::queue_notification(void* kodiBase, int type, const char* header,
+ const char* message, const char* imageFile,
+ unsigned int displayTime, bool withSound,
+ unsigned int messageTime)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || message == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_General::{} - invalid data (addon='{}', message='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(message));
+ return false;
+ }
+
+ std::string usedHeader;
+ if (header && strlen(header) > 0)
+ usedHeader = header;
+ else
+ usedHeader = addon->Name();
+
+ QueueMsg qtype = static_cast<QueueMsg>(type);
+
+ if (qtype != QUEUE_OWN_STYLE)
+ {
+ CGUIDialogKaiToast::eMessageType usedType;
+ switch (qtype)
+ {
+ case QueueMsg::QUEUE_WARNING:
+ usedType = CGUIDialogKaiToast::Warning;
+ withSound = true;
+ CLog::Log(LOGDEBUG, "Interface_General::{} - {} - Warning Message: '{}'", __FUNCTION__,
+ addon->Name(), message);
+ break;
+ case QueueMsg::QUEUE_ERROR:
+ usedType = CGUIDialogKaiToast::Error;
+ withSound = true;
+ CLog::Log(LOGDEBUG, "Interface_General::{} - {} - Error Message : '{}'", __FUNCTION__,
+ addon->Name(), message);
+ break;
+ case QueueMsg::QUEUE_INFO:
+ default:
+ usedType = CGUIDialogKaiToast::Info;
+ withSound = false;
+ CLog::Log(LOGDEBUG, "Interface_General::{} - {} - Info Message : '{}'", __FUNCTION__,
+ addon->Name(), message);
+ break;
+ }
+
+ if (imageFile && strlen(imageFile) > 0)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_General::{} - To use given image file '{}' must be type value set to "
+ "'QUEUE_OWN_STYLE'",
+ __FUNCTION__, imageFile);
+ }
+
+ CGUIDialogKaiToast::QueueNotification(usedType, usedHeader, message, 3000, withSound);
+ }
+ else
+ {
+ CGUIDialogKaiToast::QueueNotification(imageFile, usedHeader, message, displayTime, withSound, messageTime);
+ }
+ return true;
+}
+
+void Interface_General::get_md5(void* kodiBase, const char* text, char* md5)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || text == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_General::{} - invalid data (addon='{}', text='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(text));
+ return;
+ }
+
+ std::string md5Int = CDigest::Calculate(CDigest::Type::MD5, std::string(text));
+ strncpy(md5, md5Int.c_str(), 40);
+}
+
+char* Interface_General::get_region(void* kodiBase, const char* id)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || id == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_General::{} - invalid data (addon='{}', id='{}')", __FUNCTION__,
+ kodiBase, static_cast<const void*>(id));
+ return nullptr;
+ }
+
+ std::string result;
+ if (StringUtils::CompareNoCase(id, "datelong") == 0)
+ {
+ result = g_langInfo.GetDateFormat(true);
+ StringUtils::Replace(result, "DDDD", "%A");
+ StringUtils::Replace(result, "MMMM", "%B");
+ StringUtils::Replace(result, "D", "%d");
+ StringUtils::Replace(result, "YYYY", "%Y");
+ }
+ else if (StringUtils::CompareNoCase(id, "dateshort") == 0)
+ {
+ result = g_langInfo.GetDateFormat(false);
+ StringUtils::Replace(result, "MM", "%m");
+ StringUtils::Replace(result, "DD", "%d");
+#ifdef TARGET_WINDOWS
+ StringUtils::Replace(result, "M", "%#m");
+ StringUtils::Replace(result, "D", "%#d");
+#else
+ StringUtils::Replace(result, "M", "%-m");
+ StringUtils::Replace(result, "D", "%-d");
+#endif
+ StringUtils::Replace(result, "YYYY", "%Y");
+ }
+ else if (StringUtils::CompareNoCase(id, "tempunit") == 0)
+ result = g_langInfo.GetTemperatureUnitString();
+ else if (StringUtils::CompareNoCase(id, "speedunit") == 0)
+ result = g_langInfo.GetSpeedUnitString();
+ else if (StringUtils::CompareNoCase(id, "time") == 0)
+ {
+ result = g_langInfo.GetTimeFormat();
+ StringUtils::Replace(result, "H", "%H");
+ StringUtils::Replace(result, "h", "%I");
+ StringUtils::Replace(result, "mm", "%M");
+ StringUtils::Replace(result, "ss", "%S");
+ StringUtils::Replace(result, "xx", "%p");
+ }
+ else if (StringUtils::CompareNoCase(id, "meridiem") == 0)
+ result = StringUtils::Format("{}/{}", g_langInfo.GetMeridiemSymbol(MeridiemSymbolAM),
+ g_langInfo.GetMeridiemSymbol(MeridiemSymbolPM));
+ else
+ {
+ CLog::Log(LOGERROR, "Interface_General::{} - add-on '{}' requests invalid id '{}'",
+ __FUNCTION__, addon->Name(), id);
+ return nullptr;
+ }
+
+ char* buffer = strdup(result.c_str());
+ return buffer;
+}
+
+void Interface_General::get_free_mem(void* kodiBase, long* free, long* total, bool as_bytes)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || free == nullptr || total == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_General::{} - invalid data (addon='{}', free='{}', total='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(free), static_cast<void*>(total));
+ return;
+ }
+
+ KODI::MEMORY::MemoryStatus stat;
+ KODI::MEMORY::GetMemoryStatus(&stat);
+ *free = static_cast<long>(stat.availPhys);
+ *total = static_cast<long>(stat.totalPhys);
+ if (!as_bytes)
+ {
+ *free = *free / ( 1024 * 1024 );
+ *total = *total / ( 1024 * 1024 );
+ }
+}
+
+int Interface_General::get_global_idle_time(void* kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_General::{} - invalid data (addon='{}')", __FUNCTION__,
+ kodiBase);
+ return -1;
+ }
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ return appPower->GlobalIdleTime();
+}
+
+bool Interface_General::is_addon_avilable(void* kodiBase,
+ const char* id,
+ char** version,
+ bool* enabled)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || id == nullptr || version == nullptr || enabled == nullptr)
+ {
+ CLog::Log(
+ LOGERROR,
+ "Interface_General::{} - invalid data (addon='{}', id='{}', version='{}', enabled='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(id), static_cast<void*>(version),
+ static_cast<void*>(enabled));
+ return false;
+ }
+
+ AddonPtr addonInfo;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(id, addonInfo, OnlyEnabled::CHOICE_NO))
+ return false;
+
+ *version = strdup(addonInfo->Version().asString().c_str());
+ *enabled = !CServiceBroker::GetAddonMgr().IsAddonDisabled(id);
+ return true;
+}
+
+void Interface_General::kodi_version(void* kodiBase, char** compile_name, int* major, int* minor, char** revision, char** tag, char** tagversion)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || compile_name == nullptr || major == nullptr || minor == nullptr ||
+ revision == nullptr || tag == nullptr || tagversion == nullptr)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_General::{} - invalid data (addon='{}', compile_name='{}', major='{}', "
+ "minor='{}', revision='{}', tag='{}', tagversion='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(compile_name), static_cast<void*>(major),
+ static_cast<void*>(minor), static_cast<void*>(revision), static_cast<void*>(tag),
+ static_cast<void*>(tagversion));
+ return;
+ }
+
+ *compile_name = strdup(CCompileInfo::GetAppName());
+ *major = CCompileInfo::GetMajor();
+ *minor = CCompileInfo::GetMinor();
+ *revision = strdup(CCompileInfo::GetSCMID());
+ std::string tagStr = CCompileInfo::GetSuffix();
+ if (StringUtils::StartsWithNoCase(tagStr, "alpha"))
+ {
+ *tag = strdup("alpha");
+ *tagversion = strdup(StringUtils::Mid(tagStr, 5).c_str());
+ }
+ else if (StringUtils::StartsWithNoCase(tagStr, "beta"))
+ {
+ *tag = strdup("beta");
+ *tagversion = strdup(StringUtils::Mid(tagStr, 4).c_str());
+ }
+ else if (StringUtils::StartsWithNoCase(tagStr, "rc"))
+ {
+ *tag = strdup("releasecandidate");
+ *tagversion = strdup(StringUtils::Mid(tagStr, 2).c_str());
+ }
+ else if (tagStr.empty())
+ *tag = strdup("stable");
+ else
+ *tag = strdup("prealpha");
+}
+
+char* Interface_General::get_current_skin_id(void* kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_General::{} - invalid data (addon='{}')", __FUNCTION__,
+ kodiBase);
+ return nullptr;
+ }
+
+ return strdup(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN).c_str());
+}
+
+bool Interface_General::get_keyboard_layout(void* kodiBase, char** layout_name, int modifier_key, AddonKeyboardKeyTable* c_layout)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || c_layout == nullptr || layout_name == nullptr)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_General::{} - invalid data (addon='{}', c_layout='{}', layout_name='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(c_layout),
+ static_cast<void*>(layout_name));
+ return false;
+ }
+
+ std::string activeLayout = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_ACTIVEKEYBOARDLAYOUT);
+
+ CKeyboardLayout layout;
+ if (!CServiceBroker::GetKeyboardLayoutManager()->GetLayout(activeLayout, layout))
+ return false;
+
+ *layout_name = strdup(layout.GetName().c_str());
+
+ unsigned int modifiers = CKeyboardLayout::ModifierKeyNone;
+ if (modifier_key & STD_KB_MODIFIER_KEY_SHIFT)
+ modifiers |= CKeyboardLayout::ModifierKeyShift;
+ if (modifier_key & STD_KB_MODIFIER_KEY_SYMBOL)
+ modifiers |= CKeyboardLayout::ModifierKeySymbol;
+
+ for (unsigned int row = 0; row < STD_KB_BUTTONS_MAX_ROWS; row++)
+ {
+ for (unsigned int column = 0; column < STD_KB_BUTTONS_PER_ROW; column++)
+ {
+ std::string label = layout.GetCharAt(row, column, modifiers);
+ c_layout->keys[row][column] = strdup(label.c_str());
+ }
+ }
+
+ return true;
+}
+
+bool Interface_General::change_keyboard_layout(void* kodiBase, char** layout_name)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || layout_name == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_General::{} - invalid data (addon='{}', layout_name='{}')",
+ __FUNCTION__, kodiBase, static_cast<void*>(layout_name));
+ return false;
+ }
+
+ std::vector<CKeyboardLayout> layouts;
+ unsigned int currentLayout = 0;
+
+ const KeyboardLayouts& keyboardLayouts = CServiceBroker::GetKeyboardLayoutManager()->GetLayouts();
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::vector<CVariant> layoutNames = settings->GetList(CSettings::SETTING_LOCALE_KEYBOARDLAYOUTS);
+ std::string activeLayout = settings->GetString(CSettings::SETTING_LOCALE_ACTIVEKEYBOARDLAYOUT);
+
+ for (const auto& layoutName : layoutNames)
+ {
+ const auto keyboardLayout = keyboardLayouts.find(layoutName.asString());
+ if (keyboardLayout != keyboardLayouts.end())
+ {
+ layouts.emplace_back(keyboardLayout->second);
+ if (layoutName.asString() == activeLayout)
+ currentLayout = layouts.size() - 1;
+ }
+ }
+
+ currentLayout++;
+ if (currentLayout >= layouts.size())
+ currentLayout = 0;
+ CKeyboardLayout layout = layouts.empty() ? CKeyboardLayout() : layouts[currentLayout];
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_LOCALE_ACTIVEKEYBOARDLAYOUT, layout.GetName());
+
+ *layout_name = strdup(layout.GetName().c_str());
+ return true;
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/General.h b/xbmc/addons/interfaces/General.h
new file mode 100644
index 0000000..6dde356
--- /dev/null
+++ b/xbmc/addons/interfaces/General.h
@@ -0,0 +1,76 @@
+/*
+ * 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
+
+extern "C"
+{
+
+struct AddonGlobalInterface;
+struct AddonKeyboardKeyTable;
+
+namespace ADDON
+{
+
+/*!
+ * @brief Global general Add-on to Kodi callback functions
+ *
+ * To hold general functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/General.h"
+ */
+struct Interface_General
+{
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static char* unknown_to_utf8(void* kodiBase, const char* source, bool* ret, bool failOnBadChar);
+ static char* get_language(void* kodiBase, int format, bool region);
+ static bool queue_notification(void* kodiBase,
+ int type,
+ const char* header,
+ const char* message,
+ const char* imageFile,
+ unsigned int displayTime,
+ bool withSound,
+ unsigned int messageTime);
+ static void get_md5(void* kodiBase, const char* text, char* md5);
+ static char* get_region(void* kodiBase, const char* id);
+ static void get_free_mem(void* kodiInstance, long* free, long* total, bool as_bytes);
+ static int get_global_idle_time(void* kodiBase);
+ static bool is_addon_avilable(void* kodiBase, const char* id, char** version, bool* enabled);
+ static void kodi_version(void* kodiBase,
+ char** compile_name,
+ int* major,
+ int* minor,
+ char** revision,
+ char** tag,
+ char** tagversion);
+ static char* get_current_skin_id(void* kodiBase);
+ static bool change_keyboard_layout(void* kodiBase, char** layout_name);
+ static bool get_keyboard_layout(void* kodiBase,
+ char** layout_name,
+ int modifier_key,
+ AddonKeyboardKeyTable* c_layout);
+ //@}
+};
+
+} /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/Network.cpp b/xbmc/addons/interfaces/Network.cpp
new file mode 100644
index 0000000..63151f3
--- /dev/null
+++ b/xbmc/addons/interfaces/Network.cpp
@@ -0,0 +1,181 @@
+/*
+ * 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 "Network.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/Network.h"
+#include "network/DNSNameCache.h"
+#include "network/Network.h"
+#include "utils/SystemInfo.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_Network::Init(AddonGlobalInterface *addonInterface)
+{
+ addonInterface->toKodi->kodi_network = new AddonToKodiFuncTable_kodi_network();
+
+ addonInterface->toKodi->kodi_network->wake_on_lan = wake_on_lan;
+ addonInterface->toKodi->kodi_network->get_ip_address = get_ip_address;
+ addonInterface->toKodi->kodi_network->get_hostname = get_hostname;
+ addonInterface->toKodi->kodi_network->get_user_agent = get_user_agent;
+ addonInterface->toKodi->kodi_network->is_local_host = is_local_host;
+ addonInterface->toKodi->kodi_network->is_host_on_lan = is_host_on_lan;
+ addonInterface->toKodi->kodi_network->dns_lookup = dns_lookup;
+ addonInterface->toKodi->kodi_network->url_encode = url_encode;
+}
+
+void Interface_Network::DeInit(AddonGlobalInterface* addonInterface)
+{
+ if (addonInterface->toKodi) /* <-- needed as long as the old addon way is used */
+ {
+ delete addonInterface->toKodi->kodi_network;
+ addonInterface->toKodi->kodi_network = nullptr;
+ }
+}
+
+bool Interface_Network::wake_on_lan(void* kodiBase, const char* mac)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || mac == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Network::{} - invalid data (addon='{}', mac='{}')", __FUNCTION__,
+ kodiBase, static_cast<const void*>(mac));
+ return false;
+ }
+
+ return CServiceBroker::GetNetwork().WakeOnLan(mac);
+}
+
+char* Interface_Network::get_ip_address(void* kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Network::{} - invalid data (addon='{}')", __FUNCTION__,
+ kodiBase);
+ return nullptr;
+ }
+
+ std::string titleIP;
+ CNetworkInterface* iface = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+ if (iface)
+ titleIP = iface->GetCurrentIPAddress();
+ else
+ titleIP = "127.0.0.1";
+
+ char* buffer = nullptr;
+ if (!titleIP.empty())
+ buffer = strdup(titleIP.c_str());
+ return buffer;
+}
+
+char* Interface_Network::get_hostname(void* kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Network::{} - invalid data (addon='{}')", __FUNCTION__,
+ kodiBase);
+ return nullptr;
+ }
+
+ std::string hostname;
+ if (!CServiceBroker::GetNetwork().GetHostName(hostname))
+ return nullptr;
+
+ char* buffer = nullptr;
+ if (!hostname.empty())
+ buffer = strdup(hostname.c_str());
+ return buffer;
+}
+
+char* Interface_Network::get_user_agent(void* kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Network::{} - invalid data (addon='{}')", __FUNCTION__,
+ kodiBase);
+ return nullptr;
+ }
+
+ std::string string = CSysInfo::GetUserAgent();
+ char* buffer = nullptr;
+ if (!string.empty())
+ buffer = strdup(string.c_str());
+ return buffer;
+}
+
+bool Interface_Network::is_local_host(void* kodiBase, const char* hostname)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || hostname == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Network::{} - invalid data (addon='{}', hostname='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(hostname));
+ return false;
+ }
+
+ return CServiceBroker::GetNetwork().IsLocalHost(hostname);
+}
+
+bool Interface_Network::is_host_on_lan(void* kodiBase, const char* hostname, bool offLineCheck)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || hostname == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Network::{} - invalid data (addon='{}', hostname='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(hostname));
+ return false;
+ }
+
+ return URIUtils::IsHostOnLAN(hostname, offLineCheck);
+}
+
+char* Interface_Network::dns_lookup(void* kodiBase, const char* url, bool* ret)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || url == nullptr || ret == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Network::{} - invalid data (addon='{}', url='{}', ret='{}')",
+ __FUNCTION__, kodiBase, static_cast<const void*>(url), static_cast<void*>(ret));
+ return nullptr;
+ }
+
+ std::string string;
+ *ret = CDNSNameCache::Lookup(url, string);
+ char* buffer = nullptr;
+ if (!string.empty())
+ buffer = strdup(string.c_str());
+ return buffer;
+}
+
+char* Interface_Network::url_encode(void* kodiBase, const char* url)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (addon == nullptr || url == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_Network::{} - invalid data (addon='{}', url='{}')", __FUNCTION__,
+ kodiBase, static_cast<const void*>(url));
+ return nullptr;
+ }
+
+ std::string string = CURL::Encode(url);
+ char* buffer = nullptr;
+ if (!string.empty())
+ buffer = strdup(string.c_str());
+ return buffer;
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/Network.h b/xbmc/addons/interfaces/Network.h
new file mode 100644
index 0000000..b704fb1
--- /dev/null
+++ b/xbmc/addons/interfaces/Network.h
@@ -0,0 +1,54 @@
+/*
+ * 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
+
+struct AddonGlobalInterface;
+
+extern "C"
+{
+namespace ADDON
+{
+
+/*!
+ * @brief Global general Add-on to Kodi callback functions
+ *
+ * To hold network functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/Network.h"
+ */
+struct Interface_Network
+{
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static bool wake_on_lan(void* kodiBase, const char* mac);
+ static char* get_ip_address(void* kodiBase);
+ static char* get_hostname(void* kodiBase);
+ static char* get_user_agent(void* kodiBase);
+ static bool is_local_host(void* kodiBase, const char* hostname);
+ static bool is_host_on_lan(void* kodiBase, const char* hostname, bool offLineCheck);
+ static char* dns_lookup(void* kodiBase, const char* url, bool* ret);
+ static char* url_encode(void* kodiBase, const char* url);
+ //@}
+};
+
+} /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/CMakeLists.txt b/xbmc/addons/interfaces/gui/CMakeLists.txt
new file mode 100644
index 0000000..3508b82
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES GUITranslator.cpp
+ General.cpp
+ ListItem.cpp
+ Window.cpp)
+
+set(HEADERS GUITranslator.h
+ General.h
+ ListItem.h
+ Window.h)
+
+core_add_library(addons_interfaces_gui)
diff --git a/xbmc/addons/interfaces/gui/GUITranslator.cpp b/xbmc/addons/interfaces/gui/GUITranslator.cpp
new file mode 100644
index 0000000..2db2601
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/GUITranslator.cpp
@@ -0,0 +1,969 @@
+/*
+ * Copyright (C) 2005-2020 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 "GUITranslator.h"
+
+#include "input/actions/ActionIDs.h"
+
+using namespace ADDON;
+
+ADDON_ACTION CAddonGUITranslator::TranslateActionIdToAddon(int kodiId)
+{
+ switch (kodiId)
+ {
+ case ACTION_NONE:
+ return ADDON_ACTION_NONE;
+ case ACTION_MOVE_LEFT:
+ return ADDON_ACTION_MOVE_LEFT;
+ case ACTION_MOVE_RIGHT:
+ return ADDON_ACTION_MOVE_RIGHT;
+ case ACTION_MOVE_UP:
+ return ADDON_ACTION_MOVE_UP;
+ case ACTION_MOVE_DOWN:
+ return ADDON_ACTION_MOVE_DOWN;
+ case ACTION_PAGE_UP:
+ return ADDON_ACTION_PAGE_UP;
+ case ACTION_PAGE_DOWN:
+ return ADDON_ACTION_PAGE_DOWN;
+ case ACTION_SELECT_ITEM:
+ return ADDON_ACTION_SELECT_ITEM;
+ case ACTION_HIGHLIGHT_ITEM:
+ return ADDON_ACTION_HIGHLIGHT_ITEM;
+ case ACTION_PARENT_DIR:
+ return ADDON_ACTION_PARENT_DIR;
+ case ACTION_PREVIOUS_MENU:
+ return ADDON_ACTION_PREVIOUS_MENU;
+ case ACTION_SHOW_INFO:
+ return ADDON_ACTION_SHOW_INFO;
+ case ACTION_PAUSE:
+ return ADDON_ACTION_PAUSE;
+ case ACTION_STOP:
+ return ADDON_ACTION_STOP;
+ case ACTION_NEXT_ITEM:
+ return ADDON_ACTION_NEXT_ITEM;
+ case ACTION_PREV_ITEM:
+ return ADDON_ACTION_PREV_ITEM;
+ case ACTION_FORWARD:
+ return ADDON_ACTION_FORWARD;
+ case ACTION_REWIND:
+ return ADDON_ACTION_REWIND;
+ case ACTION_SHOW_GUI:
+ return ADDON_ACTION_SHOW_GUI;
+ case ACTION_ASPECT_RATIO:
+ return ADDON_ACTION_ASPECT_RATIO;
+ case ACTION_STEP_FORWARD:
+ return ADDON_ACTION_STEP_FORWARD;
+ case ACTION_STEP_BACK:
+ return ADDON_ACTION_STEP_BACK;
+ case ACTION_BIG_STEP_FORWARD:
+ return ADDON_ACTION_BIG_STEP_FORWARD;
+ case ACTION_BIG_STEP_BACK:
+ return ADDON_ACTION_BIG_STEP_BACK;
+ case ACTION_SHOW_OSD:
+ return ADDON_ACTION_SHOW_OSD;
+ case ACTION_SHOW_SUBTITLES:
+ return ADDON_ACTION_SHOW_SUBTITLES;
+ case ACTION_NEXT_SUBTITLE:
+ return ADDON_ACTION_NEXT_SUBTITLE;
+ case ACTION_PLAYER_DEBUG:
+ return ADDON_ACTION_PLAYER_DEBUG;
+ case ACTION_NEXT_PICTURE:
+ return ADDON_ACTION_NEXT_PICTURE;
+ case ACTION_PREV_PICTURE:
+ return ADDON_ACTION_PREV_PICTURE;
+ case ACTION_ZOOM_OUT:
+ return ADDON_ACTION_ZOOM_OUT;
+ case ACTION_ZOOM_IN:
+ return ADDON_ACTION_ZOOM_IN;
+ case ACTION_TOGGLE_SOURCE_DEST:
+ return ADDON_ACTION_TOGGLE_SOURCE_DEST;
+ case ACTION_SHOW_PLAYLIST:
+ return ADDON_ACTION_SHOW_PLAYLIST;
+ case ACTION_QUEUE_ITEM:
+ return ADDON_ACTION_QUEUE_ITEM;
+ case ACTION_REMOVE_ITEM:
+ return ADDON_ACTION_REMOVE_ITEM;
+ case ACTION_SHOW_FULLSCREEN:
+ return ADDON_ACTION_SHOW_FULLSCREEN;
+ case ACTION_ZOOM_LEVEL_NORMAL:
+ return ADDON_ACTION_ZOOM_LEVEL_NORMAL;
+ case ACTION_ZOOM_LEVEL_1:
+ return ADDON_ACTION_ZOOM_LEVEL_1;
+ case ACTION_ZOOM_LEVEL_2:
+ return ADDON_ACTION_ZOOM_LEVEL_2;
+ case ACTION_ZOOM_LEVEL_3:
+ return ADDON_ACTION_ZOOM_LEVEL_3;
+ case ACTION_ZOOM_LEVEL_4:
+ return ADDON_ACTION_ZOOM_LEVEL_4;
+ case ACTION_ZOOM_LEVEL_5:
+ return ADDON_ACTION_ZOOM_LEVEL_5;
+ case ACTION_ZOOM_LEVEL_6:
+ return ADDON_ACTION_ZOOM_LEVEL_6;
+ case ACTION_ZOOM_LEVEL_7:
+ return ADDON_ACTION_ZOOM_LEVEL_7;
+ case ACTION_ZOOM_LEVEL_8:
+ return ADDON_ACTION_ZOOM_LEVEL_8;
+ case ACTION_ZOOM_LEVEL_9:
+ return ADDON_ACTION_ZOOM_LEVEL_9;
+ case ACTION_CALIBRATE_SWAP_ARROWS:
+ return ADDON_ACTION_CALIBRATE_SWAP_ARROWS;
+ case ACTION_CALIBRATE_RESET:
+ return ADDON_ACTION_CALIBRATE_RESET;
+ case ACTION_ANALOG_MOVE:
+ return ADDON_ACTION_ANALOG_MOVE;
+ case ACTION_ROTATE_PICTURE_CW:
+ return ADDON_ACTION_ROTATE_PICTURE_CW;
+ case ACTION_ROTATE_PICTURE_CCW:
+ return ADDON_ACTION_ROTATE_PICTURE_CCW;
+ case ACTION_SUBTITLE_DELAY_MIN:
+ return ADDON_ACTION_SUBTITLE_DELAY_MIN;
+ case ACTION_SUBTITLE_DELAY_PLUS:
+ return ADDON_ACTION_SUBTITLE_DELAY_PLUS;
+ case ACTION_AUDIO_DELAY_MIN:
+ return ADDON_ACTION_AUDIO_DELAY_MIN;
+ case ACTION_AUDIO_DELAY_PLUS:
+ return ADDON_ACTION_AUDIO_DELAY_PLUS;
+ case ACTION_AUDIO_NEXT_LANGUAGE:
+ return ADDON_ACTION_AUDIO_NEXT_LANGUAGE;
+ case ACTION_CHANGE_RESOLUTION:
+ return ADDON_ACTION_CHANGE_RESOLUTION;
+ case REMOTE_0:
+ return ADDON_ACTION_REMOTE_0;
+ case REMOTE_1:
+ return ADDON_ACTION_REMOTE_1;
+ case REMOTE_2:
+ return ADDON_ACTION_REMOTE_2;
+ case REMOTE_3:
+ return ADDON_ACTION_REMOTE_3;
+ case REMOTE_4:
+ return ADDON_ACTION_REMOTE_4;
+ case REMOTE_5:
+ return ADDON_ACTION_REMOTE_5;
+ case REMOTE_6:
+ return ADDON_ACTION_REMOTE_6;
+ case REMOTE_7:
+ return ADDON_ACTION_REMOTE_7;
+ case REMOTE_8:
+ return ADDON_ACTION_REMOTE_8;
+ case REMOTE_9:
+ return ADDON_ACTION_REMOTE_9;
+ case ACTION_PLAYER_PROCESS_INFO:
+ return ADDON_ACTION_PLAYER_PROCESS_INFO;
+ case ACTION_PLAYER_PROGRAM_SELECT:
+ return ADDON_ACTION_PLAYER_PROGRAM_SELECT;
+ case ACTION_PLAYER_RESOLUTION_SELECT:
+ return ADDON_ACTION_PLAYER_RESOLUTION_SELECT;
+ case ACTION_SMALL_STEP_BACK:
+ return ADDON_ACTION_SMALL_STEP_BACK;
+ case ACTION_PLAYER_FORWARD:
+ return ADDON_ACTION_PLAYER_FORWARD;
+ case ACTION_PLAYER_REWIND:
+ return ADDON_ACTION_PLAYER_REWIND;
+ case ACTION_PLAYER_PLAY:
+ return ADDON_ACTION_PLAYER_PLAY;
+ case ACTION_DELETE_ITEM:
+ return ADDON_ACTION_DELETE_ITEM;
+ case ACTION_COPY_ITEM:
+ return ADDON_ACTION_COPY_ITEM;
+ case ACTION_MOVE_ITEM:
+ return ADDON_ACTION_MOVE_ITEM;
+ case ACTION_TAKE_SCREENSHOT:
+ return ADDON_ACTION_TAKE_SCREENSHOT;
+ case ACTION_RENAME_ITEM:
+ return ADDON_ACTION_RENAME_ITEM;
+ case ACTION_VOLUME_UP:
+ return ADDON_ACTION_VOLUME_UP;
+ case ACTION_VOLUME_DOWN:
+ return ADDON_ACTION_VOLUME_DOWN;
+ case ACTION_VOLAMP:
+ return ADDON_ACTION_VOLAMP;
+ case ACTION_MUTE:
+ return ADDON_ACTION_MUTE;
+ case ACTION_NAV_BACK:
+ return ADDON_ACTION_NAV_BACK;
+ case ACTION_VOLAMP_UP:
+ return ADDON_ACTION_VOLAMP_UP;
+ case ACTION_VOLAMP_DOWN:
+ return ADDON_ACTION_VOLAMP_DOWN;
+ case ACTION_CREATE_EPISODE_BOOKMARK:
+ return ADDON_ACTION_CREATE_EPISODE_BOOKMARK;
+ case ACTION_CREATE_BOOKMARK:
+ return ADDON_ACTION_CREATE_BOOKMARK;
+ case ACTION_CHAPTER_OR_BIG_STEP_FORWARD:
+ return ADDON_ACTION_CHAPTER_OR_BIG_STEP_FORWARD;
+ case ACTION_CHAPTER_OR_BIG_STEP_BACK:
+ return ADDON_ACTION_CHAPTER_OR_BIG_STEP_BACK;
+ case ACTION_CYCLE_SUBTITLE:
+ return ADDON_ACTION_CYCLE_SUBTITLE;
+ case ACTION_MOUSE_LEFT_CLICK:
+ return ADDON_ACTION_MOUSE_LEFT_CLICK;
+ case ACTION_MOUSE_RIGHT_CLICK:
+ return ADDON_ACTION_MOUSE_RIGHT_CLICK;
+ case ACTION_MOUSE_MIDDLE_CLICK:
+ return ADDON_ACTION_MOUSE_MIDDLE_CLICK;
+ case ACTION_MOUSE_DOUBLE_CLICK:
+ return ADDON_ACTION_MOUSE_DOUBLE_CLICK;
+ case ACTION_MOUSE_WHEEL_UP:
+ return ADDON_ACTION_MOUSE_WHEEL_UP;
+ case ACTION_MOUSE_WHEEL_DOWN:
+ return ADDON_ACTION_MOUSE_WHEEL_DOWN;
+ case ACTION_MOUSE_DRAG:
+ return ADDON_ACTION_MOUSE_DRAG;
+ case ACTION_MOUSE_MOVE:
+ return ADDON_ACTION_MOUSE_MOVE;
+ case ACTION_MOUSE_LONG_CLICK:
+ return ADDON_ACTION_MOUSE_LONG_CLICK;
+ case ACTION_MOUSE_DRAG_END:
+ return ADDON_ACTION_MOUSE_DRAG_END;
+ case ACTION_BACKSPACE:
+ return ADDON_ACTION_BACKSPACE;
+ case ACTION_SCROLL_UP:
+ return ADDON_ACTION_SCROLL_UP;
+ case ACTION_SCROLL_DOWN:
+ return ADDON_ACTION_SCROLL_DOWN;
+ case ACTION_ANALOG_FORWARD:
+ return ADDON_ACTION_ANALOG_FORWARD;
+ case ACTION_ANALOG_REWIND:
+ return ADDON_ACTION_ANALOG_REWIND;
+ case ACTION_MOVE_ITEM_UP:
+ return ADDON_ACTION_MOVE_ITEM_UP;
+ case ACTION_MOVE_ITEM_DOWN:
+ return ADDON_ACTION_MOVE_ITEM_DOWN;
+ case ACTION_CONTEXT_MENU:
+ return ADDON_ACTION_CONTEXT_MENU;
+ case ACTION_SHIFT:
+ return ADDON_ACTION_SHIFT;
+ case ACTION_SYMBOLS:
+ return ADDON_ACTION_SYMBOLS;
+ case ACTION_CURSOR_LEFT:
+ return ADDON_ACTION_CURSOR_LEFT;
+ case ACTION_CURSOR_RIGHT:
+ return ADDON_ACTION_CURSOR_RIGHT;
+ case ACTION_BUILT_IN_FUNCTION:
+ return ADDON_ACTION_BUILT_IN_FUNCTION;
+ case ACTION_SHOW_OSD_TIME:
+ return ADDON_ACTION_SHOW_OSD_TIME;
+ case ACTION_ANALOG_SEEK_FORWARD:
+ return ADDON_ACTION_ANALOG_SEEK_FORWARD;
+ case ACTION_ANALOG_SEEK_BACK:
+ return ADDON_ACTION_ANALOG_SEEK_BACK;
+ case ACTION_VIS_PRESET_SHOW:
+ return ADDON_ACTION_VIS_PRESET_SHOW;
+ case ACTION_VIS_PRESET_NEXT:
+ return ADDON_ACTION_VIS_PRESET_NEXT;
+ case ACTION_VIS_PRESET_PREV:
+ return ADDON_ACTION_VIS_PRESET_PREV;
+ case ACTION_VIS_PRESET_LOCK:
+ return ADDON_ACTION_VIS_PRESET_LOCK;
+ case ACTION_VIS_PRESET_RANDOM:
+ return ADDON_ACTION_VIS_PRESET_RANDOM;
+ case ACTION_VIS_RATE_PRESET_PLUS:
+ return ADDON_ACTION_VIS_RATE_PRESET_PLUS;
+ case ACTION_VIS_RATE_PRESET_MINUS:
+ return ADDON_ACTION_VIS_RATE_PRESET_MINUS;
+ case ACTION_SHOW_VIDEOMENU:
+ return ADDON_ACTION_SHOW_VIDEOMENU;
+ case ACTION_ENTER:
+ return ADDON_ACTION_ENTER;
+ case ACTION_INCREASE_RATING:
+ return ADDON_ACTION_INCREASE_RATING;
+ case ACTION_DECREASE_RATING:
+ return ADDON_ACTION_DECREASE_RATING;
+ case ACTION_NEXT_SCENE:
+ return ADDON_ACTION_NEXT_SCENE;
+ case ACTION_PREV_SCENE:
+ return ADDON_ACTION_PREV_SCENE;
+ case ACTION_NEXT_LETTER:
+ return ADDON_ACTION_NEXT_LETTER;
+ case ACTION_PREV_LETTER:
+ return ADDON_ACTION_PREV_LETTER;
+ case ACTION_JUMP_SMS2:
+ return ADDON_ACTION_JUMP_SMS2;
+ case ACTION_JUMP_SMS3:
+ return ADDON_ACTION_JUMP_SMS3;
+ case ACTION_JUMP_SMS4:
+ return ADDON_ACTION_JUMP_SMS4;
+ case ACTION_JUMP_SMS5:
+ return ADDON_ACTION_JUMP_SMS5;
+ case ACTION_JUMP_SMS6:
+ return ADDON_ACTION_JUMP_SMS6;
+ case ACTION_JUMP_SMS7:
+ return ADDON_ACTION_JUMP_SMS7;
+ case ACTION_JUMP_SMS8:
+ return ADDON_ACTION_JUMP_SMS8;
+ case ACTION_JUMP_SMS9:
+ return ADDON_ACTION_JUMP_SMS9;
+ case ACTION_FILTER_CLEAR:
+ return ADDON_ACTION_FILTER_CLEAR;
+ case ACTION_FILTER_SMS2:
+ return ADDON_ACTION_FILTER_SMS2;
+ case ACTION_FILTER_SMS3:
+ return ADDON_ACTION_FILTER_SMS3;
+ case ACTION_FILTER_SMS4:
+ return ADDON_ACTION_FILTER_SMS4;
+ case ACTION_FILTER_SMS5:
+ return ADDON_ACTION_FILTER_SMS5;
+ case ACTION_FILTER_SMS6:
+ return ADDON_ACTION_FILTER_SMS6;
+ case ACTION_FILTER_SMS7:
+ return ADDON_ACTION_FILTER_SMS7;
+ case ACTION_FILTER_SMS8:
+ return ADDON_ACTION_FILTER_SMS8;
+ case ACTION_FILTER_SMS9:
+ return ADDON_ACTION_FILTER_SMS9;
+ case ACTION_FIRST_PAGE:
+ return ADDON_ACTION_FIRST_PAGE;
+ case ACTION_LAST_PAGE:
+ return ADDON_ACTION_LAST_PAGE;
+ case ACTION_AUDIO_DELAY:
+ return ADDON_ACTION_AUDIO_DELAY;
+ case ACTION_SUBTITLE_DELAY:
+ return ADDON_ACTION_SUBTITLE_DELAY;
+ case ACTION_MENU:
+ return ADDON_ACTION_MENU;
+ case ACTION_SET_RATING:
+ return ADDON_ACTION_SET_RATING;
+ case ACTION_RECORD:
+ return ADDON_ACTION_RECORD;
+ case ACTION_PASTE:
+ return ADDON_ACTION_PASTE;
+ case ACTION_NEXT_CONTROL:
+ return ADDON_ACTION_NEXT_CONTROL;
+ case ACTION_PREV_CONTROL:
+ return ADDON_ACTION_PREV_CONTROL;
+ case ACTION_CHANNEL_SWITCH:
+ return ADDON_ACTION_CHANNEL_SWITCH;
+ case ACTION_CHANNEL_UP:
+ return ADDON_ACTION_CHANNEL_UP;
+ case ACTION_CHANNEL_DOWN:
+ return ADDON_ACTION_CHANNEL_DOWN;
+ case ACTION_NEXT_CHANNELGROUP:
+ return ADDON_ACTION_NEXT_CHANNELGROUP;
+ case ACTION_PREVIOUS_CHANNELGROUP:
+ return ADDON_ACTION_PREVIOUS_CHANNELGROUP;
+ case ACTION_PVR_PLAY:
+ return ADDON_ACTION_PVR_PLAY;
+ case ACTION_PVR_PLAY_TV:
+ return ADDON_ACTION_PVR_PLAY_TV;
+ case ACTION_PVR_PLAY_RADIO:
+ return ADDON_ACTION_PVR_PLAY_RADIO;
+ case ACTION_PVR_SHOW_TIMER_RULE:
+ return ADDON_ACTION_PVR_SHOW_TIMER_RULE;
+ case ACTION_CHANNEL_NUMBER_SEP:
+ return ADDON_ACTION_CHANNEL_NUMBER_SEP;
+ case ACTION_PVR_ANNOUNCE_REMINDERS:
+ return ADDON_ACTION_PVR_ANNOUNCE_REMINDERS;
+ case ACTION_TOGGLE_FULLSCREEN:
+ return ADDON_ACTION_TOGGLE_FULLSCREEN;
+ case ACTION_TOGGLE_WATCHED:
+ return ADDON_ACTION_TOGGLE_WATCHED;
+ case ACTION_SCAN_ITEM:
+ return ADDON_ACTION_SCAN_ITEM;
+ case ACTION_TOGGLE_DIGITAL_ANALOG:
+ return ADDON_ACTION_TOGGLE_DIGITAL_ANALOG;
+ case ACTION_RELOAD_KEYMAPS:
+ return ADDON_ACTION_RELOAD_KEYMAPS;
+ case ACTION_GUIPROFILE_BEGIN:
+ return ADDON_ACTION_GUIPROFILE_BEGIN;
+ case ACTION_TELETEXT_RED:
+ return ADDON_ACTION_TELETEXT_RED;
+ case ACTION_TELETEXT_GREEN:
+ return ADDON_ACTION_TELETEXT_GREEN;
+ case ACTION_TELETEXT_YELLOW:
+ return ADDON_ACTION_TELETEXT_YELLOW;
+ case ACTION_TELETEXT_BLUE:
+ return ADDON_ACTION_TELETEXT_BLUE;
+ case ACTION_INCREASE_PAR:
+ return ADDON_ACTION_INCREASE_PAR;
+ case ACTION_DECREASE_PAR:
+ return ADDON_ACTION_DECREASE_PAR;
+ case ACTION_VSHIFT_UP:
+ return ADDON_ACTION_VSHIFT_UP;
+ case ACTION_VSHIFT_DOWN:
+ return ADDON_ACTION_VSHIFT_DOWN;
+ case ACTION_PLAYER_PLAYPAUSE:
+ return ADDON_ACTION_PLAYER_PLAYPAUSE;
+ case ACTION_SUBTITLE_VSHIFT_UP:
+ return ADDON_ACTION_SUBTITLE_VSHIFT_UP;
+ case ACTION_SUBTITLE_VSHIFT_DOWN:
+ return ADDON_ACTION_SUBTITLE_VSHIFT_DOWN;
+ case ACTION_SUBTITLE_ALIGN:
+ return ADDON_ACTION_SUBTITLE_ALIGN;
+ case ACTION_FILTER:
+ return ADDON_ACTION_FILTER;
+ case ACTION_SWITCH_PLAYER:
+ return ADDON_ACTION_SWITCH_PLAYER;
+ case ACTION_STEREOMODE_NEXT:
+ return ADDON_ACTION_STEREOMODE_NEXT;
+ case ACTION_STEREOMODE_PREVIOUS:
+ return ADDON_ACTION_STEREOMODE_PREVIOUS;
+ case ACTION_STEREOMODE_TOGGLE:
+ return ADDON_ACTION_STEREOMODE_TOGGLE;
+ case ACTION_STEREOMODE_SELECT:
+ return ADDON_ACTION_STEREOMODE_SELECT;
+ case ACTION_STEREOMODE_TOMONO:
+ return ADDON_ACTION_STEREOMODE_TOMONO;
+ case ACTION_STEREOMODE_SET:
+ return ADDON_ACTION_STEREOMODE_SET;
+ case ACTION_SETTINGS_RESET:
+ return ADDON_ACTION_SETTINGS_RESET;
+ case ACTION_SETTINGS_LEVEL_CHANGE:
+ return ADDON_ACTION_SETTINGS_LEVEL_CHANGE;
+ case ACTION_TRIGGER_OSD:
+ return ADDON_ACTION_TRIGGER_OSD;
+ case ACTION_INPUT_TEXT:
+ return ADDON_ACTION_INPUT_TEXT;
+ case ACTION_VOLUME_SET:
+ return ADDON_ACTION_VOLUME_SET;
+ case ACTION_TOGGLE_COMMSKIP:
+ return ADDON_ACTION_TOGGLE_COMMSKIP;
+ case ACTION_BROWSE_SUBTITLE:
+ return ADDON_ACTION_BROWSE_SUBTITLE;
+ case ACTION_PLAYER_RESET:
+ return ADDON_ACTION_PLAYER_RESET;
+ case ACTION_TOGGLE_FONT:
+ return ADDON_ACTION_TOGGLE_FONT;
+ case ACTION_VIDEO_NEXT_STREAM:
+ return ADDON_ACTION_VIDEO_NEXT_STREAM;
+ case ACTION_QUEUE_ITEM_NEXT:
+ return ADDON_ACTION_QUEUE_ITEM_NEXT;
+ case ACTION_HDR_TOGGLE:
+ return ADDON_ACTION_HDR_TOGGLE;
+ case ACTION_VOICE_RECOGNIZE:
+ return ADDON_ACTION_VOICE_RECOGNIZE;
+ case ACTION_TOUCH_TAP:
+ return ADDON_ACTION_TOUCH_TAP;
+ case ACTION_TOUCH_TAP_TEN:
+ return ADDON_ACTION_TOUCH_TAP_TEN;
+ case ACTION_TOUCH_LONGPRESS:
+ return ADDON_ACTION_TOUCH_LONGPRESS;
+ case ACTION_TOUCH_LONGPRESS_TEN:
+ return ADDON_ACTION_TOUCH_LONGPRESS_TEN;
+ case ACTION_GESTURE_NOTIFY:
+ return ADDON_ACTION_GESTURE_NOTIFY;
+ case ACTION_GESTURE_BEGIN:
+ return ADDON_ACTION_GESTURE_BEGIN;
+ case ACTION_GESTURE_ZOOM:
+ return ADDON_ACTION_GESTURE_ZOOM;
+ case ACTION_GESTURE_ROTATE:
+ return ADDON_ACTION_GESTURE_ROTATE;
+ case ACTION_GESTURE_PAN:
+ return ADDON_ACTION_GESTURE_PAN;
+ case ACTION_GESTURE_ABORT:
+ return ADDON_ACTION_GESTURE_ABORT;
+ case ACTION_GESTURE_SWIPE_LEFT:
+ return ADDON_ACTION_GESTURE_SWIPE_LEFT;
+ case ACTION_GESTURE_SWIPE_LEFT_TEN:
+ return ADDON_ACTION_GESTURE_SWIPE_LEFT_TEN;
+ case ACTION_GESTURE_SWIPE_RIGHT:
+ return ADDON_ACTION_GESTURE_SWIPE_RIGHT;
+ case ACTION_GESTURE_SWIPE_RIGHT_TEN:
+ return ADDON_ACTION_GESTURE_SWIPE_RIGHT_TEN;
+ case ACTION_GESTURE_SWIPE_UP:
+ return ADDON_ACTION_GESTURE_SWIPE_UP;
+ case ACTION_GESTURE_SWIPE_UP_TEN:
+ return ADDON_ACTION_GESTURE_SWIPE_UP_TEN;
+ case ACTION_GESTURE_SWIPE_DOWN:
+ return ADDON_ACTION_GESTURE_SWIPE_DOWN;
+ case ACTION_GESTURE_SWIPE_DOWN_TEN:
+ return ADDON_ACTION_GESTURE_SWIPE_DOWN_TEN;
+ case ACTION_GESTURE_END:
+ return ADDON_ACTION_GESTURE_END;
+ case ACTION_ANALOG_MOVE_X_LEFT:
+ return ADDON_ACTION_ANALOG_MOVE_X_LEFT;
+ case ACTION_ANALOG_MOVE_X_RIGHT:
+ return ADDON_ACTION_ANALOG_MOVE_X_RIGHT;
+ case ACTION_ANALOG_MOVE_Y_UP:
+ return ADDON_ACTION_ANALOG_MOVE_Y_UP;
+ case ACTION_ANALOG_MOVE_Y_DOWN:
+ return ADDON_ACTION_ANALOG_MOVE_Y_DOWN;
+ case ACTION_ERROR:
+ return ADDON_ACTION_ERROR;
+ case ACTION_NOOP:
+ default:
+ return ADDON_ACTION_NOOP;
+ }
+}
+
+int CAddonGUITranslator::TranslateActionIdToKodi(ADDON_ACTION addonId)
+{
+ switch (addonId)
+ {
+ case ADDON_ACTION_NONE:
+ return ACTION_NONE;
+ case ADDON_ACTION_MOVE_LEFT:
+ return ACTION_MOVE_LEFT;
+ case ADDON_ACTION_MOVE_RIGHT:
+ return ACTION_MOVE_RIGHT;
+ case ADDON_ACTION_MOVE_UP:
+ return ACTION_MOVE_UP;
+ case ADDON_ACTION_MOVE_DOWN:
+ return ACTION_MOVE_DOWN;
+ case ADDON_ACTION_PAGE_UP:
+ return ACTION_PAGE_UP;
+ case ADDON_ACTION_PAGE_DOWN:
+ return ACTION_PAGE_DOWN;
+ case ADDON_ACTION_SELECT_ITEM:
+ return ACTION_SELECT_ITEM;
+ case ADDON_ACTION_HIGHLIGHT_ITEM:
+ return ACTION_HIGHLIGHT_ITEM;
+ case ADDON_ACTION_PARENT_DIR:
+ return ACTION_PARENT_DIR;
+ case ADDON_ACTION_PREVIOUS_MENU:
+ return ACTION_PREVIOUS_MENU;
+ case ADDON_ACTION_SHOW_INFO:
+ return ACTION_SHOW_INFO;
+ case ADDON_ACTION_PAUSE:
+ return ACTION_PAUSE;
+ case ADDON_ACTION_STOP:
+ return ACTION_STOP;
+ case ADDON_ACTION_NEXT_ITEM:
+ return ACTION_NEXT_ITEM;
+ case ADDON_ACTION_PREV_ITEM:
+ return ACTION_PREV_ITEM;
+ case ADDON_ACTION_FORWARD:
+ return ACTION_FORWARD;
+ case ADDON_ACTION_REWIND:
+ return ACTION_REWIND;
+ case ADDON_ACTION_SHOW_GUI:
+ return ACTION_SHOW_GUI;
+ case ADDON_ACTION_ASPECT_RATIO:
+ return ACTION_ASPECT_RATIO;
+ case ADDON_ACTION_STEP_FORWARD:
+ return ACTION_STEP_FORWARD;
+ case ADDON_ACTION_STEP_BACK:
+ return ACTION_STEP_BACK;
+ case ADDON_ACTION_BIG_STEP_FORWARD:
+ return ACTION_BIG_STEP_FORWARD;
+ case ADDON_ACTION_BIG_STEP_BACK:
+ return ACTION_BIG_STEP_BACK;
+ case ADDON_ACTION_SHOW_OSD:
+ return ACTION_SHOW_OSD;
+ case ADDON_ACTION_SHOW_SUBTITLES:
+ return ACTION_SHOW_SUBTITLES;
+ case ADDON_ACTION_NEXT_SUBTITLE:
+ return ACTION_NEXT_SUBTITLE;
+ case ADDON_ACTION_PLAYER_DEBUG:
+ return ACTION_PLAYER_DEBUG;
+ case ADDON_ACTION_NEXT_PICTURE:
+ return ACTION_NEXT_PICTURE;
+ case ADDON_ACTION_PREV_PICTURE:
+ return ACTION_PREV_PICTURE;
+ case ADDON_ACTION_ZOOM_OUT:
+ return ACTION_ZOOM_OUT;
+ case ADDON_ACTION_ZOOM_IN:
+ return ACTION_ZOOM_IN;
+ case ADDON_ACTION_TOGGLE_SOURCE_DEST:
+ return ACTION_TOGGLE_SOURCE_DEST;
+ case ADDON_ACTION_SHOW_PLAYLIST:
+ return ACTION_SHOW_PLAYLIST;
+ case ADDON_ACTION_QUEUE_ITEM:
+ return ACTION_QUEUE_ITEM;
+ case ADDON_ACTION_REMOVE_ITEM:
+ return ACTION_REMOVE_ITEM;
+ case ADDON_ACTION_SHOW_FULLSCREEN:
+ return ACTION_SHOW_FULLSCREEN;
+ case ADDON_ACTION_ZOOM_LEVEL_NORMAL:
+ return ACTION_ZOOM_LEVEL_NORMAL;
+ case ADDON_ACTION_ZOOM_LEVEL_1:
+ return ACTION_ZOOM_LEVEL_1;
+ case ADDON_ACTION_ZOOM_LEVEL_2:
+ return ACTION_ZOOM_LEVEL_2;
+ case ADDON_ACTION_ZOOM_LEVEL_3:
+ return ACTION_ZOOM_LEVEL_3;
+ case ADDON_ACTION_ZOOM_LEVEL_4:
+ return ACTION_ZOOM_LEVEL_4;
+ case ADDON_ACTION_ZOOM_LEVEL_5:
+ return ACTION_ZOOM_LEVEL_5;
+ case ADDON_ACTION_ZOOM_LEVEL_6:
+ return ACTION_ZOOM_LEVEL_6;
+ case ADDON_ACTION_ZOOM_LEVEL_7:
+ return ACTION_ZOOM_LEVEL_7;
+ case ADDON_ACTION_ZOOM_LEVEL_8:
+ return ACTION_ZOOM_LEVEL_8;
+ case ADDON_ACTION_ZOOM_LEVEL_9:
+ return ACTION_ZOOM_LEVEL_9;
+ case ADDON_ACTION_CALIBRATE_SWAP_ARROWS:
+ return ACTION_CALIBRATE_SWAP_ARROWS;
+ case ADDON_ACTION_CALIBRATE_RESET:
+ return ACTION_CALIBRATE_RESET;
+ case ADDON_ACTION_ANALOG_MOVE:
+ return ACTION_ANALOG_MOVE;
+ case ADDON_ACTION_ROTATE_PICTURE_CW:
+ return ACTION_ROTATE_PICTURE_CW;
+ case ADDON_ACTION_ROTATE_PICTURE_CCW:
+ return ACTION_ROTATE_PICTURE_CCW;
+ case ADDON_ACTION_SUBTITLE_DELAY_MIN:
+ return ACTION_SUBTITLE_DELAY_MIN;
+ case ADDON_ACTION_SUBTITLE_DELAY_PLUS:
+ return ACTION_SUBTITLE_DELAY_PLUS;
+ case ADDON_ACTION_AUDIO_DELAY_MIN:
+ return ACTION_AUDIO_DELAY_MIN;
+ case ADDON_ACTION_AUDIO_DELAY_PLUS:
+ return ACTION_AUDIO_DELAY_PLUS;
+ case ADDON_ACTION_AUDIO_NEXT_LANGUAGE:
+ return ACTION_AUDIO_NEXT_LANGUAGE;
+ case ADDON_ACTION_CHANGE_RESOLUTION:
+ return ACTION_CHANGE_RESOLUTION;
+ case ADDON_ACTION_REMOTE_0:
+ return REMOTE_0;
+ case ADDON_ACTION_REMOTE_1:
+ return REMOTE_1;
+ case ADDON_ACTION_REMOTE_2:
+ return REMOTE_2;
+ case ADDON_ACTION_REMOTE_3:
+ return REMOTE_3;
+ case ADDON_ACTION_REMOTE_4:
+ return REMOTE_4;
+ case ADDON_ACTION_REMOTE_5:
+ return REMOTE_5;
+ case ADDON_ACTION_REMOTE_6:
+ return REMOTE_6;
+ case ADDON_ACTION_REMOTE_7:
+ return REMOTE_7;
+ case ADDON_ACTION_REMOTE_8:
+ return REMOTE_8;
+ case ADDON_ACTION_REMOTE_9:
+ return REMOTE_9;
+ case ADDON_ACTION_PLAYER_PROCESS_INFO:
+ return ACTION_PLAYER_PROCESS_INFO;
+ case ADDON_ACTION_PLAYER_PROGRAM_SELECT:
+ return ACTION_PLAYER_PROGRAM_SELECT;
+ case ADDON_ACTION_PLAYER_RESOLUTION_SELECT:
+ return ACTION_PLAYER_RESOLUTION_SELECT;
+ case ADDON_ACTION_SMALL_STEP_BACK:
+ return ACTION_SMALL_STEP_BACK;
+ case ADDON_ACTION_PLAYER_FORWARD:
+ return ACTION_PLAYER_FORWARD;
+ case ADDON_ACTION_PLAYER_REWIND:
+ return ACTION_PLAYER_REWIND;
+ case ADDON_ACTION_PLAYER_PLAY:
+ return ACTION_PLAYER_PLAY;
+ case ADDON_ACTION_DELETE_ITEM:
+ return ACTION_DELETE_ITEM;
+ case ADDON_ACTION_COPY_ITEM:
+ return ACTION_COPY_ITEM;
+ case ADDON_ACTION_MOVE_ITEM:
+ return ACTION_MOVE_ITEM;
+ case ADDON_ACTION_TAKE_SCREENSHOT:
+ return ACTION_TAKE_SCREENSHOT;
+ case ADDON_ACTION_RENAME_ITEM:
+ return ACTION_RENAME_ITEM;
+ case ADDON_ACTION_VOLUME_UP:
+ return ACTION_VOLUME_UP;
+ case ADDON_ACTION_VOLUME_DOWN:
+ return ACTION_VOLUME_DOWN;
+ case ADDON_ACTION_VOLAMP:
+ return ACTION_VOLAMP;
+ case ADDON_ACTION_MUTE:
+ return ACTION_MUTE;
+ case ADDON_ACTION_NAV_BACK:
+ return ACTION_NAV_BACK;
+ case ADDON_ACTION_VOLAMP_UP:
+ return ACTION_VOLAMP_UP;
+ case ADDON_ACTION_VOLAMP_DOWN:
+ return ACTION_VOLAMP_DOWN;
+ case ADDON_ACTION_CREATE_EPISODE_BOOKMARK:
+ return ACTION_CREATE_EPISODE_BOOKMARK;
+ case ADDON_ACTION_CREATE_BOOKMARK:
+ return ACTION_CREATE_BOOKMARK;
+ case ADDON_ACTION_CHAPTER_OR_BIG_STEP_FORWARD:
+ return ACTION_CHAPTER_OR_BIG_STEP_FORWARD;
+ case ADDON_ACTION_CHAPTER_OR_BIG_STEP_BACK:
+ return ACTION_CHAPTER_OR_BIG_STEP_BACK;
+ case ADDON_ACTION_CYCLE_SUBTITLE:
+ return ACTION_CYCLE_SUBTITLE;
+ case ADDON_ACTION_MOUSE_LEFT_CLICK:
+ return ACTION_MOUSE_LEFT_CLICK;
+ case ADDON_ACTION_MOUSE_RIGHT_CLICK:
+ return ACTION_MOUSE_RIGHT_CLICK;
+ case ADDON_ACTION_MOUSE_MIDDLE_CLICK:
+ return ACTION_MOUSE_MIDDLE_CLICK;
+ case ADDON_ACTION_MOUSE_DOUBLE_CLICK:
+ return ACTION_MOUSE_DOUBLE_CLICK;
+ case ADDON_ACTION_MOUSE_WHEEL_UP:
+ return ACTION_MOUSE_WHEEL_UP;
+ case ADDON_ACTION_MOUSE_WHEEL_DOWN:
+ return ACTION_MOUSE_WHEEL_DOWN;
+ case ADDON_ACTION_MOUSE_DRAG:
+ return ACTION_MOUSE_DRAG;
+ case ADDON_ACTION_MOUSE_MOVE:
+ return ACTION_MOUSE_MOVE;
+ case ADDON_ACTION_MOUSE_LONG_CLICK:
+ return ACTION_MOUSE_LONG_CLICK;
+ case ADDON_ACTION_MOUSE_DRAG_END:
+ return ACTION_MOUSE_DRAG_END;
+ case ADDON_ACTION_BACKSPACE:
+ return ACTION_BACKSPACE;
+ case ADDON_ACTION_SCROLL_UP:
+ return ACTION_SCROLL_UP;
+ case ADDON_ACTION_SCROLL_DOWN:
+ return ACTION_SCROLL_DOWN;
+ case ADDON_ACTION_ANALOG_FORWARD:
+ return ACTION_ANALOG_FORWARD;
+ case ADDON_ACTION_ANALOG_REWIND:
+ return ACTION_ANALOG_REWIND;
+ case ADDON_ACTION_MOVE_ITEM_UP:
+ return ACTION_MOVE_ITEM_UP;
+ case ADDON_ACTION_MOVE_ITEM_DOWN:
+ return ACTION_MOVE_ITEM_DOWN;
+ case ADDON_ACTION_CONTEXT_MENU:
+ return ACTION_CONTEXT_MENU;
+ case ADDON_ACTION_SHIFT:
+ return ACTION_SHIFT;
+ case ADDON_ACTION_SYMBOLS:
+ return ACTION_SYMBOLS;
+ case ADDON_ACTION_CURSOR_LEFT:
+ return ACTION_CURSOR_LEFT;
+ case ADDON_ACTION_CURSOR_RIGHT:
+ return ACTION_CURSOR_RIGHT;
+ case ADDON_ACTION_BUILT_IN_FUNCTION:
+ return ACTION_BUILT_IN_FUNCTION;
+ case ADDON_ACTION_SHOW_OSD_TIME:
+ return ACTION_SHOW_OSD_TIME;
+ case ADDON_ACTION_ANALOG_SEEK_FORWARD:
+ return ACTION_ANALOG_SEEK_FORWARD;
+ case ADDON_ACTION_ANALOG_SEEK_BACK:
+ return ACTION_ANALOG_SEEK_BACK;
+ case ADDON_ACTION_VIS_PRESET_SHOW:
+ return ACTION_VIS_PRESET_SHOW;
+ case ADDON_ACTION_VIS_PRESET_NEXT:
+ return ACTION_VIS_PRESET_NEXT;
+ case ADDON_ACTION_VIS_PRESET_PREV:
+ return ACTION_VIS_PRESET_PREV;
+ case ADDON_ACTION_VIS_PRESET_LOCK:
+ return ACTION_VIS_PRESET_LOCK;
+ case ADDON_ACTION_VIS_PRESET_RANDOM:
+ return ACTION_VIS_PRESET_RANDOM;
+ case ADDON_ACTION_VIS_RATE_PRESET_PLUS:
+ return ACTION_VIS_RATE_PRESET_PLUS;
+ case ADDON_ACTION_VIS_RATE_PRESET_MINUS:
+ return ACTION_VIS_RATE_PRESET_MINUS;
+ case ADDON_ACTION_SHOW_VIDEOMENU:
+ return ACTION_SHOW_VIDEOMENU;
+ case ADDON_ACTION_ENTER:
+ return ACTION_ENTER;
+ case ADDON_ACTION_INCREASE_RATING:
+ return ACTION_INCREASE_RATING;
+ case ADDON_ACTION_DECREASE_RATING:
+ return ACTION_DECREASE_RATING;
+ case ADDON_ACTION_NEXT_SCENE:
+ return ACTION_NEXT_SCENE;
+ case ADDON_ACTION_PREV_SCENE:
+ return ACTION_PREV_SCENE;
+ case ADDON_ACTION_NEXT_LETTER:
+ return ACTION_NEXT_LETTER;
+ case ADDON_ACTION_PREV_LETTER:
+ return ACTION_PREV_LETTER;
+ case ADDON_ACTION_JUMP_SMS2:
+ return ACTION_JUMP_SMS2;
+ case ADDON_ACTION_JUMP_SMS3:
+ return ACTION_JUMP_SMS3;
+ case ADDON_ACTION_JUMP_SMS4:
+ return ACTION_JUMP_SMS4;
+ case ADDON_ACTION_JUMP_SMS5:
+ return ACTION_JUMP_SMS5;
+ case ADDON_ACTION_JUMP_SMS6:
+ return ACTION_JUMP_SMS6;
+ case ADDON_ACTION_JUMP_SMS7:
+ return ACTION_JUMP_SMS7;
+ case ADDON_ACTION_JUMP_SMS8:
+ return ACTION_JUMP_SMS8;
+ case ADDON_ACTION_JUMP_SMS9:
+ return ACTION_JUMP_SMS9;
+ case ADDON_ACTION_FILTER_CLEAR:
+ return ACTION_FILTER_CLEAR;
+ case ADDON_ACTION_FILTER_SMS2:
+ return ACTION_FILTER_SMS2;
+ case ADDON_ACTION_FILTER_SMS3:
+ return ACTION_FILTER_SMS3;
+ case ADDON_ACTION_FILTER_SMS4:
+ return ACTION_FILTER_SMS4;
+ case ADDON_ACTION_FILTER_SMS5:
+ return ACTION_FILTER_SMS5;
+ case ADDON_ACTION_FILTER_SMS6:
+ return ACTION_FILTER_SMS6;
+ case ADDON_ACTION_FILTER_SMS7:
+ return ACTION_FILTER_SMS7;
+ case ADDON_ACTION_FILTER_SMS8:
+ return ACTION_FILTER_SMS8;
+ case ADDON_ACTION_FILTER_SMS9:
+ return ACTION_FILTER_SMS9;
+ case ADDON_ACTION_FIRST_PAGE:
+ return ACTION_FIRST_PAGE;
+ case ADDON_ACTION_LAST_PAGE:
+ return ACTION_LAST_PAGE;
+ case ADDON_ACTION_AUDIO_DELAY:
+ return ACTION_AUDIO_DELAY;
+ case ADDON_ACTION_SUBTITLE_DELAY:
+ return ACTION_SUBTITLE_DELAY;
+ case ADDON_ACTION_MENU:
+ return ACTION_MENU;
+ case ADDON_ACTION_SET_RATING:
+ return ACTION_SET_RATING;
+ case ADDON_ACTION_RECORD:
+ return ACTION_RECORD;
+ case ADDON_ACTION_PASTE:
+ return ACTION_PASTE;
+ case ADDON_ACTION_NEXT_CONTROL:
+ return ACTION_NEXT_CONTROL;
+ case ADDON_ACTION_PREV_CONTROL:
+ return ACTION_PREV_CONTROL;
+ case ADDON_ACTION_CHANNEL_SWITCH:
+ return ACTION_CHANNEL_SWITCH;
+ case ADDON_ACTION_CHANNEL_UP:
+ return ACTION_CHANNEL_UP;
+ case ADDON_ACTION_CHANNEL_DOWN:
+ return ACTION_CHANNEL_DOWN;
+ case ADDON_ACTION_NEXT_CHANNELGROUP:
+ return ACTION_NEXT_CHANNELGROUP;
+ case ADDON_ACTION_PREVIOUS_CHANNELGROUP:
+ return ACTION_PREVIOUS_CHANNELGROUP;
+ case ADDON_ACTION_PVR_PLAY:
+ return ACTION_PVR_PLAY;
+ case ADDON_ACTION_PVR_PLAY_TV:
+ return ACTION_PVR_PLAY_TV;
+ case ADDON_ACTION_PVR_PLAY_RADIO:
+ return ACTION_PVR_PLAY_RADIO;
+ case ADDON_ACTION_PVR_SHOW_TIMER_RULE:
+ return ACTION_PVR_SHOW_TIMER_RULE;
+ case ADDON_ACTION_CHANNEL_NUMBER_SEP:
+ return ACTION_CHANNEL_NUMBER_SEP;
+ case ADDON_ACTION_PVR_ANNOUNCE_REMINDERS:
+ return ACTION_PVR_ANNOUNCE_REMINDERS;
+ case ADDON_ACTION_TOGGLE_FULLSCREEN:
+ return ACTION_TOGGLE_FULLSCREEN;
+ case ADDON_ACTION_TOGGLE_WATCHED:
+ return ACTION_TOGGLE_WATCHED;
+ case ADDON_ACTION_SCAN_ITEM:
+ return ACTION_SCAN_ITEM;
+ case ADDON_ACTION_TOGGLE_DIGITAL_ANALOG:
+ return ACTION_TOGGLE_DIGITAL_ANALOG;
+ case ADDON_ACTION_RELOAD_KEYMAPS:
+ return ACTION_RELOAD_KEYMAPS;
+ case ADDON_ACTION_GUIPROFILE_BEGIN:
+ return ACTION_GUIPROFILE_BEGIN;
+ case ADDON_ACTION_TELETEXT_RED:
+ return ACTION_TELETEXT_RED;
+ case ADDON_ACTION_TELETEXT_GREEN:
+ return ACTION_TELETEXT_GREEN;
+ case ADDON_ACTION_TELETEXT_YELLOW:
+ return ACTION_TELETEXT_YELLOW;
+ case ADDON_ACTION_TELETEXT_BLUE:
+ return ACTION_TELETEXT_BLUE;
+ case ADDON_ACTION_INCREASE_PAR:
+ return ACTION_INCREASE_PAR;
+ case ADDON_ACTION_DECREASE_PAR:
+ return ACTION_DECREASE_PAR;
+ case ADDON_ACTION_VSHIFT_UP:
+ return ACTION_VSHIFT_UP;
+ case ADDON_ACTION_VSHIFT_DOWN:
+ return ACTION_VSHIFT_DOWN;
+ case ADDON_ACTION_PLAYER_PLAYPAUSE:
+ return ACTION_PLAYER_PLAYPAUSE;
+ case ADDON_ACTION_SUBTITLE_VSHIFT_UP:
+ return ACTION_SUBTITLE_VSHIFT_UP;
+ case ADDON_ACTION_SUBTITLE_VSHIFT_DOWN:
+ return ACTION_SUBTITLE_VSHIFT_DOWN;
+ case ADDON_ACTION_SUBTITLE_ALIGN:
+ return ACTION_SUBTITLE_ALIGN;
+ case ADDON_ACTION_FILTER:
+ return ACTION_FILTER;
+ case ADDON_ACTION_SWITCH_PLAYER:
+ return ACTION_SWITCH_PLAYER;
+ case ADDON_ACTION_STEREOMODE_NEXT:
+ return ACTION_STEREOMODE_NEXT;
+ case ADDON_ACTION_STEREOMODE_PREVIOUS:
+ return ACTION_STEREOMODE_PREVIOUS;
+ case ADDON_ACTION_STEREOMODE_TOGGLE:
+ return ACTION_STEREOMODE_TOGGLE;
+ case ADDON_ACTION_STEREOMODE_SELECT:
+ return ACTION_STEREOMODE_SELECT;
+ case ADDON_ACTION_STEREOMODE_TOMONO:
+ return ACTION_STEREOMODE_TOMONO;
+ case ADDON_ACTION_STEREOMODE_SET:
+ return ACTION_STEREOMODE_SET;
+ case ADDON_ACTION_SETTINGS_RESET:
+ return ACTION_SETTINGS_RESET;
+ case ADDON_ACTION_SETTINGS_LEVEL_CHANGE:
+ return ACTION_SETTINGS_LEVEL_CHANGE;
+ case ADDON_ACTION_TRIGGER_OSD:
+ return ACTION_TRIGGER_OSD;
+ case ADDON_ACTION_INPUT_TEXT:
+ return ACTION_INPUT_TEXT;
+ case ADDON_ACTION_VOLUME_SET:
+ return ACTION_VOLUME_SET;
+ case ADDON_ACTION_TOGGLE_COMMSKIP:
+ return ACTION_TOGGLE_COMMSKIP;
+ case ADDON_ACTION_BROWSE_SUBTITLE:
+ return ACTION_BROWSE_SUBTITLE;
+ case ADDON_ACTION_PLAYER_RESET:
+ return ACTION_PLAYER_RESET;
+ case ADDON_ACTION_TOGGLE_FONT:
+ return ACTION_TOGGLE_FONT;
+ case ADDON_ACTION_VIDEO_NEXT_STREAM:
+ return ACTION_VIDEO_NEXT_STREAM;
+ case ADDON_ACTION_QUEUE_ITEM_NEXT:
+ return ACTION_QUEUE_ITEM_NEXT;
+ case ADDON_ACTION_HDR_TOGGLE:
+ return ACTION_HDR_TOGGLE;
+ case ADDON_ACTION_VOICE_RECOGNIZE:
+ return ACTION_VOICE_RECOGNIZE;
+ case ADDON_ACTION_TOUCH_TAP:
+ return ACTION_TOUCH_TAP;
+ case ADDON_ACTION_TOUCH_TAP_TEN:
+ return ACTION_TOUCH_TAP_TEN;
+ case ADDON_ACTION_TOUCH_LONGPRESS:
+ return ACTION_TOUCH_LONGPRESS;
+ case ADDON_ACTION_TOUCH_LONGPRESS_TEN:
+ return ACTION_TOUCH_LONGPRESS_TEN;
+ case ADDON_ACTION_GESTURE_NOTIFY:
+ return ACTION_GESTURE_NOTIFY;
+ case ADDON_ACTION_GESTURE_BEGIN:
+ return ACTION_GESTURE_BEGIN;
+ case ADDON_ACTION_GESTURE_ZOOM:
+ return ACTION_GESTURE_ZOOM;
+ case ADDON_ACTION_GESTURE_ROTATE:
+ return ACTION_GESTURE_ROTATE;
+ case ADDON_ACTION_GESTURE_PAN:
+ return ACTION_GESTURE_PAN;
+ case ADDON_ACTION_GESTURE_ABORT:
+ return ACTION_GESTURE_ABORT;
+ case ADDON_ACTION_GESTURE_SWIPE_LEFT:
+ return ACTION_GESTURE_SWIPE_LEFT;
+ case ADDON_ACTION_GESTURE_SWIPE_LEFT_TEN:
+ return ACTION_GESTURE_SWIPE_LEFT_TEN;
+ case ADDON_ACTION_GESTURE_SWIPE_RIGHT:
+ return ACTION_GESTURE_SWIPE_RIGHT;
+ case ADDON_ACTION_GESTURE_SWIPE_RIGHT_TEN:
+ return ACTION_GESTURE_SWIPE_RIGHT_TEN;
+ case ADDON_ACTION_GESTURE_SWIPE_UP:
+ return ACTION_GESTURE_SWIPE_UP;
+ case ADDON_ACTION_GESTURE_SWIPE_UP_TEN:
+ return ACTION_GESTURE_SWIPE_UP_TEN;
+ case ADDON_ACTION_GESTURE_SWIPE_DOWN:
+ return ACTION_GESTURE_SWIPE_DOWN;
+ case ADDON_ACTION_GESTURE_SWIPE_DOWN_TEN:
+ return ACTION_GESTURE_SWIPE_DOWN_TEN;
+ case ADDON_ACTION_GESTURE_END:
+ return ACTION_GESTURE_END;
+ case ADDON_ACTION_ANALOG_MOVE_X_LEFT:
+ return ACTION_ANALOG_MOVE_X_LEFT;
+ case ADDON_ACTION_ANALOG_MOVE_X_RIGHT:
+ return ACTION_ANALOG_MOVE_X_RIGHT;
+ case ADDON_ACTION_ANALOG_MOVE_Y_UP:
+ return ACTION_ANALOG_MOVE_Y_UP;
+ case ADDON_ACTION_ANALOG_MOVE_Y_DOWN:
+ return ACTION_ANALOG_MOVE_Y_DOWN;
+ case ADDON_ACTION_ERROR:
+ return ACTION_ERROR;
+ case ADDON_ACTION_NOOP:
+ default:
+ return ACTION_NOOP;
+ }
+}
diff --git a/xbmc/addons/interfaces/gui/GUITranslator.h b/xbmc/addons/interfaces/gui/GUITranslator.h
new file mode 100644
index 0000000..8bd6b5c
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/GUITranslator.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2005-2020 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/input/action_ids.h"
+
+namespace ADDON
+{
+
+/*!
+ * \brief Translates data types from GUI API to the corresponding format in Kodi.
+ *
+ * This class is stateless.
+ */
+class CAddonGUITranslator
+{
+ CAddonGUITranslator() = delete;
+
+public:
+ /*!
+ * \brief Translate Kodi's action id's to addon
+ * \param kodiId Kodi's action identifier
+ * \return Addon action identifier
+ */
+ static ADDON_ACTION TranslateActionIdToAddon(int kodiId);
+
+ /*!
+ * \brief Translate addon's action id's to Kodi
+ * \param addonId Addon's action identifier
+ * \return Kodi action identifier
+ */
+ static int TranslateActionIdToKodi(ADDON_ACTION addonId);
+};
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/General.cpp b/xbmc/addons/interfaces/gui/General.cpp
new file mode 100644
index 0000000..a0a5583
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/General.cpp
@@ -0,0 +1,252 @@
+/*
+ * 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 "General.h"
+
+#include "ListItem.h"
+#include "ServiceBroker.h"
+#include "Window.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/General.h"
+#include "controls/Button.h"
+#include "controls/Edit.h"
+#include "controls/FadeLabel.h"
+#include "controls/Image.h"
+#include "controls/Label.h"
+#include "controls/Progress.h"
+#include "controls/RadioButton.h"
+#include "controls/Rendering.h"
+#include "controls/SettingsSlider.h"
+#include "controls/Slider.h"
+#include "controls/Spin.h"
+#include "controls/TextBox.h"
+#include "dialogs/ContextMenu.h"
+#include "dialogs/ExtendedProgressBar.h"
+#include "dialogs/FileBrowser.h"
+#include "dialogs/Keyboard.h"
+#include "dialogs/Numeric.h"
+#include "dialogs/OK.h"
+#include "dialogs/Progress.h"
+#include "dialogs/Select.h"
+#include "dialogs/TextViewer.h"
+#include "dialogs/YesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+namespace ADDON
+{
+int Interface_GUIGeneral::m_iAddonGUILockRef = 0;
+
+void Interface_GUIGeneral::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui = new AddonToKodiFuncTable_kodi_gui();
+
+ Interface_GUIControlButton::Init(addonInterface);
+ Interface_GUIControlEdit::Init(addonInterface);
+ Interface_GUIControlFadeLabel::Init(addonInterface);
+ Interface_GUIControlImage::Init(addonInterface);
+ Interface_GUIControlLabel::Init(addonInterface);
+ Interface_GUIControlProgress::Init(addonInterface);
+ Interface_GUIControlRadioButton::Init(addonInterface);
+ Interface_GUIControlAddonRendering::Init(addonInterface);
+ Interface_GUIControlSettingsSlider::Init(addonInterface);
+ Interface_GUIControlSlider::Init(addonInterface);
+ Interface_GUIControlSpin::Init(addonInterface);
+ Interface_GUIControlTextBox::Init(addonInterface);
+ Interface_GUIDialogContextMenu::Init(addonInterface);
+ Interface_GUIDialogExtendedProgress::Init(addonInterface);
+ Interface_GUIDialogFileBrowser::Init(addonInterface);
+ Interface_GUIDialogKeyboard::Init(addonInterface);
+ Interface_GUIDialogNumeric::Init(addonInterface);
+ Interface_GUIDialogOK::Init(addonInterface);
+ Interface_GUIDialogProgress::Init(addonInterface);
+ Interface_GUIDialogSelect::Init(addonInterface);
+ Interface_GUIDialogTextViewer::Init(addonInterface);
+ Interface_GUIDialogYesNo::Init(addonInterface);
+ Interface_GUIListItem::Init(addonInterface);
+ Interface_GUIWindow::Init(addonInterface);
+
+ addonInterface->toKodi->kodi_gui->general = new AddonToKodiFuncTable_kodi_gui_general();
+
+ addonInterface->toKodi->kodi_gui->general->lock = lock;
+ addonInterface->toKodi->kodi_gui->general->unlock = unlock;
+ addonInterface->toKodi->kodi_gui->general->get_screen_height = get_screen_height;
+ addonInterface->toKodi->kodi_gui->general->get_screen_width = get_screen_width;
+ addonInterface->toKodi->kodi_gui->general->get_video_resolution = get_video_resolution;
+ addonInterface->toKodi->kodi_gui->general->get_current_window_dialog_id =
+ get_current_window_dialog_id;
+ addonInterface->toKodi->kodi_gui->general->get_current_window_id = get_current_window_id;
+ addonInterface->toKodi->kodi_gui->general->get_hw_context = get_hw_context;
+ addonInterface->toKodi->kodi_gui->general->get_adjust_refresh_rate_status =
+ get_adjust_refresh_rate_status;
+}
+
+void Interface_GUIGeneral::DeInit(AddonGlobalInterface* addonInterface)
+{
+ if (addonInterface->toKodi && /* <-- needed as long as the old addon way is used */
+ addonInterface->toKodi->kodi_gui)
+ {
+ Interface_GUIControlButton::DeInit(addonInterface);
+ Interface_GUIControlEdit::DeInit(addonInterface);
+ Interface_GUIControlFadeLabel::DeInit(addonInterface);
+ Interface_GUIControlImage::DeInit(addonInterface);
+ Interface_GUIControlLabel::DeInit(addonInterface);
+ Interface_GUIControlProgress::DeInit(addonInterface);
+ Interface_GUIControlRadioButton::DeInit(addonInterface);
+ Interface_GUIControlAddonRendering::DeInit(addonInterface);
+ Interface_GUIControlSettingsSlider::DeInit(addonInterface);
+ Interface_GUIControlSlider::DeInit(addonInterface);
+ Interface_GUIControlSpin::DeInit(addonInterface);
+ Interface_GUIControlTextBox::DeInit(addonInterface);
+ Interface_GUIDialogContextMenu::DeInit(addonInterface);
+ Interface_GUIDialogExtendedProgress::DeInit(addonInterface);
+ Interface_GUIDialogFileBrowser::DeInit(addonInterface);
+ Interface_GUIDialogKeyboard::DeInit(addonInterface);
+ Interface_GUIDialogNumeric::DeInit(addonInterface);
+ Interface_GUIDialogOK::DeInit(addonInterface);
+ Interface_GUIDialogProgress::DeInit(addonInterface);
+ Interface_GUIDialogSelect::DeInit(addonInterface);
+ Interface_GUIDialogTextViewer::DeInit(addonInterface);
+ Interface_GUIDialogYesNo::DeInit(addonInterface);
+ Interface_GUIListItem::DeInit(addonInterface);
+ Interface_GUIWindow::DeInit(addonInterface);
+
+ delete addonInterface->toKodi->kodi_gui->general;
+ delete addonInterface->toKodi->kodi_gui;
+ addonInterface->toKodi->kodi_gui = nullptr;
+ }
+}
+
+//@{
+void Interface_GUIGeneral::lock()
+{
+ if (m_iAddonGUILockRef == 0)
+ CServiceBroker::GetWinSystem()->GetGfxContext().lock();
+ ++m_iAddonGUILockRef;
+}
+
+void Interface_GUIGeneral::unlock()
+{
+ if (m_iAddonGUILockRef > 0)
+ {
+ --m_iAddonGUILockRef;
+ if (m_iAddonGUILockRef == 0)
+ CServiceBroker::GetWinSystem()->GetGfxContext().unlock();
+ }
+}
+//@}
+
+//@{
+int Interface_GUIGeneral::get_screen_height(KODI_HANDLE kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "kodi::gui::{} - invalid data", __func__);
+ return -1;
+ }
+
+ return CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight();
+}
+
+int Interface_GUIGeneral::get_screen_width(KODI_HANDLE kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "kodi::gui::{} - invalid data", __func__);
+ return -1;
+ }
+
+ return CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth();
+}
+
+int Interface_GUIGeneral::get_video_resolution(KODI_HANDLE kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "kodi::gui::{} - invalid data", __func__);
+ return -1;
+ }
+
+ return (int)CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution();
+}
+//@}
+
+//@{
+int Interface_GUIGeneral::get_current_window_dialog_id(KODI_HANDLE kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "kodi::gui::{} - invalid data", __func__);
+ return -1;
+ }
+
+ std::unique_lock<CCriticalSection> gl(CServiceBroker::GetWinSystem()->GetGfxContext());
+ return CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog();
+}
+
+int Interface_GUIGeneral::get_current_window_id(KODI_HANDLE kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "kodi::gui::{} - invalid data", __func__);
+ return -1;
+ }
+
+ std::unique_lock<CCriticalSection> gl(CServiceBroker::GetWinSystem()->GetGfxContext());
+ return CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+}
+
+ADDON_HARDWARE_CONTEXT Interface_GUIGeneral::get_hw_context(KODI_HANDLE kodiBase)
+{
+ return CServiceBroker::GetWinSystem()->GetHWContext();
+}
+
+AdjustRefreshRateStatus Interface_GUIGeneral::get_adjust_refresh_rate_status(KODI_HANDLE kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "kodi::gui::{} - invalid data", __func__);
+ return ADJUST_REFRESHRATE_STATUS_OFF;
+ }
+
+ switch (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE))
+ {
+ case AdjustRefreshRate::ADJUST_REFRESHRATE_OFF:
+ return ADJUST_REFRESHRATE_STATUS_OFF;
+ break;
+ case AdjustRefreshRate::ADJUST_REFRESHRATE_ON_START:
+ return ADJUST_REFRESHRATE_STATUS_ON_START;
+ break;
+ case AdjustRefreshRate::ADJUST_REFRESHRATE_ON_STARTSTOP:
+ return ADJUST_REFRESHRATE_STATUS_ON_STARTSTOP;
+ break;
+ case AdjustRefreshRate::ADJUST_REFRESHRATE_ALWAYS:
+ return ADJUST_REFRESHRATE_STATUS_ALWAYS;
+ break;
+ default:
+ CLog::Log(LOGERROR, "kodi::gui::{} - Unhandled Adjust refresh rate setting", __func__);
+ return ADJUST_REFRESHRATE_STATUS_OFF;
+ break;
+ }
+}
+
+//@}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/General.h b/xbmc/addons/interfaces/gui/General.h
new file mode 100644
index 0000000..146d4f7
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/General.h
@@ -0,0 +1,62 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/general.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/General.h"
+ */
+ struct Interface_GUIGeneral
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note For add of new functions use the "_" style to identify direct a
+ * add-on callback function. Everything with CamelCase is only for the
+ * usage in Kodi only.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void lock();
+ static void unlock();
+
+ static int get_screen_height(KODI_HANDLE kodiBase);
+ static int get_screen_width(KODI_HANDLE kodiBase);
+ static int get_video_resolution(KODI_HANDLE kodiBase);
+ static int get_current_window_dialog_id(KODI_HANDLE kodiBase);
+ static int get_current_window_id(KODI_HANDLE kodiBase);
+ static ADDON_HARDWARE_CONTEXT get_hw_context(KODI_HANDLE kodiBase);
+ static AdjustRefreshRateStatus get_adjust_refresh_rate_status(KODI_HANDLE kodiBase);
+ //@}
+
+ private:
+ static int m_iAddonGUILockRef;
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/ListItem.cpp b/xbmc/addons/interfaces/gui/ListItem.cpp
new file mode 100644
index 0000000..c91e451
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/ListItem.cpp
@@ -0,0 +1,428 @@
+/*
+ * 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 "ListItem.h"
+
+#include "FileItem.h"
+#include "General.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/ListItem.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIListItem::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->listItem = new AddonToKodiFuncTable_kodi_gui_listItem();
+
+ addonInterface->toKodi->kodi_gui->listItem->create = create;
+ addonInterface->toKodi->kodi_gui->listItem->destroy = destroy;
+ addonInterface->toKodi->kodi_gui->listItem->get_label = get_label;
+ addonInterface->toKodi->kodi_gui->listItem->set_label = set_label;
+ addonInterface->toKodi->kodi_gui->listItem->get_label2 = get_label2;
+ addonInterface->toKodi->kodi_gui->listItem->set_label2 = set_label2;
+ addonInterface->toKodi->kodi_gui->listItem->get_art = get_art;
+ addonInterface->toKodi->kodi_gui->listItem->set_art = set_art;
+ addonInterface->toKodi->kodi_gui->listItem->get_path = get_path;
+ addonInterface->toKodi->kodi_gui->listItem->set_path = set_path;
+ addonInterface->toKodi->kodi_gui->listItem->get_property = get_property;
+ addonInterface->toKodi->kodi_gui->listItem->set_property = set_property;
+ addonInterface->toKodi->kodi_gui->listItem->select = select;
+ addonInterface->toKodi->kodi_gui->listItem->is_selected = is_selected;
+}
+
+void Interface_GUIListItem::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->listItem;
+}
+
+KODI_GUI_LISTITEM_HANDLE Interface_GUIListItem::create(KODI_HANDLE kodiBase,
+ const char* label,
+ const char* label2,
+ const char* path)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "ADDON::Interface_GUIListItem::{} - invalid data", __func__);
+ return nullptr;
+ }
+
+ CFileItemPtr* item = new CFileItemPtr(new CFileItem());
+ if (label)
+ item->get()->SetLabel(label);
+ if (label2)
+ item->get()->SetLabel2(label2);
+ if (path)
+ item->get()->SetPath(path);
+
+ return item;
+}
+
+void Interface_GUIListItem::destroy(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "ADDON::Interface_GUIListItem::{} - invalid data", __func__);
+ return;
+ }
+
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (item)
+ delete item;
+}
+
+char* Interface_GUIListItem::get_label(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return nullptr;
+ }
+
+ char* ret;
+ Interface_GUIGeneral::lock();
+ ret = strdup(item->get()->GetLabel().c_str());
+ Interface_GUIGeneral::unlock();
+ return ret;
+}
+
+void Interface_GUIListItem::set_label(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* label)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item || !label)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "label='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(label),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ item->get()->SetLabel(label);
+ Interface_GUIGeneral::unlock();
+}
+
+char* Interface_GUIListItem::get_label2(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return nullptr;
+ }
+
+ char* ret;
+ Interface_GUIGeneral::lock();
+ ret = strdup(item->get()->GetLabel2().c_str());
+ Interface_GUIGeneral::unlock();
+ return ret;
+}
+
+void Interface_GUIListItem::set_label2(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* label)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item || !label)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "label='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(label),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ item->get()->SetLabel2(label);
+ Interface_GUIGeneral::unlock();
+}
+
+char* Interface_GUIListItem::get_art(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* type)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item || !type)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', type='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(type),
+ addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return nullptr;
+ }
+
+ char* ret;
+ Interface_GUIGeneral::lock();
+ ret = strdup(item->get()->GetArt(type).c_str());
+ Interface_GUIGeneral::unlock();
+ return ret;
+}
+
+void Interface_GUIListItem::set_art(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* type,
+ const char* label)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item || !type || !label)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', handle='{}', type= "
+ "'{}', label='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(type),
+ static_cast<const void*>(label), addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ item->get()->SetArt(type, label);
+ Interface_GUIGeneral::unlock();
+}
+
+char* Interface_GUIListItem::get_path(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return nullptr;
+ }
+
+ char* ret;
+ Interface_GUIGeneral::lock();
+ ret = strdup(item->get()->GetPath().c_str());
+ Interface_GUIGeneral::unlock();
+ return ret;
+}
+
+
+void Interface_GUIListItem::set_path(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* path)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item || !path)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', path='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(path),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ item->get()->SetPath(path);
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIListItem::set_property(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* key,
+ const char* value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item || !key || !value)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}', value='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ static_cast<const void*>(value), addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return;
+ }
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ Interface_GUIGeneral::lock();
+ item->get()->SetProperty(lowerKey, CVariant(value));
+ Interface_GUIGeneral::unlock();
+}
+
+char* Interface_GUIListItem::get_property(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* key)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item || !key)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return nullptr;
+ }
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ Interface_GUIGeneral::lock();
+ char* ret = strdup(item->get()->GetProperty(lowerKey).asString().c_str());
+ Interface_GUIGeneral::unlock();
+
+ return ret;
+}
+
+void Interface_GUIListItem::select(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ bool select)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ item->get()->Select(select);
+ Interface_GUIGeneral::unlock();
+}
+
+bool Interface_GUIListItem::is_selected(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CFileItemPtr* item = static_cast<CFileItemPtr*>(handle);
+ if (!addon || !item)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIListItem::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return false;
+ }
+
+ if (item->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIListItem::{} - empty list item called on addon '{}'",
+ __func__, addon->ID());
+ return false;
+ }
+
+ Interface_GUIGeneral::lock();
+ bool ret = item->get()->IsSelected();
+ Interface_GUIGeneral::unlock();
+
+ return ret;
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/ListItem.h b/xbmc/addons/interfaces/gui/ListItem.h
new file mode 100644
index 0000000..70e5c6a
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/ListItem.h
@@ -0,0 +1,76 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/list_item.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/ListItem.h"
+ */
+ struct Interface_GUIListItem
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static KODI_GUI_LISTITEM_HANDLE create(KODI_HANDLE kodiBase,
+ const char* label,
+ const char* label2,
+ const char* path);
+ static void destroy(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle);
+ static char* get_label(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle);
+ static void set_label(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle, const char* label);
+ static char* get_label2(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle);
+ static void set_label2(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* label);
+ static char* get_art(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle, const char* type);
+ static void set_art(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* type,
+ const char* image);
+ static char* get_path(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle);
+ static void set_path(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle, const char* path);
+ static char* get_property(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* key);
+ static void set_property(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* key,
+ const char* value);
+ static void select(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle, bool select);
+ static bool is_selected(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/Window.cpp b/xbmc/addons/interfaces/gui/Window.cpp
new file mode 100644
index 0000000..307c339
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/Window.cpp
@@ -0,0 +1,1499 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/gui/Window.h"
+
+#include "FileItem.h"
+#include "GUITranslator.h"
+#include "General.h"
+#include "ServiceBroker.h"
+#include "Window.h"
+#include "addons/Skin.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "application/Application.h"
+#include "controls/Rendering.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIRenderingControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/TextureManager.h"
+#include "input/Key.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIWindow::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->window = new AddonToKodiFuncTable_kodi_gui_window();
+
+ /* Window creation functions */
+ addonInterface->toKodi->kodi_gui->window->create = create;
+ addonInterface->toKodi->kodi_gui->window->destroy = destroy;
+ addonInterface->toKodi->kodi_gui->window->set_callbacks = set_callbacks;
+ addonInterface->toKodi->kodi_gui->window->show = show;
+ addonInterface->toKodi->kodi_gui->window->close = close;
+ addonInterface->toKodi->kodi_gui->window->do_modal = do_modal;
+
+ /* Window control functions */
+ addonInterface->toKodi->kodi_gui->window->set_focus_id = set_focus_id;
+ addonInterface->toKodi->kodi_gui->window->get_focus_id = get_focus_id;
+ addonInterface->toKodi->kodi_gui->window->set_control_label = set_control_label;
+ addonInterface->toKodi->kodi_gui->window->set_control_visible = set_control_visible;
+ addonInterface->toKodi->kodi_gui->window->set_control_selected = set_control_selected;
+
+ /* Window property functions */
+ addonInterface->toKodi->kodi_gui->window->set_property = set_property;
+ addonInterface->toKodi->kodi_gui->window->set_property_int = set_property_int;
+ addonInterface->toKodi->kodi_gui->window->set_property_bool = set_property_bool;
+ addonInterface->toKodi->kodi_gui->window->set_property_double = set_property_double;
+ addonInterface->toKodi->kodi_gui->window->get_property = get_property;
+ addonInterface->toKodi->kodi_gui->window->get_property_int = get_property_int;
+ addonInterface->toKodi->kodi_gui->window->get_property_bool = get_property_bool;
+ addonInterface->toKodi->kodi_gui->window->get_property_double = get_property_double;
+ addonInterface->toKodi->kodi_gui->window->clear_properties = clear_properties;
+ addonInterface->toKodi->kodi_gui->window->clear_property = clear_property;
+
+ /* List item functions */
+ addonInterface->toKodi->kodi_gui->window->clear_item_list = clear_item_list;
+ addonInterface->toKodi->kodi_gui->window->add_list_item = add_list_item;
+ addonInterface->toKodi->kodi_gui->window->remove_list_item_from_position =
+ remove_list_item_from_position;
+ addonInterface->toKodi->kodi_gui->window->remove_list_item = remove_list_item;
+ addonInterface->toKodi->kodi_gui->window->get_list_item = get_list_item;
+ addonInterface->toKodi->kodi_gui->window->set_current_list_position = set_current_list_position;
+ addonInterface->toKodi->kodi_gui->window->get_current_list_position = get_current_list_position;
+ addonInterface->toKodi->kodi_gui->window->get_list_size = get_list_size;
+ addonInterface->toKodi->kodi_gui->window->set_container_property = set_container_property;
+ addonInterface->toKodi->kodi_gui->window->set_container_content = set_container_content;
+ addonInterface->toKodi->kodi_gui->window->get_current_container_id = get_current_container_id;
+
+ /* Various functions */
+ addonInterface->toKodi->kodi_gui->window->mark_dirty_region = mark_dirty_region;
+
+ /* GUI control access functions */
+ addonInterface->toKodi->kodi_gui->window->get_control_button = get_control_button;
+ addonInterface->toKodi->kodi_gui->window->get_control_edit = get_control_edit;
+ addonInterface->toKodi->kodi_gui->window->get_control_fade_label = get_control_fade_label;
+ addonInterface->toKodi->kodi_gui->window->get_control_image = get_control_image;
+ addonInterface->toKodi->kodi_gui->window->get_control_label = get_control_label;
+ addonInterface->toKodi->kodi_gui->window->get_control_progress = get_control_progress;
+ addonInterface->toKodi->kodi_gui->window->get_control_radio_button = get_control_radio_button;
+ addonInterface->toKodi->kodi_gui->window->get_control_render_addon = get_control_render_addon;
+ addonInterface->toKodi->kodi_gui->window->get_control_settings_slider =
+ get_control_settings_slider;
+ addonInterface->toKodi->kodi_gui->window->get_control_slider = get_control_slider;
+ addonInterface->toKodi->kodi_gui->window->get_control_spin = get_control_spin;
+ addonInterface->toKodi->kodi_gui->window->get_control_text_box = get_control_text_box;
+}
+
+void Interface_GUIWindow::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->window;
+}
+
+/*!
+ * Window creation functions
+ */
+//@{
+KODI_GUI_WINDOW_HANDLE Interface_GUIWindow::create(KODI_HANDLE kodiBase,
+ const char* xml_filename,
+ const char* default_skin,
+ bool as_dialog,
+ bool is_media)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon || !xml_filename || !default_skin)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (xml_filename='{}', "
+ "default_skin='{}') on addon '{}'",
+ __func__, static_cast<const void*>(xml_filename),
+ static_cast<const void*>(default_skin), addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ if (as_dialog && is_media)
+ {
+ CLog::Log(LOGWARNING,
+ "Interface_GUIWindow::{}: {}/{} - addon tries to create dialog as media window who "
+ "not allowed, contact Developer '{}' of this addon",
+ __func__, CAddonInfo::TranslateType(addon->Type()), addon->Name(), addon->Author());
+ }
+
+ RESOLUTION_INFO res;
+ std::string strSkinPath = g_SkinInfo->GetSkinPath(xml_filename, &res);
+
+ if (!CFileUtils::Exists(strSkinPath))
+ {
+ std::string str("none");
+ ADDON::AddonInfoPtr addonInfo =
+ std::make_shared<ADDON::CAddonInfo>(str, ADDON::AddonType::SKIN);
+
+ // Check for the matching folder for the skin in the fallback skins folder
+ std::string fallbackPath = URIUtils::AddFileToFolder(addon->Path(), "resources", "skins");
+ std::string basePath = URIUtils::AddFileToFolder(fallbackPath, g_SkinInfo->ID());
+
+ strSkinPath = g_SkinInfo->GetSkinPath(xml_filename, &res, basePath);
+
+ // Check for the matching folder for the skin in the fallback skins folder (if it exists)
+ if (CFileUtils::Exists(basePath))
+ {
+ addonInfo->SetPath(basePath);
+ const std::shared_ptr<ADDON::CSkinInfo> skinInfo =
+ std::make_shared<ADDON::CSkinInfo>(addonInfo, res);
+ skinInfo->Start();
+ strSkinPath = skinInfo->GetSkinPath(xml_filename, &res);
+ }
+
+ if (!CFileUtils::Exists(strSkinPath))
+ {
+ // Finally fallback to the DefaultSkin as it didn't exist in either the Kodi Skin folder or the fallback skin folder
+ addonInfo->SetPath(URIUtils::AddFileToFolder(fallbackPath, default_skin));
+ const std::shared_ptr<ADDON::CSkinInfo> skinInfo =
+ std::make_shared<ADDON::CSkinInfo>(addonInfo, res);
+
+ skinInfo->Start();
+ strSkinPath = skinInfo->GetSkinPath(xml_filename, &res);
+ if (!CFileUtils::Exists(strSkinPath))
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{}: {}/{} - XML File '{}' for Window is missing, contact "
+ "Developer '{}' of this addon",
+ __func__, CAddonInfo::TranslateType(addon->Type()), addon->Name(), strSkinPath,
+ addon->Author());
+ return nullptr;
+ }
+ }
+ }
+
+ int id = GetNextAvailableWindowId();
+ if (id < 0)
+ return nullptr;
+
+ CGUIWindow* window;
+ if (!as_dialog)
+ window = new CGUIAddonWindow(id, strSkinPath, addon, is_media);
+ else
+ window = new CGUIAddonWindowDialog(id, strSkinPath, addon);
+
+ Interface_GUIGeneral::lock();
+ CServiceBroker::GetGUI()->GetWindowManager().Add(window);
+ Interface_GUIGeneral::unlock();
+
+ if (!CServiceBroker::GetGUI()->GetWindowManager().GetWindow(id))
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - Requested window id '{}' does not exist for addon '{}'",
+ __func__, id, addon->ID());
+ delete window;
+ return nullptr;
+ }
+ window->SetCoordsRes(res);
+ return window;
+}
+
+void Interface_GUIWindow::destroy(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ CGUIWindow* pWindow =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow(pAddonWindow->GetID());
+ if (pWindow)
+ {
+ // first change to an existing window
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == pAddonWindow->GetID() &&
+ !g_application.m_bStop)
+ {
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetWindow(pAddonWindow->m_oldWindowId))
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(pAddonWindow->m_oldWindowId);
+ else // old window does not exist anymore, switch to home
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_HOME);
+ }
+ // Free any window properties
+ pAddonWindow->ClearProperties();
+ // free the window's resources and unload it (free all guicontrols)
+ pAddonWindow->FreeResources(true);
+
+ CServiceBroker::GetGUI()->GetWindowManager().Remove(pAddonWindow->GetID());
+ }
+ delete pAddonWindow;
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIWindow::set_callbacks(
+ KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ KODI_GUI_CLIENT_HANDLE clienthandle,
+ bool (*CBOnInit)(KODI_GUI_CLIENT_HANDLE),
+ bool (*CBOnFocus)(KODI_GUI_CLIENT_HANDLE, int),
+ bool (*CBOnClick)(KODI_GUI_CLIENT_HANDLE, int),
+ bool (*CBOnAction)(KODI_GUI_CLIENT_HANDLE, ADDON_ACTION),
+ void (*CBGetContextButtons)(KODI_GUI_CLIENT_HANDLE, int, gui_context_menu_pair*, unsigned int*),
+ bool (*CBOnContextButton)(KODI_GUI_CLIENT_HANDLE, int, unsigned int))
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !clienthandle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (handle='{}', clienthandle='{}') "
+ "on addon '{}'",
+ __func__, handle, clienthandle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->m_clientHandle = clienthandle;
+ pAddonWindow->CBOnInit = CBOnInit;
+ pAddonWindow->CBOnClick = CBOnClick;
+ pAddonWindow->CBOnFocus = CBOnFocus;
+ pAddonWindow->CBOnAction = CBOnAction;
+ pAddonWindow->CBGetContextButtons = CBGetContextButtons;
+ pAddonWindow->CBOnContextButton = CBOnContextButton;
+ Interface_GUIGeneral::unlock();
+}
+
+bool Interface_GUIWindow::show(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon ? addon->ID() : "unknown");
+ return false;
+ }
+
+ if (pAddonWindow->m_oldWindowId != pAddonWindow->m_windowId &&
+ pAddonWindow->m_windowId != CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow())
+ pAddonWindow->m_oldWindowId = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+
+ Interface_GUIGeneral::lock();
+ if (pAddonWindow->IsDialog())
+ dynamic_cast<CGUIAddonWindowDialog*>(pAddonWindow)->Show(true, false);
+ else
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(pAddonWindow->GetID());
+ Interface_GUIGeneral::unlock();
+
+ return true;
+}
+
+bool Interface_GUIWindow::close(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon ? addon->ID() : "unknown");
+ return false;
+ }
+
+ pAddonWindow->PulseActionEvent();
+
+ Interface_GUIGeneral::lock();
+
+ // if it's a dialog, we have to close it a bit different
+ if (pAddonWindow->IsDialog())
+ dynamic_cast<CGUIAddonWindowDialog*>(pAddonWindow)->Show(false);
+ else
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(pAddonWindow->m_oldWindowId);
+ pAddonWindow->m_oldWindowId = 0;
+
+ Interface_GUIGeneral::unlock();
+
+ return true;
+}
+
+bool Interface_GUIWindow::do_modal(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon ? addon->ID() : "unknown");
+ return false;
+ }
+
+ if (pAddonWindow->GetID() == CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow())
+ return true;
+
+ if (pAddonWindow->m_oldWindowId != pAddonWindow->m_windowId &&
+ pAddonWindow->m_windowId != CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow())
+ pAddonWindow->m_oldWindowId = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+
+ Interface_GUIGeneral::lock();
+ if (pAddonWindow->IsDialog())
+ dynamic_cast<CGUIAddonWindowDialog*>(pAddonWindow)->Show(true, true);
+ else
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(pAddonWindow->GetID());
+ Interface_GUIGeneral::unlock();
+
+ return true;
+}
+//@}
+
+/*!
+ * Window control functions
+ */
+//@{
+bool Interface_GUIWindow::set_focus_id(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return false;
+ }
+
+ if (!pAddonWindow->GetControl(control_id))
+ {
+ CLog::Log(LOGERROR, "Interface_GUIWindow - {}: {} - Control does not exist in window", __func__,
+ addon->Name());
+ return false;
+ }
+
+ Interface_GUIGeneral::lock();
+ CGUIMessage msg(GUI_MSG_SETFOCUS, pAddonWindow->m_windowId, control_id);
+ pAddonWindow->OnMessage(msg);
+ Interface_GUIGeneral::unlock();
+
+ return true;
+}
+
+int Interface_GUIWindow::get_focus_id(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return -1;
+ }
+
+ Interface_GUIGeneral::lock();
+ int control_id = pAddonWindow->GetFocusedControlID();
+ Interface_GUIGeneral::unlock();
+
+ if (control_id == -1)
+ CLog::Log(LOGERROR, "Interface_GUIWindow - {}: {} - No control in this window has focus",
+ __func__, addon->Name());
+
+ return control_id;
+}
+
+void Interface_GUIWindow::set_control_label(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id,
+ const char* label)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !label)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "label='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(label),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ CGUIMessage msg(GUI_MSG_LABEL_SET, pAddonWindow->m_windowId, control_id);
+ msg.SetLabel(label);
+ pAddonWindow->OnMessage(msg);
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIWindow::set_control_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ CGUIMessage msg(visible ? GUI_MSG_VISIBLE : GUI_MSG_HIDDEN, pAddonWindow->m_windowId, control_id);
+ pAddonWindow->OnMessage(msg);
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIWindow::set_control_selected(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id,
+ bool selected)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ CGUIMessage msg(selected ? GUI_MSG_SET_SELECTED : GUI_MSG_SET_DESELECTED,
+ pAddonWindow->m_windowId, control_id);
+ pAddonWindow->OnMessage(msg);
+ Interface_GUIGeneral::unlock();
+}
+//@}
+
+/*!
+ * Window property functions
+ */
+//@{
+void Interface_GUIWindow::set_property(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ const char* value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !key || !value)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}', value='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ static_cast<const void*>(value), addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->SetProperty(lowerKey, value);
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIWindow::set_property_int(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ int value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !key)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->SetProperty(lowerKey, value);
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIWindow::set_property_bool(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ bool value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !key)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->SetProperty(lowerKey, value);
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIWindow::set_property_double(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ double value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !key)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->SetProperty(lowerKey, value);
+ Interface_GUIGeneral::unlock();
+}
+
+char* Interface_GUIWindow::get_property(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !key)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ Interface_GUIGeneral::lock();
+ std::string value = pAddonWindow->GetProperty(lowerKey).asString();
+ Interface_GUIGeneral::unlock();
+
+ return strdup(value.c_str());
+}
+
+int Interface_GUIWindow::get_property_int(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !key)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ addon ? addon->ID() : "unknown");
+ return -1;
+ }
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ Interface_GUIGeneral::lock();
+ int value = static_cast<int>(pAddonWindow->GetProperty(lowerKey).asInteger());
+ Interface_GUIGeneral::unlock();
+
+ return value;
+}
+
+bool Interface_GUIWindow::get_property_bool(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !key)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ addon ? addon->ID() : "unknown");
+ return false;
+ }
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ Interface_GUIGeneral::lock();
+ bool value = pAddonWindow->GetProperty(lowerKey).asBoolean();
+ Interface_GUIGeneral::unlock();
+
+ return value;
+}
+
+double Interface_GUIWindow::get_property_double(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !key)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ addon ? addon->ID() : "unknown");
+ return 0.0;
+ }
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ Interface_GUIGeneral::lock();
+ double value = pAddonWindow->GetProperty(lowerKey).asDouble();
+ Interface_GUIGeneral::unlock();
+
+ return value;
+}
+
+void Interface_GUIWindow::clear_properties(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->ClearProperties();
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIWindow::clear_property(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !key)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->SetProperty(lowerKey, "");
+ Interface_GUIGeneral::unlock();
+}
+//@}
+
+/*!
+ * List item functions
+ */
+//@{
+void Interface_GUIWindow::clear_item_list(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->ClearList();
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIWindow::add_list_item(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ KODI_GUI_LISTITEM_HANDLE item,
+ int list_position)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !item)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "item='{}') on addon '{}'",
+ __func__, kodiBase, handle, item, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CFileItemPtr* pItem(static_cast<CFileItemPtr*>(item));
+ if (pItem->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIWindow::{} - empty list item called on addon '{}'", __func__,
+ addon->ID());
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->AddItem(pItem, list_position);
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIWindow::remove_list_item_from_position(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int list_position)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->RemoveItem(list_position);
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIWindow::remove_list_item(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ KODI_GUI_LISTITEM_HANDLE item)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !item)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "item='{}') on addon '{}'",
+ __func__, kodiBase, handle, item, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CFileItemPtr* pItem(static_cast<CFileItemPtr*>(item));
+ if (pItem->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIWindow::{} - empty list item called on addon '{}'", __func__,
+ addon->ID());
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->RemoveItem(pItem);
+ Interface_GUIGeneral::unlock();
+}
+
+KODI_GUI_LISTITEM_HANDLE Interface_GUIWindow::get_list_item(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int list_position)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ Interface_GUIGeneral::lock();
+ CFileItemPtr* pItem(pAddonWindow->GetListItem(list_position));
+ if (pItem == nullptr || pItem->get() == nullptr)
+ {
+ CLog::Log(LOGERROR, "ADDON::Interface_GUIWindow - {}: {} - Index out of range", __func__,
+ addon->Name());
+
+ if (pItem)
+ {
+ delete pItem;
+ pItem = nullptr;
+ }
+ }
+ Interface_GUIGeneral::unlock();
+
+ return pItem;
+}
+
+void Interface_GUIWindow::set_current_list_position(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int list_position)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->SetCurrentListPosition(list_position);
+ Interface_GUIGeneral::unlock();
+}
+
+int Interface_GUIWindow::get_current_list_position(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return -1;
+ }
+
+ Interface_GUIGeneral::lock();
+ int listPos = pAddonWindow->GetCurrentListPosition();
+ Interface_GUIGeneral::unlock();
+
+ return listPos;
+}
+
+int Interface_GUIWindow::get_list_size(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return -1;
+ }
+
+ Interface_GUIGeneral::lock();
+ int listSize = pAddonWindow->GetListSize();
+ Interface_GUIGeneral::unlock();
+
+ return listSize;
+}
+
+void Interface_GUIWindow::set_container_property(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ const char* value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !key || !value)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "key='{}', value='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(key),
+ static_cast<const void*>(value), addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->SetContainerProperty(key, value);
+ Interface_GUIGeneral::unlock();
+}
+
+void Interface_GUIWindow::set_container_content(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow || !value)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "value='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(value),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->SetContainerContent(value);
+ Interface_GUIGeneral::unlock();
+}
+
+int Interface_GUIWindow::get_current_container_id(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return -1;
+ }
+
+ Interface_GUIGeneral::lock();
+ int id = pAddonWindow->GetCurrentContainerControlId();
+ Interface_GUIGeneral::unlock();
+
+ return id;
+}
+//@}
+
+/*!
+ * Various functions
+ */
+//@{
+void Interface_GUIWindow::mark_dirty_region(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ pAddonWindow->MarkDirtyRegion();
+ Interface_GUIGeneral::unlock();
+}
+//@}
+
+/*!
+ * GUI control access functions
+ */
+//@{
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_button(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ return GetControl(kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_BUTTON,
+ "button");
+}
+
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_edit(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ return GetControl(kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_EDIT, "edit");
+}
+
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_fade_label(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ return GetControl(kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_FADELABEL,
+ "fade label");
+}
+
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_image(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ return GetControl(kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_IMAGE, "image");
+}
+
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_label(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ return GetControl(kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_LABEL, "label");
+}
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_progress(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ return GetControl(kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_PROGRESS,
+ "progress");
+}
+
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_radio_button(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ return GetControl(kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_RADIO,
+ "radio button");
+}
+
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_render_addon(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ CGUIControl* pGUIControl = static_cast<CGUIControl*>(GetControl(
+ kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_RENDERADDON, "renderaddon"));
+ if (!pGUIControl)
+ return nullptr;
+
+ CGUIAddonRenderingControl* pRenderControl =
+ new CGUIAddonRenderingControl(dynamic_cast<CGUIRenderingControl*>(pGUIControl));
+ return pRenderControl;
+}
+
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_settings_slider(
+ KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id)
+{
+ return GetControl(kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_SETTINGS_SLIDER,
+ "settings slider");
+}
+
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_slider(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ return GetControl(kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_SLIDER,
+ "slider");
+}
+
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_spin(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ return GetControl(kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_SPINEX, "spin");
+}
+
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::get_control_text_box(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id)
+{
+ return GetControl(kodiBase, handle, control_id, __func__, CGUIControl::GUICONTROL_TEXTBOX,
+ "textbox");
+}
+//@}
+
+KODI_GUI_CONTROL_HANDLE Interface_GUIWindow::GetControl(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id,
+ const char* function,
+ CGUIControl::GUICONTROLTYPES type,
+ const std::string& typeName)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonWindow* pAddonWindow = static_cast<CGUIAddonWindow*>(handle);
+ if (!addon || !pAddonWindow)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - invalid handler data (kodiBase='{}', handle='{}') on "
+ "addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ return pAddonWindow->GetAddonControl(control_id, type, typeName);
+}
+
+int Interface_GUIWindow::GetNextAvailableWindowId()
+{
+ Interface_GUIGeneral::lock();
+
+ // if window WINDOW_ADDON_END is in use it means addon can't create more windows
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_ADDON_END))
+ {
+ Interface_GUIGeneral::unlock();
+ CLog::Log(LOGERROR,
+ "Interface_GUIWindow::{} - Maximum number of windows for binary addons reached",
+ __func__);
+ return -1;
+ }
+
+ // window id's WINDOW_ADDON_START - WINDOW_ADDON_END are reserved for addons
+ // get first window id that is not in use
+ int id = WINDOW_ADDON_START;
+ while (id < WINDOW_ADDON_END &&
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow(id) != nullptr)
+ id++;
+
+ Interface_GUIGeneral::unlock();
+ return id;
+}
+
+CGUIAddonWindow::CGUIAddonWindow(int id, const std::string& strXML, CAddonDll* addon, bool isMedia)
+ : CGUIMediaWindow(id, strXML.c_str()),
+ m_clientHandle{nullptr},
+ CBOnInit{nullptr},
+ CBOnFocus{nullptr},
+ CBOnClick{nullptr},
+ CBOnAction{nullptr},
+ CBGetContextButtons{nullptr},
+ CBOnContextButton{nullptr},
+ m_windowId(id),
+ m_oldWindowId(0),
+ m_actionEvent(true),
+ m_addon(addon),
+ m_isMedia(isMedia)
+{
+ m_loadType = LOAD_ON_GUI_INIT;
+}
+
+CGUIControl* CGUIAddonWindow::GetAddonControl(int controlId,
+ CGUIControl::GUICONTROLTYPES type,
+ const std::string& typeName)
+{
+ // Load window resources, if not already done, to have related xml content
+ // present and to let control find it
+ if (!m_windowLoaded)
+ {
+ if (!Initialize())
+ {
+ CLog::Log(LOGERROR,
+ "CGUIAddonGUI_Window::{}: {} - Window initialize failed by control id '{}' request "
+ "for '{}'",
+ __func__, m_addon->Name(), controlId, typeName);
+ return nullptr;
+ }
+ }
+
+ CGUIControl* pGUIControl = GetControl(controlId);
+ if (!pGUIControl)
+ {
+ CLog::Log(LOGERROR,
+ "CGUIAddonGUI_Window::{}: {} - Requested GUI control Id '{}' for '{}' not present!",
+ __func__, m_addon->Name(), controlId, typeName);
+ return nullptr;
+ }
+ else if (pGUIControl->GetControlType() != type)
+ {
+ CLog::Log(LOGERROR,
+ "CGUIAddonGUI_Window::{}: {} - Requested GUI control Id '{}' not the type '{}'!",
+ __func__, m_addon->Name(), controlId, typeName);
+ return nullptr;
+ }
+
+ return pGUIControl;
+}
+
+bool CGUIAddonWindow::OnAction(const CAction& action)
+{
+ // Let addon decide whether it wants to handle action first
+ if (CBOnAction &&
+ CBOnAction(m_clientHandle, CAddonGUITranslator::TranslateActionIdToAddon(action.GetID())))
+ return true;
+
+ return CGUIWindow::OnAction(action);
+}
+
+bool CGUIAddonWindow::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ return CGUIMediaWindow::OnMessage(message);
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CGUIMediaWindow::OnMessage(message);
+
+ if (CBOnInit)
+ CBOnInit(m_clientHandle);
+ return true;
+ }
+ break;
+
+ case GUI_MSG_FOCUSED:
+ {
+ if (m_viewControl.HasControl(message.GetControlId()) &&
+ m_viewControl.GetCurrentControl() != message.GetControlId())
+ {
+ m_viewControl.SetFocused();
+ return true;
+ }
+ // check if our focused control is one of our category buttons
+ int iControl = message.GetControlId();
+ if (CBOnFocus)
+ CBOnFocus(m_clientHandle, iControl);
+ }
+ break;
+
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ // most messages from GUI_MSG_NOTIFY_ALL break container content, whitelist working ones.
+ if (message.GetParam1() == GUI_MSG_PAGE_CHANGE ||
+ message.GetParam1() == GUI_MSG_WINDOW_RESIZE)
+ return CGUIMediaWindow::OnMessage(message);
+ return true;
+ }
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl && iControl != this->GetID())
+ {
+ CGUIControl* controlClicked = this->GetControl(iControl);
+
+ // The old python way used to check list AND SELECITEM method or if its a button, checkmark.
+ // Its done this way for now to allow other controls without a python version like togglebutton to still raise a onAction event
+ if (controlClicked) // Will get problems if we the id is not on the window and we try to do GetControlType on it. So check to make sure it exists
+ {
+ if ((controlClicked->IsContainer() && (message.GetParam1() == ACTION_SELECT_ITEM ||
+ message.GetParam1() == ACTION_MOUSE_LEFT_CLICK)) ||
+ !controlClicked->IsContainer())
+ {
+ if (CBOnClick)
+ return CBOnClick(m_clientHandle, iControl);
+ }
+ else if (controlClicked->IsContainer() &&
+ (message.GetParam1() == ACTION_MOUSE_RIGHT_CLICK ||
+ message.GetParam1() == ACTION_CONTEXT_MENU))
+ {
+ if (CBOnAction)
+ {
+ // Check addon want to handle right click for a context menu, if
+ // not used from addon becomes "GetContextButtons(...)" called.
+ if (CBOnAction(m_clientHandle, ADDON_ACTION_CONTEXT_MENU))
+ return true;
+ }
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ return CGUIMediaWindow::OnMessage(message);
+}
+
+void CGUIAddonWindow::AllocResources(bool forceLoad /*= false */)
+{
+ std::string tmpDir = URIUtils::GetDirectory(GetProperty("xmlfile").asString());
+ std::string fallbackMediaPath;
+ URIUtils::GetParentPath(tmpDir, fallbackMediaPath);
+ URIUtils::RemoveSlashAtEnd(fallbackMediaPath);
+ m_mediaDir = fallbackMediaPath;
+
+ CServiceBroker::GetGUI()->GetTextureManager().AddTexturePath(m_mediaDir);
+ CGUIMediaWindow::AllocResources(forceLoad);
+ CServiceBroker::GetGUI()->GetTextureManager().RemoveTexturePath(m_mediaDir);
+}
+
+void CGUIAddonWindow::Render()
+{
+ CServiceBroker::GetGUI()->GetTextureManager().AddTexturePath(m_mediaDir);
+ CGUIMediaWindow::Render();
+ CServiceBroker::GetGUI()->GetTextureManager().RemoveTexturePath(m_mediaDir);
+}
+
+void CGUIAddonWindow::AddItem(CFileItemPtr* fileItem, int itemPosition)
+{
+ if (itemPosition == -1 || itemPosition > m_vecItems->Size())
+ {
+ m_vecItems->Add(*fileItem);
+ }
+ else if (itemPosition < -1 && !(itemPosition - 1 < m_vecItems->Size()))
+ {
+ m_vecItems->AddFront(*fileItem, 0);
+ }
+ else
+ {
+ m_vecItems->AddFront(*fileItem, itemPosition);
+ }
+ m_viewControl.SetItems(*m_vecItems);
+ UpdateButtons();
+}
+
+void CGUIAddonWindow::RemoveItem(int itemPosition)
+{
+ m_vecItems->Remove(itemPosition);
+ m_viewControl.SetItems(*m_vecItems);
+ UpdateButtons();
+}
+
+void CGUIAddonWindow::RemoveItem(CFileItemPtr* fileItem)
+{
+ m_vecItems->Remove(fileItem->get());
+ m_viewControl.SetItems(*m_vecItems);
+ UpdateButtons();
+}
+
+int CGUIAddonWindow::GetCurrentListPosition()
+{
+ return m_viewControl.GetSelectedItem();
+}
+
+void CGUIAddonWindow::SetCurrentListPosition(int item)
+{
+ m_viewControl.SetSelectedItem(item);
+}
+
+int CGUIAddonWindow::GetListSize()
+{
+ return m_vecItems->Size();
+}
+
+CFileItemPtr* CGUIAddonWindow::GetListItem(int position)
+{
+ if (position < 0 || position >= m_vecItems->Size())
+ return nullptr;
+ return new CFileItemPtr(m_vecItems->Get(position));
+}
+
+void CGUIAddonWindow::ClearList()
+{
+ ClearFileItems();
+
+ m_viewControl.SetItems(*m_vecItems);
+ UpdateButtons();
+}
+
+void CGUIAddonWindow::SetContainerProperty(const std::string& key, const std::string& value)
+{
+ m_vecItems->SetProperty(key, value);
+}
+
+void CGUIAddonWindow::SetContainerContent(const std::string& value)
+{
+ m_vecItems->SetContent(value);
+}
+
+int CGUIAddonWindow::GetCurrentContainerControlId()
+{
+ return m_viewControl.GetCurrentControl();
+}
+
+void CGUIAddonWindow::GetContextButtons(int itemNumber, CContextButtons& buttons)
+{
+ gui_context_menu_pair c_buttons[ADDON_MAX_CONTEXT_ENTRIES] = {};
+ unsigned int size = ADDON_MAX_CONTEXT_ENTRIES;
+ if (CBGetContextButtons)
+ {
+ CBGetContextButtons(m_clientHandle, itemNumber, c_buttons, &size);
+ for (unsigned int i = 0; i < size; ++i)
+ buttons.push_back(std::pair<unsigned int, std::string>(c_buttons[i].id, c_buttons[i].name));
+ }
+}
+
+bool CGUIAddonWindow::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ if (CBOnContextButton)
+ return CBOnContextButton(m_clientHandle, itemNumber, static_cast<unsigned int>(button));
+ return false;
+}
+
+void CGUIAddonWindow::WaitForActionEvent(unsigned int timeout)
+{
+ m_actionEvent.Wait(std::chrono::milliseconds(timeout));
+ m_actionEvent.Reset();
+}
+
+void CGUIAddonWindow::PulseActionEvent()
+{
+ m_actionEvent.Set();
+}
+
+void CGUIAddonWindow::SetupShares()
+{
+ UpdateButtons();
+}
+
+
+CGUIAddonWindowDialog::CGUIAddonWindowDialog(int id, const std::string& strXML, CAddonDll* addon)
+ : CGUIAddonWindow(id, strXML, addon, false), m_bRunning(false)
+{
+}
+
+void CGUIAddonWindowDialog::Show(bool show /* = true */, bool modal /* = true*/)
+{
+ if (modal)
+ {
+ unsigned int count = CServiceBroker::GetWinSystem()->GetGfxContext().exit();
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ADDON_DIALOG, 0, show ? 1 : 0,
+ static_cast<void*>(this));
+ CServiceBroker::GetWinSystem()->GetGfxContext().restore(count);
+ }
+ else
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ADDON_DIALOG, 0, show ? 1 : 0,
+ static_cast<void*>(this));
+}
+
+void CGUIAddonWindowDialog::Show_Internal(bool show /* = true */)
+{
+ if (show)
+ {
+ m_bRunning = true;
+ CServiceBroker::GetGUI()->GetWindowManager().RegisterDialog(this);
+
+ // activate this window...
+ CGUIMessage msg(GUI_MSG_WINDOW_INIT, 0, 0, WINDOW_INVALID, GetID());
+ OnMessage(msg);
+
+ // this dialog is derived from GUIMediaWindow
+ // make sure it is rendered last
+ m_renderOrder = RENDER_ORDER_DIALOG;
+ while (m_bRunning)
+ {
+ if (!ProcessRenderLoop(false))
+ break;
+ }
+ }
+ else // hide
+ {
+ m_bRunning = false;
+
+ CGUIMessage msg(GUI_MSG_WINDOW_DEINIT, 0, 0);
+ OnMessage(msg);
+
+ CServiceBroker::GetGUI()->GetWindowManager().RemoveDialog(GetID());
+ }
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/Window.h b/xbmc/addons/interfaces/gui/Window.h
new file mode 100644
index 0000000..dad0737
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/Window.h
@@ -0,0 +1,278 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/window.h"
+#include "threads/Event.h"
+#include "windows/GUIMediaWindow.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+ struct gui_context_menu_pair;
+
+ namespace ADDON
+ {
+ class CAddonDll;
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/Window.h"
+ */
+ struct Interface_GUIWindow
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ /* Window creation functions */
+ static KODI_GUI_WINDOW_HANDLE create(KODI_HANDLE kodiBase,
+ const char* xml_filename,
+ const char* default_skin,
+ bool as_dialog,
+ bool is_media);
+ static void destroy(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ static void set_callbacks(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ KODI_GUI_CLIENT_HANDLE clienthandle,
+ bool (*CBInit)(KODI_GUI_CLIENT_HANDLE),
+ bool (*CBFocus)(KODI_GUI_CLIENT_HANDLE, int),
+ bool (*CBClick)(KODI_GUI_CLIENT_HANDLE, int),
+ bool (*CBOnAction)(KODI_GUI_CLIENT_HANDLE, ADDON_ACTION),
+ void (*CBGetContextButtons)(KODI_GUI_CLIENT_HANDLE,
+ int,
+ gui_context_menu_pair*,
+ unsigned int*),
+ bool (*CBOnContextButton)(KODI_GUI_CLIENT_HANDLE, int, unsigned int));
+ static bool show(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ static bool close(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ static bool do_modal(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+
+ /* Window control functions */
+ static bool set_focus_id(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ static int get_focus_id(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ static void set_control_label(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id,
+ const char* label);
+ static void set_control_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id,
+ bool visible);
+ static void set_control_selected(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id,
+ bool selected);
+
+ /* Window property functions */
+ static void set_property(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ const char* value);
+ static void set_property_int(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ int value);
+ static void set_property_bool(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ bool value);
+ static void set_property_double(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ double value);
+ static char* get_property(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, const char* key);
+ static int get_property_int(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key);
+ static bool get_property_bool(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key);
+ static double get_property_double(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key);
+ static void clear_properties(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ static void clear_property(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key);
+
+ /* List item functions */
+ static void clear_item_list(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ static void add_list_item(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ KODI_GUI_LISTITEM_HANDLE item,
+ int list_position);
+ static void remove_list_item_from_position(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int list_position);
+ static void remove_list_item(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ KODI_GUI_LISTITEM_HANDLE item);
+ static KODI_GUI_LISTITEM_HANDLE get_list_item(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int list_position);
+ static void set_current_list_position(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int list_position);
+ static int get_current_list_position(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ static int get_list_size(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ static void set_container_property(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ const char* value);
+ static void set_container_content(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* value);
+ static int get_current_container_id(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+
+ /* Various functions */
+ static void mark_dirty_region(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+
+ /* GUI control access functions */
+ static KODI_GUI_CONTROL_HANDLE get_control_button(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ static KODI_GUI_CONTROL_HANDLE get_control_edit(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ static KODI_GUI_CONTROL_HANDLE get_control_fade_label(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ static KODI_GUI_CONTROL_HANDLE get_control_image(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ static KODI_GUI_CONTROL_HANDLE get_control_label(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ static KODI_GUI_CONTROL_HANDLE get_control_radio_button(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ static KODI_GUI_CONTROL_HANDLE get_control_progress(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ static KODI_GUI_CONTROL_HANDLE get_control_render_addon(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ static KODI_GUI_CONTROL_HANDLE get_control_settings_slider(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ static KODI_GUI_CONTROL_HANDLE get_control_slider(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ static KODI_GUI_CONTROL_HANDLE get_control_spin(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ static KODI_GUI_CONTROL_HANDLE get_control_text_box(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id);
+ //@}
+
+ private:
+ static KODI_GUI_CONTROL_HANDLE GetControl(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id,
+ const char* function,
+ CGUIControl::GUICONTROLTYPES type,
+ const std::string& typeName);
+ static int GetNextAvailableWindowId();
+ };
+
+ class CGUIAddonWindow : public CGUIMediaWindow
+ {
+ friend struct Interface_GUIWindow;
+
+ public:
+ CGUIAddonWindow(int id, const std::string& strXML, ADDON::CAddonDll* addon, bool isMedia);
+ ~CGUIAddonWindow() override = default;
+
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void AllocResources(bool forceLoad = false) override;
+ void Render() override;
+ bool IsMediaWindow() const override { return m_isMedia; }
+
+ /* Addon to Kodi call functions */
+ void PulseActionEvent();
+ void AddItem(CFileItemPtr* fileItem, int itemPosition);
+ void RemoveItem(int itemPosition);
+ void RemoveItem(CFileItemPtr* fileItem);
+ void ClearList();
+ CFileItemPtr* GetListItem(int position);
+ int GetListSize();
+ int GetCurrentListPosition();
+ void SetCurrentListPosition(int item);
+ void SetContainerProperty(const std::string& key, const std::string& value);
+ void SetContainerContent(const std::string& value);
+ int GetCurrentContainerControlId();
+ CGUIControl* GetAddonControl(int controlId,
+ CGUIControl::GUICONTROLTYPES type,
+ const std::string& typeName);
+
+ protected:
+ void GetContextButtons(int itemNumber, CContextButtons& buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ void SetupShares() override;
+
+ /* kodi to addon callback function addresses */
+ KODI_GUI_CLIENT_HANDLE m_clientHandle;
+ bool (*CBOnInit)(KODI_GUI_CLIENT_HANDLE cbhdl);
+ bool (*CBOnFocus)(KODI_GUI_CLIENT_HANDLE cbhdl, int controlId);
+ bool (*CBOnClick)(KODI_GUI_CLIENT_HANDLE cbhdl, int controlId);
+ bool (*CBOnAction)(KODI_GUI_CLIENT_HANDLE cbhdl, ADDON_ACTION actionId);
+ void (*CBGetContextButtons)(KODI_GUI_CLIENT_HANDLE cbhdl,
+ int itemNumber,
+ gui_context_menu_pair* buttons,
+ unsigned int* size);
+ bool (*CBOnContextButton)(KODI_GUI_CLIENT_HANDLE cbhdl, int itemNumber, unsigned int button);
+
+ const int m_windowId;
+ int m_oldWindowId;
+
+ private:
+ void WaitForActionEvent(unsigned int timeout);
+
+ CEvent m_actionEvent;
+ ADDON::CAddonDll* m_addon;
+ std::string m_mediaDir;
+ bool m_isMedia;
+ };
+
+ class CGUIAddonWindowDialog : public CGUIAddonWindow
+ {
+ public:
+ CGUIAddonWindowDialog(int id, const std::string& strXML, ADDON::CAddonDll* addon);
+
+ bool IsDialogRunning() const override { return m_bRunning; }
+ bool IsDialog() const override { return true; }
+ bool IsModalDialog() const override { return true; }
+
+ void Show(bool show = true, bool modal = true);
+ void Show_Internal(bool show = true);
+
+ private:
+ bool m_bRunning;
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/Button.cpp b/xbmc/addons/interfaces/gui/controls/Button.cpp
new file mode 100644
index 0000000..24a9994
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Button.cpp
@@ -0,0 +1,146 @@
+/*
+ * 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 "Button.h"
+
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/Button.h"
+#include "guilib/GUIButtonControl.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIControlButton::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_button =
+ new AddonToKodiFuncTable_kodi_gui_control_button();
+
+ addonInterface->toKodi->kodi_gui->control_button->set_visible = set_visible;
+ addonInterface->toKodi->kodi_gui->control_button->set_enabled = set_enabled;
+
+ addonInterface->toKodi->kodi_gui->control_button->set_label = set_label;
+ addonInterface->toKodi->kodi_gui->control_button->get_label = get_label;
+
+ addonInterface->toKodi->kodi_gui->control_button->set_label2 = set_label2;
+ addonInterface->toKodi->kodi_gui->control_button->get_label2 = get_label2;
+}
+
+void Interface_GUIControlButton::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_button;
+}
+
+void Interface_GUIControlButton::set_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIButtonControl* control = static_cast<CGUIButtonControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlButton::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetVisible(visible);
+}
+
+void Interface_GUIControlButton::set_enabled(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool enabled)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIButtonControl* control = static_cast<CGUIButtonControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlButton::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetEnabled(enabled);
+}
+
+void Interface_GUIControlButton::set_label(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIButtonControl* control = static_cast<CGUIButtonControl*>(handle);
+ if (!addon || !control || !label)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlButton::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "label='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(label),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetLabel(label);
+}
+
+char* Interface_GUIControlButton::get_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIButtonControl* control = static_cast<CGUIButtonControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlButton::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ return strdup(control->GetLabel().c_str());
+}
+
+void Interface_GUIControlButton::set_label2(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIButtonControl* control = static_cast<CGUIButtonControl*>(handle);
+ if (!addon || !control || !label)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlButton::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "label='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(label),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetLabel2(label);
+}
+
+char* Interface_GUIControlButton::get_label2(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIButtonControl* control = static_cast<CGUIButtonControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlButton::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ return strdup(control->GetLabel2().c_str());
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/Button.h b/xbmc/addons/interfaces/gui/controls/Button.h
new file mode 100644
index 0000000..221ae4e
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Button.h
@@ -0,0 +1,56 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/button.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Button.h"
+ */
+ struct Interface_GUIControlButton
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_visible(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ static void set_enabled(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+
+ static void set_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* label);
+ static char* get_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ static void set_label2(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* label);
+ static char* get_label2(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/CMakeLists.txt b/xbmc/addons/interfaces/gui/controls/CMakeLists.txt
new file mode 100644
index 0000000..0a708c6
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/CMakeLists.txt
@@ -0,0 +1,27 @@
+set(SOURCES Button.cpp
+ Edit.cpp
+ FadeLabel.cpp
+ Image.cpp
+ Label.cpp
+ Progress.cpp
+ RadioButton.cpp
+ Rendering.cpp
+ SettingsSlider.cpp
+ Slider.cpp
+ Spin.cpp
+ TextBox.cpp)
+
+set(HEADERS Button.h
+ Edit.h
+ FadeLabel.h
+ Image.h
+ Label.h
+ Progress.h
+ RadioButton.h
+ Rendering.h
+ SettingsSlider.h
+ Slider.h
+ Spin.h
+ TextBox.h)
+
+core_add_library(addons_interfaces_gui_controls)
diff --git a/xbmc/addons/interfaces/gui/controls/Edit.cpp b/xbmc/addons/interfaces/gui/controls/Edit.cpp
new file mode 100644
index 0000000..b537279
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Edit.cpp
@@ -0,0 +1,239 @@
+/*
+ * 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 "Edit.h"
+
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/Edit.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIControlEdit::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_edit = new AddonToKodiFuncTable_kodi_gui_control_edit();
+
+ addonInterface->toKodi->kodi_gui->control_edit->set_visible = set_visible;
+ addonInterface->toKodi->kodi_gui->control_edit->set_enabled = set_enabled;
+ addonInterface->toKodi->kodi_gui->control_edit->set_input_type = set_input_type;
+ addonInterface->toKodi->kodi_gui->control_edit->set_label = set_label;
+ addonInterface->toKodi->kodi_gui->control_edit->get_label = get_label;
+ addonInterface->toKodi->kodi_gui->control_edit->set_text = set_text;
+ addonInterface->toKodi->kodi_gui->control_edit->get_text = get_text;
+ addonInterface->toKodi->kodi_gui->control_edit->set_cursor_position = set_cursor_position;
+ addonInterface->toKodi->kodi_gui->control_edit->get_cursor_position = get_cursor_position;
+}
+
+void Interface_GUIControlEdit::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_edit;
+}
+
+void Interface_GUIControlEdit::set_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIEditControl* control = static_cast<CGUIEditControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlEdit::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetVisible(visible);
+}
+
+void Interface_GUIControlEdit::set_enabled(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool enable)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIEditControl* control = static_cast<CGUIEditControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlEdit::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetEnabled(enable);
+}
+
+void Interface_GUIControlEdit::set_input_type(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int type,
+ const char* heading)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIEditControl* control = static_cast<CGUIEditControl*>(handle);
+ if (!addon || !control || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlEdit::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "heading='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(heading),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CGUIEditControl::INPUT_TYPE kodiType;
+ switch (static_cast<AddonGUIInputType>(type))
+ {
+ case ADDON_INPUT_TYPE_TEXT:
+ kodiType = CGUIEditControl::INPUT_TYPE_TEXT;
+ break;
+ case ADDON_INPUT_TYPE_NUMBER:
+ kodiType = CGUIEditControl::INPUT_TYPE_NUMBER;
+ break;
+ case ADDON_INPUT_TYPE_SECONDS:
+ kodiType = CGUIEditControl::INPUT_TYPE_SECONDS;
+ break;
+ case ADDON_INPUT_TYPE_TIME:
+ kodiType = CGUIEditControl::INPUT_TYPE_TIME;
+ break;
+ case ADDON_INPUT_TYPE_DATE:
+ kodiType = CGUIEditControl::INPUT_TYPE_DATE;
+ break;
+ case ADDON_INPUT_TYPE_IPADDRESS:
+ kodiType = CGUIEditControl::INPUT_TYPE_IPADDRESS;
+ break;
+ case ADDON_INPUT_TYPE_PASSWORD:
+ kodiType = CGUIEditControl::INPUT_TYPE_PASSWORD;
+ break;
+ case ADDON_INPUT_TYPE_PASSWORD_MD5:
+ kodiType = CGUIEditControl::INPUT_TYPE_PASSWORD_MD5;
+ break;
+ case ADDON_INPUT_TYPE_SEARCH:
+ kodiType = CGUIEditControl::INPUT_TYPE_SEARCH;
+ break;
+ case ADDON_INPUT_TYPE_FILTER:
+ kodiType = CGUIEditControl::INPUT_TYPE_FILTER;
+ break;
+ case ADDON_INPUT_TYPE_READONLY:
+ default:
+ kodiType = CGUIEditControl::INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW;
+ }
+
+ control->SetInputType(kodiType, heading);
+}
+
+void Interface_GUIControlEdit::set_label(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIEditControl* control = static_cast<CGUIEditControl*>(handle);
+ if (!addon || !control || !label)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlEdit::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "label='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(label),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetLabel(label);
+}
+
+char* Interface_GUIControlEdit::get_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIEditControl* control = static_cast<CGUIEditControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlEdit::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ return strdup(control->GetLabel().c_str());
+}
+
+void Interface_GUIControlEdit::set_text(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* text)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIEditControl* control = static_cast<CGUIEditControl*>(handle);
+ if (!addon || !control || !text)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlEdit::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "text='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(text),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetLabel2(text);
+}
+
+char* Interface_GUIControlEdit::get_text(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIEditControl* control = static_cast<CGUIEditControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlEdit::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ return strdup(control->GetLabel2().c_str());
+}
+
+void Interface_GUIControlEdit::set_cursor_position(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ unsigned int position)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIEditControl* control = static_cast<CGUIEditControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlEdit::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetCursorPosition(position);
+}
+
+unsigned int Interface_GUIControlEdit::get_cursor_position(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIEditControl* control = static_cast<CGUIEditControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlEdit::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return 0;
+ }
+
+ return control->GetCursorPosition();
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/Edit.h b/xbmc/addons/interfaces/gui/controls/Edit.h
new file mode 100644
index 0000000..9dda1e2
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Edit.h
@@ -0,0 +1,67 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/edit.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Edit.h"
+ */
+ struct Interface_GUIControlEdit
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_visible(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ static void set_enabled(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+
+ static void set_input_type(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int type,
+ const char* heading);
+
+ static void set_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* label);
+ static char* get_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+
+ static void set_text(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* text);
+ static char* get_text(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+
+ static void set_cursor_position(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ unsigned int position);
+ static unsigned int get_cursor_position(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/FadeLabel.cpp b/xbmc/addons/interfaces/gui/controls/FadeLabel.cpp
new file mode 100644
index 0000000..13c5604
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/FadeLabel.cpp
@@ -0,0 +1,130 @@
+/*
+ * 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 "FadeLabel.h"
+
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/FadeLabel.h"
+#include "guilib/GUIFadeLabelControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIControlFadeLabel::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_fade_label =
+ new AddonToKodiFuncTable_kodi_gui_control_fade_label();
+
+ addonInterface->toKodi->kodi_gui->control_fade_label->set_visible = set_visible;
+ addonInterface->toKodi->kodi_gui->control_fade_label->add_label = add_label;
+ addonInterface->toKodi->kodi_gui->control_fade_label->get_label = get_label;
+ addonInterface->toKodi->kodi_gui->control_fade_label->set_scrolling = set_scrolling;
+ addonInterface->toKodi->kodi_gui->control_fade_label->reset = reset;
+}
+
+void Interface_GUIControlFadeLabel::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_fade_label;
+}
+
+void Interface_GUIControlFadeLabel::set_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIFadeLabelControl* control = static_cast<CGUIFadeLabelControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlFadeLabel::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetVisible(visible);
+}
+
+void Interface_GUIControlFadeLabel::add_label(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIFadeLabelControl* control = static_cast<CGUIFadeLabelControl*>(handle);
+ if (!addon || !control || !label)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlFadeLabel::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}', label='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(label),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_ADD, control->GetParentID(), control->GetID());
+ msg.SetLabel(label);
+ control->OnMessage(msg);
+}
+
+char* Interface_GUIControlFadeLabel::get_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIFadeLabelControl* control = static_cast<CGUIFadeLabelControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlFadeLabel::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, control->GetParentID(), control->GetID());
+ control->OnMessage(msg);
+ std::string text = msg.GetLabel();
+ return strdup(text.c_str());
+}
+
+void Interface_GUIControlFadeLabel::set_scrolling(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool scroll)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIFadeLabelControl* control = static_cast<CGUIFadeLabelControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlFadeLabel::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetScrolling(scroll);
+}
+
+void Interface_GUIControlFadeLabel::reset(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIFadeLabelControl* control = static_cast<CGUIFadeLabelControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlFadeLabel::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, control->GetParentID(), control->GetID());
+ control->OnMessage(msg);
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/FadeLabel.h b/xbmc/addons/interfaces/gui/controls/FadeLabel.h
new file mode 100644
index 0000000..2c3cd68
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/FadeLabel.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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/fade_label.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/FadeLabel.h"
+ */
+ struct Interface_GUIControlFadeLabel
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_visible(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ static void set_enabled(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+ static void set_selected(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool selected);
+
+ static void add_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* label);
+ static char* get_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ static void set_scrolling(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool scroll);
+ static void reset(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/Image.cpp b/xbmc/addons/interfaces/gui/controls/Image.cpp
new file mode 100644
index 0000000..88f368c
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Image.cpp
@@ -0,0 +1,92 @@
+/*
+ * 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 "Image.h"
+
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/Image.h"
+#include "guilib/GUIImage.h"
+#include "utils/log.h"
+
+using namespace KODI;
+
+namespace ADDON
+{
+
+void Interface_GUIControlImage::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_image =
+ new AddonToKodiFuncTable_kodi_gui_control_image();
+
+ addonInterface->toKodi->kodi_gui->control_image->set_visible = set_visible;
+ addonInterface->toKodi->kodi_gui->control_image->set_filename = set_filename;
+ addonInterface->toKodi->kodi_gui->control_image->set_color_diffuse = set_color_diffuse;
+}
+
+void Interface_GUIControlImage::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_image;
+}
+
+void Interface_GUIControlImage::set_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIImage* control = static_cast<CGUIImage*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlImage::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetVisible(visible);
+}
+
+void Interface_GUIControlImage::set_filename(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* filename,
+ bool use_cache)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIImage* control = static_cast<CGUIImage*>(handle);
+ if (!addon || !control || !filename)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlImage::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "filename='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(filename),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetFileName(filename, false, use_cache);
+}
+
+void Interface_GUIControlImage::set_color_diffuse(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ uint32_t colorDiffuse)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIImage* control = static_cast<CGUIImage*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlImage::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetColorDiffuse(GUILIB::GUIINFO::CGUIInfoColor(colorDiffuse));
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/Image.h b/xbmc/addons/interfaces/gui/controls/Image.h
new file mode 100644
index 0000000..b12bece
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Image.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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/image.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Image.h"
+ */
+ struct Interface_GUIControlImage
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_visible(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ static void set_filename(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* filename,
+ bool use_cache);
+ static void set_color_diffuse(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ uint32_t color_diffuse);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/Label.cpp b/xbmc/addons/interfaces/gui/controls/Label.cpp
new file mode 100644
index 0000000..3922e3f
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Label.cpp
@@ -0,0 +1,92 @@
+/*
+ * 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 "Label.h"
+
+#include "ServiceBroker.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/Label.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUILabelControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIControlLabel::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_label =
+ new AddonToKodiFuncTable_kodi_gui_control_label();
+
+ addonInterface->toKodi->kodi_gui->control_label->set_visible = set_visible;
+ addonInterface->toKodi->kodi_gui->control_label->set_label = set_label;
+ addonInterface->toKodi->kodi_gui->control_label->get_label = get_label;
+}
+
+void Interface_GUIControlLabel::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_label;
+}
+
+void Interface_GUIControlLabel::set_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUILabelControl* control = static_cast<CGUILabelControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlLabel::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetVisible(visible);
+}
+
+void Interface_GUIControlLabel::set_label(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUILabelControl* control = static_cast<CGUILabelControl*>(handle);
+ if (!addon || !control || !label)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlLabel::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "label='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(label),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_SET, control->GetParentID(), control->GetID());
+ msg.SetLabel(label);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, control->GetParentID());
+}
+
+char* Interface_GUIControlLabel::get_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUILabelControl* control = static_cast<CGUILabelControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlLabel::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ return strdup(control->GetDescription().c_str());
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/Label.h b/xbmc/addons/interfaces/gui/controls/Label.h
new file mode 100644
index 0000000..020c3d5
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Label.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/label.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Label.h"
+ */
+ struct Interface_GUIControlLabel
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_visible(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+
+ static void set_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* label);
+ static char* get_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/Progress.cpp b/xbmc/addons/interfaces/gui/controls/Progress.cpp
new file mode 100644
index 0000000..639d079
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Progress.cpp
@@ -0,0 +1,88 @@
+/*
+ * 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 "Progress.h"
+
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/Progress.h"
+#include "guilib/GUIProgressControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIControlProgress::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_progress =
+ new AddonToKodiFuncTable_kodi_gui_control_progress();
+
+ addonInterface->toKodi->kodi_gui->control_progress->set_visible = set_visible;
+ addonInterface->toKodi->kodi_gui->control_progress->set_percentage = set_percentage;
+ addonInterface->toKodi->kodi_gui->control_progress->get_percentage = get_percentage;
+}
+
+void Interface_GUIControlProgress::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_progress;
+}
+
+void Interface_GUIControlProgress::set_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIProgressControl* control = static_cast<CGUIProgressControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlProgress::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetVisible(visible);
+}
+
+void Interface_GUIControlProgress::set_percentage(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float percent)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIProgressControl* control = static_cast<CGUIProgressControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlProgress::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetPercentage(percent);
+}
+
+float Interface_GUIControlProgress::get_percentage(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIProgressControl* control = static_cast<CGUIProgressControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlProgress::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return 0.0f;
+ }
+
+ return control->GetPercentage();
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/Progress.h b/xbmc/addons/interfaces/gui/controls/Progress.h
new file mode 100644
index 0000000..c3368fa
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Progress.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/progress.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Progress.h"
+ */
+ struct Interface_GUIControlProgress
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_visible(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+
+ static void set_percentage(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float percent);
+ static float get_percentage(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/RadioButton.cpp b/xbmc/addons/interfaces/gui/controls/RadioButton.cpp
new file mode 100644
index 0000000..9d3b826
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/RadioButton.cpp
@@ -0,0 +1,146 @@
+/*
+ * 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 "RadioButton.h"
+
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/RadioButton.h"
+#include "guilib/GUIRadioButtonControl.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIControlRadioButton::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_radio_button =
+ new AddonToKodiFuncTable_kodi_gui_control_radio_button();
+
+ addonInterface->toKodi->kodi_gui->control_radio_button->set_visible = set_visible;
+ addonInterface->toKodi->kodi_gui->control_radio_button->set_enabled = set_enabled;
+
+ addonInterface->toKodi->kodi_gui->control_radio_button->set_label = set_label;
+ addonInterface->toKodi->kodi_gui->control_radio_button->get_label = get_label;
+
+ addonInterface->toKodi->kodi_gui->control_radio_button->set_selected = set_selected;
+ addonInterface->toKodi->kodi_gui->control_radio_button->is_selected = is_selected;
+}
+
+void Interface_GUIControlRadioButton::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_radio_button;
+}
+
+void Interface_GUIControlRadioButton::set_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIRadioButtonControl* control = static_cast<CGUIRadioButtonControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlRadioButton::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetVisible(visible);
+}
+
+void Interface_GUIControlRadioButton::set_enabled(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool enabled)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIRadioButtonControl* control = static_cast<CGUIRadioButtonControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlRadioButton::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetEnabled(enabled);
+}
+
+void Interface_GUIControlRadioButton::set_label(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIRadioButtonControl* control = static_cast<CGUIRadioButtonControl*>(handle);
+ if (!addon || !control || !label)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlRadioButton::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}', label='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(label),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetLabel(label);
+}
+
+char* Interface_GUIControlRadioButton::get_label(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIRadioButtonControl* control = static_cast<CGUIRadioButtonControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlRadioButton::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ return strdup(control->GetLabel().c_str());
+}
+
+void Interface_GUIControlRadioButton::set_selected(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool selected)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIRadioButtonControl* control = static_cast<CGUIRadioButtonControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlRadioButton::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetSelected(selected);
+}
+
+bool Interface_GUIControlRadioButton::is_selected(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIRadioButtonControl* control = static_cast<CGUIRadioButtonControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlRadioButton::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return false;
+ }
+
+ return control->IsSelected();
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/RadioButton.h b/xbmc/addons/interfaces/gui/controls/RadioButton.h
new file mode 100644
index 0000000..dd7ed4e
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/RadioButton.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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/radio_button.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/RadioButton.h"
+ */
+ struct Interface_GUIControlRadioButton
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_visible(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ static void set_enabled(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+
+ static void set_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* label);
+ static char* get_label(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+
+ static void set_selected(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool selected);
+ static bool is_selected(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/Rendering.cpp b/xbmc/addons/interfaces/gui/controls/Rendering.cpp
new file mode 100644
index 0000000..a26a27f
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Rendering.cpp
@@ -0,0 +1,148 @@
+/*
+ * 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 "Rendering.h"
+
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/interfaces/gui/General.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/Rendering.h"
+#include "guilib/GUIRenderingControl.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIControlAddonRendering::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_rendering =
+ new AddonToKodiFuncTable_kodi_gui_control_rendering();
+
+ addonInterface->toKodi->kodi_gui->control_rendering->set_callbacks = set_callbacks;
+ addonInterface->toKodi->kodi_gui->control_rendering->destroy = destroy;
+}
+
+void Interface_GUIControlAddonRendering::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_rendering;
+}
+
+void Interface_GUIControlAddonRendering::set_callbacks(
+ KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ KODI_GUI_CLIENT_HANDLE clienthandle,
+ bool (*createCB)(KODI_GUI_CLIENT_HANDLE, int, int, int, int, ADDON_HARDWARE_CONTEXT),
+ void (*renderCB)(KODI_GUI_CLIENT_HANDLE),
+ void (*stopCB)(KODI_GUI_CLIENT_HANDLE),
+ bool (*dirtyCB)(KODI_GUI_CLIENT_HANDLE))
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonRenderingControl* control = static_cast<CGUIAddonRenderingControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlAddonRendering::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ control->m_clientHandle = clienthandle;
+ control->CBCreate = createCB;
+ control->CBRender = renderCB;
+ control->CBStop = stopCB;
+ control->CBDirty = dirtyCB;
+ control->m_addon = addon;
+ Interface_GUIGeneral::unlock();
+
+ control->m_control->InitCallback(control);
+}
+
+void Interface_GUIControlAddonRendering::destroy(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUIAddonRenderingControl* control = static_cast<CGUIAddonRenderingControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlAddonRendering::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ Interface_GUIGeneral::lock();
+ static_cast<CGUIAddonRenderingControl*>(handle)->Delete();
+ Interface_GUIGeneral::unlock();
+}
+
+
+CGUIAddonRenderingControl::CGUIAddonRenderingControl(CGUIRenderingControl* control)
+ : CBCreate{nullptr},
+ CBRender{nullptr},
+ CBStop{nullptr},
+ CBDirty{nullptr},
+ m_clientHandle{nullptr},
+ m_addon{nullptr},
+ m_control{control},
+ m_refCount{1}
+{
+}
+
+bool CGUIAddonRenderingControl::Create(int x, int y, int w, int h, void* device)
+{
+ if (CBCreate)
+ {
+ if (CBCreate(m_clientHandle, x, y, w, h, device))
+ {
+ ++m_refCount;
+ return true;
+ }
+ }
+ return false;
+}
+
+void CGUIAddonRenderingControl::Render()
+{
+ if (CBRender)
+ {
+ CBRender(m_clientHandle);
+ }
+}
+
+void CGUIAddonRenderingControl::Stop()
+{
+ if (CBStop)
+ {
+ CBStop(m_clientHandle);
+ }
+
+ --m_refCount;
+ if (m_refCount <= 0)
+ delete this;
+}
+
+void CGUIAddonRenderingControl::Delete()
+{
+ --m_refCount;
+ if (m_refCount <= 0)
+ delete this;
+}
+
+bool CGUIAddonRenderingControl::IsDirty()
+{
+ bool ret = true;
+ if (CBDirty)
+ {
+ ret = CBDirty(m_clientHandle);
+ }
+ return ret;
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/Rendering.h b/xbmc/addons/interfaces/gui/controls/Rendering.h
new file mode 100644
index 0000000..dbd82d8
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Rendering.h
@@ -0,0 +1,89 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/rendering.h"
+#include "guilib/IRenderingCallback.h"
+
+class CGUIRenderingControl;
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ class CAddonDll;
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Rendering.h"
+ */
+ struct Interface_GUIControlAddonRendering
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_callbacks(
+ KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ KODI_GUI_CLIENT_HANDLE clienthandle,
+ bool (*createCB)(KODI_GUI_CLIENT_HANDLE, int, int, int, int, ADDON_HARDWARE_CONTEXT),
+ void (*renderCB)(KODI_GUI_CLIENT_HANDLE),
+ void (*stopCB)(KODI_GUI_CLIENT_HANDLE),
+ bool (*dirtyCB)(KODI_GUI_CLIENT_HANDLE));
+ static void destroy(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ //@}
+ };
+
+ class CGUIAddonRenderingControl : public IRenderingCallback
+ {
+ friend struct Interface_GUIControlAddonRendering;
+
+ public:
+ explicit CGUIAddonRenderingControl(CGUIRenderingControl* pControl);
+ ~CGUIAddonRenderingControl() override = default;
+
+ bool Create(int x, int y, int w, int h, void* device) override;
+ void Render() override;
+ void Stop() override;
+ bool IsDirty() override;
+ virtual void Delete();
+
+ protected:
+ bool (*CBCreate)(KODI_GUI_CLIENT_HANDLE cbhdl, int x, int y, int w, int h, void* device);
+ void (*CBRender)(KODI_GUI_CLIENT_HANDLE cbhdl);
+ void (*CBStop)(KODI_GUI_CLIENT_HANDLE cbhdl);
+ bool (*CBDirty)(KODI_GUI_CLIENT_HANDLE cbhdl);
+
+ KODI_GUI_CLIENT_HANDLE m_clientHandle;
+ CAddonDll* m_addon;
+ CGUIRenderingControl* m_control;
+ int m_refCount;
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/SettingsSlider.cpp b/xbmc/addons/interfaces/gui/controls/SettingsSlider.cpp
new file mode 100644
index 0000000..c847a64
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/SettingsSlider.cpp
@@ -0,0 +1,311 @@
+/*
+ * 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 "SettingsSlider.h"
+
+#include "ServiceBroker.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/SettingsSlider.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUISettingsSliderControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIControlSettingsSlider::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_settings_slider =
+ new AddonToKodiFuncTable_kodi_gui_control_settings_slider();
+
+ addonInterface->toKodi->kodi_gui->control_settings_slider->set_visible = set_visible;
+ addonInterface->toKodi->kodi_gui->control_settings_slider->set_enabled = set_enabled;
+
+ addonInterface->toKodi->kodi_gui->control_settings_slider->set_text = set_text;
+ addonInterface->toKodi->kodi_gui->control_settings_slider->reset = reset;
+
+ addonInterface->toKodi->kodi_gui->control_settings_slider->set_int_range = set_int_range;
+ addonInterface->toKodi->kodi_gui->control_settings_slider->set_int_value = set_int_value;
+ addonInterface->toKodi->kodi_gui->control_settings_slider->get_int_value = get_int_value;
+ addonInterface->toKodi->kodi_gui->control_settings_slider->set_int_interval = set_int_interval;
+
+ addonInterface->toKodi->kodi_gui->control_settings_slider->set_percentage = set_percentage;
+ addonInterface->toKodi->kodi_gui->control_settings_slider->get_percentage = get_percentage;
+
+ addonInterface->toKodi->kodi_gui->control_settings_slider->set_float_range = set_float_range;
+ addonInterface->toKodi->kodi_gui->control_settings_slider->set_float_value = set_float_value;
+ addonInterface->toKodi->kodi_gui->control_settings_slider->get_float_value = get_float_value;
+ addonInterface->toKodi->kodi_gui->control_settings_slider->set_float_interval =
+ set_float_interval;
+}
+
+void Interface_GUIControlSettingsSlider::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_settings_slider;
+}
+
+void Interface_GUIControlSettingsSlider::set_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetVisible(visible);
+}
+
+void Interface_GUIControlSettingsSlider::set_enabled(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool enabled)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetEnabled(enabled);
+}
+
+void Interface_GUIControlSettingsSlider::set_text(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* text)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control || !text)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}', text='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(text),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_SET, control->GetParentID(), control->GetID());
+ msg.SetLabel(text);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, control->GetParentID());
+}
+
+void Interface_GUIControlSettingsSlider::reset(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, control->GetParentID(), control->GetID());
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, control->GetParentID());
+}
+
+void Interface_GUIControlSettingsSlider::set_int_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int start,
+ int end)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetType(SLIDER_CONTROL_TYPE_INT);
+ control->SetRange(start, end);
+}
+
+void Interface_GUIControlSettingsSlider::set_int_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetType(SLIDER_CONTROL_TYPE_INT);
+ control->SetIntValue(value);
+}
+
+int Interface_GUIControlSettingsSlider::get_int_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return -1;
+ }
+
+ return control->GetIntValue();
+}
+
+void Interface_GUIControlSettingsSlider::set_int_interval(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int interval)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetIntInterval(interval);
+}
+
+void Interface_GUIControlSettingsSlider::set_percentage(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float percent)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetType(SLIDER_CONTROL_TYPE_PERCENTAGE);
+ control->SetPercentage(percent);
+}
+
+float Interface_GUIControlSettingsSlider::get_percentage(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return 0.0f;
+ }
+
+ return control->GetPercentage();
+}
+
+void Interface_GUIControlSettingsSlider::set_float_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float start,
+ float end)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetType(SLIDER_CONTROL_TYPE_FLOAT);
+ control->SetFloatRange(start, end);
+}
+
+void Interface_GUIControlSettingsSlider::set_float_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetType(SLIDER_CONTROL_TYPE_FLOAT);
+ control->SetFloatValue(value);
+}
+
+float Interface_GUIControlSettingsSlider::get_float_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return 0.0f;
+ }
+
+ return control->GetFloatValue();
+}
+
+void Interface_GUIControlSettingsSlider::set_float_interval(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float interval)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISettingsSliderControl* control = static_cast<CGUISettingsSliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSettingsSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetFloatInterval(interval);
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/SettingsSlider.h b/xbmc/addons/interfaces/gui/controls/SettingsSlider.h
new file mode 100644
index 0000000..425c275
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/SettingsSlider.h
@@ -0,0 +1,77 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/settings_slider.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/SettingsSlider.h"
+ */
+ struct Interface_GUIControlSettingsSlider
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_visible(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ static void set_enabled(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+
+ static void set_text(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* text);
+ static void reset(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+
+ static void set_int_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int start,
+ int end);
+ static void set_int_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int value);
+ static int get_int_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ static void set_int_interval(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int interval);
+
+ static void set_percentage(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float percent);
+ static float get_percentage(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+
+ static void set_float_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float start,
+ float end);
+ static void set_float_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float value);
+ static float get_float_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ static void set_float_interval(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float interval);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/Slider.cpp b/xbmc/addons/interfaces/gui/controls/Slider.cpp
new file mode 100644
index 0000000..73502d8
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Slider.cpp
@@ -0,0 +1,305 @@
+/*
+ * 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 "Slider.h"
+
+#include "ServiceBroker.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/Slider.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUISliderControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIControlSlider::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_slider =
+ new AddonToKodiFuncTable_kodi_gui_control_slider();
+
+ addonInterface->toKodi->kodi_gui->control_slider->set_visible = set_visible;
+ addonInterface->toKodi->kodi_gui->control_slider->set_enabled = set_enabled;
+
+ addonInterface->toKodi->kodi_gui->control_slider->reset = reset;
+ addonInterface->toKodi->kodi_gui->control_slider->get_description = get_description;
+
+ addonInterface->toKodi->kodi_gui->control_slider->set_int_range = set_int_range;
+ addonInterface->toKodi->kodi_gui->control_slider->set_int_value = set_int_value;
+ addonInterface->toKodi->kodi_gui->control_slider->get_int_value = get_int_value;
+ addonInterface->toKodi->kodi_gui->control_slider->set_int_interval = set_int_interval;
+
+ addonInterface->toKodi->kodi_gui->control_slider->set_percentage = set_percentage;
+ addonInterface->toKodi->kodi_gui->control_slider->get_percentage = get_percentage;
+
+ addonInterface->toKodi->kodi_gui->control_slider->set_float_range = set_float_range;
+ addonInterface->toKodi->kodi_gui->control_slider->set_float_value = set_float_value;
+ addonInterface->toKodi->kodi_gui->control_slider->get_float_value = get_float_value;
+ addonInterface->toKodi->kodi_gui->control_slider->set_float_interval = set_float_interval;
+}
+
+void Interface_GUIControlSlider::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_slider;
+}
+
+void Interface_GUIControlSlider::set_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetVisible(visible);
+}
+
+void Interface_GUIControlSlider::set_enabled(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool enabled)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetEnabled(enabled);
+}
+
+void Interface_GUIControlSlider::reset(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, control->GetParentID(), control->GetID());
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, control->GetParentID());
+}
+
+char* Interface_GUIControlSlider::get_description(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ return strdup(control->GetDescription().c_str());
+}
+
+void Interface_GUIControlSlider::set_int_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int start,
+ int end)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetType(SLIDER_CONTROL_TYPE_INT);
+ control->SetRange(start, end);
+}
+
+void Interface_GUIControlSlider::set_int_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetType(SLIDER_CONTROL_TYPE_INT);
+ control->SetIntValue(value);
+}
+
+int Interface_GUIControlSlider::get_int_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return -1;
+ }
+
+ return control->GetIntValue();
+}
+
+void Interface_GUIControlSlider::set_int_interval(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int interval)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetIntInterval(interval);
+}
+
+void Interface_GUIControlSlider::set_percentage(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float percent)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetType(SLIDER_CONTROL_TYPE_PERCENTAGE);
+ control->SetPercentage(percent);
+}
+
+float Interface_GUIControlSlider::get_percentage(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return 0.0f;
+ }
+
+ return control->GetPercentage();
+}
+
+void Interface_GUIControlSlider::set_float_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float start,
+ float end)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetType(SLIDER_CONTROL_TYPE_FLOAT);
+ control->SetFloatRange(start, end);
+}
+
+void Interface_GUIControlSlider::set_float_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetType(SLIDER_CONTROL_TYPE_FLOAT);
+ control->SetFloatValue(value);
+}
+
+float Interface_GUIControlSlider::get_float_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return 0.0f;
+ }
+
+ return control->GetFloatValue();
+}
+
+void Interface_GUIControlSlider::set_float_interval(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float interval)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISliderControl* control = static_cast<CGUISliderControl*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSlider::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetFloatInterval(interval);
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/Slider.h b/xbmc/addons/interfaces/gui/controls/Slider.h
new file mode 100644
index 0000000..30afbaf
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Slider.h
@@ -0,0 +1,78 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/slider.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Slider.h"
+ */
+ struct Interface_GUIControlSlider
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_visible(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ static void set_enabled(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+
+ static void reset(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+
+ static char* get_description(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+
+ static void set_int_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int start,
+ int end);
+ static void set_int_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int value);
+ static int get_int_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ static void set_int_interval(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int interval);
+
+ static void set_percentage(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float percent);
+ static float get_percentage(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+
+ static void set_float_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float start,
+ float end);
+ static void set_float_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float value);
+ static float get_float_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ static void set_float_interval(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float interval);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/Spin.cpp b/xbmc/addons/interfaces/gui/controls/Spin.cpp
new file mode 100644
index 0000000..b9eb6ea
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Spin.cpp
@@ -0,0 +1,346 @@
+/*
+ * 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 "Spin.h"
+
+#include "ServiceBroker.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/Spin.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUISpinControlEx.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIControlSpin::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_spin = new AddonToKodiFuncTable_kodi_gui_control_spin();
+
+ addonInterface->toKodi->kodi_gui->control_spin->set_visible = set_visible;
+ addonInterface->toKodi->kodi_gui->control_spin->set_enabled = set_enabled;
+
+ addonInterface->toKodi->kodi_gui->control_spin->set_text = set_text;
+ addonInterface->toKodi->kodi_gui->control_spin->reset = reset;
+ addonInterface->toKodi->kodi_gui->control_spin->set_type = set_type;
+
+ addonInterface->toKodi->kodi_gui->control_spin->add_string_label = add_string_label;
+ addonInterface->toKodi->kodi_gui->control_spin->set_string_value = set_string_value;
+ addonInterface->toKodi->kodi_gui->control_spin->get_string_value = get_string_value;
+
+ addonInterface->toKodi->kodi_gui->control_spin->add_int_label = add_int_label;
+ addonInterface->toKodi->kodi_gui->control_spin->set_int_range = set_int_range;
+ addonInterface->toKodi->kodi_gui->control_spin->set_int_value = set_int_value;
+ addonInterface->toKodi->kodi_gui->control_spin->get_int_value = get_int_value;
+
+ addonInterface->toKodi->kodi_gui->control_spin->set_float_range = set_float_range;
+ addonInterface->toKodi->kodi_gui->control_spin->set_float_value = set_float_value;
+ addonInterface->toKodi->kodi_gui->control_spin->get_float_value = get_float_value;
+ addonInterface->toKodi->kodi_gui->control_spin->set_float_interval = set_float_interval;
+}
+
+void Interface_GUIControlSpin::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_spin;
+}
+
+void Interface_GUIControlSpin::set_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetVisible(visible);
+}
+
+void Interface_GUIControlSpin::set_enabled(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool enabled)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetEnabled(enabled);
+}
+
+void Interface_GUIControlSpin::set_text(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* text)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control || !text)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "text='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(text),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_SET, control->GetParentID(), control->GetID());
+ msg.SetLabel(text);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, control->GetParentID());
+}
+
+void Interface_GUIControlSpin::reset(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, control->GetParentID(), control->GetID());
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, control->GetParentID());
+}
+
+void Interface_GUIControlSpin::set_type(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int type)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetType(type);
+}
+
+void Interface_GUIControlSpin::add_string_label(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label,
+ const char* value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control || !label || !value)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "label='{}', value='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(label),
+ static_cast<const void*>(value), addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->AddLabel(std::string(label), std::string(value));
+}
+
+void Interface_GUIControlSpin::set_string_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control || !value)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "value='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(value),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetStringValue(std::string(value));
+}
+
+char* Interface_GUIControlSpin::get_string_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ return strdup(control->GetStringValue().c_str());
+}
+
+void Interface_GUIControlSpin::add_int_label(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label,
+ int value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control || !label)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}', "
+ "label='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(label),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->AddLabel(std::string(label), value);
+}
+
+void Interface_GUIControlSpin::set_int_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int start,
+ int end)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetRange(start, end);
+}
+
+void Interface_GUIControlSpin::set_int_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetValue(value);
+}
+
+int Interface_GUIControlSpin::get_int_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return -1;
+ }
+
+ return control->GetValue();
+}
+
+void Interface_GUIControlSpin::set_float_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float start,
+ float end)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetFloatRange(start, end);
+}
+
+void Interface_GUIControlSpin::set_float_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float value)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetFloatValue(value);
+}
+
+float Interface_GUIControlSpin::get_float_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return 0.0f;
+ }
+
+ return control->GetFloatValue();
+}
+
+void Interface_GUIControlSpin::set_float_interval(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float interval)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUISpinControlEx* control = static_cast<CGUISpinControlEx*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlSpin::{} - invalid handler data (kodiBase='{}', handle='{}') "
+ "on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetFloatInterval(interval);
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/Spin.h b/xbmc/addons/interfaces/gui/controls/Spin.h
new file mode 100644
index 0000000..1c7102e
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/Spin.h
@@ -0,0 +1,86 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/spin.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Spin.h"
+ */
+ struct Interface_GUIControlSpin
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_visible(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ static void set_enabled(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+
+ static void set_text(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* text);
+ static void reset(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ static void set_type(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int type);
+
+ static void add_string_label(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label,
+ const char* value);
+ static void add_int_label(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label,
+ int value);
+
+ static void set_string_value(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* value);
+ static char* get_string_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+
+ static void set_int_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int start,
+ int end);
+ static void set_int_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int value);
+ static int get_int_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+
+ static void set_float_range(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float start,
+ float end);
+ static void set_float_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float value);
+ static float get_float_value(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ static void set_float_interval(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float interval);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/controls/TextBox.cpp b/xbmc/addons/interfaces/gui/controls/TextBox.cpp
new file mode 100644
index 0000000..44c5f89
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/TextBox.cpp
@@ -0,0 +1,147 @@
+/*
+ * 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 "TextBox.h"
+
+#include "ServiceBroker.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/controls/TextBox.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUITextBox.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIControlTextBox::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->control_text_box =
+ new AddonToKodiFuncTable_kodi_gui_control_text_box();
+
+ addonInterface->toKodi->kodi_gui->control_text_box->set_visible = set_visible;
+ addonInterface->toKodi->kodi_gui->control_text_box->reset = reset;
+ addonInterface->toKodi->kodi_gui->control_text_box->set_text = set_text;
+ addonInterface->toKodi->kodi_gui->control_text_box->get_text = get_text;
+ addonInterface->toKodi->kodi_gui->control_text_box->scroll = scroll;
+ addonInterface->toKodi->kodi_gui->control_text_box->set_auto_scrolling = set_auto_scrolling;
+}
+
+void Interface_GUIControlTextBox::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->control_text_box;
+}
+
+void Interface_GUIControlTextBox::set_visible(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ bool visible)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUITextBox* control = static_cast<CGUITextBox*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlTextBox::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetVisible(visible);
+}
+
+void Interface_GUIControlTextBox::reset(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUITextBox* control = static_cast<CGUITextBox*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlTextBox::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, control->GetParentID(), control->GetID());
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, control->GetParentID());
+}
+
+void Interface_GUIControlTextBox::set_text(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* text)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUITextBox* control = static_cast<CGUITextBox*>(handle);
+ if (!addon || !control || !text)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlTextBox::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}', text='{}') on addon '{}'",
+ __func__, kodiBase, handle, static_cast<const void*>(text),
+ addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_SET, control->GetParentID(), control->GetID());
+ msg.SetLabel(text);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, control->GetParentID());
+}
+
+char* Interface_GUIControlTextBox::get_text(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUITextBox* control = static_cast<CGUITextBox*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlTextBox::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return nullptr;
+ }
+
+ return strdup(control->GetDescription().c_str());
+}
+
+void Interface_GUIControlTextBox::scroll(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ unsigned int position)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUITextBox* control = static_cast<CGUITextBox*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlTextBox::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->Scroll(position);
+}
+
+void Interface_GUIControlTextBox::set_auto_scrolling(
+ KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int delay, int time, int repeat)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ CGUITextBox* control = static_cast<CGUITextBox*>(handle);
+ if (!addon || !control)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIControlTextBox::{} - invalid handler data (kodiBase='{}', "
+ "handle='{}') on addon '{}'",
+ __func__, kodiBase, handle, addon ? addon->ID() : "unknown");
+ return;
+ }
+
+ control->SetAutoScrolling(delay, time, repeat);
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/controls/TextBox.h b/xbmc/addons/interfaces/gui/controls/TextBox.h
new file mode 100644
index 0000000..09c2ff4
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/controls/TextBox.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 "addons/kodi-dev-kit/include/kodi/c-api/gui/controls/text_box.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold general gui functions and initialize also all other gui related types not
+ * related to a instance type and usable for every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/TextBox.h"
+ */
+ struct Interface_GUIControlTextBox
+ {
+
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void set_visible(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ static void reset(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ static void set_text(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* text);
+ static char* get_text(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ static void scroll(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, unsigned int position);
+ static void set_auto_scrolling(
+ KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int delay, int time, int repeat);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/dialogs/CMakeLists.txt b/xbmc/addons/interfaces/gui/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..4dad4e9
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/CMakeLists.txt
@@ -0,0 +1,23 @@
+set(SOURCES ContextMenu.cpp
+ ExtendedProgressBar.cpp
+ FileBrowser.cpp
+ Keyboard.cpp
+ Numeric.cpp
+ OK.cpp
+ Progress.cpp
+ Select.cpp
+ TextViewer.cpp
+ YesNo.cpp)
+
+set(HEADERS ContextMenu.h
+ ExtendedProgressBar.h
+ FileBrowser.h
+ Keyboard.h
+ Numeric.h
+ OK.h
+ Progress.h
+ Select.h
+ TextViewer.h
+ YesNo.h)
+
+core_add_library(addons_interfaces_gui_dialogs)
diff --git a/xbmc/addons/interfaces/gui/dialogs/ContextMenu.cpp b/xbmc/addons/interfaces/gui/dialogs/ContextMenu.cpp
new file mode 100644
index 0000000..e0e5684
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/ContextMenu.cpp
@@ -0,0 +1,67 @@
+/*
+ * 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 "ContextMenu.h"
+
+#include "ServiceBroker.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/dialogs/ContextMenu.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIDialogContextMenu::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->dialogContextMenu =
+ new AddonToKodiFuncTable_kodi_gui_dialogContextMenu();
+
+ addonInterface->toKodi->kodi_gui->dialogContextMenu->open = open;
+}
+
+void Interface_GUIDialogContextMenu::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->dialogContextMenu;
+}
+
+int Interface_GUIDialogContextMenu::open(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* entries[],
+ unsigned int size)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogContextMenu::{} - invalid data", __func__);
+ return -1;
+ }
+
+ CGUIDialogContextMenu* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogContextMenu>(
+ WINDOW_DIALOG_CONTEXT_MENU);
+ if (!heading || !entries || !dialog)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogContextMenu::{} - invalid handler data (heading='{}', "
+ "entries='{}', dialog='{}') on addon '{}'",
+ __func__, static_cast<const void*>(heading), static_cast<const void*>(entries),
+ kodiBase, addon->ID());
+ return -1;
+ }
+
+ CContextButtons choices;
+ for (unsigned int i = 0; i < size; ++i)
+ choices.Add(i, entries[i]);
+
+ return dialog->Show(choices);
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/dialogs/ContextMenu.h b/xbmc/addons/interfaces/gui/dialogs/ContextMenu.h
new file mode 100644
index 0000000..7b41a28
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/ContextMenu.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/context_menu.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/ContextMenu.h"
+ */
+ struct Interface_GUIDialogContextMenu
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static int open(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* entries[],
+ unsigned int size);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/dialogs/ExtendedProgressBar.cpp b/xbmc/addons/interfaces/gui/dialogs/ExtendedProgressBar.cpp
new file mode 100644
index 0000000..9e7f7a7
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/ExtendedProgressBar.cpp
@@ -0,0 +1,305 @@
+/*
+ * 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 "ExtendedProgressBar.h"
+
+#include "ServiceBroker.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/dialogs/ExtendedProgress.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIDialogExtendedProgress::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress =
+ new AddonToKodiFuncTable_kodi_gui_dialogExtendedProgress();
+
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress->new_dialog = new_dialog;
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress->delete_dialog = delete_dialog;
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress->get_title = get_title;
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress->set_title = set_title;
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress->get_text = get_text;
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress->set_text = set_text;
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress->is_finished = is_finished;
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress->mark_finished = mark_finished;
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress->get_percentage = get_percentage;
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress->set_percentage = set_percentage;
+ addonInterface->toKodi->kodi_gui->dialogExtendedProgress->set_progress = set_progress;
+}
+
+void Interface_GUIDialogExtendedProgress::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->dialogExtendedProgress;
+}
+
+KODI_GUI_HANDLE Interface_GUIDialogExtendedProgress::new_dialog(KODI_HANDLE kodiBase,
+ const char* title)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogExtendedProgress::{} - invalid kodi base data",
+ __func__);
+ return nullptr;
+ }
+
+ // setup the progress dialog
+ CGUIDialogExtendedProgressBar* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(
+ WINDOW_DIALOG_EXT_PROGRESS);
+ if (!title || !dialog)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogExtendedProgress::{} - invalid handler data (title='{}', "
+ "dialog='{}') on addon '{}'",
+ __func__, static_cast<const void*>(title), static_cast<void*>(dialog), addon->ID());
+ return nullptr;
+ }
+
+ CGUIDialogProgressBarHandle* dlgProgressHandle = dialog->GetHandle(title);
+ return dlgProgressHandle;
+}
+
+void Interface_GUIDialogExtendedProgress::delete_dialog(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogExtendedProgress::{} - invalid kodi base data",
+ __func__);
+ return;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogExtendedProgress::{} - invalid handler data (handle='{}') on "
+ "addon '{}'",
+ __func__, handle, addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgressBarHandle*>(handle)->MarkFinished();
+}
+
+char* Interface_GUIDialogExtendedProgress::get_title(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogExtendedProgress::{} - invalid kodi base data",
+ __func__);
+ return nullptr;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogExtendedProgress::{} - invalid handler data (handle='{}') on "
+ "addon '{}'",
+ __func__, handle, addon->ID());
+ return nullptr;
+ }
+
+ return strdup(static_cast<CGUIDialogProgressBarHandle*>(handle)->Title().c_str());
+}
+
+void Interface_GUIDialogExtendedProgress::set_title(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ const char* title)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogExtendedProgress::{} - invalid kodi base data",
+ __func__);
+ return;
+ }
+
+ if (!handle || !title)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogExtendedProgress::{} - invalid handler data (handle='{}', "
+ "title='{}') on addon '{}'",
+ __func__, handle, static_cast<const void*>(title), addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgressBarHandle*>(handle)->SetTitle(title);
+}
+
+char* Interface_GUIDialogExtendedProgress::get_text(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogExtendedProgress::{} - invalid kodi base data",
+ __func__);
+ return nullptr;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogExtendedProgress::{} - invalid add-on data (handle='{}') on "
+ "addon '{}'",
+ __func__, handle, addon->ID());
+ return nullptr;
+ }
+
+ return strdup(static_cast<CGUIDialogProgressBarHandle*>(handle)->Text().c_str());
+}
+
+void Interface_GUIDialogExtendedProgress::set_text(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ const char* text)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogExtendedProgress::{} - invalid kodi base data",
+ __func__);
+ return;
+ }
+
+ if (!handle || !text)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogExtendedProgress::{} - invalid handler data (handle='{}', "
+ "text='{}') on addon '{}'",
+ __func__, handle, static_cast<const void*>(text), addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgressBarHandle*>(handle)->SetText(text);
+}
+
+bool Interface_GUIDialogExtendedProgress::is_finished(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogExtendedProgress::{} - invalid kodi base data",
+ __func__);
+ return false;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogExtendedProgress::{} - invalid add-on data (handle='{}') on "
+ "addon '{}'",
+ __func__, handle, addon->ID());
+ return false;
+ }
+
+ return static_cast<CGUIDialogProgressBarHandle*>(handle)->IsFinished();
+}
+
+void Interface_GUIDialogExtendedProgress::mark_finished(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogExtendedProgress::{} - invalid kodi base data",
+ __func__);
+ return;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogExtendedProgress::{} - invalid add-on data (handle='{}') on "
+ "addon '{}'",
+ __func__, handle, addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgressBarHandle*>(handle)->MarkFinished();
+}
+
+float Interface_GUIDialogExtendedProgress::get_percentage(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogExtendedProgress::{} - invalid kodi base data",
+ __func__);
+ return 0.0f;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogExtendedProgress::{} - invalid add-on data (handle='{}') on "
+ "addon '{}'",
+ __func__, handle, addon->ID());
+ return 0.0f;
+ }
+
+ return static_cast<CGUIDialogProgressBarHandle*>(handle)->Percentage();
+}
+
+void Interface_GUIDialogExtendedProgress::set_percentage(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ float percentage)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogExtendedProgress::{} - invalid kodi base data",
+ __func__);
+ return;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogExtendedProgress::{} - invalid add-on data (handle='{}') on "
+ "addon '{}'",
+ __func__, handle, addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgressBarHandle*>(handle)->SetPercentage(percentage);
+}
+
+void Interface_GUIDialogExtendedProgress::set_progress(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ int currentItem,
+ int itemCount)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogExtendedProgress::{} - invalid kodi base data",
+ __func__);
+ return;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogExtendedProgress::{} - invalid add-on data (handle='{}') on "
+ "addon '{}'",
+ __func__, handle, addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgressBarHandle*>(handle)->SetProgress(currentItem, itemCount);
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/dialogs/ExtendedProgressBar.h b/xbmc/addons/interfaces/gui/dialogs/ExtendedProgressBar.h
new file mode 100644
index 0000000..66ccb49
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/ExtendedProgressBar.h
@@ -0,0 +1,63 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/extended_progress.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/ExtendedProgress.h"
+ */
+ struct Interface_GUIDialogExtendedProgress
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static KODI_GUI_HANDLE new_dialog(KODI_HANDLE kodiBase, const char* title);
+ static void delete_dialog(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ static char* get_title(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ static void set_title(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, const char* title);
+ static char* get_text(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ static void set_text(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, const char* text);
+ static bool is_finished(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ static void mark_finished(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ static float get_percentage(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ static void set_percentage(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, float percentage);
+ static void set_progress(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ int currentItem,
+ int itemCount);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/dialogs/FileBrowser.cpp b/xbmc/addons/interfaces/gui/dialogs/FileBrowser.cpp
new file mode 100644
index 0000000..84d6443
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/FileBrowser.cpp
@@ -0,0 +1,403 @@
+/*
+ * 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 "FileBrowser.h"
+
+#include "URL.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/dialogs/FileBrowser.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "settings/MediaSourceSettings.h"
+#include "storage/MediaManager.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIDialogFileBrowser::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->dialogFileBrowser =
+ new AddonToKodiFuncTable_kodi_gui_dialogFileBrowser();
+
+ addonInterface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_directory =
+ show_and_get_directory;
+ addonInterface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_file = show_and_get_file;
+ addonInterface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_file_from_dir =
+ show_and_get_file_from_dir;
+ addonInterface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_file_list =
+ show_and_get_file_list;
+ addonInterface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_source = show_and_get_source;
+ addonInterface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_image = show_and_get_image;
+ addonInterface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_image_list =
+ show_and_get_image_list;
+ addonInterface->toKodi->kodi_gui->dialogFileBrowser->clear_file_list = clear_file_list;
+}
+
+void Interface_GUIDialogFileBrowser::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->dialogFileBrowser;
+}
+
+bool Interface_GUIDialogFileBrowser::show_and_get_directory(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* heading,
+ const char* path_in,
+ char** path_out,
+ bool write_only)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogFileBrowser::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!shares || !heading || !path_in || !path_out)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogFileBrowser::{} - invalid handler data (shares='{}', "
+ "heading='{}', path_in='{}', path_out='{}') on addon '{}'",
+ __func__, static_cast<const void*>(shares), static_cast<const void*>(heading),
+ static_cast<const void*>(path_in), static_cast<void*>(path_out), addon->ID());
+ return false;
+ }
+
+ std::string strPath = path_in;
+
+ VECSOURCES vecShares;
+ GetVECShares(vecShares, shares, strPath);
+ bool bRet = CGUIDialogFileBrowser::ShowAndGetDirectory(vecShares, heading, strPath, write_only);
+ if (bRet)
+ *path_out = strdup(strPath.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogFileBrowser::show_and_get_file(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* mask,
+ const char* heading,
+ const char* path_in,
+ char** path_out,
+ bool use_thumbs,
+ bool use_file_directories)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogFileBrowser::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!shares || !mask || !heading || !path_in || !path_out)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogFileBrowser::{} - invalid handler data (shares='{}', mask='{}', "
+ "heading='{}', path_in='{}', path_out='{}') on addon '{}'",
+ __func__, static_cast<const void*>(shares), static_cast<const void*>(mask),
+ static_cast<const void*>(heading), static_cast<const void*>(path_in),
+ static_cast<void*>(path_out), addon->ID());
+ return false;
+ }
+
+ std::string strPath = path_in;
+
+ VECSOURCES vecShares;
+ GetVECShares(vecShares, shares, strPath);
+ bool bRet = CGUIDialogFileBrowser::ShowAndGetFile(vecShares, mask, heading, strPath, use_thumbs,
+ use_file_directories);
+ if (bRet)
+ *path_out = strdup(strPath.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogFileBrowser::show_and_get_file_from_dir(KODI_HANDLE kodiBase,
+ const char* directory,
+ const char* mask,
+ const char* heading,
+ const char* path_in,
+ char** path_out,
+ bool use_thumbs,
+ bool use_file_directories,
+ bool single_list)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogFileBrowser::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!directory || !mask || !heading || !path_in || !path_out)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogFileBrowser::{} - invalid handler data (directory='{}', "
+ "mask='{}', heading='{}', path_in='{}', path_out='{}') on addon '{}'",
+ __func__, static_cast<const void*>(directory), static_cast<const void*>(mask),
+ static_cast<const void*>(heading), static_cast<const void*>(path_in),
+ static_cast<void*>(path_out), addon->ID());
+ return false;
+ }
+
+ std::string strPath = path_in;
+ bool bRet = CGUIDialogFileBrowser::ShowAndGetFile(directory, mask, heading, strPath, use_thumbs,
+ use_file_directories, single_list);
+ if (bRet)
+ *path_out = strdup(strPath.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogFileBrowser::show_and_get_file_list(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* mask,
+ const char* heading,
+ char*** file_list,
+ unsigned int* entries,
+ bool use_thumbs,
+ bool use_file_directories)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogFileBrowser::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!shares || !mask || !heading || !file_list || !entries)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogFileBrowser::{} - invalid handler data (shares='{}', mask='{}', "
+ "heading='{}', file_list='{}', entries='{}') on addon '{}'",
+ __func__, static_cast<const void*>(shares), static_cast<const void*>(mask),
+ static_cast<const void*>(heading), static_cast<void*>(file_list),
+ static_cast<void*>(entries), addon->ID());
+ return false;
+ }
+
+ VECSOURCES vecShares;
+ GetVECShares(vecShares, shares, "");
+
+ std::vector<std::string> pathsInt;
+ bool bRet = CGUIDialogFileBrowser::ShowAndGetFileList(vecShares, mask, heading, pathsInt,
+ use_thumbs, use_file_directories);
+ if (bRet)
+ {
+ *entries = pathsInt.size();
+ *file_list = static_cast<char**>(malloc(*entries * sizeof(char*)));
+ for (unsigned int i = 0; i < *entries; ++i)
+ (*file_list)[i] = strdup(pathsInt[i].c_str());
+ }
+ else
+ *entries = 0;
+ return bRet;
+}
+
+bool Interface_GUIDialogFileBrowser::show_and_get_source(KODI_HANDLE kodiBase,
+ const char* path_in,
+ char** path_out,
+ bool allowNetworkShares,
+ const char* additionalShare,
+ const char* strType)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogFileBrowser::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!strType || !additionalShare || !path_in || !path_out)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogFileBrowser::{} - invalid handler data (additionalShare='{}', "
+ "strType='{}', path_in='{}', path_out='{}') on addon '{}'",
+ __func__, static_cast<const void*>(additionalShare),
+ static_cast<const void*>(strType), static_cast<const void*>(path_in),
+ static_cast<void*>(path_out), addon->ID());
+ return false;
+ }
+
+ std::string strPath = path_in;
+
+ VECSOURCES vecShares;
+ if (additionalShare)
+ GetVECShares(vecShares, additionalShare, strPath);
+ bool bRet =
+ CGUIDialogFileBrowser::ShowAndGetSource(strPath, allowNetworkShares, &vecShares, strType);
+ if (bRet)
+ *path_out = strdup(strPath.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogFileBrowser::show_and_get_image(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* heading,
+ const char* path_in,
+ char** path_out)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogFileBrowser::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!shares || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogFileBrowser::{} - invalid handler data (shares='{}', "
+ "heading='{}') on addon '{}'",
+ __func__, static_cast<const void*>(shares), static_cast<const void*>(heading),
+ addon->ID());
+ return false;
+ }
+
+ std::string strPath = path_in;
+
+ VECSOURCES vecShares;
+ GetVECShares(vecShares, shares, strPath);
+ bool bRet = CGUIDialogFileBrowser::ShowAndGetImage(vecShares, heading, strPath);
+ if (bRet)
+ *path_out = strdup(strPath.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogFileBrowser::show_and_get_image_list(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* heading,
+ char*** file_list,
+ unsigned int* entries)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogFileBrowser::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!shares || !heading || !file_list || !entries)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogFileBrowser::{} - invalid handler data (shares='{}', "
+ "heading='{}', file_list='{}', entries='{}') on addon '{}'",
+ __func__, static_cast<const void*>(shares), static_cast<const void*>(heading),
+ static_cast<void*>(file_list), static_cast<void*>(entries), addon->ID());
+ return false;
+ }
+
+ VECSOURCES vecShares;
+ GetVECShares(vecShares, shares, "");
+
+ std::vector<std::string> pathsInt;
+ bool bRet = CGUIDialogFileBrowser::ShowAndGetImageList(vecShares, heading, pathsInt);
+ if (bRet)
+ {
+ *entries = pathsInt.size();
+ *file_list = static_cast<char**>(malloc(*entries * sizeof(char*)));
+ for (unsigned int i = 0; i < *entries; ++i)
+ (*file_list)[i] = strdup(pathsInt[i].c_str());
+ }
+ else
+ *entries = 0;
+ return bRet;
+}
+
+void Interface_GUIDialogFileBrowser::clear_file_list(KODI_HANDLE kodiBase,
+ char*** file_list,
+ unsigned int entries)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogFileBrowser::{} - invalid data", __func__);
+ return;
+ }
+
+ if (*file_list)
+ {
+ for (unsigned int i = 0; i < entries; ++i)
+ free((*file_list)[i]);
+ free(*file_list);
+ *file_list = nullptr;
+ }
+ else
+ {
+
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogFileBrowser::{} - invalid handler data (file_list='{}') on "
+ "addon '{}'",
+ __func__, static_cast<void*>(file_list), addon->ID());
+ }
+}
+
+void Interface_GUIDialogFileBrowser::GetVECShares(VECSOURCES& vecShares,
+ const std::string& strShares,
+ const std::string& strPath)
+{
+ std::size_t found;
+ found = strShares.find("local");
+ if (found != std::string::npos)
+ CServiceBroker::GetMediaManager().GetLocalDrives(vecShares);
+ found = strShares.find("network");
+ if (found != std::string::npos)
+ CServiceBroker::GetMediaManager().GetNetworkLocations(vecShares);
+ found = strShares.find("removable");
+ if (found != std::string::npos)
+ CServiceBroker::GetMediaManager().GetRemovableDrives(vecShares);
+ found = strShares.find("programs");
+ if (found != std::string::npos)
+ {
+ VECSOURCES* sources = CMediaSourceSettings::GetInstance().GetSources("programs");
+ if (sources != nullptr)
+ vecShares.insert(vecShares.end(), sources->begin(), sources->end());
+ }
+ found = strShares.find("files");
+ if (found != std::string::npos)
+ {
+ VECSOURCES* sources = CMediaSourceSettings::GetInstance().GetSources("files");
+ if (sources != nullptr)
+ vecShares.insert(vecShares.end(), sources->begin(), sources->end());
+ }
+ found = strShares.find("music");
+ if (found != std::string::npos)
+ {
+ VECSOURCES* sources = CMediaSourceSettings::GetInstance().GetSources("music");
+ if (sources != nullptr)
+ vecShares.insert(vecShares.end(), sources->begin(), sources->end());
+ }
+ found = strShares.find("video");
+ if (found != std::string::npos)
+ {
+ VECSOURCES* sources = CMediaSourceSettings::GetInstance().GetSources("video");
+ if (sources != nullptr)
+ vecShares.insert(vecShares.end(), sources->begin(), sources->end());
+ }
+ found = strShares.find("pictures");
+ if (found != std::string::npos)
+ {
+ VECSOURCES* sources = CMediaSourceSettings::GetInstance().GetSources("pictures");
+ if (sources != nullptr)
+ vecShares.insert(vecShares.end(), sources->begin(), sources->end());
+ }
+
+ if (vecShares.empty())
+ {
+ CMediaSource share;
+ std::string basePath = strPath;
+ std::string tempPath;
+ while (URIUtils::GetParentPath(basePath, tempPath))
+ basePath = tempPath;
+ share.strPath = basePath;
+ // don't include the user details in the share name
+ CURL url(share.strPath);
+ share.strName = url.GetWithoutUserDetails();
+ vecShares.push_back(share);
+ }
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/dialogs/FileBrowser.h b/xbmc/addons/interfaces/gui/dialogs/FileBrowser.h
new file mode 100644
index 0000000..c2193f3
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/FileBrowser.h
@@ -0,0 +1,116 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/filebrowser.h"
+
+#include <string>
+#include <vector>
+
+class CMediaSource;
+
+typedef std::vector<CMediaSource> VECSOURCES;
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/FileBrowser.h"
+ */
+ struct Interface_GUIDialogFileBrowser
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static bool show_and_get_directory(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* heading,
+ const char* path_in,
+ char** path_out,
+ bool write_only);
+
+ static bool show_and_get_file(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* mask,
+ const char* heading,
+ const char* path_in,
+ char** path_out,
+ bool use_thumbs,
+ bool use_file_directories);
+
+ static bool show_and_get_file_from_dir(KODI_HANDLE kodiBase,
+ const char* directory,
+ const char* mask,
+ const char* heading,
+ const char* path_in,
+ char** path_out,
+ bool use_thumbs,
+ bool use_file_directories,
+ bool singleList);
+
+ static bool show_and_get_file_list(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* mask,
+ const char* heading,
+ char*** file_list,
+ unsigned int* entries,
+ bool use_thumbs,
+ bool use_file_directories);
+
+ static bool show_and_get_source(KODI_HANDLE kodiBase,
+ const char* path_in,
+ char** path_out,
+ bool allow_network_shares,
+ const char* additional_share,
+ const char* type);
+
+ static bool show_and_get_image(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* heading,
+ const char* path_in,
+ char** path_out);
+
+ static bool show_and_get_image_list(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* heading,
+ char*** file_list,
+ unsigned int* entries);
+
+ static void clear_file_list(KODI_HANDLE kodiBase, char*** file_list, unsigned int entries);
+ //@}
+
+ private:
+ static void GetVECShares(VECSOURCES& vecShares,
+ const std::string& strShares,
+ const std::string& strPath);
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/dialogs/Keyboard.cpp b/xbmc/addons/interfaces/gui/dialogs/Keyboard.cpp
new file mode 100644
index 0000000..7ec56ab
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/Keyboard.cpp
@@ -0,0 +1,319 @@
+/*
+ * 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 "Keyboard.h"
+
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/dialogs/Keyboard.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIDialogKeyboard::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->dialogKeyboard =
+ new AddonToKodiFuncTable_kodi_gui_dialogKeyboard();
+
+ addonInterface->toKodi->kodi_gui->dialogKeyboard->show_and_get_input_with_head =
+ show_and_get_input_with_head;
+ addonInterface->toKodi->kodi_gui->dialogKeyboard->show_and_get_input = show_and_get_input;
+ addonInterface->toKodi->kodi_gui->dialogKeyboard->show_and_get_new_password_with_head =
+ show_and_get_new_password_with_head;
+ addonInterface->toKodi->kodi_gui->dialogKeyboard->show_and_get_new_password =
+ show_and_get_new_password;
+ addonInterface->toKodi->kodi_gui->dialogKeyboard->show_and_verify_new_password_with_head =
+ show_and_verify_new_password_with_head;
+ addonInterface->toKodi->kodi_gui->dialogKeyboard->show_and_verify_new_password =
+ show_and_verify_new_password;
+ addonInterface->toKodi->kodi_gui->dialogKeyboard->show_and_verify_password =
+ show_and_verify_password;
+ addonInterface->toKodi->kodi_gui->dialogKeyboard->show_and_get_filter = show_and_get_filter;
+ addonInterface->toKodi->kodi_gui->dialogKeyboard->send_text_to_active_keyboard =
+ send_text_to_active_keyboard;
+ addonInterface->toKodi->kodi_gui->dialogKeyboard->is_keyboard_activated = is_keyboard_activated;
+}
+
+void Interface_GUIDialogKeyboard::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->dialogKeyboard;
+}
+
+bool Interface_GUIDialogKeyboard::show_and_get_input_with_head(KODI_HANDLE kodiBase,
+ const char* text_in,
+ char** text_out,
+ const char* heading,
+ bool allow_empty_result,
+ bool hidden_input,
+ unsigned int auto_close_ms)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogKeyboard::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!text_in || !text_out || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogKeyboard::{} - invalid handler data (text_in='{}', "
+ "text_out='{}', heading='{}') on addon '{}'",
+ __func__, static_cast<const void*>(text_in), static_cast<void*>(text_out),
+ static_cast<const void*>(heading), addon->ID());
+ return false;
+ }
+
+ std::string str = text_in;
+ bool bRet = CGUIKeyboardFactory::ShowAndGetInput(str, CVariant{heading}, allow_empty_result,
+ hidden_input, auto_close_ms);
+ if (bRet)
+ *text_out = strdup(str.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogKeyboard::show_and_get_input(KODI_HANDLE kodiBase,
+ const char* text_in,
+ char** text_out,
+ bool allow_empty_result,
+ unsigned int auto_close_ms)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogKeyboard::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!text_in || !text_out)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogKeyboard::{} - invalid handler data (text_in='{}', "
+ "text_out='{}') on addon '{}'",
+ __func__, static_cast<const void*>(text_in), static_cast<void*>(text_out),
+ addon->ID());
+ return false;
+ }
+
+ std::string str = text_in;
+ bool bRet = CGUIKeyboardFactory::ShowAndGetInput(str, allow_empty_result, auto_close_ms);
+ if (bRet)
+ *text_out = strdup(str.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogKeyboard::show_and_get_new_password_with_head(KODI_HANDLE kodiBase,
+ const char* password_in,
+ char** password_out,
+ const char* heading,
+ bool allow_empty_result,
+ unsigned int auto_close_ms)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogKeyboard::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!password_in || !password_out || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogKeyboard::{} - invalid handler data (password_in='{}', "
+ "password_out='{}', heading='{}') on addon '{}'",
+ __func__, static_cast<const void*>(password_in), static_cast<void*>(password_out),
+ static_cast<const void*>(heading), addon->ID());
+ return false;
+ }
+
+ std::string str = password_in;
+ bool bRet =
+ CGUIKeyboardFactory::ShowAndGetNewPassword(str, heading, allow_empty_result, auto_close_ms);
+ if (bRet)
+ *password_out = strdup(str.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogKeyboard::show_and_get_new_password(KODI_HANDLE kodiBase,
+ const char* password_in,
+ char** password_out,
+ unsigned int auto_close_ms)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogKeyboard::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!password_in || !password_out)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogKeyboard::{} - invalid handler data (password_in='{}', "
+ "password_out='{}') on addon '{}'",
+ __func__, static_cast<const void*>(password_in), static_cast<void*>(password_out),
+ addon->ID());
+ return false;
+ }
+
+ std::string str = password_in;
+ bool bRet = CGUIKeyboardFactory::ShowAndGetNewPassword(str, auto_close_ms);
+ if (bRet)
+ *password_out = strdup(str.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogKeyboard::show_and_verify_new_password_with_head(KODI_HANDLE kodiBase,
+ char** password_out,
+ const char* heading,
+ bool allowEmpty,
+ unsigned int auto_close_ms)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogKeyboard::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!password_out || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogKeyboard::{} - invalid handler data (password_out='{}', "
+ "heading='{}') on addon '{}'",
+ __func__, static_cast<void*>(password_out), static_cast<const void*>(heading),
+ addon->ID());
+ return false;
+ }
+
+ std::string str;
+ bool bRet =
+ CGUIKeyboardFactory::ShowAndVerifyNewPassword(str, heading, allowEmpty, auto_close_ms);
+ if (bRet)
+ *password_out = strdup(str.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogKeyboard::show_and_verify_new_password(KODI_HANDLE kodiBase,
+ char** password_out,
+ unsigned int auto_close_ms)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogKeyboard::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!password_out)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogKeyboard::{} - invalid handler data (password_out='{}') on "
+ "addon '{}'",
+ __func__, static_cast<void*>(password_out), addon->ID());
+ return false;
+ }
+
+ std::string str;
+ bool bRet = CGUIKeyboardFactory::ShowAndVerifyNewPassword(str, auto_close_ms);
+ if (bRet)
+ *password_out = strdup(str.c_str());
+ return bRet;
+}
+
+int Interface_GUIDialogKeyboard::show_and_verify_password(KODI_HANDLE kodiBase,
+ const char* password_in,
+ char** password_out,
+ const char* heading,
+ int retries,
+ unsigned int auto_close_ms)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogKeyboard::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!password_in || !password_out || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogKeyboard::{} - invalid handler data (password_in='{}', "
+ "password_out='{}', heading='{}') on addon '{}'",
+ __func__, static_cast<const void*>(password_in), static_cast<void*>(password_out),
+ static_cast<const void*>(heading), addon->ID());
+ return false;
+ }
+
+ std::string str = password_in;
+ int iRet = CGUIKeyboardFactory::ShowAndVerifyPassword(str, heading, retries, auto_close_ms);
+ if (iRet)
+ *password_out = strdup(str.c_str());
+ return iRet;
+}
+
+bool Interface_GUIDialogKeyboard::show_and_get_filter(KODI_HANDLE kodiBase,
+ const char* text_in,
+ char** text_out,
+ bool searching,
+ unsigned int auto_close_ms)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogKeyboard::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!text_in || !text_out)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogKeyboard::{} - invalid handler data (text_in='{}', "
+ "text_out='{}') on addon '{}'",
+ __func__, static_cast<const void*>(text_in), static_cast<void*>(text_out),
+ addon->ID());
+ return false;
+ }
+
+
+ std::string str = text_in;
+ bool bRet = CGUIKeyboardFactory::ShowAndGetFilter(str, searching, auto_close_ms);
+ if (bRet)
+ *text_out = strdup(str.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogKeyboard::send_text_to_active_keyboard(KODI_HANDLE kodiBase,
+ const char* text,
+ bool close_keyboard)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogKeyboard::{} - invalid data", __func__);
+ return false;
+ }
+
+ return CGUIKeyboardFactory::SendTextToActiveKeyboard(text, close_keyboard);
+}
+
+bool Interface_GUIDialogKeyboard::is_keyboard_activated(KODI_HANDLE kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogKeyboard::{} - invalid data", __func__);
+ return false;
+ }
+
+ return CGUIKeyboardFactory::isKeyboardActivated();
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/dialogs/Keyboard.h b/xbmc/addons/interfaces/gui/dialogs/Keyboard.h
new file mode 100644
index 0000000..fb7d6b9
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/Keyboard.h
@@ -0,0 +1,94 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/keyboard.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Keyboard.h"
+ */
+ struct Interface_GUIDialogKeyboard
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static bool show_and_get_input_with_head(KODI_HANDLE kodiBase,
+ const char* text_in,
+ char** text_out,
+ const char* heading,
+ bool allow_empty_result,
+ bool hidden_input,
+ unsigned int auto_close_ms);
+ static bool show_and_get_input(KODI_HANDLE kodiBase,
+ const char* text_in,
+ char** text_out,
+ bool allow_empty_result,
+ unsigned int auto_close_ms);
+ static bool show_and_get_new_password_with_head(KODI_HANDLE kodiBase,
+ const char* password_in,
+ char** password_out,
+ const char* heading,
+ bool allow_empty_result,
+ unsigned int auto_close_ms);
+ static bool show_and_get_new_password(KODI_HANDLE kodiBase,
+ const char* password_in,
+ char** password_out,
+ unsigned int auto_close_ms);
+ static bool show_and_verify_new_password_with_head(KODI_HANDLE kodiBase,
+ char** password_out,
+ const char* heading,
+ bool allowEmpty,
+ unsigned int auto_close_ms);
+ static bool show_and_verify_new_password(KODI_HANDLE kodiBase,
+ char** password_out,
+ unsigned int auto_close_ms);
+ static int show_and_verify_password(KODI_HANDLE kodiBase,
+ const char* password_in,
+ char** password_out,
+ const char* heading,
+ int retries,
+ unsigned int auto_close_ms);
+ static bool show_and_get_filter(KODI_HANDLE kodiBase,
+ const char* text_in,
+ char** text_out,
+ bool searching,
+ unsigned int auto_close_ms);
+ static bool send_text_to_active_keyboard(KODI_HANDLE kodiBase,
+ const char* text,
+ bool close_keyboard);
+ static bool is_keyboard_activated(KODI_HANDLE kodiBase);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/dialogs/Numeric.cpp b/xbmc/addons/interfaces/gui/dialogs/Numeric.cpp
new file mode 100644
index 0000000..a78aa4a
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/Numeric.cpp
@@ -0,0 +1,270 @@
+/*
+ * 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 "Numeric.h"
+
+#include "XBDateTime.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/dialogs/Numeric.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIDialogNumeric::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->dialogNumeric =
+ new AddonToKodiFuncTable_kodi_gui_dialogNumeric();
+
+ addonInterface->toKodi->kodi_gui->dialogNumeric->show_and_verify_new_password =
+ show_and_verify_new_password;
+ addonInterface->toKodi->kodi_gui->dialogNumeric->show_and_verify_password =
+ show_and_verify_password;
+ addonInterface->toKodi->kodi_gui->dialogNumeric->show_and_verify_input = show_and_verify_input;
+ addonInterface->toKodi->kodi_gui->dialogNumeric->show_and_get_time = show_and_get_time;
+ addonInterface->toKodi->kodi_gui->dialogNumeric->show_and_get_date = show_and_get_date;
+ addonInterface->toKodi->kodi_gui->dialogNumeric->show_and_get_ip_address =
+ show_and_get_ip_address;
+ addonInterface->toKodi->kodi_gui->dialogNumeric->show_and_get_number = show_and_get_number;
+ addonInterface->toKodi->kodi_gui->dialogNumeric->show_and_get_seconds = show_and_get_seconds;
+}
+
+void Interface_GUIDialogNumeric::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->dialogNumeric;
+}
+
+bool Interface_GUIDialogNumeric::show_and_verify_new_password(KODI_HANDLE kodiBase, char** password)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogNumeric::{} - invalid data", __func__);
+ return false;
+ }
+
+ std::string str;
+ bool bRet = CGUIDialogNumeric::ShowAndVerifyNewPassword(str);
+ if (bRet)
+ *password = strdup(str.c_str());
+ return bRet;
+}
+
+int Interface_GUIDialogNumeric::show_and_verify_password(KODI_HANDLE kodiBase,
+ const char* password,
+ const char* heading,
+ int retries)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogNumeric::{} - invalid data", __func__);
+ return -1;
+ }
+
+ if (!password || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogNumeric::{} - invalid handler data (password='{}', heading='{}') "
+ "on addon '{}'",
+ __func__, static_cast<const void*>(password), static_cast<const void*>(heading),
+ addon->ID());
+ return -1;
+ }
+
+ std::string pw(password);
+ return CGUIDialogNumeric::ShowAndVerifyPassword(pw, heading, retries);
+}
+
+bool Interface_GUIDialogNumeric::show_and_verify_input(KODI_HANDLE kodiBase,
+ const char* verify_in,
+ char** verify_out,
+ const char* heading,
+ bool verify_input)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogNumeric::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!verify_in || !verify_out || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogNumeric::{} - invalid handler data (verify_in='{}', "
+ "verify_out='{}', heading='{}') on addon '{}'",
+ __func__, static_cast<const void*>(verify_in), static_cast<void*>(verify_out),
+ static_cast<const void*>(heading), addon->ID());
+ return false;
+ }
+
+ std::string str = verify_in;
+ if (CGUIDialogNumeric::ShowAndVerifyInput(str, heading, verify_input) ==
+ InputVerificationResult::SUCCESS)
+ {
+ *verify_out = strdup(str.c_str());
+ return true;
+ }
+ return false;
+}
+
+bool Interface_GUIDialogNumeric::show_and_get_time(KODI_HANDLE kodiBase,
+ tm* time,
+ const char* heading)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogNumeric::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!time || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogNumeric::{} - invalid handler data (time='{}', heading='{}') on "
+ "addon '{}'",
+ __func__, static_cast<void*>(time), static_cast<const void*>(heading), addon->ID());
+ return false;
+ }
+
+ KODI::TIME::SystemTime systemTime;
+ CDateTime dateTime(*time);
+ dateTime.GetAsSystemTime(systemTime);
+ if (CGUIDialogNumeric::ShowAndGetTime(systemTime, heading))
+ {
+ dateTime = systemTime;
+ dateTime.GetAsTm(*time);
+ return true;
+ }
+ return false;
+}
+
+bool Interface_GUIDialogNumeric::show_and_get_date(KODI_HANDLE kodiBase,
+ tm* date,
+ const char* heading)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogNumeric::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!date || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogNumeric::{} - invalid handler data (date='{}', heading='{}') on "
+ "addon '{}'",
+ __func__, static_cast<void*>(date), static_cast<const void*>(heading), addon->ID());
+ return false;
+ }
+
+ KODI::TIME::SystemTime systemTime;
+ CDateTime dateTime(*date);
+ dateTime.GetAsSystemTime(systemTime);
+ if (CGUIDialogNumeric::ShowAndGetDate(systemTime, heading))
+ {
+ dateTime = systemTime;
+ dateTime.GetAsTm(*date);
+ return true;
+ }
+ return false;
+}
+
+bool Interface_GUIDialogNumeric::show_and_get_ip_address(KODI_HANDLE kodiBase,
+ const char* ip_address_in,
+ char** ip_address_out,
+ const char* heading)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogNumeric::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!ip_address_in || !ip_address_out || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogNumeric::{} - invalid handler data (ip_address_in='{}', "
+ "ip_address_out='{}', heading='{}') on addon '{}'",
+ __func__, static_cast<const void*>(ip_address_in), static_cast<void*>(ip_address_out),
+ static_cast<const void*>(heading), addon->ID());
+ return false;
+ }
+
+ std::string strIP = ip_address_in;
+ bool bRet = CGUIDialogNumeric::ShowAndGetIPAddress(strIP, heading);
+ if (bRet)
+ *ip_address_out = strdup(strIP.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogNumeric::show_and_get_number(KODI_HANDLE kodiBase,
+ const char* number_in,
+ char** number_out,
+ const char* heading,
+ unsigned int auto_close_ms)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogNumeric::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!number_in || !number_out || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogNumeric::{} - invalid handler data (number_in='{}', "
+ "number_out='{}', heading='{}') on addon '{}'",
+ __func__, static_cast<const void*>(number_in), static_cast<void*>(number_out),
+ static_cast<const void*>(heading), addon->ID());
+ return false;
+ }
+
+ std::string str = number_in;
+ bool bRet = CGUIDialogNumeric::ShowAndGetNumber(str, heading, auto_close_ms);
+ if (bRet)
+ *number_out = strdup(str.c_str());
+ return bRet;
+}
+
+bool Interface_GUIDialogNumeric::show_and_get_seconds(KODI_HANDLE kodiBase,
+ const char* time_in,
+ char** time_out,
+ const char* heading)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogNumeric::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!time_in || !time_out || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogNumeric::{} - invalid handler data (time_in='{}', time_out='{}', "
+ "heading='{}') on addon '{}'",
+ __func__, static_cast<const void*>(time_in), static_cast<void*>(time_out),
+ static_cast<const void*>(heading), addon->ID());
+ return false;
+ }
+
+ std::string str = time_in;
+ bool bRet = CGUIDialogNumeric::ShowAndGetSeconds(str, heading);
+ if (bRet)
+ *time_out = strdup(str.c_str());
+ return bRet;
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/dialogs/Numeric.h b/xbmc/addons/interfaces/gui/dialogs/Numeric.h
new file mode 100644
index 0000000..a54ad52
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/Numeric.h
@@ -0,0 +1,74 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/numeric.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Numeric.h"
+ */
+ struct Interface_GUIDialogNumeric
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static bool show_and_verify_new_password(KODI_HANDLE kodiBase, char** password);
+ static int show_and_verify_password(KODI_HANDLE kodiBase,
+ const char* password,
+ const char* heading,
+ int retries);
+ static bool show_and_verify_input(KODI_HANDLE kodiBase,
+ const char* verify_in,
+ char** verify_out,
+ const char* heading,
+ bool verify_input);
+ static bool show_and_get_time(KODI_HANDLE kodiBase, tm* time, const char* heading);
+ static bool show_and_get_date(KODI_HANDLE kodiBase, tm* date, const char* heading);
+ static bool show_and_get_ip_address(KODI_HANDLE kodiBase,
+ const char* ip_address_in,
+ char** ip_address_out,
+ const char* heading);
+ static bool show_and_get_number(KODI_HANDLE kodiBase,
+ const char* number_in,
+ char** number_out,
+ const char* heading,
+ unsigned int auto_close_ms);
+ static bool show_and_get_seconds(KODI_HANDLE kodiBase,
+ const char* time_in,
+ char** time_out,
+ const char* heading);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/dialogs/OK.cpp b/xbmc/addons/interfaces/gui/dialogs/OK.cpp
new file mode 100644
index 0000000..90e9d69
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/OK.cpp
@@ -0,0 +1,73 @@
+/*
+ * 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 "OK.h"
+
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/dialogs/OK.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace KODI::MESSAGING;
+
+namespace ADDON
+{
+
+void Interface_GUIDialogOK::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->dialogOK = new AddonToKodiFuncTable_kodi_gui_dialogOK();
+
+ addonInterface->toKodi->kodi_gui->dialogOK->show_and_get_input_single_text =
+ show_and_get_input_single_text;
+ addonInterface->toKodi->kodi_gui->dialogOK->show_and_get_input_line_text =
+ show_and_get_input_line_text;
+}
+
+void Interface_GUIDialogOK::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->dialogOK;
+}
+
+void Interface_GUIDialogOK::show_and_get_input_single_text(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* text)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon || !heading || !text)
+ {
+ CLog::Log(
+ LOGERROR, "Interface_GUIDialogOK:{} - invalid data (addon='{}', heading='{}', text='{}')",
+ __func__, kodiBase, static_cast<const void*>(heading), static_cast<const void*>(text));
+ return;
+ }
+
+ HELPERS::ShowOKDialogText(CVariant{heading}, CVariant{text});
+}
+
+void Interface_GUIDialogOK::show_and_get_input_line_text(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* line0,
+ const char* line1,
+ const char* line2)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon || !heading || !line0 || !line1 || !line2)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogOK::{} - invalid data (addon='{}', heading='{}', line0='{}', "
+ "line1='{}', line2='{}')",
+ __func__, kodiBase, static_cast<const void*>(heading),
+ static_cast<const void*>(line0), static_cast<const void*>(line1),
+ static_cast<const void*>(line2));
+ return;
+ }
+ HELPERS::ShowOKDialogLines(CVariant{heading}, CVariant{line0}, CVariant{line1}, CVariant{line2});
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/dialogs/OK.h b/xbmc/addons/interfaces/gui/dialogs/OK.h
new file mode 100644
index 0000000..fd13719
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/OK.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 "addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/ok.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/OK.h"
+ */
+ struct Interface_GUIDialogOK
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note For add of new functions use the "_" style to identify direct a
+ * add-on callback function. Everything with CamelCase is only for the
+ * usage in Kodi only.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void show_and_get_input_single_text(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* text);
+ static void show_and_get_input_line_text(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* line0,
+ const char* line1,
+ const char* line2);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/dialogs/Progress.cpp b/xbmc/addons/interfaces/gui/dialogs/Progress.cpp
new file mode 100644
index 0000000..ff6c742
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/Progress.cpp
@@ -0,0 +1,328 @@
+/*
+ * 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 "Progress.h"
+
+#include "ServiceBroker.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/dialogs/Progress.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIDialogProgress::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->dialogProgress =
+ new AddonToKodiFuncTable_kodi_gui_dialogProgress();
+
+ addonInterface->toKodi->kodi_gui->dialogProgress->new_dialog = new_dialog;
+ addonInterface->toKodi->kodi_gui->dialogProgress->delete_dialog = delete_dialog;
+ addonInterface->toKodi->kodi_gui->dialogProgress->open = open;
+ addonInterface->toKodi->kodi_gui->dialogProgress->set_heading = set_heading;
+ addonInterface->toKodi->kodi_gui->dialogProgress->set_line = set_line;
+ addonInterface->toKodi->kodi_gui->dialogProgress->set_can_cancel = set_can_cancel;
+ addonInterface->toKodi->kodi_gui->dialogProgress->is_canceled = is_canceled;
+ addonInterface->toKodi->kodi_gui->dialogProgress->set_percentage = set_percentage;
+ addonInterface->toKodi->kodi_gui->dialogProgress->get_percentage = get_percentage;
+ addonInterface->toKodi->kodi_gui->dialogProgress->show_progress_bar = show_progress_bar;
+ addonInterface->toKodi->kodi_gui->dialogProgress->set_progress_max = set_progress_max;
+ addonInterface->toKodi->kodi_gui->dialogProgress->set_progress_advance = set_progress_advance;
+ addonInterface->toKodi->kodi_gui->dialogProgress->abort = abort;
+}
+
+void Interface_GUIDialogProgress::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->dialogProgress;
+}
+
+KODI_GUI_HANDLE Interface_GUIDialogProgress::new_dialog(KODI_HANDLE kodiBase)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return nullptr;
+ }
+
+ CGUIDialogProgress* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
+ WINDOW_DIALOG_PROGRESS);
+ if (!dialog)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (dialog='{}') on addon '{}'",
+ __func__, static_cast<void*>(dialog), addon->ID());
+ return nullptr;
+ }
+
+ return dialog;
+}
+
+void Interface_GUIDialogProgress::delete_dialog(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgress*>(handle)->Close();
+}
+
+void Interface_GUIDialogProgress::open(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgress*>(handle)->Open();
+}
+
+void Interface_GUIDialogProgress::set_heading(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ const char* heading)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return;
+ }
+
+ if (!handle || !heading)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}', heading='{}') "
+ "on addon '{}'",
+ __func__, handle, static_cast<const void*>(heading), addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgress*>(handle)->SetHeading(heading);
+}
+
+void Interface_GUIDialogProgress::set_line(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ unsigned int line,
+ const char* text)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return;
+ }
+
+ if (!handle || !text)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}', text='{}') on "
+ "addon '{}'",
+ __func__, handle, static_cast<const void*>(text), addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgress*>(handle)->SetLine(line, text);
+}
+
+void Interface_GUIDialogProgress::set_can_cancel(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ bool canCancel)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgress*>(handle)->SetCanCancel(canCancel);
+}
+
+bool Interface_GUIDialogProgress::is_canceled(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon->ID());
+ return false;
+ }
+
+ return static_cast<CGUIDialogProgress*>(handle)->IsCanceled();
+}
+
+void Interface_GUIDialogProgress::set_percentage(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ int percentage)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgress*>(handle)->SetPercentage(percentage);
+}
+
+int Interface_GUIDialogProgress::get_percentage(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return 0;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon->ID());
+ return 0;
+ }
+
+ return static_cast<CGUIDialogProgress*>(handle)->GetPercentage();
+}
+
+void Interface_GUIDialogProgress::show_progress_bar(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ bool onOff)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgress*>(handle)->ShowProgressBar(onOff);
+}
+
+void Interface_GUIDialogProgress::set_progress_max(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ int max)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgress*>(handle)->SetProgressMax(max);
+}
+
+void Interface_GUIDialogProgress::set_progress_advance(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ int nSteps)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon->ID());
+ return;
+ }
+
+ static_cast<CGUIDialogProgress*>(handle)->SetProgressAdvance(nSteps);
+}
+
+bool Interface_GUIDialogProgress::abort(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogProgress::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!handle)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogProgress::{} - invalid handler data (handle='{}') on addon '{}'",
+ __func__, handle, addon->ID());
+ return false;
+ }
+
+ return static_cast<CGUIDialogProgress*>(handle)->Abort();
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/dialogs/Progress.h b/xbmc/addons/interfaces/gui/dialogs/Progress.h
new file mode 100644
index 0000000..ba31f3c
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/Progress.h
@@ -0,0 +1,65 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/progress.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Progress.h"
+ */
+ struct Interface_GUIDialogProgress
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static KODI_GUI_HANDLE new_dialog(KODI_HANDLE kodiBase);
+ static void delete_dialog(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ static void open(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ static void set_heading(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, const char* heading);
+ static void set_line(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ unsigned int line,
+ const char* text);
+ static void set_can_cancel(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, bool canCancel);
+ static bool is_canceled(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ static void set_percentage(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, int percentage);
+ static int get_percentage(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ static void show_progress_bar(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, bool bOnOff);
+ static void set_progress_max(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, int max);
+ static void set_progress_advance(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, int nSteps);
+ static bool abort(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/dialogs/Select.cpp b/xbmc/addons/interfaces/gui/dialogs/Select.cpp
new file mode 100644
index 0000000..7ad6ed6
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/Select.cpp
@@ -0,0 +1,143 @@
+/*
+ * 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 "Select.h"
+
+#include "ServiceBroker.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/dialogs/Select.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIDialogSelect::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->dialogSelect = new AddonToKodiFuncTable_kodi_gui_dialogSelect();
+
+ addonInterface->toKodi->kodi_gui->dialogSelect->open = open;
+ addonInterface->toKodi->kodi_gui->dialogSelect->open_multi_select = open_multi_select;
+}
+
+void Interface_GUIDialogSelect::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->dialogSelect;
+}
+
+int Interface_GUIDialogSelect::open(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* entries[],
+ unsigned int size,
+ int selected,
+ unsigned int autoclose)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogSelect::{} - invalid data", __func__);
+ return -1;
+ }
+
+ CGUIDialogSelect* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!heading || !entries || !dialog)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogSelect::{} - invalid handler data (heading='{}', entries='{}', "
+ "dialog='{}') on addon '{}'",
+ __func__, static_cast<const void*>(heading), static_cast<const void*>(entries),
+ static_cast<void*>(dialog), addon->ID());
+ return -1;
+ }
+
+ dialog->Reset();
+ dialog->SetHeading(CVariant{heading});
+
+ for (unsigned int i = 0; i < size; ++i)
+ dialog->Add(entries[i]);
+
+ if (selected > 0)
+ dialog->SetSelected(selected);
+ if (autoclose > 0)
+ dialog->SetAutoClose(autoclose);
+
+ dialog->Open();
+ return dialog->GetSelectedItem();
+}
+
+
+bool Interface_GUIDialogSelect::open_multi_select(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* entryIDs[],
+ const char* entryNames[],
+ bool entriesSelected[],
+ unsigned int size,
+ unsigned int autoclose)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogMultiSelect::{} - invalid data", __func__);
+ return false;
+ }
+
+ CGUIDialogSelect* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!heading || !entryIDs || !entryNames || !entriesSelected || !dialog)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogMultiSelect::{} - invalid handler data (heading='{}', "
+ "entryIDs='{}', entryNames='{}', entriesSelected='{}', dialog='{}') on addon '{}'",
+ __func__, static_cast<const void*>(heading), static_cast<const void*>(entryIDs),
+ static_cast<const void*>(entryNames), static_cast<void*>(entriesSelected),
+ static_cast<void*>(dialog), addon->ID());
+ return false;
+ }
+
+ dialog->Reset();
+ dialog->SetMultiSelection(true);
+ dialog->SetHeading(CVariant{heading});
+
+ std::vector<int> selectedIndexes;
+
+ for (unsigned int i = 0; i < size; ++i)
+ {
+ dialog->Add(entryNames[i]);
+ if (entriesSelected[i])
+ selectedIndexes.push_back(i);
+ }
+
+ dialog->SetSelected(selectedIndexes);
+ if (autoclose > 0)
+ dialog->SetAutoClose(autoclose);
+
+ dialog->Open();
+ if (dialog->IsConfirmed())
+ {
+ for (unsigned int i = 0; i < size; ++i)
+ entriesSelected[i] = false;
+
+ selectedIndexes = dialog->GetSelectedItems();
+
+ for (unsigned int i = 0; i < selectedIndexes.size(); ++i)
+ {
+ if (selectedIndexes[i])
+ entriesSelected[selectedIndexes[i]] = true;
+ }
+ }
+
+ return true;
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/dialogs/Select.h b/xbmc/addons/interfaces/gui/dialogs/Select.h
new file mode 100644
index 0000000..5f2d0ef
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/Select.h
@@ -0,0 +1,62 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/select.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Select.h"
+ */
+ struct Interface_GUIDialogSelect
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static int open(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* entries[],
+ unsigned int size,
+ int selected,
+ unsigned int autoclose);
+ static bool open_multi_select(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* entryIDs[],
+ const char* entryNames[],
+ bool entriesSelected[],
+ unsigned int size,
+ unsigned int autoclose);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/dialogs/TextViewer.cpp b/xbmc/addons/interfaces/gui/dialogs/TextViewer.cpp
new file mode 100644
index 0000000..637975a
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/TextViewer.cpp
@@ -0,0 +1,64 @@
+/*
+ * 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 "TextViewer.h"
+
+#include "ServiceBroker.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/dialogs/TextViewer.h"
+#include "dialogs/GUIDialogTextViewer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+namespace ADDON
+{
+
+void Interface_GUIDialogTextViewer::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->dialogTextViewer =
+ new AddonToKodiFuncTable_kodi_gui_dialogTextViewer();
+
+ addonInterface->toKodi->kodi_gui->dialogTextViewer->open = open;
+}
+
+void Interface_GUIDialogTextViewer::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->dialogTextViewer;
+}
+
+void Interface_GUIDialogTextViewer::open(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* text)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogTextViewer::{} - invalid data", __func__);
+ return;
+ }
+
+ CGUIDialogTextViewer* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogTextViewer>(
+ WINDOW_DIALOG_TEXT_VIEWER);
+ if (!heading || !text || !dialog)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogTextViewer::{} - invalid handler data (heading='{}', text='{}', "
+ "dialog='{}') on addon '{}'",
+ __func__, static_cast<const void*>(heading), static_cast<const void*>(text),
+ static_cast<void*>(dialog), addon->ID());
+ return;
+ }
+
+ dialog->SetHeading(heading);
+ dialog->SetText(text);
+ dialog->Open();
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/dialogs/TextViewer.h b/xbmc/addons/interfaces/gui/dialogs/TextViewer.h
new file mode 100644
index 0000000..873dbbb
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/TextViewer.h
@@ -0,0 +1,50 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/text_viewer.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/TextViewer.h"
+ */
+ struct Interface_GUIDialogTextViewer
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static void open(KODI_HANDLE kodiBase, const char* heading, const char* text);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/gui/dialogs/YesNo.cpp b/xbmc/addons/interfaces/gui/dialogs/YesNo.cpp
new file mode 100644
index 0000000..89dea58
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/YesNo.cpp
@@ -0,0 +1,136 @@
+/*
+ * 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 "YesNo.h"
+
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/gui/dialogs/YesNo.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "utils/log.h"
+
+using namespace KODI::MESSAGING;
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+namespace ADDON
+{
+
+void Interface_GUIDialogYesNo::Init(AddonGlobalInterface* addonInterface)
+{
+ addonInterface->toKodi->kodi_gui->dialogYesNo = new AddonToKodiFuncTable_kodi_gui_dialogYesNo();
+
+ addonInterface->toKodi->kodi_gui->dialogYesNo->show_and_get_input_single_text =
+ show_and_get_input_single_text;
+ addonInterface->toKodi->kodi_gui->dialogYesNo->show_and_get_input_line_text =
+ show_and_get_input_line_text;
+ addonInterface->toKodi->kodi_gui->dialogYesNo->show_and_get_input_line_button_text =
+ show_and_get_input_line_button_text;
+}
+
+void Interface_GUIDialogYesNo::DeInit(AddonGlobalInterface* addonInterface)
+{
+ delete addonInterface->toKodi->kodi_gui->dialogYesNo;
+}
+
+bool Interface_GUIDialogYesNo::show_and_get_input_single_text(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* text,
+ bool* canceled,
+ const char* noLabel,
+ const char* yesLabel)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogYesNo::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!heading || !text || !canceled || !noLabel || !yesLabel)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogYesNo::{} - invalid handler data (heading='{}', text='{}', "
+ "canceled='{}', noLabel='{}', yesLabel='{}') on addon '{}'",
+ __func__, static_cast<const void*>(heading), static_cast<const void*>(text),
+ static_cast<void*>(canceled), static_cast<const void*>(noLabel),
+ static_cast<const void*>(yesLabel), addon->ID());
+ return false;
+ }
+
+ DialogResponse result = HELPERS::ShowYesNoDialogText(heading, text, noLabel, yesLabel);
+ *canceled = (result == DialogResponse::CHOICE_CANCELLED);
+ return (result == DialogResponse::CHOICE_YES);
+}
+
+bool Interface_GUIDialogYesNo::show_and_get_input_line_text(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* line0,
+ const char* line1,
+ const char* line2,
+ const char* noLabel,
+ const char* yesLabel)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogYesNo::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!heading || !line0 || !line1 || !line2 || !noLabel || !yesLabel)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogYesNo::{} - invalid handler data (heading='{}', line0='{}', "
+ "line1='{}', line2='{}', "
+ "noLabel='{}', yesLabel='{}') on addon '{}'",
+ __func__, static_cast<const void*>(heading), static_cast<const void*>(line0),
+ static_cast<const void*>(line1), static_cast<const void*>(line2),
+ static_cast<const void*>(noLabel), static_cast<const void*>(yesLabel), addon->ID());
+ return false;
+ }
+
+ return HELPERS::ShowYesNoDialogLines(heading, line0, line1, line2, noLabel, yesLabel) ==
+ DialogResponse::CHOICE_YES;
+}
+
+bool Interface_GUIDialogYesNo::show_and_get_input_line_button_text(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* line0,
+ const char* line1,
+ const char* line2,
+ bool* canceled,
+ const char* noLabel,
+ const char* yesLabel)
+{
+ CAddonDll* addon = static_cast<CAddonDll*>(kodiBase);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "Interface_GUIDialogYesNo::{} - invalid data", __func__);
+ return false;
+ }
+
+ if (!heading || !line0 || !line1 || !line2 || !canceled || !noLabel || !yesLabel)
+ {
+ CLog::Log(LOGERROR,
+ "Interface_GUIDialogYesNo::{} - invalid handler data (heading='{}', line0='{}', "
+ "line1='{}', line2='{}', "
+ "canceled='{}', noLabel='{}', yesLabel='{}') on addon '{}'",
+ __func__, static_cast<const void*>(heading), static_cast<const void*>(line0),
+ static_cast<const void*>(line1), static_cast<const void*>(line2),
+ static_cast<const void*>(canceled), static_cast<const void*>(noLabel),
+ static_cast<const void*>(yesLabel), addon->ID());
+ return false;
+ }
+
+ DialogResponse result =
+ HELPERS::ShowYesNoDialogLines(heading, line0, line1, line2, noLabel, yesLabel);
+ *canceled = (result == DialogResponse::CHOICE_CANCELLED);
+ return (result == DialogResponse::CHOICE_YES);
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/interfaces/gui/dialogs/YesNo.h b/xbmc/addons/interfaces/gui/dialogs/YesNo.h
new file mode 100644
index 0000000..84e5d73
--- /dev/null
+++ b/xbmc/addons/interfaces/gui/dialogs/YesNo.h
@@ -0,0 +1,72 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/yes_no.h"
+
+extern "C"
+{
+
+ struct AddonGlobalInterface;
+
+ namespace ADDON
+ {
+
+ /*!
+ * @brief Global gui Add-on to Kodi callback functions
+ *
+ * To hold functions not related to a instance type and usable for
+ * every add-on type.
+ *
+ * Related add-on header is "./xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/YesNo.h"
+ */
+ struct Interface_GUIDialogYesNo
+ {
+ static void Init(AddonGlobalInterface* addonInterface);
+ static void DeInit(AddonGlobalInterface* addonInterface);
+
+ /*!
+ * @brief callback functions from add-on to kodi
+ *
+ * @note To add a new function use the "_" style to directly identify an
+ * add-on callback function. Everything with CamelCase is only to be used
+ * in Kodi.
+ *
+ * The parameter `kodiBase` is used to become the pointer for a `CAddonDll`
+ * class.
+ */
+ //@{
+ static bool show_and_get_input_single_text(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* text,
+ bool* canceled,
+ const char* noLabel,
+ const char* yesLabel);
+
+ static bool show_and_get_input_line_text(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* line0,
+ const char* line1,
+ const char* line2,
+ const char* noLabel,
+ const char* yesLabel);
+
+ static bool show_and_get_input_line_button_text(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* line0,
+ const char* line1,
+ const char* line2,
+ bool* canceled,
+ const char* noLabel,
+ const char* yesLabel);
+ //@}
+ };
+
+ } /* namespace ADDON */
+} /* extern "C" */
diff --git a/xbmc/addons/interfaces/platform/android/System.cpp b/xbmc/addons/interfaces/platform/android/System.cpp
new file mode 100644
index 0000000..9f83b22
--- /dev/null
+++ b/xbmc/addons/interfaces/platform/android/System.cpp
@@ -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.
+ */
+
+#include "System.h"
+
+#include "CompileInfo.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/interfaces/AddonBase.h"
+#include "addons/kodi-dev-kit/include/kodi/platform/android/System.h"
+
+#include "platform/android/activity/XBMCApp.h"
+
+static AddonToKodiFuncTable_android_system function_table;
+
+namespace ADDON
+{
+
+void Interface_Android::Register()
+{
+ function_table.get_jni_env = get_jni_env;
+ function_table.get_sdk_version = get_sdk_version;
+ function_table.get_class_name = get_class_name;
+ Interface_Base::RegisterInterface(Get);
+}
+
+void* Interface_Android::Get(const std::string &name, const std::string &version)
+{
+ if (name == INTERFACE_ANDROID_SYSTEM_NAME
+ && version >= INTERFACE_ANDROID_SYSTEM_VERSION_MIN
+ && version <= INTERFACE_ANDROID_SYSTEM_VERSION)
+ return &function_table;
+
+ return nullptr;
+};
+
+void* Interface_Android::get_jni_env()
+{
+ return xbmc_jnienv();
+}
+
+int Interface_Android::get_sdk_version()
+{
+ return CXBMCApp::Get().GetSDKVersion();
+}
+
+const char *Interface_Android::get_class_name()
+{
+ return CCompileInfo::GetClass();
+}
+
+
+} //namespace ADDON
diff --git a/xbmc/addons/interfaces/platform/android/System.h b/xbmc/addons/interfaces/platform/android/System.h
new file mode 100644
index 0000000..59805ab
--- /dev/null
+++ b/xbmc/addons/interfaces/platform/android/System.h
@@ -0,0 +1,26 @@
+/*
+ * 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 <string>
+
+namespace ADDON
+{
+
+struct Interface_Android
+{
+ static void Register();
+ static void* Get(const std::string &name, const std::string &version);
+
+ static void* get_jni_env();
+ static int get_sdk_version();
+ static const char *get_class_name();
+};
+
+} //namespace ADDON
diff --git a/xbmc/addons/kodi-dev-kit/.gitignore b/xbmc/addons/kodi-dev-kit/.gitignore
new file mode 100644
index 0000000..0256029
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/.gitignore
@@ -0,0 +1,36 @@
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+
+/docs
+/build
+
+# Prevent auto generated parts
+/include/groups.dox
+/tools/code-generator/creation_log.txt
+/tools/code-generator/creation_log.txt.old
diff --git a/xbmc/addons/kodi-dev-kit/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/CMakeLists.txt
new file mode 100644
index 0000000..4b539b2
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/CMakeLists.txt
@@ -0,0 +1,4 @@
+cmake_minimum_required(VERSION 3.5)
+project(kodi-dev-kit)
+
+include(cmake/test/abi-interface-test.cmake)
diff --git a/xbmc/addons/kodi-dev-kit/cmake/test/abi-interface-test.cmake b/xbmc/addons/kodi-dev-kit/cmake/test/abi-interface-test.cmake
new file mode 100644
index 0000000..2e62034
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/cmake/test/abi-interface-test.cmake
@@ -0,0 +1,181 @@
+option(DEVKIT_TEST "To test dev-kit" TRUE)
+option(DEVKIT_TEST_CPP "To test dev-kit headers correct in \"C++\" compile" FALSE)
+option(DEVKIT_TEST_STOP_ON_ERROR "To stop build if during \"C\" ABI test an error was found" FALSE)
+
+if(NOT DEVKIT_TEST)
+ return()
+endif()
+
+include(CheckCSourceCompiles)
+include(CheckCXXSourceCompiles)
+
+list(APPEND CMAKE_REQUIRED_INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/include/kodi
+ ${CMAKE_CURRENT_SOURCE_DIR}/../..)
+
+message(STATUS "################################################################################")
+message(STATUS "# Run test about dev-kit headers for correct style")
+message(STATUS "#")
+
+# Set minimal required defines
+if(CORE_SYSTEM_NAME STREQUAL android)
+ set(DEVKIT_TEST_DEFINES "\
+#define HAS_GLES 3
+#define TARGET_POSIX
+#define TARGET_LINUX
+#define TARGET_ANDROID
+")
+elseif(CORE_SYSTEM_NAME STREQUAL darwin_embedded)
+ set(DEVKIT_TEST_DEFINES "\
+#define HAS_GLES 2
+#define TARGET_POSIX
+#define TARGET_DARWIN
+#define TARGET_DARWIN_EMBEDDED
+")
+elseif(CORE_SYSTEM_NAME STREQUAL osx)
+ set(DEVKIT_TEST_DEFINES "\
+#define HAS_GL 1
+#define TARGET_DARWIN
+#define TARGET_DARWIN_OSX
+")
+elseif(CORE_SYSTEM_NAME STREQUAL windows)
+ set(DEVKIT_TEST_DEFINES "\
+#define HAS_DX 1
+#define WIN32
+#define TARGET_WINDOWS
+#define TARGET_WINDOWS_DESKTOP
+")
+elseif(CORE_SYSTEM_NAME STREQUAL linux)
+ set(DEVKIT_TEST_DEFINES "\
+#define HAS_GL 1
+#define TARGET_POSIX
+#define TARGET_LINUX
+")
+else()
+ set(DEVKIT_TEST_DEFINES "\
+#define HAS_GL 1
+#define TARGET_POSIX
+#define TARGET_FREEBSD
+")
+endif()
+
+# Read list of all available headers
+file(GLOB_RECURSE DEVKIT_HEADERS
+ include/kodi/*.h
+)
+
+##
+# Check the "C" headers itself with compile in "C"
+#
+
+message(STATUS "")
+message(STATUS "================================================================================")
+message(STATUS "Run \"C\" headers:")
+
+set(DEVKIT_HEADER_NO 0)
+foreach(DEVKIT_HEADER ${DEVKIT_HEADERS})
+ if(NOT DEVKIT_HEADER MATCHES "c-api")
+ continue()
+ endif()
+
+ string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/include/" "" DEVKIT_HEADER_NAME ${DEVKIT_HEADER})
+
+ message(STATUS "--------------------------------------------------------------------------------")
+ message(STATUS "Checking \"C\" ABI about \"#include <${DEVKIT_HEADER_NAME}>\":")
+
+ math(EXPR DEVKIT_HEADER_NO "${DEVKIT_HEADER_NO}+1")
+ set(DEVKIT_TEST_NAME "C_TEST_BUILD_A_${DEVKIT_HEADER_NO}")
+
+ check_c_source_compiles("\
+${DEVKIT_TEST_DEFINES}
+#include \"${DEVKIT_HEADER}\"
+int main()
+{
+ return 0;
+}" ${DEVKIT_TEST_NAME})
+
+ if (NOT ${DEVKIT_TEST_NAME} EQUAL 1)
+ if(${DEVKIT_TEST_STOP_ON_ERROR})
+ message(FATAL_ERROR " - Header not match needed \"C\" ABI: \"#include <${DEVKIT_HEADER_NAME}>\" (see CMakeError.log)")
+ else()
+ message(STATUS " - WARNING: Header not match needed \"C\" ABI: \"#include <${DEVKIT_HEADER_NAME}>\" (see CMakeError.log)")
+ endif()
+ endif()
+endforeach()
+
+##
+# Check the "C++" headers itself with compile in "C"
+#
+message(STATUS "")
+message(STATUS "================================================================================")
+message(STATUS "Run \"C++\" headers (to confirm there a safe within \"C\"):")
+
+set(DEVKIT_HEADER_NO 0)
+foreach(DEVKIT_HEADER ${DEVKIT_HEADERS})
+ if(DEVKIT_HEADER MATCHES "c-api")
+ continue()
+ endif()
+
+ string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/include/" "" DEVKIT_HEADER_NAME ${DEVKIT_HEADER})
+
+ message(STATUS "--------------------------------------------------------------------------------")
+ message(STATUS "Checking \"C\" ABI about \"#include <${DEVKIT_HEADER_NAME}>\":")
+
+ math(EXPR DEVKIT_HEADER_NO "${DEVKIT_HEADER_NO}+1")
+ set(DEVKIT_TEST_NAME "C_TEST_BUILD_B_${DEVKIT_HEADER_NO}")
+
+ check_c_source_compiles("\
+${DEVKIT_TEST_DEFINES}
+#include \"${DEVKIT_HEADER}\"
+int main()
+{
+ return 0;
+}" ${DEVKIT_TEST_NAME})
+
+ if (NOT ${DEVKIT_TEST_NAME} EQUAL 1)
+ if(${DEVKIT_TEST_STOP_ON_ERROR})
+ message(FATAL_ERROR " - \"C++\" header not safe about \"C\" ABI: \"#include <${DEVKIT_HEADER_NAME}>\" (see CMakeError.log)")
+ else()
+ message(STATUS " - WARNING: \"C++\" header not safe about \"C\" ABI: \"#include <${DEVKIT_HEADER_NAME}>\" (see CMakeError.log)")
+ endif()
+ endif()
+endforeach()
+
+##
+# Check the "C++" headers itself with compile in "C++"
+#
+if(DEVKIT_TEST_CPP)
+ message(STATUS "")
+ message(STATUS "================================================================================")
+ message(STATUS "Run \"C++\" headers (to confirm there a correct within \"C++\"):")
+
+ set(DEVKIT_HEADER_NO 0)
+ foreach(DEVKIT_HEADER ${DEVKIT_HEADERS})
+ if(DEVKIT_HEADER MATCHES "c-api")
+ continue()
+ endif()
+
+ string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/include/" "" DEVKIT_HEADER_NAME ${DEVKIT_HEADER})
+
+ message(STATUS "--------------------------------------------------------------------------------")
+ message(STATUS "Checking \"C++\" about \"#include <${DEVKIT_HEADER_NAME}>\":")
+
+ math(EXPR DEVKIT_HEADER_NO "${DEVKIT_HEADER_NO}+1")
+ set(DEVKIT_TEST_NAME "CPP_TEST_BUILD_${DEVKIT_HEADER_NO}")
+
+ check_cxx_source_compiles("\
+${DEVKIT_TEST_DEFINES}
+#include \"${DEVKIT_HEADER}\"
+int main()
+{
+ return 0;
+}" ${DEVKIT_TEST_NAME})
+
+ if (NOT ${DEVKIT_TEST_NAME} EQUAL 1)
+ if(${DEVKIT_TEST_STOP_ON_ERROR})
+ message(FATAL_ERROR " - \"C++\" header failed to build: \"#include <${DEVKIT_HEADER_NAME}>\" (see CMakeError.log)")
+ else()
+ message(STATUS " - \"C++\" header failed to build: \"#include <${DEVKIT_HEADER_NAME}>\" (see CMakeError.log)")
+ endif()
+ endif()
+ endforeach()
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Doxyfile b/xbmc/addons/kodi-dev-kit/doxygen/Doxyfile
new file mode 100644
index 0000000..eea8a58
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Doxyfile
@@ -0,0 +1,2589 @@
+# Doxyfile 1.8.13
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+# This includes a own file where used on Kodi itself and by kodi-dev-kit.
+# Also used to have on new releases better overview which parts need update.
+@INCLUDE = ../../../../docs/doxygen/kodi_values.doxy
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all text
+# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv
+# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv
+# for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME = "Kodi Development"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+# PROJECT_NUMBER done by ./kodi_values.doxy
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF = "for Binary and Script based Add-Ons"
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO = ./kodi-dev.png
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY = ../docs
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF = "The $name class" \
+ "The $name widget" \
+ "The $name file" \
+ is \
+ provides \
+ specifies \
+ contains \
+ represents \
+ a \
+ an \
+ the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC = YES
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH = .
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE = 4
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines.
+
+# ALIASES done by ./kodi_values.doxy
+
+# This tag can be used to specify a number of word-keyword mappings (TCL only).
+# A mapping has the form "name=value". For example adding "class=itcl::class"
+# will allow you to use the command class in the itcl::class meaning.
+
+TCL_SUBST =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, Javascript,
+# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran:
+# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran:
+# Fortran. In the later case the parser tries to guess whether the code is fixed
+# or free formatted code, this is the default for Fortran type files), VHDL. For
+# instance to make doxygen treat .inc files as Fortran files (default is PHP),
+# and .f files as C (default is Fortran), use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen.
+
+EXTENSION_MAPPING =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See http://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 0.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS = 0
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC = YES
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS = YES
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT = YES
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE = NO
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES = NO
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# (class|struct|union) declarations. If set to NO, these declarations will be
+# included in the documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
+# names in lower-case letters. If set to YES, upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES = YES
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= YES
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES = NO
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS = NO
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS = YES
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME = YES
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING = YES
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES = NO
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES = NO
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE = DoxygenLayout.xml
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC = NO
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered.
+# The default value is: NO.
+
+WARN_AS_ERROR = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT = main.txt \
+ General/General.dox \
+ General/DoxygenOnAddon.dox \
+ ../../../GUIInfoManager.cpp \
+ Modules/modules_general.dox \
+ Modules/modules_cpp.dox \
+ Modules/modules_cpp_gui.dox \
+ Modules/modules_cpp_peripheral.dox \
+ Modules/modules_python.dox \
+ Skin/skin.dox \
+ ../../../../cmake/scripts/common/AddonHelpers.dox \
+ ../../../cores/RetroPlayer/guicontrols/GUIGameControl.dox \
+ ../../../pvr/guilib/GUIEPGGridContainer.dox \
+ ../../../guilib/GUIButtonControl.dox \
+ ../../../guilib/GUIColorButtonControl.dox \
+ ../../../guilib/GUIControlGroup.dox \
+ ../../../guilib/GUIEditControl.dox \
+ ../../../guilib/GUIFadeLabelControl.dox \
+ ../../../guilib/GUIFixedListContainer.dox \
+ ../../../guilib/GUIImage.dox \
+ ../../../guilib/GUILabelControl.dox \
+ ../../../guilib/GUIListContainer.dox \
+ ../../../guilib/GUIListGroup.dox \
+ ../../../guilib/GUIMoverControl.dox \
+ ../../../guilib/GUIMultiImage.dox \
+ ../../../guilib/GUIPanelContainer.dox \
+ ../../../guilib/GUIProgressControl.dox \
+ ../../../guilib/GUIRadioButtonControl.dox \
+ ../../../guilib/GUIRangesControl.dox \
+ ../../../guilib/GUIRenderingControl.dox \
+ ../../../guilib/GUIResizeControl.dox \
+ ../../../guilib/GUIRSSControl.dox \
+ ../../../guilib/GUIScrollBarControl.dox \
+ ../../../guilib/GUISettingsSliderControl.dox \
+ ../../../guilib/GUISliderControl.dox \
+ ../../../guilib/GUISpinControl.dox \
+ ../../../guilib/GUISpinControlEx.dox \
+ ../../../guilib/GUITextBox.dox \
+ ../../../guilib/GUIToggleButtonControl.dox \
+ ../../../guilib/GUIVideoControl.dox \
+ ../../../guilib/GUIVisualisationControl.dox \
+ ../../../guilib/GUIWrappingListContainer.dox \
+ ../../../guilib/WindowIDs.dox \
+ ../../../guilib/_Controls.dox \
+ ../../../interfaces/builtins/AddonBuiltins.cpp \
+ ../../../interfaces/builtins/AndroidBuiltins.cpp \
+ ../../../interfaces/builtins/ApplicationBuiltins.cpp \
+ ../../../interfaces/builtins/CECBuiltins.cpp \
+ ../../../interfaces/builtins/GUIBuiltins.cpp \
+ ../../../interfaces/builtins/GUIContainerBuiltins.cpp \
+ ../../../interfaces/builtins/GUIControlBuiltins.cpp \
+ ../../../interfaces/builtins/LibraryBuiltins.cpp \
+ ../../../interfaces/builtins/OpticalBuiltins.cpp \
+ ../../../interfaces/builtins/PictureBuiltins.cpp \
+ ../../../interfaces/builtins/PlayerBuiltins.cpp \
+ ../../../interfaces/builtins/ProfileBuiltins.cpp \
+ ../../../interfaces/builtins/PVRBuiltins.cpp \
+ ../../../interfaces/builtins/SkinBuiltins.cpp \
+ ../../../interfaces/builtins/SystemBuiltins.cpp \
+ ../../../interfaces/builtins/WeatherBuiltins.cpp \
+ ../../../interfaces/legacy/Addon.h \
+ ../../../interfaces/legacy/Control.h \
+ ../../../interfaces/legacy/Dialog.h \
+ ../../../interfaces/legacy/DrmCryptoSession.h \
+ ../../../interfaces/legacy/File.h \
+ ../../../interfaces/legacy/InfoTagGame.h \
+ ../../../interfaces/legacy/InfoTagMusic.h \
+ ../../../interfaces/legacy/InfoTagPicture.h \
+ ../../../interfaces/legacy/InfoTagRadioRDS.h \
+ ../../../interfaces/legacy/InfoTagVideo.h \
+ ../../../interfaces/legacy/Keyboard.h \
+ ../../../interfaces/legacy/ListItem.h \
+ ../../../interfaces/legacy/ModuleXbmcgui.h \
+ ../../../interfaces/legacy/ModuleXbmc.h \
+ ../../../interfaces/legacy/ModuleXbmcplugin.h \
+ ../../../interfaces/legacy/ModuleXbmcvfs.h \
+ ../../../interfaces/legacy/Monitor.h \
+ ../../../interfaces/legacy/Player.h \
+ ../../../interfaces/legacy/PlayList.h \
+ ../../../interfaces/legacy/RenderCapture.h \
+ ../../../interfaces/legacy/Settings.h \
+ ../../../interfaces/legacy/Stat.h \
+ ../../../interfaces/legacy/WindowDialog.h \
+ ../../../interfaces/legacy/Window.h \
+ ../../../interfaces/legacy/WindowXML.h \
+ ../../../interfaces/legacy/wsgi/WsgiErrorStream.h \
+ ../../../interfaces/legacy/wsgi/WsgiInputStream.h \
+ ../../../interfaces/legacy/wsgi/WsgiResponseBody.h \
+ ../../../interfaces/legacy/wsgi/WsgiResponse.h \
+ ../../../pictures/PictureInfoTag.cpp \
+ ../include
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: http://www.gnu.org/software/libiconv) for the list of
+# possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
+# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf.
+
+FILE_PATTERNS = *.c \
+ *.cc \
+ *.cxx \
+ *.cpp \
+ *.c++ \
+ *.java \
+ *.ii \
+ *.ixx \
+ *.ipp \
+ *.i++ \
+ *.inl \
+ *.idl \
+ *.ddl \
+ *.odl \
+ *.h \
+ *.hh \
+ *.hxx \
+ *.hpp \
+ *.h++ \
+ *.cs \
+ *.d \
+ *.php \
+ *.php4 \
+ *.php5 \
+ *.phtml \
+ *.inc \
+ *.m \
+ *.markdown \
+ *.md \
+ *.mm \
+ *.dox \
+ *.py \
+ *.f90 \
+ *.f \
+ *.for \
+ *.tcl \
+ *.vhd \
+ *.vhdl \
+ *.ucf \
+ *.qsf \
+ *.as \
+ *.js \
+ *.GPL
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH = .
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS =
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH = .
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# function all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see http://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the config file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS = YES
+
+# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the
+# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the
+# cost of reduced performance. This can be particularly helpful with template
+# rich C++ code for which doxygen's built-in parser lacks the necessary type
+# information.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse-libclang=ON option for CMake.
+# The default value is: NO.
+
+CLANG_ASSISTED_PARSING = NO
+
+# If clang assisted parsing is enabled you can provide the compiler with command
+# line options that you would normally use when invoking the compiler. Note that
+# the include paths will already be set by doxygen for the files and directories
+# specified with INPUT and INCLUDE_PATH.
+# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.
+
+CLANG_OPTIONS =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX = YES
+
+# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
+# which the alphabetical index list will be split.
+# Minimum value: 1, maximum value: 20, default value: 5.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+COLS_IN_ALPHA_INDEX = 5
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES = LICENSE.md
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# http://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP = NO
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 30
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see: http://developer.apple.com/tools/xcode/), introduced with
+# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a
+# Makefile in the HTML output directory. Running make will produce the docset in
+# that directory and running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the master .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual-
+# folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS =
+
+# The QHG_LOCATION tag can be used to specify the location of Qt's
+# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
+# generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX = YES
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW = YES
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH = 350
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW = NO
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE = 10
+
+# Use the FORMULA_TRANPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT = YES
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# http://www.mathjax.org) which uses client side Javascript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from http://www.mathjax.org before deployment.
+# The default value is: http://cdn.mathjax.org/mathjax/latest.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using Javascript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: http://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when enabling USE_PDFLATEX this option is only used for generating
+# bitmaps for formulas in the HTML output, but not in the Makefile that is
+# written to the output directory.
+# The default file is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE = a4
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. The package can be specified just
+# by its name or with the correct syntax as to be used with the LaTeX
+# \usepackage command. To get the times font for instance you can specify :
+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
+# To use the option intlimits with the amsmath package you can specify:
+# EXTRA_PACKAGES=[intlimits]{amsmath}
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES, to get a
+# higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# http://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE = plain
+
+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_TIMESTAMP = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's config
+# file, i.e. a series of assignments. You only have to provide replacements,
+# missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE =
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's config file. A template extensions file can be generated
+# using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE =
+
+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
+# with syntax highlighting in the RTF output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_SOURCE_CODE = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR =
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT = docbook
+
+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
+# program listings (including syntax highlighting and cross-referencing
+# information) to the DOCBOOK output. Note that enabling this will significantly
+# increase the size of the DOCBOOK output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sf.net) file that captures the
+# structure of the code including all documentation. Note that this feature is
+# still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF = NO
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS =
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED = DOXYGEN_SHOULD_SKIP_THIS \
+ DOXYGEN_SHOULD_USE_THIS \
+ __cplusplus
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE =
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of 'which perl').
+# The default file (with absolute path) is: /usr/bin/perl.
+
+PERL_PATH = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
+# NO turns the diagrams off. Note that this option also works with HAVE_DOT
+# disabled, but it is recommended to install and use dot, since it yields more
+# powerful graphs.
+# The default value is: YES.
+
+CLASS_DIAGRAMS = NO
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. Doxygen will then run the mscgen tool (see:
+# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the
+# documentation. The MSCGEN_PATH tag allows you to specify the directory where
+# the mscgen tool resides. If left empty the tool is assumed to be found in the
+# default search path.
+
+MSCGEN_PATH =
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH =
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: YES.
+
+HAVE_DOT = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS = 0
+
+# When you want a differently looking font in the dot files that doxygen
+# generates you can specify the font name using DOT_FONTNAME. You need to make
+# sure dot is able to find the font, which can be done by putting it in a
+# standard location or by setting the DOTFONTPATH environment variable or by
+# setting DOT_FONTPATH to the directory containing the font.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTNAME = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTSIZE = 10
+
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH =
+
+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
+# each documented class showing the direct and indirect inheritance relations.
+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CLASS_GRAPH = YES
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH = YES
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LIMIT_NUM_FIELDS = 10
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH = YES
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command. Disabling a call graph can be
+# accomplished by means of the command \hidecallgraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH = NO
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command. Disabling a caller graph can be
+# accomplished by means of the command \hidecallergraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. For an explanation of the image formats see the section
+# output formats in the documentation of the dot tool (Graphviz (see:
+# http://www.graphviz.org/)).
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, png:cairo, png:cairo:cairo, png:cairo:gd, png:gd,
+# png:gd:gd, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, gif, gif:cairo,
+# gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd, png:cairo,
+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# png:gdiplus:gdiplus.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT = svg
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG = YES
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS =
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS =
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file. If left blank, it is assumed
+# PlantUML is not used or called during a preprocessing step. Doxygen will
+# generate a warning when it encounters a \startuml command in this case and
+# will not generate output for the diagram.
+
+PLANTUML_JAR_PATH =
+
+# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
+# configuration file for plantuml.
+
+PLANTUML_CFG_FILE =
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not seem
+# to support this out of the box.
+#
+# Warning: Depending on the platform used, enabling this option may lead to
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
+# read).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_TRANSPARENT = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_CLEANUP = YES
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/DoxygenLayout.xml b/xbmc/addons/kodi-dev-kit/doxygen/DoxygenLayout.xml
new file mode 100644
index 0000000..a1f0c67
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/DoxygenLayout.xml
@@ -0,0 +1,207 @@
+<doxygenlayout version="1.0">
+ <!-- Generated by doxygen 1.8.9.1 -->
+ <!-- Navigation index tabs for HTML output -->
+ <navindex>
+ <tab type="mainpage" visible="yes" title=""/>
+ <tab type="pages" visible="yes" title="" intro=""/>
+ <tab type="modules" visible="yes" title="Language Development" intro=""/>
+<!-- <tab type="namespaces" visible="yes" title="">
+ <tab type="namespacelist" visible="yes" title="" intro=""/>
+ <tab type="namespacemembers" visible="yes" title="" intro=""/>
+ </tab>
+ <tab type="classes" visible="yes" title="">
+ <tab type="classlist" visible="yes" title="" intro=""/>
+ <tab type="classindex" visible="$ALPHABETICAL_INDEX" title=""/>
+ <tab type="hierarchy" visible="yes" title="" intro=""/>
+ <tab type="classmembers" visible="yes" title="" intro=""/>
+ </tab>-->
+ <tab type="files" visible="yes" title="">
+ <tab type="filelist" visible="yes" title="" intro=""/>
+ <tab type="globals" visible="yes" title="" intro=""/>
+ </tab>
+ <tab type="examples" visible="yes" title="" intro=""/>
+ <tab type="usergroup" title="Older versions">
+ <tab type="user" url="http://mirrors.kodi.tv/docs/python-docs/16.x-jarvis/" title="Kodi 16.x Jarvis"/>
+ <tab type="user" url="http://mirrors.kodi.tv/docs/python-docs/15.x-isengard/" title="Kodi 15.x Isengard"/>
+ <tab type="user" url="http://mirrors.kodi.tv/docs/python-docs/14.x-helix/" title="Kodi 14.x Helix"/>
+ <tab type="user" url="http://mirrors.kodi.tv/docs/python-docs/13.0-gotham/" title="XBMC 13.x Gotham"/>
+ <tab type="user" url="http://mirrors.kodi.tv/docs/python-docs/12.2-frodo/" title="XBMC 12.x Frodo"/>
+ </tab>
+ </navindex>
+
+ <!-- Layout definition for a class page -->
+ <class>
+ <briefdescription visible="yes"/>
+ <detaileddescription title=""/>
+ <includes visible="$SHOW_INCLUDE_FILES"/>
+ <inheritancegraph visible="$CLASS_GRAPH"/>
+ <collaborationgraph visible="$COLLABORATION_GRAPH"/>
+ <memberdecl>
+ <nestedclasses visible="yes" title=""/>
+ <publictypes title=""/>
+ <services title=""/>
+ <interfaces title=""/>
+ <publicslots title=""/>
+ <signals title=""/>
+ <publicmethods title=""/>
+ <publicstaticmethods title=""/>
+ <publicattributes title=""/>
+ <publicstaticattributes title=""/>
+ <protectedtypes title=""/>
+ <protectedslots title=""/>
+ <protectedmethods title=""/>
+ <protectedstaticmethods title=""/>
+ <protectedattributes title=""/>
+ <protectedstaticattributes title=""/>
+ <packagetypes title=""/>
+ <packagemethods title=""/>
+ <packagestaticmethods title=""/>
+ <packageattributes title=""/>
+ <packagestaticattributes title=""/>
+ <properties title=""/>
+ <events title=""/>
+ <privatetypes title=""/>
+ <privateslots title=""/>
+ <privatemethods title=""/>
+ <privatestaticmethods title=""/>
+ <privateattributes title=""/>
+ <privatestaticattributes title=""/>
+ <friends title=""/>
+ <related title="" subtitle=""/>
+ <membergroups visible="yes"/>
+ </memberdecl>
+<!-- <detaileddescription title=""/> -->
+ <memberdef>
+ <inlineclasses title=""/>
+ <typedefs title=""/>
+ <enums title=""/>
+ <services title=""/>
+ <interfaces title=""/>
+ <constructors title=""/>
+ <functions title=""/>
+ <related title=""/>
+ <variables title=""/>
+ <properties title=""/>
+ <events title=""/>
+ </memberdef>
+ <allmemberslink visible="yes"/>
+ <usedfiles visible="$SHOW_USED_FILES"/>
+ <authorsection visible="yes"/>
+ </class>
+
+ <!-- Layout definition for a namespace page -->
+ <namespace>
+ <briefdescription visible="yes"/>
+ <detaileddescription title=""/>
+ <memberdecl>
+ <nestednamespaces visible="yes" title=""/>
+ <constantgroups visible="yes" title=""/>
+ <classes visible="yes" title=""/>
+ <typedefs title=""/>
+ <enums title=""/>
+ <functions title=""/>
+ <variables title=""/>
+ <membergroups visible="yes"/>
+ </memberdecl>
+<!-- <detaileddescription title=""/> -->
+ <memberdef>
+ <inlineclasses title=""/>
+ <typedefs title=""/>
+ <enums title=""/>
+ <functions title=""/>
+ <variables title=""/>
+ </memberdef>
+ <authorsection visible="yes"/>
+ </namespace>
+
+ <!-- Layout definition for a file page -->
+ <file>
+ <briefdescription visible="yes"/>
+ <detaileddescription title=""/>
+ <includes visible="$SHOW_INCLUDE_FILES"/>
+ <includegraph visible="$INCLUDE_GRAPH"/>
+ <includedbygraph visible="$INCLUDED_BY_GRAPH"/>
+ <sourcelink visible="yes"/>
+ <memberdecl>
+ <classes visible="yes" title=""/>
+ <namespaces visible="yes" title=""/>
+ <constantgroups visible="yes" title=""/>
+ <defines title=""/>
+ <typedefs title=""/>
+ <enums title=""/>
+ <functions title=""/>
+ <variables title=""/>
+ <membergroups visible="yes"/>
+ </memberdecl>
+<!-- <detaileddescription title=""/> -->
+ <memberdef>
+ <inlineclasses title=""/>
+ <defines title=""/>
+ <typedefs title=""/>
+ <enums title=""/>
+ <functions title=""/>
+ <variables title=""/>
+ </memberdef>
+ <authorsection/>
+ </file>
+
+ <!-- Layout definition for a group page -->
+ <group>
+<!-- <briefdescription visible="yes"/> -->
+ <detaileddescription title=""/>
+ <groupgraph visible="$GROUP_GRAPHS"/>
+ <memberdecl>
+ <nestedgroups visible="yes" title=""/>
+ <dirs visible="yes" title=""/>
+ <files visible="yes" title=""/>
+ <namespaces visible="yes" title=""/>
+<!-- <classes visible="yes" title=""/> -->
+<!-- <defines title=""/> -->
+ <typedefs title=""/>
+<!-- <enums title=""/> -->
+ <enumvalues title=""/>
+<!-- <functions title=""/> -->
+ <variables title=""/>
+ <signals title=""/>
+ <publicslots title=""/>
+ <protectedslots title=""/>
+ <privateslots title=""/>
+ <events title=""/>
+ <properties title=""/>
+ <friends title=""/>
+<!-- <membergroups visible="yes"/>-->
+ </memberdecl>
+<!-- <detaileddescription title=""/> -->
+ <memberdef>
+ <pagedocs/>
+ <inlineclasses title=""/>
+ <defines title=""/>
+ <typedefs title=""/>
+ <enums title=""/>
+ <enumvalues title=""/>
+ <functions title=""/>
+ <variables title=""/>
+ <signals title=""/>
+ <publicslots title=""/>
+ <protectedslots title=""/>
+ <privateslots title=""/>
+ <events title=""/>
+ <properties title=""/>
+ <friends title=""/>
+ </memberdef>
+ <authorsection visible="yes"/>
+ </group>
+
+ <!-- Layout definition for a directory page -->
+ <directory>
+ <briefdescription visible="yes"/>
+ <directorygraph visible="yes"/>
+ <detaileddescription title=""/>
+ <memberdecl>
+ <dirs visible="yes"/>
+ <files visible="yes"/>
+ </memberdecl>
+<!-- <detaileddescription title=""/> -->
+ </directory>
+
+</doxygenlayout>
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/General/DoxygenOnAddon.dox b/xbmc/addons/kodi-dev-kit/doxygen/General/DoxygenOnAddon.dox
new file mode 100644
index 0000000..29fa6ee
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/General/DoxygenOnAddon.dox
@@ -0,0 +1,90 @@
+/*!
+
+@page Doxygen_On_Addon Doxygen on Kodi's Add-On headers
+
+### This page is for notes on using Doxygen to document the Kodi's Add-On headers source code.
+
+[Doxygen](http://www.stack.nl/~dimitri/doxygen/index.html), is a documentation
+system for C++, C, Java, and some other weird languages. It can generate html
+docs documenting a projects source code, by either extracting special tags from
+the source code (put there by people wanting to make use of doxygen), or doxygen
+attempts to build documentation from existing source.
+
+Doxygen seems to be installed on the NMR systems, type:
+~~~~~~~~~~~~~
+doxygen --version
+~~~~~~~~~~~~~
+
+
+_ _ _
+
+Start doxygen documentation for add-ons always with `///` and on Kodi itself with `/*!`, this makes it more easy to see for which place the documentation is.
+
+<b>Here an example on add-on about function coding style:</b>
+
+\verbatim
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief Sets the resolution
+ ///
+ /// That the coordinates of all controls are defined in. Allows Kodi
+ /// to scale control positions and width/heights to whatever resolution
+ /// Kodi is currently using.
+ ///
+ /// @param[in] res Coordinate resolution to set
+ /// Resolution is one of the following:
+ /// | value | Resolution |
+ /// |:-----:|:--------------------------|
+ /// | 0 | 1080i (1920x1080)
+ /// | 1 | 720p (1280x720)
+ /// | 2 | 480p 4:3 (720x480)
+ /// | 3 | 480p 16:9 (720x480)
+ /// | 4 | NTSC 4:3 (720x480)
+ /// | 5 | NTSC 16:9 (720x480)
+ /// | 6 | PAL 4:3 (720x576)
+ /// | 7 | PAL 16:9 (720x576)
+ /// | 8 | PAL60 4:3 (720x480)
+ /// | 9 | PAL60 16:9 (720x480)
+ /// @return Nothing only added as example here :)
+ /// @param[out] nothingExample Example here, if on value pointer data becomes
+ /// returned.
+ /// @throws TypeError If supplied argument is not of List type, or a
+ /// control is not of Control type
+ /// @throws ReferenceError If control is already used in another window
+ /// @throws RuntimeError Should not happen :-)
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// win = xbmcgui.Window(xbmcgui.getCurrentWindowId())
+ /// win.setCoordinateResolution(0)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setCoordinateResolution(...);
+#else
+ SWIGHIDDENVIRTUAL bool setCoordinateResolution(long res, int &nothingExample);
+#endif
+\endverbatim
+- \verbatim /// \ingroup\endverbatim - Define the group where the documentation part comes in.
+- \verbatim /// @brief\endverbatim - Add a small text of part there.
+- \verbatim /// TEXT_FIELD\endverbatim - Add a bigger text there if needed.
+- \verbatim /// @param[in] VALUE_NAME VALUE_TEXT\endverbatim - To set input parameter defined by name and add a description. There the example also add a small table which is useful to describe values.
+- \verbatim /// @param[out] VALUE_NAME VALUE_TEXT\endverbatim - To set output parameter defined by name and add a description.
+- \verbatim /// @return VALUE_TEXT\endverbatim - To add a description of return value.
+- \verbatim /// @throws ERROR_TYPE ERROR_TEXT\endverbatim - If also exception becomes handled, can you use this for description.
+- \verbatim /// TEXT_FIELD\endverbatim - Add a much bigger text there if needed.
+- \verbatim /// ------------------\endverbatim - Use this to define a field line, e.g. if you add example add this always before, further must you make two empty lines before to prevent add of them on string before!
+- \verbatim /// ~~~~~~~~~~~~~ \endverbatim - Here can you define a code example which must start and end with the definition string, also can you define the code style with e.g. <b>{.py}</b> for Python or <b>{.cpp}</b> for CPP code on the first line of them.
+
+@note Start all `VALUE_TEXT` at same character to hold a clean code on <c>*.cpp</c> or <c>*.h</c> files.\n\n
+ The `#ifdef DOXYGEN_SHOULD_USE_THIS` on example above can be becomes used
+ if for Doxygen another function is needed to describe.
+
+If you want to prevent a part from doxygen can you define <b>`#ifndef DOXYGEN_SHOULD_SKIP_THIS`</b>
+or <b>`#ifdef DOXYGEN_SHOULD_USE_THIS`</b> on the related code.
+*/
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/General/General.dox b/xbmc/addons/kodi-dev-kit/doxygen/General/General.dox
new file mode 100644
index 0000000..9862eb6
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/General/General.dox
@@ -0,0 +1,31 @@
+/*!
+
+\page general General
+\brief \doc_header{ General descriptions }
+
+The used code guidelines from Kodi
+@note Is not direct needed on C++ add-ons but makes it more easy for reviews and
+changes from the others.
+
+\subpage code_guidelines
+
+--------------------------------------------------------------------------------
+
+Guideline for Kodi's developers to create documentation
+
+\subpage Doxygen_On_Addon
+
+*/
+
+---------------------------------------------------------------------------------
+
+@subpage revisions "Revisions against older versions"
+*/
+
+/*!
+\page revisions Revisions
+
+\subpage python_revisions
+
+\subpage skinning_revisions
+*/ \ No newline at end of file
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/cpp_kodi_addon_vfs_protocol_1.png b/xbmc/addons/kodi-dev-kit/doxygen/Modules/cpp_kodi_addon_vfs_protocol_1.png
new file mode 100644
index 0000000..46f9b8c
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/cpp_kodi_addon_vfs_protocol_1.png
Binary files differ
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/cpp_kodi_addon_vfs_protocol_2.png b/xbmc/addons/kodi-dev-kit/doxygen/Modules/cpp_kodi_addon_vfs_protocol_2.png
new file mode 100644
index 0000000..7af0abf
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/cpp_kodi_addon_vfs_protocol_2.png
Binary files differ
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/logo-cpp.png b/xbmc/addons/kodi-dev-kit/doxygen/Modules/logo-cpp.png
new file mode 100644
index 0000000..870065e
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/logo-cpp.png
Binary files differ
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/logo-python.png b/xbmc/addons/kodi-dev-kit/doxygen/Modules/logo-python.png
new file mode 100644
index 0000000..179a2ae
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/logo-python.png
Binary files differ
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp.dox
new file mode 100644
index 0000000..0967bdd
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp.dox
@@ -0,0 +1,165 @@
+/*!
+
+\defgroup cpp C++
+\image html logo-cpp.png
+\brief \htmlonly
+ <h3><span style="text-decoration: underline;"><span style="font-style: italic;"><span
+ style="color: rgb(102, 102, 102);">C++ Binary Add-On Development</span></span></span></h3>
+ \endhtmlonly
+
+Kodi allows the use of **C++** addons to expand their possibilities. This gives you
+many options to expand it quickly and easily with new features.
+
+Here is a table of the types currently possible:
+\table_start
+ \table_h3{ Type, Class, Description }
+ \table_row3{ <b>Audio decoder</b>,
+ @ref cpp_kodi_addon_audiodecoder "kodi::addon::CInstanceAudioDecoder",
+ For audio decoders as binary add-ons. This class implements a way to handle
+ special types of audio files.
+ }
+ \table_row3{ <b>Audio encoder</b>,
+ @ref cpp_kodi_addon_audioencoder "kodi::addon::CInstanceAudioEncoder",
+ To convert audio data\, such as a CD\, to a new format and create files for this.
+ }
+ \table_row3{ <b>Game engine</b>,
+ @ref cpp_kodi_addon_game "kodi::addon::CInstanceGame",
+ This is to provide support for games in the system.
+ }
+ \table_row3{ <b>Image decoder</b>,
+ @ref cpp_kodi_addon_imagedecoder "kodi::addon::CInstanceImageDecoder",
+ This instance type is used to allow Kodi various additional image format types.
+ }
+ \table_row3{ <b>Inputstream</b>,
+ @ref cpp_kodi_addon_inputstream "kodi::addon::CInstanceInputStream",
+ To process special video and/or audio streams. This allows addons to support newer species in Kodi.
+ }
+ \table_row3{ <b>Peripheral</b>,
+ @ref cpp_kodi_addon_peripheral "kodi::addon::CInstancePeripheral",
+ Made to have Kodi controllable with new and different ways\, such as Joystick.
+ }
+ \table_row3{ <b>Screensaver</b>,
+ @ref cpp_kodi_addon_screensaver "kodi::addon::CInstanceScreensaver",
+ A screensaver is a Kodi addon that fills the screen with moving images or
+ patterns when the computer is not in use.
+ }
+ \table_row3{ <b>PVR (TV / radio handling)</b>,
+ @ref cpp_kodi_addon_pvr "kodi::addon::CInstancePVRClient",
+ Used to provide Kodi with various sources for TV and radio streams.
+ }
+ \table_row3{ <b>VFS (Virtual filesystem support)</b>,
+ @ref cpp_kodi_addon_vfs "kodi::addon::CInstanceVFS",
+ This instance type is used to allow Kodi various additional file system types.
+ }
+ \table_row3{ <b>Video codec</b>,
+ @ref cpp_kodi_addon_videocodec "kodi::addon::CInstanceVideoCodec",
+ Used to decode special video formats and make them usable for playback.
+ @note This can only be used in connection with @ref cpp_kodi_addon_inputstream "kodi::addon::CInstanceInputStream".
+ }
+ \table_row3{ <b>Visualization</b>,
+ @ref cpp_kodi_addon_visualization "kodi::addon::CInstanceVisualization",
+ Music visualization is a feature in Kodi that generates animated imagery based on a piece of music.
+ }
+\table_end
+*/
+/*!
+\defgroup cpp_cmake CMake addon creation structure
+\ingroup cpp
+\brief **CMake help macros to create addon for Kodi**
+*/
+/*!
+\defgroup cpp_kodi Interface - kodi
+\ingroup cpp
+\brief **General addon interface functions**
+*/
+ /*!
+ \defgroup cpp_kodi_Defs Definitions, structures and enumerators
+ \ingroup cpp_kodi
+ @brief **General definition values**
+ */
+/*!
+\defgroup cpp_kodi_addon Interface - kodi::addon
+\ingroup cpp
+\brief **Addon type interface functions and classes**
+*/
+ /*!
+ \defgroup cpp_kodi_addon_addonbase class CAddonBase
+ \ingroup cpp_kodi_addon
+ */
+ /*!
+ \defgroup cpp_kodi_addon_addonbase_Defs Definitions, structures and enumerators
+ \ingroup cpp_kodi_addon_addonbase
+ @brief **General definition values**
+ */
+ /*!
+ \defgroup cpp_kodi_addon_instances Addon type instances
+ \brief **Group of possible processing instances which can be made available by an add-on**\n
+ Kodi enables numerous different ways in which the necessary documentation is included in this group.
+ \ingroup cpp_kodi_addon
+ */
+ /*!
+ \defgroup cpp_kodi_addon_audiodecoder Audio Decoder
+ \ingroup cpp_kodi_addon_instances
+ */
+ /*!
+ \defgroup cpp_kodi_addon_audioencoder Audio Encoder
+ \ingroup cpp_kodi_addon_instances
+ */
+ /*!
+ \defgroup cpp_kodi_addon_game Game
+ \ingroup cpp_kodi_addon_instances
+ */
+ /*!
+ \defgroup cpp_kodi_addon_imagedecoder Image Decoder
+ \ingroup cpp_kodi_addon_instances
+ */
+ /*!
+ \defgroup cpp_kodi_addon_inputstream Inputstream
+ \ingroup cpp_kodi_addon_instances
+ */
+ /*!
+ \defgroup cpp_kodi_addon_peripheral Peripheral
+ \ingroup cpp_kodi_addon_instances
+ */
+ /*!
+ \defgroup cpp_kodi_addon_pvr PVR
+ \ingroup cpp_kodi_addon_instances
+ */
+ /*!
+ \defgroup cpp_kodi_addon_screensaver Screensaver
+ \ingroup cpp_kodi_addon_instances
+ */
+ /*!
+ \defgroup cpp_kodi_addon_vfs VFS
+ \ingroup cpp_kodi_addon_instances
+ */
+ /*!
+ \defgroup cpp_kodi_addon_videocodec Video Codec
+ \ingroup cpp_kodi_addon_instances
+ */
+ /*!
+ \defgroup cpp_kodi_addon_visualization Visualization
+ \ingroup cpp_kodi_addon_instances
+ */
+/*!
+\defgroup cpp_kodi_gui Interface - kodi::gui
+\ingroup cpp
+\brief **Graphical functions for Windows and Dialogs to show**\n
+Offers classes and functions that manipulate the Graphical User Interface
+through windows, dialogs, and various control widgets.
+*/
+/*!
+\defgroup cpp_kodi_platform Interface - kodi::platform
+\ingroup cpp
+\brief **Platform specific functions**\n
+This group contains OS platform specific functions with which Kodi is accessed.
+
+@note Its header and its classes and functions are only available in the
+associated OS and are not available outside.
+*/
+/*!
+\defgroup cpp_kodi_tools Interface - kodi::tools
+\ingroup cpp
+\brief **Helper tools and functions**\n
+This group includes things that only work indirectly with Kodi.
+*/
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_gui.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_gui.dox
new file mode 100644
index 0000000..4950766
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_gui.dox
@@ -0,0 +1,96 @@
+/*!
+@defgroup cpp_kodi_gui_Defs Definitions, structures and enumerators
+@ingroup cpp_kodi_gui
+@brief **GUI add-on interface definition values**\n
+All GUI functions associated data structures.
+
+Used to exchange the available options between Kodi and addon.\n
+The groups described here correspond to the groups of functions on GUI.
+*/
+
+
+/*!
+@defgroup cpp_kodi_gui_general 1. General
+@ingroup cpp_kodi_gui
+@brief **General GUI related functions**\n
+This includes independent functions which can be used by different locations and
+called up independently.
+
+*/
+
+/*!
+@defgroup cpp_kodi_gui_dialogs 2. Dialogs
+@ingroup cpp_kodi_gui
+@brief **Different GUI dialog for user queries**\n
+This is where the individual dialogs possible for addons are carried out, with
+which any user access can be given, e.g. Yes/No dialog.
+
+
+*/
+
+/*!
+@defgroup cpp_kodi_gui_windows 3. Windows
+@ingroup cpp_kodi_gui
+@brief **Classes and data for displaying a window in Kodi**\n
+This group contains the primary class @ref cpp_kodi_gui_windows_window "kodi::gui::CWindow"
+and also various subclasses belonging to it (various controls, list item).
+
+Kodi is noted as having a very flexible and robust framework for its GUI,
+making theme-skinning and personal customization very accessible. Users
+can create their own skin (or modify an existing skin) and share it with
+others.
+
+This class is used to process the display of a window in Kodi from the addon.
+
+The addon can process the controls stored in the XML and lists displayed in the
+GUI, set values and manage user access.
+
+*/
+
+/*!
+@defgroup cpp_kodi_gui_windows_window 1. GUI window (kodi::gui::CWindow)
+@ingroup cpp_kodi_gui_windows
+*/
+
+/*!
+@defgroup cpp_kodi_gui_windows_listitem 2. GUI list item (kodi::gui::CListItem)
+@ingroup cpp_kodi_gui_windows
+*/
+
+/*!
+@defgroup cpp_kodi_gui_windows_controls 3. GUI controls (kodi::gui::controls::C...)
+@ingroup cpp_kodi_gui_windows
+@brief @cpp_namespace{ kodi::gui::controls }
+<b>GUI control elements</b>\n
+This group contains classes which are used in @ref cpp_kodi_gui_windows_window "kodi::gui::CWindow"
+to edit associated skin control elements, be it to set or get their values, or
+to make them visible or hidden.
+
+See @ref skin_parts for a detailed description of the skin XML parts accessed
+from here.
+
+In order to access a control in skin XML using an add-on, it must have an id,
+otherwise an add-on cannot access it.
+
+~~~~~~~~~~~~~~~xml
+ <control type="..." id="1"> <!-- Id's defined here to use on addon -->
+ ...
+ </control>
+~~~~~~~~~~~~~~~
+
+@note These classes from here can only be used together with the associated
+window and cannot be used independently.
+
+
+*/
+
+
+/*!
+@defgroup cpp_kodi_gui_helpers 4. Helpers
+@ingroup cpp_kodi_gui
+@brief **Auxiliary classes for processing the GUI within the addon**\n
+The auxiliary functions and classes stored here only work indirectly with Kodi
+and are mostly only intended to simplify an add-on development.
+
+
+*/
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral.dox
new file mode 100644
index 0000000..dfc2b0a
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral.dox
@@ -0,0 +1,414 @@
+/*!
+
+@defgroup cpp_kodi_addon_peripheral_Docs Peripheral system
+@ingroup cpp_kodi_addon_peripheral
+@brief System description
+
+*/
+
+//------------------------------------------------------------------------------
+
+
+/*!
+
+@defgroup cpp_kodi_addon_peripheral_Docs_ControllerInput Controller Input
+@ingroup cpp_kodi_addon_peripheral_Docs
+@brief Controller Input for Emulator Development
+
+# Introduction
+
+Input can come from many types of peripherals such as controllers, arcade cabinets, keyboards and remotes. However, the emulator needs to emulate the peripherals of its game platform. For example, NES emulators can emulate controllers, light guns and flight simulator joysticks. This system translates input from hardware peripherals to emulated ones.
+
+## Table of contents
+
+1. @ref cpp_kodi_addon_peripheral_Docs_ControllerInput_Profiles "Controller profiles"
+2. @ref cpp_kodi_addon_peripheral_Docs_ControllerInput_JoystickDrivers "Joystick drivers"
+3. @ref cpp_kodi_addon_peripheral_Docs_ControllerInput_ButtonMaps "Button maps"
+4. @ref cpp_kodi_addon_peripheral_Docs_ControllerInput_JoystickDriverFuckery "Joystick driver fuckery"
+
+*/
+
+/*!
+
+@defgroup cpp_kodi_addon_peripheral_Docs_ControllerInput_Profiles 1. Controller profiles
+@ingroup cpp_kodi_addon_peripheral_Docs_ControllerInput
+
+# 1. Controller profiles
+
+<img src="modules_cpp_peripheral_Docs_ControllerInput_1.jpg" width="100%">
+
+Each emulated peripheral has a profile describing its input. Controller is a generic word for these peripherals, and this is the name shown in the GUI.
+
+Each controller has parts that generate input, like buttons, keys, triggers, analog sticks and accelerometers. These parts are called features of the controller.
+
+Features generate input in several different ways. For example, a button is either 1 or 0. Analog sticks, on the other hand, have two degrees of freedom. They contain two values that can range from -1.0 (fully down/left) to 1.0 (fully up/right).
+
+Features can also receive input. Rumble motors and other haptic parts accept digital or analog input.
+
+Features are grouped by the type of input they generate/accept. For example:
+
+<i>Scalar features</i>
+- Regular buttons generate a single number (either 0 or 1), so these are called scalar features.
+- Likewise, triggers and pressure-sensitive buttons generate a single number (analog value between 0.0 and 1.0, inclusive). These are also called scalar features.
+- For simplicity, keys are considered buttons
+- D-pads, also called hats, are treated as four buttons, so they result in four scalar features.
+
+<i>Vector features</i>
+- Analog sticks generate two analog values, so these are called vector features.
+- Likewise, accelerometers have an X, Y and Z axis and are also called vector features.
+
+<i>Haptic features</i>
+- Motors are technically scalar features, but because they accept input instead of generating it, they're usually processed in a different part of the code. For clarity, they are just called haptic features.
+
+*/
+
+/*!
+
+@defgroup cpp_kodi_addon_peripheral_Docs_ControllerInput_JoystickDrivers 2. Joystick drivers
+@ingroup cpp_kodi_addon_peripheral_Docs_ControllerInput
+
+# 2. Joystick drivers
+
+Unfortunately, none of the input provided by any joystick driver has information about the kind of features it belongs to. Drivers simply provide a long list of values. Generally, these are bools or floats, but some interfaces use enums, like the directions on a d-pad.
+
+To connect this information to the features of a controller profile, it is split into elements that better translate to controller features. These elements are called driver primitives.
+
+## 2.1. Types of driver primitives
+
+Buttons
+
+Consider a bool reported by the driver. This probably belongs to something that can be pressed, so it's called a button. A bool can't belong to multiple features, so it is a driver primitive.
+
+<i>Hat directions</i>
+
+Some drivers use enums or bit flags to report hat presses. In Kodi, hats are treated like four separate buttons for simplicity. Hat enums can belong to four features, so they contain four driver primitives, one for each direction.
+
+<i>Semiaxis directions</i>
+
+A float is a little trickier. The immediate assumption is an axis of an analog stick or accelerometer. However, in DirectInput, triggers are combined into a single float. Therefore, a float can map to two features, so each half of the axis (called a semiaxis) is a driver primitive.
+
+This has interesting implications. An analog stick has two axes, which is four driver primitives. Each semiaxis can map to a different feature, so the analog stick is able to emulate the four buttons of a d-pad, or the N64's C buttons.
+
+*/
+
+/*!
+
+@defgroup cpp_kodi_addon_peripheral_Docs_ControllerInput_ButtonMaps 3. Button maps
+@ingroup cpp_kodi_addon_peripheral_Docs_ControllerInput
+
+# 3. Button maps
+
+Now we return to controller profiles. Each emulator requires its own set of controllers, each with their list of features. These are mapped to the underlying driver primitives provided by the joystick driver using a button map.
+
+In the code, the button map is abstracted away behind a button map interface. This additional abstraction adds some code, but it allows button maps to be managed by an add-on.
+
+The current add-on for button maps, [peripheral.joystick](https://github.com/kodi-game/peripheral.joystick), can only recite button maps it has been given. However, it could be made smarter by using existing button map data. For example, if the add-on knows the most popular way to map a 360 controller to a SNES controller, it can generate a SNES button map knowing only the 360 one.
+
+*/
+
+/*!
+
+@defgroup cpp_kodi_addon_peripheral_Docs_ControllerInput_JoystickDriverFuckery 4. Joystick driver fuckery
+@ingroup cpp_kodi_addon_peripheral_Docs_ControllerInput
+
+# 4. Joystick driver fuckery
+
+Of course, joystick drivers have many quirks that greatly complicate things. So much so that they deserve their own chapter. Here's a list of some of the quirks I've encountered:
+
+### Combined triggers
+
+DirectInput combines left and right triggers into a single axis. They are combined using the strategy in Chapter 4: Dimension Reduction.
+
+Kodi solves this by splitting the axis into two semiaxes, as explained in Chapter 2: Joystick drivers. Each semiaxis is mapped to its own trigger.
+
+### Anomalous triggers
+
+Not all triggers start at 0.0 and travel to 1.0 (or -1.0 in DirectInput). Some triggers start at 1.0 or -1.0, and travel to 0.0 or to the opposite unit. These are called <i>anomalous triggers</i>. These triggers have two properties:
+Center - The theory here is that initial perturbations are minimal. This means that the center is determined by rounding the first value to the closest int.
+Range - The range can be half range (assumed) or full range (detected when a value has the opposite sign)
+
+### Discrete D-pads
+
+Instead of four buttons or a hat enum, D-pads can sometimes be reported as floats that use the discrete values -1.0, 0.0 and 1.0. Fortunately, because analog sticks can emulate D-pads, we can simply treat the discrete D-pad as an analog stick.
+
+### Repeated input
+
+Some buttons generate two input events. For example, some hats operate as four digital buttons AND as a discrete D-pad. This is solved via a "cooldown" while mapping, which ignores any input for around 50ms after a button is mapped.
+
+### Hat enums
+
+I consider hat enums a quirk because it just makes so much more sense to represent them using four buttons. It doesn't even guarantee mutual exclusion between opposite directions, as this can be violated by a flag with the improper bits set.
+
+### Pressure-sensitive buttons
+
+Pressure-sensitive buttons can be reported as an analog axis instead of a digital value.
+
+### Incomplete information
+
+Pertinent info (name, USB VID and PID, etc) might be missing, making it hard to identify the correct button map.
+
+*/
+
+//------------------------------------------------------------------------------
+
+/*!
+
+@defgroup cpp_kodi_addon_peripheral_Docs_Lifetime Lifetime of a button press
+@ingroup cpp_kodi_addon_peripheral_Docs
+@brief The lifetime of a button press for peripherals with input
+
+# Introduction
+
+@note This documentation assumes you have the source code for Kodi and <b>peripheral.joystick</b> open in front of you. The fixed-width font is the class or struct you should reference.
+
+When a controller is plugged in and a button is pressed, it starts a large chain reaction of different systems. The button's journey is destined to reach one or more eventual outcomes:
+- An emulator / game add-on
+- Kodi's input system
+- A configuration utility
+
+Game input
+----------------
+
+Instead of being forced to use a single controller abstraction, game add-ons can request input in the form of any platform controller known to Kodi. For example, a NES emulator receives input events for a NES controller; if the emulator doesn't care, it receives events for a default 360-style controller.
+
+Kodi's input system
+----------------
+
+If no game is receiving input in fullscreen mode, the button is translated to the default 360-style controller and Kodi's keymapping system takes over from here
+
+Configuration utilities
+----------------
+
+A configuration utility needs know the button's driver index in order to map it to a feature on its controller. In addition to raw driver events, it also needs to promiscuously monitor the input translated to its platform controller to highlight features in the GUI as they're pressed.
+
+The final system can handle any number of these input listeners, monitoring input in the form of any number of platform controllers
+
+@include{doc} Modules/modules_cpp_peripheral_lifetime_diagram_1.dox
+
+> some class names are outdated, sorry
+
+*/
+
+/*!
+@defgroup cpp_kodi_addon_peripheral_Docs_Lifetime_Scanning 1. Scanning for peripherals
+@ingroup cpp_kodi_addon_peripheral_Docs_Lifetime
+
+# 1. Scanning for peripherals
+
+When Kodi starts up, the peripherals subsystem does a scan for peripherals. Kodi supports several busses including USB. Two new virtual busses have been added:
+- Add-on bus - joysticks are provided by peripheral library binary add-ons
+- Application bus - provides the keyboard, as keyboard input comes from the logical level of the application, not a specific keyboard
+
+This chart shows joysticks being scanned through the peripheral API. The keyboard is always assumed to be attached.
+
+@include{doc} Modules/modules_cpp_peripheral_lifetime_diagram_2.dox
+
+*/
+
+/*!
+@defgroup cpp_kodi_addon_peripheral_Docs_Lifetime_Receiving 2. Receiving joystick events
+@ingroup cpp_kodi_addon_peripheral_Docs_Lifetime
+
+# 2. Receiving joystick events
+
+The peripherals subsystem asks the peripheral add-ons for events every frame. Joysticks are polled by the add-on and changes in state are sent back to the peripherals subsystem.
+
+@include{doc} Modules/modules_cpp_peripheral_lifetime_diagram_3.dox
+
+*/
+
+/*!
+@defgroup cpp_kodi_addon_peripheral_Docs_Lifetime_Events 3. Handling events
+@ingroup cpp_kodi_addon_peripheral_Docs_Lifetime
+
+# 3. Handling events
+
+Joystick drivers present three kinds of elements:
+- Buttons
+- Hats
+- Axes
+
+Most of the time we want to know how these map to a particular system's controller (Kodi uses the Xbox 360 profile).
+The controller configuration GUI, however, also needs access to the raw driver elements. There is an event handler for both of these situations.
+
+@include{doc} Modules/modules_cpp_peripheral_lifetime_diagram_4.dox
+
+@note CGenericJoystickInputHandling and CGenericJoystickButtonMapping are now called CInputHandling and CButtonMapping, respectively.
+
+*/
+
+/*!
+@defgroup cpp_kodi_addon_peripheral_Docs_Lifetime_Input 4. Handling input for the Game API
+@ingroup cpp_kodi_addon_peripheral_Docs_Lifetime
+
+# 4. Handling input for the Game API
+
+When a port is opened by the Game API, it is assigned an input handler that receives driver events involving driver
+elements, and dispatches events in the form of the desired controller.
+
+@include{doc} Modules/modules_cpp_peripheral_lifetime_diagram_5.dox
+
+*/
+
+/*!
+@defgroup cpp_kodi_addon_peripheral_Docs_Lifetime_DriverElements 5. Mapping driver elements to the system's controller
+@ingroup cpp_kodi_addon_peripheral_Docs_Lifetime
+
+# 5. Mapping driver elements to the system's controller
+
+This input handler uses an internal button map to translate driver elements to the desired system controller.
+Currently, all button mapping and translating is done inside peripheral add-ons.
+
+@include{doc} Modules/modules_cpp_peripheral_lifetime_diagram_6.dox
+
+A sample **buttonmap.xml** from the add-on's userdata folder `userdata/addon_data/peripheral.joystick`:
+
+~~~~~~~~~~~~~~~~xml
+<?xml version="1.0" ?>
+<buttonmap>
+ <devices>
+ <device name="Keyboard" provider="application">
+ <controller id="game.controller.default">
+ <feature name="x" button="128" />
+ </controller>
+ <controller id="game.controller.nes">
+ <feature name="a" button="90" />
+ <feature name="b" button="88" />
+ <feature name="down" button="129" />
+ <feature name="left" button="130" />
+ <feature name="right" button="131" />
+ <feature name="select" button="92" />
+ <feature name="start" button="13" />
+ <feature name="up" button="128" />
+ </controller>
+ </device>
+ <device name="Gamepad F310" provider="cocoa" vid="1133" pid="1133">
+ <controller id="game.controller.default">
+ <feature name="a" button="0" />
+ <feature name="b" button="1" />
+ <feature name="x" button="2" />
+ <feature name="y" button="3" />
+ <feature name="lefttrigger" axis="-4" />
+ <feature name="righttrigger" axis="-5" />
+ </controller>
+ </device>
+ </devices>
+</buttonmap>
+~~~~~~~~~~~~~~~~
+*/
+
+/*!
+@defgroup cpp_kodi_addon_peripheral_Docs_Lifetime_Translating 6. Translating Kodi controller to libretro's "RetroPad"
+@ingroup cpp_kodi_addon_peripheral_Docs_Lifetime
+
+# 6. Translating Kodi controller to libretro's "RetroPad"
+
+Unfortunately, libretro uses a fixed controller (called the RetroPad) for all of its cores, instead of a
+separate controller per system like Kodi. Therefore, each core needs a map of how its Kodi controller
+translates to the RetroPad.
+
+
+@include{doc} Modules/modules_cpp_peripheral_lifetime_diagram_7.dox
+
+~~~~~~~~~~~~~~~~xml
+<?xml version="1.0" encoding="UTF-8"?>
+<buttonmap>
+ <controller id="game.controller.nes" type="joypad">
+ <feature name="a" mapto="a"/>
+ <feature name="b" mapto="b"/>
+ <feature name="start" mapto="start"/>
+ <feature name="select" mapto="select"/>
+ <feature name="up" mapto="up"/>
+ <feature name="down" mapto="down"/>
+ <feature name="right" mapto="right"/>
+ <feature name="left" mapto="left"/>
+ </controller>
+ <controller id="game.controller.nes.zapper" type="lightgun">
+ <feature name="trigger" mapto="trigger"/>
+ </controller>
+</buttonmap>
+~~~~~~~~~~~~~~~~
+*/
+
+/*!
+@defgroup cpp_kodi_addon_peripheral_Docs_Lifetime_KodiInput 7. Kodi Input
+@ingroup cpp_kodi_addon_peripheral_Docs_Lifetime
+
+# 7. Kodi Input
+
+If the event isn't handled by the Game API, it is sent to a default controller (basically a Xbox 360 controller).
+The default controller translates it to a key for the file joystick.xml.
+
+@include{doc} Modules/modules_cpp_peripheral_lifetime_diagram_8.dox
+
+joystick.xml belongs to Kodi's [keymap system](http://kodi.wiki/view/Keymap). It maps a standardized Xbox
+360 controller to Kodi actions. It looks something like this:
+~~~~~~~~~~~~~~~~xml
+<?xml version="1.0" encoding="UTF-8"?>
+<keymap>
+ <global>
+ <joystick>
+ <a>Select</a>
+ <b>Back</b>
+ <x>ContextMenu</x>
+ <y>FullScreen</y>
+ <start>ActivateWindow(PlayerControls)</start>
+ <back>Back</back>
+ <left>Left</left>
+ <right>Right</right>
+ <up>Up</up>
+ <down>Down</down>
+ <leftthumbbutton>Screenshot</leftthumbbutton>
+ <rightthumbbutton>ActivateWindow(ShutdownMenu)</rightthumbbutton>
+ <leftstickleft>AnalogSeekBack</leftstickleft>
+ <leftstickright>AnalogSeekForward</leftstickright>
+ <rightstickup>VolumeUp</rightstickup>
+ <rightstickdown>VolumeDown</rightstickdown>
+ <guide>ActivateWindow(Home)</guide>
+ <lefttrigger>ScrollUp</lefttrigger>
+ <righttrigger>ScrollDown</righttrigger>
+ </joystick>
+ </global>
+</keymap>
+~~~~~~~~~~~~~~~~
+
+Previously, this was 4,000 lines of XML across many files that encoded a large database of how the
+buttons/hats/axes reported by the driver map to Kodi actions depending on driver name and platform.
+This mass of data has been moved to a peripheral add-on; joystick keymapping is now a much more
+pleasant experience.
+
+*/
+
+/*!
+@defgroup cpp_kodi_addon_peripheral_Docs_Lifetime_ButtonMapping 8. Button mapping (controller configuration)
+@ingroup cpp_kodi_addon_peripheral_Docs_Lifetime
+
+# 8. Button mapping (controller configuration)
+
+When the controller's button dialog wants the user to press a button, it creates a button mapper for the controller it belongs to.
+
+<img src="modules_cpp_peripheral_Docs_Lifetime_9.png" width="100%">
+
+It then waits for an event. When the event arrives, the dialog tells its button mapper to map the event to the controller feature being mapped.
+
+@include{doc} Modules/modules_cpp_peripheral_lifetime_diagram_9.dox
+
+*/
+
+/*!
+@defgroup cpp_kodi_addon_peripheral_Docs_Lifetime_KeyboardInput 9. Keyboard input
+@ingroup cpp_kodi_addon_peripheral_Docs_Lifetime
+
+# 9. Keyboard input
+
+Keyboard input is really quite simple. It just emulates a joystick with a large number of buttons.
+Any event handler down the chain that can handle joystick input can request to monitor the keyboard
+as a raw device or as any available controller profile.
+
+NES emulators, for example, don't know if their NES controller is being emulated by a joystick or a keyboard. The event handlers are identical for both.
+
+When the controller's button dialog wants the user to press a button, it creates a button mapper for the controller it belongs to.
+
+@include{doc} Modules/modules_cpp_peripheral_lifetime_diagram_10.dox
+
+*/
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_Docs_ControllerInput_1.jpg b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_Docs_ControllerInput_1.jpg
new file mode 100644
index 0000000..655b442
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_Docs_ControllerInput_1.jpg
Binary files differ
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_Docs_Lifetime_9.png b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_Docs_Lifetime_9.png
new file mode 100644
index 0000000..44a1de2
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_Docs_Lifetime_9.png
Binary files differ
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_1.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_1.dox
new file mode 100644
index 0000000..43849f0
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_1.dox
@@ -0,0 +1,294 @@
+@dot
+digraph D {
+ graph [label="Orthogonal edges", splines=ortho, nodesep=1.0];
+ node [shape=box fontname=Arial];
+
+ rankdir=LR;
+ color = "white"
+ bgcolor = "white"
+ fillcolor = "white"
+ fontcolor = "white"
+ pencolor = "white"
+
+ _1 [
+ label = <<b>Joystick</b>>
+ ];
+ _2 [
+ label = <<b>Joystick<br/>Event</b>>
+ ];
+ _3 [
+ label = <<b>Keyboard</b>>
+ ];
+ _4 [
+ label = <<b>Keyboard<br/>Event</b>>
+ ];
+
+ _5 [
+ label = <<b>Joystick</b><br/><font color="#777777"><i>CJoystick</i></font>>;
+ ]
+ _6 [
+ label = <<b>Peripheral Event</b><br/><font color="#777777"><i>kodi::addon::PeripheralEvent</i></font>>;
+ ]
+ _7 [
+ label = <<b>ButtonMap</b><br/><font color="#777777"><i>kodi::addon::JoystickFeatures*</i></font>>;
+ ]
+
+ _8 [
+ label = <<b>C struct</b><br/><font color="#777777"><i>JOYSTICK_INFO</i></font>>;
+ ]
+ _9 [
+ label = <<b>C struct</b><br/><font color="#777777"><i>PERIPHERAL_EVENT</i></font>>;
+ ]
+ _10 [
+ label = <<b>C structs</b><br/><font color="#777777"><i>JOYSTICK_FEATURE*</i></font>>;
+ ]
+
+ _11 [
+ label = <<b>Joystick Peripheral</b><br/><font color="#777777"><i>CPeripheralJoystick</i></font>>
+ ]
+
+ _12 [
+ label = <<b>Event Handling</b><br/><font color="#777777"><i>IJoystickDriverHandler</i></font>>
+ ]
+
+ _13 [
+ label = <<b>Keyboard Peripheral</b><br/><font color="#777777"><i>CPeripheralKeyboard</i></font>>
+ ]
+
+ _14 [
+ label = <<b>Keyboard Handling</b><br/><font color="#777777"><i>IKeyboardHandler</i></font>>
+ ]
+
+ _15 [
+ label = <<b>Input Handling</b><br/><font color="#777777"><i>CGenericJoystickInputHandling</i></font>>
+ ]
+
+ _16 [
+ label = <<b>Button Mapping</b><br/><font color="#777777"><i>CGenericJoystickButtonMapping</i></font>>
+ ]
+
+ _19 [
+ label = <<b>Joystick Imitation</b><br/><font color="#777777"><i>CGenericKeyboadHandler</i></font>>
+ ]
+
+ _20 [
+ label = <<b>Button Map</b><br/><font color="#777777"><i>CAddonJoystickButtonMap</i></font>>
+ ]
+
+ _21 [
+ label = <<b>Game Controller</b><br/><font color="#777777"><i>CControllerInput</i></font>>
+ ]
+
+ _22 [
+ label = <<b>Game Add-on</b><br/><font color="#777777"><i>CGameClient</i></font>>
+ ]
+
+ _23 [
+ label = <<b>Button Dialog</b><br/><font color="#777777"><i>CGUIDialogControllerInput</i></font>>
+ ]
+
+ _24 [
+ label = <<b>Game Controller Add-ons</b><br/><font color="#777777"><i>CGameController</i></font>>
+ ]
+
+ _25 [
+ label = <<b>Default Controller</b><br/><font color="#777777"><i>CDefaultController</i></font>>
+ ]
+
+ _26 [
+ label = <<b>Kodi Input Handler</b><br/><font color="#777777"><i>CButtonKeyHandler</i></font>>
+ ]
+
+ _27 [
+ label = <<b>Button Mapper</b><br/><font color="#777777"><i>CButtonMapper</i></font>>
+ ]
+
+ _28 [
+ label = <<b>Libretro Device</b><br/><font color="#777777"><i>CLibretroDevice</i></font>>
+ ]
+
+ _29 [
+ label = <<b>Libretro Core</b><br/><font color="#777777"><i>CLibretroDll</i></font>>
+ ]
+
+ _1 -> _5 [penwidth=3, weight=25];
+ _2 -> _6 [penwidth=3, weight=2];
+ _3 -> _13 [penwidth=3, weight=100];
+ _4 -> _14 [penwidth=3, weight=15];
+
+ _5 -> _8 [penwidth=3, weight=50];
+ _6 -> _9 [penwidth=3, weight=50];
+ _7 -> _10 [dir=both, penwidth=3, weight=100];
+ _10 -> _20 [dir=both, penwidth=3, weight=2];
+
+ _8 -> _11 [penwidth=3];
+ _9 -> _12 [penwidth=3, weight=21];
+
+ _14 -> _19 [penwidth=3];
+ _19 -> _12 [penwidth=3];
+
+ _12 -> _15 [penwidth=3];
+ _12 -> _16 [penwidth=3];
+
+ _15 -> { _21, _25 } [penwidth=3];
+ _16 -> _23 [penwidth=3, dir=both, weight=0];
+ _23 -> _24 [penwidth=3, dir=back, weight=0];
+ _21 -> _22 [penwidth=3, weight=5];
+ _25 -> _26 [penwidth=3, weight=4];
+ _27 -> _28 -> _29 [penwidth=3, dir=forward, constraint=false, weight=0];
+ _22 -> _28 [penwidth=3, weight=0];
+ _24 -> _21 [penwidth=3, weight=0];
+ edge[constraint=false];
+ _20 -> { _15, _16 } [penwidth=3, weight=5];
+
+
+ subgraph cluster_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>OS</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _1 [style=filled, fillcolor=white];
+ _2 [style=filled, fillcolor=white];
+ _3 [style=filled, fillcolor=white];
+ _4 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_2 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Add-on</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _5 [style=filled, fillcolor=white];
+ _6 [style=filled, fillcolor=white];
+ _7 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral API</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _8 [style=filled, fillcolor=white];
+ _9 [style=filled, fillcolor=white];
+ _10 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_4 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Busses</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ subgraph cluster_4_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Add-on Bus</b></font><br/><font color="#777777"><i>CPeripheralBusAddon</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _11 [style=filled, fillcolor=white];
+ _12 [style=filled, fillcolor=white];
+
+ }
+
+ subgraph cluster_4_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "" [
+ color="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_4_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Application Bus</b></font><br/><font color="#777777"><i>CPeripheralBusApplication</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _13 [style=filled, fillcolor=white];
+ _14 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_7 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Input Library</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+ rank=same;
+
+ subgraph cluster_7_1 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _15 [style=filled, fillcolor=white];
+ _16 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_7_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "-" [
+ color="red"
+ fontcolor="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_7_3 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _19 [style=filled, fillcolor=white];
+ _20 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_8 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Game API</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _21 [style=filled, fillcolor=white];
+ _22 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_9 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Configuration GUI</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _23 [style=filled, fillcolor=white];
+ _24 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_10 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Kodi Input</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _25 [style=filled, fillcolor=white];
+ _26 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_11 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Libretro API Wrapper</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+ rank=same;
+
+ _27 [style=filled, fillcolor=white];
+ _28 [style=filled, fillcolor=white];
+ _29 [style=filled, fillcolor=white];
+ }
+}
+@enddot
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_10.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_10.dox
new file mode 100644
index 0000000..5a1d8bf
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_10.dox
@@ -0,0 +1,149 @@
+@dot
+digraph D {
+ graph [label="Orthogonal edges", splines=ortho, nodesep=1.0];
+ node [shape=box fontname=Arial];
+
+ rankdir=LR;
+ color = "white"
+ bgcolor = "white"
+ fillcolor = "white"
+ fontcolor = "white"
+ pencolor = "white"
+
+ _1 [
+ label = <<b>Joystick</b>>
+ ];
+ _2 [
+ label = <<b>Joystick<br/>Event</b>>
+ ];
+ _3 [
+ label = <<b>Keyboard</b>>
+ ];
+ _4 [
+ label = <<b>Keyboard<br/>Event</b>>
+ ];
+
+ _11 [
+ label = <<b>Joystick Peripheral</b><br/><font color="#777777"><i>CPeripheralJoystick</i></font>>
+ ]
+
+ _12 [
+ label = <<b>Event Handling</b><br/><font color="#777777"><i>IJoystickDriverHandler</i></font>>
+ ]
+
+ _13 [
+ label = <<b>Keyboard Peripheral</b><br/><font color="#777777"><i>CPeripheralKeyboard</i></font>>
+ ]
+
+ _14 [
+ label = <<b>Keyboard Handling</b><br/><font color="#777777"><i>IKeyboardHandler</i></font>>
+ ]
+
+ _15 [
+ label = <<b>Input Handling</b><br/><font color="#777777"><i>CGenericJoystickInputHandling</i></font>>
+ ]
+
+ _16 [
+ label = <<b>Button Mapping</b><br/><font color="#777777"><i>CGenericJoystickButtonMapping</i></font>>
+ ]
+
+ _19 [
+ label = <<b>Joystick Imitation</b><br/><font color="#777777"><i>CGenericKeyboadHandler</i></font>>
+ ]
+
+ _20 [
+ label = <<b>Button Map</b><br/><font color="#777777"><i>CAddonJoystickButtonMap</i></font>>
+ ]
+
+ _4 -> _14 [penwidth=3, weight=-1];
+ _14 -> _19 [penwidth=3, weight=2];
+ _19 -> _12 [penwidth=3, weight=0];
+ _12 -> _15 [penwidth=3, weight=1];
+ _12 -> _16 [penwidth=3, weight=4];
+
+ subgraph cluster_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>OS</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _1 [style=filled, fillcolor=white];
+ _2 [style=filled, fillcolor=white];
+ _3 [style=filled, fillcolor=white];
+ _4 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_4 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Busses</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ subgraph cluster_4_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Add-on Bus</b></font><br/><font color="#777777"><i>CPeripheralBusAddon</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _11 [style=filled, fillcolor=white];
+ _12 [style=filled, fillcolor=white];
+
+ }
+
+ subgraph cluster_4_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "" [
+ color="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_4_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Application Bus</b></font><br/><font color="#777777"><i>CPeripheralBusApplication</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _13 [style=filled, fillcolor=white];
+ _14 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_7 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Input Library</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+ rank=same;
+
+ subgraph cluster_7_1 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _15 [style=filled, fillcolor=white];
+ _16 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_7_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "-" [
+ color="red"
+ fontcolor="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_7_3 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _19 [style=filled, fillcolor=white];
+ _20 [style=filled, fillcolor=white];
+ }
+ }
+}
+@enddot
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_2.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_2.dox
new file mode 100644
index 0000000..96a6474
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_2.dox
@@ -0,0 +1,139 @@
+@dot
+digraph D {
+ graph [label="Orthogonal edges", splines=ortho, nodesep=1.0];
+ node [shape=box fontname=Arial];
+
+ rankdir=LR;
+ color = "white"
+ bgcolor = "white"
+ fillcolor = "white"
+ fontcolor = "white"
+ pencolor = "white"
+
+ _1 [
+ label = <<b>Joystick</b>>
+ ];
+ _2 [
+ label = <<b>Joystick<br/>Event</b>>
+ ];
+ _3 [
+ label = <<b>Keyboard</b>>
+ ];
+ _4 [
+ label = <<b>Keyboard<br/>Event</b>>
+ ];
+
+ _5 [
+ label = <<b>Joystick</b><br/><font color="#777777"><i>CJoystick</i></font>>;
+ ]
+ _6 [
+ label = <<b>Peripheral Event</b><br/><font color="#777777"><i>kodi::addon::PeripheralEvent</i></font>>;
+ ]
+ _7 [
+ label = <<b>ButtonMap</b><br/><font color="#777777"><i>kodi::addon::JoystickFeatures*</i></font>>;
+ ]
+
+ _8 [
+ label = <<b>C struct</b><br/><font color="#777777"><i>JOYSTICK_INFO</i></font>>;
+ ]
+ _9 [
+ label = <<b>C struct</b><br/><font color="#777777"><i>PERIPHERAL_EVENT</i></font>>;
+ ]
+ _10 [
+ label = <<b>C structs</b><br/><font color="#777777"><i>JOYSTICK_FEATURE*</i></font>>;
+ ]
+
+ _11 [
+ label = <<b>Joystick Peripheral</b><br/><font color="#777777"><i>CPeripheralJoystick</i></font>>
+ ]
+
+ _12 [
+ label = <<b>Event Handling</b><br/><font color="#777777"><i>IJoystickDriverHandler</i></font>>
+ ]
+
+ _13 [
+ label = <<b>Keyboard Peripheral</b><br/><font color="#777777"><i>CPeripheralKeyboard</i></font>>
+ ]
+
+ _14 [
+ label = <<b>Keyboard Handling</b><br/><font color="#777777"><i>IKeyboardHandler</i></font>>
+ ]
+
+ _1 -> _5 [penwidth=3, weight=5];
+ _3 -> _13 [penwidth=3, weight=0];
+ _5 -> _8 [penwidth=3, weight=10];
+ _8 -> _11 [penwidth=3, weight=5];
+
+ subgraph cluster_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>OS</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _1 [style=filled, fillcolor=white];
+ _2 [style=filled, fillcolor=white];
+ _3 [style=filled, fillcolor=white];
+ _4 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_2 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Add-on</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _5 [style=filled, fillcolor=white];
+ _6 [style=filled, fillcolor=white];
+ _7 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral API</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _8 [style=filled, fillcolor=white];
+ _9 [style=filled, fillcolor=white];
+ _10 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_4 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Busses</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ subgraph cluster_4_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Add-on Bus</b></font><br/><font color="#777777"><i>CPeripheralBusAddon</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _11 [style=filled, fillcolor=white];
+ _12 [style=filled, fillcolor=white];
+
+ }
+
+ subgraph cluster_4_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "" [
+ color="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_4_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Application Bus</b></font><br/><font color="#777777"><i>CPeripheralBusApplication</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _13 [style=filled, fillcolor=white];
+ _14 [style=filled, fillcolor=white];
+ }
+ }
+}
+@enddot
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_3.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_3.dox
new file mode 100644
index 0000000..e539828
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_3.dox
@@ -0,0 +1,138 @@
+@dot
+digraph D {
+ graph [label="Orthogonal edges", splines=ortho, nodesep=1.0];
+ node [shape=box fontname=Arial];
+
+ rankdir=LR;
+ color = "white"
+ bgcolor = "white"
+ fillcolor = "white"
+ fontcolor = "white"
+ pencolor = "white"
+
+ _1 [
+ label = <<b>Joystick</b>>
+ ];
+ _2 [
+ label = <<b>Joystick<br/>Event</b>>
+ ];
+ _3 [
+ label = <<b>Keyboard</b>>
+ ];
+ _4 [
+ label = <<b>Keyboard<br/>Event</b>>
+ ];
+
+ _5 [
+ label = <<b>Joystick</b><br/><font color="#777777"><i>CJoystick</i></font>>;
+ ]
+ _6 [
+ label = <<b>Peripheral Event</b><br/><font color="#777777"><i>kodi::addon::PeripheralEvent</i></font>>;
+ ]
+ _7 [
+ label = <<b>ButtonMap</b><br/><font color="#777777"><i>kodi::addon::JoystickFeatures*</i></font>>;
+ ]
+
+ _8 [
+ label = <<b>C struct</b><br/><font color="#777777"><i>JOYSTICK_INFO</i></font>>;
+ ]
+ _9 [
+ label = <<b>C struct</b><br/><font color="#777777"><i>PERIPHERAL_EVENT</i></font>>;
+ ]
+ _10 [
+ label = <<b>C structs</b><br/><font color="#777777"><i>JOYSTICK_FEATURE*</i></font>>;
+ ]
+
+ _11 [
+ label = <<b>Joystick Peripheral</b><br/><font color="#777777"><i>CPeripheralJoystick</i></font>>
+ ]
+
+ _12 [
+ label = <<b>Event Handling</b><br/><font color="#777777"><i>IJoystickDriverHandler</i></font>>
+ ]
+
+ _13 [
+ label = <<b>Keyboard Peripheral</b><br/><font color="#777777"><i>CPeripheralKeyboard</i></font>>
+ ]
+
+ _14 [
+ label = <<b>Keyboard Handling</b><br/><font color="#777777"><i>IKeyboardHandler</i></font>>
+ ]
+
+ _2 -> _6 [penwidth=3, weight=5];
+ _6 -> _9 [penwidth=3, weight=10];
+ _9 -> _12 [penwidth=3, weight=5];
+
+ subgraph cluster_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>OS</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _1 [style=filled, fillcolor=white];
+ _2 [style=filled, fillcolor=white];
+ _3 [style=filled, fillcolor=white];
+ _4 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_2 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Add-on</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _5 [style=filled, fillcolor=white];
+ _6 [style=filled, fillcolor=white];
+ _7 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral API</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _8 [style=filled, fillcolor=white];
+ _9 [style=filled, fillcolor=white];
+ _10 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_4 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Busses</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ subgraph cluster_4_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Add-on Bus</b></font><br/><font color="#777777"><i>CPeripheralBusAddon</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _11 [style=filled, fillcolor=white];
+ _12 [style=filled, fillcolor=white];
+
+ }
+
+ subgraph cluster_4_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "" [
+ color="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_4_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Application Bus</b></font><br/><font color="#777777"><i>CPeripheralBusApplication</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _13 [style=filled, fillcolor=white];
+ _14 [style=filled, fillcolor=white];
+ }
+ }
+}
+@enddot
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_4.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_4.dox
new file mode 100644
index 0000000..b21caf3
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_4.dox
@@ -0,0 +1,94 @@
+@dot
+digraph D {
+ graph [label="Orthogonal edges", splines=ortho, nodesep=1.0];
+ node [shape=box fontname=Arial];
+
+ rankdir=LR;
+ color = "white"
+ bgcolor = "white"
+ fillcolor = "white"
+ fontcolor = "white"
+ pencolor = "white"
+
+ _11 [
+ label = <<b>Joystick Peripheral</b><br/><font color="#777777"><i>CPeripheralJoystick</i></font>>
+ ]
+
+ _12 [
+ label = <<b>Event Handling</b><br/><font color="#777777"><i>IJoystickDriverHandler</i></font>>
+ ]
+
+ _13 [
+ label = <<b>Keyboard Peripheral</b><br/><font color="#777777"><i>CPeripheralKeyboard</i></font>>
+ ]
+
+ _14 [
+ label = <<b>Keyboard Handling</b><br/><font color="#777777"><i>IKeyboardHandler</i></font>>
+ ]
+
+ _15 [
+ label = <<b>Input Handling</b><br/><font color="#777777"><i>CGenericJoystickInputHandling</i></font>>
+ ]
+
+ _16 [
+ label = <<b>Button Mapping</b><br/><font color="#777777"><i>CGenericJoystickButtonMapping</i></font>>
+ ]
+
+ _12 -> _15 [penwidth=3, weight=-10];
+ _12 -> _16 [penwidth=3];
+
+ subgraph cluster_4 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Busses</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ subgraph cluster_4_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Add-on Bus</b></font><br/><font color="#777777"><i>CPeripheralBusAddon</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _11 [style=filled, fillcolor=white];
+ _12 [style=filled, fillcolor=white];
+
+ }
+
+ subgraph cluster_4_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "" [
+ color="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_4_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Application Bus</b></font><br/><font color="#777777"><i>CPeripheralBusApplication</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _13 [style=filled, fillcolor=white];
+ _14 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_7 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Input Library</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+ rank=same;
+
+ subgraph cluster_7_1 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _15 [style=filled, fillcolor=white];
+ _16 [style=filled, fillcolor=white];
+ }
+ }
+}
+@enddot
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_5.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_5.dox
new file mode 100644
index 0000000..cc9a0dd
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_5.dox
@@ -0,0 +1,113 @@
+@dot
+digraph D {
+ graph [label="Orthogonal edges", splines=ortho, nodesep=1.0];
+ node [shape=box fontname=Arial];
+
+ rankdir=LR;
+ color = "white"
+ bgcolor = "white"
+ fillcolor = "white"
+ fontcolor = "white"
+ pencolor = "white"
+
+ _11 [
+ label = <<b>Joystick Peripheral</b><br/><font color="#777777"><i>CPeripheralJoystick</i></font>>
+ ]
+
+ _12 [
+ label = <<b>Event Handling</b><br/><font color="#777777"><i>IJoystickDriverHandler</i></font>>
+ ]
+
+ _13 [
+ label = <<b>Keyboard Peripheral</b><br/><font color="#777777"><i>CPeripheralKeyboard</i></font>>
+ ]
+
+ _14 [
+ label = <<b>Keyboard Handling</b><br/><font color="#777777"><i>IKeyboardHandler</i></font>>
+ ]
+
+ _15 [
+ label = <<b>Input Handling</b><br/><font color="#777777"><i>CGenericJoystickInputHandling</i></font>>
+ ]
+
+ _16 [
+ label = <<b>Button Mapping</b><br/><font color="#777777"><i>CGenericJoystickButtonMapping</i></font>>
+ ]
+
+ _21 [
+ label = <<b>Game Controller</b><br/><font color="#777777"><i>CControllerInput</i></font>>
+ ]
+
+ _22 [
+ label = <<b>Game Add-on</b><br/><font color="#777777"><i>CGameClient</i></font>>
+ ]
+
+ _12 -> _15 [penwidth=3, weight=10];
+ _15 -> _21 [penwidth=3, weight=10];
+ _21 -> _22 [penwidth=3, weight=0];
+
+ subgraph cluster_4 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Busses</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ subgraph cluster_4_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Add-on Bus</b></font><br/><font color="#777777"><i>CPeripheralBusAddon</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _11 [style=filled, fillcolor=white];
+ _12 [style=filled, fillcolor=white];
+
+ }
+
+ subgraph cluster_4_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "" [
+ color="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_4_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Application Bus</b></font><br/><font color="#777777"><i>CPeripheralBusApplication</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _13 [style=filled, fillcolor=white];
+ _14 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_7 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Input Library</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+ rank=same;
+
+ subgraph cluster_7_1 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _15 [style=filled, fillcolor=white];
+ _16 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_8 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Game API</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _21 [style=filled, fillcolor=white];
+ _22 [style=filled, fillcolor=white];
+ }
+}
+@enddot
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_6.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_6.dox
new file mode 100644
index 0000000..f48a12b
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_6.dox
@@ -0,0 +1,185 @@
+@dot
+digraph D {
+ graph [label="Orthogonal edges", splines=ortho, nodesep=1.0];
+ node [shape=box fontname=Arial];
+
+ rankdir=LR;
+ color = "white"
+ bgcolor = "white"
+ fillcolor = "white"
+ fontcolor = "white"
+ pencolor = "white"
+
+ _5 [
+ label = <<b>Joystick</b><br/><font color="#777777"><i>CJoystick</i></font>>;
+ ]
+ _6 [
+ label = <<b>Peripheral Event</b><br/><font color="#777777"><i>kodi::addon::PeripheralEvent</i></font>>;
+ ]
+ _7 [
+ label = <<b>ButtonMap</b><br/><font color="#777777"><i>kodi::addon::JoystickFeatures*</i></font>>;
+ ]
+
+ _8 [
+ label = <<b>C struct</b><br/><font color="#777777"><i>JOYSTICK_INFO</i></font>>;
+ ]
+ _9 [
+ label = <<b>C struct</b><br/><font color="#777777"><i>PERIPHERAL_EVENT</i></font>>;
+ ]
+ _10 [
+ label = <<b>C structs</b><br/><font color="#777777"><i>JOYSTICK_FEATURE*</i></font>>;
+ ]
+
+ _11 [
+ label = <<b>Joystick Peripheral</b><br/><font color="#777777"><i>CPeripheralJoystick</i></font>>
+ ]
+
+ _12 [
+ label = <<b>Event Handling</b><br/><font color="#777777"><i>IJoystickDriverHandler</i></font>>
+ ]
+
+ _13 [
+ label = <<b>Keyboard Peripheral</b><br/><font color="#777777"><i>CPeripheralKeyboard</i></font>>
+ ]
+
+ _14 [
+ label = <<b>Keyboard Handling</b><br/><font color="#777777"><i>IKeyboardHandler</i></font>>
+ ]
+
+ _15 [
+ label = <<b>Input Handling</b><br/><font color="#777777"><i>CGenericJoystickInputHandling</i></font>>
+ ]
+
+ _16 [
+ label = <<b>Button Mapping</b><br/><font color="#777777"><i>CGenericJoystickButtonMapping</i></font>>
+ ]
+
+ _19 [
+ label = <<b>Joystick Imitation</b><br/><font color="#777777"><i>CGenericKeyboadHandler</i></font>>
+ ]
+
+ _20 [
+ label = <<b>Button Map</b><br/><font color="#777777"><i>CAddonJoystickButtonMap</i></font>>
+ ]
+
+ _21 [
+ label = <<b>Game Controller</b><br/><font color="#777777"><i>CControllerInput</i></font>>
+ ]
+
+ _22 [
+ label = <<b>Game Add-on</b><br/><font color="#777777"><i>CGameClient</i></font>>
+ ]
+
+ _7 -> _10 [penwidth=3];
+ _10 -> _20 [penwidth=3];
+ _20 -> _15 [penwidth=3];
+ _12 -> _15 [penwidth=3];
+ _15 -> _21 [penwidth=3];
+ _21 -> _22 [penwidth=3];
+
+ subgraph cluster_2 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Add-on</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _5 [style=filled, fillcolor=white];
+ _6 [style=filled, fillcolor=white];
+ _7 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral API</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _8 [style=filled, fillcolor=white];
+ _9 [style=filled, fillcolor=white];
+ _10 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_4 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Busses</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ subgraph cluster_4_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Add-on Bus</b></font><br/><font color="#777777"><i>CPeripheralBusAddon</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _11 [style=filled, fillcolor=white];
+ _12 [style=filled, fillcolor=white];
+
+ }
+
+ subgraph cluster_4_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "" [
+ color="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_4_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Application Bus</b></font><br/><font color="#777777"><i>CPeripheralBusApplication</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _13 [style=filled, fillcolor=white];
+ _14 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_7 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Input Library</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+ rank=same;
+
+ subgraph cluster_7_1 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _15 [style=filled, fillcolor=white];
+ _16 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_7_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "-" [
+ color="red"
+ fontcolor="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_7_3 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _19 [style=filled, fillcolor=white];
+ _20 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_8 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Game API</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _21 [style=filled, fillcolor=white];
+ _22 [style=filled, fillcolor=white];
+ }
+}
+@enddot
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_7.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_7.dox
new file mode 100644
index 0000000..9393398
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_7.dox
@@ -0,0 +1,130 @@
+@dot
+digraph D {
+ graph [label="Orthogonal edges", splines=ortho, nodesep=1.0];
+ node [shape=box fontname=Arial];
+
+ rankdir=LR;
+ color = "white"
+ bgcolor = "white"
+ fillcolor = "white"
+ fontcolor = "white"
+ pencolor = "white"
+
+ _15 [
+ label = <<b>Input Handling</b><br/><font color="#777777"><i>CGenericJoystickInputHandling</i></font>>
+ ]
+
+ _16 [
+ label = <<b>Button Mapping</b><br/><font color="#777777"><i>CGenericJoystickButtonMapping</i></font>>
+ ]
+
+ _19 [
+ label = <<b>Joystick Imitation</b><br/><font color="#777777"><i>CGenericKeyboadHandler</i></font>>
+ ]
+
+ _20 [
+ label = <<b>Button Map</b><br/><font color="#777777"><i>CAddonJoystickButtonMap</i></font>>
+ ]
+
+ _21 [
+ label = <<b>Game Controller</b><br/><font color="#777777"><i>CControllerInput</i></font>>
+ ]
+
+ _22 [
+ label = <<b>Game Add-on</b><br/><font color="#777777"><i>CGameClient</i></font>>
+ ]
+
+ _23 [
+ label = <<b>Button Dialog</b><br/><font color="#777777"><i>CGUIDialogControllerInput</i></font>>
+ ]
+
+ _24 [
+ label = <<b>Game Controller Add-ons</b><br/><font color="#777777"><i>CGameController</i></font>>
+ ]
+
+ _27 [
+ label = <<b>Button Mapper</b><br/><font color="#777777"><i>CButtonMapper</i></font>>
+ ]
+
+ _28 [
+ label = <<b>Libretro Device</b><br/><font color="#777777"><i>CLibretroDevice</i></font>>
+ ]
+
+ _29 [
+ label = <<b>Libretro Core</b><br/><font color="#777777"><i>CLibretroDll</i></font>>
+ ]
+
+ _15 -> _21 [penwidth=3, weight=10];
+ _21 -> _22 [penwidth=3, weight=5];
+ _27 -> _28 -> _29 [penwidth=3, dir=forward, constraint=false, weight=10];
+ _22 -> _28 [penwidth=3, weight=10];
+ _24 -> _21 [penwidth=3, weight=10];
+
+ subgraph cluster_7 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Input Library</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+ rank=same;
+
+ subgraph cluster_7_1 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _15 [style=filled, fillcolor=white];
+ _16 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_7_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "-" [
+ color="red"
+ fontcolor="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_7_3 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _19 [style=filled, fillcolor=white];
+ _20 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_8 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Game API</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _21 [style=filled, fillcolor=white];
+ _22 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_9 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Configuration GUI</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _23 [style=filled, fillcolor=white];
+ _24 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_11 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Libretro API Wrapper</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+ rank=same;
+
+ _27 [style=filled, fillcolor=white];
+ _28 [style=filled, fillcolor=white];
+ _29 [style=filled, fillcolor=white];
+ }
+}
+@enddot
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_8.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_8.dox
new file mode 100644
index 0000000..ef50096
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_8.dox
@@ -0,0 +1,86 @@
+@dot
+digraph D {
+ graph [label="Orthogonal edges", splines=ortho, nodesep=1.0];
+ node [shape=box fontname=Arial];
+
+ rankdir=LR;
+ color = "white"
+ bgcolor = "white"
+ fillcolor = "white"
+ fontcolor = "white"
+ pencolor = "white"
+
+
+ _15 [
+ label = <<b>Input Handling</b><br/><font color="#777777"><i>CGenericJoystickInputHandling</i></font>>
+ ]
+
+ _16 [
+ label = <<b>Button Mapping</b><br/><font color="#777777"><i>CGenericJoystickButtonMapping</i></font>>
+ ]
+
+ _19 [
+ label = <<b>Joystick Imitation</b><br/><font color="#777777"><i>CGenericKeyboadHandler</i></font>>
+ ]
+
+ _20 [
+ label = <<b>Button Map</b><br/><font color="#777777"><i>CAddonJoystickButtonMap</i></font>>
+ ]
+
+ _25 [
+ label = <<b>Default Controller</b><br/><font color="#777777"><i>CDefaultController</i></font>>
+ ]
+
+ _26 [
+ label = <<b>Kodi Input Handler</b><br/><font color="#777777"><i>CButtonKeyHandler</i></font>>
+ ]
+
+ _15 -> _25 [penwidth=3];
+ _25 -> _26 [penwidth=3, weight=4];
+
+ subgraph cluster_7 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Input Library</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+ rank=same;
+
+ subgraph cluster_7_1 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _15 [style=filled, fillcolor=white];
+ _16 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_7_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "-" [
+ color="red"
+ fontcolor="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_7_3 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _19 [style=filled, fillcolor=white];
+ _20 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_10 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Kodi Input</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _25 [style=filled, fillcolor=white];
+ _26 [style=filled, fillcolor=white];
+ }
+}
+@enddot
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_9.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_9.dox
new file mode 100644
index 0000000..482e361
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_cpp_peripheral_lifetime_diagram_9.dox
@@ -0,0 +1,215 @@
+@dot
+digraph D {
+ graph [label="Orthogonal edges", splines=ortho, nodesep=1.0];
+ node [shape=box fontname=Arial];
+
+ rankdir=LR;
+ color = "white"
+ bgcolor = "white"
+ fillcolor = "white"
+ fontcolor = "white"
+ pencolor = "white"
+
+ _1 [
+ label = <<b>Joystick</b>>
+ ];
+ _2 [
+ label = <<b>Joystick<br/>Event</b>>
+ ];
+ _3 [
+ label = <<b>Keyboard</b>>
+ ];
+ _4 [
+ label = <<b>Keyboard<br/>Event</b>>
+ ];
+
+ _5 [
+ label = <<b>Joystick</b><br/><font color="#777777"><i>CJoystick</i></font>>;
+ ]
+ _6 [
+ label = <<b>Peripheral Event</b><br/><font color="#777777"><i>kodi::addon::PeripheralEvent</i></font>>;
+ ]
+ _7 [
+ label = <<b>ButtonMap</b><br/><font color="#777777"><i>kodi::addon::JoystickFeatures*</i></font>>;
+ ]
+
+ _8 [
+ label = <<b>C struct</b><br/><font color="#777777"><i>JOYSTICK_INFO</i></font>>;
+ ]
+ _9 [
+ label = <<b>C struct</b><br/><font color="#777777"><i>PERIPHERAL_EVENT</i></font>>;
+ ]
+ _10 [
+ label = <<b>C structs</b><br/><font color="#777777"><i>JOYSTICK_FEATURE*</i></font>>;
+ ]
+
+ _11 [
+ label = <<b>Joystick Peripheral</b><br/><font color="#777777"><i>CPeripheralJoystick</i></font>>
+ ]
+
+ _12 [
+ label = <<b>Event Handling</b><br/><font color="#777777"><i>IJoystickDriverHandler</i></font>>
+ ]
+
+ _13 [
+ label = <<b>Keyboard Peripheral</b><br/><font color="#777777"><i>CPeripheralKeyboard</i></font>>
+ ]
+
+ _14 [
+ label = <<b>Keyboard Handling</b><br/><font color="#777777"><i>IKeyboardHandler</i></font>>
+ ]
+
+ _15 [
+ label = <<b>Input Handling</b><br/><font color="#777777"><i>CGenericJoystickInputHandling</i></font>>
+ ]
+
+ _16 [
+ label = <<b>Button Mapping</b><br/><font color="#777777"><i>CGenericJoystickButtonMapping</i></font>>
+ ]
+
+ _19 [
+ label = <<b>Joystick Imitation</b><br/><font color="#777777"><i>CGenericKeyboadHandler</i></font>>
+ ]
+
+ _20 [
+ label = <<b>Button Map</b><br/><font color="#777777"><i>CAddonJoystickButtonMap</i></font>>
+ ]
+
+ _23 [
+ label = <<b>Button Dialog</b><br/><font color="#777777"><i>CGUIDialogControllerInput</i></font>>
+ ]
+
+ _24 [
+ label = <<b>Game Controller Add-ons</b><br/><font color="#777777"><i>CGameController</i></font>>
+ ]
+
+ _2 -> _6 [penwidth=3, weight=2];
+ _6 -> _9 [penwidth=3, weight=10];
+ _7 -> _10 [dir=back, penwidth=3, weight=0];
+ _10 -> _20 [dir=back, penwidth=3, weight=-2];
+ _9 -> _12 [penwidth=3, weight=21];
+ _12 -> _16 [penwidth=3];
+ _16 -> _23 [penwidth=3, dir=both, weight=20];
+ _23 -> _24 [penwidth=3, dir=back, weight=0];
+ edge[constraint=false];
+ _20 -> _16 [dir=back, penwidth=3, weight=0];
+
+
+ subgraph cluster_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>OS</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _1 [style=filled, fillcolor=white];
+ _2 [style=filled, fillcolor=white];
+ _3 [style=filled, fillcolor=white];
+ _4 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_2 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Add-on</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _5 [style=filled, fillcolor=white];
+ _6 [style=filled, fillcolor=white];
+ _7 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral API</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _8 [style=filled, fillcolor=white];
+ _9 [style=filled, fillcolor=white];
+ _10 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_4 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Peripheral Busses</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ subgraph cluster_4_1 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Add-on Bus</b></font><br/><font color="#777777"><i>CPeripheralBusAddon</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _11 [style=filled, fillcolor=white];
+ _12 [style=filled, fillcolor=white];
+
+ }
+
+ subgraph cluster_4_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "" [
+ color="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_4_3 {
+ graph [nodesep=6, ranksep=4];
+ label = <<font point-size='18'><b>Virtual Application Bus</b></font><br/><font color="#777777"><i>CPeripheralBusApplication</i></font>>;
+ fontcolor = "black";
+ bgcolor = "white";
+
+ _13 [style=filled, fillcolor=white];
+ _14 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_7 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Input Library</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+ rank=same;
+
+ subgraph cluster_7_1 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _15 [style=filled, fillcolor=white];
+ _16 [style=filled, fillcolor=white];
+ }
+
+ subgraph cluster_7_2 {
+ graph [nodesep=6, ranksep=4];
+ label = "";
+ "-" [
+ color="red"
+ fontcolor="red"
+ bgcolor = "red";
+ ];
+ pencolor = "red";
+ }
+
+ subgraph cluster_7_3 {
+ graph [nodesep=6, ranksep=4];
+ label = ""
+ pencolor = "red";
+ _19 [style=filled, fillcolor=white];
+ _20 [style=filled, fillcolor=white];
+ }
+ }
+
+ subgraph cluster_9 {
+ graph [nodesep=6, ranksep=4];
+ label = <<b><font point-size='20'>Configuration GUI</font></b>>;
+ fontcolor = "white";
+ bgcolor = "red";
+
+ _23 [style=filled, fillcolor=white];
+ _24 [style=filled, fillcolor=white];
+ }
+}
+@enddot
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_general.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_general.dox
new file mode 100644
index 0000000..f7e04ca
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_general.dox
@@ -0,0 +1,8 @@
+/*!
+
+\page general_parts General Development parts
+\brief \doc_header{ General Add-On Development parts }
+
+\subpage modules__infolabels_boolean_conditions
+\subpage page_List_of_built_in_functions
+*/
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_python.dox b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_python.dox
new file mode 100644
index 0000000..98783b8
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Modules/modules_python.dox
@@ -0,0 +1,299 @@
+/*!
+
+
+\defgroup python Python
+\image html logo-python.png
+\brief \htmlonly
+ <h3><span style="text-decoration: underline;"><span style="font-style: italic;"><span
+ style="color: rgb(102, 102, 102);">Python Add-On Development</span></span></span></h3>
+ \endhtmlonly
+
+Kodi includes a built-in [Python interpreter](http://en.wikipedia.org/wiki/Python_%28programming_language%29)
+that allows users to develop add-ons (scripts and plugins) that interface easily
+and cleanly with the Kodi dashboard. These add-ons can extend the functionality
+of Kodi without requiring extensive programming experience or ability. While you
+may not feel comfortable browsing the Kodi source code and submitting patches (or
+even bug reports), you can learn how to write a script or plugin with just a few
+hours' practice, using the information available in these pages.
+
+This page is intended as an introduction to Kodi Python for new developers, and
+a quick reference for more experienced programmers. If you're not interested in
+programming, you might want to visit [this page](http://kodi.wiki/view/Add-ons)
+for information about installing and using Python add-ons as an end user. If
+you're already familiar with Kodi Python, you can probably skip on down to the
+[environment details](http://kodi.wiki/view/Python_Development#Environment_details)
+or the [resource links](http://kodi.wiki/view/Python_Development#Resource_links)
+below for quick reference material.
+
+_ _ _
+
+Built-in modules
+----------------
+
+In addition to the standard libraries, Kodi [Python](https://www.python.org/)
+uses a handful of custom modules to expose Kodi functionality to Python.
+
+| Module | Description |
+|------------------------------------:|:-----------------------------------------------------------|
+| \ref python_xbmc "xbmc" | Offers classes and functions that provide information about the media currently playing and that allow manipulation of the media player (such as starting a new song). You can also find system information using the functions available in this library.
+| \ref python_xbmcgui "xbmcgui" | Offers classes and functions that manipulate the Graphical User Interface through windows, dialogs, and various control widgets.
+| \ref python_xbmcplugin "xbmcplugin" | Offers classes and functions that allow a developer to present information through Kodi's standard menu structure. While plugins don't have the same flexibility as scripts, they boast significantly quicker development time and a more consistent user experience.
+| \ref python_xbmcaddon "xbmcaddon" | Offers classes and functions that manipulate the add-on settings, information and localization.
+| \ref python_xbmcvfs "xbmcvfs" | Offers classes and functions that allow access to the Virtual File Server (VFS) which you can use to manipulate files and folders.
+| \ref python_xbmcwsgi "xbmcwsgi" | The [<b>Web Server Gateway Interface (WSGI)</b>](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) is a specification for simple and universal interface between web servers and web applications or frameworks for the Python programming language.
+| \ref python_xbmcdrm "xbmcdrm" | Offers classes and functions to create and manipulate a DRM CryptoSession .
+
+_ _ _
+
+Installing additional modules
+----------------
+
+Additional modules may be installed by simply adding the module to the root
+folder of your add-on.
+
+A common way to organized third-party modules that are not part of add-on source
+code itself, is to add a lib directory and place an __init__.py file and other
+third-party modules inside it. These modules may then normally be imported using
+from lib import some module.
+
+_ _ _
+
+Python plugins versus scripts
+----------------
+
+Please do not confuse "Plugins" with "Scripts". Unlike the Scripts, Plugins are
+not meant to be directly invoked by the user. Instead, Plugins are automatically
+invoked when the user enters such a virtual folder. Do not try to run Plugins
+files from the scripts window as that will only give you a weird error message.
+Plugins, unlike Scripts, do not really provide new functionality to Kodi,
+instead what they do do is provide an easy way to present content listings in
+Kodi through the native GUI interface.
+
+_ _ _
+
+Script development
+----------------
+
+If you're new to Python programming (or just new to Kodi Python), the easiest
+way to get started is with a script. The traditional Hello World program,
+written as an Kodi Python script, would look like this:
+~~~~~~~~~~~~~{.py}
+print("Hello World!")
+~~~~~~~~~~~~~
+That's the same code you would enter at the Python command line, because Kodi
+runs a full-featured, standard Python interpreter (for more information
+concerning the current version number and included modules see the environment
+details below). If you're already familiar with Python programming, the only new
+challenge is learning the custom modules that allow you to gather information
+from Kodi and manipulate the Graphical User Interface (GUI).
+
+There are some excellent tutorials available to introduce you to Kodi scripting
+(and Python in general). See the [HOW-TO](http://kodi.wiki/view/HOW-TO_write_Python_Scripts)
+included in the Kodi Online Manual, or visit Alexpoet's Kodi Scripting site for
+a popular beginner's tutorial (PDF).
+
+_ _ _
+Plugin development
+----------------
+
+While scripts offer you flexibility and full control over the Kodi GUI, plugins
+allow you to quickly and consistently present information to the user through
+the standard Kodi menu structure.
+
+When a user launches a plugin, the plugin generates a list of menu items and
+hands them to Kodi to draw on the screen (regardless of screen resolution, skin,
+or any other user setting). While plugin developers lose some amount of control
+over the presentation, they no longer have to make up their own UIs, or worry
+about creating a usable look and feel across multiple displays.
+
+Plugins are most commonly used to scrape websites for links to streaming videos,
+displaying the video list in Kodi just like it would movie files on the local
+hard drive, but a plugin can be used anywhere a script could, as long as the
+menu structure is a sufficient GUI for the add-on's needs.
+
+Also, note that a script can launch a plugin, and a plugin can launch a script
+(and, for that matter, it can call all the same functions available to a script)
+so the distinction is more theoretical than practical.
+
+
+@{
+\ingroup python
+\defgroup python_xbmc Library - xbmc
+
+\ingroup python
+\defgroup python_xbmcgui Library - xbmcgui
+
+\ingroup python
+\defgroup python_xbmcplugin Library - xbmcplugin
+
+\ingroup python
+\defgroup python_xbmcaddon Library - xbmcaddon
+
+\ingroup python
+\defgroup python_xbmcvfs Library - xbmcvfs
+
+\ingroup python
+\defgroup python_xbmcwsgi Library - xbmcwsgi
+@brief **Web Server Gateway Interface**
+
+The [<b>Web Server Gateway Interface (WSGI)</b>](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface)
+is a specification for simple and universal interface between web servers and
+web applications or frameworks for the Python programming language.
+
+\ingroup python
+\defgroup python_xbmcdrm Library - xbmcdrm
+@}
+
+*/
+
+/*!
+@page python_v20 Python API v20
+\python_removed_function{
+ translatePath,
+ "",
+ <b>xbmc.translatePath()</b> function was removed completely. Use <b>xbmcvfs.translatePath()</b>.
+}
+@page python_v19 Python API v19
+\python_removed_function{
+ onDatabaseUpdated,
+ "",
+ <b>xbmc.monitor().onDatabaseUpdated()</b> function was removed completely.
+}
+\python_removed_function{
+ onDatabaseScanStarted,
+ "",
+ <b>xbmc.monitor().onDatabaseScanStarted()</b> function was removed completely.
+}
+\python_removed_function{
+ onAbortRequested,
+ "",
+ <b>xbmc.monitor().onAbortRequested()</b> function was removed completely.
+}
+\python_removed_function{
+ create,
+ "",
+ <b>xbmcgui.DialogBusy().create()</b> function was removed completely.
+}
+\python_removed_function{
+ update,
+ "",
+ <b>xbmcgui.DialogBusy().update()</b> function was removed completely.
+}
+\python_removed_function{
+ close,
+ "",
+ <b>xbmcgui.DialogBusy().close()</b> function was removed completely.
+}
+\python_removed_function{
+ iscanceled,
+ "",
+ <b>xbmcgui.DialogBusy().iscanceled()</b> function was removed completely.
+}
+\python_removed_function{
+ setIconImage,
+ "",
+ <b>xbmcgui.ListItem().setIconImage()</b> function was removed completely.
+}
+\python_removed_function{
+ setThumbnailImage,
+ "",
+ <b>xbmcgui.ListItem().setThumbnailImage()</b> function was removed completely.
+}
+\python_removed_function{
+ getdescription,
+ "",
+ <b>xbmcgui.ListItem().getdescription()</b> function was removed completely.
+}
+\python_removed_function{
+ getduration,
+ "",
+ <b>xbmcgui.ListItem().getduration()</b> function was removed completely.
+}
+\python_removed_function{
+ getfilename,
+ "",
+ <b>xbmcgui.ListItem().getfilename()</b> function was removed completely.
+}
+\python_removed_function{
+ getfilename,
+ "",
+ <b>xbmcgui.Window().getResolution()</b> function was removed completely.
+}
+\python_removed_function{
+ setCoordinateResolution,
+ "",
+ <b>xbmcgui.Window().setCoordinateResolution()</b> function was removed completely.
+}
+\python_removed_function{
+ makeLegalFilename,
+ "",
+ <b>xbmc.makeLegalFilename()</b> function was moved to the xbmcvfs module.
+}
+\python_removed_function{
+ validatePath,
+ "",
+ <b>xbmc.validatePath()</b> function was moved to the xbmcvfs module.
+}
+\python_removed_function{
+ abortRequested,
+ "",
+ <b>xbmc.abortRequested</b> flag was removed completely. Use xbmc.Monitor().abortRequested().
+}
+*/
+/*!
+@page python_v18 Python API v18
+*/
+/*!
+@page python_v17 Python API v17
+\python_removed_function{
+ getCaptureState,
+ http://mirrors.kodi.tv/docs/python-docs/16.x-jarvis/xbmc.html#RenderCapture-getCaptureState,
+ <b>xbmc.RenderCapture().getCaptureState()</b> function was removed completely.
+}
+\python_removed_function{
+ waitForCaptureStateChangeEvent,
+ http://mirrors.kodi.tv/docs/python-docs/16.x-jarvis/xbmc.html#RenderCapture-waitForCaptureStateChangeEvent,
+ <b>xbmc.RenderCapture().waitForCaptureStateChangeEvent()</b> function was removed completely.
+}
+\python_removed_function{
+ disableSubtitles,
+ http://mirrors.kodi.tv/docs/python-docs/16.x-jarvis/xbmc.html#Player,
+ <b>xbmc.Player().disableSubtitles()</b> function was removed completely.
+ Use \endhtmlonly \link XBMCAddon::xbmc::Player::showSubtitles() xbmc.Player().showSubtitles(...) \endlink \htmlonly instead.
+}
+*/
+/*!
+@page python_v16 Python API v16
+*/
+/*!
+@page python_v15 Python API v15
+*/
+/*!
+@page python_v14 Python API v14
+*/
+/*!
+@page python_v13 Python API v13
+*/
+/*!
+@page python_v12 Python API v12
+\python_removed_function{
+ executehttpapi,
+ http://mirrors.kodi.tv/docs/python-docs/12.2-frodo/xbmc.html#-executehttpapi,
+ <b>xbmc.executehttpapi()</b> function was removed completely.
+}
+*/
+/*!
+@page python_revisions Python API Changes
+@brief Overview of changes on Python API for Kodi
+
+- @subpage python_v20
+- @subpage python_v19
+- @subpage python_v18
+- @subpage python_v17
+- @subpage python_v16
+- @subpage python_v15
+- @subpage python_v14
+- @subpage python_v13
+- @subpage python_v12
+
+*/
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Skin/skin.dox b/xbmc/addons/kodi-dev-kit/doxygen/Skin/skin.dox
new file mode 100644
index 0000000..e4e4fc0
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/Skin/skin.dox
@@ -0,0 +1,69 @@
+/*!
+
+\page skin_parts Skin Development
+@brief \doc_header{ Skin Add-On Development }
+
+Kodi includes a GUI library that allows you to skin/change everything you see.
+The images, sizes and positions of controls, colours, fonts, text, through to
+altering navigation and even adding new functionality.
+
+The skin system is quite complex. This portion of the manual is dedicated to
+providing in depth information on how it all works. It contains tips to make the
+experience a little more pleasant.
+
+If you are just getting started with skinning Kodi, then it is suggested that the
+best way to learn is by modifying one of the many existing skins that are
+available. The default skin, Estuary, includes almost all the various tricks
+and features that make the Kodi skinning engine so powerful, so is an ideal place
+to start. You may wish to start by having a look through the tutorial section on
+skinning Kodi as well as the "Skinning Kodi" article, and try modifying a window
+or two by adding a button, or altering the textures or layout.
+
+- \subpage skin_controls - Controls are the substance of your skin.
+- \subpage window_ids - List of available window names, definitions, IDs and the matching XML-file
+- \subpage Skin_Timers - Programatic time-based objects for Skins
+
+*/
+
+/*!
+@page skinning_v20 Skinning engine v20
+*/
+/*!
+@page skinning_v19 Skinning engine v19
+*/
+/*!
+@page skinning_v18 Skinning engine v18
+*/
+/*!
+@page skinning_v17 Skinning engine v17
+*/
+/*!
+@page skinning_v16 Skinning engine v16
+*/
+/*!
+@page skinning_v15 Skinning engine v15
+*/
+/*!
+@page skinning_v14 Skinning engine v14
+*/
+/*!
+@page skinning_v13 Skinning engine v13
+*/
+/*!
+@page skinning_v12 Skinning engine v12
+*/
+
+/*!
+@page skinning_revisions Skinning engine changes
+@brief Overview of changes on Skinning engine for Kodi
+
+- @subpage skinning_v20
+- @subpage skinning_v19
+- @subpage skinning_v18
+- @subpage skinning_v17
+- @subpage skinning_v16
+- @subpage skinning_v15
+- @subpage skinning_v14
+- @subpage skinning_v13
+- @subpage skinning_v12
+*/
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/kodi-dev.png b/xbmc/addons/kodi-dev-kit/doxygen/kodi-dev.png
new file mode 100644
index 0000000..315a13b
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/kodi-dev.png
Binary files differ
diff --git a/xbmc/addons/kodi-dev-kit/doxygen/main.txt b/xbmc/addons/kodi-dev-kit/doxygen/main.txt
new file mode 100644
index 0000000..8a60960
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/doxygen/main.txt
@@ -0,0 +1,49 @@
+/*!
+
+@mainpage
+
+### Welcome to the Documentation of Kodi for Add-On Development.
+
+__Kodi®__ media center, formerly known as XBMC Media Center, is a free and
+open-source media player software developed by the XBMC Foundation, a non-profit
+technology consortium. Kodi is available for multiple operating systems and
+hardware platforms, with a software 10-foot user interface for use with
+televisions and remote controls. It allows users to play and view most videos,
+music, such as audio and video podcasts from the internet, and all common
+digital media files from local and network storage media.
+
+Add-ons are extensions that can be run from inside the Kodi GUI and in
+addition to binary add-ons for the use of different systems. They are usual
+written by third party developers and published to our official repository.
+Add-ons can also be published in other repositories or as stand alone zip
+files anywhere on the internet. Examples of Add-ons include video website
+streams, scrapers, skins and scripts.
+
+#### Supported systems
+
+Currently support Kodi Add-Ons based upon Python and C++.
+
+#### Tutorials and Examples
+
+In the distribution of the library you find the two directories *tutorials*
+and *examples*. They contain subdirectories for the packages...
+The demos use third party libraries for the graphical user interface. The
+examples don't have this dependency and most examples are referred to in the
+user manual.
+
+#### License
+
+Kodi is distributed under a [GNU General Public License version 2](./LICENSE.md).
+
+\htmlonly
+<div style="display:none">
+\endhtmlonly
+
+\subpage general
+\subpage general_parts
+
+\htmlonly
+</div>
+\endhtmlonly
+
+*/
diff --git a/xbmc/addons/kodi-dev-kit/include/NOTE b/xbmc/addons/kodi-dev-kit/include/NOTE
new file mode 100644
index 0000000..ddc579c
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/NOTE
@@ -0,0 +1,9 @@
+NOTE:
+
+This directory contains independent Headers to build Add-on's
+without the whole Kodi source tree. The Add-on itself can add
+this headers to his source tree without dependencies to any
+Kodi related classes or functions.
+
+Also this headers are never changed without a API Version
+change.
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/AddonBase.h b/xbmc/addons/kodi-dev-kit/include/kodi/AddonBase.h
new file mode 100644
index 0000000..a626122
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/AddonBase.h
@@ -0,0 +1,1877 @@
+/*
+ * 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 "c-api/addon_base.h"
+#include "versions.h"
+
+#include <assert.h> /* assert */
+#include <stdarg.h> /* va_list, va_start, va_arg, va_end */
+
+#ifdef __cplusplus
+
+#include <cstdlib>
+#include <cstring>
+#include <ctime>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+#include "tools/StringUtils.h"
+
+namespace kodi
+{
+
+namespace gui
+{
+struct IRenderHelper;
+} /* namespace gui */
+
+//==============================================================================
+/// @ingroup cpp_kodi_Defs
+/// @defgroup cpp_kodi_Defs_HardwareContext using HardwareContext
+/// @brief **Hardware specific device context**\n
+/// This defines an independent value which is used for hardware and OS specific
+/// values.
+///
+/// This is basically a simple pointer which has to be changed to the desired
+/// format at the corresponding places using <b>`static_cast<...>(...)`</b>.
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <d3d11_1.h>
+/// ..
+/// // Note: Device() there is used inside addon child class about
+/// // kodi::addon::CInstanceVisualization
+/// ID3D11DeviceContext1* context = static_cast<ID3D11DeviceContext1*>(kodi::addon::CInstanceVisualization::Device());
+/// ..
+/// ~~~~~~~~~~~~~
+///
+///@{
+using HardwareContext = ADDON_HARDWARE_CONTEXT;
+///@}
+//------------------------------------------------------------------------------
+
+namespace addon
+{
+
+/*!
+ * @brief Internal used structure to have stored C API data above and
+ * available for everything below.
+ */
+struct ATTR_DLL_LOCAL CPrivateBase
+{
+ // Interface function table to hold addresses on add-on and from kodi
+ static AddonGlobalInterface* m_interface;
+};
+
+/*
+ * Internally used helper class to manage processing of a "C" structure in "CPP"
+ * class.
+ *
+ * At constant, the "C" structure is copied, otherwise the given pointer is
+ * superseded and is changeable.
+ *
+ * -----------------------------------------------------------------------------
+ *
+ * Example:
+ *
+ * ~~~~~~~~~~~~~{.cpp}
+ * extern "C" typedef struct C_SAMPLE_DATA
+ * {
+ * unsigned int iUniqueId;
+ * } C_SAMPLE_DATA;
+ *
+ * class CPPSampleData : public CStructHdl<CPPSampleData, C_SAMPLE_DATA>
+ * {
+ * public:
+ * CPPSampleData() = default;
+ * CPPSampleData(const CPPSampleData& sample) : CStructHdl(sample) { }
+ * CPPSampleData(const C_SAMPLE_DATA* sample) : CStructHdl(sample) { }
+ * CPPSampleData(C_SAMPLE_DATA* sample) : CStructHdl(sample) { }
+ *
+ * void SetUniqueId(unsigned int uniqueId) { m_cStructure->iUniqueId = uniqueId; }
+ * unsigned int GetUniqueId() const { return m_cStructure->iUniqueId; }
+ * };
+ *
+ * ~~~~~~~~~~~~~
+ *
+ * It also works with the following example:
+ *
+ * ~~~~~~~~~~~~~{.cpp}
+ * CPPSampleData test;
+ * // Some work
+ * C_SAMPLE_DATA* data = test;
+ * // Give "data" to Kodi
+ * ~~~~~~~~~~~~~
+ */
+template<class CPP_CLASS, typename C_STRUCT>
+class CStructHdl
+{
+public:
+ CStructHdl() : m_cStructure(new C_STRUCT()), m_owner(true) {}
+
+ CStructHdl(const CPP_CLASS& cppClass)
+ : m_cStructure(new C_STRUCT(*cppClass.m_cStructure)), m_owner(true)
+ {
+ }
+
+ CStructHdl(const C_STRUCT* cStructure) : m_cStructure(new C_STRUCT(*cStructure)), m_owner(true) {}
+
+ CStructHdl(C_STRUCT* cStructure) : m_cStructure(cStructure) { assert(cStructure); }
+
+ const CStructHdl& operator=(const CStructHdl& right)
+ {
+ assert(&right.m_cStructure);
+
+ if (this == &right)
+ return *this;
+
+ if (m_cStructure && !m_owner)
+ {
+ memcpy(m_cStructure, right.m_cStructure, sizeof(C_STRUCT));
+ }
+ else
+ {
+ if (m_owner)
+ delete m_cStructure;
+ m_owner = true;
+ m_cStructure = new C_STRUCT(*right.m_cStructure);
+ }
+ return *this;
+ }
+
+ const CStructHdl& operator=(const C_STRUCT& right)
+ {
+ assert(&right);
+
+ if (m_cStructure == &right)
+ return *this;
+
+ if (m_cStructure && !m_owner)
+ {
+ memcpy(m_cStructure, &right, sizeof(C_STRUCT));
+ }
+ else
+ {
+ if (m_owner)
+ delete m_cStructure;
+ m_owner = true;
+ m_cStructure = new C_STRUCT(*right);
+ }
+ return *this;
+ }
+
+ virtual ~CStructHdl()
+ {
+ if (m_owner)
+ delete m_cStructure;
+ }
+
+ operator C_STRUCT*() { return m_cStructure; }
+ operator const C_STRUCT*() const { return m_cStructure; }
+
+ const C_STRUCT* GetCStructure() const { return m_cStructure; }
+
+protected:
+ C_STRUCT* m_cStructure = nullptr;
+
+private:
+ bool m_owner = false;
+};
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon_addonbase_Defs
+/// @defgroup cpp_kodi_addon_addonbase_Defs_CSettingValue class CSettingValue
+/// @brief **Setting value handler**\n
+/// Inside addon main instance used helper class to give settings value.
+///
+/// This is used on @ref addon::CAddonBase::SetSetting() to inform addon about
+/// settings change by used. This becomes then used to give the related value
+/// name.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_addonbase_Defs_CSettingValue_Help
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Here is a code example how this is used:**
+///
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/AddonBase.h>
+///
+/// enum myEnumValue
+/// {
+/// valueA,
+/// valueB,
+/// valueC
+/// };
+///
+/// std::string m_myStringValue;
+/// int m_myIntegerValue;
+/// bool m_myBooleanValue;
+/// float m_myFloatingPointValue;
+/// myEnumValue m_myEnumValue;
+///
+///
+/// ADDON_STATUS CMyAddon::SetSetting(const std::string& settingName, const kodi::addon::CSettingValue& settingValue)
+/// {
+/// if (settingName == "my_string_value")
+/// m_myStringValue = settingValue.GetString();
+/// else if (settingName == "my_integer_value")
+/// m_myIntegerValue = settingValue.GetInt();
+/// else if (settingName == "my_boolean_value")
+/// m_myBooleanValue = settingValue.GetBoolean();
+/// else if (settingName == "my_float_value")
+/// m_myFloatingPointValue = settingValue.GetFloat();
+/// else if (settingName == "my_enum_value")
+/// m_myEnumValue = settingValue.GetEnum<myEnumValue>();
+/// }
+/// ~~~~~~~~~~~~~
+///
+/// @note The asked type should match the type used on settings.xml.
+///
+///@{
+class ATTR_DLL_LOCAL CSettingValue
+{
+public:
+ explicit CSettingValue(const std::string& settingValue) : str(settingValue) {}
+
+ bool empty() const { return str.empty(); }
+
+ /// @defgroup cpp_kodi_addon_addonbase_Defs_CSettingValue_Help Value Help
+ /// @ingroup cpp_kodi_addon_addonbase_Defs_CSettingValue
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_addonbase_Defs_CSettingValue :</b>
+ /// | Name | Type | Get call
+ /// |------|------|----------
+ /// | **Settings value as string** | `std::string` | @ref CSettingValue::GetString "GetString"
+ /// | **Settings value as integer** | `int` | @ref CSettingValue::GetInt "GetInt"
+ /// | **Settings value as unsigned integer** | `unsigned int` | @ref CSettingValue::GetUInt "GetUInt"
+ /// | **Settings value as boolean** | `bool` | @ref CSettingValue::GetBoolean "GetBoolean"
+ /// | **Settings value as floating point** | `float` | @ref CSettingValue::GetFloat "GetFloat"
+ /// | **Settings value as enum** | `enum` | @ref CSettingValue::GetEnum "GetEnum"
+
+ /// @addtogroup cpp_kodi_addon_addonbase_Defs_CSettingValue
+ ///@{
+
+ /// @brief To get settings value as string.
+ std::string GetString() const { return str; }
+
+ /// @brief To get settings value as integer.
+ int GetInt() const { return std::atoi(str.c_str()); }
+
+ /// @brief To get settings value as unsigned integer.
+ unsigned int GetUInt() const { return std::atoi(str.c_str()); }
+
+ /// @brief To get settings value as boolean.
+ bool GetBoolean() const { return std::atoi(str.c_str()) > 0; }
+
+ /// @brief To get settings value as floating point.
+ float GetFloat() const { return static_cast<float>(std::atof(str.c_str())); }
+
+ /// @brief To get settings value as enum.
+ /// @note Inside settings.xml them stored as integer.
+ template<typename enumType>
+ enumType GetEnum() const
+ {
+ return static_cast<enumType>(GetInt());
+ }
+
+ ///@}
+
+private:
+ const std::string str;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon_addonbase_Defs
+/// @defgroup cpp_kodi_addon_addonbase_Defs_IInstanceInfo class IInstanceInfo
+/// @brief **Instance informations**\n
+/// Class to get any instance information when creating a new one.
+///
+///@{
+class ATTR_DLL_LOCAL IInstanceInfo
+{
+public:
+ explicit IInstanceInfo(KODI_ADDON_INSTANCE_STRUCT* instance) : m_instance(instance) {}
+
+ /// @defgroup cpp_kodi_addon_addonbase_Defs_IInstanceInfo_Help Value Help
+ /// @ingroup cpp_kodi_addon_addonbase_Defs_IInstanceInfo
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_addonbase_Defs_IInstanceInfo :</b>
+ /// | Name | Type | Get call
+ /// |------|------|----------
+ /// | **Used type identifier** | @ref KODI_ADDON_INSTANCE_TYPE | @ref CSettingValue::GetType "GetType"
+ /// | **Check asked type used on this class** | `bool` | @ref CSettingValue::IsType "IsType"
+ /// | **Get optional identification number (usage relate to addon type)** | `uint32_t` | @ref CSettingValue::GetNumber "GetNumber"
+ /// | **Get optional identification string (usage relate to addon type)** | `std::string` | @ref CSettingValue::GetID "GetID"
+ /// | **Get API version from Kodi about instance** | `std::string` | @ref CSettingValue::GetAPIVersion "GetAPIVersion"
+ /// | **Check this is first created instance by Kodi** | `bool` | @ref CSettingValue::FirstInstance "FirstInstance"
+
+ /// @addtogroup cpp_kodi_addon_addonbase_Defs_IInstanceInfo
+ ///@{
+
+ /// @brief To get settings value as string.
+ KODI_ADDON_INSTANCE_TYPE GetType() const { return m_instance->info->type; }
+
+ /// @brief Check asked type used on this class.
+ ///
+ /// @param[in] type Type identifier to check
+ /// @return True if type matches
+ bool IsType(KODI_ADDON_INSTANCE_TYPE type) const { return m_instance->info->type == type; }
+
+ /// @brief Get optional identification number (usage relate to addon type).
+ uint32_t GetNumber() const { return m_instance->info->number; }
+
+ /// @brief Get optional identification string (usage relate to addon type).
+ std::string GetID() const { return m_instance->info->id; }
+
+ /// @brief Get API version from Kodi about instance.
+ std::string GetAPIVersion() const { return m_instance->info->version; }
+
+ /// @brief Check this is first created instance by Kodi.
+ bool FirstInstance() const { return m_instance->info->first_instance; }
+
+ ///@}
+
+ operator KODI_ADDON_INSTANCE_STRUCT*() { return m_instance; }
+
+ operator KODI_ADDON_INSTANCE_STRUCT*() const { return m_instance; }
+
+private:
+ IInstanceInfo() = delete;
+ IInstanceInfo(const IInstanceInfo&) = delete;
+
+ KODI_ADDON_INSTANCE_STRUCT* m_instance;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/*
+ * Internal class to control various instance types with general parts defined
+ * here.
+ *
+ * Mainly is this currently used to identify requested instance types.
+ *
+ * @note This class is not need to know during add-on development thats why
+ * commented with "*".
+ */
+class ATTR_DLL_LOCAL IAddonInstance
+{
+public:
+ explicit IAddonInstance(const kodi::addon::IInstanceInfo& instance) : m_instance(instance)
+ {
+ m_instance->functions->instance_setting_change_string = INSTANCE_instance_setting_change_string;
+ m_instance->functions->instance_setting_change_integer =
+ INSTANCE_instance_setting_change_integer;
+ m_instance->functions->instance_setting_change_boolean =
+ INSTANCE_instance_setting_change_boolean;
+ m_instance->functions->instance_setting_change_float = INSTANCE_instance_setting_change_float;
+ }
+ virtual ~IAddonInstance() = default;
+
+ virtual ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+ KODI_ADDON_INSTANCE_HDL& hdl)
+ {
+ return ADDON_STATUS_NOT_IMPLEMENTED;
+ }
+
+ std::string GetInstanceAPIVersion() const { return m_instance->info->version; }
+
+ virtual ADDON_STATUS SetInstanceSetting(const std::string& settingName,
+ const kodi::addon::CSettingValue& settingValue)
+ {
+ return ADDON_STATUS_UNKNOWN;
+ }
+
+ inline bool IsInstanceSettingUsingDefault(const std::string& settingName)
+ {
+ return m_instance->info->functions->is_instance_setting_using_default(m_instance->info->kodi,
+ settingName.c_str());
+ }
+
+ inline std::string GetInstanceUserPath(const std::string& append = "")
+ {
+ using namespace kodi::addon;
+
+ char* str = m_instance->info->functions->get_instance_user_path(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+ std::string ret = str;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ str);
+ if (!append.empty())
+ {
+ if (append.at(0) != '\\' && append.at(0) != '/')
+#ifdef TARGET_WINDOWS
+ ret.append("\\");
+#else
+ ret.append("/");
+#endif
+ ret.append(append);
+ }
+ return ret;
+ }
+
+ inline bool CheckInstanceSettingString(const std::string& settingName, std::string& settingValue)
+ {
+ char* buffer = nullptr;
+ bool ret = m_instance->info->functions->get_instance_setting_string(
+ m_instance->info->kodi, settingName.c_str(), &buffer);
+ if (buffer)
+ {
+ if (ret)
+ settingValue = buffer;
+ free(buffer);
+ }
+ return ret;
+ }
+
+ inline std::string GetInstanceSettingString(const std::string& settingName,
+ const std::string& defaultValue = "")
+ {
+ std::string settingValue = defaultValue;
+ CheckInstanceSettingString(settingName, settingValue);
+ return settingValue;
+ }
+
+ inline void SetInstanceSettingString(const std::string& settingName,
+ const std::string& settingValue)
+ {
+ m_instance->info->functions->set_instance_setting_string(
+ m_instance->info->kodi, settingName.c_str(), settingValue.c_str());
+ }
+
+ inline bool CheckInstanceSettingInt(const std::string& settingName, int& settingValue)
+ {
+ KODI_ADDON_INSTANCE_FUNC_CB* cb = m_instance->info->functions;
+ return cb->get_instance_setting_int(m_instance->info->kodi, settingName.c_str(), &settingValue);
+ }
+
+ inline int GetInstanceSettingInt(const std::string& settingName, int defaultValue = 0)
+ {
+ int settingValue = defaultValue;
+ CheckInstanceSettingInt(settingName, settingValue);
+ return settingValue;
+ }
+
+ inline void SetInstanceSettingInt(const std::string& settingName, int settingValue)
+ {
+ m_instance->info->functions->set_instance_setting_int(m_instance->info->kodi,
+ settingName.c_str(), settingValue);
+ }
+
+ inline bool CheckInstanceSettingBoolean(const std::string& settingName, bool& settingValue)
+ {
+ return m_instance->info->functions->get_instance_setting_bool(
+ m_instance->info->kodi, settingName.c_str(), &settingValue);
+ }
+
+ inline bool GetInstanceSettingBoolean(const std::string& settingName, bool defaultValue = false)
+ {
+ bool settingValue = defaultValue;
+ CheckInstanceSettingBoolean(settingName, settingValue);
+ return settingValue;
+ }
+
+ inline void SetInstanceSettingBoolean(const std::string& settingName, bool settingValue)
+ {
+ m_instance->info->functions->set_instance_setting_bool(m_instance->info->kodi,
+ settingName.c_str(), settingValue);
+ }
+
+ inline bool CheckInstanceSettingFloat(const std::string& settingName, float& settingValue)
+ {
+ return m_instance->info->functions->get_instance_setting_float(
+ m_instance->info->kodi, settingName.c_str(), &settingValue);
+ }
+
+ inline float GetInstanceSettingFloat(const std::string& settingName, float defaultValue = 0.0f)
+ {
+ float settingValue = defaultValue;
+ CheckInstanceSettingFloat(settingName, settingValue);
+ return settingValue;
+ }
+
+ inline void SetInstanceSettingFloat(const std::string& settingName, float settingValue)
+ {
+ m_instance->info->functions->set_instance_setting_float(m_instance->info->kodi,
+ settingName.c_str(), settingValue);
+ }
+
+ template<typename enumType>
+ inline bool CheckInstanceSettingEnum(const std::string& settingName, enumType& settingValue)
+ {
+ using namespace kodi::addon;
+
+ int settingValueInt = static_cast<int>(settingValue);
+ bool ret = m_instance->info->functions->get_instance_setting_int(
+ m_instance->info->kodi, settingName.c_str(), &settingValueInt);
+ if (ret)
+ settingValue = static_cast<enumType>(settingValueInt);
+ return ret;
+ }
+
+ template<typename enumType>
+ inline enumType GetInstanceSettingEnum(const std::string& settingName,
+ enumType defaultValue = static_cast<enumType>(0))
+ {
+ enumType settingValue = defaultValue;
+ CheckInstanceSettingEnum(settingName, settingValue);
+ return settingValue;
+ }
+
+ template<typename enumType>
+ inline void SetInstanceSettingEnum(const std::string& settingName, enumType settingValue)
+ {
+ m_instance->info->functions->set_instance_setting_int(
+ m_instance->info->kodi, settingName.c_str(), static_cast<int>(settingValue));
+ }
+
+private:
+ static inline ADDON_STATUS INSTANCE_instance_setting_change_string(
+ const KODI_ADDON_INSTANCE_HDL hdl, const char* name, const char* value)
+ {
+ return static_cast<IAddonInstance*>(hdl)->SetInstanceSetting(name, CSettingValue(value));
+ }
+
+ static inline ADDON_STATUS INSTANCE_instance_setting_change_boolean(
+ const KODI_ADDON_INSTANCE_HDL hdl, const char* name, bool value)
+ {
+ return static_cast<IAddonInstance*>(hdl)->SetInstanceSetting(name,
+ CSettingValue(value ? "1" : "0"));
+ }
+
+ static inline ADDON_STATUS INSTANCE_instance_setting_change_integer(
+ const KODI_ADDON_INSTANCE_HDL hdl, const char* name, int value)
+ {
+ return static_cast<IAddonInstance*>(hdl)->SetInstanceSetting(
+ name, CSettingValue(std::to_string(value)));
+ }
+
+ static inline ADDON_STATUS INSTANCE_instance_setting_change_float(
+ const KODI_ADDON_INSTANCE_HDL hdl, const char* name, float value)
+ {
+ return static_cast<IAddonInstance*>(hdl)->SetInstanceSetting(
+ name, CSettingValue(std::to_string(value)));
+ }
+
+ friend class CAddonBase;
+
+ const KODI_ADDON_INSTANCE_STRUCT* m_instance;
+};
+
+//============================================================================
+/// @addtogroup cpp_kodi_addon_addonbase
+/// @brief **Add-on main instance class**\n
+/// This is the addon main class, similar to an <b>`int main()`</b> in executable and
+/// carries out initial work and later management of it.
+///
+class ATTR_DLL_LOCAL CAddonBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_addonbase
+ /// @brief Addon base class constructor.
+ ///
+ CAddonBase()
+ {
+ CPrivateBase::m_interface->toAddon->create = nullptr;
+ CPrivateBase::m_interface->toAddon->destroy = ADDONBASE_Destroy;
+ CPrivateBase::m_interface->toAddon->create_instance = ADDONBASE_CreateInstance;
+ CPrivateBase::m_interface->toAddon->destroy_instance = ADDONBASE_DestroyInstance;
+ CPrivateBase::m_interface->toAddon->setting_change_string = ADDONBASE_setting_change_string;
+ CPrivateBase::m_interface->toAddon->setting_change_boolean = ADDONBASE_setting_change_boolean;
+ CPrivateBase::m_interface->toAddon->setting_change_integer = ADDONBASE_setting_change_integer;
+ CPrivateBase::m_interface->toAddon->setting_change_float = ADDONBASE_setting_change_float;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_addonbase
+ /// @brief Destructor.
+ ///
+ virtual ~CAddonBase() = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_addonbase
+ /// @brief Main addon creation function
+ ///
+ /// With this function addon can carry out necessary work which is required
+ /// at later points or start necessary processes.
+ ///
+ /// This function is optional and necessary work can also be carried out
+ /// using @ref CreateInstance (if it concerns any instance types).
+ ///
+ /// @return @ref ADDON_STATUS_OK if correct, for possible errors see
+ /// @ref ADDON_STATUS
+ ///
+ /// @note Terminating the add-on must be carried out using the class destructor
+ /// given by child.
+ ///
+ virtual ADDON_STATUS Create() { return ADDON_STATUS_OK; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_addonbase
+ /// @brief To inform addon about changed settings values.
+ ///
+ /// This becomes called for every entry defined inside his settings.xml and
+ /// as **last** call the one where last in xml (to identify end of calls).
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_addonbase_Defs_CSettingValue_Help
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Here is a code example how this is used:**
+ ///
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/AddonBase.h>
+ ///
+ /// enum myEnumValue
+ /// {
+ /// valueA,
+ /// valueB,
+ /// valueC
+ /// };
+ ///
+ /// std::string m_myStringValue;
+ /// int m_myIntegerValue;
+ /// bool m_myBooleanValue;
+ /// float m_myFloatingPointValue;
+ /// myEnumValue m_myEnumValue;
+ ///
+ ///
+ /// ADDON_STATUS CMyAddon::SetSetting(const std::string& settingName, const kodi::addon::CSettingValue& settingValue)
+ /// {
+ /// if (settingName == "my_string_value")
+ /// m_myStringValue = settingValue.GetString();
+ /// else if (settingName == "my_integer_value")
+ /// m_myIntegerValue = settingValue.GetInt();
+ /// else if (settingName == "my_boolean_value")
+ /// m_myBooleanValue = settingValue.GetBoolean();
+ /// else if (settingName == "my_float_value")
+ /// m_myFloatingPointValue = settingValue.GetFloat();
+ /// else if (settingName == "my_enum_value")
+ /// m_myEnumValue = settingValue.GetEnum<myEnumValue>();
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ /// @note The asked type should match the type used on settings.xml.
+ ///
+ virtual ADDON_STATUS SetSetting(const std::string& settingName,
+ const kodi::addon::CSettingValue& settingValue)
+ {
+ return ADDON_STATUS_UNKNOWN;
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_addonbase
+ /// @brief Instance created
+ ///
+ /// @param[in] instance Instance informations about
+ /// @param[out] hdl The pointer to instance class created in addon.
+ /// Needed to be able to identify them on calls.
+ /// @return @ref ADDON_STATUS_OK if correct, for possible errors see @ref ADDON_STATUS
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Here is a code example how this is used:**
+ ///
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/AddonBase.h>
+ ///
+ /// ...
+ ///
+ /// // If you use only one instance in your add-on, can be instanceType and
+ /// // instanceID ignored
+ /// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+ /// KODI_ADDON_INSTANCE_HDL& hdl)
+ /// {
+ /// if (instance.IsType(ADDON_INSTANCE_SCREENSAVER))
+ /// {
+ /// kodi::Log(ADDON_LOG_INFO, "Creating my Screensaver");
+ /// hdl = new CMyScreensaver(instance);
+ /// return ADDON_STATUS_OK;
+ /// }
+ /// else if (instance.IsType(ADDON_INSTANCE_VISUALIZATION))
+ /// {
+ /// kodi::Log(ADDON_LOG_INFO, "Creating my Visualization");
+ /// hdl = new CMyVisualization(instance);
+ /// return ADDON_STATUS_OK;
+ /// }
+ /// else if (...)
+ /// {
+ /// ...
+ /// }
+ /// return ADDON_STATUS_UNKNOWN;
+ /// }
+ ///
+ /// ...
+ ///
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+ KODI_ADDON_INSTANCE_HDL& hdl)
+ {
+ return ADDON_STATUS_NOT_IMPLEMENTED;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_addonbase
+ /// @brief Instance destroy
+ ///
+ /// This function is optional and intended to notify addon that the instance
+ /// is terminating.
+ ///
+ /// @param[in] instance Instance informations about
+ /// @param[in] hdl The pointer to instance class created in addon.
+ ///
+ /// @warning This call is only used to inform that the associated instance
+ /// is terminated. The deletion is carried out in the background.
+ ///
+ virtual void DestroyInstance(const IInstanceInfo& instance, const KODI_ADDON_INSTANCE_HDL hdl) {}
+ //--------------------------------------------------------------------------
+
+ /* Background helper for GUI render systems, e.g. Screensaver or Visualization */
+ std::shared_ptr<kodi::gui::IRenderHelper> m_renderHelper;
+
+private:
+ static inline void ADDONBASE_Destroy(const KODI_ADDON_HDL hdl)
+ {
+ delete static_cast<CAddonBase*>(hdl);
+ }
+
+ static inline ADDON_STATUS ADDONBASE_CreateInstance(const KODI_ADDON_HDL hdl,
+ struct KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ CAddonBase* base = static_cast<CAddonBase*>(hdl);
+
+ ADDON_STATUS status = ADDON_STATUS_NOT_IMPLEMENTED;
+
+ /* Check about single instance usage:
+ * 1. The kodi side instance pointer must be equal to first one
+ * 2. The addon side instance pointer must be set
+ * 3. And the requested type must be equal with used add-on class
+ */
+ if (CPrivateBase::m_interface->firstKodiInstance == instance &&
+ CPrivateBase::m_interface->globalSingleInstance &&
+ static_cast<IAddonInstance*>(CPrivateBase::m_interface->globalSingleInstance)
+ ->m_instance->info->type == instance->info->type)
+ {
+ /* The handling here is intended for the case of the add-on only one
+ * instance and this is integrated in the add-on base class.
+ */
+ instance->hdl = CPrivateBase::m_interface->globalSingleInstance;
+ status = ADDON_STATUS_OK;
+ }
+ else
+ {
+ /* Here it should use the CreateInstance instance function to allow
+ * creation of several on one addon.
+ */
+
+ IInstanceInfo instanceInfo(instance);
+
+ /* Check first a parent is defined about (e.g. Codec within inputstream) */
+ if (instance->info->parent != nullptr)
+ status = static_cast<IAddonInstance*>(instance->info->parent)
+ ->CreateInstance(instanceInfo, instance->hdl);
+
+ /* if no parent call the main instance creation function to get it */
+ if (status == ADDON_STATUS_NOT_IMPLEMENTED)
+ {
+ status = base->CreateInstance(instanceInfo, instance->hdl);
+ }
+ }
+
+ if (instance->hdl == nullptr)
+ {
+ if (status == ADDON_STATUS_OK)
+ {
+ CPrivateBase::m_interface->toKodi->addon_log_msg(
+ CPrivateBase::m_interface->toKodi->kodiBase, ADDON_LOG_FATAL,
+ "kodi::addon::CAddonBase CreateInstance returned an "
+ "empty instance pointer, but reported OK!");
+ return ADDON_STATUS_PERMANENT_FAILURE;
+ }
+ else
+ {
+ return status;
+ }
+ }
+
+ if (static_cast<IAddonInstance*>(instance->hdl)->m_instance->info->type != instance->info->type)
+ {
+ CPrivateBase::m_interface->toKodi->addon_log_msg(
+ CPrivateBase::m_interface->toKodi->kodiBase, ADDON_LOG_FATAL,
+ "kodi::addon::CAddonBase CreateInstance difference between given and returned");
+ delete static_cast<IAddonInstance*>(instance->hdl);
+ instance->hdl = nullptr;
+ return ADDON_STATUS_PERMANENT_FAILURE;
+ }
+
+ return status;
+ }
+
+ static inline void ADDONBASE_DestroyInstance(const KODI_ADDON_HDL hdl,
+ struct KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ CAddonBase* base = static_cast<CAddonBase*>(hdl);
+
+ if (CPrivateBase::m_interface->globalSingleInstance == nullptr && instance->hdl != base)
+ {
+ IInstanceInfo instanceInfo(instance);
+ base->DestroyInstance(instanceInfo, instance->hdl);
+ delete static_cast<IAddonInstance*>(instance->hdl);
+ }
+ }
+
+ static inline ADDON_STATUS ADDONBASE_setting_change_string(const KODI_ADDON_HDL hdl,
+ const char* name,
+ const char* value)
+ {
+ return static_cast<CAddonBase*>(hdl)->SetSetting(name, CSettingValue(value));
+ }
+
+ static inline ADDON_STATUS ADDONBASE_setting_change_boolean(const KODI_ADDON_HDL hdl,
+ const char* name,
+ bool value)
+ {
+ return static_cast<CAddonBase*>(hdl)->SetSetting(name, CSettingValue(value ? "1" : "0"));
+ }
+
+ static inline ADDON_STATUS ADDONBASE_setting_change_integer(const KODI_ADDON_HDL hdl,
+ const char* name,
+ int value)
+ {
+ return static_cast<CAddonBase*>(hdl)->SetSetting(name, CSettingValue(std::to_string(value)));
+ }
+
+ static inline ADDON_STATUS ADDONBASE_setting_change_float(const KODI_ADDON_HDL hdl,
+ const char* name,
+ float value)
+ {
+ return static_cast<CAddonBase*>(hdl)->SetSetting(name, CSettingValue(std::to_string(value)));
+ }
+};
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon
+/// @brief To get the addon system installation folder.
+///
+/// @param[in] append [optional] Path to append to given string
+/// @return Path where addon is installed
+///
+inline std::string ATTR_DLL_LOCAL GetAddonPath(const std::string& append = "")
+{
+ using namespace kodi::addon;
+
+ char* str = CPrivateBase::m_interface->toKodi->kodi_addon->get_addon_path(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+ std::string ret = str;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase, str);
+ if (!append.empty())
+ {
+ if (append.at(0) != '\\' && append.at(0) != '/')
+#ifdef TARGET_WINDOWS
+ ret.append("\\");
+#else
+ ret.append("/");
+#endif
+ ret.append(append);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon
+/// @brief This function gives OS associated executable binary path of the addon.
+///
+/// With some systems this can differ from the addon path at @ref GetAddonPath.
+///
+/// As an example on Linux:
+/// - Addon path is at `/usr/share/kodi/addons/YOUR_ADDON_ID`
+/// - Library path is at `/usr/lib/kodi/addons/YOUR_ADDON_ID`
+///
+/// @note In addition, in this function, for a given folder, the add-on path
+/// itself, but its parent.
+///
+/// @return Kodi's system library path where related addons are installed.
+///
+inline std::string ATTR_DLL_LOCAL GetLibPath(const std::string& append = "")
+{
+ using namespace kodi::addon;
+
+ char* str = CPrivateBase::m_interface->toKodi->kodi_addon->get_lib_path(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+ std::string ret = str;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase, str);
+ if (!append.empty())
+ {
+ if (append.at(0) != '\\' && append.at(0) != '/')
+#ifdef TARGET_WINDOWS
+ ret.append("\\");
+#else
+ ret.append("/");
+#endif
+ ret.append(append);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon
+/// @brief To get the user-related folder of the addon.
+///
+/// @note This folder is not created automatically and has to be created by the
+/// addon the first time.
+///
+/// @param[in] append [optional] Path to append to given string
+/// @return User path of addon
+///
+inline std::string ATTR_DLL_LOCAL GetUserPath(const std::string& append = "")
+{
+ using namespace kodi::addon;
+
+ char* str = CPrivateBase::m_interface->toKodi->kodi_addon->get_user_path(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+ std::string ret = str;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase, str);
+ if (!append.empty())
+ {
+ if (append.at(0) != '\\' && append.at(0) != '/')
+#ifdef TARGET_WINDOWS
+ ret.append("\\");
+#else
+ ret.append("/");
+#endif
+ ret.append(append);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon
+/// @brief To get a temporary path for the addon
+///
+/// This gives a temporary path which the addon can use individually for its things.
+///
+/// The content of this folder will be deleted when Kodi is finished!
+///
+/// @param[in] append A string to append to returned temporary path
+/// @return Individual path for the addon
+///
+inline std::string ATTR_DLL_LOCAL GetTempPath(const std::string& append = "")
+{
+ using namespace kodi::addon;
+
+ char* str = CPrivateBase::m_interface->toKodi->kodi_addon->get_temp_path(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+ std::string ret = str;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase, str);
+ if (!append.empty())
+ {
+ if (append.at(0) != '\\' && append.at(0) != '/')
+#ifdef TARGET_WINDOWS
+ ret.append("\\");
+#else
+ ret.append("/");
+#endif
+ ret.append(append);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon
+/// @brief Returns an addon's localized 'unicode string'.
+///
+/// @param[in] labelId string you want to localize
+/// @param[in] defaultStr [opt] The default message, also helps to identify
+/// the code that is used <em>(default is
+/// <b><c>empty</c></b>)</em>
+/// @return The localized message, or default if the add-on
+/// helper fails to return a message
+///
+/// @note Label id's \b 30000 to \b 30999 and \b 32000 to \b 32999 are related
+/// to the add-on's own included strings from
+/// <b>./resources/language/resource.language.??_??/strings.po</b>
+/// All other strings are from Kodi core language files.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// std::string str = kodi::GetLocalizedString(30005, "Use me as default");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetLocalizedString(uint32_t labelId,
+ const std::string& defaultStr = "")
+{
+ using namespace kodi::addon;
+
+ std::string retString = defaultStr;
+ char* strMsg = CPrivateBase::m_interface->toKodi->kodi_addon->get_localized_string(
+ CPrivateBase::m_interface->toKodi->kodiBase, labelId);
+ if (strMsg != nullptr)
+ {
+ if (std::strlen(strMsg))
+ retString = strMsg;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ strMsg);
+ }
+ return retString;
+}
+//------------------------------------------------------------------------------
+
+
+//##############################################################################
+/// @ingroup cpp_kodi_addon
+/// @defgroup cpp_kodi_settings 1. Setting control
+/// @brief **Functions to handle settings access**\n
+/// This can be used to get and set the addon related values inside his
+/// settings.xml.
+///
+/// The settings style is given with installed part on e.g.
+/// <b>`$HOME/.kodi/addons/myspecial.addon/resources/settings.xml`</b>. The
+/// related edit becomes then stored inside
+/// <b>`$HOME/.kodi/userdata/addon_data/myspecial.addon/settings.xml`</b>.
+///
+/*!@{*/
+
+//==============================================================================
+/// @brief Opens this Add-Ons settings dialog.
+///
+/// @return true if settings were changed and the dialog confirmed, false otherwise.
+///
+///
+/// --------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ..
+/// kodi::OpenSettings();
+/// ..
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL OpenSettings()
+{
+ using namespace kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_addon->open_settings_dialog(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Check the given setting name is set to default value.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of asked setting
+/// @return true if setting is the default
+///
+inline bool ATTR_DLL_LOCAL IsSettingUsingDefault(const std::string& settingName)
+{
+ using namespace kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_addon->is_setting_using_default(
+ CPrivateBase::m_interface->toKodi->kodiBase, settingName.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Check and get a string setting value.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of asked setting
+/// @param[out] settingValue The given setting value
+/// @return true if setting was successfully found and "settingValue" is set
+///
+/// @note If returns false, the "settingValue" is not changed.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// std::string value;
+/// if (!kodi::CheckSettingString("my_string_value", value))
+/// value = "my_default_if_setting_not_work";
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL CheckSettingString(const std::string& settingName,
+ std::string& settingValue)
+{
+ using namespace kodi::addon;
+
+ char* buffer = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_addon->get_setting_string(
+ CPrivateBase::m_interface->toKodi->kodiBase, settingName.c_str(), &buffer);
+ if (buffer)
+ {
+ if (ret)
+ settingValue = buffer;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ buffer);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Get string setting value.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of asked setting
+/// @param[in] defaultValue [opt] Default value if not found
+/// @return The value of setting, empty if not found;
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// std::string value = kodi::GetSettingString("my_string_value");
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetSettingString(const std::string& settingName,
+ const std::string& defaultValue = "")
+{
+ std::string settingValue = defaultValue;
+ CheckSettingString(settingName, settingValue);
+ return settingValue;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Set string setting of addon.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of setting
+/// @param[in] settingValue The setting value to write
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// std::string value = "my_new_name for";
+/// kodi::SetSettingString("my_string_value", value);
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL SetSettingString(const std::string& settingName,
+ const std::string& settingValue)
+{
+ using namespace kodi::addon;
+
+ CPrivateBase::m_interface->toKodi->kodi_addon->set_setting_string(
+ CPrivateBase::m_interface->toKodi->kodiBase, settingName.c_str(), settingValue.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Check and get a integer setting value.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of asked setting
+/// @param[out] settingValue The given setting value
+/// @return true if setting was successfully found and "settingValue" is set
+///
+/// @note If returns false, the "settingValue" is not changed.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// int value = 0;
+/// if (!kodi::CheckSettingInt("my_integer_value", value))
+/// value = 123; // My default of them
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL CheckSettingInt(const std::string& settingName, int& settingValue)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_addon->get_setting_int(
+ CPrivateBase::m_interface->toKodi->kodiBase, settingName.c_str(), &settingValue);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Get integer setting value.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of asked setting
+/// @param[in] defaultValue [opt] Default value if not found
+/// @return The value of setting, <b>`0`</b> or defaultValue if not found
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// int value = kodi::GetSettingInt("my_integer_value");
+/// ~~~~~~~~~~~~~
+///
+inline int ATTR_DLL_LOCAL GetSettingInt(const std::string& settingName, int defaultValue = 0)
+{
+ int settingValue = defaultValue;
+ CheckSettingInt(settingName, settingValue);
+ return settingValue;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Set integer setting of addon.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of setting
+/// @param[in] settingValue The setting value to write
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// int value = 123;
+/// kodi::SetSettingInt("my_integer_value", value);
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL SetSettingInt(const std::string& settingName, int settingValue)
+{
+ using namespace kodi::addon;
+
+ CPrivateBase::m_interface->toKodi->kodi_addon->set_setting_int(
+ CPrivateBase::m_interface->toKodi->kodiBase, settingName.c_str(), settingValue);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Check and get a boolean setting value.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of asked setting
+/// @param[out] settingValue The given setting value
+/// @return true if setting was successfully found and "settingValue" is set
+///
+/// @note If returns false, the "settingValue" is not changed.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// bool value = false;
+/// if (!kodi::CheckSettingBoolean("my_boolean_value", value))
+/// value = true; // My default of them
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL CheckSettingBoolean(const std::string& settingName, bool& settingValue)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_addon->get_setting_bool(
+ CPrivateBase::m_interface->toKodi->kodiBase, settingName.c_str(), &settingValue);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Get boolean setting value.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of asked setting
+/// @param[in] defaultValue [opt] Default value if not found
+/// @return The value of setting, <b>`false`</b> or defaultValue if not found
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// bool value = kodi::GetSettingBoolean("my_boolean_value");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL GetSettingBoolean(const std::string& settingName,
+ bool defaultValue = false)
+{
+ bool settingValue = defaultValue;
+ CheckSettingBoolean(settingName, settingValue);
+ return settingValue;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Set boolean setting of addon.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of setting
+/// @param[in] settingValue The setting value to write
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// bool value = true;
+/// kodi::SetSettingBoolean("my_boolean_value", value);
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL SetSettingBoolean(const std::string& settingName, bool settingValue)
+{
+ using namespace kodi::addon;
+
+ CPrivateBase::m_interface->toKodi->kodi_addon->set_setting_bool(
+ CPrivateBase::m_interface->toKodi->kodiBase, settingName.c_str(), settingValue);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Check and get a floating point setting value.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of asked setting
+/// @param[out] settingValue The given setting value
+/// @return true if setting was successfully found and "settingValue" is set
+///
+/// @note If returns false, the "settingValue" is not changed.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// float value = 0.0f;
+/// if (!kodi::CheckSettingBoolean("my_float_value", value))
+/// value = 1.0f; // My default of them
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL CheckSettingFloat(const std::string& settingName, float& settingValue)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_addon->get_setting_float(
+ CPrivateBase::m_interface->toKodi->kodiBase, settingName.c_str(), &settingValue);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Get floating point setting value.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of asked setting
+/// @param[in] defaultValue [opt] Default value if not found
+/// @return The value of setting, <b>`0.0`</b> or defaultValue if not found
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// float value = kodi::GetSettingFloat("my_float_value");
+/// ~~~~~~~~~~~~~
+///
+inline float ATTR_DLL_LOCAL GetSettingFloat(const std::string& settingName,
+ float defaultValue = 0.0f)
+{
+ float settingValue = defaultValue;
+ CheckSettingFloat(settingName, settingValue);
+ return settingValue;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Set floating point setting of addon.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of setting
+/// @param[in] settingValue The setting value to write
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// float value = 1.0f;
+/// kodi::SetSettingFloat("my_float_value", value);
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL SetSettingFloat(const std::string& settingName, float settingValue)
+{
+ using namespace kodi::addon;
+
+ CPrivateBase::m_interface->toKodi->kodi_addon->set_setting_float(
+ CPrivateBase::m_interface->toKodi->kodiBase, settingName.c_str(), settingValue);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Check and get a enum setting value.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of asked setting
+/// @param[out] settingValue The given setting value
+/// @return true if setting was successfully found and "settingValue" is set
+///
+/// @remark The enums are used as integer inside settings.xml.
+/// @note If returns false, the "settingValue" is not changed.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// enum myEnumValue
+/// {
+/// valueA,
+/// valueB,
+/// valueC
+/// };
+///
+/// myEnumValue value;
+/// if (!kodi::CheckSettingEnum<myEnumValue>("my_enum_value", value))
+/// value = valueA; // My default of them
+/// ~~~~~~~~~~~~~
+///
+template<typename enumType>
+inline bool ATTR_DLL_LOCAL CheckSettingEnum(const std::string& settingName, enumType& settingValue)
+{
+ using namespace kodi::addon;
+
+ int settingValueInt = static_cast<int>(settingValue);
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_addon->get_setting_int(
+ CPrivateBase::m_interface->toKodi->kodiBase, settingName.c_str(), &settingValueInt);
+ if (ret)
+ settingValue = static_cast<enumType>(settingValueInt);
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Get enum setting value.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of asked setting
+/// @param[in] defaultValue [opt] Default value if not found
+/// @return The value of setting, forced to <b>`0`</b> or defaultValue if not found
+///
+/// @remark The enums are used as integer inside settings.xml.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// enum myEnumValue
+/// {
+/// valueA,
+/// valueB,
+/// valueC
+/// };
+///
+/// myEnumValue value = kodi::GetSettingEnum<myEnumValue>("my_enum_value");
+/// ~~~~~~~~~~~~~
+///
+template<typename enumType>
+inline enumType ATTR_DLL_LOCAL GetSettingEnum(const std::string& settingName,
+ enumType defaultValue = static_cast<enumType>(0))
+{
+ enumType settingValue = defaultValue;
+ CheckSettingEnum(settingName, settingValue);
+ return settingValue;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @brief Set enum setting of addon.
+///
+/// The setting name relate to names used in his <b>settings.xml</b> file.
+///
+/// @param[in] settingName The name of setting
+/// @param[in] settingValue The setting value to write
+///
+/// @remark The enums are used as integer inside settings.xml.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// enum myEnumValue
+/// {
+/// valueA,
+/// valueB,
+/// valueC
+/// };
+///
+/// myEnumValue value = valueA;
+/// kodi::SetSettingEnum<myEnumValue>("my_enum_value", value);
+/// ~~~~~~~~~~~~~
+///
+template<typename enumType>
+inline void ATTR_DLL_LOCAL SetSettingEnum(const std::string& settingName, enumType settingValue)
+{
+ using namespace kodi::addon;
+
+ CPrivateBase::m_interface->toKodi->kodi_addon->set_setting_int(
+ CPrivateBase::m_interface->toKodi->kodiBase, settingName.c_str(),
+ static_cast<int>(settingValue));
+}
+//------------------------------------------------------------------------------
+
+/*!@}*/
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon
+/// @brief Returns the value of an addon property as a string
+///
+/// @param[in] id id of the property that the module needs to access
+/// | | Choices are | |
+/// |:------------:|:------------:|:------------:|
+/// | author | icon | stars |
+/// | changelog | id | summary |
+/// | description | name | type |
+/// | disclaimer | path | version |
+/// | fanart | profile | |
+///
+/// @return AddOn property as a string
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// std::string addonName = kodi::GetAddonInfo("name");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetAddonInfo(const std::string& id)
+{
+ using namespace kodi::addon;
+
+ AddonToKodiFuncTable_Addon* toKodi = CPrivateBase::m_interface->toKodi;
+
+ std::string strReturn;
+ char* strMsg = toKodi->kodi_addon->get_addon_info(toKodi->kodiBase, id.c_str());
+ if (strMsg != nullptr)
+ {
+ if (std::strlen(strMsg))
+ strReturn = strMsg;
+ toKodi->free_string(toKodi->kodiBase, strMsg);
+ }
+ return strReturn;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon
+/// @brief Returns a function table to a named interface
+///
+/// @return pointer to struct containing interface functions
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// #include <kodi/platform/foo.h>
+/// ...
+/// FuncTable_foo *table = kodi::GetPlatformInfo(foo_name, foo_version);
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline void* GetInterface(const std::string& name, const std::string& version)
+{
+ using namespace kodi::addon;
+
+ AddonToKodiFuncTable_Addon* toKodi = CPrivateBase::m_interface->toKodi;
+
+ return toKodi->kodi_addon->get_interface(toKodi->kodiBase, name.c_str(), version.c_str());
+}
+//----------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon
+/// @brief To get used version inside Kodi itself about asked type.
+///
+/// This thought to allow a addon a handling of newer addon versions within
+/// older Kodi until the type min version not changed.
+///
+/// @param[in] type The wanted type of @ref ADDON_TYPE to ask
+/// @return The version string about type in MAJOR.MINOR.PATCH style.
+///
+inline std::string ATTR_DLL_LOCAL GetKodiTypeVersion(int type)
+{
+ using namespace kodi::addon;
+
+ char* str = CPrivateBase::m_interface->toKodi->kodi_addon->get_type_version(
+ CPrivateBase::m_interface->toKodi->kodiBase, type);
+ std::string ret = str;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase, str);
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_addon
+/// @brief Get to related @ref ADDON_STATUS a human readable text.
+///
+/// @param[in] status Status value to get name for
+/// @return Wanted name, as "Unknown" if status not known
+///
+inline std::string ATTR_DLL_LOCAL TranslateAddonStatus(ADDON_STATUS status)
+{
+ switch (status)
+ {
+ case ADDON_STATUS_OK:
+ return "OK";
+ case ADDON_STATUS_LOST_CONNECTION:
+ return "Lost Connection";
+ case ADDON_STATUS_NEED_RESTART:
+ return "Need Restart";
+ case ADDON_STATUS_NEED_SETTINGS:
+ return "Need Settings";
+ case ADDON_STATUS_UNKNOWN:
+ return "Unknown error";
+ case ADDON_STATUS_PERMANENT_FAILURE:
+ return "Permanent failure";
+ case ADDON_STATUS_NOT_IMPLEMENTED:
+ return "Not implemented";
+ default:
+ break;
+ }
+ return "Unknown";
+}
+//----------------------------------------------------------------------------
+
+} /* namespace addon */
+
+//==============================================================================
+/// @ingroup cpp_kodi
+/// @brief Add a message to Kodi's log.
+///
+/// @param[in] loglevel The log level of the message.
+/// @param[in] format The format of the message to pass to Kodi.
+/// @param[in] ... Additional text to insert in format text
+///
+///
+/// @note This method uses limited buffer (16k) for the formatted output.
+/// So data, which will not fit into it, will be silently discarded.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// kodi::Log(ADDON_LOG_ERROR, "%s: There is an error occurred!", __func__);
+///
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL Log(const ADDON_LOG loglevel, const char* format, ...)
+{
+ using namespace kodi::addon;
+
+ va_list args;
+ va_start(args, format);
+ const std::string str = kodi::tools::StringUtils::FormatV(format, args);
+ va_end(args);
+ CPrivateBase::m_interface->toKodi->addon_log_msg(CPrivateBase::m_interface->toKodi->kodiBase,
+ loglevel, str.c_str());
+}
+//------------------------------------------------------------------------------
+
+} /* namespace kodi */
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon_addonbase_Defs
+/// @defgroup cpp_kodi_addon_addonbase_Defs_ADDONCREATORAddonClass macro ADDONCREATOR(AddonClass)
+/// @brief **Addon creation macro**\n
+/// This export the three mandatory "C" functions to have available for Kodi.
+///
+/// @note Only this macro can be used on a C++ addon, everything else is done
+/// automatically.
+///
+/// @param[in] AddonClass Used addon class to be exported with CAddonBase as
+/// parent.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+///
+/// #include <kodi/AddonBash.h>
+///
+/// class CMyAddon : public kodi::addon::CAddonBase
+/// {
+/// public:
+/// CMyAddon() = default;
+/// ADDON_STATUS Create() override;
+/// };
+///
+/// ADDON_STATUS CMyAddon::Create()
+/// {
+/// // Some work
+///
+/// return ADDON_STATUS_OK;
+/// }
+///
+/// ADDONCREATOR(CMyAddon)
+/// ~~~~~~~~~~~~~
+///
+/// ----------------------------------------------------------------------------
+///
+/// As information, the following functions are exported using this macro:
+/// \table_start
+/// \table_h3{ Function, Use, Description }
+/// \table_row3{ <b>`ADDON_Create(KODI_HANDLE addonInterface)`</b>,
+/// \anchor ADDON_Create
+/// _required_,
+/// <b>Addon creation call.</b>
+/// <br>
+/// Like an `int main()` is this the first on addon called place on his start
+/// and create within C++ API related class inside addon.
+/// <br>
+/// @param[in] addonInterface Handle pointer to get Kodi's given table.
+/// There have addon needed values and functions
+/// to Kodi and addon must set his functions there
+/// for Kodi.
+/// @param[in] globalApiVersion This gives the main version @ref ADDON_GLOBAL_VERSION_MAIN
+/// where currently on Kodi's side.<br>
+/// This is unused on addon as there's also other
+/// special callback functions for.<br>
+/// Only thought for future use if needed earlier.
+/// @param[in] unused This is a not used value\, only there to have in case of
+/// need no Major API version increase where break current.
+/// @return @ref ADDON_STATUS_OK if correct\, for possible errors see
+/// @ref ADDON_STATUS.
+/// <p>
+/// }
+/// \table_row3{ <b>`const char* ADDON_GetTypeVersion(int type)`</b>,
+/// \anchor ADDON_GetTypeVersion
+/// _required_,
+/// <b>Ask addon about version of given type.</b>
+/// <br>
+/// This is used to query its associated version in the addon before work
+/// is carried out in it and the Kodi can adapt to it.
+/// <br>
+/// @param[in] type With @ref ADDON_TYPE defined type to ask.
+/// @return Version as string of asked type.
+/// <p>
+/// }
+/// \table_row3{ <b>`const char* ADDON_GetTypeMinVersion(int type)`</b>,
+/// \anchor ADDON_GetTypeMinVersion
+/// _optional_,
+/// <b>Ask addon about minimal version of given type.</b>
+/// <br>
+/// This is used to query its associated min version in the addon before work
+/// is carried out in it and the Kodi can adapt to it.
+/// <br>
+/// @note The minimum version is optional\, if it were not available\, the
+/// major version is used instead.
+/// <br>
+/// @param[in] type With @ref ADDON_TYPE defined type to ask.
+/// @return Min version as string of asked type.
+/// <p>
+/// }
+/// \table_end
+///
+#define ADDONCREATOR(AddonClass) \
+ extern "C" ATTR_DLL_EXPORT ADDON_STATUS ADDON_Create(KODI_HANDLE addonInterface) \
+ { \
+ using namespace kodi::addon; \
+ CPrivateBase::m_interface = static_cast<AddonGlobalInterface*>(addonInterface); \
+ CPrivateBase::m_interface->addonBase = new AddonClass; \
+ return static_cast<CAddonBase*>(CPrivateBase::m_interface->addonBase)->Create(); \
+ } \
+ extern "C" ATTR_DLL_EXPORT const char* ADDON_GetTypeVersion(int type) \
+ { \
+ return kodi::addon::GetTypeVersion(type); \
+ } \
+ extern "C" ATTR_DLL_EXPORT const char* ADDON_GetTypeMinVersion(int type) \
+ { \
+ return kodi::addon::GetTypeMinVersion(type); \
+ } \
+ AddonGlobalInterface* kodi::addon::CPrivateBase::m_interface = nullptr;
+//------------------------------------------------------------------------------
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/AudioEngine.h b/xbmc/addons/kodi-dev-kit/include/kodi/AudioEngine.h
new file mode 100644
index 0000000..2afe351
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/AudioEngine.h
@@ -0,0 +1,619 @@
+/*
+ * Copyright (C) 2005-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 "AddonBase.h"
+#include "c-api/audio_engine.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace audioengine
+{
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// Main page text for audio engine group by Doxygen.
+//{{{
+
+//==============================================================================
+///
+/// @defgroup cpp_kodi_audioengine Interface - kodi::audioengine
+/// @ingroup cpp
+/// @brief **Audio engine functions**\n
+/// This interface contains auxiliary functions and classes which allow an addon
+/// to play their own individual audio stream in Kodi.
+///
+/// Using @ref cpp_kodi_audioengine_CAEStream "kodi::audioengine::CAEStream",
+/// a class can be created in this regard, about which the necessary stream data and
+/// information are given to Kodi.
+///
+/// Via @ref kodi::audioengine::GetCurrentSinkFormat(), the audio formats currently
+/// processed in Kodi can be called up beforehand in order to adapt your own stream
+/// to them.
+///
+/// However, the created stream can also differ from this because Kodi changes
+/// it to suit it.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+///
+/// #include <kodi/AudioEngine.h>
+///
+/// ...
+///
+/// kodi::audioengine::AudioEngineFormat format;
+/// if (!kodi::audioengine::GetCurrentSinkFormat(format))
+/// return false;
+///
+/// format.SetDataFormat(AUDIOENGINE_FMT_FLOATP);
+/// format.SetChannelLayout(std::vector<AudioEngineChannel>(AUDIOENGINE_CH_FL, AUDIOENGINE_CH_FR));
+///
+/// unsigned int myUsedSampleRate = format.GetSampleRate();
+///
+/// ...
+///
+/// kodi::audioengine::CAEStream* stream = new kodi::audioengine::CAEStream(format, AUDIO_STREAM_AUTOSTART);
+///
+/// ~~~~~~~~~~~~~
+///
+/// ------------------------------------------------------------------------
+///
+/// It has the header @ref AudioEngine.h "#include <kodi/AudioEngine.h>" be included
+/// to enjoy it.
+///
+//------------------------------------------------------------------------------
+
+//==============================================================================
+///
+/// @defgroup cpp_kodi_audioengine_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_audioengine
+/// @brief **Library definition values**\n
+/// All audio engine functions associated data structures.
+///
+//------------------------------------------------------------------------------
+
+//}}}
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" related audio engine definitions
+//{{{
+
+//==============================================================================
+/// @defgroup cpp_kodi_audioengine_Defs_AudioEngineFormat class AudioEngineFormat
+/// @ingroup cpp_kodi_audioengine_Defs
+/// @brief **Audio format structure**\n
+/// The audio format structure that fully defines a stream's audio
+/// information.
+///
+/// With the help of this format information, Kodi adjusts its processing
+/// accordingly.
+///
+///@{
+class ATTR_DLL_LOCAL AudioEngineFormat
+ : public addon::CStructHdl<AudioEngineFormat, AUDIO_ENGINE_FORMAT>
+{
+public:
+ /*! \cond PRIVATE */
+ AudioEngineFormat()
+ {
+ m_cStructure->m_dataFormat = AUDIOENGINE_FMT_INVALID;
+ m_cStructure->m_sampleRate = 0;
+ m_cStructure->m_encodedRate = 0;
+ m_cStructure->m_frames = 0;
+ m_cStructure->m_frameSize = 0;
+ m_cStructure->m_channelCount = 0;
+
+ for (size_t ch = 0; ch < AUDIOENGINE_CH_MAX; ++ch)
+ m_cStructure->m_channels[ch] = AUDIOENGINE_CH_NULL;
+ }
+ AudioEngineFormat(const AudioEngineFormat& channel) : CStructHdl(channel) {}
+ AudioEngineFormat(const AUDIO_ENGINE_FORMAT* channel) : CStructHdl(channel) {}
+ AudioEngineFormat(AUDIO_ENGINE_FORMAT* channel) : CStructHdl(channel) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_audioengine_Defs_AudioEngineFormat_Help *Value Help*
+ /// @ingroup cpp_kodi_audioengine_Defs_AudioEngineFormat
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_audioengine_Defs_AudioEngineFormat :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Data format**, see @ref AudioEngineDataFormat for available types | enum | @ref AudioEngineFormat::SetDataFormat "SetDataFormat" | @ref AudioEngineFormat::GetDataFormat "GetDataFormat"
+ /// | **Sample rate** | unsigned int | @ref AudioEngineFormat::SetSampleRate "SetSampleRate" | @ref AudioEngineFormat::GetSampleRate "GetSampleRate"
+ /// | **Encoded rate** | unsigned int | @ref AudioEngineFormat::SetEncodedRate "SetEncodedRate" | @ref AudioEngineFormat::GetEncodedRate "GetEncodedRate"
+ /// | **Channel layout**, see @ref AudioEngineChannel for available types | std::vector<enum AudioEngineChannel> | @ref AudioEngineFormat::SetChannelLayout "SetChannelLayout" | @ref AudioEngineFormat::GetChannelLayout "GetChannelLayout"
+ /// | **Frames amount** | unsigned int | @ref AudioEngineFormat::SetFramesAmount "SetFramesAmount" | @ref AudioEngineFormat::GetFramesAmount "GetFramesAmount"
+ /// | **Frame size** | unsigned int | @ref AudioEngineFormat::SetFrameSize "SetFrameSize" | @ref AudioEngineFormat::GetFrameSize "GetFrameSize"
+ ///
+ /// Further is @ref AudioEngineFormat::CompareFormat "CompareFormat" included to compare this class with another.
+ ///
+
+ /// @addtogroup cpp_kodi_audioengine_Defs_AudioEngineFormat
+ /// @copydetails cpp_kodi_audioengine_Defs_AudioEngineFormat_Help
+ ///@{
+
+ /// @brief The stream's data format (eg, AUDIOENGINE_FMT_S16LE)
+ void SetDataFormat(enum AudioEngineDataFormat format) { m_cStructure->m_dataFormat = format; }
+
+ /// @brief To get with @ref SetDataFormat changed values.
+ enum AudioEngineDataFormat GetDataFormat() const { return m_cStructure->m_dataFormat; }
+
+ /// @brief The stream's sample rate (eg, 48000)
+ void SetSampleRate(unsigned int rate) { m_cStructure->m_sampleRate = rate; }
+
+ /// @brief To get with @ref SetSampleRate changed values.
+ unsigned int GetSampleRate() const { return m_cStructure->m_sampleRate; }
+
+ /// @brief The encoded streams sample rate if a bitstream, otherwise undefined
+ void SetEncodedRate(unsigned int rate) { m_cStructure->m_encodedRate = rate; }
+
+ /// @brief To get with @ref SetEncodedRate changed values.
+ unsigned int GetEncodedRate() const { return m_cStructure->m_encodedRate; }
+
+ /// @brief The stream's channel layout
+ void SetChannelLayout(const std::vector<enum AudioEngineChannel>& layout)
+ {
+ // Reset first all to empty values to AUDIOENGINE_CH_NULL, in case given list is empty
+ m_cStructure->m_channelCount = 0;
+ for (size_t ch = 0; ch < AUDIOENGINE_CH_MAX; ++ch)
+ m_cStructure->m_channels[ch] = AUDIOENGINE_CH_NULL;
+
+ for (size_t ch = 0; ch < layout.size() && ch < AUDIOENGINE_CH_MAX; ++ch)
+ {
+ m_cStructure->m_channels[ch] = layout[ch];
+ m_cStructure->m_channelCount++;
+ }
+ }
+
+ /// @brief To get with @ref SetChannelLayout changed values.
+ std::vector<enum AudioEngineChannel> GetChannelLayout() const
+ {
+ std::vector<enum AudioEngineChannel> channels;
+ for (size_t ch = 0; ch < AUDIOENGINE_CH_MAX; ++ch)
+ {
+ if (m_cStructure->m_channels[ch] == AUDIOENGINE_CH_NULL)
+ break;
+
+ channels.push_back(m_cStructure->m_channels[ch]);
+ }
+ return channels;
+ }
+
+ /// @brief The number of frames per period
+ void SetFramesAmount(unsigned int frames) { m_cStructure->m_frames = frames; }
+
+ /// @brief To get with @ref SetFramesAmount changed values.
+ unsigned int GetFramesAmount() const { return m_cStructure->m_frames; }
+
+ /// @brief The size of one frame in bytes
+ void SetFrameSize(unsigned int frameSize) { m_cStructure->m_frameSize = frameSize; }
+
+ /// @brief To get with @ref SetFrameSize changed values.
+ unsigned int GetFrameSize() const { return m_cStructure->m_frameSize; }
+
+ /// @brief Function to compare the format structure with another
+ bool CompareFormat(const AudioEngineFormat* fmt)
+ {
+ if (!fmt)
+ {
+ return false;
+ }
+
+ if (m_cStructure->m_dataFormat != fmt->m_cStructure->m_dataFormat ||
+ m_cStructure->m_sampleRate != fmt->m_cStructure->m_sampleRate ||
+ m_cStructure->m_encodedRate != fmt->m_cStructure->m_encodedRate ||
+ m_cStructure->m_frames != fmt->m_cStructure->m_frames ||
+ m_cStructure->m_frameSize != fmt->m_cStructure->m_frameSize ||
+ m_cStructure->m_channelCount != fmt->m_cStructure->m_channelCount)
+ {
+ return false;
+ }
+
+ for (unsigned int ch = 0; ch < AUDIOENGINE_CH_MAX; ++ch)
+ {
+ if (fmt->m_cStructure->m_channels[ch] != m_cStructure->m_channels[ch])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ ///@}
+};
+///@}
+//----------------------------------------------------------------------------
+
+//}}}
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" AudioEngine addon interface
+//{{{
+
+//============================================================================
+///
+/// @defgroup cpp_kodi_audioengine_CAEStream class CAEStream
+/// @ingroup cpp_kodi_audioengine
+/// @brief **Audio Engine Stream Class**\n
+/// Class that can be created by the addon in order to be able to transfer
+/// audiostream data processed on the addon to Kodi and output it audibly.
+///
+/// This can create individually several times and performed in different
+/// processes simultaneously.
+///
+/// It has the header @ref AudioEngine.h "#include <kodi/AudioEngine.h>" be
+/// included to enjoy it.
+///
+//----------------------------------------------------------------------------
+class ATTR_DLL_LOCAL CAEStream
+{
+public:
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Constructs new class to a Kodi IAEStream in the format specified.
+ ///
+ /// @param[in] format The data format the incoming audio will be in
+ /// (e.g. @ref AUDIOENGINE_FMT_S16LE)
+ /// @param[in] options [opt] A bit field of stream options (see: enum @ref AudioEngineStreamOptions)
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_audioengine_Defs_AudioEngineFormat_Help
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Bit options to pass (on Kodi by <c>IAE::MakeStream</c>)**
+ ///
+ /// | enum AEStreamOptions | Value: | Description:
+ /// |----------------------------:|:------:|:-----------------------------------
+ /// | AUDIO_STREAM_FORCE_RESAMPLE | 1 << 0 | Force resample even if rates match
+ /// | AUDIO_STREAM_PAUSED | 1 << 1 | Create the stream paused
+ /// | AUDIO_STREAM_AUTOSTART | 1 << 2 | Autostart the stream when enough data is buffered
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ ///
+ /// #include <kodi/AudioEngine.h>
+ ///
+ /// ...
+ ///
+ /// kodi::audioengine::AudioEngineFormat format;
+ ///
+ /// format.SetDataFormat(AUDIOENGINE_FMT_FLOATP); /* The stream's data format (eg, AUDIOENGINE_FMT_S16LE) */
+ /// format.SetChannelLayout(std::vector<AudioEngineChannel>(AUDIOENGINE_CH_FL, AUDIOENGINE_CH_FR)); /* The stream's channel layout */
+ /// format.SetSampleRate(48000); /* The stream's sample rate (eg, 48000) */
+ /// format.SetFrameSize(sizeof(float)*2); /* The size of one frame in bytes */
+ /// format.SetFramesAmount(882); /* The number of samples in one frame */
+ ///
+ /// kodi::audioengine::CAEStream* stream = new kodi::audioengine::CAEStream(format, AUDIO_STREAM_AUTOSTART);
+ ///
+ /// ~~~~~~~~~~~~~
+ ///
+ CAEStream(AudioEngineFormat& format, unsigned int options = 0)
+ : m_kodiBase(::kodi::addon::CPrivateBase::m_interface->toKodi->kodiBase),
+ m_cb(::kodi::addon::CPrivateBase::m_interface->toKodi->kodi_audioengine)
+ {
+ m_StreamHandle = m_cb->make_stream(m_kodiBase, format, options);
+ if (m_StreamHandle == nullptr)
+ {
+ kodi::Log(ADDON_LOG_FATAL, "CAEStream: make_stream failed!");
+ }
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Class destructor.
+ ///
+ ~CAEStream()
+ {
+ if (m_StreamHandle)
+ {
+ m_cb->free_stream(m_kodiBase, m_StreamHandle);
+ m_StreamHandle = nullptr;
+ }
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Returns the amount of space available in the stream.
+ ///
+ /// @return The number of bytes AddData will consume
+ ///
+ unsigned int GetSpace() { return m_cb->aestream_get_space(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Add planar or interleaved PCM data to the stream.
+ ///
+ /// @param[in] data array of pointers to the planes
+ /// @param[in] offset to frame in frames
+ /// @param[in] frames number of frames
+ /// @param[in] pts [opt] presentation timestamp, default is 0
+ /// @param[in] hasDownmix [opt] set true if downmix is present, default is false
+ /// @param[in] centerMixLevel [opt] level to mix left and right to center default is 1.0
+ /// @return The number of frames consumed
+ ///
+ unsigned int AddData(uint8_t* const* data,
+ unsigned int offset,
+ unsigned int frames,
+ double pts = 0,
+ bool hasDownmix = false,
+ double centerMixLevel = 1.0)
+ {
+ return m_cb->aestream_add_data(m_kodiBase, m_StreamHandle, data, offset, frames, pts,
+ hasDownmix, centerMixLevel);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Returns the time in seconds that it will take for the next added
+ /// packet to be heard from the speakers.
+ ///
+ /// @return seconds
+ ///
+ double GetDelay() { return m_cb->aestream_get_delay(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Returns if the stream is buffering.
+ ///
+ /// @return True if the stream is buffering
+ ///
+ bool IsBuffering() { return m_cb->aestream_is_buffering(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Returns the time in seconds of the stream's cached audio samples.
+ /// Engine buffers excluded.
+ ///
+ /// @return seconds
+ ///
+ double GetCacheTime() { return m_cb->aestream_get_cache_time(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Returns the total time in seconds of the cache.
+ ///
+ /// @return seconds
+ ///
+ double GetCacheTotal() { return m_cb->aestream_get_cache_total(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Pauses the stream playback.
+ ///
+ void Pause() { return m_cb->aestream_pause(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Resumes the stream after pausing
+ ///
+ void Resume() { return m_cb->aestream_resume(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Start draining the stream.
+ ///
+ /// @param[in] wait [opt] Wait until drain is finished if set to true,
+ /// otherwise it returns direct
+ ///
+ /// @note Once called AddData will not consume more data.
+ ///
+ void Drain(bool wait = true) { return m_cb->aestream_drain(m_kodiBase, m_StreamHandle, wait); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Returns true if the is stream draining.
+ ///
+ bool IsDraining() { return m_cb->aestream_is_draining(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Returns true if the is stream has finished draining.
+ ///
+ bool IsDrained() { return m_cb->aestream_is_drained(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Flush all buffers dropping the audio data.
+ ///
+ void Flush() { return m_cb->aestream_flush(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Return the stream's current volume level.
+ ///
+ /// @return The volume level between 0.0 and 1.0
+ ///
+ float GetVolume() { return m_cb->aestream_get_volume(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Set the stream's volume level.
+ ///
+ /// @param[in] volume The new volume level between 0.0 and 1.0
+ ///
+ void SetVolume(float volume)
+ {
+ return m_cb->aestream_set_volume(m_kodiBase, m_StreamHandle, volume);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Gets the stream's volume amplification in linear units.
+ ///
+ /// @return The volume amplification factor between 1.0 and 1000.0
+ ///
+ float GetAmplification() { return m_cb->aestream_get_amplification(m_kodiBase, m_StreamHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Sets the stream's volume amplification in linear units.
+ ///
+ /// @param[in] amplify The volume amplification factor between 1.0 and 1000.0
+ ///
+ void SetAmplification(float amplify)
+ {
+ return m_cb->aestream_set_amplification(m_kodiBase, m_StreamHandle, amplify);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Returns the size of one audio frame in bytes (channelCount * resolution).
+ ///
+ /// @return The size in bytes of one frame
+ ///
+ unsigned int GetFrameSize() const
+ {
+ return m_cb->aestream_get_frame_size(m_kodiBase, m_StreamHandle);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Returns the number of channels the stream is configured to accept.
+ ///
+ /// @return The channel count
+ ///
+ unsigned int GetChannelCount() const
+ {
+ return m_cb->aestream_get_channel_count(m_kodiBase, m_StreamHandle);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Returns the stream's sample rate, if the stream is using a dynamic
+ /// sample rate, this value will NOT reflect any changes made by calls to
+ /// SetResampleRatio().
+ ///
+ /// @return The stream's sample rate (eg, 48000)
+ ///
+ unsigned int GetSampleRate() const
+ {
+ return m_cb->aestream_get_sample_rate(m_kodiBase, m_StreamHandle);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Return the data format the stream has been configured with.
+ ///
+ /// @return The stream's data format (eg, AUDIOENGINE_FMT_S16LE)
+ ///
+ AudioEngineDataFormat GetDataFormat() const
+ {
+ return m_cb->aestream_get_data_format(m_kodiBase, m_StreamHandle);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Return the resample ratio.
+ ///
+ /// @note This will return an undefined value if the stream is not resampling.
+ ///
+ /// @return the current resample ratio or undefined if the stream is not resampling
+ ///
+ double GetResampleRatio()
+ {
+ return m_cb->aestream_get_resample_ratio(m_kodiBase, m_StreamHandle);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_audioengine_CAEStream
+ /// @brief Sets the resample ratio.
+ ///
+ /// @note This function may return false if the stream is not resampling, if
+ /// you wish to use this be sure to set the AESTREAM_FORCE_RESAMPLE option.
+ ///
+ /// @param[in] ratio the new sample rate ratio, calculated by
+ /// ((double)desiredRate / (double)GetSampleRate())
+ ///
+ void SetResampleRatio(double ratio)
+ {
+ m_cb->aestream_set_resample_ratio(m_kodiBase, m_StreamHandle, ratio);
+ }
+ //--------------------------------------------------------------------------
+
+private:
+ void* m_kodiBase;
+ AddonToKodiFuncTable_kodi_audioengine* m_cb;
+ AEStreamHandle* m_StreamHandle;
+};
+//----------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_audioengine
+/// @brief Get the current sink data format.
+///
+/// @param[in] format Current sink data format. For more details see AudioEngineFormat.
+/// @return Returns true on success, else false.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+///
+/// #include <kodi/AudioEngine.h>
+///
+/// ...
+///
+/// kodi::audioengine::AudioEngineFormat format;
+/// if (!kodi::audioengine::GetCurrentSinkFormat(format))
+/// return false;
+///
+/// std::vector<AudioEngineChannel> layout = format.GetChannelLayout();
+///
+/// ...
+/// return true;
+///
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL GetCurrentSinkFormat(AudioEngineFormat& format)
+{
+ using namespace kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_audioengine->get_current_sink_format(
+ CPrivateBase::m_interface->toKodi->kodiBase, format);
+}
+//----------------------------------------------------------------------------
+
+//}}}
+
+} // namespace audioengine
+} // namespace kodi
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/CMakeLists.txt
new file mode 100644
index 0000000..5e396b9
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/CMakeLists.txt
@@ -0,0 +1,15 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ AddonBase.h
+ AudioEngine.h
+ Filesystem.h
+ General.h
+ Network.h
+ versions.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/Filesystem.h b/xbmc/addons/kodi-dev-kit/include/kodi/Filesystem.h
new file mode 100644
index 0000000..efba5c0
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/Filesystem.h
@@ -0,0 +1,2379 @@
+/*
+ * 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 "AddonBase.h"
+#include "c-api/filesystem.h"
+
+#ifdef __cplusplus
+
+#include <cstring>
+#include <map>
+#include <vector>
+
+namespace kodi
+{
+namespace vfs
+{
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// Main page text for filesystem group by Doxygen.
+//{{{
+
+//==============================================================================
+///
+/// @defgroup cpp_kodi_vfs Interface - kodi::vfs
+/// @ingroup cpp
+/// @brief **Virtual filesystem functions**\n
+/// Offers classes and functions for access to the Virtual File Server (VFS)
+/// which you can use to manipulate files and folders.
+///
+/// This system allow the use of ["Special Protocol"](https://kodi.wiki/view/Special_protocol)
+/// where is Kodi's solution to platform dependent directories. Common directory
+/// names are assigned a <b>`special://[name]`</b> path which is passed around
+/// inside Kodi and then translated to the platform specific path before the
+/// operating system sees it. This helps keep most of the platform mess
+/// centralized in the code.\n
+/// To become a correct path back can be @ref TranslateSpecialProtocol() used.
+///
+/// It has the header @ref Filesystem.h "#include <kodi/Filesystem.h>" be
+/// included to enjoy it.
+///
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_vfs_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_vfs
+/// @brief **Virtual file Server definition values**\n
+/// All to VFS system functions associated data structures.
+///
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_vfs_Directory 1. Directory functions
+/// @ingroup cpp_kodi_vfs
+/// @brief **Globally available directories related functions**\n
+/// Used to perform typical operations with it.
+///
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_vfs_File 2. File functions
+/// @ingroup cpp_kodi_vfs
+/// @brief **Globally available file related functions**\n
+/// Used to perform typical operations with it.
+///
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_vfs_General 3. General functions
+/// @ingroup cpp_kodi_vfs
+/// @brief **Other globally available functions**\n
+/// Used to perform typical operations with it.
+///
+//------------------------------------------------------------------------------
+
+//}}}
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" related filesystem definitions
+//{{{
+
+//==============================================================================
+/// @defgroup cpp_kodi_vfs_Defs_FileStatus class FileStatus
+/// @ingroup cpp_kodi_vfs_Defs
+/// @brief **File information status**\n
+/// Used on kodi::vfs::StatFile() to get detailed information about a file.
+///
+///@{
+class ATTR_DLL_LOCAL FileStatus : public kodi::addon::CStructHdl<FileStatus, STAT_STRUCTURE>
+{
+public:
+ /*! \cond PRIVATE */
+ FileStatus() { memset(m_cStructure, 0, sizeof(STAT_STRUCTURE)); }
+ FileStatus(const FileStatus& channel) : CStructHdl(channel) {}
+ FileStatus(const STAT_STRUCTURE* channel) : CStructHdl(channel) {}
+ FileStatus(STAT_STRUCTURE* channel) : CStructHdl(channel) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_vfs_Defs_FileStatus_Help Value Help
+ /// @ingroup cpp_kodi_vfs_Defs_FileStatus
+ /// ----------------------------------------------------------------------------
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_vfs_Defs_FileStatus :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **ID of device containing file** | `uint32_t` | @ref FileStatus::SetDeviceId "SetDeviceId" | @ref FileStatus::GetDeviceId "GetDeviceId"
+ /// | **Represent file serial numbers** | `uint64_t` | @ref FileStatus::SetFileSerialNumber "SetFileSerialNumber" | @ref FileStatus::GetFileSerialNumber "GetFileSerialNumber"
+ /// | **Total size, in bytes** | `uint64_t` | @ref FileStatus::SetSize "SetSize" | @ref FileStatus::GetSize "GetSize"
+ /// | **Time of last access** | `time_t` | @ref FileStatus::SetAccessTime "SetAccessTime" | @ref FileStatus::GetAccessTime "GetAccessTime"
+ /// | **Time of last modification** | `time_t` | @ref FileStatus::SetModificationTime "SetModificationTime" | @ref FileStatus::GetModificationTime "GetModificationTime"
+ /// | **Time of last status change** | `time_t` | @ref FileStatus::SetStatusTime "SetStatusTime" | @ref FileStatus::GetStatusTime "GetStatusTime"
+ /// | **Stat url is a directory** | `bool` | @ref FileStatus::SetIsDirectory "SetIsDirectory" | @ref FileStatus::GetIsDirectory "GetIsDirectory"
+ /// | **Stat url as a symbolic link** | `bool` | @ref FileStatus::SetIsSymLink "SetIsSymLink" | @ref FileStatus::GetIsSymLink "GetIsSymLink"
+ /// | **Stat url as a block special** | `bool` | @ref FileStatus::SetIsBlock "SetIsBlock" | @ref FileStatus::GetIsBlock "GetIsBlock"
+ /// | **Stat url as a character special** | `bool` | @ref FileStatus::SetIsCharacter "SetIsCharacter" | @ref FileStatus::GetIsCharacter "GetIsCharacter"
+ /// | **Stat url as a FIFO special** | `bool` | @ref FileStatus::SetIsFifo "SetIsFifo" | @ref FileStatus::GetIsFifo "GetIsFifo"
+ /// | **Stat url as a regular** | `bool` | @ref FileStatus::SetIsRegular "SetIsRegular" | @ref FileStatus::GetIsRegular "GetIsRegular"
+ /// | **Stat url as a socket** | `bool` | @ref FileStatus::SetIsSocket "SetIsSocket" | @ref FileStatus::GetIsSocket "GetIsSocket"
+ ///
+
+ /// @addtogroup cpp_kodi_vfs_Defs_FileStatus
+ /// @copydetails cpp_kodi_vfs_Defs_FileStatus_Help
+ ///@{
+
+ /// @brief Set ID of device containing file.
+ void SetDeviceId(uint32_t deviceId) { m_cStructure->deviceId = deviceId; }
+
+ /// @brief Get ID of device containing file.
+ uint32_t GetDeviceId() const { return m_cStructure->deviceId; }
+
+ /// @brief Set the file serial number, which distinguishes this file from all other files on the same device.
+ void SetFileSerialNumber(uint64_t fileSerialNumber) { m_cStructure->fileSerialNumber = fileSerialNumber; }
+
+ /// @brief Get the file serial number, which distinguishes this file from all other files on the same device.
+ uint64_t GetFileSerialNumber() const { return m_cStructure->fileSerialNumber; }
+
+ /// @brief Set total size, in bytes.
+ void SetSize(uint64_t size) { m_cStructure->size = size; }
+
+ /// @brief Get total size, in bytes.
+ uint64_t GetSize() const { return m_cStructure->size; }
+
+ /// @brief Set time of last access.
+ void SetAccessTime(time_t accessTime) { m_cStructure->accessTime = accessTime; }
+
+ /// @brief Get time of last access.
+ time_t GetAccessTime() const { return m_cStructure->accessTime; }
+
+ /// @brief Set time of last modification.
+ void SetModificationTime(time_t modificationTime)
+ {
+ m_cStructure->modificationTime = modificationTime;
+ }
+
+ /// @brief Get time of last modification.
+ time_t GetModificationTime() const { return m_cStructure->modificationTime; }
+
+ /// @brief Set time of last status change.
+ void SetStatusTime(time_t statusTime) { m_cStructure->statusTime = statusTime; }
+
+ /// @brief Get time of last status change.
+ time_t GetStatusTime() const { return m_cStructure->statusTime; }
+
+ /// @brief Set the stat url is a directory.
+ void SetIsDirectory(bool isDirectory) { m_cStructure->isDirectory = isDirectory; }
+
+ /// @brief The stat url is a directory if returns true.
+ bool GetIsDirectory() const { return m_cStructure->isDirectory; }
+
+ /// @brief Set stat url as a symbolic link.
+ void SetIsSymLink(bool isSymLink) { m_cStructure->isSymLink = isSymLink; }
+
+ /// @brief Get stat url is a symbolic link.
+ bool GetIsSymLink() const { return m_cStructure->isSymLink; }
+
+ /// @brief Set stat url as a block special.
+ void SetIsBlock(bool isBlock) { m_cStructure->isBlock = isBlock; }
+
+ /// @brief Get stat url is a block special.
+ bool GetIsBlock() const { return m_cStructure->isBlock; }
+
+ /// @brief Set stat url as a character special.
+ void SetIsCharacter(bool isCharacter) { m_cStructure->isCharacter = isCharacter; }
+
+ /// @brief Get stat url is a character special.
+ bool GetIsCharacter() const { return m_cStructure->isCharacter; }
+
+ /// @brief Set stat url as a FIFO special.
+ void SetIsFifo(bool isFifo) { m_cStructure->isFifo = isFifo; }
+
+ /// @brief Get stat url is a FIFO special.
+ bool GetIsFifo() const { return m_cStructure->isFifo; }
+
+ /// @brief Set stat url as a regular.
+ void SetIsRegular(bool isRegular) { m_cStructure->isRegular = isRegular; }
+
+ /// @brief Get stat url is a regular.
+ bool GetIsRegular() const { return m_cStructure->isRegular; }
+
+ /// @brief Set stat url is a socket.
+ void SetIsSocket(bool isSocket) { m_cStructure->isSocket = isSocket; }
+
+ /// @brief Get stat url is a regular.
+ bool GetIsSocket() const { return m_cStructure->isSocket; }
+ ///@}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_vfs_Defs_CacheStatus class CacheStatus
+/// @ingroup cpp_kodi_vfs_Defs
+/// @brief **Cache information status**\n
+/// Used on kodi::vfs::CFile::IoControlGetCacheStatus() to get running cache
+/// status of processed stream.
+///
+///@{
+class ATTR_DLL_LOCAL CacheStatus
+ : public kodi::addon::CStructHdl<CacheStatus, VFS_CACHE_STATUS_DATA>
+{
+public:
+ /*! \cond PRIVATE */
+ CacheStatus() { memset(m_cStructure, 0, sizeof(VFS_CACHE_STATUS_DATA)); }
+ CacheStatus(const CacheStatus& channel) : CStructHdl(channel) {}
+ CacheStatus(const VFS_CACHE_STATUS_DATA* channel) : CStructHdl(channel) {}
+ CacheStatus(VFS_CACHE_STATUS_DATA* channel) : CStructHdl(channel) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_vfs_Defs_CacheStatus_Help Value Help
+ /// @ingroup cpp_kodi_vfs_Defs_CacheStatus
+ /// ----------------------------------------------------------------------------
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_vfs_Defs_CacheStatus :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Number of bytes cached** | `uint64_t` | @ref CacheStatus::SetForward "SetForward" | @ref CacheStatus::GetForward "GetForward"
+ /// | **Maximum number of bytes per second** | `uint32_t` | @ref CacheStatus::SetMaxRate "SetMaxRate" | @ref CacheStatus::GetMaxRate "GetMaxRate"
+ /// | **Average read rate from source file** | `uint32_t` | @ref CacheStatus::SetCurrentRate "SetCurrentRate" | @ref CacheStatus::GetCurrentRate "GetCurrentRate"
+ /// | **Cache low speed rate detected** | `uint32_t` | @ref CacheStatus::SetLowRate "SetLowRate" | @ref CacheStatus::GetLowRate "GetLowRate"
+ ///
+
+ /// @addtogroup cpp_kodi_vfs_Defs_CacheStatus
+ /// @copydetails cpp_kodi_vfs_Defs_CacheStatus_Help
+ ///@{
+
+ /// @brief Set number of bytes cached forward of current position.
+ void SetForward(uint64_t forward) { m_cStructure->forward = forward; }
+
+ /// @brief Get number of bytes cached forward of current position.
+ uint64_t GetForward() { return m_cStructure->forward; }
+
+ /// @brief Set maximum number of bytes per second cache is allowed to fill.
+ void SetMaxRate(uint32_t maxrate) { m_cStructure->maxrate = maxrate; }
+
+ /// @brief Set maximum number of bytes per second cache is allowed to fill.
+ uint32_t GetMaxRate() { return m_cStructure->maxrate; }
+
+ /// @brief Set number of bytes per second for average read rate from source file since last position change.
+ void SetCurrentRate(uint32_t currate) { m_cStructure->currate = currate; }
+
+ /// @brief Get number of bytes per second for average read rate from source file since last position change.
+ uint32_t GetCurrentRate() { return m_cStructure->currate; }
+
+ /// @brief Set number of bytes per second for low speed rate.
+ void SetLowRate(uint32_t lowrate) { m_cStructure->lowrate = lowrate; }
+
+ /// @brief Get number of bytes per second for low speed rate.
+ uint32_t GetLowRate() { return m_cStructure->lowrate; }
+
+ ///@}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_vfs_Defs_HttpHeader class HttpHeader
+/// @ingroup cpp_kodi_vfs_Defs
+/// @brief **HTTP header information**\n
+/// The class used to access HTTP header information and get his information.
+///
+/// Used on @ref kodi::vfs::GetHttpHeader().
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_vfs_Defs_HttpHeader_Help
+///
+///@{
+class ATTR_DLL_LOCAL HttpHeader
+{
+public:
+ //==========================================================================
+ /// @brief Http header parser class constructor.
+ ///
+ HttpHeader()
+ {
+ using namespace ::kodi::addon;
+
+ CPrivateBase::m_interface->toKodi->kodi_filesystem->http_header_create(
+ CPrivateBase::m_interface->toKodi->kodiBase, &m_handle);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Class destructor.
+ ///
+ ~HttpHeader()
+ {
+ using namespace ::kodi::addon;
+
+ CPrivateBase::m_interface->toKodi->kodi_filesystem->http_header_free(
+ CPrivateBase::m_interface->toKodi->kodiBase, &m_handle);
+ }
+ //--------------------------------------------------------------------------
+
+ /// @defgroup cpp_kodi_vfs_Defs_HttpHeader_Help Value Help
+ /// @ingroup cpp_kodi_vfs_Defs_HttpHeader
+ ///
+ /// <b>The following table contains values that can be get with @ref cpp_kodi_vfs_Defs_HttpHeader :</b>
+ /// | Description | Type | Get call
+ /// |-------------|------|------------
+ /// | **Get the value associated with this parameter of these HTTP headers** | `std::string` | @ref HttpHeader::GetValue "GetValue"
+ /// | **Get the values as list associated with this parameter of these HTTP headers** | `std::vector<std::string>` | @ref HttpHeader::GetValues "GetValues"
+ /// | **Get the full header string associated with these HTTP headers** | `std::string` | @ref HttpHeader::GetHeader "GetHeader"
+ /// | **Get the mime type associated with these HTTP headers** | `std::string` | @ref HttpHeader::GetMimeType "GetMimeType"
+ /// | **Get the charset associated with these HTTP headers** | `std::string` | @ref HttpHeader::GetCharset "GetCharset"
+ /// | **The protocol line associated with these HTTP headers** | `std::string` | @ref HttpHeader::GetProtoLine "GetProtoLine"
+ ///
+
+ /// @addtogroup cpp_kodi_vfs_Defs_HttpHeader
+ ///@{
+
+ //==========================================================================
+ /// @brief Get the value associated with this parameter of these HTTP
+ /// headers.
+ ///
+ /// @param[in] param The name of the parameter a value is required for
+ /// @return The value found
+ ///
+ std::string GetValue(const std::string& param) const
+ {
+ using namespace ::kodi::addon;
+
+ if (!m_handle.handle)
+ return "";
+
+ std::string protoLine;
+ char* string = m_handle.get_value(CPrivateBase::m_interface->toKodi->kodiBase, m_handle.handle,
+ param.c_str());
+ if (string != nullptr)
+ {
+ protoLine = string;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ string);
+ }
+ return protoLine;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Get the values as list associated with this parameter of these
+ /// HTTP headers.
+ ///
+ /// @param[in] param The name of the parameter values are required for
+ /// @return The values found
+ ///
+ std::vector<std::string> GetValues(const std::string& param) const
+ {
+ using namespace kodi::addon;
+
+ if (!m_handle.handle)
+ return std::vector<std::string>();
+
+ int numValues = 0;
+ char** res(m_handle.get_values(CPrivateBase::m_interface->toKodi->kodiBase, m_handle.handle,
+ param.c_str(), &numValues));
+ if (res)
+ {
+ std::vector<std::string> vecReturn;
+ vecReturn.reserve(numValues);
+ for (int i = 0; i < numValues; ++i)
+ {
+ vecReturn.emplace_back(res[i]);
+ }
+ CPrivateBase::m_interface->toKodi->free_string_array(
+ CPrivateBase::m_interface->toKodi->kodiBase, res, numValues);
+ return vecReturn;
+ }
+ return std::vector<std::string>();
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Get the full header string associated with these HTTP headers.
+ ///
+ /// @return The header as a string
+ ///
+ std::string GetHeader() const
+ {
+ using namespace ::kodi::addon;
+
+ if (!m_handle.handle)
+ return "";
+
+ std::string header;
+ char* string =
+ m_handle.get_header(CPrivateBase::m_interface->toKodi->kodiBase, m_handle.handle);
+ if (string != nullptr)
+ {
+ header = string;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ string);
+ }
+ return header;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Get the mime type associated with these HTTP headers.
+ ///
+ /// @return The mime type
+ ///
+ std::string GetMimeType() const
+ {
+ using namespace ::kodi::addon;
+
+ if (!m_handle.handle)
+ return "";
+
+ std::string protoLine;
+ char* string =
+ m_handle.get_mime_type(CPrivateBase::m_interface->toKodi->kodiBase, m_handle.handle);
+ if (string != nullptr)
+ {
+ protoLine = string;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ string);
+ }
+ return protoLine;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Get the charset associated with these HTTP headers.
+ ///
+ /// @return The charset
+ ///
+ std::string GetCharset() const
+ {
+ using namespace ::kodi::addon;
+
+ if (!m_handle.handle)
+ return "";
+
+ std::string protoLine;
+ char* string =
+ m_handle.get_charset(CPrivateBase::m_interface->toKodi->kodiBase, m_handle.handle);
+ if (string != nullptr)
+ {
+ protoLine = string;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ string);
+ }
+ return protoLine;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief The protocol line associated with these HTTP headers.
+ ///
+ /// @return The protocol line
+ ///
+ std::string GetProtoLine() const
+ {
+ using namespace ::kodi::addon;
+
+ if (!m_handle.handle)
+ return "";
+
+ std::string protoLine;
+ char* string =
+ m_handle.get_proto_line(CPrivateBase::m_interface->toKodi->kodiBase, m_handle.handle);
+ if (string != nullptr)
+ {
+ protoLine = string;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ string);
+ }
+ return protoLine;
+ }
+ //--------------------------------------------------------------------------
+
+ ///@}
+
+ KODI_HTTP_HEADER m_handle;
+};
+///@}
+//----------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_vfs_CDirEntry class CDirEntry
+/// @ingroup cpp_kodi_vfs_Defs
+///
+/// @brief **Virtual file server directory entry**\n
+/// This class is used as an entry for files and folders in
+/// kodi::vfs::GetDirectory().
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+///
+/// ...
+///
+/// std::vector<kodi::vfs::CDirEntry> items;
+/// kodi::vfs::GetDirectory("special://temp", "", items);
+///
+/// fprintf(stderr, "Directory have %lu entries\n", items.size());
+/// for (unsigned long i = 0; i < items.size(); i++)
+/// {
+/// char buff[20];
+/// time_t now = items[i].DateTime();
+/// strftime(buff, 20, "%Y-%m-%d %H:%M:%S", gmtime(&now));
+/// fprintf(stderr, " - %04lu -- Folder: %s -- Name: %s -- Path: %s -- Time: %s\n",
+/// i+1,
+/// items[i].IsFolder() ? "yes" : "no ",
+/// items[i].Label().c_str(),
+/// items[i].Path().c_str(),
+/// buff);
+/// }
+/// ~~~~~~~~~~~~~
+///
+/// It has the header @ref Filesystem.h "#include <kodi/Filesystem.h>" be included
+/// to enjoy it.
+///
+///@{
+class ATTR_DLL_LOCAL CDirEntry
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_vfs_CDirEntry
+ /// @brief Constructor for VFS directory entry
+ ///
+ /// @param[in] label [opt] Name to use for entry
+ /// @param[in] path [opt] Used path of the entry
+ /// @param[in] folder [opt] If set entry used as folder
+ /// @param[in] size [opt] If used as file, his size defined there
+ /// @param[in] dateTime [opt] Date time of the entry
+ ///
+ CDirEntry(const std::string& label = "",
+ const std::string& path = "",
+ bool folder = false,
+ int64_t size = -1,
+ time_t dateTime = 0)
+ : m_label(label), m_path(path), m_folder(folder), m_size(size), m_dateTime(dateTime)
+ {
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ // @note Not for addon development itself needed, thats why below is
+ // disabled for doxygen!
+ //
+ // @ingroup cpp_kodi_vfs_CDirEntry
+ // @brief Constructor to create own copy
+ //
+ // @param[in] dirEntry pointer to own class type
+ //
+ explicit CDirEntry(const VFSDirEntry& dirEntry)
+ : m_label(dirEntry.label ? dirEntry.label : ""),
+ m_path(dirEntry.path ? dirEntry.path : ""),
+ m_folder(dirEntry.folder),
+ m_size(dirEntry.size),
+ m_dateTime(dirEntry.date_time)
+ {
+ }
+ //----------------------------------------------------------------------------
+
+ /// @defgroup cpp_kodi_vfs_CDirEntry_Help Value Help
+ /// @ingroup cpp_kodi_vfs_CDirEntry
+ /// --------------------------------------------------------------------------
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_vfs_CDirEntry :</b>
+ /// | Name | Type | Set call | Get call | Clear call |
+ /// |------|------|----------|----------|------------|
+ /// | **Directory entry name** | `std::string` | @ref CDirEntry::SetLabel "SetLabel" | @ref CDirEntry::Label "Label" | |
+ /// | **Title of entry** | `std::string` | @ref CDirEntry::SetTitle "SetTitle" | @ref CDirEntry::Title "Title" | |
+ /// | **Path of the entry** | `std::string` | @ref CDirEntry::SetPath "SetPath" | @ref CDirEntry::Path "Path" | |
+ /// | **Entry is folder** | `bool` | @ref CDirEntry::SetFolder "SetFolder" | @ref CDirEntry::IsFolder "IsFolder" | |
+ /// | **The size of the file** | `int64_t` | @ref CDirEntry::SetSize "SetSize" | @ref CDirEntry::Size "Size" | |
+ /// | **File time and date** | `time_t` | @ref CDirEntry::SetDateTime "SetDateTime" | @ref CDirEntry::DateTime "DateTime" | |
+ /// | **Property entries** | `std::string, std::string` | @ref CDirEntry::AddProperty "AddProperty" | @ref CDirEntry::GetProperties "GetProperties" | @ref CDirEntry::ClearProperties "ClearProperties"
+ ///
+
+ /// @addtogroup cpp_kodi_vfs_CDirEntry
+ /// @copydetails cpp_kodi_vfs_CDirEntry_Help
+ ///@{
+
+ //============================================================================
+ /// @brief Get the directory entry name.
+ ///
+ /// @return Name of the entry
+ ///
+ const std::string& Label(void) const { return m_label; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the optional title of entry.
+ ///
+ /// @return Title of the entry, if exists
+ ///
+ const std::string& Title(void) const { return m_title; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the path of the entry.
+ ///
+ /// @return File system path of the entry
+ ///
+ const std::string& Path(void) const { return m_path; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Used to check entry is folder.
+ ///
+ /// @return true if entry is a folder
+ ///
+ bool IsFolder(void) const { return m_folder; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief If file, the size of the file.
+ ///
+ /// @return Defined file size
+ ///
+ int64_t Size(void) const { return m_size; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get file time and date for a new entry.
+ ///
+ /// @return The with time_t defined date and time of file
+ ///
+ time_t DateTime() { return m_dateTime; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set the label name.
+ ///
+ /// @param[in] label name of entry
+ ///
+ void SetLabel(const std::string& label) { m_label = label; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set the title name.
+ ///
+ /// @param[in] title title name of entry
+ ///
+ void SetTitle(const std::string& title) { m_title = title; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set the path of the entry.
+ ///
+ /// @param[in] path path of entry
+ ///
+ void SetPath(const std::string& path) { m_path = path; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set the entry defined as folder.
+ ///
+ /// @param[in] folder If true becomes entry defined as folder
+ ///
+ void SetFolder(bool folder) { m_folder = folder; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set a file size for a new entry.
+ ///
+ /// @param[in] size Size to set for dir entry
+ ///
+ void SetSize(int64_t size) { m_size = size; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set file time and date for a new entry.
+ ///
+ /// @param[in] dateTime The with time_t defined date and time of file
+ ///
+ void SetDateTime(time_t dateTime) { m_dateTime = dateTime; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Add a by string defined property entry to directory entry.
+ ///
+ /// @note A property can be used to add some special information about a file
+ /// or directory entry, this can be used on other places to do the right work
+ /// of them.
+ ///
+ /// @param[in] id Identification name of property
+ /// @param[in] value The property value to add by given id
+ ///
+ void AddProperty(const std::string& id, const std::string& value) { m_properties[id] = value; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Clear all present properties.
+ ///
+ void ClearProperties() { m_properties.clear(); }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the present properties list on directory entry.
+ ///
+ /// @return map with all present properties
+ ///
+ const std::map<std::string, std::string>& GetProperties() const { return m_properties; }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+private:
+ std::string m_label;
+ std::string m_title;
+ std::string m_path;
+ std::map<std::string, std::string> m_properties;
+ bool m_folder;
+ int64_t m_size;
+ time_t m_dateTime;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//}}}
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Directory related functions
+//{{{
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_Directory
+/// @brief Make a directory.
+///
+/// The kodi::vfs::CreateDirectory() function shall create a
+/// new directory with name path.
+///
+/// The newly created directory shall be an empty directory.
+///
+/// @param[in] path Path to the directory.
+/// @return Upon successful completion, CreateDirectory() shall return true.
+/// Otherwise false shall be returned, no directory shall be created.
+///
+///
+/// -------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string directory = "C:\\my_dir";
+/// bool ret = kodi::vfs::CreateDirectory(directory);
+/// fprintf(stderr, "Directory '%s' successfully created: %s\n", directory.c_str(), ret ? "yes" : "no");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL CreateDirectory(const std::string& path)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->create_directory(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_Directory
+/// @brief Verifying the Existence of a Directory.
+///
+/// The kodi::vfs::DirectoryExists() method determines whether
+/// a specified folder exists.
+///
+/// @param[in] path Path to the directory.
+/// @return True when it exists, false otherwise.
+///
+///
+/// -------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string directory = "C:\\my_dir";
+/// bool ret = kodi::vfs::DirectoryExists(directory);
+/// fprintf(stderr, "Directory '%s' present: %s\n", directory.c_str(), ret ? "yes" : "no");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL DirectoryExists(const std::string& path)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->directory_exists(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_Directory
+/// @brief Removes a directory.
+///
+/// The kodi::vfs::RemoveDirectory() function shall remove a
+/// directory whose name is given by path.
+///
+/// @param[in] path Path to the directory.
+/// @param[in] recursive [opt] Remove directory recursive (default is false)
+/// @return Upon successful completion, the function RemoveDirectory() shall
+/// return true. Otherwise, false shall be returned, and errno set
+/// to indicate the error. If false is returned, the named directory
+/// shall not be changed.
+///
+///
+/// -------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// bool ret = kodi::vfs::RemoveDirectory("C:\\my_dir");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL RemoveDirectory(const std::string& path, bool recursive = false)
+{
+ using namespace kodi::addon;
+
+ if (!recursive)
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->remove_directory(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str());
+ else
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->remove_directory_recursive(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_Directory
+/// @brief Lists a directory.
+///
+/// Return the list of files and directories which have been found in the
+/// specified directory and which respect the given constraint.
+///
+/// It can handle the normal OS dependent paths and also the special virtual
+/// filesystem from Kodi what starts with \b special://.
+///
+/// @param[in] path The path in which the files and directories are located.
+/// @param[in] mask Mask to filter out requested files, e.g. "*.avi|*.mpg" to
+/// files with this ending.
+/// @param[out] items The returned list directory entries.
+/// @return True if listing was successful, false otherwise.
+///
+///
+/// -------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+///
+/// std::vector<kodi::vfs::CDirEntry> items;
+/// kodi::vfs::GetDirectory("special://temp", "", items);
+///
+/// fprintf(stderr, "Directory have %lu entries\n", items.size());
+/// for (unsigned long i = 0; i < items.size(); i++)
+/// {
+/// fprintf(stderr, " - %04lu -- Folder: %s -- Name: %s -- Path: %s\n",
+/// i+1,
+/// items[i].IsFolder() ? "yes" : "no ",
+/// items[i].Label().c_str(),
+/// items[i].Path().c_str());
+/// }
+/// ~~~~~~~~~~~~~
+inline bool ATTR_DLL_LOCAL GetDirectory(const std::string& path,
+ const std::string& mask,
+ std::vector<kodi::vfs::CDirEntry>& items)
+{
+ using namespace kodi::addon;
+
+ VFSDirEntry* dir_list = nullptr;
+ unsigned int num_items = 0;
+ if (CPrivateBase::m_interface->toKodi->kodi_filesystem->get_directory(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str(), mask.c_str(), &dir_list,
+ &num_items))
+ {
+ if (dir_list)
+ {
+ for (unsigned int i = 0; i < num_items; ++i)
+ items.emplace_back(dir_list[i]);
+
+ CPrivateBase::m_interface->toKodi->kodi_filesystem->free_directory(
+ CPrivateBase::m_interface->toKodi->kodiBase, dir_list, num_items);
+ }
+
+ return true;
+ }
+ return false;
+}
+//------------------------------------------------------------------------------
+
+//}}}
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" File related functions
+//{{{
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_File
+/// @brief Check if a file exists.
+///
+/// @param[in] filename The filename to check.
+/// @param[in] usecache Check in file cache.
+/// @return true if the file exists false otherwise.
+///
+///
+/// -------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// bool exists = kodi::vfs::FileExists("special://temp/kodi.log");
+/// fprintf(stderr, "Log file should be always present, is it present? %s\n", exists ? "yes" : "no");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL FileExists(const std::string& filename, bool usecache = false)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->file_exists(
+ CPrivateBase::m_interface->toKodi->kodiBase, filename.c_str(), usecache);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_File
+/// @brief Get file status.
+///
+/// These function return information about a file. Execute (search)
+/// permission is required on all of the directories in path that
+/// lead to the file.
+///
+/// The call return a stat structure, which contains the on
+/// @ref cpp_kodi_vfs_Defs_FileStatus defined values.
+///
+/// @warning Not all of the OS file systems implement all of the time fields.
+///
+/// @param[in] filename The filename to read the status from.
+/// @param[out] buffer The file status is written into this buffer.
+/// @return On success, trur is returned. On error, false is returned
+///
+///
+/// @copydetails cpp_kodi_vfs_Defs_FileStatus_Help
+///
+/// -------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// kodi::vfs::FileStatus statFile;
+/// int ret = kodi::vfs::StatFile("special://temp/kodi.log", statFile);
+/// fprintf(stderr, "deviceId (ID of device containing file) = %u\n"
+/// "size (total size, in bytes) = %lu\n"
+/// "accessTime (time of last access) = %lu\n"
+/// "modificationTime (time of last modification) = %lu\n"
+/// "statusTime (time of last status change) = %lu\n"
+/// "isDirectory (The stat url is a directory) = %s\n"
+/// "isSymLink (The stat url is a symbolic link) = %s\n"
+/// "Return value = %i\n",
+/// statFile.GetDeviceId(),
+/// statFile.GetSize(),
+/// statFile.GetAccessTime(),
+/// statFile.GetModificationTime(),
+/// statFile.GetStatusTime(),
+/// statFile.GetIsDirectory() ? "true" : "false",
+/// statFile.GetIsSymLink() ? "true" : "false",
+/// ret);
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL StatFile(const std::string& filename, kodi::vfs::FileStatus& buffer)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->stat_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, filename.c_str(), buffer);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_File
+/// @brief Deletes a file.
+///
+/// @param[in] filename The filename to delete.
+/// @return The file was successfully deleted.
+///
+///
+/// -------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// #include <kodi/gui/DialogFileBrowser.h>
+/// #include <kodi/gui/DialogOK.h>
+/// ...
+/// std::string filename;
+/// if (kodi::gui::DialogFileBrowser::ShowAndGetFile("local", "",
+/// "Test File selection and delete of them!",
+/// filename))
+/// {
+/// bool successed = kodi::vfs::DeleteFile(filename);
+/// if (!successed)
+/// kodi::gui::DialogOK::ShowAndGetInput("Error", "Delete of File", filename, "failed!");
+/// else
+/// kodi::gui::DialogOK::ShowAndGetInput("Information", "Delete of File", filename, "successfully done.");
+/// }
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL DeleteFile(const std::string& filename)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->delete_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, filename.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_File
+/// @brief Rename a file name.
+///
+/// @param[in] filename The filename to copy.
+/// @param[in] newFileName The new filename
+/// @return true if successfully renamed
+///
+///
+inline bool ATTR_DLL_LOCAL RenameFile(const std::string& filename, const std::string& newFileName)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->rename_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, filename.c_str(), newFileName.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_File
+/// @brief Copy a file from source to destination.
+///
+/// @param[in] filename The filename to copy.
+/// @param[in] destination The destination to copy file to
+/// @return true if successfully copied
+///
+///
+inline bool ATTR_DLL_LOCAL CopyFile(const std::string& filename, const std::string& destination)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->copy_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, filename.c_str(), destination.c_str());
+}
+//------------------------------------------------------------------------------
+
+//}}}
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" General filesystem functions
+//{{{
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Retrieve MD5sum of a file.
+///
+/// @param[in] path Path to the file to MD5sum
+/// @return MD5 sum of the file
+///
+///
+/// -------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// #include <kodi/gui/DialogFileBrowser.h>
+/// ...
+/// std::string md5;
+/// std::string filename;
+/// if (kodi::gui::DialogFileBrowser::ShowAndGetFile("local", "*.avi|*.mpg|*.mp4",
+/// "Test File selection to get MD5",
+/// filename))
+/// {
+/// md5 = kodi::vfs::GetFileMD5(filename);
+/// fprintf(stderr, "MD5 of file '%s' is %s\n", md5.c_str(), filename.c_str());
+/// }
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetFileMD5(const std::string& path)
+{
+ using namespace kodi::addon;
+
+ std::string strReturn;
+ char* strMd5 = CPrivateBase::m_interface->toKodi->kodi_filesystem->get_file_md5(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str());
+ if (strMd5 != nullptr)
+ {
+ if (std::strlen(strMd5))
+ strReturn = strMd5;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ strMd5);
+ }
+ return strReturn;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Returns a thumb cache filename.
+///
+/// @param[in] filename Path to file
+/// @return Cache filename
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// #include <kodi/gui/DialogFileBrowser.h>
+/// ...
+/// std::string thumb;
+/// std::string filename;
+/// if (kodi::gui::DialogFileBrowser::ShowAndGetFile("local", "*.avi|*.mpg|*.mp4",
+/// "Test File selection to get Thumnail",
+/// filename))
+/// {
+/// thumb = kodi::vfs::GetCacheThumbName(filename);
+/// fprintf(stderr, "Thumb name of file '%s' is %s\n", thumb.c_str(), filename.c_str());
+/// }
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetCacheThumbName(const std::string& filename)
+{
+ using namespace kodi::addon;
+
+ std::string strReturn;
+ char* strThumbName = CPrivateBase::m_interface->toKodi->kodi_filesystem->get_cache_thumb_name(
+ CPrivateBase::m_interface->toKodi->kodiBase, filename.c_str());
+ if (strThumbName != nullptr)
+ {
+ if (std::strlen(strThumbName))
+ strReturn = strThumbName;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ strThumbName);
+ }
+ return strReturn;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Make filename valid.
+///
+/// Function to replace not valid characters with '_'. It can be also
+/// compared with original before in a own loop until it is equal
+/// (no invalid characters).
+///
+/// @param[in] filename Filename to check and fix
+/// @return The legal filename
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string fileName = "///\\jk???lj????.mpg";
+/// std::string legalName = kodi::vfs::MakeLegalFileName(fileName);
+/// fprintf(stderr, "Legal name of '%s' is '%s'\n", fileName.c_str(), legalName.c_str());
+///
+/// /* Returns as legal: 'jk___lj____.mpg' */
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL MakeLegalFileName(const std::string& filename)
+{
+ using namespace kodi::addon;
+
+ std::string strReturn;
+ char* strLegalFileName = CPrivateBase::m_interface->toKodi->kodi_filesystem->make_legal_filename(
+ CPrivateBase::m_interface->toKodi->kodiBase, filename.c_str());
+ if (strLegalFileName != nullptr)
+ {
+ if (std::strlen(strLegalFileName))
+ strReturn = strLegalFileName;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ strLegalFileName);
+ }
+ return strReturn;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Make directory name valid.
+///
+/// Function to replace not valid characters with '_'. It can be also
+/// compared with original before in a own loop until it is equal
+/// (no invalid characters).
+///
+/// @param[in] path Directory name to check and fix
+/// @return The legal directory name
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string path = "///\\jk???lj????\\hgjkg";
+/// std::string legalPath = kodi::vfs::MakeLegalPath(path);
+/// fprintf(stderr, "Legal name of '%s' is '%s'\n", path.c_str(), legalPath.c_str());
+///
+/// /* Returns as legal: '/jk___lj____/hgjkg' */
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL MakeLegalPath(const std::string& path)
+{
+ using namespace kodi::addon;
+
+ std::string strReturn;
+ char* strLegalPath = CPrivateBase::m_interface->toKodi->kodi_filesystem->make_legal_path(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str());
+ if (strLegalPath != nullptr)
+ {
+ if (std::strlen(strLegalPath))
+ strReturn = strLegalPath;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ strLegalPath);
+ }
+ return strReturn;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Returns the translated path.
+///
+/// @param[in] source String or unicode - Path to format
+/// @return A human-readable string suitable for logging
+///
+/// @note Only useful if you are coding for both Linux and Windows. e.g.
+/// Converts 'special://masterprofile/script_data' ->
+/// '/home/user/.kodi/UserData/script_data' on Linux.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string path = kodi::vfs::TranslateSpecialProtocol("special://masterprofile/script_data");
+/// fprintf(stderr, "Translated path is: %s\n", path.c_str());
+/// ...
+/// ~~~~~~~~~~~~~
+/// or
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// fprintf(stderr, "Directory 'special://temp' is '%s'\n", kodi::vfs::TranslateSpecialProtocol("special://temp").c_str());
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL TranslateSpecialProtocol(const std::string& source)
+{
+ using namespace kodi::addon;
+
+ std::string strReturn;
+ char* protocol = CPrivateBase::m_interface->toKodi->kodi_filesystem->translate_special_protocol(
+ CPrivateBase::m_interface->toKodi->kodiBase, source.c_str());
+ if (protocol != nullptr)
+ {
+ if (std::strlen(protocol))
+ strReturn = protocol;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ protocol);
+ }
+ return strReturn;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Retrieves information about the amount of space that is available on
+/// a disk volume.
+///
+/// Path can be also with Kodi's special protocol.
+///
+/// @param[in] path Path for where to check
+/// @param[out] capacity The total number of bytes in the file system
+/// @param[out] free The total number of free bytes in the file system
+/// @param[out] available The total number of free bytes available to a
+/// non-privileged process
+/// @return true if successfully done and set
+///
+/// @warning This only works with paths belonging to OS. If <b>"special://"</b>
+/// is used, it must point to a place on your own OS.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <climits> // for ULLONG_MAX
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string path = "special://temp";
+/// uint64_t capacity = ULLONG_MAX;
+/// uint64_t free = ULLONG_MAX;
+/// uint64_t available = ULLONG_MAX;
+/// kodi::vfs::GetDiskSpace(path, capacity, free, available);
+/// fprintf(stderr, "Path '%s' sizes:\n", path.c_str());
+/// fprintf(stderr, " - capacity: %lu MByte\n", capacity / 1024 / 1024);
+/// fprintf(stderr, " - free: %lu MByte\n", free / 1024 / 1024);
+/// fprintf(stderr, " - available: %lu MByte\n", available / 1024 / 1024);
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL GetDiskSpace(const std::string& path,
+ uint64_t& capacity,
+ uint64_t& free,
+ uint64_t& available)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->get_disk_space(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str(), &capacity, &free, &available);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Return the file name from given complete path string.
+///
+/// @param[in] path The complete path include file and directory
+/// @return Filename from path
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string fileName = kodi::vfs::GetFileName("special://temp/kodi.log");
+/// fprintf(stderr, "File name is '%s'\n", fileName.c_str());
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetFileName(const std::string& path)
+{
+ /* find the last slash */
+ const size_t slash = path.find_last_of("/\\");
+ return path.substr(slash + 1);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Return the directory name from given complete path string.
+///
+/// @param[in] path The complete path include file and directory
+/// @return Directory name from path
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string dirName = kodi::vfs::GetDirectoryName("special://temp/kodi.log");
+/// fprintf(stderr, "Directory name is '%s'\n", dirName.c_str());
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetDirectoryName(const std::string& path)
+{
+ // Will from a full filename return the directory the file resides in.
+ // Keeps the final slash at end and possible |option=foo options.
+
+ size_t iPosSlash = path.find_last_of("/\\");
+ if (iPosSlash == std::string::npos)
+ return ""; // No slash, so no path (ignore any options)
+
+ size_t iPosBar = path.rfind('|');
+ if (iPosBar == std::string::npos)
+ return path.substr(0, iPosSlash + 1); // Only path
+
+ return path.substr(0, iPosSlash + 1) + path.substr(iPosBar); // Path + options
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Remove the slash on given path name.
+///
+/// @param[in,out] path The complete path
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string dirName = "special://temp/";
+/// kodi::vfs::RemoveSlashAtEnd(dirName);
+/// fprintf(stderr, "Directory name is '%s'\n", dirName.c_str());
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL RemoveSlashAtEnd(std::string& path)
+{
+ if (!path.empty())
+ {
+ char last = path[path.size() - 1];
+ if (last == '/' || last == '\\')
+ path.erase(path.size() - 1);
+ }
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Return a size aligned to the chunk size at least as large as the
+/// chunk size.
+///
+/// @param[in] chunk The chunk size
+/// @param[in] minimum The minimum size (or maybe the minimum number of chunks?)
+/// @return The aligned size
+///
+inline unsigned int ATTR_DLL_LOCAL GetChunkSize(unsigned int chunk, unsigned int minimum)
+{
+ if (chunk)
+ return chunk * ((minimum + chunk - 1) / chunk);
+ else
+ return minimum;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Checks the given path contains a known internet protocol.
+///
+/// About following protocols are the path checked:
+/// | Protocol | Return true condition | Protocol | Return true condition
+/// |----------|-----------------------|----------|-----------------------
+/// | **dav** | strictCheck = true | **rtmps** | always
+/// | **davs** | strictCheck = true | **rtmpt** | always
+/// | **ftp** | strictCheck = true | **rtmpte** | always
+/// | **ftps** | strictCheck = true | **rtp** | always
+/// | **http** | always | **rtsp** | always
+/// | **https**| always | **sdp** | always
+/// | **mms** | always | **sftp** | strictCheck = true
+/// | **mmsh** | always | **stack** | always
+/// | **mmst** | always | **tcp** | always
+/// | **rtmp** | always | **udp** | always
+/// | **rtmpe**| always | | |
+///
+/// @param[in] path To checked path/URL
+/// @param[in] strictCheck [opt] If True the set of protocols used will be
+/// extended to include ftp, ftps, dav, davs and sftp.
+/// @return True if path is to a internet stream, false otherwise
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// // Check should return false
+/// fprintf(stderr, "File name 1 is internet stream '%s' (should no)\n",
+/// kodi::vfs::IsInternetStream("D:/my-file.mkv") ? "yes" : "no");
+///
+/// // Check should return true
+/// fprintf(stderr, "File name 2 is internet stream '%s' (should yes)\n",
+/// kodi::vfs::IsInternetStream("http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_normal.mp4") ? "yes" : "no");
+///
+/// // Check should return false
+/// fprintf(stderr, "File name 1 is internet stream '%s' (should no)\n",
+/// kodi::vfs::IsInternetStream("ftp://do-somewhere.com/the-file.mkv") ? "yes" : "no", false);
+///
+/// // Check should return true
+/// fprintf(stderr, "File name 1 is internet stream '%s' (should yes)\n",
+/// kodi::vfs::IsInternetStream("ftp://do-somewhere.com/the-file.mkv") ? "yes" : "no", true);
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL IsInternetStream(const std::string& path, bool strictCheck = false)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->is_internet_stream(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str(), strictCheck);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Checks whether the specified path refers to a local network.
+///
+/// In difference to @ref IsHostOnLAN() include this more deeper checks where
+/// also handle Kodi's special protocol and stacks.
+///
+/// @param[in] path To checked path
+/// @return True if path is on LAN, false otherwise
+///
+/// @note Check includes @ref IsHostOnLAN() too.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// // Check should return true
+/// bool lan = kodi::vfs::IsOnLAN("smb://path/to/file");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL IsOnLAN(const std::string& path)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->is_on_lan(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Checks specified path for external network.
+///
+/// @param[in] path To checked path
+/// @return True if path is remote, false otherwise
+///
+/// @note This does not apply to the local network.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// // Check should return true
+/// bool remote = kodi::vfs::IsRemote("http://path/to/file");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL IsRemote(const std::string& path)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->is_remote(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Checks whether the given path refers to the own system.
+///
+/// @param[in] path To checked path
+/// @return True if path is local, false otherwise
+///
+inline bool ATTR_DLL_LOCAL IsLocal(const std::string& path)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->is_local(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Checks specified path is a regular URL, e.g. "someprotocol://path/to/file"
+///
+/// @return True if file item is URL, false otherwise
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+///
+/// bool isURL;
+/// // Check should return true
+/// isURL = kodi::vfs::IsURL("someprotocol://path/to/file");
+///
+/// // Check should return false
+/// isURL = kodi::vfs::IsURL("/path/to/file");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL IsURL(const std::string& path)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->is_url(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str());
+}
+//--------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief To get HTTP header information.
+///
+/// @param[in] url URL source of the data
+/// @param[out] header The @ref cpp_kodi_vfs_Defs_HttpHeader
+/// @return true if successfully done, otherwise false
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_vfs_Defs_HttpHeader_Help
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// kodi::vfs::HttpHeader header;
+/// bool ret = kodi::vfs::GetHttpHeader(url, header);
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL GetHttpHeader(const std::string& url, HttpHeader& header)
+{
+ using namespace ::kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->get_http_header(
+ CPrivateBase::m_interface->toKodi->kodiBase, url.c_str(), &header.m_handle);
+}
+//----------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Get file mime type.
+///
+/// @param[in] url URL source of the data
+/// @param[out] mimeType the mime type of the URL
+/// @param[in] useragent to be used when retrieving the MimeType [opt]
+/// @return true if successfully done, otherwise false
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string mimeType;.
+/// if (kodi::vfs::GetMimeType(url, mimeType))
+/// fprintf(stderr, "The mime type is '%s'\n", mimeType.c_str());
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL GetMimeType(const std::string& url,
+ std::string& mimeType,
+ const std::string& useragent = "")
+{
+ using namespace ::kodi::addon;
+
+ char* cMimeType = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_filesystem->get_mime_type(
+ CPrivateBase::m_interface->toKodi->kodiBase, url.c_str(), &cMimeType, useragent.c_str());
+ if (cMimeType != nullptr)
+ {
+ mimeType = cMimeType;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ cMimeType);
+ }
+ return ret;
+}
+//----------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Get file content-type.
+///
+/// @param[in] url URL source of the data
+/// @param[out] content The returned type
+/// @param[in] useragent to be used when retrieving the MimeType [opt]
+/// @return true if successfully done, otherwise false
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string content;.
+/// if (kodi::vfs::GetContentType(url, content))
+/// fprintf(stderr, "The content type is '%s'\n", content.c_str());
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL GetContentType(const std::string& url,
+ std::string& content,
+ const std::string& useragent = "")
+{
+ using namespace ::kodi::addon;
+
+ char* cContent = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_filesystem->get_content_type(
+ CPrivateBase::m_interface->toKodi->kodiBase, url.c_str(), &cContent, useragent.c_str());
+ if (cContent != nullptr)
+ {
+ content = cContent;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ cContent);
+ }
+ return ret;
+}
+//----------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_vfs_General
+/// @brief Get cookies stored by CURL in RFC 2109 format.
+///
+/// @param[in] url URL source of the data
+/// @param[out] cookies The text list of available cookies
+/// @return true if successfully done, otherwise false
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+/// ...
+/// std::string url = "";
+/// std::string cookies;
+/// bool ret = kodi::vfs::GetCookies(url, cookies);
+/// fprintf(stderr, "Cookies from URL '%s' are '%s' (return was %s)\n",
+/// url.c_str(), cookies.c_str(), ret ? "true" : "false");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL GetCookies(const std::string& url, std::string& cookies)
+{
+ using namespace ::kodi::addon;
+
+ char* cCookies = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_filesystem->get_cookies(
+ CPrivateBase::m_interface->toKodi->kodiBase, url.c_str(), &cCookies);
+ if (cCookies != nullptr)
+ {
+ cookies = cCookies;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ cCookies);
+ }
+ return ret;
+}
+//----------------------------------------------------------------------------
+
+//}}}
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" CFile class
+//{{{
+
+//==============================================================================
+/// @defgroup cpp_kodi_vfs_CFile 4. class CFile
+/// @ingroup cpp_kodi_vfs
+///
+/// @brief **Creatable class for virtual file server control**\n
+/// To perform file read/write with Kodi's filesystem parts.
+///
+/// CFile is the class used for handling Files in Kodi. This class can be used
+/// for creating, reading, writing and modifying files. It directly provides unbuffered, binary disk input/output services
+///
+/// It has the header @ref Filesystem.h "#include <kodi/Filesystem.h>" be included
+/// to enjoy it.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Filesystem.h>
+///
+/// ...
+///
+/// /* Create the needed file handle class */
+/// kodi::vfs::CFile myFile();
+///
+/// /* In this example we use the user path for the add-on */
+/// std::string file = kodi::GetUserPath() + "/myFile.txt";
+///
+/// /* Now create and open the file or overwrite if present */
+/// myFile.OpenFileForWrite(file, true);
+///
+/// const char* str = "I love Kodi!";
+///
+/// /* write string */
+/// myFile.Write(str, sizeof(str));
+///
+/// /* On this way the Close() is not needed to call, becomes done from destructor */
+///
+/// ~~~~~~~~~~~~~
+///
+///@{
+class ATTR_DLL_LOCAL CFile
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Construct a new, unopened file.
+ ///
+ CFile() = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief <b>`Close()`</b> is called from the destructor, so explicitly
+ /// closing the file isn't required.
+ ///
+ virtual ~CFile() { Close(); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Open the file with filename via Kodi's @ref cpp_kodi_vfs_CFile
+ /// "CFile". Needs to be closed by calling Close() when done.
+ ///
+ /// @param[in] filename The filename to open.
+ /// @param[in] flags [opt] The flags to pass, see @ref OpenFileFlags
+ /// @return True on success or false on failure
+ ///
+ bool OpenFile(const std::string& filename, unsigned int flags = 0)
+ {
+ using namespace kodi::addon;
+
+ Close();
+ m_file = CPrivateBase::m_interface->toKodi->kodi_filesystem->open_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, filename.c_str(), flags);
+ return m_file != nullptr;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Open the file with filename via Kodi's @ref cpp_kodi_vfs_CFile
+ /// "CFile" in write mode. Needs to be closed by calling Close() when
+ /// done.
+ ///
+ /// @note Related folders becomes created if not present.
+ ///
+ /// @param[in] filename The filename to open.
+ /// @param[in] overwrite True to overwrite, false otherwise.
+ /// @return True on success or false on failure
+ ///
+ bool OpenFileForWrite(const std::string& filename, bool overwrite = false)
+ {
+ using namespace kodi::addon;
+
+ Close();
+
+ // Try to open the file. If it fails, check if we need to create the directory first
+ // This way we avoid checking if the directory exists every time
+ m_file = CPrivateBase::m_interface->toKodi->kodi_filesystem->open_file_for_write(
+ CPrivateBase::m_interface->toKodi->kodiBase, filename.c_str(), overwrite);
+ if (!m_file)
+ {
+ std::string cacheDirectory = kodi::vfs::GetDirectoryName(filename);
+ if (CPrivateBase::m_interface->toKodi->kodi_filesystem->directory_exists(
+ CPrivateBase::m_interface->toKodi->kodiBase, cacheDirectory.c_str()) ||
+ CPrivateBase::m_interface->toKodi->kodi_filesystem->create_directory(
+ CPrivateBase::m_interface->toKodi->kodiBase, cacheDirectory.c_str()))
+ m_file = CPrivateBase::m_interface->toKodi->kodi_filesystem->open_file_for_write(
+ CPrivateBase::m_interface->toKodi->kodiBase, filename.c_str(), overwrite);
+ }
+ return m_file != nullptr;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Check file is opened.
+ ///
+ /// @return True on open or false on closed or failure
+ ///
+ bool IsOpen() const { return m_file != nullptr; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Close an open file.
+ ///
+ void Close()
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return;
+ CPrivateBase::m_interface->toKodi->kodi_filesystem->close_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file);
+ m_file = nullptr;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Create a Curl representation
+ ///
+ /// @param[in] url The URL of the Type.
+ /// @return True on success or false on failure
+ ///
+ bool CURLCreate(const std::string& url)
+ {
+ using namespace kodi::addon;
+
+ m_file = CPrivateBase::m_interface->toKodi->kodi_filesystem->curl_create(
+ CPrivateBase::m_interface->toKodi->kodiBase, url.c_str());
+ return m_file != nullptr;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Add options to the curl file created with CURLCreate.
+ ///
+ /// @param[in] type Option type to set, see @ref CURLOptiontype
+ /// @param[in] name Name of the option
+ /// @param[in] value Value of the option
+ /// @return True on success or false on failure
+ ///
+ bool CURLAddOption(CURLOptiontype type, const std::string& name, const std::string& value)
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ {
+ kodi::Log(ADDON_LOG_ERROR, "kodi::vfs::CURLCreate(...) needed to call before!");
+ return false;
+ }
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->curl_add_option(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file, type, name.c_str(), value.c_str());
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Open the curl file created with CURLCreate.
+ ///
+ /// @param[in] flags [opt] The flags to pass, see @ref OpenFileFlags
+ /// @return True on success or false on failure
+ ///
+ bool CURLOpen(unsigned int flags = 0)
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ {
+ kodi::Log(ADDON_LOG_ERROR, "kodi::vfs::CURLCreate(...) needed to call before!");
+ return false;
+ }
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->curl_open(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file, flags);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Read from an open file.
+ ///
+ /// @param[in] ptr The buffer to store the data in.
+ /// @param[in] size The size of the buffer.
+ /// @return number of successfully read bytes if any bytes were read and
+ /// stored in buffer, zero if no bytes are available to read (end of
+ /// file was reached) or undetectable error occur, -1 in case of any
+ /// explicit error
+ ///
+ ssize_t Read(void* ptr, size_t size)
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return -1;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->read_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file, ptr, size);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Read a string from an open file.
+ ///
+ /// @param[out] line The buffer to store the data in.
+ /// @return True when a line was read, false otherwise.
+ ///
+ bool ReadLine(std::string& line)
+ {
+ using namespace kodi::addon;
+
+ line.clear();
+ if (!m_file)
+ return false;
+ // Read 1024 chars into buffer. If file position advanced that many
+ // chars, we didn't hit a newline. Otherwise, we read a newline
+ // or we reached the end of the file.
+ //
+ // The strncpy idiom is used here (C++ allows a simpler implementation):
+ //
+ // char buffer[BUFFER_SIZE];
+ // strncpy(buffer, sourceString, BUFFER_SIZE - 1);
+ // buffer[BUFFER_SIZE - 1] = '\0';
+ //
+ char buffer[1025]{};
+ if (CPrivateBase::m_interface->toKodi->kodi_filesystem->read_file_string(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file, buffer, sizeof(buffer) - 1))
+ {
+ line = buffer;
+ return !line.empty();
+ }
+ return false;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Write to a file opened in write mode.
+ ///
+ /// @param[in] ptr Pointer to the data to write, converted to a <b>`const void*`</b>.
+ /// @param[in] size Size of the data to write.
+ /// @return number of successfully written bytes if any bytes were written,
+ /// zero if no bytes were written and no detectable error occur,-1
+ /// in case of any explicit error
+ ///
+ ssize_t Write(const void* ptr, size_t size)
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return -1;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->write_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file, ptr, size);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Flush buffered data.
+ ///
+ /// If the given stream was open for writing (or if it was open for updating
+ /// and the last i/o operation was an output operation) any unwritten data
+ /// in its output buffer is written to the file.
+ ///
+ /// The stream remains open after this call.
+ ///
+ /// When a file is closed, either because of a call to close or because the
+ /// class is destructed, all the buffers associated with it are
+ /// automatically flushed.
+ ///
+ void Flush()
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return;
+ CPrivateBase::m_interface->toKodi->kodi_filesystem->flush_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Set the file's current position.
+ ///
+ /// The whence argument is optional and defaults to SEEK_SET (0)
+ ///
+ /// @param[in] position the position that you want to seek to
+ /// @param[in] whence [optional] offset relative to You can set the value of
+ /// whence to one of three things:
+ /// | Value | int | Description |
+ /// |:--------:|:---:|:----------------------------------------------------|
+ /// | SEEK_SET | 0 | position is relative to the beginning of the file. This is probably what you had in mind anyway, and is the most commonly used value for whence.
+ /// | SEEK_CUR | 1 | position is relative to the current file pointer position. So, in effect, you can say, "Move to my current position plus 30 bytes," or, "move to my current position minus 20 bytes."
+ /// | SEEK_END | 2 | position is relative to the end of the file. Just like SEEK_SET except from the other end of the file. Be sure to use negative values for offset if you want to back up from the end of the file, instead of going past the end into oblivion.
+ ///
+ /// @return Returns the resulting offset location as measured in bytes from
+ /// the beginning of the file. On error, the value -1 is returned.
+ ///
+ int64_t Seek(int64_t position, int whence = SEEK_SET)
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return -1;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->seek_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file, position, whence);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Truncate a file to the requested size.
+ ///
+ /// @param[in] size The new max size.
+ /// @return New size? On error, the value -1 is returned.
+ ///
+ int Truncate(int64_t size)
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return -1;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->truncate_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file, size);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief The current offset in an open file.
+ ///
+ /// @return The requested offset. On error, the value -1 is returned.
+ ///
+ int64_t GetPosition() const
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return -1;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->get_file_position(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Get the file size of an open file.
+ ///
+ /// @return The requested size. On error, the value -1 is returned.
+ ///
+ int64_t GetLength() const
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return -1;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->get_file_length(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Checks the file access is on end position.
+ ///
+ /// @return If you've reached the end of the file, AtEnd() returns true.
+ ///
+ bool AtEnd() const
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return true;
+ int64_t length = CPrivateBase::m_interface->toKodi->kodi_filesystem->get_file_length(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file);
+ int64_t position = CPrivateBase::m_interface->toKodi->kodi_filesystem->get_file_position(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file);
+ return position >= length;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Get the chunk size for an open file.
+ ///
+ /// @return The requested size. On error, the value -1 is returned.
+ ///
+ int GetChunkSize() const
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return -1;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->get_file_chunk_size(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief To check seek possible on current stream by file.
+ ///
+ /// @return true if seek possible, false if not
+ ///
+ bool IoControlGetSeekPossible() const
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return false;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->io_control_get_seek_possible(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief To check a running stream on file for state of his cache.
+ ///
+ /// @param[in] status Information about current cache status
+ /// @return true if successfully done, false otherwise
+ ///
+ ///
+ /// @copydetails cpp_kodi_vfs_Defs_CacheStatus_Help
+ ///
+ bool IoControlGetCacheStatus(CacheStatus& status) const
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return false;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->io_control_get_cache_status(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file, status);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Unsigned int with speed limit for caching in bytes per second.
+ ///
+ /// @param[in] rate Cache rate size to use
+ /// @return true if successfully done, false otherwise
+ ///
+ bool IoControlSetCacheRate(uint32_t rate)
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return false;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->io_control_set_cache_rate(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file, rate);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Enable/disable retry within the protocol handler (if supported).
+ ///
+ /// @param[in] retry To set the retry, true for use, false for not
+ /// @return true if successfully done, false otherwise
+ ///
+ bool IoControlSetRetry(bool retry)
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return false;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->io_control_set_retry(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file, retry);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Retrieve a file property.
+ ///
+ /// @param[in] type The type of the file property to retrieve the value for
+ /// @param[in] name The name of a named property value (e.g. Header)
+ /// @return value of requested property, empty on failure / non-existance
+ ///
+ const std::string GetPropertyValue(FilePropertyTypes type, const std::string& name) const
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ {
+ kodi::Log(ADDON_LOG_ERROR,
+ "kodi::vfs::CURLCreate(...) needed to call before GetPropertyValue!");
+ return "";
+ }
+ std::vector<std::string> values = GetPropertyValues(type, name);
+ if (values.empty())
+ {
+ return "";
+ }
+ return values[0];
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Retrieve file property values.
+ ///
+ /// @param[in] type The type of the file property values to retrieve the value for
+ /// @param[in] name The name of the named property (e.g. Header)
+ /// @return values of requested property, empty vector on failure / non-existance
+ ///
+ const std::vector<std::string> GetPropertyValues(FilePropertyTypes type,
+ const std::string& name) const
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ {
+ kodi::Log(ADDON_LOG_ERROR,
+ "kodi::vfs::CURLCreate(...) needed to call before GetPropertyValues!");
+ return std::vector<std::string>();
+ }
+ int numValues = 0;
+ char** res(CPrivateBase::m_interface->toKodi->kodi_filesystem->get_property_values(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file, type, name.c_str(), &numValues));
+ if (res)
+ {
+ std::vector<std::string> vecReturn;
+ vecReturn.reserve(numValues);
+ for (int i = 0; i < numValues; ++i)
+ {
+ vecReturn.emplace_back(res[i]);
+ }
+ CPrivateBase::m_interface->toKodi->free_string_array(
+ CPrivateBase::m_interface->toKodi->kodiBase, res, numValues);
+ return vecReturn;
+ }
+ return std::vector<std::string>();
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_vfs_CFile
+ /// @brief Get the current download speed of file if loaded from web.
+ ///
+ /// @return The current download speed.
+ ///
+ double GetFileDownloadSpeed() const
+ {
+ using namespace kodi::addon;
+
+ if (!m_file)
+ return 0.0;
+ return CPrivateBase::m_interface->toKodi->kodi_filesystem->get_file_download_speed(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_file);
+ }
+ //--------------------------------------------------------------------------
+
+private:
+ void* m_file = nullptr;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//}}}
+
+} /* namespace vfs */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/General.h b/xbmc/addons/kodi-dev-kit/include/kodi/General.h
new file mode 100644
index 0000000..71678c2
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/General.h
@@ -0,0 +1,688 @@
+/*
+ * 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 "AddonBase.h"
+#include "c-api/general.h"
+#include "tools/StringUtils.h"
+
+#ifdef __cplusplus
+
+//==============================================================================
+/// \ingroup cpp_kodi_Defs
+/// @brief For kodi::Version used structure
+///
+typedef struct kodi_version_t
+{
+ /// Application name, normally 'Kodi'
+ std::string compile_name;
+ /// Major code version of Kodi
+ int major;
+ /// Minor code version of Kodi
+ int minor;
+ /// The Revision contains a id and the build date, e.g. 20170706-c6b22fe217-dirty
+ std::string revision;
+ /// The version candidate e.g. alpha, beta or release
+ std::string tag;
+ /// The revision of tag before
+ std::string tag_revision;
+} kodi_version_t;
+//------------------------------------------------------------------------------
+
+namespace kodi
+{
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief Translate a string with an unknown encoding to UTF8.
+///
+/// @param[in] stringSrc The string to translate.
+/// @param[out] utf8StringDst The translated string.
+/// @param[in] failOnBadChar [opt] returns failed if bad character is inside <em>(default is <b><c>false</c></b>)</em>
+/// @return true if OK
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// std::string ret;
+/// if (!kodi::UnknownToUTF8("test string", ret, true))
+/// fprintf(stderr, "Translation to UTF8 failed!\n");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL UnknownToUTF8(const std::string& stringSrc,
+ std::string& utf8StringDst,
+ bool failOnBadChar = false)
+{
+ using namespace kodi::addon;
+
+ bool ret = false;
+ char* retString = CPrivateBase::m_interface->toKodi->kodi->unknown_to_utf8(
+ CPrivateBase::m_interface->toKodi->kodiBase, stringSrc.c_str(), &ret, failOnBadChar);
+ if (retString != nullptr)
+ {
+ if (ret)
+ utf8StringDst = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief Returns the active language as a string.
+///
+/// @param[in] format Used format of the returned language string
+/// | enum code: | Description: |
+/// |----------------------:|------------------------------------------------------------|
+/// | LANG_FMT_ENGLISH_NAME | full language name in English (Default) |
+/// | LANG_FMT_ISO_639_1 | two letter code as defined in ISO 639-1 |
+/// | LANG_FMT_ISO_639_2 | three letter code as defined in ISO 639-2/T or ISO 639-2/B |
+/// @param[in] region [opt] append the region delimited by "-" of the language (setting) to the returned language string <em>(default is <b><c>false</c></b>)</em>
+/// @return active language
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// std::string language = kodi::GetLanguage(LANG_FMT_ISO_639_1, false);
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetLanguage(LangFormats format = LANG_FMT_ENGLISH_NAME,
+ bool region = false)
+{
+ using namespace kodi::addon;
+
+ std::string language;
+ char* retString = CPrivateBase::m_interface->toKodi->kodi->get_language(
+ CPrivateBase::m_interface->toKodi->kodiBase, format, region);
+ if (retString != nullptr)
+ {
+ if (std::strlen(retString))
+ language = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return language;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief Writes the C string pointed by format in the GUI. If format includes
+/// format specifiers (subsequences beginning with %), the additional arguments
+/// following format are formatted and inserted in the resulting string replacing
+/// their respective specifiers.
+///
+/// After the format parameter, the function expects at least as many additional
+/// arguments as specified by format.
+///
+/// @param[in] type The message type.
+/// | enum code: | Description: |
+/// |---------------:|-----------------------------------|
+/// | QUEUE_INFO | Show info notification message |
+/// | QUEUE_WARNING | Show warning notification message |
+/// | QUEUE_ERROR | Show error notification message |
+/// @param[in] format The format of the message to pass to display in Kodi.
+/// C string that contains the text to be written to the stream.
+/// It can optionally contain embedded format specifiers that are
+/// replaced by the values specified in subsequent additional
+/// arguments and formatted as requested.
+/// | specifier | Output | Example
+/// |------------|----------------------------------------------------|------------
+/// | d or i | Signed decimal integer | 392
+/// | u | Unsigned decimal integer | 7235
+/// | o | Unsigned octal | 610
+/// | x | Unsigned hexadecimal integer | 7fa
+/// | X | Unsigned hexadecimal integer (uppercase) | 7FA
+/// | f | Decimal floating point, lowercase | 392.65
+/// | F | Decimal floating point, uppercase | 392.65
+/// | e | Scientific notation (mantissa/exponent), lowercase | 3.9265e+2
+/// | E | Scientific notation (mantissa/exponent), uppercase | 3.9265E+2
+/// | g | Use the shortest representation: %e or %f | 392.65
+/// | G | Use the shortest representation: %E or %F | 392.65
+/// | a | Hexadecimal floating point, lowercase | -0xc.90fep-2
+/// | A | Hexadecimal floating point, uppercase | -0XC.90FEP-2
+/// | c | Character | a
+/// | s | String of characters | sample
+/// | p | Pointer address | b8000000
+/// | % | A % followed by another % character will write a single % to the stream. | %
+///
+/// The length sub-specifier modifies the length of the data type. This is a chart
+/// showing the types used to interpret the corresponding arguments with and without
+/// length specifier (if a different type is used, the proper type promotion or
+/// conversion is performed, if allowed):
+/// | length| d i | u o x X | f F e E g G a A | c | s | p | n |
+/// |-------|---------------|-----------------------|-----------------|-------|---------|---------|-----------------|
+/// | (none)| int | unsigned int | double | int | char* | void* | int* |
+/// | hh | signed char | unsigned char | | | | | signed char* |
+/// | h | short int | unsigned short int | | | | | short int* |
+/// | l | long int | unsigned long int | | wint_t| wchar_t*| | long int* |
+/// | ll | long long int | unsigned long long int| | | | | long long int* |
+/// | j | intmax_t | uintmax_t | | | | | intmax_t* |
+/// | z | size_t | size_t | | | | | size_t* |
+/// | t | ptrdiff_t | ptrdiff_t | | | | | ptrdiff_t* |
+/// | L | | | long double | | | | |
+/// <b>Note:</b> that the c specifier takes an int (or wint_t) as argument, but performs the proper conversion to a char value
+/// (or a wchar_t) before formatting it for output.
+/// @param[in] ... (additional arguments) Depending on the format string, the function
+/// may expect a sequence of additional arguments, each containing a value
+/// to be used to replace a format specifier in the format string (or a pointer
+/// to a storage location, for n).
+/// There should be at least as many of these arguments as the number of values specified
+/// in the format specifiers. Additional arguments are ignored by the function.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// kodi::QueueFormattedNotification(QUEUE_WARNING, "I'm want to inform you, here with a test call to show '%s'", "this");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL QueueFormattedNotification(QueueMsg type, const char* format, ...)
+{
+ using namespace kodi::addon;
+
+ va_list args;
+ va_start(args, format);
+ const std::string str = kodi::tools::StringUtils::FormatV(format, args);
+ va_end(args);
+ CPrivateBase::m_interface->toKodi->kodi->queue_notification(
+ CPrivateBase::m_interface->toKodi->kodiBase, type, "", str.c_str(), "", 5000, false, 1000);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief Queue a notification in the GUI.
+///
+/// @param[in] type The message type.
+/// | enum code: | Description:
+/// |----------------------:|-----------------------------------
+/// | QUEUE_INFO | Show info notification message
+/// | QUEUE_WARNING | Show warning notification message
+/// | QUEUE_ERROR | Show error notification message
+/// | QUEUE_OWN_STYLE | If used can be with imageFile the wanted image set or if leaved empty shown as info, also are the other optional values available then
+/// @param[in] header Header Name (if leaved empty becomes addon name used)
+/// @param[in] message Message to display on Kodi
+/// @param[in] imageFile [opt] The image file to show on message (to use must be type set to QUEUE_OWN_STYLE)
+/// @param[in] displayTime [opt] The time how long message is displayed <b>(default 5 sec)</b> (to use must be type set to QUEUE_OWN_STYLE)
+/// @param[in] withSound [opt] if true also warning sound becomes played <b>(default with sound)</b> (to use must be type set to QUEUE_OWN_STYLE)
+/// @param[in] messageTime [opt] how many milli seconds start show of notification <b>(default 1 sec)</b> (to use must be type set to QUEUE_OWN_STYLE)
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// kodi::QueueNotification(QUEUE_OWN_STYLE, "I'm want to inform you", "Here with a test call", "", 3000, false, 1000);
+/// ...
+/// ~~~~~~~~~~~~~
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// kodi::QueueNotification(QUEUE_WARNING, "I'm want to inform you", "Here with a test call");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// kodi::QueueNotification(QUEUE_OWN_STYLE, "", "Here with a test call", "./myImage.png");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL QueueNotification(QueueMsg type,
+ const std::string& header,
+ const std::string& message,
+ const std::string& imageFile = "",
+ unsigned int displayTime = 5000,
+ bool withSound = true,
+ unsigned int messageTime = 1000)
+{
+ using namespace kodi::addon;
+
+ CPrivateBase::m_interface->toKodi->kodi->queue_notification(
+ CPrivateBase::m_interface->toKodi->kodiBase, type, header.c_str(), message.c_str(),
+ imageFile.c_str(), displayTime, withSound, messageTime);
+}
+//------------------------------------------------------------------------------
+
+//============================================================================
+/// \ingroup cpp_kodi
+/// @brief Get the MD5 digest of the given text
+///
+/// @param[in] text text to compute the MD5 for
+/// @return Returned MD5 digest
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// std::string md5 = kodi::GetMD5("Make me as md5");
+/// fprintf(stderr, "My md5 digest is: '%s'\n", md5.c_str());
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetMD5(const std::string& text)
+{
+ using namespace kodi::addon;
+
+ char* md5ret = static_cast<char*>(malloc(40 * sizeof(char))); // md5 size normally 32 bytes
+ CPrivateBase::m_interface->toKodi->kodi->get_md5(CPrivateBase::m_interface->toKodi->kodiBase,
+ text.c_str(), md5ret);
+ std::string md5 = md5ret;
+ free(md5ret);
+ return md5;
+}
+//----------------------------------------------------------------------------
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief Returns your regions setting as a string for the specified id
+///
+/// @param[in] id id of setting to return
+/// | | Choices are | |
+/// |:------------:|:------------:|:------------:|
+/// | dateshort | time | tempunit |
+/// | datelong | meridiem | speedunit |
+///
+/// @return settings string
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// std::string timeFormat = kodi::GetRegion("time");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetRegion(const std::string& id)
+{
+ using namespace kodi::addon;
+
+ AddonToKodiFuncTable_Addon* toKodi = CPrivateBase::m_interface->toKodi;
+
+ std::string strReturn;
+ char* strMsg = toKodi->kodi->get_region(toKodi->kodiBase, id.c_str());
+ if (strMsg != nullptr)
+ {
+ if (std::strlen(strMsg))
+ strReturn = strMsg;
+ toKodi->free_string(toKodi->kodiBase, strMsg);
+ }
+ return strReturn;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief Returns the amount of free memory in MByte (or as bytes) as an long
+/// integer
+///
+/// @param[out] free free memory
+/// @param[out] total total memory
+/// @param[in] asBytes [opt] if set to true becomes returned as bytes, otherwise
+/// as mega bytes
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// long freeMem;
+/// long totalMem;
+/// kodi::GetFreeMem(freeMem, totalMem);
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL GetFreeMem(long& free, long& total, bool asBytes = false)
+{
+ using namespace kodi::addon;
+
+ free = -1;
+ total = -1;
+ AddonToKodiFuncTable_Addon* toKodi = CPrivateBase::m_interface->toKodi;
+ toKodi->kodi->get_free_mem(toKodi->kodiBase, &free, &total, asBytes);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief Returns the elapsed idle time in seconds as an integer
+///
+/// @return idle time
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// int time = kodi::GetGlobalIdleTime();
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline int ATTR_DLL_LOCAL GetGlobalIdleTime()
+{
+ using namespace kodi::addon;
+
+ AddonToKodiFuncTable_Addon* toKodi = CPrivateBase::m_interface->toKodi;
+ return toKodi->kodi->get_global_idle_time(toKodi->kodiBase);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief Get the currently used skin identification name from Kodi
+///
+/// @return The active skin id name as a string
+///
+///
+/// @note This is not the full path like 'special://home/addons/MediaCenter',
+/// but only 'MediaCenter'.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ..
+/// std::string skinid = kodi::GetCurrentSkinId();
+/// ..
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetCurrentSkinId()
+{
+ using namespace kodi::addon;
+
+ AddonToKodiFuncTable_Addon* toKodi = CPrivateBase::m_interface->toKodi;
+
+ std::string strReturn;
+ char* strMsg = toKodi->kodi->get_current_skin_id(toKodi->kodiBase);
+ if (strMsg != nullptr)
+ {
+ if (std::strlen(strMsg))
+ strReturn = strMsg;
+ toKodi->free_string(toKodi->kodiBase, strMsg);
+ }
+ return strReturn;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief To check another addon is available and usable inside Kodi.
+///
+/// @param[in] id The wanted addon identification string to check
+/// @param[out] version Version string of addon if **installed** inside Kodi
+/// @param[out] enabled Set to true <b>`true* </b> if addon is enabled
+/// @return Returns <b>`true* </b> if addon is installed
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+///
+/// bool enabled = false;
+/// std::string version;
+/// bool ret = kodi::IsAddonAvailable("inputstream.adaptive", version, enabled);
+/// fprintf(stderr, "Available inputstream.adaptive version '%s' and enabled '%s'\n",
+/// ret ? version.c_str() : "not installed", enabled ? "yes" : "no");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL IsAddonAvailable(const std::string& id,
+ std::string& version,
+ bool& enabled)
+{
+ using namespace kodi::addon;
+
+ AddonToKodiFuncTable_Addon* toKodi = CPrivateBase::m_interface->toKodi;
+
+ char* cVersion = nullptr;
+ bool ret = toKodi->kodi->is_addon_avilable(toKodi->kodiBase, id.c_str(), &cVersion, &enabled);
+ if (cVersion)
+ {
+ version = cVersion;
+ toKodi->free_string(toKodi->kodiBase, cVersion);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief Get current Kodi information and versions, returned data from the following
+/// <b><tt>kodi_version_t version; kodi::KodiVersion(version);</tt></b>
+/// is e.g.:
+/// ~~~~~~~~~~~~~{.cpp}
+/// version.compile_name = Kodi
+/// version.major = 18
+/// version.minor = 0
+/// version.revision = 20170706-c6b22fe217-di
+/// version.tag = alpha
+/// version.tag_revision = 1
+/// ~~~~~~~~~~~~~
+///
+/// @param[out] version structure to store data from kodi
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// kodi_version_t version;
+/// kodi::KodiVersion(version);
+/// fprintf(stderr,
+/// "kodi_version_t version;\n"
+/// "kodi::KodiVersion(version);\n"
+/// " - version.compile_name = %s\n"
+/// " - version.major = %i\n"
+/// " - version.minor = %i\n"
+/// " - version.revision = %s\n"
+/// " - version.tag = %s\n"
+/// " - version.tag_revision = %s\n",
+/// version.compile_name.c_str(), version.major, version.minor,
+/// version.revision.c_str(), version.tag.c_str(), version.tag_revision.c_str());
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL KodiVersion(kodi_version_t& version)
+{
+ using namespace kodi::addon;
+
+ char* compile_name = nullptr;
+ char* revision = nullptr;
+ char* tag = nullptr;
+ char* tag_revision = nullptr;
+
+ AddonToKodiFuncTable_Addon* toKodi = CPrivateBase::m_interface->toKodi;
+ toKodi->kodi->kodi_version(toKodi->kodiBase, &compile_name, &version.major, &version.minor,
+ &revision, &tag, &tag_revision);
+ if (compile_name != nullptr)
+ {
+ version.compile_name = compile_name;
+ toKodi->free_string(toKodi->kodiBase, compile_name);
+ }
+ if (revision != nullptr)
+ {
+ version.revision = revision;
+ toKodi->free_string(toKodi->kodiBase, revision);
+ }
+ if (tag != nullptr)
+ {
+ version.tag = tag;
+ toKodi->free_string(toKodi->kodiBase, tag);
+ }
+ if (tag_revision != nullptr)
+ {
+ version.tag_revision = tag_revision;
+ toKodi->free_string(toKodi->kodiBase, tag_revision);
+ }
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief To get keyboard layout characters
+///
+/// This is used to get the keyboard layout currently used from Kodi by the
+/// there set language.
+///
+/// @param[in] modifierKey the key to define the needed layout (uppercase, symbols...)
+/// @param[out] layout_name name of used layout
+/// @param[out] layout list of selected keyboard layout
+/// @return true if request successed
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// std::string layout_name;
+/// std::vector<std::vector<std::string>> layout;
+/// kodi::GetKeyboardLayout(STD_KB_MODIFIER_KEY_SHIFT | STD_KB_MODIFIER_KEY_SYMBOL, layout_name, layout);
+/// fprintf(stderr, "Layout: '%s'\n", layout_name.c_str());
+/// for (unsigned int row = 0; row < STD_KB_BUTTONS_MAX_ROWS; row++)
+/// {
+/// for (unsigned int column = 0; column < STD_KB_BUTTONS_PER_ROW; column++)
+/// {
+/// fprintf(stderr, " - Row: '%02i'; Column: '%02i'; Text: '%s'\n", row, column, layout[row][column].c_str());
+/// }
+/// }
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL GetKeyboardLayout(int modifierKey,
+ std::string& layout_name,
+ std::vector<std::vector<std::string>>& layout)
+{
+ using namespace kodi::addon;
+
+ AddonToKodiFuncTable_Addon* toKodi = CPrivateBase::m_interface->toKodi;
+ AddonKeyboardKeyTable c_layout;
+ char* c_layout_name = nullptr;
+ bool ret =
+ toKodi->kodi->get_keyboard_layout(toKodi->kodiBase, &c_layout_name, modifierKey, &c_layout);
+ if (ret)
+ {
+ if (c_layout_name)
+ {
+ layout_name = c_layout_name;
+ toKodi->free_string(toKodi->kodiBase, c_layout_name);
+ }
+
+ layout.resize(STD_KB_BUTTONS_MAX_ROWS);
+ for (unsigned int row = 0; row < STD_KB_BUTTONS_MAX_ROWS; row++)
+ {
+ layout[row].resize(STD_KB_BUTTONS_PER_ROW);
+ for (unsigned int column = 0; column < STD_KB_BUTTONS_PER_ROW; column++)
+ {
+ char* button = c_layout.keys[row][column];
+ if (button)
+ {
+ layout[row][column] = button;
+ toKodi->free_string(toKodi->kodiBase, button);
+ }
+ }
+ }
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// \ingroup cpp_kodi
+/// @brief To change keyboard layout characters
+///
+/// This is used to change the keyboard layout currently used from Kodi
+///
+/// @param[out] layout_name new name of used layout (input string not used!)
+/// @return true if request successed
+///
+/// @note @ref GetKeyboardLayout must be called afterwards.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// ...
+/// std::string layout_name;
+/// kodi::ChangeKeyboardLayout(layout_name);
+///
+/// std::vector<std::vector<std::string>> layout;
+/// kodi::GetKeyboardLayout(STD_KB_MODIFIER_KEY_SHIFT | STD_KB_MODIFIER_KEY_SYMBOL, layout_name, layout);
+/// fprintf(stderr, "Layout: '%s'\n", layout_name.c_str());
+/// for (unsigned int row = 0; row < STD_KB_BUTTONS_MAX_ROWS; row++)
+/// {
+/// for (unsigned int column = 0; column < STD_KB_BUTTONS_PER_ROW; column++)
+/// {
+/// fprintf(stderr, " - Row: '%02i'; Column: '%02i'; Text: '%s'\n", row, column, layout[row][column].c_str());
+/// }
+/// }
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL ChangeKeyboardLayout(std::string& layout_name)
+{
+ using namespace kodi::addon;
+
+ AddonToKodiFuncTable_Addon* toKodi = CPrivateBase::m_interface->toKodi;
+ char* c_layout_name = nullptr;
+ bool ret = toKodi->kodi->change_keyboard_layout(toKodi->kodiBase, &c_layout_name);
+ if (c_layout_name)
+ {
+ layout_name = c_layout_name;
+ toKodi->free_string(toKodi->kodiBase, c_layout_name);
+ }
+
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/Network.h b/xbmc/addons/kodi-dev-kit/include/kodi/Network.h
new file mode 100644
index 0000000..e6ff2c9
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/Network.h
@@ -0,0 +1,287 @@
+/*
+ * 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 "AddonBase.h"
+#include "c-api/network.h"
+
+#ifdef __cplusplus
+
+//==============================================================================
+/// @defgroup cpp_kodi_network Interface - kodi::network
+/// @ingroup cpp
+/// @brief **Network functions**\n
+/// The network module offers functions that allow you to control it.
+///
+/// It has the header @ref Network.h "#include <kodi/Network.h>" be included
+/// to enjoy it.
+///
+//------------------------------------------------------------------------------
+
+namespace kodi
+{
+namespace network
+{
+
+//============================================================================
+/// @ingroup cpp_kodi_network
+/// @brief Send WakeOnLan magic packet.
+///
+/// @param[in] mac Network address of the host to wake.
+/// @return True if the magic packet was successfully sent, false otherwise.
+///
+inline bool ATTR_DLL_LOCAL WakeOnLan(const std::string& mac)
+{
+ using namespace ::kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_network->wake_on_lan(
+ CPrivateBase::m_interface->toKodi->kodiBase, mac.c_str());
+}
+//----------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_network
+/// @brief To the current own ip address as a string.
+///
+/// @return Own system ip.
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Network.h>
+/// ...
+/// std::string ipAddress = kodi::network::GetIPAddress();
+/// fprintf(stderr, "My IP is '%s'\n", ipAddress.c_str());
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetIPAddress()
+{
+ using namespace ::kodi::addon;
+
+ std::string ip;
+ char* string = CPrivateBase::m_interface->toKodi->kodi_network->get_ip_address(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+ if (string != nullptr)
+ {
+ ip = string;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ string);
+ }
+ return ip;
+}
+//----------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_network
+/// @brief Return our hostname.
+///
+/// @return String about hostname, empty in case of error
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Network.h>
+/// ...
+/// std::string hostname = kodi::network::GetHostname();
+/// fprintf(stderr, "My hostname is '%s'\n", hostname.c_str());
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline std::string ATTR_DLL_LOCAL GetHostname()
+{
+ using namespace ::kodi::addon;
+
+ std::string ip;
+ char* string = CPrivateBase::m_interface->toKodi->kodi_network->get_hostname(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+ if (string != nullptr)
+ {
+ ip = string;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ string);
+ }
+ return ip;
+}
+//----------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_network
+/// @brief Returns Kodi's HTTP UserAgent string.
+///
+/// @return HTTP user agent
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.py}
+/// ..
+/// std::string agent = kodi::network::GetUserAgent()
+/// ..
+/// ~~~~~~~~~~~~~
+///
+/// example output:
+/// Kodi/19.0-ALPHA1 (X11; Linux x86_64) Ubuntu/20.04 App_Bitness/64 Version/19.0-ALPHA1-Git:20200522-0076d136d3-dirty
+///
+inline std::string ATTR_DLL_LOCAL GetUserAgent()
+{
+ using namespace ::kodi::addon;
+
+ std::string agent;
+ char* string = CPrivateBase::m_interface->toKodi->kodi_network->get_user_agent(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+ if (string != nullptr)
+ {
+ agent = string;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ string);
+ }
+ return agent;
+}
+//----------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_network
+/// @brief Check given name or ip address corresponds to localhost.
+///
+/// @param[in] hostname Hostname to check
+/// @return Return true if given name or ip address corresponds to localhost
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Network.h>
+/// ...
+/// if (kodi::network::IsLocalHost("127.0.0.1"))
+/// fprintf(stderr, "Is localhost\n");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL IsLocalHost(const std::string& hostname)
+{
+ using namespace ::kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_network->is_local_host(
+ CPrivateBase::m_interface->toKodi->kodiBase, hostname.c_str());
+}
+//----------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_network
+/// @brief Checks whether the specified path refers to a local network.
+///
+/// @param[in] hostname Hostname to check
+/// @param[in] offLineCheck Check if in private range, see https://en.wikipedia.org/wiki/Private_network
+/// @return True if host is on a LAN, false otherwise
+///
+inline bool ATTR_DLL_LOCAL IsHostOnLAN(const std::string& hostname, bool offLineCheck = false)
+{
+ using namespace kodi::addon;
+
+ return CPrivateBase::m_interface->toKodi->kodi_network->is_host_on_lan(
+ CPrivateBase::m_interface->toKodi->kodiBase, hostname.c_str(), offLineCheck);
+}
+//------------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_network
+/// @brief URL encodes the given string
+///
+/// This function converts the given input string to a URL encoded string and
+/// returns that as a new allocated string. All input characters that are
+/// not a-z, A-Z, 0-9, '-', '.', '_' or '~' are converted to their "URL escaped"
+/// version (%NN where NN is a two-digit hexadecimal number).
+///
+/// @param[in] url The code of the message to get.
+/// @return Encoded URL string
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Network.h>
+/// ...
+/// std::string encodedUrl = kodi::network::URLEncode("François");
+/// fprintf(stderr, "Encoded URL is '%s'\n", encodedUrl.c_str());
+/// ...
+/// ~~~~~~~~~~~~~
+/// For example, the string: François ,would be encoded as: Fran%C3%A7ois
+///
+inline std::string ATTR_DLL_LOCAL URLEncode(const std::string& url)
+{
+ using namespace ::kodi::addon;
+
+ std::string retString;
+ char* string = CPrivateBase::m_interface->toKodi->kodi_network->url_encode(
+ CPrivateBase::m_interface->toKodi->kodiBase, url.c_str());
+ if (string != nullptr)
+ {
+ retString = string;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ string);
+ }
+ return retString;
+}
+//----------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_network
+/// @brief Lookup URL in DNS cache
+///
+/// This test will get DNS record for a domain. The DNS lookup is done directly
+/// against the domain's authoritative name server, so changes to DNS Records
+/// should show up instantly. By default, the DNS lookup tool will return an
+/// IP address if you give it a name (e.g. www.example.com)
+///
+/// @param[in] hostName The code of the message to get.
+/// @param[out] ipAddress Returned address
+/// @return true if successful
+///
+///
+/// ------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/Network.h>
+/// ...
+/// std::string ipAddress;
+/// bool ret = kodi::network::DNSLookup("www.google.com", ipAddress);
+/// fprintf(stderr, "DNSLookup returned for www.google.com the IP '%s', call was %s\n", ipAddress.c_str(), ret ? "ok" : "failed");
+/// ...
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL DNSLookup(const std::string& hostName, std::string& ipAddress)
+{
+ using namespace ::kodi::addon;
+
+ bool ret = false;
+ char* string = CPrivateBase::m_interface->toKodi->kodi_network->dns_lookup(
+ CPrivateBase::m_interface->toKodi->kodiBase, hostName.c_str(), &ret);
+ if (string != nullptr)
+ {
+ ipAddress = string;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ string);
+ }
+ return ret;
+}
+//----------------------------------------------------------------------------
+
+} /* namespace network */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/AudioDecoder.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/AudioDecoder.h
new file mode 100644
index 0000000..8569bc3
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/AudioDecoder.h
@@ -0,0 +1,728 @@
+/*
+ * 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 "../AddonBase.h"
+#include "../AudioEngine.h"
+#include "../c-api/addon-instance/audiodecoder.h"
+
+#ifdef __cplusplus
+
+#include <cstring>
+#include <stdexcept>
+
+namespace kodi
+{
+namespace addon
+{
+
+class CInstanceAudioDecoder;
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_audiodecoder_Defs_AudioDecoderInfoTag class AudioDecoderInfoTag
+/// @ingroup cpp_kodi_addon_audiodecoder_Defs
+/// @brief **Info tag data structure**\n
+/// Representation of available information of processed audio file.
+///
+/// This is used to store all the necessary data of audio stream and to have on
+/// e.g. GUI for information.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_audiodecoder_Defs_AudioDecoderInfoTag_Help
+///
+///@{
+class ATTR_DLL_LOCAL AudioDecoderInfoTag
+{
+public:
+ /*! \cond PRIVATE */
+ AudioDecoderInfoTag() = default;
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_audiodecoder_Defs_AudioDecoderInfoTag_Help Value Help
+ /// @ingroup cpp_kodi_addon_audiodecoder_Defs_AudioDecoderInfoTag
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_audiodecoder_Defs_AudioDecoderInfoTag :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Title** | `std::string` | @ref AudioDecoderInfoTag::SetTitle "SetTitle" | @ref AudioDecoderInfoTag::GetTitle "GetTitle"
+ /// | **Artist** | `std::string` | @ref AudioDecoderInfoTag::SetArtist "SetArtist" | @ref AudioDecoderInfoTag::GetArtist "GetArtist"
+ /// | **Album** | `std::string` | @ref AudioDecoderInfoTag::SetAlbum "SetAlbum" | @ref AudioDecoderInfoTag::GetAlbum "GetAlbum"
+ /// | **Album artist** | `std::string` | @ref AudioDecoderInfoTag::SetAlbumArtist "SetAlbumArtist" | @ref AudioDecoderInfoTag::GetAlbumArtist "GetAlbumArtist"
+ /// | **Media type** | `std::string` | @ref AudioDecoderInfoTag::SetMediaType "SetMediaType" | @ref AudioDecoderInfoTag::GetMediaType "GetMediaType"
+ /// | **Genre** | `std::string` | @ref AudioDecoderInfoTag::SetGenre "SetGenre" | @ref AudioDecoderInfoTag::GetGenre "GetGenre"
+ /// | **Duration** | `int` | @ref AudioDecoderInfoTag::SetDuration "SetDuration" | @ref AudioDecoderInfoTag::GetDuration "GetDuration"
+ /// | **Track number** | `int` | @ref AudioDecoderInfoTag::SetTrack "SetTrack" | @ref AudioDecoderInfoTag::GetTrack "GetTrack"
+ /// | **Disc number** | `int` | @ref AudioDecoderInfoTag::SetDisc "SetDisc" | @ref AudioDecoderInfoTag::GetDisc "GetDisc"
+ /// | **Disc subtitle name** | `std::string` | @ref AudioDecoderInfoTag::SetDiscSubtitle "SetDiscSubtitle" | @ref AudioDecoderInfoTag::GetDiscSubtitle "GetDiscSubtitle"
+ /// | **Disc total amount** | `int` | @ref AudioDecoderInfoTag::SetDiscTotal "SetDiscTotal" | @ref AudioDecoderInfoTag::GetDiscTotal "GetDiscTotal"
+ /// | **Release date** | `std::string` | @ref AudioDecoderInfoTag::SetReleaseDate "SetReleaseDate" | @ref AudioDecoderInfoTag::GetReleaseDate "GetReleaseDate"
+ /// | **Lyrics** | `std::string` | @ref AudioDecoderInfoTag::SetLyrics "SetLyrics" | @ref AudioDecoderInfoTag::GetLyrics "GetLyrics"
+ /// | **Samplerate** | `int` | @ref AudioDecoderInfoTag::SetSamplerate "SetSamplerate" | @ref AudioDecoderInfoTag::GetSamplerate "GetSamplerate"
+ /// | **Channels amount** | `int` | @ref AudioDecoderInfoTag::SetChannels "SetChannels" | @ref AudioDecoderInfoTag::GetChannels "GetChannels"
+ /// | **Bitrate** | `int` | @ref AudioDecoderInfoTag::SetBitrate "SetBitrate" | @ref AudioDecoderInfoTag::GetBitrate "GetBitrate"
+ /// | **Comment text** | `std::string` | @ref AudioDecoderInfoTag::SetComment "SetComment" | @ref AudioDecoderInfoTag::GetComment "GetComment"
+ /// | **Cover art by path** | `std::string` | @ref AudioDecoderInfoTag::SetCoverArtByPath "SetCoverArtByPath" | @ref AudioDecoderInfoTag::GetCoverArtByPath "GetCoverArtByPath"
+ /// | **Cover art by memory** | `std::string` | @ref AudioDecoderInfoTag::SetCoverArtByMem "SetCoverArtByMem" | @ref AudioDecoderInfoTag::GetCoverArtByMem "GetCoverArtByMem"
+ ///
+
+ /// @addtogroup cpp_kodi_addon_audiodecoder_Defs_AudioDecoderInfoTag
+ ///@{
+
+ /// @brief Set the title from music as string on info tag.
+ void SetTitle(const std::string& title) { m_title = title; }
+
+ /// @brief Get title name
+ std::string GetTitle() const { return m_title; }
+
+ /// @brief Set artist name
+ void SetArtist(const std::string& artist) { m_artist = artist; }
+
+ /// @brief Get artist name
+ std::string GetArtist() const { return m_artist; }
+
+ /// @brief Set album name
+ void SetAlbum(const std::string& album) { m_album = album; }
+
+ /// @brief Set album name
+ std::string GetAlbum() const { return m_album; }
+
+ /// @brief Set album artist name
+ void SetAlbumArtist(const std::string& albumArtist) { m_album_artist = albumArtist; }
+
+ /// @brief Get album artist name
+ std::string GetAlbumArtist() const { return m_album_artist; }
+
+ /// @brief Set the media type of the music item.
+ ///
+ /// Available strings about media type for music:
+ /// | String | Description |
+ /// |---------------:|:--------------------------------------------------|
+ /// | artist | If it is defined as an artist
+ /// | album | If it is defined as an album
+ /// | music | If it is defined as an music
+ /// | song | If it is defined as a song
+ ///
+ void SetMediaType(const std::string& mediaType) { m_media_type = mediaType; }
+
+ /// @brief Get the media type of the music item.
+ std::string GetMediaType() const { return m_media_type; }
+
+ /// @brief Set genre name from music as string if present.
+ void SetGenre(const std::string& genre) { m_genre = genre; }
+
+ /// @brief Get genre name from music as string if present.
+ std::string GetGenre() const { return m_genre; }
+
+ /// @brief Set the duration of music as integer from info.
+ void SetDuration(int duration) { m_duration = duration; }
+
+ /// @brief Get the duration of music as integer from info.
+ int GetDuration() const { return m_duration; }
+
+ /// @brief Set track number (if present) from music info as integer.
+ void SetTrack(int track) { m_track = track; }
+
+ /// @brief Get track number (if present).
+ int GetTrack() const { return m_track; }
+
+ /// @brief Set disk number (if present) from music info as integer.
+ void SetDisc(int disc) { m_disc = disc; }
+
+ /// @brief Get disk number (if present)
+ int GetDisc() const { return m_disc; }
+
+ /// @brief Set disk subtitle name (if present) from music info.
+ void SetDiscSubtitle(const std::string& discSubtitle) { m_disc_subtitle = discSubtitle; }
+
+ /// @brief Get disk subtitle name (if present) from music info.
+ std::string GetDiscSubtitle() const { return m_disc_subtitle; }
+
+ /// @brief Set disks amount quantity (if present) from music info as integer.
+ void SetDiscTotal(int discTotal) { m_disc_total = discTotal; }
+
+ /// @brief Get disks amount quantity (if present)
+ int GetDiscTotal() const { return m_disc_total; }
+
+ /// @brief Set release date as string from music info (if present).\n
+ /// [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) date YYYY, YYYY-MM or YYYY-MM-DD
+ void SetReleaseDate(const std::string& releaseDate) { m_release_date = releaseDate; }
+
+ /// @brief Get release date as string from music info (if present).
+ std::string GetReleaseDate() const { return m_release_date; }
+
+ /// @brief Set string from lyrics.
+ void SetLyrics(const std::string& lyrics) { m_lyrics = lyrics; }
+
+ /// @brief Get string from lyrics.
+ std::string GetLyrics() const { return m_lyrics; }
+
+ /// @brief Set related stream samplerate.
+ void SetSamplerate(int samplerate) { m_samplerate = samplerate; }
+
+ /// @brief Get related stream samplerate.
+ int GetSamplerate() const { return m_samplerate; }
+
+ /// @brief Set related stream channels amount.
+ void SetChannels(int channels) { m_channels = channels; }
+
+ /// @brief Get related stream channels amount.
+ int GetChannels() const { return m_channels; }
+
+ /// @brief Set related stream bitrate.
+ void SetBitrate(int bitrate) { m_bitrate = bitrate; }
+
+ /// @brief Get related stream bitrate.
+ int GetBitrate() const { return m_bitrate; }
+
+ /// @brief Set additional information comment (if present).
+ void SetComment(const std::string& comment) { m_comment = comment; }
+
+ /// @brief Get additional information comment (if present).
+ std::string GetComment() const { return m_comment; }
+
+ /// @brief Set cover art image by path.
+ ///
+ /// @param[in] path Image position path
+ ///
+ /// @note Cannot be combined with @ref SetCoverArtByMem and @ref GetCoverArtByMem.
+ void SetCoverArtByPath(const std::string& path) { m_cover_art_path = path; }
+
+ /// @brief Get cover art image path.
+ ///
+ /// @return Image position path
+ ///
+ /// @note Only be available if set before by @ref SetCoverArtByPath.
+ /// Cannot be combined with @ref SetCoverArtByMem and @ref GetCoverArtByMem.
+ ///
+ std::string GetCoverArtByPath() const { return m_cover_art_path; }
+
+ /// @brief Set cover art image by memory.
+ ///
+ /// @param[in] data Image data
+ /// @param[in] size Image data size
+ /// @param[in] mimetype Image format mimetype
+ /// Possible mimetypes:
+ /// - "image/jpeg"
+ /// - "image/png"
+ /// - "image/gif"
+ /// - "image/bmp"
+ ///
+ void SetCoverArtByMem(const uint8_t* data, size_t size, const std::string& mimetype)
+ {
+ if (size > 0)
+ {
+ m_cover_art_mem_mimetype = mimetype;
+ m_cover_art_mem.resize(size);
+ m_cover_art_mem.assign(data, data + size);
+ }
+ }
+
+ /// @brief Get cover art data by memory.
+ ///
+ /// @param[out] size Stored size about art image
+ /// @param[in] mimetype Related image mimetype to stored data
+ /// @return Image data
+ ///
+ /// @note This only works if @ref SetCoverArtByMem was used before
+ ///
+ /// @warning This function is not thread safe and related data should never be
+ /// changed by @ref SetCoverArtByMem, if data from here is used without copy!
+ const uint8_t* GetCoverArtByMem(size_t& size, std::string& mimetype) const
+ {
+ if (!m_cover_art_mem.empty())
+ {
+ mimetype = m_cover_art_mem_mimetype;
+ size = m_cover_art_mem.size();
+ return m_cover_art_mem.data();
+ }
+ else
+ {
+ size = 0;
+ return nullptr;
+ }
+ }
+
+ ///@}
+
+private:
+ std::string m_title;
+ std::string m_artist;
+ std::string m_album;
+ std::string m_album_artist;
+ std::string m_media_type;
+ std::string m_genre;
+ int m_duration{0};
+ int m_track{0};
+ int m_disc{0};
+ std::string m_disc_subtitle;
+ int m_disc_total{0};
+ std::string m_release_date;
+ std::string m_lyrics;
+ int m_samplerate{0};
+ int m_channels{0};
+ int m_bitrate{0};
+ std::string m_comment;
+ std::string m_cover_art_path;
+ std::string m_cover_art_mem_mimetype;
+ std::vector<uint8_t> m_cover_art_mem;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_audiodecoder_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_addon_audiodecoder
+/// @brief **Audio decoder add-on instance definition values**\n
+/// All audio decoder functions associated data structures.
+///
+/// Used to exchange the available options between Kodi and addon.\n
+/// The groups described here correspond to the groups of functions on audio
+/// decoder instance class.
+///
+
+//==============================================================================
+///
+/// @addtogroup cpp_kodi_addon_audiodecoder
+/// @brief \cpp_class{ kodi::addon::CInstanceAudioDecoder }
+/// **Audio decoder add-on instance**
+///
+/// For audio decoders as binary add-ons. This class implements a way to handle
+/// special types of audio files.
+///
+/// The add-on handles loading of the source file and outputting the audio stream
+/// for consumption by the player.
+///
+/// The addon.xml defines the capabilities of this add-on.
+///
+/// @note The option to have multiple instances is possible with audio-decoder
+/// add-ons. This is useful, since some playback engines are riddled by global
+/// variables, making decoding of multiple streams using the same instance
+/// impossible.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Here's an example on addon.xml:**
+/// ~~~~~~~~~~~~~{.xml}
+/// <?xml version="1.0" encoding="UTF-8"?>
+/// <addon
+/// id="audiodecoder.myspecialnamefor"
+/// version="1.0.0"
+/// name="My special audio decoder addon"
+/// provider-name="Your Name">
+/// <requires>@ADDON_DEPENDS@</requires>
+/// <extension
+/// point="kodi.audiodecoder"
+/// name="2sf"
+/// tags="true"
+/// library_@PLATFORM@="@LIBRARY_FILENAME@">
+/// <support>
+/// <extension name=".2sf">
+/// <description>30100</description>
+/// <icon>resources/file_format_music_sound.png</icon>
+/// </extension>
+/// <extension name=".mini2sf"/>
+/// </support>
+/// </extension>
+/// <extension point="xbmc.addon.metadata">
+/// <summary lang="en_GB">My audio decoder addon addon</summary>
+/// <description lang="en_GB">My audio decoder addon description</description>
+/// <platform>@PLATFORM@</platform>
+/// </extension>
+/// </addon>
+/// ~~~~~~~~~~~~~
+///
+/// Description to audio decoder related addon.xml values:
+/// | Name | Description
+/// |:------------------------------------------------------|----------------------------------------
+/// | <b>`point`</b> | Addon type specification<br>At all addon types and for this kind always <b>"kodi.audiodecoder"</b>.
+/// | <b>`library_@PLATFORM@`</b> | Sets the used library name, which is automatically set by cmake at addon build.
+/// | <b>`name`</b> | The name of the decoder used in Kodi for display.
+/// | <b>`tags`</b> | Boolean to point out that addon can bring own information to replayed file, if <b>`false`</b> only the file name is used as info.<br>If <b>`true`</b>, @ref CInstanceAudioDecoder::ReadTag is used and must be implemented.
+/// | <b>`tracks`</b> | Boolean to in inform one file can contains several different streams.
+/// | <b>`<support><extension name="..." /></support>`</b> | The file extensions / styles supported by this addon.\nOptional can be with `<description>` and `<icon>`additional info added where used for list views in Kodi.
+/// | <b>`<support><mimetype name="..." /></support>`</b> | A stream URL mimetype where can be used to force to this addon.\nOptional can be with ``<description>` and `<icon>`additional info added where used for list views in Kodi.
+///
+/// --------------------------------------------------------------------------
+///
+/// **Here is a code example how this addon is used:**
+///
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/AudioDecoder.h>
+///
+/// class CMyAudioDecoder : public kodi::addon::CInstanceAudioDecoder
+/// {
+/// public:
+/// CMyAudioDecoder(const kodi::addon::IInstanceInfo& instance);
+///
+/// bool Init(const std::string& filename, unsigned int filecache,
+/// int& channels, int& samplerate,
+/// int& bitspersample, int64_t& totaltime,
+/// int& bitrate, AudioEngineDataFormat& format,
+/// std::vector<AudioEngineChannel>& channellist) override;
+/// int ReadPCM(uint8_t* buffer, int size, int& actualsize) override;
+/// };
+///
+/// CMyAudioDecoder::CMyAudioDecoder(const kodi::addon::IInstanceInfo& instance)
+/// : kodi::addon::CInstanceAudioDecoder(instance)
+/// {
+/// ...
+/// }
+///
+/// bool CMyAudioDecoder::Init(const std::string& filename, unsigned int filecache,
+/// int& channels, int& samplerate,
+/// int& bitspersample, int64_t& totaltime,
+/// int& bitrate, AudioEngineDataFormat& format,
+/// std::vector<AudioEngineChannel>& channellist)
+/// {
+/// ...
+/// return true;
+/// }
+///
+/// int CMyAudioDecoder::ReadPCM(uint8_t* buffer, int size, int& actualsize)
+/// {
+/// ...
+/// return AUDIODECODER_READ_SUCCESS;
+/// }
+///
+///
+/// //----------------------------------------------------------------------
+///
+/// class CMyAddon : public kodi::addon::CAddonBase
+/// {
+/// public:
+/// CMyAddon() = default;
+/// ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl) override;
+/// };
+///
+/// // If you use only one instance in your add-on, can be instanceType and
+/// // instanceID ignored
+/// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl)
+/// {
+/// if (instance.IsType(ADDON_INSTANCE_AUDIODECODER))
+/// {
+/// kodi::Log(ADDON_LOG_INFO, "Creating my audio decoder");
+/// hdl = new CMyAudioDecoder(instance);
+/// return ADDON_STATUS_OK;
+/// }
+/// else if (...)
+/// {
+/// ...
+/// }
+/// return ADDON_STATUS_UNKNOWN;
+/// }
+///
+/// ADDONCREATOR(CMyAddon)
+/// ~~~~~~~~~~~~~
+///
+/// The destruction of the example class `CMyAudioDecoder` is called from
+/// Kodi's header. Manually deleting the add-on instance is not required.
+///
+class ATTR_DLL_LOCAL CInstanceAudioDecoder : public IAddonInstance
+{
+public:
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_audiodecoder
+ /// @brief Audio decoder class constructor used to support multiple instance
+ /// types.
+ ///
+ /// @param[in] instance The instance value given to
+ /// <b>`kodi::addon::CAddonBase::CreateInstance(...)`</b>.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Here's example about the use of this:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// class CMyAudioDecoder : public kodi::addon::CInstanceAudioDecoder
+ /// {
+ /// public:
+ /// CMyAudioDecoder(KODI_HANDLE instance)
+ /// : kodi::addon::CInstanceAudioDecoder(instance)
+ /// {
+ /// ...
+ /// }
+ ///
+ /// ...
+ /// };
+ ///
+ /// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+ /// KODI_ADDON_INSTANCE_HDL& hdl)
+ /// {
+ /// kodi::Log(ADDON_LOG_INFO, "Creating my audio decoder");
+ /// hdl = new CMyAudioDecoder(instance);
+ /// return ADDON_STATUS_OK;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ explicit CInstanceAudioDecoder(const kodi::addon::IInstanceInfo& instance)
+ : IAddonInstance(instance)
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstanceAudioDecoder: Creation of multiple together with single instance way is not allowed!");
+
+ SetAddonStruct(instance);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_audiodecoder
+ /// @brief Checks addon support given file path.
+ ///
+ /// @param[in] filename The file to read
+ /// @return true if successfully done and supported, otherwise false
+ ///
+ /// @note Optional to add, as default becomes `true` used.
+ ///
+ virtual bool SupportsFile(const std::string& filename) { return true; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_audiodecoder
+ /// @brief Initialize a decoder.
+ ///
+ /// @param[in] filename The file to read
+ /// @param[in] filecache The file cache size
+ /// @param[out] channels Number of channels in output stream
+ /// @param[out] samplerate Samplerate of output stream
+ /// @param[out] bitspersample Bits per sample in output stream
+ /// @param[out] totaltime Total time for stream
+ /// @param[out] bitrate Average bitrate of input stream
+ /// @param[out] format Data format for output stream, see
+ /// @ref cpp_kodi_audioengine_Defs_AudioEngineFormat for
+ /// available values
+ /// @param[out] channellist Channel mapping for output stream, see
+ /// @ref cpp_kodi_audioengine_Defs_AudioEngineChannel
+ /// for available values
+ /// @return true if successfully done, otherwise false
+ ///
+ virtual bool Init(const std::string& filename,
+ unsigned int filecache,
+ int& channels,
+ int& samplerate,
+ int& bitspersample,
+ int64_t& totaltime,
+ int& bitrate,
+ AudioEngineDataFormat& format,
+ std::vector<AudioEngineChannel>& channellist) = 0;
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_audiodecoder
+ /// @brief Produce some noise.
+ ///
+ /// @param[in] buffer Output buffer
+ /// @param[in] size Size of output buffer
+ /// @param[out] actualsize Actual number of bytes written to output buffer
+ /// @return @copydetails cpp_kodi_addon_audiodecoder_Defs_AUDIODECODER_READ_RETURN
+ ///
+ virtual int ReadPCM(uint8_t* buffer, size_t size, size_t& actualsize) = 0;
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_audiodecoder
+ /// @brief Seek in output stream.
+ ///
+ /// @param[in] time Time position to seek to in milliseconds
+ /// @return Time position seek ended up on
+ ///
+ virtual int64_t Seek(int64_t time) { return time; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_audiodecoder
+ /// @brief Read tag of a file.
+ ///
+ /// @param[in] file File to read tag for
+ /// @param[out] tag Information tag about
+ /// @return True on success, false on failure
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_audiodecoder_Defs_AudioDecoderInfoTag_Help
+ ///
+ virtual bool ReadTag(const std::string& file, kodi::addon::AudioDecoderInfoTag& tag)
+ {
+ return false;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_audiodecoder
+ /// @brief Get number of tracks in a file.
+ ///
+ /// @param[in] file File to read tag for
+ /// @return Number of tracks in file
+ ///
+ virtual int TrackCount(const std::string& file) { return 1; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_audiodecoder
+ /// @brief Static auxiliary function to read the track number used from the
+ /// given path.
+ ///
+ /// If track number is not found in file name, the originally given file
+ /// name is returned, track number then remains at "0".
+ ///
+ /// @param[in] name The value specified in addon.xml extension under `name="???"`
+ /// @param[in] trackPath The full path to evaluate
+ /// @param[out] track The track number read out in the path, 0 if not
+ /// identified as a track path.
+ /// @return Path to the associated file
+ ///
+ inline static std::string GetTrack(const std::string& name,
+ const std::string& trackPath,
+ int& track)
+ {
+ /*
+ * get the track name from path
+ */
+ track = 0;
+ std::string toLoad(trackPath);
+ const std::string ext = "." + name + KODI_ADDON_AUDIODECODER_TRACK_EXT;
+ if (toLoad.find(ext) != std::string::npos)
+ {
+ size_t iStart = toLoad.rfind('-') + 1;
+ track = atoi(toLoad.substr(iStart, toLoad.size() - iStart - ext.size()).c_str());
+ // The directory we are in, is the file
+ // that contains the bitstream to play,
+ // so extract it
+ size_t slash = trackPath.rfind('\\');
+ if (slash == std::string::npos)
+ slash = trackPath.rfind('/');
+ toLoad = trackPath.substr(0, slash);
+ }
+
+ return toLoad;
+ }
+ //--------------------------------------------------------------------------
+
+private:
+ void SetAddonStruct(KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ instance->hdl = this;
+ instance->audiodecoder->toAddon->supports_file = ADDON_supports_file;
+ instance->audiodecoder->toAddon->init = ADDON_init;
+ instance->audiodecoder->toAddon->read_pcm = ADDON_read_pcm;
+ instance->audiodecoder->toAddon->seek = ADDON_seek;
+ instance->audiodecoder->toAddon->read_tag = ADDON_read_tag;
+ instance->audiodecoder->toAddon->track_count = ADDON_track_count;
+ }
+
+ inline static bool ADDON_supports_file(const KODI_ADDON_AUDIODECODER_HDL hdl, const char* file)
+ {
+ return static_cast<CInstanceAudioDecoder*>(hdl)->SupportsFile(file);
+ }
+
+ inline static bool ADDON_init(const KODI_ADDON_AUDIODECODER_HDL hdl,
+ const char* file,
+ unsigned int filecache,
+ int* channels,
+ int* samplerate,
+ int* bitspersample,
+ int64_t* totaltime,
+ int* bitrate,
+ AudioEngineDataFormat* format,
+ enum AudioEngineChannel info[AUDIOENGINE_CH_MAX])
+ {
+ CInstanceAudioDecoder* thisClass = static_cast<CInstanceAudioDecoder*>(hdl);
+
+ std::vector<AudioEngineChannel> channelList;
+
+ bool ret = thisClass->Init(file, filecache, *channels, *samplerate, *bitspersample, *totaltime,
+ *bitrate, *format, channelList);
+ if (!channelList.empty())
+ {
+ if (channelList.back() != AUDIOENGINE_CH_NULL)
+ channelList.push_back(AUDIOENGINE_CH_NULL);
+
+ for (unsigned int i = 0; i < channelList.size(); ++i)
+ {
+ info[i] = channelList[i];
+ }
+ }
+ return ret;
+ }
+
+ inline static int ADDON_read_pcm(const KODI_ADDON_AUDIODECODER_HDL hdl,
+ uint8_t* buffer,
+ size_t size,
+ size_t* actualsize)
+ {
+ return static_cast<CInstanceAudioDecoder*>(hdl)->ReadPCM(buffer, size, *actualsize);
+ }
+
+ inline static int64_t ADDON_seek(const KODI_ADDON_AUDIODECODER_HDL hdl, int64_t time)
+ {
+ return static_cast<CInstanceAudioDecoder*>(hdl)->Seek(time);
+ }
+
+ inline static bool ADDON_read_tag(const KODI_ADDON_AUDIODECODER_HDL hdl,
+ const char* file,
+ struct KODI_ADDON_AUDIODECODER_INFO_TAG* tag)
+ {
+#ifdef _WIN32
+#pragma warning(push)
+#pragma warning(disable : 4996)
+#endif // _WIN32
+ kodi::addon::AudioDecoderInfoTag cppTag;
+ bool ret = static_cast<CInstanceAudioDecoder*>(hdl)->ReadTag(file, cppTag);
+ if (ret)
+ {
+ tag->title = strdup(cppTag.GetTitle().c_str());
+ tag->artist = strdup(cppTag.GetArtist().c_str());
+ tag->album = strdup(cppTag.GetAlbum().c_str());
+ tag->album_artist = strdup(cppTag.GetAlbumArtist().c_str());
+ tag->media_type = strdup(cppTag.GetMediaType().c_str());
+ tag->genre = strdup(cppTag.GetGenre().c_str());
+ tag->duration = cppTag.GetDuration();
+ tag->track = cppTag.GetTrack();
+ tag->disc = cppTag.GetDisc();
+ tag->disc_subtitle = strdup(cppTag.GetDiscSubtitle().c_str());
+ tag->disc_total = cppTag.GetDiscTotal();
+ tag->release_date = strdup(cppTag.GetReleaseDate().c_str());
+ tag->lyrics = strdup(cppTag.GetLyrics().c_str());
+ tag->samplerate = cppTag.GetSamplerate();
+ tag->channels = cppTag.GetChannels();
+ tag->bitrate = cppTag.GetBitrate();
+ tag->comment = strdup(cppTag.GetComment().c_str());
+ std::string mimetype;
+ size_t size = 0;
+ const uint8_t* mem = cppTag.GetCoverArtByMem(size, mimetype);
+ if (mem && size > 0)
+ {
+ tag->cover_art_mem_mimetype = strdup(mimetype.c_str());
+ tag->cover_art_mem_size = size;
+ tag->cover_art_mem = static_cast<uint8_t*>(malloc(size));
+ memcpy(tag->cover_art_mem, mem, size);
+ }
+ else
+ {
+ tag->cover_art_path = strdup(cppTag.GetCoverArtByPath().c_str());
+ }
+ }
+ return ret;
+#ifdef _WIN32
+#pragma warning(pop)
+#endif // _WIN32
+ }
+
+ inline static int ADDON_track_count(const KODI_ADDON_AUDIODECODER_HDL hdl, const char* file)
+ {
+ return static_cast<CInstanceAudioDecoder*>(hdl)->TrackCount(file);
+ }
+};
+
+} /* namespace addon */
+} /* namespace kodi */
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/AudioEncoder.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/AudioEncoder.h
new file mode 100644
index 0000000..c79a472
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/AudioEncoder.h
@@ -0,0 +1,514 @@
+/*
+ * 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 "../AddonBase.h"
+#include "../c-api/addon-instance/audioencoder.h"
+
+#ifdef __cplusplus
+
+#include <stdexcept>
+
+namespace kodi
+{
+namespace addon
+{
+
+class CInstanceAudioEncoder;
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_audioencoder_Defs_AudioEncoderInfoTag class AudioEncoderInfoTag
+/// @ingroup cpp_kodi_addon_audioencoder_Defs
+/// @brief **Info tag data structure**\n
+/// Representation of available information of processed audio file.
+///
+/// This is used to get all the necessary data of audio stream and to have on
+/// created files by encoders.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_audioencoder_Defs_AudioEncoderInfoTag_Help
+///
+///@{
+class ATTR_DLL_LOCAL AudioEncoderInfoTag
+{
+public:
+ /*! \cond PRIVATE */
+ AudioEncoderInfoTag() = default;
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_audioencoder_Defs_AudioEncoderInfoTag_Help Value Help
+ /// @ingroup cpp_kodi_addon_audioencoder_Defs_AudioEncoderInfoTag
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_audioencoder_Defs_AudioEncoderInfoTag :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Title** | `std::string` | @ref AudioEncoderInfoTag::SetTitle "SetTitle" | @ref AudioEncoderInfoTag::GetTitle "GetTitle"
+ /// | **Artist** | `std::string` | @ref AudioEncoderInfoTag::SetArtist "SetArtist" | @ref AudioEncoderInfoTag::GetArtist "GetArtist"
+ /// | **Album** | `std::string` | @ref AudioEncoderInfoTag::SetAlbum "SetAlbum" | @ref AudioEncoderInfoTag::GetAlbum "GetAlbum"
+ /// | **Album artist** | `std::string` | @ref AudioEncoderInfoTag::SetAlbumArtist "SetAlbumArtist" | @ref AudioEncoderInfoTag::GetAlbumArtist "GetAlbumArtist"
+ /// | **Media type** | `std::string` | @ref AudioEncoderInfoTag::SetMediaType "SetMediaType" | @ref AudioEncoderInfoTag::GetMediaType "GetMediaType"
+ /// | **Genre** | `std::string` | @ref AudioEncoderInfoTag::SetGenre "SetGenre" | @ref AudioEncoderInfoTag::GetGenre "GetGenre"
+ /// | **Duration** | `int` | @ref AudioEncoderInfoTag::SetDuration "SetDuration" | @ref AudioEncoderInfoTag::GetDuration "GetDuration"
+ /// | **Track number** | `int` | @ref AudioEncoderInfoTag::SetTrack "SetTrack" | @ref AudioEncoderInfoTag::GetTrack "GetTrack"
+ /// | **Disc number** | `int` | @ref AudioEncoderInfoTag::SetDisc "SetDisc" | @ref AudioEncoderInfoTag::GetDisc "GetDisc"
+ /// | **Disc subtitle name** | `std::string` | @ref AudioEncoderInfoTag::SetDiscSubtitle "SetDiscSubtitle" | @ref AudioEncoderInfoTag::GetDiscSubtitle "GetDiscSubtitle"
+ /// | **Disc total amount** | `int` | @ref AudioEncoderInfoTag::SetDiscTotal "SetDiscTotal" | @ref AudioEncoderInfoTag::GetDiscTotal "GetDiscTotal"
+ /// | **Release date** | `std::string` | @ref AudioEncoderInfoTag::SetReleaseDate "SetReleaseDate" | @ref AudioEncoderInfoTag::GetReleaseDate "GetReleaseDate"
+ /// | **Lyrics** | `std::string` | @ref AudioEncoderInfoTag::SetLyrics "SetLyrics" | @ref AudioEncoderInfoTag::GetLyrics "GetLyrics"
+ /// | **Samplerate** | `int` | @ref AudioEncoderInfoTag::SetSamplerate "SetSamplerate" | @ref AudioEncoderInfoTag::GetSamplerate "GetSamplerate"
+ /// | **Channels amount** | `int` | @ref AudioEncoderInfoTag::SetChannels "SetChannels" | @ref AudioEncoderInfoTag::GetChannels "GetChannels"
+ /// | **Bits per sample** | `int` | @ref AudioEncoderInfoTag::SetBitsPerSample "SetBitsPerSample" | @ref AudioEncoderInfoTag::GetBitsPerSample "GetBitsPerSample"
+ /// | **Track length** | `int` | @ref AudioEncoderInfoTag::SetTrackLength "SetTrackLength" | @ref AudioEncoderInfoTag::GetTrackLength "GetTrackLength"
+ /// | **Comment text** | `std::string` | @ref AudioEncoderInfoTag::SetComment "SetComment" | @ref AudioEncoderInfoTag::GetComment "GetComment"
+ ///
+ /// @addtogroup cpp_kodi_addon_audioencoder_Defs_AudioEncoderInfoTag
+ ///@{
+
+ /// @brief Set the title from music as string on info tag.
+ void SetTitle(const std::string& title) { m_title = title; }
+
+ /// @brief Get title name
+ const std::string& GetTitle() const { return m_title; }
+
+ /// @brief Set artist name
+ void SetArtist(const std::string& artist) { m_artist = artist; }
+
+ /// @brief Get artist name
+ const std::string& GetArtist() const { return m_artist; }
+
+ /// @brief Set album name
+ void SetAlbum(const std::string& album) { m_album = album; }
+
+ /// @brief Get album name
+ const std::string& GetAlbum() const { return m_album; }
+
+ /// @brief Set album artist name
+ void SetAlbumArtist(const std::string& albumArtist) { m_album_artist = albumArtist; }
+
+ /// @brief Get album artist name
+ const std::string& GetAlbumArtist() const { return m_album_artist; }
+
+ /// @brief Set the media type of the music item.
+ ///
+ /// Available strings about media type for music:
+ /// | String | Description |
+ /// |---------------:|:--------------------------------------------------|
+ /// | artist | If it is defined as an artist
+ /// | album | If it is defined as an album
+ /// | music | If it is defined as an music
+ /// | song | If it is defined as a song
+ ///
+ void SetMediaType(const std::string& mediaType) { m_media_type = mediaType; }
+
+ /// @brief Get the media type of the music item.
+ const std::string& GetMediaType() const { return m_media_type; }
+
+ /// @brief Set genre name from music as string if present.
+ void SetGenre(const std::string& genre) { m_genre = genre; }
+
+ /// @brief Get genre name from music as string if present.
+ const std::string& GetGenre() const { return m_genre; }
+
+ /// @brief Set the duration of music as integer from info.
+ void SetDuration(int duration) { m_duration = duration; }
+
+ /// @brief Get the duration of music as integer from info.
+ int GetDuration() const { return m_duration; }
+
+ /// @brief Set track number (if present) from music info as integer.
+ void SetTrack(int track) { m_track = track; }
+
+ /// @brief Get track number (if present).
+ int GetTrack() const { return m_track; }
+
+ /// @brief Set disk number (if present) from music info as integer.
+ void SetDisc(int disc) { m_disc = disc; }
+
+ /// @brief Get disk number (if present)
+ int GetDisc() const { return m_disc; }
+
+ /// @brief Set disk subtitle name (if present) from music info.
+ void SetDiscSubtitle(const std::string& discSubtitle) { m_disc_subtitle = discSubtitle; }
+
+ /// @brief Get disk subtitle name (if present) from music info.
+ const std::string& GetDiscSubtitle() const { return m_disc_subtitle; }
+
+ /// @brief Set disks amount quantity (if present) from music info as integer.
+ void SetDiscTotal(int discTotal) { m_disc_total = discTotal; }
+
+ /// @brief Get disks amount quantity (if present)
+ int GetDiscTotal() const { return m_disc_total; }
+
+ /// @brief Set release date as string from music info (if present).\n
+ /// [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) date YYYY, YYYY-MM or YYYY-MM-DD
+ void SetReleaseDate(const std::string& releaseDate) { m_release_date = releaseDate; }
+
+ /// @brief Get release date as string from music info (if present).
+ const std::string& GetReleaseDate() const { return m_release_date; }
+
+ /// @brief Set string from lyrics.
+ void SetLyrics(const std::string& lyrics) { m_lyrics = lyrics; }
+
+ /// @brief Get string from lyrics.
+ const std::string& GetLyrics() const { return m_lyrics; }
+
+ /// @brief Set related stream samplerate.
+ void SetSamplerate(int samplerate) { m_samplerate = samplerate; }
+
+ /// @brief Get related stream samplerate.
+ int GetSamplerate() const { return m_samplerate; }
+
+ /// @brief Set related stream channels amount.
+ void SetChannels(int channels) { m_channels = channels; }
+
+ /// @brief Get related stream channels amount.
+ int GetChannels() const { return m_channels; }
+
+ /// @brief Set related stream bits per sample.
+ void SetBitsPerSample(int bits_per_sample) { m_bits_per_sample = bits_per_sample; }
+
+ /// @brief Get related stream bits per sample.
+ int GetBitsPerSample() const { return m_bits_per_sample; }
+
+ /// @brief Set related stream track length.
+ void SetTrackLength(int track_length) { m_track_length = track_length; }
+
+ /// @brief Get related stream track length.
+ int GetTrackLength() const { return m_track_length; }
+
+ /// @brief Set additional information comment (if present).
+ void SetComment(const std::string& comment) { m_comment = comment; }
+
+ /// @brief Get additional information comment (if present).
+ const std::string& GetComment() const { return m_comment; }
+
+ ///@}
+
+private:
+ friend class CInstanceAudioEncoder;
+
+ AudioEncoderInfoTag(const struct KODI_ADDON_AUDIOENCODER_INFO_TAG* tag)
+ {
+ if (tag->title)
+ m_title = tag->title;
+ if (tag->artist)
+ m_artist = tag->artist;
+ if (tag->album)
+ m_album = tag->album;
+ if (tag->album_artist)
+ m_album_artist = tag->album_artist;
+ if (tag->media_type)
+ m_media_type = tag->media_type;
+ if (tag->genre)
+ m_genre = tag->genre;
+ m_duration = tag->duration;
+ m_track = tag->track;
+ m_disc = tag->disc;
+ if (tag->artist)
+ m_disc_subtitle = tag->artist;
+ m_disc_total = tag->disc_total;
+ if (tag->release_date)
+ m_release_date = tag->release_date;
+ if (tag->lyrics)
+ m_lyrics = tag->lyrics;
+ m_samplerate = tag->samplerate;
+ m_channels = tag->channels;
+ m_bits_per_sample = tag->bits_per_sample;
+ m_track_length = tag->track_length;
+ if (tag->comment)
+ m_comment = tag->comment;
+ }
+
+ std::string m_title;
+ std::string m_artist;
+ std::string m_album;
+ std::string m_album_artist;
+ std::string m_media_type;
+ std::string m_genre;
+ int m_duration{0};
+ int m_track{0};
+ int m_disc{0};
+ std::string m_disc_subtitle;
+ int m_disc_total{0};
+ std::string m_release_date;
+ std::string m_lyrics;
+ int m_samplerate{0};
+ int m_channels{0};
+ int m_bits_per_sample{0};
+ int m_track_length{0};
+ std::string m_comment;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @addtogroup cpp_kodi_addon_audioencoder
+/// @brief \cpp_class{ kodi::addon::CInstanceAudioEncoder }
+/// **Audio encoder add-on instance.**\n
+/// For audio encoders as binary add-ons. This class implements a way to handle
+/// the encode of given stream to a new format.
+///
+/// The addon.xml defines the capabilities of this add-on.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Here's an example on addon.xml:**
+/// ~~~~~~~~~~~~~{.xml}
+/// <extension
+/// point="kodi.audioencoder"
+/// extension=".flac"
+/// library_@PLATFORM@="@LIBRARY_FILENAME@"/>
+/// ~~~~~~~~~~~~~
+///
+/// Description to audio encoder related addon.xml values:
+/// | Name | Description
+/// |:------------------------------|----------------------------------------
+/// | <b>`point`</b> | Addon type specification<br>At all addon types and for this kind always <b>"kodi.audioencoder"</b>.
+/// | <b>`library_@PLATFORM@`</b> | Sets the used library name, which is automatically set by cmake at addon build.
+/// | <b>`extension`</b> | The file extensions / styles supported by this addon.
+///
+/// --------------------------------------------------------------------------
+///
+/// --------------------------------------------------------------------------
+///
+/// **Here is a code example how this addon is used:**
+///
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/AudioEncoder.h>
+///
+/// class ATTR_DLL_LOCAL CMyAudioEncoder : public kodi::addon::CInstanceAudioEncoder
+/// {
+/// public:
+/// CMyAudioEncoder(const kodi::addon::IInstanceInfo& instance);
+///
+/// bool Start(const kodi::addon::AudioEncoderInfoTag& tag) override;
+/// int Encode(int numBytesRead, const uint8_t* pbtStream) override;
+/// bool Finish() override; // Optional
+/// };
+///
+/// CMyAudioEncoder::CMyAudioEncoder(const kodi::addon::IInstanceInfo& instance)
+/// : kodi::addon::CInstanceAudioEncoder(instance)
+/// {
+/// ...
+/// }
+///
+/// bool CMyAudioEncoder::Start(const kodi::addon::AudioEncoderInfoTag& tag)
+/// {
+/// ...
+/// return true;
+/// }
+///
+/// ssize_t CMyAudioEncoder::Encode(const uint8_t* pbtStream, size_t numBytesRead)
+/// {
+/// uint8_t* data = nullptr;
+/// size_t length = 0;
+/// ...
+/// kodi::addon::CInstanceAudioEncoder::Write(data, length);
+///
+/// return 0;
+/// }
+///
+///
+/// bool CMyAudioEncoder::Finish()
+/// {
+/// ...
+/// return true;
+/// }
+///
+/// //----------------------------------------------------------------------
+///
+/// class CMyAddon : public kodi::addon::CAddonBase
+/// {
+/// public:
+/// CMyAddon() = default;
+/// ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl) override;
+/// };
+///
+/// // If you use only one instance in your add-on, can be instanceType and
+/// // instanceID ignored
+/// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl)
+/// {
+/// if (instance.IsType(ADDON_INSTANCE_AUDIOENCODER))
+/// {
+/// kodi::Log(ADDON_LOG_INFO, "Creating my audio encoder instance");
+/// hdl = new CMyAudioEncoder(instance);
+/// return ADDON_STATUS_OK;
+/// }
+/// else if (...)
+/// {
+/// ...
+/// }
+/// return ADDON_STATUS_UNKNOWN;
+/// }
+///
+/// ADDONCREATOR(CMyAddon)
+/// ~~~~~~~~~~~~~
+///
+/// The destruction of the example class `CMyAudioEncoder` is called from
+/// Kodi's header. Manually deleting the add-on instance is not required.
+///
+class ATTR_DLL_LOCAL CInstanceAudioEncoder : public IAddonInstance
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_audioencoder
+ /// @brief Audio encoder class constructor used to support multiple instances.
+ ///
+ /// @param[in] instance The instance value given to
+ /// <b>`kodi::addon::CAddonBase::CreateInstance(...)`</b>.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Here's example about the use of this:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// class CMyAudioEncoder : public kodi::addon::CInstanceAudioEncoder
+ /// {
+ /// public:
+ /// CMyAudioEncoder(const kodi::addon::IInstanceInfo& instance)
+ /// : kodi::addon::CInstanceAudioEncoder(instance)
+ /// {
+ /// ...
+ /// }
+ ///
+ /// ...
+ /// };
+ ///
+ /// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+ /// KODI_ADDON_INSTANCE_HDL& hdl)
+ /// {
+ /// kodi::Log(ADDON_LOG_INFO, "Creating my audio encoder instance");
+ /// hdl = new CMyAudioEncoder(instance);
+ /// return ADDON_STATUS_OK;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ explicit CInstanceAudioEncoder(const kodi::addon::IInstanceInfo& instance)
+ : IAddonInstance(instance)
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstanceAudioEncoder: Creation of multiple together "
+ "with single instance way is not allowed!");
+
+ SetAddonStruct(instance);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_audioencoder
+ /// @brief Start encoder (**required**)
+ ///
+ /// @param[in] tag Information tag about
+ /// @return True on success, false on failure.
+ ///
+ virtual bool Start(const kodi::addon::AudioEncoderInfoTag& tag) = 0;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_audioencoder
+ /// @brief Encode a chunk of audio (**required**)
+ ///
+ /// @param[in] pbtStream The input buffer
+ /// @param[in] numBytesRead Number of bytes in input buffer
+ /// @return Number of bytes consumed
+ ///
+ virtual ssize_t Encode(const uint8_t* pbtStream, size_t numBytesRead) = 0;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_audioencoder
+ /// @brief Finalize encoding (**optional**)
+ ///
+ /// @return True on success, false on failure.
+ ///
+ virtual bool Finish() { return true; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_audioencoder
+ /// @brief Write block of data
+ ///
+ /// @param[in] data Pointer to the array of elements to be written
+ /// @param[in] length Size in bytes to be written.
+ /// @return The total number of bytes successfully written is returned.
+ ///
+ /// @remarks Only called from addon itself.
+ ///
+ ssize_t Write(const uint8_t* data, size_t length)
+ {
+ return m_kodi->write(m_kodi->kodiInstance, data, length);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_audioencoder
+ /// @brief Set the file's current position.
+ ///
+ /// The whence argument is optional and defaults to SEEK_SET (0)
+ ///
+ /// @param[in] position The position that you want to seek to
+ /// @param[in] whence [optional] offset relative to\n
+ /// You can set the value of whence to one
+ /// of three things:
+ /// | Value | int | Description |
+ /// |:--------:|:---:|:---------------------------------------------------|
+ /// | SEEK_SET | 0 | position is relative to the beginning of the file. This is probably what you had in mind anyway, and is the most commonly used value for whence.
+ /// | SEEK_CUR | 1 | position is relative to the current file pointer position. So, in effect, you can say, "Move to my current position plus 30 bytes," or, "move to my current position minus 20 bytes."
+ /// | SEEK_END | 2 | position is relative to the end of the file. Just like SEEK_SET except from the other end of the file. Be sure to use negative values for offset if you want to back up from the end of the file, instead of going past the end into oblivion.
+ ///
+ /// @return Returns the resulting offset location as measured in bytes from
+ /// the beginning of the file. On error, the value -1 is returned.
+ ///
+ /// @remarks Only called from addon itself.
+ ///
+ ssize_t Seek(ssize_t position, int whence = SEEK_SET)
+ {
+ return m_kodi->seek(m_kodi->kodiInstance, position, whence);
+ }
+ //----------------------------------------------------------------------------
+
+private:
+ void SetAddonStruct(KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ instance->hdl = this;
+ instance->audioencoder->toAddon->start = ADDON_start;
+ instance->audioencoder->toAddon->encode = ADDON_encode;
+ instance->audioencoder->toAddon->finish = ADDON_finish;
+ m_kodi = instance->audioencoder->toKodi;
+ }
+
+ inline static bool ADDON_start(const KODI_ADDON_AUDIOENCODER_HDL hdl,
+ const struct KODI_ADDON_AUDIOENCODER_INFO_TAG* tag)
+ {
+ return static_cast<CInstanceAudioEncoder*>(hdl)->Start(tag);
+ }
+
+ inline static ssize_t ADDON_encode(const KODI_ADDON_AUDIOENCODER_HDL hdl,
+ const uint8_t* pbtStream,
+ size_t num_bytes_read)
+ {
+ return static_cast<CInstanceAudioEncoder*>(hdl)->Encode(pbtStream, num_bytes_read);
+ }
+
+ inline static bool ADDON_finish(const KODI_ADDON_AUDIOENCODER_HDL hdl)
+ {
+ return static_cast<CInstanceAudioEncoder*>(hdl)->Finish();
+ }
+
+ AddonToKodiFuncTable_AudioEncoder* m_kodi{nullptr};
+};
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/CMakeLists.txt
new file mode 100644
index 0000000..8f7d20c
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/CMakeLists.txt
@@ -0,0 +1,20 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ AudioDecoder.h
+ AudioEncoder.h
+ Game.h
+ ImageDecoder.h
+ Inputstream.h
+ PVR.h
+ Peripheral.h
+ Screensaver.h
+ VFS.h
+ VideoCodec.h
+ Visualization.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_addon-instance)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Game.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Game.h
new file mode 100644
index 0000000..b4f1a24
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Game.h
@@ -0,0 +1,1432 @@
+/*
+ * Copyright (C) 2014-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 "../AddonBase.h"
+#include "../c-api/addon-instance/game.h"
+
+#include <algorithm>
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @addtogroup cpp_kodi_addon_game
+///
+/// To use on Libretro and for stand-alone games or emulators that does not use
+/// the Libretro API.
+///
+/// Possible examples could be, Nvidia GameStream via Limelight or WINE capture
+/// could possible through the Game API.
+///
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_game_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_addon_game
+/// @brief **Game add-on instance definition values**
+///
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_game_Defs_InputTypes_GameControllerLayout class GameControllerLayout
+/// @ingroup cpp_kodi_addon_game_Defs_InputTypes
+/// @brief Data of layouts for known controllers.
+///
+/// Used on @ref kodi::addon::CInstanceGame::SetControllerLayouts().
+///@{
+class GameControllerLayout
+{
+public:
+ /*! @cond PRIVATE */
+ explicit GameControllerLayout() = default;
+ GameControllerLayout(const game_controller_layout& layout) : controller_id(layout.controller_id)
+ {
+ provides_input = layout.provides_input;
+ for (unsigned int i = 0; i < layout.digital_button_count; ++i)
+ digital_buttons.push_back(layout.digital_buttons[i]);
+ for (unsigned int i = 0; i < layout.analog_button_count; ++i)
+ analog_buttons.push_back(layout.analog_buttons[i]);
+ for (unsigned int i = 0; i < layout.analog_stick_count; ++i)
+ analog_sticks.push_back(layout.analog_sticks[i]);
+ for (unsigned int i = 0; i < layout.accelerometer_count; ++i)
+ accelerometers.push_back(layout.accelerometers[i]);
+ for (unsigned int i = 0; i < layout.key_count; ++i)
+ keys.push_back(layout.keys[i]);
+ for (unsigned int i = 0; i < layout.rel_pointer_count; ++i)
+ rel_pointers.push_back(layout.rel_pointers[i]);
+ for (unsigned int i = 0; i < layout.abs_pointer_count; ++i)
+ abs_pointers.push_back(layout.abs_pointers[i]);
+ for (unsigned int i = 0; i < layout.motor_count; ++i)
+ motors.push_back(layout.motors[i]);
+ }
+ /*! @endcond */
+
+ /// @brief Controller identifier.
+ std::string controller_id;
+
+ /// @brief Provides input.
+ ///
+ /// False for multitaps
+ bool provides_input{false};
+
+ /// @brief Digital buttons.
+ std::vector<std::string> digital_buttons;
+
+ /// @brief Analog buttons.
+ std::vector<std::string> analog_buttons;
+
+ /// @brief Analog sticks.
+ std::vector<std::string> analog_sticks;
+
+ /// @brief Accelerometers.
+ std::vector<std::string> accelerometers;
+
+ /// @brief Keys.
+ std::vector<std::string> keys;
+
+ /// @brief Relative pointers.
+ std::vector<std::string> rel_pointers;
+
+ /// @brief Absolute pointers.
+ std::vector<std::string> abs_pointers;
+
+ /// @brief Motors.
+ std::vector<std::string> motors;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @addtogroup cpp_kodi_addon_game
+/// @brief @cpp_class{ kodi::addon::CInstanceGame }
+/// **Game add-on instance**\n
+/// This class provides the basic game processing system for use as an add-on in
+/// Kodi.
+///
+/// This class is created at addon by Kodi.
+///
+class ATTR_DLL_LOCAL CInstanceGame : public IAddonInstance
+{
+public:
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_game_Base 1. Basic functions
+ /// @ingroup cpp_kodi_addon_game
+ /// @brief **Functions to manage the addon and get basic information about it**
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Game class constructor
+ ///
+ /// Used by an add-on that only supports only Game and only in one instance.
+ ///
+ /// This class is created at addon by Kodi.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ ///
+ /// **Here's example about the use of this:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/addon-instance/Game.h>
+ /// ...
+ ///
+ /// class ATTR_DLL_LOCAL CGameExample
+ /// : public kodi::addon::CAddonBase,
+ /// public kodi::addon::CInstanceGame
+ /// {
+ /// public:
+ /// CGameExample()
+ /// {
+ /// }
+ ///
+ /// virtual ~CGameExample();
+ /// {
+ /// }
+ ///
+ /// ...
+ /// };
+ ///
+ /// ADDONCREATOR(CGameExample)
+ /// ~~~~~~~~~~~~~
+ ///
+ CInstanceGame() : IAddonInstance(IInstanceInfo(CPrivateBase::m_interface->firstKodiInstance))
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstanceGame: Creation of more as one in single "
+ "instance way is not allowed!");
+
+ SetAddonStruct(CPrivateBase::m_interface->firstKodiInstance);
+ CPrivateBase::m_interface->globalSingleInstance = this;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Destructor
+ ///
+ ~CInstanceGame() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// The path of the game client being loaded.
+ ///
+ /// @return the used game client Dll path
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ std::string GameClientDllPath() const { return m_instanceData->props->game_client_dll_path; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Paths to proxy DLLs used to load the game client.
+ ///
+ /// @param[out] paths vector list to store available dll paths
+ /// @return true if success and dll paths present
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ bool ProxyDllPaths(std::vector<std::string>& paths)
+ {
+ for (unsigned int i = 0; i < m_instanceData->props->proxy_dll_count; ++i)
+ {
+ if (m_instanceData->props->proxy_dll_paths[i] != nullptr)
+ paths.push_back(m_instanceData->props->proxy_dll_paths[i]);
+ }
+ return !paths.empty();
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// The "system" directories of the frontend.
+ ///
+ /// These directories can be used to store system-specific ROMs such as
+ /// BIOSes, configuration data, etc.
+ ///
+ /// @return the used resource directory
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ bool ResourceDirectories(std::vector<std::string>& dirs)
+ {
+ for (unsigned int i = 0; i < m_instanceData->props->resource_directory_count; ++i)
+ {
+ if (m_instanceData->props->resource_directories[i] != nullptr)
+ dirs.push_back(m_instanceData->props->resource_directories[i]);
+ }
+ return !dirs.empty();
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// The writable directory of the frontend.
+ ///
+ /// This directory can be used to store SRAM, memory cards, high scores,
+ /// etc, if the game client cannot use the regular memory interface,
+ /// GetMemoryData().
+ ///
+ /// @return the used profile directory
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ std::string ProfileDirectory() const { return m_instanceData->props->profile_directory; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// The value of the <supports_vfs> property from addon.xml.
+ ///
+ /// @return true if VFS is supported
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ bool SupportsVFS() const { return m_instanceData->props->supports_vfs; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// The extensions in the <extensions> property from addon.xml.
+ ///
+ /// @param[out] extensions vector list to store available extension
+ /// @return true if success and extensions present
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ bool Extensions(std::vector<std::string>& extensions)
+ {
+ for (unsigned int i = 0; i < m_instanceData->props->extension_count; ++i)
+ {
+ if (m_instanceData->props->extensions[i] != nullptr)
+ extensions.push_back(m_instanceData->props->extensions[i]);
+ }
+ return !extensions.empty();
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+//--==----==----==----==----==----==----==----==----==----==----==----==----==--
+
+ //============================================================================
+ ///
+ /// @defgroup cpp_kodi_addon_game_Operation 2. Game operations
+ /// @ingroup cpp_kodi_addon_game
+ /// @brief **Game operations**
+ ///
+ /// These are mandatory functions for using this addon to get the available
+ /// channels.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Game operation parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_game_Operation_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_game_Operation_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Load a game
+ ///
+ /// @param[in] url The URL to load
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the game was loaded
+ ///
+ virtual GAME_ERROR LoadGame(const std::string& url)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Load a game that requires multiple files
+ ///
+ /// @param[in] type The game type
+ /// @param[in] urls An array of urls
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the game was loaded
+ ///
+ virtual GAME_ERROR LoadGameSpecial(SPECIAL_GAME_TYPE type, const std::vector<std::string>& urls)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Begin playing without a game file
+ ///
+ /// If the add-on supports standalone mode, it must add the <supports_standalone>
+ /// tag to the extension point in addon.xml:
+ ///
+ /// <supports_no_game>false</supports_no_game>
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the game add-on was loaded
+ ///
+ virtual GAME_ERROR LoadStandalone()
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Unload the current game
+ ///
+ /// Unloads a currently loaded game
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the game was unloaded
+ ///
+ virtual GAME_ERROR UnloadGame()
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get timing information about the loaded game
+ ///
+ /// @param[out] timing_info The info structure to fill
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if info was filled
+ ///
+ virtual GAME_ERROR GetGameTiming(game_system_timing& timing_info)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get region of the loaded game
+ ///
+ /// @return the region, or @ref GAME_REGION_UNKNOWN if unknown or no game is loaded
+ ///
+ virtual GAME_REGION GetRegion()
+ {
+ return GAME_REGION_UNKNOWN;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Return true if the client requires the frontend to provide a game loop
+ ///
+ /// The game loop is a thread that calls RunFrame() in a loop at a rate
+ /// determined by the playback speed and the client's FPS.
+ ///
+ /// @return true if the frontend should provide a game loop, false otherwise
+ ///
+ virtual bool RequiresGameLoop()
+ {
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Run a single frame for add-ons that use a game loop
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if there was no error
+ ///
+ virtual GAME_ERROR RunFrame()
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Reset the current game
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the game was reset
+ ///
+ virtual GAME_ERROR Reset()
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Requests the frontend to stop the current game
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ void CloseGame(void) { m_instanceData->toKodi->CloseGame(m_instanceData->toKodi->kodiInstance); }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_game_Operation_CStream Class: CStream
+ /// @ingroup cpp_kodi_addon_game_Operation
+ /// @brief @cpp_class{ kodi::addon::CInstanceGame::CStream }
+ /// **Game stream handler**
+ ///
+ /// This class will be integrated into the addon, which can then open it if
+ /// necessary for the processing of an audio or video stream.
+ ///
+ ///
+ /// @note Callback to Kodi class
+ ///@{
+ class CStream
+ {
+ public:
+ CStream() = default;
+
+ CStream(const game_stream_properties& properties)
+ {
+ Open(properties);
+ }
+
+ ~CStream()
+ {
+ Close();
+ }
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_game_Operation_CStream
+ /// @brief Create a stream for gameplay data
+ ///
+ /// @param[in] properties The stream properties
+ /// @return A stream handle, or `nullptr` on failure
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ bool Open(const game_stream_properties& properties)
+ {
+ if (!CPrivateBase::m_interface->globalSingleInstance)
+ return false;
+
+ if (m_handle)
+ {
+ kodi::Log(ADDON_LOG_INFO, "kodi::addon::CInstanceGame::CStream already becomes reopened");
+ Close();
+ }
+
+ AddonToKodiFuncTable_Game& cb =
+ *static_cast<CInstanceGame*>(CPrivateBase::m_interface->globalSingleInstance)
+ ->m_instanceData->toKodi;
+ m_handle = cb.OpenStream(cb.kodiInstance, &properties);
+ return m_handle != nullptr;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_game_Operation_CStream
+ /// @brief Free the specified stream
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ void Close()
+ {
+ if (!m_handle || !CPrivateBase::m_interface->globalSingleInstance)
+ return;
+
+ AddonToKodiFuncTable_Game& cb =
+ *static_cast<CInstanceGame*>(CPrivateBase::m_interface->globalSingleInstance)
+ ->m_instanceData->toKodi;
+ cb.CloseStream(cb.kodiInstance, m_handle);
+ m_handle = nullptr;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_game_Operation_CStream
+ /// @brief Get a buffer for zero-copy stream data
+ ///
+ /// @param[in] width The framebuffer width, or 0 for no width specified
+ /// @param[in] height The framebuffer height, or 0 for no height specified
+ /// @param[out] buffer The buffer, or unmodified if false is returned
+ /// @return True if buffer was set, false otherwise
+ ///
+ /// @note If this returns true, buffer must be freed using @ref ReleaseBuffer().
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ bool GetBuffer(unsigned int width, unsigned int height, game_stream_buffer& buffer)
+ {
+ if (!m_handle || !CPrivateBase::m_interface->globalSingleInstance)
+ return false;
+
+ AddonToKodiFuncTable_Game& cb =
+ *static_cast<CInstanceGame*>(CPrivateBase::m_interface->globalSingleInstance)
+ ->m_instanceData->toKodi;
+ return cb.GetStreamBuffer(cb.kodiInstance, m_handle, width, height, &buffer);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_game_Operation_CStream
+ /// @brief Add a data packet to a stream
+ ///
+ /// @param[in] packet The data packet
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ void AddData(const game_stream_packet& packet)
+ {
+ if (!m_handle || !CPrivateBase::m_interface->globalSingleInstance)
+ return;
+
+ AddonToKodiFuncTable_Game& cb =
+ *static_cast<CInstanceGame*>(CPrivateBase::m_interface->globalSingleInstance)
+ ->m_instanceData->toKodi;
+ cb.AddStreamData(cb.kodiInstance, m_handle, &packet);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_game_Operation_CStream
+ /// @brief Free an allocated buffer
+ ///
+ /// @param[in] buffer The buffer returned from GetStreamBuffer()
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ void ReleaseBuffer(game_stream_buffer& buffer)
+ {
+ if (!m_handle || !CPrivateBase::m_interface->globalSingleInstance)
+ return;
+
+ AddonToKodiFuncTable_Game& cb =
+ *static_cast<CInstanceGame*>(CPrivateBase::m_interface->globalSingleInstance)
+ ->m_instanceData->toKodi;
+ cb.ReleaseStreamBuffer(cb.kodiInstance, m_handle, &buffer);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_game_Operation_CStream
+ /// @brief To check stream open was OK, e.g. after use of constructor
+ ///
+ /// @return true if stream was successfully opened
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ bool IsOpen() const { return m_handle != nullptr; }
+ //--------------------------------------------------------------------------
+
+ private:
+ KODI_GAME_STREAM_HANDLE m_handle = nullptr;
+ };
+ ///@}
+
+ ///@}
+
+//--==----==----==----==----==----==----==----==----==----==----==----==----==--
+
+ //============================================================================
+ ///
+ /// @defgroup cpp_kodi_addon_game_HardwareRendering 3. Hardware rendering operations
+ /// @ingroup cpp_kodi_addon_game
+ /// @brief **Hardware rendering operations**
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Hardware rendering operation parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_game_HardwareRendering_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_game_HardwareRendering_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Invalidates the current HW context and reinitializes GPU resources
+ ///
+ /// Any GL state is lost, and must not be deinitialized explicitly.
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the HW context was reset
+ ///
+ virtual GAME_ERROR HwContextReset()
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Called before the context is destroyed
+ ///
+ /// Resources can be deinitialized at this step.
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the HW context was destroyed
+ ///
+ virtual GAME_ERROR HwContextDestroy()
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**<br>Get a symbol from the hardware context
+ ///
+ /// @param[in] sym The symbol's name
+ ///
+ /// @return A function pointer for the specified symbol
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ game_proc_address_t HwGetProcAddress(const char* sym)
+ {
+ return m_instanceData->toKodi->HwGetProcAddress(m_instanceData->toKodi->kodiInstance, sym);
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+//--==----==----==----==----==----==----==----==----==----==----==----==----==--
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_game_InputOperations 4. Input operations
+ /// @ingroup cpp_kodi_addon_game
+ /// @brief **Input operations**
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Hardware rendering operation parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_game_InputOperations_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_game_InputOperations_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Check if input is accepted for a feature on the controller
+ ///
+ /// If only a subset of the controller profile is used, this can return false
+ /// for unsupported features to not absorb their input.
+ ///
+ /// If the entire controller profile is used, this should always return true.
+ ///
+ /// @param[in] controller_id The ID of the controller profile
+ /// @param[in] feature_name The name of a feature in that profile
+ /// @return true if input is accepted for the feature, false otherwise
+ ///
+ virtual bool HasFeature(const std::string& controller_id, const std::string& feature_name)
+ {
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the input topology that specifies which controllers can be connected
+ ///
+ /// @return The input topology, or null to use the default
+ ///
+ /// If this returns non-null, topology must be freed using FreeTopology().
+ ///
+ /// If this returns null, the topology will default to a single port that can
+ /// accept all controllers imported by addon.xml. The port ID is set to
+ /// the @ref DEFAULT_PORT_ID constant.
+ ///
+ virtual game_input_topology* GetTopology()
+ {
+ return nullptr;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Free the topology's resources
+ ///
+ /// @param[in] topology The topology returned by GetTopology()
+ ///
+ virtual void FreeTopology(game_input_topology* topology)
+ {
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set the layouts for known controllers
+ ///
+ /// @param[in] controllers The controller layouts
+ ///
+ /// After loading the input topology, the frontend will call this with
+ /// controller layouts for all controllers discovered in the topology.
+ ///
+ virtual void SetControllerLayouts(const std::vector<kodi::addon::GameControllerLayout>& controllers)
+ {
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Enable/disable keyboard input using the specified controller
+ ///
+ /// @param[in] enable True to enable input, false otherwise
+ /// @param[in] controller_id The controller ID if enabling, or unused if disabling
+ ///
+ /// @return True if keyboard input was enabled, false otherwise
+ ///
+ virtual bool EnableKeyboard(bool enable, const std::string& controller_id)
+ {
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Enable/disable mouse input using the specified controller
+ ///
+ /// @param[in] enable True to enable input, false otherwise
+ /// @param[in] controller_id The controller ID if enabling, or unused if disabling
+ ///
+ /// @return True if mouse input was enabled, false otherwise
+ ///
+ virtual bool EnableMouse(bool enable, const std::string& controller_id)
+ {
+ return false;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Connect/disconnect a controller to a port on the virtual game console
+ ///
+ /// @param[in] connect True to connect a controller, false to disconnect
+ /// @param[in] port_address The address of the port
+ /// @param[in] controller_id The controller ID if connecting, or unused if disconnecting
+ /// @return True if the \p controller was (dis-)connected to the port, false otherwise
+ ///
+ /// The address is a string that allows traversal of the controller topology.
+ /// It is formed by alternating port IDs and controller IDs separated by "/".
+ ///
+ /// For example, assume that the topology represented in XML for Snes9x is:
+ ///
+ /// ~~~~~~~~~~~~~{.xml}
+ /// <logicaltopology>
+ /// <port type="controller" id="1">
+ /// <accepts controller="game.controller.snes"/>
+ /// <accepts controller="game.controller.snes.multitap">
+ /// <port type="controller" id="1">
+ /// <accepts controller="game.controller.snes"/>
+ /// </port>
+ /// <port type="controller" id="2">
+ /// <accepts controller="game.controller.snes"/>
+ /// </port>
+ /// ...
+ /// </accepts>
+ /// </port>
+ /// </logicaltopology>
+ /// ~~~~~~~~~~~~~
+ ///
+ /// To connect a multitap to the console's first port, the multitap's controller
+ /// info is set using the port address:
+ ///
+ /// 1
+ ///
+ /// To connect a SNES controller to the second port of the multitap, the
+ /// controller info is next set using the address:
+ ///
+ /// 1/game.controller.multitap/2
+ ///
+ /// Any attempts to connect a controller to a port on a disconnected multitap
+ /// will return false.
+ ///
+ virtual bool ConnectController(bool connect,
+ const std::string& port_address,
+ const std::string& controller_id)
+ {
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Notify the add-on of an input event
+ ///
+ /// @param[in] event The input event
+ ///
+ /// @return true if the event was handled, false otherwise
+ ///
+ virtual bool InputEvent(const game_input_event& event)
+ {
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**<br>Notify the port of an input event
+ ///
+ /// @param[in] event The input event
+ /// @return true if the event was handled, false otherwise
+ ///
+ /// @note Input events can arrive for the following sources:
+ /// - @ref GAME_INPUT_EVENT_MOTOR
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ bool KodiInputEvent(const game_input_event& event)
+ {
+ return m_instanceData->toKodi->InputEvent(m_instanceData->toKodi->kodiInstance, &event);
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+//--==----==----==----==----==----==----==----==----==----==----==----==----==--
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_game_SerializationOperations 5. Serialization operations
+ /// @ingroup cpp_kodi_addon_game
+ /// @brief **Serialization operations**
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Serialization operation parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_game_SerializationOperations_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_game_SerializationOperations_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Get the number of bytes required to serialize the game
+ ///
+ /// @return the number of bytes, or 0 if serialization is not supported
+ ///
+ virtual size_t SerializeSize()
+ {
+ return 0;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Serialize the state of the game
+ ///
+ /// @param[in] data The buffer receiving the serialized game data
+ /// @param[in] size The size of the buffer
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the game was serialized into the buffer
+ ///
+ virtual GAME_ERROR Serialize(uint8_t* data, size_t size)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Deserialize the game from the given state
+ ///
+ /// @param[in] data A buffer containing the game's new state
+ /// @param[in] size The size of the buffer
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the game deserialized
+ ///
+ virtual GAME_ERROR Deserialize(const uint8_t* data, size_t size)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+//--==----==----==----==----==----==----==----==----==----==----==----==----==--
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_game_CheatOperations 6. Cheat operations
+ /// @ingroup cpp_kodi_addon_game
+ /// @brief **Cheat operations**
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Cheat operation parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_game_CheatOperations_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_game_CheatOperations_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Reset the cheat system
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the cheat system was reset
+ ///
+ virtual GAME_ERROR CheatReset()
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get a region of memory
+ ///
+ /// @param[in] type The type of memory to retrieve
+ /// @param[in] data Set to the region of memory; must remain valid until UnloadGame() is called
+ /// @param[in] size Set to the size of the region of memory
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if data was set to a valid buffer
+ ///
+ virtual GAME_ERROR GetMemory(GAME_MEMORY type, uint8_t*& data, size_t& size)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set a cheat code
+ ///
+ /// @param[in] index
+ /// @param[in] enabled
+ /// @param[in] code
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the cheat was set
+ ///
+ virtual GAME_ERROR SetCheat(unsigned int index, bool enabled, const std::string& code)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+
+ //============================================================================
+ /// @brief Generates a RetroAchievements hash for a given game that
+ /// can be used to identify the game by RetroAchievements
+ ///
+ /// @param[out] hash The hash of the file. Its size must be >=33 characters
+ /// @param[in] consoleID The console ID as it is defined by rcheevos for
+ /// the console the ROM is made for
+ /// @param[in] filePath The path of the rom
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the hash was generated
+ /// successfully
+ ///
+ virtual GAME_ERROR RCGenerateHashFromFile(std::string& hash,
+ unsigned int consoleID,
+ const std::string& filePath)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+
+ //============================================================================
+ /// @brief Gets a URL to the endpoint that returns the game ID
+ ///
+ /// @param[out] url The URL to GET the game ID
+ /// @param[in] size The size of the URL char array
+ /// @param[in] hash The hash of the rom
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the URL was created
+ ///
+ virtual GAME_ERROR RCGetGameIDUrl(std::string& url, const std::string& hash)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+
+ //============================================================================
+ /// @brief Gets a URL to the endpoint that returns the patch file
+ ///
+ /// @param[out] url The URL to GET the game patch file
+ /// @param[in] size The size of the URL char array
+ /// @param[in] username The RetroAchievements username of the user
+ /// @param[in] token The login token to RetroAchievements of the user
+ /// @param[in] gameID The ID of the game in RetroAchievements API
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the URL was created
+ ///
+ virtual GAME_ERROR RCGetPatchFileUrl(std::string& url,
+ const std::string& username,
+ const std::string& token,
+ unsigned int gameID)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+
+ //============================================================================
+ /// @brief Gets a URL to the endpoint that updates the rich presence
+ /// in the user's RetroAchievements profile
+ ///
+ /// @param[out] url The URL to POST the rich presence to RetroAchievements
+ /// @param[in] urlSize The size of the URL char array
+ /// @param[out] postData The post data of the request
+ /// @param[in] postSize The size of the post data char array
+ /// @param[in] username The RetroAchievements username of the user
+ /// @param[in] token The login token to RetroAchievements of the user
+ /// @param[in] gameID The ID of the game in RetroAchievements API
+ /// @param[in] richPresence The rich presence evaluation to POST
+ ///
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the URL and post data
+ /// were created
+ ///
+ virtual GAME_ERROR RCPostRichPresenceUrl(std::string& url,
+ std::string& postData,
+ const std::string& username,
+ const std::string& token,
+ unsigned int gameID,
+ const std::string& richPresence)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+
+ //============================================================================
+ /// @brief Enables rich presence
+ ///
+ /// @param[in] script The rich presence script from RetroAchievements
+ ///
+ /// @return the error, or GAME_ERROR_NO_ERROR if rich presence was enabled
+ ///
+ virtual GAME_ERROR RCEnableRichPresence(const std::string& script)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+
+ //============================================================================
+ /// @brief Gets the rich presence evaluation for the current frame.
+ /// Rich presence must be enabled first or this will fail.
+ ///
+ /// @param[out] evaluation The evaluation of what the player is doing in
+ /// the game this frame
+ /// @param[in] size The size of the evaluation char pointer
+ /// @param[in] consoleID The console ID as it is defined by rcheevos for
+ /// the console the rom is made for
+ /// @return the error, or @ref GAME_ERROR_NO_ERROR if the evaluation was
+ /// created successfully
+ ///
+ virtual GAME_ERROR RCGetRichPresenceEvaluation(std::string& evaluation, unsigned int consoleID)
+ {
+ return GAME_ERROR_NOT_IMPLEMENTED;
+ }
+
+ //============================================================================
+ /// @brief Resets the runtime. Must be called each time a new rom is starting
+ /// and when the savestate is changed
+ ///
+ /// @return the error, or GAME_ERROR_NO_ERROR if the runtim was reseted
+ /// successfully
+ ///
+ virtual GAME_ERROR RCResetRuntime() { return GAME_ERROR_NOT_IMPLEMENTED; }
+
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+private:
+ void SetAddonStruct(KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ instance->hdl = this;
+
+ instance->game->toAddon->LoadGame = ADDON_LoadGame;
+ instance->game->toAddon->LoadGameSpecial = ADDON_LoadGameSpecial;
+ instance->game->toAddon->LoadStandalone = ADDON_LoadStandalone;
+ instance->game->toAddon->UnloadGame = ADDON_UnloadGame;
+ instance->game->toAddon->GetGameTiming = ADDON_GetGameTiming;
+ instance->game->toAddon->GetRegion = ADDON_GetRegion;
+ instance->game->toAddon->RequiresGameLoop = ADDON_RequiresGameLoop;
+ instance->game->toAddon->RunFrame = ADDON_RunFrame;
+ instance->game->toAddon->Reset = ADDON_Reset;
+
+ instance->game->toAddon->HwContextReset = ADDON_HwContextReset;
+ instance->game->toAddon->HwContextDestroy = ADDON_HwContextDestroy;
+
+ instance->game->toAddon->HasFeature = ADDON_HasFeature;
+ instance->game->toAddon->GetTopology = ADDON_GetTopology;
+ instance->game->toAddon->FreeTopology = ADDON_FreeTopology;
+ instance->game->toAddon->SetControllerLayouts = ADDON_SetControllerLayouts;
+ instance->game->toAddon->EnableKeyboard = ADDON_EnableKeyboard;
+ instance->game->toAddon->EnableMouse = ADDON_EnableMouse;
+ instance->game->toAddon->ConnectController = ADDON_ConnectController;
+ instance->game->toAddon->InputEvent = ADDON_InputEvent;
+
+ instance->game->toAddon->SerializeSize = ADDON_SerializeSize;
+ instance->game->toAddon->Serialize = ADDON_Serialize;
+ instance->game->toAddon->Deserialize = ADDON_Deserialize;
+
+ instance->game->toAddon->CheatReset = ADDON_CheatReset;
+ instance->game->toAddon->GetMemory = ADDON_GetMemory;
+ instance->game->toAddon->SetCheat = ADDON_SetCheat;
+
+ instance->game->toAddon->RCGenerateHashFromFile = ADDON_RCGenerateHashFromFile;
+ instance->game->toAddon->RCGetGameIDUrl = ADDON_RCGetGameIDUrl;
+ instance->game->toAddon->RCGetPatchFileUrl = ADDON_RCGetPatchFileUrl;
+ instance->game->toAddon->RCPostRichPresenceUrl = ADDON_RCPostRichPresenceUrl;
+ instance->game->toAddon->RCEnableRichPresence = ADDON_RCEnableRichPresence;
+ instance->game->toAddon->RCGetRichPresenceEvaluation = ADDON_RCGetRichPresenceEvaluation;
+ instance->game->toAddon->RCResetRuntime = ADDON_RCResetRuntime;
+
+ instance->game->toAddon->FreeString = ADDON_FreeString;
+
+ m_instanceData = instance->game;
+ m_instanceData->toAddon->addonInstance = this;
+ }
+
+ // --- Game operations ---------------------------------------------------------
+
+ inline static GAME_ERROR ADDON_LoadGame(const AddonInstance_Game* instance, const char* url)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->LoadGame(url);
+ }
+
+ inline static GAME_ERROR ADDON_LoadGameSpecial(const AddonInstance_Game* instance,
+ SPECIAL_GAME_TYPE type,
+ const char** urls,
+ size_t urlCount)
+ {
+ std::vector<std::string> urlList;
+ for (size_t i = 0; i < urlCount; ++i)
+ {
+ if (urls[i] != nullptr)
+ urlList.push_back(urls[i]);
+ }
+
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->LoadGameSpecial(type, urlList);
+ }
+
+ inline static GAME_ERROR ADDON_LoadStandalone(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->LoadStandalone();
+ }
+
+ inline static GAME_ERROR ADDON_UnloadGame(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->UnloadGame();
+ }
+
+ inline static GAME_ERROR ADDON_GetGameTiming(const AddonInstance_Game* instance,
+ game_system_timing* timing_info)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->GetGameTiming(*timing_info);
+ }
+
+ inline static GAME_REGION ADDON_GetRegion(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->GetRegion();
+ }
+
+ inline static bool ADDON_RequiresGameLoop(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->RequiresGameLoop();
+ }
+
+ inline static GAME_ERROR ADDON_RunFrame(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->RunFrame();
+ }
+
+ inline static GAME_ERROR ADDON_Reset(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->Reset();
+ }
+
+
+ // --- Hardware rendering operations -------------------------------------------
+
+ inline static GAME_ERROR ADDON_HwContextReset(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->HwContextReset();
+ }
+
+ inline static GAME_ERROR ADDON_HwContextDestroy(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->HwContextDestroy();
+ }
+
+
+ // --- Input operations --------------------------------------------------------
+
+ inline static bool ADDON_HasFeature(const AddonInstance_Game* instance,
+ const char* controller_id,
+ const char* feature_name)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->HasFeature(controller_id, feature_name);
+ }
+
+ inline static game_input_topology* ADDON_GetTopology(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->GetTopology();
+ }
+
+ inline static void ADDON_FreeTopology(const AddonInstance_Game* instance,
+ game_input_topology* topology)
+ {
+ static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->FreeTopology(topology);
+ }
+
+ inline static void ADDON_SetControllerLayouts(const AddonInstance_Game* instance,
+ const game_controller_layout* controllers,
+ unsigned int controller_count)
+ {
+ if (controllers == nullptr)
+ return;
+
+ std::vector<GameControllerLayout> controllerList;
+ for (unsigned int i = 0; i < controller_count; ++i)
+ controllerList.push_back(controllers[i]);
+
+ static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->SetControllerLayouts(controllerList);
+ }
+
+ inline static bool ADDON_EnableKeyboard(const AddonInstance_Game* instance,
+ bool enable,
+ const char* controller_id)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->EnableKeyboard(enable, controller_id);
+ }
+
+ inline static bool ADDON_EnableMouse(const AddonInstance_Game* instance,
+ bool enable,
+ const char* controller_id)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->EnableMouse(enable, controller_id);
+ }
+
+ inline static bool ADDON_ConnectController(const AddonInstance_Game* instance,
+ bool connect,
+ const char* port_address,
+ const char* controller_id)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->ConnectController(connect, port_address, controller_id);
+ }
+
+ inline static bool ADDON_InputEvent(const AddonInstance_Game* instance,
+ const game_input_event* event)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->InputEvent(*event);
+ }
+
+
+ // --- Serialization operations ------------------------------------------------
+
+ inline static size_t ADDON_SerializeSize(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->SerializeSize();
+ }
+
+ inline static GAME_ERROR ADDON_Serialize(const AddonInstance_Game* instance,
+ uint8_t* data,
+ size_t size)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->Serialize(data, size);
+ }
+
+ inline static GAME_ERROR ADDON_Deserialize(const AddonInstance_Game* instance,
+ const uint8_t* data,
+ size_t size)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->Deserialize(data, size);
+ }
+
+
+ // --- Cheat operations --------------------------------------------------------
+
+ inline static GAME_ERROR ADDON_CheatReset(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->CheatReset();
+ }
+
+ inline static GAME_ERROR ADDON_GetMemory(const AddonInstance_Game* instance,
+ GAME_MEMORY type,
+ uint8_t** data,
+ size_t* size)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->GetMemory(type, *data, *size);
+ }
+
+ inline static GAME_ERROR ADDON_SetCheat(const AddonInstance_Game* instance,
+ unsigned int index,
+ bool enabled,
+ const char* code)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->SetCheat(index, enabled, code);
+ }
+
+ inline static GAME_ERROR ADDON_RCGenerateHashFromFile(const AddonInstance_Game* instance,
+ char** hash,
+ unsigned int consoleID,
+ const char* filePath)
+ {
+ std::string cppHash;
+
+ GAME_ERROR ret = static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->RCGenerateHashFromFile(cppHash, consoleID, filePath);
+ if (!cppHash.empty() && hash)
+ {
+ *hash = new char[cppHash.size() + 1];
+ std::copy(cppHash.begin(), cppHash.end(), *hash);
+ (*hash)[cppHash.size()] = '\0';
+ }
+ return ret;
+ }
+
+ inline static GAME_ERROR ADDON_RCGetGameIDUrl(const AddonInstance_Game* instance,
+ char** url,
+ const char* hash)
+ {
+ std::string cppUrl;
+ GAME_ERROR ret =
+ static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->RCGetGameIDUrl(cppUrl, hash);
+ if (!cppUrl.empty() && url)
+ {
+ *url = new char[cppUrl.size() + 1];
+ std::copy(cppUrl.begin(), cppUrl.end(), *url);
+ (*url)[cppUrl.size()] = '\0';
+ }
+ return ret;
+ }
+
+ inline static GAME_ERROR ADDON_RCGetPatchFileUrl(const AddonInstance_Game* instance,
+ char** url,
+ const char* username,
+ const char* token,
+ unsigned int gameID)
+ {
+ std::string cppUrl;
+
+ GAME_ERROR ret = static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->RCGetPatchFileUrl(cppUrl, username, token, gameID);
+ if (!cppUrl.empty() && url)
+ {
+ *url = new char[cppUrl.size() + 1];
+ std::copy(cppUrl.begin(), cppUrl.end(), *url);
+ (*url)[cppUrl.size()] = '\0';
+ }
+ return ret;
+ }
+
+ inline static GAME_ERROR ADDON_RCPostRichPresenceUrl(const AddonInstance_Game* instance,
+ char** url,
+ char** postData,
+ const char* username,
+ const char* token,
+ unsigned int gameID,
+ const char* richPresence)
+ {
+ std::string cppUrl;
+ std::string cppPostData;
+ GAME_ERROR ret =
+ static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->RCPostRichPresenceUrl(cppUrl, cppPostData, username, token, gameID, richPresence);
+ if (!cppUrl.empty())
+ {
+ *url = new char[cppUrl.size() + 1];
+ std::copy(cppUrl.begin(), cppUrl.end(), *url);
+ (*url)[cppUrl.size()] = '\0';
+ }
+ if (!cppPostData.empty())
+ {
+ *postData = new char[cppPostData.size() + 1];
+ std::copy(cppPostData.begin(), cppPostData.end(), *postData);
+ (*postData)[cppPostData.size()] = '\0';
+ }
+
+ return ret;
+ }
+
+ inline static GAME_ERROR ADDON_RCEnableRichPresence(const AddonInstance_Game* instance,
+ const char* script)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->RCEnableRichPresence(script);
+ }
+
+ inline static GAME_ERROR ADDON_RCGetRichPresenceEvaluation(const AddonInstance_Game* instance,
+ char** evaluation,
+ unsigned int consoleID)
+ {
+ std::string cppEvaluation;
+ GAME_ERROR ret = static_cast<CInstanceGame*>(instance->toAddon->addonInstance)
+ ->RCGetRichPresenceEvaluation(cppEvaluation, consoleID);
+ if (!cppEvaluation.empty())
+ {
+ *evaluation = new char[cppEvaluation.size() + 1];
+ std::copy(cppEvaluation.begin(), cppEvaluation.end(), *evaluation);
+ (*evaluation)[cppEvaluation.size()] = '\0';
+ }
+
+ return ret;
+ }
+
+ inline static GAME_ERROR ADDON_RCResetRuntime(const AddonInstance_Game* instance)
+ {
+ return static_cast<CInstanceGame*>(instance->toAddon->addonInstance)->RCResetRuntime();
+ }
+
+ inline static void ADDON_FreeString(const AddonInstance_Game* instance, char* str)
+ {
+ delete[] str;
+ }
+
+ AddonInstance_Game* m_instanceData;
+};
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/ImageDecoder.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/ImageDecoder.h
new file mode 100644
index 0000000..eb98f90
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/ImageDecoder.h
@@ -0,0 +1,711 @@
+/*
+ * 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 "../AddonBase.h"
+#include "../c-api/addon-instance/imagedecoder.h"
+
+#ifdef __cplusplus
+
+#include <stdexcept>
+
+namespace kodi
+{
+namespace addon
+{
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_imagedecoder_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_addon_imagedecoder
+/// @brief **Image decoder add-on general variables**
+///
+/// Used to exchange the available options between Kodi and addon.
+///
+///
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_imagedecoder_Defs_ImageDecoderInfoTag class ImageDecoderInfoTag
+/// @ingroup cpp_kodi_addon_imagedecoder_Defs
+/// @brief **Info tag data structure**\n
+/// Representation of available information of processed audio file.
+///
+/// This is used to get all the necessary data of audio stream and to have on
+/// created files by encoders.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_imagedecoder_Defs_ImageDecoderInfoTag_Help
+///
+///@{
+class ATTR_DLL_LOCAL ImageDecoderInfoTag
+{
+public:
+ /*! \cond PRIVATE */
+ ImageDecoderInfoTag() = default;
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_imagedecoder_Defs_ImageDecoderInfoTag_Help Value Help
+ /// @ingroup cpp_kodi_addon_imagedecoder_Defs_ImageDecoderInfoTag
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_imagedecoder_Defs_ImageDecoderInfoTag :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Width** | `int` | @ref ImageDecoderInfoTag::SetWidth "SetWidth" | @ref ImageDecoderInfoTag::GetWidth "GetWidth"
+ /// | **Height** | `int` | @ref ImageDecoderInfoTag::SetHeight "SetHeight" | @ref ImageDecoderInfoTag::GetHeight "GetHeight"
+ /// | **Distance** | `float` | @ref ImageDecoderInfoTag::SetDistance "SetDistance" | @ref ImageDecoderInfoTag::GetDistance "GetDistance"
+ /// | **Color** | @ref cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_COLOR | @ref ImageDecoderInfoTag::SetColor "SetColor" | @ref ImageDecoderInfoTag::GetColor "GetColor"
+ /// | **Orientation** | @ref cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_ORIENTATION | @ref ImageDecoderInfoTag::SetOrientation "SetOrientation" | @ref ImageDecoderInfoTag::GetOrientation "GetOrientation"
+ /// | **Metering mode** | @ref cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_METERING_MODE | @ref ImageDecoderInfoTag::SetMeteringMode "SetMeteringMode" | @ref ImageDecoderInfoTag::GetMeteringMode "GetMeteringMode"
+ /// | **Exposure time** | `float` | @ref ImageDecoderInfoTag::SetExposureTime "SetExposureTime" | @ref ImageDecoderInfoTag::GetExposureTime "GetExposureTime"
+ /// | **Exposure bias** | `float` | @ref ImageDecoderInfoTag::SetExposureBias "SetExposureBias" | @ref ImageDecoderInfoTag::GetExposureBias "GetExposureBias"
+ /// | **Exposure program** | @ref cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_EXPOSURE_PROGRAM | @ref ImageDecoderInfoTag::SetExposureProgram "SetExposureProgram" | @ref ImageDecoderInfoTag::GetExposureProgram "GetExposureProgram"
+ /// | **Exposure mode** | @ref cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_EXPOSURE_MODE | @ref ImageDecoderInfoTag::SetExposureMode "SetExposureMode" | @ref ImageDecoderInfoTag::GetExposureMode "GetExposureMode"
+ /// | **Time created** | `time_t` | @ref ImageDecoderInfoTag::SetTimeCreated "SetTimeCreated" | @ref ImageDecoderInfoTag::GetTimeCreated "GetTimeCreated"
+ /// | **Aperture F number** | `float` | @ref ImageDecoderInfoTag::SetApertureFNumber "SetApertureFNumber" | @ref ImageDecoderInfoTag::GetApertureFNumber "GetApertureFNumber"
+ /// | **Flash used** | @ref ADDON_IMG_FLASH_TYPE | @ref ImageDecoderInfoTag::SetFlashUsed "SetFlashUsed" | @ref ImageDecoderInfoTag::GetFlashUsed "GetFlashUsed"
+ /// | **Light source** | @ref ADDON_IMG_LIGHT_SOURCE | @ref ImageDecoderInfoTag::SetLightSource "SetLightSource" | @ref ImageDecoderInfoTag::GetLightSource "GetLightSource"
+ /// | **Focal length** | `int` | @ref ImageDecoderInfoTag::SetFocalLength "SetFocalLength" | @ref ImageDecoderInfoTag::GetFocalLength "GetFocalLength"
+ /// | **Focal length in 35 mm format** | `int` | @ref ImageDecoderInfoTag::SetFocalLengthIn35mmFormat "SetFocalLengthIn35mmFormat" | @ref ImageDecoderInfoTag::GetFocalLengthIn35mmFormat "GetFocalLengthIn35mmFormat"
+ /// | **Digital zoom ratio** | `float` | @ref ImageDecoderInfoTag::SetDigitalZoomRatio "SetDigitalZoomRatio" | @ref ImageDecoderInfoTag::GetDigitalZoomRatio "GetDigitalZoomRatio"
+ /// | **ISO sensitivity** | `float` | @ref ImageDecoderInfoTag::SetISOSpeed "SetISOSpeed" | @ref ImageDecoderInfoTag::GetISOSpeed "GetISOSpeed"
+ /// | **Camera manufacturer** | `std::string` | @ref ImageDecoderInfoTag::SetCameraManufacturer "SetCameraManufacturer" | @ref ImageDecoderInfoTag::GetCameraManufacturer "GetCameraManufacturer"
+ /// | **GPS info** | `bool, char, float[3], char, float[3], int, float` | @ref ImageDecoderInfoTag::SetGPSInfo "SetGPSInfo" | @ref ImageDecoderInfoTag::GetGPSInfo "GetGPSInfo"
+ /// | **Camera model** | `std::string` | @ref ImageDecoderInfoTag::SetCameraModel "SetCameraModel" | @ref ImageDecoderInfoTag::GetCameraModel "GetCameraModel"
+ /// | **Author** | `std::string` | @ref ImageDecoderInfoTag::SetAuthor "SetAuthor" | @ref ImageDecoderInfoTag::GetAuthor "GetAuthor"
+ /// | **Description** | `std::string` | @ref ImageDecoderInfoTag::SetDescription "SetDescription" | @ref ImageDecoderInfoTag::GetDescription "GetDescription"
+ /// | **Copyright** | `std::string` | @ref ImageDecoderInfoTag::SetCopyright "SetCopyright" | @ref ImageDecoderInfoTag::GetCopyright "GetCopyright"
+
+ /// @addtogroup cpp_kodi_addon_imagedecoder_Defs_ImageDecoderInfoTag
+ ///@{
+
+ /// @brief Set the camera manufacturer as string on info tag.
+ void SetWidth(int width) { m_width = width; }
+
+ /// @brief Get image width as pixels
+ int GetWidth() const { return m_width; }
+
+ /// @brief Set the image height as pixels.
+ void SetHeight(int height) { m_height = height; }
+
+ /// @brief Get image height as pixels.
+ int GetHeight() const { return m_height; }
+
+ /// @brief Set the image distance in meters.
+ void SetDistance(int distance) { m_distance = distance; }
+
+ /// @brief Get mage distance in meters.
+ int GetDistance() const { return m_distance; }
+
+ /// @brief Set the image color type.
+ ///
+ /// @copydetails cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_COLOR
+ void SetColor(ADDON_IMG_COLOR color) { m_color = color; }
+
+ /// @brief Get image image color type.
+ ADDON_IMG_COLOR GetColor() const { return m_color; }
+
+ /// @brief Set metering mode.
+ ///
+ /// @copydetails cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_METERING_MODE
+ void SetMeteringMode(ADDON_IMG_METERING_MODE metering_mode) { m_metering_mode = metering_mode; }
+
+ /// @brief Get metering mode.
+ ADDON_IMG_METERING_MODE GetMeteringMode() const { return m_metering_mode; }
+
+ /// @brief Set exposure time.
+ void SetExposureTime(float exposure_time) { m_exposure_time = exposure_time; }
+
+ /// @brief Get exposure time.
+ float GetExposureTime() const { return m_exposure_time; }
+
+ /// @brief Set exposure bias.
+ void SetExposureBias(float exposure_bias) { m_exposure_bias = exposure_bias; }
+
+ /// @brief Get exposure bias.
+ float GetExposureBias() const { return m_exposure_bias; }
+
+ /// @brief Set Exposure program.
+ ///
+ /// @copydetails cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_EXPOSURE_PROGRAM
+ void SetExposureProgram(ADDON_IMG_EXPOSURE_PROGRAM exposure_program)
+ {
+ m_exposure_program = exposure_program;
+ }
+
+ /// @brief Get Exposure program.
+ ADDON_IMG_EXPOSURE_PROGRAM GetExposureProgram() const { return m_exposure_program; }
+
+ /// @brief Set Exposure mode.
+ ///
+ /// @copydetails cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_EXPOSURE_MODE
+ void SetExposureMode(ADDON_IMG_EXPOSURE_MODE exposure_mode) { m_exposure_mode = exposure_mode; }
+
+ /// @brief Get Exposure mode.
+ ADDON_IMG_EXPOSURE_MODE GetExposureMode() const { return m_exposure_mode; }
+
+ /// @brief Set the orientation.
+ ///
+ /// @copydetails cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_ORIENTATION
+ void SetOrientation(ADDON_IMG_ORIENTATION orientation) { m_orientation = orientation; }
+
+ /// @brief Get image orientation.
+ ADDON_IMG_ORIENTATION GetOrientation() const { return m_orientation; }
+
+ /// @brief Set the image creation time in time_t format (number of seconds elapsed since 00:00 hours, Jan 1, 1970 UTC).
+ void SetTimeCreated(time_t time_created) { m_time_created = time_created; }
+
+ /// @brief Get image creation time.
+ time_t GetTimeCreated() const { return m_time_created; }
+
+ /// @brief Set Aperture F number.
+ void SetApertureFNumber(float aperture_f_number) { m_aperture_f_number = aperture_f_number; }
+
+ /// @brief Get Aperture F number.
+ float GetApertureFNumber() const { return m_aperture_f_number; }
+
+ /// @brief Set to true if image was created with flash.
+ void SetFlashUsed(ADDON_IMG_FLASH_TYPE flash_used) { m_flash_used = flash_used; }
+
+ /// @brief Get info about image was created with flash.
+ ADDON_IMG_FLASH_TYPE GetFlashUsed() const { return m_flash_used; }
+
+ /// @brief Set focal length
+ void SetFocalLength(int focal_length) { m_focal_length = focal_length; }
+
+ /// @brief Get focal length
+ int GetFocalLength() const { return m_focal_length; }
+
+ /// @brief Set light source
+ void SetLightSource(ADDON_IMG_LIGHT_SOURCE light_source) { m_light_source = light_source; }
+
+ /// @brief Get light source
+ ADDON_IMG_LIGHT_SOURCE GetLightSource() const { return m_light_source; }
+
+ /// @brief Set focal length in 35 mm format.
+ ///
+ /// @note Same as FocalLengthIn35mmFilm in EXIF standard, tag 0xa405.
+ void SetFocalLengthIn35mmFormat(int focal_length_in_35mm_format)
+ {
+ m_focal_length_in_35mm_format = focal_length_in_35mm_format;
+ }
+
+ /// @brief Get focal length in 35 mm format.
+ int GetFocalLengthIn35mmFormat() const { return m_focal_length_in_35mm_format; }
+
+ /// @brief Set digital zoom ratio.
+ void SetDigitalZoomRatio(float digital_zoom_ratio) { m_digital_zoom_ratio = digital_zoom_ratio; }
+
+ /// @brief Get digital zoom ratio.
+ float GetDigitalZoomRatio() const { return m_digital_zoom_ratio; }
+
+ /// @brief Set ISO sensitivity.
+ void SetISOSpeed(float iso_speed) { m_iso_speed = iso_speed; }
+
+ /// @brief Get ISO sensitivity.
+ float GetISOSpeed() const { return m_iso_speed; }
+
+ /// @brief Set GPS position information.
+ void SetGPSInfo(bool gps_info_present,
+ char latitude_ref,
+ float latitude[3],
+ char longitude_ref,
+ float longitude[3],
+ int altitude_ref,
+ float altitude)
+ {
+ if (!latitude || !longitude)
+ return;
+
+ m_gps_info_present = gps_info_present;
+ if (gps_info_present)
+ {
+ m_latitude_ref = latitude_ref;
+ m_longitude_ref = longitude_ref;
+ for (int i = 0; i < 3; i++)
+ {
+ m_latitude[i] = latitude[i];
+ m_longitude[i] = longitude[i];
+ }
+ m_altitude_ref = altitude_ref;
+ m_altitude = altitude;
+ }
+ else
+ {
+ m_latitude_ref = 0.0f;
+ m_longitude_ref = 0.0f;
+ for (int i = 0; i < 3; i++)
+ latitude[i] = longitude[i] = 0.0f;
+ m_altitude_ref = 0;
+ m_altitude = 0.0f;
+ }
+ }
+
+ /// @brief Get GPS position information.
+ void GetGPSInfo(bool& gps_info_present,
+ char& latitude_ref,
+ float latitude[3],
+ char& longitude_ref,
+ float longitude[3],
+ int& altitude_ref,
+ float& altitude)
+ {
+ if (!latitude || !longitude)
+ return;
+
+ gps_info_present = m_gps_info_present;
+ if (m_gps_info_present)
+ {
+ latitude_ref = m_latitude_ref;
+ longitude_ref = m_longitude_ref;
+ for (int i = 0; i < 3; i++)
+ {
+ latitude[i] = m_latitude[i];
+ longitude[i] = m_longitude[i];
+ }
+ altitude_ref = m_altitude_ref;
+ altitude = m_altitude;
+ }
+ else
+ {
+ latitude_ref = ' ';
+ longitude_ref = ' ';
+ for (int i = 0; i < 3; i++)
+ latitude[i] = longitude[i] = 0.0f;
+ altitude_ref = 0;
+ altitude = 0.0f;
+ }
+ }
+
+ /// @brief Set the camera manufacturer as string on info tag.
+ void SetCameraManufacturer(const std::string& camera_manufacturer)
+ {
+ m_camera_manufacturer = camera_manufacturer;
+ }
+
+ /// @brief Get camera manufacturer
+ std::string GetCameraManufacturer() const { return m_camera_manufacturer; }
+
+ /// @brief Set camera model
+ void SetCameraModel(const std::string& camera_model) { m_camera_model = camera_model; }
+
+ /// @brief Get camera model
+ std::string GetCameraModel() const { return m_camera_model; }
+
+ /// @brief Set author
+ void SetAuthor(const std::string& author) { m_author = author; }
+
+ /// @brief Get author
+ std::string GetAuthor() const { return m_author; }
+
+ /// @brief Set description
+ void SetDescription(const std::string& description) { m_description = description; }
+
+ /// @brief Get description
+ std::string GetDescription() const { return m_description; }
+
+ /// @brief Set copyright
+ void SetCopyright(const std::string& copyright) { m_copyright = copyright; }
+
+ /// @brief Get copyright
+ std::string GetCopyright() const { return m_copyright; }
+
+ ///@}
+
+private:
+ int m_width{};
+ int m_height{};
+ float m_distance{};
+ ADDON_IMG_ORIENTATION m_orientation{ADDON_IMG_ORIENTATION_NONE};
+ ADDON_IMG_COLOR m_color{ADDON_IMG_COLOR_COLORED};
+ ADDON_IMG_METERING_MODE m_metering_mode{ADDON_IMG_METERING_MODE_UNKNOWN};
+ float m_exposure_time{};
+ float m_exposure_bias{};
+ ADDON_IMG_EXPOSURE_PROGRAM m_exposure_program{ADDON_IMG_EXPOSURE_PROGRAM_UNDEFINED};
+ ADDON_IMG_EXPOSURE_MODE m_exposure_mode{ADDON_IMG_EXPOSURE_MODE_AUTO};
+ time_t m_time_created{};
+ float m_aperture_f_number{};
+ ADDON_IMG_FLASH_TYPE m_flash_used{ADDON_IMG_FLASH_TYPE_NO_FLASH};
+ ADDON_IMG_LIGHT_SOURCE m_light_source{};
+ int m_focal_length{};
+ int m_focal_length_in_35mm_format{};
+ float m_digital_zoom_ratio{};
+ float m_iso_speed{};
+
+ bool m_gps_info_present{false};
+ char m_latitude_ref{' '};
+ float m_latitude[3]{}; /* Deg,min,sec */
+ char m_longitude_ref{' '};
+ float m_longitude[3]{}; /* Deg,min,sec */
+ int m_altitude_ref{};
+ float m_altitude{};
+
+ std::string m_camera_manufacturer;
+ std::string m_camera_model;
+ std::string m_author;
+ std::string m_description;
+ std::string m_copyright;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+///
+/// @addtogroup cpp_kodi_addon_imagedecoder
+/// @brief @cpp_class{ kodi::addon::CInstanceImageDecoder }
+/// **Image decoder add-on instance**\n
+/// This instance type is used to allow Kodi various additional image format
+/// types.
+///
+/// This usage can be requested under various conditions, by a Mimetype protocol
+/// defined in <b>`addon.xml`</b> or supported file extensions.
+///
+/// Include the header @ref ImageDecoder.h "#include <kodi/addon-instance/ImageDecoder.h>"
+/// to use this class.
+///
+/// ----------------------------------------------------------------------------
+///
+/// Here is an example of what the <b>`addon.xml.in`</b> would look like for an
+/// image decoder addon:
+///
+/// ~~~~~~~~~~~~~{.xml}
+/// <?xml version="1.0" encoding="UTF-8"?>
+/// <addon
+/// id="imagedecoder.myspecialnamefor"
+/// version="1.0.0"
+/// name="My image decoder addon"
+/// provider-name="Your Name">
+/// <requires>@ADDON_DEPENDS@</requires>
+/// <extension
+/// point="kodi.imagedecoder"
+/// library_@PLATFORM@="@LIBRARY_FILENAME@">
+/// <support>
+/// <mimetype name="image/mymimea">
+/// <extension>.imga</extension>
+/// <description>30100</description>
+/// <icon>resources/file_format_icon_a.png</icon>
+/// </mimetype>
+/// <mimetype name="image/mymimeb">
+/// <extension>.imgb</extension>
+/// <description>30101</description>
+/// <icon>resources/file_format_icon_b.png</icon>
+/// </mimetype>
+/// </support>
+/// </extension>
+/// <extension point="xbmc.addon.metadata">
+/// <summary lang="en_GB">My image decoder addon summary</summary>
+/// <description lang="en_GB">My image decoder description</description>
+/// <platform>@PLATFORM@</platform>
+/// </extension>
+/// </addon>
+/// ~~~~~~~~~~~~~
+///
+/// ### Standard values that can be declared for processing in `addon.xml`.
+///
+/// These values are used by Kodi to identify associated images and file
+/// extensions and then to select the associated addon.
+///
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`point`</b>,
+/// @anchor cpp_kodi_addon_imagedecoder_point
+/// string,
+/// The identification of the addon instance to image decoder is mandatory
+/// <b>`kodi.imagedecoder`</b>. In addition\, the instance declared in the
+/// first <b>`<support>`</b> is also the main type of addon.
+/// }
+/// \table_row3{ <b>`library_@PLATFORM@`</b>,
+/// @anchor cpp_kodi_addon_imagedecoder_library
+/// string,
+/// The runtime library used for the addon. This is usually declared by `cmake` and correctly displayed in the translated <b>`addon.xml`</b>.
+/// }
+/// \table_row3{ <b>`<support>...</support>`</b>,
+/// @anchor cpp_kodi_addon_imagedecoder_support
+/// XML group,
+/// Contains the formats supported by the addon.
+/// }
+/// \table_row3{ <b>`<mimetype name="image/mymimea">...</mimetype>`</b>,
+/// @anchor cpp_kodi_addon_imagedecoder_mimetype
+/// string / group,
+/// The from addon operated image [mimetypes](https://en.wikipedia.org/wiki/Media_type).\n
+/// Optional can be with `<description>` and `<icon>` additional info added where used for list views in Kodi.
+/// }
+/// \table_row3{ <b>`<mimetype ...><extension>...</extension></mimetype>`</b>,
+/// @anchor cpp_kodi_addon_imagedecoder_mimetype
+/// string,
+/// The file extensions / styles supported by this addon and relates to given mimetype before.\n
+/// @note Required to use about info support by @ref CInstanceImageDecoder::SupportsFile and @ref CInstanceImageDecoder::ReadTag!
+/// }
+/// \table_end
+///
+/// @remark For more detailed description of the <b>`addon.xml`</b>, see also https://kodi.wiki/view/Addon.xml.
+///
+///
+/// --------------------------------------------------------------------------
+///
+///
+/// **Example:**
+///
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/ImageDecoder.h>
+///
+/// class ATTR_DLL_LOCAL CMyImageDecoder : public kodi::addon::CInstanceImageDecoder
+/// {
+/// public:
+/// CMyImageDecoder(const kodi::addon::IInstanceInfo& instance);
+///
+/// bool LoadImageFromMemory(const uint8_t* buffer,
+/// size_t bufSize,
+/// unsigned int& width,
+/// unsigned int& height) override;
+///
+/// bool Decode(uint8_t* pixels,
+/// unsigned int width,
+/// unsigned int height,
+/// unsigned int pitch,
+/// ADDON_IMG_FMT format) override;
+///
+/// ...
+/// };
+///
+/// CMyImageDecoder::CMyImageDecoder(const kodi::addon::IInstanceInfo& instance)
+/// : CInstanceImageDecoder(instance)
+/// {
+/// ...
+/// }
+///
+/// bool CMyImageDecoder::LoadImageFromMemory(const uint8_t* buffer,
+/// size_t bufSize,
+/// unsigned int& width,
+/// unsigned int& height)
+/// {
+/// ...
+/// return true;
+/// }
+///
+/// bool CMyImageDecoder::Decode(uint8_t* pixels,
+/// unsigned int width,
+/// unsigned int height,
+/// unsigned int pitch,
+/// ADDON_IMG_FMT format) override;
+/// {
+/// ...
+/// return true;
+/// }
+///
+/// //----------------------------------------------------------------------
+///
+/// class ATTR_DLL_LOCAL CMyAddon : public kodi::addon::CAddonBase
+/// {
+/// public:
+/// CMyAddon() = default;
+/// ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl) override;
+/// };
+///
+/// // If you use only one instance in your add-on, can be instanceType and
+/// // instanceID ignored
+/// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl)
+/// {
+/// if (instance.IsType(ADDON_INSTANCE_IMAGEDECODER))
+/// {
+/// kodi::Log(ADDON_LOG_INFO, "Creating my image decoder instance");
+/// hdl = new CMyImageDecoder(instance);
+/// return ADDON_STATUS_OK;
+/// }
+/// else if (...)
+/// {
+/// ...
+/// }
+/// return ADDON_STATUS_UNKNOWN;
+/// }
+///
+/// ADDONCREATOR(CMyAddon)
+/// ~~~~~~~~~~~~~
+///
+/// The destruction of the example class `CMyImageDecoder` is called from
+/// Kodi's header. Manually deleting the add-on instance is not required.
+///
+//------------------------------------------------------------------------------
+class ATTR_DLL_LOCAL CInstanceImageDecoder : public IAddonInstance
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_imagedecoder
+ /// @brief Class constructor.
+ ///
+ /// @param[in] instance The from Kodi given instance given be add-on
+ /// CreateInstance call with instance id
+ /// @ref ADDON_INSTANCE_IMAGEDECODER.
+ ///
+ explicit CInstanceImageDecoder(const IInstanceInfo& instance) : IAddonInstance(instance)
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstanceImageDecoder: Creation of multiple together "
+ "with single instance way is not allowed!");
+
+ SetAddonStruct(instance);
+ }
+ //----------------------------------------------------------------------------
+
+ ~CInstanceImageDecoder() override = default;
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_imagedecoder
+ /// @brief Checks addon support given file path.
+ ///
+ /// @param[in] filename The file to read
+ /// @return true if successfully done and supported, otherwise false
+ ///
+ /// @note Optional to add, as default becomes `true` used.
+ ///
+ virtual bool SupportsFile(const std::string& filename) { return true; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_imagedecoder
+ /// @brief Read tag of a file.
+ ///
+ /// @param[in] file File to read tag for
+ /// @param[out] tag Information tag about
+ /// @return True on success, false on failure
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_imagedecoder_Defs_ImageDecoderInfoTag_Help
+ ///
+ virtual bool ReadTag(const std::string& file, kodi::addon::ImageDecoderInfoTag& tag)
+ {
+ return false;
+ }
+ //--------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_imagedecoder
+ /// @brief Initialize an encoder.
+ ///
+ /// @param[in] mimetype The mimetype wanted from Kodi
+ /// @param[in] buffer The data to read from memory
+ /// @param[in] bufSize The buffer size
+ /// @param[in,out] width The optimal width of image on entry, obtained width
+ /// on return
+ /// @param[in,out] height The optimal height of image, actual obtained height
+ /// on return
+ /// @return true if successful done, false on error
+ ///
+ virtual bool LoadImageFromMemory(const std::string& mimetype,
+ const uint8_t* buffer,
+ size_t bufSize,
+ unsigned int& width,
+ unsigned int& height) = 0;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_imagedecoder
+ /// @brief Decode previously loaded image.
+ ///
+ /// @param[in] pixels Output buffer
+ /// @param[in] width Width of output image
+ /// @param[in] height Height of output image
+ /// @param[in] pitch Pitch of output image
+ /// @param[in] format Format of output image
+ /// @return true if successful done, false on error
+ ///
+ virtual bool Decode(uint8_t* pixels,
+ unsigned int width,
+ unsigned int height,
+ unsigned int pitch,
+ ADDON_IMG_FMT format) = 0;
+ //----------------------------------------------------------------------------
+
+private:
+ void SetAddonStruct(KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ instance->hdl = this;
+ instance->imagedecoder->toAddon->supports_file = ADDON_supports_file;
+ instance->imagedecoder->toAddon->read_tag = ADDON_read_tag;
+ instance->imagedecoder->toAddon->load_image_from_memory = ADDON_load_image_from_memory;
+ instance->imagedecoder->toAddon->decode = ADDON_decode;
+ }
+
+ inline static bool ADDON_supports_file(const KODI_ADDON_IMAGEDECODER_HDL hdl, const char* file)
+ {
+ return static_cast<CInstanceImageDecoder*>(hdl)->SupportsFile(file);
+ }
+
+ inline static bool ADDON_read_tag(const KODI_ADDON_IMAGEDECODER_HDL hdl,
+ const char* file,
+ struct KODI_ADDON_IMAGEDECODER_INFO_TAG* tag)
+ {
+#ifdef _WIN32
+#pragma warning(push)
+#pragma warning(disable : 4996)
+#endif // _WIN32
+ kodi::addon::ImageDecoderInfoTag cppTag;
+ bool ret = static_cast<CInstanceImageDecoder*>(hdl)->ReadTag(file, cppTag);
+ if (ret)
+ {
+ tag->width = cppTag.GetWidth();
+ tag->height = cppTag.GetHeight();
+ tag->distance = cppTag.GetDistance();
+ tag->color = cppTag.GetColor();
+ tag->orientation = cppTag.GetOrientation();
+ tag->metering_mode = cppTag.GetMeteringMode();
+ tag->exposure_time = cppTag.GetExposureTime();
+ tag->exposure_bias = cppTag.GetExposureBias();
+ tag->exposure_mode = cppTag.GetExposureMode();
+ tag->exposure_program = cppTag.GetExposureProgram();
+ tag->time_created = cppTag.GetTimeCreated();
+ tag->aperture_f_number = cppTag.GetApertureFNumber();
+ tag->flash_used = cppTag.GetFlashUsed();
+ tag->light_source = cppTag.GetLightSource();
+ tag->focal_length = cppTag.GetFocalLength();
+ tag->focal_length_in_35mm_format = cppTag.GetFocalLengthIn35mmFormat();
+ tag->iso_speed = cppTag.GetISOSpeed();
+ tag->digital_zoom_ratio = cppTag.GetDigitalZoomRatio();
+ cppTag.GetGPSInfo(tag->gps_info_present, tag->latitude_ref, tag->latitude, tag->longitude_ref,
+ tag->longitude, tag->altitude_ref, tag->altitude);
+ tag->camera_manufacturer = strdup(cppTag.GetCameraManufacturer().c_str());
+ tag->camera_model = strdup(cppTag.GetCameraModel().c_str());
+ tag->author = strdup(cppTag.GetAuthor().c_str());
+ tag->description = strdup(cppTag.GetDescription().c_str());
+ tag->copyright = strdup(cppTag.GetCopyright().c_str());
+ }
+ return ret;
+#ifdef _WIN32
+#pragma warning(pop)
+#endif // _WIN32
+ }
+
+ inline static bool ADDON_load_image_from_memory(const KODI_ADDON_IMAGEDECODER_HDL hdl,
+ const char* mimetype,
+ const uint8_t* buffer,
+ size_t bufSize,
+ unsigned int* width,
+ unsigned int* height)
+ {
+ return static_cast<CInstanceImageDecoder*>(hdl)->LoadImageFromMemory(mimetype, buffer, bufSize,
+ *width, *height);
+ }
+
+ inline static bool ADDON_decode(const KODI_ADDON_IMAGEDECODER_HDL hdl,
+ uint8_t* pixels,
+ size_t pixels_size,
+ unsigned int width,
+ unsigned int height,
+ unsigned int pitch,
+ enum ADDON_IMG_FMT format)
+ {
+ return static_cast<CInstanceImageDecoder*>(hdl)->Decode(pixels, width, height, pitch, format);
+ }
+};
+
+} /* namespace addon */
+} /* namespace kodi */
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Inputstream.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Inputstream.h
new file mode 100644
index 0000000..0320074
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Inputstream.h
@@ -0,0 +1,2050 @@
+/*
+ * 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 "../AddonBase.h"
+#include "../c-api/addon-instance/inputstream.h"
+#include "inputstream/StreamCodec.h"
+#include "inputstream/StreamConstants.h"
+#include "inputstream/StreamCrypto.h"
+#include "inputstream/TimingConstants.h"
+
+#ifdef __cplusplus
+
+#include <map>
+
+namespace kodi
+{
+namespace addon
+{
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Doxygen group set for the definitions
+//{{{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_inputstream_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_addon_inputstream
+/// @brief **Inputstream add-on instance definition values**\n
+/// All inputstream functions associated data structures.
+///
+/// Used to exchange the available options between Kodi and addon.\n
+/// The groups described here correspond to the groups of functions on inputstream
+/// instance class.
+///
+/// In addition, some of the things described here are also used on the
+/// @ref cpp_kodi_addon_videocodec "video codec" addon.
+///
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_inputstream_Defs_Interface 1. Interface
+/// @ingroup cpp_kodi_addon_inputstream_Defs
+/// @brief **Inputstream add-on general variables**\n
+/// Used to exchange the available options between Kodi and addon.
+///
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_inputstream_Defs_StreamConstants 2. Stream constants
+/// @ingroup cpp_kodi_addon_inputstream_Defs
+/// @brief **Used to exchange any additional data between the caller and processor.**\n
+/// This includes the standardized values, in addition, an addon can also use
+/// its own special uses to be exchanged in the same way.
+///
+/// These values can be used by other addons (e.g. video addon) or stream files
+/// to select the respective inputstream addon and to transfer additional values.
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+///
+/// This example use the @ref STREAM_PROPERTY_INPUTSTREAM on a <b>`*.strm`</b>
+/// file to select needed addon and a additional value where related to selected
+/// addon itself.
+///
+/// ~~~~~~~~~~~~
+/// #KODIPROP:inputstream=inputstream.adaptive
+/// #KODIPROP:inputstream.adaptive.manifest_type=mpd
+/// http://rdmedia.bbc.co.uk/dash/ondemand/testcard/1/client_manifest-events-multilang.mpd
+/// ~~~~~~~~~~~~
+///
+/// These are then given to Kodi and the respectively selected addon by means of
+/// his @ref kodi::addon::CInstanceInputStream::Open "Open" call
+/// in @ref kodi::addon::InputstreamProperty::GetProperties.
+///
+/// The largest possible amount of these <b>`#KODIPROP`</b> values is defined
+/// with @ref STREAM_MAX_PROPERTY_COUNT.
+///
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_inputstream_Defs_TimingConstants 3. Stream timing
+/// @ingroup cpp_kodi_addon_inputstream_Defs
+/// @brief **Timebase and timestamp definitions.**\n
+/// Used to exchange the available options between Kodi and addon.
+///
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_inputstream_Defs_StreamCodec 4. Stream codec
+/// @ingroup cpp_kodi_addon_inputstream_Defs
+/// @brief **Inputstream codec control**\n
+/// Used to manage stream codec data.
+///
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_inputstream_Defs_StreamEncryption 5. Stream encryption
+/// @ingroup cpp_kodi_addon_inputstream_Defs
+/// @brief **Inputstream encryption control**\n
+/// Used to manage encrypted streams within addon.
+///
+
+//}}}
+//______________________________________________________________________________
+
+class CInstanceInputStream;
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_inputstream_Defs_InputstreamProperty class InputstreamProperty
+/// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+/// @brief <b>URL and Data of key/value pairs passed to addon on @ref kodi::addon::CInstanceInputStream::Open "Open".</b>\n
+/// This is used to have the necessary data of the stream to be opened.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_inputstream_Defs_InputstreamProperty_Help
+///
+/// @warning This data are only given from Kodi to addon and can't be used
+/// on other places on addon.
+///
+///@{
+class ATTR_DLL_LOCAL InputstreamProperty
+ : public CStructHdl<InputstreamProperty, INPUTSTREAM_PROPERTY>
+{
+ /*! \cond PRIVATE */
+ friend class CInstanceInputStream;
+ /*! \endcond */
+
+public:
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_InputstreamProperty_Help Value Help
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_InputstreamProperty
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_inputstream_Defs_InputstreamProperty :</b>
+ /// | Name | Type | Get call
+ /// |------|------|----------
+ /// | **Stream URL** | `std::string` | @ref InputstreamProperty::GetURL "GetURL"
+ /// | **Mime type** | `std::string` | @ref InputstreamProperty::GetMimeType "GetMimeType"
+ /// | **Available amount of properties** | `unsigned int` | @ref InputstreamProperty::GetPropertiesAmount "GetPropertiesAmount"
+ /// | **List of properties** | `std::map<std::string, std::string>` | @ref InputstreamProperty::GetProperties "GetProperties"
+ /// | **Get addon library folder** | `std::string` | @ref InputstreamProperty::GetLibFolder "GetLibFolder"
+ /// | **Get addon profile/user folder** | `std::string` | @ref InputstreamProperty::GetProfileFolder "GetProfileFolder"
+
+ /// @addtogroup cpp_kodi_addon_inputstream_Defs_InputstreamProperty
+ ///@{
+
+ /// @brief Stream URL to open.
+ std::string GetURL() const { return m_cStructure->m_strURL; }
+
+ /// @brief Stream mime type.
+ std::string GetMimeType() const { return m_cStructure->m_mimeType; }
+
+ /// @brief Amount of available properties.
+ unsigned int GetPropertiesAmount() const
+ {
+ return m_cStructure->m_nCountInfoValues;
+ }
+
+ /// @brief List of available properties-
+ const std::map<std::string, std::string> GetProperties() const
+ {
+ std::map<std::string, std::string> props;
+ for (unsigned int i = 0; i < m_cStructure->m_nCountInfoValues; ++i)
+ {
+ props.emplace(m_cStructure->m_ListItemProperties[i].m_strKey,
+ m_cStructure->m_ListItemProperties[i].m_strValue);
+ }
+ return props;
+ }
+
+ /// @brief Get addon library folder.
+ ///
+ /// @note As alternative can also @ref kodi::GetAddonPath used.
+ std::string GetLibFolder() const { return m_cStructure->m_libFolder; }
+
+ /// @brief Get addon profile/user folder.
+ ///
+ /// @note As alternative can also @ref kodi::GetBaseUserPath used.
+ std::string GetProfileFolder() const { return m_cStructure->m_profileFolder; }
+
+ ///@}
+
+private:
+ InputstreamProperty() = delete;
+ InputstreamProperty(const InputstreamProperty& stream) = delete;
+ InputstreamProperty(const INPUTSTREAM_PROPERTY* stream) : CStructHdl(stream) {}
+ InputstreamProperty(INPUTSTREAM_PROPERTY* stream) : CStructHdl(stream) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamCapabilities class InputstreamCapabilities
+/// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+/// @brief **InputStream add-on capabilities. All capabilities are set to "false" as default.**\n
+/// Asked to addon on @ref kodi::addon::CInstanceInputStream::GetCapabilities "GetCapabilities".
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_inputstream_Defs_Interface_InputstreamCapabilities_Help
+///
+///@{
+class ATTR_DLL_LOCAL InputstreamCapabilities
+ : public CStructHdl<InputstreamCapabilities, INPUTSTREAM_CAPABILITIES>
+{
+ /*! \cond PRIVATE */
+ friend class CInstanceInputStream;
+ /*! \endcond */
+
+public:
+ /*! \cond PRIVATE */
+ InputstreamCapabilities() = default;
+ InputstreamCapabilities(const InputstreamCapabilities& stream) : CStructHdl(stream) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamCapabilities_Help Value Help
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamCapabilities
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_inputstream_Defs_Interface_InputstreamCapabilities :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Capabilities bit mask** | `uint32_t` | @ref InputstreamCapabilities::SetMask "SetMask" | @ref InputstreamCapabilities::GetMask "GetMask"
+
+ /// @addtogroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamCapabilities
+ ///@{
+
+ /// @brief Set of supported capabilities.
+ void SetMask(uint32_t mask) const { m_cStructure->m_mask = mask; }
+
+ /// @brief Get of supported capabilities.
+ uint32_t GetMask() const { return m_cStructure->m_mask; }
+
+ ///@}
+
+private:
+ InputstreamCapabilities(const INPUTSTREAM_CAPABILITIES* stream) : CStructHdl(stream) {}
+ InputstreamCapabilities(INPUTSTREAM_CAPABILITIES* stream) : CStructHdl(stream) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamMasteringMetadata class InputstreamMasteringMetadata
+/// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+/// @brief **Mastering metadata.**\n
+/// Describes the metadata for [HDR10](https://en.wikipedia.org/wiki/High-dynamic-range_video).
+///
+/// Used when video is compressed using [High Efficiency Video Coding (HEVC)](https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding).
+/// This is used to describe the capabilities of the display used to master the
+/// content and the luminance values of the content.
+///
+/// Used on @ref kodi::addon::InputstreamInfo::SetMasteringMetadata and @ref kodi::addon::InputstreamInfo::GetMasteringMetadata.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_inputstream_Defs_Interface_InputstreamMasteringMetadata_Help
+///
+///@{
+class ATTR_DLL_LOCAL InputstreamMasteringMetadata
+ : public CStructHdl<InputstreamMasteringMetadata, INPUTSTREAM_MASTERING_METADATA>
+{
+ /*! \cond PRIVATE */
+ friend class CInstanceInputStream;
+ friend class InputstreamInfo;
+ /*! \endcond */
+
+public:
+ /*! \cond PRIVATE */
+ InputstreamMasteringMetadata() = default;
+ InputstreamMasteringMetadata(const InputstreamMasteringMetadata& stream) : CStructHdl(stream) {}
+ InputstreamMasteringMetadata& operator=(const InputstreamMasteringMetadata&) = default;
+
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamMasteringMetadata_Help Value Help
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamMasteringMetadata
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_inputstream_Defs_Interface_InputstreamMasteringMetadata :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Chromaticity X coordinates of the red** | `double` | @ref InputstreamMasteringMetadata::SetPrimaryR_ChromaticityX "SetPrimaryR_ChromaticityX" | @ref InputstreamMasteringMetadata::GetPrimaryR_ChromaticityX "GetPrimaryR_ChromaticityX"
+ /// | **Chromaticity Y coordinates of the red** | `double` | @ref InputstreamMasteringMetadata::SetPrimaryR_ChromaticityY "SetPrimaryR_ChromaticityY" | @ref InputstreamMasteringMetadata::GetPrimaryR_ChromaticityY "GetPrimaryR_ChromaticityY"
+ /// | **Chromaticity X coordinates of the green** | `double` | @ref InputstreamMasteringMetadata::SetPrimaryG_ChromaticityX "SetPrimaryG_ChromaticityX" | @ref InputstreamMasteringMetadata::GetPrimaryG_ChromaticityX "GetPrimaryG_ChromaticityX"
+ /// | **Chromaticity Y coordinates of the green** | `double` | @ref InputstreamMasteringMetadata::SetPrimaryG_ChromaticityY "SetPrimaryG_ChromaticityY" | @ref InputstreamMasteringMetadata::GetPrimaryG_ChromaticityY "GetPrimaryG_ChromaticityY"
+ /// | **Chromaticity X coordinates of the blue** | `double` | @ref InputstreamMasteringMetadata::SetPrimaryB_ChromaticityX "SetPrimaryB_ChromaticityX" | @ref InputstreamMasteringMetadata::GetPrimaryB_ChromaticityX "GetPrimaryB_ChromaticityX"
+ /// | **Chromaticity Y coordinates of the blue** | `double` | @ref InputstreamMasteringMetadata::SetPrimaryB_ChromaticityY "SetPrimaryB_ChromaticityY" | @ref InputstreamMasteringMetadata::GetPrimaryB_ChromaticityY "GetPrimaryB_ChromaticityY"
+ /// | **Chromaticity X coordinates of the white point** | `double` | @ref InputstreamMasteringMetadata::SetWhitePoint_ChromaticityX "SetWhitePoint_ChromaticityX" | @ref InputstreamMasteringMetadata::GetWhitePoint_ChromaticityX "GetWhitePoint_ChromaticityX"
+ /// | **Chromaticity Y coordinates of the white point** | `double` | @ref InputstreamMasteringMetadata::SetWhitePoint_ChromaticityY "SetWhitePoint_ChromaticityY" | @ref InputstreamMasteringMetadata::GetWhitePoint_ChromaticityY "GetWhitePoint_ChromaticityY"
+ /// | **Maximum number of bits of the display** | `double` | @ref InputstreamMasteringMetadata::SetLuminanceMax "SetLuminanceMax" | @ref InputstreamMasteringMetadata::GetLuminanceMax "GetLuminanceMax"
+ /// | **Minimum number of bits of the display** | `double` | @ref InputstreamMasteringMetadata::SetLuminanceMin "SetLuminanceMin" | @ref InputstreamMasteringMetadata::GetLuminanceMin "GetLuminanceMin"
+
+ /// @addtogroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamMasteringMetadata
+ ///@{
+
+ /// @brief Metadata class compare.
+ ///
+ /// To compare the metadata with another one.
+ ///
+ /// @return true if they equal, false otherwise
+ bool operator==(const kodi::addon::InputstreamMasteringMetadata& right) const
+ {
+ if (memcmp(m_cStructure, right.m_cStructure, sizeof(INPUTSTREAM_MASTERING_METADATA)) == 0)
+ return true;
+ return false;
+ }
+
+ /// @brief Set the chromaticity coordinates of the red value in the
+ /// [CIE1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space.
+ ///
+ /// X coordinate. The values are normalized to 50,000.
+ void SetPrimaryR_ChromaticityX(double value) { m_cStructure->primary_r_chromaticity_x = value; }
+
+ /// @brief Get the chromaticity X coordinates of the red value.
+ double GetPrimaryR_ChromaticityX() { return m_cStructure->primary_r_chromaticity_x; }
+
+ /// @brief The chromaticity coordinates of the red value in the
+ /// [CIE1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space.
+ ///
+ /// Y coordinate. The values are normalized to 50,000.
+ void SetPrimaryR_ChromaticityY(double value) { m_cStructure->primary_r_chromaticity_y = value; }
+
+ /// @brief Get the chromaticity Y coordinates of the red value.
+ double GetPrimaryR_ChromaticityY() { return m_cStructure->primary_r_chromaticity_y; }
+
+ /// @brief Set the chromaticity coordinates of the green value in the
+ /// [CIE1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space.
+ ///
+ /// X coordinate. The values are normalized to 50,000.
+ void SetPrimaryG_ChromaticityX(double value) { m_cStructure->primary_g_chromaticity_x = value; }
+
+ /// @brief Get the chromaticity X coordinates of the green value.
+ double GetPrimaryG_ChromaticityX() { return m_cStructure->primary_g_chromaticity_x; }
+
+ /// @brief Set the chromaticity coordinates of the green value in the
+ /// [CIE1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space.
+ ///
+ /// Y coordinate. The values are normalized to 50,000.
+ void SetPrimaryG_ChromaticityY(double value) { m_cStructure->primary_g_chromaticity_y = value; }
+
+ /// @brief Get the chromaticity Y coordinates of the green value.
+ double GetPrimaryG_ChromaticityY() { return m_cStructure->primary_g_chromaticity_y; }
+
+ /// @brief The chromaticity coordinates of the blue value in the
+ /// [CIE1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space.
+ ///
+ /// X coordinate. The values are normalized to 50,000.
+ void SetPrimaryB_ChromaticityX(double value) { m_cStructure->primary_b_chromaticity_x = value; }
+
+ /// @brief Get the chromaticity X coordinates of the blue value.
+ double GetPrimaryB_ChromaticityX() { return m_cStructure->primary_b_chromaticity_x; }
+
+ /// @brief The chromaticity coordinates of the blue value in the
+ /// [CIE1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space.
+ ///
+ /// Y coordinate. The values are normalized to 50,000.
+ void SetPrimaryB_ChromaticityY(double value) { m_cStructure->primary_b_chromaticity_y = value; }
+
+ /// @brief Get the chromaticity Y coordinates of the blue value.
+ double GetPrimaryB_ChromaticityY() { return m_cStructure->primary_b_chromaticity_y; }
+
+ /// @brief Set the chromaticity coordinates of the white point in the
+ /// [CIE1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space.
+ ///
+ /// X coordinate. The values are normalized to 50,000.
+ void SetWhitePoint_ChromaticityX(double value)
+ {
+ m_cStructure->white_point_chromaticity_x = value;
+ }
+
+ /// @brief Get the chromaticity X coordinates of the white point
+ double GetWhitePoint_ChromaticityX() { return m_cStructure->white_point_chromaticity_x; }
+
+ /// @brief Set the chromaticity coordinates of the white point in the
+ /// [CIE1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space.
+ ///
+ /// Y coordinate. The values are normalized to 50,000.
+ void SetWhitePoint_ChromaticityY(double value)
+ {
+ m_cStructure->white_point_chromaticity_y = value;
+ }
+
+ /// @brief Get the chromaticity Y coordinates of the white point.
+ double GetWhitePoint_ChromaticityY() { return m_cStructure->white_point_chromaticity_y; }
+
+ /// @brief Set the maximum number of bits of the display used to master the content.
+ ///
+ /// Values are normalized to 10,000.
+ void SetLuminanceMax(double value) { m_cStructure->luminance_max = value; }
+
+ /// @brief Get the maximum number of bits of the display.
+ double GetLuminanceMax() { return m_cStructure->luminance_max; }
+
+ /// @brief Set the minimum number of bits of the display used to master the content.
+ ///
+ /// Values are normalized to 10,000.
+ void SetLuminanceMin(double value) { m_cStructure->luminance_min = value; }
+
+ /// @brief Get the minimum number of bits of the display.
+ double GetLuminanceMin() { return m_cStructure->luminance_min; }
+
+ ///@}
+
+private:
+ InputstreamMasteringMetadata(const INPUTSTREAM_MASTERING_METADATA* stream) : CStructHdl(stream) {}
+ InputstreamMasteringMetadata(INPUTSTREAM_MASTERING_METADATA* stream) : CStructHdl(stream) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamContentlightMetadata class InputstreamContentlightMetadata
+/// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+/// @brief **Contentlight metadata**\n
+/// Describes the metadata for [HDR10](https://en.wikipedia.org/wiki/High-dynamic-range_video).
+/// See also @ref cpp_kodi_addon_inputstream_Defs_Interface_InputstreamMasteringMetadata "InputstreamMasteringMetadata".
+///
+/// Used on @ref kodi::addon::InputstreamInfo::SetContentLightMetadata and @ref kodi::addon::InputstreamInfo::GetContentLightMetadata.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_inputstream_Defs_Interface_InputstreamContentlightMetadata_Help
+///
+///@{
+class ATTR_DLL_LOCAL InputstreamContentlightMetadata
+ : public CStructHdl<InputstreamContentlightMetadata, INPUTSTREAM_CONTENTLIGHT_METADATA>
+{
+ /*! \cond PRIVATE */
+ friend class CInstanceInputStream;
+ friend class InputstreamInfo;
+ /*! \endcond */
+
+public:
+ /*! \cond PRIVATE */
+ InputstreamContentlightMetadata() = default;
+ InputstreamContentlightMetadata(const InputstreamContentlightMetadata& stream)
+ : CStructHdl(stream)
+ {
+ }
+ InputstreamContentlightMetadata& operator=(const InputstreamContentlightMetadata&) = default;
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamContentlightMetadata_Help Value Help
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamContentlightMetadata
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_inputstream_Defs_Interface_InputstreamContentlightMetadata :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Maximum content light level** | `double` | @ref InputstreamContentlightMetadata::SetMaxCll "SetMaxCll" | @ref InputstreamContentlightMetadata::GetMaxCll "GetMaxCll"
+ /// | **Maximum frame average light level** | `double` | @ref InputstreamContentlightMetadata::SetMaxFall "SetMaxFall" | @ref InputstreamContentlightMetadata::GetMaxFall "GetMaxFall"
+
+ /// @addtogroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamContentlightMetadata
+ ///@{
+
+ /// @brief Metadata class compare.
+ ///
+ /// To compare the metadata with another one.
+ ///
+ /// @return true if they equal, false otherwise
+ bool operator==(const kodi::addon::InputstreamContentlightMetadata& right) const
+ {
+ if (memcmp(m_cStructure, right.m_cStructure, sizeof(INPUTSTREAM_CONTENTLIGHT_METADATA)) == 0)
+ return true;
+ return false;
+ }
+
+ /// @brief Set the maximum content light level (MaxCLL).
+ ///
+ /// This is the bit value corresponding to the brightest pixel used anywhere
+ /// in the content.
+ void SetMaxCll(uint64_t value) { m_cStructure->max_cll = value; }
+
+ /// @brief Get the maximum content light level (MaxCLL).
+ uint64_t GetMaxCll() { return m_cStructure->max_cll; }
+
+ /// @brief Set the maximum frame average light level (MaxFALL).
+ ///
+ /// This is the bit value corresponding to the average luminance of the frame
+ /// which has the brightest average luminance anywhere in the content.
+ void SetMaxFall(uint64_t value) { m_cStructure->max_fall = value; }
+
+ /// @brief Get the maximum frame average light level (MaxFALL).
+ uint64_t GetMaxFall() { return m_cStructure->max_fall; }
+
+ ///@}
+
+private:
+ InputstreamContentlightMetadata(const INPUTSTREAM_CONTENTLIGHT_METADATA* stream)
+ : CStructHdl(stream)
+ {
+ }
+ InputstreamContentlightMetadata(INPUTSTREAM_CONTENTLIGHT_METADATA* stream) : CStructHdl(stream) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamInfo class InputstreamInfo
+/// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+/// @brief **Inputstream add-on stream info**\n
+/// This is used to give Kodi the associated and necessary data for an open stream.
+///
+/// Used on @ref kodi::addon::CInstanceInputStream::GetStream().
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_inputstream_Defs_Interface_InputstreamInfo_Help
+///
+///@{
+class ATTR_DLL_LOCAL InputstreamInfo : public CStructHdl<InputstreamInfo, INPUTSTREAM_INFO>
+{
+ /*! \cond PRIVATE */
+ friend class CInstanceInputStream;
+ /*! \endcond */
+
+public:
+ /*! \cond PRIVATE */
+ InputstreamInfo() = default;
+ InputstreamInfo(const InputstreamInfo& stream) : CStructHdl(stream)
+ {
+ SetCryptoSession(stream.GetCryptoSession());
+ CopyExtraData();
+ }
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamInfo_Help Value Help
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamInfo
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_inputstream_Defs_Interface_InputstreamInfo :</b>
+ /// | Name | Type used | Required | Set call | Get call
+ /// |------|-----------|----------|----------|---------
+ /// | **Stream type** | all | yes | @ref InputstreamInfo::SetStreamType "SetStreamType" | @ref InputstreamInfo::GetStreamType "GetStreamType"
+ /// | **Feature flags** | all | yes | @ref InputstreamInfo::SetFeatures "SetFeatures" | @ref InputstreamInfo::GetFeatures "GetFeatures"
+ /// | **Flags** | all | yes | @ref InputstreamInfo::SetFlags "SetFlags" | @ref InputstreamInfo::GetFlags "GetFlags"
+ /// | **Name** | all | no | @ref InputstreamInfo::SetName "SetName" | @ref InputstreamInfo::GetName "GetName"
+ /// | **Codec name** | all | yes | @ref InputstreamInfo::SetCodecName "SetCodecName" | @ref InputstreamInfo::GetCodecName "GetCodecName"
+ /// | **Codec internal name** | all | no | @ref InputstreamInfo::SetCodecInternalName "SetCodecInternalName" | @ref InputstreamInfo::GetCodecInternalName "GetCodecInternalName"
+ /// | **Codec Profile** | all | no | @ref InputstreamInfo::SetCodecProfile "SetCodecProfile" | @ref InputstreamInfo::GetCodecProfile "GetCodecProfile"
+ /// | **Physical index** | all | yes | @ref InputstreamInfo::SetPhysicalIndex "SetPhysicalIndex" | @ref InputstreamInfo::GetPhysicalIndex "GetPhysicalIndex"
+ /// | **Extra data** | Subtitle / all | Type related required | @ref InputstreamInfo::SetExtraData "SetExtraData" | @ref InputstreamInfo::GetExtraData "GetExtraData"
+ /// | **RFC 5646 language code** | all | no | @ref InputstreamInfo::SetLanguage "SetLanguage" | @ref InputstreamInfo::GetLanguage "GetLanguage"
+ /// | **FPS scale** | Video | Type related required | @ref InputstreamInfo::SetFpsScale "SetFpsScale" | @ref InputstreamInfo::GetFpsScale "GetFpsScale"
+ /// | **FPS rate** | Video | Type related required | @ref InputstreamInfo::SetFpsRate "SetFpsRate" | @ref InputstreamInfo::GetFpsRate "GetFpsRate"
+ /// | **Height** | Video | Type related required | @ref InputstreamInfo::SetHeight "SetHeight" | @ref InputstreamInfo::GetHeight "GetHeight"
+ /// | **Width** | Video | Type related required | @ref InputstreamInfo::SetWidth "SetWidth" | @ref InputstreamInfo::GetWidth "GetWidth"
+ /// | **Aspect** | Video | Type related required | @ref InputstreamInfo::SetAspect "SetAspect" | @ref InputstreamInfo::GetAspect "GetAspect"
+ /// | **Channel quantity** | Audio | Type related required | @ref InputstreamInfo::SetChannels "SetChannels" | @ref InputstreamInfo::GetChannels "GetChannels"
+ /// | **Sample rate** | Audio | Type related required | @ref InputstreamInfo::SetSampleRate "SetSampleRate" | @ref InputstreamInfo::GetSampleRate "GetSampleRate"
+ /// | **Bit rate** | Audio | Type related required | @ref InputstreamInfo::SetBitRate "SetBitRate" | @ref InputstreamInfo::GetBitRate "GetBitRate"
+ /// | **Bits per sample** | Audio | Type related required | @ref InputstreamInfo::SetBitsPerSample "SetBitsPerSample" | @ref InputstreamInfo::GetBitsPerSample "GetBitsPerSample"
+ /// | **Block align** | | no | @ref InputstreamInfo::SetBlockAlign "SetBlockAlign" | @ref InputstreamInfo::GetBlockAlign "GetBlockAlign"
+ /// | **Crypto session info** | | no | @ref InputstreamInfo::SetCryptoSession "SetCryptoSession" | @ref InputstreamInfo::GetCryptoSession "GetCryptoSession"
+ /// | **Four CC code** | | no | @ref InputstreamInfo::SetCodecFourCC "SetCodecFourCC" | @ref InputstreamInfo::GetCodecFourCC "GetCodecFourCC"
+ /// | **Color space** | | no | @ref InputstreamInfo::SetColorSpace "SetColorSpace" | @ref InputstreamInfo::GetColorSpace "GetColorSpace"
+ /// | **Color range** | | no | @ref InputstreamInfo::SetColorRange "SetColorRange" | @ref InputstreamInfo::GetColorRange "GetColorRange"
+ /// | **Color primaries** | | no | @ref InputstreamInfo::SetColorPrimaries "SetColorPrimaries" | @ref InputstreamInfo::GetColorPrimaries "GetColorPrimaries"
+ /// | **Color transfer characteristic** | | no | @ref InputstreamInfo::SetColorTransferCharacteristic "SetColorTransferCharacteristic" | @ref InputstreamInfo::GetColorTransferCharacteristic "GetColorTransferCharacteristic"
+ /// | **Mastering metadata** | | no | @ref InputstreamInfo::SetMasteringMetadata "SetMasteringMetadata" | @ref InputstreamInfo::GetMasteringMetadata "GetMasteringMetadata"
+ /// | **Content light metadata** | | no | @ref InputstreamInfo::SetContentLightMetadata "SetContentLightMetadata" | @ref InputstreamInfo::GetContentLightMetadata "GetContentLightMetadata"
+ ///
+
+ /// @addtogroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamInfo
+ ///@{
+
+ /// @brief Set the wanted stream type.
+ ///
+ /// @param[in] streamType By @ref INPUTSTREAM_TYPE defined type
+ void SetStreamType(INPUTSTREAM_TYPE streamType) { m_cStructure->m_streamType = streamType; }
+
+ /// @brief To get with @ref SetStreamType changed values.
+ INPUTSTREAM_TYPE GetStreamType() const { return m_cStructure->m_streamType; }
+
+ /// @brief Set special supported feature flags of inputstream.
+ ///
+ /// @param[in] features By @ref INPUTSTREAM_CODEC_FEATURES defined type
+ void SetFeatures(uint32_t features) { m_cStructure->m_features = features; }
+
+ /// @brief To get with @ref SetFeatures changed values.
+ uint32_t GetFeatures() const { return m_cStructure->m_features; }
+
+ /// @brief Set supported flags of inputstream.
+ ///
+ /// @param[in] flags The on @ref INPUTSTREAM_FLAGS defined flags to set
+ void SetFlags(uint32_t flags) { m_cStructure->m_flags = flags; }
+
+ /// @brief To get with @ref SetFeatures changed values.
+ uint32_t GetFlags() const { return m_cStructure->m_flags; }
+
+ /// @brief (optional) Name of the stream, leave empty for default handling.
+ ///
+ /// @param[in] name Stream name
+ void SetName(const std::string& name)
+ {
+ strncpy(m_cStructure->m_name, name.c_str(), INPUTSTREAM_MAX_STRING_NAME_SIZE);
+ }
+
+ /// @brief To get with @ref SetName changed values.
+ std::string GetName() const { return m_cStructure->m_name; }
+
+ /// @brief (required) Name of codec according to ffmpeg.
+ ///
+ /// See https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/codec_desc.c about
+ /// available names.
+ ///
+ /// @remark On @ref INPUTSTREAM_TYPE_TELETEXT, @ref INPUTSTREAM_TYPE_RDS, and
+ /// @ref INPUTSTREAM_TYPE_ID3 this can be ignored and leaved empty.
+ ///
+ /// @param[in] codeName Codec name
+ void SetCodecName(const std::string& codecName)
+ {
+ strncpy(m_cStructure->m_codecName, codecName.c_str(), INPUTSTREAM_MAX_STRING_CODEC_SIZE);
+ }
+
+ /// @brief To get with @ref SetCodecName changed values.
+ std::string GetCodecName() const { return m_cStructure->m_codecName; }
+
+ /// @brief (optional) Internal name of codec (selectionstream info).
+ ///
+ /// @param[in] codecName Internal codec name
+ void SetCodecInternalName(const std::string& codecName)
+ {
+ strncpy(m_cStructure->m_codecInternalName, codecName.c_str(),
+ INPUTSTREAM_MAX_STRING_CODEC_SIZE);
+ }
+
+ /// @brief To get with @ref SetCodecInternalName changed values.
+ std::string GetCodecInternalName() const { return m_cStructure->m_codecInternalName; }
+
+ /// @brief (optional) The profile of the codec.
+ ///
+ /// @param[in] codecProfile Values with @ref STREAMCODEC_PROFILE to use
+ void SetCodecProfile(STREAMCODEC_PROFILE codecProfile)
+ {
+ m_cStructure->m_codecProfile = codecProfile;
+ }
+
+ /// @brief To get with @ref SetCodecProfile changed values.
+ STREAMCODEC_PROFILE GetCodecProfile() const { return m_cStructure->m_codecProfile; }
+
+ /// @brief (required) Physical index.
+ ///
+ /// @param[in] id Index identifier
+ void SetPhysicalIndex(unsigned int id) { m_cStructure->m_pID = id; }
+
+ /// @brief To get with @ref SetPhysicalIndex changed values.
+ unsigned int GetPhysicalIndex() const { return m_cStructure->m_pID; }
+
+ /// @brief Additional data where can needed on streams.
+ ///
+ /// @param[in] extraData List with memory of extra data
+ void SetExtraData(const std::vector<uint8_t>& extraData)
+ {
+ m_extraData = extraData;
+ m_cStructure->m_ExtraData = m_extraData.data();
+ m_cStructure->m_ExtraSize = m_extraData.size();
+ }
+
+ /// @brief Additional data where can needed on streams.
+ ///
+ /// @param[in] extraData Pointer with memory of extra data
+ /// @param[in] extraSize Size to store
+ void SetExtraData(const uint8_t* extraData, size_t extraSize)
+ {
+ m_extraData.clear();
+ if (extraData && extraSize > 0)
+ {
+ for (size_t i = 0; i < extraSize; ++i)
+ m_extraData.emplace_back(extraData[i]);
+ }
+
+ m_cStructure->m_ExtraData = m_extraData.data();
+ m_cStructure->m_ExtraSize = m_extraData.size();
+ }
+
+ /// @brief To get with @ref SetExtraData changed values.
+ const std::vector<uint8_t>& GetExtraData() { return m_extraData; }
+
+ /// @brief To get size with @ref SetExtraData changed values.
+ size_t GetExtraDataSize() { return m_extraData.size(); }
+
+ /// @brief Compare extra data from outside with class
+ ///
+ /// @param[in] extraData Pointer with memory of extra data for compare
+ /// @param[in] extraSize Size to compare
+ /// @return true if they equal, false otherwise
+ bool CompareExtraData(const uint8_t* extraData, size_t extraSize) const
+ {
+ if (!extraData || m_extraData.size() != extraSize)
+ return false;
+ for (size_t i = 0; i < extraSize; ++i)
+ {
+ if (m_extraData[i] != extraData[i])
+ return false;
+ }
+ return true;
+ }
+
+ /// @brief Clear additional data.
+ void ClearExtraData()
+ {
+ m_extraData.clear();
+ m_cStructure->m_ExtraData = m_extraData.data();
+ m_cStructure->m_ExtraSize = m_extraData.size();
+ }
+
+ /// @brief RFC 5646 language code (empty string if undefined).
+ ///
+ /// @param[in] language The language to set
+ void SetLanguage(const std::string& language)
+ {
+ strncpy(m_cStructure->m_language, language.c_str(), INPUTSTREAM_MAX_STRING_LANGUAGE_SIZE);
+ }
+
+ /// @brief To get with @ref SetLanguage changed values.
+ std::string GetLanguage() const { return m_cStructure->m_language; }
+
+ /// @brief Scale of 1000 and a rate of 29970 will result in 29.97 fps.
+ ///
+ /// @param[in] fpsScale Scale rate
+ void SetFpsScale(unsigned int fpsScale) { m_cStructure->m_FpsScale = fpsScale; }
+
+ /// @brief To get with @ref SetFpsScale changed values.
+ unsigned int GetFpsScale() const { return m_cStructure->m_FpsScale; }
+
+ /// @brief Rate to use for stream.
+ ///
+ /// @param[in] fpsRate Rate to use
+ void SetFpsRate(unsigned int fpsRate) { m_cStructure->m_FpsRate = fpsRate; }
+
+ /// @brief To get with @ref SetFpsRate changed values.
+ unsigned int GetFpsRate() const { return m_cStructure->m_FpsRate; }
+
+ /// @brief Height of the stream reported by the demuxer.
+ ///
+ /// @param[in] height Height to use
+ void SetHeight(unsigned int height) { m_cStructure->m_Height = height; }
+
+ /// @brief To get with @ref SetHeight changed values.
+ unsigned int GetHeight() const { return m_cStructure->m_Height; }
+
+ /// @brief Width of the stream reported by the demuxer.
+ ///
+ /// @param[in] width Width to use
+ void SetWidth(unsigned int width) { m_cStructure->m_Width = width; }
+
+ /// @brief To get with @ref SetWidth changed values.
+ unsigned int GetWidth() const { return m_cStructure->m_Width; }
+
+ /// @brief Display aspect of stream.
+ ///
+ /// @param[in] aspect Aspect ratio to use
+ void SetAspect(float aspect) { m_cStructure->m_Aspect = aspect; }
+
+ /// @brief To get with @ref SetAspect changed values.
+ float GetAspect() const { return m_cStructure->m_Aspect; }
+
+ /// @brief (required) Amount of channels.
+ ///
+ /// @param[in] sampleRate Channels to use
+ void SetChannels(unsigned int channels) { m_cStructure->m_Channels = channels; }
+
+ /// @brief To get with @ref SetChannels changed values.
+ unsigned int GetChannels() const { return m_cStructure->m_Channels; }
+
+ /// @brief (required) Sample rate.
+ ///
+ /// @param[in] sampleRate Rate to use
+ void SetSampleRate(unsigned int sampleRate) { m_cStructure->m_SampleRate = sampleRate; }
+
+ /// @brief To get with @ref SetSampleRate changed values.
+ unsigned int GetSampleRate() const { return m_cStructure->m_SampleRate; }
+
+ /// @brief Bit rate.
+ ///
+ /// @param[in] bitRate Rate to use
+ void SetBitRate(unsigned int bitRate) { m_cStructure->m_BitRate = bitRate; }
+
+ /// @brief To get with @ref SetBitRate changed values.
+ unsigned int GetBitRate() const { return m_cStructure->m_BitRate; }
+
+ /// @brief (required) Bits per sample.
+ ///
+ /// @param[in] bitsPerSample Bits per sample to use
+ void SetBitsPerSample(unsigned int bitsPerSample)
+ {
+ m_cStructure->m_BitsPerSample = bitsPerSample;
+ }
+
+ /// @brief To get with @ref SetBitsPerSample changed values.
+ unsigned int GetBitsPerSample() const { return m_cStructure->m_BitsPerSample; }
+
+ /// @brief To set the necessary stream block alignment size.
+ ///
+ /// @param[in] blockAlign Block size in byte
+ void SetBlockAlign(unsigned int blockAlign) { m_cStructure->m_BlockAlign = blockAlign; }
+
+ /// @brief To get with @ref SetBlockAlign changed values.
+ unsigned int GetBlockAlign() const { return m_cStructure->m_BlockAlign; }
+
+ /// @brief To set stream crypto session information.
+ ///
+ /// @param[in] cryptoSession The with @ref cpp_kodi_addon_inputstream_Defs_Interface_StreamCryptoSession setable info
+ ///
+ void SetCryptoSession(const kodi::addon::StreamCryptoSession& cryptoSession)
+ {
+ m_cryptoSession = cryptoSession;
+ memcpy(&m_cStructure->m_cryptoSession, m_cryptoSession.GetCStructure(),
+ sizeof(STREAM_CRYPTO_SESSION));
+ }
+
+ /// @brief To get with @ref GetCryptoSession changed values.
+ const kodi::addon::StreamCryptoSession& GetCryptoSession() const { return m_cryptoSession; }
+
+ /// @brief Codec If available, the fourcc code codec.
+ ///
+ /// @param[in] codecFourCC Codec four CC code
+ void SetCodecFourCC(unsigned int codecFourCC) { m_cStructure->m_codecFourCC = codecFourCC; }
+
+ /// @brief To get with @ref SetCodecFourCC changed values
+ unsigned int GetCodecFourCC() const { return m_cStructure->m_codecFourCC; }
+
+ /// @brief Definition of colorspace.
+ ///
+ /// @param[in] colorSpace The with @ref INPUTSTREAM_COLORSPACE setable color space
+ void SetColorSpace(INPUTSTREAM_COLORSPACE colorSpace) { m_cStructure->m_colorSpace = colorSpace; }
+
+ /// @brief To get with @ref SetColorSpace changed values.
+ INPUTSTREAM_COLORSPACE GetColorSpace() const { return m_cStructure->m_colorSpace; }
+
+ /// @brief Color range if available.
+ ///
+ /// @param[in] colorRange The with @ref INPUTSTREAM_COLORRANGE setable color space
+ void SetColorRange(INPUTSTREAM_COLORRANGE colorRange) { m_cStructure->m_colorRange = colorRange; }
+
+ /// @brief To get with @ref SetColorRange changed values.
+ INPUTSTREAM_COLORRANGE GetColorRange() const { return m_cStructure->m_colorRange; }
+
+ /// @brief Chromaticity coordinates of the source primaries. These values match the ones defined by ISO/IEC 23001-8_2013 § 7.1.
+ ///
+ /// @param[in] colorPrimaries The with @ref INPUTSTREAM_COLORPRIMARIES setable values
+ void SetColorPrimaries(INPUTSTREAM_COLORPRIMARIES colorPrimaries)
+ {
+ m_cStructure->m_colorPrimaries = colorPrimaries;
+ }
+
+ /// @brief To get with @ref SetColorPrimaries changed values.
+ INPUTSTREAM_COLORPRIMARIES GetColorPrimaries() const { return m_cStructure->m_colorPrimaries; }
+
+ /// @brief Color Transfer Characteristic. These values match the ones defined by ISO/IEC 23001-8_2013 § 7.2.
+ ///
+ /// @param[in] colorTransferCharacteristic The with @ref INPUTSTREAM_COLORTRC setable characteristic
+ void SetColorTransferCharacteristic(INPUTSTREAM_COLORTRC colorTransferCharacteristic)
+ {
+ m_cStructure->m_colorTransferCharacteristic = colorTransferCharacteristic;
+ }
+
+ /// @brief To get with @ref SetColorTransferCharacteristic changed values.
+ INPUTSTREAM_COLORTRC GetColorTransferCharacteristic() const
+ {
+ return m_cStructure->m_colorTransferCharacteristic;
+ }
+
+ /// @brief Mastering static Metadata.
+ ///
+ /// Describes the metadata for HDR10, used when video is compressed using High
+ /// Efficiency Video Coding (HEVC). This is used to describe the capabilities
+ /// of the display used to master the content and the luminance values of the
+ /// content.
+ ///
+ /// @param[in] masteringMetadata The with @ref cpp_kodi_addon_inputstream_Defs_Interface_InputstreamMasteringMetadata setable metadata
+ void SetMasteringMetadata(const kodi::addon::InputstreamMasteringMetadata& masteringMetadata)
+ {
+ m_masteringMetadata = masteringMetadata;
+ m_cStructure->m_masteringMetadata = m_masteringMetadata;
+ }
+
+ /// @brief To get with @ref SetMasteringMetadata changed values.
+ const kodi::addon::InputstreamMasteringMetadata& GetMasteringMetadata() const
+ {
+ return m_masteringMetadata;
+ }
+
+ /// @brief Clear mastering static Metadata.
+ void ClearMasteringMetadata() { m_cStructure->m_masteringMetadata = nullptr; }
+
+ /// @brief Content light static Metadata.
+ ///
+ /// The maximum content light level (MaxCLL) and frame average light level
+ /// (MaxFALL) for the metadata for HDR10.
+ ///
+ /// @param[in] contentLightMetadata The with @ref cpp_kodi_addon_inputstream_Defs_Interface_InputstreamContentlightMetadata setable metadata
+ void SetContentLightMetadata(
+ const kodi::addon::InputstreamContentlightMetadata& contentLightMetadata)
+ {
+ m_contentLightMetadata = contentLightMetadata;
+ m_cStructure->m_contentLightMetadata = m_contentLightMetadata;
+ }
+
+ /// @brief To get with @ref SetContentLightMetadata changed values.
+ const kodi::addon::InputstreamContentlightMetadata& GetContentLightMetadata() const
+ {
+ return m_contentLightMetadata;
+ }
+
+ /// @brief Clear content light static Metadata.
+ void ClearContentLightMetadata() { m_cStructure->m_contentLightMetadata = nullptr; }
+
+ ///@}
+
+private:
+ InputstreamInfo(const INPUTSTREAM_INFO* stream) : CStructHdl(stream)
+ {
+ SetCryptoSession(StreamCryptoSession(&stream->m_cryptoSession));
+ CopyExtraData();
+ }
+ InputstreamInfo(INPUTSTREAM_INFO* stream) : CStructHdl(stream)
+ {
+ SetCryptoSession(StreamCryptoSession(&stream->m_cryptoSession));
+ CopyExtraData();
+ }
+
+ void CopyExtraData()
+ {
+ if (m_cStructure->m_ExtraData && m_cStructure->m_ExtraSize > 0)
+ {
+ for (unsigned int i = 0; i < m_cStructure->m_ExtraSize; ++i)
+ m_extraData.emplace_back(m_cStructure->m_ExtraData[i]);
+ }
+ if (m_cStructure->m_masteringMetadata)
+ m_masteringMetadata = m_cStructure->m_masteringMetadata;
+ if (m_cStructure->m_contentLightMetadata)
+ m_contentLightMetadata = m_cStructure->m_contentLightMetadata;
+ }
+ std::vector<uint8_t> m_extraData;
+ StreamCryptoSession m_cryptoSession;
+ InputstreamMasteringMetadata m_masteringMetadata;
+ InputstreamContentlightMetadata m_contentLightMetadata;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamTimes class InputstreamTimes
+/// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+/// @brief **Inputstream add-on times**\n
+/// Used on @ref kodi::addon::CInstanceInputStream::GetTimes().
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_inputstream_Defs_Interface_InputstreamTimes_Help
+///
+///@{
+class ATTR_DLL_LOCAL InputstreamTimes : public CStructHdl<InputstreamTimes, INPUTSTREAM_TIMES>
+{
+ /*! \cond PRIVATE */
+ friend class CInstanceInputStream;
+ /*! \endcond */
+
+public:
+ /*! \cond PRIVATE */
+ InputstreamTimes() = default;
+ InputstreamTimes(const InputstreamTimes& stream) : CStructHdl(stream) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamTimes_Help Value Help
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamTimes
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_inputstream_Defs_Interface_InputstreamTimes :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|--------------------
+ /// | **Start time** | `time_t` | @ref InputstreamTimes::SetStartTime "SetStartTime" | @ref InputstreamTimes::GetStartTime "GetStartTime"
+ /// | **PTS start** | `double` | @ref InputstreamTimes::SetPtsStart "SetPtsStart" | @ref InputstreamTimes::GetPtsStart "GetPtsStart"
+ /// | **PTS begin** | `double` | @ref InputstreamTimes::SetPtsBegin "SetPtsBegin" | @ref InputstreamTimes::GetPtsBegin "GetPtsBegin"
+ /// | **PTS end** | `double` | @ref InputstreamTimes::SetPtsEnd "SetPtsEnd" | @ref InputstreamTimes::GetPtsEnd "GetPtsEnd"
+ ///
+
+ /// @addtogroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamTimes
+ ///@{
+
+ /// @brief Start time in milliseconds
+ void SetStartTime(time_t startTime) const { m_cStructure->startTime = startTime; }
+
+ /// @brief To get with @ref SetStartTime changed values
+ time_t GetStartTime() const { return m_cStructure->startTime; }
+
+ /// @brief Start PTS
+ void SetPtsStart(double ptsStart) const { m_cStructure->ptsStart = ptsStart; }
+
+ /// @brief To get with @ref SetPtsStart changed values
+ double GetPtsStart() const { return m_cStructure->ptsStart; }
+
+ /// @brief Begin PTS
+ void SetPtsBegin(double ptsBegin) const { m_cStructure->ptsBegin = ptsBegin; }
+
+ /// @brief To get with @ref SetPtsBegin changed values
+ double GetPtsBegin() const { return m_cStructure->ptsBegin; }
+
+ /// @brief End PTS
+ void SetPtsEnd(double ptsEnd) const { m_cStructure->ptsEnd = ptsEnd; }
+
+ /// @brief To get with @ref SetPtsEnd changed values
+ double GetPtsEnd() const { return m_cStructure->ptsEnd; }
+
+ ///@}
+
+private:
+ InputstreamTimes(const INPUTSTREAM_TIMES* stream) : CStructHdl(stream) {}
+ InputstreamTimes(INPUTSTREAM_TIMES* stream) : CStructHdl(stream) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//============================================================================
+///
+/// @addtogroup cpp_kodi_addon_inputstream
+/// @brief \cpp_class{ kodi::addon::CInstanceInputStream }
+/// **Inputstream add-on instance**
+///
+/// This instance type is for using input streams to video and audio, to process
+/// and then give them to Kodi.
+///
+/// This usage can be requested under various conditions, for example explicitly
+/// by another addon, by a Mimetype protocol defined in `addon.xml` or supported
+/// file extensions.
+///
+/// In addition, stream files (* .strm) can be linked to an inputstream addon
+/// using <b>`#KODIPROP:inputstream=<ADDON_NAME>`</b>.
+///
+/// Include the header @ref Inputstream.h "#include <kodi/addon-instance/Inputstream.h>"
+/// to use this class.
+///
+/// ----------------------------------------------------------------------------
+///
+/// Here is an example of what the <b>`addon.xml.in`</b> would look like for an inputstream addon:
+///
+/// ~~~~~~~~~~~~~{.xml}
+/// <?xml version="1.0" encoding="UTF-8"?>
+/// <addon
+/// id="inputstream.myspecialnamefor"
+/// version="1.0.0"
+/// name="My InputStream addon"
+/// provider-name="Your Name">
+/// <requires>@ADDON_DEPENDS@</requires>
+/// <extension
+/// point="kodi.inputstream"
+/// extension=".xyz|.zyx"
+/// listitemprops="license_type|license_key|license_data|license_flags"
+/// protocols="myspecialnamefor|myspecialnamefors"
+/// library_@PLATFORM@="@LIBRARY_FILENAME@"/>
+/// <extension point="xbmc.addon.metadata">
+/// <summary lang="en_GB">My InputStream addon</summary>
+/// <description lang="en_GB">My InputStream description</description>
+/// <platform>@PLATFORM@</platform>
+/// </extension>
+/// </addon>
+/// ~~~~~~~~~~~~~
+///
+///
+/// At <b>`<extension point="kodi.inputstream" ...>`</b> the basic instance definition is declared, this is intended to identify the addon as an input stream and to see its supported types:
+/// | Name | Description
+/// |------|----------------------
+/// | <b>`point`</b> | The identification of the addon instance to inputstream is mandatory <b>`kodi.inputstream`</b>. In addition, the instance declared in the first <b>`<extension ... />`</b> is also
+/// | <b>`extension`</b> | A filename extension is an identifier specified as a suffix to the name of a computer file where supported by addon.
+/// | <b>`listitemprops`</b> | Values that are available to the addon at @ref InputstreamProperty::GetProperties() and that can be passed to @ref CInstanceInputStream::Open() ith the respective values.
+/// | <b>`protocols`</b> | The streaming protocol is a special protocol supported by the addon for the transmission of streaming media data over a network.
+/// | <b>`library_@PLATFORM@`</b> | The runtime library used for the addon. This is usually declared by cmake and correctly displayed in the translated `addon.xml`.
+///
+///
+/// @remark For more detailed description of the <b>`addon.xml`</b>, see also https://kodi.wiki/view/Addon.xml.
+///
+///
+/// --------------------------------------------------------------------------
+///
+///
+/// **Example:**
+///
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/Inputstream.h>
+///
+/// class CMyInputstream : public kodi::addon::CInstanceInputStream
+/// {
+/// public:
+/// CMyInputstream(const kodi::addon::IInstanceInfo& instance);
+///
+/// void GetCapabilities(kodi::addon::InputstreamCapabilities& capabilities) override;
+/// bool Open(const kodi::addon::InputstreamProperty& props) override;
+/// void Close() override;
+/// ...
+/// };
+///
+/// CMyInputstream::CMyInputstream(const kodi::addon::IInstanceInfo& instance)
+/// : kodi::addon::CInstanceInputStream(instance)
+/// {
+/// ...
+/// }
+///
+/// void CMyInputstream::GetCapabilities(kodi::addon::InputstreamCapabilities& capabilities)
+/// {
+/// capabilities.SetMask(INPUTSTREAM_SUPPORTS_IDEMUX | INPUTSTREAM_SUPPORTS_PAUSE);
+/// }
+///
+/// void CMyInputstream::Open(const kodi::addon::InputstreamProperty& props)
+/// {
+/// std::string url = props.GetURL();
+/// ...
+/// }
+///
+/// void CMyInputstream::Close()
+/// {
+/// ...
+/// }
+///
+/// ...
+///
+/// //----------------------------------------------------------------------
+///
+/// class CMyAddon : public kodi::addon::CAddonBase
+/// {
+/// public:
+/// CMyAddon() = default;
+/// ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl) override;
+/// };
+///
+/// // If you use only one instance in your add-on, can be instanceType and
+/// // instanceID ignored
+/// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl)
+/// {
+/// if (instance.IsType(ADDON_INSTANCE_INPUTSTREAM))
+/// {
+/// kodi::Log(ADDON_LOG_NOTICE, "Creating my Inputstream");
+/// hdl = new CMyInputstream(instance);
+/// return ADDON_STATUS_OK;
+/// }
+/// else if (...)
+/// {
+/// ...
+/// }
+/// return ADDON_STATUS_UNKNOWN;
+/// }
+///
+/// ADDONCREATOR(CMyAddon)
+/// ~~~~~~~~~~~~~
+///
+/// The destruction of the example class `CMyInputstream` is called from
+/// Kodi's header. Manually deleting the add-on instance is not required.
+///
+//------------------------------------------------------------------------------
+class ATTR_DLL_LOCAL CInstanceInputStream : public IAddonInstance
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_inputstream
+ /// @brief Inputstream class constructor used to support multiple instance
+ /// types
+ ///
+ /// @param[in] instance The instance value given to <b>`kodi::addon::CAddonBase::CreateInstance(...)`</b>
+ /// @param[in] kodiVersion [opt] Version used in Kodi for this instance, to
+ /// allow compatibility to older Kodi versions.
+ ///
+ /// @warning Only use `instance` from the @ref CAddonBase::CreateInstance call.
+ ///
+ explicit CInstanceInputStream(const IInstanceInfo& instance) : IAddonInstance(instance)
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstanceInputStream: Creation of multiple together "
+ "with single instance way is not allowed!");
+
+ SetAddonStruct(instance);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_inputstream
+ /// @brief Destructor
+ ///
+ ~CInstanceInputStream() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_inputstream
+ /// @brief Get the list of features that this add-on provides.
+ ///
+ /// Called by Kodi to query the add-on's capabilities.
+ /// Used to check which options should be presented in the UI, which methods to call, etc.
+ /// All capabilities that the add-on supports should be set to true.
+ ///
+ /// @param[out] capabilities The with @ref cpp_kodi_addon_inputstream_Defs_Capabilities defined add-on's capabilities.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_inputstream_Defs_Interface_InputstreamCapabilities_Help
+ ///
+ /// --------------------------------------------------------------------------
+ /// @note Valid implementation required.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ ///
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// void CMyInputstream::GetCapabilities(kodi::addon::InputstreamCapabilities& capabilities)
+ /// {
+ /// capabilities.SetMask(INPUTSTREAM_SUPPORTS_IDEMUX | INPUTSTREAM_SUPPORTS_PAUSE);
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual void GetCapabilities(kodi::addon::InputstreamCapabilities& capabilities) = 0;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_inputstream
+ /// @brief Open a stream.
+ ///
+ /// @param[in] props The used properties about the stream
+ /// @return True if the stream has been opened successfully, false otherwise.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_inputstream_Defs_InputstreamProperty_Help
+ ///
+ /// --------------------------------------------------------------------------
+ /// @note Valid implementation required.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ ///
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// void CMyInputstream::Open(const kodi::addon::InputstreamProperty& props)
+ /// {
+ /// std::string url = props.GetURL();
+ /// std::string license_key = props.GetProperties()["inputstream.myspecialnamefor.license_key"];
+ /// ...
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual bool Open(const kodi::addon::InputstreamProperty& props) = 0;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_inputstream
+ /// @brief Close an open stream.
+ ///
+ /// @remarks
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// @note Valid implementation required.
+ ///
+ virtual void Close() = 0;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_inputstream
+ /// @brief Check for real-time streaming
+ ///
+ /// @return true if current stream is real-time
+ ///
+ virtual bool IsRealTimeStream() { return true; }
+ //----------------------------------------------------------------------------
+
+ //############################################################################
+ /// @defgroup cpp_kodi_addon_inputstream_Read 1. Stream read
+ /// @brief **Functions required to read streams direct and demux inside Kodi.**
+ ///
+ /// This part contains at least the functions necessary for addon that have to
+ /// be supported. This can only be ignored if you use your own demux.
+ ///
+ /// The data loaded by Kodi is then processed in it itself. The source does not
+ /// matter, only Kodi must be able to process this stream data itself and is
+ /// therefore limited in some things.
+ ///
+ /// For more complex things, the addon must integrate its own demuxer and,
+ /// if necessary, even its own codec processing (see @ref cpp_kodi_addon_codec "Codec").
+ ///
+ /// @note These are used and must be set by the addon if the @ref INPUTSTREAM_SUPPORTS_IDEMUX
+ /// is <em><b>undefined</b></em> in the capabilities (see @ref GetCapabilities()).
+ /// Otherwise becomes @ref cpp_kodi_addon_inputstream_Demux "demuxing" used.
+ ///
+ /// @ingroup cpp_kodi_addon_inputstream
+ ///@{
+
+ //============================================================================
+ /// @brief Read from an open stream.
+ ///
+ /// @param[in] buffer The buffer to store the data in.
+ /// @param[in] bufferSize The amount of bytes to read.
+ /// @return The amount of bytes that were actually read from the stream.
+ ///
+ virtual int ReadStream(uint8_t* buffer, unsigned int bufferSize) { return -1; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Seek in a stream.
+ ///
+ /// @param[in] position The position to seek to
+ /// @param[in] whence offset relative to<br>
+ /// You can set the value of whence to one of three things:
+ /// | Value | int | Description |
+ /// |:------------:|:-----:|:----------------------------------------------------|
+ /// | **SEEK_SET** | `0` | position is relative to the beginning of the file. This is probably what you had in mind anyway, and is the most commonly used value for whence.
+ /// | **SEEK_CUR** | `1` | position is relative to the current file pointer position. So, in effect, you can say, "Move to my current position plus 30 bytes," or, "move to my current position minus 20 bytes."
+ /// | **SEEK_END** | `2` | position is relative to the end of the file. Just like SEEK_SET except from the other end of the file. Be sure to use negative values for offset if you want to back up from the end of the file, instead of going past the end into oblivion.
+ ///
+ /// @return Returns the resulting offset location as measured in bytes from
+ /// the beginning of the file. On error, the value -1 is returned.
+ ///
+ /// @remarks Optional and can leaved away or return -1 if this add-on won't
+ /// provide this function.
+ ///
+ virtual int64_t SeekStream(int64_t position, int whence = SEEK_SET) { return -1; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief The position in the stream that's currently being read.
+ ///
+ /// @return Stream position
+ ///
+ /// @remarks Optional and can leaved away or return -1 if this add-on won't
+ /// provide this function.
+ ///
+ virtual int64_t PositionStream() { return -1; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief The Total length of the stream that's currently being read.
+ ///
+ /// @return Length of the stream
+ ///
+ /// @remarks Optional and can leaved away or return -1 if this add-on won't
+ /// provide this function.
+ ///
+ virtual int64_t LengthStream() { return -1; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Obtain the chunk size to use when reading streams.
+ ///
+ /// @return Block chunk size
+ ///
+ /// @remarks Optional and can leaved away or return 0 if this add-on won't
+ /// provide this function.
+ ///
+ virtual int GetBlockSize() { return 0; }
+ //--------------------------------------------------------------------------
+
+ ///@}
+
+ //############################################################################
+ /// @defgroup cpp_kodi_addon_inputstream_Demux 2. Stream demuxing (optional)
+ /// @brief **Read demux streams.**
+ ///
+ /// @note These are used and must be set by the addon if the @ref INPUTSTREAM_SUPPORTS_IDEMUX is set in the capabilities (see @ref GetCapabilities()).
+ ///
+ /// @ingroup cpp_kodi_addon_inputstream
+ ///@{
+
+ //============================================================================
+ /// @brief Get IDs of available streams
+ ///
+ /// @param[in] ids list of used identifications
+ /// @return true if successfully done, otherwise false
+ ///
+ /// @remarks This id's are used to identify wanted data on @ref GetStream call.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ ///
+ /// ~~~~~~~~~~~~~{.cpp}
+ ///
+ /// bool CMyInputstream::GetStreamIds(std::vector<unsigned int>& ids)
+ /// {
+ /// kodi::Log(ADDON_LOG_DEBUG, "GetStreamIds(...)");
+ ///
+ /// if (m_opened)
+ /// {
+ /// // This check not needed to have, the ABI checks also about, but make
+ /// // sure you not give more as 32 streams.
+ /// if (m_myStreams.size() > MAX_STREAM_COUNT)
+ /// {
+ /// kodi::Log(ADDON_LOG_ERROR, "Too many streams, only %u supported", MAX_STREAM_COUNT);
+ /// return false;
+ /// }
+ ///
+ /// ids.emplace_back(m_myAudioStreamId);
+ ///
+ /// for (const auto& streamPair : m_myOtherStreams)
+ /// {
+ /// ids.emplace_back(streamPair.second->uniqueId);
+ /// }
+ /// }
+ ///
+ /// return !ids.empty();
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual bool GetStreamIds(std::vector<unsigned int>& ids) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Function for giving detailed stream information
+ ///
+ /// The associated information is set here for IDs previously given with
+ /// @ref GetStreamIds.
+ ///
+ /// This data is required to identify the associated codec and, if necessary,
+ /// to refer to your own codec (if available in the addon).
+ ///
+ /// @param[in] streamid unique id of stream
+ /// @param[out] stream Information data of wanted stream
+ /// @return true if successfully done, otherwise false
+ ///
+ /// @remarks Available stream id's previously asked by @ref GetStreamIds
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_inputstream_Defs_Interface_InputstreamInfo_Help
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ ///
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// bool CMyInputstream::GetStream(int streamid, kodi::addon::InputstreamInfo& stream)
+ /// {
+ /// // This is just a small example, this type will be significantly larger
+ /// // for larger and more complex streams.
+ /// if (streamid == m_myAudioStreamId)
+ /// {
+ /// // This only a minimal exampl
+ /// stream.SetStreamType(INPUTSTREAM_TYPE_AUDIO);
+ /// stream.SetFeatures(INPUTSTREAM_FEATURE_NONE); // Only added to example, INPUTSTREAM_FEATURE_NONE is default and no need to call
+ /// stream.SetFlags(INPUTSTREAM_FLAG_NONE); // Only added to example, INPUTSTREAM_FLAG_NONE is default and no need to call
+ /// stream.SetCodecName("mp2"); // Codec name, string must by equal with FFmpeg, see https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/codec_desc.c
+ /// stream.SetPhysicalIndex(1); // Identifier required to set
+ /// stream.SetLanguage("en");
+ /// stream.SetChannels(2);
+ /// stream.SetSampleRate(48000);
+ /// stream.SetBitRate(0);
+ /// stream.SetBitsPerSample(16);
+ /// }
+ /// else ...
+ /// ...
+ /// return true;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual bool GetStream(int streamid, kodi::addon::InputstreamInfo& stream) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Enable or disable a stream.
+ ///
+ /// A disabled stream does not send demux packets
+ ///
+ /// @param[in] streamid unique id of stream
+ /// @param[in] enable true for enable, false for disable
+ ///
+ /// @remarks Available stream id's previously asked by @ref GetStreamIds
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// @note Valid implementation required.
+ ///
+ virtual void EnableStream(int streamid, bool enable) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Opens a stream for playback.
+ ///
+ /// @param[in] streamid unique id of stream
+ ///
+ /// @remarks Available stream id's previously asked by @ref GetStreamIds
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// @note Valid implementation required.
+ ///
+ virtual bool OpenStream(int streamid) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Reset the demultiplexer in the add-on.
+ ///
+ /// @remarks Optional, and only used if addon has its own demuxer.
+ ///
+ virtual void DemuxReset() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Abort the demultiplexer thread in the add-on.
+ ///
+ /// @remarks Optional, and only used if addon has its own demuxer.
+ ///
+ virtual void DemuxAbort() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Flush all data that's currently in the demultiplexer buffer in the add-on.
+ ///
+ /// @remarks Optional, and only used if addon has its own demuxer.
+ ///
+ virtual void DemuxFlush() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Read the next packet from the demultiplexer, if there is one.
+ ///
+ /// @return The next packet.
+ /// If there is no next packet, then the add-on should return the
+ /// packet created by calling @ref AllocateDemuxPacket "AllocateDemuxPacket(0)" on the callback.
+ /// If the stream changed and Kodi's player needs to be reinitialised,
+ /// then, the add-on should call @ref AllocateDemuxPacket "AllocateDemuxPacket(0)" on the
+ /// callback, and set the streamid to DMX_SPECIALID_STREAMCHANGE and
+ /// return the value.
+ /// The add-on should return <b>`nullptr`</b> if an error occurred.
+ ///
+ /// @remarks Return <b>`nullptr`</b> if this add-on won't provide this function.
+ ///
+ virtual DEMUX_PACKET* DemuxRead() { return nullptr; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Notify the InputStream addon/demuxer that Kodi wishes to seek the stream by time
+ ///
+ /// Demuxer is required to set stream to an IDR frame
+ ///
+ /// @param[in] time The absolute time since stream start
+ /// @param[in] backwards True to seek to keyframe BEFORE time, else AFTER
+ /// @param[in] startpts can be updated to point to where display should start
+ /// @return True if the seek operation was possible
+ ///
+ /// @remarks Optional, and only used if addon has its own demuxer.
+ ///
+ virtual bool DemuxSeekTime(double time, bool backwards, double& startpts) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Notify the InputStream addon/demuxer that Kodi wishes to change playback speed
+ ///
+ /// @param[in] speed The requested playback speed
+ ///
+ /// @remarks Optional, and only used if addon has its own demuxer.
+ ///
+ virtual void DemuxSetSpeed(int speed) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Notify current screen resolution
+ ///
+ /// @param[in] width Width to set
+ /// @param[in] height Height to set
+ ///
+ virtual void SetVideoResolution(unsigned int width, unsigned int height) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Notify current screen resolution and max screen resolution allowed
+ ///
+ /// @param[in] width Width to set
+ /// @param[in] height Height to set
+ /// @param[in] maxWidth Max width allowed
+ /// @param[in] maxHeight Max height allowed
+ ///
+ virtual void SetVideoResolution(unsigned int width,
+ unsigned int height,
+ unsigned int maxWidth,
+ unsigned int maxHeight)
+ {
+ }
+ //----------------------------------------------------------------------------
+
+ //=============================================================================
+ /// @brief Allocate a demux packet. Free with @ref FreeDemuxPacket
+ ///
+ /// @param[in] dataSize The size of the data that will go into the packet
+ /// @return The allocated packet
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ DEMUX_PACKET* AllocateDemuxPacket(int dataSize)
+ {
+ return m_instanceData->toKodi->allocate_demux_packet(m_instanceData->toKodi->kodiInstance,
+ dataSize);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Allocate a encrypted demux packet. Free with @ref FreeDemuxPacket
+ ///
+ /// @param[in] dataSize The size of the data that will go into the packet
+ /// @param[in] encryptedSubsampleCount The encrypted subsample count
+ /// @return The allocated packet
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ DEMUX_PACKET* AllocateEncryptedDemuxPacket(int dataSize, unsigned int encryptedSubsampleCount)
+ {
+ return m_instanceData->toKodi->allocate_encrypted_demux_packet(
+ m_instanceData->toKodi->kodiInstance, dataSize, encryptedSubsampleCount);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Free a packet that was allocated with AllocateDemuxPacket
+ ///
+ /// @param[in] packet The packet to free
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ void FreeDemuxPacket(DEMUX_PACKET* packet)
+ {
+ return m_instanceData->toKodi->free_demux_packet(m_instanceData->toKodi->kodiInstance, packet);
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //############################################################################
+ /// @defgroup cpp_kodi_addon_inputstream_Time 3. Time (optional)
+ /// @brief **To get stream position time.**
+ ///
+ /// @note These are used and must be set by the addon if the @ref INPUTSTREAM_SUPPORTS_IDISPLAYTIME is set in the capabilities (see @ref GetCapabilities()).
+ ///
+ /// @ingroup cpp_kodi_addon_inputstream
+ ///@{
+
+ //==========================================================================
+ /// @brief Totel time in ms
+ ///
+ /// @return Total time in milliseconds
+ ///
+ /// @remarks
+ ///
+ virtual int GetTotalTime() { return -1; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Playing time in ms
+ ///
+ /// @return Playing time in milliseconds
+ ///
+ /// @remarks
+ ///
+ virtual int GetTime() { return -1; }
+ //--------------------------------------------------------------------------
+
+ ///@}
+
+ //############################################################################
+ /// @defgroup cpp_kodi_addon_inputstream_Times 4. Times (optional)
+ /// @brief **Another way to get stream position time.**
+ ///
+ /// @note These are used and must be set by the addon if the @ref INPUTSTREAM_SUPPORTS_ITIME is set in the capabilities (see @ref GetCapabilities()).
+ ///
+ /// @ingroup cpp_kodi_addon_inputstream
+ ///@{
+
+ //============================================================================
+ /// @brief Get current timing values in PTS scale
+ ///
+ /// @param[out] times The with @ref InputstreamTimes to given times
+ /// @return true if successfully done, false if not
+ ///
+ /// @copydetails cpp_kodi_addon_inputstream_Defs_Times_Help
+ ///
+ /// @remarks
+ ///
+ virtual bool GetTimes(InputstreamTimes& times) { return false; }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //############################################################################
+ /// @defgroup cpp_kodi_addon_inputstream_PosTime 5. Position time (optional)
+ /// @brief **Third way get stream position time.**
+ ///
+ /// @note These are used and must be set by the addon if the @ref INPUTSTREAM_SUPPORTS_IPOSTIME is set in the capabilities (see @ref GetCapabilities()).
+ ///
+ /// @ingroup cpp_kodi_addon_inputstream
+ ///@{
+
+ //============================================================================
+ /// @brief Positions inputstream to playing time given in ms
+ ///
+ /// @param[in] ms Position time in milliseconds
+ ///
+ /// @remarks
+ ///
+ virtual bool PosTime(int ms) { return false; }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //############################################################################
+ /// @defgroup cpp_kodi_addon_inputstream_Chapter 6. Chapter (optional)
+ /// @brief **Used to get available chapters.**
+ ///
+ /// @note These are used and must be set by the addon if the @ref INPUTSTREAM_SUPPORTS_ICHAPTER is set in the capabilities (see @ref GetCapabilities()).
+ ///
+ /// @ingroup cpp_kodi_addon_inputstream
+ ///@{
+
+ //==========================================================================
+ ///
+ /// @brief Return currently selected chapter
+ ///
+ /// @return Chapter number
+ ///
+ /// @remarks
+ ///
+ virtual int GetChapter() { return -1; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ ///
+ /// @brief Return number of available chapters
+ ///
+ /// @return Chapter count
+ ///
+ /// @remarks
+ ///
+ virtual int GetChapterCount() { return 0; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ ///
+ /// @brief Return name of chapter
+ ///
+ /// @param[in] ch Chapter identifier
+ /// @return Chapter name
+ ///
+ /// @remarks
+ ///
+ virtual const char* GetChapterName(int ch) { return nullptr; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ ///
+ /// @brief Return position if chapter # ch in milliseconds
+ ///
+ /// @param[in] ch Chapter to get position from
+ /// @return Position in milliseconds
+ ///
+ /// @remarks
+ ///
+ virtual int64_t GetChapterPos(int ch) { return 0; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ ///
+ /// @brief Seek to the beginning of chapter # ch
+ ///
+ /// @param[in] ch Chapter to seek
+ /// @return True if successfully done, false if not
+ ///
+ /// @remarks
+ ///
+ virtual bool SeekChapter(int ch) { return false; }
+ //--------------------------------------------------------------------------
+
+ ///@}
+
+private:
+ static int compareVersion(const int v1[3], const int v2[3])
+ {
+ for (unsigned i(0); i < 3; ++i)
+ if (v1[i] != v2[i])
+ return v1[i] - v2[i];
+ return 0;
+ }
+
+ void SetAddonStruct(KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ int api[3] = { 0, 0, 0 };
+ sscanf(GetInstanceAPIVersion().c_str(), "%d.%d.%d", &api[0], &api[1], &api[2]);
+
+ instance->hdl = this;
+ instance->inputstream->toAddon->open = ADDON_Open;
+ instance->inputstream->toAddon->close = ADDON_Close;
+ instance->inputstream->toAddon->get_capabilities = ADDON_GetCapabilities;
+
+ instance->inputstream->toAddon->get_stream_ids = ADDON_GetStreamIds;
+ instance->inputstream->toAddon->get_stream = ADDON_GetStream;
+ instance->inputstream->toAddon->enable_stream = ADDON_EnableStream;
+ instance->inputstream->toAddon->open_stream = ADDON_OpenStream;
+ instance->inputstream->toAddon->demux_reset = ADDON_DemuxReset;
+ instance->inputstream->toAddon->demux_abort = ADDON_DemuxAbort;
+ instance->inputstream->toAddon->demux_flush = ADDON_DemuxFlush;
+ instance->inputstream->toAddon->demux_read = ADDON_DemuxRead;
+ instance->inputstream->toAddon->demux_seek_time = ADDON_DemuxSeekTime;
+ instance->inputstream->toAddon->demux_set_speed = ADDON_DemuxSetSpeed;
+ instance->inputstream->toAddon->set_video_resolution = ADDON_SetVideoResolution;
+
+ instance->inputstream->toAddon->get_total_time = ADDON_GetTotalTime;
+ instance->inputstream->toAddon->get_time = ADDON_GetTime;
+
+ instance->inputstream->toAddon->get_times = ADDON_GetTimes;
+ instance->inputstream->toAddon->pos_time = ADDON_PosTime;
+
+ instance->inputstream->toAddon->read_stream = ADDON_ReadStream;
+ instance->inputstream->toAddon->seek_stream = ADDON_SeekStream;
+ instance->inputstream->toAddon->position_stream = ADDON_PositionStream;
+ instance->inputstream->toAddon->length_stream = ADDON_LengthStream;
+ instance->inputstream->toAddon->is_real_time_stream = ADDON_IsRealTimeStream;
+
+ // Added on 2.0.10
+ instance->inputstream->toAddon->get_chapter = ADDON_GetChapter;
+ instance->inputstream->toAddon->get_chapter_count = ADDON_GetChapterCount;
+ instance->inputstream->toAddon->get_chapter_name = ADDON_GetChapterName;
+ instance->inputstream->toAddon->get_chapter_pos = ADDON_GetChapterPos;
+ instance->inputstream->toAddon->seek_chapter = ADDON_SeekChapter;
+
+ // Added on 2.0.12
+ instance->inputstream->toAddon->block_size_stream = ADDON_GetBlockSize;
+
+ /*
+ // Way to include part on new API version
+ int minPartVersion[3] = { 3, 0, 0 };
+ if (compareVersion(api, minPartVersion) >= 0)
+ {
+
+ }
+ */
+
+ m_instanceData = instance->inputstream;
+ m_instanceData->toAddon->addonInstance = this;
+ }
+
+ inline static bool ADDON_Open(const AddonInstance_InputStream* instance,
+ INPUTSTREAM_PROPERTY* props)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->Open(props);
+ }
+
+ inline static void ADDON_Close(const AddonInstance_InputStream* instance)
+ {
+ static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->Close();
+ }
+
+ inline static void ADDON_GetCapabilities(const AddonInstance_InputStream* instance,
+ INPUTSTREAM_CAPABILITIES* capabilities)
+ {
+ InputstreamCapabilities caps(capabilities);
+ static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->GetCapabilities(caps);
+ }
+
+
+ // IDemux
+ inline static bool ADDON_GetStreamIds(const AddonInstance_InputStream* instance,
+ struct INPUTSTREAM_IDS* ids)
+ {
+ std::vector<unsigned int> idList;
+ bool ret =
+ static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->GetStreamIds(idList);
+ if (ret)
+ {
+ for (size_t i = 0; i < idList.size() && i < INPUTSTREAM_MAX_STREAM_COUNT; ++i)
+ {
+ ids->m_streamCount++;
+ ids->m_streamIds[i] = idList[i];
+ }
+ }
+ return ret;
+ }
+
+ inline static bool ADDON_GetStream(
+ const AddonInstance_InputStream* instance,
+ int streamid,
+ struct INPUTSTREAM_INFO* info,
+ KODI_HANDLE* demuxStream,
+ KODI_HANDLE (*transfer_stream)(KODI_HANDLE handle,
+ int streamId,
+ struct INPUTSTREAM_INFO* stream))
+ {
+ InputstreamInfo infoData(info);
+ bool ret = static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)
+ ->GetStream(streamid, infoData);
+ if (ret && transfer_stream)
+ {
+ // Do this with given callback to prevent memory problems and leaks. This
+ // then create on Kodi the needed class where then given back on demuxStream.
+ *demuxStream = transfer_stream(instance->toKodi->kodiInstance, streamid, info);
+ }
+ return ret;
+ }
+
+ inline static void ADDON_EnableStream(const AddonInstance_InputStream* instance,
+ int streamid,
+ bool enable)
+ {
+ static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)
+ ->EnableStream(streamid, enable);
+ }
+
+ inline static bool ADDON_OpenStream(const AddonInstance_InputStream* instance, int streamid)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)
+ ->OpenStream(streamid);
+ }
+
+ inline static void ADDON_DemuxReset(const AddonInstance_InputStream* instance)
+ {
+ static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->DemuxReset();
+ }
+
+ inline static void ADDON_DemuxAbort(const AddonInstance_InputStream* instance)
+ {
+ static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->DemuxAbort();
+ }
+
+ inline static void ADDON_DemuxFlush(const AddonInstance_InputStream* instance)
+ {
+ static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->DemuxFlush();
+ }
+
+ inline static DEMUX_PACKET* ADDON_DemuxRead(const AddonInstance_InputStream* instance)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->DemuxRead();
+ }
+
+ inline static bool ADDON_DemuxSeekTime(const AddonInstance_InputStream* instance,
+ double time,
+ bool backwards,
+ double* startpts)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)
+ ->DemuxSeekTime(time, backwards, *startpts);
+ }
+
+ inline static void ADDON_DemuxSetSpeed(const AddonInstance_InputStream* instance, int speed)
+ {
+ static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->DemuxSetSpeed(speed);
+ }
+
+ inline static void ADDON_SetVideoResolution(const AddonInstance_InputStream* instance,
+ unsigned int width,
+ unsigned int height,
+ unsigned int maxWidth,
+ unsigned int maxHeight)
+ {
+ static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)
+ ->SetVideoResolution(width, height);
+ static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)
+ ->SetVideoResolution(width, height, maxWidth, maxHeight);
+ }
+
+ // IDisplayTime
+ inline static int ADDON_GetTotalTime(const AddonInstance_InputStream* instance)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->GetTotalTime();
+ }
+
+ inline static int ADDON_GetTime(const AddonInstance_InputStream* instance)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->GetTime();
+ }
+
+ // ITime
+ inline static bool ADDON_GetTimes(const AddonInstance_InputStream* instance,
+ INPUTSTREAM_TIMES* times)
+ {
+ InputstreamTimes cppTimes(times);
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->GetTimes(cppTimes);
+ }
+
+ // IPosTime
+ inline static bool ADDON_PosTime(const AddonInstance_InputStream* instance, int ms)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->PosTime(ms);
+ }
+
+ inline static int ADDON_GetChapter(const AddonInstance_InputStream* instance)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->GetChapter();
+ }
+
+ inline static int ADDON_GetChapterCount(const AddonInstance_InputStream* instance)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->GetChapterCount();
+ }
+
+ inline static const char* ADDON_GetChapterName(const AddonInstance_InputStream* instance, int ch)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->GetChapterName(ch);
+ }
+
+ inline static int64_t ADDON_GetChapterPos(const AddonInstance_InputStream* instance, int ch)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->GetChapterPos(ch);
+ }
+
+ inline static bool ADDON_SeekChapter(const AddonInstance_InputStream* instance, int ch)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->SeekChapter(ch);
+ }
+
+ inline static int ADDON_ReadStream(const AddonInstance_InputStream* instance,
+ uint8_t* buffer,
+ unsigned int bufferSize)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)
+ ->ReadStream(buffer, bufferSize);
+ }
+
+ inline static int64_t ADDON_SeekStream(const AddonInstance_InputStream* instance,
+ int64_t position,
+ int whence)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)
+ ->SeekStream(position, whence);
+ }
+
+ inline static int64_t ADDON_PositionStream(const AddonInstance_InputStream* instance)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->PositionStream();
+ }
+
+ inline static int64_t ADDON_LengthStream(const AddonInstance_InputStream* instance)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->LengthStream();
+ }
+
+ inline static int ADDON_GetBlockSize(const AddonInstance_InputStream* instance)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->GetBlockSize();
+ }
+
+ inline static bool ADDON_IsRealTimeStream(const AddonInstance_InputStream* instance)
+ {
+ return static_cast<CInstanceInputStream*>(instance->toAddon->addonInstance)->IsRealTimeStream();
+ }
+
+ AddonInstance_InputStream* m_instanceData;
+};
+//------------------------------------------------------------------------------
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/PVR.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/PVR.h
new file mode 100644
index 0000000..0ee71e2
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/PVR.h
@@ -0,0 +1,3568 @@
+/*
+ * 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 "../c-api/addon-instance/pvr.h"
+#include "pvr/ChannelGroups.h"
+#include "pvr/Channels.h"
+#include "pvr/EDL.h"
+#include "pvr/EPG.h"
+#include "pvr/General.h"
+#include "pvr/MenuHook.h"
+#include "pvr/Providers.h"
+#include "pvr/Recordings.h"
+#include "pvr/Stream.h"
+#include "pvr/Timers.h"
+
+#ifdef __cplusplus
+
+/*!
+ * @internal
+ * @brief PVR "C++" API interface
+ *
+ * In this field are the pure addon-side C++ data.
+ *
+ * @note Changes can be made without problems and have no influence on other
+ * PVR addons that have already been created.\n
+ * \n
+ * Therefore, @ref ADDON_INSTANCE_VERSION_PVR_MIN can be ignored for these
+ * fields and only the @ref ADDON_INSTANCE_VERSION_PVR needs to be increased.\n
+ * \n
+ * Only must be min version increased if a new compile of addon breaks after
+ * changes here.
+ *
+ * Have by add of new parts a look about **Doxygen** `\@ingroup`, so that
+ * added parts included in documentation.
+ *
+ * If you add addon side related documentation, where his dev need know, use `///`.
+ * For parts only for Kodi make it like here.
+ *
+ * @endinternal
+ */
+
+namespace kodi
+{
+namespace addon
+{
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Doxygen group set for the definitions
+//{{{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_addon_pvr
+/// @brief **PVR client add-on instance definition values**\n
+/// All PVR functions associated data structures.
+///
+/// Used to exchange the available options between Kodi and addon.\n
+/// The groups described here correspond to the groups of functions on PVR
+/// instance class.
+///
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_pvr_Defs_General 1. General
+/// @ingroup cpp_kodi_addon_pvr_Defs
+/// @brief **PVR add-on general variables**\n
+/// Used to exchange the available options between Kodi and addon.
+///
+/// This group also includes @ref cpp_kodi_addon_pvr_Defs_PVRCapabilities with
+/// which Kodi an @ref kodi::addon::CInstancePVRClient::GetCapabilities()
+/// queries the supported **modules** of the addon.
+///
+/// The standard values are also below, once for error messages and once to
+/// @ref kodi::addon::CInstancePVRClient::ConnectionStateChange() to give Kodi
+/// any information.
+///
+///@{
+//##############################################################################
+/// @defgroup cpp_kodi_addon_pvr_Defs_General_Inputstream class PVRStreamProperty & definition PVR_STREAM_PROPERTY
+/// @ingroup cpp_kodi_addon_pvr_Defs_General
+/// @brief **Inputstream variables**\n
+/// This includes values related to the outside of PVR available inputstream
+/// system.
+///
+/// This can be by separate instance on same addon, by handling in Kodi itself
+/// or to reference of another addon where support needed inputstream.
+///
+/// @note This is complete independent from own system included here
+/// @ref cpp_kodi_addon_pvr_Streams "inputstream".
+///
+//------------------------------------------------------------------------------
+///@}
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_pvr_Defs_Channel 2. Channel
+/// @ingroup cpp_kodi_addon_pvr_Defs
+/// @brief **PVR add-on channel**\n
+/// Used to exchange the available channel options between Kodi and addon.
+///
+/// Modules here are mainly intended for @ref cpp_kodi_addon_pvr_Channels "channels",
+/// but are also used on other modules to identify the respective TV/radio
+/// channel.
+///
+/// Because of @ref cpp_kodi_addon_pvr_Defs_Channel_PVRSignalStatus and
+/// @ref cpp_kodi_addon_pvr_Defs_Channel_PVRDescrambleInfo is a special case at
+/// this point. This is currently only used on running streams, but it may be
+/// possible that this must always be usable in connection with PiP in the
+/// future.
+///
+//------------------------------------------------------------------------------
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_pvr_Defs_ChannelGroup 3. Channel Group
+/// @ingroup cpp_kodi_addon_pvr_Defs
+/// @brief **PVR add-on channel group**\n
+/// This group contains data classes and values which are used in PVR on
+/// @ref cpp_kodi_addon_pvr_supportsChannelGroups "channel groups".
+///
+//------------------------------------------------------------------------------
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_pvr_Defs_epg 4. EPG Tag
+/// @ingroup cpp_kodi_addon_pvr_Defs
+/// @brief **PVR add-on EPG data**\n
+/// Used on @ref cpp_kodi_addon_pvr_EPGTag "EPG methods in PVR instance class".
+///
+/// See related modules about, also below in this view are few macros where
+/// default values of associated places.
+///
+//------------------------------------------------------------------------------
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_pvr_Defs_Recording 5. Recording
+/// @ingroup cpp_kodi_addon_pvr_Defs
+/// @brief **Representation of a recording**\n
+/// Used to exchange the available recording data between Kodi and addon on
+/// @ref cpp_kodi_addon_pvr_Recordings "Recordings methods in PVR instance class".
+///
+//------------------------------------------------------------------------------
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_pvr_Defs_Timer 6. Timer
+/// @ingroup cpp_kodi_addon_pvr_Defs
+/// @brief **PVR add-on timer data**\n
+/// Used to exchange the available timer data between Kodi and addon on
+/// @ref cpp_kodi_addon_pvr_Timers "Timers methods in PVR instance class".
+///
+//------------------------------------------------------------------------------
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_pvr_Defs_Provider 7. Provider
+/// @ingroup cpp_kodi_addon_pvr_Defs
+/// @brief **Representation of a provider**\n
+/// For list of all providers from the backend.
+///
+//------------------------------------------------------------------------------
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_pvr_Defs_Menuhook 8. Menuhook
+/// @ingroup cpp_kodi_addon_pvr_Defs
+/// @brief **PVR Context menu data**\n
+/// Define data for the context menus available to the user
+///
+//------------------------------------------------------------------------------
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_pvr_Defs_EDLEntry 9. Edit decision list (EDL)
+/// @ingroup cpp_kodi_addon_pvr_Defs
+/// @brief **An edit decision list or EDL is used in the post-production process
+/// of film editing and video editing**\n
+/// Used on @ref kodi::addon::CInstancePVRClient::GetEPGTagEdl and
+/// @ref kodi::addon::CInstancePVRClient::GetRecordingEdl
+///
+//------------------------------------------------------------------------------
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_pvr_Defs_Stream 10. Inputstream
+/// @ingroup cpp_kodi_addon_pvr_Defs
+/// @brief **Inputstream**\n
+/// This includes classes and values that are used in the PVR inputstream.
+///
+/// Used on @ref cpp_kodi_addon_pvr_Streams "Inputstream methods in PVR instance class".
+///
+/// @note The parts here will be removed in the future and replaced by the
+/// separate @ref cpp_kodi_addon_inputstream "inputstream addon instance".
+/// If there is already a possibility, new addons should do it via the
+/// inputstream instance.
+///
+//------------------------------------------------------------------------------
+
+//}}}
+//______________________________________________________________________________
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" PVR addon instance class
+//{{{
+
+//==============================================================================
+/// @addtogroup cpp_kodi_addon_pvr
+/// @brief \cpp_class{ kodi::addon::CInstancePVRClient }
+/// **PVR client add-on instance**
+///
+/// Kodi features powerful [Live TV](https://kodi.wiki/view/Live_TV) and
+/// [video recording (DVR/PVR)](http://en.wikipedia.org/wiki/Digital_video_recorder)
+/// abilities using a very flexible distributed application structure. That is, by
+/// leveraging other existing third-party
+/// [PVR backend applications](https://kodi.wiki/view/PVR_backend) or
+/// [DVR devices](https://kodi.wiki/view/PVR_backend)
+/// that specialize in receiving television signals and also support the same type
+/// of [client–server model](http://en.wikipedia.org/wiki/client%E2%80%93server_model)
+/// which Kodi uses, (following a [frontend-backend](http://en.wikipedia.org/wiki/Front_and_back_ends)
+/// design principle for [separation of concerns](http://en.wikipedia.org/wiki/Separation_of_concerns)),
+/// these PVR features in Kodi allow you to watch Live TV, listen to radio, view an EPG TV-Guide
+/// and schedule recordings, and also enables many other TV related features, all using
+/// Kodi as your primary interface once the initial pairing connection and
+/// configuration have been done.
+///
+/// @note It is very important to understand that with "Live TV" in the reference
+/// to PVR in Kodi, we do not mean [streaming video](http://en.wikipedia.org/wiki/Streaming_media)
+/// from the internet via websites providing [free content](https://kodi.wiki/view/Free_content)
+/// or online services such as Netflix, Hulu, Vudu and similar, no matter if that
+/// content is actually streamed live or not. If that is what you are looking for
+/// then you might want to look into [Video Addons](https://kodi.wiki/view/Add-ons)
+/// for Kodi instead, (which again is not the same as the "PVR" or "Live TV" we
+/// discuss in this article), but remember that [Kodi does not provide any video
+/// content or video streaming services](https://kodi.wiki/view/Free_content).
+///
+/// The use of the PVR is based on the @ref CInstancePVRClient.
+///
+/// Include the header @ref PVR.h "#include <kodi/addon-instance/PVR.h>"
+/// to use this class.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// Here is an example of what the <b>`addon.xml.in`</b> would look like for an PVR addon:
+///
+/// ~~~~~~~~~~~~~{.xml}
+/// <?xml version="1.0" encoding="UTF-8"?>
+/// <addon
+/// id="pvr.myspecialnamefor"
+/// version="1.0.0"
+/// name="My special PVR addon"
+/// provider-name="Your Name">
+/// <requires>@ADDON_DEPENDS@</requires>
+/// <extension
+/// point="kodi.pvrclient"
+/// library_@PLATFORM@="@LIBRARY_FILENAME@"/>
+/// <extension point="xbmc.addon.metadata">
+/// <summary lang="en_GB">My PVR addon addon</summary>
+/// <description lang="en_GB">My PVR addon description</description>
+/// <platform>@PLATFORM@</platform>
+/// </extension>
+/// </addon>
+/// ~~~~~~~~~~~~~
+///
+///
+/// At <b>`<extension point="kodi.pvrclient" ...>`</b> the basic instance definition is declared, this is intended to identify the addon as an PVR and to see its supported types:
+/// | Name | Description
+/// |------|----------------------
+/// | <b>`point`</b> | The identification of the addon instance to inputstream is mandatory <b>`kodi.pvrclient`</b>. In addition, the instance declared in the first <b>`<extension ... />`</b> is also the main type of addon.
+/// | <b>`library_@PLATFORM@`</b> | The runtime library used for the addon. This is usually declared by cmake and correctly displayed in the translated `addon.xml`.
+///
+///
+/// @remark For more detailed description of the <b>`addon.xml`</b>, see also https://kodi.wiki/view/Addon.xml.
+///
+///
+/// --------------------------------------------------------------------------
+///
+/// **Example:**
+///
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/PVR.h>
+///
+/// class CMyPVRClient : public ::kodi::addon::CInstancePVRClient
+/// {
+/// public:
+/// CMyPVRClient(const kodi::addon::IInstanceInfo& instance);
+///
+/// PVR_ERROR GetCapabilities(kodi::addon::PVRCapabilities& capabilities) override;
+/// PVR_ERROR GetBackendName(std::string& name) override;
+/// PVR_ERROR GetBackendVersion(std::string& version) override;
+///
+/// PVR_ERROR GetProvidersAmount(int& amount) override;
+/// PVR_ERROR GetProviders(std::vector<kodi::addon::PVRProvider>& providers) override;
+/// PVR_ERROR GetChannelsAmount(int& amount) override;
+/// PVR_ERROR GetChannels(bool radio, std::vector<kodi::addon::PVRChannel>& channels) override;
+/// PVR_ERROR GetChannelStreamProperties(const kodi::addon::PVRChannel& channel,
+/// std::vector<kodi::addon::PVRStreamProperty>& properties) override;
+///
+/// private:
+/// std::vector<kodi::addon::PVRChannel> m_myChannels;
+/// };
+///
+/// CMyPVRClient::CMyPVRClient(const kodi::addon::IInstanceInfo& instance)
+/// : CInstancePVRClient(instance)
+/// {
+/// kodi::addon::PVRChannel channel;
+/// channel.SetUniqueId(123);
+/// channel.SetChannelNumber(1);
+/// channel.SetChannelName("My test channel");
+/// m_myChannels.push_back(channel);
+/// }
+///
+/// PVR_ERROR CMyPVRClient::GetCapabilities(kodi::addon::PVRCapabilities& capabilities)
+/// {
+/// capabilities.SetSupportsTV(true);
+/// return PVR_ERROR_NO_ERROR;
+/// }
+///
+/// PVR_ERROR CMyPVRClient::GetBackendName(std::string& name)
+/// {
+/// name = "My special PVR client";
+/// return PVR_ERROR_NO_ERROR;
+/// }
+///
+/// PVR_ERROR CMyPVRClient::GetBackendVersion(std::string& version)
+/// {
+/// version = "1.0.0";
+/// return PVR_ERROR_NO_ERROR;
+/// }
+///
+/// PVR_ERROR CMyInstance::GetProvidersAmount(int& amount)
+/// {
+/// amount = m_myProviders.size();
+/// return PVR_ERROR_NO_ERROR;
+/// }
+///
+/// PVR_ERROR CMyPVRClient::GetProviders(std::vector<kodi::addon::PVRProvider>& providers)
+/// {
+/// providers = m_myProviders;
+/// return PVR_ERROR_NO_ERROR;
+/// }
+///
+/// PVR_ERROR CMyInstance::GetChannelsAmount(int& amount)
+/// {
+/// amount = m_myChannels.size();
+/// return PVR_ERROR_NO_ERROR;
+/// }
+///
+/// PVR_ERROR CMyPVRClient::GetChannels(bool radio, std::vector<kodi::addon::PVRChannel>& channels)
+/// {
+/// channels = m_myChannels;
+/// return PVR_ERROR_NO_ERROR;
+/// }
+///
+/// PVR_ERROR CMyPVRClient::GetChannelStreamProperties(const kodi::addon::PVRChannel& channel,
+/// std::vector<kodi::addon::PVRStreamProperty>& properties)
+/// {
+/// if (channel.GetUniqueId() == 123)
+/// {
+/// properties.push_back(PVR_STREAM_PROPERTY_STREAMURL, "http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_normal.mp4");
+/// properties.push_back(PVR_STREAM_PROPERTY_ISREALTIMESTREAM, "true");
+/// return PVR_ERROR_NO_ERROR;
+/// }
+/// return PVR_ERROR_UNKNOWN;
+/// }
+///
+/// ...
+///
+/// //----------------------------------------------------------------------
+///
+/// class CMyAddon : public ::kodi::addon::CAddonBase
+/// {
+/// public:
+/// CMyAddon() = default;
+/// ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl) override;
+/// };
+///
+/// // If you use only one instance in your add-on, can be instanceType and
+/// // instanceID ignored
+/// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl)
+/// {
+/// if (instance.IsType(ADDON_INSTANCE_PVR))
+/// {
+/// kodi::Log(ADDON_LOG_INFO, "Creating my PVR client instance");
+/// hdl = new CMyPVRClient(instance, version);
+/// return ADDON_STATUS_OK;
+/// }
+/// else if (...)
+/// {
+/// ...
+/// }
+/// return ADDON_STATUS_UNKNOWN;
+/// }
+///
+/// ADDONCREATOR(CMyAddon)
+/// ~~~~~~~~~~~~~
+///
+/// The destruction of the example class `CMyPVRClient` is called from
+/// Kodi's header. Manually deleting the add-on instance is not required.
+///
+class ATTR_DLL_LOCAL CInstancePVRClient : public IAddonInstance
+{
+public:
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Base 1. Basic functions
+ /// @ingroup cpp_kodi_addon_pvr
+ /// @brief **Functions to manage the addon and get basic information about it**\n
+ /// These are e.g. @ref GetCapabilities to know supported groups at
+ /// this addon or the others to get information about the source of the PVR
+ /// stream.
+ ///
+ /// The with "Valid implementation required." declared functions are mandatory,
+ /// all others are an option.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Basic parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Base_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_Base_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief PVR client class constructor.
+ ///
+ /// Used by an add-on that only supports only PVR and only in one instance.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Here's example about the use of this:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/addon-instance/PVR.h>
+ /// ...
+ ///
+ /// class ATTR_DLL_LOCAL CPVRExample
+ /// : public kodi::addon::CAddonBase,
+ /// public kodi::addon::CInstancePVRClient
+ /// {
+ /// public:
+ /// CPVRExample()
+ /// {
+ /// }
+ ///
+ /// ~CPVRExample() override;
+ /// {
+ /// }
+ ///
+ /// ...
+ /// };
+ ///
+ /// ADDONCREATOR(CPVRExample)
+ /// ~~~~~~~~~~~~~
+ ///
+ CInstancePVRClient() : IAddonInstance(IInstanceInfo(CPrivateBase::m_interface->firstKodiInstance))
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstancePVRClient: Creation of more as one in single "
+ "instance way is not allowed!");
+
+ SetAddonStruct(CPrivateBase::m_interface->firstKodiInstance);
+ CPrivateBase::m_interface->globalSingleInstance = this;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief PVR client class constructor used to support multiple instance
+ /// types.
+ ///
+ /// @param[in] instance The instance value given to
+ /// <b>`kodi::addon::CAddonBase::CreateInstance(...)`</b>.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Here's example about the use of this:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// class CMyPVRClient : public ::kodi::addon::CInstancePVRClient
+ /// {
+ /// public:
+ /// CMyPVRClient(const kodi::addon::IInstanceInfo& instance)
+ /// : CInstancePVRClient(instance)
+ /// {
+ /// ...
+ /// }
+ ///
+ /// ...
+ /// };
+ ///
+ /// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+ /// KODI_ADDON_INSTANCE_HDL& hdl)
+ /// {
+ /// kodi::Log(ADDON_LOG_INFO, "Creating my PVR client instance");
+ /// hdl = new CMyPVRClient(instance);
+ /// return ADDON_STATUS_OK;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ explicit CInstancePVRClient(const IInstanceInfo& instance) : IAddonInstance(instance)
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstancePVRClient: Creation of multiple together with "
+ "single instance way is not allowed!");
+
+ SetAddonStruct(instance);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Destructor
+ ///
+ ~CInstancePVRClient() override = default;
+ //----------------------------------------------------------------------------
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ //============================================================================
+ /// @brief Get the list of features that this add-on provides.
+ ///
+ /// Called by Kodi to query the add-on's capabilities.
+ /// Used to check which options should be presented in the UI, which methods to call, etc.
+ /// All capabilities that the add-on supports should be set to true.
+ ///
+ /// @param capabilities The with @ref cpp_kodi_addon_pvr_Defs_PVRCapabilities defined add-on's capabilities.
+ /// @return @ref PVR_ERROR_NO_ERROR if the properties were fetched successfully.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_PVRCapabilities_Help
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// PVR_ERROR CMyPVRClient::GetCapabilities(kodi::addon::PVRCapabilities& capabilities)
+ /// {
+ /// capabilities.SetSupportsTV(true);
+ /// capabilities.SetSupportsEPG(true);
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @note Valid implementation required.
+ ///
+ virtual PVR_ERROR GetCapabilities(kodi::addon::PVRCapabilities& capabilities) = 0;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the name reported by the backend that will be displayed in the UI.
+ ///
+ /// @param[out] name The name reported by the backend that will be displayed in the UI.
+ /// @return @ref PVR_ERROR_NO_ERROR if successfully done
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// PVR_ERROR CMyPVRClient::GetBackendName(std::string& name)
+ /// {
+ /// name = "My special PVR client";
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @note Valid implementation required.
+ ///
+ virtual PVR_ERROR GetBackendName(std::string& name) = 0;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the version string reported by the backend that will be
+ /// displayed in the UI.
+ ///
+ /// @param[out] version The version string reported by the backend that will be
+ /// displayed in the UI.
+ /// @return @ref PVR_ERROR_NO_ERROR if successfully done
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// PVR_ERROR CMyPVRClient::GetBackendVersion(std::string& version)
+ /// {
+ /// version = "1.0.0";
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @note Valid implementation required.
+ ///
+ virtual PVR_ERROR GetBackendVersion(std::string& version) = 0;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the hostname of the pvr backend server
+ ///
+ /// @param[out] hostname Hostname as ip address or alias. If backend does not
+ /// utilize a server, return empty string.
+ /// @return @ref PVR_ERROR_NO_ERROR if successfully done
+ ///
+ virtual PVR_ERROR GetBackendHostname(std::string& hostname) { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief To get the connection string reported by the backend that will be
+ /// displayed in the UI.
+ ///
+ /// @param[out] connection The connection string reported by the backend that
+ /// will be displayed in the UI.
+ /// @return @ref PVR_ERROR_NO_ERROR if successfully done
+ ///
+ virtual PVR_ERROR GetConnectionString(std::string& connection)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the disk space reported by the backend (if supported).
+ ///
+ /// @param[in] total The total disk space in KiB.
+ /// @param[in] used The used disk space in KiB.
+ /// @return @ref PVR_ERROR_NO_ERROR if the drive space has been fetched
+ /// successfully.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// PVR_ERROR CMyPVRClient::GetDriveSpace(uint64_t& total, uint64_t& used)
+ /// {
+ /// total = 100 * 1024 * 1024; // To set complete size of drive in KiB (100GB)
+ /// used = 12232424; // To set the used amount
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetDriveSpace(uint64_t& total, uint64_t& used)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Call one of the settings related menu hooks (if supported).
+ ///
+ /// Supported @ref cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook "menu hook "
+ /// instances have to be added in `constructor()`, by calling @ref AddMenuHook()
+ /// on the callback.
+ ///
+ /// @param[in] menuhook The hook to call.
+ /// @return @ref PVR_ERROR_NO_ERROR if the hook was called successfully.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook_Help
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// PVR_ERROR CMyPVRClient::CallSettingsMenuHook(const kodi::addon::PVRMenuhook& menuhook)
+ /// {
+ /// if (menuhook.GetHookId() == 2)
+ /// kodi::QueueNotification(QUEUE_INFO, "", kodi::GetLocalizedString(menuhook.GetLocalizedStringId()));
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR CallSettingsMenuHook(const kodi::addon::PVRMenuhook& menuhook)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief **Callback to Kodi Function**\nAdd or replace a menu hook for the context menu for this add-on
+ ///
+ /// This is a callback function, called from addon to give Kodi his context menu's.
+ ///
+ /// @param[in] menuhook The with @ref cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook defined hook to add
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook_Help
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Here's an example of the use of it:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/addon-instance/PVR.h>
+ /// ...
+ ///
+ /// {
+ /// kodi::addon::PVRMenuhook hook;
+ /// hook.SetHookId(1);
+ /// hook.SetCategory(PVR_MENUHOOK_CHANNEL);
+ /// hook.SetLocalizedStringId(30000);
+ /// AddMenuHook(hook);
+ /// }
+ ///
+ /// {
+ /// kodi::addon::PVRMenuhook hook;
+ /// hook.SetHookId(2);
+ /// hook.SetCategory(PVR_MENUHOOK_SETTING);
+ /// hook.SetLocalizedStringId(30001);
+ /// AddMenuHook(hook);
+ /// }
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ /// **Here another way:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/addon-instance/PVR.h>
+ /// ...
+ ///
+ /// AddMenuHook(kodi::addon::PVRMenuhook(1, 30000, PVR_MENUHOOK_CHANNEL));
+ /// AddMenuHook(kodi::addon::PVRMenuhook(2, 30001, PVR_MENUHOOK_SETTING));
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ inline void AddMenuHook(const kodi::addon::PVRMenuhook& hook)
+ {
+ m_instanceData->toKodi->AddMenuHook(m_instanceData->toKodi->kodiInstance, hook);
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Notify a state change for a PVR backend connection.
+ ///
+ /// @param[in] connectionString The connection string reported by the backend
+ /// that can be displayed in the UI.
+ /// @param[in] newState The by @ref PVR_CONNECTION_STATE defined new state.
+ /// @param[in] message A localized addon-defined string representing the new
+ /// state, that can be displayed in the UI or **empty** if
+ /// the Kodi-defined default string for the new state
+ /// shall be displayed.
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ ///
+ /// **Here's an example of the use of it:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/addon-instance/PVR.h>
+ /// #include <kodi/General.h> /* for kodi::GetLocalizedString(...) */
+ /// ...
+ ///
+ /// ConnectionStateChange("PVR demo connection lost", PVR_CONNECTION_STATE_DISCONNECTED, kodi::GetLocalizedString(30005, "Lost connection to Server"););
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ inline void ConnectionStateChange(const std::string& connectionString,
+ PVR_CONNECTION_STATE newState,
+ const std::string& message)
+ {
+ m_instanceData->toKodi->ConnectionStateChange(
+ m_instanceData->toKodi->kodiInstance, connectionString.c_str(), newState, message.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Get user data path of the PVR addon.
+ ///
+ /// @return Path of current Kodi user
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ /// @note Alternatively, @ref kodi::GetAddonPath() can be used for this.
+ ///
+ inline std::string UserPath() const { return m_instanceData->props->strUserPath; }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Get main client path of the PVR addon.
+ ///
+ /// @return Path of addon client
+ ///
+ /// @remarks Only called from addon itself.
+ ///
+ /// @note Alternatively, @ref kodi::GetBaseUserPath() can be used for this.
+ ///
+ inline std::string ClientPath() const { return m_instanceData->props->strClientPath; }
+ //----------------------------------------------------------------------------
+
+ ///@}
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Channels 2. Channels (required)
+ /// @ingroup cpp_kodi_addon_pvr
+ /// @brief **Functions to get available TV or Radio channels**\n
+ /// These are mandatory functions for using this addon to get the available
+ /// channels.
+ ///
+ /// @remarks Either @ref PVRCapabilities::SetSupportsTV "SetSupportsTV()" or
+ /// @ref PVRCapabilities::SetSupportsRadio "SetSupportsRadio()" is required to
+ /// be set to <b>`true`</b>.\n
+ /// If a channel changes after the initial import, or if a new one was added,
+ /// then the add-on should call @ref TriggerChannelUpdate().
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Channel parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Channels_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_Channels_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief The total amount of providers on the backend
+ ///
+ /// @param[out] amount The total amount of providers on the backend
+ /// @return @ref PVR_ERROR_NO_ERROR if the amount has been fetched successfully.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsProviders
+ /// "supportsProviders" is set to true.
+ ///
+ virtual PVR_ERROR GetProvidersAmount(int& amount) { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Request the list of all providers from the backend.
+ ///
+ /// @param[out] results The channels defined with
+ /// @ref cpp_kodi_addon_pvr_Defs_PVRProvider and
+ /// available at the addon, then transferred with
+ /// @ref cpp_kodi_addon_pvr_Defs_PVRProvidersResultSet.
+ /// @return @ref PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsProviders
+ /// "supportsProviders" is set to true.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_PVRProvider_Help
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// PVR_ERROR CMyPVRInstance::GetProviders(kodi::addon::PVRProvidersResultSet& results)
+ /// {
+ /// // Minimal demo example, in reality bigger and loop to transfer all
+ /// kodi::addon::PVRProvider provider;
+ /// provider.SetUniqueId(123);
+ /// provider.SetProviderName("My provider name");
+ /// provider.SetProviderType(PVR_PROVIDER_TYPE_SATELLITE);
+ /// ...
+ ///
+ /// // Give it now to Kodi
+ /// results.Add(provider);
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetProviders(kodi::addon::PVRProvidersResultSet& results)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Request Kodi to update it's list of providers.
+ ///
+ /// @remarks Only called from addon itself.
+ ///
+ inline void TriggerProvidersUpdate()
+ {
+ m_instanceData->toKodi->TriggerProvidersUpdate(m_instanceData->toKodi->kodiInstance);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief The total amount of channels on the backend
+ ///
+ /// @param[out] amount The total amount of channels on the backend
+ /// @return @ref PVR_ERROR_NO_ERROR if the amount has been fetched successfully.
+ ///
+ /// @remarks Valid implementation required.
+ ///
+ virtual PVR_ERROR GetChannelsAmount(int& amount) { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Request the list of all channels from the backend.
+ ///
+ /// @param[in] radio True to get the radio channels, false to get the TV channels.
+ /// @param[out] results The channels defined with @ref cpp_kodi_addon_pvr_Defs_Channel_PVRChannel
+ /// and available at the addon, them transferred with
+ /// @ref cpp_kodi_addon_pvr_Defs_Channel_PVRChannelsResultSet.
+ /// @return @ref PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Channel_PVRChannel_Help
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @remarks
+ /// If @ref PVRCapabilities::SetSupportsTV() is set to
+ /// <b>`true`</b>, a valid result set needs to be provided for <b>`radio = false`</b>.\n
+ /// If @ref PVRCapabilities::SetSupportsRadio() is set to
+ /// <b>`true`</b>, a valid result set needs to be provided for <b>`radio = true`</b>.
+ /// At least one of these two must provide a valid result set.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// PVR_ERROR CMyPVRInstance::GetChannels(bool radio, kodi::addon::PVRChannelsResultSet& results)
+ /// {
+ /// // Minimal demo example, in reality bigger and loop to transfer all
+ /// kodi::addon::PVRChannel channel;
+ /// channel.SetUniqueId(123);
+ /// channel.SetIsRadio(false);
+ /// channel.SetChannelNumber(1);
+ /// channel.SetChannelName("My channel name");
+ /// ...
+ ///
+ /// // Give it now to Kodi
+ /// results.Add(channel);
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetChannels(bool radio, kodi::addon::PVRChannelsResultSet& results)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the stream properties for a channel from the backend.
+ ///
+ /// @param[in] channel The channel to get the stream properties for.
+ /// @param[out] properties the properties required to play the stream.
+ /// @return @ref PVR_ERROR_NO_ERROR if the stream is available.
+ ///
+ /// @remarks If @ref PVRCapabilities::SetSupportsTV "SetSupportsTV" or
+ /// @ref PVRCapabilities::SetSupportsRadio "SetSupportsRadio" are set to true
+ /// and @ref PVRCapabilities::SetHandlesInputStream "SetHandlesInputStream" is
+ /// set to false.\n\n
+ /// In this case the implementation must fill the property @ref PVR_STREAM_PROPERTY_STREAMURL
+ /// with the URL Kodi should resolve to playback the channel.
+ ///
+ /// @note The value directly related to inputstream must always begin with the
+ /// name of the associated add-on, e.g. <b>`"inputstream.adaptive.manifest_update_parameter"`</b>.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// PVR_ERROR CMyPVRInstance::GetChannelStreamProperties(const kodi::addon::PVRChannel& channel,
+ /// std::vector<kodi::addon::PVRStreamProperty>& properties)
+ /// {
+ /// ...
+ /// properties.emplace_back(PVR_STREAM_PROPERTY_INPUTSTREAM, "inputstream.adaptive");
+ /// properties.emplace_back("inputstream.adaptive.manifest_type", "mpd");
+ /// properties.emplace_back("inputstream.adaptive.manifest_update_parameter", "full");
+ /// properties.emplace_back(PVR_STREAM_PROPERTY_MIMETYPE, "application/xml+dash");
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetChannelStreamProperties(
+ const kodi::addon::PVRChannel& channel,
+ std::vector<kodi::addon::PVRStreamProperty>& properties)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the signal status of the stream that's currently open.
+ ///
+ /// @param[out] signalStatus The signal status.
+ /// @return @ref PVR_ERROR_NO_ERROR if the signal status has been read successfully, false otherwise.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetHandlesInputStream "SetHandlesInputStream"
+ /// is set to true.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Channel_PVRSignalStatus_Help
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ ///
+ /// **Here's example about the use of this:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/addon-instance/PVR.h>
+ /// ...
+ ///
+ /// class ATTR_DLL_LOCAL CPVRExample
+ /// : public kodi::addon::CAddonBase,
+ /// public kodi::addon::CInstancePVRClient
+ /// {
+ /// public:
+ /// ...
+ /// PVR_ERROR SignalStatus(PVRSignalStatus &signalStatus) override
+ /// {
+ /// signalStatus.SetAapterName("Example adapter 1");
+ /// signalStatus.SetAdapterStatus("OK");
+ /// signalStatus.SetSignal(0xFFFF); // 100%
+ ///
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// };
+ ///
+ /// ADDONCREATOR(CPVRExample)
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetSignalStatus(int channelUid, kodi::addon::PVRSignalStatus& signalStatus)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the descramble information of the stream that's currently open.
+ ///
+ /// @param[out] descrambleInfo The descramble information.
+ /// @return @ref PVR_ERROR_NO_ERROR if the descramble information has been
+ /// read successfully, false otherwise.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsDescrambleInfo "supportsDescrambleInfo"
+ /// is set to true.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Channel_PVRDescrambleInfo_Help
+ ///
+ virtual PVR_ERROR GetDescrambleInfo(int channelUid,
+ kodi::addon::PVRDescrambleInfo& descrambleInfo)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Request Kodi to update it's list of channels.
+ ///
+ /// @remarks Only called from addon itself.
+ ///
+ inline void TriggerChannelUpdate()
+ {
+ m_instanceData->toKodi->TriggerChannelUpdate(m_instanceData->toKodi->kodiInstance);
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_supportsChannelGroups 3. Channel Groups (optional)
+ /// @ingroup cpp_kodi_addon_pvr
+ /// @brief <b>Bring in this functions if you have set @ref PVRCapabilities::SetSupportsChannelGroups "supportsChannelGroups"
+ /// to true</b>\n
+ /// This is used to divide available addon channels into groups, which can
+ /// then be selected by the user.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Channel group parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_supportsChannelGroups_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_supportsChannelGroups_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Get the total amount of channel groups on the backend if it supports channel groups.
+ ///
+ /// @param[out] amount The total amount of channel groups on the backend
+ /// @return @ref PVR_ERROR_NO_ERROR if the amount has been fetched successfully.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsChannelGroups "supportsChannelGroups" is set to true.
+ ///
+ virtual PVR_ERROR GetChannelGroupsAmount(int& amount) { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get a list of available channel groups on addon
+ ///
+ /// Request the list of all channel groups from the backend if it supports
+ /// channel groups.
+ ///
+ /// @param[in] radio True to get the radio channel groups, false to get the
+ /// TV channel groups.
+ /// @param[out] results List of available groups on addon defined with
+ /// @ref cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroup,
+ /// them transferred with
+ /// @ref cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupsResultSet.
+ /// @return @ref PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroup_Help
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsChannelGroups "supportsChannelGroups"
+ /// is set to true.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// PVR_ERROR CMyPVRInstance::GetChannelGroups(bool radio, kodi::addon::PVRChannelGroupsResultSet& groups)
+ /// {
+ /// kodi::addon::PVRChannelGroup group;
+ /// group.SetIsRadio(false);
+ /// group.SetGroupName("My group name");
+ /// group.SetPosition(1);
+ /// ...
+ ///
+ /// // Give it now to Kodi
+ /// results.Add(group);
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetChannelGroups(bool radio, kodi::addon::PVRChannelGroupsResultSet& results)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get a list of members on a group
+ ///
+ /// Request the list of all group members of a group from the backend if it
+ /// supports channel groups.
+ ///
+ /// @param[in] group The group to get the members for.
+ /// @param[out] results List of available group member channels defined with
+ /// @ref cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMember,
+ /// them transferred with
+ /// @ref PVRChannelGroupMembersResultSet.
+ /// @return @ref PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMember_Help
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsChannelGroups "supportsChannelGroups"
+ /// is set to true.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// PVR_ERROR CMyPVRInstance::GetChannelGroupMembers(const kodi::addon::PVRChannelGroup& group,
+ /// kodi::addon::PVRChannelGroupMembersResultSet& results)
+ /// {
+ /// for (const auto& myGroup : m_myGroups)
+ /// {
+ /// if (myGroup.strGroupName == group.GetGroupName())
+ /// {
+ /// for (unsigned int iChannelPtr = 0; iChannelPtr < myGroup.members.size(); iChannelPtr++)
+ /// {
+ /// int iId = myGroup.members.at(iChannelPtr) - 1;
+ /// if (iId < 0 || iId > (int)m_channels.size() - 1)
+ /// continue;
+ ///
+ /// PVRDemoChannel &channel = m_channels.at(iId);
+ /// kodi::addon::PVRChannelGroupMember kodiGroupMember;
+ /// kodiGroupMember.SetGroupName(group.GetGroupName());
+ /// kodiGroupMember.SetChannelUniqueId(channel.iUniqueId);
+ /// kodiGroupMember.SetChannelNumber(channel.iChannelNumber);
+ /// kodiGroupMember.SetSubChannelNumber(channel.iSubChannelNumber);
+ ///
+ /// results.Add(kodiGroupMember);
+ /// }
+ /// }
+ /// }
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetChannelGroupMembers(const kodi::addon::PVRChannelGroup& group,
+ kodi::addon::PVRChannelGroupMembersResultSet& results)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Request Kodi to update it's list of channel groups.
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ inline void TriggerChannelGroupsUpdate()
+ {
+ m_instanceData->toKodi->TriggerChannelGroupsUpdate(m_instanceData->toKodi->kodiInstance);
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_supportsChannelEdit 4. Channel edit (optional)
+ /// @ingroup cpp_kodi_addon_pvr
+ /// @brief <b>Bring in this functions if you have set @ref PVRCapabilities::SetSupportsChannelSettings "supportsChannelSettings"
+ /// to true or for @ref OpenDialogChannelScan() set @ref PVRCapabilities::SetSupportsChannelScan "supportsChannelScan"
+ /// to true</b>\n
+ /// The support of this is a pure option and not mandatory.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Channel edit parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_supportsChannelEdit_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_supportsChannelEdit_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Delete a channel from the backend.
+ ///
+ /// @param[in] channel The channel to delete.
+ /// @return @ref PVR_ERROR_NO_ERROR if the channel has been deleted successfully.
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsChannelSettings "supportsChannelSettings"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR DeleteChannel(const kodi::addon::PVRChannel& channel)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Rename a channel on the backend.
+ ///
+ /// @param[in] channel The channel to rename, containing the new channel name.
+ /// @return @ref PVR_ERROR_NO_ERROR if the channel has been renamed successfully.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Channel_PVRChannel_Help
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsChannelSettings "supportsChannelSettings"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR RenameChannel(const kodi::addon::PVRChannel& channel)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Show the channel settings dialog, if supported by the backend.
+ ///
+ /// @param[in] channel The channel to show the dialog for.
+ /// @return @ref PVR_ERROR_NO_ERROR if the dialog has been displayed successfully.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsChannelSettings "supportsChannelSettings" is set to true.
+ /// @note Use @ref cpp_kodi_gui_CWindow "kodi::gui::CWindow" to create dialog for them.
+ ///
+ virtual PVR_ERROR OpenDialogChannelSettings(const kodi::addon::PVRChannel& channel)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Show the dialog to add a channel on the backend, if supported by the backend.
+ ///
+ /// @param[in] channel The channel to add.
+ /// @return @ref PVR_ERROR_NO_ERROR if the channel has been added successfully.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsChannelSettings "supportsChannelSettings" is set to true.
+ /// @note Use @ref cpp_kodi_gui_CWindow "kodi::gui::CWindow" to create dialog for them.
+ ///
+ virtual PVR_ERROR OpenDialogChannelAdd(const kodi::addon::PVRChannel& channel)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Show the channel scan dialog if this backend supports it.
+ ///
+ /// @return @ref PVR_ERROR_NO_ERROR if the dialog was displayed successfully.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsChannelScan "supportsChannelScan" is set to true.
+ /// @note Use @ref cpp_kodi_gui_CWindow "kodi::gui::CWindow" to create dialog for them.
+ ///
+ virtual PVR_ERROR OpenDialogChannelScan() { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Call one of the channel related menu hooks (if supported).
+ ///
+ /// Supported @ref cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook instances have to be added in
+ /// `constructor()`, by calling @ref AddMenuHook() on the callback.
+ ///
+ /// @param[in] menuhook The hook to call.
+ /// @param[in] item The selected channel item for which the hook was called.
+ /// @return @ref PVR_ERROR_NO_ERROR if the hook was called successfully.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook_Help
+ ///
+ virtual PVR_ERROR CallChannelMenuHook(const kodi::addon::PVRMenuhook& menuhook,
+ const kodi::addon::PVRChannel& item)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_EPGTag 4. EPG methods (optional)
+ /// @ingroup cpp_kodi_addon_pvr
+ /// @brief **PVR EPG methods**\n
+ /// These C ++ class functions of are intended for processing EPG information
+ /// and for giving it to Kodi.
+ ///
+ /// The necessary data is transferred with @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag.
+ ///
+ /// @remarks Only used by Kodi if @ref PVRCapabilities::SetSupportsEPG "supportsEPG"
+ /// is set to true.\n\n
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **EPG parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_EPGTag_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_EPGTag_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Request the EPG for a channel from the backend.
+ ///
+ /// @param[in] channelUid The UID of the channel to get the EPG table for.
+ /// @param[in] start Get events after this time (UTC).
+ /// @param[in] end Get events before this time (UTC).
+ /// @param[out] results List where available EPG information becomes
+ /// transferred with @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag
+ /// and given to Kodi
+ /// @return @ref PVR_ERROR_NO_ERROR if the table has been fetched successfully.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_epg_PVREPGTag_Help
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsEPG "supportsEPG" is set to true.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// PVR_ERROR CMyPVRInstance::GetEPGForChannel(int channelUid,
+ /// time_t start,
+ /// time_t end,
+ /// kodi::addon::PVREPGTagsResultSet& results)
+ /// {
+ /// // Minimal demo example, in reality bigger, loop to transfer all and to
+ /// // match wanted times.
+ /// kodi::addon::PVREPGTag tag;
+ /// tag.SetUniqueBroadcastId(123);
+ /// tag.SetUniqueChannelId(123);
+ /// tag.SetTitle("My epg entry name");
+ /// tag.SetGenreType(EPG_EVENT_CONTENTMASK_MOVIEDRAMA);
+ /// tag.SetStartTime(1589148283); // Seconds elapsed since 00:00 hours, Jan 1, 1970 UTC
+ /// tag.SetEndTime(1589151913);
+ /// ...
+ ///
+ /// // Give it now to Kodi
+ /// results.Add(tag);
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetEPGForChannel(int channelUid,
+ time_t start,
+ time_t end,
+ kodi::addon::PVREPGTagsResultSet& results)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Check if the given EPG tag can be recorded.
+ ///
+ /// @param[in] tag the @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag "epg tag" to check.
+ /// @param[out] isRecordable Set to true if the tag can be recorded.
+ /// @return @ref PVR_ERROR_NO_ERROR if bIsRecordable has been set successfully.
+ ///
+ /// @remarks Optional, it return @ref PVR_ERROR_NOT_IMPLEMENTED by parent to let Kodi decide.
+ ///
+ virtual PVR_ERROR IsEPGTagRecordable(const kodi::addon::PVREPGTag& tag, bool& isRecordable)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Check if the given EPG tag can be played.
+ ///
+ /// @param[in] tag the @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag "epg tag" to check.
+ /// @param[out] isPlayable Set to true if the tag can be played.
+ /// @return @ref PVR_ERROR_NO_ERROR if bIsPlayable has been set successfully.
+ ///
+ /// @remarks Required if add-on supports playing epg tags.
+ ///
+ virtual PVR_ERROR IsEPGTagPlayable(const kodi::addon::PVREPGTag& tag, bool& isPlayable)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Retrieve the edit decision list (EDL) of an EPG tag on the backend.
+ ///
+ /// @param[in] tag The @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag "epg tag".
+ /// @param[out] edl The function has to write the EDL into this array.
+ /// @return @ref PVR_ERROR_NO_ERROR if the EDL was successfully read or no EDL exists.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsEPGEdl "supportsEPGEdl" is set to true.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_EDLEntry_PVREDLEntry_Help
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsEPGEdl "supportsEPGEdl" is set to true.
+ ///
+ virtual PVR_ERROR GetEPGTagEdl(const kodi::addon::PVREPGTag& tag,
+ std::vector<kodi::addon::PVREDLEntry>& edl)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the stream properties for an epg tag from the backend.
+ ///
+ /// @param[in] tag The @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag "epg tag" to get the stream properties for.
+ /// @param[out] properties the properties required to play the stream.
+ /// @return @ref PVR_ERROR_NO_ERROR if the stream is available.
+ ///
+ /// @remarks Required if add-on supports playing epg tags.
+ /// In this case your implementation must fill the property @ref PVR_STREAM_PROPERTY_STREAMURL
+ /// with the URL Kodi should resolve to playback the epg tag.
+ /// It return @ref PVR_ERROR_NOT_IMPLEMENTED from parent if this add-on won't provide this function.
+ ///
+ /// @note The value directly related to inputstream must always begin with the
+ /// name of the associated add-on, e.g. <b>`"inputstream.adaptive.manifest_update_parameter"`</b>.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// PVR_ERROR CMyPVRInstance::GetEPGTagStreamProperties(const kodi::addon::PVREPGTag& tag,
+ /// std::vector<kodi::addon::PVRStreamProperty>& properties)
+ /// {
+ /// ...
+ /// properties.emplace_back(PVR_STREAM_PROPERTY_INPUTSTREAM, "inputstream.adaptive");
+ /// properties.emplace_back("inputstream.adaptive.manifest_type", "mpd");
+ /// properties.emplace_back("inputstream.adaptive.manifest_update_parameter", "full");
+ /// properties.emplace_back(PVR_STREAM_PROPERTY_MIMETYPE, "application/xml+dash");
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetEPGTagStreamProperties(
+ const kodi::addon::PVREPGTag& tag, std::vector<kodi::addon::PVRStreamProperty>& properties)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Tell the client the past time frame to use when notifying epg events back to Kodi
+ ///
+ /// The client might push epg events asynchronously to Kodi using the callback function
+ /// @ref EpgEventStateChange. To be able to only push events that are actually of
+ /// interest for Kodi, client needs to know about the epg time frame Kodi uses. Kodi supplies
+ /// the current epg max past time frame value @ref EpgMaxPastDays() when creating the addon
+ /// and calls @ref SetEPGMaxPastDays later whenever Kodi's epg time frame value changes.
+ ///
+ /// @param[in] pastDays number of days before "now". @ref EPG_TIMEFRAME_UNLIMITED means that Kodi
+ /// is interested in all epg events, regardless of event times.
+ /// @return @ref PVR_ERROR_NO_ERROR if new value was successfully set.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsEPG "supportsEPG" is set to true.
+ ///
+ virtual PVR_ERROR SetEPGMaxPastDays(int pastDays) { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Tell the client the future time frame to use when notifying epg events back to Kodi
+ ///
+ /// The client might push epg events asynchronously to Kodi using the callback function
+ /// @ref EpgEventStateChange. To be able to only push events that are actually of
+ /// interest for Kodi, client needs to know about the epg time frame Kodi uses. Kodi supplies
+ /// the current epg max future time frame value @ref EpgMaxFutureDays() when creating the addon
+ /// and calls @ref SetEPGMaxFutureDays later whenever Kodi's epg time frame value changes.
+ ///
+ /// @param[in] futureDays number of days from "now". @ref EPG_TIMEFRAME_UNLIMITED means that Kodi
+ /// is interested in all epg events, regardless of event times.
+ /// @return @ref PVR_ERROR_NO_ERROR if new value was successfully set.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsEPG "supportsEPG" is set to true.
+ ///
+ virtual PVR_ERROR SetEPGMaxFutureDays(int futureDays) { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Call one of the EPG related menu hooks (if supported).
+ ///
+ /// Supported @ref cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook instances have to be added in
+ /// `constructor()`, by calling @ref AddMenuHook() on the callback.
+ ///
+ /// @param[in] menuhook The hook to call.
+ /// @param[in] tag The selected EPG item for which the hook was called.
+ /// @return @ref PVR_ERROR_NO_ERROR if the hook was called successfully.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook_Help
+ ///
+ virtual PVR_ERROR CallEPGMenuHook(const kodi::addon::PVRMenuhook& menuhook,
+ const kodi::addon::PVREPGTag& tag)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Get the Max past days handled by Kodi.
+ ///
+ /// If > @ref EPG_TIMEFRAME_UNLIMITED, in async epg mode, deliver only events in the
+ /// range from 'end time > now - EpgMaxPastDays()' to 'start time < now + EpgMaxFutureDays().
+ /// @ref EPG_TIMEFRAME_UNLIMITED, notify all events.
+ ///
+ /// @return The Max past days handled by Kodi
+ ///
+ inline int EpgMaxPastDays() const { return m_instanceData->props->iEpgMaxPastDays; }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Get the Max future days handled by Kodi.
+ ///
+ /// If > @ref EPG_TIMEFRAME_UNLIMITED, in async epg mode, deliver only events in the
+ /// range from 'end time > now - EpgMaxPastDays()' to 'start time < now + EpgMaxFutureDays().
+ /// @ref EPG_TIMEFRAME_UNLIMITED, notify all events.
+ ///
+ /// @return The Max future days handled by Kodi
+ ///
+ inline int EpgMaxFutureDays() const { return m_instanceData->props->iEpgMaxFutureDays; }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Schedule an EPG update for the given channel channel.
+ ///
+ /// @param[in] channelUid The unique id of the channel for this add-on
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ inline void TriggerEpgUpdate(unsigned int channelUid)
+ {
+ m_instanceData->toKodi->TriggerEpgUpdate(m_instanceData->toKodi->kodiInstance, channelUid);
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Notify a state change for an EPG event.
+ ///
+ /// @param[in] tag The @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag "EPG tag" where have event.
+ /// @param[in] newState The new state.
+ /// - For @ref EPG_EVENT_CREATED and @ref EPG_EVENT_UPDATED, tag must be filled with all available event data, not just a delta.
+ /// - For @ref EPG_EVENT_DELETED, it is sufficient to fill @ref kodi::addon::PVREPGTag::SetUniqueBroadcastId
+ ///
+ /// @remarks Only called from addon itself,
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ ///
+ /// void CMyPVRInstance::MyProcessFunction()
+ /// {
+ /// ...
+ /// kodi::addon::PVREPGTag tag; // Here as mini add, in real it should be a complete tag
+ /// tag.SetUniqueId(123);
+ ///
+ /// // added namespace here not needed to have, only to have more clear for where is
+ /// kodi::addon::CInstancePVRClient::EpgEventStateChange(tag, EPG_EVENT_UPDATED);
+ /// ...
+ /// }
+ ///
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ inline void EpgEventStateChange(kodi::addon::PVREPGTag& tag, EPG_EVENT_STATE newState)
+ {
+ m_instanceData->toKodi->EpgEventStateChange(m_instanceData->toKodi->kodiInstance, tag.GetTag(),
+ newState);
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Recordings 5. Recordings (optional)
+ /// @ingroup cpp_kodi_addon_pvr
+ /// @brief **PVR recording methods**\n
+ /// To transfer available recordings of the PVR backend and to allow possible
+ /// playback.
+ ///
+ /// @remarks Only used by Kodi if @ref PVRCapabilities::SetSupportsRecordings "supportsRecordings"
+ /// is set to true.\n\n
+ /// If a recordings changes after the initial import, or if a new one was added,
+ /// then the add-on should call @ref TriggerRecordingUpdate().
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Recordings parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Recordings_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_Recordings_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief To get amount of recording present on backend
+ ///
+ /// @param[in] deleted if set return deleted recording (called if
+ /// @ref PVRCapabilities::SetSupportsRecordingsUndelete "supportsRecordingsUndelete"
+ /// set to true)
+ /// @param[out] amount The total amount of recordings on the backend
+ /// @return @ref PVR_ERROR_NO_ERROR if the amount has been fetched successfully.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsRecordings "supportsRecordings" is set to true.
+ ///
+ virtual PVR_ERROR GetRecordingsAmount(bool deleted, int& amount)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Request the list of all recordings from the backend, if supported.
+ ///
+ /// Recording entries are added to Kodi by calling TransferRecordingEntry() on the callback.
+ ///
+ /// @param[in] deleted if set return deleted recording (called if
+ /// @ref PVRCapabilities::SetSupportsRecordingsUndelete "supportsRecordingsUndelete"
+ /// set to true)
+ /// @param[out] results List of available recordings with @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecording
+ /// becomes transferred with @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecordingsResultSet
+ /// and given to Kodi
+ /// @return @ref PVR_ERROR_NO_ERROR if the recordings have been fetched successfully.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsRecordings "supportsRecordings"
+ /// is set to true.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Recording_PVRRecording_Help
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// PVR_ERROR CMyPVRInstance::GetRecordings(bool deleted, kodi::addon::PVRRecordingsResultSet& results)
+ /// {
+ /// // Minimal demo example, in reality bigger and loop to transfer all
+ /// kodi::addon::PVRRecording recording;
+ /// recording.SetRecordingId(123);
+ /// recording.SetTitle("My recording name");
+ /// ...
+ ///
+ /// // Give it now to Kodi
+ /// results.Add(recording);
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetRecordings(bool deleted, kodi::addon::PVRRecordingsResultSet& results)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Delete a recording on the backend.
+ ///
+ /// @param[in] recording The @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecording to delete.
+ /// @return @ref PVR_ERROR_NO_ERROR if the recording has been deleted successfully.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsRecordings "supportsRecordings"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR DeleteRecording(const kodi::addon::PVRRecording& recording)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Undelete a recording on the backend.
+ ///
+ /// @param[in] recording The @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecording to undelete.
+ /// @return @ref PVR_ERROR_NO_ERROR if the recording has been undeleted successfully.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsRecordingsUndelete "supportsRecordingsUndelete"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR UndeleteRecording(const kodi::addon::PVRRecording& recording)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Delete all recordings permanent which in the deleted folder on the backend.
+ ///
+ /// @return @ref PVR_ERROR_NO_ERROR if the recordings has been deleted successfully.
+ ///
+ virtual PVR_ERROR DeleteAllRecordingsFromTrash() { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Rename a recording on the backend.
+ ///
+ /// @param[in] recording The @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecording
+ /// to rename, containing the new name.
+ /// @return @ref PVR_ERROR_NO_ERROR if the recording has been renamed successfully.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsRecordings "supportsRecordings"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR RenameRecording(const kodi::addon::PVRRecording& recording)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set the lifetime of a recording on the backend.
+ ///
+ /// @param[in] recording The @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecording
+ /// to change the lifetime for. recording.iLifetime
+ /// contains the new lieftime value.
+ /// @return @ref PVR_ERROR_NO_ERROR if the recording's lifetime has been set
+ /// successfully.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsRecordingsLifetimeChange "supportsRecordingsLifetimeChange"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR SetRecordingLifetime(const kodi::addon::PVRRecording& recording)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set the play count of a recording on the backend.
+ ///
+ /// @param[in] recording The @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecording
+ /// to change the play count.
+ /// @param[in] count Play count.
+ /// @return @ref PVR_ERROR_NO_ERROR if the recording's play count has been set
+ /// successfully.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsRecordingPlayCount "supportsRecordingPlayCount"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR SetRecordingPlayCount(const kodi::addon::PVRRecording& recording, int count)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set the last watched position of a recording on the backend.
+ ///
+ /// @param[in] recording The @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecording.
+ /// @param[in] lastplayedposition The last watched position in seconds
+ /// @return @ref PVR_ERROR_NO_ERROR if the position has been stored successfully.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsLastPlayedPosition "supportsLastPlayedPosition"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR SetRecordingLastPlayedPosition(const kodi::addon::PVRRecording& recording,
+ int lastplayedposition)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Retrieve the last watched position of a recording on the backend.
+ ///
+ /// @param[in] recording The @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecording.
+ /// @param[out] position The last watched position in seconds
+ /// @return @ref PVR_ERROR_NO_ERROR if the amount has been fetched successfully.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsRecordingPlayCount "supportsRecordingPlayCount"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR GetRecordingLastPlayedPosition(const kodi::addon::PVRRecording& recording,
+ int& position)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Retrieve the edit decision list (EDL) of a recording on the backend.
+ ///
+ /// @param[in] recording The @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecording.
+ /// @param[out] edl The function has to write the EDL into this array.
+ /// @return @ref PVR_ERROR_NO_ERROR if the EDL was successfully read or no EDL exists.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsRecordingEdl "supportsRecordingEdl"
+ /// is set to true.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_EDLEntry_PVREDLEntry_Help
+ ///
+ virtual PVR_ERROR GetRecordingEdl(const kodi::addon::PVRRecording& recording,
+ std::vector<kodi::addon::PVREDLEntry>& edl)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Retrieve the size of a recording on the backend.
+ ///
+ /// @param[in] recording The recording to get the size in bytes for.
+ /// @param[out] size The size in bytes of the recording
+ /// @return @ref PVR_ERROR_NO_ERROR if the recording's size has been set successfully.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsRecordingSize "supportsRecordingSize"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR GetRecordingSize(const kodi::addon::PVRRecording& recording, int64_t& size)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the stream properties for a recording from the backend.
+ ///
+ /// @param[in] recording The @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecording
+ /// to get the stream properties for.
+ /// @param[out] properties The properties required to play the stream.
+ /// @return @ref PVR_ERROR_NO_ERROR if the stream is available.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetSupportsRecordings "supportsRecordings"
+ /// is set to true and the add-on does not implement recording stream functions
+ /// (@ref OpenRecordedStream, ...).\n
+ /// In this case your implementation must fill the property @ref PVR_STREAM_PROPERTY_STREAMURL
+ /// with the URL Kodi should resolve to playback the recording.
+ ///
+ /// @note The value directly related to inputstream must always begin with the
+ /// name of the associated add-on, e.g. <b>`"inputstream.adaptive.manifest_update_parameter"`</b>.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// PVR_ERROR CMyPVRInstance::GetRecordingStreamProperties(const kodi::addon::PVRRecording& recording,
+ /// std::vector<kodi::addon::PVRStreamProperty>& properties)
+ /// {
+ /// ...
+ /// properties.emplace_back(PVR_STREAM_PROPERTY_INPUTSTREAM, "inputstream.adaptive");
+ /// properties.emplace_back("inputstream.adaptive.manifest_type", "mpd");
+ /// properties.emplace_back("inputstream.adaptive.manifest_update_parameter", "full");
+ /// properties.emplace_back(PVR_STREAM_PROPERTY_MIMETYPE, "application/xml+dash");
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetRecordingStreamProperties(
+ const kodi::addon::PVRRecording& recording,
+ std::vector<kodi::addon::PVRStreamProperty>& properties)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @brief Call one of the recording related menu hooks (if supported).
+ ///
+ /// Supported @ref cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook instances have to be added in
+ /// `constructor()`, by calling @ref AddMenuHook() on the callback.
+ ///
+ /// @param[in] menuhook The hook to call.
+ /// @param[in] item The selected recording item for which the hook was called.
+ /// @return @ref PVR_ERROR_NO_ERROR if the hook was called successfully.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook_Help
+ ///
+ virtual PVR_ERROR CallRecordingMenuHook(const kodi::addon::PVRMenuhook& menuhook,
+ const kodi::addon::PVRRecording& item)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Display a notification in Kodi that a recording started or stopped on the
+ /// server.
+ ///
+ /// @param[in] recordingName The name of the recording to display
+ /// @param[in] fileName The filename of the recording
+ /// @param[in] on True when recording started, false when it stopped
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ inline void RecordingNotification(const std::string& recordingName,
+ const std::string& fileName,
+ bool on)
+ {
+ m_instanceData->toKodi->RecordingNotification(m_instanceData->toKodi->kodiInstance,
+ recordingName.c_str(), fileName.c_str(), on);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Request Kodi to update it's list of recordings.
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ inline void TriggerRecordingUpdate()
+ {
+ m_instanceData->toKodi->TriggerRecordingUpdate(m_instanceData->toKodi->kodiInstance);
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Timers 6. Timers (optional)
+ /// @ingroup cpp_kodi_addon_pvr
+ /// @brief **PVR timer methods**\n
+ /// For editing and displaying timed work, such as video recording.
+ ///
+ /// @remarks Only used by Kodi if @ref PVRCapabilities::SetSupportsTimers "supportsTimers"
+ /// is set to true.\n\n
+ /// If a timer changes after the initial import, or if a new one was added,
+ /// then the add-on should call @ref TriggerTimerUpdate().
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Timer parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Timers_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_Timers_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Retrieve the timer types supported by the backend.
+ ///
+ /// @param[out] types The function has to write the definition of the
+ /// @ref cpp_kodi_addon_pvr_Defs_Timer_PVRTimerType types
+ /// into this array.
+ /// @return @ref PVR_ERROR_NO_ERROR if the types were successfully written to
+ /// the array.
+ ///
+ /// @note Maximal 32 entries are allowed inside.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Timer_PVRTimerType_Help
+ ///
+ virtual PVR_ERROR GetTimerTypes(std::vector<kodi::addon::PVRTimerType>& types)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief To get total amount of timers on the backend or -1 on error.
+ ///
+ /// @param[out] amount The total amount of timers on the backend
+ /// @return @ref PVR_ERROR_NO_ERROR if the amount has been fetched successfully.
+ ///
+ /// @note Required to use if @ref PVRCapabilities::SetSupportsTimers "supportsTimers"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR GetTimersAmount(int& amount) { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Request the list of all timers from the backend if supported.
+ ///
+ /// @param[out] results List of available timers with @ref cpp_kodi_addon_pvr_Defs_Timer_PVRTimer
+ /// becomes transferred with @ref cpp_kodi_addon_pvr_Defs_Timer_PVRTimersResultSet
+ /// and given to Kodi
+ /// @return @ref PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ ///
+ /// @note Required to use if @ref PVRCapabilities::SetSupportsTimers "supportsTimers"
+ /// is set to true.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Timer_PVRTimer_Help
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// PVR_ERROR CMyPVRInstance::GetTimers(kodi::addon::PVRTimersResultSet& results)
+ /// {
+ /// // Minimal demo example, in reality bigger and loop to transfer all
+ /// kodi::addon::PVRTimer timer;
+ /// timer.SetClientIndex(123);
+ /// timer.SetState(PVR_TIMER_STATE_SCHEDULED);
+ /// timer.SetTitle("My timer name");
+ /// ...
+ ///
+ /// // Give it now to Kodi
+ /// results.Add(timer);
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual PVR_ERROR GetTimers(kodi::addon::PVRTimersResultSet& results)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Add a timer on the backend.
+ ///
+ /// @param[in] timer The timer to add.
+ /// @return @ref PVR_ERROR_NO_ERROR if the timer has been added successfully.
+ ///
+ /// @note Required to use if @ref PVRCapabilities::SetSupportsTimers "supportsTimers"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR AddTimer(const kodi::addon::PVRTimer& timer)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Delete a timer on the backend.
+ ///
+ /// @param[in] timer The timer to delete.
+ /// @param[in] forceDelete Set to true to delete a timer that is currently
+ /// recording a program.
+ /// @return @ref PVR_ERROR_NO_ERROR if the timer has been deleted successfully.
+ ///
+ /// @note Required to use if @ref PVRCapabilities::SetSupportsTimers "supportsTimers"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR DeleteTimer(const kodi::addon::PVRTimer& timer, bool forceDelete)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Update the timer information on the backend.
+ ///
+ /// @param[in] timer The timer to update.
+ /// @return @ref PVR_ERROR_NO_ERROR if the timer has been updated successfully.
+ ///
+ /// @note Required to use if @ref PVRCapabilities::SetSupportsTimers "supportsTimers"
+ /// is set to true.
+ ///
+ virtual PVR_ERROR UpdateTimer(const kodi::addon::PVRTimer& timer)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Call one of the timer related menu hooks (if supported).
+ ///
+ /// Supported @ref cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook instances have
+ /// to be added in `constructor()`, by calling @ref AddMenuHook() on the
+ /// callback.
+ ///
+ /// @param[in] menuhook The hook to call.
+ /// @param[in] item The selected timer item for which the hook was called.
+ /// @return @ref PVR_ERROR_NO_ERROR if the hook was called successfully.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook_Help
+ ///
+ virtual PVR_ERROR CallTimerMenuHook(const kodi::addon::PVRMenuhook& menuhook,
+ const kodi::addon::PVRTimer& item)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Request Kodi to update it's list of timers.
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ inline void TriggerTimerUpdate()
+ {
+ m_instanceData->toKodi->TriggerTimerUpdate(m_instanceData->toKodi->kodiInstance);
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_PowerManagement 7. Power management events (optional)
+ /// @ingroup cpp_kodi_addon_pvr
+ /// @brief **Used to notify the pvr addon for power management events**\n
+ /// Used to allow any energy savings.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Power management events in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_PowerManagement_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_PowerManagement_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief To notify addon about system sleep
+ ///
+ /// @return @ref PVR_ERROR_NO_ERROR If successfully done.
+ ///
+ virtual PVR_ERROR OnSystemSleep() { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief To notify addon about system wake up
+ ///
+ /// @return @ref PVR_ERROR_NO_ERROR If successfully done.
+ ///
+ virtual PVR_ERROR OnSystemWake() { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief To notify addon power saving on system is activated
+ ///
+ /// @return @ref PVR_ERROR_NO_ERROR If successfully done.
+ ///
+ virtual PVR_ERROR OnPowerSavingActivated() { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief To notify addon power saving on system is deactivated
+ ///
+ /// @return @ref PVR_ERROR_NO_ERROR If successfully done.
+ ///
+ virtual PVR_ERROR OnPowerSavingDeactivated() { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ ///@}
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Streams 8. Inputstream
+ /// @ingroup cpp_kodi_addon_pvr
+ /// @brief **PVR Inputstream**\n
+ /// This includes functions that are used in the PVR inputstream.
+ ///
+ /// @warning The parts here will be removed in the future and replaced by the
+ /// separate @ref cpp_kodi_addon_inputstream "inputstream addon instance".
+ /// If there is already a possibility, new addons should do it via the
+ /// inputstream instance.
+ ///
+ ///@{
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Streams_TV 8.1. TV stream
+ /// @ingroup cpp_kodi_addon_pvr_Streams
+ /// @brief **PVR TV stream**\n
+ /// Stream processing regarding live TV.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **TV stream parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Streams_TV_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_Streams_TV_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Open a live stream on the backend.
+ ///
+ /// @param[in] channel The channel to stream.
+ /// @return True if the stream has been opened successfully, false otherwise.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Channel_PVRChannel_Help
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetHandlesInputStream() or
+ /// @ref PVRCapabilities::SetHandlesDemuxing() is set to true.
+ /// @ref CloseLiveStream() will always be called by Kodi prior to calling this
+ /// function.
+ ///
+ virtual bool OpenLiveStream(const kodi::addon::PVRChannel& channel) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Close an open live stream.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetHandlesInputStream() or
+ /// @ref PVRCapabilities::SetHandlesDemuxing() is set to true.
+ ///
+ virtual void CloseLiveStream() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Read from an open live stream.
+ ///
+ /// @param[in] pBuffer The buffer to store the data in.
+ /// @param[in] iBufferSize The amount of bytes to read.
+ /// @return The amount of bytes that were actually read from the stream.
+ ///
+ /// @remarks Required if @ref PVRCapabilities::SetHandlesInputStream() is set
+ /// to true.
+ ///
+ virtual int ReadLiveStream(unsigned char* buffer, unsigned int size) { return 0; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Seek in a live stream on a backend that supports timeshifting.
+ ///
+ /// @param[in] position The position to seek to.
+ /// @param[in] whence [optional] offset relative to
+ /// You can set the value of whence to one of three things:
+ /// | Value | int | Description |
+ /// |:--------:|:---:|:----------------------------------------------------|
+ /// | SEEK_SET | 0 | position is relative to the beginning of the file. This is probably what you had in mind anyway, and is the most commonly used value for whence.
+ /// | SEEK_CUR | 1 | position is relative to the current file pointer position. So, in effect, you can say, "Move to my current position plus 30 bytes," or, "move to my current position minus 20 bytes."
+ /// | SEEK_END | 2 | position is relative to the end of the file. Just like SEEK_SET except from the other end of the file. Be sure to use negative values for offset if you want to back up from the end of the file, instead of going past the end into oblivion.
+ ///
+ /// @return The new position.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetHandlesInputStream()
+ /// is set to true.
+ ///
+ virtual int64_t SeekLiveStream(int64_t position, int whence) { return 0; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Obtain the length of a live stream.
+ ///
+ /// @return The total length of the stream that's currently being read.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetHandlesInputStream()
+ /// is set to true.
+ ///
+ virtual int64_t LengthLiveStream() { return 0; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Streams_TV_Demux 8.1.1. Stream demuxing
+ /// @ingroup cpp_kodi_addon_pvr_Streams_TV
+ /// @brief **PVR stream demuxing**\n
+ /// Read TV streams with own demux within addon.
+ ///
+ /// This is only on Live TV streams and only if @ref PVRCapabilities::SetHandlesDemuxing()
+ /// has been set to "true".
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Stream demuxing parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Streams_TV_Demux_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_Streams_TV_Demux_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Get the stream properties of the stream that's currently being read.
+ ///
+ /// @param[in] properties The properties of the currently playing stream.
+ /// @return @ref PVR_ERROR_NO_ERROR if the properties have been fetched successfully.
+ ///
+ /// @remarks Required, and only used if addon has its own demuxer.
+ ///
+ virtual PVR_ERROR GetStreamProperties(std::vector<kodi::addon::PVRStreamProperties>& properties)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Read the next packet from the demultiplexer, if there is one.
+ ///
+ /// @return The next packet.
+ /// If there is no next packet, then the add-on should return the packet
+ /// created by calling @ref AllocateDemuxPacket(0) on the callback.
+ /// If the stream changed and Kodi's player needs to be reinitialised, then,
+ /// the add-on should call @ref AllocateDemuxPacket(0) on the callback, and set
+ /// the streamid to @ref DMX_SPECIALID_STREAMCHANGE and return the value.
+ /// The add-on should return `nullptr` if an error occurred.
+ ///
+ /// @remarks Required, and only used if addon has its own demuxer.
+ /// Return `nullptr` if this add-on won't provide this function.
+ ///
+ virtual DEMUX_PACKET* DemuxRead() { return nullptr; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Reset the demultiplexer in the add-on.
+ ///
+ /// @remarks Required, and only used if addon has its own demuxer.
+ ///
+ virtual void DemuxReset() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Abort the demultiplexer thread in the add-on.
+ ///
+ /// @remarks Required, and only used if addon has its own demuxer.
+ ///
+ virtual void DemuxAbort() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Flush all data that's currently in the demultiplexer buffer in the
+ /// add-on.
+ ///
+ /// @remarks Required, and only used if addon has its own demuxer.
+ ///
+ virtual void DemuxFlush() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Notify the pvr addon/demuxer that Kodi wishes to change playback
+ /// speed.
+ ///
+ /// @param[in] speed The requested playback speed
+ ///
+ /// @remarks Optional, and only used if addon has its own demuxer.
+ ///
+ virtual void SetSpeed(int speed) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Notify the pvr addon/demuxer that Kodi wishes to fill demux queue.
+ ///
+ /// @param[in] mode The requested filling mode
+ ///
+ /// @remarks Optional, and only used if addon has its own demuxer.
+ ///
+ virtual void FillBuffer(bool mode) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Notify the pvr addon/demuxer that Kodi wishes to seek the stream by
+ /// time.
+ ///
+ /// @param[in] time The absolute time since stream start
+ /// @param[in] backwards True to seek to keyframe BEFORE time, else AFTER
+ /// @param[in] startpts can be updated to point to where display should start
+ /// @return True if the seek operation was possible
+ ///
+ /// @remarks Optional, and only used if addon has its own demuxer.
+ /// Return False if this add-on won't provide this function.
+ ///
+ virtual bool SeekTime(double time, bool backwards, double& startpts) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Get the codec id used by Kodi.
+ ///
+ /// @param[in] codecName The name of the codec
+ /// @return The codec_id, or a codec_id with 0 values when not supported
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ inline PVRCodec GetCodecByName(const std::string& codecName) const
+ {
+ return PVRCodec(m_instanceData->toKodi->GetCodecByName(m_instanceData->toKodi->kodiInstance,
+ codecName.c_str()));
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Allocate a demux packet. Free with @ref FreeDemuxPacket().
+ ///
+ /// @param[in] iDataSize The size of the data that will go into the packet
+ /// @return The allocated packet
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ inline DEMUX_PACKET* AllocateDemuxPacket(int iDataSize)
+ {
+ return m_instanceData->toKodi->AllocateDemuxPacket(m_instanceData->toKodi->kodiInstance,
+ iDataSize);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Callback to Kodi Function**\n
+ /// Free a packet that was allocated with @ref AllocateDemuxPacket().
+ ///
+ /// @param[in] pPacket The packet to free
+ ///
+ /// @remarks Only called from addon itself.
+ ///
+ inline void FreeDemuxPacket(DEMUX_PACKET* pPacket)
+ {
+ m_instanceData->toKodi->FreeDemuxPacket(m_instanceData->toKodi->kodiInstance, pPacket);
+ }
+ //----------------------------------------------------------------------------
+ ///@}
+
+ ///@}
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Streams_Recording 8.2. Recording stream
+ /// @ingroup cpp_kodi_addon_pvr_Streams
+ /// @brief **PVR Recording stream**\n
+ /// Stream processing regarding recordings.
+ ///
+ /// @note Demuxing is not possible with the recordings.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Recording stream parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Streams_Recording_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_Streams_Recording_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Open a stream to a recording on the backend.
+ ///
+ /// @param[in] recording The recording to open.
+ /// @return True if the stream has been opened successfully, false otherwise.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsRecordings()
+ /// is set to true. @ref CloseRecordedStream() will always be called by Kodi
+ /// prior to calling this function.
+ ///
+ virtual bool OpenRecordedStream(const kodi::addon::PVRRecording& recording) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Close an open stream from a recording.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsRecordings()
+ /// is set to true.
+ ///
+ virtual void CloseRecordedStream() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Read from a recording.
+ ///
+ /// @param[in] buffer The buffer to store the data in.
+ /// @param[in] size The amount of bytes to read.
+ /// @return The amount of bytes that were actually read from the stream.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsRecordings()
+ /// is set to true.
+ ///
+ virtual int ReadRecordedStream(unsigned char* buffer, unsigned int size) { return 0; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Seek in a recorded stream.
+ ///
+ /// @param[in] position The position to seek to.
+ /// @param[in] whence [optional] offset relative to
+ /// You can set the value of whence to one of three things:
+ /// | Value | int | Description |
+ /// |:--------:|:---:|:----------------------------------------------------|
+ /// | SEEK_SET | 0 | position is relative to the beginning of the file. This is probably what you had in mind anyway, and is the most commonly used value for whence.
+ /// | SEEK_CUR | 1 | position is relative to the current file pointer position. So, in effect, you can say, "Move to my current position plus 30 bytes," or, "move to my current position minus 20 bytes."
+ /// | SEEK_END | 2 | position is relative to the end of the file. Just like SEEK_SET except from the other end of the file. Be sure to use negative values for offset if you want to back up from the end of the file, instead of going past the end into oblivion.
+ ///
+ /// @return The new position.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsRecordings()
+ /// is set to true.
+ ///
+ virtual int64_t SeekRecordedStream(int64_t position, int whence) { return 0; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Obtain the length of a recorded stream.
+ ///
+ /// @return The total length of the stream that's currently being read.
+ ///
+ /// @remarks Optional, and only used if @ref PVRCapabilities::SetSupportsRecordings()
+ /// is true (=> @ref ReadRecordedStream).
+ ///
+ virtual int64_t LengthRecordedStream() { return 0; }
+ //----------------------------------------------------------------------------
+
+ ///@}
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Streams_Various 8.3. Various functions
+ /// @ingroup cpp_kodi_addon_pvr_Streams
+ /// @brief **Various other PVR stream related functions**\n
+ /// These apply to all other groups in inputstream and are therefore declared
+ /// as several.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Various stream parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Streams_Various_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_pvr_Streams_Various_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ ///
+ /// @brief Check if the backend support pausing the currently playing stream.
+ ///
+ /// This will enable/disable the pause button in Kodi based on the return
+ /// value.
+ ///
+ /// @return false if the PVR addon/backend does not support pausing, true if
+ /// possible
+ ///
+ virtual bool CanPauseStream() { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ ///
+ /// @brief Check if the backend supports seeking for the currently playing
+ /// stream.
+ ///
+ /// This will enable/disable the rewind/forward buttons in Kodi based on the
+ /// return value.
+ ///
+ /// @return false if the PVR addon/backend does not support seeking, true if
+ /// possible
+ ///
+ virtual bool CanSeekStream() { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ ///
+ /// @brief Notify the pvr addon that Kodi (un)paused the currently playing
+ /// stream.
+ ///
+ /// @param[in] paused To inform by `true` is paused and with `false` playing
+ ///
+ virtual void PauseStream(bool paused) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ ///
+ /// @brief Check for real-time streaming.
+ ///
+ /// @return true if current stream is real-time
+ ///
+ virtual bool IsRealTimeStream() { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ ///
+ /// @brief Get stream times.
+ ///
+ /// @param[out] times A pointer to the data to be filled by the implementation.
+ /// @return @ref PVR_ERROR_NO_ERROR on success.
+ ///
+ virtual PVR_ERROR GetStreamTimes(kodi::addon::PVRStreamTimes& times)
+ {
+ return PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ ///
+ /// @brief Obtain the chunk size to use when reading streams.
+ ///
+ /// @param[out] chunksize must be filled with the chunk size in bytes.
+ /// @return @ref PVR_ERROR_NO_ERROR if the chunk size has been fetched successfully.
+ ///
+ /// @remarks Optional, and only used if not reading from demuxer (=> @ref DemuxRead) and
+ /// @ref PVRCapabilities::SetSupportsRecordings() is true (=> @ref ReadRecordedStream) or
+ /// @ref PVRCapabilities::SetHandlesInputStream() is true (=> @ref ReadLiveStream).
+ ///
+ virtual PVR_ERROR GetStreamReadChunkSize(int& chunksize) { return PVR_ERROR_NOT_IMPLEMENTED; }
+ //----------------------------------------------------------------------------
+
+ ///@}
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+private:
+ void SetAddonStruct(KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ instance->hdl = this;
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->GetCapabilities = ADDON_GetCapabilities;
+ instance->pvr->toAddon->GetConnectionString = ADDON_GetConnectionString;
+ instance->pvr->toAddon->GetBackendName = ADDON_GetBackendName;
+ instance->pvr->toAddon->GetBackendVersion = ADDON_GetBackendVersion;
+ instance->pvr->toAddon->GetBackendHostname = ADDON_GetBackendHostname;
+ instance->pvr->toAddon->GetDriveSpace = ADDON_GetDriveSpace;
+ instance->pvr->toAddon->CallSettingsMenuHook = ADDON_CallSettingsMenuHook;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->GetChannelsAmount = ADDON_GetChannelsAmount;
+ instance->pvr->toAddon->GetChannels = ADDON_GetChannels;
+ instance->pvr->toAddon->GetChannelStreamProperties = ADDON_GetChannelStreamProperties;
+ instance->pvr->toAddon->GetSignalStatus = ADDON_GetSignalStatus;
+ instance->pvr->toAddon->GetDescrambleInfo = ADDON_GetDescrambleInfo;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->GetProvidersAmount = ADDON_GetProvidersAmount;
+ instance->pvr->toAddon->GetProviders = ADDON_GetProviders;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->GetChannelGroupsAmount = ADDON_GetChannelGroupsAmount;
+ instance->pvr->toAddon->GetChannelGroups = ADDON_GetChannelGroups;
+ instance->pvr->toAddon->GetChannelGroupMembers = ADDON_GetChannelGroupMembers;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->DeleteChannel = ADDON_DeleteChannel;
+ instance->pvr->toAddon->RenameChannel = ADDON_RenameChannel;
+ instance->pvr->toAddon->OpenDialogChannelSettings = ADDON_OpenDialogChannelSettings;
+ instance->pvr->toAddon->OpenDialogChannelAdd = ADDON_OpenDialogChannelAdd;
+ instance->pvr->toAddon->OpenDialogChannelScan = ADDON_OpenDialogChannelScan;
+ instance->pvr->toAddon->CallChannelMenuHook = ADDON_CallChannelMenuHook;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->GetEPGForChannel = ADDON_GetEPGForChannel;
+ instance->pvr->toAddon->IsEPGTagRecordable = ADDON_IsEPGTagRecordable;
+ instance->pvr->toAddon->IsEPGTagPlayable = ADDON_IsEPGTagPlayable;
+ instance->pvr->toAddon->GetEPGTagEdl = ADDON_GetEPGTagEdl;
+ instance->pvr->toAddon->GetEPGTagStreamProperties = ADDON_GetEPGTagStreamProperties;
+ instance->pvr->toAddon->SetEPGMaxPastDays = ADDON_SetEPGMaxPastDays;
+ instance->pvr->toAddon->SetEPGMaxFutureDays = ADDON_SetEPGMaxFutureDays;
+ instance->pvr->toAddon->CallEPGMenuHook = ADDON_CallEPGMenuHook;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->GetRecordingsAmount = ADDON_GetRecordingsAmount;
+ instance->pvr->toAddon->GetRecordings = ADDON_GetRecordings;
+ instance->pvr->toAddon->DeleteRecording = ADDON_DeleteRecording;
+ instance->pvr->toAddon->UndeleteRecording = ADDON_UndeleteRecording;
+ instance->pvr->toAddon->DeleteAllRecordingsFromTrash = ADDON_DeleteAllRecordingsFromTrash;
+ instance->pvr->toAddon->RenameRecording = ADDON_RenameRecording;
+ instance->pvr->toAddon->SetRecordingLifetime = ADDON_SetRecordingLifetime;
+ instance->pvr->toAddon->SetRecordingPlayCount = ADDON_SetRecordingPlayCount;
+ instance->pvr->toAddon->SetRecordingLastPlayedPosition = ADDON_SetRecordingLastPlayedPosition;
+ instance->pvr->toAddon->GetRecordingLastPlayedPosition = ADDON_GetRecordingLastPlayedPosition;
+ instance->pvr->toAddon->GetRecordingEdl = ADDON_GetRecordingEdl;
+ instance->pvr->toAddon->GetRecordingSize = ADDON_GetRecordingSize;
+ instance->pvr->toAddon->GetRecordingStreamProperties = ADDON_GetRecordingStreamProperties;
+ instance->pvr->toAddon->CallRecordingMenuHook = ADDON_CallRecordingMenuHook;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->GetTimerTypes = ADDON_GetTimerTypes;
+ instance->pvr->toAddon->GetTimersAmount = ADDON_GetTimersAmount;
+ instance->pvr->toAddon->GetTimers = ADDON_GetTimers;
+ instance->pvr->toAddon->AddTimer = ADDON_AddTimer;
+ instance->pvr->toAddon->DeleteTimer = ADDON_DeleteTimer;
+ instance->pvr->toAddon->UpdateTimer = ADDON_UpdateTimer;
+ instance->pvr->toAddon->CallTimerMenuHook = ADDON_CallTimerMenuHook;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->OnSystemSleep = ADDON_OnSystemSleep;
+ instance->pvr->toAddon->OnSystemWake = ADDON_OnSystemWake;
+ instance->pvr->toAddon->OnPowerSavingActivated = ADDON_OnPowerSavingActivated;
+ instance->pvr->toAddon->OnPowerSavingDeactivated = ADDON_OnPowerSavingDeactivated;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->OpenLiveStream = ADDON_OpenLiveStream;
+ instance->pvr->toAddon->CloseLiveStream = ADDON_CloseLiveStream;
+ instance->pvr->toAddon->ReadLiveStream = ADDON_ReadLiveStream;
+ instance->pvr->toAddon->SeekLiveStream = ADDON_SeekLiveStream;
+ instance->pvr->toAddon->LengthLiveStream = ADDON_LengthLiveStream;
+ instance->pvr->toAddon->GetStreamProperties = ADDON_GetStreamProperties;
+ instance->pvr->toAddon->GetStreamReadChunkSize = ADDON_GetStreamReadChunkSize;
+ instance->pvr->toAddon->IsRealTimeStream = ADDON_IsRealTimeStream;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->OpenRecordedStream = ADDON_OpenRecordedStream;
+ instance->pvr->toAddon->CloseRecordedStream = ADDON_CloseRecordedStream;
+ instance->pvr->toAddon->ReadRecordedStream = ADDON_ReadRecordedStream;
+ instance->pvr->toAddon->SeekRecordedStream = ADDON_SeekRecordedStream;
+ instance->pvr->toAddon->LengthRecordedStream = ADDON_LengthRecordedStream;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->DemuxReset = ADDON_DemuxReset;
+ instance->pvr->toAddon->DemuxAbort = ADDON_DemuxAbort;
+ instance->pvr->toAddon->DemuxFlush = ADDON_DemuxFlush;
+ instance->pvr->toAddon->DemuxRead = ADDON_DemuxRead;
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ instance->pvr->toAddon->CanPauseStream = ADDON_CanPauseStream;
+ instance->pvr->toAddon->PauseStream = ADDON_PauseStream;
+ instance->pvr->toAddon->CanSeekStream = ADDON_CanSeekStream;
+ instance->pvr->toAddon->SeekTime = ADDON_SeekTime;
+ instance->pvr->toAddon->SetSpeed = ADDON_SetSpeed;
+ instance->pvr->toAddon->FillBuffer = ADDON_FillBuffer;
+ instance->pvr->toAddon->GetStreamTimes = ADDON_GetStreamTimes;
+
+ m_instanceData = instance->pvr;
+ m_instanceData->toAddon->addonInstance = this;
+ }
+
+ inline static PVR_ERROR ADDON_GetCapabilities(const AddonInstance_PVR* instance,
+ PVR_ADDON_CAPABILITIES* capabilities)
+ {
+ PVRCapabilities cppCapabilities(capabilities);
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetCapabilities(cppCapabilities);
+ }
+
+ inline static PVR_ERROR ADDON_GetBackendName(const AddonInstance_PVR* instance,
+ char* str,
+ int memSize)
+ {
+ std::string backendName;
+ PVR_ERROR err = static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetBackendName(backendName);
+ if (err == PVR_ERROR_NO_ERROR)
+ strncpy(str, backendName.c_str(), memSize);
+ return err;
+ }
+
+ inline static PVR_ERROR ADDON_GetBackendVersion(const AddonInstance_PVR* instance,
+ char* str,
+ int memSize)
+ {
+ std::string backendVersion;
+ PVR_ERROR err = static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetBackendVersion(backendVersion);
+ if (err == PVR_ERROR_NO_ERROR)
+ strncpy(str, backendVersion.c_str(), memSize);
+ return err;
+ }
+
+ inline static PVR_ERROR ADDON_GetBackendHostname(const AddonInstance_PVR* instance,
+ char* str,
+ int memSize)
+ {
+ std::string backendHostname;
+ PVR_ERROR err = static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetBackendHostname(backendHostname);
+ if (err == PVR_ERROR_NO_ERROR)
+ strncpy(str, backendHostname.c_str(), memSize);
+ return err;
+ }
+
+ inline static PVR_ERROR ADDON_GetConnectionString(const AddonInstance_PVR* instance,
+ char* str,
+ int memSize)
+ {
+ std::string connectionString;
+ PVR_ERROR err = static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetConnectionString(connectionString);
+ if (err == PVR_ERROR_NO_ERROR)
+ strncpy(str, connectionString.c_str(), memSize);
+ return err;
+ }
+
+ inline static PVR_ERROR ADDON_GetDriveSpace(const AddonInstance_PVR* instance,
+ uint64_t* total,
+ uint64_t* used)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetDriveSpace(*total, *used);
+ }
+
+ inline static PVR_ERROR ADDON_CallSettingsMenuHook(const AddonInstance_PVR* instance,
+ const PVR_MENUHOOK* menuhook)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->CallSettingsMenuHook(menuhook);
+ }
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ inline static PVR_ERROR ADDON_GetChannelsAmount(const AddonInstance_PVR* instance, int* amount)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetChannelsAmount(*amount);
+ }
+
+ inline static PVR_ERROR ADDON_GetChannels(const AddonInstance_PVR* instance,
+ PVR_HANDLE handle,
+ bool radio)
+ {
+ PVRChannelsResultSet result(instance, handle);
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetChannels(radio, result);
+ }
+
+ inline static PVR_ERROR ADDON_GetChannelStreamProperties(const AddonInstance_PVR* instance,
+ const PVR_CHANNEL* channel,
+ PVR_NAMED_VALUE* properties,
+ unsigned int* propertiesCount)
+ {
+ *propertiesCount = 0;
+ std::vector<PVRStreamProperty> propertiesList;
+ PVR_ERROR error = static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetChannelStreamProperties(channel, propertiesList);
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ for (const auto& property : propertiesList)
+ {
+ strncpy(properties[*propertiesCount].strName, property.GetCStructure()->strName,
+ sizeof(properties[*propertiesCount].strName) - 1);
+ strncpy(properties[*propertiesCount].strValue, property.GetCStructure()->strValue,
+ sizeof(properties[*propertiesCount].strValue) - 1);
+ ++*propertiesCount;
+ if (*propertiesCount > STREAM_MAX_PROPERTY_COUNT)
+ break;
+ }
+ }
+ return error;
+ }
+
+ inline static PVR_ERROR ADDON_GetSignalStatus(const AddonInstance_PVR* instance,
+ int channelUid,
+ PVR_SIGNAL_STATUS* signalStatus)
+ {
+ PVRSignalStatus cppSignalStatus(signalStatus);
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetSignalStatus(channelUid, cppSignalStatus);
+ }
+
+ inline static PVR_ERROR ADDON_GetDescrambleInfo(const AddonInstance_PVR* instance,
+ int channelUid,
+ PVR_DESCRAMBLE_INFO* descrambleInfo)
+ {
+ PVRDescrambleInfo cppDescrambleInfo(descrambleInfo);
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetDescrambleInfo(channelUid, cppDescrambleInfo);
+ }
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ inline static PVR_ERROR ADDON_GetProvidersAmount(const AddonInstance_PVR* instance, int* amount)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetProvidersAmount(*amount);
+ }
+
+ inline static PVR_ERROR ADDON_GetProviders(const AddonInstance_PVR* instance, PVR_HANDLE handle)
+ {
+ PVRProvidersResultSet result(instance, handle);
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->GetProviders(result);
+ }
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ inline static PVR_ERROR ADDON_GetChannelGroupsAmount(const AddonInstance_PVR* instance,
+ int* amount)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetChannelGroupsAmount(*amount);
+ }
+
+ inline static PVR_ERROR ADDON_GetChannelGroups(const AddonInstance_PVR* instance,
+ PVR_HANDLE handle,
+ bool radio)
+ {
+ PVRChannelGroupsResultSet result(instance, handle);
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetChannelGroups(radio, result);
+ }
+
+ inline static PVR_ERROR ADDON_GetChannelGroupMembers(const AddonInstance_PVR* instance,
+ PVR_HANDLE handle,
+ const PVR_CHANNEL_GROUP* group)
+ {
+ PVRChannelGroupMembersResultSet result(instance, handle);
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetChannelGroupMembers(group, result);
+ }
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ inline static PVR_ERROR ADDON_DeleteChannel(const AddonInstance_PVR* instance,
+ const PVR_CHANNEL* channel)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->DeleteChannel(channel);
+ }
+
+ inline static PVR_ERROR ADDON_RenameChannel(const AddonInstance_PVR* instance,
+ const PVR_CHANNEL* channel)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->RenameChannel(channel);
+ }
+
+ inline static PVR_ERROR ADDON_OpenDialogChannelSettings(const AddonInstance_PVR* instance,
+ const PVR_CHANNEL* channel)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->OpenDialogChannelSettings(channel);
+ }
+
+ inline static PVR_ERROR ADDON_OpenDialogChannelAdd(const AddonInstance_PVR* instance,
+ const PVR_CHANNEL* channel)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->OpenDialogChannelAdd(channel);
+ }
+
+ inline static PVR_ERROR ADDON_OpenDialogChannelScan(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->OpenDialogChannelScan();
+ }
+
+ inline static PVR_ERROR ADDON_CallChannelMenuHook(const AddonInstance_PVR* instance,
+ const PVR_MENUHOOK* menuhook,
+ const PVR_CHANNEL* channel)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->CallChannelMenuHook(menuhook, channel);
+ }
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ inline static PVR_ERROR ADDON_GetEPGForChannel(const AddonInstance_PVR* instance,
+ PVR_HANDLE handle,
+ int channelUid,
+ time_t start,
+ time_t end)
+ {
+ PVREPGTagsResultSet result(instance, handle);
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetEPGForChannel(channelUid, start, end, result);
+ }
+
+ inline static PVR_ERROR ADDON_IsEPGTagRecordable(const AddonInstance_PVR* instance,
+ const EPG_TAG* tag,
+ bool* isRecordable)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->IsEPGTagRecordable(tag, *isRecordable);
+ }
+
+ inline static PVR_ERROR ADDON_IsEPGTagPlayable(const AddonInstance_PVR* instance,
+ const EPG_TAG* tag,
+ bool* isPlayable)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->IsEPGTagPlayable(tag, *isPlayable);
+ }
+
+ inline static PVR_ERROR ADDON_GetEPGTagEdl(const AddonInstance_PVR* instance,
+ const EPG_TAG* tag,
+ PVR_EDL_ENTRY* edl,
+ int* size)
+ {
+ std::vector<PVREDLEntry> edlList;
+ PVR_ERROR error = static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetEPGTagEdl(tag, edlList);
+ if (static_cast<int>(edlList.size()) > *size)
+ {
+ kodi::Log(
+ ADDON_LOG_WARNING,
+ "CInstancePVRClient::%s: Truncating %d EDL entries from client to permitted size %d",
+ __func__, static_cast<int>(edlList.size()), *size);
+ edlList.resize(*size);
+ }
+ *size = 0;
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ for (const auto& edlEntry : edlList)
+ {
+ edl[*size] = *edlEntry;
+ ++*size;
+ }
+ }
+ return error;
+ }
+
+ inline static PVR_ERROR ADDON_GetEPGTagStreamProperties(const AddonInstance_PVR* instance,
+ const EPG_TAG* tag,
+ PVR_NAMED_VALUE* properties,
+ unsigned int* propertiesCount)
+ {
+ *propertiesCount = 0;
+ std::vector<PVRStreamProperty> propertiesList;
+ PVR_ERROR error = static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetEPGTagStreamProperties(tag, propertiesList);
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ for (const auto& property : propertiesList)
+ {
+ strncpy(properties[*propertiesCount].strName, property.GetCStructure()->strName,
+ sizeof(properties[*propertiesCount].strName) - 1);
+ strncpy(properties[*propertiesCount].strValue, property.GetCStructure()->strValue,
+ sizeof(properties[*propertiesCount].strValue) - 1);
+ ++*propertiesCount;
+ if (*propertiesCount > STREAM_MAX_PROPERTY_COUNT)
+ break;
+ }
+ }
+ return error;
+ }
+
+ inline static PVR_ERROR ADDON_SetEPGMaxPastDays(const AddonInstance_PVR* instance, int pastDays)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->SetEPGMaxPastDays(pastDays);
+ }
+
+ inline static PVR_ERROR ADDON_SetEPGMaxFutureDays(const AddonInstance_PVR* instance,
+ int futureDays)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->SetEPGMaxFutureDays(futureDays);
+ }
+
+ inline static PVR_ERROR ADDON_CallEPGMenuHook(const AddonInstance_PVR* instance,
+ const PVR_MENUHOOK* menuhook,
+ const EPG_TAG* tag)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->CallEPGMenuHook(menuhook, tag);
+ }
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ inline static PVR_ERROR ADDON_GetRecordingsAmount(const AddonInstance_PVR* instance,
+ bool deleted,
+ int* amount)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetRecordingsAmount(deleted, *amount);
+ }
+
+ inline static PVR_ERROR ADDON_GetRecordings(const AddonInstance_PVR* instance,
+ PVR_HANDLE handle,
+ bool deleted)
+ {
+ PVRRecordingsResultSet result(instance, handle);
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetRecordings(deleted, result);
+ }
+
+ inline static PVR_ERROR ADDON_DeleteRecording(const AddonInstance_PVR* instance,
+ const PVR_RECORDING* recording)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->DeleteRecording(recording);
+ }
+
+ inline static PVR_ERROR ADDON_UndeleteRecording(const AddonInstance_PVR* instance,
+ const PVR_RECORDING* recording)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->UndeleteRecording(recording);
+ }
+
+ inline static PVR_ERROR ADDON_DeleteAllRecordingsFromTrash(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->DeleteAllRecordingsFromTrash();
+ }
+
+ inline static PVR_ERROR ADDON_RenameRecording(const AddonInstance_PVR* instance,
+ const PVR_RECORDING* recording)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->RenameRecording(recording);
+ }
+
+ inline static PVR_ERROR ADDON_SetRecordingLifetime(const AddonInstance_PVR* instance,
+ const PVR_RECORDING* recording)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->SetRecordingLifetime(recording);
+ }
+
+ inline static PVR_ERROR ADDON_SetRecordingPlayCount(const AddonInstance_PVR* instance,
+ const PVR_RECORDING* recording,
+ int count)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->SetRecordingPlayCount(recording, count);
+ }
+
+ inline static PVR_ERROR ADDON_SetRecordingLastPlayedPosition(const AddonInstance_PVR* instance,
+ const PVR_RECORDING* recording,
+ int lastplayedposition)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->SetRecordingLastPlayedPosition(recording, lastplayedposition);
+ }
+
+ inline static PVR_ERROR ADDON_GetRecordingLastPlayedPosition(const AddonInstance_PVR* instance,
+ const PVR_RECORDING* recording,
+ int* position)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetRecordingLastPlayedPosition(recording, *position);
+ }
+
+ inline static PVR_ERROR ADDON_GetRecordingEdl(const AddonInstance_PVR* instance,
+ const PVR_RECORDING* recording,
+ PVR_EDL_ENTRY* edl,
+ int* size)
+ {
+ std::vector<PVREDLEntry> edlList;
+ PVR_ERROR error = static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetRecordingEdl(recording, edlList);
+ if (static_cast<int>(edlList.size()) > *size)
+ {
+ kodi::Log(
+ ADDON_LOG_WARNING,
+ "CInstancePVRClient::%s: Truncating %d EDL entries from client to permitted size %d",
+ __func__, static_cast<int>(edlList.size()), *size);
+ edlList.resize(*size);
+ }
+ *size = 0;
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ for (const auto& edlEntry : edlList)
+ {
+ edl[*size] = *edlEntry;
+ ++*size;
+ }
+ }
+ return error;
+ }
+
+ inline static PVR_ERROR ADDON_GetRecordingSize(const AddonInstance_PVR* instance,
+ const PVR_RECORDING* recording,
+ int64_t* size)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetRecordingSize(recording, *size);
+ }
+
+ inline static PVR_ERROR ADDON_GetRecordingStreamProperties(const AddonInstance_PVR* instance,
+ const PVR_RECORDING* recording,
+ PVR_NAMED_VALUE* properties,
+ unsigned int* propertiesCount)
+ {
+ *propertiesCount = 0;
+ std::vector<PVRStreamProperty> propertiesList;
+ PVR_ERROR error = static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetRecordingStreamProperties(recording, propertiesList);
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ for (const auto& property : propertiesList)
+ {
+ strncpy(properties[*propertiesCount].strName, property.GetCStructure()->strName,
+ sizeof(properties[*propertiesCount].strName) - 1);
+ strncpy(properties[*propertiesCount].strValue, property.GetCStructure()->strValue,
+ sizeof(properties[*propertiesCount].strValue) - 1);
+ ++*propertiesCount;
+ if (*propertiesCount > STREAM_MAX_PROPERTY_COUNT)
+ break;
+ }
+ }
+ return error;
+ }
+
+ inline static PVR_ERROR ADDON_CallRecordingMenuHook(const AddonInstance_PVR* instance,
+ const PVR_MENUHOOK* menuhook,
+ const PVR_RECORDING* recording)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->CallRecordingMenuHook(menuhook, recording);
+ }
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+
+ inline static PVR_ERROR ADDON_GetTimerTypes(const AddonInstance_PVR* instance,
+ PVR_TIMER_TYPE* types,
+ int* typesCount)
+ {
+ *typesCount = 0;
+ std::vector<PVRTimerType> timerTypes;
+ PVR_ERROR error = static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetTimerTypes(timerTypes);
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ for (const auto& timerType : timerTypes)
+ {
+ types[*typesCount] = *timerType;
+ ++*typesCount;
+ if (*typesCount >= PVR_ADDON_TIMERTYPE_ARRAY_SIZE)
+ break;
+ }
+ }
+ return error;
+ }
+
+ inline static PVR_ERROR ADDON_GetTimersAmount(const AddonInstance_PVR* instance, int* amount)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetTimersAmount(*amount);
+ }
+
+ inline static PVR_ERROR ADDON_GetTimers(const AddonInstance_PVR* instance, PVR_HANDLE handle)
+ {
+ PVRTimersResultSet result(instance, handle);
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->GetTimers(result);
+ }
+
+ inline static PVR_ERROR ADDON_AddTimer(const AddonInstance_PVR* instance, const PVR_TIMER* timer)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->AddTimer(timer);
+ }
+
+ inline static PVR_ERROR ADDON_DeleteTimer(const AddonInstance_PVR* instance,
+ const PVR_TIMER* timer,
+ bool forceDelete)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->DeleteTimer(timer, forceDelete);
+ }
+
+ inline static PVR_ERROR ADDON_UpdateTimer(const AddonInstance_PVR* instance,
+ const PVR_TIMER* timer)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->UpdateTimer(timer);
+ }
+
+ inline static PVR_ERROR ADDON_CallTimerMenuHook(const AddonInstance_PVR* instance,
+ const PVR_MENUHOOK* menuhook,
+ const PVR_TIMER* timer)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->CallTimerMenuHook(menuhook, timer);
+ }
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+
+ inline static PVR_ERROR ADDON_OnSystemSleep(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->OnSystemSleep();
+ }
+
+ inline static PVR_ERROR ADDON_OnSystemWake(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->OnSystemWake();
+ }
+
+ inline static PVR_ERROR ADDON_OnPowerSavingActivated(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->OnPowerSavingActivated();
+ }
+
+ inline static PVR_ERROR ADDON_OnPowerSavingDeactivated(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->OnPowerSavingDeactivated();
+ }
+
+ // obsolete parts below
+ ///@{
+
+ inline static bool ADDON_OpenLiveStream(const AddonInstance_PVR* instance,
+ const PVR_CHANNEL* channel)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->OpenLiveStream(channel);
+ }
+
+ inline static void ADDON_CloseLiveStream(const AddonInstance_PVR* instance)
+ {
+ static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->CloseLiveStream();
+ }
+
+ inline static int ADDON_ReadLiveStream(const AddonInstance_PVR* instance,
+ unsigned char* buffer,
+ unsigned int size)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->ReadLiveStream(buffer, size);
+ }
+
+ inline static int64_t ADDON_SeekLiveStream(const AddonInstance_PVR* instance,
+ int64_t position,
+ int whence)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->SeekLiveStream(position, whence);
+ }
+
+ inline static int64_t ADDON_LengthLiveStream(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->LengthLiveStream();
+ }
+
+ inline static PVR_ERROR ADDON_GetStreamProperties(const AddonInstance_PVR* instance,
+ PVR_STREAM_PROPERTIES* properties)
+ {
+ properties->iStreamCount = 0;
+ std::vector<PVRStreamProperties> cppProperties;
+ PVR_ERROR err = static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetStreamProperties(cppProperties);
+ if (err == PVR_ERROR_NO_ERROR)
+ {
+ for (unsigned int i = 0; i < cppProperties.size(); ++i)
+ {
+ memcpy(&properties->stream[i],
+ static_cast<PVR_STREAM_PROPERTIES::PVR_STREAM*>(cppProperties[i]),
+ sizeof(PVR_STREAM_PROPERTIES::PVR_STREAM));
+ ++properties->iStreamCount;
+
+ if (properties->iStreamCount >= PVR_STREAM_MAX_STREAMS)
+ {
+ kodi::Log(
+ ADDON_LOG_ERROR,
+ "CInstancePVRClient::%s: Addon given with '%li' more allowed streams where '%i'",
+ __func__, cppProperties.size(), PVR_STREAM_MAX_STREAMS);
+ break;
+ }
+ }
+ }
+
+ return err;
+ }
+
+ inline static PVR_ERROR ADDON_GetStreamReadChunkSize(const AddonInstance_PVR* instance,
+ int* chunksize)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetStreamReadChunkSize(*chunksize);
+ }
+
+ inline static bool ADDON_IsRealTimeStream(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->IsRealTimeStream();
+ }
+
+ inline static bool ADDON_OpenRecordedStream(const AddonInstance_PVR* instance,
+ const PVR_RECORDING* recording)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->OpenRecordedStream(recording);
+ }
+
+ inline static void ADDON_CloseRecordedStream(const AddonInstance_PVR* instance)
+ {
+ static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->CloseRecordedStream();
+ }
+
+ inline static int ADDON_ReadRecordedStream(const AddonInstance_PVR* instance,
+ unsigned char* buffer,
+ unsigned int size)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->ReadRecordedStream(buffer, size);
+ }
+
+ inline static int64_t ADDON_SeekRecordedStream(const AddonInstance_PVR* instance,
+ int64_t position,
+ int whence)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->SeekRecordedStream(position, whence);
+ }
+
+ inline static int64_t ADDON_LengthRecordedStream(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->LengthRecordedStream();
+ }
+
+ inline static void ADDON_DemuxReset(const AddonInstance_PVR* instance)
+ {
+ static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->DemuxReset();
+ }
+
+ inline static void ADDON_DemuxAbort(const AddonInstance_PVR* instance)
+ {
+ static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->DemuxAbort();
+ }
+
+ inline static void ADDON_DemuxFlush(const AddonInstance_PVR* instance)
+ {
+ static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->DemuxFlush();
+ }
+
+ inline static DEMUX_PACKET* ADDON_DemuxRead(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->DemuxRead();
+ }
+
+ inline static bool ADDON_CanPauseStream(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->CanPauseStream();
+ }
+
+ inline static bool ADDON_CanSeekStream(const AddonInstance_PVR* instance)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->CanSeekStream();
+ }
+
+ inline static void ADDON_PauseStream(const AddonInstance_PVR* instance, bool bPaused)
+ {
+ static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->PauseStream(bPaused);
+ }
+
+ inline static bool ADDON_SeekTime(const AddonInstance_PVR* instance,
+ double time,
+ bool backwards,
+ double* startpts)
+ {
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->SeekTime(time, backwards, *startpts);
+ }
+
+ inline static void ADDON_SetSpeed(const AddonInstance_PVR* instance, int speed)
+ {
+ static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->SetSpeed(speed);
+ }
+
+ inline static void ADDON_FillBuffer(const AddonInstance_PVR* instance, bool mode)
+ {
+ static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)->FillBuffer(mode);
+ }
+
+ inline static PVR_ERROR ADDON_GetStreamTimes(const AddonInstance_PVR* instance,
+ PVR_STREAM_TIMES* times)
+ {
+ PVRStreamTimes cppTimes(times);
+ return static_cast<CInstancePVRClient*>(instance->toAddon->addonInstance)
+ ->GetStreamTimes(cppTimes);
+ }
+ ///@}
+
+ AddonInstance_PVR* m_instanceData = nullptr;
+};
+//}}}
+//______________________________________________________________________________
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Peripheral.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Peripheral.h
new file mode 100644
index 0000000..ea07498
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Peripheral.h
@@ -0,0 +1,889 @@
+/*
+ * Copyright (C) 2014-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 "../AddonBase.h"
+#include "peripheral/PeripheralUtils.h"
+
+#ifdef __cplusplus
+namespace kodi
+{
+namespace addon
+{
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_peripheral_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_addon_peripheral
+/// @brief %Peripheral add-on general variables
+///
+/// Used to exchange the available options between Kodi and addon.
+///
+///
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_peripheral_Defs_General 1. General
+/// @ingroup cpp_kodi_addon_peripheral_Defs
+/// @brief **%Peripheral add-on general variables**\n
+/// Used to exchange the available options between Kodi and addon.
+///
+/// This group also includes @ref cpp_kodi_addon_peripheral_Defs_PeripheralCapabilities
+/// with which Kodi an @ref kodi::addon::CInstancePeripheral::GetCapabilities()
+/// queries the supported **modules** of the addon.
+///
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_peripheral_Defs_Peripheral 2. Peripheral
+/// @ingroup cpp_kodi_addon_peripheral_Defs
+/// @brief **%Peripheral add-on operation variables**\n
+/// Used to exchange the available options between Kodi and addon.
+///
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_peripheral_Defs_Event 3. Event
+/// @ingroup cpp_kodi_addon_peripheral_Defs
+/// @brief **%Event add-on operation variables**\n
+/// Used to exchange the available options between Kodi and addon.
+///
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick 4. Joystick
+/// @ingroup cpp_kodi_addon_peripheral_Defs
+/// @brief **%Joystick add-on operation variables**\n
+/// Used to exchange the available options between Kodi and addon.
+///
+
+//==============================================================================
+/// @addtogroup cpp_kodi_addon_peripheral
+/// @brief \cpp_class{ kodi::addon::CInstancePeripheral }
+/// **%Peripheral add-on instance**
+///
+/// The peripheral add-ons provides access to many joystick and gamepad
+/// interfaces across various platforms. An input addon is used to map the
+/// buttons/axis on your physical input device, to the buttons/axis of your
+/// virtual system. This is necessary because different retro systems usually
+/// have different button layouts. A controller configuration utility is also
+/// in the works.
+///
+/// ----------------------------------------------------------------------------
+///
+/// Here is an example of what the <b>`addon.xml.in`</b> would look like for an
+/// peripheral addon:
+///
+/// ~~~~~~~~~~~~~{.xml}
+/// <?xml version="1.0" encoding="UTF-8"?>
+/// <addon
+/// id="peripheral.myspecialnamefor"
+/// version="1.0.0"
+/// name="My special peripheral addon"
+/// provider-name="Your Name">
+/// <requires>@ADDON_DEPENDS@</requires>
+/// <extension
+/// point="kodi.peripheral"
+/// provides_joysticks="true"
+/// provides_buttonmaps="true"
+/// library_@PLATFORM@="@LIBRARY_FILENAME@"/>
+/// <extension point="xbmc.addon.metadata">
+/// <summary lang="en_GB">My peripheral addon</summary>
+/// <description lang="en_GB">My peripheral addon description</description>
+/// <platform>@PLATFORM@</platform>
+/// </extension>
+/// </addon>
+/// ~~~~~~~~~~~~~
+///
+/// Description to peripheral related addon.xml values:
+/// | Name | Description
+/// |:------------------------------|----------------------------------------
+/// | <b>`provides_joysticks`</b> | Set to "true" if addon provides joystick support.
+/// | <b>`provides_buttonmaps`</b> | Set to "true" if button map is used and supported by addon.
+/// | <b>`point`</b> | Addon type specification<br>At all addon types and for this kind always <b>"kodi.peripheral"</b>.
+/// | <b>`library_@PLATFORM@`</b> | Sets the used library name, which is automatically set by cmake at addon build.
+///
+/// @remark For more detailed description of the <b>`addon.xml`</b>, see also https://kodi.wiki/view/Addon.xml.
+///
+///
+/// --------------------------------------------------------------------------
+///
+/// **Here is an example of how addon can be used as a single:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/Peripheral.h>
+///
+/// class CMyPeripheralAddon : public kodi::addon::CAddonBase,
+/// public kodi::addon::CInstancePeripheral
+/// {
+/// public:
+/// CMyPeripheralAddon();
+///
+/// void GetCapabilities(kodi::addon::PeripheralCapabilities& capabilities) override;
+/// ...
+/// };
+///
+/// CMyPeripheralAddon::CMyPeripheralAddon()
+/// {
+/// ...
+/// }
+///
+/// void CMyPeripheralAddon::GetCapabilities(kodi::addon::PeripheralCapabilities& capabilities)
+/// {
+/// capabilities.SetProvidesJoysticks(true);
+/// capabilities.SetProvidesButtonmaps(true);
+/// ...
+/// }
+///
+/// ADDONCREATOR(CMyPeripheralAddon)
+/// ~~~~~~~~~~~~~
+///
+/// @note It is imperative to use the necessary functions of this class in the
+/// addon.
+///
+/// --------------------------------------------------------------------------
+///
+///
+/// **Here is another example where the peripheral is used together with
+/// other instance types:**
+///
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/Peripheral.h>
+///
+/// class CMyPeripheralAddon : public kodi::addon::CInstancePeripheral
+/// {
+/// public:
+/// CMyPeripheralAddon(const kodi::addon::IInstanceInfo& instance);
+///
+/// void GetCapabilities(kodi::addon::PeripheralCapabilities& capabilities) override;
+/// ...
+/// };
+///
+/// CMyPeripheralAddon::CMyPeripheralAddon(const kodi::addon::IInstanceInfo& instance)
+/// : CInstancePeripheral(instance)
+/// {
+/// ...
+/// }
+///
+/// void CMyPeripheralAddon::GetCapabilities(kodi::addon::PeripheralCapabilities& capabilities)
+/// {
+/// capabilities.SetProvidesJoysticks(true);
+/// capabilities.SetProvidesButtonmaps(true);
+/// ...
+/// }
+///
+/// //----------------------------------------------------------------------
+///
+/// class CMyAddon : public kodi::addon::CAddonBase
+/// {
+/// public:
+/// CMyAddon() = default;
+/// ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl) override;
+/// };
+///
+/// // If you use only one instance in your add-on, can be instanceType and
+/// // instanceID ignored
+/// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl)
+/// {
+/// if (instance.IsType(ADDON_INSTANCE_PERIPHERAL))
+/// {
+/// kodi::Log(ADDON_LOG_INFO, "Creating my peripheral addon");
+/// addonInstance = new CMyPeripheralAddon(instance);
+/// return ADDON_STATUS_OK;
+/// }
+/// else if (...)
+/// {
+/// ...
+/// }
+/// return ADDON_STATUS_UNKNOWN;
+/// }
+///
+/// ADDONCREATOR(CMyAddon)
+/// ~~~~~~~~~~~~~
+///
+/// The destruction of the example class `CMyPeripheralAddon` is called from
+/// Kodi's header. Manually deleting the add-on instance is not required.
+///
+class ATTR_DLL_LOCAL CInstancePeripheral : public IAddonInstance
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_peripheral
+ /// @brief Peripheral class constructor.
+ ///
+ /// Used by an add-on that only supports peripheral.
+ ///
+ CInstancePeripheral()
+ : IAddonInstance(IInstanceInfo(CPrivateBase::m_interface->firstKodiInstance))
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstancePeripheral: Creation of more as one in single "
+ "instance way is not allowed!");
+
+ SetAddonStruct(CPrivateBase::m_interface->firstKodiInstance);
+ CPrivateBase::m_interface->globalSingleInstance = this;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_peripheral
+ /// @brief Peripheral addon class constructor used to support multiple
+ /// instance types.
+ ///
+ /// @param[in] instance The instance value given to
+ /// <b>`kodi::addon::CAddonBase::CreateInstance(...)`</b>.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ //////*Here's example about the use of this:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// class CMyPeripheralAddon : public kodi::addon::CInstancePeripheral
+ /// {
+ /// public:
+ /// CMyPeripheralAddon(const kodi::addon::IInstanceInfo& instance)
+ /// : kodi::addon::CInstancePeripheral(instance)
+ /// {
+ /// ...
+ /// }
+ ///
+ /// ...
+ /// };
+ ///
+ /// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+ /// KODI_ADDON_INSTANCE_HDL& hdl)
+ /// {
+ /// kodi::Log(ADDON_LOG_INFO, "Creating my peripheral");
+ /// hdl = new CMyPeripheralAddon(instance,);
+ /// return ADDON_STATUS_OK;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ explicit CInstancePeripheral(const IInstanceInfo& instance) : IAddonInstance(instance)
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstancePeripheral: Creation of multiple together with "
+ "single instance way is not allowed!");
+
+ SetAddonStruct(instance);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_peripheral
+ /// @brief Destructor.
+ ///
+ ~CInstancePeripheral() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_peripheralOp 1. Peripheral operations
+ /// @ingroup cpp_kodi_addon_peripheral
+ /// @brief %Peripheral operations to handle control about.
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **%Peripheral parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_peripheral_peripheralOp_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_peripheral_peripheralOp_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Get the list of features that this add-on provides.
+ ///
+ /// Called by the frontend to query the add-on's capabilities and supported
+ /// peripherals. All capabilities that the add-on supports should be set to true.
+ ///
+ /// @param[out] capabilities The add-on's capabilities
+ ///
+ /// @remarks Valid implementation required.
+ ///
+ ///
+ /// ----------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_peripheral_Defs_PeripheralCapabilities_Help
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// void CMyPeripheralAddon::GetCapabilities(kodi::addon::PeripheralCapabilities& capabilities)
+ /// {
+ /// capabilities.SetProvidesJoysticks(true);
+ /// capabilities.SetProvidesButtonmaps(true);
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual void GetCapabilities(kodi::addon::PeripheralCapabilities& capabilities) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Perform a scan for joysticks
+ ///
+ /// The frontend calls this when a hardware change is detected. If an add-on
+ /// detects a hardware change, it can trigger this function using the
+ /// @ref TriggerScan() callback.
+ ///
+ /// @param[in] scan_results Assigned to allocated memory
+ /// @return @ref PERIPHERAL_NO_ERROR if successful
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_peripheral_Defs_Peripheral_Peripheral_Help
+ ///
+ virtual PERIPHERAL_ERROR PerformDeviceScan(
+ std::vector<std::shared_ptr<kodi::addon::Peripheral>>& scan_results)
+ {
+ return PERIPHERAL_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get all events that have occurred since the last call to
+ /// @ref GetEvents().
+ ///
+ /// @param[out] events List of available events within addon
+ /// @return @ref PERIPHERAL_NO_ERROR if successful
+ ///
+ /// ----------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_peripheral_Defs_Peripheral_PeripheralEvent_Help
+ ///
+ virtual PERIPHERAL_ERROR GetEvents(std::vector<kodi::addon::PeripheralEvent>& events)
+ {
+ return PERIPHERAL_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Send an input event to the peripheral.
+ ///
+ /// @param[in] event The input event
+ /// @return true if the event was handled, false otherwise
+ ///
+ virtual bool SendEvent(const kodi::addon::PeripheralEvent& event) { return false; }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_joystickOp 2. Joystick operations
+ /// @ingroup cpp_kodi_addon_peripheral
+ /// @brief %Joystick operations to handle control about.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **%Joystick parts in interface:**\n
+ /// Copy this to your project and extend with your parts or leave functions
+ /// complete away where not used or supported.
+ ///
+ /// @copydetails cpp_kodi_addon_peripheral_joystickOp_header_addon_auto_check
+ /// @copydetails cpp_kodi_addon_peripheral_joystickOp_source_addon_auto_check
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Get extended info about an attached joystick.
+ ///
+ /// @param[in] index The joystick's driver index
+ /// @param[out] info The container for the allocated joystick info
+ /// @return @ref PERIPHERAL_NO_ERROR if successful
+ ///
+ ///
+ /// ----------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_peripheral_Defs_Joystick_Joystick_Help
+ ///
+ virtual PERIPHERAL_ERROR GetJoystickInfo(unsigned int index, kodi::addon::Joystick& info)
+ {
+ return PERIPHERAL_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the features that allow translating the joystick into the
+ /// controller profile.
+ ///
+ /// @param[in] joystick The device's joystick properties; unknown values may
+ /// be left at their default
+ /// @param[in] controller_id The controller profile being requested, e.g.
+ /// `game.controller.default`
+ /// @param[out] features The array of allocated features
+ /// @return @ref PERIPHERAL_NO_ERROR if successful
+ ///
+ virtual PERIPHERAL_ERROR GetFeatures(const kodi::addon::Joystick& joystick,
+ const std::string& controller_id,
+ std::vector<kodi::addon::JoystickFeature>& features)
+ {
+ return PERIPHERAL_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Add or update joystick features.
+ ///
+ /// @param[in] joystick The device's joystick properties; unknown values may be
+ /// left at their default
+ /// @param[in] controller_id The game controller profile being updated
+ /// @param[in] features The array of features
+ /// @return @ref PERIPHERAL_NO_ERROR if successful
+ ///
+ virtual PERIPHERAL_ERROR MapFeatures(const kodi::addon::Joystick& joystick,
+ const std::string& controller_id,
+ const std::vector<kodi::addon::JoystickFeature>& features)
+ {
+ return PERIPHERAL_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get the driver primitives that should be ignored while mapping the
+ /// device.
+ ///
+ /// @param[in] joystick The device's joystick properties; unknown values may
+ /// be left at their default
+ /// @param[out] primitives The array of allocated driver primitives to be
+ /// ignored
+ /// @return @ref PERIPHERAL_NO_ERROR if successful
+ ///
+ virtual PERIPHERAL_ERROR GetIgnoredPrimitives(
+ const kodi::addon::Joystick& joystick, std::vector<kodi::addon::DriverPrimitive>& primitives)
+ {
+ return PERIPHERAL_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Set the list of driver primitives that are ignored for the device.
+ ///
+ /// @param[in] joystick The device's joystick properties; unknown values may be left at their default
+ /// @param[in] primitives The array of driver primitives to ignore
+ /// @return @ref PERIPHERAL_NO_ERROR if successful
+ ///
+ virtual PERIPHERAL_ERROR SetIgnoredPrimitives(
+ const kodi::addon::Joystick& joystick,
+ const std::vector<kodi::addon::DriverPrimitive>& primitives)
+ {
+ return PERIPHERAL_ERROR_NOT_IMPLEMENTED;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Save the button map for the given joystick.
+ ///
+ /// @param[in] joystick The device's joystick properties
+ ///
+ virtual void SaveButtonMap(const kodi::addon::Joystick& joystick) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Revert the button map to the last time it was loaded or committed to disk
+ /// @param[in] joystick The device's joystick properties
+ ///
+ virtual void RevertButtonMap(const kodi::addon::Joystick& joystick) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Reset the button map for the given joystick and controller profile ID
+ /// @param[in] joystick The device's joystick properties
+ /// @param[in] controller_id The game controller profile being reset
+ ///
+ virtual void ResetButtonMap(const kodi::addon::Joystick& joystick,
+ const std::string& controller_id)
+ {
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Powers off the given joystick if supported
+ /// @param[in] index The joystick's driver index
+ ///
+ virtual void PowerOffJoystick(unsigned int index) {}
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_callbacks 3. Callback functions
+ /// @ingroup cpp_kodi_addon_peripheral
+ /// @brief Callback to Kodi functions.
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Used to get the full path where the add-on is installed.
+ ///
+ /// @return The add-on installation path
+ ///
+ const std::string AddonPath() const { return m_instanceData->props->addon_path; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Used to get the full path to the add-on's user profile.
+ ///
+ /// @note The trailing folder (consisting of the add-on's ID) is not created
+ /// by default. If it is needed, you must call kodi::vfs::CreateDirectory()
+ /// to create the folder.
+ ///
+ /// @return Path to the user profile
+ ///
+ const std::string UserPath() const { return m_instanceData->props->user_path; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Trigger a scan for peripherals
+ ///
+ /// The add-on calls this if a change in hardware is detected.
+ ///
+ void TriggerScan(void)
+ {
+ return m_instanceData->toKodi->trigger_scan(m_instanceData->toKodi->kodiInstance);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Notify the frontend that button maps have changed.
+ ///
+ /// @param[in] deviceName [optional] The name of the device to refresh, or
+ /// empty/null for all devices
+ /// @param[in] controllerId [optional] The controller ID to refresh, or
+ /// empty/null for all controllers
+ ///
+ void RefreshButtonMaps(const std::string& deviceName = "", const std::string& controllerId = "")
+ {
+ return m_instanceData->toKodi->refresh_button_maps(m_instanceData->toKodi->kodiInstance,
+ deviceName.c_str(), controllerId.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Return the number of features belonging to the specified
+ /// controller.
+ ///
+ /// @param[in] controllerId The controller ID to enumerate
+ /// @param[in] type [optional] Type to filter by, or @ref JOYSTICK_FEATURE_TYPE_UNKNOWN
+ /// for all features
+ /// @return The number of features matching the request parameters
+ ///
+ unsigned int FeatureCount(const std::string& controllerId,
+ JOYSTICK_FEATURE_TYPE type = JOYSTICK_FEATURE_TYPE_UNKNOWN)
+ {
+ return m_instanceData->toKodi->feature_count(m_instanceData->toKodi->kodiInstance,
+ controllerId.c_str(), type);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Return the type of the feature.
+ ///
+ /// @param[in] controllerId The controller ID to check
+ /// @param[in] featureName The feature to check
+ /// @return The type of the specified feature, or @ref JOYSTICK_FEATURE_TYPE_UNKNOWN
+ /// if unknown
+ ///
+ JOYSTICK_FEATURE_TYPE FeatureType(const std::string& controllerId, const std::string& featureName)
+ {
+ return m_instanceData->toKodi->feature_type(m_instanceData->toKodi->kodiInstance,
+ controllerId.c_str(), featureName.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+private:
+ void SetAddonStruct(KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ instance->hdl = this;
+
+ instance->peripheral->toAddon->get_capabilities = ADDON_GetCapabilities;
+ instance->peripheral->toAddon->perform_device_scan = ADDON_PerformDeviceScan;
+ instance->peripheral->toAddon->free_scan_results = ADDON_FreeScanResults;
+ instance->peripheral->toAddon->get_events = ADDON_GetEvents;
+ instance->peripheral->toAddon->free_events = ADDON_FreeEvents;
+ instance->peripheral->toAddon->send_event = ADDON_SendEvent;
+
+ instance->peripheral->toAddon->get_joystick_info = ADDON_GetJoystickInfo;
+ instance->peripheral->toAddon->free_joystick_info = ADDON_FreeJoystickInfo;
+ instance->peripheral->toAddon->get_features = ADDON_GetFeatures;
+ instance->peripheral->toAddon->free_features = ADDON_FreeFeatures;
+ instance->peripheral->toAddon->map_features = ADDON_MapFeatures;
+ instance->peripheral->toAddon->get_ignored_primitives = ADDON_GetIgnoredPrimitives;
+ instance->peripheral->toAddon->free_primitives = ADDON_FreePrimitives;
+ instance->peripheral->toAddon->set_ignored_primitives = ADDON_SetIgnoredPrimitives;
+ instance->peripheral->toAddon->save_button_map = ADDON_SaveButtonMap;
+ instance->peripheral->toAddon->revert_button_map = ADDON_RevertButtonMap;
+ instance->peripheral->toAddon->reset_button_map = ADDON_ResetButtonMap;
+ instance->peripheral->toAddon->power_off_joystick = ADDON_PowerOffJoystick;
+
+ m_instanceData = instance->peripheral;
+ m_instanceData->toAddon->addonInstance = this;
+ }
+
+ inline static void ADDON_GetCapabilities(const AddonInstance_Peripheral* addonInstance,
+ PERIPHERAL_CAPABILITIES* capabilities)
+ {
+ if (!addonInstance || !capabilities)
+ return;
+
+ kodi::addon::PeripheralCapabilities peripheralCapabilities(capabilities);
+ static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->GetCapabilities(peripheralCapabilities);
+ }
+
+ inline static PERIPHERAL_ERROR ADDON_PerformDeviceScan(
+ const AddonInstance_Peripheral* addonInstance,
+ unsigned int* peripheral_count,
+ PERIPHERAL_INFO** scan_results)
+ {
+ if (!addonInstance || !peripheral_count || !scan_results)
+ return PERIPHERAL_ERROR_INVALID_PARAMETERS;
+
+ std::vector<std::shared_ptr<kodi::addon::Peripheral>> peripherals;
+ PERIPHERAL_ERROR err = static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->PerformDeviceScan(peripherals);
+ if (err == PERIPHERAL_NO_ERROR)
+ {
+ *peripheral_count = static_cast<unsigned int>(peripherals.size());
+ kodi::addon::Peripherals::ToStructs(peripherals, scan_results);
+ }
+
+ return err;
+ }
+
+ inline static void ADDON_FreeScanResults(const AddonInstance_Peripheral* addonInstance,
+ unsigned int peripheral_count,
+ PERIPHERAL_INFO* scan_results)
+ {
+ if (!addonInstance)
+ return;
+
+ kodi::addon::Peripherals::FreeStructs(peripheral_count, scan_results);
+ }
+
+ inline static PERIPHERAL_ERROR ADDON_GetEvents(const AddonInstance_Peripheral* addonInstance,
+ unsigned int* event_count,
+ PERIPHERAL_EVENT** events)
+ {
+ if (!addonInstance || !event_count || !events)
+ return PERIPHERAL_ERROR_INVALID_PARAMETERS;
+
+ std::vector<kodi::addon::PeripheralEvent> peripheralEvents;
+ PERIPHERAL_ERROR err = static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->GetEvents(peripheralEvents);
+ if (err == PERIPHERAL_NO_ERROR)
+ {
+ *event_count = static_cast<unsigned int>(peripheralEvents.size());
+ kodi::addon::PeripheralEvents::ToStructs(peripheralEvents, events);
+ }
+
+ return err;
+ }
+
+ inline static void ADDON_FreeEvents(const AddonInstance_Peripheral* addonInstance,
+ unsigned int event_count,
+ PERIPHERAL_EVENT* events)
+ {
+ if (!addonInstance)
+ return;
+
+ kodi::addon::PeripheralEvents::FreeStructs(event_count, events);
+ }
+
+ inline static bool ADDON_SendEvent(const AddonInstance_Peripheral* addonInstance,
+ const PERIPHERAL_EVENT* event)
+ {
+ if (!addonInstance || !event)
+ return false;
+ return static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->SendEvent(kodi::addon::PeripheralEvent(*event));
+ }
+
+
+ inline static PERIPHERAL_ERROR ADDON_GetJoystickInfo(
+ const AddonInstance_Peripheral* addonInstance, unsigned int index, JOYSTICK_INFO* info)
+ {
+ if (!addonInstance || !info)
+ return PERIPHERAL_ERROR_INVALID_PARAMETERS;
+
+ kodi::addon::Joystick addonInfo;
+ PERIPHERAL_ERROR err = static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->GetJoystickInfo(index, addonInfo);
+ if (err == PERIPHERAL_NO_ERROR)
+ {
+ addonInfo.ToStruct(*info);
+ }
+
+ return err;
+ }
+
+ inline static void ADDON_FreeJoystickInfo(const AddonInstance_Peripheral* addonInstance,
+ JOYSTICK_INFO* info)
+ {
+ if (!addonInstance)
+ return;
+
+ kodi::addon::Joystick::FreeStruct(*info);
+ }
+
+ inline static PERIPHERAL_ERROR ADDON_GetFeatures(const AddonInstance_Peripheral* addonInstance,
+ const JOYSTICK_INFO* joystick,
+ const char* controller_id,
+ unsigned int* feature_count,
+ JOYSTICK_FEATURE** features)
+ {
+ if (!addonInstance || !joystick || !controller_id || !feature_count || !features)
+ return PERIPHERAL_ERROR_INVALID_PARAMETERS;
+
+ kodi::addon::Joystick addonJoystick(*joystick);
+ std::vector<kodi::addon::JoystickFeature> featuresVector;
+
+ PERIPHERAL_ERROR err = static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->GetFeatures(addonJoystick, controller_id, featuresVector);
+ if (err == PERIPHERAL_NO_ERROR)
+ {
+ *feature_count = static_cast<unsigned int>(featuresVector.size());
+ kodi::addon::JoystickFeatures::ToStructs(featuresVector, features);
+ }
+
+ return err;
+ }
+
+ inline static void ADDON_FreeFeatures(const AddonInstance_Peripheral* addonInstance,
+ unsigned int feature_count,
+ JOYSTICK_FEATURE* features)
+ {
+ if (!addonInstance)
+ return;
+
+ kodi::addon::JoystickFeatures::FreeStructs(feature_count, features);
+ }
+
+ inline static PERIPHERAL_ERROR ADDON_MapFeatures(const AddonInstance_Peripheral* addonInstance,
+ const JOYSTICK_INFO* joystick,
+ const char* controller_id,
+ unsigned int feature_count,
+ const JOYSTICK_FEATURE* features)
+ {
+ if (!addonInstance || !joystick || !controller_id || (feature_count > 0 && !features))
+ return PERIPHERAL_ERROR_INVALID_PARAMETERS;
+
+ kodi::addon::Joystick addonJoystick(*joystick);
+ std::vector<kodi::addon::JoystickFeature> primitiveVector;
+
+ for (unsigned int i = 0; i < feature_count; i++)
+ primitiveVector.emplace_back(*(features + i));
+
+ return static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->MapFeatures(addonJoystick, controller_id, primitiveVector);
+ }
+
+ inline static PERIPHERAL_ERROR ADDON_GetIgnoredPrimitives(
+ const AddonInstance_Peripheral* addonInstance,
+ const JOYSTICK_INFO* joystick,
+ unsigned int* primitive_count,
+ JOYSTICK_DRIVER_PRIMITIVE** primitives)
+ {
+ if (!addonInstance || !joystick || !primitive_count || !primitives)
+ return PERIPHERAL_ERROR_INVALID_PARAMETERS;
+
+ kodi::addon::Joystick addonJoystick(*joystick);
+ std::vector<kodi::addon::DriverPrimitive> primitiveVector;
+
+ PERIPHERAL_ERROR err = static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->GetIgnoredPrimitives(addonJoystick, primitiveVector);
+ if (err == PERIPHERAL_NO_ERROR)
+ {
+ *primitive_count = static_cast<unsigned int>(primitiveVector.size());
+ kodi::addon::DriverPrimitives::ToStructs(primitiveVector, primitives);
+ }
+
+ return err;
+ }
+
+ inline static void ADDON_FreePrimitives(const AddonInstance_Peripheral* addonInstance,
+ unsigned int primitive_count,
+ JOYSTICK_DRIVER_PRIMITIVE* primitives)
+ {
+ if (!addonInstance)
+ return;
+
+ kodi::addon::DriverPrimitives::FreeStructs(primitive_count, primitives);
+ }
+
+ inline static PERIPHERAL_ERROR ADDON_SetIgnoredPrimitives(
+ const AddonInstance_Peripheral* addonInstance,
+ const JOYSTICK_INFO* joystick,
+ unsigned int primitive_count,
+ const JOYSTICK_DRIVER_PRIMITIVE* primitives)
+ {
+ if (!addonInstance || !joystick || (primitive_count > 0 && !primitives))
+ return PERIPHERAL_ERROR_INVALID_PARAMETERS;
+
+ kodi::addon::Joystick addonJoystick(*joystick);
+ std::vector<kodi::addon::DriverPrimitive> primitiveVector;
+
+ for (unsigned int i = 0; i < primitive_count; i++)
+ primitiveVector.emplace_back(*(primitives + i));
+
+ return static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->SetIgnoredPrimitives(addonJoystick, primitiveVector);
+ }
+
+ inline static void ADDON_SaveButtonMap(const AddonInstance_Peripheral* addonInstance,
+ const JOYSTICK_INFO* joystick)
+ {
+ if (!addonInstance || !joystick)
+ return;
+
+ kodi::addon::Joystick addonJoystick(*joystick);
+ static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->SaveButtonMap(addonJoystick);
+ }
+
+ inline static void ADDON_RevertButtonMap(const AddonInstance_Peripheral* addonInstance,
+ const JOYSTICK_INFO* joystick)
+ {
+ if (!addonInstance || !joystick)
+ return;
+
+ kodi::addon::Joystick addonJoystick(*joystick);
+ static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->RevertButtonMap(addonJoystick);
+ }
+
+ inline static void ADDON_ResetButtonMap(const AddonInstance_Peripheral* addonInstance,
+ const JOYSTICK_INFO* joystick,
+ const char* controller_id)
+ {
+ if (!addonInstance || !joystick || !controller_id)
+ return;
+
+ kodi::addon::Joystick addonJoystick(*joystick);
+ static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->ResetButtonMap(addonJoystick, controller_id);
+ }
+
+ inline static void ADDON_PowerOffJoystick(const AddonInstance_Peripheral* addonInstance,
+ unsigned int index)
+ {
+ if (!addonInstance)
+ return;
+
+ static_cast<CInstancePeripheral*>(addonInstance->toAddon->addonInstance)
+ ->PowerOffJoystick(index);
+ }
+
+ AddonInstance_Peripheral* m_instanceData;
+};
+
+} /* namespace addon */
+} /* namespace kodi */
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Screensaver.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Screensaver.h
new file mode 100644
index 0000000..1fd36fb
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Screensaver.h
@@ -0,0 +1,424 @@
+/*
+ * 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 "../AddonBase.h"
+#include "../c-api/addon-instance/screensaver.h"
+#include "../gui/renderHelper.h"
+
+#ifdef __cplusplus
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @addtogroup cpp_kodi_addon_screensaver
+/// @brief \cpp_class{ kodi::addon::CInstanceScreensaver }
+/// **Screensaver add-on instance**
+///
+/// A screensaver is a Kodi addon that fills the screen with moving images or
+/// patterns when the computer is not in use. Initially designed to prevent
+/// phosphor burn-in on CRT and plasma computer monitors (hence the name),
+/// screensavers are now used primarily for entertainment, security or to
+/// display system status information.
+///
+/// Include the header @ref Screensaver.h "#include <kodi/addon-instance/ScreenSaver.h>"
+/// to use this class.
+///
+/// This interface allows the creating of screensavers for Kodi, based upon
+/// **DirectX** or/and **OpenGL** rendering with `C++` code.
+///
+/// The interface is small and easy usable. It has three functions:
+///
+/// * <b><c>Start()</c></b> - Called on creation
+/// * <b><c>Render()</c></b> - Called at render time
+/// * <b><c>Stop()</c></b> - Called when the screensaver has no work
+///
+/// Additionally, there are several @ref cpp_kodi_addon_screensaver_CB "other functions"
+/// available in which the child class can ask about the current hardware,
+/// including the device, display and several other parts.
+///
+/// ----------------------------------------------------------------------------
+///
+/// Here is an example of what the <b>`addon.xml.in`</b> would look like for an
+/// screensaver addon:
+///
+/// ~~~~~~~~~~~~~{.xml}
+/// <?xml version="1.0" encoding="UTF-8"?>
+/// <addon
+/// id="screensaver.myspecialnamefor"
+/// version="1.0.0"
+/// name="My special screensaver addon"
+/// provider-name="Your Name">
+/// <requires>@ADDON_DEPENDS@</requires>
+/// <extension
+/// point="xbmc.ui.screensaver"
+/// library_@PLATFORM@="@LIBRARY_FILENAME@"/>
+/// <extension point="xbmc.addon.metadata">
+/// <summary lang="en_GB">My screensaver addon</summary>
+/// <description lang="en_GB">My screensaver addon description</description>
+/// <platform>@PLATFORM@</platform>
+/// </extension>
+/// </addon>
+/// ~~~~~~~~~~~~~
+///
+/// Description to screensaver related addon.xml values:
+/// | Name | Description
+/// |:------------------------------|----------------------------------------
+/// | <b>`point`</b> | Addon type specification<br>At all addon types and for this kind always <b>"xbmc.ui.screensaver"</b>.
+/// | <b>`library_@PLATFORM@`</b> | Sets the used library name, which is automatically set by cmake at addon build.
+///
+/// @remark For more detailed description of the <b>`addon.xml`</b>, see also https://kodi.wiki/view/Addon.xml.
+///
+///
+/// --------------------------------------------------------------------------
+///
+/// **Here is an example of the minimum required code to start a screensaver:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/Screensaver.h>
+///
+/// class CMyScreenSaver : public kodi::addon::CAddonBase,
+/// public kodi::addon::CInstanceScreensaver
+/// {
+/// public:
+/// CMyScreenSaver();
+///
+/// bool Start() override;
+/// void Render() override;
+/// };
+///
+/// CMyScreenSaver::CMyScreenSaver()
+/// {
+/// ...
+/// }
+///
+/// bool CMyScreenSaver::Start()
+/// {
+/// ...
+/// return true;
+/// }
+///
+/// void CMyScreenSaver::Render()
+/// {
+/// ...
+/// }
+///
+/// ADDONCREATOR(CMyScreenSaver)
+/// ~~~~~~~~~~~~~
+///
+///
+/// --------------------------------------------------------------------------
+///
+///
+/// **Here is another example where the screensaver is used together with
+/// other instance types:**
+///
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/Screensaver.h>
+///
+/// class CMyScreenSaver : public kodi::addon::CInstanceScreensaver
+/// {
+/// public:
+/// CMyScreenSaver(const kodi::addon::IInstanceInfo& instance);
+///
+/// bool Start() override;
+/// void Render() override;
+/// };
+///
+/// CMyScreenSaver::CMyScreenSaver(const kodi::addon::IInstanceInfo& instance)
+/// : CInstanceScreensaver(instance)
+/// {
+/// ...
+/// }
+///
+/// bool CMyScreenSaver::Start()
+/// {
+/// ...
+/// return true;
+/// }
+///
+/// void CMyScreenSaver::Render()
+/// {
+/// ...
+/// }
+///
+///
+/// //----------------------------------------------------------------------
+///
+/// class CMyAddon : public kodi::addon::CAddonBase
+/// {
+/// public:
+/// CMyAddon() = default;
+/// ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl) override;
+/// };
+///
+/// // If you use only one instance in your add-on, can be instanceType and
+/// // instanceID ignored
+/// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl)
+/// {
+/// if (instance.IsType(ADDON_INSTANCE_SCREENSAVER))
+/// {
+/// kodi::Log(ADDON_LOG_INFO, "Creating my Screensaver");
+/// hdl = new CMyScreenSaver(instance, version);
+/// return ADDON_STATUS_OK;
+/// }
+/// else if (...)
+/// {
+/// ...
+/// }
+/// return ADDON_STATUS_UNKNOWN;
+/// }
+///
+/// ADDONCREATOR(CMyAddon)
+/// ~~~~~~~~~~~~~
+///
+/// The destruction of the example class `CMyScreenSaver` is called from
+/// Kodi's header. Manually deleting the add-on instance is not required.
+///
+class ATTR_DLL_LOCAL CInstanceScreensaver : public IAddonInstance
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver
+ /// @brief Screensaver class constructor.
+ ///
+ /// Used by an add-on that only supports screensavers.
+ ///
+ CInstanceScreensaver()
+ : IAddonInstance(IInstanceInfo(CPrivateBase::m_interface->firstKodiInstance))
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstanceScreensaver: Creation of more as one in single "
+ "instance way is not allowed!");
+
+ SetAddonStruct(CPrivateBase::m_interface->firstKodiInstance);
+ CPrivateBase::m_interface->globalSingleInstance = this;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver
+ /// @brief Screensaver class constructor used to support multiple instance
+ /// types.
+ ///
+ /// @param[in] instance The instance value given to
+ /// <b>`kodi::addon::CAddonBase::CreateInstance(...)`</b>.
+ /// @param[in] kodiVersion [opt] Version used in Kodi for this instance, to
+ /// allow compatibility to older Kodi versions.
+ ///
+ /// @note Recommended to set <b>`kodiVersion`</b>.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Here's example about the use of this:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// class CMyScreenSaver : public kodi::addon::CInstanceScreensaver
+ /// {
+ /// public:
+ /// CMyScreenSaver(KODI_HANDLE instance, const std::string& kodiVersion)
+ /// : kodi::addon::CInstanceScreensaver(instance, kodiVersion)
+ /// {
+ /// ...
+ /// }
+ ///
+ /// ...
+ /// };
+ ///
+ /// ADDON_STATUS CMyAddon::CreateInstance(int instanceType,
+ /// const std::string& instanceID,
+ /// KODI_HANDLE instance,
+ /// const std::string& version,
+ /// KODI_HANDLE& addonInstance)
+ /// {
+ /// kodi::Log(ADDON_LOG_INFO, "Creating my screensaver");
+ /// addonInstance = new CMyScreenSaver(instance, version);
+ /// return ADDON_STATUS_OK;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ explicit CInstanceScreensaver(const IInstanceInfo& instance) : IAddonInstance(instance)
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstanceScreensaver: Creation of multiple together "
+ "with single instance way is not allowed!");
+
+ SetAddonStruct(instance);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver
+ /// @brief Destructor.
+ ///
+ ~CInstanceScreensaver() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver
+ /// @brief Used to notify the screensaver that it has been started.
+ ///
+ /// @return true if the screensaver was started successfully, false otherwise
+ ///
+ virtual bool Start() { return true; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver
+ /// @brief Used to inform the screensaver that the rendering control was
+ /// stopped.
+ ///
+ virtual void Stop() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver
+ /// @brief Used to indicate when the add-on should render
+ ///
+ virtual void Render() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_screensaver_CB Information functions
+ /// @ingroup cpp_kodi_addon_screensaver
+ /// @brief **To get info about the device, display and several other parts**
+ ///
+ ///@{
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver_CB
+ /// @brief Device that represents the display adapter.
+ ///
+ /// @return A pointer to the device
+ ///
+ /// @note This is only available on **DirectX**, It us unused (`nullptr`) on
+ /// **OpenGL**
+ ///
+ /// This value can also be becomed by @ref kodi::gui::GetHWContext() and is
+ /// recommended to use.
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <d3d11_1.h>
+ /// ..
+ /// // Note: Device() there is used inside addon child class about
+ /// // kodi::addon::CInstanceVisualization
+ /// ID3D11DeviceContext1* context = static_cast<ID3D11DeviceContext1*>(kodi::addon::CInstanceVisualization::Device());
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ inline kodi::HardwareContext Device() { return m_props.device; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver_CB
+ /// @brief Returns the X position of the rendering window.
+ ///
+ /// @return The X position, in pixels
+ ///
+ inline int X() { return m_props.x; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver_CB
+ /// @brief Returns the Y position of the rendering window.
+ ///
+ /// @return The Y position, in pixels
+ ///
+ inline int Y() { return m_props.y; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver_CB
+ /// @brief Returns the width of the rendering window.
+ ///
+ /// @return The width, in pixels
+ ///
+ inline int Width() { return m_props.width; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver_CB
+ /// @brief Returns the height of the rendering window.
+ ///
+ /// @return The height, in pixels
+ ///
+ inline int Height() { return m_props.height; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_screensaver_CB
+ /// @brief Pixel aspect ratio (often abbreviated PAR) is a ratio that
+ /// describes how the width of a pixel compares to the height of that pixel.
+ ///
+ /// @return The pixel aspect ratio used by the display
+ ///
+ inline float PixelRatio() { return m_props.pixelRatio; }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+private:
+ void SetAddonStruct(KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ instance->hdl = this;
+ instance->screensaver->toAddon->start = ADDON_start;
+ instance->screensaver->toAddon->stop = ADDON_stop;
+ instance->screensaver->toAddon->render = ADDON_render;
+
+ instance->screensaver->toKodi->get_properties(instance->info->kodi, &m_props);
+ }
+
+ inline static bool ADDON_start(const KODI_ADDON_SCREENSAVER_HDL hdl)
+ {
+ CInstanceScreensaver* thisClass = static_cast<CInstanceScreensaver*>(hdl);
+ thisClass->m_renderHelper = kodi::gui::GetRenderHelper();
+ return thisClass->Start();
+ }
+
+ inline static void ADDON_stop(const KODI_ADDON_SCREENSAVER_HDL hdl)
+ {
+ CInstanceScreensaver* thisClass = static_cast<CInstanceScreensaver*>(hdl);
+ thisClass->Stop();
+ thisClass->m_renderHelper = nullptr;
+ }
+
+ inline static void ADDON_render(const KODI_ADDON_SCREENSAVER_HDL hdl)
+ {
+ CInstanceScreensaver* thisClass = static_cast<CInstanceScreensaver*>(hdl);
+
+ if (!thisClass->m_renderHelper)
+ return;
+ thisClass->m_renderHelper->Begin();
+ thisClass->Render();
+ thisClass->m_renderHelper->End();
+ }
+
+ /*
+ * Background render helper holds here and in addon base.
+ * In addon base also to have for the others, and stored here for the worst
+ * case where this class is independent from base and base becomes closed
+ * before.
+ *
+ * This is on Kodi with GL unused and the calls to there are empty (no work)
+ * On Kodi with Direct X where angle is present becomes this used.
+ */
+ std::shared_ptr<kodi::gui::IRenderHelper> m_renderHelper;
+
+ KODI_ADDON_SCREENSAVER_PROPS m_props = {};
+};
+
+} /* namespace addon */
+} /* namespace kodi */
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/VFS.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/VFS.h
new file mode 100644
index 0000000..6affffe
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/VFS.h
@@ -0,0 +1,1211 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "../AddonBase.h"
+#include "../Filesystem.h"
+#include "../c-api/addon-instance/vfs.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+class CInstanceVFS;
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon_vfs_Defs
+/// @brief **VFS add-on file handle**\n
+/// This used to handle opened files of addon with related memory pointer about
+/// class or structure and to have on further file control functions available.
+///
+/// See @ref cpp_kodi_addon_vfs_filecontrol "file editing functions" for used
+/// places.
+///
+///@{
+using VFSFileHandle = VFS_FILE_HANDLE;
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_vfs_Defs_VFSUrl class VFSUrl
+/// @ingroup cpp_kodi_addon_vfs_Defs
+/// @brief **VFS add-on URL data**\n
+/// This class is used to inform the addon of the desired wanted connection.
+///
+/// Used on mostly all addon functions to identify related target.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_vfs_Defs_VFSUrl_Help
+///
+///@{
+class ATTR_DLL_LOCAL VFSUrl : public CStructHdl<VFSUrl, VFSURL>
+{
+ /*! \cond PRIVATE */
+ friend class CInstanceVFS;
+ /*! \endcond */
+
+public:
+ /// @defgroup cpp_kodi_addon_vfs_Defs_VFSUrl_Help Value Help
+ /// @ingroup cpp_kodi_addon_vfs_Defs_VFSUrl
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_vfs_Defs_VFSUrl :</b>
+ /// | Name | Type | Get call
+ /// |------|------|----------
+ /// | **URL** | `std::string` | @ref VFSUrl::GetURL "GetURL"
+ /// | **Domain name** | `std::string` | @ref VFSUrl::GetDomain "GetDomain"
+ /// | **Hostname** | `std::string` | @ref VFSUrl::GetHostname "GetHostname"
+ /// | **Filename** | `std::string` | @ref VFSUrl::GetFilename "GetFilename"
+ /// | **Network port** | `unsigned int` | @ref VFSUrl::GetPort "GetPort"
+ /// | **Special options** | `std::string` | @ref VFSUrl::GetOptions "GetOptions"
+ /// | **Username** | `std::string` | @ref VFSUrl::GetUsername "GetUsername"
+ /// | **Password** | `std::string` | @ref VFSUrl::GetPassword "GetPassword"
+ /// | **Get URL with user and password hidden** | `std::string` | @ref VFSUrl::GetRedacted "GetRedacted"
+ /// | **Sharename** | `std::string` | @ref VFSUrl::GetSharename "GetSharename"
+ /// | **Network protocol** | `std::string` | @ref VFSUrl::GetProtocol "GetProtocol"
+ ///
+
+ /// @addtogroup cpp_kodi_addon_vfs_Defs_VFSUrl
+ ///@{
+
+ /// @brief Desired URL of the file system to be edited
+ ///
+ /// This includes all available parts of the access and is structured as
+ /// follows:
+ /// -
+ /// <b>`<PROTOCOL>`://`<USERNAME>`:`<PASSWORD>``@``<HOSTNAME>`:`<PORT>`/`<FILENAME>`?`<OPTIONS>`</b>
+ std::string GetURL() const { return m_cStructure->url; }
+
+ /// @brief The associated domain name, which is optional and not available
+ /// in all cases.
+ std::string GetDomain() const { return m_cStructure->domain; }
+
+ /// @brief This includes the network address (e.g. `192.168.0.123`) or if
+ /// the addon refers to file packages the path to it
+ /// (e.g. `/home/by_me/MyPacket.rar`).
+ std::string GetHostname() const { return m_cStructure->hostname; }
+
+ /// @brief With this variable the desired path to a folder or file within
+ /// the hostname is given (e.g. `storage/videos/00001.ts`).
+ std::string GetFilename() const { return m_cStructure->filename; }
+
+ /// @brief [Networking port](https://en.wikipedia.org/wiki/Port_(computer_networking))
+ /// to use for protocol.
+ unsigned int GetPort() const { return m_cStructure->port; }
+
+ /// @brief Special options on opened URL, this can e.g. on RAR packages
+ /// <b>`?flags=8&nextvalue=123`</b> to inform about to not cache a read.
+ ///
+ /// Available options from Kodi:
+ /// | Value: | Description:
+ /// |-----------|-------------------
+ /// | flags=8 | Used on RAR packages so that no data is cached from the requested source.
+ /// | cache=no | Used on ZIP packages so that no data from the requested source is stored in the cache. However, this is currently not available from addons!
+ ///
+ /// In addition, other addons can use the URLs given by them to give options
+ /// that fit the respective VFS addon and allow special operations.
+ ///
+ /// @note This procedure is not yet standardized and is currently not
+ /// exactly available which are handed over.
+ std::string GetOptions() const { return m_cStructure->options; }
+
+ /// @brief Desired username.
+ std::string GetUsername() const { return m_cStructure->username; }
+
+ /// @brief Desired password.
+ std::string GetPassword() const { return m_cStructure->password; }
+
+ /// @brief The complete URL is passed on here, but the user name and
+ /// password are not shown and only appear to there as `USERNAME:PASSWORD`.
+ ///
+ /// As example <b>`sftp://USERNAME:PASSWORD@192.168.178.123/storage/videos/00001.ts`</b>.
+ std::string GetRedacted() const { return m_cStructure->redacted; }
+
+ /// @brief The name which is taken as the basis by source and would be first
+ /// in folder view.
+ ///
+ /// As example on <b>`sftp://dudu:isprivate@192.168.178.123/storage/videos/00001.ts`</b>
+ /// becomes then <b>`storage`</b> used here.
+ std::string GetSharename() const { return m_cStructure->sharename; }
+
+ /// @brief Protocol name used on this stream, e.g. <b>`sftp`</b>.
+ std::string GetProtocol() const { return m_cStructure->protocol; }
+
+ ///@}
+
+private:
+ VFSUrl() = delete;
+ VFSUrl(const VFSUrl& channel) = delete;
+ VFSUrl(const VFSURL* channel) : CStructHdl(channel) {}
+ VFSUrl(VFSURL* channel) : CStructHdl(channel) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_vfs_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_addon_vfs
+/// @brief **VFS add-on general variables**
+///
+/// Used to exchange the available options between Kodi and addon.
+///
+///
+
+//==============================================================================
+///
+/// @addtogroup cpp_kodi_addon_vfs
+/// @brief \cpp_class{ kodi::addon::CInstanceVFS }
+/// **Virtual Filesystem (VFS) add-on instance**
+///
+/// This instance type is used to allow Kodi various additional file system
+/// types. Be it a special file system, a compressed package or a system
+/// available over the network, everything is possible with it.
+///
+/// This usage can be requested under various conditions, for example explicitly
+/// by another addon, by a Mimetype protocol defined in <b>`addon.xml`</b> or supported
+/// file extensions.
+///
+/// Include the header @ref VFS.h "#include <kodi/addon-instance/VFS.h>"
+/// to use this class.
+///
+/// ----------------------------------------------------------------------------
+///
+/// Here is an example of what the <b>`addon.xml.in`</b> would look like for an VFS addon:
+///
+/// ~~~~~~~~~~~~~{.xml}
+/// <?xml version="1.0" encoding="UTF-8"?>
+/// <addon
+/// id="vfs.myspecialnamefor"
+/// version="1.0.0"
+/// name="My VFS addon"
+/// provider-name="Your Name">
+/// <requires>@ADDON_DEPENDS@</requires>
+/// <extension
+/// point="kodi.vfs"
+/// protocols="myprot"
+/// extensions=".abc|.def"
+/// files="true"
+/// filedirectories="true"
+/// directories="true"
+/// encodedhostname="true"
+/// supportDialog="true"
+/// supportPath="true"
+/// supportUsername="true"
+/// supportPassword="true"
+/// supportPort="true"
+/// supportBrowsing="true"
+/// supportWrite="true"
+/// defaultPort="1234"
+/// label="30000"
+/// zeroconf="your_special_zeroconf_allowed_identifier"
+/// library_@PLATFORM@="@LIBRARY_FILENAME@"/>
+/// <extension point="xbmc.addon.metadata">
+/// <summary lang="en_GB">My VFS addon summary</summary>
+/// <description lang="en_GB">My VFS description</description>
+/// <platform>@PLATFORM@</platform>
+/// </extension>
+/// </addon>
+/// ~~~~~~~~~~~~~
+///
+/// @note Regarding boolean values with "false", these can also be omitted,
+/// since this would be the default.
+///
+///
+/// ### Standard values that can be declared for processing in `addon.xml`.
+///
+/// These values are used by Kodi to identify associated streams and file
+/// extensions and then to select the associated addon.
+///
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`point`</b>,
+/// \anchor cpp_kodi_addon_vfs_point
+/// string,
+/// The identification of the addon instance to VFS is mandatory <b>`kodi.vfs`</b>.
+/// In addition\, the instance declared in the first <b>`<extension ... />`</b> is also the main type of addon.
+/// }
+/// \table_row3{ <b>`defaultPort`</b>,
+/// \anchor cpp_kodi_addon_vfs_defaultPort
+/// integer,
+/// Default [networking port](https://en.wikipedia.org/wiki/Port_(computer_networking))
+/// to use for protocol.
+/// }
+/// \table_row3{ <b>`directories`</b>,
+/// \anchor cpp_kodi_addon_vfs_directories
+/// boolean,
+/// VFS entry can list directories.
+/// }
+/// \table_row3{ <b>`extensions`</b>,
+/// \anchor cpp_kodi_addon_vfs_extensions
+/// string,
+/// Extensions for VFS entry.\n
+/// It is possible to declare several using <b>`|`</b>\, e.g. <b>`.abc|.def|.ghi`</b>.
+/// }
+/// \table_row3{ <b>`encodedhostname`</b>,
+/// \anchor cpp_kodi_addon_vfs_encodedhostname
+/// boolean,
+/// URL protocol from add-ons use encoded hostnames.
+/// }
+/// \table_row3{ <b>`filedirectories`</b>,
+/// \anchor cpp_kodi_addon_vfs_filedirectories
+/// boolean,
+/// VFS entry contains file directories.
+/// }
+/// \table_row3{ <b>`files`</b>,
+/// \anchor cpp_kodi_addon_vfs_directories
+/// boolean,
+/// Set to declare that VFS provides files.
+/// }
+/// \table_row3{ <b>`protocols`</b>,
+/// \anchor cpp_kodi_addon_vfs_protocols
+/// boolean,
+/// Protocols for VFS entry.\n
+/// It is possible to declare several using <b>`|`</b>\, e.g. <b>`myprot1|myprot2`</b>.\n
+/// @note This field also used to show on GUI\, see <b>`supportBrowsing`</b> below about <b>*2:</b>.
+/// When used there\, however\, only a **single** protocol is possible!
+/// }
+/// \table_row3{ <b>`supportWrite`</b>,
+/// \anchor cpp_kodi_addon_vfs_supportWrite
+/// boolean,
+/// Protocol supports write operations.
+/// }
+/// \table_row3{ <b>`zeroconf`</b>,
+/// \anchor cpp_kodi_addon_vfs_zeroconf
+/// string,
+/// [Zero conf](https://en.wikipedia.org/wiki/Zero-configuration_networking) announce string for VFS protocol.
+/// }
+/// \table_row3{ <b>`library_@PLATFORM@`</b>,
+/// \anchor cpp_kodi_addon_vfs_library
+/// string,
+/// The runtime library used for the addon. This is usually declared by `cmake` and correctly displayed in the translated <b>`addon.xml`</b>.
+/// }
+/// \table_end
+///
+///
+/// ### User selectable parts of the addon.
+///
+/// The following table describes the values that can be defined by <b>`addon.xml`</b>
+/// and which part they relate to for user input.
+///
+/// \table_start
+/// \table_h3{ Labels, Type, Description }
+/// \table_row3{ <b>`supportBrowsing`</b>,
+/// \anchor cpp_kodi_addon_vfs_protocol_supportBrowsing
+/// boolean,
+/// Protocol supports server browsing. Used to open related sources by users in the window.\n\n
+/// | Associated places in Kodi: |
+/// | :---- |
+/// | \image html cpp_kodi_addon_vfs_protocol_1.png |
+/// <br>
+/// <b>*1:</b> The entry in the menu represented by this option corresponds to the text given with <b>`label`</b>.
+/// When the button is pressed\, @ref CInstanceVFS::GetDirectory is called on the add-on to get its content.\n
+/// <b>*2:</b> Protocol name of the stream defined with <b>`protocols`</b> in xml.\n
+/// @remark See also <b>`supportDialog`</b> about <b>*3:</b>.
+/// }
+/// \table_row3{ <b>`supportDialog`</b>,
+/// \anchor cpp_kodi_addon_vfs_protocol_supportDialog
+/// boolean,
+/// To point out that Kodi assigns a dialog to this VFS in order to compare it with other values e.g. query supportPassword in it.\n
+/// This will be available when adding sources in Kodi under <b>"Add network location..."</b>.\n\n
+/// | Associated places in Kodi: |
+/// | :---- |
+/// | \image html cpp_kodi_addon_vfs_protocol_2.png |
+/// <br>
+/// <b>*1:</b> Field for selecting the VFS handler\, the addon will be available if <b>`supportDialog`</b> is set to <b>`true`</b>.\n
+/// <b>*2:</b> To set the associated server address. **Note:** *This field is always activated and cannot be changed by the addon.*\n
+/// <b>*3:</b> If <b>`supportBrowsing`</b> is set to <b>`true`</b>\, the button for opening a file selection dialog is given here too\, as in the file window.\n
+/// <b>*4:</b> This field is available if <b>`supportPath`</b> is set to <b>`true`</b>.\n
+/// <b>*5:</b> To edit the connection port. This field is available if <b>`supportPort`</b> is set to <b>`true`</b>.\n
+/// <b>*6:</b> This sets the required username and is available when <b>`supportUsername`</b> is set to <b>`true`</b>.\n
+/// <b>*7:</b> This sets the required password and is available when <b>`supportPassword`</b> is set to <b>`true`</b>.
+/// }
+/// \table_row3{ <b>`supportPath`</b>,
+/// \anchor cpp_kodi_addon_vfs_protocol_supportPath
+/// boolean,
+/// Protocol has path in addition to server name (see <b>`supportDialog`</b> about <b>*4:</b>).
+/// }
+/// \table_row3{ <b>`supportPort`</b>,
+/// \anchor cpp_kodi_addon_vfs_protocol_supportPort
+/// boolean,
+/// Protocol supports port customization (see <b>`supportDialog`</b> about <b>*5:</b>).
+/// }
+/// \table_row3{ <b>`supportUsername`</b>,
+/// \anchor cpp_kodi_addon_vfs_protocol_supportUsername
+/// boolean,
+/// Protocol uses logins (see <b>`supportDialog`</b> about <b>*6:</b>).
+/// }
+/// \table_row3{ <b>`supportPassword`</b>,
+/// \anchor cpp_kodi_addon_vfs_protocol_supportPassword
+/// boolean,
+/// Protocol supports passwords (see <b>`supportDialog`</b> about <b>*7:</b>).
+/// }
+/// \table_row3{ <b>`protocols`</b>,
+/// \anchor cpp_kodi_addon_vfs_protocol_protocols
+/// string,
+/// Protocols for VFS entry.
+/// @note This field is not editable and only used on GUI to show his name\, see <b>`supportBrowsing`</b> about <b>*2:</b>
+/// }
+/// \table_row3{ <b>`label`</b>,
+/// \anchor cpp_kodi_addon_vfs_protocol_label
+/// integer,
+/// The text identification number used in Kodi for display in the menu at <b>`supportDialog`</b>
+/// as a selection option and at <b>`supportBrowsing`</b> (see his image reference <b>*1</b>) as a menu entry.\n
+/// This can be a text identifier in Kodi or from addon.\n
+/// @remark For addon within <b>30000</b>-<b>30999</b> or <b>32000</b>-<b>32999</b>.
+/// }
+/// \table_end
+///
+/// @remark For more detailed description of the <b>`addon.xml`</b>, see also https://kodi.wiki/view/Addon.xml.
+///
+///
+/// --------------------------------------------------------------------------
+///
+///
+/// **Example:**
+///
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/VFS.h>
+///
+/// class CMyVFS : public kodi::addon::CInstanceVFS
+/// {
+/// public:
+/// CMyVFS(const kodi::addon::IInstanceInfo& instance);
+///
+/// // Add all your required functions, the most CInstanceVFS functions of
+/// // must be included to have addon working correctly.
+/// ...
+/// };
+///
+/// CMyVFS::CMyVFS(const kodi::addon::IInstanceInfo& instance)
+/// : kodi::addon::CInstanceVFS(instance)
+/// {
+/// ...
+/// }
+///
+/// ...
+///
+/// //----------------------------------------------------------------------
+///
+/// class CMyAddon : public kodi::addon::CAddonBase
+/// {
+/// public:
+/// CMyAddon() = default;
+/// ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl) override;
+/// };
+///
+/// // If you use only one instance in your add-on, can be instanceType and
+/// // instanceID ignored
+/// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl)
+/// {
+/// if (instance.IsType(ADDON_INSTANCE_VFS))
+/// {
+/// kodi::Log(ADDON_LOG_INFO, "Creating my VFS instance");
+/// hdl = new CMyVFS(instance);
+/// return ADDON_STATUS_OK;
+/// }
+/// else if (...)
+/// {
+/// ...
+/// }
+/// return ADDON_STATUS_UNKNOWN;
+/// }
+///
+/// ADDONCREATOR(CMyAddon)
+/// ~~~~~~~~~~~~~
+///
+/// The destruction of the example class `CMyVFS` is called from
+/// Kodi's header. Manually deleting the add-on instance is not required.
+///
+//------------------------------------------------------------------------------
+class ATTR_DLL_LOCAL CInstanceVFS : public IAddonInstance
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs
+ /// @brief VFS class constructor used to support multiple instance
+ /// types
+ ///
+ /// @param[in] instance The instance value given to <b>`kodi::addon::CAddonBase::CreateInstance(...)`</b>.
+ ///
+ /// @note Instance path as a single is not supported by this type. It must
+ /// ensure that it can be called up several times.
+ ///
+ /// @warning Only use `instance` from the @ref CAddonBase::CreateInstance call.
+ ///
+ explicit CInstanceVFS(const IInstanceInfo& instance) : IAddonInstance(instance)
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstanceVFS: Creation of multiple together with single "
+ "instance way is not allowed!");
+
+ SetAddonStruct(instance);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs
+ /// @brief Destructor
+ ///
+ ~CInstanceVFS() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_vfs_general 1. General access functions
+ /// @ingroup cpp_kodi_addon_vfs
+ /// @brief **General access functions**
+ ///
+ /// This functions which are intended for getting folders, editing storage
+ /// locations and file system queries.
+ ///
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_vfs_filecontrol 2. File editing functions
+ /// @ingroup cpp_kodi_addon_vfs
+ /// @brief **File editing functions.**
+ ///
+ /// This value represents the addon-side handlers and to be able to identify
+ /// his own parts in the event of further access.
+ ///
+
+ //@{
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Open a file for input
+ ///
+ /// @param[in] url The URL of the file
+ /// @return Context for the opened file
+ ///
+ ///
+ /// ----------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_vfs_Defs_VFSUrl_Help
+ ///
+ virtual kodi::addon::VFSFileHandle Open(const kodi::addon::VFSUrl& url) { return nullptr; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Open a file for output
+ ///
+ /// @param[in] url The URL of the file
+ /// @param[in] overWrite Whether or not to overwrite an existing file
+ /// @return Context for the opened file
+ ///
+ virtual kodi::addon::VFSFileHandle OpenForWrite(const kodi::addon::VFSUrl& url, bool overWrite)
+ {
+ return nullptr;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Close a file
+ ///
+ /// @param[in] context The context of the file
+ /// @return True on success, false on failure
+ ///
+ virtual bool Close(kodi::addon::VFSFileHandle context) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Read from a file
+ ///
+ /// @param[in] context The context of the file
+ /// @param[out] buffer The buffer to read data into
+ /// @param[in] uiBufSize Number of bytes to read
+ /// @return Number of bytes read
+ ///
+ virtual ssize_t Read(kodi::addon::VFSFileHandle context, uint8_t* buffer, size_t uiBufSize)
+ {
+ return -1;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Write to a file
+ ///
+ /// @param[in] context The context of the file
+ /// @param[in] buffer The buffer to read data from
+ /// @param[in] uiBufSize Number of bytes to write
+ /// @return Number of bytes written
+ ///
+ virtual ssize_t Write(kodi::addon::VFSFileHandle context, const uint8_t* buffer, size_t uiBufSize)
+ {
+ return -1;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Seek in a file
+ ///
+ /// @param[in] context The context of the file
+ /// @param[in] position The position to seek to
+ /// @param[in] whence Position in file 'position' is relative to (SEEK_CUR, SEEK_SET, SEEK_END):
+ /// | Value | int | Description |
+ /// |:--------:|:---:|:----------------------------------------------------|
+ /// | SEEK_SET | 0 | position is relative to the beginning of the file. This is probably what you had in mind anyway, and is the most commonly used value for whence.
+ /// | SEEK_CUR | 1 | position is relative to the current file pointer position. So, in effect, you can say, "Move to my current position plus 30 bytes," or, "move to my current position minus 20 bytes."
+ /// | SEEK_END | 2 | position is relative to the end of the file. Just like SEEK_SET except from the other end of the file. Be sure to use negative values for offset if you want to back up from the end of the file, instead of going past the end into oblivion.
+ /// @return Offset in file after seek
+ ///
+ virtual int64_t Seek(kodi::addon::VFSFileHandle context, int64_t position, int whence)
+ {
+ return -1;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Truncate a file
+ ///
+ /// @param[in] context The context of the file
+ /// @param[in] size The size to truncate the file to
+ /// @return 0 on success, -1 on error
+ ///
+ virtual int Truncate(kodi::addon::VFSFileHandle context, int64_t size) { return -1; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Get total size of a file
+ ///
+ /// @param[in] context The context of the file
+ /// @return Total file size
+ ///
+ virtual int64_t GetLength(kodi::addon::VFSFileHandle context) { return 0; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Get current position in a file
+ ///
+ /// @param[in] context The context of the file
+ /// @return Current position
+ ///
+ virtual int64_t GetPosition(kodi::addon::VFSFileHandle context) { return 0; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Get chunk size of a file
+ ///
+ /// @param[in] context The context of the file
+ /// @return Chunk size
+ ///
+ virtual int GetChunkSize(kodi::addon::VFSFileHandle context) { return 1; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief To check seek possible on current stream by file.
+ ///
+ /// @return true if seek possible, false if not
+ ///
+ virtual bool IoControlGetSeekPossible(kodi::addon::VFSFileHandle context) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief To check a running stream on file for state of his cache.
+ ///
+ /// @param[in] status Information about current cache status
+ /// @return true if successfully done, false otherwise
+ ///
+ ///
+ /// @copydetails cpp_kodi_vfs_Defs_CacheStatus_Help
+ ///
+ virtual bool IoControlGetCacheStatus(kodi::addon::VFSFileHandle context,
+ kodi::vfs::CacheStatus& status)
+ {
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Unsigned int with speed limit for caching in bytes per second.
+ ///
+ /// @param[in] rate Cache rate size to use
+ /// @return true if successfully done, false otherwise
+ ///
+ virtual bool IoControlSetCacheRate(kodi::addon::VFSFileHandle context, uint32_t rate)
+ {
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_filecontrol
+ /// @brief Enable/disable retry within the protocol handler (if supported).
+ ///
+ /// @param[in] retry To set the retry, true for use, false for not
+ /// @return true if successfully done, false otherwise
+ ///
+ virtual bool IoControlSetRetry(kodi::addon::VFSFileHandle context, bool retry) { return false; }
+ //----------------------------------------------------------------------------
+ //@}
+
+ //@{
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief Stat a file
+ ///
+ /// @param[in] url The URL of the file
+ /// @param[in] buffer The buffer to store results in
+ /// @return -1 on error, 0 otherwise
+ ///
+ ///
+ /// ----------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_vfs_Defs_VFSUrl_Help
+ ///
+ virtual int Stat(const kodi::addon::VFSUrl& url, kodi::vfs::FileStatus& buffer) { return 0; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief Check for file existence
+ ///
+ /// @param[in] url The URL of the file
+ /// @return True if file exists, false otherwise
+ ///
+ virtual bool Exists(const kodi::addon::VFSUrl& url) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief Clear out any idle connections
+ ///
+ virtual void ClearOutIdle() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief Disconnect all connections
+ ///
+ virtual void DisconnectAll() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief Delete a file
+ ///
+ /// @param[in] url The URL of the file
+ /// @return True if deletion was successful, false otherwise
+ ///
+ virtual bool Delete(const kodi::addon::VFSUrl& url) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief Rename a file
+ ///
+ /// @param[in] url The URL of the source file
+ /// @param[in] url2 The URL of the destination file
+ /// @return True if deletion was successful, false otherwise
+ ///
+ virtual bool Rename(const kodi::addon::VFSUrl& url, const kodi::addon::VFSUrl& url2)
+ {
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief Check for directory existence
+ ///
+ /// @param[in] url The URL of the file
+ /// @return True if directory exists, false otherwise
+ ///
+ virtual bool DirectoryExists(const kodi::addon::VFSUrl& url) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief Remove a directory
+ ///
+ /// @param[in] url The URL of the directory
+ /// @return True if removal was successful, false otherwise
+ ///
+ virtual bool RemoveDirectory(const kodi::addon::VFSUrl& url) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief Create a directory
+ ///
+ /// @param[in] url The URL of the file
+ /// @return True if creation was successful, false otherwise
+ ///
+ virtual bool CreateDirectory(const kodi::addon::VFSUrl& url) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_vfs_general_cb_GetDirectory Callbacks GetDirectory()
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief Callback functions on GetDirectory()
+ ///
+ /// This functions becomes available during call of GetDirectory() from
+ /// Kodi.
+ ///
+ /// If GetDirectory() returns false becomes the parts from here used on
+ /// next call of the function.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ ///
+ /// #include <kodi/addon-instance/VFS.h>
+ ///
+ /// ...
+ ///
+ /// bool CMyVFS::GetDirectory(const kodi::addon::VFSUrl& url,
+ /// std::vector<kodi::vfs::CDirEntry>& items,
+ /// CVFSCallbacks callbacks)
+ /// {
+ /// std::string neededString;
+ /// callbacks.GetKeyboardInput("Test", neededString, true);
+ /// if (neededString.empty())
+ /// return false;
+ ///
+ /// // Do the work
+ /// ...
+ /// return true;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ class CVFSCallbacks
+ {
+ public:
+ /// @ingroup cpp_kodi_addon_vfs_general_cb_GetDirectory
+ /// @brief Require keyboard input
+ ///
+ /// Becomes called if GetDirectory() returns false and GetDirectory()
+ /// becomes after entry called again.
+ ///
+ /// @param[in] heading The heading of the keyboard dialog
+ /// @param[out] input The resulting string. Returns string after
+ /// second call!
+ /// @param[in] hiddenInput To show input only as "*" on dialog
+ /// @return True if input was received, false otherwise
+ ///
+ bool GetKeyboardInput(const std::string& heading, std::string& input, bool hiddenInput = false)
+ {
+ char* cInput = nullptr;
+ bool ret = m_cb->get_keyboard_input(m_cb->ctx, heading.c_str(), &cInput, hiddenInput);
+ if (cInput)
+ {
+ input = cInput;
+ ::kodi::addon::CPrivateBase::m_interface->toKodi->free_string(
+ ::kodi::addon::CPrivateBase::m_interface->toKodi->kodiBase, cInput);
+ }
+ return ret;
+ }
+
+ /// @ingroup cpp_kodi_addon_vfs_general_cb_GetDirectory
+ /// @brief Display an error dialog
+ ///
+ /// @param[in] heading The heading of the error dialog
+ /// @param[in] line1 The first line of the error dialog
+ /// @param[in] line2 [opt] The second line of the error dialog
+ /// @param[in] line3 [opt] The third line of the error dialog
+ ///
+ void SetErrorDialog(const std::string& heading,
+ const std::string& line1,
+ const std::string& line2 = "",
+ const std::string& line3 = "")
+ {
+ m_cb->set_error_dialog(m_cb->ctx, heading.c_str(), line1.c_str(), line2.c_str(),
+ line3.c_str());
+ }
+
+ /// @ingroup cpp_kodi_addon_vfs_general_cb_GetDirectory
+ /// @brief Prompt the user for authentication of a URL
+ ///
+ /// @param[in] url The URL
+ void RequireAuthentication(const std::string& url)
+ {
+ m_cb->require_authentication(m_cb->ctx, url.c_str());
+ }
+
+ explicit CVFSCallbacks(const VFSGetDirectoryCallbacks* cb) : m_cb(cb) {}
+
+ private:
+ const VFSGetDirectoryCallbacks* m_cb;
+ };
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief List a directory
+ ///
+ /// @param[in] url The URL of the directory
+ /// @param[out] entries The entries in the directory, see
+ /// @ref cpp_kodi_vfs_CDirEntry "kodi::vfs::CDirEntry"
+ /// about his content
+ /// @param[in] callbacks A callback structure
+ /// @return Context for the directory listing
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// ### Callbacks:
+ /// @copydetails cpp_kodi_addon_vfs_general_cb_GetDirectory
+ ///
+ /// **Available callback functions**
+ /// | Function: | Description
+ /// |--|--
+ /// | CVFSCallbacks::GetKeyboardInput | @copybrief CVFSCallbacks::GetKeyboardInput @copydetails CVFSCallbacks::GetKeyboardInput
+ /// | CVFSCallbacks::SetErrorDialog | @copybrief CVFSCallbacks::SetErrorDialog @copydetails CVFSCallbacks::SetErrorDialog
+ /// | CVFSCallbacks::RequireAuthentication | @copybrief CVFSCallbacks::RequireAuthentication @copydetails CVFSCallbacks::RequireAuthentication
+ ///
+ virtual bool GetDirectory(const kodi::addon::VFSUrl& url,
+ std::vector<kodi::vfs::CDirEntry>& entries,
+ CVFSCallbacks callbacks)
+ {
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_vfs_general
+ /// @brief Check if file should be presented as a directory (multiple streams)
+ ///
+ /// @param[in] url The URL of the file
+ /// @param[out] entries The entries in the directory, see
+ /// @ref cpp_kodi_vfs_CDirEntry "kodi::vfs::CDirEntry"
+ /// about his content
+ /// @param[out] rootPath Path to root directory if multiple entries
+ /// @return Context for the directory listing
+ ///
+ virtual bool ContainsFiles(const kodi::addon::VFSUrl& url,
+ std::vector<kodi::vfs::CDirEntry>& entries,
+ std::string& rootPath)
+ {
+ return false;
+ }
+ //----------------------------------------------------------------------------
+ //@}
+
+private:
+ void SetAddonStruct(KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ m_instanceData = instance;
+ m_instanceData->hdl = this;
+ m_instanceData->vfs->toAddon->addonInstance = this;
+ m_instanceData->vfs->toAddon->open = ADDON_Open;
+ m_instanceData->vfs->toAddon->open_for_write = ADDON_OpenForWrite;
+ m_instanceData->vfs->toAddon->read = ADDON_Read;
+ m_instanceData->vfs->toAddon->write = ADDON_Write;
+ m_instanceData->vfs->toAddon->seek = ADDON_Seek;
+ m_instanceData->vfs->toAddon->truncate = ADDON_Truncate;
+ m_instanceData->vfs->toAddon->get_length = ADDON_GetLength;
+ m_instanceData->vfs->toAddon->get_position = ADDON_GetPosition;
+ m_instanceData->vfs->toAddon->get_chunk_size = ADDON_GetChunkSize;
+ m_instanceData->vfs->toAddon->io_control_get_seek_possible = ADDON_IoControlGetSeekPossible;
+ m_instanceData->vfs->toAddon->io_control_get_cache_status = ADDON_IoControlGetCacheStatus;
+ m_instanceData->vfs->toAddon->io_control_set_cache_rate = ADDON_IoControlSetCacheRate;
+ m_instanceData->vfs->toAddon->io_control_set_retry = ADDON_IoControlSetRetry;
+ m_instanceData->vfs->toAddon->stat = ADDON_Stat;
+ m_instanceData->vfs->toAddon->close = ADDON_Close;
+ m_instanceData->vfs->toAddon->exists = ADDON_Exists;
+ m_instanceData->vfs->toAddon->clear_out_idle = ADDON_ClearOutIdle;
+ m_instanceData->vfs->toAddon->disconnect_all = ADDON_DisconnectAll;
+ m_instanceData->vfs->toAddon->delete_it = ADDON_Delete;
+ m_instanceData->vfs->toAddon->rename = ADDON_Rename;
+ m_instanceData->vfs->toAddon->directory_exists = ADDON_DirectoryExists;
+ m_instanceData->vfs->toAddon->remove_directory = ADDON_RemoveDirectory;
+ m_instanceData->vfs->toAddon->create_directory = ADDON_CreateDirectory;
+ m_instanceData->vfs->toAddon->get_directory = ADDON_GetDirectory;
+ m_instanceData->vfs->toAddon->free_directory = ADDON_FreeDirectory;
+ m_instanceData->vfs->toAddon->contains_files = ADDON_ContainsFiles;
+ }
+
+ inline static VFS_FILE_HANDLE ADDON_Open(const AddonInstance_VFSEntry* instance,
+ const VFSURL* url)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->Open(url);
+ }
+
+ inline static VFS_FILE_HANDLE ADDON_OpenForWrite(const AddonInstance_VFSEntry* instance,
+ const VFSURL* url,
+ bool overWrite)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)
+ ->OpenForWrite(url, overWrite);
+ }
+
+ inline static ssize_t ADDON_Read(const AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ uint8_t* buffer,
+ size_t uiBufSize)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)
+ ->Read(context, buffer, uiBufSize);
+ }
+
+ inline static ssize_t ADDON_Write(const AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ const uint8_t* buffer,
+ size_t uiBufSize)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)
+ ->Write(context, buffer, uiBufSize);
+ }
+
+ inline static int64_t ADDON_Seek(const AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ int64_t position,
+ int whence)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)
+ ->Seek(context, position, whence);
+ }
+
+ inline static int ADDON_Truncate(const AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ int64_t size)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->Truncate(context, size);
+ }
+
+ inline static int64_t ADDON_GetLength(const AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->GetLength(context);
+ }
+
+ inline static int64_t ADDON_GetPosition(const AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->GetPosition(context);
+ }
+
+ inline static int ADDON_GetChunkSize(const AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->GetChunkSize(context);
+ }
+
+ inline static bool ADDON_IoControlGetSeekPossible(const AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)
+ ->IoControlGetSeekPossible(context);
+ }
+
+ inline static bool ADDON_IoControlGetCacheStatus(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ VFS_CACHE_STATUS_DATA* status)
+ {
+ kodi::vfs::CacheStatus cppStatus(status);
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)
+ ->IoControlGetCacheStatus(context, cppStatus);
+ }
+
+ inline static bool ADDON_IoControlSetCacheRate(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ uint32_t rate)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)
+ ->IoControlSetCacheRate(context, rate);
+ }
+
+ inline static bool ADDON_IoControlSetRetry(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ bool retry)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)
+ ->IoControlSetRetry(context, retry);
+ }
+
+ inline static int ADDON_Stat(const AddonInstance_VFSEntry* instance,
+ const VFSURL* url,
+ struct STAT_STRUCTURE* buffer)
+ {
+ kodi::vfs::FileStatus cppBuffer(buffer);
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->Stat(url, cppBuffer);
+ }
+
+ inline static bool ADDON_Close(const AddonInstance_VFSEntry* instance, VFS_FILE_HANDLE context)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->Close(context);
+ }
+
+ inline static bool ADDON_Exists(const AddonInstance_VFSEntry* instance, const VFSURL* url)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->Exists(url);
+ }
+
+ inline static void ADDON_ClearOutIdle(const AddonInstance_VFSEntry* instance)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->ClearOutIdle();
+ }
+
+ inline static void ADDON_DisconnectAll(const AddonInstance_VFSEntry* instance)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->DisconnectAll();
+ }
+
+ inline static bool ADDON_Delete(const AddonInstance_VFSEntry* instance, const VFSURL* url)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->Delete(url);
+ }
+
+ inline static bool ADDON_Rename(const AddonInstance_VFSEntry* instance,
+ const VFSURL* url,
+ const VFSURL* url2)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->Rename(url, url2);
+ }
+
+ inline static bool ADDON_DirectoryExists(const AddonInstance_VFSEntry* instance,
+ const VFSURL* url)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->DirectoryExists(url);
+ }
+
+ inline static bool ADDON_RemoveDirectory(const AddonInstance_VFSEntry* instance,
+ const VFSURL* url)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->RemoveDirectory(url);
+ }
+
+ inline static bool ADDON_CreateDirectory(const AddonInstance_VFSEntry* instance,
+ const VFSURL* url)
+ {
+ return static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)->CreateDirectory(url);
+ }
+
+ inline static bool ADDON_GetDirectory(const AddonInstance_VFSEntry* instance,
+ const VFSURL* url,
+ VFSDirEntry** retEntries,
+ int* num_entries,
+ VFSGetDirectoryCallbacks* callbacks)
+ {
+ std::vector<kodi::vfs::CDirEntry> addonEntries;
+ bool ret = static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)
+ ->GetDirectory(url, addonEntries, CVFSCallbacks(callbacks));
+ if (ret)
+ {
+ VFSDirEntry* entries =
+ static_cast<VFSDirEntry*>(malloc(sizeof(VFSDirEntry) * addonEntries.size()));
+ for (unsigned int i = 0; i < addonEntries.size(); ++i)
+ {
+ entries[i].label = strdup(addonEntries[i].Label().c_str());
+ entries[i].title = strdup(addonEntries[i].Title().c_str());
+ entries[i].path = strdup(addonEntries[i].Path().c_str());
+ entries[i].folder = addonEntries[i].IsFolder();
+ entries[i].size = addonEntries[i].Size();
+ entries[i].date_time = addonEntries[i].DateTime();
+
+ entries[i].num_props = 0;
+ const std::map<std::string, std::string>& props = addonEntries[i].GetProperties();
+ if (!props.empty())
+ {
+ entries[i].properties =
+ static_cast<VFSProperty*>(malloc(sizeof(VFSProperty) * props.size()));
+ for (const auto& prop : props)
+ {
+ entries[i].properties[entries[i].num_props].name = strdup(prop.first.c_str());
+ entries[i].properties[entries[i].num_props].val = strdup(prop.second.c_str());
+ ++entries[i].num_props;
+ }
+ }
+ else
+ entries[i].properties = nullptr;
+ }
+ *retEntries = entries;
+ *num_entries = static_cast<int>(addonEntries.size());
+ }
+ return ret;
+ }
+
+ inline static void ADDON_FreeDirectory(const AddonInstance_VFSEntry* instance,
+ VFSDirEntry* entries,
+ int num_entries)
+ {
+ for (int i = 0; i < num_entries; ++i)
+ {
+ if (entries[i].properties)
+ {
+ for (unsigned int j = 0; j < entries[i].num_props; ++j)
+ {
+ free(entries[i].properties[j].name);
+ free(entries[i].properties[j].val);
+ }
+ free(entries[i].properties);
+ }
+ free(entries[i].label);
+ free(entries[i].title);
+ free(entries[i].path);
+ }
+ free(entries);
+ }
+
+ inline static bool ADDON_ContainsFiles(const AddonInstance_VFSEntry* instance,
+ const VFSURL* url,
+ VFSDirEntry** retEntries,
+ int* num_entries,
+ char* rootpath)
+ {
+ std::string cppRootPath;
+ std::vector<kodi::vfs::CDirEntry> addonEntries;
+ bool ret = static_cast<CInstanceVFS*>(instance->toAddon->addonInstance)
+ ->ContainsFiles(url, addonEntries, cppRootPath);
+ if (ret)
+ {
+ strncpy(rootpath, cppRootPath.c_str(), ADDON_STANDARD_STRING_LENGTH);
+
+ VFSDirEntry* entries =
+ static_cast<VFSDirEntry*>(malloc(sizeof(VFSDirEntry) * addonEntries.size()));
+ for (size_t i = 0; i < addonEntries.size(); ++i)
+ {
+ entries[i].label = strdup(addonEntries[i].Label().c_str());
+ entries[i].title = strdup(addonEntries[i].Title().c_str());
+ entries[i].path = strdup(addonEntries[i].Path().c_str());
+ entries[i].folder = addonEntries[i].IsFolder();
+ entries[i].size = addonEntries[i].Size();
+ entries[i].date_time = addonEntries[i].DateTime();
+
+ entries[i].num_props = 0;
+ const std::map<std::string, std::string>& props = addonEntries[i].GetProperties();
+ if (!props.empty())
+ {
+ entries[i].properties =
+ static_cast<VFSProperty*>(malloc(sizeof(VFSProperty) * props.size()));
+ for (const auto& prop : props)
+ {
+ entries[i].properties[entries[i].num_props].name = strdup(prop.first.c_str());
+ entries[i].properties[entries[i].num_props].val = strdup(prop.second.c_str());
+ ++entries[i].num_props;
+ }
+ }
+ else
+ entries[i].properties = nullptr;
+ }
+ *retEntries = entries;
+ *num_entries = static_cast<int>(addonEntries.size());
+ }
+ return ret;
+ }
+
+ KODI_ADDON_INSTANCE_STRUCT* m_instanceData{nullptr};
+};
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/VideoCodec.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/VideoCodec.h
new file mode 100644
index 0000000..06f0fc8
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/VideoCodec.h
@@ -0,0 +1,446 @@
+/*
+ * Copyright (C) 2017-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 "../AddonBase.h"
+#include "../c-api/addon-instance/video_codec.h"
+#include "inputstream/DemuxPacket.h"
+#include "inputstream/StreamCodec.h"
+#include "inputstream/StreamCrypto.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+class CInstanceVideoCodec;
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_videocodec_Defs_VideoCodecInitdata class VideoCodecInitdata
+/// @ingroup cpp_kodi_addon_videocodec_Defs
+/// @brief Initialization data to open a video codec stream.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_videocodec_Defs_VideoCodecInitdata_Help
+///
+///@{
+class ATTR_DLL_LOCAL VideoCodecInitdata : public CStructHdl<VideoCodecInitdata, VIDEOCODEC_INITDATA>
+{
+ /*! \cond PRIVATE */
+ friend class CInstanceVideoCodec;
+ /*! \endcond */
+
+public:
+ /// @defgroup cpp_kodi_addon_videocodec_Defs_VideoCodecInitdata_Help Value Help
+ /// @ingroup cpp_kodi_addon_videocodec_Defs_VideoCodecInitdata
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_videocodec_Defs_VideoCodecInitdata :</b>
+ /// | Name | Type | Get call
+ /// |------|------|----------
+ /// | **Codec type** | `VIDEOCODEC_TYPE` | @ref VideoCodecInitdata::GetCodecType "GetCodecType"
+ /// | **Codec profile** | `STREAMCODEC_PROFILE` | @ref VideoCodecInitdata::GetCodecProfile "GetCodecProfile"
+ /// | **Video formats** | `std::vector<VIDEOCODEC_FORMAT>` | @ref VideoCodecInitdata::GetVideoFormats "GetVideoFormats"
+ /// | **Width** | `uint32_t` | @ref VideoCodecInitdata::GetWidth "GetWidth"
+ /// | **Height** | `uint32_t` | @ref VideoCodecInitdata::GetHeight "GetHeight"
+ /// | **Extra data** | `const uint8_t*` | @ref VideoCodecInitdata::GetExtraData "GetExtraData"
+ /// | **Extra data size** | `unsigned int` | @ref VideoCodecInitdata::GetExtraDataSize "GetExtraDataSize"
+ /// | **Crypto session** | `kodi::addon::StreamCryptoSession` | @ref VideoCodecInitdata::GetCryptoSession "GetCryptoSession"
+ ///
+
+ /// @addtogroup cpp_kodi_addon_videocodec_Defs_VideoCodecInitdata
+ ///@{
+
+ /// @brief The codec type required by Kodi to process the stream.
+ ///
+ /// See @ref VIDEOCODEC_TYPE for possible values.
+ VIDEOCODEC_TYPE GetCodecType() const { return m_cStructure->codec; }
+
+ /// @brief Used profiles for non-scalable 2D video
+ STREAMCODEC_PROFILE GetCodecProfile() const { return m_cStructure->codecProfile; }
+
+ /// @brief The video stream representations requested by Kodi
+ ///
+ /// This contains a list of the required video formats. One of them has to
+ /// select the addon to return the created image.
+ ///
+ std::vector<VIDEOCODEC_FORMAT> GetVideoFormats() const
+ {
+ std::vector<VIDEOCODEC_FORMAT> formats;
+ unsigned int i = 0;
+ while (i < VIDEOCODEC_FORMAT_MAXFORMATS &&
+ m_cStructure->videoFormats[i] != VIDEOCODEC_FORMAT_UNKNOWN)
+ formats.emplace_back(m_cStructure->videoFormats[i++]);
+ if (formats.empty())
+ formats.emplace_back(VIDEOCODEC_FORMAT_UNKNOWN);
+ return formats;
+ }
+
+ /// @brief Picture width.
+ uint32_t GetWidth() const { return m_cStructure->width; }
+
+ /// @brief Picture height.
+ uint32_t GetHeight() const { return m_cStructure->height; }
+
+ /// @brief Depending on the required decoding, additional data given by the stream.
+ const uint8_t* GetExtraData() const { return m_cStructure->extraData; }
+
+ /// @brief Size of the data given with @ref extraData.
+ unsigned int GetExtraDataSize() const { return m_cStructure->extraDataSize; }
+
+ /// @brief **Data to manage stream cryptography**\n
+ /// To get class structure manages any encryption values required in order to have
+ /// them available in their stream processing.
+ ///
+ /// ----------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_inputstream_Defs_Info_StreamCryptoSession_Help
+ ///
+ kodi::addon::StreamCryptoSession GetCryptoSession() const { return &m_cStructure->cryptoSession; }
+
+ ///@}
+
+private:
+ VideoCodecInitdata() = delete;
+ VideoCodecInitdata(const VideoCodecInitdata& session) : CStructHdl(session) {}
+ VideoCodecInitdata(const VIDEOCODEC_INITDATA* session) : CStructHdl(session) {}
+ VideoCodecInitdata(VIDEOCODEC_INITDATA* session) : CStructHdl(session) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//##############################################################################
+/// @defgroup cpp_kodi_addon_videocodec_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_addon_videocodec
+/// @brief **Video codec add-on general variables**
+///
+/// Used to exchange the available options between Kodi and addon.
+///
+///
+
+//============================================================================
+///
+/// @addtogroup cpp_kodi_addon_videocodec
+/// @brief \cpp_class{ kodi::addon::CInstanceVideoCodec }
+/// **Video codec add-on instance**
+///
+/// This is an addon instance class to add an additional video decoder to Kodi
+/// using addon.
+///
+/// This means that either a new type of decoding can be introduced to an input
+/// stream add-on that requires special types of decoding.
+///
+/// When using the inputstream addon, @ref cpp_kodi_addon_inputstream_Defs_Interface_InputstreamInfo
+/// to @ref cpp_kodi_addon_inputstream_Defs_Info is used to declare that the
+/// decoder stored in the addon is used.
+///
+/// @note At the moment this can only be used together with input stream addons,
+/// independent use as a codec addon is not yet possible.
+///
+/// Include the header @ref VideoCodec.h "#include <kodi/addon-instance/VideoCodec.h>"
+/// to use this class.
+///
+/// --------------------------------------------------------------------------
+///
+/// **Example:**
+/// This as an example when used together with @ref cpp_kodi_addon_inputstream "kodi::addon::CInstanceInputStream".
+///
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/Inputstream.h>
+/// #include <kodi/addon-instance/VideoCodec.h>
+///
+/// class CMyVideoCodec : public kodi::addon::CInstanceVideoCodec
+/// {
+/// public:
+/// CMyVideoCodec(const kodi::addon::IInstanceInfo& instance,
+/// CMyInputstream* inputstream);
+/// ...
+///
+/// private:
+/// CMyInputstream* m_inputstream;
+/// };
+///
+/// CMyVideoCodec::CMyVideoCodec(const kodi::addon::IInstanceInfo& instance,
+/// CMyInputstream* inputstream)
+/// : kodi::addon::CInstanceVideoCodec(instance),
+/// m_inputstream(inputstream)
+/// {
+/// ...
+/// }
+/// ...
+///
+/// //----------------------------------------------------------------------
+///
+/// class CMyInputstream : public kodi::addon::CInstanceInputStream
+/// {
+/// public:
+/// CMyInputstream(KODI_HANDLE instance, const std::string& kodiVersion);
+///
+/// ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl) override;
+/// ...
+/// };
+///
+/// CMyInputstream::CMyInputstream(KODI_HANDLE instance, const std::string& kodiVersion)
+/// : kodi::addon::CInstanceInputStream(instance, kodiVersion)
+/// {
+/// ...
+/// }
+///
+/// ADDON_STATUS CMyInputstream::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl)
+/// {
+/// {
+/// if (instance.IsType(ADDON_INSTANCE_VIDEOCODEC))
+/// {
+/// addonInstance = new CMyVideoCodec(instance, this);
+/// return ADDON_STATUS_OK;
+/// }
+/// return ADDON_STATUS_NOT_IMPLEMENTED;
+/// }
+///
+/// ...
+///
+/// //----------------------------------------------------------------------
+///
+/// class CMyAddon : public kodi::addon::CAddonBase
+/// {
+/// public:
+/// CMyAddon() = default;
+/// ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl) override;
+/// };
+///
+/// // If you use only one instance in your add-on, can be instanceType and
+/// // instanceID ignored
+/// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl)
+/// {
+/// if (instance.IsType(ADDON_INSTANCE_INPUTSTREAM))
+/// {
+/// kodi::Log(ADDON_LOG_NOTICE, "Creating my Inputstream");
+/// hdl = new CMyInputstream(instance);
+/// return ADDON_STATUS_OK;
+/// }
+/// else if (...)
+/// {
+/// ...
+/// }
+/// return ADDON_STATUS_UNKNOWN;
+/// }
+///
+/// ADDONCREATOR(CMyAddon)
+/// ~~~~~~~~~~~~~
+///
+/// The destruction of the example class `CMyInputstream` is called from
+/// Kodi's header. Manually deleting the add-on instance is not required.
+///
+///
+class ATTR_DLL_LOCAL CInstanceVideoCodec : public IAddonInstance
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec
+ /// @brief Video codec class constructor used to support multiple instance
+ /// types
+ ///
+ /// @param[in] instance The instance value given to <b>`kodi::addon::CAddonBase::CreateInstance(...)`</b>,
+ /// or by a inputstream instance if them declared as parent.
+ /// @param[in] kodiVersion [opt] Version used in Kodi for this instance, to
+ /// allow compatibility to older Kodi versions.
+ ///
+ explicit CInstanceVideoCodec(const IInstanceInfo& instance) : IAddonInstance(instance)
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstanceVideoCodec: Creation of multiple together with "
+ "single instance way is not allowed!");
+
+ SetAddonStruct(instance);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec
+ /// @brief Destructor
+ ///
+ ~CInstanceVideoCodec() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec
+ /// @brief Open the decoder, returns true on success
+ ///
+ /// Decoders not capable of running multiple instances should return false in case
+ /// there is already a instance open.
+ ///
+ /// @param[in] initData Video codec init data
+ /// @return true if successfully done
+ ///
+ ///
+ /// ----------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_videocodec_Defs_VideoCodecInitdata_Help
+ ///
+ virtual bool Open(const kodi::addon::VideoCodecInitdata& initData) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec
+ /// @brief Reconfigure the decoder, returns true on success
+ ///
+ /// Decoders not capable of running multiple instances may be capable of reconfiguring
+ /// the running instance. If Reconfigure returns false, player will close / open
+ /// the decoder
+ ///
+ /// @param[in] initData Video codec reconfigure data
+ /// @return true if successfully done
+ ///
+ virtual bool Reconfigure(const kodi::addon::VideoCodecInitdata& initData) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec
+ /// @brief add data, decoder has to consume the entire packet
+ ///
+ /// @param[in] packet Data to process for decode
+ /// @return true if the packet was consumed or if resubmitting it is useless
+ ///
+ virtual bool AddData(const DEMUX_PACKET& packet) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec
+ /// @brief GetPicture controls decoding.
+ ///
+ /// Player calls it on every cycle it can signal a picture, request a buffer,
+ /// or return none, if nothing applies the data is valid until the next
+ /// GetPicture return @ref VC_PICTURE
+ ///
+ /// @param[in,out] Structure which contains the necessary data
+ /// @return The with @ref VIDEOCODEC_RETVAL return values
+ ///
+ virtual VIDEOCODEC_RETVAL GetPicture(VIDEOCODEC_PICTURE& picture) { return VC_ERROR; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec
+ /// @brief should return codecs name
+ ///
+ /// @return Codec name
+ ///
+ virtual const char* GetName() { return nullptr; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec
+ /// @brief Reset the decoder
+ ///
+ virtual void Reset() {}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief AddonToKodi interface
+ */
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec
+ /// @brief All picture members can be expected to be set correctly except
+ /// decodedData and pts.
+ ///
+ /// GetFrameBuffer has to set decodedData to a valid memory address and return true.
+ ///
+ /// @param[out] picture The buffer, or unmodified if false is returned
+ /// @return In case buffer allocation fails, it return false.
+ ///
+ /// @note If this returns true, buffer must be freed using @ref ReleaseFrameBuffer().
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ bool GetFrameBuffer(VIDEOCODEC_PICTURE& picture)
+ {
+ return m_instanceData->toKodi->get_frame_buffer(m_instanceData->toKodi->kodiInstance, &picture);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ ///
+ /// @ingroup cpp_kodi_addon_videocodec
+ /// @brief Release the with @ref GetFrameBuffer() given framebuffer.
+ ///
+ /// @param[in] handle the on @ref VIDEOCODEC_PICTURE.videoBufferHandle defined buffer handle
+ ///
+ /// @remarks Only called from addon itself
+ ///
+ void ReleaseFrameBuffer(void* buffer)
+ {
+ return m_instanceData->toKodi->release_frame_buffer(m_instanceData->toKodi->kodiInstance,
+ buffer);
+ }
+ //----------------------------------------------------------------------------
+
+private:
+ void SetAddonStruct(KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ instance->hdl = this;
+ instance->videocodec->toAddon->open = ADDON_Open;
+ instance->videocodec->toAddon->reconfigure = ADDON_Reconfigure;
+ instance->videocodec->toAddon->add_data = ADDON_AddData;
+ instance->videocodec->toAddon->get_picture = ADDON_GetPicture;
+ instance->videocodec->toAddon->get_name = ADDON_GetName;
+ instance->videocodec->toAddon->reset = ADDON_Reset;
+
+ m_instanceData = instance->videocodec;
+ m_instanceData->toAddon->addonInstance = this;
+ }
+
+ inline static bool ADDON_Open(const AddonInstance_VideoCodec* instance,
+ VIDEOCODEC_INITDATA* initData)
+ {
+ return static_cast<CInstanceVideoCodec*>(instance->toAddon->addonInstance)->Open(initData);
+ }
+
+ inline static bool ADDON_Reconfigure(const AddonInstance_VideoCodec* instance,
+ VIDEOCODEC_INITDATA* initData)
+ {
+ return static_cast<CInstanceVideoCodec*>(instance->toAddon->addonInstance)
+ ->Reconfigure(initData);
+ }
+
+ inline static bool ADDON_AddData(const AddonInstance_VideoCodec* instance,
+ const DEMUX_PACKET* packet)
+ {
+ return static_cast<CInstanceVideoCodec*>(instance->toAddon->addonInstance)->AddData(*packet);
+ }
+
+ inline static VIDEOCODEC_RETVAL ADDON_GetPicture(const AddonInstance_VideoCodec* instance,
+ VIDEOCODEC_PICTURE* picture)
+ {
+ return static_cast<CInstanceVideoCodec*>(instance->toAddon->addonInstance)
+ ->GetPicture(*picture);
+ }
+
+ inline static const char* ADDON_GetName(const AddonInstance_VideoCodec* instance)
+ {
+ return static_cast<CInstanceVideoCodec*>(instance->toAddon->addonInstance)->GetName();
+ }
+
+ inline static void ADDON_Reset(const AddonInstance_VideoCodec* instance)
+ {
+ return static_cast<CInstanceVideoCodec*>(instance->toAddon->addonInstance)->Reset();
+ }
+
+ AddonInstance_VideoCodec* m_instanceData;
+};
+
+} // namespace addon
+} // namespace kodi
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Visualization.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Visualization.h
new file mode 100644
index 0000000..ab35c52
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/Visualization.h
@@ -0,0 +1,917 @@
+/*
+ * 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 "../AddonBase.h"
+#include "../c-api/addon-instance/visualization.h"
+#include "../gui/renderHelper.h"
+
+#ifdef __cplusplus
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_visualization_Defs_VisualizationTrack class VisualizationTrack
+/// @ingroup cpp_kodi_addon_visualization_Defs
+/// @brief **Info tag data structure**\n
+/// Representation of available information of processed audio file.
+///
+/// This is used to store all the necessary data of audio stream and to have on
+/// e.g. GUI for information.
+///
+/// Called from @ref kodi::addon::CInstanceVisualization::UpdateTrack() with the
+/// information of the currently-playing song.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_visualization_Defs_VisualizationTrack_Help
+///
+///@{
+class VisualizationTrack
+{
+ /*! \cond PRIVATE */
+ friend class CInstanceVisualization;
+ /*! \endcond */
+
+public:
+ /*! \cond PRIVATE */
+ VisualizationTrack() = default;
+ VisualizationTrack(const VisualizationTrack& tag) { *this = tag; }
+
+ VisualizationTrack& operator=(const VisualizationTrack& right)
+ {
+ if (&right == this)
+ return *this;
+
+ m_title = right.m_title;
+ m_artist = right.m_artist;
+ m_album = right.m_album;
+ m_albumArtist = right.m_albumArtist;
+ m_genre = right.m_genre;
+ m_comment = right.m_comment;
+ m_lyrics = right.m_lyrics;
+
+ m_trackNumber = right.m_trackNumber;
+ m_discNumber = right.m_discNumber;
+ m_duration = right.m_duration;
+ m_year = right.m_year;
+ m_rating = right.m_rating;
+ return *this;
+ }
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_visualization_Defs_VisualizationTrack_Help Value Help
+ /// @ingroup cpp_kodi_addon_visualization_Defs_VisualizationTrack
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_visualization_Defs_VisualizationTrack :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Title of the current song.** | `std::string` | @ref VisualizationTrack::SetTitle "SetTitle" | @ref VisualizationTrack::GetTitle "GetTitle"
+ /// | **Artist names, as a single string** | `std::string` | @ref VisualizationTrack::SetArtist "SetArtist" | @ref VisualizationTrack::GetArtist "GetArtist"
+ /// | **Album that the current song is from.** | `std::string` | @ref VisualizationTrack::SetAlbum "SetAlbum" | @ref VisualizationTrack::GetAlbum "GetAlbum"
+ /// | **Album artist names, as a single string** | `std::string` | @ref VisualizationTrack::SetAlbumArtist "SetAlbumArtist" | @ref VisualizationTrack::GetAlbumArtist "GetAlbumArtist"
+ /// | **The genre name from the music tag, if present** | `std::string` | @ref VisualizationTrack::SetGenre "SetGenre" | @ref VisualizationTrack::GetGenre "GetGenre"
+ /// | **Duration of the current song, in seconds** | `int` | @ref VisualizationTrack::SetDuration "SetDuration" | @ref VisualizationTrack::GetDuration "GetDuration"
+ /// | **Track number of the current song** | `int` | @ref VisualizationTrack::SetTrack "SetTrack" | @ref VisualizationTrack::GetTrack "GetTrack"
+ /// | **Disc number of the current song stored in the ID tag info** | `int` | @ref VisualizationTrack::SetDisc "SetDisc" | @ref VisualizationTrack::GetDisc "GetDisc"
+ /// | **Year that the current song was released** | `int` | @ref VisualizationTrack::SetYear "SetYear" | @ref VisualizationTrack::GetYear "GetYear"
+ /// | **Lyrics of the current song, if available** | `std::string` | @ref VisualizationTrack::SetLyrics "SetLyrics" | @ref VisualizationTrack::GetLyrics "GetLyrics"
+ /// | **The user-defined rating of the current song** | `int` | @ref VisualizationTrack::SetRating "SetRating" | @ref VisualizationTrack::GetRating "GetRating"
+ /// | **Comment of the current song stored in the ID tag info** | `std::string` | @ref VisualizationTrack::SetComment "SetComment" | @ref VisualizationTrack::GetComment "GetComment"
+ ///
+
+ /// @addtogroup cpp_kodi_addon_visualization_Defs_VisualizationTrack
+ ///@{
+
+ /// @brief Set title of the current song.
+ void SetTitle(const std::string& title) { m_title = title; }
+
+ /// @brief Get title of the current song.
+ const std::string& GetTitle() const { return m_title; }
+
+ /// @brief Set artist names, as a single string-
+ void SetArtist(const std::string& artist) { m_artist = artist; }
+
+ /// @brief Get artist names, as a single string-
+ const std::string& GetArtist() const { return m_artist; }
+
+ /// @brief Set Album that the current song is from.
+ void SetAlbum(const std::string& album) { m_album = album; }
+
+ /// @brief Get Album that the current song is from.
+ const std::string& GetAlbum() const { return m_album; }
+
+ /// @brief Set album artist names, as a single stringalbum artist name
+ void SetAlbumArtist(const std::string& albumArtist) { m_albumArtist = albumArtist; }
+
+ /// @brief Get album artist names, as a single string-
+ const std::string& GetAlbumArtist() const { return m_albumArtist; }
+
+ /// @brief Set genre name from music as string if present.
+ void SetGenre(const std::string& genre) { m_genre = genre; }
+
+ /// @brief Get genre name from music as string if present.
+ const std::string& GetGenre() const { return m_genre; }
+
+ /// @brief Set the duration of music as integer from info.
+ void SetDuration(int duration) { m_duration = duration; }
+
+ /// @brief Get the duration of music as integer from info.
+ int GetDuration() const { return m_duration; }
+
+ /// @brief Set track number (if present) from music info as integer.
+ void SetTrack(int trackNumber) { m_trackNumber = trackNumber; }
+
+ /// @brief Get track number (if present).
+ int GetTrack() const { return m_trackNumber; }
+
+ /// @brief Set disk number (if present) from music info as integer.
+ void SetDisc(int discNumber) { m_discNumber = discNumber; }
+
+ /// @brief Get disk number (if present)
+ int GetDisc() const { return m_discNumber; }
+
+ /// @brief Set year that the current song was released.
+ void SetYear(int year) { m_year = year; }
+
+ /// @brief Get year that the current song was released.
+ int GetYear() const { return m_year; }
+
+ /// @brief Set string from lyrics.
+ void SetLyrics(const std::string& lyrics) { m_lyrics = lyrics; }
+
+ /// @brief Get string from lyrics.
+ const std::string& GetLyrics() const { return m_lyrics; }
+
+ /// @brief Set the user-defined rating of the current song.
+ void SetRating(int rating) { m_rating = rating; }
+
+ /// @brief Get the user-defined rating of the current song.
+ int GetRating() const { return m_rating; }
+
+ /// @brief Set additional information comment (if present).
+ void SetComment(const std::string& comment) { m_comment = comment; }
+
+ /// @brief Get additional information comment (if present).
+ const std::string& GetComment() const { return m_comment; }
+
+ ///@}
+
+private:
+ VisualizationTrack(const KODI_ADDON_VISUALIZATION_TRACK* tag)
+ {
+ if (!tag)
+ return;
+
+ m_title = tag->title ? tag->title : "";
+ m_artist = tag->artist ? tag->artist : "";
+ m_album = tag->album ? tag->album : "";
+ m_albumArtist = tag->albumArtist ? tag->albumArtist : "";
+ m_genre = tag->genre ? tag->genre : "";
+ m_comment = tag->comment ? tag->comment : "";
+ m_lyrics = tag->lyrics ? tag->lyrics : "";
+
+ m_trackNumber = tag->trackNumber;
+ m_discNumber = tag->discNumber;
+ m_duration = tag->duration;
+ m_year = tag->year;
+ m_rating = tag->rating;
+ }
+
+ std::string m_title;
+ std::string m_artist;
+ std::string m_album;
+ std::string m_albumArtist;
+ std::string m_genre;
+ std::string m_comment;
+ std::string m_lyrics;
+
+ int m_trackNumber = 0;
+ int m_discNumber = 0;
+ int m_duration = 0;
+ int m_year = 0;
+ int m_rating = 0;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_visualization_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_addon_visualization
+/// @brief **Visualization add-on instance definition values**\n
+/// All visualization functions associated data structures.
+///
+/// Used to exchange the available options between Kodi and addon.
+///
+
+//==============================================================================
+/// @addtogroup cpp_kodi_addon_visualization
+/// @brief \cpp_class{ kodi::addon::CInstanceVisualization }
+/// **Visualization add-on instance**\n
+/// [Music visualization](https://en.wikipedia.org/wiki/Music_visualization),
+/// or music visualisation, is a feature in Kodi that generates animated
+/// imagery based on a piece of music. The imagery is usually generated and
+/// rendered in real time synchronized to the music.
+///
+/// Visualization techniques range from simple ones (e.g., a simulation of an
+/// oscilloscope display) to elaborate ones, which often include a plurality
+/// of composited effects. The changes in the music's loudness and frequency
+/// spectrum are among the properties used as input to the visualization.
+///
+/// Include the header @ref Visualization.h "#include <kodi/addon-instance/Visualization.h>"
+/// to use this class.
+///
+/// This interface allows the creation of visualizations for Kodi, based upon
+/// **DirectX** or/and **OpenGL** rendering with `C++` code.
+///
+/// Additionally, there are several @ref cpp_kodi_addon_visualization_CB "other functions"
+/// available in which the child class can ask about the current hardware,
+/// including the device, display and several other parts.
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Here's an example on addon.xml:**
+/// ~~~~~~~~~~~~~{.xml}
+/// <?xml version="1.0" encoding="UTF-8"?>
+/// <addon
+/// id="visualization.myspecialnamefor"
+/// version="1.0.0"
+/// name="My special visualization addon"
+/// provider-name="Your Name">
+/// <requires>@ADDON_DEPENDS@</requires>
+/// <extension
+/// point="xbmc.player.musicviz"
+/// library_@PLATFORM@="@LIBRARY_FILENAME@"/>
+/// <extension point="xbmc.addon.metadata">
+/// <summary lang="en_GB">My visualization addon addon</summary>
+/// <description lang="en_GB">My visualization addon description</description>
+/// <platform>@PLATFORM@</platform>
+/// </extension>
+/// </addon>
+/// ~~~~~~~~~~~~~
+///
+/// Description to visualization related addon.xml values:
+/// | Name | Description
+/// |:------------------------------|----------------------------------------
+/// | <b>`point`</b> | Addon type specification<br>At all addon types and for this kind always <b>"xbmc.player.musicviz"</b>.
+/// | <b>`library_@PLATFORM@`</b> | Sets the used library name, which is automatically set by cmake at addon build.
+///
+/// --------------------------------------------------------------------------
+///
+/// **Here is an example of the minimum required code to start a visualization:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/Visualization.h>
+///
+/// class CMyVisualization : public kodi::addon::CAddonBase,
+/// public kodi::addon::CInstanceVisualization
+/// {
+/// public:
+/// CMyVisualization();
+///
+/// bool Start(int channels, int samplesPerSec, int bitsPerSample, const std::string& songName) override;
+/// void AudioData(const float* audioData, size_t audioDataLength) override;
+/// void Render() override;
+/// };
+///
+/// CMyVisualization::CMyVisualization()
+/// {
+/// ...
+/// }
+///
+/// bool CMyVisualization::Start(int channels, int samplesPerSec, int bitsPerSample, const std::string& songName)
+/// {
+/// ...
+/// return true;
+/// }
+///
+/// void CMyVisualization::AudioData(const float* audioData, size_t audioDataLength)
+/// {
+/// ...
+/// }
+///
+/// void CMyVisualization::Render()
+/// {
+/// ...
+/// }
+///
+/// ADDONCREATOR(CMyVisualization)
+/// ~~~~~~~~~~~~~
+///
+///
+/// --------------------------------------------------------------------------
+///
+///
+/// **Here is another example where the visualization is used together with
+/// other instance types:**
+///
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/addon-instance/Visualization.h>
+///
+/// class CMyVisualization : public kodi::addon::CInstanceVisualization
+/// {
+/// public:
+/// CMyVisualization(const kodi::addon::IInstanceInfo& instance);
+///
+/// bool Start(int channels, int samplesPerSec, int bitsPerSample, const std::string& songName) override;
+/// void AudioData(const float* audioData, size_t audioDataLength) override;
+/// void Render() override;
+/// };
+///
+/// CMyVisualization::CMyVisualization(const kodi::addon::IInstanceInfo& instance)
+/// : kodi::addon::CInstanceAudioDecoder(instance)
+/// {
+/// ...
+/// }
+///
+/// bool CMyVisualization::Start(int channels, int samplesPerSec, int bitsPerSample, const std::string& songName)
+/// {
+/// ...
+/// return true;
+/// }
+///
+/// void CMyVisualization::AudioData(const float* audioData, size_t audioDataLength)
+/// {
+/// ...
+/// }
+///
+/// void CMyVisualization::Render()
+/// {
+/// ...
+/// }
+///
+///
+/// //----------------------------------------------------------------------
+///
+/// class CMyAddon : public kodi::addon::CAddonBase
+/// {
+/// public:
+/// CMyAddon() = default;
+/// ADDON_STATUS CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl) override;
+/// };
+///
+/// // If you use only one instance in your add-on, can be instanceType and
+/// // instanceID ignored
+/// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+/// KODI_ADDON_INSTANCE_HDL& hdl)
+/// {
+/// if (instance.IsType(ADDON_INSTANCE_VISUALIZATION))
+/// {
+/// kodi::Log(ADDON_LOG_INFO, "Creating my visualization");
+/// hdl = new CMyVisualization(instance);
+/// return ADDON_STATUS_OK;
+/// }
+/// else if (...)
+/// {
+/// ...
+/// }
+/// return ADDON_STATUS_UNKNOWN;
+/// }
+///
+/// ADDONCREATOR(CMyAddon)
+/// ~~~~~~~~~~~~~
+///
+/// The destruction of the example class `CMyVisualization` is called from
+/// Kodi's header. Manually deleting the add-on instance is not required.
+///
+class ATTR_DLL_LOCAL CInstanceVisualization : public IAddonInstance
+{
+public:
+ //============================================================================
+ ///
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Visualization class constructor
+ ///
+ /// Used by an add-on that only supports visualizations.
+ ///
+ CInstanceVisualization()
+ : IAddonInstance(IInstanceInfo(CPrivateBase::m_interface->firstKodiInstance))
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error(
+ "kodi::addon::CInstanceVisualization: Cannot create multiple instances of add-on.");
+
+ SetAddonStruct(CPrivateBase::m_interface->firstKodiInstance);
+ CPrivateBase::m_interface->globalSingleInstance = this;
+ }
+ //----------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Visualization class constructor used to support multiple instance
+ /// types.
+ ///
+ /// @param[in] instance The instance value given to
+ /// <b>`kodi::addon::CAddonBase::CreateInstance(...)`</b>.
+ /// @param[in] kodiVersion [opt] Version used in Kodi for this instance, to
+ /// allow compatibility to older Kodi versions.
+ ///
+ /// @note Recommended to set <b>`kodiVersion`</b>.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Here's example about the use of this:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// class CMyVisualization : public kodi::addon::CInstanceAudioDecoder
+ /// {
+ /// public:
+ /// CMyVisualization(const kodi::addon::IInstanceInfo& instance)
+ /// : kodi::addon::CInstanceAudioDecoder(instance)
+ /// {
+ /// ...
+ /// }
+ ///
+ /// ...
+ /// };
+ ///
+ /// ADDON_STATUS CMyAddon::CreateInstance(const kodi::addon::IInstanceInfo& instance,
+ /// KODI_ADDON_INSTANCE_HDL& hdl)
+ /// {
+ /// kodi::Log(ADDON_LOG_INFO, "Creating my visualization");
+ /// hdl = new CMyVisualization(instance);
+ /// return ADDON_STATUS_OK;
+ /// }
+ /// ~~~~~~~~~~~~~
+ ///
+ explicit CInstanceVisualization(const IInstanceInfo& instance) : IAddonInstance(instance)
+ {
+ if (CPrivateBase::m_interface->globalSingleInstance != nullptr)
+ throw std::logic_error("kodi::addon::CInstanceVisualization: Creation of multiple together "
+ "with single instance way is not allowed!");
+
+ SetAddonStruct(instance);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Destructor.
+ ///
+ ~CInstanceVisualization() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Used to notify the visualization that a new song has been started.
+ ///
+ /// @param[in] channels Number of channels in the stream
+ /// @param[in] samplesPerSec Samples per second of stream
+ /// @param[in] bitsPerSample Number of bits in one sample
+ /// @param[in] songName The name of the currently-playing song
+ /// @return true if start successful done
+ ///
+ virtual bool Start(int channels,
+ int samplesPerSec,
+ int bitsPerSample,
+ const std::string& songName)
+ {
+ return true;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Used to inform the visualization that the rendering control was
+ /// stopped.
+ ///
+ virtual void Stop() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Pass audio data to the visualization.
+ ///
+ /// @param[in] audioData The raw audio data
+ /// @param[in] audioDataLength Length of the audioData array
+ ///
+ virtual void AudioData(const float* audioData, size_t audioDataLength) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Used to inform Kodi that the rendered region is dirty and need an
+ /// update.
+ ///
+ /// @return True if dirty
+ ///
+ virtual bool IsDirty() { return true; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Used to indicate when the add-on should render.
+ ///
+ virtual void Render() {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Used to get the number of buffers from the current visualization.
+ ///
+ /// @return The number of buffers to delay before calling @ref AudioData()
+ ///
+ /// @note If this function is not implemented, it will default to 0.
+ ///
+ virtual int GetSyncDelay() { return 0; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Used to get a list of visualization presets the user can select.
+ /// from
+ ///
+ /// @param[out] presets The vector list containing the names of presets that
+ /// the user can select
+ /// @return Return true if successful, or false if there are no presets to
+ /// choose from
+ ///
+ virtual bool GetPresets(std::vector<std::string>& presets) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Get the index of the current preset.
+ ///
+ /// @return Index number of the current preset
+ ///
+ virtual int GetActivePreset() { return -1; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Check if the add-on is locked to the current preset.
+ ///
+ /// @return True if locked to the current preset
+ ///
+ virtual bool IsLocked() { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Load the previous visualization preset.
+ ///
+ /// @return Return true if the previous preset was loaded
+ ///
+ virtual bool PrevPreset() { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Load the next visualization preset.
+ ///
+ /// @return Return true if the next preset was loaded
+ ///
+ virtual bool NextPreset() { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Load a visualization preset.
+ ///
+ /// This function is called after a new preset is selected.
+ ///
+ /// @param[in] select Preset index to use
+ /// @return Return true if the preset is loaded
+ ///
+ virtual bool LoadPreset(int select) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Switch to a new random preset.
+ ///
+ /// @return Return true if a random preset was loaded
+ ///
+ virtual bool RandomPreset() { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Lock the current visualization preset, preventing it from changing.
+ ///
+ /// @param[in] lockUnlock If set to true, the preset should be locked
+ /// @return Return true if the current preset is locked
+ ///
+ virtual bool LockPreset(bool lockUnlock) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Used to increase/decrease the visualization preset rating.
+ ///
+ /// @param[in] plusMinus If set to true the rating is increased, otherwise
+ /// decreased
+ /// @return Return true if the rating is modified
+ ///
+ virtual bool RatePreset(bool plusMinus) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Inform the visualization of the current album art image.
+ ///
+ /// @param[in] albumart Path to the current album art image
+ /// @return Return true if the image is used
+ ///
+ virtual bool UpdateAlbumart(const std::string& albumart) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief Inform the visualization of the current track's tag information.
+ ///
+ /// @param[in] track Visualization track information structure
+ /// @return Return true if the track information is used
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_visualization_Defs_VisualizationTrack_Help
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ ///
+ /// #include <kodi/addon-instance/Visualization.h>
+ ///
+ /// class CMyVisualization : public kodi::addon::CInstanceVisualization
+ /// {
+ /// public:
+ /// CMyVisualization(KODI_HANDLE instance, const std::string& version);
+ ///
+ /// ...
+ ///
+ /// private:
+ /// kodi::addon::VisualizationTrack m_runningTrack;
+ /// };
+ ///
+ /// bool CMyVisualization::UpdateTrack(const kodi::addon::VisualizationTrack& track)
+ /// {
+ /// m_runningTrack = track;
+ /// return true;
+ /// }
+ ///
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual bool UpdateTrack(const kodi::addon::VisualizationTrack& track) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_visualization_CB Information functions
+ /// @ingroup cpp_kodi_addon_visualization
+ /// @brief **To get info about the device, display and several other parts**\n
+ /// These are functions to query any values or to transfer them to Kodi.
+ ///
+ ///@{
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization_CB
+ /// @brief To transfer available presets on addon.
+ ///
+ /// Used if @ref GetPresets not possible to use, e.g. where available presets
+ /// are only known during @ref Start call.
+ ///
+ /// @param[in] presets List to store available presets.
+ ///
+ /// @note The function should only be called once, if possible
+ ///
+ inline void TransferPresets(const std::vector<std::string>& presets)
+ {
+ m_instanceData->visualization->toKodi->clear_presets(m_instanceData->info->kodi);
+ for (const auto& it : presets)
+ m_instanceData->visualization->toKodi->transfer_preset(m_instanceData->info->kodi,
+ it.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization_CB
+ /// @brief Device that represents the display adapter.
+ ///
+ /// @return A pointer to the used device with @ref cpp_kodi_Defs_HardwareContext "HardwareContext"
+ ///
+ /// @note This is only available on **DirectX**, It us unused (`nullptr`) on
+ /// **OpenGL**
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <d3d11_1.h>
+ /// ..
+ /// // Note: Device() there is used inside addon child class about
+ /// // kodi::addon::CInstanceVisualization
+ /// ID3D11DeviceContext1* context = static_cast<ID3D11DeviceContext1*>(kodi::addon::CInstanceVisualization::Device());
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ inline kodi::HardwareContext Device() { return m_props.device; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization_CB
+ /// @brief Returns the X position of the rendering window.
+ ///
+ /// @return The X position, in pixels
+ ///
+ inline int X() { return m_props.x; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization_CB
+ /// @brief Returns the Y position of the rendering window.
+ ///
+ /// @return The Y position, in pixels
+ ///
+ inline int Y() { return m_props.y; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization_CB
+ /// @brief Returns the width of the rendering window.
+ ///
+ /// @return The width, in pixels
+ ///
+ inline int Width() { return m_props.width; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization_CB
+ /// @brief Returns the height of the rendering window.
+ ///
+ /// @return The height, in pixels
+ ///
+ inline int Height() { return m_props.height; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_visualization_CB
+ /// @brief Pixel aspect ratio (often abbreviated PAR) is a ratio that
+ /// describes how the width of a pixel compares to the height of that pixel.
+ ///
+ /// @return The pixel aspect ratio used by the display
+ ///
+ inline float PixelRatio() { return m_props.pixelRatio; }
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+private:
+ void SetAddonStruct(KODI_ADDON_INSTANCE_STRUCT* instance)
+ {
+ m_instanceData = instance;
+ m_instanceData->hdl = this;
+ m_instanceData->visualization->toAddon->start = ADDON_start;
+ m_instanceData->visualization->toAddon->stop = ADDON_stop;
+ m_instanceData->visualization->toAddon->audio_data = ADDON_audio_data;
+ m_instanceData->visualization->toAddon->is_dirty = ADDON_is_dirty;
+ m_instanceData->visualization->toAddon->render = ADDON_render;
+ m_instanceData->visualization->toAddon->get_sync_delay = ADDON_get_sync_delay;
+ m_instanceData->visualization->toAddon->prev_preset = ADDON_prev_preset;
+ m_instanceData->visualization->toAddon->next_preset = ADDON_next_preset;
+ m_instanceData->visualization->toAddon->load_preset = ADDON_load_preset;
+ m_instanceData->visualization->toAddon->random_preset = ADDON_random_preset;
+ m_instanceData->visualization->toAddon->lock_preset = ADDON_lock_preset;
+ m_instanceData->visualization->toAddon->rate_preset = ADDON_rate_preset;
+ m_instanceData->visualization->toAddon->update_albumart = ADDON_update_albumart;
+ m_instanceData->visualization->toAddon->update_track = ADDON_update_track;
+ m_instanceData->visualization->toAddon->get_presets = ADDON_get_presets;
+ m_instanceData->visualization->toAddon->get_active_preset = ADDON_get_active_preset;
+ m_instanceData->visualization->toAddon->is_locked = ADDON_is_locked;
+
+ m_instanceData->visualization->toKodi->get_properties(instance->info->kodi, &m_props);
+ }
+
+ inline static bool ADDON_start(const KODI_ADDON_VISUALIZATION_HDL hdl,
+ int channels,
+ int samplesPerSec,
+ int bitsPerSample,
+ const char* songName)
+ {
+ CInstanceVisualization* thisClass = static_cast<CInstanceVisualization*>(hdl);
+ thisClass->m_renderHelper = kodi::gui::GetRenderHelper();
+ return thisClass->Start(channels, samplesPerSec, bitsPerSample, songName);
+ }
+
+ inline static void ADDON_stop(const KODI_ADDON_VISUALIZATION_HDL hdl)
+ {
+ CInstanceVisualization* thisClass = static_cast<CInstanceVisualization*>(hdl);
+ thisClass->Stop();
+ thisClass->m_renderHelper = nullptr;
+ }
+
+ inline static void ADDON_audio_data(const KODI_ADDON_VISUALIZATION_HDL hdl,
+ const float* audioData,
+ size_t audioDataLength)
+ {
+ static_cast<CInstanceVisualization*>(hdl)->AudioData(audioData, audioDataLength);
+ }
+
+ inline static bool ADDON_is_dirty(const KODI_ADDON_VISUALIZATION_HDL hdl)
+ {
+ return static_cast<CInstanceVisualization*>(hdl)->IsDirty();
+ }
+
+ inline static void ADDON_render(const KODI_ADDON_VISUALIZATION_HDL hdl)
+ {
+ CInstanceVisualization* thisClass = static_cast<CInstanceVisualization*>(hdl);
+ if (!thisClass->m_renderHelper)
+ return;
+ thisClass->m_renderHelper->Begin();
+ thisClass->Render();
+ thisClass->m_renderHelper->End();
+ }
+
+ inline static int ADDON_get_sync_delay(const KODI_ADDON_VISUALIZATION_HDL hdl)
+ {
+ return static_cast<CInstanceVisualization*>(hdl)->GetSyncDelay();
+ }
+
+ inline static unsigned int ADDON_get_presets(const KODI_ADDON_VISUALIZATION_HDL hdl)
+ {
+ CInstanceVisualization* thisClass = static_cast<CInstanceVisualization*>(hdl);
+ std::vector<std::string> presets;
+ if (thisClass->GetPresets(presets))
+ {
+ for (const auto& it : presets)
+ thisClass->m_instanceData->visualization->toKodi->transfer_preset(
+ thisClass->m_instanceData->info->kodi, it.c_str());
+ }
+
+ return static_cast<unsigned int>(presets.size());
+ }
+
+ inline static int ADDON_get_active_preset(const KODI_ADDON_VISUALIZATION_HDL hdl)
+ {
+ return static_cast<CInstanceVisualization*>(hdl)->GetActivePreset();
+ }
+
+ inline static bool ADDON_prev_preset(const KODI_ADDON_VISUALIZATION_HDL hdl)
+ {
+ return static_cast<CInstanceVisualization*>(hdl)->PrevPreset();
+ }
+
+ inline static bool ADDON_next_preset(const KODI_ADDON_VISUALIZATION_HDL hdl)
+ {
+ return static_cast<CInstanceVisualization*>(hdl)->NextPreset();
+ }
+
+ inline static bool ADDON_load_preset(const KODI_ADDON_VISUALIZATION_HDL hdl, int select)
+
+ {
+ return static_cast<CInstanceVisualization*>(hdl)->LoadPreset(select);
+ }
+
+ inline static bool ADDON_random_preset(const KODI_ADDON_VISUALIZATION_HDL hdl)
+ {
+ return static_cast<CInstanceVisualization*>(hdl)->RandomPreset();
+ }
+
+ inline static bool ADDON_lock_preset(const KODI_ADDON_VISUALIZATION_HDL hdl)
+ {
+ CInstanceVisualization* thisClass = static_cast<CInstanceVisualization*>(hdl);
+ thisClass->m_presetLockedByUser = !thisClass->m_presetLockedByUser;
+ return thisClass->LockPreset(thisClass->m_presetLockedByUser);
+ }
+
+ inline static bool ADDON_rate_preset(const KODI_ADDON_VISUALIZATION_HDL hdl, bool plus_minus)
+ {
+ return static_cast<CInstanceVisualization*>(hdl)->RatePreset(plus_minus);
+ }
+
+ inline static bool ADDON_is_locked(const KODI_ADDON_VISUALIZATION_HDL hdl)
+ {
+ return static_cast<CInstanceVisualization*>(hdl)->IsLocked();
+ }
+
+ inline static bool ADDON_update_albumart(const KODI_ADDON_VISUALIZATION_HDL hdl,
+ const char* albumart)
+ {
+ return static_cast<CInstanceVisualization*>(hdl)->UpdateAlbumart(albumart);
+ }
+
+ inline static bool ADDON_update_track(const KODI_ADDON_VISUALIZATION_HDL hdl,
+ const KODI_ADDON_VISUALIZATION_TRACK* track)
+ {
+ VisualizationTrack cppTrack(track);
+ return static_cast<CInstanceVisualization*>(hdl)->UpdateTrack(cppTrack);
+ }
+
+ std::shared_ptr<kodi::gui::IRenderHelper> m_renderHelper;
+ bool m_presetLockedByUser = false;
+ KODI_ADDON_INSTANCE_STRUCT* m_instanceData{nullptr};
+ KODI_ADDON_VISUALIZATION_PROPS m_props = {};
+};
+
+} /* namespace addon */
+} /* namespace kodi */
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/CMakeLists.txt
new file mode 100644
index 0000000..ebbeeb7
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/CMakeLists.txt
@@ -0,0 +1,14 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ DemuxPacket.h
+ StreamCodec.h
+ StreamConstants.h
+ StreamCrypto.h
+ TimingConstants.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_addon-instance_inputstream)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/DemuxPacket.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/DemuxPacket.h
new file mode 100644
index 0000000..f965b9f
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/DemuxPacket.h
@@ -0,0 +1,12 @@
+/*
+ * Copyright (C) 2005-2020 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 "TimingConstants.h"
+#include "../../c-api/addon-instance/inputstream/demux_packet.h"
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamCodec.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamCodec.h
new file mode 100644
index 0000000..e80e2ca
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamCodec.h
@@ -0,0 +1,11 @@
+/*
+ * Copyright (C) 2005-2020 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 "../../c-api/addon-instance/inputstream/stream_codec.h"
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamConstants.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamConstants.h
new file mode 100644
index 0000000..200631b
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamConstants.h
@@ -0,0 +1,11 @@
+/*
+ * Copyright (C) 2005-2020 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 "../../c-api/addon-instance/inputstream/stream_constants.h"
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamCrypto.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamCrypto.h
new file mode 100644
index 0000000..8e04ae3
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/StreamCrypto.h
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2005-2020 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 "../../c-api/addon-instance/inputstream/stream_crypto.h"
+#include "../../AddonBase.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+class CInstanceInputStream;
+class InputstreamInfo;
+class VideoCodecInitdata;
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_inputstream_Defs_StreamEncryption_StreamCryptoSession class StreamCryptoSession
+/// @ingroup cpp_kodi_addon_inputstream_Defs_StreamEncryption
+/// @brief **Data to manage stream cryptography**\n
+/// This class structure manages any encryption values required in order to have
+/// them available in their stream processing.
+///
+/// Used on inputstream by @ref kodi::addon::InputstreamInfo::SetCryptoSession /
+/// @ref kodi::addon::InputstreamInfo::GetCryptoSession and are given to the
+/// used video codec to decrypt related data.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_inputstream_Defs_Info_StreamCryptoSession_Help
+///
+///@{
+class ATTR_DLL_LOCAL StreamCryptoSession
+ : public CStructHdl<StreamCryptoSession, STREAM_CRYPTO_SESSION>
+{
+ /*! \cond PRIVATE */
+ friend class CInstanceInputStream;
+ friend class InputstreamInfo;
+ friend class VideoCodecInitdata;
+ /*! \endcond */
+
+public:
+ /*! \cond PRIVATE */
+ StreamCryptoSession() { memset(m_cStructure, 0, sizeof(STREAM_CRYPTO_SESSION)); }
+ StreamCryptoSession(const StreamCryptoSession& session) : CStructHdl(session) {}
+ StreamCryptoSession& operator=(const StreamCryptoSession&) = default;
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Info_StreamCryptoSession_Help Value Help
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Info_StreamCryptoSession
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_inputstream_Defs_Info_StreamCryptoSession :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Keysystem for encrypted media** | @ref STREAM_CRYPTO_KEY_SYSTEM | @ref StreamCryptoSession::SetKeySystem "SetKeySystem" | @ref StreamCryptoSession::GetKeySystem "GetKeySystem"
+ /// | **Flags for special conditions** | `uint8_t` | @ref StreamCryptoSession::SetFlags "SetFlags" | @ref StreamCryptoSession::GetFlags "GetFlags"
+ /// | **Crypto session key id** | `std::string` | @ref StreamCryptoSession::SetSessionId "SetSessionId" | @ref StreamCryptoSession::GetSessionId "GetSessionId"
+
+ /// @brief To set keysystem for encrypted media, @ref STREAM_CRYPTO_KEY_SYSTEM_NONE for
+ /// unencrypted media.
+ ///
+ /// See @ref STREAM_CRYPTO_KEY_SYSTEM for available options.
+ void SetKeySystem(STREAM_CRYPTO_KEY_SYSTEM keySystem) { m_cStructure->keySystem = keySystem; }
+
+ /// @brief Get keysystem for encrypted media.
+ STREAM_CRYPTO_KEY_SYSTEM GetKeySystem() const { return m_cStructure->keySystem; }
+
+ /// @brief Set bit flags to use special conditions, see @ref STREAM_CRYPTO_FLAGS
+ /// for available flags.
+ void SetFlags(uint8_t flags) { m_cStructure->flags = flags; }
+
+ /// @brief Get flags for special conditions.
+ uint8_t GetFlags() const { return m_cStructure->flags; }
+
+ /// @brief To set the crypto session key identifier.
+ void SetSessionId(const std::string& sessionId)
+ {
+ strncpy(m_cStructure->sessionId, sessionId.c_str(), sizeof(m_cStructure->sessionId) - 1);
+ }
+
+ /// @brief To get the crypto session key identifier.
+ std::string GetSessionId() const { return m_cStructure->sessionId; }
+
+private:
+ StreamCryptoSession(const STREAM_CRYPTO_SESSION* session) : CStructHdl(session) {}
+ StreamCryptoSession(STREAM_CRYPTO_SESSION* session) : CStructHdl(session) {}
+};
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/TimingConstants.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/TimingConstants.h
new file mode 100644
index 0000000..22f8952
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/inputstream/TimingConstants.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2005-2020 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 "../../c-api/addon-instance/inputstream/timing_constants.h"
+
+#ifdef __cplusplus
+
+// Unset the on timing_constants.h given defines
+#undef STREAM_TIME_TO_MSEC
+#undef STREAM_SEC_TO_TIME
+#undef STREAM_MSEC_TO_TIME
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_TimingConstants
+/// @brief Converts a stream time to milliseconds as an integer value.
+///
+/// @param[in] x Stream time
+/// @return Milliseconds
+///
+/// @note Within "C" code this is used as `#define`.
+///
+constexpr int STREAM_TIME_TO_MSEC(double x)
+{
+ return static_cast<int>(x * 1000 / STREAM_TIME_BASE);
+}
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_TimingConstants
+/// @brief Converts a time in seconds to the used stream time format.
+///
+/// @param[in] x Seconds
+/// @return Stream time
+///
+/// @note Within "C" code this is used as `#define`.
+///
+constexpr double STREAM_SEC_TO_TIME(double x)
+{
+ return x * STREAM_TIME_BASE;
+}
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_TimingConstants
+/// @brief Converts a time in milliseconds to the used stream time format.
+///
+/// @param[in] x Milliseconds
+/// @return Stream time
+///
+/// @note Within "C" code this is used as `#define`.
+///
+constexpr double STREAM_MSEC_TO_TIME(double x)
+{
+ return x * STREAM_TIME_BASE / 1000;
+}
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/peripheral/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/peripheral/CMakeLists.txt
new file mode 100644
index 0000000..135358e
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/peripheral/CMakeLists.txt
@@ -0,0 +1,10 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ PeripheralUtils.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_addon-instance_peripheral)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/peripheral/PeripheralUtils.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/peripheral/PeripheralUtils.h
new file mode 100644
index 0000000..bedd7cb
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/peripheral/PeripheralUtils.h
@@ -0,0 +1,1277 @@
+/*
+ * Copyright (C) 2014-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 "../../AddonBase.h"
+#include "../../c-api/addon-instance/peripheral.h"
+
+#ifdef __cplusplus
+
+#include <array> // Requires c++11
+#include <cstring>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#define PERIPHERAL_SAFE_DELETE(x) \
+ do \
+ { \
+ delete (x); \
+ (x) = NULL; \
+ } while (0)
+#define PERIPHERAL_SAFE_DELETE_ARRAY(x) \
+ do \
+ { \
+ delete[](x); \
+ (x) = NULL; \
+ } while (0)
+
+namespace kodi
+{
+namespace addon
+{
+
+class CInstancePeripheral;
+
+/*!
+ * Utility class to manipulate arrays of peripheral types.
+ */
+template<class THE_CLASS, typename THE_STRUCT>
+class PeripheralVector
+{
+public:
+ static void ToStructs(const std::vector<THE_CLASS>& vecObjects, THE_STRUCT** pStructs)
+ {
+ if (!pStructs)
+ return;
+
+ if (vecObjects.empty())
+ {
+ *pStructs = NULL;
+ }
+ else
+ {
+ (*pStructs) = new THE_STRUCT[vecObjects.size()];
+ for (unsigned int i = 0; i < vecObjects.size(); i++)
+ vecObjects.at(i).ToStruct((*pStructs)[i]);
+ }
+ }
+
+ static void ToStructs(const std::vector<THE_CLASS*>& vecObjects, THE_STRUCT** pStructs)
+ {
+ if (!pStructs)
+ return;
+
+ if (vecObjects.empty())
+ {
+ *pStructs = NULL;
+ }
+ else
+ {
+ *pStructs = new THE_STRUCT[vecObjects.size()];
+ for (unsigned int i = 0; i < vecObjects.size(); i++)
+ vecObjects.at(i)->ToStruct((*pStructs)[i]);
+ }
+ }
+
+ static void ToStructs(const std::vector<std::shared_ptr<THE_CLASS>>& vecObjects,
+ THE_STRUCT** pStructs)
+ {
+ if (!pStructs)
+ return;
+
+ if (vecObjects.empty())
+ {
+ *pStructs = NULL;
+ }
+ else
+ {
+ *pStructs = new THE_STRUCT[vecObjects.size()];
+ for (unsigned int i = 0; i < vecObjects.size(); i++)
+ vecObjects.at(i)->ToStruct((*pStructs)[i]);
+ }
+ }
+
+ static void FreeStructs(unsigned int structCount, THE_STRUCT* structs)
+ {
+ if (structs)
+ {
+ for (unsigned int i = 0; i < structCount; i++)
+ THE_CLASS::FreeStruct(structs[i]);
+ }
+ PERIPHERAL_SAFE_DELETE_ARRAY(structs);
+ }
+};
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_peripheral_Defs_PeripheralCapabilities class PeripheralCapabilities
+/// @ingroup cpp_kodi_addon_peripheral_Defs_General
+/// @brief **%Peripheral add-on capabilities**\n
+/// This class is needed to tell Kodi which options are supported on the addon.
+///
+/// If a capability is set to **true**, then the corresponding methods from
+/// @ref cpp_kodi_addon_peripheral "kodi::addon::CInstancePeripheral" need to be
+/// implemented.
+///
+/// As default them all set to **false**.
+///
+/// Used on @ref kodi::addon::CInstancePeripheral::GetCapabilities().
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_peripheral_Defs_PeripheralCapabilities_Help
+///
+///@{
+class PeripheralCapabilities : public CStructHdl<PeripheralCapabilities, PERIPHERAL_CAPABILITIES>
+{
+ /*! \cond PRIVATE */
+ friend class CInstancePeripheral;
+ /*! \endcond */
+
+public:
+ /*! \cond PRIVATE */
+ PeripheralCapabilities()
+ {
+ m_cStructure->provides_joysticks = false;
+ m_cStructure->provides_joystick_rumble = false;
+ m_cStructure->provides_joystick_power_off = false;
+ m_cStructure->provides_buttonmaps = false;
+ }
+
+ PeripheralCapabilities(const PeripheralCapabilities& data) : CStructHdl(data) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_PeripheralCapabilities_Help Value Help
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_PeripheralCapabilities
+ /// ----------------------------------------------------------------------------
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_peripheral_Defs_PeripheralCapabilities :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Provides joysticks** | `boolean` | @ref PeripheralCapabilities::SetProvidesJoysticks "SetProvidesJoysticks" | @ref PeripheralCapabilities::GetProvidesJoysticks "GetProvidesJoysticks"
+ /// | **Provides joystick rumble** | `boolean` | @ref PeripheralCapabilities::SetProvidesJoystickRumble "SetProvidesJoystickRumble" | @ref PeripheralCapabilities::GetProvidesJoystickRumble "GetProvidesJoystickRumble"
+ /// | **Provides joystick power off** | `boolean` | @ref PeripheralCapabilities::SetProvidesJoystickPowerOff "SetProvidesJoystickPowerOff" | @ref PeripheralCapabilities::GetProvidesJoystickPowerOff "GetProvidesJoystickPowerOff"
+ /// | **Provides button maps** | `boolean` | @ref PeripheralCapabilities::SetProvidesButtonmaps "SetProvidesButtonmaps" | @ref PeripheralCapabilities::GetProvidesButtonmaps "GetProvidesButtonmaps"
+
+ /// @addtogroup cpp_kodi_addon_peripheral_Defs_PeripheralCapabilities
+ ///@{
+
+ /// @brief Set true if the add-on provides joysticks.
+ void SetProvidesJoysticks(bool providesJoysticks)
+ {
+ m_cStructure->provides_joysticks = providesJoysticks;
+ }
+
+ /// @brief To get with @ref SetProvidesJoysticks changed values.
+ bool GetProvidesJoysticks() const { return m_cStructure->provides_joysticks; }
+
+ /// @brief Set true if the add-on provides joystick rumble.
+ void SetProvidesJoystickRumble(bool providesJoystickRumble)
+ {
+ m_cStructure->provides_joystick_rumble = providesJoystickRumble;
+ }
+
+ /// @brief To get with @ref SetProvidesJoystickRumble changed values.
+ bool GetProvidesJoystickRumble() const { return m_cStructure->provides_joystick_rumble; }
+
+ /// @brief Set true if the add-on provides power off about joystick.
+ void SetProvidesJoystickPowerOff(bool providesJoystickPowerOff)
+ {
+ m_cStructure->provides_joystick_power_off = providesJoystickPowerOff;
+ }
+
+ /// @brief To get with @ref SetProvidesJoystickPowerOff changed values.
+ bool GetProvidesJoystickPowerOff() const { return m_cStructure->provides_joystick_power_off; }
+
+ /// @brief Set true if the add-on provides button maps.
+ void SetProvidesButtonmaps(bool providesButtonmaps)
+ {
+ m_cStructure->provides_buttonmaps = providesButtonmaps;
+ }
+
+ /// @brief To get with @ref SetProvidesButtonmaps changed values.
+ bool GetProvidesButtonmaps() const { return m_cStructure->provides_buttonmaps; }
+
+ ///@}
+
+private:
+ PeripheralCapabilities(const PERIPHERAL_CAPABILITIES* data) : CStructHdl(data) {}
+ PeripheralCapabilities(PERIPHERAL_CAPABILITIES* data) : CStructHdl(data) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_peripheral_Defs_Peripheral_Peripheral class Peripheral
+/// @ingroup cpp_kodi_addon_peripheral_Defs_Peripheral
+/// @brief **Wrapper class providing peripheral information**\n
+/// Classes can extend %Peripheral to inherit peripheral properties.
+///
+/// Used on @ref kodi::addon::CInstancePeripheral::PerformDeviceScan().
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_peripheral_Defs_Peripheral_Peripheral_Help
+///
+///@{
+class Peripheral
+{
+public:
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Peripheral_Peripheral_Help Value Help
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Peripheral_Peripheral
+ /// ----------------------------------------------------------------------------
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_peripheral_Defs_Peripheral_Peripheral :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **%Peripheral type** | @ref PERIPHERAL_TYPE | @ref Peripheral::SetType "SetType" | @ref Peripheral::Type "Type"
+ /// | **%Peripheral name** | `const std::string&` | @ref Peripheral::SetName "SetName" | @ref Peripheral::Name "Name"
+ /// | **%Peripheral vendor id** | `uint16_t` | @ref Peripheral::SetVendorID "SetVendorID" | @ref Peripheral::VendorID "VendorID"
+ /// | **%Peripheral product id** | `uint16_t` | @ref Peripheral::SetProductID "SetProductID" | @ref Peripheral::ProductID "ProductID"
+ /// | **%Peripheral index** | `unsigned int` | @ref Peripheral::SetIndex "SetIndex" | @ref Peripheral::Index "Index"
+ ///
+ /// Further are following included:
+ /// - @ref Peripheral::Peripheral "Peripheral(PERIPHERAL_TYPE type = PERIPHERAL_TYPE_UNKNOWN, const std::string& strName = \"\")": Class constructor.
+ /// - @ref Peripheral::IsVidPidKnown "IsVidPidKnown()": To check VID and PID are known.
+ ///
+
+ /// @addtogroup cpp_kodi_addon_peripheral_Defs_Peripheral_Peripheral
+ ///@{
+
+ /// @brief Constructor.
+ ///
+ /// @param[in] type [optional] Peripheral type, or @ref PERIPHERAL_TYPE_UNKNOWN
+ /// as default
+ /// @param[in] strName [optional] Name of related peripheral
+ Peripheral(PERIPHERAL_TYPE type = PERIPHERAL_TYPE_UNKNOWN, const std::string& strName = "")
+ : m_type(type), m_strName(strName)
+ {
+ }
+
+ /// @brief Destructor.
+ virtual ~Peripheral(void) = default;
+
+ /// @brief Get peripheral type.
+ ///
+ /// @return Type defined with @ref PERIPHERAL_TYPE
+ PERIPHERAL_TYPE Type(void) const { return m_type; }
+
+ /// @brief Get peripheral name.
+ ///
+ /// @return Name string of peripheral
+ const std::string& Name(void) const { return m_strName; }
+
+ /// @brief Get peripheral vendor id.
+ ///
+ /// @return Vendor id
+ uint16_t VendorID(void) const { return m_vendorId; }
+
+ /// @brief Get peripheral product id.
+ ///
+ /// @return Product id
+ uint16_t ProductID(void) const { return m_productId; }
+
+ /// @brief Get peripheral index identifier.
+ ///
+ /// @return Index number
+ unsigned int Index(void) const { return m_index; }
+
+ /// @brief Check VID and PID are known.
+ ///
+ /// @return true if VID and PID are not 0
+ ///
+ /// @note Derived property: VID and PID are `0x0000` if unknown
+ bool IsVidPidKnown(void) const { return m_vendorId != 0 || m_productId != 0; }
+
+ /// @brief Set peripheral type.
+ ///
+ /// @param[in] type Type to set
+ void SetType(PERIPHERAL_TYPE type) { m_type = type; }
+
+ /// @brief Set peripheral name.
+ ///
+ /// @param[in] strName Name to set
+ void SetName(const std::string& strName) { m_strName = strName; }
+
+ /// @brief Set peripheral vendor id.
+ ///
+ /// @param[in] vendorId Type to set
+ void SetVendorID(uint16_t vendorId) { m_vendorId = vendorId; }
+
+ /// @brief Set peripheral product identifier.
+ ///
+ /// @param[in] productId Type to set
+ void SetProductID(uint16_t productId) { m_productId = productId; }
+
+ /// @brief Set peripheral index.
+ ///
+ /// @param[in] index Type to set
+ void SetIndex(unsigned int index) { m_index = index; }
+
+ ///@}
+
+ explicit Peripheral(const PERIPHERAL_INFO& info)
+ : m_type(info.type),
+ m_strName(info.name ? info.name : ""),
+ m_vendorId(info.vendor_id),
+ m_productId(info.product_id),
+ m_index(info.index)
+ {
+ }
+
+ void ToStruct(PERIPHERAL_INFO& info) const
+ {
+ info.type = m_type;
+ info.name = new char[m_strName.size() + 1];
+ info.vendor_id = m_vendorId;
+ info.product_id = m_productId;
+ info.index = m_index;
+
+ std::strcpy(info.name, m_strName.c_str());
+ }
+
+ static void FreeStruct(PERIPHERAL_INFO& info) { PERIPHERAL_SAFE_DELETE_ARRAY(info.name); }
+
+private:
+ PERIPHERAL_TYPE m_type;
+ std::string m_strName;
+ uint16_t m_vendorId = 0;
+ uint16_t m_productId = 0;
+ unsigned int m_index = 0;
+};
+///@}
+//------------------------------------------------------------------------------
+
+typedef PeripheralVector<Peripheral, PERIPHERAL_INFO> Peripherals;
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_peripheral_Defs_Peripheral_PeripheralEvent class PeripheralEvent
+/// @ingroup cpp_kodi_addon_peripheral_Defs_Peripheral
+/// @brief **Wrapper class for %peripheral events**\n
+/// To handle data of change events between add-on and Kodi.
+///
+/// Used on @ref kodi::addon::CInstancePeripheral::GetEvents() and
+/// @ref kodi::addon::CInstancePeripheral::SendEvent().
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_peripheral_Defs_Peripheral_PeripheralEvent_Help
+///
+///@{
+class PeripheralEvent
+{
+public:
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Peripheral_PeripheralEvent_Help Value Help
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Peripheral_PeripheralEvent
+ /// ----------------------------------------------------------------------------
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_peripheral_Defs_Peripheral_PeripheralEvent :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **%Peripheral event type** | @ref PERIPHERAL_EVENT_TYPE | @ref PeripheralEvent::SetType "SetType" | @ref PeripheralEvent::Type "Type"
+ /// | **%Peripheral index** | `unsigned int` | @ref PeripheralEvent::SetPeripheralIndex "SetPeripheralIndex" | @ref PeripheralEvent::PeripheralIndex "PeripheralIndex"
+ /// | **%Peripheral event driver index** | `unsigned int` | @ref PeripheralEvent::SetDriverIndex "SetDriverIndex" | @ref PeripheralEvent::DriverIndex "DriverIndex"
+ /// | **%Peripheral event button state** | @ref JOYSTICK_STATE_BUTTON | @ref PeripheralEvent::SetButtonState "SetButtonState" | @ref PeripheralEvent::ButtonState "ButtonState"
+ /// | **%Peripheral event hat state** | @ref JOYSTICK_STATE_HAT | @ref PeripheralEvent::SetHatState "SetHatState" | @ref PeripheralEvent::HatState "HatState"
+ /// | **%Peripheral event axis state** | @ref JOYSTICK_STATE_AXIS (`float`) | @ref PeripheralEvent::SetAxisState "SetAxisState" | @ref PeripheralEvent::AxisState "AxisState"
+ /// | **%Peripheral event motor state** | @ref JOYSTICK_STATE_MOTOR (`float`) | @ref PeripheralEvent::SetMotorState "SetMotorState" | @ref PeripheralEvent::MotorState "MotorState"
+ ///
+ /// Further are several class constructors with values included.
+
+ /// @addtogroup cpp_kodi_addon_peripheral_Defs_Peripheral_PeripheralEvent
+ ///@{
+
+ /// @brief Constructor.
+ PeripheralEvent() = default;
+
+ /// @brief Constructor.
+ ///
+ /// @param[in] peripheralIndex %Peripheral index
+ /// @param[in] buttonIndex Button index
+ /// @param[in] state Joystick state button
+ PeripheralEvent(unsigned int peripheralIndex,
+ unsigned int buttonIndex,
+ JOYSTICK_STATE_BUTTON state)
+ : m_type(PERIPHERAL_EVENT_TYPE_DRIVER_BUTTON),
+ m_peripheralIndex(peripheralIndex),
+ m_driverIndex(buttonIndex),
+ m_buttonState(state)
+ {
+ }
+
+ /// @brief Constructor.
+ ///
+ /// @param[in] peripheralIndex %Peripheral index
+ /// @param[in] hatIndex Hat index
+ /// @param[in] state Joystick state hat
+ PeripheralEvent(unsigned int peripheralIndex, unsigned int hatIndex, JOYSTICK_STATE_HAT state)
+ : m_type(PERIPHERAL_EVENT_TYPE_DRIVER_HAT),
+ m_peripheralIndex(peripheralIndex),
+ m_driverIndex(hatIndex),
+ m_hatState(state)
+ {
+ }
+
+ /// @brief Constructor.
+ ///
+ /// @param[in] peripheralIndex %Peripheral index
+ /// @param[in] axisIndex Axis index
+ /// @param[in] state Joystick state axis
+ PeripheralEvent(unsigned int peripheralIndex, unsigned int axisIndex, JOYSTICK_STATE_AXIS state)
+ : m_type(PERIPHERAL_EVENT_TYPE_DRIVER_AXIS),
+ m_peripheralIndex(peripheralIndex),
+ m_driverIndex(axisIndex),
+ m_axisState(state)
+ {
+ }
+
+ /// @brief Get type of event.
+ ///
+ /// @return Type defined with @ref PERIPHERAL_EVENT_TYPE
+ PERIPHERAL_EVENT_TYPE Type(void) const { return m_type; }
+
+ /// @brief Get peripheral index.
+ ///
+ /// @return %Peripheral index number
+ unsigned int PeripheralIndex(void) const { return m_peripheralIndex; }
+
+ /// @brief Get driver index.
+ ///
+ /// @return Driver index number
+ unsigned int DriverIndex(void) const { return m_driverIndex; }
+
+ /// @brief Get button state.
+ ///
+ /// @return Button state as @ref JOYSTICK_STATE_BUTTON
+ JOYSTICK_STATE_BUTTON ButtonState(void) const { return m_buttonState; }
+
+ /// @brief Get hat state.
+ ///
+ /// @return Hat state
+ JOYSTICK_STATE_HAT HatState(void) const { return m_hatState; }
+
+ /// @brief Get axis state.
+ ///
+ /// @return Axis state
+ JOYSTICK_STATE_AXIS AxisState(void) const { return m_axisState; }
+
+ /// @brief Get motor state.
+ ///
+ /// @return Motor state
+ JOYSTICK_STATE_MOTOR MotorState(void) const { return m_motorState; }
+
+ /// @brief Set type of event.
+ ///
+ /// @param[in] type Type defined with @ref PERIPHERAL_EVENT_TYPE
+ void SetType(PERIPHERAL_EVENT_TYPE type) { m_type = type; }
+
+ /// @brief Set peripheral index.
+ ///
+ /// @param[in] index %Peripheral index number
+ void SetPeripheralIndex(unsigned int index) { m_peripheralIndex = index; }
+
+ /// @brief Set driver index.
+ ///
+ /// @param[in] index Driver index number
+ void SetDriverIndex(unsigned int index) { m_driverIndex = index; }
+
+ /// @brief Set button state.
+ ///
+ /// @param[in] state Button state as @ref JOYSTICK_STATE_BUTTON
+ void SetButtonState(JOYSTICK_STATE_BUTTON state) { m_buttonState = state; }
+
+ /// @brief Set hat state.
+ ///
+ /// @param[in] state Hat state as @ref JOYSTICK_STATE_HAT (float)
+ void SetHatState(JOYSTICK_STATE_HAT state) { m_hatState = state; }
+
+ /// @brief Set axis state.
+ ///
+ /// @param[in] state Axis state as @ref JOYSTICK_STATE_AXIS (float)
+ void SetAxisState(JOYSTICK_STATE_AXIS state) { m_axisState = state; }
+
+ /// @brief Set motor state.
+ ///
+ /// @param[in] state Motor state as @ref JOYSTICK_STATE_MOTOR (float)
+ void SetMotorState(JOYSTICK_STATE_MOTOR state) { m_motorState = state; }
+
+ ///@}
+
+ explicit PeripheralEvent(const PERIPHERAL_EVENT& event)
+ : m_type(event.type),
+ m_peripheralIndex(event.peripheral_index),
+ m_driverIndex(event.driver_index),
+ m_buttonState(event.driver_button_state),
+ m_hatState(event.driver_hat_state),
+ m_axisState(event.driver_axis_state),
+ m_motorState(event.motor_state)
+ {
+ }
+
+ void ToStruct(PERIPHERAL_EVENT& event) const
+ {
+ event.type = m_type;
+ event.peripheral_index = m_peripheralIndex;
+ event.driver_index = m_driverIndex;
+ event.driver_button_state = m_buttonState;
+ event.driver_hat_state = m_hatState;
+ event.driver_axis_state = m_axisState;
+ event.motor_state = m_motorState;
+ }
+
+ static void FreeStruct(PERIPHERAL_EVENT& event) { (void)event; }
+
+private:
+ PERIPHERAL_EVENT_TYPE m_type = PERIPHERAL_EVENT_TYPE_NONE;
+ unsigned int m_peripheralIndex = 0;
+ unsigned int m_driverIndex = 0;
+ JOYSTICK_STATE_BUTTON m_buttonState = JOYSTICK_STATE_BUTTON_UNPRESSED;
+ JOYSTICK_STATE_HAT m_hatState = JOYSTICK_STATE_HAT_UNPRESSED;
+ JOYSTICK_STATE_AXIS m_axisState = 0.0f;
+ JOYSTICK_STATE_MOTOR m_motorState = 0.0f;
+};
+///@}
+//------------------------------------------------------------------------------
+
+typedef PeripheralVector<PeripheralEvent, PERIPHERAL_EVENT> PeripheralEvents;
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick_Joystick class Joystick
+/// @ingroup cpp_kodi_addon_peripheral_Defs_Joystick
+/// @brief **Wrapper class providing additional joystick information**\n
+/// This is a child class to expand another class with necessary joystick data.
+///
+/// For data not provided by @ref cpp_kodi_addon_peripheral_Defs_Peripheral_Peripheral.
+///
+/// Used on:
+/// - @ref kodi::addon::CInstancePeripheral::GetJoystickInfo()
+/// - @ref kodi::addon::CInstancePeripheral::GetFeatures().
+/// - @ref kodi::addon::CInstancePeripheral::MapFeatures().
+/// - @ref kodi::addon::CInstancePeripheral::GetIgnoredPrimitives().
+/// - @ref kodi::addon::CInstancePeripheral::SetIgnoredPrimitives().
+/// - @ref kodi::addon::CInstancePeripheral::SaveButtonMap().
+/// - @ref kodi::addon::CInstancePeripheral::RevertButtonMap().
+/// - @ref kodi::addon::CInstancePeripheral::ResetButtonMap().
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_peripheral_Defs_Joystick_Joystick_Help
+///
+///@{
+class Joystick : public Peripheral
+{
+public:
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick_Joystick_Help Value Help
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Joystick_Joystick
+ /// ----------------------------------------------------------------------------
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_peripheral_Defs_Joystick_Joystick :</b>
+ /// | Name | Type | Class | Set call | Get call
+ /// |------|------|-------|----------|----------
+ /// | **%Joystick provider** | `const std::string&` | @ref Joystick | @ref Joystick::SetProvider "SetProvider" | @ref Joystick::Provider "Provider"
+ /// | **%Joystick requested port** | `int` | @ref Joystick | @ref Joystick::SetRequestedPort "SetRequestedPort" | @ref Joystick::RequestedPort "RequestedPort"
+ /// | **%Joystick button count** | `unsigned int` | @ref Joystick | @ref Joystick::SetButtonCount "SetButtonCount" | @ref Joystick::ButtonCount "ButtonCount"
+ /// | **%Joystick hat count** | `unsigned int` | @ref Joystick | @ref Joystick::SetHatCount "SetHatCount" | @ref Joystick::HatCount "HatCount"
+ /// | **%Joystick axis count** | `unsigned int` | @ref Joystick | @ref Joystick::SetAxisCount "SetAxisCount" | @ref Joystick::AxisCount "AxisCount"
+ /// | **%Joystick motor count** | `unsigned int` | @ref Joystick | @ref Joystick::SetMotorCount "SetMotorCount" | @ref Joystick::MotorCount "MotorCount"
+ /// | **%Joystick support power off** | `bool` | @ref Joystick | @ref Joystick::SetSupportsPowerOff "SetSupportsPowerOff" | @ref Joystick::SupportsPowerOff "SupportsPowerOff"
+ /// | **%Peripheral type** | @ref PERIPHERAL_TYPE | @ref Peripheral | @ref Peripheral::SetType "SetType" | @ref Peripheral::Type "Type"
+ /// | **%Peripheral name** | `const std::string&` | @ref Peripheral | @ref Peripheral::SetName "SetName" | @ref Peripheral::Name "Name"
+ /// | **%Peripheral vendor id** | `uint16_t` | @ref Peripheral | @ref Peripheral::SetVendorID "SetVendorID" | @ref Peripheral::VendorID "VendorID"
+ /// | **%Peripheral product id** | `uint16_t` | @ref Peripheral | @ref Peripheral::SetProductID "SetProductID" | @ref Peripheral::ProductID "ProductID"
+ /// | **%Peripheral index** | `unsigned int` | @ref Peripheral | @ref Peripheral::SetIndex "SetIndex" | @ref Peripheral::Index "Index"
+ ///
+ /// Further are following included:
+ /// - @ref Joystick::Joystick "Joystick(const std::string& provider = \"\", const std::string& strName = \"\")"
+ /// - @ref Joystick::operator= "Joystick& operator=(const Joystick& rhs)"
+ /// - @ref Peripheral::IsVidPidKnown "IsVidPidKnown()": To check VID and PID are known.
+ ///
+
+ /// @addtogroup cpp_kodi_addon_peripheral_Defs_Joystick_Joystick
+ ///@{
+
+ /// @brief Constructor.
+ ///
+ /// @param[in] provider [optional] Provide name
+ /// @param[in] strName [optional] Name of related joystick
+ Joystick(const std::string& provider = "", const std::string& strName = "")
+ : Peripheral(PERIPHERAL_TYPE_JOYSTICK, strName),
+ m_provider(provider),
+ m_requestedPort(NO_PORT_REQUESTED)
+ {
+ }
+
+ /// @brief Class copy constructor.
+ ///
+ /// @param[in] other Other class to copy on construct here
+ Joystick(const Joystick& other) : Peripheral(other) { *this = other; }
+
+ /// @brief Destructor.
+ ///
+ ~Joystick(void) override = default;
+
+ /// @brief Copy data from another @ref Joystick class to here.
+ ///
+ /// @param[in] other Other class to copy here
+ Joystick& operator=(const Joystick& rhs)
+ {
+ if (this != &rhs)
+ {
+ Peripheral::operator=(rhs);
+
+ m_provider = rhs.m_provider;
+ m_requestedPort = rhs.m_requestedPort;
+ m_buttonCount = rhs.m_buttonCount;
+ m_hatCount = rhs.m_hatCount;
+ m_axisCount = rhs.m_axisCount;
+ m_motorCount = rhs.m_motorCount;
+ m_supportsPowerOff = rhs.m_supportsPowerOff;
+ }
+ return *this;
+ }
+
+ /// @brief Get provider name.
+ ///
+ /// @return Name of provider
+ const std::string& Provider(void) const { return m_provider; }
+
+ /// @brief Get requested port number.
+ ///
+ /// @return Port
+ int RequestedPort(void) const { return m_requestedPort; }
+
+ /// @brief Get button count.
+ ///
+ /// @return Button count
+ unsigned int ButtonCount(void) const { return m_buttonCount; }
+
+ /// @brief Get hat count.
+ ///
+ /// @return Hat count
+ unsigned int HatCount(void) const { return m_hatCount; }
+
+ /// @brief Get axis count.
+ ///
+ /// @return Axis count
+ unsigned int AxisCount(void) const { return m_axisCount; }
+
+ /// @brief Get motor count.
+ ///
+ /// @return Motor count
+ unsigned int MotorCount(void) const { return m_motorCount; }
+
+ /// @brief Get supports power off.
+ ///
+ /// @return True if power off is supported, false otherwise
+ bool SupportsPowerOff(void) const { return m_supportsPowerOff; }
+
+ /// @brief Set provider name.
+ ///
+ /// @param[in] provider Name of provider
+ void SetProvider(const std::string& provider) { m_provider = provider; }
+
+ /// @brief Get requested port number.
+ ///
+ /// @param[in] requestedPort Port
+ void SetRequestedPort(int requestedPort) { m_requestedPort = requestedPort; }
+
+ /// @brief Get button count.
+ ///
+ /// @param[in] buttonCount Button count
+ void SetButtonCount(unsigned int buttonCount) { m_buttonCount = buttonCount; }
+
+ /// @brief Get hat count.
+ ///
+ /// @param[in] hatCount Hat count
+ void SetHatCount(unsigned int hatCount) { m_hatCount = hatCount; }
+
+ /// @brief Get axis count.
+ ///
+ /// @param[in] axisCount Axis count
+ void SetAxisCount(unsigned int axisCount) { m_axisCount = axisCount; }
+
+ /// @brief Get motor count.
+ ///
+ /// @param[in] motorCount Motor count
+ void SetMotorCount(unsigned int motorCount) { m_motorCount = motorCount; }
+
+ /// @brief Get supports power off.
+ ///
+ /// @param[in] supportsPowerOff True if power off is supported, false otherwise
+ void SetSupportsPowerOff(bool supportsPowerOff) { m_supportsPowerOff = supportsPowerOff; }
+
+ ///@}
+
+ explicit Joystick(const JOYSTICK_INFO& info)
+ : Peripheral(info.peripheral),
+ m_provider(info.provider ? info.provider : ""),
+ m_requestedPort(info.requested_port),
+ m_buttonCount(info.button_count),
+ m_hatCount(info.hat_count),
+ m_axisCount(info.axis_count),
+ m_motorCount(info.motor_count),
+ m_supportsPowerOff(info.supports_poweroff)
+ {
+ }
+
+ void ToStruct(JOYSTICK_INFO& info) const
+ {
+ Peripheral::ToStruct(info.peripheral);
+
+ info.provider = new char[m_provider.size() + 1];
+ info.requested_port = m_requestedPort;
+ info.button_count = m_buttonCount;
+ info.hat_count = m_hatCount;
+ info.axis_count = m_axisCount;
+ info.motor_count = m_motorCount;
+ info.supports_poweroff = m_supportsPowerOff;
+
+ std::strcpy(info.provider, m_provider.c_str());
+ }
+
+ static void FreeStruct(JOYSTICK_INFO& info)
+ {
+ Peripheral::FreeStruct(info.peripheral);
+
+ PERIPHERAL_SAFE_DELETE_ARRAY(info.provider);
+ }
+
+private:
+ std::string m_provider;
+ int m_requestedPort;
+ unsigned int m_buttonCount = 0;
+ unsigned int m_hatCount = 0;
+ unsigned int m_axisCount = 0;
+ unsigned int m_motorCount = 0;
+ bool m_supportsPowerOff = false;
+};
+///@}
+//------------------------------------------------------------------------------
+
+typedef PeripheralVector<Joystick, JOYSTICK_INFO> Joysticks;
+
+class JoystickFeature;
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick_DriverPrimitive class DriverPrimitive
+/// @ingroup cpp_kodi_addon_peripheral_Defs_Joystick
+/// @brief **Base class for joystick driver primitives**
+///
+/// A driver primitive can be:
+///
+/// 1. a button
+/// 2. a hat direction
+/// 3. a semiaxis (either the positive or negative half of an axis)
+/// 4. a motor
+/// 5. a keyboard key
+/// 6. a mouse button
+/// 7. a relative pointer direction
+///
+/// The type determines the fields in use:
+///
+/// Button:
+/// - driver index
+///
+/// Hat direction:
+/// - driver index
+/// - hat direction
+///
+/// Semiaxis:
+/// - driver index
+/// - center
+/// - semiaxis direction
+/// - range
+///
+/// Motor:
+/// - driver index
+///
+/// Key:
+/// - key code
+///
+/// Mouse button:
+/// - driver index
+///
+/// Relative pointer direction:
+/// - relative pointer direction
+///
+///@{
+struct DriverPrimitive
+{
+protected:
+ /*!
+ * @brief Construct a driver primitive of the specified type
+ */
+ DriverPrimitive(JOYSTICK_DRIVER_PRIMITIVE_TYPE type, unsigned int driverIndex)
+ : m_type(type), m_driverIndex(driverIndex)
+ {
+ }
+
+public:
+ /// @addtogroup cpp_kodi_addon_peripheral_Defs_Joystick_DriverPrimitive
+ ///@{
+
+ /// @brief Construct an invalid driver primitive.
+ DriverPrimitive(void) = default;
+
+ /// @brief Construct a driver primitive representing a joystick button.
+ ///
+ /// @param[in] buttonIndex Index
+ /// @return Created class
+ static DriverPrimitive CreateButton(unsigned int buttonIndex)
+ {
+ return DriverPrimitive(JOYSTICK_DRIVER_PRIMITIVE_TYPE_BUTTON, buttonIndex);
+ }
+
+ /// @brief Construct a driver primitive representing one of the four direction
+ /// arrows on a dpad.
+ ///
+ /// @param[in] hatIndex Hat index
+ /// @param[in] direction With @ref JOYSTICK_DRIVER_HAT_DIRECTION defined direction
+ DriverPrimitive(unsigned int hatIndex, JOYSTICK_DRIVER_HAT_DIRECTION direction)
+ : m_type(JOYSTICK_DRIVER_PRIMITIVE_TYPE_HAT_DIRECTION),
+ m_driverIndex(hatIndex),
+ m_hatDirection(direction)
+ {
+ }
+
+ /// @brief Construct a driver primitive representing the positive or negative
+ /// half of an axis.
+ ///
+ /// @param[in] axisIndex Axis index
+ /// @param[in] center Center
+ /// @param[in] direction With @ref JOYSTICK_DRIVER_HAT_DIRECTION defined direction
+ /// @param[in] range Range
+ DriverPrimitive(unsigned int axisIndex,
+ int center,
+ JOYSTICK_DRIVER_SEMIAXIS_DIRECTION direction,
+ unsigned int range)
+ : m_type(JOYSTICK_DRIVER_PRIMITIVE_TYPE_SEMIAXIS),
+ m_driverIndex(axisIndex),
+ m_center(center),
+ m_semiAxisDirection(direction),
+ m_range(range)
+ {
+ }
+
+ /// @brief Construct a driver primitive representing a motor.
+ ///
+ /// @param[in] motorIndex Motor index number
+ /// @return Constructed driver primitive representing a motor
+ static DriverPrimitive CreateMotor(unsigned int motorIndex)
+ {
+ return DriverPrimitive(JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOTOR, motorIndex);
+ }
+
+ /// @brief Construct a driver primitive representing a key on a keyboard.
+ ///
+ /// @param[in] keycode Keycode to use
+ DriverPrimitive(std::string keycode)
+ : m_type(JOYSTICK_DRIVER_PRIMITIVE_TYPE_KEY), m_keycode(std::move(keycode))
+ {
+ }
+
+ /// @brief Construct a driver primitive representing a mouse button.
+ ///
+ /// @param[in] buttonIndex Index
+ /// @return Constructed driver primitive representing a mouse button
+ static DriverPrimitive CreateMouseButton(JOYSTICK_DRIVER_MOUSE_INDEX buttonIndex)
+ {
+ return DriverPrimitive(JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOUSE_BUTTON,
+ static_cast<unsigned int>(buttonIndex));
+ }
+
+ /// @brief Construct a driver primitive representing one of the four
+ /// direction in which a relative pointer can move
+ ///
+ /// @param[in] direction With @ref JOYSTICK_DRIVER_RELPOINTER_DIRECTION defined direction
+ DriverPrimitive(JOYSTICK_DRIVER_RELPOINTER_DIRECTION direction)
+ : m_type(JOYSTICK_DRIVER_PRIMITIVE_TYPE_RELPOINTER_DIRECTION), m_relPointerDirection(direction)
+ {
+ }
+
+ /// @brief Get type of primitive.
+ ///
+ /// @return The with @ref JOYSTICK_DRIVER_PRIMITIVE_TYPE defined type
+ JOYSTICK_DRIVER_PRIMITIVE_TYPE Type(void) const { return m_type; }
+
+ /// @brief Get driver index.
+ ///
+ /// @return Index number
+ unsigned int DriverIndex(void) const { return m_driverIndex; }
+
+ /// @brief Get hat direction
+ ///
+ /// @return The with @ref JOYSTICK_DRIVER_HAT_DIRECTION defined direction
+ JOYSTICK_DRIVER_HAT_DIRECTION HatDirection(void) const { return m_hatDirection; }
+
+ /// @brief Get center
+ ///
+ /// @return Center
+ int Center(void) const { return m_center; }
+
+ /// @brief Get semi axis direction
+ ///
+ /// @return With @ref JOYSTICK_DRIVER_SEMIAXIS_DIRECTION defined direction
+ JOYSTICK_DRIVER_SEMIAXIS_DIRECTION SemiAxisDirection(void) const { return m_semiAxisDirection; }
+
+ /// @brief Get range.
+ ///
+ /// @return Range
+ unsigned int Range(void) const { return m_range; }
+
+ /// @brief Get key code as string.
+ ///
+ /// @return Key code
+ const std::string& Keycode(void) const { return m_keycode; }
+
+ /// @brief Get mouse index
+ ///
+ /// @return With @ref JOYSTICK_DRIVER_MOUSE_INDEX defined mouse index
+ JOYSTICK_DRIVER_MOUSE_INDEX MouseIndex(void) const
+ {
+ return static_cast<JOYSTICK_DRIVER_MOUSE_INDEX>(m_driverIndex);
+ }
+
+ /// @brief Get relative pointer direction.
+ ///
+ /// @return With @ref JOYSTICK_DRIVER_RELPOINTER_DIRECTION defined direction
+ JOYSTICK_DRIVER_RELPOINTER_DIRECTION RelPointerDirection(void) const
+ {
+ return m_relPointerDirection;
+ }
+
+ /// @brief Compare this with another class of this type.
+ ///
+ /// @param[in] other Other class to compare
+ /// @return True if they are equal, false otherwise
+ bool operator==(const DriverPrimitive& other) const
+ {
+ if (m_type == other.m_type)
+ {
+ switch (m_type)
+ {
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_BUTTON:
+ {
+ return m_driverIndex == other.m_driverIndex;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_HAT_DIRECTION:
+ {
+ return m_driverIndex == other.m_driverIndex && m_hatDirection == other.m_hatDirection;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_SEMIAXIS:
+ {
+ return m_driverIndex == other.m_driverIndex && m_center == other.m_center &&
+ m_semiAxisDirection == other.m_semiAxisDirection && m_range == other.m_range;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_KEY:
+ {
+ return m_keycode == other.m_keycode;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOTOR:
+ {
+ return m_driverIndex == other.m_driverIndex;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOUSE_BUTTON:
+ {
+ return m_driverIndex == other.m_driverIndex;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_RELPOINTER_DIRECTION:
+ {
+ return m_relPointerDirection == other.m_relPointerDirection;
+ }
+ default:
+ break;
+ }
+ }
+ return false;
+ }
+
+ ///@}
+
+ explicit DriverPrimitive(const JOYSTICK_DRIVER_PRIMITIVE& primitive) : m_type(primitive.type)
+ {
+ switch (m_type)
+ {
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_BUTTON:
+ {
+ m_driverIndex = primitive.button.index;
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_HAT_DIRECTION:
+ {
+ m_driverIndex = primitive.hat.index;
+ m_hatDirection = primitive.hat.direction;
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_SEMIAXIS:
+ {
+ m_driverIndex = primitive.semiaxis.index;
+ m_center = primitive.semiaxis.center;
+ m_semiAxisDirection = primitive.semiaxis.direction;
+ m_range = primitive.semiaxis.range;
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOTOR:
+ {
+ m_driverIndex = primitive.motor.index;
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_KEY:
+ {
+ m_keycode = primitive.key.keycode;
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOUSE_BUTTON:
+ {
+ m_driverIndex = primitive.mouse.button;
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_RELPOINTER_DIRECTION:
+ {
+ m_relPointerDirection = primitive.relpointer.direction;
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ void ToStruct(JOYSTICK_DRIVER_PRIMITIVE& driver_primitive) const
+ {
+ driver_primitive.type = m_type;
+ switch (m_type)
+ {
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_BUTTON:
+ {
+ driver_primitive.button.index = m_driverIndex;
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_HAT_DIRECTION:
+ {
+ driver_primitive.hat.index = m_driverIndex;
+ driver_primitive.hat.direction = m_hatDirection;
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_SEMIAXIS:
+ {
+ driver_primitive.semiaxis.index = m_driverIndex;
+ driver_primitive.semiaxis.center = m_center;
+ driver_primitive.semiaxis.direction = m_semiAxisDirection;
+ driver_primitive.semiaxis.range = m_range;
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOTOR:
+ {
+ driver_primitive.motor.index = m_driverIndex;
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_KEY:
+ {
+ const size_t size = sizeof(driver_primitive.key.keycode);
+ std::strncpy(driver_primitive.key.keycode, m_keycode.c_str(), size - 1);
+ driver_primitive.key.keycode[size - 1] = '\0';
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOUSE_BUTTON:
+ {
+ driver_primitive.mouse.button = static_cast<JOYSTICK_DRIVER_MOUSE_INDEX>(m_driverIndex);
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_RELPOINTER_DIRECTION:
+ {
+ driver_primitive.relpointer.direction = m_relPointerDirection;
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ static void FreeStruct(JOYSTICK_DRIVER_PRIMITIVE& primitive) { (void)primitive; }
+
+private:
+ JOYSTICK_DRIVER_PRIMITIVE_TYPE m_type = JOYSTICK_DRIVER_PRIMITIVE_TYPE_UNKNOWN;
+ unsigned int m_driverIndex = 0;
+ JOYSTICK_DRIVER_HAT_DIRECTION m_hatDirection = JOYSTICK_DRIVER_HAT_UNKNOWN;
+ int m_center = 0;
+ JOYSTICK_DRIVER_SEMIAXIS_DIRECTION m_semiAxisDirection = JOYSTICK_DRIVER_SEMIAXIS_UNKNOWN;
+ unsigned int m_range = 1;
+ std::string m_keycode;
+ JOYSTICK_DRIVER_RELPOINTER_DIRECTION m_relPointerDirection = JOYSTICK_DRIVER_RELPOINTER_UNKNOWN;
+};
+///@}
+//------------------------------------------------------------------------------
+
+typedef PeripheralVector<DriverPrimitive, JOYSTICK_DRIVER_PRIMITIVE> DriverPrimitives;
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick_JoystickFeature class JoystickFeature
+/// @ingroup cpp_kodi_addon_peripheral_Defs_Joystick
+/// @brief **Base class for joystick feature primitives**
+///
+/// Class for joystick features. A feature can be:
+///
+/// 1. scalar *[1]*
+/// 2. analog stick
+/// 3. accelerometer
+/// 4. motor
+/// 5. relative pointer *[2]*
+/// 6. absolute pointer
+/// 7. wheel
+/// 8. throttle
+/// 9. keyboard key
+///
+/// *[1]* All three driver primitives (buttons, hats and axes) have a state that
+/// can be represented using a single scalar value. For this reason,
+/// features that map to a single primitive are called "scalar features".
+///
+/// *[2]* Relative pointers are similar to analog sticks, but they use
+/// relative distances instead of positions.
+///
+///@{
+class JoystickFeature
+{
+public:
+ /// @addtogroup cpp_kodi_addon_peripheral_Defs_Joystick_JoystickFeature
+ ///@{
+
+ /// @brief Class constructor.
+ ///
+ /// @param[in] name [optional] Name of the feature
+ /// @param[in] type [optional] Type of the feature, @ref JOYSTICK_FEATURE_TYPE_UNKNOWN
+ /// as default
+ JoystickFeature(const std::string& name = "",
+ JOYSTICK_FEATURE_TYPE type = JOYSTICK_FEATURE_TYPE_UNKNOWN)
+ : m_name(name), m_type(type), m_primitives{}
+ {
+ }
+
+ /// @brief Class copy constructor.
+ ///
+ /// @param[in] other Other class to copy on construct here
+ JoystickFeature(const JoystickFeature& other) { *this = other; }
+
+ /// @brief Copy data from another @ref JoystickFeature class to here.
+ ///
+ /// @param[in] other Other class to copy here
+ JoystickFeature& operator=(const JoystickFeature& rhs)
+ {
+ if (this != &rhs)
+ {
+ m_name = rhs.m_name;
+ m_type = rhs.m_type;
+ m_primitives = rhs.m_primitives;
+ }
+ return *this;
+ }
+
+ /// @brief Compare this with another class of this type.
+ ///
+ /// @param[in] other Other class to compare
+ /// @return True if they are equal, false otherwise
+ bool operator==(const JoystickFeature& other) const
+ {
+ return m_name == other.m_name && m_type == other.m_type && m_primitives == other.m_primitives;
+ }
+
+ /// @brief Get name of feature.
+ ///
+ /// @return Name of feature
+ const std::string& Name(void) const { return m_name; }
+
+ /// @brief Get name of feature.
+ ///
+ /// @return Type of feature defined with @ref JOYSTICK_FEATURE_TYPE
+ JOYSTICK_FEATURE_TYPE Type(void) const { return m_type; }
+
+ /// @brief Check this feature is valid.
+ ///
+ /// @return True if valid (type != JOYSTICK_FEATURE_TYPE_UNKNOWN), false otherwise
+ bool IsValid() const { return m_type != JOYSTICK_FEATURE_TYPE_UNKNOWN; }
+
+ /// @brief Set name of feature.
+ ///
+ /// @param[in] name Name of feature
+ void SetName(const std::string& name) { m_name = name; }
+
+ /// @brief Set type of feature.
+ ///
+ /// @param[in] type Type of feature
+ void SetType(JOYSTICK_FEATURE_TYPE type) { m_type = type; }
+
+ /// @brief Set type as invalid.
+ void SetInvalid(void) { m_type = JOYSTICK_FEATURE_TYPE_UNKNOWN; }
+
+ /// @brief Get primitive of feature by wanted type.
+ ///
+ /// @param[in] which Type of feature, defined with @ref JOYSTICK_FEATURE_PRIMITIVE
+ /// @return Primitive of asked type
+ const DriverPrimitive& Primitive(JOYSTICK_FEATURE_PRIMITIVE which) const
+ {
+ return m_primitives[which];
+ }
+
+ /// @brief Set primitive for feature by wanted type.
+ ///
+ /// @param[in] which Type of feature, defined with @ref JOYSTICK_FEATURE_PRIMITIVE
+ /// @param[in] primitive The with @ref DriverPrimitive defined primitive to set
+ void SetPrimitive(JOYSTICK_FEATURE_PRIMITIVE which, const DriverPrimitive& primitive)
+ {
+ m_primitives[which] = primitive;
+ }
+
+ /// @brief Get all primitives on this class.
+ ///
+ /// @return Array list of primitives
+ std::array<DriverPrimitive, JOYSTICK_PRIMITIVE_MAX>& Primitives() { return m_primitives; }
+
+ /// @brief Get all primitives on this class (as constant).
+ ///
+ /// @return Constant a´rray list of primitives
+ const std::array<DriverPrimitive, JOYSTICK_PRIMITIVE_MAX>& Primitives() const
+ {
+ return m_primitives;
+ }
+
+ ///@}
+
+ explicit JoystickFeature(const JOYSTICK_FEATURE& feature)
+ : m_name(feature.name ? feature.name : ""), m_type(feature.type)
+ {
+ for (unsigned int i = 0; i < JOYSTICK_PRIMITIVE_MAX; i++)
+ m_primitives[i] = DriverPrimitive(feature.primitives[i]);
+ }
+
+ void ToStruct(JOYSTICK_FEATURE& feature) const
+ {
+ feature.name = new char[m_name.length() + 1];
+ feature.type = m_type;
+ for (unsigned int i = 0; i < JOYSTICK_PRIMITIVE_MAX; i++)
+ m_primitives[i].ToStruct(feature.primitives[i]);
+
+ std::strcpy(feature.name, m_name.c_str());
+ }
+
+ static void FreeStruct(JOYSTICK_FEATURE& feature) { PERIPHERAL_SAFE_DELETE_ARRAY(feature.name); }
+
+private:
+ std::string m_name;
+ JOYSTICK_FEATURE_TYPE m_type;
+ std::array<DriverPrimitive, JOYSTICK_PRIMITIVE_MAX> m_primitives;
+};
+///@}
+//------------------------------------------------------------------------------
+
+typedef PeripheralVector<JoystickFeature, JOYSTICK_FEATURE> JoystickFeatures;
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/CMakeLists.txt
new file mode 100644
index 0000000..1690c39
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/CMakeLists.txt
@@ -0,0 +1,19 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ ChannelGroups.h
+ Channels.h
+ EDL.h
+ EPG.h
+ General.h
+ MenuHook.h
+ Providers.h
+ Recordings.h
+ Stream.h
+ Timers.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_addon-instance_pvr)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/ChannelGroups.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/ChannelGroups.h
new file mode 100644
index 0000000..37340bf
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/ChannelGroups.h
@@ -0,0 +1,271 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/addon-instance/pvr.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Definitions group 3 - PVR channel group
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroup class PVRChannelGroup
+/// @ingroup cpp_kodi_addon_pvr_Defs_ChannelGroup
+/// @brief **PVR add-on channel group**\n
+/// To define a group for channels, this becomes be asked from
+/// @ref kodi::addon::CInstancePVRClient::GetChannelGroups() and used on
+/// @ref kodi::addon::CInstancePVRClient::GetChannelGroupMembers() to get his
+/// content with @ref cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMember "PVRChannelGroupMember".
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroup_Help
+///
+///@{
+class PVRChannelGroup : public CStructHdl<PVRChannelGroup, PVR_CHANNEL_GROUP>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRChannelGroup() { memset(m_cStructure, 0, sizeof(PVR_CHANNEL_GROUP)); }
+ PVRChannelGroup(const PVRChannelGroup& channel) : CStructHdl(channel) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroup_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroup
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroup :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |------|------|----------|----------|-----------
+ /// | **Group name** | `std::string` | @ref PVRChannelGroup::SetGroupName "SetGroupName" | @ref PVRChannelGroup::GetGroupName "GetGroupName" | *required to set*
+ /// | **Is radio** | `bool` | @ref PVRChannelGroup::SetIsRadio "SetIsRadio" | @ref PVRChannelGroup::GetIsRadio "GetIsRadio" | *required to set*
+ /// | **Position** | `unsigned int` | @ref PVRChannelGroup::SetPosition "SetPosition" | @ref PVRChannelGroup::GetPosition "GetPosition" | *optional*
+ ///
+
+ /// @ingroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroup
+ ///@{
+
+ /// @brief **required**\n
+ /// Name of this channel group.
+ void SetGroupName(const std::string& groupName)
+ {
+ strncpy(m_cStructure->strGroupName, groupName.c_str(), sizeof(m_cStructure->strGroupName) - 1);
+ }
+
+ /// @brief To get with @ref SetGroupName changed values.
+ std::string GetGroupName() const { return m_cStructure->strGroupName; }
+
+ /// @brief **required**\n
+ /// **true** If this is a radio channel group, **false** otherwise.
+ void SetIsRadio(bool isRadio) { m_cStructure->bIsRadio = isRadio; }
+
+ /// @brief To get with @ref SetIsRadio changed values.
+ bool GetIsRadio() const { return m_cStructure->bIsRadio; }
+
+ /// @brief **optional**\n
+ /// Sort position of the group (<b>`0`</b> indicates that the backend doesn't
+ /// support sorting of groups).
+ void SetPosition(unsigned int position) { m_cStructure->iPosition = position; }
+
+ /// @brief To get with @ref SetPosition changed values.
+ unsigned int GetPosition() const { return m_cStructure->iPosition; }
+
+ ///@}
+
+private:
+ PVRChannelGroup(const PVR_CHANNEL_GROUP* channel) : CStructHdl(channel) {}
+ PVRChannelGroup(PVR_CHANNEL_GROUP* channel) : CStructHdl(channel) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupsResultSet class PVRChannelGroupsResultSet
+/// @ingroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroup
+/// @brief **PVR add-on channel group member transfer class**\n
+/// To transfer the content of @ref kodi::addon::CInstancePVRClient::GetChannelGroups().
+///
+///@{
+class PVRChannelGroupsResultSet
+{
+public:
+ /*! \cond PRIVATE */
+ PVRChannelGroupsResultSet() = delete;
+ PVRChannelGroupsResultSet(const AddonInstance_PVR* instance, PVR_HANDLE handle)
+ : m_instance(instance), m_handle(handle)
+ {
+ }
+ /*! \endcond */
+
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupsResultSet
+ ///@{
+
+ /// @brief To add and give content from addon to Kodi on related call.
+ ///
+ /// @param[in] tag The to transferred data.
+ void Add(const kodi::addon::PVRChannelGroup& tag)
+ {
+ m_instance->toKodi->TransferChannelGroup(m_instance->toKodi->kodiInstance, m_handle, tag);
+ }
+
+ ///@}
+
+private:
+ const AddonInstance_PVR* m_instance = nullptr;
+ const PVR_HANDLE m_handle;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMember class PVRChannelGroupMember
+/// @ingroup cpp_kodi_addon_pvr_Defs_ChannelGroup
+/// @brief **PVR add-on channel group member**\n
+/// To define the content of @ref kodi::addon::CInstancePVRClient::GetChannelGroups()
+/// given groups.
+///
+/// This content becomes then requested with @ref kodi::addon::CInstancePVRClient::GetChannelGroupMembers().
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMember_Help
+///
+///@{
+class PVRChannelGroupMember : public CStructHdl<PVRChannelGroupMember, PVR_CHANNEL_GROUP_MEMBER>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRChannelGroupMember() { memset(m_cStructure, 0, sizeof(PVR_CHANNEL_GROUP_MEMBER)); }
+ PVRChannelGroupMember(const PVRChannelGroupMember& channel) : CStructHdl(channel) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMember_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMember
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMember :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |-------|-------|-----------|----------|-----------
+ /// | **Group name** | `std::string` | @ref PVRChannelGroupMember::SetGroupName "SetGroupName" | @ref PVRChannelGroupMember::GetGroupName "GetGroupName" | *required to set*
+ /// | **Channel unique id** | `unsigned int` | @ref PVRChannelGroupMember::SetChannelUniqueId "SetChannelUniqueId" | @ref PVRChannelGroupMember::GetChannelUniqueId "GetChannelUniqueId" | *required to set*
+ /// | **Channel Number** | `unsigned int` | @ref PVRChannelGroupMember::SetChannelNumber "SetChannelNumber" | @ref PVRChannelGroupMember::GetChannelNumber "GetChannelNumber" | *optional*
+ /// | **Sub channel number** | `unsigned int` | @ref PVRChannelGroupMember::SetSubChannelNumber "SetSubChannelNumber"| @ref PVRChannelGroupMember::GetSubChannelNumber "GetSubChannelNumber" | *optional*
+ /// | **Order** | `int` | @ref PVRChannel::SetOrder "SetOrder" | @ref PVRChannel::GetOrder "GetOrder" | *optional*
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMember
+ ///@{
+
+ /// @brief **required**\n
+ /// Name of the channel group to add the channel to.
+ void SetGroupName(const std::string& groupName)
+ {
+ strncpy(m_cStructure->strGroupName, groupName.c_str(), sizeof(m_cStructure->strGroupName) - 1);
+ }
+
+ /// @brief To get with @ref SetGroupName changed values.
+ std::string GetGroupName() const { return m_cStructure->strGroupName; }
+
+ /// @brief **required**\n
+ /// Unique id of the member.
+ void SetChannelUniqueId(unsigned int channelUniqueId)
+ {
+ m_cStructure->iChannelUniqueId = channelUniqueId;
+ }
+
+ /// @brief To get with @ref SetChannelUniqueId changed values.
+ unsigned int GetChannelUniqueId() const { return m_cStructure->iChannelUniqueId; }
+
+ /// @brief **optional**\n
+ /// Channel number within the group.
+ void SetChannelNumber(unsigned int channelNumber)
+ {
+ m_cStructure->iChannelNumber = channelNumber;
+ }
+
+ /// @brief To get with @ref SetChannelNumber changed values.
+ unsigned int GetChannelNumber() const { return m_cStructure->iChannelNumber; }
+
+ /// @brief **optional**\n
+ /// Sub channel number within the group (ATSC).
+ void SetSubChannelNumber(unsigned int subChannelNumber)
+ {
+ m_cStructure->iSubChannelNumber = subChannelNumber;
+ }
+
+ /// @brief To get with @ref SetSubChannelNumber changed values.
+ unsigned int GetSubChannelNumber() const { return m_cStructure->iSubChannelNumber; }
+
+ /// @brief **optional**\n
+ /// The value denoting the order of this channel in the <b>'All channels'</b> group.
+ void SetOrder(bool order) { m_cStructure->iOrder = order; }
+
+ /// @brief To get with @ref SetOrder changed values.
+ bool GetOrder() const { return m_cStructure->iOrder; }
+
+ ///@}
+
+private:
+ PVRChannelGroupMember(const PVR_CHANNEL_GROUP_MEMBER* channel) : CStructHdl(channel) {}
+ PVRChannelGroupMember(PVR_CHANNEL_GROUP_MEMBER* channel) : CStructHdl(channel) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMembersResultSet class PVRChannelGroupMembersResultSet
+/// @ingroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMember
+/// @brief **PVR add-on channel group member transfer class**\n
+/// To transfer the content of @ref kodi::addon::CInstancePVRClient::GetChannelGroupMembers().
+///
+///@{
+class PVRChannelGroupMembersResultSet
+{
+public:
+ /*! \cond PRIVATE */
+ PVRChannelGroupMembersResultSet() = delete;
+ PVRChannelGroupMembersResultSet(const AddonInstance_PVR* instance, PVR_HANDLE handle)
+ : m_instance(instance), m_handle(handle)
+ {
+ }
+ /*! \endcond */
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_ChannelGroup_PVRChannelGroupMembersResultSet
+ ///@{
+
+ /// @brief To add and give content from addon to Kodi on related call.
+ ///
+ /// @param[in] tag The to transferred data.
+ void Add(const kodi::addon::PVRChannelGroupMember& tag)
+ {
+ m_instance->toKodi->TransferChannelGroupMember(m_instance->toKodi->kodiInstance, m_handle, tag);
+ }
+
+ ///@}
+
+private:
+ const AddonInstance_PVR* m_instance = nullptr;
+ const PVR_HANDLE m_handle;
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Channels.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Channels.h
new file mode 100644
index 0000000..d59803d
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Channels.h
@@ -0,0 +1,535 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/addon-instance/pvr.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Definitions group 2 - PVR channel
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Channel_PVRChannel class PVRChannel
+/// @ingroup cpp_kodi_addon_pvr_Defs_Channel
+/// @brief **Channel data structure**\n
+/// Representation of a TV or radio channel.
+///
+/// This is used to store all the necessary TV or radio channel data and can
+/// either provide the necessary data from / to Kodi for the associated
+/// functions or can also be used in the addon to store its data.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_Channel_PVRChannel_Help
+///
+///@{
+class PVRChannel : public CStructHdl<PVRChannel, PVR_CHANNEL>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRChannel()
+ {
+ memset(m_cStructure, 0, sizeof(PVR_CHANNEL));
+ m_cStructure->iClientProviderUid = PVR_PROVIDER_INVALID_UID;
+ }
+ PVRChannel(const PVRChannel& channel) : CStructHdl(channel) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Channel_PVRChannel_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Channel_PVRChannel
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_Channel_PVRChannel :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |------|------|----------|----------|-----------
+ /// | **Unique id** | `unsigned int` | @ref PVRChannel::SetUniqueId "SetUniqueId" | @ref PVRChannel::GetUniqueId "GetUniqueId" | *required to set*
+ /// | **Is radio** | `bool` | @ref PVRChannel::SetIsRadio "SetIsRadio" | @ref PVRChannel::GetIsRadio "GetIsRadio" | *required to set*
+ /// | **Channel number** | `unsigned int` | @ref PVRChannel::SetChannelNumber "SetChannelNumber" | @ref PVRChannel::GetChannelNumber "GetChannelNumber" | *optional*
+ /// | **Sub channel number** | `unsigned int` | @ref PVRChannel::SetSubChannelNumber "SetSubChannelNumber" | @ref PVRChannel::GetSubChannelNumber "GetSubChannelNumber" | *optional*
+ /// | **Channel name** | `std::string` | @ref PVRChannel::SetChannelName "SetChannelName" | @ref PVRChannel::GetChannelName "GetChannelName" | *optional*
+ /// | **Mime type** | `std::string` | @ref PVRChannel::SetMimeType "SetMimeType" | @ref PVRChannel::GetMimeType "GetMimeType" | *optional*
+ /// | **Encryption system** | `unsigned int` | @ref PVRChannel::SetEncryptionSystem "SetEncryptionSystem" | @ref PVRChannel::GetEncryptionSystem "GetEncryptionSystem" | *optional*
+ /// | **Icon path** | `std::string` | @ref PVRChannel::SetIconPath "SetIconPath" | @ref PVRChannel::GetIconPath "GetIconPath" | *optional*
+ /// | **Is hidden** | `bool` | @ref PVRChannel::SetIsHidden "SetIsHidden" | @ref PVRChannel::GetIsHidden "GetIsHidden" | *optional*
+ /// | **Has archive** | `bool` | @ref PVRChannel::SetHasArchive "SetHasArchive" | @ref PVRChannel::GetHasArchive "GetHasArchive" | *optional*
+ /// | **Order** | `int` | @ref PVRChannel::SetOrder "SetOrder" | @ref PVRChannel::GetOrder "GetOrder" | *optional*
+ /// | **Client provider unique identifier** | `int` | @ref PVRChannel::SetClientProviderUid "SetClientProviderUid" | @ref PVRTimer::GetClientProviderUid "GetClientProviderUid" | *optional*
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Channel_PVRChannel
+ ///@{
+
+ /// @brief **required**\n
+ /// Unique identifier for this channel.
+ void SetUniqueId(unsigned int uniqueId) { m_cStructure->iUniqueId = uniqueId; }
+
+ /// @brief To get with @ref SetUniqueId changed values.
+ unsigned int GetUniqueId() const { return m_cStructure->iUniqueId; }
+
+ /// @brief **required**\n
+ /// **true** if this is a radio channel, **false** if it's a TV channel.
+ void SetIsRadio(bool isRadio) { m_cStructure->bIsRadio = isRadio; }
+
+ /// @brief To get with @ref SetIsRadio changed values.
+ bool GetIsRadio() const { return m_cStructure->bIsRadio; }
+
+ /// @brief **optional**\n
+ /// Channel number of this channel on the backend.
+ void SetChannelNumber(unsigned int channelNumber)
+ {
+ m_cStructure->iChannelNumber = channelNumber;
+ }
+
+ /// @brief To get with @ref SetChannelNumber changed values.
+ unsigned int GetChannelNumber() const { return m_cStructure->iChannelNumber; }
+
+ /// @brief **optional**\n
+ /// Sub channel number of this channel on the backend (ATSC).
+ void SetSubChannelNumber(unsigned int subChannelNumber)
+ {
+ m_cStructure->iSubChannelNumber = subChannelNumber;
+ }
+
+ /// @brief To get with @ref SetSubChannelNumber changed values.
+ unsigned int GetSubChannelNumber() const { return m_cStructure->iSubChannelNumber; }
+
+ /// @brief **optional**\n
+ /// Channel name given to this channel.
+ void SetChannelName(const std::string& channelName)
+ {
+ strncpy(m_cStructure->strChannelName, channelName.c_str(),
+ sizeof(m_cStructure->strChannelName) - 1);
+ }
+
+ /// @brief To get with @ref SetChannelName changed values.
+ std::string GetChannelName() const { return m_cStructure->strChannelName; }
+
+ /// @brief **optional**\n
+ /// Input format mime type.
+ ///
+ /// Available types can be found in https://www.iana.org/assignments/media-types/media-types.xhtml
+ /// on "application" and "video" or leave empty if unknown.
+ ///
+ void SetMimeType(const std::string& inputFormat)
+ {
+ strncpy(m_cStructure->strMimeType, inputFormat.c_str(), sizeof(m_cStructure->strMimeType) - 1);
+ }
+
+ /// @brief To get with @ref SetMimeType changed values.
+ std::string GetMimeType() const { return m_cStructure->strMimeType; }
+
+ /// @brief **optional**\n
+ /// The encryption ID or CaID of this channel (Conditional access systems).
+ ///
+ /// Lists about available ID's:
+ /// - http://www.dvb.org/index.php?id=174
+ /// - http://en.wikipedia.org/wiki/Conditional_access_system
+ ///
+ void SetEncryptionSystem(unsigned int encryptionSystem)
+ {
+ m_cStructure->iEncryptionSystem = encryptionSystem;
+ }
+
+ /// @brief To get with @ref SetEncryptionSystem changed values.
+ unsigned int GetEncryptionSystem() const { return m_cStructure->iEncryptionSystem; }
+
+ /// @brief **optional**\n
+ /// Path to the channel icon (if present).
+ void SetIconPath(const std::string& iconPath)
+ {
+ strncpy(m_cStructure->strIconPath, iconPath.c_str(), sizeof(m_cStructure->strIconPath) - 1);
+ }
+
+ /// @brief To get with @ref SetIconPath changed values.
+ std::string GetIconPath() const { return m_cStructure->strIconPath; }
+
+ /// @brief **optional**\n
+ /// **true** if this channel is marked as hidden.
+ void SetIsHidden(bool isHidden) { m_cStructure->bIsHidden = isHidden; }
+
+ /// @brief To get with @ref GetIsRadio changed values.
+ bool GetIsHidden() const { return m_cStructure->bIsHidden; }
+
+ /// @brief **optional**\n
+ /// **true** if this channel has a server-side back buffer.
+ void SetHasArchive(bool hasArchive) { m_cStructure->bHasArchive = hasArchive; }
+
+ /// @brief To get with @ref GetIsRadio changed values.
+ bool GetHasArchive() const { return m_cStructure->bHasArchive; }
+
+ /// @brief **optional**\n
+ /// The value denoting the order of this channel in the 'All channels' group.
+ void SetOrder(bool order) { m_cStructure->iOrder = order; }
+
+ /// @brief To get with @ref SetOrder changed values.
+ bool GetOrder() const { return m_cStructure->iOrder; }
+ ///@}
+
+ /// @brief **optional**\n
+ /// Unique identifier of the provider this channel belongs to.
+ ///
+ /// @ref PVR_PROVIDER_INVALID_UID denotes that provider uid is not available.
+ void SetClientProviderUid(int iClientProviderUid)
+ {
+ m_cStructure->iClientProviderUid = iClientProviderUid;
+ }
+
+ /// @brief To get with @ref SetClientProviderUid changed values
+ int GetClientProviderUid() const { return m_cStructure->iClientProviderUid; }
+
+private:
+ PVRChannel(const PVR_CHANNEL* channel) : CStructHdl(channel) {}
+ PVRChannel(PVR_CHANNEL* channel) : CStructHdl(channel) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Channel_PVRChannelsResultSet class PVRChannelsResultSet
+/// @ingroup cpp_kodi_addon_pvr_Defs_Channel_PVRChannel
+/// @brief **PVR add-on channel transfer class**\n
+/// To transfer the content of @ref kodi::addon::CInstancePVRClient::GetChannels().
+///
+///@{
+class PVRChannelsResultSet
+{
+public:
+ /*! \cond PRIVATE */
+ PVRChannelsResultSet() = delete;
+ PVRChannelsResultSet(const AddonInstance_PVR* instance, PVR_HANDLE handle)
+ : m_instance(instance), m_handle(handle)
+ {
+ }
+ /*! \endcond */
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Channel_PVRChannelsResultSet
+ ///@{
+
+ /// @brief To add and give content from addon to Kodi on related call.
+ ///
+ /// @param[in] tag The to transferred data.
+ void Add(const kodi::addon::PVRChannel& tag)
+ {
+ m_instance->toKodi->TransferChannelEntry(m_instance->toKodi->kodiInstance, m_handle, tag);
+ }
+
+ ///@}
+
+private:
+ const AddonInstance_PVR* m_instance = nullptr;
+ const PVR_HANDLE m_handle;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Channel_PVRSignalStatus class PVRSignalStatus
+/// @ingroup cpp_kodi_addon_pvr_Defs_Channel
+/// @brief **PVR Signal status information**\n
+/// This class gives current status information from stream to Kodi.
+///
+/// Used to get information for user by call of @ref kodi::addon::CInstancePVRClient::GetSignalStatus()
+/// to see current quality and source.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_Channel_PVRSignalStatus_Help
+///
+///@{
+class PVRSignalStatus : public CStructHdl<PVRSignalStatus, PVR_SIGNAL_STATUS>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRSignalStatus() = default;
+ PVRSignalStatus(const PVRSignalStatus& type) : CStructHdl(type) {}
+ /*! \endcond */
+
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Channel_PVRSignalStatus_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Channel_PVRSignalStatus
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_Channel_PVRSignalStatus :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |------|------|----------|----------|-----------
+ /// | **Adapter name** | `std::string` | @ref PVRSignalStatus::SetAdapterName "SetAdapterName" | @ref PVRSignalStatus::GetAdapterName "GetAdapterName" | *optional*
+ /// | **Adapter status** | `std::string` | @ref PVRSignalStatus::SetAdapterStatus "SetAdapterStatus" | @ref PVRSignalStatus::GetAdapterStatus "GetAdapterStatus" | *optional*
+ /// | **Service name** | `std::string` | @ref PVRSignalStatus::SetServiceName "SetServiceName" | @ref PVRSignalStatus::GetServiceName "GetServiceName" | *optional*
+ /// | **Provider name** | `std::string` | @ref PVRSignalStatus::SetProviderName "SetProviderName" | @ref PVRSignalStatus::GetProviderName "GetProviderName" | *optional*
+ /// | **Mux name** | `std::string` | @ref PVRSignalStatus::SetMuxName "SetMuxName" | @ref PVRSignalStatus::GetMuxName "GetMuxName" | *optional*
+ /// | **Signal/noise ratio** | `int` | @ref PVRSignalStatus::SetSNR "SetSNR" | @ref PVRSignalStatus::GetSNR "GetSNR" | *optional*
+ /// | **Signal strength** | `int` | @ref PVRSignalStatus::SetSignal "SetSignal" | @ref PVRSignalStatus::GetSignal "GetSignal" | *optional*
+ /// | **Bit error rate** | `long` | @ref PVRSignalStatus::SetBER "SetBER" | @ref PVRSignalStatus::GetBER "GetBER" | *optional*
+ /// | **Uncorrected blocks** | `long` | @ref PVRSignalStatus::SetUNC "SetUNC" | @ref PVRSignalStatus::GetUNC "GetUNC" | *optional*
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Channel_PVRSignalStatus
+ ///@{
+
+ /// @brief **optional**\n
+ /// Name of the adapter that's being used.
+ void SetAdapterName(const std::string& adapterName)
+ {
+ strncpy(m_cStructure->strAdapterName, adapterName.c_str(),
+ sizeof(m_cStructure->strAdapterName) - 1);
+ }
+
+ /// @brief To get with @ref SetAdapterName changed values.
+ std::string GetAdapterName() const { return m_cStructure->strAdapterName; }
+
+ /// @brief **optional**\n
+ /// Status of the adapter that's being used.
+ void SetAdapterStatus(const std::string& adapterStatus)
+ {
+ strncpy(m_cStructure->strAdapterStatus, adapterStatus.c_str(),
+ sizeof(m_cStructure->strAdapterStatus) - 1);
+ }
+
+ /// @brief To get with @ref SetAdapterStatus changed values.
+ std::string GetAdapterStatus() const { return m_cStructure->strAdapterStatus; }
+
+ /// @brief **optional**\n
+ /// Name of the current service.
+ void SetServiceName(const std::string& serviceName)
+ {
+ strncpy(m_cStructure->strServiceName, serviceName.c_str(),
+ sizeof(m_cStructure->strServiceName) - 1);
+ }
+
+ /// @brief To get with @ref SetServiceName changed values.
+ std::string GetServiceName() const { return m_cStructure->strServiceName; }
+
+ /// @brief **optional**\n
+ /// Name of the current service's provider.
+ void SetProviderName(const std::string& providerName)
+ {
+ strncpy(m_cStructure->strProviderName, providerName.c_str(),
+ sizeof(m_cStructure->strProviderName) - 1);
+ }
+
+ /// @brief To get with @ref SetProviderName changed values.
+ std::string GetProviderName() const { return m_cStructure->strProviderName; }
+
+ /// @brief **optional**\n
+ /// Name of the current mux.
+ void SetMuxName(const std::string& muxName)
+ {
+ strncpy(m_cStructure->strMuxName, muxName.c_str(), sizeof(m_cStructure->strMuxName) - 1);
+ }
+
+ /// @brief To get with @ref SetMuxName changed values.
+ std::string GetMuxName() const { return m_cStructure->strMuxName; }
+
+ /// @brief **optional**\n
+ /// Signal/noise ratio.
+ ///
+ /// @note 100% is 0xFFFF 65535
+ void SetSNR(int snr) { m_cStructure->iSNR = snr; }
+
+ /// @brief To get with @ref SetSNR changed values.
+ int GetSNR() const { return m_cStructure->iSNR; }
+
+ /// @brief **optional**\n
+ /// Signal strength.
+ ///
+ /// @note 100% is 0xFFFF 65535
+ void SetSignal(int signal) { m_cStructure->iSignal = signal; }
+
+ /// @brief To get with @ref SetSignal changed values.
+ int GetSignal() const { return m_cStructure->iSignal; }
+
+ /// @brief **optional**\n
+ /// Bit error rate.
+ void SetBER(long ber) { m_cStructure->iBER = ber; }
+
+ /// @brief To get with @ref SetBER changed values.
+ long GetBER() const { return m_cStructure->iBER; }
+
+ /// @brief **optional**\n
+ /// Uncorrected blocks:
+ void SetUNC(long unc) { m_cStructure->iUNC = unc; }
+
+ /// @brief To get with @ref SetBER changed values.
+ long GetUNC() const { return m_cStructure->iUNC; }
+ ///@}
+
+private:
+ PVRSignalStatus(const PVR_SIGNAL_STATUS* type) : CStructHdl(type) {}
+ PVRSignalStatus(PVR_SIGNAL_STATUS* type) : CStructHdl(type) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Channel_PVRDescrambleInfo class PVRDescrambleInfo
+/// @ingroup cpp_kodi_addon_pvr_Defs_Channel
+/// @brief **Data structure for descrample info**\n
+/// Information data to give via this to Kodi.
+///
+/// As description see also here https://en.wikipedia.org/wiki/Conditional_access.
+///
+/// Used on @ref kodi::addon::CInstancePVRClient::GetDescrambleInfo().
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_Channel_PVRDescrambleInfo_Help
+///
+///@{
+class PVRDescrambleInfo : public CStructHdl<PVRDescrambleInfo, PVR_DESCRAMBLE_INFO>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRDescrambleInfo()
+ {
+ m_cStructure->iPid = PVR_DESCRAMBLE_INFO_NOT_AVAILABLE;
+ m_cStructure->iCaid = PVR_DESCRAMBLE_INFO_NOT_AVAILABLE;
+ m_cStructure->iProvid = PVR_DESCRAMBLE_INFO_NOT_AVAILABLE;
+ m_cStructure->iEcmTime = PVR_DESCRAMBLE_INFO_NOT_AVAILABLE;
+ m_cStructure->iHops = PVR_DESCRAMBLE_INFO_NOT_AVAILABLE;
+ }
+ PVRDescrambleInfo(const PVRDescrambleInfo& type) : CStructHdl(type) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Channel_PVRDescrambleInfo_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Channel_PVRDescrambleInfo
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_Channel_PVRDescrambleInfo :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |------|------|----------|----------|-----------
+ /// | **Packet identifier** | `int` | @ref PVRDescrambleInfo::SetPID "SetPID" | @ref PVRDescrambleInfo::GetPID "GetPID" | *optional*
+ /// | **Conditional access identifier** | `int` | @ref PVRDescrambleInfo::SetCAID "SetCAID" | @ref PVRDescrambleInfo::GetCAID "GetCAID" | *optional*
+ /// | **Provider-ID** | `int` | @ref PVRDescrambleInfo::SetProviderID "SetProviderID" | @ref PVRDescrambleInfo::GetProviderID "GetProviderID" | *optional*
+ /// | **ECM time** | `int` | @ref PVRDescrambleInfo::SetECMTime "SetECMTime" | @ref PVRDescrambleInfo::GetECMTime "GetECMTime" | *optional*
+ /// | **Hops** | `int` | @ref PVRDescrambleInfo::SetHops "SetHops" | @ref PVRDescrambleInfo::GetHops "GetHops" | *optional*
+ /// | **Descramble card system** | `std::string` | @ref PVRDescrambleInfo::SetHops "SetHops" | @ref PVRDescrambleInfo::GetHops "GetHops" | *optional*
+ /// | **Reader** | `std::string` | @ref PVRDescrambleInfo::SetReader "SetReader" | @ref PVRDescrambleInfo::GetReader "GetReader" | *optional*
+ /// | **From** | `std::string` | @ref PVRDescrambleInfo::SetFrom "SetFrom" | @ref PVRDescrambleInfo::GetFrom "GetFrom" | *optional*
+ /// | **Protocol** | `std::string` | @ref PVRDescrambleInfo::SetProtocol "SetProtocol" | @ref PVRDescrambleInfo::GetProtocol "GetProtocol" | *optional*
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Channel_PVRDescrambleInfo
+ ///@{
+
+ /// @brief **optional**\n
+ /// Packet identifier.
+ ///
+ /// Each table or elementary stream in a transport stream is identified by
+ /// a 13-bit packet identifier (PID).
+ ///
+ /// Is @ref PVR_DESCRAMBLE_INFO_NOT_AVAILABLE as default, if not available
+ void SetPID(int pid) { m_cStructure->iPid = pid; }
+
+ /// @brief To get with @ref SetPID changed values
+ int GetPID() const { return m_cStructure->iPid; }
+
+ /// @brief **optional**\n
+ /// Conditional access identifier.
+ ///
+ /// Conditional access (abbreviated CA) or conditional access system (abbreviated CAS)
+ /// is the protection of content by requiring certain criteria to be met before granting
+ /// access to the content.
+ ///
+ /// Available CA system ID's listed here https://www.dvbservices.com/identifiers/ca_system_id.
+ ///
+ /// @ref PVR_DESCRAMBLE_INFO_NOT_AVAILABLE if not available.
+ void SetCAID(int iCaid) { m_cStructure->iCaid = iCaid; }
+
+ /// @brief To get with @ref SetCAID changed values.
+ int GetCAID() const { return m_cStructure->iCaid; }
+
+ /// @brief **optional**\n
+ /// Provider-ID.
+ ///
+ /// Is @ref PVR_DESCRAMBLE_INFO_NOT_AVAILABLE as default, if not available.
+ void SetProviderID(int provid) { m_cStructure->iProvid = provid; }
+
+ /// @brief To get with @ref SetProviderID changed values
+ int GetProviderID() const { return m_cStructure->iProvid; }
+
+ /// @brief **optional**\n
+ /// ECM time.
+ ///
+ /// Is @ref PVR_DESCRAMBLE_INFO_NOT_AVAILABLE as default, if not available.
+ void SetECMTime(int ecmTime) { m_cStructure->iEcmTime = ecmTime; }
+
+ /// @brief To get with @ref SetECMTime changed values.
+ int GetECMTime() const { return m_cStructure->iEcmTime; }
+
+ /// @brief **optional**\n
+ /// Hops.
+ ///
+ /// Is @ref PVR_DESCRAMBLE_INFO_NOT_AVAILABLE as default, if not available.
+ void SetHops(int hops) { m_cStructure->iHops = hops; }
+
+ /// @brief To get with @ref SetHops changed values.
+ int GetHops() const { return m_cStructure->iHops; }
+
+ /// @brief **optional**\n
+ /// Empty string if not available.
+ void SetCardSystem(const std::string& cardSystem)
+ {
+ strncpy(m_cStructure->strCardSystem, cardSystem.c_str(),
+ sizeof(m_cStructure->strCardSystem) - 1);
+ }
+
+ /// @brief To get with @ref SetCardSystem changed values.
+ std::string GetCardSystem() const { return m_cStructure->strCardSystem; }
+
+ /// @brief **optional**\n
+ /// Empty string if not available.
+ void SetReader(const std::string& reader)
+ {
+ strncpy(m_cStructure->strReader, reader.c_str(), sizeof(m_cStructure->strReader) - 1);
+ }
+
+ /// @brief To get with @ref SetReader changed values.
+ std::string GetReader() const { return m_cStructure->strReader; }
+
+ /// @brief **optional**\n
+ /// Empty string if not available.
+ void SetFrom(const std::string& from)
+ {
+ strncpy(m_cStructure->strFrom, from.c_str(), sizeof(m_cStructure->strFrom) - 1);
+ }
+
+ /// @brief To get with @ref SetFrom changed values.
+ std::string GetFrom() const { return m_cStructure->strFrom; }
+
+ /// @brief **optional**\n
+ /// Empty string if not available.
+ void SetProtocol(const std::string& protocol)
+ {
+ strncpy(m_cStructure->strProtocol, protocol.c_str(), sizeof(m_cStructure->strProtocol) - 1);
+ }
+
+ /// @brief To get with @ref SetProtocol changed values.
+ std::string GetProtocol() const { return m_cStructure->strProtocol; }
+ ///@}
+
+private:
+ PVRDescrambleInfo(const PVR_DESCRAMBLE_INFO* type) : CStructHdl(type) {}
+ PVRDescrambleInfo(PVR_DESCRAMBLE_INFO* type) : CStructHdl(type) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/EDL.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/EDL.h
new file mode 100644
index 0000000..34c7c41
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/EDL.h
@@ -0,0 +1,90 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/addon-instance/pvr/pvr_edl.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Definitions group 8 - PVR Edit definition list (EDL)
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_EDLEntry_PVREDLEntry class PVREDLEntry
+/// @ingroup cpp_kodi_addon_pvr_Defs_EDLEntry
+/// @brief **Edit definition list (EDL) entry**\n
+/// Time places and type of related fields.
+///
+/// This used within @ref cpp_kodi_addon_pvr_EPGTag "EPG" and
+/// @ref cpp_kodi_addon_pvr_Recordings "recordings".
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_EDLEntry_PVREDLEntry_Help
+///
+///@{
+class PVREDLEntry : public CStructHdl<PVREDLEntry, PVR_EDL_ENTRY>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVREDLEntry() { memset(m_cStructure, 0, sizeof(PVR_EDL_ENTRY)); }
+ PVREDLEntry(const PVREDLEntry& type) : CStructHdl(type) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_EDLEntry_PVREDLEntry_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_EDLEntry_PVREDLEntry
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_EDLEntry_PVREDLEntry :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |------|------|----------|----------|-----------
+ /// | **Start time** | `int64_t` | @ref PVREDLEntry::SetStart "SetStart" | @ref PVREDLEntry::GetStart "GetStart" | *required to set*
+ /// | **End time** | `int64_t` | @ref PVREDLEntry::SetEnd "SetEnd" | @ref PVREDLEntry::GetEnd "GetEnd" | *required to set*
+ /// | **Type** | @ref PVR_EDL_TYPE | @ref PVREDLEntry::SetType "SetType" | @ref PVREDLEntry::GetType "GetType" | *required to set*
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_EDLEntry_PVREDLEntry
+ ///@{
+
+ /// @brief Start time in milliseconds.
+ void SetStart(int64_t start) { m_cStructure->start = start; }
+
+ /// @brief To get with @ref SetStart() changed values.
+ int64_t GetStart() const { return m_cStructure->start; }
+
+ /// @brief End time in milliseconds.
+ void SetEnd(int64_t end) { m_cStructure->end = end; }
+
+ /// @brief To get with @ref SetEnd() changed values.
+ int64_t GetEnd() const { return m_cStructure->end; }
+
+ /// @brief The with @ref PVR_EDL_TYPE used definition list type.
+ void SetType(PVR_EDL_TYPE type) { m_cStructure->type = type; }
+
+ /// @brief To get with @ref SetType() changed values.
+ PVR_EDL_TYPE GetType() const { return m_cStructure->type; }
+ ///@}
+
+private:
+ PVREDLEntry(const PVR_EDL_ENTRY* type) : CStructHdl(type) {}
+ PVREDLEntry(PVR_EDL_ENTRY* type) : CStructHdl(type) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/EPG.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/EPG.h
new file mode 100644
index 0000000..7603a75
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/EPG.h
@@ -0,0 +1,516 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/addon-instance/pvr.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Definitions group 4 - PVR EPG
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_epg_PVREPGTag class PVREPGTag
+/// @ingroup cpp_kodi_addon_pvr_Defs_epg
+/// @brief **PVR add-on EPG data tag**\n
+/// Representation of an EPG event.
+///
+/// Herewith all EPG related data are saved in one class whereby the data can
+/// be exchanged with Kodi, or can also be used on the addon to save there.
+///
+/// See @ref cpp_kodi_addon_pvr_EPGTag "EPG methods" about usage.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_epg_PVREPGTag_Help
+///
+///@{
+class PVREPGTag : public CStructHdl<PVREPGTag, EPG_TAG>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVREPGTag()
+ {
+ memset(m_cStructure, 0, sizeof(EPG_TAG));
+ m_cStructure->iSeriesNumber = EPG_TAG_INVALID_SERIES_EPISODE;
+ m_cStructure->iEpisodeNumber = EPG_TAG_INVALID_SERIES_EPISODE;
+ m_cStructure->iEpisodePartNumber = EPG_TAG_INVALID_SERIES_EPISODE;
+ }
+ PVREPGTag(const PVREPGTag& epg)
+ : CStructHdl(epg),
+ m_title(epg.m_title),
+ m_plotOutline(epg.m_plotOutline),
+ m_plot(epg.m_plot),
+ m_originalTitle(epg.m_originalTitle),
+ m_cast(epg.m_cast),
+ m_director(epg.m_director),
+ m_writer(epg.m_writer),
+ m_IMDBNumber(epg.m_IMDBNumber),
+ m_episodeName(epg.m_episodeName),
+ m_iconPath(epg.m_iconPath),
+ m_seriesLink(epg.m_seriesLink),
+ m_genreDescription(epg.m_genreDescription),
+ m_parentalRatingCode(epg.m_parentalRatingCode),
+ m_firstAired(epg.m_firstAired)
+ {
+ }
+ /*! \endcond */
+
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_epg_PVREPGTag_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_PVREPGTag
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |------|------|----------|----------|---------
+ /// | **Unique broadcast id** | `unsigned int` | @ref PVREPGTag::SetUniqueBroadcastId "SetUniqueBroadcastId" | @ref PVREPGTag::GetUniqueBroadcastId "GetUniqueBroadcastId" | *required to set*
+ /// | **Unique channel id** | `unsigned int` | @ref PVREPGTag::SetUniqueChannelId "SetUniqueChannelId" | @ref PVREPGTag::GetUniqueChannelId "GetUniqueChannelId" | *required to set*
+ /// | **Title** | `std::string` | @ref PVREPGTag::SetTitle "SetTitle" | @ref PVREPGTag::GetTitle "GetTitle" | *required to set*
+ /// | **Start time** | `time_t` | @ref PVREPGTag::SetStartTime "SetStartTime" | @ref PVREPGTag::GetStartTime "GetStartTime" | *required to set*
+ /// | **End time** | `time_t` | @ref PVREPGTag::SetEndTime "SetEndTime" | @ref PVREPGTag::GetEndTime "GetEndTime" | *required to set*
+ /// | **Plot outline** | `std::string` | @ref PVREPGTag::SetPlotOutline "SetPlotOutline" | @ref PVREPGTag::GetPlotOutline "GetPlotOutline" | *optional*
+ /// | **Plot** | `std::string` | @ref PVREPGTag::SetPlot "SetPlot" | @ref PVREPGTag::GetPlot "GetPlot" | *optional*
+ /// | **Original title** | `std::string` | @ref PVREPGTag::SetOriginalTitle "SetOriginalTitle" | @ref PVREPGTag::GetOriginalTitle "GetOriginalTitle" | *optional*
+ /// | **Cast** | `std::string` | @ref PVREPGTag::SetCast "SetCast" | @ref PVREPGTag::GetCast "GetCast" | *optional*
+ /// | **Director** | `std::string` | @ref PVREPGTag::SetDirector "SetDirector" | @ref PVREPGTag::GetDirector "GetDirector" | *optional*
+ /// | **Writer** | `std::string` | @ref PVREPGTag::SetWriter "SetWriter" | @ref PVREPGTag::GetWriter "GetWriter" | *optional*
+ /// | **Year** | `int` | @ref PVREPGTag::SetYear "SetYear" | @ref PVREPGTag::GetYear "GetYear" | *optional*
+ /// | **IMDB number** | `std::string` | @ref PVREPGTag::SetIMDBNumber "SetIMDBNumber" | @ref PVREPGTag::GetIMDBNumber "GetIMDBNumber" | *optional*
+ /// | **Icon path** | `std::string` | @ref PVREPGTag::SetIconPath "SetIconPath" | @ref PVREPGTag::GetIconPath "GetIconPath" | *optional*
+ /// | **Genre type** | `int` | @ref PVREPGTag::SetGenreType "SetGenreType" | @ref PVREPGTag::GetGenreType "GetGenreType" | *optional*
+ /// | **Genre sub type** | `int` | @ref PVREPGTag::SetGenreSubType "SetGenreSubType" | @ref PVREPGTag::GetGenreSubType "GetGenreSubType" | *optional*
+ /// | **Genre description** | `std::string` | @ref PVREPGTag::SetGenreDescription "SetGenreDescription" | @ref PVREPGTag::GetGenreDescription "GetGenreDescription" | *optional*
+ /// | **First aired** | `time_t` | @ref PVREPGTag::SetFirstAired "SetFirstAired" | @ref PVREPGTag::GetFirstAired "GetFirstAired" | *optional*
+ /// | **Parental rating** | `int` | @ref PVREPGTag::SetParentalRating "SetParentalRating" | @ref PVREPGTag::GetParentalRating "GetParentalRating" | *optional*
+ /// | **Parental rating code** | `int` | @ref PVREPGTag::SetParentalRatingCode "SetParentalRatingCode" | @ref PVREPGTag::GetParentalRatingCode "GetParentalRatingCode" | *optional*
+ /// | **Star rating** | `int` | @ref PVREPGTag::SetStarRating "SetStarRating" | @ref PVREPGTag::GetStarRating "GetStarRating" | *optional*
+ /// | **Series number** | `int` | @ref PVREPGTag::SetSeriesNumber "SetSeriesNumber" | @ref PVREPGTag::GetSeriesNumber "GetSeriesNumber" | *optional*
+ /// | **Episode number** | `int` | @ref PVREPGTag::SetEpisodeNumber "SetEpisodeNumber" | @ref PVREPGTag::GetEpisodeNumber "GetEpisodeNumber" | *optional*
+ /// | **Episode part number** | `int` | @ref PVREPGTag::SetEpisodePartNumber "SetEpisodePartNumber" | @ref PVREPGTag::GetEpisodePartNumber "GetEpisodePartNumber" | *optional*
+ /// | **Episode name** | `std::string` | @ref PVREPGTag::SetEpisodeName "SetEpisodeName" | @ref PVREPGTag::GetEpisodeName "GetEpisodeName" | *optional*
+ /// | **Flags** | `unsigned int` | @ref PVREPGTag::SetFlags "SetFlags" | @ref PVREPGTag::GetFlags "GetFlags" | *optional*
+ /// | **Series link** | `std::string` | @ref PVREPGTag::SetSeriesLink "SetSeriesLink" | @ref PVREPGTag::GetSeriesLink "GetSeriesLink" | *optional*
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_epg_PVREPGTag
+ ///@{
+
+ /// @brief **required**\n
+ /// Identifier for this event. Event uids must be unique for a channel. Valid uids must be greater than @ref EPG_TAG_INVALID_UID.
+ void SetUniqueBroadcastId(unsigned int uniqueBroadcastId)
+ {
+ m_cStructure->iUniqueBroadcastId = uniqueBroadcastId;
+ }
+
+ /// @brief To get with @ref SetUniqueBroadcastId changed values.
+ unsigned int GetUniqueBroadcastId() const { return m_cStructure->iUniqueBroadcastId; }
+
+ /// @brief **required**\n
+ /// Unique identifier of the channel this event belongs to.
+ void SetUniqueChannelId(unsigned int uniqueChannelId)
+ {
+ m_cStructure->iUniqueChannelId = uniqueChannelId;
+ }
+
+ /// @brief To get with @ref SetUniqueChannelId changed values
+ unsigned int GetUniqueChannelId() const { return m_cStructure->iUniqueChannelId; }
+
+ /// @brief **required**\n
+ /// This event's title.
+ void SetTitle(const std::string& title) { m_title = title; }
+
+ /// @brief To get with @ref SetTitle changed values.
+ std::string GetTitle() const { return m_title; }
+
+ /// @brief **required**\n
+ /// Start time in UTC.
+ ///
+ /// Seconds elapsed since 00:00 hours, Jan 1, 1970 UTC.
+ void SetStartTime(time_t startTime) { m_cStructure->startTime = startTime; }
+
+ /// @brief To get with @ref SetStartTime changed values.
+ time_t GetStartTime() const { return m_cStructure->startTime; }
+
+ /// @brief **required**\n
+ /// End time in UTC.
+ ///
+ /// Seconds elapsed since 00:00 hours, Jan 1, 1970 UTC.
+ void SetEndTime(time_t endTime) { m_cStructure->endTime = endTime; }
+
+ /// @brief To get with @ref SetEndTime changed values.
+ time_t GetEndTime() const { return m_cStructure->endTime; }
+
+ /// @brief **optional**\n
+ /// Plot outline name.
+ void SetPlotOutline(const std::string& plotOutline) { m_plotOutline = plotOutline; }
+
+ /// @brief To get with @ref SetPlotOutline changed values.
+ std::string GetPlotOutline() const { return m_plotOutline; }
+
+ /// @brief **optional**\n
+ /// Plot name.
+ void SetPlot(const std::string& plot) { m_plot = plot; }
+
+ /// @brief To get with @ref GetPlot changed values.
+ std::string GetPlot() const { return m_plot; }
+
+ /// @brief **optional**\n
+ /// Original title.
+ void SetOriginalTitle(const std::string& originalTitle) { m_originalTitle = originalTitle; }
+
+ /// @brief To get with @ref SetOriginalTitle changed values
+ std::string GetOriginalTitle() const { return m_originalTitle; }
+
+ /// @brief **optional**\n
+ /// Cast name(s).
+ ///
+ /// @note Use @ref EPG_STRING_TOKEN_SEPARATOR to separate different persons.
+ void SetCast(const std::string& cast) { m_cast = cast; }
+
+ /// @brief To get with @ref SetCast changed values
+ std::string GetCast() const { return m_cast; }
+
+ /// @brief **optional**\n
+ /// Director name(s).
+ ///
+ /// @note Use @ref EPG_STRING_TOKEN_SEPARATOR to separate different persons.
+ void SetDirector(const std::string& director) { m_director = director; }
+
+ /// @brief To get with @ref SetDirector changed values.
+ std::string GetDirector() const { return m_director; }
+
+ /// @brief **optional**\n
+ /// Writer name(s).
+ ///
+ /// @note Use @ref EPG_STRING_TOKEN_SEPARATOR to separate different persons.
+ void SetWriter(const std::string& writer) { m_writer = writer; }
+
+ /// @brief To get with @ref SetDirector changed values
+ std::string GetWriter() const { return m_writer; }
+
+ /// @brief **optional**\n
+ /// Year.
+ void SetYear(int year) { m_cStructure->iYear = year; }
+
+ /// @brief To get with @ref SetYear changed values.
+ int GetYear() const { return m_cStructure->iYear; }
+
+ /// @brief **optional**\n
+ /// [IMDB](https://en.wikipedia.org/wiki/IMDb) identification number.
+ void SetIMDBNumber(const std::string& IMDBNumber) { m_IMDBNumber = IMDBNumber; }
+
+ /// @brief To get with @ref SetIMDBNumber changed values.
+ std::string GetIMDBNumber() const { return m_IMDBNumber; }
+
+ /// @brief **optional**\n
+ /// Icon path.
+ void SetIconPath(const std::string& iconPath) { m_iconPath = iconPath; }
+
+ /// @brief To get with @ref SetIconPath changed values.
+ std::string GetIconPath() const { return m_iconPath; }
+
+ /// @brief **optional**\n
+ /// Genre type.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails EPG_EVENT_CONTENTMASK
+ ///
+ /// Use @ref EPG_GENRE_USE_STRING if type becomes given by @ref SetGenreDescription.
+ ///
+ /// @note If confirmed that backend brings the types in [ETSI EN 300 468](https://www.etsi.org/deliver/etsi_en/300400_300499/300468/01.14.01_60/en_300468v011401p.pdf)
+ /// conform values, can be @ref EPG_EVENT_CONTENTMASK ignored and to set here
+ /// with backend value.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example 1:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVREPGTag tag;
+ /// tag.SetGenreType(EPG_EVENT_CONTENTMASK_MOVIEDRAMA);
+ /// ~~~~~~~~~~~~~
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example 2** (in case of other, not ETSI EN 300 468 conform genre types):
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVREPGTag tag;
+ /// tag.SetGenreType(EPG_GENRE_USE_STRING);
+ /// tag.SetGenreDescription("My special genre name"); // Should use (if possible) kodi::GetLocalizedString(...) to have match user language.
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetGenreType(int genreType) { m_cStructure->iGenreType = genreType; }
+
+ /// @brief To get with @ref SetGenreType changed values
+ int GetGenreType() const { return m_cStructure->iGenreType; }
+
+ /// @brief **optional**\n
+ /// Genre sub type.
+ ///
+ /// @copydetails EPG_EVENT_CONTENTMASK
+ ///
+ /// Subtypes groups related to set by @ref SetGenreType:
+ /// | Main genre type | List with available sub genre types
+ /// |-----------------|-----------------------------------------
+ /// | @ref EPG_EVENT_CONTENTMASK_UNDEFINED | Nothing, should be 0
+ /// | @ref EPG_EVENT_CONTENTMASK_MOVIEDRAMA | @ref EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA
+ /// | @ref EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS | @ref EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS
+ /// | @ref EPG_EVENT_CONTENTMASK_SHOW | @ref EPG_EVENT_CONTENTSUBMASK_SHOW
+ /// | @ref EPG_EVENT_CONTENTMASK_SPORTS | @ref EPG_EVENT_CONTENTSUBMASK_SPORTS
+ /// | @ref EPG_EVENT_CONTENTMASK_CHILDRENYOUTH | @ref EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH
+ /// | @ref EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE | @ref EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE
+ /// | @ref EPG_EVENT_CONTENTMASK_ARTSCULTURE | @ref EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE
+ /// | @ref EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS | @ref EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS
+ /// | @ref EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE | @ref EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE
+ /// | @ref EPG_EVENT_CONTENTMASK_LEISUREHOBBIES | @ref EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES
+ /// | @ref EPG_EVENT_CONTENTMASK_SPECIAL | @ref EPG_EVENT_CONTENTSUBMASK_SPECIAL
+ /// | @ref EPG_EVENT_CONTENTMASK_USERDEFINED | Can be defined by you
+ /// | @ref EPG_GENRE_USE_STRING | **Kodi's own value**, which declares that the type with @ref SetGenreDescription is given.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVREPGTag tag;
+ /// tag.SetGenreType(EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE);
+ /// tag.SetGenreSubType(EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE_JAZZ);
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetGenreSubType(int genreSubType) { m_cStructure->iGenreSubType = genreSubType; }
+
+ /// @brief To get with @ref SetGenreSubType changed values.
+ int GetGenreSubType() const { return m_cStructure->iGenreSubType; }
+
+ /// @brief **optional**\n genre. Will be used only when genreType == @ref EPG_GENRE_USE_STRING
+ /// or genreSubType == @ref EPG_GENRE_USE_STRING.
+ ///
+ /// Use @ref EPG_STRING_TOKEN_SEPARATOR to separate different genres.
+ ///
+ /// In case of other, not [ETSI EN 300 468](https://www.etsi.org/deliver/etsi_en/300400_300499/300468/01.14.01_60/en_300468v011401p.pdf)
+ /// conform genre types or something special.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVREPGTag tag;
+ /// tag.SetGenreType(EPG_GENRE_USE_STRING);
+ /// tag.SetGenreDescription("Action" + EPG_STRING_TOKEN_SEPARATOR + "Thriller");
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetGenreDescription(const std::string& genreDescription)
+ {
+ m_genreDescription = genreDescription;
+ }
+
+ /// @brief To get with @ref SetGenreDescription changed values.
+ std::string GetGenreDescription() const { return m_genreDescription; }
+
+ /// @brief **optional**\n
+ /// First aired in UTC.
+ void SetFirstAired(const std::string& firstAired) { m_firstAired = firstAired; }
+
+ /// @brief To get with @ref SetFirstAired changed values.
+ std::string GetFirstAired() const { return m_firstAired; }
+
+ /// @brief **optional**\n
+ /// Parental rating.
+ void SetParentalRating(int parentalRating) { m_cStructure->iParentalRating = parentalRating; }
+
+ /// @brief To get with @ref SetParentalRatinge changed values.
+ int GetParentalRating() const { return m_cStructure->iParentalRating; }
+
+ /// @brief **required**\n
+ /// This event's parental rating code.
+ void SetParentalRatingCode(const std::string& parentalRatingCode)
+ {
+ m_parentalRatingCode = parentalRatingCode;
+ }
+
+ /// @brief To get with @ref SetParentalRatingCode changed values.
+ std::string GetParentalRatingCode() const { return m_parentalRatingCode; }
+
+ /// @brief **optional**\n
+ /// Star rating.
+ void SetStarRating(int starRating) { m_cStructure->iStarRating = starRating; }
+
+ /// @brief To get with @ref SetStarRating changed values.
+ int GetStarRating() const { return m_cStructure->iStarRating; }
+
+ /// @brief **optional**\n
+ /// Series number.
+ void SetSeriesNumber(int seriesNumber) { m_cStructure->iSeriesNumber = seriesNumber; }
+
+ /// @brief To get with @ref SetSeriesNumber changed values.
+ int GetSeriesNumber() const { return m_cStructure->iSeriesNumber; }
+
+ /// @brief **optional**\n
+ /// Episode number.
+ void SetEpisodeNumber(int episodeNumber) { m_cStructure->iEpisodeNumber = episodeNumber; }
+
+ /// @brief To get with @ref SetEpisodeNumber changed values.
+ int GetEpisodeNumber() const { return m_cStructure->iEpisodeNumber; }
+
+ /// @brief **optional**\n
+ /// Episode part number.
+ void SetEpisodePartNumber(int episodePartNumber)
+ {
+ m_cStructure->iEpisodePartNumber = episodePartNumber;
+ }
+
+ /// @brief To get with @ref SetEpisodePartNumber changed values.
+ int GetEpisodePartNumber() const { return m_cStructure->iEpisodePartNumber; }
+
+ /// @brief **optional**\n
+ /// Episode name.
+ void SetEpisodeName(const std::string& episodeName) { m_episodeName = episodeName; }
+
+ /// @brief To get with @ref SetEpisodeName changed values.
+ std::string GetEpisodeName() const { return m_episodeName; }
+
+ /// @brief **optional**\n
+ /// Bit field of independent flags associated with the EPG entry.
+ ///
+ /// See @ref cpp_kodi_addon_pvr_Defs_epg_EPG_TAG_FLAG for available bit flags.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_epg_EPG_TAG_FLAG
+ ///
+ void SetFlags(unsigned int flags) { m_cStructure->iFlags = flags; }
+
+ /// @brief To get with @ref SetFlags changed values.
+ unsigned int GetFlags() const { return m_cStructure->iFlags; }
+
+ /// @brief **optional**\n
+ /// Series link for this event.
+ void SetSeriesLink(const std::string& seriesLink) { m_seriesLink = seriesLink; }
+
+ /// @brief To get with @ref SetSeriesLink changed values.
+ std::string GetSeriesLink() const { return m_seriesLink; }
+
+ ///@}
+
+ // Internal used, as this have own memory for strings and to translate them to "C"
+ EPG_TAG* GetTag() const
+ {
+ m_cStructure->strTitle = m_title.c_str();
+ m_cStructure->strPlotOutline = m_plotOutline.c_str();
+ m_cStructure->strPlot = m_plot.c_str();
+ m_cStructure->strOriginalTitle = m_originalTitle.c_str();
+ m_cStructure->strCast = m_cast.c_str();
+ m_cStructure->strDirector = m_director.c_str();
+ m_cStructure->strWriter = m_writer.c_str();
+ m_cStructure->strIMDBNumber = m_IMDBNumber.c_str();
+ m_cStructure->strIconPath = m_iconPath.c_str();
+ m_cStructure->strGenreDescription = m_genreDescription.c_str();
+ m_cStructure->strParentalRatingCode = m_parentalRatingCode.c_str();
+ m_cStructure->strEpisodeName = m_episodeName.c_str();
+ m_cStructure->strSeriesLink = m_seriesLink.c_str();
+ m_cStructure->strFirstAired = m_firstAired.c_str();
+
+ return m_cStructure;
+ }
+
+private:
+ PVREPGTag(const EPG_TAG* epg) : CStructHdl(epg) { SetData(epg); }
+ PVREPGTag(EPG_TAG* epg) : CStructHdl(epg) { SetData(epg); }
+
+ const PVREPGTag& operator=(const PVREPGTag& right);
+ const PVREPGTag& operator=(const EPG_TAG& right);
+ operator EPG_TAG*();
+
+ std::string m_title;
+ std::string m_plotOutline;
+ std::string m_plot;
+ std::string m_originalTitle;
+ std::string m_cast;
+ std::string m_director;
+ std::string m_writer;
+ std::string m_IMDBNumber;
+ std::string m_episodeName;
+ std::string m_iconPath;
+ std::string m_seriesLink;
+ std::string m_genreDescription;
+ std::string m_parentalRatingCode;
+ std::string m_firstAired;
+
+ void SetData(const EPG_TAG* tag)
+ {
+ m_title = tag->strTitle == nullptr ? "" : tag->strTitle;
+ m_plotOutline = tag->strPlotOutline == nullptr ? "" : tag->strPlotOutline;
+ m_plot = tag->strPlot == nullptr ? "" : tag->strPlot;
+ m_originalTitle = tag->strOriginalTitle == nullptr ? "" : tag->strOriginalTitle;
+ m_cast = tag->strCast == nullptr ? "" : tag->strCast;
+ m_director = tag->strDirector == nullptr ? "" : tag->strDirector;
+ m_writer = tag->strWriter == nullptr ? "" : tag->strWriter;
+ m_IMDBNumber = tag->strIMDBNumber == nullptr ? "" : tag->strIMDBNumber;
+ m_iconPath = tag->strIconPath == nullptr ? "" : tag->strIconPath;
+ m_genreDescription = tag->strGenreDescription == nullptr ? "" : tag->strGenreDescription;
+ m_parentalRatingCode = tag->strParentalRatingCode == nullptr ? "" : tag->strParentalRatingCode;
+ m_episodeName = tag->strEpisodeName == nullptr ? "" : tag->strEpisodeName;
+ m_seriesLink = tag->strSeriesLink == nullptr ? "" : tag->strSeriesLink;
+ m_firstAired = tag->strFirstAired == nullptr ? "" : tag->strFirstAired;
+ }
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_epg_PVREPGTagsResultSet class PVREPGTagsResultSet
+/// @ingroup cpp_kodi_addon_pvr_Defs_epg_PVREPGTag
+/// @brief **PVR add-on EPG entry transfer class**\n
+/// To transfer the content of @ref kodi::addon::CInstancePVRClient::GetEPGForChannel().
+///
+/// @note This becomes only be used on addon call above, not usable outside on
+/// addon itself.
+///@{
+class PVREPGTagsResultSet
+{
+public:
+ /*! \cond PRIVATE */
+ PVREPGTagsResultSet() = delete;
+ PVREPGTagsResultSet(const AddonInstance_PVR* instance, PVR_HANDLE handle)
+ : m_instance(instance), m_handle(handle)
+ {
+ }
+ /*! \endcond */
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_epg_PVREPGTagsResultSet
+ ///@{
+
+ /// @brief To add and give content from addon to Kodi on related call.
+ ///
+ /// @param[in] tag The to transferred data.
+ void Add(const kodi::addon::PVREPGTag& tag)
+ {
+ m_instance->toKodi->TransferEpgEntry(m_instance->toKodi->kodiInstance, m_handle, tag.GetTag());
+ }
+
+ ///@}
+
+private:
+ const AddonInstance_PVR* m_instance = nullptr;
+ const PVR_HANDLE m_handle;
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/General.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/General.h
new file mode 100644
index 0000000..758feed
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/General.h
@@ -0,0 +1,536 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/addon-instance/pvr/pvr_general.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Definitions group 1 - General PVR
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_PVRTypeIntValue class PVRTypeIntValue
+/// @ingroup cpp_kodi_addon_pvr_Defs_General
+/// @brief **PVR add-on type value**\n
+/// Representation of a <b>`<int, std::string>`</b> event related value.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_PVRTypeIntValue_Help
+///
+///@{
+class PVRTypeIntValue : public CStructHdl<PVRTypeIntValue, PVR_ATTRIBUTE_INT_VALUE>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRTypeIntValue(const PVRTypeIntValue& data) : CStructHdl(data) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_PVRTypeIntValue_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_PVRTypeIntValue
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_PVRTypeIntValue :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Value** | `int` | @ref PVRTypeIntValue::SetValue "SetValue" | @ref PVRTypeIntValue::GetValue "GetValue"
+ /// | **Description** | `std::string` | @ref PVRTypeIntValue::SetDescription "SetDescription" | @ref PVRTypeIntValue::GetDescription "GetDescription"
+ ///
+ /// @remark Further can there be used his class constructor to set values.
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_PVRTypeIntValue
+ ///@{
+
+ /// @brief Default class constructor.
+ ///
+ /// @note Values must be set afterwards.
+ PVRTypeIntValue() = default;
+
+ /// @brief Class constructor with integrated value set.
+ ///
+ /// @param[in] value Type identification value
+ /// @param[in] description Type description text
+ PVRTypeIntValue(int value, const std::string& description)
+ {
+ SetValue(value);
+ SetDescription(description);
+ }
+
+ /// @brief To set with the identification value.
+ void SetValue(int value) { m_cStructure->iValue = value; }
+
+ /// @brief To get with the identification value.
+ int GetValue() const { return m_cStructure->iValue; }
+
+ /// @brief To set with the description text of the value.
+ void SetDescription(const std::string& description)
+ {
+ strncpy(m_cStructure->strDescription, description.c_str(),
+ sizeof(m_cStructure->strDescription) - 1);
+ }
+
+ /// @brief To get with the description text of the value.
+ std::string GetDescription() const { return m_cStructure->strDescription; }
+ ///@}
+
+private:
+ PVRTypeIntValue(const PVR_ATTRIBUTE_INT_VALUE* data) : CStructHdl(data) {}
+ PVRTypeIntValue(PVR_ATTRIBUTE_INT_VALUE* data) : CStructHdl(data) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_PVRCapabilities class PVRCapabilities
+/// @ingroup cpp_kodi_addon_pvr_Defs_General
+/// @brief **PVR add-on capabilities**\n
+/// This class is needed to tell Kodi which options are supported on the addon.
+///
+/// If a capability is set to **true**, then the corresponding methods from
+/// @ref cpp_kodi_addon_pvr "kodi::addon::CInstancePVRClient" need to be
+/// implemented.
+///
+/// As default them all set to **false**.
+///
+/// Used on @ref kodi::addon::CInstancePVRClient::GetCapabilities().
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_PVRCapabilities_Help
+///
+///@{
+class PVRCapabilities
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ explicit PVRCapabilities() = delete;
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_PVRCapabilities_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_PVRCapabilities
+ /// ----------------------------------------------------------------------------
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_PVRCapabilities :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Supports EPG** | `boolean` | @ref PVRCapabilities::SetSupportsEPG "SetSupportsEPG" | @ref PVRCapabilities::GetSupportsEPG "GetSupportsEPG"
+ /// | **Supports EPG EDL** | `boolean` | @ref PVRCapabilities::SetSupportsEPGEdl "SetSupportsEPGEdl" | @ref PVRCapabilities::GetSupportsEPGEdl "GetSupportsEPGEdl"
+ /// | **Supports TV** | `boolean` | @ref PVRCapabilities::SetSupportsTV "SetSupportsTV" | @ref PVRCapabilities::GetSupportsTV "GetSupportsTV"
+ /// | **Supports radio** | `boolean` | @ref PVRCapabilities::SetSupportsRadio "SetSupportsRadio" | @ref PVRCapabilities::GetSupportsRadio "GetSupportsRadio"
+ /// | **Supports recordings** | `boolean` | @ref PVRCapabilities::SetSupportsRecordings "SetSupportsRecordings" | @ref PVRCapabilities::GetSupportsRecordings "GetSupportsRecordings"
+ /// | **Supports recordings undelete** | `boolean` | @ref PVRCapabilities::SetSupportsRecordingsUndelete "SetSupportsRecordingsUndelete" | @ref PVRCapabilities::GetSupportsRecordingsUndelete "SetSupportsRecordingsUndelete"
+ /// | **Supports timers** | `boolean` | @ref PVRCapabilities::SetSupportsTimers "SetSupportsTimers" | @ref PVRCapabilities::GetSupportsTimers "GetSupportsTimers"
+ /// | **Supports providers** | `boolean` | @ref PVRCapabilities::SetSupportsProviders "SetSupportsProviders" | @ref PVRCapabilities::GetSupportsProviders "GetSupportsProviders"
+ /// | **Supports channel groups** | `boolean` | @ref PVRCapabilities::SetSupportsChannelGroups "SetSupportsChannelGroups" | @ref PVRCapabilities::GetSupportsChannelGroups "GetSupportsChannelGroups"
+ /// | **Supports channel scan** | `boolean` | @ref PVRCapabilities::SetSupportsChannelScan "SetSupportsChannelScan" | @ref PVRCapabilities::GetSupportsChannelScan "GetSupportsChannelScan"
+ /// | **Supports channel settings** | `boolean` | @ref PVRCapabilities::SetSupportsChannelSettings "SetSupportsChannelSettings" | @ref PVRCapabilities::GetSupportsChannelSettings "GetSupportsChannelSettings"
+ /// | **Handles input stream** | `boolean` | @ref PVRCapabilities::SetHandlesInputStream "SetHandlesInputStream" | @ref PVRCapabilities::GetHandlesInputStream "GetHandlesInputStream"
+ /// | **Handles demuxing** | `boolean` | @ref PVRCapabilities::SetHandlesDemuxing "SetHandlesDemuxing" | @ref PVRCapabilities::GetHandlesDemuxing "GetHandlesDemuxing"
+ /// | **Supports recording play count** | `boolean` | @ref PVRCapabilities::SetSupportsRecordingPlayCount "SetSupportsRecordingPlayCount" | @ref PVRCapabilities::GetSupportsRecordingPlayCount "GetSupportsRecordingPlayCount"
+ /// | **Supports last played position** | `boolean` | @ref PVRCapabilities::SetSupportsLastPlayedPosition "SetSupportsLastPlayedPosition" | @ref PVRCapabilities::GetSupportsLastPlayedPosition "GetSupportsLastPlayedPosition"
+ /// | **Supports recording EDL** | `boolean` | @ref PVRCapabilities::SetSupportsRecordingEdl "SetSupportsRecordingEdl" | @ref PVRCapabilities::GetSupportsRecordingEdl "GetSupportsRecordingEdl"
+ /// | **Supports recordings rename** | `boolean` | @ref PVRCapabilities::SetSupportsRecordingsRename "SetSupportsRecordingsRename" | @ref PVRCapabilities::GetSupportsRecordingsRename "GetSupportsRecordingsRename"
+ /// | **Supports recordings lifetime change** | `boolean` | @ref PVRCapabilities::SetSupportsRecordingsLifetimeChange "SetSupportsRecordingsLifetimeChange" | @ref PVRCapabilities::GetSupportsRecordingsLifetimeChange "GetSupportsRecordingsLifetimeChange"
+ /// | **Supports descramble info** | `boolean` | @ref PVRCapabilities::SetSupportsDescrambleInfo "SetSupportsDescrambleInfo" | @ref PVRCapabilities::GetSupportsDescrambleInfo "GetSupportsDescrambleInfo"
+ /// | **Supports async EPG transfer** | `boolean` | @ref PVRCapabilities::SetSupportsAsyncEPGTransfer "SetSupportsAsyncEPGTransfer" | @ref PVRCapabilities::GetSupportsAsyncEPGTransfer "GetSupportsAsyncEPGTransfer"
+ /// | **Supports recording size** | `boolean` | @ref PVRCapabilities::SetSupportsRecordingSize "SetSupportsRecordingSize" | @ref PVRCapabilities::GetSupportsRecordingSize "GetSupportsRecordingSize"
+ /// | **Supports recordings delete** | `boolean` | @ref PVRCapabilities::SetSupportsRecordingsDelete "SetSupportsRecordingsDelete" | @ref PVRCapabilities::GetSupportsRecordingsDelete "SetSupportsRecordingsDelete"
+ /// | **Recordings lifetime values** | @ref cpp_kodi_addon_pvr_Defs_PVRTypeIntValue "PVRTypeIntValue" | @ref PVRCapabilities::SetRecordingsLifetimeValues "SetRecordingsLifetimeValues" | @ref PVRCapabilities::GetRecordingsLifetimeValues "GetRecordingsLifetimeValues"
+ ///
+ /// @warning This class can not be used outside of @ref kodi::addon::CInstancePVRClient::GetCapabilities()
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_PVRCapabilities
+ ///@{
+
+ /// @brief Set **true** if the add-on provides EPG information.
+ void SetSupportsEPG(bool supportsEPG) { m_capabilities->bSupportsEPG = supportsEPG; }
+
+ /// @brief To get with @ref SetSupportsEPG changed values.
+ bool GetSupportsEPG() const { return m_capabilities->bSupportsEPG; }
+
+ /// @brief Set **true** if the backend supports retrieving an edit decision
+ /// list for an EPG tag.
+ void SetSupportsEPGEdl(bool supportsEPGEdl) { m_capabilities->bSupportsEPGEdl = supportsEPGEdl; }
+
+ /// @brief To get with @ref SetSupportsEPGEdl changed values.
+ bool GetSupportsEPGEdl() const { return m_capabilities->bSupportsEPGEdl; }
+
+ /// @brief Set **true** if this add-on provides TV channels.
+ void SetSupportsTV(bool supportsTV) { m_capabilities->bSupportsTV = supportsTV; }
+
+ /// @brief To get with @ref SetSupportsTV changed values.
+ bool GetSupportsTV() const { return m_capabilities->bSupportsTV; }
+
+ /// @brief Set **true** if this add-on provides TV channels.
+ void SetSupportsRadio(bool supportsRadio) { m_capabilities->bSupportsRadio = supportsRadio; }
+
+ /// @brief To get with @ref SetSupportsRadio changed values.
+ bool GetSupportsRadio() const { return m_capabilities->bSupportsRadio; }
+
+ /// @brief **true** if this add-on supports playback of recordings stored on
+ /// the backend.
+ void SetSupportsRecordings(bool supportsRecordings)
+ {
+ m_capabilities->bSupportsRecordings = supportsRecordings;
+ }
+
+ /// @brief To get with @ref SetSupportsRecordings changed values.
+ bool GetSupportsRecordings() const { return m_capabilities->bSupportsRecordings; }
+
+ /// @brief Set **true** if this add-on supports undelete of recordings stored
+ /// on the backend.
+ void SetSupportsRecordingsUndelete(bool supportsRecordingsUndelete)
+ {
+ m_capabilities->bSupportsRecordingsUndelete = supportsRecordingsUndelete;
+ }
+
+ /// @brief To get with @ref SetSupportsRecordings changed values.
+ bool GetSupportsRecordingsUndelete() const { return m_capabilities->bSupportsRecordingsUndelete; }
+
+ /// @brief Set **true** if this add-on supports the creation and editing of
+ /// timers.
+ void SetSupportsTimers(bool supportsTimers) { m_capabilities->bSupportsTimers = supportsTimers; }
+
+ /// @brief To get with @ref SetSupportsTimers changed values.
+ bool GetSupportsTimers() const { return m_capabilities->bSupportsTimers; }
+
+ /// @brief Set **true** if this add-on supports providers.
+ ///
+ /// It uses the following functions:
+ /// - @ref kodi::addon::CInstancePVRClient::GetProvidersAmount()
+ /// - @ref kodi::addon::CInstancePVRClient::GetProviders()
+ void SetSupportsProviders(bool supportsProviders)
+ {
+ m_capabilities->bSupportsProviders = supportsProviders;
+ }
+
+ /// @brief To get with @ref SetSupportsProviders changed values.
+ bool GetSupportsProviders() const { return m_capabilities->bSupportsProviders; }
+
+ /// @brief Set **true** if this add-on supports channel groups.
+ ///
+ /// It use the following functions:
+ /// - @ref kodi::addon::CInstancePVRClient::GetChannelGroupsAmount()
+ /// - @ref kodi::addon::CInstancePVRClient::GetChannelGroups()
+ /// - @ref kodi::addon::CInstancePVRClient::GetChannelGroupMembers()
+ void SetSupportsChannelGroups(bool supportsChannelGroups)
+ {
+ m_capabilities->bSupportsChannelGroups = supportsChannelGroups;
+ }
+
+ /// @brief To get with @ref SetSupportsChannelGroups changed values.
+ bool GetSupportsChannelGroups() const { return m_capabilities->bSupportsChannelGroups; }
+
+ /// @brief Set **true** if this add-on support scanning for new channels on
+ /// the backend.
+ ///
+ /// It use the following function:
+ /// - @ref kodi::addon::CInstancePVRClient::OpenDialogChannelScan()
+ void SetSupportsChannelScan(bool supportsChannelScan)
+ {
+ m_capabilities->bSupportsChannelScan = supportsChannelScan;
+ }
+
+ /// @brief To get with @ref SetSupportsChannelScan changed values.
+ bool GetSupportsChannelScan() const { return m_capabilities->bSupportsChannelScan; }
+
+ /// @brief Set **true** if this add-on supports channel edit.
+ ///
+ /// It use the following functions:
+ /// - @ref kodi::addon::CInstancePVRClient::DeleteChannel()
+ /// - @ref kodi::addon::CInstancePVRClient::RenameChannel()
+ /// - @ref kodi::addon::CInstancePVRClient::OpenDialogChannelSettings()
+ /// - @ref kodi::addon::CInstancePVRClient::OpenDialogChannelAdd()
+ void SetSupportsChannelSettings(bool supportsChannelSettings)
+ {
+ m_capabilities->bSupportsChannelSettings = supportsChannelSettings;
+ }
+
+ /// @brief To get with @ref SetSupportsChannelSettings changed values.
+ bool GetSupportsChannelSettings() const { return m_capabilities->bSupportsChannelSettings; }
+
+ /// @brief Set **true** if this add-on provides an input stream. false if Kodi
+ /// handles the stream.
+ void SetHandlesInputStream(bool handlesInputStream)
+ {
+ m_capabilities->bHandlesInputStream = handlesInputStream;
+ }
+
+ /// @brief To get with @ref SetHandlesInputStream changed values.
+ bool GetHandlesInputStream() const { return m_capabilities->bHandlesInputStream; }
+
+ /// @brief Set **true** if this add-on demultiplexes packets.
+ void SetHandlesDemuxing(bool handlesDemuxing)
+ {
+ m_capabilities->bHandlesDemuxing = handlesDemuxing;
+ }
+
+ /// @brief To get with @ref SetHandlesDemuxing changed values.
+ bool GetHandlesDemuxing() const { return m_capabilities->bHandlesDemuxing; }
+
+ /// @brief Set **true** if the backend supports play count for recordings.
+ void SetSupportsRecordingPlayCount(bool supportsRecordingPlayCount)
+ {
+ m_capabilities->bSupportsRecordingPlayCount = supportsRecordingPlayCount;
+ }
+
+ /// @brief To get with @ref SetSupportsRecordingPlayCount changed values.
+ bool GetSupportsRecordingPlayCount() const { return m_capabilities->bSupportsRecordingPlayCount; }
+
+ /// @brief Set **true** if the backend supports store/retrieve of last played
+ /// position for recordings.
+ void SetSupportsLastPlayedPosition(bool supportsLastPlayedPosition)
+ {
+ m_capabilities->bSupportsLastPlayedPosition = supportsLastPlayedPosition;
+ }
+
+ /// @brief To get with @ref SetSupportsLastPlayedPosition changed values.
+ bool GetSupportsLastPlayedPosition() const { return m_capabilities->bSupportsLastPlayedPosition; }
+
+ /// @brief Set **true** if the backend supports retrieving an edit decision
+ /// list for recordings.
+ void SetSupportsRecordingEdl(bool supportsRecordingEdl)
+ {
+ m_capabilities->bSupportsRecordingEdl = supportsRecordingEdl;
+ }
+
+ /// @brief To get with @ref SetSupportsRecordingEdl changed values.
+ bool GetSupportsRecordingEdl() const { return m_capabilities->bSupportsRecordingEdl; }
+
+ /// @brief Set **true** if the backend supports renaming recordings.
+ void SetSupportsRecordingsRename(bool supportsRecordingsRename)
+ {
+ m_capabilities->bSupportsRecordingsRename = supportsRecordingsRename;
+ }
+
+ /// @brief To get with @ref SetSupportsRecordingsRename changed values.
+ bool GetSupportsRecordingsRename() const { return m_capabilities->bSupportsRecordingsRename; }
+
+ /// @brief Set **true** if the backend supports changing lifetime for
+ /// recordings.
+ void SetSupportsRecordingsLifetimeChange(bool supportsRecordingsLifetimeChange)
+ {
+ m_capabilities->bSupportsRecordingsLifetimeChange = supportsRecordingsLifetimeChange;
+ }
+
+ /// @brief To get with @ref SetSupportsRecordingsLifetimeChange changed
+ /// values.
+ bool GetSupportsRecordingsLifetimeChange() const
+ {
+ return m_capabilities->bSupportsRecordingsLifetimeChange;
+ }
+
+ /// @brief Set **true** if the backend supports descramble information for
+ /// playing channels.
+ void SetSupportsDescrambleInfo(bool supportsDescrambleInfo)
+ {
+ m_capabilities->bSupportsDescrambleInfo = supportsDescrambleInfo;
+ }
+
+ /// @brief To get with @ref SetSupportsDescrambleInfo changed values.
+ bool GetSupportsDescrambleInfo() const { return m_capabilities->bSupportsDescrambleInfo; }
+
+ /// @brief Set **true** if this addon-on supports asynchronous transfer of epg
+ /// events to Kodi using the callback function
+ /// @ref kodi::addon::CInstancePVRClient::EpgEventStateChange().
+ void SetSupportsAsyncEPGTransfer(bool supportsAsyncEPGTransfer)
+ {
+ m_capabilities->bSupportsAsyncEPGTransfer = supportsAsyncEPGTransfer;
+ }
+
+ /// @brief To get with @ref SetSupportsAsyncEPGTransfer changed values.
+ bool GetSupportsAsyncEPGTransfer() const { return m_capabilities->bSupportsAsyncEPGTransfer; }
+
+ /// @brief Set **true** if this addon-on supports retrieving size of recordings.
+ void SetSupportsRecordingSize(bool supportsRecordingSize)
+ {
+ m_capabilities->bSupportsRecordingSize = supportsRecordingSize;
+ }
+
+ /// @brief To get with @ref SetSupportsRecordingSize changed values.
+ bool GetSupportsRecordingSize() const { return m_capabilities->bSupportsRecordingSize; }
+
+ /// @brief Set **true** if this add-on supports delete of recordings stored
+ /// on the backend.
+ void SetSupportsRecordingsDelete(bool supportsRecordingsDelete)
+ {
+ m_capabilities->bSupportsRecordingsDelete = supportsRecordingsDelete;
+ }
+
+ /// @brief To get with @ref SetSupportsRecordingsDelete changed values.
+ bool GetSupportsRecordingsDelete() const { return m_capabilities->bSupportsRecordingsDelete; }
+
+ /// @brief **optional**\n
+ /// Set array containing the possible values for @ref PVRRecording::SetLifetime().
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_PVRTypeIntValue_Help
+ void SetRecordingsLifetimeValues(
+ const std::vector<PVRTypeIntValue>& recordingsLifetimeValues)
+ {
+ m_capabilities->iRecordingsLifetimesSize = 0;
+ for (unsigned int i = 0; i < recordingsLifetimeValues.size() &&
+ i < sizeof(m_capabilities->recordingsLifetimeValues);
+ ++i)
+ {
+ m_capabilities->recordingsLifetimeValues[i].iValue =
+ recordingsLifetimeValues[i].GetCStructure()->iValue;
+ strncpy(m_capabilities->recordingsLifetimeValues[i].strDescription,
+ recordingsLifetimeValues[i].GetCStructure()->strDescription,
+ sizeof(m_capabilities->recordingsLifetimeValues[i].strDescription) - 1);
+ ++m_capabilities->iRecordingsLifetimesSize;
+ }
+ }
+
+ /// @brief To get with @ref SetRecordingsLifetimeValues changed values.
+ std::vector<PVRTypeIntValue> GetRecordingsLifetimeValues() const
+ {
+ std::vector<PVRTypeIntValue> recordingsLifetimeValues;
+ for (unsigned int i = 0; i < m_capabilities->iRecordingsLifetimesSize; ++i)
+ recordingsLifetimeValues.emplace_back(
+ m_capabilities->recordingsLifetimeValues[i].iValue,
+ m_capabilities->recordingsLifetimeValues[i].strDescription);
+ return recordingsLifetimeValues;
+ }
+ ///@}
+
+private:
+ PVRCapabilities(PVR_ADDON_CAPABILITIES* capabilities) : m_capabilities(capabilities) {}
+
+ PVR_ADDON_CAPABILITIES* m_capabilities;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_General_Inputstream_PVRStreamProperty class PVRStreamProperty
+/// @ingroup cpp_kodi_addon_pvr_Defs_General_Inputstream
+/// @brief **PVR stream property value handler**\n
+/// To set for Kodi wanted stream properties.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_General_Inputstream_PVRStreamProperty_Help
+///
+///---------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// ...
+///
+/// PVR_ERROR CMyPVRInstance::GetChannelStreamProperties(const kodi::addon::PVRChannel& channel,
+/// std::vector<kodi::addon::PVRStreamProperty>& properties)
+/// {
+/// ...
+/// properties.emplace_back(PVR_STREAM_PROPERTY_INPUTSTREAM, "inputstream.adaptive");
+/// return PVR_ERROR_NO_ERROR;
+/// }
+///
+/// ...
+/// ~~~~~~~~~~~~~
+///
+///
+/// **Example 2:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// ...
+///
+/// PVR_ERROR CMyPVRInstance::GetChannelStreamProperties(const kodi::addon::PVRChannel& channel,
+/// std::vector<kodi::addon::PVRStreamProperty>& properties)
+/// {
+/// ...
+/// kodi::addon::PVRStreamProperty property;
+/// property.SetName(PVR_STREAM_PROPERTY_INPUTSTREAM);
+/// property.SetValue("inputstream.adaptive");
+/// properties.emplace_back(property);
+/// return PVR_ERROR_NO_ERROR;
+/// }
+///
+/// ...
+/// ~~~~~~~~~~~~~
+///
+///@{
+class PVRStreamProperty : public CStructHdl<PVRStreamProperty, PVR_NAMED_VALUE>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRStreamProperty(const PVRStreamProperty& data) : CStructHdl(data) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_General_Inputstream_PVRStreamProperty_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_General_Inputstream_PVRStreamProperty
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_General_Inputstream_PVRStreamProperty :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Name** | `int` | @ref PVRStreamProperty::SetValue "SetName" | @ref PVRStreamProperty::GetName "GetName"
+ /// | **Value** | `std::string` | @ref PVRStreamProperty::SetValue "SetValue" | @ref PVRStreamProperty::GetValue "GetValue"
+ ///
+ /// @remark Further can there be used his class constructor to set values.
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_General_Inputstream_PVRStreamProperty
+ ///@{
+
+ /// @brief Default class constructor.
+ ///
+ /// @note Values must be set afterwards.
+ PVRStreamProperty() = default;
+
+ /// @brief Class constructor with integrated value set.
+ ///
+ /// @param[in] name Type identification
+ /// @param[in] value Type used property value
+ PVRStreamProperty(const std::string& name, const std::string& value)
+ {
+ SetName(name);
+ SetValue(value);
+ }
+
+ /// @brief To set with the identification name.
+ void SetName(const std::string& name)
+ {
+ strncpy(m_cStructure->strName, name.c_str(), sizeof(m_cStructure->strName) - 1);
+ }
+
+ /// @brief To get with the identification name.
+ std::string GetName() const { return m_cStructure->strName; }
+
+ /// @brief To set with the used property value.
+ void SetValue(const std::string& value)
+ {
+ strncpy(m_cStructure->strValue, value.c_str(), sizeof(m_cStructure->strValue) - 1);
+ }
+
+ /// @brief To get with the used property value.
+ std::string GetValue() const { return m_cStructure->strValue; }
+ ///@}
+
+private:
+ PVRStreamProperty(const PVR_NAMED_VALUE* data) : CStructHdl(data) {}
+ PVRStreamProperty(PVR_NAMED_VALUE* data) : CStructHdl(data) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/MenuHook.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/MenuHook.h
new file mode 100644
index 0000000..053a4d5
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/MenuHook.h
@@ -0,0 +1,130 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/addon-instance/pvr/pvr_menu_hook.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Definitions group 7 - Menu hook
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook class PVRMenuhook
+/// @ingroup cpp_kodi_addon_pvr_Defs_Menuhook
+/// @brief **Context menu hook**\n
+/// Menu hooks that are available in the context menus while playing a stream via this add-on.
+/// And in the Live TV settings dialog.
+///
+/// Possible menu's given to Kodi.
+///
+/// This can be becomes used on this, if @ref kodi::addon::CInstancePVRClient::AddMenuHook()
+/// was set to related type:
+/// - @ref kodi::addon::CInstancePVRClient::CallSettingsMenuHook()
+/// - @ref kodi::addon::CInstancePVRClient::CallChannelMenuHook()
+/// - @ref kodi::addon::CInstancePVRClient::CallEPGMenuHook()
+/// - @ref kodi::addon::CInstancePVRClient::CallRecordingMenuHook()
+/// - @ref kodi::addon::CInstancePVRClient::CallTimerMenuHook()
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook_Help
+///
+///@{
+class PVRMenuhook : public CStructHdl<PVRMenuhook, PVR_MENUHOOK>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook
+ /// @brief Optional class constructor with value set.
+ ///
+ /// @param[in] hookId This hook's identifier
+ /// @param[in] localizedStringId Localized string identifier
+ /// @param[in] category Category of menu hook, defined with @ref PVR_MENUHOOK_CAT
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// AddMenuHook(kodi::addon::PVRMenuhook(1, 30001, PVR_MENUHOOK_CHANNEL));
+ /// ~~~~~~~~~~~~~
+ ///
+ PVRMenuhook(unsigned int hookId, unsigned int localizedStringId, PVR_MENUHOOK_CAT category)
+ {
+ m_cStructure->iHookId = hookId;
+ m_cStructure->iLocalizedStringId = localizedStringId;
+ m_cStructure->category = category;
+ }
+
+ /*! \cond PRIVATE */
+ PVRMenuhook()
+ {
+ m_cStructure->iHookId = 0;
+ m_cStructure->iLocalizedStringId = 0;
+ m_cStructure->category = PVR_MENUHOOK_UNKNOWN;
+ }
+ PVRMenuhook(const PVRMenuhook& data) : CStructHdl(data) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |------|------|----------|----------|-----------
+ /// | **This hook's identifier** | `unsigned int` | @ref PVRMenuhook::SetHookId "SetHookId" | @ref PVRMenuhook::GetHookId "GetHookId" | *required to set*
+ /// | **Localized string Identifier** | `unsigned int` | @ref PVRMenuhook::SetLocalizedStringId "SetLocalizedStringId" | @ref PVRMenuhook::GetLocalizedStringId "GetLocalizedStringId" | *required to set*
+ /// | **Category of menu hook** | @ref PVR_MENUHOOK_CAT | @ref PVRMenuhook::SetCategory "SetCategory" | @ref PVRMenuhook::GetCategory "GetCategory" | *required to set*
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Menuhook_PVRMenuhook
+ ///@{
+
+ /// @brief **required**\n
+ /// This hook's identifier.
+ void SetHookId(unsigned int hookId) { m_cStructure->iHookId = hookId; }
+
+ /// @brief To get with @ref SetHookId() changed values.
+ unsigned int GetHookId() const { return m_cStructure->iHookId; }
+
+ /// @brief **required**\n
+ /// The id of the label for this hook in @ref kodi::GetLocalizedString().
+ void SetLocalizedStringId(unsigned int localizedStringId)
+ {
+ m_cStructure->iLocalizedStringId = localizedStringId;
+ }
+
+ /// @brief To get with @ref SetLocalizedStringId() changed values.
+ unsigned int GetLocalizedStringId() const { return m_cStructure->iLocalizedStringId; }
+
+ /// @brief **required**\n
+ /// Category of menu hook.
+ void SetCategory(PVR_MENUHOOK_CAT category) { m_cStructure->category = category; }
+
+ /// @brief To get with @ref SetCategory() changed values.
+ PVR_MENUHOOK_CAT GetCategory() const { return m_cStructure->category; }
+ ///@}
+
+private:
+ PVRMenuhook(const PVR_MENUHOOK* data) : CStructHdl(data) {}
+ PVRMenuhook(PVR_MENUHOOK* data) : CStructHdl(data) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Providers.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Providers.h
new file mode 100644
index 0000000..8ccd7a7
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Providers.h
@@ -0,0 +1,195 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/addon-instance/pvr.h"
+#include "../../tools/StringUtils.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Definitions group 2 - PVR provider
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_PVRProvider class PVRProvider
+/// @ingroup cpp_kodi_addon_pvr_Defs_Provider
+/// @brief **Provider data structure**\n
+/// Representation of a provider.
+///
+/// This is used to store all the necessary provider data and can
+/// either provide the necessary data from / to Kodi for the associated
+/// functions or can also be used in the addon to store its data.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_PVRProvider_Help
+///
+///@{
+class PVRProvider : public CStructHdl<PVRProvider, PVR_PROVIDER>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRProvider() { memset(m_cStructure, 0, sizeof(PVR_PROVIDER)); }
+ PVRProvider(const PVRProvider& provider) : CStructHdl(provider) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_PVRProvider_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_PVRProvider
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_PVRProvider :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |------|------|----------|----------|-----------
+ /// | **Unique id** | `unsigned int` | @ref PVRProvider::SetUniqueId "SetUniqueId" | @ref PVRProvider::GetUniqueId "GetUniqueId" | *required to set*
+ /// | **Provider name** | `std::string` | @ref PVRProvider::SetName "SetName" | @ref PVRProvider::GetName "GetName" | *required to set*
+ /// | **Provider type** | @ref PVR_PROVIDER_TYPE | @ref PVRProvider::SetType "SetType" | @ref PVRProvider::GetType "GetType" | *optional*
+ /// | **Icon path** | `std::string` | @ref PVRProvider::SetIconPath "SetIconPath" | @ref PVRProvider::GetIconPath "GetIconPath" | *optional*
+ /// | **Countries** | `std::vector<std::string>` | @ref PVRProvider::SetCountries "SetCountries" | @ref PVRProvider::GetCountries "GetCountries" | *optional*
+ /// | **Languages** | `std::vector<std::string>` | @ref PVRProvider::SetLanguages "SetLanguages" | @ref PVRProvider::GetLanguages "GetLanguages" | *optional*
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_PVRProvider
+ ///@{
+
+ /// @brief **required**\n
+ /// Unique identifier for this provider.
+ void SetUniqueId(unsigned int uniqueId) { m_cStructure->iUniqueId = uniqueId; }
+
+ /// @brief To get with @ref SetUniqueId changed values.
+ unsigned int GetUniqueId() const { return m_cStructure->iUniqueId; }
+
+ /// @brief **required**\n
+ /// Name given to this provider.
+ void SetName(const std::string& name)
+ {
+ strncpy(m_cStructure->strName, name.c_str(), sizeof(m_cStructure->strName) - 1);
+ }
+
+ /// @brief To get with @ref SetName changed values.
+ std::string GetName() const { return m_cStructure->strName; }
+
+ /// @brief **optional**\n
+ /// Provider type.
+ ///
+ /// Set to @ref PVR_PROVIDER_TYPE_UNKNOWN if the type cannot be
+ /// determined.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRProvider tag;
+ /// tag.SetType(PVR_PROVIDER_TYPE_SATELLITE);
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetType(PVR_PROVIDER_TYPE type) { m_cStructure->type = type; }
+
+ /// @brief To get with @ref SetType changed values
+ PVR_PROVIDER_TYPE GetType() const { return m_cStructure->type; }
+
+ /// @brief **optional**\n
+ /// Path to the provider icon (if present).
+ void SetIconPath(const std::string& iconPath)
+ {
+ strncpy(m_cStructure->strIconPath, iconPath.c_str(), sizeof(m_cStructure->strIconPath) - 1);
+ }
+
+ /// @brief To get with @ref SetIconPath changed values.
+ std::string GetIconPath() const { return m_cStructure->strIconPath; }
+ ///@}
+
+ /// @brief **optional**\n
+ /// The country codes for the provider.
+ ///
+ /// @note ISO 3166 country codes required (e.g 'GB,IE,CA').
+ void SetCountries(const std::vector<std::string>& countries)
+ {
+ const std::string str = tools::StringUtils::Join(countries, PROVIDER_STRING_TOKEN_SEPARATOR);
+ strncpy(m_cStructure->strCountries, str.c_str(), sizeof(m_cStructure->strCountries) - 1);
+ }
+
+ /// @brief To get with @ref SetCountries changed values.
+ std::vector<std::string> GetCountries() const
+ {
+ return tools::StringUtils::Split(m_cStructure->strCountries, PROVIDER_STRING_TOKEN_SEPARATOR);
+ }
+ ///@}
+
+ /// @brief **optional**\n
+ /// The language codes for the provider.
+ ///
+ /// @note RFC 5646 standard codes required (e.g.: 'en_GB,fr_CA').
+ void SetLanguages(const std::vector<std::string>& languages)
+ {
+ const std::string str = tools::StringUtils::Join(languages, PROVIDER_STRING_TOKEN_SEPARATOR);
+ strncpy(m_cStructure->strLanguages, str.c_str(), sizeof(m_cStructure->strLanguages) - 1);
+ }
+
+ /// @brief To get with @ref SetLanguages changed values.
+ std::vector<std::string> GetLanguages() const
+ {
+ return tools::StringUtils::Split(m_cStructure->strLanguages, PROVIDER_STRING_TOKEN_SEPARATOR);
+ }
+ ///@}
+
+private:
+ PVRProvider(const PVR_PROVIDER* provider) : CStructHdl(provider) {}
+ PVRProvider(PVR_PROVIDER* provider) : CStructHdl(provider) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_PVRProvidersResultSet class PVRProvidersResultSet
+/// @ingroup cpp_kodi_addon_pvr_Defs_PVRProvider
+/// @brief **PVR add-on provider transfer class**\n
+/// To transfer the content of @ref kodi::addon::CInstancePVRClient::GetProviders().
+///
+///@{
+class PVRProvidersResultSet
+{
+public:
+ /*! \cond PRIVATE */
+ PVRProvidersResultSet() = delete;
+ PVRProvidersResultSet(const AddonInstance_PVR* instance, PVR_HANDLE handle)
+ : m_instance(instance), m_handle(handle)
+ {
+ }
+ /*! \endcond */
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_PVRProvidersResultSet
+ ///@{
+
+ /// @brief To add and give content from addon to Kodi on related call.
+ ///
+ /// @param[in] provider The to transferred data.
+ void Add(const kodi::addon::PVRProvider& provider)
+ {
+ m_instance->toKodi->TransferProviderEntry(m_instance->toKodi->kodiInstance, m_handle, provider);
+ }
+
+ ///@}
+
+private:
+ const AddonInstance_PVR* m_instance = nullptr;
+ const PVR_HANDLE m_handle;
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Recordings.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Recordings.h
new file mode 100644
index 0000000..b7f498a
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Recordings.h
@@ -0,0 +1,545 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/addon-instance/pvr.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Definitions group 5 - PVR recordings
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Recording_PVRRecording class PVRRecording
+/// @ingroup cpp_kodi_addon_pvr_Defs_Recording
+/// @brief **Data structure with available recordings data**\n
+/// With this, recordings related data are transferred between addon and Kodi
+/// and can also be used by the addon itself.
+///
+/// The related values here are automatically initiated to defaults and need
+/// only be set if supported and used.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_Recording_PVRRecording_Help
+///
+///@{
+class PVRRecording : public CStructHdl<PVRRecording, PVR_RECORDING>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRRecording()
+ {
+ m_cStructure->iSeriesNumber = PVR_RECORDING_INVALID_SERIES_EPISODE;
+ m_cStructure->iEpisodeNumber = PVR_RECORDING_INVALID_SERIES_EPISODE;
+ m_cStructure->recordingTime = 0;
+ m_cStructure->iDuration = PVR_RECORDING_VALUE_NOT_AVAILABLE;
+ m_cStructure->iPriority = PVR_RECORDING_VALUE_NOT_AVAILABLE;
+ m_cStructure->iLifetime = PVR_RECORDING_VALUE_NOT_AVAILABLE;
+ m_cStructure->iGenreType = PVR_RECORDING_VALUE_NOT_AVAILABLE;
+ m_cStructure->iGenreSubType = PVR_RECORDING_VALUE_NOT_AVAILABLE;
+ m_cStructure->iPlayCount = PVR_RECORDING_VALUE_NOT_AVAILABLE;
+ m_cStructure->iLastPlayedPosition = PVR_RECORDING_VALUE_NOT_AVAILABLE;
+ m_cStructure->bIsDeleted = false;
+ m_cStructure->iEpgEventId = 0;
+ m_cStructure->iChannelUid = PVR_RECORDING_VALUE_NOT_AVAILABLE;
+ m_cStructure->channelType = PVR_RECORDING_CHANNEL_TYPE_UNKNOWN;
+ m_cStructure->iFlags = 0;
+ m_cStructure->sizeInBytes = PVR_RECORDING_VALUE_NOT_AVAILABLE;
+ }
+ PVRRecording(const PVRRecording& recording) : CStructHdl(recording) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Recording_PVRRecording_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Recording_PVRRecording
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_Recording_PVRRecording :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |------|------|----------|----------|-----------
+ /// | **Recording id** | `std::string` | @ref PVRRecording::SetRecordingId "SetRecordingId" | @ref PVRRecording::GetRecordingId "GetRecordingId" | *required to set*
+ /// | **Title** | `std::string` | @ref PVRRecording::SetTitle "SetTitle" | @ref PVRRecording::GetTitle "GetTitle" | *required to set*
+ /// | **Episode name** | `std::string` | @ref PVRRecording::SetEpisodeName "SetEpisodeName" | @ref PVRRecording::GetEpisodeName "GetEpisodeName" | *optional*
+ /// | **Series number** | `int` | @ref PVRRecording::SetSeriesNumber "SetSeriesNumber" | @ref PVRRecording::GetSeriesNumber "GetSeriesNumber" | *optional*
+ /// | **Episode number** | `int` | @ref PVRRecording::SetEpisodeNumber "SetEpisodeNumber" | @ref PVRRecording::GetEpisodeNumber "GetEpisodeNumber" | *optional*
+ /// | **Year** | `int` | @ref PVRRecording::SetYear "SetYear" | @ref PVRRecording::GetYear "GetYear" | *optional*
+ /// | **Directory** | `std::string` | @ref PVRRecording::SetDirectory "SetDirectory" | @ref PVRRecording::GetDirectory "GetDirectory" | *optional*
+ /// | **Plot outline** | `std::string` | @ref PVRRecording::SetPlotOutline "SetPlotOutline" | @ref PVRRecording::GetPlotOutline "GetPlotOutline" | *optional*
+ /// | **Plot** | `std::string` | @ref PVRRecording::SetPlot "SetPlot" | @ref PVRRecording::GetPlot "GetPlot" | *optional*
+ /// | **Genre description** | `std::string` | @ref PVRRecording::SetGenreDescription "SetGenreDescription" | @ref PVRRecording::GetGenreDescription "GetGenreDescription" | *optional*
+ /// | **Channel name** | `std::string` | @ref PVRRecording::SetChannelName "SetChannelName" | @ref PVRRecording::GetChannelName "GetChannelName" | *optional*
+ /// | **Icon path** | `std::string` | @ref PVRRecording::SetIconPath "SetIconPath" | @ref PVRRecording::GetIconPath "GetIconPath" | *optional*
+ /// | **Thumbnail path** | `std::string` | @ref PVRRecording::SetThumbnailPath "SetThumbnailPath" | @ref PVRRecording::GetThumbnailPath "GetThumbnailPath" | *optional*
+ /// | **Fanart path** | `std::string` | @ref PVRRecording::SetFanartPath "SetFanartPath" | @ref PVRRecording::GetFanartPath "GetFanartPath" | *optional*
+ /// | **Recording time** | `time_t` | @ref PVRRecording::SetRecordingTime "SetRecordingTime" | @ref PVRRecording::GetRecordingTime "GetRecordingTime" | *optional*
+ /// | **Duration** | `int` | @ref PVRRecording::SetDuration "SetDuration" | @ref PVRRecording::GetDuration "GetDuration" | *optional*
+ /// | **Priority** | `int` | @ref PVRRecording::SetPriority "SetPriority" | @ref PVRRecording::GetPriority "GetPriority" | *optional*
+ /// | **Lifetime** | `int` | @ref PVRRecording::SetLifetime "SetLifetime" | @ref PVRRecording::GetLifetime "GetLifetime" | *optional*
+ /// | **Genre type** | `int` | @ref PVRRecording::SetGenreType "SetGenreType" | @ref PVRRecording::GetGenreType "GetGenreType" | *optional*
+ /// | **Genre sub type** | `int` | @ref PVRRecording::SetGenreSubType "SetGenreSubType" | @ref PVRRecording::GetGenreSubType "GetGenreSubType" | *optional*
+ /// | **Play count** | `int` | @ref PVRRecording::SetPlayCount "SetPlayCount" | @ref PVRRecording::GetPlayCount "GetPlayCount" | *optional*
+ /// | **Last played position** | `int` | @ref PVRRecording::SetLastPlayedPosition "SetLastPlayedPosition" | @ref PVRRecording::GetLastPlayedPosition "GetLastPlayedPosition" | *optional*
+ /// | **Is deleted** | `bool` | @ref PVRRecording::SetIsDeleted "SetIsDeleted" | @ref PVRRecording::GetIsDeleted "GetIsDeleted" | *optional*
+ /// | **EPG event id** | `unsigned int` | @ref PVRRecording::SetEPGEventId "SetEPGEventId" | @ref PVRRecording::GetEPGEventId "GetEPGEventId" | *optional*
+ /// | **Channel unique id** | `int` | @ref PVRRecording::SetChannelUid "SetChannelUid" | @ref PVRRecording::GetChannelUid "GetChannelUid" | *optional*
+ /// | **Channel type** | @ref PVR_RECORDING_CHANNEL_TYPE | @ref PVRRecording::SetChannelType "SetChannelType" | @ref PVRRecording::GetChannelType "GetChannelType" | *optional*
+ /// | **First aired** | `std::string` | @ref PVRRecording::SetFirstAired "SetFirstAired" | @ref PVRRecording::GetFirstAired "GetFirstAired" | *optional*
+ /// | **Flags** | `std::string` | @ref PVRRecording::SetFlags "SetFlags" | @ref PVRRecording::GetFlags "GetFlags" | *optional*
+ /// | **Size in bytes** | `std::string` | @ref PVRRecording::SetSizeInBytes "SetSizeInBytes" | @ref PVRRecording::GetSizeInBytes "GetSizeInBytes" | *optional*
+ /// | **Client provider unique identifier** | `int` | @ref PVRChannel::SetClientProviderUid "SetClientProviderUid" | @ref PVRTimer::GetClientProviderUid "GetClientProviderUid" | *optional*
+ /// | **Provider name** | `std::string` | @ref PVRChannel::SetProviderName "SetProviderlName" | @ref PVRChannel::GetProviderName "GetProviderName" | *optional*
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Recording_PVRRecording
+ ///@{
+
+ /// @brief **required**\n
+ /// Unique identifier of the recording on the client.
+ void SetRecordingId(const std::string& recordingId)
+ {
+ strncpy(m_cStructure->strRecordingId, recordingId.c_str(),
+ sizeof(m_cStructure->strRecordingId) - 1);
+ }
+
+ /// @brief To get with @ref SetRecordingId changed values.
+ std::string GetRecordingId() const { return m_cStructure->strRecordingId; }
+
+ /// @brief **required**\n
+ /// The title of this recording.
+ void SetTitle(const std::string& title)
+ {
+ strncpy(m_cStructure->strTitle, title.c_str(), sizeof(m_cStructure->strTitle) - 1);
+ }
+
+ /// @brief To get with @ref SetTitle changed values.
+ std::string GetTitle() const { return m_cStructure->strTitle; }
+
+ /// @brief **optional**\n
+ /// Episode name (also known as subtitle).
+ void SetEpisodeName(const std::string& episodeName)
+ {
+ strncpy(m_cStructure->strEpisodeName, episodeName.c_str(),
+ sizeof(m_cStructure->strEpisodeName) - 1);
+ }
+
+ /// @brief To get with @ref SetEpisodeName changed values.
+ std::string GetEpisodeName() const { return m_cStructure->strEpisodeName; }
+
+ /// @brief **optional**\n
+ /// Series number (usually called season).
+ ///
+ /// Set to "0" for specials/pilot. For 'invalid' see @ref SetEpisodeNumber or set to -1.
+ void SetSeriesNumber(int seriesNumber) { m_cStructure->iSeriesNumber = seriesNumber; }
+
+ /// @brief To get with @ref SetSeriesNumber changed values.
+ int GetSeriesNumber() const { return m_cStructure->iSeriesNumber; }
+
+ /// @brief **optional**\n
+ /// Eepisode number within the "iSeriesNumber" season.
+ ///
+ /// For 'invalid' set to -1 or seriesNumber=episodeNumber=0 to show both are invalid.
+ void SetEpisodeNumber(int episodeNumber) { m_cStructure->iEpisodeNumber = episodeNumber; }
+
+ /// @brief To get with @ref SetEpisodeNumber changed values.
+ int GetEpisodeNumber() const { return m_cStructure->iEpisodeNumber; }
+
+ /// @brief **optional**\n
+ /// Year of first release (use to identify a specific movie re-make) / first
+ /// airing for TV shows.
+ ///
+ /// Set to '0' for invalid.
+ void SetYear(int year) { m_cStructure->iYear = year; }
+
+ /// @brief To get with @ref SetYear changed values.
+ int GetYear() const { return m_cStructure->iYear; }
+
+ /// @brief **optional**\n
+ ///
+ /// Directory of this recording on the client.
+ void SetDirectory(const std::string& directory)
+ {
+ strncpy(m_cStructure->strDirectory, directory.c_str(), sizeof(m_cStructure->strDirectory) - 1);
+ }
+
+ /// @brief To get with @ref SetDirectory changed values.
+ std::string GetDirectory() const { return m_cStructure->strDirectory; }
+
+ /// @brief **optional**\n
+ /// Plot outline name.
+ void SetPlotOutline(const std::string& plotOutline)
+ {
+ strncpy(m_cStructure->strPlotOutline, plotOutline.c_str(),
+ sizeof(m_cStructure->strPlotOutline) - 1);
+ }
+
+ /// @brief To get with @ref SetPlotOutline changed values.
+ std::string GetPlotOutline() const { return m_cStructure->strPlotOutline; }
+
+ /// @brief **optional**\n
+ /// Plot name.
+ void SetPlot(const std::string& plot)
+ {
+ strncpy(m_cStructure->strPlot, plot.c_str(), sizeof(m_cStructure->strPlot) - 1);
+ }
+
+ /// @brief To get with @ref SetPlot changed values.
+ std::string GetPlot() const { return m_cStructure->strPlot; }
+
+ /// @brief **optional**\n
+ /// Channel name.
+ void SetChannelName(const std::string& channelName)
+ {
+ strncpy(m_cStructure->strChannelName, channelName.c_str(),
+ sizeof(m_cStructure->strChannelName) - 1);
+ }
+
+ /// @brief To get with @ref SetChannelName changed values.
+ std::string GetChannelName() const { return m_cStructure->strChannelName; }
+
+ /// @brief **optional**\n
+ /// Channel logo (icon) path.
+ void SetIconPath(const std::string& iconPath)
+ {
+ strncpy(m_cStructure->strIconPath, iconPath.c_str(), sizeof(m_cStructure->strIconPath) - 1);
+ }
+
+ /// @brief To get with @ref SetIconPath changed values.
+ std::string GetIconPath() const { return m_cStructure->strIconPath; }
+
+ /// @brief **optional**\n
+ /// Thumbnail path.
+ void SetThumbnailPath(const std::string& thumbnailPath)
+ {
+ strncpy(m_cStructure->strThumbnailPath, thumbnailPath.c_str(),
+ sizeof(m_cStructure->strThumbnailPath) - 1);
+ }
+
+ /// @brief To get with @ref SetThumbnailPath changed values.
+ std::string GetThumbnailPath() const { return m_cStructure->strThumbnailPath; }
+
+ /// @brief **optional**\n
+ /// Fanart path.
+ void SetFanartPath(const std::string& fanartPath)
+ {
+ strncpy(m_cStructure->strFanartPath, fanartPath.c_str(),
+ sizeof(m_cStructure->strFanartPath) - 1);
+ }
+
+ /// @brief To get with @ref SetFanartPath changed values.
+ std::string GetFanartPath() const { return m_cStructure->strFanartPath; }
+
+ /// @brief **optional**\n
+ /// Start time of the recording.
+ void SetRecordingTime(time_t recordingTime) { m_cStructure->recordingTime = recordingTime; }
+
+ /// @brief To get with @ref SetRecordingTime changed values.
+ time_t GetRecordingTime() const { return m_cStructure->recordingTime; }
+
+ /// @brief **optional**\n
+ /// Duration of the recording in seconds.
+ void SetDuration(int duration) { m_cStructure->iDuration = duration; }
+
+ /// @brief To get with @ref SetDuration changed values.
+ int GetDuration() const { return m_cStructure->iDuration; }
+
+ /// @brief **optional**\n
+ /// Priority of this recording (from 0 - 100).
+ void SetPriority(int priority) { m_cStructure->iPriority = priority; }
+
+ /// @brief To get with @ref SetPriority changed values.
+ int GetPriority() const { return m_cStructure->iPriority; }
+
+ /// @brief **optional**\n
+ /// Life time in days of this recording.
+ void SetLifetime(int lifetime) { m_cStructure->iLifetime = lifetime; }
+
+ /// @brief To get with @ref SetLifetime changed values.
+ int GetLifetime() const { return m_cStructure->iLifetime; }
+
+ /// @brief **optional**\n
+ /// Genre type.
+ ///
+ /// Use @ref EPG_GENRE_USE_STRING if type becomes given by @ref SetGenreDescription.
+ ///
+ /// @note If confirmed that backend brings the types in [ETSI EN 300 468](https://www.etsi.org/deliver/etsi_en/300400_300499/300468/01.14.01_60/en_300468v011401p.pdf)
+ /// conform values, can be @ref EPG_EVENT_CONTENTMASK ignored and to set here
+ /// with backend value.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example 1:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRRecording tag;
+ /// tag.SetGenreType(EPG_EVENT_CONTENTMASK_MOVIEDRAMA);
+ /// ~~~~~~~~~~~~~
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example 2** (in case of other, not ETSI EN 300 468 conform genre types):
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRRecording tag;
+ /// tag.SetGenreType(EPG_GENRE_USE_STRING);
+ /// tag.SetGenreDescription("My special genre name"); // Should use (if possible) kodi::GetLocalizedString(...) to have match user language.
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetGenreType(int genreType) { m_cStructure->iGenreType = genreType; }
+
+ /// @brief To get with @ref SetGenreType changed values.
+ int GetGenreType() const { return m_cStructure->iGenreType; }
+
+ /// @brief **optional**\n
+ /// Genre sub type.
+ ///
+ /// Subtypes groups related to set by @ref SetGenreType:
+ /// | Main genre type | List with available sub genre types
+ /// |-----------------|-----------------------------------------
+ /// | @ref EPG_EVENT_CONTENTMASK_UNDEFINED | Nothing, should be 0
+ /// | @ref EPG_EVENT_CONTENTMASK_MOVIEDRAMA | @ref EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA
+ /// | @ref EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS | @ref EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS
+ /// | @ref EPG_EVENT_CONTENTMASK_SHOW | @ref EPG_EVENT_CONTENTSUBMASK_SHOW
+ /// | @ref EPG_EVENT_CONTENTMASK_SPORTS | @ref EPG_EVENT_CONTENTSUBMASK_SPORTS
+ /// | @ref EPG_EVENT_CONTENTMASK_CHILDRENYOUTH | @ref EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH
+ /// | @ref EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE | @ref EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE
+ /// | @ref EPG_EVENT_CONTENTMASK_ARTSCULTURE | @ref EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE
+ /// | @ref EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS | @ref EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS
+ /// | @ref EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE | @ref EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE
+ /// | @ref EPG_EVENT_CONTENTMASK_LEISUREHOBBIES | @ref EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES
+ /// | @ref EPG_EVENT_CONTENTMASK_SPECIAL | @ref EPG_EVENT_CONTENTSUBMASK_SPECIAL
+ /// | @ref EPG_EVENT_CONTENTMASK_USERDEFINED | Can be defined by you
+ /// | @ref EPG_GENRE_USE_STRING | **Kodi's own value**, which declares that the type with @ref SetGenreDescription is given.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRRecording tag;
+ /// tag.SetGenreType(EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE);
+ /// tag.SetGenreSubType(EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE_JAZZ);
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetGenreSubType(int genreSubType) { m_cStructure->iGenreSubType = genreSubType; }
+
+ /// @brief To get with @ref SetGenreSubType changed values.
+ int GetGenreSubType() const { return m_cStructure->iGenreSubType; }
+
+ /// @brief **optional**\n
+ /// To set own genre description name.
+ ///
+ /// Will be used only when genreType == @ref EPG_GENRE_USE_STRING or
+ /// genreSubType == @ref EPG_GENRE_USE_STRING.
+ ///
+ /// Use @ref EPG_STRING_TOKEN_SEPARATOR to separate different genres.
+ ///
+ /// In case of other, not [ETSI EN 300 468](https://www.etsi.org/deliver/etsi_en/300400_300499/300468/01.14.01_60/en_300468v011401p.pdf)
+ /// conform genre types or something special.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRRecording tag;
+ /// tag.SetGenreType(EPG_GENRE_USE_STRING);
+ /// tag.SetGenreDescription("Action" + EPG_STRING_TOKEN_SEPARATOR + "Thriller");
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetGenreDescription(const std::string& genreDescription)
+ {
+ strncpy(m_cStructure->strGenreDescription, genreDescription.c_str(),
+ sizeof(m_cStructure->strGenreDescription) - 1);
+ }
+
+ /// @brief To get with @ref SetGenreDescription changed values.
+ std::string GetGenreDescription() const { return m_cStructure->strGenreDescription; }
+
+ /// @brief **optional**\n
+ /// Play count of this recording on the client.
+ void SetPlayCount(int playCount) { m_cStructure->iPlayCount = playCount; }
+
+ /// @brief To get with @ref SetPlayCount changed values.
+ int GetPlayCount() const { return m_cStructure->iPlayCount; }
+
+ /// @brief **optional**\n
+ /// Last played position of this recording on the client.
+ void SetLastPlayedPosition(int lastPlayedPosition)
+ {
+ m_cStructure->iLastPlayedPosition = lastPlayedPosition;
+ }
+
+ /// @brief To get with @ref SetLastPlayedPosition changed values.
+ int GetLastPlayedPosition() const { return m_cStructure->iLastPlayedPosition; }
+
+ /// @brief **optional**\n
+ /// Shows this recording is deleted and can be undelete.
+ void SetIsDeleted(int isDeleted) { m_cStructure->bIsDeleted = isDeleted; }
+
+ /// @brief To get with @ref SetIsDeleted changed values.
+ int GetIsDeleted() const { return m_cStructure->bIsDeleted; }
+
+ /// @brief **optional**\n
+ /// EPG event id associated with this recording. Valid ids must be greater than @ref EPG_TAG_INVALID_UID.
+ void SetEPGEventId(unsigned int epgEventId) { m_cStructure->iEpgEventId = epgEventId; }
+
+ /// @brief To get with @ref SetEPGEventId changed values.
+ unsigned int GetEPGEventId() const { return m_cStructure->iEpgEventId; }
+
+ /// @brief **optional**\n
+ /// Unique identifier of the channel for this recording. @ref PVR_CHANNEL_INVALID_UID
+ /// denotes that channel uid is not available.
+ void SetChannelUid(int channelUid) { m_cStructure->iChannelUid = channelUid; }
+
+ /// @brief To get with @ref SetChannelUid changed values
+ int GetChannelUid() const { return m_cStructure->iChannelUid; }
+
+ /// @brief **optional**\n
+ /// Channel type.
+ ///
+ /// Set to @ref PVR_RECORDING_CHANNEL_TYPE_UNKNOWN if the type cannot be
+ /// determined.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRRecording tag;
+ /// tag.SetChannelType(PVR_RECORDING_CHANNEL_TYPE_TV);
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetChannelType(PVR_RECORDING_CHANNEL_TYPE channelType)
+ {
+ m_cStructure->channelType = channelType;
+ }
+
+ /// @brief To get with @ref SetChannelType changed values
+ PVR_RECORDING_CHANNEL_TYPE GetChannelType() const { return m_cStructure->channelType; }
+
+ /// @brief **optional**\n
+ /// First aired date of this recording.
+ ///
+ /// Used only for display purposes. Specify in W3C date format "YYYY-MM-DD".
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRRecording tag;
+ /// tag.SetFirstAired(1982-10-22);
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetFirstAired(const std::string& firstAired)
+ {
+ strncpy(m_cStructure->strFirstAired, firstAired.c_str(),
+ sizeof(m_cStructure->strFirstAired) - 1);
+ }
+
+ /// @brief To get with @ref SetFirstAired changed values
+ std::string GetFirstAired() const { return m_cStructure->strFirstAired; }
+
+ /// @brief **optional**\n
+ /// Bit field of independent flags associated with the recording.
+ ///
+ /// See @ref cpp_kodi_addon_pvr_Defs_Recording_PVR_RECORDING_FLAG for
+ /// available bit flags.
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_Recording_PVR_RECORDING_FLAG
+ ///
+ void SetFlags(unsigned int flags) { m_cStructure->iFlags = flags; }
+
+ /// @brief To get with @ref SetFlags changed values.
+ unsigned int GetFlags() const { return m_cStructure->iFlags; }
+
+ /// @brief **optional**\n
+ /// Size of the recording in bytes.
+ void SetSizeInBytes(int64_t sizeInBytes) { m_cStructure->sizeInBytes = sizeInBytes; }
+
+ /// @brief To get with @ref SetSizeInBytes changed values.
+ int64_t GetSizeInBytes() const { return m_cStructure->sizeInBytes; }
+ ///@}
+
+ /// @brief **optional**\n
+ /// Unique identifier of the provider this channel belongs to.
+ ///
+ /// @ref PVR_PROVIDER_INVALID_UID denotes that provider uid is not available.
+ void SetClientProviderUid(int iClientProviderUid)
+ {
+ m_cStructure->iClientProviderUid = iClientProviderUid;
+ }
+
+ /// @brief To get with @ref SetClientProviderUid changed values
+ int GetClientProviderUid() const { return m_cStructure->iClientProviderUid; }
+
+ /// @brief **optional**\n
+ /// Name for the provider of this channel.
+ void SetProviderName(const std::string& providerName)
+ {
+ strncpy(m_cStructure->strProviderName, providerName.c_str(),
+ sizeof(m_cStructure->strProviderName) - 1);
+ }
+
+ /// @brief To get with @ref SetProviderName changed values.
+ std::string GetProviderName() const { return m_cStructure->strProviderName; }
+
+private:
+ PVRRecording(const PVR_RECORDING* recording) : CStructHdl(recording) {}
+ PVRRecording(PVR_RECORDING* recording) : CStructHdl(recording) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Recording_PVRRecordingsResultSet class PVRRecordingsResultSet
+/// @ingroup cpp_kodi_addon_pvr_Defs_Recording_PVRRecording
+/// @brief **PVR add-on recording transfer class**\n
+/// To transfer the content of @ref kodi::addon::CInstancePVRClient::GetRecordings().
+///
+/// @note This becomes only be used on addon call above, not usable outside on
+/// addon itself.
+///@{
+class PVRRecordingsResultSet
+{
+public:
+ /*! \cond PRIVATE */
+ PVRRecordingsResultSet() = delete;
+ PVRRecordingsResultSet(const AddonInstance_PVR* instance, PVR_HANDLE handle)
+ : m_instance(instance), m_handle(handle)
+ {
+ }
+ /*! \endcond */
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Recording_PVRRecordingsResultSet
+ ///@{
+
+ /// @brief To add and give content from addon to Kodi on related call.
+ ///
+ /// @param[in] tag The to transferred data.
+ void Add(const kodi::addon::PVRRecording& tag)
+ {
+ m_instance->toKodi->TransferRecordingEntry(m_instance->toKodi->kodiInstance, m_handle, tag);
+ }
+
+ ///@}
+
+private:
+ const AddonInstance_PVR* m_instance = nullptr;
+ const PVR_HANDLE m_handle;
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Stream.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Stream.h
new file mode 100644
index 0000000..4241c23
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Stream.h
@@ -0,0 +1,330 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/addon-instance/pvr/pvr_stream.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Definitions group 9 - PVR stream definitions (NOTE: Becomes replaced
+// in future by inputstream addon instance way)
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Stream_PVRCodec class PVRCodec
+/// @ingroup cpp_kodi_addon_pvr_Defs_Stream
+/// @brief **PVR codec identifier**\n
+/// Used to exchange the desired codec type between Kodi and addon.
+///
+/// @ref kodi::addon::CInstancePVRClient::GetCodecByName is used to get this data.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_Stream_PVRCodec_Help
+///
+///@{
+class PVRCodec : public CStructHdl<PVRCodec, PVR_CODEC>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRCodec()
+ {
+ m_cStructure->codec_type = PVR_CODEC_TYPE_UNKNOWN;
+ m_cStructure->codec_id = PVR_INVALID_CODEC_ID;
+ }
+ PVRCodec(const PVRCodec& type) : CStructHdl(type) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Stream_PVRCodec_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Stream_PVRCodec
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_Stream_PVRCodec :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Codec type** | @ref PVR_CODEC_TYPE | @ref PVRCodec::SetCodecType "SetCodecType" | @ref PVRCodec::GetCodecType "GetCodecType"
+ /// | **Codec identifier** | `unsigned int` | @ref PVRCodec::SetCodecId "SetCodecId" | @ref PVRCodec::GetCodecId "GetCodecId"
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Stream_PVRCodec
+ ///@{
+
+ /// @brief Codec type.
+ void SetCodecType(PVR_CODEC_TYPE codecType) { m_cStructure->codec_type = codecType; }
+
+ /// @brief To get with @ref SetCodecType() changed values.
+ PVR_CODEC_TYPE GetCodecType() const { return m_cStructure->codec_type; }
+
+ /// @brief Codec id.
+ ///
+ /// Related codec identifier, normally match the ffmpeg id's.
+ void SetCodecId(unsigned int codecId) { m_cStructure->codec_id = codecId; }
+
+ /// @brief To get with @ref SetCodecId() changed values.
+ unsigned int GetCodecId() const { return m_cStructure->codec_id; }
+ ///@}
+
+private:
+ PVRCodec(const PVR_CODEC& type) : CStructHdl(&type) {}
+ PVRCodec(const PVR_CODEC* type) : CStructHdl(type) {}
+ PVRCodec(PVR_CODEC* type) : CStructHdl(type) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties class PVRStreamProperties
+/// @ingroup cpp_kodi_addon_pvr_Defs_Stream
+/// @brief **PVR stream properties**\n
+/// All information about a respective stream is stored in this, so that Kodi
+/// can process the data given by the addon after demux.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties_Help
+///
+///@{
+class PVRStreamProperties
+ : public CStructHdl<PVRStreamProperties, PVR_STREAM_PROPERTIES::PVR_STREAM>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRStreamProperties() { memset(m_cStructure, 0, sizeof(PVR_STREAM_PROPERTIES::PVR_STREAM)); }
+ PVRStreamProperties(const PVRStreamProperties& type) : CStructHdl(type) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties
+ /// ----------------------------------------------------------------------------
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **PID** | `unsigned int` | @ref PVRStreamProperties::SetPID "SetPID" | @ref PVRStreamProperties::GetPID "GetPID"
+ /// | **Codec type** | @ref PVR_CODEC_TYPE | @ref PVRStreamProperties::SetCodecType "SetCodecType" | @ref PVRStreamProperties::GetCodecType "GetCodecType"
+ /// | **Codec identifier** | `unsigned int` | @ref PVRStreamProperties::SetCodecId "SetCodecId" | @ref PVRStreamProperties::GetCodecId "GetCodecId"
+ /// | **Language** | `std::string` | @ref PVRStreamProperties::SetLanguage "SetLanguage" | @ref PVRStreamProperties::GetLanguage "GetLanguage"
+ /// | **Subtitle info** | `int` | @ref PVRStreamProperties::SetSubtitleInfo "SetSubtitleInfo" | @ref PVRStreamProperties::GetSubtitleInfo "GetSubtitleInfo"
+ /// | **FPS scale** | `int` | @ref PVRStreamProperties::SetFPSScale "SetFPSScale" | @ref PVRStreamProperties::GetFPSScale "GetFPSScale"
+ /// | **FPS rate** | `int` | @ref PVRStreamProperties::SetFPSRate "SetFPSRate" | @ref PVRStreamProperties::GetFPSRate "GetFPSRate"
+ /// | **Height** | `int` | @ref PVRStreamProperties::SetHeight "SetHeight" | @ref PVRStreamProperties::GetHeight "GetHeight"
+ /// | **Width** | `int` | @ref PVRStreamProperties::SetWidth "SetWidth" | @ref PVRStreamProperties::GetWidth "GetWidth"
+ /// | **Aspect ratio** | `float` | @ref PVRStreamProperties::SetAspect "SetAspect" | @ref PVRStreamProperties::GetAspect "GetAspect"
+ /// | **Channels** | `int` | @ref PVRStreamProperties::SetChannels "SetChannels" | @ref PVRStreamProperties::GetChannels "GetChannels"
+ /// | **Samplerate** | `int` | @ref PVRStreamProperties::SetSampleRate "SetSampleRate" | @ref PVRStreamProperties::GetSampleRate "GetSampleRate"
+ /// | **Block align** | `int` | @ref PVRStreamProperties::SetBlockAlign "SetBlockAlign" | @ref PVRStreamProperties::GetBlockAlign "GetBlockAlign"
+ /// | **Bit rate** | `int` | @ref PVRStreamProperties::SetBitRate "SetBitRate" | @ref PVRStreamProperties::GetBitRate "GetBitRate"
+ /// | **Bits per sample** | `int` | @ref PVRStreamProperties::SetBitsPerSample "SetBitsPerSample" | @ref PVRStreamProperties::GetBitsPerSample "GetBitsPerSample"
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties
+ ///@{
+
+ /// @brief PID.
+ void SetPID(unsigned int pid) { m_cStructure->iPID = pid; }
+
+ /// @brief To get with @ref SetPID() changed values.
+ unsigned int GetPID() const { return m_cStructure->iPID; }
+
+ /// @brief Codec type this stream.
+ void SetCodecType(PVR_CODEC_TYPE codecType) { m_cStructure->iCodecType = codecType; }
+
+ /// @brief To get with @ref SetCodecType() changed values.
+ PVR_CODEC_TYPE GetCodecType() const { return m_cStructure->iCodecType; }
+
+ /// @brief Codec id of this stream.
+ void SetCodecId(unsigned int codecId) { m_cStructure->iCodecId = codecId; }
+
+ /// @brief To get with @ref SetCodecId() changed values.
+ unsigned int GetCodecId() const { return m_cStructure->iCodecId; }
+
+ /// @brief 3 letter language id.
+ void SetLanguage(const std::string& language)
+ {
+ if (language.size() > 3)
+ {
+ kodi::Log(ADDON_LOG_ERROR,
+ "PVRStreamProperties::%s: Language string size '%li' higher as needed 3", __func__,
+ language.size());
+ return;
+ }
+ m_cStructure->strLanguage[0] = language[0];
+ m_cStructure->strLanguage[1] = language[1];
+ m_cStructure->strLanguage[2] = language[2];
+ m_cStructure->strLanguage[3] = 0;
+ }
+
+ /// @brief To get with @ref SetLanguage() changed values.
+ std::string GetLanguage() const { return m_cStructure->strLanguage; }
+
+ /// @brief Subtitle Info
+ void SetSubtitleInfo(int subtitleInfo) { m_cStructure->iSubtitleInfo = subtitleInfo; }
+
+ /// @brief To get with @ref SetSubtitleInfo() changed values.
+ int GetSubtitleInfo() const { return m_cStructure->iSubtitleInfo; }
+
+ /// @brief To set scale of 1000 and a rate of 29970 will result in 29.97 fps.
+ void SetFPSScale(int fpsScale) { m_cStructure->iFPSScale = fpsScale; }
+
+ /// @brief To get with @ref SetFPSScale() changed values.
+ int GetFPSScale() const { return m_cStructure->iFPSScale; }
+
+ /// @brief FPS rate
+ void SetFPSRate(int fpsRate) { m_cStructure->iFPSRate = fpsRate; }
+
+ /// @brief To get with @ref SetFPSRate() changed values.
+ int GetFPSRate() const { return m_cStructure->iFPSRate; }
+
+ /// @brief Height of the stream reported by the demuxer
+ void SetHeight(int height) { m_cStructure->iHeight = height; }
+
+ /// @brief To get with @ref SetHeight() changed values.
+ int GetHeight() const { return m_cStructure->iHeight; }
+
+ /// @brief Width of the stream reported by the demuxer.
+ void SetWidth(int width) { m_cStructure->iWidth = width; }
+
+ /// @brief To get with @ref SetWidth() changed values.
+ int GetWidth() const { return m_cStructure->iWidth; }
+
+ /// @brief Display aspect ratio of the stream.
+ void SetAspect(float aspect) { m_cStructure->fAspect = aspect; }
+
+ /// @brief To get with @ref SetAspect() changed values.
+ float GetAspect() const { return m_cStructure->fAspect; }
+
+ /// @brief Amount of channels.
+ void SetChannels(int channels) { m_cStructure->iChannels = channels; }
+
+ /// @brief To get with @ref SetChannels() changed values.
+ int GetChannels() const { return m_cStructure->iChannels; }
+
+ /// @brief Sample rate.
+ void SetSampleRate(int sampleRate) { m_cStructure->iSampleRate = sampleRate; }
+
+ /// @brief To get with @ref SetSampleRate() changed values.
+ int GetSampleRate() const { return m_cStructure->iSampleRate; }
+
+ /// @brief Block alignment
+ void SetBlockAlign(int blockAlign) { m_cStructure->iBlockAlign = blockAlign; }
+
+ /// @brief To get with @ref SetBlockAlign() changed values.
+ int GetBlockAlign() const { return m_cStructure->iBlockAlign; }
+
+ /// @brief Bit rate.
+ void SetBitRate(int bitRate) { m_cStructure->iBitRate = bitRate; }
+
+ /// @brief To get with @ref SetBitRate() changed values.
+ int GetBitRate() const { return m_cStructure->iBitRate; }
+
+ /// @brief Bits per sample.
+ void SetBitsPerSample(int bitsPerSample) { m_cStructure->iBitsPerSample = bitsPerSample; }
+
+ /// @brief To get with @ref SetBitsPerSample() changed values.
+ int GetBitsPerSample() const { return m_cStructure->iBitsPerSample; }
+ ///@}
+
+private:
+ PVRStreamProperties(const PVR_STREAM_PROPERTIES::PVR_STREAM* type) : CStructHdl(type) {}
+ PVRStreamProperties(PVR_STREAM_PROPERTIES::PVR_STREAM* type) : CStructHdl(type) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Stream_PVRStreamTimes class PVRStreamTimes
+/// @ingroup cpp_kodi_addon_pvr_Defs_Stream
+/// @brief **Times of playing stream (Live TV and recordings)**\n
+/// This class is used to transfer the necessary data when
+/// @ref kodi::addon::PVRStreamProperties::GetStreamTimes is called.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_Stream_PVRStreamTimes_Help
+///
+///@{
+class PVRStreamTimes : public CStructHdl<PVRStreamTimes, PVR_STREAM_TIMES>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRStreamTimes() { memset(m_cStructure, 0, sizeof(PVR_STREAM_TIMES)); }
+ PVRStreamTimes(const PVRStreamTimes& type) : CStructHdl(type) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Stream_PVRStreamTimes_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Stream_PVRStreamTimes
+ /// ----------------------------------------------------------------------------
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_Stream_PVRStreamTimes :</b>
+ /// | Name | Type | Set call | Get call
+ /// |------|------|----------|----------
+ /// | **Start time** | `time_t` | @ref PVRStreamTimes::SetStartTime "SetStartTime" | @ref PVRStreamTimes::GetStartTime "GetStartTime"
+ /// | **PTS start** | `int64_t` | @ref PVRStreamTimes::SetPTSStart "SetPTSStart" | @ref PVRStreamTimes::GetPTSStart "GetPTSStart"
+ /// | **PTS begin** | `int64_t` | @ref PVRStreamTimes::SetPTSBegin "SetPTSBegin" | @ref PVRStreamTimes::GetPTSBegin "GetPTSBegin"
+ /// | **PTS end** | `int64_t` | @ref PVRStreamTimes::SetPTSEnd "SetPTSEnd" | @ref PVRStreamTimes::GetPTSEnd "GetPTSEnd"
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Stream_PVRStreamTimes
+ ///@{
+
+ /// @brief For recordings, this must be zero. For Live TV, this is a reference
+ /// time in units of time_t (UTC) from which time elapsed starts. Ideally start
+ /// of tv show, but can be any other value.
+ void SetStartTime(time_t startTime) { m_cStructure->startTime = startTime; }
+
+ /// @brief To get with @ref SetStartTime() changed values.
+ time_t GetStartTime() const { return m_cStructure->startTime; }
+
+ /// @brief The pts of startTime.
+ void SetPTSStart(int64_t ptsStart) { m_cStructure->ptsStart = ptsStart; }
+
+ /// @brief To get with @ref SetPTSStart() changed values.
+ int64_t GetPTSStart() const { return m_cStructure->ptsStart; }
+
+ /// @brief Earliest pts player can seek back. Value is in micro seconds,
+ /// relative to PTS start. For recordings, this must be zero. For Live TV, this
+ /// must be zero if not timeshifting and must point to begin of the timeshift
+ /// buffer, otherwise.
+ void SetPTSBegin(int64_t ptsBegin) { m_cStructure->ptsBegin = ptsBegin; }
+
+ /// @brief To get with @ref SetPTSBegin() changed values.
+ int64_t GetPTSBegin() const { return m_cStructure->ptsBegin; }
+
+ /// @brief Latest pts player can seek forward. Value is in micro seconds,
+ /// relative to PTS start. For recordings, this must be the total length. For
+ /// Live TV, this must be zero if not timeshifting and must point to end of
+ /// the timeshift buffer, otherwise.
+ void SetPTSEnd(int64_t ptsEnd) { m_cStructure->ptsEnd = ptsEnd; }
+
+ /// @brief To get with @ref SetPTSEnd() changed values.
+ int64_t GetPTSEnd() const { return m_cStructure->ptsEnd; }
+ ///@}
+
+private:
+ PVRStreamTimes(const PVR_STREAM_TIMES* type) : CStructHdl(type) {}
+ PVRStreamTimes(PVR_STREAM_TIMES* type) : CStructHdl(type) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Timers.h b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Timers.h
new file mode 100644
index 0000000..5ea48fd
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/addon-instance/pvr/Timers.h
@@ -0,0 +1,896 @@
+/*
+ * 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 "General.h"
+#include "../../AddonBase.h"
+#include "../../c-api/addon-instance/pvr.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C++" Definitions group 6 - PVR timers
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace addon
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Timer_PVRTimer class PVRTimer
+/// @ingroup cpp_kodi_addon_pvr_Defs_Timer
+/// @brief **PVR add-on timer type**\n
+/// Representation of a timer event.
+///
+/// The related values here are automatically initiated to defaults and need
+/// only be set if supported and used.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_Timer_PVRTimer_Help
+///
+///@{
+class PVRTimer : public CStructHdl<PVRTimer, PVR_TIMER>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRTimer()
+ {
+ m_cStructure->iClientIndex = 0;
+ m_cStructure->state = PVR_TIMER_STATE_NEW;
+ m_cStructure->iTimerType = PVR_TIMER_TYPE_NONE;
+ m_cStructure->iParentClientIndex = 0;
+ m_cStructure->iClientChannelUid = PVR_TIMER_VALUE_NOT_AVAILABLE;
+ m_cStructure->startTime = 0;
+ m_cStructure->endTime = 0;
+ m_cStructure->bStartAnyTime = false;
+ m_cStructure->bEndAnyTime = false;
+ m_cStructure->bFullTextEpgSearch = false;
+ m_cStructure->iPriority = PVR_TIMER_VALUE_NOT_AVAILABLE;
+ m_cStructure->iLifetime = PVR_TIMER_VALUE_NOT_AVAILABLE;
+ m_cStructure->iMaxRecordings = PVR_TIMER_VALUE_NOT_AVAILABLE;
+ m_cStructure->iRecordingGroup = 0;
+ m_cStructure->firstDay = 0;
+ m_cStructure->iWeekdays = PVR_WEEKDAY_NONE;
+ m_cStructure->iPreventDuplicateEpisodes = 0;
+ m_cStructure->iEpgUid = 0;
+ m_cStructure->iMarginStart = 0;
+ m_cStructure->iMarginEnd = 0;
+ m_cStructure->iGenreType = PVR_TIMER_VALUE_NOT_AVAILABLE;
+ m_cStructure->iGenreSubType = PVR_TIMER_VALUE_NOT_AVAILABLE;
+ }
+ PVRTimer(const PVRTimer& data) : CStructHdl(data) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Timer_PVRTimer_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Timer_PVRTimer
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_Timer_PVRTimer :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |------|------|----------|----------|-----------
+ /// | **Client index** | `unsigned int` | @ref PVRTimer::SetClientIndex "SetClientIndex" | @ref PVRTimer::GetClientIndex "GetClientIndex" | *required to set*
+ /// | **State** | @ref PVR_TIMER_STATE | @ref PVRTimer::SetState "SetState" | @ref PVRTimer::GetState "GetState" | *required to set*
+ /// | **Type** | `unsigned int` | @ref PVRTimer::SetTimerType "SetTimerType" | @ref PVRTimer::GetTimerType "GetTimerType" | *required to set*
+ /// | **Title** | `std::string` | @ref PVRTimer::SetTitle "SetTitle" | @ref PVRTimer::GetTitle "GetTitle" | *required to set*
+ /// | **Parent client index** | `unsigned int` | @ref PVRTimer::SetParentClientIndex "SetParentClientIndex" | @ref PVRTimer::GetParentClientIndex "GetParentClientIndex" | *optional*
+ /// | **Client channel unique identifier** | `int` | @ref PVRTimer::SetClientChannelUid "SetClientChannelUid" | @ref PVRTimer::GetClientChannelUid "GetClientChannelUid" | *optional*
+ /// | **Start time** | `time_t` | @ref PVRTimer::SetStartTime "SetStartTime" | @ref PVRTimer::GetStartTime "GetStartTime" | *optional*
+ /// | **End time** | `time_t` | @ref PVRTimer::SetEndTime "SetEndTime" | @ref PVRTimer::GetEndTime "GetEndTime" | *optional*
+ /// | **Start any time** | `bool` | @ref PVRTimer::SetStartAnyTime "SetStartAnyTime" | @ref PVRTimer::GetStartAnyTime "GetStartAnyTime" | *optional*
+ /// | **End any time** | `bool` | @ref PVRTimer::SetEndAnyTime "SetEndAnyTime" | @ref PVRTimer::GetEndAnyTime "GetEndAnyTime" | *optional*
+ /// | **EPG search string** | `std::string` | @ref PVRTimer::SetEPGSearchString "SetEPGSearchString" | @ref PVRTimer::GetEPGSearchString "GetEPGSearchString" | *optional*
+ /// | **Full text EPG search** | `bool` | @ref PVRTimer::SetFullTextEpgSearch "SetFullTextEpgSearch" | @ref PVRTimer::GetFullTextEpgSearch "GetFullTextEpgSearch" | *optional*
+ /// | **Recording store directory** | `std::string` | @ref PVRTimer::SetDirectory "SetDirectory" | @ref PVRTimer::GetDirectory "GetDirectory" | *optional*
+ /// | **Timer priority** | `int` | @ref PVRTimer::SetPriority "SetPriority" | @ref PVRTimer::GetPriority "GetPriority" | *optional*
+ /// | **Timer lifetime** | `int` | @ref PVRTimer::SetLifetime "SetLifetime" | @ref PVRTimer::GetLifetime "GetLifetime" | *optional*
+ /// | **Max recordings** | `int` | @ref PVRTimer::SetMaxRecordings "SetMaxRecordings" | @ref PVRTimer::GetMaxRecordings "GetMaxRecordings" | *optional*
+ /// | **Recording group** | `unsigned int` | @ref PVRTimer::SetRecordingGroup "SetRecordingGroup" | @ref PVRTimer::GetRecordingGroup "GetRecordingGroup" | *optional*
+ /// | **First start day** | `time_t` | @ref PVRTimer::SetFirstDay "SetFirstDay" | @ref PVRTimer::GetFirstDay "GetFirstDay" | *optional*
+ /// | **Used timer weekdays** | `unsigned int` | @ref PVRTimer::SetWeekdays "SetWeekdays" | @ref PVRTimer::GetWeekdays "GetWeekdays" | *optional*
+ /// | **Prevent duplicate episodes** | `unsigned int` | @ref PVRTimer::SetPreventDuplicateEpisodes "SetPreventDuplicateEpisodes" | @ref PVRTimer::GetPreventDuplicateEpisodes "GetPreventDuplicateEpisodes" | *optional*
+ /// | **EPG unique identifier** | `unsigned int` | @ref PVRTimer::SetEPGUid "SetEPGUid" | @ref PVRTimer::GetEPGUid "GetEPGUid" | *optional*
+ /// | **Margin start** | `unsigned int` | @ref PVRTimer::SetMarginStart "SetMarginStart" | @ref PVRTimer::GetMarginStart "GetMarginStart" | *optional*
+ /// | **Margin end** | `unsigned int` | @ref PVRTimer::SetMarginEnd "SetMarginEnd" | @ref PVRTimer::GetMarginEnd "GetMarginEnd" | *optional*
+ /// | **Genre type** | `int` | @ref PVRTimer::SetGenreType "SetGenreType" | @ref PVRTimer::GetGenreType "GetGenreType" | *optional*
+ /// | **Genre sub type** | `int` | @ref PVRTimer::SetGenreSubType "SetGenreSubType" | @ref PVRTimer::GetGenreSubType "GetGenreSubType" | *optional*
+ /// | **Series link** | `std::string` | @ref PVRTimer::SetSeriesLink "SetSeriesLink" | @ref PVRTimer::GetSeriesLink "GetSeriesLink" | *optional*
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Timer_PVRTimer
+ ///@{
+
+ /// @brief **required**\n
+ /// The index of this timer given by the client.
+ ///
+ /// @ref PVR_TIMER_NO_CLIENT_INDEX indicates that the index was not yet set
+ /// by the client, for example for new timers created by Kodi and passed the
+ /// first time to the client. A valid index must be greater than
+ /// @ref PVR_TIMER_NO_CLIENT_INDEX.
+ ///
+ void SetClientIndex(unsigned int clientIndex) { m_cStructure->iClientIndex = clientIndex; }
+
+ /// @brief To get with @ref SetClientIndex changed values.
+ unsigned int GetClientIndex() const { return m_cStructure->iClientIndex; }
+
+ /// @brief **required**\n
+ /// The state of this timer.
+ ///
+ /// @note @ref PVR_TIMER_STATE_NEW is default.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRTimer tag;
+ /// tag.SetState(PVR_TIMER_STATE_RECORDING);
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetState(PVR_TIMER_STATE state) { m_cStructure->state = state; }
+
+ /// @brief To get with @ref SetState changed values.
+ PVR_TIMER_STATE GetState() const { return m_cStructure->state; }
+
+ /// @brief **required**\n
+ /// The type of this timer.
+ ///
+ /// It is private to the addon and can be freely defined by the addon.
+ /// The value must be greater than @ref PVR_TIMER_TYPE_NONE.
+ ///
+ /// Kodi does not interpret this value (except for checking for @ref PVR_TIMER_TYPE_NONE),
+ /// but will pass the right id to the addon with every @ref PVRTimer instance,
+ /// thus the addon easily can determine the timer type.
+ ///
+ /// @note @ref PVR_TIMER_TYPE_NONE is default.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRTimer tag;
+ /// tag.SetTimerType(123);
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetTimerType(unsigned int timerType) { m_cStructure->iTimerType = timerType; }
+
+ /// @brief To get with @ref SetTimerType changed values.
+ unsigned int GetTimerType() const { return m_cStructure->iTimerType; }
+
+ /// @brief **required**\n
+ /// A title for this timer.
+ void SetTitle(const std::string& title)
+ {
+ strncpy(m_cStructure->strTitle, title.c_str(), sizeof(m_cStructure->strTitle) - 1);
+ }
+
+ /// @brief To get with @ref SetTitle changed values.
+ std::string GetTitle() const { return m_cStructure->strTitle; }
+
+ /// @brief **optional**\n
+ /// For timers scheduled by a repeating timer.
+ ///
+ /// The index of the repeating timer that scheduled this timer (it's
+ /// @ref clientIndex value). Use @ref PVR_TIMER_NO_PARENT to indicate that
+ /// this timer was no scheduled by a repeating timer.
+ void SetParentClientIndex(unsigned int parentClientIndex)
+ {
+ m_cStructure->iParentClientIndex = parentClientIndex;
+ }
+
+ /// @brief To get with @ref SetParentClientIndex changed values.
+ unsigned int GetParentClientIndex() const { return m_cStructure->iParentClientIndex; }
+
+ /// @brief **optional**\n
+ /// Unique identifier of the channel to record on.
+ ///
+ /// @ref PVR_TIMER_ANY_CHANNEL will denote "any channel", not a specific one.
+ /// @ref PVR_CHANNEL_INVALID_UID denotes that channel uid is not available.
+ void SetClientChannelUid(int clientChannelUid)
+ {
+ m_cStructure->iClientChannelUid = clientChannelUid;
+ }
+
+ /// @brief To get with @ref SetClientChannelUid changed values
+ int GetClientChannelUid() const { return m_cStructure->iClientChannelUid; }
+
+ /// @brief **optional**\n
+ /// Start time of the recording in UTC.
+ ///
+ /// Instant timers that are sent to the add-on by Kodi will have this value
+ /// set to 0.
+ void SetStartTime(time_t startTime) { m_cStructure->startTime = startTime; }
+
+ /// @brief To get with @ref SetStartTime changed values.
+ time_t GetStartTime() const { return m_cStructure->startTime; }
+
+ /// @brief **optional**\n
+ /// End time of the recording in UTC.
+ void SetEndTime(time_t endTime) { m_cStructure->endTime = endTime; }
+
+ /// @brief To get with @ref SetEndTime changed values.
+ time_t GetEndTime() const { return m_cStructure->endTime; }
+
+ /// @brief **optional**\n
+ /// For EPG based (not Manual) timers indicates startTime does not apply.
+ ///
+ /// Default = false.
+ void SetStartAnyTime(bool startAnyTime) { m_cStructure->bStartAnyTime = startAnyTime; }
+
+ /// @brief To get with @ref SetStartAnyTime changed values.
+ bool GetStartAnyTime() const { return m_cStructure->bStartAnyTime; }
+
+ /// @brief **optional**\n
+ /// For EPG based (not Manual) timers indicates endTime does not apply.
+ ///
+ /// Default = false
+ void SetEndAnyTime(bool endAnyTime) { m_cStructure->bEndAnyTime = endAnyTime; }
+
+ /// @brief To get with @ref SetEndAnyTime changed values.
+ bool GetEndAnyTime() const { return m_cStructure->bEndAnyTime; }
+
+ /// @brief **optional**\n
+ /// A string used to search epg data for repeating epg-based timers.
+ ///
+ /// Format is backend-dependent, for example regexp.
+ void SetEPGSearchString(const std::string& epgSearchString)
+ {
+ strncpy(m_cStructure->strEpgSearchString, epgSearchString.c_str(),
+ sizeof(m_cStructure->strEpgSearchString) - 1);
+ }
+
+ /// @brief To get with @ref SetEPGSearchString changed values
+ std::string GetEPGSearchString() const { return m_cStructure->strEpgSearchString; }
+
+ /// @brief **optional**\n
+ /// Indicates, whether @ref SetEPGSearchString() is to match against the epg
+ /// episode title only or also against "other" epg data (backend-dependent).
+ void SetFullTextEpgSearch(bool fullTextEpgSearch)
+ {
+ m_cStructure->bFullTextEpgSearch = fullTextEpgSearch;
+ }
+
+ /// @brief To get with @ref SetFullTextEpgSearch changed values.
+ bool GetFullTextEpgSearch() const { return m_cStructure->bFullTextEpgSearch; }
+
+ /// @brief **optional**\n
+ /// The (relative) directory where the recording will be stored in.
+ void SetDirectory(const std::string& directory)
+ {
+ strncpy(m_cStructure->strDirectory, directory.c_str(), sizeof(m_cStructure->strDirectory) - 1);
+ }
+
+ /// @brief To get with @ref SetDirectory changed values.
+ std::string GetDirectory() const { return m_cStructure->strDirectory; }
+
+ /// @brief **optional**\n
+ /// The summary for this timer.
+ void SetSummary(const std::string& summary)
+ {
+ strncpy(m_cStructure->strSummary, summary.c_str(), sizeof(m_cStructure->strSummary) - 1);
+ }
+
+ /// @brief To get with @ref SetDirectory changed values.
+ std::string GetSummary() const { return m_cStructure->strSummary; }
+
+ /// @brief **optional**\n
+ /// The priority of this timer.
+ void SetPriority(int priority) { m_cStructure->iPriority = priority; }
+
+ /// @brief To get with @ref SetPriority changed values.
+ int GetPriority() const { return m_cStructure->iPriority; }
+
+ /// @brief **optional**\n
+ /// Lifetime of recordings created by this timer.
+ ///
+ /// Value > 0 days after which recordings will be deleted by the backend, < 0
+ /// addon defined integer list reference, == 0 disabled.
+ void SetLifetime(int priority) { m_cStructure->iLifetime = priority; }
+
+ /// @brief To get with @ref SetLifetime changed values.
+ int GetLifetime() const { return m_cStructure->iLifetime; }
+
+ /// @brief **optional**\n
+ /// Maximum number of recordings this timer shall create.
+ ///
+ /// Value > 0 number of recordings, < 0 addon defined integer list reference, == 0 disabled.
+ void SetMaxRecordings(int maxRecordings) { m_cStructure->iMaxRecordings = maxRecordings; }
+
+ /// @brief To get with @ref SetMaxRecordings changed values.
+ int GetMaxRecordings() const { return m_cStructure->iMaxRecordings; }
+
+ /// @brief **optional**\n
+ /// Integer ref to addon/backend defined list of recording groups.
+ void SetRecordingGroup(unsigned int recordingGroup)
+ {
+ m_cStructure->iRecordingGroup = recordingGroup;
+ }
+
+ /// @brief To get with @ref SetRecordingGroup changed values.
+ unsigned int GetRecordingGroup() const { return m_cStructure->iRecordingGroup; }
+
+ /// @brief **optional**\n
+ /// The first day this timer is active, for repeating timers.
+ void SetFirstDay(time_t firstDay) { m_cStructure->firstDay = firstDay; }
+
+ /// @brief To get with @ref SetFirstDay changed values.
+ time_t GetFirstDay() const { return m_cStructure->firstDay; }
+
+ /// @brief **optional**\n
+ /// Week days, for repeating timers (see
+ /// @ref cpp_kodi_addon_pvr_Defs_Timer_PVR_WEEKDAY "PVR_WEEKDAY_*" constant values)
+ ///
+ /// @note @ref PVR_WEEKDAY_NONE is default.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// kodi::addon::PVRTimer tag;
+ /// tag.SetWeekdays(PVR_WEEKDAY_MONDAY | PVR_WEEKDAY_SATURDAY);
+ /// ...
+ /// ~~~~~~~~~~~~~
+ void SetWeekdays(unsigned int weekdays) { m_cStructure->iWeekdays = weekdays; }
+
+ /// @brief To get with @ref SetFirstDay changed values.
+ unsigned int GetWeekdays() const { return m_cStructure->iWeekdays; }
+
+ /// @brief **optional**\n
+ /// Prevent duplicate episodes.
+ ///
+ /// Should 1 if backend should only record new episodes in case of a repeating
+ /// epg-based timer, 0 if all episodes shall be recorded (no duplicate detection).
+ ///
+ /// Actual algorithm for duplicate detection is defined by the backend.
+ /// Addons may define own values for different duplicate detection
+ /// algorithms, thus this is not just a bool.
+ void SetPreventDuplicateEpisodes(unsigned int preventDuplicateEpisodes)
+ {
+ m_cStructure->iPreventDuplicateEpisodes = preventDuplicateEpisodes;
+ }
+
+ /// @brief To get with @ref SetPreventDuplicateEpisodes changed values.
+ unsigned int GetPreventDuplicateEpisodes() const
+ {
+ return m_cStructure->iPreventDuplicateEpisodes;
+ }
+
+ /// @brief **optional**\n
+ /// EPG event id associated with this timer. Event ids must be unique for a
+ /// channel.
+ ///
+ /// Valid ids must be greater than @ref EPG_TAG_INVALID_UID.
+ void SetEPGUid(unsigned int epgUid) { m_cStructure->iEpgUid = epgUid; }
+
+ /// @brief To get with @ref SetEPGUid changed values.
+ unsigned int GetEPGUid() const { return m_cStructure->iEpgUid; }
+
+ /// @brief **optional**\n
+ /// If set, the backend starts the recording selected minutes before
+ /// @ref SetStartTime.
+ void SetMarginStart(unsigned int marginStart) { m_cStructure->iMarginStart = marginStart; }
+
+ /// @brief To get with @ref SetMarginStart changed values.
+ unsigned int GetMarginStart() const { return m_cStructure->iMarginStart; }
+
+ /// @brief **optional**\n
+ /// If set, the backend ends the recording selected minutes after
+ /// @ref SetEndTime.
+ void SetMarginEnd(unsigned int marginEnd) { m_cStructure->iMarginEnd = marginEnd; }
+
+ /// @brief To get with @ref SetMarginEnd changed values.
+ unsigned int GetMarginEnd() const { return m_cStructure->iMarginEnd; }
+
+ /// @brief **optional**\n
+ /// Genre type.
+ ///
+ /// @copydetails EPG_EVENT_CONTENTMASK
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// kodi::addon::PVRTimer tag;
+ /// tag.SetGenreType(EPG_EVENT_CONTENTMASK_MOVIEDRAMA);
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ /// @note If confirmed that backend brings the types in [ETSI EN 300 468](https://www.etsi.org/deliver/etsi_en/300400_300499/300468/01.14.01_60/en_300468v011401p.pdf)
+ /// conform values, can be @ref EPG_EVENT_CONTENTMASK ignored and to set here
+ /// with backend value.
+ ///
+ void SetGenreType(int genreType) { m_cStructure->iGenreType = genreType; }
+
+ /// @brief To get with @ref SetGenreType changed values.
+ int GetGenreType() const { return m_cStructure->iGenreType; }
+
+ /// @brief **optional**\n
+ /// Genre sub type.
+ ///
+ /// @copydetails EPG_EVENT_CONTENTMASK
+ ///
+ /// Subtypes groups related to set by @ref SetGenreType:
+ /// | Main genre type | List with available sub genre types
+ /// |-----------------|-----------------------------------------
+ /// | @ref EPG_EVENT_CONTENTMASK_UNDEFINED | Nothing, should be 0
+ /// | @ref EPG_EVENT_CONTENTMASK_MOVIEDRAMA | @ref EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA
+ /// | @ref EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS | @ref EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS
+ /// | @ref EPG_EVENT_CONTENTMASK_SHOW | @ref EPG_EVENT_CONTENTSUBMASK_SHOW
+ /// | @ref EPG_EVENT_CONTENTMASK_SPORTS | @ref EPG_EVENT_CONTENTSUBMASK_SPORTS
+ /// | @ref EPG_EVENT_CONTENTMASK_CHILDRENYOUTH | @ref EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH
+ /// | @ref EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE | @ref EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE
+ /// | @ref EPG_EVENT_CONTENTMASK_ARTSCULTURE | @ref EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE
+ /// | @ref EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS | @ref EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS
+ /// | @ref EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE | @ref EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE
+ /// | @ref EPG_EVENT_CONTENTMASK_LEISUREHOBBIES | @ref EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES
+ /// | @ref EPG_EVENT_CONTENTMASK_SPECIAL | @ref EPG_EVENT_CONTENTSUBMASK_SPECIAL
+ /// | @ref EPG_EVENT_CONTENTMASK_USERDEFINED | Can be defined by you
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// kodi::addon::PVRTimer tag;
+ /// tag.SetGenreType(EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE);
+ /// tag.SetGenreSubType(EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE_JAZZ);
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetGenreSubType(int genreSubType) { m_cStructure->iGenreSubType = genreSubType; }
+
+ /// @brief To get with @ref SetGenreType changed values.
+ int GetGenreSubType() const { return m_cStructure->iGenreSubType; }
+
+ /// @brief **optional**\n
+ /// Series link for this timer.
+ ///
+ /// If set for an epg-based timer rule, matching events will be found by
+ /// checking with here, instead of @ref SetTitle() (and @ref SetFullTextEpgSearch()).
+ void SetSeriesLink(const std::string& seriesLink)
+ {
+ strncpy(m_cStructure->strSeriesLink, seriesLink.c_str(),
+ sizeof(m_cStructure->strSeriesLink) - 1);
+ }
+
+ /// @brief To get with @ref SetSeriesLink changed values.
+ std::string GetSeriesLink() const { return m_cStructure->strSeriesLink; }
+ ///@}
+
+private:
+ PVRTimer(const PVR_TIMER* data) : CStructHdl(data) {}
+ PVRTimer(PVR_TIMER* data) : CStructHdl(data) {}
+};
+
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Timer_PVRTimersResultSet class PVRTimersResultSet
+/// @ingroup cpp_kodi_addon_pvr_Defs_Timer_PVRTimer
+/// @brief **PVR add-on timer transfer class**\n
+/// To transfer the content of @ref kodi::addon::CInstancePVRClient::GetTimers().
+///
+/// @note This becomes only be used on addon call above, not usable outside on
+/// addon itself.
+///@{
+class PVRTimersResultSet
+{
+public:
+ /*! \cond PRIVATE */
+ PVRTimersResultSet() = delete;
+ PVRTimersResultSet(const AddonInstance_PVR* instance, PVR_HANDLE handle)
+ : m_instance(instance), m_handle(handle)
+ {
+ }
+ /*! \endcond */
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Timer_PVRTimersResultSet
+ ///@{
+
+ /// @brief To add and give content from addon to Kodi on related call.
+ ///
+ /// @param[in] tag The to transferred data.
+ void Add(const kodi::addon::PVRTimer& tag)
+ {
+ m_instance->toKodi->TransferTimerEntry(m_instance->toKodi->kodiInstance, m_handle, tag);
+ }
+
+ ///@}
+
+private:
+ const AddonInstance_PVR* m_instance = nullptr;
+ const PVR_HANDLE m_handle;
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_addon_pvr_Defs_Timer_PVRTimerType class PVRTimerType
+/// @ingroup cpp_kodi_addon_pvr_Defs_Timer
+/// @brief **PVR add-on timer type**\n
+/// To define the content of @ref kodi::addon::CInstancePVRClient::GetTimerTypes()
+/// given groups.
+///
+/// ----------------------------------------------------------------------------
+///
+/// @copydetails cpp_kodi_addon_pvr_Defs_Timer_PVRTimerType_Help
+///
+///@{
+class PVRTimerType : public CStructHdl<PVRTimerType, PVR_TIMER_TYPE>
+{
+ friend class CInstancePVRClient;
+
+public:
+ /*! \cond PRIVATE */
+ PVRTimerType()
+ {
+ memset(m_cStructure, 0, sizeof(PVR_TIMER_TYPE));
+ m_cStructure->iPrioritiesDefault = -1;
+ m_cStructure->iLifetimesDefault = -1;
+ m_cStructure->iPreventDuplicateEpisodesDefault = -1;
+ m_cStructure->iRecordingGroupDefault = -1;
+ m_cStructure->iMaxRecordingsDefault = -1;
+ }
+ PVRTimerType(const PVRTimerType& type) : CStructHdl(type) {}
+ /*! \endcond */
+
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Timer_PVRTimerType_Help Value Help
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Timer_PVRTimerType
+ /// ----------------------------------------------------------------------------
+ ///
+ /// <b>The following table contains values that can be set with @ref cpp_kodi_addon_pvr_Defs_Timer_PVRTimerType :</b>
+ /// | Name | Type | Set call | Get call | Usage
+ /// |------|------|----------|----------|-----------
+ /// | **Identifier** | `unsigned int` | @ref PVRTimerType::SetId "SetId" | @ref PVRTimerType::GetId "GetId" | *required to set*
+ /// | **Attributes** | `unsigned int` | @ref PVRTimerType::SetAttributes "SetAttributes" | @ref PVRTimerType::GetAttributes "GetAttributes" | *required to set*
+ /// | **Description** | `std::string` | @ref PVRTimerType::SetDescription "SetDescription" | @ref PVRTimerType::GetDescription "GetDescription" | *optional*
+ /// | | | | | |
+ /// | **Priority selection** | @ref cpp_kodi_addon_pvr_Defs_PVRTypeIntValue "PVRTypeIntValue" | @ref PVRTimerType::SetPriorities "SetPriorities" | @ref PVRTimerType::GetPriorities "GetPriorities" | *optional*
+ /// | **Priority default selection** | `int`| @ref PVRTimerType::SetPrioritiesDefault "SetPrioritiesDefault" | @ref PVRTimerType::GetPrioritiesDefault "GetPrioritiesDefault" | *optional*
+ /// | | | | | |
+ /// | **Lifetime selection** | @ref cpp_kodi_addon_pvr_Defs_PVRTypeIntValue "PVRTypeIntValue" | @ref PVRTimerType::SetLifetimes "SetLifetimes" | @ref PVRTimerType::GetLifetimes "GetLifetimes" | *optional*
+ /// | **Lifetime default selection** | `int`| @ref PVRTimerType::SetLifetimesDefault "SetLifetimesDefault" | @ref PVRTimerType::GetLifetimesDefault "GetLifetimesDefault" | *optional*
+ /// | | | | | |
+ /// | **Prevent duplicate episodes selection** | @ref cpp_kodi_addon_pvr_Defs_PVRTypeIntValue "PVRTypeIntValue" | @ref PVRTimerType::SetPreventDuplicateEpisodes "SetPreventDuplicateEpisodes" | @ref PVRTimerType::GetPreventDuplicateEpisodes "GetPreventDuplicateEpisodes" | *optional*
+ /// | **Prevent duplicate episodes default** | `int`| @ref PVRTimerType::SetPreventDuplicateEpisodesDefault "SetPreventDuplicateEpisodesDefault" | @ref PVRTimerType::GetPreventDuplicateEpisodesDefault "GetPreventDuplicateEpisodesDefault" | *optional*
+ /// | | | | | |
+ /// | **Recording group selection**| @ref cpp_kodi_addon_pvr_Defs_PVRTypeIntValue "PVRTypeIntValue" | @ref PVRTimerType::SetRecordingGroups "SetRecordingGroups" | @ref PVRTimerType::GetRecordingGroups "GetRecordingGroups" | *optional*
+ /// | **Recording group default** | `int`| @ref PVRTimerType::SetRecordingGroupDefault "SetRecordingGroupDefault" | @ref PVRTimerType::GetRecordingGroupDefault "GetRecordingGroupDefault" | *optional*
+ /// | | | | | |
+ /// | **Max recordings selection** | @ref cpp_kodi_addon_pvr_Defs_PVRTypeIntValue "PVRTypeIntValue" | @ref PVRTimerType::SetMaxRecordings "SetMaxRecordings" | @ref PVRTimerType::GetMaxRecordings "GetMaxRecordings" | *optional*
+ /// | **Max recordings default** | `int`| @ref PVRTimerType::SetMaxRecordingsDefault "SetMaxRecordingsDefault" | @ref PVRTimerType::GetMaxRecordingsDefault "GetMaxRecordingsDefault" | *optional*
+ ///
+
+ /// @addtogroup cpp_kodi_addon_pvr_Defs_Timer_PVRTimerType
+ ///@{
+
+ /// @brief **required**\n
+ /// This type's identifier. Ids must be > @ref PVR_TIMER_TYPE_NONE.
+ void SetId(unsigned int id) { m_cStructure->iId = id; }
+
+ /// @brief To get with @ref SetAttributes changed values.
+ unsigned int GetId() const { return m_cStructure->iId; }
+
+ /// @brief **required**\n
+ /// Defines the attributes for this type (@ref cpp_kodi_addon_pvr_Defs_Timer_PVR_TIMER_TYPE "PVR_TIMER_TYPE_*" constants).
+ ///
+ /// To defines the attributes for a type. These values are bit fields that can be
+ /// used together.
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRTimerType tag;
+ /// tag.SetAttributes(PVR_TIMER_TYPE_IS_MANUAL | PVR_TIMER_TYPE_IS_REPEATING);
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetAttributes(uint64_t attributes) { m_cStructure->iAttributes = attributes; }
+
+ /// @brief To get with @ref SetAttributes changed values.
+ uint64_t GetAttributes() const { return m_cStructure->iAttributes; }
+
+ /// @brief **optional**\n
+ /// A short localized string describing the purpose of the type. (e.g.
+ /// "Any time at this channel if title matches").
+ ///
+ /// If left blank, Kodi will generate a description based on the attributes
+ /// REPEATING and MANUAL. (e.g. "Repeating EPG-based.")
+ void SetDescription(const std::string& description)
+ {
+ strncpy(m_cStructure->strDescription, description.c_str(),
+ sizeof(m_cStructure->strDescription) - 1);
+ }
+
+ /// @brief To get with @ref SetDescription changed values.
+ std::string GetDescription() const { return m_cStructure->strDescription; }
+
+ //----------------------------------------------------------------------------
+
+ /// @brief **optional**\n
+ /// Priority value definitions.
+ ///
+ /// Array containing the possible values for @ref PVRTimer::SetPriority().
+ ///
+ /// @param[in] priorities List of priority values
+ /// @param[in] prioritiesDefault [opt] The default value in list, can also be
+ /// set by @ref SetPrioritiesDefault()
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_PVRTypeIntValue_Help
+ void SetPriorities(const std::vector<PVRTypeIntValue>& priorities, int prioritiesDefault = -1)
+ {
+ m_cStructure->iPrioritiesSize = static_cast<unsigned int>(priorities.size());
+ for (unsigned int i = 0;
+ i < m_cStructure->iPrioritiesSize && i < sizeof(m_cStructure->priorities); ++i)
+ {
+ m_cStructure->priorities[i].iValue = priorities[i].GetCStructure()->iValue;
+ strncpy(m_cStructure->priorities[i].strDescription,
+ priorities[i].GetCStructure()->strDescription,
+ sizeof(m_cStructure->priorities[i].strDescription) - 1);
+ }
+ if (prioritiesDefault != -1)
+ m_cStructure->iPrioritiesDefault = prioritiesDefault;
+ }
+
+ /// @brief To get with @ref SetPriorities changed values.
+ std::vector<PVRTypeIntValue> GetPriorities() const
+ {
+ std::vector<PVRTypeIntValue> ret;
+ for (unsigned int i = 0; i < m_cStructure->iPrioritiesSize; ++i)
+ ret.emplace_back(m_cStructure->priorities[i].iValue,
+ m_cStructure->priorities[i].strDescription);
+ return ret;
+ }
+
+ /// @brief **optional**\n
+ /// The default value for @ref PVRTimer::SetPriority().
+ ///
+ /// @note Must be filled if @ref SetPriorities contain values and not
+ /// defined there on second function value.
+ void SetPrioritiesDefault(int prioritiesDefault)
+ {
+ m_cStructure->iPrioritiesDefault = prioritiesDefault;
+ }
+
+ /// @brief To get with @ref SetPrioritiesDefault changed values.
+ int GetPrioritiesDefault() const { return m_cStructure->iPrioritiesDefault; }
+
+ //----------------------------------------------------------------------------
+
+ /// @brief **optional**\n
+ /// Lifetime value definitions.
+ ///
+ /// Array containing the possible values for @ref PVRTimer::SetLifetime().
+ ///
+ /// @param[in] lifetimes List of lifetimes values
+ /// @param[in] lifetimesDefault [opt] The default value in list, can also be
+ /// set by @ref SetLifetimesDefault()
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_PVRTypeIntValue_Help
+ void SetLifetimes(const std::vector<PVRTypeIntValue>& lifetimes, int lifetimesDefault = -1)
+ {
+ m_cStructure->iLifetimesSize = static_cast<unsigned int>(lifetimes.size());
+ for (unsigned int i = 0;
+ i < m_cStructure->iLifetimesSize && i < sizeof(m_cStructure->lifetimes); ++i)
+ {
+ m_cStructure->lifetimes[i].iValue = lifetimes[i].GetCStructure()->iValue;
+ strncpy(m_cStructure->lifetimes[i].strDescription,
+ lifetimes[i].GetCStructure()->strDescription,
+ sizeof(m_cStructure->lifetimes[i].strDescription) - 1);
+ }
+ if (lifetimesDefault != -1)
+ m_cStructure->iLifetimesDefault = lifetimesDefault;
+ }
+
+ /// @brief To get with @ref SetLifetimes changed values.
+ std::vector<PVRTypeIntValue> GetLifetimes() const
+ {
+ std::vector<PVRTypeIntValue> ret;
+ for (unsigned int i = 0; i < m_cStructure->iLifetimesSize; ++i)
+ ret.emplace_back(m_cStructure->lifetimes[i].iValue,
+ m_cStructure->lifetimes[i].strDescription);
+ return ret;
+ }
+
+ /// @brief **optional**\n
+ /// The default value for @ref SetLifetimes().
+ ///
+ /// @note Must be filled if @ref SetLifetimes contain values and not
+ /// defined there on second function value.
+ void SetLifetimesDefault(int lifetimesDefault)
+ {
+ m_cStructure->iLifetimesDefault = lifetimesDefault;
+ }
+
+ /// @brief To get with @ref SetLifetimesDefault changed values.
+ int GetLifetimesDefault() const { return m_cStructure->iLifetimesDefault; }
+
+ //----------------------------------------------------------------------------
+
+ /// @brief **optional**\n
+ /// Prevent duplicate episodes value definitions.
+ ///
+ /// Array containing the possible values for @ref PVRTimer::SetPreventDuplicateEpisodes().
+ ///
+ /// @note Must be filled if @ref PVRTimer::SetPreventDuplicateEpisodes() is not empty.
+ ///
+ /// @param[in] preventDuplicateEpisodes List of duplicate episodes values
+ /// @param[in] preventDuplicateEpisodesDefault [opt] The default value in list, can also be
+ /// set by @ref SetPreventDuplicateEpisodesDefault()
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_PVRTypeIntValue_Help
+ void SetPreventDuplicateEpisodes(
+ const std::vector<PVRTypeIntValue>& preventDuplicateEpisodes,
+ int preventDuplicateEpisodesDefault = -1)
+ {
+ m_cStructure->iPreventDuplicateEpisodesSize =
+ static_cast<unsigned int>(preventDuplicateEpisodes.size());
+ for (unsigned int i = 0; i < m_cStructure->iPreventDuplicateEpisodesSize &&
+ i < sizeof(m_cStructure->preventDuplicateEpisodes);
+ ++i)
+ {
+ m_cStructure->preventDuplicateEpisodes[i].iValue =
+ preventDuplicateEpisodes[i].GetCStructure()->iValue;
+ strncpy(m_cStructure->preventDuplicateEpisodes[i].strDescription,
+ preventDuplicateEpisodes[i].GetCStructure()->strDescription,
+ sizeof(m_cStructure->preventDuplicateEpisodes[i].strDescription) - 1);
+ }
+ if (preventDuplicateEpisodesDefault != -1)
+ m_cStructure->iPreventDuplicateEpisodesDefault = preventDuplicateEpisodesDefault;
+ }
+
+ /// @brief To get with @ref SetPreventDuplicateEpisodes changed values.
+ std::vector<PVRTypeIntValue> GetPreventDuplicateEpisodes() const
+ {
+ std::vector<PVRTypeIntValue> ret;
+ for (unsigned int i = 0; i < m_cStructure->iPreventDuplicateEpisodesSize; ++i)
+ ret.emplace_back(m_cStructure->preventDuplicateEpisodes[i].iValue,
+ m_cStructure->preventDuplicateEpisodes[i].strDescription);
+ return ret;
+ }
+
+ /// @brief **optional**\n
+ /// The default value for @ref PVRTimer::SetPreventDuplicateEpisodes().
+ ///
+ /// @note Must be filled if @ref SetPreventDuplicateEpisodes contain values and not
+ /// defined there on second function value.
+ void SetPreventDuplicateEpisodesDefault(int preventDuplicateEpisodesDefault)
+ {
+ m_cStructure->iPreventDuplicateEpisodesDefault = preventDuplicateEpisodesDefault;
+ }
+
+ /// @brief To get with @ref SetPreventDuplicateEpisodesDefault changed values.
+ int GetPreventDuplicateEpisodesDefault() const
+ {
+ return m_cStructure->iPreventDuplicateEpisodesDefault;
+ }
+
+ //----------------------------------------------------------------------------
+
+ /// @brief **optional**\n
+ /// Array containing the possible values of @ref PVRTimer::SetRecordingGroup()
+ ///
+ /// @param[in] recordingGroup List of recording group values
+ /// @param[in] recordingGroupDefault [opt] The default value in list, can also be
+ /// set by @ref SetRecordingGroupDefault()
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_PVRTypeIntValue_Help
+ void SetRecordingGroups(const std::vector<PVRTypeIntValue>& recordingGroup,
+ int recordingGroupDefault = -1)
+ {
+ m_cStructure->iRecordingGroupSize = static_cast<unsigned int>(recordingGroup.size());
+ for (unsigned int i = 0;
+ i < m_cStructure->iRecordingGroupSize && i < sizeof(m_cStructure->recordingGroup); ++i)
+ {
+ m_cStructure->recordingGroup[i].iValue = recordingGroup[i].GetCStructure()->iValue;
+ strncpy(m_cStructure->recordingGroup[i].strDescription,
+ recordingGroup[i].GetCStructure()->strDescription,
+ sizeof(m_cStructure->recordingGroup[i].strDescription) - 1);
+ }
+ if (recordingGroupDefault != -1)
+ m_cStructure->iRecordingGroupDefault = recordingGroupDefault;
+ }
+
+ /// @brief To get with @ref SetRecordingGroups changed values
+ std::vector<PVRTypeIntValue> GetRecordingGroups() const
+ {
+ std::vector<PVRTypeIntValue> ret;
+ for (unsigned int i = 0; i < m_cStructure->iRecordingGroupSize; ++i)
+ ret.emplace_back(m_cStructure->recordingGroup[i].iValue,
+ m_cStructure->recordingGroup[i].strDescription);
+ return ret;
+ }
+
+ /// @brief **optional**\n
+ /// The default value for @ref PVRTimer::SetRecordingGroup().
+ ///
+ /// @note Must be filled if @ref SetRecordingGroups contain values and not
+ /// defined there on second function value.
+ void SetRecordingGroupDefault(int recordingGroupDefault)
+ {
+ m_cStructure->iRecordingGroupDefault = recordingGroupDefault;
+ }
+
+ /// @brief To get with @ref SetRecordingGroupDefault changed values
+ int GetRecordingGroupDefault() const { return m_cStructure->iRecordingGroupDefault; }
+
+ //----------------------------------------------------------------------------
+
+ /// @brief **optional**\n
+ /// Array containing the possible values of @ref PVRTimer::SetMaxRecordings().
+ ///
+ /// @param[in] maxRecordings List of lifetimes values
+ /// @param[in] maxRecordingsDefault [opt] The default value in list, can also be
+ /// set by @ref SetMaxRecordingsDefault()
+ ///
+ /// --------------------------------------------------------------------------
+ ///
+ /// @copydetails cpp_kodi_addon_pvr_Defs_PVRTypeIntValue_Help
+ void SetMaxRecordings(const std::vector<PVRTypeIntValue>& maxRecordings,
+ int maxRecordingsDefault = -1)
+ {
+ m_cStructure->iMaxRecordingsSize = static_cast<unsigned int>(maxRecordings.size());
+ for (unsigned int i = 0;
+ i < m_cStructure->iMaxRecordingsSize && i < sizeof(m_cStructure->maxRecordings); ++i)
+ {
+ m_cStructure->maxRecordings[i].iValue = maxRecordings[i].GetCStructure()->iValue;
+ strncpy(m_cStructure->maxRecordings[i].strDescription,
+ maxRecordings[i].GetCStructure()->strDescription,
+ sizeof(m_cStructure->maxRecordings[i].strDescription) - 1);
+ }
+ if (maxRecordingsDefault != -1)
+ m_cStructure->iMaxRecordingsDefault = maxRecordingsDefault;
+ }
+
+ /// @brief To get with @ref SetMaxRecordings changed values
+ std::vector<PVRTypeIntValue> GetMaxRecordings() const
+ {
+ std::vector<PVRTypeIntValue> ret;
+ for (unsigned int i = 0; i < m_cStructure->iMaxRecordingsSize; ++i)
+ ret.emplace_back(m_cStructure->maxRecordings[i].iValue,
+ m_cStructure->maxRecordings[i].strDescription);
+ return ret;
+ }
+
+ /// @brief **optional**\n
+ /// The default value for @ref SetMaxRecordings().
+ ///
+ /// Can be set with here if on @ref SetMaxRecordings not given as second value.
+ void SetMaxRecordingsDefault(int maxRecordingsDefault)
+ {
+ m_cStructure->iMaxRecordingsDefault = maxRecordingsDefault;
+ }
+
+ /// @brief To get with @ref SetMaxRecordingsDefault changed values
+ int GetMaxRecordingsDefault() const { return m_cStructure->iMaxRecordingsDefault; }
+ ///@}
+
+private:
+ PVRTimerType(const PVR_TIMER_TYPE* type) : CStructHdl(type) {}
+ PVRTimerType(PVR_TIMER_TYPE* type) : CStructHdl(type) {}
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace addon */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/CMakeLists.txt
new file mode 100644
index 0000000..a7dd1d3
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/CMakeLists.txt
@@ -0,0 +1,14 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ addon_base.h
+ audio_engine.h
+ filesystem.h
+ general.h
+ network.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_c-api)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/CMakeLists.txt
new file mode 100644
index 0000000..52670eb
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/CMakeLists.txt
@@ -0,0 +1,20 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ audiodecoder.h
+ audioencoder.h
+ game.h
+ imagedecoder.h
+ inputstream.h
+ peripheral.h
+ pvr.h
+ screensaver.h
+ vfs.h
+ video_codec.h
+ visualization.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_c-api_addon-instance)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audiodecoder.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audiodecoder.h
new file mode 100644
index 0000000..c75ec48
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audiodecoder.h
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_AUDIODECODER_H
+#define C_API_ADDONINSTANCE_AUDIODECODER_H
+
+#include "../addon_base.h"
+#include "../audio_engine.h"
+
+//============================================================================
+/// @ingroup cpp_kodi_addon_audiodecoder_Defs
+/// @brief Identifier which is attached to stream files and with defined names
+/// in addon.xml (`name="???"`) if addon supports "tracks" (set in addon.xml
+/// `tracks="true"`).
+///
+/// @note This macro is largely unnecessary to use directly on the addon,
+/// addon can use the @ref KODI_ADDON_AUDIODECODER_GET_TRACK_EXT around its
+/// associated name.
+///
+///@{
+#define KODI_ADDON_AUDIODECODER_TRACK_EXT "_adecstrm"
+///@}
+//----------------------------------------------------------------------------
+
+//============================================================================
+/// @ingroup cpp_kodi_addon_audiodecoder_Defs
+/// @brief Macro to get file extension to track supported files.
+///
+/// This macro can be used if `tracks="true"` is set in addon.xml, in this
+/// case the addon.xml field `name="???"` is used to identify stream.
+/// Which must then also be used for here.
+///
+///@{
+#define KODI_ADDON_AUDIODECODER_GET_TRACK_EXT(name) "." name KODI_ADDON_AUDIODECODER_TRACK_EXT
+///@}
+//----------------------------------------------------------------------------
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef KODI_ADDON_INSTANCE_HDL KODI_ADDON_AUDIODECODER_HDL;
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_audiodecoder_Defs_AUDIODECODER_READ_RETURN enum AUDIODECODER_READ_RETURN
+ /// @ingroup cpp_kodi_addon_audiodecoder_Defs
+ /// @brief **Return value about** @ref kodi::addon::CInstanceAudioDecoder::ReadPCM()
+ ///
+ /// Possible values are:
+ /// | Value | enum | Description
+ /// |:-----:|:-------------------------------|:--------------------------
+ /// | 0 | @ref AUDIODECODER_READ_SUCCESS | on success
+ /// | -1 | @ref AUDIODECODER_READ_EOF | on end of stream
+ /// | 1 | @ref AUDIODECODER_READ_ERROR | on failure
+ ///
+ ///@{
+ typedef enum AUDIODECODER_READ_RETURN
+ {
+ /// @brief On end of stream
+ AUDIODECODER_READ_EOF = -1,
+
+ /// @brief On success
+ AUDIODECODER_READ_SUCCESS = 0,
+
+ /// @brief On failure
+ AUDIODECODER_READ_ERROR = 1
+ } AUDIODECODER_READ_RETURN;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ struct KODI_ADDON_AUDIODECODER_INFO_TAG
+ {
+ char* title;
+ char* artist;
+ char* album;
+ char* album_artist;
+ char* media_type;
+ char* genre;
+ int duration;
+ int track;
+ int disc;
+ char* disc_subtitle;
+ int disc_total;
+ char* release_date;
+ char* lyrics;
+ int samplerate;
+ int channels;
+ int bitrate;
+ char* comment;
+ char* cover_art_path;
+ char* cover_art_mem_mimetype;
+ uint8_t* cover_art_mem;
+ size_t cover_art_mem_size;
+ };
+
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_AUDIODECODER_SUPPORTS_FILE_V1)(
+ const KODI_ADDON_AUDIODECODER_HDL hdl, const char* file);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_AUDIODECODER_INIT_V1)(
+ const KODI_ADDON_AUDIODECODER_HDL hdl,
+ const char* file,
+ unsigned int filecache,
+ int* channels,
+ int* samplerate,
+ int* bitspersample,
+ int64_t* totaltime,
+ int* bitrate,
+ enum AudioEngineDataFormat* format,
+ enum AudioEngineChannel info[AUDIOENGINE_CH_MAX]);
+ typedef int(ATTR_APIENTRYP PFN_KODI_ADDON_AUDIODECODER_READ_PCM_V1)(
+ const KODI_ADDON_AUDIODECODER_HDL hdl, uint8_t* buffer, size_t size, size_t* actualsize);
+ typedef int64_t(ATTR_APIENTRYP PFN_KODI_ADDON_AUDIODECODER_SEEK_V1)(
+ const KODI_ADDON_AUDIODECODER_HDL hdl, int64_t time);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_AUDIODECODER_READ_TAG_V1)(
+ const KODI_ADDON_AUDIODECODER_HDL hdl,
+ const char* file,
+ struct KODI_ADDON_AUDIODECODER_INFO_TAG* tag);
+ typedef int(ATTR_APIENTRYP PFN_KODI_ADDON_AUDIODECODER_TRACK_COUNT_V1)(
+ const KODI_ADDON_AUDIODECODER_HDL hdl, const char* file);
+
+ typedef struct AddonToKodiFuncTable_AudioDecoder
+ {
+ KODI_HANDLE kodiInstance;
+ } AddonToKodiFuncTable_AudioDecoder;
+
+ typedef struct KodiToAddonFuncTable_AudioDecoder
+ {
+ PFN_KODI_ADDON_AUDIODECODER_SUPPORTS_FILE_V1 supports_file;
+ PFN_KODI_ADDON_AUDIODECODER_INIT_V1 init;
+ PFN_KODI_ADDON_AUDIODECODER_READ_PCM_V1 read_pcm;
+ PFN_KODI_ADDON_AUDIODECODER_SEEK_V1 seek;
+ PFN_KODI_ADDON_AUDIODECODER_READ_TAG_V1 read_tag;
+ PFN_KODI_ADDON_AUDIODECODER_TRACK_COUNT_V1 track_count;
+ } KodiToAddonFuncTable_AudioDecoder;
+
+ typedef struct AddonInstance_AudioDecoder
+ {
+ struct AddonToKodiFuncTable_AudioDecoder* toKodi;
+ struct KodiToAddonFuncTable_AudioDecoder* toAddon;
+ } AddonInstance_AudioDecoder;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_AUDIODECODER_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audioencoder.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audioencoder.h
new file mode 100644
index 0000000..220e4e4
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audioencoder.h
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_AUDIO_ENCODER_H
+#define C_API_ADDONINSTANCE_AUDIO_ENCODER_H
+
+#include "../addon_base.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef KODI_ADDON_INSTANCE_HDL KODI_ADDON_AUDIOENCODER_HDL;
+
+ struct KODI_ADDON_AUDIOENCODER_INFO_TAG
+ {
+ const char* title;
+ const char* artist;
+ const char* album;
+ const char* album_artist;
+ const char* media_type;
+ const char* genre;
+ int duration;
+ int track;
+ int disc;
+ const char* disc_subtitle;
+ int disc_total;
+ const char* release_date;
+ const char* lyrics;
+ int samplerate;
+ int channels;
+ int bits_per_sample;
+ int track_length;
+ const char* comment;
+ };
+
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_AUDIOENCODER_START_V1)(
+ KODI_ADDON_AUDIOENCODER_HDL hdl, const struct KODI_ADDON_AUDIOENCODER_INFO_TAG* tag);
+ typedef ssize_t(ATTR_APIENTRYP PFN_KODI_ADDON_AUDIOENCODER_ENCODE_V1)(
+ KODI_ADDON_AUDIOENCODER_HDL hdl, const uint8_t* pbt_stream, size_t num_bytes_read);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_AUDIOENCODER_FINISH_V1)(
+ KODI_ADDON_AUDIOENCODER_HDL hdl);
+
+ typedef struct AddonToKodiFuncTable_AudioEncoder
+ {
+ KODI_HANDLE kodiInstance;
+ ssize_t (*write)(KODI_HANDLE kodiInstance, const uint8_t* data, size_t len);
+ ssize_t (*seek)(KODI_HANDLE kodiInstance, ssize_t pos, int whence);
+ } AddonToKodiFuncTable_AudioEncoder;
+
+ typedef struct KodiToAddonFuncTable_AudioEncoder
+ {
+ PFN_KODI_ADDON_AUDIOENCODER_START_V1 start;
+ PFN_KODI_ADDON_AUDIOENCODER_ENCODE_V1 encode;
+ PFN_KODI_ADDON_AUDIOENCODER_FINISH_V1 finish;
+ } KodiToAddonFuncTable_AudioEncoder;
+
+ typedef struct AddonInstance_AudioEncoder
+ {
+ struct AddonToKodiFuncTable_AudioEncoder* toKodi;
+ struct KodiToAddonFuncTable_AudioEncoder* toAddon;
+ } AddonInstance_AudioEncoder;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_AUDIO_ENCODER_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/game.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/game.h
new file mode 100644
index 0000000..e0bbf94
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/game.h
@@ -0,0 +1,1235 @@
+/*
+ * Copyright (C) 2014-2020 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_GAME_H
+#define C_API_ADDONINSTANCE_GAME_H
+
+#include "../addon_base.h"
+
+#include <stddef.h> /* size_t */
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon_game_Defs
+/// @brief **Port ID used when topology is unknown**
+#define DEFAULT_PORT_ID "1"
+//------------------------------------------------------------------------------
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_game_Defs
+ /// @brief **Game add-on error codes**
+ ///
+ /// Used as return values on most Game related functions.
+ ///
+ typedef enum GAME_ERROR
+ {
+ /// @brief no error occurred
+ GAME_ERROR_NO_ERROR,
+
+ /// @brief an unknown error occurred
+ GAME_ERROR_UNKNOWN,
+
+ /// @brief the method that the frontend called is not implemented
+ GAME_ERROR_NOT_IMPLEMENTED,
+
+ /// @brief the command was rejected by the game client
+ GAME_ERROR_REJECTED,
+
+ /// @brief the parameters of the method that was called are invalid for this operation
+ GAME_ERROR_INVALID_PARAMETERS,
+
+ /// @brief the command failed
+ GAME_ERROR_FAILED,
+
+ /// @brief no game is loaded
+ GAME_ERROR_NOT_LOADED,
+
+ /// @brief game requires restricted resources
+ GAME_ERROR_RESTRICTED,
+ } GAME_ERROR;
+ //----------------------------------------------------------------------------
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==--
+ /// @defgroup cpp_kodi_addon_game_Defs_AudioStream 1. Audio stream
+ /// @ingroup cpp_kodi_addon_game_Defs
+ /// @brief **The for Audio stream used data system**
+ ///
+ /// Used to give Addon currently used audio stream configuration on Kodi and
+ /// arrays to give related data to Kodi on callbacks.
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief **Stream Format**
+ ///
+ /// From Kodi requested specified audio sample format.
+ ///
+ typedef enum GAME_PCM_FORMAT
+ {
+ GAME_PCM_FORMAT_UNKNOWN,
+
+ /// @brief S16NE sample format
+ GAME_PCM_FORMAT_S16NE,
+ } GAME_PCM_FORMAT;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Audio channel**
+ ///
+ /// Channel identification flags.
+ ///
+ typedef enum GAME_AUDIO_CHANNEL
+ {
+ /// @brief Channel list terminator
+ GAME_CH_NULL,
+
+ /// @brief Channel front left
+ GAME_CH_FL,
+
+ /// @brief Channel front right
+ GAME_CH_FR,
+
+ /// @brief Channel front center
+ GAME_CH_FC,
+
+ /// @brief Channel Low Frequency Effects / Subwoofer
+ GAME_CH_LFE,
+
+ /// @brief Channel back left
+ GAME_CH_BL,
+
+ /// @brief Channel back right
+ GAME_CH_BR,
+
+ /// @brief Channel front left over center
+ GAME_CH_FLOC,
+
+ /// @brief Channel front right over center
+ GAME_CH_FROC,
+
+ /// @brief Channel back center
+ GAME_CH_BC,
+
+ /// @brief Channel surround/side left
+ GAME_CH_SL,
+
+ /// @brief Channel surround/side right
+ GAME_CH_SR,
+
+ /// @brief Channel top front left
+ GAME_CH_TFL,
+
+ /// @brief Channel top front right
+ GAME_CH_TFR,
+
+ /// @brief Channel top front center
+ GAME_CH_TFC,
+
+ /// @brief Channel top center
+ GAME_CH_TC,
+
+ /// @brief Channel top back left
+ GAME_CH_TBL,
+
+ /// @brief Channel top back right
+ GAME_CH_TBR,
+
+ /// @brief Channel top back center
+ GAME_CH_TBC,
+
+ /// @brief Channel bacl left over center
+ GAME_CH_BLOC,
+
+ /// @brief Channel back right over center
+ GAME_CH_BROC,
+ } GAME_AUDIO_CHANNEL;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Game audio stream properties**
+ ///
+ /// Used by Kodi to pass the currently required audio stream settings to the addon
+ ///
+ typedef struct game_stream_audio_properties
+ {
+ GAME_PCM_FORMAT format;
+ const GAME_AUDIO_CHANNEL* channel_map;
+ } ATTR_PACKED game_stream_audio_properties;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Audio stream packet**
+ ///
+ /// This packet contains audio stream data passed to Kodi.
+ ///
+ typedef struct game_stream_audio_packet
+ {
+ /// @brief Pointer for audio stream data given to Kodi
+ const uint8_t* data;
+
+ /// @brief Size of data array
+ size_t size;
+ } ATTR_PACKED game_stream_audio_packet;
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==--
+ /// @defgroup cpp_kodi_addon_game_Defs_VideoStream 2. Video stream
+ /// @ingroup cpp_kodi_addon_game_Defs
+ /// @brief **The for Video stream used data system**
+ ///
+ /// Used to give Addon currently used video stream configuration on Kodi and
+ /// arrays to give related data to Kodi on callbacks.
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief **Pixel format**
+ ///
+ /// From Kodi requested specified video RGB color model format.
+ ///
+ typedef enum GAME_PIXEL_FORMAT
+ {
+ GAME_PIXEL_FORMAT_UNKNOWN,
+
+ /// @brief 0RGB8888 Format
+ GAME_PIXEL_FORMAT_0RGB8888,
+
+ /// @brief RGB565 Format
+ GAME_PIXEL_FORMAT_RGB565,
+
+ /// @brief 0RGB1555 Format
+ GAME_PIXEL_FORMAT_0RGB1555,
+ } GAME_PIXEL_FORMAT;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Video rotation position**
+ ///
+ /// To define position how video becomes shown.
+ ///
+ typedef enum GAME_VIDEO_ROTATION
+ {
+ /// @brief 0° and Without rotation
+ GAME_VIDEO_ROTATION_0,
+
+ /// @brief rotate 90° counterclockwise
+ GAME_VIDEO_ROTATION_90_CCW,
+
+ /// @brief rotate 180° counterclockwise
+ GAME_VIDEO_ROTATION_180_CCW,
+
+ /// @brief rotate 270° counterclockwise
+ GAME_VIDEO_ROTATION_270_CCW,
+ } GAME_VIDEO_ROTATION;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Game video stream properties**
+ ///
+ /// Used by Kodi to pass the currently required video stream settings to the addon
+ ///
+ typedef struct game_stream_video_properties
+ {
+ /// @brief The stream's pixel format
+ GAME_PIXEL_FORMAT format;
+
+ /// @brief The nominal used width
+ unsigned int nominal_width;
+
+ /// @brief The nominal used height
+ unsigned int nominal_height;
+
+ /// @brief The maximal used width
+ unsigned int max_width;
+
+ /// @brief The maximal used height
+ unsigned int max_height;
+
+ /// @brief On video stream used aspect ration
+ ///
+ /// @note If aspect_ratio is <= 0.0, an aspect ratio of nominal_width / nominal_height is assumed
+ float aspect_ratio;
+ } ATTR_PACKED game_stream_video_properties;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Video stream packet**
+ ///
+ /// This packet contains video stream data passed to Kodi.
+ ///
+ typedef struct game_stream_video_packet
+ {
+ /// @brief Video height
+ unsigned int width;
+
+ /// @brief Video width
+ unsigned int height;
+
+ /// @brief Width @ref GAME_VIDEO_ROTATION defined rotation angle.
+ GAME_VIDEO_ROTATION rotation;
+
+ /// @brief Pointer for video stream data given to Kodi
+ const uint8_t* data;
+
+ /// @brief Size of data array
+ size_t size;
+ } ATTR_PACKED game_stream_video_packet;
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==--
+ /// @defgroup cpp_kodi_addon_game_Defs_HardwareFramebuffer 3. Hardware framebuffer stream
+ /// @ingroup cpp_kodi_addon_game_Defs
+ /// @brief **Hardware framebuffer stream data**
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief **Hardware framebuffer type**
+ ///
+ typedef enum GAME_HW_CONTEXT_TYPE
+ {
+ /// @brief None context
+ GAME_HW_CONTEXT_NONE,
+
+ /// @brief OpenGL 2.x. Driver can choose to use latest compatibility context
+ GAME_HW_CONTEXT_OPENGL,
+
+ /// @brief OpenGL ES 2.0
+ GAME_HW_CONTEXT_OPENGLES2,
+
+ /// @brief Modern desktop core GL context. Use major/minor fields to set GL version
+ GAME_HW_CONTEXT_OPENGL_CORE,
+
+ /// @brief OpenGL ES 3.0
+ GAME_HW_CONTEXT_OPENGLES3,
+
+ /// @brief OpenGL ES 3.1+. Set major/minor fields.
+ GAME_HW_CONTEXT_OPENGLES_VERSION,
+
+ /// @brief Vulkan
+ GAME_HW_CONTEXT_VULKAN
+ } GAME_HW_CONTEXT_TYPE;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Hardware framebuffer properties**
+ ///
+ typedef struct game_stream_hw_framebuffer_properties
+ {
+ /// @brief The API to use.
+ ///
+ GAME_HW_CONTEXT_TYPE context_type;
+
+ /// @brief Set if render buffers should have depth component attached.
+ ///
+ /// @todo: Obsolete
+ ///
+ bool depth;
+
+ /// @brief Set if stencil buffers should be attached.
+ ///
+ /// If depth and stencil are true, a packed 24/8 buffer will be added.
+ /// Only attaching stencil is invalid and will be ignored.
+ ///
+ /// @todo: Obsolete.
+ ///
+ bool stencil;
+
+ /// @brief Use conventional bottom-left origin convention.
+ ///
+ /// If false, standard top-left origin semantics are used.
+ ///
+ /// @todo: Move to GL specific interface
+ ///
+ bool bottom_left_origin;
+
+ /// @brief Major version number for core GL context or GLES 3.1+.
+ unsigned int version_major;
+
+ /// @brief Minor version number for core GL context or GLES 3.1+.
+ unsigned int version_minor;
+
+ /// @brief If this is true, the frontend will go very far to avoid resetting context
+ /// in scenarios like toggling fullscreen, etc.
+ ///
+ /// @todo: Obsolete? Maybe frontend should just always assume this...
+ ///
+ /// The reset callback might still be called in extreme situations such as if
+ /// the context is lost beyond recovery.
+ ///
+ /// For optimal stability, set this to false, and allow context to be reset at
+ /// any time.
+ ///
+ bool cache_context;
+
+ /// @brief Creates a debug context.
+ bool debug_context;
+ } ATTR_PACKED game_stream_hw_framebuffer_properties;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Hardware framebuffer buffer**
+ ///
+ typedef struct game_stream_hw_framebuffer_buffer
+ {
+ /// @brief
+ uintptr_t framebuffer;
+ } ATTR_PACKED game_stream_hw_framebuffer_buffer;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Hardware framebuffer packet**
+ ///
+ typedef struct game_stream_hw_framebuffer_packet
+ {
+ /// @brief
+ uintptr_t framebuffer;
+ } ATTR_PACKED game_stream_hw_framebuffer_packet;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Hardware framebuffer process function address**
+ ///
+ typedef void (*game_proc_address_t)(void);
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==--
+ /// @defgroup cpp_kodi_addon_game_Defs_SoftwareFramebuffer 4. Software framebuffer stream
+ /// @ingroup cpp_kodi_addon_game_Defs
+ /// @brief **Software framebuffer stream data**
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief **Game video stream properties**
+ ///
+ /// Used by Kodi to pass the currently required video stream settings to the addon
+ ///
+ typedef game_stream_video_properties game_stream_sw_framebuffer_properties;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Hardware framebuffer type**
+ ///
+ typedef struct game_stream_sw_framebuffer_buffer
+ {
+ GAME_PIXEL_FORMAT format;
+ uint8_t* data;
+ size_t size;
+ } ATTR_PACKED game_stream_sw_framebuffer_buffer;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Video stream packet**
+ ///
+ /// This packet contains video stream data passed to Kodi.
+ ///
+ typedef game_stream_video_packet game_stream_sw_framebuffer_packet;
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==--
+ /// @defgroup cpp_kodi_addon_game_Defs_StreamTypes 5. Stream types
+ /// @ingroup cpp_kodi_addon_game_Defs
+ /// @brief **Stream types data**
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief **Game stream types**
+ ///
+ typedef enum GAME_STREAM_TYPE
+ {
+ /// @brief Unknown
+ GAME_STREAM_UNKNOWN,
+
+ /// @brief Audio stream
+ GAME_STREAM_AUDIO,
+
+ /// @brief Video stream
+ GAME_STREAM_VIDEO,
+
+ /// @brief Hardware framebuffer
+ GAME_STREAM_HW_FRAMEBUFFER,
+
+ /// @brief Software framebuffer
+ GAME_STREAM_SW_FRAMEBUFFER,
+ } GAME_STREAM_TYPE;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Immutable stream metadata**
+ ///
+ /// This metadata is provided when the stream is opened. If any stream
+ /// properties change, a new stream must be opened.
+ ///
+ typedef struct game_stream_properties
+ {
+ /// @brief
+ GAME_STREAM_TYPE type;
+ union
+ {
+ /// @brief
+ game_stream_audio_properties audio;
+
+ /// @brief
+ game_stream_video_properties video;
+
+ /// @brief
+ game_stream_hw_framebuffer_properties hw_framebuffer;
+
+ /// @brief
+ game_stream_sw_framebuffer_properties sw_framebuffer;
+ };
+ } ATTR_PACKED game_stream_properties;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Stream buffers for hardware rendering and zero-copy support**
+ ///
+ typedef struct game_stream_buffer
+ {
+ /// @brief
+ GAME_STREAM_TYPE type;
+ union
+ {
+ /// @brief
+ game_stream_hw_framebuffer_buffer hw_framebuffer;
+
+ /// @brief
+ game_stream_sw_framebuffer_buffer sw_framebuffer;
+ };
+ } ATTR_PACKED game_stream_buffer;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Stream packet and ephemeral metadata**
+ ///
+ /// This packet contains stream data and accompanying metadata. The metadata
+ /// is ephemeral, meaning it only applies to the current packet and can change
+ /// from packet to packet in the same stream.
+ ///
+ typedef struct game_stream_packet
+ {
+ /// @brief
+ GAME_STREAM_TYPE type;
+ union
+ {
+ /// @brief
+ game_stream_audio_packet audio;
+
+ /// @brief
+ game_stream_video_packet video;
+
+ /// @brief
+ game_stream_hw_framebuffer_packet hw_framebuffer;
+
+ /// @brief
+ game_stream_sw_framebuffer_packet sw_framebuffer;
+ };
+ } ATTR_PACKED game_stream_packet;
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==--
+ /// @defgroup cpp_kodi_addon_game_Defs_GameTypes 6. Game types
+ /// @ingroup cpp_kodi_addon_game_Defs
+ /// @brief **Game types data**
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief **Game reguin definition**
+ ///
+ /// Returned from game_get_region()
+ typedef enum GAME_REGION
+ {
+ /// @brief Game region unknown
+ GAME_REGION_UNKNOWN,
+
+ /// @brief Game region NTSC
+ GAME_REGION_NTSC,
+
+ /// @brief Game region PAL
+ GAME_REGION_PAL,
+ } GAME_REGION;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Special game types passed into game_load_game_special().**
+ ///
+ /// @remark Only used when multiple ROMs are required.
+ ///
+ typedef enum SPECIAL_GAME_TYPE
+ {
+ /// @brief Game Type BSX
+ SPECIAL_GAME_TYPE_BSX,
+
+ /// @brief Game Type BSX slotted
+ SPECIAL_GAME_TYPE_BSX_SLOTTED,
+
+ /// @brief Game Type sufami turbo
+ SPECIAL_GAME_TYPE_SUFAMI_TURBO,
+
+ /// @brief Game Type super game boy
+ SPECIAL_GAME_TYPE_SUPER_GAME_BOY,
+ } SPECIAL_GAME_TYPE;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **Game Memory**
+ ///
+ typedef enum GAME_MEMORY
+ {
+ /// @brief Passed to game_get_memory_data/size(). If the memory type doesn't apply
+ /// to the implementation NULL/0 can be returned.
+ GAME_MEMORY_MASK = 0xff,
+
+ /// @brief Regular save ram.
+ ///
+ /// This ram is usually found on a game cartridge, backed
+ /// up by a battery. If save game data is too complex for a single memory
+ /// buffer, the SYSTEM_DIRECTORY environment callback can be used.
+ GAME_MEMORY_SAVE_RAM = 0,
+
+ /// @brief Some games have a built-in clock to keep track of time.
+ ///
+ /// This memory is usually just a couple of bytes to keep track of time.
+ GAME_MEMORY_RTC = 1,
+
+ /// @brief System ram lets a frontend peek into a game systems main RAM
+ GAME_MEMORY_SYSTEM_RAM = 2,
+
+ /// @brief Video ram lets a frontend peek into a game systems video RAM (VRAM)
+ GAME_MEMORY_VIDEO_RAM = 3,
+
+ /// @brief Special memory type
+ GAME_MEMORY_SNES_BSX_RAM = ((1 << 8) | GAME_MEMORY_SAVE_RAM),
+
+ /// @brief Special memory type
+ GAME_MEMORY_SNES_BSX_PRAM = ((2 << 8) | GAME_MEMORY_SAVE_RAM),
+
+ /// @brief Special memory type
+ GAME_MEMORY_SNES_SUFAMI_TURBO_A_RAM = ((3 << 8) | GAME_MEMORY_SAVE_RAM),
+
+ /// @brief Special memory type
+ GAME_MEMORY_SNES_SUFAMI_TURBO_B_RAM = ((4 << 8) | GAME_MEMORY_SAVE_RAM),
+
+ /// @brief Special memory type
+ GAME_MEMORY_SNES_GAME_BOY_RAM = ((5 << 8) | GAME_MEMORY_SAVE_RAM),
+
+ /// @brief Special memory type
+ GAME_MEMORY_SNES_GAME_BOY_RTC = ((6 << 8) | GAME_MEMORY_RTC),
+ } GAME_MEMORY;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief **ID values for SIMD CPU features**
+ typedef enum GAME_SIMD
+ {
+ /// @brief SIMD CPU SSE
+ GAME_SIMD_SSE = (1 << 0),
+
+ /// @brief SIMD CPU SSE2
+ GAME_SIMD_SSE2 = (1 << 1),
+
+ /// @brief SIMD CPU VMX
+ GAME_SIMD_VMX = (1 << 2),
+
+ /// @brief SIMD CPU VMX128
+ GAME_SIMD_VMX128 = (1 << 3),
+
+ /// @brief SIMD CPU AVX
+ GAME_SIMD_AVX = (1 << 4),
+
+ /// @brief SIMD CPU NEON
+ GAME_SIMD_NEON = (1 << 5),
+
+ /// @brief SIMD CPU SSE3
+ GAME_SIMD_SSE3 = (1 << 6),
+
+ /// @brief SIMD CPU SSSE3
+ GAME_SIMD_SSSE3 = (1 << 7),
+
+ /// @brief SIMD CPU MMX
+ GAME_SIMD_MMX = (1 << 8),
+
+ /// @brief SIMD CPU MMXEXT
+ GAME_SIMD_MMXEXT = (1 << 9),
+
+ /// @brief SIMD CPU SSE4
+ GAME_SIMD_SSE4 = (1 << 10),
+
+ /// @brief SIMD CPU SSE42
+ GAME_SIMD_SSE42 = (1 << 11),
+
+ /// @brief SIMD CPU AVX2
+ GAME_SIMD_AVX2 = (1 << 12),
+
+ /// @brief SIMD CPU VFPU
+ GAME_SIMD_VFPU = (1 << 13),
+ } GAME_SIMD;
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==--
+ /// @defgroup cpp_kodi_addon_game_Defs_InputTypes 7. Input types
+ /// @ingroup cpp_kodi_addon_game_Defs
+ /// @brief **Input types**
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief
+ typedef enum GAME_INPUT_EVENT_SOURCE
+ {
+ /// @brief
+ GAME_INPUT_EVENT_DIGITAL_BUTTON,
+
+ /// @brief
+ GAME_INPUT_EVENT_ANALOG_BUTTON,
+
+ /// @brief
+ GAME_INPUT_EVENT_AXIS,
+
+ /// @brief
+ GAME_INPUT_EVENT_ANALOG_STICK,
+
+ /// @brief
+ GAME_INPUT_EVENT_ACCELEROMETER,
+
+ /// @brief
+ GAME_INPUT_EVENT_KEY,
+
+ /// @brief
+ GAME_INPUT_EVENT_RELATIVE_POINTER,
+
+ /// @brief
+ GAME_INPUT_EVENT_ABSOLUTE_POINTER,
+
+ /// @brief
+ GAME_INPUT_EVENT_MOTOR,
+ } GAME_INPUT_EVENT_SOURCE;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief
+ typedef enum GAME_KEY_MOD
+ {
+ /// @brief
+ GAME_KEY_MOD_NONE = 0x0000,
+
+ /// @brief
+ GAME_KEY_MOD_SHIFT = 0x0001,
+
+ /// @brief
+ GAME_KEY_MOD_CTRL = 0x0002,
+
+ /// @brief
+ GAME_KEY_MOD_ALT = 0x0004,
+
+ /// @brief
+ GAME_KEY_MOD_META = 0x0008,
+
+ /// @brief
+ GAME_KEY_MOD_SUPER = 0x0010,
+
+ /// @brief
+ GAME_KEY_MOD_NUMLOCK = 0x0100,
+
+ /// @brief
+ GAME_KEY_MOD_CAPSLOCK = 0x0200,
+
+ /// @brief
+ GAME_KEY_MOD_SCROLLOCK = 0x0400,
+ } GAME_KEY_MOD;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Type of port on the virtual game console
+ typedef enum GAME_PORT_TYPE
+ {
+ /// @brief Game port unknown
+ GAME_PORT_UNKNOWN,
+
+ /// @brief Game port Keyboard
+ GAME_PORT_KEYBOARD,
+
+ /// @brief Game port mouse
+ GAME_PORT_MOUSE,
+
+ /// @brief Game port controller
+ GAME_PORT_CONTROLLER,
+ } GAME_PORT_TYPE;
+ //----------------------------------------------------------------------------
+
+ /*! @cond PRIVATE */
+ /*!
+ * @brief "C" Game add-on controller layout.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref AddonGameControllerLayout for description of values.
+ */
+ typedef struct game_controller_layout
+ {
+ char* controller_id;
+ bool provides_input; // False for multitaps
+ char** digital_buttons;
+ unsigned int digital_button_count;
+ char** analog_buttons;
+ unsigned int analog_button_count;
+ char** analog_sticks;
+ unsigned int analog_stick_count;
+ char** accelerometers;
+ unsigned int accelerometer_count;
+ char** keys;
+ unsigned int key_count;
+ char** rel_pointers;
+ unsigned int rel_pointer_count;
+ char** abs_pointers;
+ unsigned int abs_pointer_count;
+ char** motors;
+ unsigned int motor_count;
+ } ATTR_PACKED game_controller_layout;
+ /*! @endcond */
+
+ struct game_input_port;
+
+ //============================================================================
+ /// @brief Device that can provide input
+ typedef struct game_input_device
+ {
+ /// @brief ID used in the Kodi controller API
+ const char* controller_id;
+
+ /// @brief
+ const char* port_address;
+
+ /// @brief
+ struct game_input_port* available_ports;
+
+ /// @brief
+ unsigned int port_count;
+ } ATTR_PACKED game_input_device;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Port that can provide input
+ ///
+ /// Ports can accept multiple devices and devices can have multiple ports, so
+ /// the topology of possible configurations is a tree structure of alternating
+ /// port and device nodes.
+ ///
+ typedef struct game_input_port
+ {
+ /// @brief
+ GAME_PORT_TYPE type;
+
+ /// @brief Required for GAME_PORT_CONTROLLER type
+ const char* port_id;
+
+ /// @brief Flag to prevent a port from being disconnected
+ ///
+ /// Set to true to prevent a disconnection option from appearing in the
+ /// GUI.
+ ///
+ bool force_connected;
+
+ /// @brief
+ game_input_device* accepted_devices;
+
+ /// @brief
+ unsigned int device_count;
+ } ATTR_PACKED game_input_port;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief The input topology is the possible ways to connect input devices
+ ///
+ /// This represents the logical topology, which is the possible connections that
+ /// the game client's logic can handle. It is strictly a subset of the physical
+ /// topology. Loops are not allowed.
+ ///
+ typedef struct game_input_topology
+ {
+ /// @brief The list of ports on the virtual game console
+ game_input_port* ports;
+
+ /// @brief The number of ports
+ unsigned int port_count;
+
+ /// @brief A limit on the number of input-providing devices, or -1 for no limit
+ int player_limit;
+ } ATTR_PACKED game_input_topology;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief
+ typedef struct game_digital_button_event
+ {
+ /// @brief
+ bool pressed;
+ } ATTR_PACKED game_digital_button_event;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief
+ typedef struct game_analog_button_event
+ {
+ /// @brief
+ float magnitude;
+ } ATTR_PACKED game_analog_button_event;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief
+ typedef struct game_axis_event
+ {
+ /// @brief
+ float position;
+ } ATTR_PACKED game_axis_event;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief
+ typedef struct game_analog_stick_event
+ {
+ /// @brief
+ float x;
+
+ /// @brief
+ float y;
+ } ATTR_PACKED game_analog_stick_event;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief
+ typedef struct game_accelerometer_event
+ {
+ /// @brief
+ float x;
+
+ /// @brief
+ float y;
+
+ /// @brief
+ float z;
+ } ATTR_PACKED game_accelerometer_event;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief
+ typedef struct game_key_event
+ {
+ /// @brief
+ bool pressed;
+
+ /// @brief If the keypress generates a printing character
+ ///
+ /// The unicode value contains the character generated. If the key is a
+ /// non-printing character, e.g. a function or arrow key, the unicode value
+ /// is zero.
+ uint32_t unicode;
+
+ /// @brief
+ GAME_KEY_MOD modifiers;
+ } ATTR_PACKED game_key_event;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief
+ typedef struct game_rel_pointer_event
+ {
+ /// @brief
+ int x;
+
+ /// @brief
+ int y;
+ } ATTR_PACKED game_rel_pointer_event;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief
+ typedef struct game_abs_pointer_event
+ {
+ /// @brief
+ bool pressed;
+
+ /// @brief
+ float x;
+
+ /// @brief
+ float y;
+ } ATTR_PACKED game_abs_pointer_event;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief
+ typedef struct game_motor_event
+ {
+ /// @brief
+ float magnitude;
+ } ATTR_PACKED game_motor_event;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief
+ typedef struct game_input_event
+ {
+ /// @brief
+ GAME_INPUT_EVENT_SOURCE type;
+
+ /// @brief
+ const char* controller_id;
+
+ /// @brief
+ GAME_PORT_TYPE port_type;
+
+ /// @brief
+ const char* port_address;
+
+ /// @brief
+ const char* feature_name;
+ union
+ {
+ /// @brief
+ struct game_digital_button_event digital_button;
+
+ /// @brief
+ struct game_analog_button_event analog_button;
+
+ /// @brief
+ struct game_axis_event axis;
+
+ /// @brief
+ struct game_analog_stick_event analog_stick;
+
+ /// @brief
+ struct game_accelerometer_event accelerometer;
+
+ /// @brief
+ struct game_key_event key;
+
+ /// @brief
+ struct game_rel_pointer_event rel_pointer;
+
+ /// @brief
+ struct game_abs_pointer_event abs_pointer;
+
+ /// @brief
+ struct game_motor_event motor;
+ };
+ } ATTR_PACKED game_input_event;
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==--
+ /// @defgroup cpp_kodi_addon_game_Defs_EnvironmentTypes 8. Environment types
+ /// @ingroup cpp_kodi_addon_game_Defs
+ /// @brief **Environment types**
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Game system timing
+ ///
+ struct game_system_timing
+ {
+ /// @brief FPS of video content.
+ double fps;
+
+ /// @brief Sampling rate of audio.
+ double sample_rate;
+ };
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==--
+
+ /*!
+ * @brief Game properties
+ *
+ * Not to be used outside this header.
+ */
+ typedef struct AddonProps_Game
+ {
+ /*!
+ * The path of the game client being loaded.
+ */
+ const char* game_client_dll_path;
+
+ /*!
+ * Paths to proxy DLLs used to load the game client.
+ */
+ const char** proxy_dll_paths;
+
+ /*!
+ * Number of proxy DLL paths provided.
+ */
+ unsigned int proxy_dll_count;
+
+ /*!
+ * The "system" directories of the frontend. These directories can be used to
+ * store system-specific ROMs such as BIOSes, configuration data, etc.
+ */
+ const char** resource_directories;
+
+ /*!
+ * Number of resource directories provided
+ */
+ unsigned int resource_directory_count;
+
+ /*!
+ * The writable directory of the frontend. This directory can be used to store
+ * SRAM, memory cards, high scores, etc, if the game client cannot use the
+ * regular memory interface, GetMemoryData().
+ */
+ const char* profile_directory;
+
+ /*!
+ * The value of the <supports_vfs> property from addon.xml
+ */
+ bool supports_vfs;
+
+ /*!
+ * The extensions in the <extensions> property from addon.xml
+ */
+ const char** extensions;
+
+ /*!
+ * Number of extensions provided
+ */
+ unsigned int extension_count;
+ } AddonProps_Game;
+
+ typedef void* KODI_GAME_STREAM_HANDLE;
+
+ /*! Structure to transfer the methods from kodi_game_dll.h to Kodi */
+
+ struct AddonInstance_Game;
+
+ /*!
+ * @brief Game callbacks
+ *
+ * Not to be used outside this header.
+ */
+ typedef struct AddonToKodiFuncTable_Game
+ {
+ KODI_HANDLE kodiInstance;
+
+ void (*CloseGame)(KODI_HANDLE kodiInstance);
+ KODI_GAME_STREAM_HANDLE (*OpenStream)(KODI_HANDLE, const struct game_stream_properties*);
+ bool (*GetStreamBuffer)(KODI_HANDLE,
+ KODI_GAME_STREAM_HANDLE,
+ unsigned int,
+ unsigned int,
+ struct game_stream_buffer*);
+ void (*AddStreamData)(KODI_HANDLE, KODI_GAME_STREAM_HANDLE, const struct game_stream_packet*);
+ void (*ReleaseStreamBuffer)(KODI_HANDLE, KODI_GAME_STREAM_HANDLE, struct game_stream_buffer*);
+ void (*CloseStream)(KODI_HANDLE, KODI_GAME_STREAM_HANDLE);
+ game_proc_address_t (*HwGetProcAddress)(KODI_HANDLE kodiInstance, const char* symbol);
+ bool (*InputEvent)(KODI_HANDLE kodiInstance, const struct game_input_event* event);
+ } AddonToKodiFuncTable_Game;
+
+ /*!
+ * @brief Game function hooks
+ *
+ * Not to be used outside this header.
+ */
+ typedef struct KodiToAddonFuncTable_Game
+ {
+ KODI_HANDLE addonInstance;
+
+ GAME_ERROR(__cdecl* LoadGame)(const struct AddonInstance_Game*, const char*);
+ GAME_ERROR(__cdecl* LoadGameSpecial)
+ (const struct AddonInstance_Game*, enum SPECIAL_GAME_TYPE, const char**, size_t);
+ GAME_ERROR(__cdecl* LoadStandalone)(const struct AddonInstance_Game*);
+ GAME_ERROR(__cdecl* UnloadGame)(const struct AddonInstance_Game*);
+ GAME_ERROR(__cdecl* GetGameTiming)
+ (const struct AddonInstance_Game*, struct game_system_timing*);
+ GAME_REGION(__cdecl* GetRegion)(const struct AddonInstance_Game*);
+ bool(__cdecl* RequiresGameLoop)(const struct AddonInstance_Game*);
+ GAME_ERROR(__cdecl* RunFrame)(const struct AddonInstance_Game*);
+ GAME_ERROR(__cdecl* Reset)(const struct AddonInstance_Game*);
+ GAME_ERROR(__cdecl* HwContextReset)(const struct AddonInstance_Game*);
+ GAME_ERROR(__cdecl* HwContextDestroy)(const struct AddonInstance_Game*);
+ bool(__cdecl* HasFeature)(const struct AddonInstance_Game*, const char*, const char*);
+ game_input_topology*(__cdecl* GetTopology)(const struct AddonInstance_Game*);
+ void(__cdecl* FreeTopology)(const struct AddonInstance_Game*, struct game_input_topology*);
+ void(__cdecl* SetControllerLayouts)(const struct AddonInstance_Game*,
+ const struct game_controller_layout*,
+ unsigned int);
+ bool(__cdecl* EnableKeyboard)(const struct AddonInstance_Game*, bool, const char*);
+ bool(__cdecl* EnableMouse)(const struct AddonInstance_Game*, bool, const char*);
+ bool(__cdecl* ConnectController)(const struct AddonInstance_Game*,
+ bool,
+ const char*,
+ const char*);
+ bool(__cdecl* InputEvent)(const struct AddonInstance_Game*, const struct game_input_event*);
+ size_t(__cdecl* SerializeSize)(const struct AddonInstance_Game*);
+ GAME_ERROR(__cdecl* Serialize)(const struct AddonInstance_Game*, uint8_t*, size_t);
+ GAME_ERROR(__cdecl* Deserialize)(const struct AddonInstance_Game*, const uint8_t*, size_t);
+ GAME_ERROR(__cdecl* CheatReset)(const struct AddonInstance_Game*);
+ GAME_ERROR(__cdecl* GetMemory)
+ (const struct AddonInstance_Game*, enum GAME_MEMORY, uint8_t**, size_t*);
+ GAME_ERROR(__cdecl* SetCheat)
+ (const struct AddonInstance_Game*, unsigned int, bool, const char*);
+ GAME_ERROR(__cdecl* RCGenerateHashFromFile)
+ (const AddonInstance_Game*, char**, unsigned int, const char*);
+ GAME_ERROR(__cdecl* RCGetGameIDUrl)(const AddonInstance_Game*, char**, const char*);
+ GAME_ERROR(__cdecl* RCGetPatchFileUrl)
+ (const AddonInstance_Game*, char**, const char*, const char*, unsigned int);
+ GAME_ERROR(__cdecl* RCPostRichPresenceUrl)
+ (const AddonInstance_Game*,
+ char**,
+ char**,
+ const char*,
+ const char*,
+ unsigned int,
+ const char*);
+ GAME_ERROR(__cdecl* RCEnableRichPresence)(const AddonInstance_Game*, const char*);
+ GAME_ERROR(__cdecl* RCGetRichPresenceEvaluation)
+ (const AddonInstance_Game*, char**, unsigned int);
+ GAME_ERROR(__cdecl* RCResetRuntime)(const AddonInstance_Game*);
+ void(__cdecl* FreeString)(const AddonInstance_Game*, char*);
+ } KodiToAddonFuncTable_Game;
+
+ /*!
+ * @brief Game instance
+ *
+ * Not to be used outside this header.
+ */
+ typedef struct AddonInstance_Game
+ {
+ struct AddonProps_Game* props;
+ struct AddonToKodiFuncTable_Game* toKodi;
+ struct KodiToAddonFuncTable_Game* toAddon;
+ } AddonInstance_Game;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_GAME_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/imagedecoder.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/imagedecoder.h
new file mode 100644
index 0000000..a72e2f8
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/imagedecoder.h
@@ -0,0 +1,450 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_IMAGEDECODER_H
+#define C_API_ADDONINSTANCE_IMAGEDECODER_H
+
+#include "../addon_base.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef KODI_ADDON_INSTANCE_HDL KODI_ADDON_IMAGEDECODER_HDL;
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_FMT enum ADDON_IMG_FMT
+ /// @ingroup cpp_kodi_addon_imagedecoder_Defs
+ /// @brief **Image format types**\n
+ /// Used to define wanted target format where image decoder should give to
+ /// Kodi.
+ ///
+ ///@{
+ typedef enum ADDON_IMG_FMT
+ {
+ /// @brief A 32-bit ARGB pixel format, with alpha, that uses 8 bits per
+ /// channel, ARGBARGB...
+ ADDON_IMG_FMT_A8R8G8B8 = 1,
+
+ /// @brief A 8, alpha only, 8bpp, AAA...
+ ADDON_IMG_FMT_A8 = 2,
+
+ /// @brief RGBA 8:8:8:8, with alpha, 32bpp, RGBARGBA...
+ ADDON_IMG_FMT_RGBA8 = 3,
+
+ /// @brief RGB 8:8:8, with alpha, 24bpp, RGBRGB...
+ ADDON_IMG_FMT_RGB8 = 4
+ } ADDON_IMG_FMT;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_ORIENTATION enum ADDON_IMG_ORIENTATION
+ /// @ingroup cpp_kodi_addon_imagedecoder_Defs
+ /// @brief **Image orientation types**\n
+ /// Used to define how image becomes orientated for show.
+ ///
+ ///@{
+ typedef enum ADDON_IMG_ORIENTATION
+ {
+ /// @brief If not available
+ ADDON_IMG_ORIENTATION_NONE = 0,
+
+ /// @brief Flip horizontal
+ ADDON_IMG_ORIENTATION_FLIP_HORIZONTAL = 1,
+
+ /// @brief Rotate 180° CCW
+ ADDON_IMG_ORIENTATION_ROTATE_180_CCW = 2,
+
+ /// @brief Flip vertical
+ ADDON_IMG_ORIENTATION_FLIP_VERTICAL = 3,
+
+ /// @brief Transpose
+ ADDON_IMG_ORIENTATION_TRANSPOSE = 4,
+
+ /// @brief Rotate 270° CCW
+ ADDON_IMG_ORIENTATION_ROTATE_270_CCW = 5,
+
+ /// @brief Transpose off axis
+ ADDON_IMG_ORIENTATION_TRANSPOSE_OFF_AXIS = 6,
+
+ /// @brief Rotate 90° CCW
+ ADDON_IMG_ORIENTATION_ROTATE_90_CCW = 7,
+ } ADDON_IMG_ORIENTATION;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_COLOR enum ADDON_IMG_COLOR
+ /// @ingroup cpp_kodi_addon_imagedecoder_Defs
+ /// @brief **Image color type**\n
+ /// To set image as colored or black/white.
+ ///
+ ///@{
+ typedef enum ADDON_IMG_COLOR
+ {
+ /// @brief Colored image
+ ADDON_IMG_COLOR_COLORED,
+
+ /// @brief Black/White image
+ ADDON_IMG_COLOR_BLACK_WHITE
+ } ADDON_IMG_COLOR;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_METERING_MODE enum ADDON_IMG_METERING_MODE
+ /// @ingroup cpp_kodi_addon_imagedecoder_Defs
+ /// @brief **Image metering mode**
+ ///
+ ///@{
+ typedef enum ADDON_IMG_METERING_MODE
+ {
+ /// @brief 0 = Unknown
+ ADDON_IMG_METERING_MODE_UNKNOWN = 0,
+
+ /// @brief 1 = Average
+ ADDON_IMG_METERING_MODE_AVERAGE = 1,
+
+ /// @brief 2 = Center-weighted average
+ ADDON_IMG_METERING_MODE_CENTER_WEIGHT = 2,
+
+ /// @brief 3 = Spot
+ ADDON_IMG_METERING_MODE_SPOT = 3,
+
+ /// @brief 4 = MultiSpot
+ ADDON_IMG_METERING_MODE_MULTI_SPOT = 4,
+
+ /// @brief 5 = Pattern
+ ADDON_IMG_METERING_MODE_MULTI_SEGMENT = 5,
+
+ /// @brief 6 = Partial
+ ADDON_IMG_METERING_MODE_PARTIAL = 6,
+
+ /// @brief 255 = other
+ ADDON_IMG_METERING_MODE_OTHER = 255,
+ } ADDON_IMG_METERING_MODE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_EXPOSURE_PROGRAM enum ADDON_IMG_EXPOSURE_PROGRAM
+ /// @ingroup cpp_kodi_addon_imagedecoder_Defs
+ /// @brief **Exposure program**\n
+ /// The class of the program used by the camera to set exposure when the picture is taken.
+ ///
+ ///@{
+ typedef enum ADDON_IMG_EXPOSURE_PROGRAM
+ {
+ /// @brief 0 = Not Defined
+ ADDON_IMG_EXPOSURE_PROGRAM_UNDEFINED = 0,
+
+ /// @brief 1 = Manual
+ ADDON_IMG_EXPOSURE_PROGRAM_MANUAL = 1,
+
+ /// @brief 2 = Normal program
+ ADDON_IMG_EXPOSURE_PROGRAM_NORMAL = 2,
+
+ /// @brief 3 = Aperture-priority
+ ADDON_IMG_EXPOSURE_PROGRAM_APERTURE_PRIORITY = 3,
+
+ /// @brief 4 = Shutter speed priority
+ ADDON_IMG_EXPOSURE_PROGRAM_SHUTTER_SPEED_PRIORITY = 4,
+
+ /// @brief 5 = Creative program (biased toward depth of field)
+ ADDON_IMG_EXPOSURE_PROGRAM_CREATIVE = 5,
+
+ /// @brief 6 = Action program (biased toward fast shutter speed)
+ ADDON_IMG_EXPOSURE_PROGRAM_ACTION = 6,
+
+ /// @brief 7 = Portrait mode (for closeup photos with the background out of focus)
+ ADDON_IMG_EXPOSURE_PROGRAM_PORTRAIT = 7,
+
+ /// @brief 8 = Landscape mode (for landscape photos with the background in focus)
+ ADDON_IMG_EXPOSURE_PROGRAM_LANDSCAPE = 8,
+
+ /// @brief 9 = Bulb
+ /// @note The value of 9 is not standard EXIF, but is used by the Canon EOS 7D
+ ADDON_IMG_EXPOSURE_PROGRAM_BULB = 9
+ } ADDON_IMG_EXPOSURE_PROGRAM;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_EXPOSURE_MODE enum ADDON_IMG_EXPOSURE_MODE
+ /// @ingroup cpp_kodi_addon_imagedecoder_Defs
+ /// @brief **Exposure mode**\n
+ /// Indicates the exposure mode set when the image was shot.
+ ///
+ /// In auto-bracketing mode, the camera shoots a series of frames of the same
+ /// scene at different exposure settings.
+ ///
+ ///@{
+ typedef enum ADDON_IMG_EXPOSURE_MODE
+ {
+ /// @brief 0 = Auto exposure
+ ADDON_IMG_EXPOSURE_MODE_AUTO = 0,
+
+ /// @brief 1 = Manual exposure
+ ADDON_IMG_EXPOSURE_MODE_MANUAL = 1,
+
+ /// @brief 2 = Auto bracket
+ ADDON_IMG_EXPOSURE_MODE_AUTO_TRACKED = 2,
+ } ADDON_IMG_EXPOSURE_MODE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_LIGHT_SOURCE enum ADDON_IMG_LIGHT_SOURCE
+ /// @ingroup cpp_kodi_addon_imagedecoder_Defs
+ /// @brief **Kind of light source**
+ ///
+ ///@{
+ typedef enum ADDON_IMG_LIGHT_SOURCE
+ {
+ /// @brief 0 = Unknown
+ ADDON_IMG_LIGHT_SOURCE_UNKNOWN = 0,
+
+ /// @brief 1 = Daylight
+ ADDON_IMG_LIGHT_SOURCE_DAYLIGHT = 1,
+
+ /// @brief 2 = Fluorescent
+ ADDON_IMG_LIGHT_SOURCE_FLUORESCENT = 2,
+
+ /// @brief 3 = Tungsten (incandescent light)
+ ADDON_IMG_LIGHT_SOURCE_TUNGSTEN = 3,
+
+ /// @brief 4 = Flash
+ ADDON_IMG_LIGHT_SOURCE_FLASH = 4,
+
+ /// @brief 9 = Fine weather
+ ADDON_IMG_LIGHT_SOURCE_FINE_WEATHER = 9,
+
+ /// @brief 10 = Cloudy weather
+ ADDON_IMG_LIGHT_SOURCE_CLOUDY_WEATHER = 10,
+
+ /// @brief 11 = Shade
+ ADDON_IMG_LIGHT_SOURCE_SHADE = 11,
+
+ /// @brief 12 = Daylight fluorescent (D 5700 - 7100K)
+ ADDON_IMG_LIGHT_SOURCE_DAYLIGHT_FLUORESCENT = 12,
+
+ /// @brief 13 = Day white fluorescent (N 4600 - 5400K)
+ ADDON_IMG_LIGHT_SOURCE_DAY_WHITE_FLUORESCENT = 13,
+
+ /// @brief 14 = Cool white fluorescent (W 3900 - 4500K)
+ ADDON_IMG_LIGHT_SOURCE_COOL_WHITE_FLUORESCENT = 14,
+
+ /// @brief 15 = White fluorescent (WW 3200 - 3700K)
+ ADDON_IMG_LIGHT_SOURCE_WHITE_FLUORESCENT = 15,
+
+ /// @brief 17 = Standard light A
+ ADDON_IMG_LIGHT_SOURCE_STANDARD_LIGHT_A = 17,
+
+ /// @brief 18 = Standard light B
+ ADDON_IMG_LIGHT_SOURCE_STANDARD_LIGHT_B = 18,
+
+ /// @brief 19 = Standard light C
+ ADDON_IMG_LIGHT_SOURCE_STANDARD_LIGHT_C = 19,
+
+ /// @brief 20 = D55
+ ADDON_IMG_LIGHT_SOURCE_D55 = 20,
+
+ /// @brief 21 = D65
+ ADDON_IMG_LIGHT_SOURCE_D65 = 21,
+
+ /// @brief 22 = D75
+ ADDON_IMG_LIGHT_SOURCE_D75 = 22,
+
+ /// @brief 23 = D50
+ ADDON_IMG_LIGHT_SOURCE_D50 = 23,
+
+ /// @brief 24 = ISO Studio Tungsten
+ ADDON_IMG_LIGHT_SOURCE_ISO_STUDIO_TUNGSTEN = 24,
+
+ /// @brief 255 = Other
+ ADDON_IMG_LIGHT_SOURCE_OTHER = 255,
+ } ADDON_IMG_LIGHT_SOURCE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_imagedecoder_Defs_ADDON_IMG_FLASH_TYPE enum ADDON_IMG_FLASH_TYPE
+ /// @ingroup cpp_kodi_addon_imagedecoder_Defs
+ /// @brief **Flash Values**
+ ///
+ ///@{
+ typedef enum ADDON_IMG_FLASH_TYPE
+ {
+ /// @brief 0x0 = No Flash
+ ADDON_IMG_FLASH_TYPE_NO_FLASH = 0x0,
+
+ /// @brief 0x1 = Fired
+ ADDON_IMG_FLASH_TYPE_FIRED = 0x1,
+
+ /// @brief 0x5 = Fired, Return not detected
+ ADDON_IMG_FLASH_TYPE_FIRED_RETURN_NOT_DETECTED = 0x5,
+
+ /// @brief 0x7 = Fired, Return detected
+ ADDON_IMG_FLASH_TYPE_FIRED_RETURN_DETECTED = 0x7,
+
+ /// @brief 0x8 = On, Did not fire
+ ADDON_IMG_FLASH_TYPE_ = 0x8,
+
+ /// @brief 0x9 = On, Fired
+ ADDON_IMG_FLASH_TYPE_ON_Fired = 0x9,
+
+ /// @brief 0xd = On, Return not detected
+ ADDON_IMG_FLASH_TYPE_ON_RETURN_NOT_DETECTED = 0xd,
+
+ /// @brief 0xf = On, Return detected
+ ADDON_IMG_FLASH_TYPE_ON_RETURN_DETECTED = 0xf,
+
+ /// @brief 0x10 = Off, Did not fire
+ ADDON_IMG_FLASH_TYPE_OFF_DID_NOT_FIRE = 0x10,
+
+ /// @brief 0x14 = Off, Did not fire, Return not detected
+ ADDON_IMG_FLASH_TYPE_OFF_DID_NOT_FIRE_RETURN_NOT_DETECTED = 0x14,
+
+ /// @brief 0x18 = Auto, Did not fire
+ ADDON_IMG_FLASH_TYPE_AUTO_DID_NOT_FIRE = 0x18,
+
+ /// @brief 0x19 = Auto, Fired
+ ADDON_IMG_FLASH_TYPE_AUTO_FIRED = 0x19,
+
+ /// @brief 0x1d = Auto, Fired, Return not detected
+ ADDON_IMG_FLASH_TYPE_AUTO_FIRED_RETURN_NOT_DETECTED = 0x1d,
+
+ /// @brief 0x1f = Auto, Fired, Return detected
+ ADDON_IMG_FLASH_TYPE_AUTO_FIRED_RETURN_DETECTED = 0x1f,
+
+ /// @brief 0x20 = No flash function
+ ADDON_IMG_FLASH_TYPE_NO_FLASH_FUNCTION = 0x20,
+
+ /// @brief 0x30 = Off, No flash function
+ ADDON_IMG_FLASH_TYPE_OFF_NO_FLASH_FUNCTION = 0x30,
+
+ /// @brief 0x41 = Fired, Red-eye reduction
+ ADDON_IMG_FLASH_TYPE_FIRED_REDEYE_REDUCTION = 0x41,
+
+ /// @brief 0x45 = Fired, Red-eye reduction, Return not detected
+ ADDON_IMG_FLASH_TYPE_FIRED_REDEYE_REDUCTION_RETURN_NOT_DETECTED = 0x45,
+
+ /// @brief 0x47 = Fired, Red-eye reduction, Return detected
+ ADDON_IMG_FLASH_TYPE_FIRED_REDEYE_REDUCTION_RETURN_DETECTED = 0x47,
+
+ /// @brief 0x49 = On, Red-eye reduction
+ ADDON_IMG_FLASH_TYPE_ON_REDEYE_REDUCTION = 0x49,
+
+ /// @brief 0x4d = On, Red-eye reduction, Return not detected
+ ADDON_IMG_FLASH_TYPE_ON_REDEYE_REDUCTION_RETURN_NOT_DETECTED = 0x4d,
+
+ /// @brief 0x4f = On, Red-eye reduction, Return detected
+ ADDON_IMG_FLASH_TYPE_ON_REDEYE_REDUCTION_RETURN_DETECTED = 0x4f,
+
+ /// @brief 0x50 = Off, Red-eye reduction
+ ADDON_IMG_FLASH_TYPE_OFF_REDEYE_REDUCTION = 0x50,
+
+ /// @brief 0x58 = Auto, Did not fire, Red-eye reduction
+ ADDON_IMG_FLASH_TYPE_AUTO_DID_NOT_FIRE_REDEYE_REDUCTION = 0x58,
+
+ /// @brief 0x59 = Auto, Fired, Red-eye reduction
+ ADDON_IMG_FLASH_TYPE_AUTO_FIRED_REDEYE_REDUCTION = 0x59,
+
+ /// @brief 0x5d = Auto, Fired, Red-eye reduction, Return not detected
+ ADDON_IMG_FLASH_TYPE_AUTO_FIRED_REDEYE_REDUCTION_RETURN_NOT_DETECTED = 0x5d,
+
+ /// @brief 0x5f = Auto, Fired, Red-eye reduction, Return detected
+ ADDON_IMG_FLASH_TYPE_AUTO_FIRED_REDEYE_REDUCTION_RETURN_DETECTED = 0x5f,
+ } ADDON_IMG_FLASH_TYPE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ struct KODI_ADDON_IMAGEDECODER_INFO_TAG
+ {
+ int width;
+ int height;
+ float distance;
+ enum ADDON_IMG_ORIENTATION orientation;
+ enum ADDON_IMG_COLOR color;
+ enum ADDON_IMG_METERING_MODE metering_mode;
+ float exposure_time;
+ float exposure_bias;
+ enum ADDON_IMG_EXPOSURE_PROGRAM exposure_program;
+ enum ADDON_IMG_EXPOSURE_MODE exposure_mode;
+ time_t time_created;
+ float aperture_f_number;
+ enum ADDON_IMG_FLASH_TYPE flash_used;
+ int focal_length;
+ int focal_length_in_35mm_format;
+ float digital_zoom_ratio;
+ float iso_speed;
+ enum ADDON_IMG_LIGHT_SOURCE light_source;
+
+ bool gps_info_present;
+ char latitude_ref;
+ float latitude[3]; /* Deg,min,sec */
+ char longitude_ref;
+ float longitude[3]; /* Deg,min,sec */
+ int altitude_ref;
+ float altitude;
+
+ char* camera_manufacturer;
+ char* camera_model;
+ char* author;
+ char* description;
+ char* copyright;
+ };
+
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_IMAGEDECODER_SUPPORTS_FILE_V1)(
+ const KODI_ADDON_IMAGEDECODER_HDL hdl, const char* file);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_IMAGEDECODER_READ_TAG_V1)(
+ const KODI_ADDON_IMAGEDECODER_HDL hdl,
+ const char* file,
+ struct KODI_ADDON_IMAGEDECODER_INFO_TAG* info);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_IMAGEDECODER_LOAD_IMAGE_FROM_MEMORY_V1)(
+ const KODI_ADDON_IMAGEDECODER_HDL hdl,
+ const char* mimetype,
+ const uint8_t* buffer,
+ size_t buf_size,
+ unsigned int* width,
+ unsigned int* height);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_IMAGEDECODER_DECODE_V1)(
+ const KODI_ADDON_IMAGEDECODER_HDL hdl,
+ uint8_t* pixels,
+ size_t pixels_size,
+ unsigned int width,
+ unsigned int height,
+ unsigned int pitch,
+ enum ADDON_IMG_FMT format);
+
+ typedef struct KodiToAddonFuncTable_ImageDecoder
+ {
+ PFN_KODI_ADDON_IMAGEDECODER_SUPPORTS_FILE_V1 supports_file;
+ PFN_KODI_ADDON_IMAGEDECODER_READ_TAG_V1 read_tag;
+ PFN_KODI_ADDON_IMAGEDECODER_LOAD_IMAGE_FROM_MEMORY_V1 load_image_from_memory;
+ PFN_KODI_ADDON_IMAGEDECODER_DECODE_V1 decode;
+ } KodiToAddonFuncTable_ImageDecoder;
+
+ typedef struct AddonToKodiFuncTable_ImageDecoder
+ {
+ KODI_HANDLE kodi_instance;
+ } AddonToKodiFuncTable_ImageDecoder;
+
+ typedef struct AddonInstance_ImageDecoder
+ {
+ struct AddonToKodiFuncTable_ImageDecoder* toKodi;
+ struct KodiToAddonFuncTable_ImageDecoder* toAddon;
+ } AddonInstance_ImageDecoder;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_IMAGEDECODER_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream.h
new file mode 100644
index 0000000..7e33015
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream.h
@@ -0,0 +1,708 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_INPUTSTREAM_H
+#define C_API_ADDONINSTANCE_INPUTSTREAM_H
+
+#include "../addon_base.h"
+#include "inputstream/demux_packet.h"
+#include "inputstream/stream_codec.h"
+#include "inputstream/stream_constants.h"
+#include "inputstream/stream_crypto.h"
+#include "inputstream/timing_constants.h"
+
+#include <time.h>
+
+// Increment this level always if you add features which can lead to compile failures in the addon
+#define INPUTSTREAM_VERSION_LEVEL 4
+
+#define INPUTSTREAM_MAX_INFO_COUNT 8
+#define INPUTSTREAM_MAX_STREAM_COUNT 256
+#define INPUTSTREAM_MAX_STRING_NAME_SIZE 256
+#define INPUTSTREAM_MAX_STRING_CODEC_SIZE 32
+#define INPUTSTREAM_MAX_STRING_LANGUAGE_SIZE 64
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //==============================================================================
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface_InputstreamCapabilities
+ /// @brief **Capability types of inputstream addon.**\n
+ /// This values are needed to tell Kodi which options are supported on the addon.
+ ///
+ /// If one of this is defined, then the corresponding methods from
+ /// @ref cpp_kodi_addon_inputstream "kodi::addon::CInstanceInputStream" need
+ /// to be implemented.
+ ///
+ /// Used on @ref kodi::addon::CInstanceInputStream::GetCapabilities().
+ ///
+ ///@{
+ enum INPUTSTREAM_MASKTYPE
+ {
+ /// @brief **0000 0000 0000 0001 :** Supports interface demuxing.
+ ///
+ /// If set must be @ref cpp_kodi_addon_inputstream_Demux "Demux support" included.
+ INPUTSTREAM_SUPPORTS_IDEMUX = (1 << 0),
+
+ /// @brief **0000 0000 0000 0010 :** Supports interface position time.
+ ///
+ /// This means that the start time and the current stream time are used.
+ ///
+ /// If set must be @ref cpp_kodi_addon_inputstream_Time "Time support" included.
+ INPUTSTREAM_SUPPORTS_IPOSTIME = (1 << 1),
+
+ /// @brief **0000 0000 0000 0100 :** Supports interface for display time.
+ ///
+ /// This will call up the complete stream time information. The start time
+ /// and the individual PTS times are then given using @ref cpp_kodi_addon_inputstream_Defs_Times.
+ ///
+ /// If set must be @ref cpp_kodi_addon_inputstream_Times "Times support" included.
+ INPUTSTREAM_SUPPORTS_IDISPLAYTIME = (1 << 2),
+
+ /// @brief **0000 0000 0000 1000 :** Supports seek
+ INPUTSTREAM_SUPPORTS_SEEK = (1 << 3),
+
+ /// @brief **0000 0000 0001 0000 :** Supports pause
+ INPUTSTREAM_SUPPORTS_PAUSE = (1 << 4),
+
+ /// @brief **0000 0000 0010 0000 :** Supports interface to give position time.
+ ///
+ /// This will only ask for the current time of the stream, not for length or
+ /// start.
+ ///
+ /// If set must be @ref cpp_kodi_addon_inputstream_PosTime "Position time support" included.
+ INPUTSTREAM_SUPPORTS_ITIME = (1 << 5),
+
+ /// @brief **0000 0000 0100 0000 :** Supports interface for chapter selection.
+ ///
+ /// If set must be @ref cpp_kodi_addon_inputstream_Chapter "Chapter support" included.
+ INPUTSTREAM_SUPPORTS_ICHAPTER = (1 << 6),
+ };
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief InputStream add-on capabilities. All capabilities are set to "false" as default.
+ */
+ struct INPUTSTREAM_CAPABILITIES
+ {
+ /// set of supported capabilities
+ uint32_t m_mask;
+ };
+
+ /*!
+ * @brief structure of key/value pairs passed to addon on Open()
+ */
+ struct INPUTSTREAM_PROPERTY
+ {
+ const char* m_strURL;
+ const char* m_mimeType;
+
+ unsigned int m_nCountInfoValues;
+ struct LISTITEMPROPERTY
+ {
+ const char* m_strKey;
+ const char* m_strValue;
+ } m_ListItemProperties[STREAM_MAX_PROPERTY_COUNT];
+
+ const char* m_libFolder;
+ const char* m_profileFolder;
+ };
+
+ /*!
+ * @brief Array of stream IDs
+ */
+ struct INPUTSTREAM_IDS
+ {
+ unsigned int m_streamCount;
+ unsigned int m_streamIds[INPUTSTREAM_MAX_STREAM_COUNT];
+ };
+
+ /*!
+ * @brief MASTERING Metadata
+ */
+ struct INPUTSTREAM_MASTERING_METADATA
+ {
+ double primary_r_chromaticity_x;
+ double primary_r_chromaticity_y;
+ double primary_g_chromaticity_x;
+ double primary_g_chromaticity_y;
+ double primary_b_chromaticity_x;
+ double primary_b_chromaticity_y;
+ double white_point_chromaticity_x;
+ double white_point_chromaticity_y;
+ double luminance_max;
+ double luminance_min;
+ };
+
+ /*!
+ * @brief CONTENTLIGHT Metadata
+ */
+ struct INPUTSTREAM_CONTENTLIGHT_METADATA
+ {
+ uint64_t max_cll;
+ uint64_t max_fall;
+ };
+
+ //==============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_INPUTSTREAM_TYPE enum INPUTSTREAM_TYPE
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+ /// @brief **Inputstream types**\n
+ /// To identify type on stream.
+ ///
+ /// Used on @ref kodi::addon::InputstreamInfo::SetStreamType and @ref kodi::addon::InputstreamInfo::GetStreamType.
+ ///
+ ///@{
+ enum INPUTSTREAM_TYPE
+ {
+ /// @brief **0 :** To set nothing defined
+ INPUTSTREAM_TYPE_NONE = 0,
+
+ /// @brief **1 :** To identify @ref cpp_kodi_addon_inputstream_Defs_Info as Video
+ INPUTSTREAM_TYPE_VIDEO,
+
+ /// @brief **2 :** To identify @ref cpp_kodi_addon_inputstream_Defs_Info as Audio
+ INPUTSTREAM_TYPE_AUDIO,
+
+ /// @brief **3 :** To identify @ref cpp_kodi_addon_inputstream_Defs_Info as Subtitle
+ INPUTSTREAM_TYPE_SUBTITLE,
+
+ /// @brief **4 :** To identify @ref cpp_kodi_addon_inputstream_Defs_Info as Teletext
+ INPUTSTREAM_TYPE_TELETEXT,
+
+ /// @brief **5 :** To identify @ref cpp_kodi_addon_inputstream_Defs_Info as Radio RDS
+ INPUTSTREAM_TYPE_RDS,
+
+ /// @brief **6 :** To identify @ref cpp_kodi_addon_inputstream_Defs_Info as Audio ID3 tags
+ INPUTSTREAM_TYPE_ID3,
+ };
+ ///@}
+ //------------------------------------------------------------------------------
+
+ //==============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_INPUTSTREAM_CODEC_FEATURES enum INPUTSTREAM_CODEC_FEATURES
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+ /// @brief **Inputstream codec features**\n
+ /// To identify special extra features used for optional codec on inputstream.
+ ///
+ /// Used on @ref kodi::addon::InputstreamInfo::SetFeatures and @ref kodi::addon::InputstreamInfo::GetFeatures.
+ ///
+ /// @note These variables are bit flags which are created using "|" can be used together.
+ ///
+ ///@{
+ enum INPUTSTREAM_CODEC_FEATURES
+ {
+ /// @brief **0000 0000 0000 0000 :** Empty to set if nothing is used
+ INPUTSTREAM_FEATURE_NONE = 0,
+
+ /// @brief **0000 0000 0000 0001 :** To set addon decode should used with @ref cpp_kodi_addon_videocodec.
+ INPUTSTREAM_FEATURE_DECODE = (1 << 0)
+ };
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_INPUTSTREAM_FLAGS enum INPUTSTREAM_FLAGS
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+ /// @brief **Inputstream flags**\n
+ /// To identify extra stream flags used on inputstream.
+ ///
+ /// Used on @ref kodi::addon::InputstreamInfo::SetFlags and @ref kodi::addon::InputstreamInfo::GetFlags.
+ ///
+ /// @note These variables are bit flags which are created using "|" can be used together.
+ ///
+ ///@{
+ enum INPUTSTREAM_FLAGS
+ {
+ /// @brief **0000 0000 0000 0000 :** Empty to set if nothing is used
+ INPUTSTREAM_FLAG_NONE = 0,
+
+ /// @brief **0000 0000 0000 0001 :** Default
+ INPUTSTREAM_FLAG_DEFAULT = (1 << 0),
+
+ /// @brief **0000 0000 0000 0010 :** Dub
+ INPUTSTREAM_FLAG_DUB = (1 << 1),
+
+ /// @brief **0000 0000 0000 0100 :** Original
+ INPUTSTREAM_FLAG_ORIGINAL = (1 << 2),
+
+ /// @brief **0000 0000 0000 1000 :** Comment
+ INPUTSTREAM_FLAG_COMMENT = (1 << 3),
+
+ /// @brief **0000 0000 0001 0000 :** Lyrics
+ INPUTSTREAM_FLAG_LYRICS = (1 << 4),
+
+ /// @brief **0000 0000 0010 0000 :** Karaoke
+ INPUTSTREAM_FLAG_KARAOKE = (1 << 5),
+
+ /// @brief **0000 0000 0100 0000 :** Forced
+ INPUTSTREAM_FLAG_FORCED = (1 << 6),
+
+ /// @brief **0000 0000 1000 0000 :** Hearing impaired
+ INPUTSTREAM_FLAG_HEARING_IMPAIRED = (1 << 7),
+
+ /// @brief **0000 0001 0000 0000 :** Visual impaired
+ INPUTSTREAM_FLAG_VISUAL_IMPAIRED = (1 << 8),
+ };
+ ///@}
+ //----------------------------------------------------------------------------
+
+ // Keep in sync with AVColorSpace
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_INPUTSTREAM_COLORSPACE enum INPUTSTREAM_COLORSPACE
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+ /// @brief **Inputstream color space flags**\n
+ /// YUV colorspace type. These values match the ones defined by ISO/IEC 23001-8_2013 § 7.3.
+ ///
+ /// Used on @ref kodi::addon::InputstreamInfo::SetColorSpace and @ref kodi::addon::InputstreamInfo::GetColorSpace.
+ ///
+ ///@{
+ enum INPUTSTREAM_COLORSPACE
+ {
+ /// @brief **0 :** Order of coefficients is actually GBR, also IEC 61966-2-1 (sRGB)
+ INPUTSTREAM_COLORSPACE_RGB = 0,
+
+ /// @brief **1 :** Also ITU-R BT1361 / IEC 61966-2-4 xvYCC709 / SMPTE RP177 Annex B
+ INPUTSTREAM_COLORSPACE_BT709 = 1,
+
+ /// @brief **2 :** To set stream is unspecified
+ INPUTSTREAM_COLORSPACE_UNSPECIFIED = 2,
+
+ /// @brief **2 :** To set stream is unknown
+ /// @note Same as @ref INPUTSTREAM_COLORSPACE_UNSPECIFIED
+ INPUTSTREAM_COLORSPACE_UNKNOWN = INPUTSTREAM_COLORSPACE_UNSPECIFIED, // compatibility
+
+ /// @brief **3 :** To set colorspace reserved
+ INPUTSTREAM_COLORSPACE_RESERVED = 3,
+
+ /// @brief **4 :** FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
+ INPUTSTREAM_COLORSPACE_FCC = 4,
+
+ /// @brief **5 :** Also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM / IEC 61966-2-4 xvYCC601
+ INPUTSTREAM_COLORSPACE_BT470BG = 5,
+
+ /// @brief **6 :** Also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
+ INPUTSTREAM_COLORSPACE_SMPTE170M = 6,
+
+ /// @brief **7 :** Functionally identical to above @ref INPUTSTREAM_COLORSPACE_SMPTE170M
+ INPUTSTREAM_COLORSPACE_SMPTE240M = 7,
+
+ /// @brief **8 :** Used by Dirac / VC-2 and H.264 FRext, see ITU-T SG16
+ INPUTSTREAM_COLORSPACE_YCGCO = 8,
+
+ /// @brief **8 :** To set colorspace as YCOCG
+ /// @note Same as @ref INPUTSTREAM_COLORSPACE_YCGCO
+ INPUTSTREAM_COLORSPACE_YCOCG = INPUTSTREAM_COLORSPACE_YCGCO,
+
+ /// @brief **9 :** ITU-R BT2020 non-constant luminance system
+ INPUTSTREAM_COLORSPACE_BT2020_NCL = 9,
+
+ /// @brief **10 :** ITU-R BT2020 constant luminance system
+ INPUTSTREAM_COLORSPACE_BT2020_CL = 10,
+
+ /// @brief **11 :** SMPTE 2085, Y'D'zD'x
+ INPUTSTREAM_COLORSPACE_SMPTE2085 = 11,
+
+ /// @brief **12 :** Chromaticity-derived non-constant luminance system
+ INPUTSTREAM_COLORSPACE_CHROMA_DERIVED_NCL = 12,
+
+ /// @brief **13 :** Chromaticity-derived constant luminance system
+ INPUTSTREAM_COLORSPACE_CHROMA_DERIVED_CL = 13,
+
+ /// @brief **14 :** ITU-R BT.2100-0, ICtCp
+ INPUTSTREAM_COLORSPACE_ICTCP = 14,
+
+ /// @brief The maximum value to use in a list.
+ INPUTSTREAM_COLORSPACE_MAX
+ };
+ ///@}
+ //------------------------------------------------------------------------------
+
+ // Keep in sync with AVColorPrimaries
+ //==============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_INPUTSTREAM_COLORPRIMARIES enum INPUTSTREAM_COLORPRIMARIES
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+ /// @brief **Inputstream color primaries flags**\n
+ /// Chromaticity coordinates of the source primaries. These values match the ones defined by ISO/IEC 23001-8_2013 § 7.1.
+ ///
+ /// Used on @ref kodi::addon::InputstreamInfo::SetColorPrimaries and @ref kodi::addon::InputstreamInfo::GetColorPrimaries.
+ ///
+ ///@{
+ enum INPUTSTREAM_COLORPRIMARIES
+ {
+ /// @brief **0 :** Reserved
+ INPUTSTREAM_COLORPRIMARY_RESERVED0 = 0,
+
+ /// @brief **1 :** Also ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP177 Annex B
+ INPUTSTREAM_COLORPRIMARY_BT709 = 1,
+
+ /// @brief **2 :** Unspecified
+ INPUTSTREAM_COLORPRIMARY_UNSPECIFIED = 2,
+
+ /// @brief **3 :** Reserved
+ INPUTSTREAM_COLORPRIMARY_RESERVED = 3,
+
+ /// @brief **4 :** Also FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
+ INPUTSTREAM_COLORPRIMARY_BT470M = 4,
+
+ /// @brief **5 :** Also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM
+ INPUTSTREAM_COLORPRIMARY_BT470BG = 5,
+
+ /// @brief **6 :** Also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
+ INPUTSTREAM_COLORPRIMARY_SMPTE170M = 6,
+
+ /// @brief **7 :** Functionally identical to above
+ INPUTSTREAM_COLORPRIMARY_SMPTE240M = 7,
+
+ /// @brief **8 :** Colour filters using Illuminant C
+ INPUTSTREAM_COLORPRIMARY_FILM = 8,
+
+ /// @brief **9 :** ITU-R BT2020
+ INPUTSTREAM_COLORPRIMARY_BT2020 = 9,
+
+ /// @brief **10 :** SMPTE ST 428-1 (CIE 1931 XYZ)
+ INPUTSTREAM_COLORPRIMARY_SMPTE428 = 10,
+
+ /// @brief **10 :**
+ /// @note Same as @ref INPUTSTREAM_COLORPRIMARY_SMPTE428
+ INPUTSTREAM_COLORPRIMARY_SMPTEST428_1 = INPUTSTREAM_COLORPRIMARY_SMPTE428,
+
+ /// @brief **11 :** SMPTE ST 431-2 (2011) / DCI P3
+ INPUTSTREAM_COLORPRIMARY_SMPTE431 = 11,
+
+ /// @brief **12 :** SMPTE ST 432-1 (2010) / P3 D65 / Display P3
+ INPUTSTREAM_COLORPRIMARY_SMPTE432 = 12,
+
+ /// @brief **22 :** JEDEC P22 phosphors
+ INPUTSTREAM_COLORPRIMARY_JEDEC_P22 = 22,
+
+ /// @brief The maximum value to use in a list.
+ INPUTSTREAM_COLORPRIMARY_MAX
+ };
+ ///@}
+ //------------------------------------------------------------------------------
+
+ // Keep in sync with AVColorRange
+ //==============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_INPUTSTREAM_COLORRANGE enum INPUTSTREAM_COLORRANGE
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+ /// @brief **Inputstream color range flags**\n
+ /// MPEG vs JPEG YUV range.
+ ///
+ /// Used on @ref kodi::addon::InputstreamInfo::SetColorRange and @ref kodi::addon::InputstreamInfo::GetColorRange.
+ ///
+ ///@{
+ enum INPUTSTREAM_COLORRANGE
+ {
+ /// @brief **0 :** To define as unknown
+ INPUTSTREAM_COLORRANGE_UNKNOWN = 0,
+
+ /// @brief **1 :** The normal 219*2^(n-8) "MPEG" YUV ranges
+ INPUTSTREAM_COLORRANGE_LIMITED,
+
+ /// @brief **2 :** The normal 2^n-1 "JPEG" YUV ranges
+ INPUTSTREAM_COLORRANGE_FULLRANGE,
+
+ /// @brief The maximum value to use in a list.
+ INPUTSTREAM_COLORRANGE_MAX
+ };
+ ///@}
+ //------------------------------------------------------------------------------
+
+ // keep in sync with AVColorTransferCharacteristic
+ //==============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_INPUTSTREAM_COLORTRC enum INPUTSTREAM_COLORTRC
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+ /// @brief **Inputstream color TRC flags**\n
+ /// Color Transfer Characteristic. These values match the ones defined by ISO/IEC 23001-8_2013 § 7.2.
+ ///
+ /// Used on @ref kodi::addon::InputstreamInfo::SetColorTransferCharacteristic and @ref kodi::addon::InputstreamInfo::GetColorTransferCharacteristic.
+ ///
+ ///@{
+ enum INPUTSTREAM_COLORTRC
+ {
+ /// @brief **0 :** Reserved
+ INPUTSTREAM_COLORTRC_RESERVED0 = 0,
+
+ /// @brief **1 :** Also ITU-R BT1361
+ INPUTSTREAM_COLORTRC_BT709 = 1,
+
+ /// @brief **2 :** Unspecified
+ INPUTSTREAM_COLORTRC_UNSPECIFIED = 2,
+
+ /// @brief **3 :** Reserved
+ INPUTSTREAM_COLORTRC_RESERVED = 3,
+
+ /// @brief **4 :** Also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM
+ INPUTSTREAM_COLORTRC_GAMMA22 = 4,
+
+ /// @brief **5 :** Also ITU-R BT470BG
+ INPUTSTREAM_COLORTRC_GAMMA28 = 5,
+
+ /// @brief **6 :** Also ITU-R BT601-6 525 or 625 / ITU-R BT1358 525 or 625 / ITU-R BT1700 NTSC
+ INPUTSTREAM_COLORTRC_SMPTE170M = 6,
+
+ /// @brief **7 :** Functionally identical to above @ref INPUTSTREAM_COLORTRC_SMPTE170M
+ INPUTSTREAM_COLORTRC_SMPTE240M = 7,
+
+ /// @brief **8 :** Linear transfer characteristics
+ INPUTSTREAM_COLORTRC_LINEAR = 8,
+
+ /// @brief **9 :** Logarithmic transfer characteristic (100:1 range)
+ INPUTSTREAM_COLORTRC_LOG = 9,
+
+ /// @brief **10 :** Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range)
+ INPUTSTREAM_COLORTRC_LOG_SQRT = 10,
+
+ /// @brief **11 :** IEC 61966-2-4
+ INPUTSTREAM_COLORTRC_IEC61966_2_4 = 11,
+
+ /// @brief **12 :** ITU-R BT1361 Extended Colour Gamut
+ INPUTSTREAM_COLORTRC_BT1361_ECG = 12,
+
+ /// @brief **13 :** IEC 61966-2-1 (sRGB or sYCC)
+ INPUTSTREAM_COLORTRC_IEC61966_2_1 = 13,
+
+ /// @brief **14 :** ITU-R BT2020 for 10-bit system
+ INPUTSTREAM_COLORTRC_BT2020_10 = 14,
+
+ /// @brief **15 :** ITU-R BT2020 for 12-bit system
+ INPUTSTREAM_COLORTRC_BT2020_12 = 15,
+
+ /// @brief **16 :** SMPTE ST 2084 for 10-, 12-, 14- and 16-bit systems
+ INPUTSTREAM_COLORTRC_SMPTE2084 = 16,
+
+ /// @brief **16 :** Same as @ref INPUTSTREAM_COLORTRC_SMPTE2084
+ INPUTSTREAM_COLORTRC_SMPTEST2084 = INPUTSTREAM_COLORTRC_SMPTE2084,
+
+ /// @brief **17 :** SMPTE ST 428-1
+ INPUTSTREAM_COLORTRC_SMPTE428 = 17,
+
+ /// @brief **17 :** Same as @ref INPUTSTREAM_COLORTRC_SMPTE428
+ INPUTSTREAM_COLORTRC_SMPTEST428_1 = INPUTSTREAM_COLORTRC_SMPTE428,
+
+ /// @brief **18 :** ARIB STD-B67, known as "Hybrid log-gamma"
+ INPUTSTREAM_COLORTRC_ARIB_STD_B67 = 18,
+
+ /// @brief The maximum value to use in a list.
+ INPUTSTREAM_COLORTRC_MAX
+ };
+ ///@}
+ //------------------------------------------------------------------------------
+
+ /*!
+ * @brief stream properties
+ */
+ struct INPUTSTREAM_INFO
+ {
+ enum INPUTSTREAM_TYPE m_streamType;
+ uint32_t m_features;
+ uint32_t m_flags;
+
+ //! @brief (optional) name of the stream, \0 for default handling
+ char m_name[INPUTSTREAM_MAX_STRING_NAME_SIZE];
+
+ //! @brief (required) name of codec according to ffmpeg
+ char m_codecName[INPUTSTREAM_MAX_STRING_CODEC_SIZE];
+
+ //! @brief (optional) internal name of codec (selectionstream info)
+ char m_codecInternalName[INPUTSTREAM_MAX_STRING_CODEC_SIZE];
+
+ //! @brief (optional) the profile of the codec
+ enum STREAMCODEC_PROFILE m_codecProfile;
+
+ //! @brief (required) physical index
+ unsigned int m_pID;
+
+ const uint8_t* m_ExtraData;
+ unsigned int m_ExtraSize;
+
+ //! @brief RFC 5646 language code (empty string if undefined)
+ char m_language[INPUTSTREAM_MAX_STRING_LANGUAGE_SIZE];
+
+ //! Video stream related data
+ //@{
+
+ //! @brief Scale of 1000 and a rate of 29970 will result in 29.97 fps
+ unsigned int m_FpsScale;
+
+ unsigned int m_FpsRate;
+
+ //! @brief height of the stream reported by the demuxer
+ unsigned int m_Height;
+
+ //! @brief width of the stream reported by the demuxer
+ unsigned int m_Width;
+
+ //! @brief display aspect of stream
+ float m_Aspect;
+
+ //@}
+
+ //! Audio stream related data
+ //@{
+
+ //! @brief (required) amount of channels
+ unsigned int m_Channels;
+
+ //! @brief (required) sample rate
+ unsigned int m_SampleRate;
+
+ //! @brief (required) bit rate
+ unsigned int m_BitRate;
+
+ //! @brief (required) bits per sample
+ unsigned int m_BitsPerSample;
+
+ unsigned int m_BlockAlign;
+
+ //@}
+
+ struct STREAM_CRYPTO_SESSION m_cryptoSession;
+
+ // new in API version 2.0.8
+ //@{
+ //! @brief Codec If available, the fourcc code codec
+ unsigned int m_codecFourCC;
+
+ //! @brief definition of colorspace
+ enum INPUTSTREAM_COLORSPACE m_colorSpace;
+
+ //! @brief color range if available
+ enum INPUTSTREAM_COLORRANGE m_colorRange;
+ //@}
+
+ //new in API 2.0.9 / INPUTSTREAM_VERSION_LEVEL 1
+ //@{
+ enum INPUTSTREAM_COLORPRIMARIES m_colorPrimaries;
+ enum INPUTSTREAM_COLORTRC m_colorTransferCharacteristic;
+ //@}
+
+ //! @brief mastering static Metadata
+ struct INPUTSTREAM_MASTERING_METADATA* m_masteringMetadata;
+
+ //! @brief content light static Metadata
+ struct INPUTSTREAM_CONTENTLIGHT_METADATA* m_contentLightMetadata;
+ };
+
+ struct INPUTSTREAM_TIMES
+ {
+ time_t startTime;
+ double ptsStart;
+ double ptsBegin;
+ double ptsEnd;
+ };
+
+ /*!
+ * @brief "C" ABI Structures to transfer the methods from this to Kodi
+ */
+
+ // this are properties given to the addon on create
+ // at this time we have no parameters for the addon
+ typedef struct AddonProps_InputStream /* internal */
+ {
+ int dummy;
+ } AddonProps_InputStream;
+
+ typedef struct AddonToKodiFuncTable_InputStream /* internal */
+ {
+ KODI_HANDLE kodiInstance;
+ struct DEMUX_PACKET* (*allocate_demux_packet)(void* kodiInstance, int data_size);
+ struct DEMUX_PACKET* (*allocate_encrypted_demux_packet)(void* kodiInstance,
+ unsigned int data_size,
+ unsigned int encrypted_subsample_count);
+ void (*free_demux_packet)(void* kodiInstance, struct DEMUX_PACKET* packet);
+ } AddonToKodiFuncTable_InputStream;
+
+ struct AddonInstance_InputStream;
+ typedef struct KodiToAddonFuncTable_InputStream /* internal */
+ {
+ KODI_HANDLE addonInstance;
+
+ bool(__cdecl* open)(const struct AddonInstance_InputStream* instance,
+ struct INPUTSTREAM_PROPERTY* props);
+ void(__cdecl* close)(const struct AddonInstance_InputStream* instance);
+ const char*(__cdecl* get_path_list)(const struct AddonInstance_InputStream* instance);
+ void(__cdecl* get_capabilities)(const struct AddonInstance_InputStream* instance,
+ struct INPUTSTREAM_CAPABILITIES* capabilities);
+
+ // IDemux
+ bool(__cdecl* get_stream_ids)(const struct AddonInstance_InputStream* instance,
+ struct INPUTSTREAM_IDS* ids);
+ bool(__cdecl* get_stream)(const struct AddonInstance_InputStream* instance,
+ int streamid,
+ struct INPUTSTREAM_INFO* info,
+ KODI_HANDLE* demuxStream,
+ KODI_HANDLE (*transfer_stream)(KODI_HANDLE handle,
+ int streamId,
+ struct INPUTSTREAM_INFO* stream));
+ void(__cdecl* enable_stream)(const struct AddonInstance_InputStream* instance,
+ int streamid,
+ bool enable);
+ bool(__cdecl* open_stream)(const struct AddonInstance_InputStream* instance, int streamid);
+ void(__cdecl* demux_reset)(const struct AddonInstance_InputStream* instance);
+ void(__cdecl* demux_abort)(const struct AddonInstance_InputStream* instance);
+ void(__cdecl* demux_flush)(const struct AddonInstance_InputStream* instance);
+ struct DEMUX_PACKET*(__cdecl* demux_read)(const struct AddonInstance_InputStream* instance);
+ bool(__cdecl* demux_seek_time)(const struct AddonInstance_InputStream* instance,
+ double time,
+ bool backwards,
+ double* startpts);
+ void(__cdecl* demux_set_speed)(const struct AddonInstance_InputStream* instance, int speed);
+ void(__cdecl* set_video_resolution)(const struct AddonInstance_InputStream* instance,
+ unsigned int width,
+ unsigned int height,
+ unsigned int maxWidth,
+ unsigned int maxHeight);
+
+ // IDisplayTime
+ int(__cdecl* get_total_time)(const struct AddonInstance_InputStream* instance);
+ int(__cdecl* get_time)(const struct AddonInstance_InputStream* instance);
+
+ // ITime
+ bool(__cdecl* get_times)(const struct AddonInstance_InputStream* instance,
+ struct INPUTSTREAM_TIMES* times);
+
+ // IPosTime
+ bool(__cdecl* pos_time)(const struct AddonInstance_InputStream* instance, int ms);
+
+ int(__cdecl* read_stream)(const struct AddonInstance_InputStream* instance,
+ uint8_t* buffer,
+ unsigned int bufferSize);
+ int64_t(__cdecl* seek_stream)(const struct AddonInstance_InputStream* instance,
+ int64_t position,
+ int whence);
+ int64_t(__cdecl* position_stream)(const struct AddonInstance_InputStream* instance);
+ int64_t(__cdecl* length_stream)(const struct AddonInstance_InputStream* instance);
+ bool(__cdecl* is_real_time_stream)(const struct AddonInstance_InputStream* instance);
+
+ // IChapter
+ int(__cdecl* get_chapter)(const struct AddonInstance_InputStream* instance);
+ int(__cdecl* get_chapter_count)(const struct AddonInstance_InputStream* instance);
+ const char*(__cdecl* get_chapter_name)(const struct AddonInstance_InputStream* instance,
+ int ch);
+ int64_t(__cdecl* get_chapter_pos)(const struct AddonInstance_InputStream* instance, int ch);
+ bool(__cdecl* seek_chapter)(const struct AddonInstance_InputStream* instance, int ch);
+
+ int(__cdecl* block_size_stream)(const struct AddonInstance_InputStream* instance);
+ } KodiToAddonFuncTable_InputStream;
+
+ typedef struct AddonInstance_InputStream /* internal */
+ {
+ struct AddonProps_InputStream* props;
+ struct AddonToKodiFuncTable_InputStream* toKodi;
+ struct KodiToAddonFuncTable_InputStream* toAddon;
+ } AddonInstance_InputStream;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_INPUTSTREAM_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/CMakeLists.txt
new file mode 100644
index 0000000..c7d9451
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/CMakeLists.txt
@@ -0,0 +1,14 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ demux_packet.h
+ stream_codec.h
+ stream_constants.h
+ stream_crypto.h
+ timing_constants.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_c-api_addon-instance_inputstream)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/demux_packet.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/demux_packet.h
new file mode 100644
index 0000000..79686ab
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/demux_packet.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_INPUTSTREAM_DEMUXPACKET_H
+#define C_API_ADDONINSTANCE_INPUTSTREAM_DEMUXPACKET_H
+
+#include "timing_constants.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#define DEMUX_SPECIALID_STREAMINFO -10
+#define DEMUX_SPECIALID_STREAMCHANGE -11
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ struct DEMUX_CRYPTO_INFO;
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_Interface_DEMUX_PACKET struct DEMUX_PACKET
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_Interface
+ /// @brief **Demux packet**\n
+ /// To processed codec and demux inputstream stream.
+ ///
+ /// This part is in the "C" style in order to have better performance and
+ /// possibly to be used in "C" libraries.
+ ///
+ /// The structure should be created with @ref kodi::addon::CInstanceInputStream::AllocateDemuxPacket()
+ /// or @ref kodi::addon::CInstanceInputStream::AllocateEncryptedDemuxPacket()
+ /// and if not added to Kodi with @ref kodi::addon::CInstanceInputStream::FreeDemuxPacket()
+ /// be deleted again.
+ ///
+ /// Packages that have been given to Kodi and processed will then be deleted
+ /// by him.
+ ///
+ ///@{
+ struct DEMUX_PACKET
+ {
+ /// @brief Stream package which is given for decoding.
+ ///
+ /// @note Associated storage from here is created using
+ /// @ref kodi::addon::CInstanceInputStream::AllocateDemuxPacket()
+ /// or @ref kodi::addon::CInstanceInputStream::AllocateEncryptedDemuxPacket().
+ uint8_t* pData;
+
+ /// @brief Size of the package given at @ref pData.
+ int iSize;
+
+ /// @brief Identification of the stream.
+ int iStreamId;
+
+ /// @brief Identification of the associated demuxer, this can be identical
+ /// on several streams.
+ int64_t demuxerId;
+
+ /// @brief The group this data belongs to, used to group data from different
+ /// streams together.
+ int iGroupId;
+
+ //------------------------------------------
+
+ /// @brief Additional packet data that can be provided by the container.
+ ///
+ /// Packet can contain several types of side information.
+ ///
+ /// This is usually based on that of ffmpeg, see
+ /// [AVPacketSideData](https://ffmpeg.org/doxygen/trunk/structAVPacketSideData.html).
+ void* pSideData;
+
+ /// @brief Data elements stored at @ref pSideData.
+ int iSideDataElems;
+
+ //------------------------------------------
+
+ /// @brief Presentation time stamp (PTS).
+ double pts;
+
+ /// @brief Decoding time stamp (DTS).
+ double dts;
+
+ /// @brief Duration in @ref STREAM_TIME_BASE if available
+ double duration;
+
+ /// @brief Display time from input stream
+ int dispTime;
+
+ /// @brief To show that this package allows recreating the presentation by
+ /// mistake.
+ bool recoveryPoint;
+
+ //------------------------------------------
+
+ /// @brief Optional data to allow decryption at processing site if
+ /// necessary.
+ ///
+ /// This can be created using @ref kodi::addon::CInstanceInputStream::AllocateEncryptedDemuxPacket(),
+ /// otherwise this is declared as <b>`nullptr`</b>.
+ ///
+ /// See @ref DEMUX_CRYPTO_INFO for their style.
+ struct DEMUX_CRYPTO_INFO* cryptoInfo;
+ };
+ ///@}
+ //----------------------------------------------------------------------------
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_INPUTSTREAM_DEMUXPACKET_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_codec.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_codec.h
new file mode 100644
index 0000000..91789e7
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_codec.h
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017-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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_INPUTSTREAM_STREAMCODEC_H
+#define C_API_ADDONINSTANCE_INPUTSTREAM_STREAMCODEC_H
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //==============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_StreamEncryption_STREAMCODEC_PROFILE enum STREAMCODEC_PROFILE
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_StreamCodec
+ /// @brief **The standard defines several sets of capabilities.**\n
+ /// Which are referred to as profiles, targeting specific classes of applications.
+ ///
+ ///@{
+ enum STREAMCODEC_PROFILE
+ {
+ /// @brief Unknown codec profile
+ CodecProfileUnknown = 0,
+
+ /// @brief If a codec profile is not required
+ CodecProfileNotNeeded,
+
+ /// @brief **H264** Baseline Profile (BP, 66)\n
+ /// \n
+ /// Primarily for low-cost applications that require additional data loss
+ /// robustness, this profile is used in some videoconferencing and mobile
+ /// applications. This profile includes all features that are supported
+ /// in the Constrained Baseline Profile, plus three additional features
+ /// that can be used for loss robustness (or for other purposes such as
+ /// low-delay multi-point video stream compositing). The importance of
+ /// this profile has faded somewhat since the definition of the Constrained
+ /// Baseline Profile in 2009. All Constrained Baseline Profile bitstreams
+ /// are also considered to be Baseline Profile bitstreams, as these two
+ /// profiles share the same profile identifier code value.
+ H264CodecProfileBaseline,
+
+ /// @brief **H264** Main Profile (MP, 77)\n
+ /// \n
+ /// This profile is used for standard-definition digital TV broadcasts that
+ /// use the MPEG-4 format as defined in the
+ /// [DVB standard](http://www.etsi.org/deliver/etsi_ts/101100_101199/101154/01.09.01_60/ts_101154v010901p.pdf).
+ /// It is not, however, used for high-definition television broadcasts, as the
+ /// importance of this profile faded when the High Profile was developed
+ /// in 2004 for that application.
+ H264CodecProfileMain,
+
+ /// @brief **H264** Extended Profile (XP, 88)\n
+ /// \n
+ /// Intended as the streaming video profile, this profile has relatively high
+ /// compression capability and some extra tricks for robustness to data losses
+ /// and server stream switching.
+ H264CodecProfileExtended,
+
+ /// @brief **H264** High Profile (HiP, 100)\n
+ /// \n
+ /// The primary profile for broadcast and disc storage applications,
+ /// particularly for high-definition television applications (for example,
+ /// this is the profile adopted by the [Blu-ray Disc](https://en.wikipedia.org/wiki/Blu-ray_Disc)
+ /// storage format and the [DVB](https://en.wikipedia.org/wiki/Digital_Video_Broadcasting)
+ /// HDTV broadcast service).
+ H264CodecProfileHigh,
+
+ /// @brief **H264** High 10 Profile (Hi10P, 110)\n
+ /// \n
+ /// Going beyond typical mainstream consumer product capabilities, this
+ /// profile builds on top of the High Profile, adding support for up to 10
+ /// bits per sample of decoded picture precision.
+ H264CodecProfileHigh10,
+
+ /// @brief **H264** High 4:2:2 Profile (Hi422P, 122)\n
+ /// \n
+ /// Primarily targeting professional applications that use interlaced video,
+ /// this profile builds on top of the High 10 Profile, adding support for the
+ /// 4:2:2 chroma sampling format while using up to 10 bits per sample of
+ /// decoded picture precision.
+ H264CodecProfileHigh422,
+
+ /// @brief **H264** High 4:4:4 Predictive Profile (Hi444PP, 244)\n
+ /// \n
+ /// This profile builds on top of the High 4:2:2 Profile, supporting up to
+ /// 4:4:4 chroma sampling, up to 14 bits per sample, and additionally
+ /// supporting efficient lossless region coding and the coding of each
+ /// picture as three separate color planes.
+ H264CodecProfileHigh444Predictive,
+
+ /// @brief **VP9** profile 0\n
+ /// \n
+ /// There are several variants of the VP9 format (known as "coding profiles"),
+ /// which successively allow more features; profile 0 is the basic variant,
+ /// requiring the least from a hardware implementation.
+ ///
+ /// [Color depth](https://en.wikipedia.org/wiki/Color_depth): 8 bit/sample,
+ /// [chroma subsampling](https://en.wikipedia.org/wiki/Chroma_subsampling): 4:2:0
+ VP9CodecProfile0 = 20,
+
+ /// @brief **VP9** profile 1\n
+ /// \n
+ /// [Color depth](https://en.wikipedia.org/wiki/Color_depth): 8 bit,
+ /// [chroma subsampling](https://en.wikipedia.org/wiki/Chroma_subsampling): 4:2:0, 4:2:2, 4:4:4
+ VP9CodecProfile1,
+
+ /// @brief **VP9** profile 2\n
+ /// \n
+ /// [Color depth](https://en.wikipedia.org/wiki/Color_depth): 10–12 bit,
+ /// [chroma subsampling](https://en.wikipedia.org/wiki/Chroma_subsampling): 4:2:0
+ VP9CodecProfile2,
+
+ /// @brief **VP9** profile 3\n
+ /// \n
+ /// [Color depth](https://en.wikipedia.org/wiki/Color_depth): 10–12 bit,
+ /// [chroma subsampling](https://en.wikipedia.org/wiki/Chroma_subsampling): 4:2:0, 4:2:2, 4:4:4,
+ /// see [VP9 Bitstream & Decoding Process Specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf)
+ VP9CodecProfile3,
+
+ /// @brief **AV1** Main profile\n
+ /// \n
+ /// [Color depth](https://en.wikipedia.org/wiki/Color_depth): 8–10 bit,
+ /// [chroma subsampling](https://en.wikipedia.org/wiki/Chroma_subsampling): 4:2:0
+ /// see [AV1 specification](https://aomedia.org/av1/specification/)
+ AV1CodecProfileMain,
+
+ /// @brief **AV1** High profile\n
+ /// \n
+ /// [Color depth](https://en.wikipedia.org/wiki/Color_depth): 8–10 bit,
+ /// [chroma subsampling](https://en.wikipedia.org/wiki/Chroma_subsampling): 4:2:0, 4:4:4
+ /// see [AV1 specification](https://aomedia.org/av1/specification/)
+ AV1CodecProfileHigh,
+
+ /// @brief **AV1** Professional profile\n
+ /// \n
+ /// [Color depth](https://en.wikipedia.org/wiki/Color_depth): 8–12 bit,
+ /// [chroma subsampling](https://en.wikipedia.org/wiki/Chroma_subsampling): 4:2:0, 4:2:2, 4:4:4
+ /// see [AV1 specification](https://aomedia.org/av1/specification/)
+ AV1CodecProfileProfessional,
+ };
+ ///@}
+ //------------------------------------------------------------------------------
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_INPUTSTREAM_STREAMCODEC_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_constants.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_constants.h
new file mode 100644
index 0000000..8c12c26
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_constants.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017-2020 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_INPUTSTREAM_STREAMCONSTANTS_H
+#define C_API_ADDONINSTANCE_INPUTSTREAM_STREAMCONSTANTS_H
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_StreamConstants
+/// @brief The name of the inputstream add-on that should be used by Kodi to
+/// play the stream.
+///
+/// Leave blank to use Kodi's built-in playing capabilities or to allow
+/// ffmpeg to handle directly set to @ref STREAM_PROPERTY_VALUE_INPUTSTREAMFFMPEG.
+#define STREAM_PROPERTY_INPUTSTREAM "inputstream"
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_StreamConstants
+/// @brief The name of the default player to use for this inputstream add-on that
+/// should be used by Kodi to play the stream.
+///
+/// Leave blank to use Kodi's built-in player selection mechanism.
+/// Permitted values are:
+/// - "videodefaultplayer"
+/// - "audiodefaultplayer"
+#define STREAM_PROPERTY_INPUTSTREAM_PLAYER "inputstream-player"
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_StreamConstants
+/// @brief Identification string for an input stream.
+///
+/// This value can be used in addition to @ref STREAM_PROPERTY_INPUTSTREAM. It is
+/// used to provide the respective inpustream addon with additional
+/// identification.
+///
+/// The difference between this and other stream properties is that it is also
+/// passed in the associated @ref kodi::addon::CAddonBase::CreateInstance call.
+///
+/// This makes it possible to select different processing classes within the
+/// associated add-on.
+#define STREAM_PROPERTY_INPUTSTREAM_INSTANCE_ID "inputstream-instance-id"
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_StreamConstants
+/// @brief "true" to denote that the stream that should be played is a
+/// realtime stream. Any other value indicates that this is not a realtime
+/// stream.
+#define STREAM_PROPERTY_ISREALTIMESTREAM "isrealtimestream"
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_StreamConstants
+/// @brief Special value for @ref STREAM_PROPERTY_INPUTSTREAM to use
+/// ffmpeg to directly play a stream URL.
+#define STREAM_PROPERTY_VALUE_INPUTSTREAMFFMPEG "inputstream.ffmpeg"
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_StreamConstants
+/// @brief Max number of properties that can be sent to an Inputstream addon
+#define STREAM_MAX_PROPERTY_COUNT 30
+
+#endif /* !C_API_ADDONINSTANCE_INPUTSTREAM_STREAMCONSTANTS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_crypto.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_crypto.h
new file mode 100644
index 0000000..3d4f621
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_crypto.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2017-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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_INPUTSTREAM_STREAMCRYPTO_H
+#define C_API_ADDONINSTANCE_INPUTSTREAM_STREAMCRYPTO_H
+
+#include <stdint.h>
+#include <string.h>
+
+#define STREAMCRYPTO_VERSION_LEVEL 2
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_StreamEncryption_STREAM_CRYPTO_KEY_SYSTEM enum STREAM_CRYPTO_KEY_SYSTEM
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_StreamEncryption
+ /// @brief **Available ways to process stream cryptography**\n
+ /// For @ref cpp_kodi_addon_inputstream_Defs_StreamEncryption_StreamCryptoSession,
+ /// this defines the used and required auxiliary modules which are required to
+ /// process the encryption stream.
+ ///
+ /// Used to set wanted [digital rights management](https://en.wikipedia.org/wiki/Digital_rights_management)
+ /// (DRM) technology provider for stream.
+ ///@{
+ enum STREAM_CRYPTO_KEY_SYSTEM
+ {
+ /// @brief **0** - If no path is to be used, this is the default
+ STREAM_CRYPTO_KEY_SYSTEM_NONE = 0,
+
+ /// @brief **1** - To use [Widevine](https://en.wikipedia.org/wiki/Widevine) for processing
+ STREAM_CRYPTO_KEY_SYSTEM_WIDEVINE,
+
+ /// @brief **2** - To use [Playready](https://en.wikipedia.org/wiki/PlayReady) for processing
+ STREAM_CRYPTO_KEY_SYSTEM_PLAYREADY,
+
+ /// @brief **3** - To use Wiseplay for processing
+ STREAM_CRYPTO_KEY_SYSTEM_WISEPLAY,
+
+ /// @brief **4** - The maximum value to use in a list.
+ STREAM_CRYPTO_KEY_SYSTEM_COUNT
+ };
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_StreamEncryption_STREAM_CRYPTO_FLAGS enum STREAM_CRYPTO_FLAGS
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_StreamEncryption
+ /// @brief **Cryptography flags to use special conditions**\n
+ /// To identify special extra conditions.
+ ///
+ /// @note These variables are bit flags which are created using "|" can be used
+ /// together.
+ ///
+ ///@{
+ enum STREAM_CRYPTO_FLAGS
+ {
+ /// @brief **0000 0000** - Empty to set if nothing is used.
+ STREAM_CRYPTO_FLAG_NONE = 0,
+
+ /// @brief **0000 0001** - Is set in flags if decoding has to be done in
+ /// [trusted execution environment (TEE)](https://en.wikipedia.org/wiki/Trusted_execution_environment).
+ STREAM_CRYPTO_FLAG_SECURE_DECODER = (1 << 0)
+ };
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_inputstream_Defs_StreamEncryption_DEMUX_CRYPTO_INFO struct DEMUX_CRYPTO_INFO
+ /// @ingroup cpp_kodi_addon_inputstream_Defs_StreamEncryption
+ /// @brief **C data structure for processing encrypted streams.**\n
+ /// If required, this structure is used for every DEMUX_PACKET to be processed.
+ ///
+ ///@{
+ struct DEMUX_CRYPTO_INFO
+ {
+ /// @brief Number of subsamples.
+ uint16_t numSubSamples;
+
+ /// @brief Flags for later use.
+ uint16_t flags;
+
+ /// @brief @ref numSubSamples uint16_t's which define the size of clear size
+ /// of a subsample.
+ uint16_t* clearBytes;
+
+ /// @brief @ref numSubSamples uint32_t's which define the size of cipher size
+ /// of a subsample.
+ uint32_t* cipherBytes;
+
+ /// @brief Initialization vector
+ uint8_t iv[16];
+
+ /// @brief Key id
+ uint8_t kid[16];
+
+ /// @brief Encryption mode
+ uint16_t mode;
+
+ /// @brief Crypt blocks - number of blocks to encrypt in sample encryption pattern
+ uint8_t cryptBlocks;
+
+ /// @brief Skip blocks - number of blocks to skip in sample encryption pattern
+ uint8_t skipBlocks;
+ };
+ ///@}
+ //----------------------------------------------------------------------------
+
+ // Data to manage stream cryptography
+ struct STREAM_CRYPTO_SESSION
+ {
+ // keysystem for encrypted media, STREAM_CRYPTO_KEY_SYSTEM_NONE for unencrypted
+ // media.
+ //
+ // See STREAM_CRYPTO_KEY_SYSTEM for available options.
+ enum STREAM_CRYPTO_KEY_SYSTEM keySystem;
+
+ // Flags to use special conditions, see STREAM_CRYPTO_FLAGS for available flags.
+ uint8_t flags;
+
+ // The crypto session key id.
+ char sessionId[256];
+ };
+ ///@}
+ //----------------------------------------------------------------------------
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_INPUTSTREAM_STREAMCRYPTO_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/timing_constants.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/timing_constants.h
new file mode 100644
index 0000000..a226a0d
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/timing_constants.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015-2020 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_INPUTSTREAM_TIMINGCONSTANTS_H
+#define C_API_ADDONINSTANCE_INPUTSTREAM_TIMINGCONSTANTS_H
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_TimingConstants
+/// @brief Speed value to pause stream in playback.
+///
+#define STREAM_PLAYSPEED_PAUSE 0 // frame stepping
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_TimingConstants
+/// @brief Speed value to perform stream playback at normal speed.
+///
+/// See @ref STREAM_PLAYSPEED_PAUSE for pause of stream.
+///
+#define STREAM_PLAYSPEED_NORMAL 1000
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_TimingConstants
+/// @brief Time base represented as integer.
+///
+#define STREAM_TIME_BASE 1000000
+
+/// @ingroup cpp_kodi_addon_inputstream_Defs_TimingConstants
+/// @brief Undefined timestamp value.
+///
+/// Usually reported by demuxer that work on containers that do not provide
+/// either pts or dts.
+///
+#define STREAM_NOPTS_VALUE 0xFFF0000000000000
+
+// "C" defines to translate stream times
+#define STREAM_TIME_TO_MSEC(x) ((int)((double)(x)*1000 / STREAM_TIME_BASE))
+#define STREAM_SEC_TO_TIME(x) ((double)(x)*STREAM_TIME_BASE)
+#define STREAM_MSEC_TO_TIME(x) ((double)(x)*STREAM_TIME_BASE / 1000)
+
+#endif /* !C_API_ADDONINSTANCE_INPUTSTREAM_TIMINGCONSTANTS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/peripheral.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/peripheral.h
new file mode 100644
index 0000000..d5e1d2b
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/peripheral.h
@@ -0,0 +1,707 @@
+/*
+ * Copyright (C) 2014-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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PERIPHERAL_H
+#define C_API_ADDONINSTANCE_PERIPHERAL_H
+
+#include "../addon_base.h"
+
+/* indicates a joystick has no preference for port number */
+#define NO_PORT_REQUESTED (-1)
+
+/* joystick's driver button/hat/axis index is unknown */
+#define DRIVER_INDEX_UNKNOWN (-1)
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_General_PERIPHERAL_ERROR enum PERIPHERAL_ERROR
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_General
+ /// @brief **Peripheral add-on error codes**\n
+ /// Used as return values on most peripheral related functions.
+ ///
+ /// In this way, a peripheral instance signals errors in its processing and,
+ /// under certain conditions, allows Kodi to make corrections.
+ ///
+ ///@{
+ typedef enum PERIPHERAL_ERROR
+ {
+ /// @brief __0__ : No error occurred
+ PERIPHERAL_NO_ERROR = 0,
+
+ /// @brief __-1__ : An unknown error occurred
+ PERIPHERAL_ERROR_UNKNOWN = -1,
+
+ /// @brief __-2__ : The command failed
+ PERIPHERAL_ERROR_FAILED = -2,
+
+ /// @brief __-3__ : The parameters of the method are invalid for this operation
+ PERIPHERAL_ERROR_INVALID_PARAMETERS = -3,
+
+ /// @brief __-4__ : The method that the frontend called is not implemented
+ PERIPHERAL_ERROR_NOT_IMPLEMENTED = -4,
+
+ /// @brief __-5__ : No peripherals are connected
+ PERIPHERAL_ERROR_NOT_CONNECTED = -5,
+
+ /// @brief __-6__ : Peripherals are connected, but command was interrupted
+ PERIPHERAL_ERROR_CONNECTION_FAILED = -6,
+ } PERIPHERAL_ERROR;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ // @name Peripheral types
+ //{
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Peripheral_PERIPHERAL_TYPE enum PERIPHERAL_TYPE
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Peripheral
+ /// @brief **Peripheral types**\n
+ /// Types used to identify wanted peripheral.
+ ///@{
+ typedef enum PERIPHERAL_TYPE
+ {
+ /// @brief Type declared as unknown.
+ PERIPHERAL_TYPE_UNKNOWN,
+
+ /// @brief Type declared as joystick.
+ PERIPHERAL_TYPE_JOYSTICK,
+
+ /// @brief Type declared as keyboard.
+ PERIPHERAL_TYPE_KEYBOARD,
+ } PERIPHERAL_TYPE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief Information shared between peripherals
+ */
+ typedef struct PERIPHERAL_INFO
+ {
+ PERIPHERAL_TYPE type; /*!< type of peripheral */
+ char* name; /*!< name of peripheral */
+ uint16_t vendor_id; /*!< vendor ID of peripheral, 0x0000 if unknown */
+ uint16_t product_id; /*!< product ID of peripheral, 0x0000 if unknown */
+ unsigned int index; /*!< the order in which the add-on identified this peripheral */
+ } ATTR_PACKED PERIPHERAL_INFO;
+
+ /*!
+ * @brief Peripheral add-on capabilities.
+ */
+ typedef struct PERIPHERAL_CAPABILITIES
+ {
+ bool provides_joysticks; /*!< true if the add-on provides joysticks */
+ bool provides_joystick_rumble;
+ bool provides_joystick_power_off;
+ bool provides_buttonmaps; /*!< true if the add-on provides button maps */
+ } ATTR_PACKED PERIPHERAL_CAPABILITIES;
+
+ //}
+
+ // @name Event types
+ //{
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Event_PERIPHERAL_EVENT_TYPE enum PERIPHERAL_EVENT_TYPE
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Event
+ /// @brief **Event types**\n
+ /// Types of events that can be sent and received.
+ ///@{
+ typedef enum PERIPHERAL_EVENT_TYPE
+ {
+ /// @brief unknown event
+ PERIPHERAL_EVENT_TYPE_NONE,
+
+ /// @brief state changed for joystick driver button
+ PERIPHERAL_EVENT_TYPE_DRIVER_BUTTON,
+
+ /// @brief state changed for joystick driver hat
+ PERIPHERAL_EVENT_TYPE_DRIVER_HAT,
+
+ /// @brief state changed for joystick driver axis
+ PERIPHERAL_EVENT_TYPE_DRIVER_AXIS,
+
+ /// @brief set the state for joystick rumble motor
+ PERIPHERAL_EVENT_TYPE_SET_MOTOR,
+ } PERIPHERAL_EVENT_TYPE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Event_JOYSTICK_STATE_BUTTON enum JOYSTICK_STATE_BUTTON
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Event
+ /// @brief **State button**\n
+ /// States a button can have
+ ///@{
+ typedef enum JOYSTICK_STATE_BUTTON
+ {
+ /// @brief button is released
+ JOYSTICK_STATE_BUTTON_UNPRESSED = 0x0,
+
+ /// @brief button is pressed
+ JOYSTICK_STATE_BUTTON_PRESSED = 0x1,
+ } JOYSTICK_STATE_BUTTON;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Event_JOYSTICK_STATE_HAT enum JOYSTICK_STATE_HAT
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Event
+ /// @brief **State hat**\n
+ /// States a D-pad (also called a hat) can have
+ ///@{
+ typedef enum JOYSTICK_STATE_HAT
+ {
+ /// @brief no directions are pressed
+ JOYSTICK_STATE_HAT_UNPRESSED = 0x0,
+
+ /// @brief only left is pressed
+ JOYSTICK_STATE_HAT_LEFT = 0x1,
+
+ /// @brief only right is pressed
+ JOYSTICK_STATE_HAT_RIGHT = 0x2,
+
+ /// @brief only up is pressed
+ JOYSTICK_STATE_HAT_UP = 0x4,
+
+ /// @brief only down is pressed
+ JOYSTICK_STATE_HAT_DOWN = 0x8,
+
+ /// @brief left and up is pressed
+ JOYSTICK_STATE_HAT_LEFT_UP = JOYSTICK_STATE_HAT_LEFT | JOYSTICK_STATE_HAT_UP,
+
+ /// @brief left and down is pressed
+ JOYSTICK_STATE_HAT_LEFT_DOWN = JOYSTICK_STATE_HAT_LEFT | JOYSTICK_STATE_HAT_DOWN,
+
+ /// @brief right and up is pressed
+ JOYSTICK_STATE_HAT_RIGHT_UP = JOYSTICK_STATE_HAT_RIGHT | JOYSTICK_STATE_HAT_UP,
+
+ /// @brief right and down is pressed
+ JOYSTICK_STATE_HAT_RIGHT_DOWN = JOYSTICK_STATE_HAT_RIGHT | JOYSTICK_STATE_HAT_DOWN,
+ } JOYSTICK_STATE_HAT;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Event
+ /// @brief Axis value in the closed interval [-1.0, 1.0]
+ ///
+ /// The axis state uses the XInput coordinate system:
+ /// - Negative values signify down or to the left
+ /// - Positive values signify up or to the right
+ ///
+ typedef float JOYSTICK_STATE_AXIS;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Event
+ /// @brief Motor value in the closed interval [0.0, 1.0]
+ typedef float JOYSTICK_STATE_MOTOR;
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief Event information
+ */
+ typedef struct PERIPHERAL_EVENT
+ {
+ /*! @brief Index of the peripheral handling/receiving the event */
+ unsigned int peripheral_index;
+
+ /*! @brief Type of the event used to determine which enum field to access below */
+ PERIPHERAL_EVENT_TYPE type;
+
+ /*! @brief The index of the event source */
+ unsigned int driver_index;
+
+ JOYSTICK_STATE_BUTTON driver_button_state;
+ JOYSTICK_STATE_HAT driver_hat_state;
+ JOYSTICK_STATE_AXIS driver_axis_state;
+ JOYSTICK_STATE_MOTOR motor_state;
+ } ATTR_PACKED PERIPHERAL_EVENT;
+
+ //}
+
+ // @name Joystick types
+ //{
+
+ /*!
+ * @brief Info specific to joystick peripherals
+ */
+ typedef struct JOYSTICK_INFO
+ {
+ PERIPHERAL_INFO peripheral; /*!< @brief peripheral info for this joystick */
+ char* provider; /*!< @brief name of the driver or interface providing the joystick */
+ int requested_port; /*!< @brief requested port number (such as for 360 controllers), or NO_PORT_REQUESTED */
+ unsigned int button_count; /*!< @brief number of buttons reported by the driver */
+ unsigned int hat_count; /*!< @brief number of hats reported by the driver */
+ unsigned int axis_count; /*!< @brief number of axes reported by the driver */
+ unsigned int motor_count; /*!< @brief number of motors reported by the driver */
+ bool supports_poweroff; /*!< @brief whether the joystick supports being powered off */
+ } ATTR_PACKED JOYSTICK_INFO;
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick_JOYSTICK_DRIVER_PRIMITIVE_TYPE enum JOYSTICK_DRIVER_PRIMITIVE_TYPE
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Joystick
+ /// @brief **Driver primitive type**\n
+ /// Driver input primitives
+ ///
+ /// Mapping lower-level driver values to higher-level controller features is
+ /// non-injective; two triggers can share a single axis.
+ ///
+ /// To handle this, driver values are subdivided into "primitives" that map
+ /// injectively to higher-level features.
+ ///
+ ///@{
+ typedef enum JOYSTICK_DRIVER_PRIMITIVE_TYPE
+ {
+ /// @brief Driver input primitive type unknown
+ JOYSTICK_DRIVER_PRIMITIVE_TYPE_UNKNOWN,
+
+ /// @brief Driver input primitive type button
+ JOYSTICK_DRIVER_PRIMITIVE_TYPE_BUTTON,
+
+ /// @brief Driver input primitive type hat direction
+ JOYSTICK_DRIVER_PRIMITIVE_TYPE_HAT_DIRECTION,
+
+ /// @brief Driver input primitive type semiaxis
+ JOYSTICK_DRIVER_PRIMITIVE_TYPE_SEMIAXIS,
+
+ /// @brief Driver input primitive type motor
+ JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOTOR,
+
+ /// @brief Driver input primitive type key
+ JOYSTICK_DRIVER_PRIMITIVE_TYPE_KEY,
+
+ /// @brief Driver input primitive type mouse button
+ JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOUSE_BUTTON,
+
+ /// @brief Driver input primitive type relative pointer direction
+ JOYSTICK_DRIVER_PRIMITIVE_TYPE_RELPOINTER_DIRECTION,
+ } JOYSTICK_DRIVER_PRIMITIVE_TYPE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief Button primitive
+ */
+ typedef struct JOYSTICK_DRIVER_BUTTON
+ {
+ int index;
+ } ATTR_PACKED JOYSTICK_DRIVER_BUTTON;
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick_JOYSTICK_DRIVER_HAT_DIRECTION enum JOYSTICK_DRIVER_HAT_DIRECTION
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Joystick
+ /// @brief **Driver direction**\n
+ /// Hat direction.
+ ///@{
+ typedef enum JOYSTICK_DRIVER_HAT_DIRECTION
+ {
+ /// @brief Driver hat unknown
+ JOYSTICK_DRIVER_HAT_UNKNOWN,
+
+ /// @brief Driver hat left
+ JOYSTICK_DRIVER_HAT_LEFT,
+
+ /// @brief Driver hat right
+ JOYSTICK_DRIVER_HAT_RIGHT,
+
+ /// @brief Driver hat up
+ JOYSTICK_DRIVER_HAT_UP,
+
+ /// @brief Driver hat down
+ JOYSTICK_DRIVER_HAT_DOWN,
+ } JOYSTICK_DRIVER_HAT_DIRECTION;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief Hat direction primitive
+ */
+ typedef struct JOYSTICK_DRIVER_HAT
+ {
+ int index;
+ JOYSTICK_DRIVER_HAT_DIRECTION direction;
+ } ATTR_PACKED JOYSTICK_DRIVER_HAT;
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick_JOYSTICK_DRIVER_SEMIAXIS_DIRECTION enum JOYSTICK_DRIVER_SEMIAXIS_DIRECTION
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Joystick
+ /// @brief **Driver direction**\n
+ /// Semiaxis direction.
+ ///@{
+ typedef enum JOYSTICK_DRIVER_SEMIAXIS_DIRECTION
+ {
+ /// @brief negative half of the axis
+ JOYSTICK_DRIVER_SEMIAXIS_NEGATIVE = -1,
+
+ /// @brief unknown direction
+ JOYSTICK_DRIVER_SEMIAXIS_UNKNOWN = 0,
+
+ /// @brief positive half of the axis
+ JOYSTICK_DRIVER_SEMIAXIS_POSITIVE = 1,
+ } JOYSTICK_DRIVER_SEMIAXIS_DIRECTION;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief Semiaxis primitive
+ */
+ typedef struct JOYSTICK_DRIVER_SEMIAXIS
+ {
+ int index;
+ int center;
+ JOYSTICK_DRIVER_SEMIAXIS_DIRECTION direction;
+ unsigned int range;
+ } ATTR_PACKED JOYSTICK_DRIVER_SEMIAXIS;
+
+ /*!
+ * @brief Motor primitive
+ */
+ typedef struct JOYSTICK_DRIVER_MOTOR
+ {
+ int index;
+ } ATTR_PACKED JOYSTICK_DRIVER_MOTOR;
+
+ /*!
+ * @brief Keyboard key primitive
+ */
+ typedef struct JOYSTICK_DRIVER_KEY
+ {
+ char keycode[16];
+ } ATTR_PACKED JOYSTICK_DRIVER_KEY;
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick_JOYSTICK_DRIVER_MOUSE_INDEX enum JOYSTICK_DRIVER_MOUSE_INDEX
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Joystick
+ /// @brief **Buttons**\n
+ /// Mouse buttons.
+ ///@{
+ typedef enum JOYSTICK_DRIVER_MOUSE_INDEX
+ {
+ /// @brief Mouse index unknown
+ JOYSTICK_DRIVER_MOUSE_INDEX_UNKNOWN,
+
+ /// @brief Mouse index left
+ JOYSTICK_DRIVER_MOUSE_INDEX_LEFT,
+
+ /// @brief Mouse index right
+ JOYSTICK_DRIVER_MOUSE_INDEX_RIGHT,
+
+ /// @brief Mouse index middle
+ JOYSTICK_DRIVER_MOUSE_INDEX_MIDDLE,
+
+ /// @brief Mouse index button 4
+ JOYSTICK_DRIVER_MOUSE_INDEX_BUTTON4,
+
+ /// @brief Mouse index button 5
+ JOYSTICK_DRIVER_MOUSE_INDEX_BUTTON5,
+
+ /// @brief Mouse index wheel up
+ JOYSTICK_DRIVER_MOUSE_INDEX_WHEEL_UP,
+
+ /// @brief Mouse index wheel down
+ JOYSTICK_DRIVER_MOUSE_INDEX_WHEEL_DOWN,
+
+ /// @brief Mouse index horizontal wheel left
+ JOYSTICK_DRIVER_MOUSE_INDEX_HORIZ_WHEEL_LEFT,
+
+ /// @brief Mouse index horizontal wheel right
+ JOYSTICK_DRIVER_MOUSE_INDEX_HORIZ_WHEEL_RIGHT,
+ } JOYSTICK_DRIVER_MOUSE_INDEX;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief Mouse button primitive
+ */
+ typedef struct JOYSTICK_DRIVER_MOUSE_BUTTON
+ {
+ JOYSTICK_DRIVER_MOUSE_INDEX button;
+ } ATTR_PACKED JOYSTICK_DRIVER_MOUSE_BUTTON;
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick_JOYSTICK_DRIVER_RELPOINTER_DIRECTION enum JOYSTICK_DRIVER_RELPOINTER_DIRECTION
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Joystick
+ /// @brief **Pointer direction**\n
+ /// Relative pointer direction
+ ///@{
+ typedef enum JOYSTICK_DRIVER_RELPOINTER_DIRECTION
+ {
+ /// @brief Relative pointer direction unknown
+ JOYSTICK_DRIVER_RELPOINTER_UNKNOWN,
+
+ /// @brief Relative pointer direction left
+ JOYSTICK_DRIVER_RELPOINTER_LEFT,
+
+ /// @brief Relative pointer direction right
+ JOYSTICK_DRIVER_RELPOINTER_RIGHT,
+
+ /// @brief Relative pointer direction up
+ JOYSTICK_DRIVER_RELPOINTER_UP,
+
+ /// @brief Relative pointer direction down
+ JOYSTICK_DRIVER_RELPOINTER_DOWN,
+ } JOYSTICK_DRIVER_RELPOINTER_DIRECTION;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief Relative pointer direction primitive
+ */
+ typedef struct JOYSTICK_DRIVER_RELPOINTER
+ {
+ JOYSTICK_DRIVER_RELPOINTER_DIRECTION direction;
+ } ATTR_PACKED JOYSTICK_DRIVER_RELPOINTER;
+
+ /*!
+ * @brief Driver primitive struct
+ */
+ typedef struct JOYSTICK_DRIVER_PRIMITIVE
+ {
+ JOYSTICK_DRIVER_PRIMITIVE_TYPE type;
+ union
+ {
+ struct JOYSTICK_DRIVER_BUTTON button;
+ struct JOYSTICK_DRIVER_HAT hat;
+ struct JOYSTICK_DRIVER_SEMIAXIS semiaxis;
+ struct JOYSTICK_DRIVER_MOTOR motor;
+ struct JOYSTICK_DRIVER_KEY key;
+ struct JOYSTICK_DRIVER_MOUSE_BUTTON mouse;
+ struct JOYSTICK_DRIVER_RELPOINTER relpointer;
+ };
+ } ATTR_PACKED JOYSTICK_DRIVER_PRIMITIVE;
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick_JOYSTICK_FEATURE_TYPE enum JOYSTICK_FEATURE_TYPE
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Joystick
+ /// @brief **Feature type**\n
+ /// Controller feature.
+ ///
+ /// Controller features are an abstraction over driver values. Each feature
+ /// maps to one or more driver primitives.
+ ///
+ ///@{
+ typedef enum JOYSTICK_FEATURE_TYPE
+ {
+ /// @brief Unknown type
+ JOYSTICK_FEATURE_TYPE_UNKNOWN,
+
+ /// @brief Type scalar
+ JOYSTICK_FEATURE_TYPE_SCALAR,
+
+ /// @brief Type analog stick
+ JOYSTICK_FEATURE_TYPE_ANALOG_STICK,
+
+ /// @brief Type accelerometer
+ JOYSTICK_FEATURE_TYPE_ACCELEROMETER,
+
+ /// @brief Type motor
+ JOYSTICK_FEATURE_TYPE_MOTOR,
+
+ /// @brief Type relative pointer
+ JOYSTICK_FEATURE_TYPE_RELPOINTER,
+
+ /// @brief Type absolute pointer
+ JOYSTICK_FEATURE_TYPE_ABSPOINTER,
+
+ /// @brief Type wheel
+ JOYSTICK_FEATURE_TYPE_WHEEL,
+
+ /// @brief Type throttle
+ JOYSTICK_FEATURE_TYPE_THROTTLE,
+
+ /// @brief Type key
+ JOYSTICK_FEATURE_TYPE_KEY,
+ } JOYSTICK_FEATURE_TYPE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_peripheral_Defs_Joystick_JOYSTICK_FEATURE_PRIMITIVE enum JOYSTICK_FEATURE_PRIMITIVE
+ /// @ingroup cpp_kodi_addon_peripheral_Defs_Joystick
+ /// @brief **Feature primitives**\n
+ /// Indices used to access a feature's driver primitives.
+ ///
+ ///@{
+ typedef enum JOYSTICK_FEATURE_PRIMITIVE
+ {
+ /// @brief Scalar feature (a button, hat direction or semiaxis)
+ JOYSTICK_SCALAR_PRIMITIVE = 0,
+
+ /// @brief Analog stick up
+ JOYSTICK_ANALOG_STICK_UP = 0,
+ /// @brief Analog stick down
+ JOYSTICK_ANALOG_STICK_DOWN = 1,
+ /// @brief Analog stick right
+ JOYSTICK_ANALOG_STICK_RIGHT = 2,
+ /// @brief Analog stick left
+ JOYSTICK_ANALOG_STICK_LEFT = 3,
+
+ /// @brief Accelerometer X
+ JOYSTICK_ACCELEROMETER_POSITIVE_X = 0,
+ /// @brief Accelerometer Y
+ JOYSTICK_ACCELEROMETER_POSITIVE_Y = 1,
+ /// @brief Accelerometer Z
+ JOYSTICK_ACCELEROMETER_POSITIVE_Z = 2,
+
+ /// @brief Motor
+ JOYSTICK_MOTOR_PRIMITIVE = 0,
+
+ /// @brief Wheel left
+ JOYSTICK_WHEEL_LEFT = 0,
+ /// @brief Wheel right
+ JOYSTICK_WHEEL_RIGHT = 1,
+
+ /// @brief Throttle up
+ JOYSTICK_THROTTLE_UP = 0,
+ /// @brief Throttle down
+ JOYSTICK_THROTTLE_DOWN = 1,
+
+ /// @brief Key
+ JOYSTICK_KEY_PRIMITIVE = 0,
+
+ /// @brief Mouse button
+ JOYSTICK_MOUSE_BUTTON = 0,
+
+ /// @brief Relative pointer direction up
+ JOYSTICK_RELPOINTER_UP = 0,
+ /// @brief Relative pointer direction down
+ JOYSTICK_RELPOINTER_DOWN = 1,
+ /// @brief Relative pointer direction right
+ JOYSTICK_RELPOINTER_RIGHT = 2,
+ /// @brief Relative pointer direction left
+ JOYSTICK_RELPOINTER_LEFT = 3,
+
+ /// @brief Maximum number of primitives
+ JOYSTICK_PRIMITIVE_MAX = 4,
+ } JOYSTICK_FEATURE_PRIMITIVE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief Mapping between higher-level controller feature and its driver primitives
+ */
+ typedef struct JOYSTICK_FEATURE
+ {
+ char* name;
+ JOYSTICK_FEATURE_TYPE type;
+ struct JOYSTICK_DRIVER_PRIMITIVE primitives[JOYSTICK_PRIMITIVE_MAX];
+ } ATTR_PACKED JOYSTICK_FEATURE;
+ //}
+
+ typedef struct AddonProps_Peripheral
+ {
+ const char* user_path; /*!< @brief path to the user profile */
+ const char* addon_path; /*!< @brief path to this add-on */
+ } ATTR_PACKED AddonProps_Peripheral;
+
+ struct AddonInstance_Peripheral;
+
+ typedef struct AddonToKodiFuncTable_Peripheral
+ {
+ KODI_HANDLE kodiInstance;
+ void (*trigger_scan)(void* kodiInstance);
+ void (*refresh_button_maps)(void* kodiInstance,
+ const char* device_name,
+ const char* controller_id);
+ unsigned int (*feature_count)(void* kodiInstance,
+ const char* controller_id,
+ JOYSTICK_FEATURE_TYPE type);
+ JOYSTICK_FEATURE_TYPE(*feature_type)
+ (void* kodiInstance, const char* controller_id, const char* feature_name);
+ } AddonToKodiFuncTable_Peripheral;
+
+ //! @todo Mouse, light gun, multitouch
+
+ typedef struct KodiToAddonFuncTable_Peripheral
+ {
+ KODI_HANDLE addonInstance;
+
+ void(__cdecl* get_capabilities)(const struct AddonInstance_Peripheral* addonInstance,
+ struct PERIPHERAL_CAPABILITIES* capabilities);
+ PERIPHERAL_ERROR(__cdecl* perform_device_scan)
+ (const struct AddonInstance_Peripheral* addonInstance,
+ unsigned int* peripheral_count,
+ struct PERIPHERAL_INFO** scan_results);
+ void(__cdecl* free_scan_results)(const struct AddonInstance_Peripheral* addonInstance,
+ unsigned int peripheral_count,
+ struct PERIPHERAL_INFO* scan_results);
+ PERIPHERAL_ERROR(__cdecl* get_events)
+ (const struct AddonInstance_Peripheral* addonInstance,
+ unsigned int* event_count,
+ struct PERIPHERAL_EVENT** events);
+ void(__cdecl* free_events)(const struct AddonInstance_Peripheral* addonInstance,
+ unsigned int event_count,
+ struct PERIPHERAL_EVENT* events);
+ bool(__cdecl* send_event)(const struct AddonInstance_Peripheral* addonInstance,
+ const struct PERIPHERAL_EVENT* event);
+
+ /// @name Joystick operations
+ ///{
+ PERIPHERAL_ERROR(__cdecl* get_joystick_info)
+ (const struct AddonInstance_Peripheral* addonInstance,
+ unsigned int index,
+ struct JOYSTICK_INFO* info);
+ void(__cdecl* free_joystick_info)(const struct AddonInstance_Peripheral* addonInstance,
+ struct JOYSTICK_INFO* info);
+ PERIPHERAL_ERROR(__cdecl* get_features)
+ (const struct AddonInstance_Peripheral* addonInstance,
+ const struct JOYSTICK_INFO* joystick,
+ const char* controller_id,
+ unsigned int* feature_count,
+ struct JOYSTICK_FEATURE** features);
+ void(__cdecl* free_features)(const struct AddonInstance_Peripheral* addonInstance,
+ unsigned int feature_count,
+ struct JOYSTICK_FEATURE* features);
+ PERIPHERAL_ERROR(__cdecl* map_features)
+ (const struct AddonInstance_Peripheral* addonInstance,
+ const struct JOYSTICK_INFO* joystick,
+ const char* controller_id,
+ unsigned int feature_count,
+ const struct JOYSTICK_FEATURE* features);
+ PERIPHERAL_ERROR(__cdecl* get_ignored_primitives)
+ (const struct AddonInstance_Peripheral* addonInstance,
+ const struct JOYSTICK_INFO* joystick,
+ unsigned int* feature_count,
+ struct JOYSTICK_DRIVER_PRIMITIVE** primitives);
+ void(__cdecl* free_primitives)(const struct AddonInstance_Peripheral* addonInstance,
+ unsigned int,
+ struct JOYSTICK_DRIVER_PRIMITIVE* primitives);
+ PERIPHERAL_ERROR(__cdecl* set_ignored_primitives)
+ (const struct AddonInstance_Peripheral* addonInstance,
+ const struct JOYSTICK_INFO* joystick,
+ unsigned int primitive_count,
+ const struct JOYSTICK_DRIVER_PRIMITIVE* primitives);
+ void(__cdecl* save_button_map)(const struct AddonInstance_Peripheral* addonInstance,
+ const struct JOYSTICK_INFO* joystick);
+ void(__cdecl* revert_button_map)(const struct AddonInstance_Peripheral* addonInstance,
+ const struct JOYSTICK_INFO* joystick);
+ void(__cdecl* reset_button_map)(const struct AddonInstance_Peripheral* addonInstance,
+ const struct JOYSTICK_INFO* joystick,
+ const char* controller_id);
+ void(__cdecl* power_off_joystick)(const struct AddonInstance_Peripheral* addonInstance,
+ unsigned int index);
+ ///}
+ } KodiToAddonFuncTable_Peripheral;
+
+ typedef struct AddonInstance_Peripheral
+ {
+ struct AddonProps_Peripheral* props;
+ struct AddonToKodiFuncTable_Peripheral* toKodi;
+ struct KodiToAddonFuncTable_Peripheral* toAddon;
+ } AddonInstance_Peripheral;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PERIPHERAL_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr.h
new file mode 100644
index 0000000..96e1eea
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr.h
@@ -0,0 +1,343 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_H
+#define C_API_ADDONINSTANCE_PVR_H
+
+#include "../../AddonBase.h"
+#include "pvr/pvr_channel_groups.h"
+#include "pvr/pvr_channels.h"
+#include "pvr/pvr_defines.h"
+#include "pvr/pvr_edl.h"
+#include "pvr/pvr_epg.h"
+#include "pvr/pvr_general.h"
+#include "pvr/pvr_menu_hook.h"
+#include "pvr/pvr_providers.h"
+#include "pvr/pvr_recordings.h"
+#include "pvr/pvr_stream.h"
+#include "pvr/pvr_timers.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" main interface function tables between Kodi and addon
+//
+// Values related to all parts and not used direct on addon, are to define here.
+//
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ /*!
+ * @internal
+ * @brief PVR "C" basis API interface
+ *
+ * This field contains things that are exchanged between Kodi and Addon
+ * and is the basis of the PVR-side "C" API.
+ *
+ * @warning Care should be taken when making changes in this fields!\n
+ * Changes can destroy API in addons that have already been created. If a
+ * necessary change or new feature is added, the version of the PVR
+ * at @ref ADDON_INSTANCE_VERSION_PVR_MIN must be increased too.\n
+ * \n
+ * Conditional changes can be made in some places, without min PVR version
+ * increase. The add-on should then use CreateInstanceEx and add partial tests
+ * for this in the C++ header.
+ *
+ * Have by add of new parts a look about **Doxygen** `\\ingroup`, so that
+ * added parts included in documentation.
+ *
+ * If you add addon side related documentation, where his dev need know,
+ * use `///`. For parts only for Kodi make it like here.
+ *
+ * @endinternal
+ */
+
+ struct AddonInstance_PVR;
+
+ /*!
+ * @brief Structure to define typical standard values
+ */
+ typedef struct AddonProperties_PVR
+ {
+ const char* strUserPath;
+ const char* strClientPath;
+ int iEpgMaxFutureDays;
+ int iEpgMaxPastDays;
+ } AddonProperties_PVR;
+
+ /*!
+ * @brief Structure to transfer the methods from Kodi to addon
+ */
+ typedef struct AddonToKodiFuncTable_PVR
+ {
+ // Pointer inside Kodi where used from him to find his class
+ KODI_HANDLE kodiInstance;
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // General callback functions
+ void (*AddMenuHook)(void* kodiInstance, const struct PVR_MENUHOOK* hook);
+ void (*RecordingNotification)(void* kodiInstance,
+ const char* name,
+ const char* fileName,
+ bool on);
+ void (*ConnectionStateChange)(void* kodiInstance,
+ const char* strConnectionString,
+ enum PVR_CONNECTION_STATE newState,
+ const char* strMessage);
+ void (*EpgEventStateChange)(void* kodiInstance,
+ struct EPG_TAG* tag,
+ enum EPG_EVENT_STATE newState);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Transfer functions where give data back to Kodi, e.g. GetChannels calls TransferChannelEntry
+ void (*TransferChannelEntry)(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const struct PVR_CHANNEL* chan);
+ void (*TransferProviderEntry)(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const struct PVR_PROVIDER* chanProvider);
+ void (*TransferChannelGroup)(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const struct PVR_CHANNEL_GROUP* group);
+ void (*TransferChannelGroupMember)(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const struct PVR_CHANNEL_GROUP_MEMBER* member);
+ void (*TransferEpgEntry)(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const struct EPG_TAG* epgentry);
+ void (*TransferRecordingEntry)(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const struct PVR_RECORDING* recording);
+ void (*TransferTimerEntry)(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const struct PVR_TIMER* timer);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Kodi inform interface functions
+ void (*TriggerChannelUpdate)(void* kodiInstance);
+ void (*TriggerProvidersUpdate)(void* kodiInstance);
+ void (*TriggerChannelGroupsUpdate)(void* kodiInstance);
+ void (*TriggerEpgUpdate)(void* kodiInstance, unsigned int iChannelUid);
+ void (*TriggerRecordingUpdate)(void* kodiInstance);
+ void (*TriggerTimerUpdate)(void* kodiInstance);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Stream demux interface functions
+ void (*FreeDemuxPacket)(void* kodiInstance, struct DEMUX_PACKET* pPacket);
+ struct DEMUX_PACKET* (*AllocateDemuxPacket)(void* kodiInstance, int iDataSize);
+ struct PVR_CODEC (*GetCodecByName)(const void* kodiInstance, const char* strCodecName);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // New functions becomes added below and can be on another API change (where
+ // breaks min API version) moved up.
+ } AddonToKodiFuncTable_PVR;
+
+ /*!
+ * @brief Structure to transfer the methods from addon to Kodi
+ */
+ typedef struct KodiToAddonFuncTable_PVR
+ {
+ // Pointer inside addon where used on them to find his instance class (currently unused!)
+ KODI_HANDLE addonInstance;
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // General interface functions
+ enum PVR_ERROR(__cdecl* GetCapabilities)(const struct AddonInstance_PVR*,
+ struct PVR_ADDON_CAPABILITIES*);
+ enum PVR_ERROR(__cdecl* GetBackendName)(const struct AddonInstance_PVR*, char*, int);
+ enum PVR_ERROR(__cdecl* GetBackendVersion)(const struct AddonInstance_PVR*, char*, int);
+ enum PVR_ERROR(__cdecl* GetBackendHostname)(const struct AddonInstance_PVR*, char*, int);
+ enum PVR_ERROR(__cdecl* GetConnectionString)(const struct AddonInstance_PVR*, char*, int);
+ enum PVR_ERROR(__cdecl* GetDriveSpace)(const struct AddonInstance_PVR*, uint64_t*, uint64_t*);
+ enum PVR_ERROR(__cdecl* CallSettingsMenuHook)(const struct AddonInstance_PVR*,
+ const struct PVR_MENUHOOK*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Channel interface functions
+
+ enum PVR_ERROR(__cdecl* GetChannelsAmount)(const struct AddonInstance_PVR*, int*);
+ enum PVR_ERROR(__cdecl* GetChannels)(const struct AddonInstance_PVR*, PVR_HANDLE, bool);
+ enum PVR_ERROR(__cdecl* GetChannelStreamProperties)(const struct AddonInstance_PVR*,
+ const struct PVR_CHANNEL*,
+ struct PVR_NAMED_VALUE*,
+ unsigned int*);
+ enum PVR_ERROR(__cdecl* GetSignalStatus)(const struct AddonInstance_PVR*,
+ int,
+ struct PVR_SIGNAL_STATUS*);
+ enum PVR_ERROR(__cdecl* GetDescrambleInfo)(const struct AddonInstance_PVR*,
+ int,
+ struct PVR_DESCRAMBLE_INFO*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Provider interface functions
+
+ enum PVR_ERROR(__cdecl* GetProvidersAmount)(const struct AddonInstance_PVR*, int*);
+ enum PVR_ERROR(__cdecl* GetProviders)(const struct AddonInstance_PVR*, PVR_HANDLE);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Channel group interface functions
+ enum PVR_ERROR(__cdecl* GetChannelGroupsAmount)(const struct AddonInstance_PVR*, int*);
+ enum PVR_ERROR(__cdecl* GetChannelGroups)(const struct AddonInstance_PVR*, PVR_HANDLE, bool);
+ enum PVR_ERROR(__cdecl* GetChannelGroupMembers)(const struct AddonInstance_PVR*,
+ PVR_HANDLE,
+ const struct PVR_CHANNEL_GROUP*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Channel edit interface functions
+ enum PVR_ERROR(__cdecl* DeleteChannel)(const struct AddonInstance_PVR*,
+ const struct PVR_CHANNEL*);
+ enum PVR_ERROR(__cdecl* RenameChannel)(const struct AddonInstance_PVR*,
+ const struct PVR_CHANNEL*);
+ enum PVR_ERROR(__cdecl* OpenDialogChannelSettings)(const struct AddonInstance_PVR*,
+ const struct PVR_CHANNEL*);
+ enum PVR_ERROR(__cdecl* OpenDialogChannelAdd)(const struct AddonInstance_PVR*,
+ const struct PVR_CHANNEL*);
+ enum PVR_ERROR(__cdecl* OpenDialogChannelScan)(const struct AddonInstance_PVR*);
+ enum PVR_ERROR(__cdecl* CallChannelMenuHook)(const struct AddonInstance_PVR*,
+ const PVR_MENUHOOK*,
+ const PVR_CHANNEL*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // EPG interface functions
+ enum PVR_ERROR(__cdecl* GetEPGForChannel)(
+ const struct AddonInstance_PVR*, PVR_HANDLE, int, time_t, time_t);
+ enum PVR_ERROR(__cdecl* IsEPGTagRecordable)(const struct AddonInstance_PVR*,
+ const struct EPG_TAG*,
+ bool*);
+ enum PVR_ERROR(__cdecl* IsEPGTagPlayable)(const struct AddonInstance_PVR*,
+ const struct EPG_TAG*,
+ bool*);
+ enum PVR_ERROR(__cdecl* GetEPGTagEdl)(const struct AddonInstance_PVR*,
+ const struct EPG_TAG*,
+ struct PVR_EDL_ENTRY[],
+ int*);
+ enum PVR_ERROR(__cdecl* GetEPGTagStreamProperties)(const struct AddonInstance_PVR*,
+ const struct EPG_TAG*,
+ struct PVR_NAMED_VALUE*,
+ unsigned int*);
+ enum PVR_ERROR(__cdecl* SetEPGMaxPastDays)(const struct AddonInstance_PVR*, int);
+ enum PVR_ERROR(__cdecl* SetEPGMaxFutureDays)(const struct AddonInstance_PVR*, int);
+ enum PVR_ERROR(__cdecl* CallEPGMenuHook)(const struct AddonInstance_PVR*,
+ const struct PVR_MENUHOOK*,
+ const struct EPG_TAG*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Recording interface functions
+ enum PVR_ERROR(__cdecl* GetRecordingsAmount)(const struct AddonInstance_PVR*, bool, int*);
+ enum PVR_ERROR(__cdecl* GetRecordings)(const struct AddonInstance_PVR*, PVR_HANDLE, bool);
+ enum PVR_ERROR(__cdecl* DeleteRecording)(const struct AddonInstance_PVR*,
+ const struct PVR_RECORDING*);
+ enum PVR_ERROR(__cdecl* UndeleteRecording)(const struct AddonInstance_PVR*,
+ const struct PVR_RECORDING*);
+ enum PVR_ERROR(__cdecl* DeleteAllRecordingsFromTrash)(const struct AddonInstance_PVR*);
+ enum PVR_ERROR(__cdecl* RenameRecording)(const struct AddonInstance_PVR*,
+ const struct PVR_RECORDING*);
+ enum PVR_ERROR(__cdecl* SetRecordingLifetime)(const struct AddonInstance_PVR*,
+ const struct PVR_RECORDING*);
+ enum PVR_ERROR(__cdecl* SetRecordingPlayCount)(const struct AddonInstance_PVR*,
+ const struct PVR_RECORDING*,
+ int);
+ enum PVR_ERROR(__cdecl* SetRecordingLastPlayedPosition)(const struct AddonInstance_PVR*,
+ const struct PVR_RECORDING*,
+ int);
+ enum PVR_ERROR(__cdecl* GetRecordingLastPlayedPosition)(const struct AddonInstance_PVR*,
+ const struct PVR_RECORDING*,
+ int*);
+ enum PVR_ERROR(__cdecl* GetRecordingEdl)(const struct AddonInstance_PVR*,
+ const struct PVR_RECORDING*,
+ struct PVR_EDL_ENTRY[],
+ int*);
+ enum PVR_ERROR(__cdecl* GetRecordingSize)(const struct AddonInstance_PVR*,
+ const PVR_RECORDING*,
+ int64_t*);
+ enum PVR_ERROR(__cdecl* GetRecordingStreamProperties)(const struct AddonInstance_PVR*,
+ const struct PVR_RECORDING*,
+ struct PVR_NAMED_VALUE*,
+ unsigned int*);
+ enum PVR_ERROR(__cdecl* CallRecordingMenuHook)(const struct AddonInstance_PVR*,
+ const struct PVR_MENUHOOK*,
+ const struct PVR_RECORDING*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Timer interface functions
+ enum PVR_ERROR(__cdecl* GetTimerTypes)(const struct AddonInstance_PVR*,
+ struct PVR_TIMER_TYPE[],
+ int*);
+ enum PVR_ERROR(__cdecl* GetTimersAmount)(const struct AddonInstance_PVR*, int*);
+ enum PVR_ERROR(__cdecl* GetTimers)(const struct AddonInstance_PVR*, PVR_HANDLE);
+ enum PVR_ERROR(__cdecl* AddTimer)(const struct AddonInstance_PVR*, const struct PVR_TIMER*);
+ enum PVR_ERROR(__cdecl* DeleteTimer)(const struct AddonInstance_PVR*,
+ const struct PVR_TIMER*,
+ bool);
+ enum PVR_ERROR(__cdecl* UpdateTimer)(const struct AddonInstance_PVR*, const struct PVR_TIMER*);
+ enum PVR_ERROR(__cdecl* CallTimerMenuHook)(const struct AddonInstance_PVR*,
+ const struct PVR_MENUHOOK*,
+ const struct PVR_TIMER*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Powersaving interface functions
+ enum PVR_ERROR(__cdecl* OnSystemSleep)(const struct AddonInstance_PVR*);
+ enum PVR_ERROR(__cdecl* OnSystemWake)(const struct AddonInstance_PVR*);
+ enum PVR_ERROR(__cdecl* OnPowerSavingActivated)(const struct AddonInstance_PVR*);
+ enum PVR_ERROR(__cdecl* OnPowerSavingDeactivated)(const struct AddonInstance_PVR*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Live stream read interface functions
+ bool(__cdecl* OpenLiveStream)(const struct AddonInstance_PVR*, const struct PVR_CHANNEL*);
+ void(__cdecl* CloseLiveStream)(const struct AddonInstance_PVR*);
+ int(__cdecl* ReadLiveStream)(const struct AddonInstance_PVR*, unsigned char*, unsigned int);
+ int64_t(__cdecl* SeekLiveStream)(const struct AddonInstance_PVR*, int64_t, int);
+ int64_t(__cdecl* LengthLiveStream)(const struct AddonInstance_PVR*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Recording stream read interface functions
+ bool(__cdecl* OpenRecordedStream)(const struct AddonInstance_PVR*, const struct PVR_RECORDING*);
+ void(__cdecl* CloseRecordedStream)(const struct AddonInstance_PVR*);
+ int(__cdecl* ReadRecordedStream)(const struct AddonInstance_PVR*, unsigned char*, unsigned int);
+ int64_t(__cdecl* SeekRecordedStream)(const struct AddonInstance_PVR*, int64_t, int);
+ int64_t(__cdecl* LengthRecordedStream)(const struct AddonInstance_PVR*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // Stream demux interface functions
+ enum PVR_ERROR(__cdecl* GetStreamProperties)(const struct AddonInstance_PVR*,
+ struct PVR_STREAM_PROPERTIES*);
+ struct DEMUX_PACKET*(__cdecl* DemuxRead)(const struct AddonInstance_PVR*);
+ void(__cdecl* DemuxReset)(const struct AddonInstance_PVR*);
+ void(__cdecl* DemuxAbort)(const struct AddonInstance_PVR*);
+ void(__cdecl* DemuxFlush)(const struct AddonInstance_PVR*);
+ void(__cdecl* SetSpeed)(const struct AddonInstance_PVR*, int);
+ void(__cdecl* FillBuffer)(const struct AddonInstance_PVR*, bool);
+ bool(__cdecl* SeekTime)(const struct AddonInstance_PVR*, double, bool, double*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // General stream interface functions
+ bool(__cdecl* CanPauseStream)(const struct AddonInstance_PVR*);
+ void(__cdecl* PauseStream)(const struct AddonInstance_PVR*, bool);
+ bool(__cdecl* CanSeekStream)(const struct AddonInstance_PVR*);
+ bool(__cdecl* IsRealTimeStream)(const struct AddonInstance_PVR*);
+ enum PVR_ERROR(__cdecl* GetStreamTimes)(const struct AddonInstance_PVR*,
+ struct PVR_STREAM_TIMES*);
+ enum PVR_ERROR(__cdecl* GetStreamReadChunkSize)(const struct AddonInstance_PVR*, int*);
+
+ //--==----==----==----==----==----==----==----==----==----==----==----==----==
+ // New functions becomes added below and can be on another API change (where
+ // breaks min API version) moved up.
+ } KodiToAddonFuncTable_PVR;
+
+ typedef struct AddonInstance_PVR
+ {
+ struct AddonProperties_PVR* props;
+ struct AddonToKodiFuncTable_PVR* toKodi;
+ struct KodiToAddonFuncTable_PVR* toAddon;
+ } AddonInstance_PVR;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/CMakeLists.txt
new file mode 100644
index 0000000..c5ade1c
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/CMakeLists.txt
@@ -0,0 +1,20 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ pvr_channel_groups.h
+ pvr_channels.h
+ pvr_defines.h
+ pvr_edl.h
+ pvr_epg.h
+ pvr_general.h
+ pvr_menu_hook.h
+ pvr_providers.h
+ pvr_recordings.h
+ pvr_stream.h
+ pvr_timers.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_c-api_addon-instance_pvr)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channel_groups.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channel_groups.h
new file mode 100644
index 0000000..f8213fb
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channel_groups.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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_CHANNEL_GROUPS_H
+#define C_API_ADDONINSTANCE_PVR_CHANNEL_GROUPS_H
+
+#include "pvr_defines.h"
+
+#include <stdbool.h>
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" Definitions group 3 - PVR channel group
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ /*!
+ * @brief "C" PVR add-on channel group.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref kodi::addon::PVRChannelGroup for description of values.
+ */
+ typedef struct PVR_CHANNEL_GROUP
+ {
+ char strGroupName[PVR_ADDON_NAME_STRING_LENGTH];
+ bool bIsRadio;
+ unsigned int iPosition;
+ } PVR_CHANNEL_GROUP;
+
+ /*!
+ * @brief "C" PVR add-on channel group member.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref kodi::addon::PVRChannelGroupMember for description of values.
+ */
+ typedef struct PVR_CHANNEL_GROUP_MEMBER
+ {
+ char strGroupName[PVR_ADDON_NAME_STRING_LENGTH];
+ unsigned int iChannelUniqueId;
+ unsigned int iChannelNumber;
+ unsigned int iSubChannelNumber;
+ int iOrder;
+ } PVR_CHANNEL_GROUP_MEMBER;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_CHANNEL_GROUPS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h
new file mode 100644
index 0000000..134384f
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_CHANNELS_H
+#define C_API_ADDONINSTANCE_PVR_CHANNELS_H
+
+#include "pvr_defines.h"
+
+#include <stdbool.h>
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" Definitions group 2 - PVR channel
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Channel
+ /// @brief Denotes that no channel uid is available.
+ ///
+ /// Special @ref kodi::addon::PVRTimer::SetClientChannelUid() and
+ /// @ref kodi::addon::PVRRecording::SetChannelUid() value to indicate that no
+ /// channel uid is available.
+ #define PVR_CHANNEL_INVALID_UID -1
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief "C" PVR add-on channel.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref kodi::addon::PVRChannel for description of values.
+ */
+ typedef struct PVR_CHANNEL
+ {
+ unsigned int iUniqueId;
+ bool bIsRadio;
+ unsigned int iChannelNumber;
+ unsigned int iSubChannelNumber;
+ char strChannelName[PVR_ADDON_NAME_STRING_LENGTH];
+ char strMimeType[PVR_ADDON_INPUT_FORMAT_STRING_LENGTH];
+ unsigned int iEncryptionSystem;
+ char strIconPath[PVR_ADDON_URL_STRING_LENGTH];
+ bool bIsHidden;
+ bool bHasArchive;
+ int iOrder;
+ int iClientProviderUid;
+ } PVR_CHANNEL;
+
+ /*!
+ * @brief "C" PVR add-on signal status information.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref kodi::addon::PVRSignalStatus for description of values.
+ */
+ typedef struct PVR_SIGNAL_STATUS
+ {
+ char strAdapterName[PVR_ADDON_NAME_STRING_LENGTH];
+ char strAdapterStatus[PVR_ADDON_NAME_STRING_LENGTH];
+ char strServiceName[PVR_ADDON_NAME_STRING_LENGTH];
+ char strProviderName[PVR_ADDON_NAME_STRING_LENGTH];
+ char strMuxName[PVR_ADDON_NAME_STRING_LENGTH];
+ int iSNR;
+ int iSignal;
+ long iBER;
+ long iUNC;
+ } PVR_SIGNAL_STATUS;
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Channel_PVRDescrambleInfo
+ /// @brief Special @ref cpp_kodi_addon_pvr_Defs_Channel_PVRDescrambleInfo
+ /// value to indicate that a struct member's value is not available
+ ///
+ #define PVR_DESCRAMBLE_INFO_NOT_AVAILABLE -1
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief "C" PVR add-on descramble information.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref kodi::addon::PVRDescrambleInfo for description of values.
+ */
+ typedef struct PVR_DESCRAMBLE_INFO
+ {
+ int iPid;
+ int iCaid;
+ int iProvid;
+ int iEcmTime;
+ int iHops;
+ char strCardSystem[PVR_ADDON_DESCRAMBLE_INFO_STRING_LENGTH];
+ char strReader[PVR_ADDON_DESCRAMBLE_INFO_STRING_LENGTH];
+ char strFrom[PVR_ADDON_DESCRAMBLE_INFO_STRING_LENGTH];
+ char strProtocol[PVR_ADDON_DESCRAMBLE_INFO_STRING_LENGTH];
+ } PVR_DESCRAMBLE_INFO;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_CHANNELS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_defines.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_defines.h
new file mode 100644
index 0000000..5bd24dc
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_defines.h
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_DEFINES_H
+#define C_API_ADDONINSTANCE_PVR_DEFINES_H
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" Standard PVR definitions
+//
+// Values related to all parts and not used direct on addon, are to define here.
+//
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ /*!
+ * @brief API array sizes which are used for data exchange between
+ * Kodi and addon.
+ */
+ ///@{
+ #define PVR_ADDON_NAME_STRING_LENGTH 1024
+ #define PVR_ADDON_URL_STRING_LENGTH 1024
+ #define PVR_ADDON_DESC_STRING_LENGTH 1024
+ #define PVR_ADDON_INPUT_FORMAT_STRING_LENGTH 32
+ #define PVR_ADDON_EDL_LENGTH 64
+ #define PVR_ADDON_TIMERTYPE_ARRAY_SIZE 32
+ #define PVR_ADDON_TIMERTYPE_VALUES_ARRAY_SIZE 512
+ #define PVR_ADDON_TIMERTYPE_VALUES_ARRAY_SIZE_SMALL 128
+ #define PVR_ADDON_TIMERTYPE_STRING_LENGTH 128
+ #define PVR_ADDON_ATTRIBUTE_DESC_LENGTH 128
+ #define PVR_ADDON_ATTRIBUTE_VALUES_ARRAY_SIZE 512
+ #define PVR_ADDON_DESCRAMBLE_INFO_STRING_LENGTH 64
+ #define PVR_ADDON_DATE_STRING_LENGTH 32
+ #define PVR_ADDON_COUNTRIES_STRING_LENGTH 128
+ #define PVR_ADDON_LANGUAGES_STRING_LENGTH 128
+ ///@}
+
+ /*!
+ * @brief "C" Representation of a general attribute integer value.
+ */
+ typedef struct PVR_ATTRIBUTE_INT_VALUE
+ {
+ int iValue;
+ char strDescription[PVR_ADDON_ATTRIBUTE_DESC_LENGTH];
+ } PVR_ATTRIBUTE_INT_VALUE;
+
+ /*!
+ * @brief "C" Representation of a named value.
+ */
+ typedef struct PVR_NAMED_VALUE
+ {
+ char strName[PVR_ADDON_NAME_STRING_LENGTH];
+ char strValue[PVR_ADDON_NAME_STRING_LENGTH];
+ } PVR_NAMED_VALUE;
+
+ /*!
+ * @brief Handle used to return data from the PVR add-on to CPVRClient
+ */
+ struct PVR_HANDLE_STRUCT
+ {
+ void* callerAddress; /*!< address of the caller */
+ void* dataAddress; /*!< address to store data in */
+ int dataIdentifier; /*!< parameter to pass back when calling the callback */
+ };
+ typedef struct PVR_HANDLE_STRUCT* PVR_HANDLE;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_DEFINES_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_edl.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_edl.h
new file mode 100644
index 0000000..c74a113
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_edl.h
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_EDL_H
+#define C_API_ADDONINSTANCE_PVR_EDL_H
+
+#include "pvr_defines.h"
+
+#include <stdint.h>
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" Definitions group 8 - PVR Edit definition list (EDL)
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_EDLEntry_PVR_EDL_TYPE enum PVR_EDL_TYPE
+ /// @ingroup cpp_kodi_addon_pvr_Defs_EDLEntry
+ /// @brief **Edit definition list types**\n
+ /// Possible type values for @ref cpp_kodi_addon_pvr_Defs_EDLEntry_PVREDLEntry.
+ ///
+ ///@{
+ typedef enum PVR_EDL_TYPE
+ {
+ /// @brief __0__ : cut (completely remove content)
+ PVR_EDL_TYPE_CUT = 0,
+
+ /// @brief __1__ : mute audio
+ PVR_EDL_TYPE_MUTE = 1,
+
+ /// @brief __2__ : scene markers (chapter seeking)
+ PVR_EDL_TYPE_SCENE = 2,
+
+ /// @brief __3__ : commercial breaks
+ PVR_EDL_TYPE_COMBREAK = 3
+ } PVR_EDL_TYPE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief "C" Edit definition list entry.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref kodi::addon::PVREDLEntry for description of values.
+ */
+ typedef struct PVR_EDL_ENTRY
+ {
+ int64_t start;
+ int64_t end;
+ enum PVR_EDL_TYPE type;
+ } PVR_EDL_ENTRY;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_EDL_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h
new file mode 100644
index 0000000..9128b0d
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h
@@ -0,0 +1,657 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_EPG_H
+#define C_API_ADDONINSTANCE_PVR_EPG_H
+
+#include "pvr_defines.h"
+
+#include <time.h>
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" Definitions group 4 - PVR EPG
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT enum EPG_EVENT_CONTENTMASK (and sub types)
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg
+ /// @brief **EPG entry content event types.**\n
+ /// These ID's come from the DVB-SI EIT table "content descriptor"
+ /// Also known under the name "E-book genre assignments".
+ ///
+ /// See [ETSI EN 300 468 V1.14.1 (2014-05)](https://www.etsi.org/deliver/etsi_en/300400_300499/300468/01.14.01_60/en_300468v011401p.pdf)
+ /// about.
+ ///
+ /// Values used by this functions:
+ /// - @ref kodi::addon::PVREPGTag::SetGenreType()
+ /// - @ref kodi::addon::PVREPGTag::SetGenreSubType()
+ /// - @ref kodi::addon::PVRRecording::SetGenreType()
+ /// - @ref kodi::addon::PVRRecording::SetGenreSubType()
+ ///
+ /// Following types are listed here:
+ /// | emum Type | Description
+ /// |-----------|--------------------
+ /// | @ref EPG_EVENT_CONTENTMASK | EPG entry main content to use.
+ /// | @ref EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA | EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_MOVIEDRAMA event types for sub type of <b>"Movie/Drama"</b>.
+ /// | @ref EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS | EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS event types for sub type of <b>"News/Current affairs"</b>.
+ /// | @ref EPG_EVENT_CONTENTSUBMASK_SHOW | EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_SHOW event types for sub type of <b>"Show/Game show"</b>.
+ /// | @ref EPG_EVENT_CONTENTSUBMASK_SPORTS | @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_SPORTS event types for sub type of <b>"Sports"</b>.
+ /// | @ref EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH | EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_CHILDRENYOUTH event types for sub type of <b>"Children's/Youth programmes"</b>.
+ /// | @ref EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE | EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE event types for sub type of <b>"Music/Ballet/Dance"</b>.
+ /// | @ref EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE | EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_ARTSCULTURE event types for sub type of <b>"Arts/Culture (without music)"</b>.
+ /// | @ref EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS | EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS event types for sub type of <b>"Social/Political issues/Economics"</b>.
+ /// | @ref EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE | EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE event types for sub type of <b>"Education/Science/Factual topics"</b>.
+ /// | @ref EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES | EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_LEISUREHOBBIES event types for sub type of <b>"Leisure hobbies"</b>.
+ /// | @ref EPG_EVENT_CONTENTSUBMASK_SPECIAL | EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_SPECIAL event types for sub type of <b>"Special characteristics"</b>.
+ ///@{
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry main content to use.
+ ///
+ ///@{
+ typedef enum EPG_EVENT_CONTENTMASK
+ {
+ /// @brief __0x00__ : Undefined content mask entry.
+ EPG_EVENT_CONTENTMASK_UNDEFINED = 0x00,
+
+ /// @brief __0x10__ : Movie/Drama.\n
+ /// \n
+ /// See @ref EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA about related sub types.
+ EPG_EVENT_CONTENTMASK_MOVIEDRAMA = 0x10,
+
+ /// @brief __0x20__ : News/Current affairs.\n
+ /// \n
+ /// See @ref EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS about related sub types.
+ EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS = 0x20,
+
+ /// @brief __0x30__ : Show/Game show.\n
+ /// \n
+ /// See @ref EPG_EVENT_CONTENTSUBMASK_SHOW about related sub types.
+ EPG_EVENT_CONTENTMASK_SHOW = 0x30,
+
+ /// @brief __0x40__ : Sports.\n
+ /// \n
+ /// See @ref EPG_EVENT_CONTENTSUBMASK_SPORTS about related sub types.
+ EPG_EVENT_CONTENTMASK_SPORTS = 0x40,
+
+ /// @brief __0x50__ : Children's/Youth programmes.\n
+ /// \n
+ /// See @ref EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH about related sub types.
+ EPG_EVENT_CONTENTMASK_CHILDRENYOUTH = 0x50,
+
+ /// @brief __0x60__ : Music/Ballet/Dance.\n
+ /// \n
+ /// See @ref EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE about related sub types.
+ EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE = 0x60,
+
+ /// @brief __0x70__ : Arts/Culture (without music).\n
+ /// \n
+ /// See @ref EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE about related sub types.
+ EPG_EVENT_CONTENTMASK_ARTSCULTURE = 0x70,
+
+ /// @brief __0x80__ : Social/Political issues/Economics.\n
+ /// \n
+ /// See @ref EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS about related sub types.
+ EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS = 0x80,
+
+ /// @brief __0x90__ : Education/Science/Factual topics.\n
+ /// \n
+ /// See @ref EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE about related sub types.
+ EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE = 0x90,
+
+ /// @brief __0xA0__ : Leisure hobbies.\n
+ /// \n
+ /// See @ref EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES about related sub types.
+ EPG_EVENT_CONTENTMASK_LEISUREHOBBIES = 0xA0,
+
+ /// @brief __0xB0__ : Special characteristics.\n
+ /// \n
+ /// See @ref EPG_EVENT_CONTENTSUBMASK_SPECIAL about related sub types.
+ EPG_EVENT_CONTENTMASK_SPECIAL = 0xB0,
+
+ /// @brief __0xF0__ User defined.
+ EPG_EVENT_CONTENTMASK_USERDEFINED = 0xF0,
+
+ /// @brief Used to override standard genre types with a own name about.\n
+ /// \n
+ /// Set to this value @ref EPG_GENRE_USE_STRING on following places:
+ /// - @ref kodi::addon::PVREPGTag::SetGenreType()
+ /// - @ref kodi::addon::PVREPGTag::SetGenreSubType()
+ /// - @ref kodi::addon::PVRRecording::SetGenreType()
+ /// - @ref kodi::addon::PVRRecording::SetGenreSubType()
+ ///
+ /// @warning Value here is not a [ETSI EN 300 468 V1.14.1 (2014-05)](https://www.etsi.org/deliver/etsi_en/300400_300499/300468/01.14.01_60/en_300468v011401p.pdf)
+ /// conform.
+ ///
+ /// @note This is a own Kodi definition to set that genre is given by own
+ /// string. Used on @ref kodi::addon::PVREPGTag::SetGenreDescription() and
+ /// @ref kodi::addon::PVRRecording::SetGenreDescription()
+ EPG_GENRE_USE_STRING = 0x100
+ } EPG_EVENT_CONTENTMASK;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_MOVIEDRAMA event
+ /// types for sub type of <b>"Movie/Drama"</b>.
+ ///
+ ///@{
+ typedef enum EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA
+ {
+ /// @brief __0x0__ : Movie/drama (general).
+ EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA_GENERAL = 0x0,
+
+ /// @brief __0x1__ : Detective/thriller.
+ EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA_DETECTIVE_THRILLER = 0x1,
+
+ /// @brief __0x2__ : Adventure/western/war.
+ EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA_ADVENTURE_WESTERN_WAR = 0x2,
+
+ /// @brief __0x3__ : Science fiction/fantasy/horror.
+ EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA_SCIENCEFICTION_FANTASY_HORROR = 0x3,
+
+ /// @brief __0x4__ : Comedy.
+ EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA_COMEDY = 0x4,
+
+ /// @brief __0x5__ : Soap/melodrama/folkloric.
+ EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA_SOAP_MELODRAMA_FOLKLORIC = 0x5,
+
+ /// @brief __0x6__ : Romance.
+ EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA_ROMANCE = 0x6,
+
+ /// @brief __0x7__ : Serious/classical/religious/historical movie/drama.
+ EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA_SERIOUS_CLASSICAL_RELIGIOUS_HISTORICAL = 0x7,
+
+ /// @brief __0x8__ : Adult movie/drama.
+ EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA_ADULT = 0x8,
+
+ /// @brief __0xF__ : User defined.
+ EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA_USERDEFINED = 0xF
+ } EPG_EVENT_CONTENTSUBMASK_MOVIEDRAMA;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS event
+ /// types for sub type of <b>"News/Current affairs"</b>.
+ ///
+ typedef enum EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS
+ {
+ /// @brief __0x0__ : News/current affairs (general).
+ EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS_GENERAL = 0x0,
+
+ /// @brief __0x1__ : News/weather report.
+ EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS_WEATHER = 0x1,
+
+ /// @brief __0x2__ : News magazine.
+ EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS_MAGAZINE = 0x2,
+
+ /// @brief __0x3__ : Documentary.
+ EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS_DOCUMENTARY = 0x3,
+
+ /// @brief __0x4__ : Discussion/interview/debate
+ EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS_DISCUSSION_INTERVIEW_DEBATE = 0x4,
+
+ /// @brief __0xF__ : User defined.
+ EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS_USERDEFINED = 0xF
+ } EPG_EVENT_CONTENTSUBMASK_NEWSCURRENTAFFAIRS;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_SHOW event
+ /// types for sub type of <b>"Show/Game show"</b>.
+ ///
+ typedef enum EPG_EVENT_CONTENTSUBMASK_SHOW
+ {
+ /// @brief __0x0__ : Show/game show (general).
+ EPG_EVENT_CONTENTSUBMASK_SHOW_GENERAL = 0x0,
+
+ /// @brief __0x1__ : Game show/quiz/contest.
+ EPG_EVENT_CONTENTSUBMASK_SHOW_GAMESHOW_QUIZ_CONTEST = 0x1,
+
+ /// @brief __0x2__ : Variety show.
+ EPG_EVENT_CONTENTSUBMASK_SHOW_VARIETY_SHOW = 0x2,
+
+ /// @brief __0x3__ : Talk show.
+ EPG_EVENT_CONTENTSUBMASK_SHOW_TALK_SHOW = 0x3,
+
+ /// @brief __0xF__ : User defined.
+ EPG_EVENT_CONTENTSUBMASK_SHOW_USERDEFINED = 0xF
+ } EPG_EVENT_CONTENTSUBMASK_SHOW;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_SPORTS event
+ /// types for sub type of <b>"Sports"</b>.
+ ///
+ typedef enum EPG_EVENT_CONTENTSUBMASK_SPORTS
+ {
+ /// @brief __0x0__ : Sports (general).
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_GENERAL = 0x0,
+
+ /// @brief __0x1__ : Special events (Olympic Games, World Cup, etc.).
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_OLYMPICGAMES_WORLDCUP = 0x1,
+
+ /// @brief __0x2__ : Sports magazines.
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_SPORTS_MAGAZINES = 0x2,
+
+ /// @brief __0x3__ : Football/soccer.
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_FOOTBALL_SOCCER = 0x3,
+
+ /// @brief __0x4__ : Tennis/squash.
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_TENNIS_SQUASH = 0x4,
+
+ /// @brief __0x5__ : Team sports (excluding football).
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_TEAMSPORTS = 0x5,
+
+ /// @brief __0x6__ : Athletics.
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_ATHLETICS = 0x6,
+
+ /// @brief __0x7__ : Motor sport.
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_MOTORSPORT = 0x7,
+
+ /// @brief __0x8__ : Water sport.
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_WATERSPORT = 0x8,
+
+ /// @brief __0x9__ : Winter sports.
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_WINTERSPORTS = 0x9,
+
+ /// @brief __0xA__ : Equestrian.
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_EQUESTRIAN = 0xA,
+
+ /// @brief __0xB__ : Martial sports.
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_MARTIALSPORTS = 0xB,
+
+ /// @brief __0xF__ : User defined.
+ EPG_EVENT_CONTENTSUBMASK_SPORTS_USERDEFINED = 0xF
+ } EPG_EVENT_CONTENTSUBMASK_SPORTS;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_CHILDRENYOUTH event
+ /// types for sub type of <b>"Children's/Youth programmes"</b>.
+ ///
+ typedef enum EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH
+ {
+ /// @brief __0x0__ : Children's/youth programmes (general).
+ EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH_GENERAL = 0x0,
+
+ /// @brief __0x1__ : Pre-school children's programmes.
+ EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH_PRESCHOOL_CHILDREN = 0x1,
+
+ /// @brief __0x2__ : Entertainment programmes for 6 to 14.
+ EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH_ENTERTAIN_6TO14 = 0x2,
+
+ /// @brief __0x3__ : Entertainment programmes for 10 to 16.
+ EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH_ENTERTAIN_10TO16 = 0x3,
+
+ /// @brief __0x4__ : Informational/educational/school programmes.
+ EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH_INFORMATIONAL_EDUCATIONAL_SCHOOL = 0x4,
+
+ /// @brief __0x5__ : Cartoons/puppets.
+ EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH_CARTOONS_PUPPETS = 0x5,
+
+ /// @brief __0xF__ : User defined.
+ EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH_USERDEFINED = 0xF
+ } EPG_EVENT_CONTENTSUBMASK_CHILDRENYOUTH;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE event
+ /// types for sub type of <b>"Music/Ballet/Dance"</b>.
+ ///
+ typedef enum EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE
+ {
+ /// @brief __0x0__ : Music/ballet/dance (general).
+ EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE_GENERAL = 0x0,
+
+ /// @brief __0x1__ : Rock/pop.
+ EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE_ROCKPOP = 0x1,
+
+ /// @brief __0x2__ : Serious music/classical music.
+ EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE_SERIOUSMUSIC_CLASSICALMUSIC = 0x2,
+
+ /// @brief __0x3__ : Folk/traditional music.
+ EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE_FOLK_TRADITIONAL_MUSIC = 0x3,
+
+ /// @brief __0x4__ : Jazz.
+ EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE_JAZZ = 0x4,
+
+ /// @brief __0x5__ : Musical/opera.
+ EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE_MUSICAL_OPERA = 0x5,
+
+ /// @brief __0x6__ : Ballet.
+ EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE_BALLET = 0x6,
+
+ /// @brief __0xF__ : User defined.
+ EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE_USERDEFINED = 0xF
+ } EPG_EVENT_CONTENTSUBMASK_MUSICBALLETDANCE;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_ARTSCULTURE event
+ /// types for sub type of <b>"Arts/Culture (without music)"</b>.
+ ///
+ typedef enum EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE
+ {
+ /// @brief __0x0__ : Arts/culture (without music, general).
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_GENERAL = 0x0,
+
+ /// @brief __0x1__ : Performing arts.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_PERFORMINGARTS = 0x1,
+
+ /// @brief __0x2__ : Fine arts.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_FINEARTS = 0x2,
+
+ /// @brief __0x3__ : Religion.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_RELIGION = 0x3,
+
+ /// @brief __0x4__ : Popular culture/traditional arts.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_POPULARCULTURE_TRADITIONALARTS = 0x4,
+
+ /// @brief __0x5__ : Literature.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_LITERATURE = 0x5,
+
+ /// @brief __0x6__ : Film/cinema.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_FILM_CINEMA = 0x6,
+
+ /// @brief __0x7__ : Experimental film/video.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_EXPERIMENTALFILM_VIDEO = 0x7,
+
+ /// @brief __0x8__ : Broadcasting/press.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_BROADCASTING_PRESS = 0x8,
+
+ /// @brief __0x9__ : New media.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_NEWMEDIA = 0x9,
+
+ /// @brief __0xA__ : Arts/culture magazines.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_ARTS_CULTUREMAGAZINES = 0xA,
+
+ /// @brief __0xB__ : Fashion.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_FASHION = 0xB,
+
+ /// @brief __0xF__ : User defined.
+ EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE_USERDEFINED = 0xF
+ } EPG_EVENT_CONTENTSUBMASK_ARTSCULTURE;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS event
+ /// types for sub type of <b>"Social/Political issues/Economics"</b>.
+ ///
+ typedef enum EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS
+ {
+ /// @brief __0x0__ : Social/political issues/economics (general).
+ EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS_GENERAL = 0x0,
+
+ /// @brief __0x1__ : Magazines/reports/documentary.
+ EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS_MAGAZINES_REPORTS_DOCUMENTARY = 0x1,
+
+ /// @brief __0x2__ : Economics/social advisory.
+ EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS_ECONOMICS_SOCIALADVISORY = 0x2,
+
+ /// @brief __0x3__ : Remarkable people.
+ EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS_REMARKABLEPEOPLE = 0x3,
+
+ /// @brief __0xF__ : User defined.
+ EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS_USERDEFINED = 0xF
+ } EPG_EVENT_CONTENTSUBMASK_SOCIALPOLITICALECONOMICS;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE event
+ /// types for sub type of <b>"Education/Science/Factual topics"</b>.
+ ///
+ typedef enum EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE
+ {
+ /// @brief __0x0__ : Education/science/factual topics (general).
+ EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE_GENERAL = 0x0,
+
+ /// @brief __0x1__ : Nature/animals/environment.
+ EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE_NATURE_ANIMALS_ENVIRONMENT = 0x1,
+
+ /// @brief __0x2__ : Technology/natural sciences.
+ EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE_TECHNOLOGY_NATURALSCIENCES = 0x2,
+
+ /// @brief __0x3__ : Medicine/physiology/psychology.
+ EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE_MEDICINE_PHYSIOLOGY_PSYCHOLOGY = 0x3,
+
+ /// @brief __0x4__ : Foreign countries/expeditions.
+ EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE_FOREIGNCOUNTRIES_EXPEDITIONS = 0x4,
+
+ /// @brief __0x5__ : Social/spiritual sciences.
+ EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE_SOCIAL_SPIRITUALSCIENCES = 0x5,
+
+ /// @brief __0x6__ : Further education.
+ EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE_FURTHEREDUCATION = 0x6,
+
+ /// @brief __0x7__ : Languages.
+ EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE_LANGUAGES = 0x7,
+
+ /// @brief __0xF__ : User defined.
+ EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE_USERDEFINED = 0xF
+ } EPG_EVENT_CONTENTSUBMASK_EDUCATIONALSCIENCE;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_LEISUREHOBBIES event
+ /// types for sub type of <b>"Leisure hobbies"</b>.
+ ///
+ typedef enum EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES
+ {
+ /// @brief __0x0__ : Leisure hobbies (general) .
+ EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES_GENERAL = 0x0,
+
+ /// @brief __0x1__ : Tourism/travel.
+ EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES_TOURISM_TRAVEL = 0x1,
+
+ /// @brief __0x2__ : Handicraft.
+ EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES_HANDICRAFT = 0x2,
+
+ /// @brief __0x3__ : Motoring.
+ EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES_MOTORING = 0x3,
+
+ /// @brief __0x4__ : Fitness and health.
+ EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES_FITNESSANDHEALTH = 0x4,
+
+ /// @brief __0x5__ : Cooking.
+ EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES_COOKING = 0x5,
+
+ /// @brief __0x6__ : Advertisement/shopping.
+ EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES_ADVERTISEMENT_SHOPPING = 0x6,
+
+ /// @brief __0x7__ : Gardening.
+ EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES_GARDENING = 0x7,
+
+ /// @brief __0xF__ : User defined.
+ EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES_USERDEFINED = 0xF
+ } EPG_EVENT_CONTENTSUBMASK_LEISUREHOBBIES;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT
+ /// @brief EPG entry sub content to @ref EPG_EVENT_CONTENTMASK_SPECIAL event
+ /// types for sub type of <b>"Special characteristics"</b>.
+ ///
+ typedef enum EPG_EVENT_CONTENTSUBMASK_SPECIAL
+ {
+ /// @brief __0x0__ : Special characteristics / Original language (general).
+ EPG_EVENT_CONTENTSUBMASK_SPECIAL_GENERAL = 0x0,
+
+ /// @brief __0x1__ : Black and white.
+ EPG_EVENT_CONTENTSUBMASK_SPECIAL_BLACKANDWHITE = 0x1,
+
+ /// @brief __0x2__ : Unpublished.
+ EPG_EVENT_CONTENTSUBMASK_SPECIAL_UNPUBLISHED = 0x2,
+
+ /// @brief __0x3__ : Live broadcast.
+ EPG_EVENT_CONTENTSUBMASK_SPECIAL_LIVEBROADCAST = 0x3,
+
+ /// @brief __0x4__ : Plano-stereoscopic.
+ EPG_EVENT_CONTENTSUBMASK_SPECIAL_PLANOSTEREOSCOPIC = 0x4,
+
+ /// @brief __0x5__ : Local or regional.
+ EPG_EVENT_CONTENTSUBMASK_SPECIAL_LOCALORREGIONAL = 0x5,
+
+ /// @brief __0xF__ : User defined.
+ EPG_EVENT_CONTENTSUBMASK_SPECIAL_USERDEFINED = 0xF
+ } EPG_EVENT_CONTENTSUBMASK_SPECIAL;
+ //----------------------------------------------------------------------------
+
+ ///@}
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg
+ /// @brief Separator to use in strings containing different tokens, for example
+ /// writers, directors, actors of an event.
+ ///
+ #define EPG_STRING_TOKEN_SEPARATOR ","
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_epg_EPG_TAG_FLAG enum EPG_TAG_FLAG
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg
+ /// @brief <b>Bit field of independent flags associated with the EPG entry.</b>\n
+ /// Values used by @ref kodi::addon::PVREPGTag::SetFlags().
+ ///
+ /// Here's example about the use of this:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVREPGTag tag;
+ /// tag.SetFlags(EPG_TAG_FLAG_IS_SERIES | EPG_TAG_FLAG_IS_NEW);
+ /// ~~~~~~~~~~~~~
+ ///
+ ///@{
+ typedef enum EPG_TAG_FLAG
+ {
+ /// @brief __0000 0000__ : Nothing special to say about this entry.
+ EPG_TAG_FLAG_UNDEFINED = 0,
+
+ /// @brief __0000 0001__ : This EPG entry is part of a series.
+ EPG_TAG_FLAG_IS_SERIES = (1 << 0),
+
+ /// @brief __0000 0010__ : This EPG entry will be flagged as new.
+ EPG_TAG_FLAG_IS_NEW = (1 << 1),
+
+ /// @brief __0000 0100__ : This EPG entry will be flagged as a premiere.
+ EPG_TAG_FLAG_IS_PREMIERE = (1 << 2),
+
+ /// @brief __0000 1000__ : This EPG entry will be flagged as a finale.
+ EPG_TAG_FLAG_IS_FINALE = (1 << 3),
+
+ /// @brief __0001 0000__ : This EPG entry will be flagged as live.
+ EPG_TAG_FLAG_IS_LIVE = (1 << 4),
+ } EPG_TAG_FLAG;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg
+ /// @brief Special PVREPGTag::SetUniqueBroadcastId value
+ ///
+ /// Special @ref kodi::addon::PVREPGTag::SetUniqueBroadcastId() value to
+ /// indicate that a tag has not a valid EPG event uid.
+ ///
+ #define EPG_TAG_INVALID_UID 0
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg
+ /// @brief Special @ref kodi::addon::PVREPGTag::SetSeriesNumber(), @ref kodi::addon::PVREPGTag::SetEpisodeNumber()
+ /// and @ref kodi::addon::PVREPGTag::SetEpisodePartNumber() value to indicate
+ /// it is not to be used.
+ ///
+ #define EPG_TAG_INVALID_SERIES_EPISODE -1
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg
+ /// @brief Timeframe value for use with @ref kodi::addon::CInstancePVRClient::SetEPGTimeFrame()
+ /// function to indicate "no timeframe".
+ ///
+ #define EPG_TIMEFRAME_UNLIMITED -1
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_epg_EPG_EVENT_STATE enum EPG_EVENT_STATE
+ /// @ingroup cpp_kodi_addon_pvr_Defs_epg
+ /// @brief **EPG event states.**\n
+ /// Used with @ref kodi::addon::CInstancePVRClient::EpgEventStateChange()
+ /// callback.
+ ///
+ ///@{
+ typedef enum EPG_EVENT_STATE
+ {
+ /// @brief __0__ : Event created.
+ EPG_EVENT_CREATED = 0,
+
+ /// @brief __1__ : Event updated.
+ EPG_EVENT_UPDATED = 1,
+
+ /// @brief __2__ : Event deleted.
+ EPG_EVENT_DELETED = 2,
+ } EPG_EVENT_STATE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief "C" PVR add-on channel group member.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref kodi::addon::PVREPGTag for description of values.
+ */
+ typedef struct EPG_TAG
+ {
+ unsigned int iUniqueBroadcastId;
+ unsigned int iUniqueChannelId;
+ const char* strTitle;
+ time_t startTime;
+ time_t endTime;
+ const char* strPlotOutline;
+ const char* strPlot;
+ const char* strOriginalTitle;
+ const char* strCast;
+ const char* strDirector;
+ const char* strWriter;
+ int iYear;
+ const char* strIMDBNumber;
+ const char* strIconPath;
+ int iGenreType;
+ int iGenreSubType;
+ const char* strGenreDescription;
+ const char* strFirstAired;
+ int iParentalRating;
+ const char* strParentalRatingCode;
+ int iStarRating;
+ int iSeriesNumber;
+ int iEpisodeNumber;
+ int iEpisodePartNumber;
+ const char* strEpisodeName;
+ unsigned int iFlags;
+ const char* strSeriesLink;
+ } EPG_TAG;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_EPG_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h
new file mode 100644
index 0000000..32413cb
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h
@@ -0,0 +1,300 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_GENERAL_H
+#define C_API_ADDONINSTANCE_PVR_GENERAL_H
+
+#include "../inputstream/stream_constants.h"
+#include "pvr_defines.h"
+
+#include <stdbool.h>
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" Definitions group 1 - General PVR
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_General_PVR_ERROR enum PVR_ERROR
+ /// @ingroup cpp_kodi_addon_pvr_Defs_General
+ /// @brief **PVR add-on error codes**\n
+ /// Used as return values on most PVR related functions.
+ ///
+ /// In this way, a PVR instance signals errors in its processing and, under
+ /// certain conditions, allows Kodi to make corrections.
+ ///
+ ///@{
+ typedef enum PVR_ERROR
+ {
+ /// @brief __0__ : No error occurred.
+ PVR_ERROR_NO_ERROR = 0,
+
+ /// @brief __-1__ : An unknown error occurred.
+ PVR_ERROR_UNKNOWN = -1,
+
+ /// @brief __-2__ : The method that Kodi called is not implemented by the add-on.
+ PVR_ERROR_NOT_IMPLEMENTED = -2,
+
+ /// @brief __-3__ : The backend reported an error, or the add-on isn't connected.
+ PVR_ERROR_SERVER_ERROR = -3,
+
+ /// @brief __-4__ : The command was sent to the backend, but the response timed out.
+ PVR_ERROR_SERVER_TIMEOUT = -4,
+
+ /// @brief __-5__ : The command was rejected by the backend.
+ PVR_ERROR_REJECTED = -5,
+
+ /// @brief __-6__ : The requested item can not be added, because it's already present.
+ PVR_ERROR_ALREADY_PRESENT = -6,
+
+ /// @brief __-7__ : The parameters of the method that was called are invalid for this
+ /// operation.
+ PVR_ERROR_INVALID_PARAMETERS = -7,
+
+ /// @brief __-8__ : A recording is running, so the timer can't be deleted without
+ /// doing a forced delete.
+ PVR_ERROR_RECORDING_RUNNING = -8,
+
+ /// @brief __-9__ : The command failed.
+ PVR_ERROR_FAILED = -9,
+ } PVR_ERROR;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_General_PVR_CONNECTION_STATE enum PVR_CONNECTION_STATE
+ /// @ingroup cpp_kodi_addon_pvr_Defs_General
+ /// @brief **PVR backend connection states**\n
+ /// Used with @ref kodi::addon::CInstancePVRClient::ConnectionStateChange() callback.
+ ///
+ /// With this, a PVR instance signals that Kodi should perform special
+ /// operations.
+ ///
+ ///@{
+ typedef enum PVR_CONNECTION_STATE
+ {
+ /// @brief __0__ : Unknown state (e.g. not yet tried to connect).
+ PVR_CONNECTION_STATE_UNKNOWN = 0,
+
+ /// @brief __1__ : Backend server is not reachable (e.g. server not existing or
+ /// network down).
+ PVR_CONNECTION_STATE_SERVER_UNREACHABLE = 1,
+
+ /// @brief __2__ : Backend server is reachable, but there is not the expected type of
+ /// server running (e.g. HTSP required, but FTP running at given server:port).
+ PVR_CONNECTION_STATE_SERVER_MISMATCH = 2,
+
+ /// @brief __3__ : Backend server is reachable, but server version does not match
+ /// client requirements.
+ PVR_CONNECTION_STATE_VERSION_MISMATCH = 3,
+
+ /// @brief __4__ : Backend server is reachable, but denies client access (e.g. due
+ /// to wrong credentials).
+ PVR_CONNECTION_STATE_ACCESS_DENIED = 4,
+
+ /// @brief __5__ : Connection to backend server is established.
+ PVR_CONNECTION_STATE_CONNECTED = 5,
+
+ /// @brief __6__ : No connection to backend server (e.g. due to network errors or
+ /// client initiated disconnect).
+ PVR_CONNECTION_STATE_DISCONNECTED = 6,
+
+ /// @brief __7__ : Connecting to backend.
+ PVR_CONNECTION_STATE_CONNECTING = 7,
+ } PVR_CONNECTION_STATE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_General_PVR_STREAM_PROPERTY definition PVR_STREAM_PROPERTY
+ /// @ingroup cpp_kodi_addon_pvr_Defs_General_Inputstream
+ /// @brief **PVR related stream property values**\n
+ /// This is used to pass additional data to Kodi on a given PVR stream.
+ ///
+ /// Then transferred to livestream, recordings or EPG Tag stream using the
+ /// properties.
+ ///
+ /// This defines are used by:
+ /// - @ref kodi::addon::CInstancePVRClient::GetChannelStreamProperties()
+ /// - @ref kodi::addon::CInstancePVRClient::GetEPGTagStreamProperties()
+ /// - @ref kodi::addon::CInstancePVRClient::GetRecordingStreamProperties()
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ ///
+ /// PVR_ERROR CMyPVRInstance::GetChannelStreamProperties(const kodi::addon::PVRChannel& channel,
+ /// std::vector<PVRStreamProperty>& properties)
+ /// {
+ /// ...
+ /// properties.emplace_back(PVR_STREAM_PROPERTY_INPUTSTREAM, "inputstream.adaptive");
+ /// properties.emplace_back("inputstream.adaptive.manifest_type", "mpd");
+ /// properties.emplace_back("inputstream.adaptive.manifest_update_parameter", "full");
+ /// properties.emplace_back(PVR_STREAM_PROPERTY_MIMETYPE, "application/xml+dash");
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ ///
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ ///@{
+
+ /// @brief the URL of the stream that should be played.
+ ///
+ #define PVR_STREAM_PROPERTY_STREAMURL "streamurl"
+
+ /// @brief To define in stream properties the name of the inputstream add-on
+ /// that should be used.
+ ///
+ /// Leave blank to use Kodi's built-in playing capabilities or to allow ffmpeg
+ /// to handle directly set to @ref PVR_STREAM_PROPERTY_VALUE_INPUTSTREAMFFMPEG.
+ ///
+ #define PVR_STREAM_PROPERTY_INPUTSTREAM STREAM_PROPERTY_INPUTSTREAM
+
+ /// @brief To define in stream properties the player the inputstream add-on
+ /// should use.
+ ///
+ /// Leave blank to use Kodi's built-in player selection mechanism.
+ /// Permitted values are:
+ /// - "videodefaultplayer"
+ /// - "audiodefaultplayer"
+ ///
+ #define PVR_STREAM_PROPERTY_INPUTSTREAM_PLAYER STREAM_PROPERTY_INPUTSTREAM_PLAYER
+
+ /// @brief Identification string for an input stream.
+ ///
+ /// This value can be used in addition to @ref PVR_STREAM_PROPERTY_INPUTSTREAM.
+ /// It is used to provide the respective inpustream addon with additional
+ /// identification.
+ ///
+ /// The difference between this and other stream properties is that it is also
+ /// passed in the associated @ref kodi::addon::CAddonBase::CreateInstance()
+ /// call.
+ ///
+ /// This makes it possible to select different processing classes within the
+ /// associated add-on.
+ ///
+ ///
+ ///---------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ ///
+ /// // On PVR instance of addon
+ /// PVR_ERROR CMyPVRInstance::GetChannelStreamProperties(const kodi::addon::PVRChannel& channel,
+ /// std::vector<PVRStreamProperty>& properties)
+ /// {
+ /// ...
+ /// // For here on example the inpustream is also inside the PVR addon
+ /// properties.emplace_back(PVR_STREAM_PROPERTY_INPUTSTREAM, "pvr.my_one");
+ /// properties.emplace_back(PVR_STREAM_PROPERTY_INPUTSTREAM_INSTANCE_ID, "my_special_id_1");
+ /// return PVR_ERROR_NO_ERROR;
+ /// }
+ ///
+ /// ...
+ ///
+ /// // On CAddonBase part of addon
+ /// ADDON_STATUS CMyAddon::CreateInstanceEx(int instanceType,
+ /// std::string instanceID,
+ /// KODI_HANDLE instance,
+ /// KODI_HANDLE& addonInstance
+ /// const std::string& version)
+ /// {
+ /// if (instanceType == ADDON_INSTANCE_INPUTSTREAM)
+ /// {
+ /// kodi::Log(ADDON_LOG_INFO, "Creating my special inputstream");
+ /// if (instanceID == "my_special_id_1")
+ /// addonInstance = new CMyPVRClientInstance_Type1(instance, version);
+ /// else if (instanceID == "my_special_id_2")
+ /// addonInstance = new CMyPVRClientInstance_Type2(instance, version);
+ /// return ADDON_STATUS_OK;
+ /// }
+ /// else if (...)
+ /// {
+ /// ...
+ /// }
+ /// return ADDON_STATUS_UNKNOWN;
+ /// }
+ ///
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ #define PVR_STREAM_PROPERTY_INPUTSTREAM_INSTANCE_ID STREAM_PROPERTY_INPUTSTREAM_INSTANCE_ID
+
+ /// @brief the MIME type of the stream that should be played.
+ ///
+ #define PVR_STREAM_PROPERTY_MIMETYPE "mimetype"
+
+ /// @brief <b>"true"</b> to denote that the stream that should be played is a
+ /// realtime stream.
+ ///
+ /// Any other value indicates that this is no realtime stream.
+ ///
+ #define PVR_STREAM_PROPERTY_ISREALTIMESTREAM STREAM_PROPERTY_ISREALTIMESTREAM
+
+ /// @brief <b>"true"</b> to denote that if the stream is from an EPG tag.
+ ///
+ /// It should be played is a live stream. Otherwise if it's a EPG tag it will
+ /// play as normal video.
+ ///
+ #define PVR_STREAM_PROPERTY_EPGPLAYBACKASLIVE "epgplaybackaslive"
+
+ /// @brief Special value for @ref PVR_STREAM_PROPERTY_INPUTSTREAM to use
+ /// ffmpeg to directly play a stream URL.
+ #define PVR_STREAM_PROPERTY_VALUE_INPUTSTREAMFFMPEG STREAM_PROPERTY_VALUE_INPUTSTREAMFFMPEG
+
+ ///@}
+ //-----------------------------------------------------------------------------
+
+ /*!
+ * @brief "C" PVR add-on capabilities.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref kodi::addon::PVRCapabilities for description of values.
+ */
+ typedef struct PVR_ADDON_CAPABILITIES
+ {
+ bool bSupportsEPG;
+ bool bSupportsEPGEdl;
+ bool bSupportsTV;
+ bool bSupportsRadio;
+ bool bSupportsRecordings;
+ bool bSupportsRecordingsUndelete;
+ bool bSupportsTimers;
+ bool bSupportsChannelGroups;
+ bool bSupportsChannelScan;
+ bool bSupportsChannelSettings;
+ bool bHandlesInputStream;
+ bool bHandlesDemuxing;
+ bool bSupportsRecordingPlayCount;
+ bool bSupportsLastPlayedPosition;
+ bool bSupportsRecordingEdl;
+ bool bSupportsRecordingsRename;
+ bool bSupportsRecordingsLifetimeChange;
+ bool bSupportsDescrambleInfo;
+ bool bSupportsAsyncEPGTransfer;
+ bool bSupportsRecordingSize;
+ bool bSupportsProviders;
+ bool bSupportsRecordingsDelete;
+
+ unsigned int iRecordingsLifetimesSize;
+ struct PVR_ATTRIBUTE_INT_VALUE recordingsLifetimeValues[PVR_ADDON_ATTRIBUTE_VALUES_ARRAY_SIZE];
+ } PVR_ADDON_CAPABILITIES;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_GENERAL_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_menu_hook.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_menu_hook.h
new file mode 100644
index 0000000..e204311
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_menu_hook.h
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_MENUHOOK_H
+#define C_API_ADDONINSTANCE_PVR_MENUHOOK_H
+
+#include "pvr_defines.h"
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" Definitions group 7 - Menu hook
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Menuhook_PVR_MENUHOOK_CAT enum PVR_MENUHOOK_CAT
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Menuhook
+ /// @brief **PVR context menu hook categories**\n
+ /// Possible menu types given to Kodi with @ref kodi::addon::CInstancePVRClient::AddMenuHook().
+ ///
+ ///@{
+ typedef enum PVR_MENUHOOK_CAT
+ {
+ /// @brief __-1__ : Unknown menu hook.
+ PVR_MENUHOOK_UNKNOWN = -1,
+
+ /// @brief __0__ : All categories.
+ PVR_MENUHOOK_ALL = 0,
+
+ /// @brief __1__ : For channels.
+ PVR_MENUHOOK_CHANNEL = 1,
+
+ /// @brief __2__ : For timers.
+ PVR_MENUHOOK_TIMER = 2,
+
+ /// @brief __3__ : For EPG.
+ PVR_MENUHOOK_EPG = 3,
+
+ /// @brief __4__ : For recordings.
+ PVR_MENUHOOK_RECORDING = 4,
+
+ /// @brief __5__ : For deleted recordings.
+ PVR_MENUHOOK_DELETED_RECORDING = 5,
+
+ /// @brief __6__ : For settings.
+ PVR_MENUHOOK_SETTING = 6,
+ } PVR_MENUHOOK_CAT;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief "C" PVR add-on menu hook.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref kodi::addon::PVRMenuhook for description of values.
+ */
+ typedef struct PVR_MENUHOOK
+ {
+ unsigned int iHookId;
+ unsigned int iLocalizedStringId;
+ enum PVR_MENUHOOK_CAT category;
+ } PVR_MENUHOOK;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_MENUHOOK_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h
new file mode 100644
index 0000000..b1744aa
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_PROVIDERS_H
+#define C_API_ADDONINSTANCE_PVR_PROVIDERS_H
+
+#include "pvr_defines.h"
+
+#include <stdbool.h>
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" Definitions group 2 - PVR providers
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Provider
+ /// @brief Denotes that no provider uid is available.
+ ///
+ /// Special @ref kodi::addon::PVRChannel::SetClientProviderUid()
+ #define PVR_PROVIDER_INVALID_UID -1
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Provider
+ /// @brief Separator to use in strings containing different tokens, for example
+ /// country and language.
+ ///
+ #define PROVIDER_STRING_TOKEN_SEPARATOR ","
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Channel_PVR_PROVIDER_TYPE enum PVR_PROVIDER_TYPE
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Channel
+ /// @brief **PVR provider types**\n
+ /// Used on @ref kodi::addon::PVRProvider:SetProviderType() value to set related
+ /// type.
+ ///
+ ///@{
+ typedef enum PVR_PROVIDER_TYPE
+ {
+ /// @brief __0__ : Unknown type.
+ PVR_PROVIDER_TYPE_UNKNOWN = 0,
+
+ /// @brief __1__ : IPTV provider.
+ PVR_PROVIDER_TYPE_ADDON = 1,
+
+ /// @brief __2__ : Satellite provider.
+ PVR_PROVIDER_TYPE_SATELLITE = 2,
+
+ /// @brief __3__ : Cable provider.
+ PVR_PROVIDER_TYPE_CABLE = 3,
+
+ /// @brief __4__ : Aerial provider.
+ PVR_PROVIDER_TYPE_AERIAL = 4,
+
+ /// @brief __5__ : IPTV provider.
+ PVR_PROVIDER_TYPE_IPTV = 5,
+
+ /// @brief __6__ : Other type of provider.
+ PVR_PROVIDER_TYPE_OTHER = 6,
+ } PVR_PROVIDER_TYPE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief "C" PVR add-on provider.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref kodi::addon::PVRProvider for description of values.
+ */
+ typedef struct PVR_PROVIDER
+ {
+ unsigned int iUniqueId;
+ char strName[PVR_ADDON_NAME_STRING_LENGTH];
+ enum PVR_PROVIDER_TYPE type;
+ char strIconPath[PVR_ADDON_URL_STRING_LENGTH];
+ //! @brief ISO 3166 country codes, separated by PROVIDER_STRING_TOKEN_SEPARATOR
+ /// (e.g 'GB,IE,FR,CA'), an empty string means this value is undefined
+ char strCountries[PVR_ADDON_COUNTRIES_STRING_LENGTH];
+ //! @brief RFC 5646 language codes, separated by PROVIDER_STRING_TOKEN_SEPARATOR
+ /// (e.g. 'en_GB,fr_CA'), an empty string means this value is undefined
+ char strLanguages[PVR_ADDON_LANGUAGES_STRING_LENGTH];
+ } PVR_PROVIDER;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_PROVIDERS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_recordings.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_recordings.h
new file mode 100644
index 0000000..f04ef34
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_recordings.h
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_RECORDINGS_H
+#define C_API_ADDONINSTANCE_PVR_RECORDINGS_H
+
+#include "pvr_defines.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <time.h>
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" Definitions group 5 - PVR recordings
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Recording_PVR_RECORDING_FLAG enum PVR_RECORDING_FLAG
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Recording
+ /// @brief **Bit field of independent flags associated with the EPG entry.**\n
+ /// Values used by @ref kodi::addon::PVRRecording::SetFlags().
+ ///
+ /// Here's example about the use of this:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRRecording tag;
+ /// tag.SetFlags(PVR_RECORDING_FLAG_IS_SERIES | PVR_RECORDING_FLAG_IS_PREMIERE);
+ /// ~~~~~~~~~~~~~
+ ///
+ ///@{
+ typedef enum PVR_RECORDING_FLAG
+ {
+ /// @brief __0000 0000__ : Nothing special to say about this recording.
+ PVR_RECORDING_FLAG_UNDEFINED = 0,
+
+ /// @brief __0000 0001__ : This recording is part of a series.
+ PVR_RECORDING_FLAG_IS_SERIES = (1 << 0),
+
+ /// @brief __0000 0010__ : This recording will be flagged as new.
+ PVR_RECORDING_FLAG_IS_NEW = (1 << 1),
+
+ /// @brief __0000 0100__ : This recording will be flagged as a premiere.
+ PVR_RECORDING_FLAG_IS_PREMIERE = (1 << 2),
+
+ /// @brief __0000 1000__ : This recording will be flagged as a finale.
+ PVR_RECORDING_FLAG_IS_FINALE = (1 << 3),
+
+ /// @brief __0001 0000__ : This recording will be flagged as live.
+ PVR_RECORDING_FLAG_IS_LIVE = (1 << 4),
+ } PVR_RECORDING_FLAG;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Recording_PVRRecording
+ /// @brief Special @ref kodi::addon::PVRRecording::SetSeriesNumber() and
+ /// @ref kodi::addon::PVRRecording::SetEpisodeNumber() value to indicate it is
+ /// not to be used.
+ ///
+ /// Used if recording has no valid season and/or episode info.
+ ///
+ #define PVR_RECORDING_INVALID_SERIES_EPISODE EPG_TAG_INVALID_SERIES_EPISODE
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Recording_PVRRecording
+ /// @brief Value where set in background to inform that related part not used.
+ ///
+ /// Normally this related parts need not to set by this as it is default.
+ #define PVR_RECORDING_VALUE_NOT_AVAILABLE -1
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Recording_PVR_RECORDING_CHANNEL_TYPE enum PVR_RECORDING_CHANNEL_TYPE
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Recording
+ /// @brief **PVR recording channel types**\n
+ /// Used on @ref kodi::addon::PVRRecording::SetChannelType() value to set related
+ /// type.
+ ///
+ ///@{
+ typedef enum PVR_RECORDING_CHANNEL_TYPE
+ {
+ /// @brief __0__ : Unknown type.
+ PVR_RECORDING_CHANNEL_TYPE_UNKNOWN = 0,
+
+ /// @brief __1__ : TV channel.
+ PVR_RECORDING_CHANNEL_TYPE_TV = 1,
+
+ /// @brief __2__ : Radio channel.
+ PVR_RECORDING_CHANNEL_TYPE_RADIO = 2,
+ } PVR_RECORDING_CHANNEL_TYPE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief "C" PVR add-on recording.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref kodi::addon::PVRRecording for description of values.
+ */
+ typedef struct PVR_RECORDING
+ {
+ char strRecordingId[PVR_ADDON_NAME_STRING_LENGTH];
+ char strTitle[PVR_ADDON_NAME_STRING_LENGTH];
+ char strEpisodeName[PVR_ADDON_NAME_STRING_LENGTH];
+ int iSeriesNumber;
+ int iEpisodeNumber;
+ int iYear;
+ char strDirectory[PVR_ADDON_URL_STRING_LENGTH];
+ char strPlotOutline[PVR_ADDON_DESC_STRING_LENGTH];
+ char strPlot[PVR_ADDON_DESC_STRING_LENGTH];
+ char strGenreDescription[PVR_ADDON_DESC_STRING_LENGTH];
+ char strChannelName[PVR_ADDON_NAME_STRING_LENGTH];
+ char strIconPath[PVR_ADDON_URL_STRING_LENGTH];
+ char strThumbnailPath[PVR_ADDON_URL_STRING_LENGTH];
+ char strFanartPath[PVR_ADDON_URL_STRING_LENGTH];
+ time_t recordingTime;
+ int iDuration;
+ int iPriority;
+ int iLifetime;
+ int iGenreType;
+ int iGenreSubType;
+ int iPlayCount;
+ int iLastPlayedPosition;
+ bool bIsDeleted;
+ unsigned int iEpgEventId;
+ int iChannelUid;
+ enum PVR_RECORDING_CHANNEL_TYPE channelType;
+ char strFirstAired[PVR_ADDON_DATE_STRING_LENGTH];
+ unsigned int iFlags;
+ int64_t sizeInBytes;
+ int iClientProviderUid;
+ char strProviderName[PVR_ADDON_NAME_STRING_LENGTH];
+ } PVR_RECORDING;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_RECORDINGS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_stream.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_stream.h
new file mode 100644
index 0000000..62a3cbd
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_stream.h
@@ -0,0 +1,156 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_STREAM_H
+#define C_API_ADDONINSTANCE_PVR_STREAM_H
+
+#include "../inputstream/demux_packet.h"
+#include "pvr_defines.h"
+
+#include <stdint.h>
+#include <time.h>
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" Definitions group 9 - PVR stream definitions (NOTE: Becomes replaced
+// in future by inputstream addon instance way)
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Stream
+ /// @brief Maximum of allowed streams
+ ///
+ #define PVR_STREAM_MAX_STREAMS 20
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Stream
+ /// @brief Invalid codec identifier
+ ///
+ #define PVR_INVALID_CODEC_ID 0
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Stream
+ /// @brief Invalid codec
+ ///
+ #define PVR_INVALID_CODEC \
+ { \
+ PVR_CODEC_TYPE_UNKNOWN, PVR_INVALID_CODEC_ID \
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Stream_PVR_CODEC_TYPE enum PVR_CODEC_TYPE
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Stream
+ /// @brief **Inputstream types**\n
+ /// To identify type on stream.
+ ///
+ /// Used on @ref kodi::addon::PVRStreamProperties::SetCodecType and @ref kodi::addon::PVRStreamProperties::SetCodecType.
+ ///
+ ///@{
+ typedef enum PVR_CODEC_TYPE
+ {
+ /// @brief To set nothing defined.
+ PVR_CODEC_TYPE_UNKNOWN = -1,
+
+ /// @brief To identify @ref cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties as Video.
+ PVR_CODEC_TYPE_VIDEO,
+
+ /// @brief To identify @ref cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties as Audio.
+ PVR_CODEC_TYPE_AUDIO,
+
+ /// @brief To identify @ref cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties as Data.
+ ///
+ /// With codec id related source identified.
+ PVR_CODEC_TYPE_DATA,
+
+ /// @brief To identify @ref cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties as Subtitle.
+ PVR_CODEC_TYPE_SUBTITLE,
+
+ /// @brief To identify @ref cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties as Radio RDS.
+ PVR_CODEC_TYPE_RDS,
+
+ /// @brief To identify @ref cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties as Audio ID3 tags.
+ PVR_CODEC_TYPE_ID3,
+
+ PVR_CODEC_TYPE_NB
+ } PVR_CODEC_TYPE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Stream_PVR_CODEC struct PVR_CODEC
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Stream
+ /// @brief **Codec identification structure**\n
+ /// Identifier about stream between Kodi and addon.
+ ///
+ ///@{
+ typedef struct PVR_CODEC
+ {
+ /// @brief Used codec type for stream.
+ enum PVR_CODEC_TYPE codec_type;
+
+ /// @brief Related codec identifier, normally match the ffmpeg id's.
+ unsigned int codec_id;
+ } PVR_CODEC;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief "C" Stream properties
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref cpp_kodi_addon_pvr_Defs_Stream_PVRStreamProperties for description of values.
+ */
+ typedef struct PVR_STREAM_PROPERTIES
+ {
+ unsigned int iStreamCount;
+ struct PVR_STREAM
+ {
+ unsigned int iPID;
+ enum PVR_CODEC_TYPE iCodecType;
+ unsigned int iCodecId;
+ char strLanguage[4];
+ int iSubtitleInfo;
+ int iFPSScale;
+ int iFPSRate;
+ int iHeight;
+ int iWidth;
+ float fAspect;
+ int iChannels;
+ int iSampleRate;
+ int iBlockAlign;
+ int iBitRate;
+ int iBitsPerSample;
+ } stream[PVR_STREAM_MAX_STREAMS];
+ } PVR_STREAM_PROPERTIES;
+
+ /*!
+ * @brief "C" Times of playing stream (Live TV and recordings)
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref cpp_kodi_addon_pvr_Defs_Stream_PVRStreamTimes for description of values.
+ */
+ typedef struct PVR_STREAM_TIMES
+ {
+ time_t startTime;
+ int64_t ptsStart;
+ int64_t ptsBegin;
+ int64_t ptsEnd;
+ } PVR_STREAM_TIMES;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_STREAM_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_timers.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_timers.h
new file mode 100644
index 0000000..6cae33e
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_timers.h
@@ -0,0 +1,410 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_PVR_TIMERS_H
+#define C_API_ADDONINSTANCE_PVR_TIMERS_H
+
+#include "pvr_defines.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <time.h>
+
+//¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+// "C" Definitions group 6 - PVR timers
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Timer_PVR_TIMER_ definition PVR_TIMER (various)
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Timer
+ /// @brief **PVR timer various different definitions**\n
+ /// This mostly used on @ref cpp_kodi_addon_pvr_Defs_Timer_PVRTimer "kodi::addon::PVRTimer"
+ /// to define default or not available.
+ ///
+ ///@{
+
+ //============================================================================
+ /// @brief Numeric PVR timer type definitions (@ref kodi::addon::PVRTimer::SetTimerType()
+ /// values).
+ ///
+ /// "Null" value for a numeric timer type.
+ #define PVR_TIMER_TYPE_NONE 0
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Special @ref kodi::addon::PVRTimer::SetClientIndex() value to indicate
+ /// that a timer has not (yet) a valid client index.
+ ///
+ /// Timer has not (yet) a valid client index.
+ #define PVR_TIMER_NO_CLIENT_INDEX 0
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Special @ref kodi::addon::PVRTimer::SetParentClientIndex() value to
+ /// indicate that a timer has no parent.
+ ///
+ /// Timer has no parent; it was not scheduled by a repeating timer.
+ #define PVR_TIMER_NO_PARENT PVR_TIMER_NO_CLIENT_INDEX
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Special @ref kodi::addon::PVRTimer::SetEPGUid() value to indicate
+ /// that a timer has no EPG event uid.
+ ///
+ /// Timer has no EPG event unique identifier.
+ #define PVR_TIMER_NO_EPG_UID EPG_TAG_INVALID_UID
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Special @ref kodi::addon::PVRTimer::SetClientChannelUid() value to
+ /// indicate "any channel". Useful for some repeating timer types.
+ ///
+ /// denotes "any channel", not a specific one.
+ ///
+ #define PVR_TIMER_ANY_CHANNEL -1
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Value where set in background to inform that related part not used.
+ ///
+ /// Normally this related parts need not to set by this as it is default.
+ #define PVR_TIMER_VALUE_NOT_AVAILABLE -1
+ //----------------------------------------------------------------------------
+
+ ///@}
+ //----------------------------------------------------------------------------
+
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Timer_PVR_TIMER_TYPES enum PVR_TIMER_TYPES
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Timer
+ /// @brief **PVR timer type attributes (@ref kodi::addon::PVRTimerType::SetAttributes() values).**\n
+ /// To defines the attributes for a type. These values are bit fields that can be
+ /// used together.
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::addon::PVRTimerType tag;
+ /// tag.SetAttributes(PVR_TIMER_TYPE_IS_MANUAL | PVR_TIMER_TYPE_IS_REPEATING);
+ /// ~~~~~~~~~~~~~
+ ///
+ ///@{
+ typedef enum PVR_TIMER_TYPES
+ {
+ /// @brief __0000 0000 0000 0000 0000 0000 0000 0000__ :\n Empty attribute value.
+ PVR_TIMER_TYPE_ATTRIBUTE_NONE = 0,
+
+ /// @brief __0000 0000 0000 0000 0000 0000 0000 0001__ :\n Defines whether this is a type for
+ /// manual (time-based) or epg-based timers.
+ PVR_TIMER_TYPE_IS_MANUAL = (1 << 0),
+
+ /// @brief __0000 0000 0000 0000 0000 0000 0000 0010__ :\n Defines whether this is a type for
+ /// repeating or one-shot timers.
+ PVR_TIMER_TYPE_IS_REPEATING = (1 << 1),
+
+ /// @brief __0000 0000 0000 0000 0000 0000 0000 0100__ :\n Timers of this type must not be edited
+ /// by Kodi.
+ PVR_TIMER_TYPE_IS_READONLY = (1 << 2),
+
+ /// @brief __0000 0000 0000 0000 0000 0000 0000 1000__ :\n Timers of this type must not be created
+ /// by Kodi. All other operations are allowed, though.
+ PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES = (1 << 3),
+
+ /// @brief __0000 0000 0000 0000 0000 0000 0001 0000__ :\n This type supports enabling/disabling
+ /// of the timer (@ref kodi::addon::PVRTimer::SetState() with
+ /// @ref PVR_TIMER_STATE_SCHEDULED | @ref PVR_TIMER_STATE_DISABLED).
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE = (1 << 4),
+
+ /// @brief __0000 0000 0000 0000 0000 0000 0010 0000__ :\n This type supports channels
+ /// (@ref kodi::addon::PVRTimer::SetClientChannelUid()).
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS = (1 << 5),
+
+ /// @brief __0000 0000 0000 0000 0000 0000 0100 0000__ :\n This type supports a recording start
+ /// time (@ref kodi::addon::PVRTimer::SetStartTime()).
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME = (1 << 6),
+
+ /// @brief __0000 0000 0000 0000 0000 0000 1000 0000__ :\n This type supports matching epg episode
+ /// title using@ref kodi::addon::PVRTimer::SetEPGSearchString().
+ PVR_TIMER_TYPE_SUPPORTS_TITLE_EPG_MATCH = (1 << 7),
+
+ /// @brief __0000 0000 0000 0000 0000 0001 0000 0000__ :\n This type supports matching "more" epg
+ /// data (not just episode title) using @ref kodi::addon::PVRTimer::SetEPGSearchString().
+ /// Setting @ref PVR_TIMER_TYPE_SUPPORTS_FULLTEXT_EPG_MATCH implies
+ /// @ref PVR_TIMER_TYPE_SUPPORTS_TITLE_EPG_MATCH.
+ PVR_TIMER_TYPE_SUPPORTS_FULLTEXT_EPG_MATCH = (1 << 8),
+
+ /// @brief __0000 0000 0000 0000 0000 0010 0000 0000__ :\n This type supports a first day the
+ /// timer gets active (@ref kodi::addon::PVRTimer::SetFirstDay()).
+ PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY = (1 << 9),
+
+ /// @brief __0000 0000 0000 0000 0000 0100 0000 0000__ :\n This type supports weekdays for
+ /// defining the recording schedule (@ref kodi::addon::PVRTimer::SetWeekdays()).
+ PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS = (1 << 10),
+
+ /// @brief __0000 0000 0000 0000 0000 1000 0000 0000__ :\n This type supports the <b>"record only new episodes"</b> feature
+ /// (@ref kodi::addon::PVRTimer::SetPreventDuplicateEpisodes()).
+ PVR_TIMER_TYPE_SUPPORTS_RECORD_ONLY_NEW_EPISODES = (1 << 11),
+
+ /// @brief __0000 0000 0000 0000 0001 0000 0000 0000__ :\n This type supports pre and post record time (@ref kodi::addon::PVRTimer::SetMarginStart(),
+ /// @ref kodi::addon::PVRTimer::SetMarginEnd()).
+ PVR_TIMER_TYPE_SUPPORTS_START_END_MARGIN = (1 << 12),
+
+ /// @brief __0000 0000 0000 0000 0010 0000 0000 0000__ :\n This type supports recording priority (@ref kodi::addon::PVRTimer::SetPriority()).
+ PVR_TIMER_TYPE_SUPPORTS_PRIORITY = (1 << 13),
+
+ /// @brief __0000 0000 0000 0000 0100 0000 0000 0000__ :\n This type supports recording lifetime (@ref kodi::addon::PVRTimer::SetLifetime()).
+ PVR_TIMER_TYPE_SUPPORTS_LIFETIME = (1 << 14),
+
+ /// @brief __0000 0000 0000 0000 1000 0000 0000 0000__ :\n This type supports placing recordings in user defined folders
+ /// (@ref kodi::addon::PVRTimer::SetDirectory()).
+ PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS = (1 << 15),
+
+ /// @brief __0000 0000 0000 0001 0000 0000 0000 0000__ :\n This type supports a list of recording groups
+ /// (@ref kodi::addon::PVRTimer::SetRecordingGroup()).
+ PVR_TIMER_TYPE_SUPPORTS_RECORDING_GROUP = (1 << 16),
+
+ /// @brief __0000 0000 0000 0010 0000 0000 0000 0000__ :\n This type supports a recording end time (@ref kodi::addon::PVRTimer::SetEndTime()).
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME = (1 << 17),
+
+ /// @brief __0000 0000 0000 0100 0000 0000 0000 0000__ :\n Enables an 'Any Time' over-ride option for start time
+ /// (using @ref kodi::addon::PVRTimer::SetStartAnyTime()).
+ PVR_TIMER_TYPE_SUPPORTS_START_ANYTIME = (1 << 18),
+
+ /// @brief __0000 0000 0000 1000 0000 0000 0000 0000__ :\n Enables a separate <b>'Any Time'</b> over-ride for end time
+ /// (using @ref kodi::addon::PVRTimer::SetEndAnyTime()).
+ PVR_TIMER_TYPE_SUPPORTS_END_ANYTIME = (1 << 19),
+
+ /// @brief __0000 0000 0001 0000 0000 0000 0000 0000__ :\n This type supports specifying a maximum recordings setting'
+ /// (@ref kodi::addon::PVRTimer::SetMaxRecordings()).
+ PVR_TIMER_TYPE_SUPPORTS_MAX_RECORDINGS = (1 << 20),
+
+ /// @brief __0000 0000 0010 0000 0000 0000 0000 0000__ :\n This type should not appear on any create menus which don't
+ /// provide an associated @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag "EPG tag".
+ PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE = (1 << 21),
+
+ /// @brief __0000 0000 0100 0000 0000 0000 0000 0000__ :\n This type should not appear on any create menus which provide an
+ /// associated @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag "EPG tag".
+ PVR_TIMER_TYPE_FORBIDS_EPG_TAG_ON_CREATE = (1 << 22),
+
+ /// @brief __0000 0000 1000 0000 0000 0000 0000 0000__ :\n This type should not appear on any create menus unless associated
+ /// with an @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag "EPG tag" with
+ /// 'series' attributes.
+ ///
+ /// Following conditions allow this:
+ /// - @ref kodi::addon::PVREPGTag::SetFlags() have flag @ref EPG_TAG_FLAG_IS_SERIES
+ /// - @ref kodi::addon::PVREPGTag::SetSeriesNumber() > 0
+ /// - @ref kodi::addon::PVREPGTag::SetEpisodeNumber() > 0
+ /// - @ref kodi::addon::PVREPGTag::SetEpisodePartNumber() > 0
+ ///
+ /// Implies @ref PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE.
+ PVR_TIMER_TYPE_REQUIRES_EPG_SERIES_ON_CREATE = (1 << 23),
+
+ /// @brief __0000 0001 0000 0000 0000 0000 0000 0000__ :\n This type supports 'any channel', for example when defining a timer
+ /// rule that should match any channel instead of a particular channel.
+ PVR_TIMER_TYPE_SUPPORTS_ANY_CHANNEL = (1 << 24),
+
+ /// @brief __0000 0010 0000 0000 0000 0000 0000 0000__ :\n This type should not appear on any create menus which don't provide
+ /// an associated @ref cpp_kodi_addon_pvr_Defs_epg_PVREPGTag "EPG tag" with
+ /// a series link.
+ PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE = (1 << 25),
+
+ /// @brief __0000 0100 0000 0000 0000 0000 0000 0000__ :\n This type allows deletion of an otherwise read-only timer.
+ PVR_TIMER_TYPE_SUPPORTS_READONLY_DELETE = (1 << 26),
+
+ /// @brief __0000 1000 0000 0000 0000 0000 0000 0000__ :\n Timers of this type do trigger a reminder if time is up.
+ PVR_TIMER_TYPE_IS_REMINDER = (1 << 27),
+
+ /// @brief __0001 0000 0000 0000 0000 0000 0000 0000__ :\n This type supports pre record time (@ref kodi::addon::PVRTimer::SetMarginStart()).
+ PVR_TIMER_TYPE_SUPPORTS_START_MARGIN = (1 << 28),
+
+ /// @brief __0010 0000 0000 0000 0000 0000 0000 0000__ :\n This type supports post record time (@ref kodi::addon::PVRTimer::SetMarginEnd()).
+ PVR_TIMER_TYPE_SUPPORTS_END_MARGIN = (1 << 29),
+ } PVR_TIMER_TYPES;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Timer_PVR_WEEKDAY enum PVR_WEEKDAY
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Timer
+ /// @brief **PVR timer weekdays** (@ref kodi::addon::PVRTimer::SetWeekdays() **values**)\n
+ /// Used to select the days of a week you want.
+ ///
+ /// It can be also used to select several days e.g.:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ /// unsigned int day = PVR_WEEKDAY_MONDAY | PVR_WEEKDAY_SATURDAY;
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ ///@{
+ typedef enum PVR_WEEKDAYS
+ {
+ /// @brief __0000 0000__ : Nothing selected.
+ PVR_WEEKDAY_NONE = 0,
+
+ /// @brief __0000 0001__ : To select Monday.
+ PVR_WEEKDAY_MONDAY = (1 << 0),
+
+ /// @brief __0000 0010__ : To select Tuesday.
+ PVR_WEEKDAY_TUESDAY = (1 << 1),
+
+ /// @brief __0000 0100__ : To select Wednesday.
+ PVR_WEEKDAY_WEDNESDAY = (1 << 2),
+
+ /// @brief __0000 1000__ : To select Thursday.
+ PVR_WEEKDAY_THURSDAY = (1 << 3),
+
+ /// @brief __0001 0000__ : To select Friday.
+ PVR_WEEKDAY_FRIDAY = (1 << 4),
+
+ /// @brief __0010 0000__ : To select Saturday.
+ PVR_WEEKDAY_SATURDAY = (1 << 5),
+
+ /// @brief __0100 0000__ : To select Sunday.
+ PVR_WEEKDAY_SUNDAY = (1 << 6),
+
+ /// @brief __0111 1111__ : To select all days of week.
+ PVR_WEEKDAY_ALLDAYS = PVR_WEEKDAY_MONDAY | PVR_WEEKDAY_TUESDAY | PVR_WEEKDAY_WEDNESDAY |
+ PVR_WEEKDAY_THURSDAY | PVR_WEEKDAY_FRIDAY | PVR_WEEKDAY_SATURDAY |
+ PVR_WEEKDAY_SUNDAY
+ } PVR_WEEKDAY;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_pvr_Defs_Timer_PVR_TIMER_STATE enum PVR_TIMER_STATE
+ /// @ingroup cpp_kodi_addon_pvr_Defs_Timer
+ /// @brief **PVR timer states**\n
+ /// To set within @ref cpp_kodi_addon_pvr_Defs_Timer_PVRTimer "kodi::addon::PVRTimer"
+ /// the needed state about.
+ ///
+ ///@{
+ typedef enum PVR_TIMER_STATE
+ {
+ /// @brief __0__ : The timer was just created on the backend and is not yet active.
+ ///
+ /// This state must not be used for timers just created on the client side.
+ PVR_TIMER_STATE_NEW = 0,
+
+ /// @brief __1__ : The timer is scheduled for recording.
+ PVR_TIMER_STATE_SCHEDULED = 1,
+
+ /// @brief __2__ : The timer is currently recordings.
+ PVR_TIMER_STATE_RECORDING = 2,
+
+ /// @brief __3__ : The recording completed successfully.
+ PVR_TIMER_STATE_COMPLETED = 3,
+
+ /// @brief __4__ : Recording started, but was aborted.
+ PVR_TIMER_STATE_ABORTED = 4,
+
+ /// @brief __5__ : The timer was scheduled, but was canceled.
+ PVR_TIMER_STATE_CANCELLED = 5,
+
+ /// @brief __6__ : The scheduled timer conflicts with another one, but will be
+ /// recorded.
+ PVR_TIMER_STATE_CONFLICT_OK = 6,
+
+ /// @brief __7__ : The scheduled timer conflicts with another one and won't be
+ /// recorded.
+ PVR_TIMER_STATE_CONFLICT_NOK = 7,
+
+ /// @brief __8__ : The timer is scheduled, but can't be recorded for some reason.
+ PVR_TIMER_STATE_ERROR = 8,
+
+ /// @brief __9__ : The timer was disabled by the user, can be enabled via setting
+ /// the state to @ref PVR_TIMER_STATE_SCHEDULED.
+ PVR_TIMER_STATE_DISABLED = 9,
+ } PVR_TIMER_STATE;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief "C" PVR add-on timer event.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref cpp_kodi_addon_pvr_Defs_Timer_PVRTimer "kodi::addon::PVRTimer" for
+ * description of values.
+ */
+ typedef struct PVR_TIMER
+ {
+ unsigned int iClientIndex;
+ unsigned int iParentClientIndex;
+ int iClientChannelUid;
+ time_t startTime;
+ time_t endTime;
+ bool bStartAnyTime;
+ bool bEndAnyTime;
+ enum PVR_TIMER_STATE state;
+ unsigned int iTimerType;
+ char strTitle[PVR_ADDON_NAME_STRING_LENGTH];
+ char strEpgSearchString[PVR_ADDON_NAME_STRING_LENGTH];
+ bool bFullTextEpgSearch;
+ char strDirectory[PVR_ADDON_URL_STRING_LENGTH];
+ char strSummary[PVR_ADDON_DESC_STRING_LENGTH];
+ int iPriority;
+ int iLifetime;
+ int iMaxRecordings;
+ unsigned int iRecordingGroup;
+ time_t firstDay;
+ unsigned int iWeekdays;
+ unsigned int iPreventDuplicateEpisodes;
+ unsigned int iEpgUid;
+ unsigned int iMarginStart;
+ unsigned int iMarginEnd;
+ int iGenreType;
+ int iGenreSubType;
+ char strSeriesLink[PVR_ADDON_URL_STRING_LENGTH];
+ } PVR_TIMER;
+
+ /*!
+ * @brief "C" PVR add-on timer event type.
+ *
+ * Structure used to interface in "C" between Kodi and Addon.
+ *
+ * See @ref cpp_kodi_addon_pvr_Defs_Timer_PVRTimerType "kodi::addon::PVRTimerType" for
+ * description of values.
+ */
+ typedef struct PVR_TIMER_TYPE
+ {
+ unsigned int iId;
+ uint64_t iAttributes;
+ char strDescription[PVR_ADDON_TIMERTYPE_STRING_LENGTH];
+
+ unsigned int iPrioritiesSize;
+ struct PVR_ATTRIBUTE_INT_VALUE priorities[PVR_ADDON_TIMERTYPE_VALUES_ARRAY_SIZE];
+ int iPrioritiesDefault;
+
+ unsigned int iLifetimesSize;
+ struct PVR_ATTRIBUTE_INT_VALUE lifetimes[PVR_ADDON_TIMERTYPE_VALUES_ARRAY_SIZE];
+ int iLifetimesDefault;
+
+ unsigned int iPreventDuplicateEpisodesSize;
+ struct PVR_ATTRIBUTE_INT_VALUE preventDuplicateEpisodes[PVR_ADDON_TIMERTYPE_VALUES_ARRAY_SIZE];
+ unsigned int iPreventDuplicateEpisodesDefault;
+
+ unsigned int iRecordingGroupSize;
+ struct PVR_ATTRIBUTE_INT_VALUE recordingGroup[PVR_ADDON_TIMERTYPE_VALUES_ARRAY_SIZE];
+ unsigned int iRecordingGroupDefault;
+
+ unsigned int iMaxRecordingsSize;
+ struct PVR_ATTRIBUTE_INT_VALUE maxRecordings[PVR_ADDON_TIMERTYPE_VALUES_ARRAY_SIZE_SMALL];
+ int iMaxRecordingsDefault;
+ } PVR_TIMER_TYPE;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_PVR_TIMERS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/screensaver.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/screensaver.h
new file mode 100644
index 0000000..df1eb19
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/screensaver.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_SCREENSAVER_H
+#define C_API_ADDONINSTANCE_SCREENSAVER_H
+
+#include "../addon_base.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef KODI_ADDON_INSTANCE_HDL KODI_ADDON_SCREENSAVER_HDL;
+
+ struct KODI_ADDON_SCREENSAVER_PROPS
+ {
+ ADDON_HARDWARE_CONTEXT device;
+ int x;
+ int y;
+ int width;
+ int height;
+ float pixelRatio;
+ };
+
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_SCREENSAVER_START_V1)(
+ const KODI_ADDON_SCREENSAVER_HDL hdl);
+ typedef void(ATTR_APIENTRYP PFN_KODI_ADDON_SCREENSAVER_STOP_V1)(
+ const KODI_ADDON_SCREENSAVER_HDL hdl);
+ typedef void(ATTR_APIENTRYP PFN_KODI_ADDON_SCREENSAVER_RENDER_V1)(
+ const KODI_ADDON_SCREENSAVER_HDL hdl);
+
+ typedef struct KodiToAddonFuncTable_Screensaver
+ {
+ PFN_KODI_ADDON_SCREENSAVER_START_V1 start;
+ PFN_KODI_ADDON_SCREENSAVER_STOP_V1 stop;
+ PFN_KODI_ADDON_SCREENSAVER_RENDER_V1 render;
+ } KodiToAddonFuncTable_Screensaver;
+
+ typedef struct AddonToKodiFuncTable_Screensaver
+ {
+ void (*get_properties)(const KODI_HANDLE hdl, struct KODI_ADDON_SCREENSAVER_PROPS* props);
+ } AddonToKodiFuncTable_Screensaver;
+
+ typedef struct AddonInstance_Screensaver
+ {
+ struct AddonToKodiFuncTable_Screensaver* toKodi;
+ struct KodiToAddonFuncTable_Screensaver* toAddon;
+ } AddonInstance_Screensaver;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_SCREENSAVER_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/vfs.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/vfs.h
new file mode 100644
index 0000000..b4fd90c
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/vfs.h
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2005-2020 Team Kodi
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#ifndef C_API_ADDONINSTANCE_VFS_H
+#define C_API_ADDONINSTANCE_VFS_H
+
+#include "../addon_base.h"
+#include "../filesystem.h"
+
+#define VFS_FILE_HANDLE void*
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ struct VFSURL
+ {
+ const char* url;
+ const char* domain;
+ const char* hostname;
+ const char* filename;
+ unsigned int port;
+ const char* options;
+ const char* username;
+ const char* password;
+ const char* redacted;
+ const char* sharename;
+ const char* protocol;
+ };
+
+ typedef struct VFSGetDirectoryCallbacks /* internal */
+ {
+ bool(__cdecl* get_keyboard_input)(KODI_HANDLE ctx,
+ const char* heading,
+ char** input,
+ bool hidden_input);
+ void(__cdecl* set_error_dialog)(KODI_HANDLE ctx,
+ const char* heading,
+ const char* line1,
+ const char* line2,
+ const char* line3);
+ void(__cdecl* require_authentication)(KODI_HANDLE ctx, const char* url);
+ KODI_HANDLE ctx;
+ } VFSGetDirectoryCallbacks;
+
+ typedef struct AddonProps_VFSEntry /* internal */
+ {
+ int dummy;
+ } AddonProps_VFSEntry;
+
+ typedef struct AddonToKodiFuncTable_VFSEntry /* internal */
+ {
+ KODI_HANDLE kodiInstance;
+ } AddonToKodiFuncTable_VFSEntry;
+
+ struct AddonInstance_VFSEntry;
+ typedef struct KodiToAddonFuncTable_VFSEntry /* internal */
+ {
+ KODI_HANDLE addonInstance;
+
+ VFS_FILE_HANDLE(__cdecl* open)
+ (const struct AddonInstance_VFSEntry* instance, const struct VFSURL* url);
+ VFS_FILE_HANDLE(__cdecl* open_for_write)
+ (const struct AddonInstance_VFSEntry* instance, const struct VFSURL* url, bool overwrite);
+ ssize_t(__cdecl* read)(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ uint8_t* buffer,
+ size_t buf_size);
+ ssize_t(__cdecl* write)(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ const uint8_t* buffer,
+ size_t buf_size);
+ int64_t(__cdecl* seek)(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ int64_t position,
+ int whence);
+ int(__cdecl* truncate)(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ int64_t size);
+ int64_t(__cdecl* get_length)(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context);
+ int64_t(__cdecl* get_position)(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context);
+ int(__cdecl* get_chunk_size)(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context);
+ bool(__cdecl* io_control_get_seek_possible)(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context);
+ bool(__cdecl* io_control_get_cache_status)(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ struct VFS_CACHE_STATUS_DATA* status);
+ bool(__cdecl* io_control_set_cache_rate)(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ uint32_t rate);
+ bool(__cdecl* io_control_set_retry)(const struct AddonInstance_VFSEntry* instance,
+ VFS_FILE_HANDLE context,
+ bool retry);
+ int(__cdecl* stat)(const struct AddonInstance_VFSEntry* instance,
+ const struct VFSURL* url,
+ struct STAT_STRUCTURE* buffer);
+ bool(__cdecl* close)(const struct AddonInstance_VFSEntry* instance, VFS_FILE_HANDLE context);
+
+ bool(__cdecl* exists)(const struct AddonInstance_VFSEntry* instance, const struct VFSURL* url);
+ void(__cdecl* clear_out_idle)(const struct AddonInstance_VFSEntry* instance);
+ void(__cdecl* disconnect_all)(const struct AddonInstance_VFSEntry* instance);
+ bool(__cdecl* delete_it)(const struct AddonInstance_VFSEntry* instance,
+ const struct VFSURL* url);
+ bool(__cdecl* rename)(const struct AddonInstance_VFSEntry* instance,
+ const struct VFSURL* url,
+ const struct VFSURL* url2);
+ bool(__cdecl* directory_exists)(const struct AddonInstance_VFSEntry* instance,
+ const struct VFSURL* url);
+ bool(__cdecl* remove_directory)(const struct AddonInstance_VFSEntry* instance,
+ const struct VFSURL* url);
+ bool(__cdecl* create_directory)(const struct AddonInstance_VFSEntry* instance,
+ const struct VFSURL* url);
+ bool(__cdecl* get_directory)(const struct AddonInstance_VFSEntry* instance,
+ const struct VFSURL* url,
+ struct VFSDirEntry** entries,
+ int* num_entries,
+ struct VFSGetDirectoryCallbacks* callbacks);
+ bool(__cdecl* contains_files)(const struct AddonInstance_VFSEntry* instance,
+ const struct VFSURL* url,
+ struct VFSDirEntry** entries,
+ int* num_entries,
+ char* rootpath);
+ void(__cdecl* free_directory)(const struct AddonInstance_VFSEntry* instance,
+ struct VFSDirEntry* entries,
+ int num_entries);
+ } KodiToAddonFuncTable_VFSEntry;
+
+ typedef struct AddonInstance_VFSEntry /* internal */
+ {
+ struct AddonProps_VFSEntry* props;
+ struct AddonToKodiFuncTable_VFSEntry* toKodi;
+ struct KodiToAddonFuncTable_VFSEntry* toAddon;
+ } AddonInstance_VFSEntry;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_VFS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/video_codec.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/video_codec.h
new file mode 100644
index 0000000..0a70c15
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/video_codec.h
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2017-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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_VIDEOCODEC_H
+#define C_API_ADDONINSTANCE_VIDEOCODEC_H
+
+#include "../addon_base.h"
+#include "inputstream/demux_packet.h"
+#include "inputstream/stream_codec.h"
+#include "inputstream/stream_crypto.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec_Defs
+ /// @brief Return values used by video decoder interface
+ enum VIDEOCODEC_RETVAL
+ {
+ /// @brief Noop
+ VC_NONE = 0,
+
+ /// @brief An error occurred, no other messages will be returned
+ VC_ERROR,
+
+ /// @brief The decoder needs more data
+ VC_BUFFER,
+
+ /// @brief The decoder got a picture
+ VC_PICTURE,
+
+ /// @brief The decoder signals EOF
+ VC_EOF,
+ };
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec_Defs_VideoCodecInitdata
+ /// @brief The video stream representations requested by Kodi
+ ///
+ enum VIDEOCODEC_FORMAT
+ {
+ /// @brief Unknown types, this is used to declare the end of a list of
+ /// requested types
+ VIDEOCODEC_FORMAT_UNKNOWN = 0,
+
+ /// @brief YV12 4:2:0 YCrCb planar format
+ VIDEOCODEC_FORMAT_YV12,
+
+ /// @brief These formats are identical to YV12 except that the U and V plane
+ /// order is reversed.
+ VIDEOCODEC_FORMAT_I420,
+
+ /// @brief The maximum value to use in a list.
+ VIDEOCODEC_FORMAT_MAXFORMATS
+ };
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec_Defs_VideoCodecInitdata
+ /// @brief Video codec types that can be requested from Kodi
+ ///
+ enum VIDEOCODEC_TYPE
+ {
+ /// @brief Unknown or other type requested
+ ///
+ VIDEOCODEC_UNKNOWN = 0,
+
+ /// @brief [VP8](https://en.wikipedia.org/wiki/VP8) video coding format
+ ///
+ VIDEOCODEC_VP8,
+
+ /// @brief [Advanced Video Coding (AVC)](https://en.wikipedia.org/wiki/Advanced_Video_Coding),
+ /// also referred to as H.264 or [MPEG-4](https://en.wikipedia.org/wiki/MPEG-4)
+ /// Part 10, Advanced Video Coding (MPEG-4 AVC).
+ VIDEOCODEC_H264,
+
+ /// @brief [VP9](https://en.wikipedia.org/wiki/VP9) video coding format\n
+ /// \n
+ /// VP9 is the successor to VP8 and competes mainly with MPEG's
+ /// [High Efficiency Video Coding](https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding)
+ /// (HEVC/H.265).
+ VIDEOCODEC_VP9,
+
+ /// @brief [AV1](https://en.wikipedia.org/wiki/AV1) video coding format\n
+ /// \n
+ /// AV1 is the successor to VP9 and competes mainly with MPEG's
+ /// [High Efficiency Video Coding](https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding)
+ /// (HEVC/H.265).
+ VIDEOCODEC_AV1,
+ };
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec_Defs_VIDEOCODEC_PICTURE
+ /// @brief YUV Plane identification pointers
+ ///
+ /// YUV is a color encoding system typically used as part of a color image pipeline.
+ ///
+ /// These are used to access stored data in @ref VIDEOCODEC_PICTURE::planeOffsets
+ /// and @ref VIDEOCODEC_PICTURE::stride.
+ ///
+ enum VIDEOCODEC_PLANE
+ {
+ /// @brief "luminance" component Y (equivalent to grey scale)
+ VIDEOCODEC_PICTURE_Y_PLANE = 0,
+
+ /// @brief "chrominance" component U (blue projection)
+ VIDEOCODEC_PICTURE_U_PLANE,
+
+ /// @brief "chrominance" component V (red projection)
+ VIDEOCODEC_PICTURE_V_PLANE,
+
+ /// @brief The maximum value to use in a list.
+ VIDEOCODEC_PICTURE_MAXPLANES = 3,
+ };
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_videocodec_Defs_VIDEOCODEC_PICTURE
+ /// @brief Video coded process flags, used to perform special operations in
+ /// stream calls.
+ ///
+ /// These are used to access stored data in @ref VIDEOCODEC_PICTURE::flags.
+ ///
+ /// @note These variables are bit flags which are created using "|" can be used together.
+ ///
+ enum VIDEOCODEC_PICTURE_FLAG
+ {
+ /// @brief Empty and nothing defined
+ VIDEOCODEC_PICTURE_FLAG_NONE = 0,
+
+ /// @brief Drop in decoder
+ VIDEOCODEC_PICTURE_FLAG_DROP = (1 << 0),
+
+ /// @brief Squeeze out pictured without feeding new packets
+ VIDEOCODEC_PICTURE_FLAG_DRAIN = (1 << 1),
+ };
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_addon_videocodec_Defs_VIDEOCODEC_PICTURE struct VIDEOCODEC_PICTURE
+ /// @ingroup cpp_kodi_addon_videocodec_Defs
+ /// @brief Data structure which is given to the addon when a decoding call is made.
+ ///
+ ///@{
+ struct VIDEOCODEC_PICTURE
+ {
+ /// @brief The video format declared with @ref VIDEOCODEC_FORMAT and to be
+ /// used on the addon.
+ enum VIDEOCODEC_FORMAT videoFormat;
+
+ /// @brief Video coded process flags, used to perform special operations in
+ /// stream calls.
+ ///
+ /// Possible flags are declared here @ref VIDEOCODEC_PICTURE_FLAGS.
+ uint32_t flags;
+
+ /// @brief Picture width.
+ uint32_t width;
+
+ /// @brief Picture height.
+ uint32_t height;
+
+ /// @brief Data to be decoded in the addon.
+ uint8_t* decodedData;
+
+ /// @brief Size of the data given with @ref decodedData
+ size_t decodedDataSize;
+
+ /// @brief YUV color plane calculation array.
+ ///
+ /// This includes the three values of the YUV and can be identified using
+ /// @ref VIDEOCODEC_PLANE.
+ uint32_t planeOffsets[VIDEOCODEC_PICTURE_MAXPLANES];
+
+ /// @brief YUV color stride calculation array
+ ///
+ /// This includes the three values of the YUV and can be identified using
+ /// @ref VIDEOCODEC_PLANE.
+ uint32_t stride[VIDEOCODEC_PICTURE_MAXPLANES];
+
+ /// @brief Picture presentation time stamp (PTS).
+ int64_t pts;
+
+ /// @brief This is used to save the related handle from Kodi.
+ ///
+ /// To handle the input stream buffer, this is given by Kodi using
+ /// @ref kodi::addon::CInstanceVideoCodec::GetFrameBuffer and must be
+ /// released again using @ref kodi::addon::CInstanceVideoCodec::ReleaseFrameBuffer.
+ KODI_HANDLE videoBufferHandle;
+ };
+ ///@}
+ //----------------------------------------------------------------------------
+
+ struct VIDEOCODEC_INITDATA
+ {
+ enum VIDEOCODEC_TYPE codec;
+ enum STREAMCODEC_PROFILE codecProfile;
+ enum VIDEOCODEC_FORMAT* videoFormats;
+ uint32_t width;
+ uint32_t height;
+ const uint8_t* extraData;
+ unsigned int extraDataSize;
+ struct STREAM_CRYPTO_SESSION cryptoSession;
+ };
+
+ // this are properties given to the addon on create
+ // at this time we have no parameters for the addon
+ typedef struct AddonProps_VideoCodec
+ {
+ int dummy;
+ } AddonProps_VideoCodec;
+
+ struct AddonInstance_VideoCodec;
+ typedef struct KodiToAddonFuncTable_VideoCodec
+ {
+ KODI_HANDLE addonInstance;
+
+ //! @brief Opens a codec
+ bool(__cdecl* open)(const struct AddonInstance_VideoCodec* instance,
+ struct VIDEOCODEC_INITDATA* initData);
+
+ //! @brief Reconfigures a codec
+ bool(__cdecl* reconfigure)(const struct AddonInstance_VideoCodec* instance,
+ struct VIDEOCODEC_INITDATA* initData);
+
+ //! @brief Feed codec if requested from GetPicture() (return VC_BUFFER)
+ bool(__cdecl* add_data)(const struct AddonInstance_VideoCodec* instance,
+ const struct DEMUX_PACKET* packet);
+
+ //! @brief Get a decoded picture / request new data
+ enum VIDEOCODEC_RETVAL(__cdecl* get_picture)(const struct AddonInstance_VideoCodec* instance,
+ struct VIDEOCODEC_PICTURE* picture);
+
+ //! @brief Get the name of this video decoder
+ const char*(__cdecl* get_name)(const struct AddonInstance_VideoCodec* instance);
+
+ //! @brief Reset the codec
+ void(__cdecl* reset)(const struct AddonInstance_VideoCodec* instance);
+ } KodiToAddonFuncTable_VideoCodec;
+
+ typedef struct AddonToKodiFuncTable_VideoCodec
+ {
+ KODI_HANDLE kodiInstance;
+ bool (*get_frame_buffer)(void* kodiInstance, struct VIDEOCODEC_PICTURE* picture);
+ void (*release_frame_buffer)(void* kodiInstance, void* buffer);
+ } AddonToKodiFuncTable_VideoCodec;
+
+ typedef struct AddonInstance_VideoCodec
+ {
+ struct AddonProps_VideoCodec* props;
+ struct AddonToKodiFuncTable_VideoCodec* toKodi;
+ struct KodiToAddonFuncTable_VideoCodec* toAddon;
+ } AddonInstance_VideoCodec;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_VIDEOCODEC_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/visualization.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/visualization.h
new file mode 100644
index 0000000..ae5b2d0
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon-instance/visualization.h
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_ADDONINSTANCE_VISUALIZATION_H
+#define C_API_ADDONINSTANCE_VISUALIZATION_H
+
+#include "../addon_base.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef KODI_ADDON_INSTANCE_HDL KODI_ADDON_VISUALIZATION_HDL;
+
+ struct KODI_ADDON_VISUALIZATION_TRACK
+ {
+ const char* title;
+ const char* artist;
+ const char* album;
+ const char* albumArtist;
+ const char* genre;
+ const char* comment;
+ const char* lyrics;
+
+ const char* reserved1;
+ const char* reserved2;
+
+ int trackNumber;
+ int discNumber;
+ int duration;
+ int year;
+ int rating;
+
+ int reserved3;
+ int reserved4;
+ };
+
+ struct KODI_ADDON_VISUALIZATION_PROPS
+ {
+ ADDON_HARDWARE_CONTEXT device;
+ int x;
+ int y;
+ int width;
+ int height;
+ float pixelRatio;
+ };
+
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_START_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl,
+ int channels,
+ int samples_per_sec,
+ int bits_per_sample,
+ const char* song_name);
+ typedef void(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_STOP_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl);
+
+ typedef int(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_GET_SYNC_DELAY_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl);
+
+ typedef void(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_AUDIO_DATA_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl, const float* audio_data, size_t audio_data_length);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_IS_DIRTY_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl);
+ typedef void(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_RENDER_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl);
+
+ typedef unsigned int(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_GET_PRESETS_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl);
+ typedef int(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_GET_ACTIVE_PRESET_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_PREV_PRESET_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_NEXT_PRESET_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_LOAD_PRESET_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl, int select);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_RANDOM_PRESET_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_LOCK_PRESET_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_RATE_PRESET_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl, bool plus_minus);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_IS_LOCKED_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl);
+
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_UPDATE_ALBUMART_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl, const char* albumart);
+ typedef bool(ATTR_APIENTRYP PFN_KODI_ADDON_VISUALIZATION_UPDATE_TRACK_V1)(
+ const KODI_ADDON_VISUALIZATION_HDL hdl, const struct KODI_ADDON_VISUALIZATION_TRACK* track);
+
+ typedef struct KodiToAddonFuncTable_Visualization
+ {
+ PFN_KODI_ADDON_VISUALIZATION_START_V1 start;
+ PFN_KODI_ADDON_VISUALIZATION_STOP_V1 stop;
+
+ PFN_KODI_ADDON_VISUALIZATION_GET_SYNC_DELAY_V1 get_sync_delay;
+
+ PFN_KODI_ADDON_VISUALIZATION_AUDIO_DATA_V1 audio_data;
+ PFN_KODI_ADDON_VISUALIZATION_IS_DIRTY_V1 is_dirty;
+ PFN_KODI_ADDON_VISUALIZATION_RENDER_V1 render;
+
+ PFN_KODI_ADDON_VISUALIZATION_GET_PRESETS_V1 get_presets;
+ PFN_KODI_ADDON_VISUALIZATION_GET_ACTIVE_PRESET_V1 get_active_preset;
+ PFN_KODI_ADDON_VISUALIZATION_PREV_PRESET_V1 prev_preset;
+ PFN_KODI_ADDON_VISUALIZATION_NEXT_PRESET_V1 next_preset;
+ PFN_KODI_ADDON_VISUALIZATION_LOAD_PRESET_V1 load_preset;
+ PFN_KODI_ADDON_VISUALIZATION_RANDOM_PRESET_V1 random_preset;
+ PFN_KODI_ADDON_VISUALIZATION_LOCK_PRESET_V1 lock_preset;
+ PFN_KODI_ADDON_VISUALIZATION_RATE_PRESET_V1 rate_preset;
+ PFN_KODI_ADDON_VISUALIZATION_IS_LOCKED_V1 is_locked;
+
+ PFN_KODI_ADDON_VISUALIZATION_UPDATE_ALBUMART_V1 update_albumart;
+ PFN_KODI_ADDON_VISUALIZATION_UPDATE_TRACK_V1 update_track;
+ } KodiToAddonFuncTable_Visualization;
+
+ typedef struct AddonToKodiFuncTable_Visualization
+ {
+ void (*get_properties)(const KODI_HANDLE hdl, struct KODI_ADDON_VISUALIZATION_PROPS* props);
+ void (*transfer_preset)(const KODI_HANDLE hdl, const char* preset);
+ void (*clear_presets)(const KODI_HANDLE hdl);
+ } AddonToKodiFuncTable_Visualization;
+
+ typedef struct AddonInstance_Visualization
+ {
+ struct AddonToKodiFuncTable_Visualization* toKodi;
+ struct KodiToAddonFuncTable_Visualization* toAddon;
+ } AddonInstance_Visualization;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDONINSTANCE_VISUALIZATION_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon_base.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon_base.h
new file mode 100644
index 0000000..89403a3
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/addon_base.h
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2005-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.
+ */
+
+#ifndef C_API_ADDON_BASE_H
+#define C_API_ADDON_BASE_H
+
+#if !defined(NOMINMAX)
+#define NOMINMAX
+#endif
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#ifndef TARGET_WINDOWS
+#ifndef __cdecl
+#define __cdecl
+#endif
+#ifndef __declspec
+#define __declspec(X)
+#endif
+#endif
+
+// Generic helper definitions for smallest possible alignment
+//@{
+#undef ATTR_PACKED
+#undef PRAGMA_PACK_BEGIN
+#undef PRAGMA_PACK_END
+
+#if defined(__GNUC__)
+#define ATTR_PACKED __attribute__((packed))
+#define PRAGMA_PACK 0
+#endif
+
+#if !defined(ATTR_PACKED)
+#define ATTR_PACKED
+#define PRAGMA_PACK 1
+#endif
+//@}
+
+// Generic helper definitions for inline function support
+//@{
+#ifdef _MSC_VER
+#define ATTR_FORCEINLINE __forceinline
+#elif defined(__GNUC__)
+#define ATTR_FORCEINLINE inline __attribute__((__always_inline__))
+#elif defined(__CLANG__)
+#if __has_attribute(__always_inline__)
+#define ATTR_FORCEINLINE inline __attribute__((__always_inline__))
+#else
+#define ATTR_FORCEINLINE inline
+#endif
+#else
+#define ATTR_FORCEINLINE inline
+#endif
+//@}
+
+// Generic helper definitions for shared library support
+//@{
+#if defined _WIN32 || defined _WIN64 || defined __CYGWIN__
+#define ATTR_DLL_IMPORT __declspec(dllimport)
+#define ATTR_DLL_EXPORT __declspec(dllexport)
+#define ATTR_DLL_LOCAL
+#ifdef _WIN64
+#define ATTR_APIENTRY __stdcall
+#else
+#define ATTR_APIENTRY __cdecl
+#endif
+#else
+#if __GNUC__ >= 4
+#define ATTR_DLL_IMPORT __attribute__((visibility("default")))
+#define ATTR_DLL_EXPORT __attribute__((visibility("default")))
+#ifndef SWIG
+#define ATTR_DLL_LOCAL __attribute__((visibility("hidden")))
+#else
+#define ATTR_DLL_LOCAL
+#endif
+#else
+#define ATTR_DLL_IMPORT
+#define ATTR_DLL_EXPORT
+#define ATTR_DLL_LOCAL
+#endif
+#define ATTR_APIENTRY
+#endif
+
+#ifndef ATTR_APIENTRYP
+#define ATTR_APIENTRYP ATTR_APIENTRY*
+#endif
+//@}
+
+#ifdef _WIN32 // windows
+#if !defined(_SSIZE_T_DEFINED) && !defined(HAVE_SSIZE_T)
+typedef intptr_t ssize_t;
+#define _SSIZE_T_DEFINED
+#endif // !_SSIZE_T_DEFINED
+#ifndef SSIZE_MAX
+#define SSIZE_MAX INTPTR_MAX
+#endif // !SSIZE_MAX
+#else // Linux, Mac, FreeBSD
+#include <sys/types.h>
+#endif // TARGET_POSIX
+
+/*
+ * To have a on add-on and kodi itself handled string always on known size!
+ */
+#define ADDON_STANDARD_STRING_LENGTH 1024
+#define ADDON_STANDARD_STRING_LENGTH_SMALL 256
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef void* KODI_ADDON_HDL;
+ typedef void* KODI_ADDON_BACKEND_HDL;
+ typedef void* KODI_ADDON_INSTANCE_HDL;
+ typedef void* KODI_ADDON_INSTANCE_BACKEND_HDL;
+
+ // Hardware specific device context interface
+ typedef void* ADDON_HARDWARE_CONTEXT;
+
+ typedef void* KODI_ADDON_FUNC_DUMMY;
+
+ //============================================================================
+ /// @ingroup cpp_kodi_addon_addonbase_Defs
+ /// @defgroup cpp_kodi_addon_addonbase_Defs_ADDON_STATUS enum ADDON_STATUS
+ /// @brief <b>Return value of functions in @ref cpp_kodi_addon_addonbase "kodi::addon::CAddonBase"
+ /// and associated classes</b>\n
+ /// With this Kodi can do any follow-up work or add-on e.g. declare it as defective.
+ ///
+ ///@{
+ typedef enum ADDON_STATUS
+ {
+ /// @brief For everything OK and no error
+ ADDON_STATUS_OK,
+
+ /// @brief A needed connection was lost
+ ADDON_STATUS_LOST_CONNECTION,
+
+ /// @brief Addon needs a restart inside Kodi
+ ADDON_STATUS_NEED_RESTART,
+
+ /// @brief Necessary settings are not yet set
+ ADDON_STATUS_NEED_SETTINGS,
+
+ /// @brief Unknown and incomprehensible error
+ ADDON_STATUS_UNKNOWN,
+
+ /// @brief Permanent failure, like failing to resolve methods
+ ADDON_STATUS_PERMANENT_FAILURE,
+
+ /* internal used return error if function becomes not used from child on
+ * addon */
+ ADDON_STATUS_NOT_IMPLEMENTED
+ } ADDON_STATUS;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_Defs_ADDON_LOG enum ADDON_LOG
+ /// @ingroup cpp_kodi_Defs
+ /// @brief **Log file type definitions**\n
+ /// These define the types of log entries given with @ref kodi::Log() to Kodi.
+ ///
+ /// -------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/General.h>
+ ///
+ /// kodi::Log(ADDON_LOG_ERROR, "%s: There is an error occurred!", __func__);
+ ///
+ /// ~~~~~~~~~~~~~
+ ///
+ ///@{
+ typedef enum ADDON_LOG
+ {
+ /// @brief **0** : To include debug information in the log file.
+ ADDON_LOG_DEBUG = 0,
+
+ /// @brief **1** : To include information messages in the log file.
+ ADDON_LOG_INFO = 1,
+
+ /// @brief **2** : To write warnings in the log file.
+ ADDON_LOG_WARNING = 2,
+
+ /// @brief **3** : To report error messages in the log file.
+ ADDON_LOG_ERROR = 3,
+
+ /// @brief **4** : To notify fatal unrecoverable errors, which can may also indicate
+ /// upcoming crashes.
+ ADDON_LOG_FATAL = 4
+ } ADDON_LOG;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ typedef enum ADDON_STATUS(ATTR_APIENTRYP PFN_KODI_ADDON_INSTANCE_SETTING_CHANGE_STRING_V1)(
+ const KODI_ADDON_INSTANCE_HDL hdl, const char* name, const char* value);
+ typedef enum ADDON_STATUS(ATTR_APIENTRYP PFN_KODI_ADDON_INSTANCE_SETTING_CHANGE_BOOLEAN_V1)(
+ const KODI_ADDON_INSTANCE_HDL hdl, const char* name, bool value);
+ typedef enum ADDON_STATUS(ATTR_APIENTRYP PFN_KODI_ADDON_INSTANCE_SETTING_CHANGE_INTEGER_V1)(
+ const KODI_ADDON_INSTANCE_HDL hdl, const char* name, int value);
+ typedef enum ADDON_STATUS(ATTR_APIENTRYP PFN_KODI_ADDON_INSTANCE_SETTING_CHANGE_FLOAT_V1)(
+ const KODI_ADDON_INSTANCE_HDL hdl, const char* name, float value);
+
+ typedef struct KODI_ADDON_INSTANCE_FUNC
+ {
+ PFN_KODI_ADDON_INSTANCE_SETTING_CHANGE_STRING_V1 instance_setting_change_string;
+ PFN_KODI_ADDON_INSTANCE_SETTING_CHANGE_BOOLEAN_V1 instance_setting_change_boolean;
+ PFN_KODI_ADDON_INSTANCE_SETTING_CHANGE_INTEGER_V1 instance_setting_change_integer;
+ PFN_KODI_ADDON_INSTANCE_SETTING_CHANGE_FLOAT_V1 instance_setting_change_float;
+ } KODI_ADDON_INSTANCE_FUNC;
+
+ typedef struct KODI_ADDON_INSTANCE_FUNC_CB
+ {
+ char* (*get_instance_user_path)(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl);
+ bool (*is_instance_setting_using_default)(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id);
+
+ bool (*get_instance_setting_bool)(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ bool* value);
+ bool (*get_instance_setting_int)(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ int* value);
+ bool (*get_instance_setting_float)(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ float* value);
+ bool (*get_instance_setting_string)(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ char** value);
+
+ bool (*set_instance_setting_bool)(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ bool value);
+ bool (*set_instance_setting_int)(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ int value);
+ bool (*set_instance_setting_float)(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ float value);
+ bool (*set_instance_setting_string)(const KODI_ADDON_INSTANCE_BACKEND_HDL hdl,
+ const char* id,
+ const char* value);
+ } KODI_ADDON_INSTANCE_FUNC_CB;
+
+ typedef int KODI_ADDON_INSTANCE_TYPE;
+
+ typedef struct KODI_ADDON_INSTANCE_INFO
+ {
+ KODI_ADDON_INSTANCE_TYPE type;
+ uint32_t number;
+ const char* id;
+ const char* version;
+ KODI_ADDON_INSTANCE_BACKEND_HDL kodi;
+ KODI_ADDON_INSTANCE_HDL parent;
+ bool first_instance;
+
+ struct KODI_ADDON_INSTANCE_FUNC_CB* functions;
+ } KODI_ADDON_INSTANCE_INFO;
+
+ typedef struct KODI_ADDON_INSTANCE_STRUCT
+ {
+ const KODI_ADDON_INSTANCE_INFO* info;
+
+ KODI_ADDON_INSTANCE_HDL hdl;
+ struct KODI_ADDON_INSTANCE_FUNC* functions;
+ union {
+ KODI_ADDON_FUNC_DUMMY dummy;
+ struct AddonInstance_AudioDecoder* audiodecoder;
+ struct AddonInstance_AudioEncoder* audioencoder;
+ struct AddonInstance_ImageDecoder* imagedecoder;
+ struct AddonInstance_Game* game;
+ struct AddonInstance_InputStream* inputstream;
+ struct AddonInstance_Peripheral* peripheral;
+ struct AddonInstance_PVR* pvr;
+ struct AddonInstance_Screensaver* screensaver;
+ struct AddonInstance_VFSEntry* vfs;
+ struct AddonInstance_VideoCodec* videocodec;
+ struct AddonInstance_Visualization* visualization;
+ };
+ } KODI_ADDON_INSTANCE_STRUCT;
+
+ /*! @brief Standard undefined pointer handle */
+ typedef void* KODI_HANDLE;
+
+ typedef struct AddonToKodiFuncTable_kodi_addon
+ {
+ char* (*get_addon_path)(const KODI_ADDON_BACKEND_HDL hdl);
+ char* (*get_lib_path)(const KODI_ADDON_BACKEND_HDL hdl);
+ char* (*get_user_path)(const KODI_ADDON_BACKEND_HDL hdl);
+ char* (*get_temp_path)(const KODI_ADDON_BACKEND_HDL hdl);
+
+ char* (*get_localized_string)(const KODI_ADDON_BACKEND_HDL hdl, long label_id);
+
+ bool (*open_settings_dialog)(const KODI_ADDON_BACKEND_HDL hdl);
+ bool (*is_setting_using_default)(const KODI_ADDON_BACKEND_HDL hdl, const char* id);
+
+ bool (*get_setting_bool)(const KODI_ADDON_BACKEND_HDL hdl, const char* id, bool* value);
+ bool (*get_setting_int)(const KODI_ADDON_BACKEND_HDL hdl, const char* id, int* value);
+ bool (*get_setting_float)(const KODI_ADDON_BACKEND_HDL hdl, const char* id, float* value);
+ bool (*get_setting_string)(const KODI_ADDON_BACKEND_HDL hdl, const char* id, char** value);
+
+ bool (*set_setting_bool)(const KODI_ADDON_BACKEND_HDL hdl, const char* id, bool value);
+ bool (*set_setting_int)(const KODI_ADDON_BACKEND_HDL hdl, const char* id, int value);
+ bool (*set_setting_float)(const KODI_ADDON_BACKEND_HDL hdl, const char* id, float value);
+ bool (*set_setting_string)(const KODI_ADDON_BACKEND_HDL hdl, const char* id, const char* value);
+
+ char* (*get_addon_info)(const KODI_ADDON_BACKEND_HDL hdl, const char* id);
+
+ char* (*get_type_version)(const KODI_ADDON_BACKEND_HDL hdl, int type);
+ void* (*get_interface)(const KODI_ADDON_BACKEND_HDL hdl, const char* name, const char* version);
+ } AddonToKodiFuncTable_kodi_addon;
+
+ /*!
+ * @brief Callback function tables from addon to Kodi
+ * Set complete from Kodi!
+ */
+ typedef struct AddonToKodiFuncTable_Addon
+ {
+ // Pointer inside Kodi, used on callback functions to give related handle
+ // class, for this ADDON::CAddonDll inside Kodi.
+ KODI_ADDON_BACKEND_HDL kodiBase;
+
+ void (*free_string)(const KODI_ADDON_BACKEND_HDL hdl, char* str);
+ void (*free_string_array)(const KODI_ADDON_BACKEND_HDL hdl, char** arr, int numElements);
+ void (*addon_log_msg)(const KODI_ADDON_BACKEND_HDL hdl, const int loglevel, const char* msg);
+
+ struct AddonToKodiFuncTable_kodi* kodi;
+ struct AddonToKodiFuncTable_kodi_addon* kodi_addon;
+ struct AddonToKodiFuncTable_kodi_audioengine* kodi_audioengine;
+ struct AddonToKodiFuncTable_kodi_filesystem* kodi_filesystem;
+ struct AddonToKodiFuncTable_kodi_gui* kodi_gui;
+ struct AddonToKodiFuncTable_kodi_network* kodi_network;
+ } AddonToKodiFuncTable_Addon;
+
+ typedef ADDON_STATUS(ATTR_APIENTRYP PFN_KODI_ADDON_CREATE_V1)(
+ const KODI_ADDON_INSTANCE_BACKEND_HDL first_instance, KODI_ADDON_HDL* hdl);
+ typedef void(ATTR_APIENTRYP PFN_KODI_ADDON_DESTROY_V1)(const KODI_ADDON_HDL hdl);
+ typedef ADDON_STATUS(ATTR_APIENTRYP PFN_KODI_ADDON_CREATE_INSTANCE_V1)(
+ const KODI_ADDON_HDL hdl, struct KODI_ADDON_INSTANCE_STRUCT* instance);
+ typedef void(ATTR_APIENTRYP PFN_KODI_ADDON_DESTROY_INSTANCE_V1)(
+ const KODI_ADDON_HDL hdl, struct KODI_ADDON_INSTANCE_STRUCT* instance);
+ typedef enum ADDON_STATUS(ATTR_APIENTRYP PFN_KODI_ADDON_SETTING_CHANGE_STRING_V1)(
+ const KODI_ADDON_HDL hdl, const char* name, const char* value);
+ typedef enum ADDON_STATUS(ATTR_APIENTRYP PFN_KODI_ADDON_SETTING_CHANGE_BOOLEAN_V1)(
+ const KODI_ADDON_HDL hdl, const char* name, bool value);
+ typedef enum ADDON_STATUS(ATTR_APIENTRYP PFN_KODI_ADDON_SETTING_CHANGE_INTEGER_V1)(
+ const KODI_ADDON_HDL hdl, const char* name, int value);
+ typedef enum ADDON_STATUS(ATTR_APIENTRYP PFN_KODI_ADDON_SETTING_CHANGE_FLOAT_V1)(
+ const KODI_ADDON_HDL hdl, const char* name, float value);
+
+ /*!
+ * @brief Function tables from Kodi to addon
+ */
+ typedef struct KodiToAddonFuncTable_Addon
+ {
+ PFN_KODI_ADDON_CREATE_V1 create;
+ PFN_KODI_ADDON_DESTROY_V1 destroy;
+ PFN_KODI_ADDON_CREATE_INSTANCE_V1 create_instance;
+ PFN_KODI_ADDON_DESTROY_INSTANCE_V1 destroy_instance;
+ PFN_KODI_ADDON_SETTING_CHANGE_STRING_V1 setting_change_string;
+ PFN_KODI_ADDON_SETTING_CHANGE_BOOLEAN_V1 setting_change_boolean;
+ PFN_KODI_ADDON_SETTING_CHANGE_INTEGER_V1 setting_change_integer;
+ PFN_KODI_ADDON_SETTING_CHANGE_FLOAT_V1 setting_change_float;
+ } KodiToAddonFuncTable_Addon;
+
+ /*!
+ * @brief Main structure passed from kodi to addon with basic information needed to
+ * create add-on.
+ */
+ typedef struct AddonGlobalInterface
+ {
+ // Pointer of first created instance, used in case this add-on goes with single way
+ // Set from Kodi!
+ struct KODI_ADDON_INSTANCE_STRUCT* firstKodiInstance;
+
+ // Pointer to master base class inside add-on
+ // Set from addon header (kodi::addon::CAddonBase)!
+ KODI_ADDON_HDL addonBase;
+
+ // Pointer to a instance used on single way (together with this class)
+ // Set from addon header (kodi::addon::IAddonInstance)!
+ KODI_ADDON_INSTANCE_HDL globalSingleInstance;
+
+ // Callback function tables from addon to Kodi
+ // Set from Kodi!
+ AddonToKodiFuncTable_Addon* toKodi;
+
+ // Function tables from Kodi to addon
+ // Set from addon header!
+ KodiToAddonFuncTable_Addon* toAddon;
+ } AddonGlobalInterface;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_ADDON_BASE_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/audio_engine.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/audio_engine.h
new file mode 100644
index 0000000..dcb004c
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/audio_engine.h
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2005-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.
+ */
+
+#ifndef C_API_AUDIO_ENGINE_H
+#define C_API_AUDIO_ENGINE_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+ // "C" Definitions, structures and enumerators of audio engine
+ //{{{
+
+ //============================================================================
+ /// @defgroup cpp_kodi_audioengine_Defs_AudioEngineStreamOptions enum AudioEngineStreamOptions
+ /// @ingroup cpp_kodi_audioengine_Defs
+ /// @brief **Bit options to pass to CAEStream**\n
+ /// A bit field of stream options.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Usage example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// // Here only as minimal, "format" must be set to wanted types
+ /// kodi::audioengine::AudioEngineFormat format;
+ /// m_audioengine = new kodi::audioengine::CAEStream(format, AUDIO_STREAM_FORCE_RESAMPLE | AUDIO_STREAM_AUTOSTART);
+ /// ~~~~~~~~~~~~~
+ ///
+ ///@{
+ typedef enum AudioEngineStreamOptions
+ {
+ /// force resample even if rates match
+ AUDIO_STREAM_FORCE_RESAMPLE = 1 << 0,
+ /// create the stream paused
+ AUDIO_STREAM_PAUSED = 1 << 1,
+ /// autostart the stream when enough data is buffered
+ AUDIO_STREAM_AUTOSTART = 1 << 2,
+ } AudioEngineStreamOptions;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_audioengine_Defs_AudioEngineChannel enum AudioEngineChannel
+ /// @ingroup cpp_kodi_audioengine_Defs
+ /// @brief **The possible channels**\n
+ /// Used to set available or used channels on stream.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Usage example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::audioengine::AudioEngineFormat format;
+ /// format.SetChannelLayout(std::vector<AudioEngineChannel>(AUDIOENGINE_CH_FL, AUDIOENGINE_CH_FR));
+ /// ~~~~~~~~~~~~~
+ ///
+ ///@{
+ enum AudioEngineChannel
+ {
+ /// Used inside to indicate the end of a list and not for addon use directly.
+ AUDIOENGINE_CH_NULL = -1,
+ /// RAW Audio format
+ AUDIOENGINE_CH_RAW,
+ /// Front left
+ AUDIOENGINE_CH_FL,
+ /// Front right
+ AUDIOENGINE_CH_FR,
+ /// Front center
+ AUDIOENGINE_CH_FC,
+ /// LFE / Subwoofer
+ AUDIOENGINE_CH_LFE,
+ /// Back left
+ AUDIOENGINE_CH_BL,
+ /// Back right
+ AUDIOENGINE_CH_BR,
+ /// Front left over center
+ AUDIOENGINE_CH_FLOC,
+ /// Front right over center
+ AUDIOENGINE_CH_FROC,
+ /// Back center
+ AUDIOENGINE_CH_BC,
+ /// Side left
+ AUDIOENGINE_CH_SL,
+ /// Side right
+ AUDIOENGINE_CH_SR,
+ /// Top front left
+ AUDIOENGINE_CH_TFL,
+ /// Top front right
+ AUDIOENGINE_CH_TFR,
+ /// Top front center
+ AUDIOENGINE_CH_TFC,
+ /// Top center
+ AUDIOENGINE_CH_TC,
+ /// Top back left
+ AUDIOENGINE_CH_TBL,
+ /// Top back right
+ AUDIOENGINE_CH_TBR,
+ /// Top back center
+ AUDIOENGINE_CH_TBC,
+ /// Back left over center
+ AUDIOENGINE_CH_BLOC,
+ /// Back right over center
+ AUDIOENGINE_CH_BROC,
+ /// Maximum possible value, to use e.g. as size inside list
+ AUDIOENGINE_CH_MAX
+ };
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_audioengine_Defs_AudioEngineDataFormat enum AudioEngineDataFormat
+ /// @ingroup cpp_kodi_audioengine_Defs
+ /// @brief **Audio sample formats**\n
+ /// The bit layout of the audio data.
+ ///
+ /// LE = Little Endian, BE = Big Endian, NE = Native Endian
+ ///
+ /// For planar sample formats, each audio channel is in a separate data plane,
+ /// and linesize is the buffer size, in bytes, for a single plane. All data
+ /// planes must be the same size. For packed sample formats, only the first
+ /// data plane is used, and samples for each channel are interleaved. In this
+ /// case, linesize is the buffer size, in bytes, for the 1 plane.
+ ///
+ /// @note This is ordered from the worst to best preferred formats
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Usage example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// kodi::audioengine::AudioEngineFormat format;
+ /// format.SetDataFormat(AUDIOENGINE_FMT_FLOATP);
+ /// ~~~~~~~~~~~~~
+ ///
+ ///@{
+ enum AudioEngineDataFormat
+ {
+ /// To define format as invalid
+ AUDIOENGINE_FMT_INVALID = -1,
+
+ /// Unsigned integer 8 bit
+ AUDIOENGINE_FMT_U8,
+
+ /// Big Endian signed integer 16 bit
+ AUDIOENGINE_FMT_S16BE,
+ /// Little Endian signed integer 16 bit
+ AUDIOENGINE_FMT_S16LE,
+ /// Native Endian signed integer 16 bit
+ AUDIOENGINE_FMT_S16NE,
+
+ /// Big Endian signed integer 32 bit
+ AUDIOENGINE_FMT_S32BE,
+ /// Little Endian signed integer 32 bit
+ AUDIOENGINE_FMT_S32LE,
+ /// Native Endian signed integer 32 bit
+ AUDIOENGINE_FMT_S32NE,
+
+ /// Big Endian signed integer 24 bit (in 4 bytes)
+ AUDIOENGINE_FMT_S24BE4,
+ /// Little Endian signed integer 24 bit (in 4 bytes)
+ AUDIOENGINE_FMT_S24LE4,
+ /// Native Endian signed integer 24 bit (in 4 bytes)
+ AUDIOENGINE_FMT_S24NE4,
+ /// S32 with bits_per_sample < 32
+ AUDIOENGINE_FMT_S24NE4MSB,
+
+ /// Big Endian signed integer 24 bit (3 bytes)
+ AUDIOENGINE_FMT_S24BE3,
+ /// Little Endian signed integer 24 bit (3 bytes)
+ AUDIOENGINE_FMT_S24LE3,
+ /// Native Endian signed integer 24 bit (3 bytes)
+ AUDIOENGINE_FMT_S24NE3,
+
+ /// Double floating point
+ AUDIOENGINE_FMT_DOUBLE,
+ /// Floating point
+ AUDIOENGINE_FMT_FLOAT,
+
+ /// **Bitstream**\n
+ /// RAW Audio format
+ AUDIOENGINE_FMT_RAW,
+
+ /// **Planar format**\n
+ /// Unsigned byte
+ AUDIOENGINE_FMT_U8P,
+ /// **Planar format**\n
+ /// Native Endian signed 16 bit
+ AUDIOENGINE_FMT_S16NEP,
+ /// **Planar format**\n
+ /// Native Endian signed 32 bit
+ AUDIOENGINE_FMT_S32NEP,
+ /// **Planar format**\n
+ /// Native Endian signed integer 24 bit (in 4 bytes)
+ AUDIOENGINE_FMT_S24NE4P,
+ /// **Planar format**\n
+ /// S32 with bits_per_sample < 32
+ AUDIOENGINE_FMT_S24NE4MSBP,
+ /// **Planar format**\n
+ /// Native Endian signed integer 24 bit (in 3 bytes)
+ AUDIOENGINE_FMT_S24NE3P,
+ /// **Planar format**\n
+ /// Double floating point
+ AUDIOENGINE_FMT_DOUBLEP,
+ /// **Planar format**\n
+ /// Floating point
+ AUDIOENGINE_FMT_FLOATP,
+
+ /// Amount of sample formats.
+ AUDIOENGINE_FMT_MAX
+ };
+ ///@}
+ //----------------------------------------------------------------------------
+
+ /*!
+ * @brief Internal API structure which are used for data exchange between
+ * Kodi and addon.
+ */
+ struct AUDIO_ENGINE_FORMAT
+ {
+ /*! The stream's data format (eg, AUDIOENGINE_FMT_S16LE) */
+ enum AudioEngineDataFormat m_dataFormat;
+
+ /*! The stream's sample rate (eg, 48000) */
+ unsigned int m_sampleRate;
+
+ /*! The encoded streams sample rate if a bitstream, otherwise undefined */
+ unsigned int m_encodedRate;
+
+ /*! The amount of used speaker channels */
+ unsigned int m_channelCount;
+
+ /*! The stream's channel layout */
+ enum AudioEngineChannel m_channels[AUDIOENGINE_CH_MAX];
+
+ /*! The number of frames per period */
+ unsigned int m_frames;
+
+ /*! The size of one frame in bytes */
+ unsigned int m_frameSize;
+ };
+
+ /* A stream handle pointer, which is only used internally by the addon stream handle */
+ typedef void AEStreamHandle;
+
+ //}}}
+
+ //¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+ // "C" Internal interface tables for intercommunications between addon and kodi
+ //{{{
+
+ /*
+ * Function address structure, not need to visible on dev kit doxygen
+ * documentation
+ */
+ typedef struct AddonToKodiFuncTable_kodi_audioengine
+ {
+ AEStreamHandle* (*make_stream)(void* kodiBase,
+ struct AUDIO_ENGINE_FORMAT* format,
+ unsigned int options);
+ void (*free_stream)(void* kodiBase, AEStreamHandle* stream);
+ bool (*get_current_sink_format)(void* kodiBase, struct AUDIO_ENGINE_FORMAT* sink_format);
+
+ // Audio Engine Stream definitions
+ unsigned int (*aestream_get_space)(void* kodiBase, AEStreamHandle* handle);
+ unsigned int (*aestream_add_data)(void* kodiBase,
+ AEStreamHandle* handle,
+ uint8_t* const* data,
+ unsigned int offset,
+ unsigned int frames,
+ double pts,
+ bool hasDownmix,
+ double centerMixLevel);
+ double (*aestream_get_delay)(void* kodiBase, AEStreamHandle* handle);
+ bool (*aestream_is_buffering)(void* kodiBase, AEStreamHandle* handle);
+ double (*aestream_get_cache_time)(void* kodiBase, AEStreamHandle* handle);
+ double (*aestream_get_cache_total)(void* kodiBase, AEStreamHandle* handle);
+ void (*aestream_pause)(void* kodiBase, AEStreamHandle* handle);
+ void (*aestream_resume)(void* kodiBase, AEStreamHandle* handle);
+ void (*aestream_drain)(void* kodiBase, AEStreamHandle* handle, bool wait);
+ bool (*aestream_is_draining)(void* kodiBase, AEStreamHandle* handle);
+ bool (*aestream_is_drained)(void* kodiBase, AEStreamHandle* handle);
+ void (*aestream_flush)(void* kodiBase, AEStreamHandle* handle);
+ float (*aestream_get_volume)(void* kodiBase, AEStreamHandle* handle);
+ void (*aestream_set_volume)(void* kodiBase, AEStreamHandle* handle, float volume);
+ float (*aestream_get_amplification)(void* kodiBase, AEStreamHandle* handle);
+ void (*aestream_set_amplification)(void* kodiBase, AEStreamHandle* handle, float amplify);
+ unsigned int (*aestream_get_frame_size)(void* kodiBase, AEStreamHandle* handle);
+ unsigned int (*aestream_get_channel_count)(void* kodiBase, AEStreamHandle* handle);
+ unsigned int (*aestream_get_sample_rate)(void* kodiBase, AEStreamHandle* handle);
+ enum AudioEngineDataFormat (*aestream_get_data_format)(void* kodiBase, AEStreamHandle* handle);
+ double (*aestream_get_resample_ratio)(void* kodiBase, AEStreamHandle* handle);
+ void (*aestream_set_resample_ratio)(void* kodiBase, AEStreamHandle* handle, double ratio);
+ } AddonToKodiFuncTable_kodi_audioengine;
+
+ //}}}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_AUDIO_ENGINE_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/filesystem.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/filesystem.h
new file mode 100644
index 0000000..aedcf64
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/filesystem.h
@@ -0,0 +1,322 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_FILESYSTEM_H
+#define C_API_FILESYSTEM_H
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <time.h>
+
+#ifdef _WIN32 // windows
+#ifndef _SSIZE_T_DEFINED
+typedef intptr_t ssize_t;
+#define _SSIZE_T_DEFINED
+#endif // !_SSIZE_T_DEFINED
+
+// Prevent conflicts with Windows macros where have this names.
+#ifdef CreateDirectory
+#undef CreateDirectory
+#endif // CreateDirectory
+#ifdef DeleteFile
+#undef DeleteFile
+#endif // DeleteFile
+#ifdef RemoveDirectory
+#undef RemoveDirectory
+#endif // RemoveDirectory
+#endif // _WIN32
+
+#ifdef TARGET_POSIX // Linux, Mac, FreeBSD
+#include <sys/types.h>
+#endif // TARGET_POSIX
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+ // "C" Definitions, structures and enumerators of filesystem
+ //{{{
+
+ //============================================================================
+ /// @defgroup cpp_kodi_vfs_Defs_OpenFileFlags enum OpenFileFlags
+ /// @ingroup cpp_kodi_vfs_Defs
+ /// @brief **Flags to define way how file becomes opened**\n
+ /// The values can be used together, e.g. <b>`file.Open("myfile", ADDON_READ_TRUNCATED | ADDON_READ_CHUNKED);`</b>
+ ///
+ /// Used on @ref kodi::vfs::CFile::OpenFile().
+ ///
+ ///@{
+ typedef enum OpenFileFlags
+ {
+ /// @brief **0000 0000 0001** :\n
+ /// Indicate that caller can handle truncated reads, where function
+ /// returns before entire buffer has been filled.
+ ADDON_READ_TRUNCATED = 0x01,
+
+ /// @brief **0000 0000 0010** :\n
+ /// Indicate that that caller support read in the minimum defined
+ /// chunk size, this disables internal cache then.
+ ADDON_READ_CHUNKED = 0x02,
+
+ /// @brief **0000 0000 0100** :\n
+ /// Use cache to access this file.
+ ADDON_READ_CACHED = 0x04,
+
+ /// @brief **0000 0000 1000** :\n
+ /// Open without caching. regardless to file type.
+ ADDON_READ_NO_CACHE = 0x08,
+
+ /// @brief **0000 0001 0000** :\n
+ /// Calculate bitrate for file while reading.
+ ADDON_READ_BITRATE = 0x10,
+
+ /// @brief **0000 0010 0000** :\n
+ /// Indicate to the caller we will seek between multiple streams in
+ /// the file frequently.
+ ADDON_READ_MULTI_STREAM = 0x20,
+
+ /// @brief **0000 0100 0000** :\n
+ /// indicate to the caller file is audio and/or video (and e.g. may
+ /// grow).
+ ADDON_READ_AUDIO_VIDEO = 0x40,
+
+ /// @brief **0000 1000 0000** :\n
+ /// Indicate that caller will do write operations before reading.
+ ADDON_READ_AFTER_WRITE = 0x80,
+
+ /// @brief **0001 0000 0000** :\n
+ /// Indicate that caller want to reopen a file if its already open.
+ ADDON_READ_REOPEN = 0x100
+ } OpenFileFlags;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_vfs_Defs_CURLOptiontype enum CURLOptiontype
+ /// @ingroup cpp_kodi_vfs_Defs
+ /// @brief **CURL message types**\n
+ /// Used on kodi::vfs::CFile::CURLAddOption().
+ ///
+ ///@{
+ typedef enum CURLOptiontype
+ {
+ /// @brief Set a general option.
+ ADDON_CURL_OPTION_OPTION,
+
+ /// @brief Set a protocol option.\n
+ ///\n
+ /// The following names for *ADDON_CURL_OPTION_PROTOCOL* are possible:
+ /// | Option name | Description
+ /// |------------------------------------:|:--------------------------------
+ /// | <b>`accept-charset`</b> | Set the "accept-charset" header
+ /// | <b>`acceptencoding or encoding`</b> | Set the "accept-encoding" header
+ /// | <b>`active-remote`</b> | Set the "active-remote" header
+ /// | <b>`auth`</b> | Set the authentication method. Possible values: any, anysafe, digest, ntlm
+ /// | <b>`connection-timeout`</b> | Set the connection timeout in seconds
+ /// | <b>`cookie`</b> | Set the "cookie" header
+ /// | <b>`customrequest`</b> | Set a custom HTTP request like DELETE
+ /// | <b>`noshout`</b> | Set to true if kodi detects a stream as shoutcast by mistake.
+ /// | <b>`postdata`</b> | Set the post body (value needs to be base64 encoded). (Implicitly sets the request to POST)
+ /// | <b>`referer`</b> | Set the "referer" header
+ /// | <b>`user-agent`</b> | Set the "user-agent" header
+ /// | <b>`seekable`</b> | Set the stream seekable. 1: enable, 0: disable
+ /// | <b>`sslcipherlist`</b> | Set list of accepted SSL ciphers.
+ ///
+ ADDON_CURL_OPTION_PROTOCOL,
+
+ /// @brief Set User and password
+ ADDON_CURL_OPTION_CREDENTIALS,
+
+ /// @brief Add a Header
+ ADDON_CURL_OPTION_HEADER
+ } CURLOptiontype;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_vfs_Defs_FilePropertyTypes enum FilePropertyTypes
+ /// @ingroup cpp_kodi_vfs_Defs
+ /// @brief **File property types**\n
+ /// Mostly to read internet sources.
+ ///
+ /// Used on kodi::vfs::CFile::GetPropertyValue() and kodi::vfs::CFile::GetPropertyValues().
+ ///
+ ///@{
+ typedef enum FilePropertyTypes
+ {
+ /// @brief Get protocol response line.
+ ADDON_FILE_PROPERTY_RESPONSE_PROTOCOL,
+ /// @brief Get a response header.
+ ADDON_FILE_PROPERTY_RESPONSE_HEADER,
+ /// @brief Get file content type.
+ ADDON_FILE_PROPERTY_CONTENT_TYPE,
+ /// @brief Get file content charset.
+ ADDON_FILE_PROPERTY_CONTENT_CHARSET,
+ /// @brief Get file mime type.
+ ADDON_FILE_PROPERTY_MIME_TYPE,
+ /// @brief Get file effective URL (last one if redirected).
+ ADDON_FILE_PROPERTY_EFFECTIVE_URL
+ } FilePropertyTypes;
+ ///@}
+ //----------------------------------------------------------------------------
+
+ //}}}
+
+ //¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+ // "C" Internal interface tables for intercommunications between addon and kodi
+ //{{{
+
+ struct KODI_HTTP_HEADER
+ {
+ void* handle;
+
+ char* (*get_value)(void* kodiBase, void* handle, const char* param);
+ char** (*get_values)(void* kodiBase, void* handle, const char* param, int* length);
+ char* (*get_header)(void* kodiBase, void* handle);
+ char* (*get_mime_type)(void* kodiBase, void* handle);
+ char* (*get_charset)(void* kodiBase, void* handle);
+ char* (*get_proto_line)(void* kodiBase, void* handle);
+ };
+
+ struct STAT_STRUCTURE
+ {
+ /// ID of device containing file
+ uint32_t deviceId;
+ /// Total size, in bytes
+ uint64_t size;
+ /// Time of last access
+ time_t accessTime;
+ /// Time of last modification
+ time_t modificationTime;
+ /// Time of last status change
+ time_t statusTime;
+ /// The stat url is a directory
+ bool isDirectory;
+ /// The stat url is a symbolic link
+ bool isSymLink;
+ /// The stat url is block special
+ bool isBlock;
+ /// The stat url is character special
+ bool isCharacter;
+ /// The stat url is FIFO special
+ bool isFifo;
+ /// The stat url is regular
+ bool isRegular;
+ /// The stat url is socket
+ bool isSocket;
+ /// The file serial number, which distinguishes this file from all other files on the same
+ /// device.
+ uint64_t fileSerialNumber;
+ };
+
+ struct VFS_CACHE_STATUS_DATA
+ {
+ uint64_t forward;
+ uint32_t maxrate;
+ uint32_t currate;
+ uint32_t lowrate;
+ };
+
+ struct VFSProperty
+ {
+ char* name;
+ char* val;
+ };
+
+ struct VFSDirEntry
+ {
+ char* label; //!< item label
+ char* title; //!< item title
+ char* path; //!< item path
+ unsigned int num_props; //!< Number of properties attached to item
+ struct VFSProperty* properties; //!< Properties
+ time_t date_time; //!< file creation date & time
+ bool folder; //!< Item is a folder
+ uint64_t size; //!< Size of file represented by item
+ };
+
+ typedef struct AddonToKodiFuncTable_kodi_filesystem
+ {
+ bool (*can_open_directory)(void* kodiBase, const char* url);
+ bool (*create_directory)(void* kodiBase, const char* path);
+ bool (*remove_directory)(void* kodiBase, const char* path);
+ bool (*directory_exists)(void* kodiBase, const char* path);
+ bool (*get_directory)(void* kodiBase,
+ const char* path,
+ const char* mask,
+ struct VFSDirEntry** items,
+ unsigned int* num_items);
+ void (*free_directory)(void* kodiBase, struct VFSDirEntry* items, unsigned int num_items);
+
+ bool (*file_exists)(void* kodiBase, const char* filename, bool useCache);
+ bool (*stat_file)(void* kodiBase, const char* filename, struct STAT_STRUCTURE* buffer);
+ bool (*delete_file)(void* kodiBase, const char* filename);
+ bool (*rename_file)(void* kodiBase, const char* filename, const char* newFileName);
+ bool (*copy_file)(void* kodiBase, const char* filename, const char* dest);
+
+ char* (*get_file_md5)(void* kodiBase, const char* filename);
+ char* (*get_cache_thumb_name)(void* kodiBase, const char* filename);
+ char* (*make_legal_filename)(void* kodiBase, const char* filename);
+ char* (*make_legal_path)(void* kodiBase, const char* path);
+ char* (*translate_special_protocol)(void* kodiBase, const char* strSource);
+ bool (*is_internet_stream)(void* kodiBase, const char* path, bool strictCheck);
+ bool (*is_on_lan)(void* kodiBase, const char* path);
+ bool (*is_remote)(void* kodiBase, const char* path);
+ bool (*is_local)(void* kodiBase, const char* path);
+ bool (*is_url)(void* kodiBase, const char* path);
+ bool (*get_http_header)(void* kodiBase, const char* url, struct KODI_HTTP_HEADER* headers);
+ bool (*get_mime_type)(void* kodiBase, const char* url, char** content, const char* useragent);
+ bool (*get_content_type)(void* kodiBase,
+ const char* url,
+ char** content,
+ const char* useragent);
+ bool (*get_cookies)(void* kodiBase, const char* url, char** cookies);
+ bool (*http_header_create)(void* kodiBase, struct KODI_HTTP_HEADER* headers);
+ void (*http_header_free)(void* kodiBase, struct KODI_HTTP_HEADER* headers);
+
+ void* (*open_file)(void* kodiBase, const char* filename, unsigned int flags);
+ void* (*open_file_for_write)(void* kodiBase, const char* filename, bool overwrite);
+ ssize_t (*read_file)(void* kodiBase, void* file, void* ptr, size_t size);
+ bool (*read_file_string)(void* kodiBase, void* file, char* szLine, int iLineLength);
+ ssize_t (*write_file)(void* kodiBase, void* file, const void* ptr, size_t size);
+ void (*flush_file)(void* kodiBase, void* file);
+ int64_t (*seek_file)(void* kodiBase, void* file, int64_t position, int whence);
+ int (*truncate_file)(void* kodiBase, void* file, int64_t size);
+ int64_t (*get_file_position)(void* kodiBase, void* file);
+ int64_t (*get_file_length)(void* kodiBase, void* file);
+ double (*get_file_download_speed)(void* kodiBase, void* file);
+ void (*close_file)(void* kodiBase, void* file);
+ int (*get_file_chunk_size)(void* kodiBase, void* file);
+ bool (*io_control_get_seek_possible)(void* kodiBase, void* file);
+ bool (*io_control_get_cache_status)(void* kodiBase,
+ void* file,
+ struct VFS_CACHE_STATUS_DATA* status);
+ bool (*io_control_set_cache_rate)(void* kodiBase, void* file, uint32_t rate);
+ bool (*io_control_set_retry)(void* kodiBase, void* file, bool retry);
+ char** (*get_property_values)(
+ void* kodiBase, void* file, int type, const char* name, int* numValues);
+
+ void* (*curl_create)(void* kodiBase, const char* url);
+ bool (*curl_add_option)(
+ void* kodiBase, void* file, int type, const char* name, const char* value);
+ bool (*curl_open)(void* kodiBase, void* file, unsigned int flags);
+
+ bool (*get_disk_space)(
+ void* kodiBase, const char* path, uint64_t* capacity, uint64_t* free, uint64_t* available);
+ bool (*remove_directory_recursive)(void* kodiBase, const char* path);
+ } AddonToKodiFuncTable_kodi_filesystem;
+
+ //}}}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_FILESYSTEM_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/general.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/general.h
new file mode 100644
index 0000000..2188db9
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/general.h
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_GENERAL_H
+#define C_API_GENERAL_H
+
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// \ingroup cpp_kodi_Defs
+ /// @brief For kodi::CurrentKeyboardLayout used defines
+ ///
+ typedef enum StdKbButtons
+ {
+ /// The quantity of buttons per row on Kodi's standard keyboard
+ STD_KB_BUTTONS_PER_ROW = 20,
+ /// The quantity of rows on Kodi's standard keyboard
+ STD_KB_BUTTONS_MAX_ROWS = 4,
+ /// Keyboard layout type, this for initial standard
+ STD_KB_MODIFIER_KEY_NONE = 0x00,
+ /// Keyboard layout type, this for shift controlled layout (uppercase)
+ STD_KB_MODIFIER_KEY_SHIFT = 0x01,
+ /// Keyboard layout type, this to show symbols
+ STD_KB_MODIFIER_KEY_SYMBOL = 0x02
+ } StdKbButtons;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// \ingroup cpp_kodi_Defs
+ /// @brief For kodi::QueueNotification() used message types
+ ///
+ typedef enum QueueMsg
+ {
+ /// Show info notification message
+ QUEUE_INFO,
+ /// Show warning notification message
+ QUEUE_WARNING,
+ /// Show error notification message
+ QUEUE_ERROR,
+ /// Show with own given image and parts if set on values
+ QUEUE_OWN_STYLE
+ } QueueMsg;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// \ingroup cpp_kodi_Defs
+ /// @brief Format codes to get string from them.
+ ///
+ /// Used on kodi::GetLanguage().
+ ///
+ typedef enum LangFormats
+ {
+ /// two letter code as defined in ISO 639-1
+ LANG_FMT_ISO_639_1,
+ /// three letter code as defined in ISO 639-2/T or ISO 639-2/B
+ LANG_FMT_ISO_639_2,
+ /// full language name in English
+ LANG_FMT_ENGLISH_NAME
+ } LangFormats;
+ //----------------------------------------------------------------------------
+
+ /*
+ * For interface between add-on and kodi.
+ *
+ * This structure defines the addresses of functions stored inside Kodi which
+ * are then available for the add-on to call
+ *
+ * All function pointers there are used by the C++ interface functions below.
+ * You find the set of them on xbmc/addons/interfaces/General.cpp
+ *
+ * Note: For add-on development itself this is not needed
+ */
+ typedef struct AddonKeyboardKeyTable
+ {
+ char* keys[STD_KB_BUTTONS_MAX_ROWS][STD_KB_BUTTONS_PER_ROW];
+ } AddonKeyboardKeyTable;
+ typedef struct AddonToKodiFuncTable_kodi
+ {
+ char* (*unknown_to_utf8)(void* kodiBase, const char* source, bool* ret, bool failOnBadChar);
+ char* (*get_language)(void* kodiBase, int format, bool region);
+ bool (*queue_notification)(void* kodiBase,
+ int type,
+ const char* header,
+ const char* message,
+ const char* imageFile,
+ unsigned int displayTime,
+ bool withSound,
+ unsigned int messageTime);
+ void (*get_md5)(void* kodiBase, const char* text, char* md5);
+ char* (*get_region)(void* kodiBase, const char* id);
+ void (*get_free_mem)(void* kodiBase, long* free, long* total, bool as_bytes);
+ int (*get_global_idle_time)(void* kodiBase);
+ bool (*is_addon_avilable)(void* kodiBase, const char* id, char** version, bool* enabled);
+ void (*kodi_version)(void* kodiBase,
+ char** compile_name,
+ int* major,
+ int* minor,
+ char** revision,
+ char** tag,
+ char** tagversion);
+ char* (*get_current_skin_id)(void* kodiBase);
+ bool (*get_keyboard_layout)(void* kodiBase,
+ char** layout_name,
+ int modifier_key,
+ struct AddonKeyboardKeyTable* layout);
+ bool (*change_keyboard_layout)(void* kodiBase, char** layout_name);
+ } AddonToKodiFuncTable_kodi;
+
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GENERAL_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/CMakeLists.txt
new file mode 100644
index 0000000..0971ed5
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/CMakeLists.txt
@@ -0,0 +1,13 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ definitions.h
+ general.h
+ list_item.h
+ window.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_c-api_gui)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/CMakeLists.txt
new file mode 100644
index 0000000..853eb2d
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/CMakeLists.txt
@@ -0,0 +1,21 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ button.h
+ edit.h
+ fade_label.h
+ image.h
+ label.h
+ progress.h
+ radio_button.h
+ rendering.h
+ settings_slider.h
+ slider.h
+ spin.h
+ text_box.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_c-api_gui_controls)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/button.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/button.h
new file mode 100644
index 0000000..46de262
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/button.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_BUTTON_H
+#define C_API_GUI_CONTROLS_BUTTON_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_button
+ {
+ void (*set_visible)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ void (*set_enabled)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+ void (*set_label)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* label);
+ char* (*get_label)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_label2)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* label);
+ char* (*get_label2)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ } AddonToKodiFuncTable_kodi_gui_control_button;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_BUTTON_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/edit.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/edit.h
new file mode 100644
index 0000000..571c7ce
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/edit.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_EDIT_H
+#define C_API_GUI_CONTROLS_EDIT_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit_Defs
+ /// @{
+ /// @anchor AddonGUIInputType
+ /// @brief Text input types used on kodi::gui::controls::CEdit
+ enum AddonGUIInputType
+ {
+ /// Text inside edit control only readable
+ ADDON_INPUT_TYPE_READONLY = -1,
+ /// Normal text entries
+ ADDON_INPUT_TYPE_TEXT = 0,
+ /// To use on edit control only numeric numbers
+ ADDON_INPUT_TYPE_NUMBER,
+ /// To insert seconds
+ ADDON_INPUT_TYPE_SECONDS,
+ /// To insert time
+ ADDON_INPUT_TYPE_TIME,
+ /// To insert a date
+ ADDON_INPUT_TYPE_DATE,
+ /// Used for write in IP addresses
+ ADDON_INPUT_TYPE_IPADDRESS,
+ /// Text field used as password entry field with not visible text
+ ADDON_INPUT_TYPE_PASSWORD,
+ /// Text field used as password entry field with not visible text but
+ /// returned as MD5 value
+ ADDON_INPUT_TYPE_PASSWORD_MD5,
+ /// Use text field for search purpose
+ ADDON_INPUT_TYPE_SEARCH,
+ /// Text field as filter
+ ADDON_INPUT_TYPE_FILTER,
+ ///
+ ADDON_INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW
+ };
+ /// @}
+ //----------------------------------------------------------------------------
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_edit
+ {
+ void (*set_visible)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ void (*set_enabled)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+ void (*set_label)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* label);
+ char* (*get_label)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_text)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* text);
+ char* (*get_text)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_cursor_position)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ unsigned int position);
+ unsigned int (*get_cursor_position)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_input_type)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ int type,
+ const char* heading);
+ } AddonToKodiFuncTable_kodi_gui_control_edit;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_EDIT_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/fade_label.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/fade_label.h
new file mode 100644
index 0000000..dab4574
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/fade_label.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_FADE_LABEL_H
+#define C_API_GUI_CONTROLS_FADE_LABEL_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_fade_label
+ {
+ void (*set_visible)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ void (*add_label)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* text);
+ char* (*get_label)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_scrolling)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool scroll);
+ void (*reset)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ } AddonToKodiFuncTable_kodi_gui_control_fade_label;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_FADE_LABEL_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/image.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/image.h
new file mode 100644
index 0000000..257dd67
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/image.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_IMAGE_H
+#define C_API_GUI_CONTROLS_IMAGE_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_image
+ {
+ void (*set_visible)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ void (*set_filename)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* filename,
+ bool use_cache);
+ void (*set_color_diffuse)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ uint32_t color_diffuse);
+ } AddonToKodiFuncTable_kodi_gui_control_image;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_IMAGE_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/label.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/label.h
new file mode 100644
index 0000000..92af25c
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/label.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_LABEL_H
+#define C_API_GUI_CONTROLS_LABEL_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_label
+ {
+ void (*set_visible)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ void (*set_label)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* text);
+ char* (*get_label)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ } AddonToKodiFuncTable_kodi_gui_control_label;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_LABEL_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/progress.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/progress.h
new file mode 100644
index 0000000..4f211fd
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/progress.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_PROGRESS_H
+#define C_API_GUI_CONTROLS_PROGRESS_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_progress
+ {
+ void (*set_visible)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ void (*set_percentage)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float percent);
+ float (*get_percentage)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ } AddonToKodiFuncTable_kodi_gui_control_progress;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_PROGRESS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/radio_button.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/radio_button.h
new file mode 100644
index 0000000..d652e67
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/radio_button.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_RADIO_BUTTON_H
+#define C_API_GUI_CONTROLS_RADIO_BUTTON_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_radio_button
+ {
+ void (*set_visible)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ void (*set_enabled)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+ void (*set_label)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* text);
+ char* (*get_label)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_selected)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool selected);
+ bool (*is_selected)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ } AddonToKodiFuncTable_kodi_gui_control_radio_button;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_RADIO_BUTTON_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/rendering.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/rendering.h
new file mode 100644
index 0000000..792a8d6
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/rendering.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_RENDERING_H
+#define C_API_GUI_CONTROLS_RENDERING_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_rendering
+ {
+ void (*set_callbacks)(
+ KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ KODI_GUI_CLIENT_HANDLE clienthandle,
+ bool (*createCB)(KODI_GUI_CLIENT_HANDLE, int, int, int, int, ADDON_HARDWARE_CONTEXT),
+ void (*renderCB)(KODI_GUI_CLIENT_HANDLE),
+ void (*stopCB)(KODI_GUI_CLIENT_HANDLE),
+ bool (*dirtyCB)(KODI_GUI_CLIENT_HANDLE));
+ void (*destroy)(void* kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ } AddonToKodiFuncTable_kodi_gui_control_rendering;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_RENDERING_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/settings_slider.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/settings_slider.h
new file mode 100644
index 0000000..4299252
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/settings_slider.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_SETTINGS_SLIDER_H
+#define C_API_GUI_CONTROLS_SETTINGS_SLIDER_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_settings_slider
+ {
+ void (*set_visible)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ void (*set_enabled)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+ void (*set_text)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* label);
+ void (*reset)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_int_range)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int start, int end);
+ void (*set_int_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int value);
+ int (*get_int_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_int_interval)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int interval);
+ void (*set_percentage)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float percent);
+ float (*get_percentage)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_float_range)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float start,
+ float end);
+ void (*set_float_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float value);
+ float (*get_float_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_float_interval)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float interval);
+ } AddonToKodiFuncTable_kodi_gui_control_settings_slider;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_SETTINGS_SLIDER_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/slider.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/slider.h
new file mode 100644
index 0000000..67c6c2f
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/slider.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_SLIDER_H
+#define C_API_GUI_CONTROLS_SLIDER_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_slider
+ {
+ void (*set_visible)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ void (*set_enabled)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+ void (*reset)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ char* (*get_description)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_int_range)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int start, int end);
+ void (*set_int_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int value);
+ int (*get_int_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_int_interval)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int interval);
+ void (*set_percentage)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float percent);
+ float (*get_percentage)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_float_range)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float start,
+ float end);
+ void (*set_float_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float value);
+ float (*get_float_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_float_interval)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float interval);
+ } AddonToKodiFuncTable_kodi_gui_control_slider;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_SLIDER_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/spin.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/spin.h
new file mode 100644
index 0000000..0373636
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/spin.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_SPIN_H
+#define C_API_GUI_CONTROLS_SPIN_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_spin
+ {
+ void (*set_visible)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ void (*set_enabled)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool enabled);
+ void (*set_text)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* text);
+ void (*reset)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_type)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int type);
+ void (*add_string_label)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label,
+ const char* value);
+ void (*set_string_value)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* value);
+ char* (*get_string_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*add_int_label)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ const char* label,
+ int value);
+ void (*set_int_range)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int start, int end);
+ void (*set_int_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int value);
+ int (*get_int_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_float_range)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float start,
+ float end);
+ void (*set_float_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, float value);
+ float (*get_float_value)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_float_interval)(KODI_HANDLE kodiBase,
+ KODI_GUI_CONTROL_HANDLE handle,
+ float interval);
+ } AddonToKodiFuncTable_kodi_gui_control_spin;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_SPIN_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/text_box.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/text_box.h
new file mode 100644
index 0000000..333689e
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/controls/text_box.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_CONTROLS_TEXT_BOX_H
+#define C_API_GUI_CONTROLS_TEXT_BOX_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_control_text_box
+ {
+ void (*set_visible)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, bool visible);
+ void (*reset)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*set_text)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, const char* text);
+ char* (*get_text)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle);
+ void (*scroll)(KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, unsigned int scroll);
+ void (*set_auto_scrolling)(
+ KODI_HANDLE kodiBase, KODI_GUI_CONTROL_HANDLE handle, int delay, int time, int repeat);
+ } AddonToKodiFuncTable_kodi_gui_control_text_box;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_CONTROLS_TEXT_BOX_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/definitions.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/definitions.h
new file mode 100644
index 0000000..75fa721
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/definitions.h
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_GUI_DEFINITIONS_H
+#define C_API_GUI_DEFINITIONS_H
+
+#include "../addon_base.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef void* KODI_GUI_HANDLE;
+ typedef void* KODI_GUI_CLIENT_HANDLE;
+ typedef void* KODI_GUI_CONTROL_HANDLE;
+ typedef void* KODI_GUI_LISTITEM_HANDLE;
+ typedef void* KODI_GUI_WINDOW_HANDLE;
+
+ typedef struct AddonToKodiFuncTable_kodi_gui
+ {
+ struct AddonToKodiFuncTable_kodi_gui_general* general;
+ struct AddonToKodiFuncTable_kodi_gui_control_button* control_button;
+ struct AddonToKodiFuncTable_kodi_gui_control_edit* control_edit;
+ struct AddonToKodiFuncTable_kodi_gui_control_fade_label* control_fade_label;
+ struct AddonToKodiFuncTable_kodi_gui_control_label* control_label;
+ struct AddonToKodiFuncTable_kodi_gui_control_image* control_image;
+ struct AddonToKodiFuncTable_kodi_gui_control_progress* control_progress;
+ struct AddonToKodiFuncTable_kodi_gui_control_radio_button* control_radio_button;
+ struct AddonToKodiFuncTable_kodi_gui_control_rendering* control_rendering;
+ struct AddonToKodiFuncTable_kodi_gui_control_settings_slider* control_settings_slider;
+ struct AddonToKodiFuncTable_kodi_gui_control_slider* control_slider;
+ struct AddonToKodiFuncTable_kodi_gui_control_spin* control_spin;
+ struct AddonToKodiFuncTable_kodi_gui_control_text_box* control_text_box;
+ KODI_HANDLE control_dummy1;
+ KODI_HANDLE control_dummy2;
+ KODI_HANDLE control_dummy3;
+ KODI_HANDLE control_dummy4;
+ KODI_HANDLE control_dummy5;
+ KODI_HANDLE control_dummy6;
+ KODI_HANDLE control_dummy7;
+ KODI_HANDLE control_dummy8;
+ KODI_HANDLE control_dummy9;
+ KODI_HANDLE control_dummy10; /* This and above used to add new controls */
+ struct AddonToKodiFuncTable_kodi_gui_dialogContextMenu* dialogContextMenu;
+ struct AddonToKodiFuncTable_kodi_gui_dialogExtendedProgress* dialogExtendedProgress;
+ struct AddonToKodiFuncTable_kodi_gui_dialogFileBrowser* dialogFileBrowser;
+ struct AddonToKodiFuncTable_kodi_gui_dialogKeyboard* dialogKeyboard;
+ struct AddonToKodiFuncTable_kodi_gui_dialogNumeric* dialogNumeric;
+ struct AddonToKodiFuncTable_kodi_gui_dialogOK* dialogOK;
+ struct AddonToKodiFuncTable_kodi_gui_dialogProgress* dialogProgress;
+ struct AddonToKodiFuncTable_kodi_gui_dialogSelect* dialogSelect;
+ struct AddonToKodiFuncTable_kodi_gui_dialogTextViewer* dialogTextViewer;
+ struct AddonToKodiFuncTable_kodi_gui_dialogYesNo* dialogYesNo;
+ KODI_HANDLE dialog_dummy1;
+ KODI_HANDLE dialog_dummy2;
+ KODI_HANDLE dialog_dummy3;
+ KODI_HANDLE dialog_dummy4;
+ KODI_HANDLE dialog_dummy5;
+ KODI_HANDLE dialog_dummy6;
+ KODI_HANDLE dialog_dummy7;
+ KODI_HANDLE dialog_dummy8;
+ KODI_HANDLE dialog_dummy9;
+ KODI_HANDLE dialog_dummy10; /* This and above used to add new dialogs */
+ struct AddonToKodiFuncTable_kodi_gui_listItem* listItem;
+ struct AddonToKodiFuncTable_kodi_gui_window* window;
+ } AddonToKodiFuncTable_kodi_gui;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_DEFINITIONS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..91caa50
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/CMakeLists.txt
@@ -0,0 +1,19 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ context_menu.h
+ extended_progress.h
+ filebrowser.h
+ keyboard.h
+ numeric.h
+ ok.h
+ progress.h
+ select.h
+ text_viewer.h
+ yes_no.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_c-api_gui_dialogs)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/context_menu.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/context_menu.h
new file mode 100644
index 0000000..130d831
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/context_menu.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_DIALOGS_CONTEXT_MENU_H
+#define C_API_GUI_DIALOGS_CONTEXT_MENU_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_dialogContextMenu
+ {
+ int (*open)(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* entries[],
+ unsigned int size);
+ } AddonToKodiFuncTable_kodi_gui_dialogContextMenu;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_DIALOGS_CONTEXT_MENU_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/extended_progress.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/extended_progress.h
new file mode 100644
index 0000000..0992d5c
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/extended_progress.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_DIALOGS_EXTENDED_PROGRESS_H
+#define C_API_GUI_DIALOGS_EXTENDED_PROGRESS_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_dialogExtendedProgress
+ {
+ KODI_GUI_HANDLE (*new_dialog)(KODI_HANDLE kodiBase, const char* title);
+ void (*delete_dialog)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ char* (*get_title)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ void (*set_title)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, const char* title);
+ char* (*get_text)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ void (*set_text)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, const char* text);
+ bool (*is_finished)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ void (*mark_finished)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ float (*get_percentage)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ void (*set_percentage)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, float percentage);
+ void (*set_progress)(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ int currentItem,
+ int itemCount);
+ } AddonToKodiFuncTable_kodi_gui_dialogExtendedProgress;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_DIALOGS_EXTENDED_PROGRESS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/filebrowser.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/filebrowser.h
new file mode 100644
index 0000000..ef3945b
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/filebrowser.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_DIALOGS_FILEBROWSER_H
+#define C_API_GUI_DIALOGS_FILEBROWSER_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_dialogFileBrowser
+ {
+ bool (*show_and_get_directory)(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* heading,
+ const char* path_in,
+ char** path_out,
+ bool writeOnly);
+ bool (*show_and_get_file)(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* mask,
+ const char* heading,
+ const char* path_in,
+ char** path_out,
+ bool use_thumbs,
+ bool use_file_directories);
+ bool (*show_and_get_file_from_dir)(KODI_HANDLE kodiBase,
+ const char* directory,
+ const char* mask,
+ const char* heading,
+ const char* path_in,
+ char** path_out,
+ bool use_thumbs,
+ bool use_file_directories,
+ bool singleList);
+ bool (*show_and_get_file_list)(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* mask,
+ const char* heading,
+ char*** file_list,
+ unsigned int* entries,
+ bool use_thumbs,
+ bool use_file_directories);
+ bool (*show_and_get_source)(KODI_HANDLE kodiBase,
+ const char* path_in,
+ char** path_out,
+ bool allow_network_shares,
+ const char* additional_share,
+ const char* type);
+ bool (*show_and_get_image)(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* heading,
+ const char* path_in,
+ char** path_out);
+ bool (*show_and_get_image_list)(KODI_HANDLE kodiBase,
+ const char* shares,
+ const char* heading,
+ char*** file_list,
+ unsigned int* entries);
+ void (*clear_file_list)(KODI_HANDLE kodiBase, char*** file_list, unsigned int entries);
+ } AddonToKodiFuncTable_kodi_gui_dialogFileBrowser;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_DIALOGS_FILEBROWSER_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/keyboard.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/keyboard.h
new file mode 100644
index 0000000..f84dc20
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/keyboard.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_DIALOGS_KEYBOARD_H
+#define C_API_GUI_DIALOGS_KEYBOARD_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_dialogKeyboard
+ {
+ bool (*show_and_get_input_with_head)(KODI_HANDLE kodiBase,
+ const char* text_in,
+ char** text_out,
+ const char* heading,
+ bool allow_empty_result,
+ bool hiddenInput,
+ unsigned int auto_close_ms);
+ bool (*show_and_get_input)(KODI_HANDLE kodiBase,
+ const char* text_in,
+ char** text_out,
+ bool allow_empty_result,
+ unsigned int auto_close_ms);
+ bool (*show_and_get_new_password_with_head)(KODI_HANDLE kodiBase,
+ const char* password_in,
+ char** password_out,
+ const char* heading,
+ bool allow_empty_result,
+ unsigned int auto_close_ms);
+ bool (*show_and_get_new_password)(KODI_HANDLE kodiBase,
+ const char* password_in,
+ char** password_out,
+ unsigned int auto_close_ms);
+ bool (*show_and_verify_new_password_with_head)(KODI_HANDLE kodiBase,
+ char** password_out,
+ const char* heading,
+ bool allow_empty_result,
+ unsigned int auto_close_ms);
+ bool (*show_and_verify_new_password)(KODI_HANDLE kodiBase,
+ char** password_out,
+ unsigned int auto_close_ms);
+ int (*show_and_verify_password)(KODI_HANDLE kodiBase,
+ const char* password_in,
+ char** password_out,
+ const char* heading,
+ int retries,
+ unsigned int auto_close_ms);
+ bool (*show_and_get_filter)(KODI_HANDLE kodiBase,
+ const char* text_in,
+ char** text_out,
+ bool searching,
+ unsigned int auto_close_ms);
+ bool (*send_text_to_active_keyboard)(KODI_HANDLE kodiBase,
+ const char* text,
+ bool close_keyboard);
+ bool (*is_keyboard_activated)(KODI_HANDLE kodiBase);
+ } AddonToKodiFuncTable_kodi_gui_dialogKeyboard;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_DIALOGS_KEYBOARD_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/numeric.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/numeric.h
new file mode 100644
index 0000000..08aaf25
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/numeric.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_DIALOGS_NUMERIC_H
+#define C_API_GUI_DIALOGS_NUMERIC_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_dialogNumeric
+ {
+ bool (*show_and_verify_new_password)(KODI_HANDLE kodiBase, char** password);
+ int (*show_and_verify_password)(KODI_HANDLE kodiBase,
+ const char* password,
+ const char* heading,
+ int retries);
+ bool (*show_and_verify_input)(KODI_HANDLE kodiBase,
+ const char* verify_in,
+ char** verify_out,
+ const char* heading,
+ bool verify_input);
+ bool (*show_and_get_time)(KODI_HANDLE kodiBase, struct tm* time, const char* heading);
+ bool (*show_and_get_date)(KODI_HANDLE kodiBase, struct tm* date, const char* heading);
+ bool (*show_and_get_ip_address)(KODI_HANDLE kodiBase,
+ const char* ip_address_in,
+ char** ip_address_out,
+ const char* heading);
+ bool (*show_and_get_number)(KODI_HANDLE kodiBase,
+ const char* input_in,
+ char** input_out,
+ const char* heading,
+ unsigned int auto_close_ms);
+ bool (*show_and_get_seconds)(KODI_HANDLE kodiBase,
+ const char* time_in,
+ char** time_out,
+ const char* heading);
+ } AddonToKodiFuncTable_kodi_gui_dialogNumeric;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_DIALOGS_NUMERIC_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/ok.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/ok.h
new file mode 100644
index 0000000..45fe0fc
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/ok.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_DIALOGS_OK_H
+#define C_API_GUI_DIALOGS_OK_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_dialogOK
+ {
+ void (*show_and_get_input_single_text)(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* text);
+ void (*show_and_get_input_line_text)(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* line0,
+ const char* line1,
+ const char* line2);
+ } AddonToKodiFuncTable_kodi_gui_dialogOK;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_DIALOGS_OK_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/progress.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/progress.h
new file mode 100644
index 0000000..60a003a
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/progress.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_DIALOGS_PROGRESS_H
+#define C_API_GUI_DIALOGS_PROGRESS_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_dialogProgress
+ {
+ KODI_GUI_HANDLE (*new_dialog)(KODI_HANDLE kodiBase);
+ void (*delete_dialog)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ void (*open)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ void (*set_heading)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, const char* heading);
+ void (*set_line)(KODI_HANDLE kodiBase,
+ KODI_GUI_HANDLE handle,
+ unsigned int lineNo,
+ const char* line);
+ void (*set_can_cancel)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, bool canCancel);
+ bool (*is_canceled)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ void (*set_percentage)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, int percentage);
+ int (*get_percentage)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ void (*show_progress_bar)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, bool pnOff);
+ void (*set_progress_max)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, int max);
+ void (*set_progress_advance)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle, int nSteps);
+ bool (*abort)(KODI_HANDLE kodiBase, KODI_GUI_HANDLE handle);
+ } AddonToKodiFuncTable_kodi_gui_dialogProgress;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_DIALOGS_PROGRESS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/select.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/select.h
new file mode 100644
index 0000000..79635d7
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/select.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_DIALOGS_SELECT_H
+#define C_API_GUI_DIALOGS_SELECT_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_dialogSelect
+ {
+ int (*open)(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* entries[],
+ unsigned int size,
+ int selected,
+ unsigned int autoclose);
+ bool (*open_multi_select)(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* entryIDs[],
+ const char* entryNames[],
+ bool entriesSelected[],
+ unsigned int size,
+ unsigned int autoclose);
+ } AddonToKodiFuncTable_kodi_gui_dialogSelect;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_DIALOGS_SELECT_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/text_viewer.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/text_viewer.h
new file mode 100644
index 0000000..4a1cfe6
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/text_viewer.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_DIALOGS_TEXT_VIEWER_H
+#define C_API_GUI_DIALOGS_TEXT_VIEWER_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_dialogTextViewer
+ {
+ void (*open)(KODI_HANDLE kodiBase, const char* heading, const char* text);
+ } AddonToKodiFuncTable_kodi_gui_dialogTextViewer;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_DIALOGS_TEXT_VIEWER_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/yes_no.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/yes_no.h
new file mode 100644
index 0000000..8558acb
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/dialogs/yes_no.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_DIALOGS_YES_NO_H
+#define C_API_GUI_DIALOGS_YES_NO_H
+
+#include "../definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_dialogYesNo
+ {
+ bool (*show_and_get_input_single_text)(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* text,
+ bool* canceled,
+ const char* noLabel,
+ const char* yesLabel);
+ bool (*show_and_get_input_line_text)(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* line0,
+ const char* line1,
+ const char* line2,
+ const char* noLabel,
+ const char* yesLabel);
+ bool (*show_and_get_input_line_button_text)(KODI_HANDLE kodiBase,
+ const char* heading,
+ const char* line0,
+ const char* line1,
+ const char* line2,
+ bool* canceled,
+ const char* noLabel,
+ const char* yesLabel);
+ } AddonToKodiFuncTable_kodi_gui_dialogYesNo;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_DIALOGS_YES_NO_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/general.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/general.h
new file mode 100644
index 0000000..77e43c5
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/general.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_GENERAL_H
+#define C_API_GUI_GENERAL_H
+
+#include "definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ //==============================================================================
+ /// @ingroup cpp_kodi_gui_general
+ /// @brief **Adjust refresh rate enum**\n
+ /// Used to get the Adjust refresh rate status info.
+ ///
+ enum AdjustRefreshRateStatus
+ {
+ ADJUST_REFRESHRATE_STATUS_OFF = 0,
+ ADJUST_REFRESHRATE_STATUS_ALWAYS,
+ ADJUST_REFRESHRATE_STATUS_ON_STARTSTOP,
+ ADJUST_REFRESHRATE_STATUS_ON_START,
+ };
+ //------------------------------------------------------------------------------
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_general
+ {
+ void (*lock)();
+ void (*unlock)();
+ int (*get_screen_height)(KODI_HANDLE kodiBase);
+ int (*get_screen_width)(KODI_HANDLE kodiBase);
+ int (*get_video_resolution)(KODI_HANDLE kodiBase);
+ int (*get_current_window_dialog_id)(KODI_HANDLE kodiBase);
+ int (*get_current_window_id)(KODI_HANDLE kodiBase);
+ ADDON_HARDWARE_CONTEXT (*get_hw_context)(KODI_HANDLE kodiBase);
+ AdjustRefreshRateStatus (*get_adjust_refresh_rate_status)(KODI_HANDLE kodiBase);
+ } AddonToKodiFuncTable_kodi_gui_general;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_GENERAL_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/input/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/input/CMakeLists.txt
new file mode 100644
index 0000000..eb7dde0
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/input/CMakeLists.txt
@@ -0,0 +1,10 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ action_ids.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_c-api_gui_input)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/input/action_ids.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/input/action_ids.h
new file mode 100644
index 0000000..5d62587
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/input/action_ids.h
@@ -0,0 +1,761 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_ACTION_IDS_H
+#define C_API_GUI_ACTION_IDS_H
+
+/// @defgroup cpp_kodi_gui_Defs_action_ids enum ADDON_ACTION
+/// @ingroup cpp_kodi_gui_Defs
+/// @brief **Action Id's**\n
+/// Actions that we have defined.
+///
+///@{
+enum ADDON_ACTION
+{
+ /// @ingroup cpp_kodi_gui_key_action_ids
+ ///@{
+
+ /// @brief <b>`0 `</b>: None.
+ ADDON_ACTION_NONE = 0,
+
+ /// @brief <b>`1 `</b>: Move left.
+ ADDON_ACTION_MOVE_LEFT = 1,
+
+ /// @brief <b>`2 `</b>: Move right.
+ ADDON_ACTION_MOVE_RIGHT = 2,
+
+ /// @brief <b>`3 `</b>: Move up.
+ ADDON_ACTION_MOVE_UP = 3,
+
+ /// @brief <b>`4 `</b>: Move down.
+ ADDON_ACTION_MOVE_DOWN = 4,
+
+ /// @brief <b>`5 `</b>: Page up.
+ ADDON_ACTION_PAGE_UP = 5,
+
+ /// @brief <b>`6 `</b>: Page down.
+ ADDON_ACTION_PAGE_DOWN = 6,
+
+ /// @brief <b>`7 `</b>: Select item.
+ ADDON_ACTION_SELECT_ITEM = 7,
+
+ /// @brief <b>`8 `</b>: Highlight item.
+ ADDON_ACTION_HIGHLIGHT_ITEM = 8,
+
+ /// @brief <b>`9 `</b>: Parent directory.
+ ADDON_ACTION_PARENT_DIR = 9,
+
+ /// @brief <b>`10 `</b>: Previous menu.
+ ADDON_ACTION_PREVIOUS_MENU = 10,
+
+ /// @brief <b>`11 `</b>: Show info.
+ ADDON_ACTION_SHOW_INFO = 11,
+
+ /// @brief <b>`12 `</b>: Pause.
+ ADDON_ACTION_PAUSE = 12,
+
+ /// @brief <b>`13 `</b>: Stop.
+ ADDON_ACTION_STOP = 13,
+
+ /// @brief <b>`14 `</b>: Next item.
+ ADDON_ACTION_NEXT_ITEM = 14,
+
+ /// @brief <b>`15 `</b>: Previous item.
+ ADDON_ACTION_PREV_ITEM = 15,
+
+ /// @brief <b>`16 `</b>: Can be used to specify specific action in a window, Playback control is handled in ADDON_ACTION_PLAYER_*
+ ADDON_ACTION_FORWARD = 16,
+
+ /// @brief <b>`17 `</b>: Can be used to specify specific action in a window, Playback control is handled in ADDON_ACTION_PLAYER_*
+ ADDON_ACTION_REWIND = 17,
+
+ /// @brief <b>`18 `</b>: Toggle between GUI and movie or GUI and visualisation.
+ ADDON_ACTION_SHOW_GUI = 18,
+
+ /// @brief <b>`19 `</b>: Toggle quick-access zoom modes. Can b used in videoFullScreen.zml window id=2005
+ ADDON_ACTION_ASPECT_RATIO = 19,
+
+ /// @brief <b>`20 `</b>: Seek +1% in the movie. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_STEP_FORWARD = 20,
+
+ /// @brief <b>`21 `</b>: Seek -1% in the movie. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_STEP_BACK = 21,
+
+ /// @brief <b>`22 `</b>: Seek +10% in the movie. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_BIG_STEP_FORWARD = 22,
+
+ /// @brief <b>`23 `</b>: Seek -10% in the movie. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_BIG_STEP_BACK = 23,
+
+ /// @brief <b>`24 `</b>: Show/hide OSD. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_SHOW_OSD = 24,
+
+ /// @brief <b>`25 `</b>: Turn subtitles on/off. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_SHOW_SUBTITLES = 25,
+
+ /// @brief <b>`26 `</b>: Switch to next subtitle of movie. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_NEXT_SUBTITLE = 26,
+
+ /// @brief <b>`27 `</b>: Show debug info for VideoPlayer
+ ADDON_ACTION_PLAYER_DEBUG = 27,
+
+ /// @brief <b>`28 `</b>: Show next picture of slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_NEXT_PICTURE = 28,
+
+ /// @brief <b>`29 `</b>: Show previous picture of slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_PREV_PICTURE = 29,
+
+ /// @brief <b>`30 `</b>: Zoom in picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_OUT = 30,
+
+ /// @brief <b>`31 `</b>: Zoom out picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_IN = 31,
+
+ /// @brief <b>`32 `</b>: Used to toggle between source view and destination view. Can be used in myfiles.xml window id=3
+ ADDON_ACTION_TOGGLE_SOURCE_DEST = 32,
+
+ /// @brief <b>`33 `</b>: Used to toggle between current view and playlist view. Can b used in all mymusic xml files
+ ADDON_ACTION_SHOW_PLAYLIST = 33,
+
+ /// @brief <b>`34 `</b>: Used to queue a item to the playlist. Can b used in all mymusic xml files
+ ADDON_ACTION_QUEUE_ITEM = 34,
+
+ /// @brief <b>`35 `</b>: Not used anymore
+ ADDON_ACTION_REMOVE_ITEM = 35,
+
+ /// @brief <b>`36 `</b>: Not used anymore
+ ADDON_ACTION_SHOW_FULLSCREEN = 36,
+
+ /// @brief <b>`37 `</b>: Zoom 1x picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_LEVEL_NORMAL = 37,
+
+ /// @brief <b>`38 `</b>: Zoom 2x picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_LEVEL_1 = 38,
+
+ /// @brief <b>`39 `</b>: Zoom 3x picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_LEVEL_2 = 39,
+
+ /// @brief <b>`40 `</b>: Zoom 4x picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_LEVEL_3 = 40,
+
+ /// @brief <b>`41 `</b>: Zoom 5x picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_LEVEL_4 = 41,
+
+ /// @brief <b>`42 `</b>: Zoom 6x picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_LEVEL_5 = 42,
+
+ /// @brief <b>`43 `</b>: Zoom 7x picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_LEVEL_6 = 43,
+
+ /// @brief <b>`44 `</b>: Zoom 8x picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_LEVEL_7 = 44,
+
+ /// @brief <b>`45 `</b>: Zoom 9x picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_LEVEL_8 = 45,
+
+ /// @brief <b>`46 `</b>: Zoom 10x picture during slideshow. Can b used in slideshow.xml window id=2007
+ ADDON_ACTION_ZOOM_LEVEL_9 = 46,
+
+ /// @brief <b>`47 `</b>: Select next arrow. Can b used in: settingsScreenCalibration.xml windowid=11
+ ADDON_ACTION_CALIBRATE_SWAP_ARROWS = 47,
+
+ /// @brief <b>`48 `</b>: Reset calibration to defaults. Can b used in: `settingsScreenCalibration.xml` windowid=11/settingsUICalibration.xml windowid=10
+ ADDON_ACTION_CALIBRATE_RESET = 48,
+
+ /// @brief <b>`49 `</b>: Analog thumbstick move. Can b used in: `slideshow.xml`
+ /// windowid=2007/settingsScreenCalibration.xml windowid=11/settingsUICalibration.xml
+ /// windowid=10
+ /// @note see also ADDON_ACTION_ANALOG_MOVE_X_LEFT, ADDON_ACTION_ANALOG_MOVE_X_RIGHT,
+ /// ADDON_ACTION_ANALOG_MOVE_Y_UP, ADDON_ACTION_ANALOG_MOVE_Y_DOWN
+ ADDON_ACTION_ANALOG_MOVE = 49,
+
+ /// @brief <b>`50 `</b>: Rotate current picture clockwise during slideshow. Can be used in slideshow.xml window id=2007
+ ADDON_ACTION_ROTATE_PICTURE_CW = 50,
+
+ /// @brief <b>`51 `</b>: Rotate current picture counterclockwise during slideshow. Can be used in slideshow.xml window id=2007
+ ADDON_ACTION_ROTATE_PICTURE_CCW = 51,
+
+ /// @brief <b>`52 `</b>: Decrease subtitle/movie Delay. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_SUBTITLE_DELAY_MIN = 52,
+
+ /// @brief <b>`53 `</b>: Increase subtitle/movie Delay. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_SUBTITLE_DELAY_PLUS = 53,
+
+ /// @brief <b>`54 `</b>: Increase avsync delay. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_AUDIO_DELAY_MIN = 54,
+
+ /// @brief <b>`55 `</b>: Decrease avsync delay. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_AUDIO_DELAY_PLUS = 55,
+
+ /// @brief <b>`56 `</b>: Select next language in movie. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_AUDIO_NEXT_LANGUAGE = 56,
+
+ /// @brief <b>`57 `</b>: Switch 2 next resolution. Can b used during screen calibration settingsScreenCalibration.xml windowid=11
+ ADDON_ACTION_CHANGE_RESOLUTION = 57,
+
+ /// @brief <b>`58 `</b>: remote keys 0-9. are used by multiple windows
+ /// for example in videoFullScreen.xml window id=2005 you can
+ /// enter time (mmss) to jump to particular point in the movie
+ /// with spincontrols you can enter 3digit number to quickly set
+ /// spincontrol to desired value
+ ///
+ /// Remote key 0
+ ADDON_ACTION_REMOTE_0 = 58,
+
+ /// @brief <b>`59 `</b>: Remote key 1
+ ADDON_ACTION_REMOTE_1 = 59,
+
+ /// @brief <b>`60 `</b>: Remote key 2
+ ADDON_ACTION_REMOTE_2 = 60,
+
+ /// @brief <b>`61 `</b>: Remote key 3
+ ADDON_ACTION_REMOTE_3 = 61,
+
+ /// @brief <b>`62 `</b>: Remote key 4
+ ADDON_ACTION_REMOTE_4 = 62,
+
+ /// @brief <b>`63 `</b>: Remote key 5
+ ADDON_ACTION_REMOTE_5 = 63,
+
+ /// @brief <b>`64 `</b>: Remote key 6
+ ADDON_ACTION_REMOTE_6 = 64,
+
+ /// @brief <b>`65 `</b>: Remote key 7
+ ADDON_ACTION_REMOTE_7 = 65,
+
+ /// @brief <b>`66 `</b>: Remote key 8
+ ADDON_ACTION_REMOTE_8 = 66,
+
+ /// @brief <b>`67 `</b>: Remote key 9
+ ADDON_ACTION_REMOTE_9 = 67,
+
+ /// @brief <b>`69 `</b>: Show player process info (video decoder, pixel format, pvr signal strength and the like
+ ADDON_ACTION_PLAYER_PROCESS_INFO = 69,
+
+ /// @brief <b>`70 `</b>: Program select.
+ ADDON_ACTION_PLAYER_PROGRAM_SELECT = 70,
+
+ /// @brief <b>`71 `</b>: Resolution select.
+ ADDON_ACTION_PLAYER_RESOLUTION_SELECT = 71,
+
+ /// @brief <b>`76 `</b>: Jumps a few seconds back during playback of movie. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_SMALL_STEP_BACK = 76,
+
+ /// @brief <b>`77 `</b>: FF in current file played. global action, can be used anywhere
+ ADDON_ACTION_PLAYER_FORWARD = 77,
+
+ /// @brief <b>`78 `</b>: RW in current file played. global action, can be used anywhere
+ ADDON_ACTION_PLAYER_REWIND = 78,
+
+ /// @brief <b>`79 `</b>: Play current song. Unpauses song and sets playspeed to 1x. global action, can be used anywhere
+ ADDON_ACTION_PLAYER_PLAY = 79,
+
+ /// @brief <b>`80 `</b>: Delete current selected item. Can be used in myfiles.xml window id=3 and in myvideoTitle.xml window id=25
+ ADDON_ACTION_DELETE_ITEM = 80,
+
+ /// @brief <b>`81 `</b>: Copy current selected item. Can be used in myfiles.xml window id=3
+ ADDON_ACTION_COPY_ITEM = 81,
+
+ /// @brief <b>`82 `</b>: move current selected item. Can be used in myfiles.xml window id=3
+ ADDON_ACTION_MOVE_ITEM = 82,
+
+ /// @brief <b>`85 `</b>: Take a screenshot.
+ ADDON_ACTION_TAKE_SCREENSHOT = 85,
+
+ /// @brief <b>`87 `</b>: Rename item.
+ ADDON_ACTION_RENAME_ITEM = 87,
+
+ /// @brief <b>`87 `</b>: Volume up.
+ ADDON_ACTION_VOLUME_UP = 88,
+
+ /// @brief <b>`87 `</b>: Volume down.
+ ADDON_ACTION_VOLUME_DOWN = 89,
+
+ /// @brief <b>`90 `</b>: Volume amplication.
+ ADDON_ACTION_VOLAMP = 90,
+
+ /// @brief <b>`90 `</b>: Mute.
+ ADDON_ACTION_MUTE = 91,
+
+ /// @brief <b>`90 `</b>: Nav back.
+ ADDON_ACTION_NAV_BACK = 92,
+
+ /// @brief <b>`90 `</b>: Volume amp up,
+ ADDON_ACTION_VOLAMP_UP = 93,
+
+ /// @brief <b>`94 `</b>: Volume amp down.
+ ADDON_ACTION_VOLAMP_DOWN = 94,
+
+ /// @brief <b>`95 `</b>: Creates an episode bookmark on the currently playing video file containing more than one
+ /// episode
+ ADDON_ACTION_CREATE_EPISODE_BOOKMARK = 95,
+
+ /// @brief <b>`96 `</b>: Creates a bookmark of the currently playing video file
+ ADDON_ACTION_CREATE_BOOKMARK = 96,
+
+ /// @brief <b>`97 `</b>: Goto the next chapter, if not available perform a big step forward
+ ADDON_ACTION_CHAPTER_OR_BIG_STEP_FORWARD = 97,
+
+ /// @brief <b>`98 `</b>: Goto the previous chapter, if not available perform a big step back
+ ADDON_ACTION_CHAPTER_OR_BIG_STEP_BACK = 98,
+
+ /// @brief <b>`99 `</b>: Switch to next subtitle of movie, but will not enable/disable the subtitles. Can be used
+ /// in videoFullScreen.xml window id=2005
+ ADDON_ACTION_CYCLE_SUBTITLE = 99,
+
+ /// @brief <b>`100`</b>: Mouse action values start.
+ ///
+ /// Ends with @ref ADDON_ACTION_MOUSE_END.
+ ADDON_ACTION_MOUSE_START = 100,
+
+ /// @brief <b>`100`</b>: Mouse left click.
+ ADDON_ACTION_MOUSE_LEFT_CLICK = 100,
+
+ /// @brief <b>`101`</b>: Mouse right click.
+ ADDON_ACTION_MOUSE_RIGHT_CLICK = 101,
+
+ /// @brief <b>`102`</b>: Mouse middle click.
+ ADDON_ACTION_MOUSE_MIDDLE_CLICK = 102,
+
+ /// @brief <b>`103`</b>: Mouse double click.
+ ADDON_ACTION_MOUSE_DOUBLE_CLICK = 103,
+
+ /// @brief <b>`104`</b>: Mouse wheel up.
+ ADDON_ACTION_MOUSE_WHEEL_UP = 104,
+
+ /// @brief <b>`105`</b>: Mouse wheel down.
+ ADDON_ACTION_MOUSE_WHEEL_DOWN = 105,
+
+ /// @brief <b>`106`</b>: Mouse drag.
+ ADDON_ACTION_MOUSE_DRAG = 106,
+
+ /// @brief <b>`107`</b>: Mouse move.
+ ADDON_ACTION_MOUSE_MOVE = 107,
+
+ /// @brief <b>`108`</b>: Mouse long click.
+ ADDON_ACTION_MOUSE_LONG_CLICK = 108,
+
+ /// @brief <b>`109`</b>: Mouse drag end.
+ ADDON_ACTION_MOUSE_DRAG_END = 109,
+
+ /// @brief <b>`109`</b>: Mouse action values end.
+ ///
+ /// Starts with @ref ADDON_ACTION_MOUSE_START.
+ ADDON_ACTION_MOUSE_END = 109,
+
+ /// @brief <b>`110`</b>: Backspace.
+ ADDON_ACTION_BACKSPACE = 110,
+
+ /// @brief <b>`111`</b>: Scroll up.
+ ADDON_ACTION_SCROLL_UP = 111,
+
+ /// @brief <b>`112`</b>: Scroll down.
+ ADDON_ACTION_SCROLL_DOWN = 112,
+
+ /// @brief <b>`113`</b>: Analog forward.
+ ADDON_ACTION_ANALOG_FORWARD = 113,
+
+ /// @brief <b>`114`</b>: Analog rewind.
+ ADDON_ACTION_ANALOG_REWIND = 114,
+
+ /// @brief <b>`115`</b>: move item up in playlist
+ ADDON_ACTION_MOVE_ITEM_UP = 115,
+
+ /// @brief <b>`116`</b>: move item down in playlist
+ ADDON_ACTION_MOVE_ITEM_DOWN = 116,
+
+ /// @brief <b>`117`</b>: pops up the context menu
+ ADDON_ACTION_CONTEXT_MENU = 117,
+
+ /// @brief <b>`118`</b>: stuff for virtual keyboard shortcuts
+ ADDON_ACTION_SHIFT = 118,
+
+ /// @brief <b>`119`</b>: stuff for virtual keyboard shortcuts
+ ADDON_ACTION_SYMBOLS = 119,
+
+ /// @brief <b>`120`</b>: stuff for virtual keyboard shortcuts
+ ADDON_ACTION_CURSOR_LEFT = 120,
+
+ /// @brief <b>`121`</b>: stuff for virtual keyboard shortcuts
+ ADDON_ACTION_CURSOR_RIGHT = 121,
+
+ /// @brief <b>`122`</b>: Build in function
+ ADDON_ACTION_BUILT_IN_FUNCTION = 122,
+
+ /// @brief <b>`114`</b>: Displays current time, can be used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_SHOW_OSD_TIME = 123,
+
+ /// @brief <b>`124`</b>: Seeks forward, and displays the seek bar.
+ ADDON_ACTION_ANALOG_SEEK_FORWARD = 124,
+
+ /// @brief <b>`125`</b>: Seeks backward, and displays the seek bar.
+ ADDON_ACTION_ANALOG_SEEK_BACK = 125,
+
+ /// @brief <b>`126`</b>: Visualization preset show.
+ ADDON_ACTION_VIS_PRESET_SHOW = 126,
+
+ /// @brief <b>`128`</b>: Visualization preset next.
+ ADDON_ACTION_VIS_PRESET_NEXT = 128,
+
+ /// @brief <b>`129`</b>: Visualization preset previous.
+ ADDON_ACTION_VIS_PRESET_PREV = 129,
+
+ /// @brief <b>`130`</b>: Visualization preset lock.
+ ADDON_ACTION_VIS_PRESET_LOCK = 130,
+
+ /// @brief <b>`131`</b>: Visualization preset random.
+ ADDON_ACTION_VIS_PRESET_RANDOM = 131,
+
+ /// @brief <b>`132`</b>: Visualization preset plus.
+ ADDON_ACTION_VIS_RATE_PRESET_PLUS = 132,
+
+ /// @brief <b>`133`</b>: Visualization preset minus.
+ ADDON_ACTION_VIS_RATE_PRESET_MINUS = 133,
+
+ /// @brief <b>`134`</b>: Show Videomenu
+ ADDON_ACTION_SHOW_VIDEOMENU = 134,
+
+ /// @brief <b>`135`</b>: Enter.
+ ADDON_ACTION_ENTER = 135,
+
+ /// @brief <b>`136`</b>: Increase rating.
+ ADDON_ACTION_INCREASE_RATING = 136,
+
+ /// @brief <b>`137`</b>: Decrease rating.
+ ADDON_ACTION_DECREASE_RATING = 137,
+
+ /// @brief <b>`138`</b>: Switch to next scene/cutpoint in movie.
+ ADDON_ACTION_NEXT_SCENE = 138,
+
+ /// @brief <b>`139`</b>: Switch to previous scene/cutpoint in movie.
+ ADDON_ACTION_PREV_SCENE = 139,
+
+ /// @brief <b>`140`</b>: Jump through a list or container to next letter.
+ ADDON_ACTION_NEXT_LETTER = 140,
+
+ /// @brief <b>`141`</b>: Jump through a list or container to previous letter.
+ ADDON_ACTION_PREV_LETTER = 141,
+
+ /// @brief <b>`142`</b>: Jump direct to a particular letter using SMS-style input
+ ///
+ /// Jump to SMS2.
+ ADDON_ACTION_JUMP_SMS2 = 142,
+
+ /// @brief <b>`143`</b>: Jump to SMS3.
+ ADDON_ACTION_JUMP_SMS3 = 143,
+
+ /// @brief <b>`144`</b>: Jump to SMS4.
+ ADDON_ACTION_JUMP_SMS4 = 144,
+
+ /// @brief <b>`145`</b>: Jump to SMS5.
+ ADDON_ACTION_JUMP_SMS5 = 145,
+
+ /// @brief <b>`146`</b>: Jump to SMS6.
+ ADDON_ACTION_JUMP_SMS6 = 146,
+
+ /// @brief <b>`147`</b>: Jump to SMS7.
+ ADDON_ACTION_JUMP_SMS7 = 147,
+
+ /// @brief <b>`148`</b>: Jump to SMS8.
+ ADDON_ACTION_JUMP_SMS8 = 148,
+
+ /// @brief <b>`149`</b>: Jump to SMS9.
+ ADDON_ACTION_JUMP_SMS9 = 149,
+
+ /// @brief <b>`150`</b>: Filter clear.
+ ADDON_ACTION_FILTER_CLEAR = 150,
+
+ /// @brief <b>`151`</b>: Filter SMS2.
+ ADDON_ACTION_FILTER_SMS2 = 151,
+
+ /// @brief <b>`152`</b>: Filter SMS3.
+ ADDON_ACTION_FILTER_SMS3 = 152,
+
+ /// @brief <b>`153`</b>: Filter SMS4.
+ ADDON_ACTION_FILTER_SMS4 = 153,
+
+ /// @brief <b>`154`</b>: Filter SMS5.
+ ADDON_ACTION_FILTER_SMS5 = 154,
+
+ /// @brief <b>`155`</b>: Filter SMS6.
+ ADDON_ACTION_FILTER_SMS6 = 155,
+
+ /// @brief <b>`156`</b>: Filter SMS7.
+ ADDON_ACTION_FILTER_SMS7 = 156,
+
+ /// @brief <b>`157`</b>: Filter SMS8.
+ ADDON_ACTION_FILTER_SMS8 = 157,
+
+ /// @brief <b>`158`</b>: Filter SMS9.
+ ADDON_ACTION_FILTER_SMS9 = 158,
+
+ /// @brief <b>`159`</b>: First page.
+ ADDON_ACTION_FIRST_PAGE = 159,
+
+ /// @brief <b>`160`</b>: Last page.
+ ADDON_ACTION_LAST_PAGE = 160,
+
+ /// @brief <b>`161`</b>: Audio delay.
+ ADDON_ACTION_AUDIO_DELAY = 161,
+
+ /// @brief <b>`162`</b>: Subtitle delay.
+ ADDON_ACTION_SUBTITLE_DELAY = 162,
+
+ /// @brief <b>`163`</b>: Menu.
+ ADDON_ACTION_MENU = 163,
+
+ /// @brief <b>`164`</b>: Set rating.
+ ADDON_ACTION_SET_RATING = 164,
+
+ /// @brief <b>`170`</b>: Record.
+ ADDON_ACTION_RECORD = 170,
+
+ /// @brief <b>`180`</b>: Paste.
+ ADDON_ACTION_PASTE = 180,
+
+ /// @brief <b>`181`</b>: Next control.
+ ADDON_ACTION_NEXT_CONTROL = 181,
+
+ /// @brief <b>`182`</b>: Previous control.
+ ADDON_ACTION_PREV_CONTROL = 182,
+
+ /// @brief <b>`183`</b>: Channel switch.
+ ADDON_ACTION_CHANNEL_SWITCH = 183,
+
+ /// @brief <b>`184`</b>: Channel up.
+ ADDON_ACTION_CHANNEL_UP = 184,
+
+ /// @brief <b>`185`</b>: Channel down.
+ ADDON_ACTION_CHANNEL_DOWN = 185,
+
+ /// @brief <b>`186`</b>: Next channel group.
+ ADDON_ACTION_NEXT_CHANNELGROUP = 186,
+
+ /// @brief <b>`187`</b>: Previous channel group.
+ ADDON_ACTION_PREVIOUS_CHANNELGROUP = 187,
+
+ /// @brief <b>`188`</b>: PVR play.
+ ADDON_ACTION_PVR_PLAY = 188,
+
+ /// @brief <b>`189`</b>: PVR play TV.
+ ADDON_ACTION_PVR_PLAY_TV = 189,
+
+ /// @brief <b>`190`</b>: PVR play radio.
+ ADDON_ACTION_PVR_PLAY_RADIO = 190,
+
+ /// @brief <b>`191`</b>: PVR show timer rule.
+ ADDON_ACTION_PVR_SHOW_TIMER_RULE = 191,
+
+ /// @brief <b>`192`</b>: Channel number sep
+ ADDON_ACTION_CHANNEL_NUMBER_SEP = 192,
+
+ /// @brief <b>`193`</b>: PVR announce reminders
+ ADDON_ACTION_PVR_ANNOUNCE_REMINDERS = 193,
+
+ /// @brief <b>`199`</b>: Switch 2 desktop resolution
+ ADDON_ACTION_TOGGLE_FULLSCREEN = 199,
+
+ /// @brief <b>`200`</b>: Toggle watched status (videos)
+ ADDON_ACTION_TOGGLE_WATCHED = 200,
+
+ /// @brief <b>`201`</b>: Scan item
+ ADDON_ACTION_SCAN_ITEM = 201,
+
+ /// @brief <b>`202`</b>: Switch digital <-> analog
+ ADDON_ACTION_TOGGLE_DIGITAL_ANALOG = 202,
+
+ /// @brief <b>`203`</b>: Reloads CButtonTranslator's keymaps
+ ADDON_ACTION_RELOAD_KEYMAPS = 203,
+
+ /// @brief <b>`204`</b>: Start the GUIControlProfiler running
+ ADDON_ACTION_GUIPROFILE_BEGIN = 204,
+
+ /// @brief <b>`215`</b>: Teletext Color button <b>Red</b> to control TopText
+ ADDON_ACTION_TELETEXT_RED = 215,
+
+ /// @brief <b>`216`</b>: Teletext Color button <b>Green</b> to control TopText
+ ADDON_ACTION_TELETEXT_GREEN = 216,
+
+ /// @brief <b>`217`</b>: Teletext Color button <b>Yellow</b> to control TopText
+ ADDON_ACTION_TELETEXT_YELLOW = 217,
+
+ /// @brief <b>`218`</b>: Teletext Color button <b>Blue</b> to control TopText
+ ADDON_ACTION_TELETEXT_BLUE = 218,
+
+ /// @brief <b>`219`</b>: Increase par.
+ ADDON_ACTION_INCREASE_PAR = 219,
+
+ /// @brief <b>`220`</b>: Decrease par.
+ ADDON_ACTION_DECREASE_PAR = 220,
+
+ /// @brief <b>`227`</b>: Shift up video image in VideoPlayer
+ ADDON_ACTION_VSHIFT_UP = 227,
+
+ /// @brief <b>`228`</b>: Shift down video image in VideoPlayer
+ ADDON_ACTION_VSHIFT_DOWN = 228,
+
+ /// @brief <b>`229`</b>: Play/pause. If playing it pauses, if paused it plays.
+ ADDON_ACTION_PLAYER_PLAYPAUSE = 229,
+
+ /// @brief <b>`230`</b>: Shift up subtitles in VideoPlayer
+ ADDON_ACTION_SUBTITLE_VSHIFT_UP = 230,
+
+ /// @brief <b>`231`</b>: Shift down subtitles in VideoPlayer
+ ADDON_ACTION_SUBTITLE_VSHIFT_DOWN = 231,
+
+ /// @brief <b>`232`</b>: Toggle vertical alignment of subtitles
+ ADDON_ACTION_SUBTITLE_ALIGN = 232,
+
+ /// @brief <b>`233`</b>: Filter.
+ ADDON_ACTION_FILTER = 233,
+
+ /// @brief <b>`234`</b>: Switch player.
+ ADDON_ACTION_SWITCH_PLAYER = 234,
+
+ /// @brief <b>`235`</b>: Stereo mode next.
+ ADDON_ACTION_STEREOMODE_NEXT = 235,
+
+ /// @brief <b>`236`</b>: Stereo mode previous.
+ ADDON_ACTION_STEREOMODE_PREVIOUS = 236,
+
+ /// @brief <b>`237`</b>: Turns 3d mode on/off.
+ ADDON_ACTION_STEREOMODE_TOGGLE = 237,
+
+ /// @brief <b>`238`</b>: Stereo mode select.
+ ADDON_ACTION_STEREOMODE_SELECT = 238,
+
+ /// @brief <b>`239`</b>: Stereo mode to mono.
+ ADDON_ACTION_STEREOMODE_TOMONO = 239,
+
+ /// @brief <b>`240`</b>: Stereo mode set.
+ ADDON_ACTION_STEREOMODE_SET = 240,
+
+ /// @brief <b>`241`</b>: Settings reset.
+ ADDON_ACTION_SETTINGS_RESET = 241,
+
+ /// @brief <b>`242`</b>: Settings level change.
+ ADDON_ACTION_SETTINGS_LEVEL_CHANGE = 242,
+
+ /// @brief <b>`243`</b>: Show autoclosing OSD. Can b used in videoFullScreen.xml window id=2005
+ ADDON_ACTION_TRIGGER_OSD = 243,
+
+ /// @brief <b>`244`</b>: Input text.
+ ADDON_ACTION_INPUT_TEXT = 244,
+
+ /// @brief <b>`245`</b>: Volume set.
+ ADDON_ACTION_VOLUME_SET = 245,
+
+ /// @brief <b>`246`</b>: Toggle commercial skip.
+ ADDON_ACTION_TOGGLE_COMMSKIP = 246,
+
+ /// @brief <b>`247`</b>: Browse for subtitle. Can be used in videofullscreen
+ ADDON_ACTION_BROWSE_SUBTITLE = 247,
+
+ /// @brief <b>`248`</b>: Send a reset command to the active game
+ ADDON_ACTION_PLAYER_RESET = 248,
+
+ /// @brief <b>`249`</b>: Toggle font. Used in TextViewer dialog
+ ADDON_ACTION_TOGGLE_FONT = 249,
+
+ /// @brief <b>`250`</b>: Cycle video streams. Used in videofullscreen.
+ ADDON_ACTION_VIDEO_NEXT_STREAM = 250,
+
+ /// @brief <b>`251`</b>: Used to queue an item to the next position in the playlist
+ ADDON_ACTION_QUEUE_ITEM_NEXT = 251,
+
+ /// @brief <b>`247`</b>: Toggle display HDR on/off
+ ADDON_ACTION_HDR_TOGGLE = 260,
+
+ /// @brief <b>`300`</b>: Voice actions
+ ADDON_ACTION_VOICE_RECOGNIZE = 300,
+
+ // Number 347 used om front by ADDON_ACTION_BROWSE_SUBTITLE
+
+ /// @brief <b>`401`</b>: Touch actions
+ ADDON_ACTION_TOUCH_TAP = 401,
+
+ /// @brief <b>`410`</b>: Touch actions
+ ADDON_ACTION_TOUCH_TAP_TEN = 410,
+
+ /// @brief <b>`411`</b>: Touch actions
+ ADDON_ACTION_TOUCH_LONGPRESS = 411,
+
+ /// @brief <b>`412`</b>: Touch actions
+ ADDON_ACTION_TOUCH_LONGPRESS_TEN = 420,
+
+ /// @brief <b>`500`</b>: Gesture notify.
+ ADDON_ACTION_GESTURE_NOTIFY = 500,
+
+ /// @brief <b>`501`</b>: Gesture begin.
+ ADDON_ACTION_GESTURE_BEGIN = 501,
+
+ /// @brief <b>`502`</b>: Send action with point and currentPinchScale (fingers together < 1.0 -> fingers apart > 1.0)
+ ADDON_ACTION_GESTURE_ZOOM = 502,
+
+ /// @brief <b>`503`</b>: Gesture rotate.
+ ADDON_ACTION_GESTURE_ROTATE = 503,
+
+ /// @brief <b>`504`</b>: Gesture pan.
+ ADDON_ACTION_GESTURE_PAN = 504,
+
+ /// @brief <b>`505`</b>: Gesture was interrupted in unspecified state
+ ADDON_ACTION_GESTURE_ABORT = 505,
+
+ /// @brief <b>`511`</b>: Gesture swipe left.
+ ADDON_ACTION_GESTURE_SWIPE_LEFT = 511,
+
+ /// @brief <b>`520`</b>: Gesture swipe left ten
+ ADDON_ACTION_GESTURE_SWIPE_LEFT_TEN = 520,
+
+ /// @brief <b>`521`</b>: Gesture swipe right
+ ADDON_ACTION_GESTURE_SWIPE_RIGHT = 521,
+
+ /// @brief <b>`530`</b>: Gesture swipe right ten
+ ADDON_ACTION_GESTURE_SWIPE_RIGHT_TEN = 530,
+
+ /// @brief <b>`531`</b>: Gesture swipe up
+ ADDON_ACTION_GESTURE_SWIPE_UP = 531,
+
+ /// @brief <b>`540`</b>: Gesture swipe up ten
+ ADDON_ACTION_GESTURE_SWIPE_UP_TEN = 540,
+
+ /// @brief <b>`541`</b>: Gesture swipe down.
+ ADDON_ACTION_GESTURE_SWIPE_DOWN = 541,
+
+ /// @brief <b>`550`</b>: Gesture swipe down ten.
+ ADDON_ACTION_GESTURE_SWIPE_DOWN_TEN = 550,
+
+ /// @brief <b>`599`</b>: 5xx is reserved for additional gesture actions
+ ADDON_ACTION_GESTURE_END = 599,
+
+ // other, non-gesture actions
+
+ /// @brief <b>`601`</b>: Analog thumbstick move, horizontal axis, left; see ADDON_ACTION_ANALOG_MOVE
+ ADDON_ACTION_ANALOG_MOVE_X_LEFT = 601,
+
+ /// @brief <b>`602`</b>: Analog thumbstick move, horizontal axis, right; see ADDON_ACTION_ANALOG_MOVE
+ ADDON_ACTION_ANALOG_MOVE_X_RIGHT = 602,
+
+ /// @brief <b>`603`</b>: Analog thumbstick move, vertical axis, up; see ADDON_ACTION_ANALOG_MOVE
+ ADDON_ACTION_ANALOG_MOVE_Y_UP = 603,
+
+ /// @brief <b>`604`</b>: Analog thumbstick move, vertical axis, down; see ADDON_ACTION_ANALOG_MOVE
+ ADDON_ACTION_ANALOG_MOVE_Y_DOWN = 604,
+
+ /// @brief <b>`998`</b>: ERROR action is used to play an error sound.
+ ADDON_ACTION_ERROR = 998,
+
+ /// @brief <b>`999`</b>: The NOOP action can be specified to disable an input event. This is
+ /// useful in user keyboard.xml etc to disable actions specified in the
+ /// system mappings.
+ ADDON_ACTION_NOOP = 999
+ ///@}
+};
+///@}
+
+#endif /* !C_API_GUI_ACTION_IDS_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/list_item.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/list_item.h
new file mode 100644
index 0000000..d2120ac
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/list_item.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_LIST_ITEM_H
+#define C_API_GUI_LIST_ITEM_H
+
+#include "definitions.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_listItem
+ {
+ KODI_GUI_LISTITEM_HANDLE(*create)
+ (KODI_HANDLE kodiBase,
+ const char* label,
+ const char* label2,
+ const char* path);
+ void (*destroy)(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle);
+
+ char* (*get_label)(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle);
+ void (*set_label)(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle, const char* label);
+ char* (*get_label2)(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle);
+ void (*set_label2)(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle, const char* label);
+ char* (*get_art)(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle, const char* type);
+ void (*set_art)(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* type,
+ const char* image);
+ char* (*get_path)(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle);
+ void (*set_path)(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle, const char* path);
+ char* (*get_property)(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle, const char* key);
+ void (*set_property)(KODI_HANDLE kodiBase,
+ KODI_GUI_LISTITEM_HANDLE handle,
+ const char* key,
+ const char* value);
+ void (*select)(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle, bool select);
+ bool (*is_selected)(KODI_HANDLE kodiBase, KODI_GUI_LISTITEM_HANDLE handle);
+ } AddonToKodiFuncTable_kodi_gui_listItem;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_LIST_ITEM_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/window.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/window.h
new file mode 100644
index 0000000..d7181c0
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/gui/window.h
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+#ifndef C_API_GUI_WINDOW_H
+#define C_API_GUI_WINDOW_H
+
+#include "definitions.h"
+#include "input/action_ids.h"
+
+#include <stddef.h>
+
+#define ADDON_MAX_CONTEXT_ENTRIES 20
+#define ADDON_MAX_CONTEXT_ENTRY_NAME_LENGTH 80
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ typedef struct gui_context_menu_pair
+ {
+ unsigned int id;
+ char name[ADDON_MAX_CONTEXT_ENTRY_NAME_LENGTH];
+ } gui_context_menu_pair;
+
+ typedef struct AddonToKodiFuncTable_kodi_gui_window
+ {
+ /* Window creation functions */
+ KODI_GUI_WINDOW_HANDLE(*create)
+ (KODI_HANDLE kodiBase,
+ const char* xml_filename,
+ const char* default_skin,
+ bool as_dialog,
+ bool is_media);
+ void (*destroy)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+
+ void (*set_callbacks)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ KODI_GUI_CLIENT_HANDLE clienthandle,
+ bool (*CBInit)(KODI_GUI_CLIENT_HANDLE),
+ bool (*CBFocus)(KODI_GUI_CLIENT_HANDLE, int),
+ bool (*CBClick)(KODI_GUI_CLIENT_HANDLE, int),
+ bool (*CBOnAction)(KODI_GUI_CLIENT_HANDLE, enum ADDON_ACTION),
+ void (*CBGetContextButtons)(
+ KODI_GUI_CLIENT_HANDLE, int, gui_context_menu_pair*, unsigned int*),
+ bool (*CBOnContextButton)(KODI_GUI_CLIENT_HANDLE, int, unsigned int));
+ bool (*show)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ bool (*close)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ bool (*do_modal)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+
+ /* Window control functions */
+ bool (*set_focus_id)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ int (*get_focus_id)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ void (*set_control_label)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id,
+ const char* label);
+ void (*set_control_visible)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id,
+ bool visible);
+ void (*set_control_selected)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int control_id,
+ bool selected);
+
+ /* Window property functions */
+ void (*set_property)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ const char* value);
+ void (*set_property_int)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ int value);
+ void (*set_property_bool)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ bool value);
+ void (*set_property_double)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ double value);
+ char* (*get_property)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, const char* key);
+ int (*get_property_int)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, const char* key);
+ bool (*get_property_bool)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, const char* key);
+ double (*get_property_double)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key);
+ void (*clear_properties)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ void (*clear_property)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, const char* key);
+
+ /* List item functions */
+ void (*clear_item_list)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ void (*add_list_item)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ KODI_GUI_LISTITEM_HANDLE item,
+ int list_position);
+ void (*remove_list_item_from_position)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int list_position);
+ void (*remove_list_item)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ KODI_GUI_LISTITEM_HANDLE item);
+ KODI_GUI_LISTITEM_HANDLE(*get_list_item)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int list_position);
+ void (*set_current_list_position)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ int list_position);
+ int (*get_current_list_position)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ int (*get_list_size)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+ void (*set_container_property)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* key,
+ const char* value);
+ void (*set_container_content)(KODI_HANDLE kodiBase,
+ KODI_GUI_WINDOW_HANDLE handle,
+ const char* value);
+ int (*get_current_container_id)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+
+ /* Various functions */
+ void (*mark_dirty_region)(KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle);
+
+ /* GUI control access functions */
+ KODI_GUI_CONTROL_HANDLE(*get_control_button)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_edit)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_fade_label)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_image)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_label)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_progress)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_radio_button)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_render_addon)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_settings_slider)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_slider)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_spin)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_text_box)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_dummy1)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_dummy2)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_dummy3)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_dummy4)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_dummy5)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_dummy6)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_dummy7)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_dummy8)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_dummy9)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ KODI_GUI_CONTROL_HANDLE(*get_control_dummy10)
+ (KODI_HANDLE kodiBase, KODI_GUI_WINDOW_HANDLE handle, int control_id);
+ /* This above used to add new get_control_* functions */
+ } AddonToKodiFuncTable_kodi_gui_window;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* !C_API_GUI_WINDOW_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/network.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/network.h
new file mode 100644
index 0000000..8bd987b
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/network.h
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+#ifndef C_API_NETWORK_H
+#define C_API_NETWORK_H
+
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ /*
+ * For interface between add-on and kodi.
+ *
+ * This structure defines the addresses of functions stored inside Kodi which
+ * are then available for the add-on to call
+ *
+ * All function pointers there are used by the C++ interface functions below.
+ * You find the set of them on xbmc/addons/interfaces/General.cpp
+ *
+ * Note: For add-on development itself this is not needed
+ */
+ typedef struct AddonToKodiFuncTable_kodi_network
+ {
+ bool (*wake_on_lan)(void* kodiBase, const char* mac);
+ char* (*get_ip_address)(void* kodiBase);
+ char* (*dns_lookup)(void* kodiBase, const char* url, bool* ret);
+ char* (*url_encode)(void* kodiBase, const char* url);
+ char* (*get_hostname)(void* kodiBase);
+ bool (*is_local_host)(void* kodiBase, const char* hostname);
+ bool (*is_host_on_lan)(void* kodiBase, const char* hostname, bool offLineCheck);
+ char* (*get_user_agent)(void* kodiBase);
+ } AddonToKodiFuncTable_kodi_network;
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
+
+#endif /* C_API_NETWORK_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/CMakeLists.txt
new file mode 100644
index 0000000..0015a90
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/CMakeLists.txt
@@ -0,0 +1,9 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_c-api_platform)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/android/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/android/CMakeLists.txt
new file mode 100644
index 0000000..3704ed6
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/android/CMakeLists.txt
@@ -0,0 +1,10 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ $<$<STREQUAL:${CORE_SYSTEM_NAME},android>:system.h>
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_c-api_platform_android)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/android/system.h b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/android/system.h
new file mode 100644
index 0000000..c2174d0
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/c-api/platform/android/system.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2005-2020 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.
+ */
+
+/*---AUTO_GEN_PARSE<$$CORE_SYSTEM_NAME:android>---*/
+
+#ifndef C_API_PLATFORM_ANDROID_H
+#define C_API_PLATFORM_ANDROID_H
+
+#define INTERFACE_ANDROID_SYSTEM_NAME "ANDROID_SYSTEM"
+#define INTERFACE_ANDROID_SYSTEM_VERSION "1.0.2"
+#define INTERFACE_ANDROID_SYSTEM_VERSION_MIN "1.0.1"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ struct AddonToKodiFuncTable_android_system
+ {
+ void* (*get_jni_env)();
+ int (*get_sdk_version)();
+ const char *(*get_class_name)();
+ };
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif /* !C_API_PLATFORM_ANDROID_H */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/gui/CMakeLists.txt
new file mode 100644
index 0000000..e94f764
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/CMakeLists.txt
@@ -0,0 +1,13 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ General.h
+ ListItem.h
+ Window.h
+ renderHelper.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_gui)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/General.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/General.h
new file mode 100644
index 0000000..0c639ef
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/General.h
@@ -0,0 +1,190 @@
+/*
+ * 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 "../AddonBase.h"
+#include "../c-api/gui/general.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+
+//==============================================================================
+/// @addtogroup cpp_kodi_gui_general
+/// Permits the use of the required functions of the add-on to Kodi.
+///
+/// These are pure functions them no other initialization need.
+///
+/// It has the header @ref kodi/gui/General.h "#include <kodi/gui/General.h>" be included
+/// to enjoy it.
+///
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_general
+/// @brief Performs a graphical lock of rendering engine.
+///
+inline void ATTR_DLL_LOCAL Lock()
+{
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->general->lock();
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_general
+/// @brief Performs a graphical unlock of previous locked rendering engine.
+///
+inline void ATTR_DLL_LOCAL Unlock()
+{
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->general->unlock();
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_general
+/// @brief Return the the current screen height with pixel.
+///
+/// @return Screen height with pixel
+///
+inline int ATTR_DLL_LOCAL GetScreenHeight()
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->general->get_screen_height(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_general
+/// @brief Return the the current screen width with pixel.
+///
+/// @return Screen width with pixel
+///
+inline int ATTR_DLL_LOCAL GetScreenWidth()
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->general->get_screen_width(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_general
+/// @brief Return the the current screen rendering resolution.
+///
+/// @return Current screen rendering resolution
+///
+inline int ATTR_DLL_LOCAL GetVideoResolution()
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->general->get_video_resolution(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_general
+/// @brief Returns the id for the current 'active' dialog as an integer.
+///
+/// @return The currently active dialog Id
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// ..
+/// int wid = kodi::gui::GetCurrentWindowDialogId();
+/// ..
+/// ~~~~~~~~~~~~~
+///
+inline int ATTR_DLL_LOCAL GetCurrentWindowDialogId()
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->general->get_current_window_dialog_id(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_general
+/// @brief Returns the id for the current 'active' window as an integer.
+///
+/// @return The currently active window Id
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// ..
+/// int wid = kodi::gui::GetCurrentWindowId();
+/// ..
+/// ~~~~~~~~~~~~~
+///
+inline int ATTR_DLL_LOCAL GetCurrentWindowId()
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->general->get_current_window_id(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_general
+/// @brief To get hardware specific device context interface.
+///
+/// @return A pointer to the used device with @ref cpp_kodi_Defs_HardwareContext "kodi::HardwareContext"
+///
+/// @warning This function is only be supported under Windows, on all other
+/// OS it return `nullptr`!
+///
+/// @note Returned Windows class pointer is `ID3D11DeviceContext1`.
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <d3d11_1.h>
+/// ..
+/// ID3D11DeviceContext1* context = static_cast<ID3D11DeviceContext1*>(kodi::gui::GetHWContext());
+/// ..
+/// ~~~~~~~~~~~~~
+///
+inline kodi::HardwareContext GetHWContext()
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->general->get_hw_context(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_general
+/// @brief Get Adjust refresh rate setting status.
+///
+/// @return The Adjust refresh rate setting status
+///
+inline AdjustRefreshRateStatus ATTR_DLL_LOCAL GetAdjustRefreshRateStatus()
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->general->get_adjust_refresh_rate_status(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+}
+//------------------------------------------------------------------------------
+
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/ListItem.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/ListItem.h
new file mode 100644
index 0000000..0d3e057
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/ListItem.h
@@ -0,0 +1,345 @@
+/*
+ * 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 "../AddonBase.h"
+#include "../c-api/gui/list_item.h"
+
+#ifdef __cplusplus
+
+#include <memory>
+
+namespace kodi
+{
+namespace gui
+{
+
+class CWindow;
+
+class ATTR_DLL_LOCAL CAddonGUIControlBase
+{
+public:
+ KODI_GUI_LISTITEM_HANDLE GetControlHandle() const { return m_controlHandle; }
+
+protected:
+ explicit CAddonGUIControlBase(CAddonGUIControlBase* window)
+ : m_controlHandle(nullptr),
+ m_interface(::kodi::addon::CPrivateBase::m_interface->toKodi),
+ m_Window(window)
+ {
+ }
+
+ virtual ~CAddonGUIControlBase() = default;
+
+ friend class CWindow;
+
+ KODI_GUI_LISTITEM_HANDLE m_controlHandle;
+ AddonToKodiFuncTable_Addon* m_interface;
+ CAddonGUIControlBase* m_Window;
+
+private:
+ CAddonGUIControlBase() = delete;
+ CAddonGUIControlBase(const CAddonGUIControlBase&) = delete;
+ CAddonGUIControlBase& operator=(const CAddonGUIControlBase&) = delete;
+};
+
+class CListItem;
+
+//==============================================================================
+/// @addtogroup cpp_kodi_gui_windows_listitem
+/// @brief @cpp_class{ kodi::gui::CListItem }
+/// **Selectable window list item**\n
+/// The list item control is used for creating item lists in Kodi.
+///
+/// The with @ref ListItem.h "#include <kodi/gui/ListItem.h>" given
+/// class is used to create a item entry for a list on window and to support it's
+/// control.
+///
+class ATTR_DLL_LOCAL CListItem : public CAddonGUIControlBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Class constructor with parameters.
+ ///
+ /// @param[in] label [opt] Item label
+ /// @param[in] label2 [opt] Second Item label (if needed)
+ /// @param[in] path [opt] Path to where item is defined
+ ///
+ CListItem(const std::string& label = "",
+ const std::string& label2 = "",
+ const std::string& path = "")
+ : CAddonGUIControlBase(nullptr)
+ {
+ m_controlHandle = m_interface->kodi_gui->listItem->create(m_interface->kodiBase, label.c_str(),
+ label2.c_str(), path.c_str());
+ }
+
+ /*
+ * Constructor used for parts given by list items from addon window
+ *
+ * Related to call of "std::shared_ptr<CListItem> kodi::gui::CWindow::GetListItem(int listPos)"
+ * Not needed for addon development itself
+ */
+ explicit CListItem(KODI_GUI_LISTITEM_HANDLE listItemHandle) : CAddonGUIControlBase(nullptr)
+ {
+ m_controlHandle = listItemHandle;
+ }
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Class destructor
+ ///
+ ~CListItem() override
+ {
+ m_interface->kodi_gui->listItem->destroy(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Returns the listitem label.
+ ///
+ /// @return Label of item
+ ///
+ std::string GetLabel()
+ {
+ std::string label;
+ char* ret = m_interface->kodi_gui->listItem->get_label(m_interface->kodiBase, m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ label = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return label;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Sets the listitem label.
+ ///
+ /// @param[in] label string or unicode - text string.
+ ///
+ void SetLabel(const std::string& label)
+ {
+ m_interface->kodi_gui->listItem->set_label(m_interface->kodiBase, m_controlHandle,
+ label.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Returns the second listitem label.
+ ///
+ /// @return Second label of item
+ ///
+ std::string GetLabel2()
+ {
+ std::string label;
+ char* ret = m_interface->kodi_gui->listItem->get_label2(m_interface->kodiBase, m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ label = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return label;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Sets the listitem's label2.
+ ///
+ /// @param[in] label string or unicode - text string.
+ ///
+ void SetLabel2(const std::string& label)
+ {
+ m_interface->kodi_gui->listItem->set_label2(m_interface->kodiBase, m_controlHandle,
+ label.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Sets the listitem's art
+ ///
+ /// @param[in] type Type of Art to set
+ /// - Some default art values (any string possible):
+ /// | value (type) | Type |
+ /// |:-------------:|:--------------------------------------------------|
+ /// | thumb | string - image filename
+ /// | poster | string - image filename
+ /// | banner | string - image filename
+ /// | fanart | string - image filename
+ /// | clearart | string - image filename
+ /// | clearlogo | string - image filename
+ /// | landscape | string - image filename
+ /// | icon | string - image filename
+ /// @return The url to use for Art
+ ///
+ std::string GetArt(const std::string& type)
+ {
+ std::string strReturn;
+ char* ret = m_interface->kodi_gui->listItem->get_art(m_interface->kodiBase, m_controlHandle,
+ type.c_str());
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ strReturn = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return strReturn;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Sets the listitem's art
+ ///
+ /// @param[in] type Type of Art to set
+ /// @param[in] url The url to use for Art
+ /// - Some default art values (any string possible):
+ /// | value (type) | Type |
+ /// |:-------------:|:--------------------------------------------------|
+ /// | thumb | string - image filename
+ /// | poster | string - image filename
+ /// | banner | string - image filename
+ /// | fanart | string - image filename
+ /// | clearart | string - image filename
+ /// | clearlogo | string - image filename
+ /// | landscape | string - image filename
+ /// | icon | string - image filename
+ ///
+ void SetArt(const std::string& type, const std::string& url)
+ {
+ m_interface->kodi_gui->listItem->set_art(m_interface->kodiBase, m_controlHandle, type.c_str(),
+ url.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Returns the path / filename of this listitem.
+ ///
+ /// @return Path string
+ ///
+ std::string GetPath()
+ {
+ std::string strReturn;
+ char* ret = m_interface->kodi_gui->listItem->get_path(m_interface->kodiBase, m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ strReturn = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return strReturn;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Sets the listitem's path.
+ ///
+ /// @param[in] path string or unicode - path, activated when item is clicked.
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ void SetPath(const std::string& path)
+ {
+ m_interface->kodi_gui->listItem->set_path(m_interface->kodiBase, m_controlHandle, path.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Sets a listitem property, similar to an infolabel.
+ ///
+ /// @param[in] key string - property name.
+ /// @param[in] value string or unicode - value of property.
+ ///
+ /// @note Key is NOT case sensitive.
+ /// You can use the above as keywords for arguments and skip certain@n
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.
+ ///
+ /// Some of these are treated internally by Kodi, such as the
+ /// <b>'StartOffset'</b> property, which is the offset in seconds at which to
+ /// start playback of an item. Others may be used in the skin to add
+ /// extra information, such as <b>'WatchedCount'</b> for tvshow items
+ ///
+ void SetProperty(const std::string& key, const std::string& value)
+ {
+ m_interface->kodi_gui->listItem->set_property(m_interface->kodiBase, m_controlHandle,
+ key.c_str(), value.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Returns a listitem property as a string, similar to an infolabel.
+ ///
+ /// @param[in] key string - property name.
+ /// @return string - List item property
+ ///
+ /// @note Key is NOT case sensitive.\n
+ /// You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.
+ ///
+ std::string GetProperty(const std::string& key)
+ {
+ std::string label;
+ char* ret = m_interface->kodi_gui->listItem->get_property(m_interface->kodiBase,
+ m_controlHandle, key.c_str());
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ label = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return label;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief To control selection of item in list (also multiple selection,
+ /// in list on several items possible).
+ ///
+ /// @param[in] selected if true becomes set as selected
+ ///
+ void Select(bool selected)
+ {
+ m_interface->kodi_gui->listItem->select(m_interface->kodiBase, m_controlHandle, selected);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_listitem
+ /// @brief Returns the listitem's selected status.
+ ///
+ /// @return true if selected, otherwise false
+ ///
+ bool IsSelected()
+ {
+ return m_interface->kodi_gui->listItem->is_selected(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+};
+
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/Window.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/Window.h
new file mode 100644
index 0000000..e424c82
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/Window.h
@@ -0,0 +1,915 @@
+/*
+ * 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 "../AddonBase.h"
+#include "../c-api/gui/window.h"
+#include "ListItem.h"
+#include "input/ActionIDs.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_windows_window_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_gui_windows_window
+/// @brief **Library definition values**\n
+/// Additional values, structures and things that are used in the Window class.
+///
+/// ------------------------------------------------------------------------
+///
+/// @link cpp_kodi_gui_windows_window Go back to normal functions from CWindow@endlink
+///
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_windows_window_Defs
+/// @brief **Handler for addon-sided processing class**\n
+/// If the callback functions used by the window are not used directly in the
+/// @ref cpp_kodi_gui_windows_window "CWindow" class and are outside of it.
+///
+/// This value here corresponds to a <b>`void*`</b> and returns the address
+/// requested by the add-on for callbacks.
+///
+using ClientHandle = KODI_GUI_CLIENT_HANDLE;
+//------------------------------------------------------------------------------
+
+class CListItem;
+
+//==============================================================================
+/// @addtogroup cpp_kodi_gui_windows_window
+/// @brief @cpp_class{ kodi::gui::CWindow }
+/// **Main window control class**\n
+/// The addon uses its own skin xml file and displays it in Kodi using this class.
+///
+/// The with @ref Window.h "#include <kodi/gui/Window.h>"
+/// included file brings support to create a window or dialog on Kodi.
+///
+/// The add-on has to integrate its own @ref cpp_kodi_gui_windows_window_callbacks "callback functions"
+/// in order to process the necessary user access to its window.
+///
+///
+/// --------------------------------------------------------------------------
+///
+/// **Window header example:**
+/// ~~~~~~~~~~~~~{.xml}
+/// <?xml version="1.0" encoding="UTF-8"?>
+/// <window>
+/// <onload>RunScript(script.foobar)</onload>
+/// <onunload>SetProperty(foo,bar)</onunload>
+/// <defaultcontrol always="false">2</defaultcontrol>
+/// <menucontrol>9000</menucontrol>
+/// <backgroundcolor>0xff00ff00</backgroundcolor>
+/// <views>50,51,509,510</views>
+/// <visible>Window.IsActive(Home)</visible>
+/// <animation effect="fade" time="100">WindowOpen</animation>
+/// <animation effect="slide" end="0,576" time="100">WindowClose</animation>
+/// <zorder>1</zorder>
+/// <coordinates>
+/// <left>40</left>
+/// <top>50</top>
+/// <origin x="100" y="50">Window.IsActive(Home)</origin>
+/// </coordinates>
+/// <previouswindow>MyVideos</previouswindow>
+/// <controls>
+/// <control>
+/// </control>
+/// ....
+/// </controls>
+/// </window>
+/// ~~~~~~~~~~~~~
+///
+/// --------------------------------------------------------------------------
+///
+/// On functions defined input variable <b><tt>controlId</tt> (GUI control identifier)</b>
+/// is the on window.xml defined value behind type added with <tt><b>id="..."</b></tt> and
+/// used to identify for changes there and on callbacks.
+///
+/// ~~~~~~~~~~~~~{.xml}
+/// <control type="label" id="31">
+/// <description>Title Label</description>
+/// ...
+/// </control>
+/// <control type="progress" id="32">
+/// <description>progress control</description>
+/// ...
+/// </control>
+/// ~~~~~~~~~~~~~
+///
+///
+class ATTR_DLL_LOCAL CWindow : public CAddonGUIControlBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Class constructor with needed values for window / dialog.
+ ///
+ /// Creates a new Window class.
+ ///
+ /// @param[in] xmlFilename XML file for the skin
+ /// @param[in] defaultSkin Default skin to use if needed not available
+ /// @param[in] asDialog Use window as dialog if set
+ /// @param[in] isMedia [opt] bool - if False, create a regular window.
+ /// if True, create a mediawindow. (default=false)
+ ///
+ /// @note <b>`isMedia`</b> value as true only usable for windows not for dialogs.
+ ///
+ CWindow(const std::string& xmlFilename,
+ const std::string& defaultSkin,
+ bool asDialog,
+ bool isMedia = false)
+ : CAddonGUIControlBase(nullptr)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->create(
+ m_interface->kodiBase, xmlFilename.c_str(), defaultSkin.c_str(), asDialog, isMedia);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL, "kodi::gui::CWindow can't create window class from Kodi !!!");
+ m_interface->kodi_gui->window->set_callbacks(m_interface->kodiBase, m_controlHandle, this,
+ CBOnInit, CBOnFocus, CBOnClick, CBOnAction,
+ CBGetContextButtons, CBOnContextButton);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup CWindow
+ /// @brief Class destructor.
+ ///
+ ~CWindow() override
+ {
+ if (m_controlHandle)
+ m_interface->kodi_gui->window->destroy(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Show this window.
+ ///
+ /// Shows this window by activating it, calling close() after it will activate
+ /// the current window again.
+ ///
+ /// @note If your Add-On ends this window will be closed to. To show it forever,
+ /// make a loop at the end of your Add-On or use @ref DoModal() instead.
+ ///
+ /// @warning If used must be the class be global present until Kodi becomes
+ /// closed. The creation can be done before "Show" becomes called, but
+ /// not delete class after them.
+ ///
+ /// @return Return true if call and show is successed, if false was something
+ /// failed to get needed skin parts.
+ ///
+ bool Show()
+ {
+ return m_interface->kodi_gui->window->show(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Closes this window.
+ ///
+ /// Closes this window by activating the old window.
+ /// @note The window is not deleted with this method.
+ ///
+ void Close() { m_interface->kodi_gui->window->close(m_interface->kodiBase, m_controlHandle); }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Display this window until close() is called.
+ ///
+ void DoModal()
+ {
+ m_interface->kodi_gui->window->do_modal(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Gives the control with the supplied focus.
+ ///
+ /// @param[in] controlId On skin defined id of control
+ /// @return Return true if call and focus is successed, if false was something
+ /// failed to get needed skin parts
+ ///
+ bool SetFocusId(int controlId)
+ {
+ return m_interface->kodi_gui->window->set_focus_id(m_interface->kodiBase, m_controlHandle,
+ controlId);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Returns the id of the control which is focused.
+ ///
+ /// @return Focused control id
+ ///
+ int GetFocusId()
+ {
+ return m_interface->kodi_gui->window->get_focus_id(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief To set the used label on given control id.
+ ///
+ /// @param[in] controlId Control id where label need to set
+ /// @param[in] label Label to use
+ ///
+ void SetControlLabel(int controlId, const std::string& label)
+ {
+ m_interface->kodi_gui->window->set_control_label(m_interface->kodiBase, m_controlHandle,
+ controlId, label.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief To set the visibility on given control id.
+ ///
+ /// @param[in] controlId Control id where visibility is changed
+ /// @param[in] visible Boolean value with `true` for visible, `false` for hidden
+ ///
+ void SetControlVisible(int controlId, bool visible)
+ {
+ m_interface->kodi_gui->window->set_control_visible(m_interface->kodiBase, m_controlHandle,
+ controlId, visible);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief To set the selection on given control id.
+ ///
+ /// @param[in] controlId Control id where selection is changed
+ /// @param[in] selected Boolean value with `true` for selected, `false` for not
+ ///
+ void SetControlSelected(int controlId, bool selected)
+ {
+ m_interface->kodi_gui->window->set_control_selected(m_interface->kodiBase, m_controlHandle,
+ controlId, selected);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Sets a window property, similar to an infolabel.
+ ///
+ /// @param[in] key string - property name.
+ /// @param[in] value string or unicode - value of property.
+ ///
+ /// @note Key is NOT case sensitive. Setting value to an empty string is
+ /// equivalent to clearProperty(key).\n
+ /// You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.
+ ///
+ void SetProperty(const std::string& key, const std::string& value)
+ {
+ m_interface->kodi_gui->window->set_property(m_interface->kodiBase, m_controlHandle, key.c_str(),
+ value.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Returns a window property as a string, similar to an infolabel.
+ ///
+ /// @param[in] key string - property name.
+ /// @return The property as string (if present)
+ ///
+ /// @note Key is NOT case sensitive. Setting value to an empty string is
+ /// equivalent to clearProperty(key).\n
+ /// You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.
+ ///
+ std::string GetProperty(const std::string& key) const
+ {
+ std::string label;
+ char* ret = m_interface->kodi_gui->window->get_property(m_interface->kodiBase, m_controlHandle,
+ key.c_str());
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ label = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return label;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Sets a window property with integer value
+ ///
+ /// @param[in] key string - property name.
+ /// @param[in] value integer value to set
+ ///
+ void SetPropertyInt(const std::string& key, int value)
+ {
+ m_interface->kodi_gui->window->set_property_int(m_interface->kodiBase, m_controlHandle,
+ key.c_str(), value);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Returns a window property with integer value
+ ///
+ /// @param[in] key string - property name.
+ /// @return integer value of property
+ ///
+ int GetPropertyInt(const std::string& key) const
+ {
+ return m_interface->kodi_gui->window->get_property_int(m_interface->kodiBase, m_controlHandle,
+ key.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Sets a window property with boolean value
+ ///
+ /// @param[in] key string - property name.
+ /// @param[in] value boolean value to set
+ ///
+ void SetPropertyBool(const std::string& key, bool value)
+ {
+ m_interface->kodi_gui->window->set_property_bool(m_interface->kodiBase, m_controlHandle,
+ key.c_str(), value);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Returns a window property with boolean value
+ ///
+ /// @param[in] key string - property name.
+ /// @return boolean value of property
+ ///
+ bool GetPropertyBool(const std::string& key) const
+ {
+ return m_interface->kodi_gui->window->get_property_bool(m_interface->kodiBase, m_controlHandle,
+ key.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Sets a window property with double value
+ ///
+ /// @param[in] key string - property name.
+ /// @param[in] value double value to set
+ ///
+ void SetPropertyDouble(const std::string& key, double value)
+ {
+ m_interface->kodi_gui->window->set_property_double(m_interface->kodiBase, m_controlHandle,
+ key.c_str(), value);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Returns a window property with double value
+ ///
+ /// @param[in] key string - property name.
+ /// @return double value of property
+ ///
+ double GetPropertyDouble(const std::string& key) const
+ {
+ return m_interface->kodi_gui->window->get_property_double(m_interface->kodiBase,
+ m_controlHandle, key.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Remove all present properties from window
+ ///
+ void ClearProperties()
+ {
+ m_interface->kodi_gui->window->clear_properties(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Clears the specific window property.
+ ///
+ /// @param[in] key string - property name.
+ ///
+ /// @note Key is NOT case sensitive. Equivalent to SetProperty(key, "")
+ /// You can use the above as keywords for arguments and skip certain
+ /// optional arguments.
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ..
+ /// ClearProperty('Category')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ void ClearProperty(const std::string& key)
+ {
+ m_interface->kodi_gui->window->clear_property(m_interface->kodiBase, m_controlHandle,
+ key.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ /// @{
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Function delete all entries in integrated list.
+ ///
+ void ClearList()
+ {
+ m_interface->kodi_gui->window->clear_item_list(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief To add a list item in the on window integrated list.
+ ///
+ /// @param[in] item List item to add
+ /// @param[in] itemPosition [opt] The position for item, default is on end
+ ///
+ void AddListItem(const std::shared_ptr<CListItem>& item, int itemPosition = -1)
+ {
+ m_interface->kodi_gui->window->add_list_item(m_interface->kodiBase, m_controlHandle,
+ item->m_controlHandle, itemPosition);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief To add a list item based upon string in the on window integrated list.
+ ///
+ /// @param[in] item List item to add
+ /// @param[in] itemPosition [opt] The position for item, default is on end
+ ///
+ void AddListItem(const std::string& item, int itemPosition = -1)
+ {
+ m_interface->kodi_gui->window->add_list_item(
+ m_interface->kodiBase, m_controlHandle,
+ std::make_shared<kodi::gui::CListItem>(item)->m_controlHandle, itemPosition);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Remove list item on position.
+ ///
+ /// @param[in] itemPosition List position to remove
+ ///
+ void RemoveListItem(int itemPosition)
+ {
+ m_interface->kodi_gui->window->remove_list_item_from_position(m_interface->kodiBase,
+ m_controlHandle, itemPosition);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Remove item with given control class from list.
+ ///
+ /// @param[in] item List item control class to remove
+ ///
+ void RemoveListItem(const std::shared_ptr<CListItem>& item)
+ {
+ m_interface->kodi_gui->window->remove_list_item(m_interface->kodiBase, m_controlHandle,
+ item->m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief To get list item control class on wanted position.
+ ///
+ /// @param[in] listPos Position from where control is needed
+ /// @return The list item control class or null if not found
+ ///
+ /// @warning Function returns a new generated **CListItem** class!
+ ///
+ std::shared_ptr<CListItem> GetListItem(int listPos)
+ {
+ KODI_GUI_LISTITEM_HANDLE handle = m_interface->kodi_gui->window->get_list_item(
+ m_interface->kodiBase, m_controlHandle, listPos);
+ if (!handle)
+ return std::shared_ptr<CListItem>();
+
+ return std::make_shared<kodi::gui::CListItem>(handle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief To set position of selected part in list.
+ ///
+ /// @param[in] listPos Position to use
+ ///
+ void SetCurrentListPosition(int listPos)
+ {
+ m_interface->kodi_gui->window->set_current_list_position(m_interface->kodiBase, m_controlHandle,
+ listPos);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief To get current selected position in list
+ ///
+ /// @return Current list position
+ ///
+ int GetCurrentListPosition()
+ {
+ return m_interface->kodi_gui->window->get_current_list_position(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief To get the amount of entries in the list.
+ ///
+ /// @return Size of in window integrated control class
+ ///
+ int GetListSize()
+ {
+ return m_interface->kodi_gui->window->get_list_size(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Sets a container property, similar to an infolabel.
+ ///
+ /// @param[in] key string - property name.
+ /// @param[in] value string or unicode - value of property.
+ ///
+ /// @note Key is NOT case sensitive.\n
+ /// You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.
+ ///
+ void SetContainerProperty(const std::string& key, const std::string& value)
+ {
+ m_interface->kodi_gui->window->set_container_property(m_interface->kodiBase, m_controlHandle,
+ key.c_str(), value.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Sets the content type of the container.
+ ///
+ /// @param[in] value string or unicode - content value.
+ ///
+ /// __Available content types__
+ /// | Name | Media |
+ /// |:-----------:|:-----------------------------------------|
+ /// | actors | Videos
+ /// | addons | Addons, Music, Pictures, Programs, Videos
+ /// | albums | Music, Videos
+ /// | artists | Music, Videos
+ /// | countries | Music, Videos
+ /// | directors | Videos
+ /// | files | Music, Videos
+ /// | games | Games
+ /// | genres | Music, Videos
+ /// | images | Pictures
+ /// | mixed | Music, Videos
+ /// | movies | Videos
+ /// | Musicvideos | Music, Videos
+ /// | playlists | Music, Videos
+ /// | seasons | Videos
+ /// | sets | Videos
+ /// | songs | Music
+ /// | studios | Music, Videos
+ /// | tags | Music, Videos
+ /// | tvshows | Videos
+ /// | videos | Videos
+ /// | years | Music, Videos
+ ///
+ void SetContainerContent(const std::string& value)
+ {
+ m_interface->kodi_gui->window->set_container_content(m_interface->kodiBase, m_controlHandle,
+ value.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief Get the id of the currently visible container.
+ ///
+ /// @return currently visible container id
+ ///
+ int GetCurrentContainerId()
+ {
+ return m_interface->kodi_gui->window->get_current_container_id(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+ /// @}
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @brief To inform Kodi that it need to render region new.
+ ///
+ void MarkDirtyRegion()
+ {
+ return m_interface->kodi_gui->window->mark_dirty_region(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_gui_windows_window_callbacks Callback functions from Kodi to add-on
+ /// @ingroup cpp_kodi_gui_windows_window
+ /// @{
+ /// @brief <b>GUI window callback functions.</b>\n
+ /// Functions to handle control callbacks from Kodi
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// @link cpp_kodi_gui_windows_window Go back to normal functions from CWindow@endlink
+ //
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window_callbacks
+ /// @brief OnInit method.
+ ///
+ /// @return Return true if initialize was done successful
+ ///
+ ///
+ virtual bool OnInit() { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window_callbacks
+ /// @brief OnFocus method.
+ ///
+ /// @param[in] controlId GUI control identifier
+ /// @return Return true if focus condition was handled there or false to handle
+ /// them by Kodi itself
+ ///
+ ///
+ virtual bool OnFocus(int controlId) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window_callbacks
+ /// @brief OnClick method.
+ ///
+ /// @param[in] controlId GUI control identifier
+ /// @return Return true if click was handled there or false to handle them by
+ /// Kodi itself
+ ///
+ ///
+ virtual bool OnClick(int controlId) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window_callbacks
+ /// @brief OnAction method.
+ ///
+ /// @param[in] actionId The action id to perform, see
+ /// @ref kodi_key_action_ids to get list of
+ /// them
+ /// @return Return true if action was handled there
+ /// or false to handle them by Kodi itself
+ ///
+ ///
+ /// This method will receive all actions that the main program will send
+ /// to this window.
+ ///
+ /// @note
+ /// - By default, only the @c ADDON_ACTION_PREVIOUS_MENU and @c ADDON_ACTION_NAV_BACK actions are handled.
+ /// - Overwrite this method to let your code handle all actions.
+ /// - Don't forget to capture @ref ADDON_ACTION_PREVIOUS_MENU or @ref ADDON_ACTION_NAV_BACK, else the user can't close this window.
+ ///
+ ///
+ ///----------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ..
+ /// // Window used with parent / child way
+ /// bool cYOUR_CLASS::OnAction(ADDON_ACTION actionId)
+ /// {
+ /// switch (action)
+ /// {
+ /// case ADDON_ACTION_PREVIOUS_MENU:
+ /// case ADDON_ACTION_NAV_BACK:
+ /// printf("action received: previous");
+ /// Close();
+ /// return true;
+ /// case ADDON_ACTION_SHOW_INFO:
+ /// printf("action received: show info");
+ /// break;
+ /// case ADDON_ACTION_STOP:
+ /// printf("action received: stop");
+ /// break;
+ /// case ADDON_ACTION_PAUSE:
+ /// printf("action received: pause");
+ /// break;
+ /// default:
+ /// break;
+ /// }
+ /// return false;
+ /// }
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual bool OnAction(ADDON_ACTION actionId)
+ {
+ switch (actionId)
+ {
+ case ADDON_ACTION_PREVIOUS_MENU:
+ case ADDON_ACTION_NAV_BACK:
+ Close();
+ return true;
+ default:
+ break;
+ }
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window_callbacks
+ /// @brief Get context menu buttons for list entry.
+ ///
+ /// @param[in] itemNumber Selected list item entry
+ /// @param[in] buttons List where context menus becomes added with his
+ /// identifier and name
+ ///
+ virtual void GetContextButtons(int itemNumber,
+ std::vector<std::pair<unsigned int, std::string>>& buttons)
+ {
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window_callbacks
+ /// @brief Called after selection in context menu.
+ ///
+ /// @param[in] itemNumber Selected list item entry
+ /// @param[in] button The pressed button id
+ /// @return true if handled, otherwise false
+ ///
+ virtual bool OnContextButton(int itemNumber, unsigned int button) { return false; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_window_callbacks
+ /// @brief **Set independent callbacks**
+ ///
+ /// If the class is used independent (with "new CWindow") and
+ /// not as parent (with \"cCLASS_own : public @ref cpp_kodi_gui_windows_window "kodi::gui::CWindow"\") from own must be the
+ /// callback from Kodi to add-on overdriven with own functions!
+ ///
+ /// @param[in] cbhdl The pointer to own handle data structure / class
+ /// @param[in] CBOnInit Own defined window init function
+ /// @param[in] CBOnFocus Own defined focus function
+ /// @param[in] CBOnClick Own defined click function
+ /// @param[in] CBOnAction Own defined action function
+ /// @param[in] CBGetContextButtons [opt] To get context menu entries for
+ /// lists function
+ /// @param[in] CBOnContextButton [opt] Used context menu entry function
+ ///
+ ///
+ ///----------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// ...
+ ///
+ /// bool OnInit(kodi::gui::ClientHandle cbhdl)
+ /// {
+ /// ...
+ /// return true;
+ /// }
+ ///
+ /// bool OnFocus(kodi::gui::ClientHandle cbhdl, int controlId)
+ /// {
+ /// ...
+ /// return true;
+ /// }
+ ///
+ /// bool OnClick(kodi::gui::ClientHandle cbhdl, int controlId)
+ /// {
+ /// ...
+ /// return true;
+ /// }
+ ///
+ /// bool OnAction(kodi::gui::ClientHandle cbhdl, ADDON_ACTION actionId)
+ /// {
+ /// ...
+ /// return true;
+ /// }
+ ///
+ /// ...
+ /// // Somewhere where you create the window
+ /// CWindow myWindow = new CWindow;
+ /// myWindow->SetIndependentCallbacks(myWindow, OnInit, OnFocus, OnClick, OnAction);
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ void SetIndependentCallbacks(kodi::gui::ClientHandle cbhdl,
+ bool (*CBOnInit)(kodi::gui::ClientHandle cbhdl),
+ bool (*CBOnFocus)(kodi::gui::ClientHandle cbhdl, int controlId),
+ bool (*CBOnClick)(kodi::gui::ClientHandle cbhdl, int controlId),
+ bool (*CBOnAction)(kodi::gui::ClientHandle cbhdl,
+ ADDON_ACTION actionId),
+ void (*CBGetContextButtons)(kodi::gui::ClientHandle cbhdl,
+ int itemNumber,
+ gui_context_menu_pair* buttons,
+ unsigned int* size) = nullptr,
+ bool (*CBOnContextButton)(kodi::gui::ClientHandle cbhdl,
+ int itemNumber,
+ unsigned int button) = nullptr)
+ {
+ if (!cbhdl || !CBOnInit || !CBOnFocus || !CBOnClick || !CBOnAction)
+ {
+ kodi::Log(ADDON_LOG_FATAL, "kodi::gui::CWindow::%s called with nullptr !!!", __FUNCTION__);
+ return;
+ }
+
+ m_interface->kodi_gui->window->set_callbacks(m_interface->kodiBase, m_controlHandle, cbhdl,
+ CBOnInit, CBOnFocus, CBOnClick, CBOnAction,
+ CBGetContextButtons, CBOnContextButton);
+ }
+ //----------------------------------------------------------------------------
+ /// @}
+
+private:
+ static bool CBOnInit(KODI_GUI_CLIENT_HANDLE cbhdl)
+ {
+ return static_cast<CWindow*>(cbhdl)->OnInit();
+ }
+
+ static bool CBOnFocus(KODI_GUI_CLIENT_HANDLE cbhdl, int controlId)
+ {
+ return static_cast<CWindow*>(cbhdl)->OnFocus(controlId);
+ }
+
+ static bool CBOnClick(KODI_GUI_CLIENT_HANDLE cbhdl, int controlId)
+ {
+ return static_cast<CWindow*>(cbhdl)->OnClick(controlId);
+ }
+
+ static bool CBOnAction(KODI_GUI_CLIENT_HANDLE cbhdl, ADDON_ACTION actionId)
+ {
+ return static_cast<CWindow*>(cbhdl)->OnAction(actionId);
+ }
+
+ static void CBGetContextButtons(KODI_GUI_CLIENT_HANDLE cbhdl,
+ int itemNumber,
+ gui_context_menu_pair* buttons,
+ unsigned int* size)
+ {
+ std::vector<std::pair<unsigned int, std::string>> buttonList;
+ static_cast<CWindow*>(cbhdl)->GetContextButtons(itemNumber, buttonList);
+ if (!buttonList.empty())
+ {
+ unsigned int presentSize = static_cast<unsigned int>(buttonList.size());
+ if (presentSize > *size)
+ kodi::Log(ADDON_LOG_WARNING, "GetContextButtons: More as allowed '%i' entries present!",
+ *size);
+ else
+ *size = presentSize;
+ for (unsigned int i = 0; i < *size; ++i)
+ {
+ buttons[i].id = buttonList[i].first;
+ strncpy(buttons[i].name, buttonList[i].second.c_str(), ADDON_MAX_CONTEXT_ENTRY_NAME_LENGTH);
+ }
+ }
+ }
+
+ static bool CBOnContextButton(KODI_GUI_CLIENT_HANDLE cbhdl, int itemNumber, unsigned int button)
+ {
+ return static_cast<CWindow*>(cbhdl)->OnContextButton(itemNumber, button);
+ }
+};
+
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Button.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Button.h
new file mode 100644
index 0000000..a34d44b
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Button.h
@@ -0,0 +1,166 @@
+/*
+ * 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 "../../c-api/gui/controls/button.h"
+#include "../Window.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CButton Control Button
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CButton }
+/// **Standard push button control for window**\n
+/// The button control is used for creating push buttons in Kodi.
+///
+/// You can choose the position, size, and look of the button, as well as
+/// choosing what action(s) should be performed when pushed.
+///
+/// It has the header @ref Button.h "#include <kodi/gui/controls/Button.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin part for a @ref skin_Button_control "button control"
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+class ATTR_DLL_LOCAL CButton : public CAddonGUIControlBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CButton
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window Related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CButton(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_button(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL, "kodi::gui::CButton can't create control class from Kodi !!!");
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CButton
+ /// @brief Destructor.
+ ///
+ ~CButton() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CButton
+ /// @brief Set the control on window to visible.
+ ///
+ /// @param[in] visible If true visible, otherwise hidden
+ ///
+ void SetVisible(bool visible)
+ {
+ m_interface->kodi_gui->control_button->set_visible(m_interface->kodiBase, m_controlHandle,
+ visible);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CButton
+ /// @brief Set's the control's enabled/disabled state.
+ ///
+ /// @param[in] enabled If true enabled, otherwise disabled
+ ///
+ void SetEnabled(bool enabled)
+ {
+ m_interface->kodi_gui->control_button->set_enabled(m_interface->kodiBase, m_controlHandle,
+ enabled);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CButton
+ /// @brief To set the text string on button.
+ ///
+ /// @param[in] label Text to show
+ ///
+ void SetLabel(const std::string& label)
+ {
+ m_interface->kodi_gui->control_button->set_label(m_interface->kodiBase, m_controlHandle,
+ label.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CButton
+ /// @brief Get the used text from button.
+ ///
+ /// @return Text shown
+ ///
+ std::string GetLabel() const
+ {
+ std::string label;
+ char* ret =
+ m_interface->kodi_gui->control_button->get_label(m_interface->kodiBase, m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ label = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return label;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CButton
+ /// @brief If two labels are used for button becomes it set with them.
+ ///
+ /// @param[in] label Text for second label
+ ///
+ void SetLabel2(const std::string& label)
+ {
+ m_interface->kodi_gui->control_button->set_label2(m_interface->kodiBase, m_controlHandle,
+ label.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CButton
+ /// @brief Get the second label if present.
+ ///
+ /// @return Second label
+ ///
+ std::string GetLabel2() const
+ {
+ std::string label;
+ char* ret =
+ m_interface->kodi_gui->control_button->get_label2(m_interface->kodiBase, m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ label = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return label;
+ }
+ //----------------------------------------------------------------------------
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/CMakeLists.txt
new file mode 100644
index 0000000..bc21108
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/CMakeLists.txt
@@ -0,0 +1,21 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ Button.h
+ Edit.h
+ FadeLabel.h
+ Image.h
+ Label.h
+ Progress.h
+ RadioButton.h
+ Rendering.h
+ SettingsSlider.h
+ Slider.h
+ Spin.h
+ TextBox.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_gui_controls)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Edit.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Edit.h
new file mode 100644
index 0000000..334dbc3
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Edit.h
@@ -0,0 +1,217 @@
+/*
+ * 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 "../../c-api/gui/controls/edit.h"
+#include "../Window.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CEdit Control Edit
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CEdit }
+/// **Editable window text control used as an input control for the osd keyboard
+/// and other input fields**\n
+/// The edit control allows a user to input text in Kodi.
+///
+/// You can choose the font, size, colour, location and header of the text to be
+/// displayed.
+///
+/// It has the header @ref Edit.h "#include <kodi/gui/controls/Edit.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin partfor a @ref skin_Edit_control "edit control".
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+
+//==============================================================================
+// see gui/definition.h for use of group "cpp_kodi_gui_windows_controls_CEdit_Defs"
+///
+/// @defgroup cpp_kodi_gui_windows_controls_CEdit_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_gui_windows_controls_CEdit
+/// @brief **Library definition values**
+///
+
+class ATTR_DLL_LOCAL CEdit : public CAddonGUIControlBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window Related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CEdit(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_edit(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::control::CEdit can't create control class from Kodi !!!");
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit
+ /// @brief Destructor.
+ ///
+ ~CEdit() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit
+ /// @brief Set the control on window to visible.
+ ///
+ /// @param[in] visible If true visible, otherwise hidden
+ ///
+ void SetVisible(bool visible)
+ {
+ m_interface->kodi_gui->control_edit->set_visible(m_interface->kodiBase, m_controlHandle,
+ visible);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit
+ /// @brief Set's the control's enabled/disabled state.
+ ///
+ /// @param[in] enabled If true enabled, otherwise disabled
+ ///
+ void SetEnabled(bool enabled)
+ {
+ m_interface->kodi_gui->control_edit->set_enabled(m_interface->kodiBase, m_controlHandle,
+ enabled);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit
+ /// @brief To set the text string on edit control.
+ ///
+ /// @param[in] label Text to show
+ ///
+ void SetLabel(const std::string& label)
+ {
+ m_interface->kodi_gui->control_edit->set_label(m_interface->kodiBase, m_controlHandle,
+ label.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit
+ /// @brief Returns the text heading for this edit control.
+ ///
+ /// @return Heading text
+ ///
+ std::string GetLabel() const
+ {
+ std::string label;
+ char* ret =
+ m_interface->kodi_gui->control_edit->get_label(m_interface->kodiBase, m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ label = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return label;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit
+ /// @brief Set's text heading for this edit control.
+ ///
+ /// @param[in] text string or unicode - text string.
+ ///
+ void SetText(const std::string& text)
+ {
+ m_interface->kodi_gui->control_edit->set_text(m_interface->kodiBase, m_controlHandle,
+ text.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit
+ /// @brief Returns the text value for this edit control.
+ ///
+ /// @return Text value of control
+ ///
+ std::string GetText() const
+ {
+ std::string text;
+ char* ret =
+ m_interface->kodi_gui->control_edit->get_text(m_interface->kodiBase, m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ text = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return text;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit
+ /// @brief Set the cursor position on text.
+ ///
+ /// @param[in] position The position to set
+ ///
+ void SetCursorPosition(unsigned int position)
+ {
+ m_interface->kodi_gui->control_edit->set_cursor_position(m_interface->kodiBase, m_controlHandle,
+ position);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit
+ /// @brief To get current cursor position on text field.
+ ///
+ /// @return The current cursor position
+ ///
+ unsigned int GetCursorPosition()
+ {
+ return m_interface->kodi_gui->control_edit->get_cursor_position(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CEdit
+ /// @brief To set field input type which are defined on @ref AddonGUIInputType.
+ ///
+ /// @param[in] type The @ref AddonGUIInputType "Add-on input type" to use
+ /// @param[in] heading The heading text for related keyboard dialog
+ ///
+ void SetInputType(AddonGUIInputType type, const std::string& heading)
+ {
+ m_interface->kodi_gui->control_edit->set_input_type(m_interface->kodiBase, m_controlHandle,
+ static_cast<int>(type), heading.c_str());
+ }
+ //----------------------------------------------------------------------------
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/FadeLabel.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/FadeLabel.h
new file mode 100644
index 0000000..2a888a6
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/FadeLabel.h
@@ -0,0 +1,148 @@
+/*
+ * 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 "../../c-api/gui/controls/fade_label.h"
+#include "../Window.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CFadeLabel Control Fade Label
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CFadeLabel }
+/// **Window control used to show multiple pieces of text in the same position,
+/// by fading from one to the other**\n
+/// The fade label control is used for displaying multiple pieces of text in
+/// the same space in Kodi.
+///
+/// You can choose the font, size, colour, location and contents of the text to
+/// be displayed. The first piece of information to display fades in over 50
+/// frames, then scrolls off to the left. Once it is finished scrolling off
+/// screen, the second piece of information fades in and the process repeats.
+/// A fade label control is not supported in a list container.
+///
+/// It has the header @ref FadeLabel.h "#include <kodi/gui/controls/FadeLabel.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin part for a @ref Fade_Label_Control "fade label control".
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+class ATTR_DLL_LOCAL CFadeLabel : public CAddonGUIControlBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CFadeLabel
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window Related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CFadeLabel(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_fade_label(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::controls::CFadeLabel can't create control class from Kodi !!!");
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CFadeLabel
+ /// @brief Destructor.
+ ///
+ ~CFadeLabel() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CFadeLabel
+ /// @brief Set the control on window to visible.
+ ///
+ /// @param[in] visible If true visible, otherwise hidden
+ ///
+ void SetVisible(bool visible)
+ {
+ m_interface->kodi_gui->control_fade_label->set_visible(m_interface->kodiBase, m_controlHandle,
+ visible);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CFadeLabel
+ /// @brief To add additional text string on fade label.
+ ///
+ /// @param[in] label Text to show
+ ///
+ void AddLabel(const std::string& label)
+ {
+ m_interface->kodi_gui->control_fade_label->add_label(m_interface->kodiBase, m_controlHandle,
+ label.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CFadeLabel
+ /// @brief Get the used text from button.
+ ///
+ /// @return Text shown
+ ///
+ std::string GetLabel() const
+ {
+ std::string label;
+ char* ret = m_interface->kodi_gui->control_fade_label->get_label(m_interface->kodiBase,
+ m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ label = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return label;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CFadeLabel
+ /// @brief To enable or disable scrolling on fade label.
+ ///
+ /// @param[in] scroll To enable scrolling set to true, otherwise is disabled
+ ///
+ void SetScrolling(bool scroll)
+ {
+ m_interface->kodi_gui->control_fade_label->set_scrolling(m_interface->kodiBase, m_controlHandle,
+ scroll);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CFadeLabel
+ /// @brief To reset al inserted labels.
+ ///
+ void Reset()
+ {
+ m_interface->kodi_gui->control_fade_label->reset(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Image.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Image.h
new file mode 100644
index 0000000..842d32a
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Image.h
@@ -0,0 +1,112 @@
+/*
+ * 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 "../../c-api/gui/controls/image.h"
+#include "../Window.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CImage Control Image
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CImage }
+/// **Window control used to show an image.**\n
+/// The image control is used for displaying images in Kodi. You can choose
+/// the position, size, transparency and contents of the image to be displayed.
+///
+/// It has the header @ref Image.h "#include <kodi/gui/controls/Image.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin part for a @ref Image_Control "image control".
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+class ATTR_DLL_LOCAL CImage : public CAddonGUIControlBase
+{
+public:
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CImage
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window Related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CImage(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_image(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::controls::CImage can't create control class from Kodi !!!");
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CImage
+ /// @brief Destructor.
+ ///
+ ~CImage() override = default;
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CImage
+ /// @brief Set the control on window to visible.
+ ///
+ /// @param[in] visible If true visible, otherwise hidden
+ ///
+ void SetVisible(bool visible)
+ {
+ m_interface->kodi_gui->control_image->set_visible(m_interface->kodiBase, m_controlHandle,
+ visible);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CImage
+ /// @brief To set the filename used on image control.
+ ///
+ /// @param[in] filename Image file to use
+ /// @param[in] useCache To define storage of image, default is in cache, if
+ /// false becomes it loaded always on changes again
+ ///
+ void SetFileName(const std::string& filename, bool useCache = true)
+ {
+ m_interface->kodi_gui->control_image->set_filename(m_interface->kodiBase, m_controlHandle,
+ filename.c_str(), useCache);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CImage
+ /// @brief To set set the diffuse color on image.
+ ///
+ /// @param[in] colorDiffuse Color to use for diffuse
+ ///
+ void SetColorDiffuse(uint32_t colorDiffuse)
+ {
+ m_interface->kodi_gui->control_image->set_color_diffuse(m_interface->kodiBase, m_controlHandle,
+ colorDiffuse);
+ }
+ //--------------------------------------------------------------------------
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Label.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Label.h
new file mode 100644
index 0000000..fbc35c3
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Label.h
@@ -0,0 +1,118 @@
+/*
+ * 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 "../../c-api/gui/controls/label.h"
+#include "../Window.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CLabel Control Label
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CLabel }
+/// **Window control used to show some lines of text**\n
+/// The label control is used for displaying text in Kodi. You can choose
+/// the font, size, colour, location and contents of the text to be displayed.
+///
+/// It has the header @ref Label.h "#include <kodi/gui/controls/Label.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin part for a @ref Label_Control "label control".
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+class ATTR_DLL_LOCAL CLabel : public CAddonGUIControlBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CLabel
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window Related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CLabel(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_label(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::controls::CLabel can't create control class from Kodi !!!");
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CLabel
+ /// @brief Destructor.
+ ///
+ ~CLabel() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CLabel
+ /// @brief Set the control on window to visible.
+ ///
+ /// @param[in] visible If true visible, otherwise hidden
+ ///
+ void SetVisible(bool visible)
+ {
+ m_interface->kodi_gui->control_label->set_visible(m_interface->kodiBase, m_controlHandle,
+ visible);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CLabel
+ /// @brief To set the text string on label.
+ ///
+ /// @param[in] text Text to show
+ ///
+ void SetLabel(const std::string& text)
+ {
+ m_interface->kodi_gui->control_label->set_label(m_interface->kodiBase, m_controlHandle,
+ text.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CLabel
+ /// @brief Get the used text from control.
+ ///
+ /// @return Used text on label control
+ ///
+ std::string GetLabel() const
+ {
+ std::string label;
+ char* ret =
+ m_interface->kodi_gui->control_label->get_label(m_interface->kodiBase, m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ label = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return label;
+ }
+ //----------------------------------------------------------------------------
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Progress.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Progress.h
new file mode 100644
index 0000000..8393b1d
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Progress.h
@@ -0,0 +1,112 @@
+/*
+ * 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 "../../c-api/gui/controls/progress.h"
+#include "../Window.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CProgress Control Progress
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CProgress }
+/// **Window control to show the progress of a particular operation**\n
+/// The progress control is used to show the progress of an item that may take
+/// a long time, or to show how far through a movie you are.
+///
+/// You can choose the position, size, and look of the progress control.
+///
+/// It has the header @ref Progress.h "#include <kodi/gui/controls/Progress.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin part for a @ref Progress_Control "progress control".
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+class ATTR_DLL_LOCAL CProgress : public CAddonGUIControlBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CProgress
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window Related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CProgress(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_progress(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::controls::CProgress can't create control class from Kodi !!!");
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CProgress
+ /// @brief Destructor.
+ ///
+ ~CProgress() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CProgress
+ /// @brief Set the control on window to visible.
+ ///
+ /// @param[in] visible If true visible, otherwise hidden
+ ///
+ void SetVisible(bool visible)
+ {
+ m_interface->kodi_gui->control_progress->set_visible(m_interface->kodiBase, m_controlHandle,
+ visible);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CProgress
+ /// @brief To set Percent position of control.
+ ///
+ /// @param[in] percent The percent position to use
+ ///
+ void SetPercentage(float percent)
+ {
+ m_interface->kodi_gui->control_progress->set_percentage(m_interface->kodiBase, m_controlHandle,
+ percent);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CProgress
+ /// @brief Get the active percent position of progress bar.
+ ///
+ /// @return Progress position as percent
+ ///
+ float GetPercentage() const
+ {
+ return m_interface->kodi_gui->control_progress->get_percentage(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/RadioButton.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/RadioButton.h
new file mode 100644
index 0000000..2d70fdc
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/RadioButton.h
@@ -0,0 +1,214 @@
+/*
+ * 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 "../../c-api/gui/controls/radio_button.h"
+#include "../Window.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CRadioButton Control Radio Button
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CRadioButton }
+/// **Window control for a radio button (as used for on/off settings)**\n
+/// The radio button control is used for creating push button on/off settings
+/// in Kodi.
+///
+/// You can choose the position, size, and look of the button. When the user
+/// clicks on the radio button, the state will change, toggling the extra
+/// textures (textureradioon and textureradiooff). Used for settings
+/// controls.
+///
+/// It has the header @ref RadioButton.h "#include <kodi/gui/controls/RadioButton.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin part for a @ref Radio_button_control "radio button control".
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+///
+/// --------------------------------------------------------------------------
+/// **Example:**
+/// ~~~~~~~~~~~~cpp
+/// #include <kodi/gui/Window.h>
+///
+/// #define MY_RADIO_BUTTON_CONTROL 1
+///
+/// class CMyWindow : public kodi::gui::CWindow
+/// {
+/// public:
+/// CMyWindow()
+///
+/// void ShowWindow();
+///
+/// bool OnInit() override;
+/// bool OnClick(int controlId) override;
+///
+/// private:
+/// kodi::gui::controls::CSpin m_myRadioButtonControl;
+/// };
+///
+/// CMyWindow::CMyWindow()
+/// : kodi::gui::CWindow("my_skin.xml", "skin.estuary", true, false),
+/// m_myRadioButtonControl(this, MY_RADIO_BUTTON_CONTROL)
+/// {
+/// }
+///
+/// void CMyWindow::ShowWindow()
+/// {
+/// kodi::gui::CWindow::DoModal();
+/// }
+///
+/// bool CMyWindow::OnInit()
+/// {
+/// m_myRadioButtonControl.SetSelected(false); // can also on skin set to default
+/// return true;
+/// }
+///
+/// bool CMyWindow::OnClick(int controlId)
+/// {
+/// if (controlId == MY_RADIO_BUTTON_CONTROL)
+/// {
+/// bool selected = m_myRadioButtonControl.IsSelected();
+/// ...
+/// }
+/// return true;
+/// }
+/// return false;
+/// }
+/// ~~~~~~~~~~~~
+///
+class ATTR_DLL_LOCAL CRadioButton : public CAddonGUIControlBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRadioButton
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window Related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CRadioButton(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_radio_button(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::controls::CRadioButton can't create control class from Kodi !!!");
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRadioButton
+ /// @brief Destructor.
+ ///
+ ~CRadioButton() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRadioButton.
+ /// @brief Set the control on window to visible.
+ ///
+ /// @param[in] visible If true visible, otherwise hidden
+ ///
+ void SetVisible(bool visible)
+ {
+ m_interface->kodi_gui->control_radio_button->set_visible(m_interface->kodiBase, m_controlHandle,
+ visible);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRadioButton
+ /// @brief Set's the control's enabled/disabled state.
+ ///
+ /// @param[in] enabled If true enabled, otherwise disabled
+ ///
+ void SetEnabled(bool enabled)
+ {
+ m_interface->kodi_gui->control_radio_button->set_enabled(m_interface->kodiBase, m_controlHandle,
+ enabled);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRadioButton
+ /// @brief To set the text string on radio button.
+ ///
+ /// @param[in] label Text to show
+ ///
+ void SetLabel(const std::string& label)
+ {
+ m_interface->kodi_gui->control_radio_button->set_label(m_interface->kodiBase, m_controlHandle,
+ label.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRadioButton
+ /// @brief Get the used text from control.
+ ///
+ /// @return Text shown
+ ///
+ std::string GetLabel() const
+ {
+ std::string label;
+ char* ret = m_interface->kodi_gui->control_radio_button->get_label(m_interface->kodiBase,
+ m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ label = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return label;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRadioButton
+ /// @brief To set radio button condition to on or off.
+ ///
+ /// @param[in] selected true set radio button to selection on, otherwise off
+ ///
+ void SetSelected(bool selected)
+ {
+ m_interface->kodi_gui->control_radio_button->set_selected(m_interface->kodiBase,
+ m_controlHandle, selected);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRadioButton
+ /// @brief Get the current selected condition of radio button.
+ ///
+ /// @return Selected condition
+ ///
+ bool IsSelected() const
+ {
+ return m_interface->kodi_gui->control_radio_button->is_selected(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Rendering.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Rendering.h
new file mode 100644
index 0000000..afaf76b
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Rendering.h
@@ -0,0 +1,217 @@
+/*
+ * 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 "../../c-api/gui/controls/rendering.h"
+#include "../Window.h"
+#include "../renderHelper.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CRendering Control Rendering
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CRendering }
+/// **Window control for rendering own parts**\n
+/// This rendering control is used when own parts are needed.
+///
+/// You have the control over them to render direct OpenGL or DirectX content
+/// to the screen set by the size of them.
+///
+/// Alternative can be the virtual functions from t his been ignored if the
+/// callbacks are defined by the @ref CRendering_SetIndependentCallbacks
+/// function and class is used as single and not as a parent class.
+///
+/// It has the header @ref Rendering.h "#include <kodi/gui/controls/Rendering.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin part for a @ref Addon_Rendering_control "rendering control".
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+class ATTR_DLL_LOCAL CRendering : public CAddonGUIControlBase
+{
+public:
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRendering
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window Related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CRendering(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_render_addon(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (m_controlHandle)
+ m_interface->kodi_gui->control_rendering->set_callbacks(m_interface->kodiBase,
+ m_controlHandle, this, OnCreateCB,
+ OnRenderCB, OnStopCB, OnDirtyCB);
+ else
+ kodi::Log(ADDON_LOG_FATAL, "kodi::gui::controls::%s can't create control class from Kodi !!!",
+ __FUNCTION__);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRendering
+ /// @brief Destructor.
+ ///
+ ~CRendering() override
+ {
+ m_interface->kodi_gui->control_rendering->destroy(m_interface->kodiBase, m_controlHandle);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRendering
+ /// @brief To create rendering control on Add-on.
+ ///
+ /// Function creates the needed rendering control for Kodi which becomes
+ /// handled and processed from Add-on
+ ///
+ /// @note This is callback function from Kodi to Add-on and not to use
+ /// for calls from add-on to this function.
+ ///
+ /// @param[in] x Horizontal position
+ /// @param[in] y Vertical position
+ /// @param[in] w Width of control
+ /// @param[in] h Height of control
+ /// @param[in] device The device to use. For OpenGL is empty on Direct X is
+ /// the needed device send.
+ /// @return Add-on needs to return true if successed, otherwise false.
+ ///
+ /// @note The @ref kodi::HardwareContext is basically a simple pointer which
+ /// has to be changed to the desired format at the corresponding places using
+ /// <b>`static_cast<...>(...)`</b>.
+ ///
+ virtual bool Create(int x, int y, int w, int h, kodi::HardwareContext device) { return false; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRendering
+ /// @brief Render process call from Kodi.
+ ///
+ /// @note This is callback function from Kodi to Add-on and not to use for
+ /// calls from add-on to this function.
+ ///
+ virtual void Render() {}
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRendering
+ /// @brief Call from Kodi to stop rendering process.
+ ///
+ /// @note This is callback function from Kodi to Add-on and not to use
+ /// for calls from add-on to this function.
+ ///
+ virtual void Stop() {}
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRendering
+ /// @brief Call from Kodi where add-on becomes asked about dirty rendering
+ /// region.
+ ///
+ /// @note This is callback function from Kodi to Add-on and not to use
+ /// for calls from add-on to this function.
+ ///
+ /// @return True if a render region is dirty and need rendering.
+ ///
+ virtual bool Dirty() { return false; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CRendering
+ /// @anchor CRendering_SetIndependentCallbacks
+ /// @brief If the class is used independent (with "new CRendering")
+ /// and not as parent (with "cCLASS_own : CRendering") from own must
+ /// be the callback from Kodi to add-on overdriven with own functions!
+ ///
+ /// @param[in] cbhdl Addon related class point where becomes given as value on
+ /// related functions.
+ /// @param[in] CBCreate External creation function pointer, see also @ref Create
+ /// about related values
+ /// @param[in] CBRender External render function pointer, see also @ref Render
+ /// about related values
+ /// @param[in] CBStop External stop function pointer, see also @ref Stop
+ /// about related values
+ /// @param[in] CBDirty External dirty function pointer, see also @ref Dirty
+ /// about related values
+ ///
+ void SetIndependentCallbacks(kodi::gui::ClientHandle cbhdl,
+ bool (*CBCreate)(kodi::gui::ClientHandle cbhdl,
+ int x,
+ int y,
+ int w,
+ int h,
+ kodi::HardwareContext device),
+ void (*CBRender)(kodi::gui::ClientHandle cbhdl),
+ void (*CBStop)(kodi::gui::ClientHandle cbhdl),
+ bool (*CBDirty)(kodi::gui::ClientHandle cbhdl))
+ {
+ if (!cbhdl || !CBCreate || !CBRender || !CBStop || !CBDirty)
+ {
+ kodi::Log(ADDON_LOG_ERROR, "kodi::gui::controls::%s called with nullptr !!!", __FUNCTION__);
+ return;
+ }
+
+ m_interface->kodi_gui->control_rendering->set_callbacks(
+ m_interface->kodiBase, m_controlHandle, cbhdl, CBCreate, CBRender, CBStop, CBDirty);
+ }
+ //--------------------------------------------------------------------------
+
+private:
+ /*
+ * Defined callback functions from Kodi to add-on, for use in parent / child system
+ * (is private)!
+ */
+ static bool OnCreateCB(
+ KODI_GUI_CLIENT_HANDLE cbhdl, int x, int y, int w, int h, ADDON_HARDWARE_CONTEXT device)
+ {
+ static_cast<CRendering*>(cbhdl)->m_renderHelper = kodi::gui::GetRenderHelper();
+ return static_cast<CRendering*>(cbhdl)->Create(x, y, w, h, device);
+ }
+
+ static void OnRenderCB(KODI_GUI_CLIENT_HANDLE cbhdl)
+ {
+ if (!static_cast<CRendering*>(cbhdl)->m_renderHelper)
+ return;
+ static_cast<CRendering*>(cbhdl)->m_renderHelper->Begin();
+ static_cast<CRendering*>(cbhdl)->Render();
+ static_cast<CRendering*>(cbhdl)->m_renderHelper->End();
+ }
+
+ static void OnStopCB(KODI_GUI_CLIENT_HANDLE cbhdl)
+ {
+ static_cast<CRendering*>(cbhdl)->Stop();
+ static_cast<CRendering*>(cbhdl)->m_renderHelper = nullptr;
+ }
+
+ static bool OnDirtyCB(KODI_GUI_CLIENT_HANDLE cbhdl)
+ {
+ return static_cast<CRendering*>(cbhdl)->Dirty();
+ }
+
+ std::shared_ptr<kodi::gui::IRenderHelper> m_renderHelper;
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/SettingsSlider.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/SettingsSlider.h
new file mode 100644
index 0000000..e1de9d0
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/SettingsSlider.h
@@ -0,0 +1,314 @@
+/*
+ * 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 "../../c-api/gui/controls/settings_slider.h"
+#include "../Window.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CSettingsSlider Control Settings Slider
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CSettingsSlider }
+/// **Window control for moveable slider with text name**\n
+/// The settings slider control is used in the settings screens for when an
+/// option is best specified on a sliding scale.
+///
+/// You can choose the position, size, and look of the slider control. It is
+/// basically a cross between the button control and a slider control. It has a
+/// label and focus and non focus textures, as well as a slider control on the
+/// right.
+///
+/// It has the header @ref SettingsSlider.h "#include <kodi/gui/controls/SettingsSlider.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin part for a @ref Settings_Slider_Control "settings slider control".
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+class ATTR_DLL_LOCAL CSettingsSlider : public CAddonGUIControlBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window Related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CSettingsSlider(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_settings_slider(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::controls::CSettingsSlider can't create control class from Kodi !!!");
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief Destructor.
+ ///
+ ~CSettingsSlider() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief Set the control on window to visible.
+ ///
+ /// @param[in] visible If true visible, otherwise hidden
+ ///
+ void SetVisible(bool visible)
+ {
+ m_interface->kodi_gui->control_settings_slider->set_visible(m_interface->kodiBase,
+ m_controlHandle, visible);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief Set's the control's enabled/disabled state.
+ ///
+ /// @param[in] enabled If true enabled, otherwise disabled
+ ///
+ void SetEnabled(bool enabled)
+ {
+ m_interface->kodi_gui->control_settings_slider->set_enabled(m_interface->kodiBase,
+ m_controlHandle, enabled);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief To set the text string on settings slider.
+ ///
+ /// @param[in] text Text to show
+ ///
+ void SetText(const std::string& text)
+ {
+ m_interface->kodi_gui->control_settings_slider->set_text(m_interface->kodiBase, m_controlHandle,
+ text.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief To reset slider on defaults.
+ ///
+ void Reset()
+ {
+ m_interface->kodi_gui->control_settings_slider->reset(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief To set the the range as integer of slider, e.g. -10 is the slider
+ /// start and e.g. +10 is the from here defined position where it reach the
+ /// end.
+ ///
+ /// Ad default is the range from 0 to 100.
+ ///
+ /// The integer interval is as default 1 and can be changed with
+ /// @ref SetIntInterval.
+ ///
+ /// @param[in] start Integer start value
+ /// @param[in] end Integer end value
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ void SetIntRange(int start, int end)
+ {
+ m_interface->kodi_gui->control_settings_slider->set_int_range(m_interface->kodiBase,
+ m_controlHandle, start, end);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief Set the slider position with the given integer value. The Range
+ /// must be defined with a call from @ref SetIntRange before.
+ ///
+ /// @param[in] value Position in range to set with integer
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ void SetIntValue(int value)
+ {
+ m_interface->kodi_gui->control_settings_slider->set_int_value(m_interface->kodiBase,
+ m_controlHandle, value);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief To get the current position as integer value.
+ ///
+ /// @return The position as integer
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ int GetIntValue() const
+ {
+ return m_interface->kodi_gui->control_settings_slider->get_int_value(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief To set the interval steps of slider, as default is it 1. If it
+ /// becomes changed with this function will a step of the user with the
+ /// value fixed here be executed.
+ ///
+ /// @param[in] interval Intervall step to set.
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ void SetIntInterval(int interval)
+ {
+ m_interface->kodi_gui->control_settings_slider->set_int_interval(m_interface->kodiBase,
+ m_controlHandle, interval);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief Sets the percent of the slider.
+ ///
+ /// @param[in] percent float - Percent value of slide
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ void SetPercentage(float percent)
+ {
+ m_interface->kodi_gui->control_settings_slider->set_percentage(m_interface->kodiBase,
+ m_controlHandle, percent);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief Returns a float of the percent of the slider.
+ ///
+ /// @return float - Percent of slider
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ float GetPercentage() const
+ {
+ return m_interface->kodi_gui->control_settings_slider->get_percentage(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief To set the the range as float of slider, e.g. -25.0 is the slider
+ /// start and e.g. +25.0 is the from here defined position where it reach
+ /// the end.
+ ///
+ /// As default is the range 0.0 to 1.0.
+ ///
+ /// The float interval is as default 0.1 and can be changed with
+ /// @ref SetFloatInterval.
+ ///
+ /// @param[in] start Integer start value
+ /// @param[in] end Integer end value
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ void SetFloatRange(float start, float end)
+ {
+ m_interface->kodi_gui->control_settings_slider->set_float_range(m_interface->kodiBase,
+ m_controlHandle, start, end);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief Set the slider position with the given float value. The Range can
+ /// be defined with a call from @ref SetIntRange before, as default it
+ /// is 0.0 to 1.0.
+ ///
+ /// @param[in] value Position in range to set with float
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ void SetFloatValue(float value)
+ {
+ m_interface->kodi_gui->control_settings_slider->set_float_value(m_interface->kodiBase,
+ m_controlHandle, value);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief To get the current position as float value.
+ ///
+ /// @return The position as float
+ ///
+ float GetFloatValue() const
+ {
+ return m_interface->kodi_gui->control_settings_slider->get_float_value(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSettingsSlider
+ /// @brief To set the interval steps of slider, as default is it 0.1 If it
+ /// becomes changed with this function will a step of the user with the
+ /// value fixed here be executed.
+ ///
+ /// @param[in] interval Intervall step to set.
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ void SetFloatInterval(float interval)
+ {
+ m_interface->kodi_gui->control_settings_slider->set_float_interval(m_interface->kodiBase,
+ m_controlHandle, interval);
+ }
+ //----------------------------------------------------------------------------
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Slider.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Slider.h
new file mode 100644
index 0000000..405cc65
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Slider.h
@@ -0,0 +1,326 @@
+/*
+ * 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 "../../c-api/gui/controls/slider.h"
+#include "../Window.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CSlider Control Slider
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CSlider }
+/// **Window control for moveable slider**\n
+/// The slider control is used for things where a sliding bar best represents
+/// the operation at hand (such as a volume control or seek control).
+///
+/// You can choose the position, size, and look of the slider control.
+///
+/// It has the header @ref Slider.h "#include <kodi/gui/controls/Slider.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin part for a @ref Slider_Control "slider control".
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+class ATTR_DLL_LOCAL CSlider : public CAddonGUIControlBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window Related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CSlider(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_slider(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::controls::CSlider can't create control class from Kodi !!!");
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief Destructor.
+ ///
+ ~CSlider() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief Set the control on window to visible.
+ ///
+ /// @param[in] visible If true visible, otherwise hidden
+ ///
+ void SetVisible(bool visible)
+ {
+ m_interface->kodi_gui->control_slider->set_visible(m_interface->kodiBase, m_controlHandle,
+ visible);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief Set's the control's enabled/disabled state.
+ ///
+ /// @param[in] enabled If true enabled, otherwise disabled
+ ///
+ void SetEnabled(bool enabled)
+ {
+ m_interface->kodi_gui->control_slider->set_enabled(m_interface->kodiBase, m_controlHandle,
+ enabled);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief To reset slider on defaults.
+ ///
+ void Reset()
+ {
+ m_interface->kodi_gui->control_slider->reset(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief With GetDescription becomes a string value of position returned.
+ ///
+ /// @return Text string about current slider position
+ ///
+ /// The following are the text definition returned from this:
+ /// | Value | Without range selection | With range selection |
+ /// |:---------:|:------------------------|:-------------------------------|
+ /// | float | <c>%2.2f</c> | <c>[%2.2f, %2.2f]</c> |
+ /// | integer | <c>%i</c> | <c>[%i, %i]</c> |
+ /// | percent | <c>%i%%</c> | <c>[%i%%, %i%%]</c> |
+ ///
+ std::string GetDescription() const
+ {
+ std::string text;
+ char* ret = m_interface->kodi_gui->control_slider->get_description(m_interface->kodiBase,
+ m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ text = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return text;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief To set the the range as integer of slider, e.g. -10 is the slider
+ /// start and e.g. +10 is the from here defined position where it reach the
+ /// end.
+ ///
+ /// Ad default is the range from 0 to 100.
+ ///
+ /// The integer interval is as default 1 and can be changed with
+ /// @ref SetIntInterval.
+ ///
+ /// @param[in] start Integer start value
+ /// @param[in] end Integer end value
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only one
+ /// each can be used.
+ ///
+ void SetIntRange(int start, int end)
+ {
+ m_interface->kodi_gui->control_slider->set_int_range(m_interface->kodiBase, m_controlHandle,
+ start, end);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief Set the slider position with the given integer value. The Range
+ /// must be defined with a call from @ref SetIntRange before.
+ ///
+ /// @param[in] value Position in range to set with integer
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only one
+ /// each can be used.
+ ///
+ void SetIntValue(int value)
+ {
+ m_interface->kodi_gui->control_slider->set_int_value(m_interface->kodiBase, m_controlHandle,
+ value);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief To get the current position as integer value.
+ ///
+ /// @return The position as integer
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ int GetIntValue() const
+ {
+ return m_interface->kodi_gui->control_slider->get_int_value(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief To set the interval steps of slider, as default is it 1. If it
+ /// becomes changed with this function will a step of the user with the
+ /// value fixed here be executed.
+ ///
+ /// @param[in] interval Intervall step to set.
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only one
+ /// each can be used.
+ ///
+ void SetIntInterval(int interval)
+ {
+ m_interface->kodi_gui->control_slider->set_int_interval(m_interface->kodiBase, m_controlHandle,
+ interval);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief Sets the percent of the slider.
+ ///
+ /// @param[in] percent float - Percent value of slide
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only one
+ /// each can be used.
+ ///
+ void SetPercentage(float percent)
+ {
+ m_interface->kodi_gui->control_slider->set_percentage(m_interface->kodiBase, m_controlHandle,
+ percent);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief Returns a float of the percent of the slider.
+ ///
+ /// @return float - Percent of slider
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only one
+ /// each can be used.
+ ///
+ float GetPercentage() const
+ {
+ return m_interface->kodi_gui->control_slider->get_percentage(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief To set the the range as float of slider, e.g. -25.0 is the slider
+ /// start and e.g. +25.0 is the from here defined position where it reach
+ /// the end.
+ ///
+ /// As default is the range 0.0 to 1.0.
+ ///
+ /// The float interval is as default 0.1 and can be changed with
+ /// @ref SetFloatInterval.
+ ///
+ /// @param[in] start Integer start value
+ /// @param[in] end Integer end value
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ void SetFloatRange(float start, float end)
+ {
+ m_interface->kodi_gui->control_slider->set_float_range(m_interface->kodiBase, m_controlHandle,
+ start, end);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief Set the slider position with the given float value. The Range
+ /// can be defined with a call from @ref SetIntRange before, as default it
+ /// is 0.0 to 1.0.
+ ///
+ /// @param[in] value Position in range to set with float
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only one
+ /// each can be used.
+ ///
+ void SetFloatValue(float value)
+ {
+ m_interface->kodi_gui->control_slider->set_float_value(m_interface->kodiBase, m_controlHandle,
+ value);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief To get the current position as float value.
+ ///
+ /// @return The position as float
+ ///
+ float GetFloatValue() const
+ {
+ return m_interface->kodi_gui->control_slider->get_float_value(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSlider
+ /// @brief To set the interval steps of slider, as default is it 0.1 If it
+ /// becomes changed with this function will a step of the user with the
+ /// value fixed here be executed.
+ ///
+ /// @param[in] interval Intervall step to set.
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used.
+ ///
+ void SetFloatInterval(float interval)
+ {
+ m_interface->kodi_gui->control_slider->set_float_interval(m_interface->kodiBase,
+ m_controlHandle, interval);
+ }
+ //----------------------------------------------------------------------------
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Spin.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Spin.h
new file mode 100644
index 0000000..d0a7306
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/Spin.h
@@ -0,0 +1,416 @@
+/*
+ * 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 "../../c-api/gui/controls/spin.h"
+#include "../Window.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CSpin Control Spin
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CSpin }
+/// **Window control used for cycling up/down controls**\n
+/// The settings spin control is used in the settings screens for when a list
+/// of options can be chosen from using up/down arrows.
+///
+/// You can choose the position, size, and look of the spin control. It is
+/// basically a cross between the button control and a spin control. It has a
+/// label and focus and non focus textures, as well as a spin control on the
+/// right.
+///
+/// It has the header @ref Spin.h "#include <kodi/gui/controls/Spin.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin part for a @ref Spin_Control "spin control".
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+/// --------------------------------------------------------------------------
+/// **Example:**
+/// ~~~~~~~~~~~~cpp
+/// #include <kodi/gui/Window.h>
+///
+/// #define MY_SPIN_CONTROL 1
+///
+/// class CMyWindow : public kodi::gui::CWindow
+/// {
+/// public:
+/// CMyWindow()
+///
+/// void ShowWindow();
+///
+/// bool OnInit() override;
+/// bool OnClick(int controlId) override;
+///
+/// private:
+/// kodi::gui::controls::CSpin m_mySpinControl;
+/// };
+///
+/// CMyWindow::CMyWindow()
+/// : kodi::gui::CWindow("my_skin.xml", "skin.estuary", true, false),
+/// m_mySpinControl(this, MY_SPIN_CONTROL)
+/// {
+/// }
+///
+/// void CMyWindow::ShowWindow()
+/// {
+/// kodi::gui::CWindow::DoModal();
+/// }
+///
+/// bool CMyWindow::OnInit()
+/// {
+/// m_mySpinControl.SetType(kodi::gui::controls::ADDON_SPIN_CONTROL_TYPE_INT);
+/// m_mySpinControl.SetIntRange(1, 80);
+/// return true;
+/// }
+///
+/// bool CMyWindow::OnClick(int controlId)
+/// {
+/// if (controlId == MY_SPIN_CONTROL)
+/// {
+/// int value = m_mySpinControl.GetIntValue();
+/// ...
+/// }
+/// return true;
+/// }
+/// return false;
+/// }
+/// ~~~~~~~~~~~~
+///
+
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_windows_controls_CSpin
+/// @anchor AddonGUISpinControlType
+/// @brief The values here defines the used value format for steps on
+/// spin control.
+///
+typedef enum AddonGUISpinControlType
+{
+ /// One spin step interpreted as integer
+ ADDON_SPIN_CONTROL_TYPE_INT = 1,
+ /// One spin step interpreted as floating point value
+ ADDON_SPIN_CONTROL_TYPE_FLOAT = 2,
+ /// One spin step interpreted as text string
+ ADDON_SPIN_CONTROL_TYPE_TEXT = 3,
+ /// One spin step interpreted as a page change value
+ ADDON_SPIN_CONTROL_TYPE_PAGE = 4
+} AddonGUISpinControlType;
+//------------------------------------------------------------------------------
+
+class ATTR_DLL_LOCAL CSpin : public CAddonGUIControlBase
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window Related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CSpin(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_spin(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::controls::CSpin can't create control class from Kodi !!!");
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief Destructor.
+ ///
+ ~CSpin() override = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief Set the control on window to visible.
+ ///
+ /// @param[in] visible If true visible, otherwise hidden
+ ///
+ void SetVisible(bool visible)
+ {
+ m_interface->kodi_gui->control_spin->set_visible(m_interface->kodiBase, m_controlHandle,
+ visible);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief Set's the control's enabled/disabled state.
+ ///
+ /// @param[in] enabled If true enabled, otherwise disabled
+ ///
+ void SetEnabled(bool enabled)
+ {
+ m_interface->kodi_gui->control_spin->set_enabled(m_interface->kodiBase, m_controlHandle,
+ enabled);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To set the text string on spin control.
+ ///
+ /// @param[in] text Text to show as name for spin
+ ///
+ void SetText(const std::string& text)
+ {
+ m_interface->kodi_gui->control_spin->set_text(m_interface->kodiBase, m_controlHandle,
+ text.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To reset spin control to defaults.
+ ///
+ void Reset()
+ {
+ m_interface->kodi_gui->control_spin->reset(m_interface->kodiBase, m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To set the with SpinControlType defined types of spin.
+ ///
+ /// @param[in] type The type to use
+ ///
+ /// @note See description of @ref AddonGUISpinControlType for available types.
+ ///
+ void SetType(AddonGUISpinControlType type)
+ {
+ m_interface->kodi_gui->control_spin->set_type(m_interface->kodiBase, m_controlHandle,
+ (int)type);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To add a label entry in spin defined with a value as string.
+ ///
+ /// Format must be set to @ref ADDON_SPIN_CONTROL_TYPE_TEXT to use this function.
+ ///
+ /// @param[in] label Label string to view on skin
+ /// @param[in] value String value to use for selection of them
+ ///
+ void AddLabel(const std::string& label, const std::string& value)
+ {
+ m_interface->kodi_gui->control_spin->add_string_label(m_interface->kodiBase, m_controlHandle,
+ label.c_str(), value.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To add a label entry in spin defined with a value as integer.
+ ///
+ /// Format must be set to @ref ADDON_SPIN_CONTROL_TYPE_INT to use this function.
+ ///
+ /// @param[in] label Label string to view on skin
+ /// @param[in] value Integer value to use for selection of them.
+ ///
+ void AddLabel(const std::string& label, int value)
+ {
+ m_interface->kodi_gui->control_spin->add_int_label(m_interface->kodiBase, m_controlHandle,
+ label.c_str(), value);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To change the spin to position with them string as value.
+ ///
+ /// Format must be set to @ref ADDON_SPIN_CONTROL_TYPE_TEXT to use this function.
+ ///
+ /// @param[in] value String value to change to
+ ///
+ void SetStringValue(const std::string& value)
+ {
+ m_interface->kodi_gui->control_spin->set_string_value(m_interface->kodiBase, m_controlHandle,
+ value.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To get the current spin control position with text string value.
+ ///
+ /// Format must be set to @ref ADDON_SPIN_CONTROL_TYPE_TEXT to use this function.
+ ///
+ /// @return Currently selected string value
+ ///
+ std::string GetStringValue() const
+ {
+ std::string value;
+ char* ret = m_interface->kodi_gui->control_spin->get_string_value(m_interface->kodiBase,
+ m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ value = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return value;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To set the the range as integer of slider, e.g. -10 is the slider
+ /// start and e.g. +10 is the from here defined position where it reach the
+ /// end.
+ ///
+ /// Ad default is the range from 0 to 100.
+ ///
+ /// @param[in] start Integer start value
+ /// @param[in] end Integer end value
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used and must be defined with @ref SetType before.
+ ///
+ void SetIntRange(int start, int end)
+ {
+ m_interface->kodi_gui->control_spin->set_int_range(m_interface->kodiBase, m_controlHandle,
+ start, end);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief Set the slider position with the given integer value. The Range
+ /// must be defined with a call from @ref SetIntRange before.
+ ///
+ /// @param[in] value Position in range to set with integer
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used and must be defined with @ref SetType before.
+ ///
+ void SetIntValue(int value)
+ {
+ m_interface->kodi_gui->control_spin->set_int_value(m_interface->kodiBase, m_controlHandle,
+ value);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To get the current position as integer value.
+ ///
+ /// @return The position as integer
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used and must be defined with @ref SetType before.
+ ///
+ int GetIntValue() const
+ {
+ return m_interface->kodi_gui->control_spin->get_int_value(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To set the the range as float of spin, e.g. -25.0 is the spin
+ /// start and e.g. +25.0 is the from here defined position where it reach
+ /// the end.
+ ///
+ /// As default is the range 0.0 to 1.0.
+ ///
+ /// The float interval is as default 0.1 and can be changed with
+ /// @ref SetFloatInterval.
+ ///
+ /// @param[in] start Integer start value
+ /// @param[in] end Integer end value
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used and must be defined with @ref SetType before.
+ ///
+ void SetFloatRange(float start, float end)
+ {
+ m_interface->kodi_gui->control_spin->set_float_range(m_interface->kodiBase, m_controlHandle,
+ start, end);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief Set the spin position with the given float value. The Range
+ /// can be defined with a call from @ref SetIntRange before, as default it
+ /// is 0.0 to 1.0.
+ ///
+ /// @param[in] value Position in range to set with float
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used and must be defined with @ref SetType before.
+ ///
+ void SetFloatValue(float value)
+ {
+ m_interface->kodi_gui->control_spin->set_float_value(m_interface->kodiBase, m_controlHandle,
+ value);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To get the current position as float value.
+ ///
+ /// @return The position as float
+ ///
+ float GetFloatValue() const
+ {
+ return m_interface->kodi_gui->control_spin->get_float_value(m_interface->kodiBase,
+ m_controlHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CSpin
+ /// @brief To set the interval steps of spin, as default is it 0.1 If it
+ /// becomes changed with this function will a step of the user with the
+ /// value fixed here be executed.
+ ///
+ /// @param[in] interval Intervall step to set.
+ ///
+ /// @note Percent, floating point or integer are alone possible. Combining
+ /// these different values can be not together and can, therefore, only
+ /// one each can be used and must be defined with @ref SetType before.
+ ///
+ void SetFloatInterval(float interval)
+ {
+ m_interface->kodi_gui->control_spin->set_float_interval(m_interface->kodiBase, m_controlHandle,
+ interval);
+ }
+ //----------------------------------------------------------------------------
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/TextBox.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/TextBox.h
new file mode 100644
index 0000000..5ef9a15
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/controls/TextBox.h
@@ -0,0 +1,164 @@
+/*
+ * 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 "../../c-api/gui/controls/text_box.h"
+#include "../Window.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace controls
+{
+
+//============================================================================
+/// @defgroup cpp_kodi_gui_windows_controls_CTextBox Control Text Box
+/// @ingroup cpp_kodi_gui_windows_controls
+/// @brief @cpp_class{ kodi::gui::controls::CTextBox }
+/// **Used to show a multi-page piece of text**\n
+/// The text box control can be used to display descriptions, help texts or
+/// other larger texts.
+///
+/// It corresponds to the representation which is also to be seen on the
+/// @ref CDialogTextViewer.
+///
+/// It has the header @ref TextBox.h "#include <kodi/gui/controls/TextBox.h>"
+/// be included to enjoy it.
+///
+/// Here you find the needed skin part for a @ref Text_Box "textbox control".
+///
+/// @note The call of the control is only possible from the corresponding
+/// window as its class and identification number is required.
+///
+class ATTR_DLL_LOCAL CTextBox : public CAddonGUIControlBase
+{
+public:
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CTextBox
+ /// @brief Construct a new control.
+ ///
+ /// @param[in] window related window control class
+ /// @param[in] controlId Used skin xml control id
+ ///
+ CTextBox(CWindow* window, int controlId) : CAddonGUIControlBase(window)
+ {
+ m_controlHandle = m_interface->kodi_gui->window->get_control_text_box(
+ m_interface->kodiBase, m_Window->GetControlHandle(), controlId);
+ if (!m_controlHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::controls::CTextBox can't create control class from Kodi !!!");
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CTextBox
+ /// @brief Destructor.
+ ///
+ ~CTextBox() override = default;
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CTextBox
+ /// @brief Set the control on window to visible.
+ ///
+ /// @param[in] visible If true visible, otherwise hidden
+ ///
+ void SetVisible(bool visible)
+ {
+ m_interface->kodi_gui->control_text_box->set_visible(m_interface->kodiBase, m_controlHandle,
+ visible);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CTextBox
+ /// @brief To reset box an remove all the text.
+ ///
+ void Reset() { m_interface->kodi_gui->control_text_box->reset(m_controlHandle, m_controlHandle); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CTextBox
+ /// @brief To set the text on box.
+ ///
+ /// @param[in] text Text to show
+ ///
+ void SetText(const std::string& text)
+ {
+ m_interface->kodi_gui->control_text_box->set_text(m_interface->kodiBase, m_controlHandle,
+ text.c_str());
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CTextBox
+ /// @brief Get the used text from control.
+ ///
+ /// @return Text shown
+ ///
+ std::string GetText() const
+ {
+ std::string text;
+ char* ret =
+ m_interface->kodi_gui->control_text_box->get_text(m_interface->kodiBase, m_controlHandle);
+ if (ret != nullptr)
+ {
+ if (std::strlen(ret))
+ text = ret;
+ m_interface->free_string(m_interface->kodiBase, ret);
+ }
+ return text;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CTextBox
+ /// @brief To scroll text on other position.
+ ///
+ /// @param[in] position The line position to scroll to
+ ///
+ void Scroll(unsigned int position)
+ {
+ m_interface->kodi_gui->control_text_box->scroll(m_interface->kodiBase, m_controlHandle,
+ position);
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_windows_controls_CTextBox
+ /// @brief To set automatic scrolling of textbox
+ ///
+ /// Specifies the timing and conditions of any autoscrolling this textbox
+ /// should have. Times are in milliseconds. The content is delayed for the
+ /// given delay, then scrolls at a rate of one line per time interval until
+ /// the end. If the repeat tag is present, it then delays for the repeat
+ /// time, fades out over 1 second, and repeats. It does not wrap or reset
+ /// to the top at the end of the scroll.
+ ///
+ /// @param[in] delay Content delay
+ /// @param[in] time One line per time interval
+ /// @param[in] repeat Delays with given time, fades out over 1 second, and
+ /// repeats
+ ///
+ void SetAutoScrolling(int delay, int time, int repeat)
+ {
+ m_interface->kodi_gui->control_text_box->set_auto_scrolling(
+ m_interface->kodiBase, m_controlHandle, delay, time, repeat);
+ }
+ //--------------------------------------------------------------------------
+};
+
+} /* namespace controls */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..a6d32bb
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/CMakeLists.txt
@@ -0,0 +1,19 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ ContextMenu.h
+ ExtendedProgress.h
+ FileBrowser.h
+ Keyboard.h
+ Numeric.h
+ OK.h
+ Progress.h
+ Select.h
+ TextViewer.h
+ YesNo.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_gui_dialogs)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/ContextMenu.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/ContextMenu.h
new file mode 100644
index 0000000..93d75cb
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/ContextMenu.h
@@ -0,0 +1,185 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/gui/dialogs/context_menu.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace dialogs
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_dialogs_ContextMenu Dialog Context Menu
+/// @ingroup cpp_kodi_gui_dialogs
+/// @brief @cpp_namespace{ kodi::gui::dialogs::ContextMenu }
+/// **Context menu dialog**@n
+/// The function listed below permits the call of a dialogue as context menu to
+/// select of an entry as a key
+///
+/// It has the header @ref ContextMenu.h "#include <kodi/gui/dialogs/ContextMenu.h>"
+/// be included to enjoy it.
+///
+///
+namespace ContextMenu
+{
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_ContextMenu
+/// @brief Show a context menu dialog about given parts.
+///
+/// @param[in] heading Dialog heading name
+/// @param[in] entries String list about entries
+/// @return The selected entry, if return <tt>-1</tt> was nothing selected or canceled
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/ContextMenu.h>
+///
+/// const std::vector<std::string> entries
+/// {
+/// "Test 1",
+/// "Test 2",
+/// "Test 3",
+/// "Test 4",
+/// "Test 5"
+/// };
+///
+/// int selected = kodi::gui::dialogs::ContextMenu::Show("Test selection", entries);
+/// if (selected < 0)
+/// fprintf(stderr, "Item selection canceled\n");
+/// else
+/// fprintf(stderr, "Selected item is: %i\n", selected);
+/// ~~~~~~~~~~~~~
+///
+inline int ATTR_DLL_LOCAL Show(const std::string& heading, const std::vector<std::string>& entries)
+{
+ using namespace ::kodi::addon;
+ unsigned int size = static_cast<unsigned int>(entries.size());
+ const char** cEntries = static_cast<const char**>(malloc(size * sizeof(const char**)));
+ for (unsigned int i = 0; i < size; ++i)
+ {
+ cEntries[i] = entries[i].c_str();
+ }
+ int ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogContextMenu->open(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), cEntries, size);
+ free(cEntries);
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_ContextMenu
+/// @brief Show a context menu dialog about given parts.
+///
+/// @param[in] heading Dialog heading name
+/// @param[in] entries String list about entries
+/// @return The selected entry, if return <tt>-1</tt> was nothing selected or canceled
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/ContextMenu.h>
+///
+/// const std::vector<std::pair<std::string, std::string>> entries
+/// {
+/// { "ID 1", "Test 1" },
+/// { "ID 2", "Test 2" },
+/// { "ID 3", "Test 3" },
+/// { "ID 4", "Test 4" },
+/// { "ID 5", "Test 5" }
+/// };
+///
+/// int selected = kodi::gui::dialogs::ContextMenu::Show("Test selection", entries);
+/// if (selected < 0)
+/// fprintf(stderr, "Item selection canceled\n");
+/// else
+/// fprintf(stderr, "Selected item is: %i\n", selected);
+/// ~~~~~~~~~~~~~
+///
+inline int ATTR_DLL_LOCAL Show(const std::string& heading,
+ const std::vector<std::pair<std::string, std::string>>& entries)
+{
+ using namespace ::kodi::addon;
+ unsigned int size = static_cast<unsigned int>(entries.size());
+ const char** cEntries = static_cast<const char**>(malloc(size * sizeof(const char**)));
+ for (unsigned int i = 0; i < size; ++i)
+ {
+ cEntries[i] = entries[i].second.c_str();
+ }
+ int ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogContextMenu->open(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), cEntries, size);
+ free(cEntries);
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_ContextMenu
+/// @brief Show a context menu dialog about given parts.
+///
+/// @param[in] heading Dialog heading name
+/// @param[in] entries String list about entries
+/// @return The selected entry, if return <tt>-1</tt> was nothing selected or canceled
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/ContextMenu.h>
+///
+/// const std::vector<std::pair<int, std::string>> entries
+/// {
+/// { 1, "Test 1" },
+/// { 2, "Test 2" },
+/// { 3, "Test 3" },
+/// { 4, "Test 4" },
+/// { 5, "Test 5" }
+/// };
+///
+/// int selected = kodi::gui::dialogs::ContextMenu::Show("Test selection", entries);
+/// if (selected < 0)
+/// fprintf(stderr, "Item selection canceled\n");
+/// else
+/// fprintf(stderr, "Selected item is: %i\n", selected);
+/// ~~~~~~~~~~~~~
+///
+inline int ATTR_DLL_LOCAL Show(const std::string& heading,
+ const std::vector<std::pair<int, std::string>>& entries)
+{
+ using namespace ::kodi::addon;
+ unsigned int size = static_cast<unsigned int>(entries.size());
+ const char** cEntries = static_cast<const char**>(malloc(size * sizeof(const char**)));
+ for (unsigned int i = 0; i < size; ++i)
+ {
+ cEntries[i] = entries[i].second.c_str();
+ }
+ int ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogContextMenu->open(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), cEntries, size);
+ free(cEntries);
+ return ret;
+}
+//------------------------------------------------------------------------------
+}; // namespace ContextMenu
+
+} /* namespace dialogs */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/ExtendedProgress.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/ExtendedProgress.h
new file mode 100644
index 0000000..3c21877
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/ExtendedProgress.h
@@ -0,0 +1,243 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/gui/dialogs/extended_progress.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace dialogs
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_dialogs_CExtendedProgress Dialog Extended Progress
+/// @ingroup cpp_kodi_gui_dialogs
+/// @brief @cpp_class{ kodi::gui::dialogs::ExtendedProgress }
+/// **Progress dialog shown for background work**
+///
+/// The with @ref ExtendedProgress.h "#include <kodi/gui/dialogs/ExtendedProgress.h>"
+/// given class are basically used to create Kodi's extended progress.
+///
+///
+/// --------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/ExtendedProgress.h>
+///
+/// kodi::gui::dialogs::CExtendedProgress *ext_progress = new kodi::gui::dialogs::CExtendedProgress("Test Extended progress");
+/// ext_progress->SetText("Test progress");
+/// for (unsigned int i = 0; i < 50; i += 10)
+/// {
+/// ext_progress->SetProgress(i, 100);
+/// sleep(1);
+/// }
+///
+/// ext_progress->SetTitle("Test Extended progress - Second round");
+/// ext_progress->SetText("Test progress - Step 2");
+///
+/// for (unsigned int i = 50; i < 100; i += 10)
+/// {
+/// ext_progress->SetProgress(i, 100);
+/// sleep(1);
+/// }
+/// delete ext_progress;
+/// ~~~~~~~~~~~~~
+///
+class ATTR_DLL_LOCAL CExtendedProgress
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CExtendedProgress
+ /// Construct a new dialog.
+ ///
+ /// @param[in] title [opt] Title string
+ ///
+ explicit CExtendedProgress(const std::string& title = "")
+ {
+ using namespace ::kodi::addon;
+ m_DialogHandle =
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogExtendedProgress->new_dialog(
+ CPrivateBase::m_interface->toKodi->kodiBase, title.c_str());
+ if (!m_DialogHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::CDialogExtendedProgress can't create window class from Kodi !!!");
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CExtendedProgress
+ /// Destructor.
+ ///
+ ~CExtendedProgress()
+ {
+ using namespace ::kodi::addon;
+ if (m_DialogHandle)
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogExtendedProgress->delete_dialog(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CExtendedProgress
+ /// @brief Get the used title.
+ ///
+ /// @return Title string
+ ///
+ std::string Title() const
+ {
+ using namespace ::kodi::addon;
+ std::string text;
+ char* strMsg = CPrivateBase::m_interface->toKodi->kodi_gui->dialogExtendedProgress->get_title(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle);
+ if (strMsg != nullptr)
+ {
+ if (std::strlen(strMsg))
+ text = strMsg;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ strMsg);
+ }
+ return text;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CExtendedProgress
+ /// @brief To set the title of dialog.
+ ///
+ /// @param[in] title Title string
+ ///
+ void SetTitle(const std::string& title)
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogExtendedProgress->set_title(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle, title.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CExtendedProgress
+ /// @brief Get the used text information string.
+ ///
+ /// @return Text string
+ ///
+ std::string Text() const
+ {
+ using namespace ::kodi::addon;
+ std::string text;
+ char* strMsg = CPrivateBase::m_interface->toKodi->kodi_gui->dialogExtendedProgress->get_text(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle);
+ if (strMsg != nullptr)
+ {
+ if (std::strlen(strMsg))
+ text = strMsg;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ strMsg);
+ }
+ return text;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CExtendedProgress
+ /// @brief To set the used text information string.
+ ///
+ /// @param[in] text Information text to set
+ ///
+ void SetText(const std::string& text)
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogExtendedProgress->set_text(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle, text.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CExtendedProgress
+ /// @brief To ask dialog is finished.
+ ///
+ /// @return True if on end
+ ///
+ bool IsFinished() const
+ {
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogExtendedProgress->is_finished(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CExtendedProgress
+ /// @brief Mark progress finished.
+ ///
+ void MarkFinished()
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogExtendedProgress->mark_finished(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CExtendedProgress
+ /// @brief Get the current progress position as percent.
+ ///
+ /// @return Position
+ ///
+ float Percentage() const
+ {
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogExtendedProgress->get_percentage(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CExtendedProgress
+ /// @brief To set the current progress position as percent.
+ ///
+ /// @param[in] percentage Position to use from 0.0 to 100.0
+ ///
+ void SetPercentage(float percentage)
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogExtendedProgress->set_percentage(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle, percentage);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CExtendedProgress
+ /// @brief To set progress position with predefined places.
+ ///
+ /// @param[in] currentItem Place position to use
+ /// @param[in] itemCount Amount of used places
+ ///
+ void SetProgress(int currentItem, int itemCount)
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogExtendedProgress->set_progress(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle, currentItem, itemCount);
+ }
+ //----------------------------------------------------------------------------
+
+private:
+ KODI_GUI_HANDLE m_DialogHandle;
+};
+
+} /* namespace dialogs */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/FileBrowser.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/FileBrowser.h
new file mode 100644
index 0000000..b7550f2
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/FileBrowser.h
@@ -0,0 +1,304 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/gui/dialogs/filebrowser.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace dialogs
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_dialogs_FileBrowser Dialog File Browser
+/// @ingroup cpp_kodi_gui_dialogs
+/// @brief @cpp_namespace{ kodi::gui::dialogs::FileBrowser }
+/// **File browser dialog**\n
+/// The functions listed below of the class "FileBrowser" offer the possibility
+/// to select to a file by the user of the add-on.
+///
+/// It allows all the options that are possible in Kodi itself and offers all
+/// support file types.
+///
+/// It has the header @ref FileBrowser.h "#include <kodi/gui/dialogs/FileBrowser.h>"
+/// be included to enjoy it.
+///
+namespace FileBrowser
+{
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_FileBrowser
+/// @brief Directory selection dialog.
+///
+/// @param[in] shares With Shares becomes the available start folders be set
+/// @param[in] heading Dialog header name
+/// @param[in,out] path As in the path to start and return value about
+/// selected directory
+/// @param[in] writeOnly [opt] If set only writeable folders are shown
+/// @return False if selection becomes canceled
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/FileBrowser.h>
+///
+/// // Example show directory selection dialog with on 'share' (first value)
+/// // defined directory types.
+/// //
+/// // If this becomes leaved empty and 'directory' is empty goes it to the
+/// // base path of the hard disk.
+/// //
+/// // Also can be with path written to 'directory' before the dialog forced
+/// // to a start place.
+/// std::string directory;
+/// bool ret = kodi::gui::dialogs::FileBrowser::ShowAndGetDirectory("local|network|removable",
+/// "Test directory selection",
+/// directory,
+/// false);
+/// fprintf(stderr, "Selected directory is : %s and was %s\n", directory.c_str(), ret ? "OK" : "Canceled");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetDirectory(const std::string& shares,
+ const std::string& heading,
+ std::string& path,
+ bool writeOnly = false)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_directory(
+ CPrivateBase::m_interface->toKodi->kodiBase, shares.c_str(), heading.c_str(), path.c_str(),
+ &retString, writeOnly);
+ if (retString != nullptr)
+ {
+ if (std::strlen(retString))
+ path = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_FileBrowser
+/// @brief File selection dialog.
+///
+/// @param[in] shares With Shares becomes the available start folders be set.
+/// @param[in] mask The mask to filter visible files, e.g. ".m3u|.pls|.b4s|.wpl"
+/// @param[in] heading Dialog header name
+/// @param[in,out] path As in the path to start and Return value about selected
+/// file
+/// @param[in] useThumbs [opt] If set show thumbs if possible on dialog
+/// @param[in] useFileDirectories [opt] If set also packages (e.g. *.zip) are
+/// handled as directories.
+/// @return False if selection becomes canceled
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetFile(const std::string& shares,
+ const std::string& mask,
+ const std::string& heading,
+ std::string& path,
+ bool useThumbs = false,
+ bool useFileDirectories = false)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_file(
+ CPrivateBase::m_interface->toKodi->kodiBase, shares.c_str(), mask.c_str(), heading.c_str(),
+ path.c_str(), &retString, useThumbs, useFileDirectories);
+ if (retString != nullptr)
+ {
+ if (std::strlen(retString))
+ path = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_FileBrowser
+/// @brief File selection from a directory.
+///
+/// @param[in] directory The directory name where the dialog start, possible are
+/// normal names and kodi's special names
+/// @param[in] mask The mask to filter visible files, e.g. ".m3u|.pls|.b4s|.wpl"
+/// @param[in] heading Dialog header name
+/// @param[in,out] path As in the path to start and Return value about selected
+/// file
+/// @param[in] useThumbs [opt] If set show thumbs if possible on dialog
+/// @param[in] useFileDirectories [opt] If set also packages (e.g. *.zip) are
+/// handled as directories
+/// @param[in] singleList [opt]
+/// @return False if selection becomes canceled
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetFileFromDir(const std::string& directory,
+ const std::string& mask,
+ const std::string& heading,
+ std::string& path,
+ bool useThumbs = false,
+ bool useFileDirectories = false,
+ bool singleList = false)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret =
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_file_from_dir(
+ CPrivateBase::m_interface->toKodi->kodiBase, directory.c_str(), mask.c_str(),
+ heading.c_str(), path.c_str(), &retString, useThumbs, useFileDirectories, singleList);
+ if (retString != nullptr)
+ {
+ if (std::strlen(retString))
+ path = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_FileBrowser
+/// @brief File selection dialog to get several in to a list.
+///
+/// @param[in] shares With Shares becomes the available start folders be set.
+/// @param[in] mask The mask to filter visible files, e.g. ".m3u|.pls|.b4s|.wpl"
+/// @param[in] heading Dialog header name
+/// @param[out] fileList Return value about selected files
+/// @param[in] useThumbs [opt] If set show thumbs if possible on dialog.
+/// @param[in] useFileDirectories [opt] If set also packages (e.g. *.zip) are
+/// handled as directories.
+/// @return False if selection becomes canceled.
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetFileList(const std::string& shares,
+ const std::string& mask,
+ const std::string& heading,
+ std::vector<std::string>& fileList,
+ bool useThumbs = false,
+ bool useFileDirectories = false)
+{
+ using namespace ::kodi::addon;
+ char** list = nullptr;
+ unsigned int listSize = 0;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_file_list(
+ CPrivateBase::m_interface->toKodi->kodiBase, shares.c_str(), mask.c_str(), heading.c_str(),
+ &list, &listSize, useThumbs, useFileDirectories);
+ if (ret)
+ {
+ for (unsigned int i = 0; i < listSize; ++i)
+ fileList.emplace_back(list[i]);
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogFileBrowser->clear_file_list(
+ CPrivateBase::m_interface->toKodi->kodiBase, &list, listSize);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_FileBrowser
+/// @brief Source selection dialog.
+///
+/// @param[in,out] path As in the path to start and Return value about selected
+/// source
+/// @param[in] allowNetworkShares Allow also access to network
+/// @param[in] additionalShare [opt] With additionalShare becomes the available
+/// start folders be set.
+/// @param[in] type [opt]
+/// @return False if selection becomes canceled
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetSource(std::string& path,
+ bool allowNetworkShares,
+ const std::string& additionalShare = "",
+ const std::string& type = "")
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_source(
+ CPrivateBase::m_interface->toKodi->kodiBase, path.c_str(), &retString, allowNetworkShares,
+ additionalShare.c_str(), type.c_str());
+ if (retString != nullptr)
+ {
+ if (std::strlen(retString))
+ path = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_FileBrowser
+/// @brief Image selection dialog.
+///
+/// @param[in] shares With Shares becomes the available start folders be set
+/// @param[in] heading Dialog header name
+/// @param[out] path Return value about selected image
+/// @return False if selection becomes canceled
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetImage(const std::string& shares,
+ const std::string& heading,
+ std::string& path)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_image(
+ CPrivateBase::m_interface->toKodi->kodiBase, shares.c_str(), heading.c_str(), path.c_str(),
+ &retString);
+ if (retString != nullptr)
+ {
+ if (std::strlen(retString))
+ path = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_FileBrowser
+/// @brief Image selection dialog to get several in to a list.
+///
+/// @param[in] shares With Shares becomes the available start folders be set
+/// @param[in] heading Dialog header name
+/// @param[out] file_list Return value about selected images
+/// @return False if selection becomes canceled
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetImageList(const std::string& shares,
+ const std::string& heading,
+ std::vector<std::string>& file_list)
+{
+ using namespace ::kodi::addon;
+ char** list = nullptr;
+ unsigned int listSize = 0;
+ bool ret =
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogFileBrowser->show_and_get_image_list(
+ CPrivateBase::m_interface->toKodi->kodiBase, shares.c_str(), heading.c_str(), &list,
+ &listSize);
+ if (ret)
+ {
+ for (unsigned int i = 0; i < listSize; ++i)
+ file_list.emplace_back(list[i]);
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogFileBrowser->clear_file_list(
+ CPrivateBase::m_interface->toKodi->kodiBase, &list, listSize);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+}; // namespace FileBrowser
+
+} /* namespace dialogs */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Keyboard.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Keyboard.h
new file mode 100644
index 0000000..9aded50
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Keyboard.h
@@ -0,0 +1,405 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/gui/dialogs/keyboard.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace dialogs
+{
+
+//================================================================================
+/// @defgroup cpp_kodi_gui_dialogs_Keyboard Dialog Keyboard
+/// @ingroup cpp_kodi_gui_dialogs
+/// @brief @cpp_namespace{ kodi::gui::dialogs::Keyboard }
+/// **Keyboard dialogs**\n
+/// The functions listed below have to be permitted by the user for the
+/// representation of a keyboard around an input.
+///
+/// The class supports several kinds, from an easy text choice up to the
+/// passport Word production and their confirmation for add-on.
+///
+/// It has the header @ref Keyboard.h "#include <kodi/gui/dialogs/Keyboard.h>"
+/// be included to enjoy it.
+///
+namespace Keyboard
+{
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Keyboard
+/// @brief Show keyboard with initial value `text` and replace with result
+/// string.
+///
+/// @param[in,out] text Overwritten with user input if return=true.
+/// @param[in] heading String shown on dialog title.
+/// @param[in] allowEmptyResult Whether a blank password is valid or not.
+/// @param[in] hiddenInput [opt] The inserted input is not shown as text.
+/// @param[in] autoCloseMs [opt] To close the dialog after a specified time, in
+/// milliseconds, default is 0 which keeps the dialog
+/// open indefinitely.
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/Keyboard.h>
+///
+/// // The example shows the display of keyboard call dialog at Kodi from the add-on.
+/// // Below all values are set, however, can last two (hidden input = false and autoCloseMs = 0)
+/// // to be released if not needed.
+/// std::string text = "Please change me to them want you want"; // It can be leaved empty or a entry text added
+/// bool bRet = ::kodi::gui::dialogs::Keyboard::ShowAndGetInput(text,
+/// "Demonstration text entry",
+/// true,
+/// false,
+/// 0);
+/// fprintf(stderr, "Written keyboard input is : '%s' and was %s\n",
+/// text.c_str(), bRet ? "OK" : "Canceled");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetInput(std::string& text,
+ const std::string& heading,
+ bool allowEmptyResult,
+ bool hiddenInput = false,
+ unsigned int autoCloseMs = 0)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret =
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogKeyboard->show_and_get_input_with_head(
+ CPrivateBase::m_interface->toKodi->kodiBase, text.c_str(), &retString, heading.c_str(),
+ allowEmptyResult, hiddenInput, autoCloseMs);
+ if (retString != nullptr)
+ {
+ text = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Keyboard
+/// @brief The example shows the display of keyboard call dialog at Kodi
+/// from the add-on.
+///
+/// @param[out] text Overwritten with user input if return=true.
+/// @param[in] allowEmptyResult If set to true keyboard can also exited without
+/// entered text.
+/// @param[in] autoCloseMs [opt] To close the dialog after a specified time, in
+/// milliseconds, default is 0 which keeps the dialog
+/// open indefinitely.
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetInput(std::string& text,
+ bool allowEmptyResult,
+ unsigned int autoCloseMs = 0)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogKeyboard->show_and_get_input(
+ CPrivateBase::m_interface->toKodi->kodiBase, text.c_str(), &retString, allowEmptyResult,
+ autoCloseMs);
+ if (retString != nullptr)
+ {
+ text = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Keyboard
+/// @brief Shows keyboard and prompts for a password. Differs from
+/// `ShowAndVerifyNewPassword()` in that no second verification.
+///
+/// @param[in,out] newPassword Overwritten with user input if return=true.
+/// @param[in] heading String shown on dialog title.
+/// @param[in] allowEmptyResult Whether a blank password is valid or not.
+/// @param[in] autoCloseMs [opt] To close the dialog after a specified time, in
+/// milliseconds, default is 0 which keeps the dialog
+/// open indefinitely.
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetNewPassword(std::string& newPassword,
+ const std::string& heading,
+ bool allowEmptyResult,
+ unsigned int autoCloseMs = 0)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogKeyboard
+ ->show_and_get_new_password_with_head(
+ CPrivateBase::m_interface->toKodi->kodiBase, newPassword.c_str(), &retString,
+ heading.c_str(), allowEmptyResult, autoCloseMs);
+ if (retString != nullptr)
+ {
+ newPassword = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Keyboard
+/// @brief Shows keyboard and prompts for a password. Differs from
+/// `ShowAndVerifyNewPassword()` in that no second verification.
+///
+/// @param[in,out] newPassword Overwritten with user input if return=true.
+/// @param[in] autoCloseMs [opt] To close the dialog after a specified time, in
+/// milliseconds, default is 0 which keeps the dialog
+/// open indefinitely.
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetNewPassword(std::string& newPassword,
+ unsigned int autoCloseMs = 0)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogKeyboard->show_and_get_new_password(
+ CPrivateBase::m_interface->toKodi->kodiBase, newPassword.c_str(), &retString, autoCloseMs);
+ if (retString != nullptr)
+ {
+ newPassword = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Keyboard
+/// @brief Show keyboard twice to get and confirm a user-entered password
+/// string.
+///
+/// @param[out] newPassword Overwritten with user input if return=true.
+/// @param[in] heading String shown on dialog title.
+/// @param[in] allowEmptyResult
+/// @param[in] autoCloseMs [opt] To close the dialog after a specified time, in
+/// milliseconds, default is 0 which keeps the dialog
+/// open indefinitely.
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/General.h>
+/// #include <kodi/gui/dialogs/Keyboard.h>
+///
+/// // The example below shows the complete use of keyboard dialog for password
+/// // check. If only one check from add-on needed can be function with retries
+/// // set to '0' called alone.
+/// //
+/// // The use of MD5 translated password is always required for the check on Kodi!
+///
+/// int maxretries = 3;
+/// // Password names need to be send as md5 sum to kodi.
+/// std::string password;
+/// kodi::GetMD5("kodi", password);
+///
+/// // To the loop about password checks.
+/// int ret;
+/// for (unsigned int i = 0; i < maxretries; i++)
+/// {
+/// // Ask the user about the password.
+/// ret = ::kodi::gui::dialogs::Keyboard::ShowAndVerifyPassword(password, "Demo password call for PW 'kodi'", i, 0);
+/// if (ret == 0)
+/// {
+/// fprintf(stderr, "Password successfully confirmed after '%i' tries\n", i+1);
+/// break;
+/// }
+/// else if (ret < 0)
+/// {
+/// fprintf(stderr, "Canceled editing on try '%i'\n", i+1);
+/// break;
+/// }
+/// else // if (ret > 0)
+/// {
+/// fprintf(stderr, "Wrong password entered on try '%i'\n", i+1);
+/// }
+/// }
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL ShowAndVerifyNewPassword(std::string& newPassword,
+ const std::string& heading,
+ bool allowEmptyResult,
+ unsigned int autoCloseMs = 0)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogKeyboard
+ ->show_and_verify_new_password_with_head(
+ CPrivateBase::m_interface->toKodi->kodiBase, &retString, heading.c_str(),
+ allowEmptyResult, autoCloseMs);
+ if (retString != nullptr)
+ {
+ newPassword = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Keyboard
+/// @brief Show keyboard twice to get and confirm a user-entered password
+/// string.
+///
+/// @param[out] newPassword Overwritten with user input if return=true.
+/// @param[in] autoCloseMs [opt] To close the dialog after a specified time, in
+/// milliseconds, default is 0 which keeps the dialog
+/// open indefinitely.
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+inline bool ATTR_DLL_LOCAL ShowAndVerifyNewPassword(std::string& newPassword,
+ unsigned int autoCloseMs = 0)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret =
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogKeyboard->show_and_verify_new_password(
+ CPrivateBase::m_interface->toKodi->kodiBase, &retString, autoCloseMs);
+ if (retString != nullptr)
+ {
+ newPassword = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Keyboard
+/// @brief Show keyboard and verify user input against `password`.
+///
+/// @param[in,out] password Value to compare against user input.
+/// @param[in] heading String shown on dialog title.
+/// @param[in] retries If greater than 0, shows "Incorrect password, %d retries
+/// left" on dialog line 2, else line 2 is blank.
+/// @param[in] autoCloseMs [opt] To close the dialog after a specified time, in
+/// milliseconds, default is 0 which keeps the dialog
+/// open indefinitely.
+/// @return 0 if successful display and user input. 1 if unsuccessful input.
+/// -1 if no user input or canceled editing.
+///
+inline int ATTR_DLL_LOCAL ShowAndVerifyPassword(std::string& password,
+ const std::string& heading,
+ int retries,
+ unsigned int autoCloseMs = 0)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ int ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogKeyboard->show_and_verify_password(
+ CPrivateBase::m_interface->toKodi->kodiBase, password.c_str(), &retString, heading.c_str(),
+ retries, autoCloseMs);
+ if (retString != nullptr)
+ {
+ password = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Keyboard
+/// @brief Shows a filter related keyboard.
+///
+/// @param[in,out] text Overwritten with user input if return=true.
+/// @param[in] searching Use dialog for search and send our search message in
+/// safe way (only the active window needs it)
+/// - header name if true is "Enter search string"
+/// - header name if false is "Enter value"
+/// @param autoCloseMs [opt] To close the dialog after a specified time, in
+/// milliseconds, default is 0 which keeps the dialog open
+/// indefinitely.
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetFilter(std::string& text,
+ bool searching,
+ unsigned int autoCloseMs = 0)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogKeyboard->show_and_get_filter(
+ CPrivateBase::m_interface->toKodi->kodiBase, text.c_str(), &retString, searching,
+ autoCloseMs);
+ if (retString != nullptr)
+ {
+ text = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Keyboard
+/// @brief Send a text to a visible keyboard.
+///
+/// @param[in] text Overwritten with user input if return=true.
+/// @param[in] closeKeyboard [opt] The open dialog is if also closed on 'true'.
+/// @return true if successful done, false if unsuccessful or keyboard not
+/// present.
+///
+inline bool ATTR_DLL_LOCAL SendTextToActiveKeyboard(const std::string& text,
+ bool closeKeyboard = false)
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogKeyboard->send_text_to_active_keyboard(
+ CPrivateBase::m_interface->toKodi->kodiBase, text.c_str(), closeKeyboard);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Keyboard
+/// @brief Check for visible keyboard on GUI.
+///
+/// @return true if keyboard present, false if not present
+///
+inline bool ATTR_DLL_LOCAL IsKeyboardActivated()
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogKeyboard->is_keyboard_activated(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+}
+//------------------------------------------------------------------------------
+}; // namespace Keyboard
+
+} /* namespace dialogs */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Numeric.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Numeric.h
new file mode 100644
index 0000000..54b7720
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Numeric.h
@@ -0,0 +1,347 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/gui/dialogs/numeric.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace dialogs
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_dialogs_Numeric Dialog Numeric
+/// @ingroup cpp_kodi_gui_dialogs
+/// @{
+/// @brief @cpp_namespace{ kodi::gui::dialogs::Numeric }
+/// **Numeric dialogs**\n
+/// The functions listed below have to be permitted by the user for the
+/// representation of a numeric keyboard around an input.
+///
+/// The class supports several kinds, from an easy number choice up to the
+/// passport Word production and their confirmation for add-on.
+///
+/// It has the header @ref Numeric.h "#include <kodi/gui/dialogs/Numeric.h>"
+/// be included to enjoy it.
+///
+namespace Numeric
+{
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Numeric
+/// @brief Use dialog to get numeric new password
+///
+/// @param[out] newPassword String to preload into the keyboard accumulator.
+/// Overwritten with user input if return=true.
+/// Returned in MD5 format.
+/// @return true if successful display and user input entry/re-entry. false if
+/// unsuccessful display, no user input, or canceled editing.
+///
+inline bool ATTR_DLL_LOCAL ShowAndVerifyNewPassword(std::string& newPassword)
+{
+ using namespace ::kodi::addon;
+ char* pw = nullptr;
+ bool ret =
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogNumeric->show_and_verify_new_password(
+ CPrivateBase::m_interface->toKodi->kodiBase, &pw);
+ if (pw != nullptr)
+ {
+ newPassword = pw;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase, pw);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+///
+/// @ingroup cpp_kodi_gui_dialogs_Numeric
+/// @brief Use dialog to verify numeric password.
+///
+/// @param[in] password Password to compare with user input, need
+/// in MD5 format.
+/// @param[in] heading Heading to display
+/// @param[in] retries If greater than 0, shows "Incorrect
+/// password, %d retries left" on dialog
+/// line 2, else line 2 is blank.
+/// @return Possible values:
+/// - 0 if successful display and user input.
+/// - 1 if unsuccessful input.
+/// - -1 if no user input or canceled editing.
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <stdio.h> // fprintf
+/// #include <kodi/General.h>
+/// #include <kodi/gui/dialogs/Numeric.h>
+///
+/// // The example below shows the complete use of keyboard dialog for password
+/// // check. If only one check from add-on needed can be function with retries
+/// // set to '0' called alone.
+/// //
+/// // The use of MD5 translated password is always required for the check on Kodi!
+///
+/// int maxretries = 3;
+///
+/// // Password names need to be send as md5 sum to kodi.
+/// std::string password = kodi::GetMD5("1234");
+///
+/// // To the loop about password checks.
+/// int ret;
+/// for (unsigned int i = 0; i < maxretries; i++)
+/// {
+/// // Ask the user about the password.
+/// ret = kodi::gui::dialogs::Numeric::ShowAndVerifyPassword(password, "Demo numeric password call for PW '1234'", i);
+/// if (ret == 0)
+/// {
+/// fprintf(stderr, "Numeric password successfully confirmed after '%i' tries\n", i+1);
+/// break;
+/// }
+/// else if (ret < 0)
+/// {
+/// fprintf(stderr, "Canceled editing on try '%i'\n", i+1);
+/// break;
+/// }
+/// else // if (ret > 0)
+/// {
+/// fprintf(stderr, "Wrong numeric password entered on try '%i'\n", i+1);
+/// }
+/// }
+/// ~~~~~~~~~~~~~
+///
+inline int ATTR_DLL_LOCAL ShowAndVerifyPassword(const std::string& password,
+ const std::string& heading,
+ int retries)
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogNumeric->show_and_verify_password(
+ CPrivateBase::m_interface->toKodi->kodiBase, password.c_str(), heading.c_str(), retries);
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Numeric
+/// @brief Use dialog to verify numeric password
+///
+/// @param[in,out] toVerify Value to compare against user input.
+/// @param[in] heading Heading to display
+/// @param[in] verifyInput If set as true we verify the users input versus
+/// toVerify.
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+inline bool ATTR_DLL_LOCAL ShowAndVerifyInput(std::string& toVerify,
+ const std::string& heading,
+ bool verifyInput)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogNumeric->show_and_verify_input(
+ CPrivateBase::m_interface->toKodi->kodiBase, toVerify.c_str(), &retString, heading.c_str(),
+ verifyInput);
+ if (retString != nullptr)
+ {
+ toVerify = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Numeric
+/// @brief Use dialog to get time value.
+///
+/// @param[out] time Overwritten with user input if return=true and time
+/// inserted.
+/// @param[in] heading Heading to display.
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <stdio.h> // printf
+/// #include <time.h> // time_t, struct tm, time, localtime, strftime
+/// #include <kodi/gui/dialogs/Numeric.h>
+///
+/// time_t rawtime;
+/// struct tm * timeinfo;
+/// char buffer [10];
+///
+/// time (&rawtime);
+/// timeinfo = localtime(&rawtime);
+/// bool bRet = kodi::gui::dialogs::Numeric::ShowAndGetTime(*timeinfo, "Selected time test call");
+/// strftime(buffer, sizeof(buffer), "%H:%M.", timeinfo);
+/// printf("Selected time it's %s and was on Dialog %s\n", buffer, bRet ? "OK" : "Canceled");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetTime(tm& time, const std::string& heading)
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogNumeric->show_and_get_time(
+ CPrivateBase::m_interface->toKodi->kodiBase, &time, heading.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Numeric
+/// @brief Use dialog to get date value.
+///
+/// @param[in,out] date Overwritten with user input if return=true and date
+/// inserted.
+/// @param[in] heading Heading to display
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <stdio.h> // printf
+/// #include <time.h> // time_t, struct tm, time, localtime, strftime
+/// #include <kodi/gui/dialogs/Numeric.h>
+///
+/// time_t rawtime;
+/// struct tm * timeinfo;
+/// char buffer [20];
+///
+/// time (&rawtime);
+/// timeinfo = localtime(&rawtime);
+/// bool bRet = kodi::gui::dialogs::Numeric::ShowAndGetDate(*timeinfo, "Selected date test call");
+/// strftime(buffer, sizeof(buffer), "%Y-%m-%d", timeinfo);
+/// printf("Selected date it's %s and was on Dialog %s\n", buffer, bRet ? "OK" : "Canceled");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetDate(tm& date, const std::string& heading)
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogNumeric->show_and_get_date(
+ CPrivateBase::m_interface->toKodi->kodiBase, &date, heading.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Numeric
+/// @brief Use dialog to get a IP
+///
+/// @param[in,out] ipAddress Overwritten with user input if return=true and
+/// IP address inserted.
+/// @param[in] heading Heading to display.
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetIPAddress(std::string& ipAddress, const std::string& heading)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogNumeric->show_and_get_ip_address(
+ CPrivateBase::m_interface->toKodi->kodiBase, ipAddress.c_str(), &retString, heading.c_str());
+ if (retString != nullptr)
+ {
+ ipAddress = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Numeric
+/// @brief Use dialog to get normal number.
+///
+/// @param[in,out] input Overwritten with user input if return=true and time
+/// in seconds inserted
+/// @param[in] heading Heading to display
+/// @param[in] autoCloseTimeoutMs [opt] To close the dialog after a specified
+/// time, in milliseconds, default is 0
+/// which keeps the dialog open
+/// indefinitely.
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <stdio.h> // printf
+/// #include <stdlib.h> // strtoull (C++11)
+/// #include <kodi/gui/dialogs/Numeric.h>
+///
+/// std::string number;
+/// bool bRet = kodi::gui::dialogs::Numeric::ShowAndGetNumber(number, "Number test call");
+/// printf("Written number input is : %llu and was %s\n",
+/// strtoull(number.c_str(), nullptr, 0), bRet ? "OK" : "Canceled");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetNumber(std::string& input,
+ const std::string& heading,
+ unsigned int autoCloseTimeoutMs = 0)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogNumeric->show_and_get_number(
+ CPrivateBase::m_interface->toKodi->kodiBase, input.c_str(), &retString, heading.c_str(),
+ autoCloseTimeoutMs);
+ if (retString != nullptr)
+ {
+ input = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Numeric
+/// @brief Show numeric keypad to get seconds.
+///
+/// @param[in,out] time Overwritten with user input if return=true and time
+/// in seconds inserted.
+/// @param[in] heading Heading to display
+/// @return true if successful display and user input. false if unsuccessful
+/// display, no user input, or canceled editing.
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetSeconds(std::string& time, const std::string& heading)
+{
+ using namespace ::kodi::addon;
+ char* retString = nullptr;
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogNumeric->show_and_get_seconds(
+ CPrivateBase::m_interface->toKodi->kodiBase, time.c_str(), &retString, heading.c_str());
+ if (retString != nullptr)
+ {
+ time = retString;
+ CPrivateBase::m_interface->toKodi->free_string(CPrivateBase::m_interface->toKodi->kodiBase,
+ retString);
+ }
+ return ret;
+}
+//------------------------------------------------------------------------------
+}; // namespace Numeric
+/// @}
+
+} /* namespace dialogs */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/OK.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/OK.h
new file mode 100644
index 0000000..a9fcb3f
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/OK.h
@@ -0,0 +1,101 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/gui/dialogs/ok.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace dialogs
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_dialogs_OK Dialog OK
+/// @ingroup cpp_kodi_gui_dialogs
+/// @{
+/// @brief @cpp_namespace{ kodi::gui::dialogs::OK }
+/// **OK dialog**\n
+/// The functions listed below permit the call of a dialogue of information, a
+/// confirmation of the user by press from OK required.
+///
+/// It has the header @ref OK.h "#include <kodi/gui/dialogs/OK.h>"
+/// be included to enjoy it.
+///
+namespace OK
+{
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_OK
+/// @brief Use dialog to inform user with text and confirmation with OK with
+/// continued string.
+///
+/// @param[in] heading Dialog heading.
+/// @param[in] text Multi-line text.
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/OK.h>
+/// ...
+/// kodi::gui::dialogs::OK::ShowAndGetInput("Test dialog", "Hello World!\nI'm a call from add-on\n :) :D");
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL ShowAndGetInput(const std::string& heading, const std::string& text)
+{
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogOK->show_and_get_input_single_text(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), text.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_OK
+/// @brief Use dialog to inform user with text and confirmation with OK with
+/// strings separated to the lines.
+///
+/// @param[in] heading Dialog heading.
+/// @param[in] line0 Line #1 text.
+/// @param[in] line1 Line #2 text.
+/// @param[in] line2 Line #3 text.
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/OK.h>
+/// ...
+/// kodi::gui::dialogs::OK::ShowAndGetInput("Test dialog", "Hello World!", "I'm a call from add-on", " :) :D");
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL ShowAndGetInput(const std::string& heading,
+ const std::string& line0,
+ const std::string& line1,
+ const std::string& line2)
+{
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogOK->show_and_get_input_line_text(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), line0.c_str(), line1.c_str(),
+ line2.c_str());
+}
+//------------------------------------------------------------------------------
+} // namespace OK
+/// @}
+
+} /* namespace dialogs */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Progress.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Progress.h
new file mode 100644
index 0000000..3dfd8ba
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Progress.h
@@ -0,0 +1,244 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/gui/dialogs/progress.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace dialogs
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_dialogs_CProgress Dialog Progress
+/// @ingroup cpp_kodi_gui_dialogs
+/// @brief @cpp_class{ kodi::gui::dialogs::CProgress }
+/// **Progress dialog shown in center**\n
+/// The with @ref Progress.h "#include <kodi/gui/dialogs/Progress.h>"
+/// given class are basically used to create Kodi's progress dialog with named
+/// text fields.
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/Progress.h>
+///
+/// kodi::gui::dialogs::CProgress *progress = new kodi::gui::dialogs::CProgress;
+/// progress->SetHeading("Test progress");
+/// progress->SetLine(1, "line 1");
+/// progress->SetLine(2, "line 2");
+/// progress->SetLine(3, "line 3");
+/// progress->SetCanCancel(true);
+/// progress->ShowProgressBar(true);
+/// progress->Open();
+/// for (unsigned int i = 0; i < 100; i += 10)
+/// {
+/// progress->SetPercentage(i);
+/// sleep(1);
+/// }
+/// delete progress;
+/// ~~~~~~~~~~~~~
+///
+class ATTR_DLL_LOCAL CProgress
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief Construct a new dialog
+ ///
+ CProgress()
+ {
+ using namespace ::kodi::addon;
+ m_DialogHandle = CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->new_dialog(
+ CPrivateBase::m_interface->toKodi->kodiBase);
+ if (!m_DialogHandle)
+ kodi::Log(ADDON_LOG_FATAL,
+ "kodi::gui::dialogs::CProgress can't create window class from Kodi !!!");
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief Destructor
+ ///
+ ~CProgress()
+ {
+ using namespace ::kodi::addon;
+ if (m_DialogHandle)
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->delete_dialog(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief To open the dialog
+ ///
+ void Open()
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->open(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief Set the heading title of dialog
+ ///
+ /// @param[in] heading Title string to use
+ ///
+ void SetHeading(const std::string& heading)
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->set_heading(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle, heading.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief To set the line text field on dialog from 0 - 2
+ ///
+ /// @param[in] iLine Line number
+ /// @param[in] line Text string
+ ///
+ void SetLine(unsigned int iLine, const std::string& line)
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->set_line(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle, iLine, line.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief To enable and show cancel button on dialog
+ ///
+ /// @param[in] canCancel if true becomes it shown
+ ///
+ void SetCanCancel(bool canCancel)
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->set_can_cancel(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle, canCancel);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief To check dialog for clicked cancel button
+ ///
+ /// @return True if canceled
+ ///
+ bool IsCanceled() const
+ {
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->is_canceled(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief Get the current progress position as percent
+ ///
+ /// @param[in] percentage Position to use from 0 to 100
+ ///
+ void SetPercentage(int percentage)
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->set_percentage(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle, percentage);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief To set the current progress position as percent
+ ///
+ /// @return Current Position used from 0 to 100
+ ///
+ int GetPercentage() const
+ {
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->get_percentage(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief To show or hide progress bar dialog
+ ///
+ /// @param[in] onOff If true becomes it shown
+ ///
+ void ShowProgressBar(bool onOff)
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->show_progress_bar(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle, onOff);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief Set the maximum position of progress, needed if `SetProgressAdvance(...)` is used
+ ///
+ /// @param[in] max Biggest usable position to use
+ ///
+ void SetProgressMax(int max)
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->set_progress_max(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle, max);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief To increase progress bar by defined step size until reach of maximum position
+ ///
+ /// @param[in] steps Step size to increase, default is 1
+ ///
+ void SetProgressAdvance(int steps = 1)
+ {
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->set_progress_advance(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle, steps);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_gui_dialogs_CProgress
+ /// @brief To check progress was canceled on work
+ ///
+ /// @return True if aborted
+ ///
+ bool Abort()
+ {
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogProgress->abort(
+ CPrivateBase::m_interface->toKodi->kodiBase, m_DialogHandle);
+ }
+ //----------------------------------------------------------------------------
+
+private:
+ KODI_GUI_HANDLE m_DialogHandle;
+};
+
+} /* namespace dialogs */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Select.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Select.h
new file mode 100644
index 0000000..09bccc3
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/Select.h
@@ -0,0 +1,270 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/gui/dialogs/select.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace dialogs
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_dialogs_Select_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_gui_dialogs_Select
+/// @brief **Dialog Select definition values**\n
+/// Data structures associated with this dialog.
+///
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Select_Defs
+/// @brief **Selection entry structure**\n
+/// Used to provide the necessary data for the selection dialog and to declare
+/// the selected position in it.
+///
+typedef struct SSelectionEntry
+{
+ /*! \cond PRIVATE */
+ SSelectionEntry() = default;
+ /*! \endcond */
+
+ /// Entry identfication string
+ std::string id;
+
+ /// Entry name to show on GUI dialog
+ std::string name;
+
+ /// Place where entry can be preselected and after return the from user
+ /// selected is set.
+ bool selected = false;
+} SSelectionEntry;
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_dialogs_Select Dialog Select
+/// @ingroup cpp_kodi_gui_dialogs
+/// @{
+/// @brief @cpp_namespace{ kodi::gui::dialogs::Select }
+/// **Selection dialog**\n
+/// The function listed below permits the call of a dialogue to select of an
+/// entry as a key
+///
+/// It has the header @ref Select.h "#include <kodi/gui/dialogs/Select.h>"
+/// be included to enjoy it.
+///
+///
+namespace Select
+{
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Select
+/// @brief Show a selection dialog about given parts.
+///
+/// @param[in] heading Dialog heading name
+/// @param[in] entries String list about entries
+/// @param[in] selected [opt] Predefined selection (default is <tt>-1</tt> for
+/// the first)
+/// @param[in] autoclose [opt] To close dialog automatic after the given time
+/// in ms. As '0' it stays open.
+/// @return The selected entry, if return <tt>-1</tt> was nothing selected or
+/// canceled
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/Select.h>
+///
+/// const std::vector<std::string> entries
+/// {
+/// "Test 1",
+/// "Test 2",
+/// "Test 3",
+/// "Test 4",
+/// "Test 5"
+/// };
+///
+/// int selected = kodi::gui::dialogs::Select::Show("Test selection", entries, -1);
+/// if (selected < 0)
+/// fprintf(stderr, "Item selection canceled\n");
+/// else
+/// fprintf(stderr, "Selected item is: %i\n", selected);
+/// ~~~~~~~~~~~~~
+///
+inline int ATTR_DLL_LOCAL Show(const std::string& heading,
+ const std::vector<std::string>& entries,
+ int selected = -1,
+ unsigned int autoclose = 0)
+{
+ using namespace ::kodi::addon;
+ unsigned int size = static_cast<unsigned int>(entries.size());
+ const char** cEntries = (const char**)malloc(size * sizeof(const char**));
+ for (unsigned int i = 0; i < size; ++i)
+ {
+ cEntries[i] = entries[i].c_str();
+ }
+ int ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogSelect->open(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), cEntries, size, selected,
+ autoclose);
+ free(cEntries);
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Select
+/// @brief Show a selection dialog about given parts.
+///
+/// This function is mostly equal to the other, only becomes the string list
+/// here done by a @ref SSelectionEntry, where a ID string can be defined.
+///
+/// @param[in] heading Dialog heading name
+/// @param[in] entries @ref SSelectionEntry list about entries
+/// @param[in] selected [opt] Predefined selection (default is <tt>-1</tt> for
+/// the first)
+/// @param[in] autoclose [opt] To close dialog automatic after the given time
+/// in ms. As '0' it stays open.
+/// @return The selected entry, if return <tt>-1</tt> was nothing selected
+/// or canceled
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/Select.h>
+///
+/// std::vector<kodi::gui::dialogs::SSelectionEntry> entries
+/// {
+/// { "ID 1", "Test 1", false },
+/// { "ID 2", "Test 2", false },
+/// { "ID 3", "Test 3", false },
+/// { "ID 4", "Test 4", false },
+/// { "ID 5", "Test 5", false }
+/// };
+///
+/// int selected = kodi::gui::dialogs::Select::Show("Test selection", entries, -1);
+/// if (selected < 0)
+/// fprintf(stderr, "Item selection canceled\n");
+/// else
+/// fprintf(stderr, "Selected item is: %i\n", selected);
+/// ~~~~~~~~~~~~~
+///
+inline int ATTR_DLL_LOCAL Show(const std::string& heading,
+ std::vector<kodi::gui::dialogs::SSelectionEntry>& entries,
+ int selected = -1,
+ unsigned int autoclose = 0)
+{
+ using namespace ::kodi::addon;
+ unsigned int size = static_cast<unsigned int>(entries.size());
+ const char** cEntries = static_cast<const char**>(malloc(size * sizeof(const char*)));
+ for (unsigned int i = 0; i < size; ++i)
+ {
+ cEntries[i] = entries[i].name.c_str();
+ if (selected == -1 && entries[i].selected)
+ selected = i;
+ }
+ int ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogSelect->open(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), cEntries, size, selected,
+ autoclose);
+ if (ret >= 0)
+ {
+ entries[ret].selected = true;
+ }
+ free(cEntries);
+ return ret;
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_Select
+/// @brief Show a multiple selection dialog about given parts.
+///
+/// @param[in] heading Dialog heading name
+/// @param[in] entries @ref SSelectionEntry list about entries
+/// @param[in] autoclose [opt] To close dialog automatic after the given time in
+/// ms. As '0' it stays open.
+/// @return The selected entries, if return <tt>empty</tt> was nothing selected
+/// or canceled
+///
+/// With selected on @ref SSelectionEntry can be a pre selection defined.
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/Select.h>
+///
+/// std::vector<kodi::gui::dialogs::SSelectionEntry> entries
+/// {
+/// { "ID 1", "Test 1", false },
+/// { "ID 2", "Test 2", false },
+/// { "ID 3", "Test 3", false },
+/// { "ID 4", "Test 4", false },
+/// { "ID 5", "Test 5", false }
+/// };
+///
+/// bool ret = kodi::gui::dialogs::Select::ShowMultiSelect("Test selection", entries);
+/// if (!ret)
+/// fprintf(stderr, "Selection canceled\n");
+/// else
+/// {
+/// fprintf(stderr, "Selected items:\n");
+/// for (const auto& entry : entries)
+/// {
+/// if (entry.selected)
+/// fprintf(stderr, " - %s\n", entry.selected.id.c_str());
+/// }
+/// }
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL
+ShowMultiSelect(const std::string& heading,
+ std::vector<kodi::gui::dialogs::SSelectionEntry>& entries,
+ int autoclose = 0)
+{
+ using namespace ::kodi::addon;
+ unsigned int size = static_cast<unsigned int>(entries.size());
+ const char** cEntryIDs = static_cast<const char**>(malloc(size * sizeof(const char*)));
+ const char** cEntryNames = static_cast<const char**>(malloc(size * sizeof(const char*)));
+ bool* cEntriesSelected = static_cast<bool*>(malloc(size * sizeof(bool)));
+ for (unsigned int i = 0; i < size; ++i)
+ {
+ cEntryIDs[i] = entries[i].id.c_str();
+ cEntryNames[i] = entries[i].name.c_str();
+ cEntriesSelected[i] = entries[i].selected;
+ }
+ bool ret = CPrivateBase::m_interface->toKodi->kodi_gui->dialogSelect->open_multi_select(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), cEntryIDs, cEntryNames,
+ cEntriesSelected, size, autoclose);
+ if (ret)
+ {
+ for (unsigned int i = 0; i < size; ++i)
+ entries[i].selected = cEntriesSelected[i];
+ }
+ free(cEntryNames);
+ free(cEntryIDs);
+ free(cEntriesSelected);
+ return ret;
+}
+//------------------------------------------------------------------------------
+}; // namespace Select
+/// @}
+
+} /* namespace dialogs */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/TextViewer.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/TextViewer.h
new file mode 100644
index 0000000..7c2e6ca
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/TextViewer.h
@@ -0,0 +1,109 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/gui/dialogs/text_viewer.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace dialogs
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_dialogs_TextViewer Dialog Text Viewer
+/// @ingroup cpp_kodi_gui_dialogs
+/// @{
+/// @brief @cpp_namespace{ kodi::gui::dialogs::TextViewer }
+/// **Text viewer dialog**\n
+/// The text viewer dialog can be used to display descriptions, help texts or
+/// other larger texts.
+///
+/// In order to achieve a line break is a <b>\\n</b> directly in the text or
+/// in the <em>"./resources/language/resource.language.??_??/strings.po"</em>
+/// to call with <b>std::string kodi::general::GetLocalizedString(...);</b>.
+///
+/// It has the header @ref TextViewer.h "#include <kodi/gui/dialogs/TextViewer.h>"
+/// be included to enjoy it.
+///
+namespace TextViewer
+{
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_TextViewer
+/// @brief Show info text dialog
+///
+/// @param[in] heading mall heading text
+/// @param[in] text Showed text on dialog
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/TextViewer.h>
+///
+/// kodi::gui::dialogs::TextViewer::Show("The Wizard of Oz (1939 film)",
+/// "The Wizard of Oz is a 1939 American musical comedy-drama fantasy film "
+/// "produced by Metro-Goldwyn-Mayer, and the most well-known and commercially "
+/// "successful adaptation based on the 1900 novel The Wonderful Wizard of Oz "
+/// "by L. Frank Baum. The film stars Judy Garland as Dorothy Gale. The film"
+/// "co-stars Terry the dog, billed as Toto; Ray Bolger, Jack Haley, Bert Lahr, "
+/// "Frank Morgan, Billie Burke, Margaret Hamilton, with Charley Grapewin and "
+/// "Clara Blandick, and the Singer Midgets as the Munchkins.\n"
+/// "\n"
+/// "Notable for its use of Technicolor, fantasy storytelling, musical score and "
+/// "unusual characters, over the years it has become an icon of American popular "
+/// "culture. It was nominated for six Academy Awards, including Best Picture but "
+/// "lost to Gone with the Wind. It did win in two other categories including Best "
+/// "Original Song for \"Over the Rainbow\". However, the film was a box office "
+/// "disappointment on its initial release, earning only $3,017,000 on a $2,777,000 "
+/// "budget, despite receiving largely positive reviews. It was MGM's most "
+/// "expensive production at that time, and did not completely recoup the studio's "
+/// "investment and turn a profit until theatrical re-releases starting in 1949.\n"
+/// "\n"
+/// "The 1956 broadcast television premiere of the film on CBS re-introduced the "
+/// "film to the wider public and eventually made the presentation an annual "
+/// "tradition, making it one of the most known films in cinema history. The "
+/// "film was named the most-viewed motion picture on television syndication by "
+/// "the Library of Congress who also included the film in its National Film "
+/// "Registry in its inaugural year in 1989. Designation on the registry calls "
+/// "for efforts to preserve it for being \"culturally, historically, and "
+/// "aesthetically significant\". It is also one of the few films on UNESCO's "
+/// "Memory of the World Register.\n"
+/// "\n"
+/// "The Wizard of Oz is often ranked on best-movie lists in critics' and public "
+/// "polls. It is the source of many quotes referenced in modern popular culture. "
+/// "It was directed primarily by Victor Fleming (who left production to take "
+/// "over direction on the troubled Gone with the Wind production). Noel Langley, "
+/// "Florence Ryerson and Edgar Allan Woolf received credit for the screenplay, "
+/// "but there were uncredited contributions by others. The songs were written "
+/// "by Edgar \"Yip\" Harburg (lyrics) and Harold Arlen (music). The incidental "
+/// "music, based largely on the songs, was composed by Herbert Stothart, with "
+/// "interspersed renderings from classical composers.\n");
+/// ~~~~~~~~~~~~~
+///
+inline void ATTR_DLL_LOCAL Show(const std::string& heading, const std::string& text)
+{
+ using namespace ::kodi::addon;
+ CPrivateBase::m_interface->toKodi->kodi_gui->dialogTextViewer->open(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), text.c_str());
+}
+//------------------------------------------------------------------------------
+}; // namespace TextViewer
+/// @}
+
+} /* namespace dialogs */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/YesNo.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/YesNo.h
new file mode 100644
index 0000000..e4d3d12
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/dialogs/YesNo.h
@@ -0,0 +1,188 @@
+/*
+ * 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 "../../AddonBase.h"
+#include "../../c-api/gui/dialogs/yes_no.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+namespace dialogs
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_dialogs_YesNo Dialog Yes/No
+/// @ingroup cpp_kodi_gui_dialogs
+/// @{
+/// @brief @cpp_namespace{ kodi::gui::dialogs::YesNo }
+/// **Yes / No dialog**\n
+/// The Yes / No dialog can be used to inform the user about questions and get
+/// the answer.
+///
+/// In order to achieve a line break is a <b>\\n</b> directly in the text or
+/// in the <em>"./resources/language/resource.language.??_??/strings.po"</em>
+/// to call with <b>std::string kodi::general::GetLocalizedString(...);</b>.
+///
+/// It has the header @ref YesNo.h "#include <kodi/gui/dialogs/YesNo.h>"
+/// be included to enjoy it.
+///
+namespace YesNo
+{
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_YesNo
+/// @brief Use dialog to get numeric new password with one text string shown
+/// everywhere and cancel return field.
+///
+/// @param[in] heading Dialog heading
+/// @param[in] text Multi-line text
+/// @param[out] canceled Return value about cancel button
+/// @param[in] noLabel [opt] label to put on the no button
+/// @param[in] yesLabel [opt] label to put on the yes button
+/// @return Returns True if 'Yes' was pressed, else False
+///
+/// @note It is preferred to only use this as it is actually a multi-line text.
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/YesNo.h>
+///
+/// bool canceled = false;
+/// bool ret = kodi::gui::dialogs::YesNo::ShowAndGetInput(
+/// "Yes / No test call", // The Header
+/// "You has opened Yes / No dialog for test\n\nIs this OK for you?",
+/// canceled, // return value about cancel button
+/// "Not really", // No label, is optional and if empty "No"
+/// "Ohhh yes"); // Yes label, also optional and if empty "Yes"
+/// fprintf(stderr, "You has called Yes/No, returned '%s' and was %s\n",
+/// ret ? "yes" : "no",
+/// canceled ? "canceled" : "not canceled");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetInput(const std::string& heading,
+ const std::string& text,
+ bool& canceled,
+ const std::string& noLabel = "",
+ const std::string& yesLabel = "")
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogYesNo->show_and_get_input_single_text(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), text.c_str(), &canceled,
+ noLabel.c_str(), yesLabel.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_YesNo
+/// @brief Use dialog to get numeric new password with separated line strings.
+///
+/// @param[in] heading Dialog heading
+/// @param[in] line0 Line #0 text
+/// @param[in] line1 Line #1 text
+/// @param[in] line2 Line #2 text
+/// @param[in] noLabel [opt] label to put on the no button
+/// @param[in] yesLabel [opt] label to put on the yes button
+/// @return Returns True if 'Yes' was pressed, else False
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/YesNo.h>
+///
+/// bool ret = kodi::gui::dialogs::YesNo::ShowAndGetInput(
+/// "Yes / No test call", // The Header
+/// "You has opened Yes / No dialog for test",
+/// "",
+/// "Is this OK for you?",
+/// "Not really", // No label, is optional and if empty "No"
+/// "Ohhh yes"); // Yes label, also optional and if empty "Yes"
+/// fprintf(stderr, "You has called Yes/No, returned '%s'\n",
+/// ret ? "yes" : "no");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetInput(const std::string& heading,
+ const std::string& line0,
+ const std::string& line1,
+ const std::string& line2,
+ const std::string& noLabel = "",
+ const std::string& yesLabel = "")
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogYesNo->show_and_get_input_line_text(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), line0.c_str(), line1.c_str(),
+ line2.c_str(), noLabel.c_str(), yesLabel.c_str());
+}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @ingroup cpp_kodi_gui_dialogs_YesNo
+/// @brief Use dialog to get numeric new password with separated line strings
+/// and cancel return field.
+///
+/// @param[in] heading Dialog heading
+/// @param[in] line0 Line #0 text
+/// @param[in] line1 Line #1 text
+/// @param[in] line2 Line #2 text
+/// @param[out] canceled Return value about cancel button
+/// @param[in] noLabel [opt] label to put on the no button
+/// @param[in] yesLabel [opt] label to put on the yes button
+/// @return Returns True if 'Yes' was pressed, else False
+///
+///
+///-------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/gui/dialogs/YesNo.h>
+///
+/// bool canceled = false;
+/// bool ret = kodi::gui::dialogs::YesNo::ShowAndGetInput(
+/// "Yes / No test call", // The Header
+/// "You has opened Yes / No dialog for test",
+/// "",
+/// "Is this OK for you?",
+/// canceled, // return value about cancel button
+/// "Not really", // No label, is optional and if empty "No"
+/// "Ohhh yes"); // Yes label, also optional and if empty "Yes"
+/// fprintf(stderr, "You has called Yes/No, returned '%s' and was %s\n",
+/// ret ? "yes" : "no",
+/// canceled ? "canceled" : "not canceled");
+/// ~~~~~~~~~~~~~
+///
+inline bool ATTR_DLL_LOCAL ShowAndGetInput(const std::string& heading,
+ const std::string& line0,
+ const std::string& line1,
+ const std::string& line2,
+ bool& canceled,
+ const std::string& noLabel = "",
+ const std::string& yesLabel = "")
+{
+ using namespace ::kodi::addon;
+ return CPrivateBase::m_interface->toKodi->kodi_gui->dialogYesNo
+ ->show_and_get_input_line_button_text(
+ CPrivateBase::m_interface->toKodi->kodiBase, heading.c_str(), line0.c_str(),
+ line1.c_str(), line2.c_str(), &canceled, noLabel.c_str(), yesLabel.c_str());
+}
+//------------------------------------------------------------------------------
+}; // namespace YesNo
+/// @}
+
+} /* namespace dialogs */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/CMakeLists.txt
new file mode 100644
index 0000000..36b823c
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/CMakeLists.txt
@@ -0,0 +1,12 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ GL.h
+ GLonDX.h
+ Shader.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_gui_gl)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/GL.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/GL.h
new file mode 100644
index 0000000..16d43e3
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/GL.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2005-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
+
+#ifdef __cplusplus
+
+//==============================================================================
+/// @defgroup cpp_kodi_gui_helpers_gl OpenGL helpers
+/// @ingroup cpp_kodi_gui_helpers
+/// @brief **Auxiliary functions for Open GL**\n
+/// This group includes help for definitions, functions, and classes for
+/// OpenGL.
+///
+/// To use OpenGL for your system, add the @ref GL.h "#include <kodi/gui/gl/GL.h>".
+///
+/// The @ref HAS_GL is declared if Open GL is required and @ref HAS_GLES if Open GL
+/// Embedded Systems (ES) is required, with ES the version is additionally given
+/// in the definition, this can be "2" or "3".
+///
+///
+///-----------------------------------------------------------------------------
+///
+/// Following @ref GL_TYPE_STRING define can be used, for example, to manage
+/// different folders for GL and GLES and make the selection easier.
+/// This are on OpenGL <b>"GL"</b> and on Open GL|ES <b>"GLES"</b>.
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~~~~~{.cpp}
+/// kodi::GetAddonPath("resources/shaders/" GL_TYPE_STRING "/frag.glsl");
+/// ~~~~~~~~~~~~~~~~~
+///
+///
+///----------------------------------------------------------------------------
+///
+/// In addition, @ref BUFFER_OFFSET is declared in it which can be used to give an
+/// offset on the array to GL.
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~~~~~{.cpp}
+/// const struct PackedVertex {
+/// float position[3]; // Position x, y, z
+/// float color[4]; // Color r, g, b, a
+/// } vertices[3] = {
+/// { { -0.5f, -0.5f, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
+/// { { 0.5f, -0.5f, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
+/// { { 0.0f, 0.5f, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }
+/// };
+///
+/// glVertexAttribPointer(m_aPosition, 3, GL_FLOAT, GL_FALSE, sizeof(PackedVertex), BUFFER_OFFSET(offsetof(PackedVertex, position)));
+/// glEnableVertexAttribArray(m_aPosition);
+///
+/// glVertexAttribPointer(m_aColor, 4, GL_FLOAT, GL_FALSE, sizeof(PackedVertex), BUFFER_OFFSET(offsetof(PackedVertex, color)));
+/// glEnableVertexAttribArray(m_aColor);
+/// ~~~~~~~~~~~~~~~~~
+
+#if HAS_GL
+#define GL_TYPE_STRING "GL"
+// always define GL_GLEXT_PROTOTYPES before include gl headers
+#if !defined(GL_GLEXT_PROTOTYPES)
+#define GL_GLEXT_PROTOTYPES
+#endif
+#if defined(TARGET_LINUX)
+#include <GL/gl.h>
+#include <GL/glext.h>
+#elif defined(TARGET_FREEBSD)
+#include <GL/gl.h>
+#elif defined(TARGET_DARWIN)
+#include <OpenGL/gl3.h>
+#include <OpenGL/gl3ext.h>
+#elif defined(WIN32)
+#error Use of GL under Windows is not possible
+#endif
+#elif HAS_GLES >= 2
+#define GL_TYPE_STRING "GLES"
+#if defined(WIN32)
+#if defined(HAS_ANGLE)
+#include <angle_gl.h>
+#else
+#error Use of GLES only be available under Windows by the use of angle
+#endif
+#elif defined(TARGET_DARWIN)
+#if HAS_GLES == 3
+#include <OpenGLES/ES3/gl.h>
+#include <OpenGLES/ES3/glext.h>
+#else
+#include <OpenGLES/ES2/gl.h>
+#include <OpenGLES/ES2/glext.h>
+#endif
+#else
+#if HAS_GLES == 3
+#include <GLES3/gl3.h>
+#include <GLES3/gl3ext.h>
+#else
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+#endif
+#endif
+#endif
+
+#ifndef BUFFER_OFFSET
+/// @ingroup cpp_kodi_gui_helpers_gl
+/// @brief To give a offset number as pointer value.
+#define BUFFER_OFFSET(i) ((char*)nullptr + (i))
+#endif
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/GLonDX.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/GLonDX.h
new file mode 100644
index 0000000..9ba76ea
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/GLonDX.h
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2005-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
+
+#ifdef __cplusplus
+
+#if defined(WIN32) && defined(HAS_ANGLE)
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+#include <angle_gl.h>
+#include <d3d11.h>
+#include <d3dcompiler.h>
+#include <kodi/AddonBase.h>
+#include <kodi/gui/General.h>
+#include <wrl/client.h>
+
+#pragma comment(lib, "d3dcompiler.lib")
+#ifndef GL_CLIENT_VERSION
+#define GL_CLIENT_VERSION 3
+#endif
+
+namespace kodi
+{
+namespace gui
+{
+namespace gl
+{
+
+class ATTR_DLL_LOCAL CGLonDX : public kodi::gui::IRenderHelper
+{
+public:
+ explicit CGLonDX() : m_pContext(reinterpret_cast<ID3D11DeviceContext*>(kodi::gui::GetHWContext()))
+ {
+ }
+ ~CGLonDX() override { destruct(); }
+
+ bool Init() override
+ {
+ EGLint egl_display_attrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE,
+ EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE,
+ EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE,
+ EGL_DONT_CARE,
+ EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE,
+ EGL_DONT_CARE,
+ EGL_EXPERIMENTAL_PRESENT_PATH_ANGLE,
+ EGL_EXPERIMENTAL_PRESENT_PATH_FAST_ANGLE,
+ EGL_NONE};
+ EGLint egl_config_attrs[] = {EGL_RED_SIZE,
+ 8,
+ EGL_GREEN_SIZE,
+ 8,
+ EGL_BLUE_SIZE,
+ 8,
+ EGL_ALPHA_SIZE,
+ 8,
+ EGL_BIND_TO_TEXTURE_RGBA,
+ EGL_TRUE,
+ EGL_RENDERABLE_TYPE,
+ GL_CLIENT_VERSION == 3 ? EGL_OPENGL_ES3_BIT : EGL_OPENGL_ES2_BIT,
+ EGL_SURFACE_TYPE,
+ EGL_PBUFFER_BIT,
+ EGL_NONE};
+ EGLint egl_context_attrs[] = {EGL_CONTEXT_CLIENT_VERSION, GL_CLIENT_VERSION, EGL_NONE};
+
+ m_eglDisplay =
+ eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, EGL_DEFAULT_DISPLAY, egl_display_attrs);
+ if (m_eglDisplay == EGL_NO_DISPLAY)
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to get EGL display (%s)", eglGetErrorString());
+ return false;
+ }
+
+ if (eglInitialize(m_eglDisplay, nullptr, nullptr) != EGL_TRUE)
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to init EGL display (%s)", eglGetErrorString());
+ return false;
+ }
+
+ EGLint numConfigs = 0;
+ if (eglChooseConfig(m_eglDisplay, egl_config_attrs, &m_eglConfig, 1, &numConfigs) != EGL_TRUE ||
+ numConfigs == 0)
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to get EGL config (%s)", eglGetErrorString());
+ return false;
+ }
+
+ m_eglContext = eglCreateContext(m_eglDisplay, m_eglConfig, nullptr, egl_context_attrs);
+ if (m_eglContext == EGL_NO_CONTEXT)
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to create EGL context (%s)", eglGetErrorString());
+ return false;
+ }
+
+ if (!createD3DResources())
+ return false;
+
+ if (eglMakeCurrent(m_eglDisplay, m_eglBuffer, m_eglBuffer, m_eglContext) != EGL_TRUE)
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to make current EGL (%s)", eglGetErrorString());
+ return false;
+ }
+ return true;
+ }
+
+ void CheckGL(ID3D11DeviceContext* device)
+ {
+ if (m_pContext != device)
+ {
+ m_pSRView = nullptr;
+ m_pVShader = nullptr;
+ m_pPShader = nullptr;
+ m_pContext = device;
+
+ if (m_eglBuffer != EGL_NO_SURFACE)
+ {
+ eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+ eglDestroySurface(m_eglDisplay, m_eglBuffer);
+ m_eglBuffer = EGL_NO_SURFACE;
+ }
+
+ // create new resources
+ if (!createD3DResources())
+ return;
+
+ eglMakeCurrent(m_eglDisplay, m_eglBuffer, m_eglBuffer, m_eglContext);
+ }
+ }
+
+ void Begin() override
+ {
+ // confirm on begin D3D context is correct
+ CheckGL(reinterpret_cast<ID3D11DeviceContext*>(kodi::gui::GetHWContext()));
+
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ }
+
+ void End() override
+ {
+ glFlush();
+
+ // set our primitive shaders
+ m_pContext->VSSetShader(m_pVShader.Get(), nullptr, 0);
+ m_pContext->PSSetShader(m_pPShader.Get(), nullptr, 0);
+ m_pContext->PSSetShaderResources(0, 1, m_pSRView.GetAddressOf());
+ // draw texture
+ m_pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
+ m_pContext->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr);
+ m_pContext->IASetInputLayout(nullptr);
+ m_pContext->Draw(4, 0);
+ // unset shaders
+ m_pContext->PSSetShader(nullptr, nullptr, 0);
+ m_pContext->VSSetShader(nullptr, nullptr, 0);
+ // unbind our view
+ ID3D11ShaderResourceView* views[1] = {};
+ m_pContext->PSSetShaderResources(0, 1, views);
+ }
+
+private:
+ enum ShaderType
+ {
+ VERTEX_SHADER,
+ PIXEL_SHADER
+ };
+
+ bool createD3DResources()
+ {
+ HANDLE sharedHandle;
+ Microsoft::WRL::ComPtr<ID3D11Device> pDevice;
+ Microsoft::WRL::ComPtr<ID3D11RenderTargetView> pRTView;
+ Microsoft::WRL::ComPtr<ID3D11Resource> pRTResource;
+ Microsoft::WRL::ComPtr<ID3D11Texture2D> pRTTexture;
+ Microsoft::WRL::ComPtr<ID3D11Texture2D> pOffScreenTexture;
+ Microsoft::WRL::ComPtr<IDXGIResource> dxgiResource;
+
+ m_pContext->GetDevice(&pDevice);
+ m_pContext->OMGetRenderTargets(1, &pRTView, nullptr);
+ if (!pRTView)
+ return false;
+
+ pRTView->GetResource(&pRTResource);
+ if (FAILED(pRTResource.As(&pRTTexture)))
+ return false;
+
+ D3D11_TEXTURE2D_DESC texDesc;
+ pRTTexture->GetDesc(&texDesc);
+ texDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+ texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
+ texDesc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
+ if (FAILED(pDevice->CreateTexture2D(&texDesc, nullptr, &pOffScreenTexture)))
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to create intermediate texture");
+ return false;
+ }
+
+ CD3D11_SHADER_RESOURCE_VIEW_DESC srvDesc(pOffScreenTexture.Get(),
+ D3D11_SRV_DIMENSION_TEXTURE2D);
+ if (FAILED(pDevice->CreateShaderResourceView(pOffScreenTexture.Get(), &srvDesc, &m_pSRView)))
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to create shader view");
+ return false;
+ }
+
+ if (FAILED(pOffScreenTexture.As(&dxgiResource)) ||
+ FAILED(dxgiResource->GetSharedHandle(&sharedHandle)))
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable get shared handle for texture");
+ return false;
+ }
+
+ // initiate simple shaders
+ if (FAILED(d3dCreateShader(VERTEX_SHADER, vs_out_shader_text, &m_pVShader)))
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to create vertex shader view");
+ return false;
+ }
+
+ if (FAILED(d3dCreateShader(PIXEL_SHADER, ps_out_shader_text, &m_pPShader)))
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to create pixel shader view");
+ return false;
+ }
+
+ // create EGL buffer from D3D shared texture
+ EGLint egl_buffer_attrs[] = {EGL_WIDTH,
+ static_cast<EGLint>(texDesc.Width),
+ EGL_HEIGHT,
+ static_cast<EGLint>(texDesc.Height),
+ EGL_TEXTURE_TARGET,
+ EGL_TEXTURE_2D,
+ EGL_TEXTURE_FORMAT,
+ EGL_TEXTURE_RGBA,
+ EGL_NONE};
+
+ m_eglBuffer =
+ eglCreatePbufferFromClientBuffer(m_eglDisplay, EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE,
+ sharedHandle, m_eglConfig, egl_buffer_attrs);
+
+ if (m_eglBuffer == EGL_NO_SURFACE)
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to create EGL buffer (%s)", eglGetErrorString());
+ return false;
+ }
+ return true;
+ }
+
+ HRESULT d3dCreateShader(ShaderType shaderType,
+ const std::string& source,
+ IUnknown** ppShader) const
+ {
+ Microsoft::WRL::ComPtr<ID3DBlob> pBlob;
+ Microsoft::WRL::ComPtr<ID3DBlob> pErrors;
+
+ auto hr = D3DCompile(source.c_str(), source.length(), nullptr, nullptr, nullptr, "main",
+ shaderType == PIXEL_SHADER ? "ps_4_0" : "vs_4_0", 0, 0, &pBlob, &pErrors);
+
+ if (SUCCEEDED(hr))
+ {
+ Microsoft::WRL::ComPtr<ID3D11Device> pDevice;
+ m_pContext->GetDevice(&pDevice);
+
+ if (shaderType == PIXEL_SHADER)
+ {
+ hr = pDevice->CreatePixelShader(pBlob->GetBufferPointer(), pBlob->GetBufferSize(), nullptr,
+ reinterpret_cast<ID3D11PixelShader**>(ppShader));
+ }
+ else
+ {
+ hr = pDevice->CreateVertexShader(pBlob->GetBufferPointer(), pBlob->GetBufferSize(), nullptr,
+ reinterpret_cast<ID3D11VertexShader**>(ppShader));
+ }
+
+ if (FAILED(hr))
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to create %s shader",
+ shaderType == PIXEL_SHADER ? "pixel" : "vertex");
+ }
+ }
+ else
+ {
+ Log(ADDON_LOG_ERROR, "GLonDX: unable to compile shader (%s)", pErrors->GetBufferPointer());
+ }
+ return hr;
+ }
+
+ static const char* eglGetErrorString()
+ {
+#define CASE_STR(value) \
+ case value: \
+ return #value
+ switch (eglGetError())
+ {
+ CASE_STR(EGL_SUCCESS);
+ CASE_STR(EGL_NOT_INITIALIZED);
+ CASE_STR(EGL_BAD_ACCESS);
+ CASE_STR(EGL_BAD_ALLOC);
+ CASE_STR(EGL_BAD_ATTRIBUTE);
+ CASE_STR(EGL_BAD_CONTEXT);
+ CASE_STR(EGL_BAD_CONFIG);
+ CASE_STR(EGL_BAD_CURRENT_SURFACE);
+ CASE_STR(EGL_BAD_DISPLAY);
+ CASE_STR(EGL_BAD_SURFACE);
+ CASE_STR(EGL_BAD_MATCH);
+ CASE_STR(EGL_BAD_PARAMETER);
+ CASE_STR(EGL_BAD_NATIVE_PIXMAP);
+ CASE_STR(EGL_BAD_NATIVE_WINDOW);
+ CASE_STR(EGL_CONTEXT_LOST);
+ default:
+ return "Unknown";
+ }
+#undef CASE_STR
+ }
+
+ void destruct()
+ {
+ if (m_eglDisplay != EGL_NO_DISPLAY)
+ {
+ eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+
+ if (m_eglBuffer != EGL_NO_SURFACE)
+ {
+ eglDestroySurface(m_eglDisplay, m_eglBuffer);
+ m_eglBuffer = EGL_NO_SURFACE;
+ }
+
+ if (m_eglContext != EGL_NO_CONTEXT)
+ {
+ eglDestroyContext(m_eglDisplay, m_eglContext);
+ m_eglContext = EGL_NO_CONTEXT;
+ }
+
+ eglTerminate(m_eglDisplay);
+ m_eglDisplay = EGL_NO_DISPLAY;
+ }
+
+ m_pSRView = nullptr;
+ m_pVShader = nullptr;
+ m_pPShader = nullptr;
+ m_pContext = nullptr;
+ }
+
+ EGLConfig m_eglConfig = EGL_NO_CONFIG_KHR;
+ EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
+ EGLContext m_eglContext = EGL_NO_CONTEXT;
+ EGLSurface m_eglBuffer = EGL_NO_SURFACE;
+
+ ID3D11DeviceContext* m_pContext = nullptr; // don't hold context
+ Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_pSRView = nullptr;
+ Microsoft::WRL::ComPtr<ID3D11VertexShader> m_pVShader = nullptr;
+ Microsoft::WRL::ComPtr<ID3D11PixelShader> m_pPShader = nullptr;
+
+#define TO_STRING(...) #__VA_ARGS__
+ std::string vs_out_shader_text = TO_STRING(void main(uint id
+ : SV_VertexId, out float2 tex
+ : TEXCOORD0, out float4 pos
+ : SV_POSITION) {
+ tex = float2(id % 2, (id % 4) >> 1);
+ pos = float4((tex.x - 0.5f) * 2, -(tex.y - 0.5f) * 2, 0, 1);
+ });
+
+ std::string ps_out_shader_text = TO_STRING(
+ Texture2D texMain : register(t0);
+ SamplerState Sampler
+ {
+ Filter = MIN_MAG_MIP_LINEAR;
+ AddressU = CLAMP;
+ AddressV = CLAMP;
+ Comparison = NEVER;
+ };
+
+ float4 main(in float2 tex : TEXCOORD0) : SV_TARGET
+ {
+ return texMain.Sample(Sampler, tex);
+ });
+#undef TO_STRING
+}; /* class CGLonDX */
+
+} /* namespace gl */
+
+using CRenderHelper = gl::CGLonDX;
+} /* namespace gui */
+} /* namespace kodi */
+
+#else /* defined(WIN32) && defined(HAS_ANGLE) */
+#pragma message ( "WARNING: GLonDX.h only be available on Windows by use of Angle as depend!" )
+#endif /* defined(WIN32) && defined(HAS_ANGLE) */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/Shader.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/Shader.h
new file mode 100644
index 0000000..ae78b7c
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/gl/Shader.h
@@ -0,0 +1,571 @@
+/*
+ * Copyright (C) 2005-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 "GL.h"
+
+#ifdef __cplusplus
+
+#include <stdio.h>
+#include <string>
+#include <vector>
+
+#include <kodi/AddonBase.h>
+#include <kodi/Filesystem.h>
+
+#define LOG_SIZE 1024
+#define GLchar char
+
+namespace kodi
+{
+namespace gui
+{
+namespace gl
+{
+
+//========================================================================
+/// CShader - base class
+class ATTR_DLL_LOCAL CShader
+{
+public:
+ CShader() = default;
+ virtual ~CShader() = default;
+ virtual bool Compile(const std::string& extraBegin = "", const std::string& extraEnd = "") = 0;
+ virtual void Free() = 0;
+ virtual GLuint Handle() = 0;
+
+ bool LoadSource(const std::string& file)
+ {
+ char buffer[16384];
+
+ kodi::vfs::CFile source;
+ if (!source.OpenFile(file))
+ {
+ kodi::Log(ADDON_LOG_ERROR, "CShader::%s: Failed to open file '%s'", __FUNCTION__,
+ file.c_str());
+ return false;
+ }
+ size_t len = source.Read(buffer, sizeof(buffer));
+ m_source.assign(buffer);
+ m_source[len] = 0;
+ source.Close();
+ return true;
+ }
+
+ bool OK() const { return m_compiled; }
+
+protected:
+ std::string m_source;
+ std::string m_lastLog;
+ bool m_compiled = false;
+};
+//------------------------------------------------------------------------
+
+//========================================================================
+/// CVertexShader
+class ATTR_DLL_LOCAL CVertexShader : public CShader
+{
+public:
+ CVertexShader() = default;
+ ~CVertexShader() override { Free(); }
+
+ void Free() override
+ {
+ if (m_vertexShader)
+ glDeleteShader(m_vertexShader);
+ m_vertexShader = 0;
+ }
+
+ bool Compile(const std::string& extraBegin = "", const std::string& extraEnd = "") override
+ {
+ GLint params[4];
+
+ Free();
+
+ m_vertexShader = glCreateShader(GL_VERTEX_SHADER);
+
+ GLsizei count = 0;
+ const char* sources[3];
+ if (!extraBegin.empty())
+ sources[count++] = extraBegin.c_str();
+ if (!m_source.empty())
+ sources[count++] = m_source.c_str();
+ if (!extraEnd.empty())
+ sources[count++] = extraEnd.c_str();
+
+ glShaderSource(m_vertexShader, count, sources, nullptr);
+ glCompileShader(m_vertexShader);
+ glGetShaderiv(m_vertexShader, GL_COMPILE_STATUS, params);
+ if (params[0] != GL_TRUE)
+ {
+ GLchar log[LOG_SIZE];
+ glGetShaderInfoLog(m_vertexShader, LOG_SIZE, nullptr, log);
+ kodi::Log(ADDON_LOG_ERROR, "CVertexShader::%s: %s", __FUNCTION__, log);
+ fprintf(stderr, "CVertexShader::%s: %s\n", __FUNCTION__, log);
+ m_lastLog = log;
+ m_compiled = false;
+ }
+ else
+ {
+ GLchar log[LOG_SIZE];
+ glGetShaderInfoLog(m_vertexShader, LOG_SIZE, nullptr, log);
+ m_lastLog = log;
+ m_compiled = true;
+ }
+ return m_compiled;
+ }
+
+ GLuint Handle() override { return m_vertexShader; }
+
+protected:
+ GLuint m_vertexShader = 0;
+};
+//------------------------------------------------------------------------
+
+//========================================================================
+/// CPixelShader
+class ATTR_DLL_LOCAL CPixelShader : public CShader
+{
+public:
+ CPixelShader() = default;
+ ~CPixelShader() { Free(); }
+ void Free() override
+ {
+ if (m_pixelShader)
+ glDeleteShader(m_pixelShader);
+ m_pixelShader = 0;
+ }
+
+ bool Compile(const std::string& extraBegin = "", const std::string& extraEnd = "") override
+ {
+ GLint params[4];
+
+ Free();
+
+ m_pixelShader = glCreateShader(GL_FRAGMENT_SHADER);
+
+ GLsizei count = 0;
+ const char* sources[3];
+ if (!extraBegin.empty())
+ sources[count++] = extraBegin.c_str();
+ if (!m_source.empty())
+ sources[count++] = m_source.c_str();
+ if (!extraEnd.empty())
+ sources[count++] = extraEnd.c_str();
+
+ glShaderSource(m_pixelShader, count, sources, 0);
+ glCompileShader(m_pixelShader);
+ glGetShaderiv(m_pixelShader, GL_COMPILE_STATUS, params);
+ if (params[0] != GL_TRUE)
+ {
+ GLchar log[LOG_SIZE];
+ glGetShaderInfoLog(m_pixelShader, LOG_SIZE, nullptr, log);
+ kodi::Log(ADDON_LOG_ERROR, "CPixelShader::%s: %s", __FUNCTION__, log);
+ fprintf(stderr, "CPixelShader::%s: %s\n", __FUNCTION__, log);
+ m_lastLog = log;
+ m_compiled = false;
+ }
+ else
+ {
+ GLchar log[LOG_SIZE];
+ glGetShaderInfoLog(m_pixelShader, LOG_SIZE, nullptr, log);
+ m_lastLog = log;
+ m_compiled = true;
+ }
+ return m_compiled;
+ }
+
+ GLuint Handle() override { return m_pixelShader; }
+
+protected:
+ GLuint m_pixelShader = 0;
+};
+//------------------------------------------------------------------------
+
+//============================================================================
+/// @defgroup cpp_kodi_gui_helpers_gl_CShaderProgram GL Shader Program
+/// @ingroup cpp_kodi_gui_helpers_gl
+/// @brief @cpp_class{ kodi::gui::gl::CShaderProgram }
+/// **Class to manage an OpenGL shader program**\n
+/// With this class the used GL shader code can be defined on the GPU and
+/// its variables can be managed between CPU and GPU.
+///
+/// It has the header @ref Shader.h "#include <kodi/gui/gl/Shader.h>"
+/// be included to enjoy it.
+///
+/// ----------------------------------------------------------------------------
+///
+/// <b>Example:</b>
+///
+/// ~~~~~~~~~~~~~{.cpp}
+///
+/// #include <kodi/gui/gl/Shader.h>
+/// ...
+///
+/// class ATTR_DLL_LOCAL CExample
+/// : ...,
+/// public kodi::gui::gl::CShaderProgram
+/// {
+/// public:
+/// CExample() = default;
+///
+/// bool Start();
+/// void Render();
+///
+/// // override functions for kodi::gui::gl::CShaderProgram
+/// void OnCompiledAndLinked() override;
+/// bool OnEnabled() override;
+///
+/// private:
+/// ...
+/// GLint m_aPosition = -1;
+/// GLint m_aColor = -1;
+/// };
+///
+/// bool CExample::Start()
+/// {
+/// // Define shaders and load
+/// std::string fraqShader = kodi::GetAddonPath("resources/shaders/" GL_TYPE_STRING "/glsl.frag");
+/// std::string vertShader = kodi::GetAddonPath("resources/shaders/" GL_TYPE_STRING "/glsl.vert");
+/// if (!LoadShaderFiles(vertShader, fraqShader) || !CompileAndLink())
+/// return false;
+///
+/// ...
+/// return true;
+/// }
+///
+/// ...
+///
+/// void CExample::Render()
+/// {
+/// ...
+///
+/// EnableShader();
+/// ...
+/// DO WORK
+/// ...
+/// DisableShader();
+/// }
+///
+/// void CExample::OnCompiledAndLinked()
+/// {
+/// ...
+/// DO YOUR WORK HERE FOR WHAT IS ONE TIME REQUIRED DURING COMPILE OF SHADER, E.G.:
+///
+/// m_aPosition = glGetAttribLocation(ProgramHandle(), "a_position");
+/// m_aColor = glGetAttribLocation(ProgramHandle(), "a_color");
+/// }
+///
+/// bool OnEnabled() override
+/// {
+/// ...
+/// DO YOUR WORK HERE FOR WHAT REQUIRED DURING ENABLE OF SHADER
+/// ...
+/// return true;
+/// }
+///
+/// ADDONCREATOR(CExample);
+/// ~~~~~~~~~~~~~
+///
+class ATTR_DLL_LOCAL CShaderProgram
+{
+public:
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief Construct a new shader.
+ ///
+ /// Load must be done later with @ref LoadShaderFiles.
+ ///
+ CShaderProgram() = default;
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief Construct a new shader and load defined shader files.
+ ///
+ /// @param[in] vert Path to used GL vertext shader
+ /// @param[in] frag Path to used GL fragment shader
+ ///
+ CShaderProgram(const std::string& vert, const std::string& frag) { LoadShaderFiles(vert, frag); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief Destructor.
+ ///
+ virtual ~CShaderProgram() { ShaderFree(); }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief To load manually the needed shader files.
+ ///
+ /// @param[in] vert Path to used GL vertext shader
+ /// @param[in] frag Path to used GL fragment shader
+ ///
+ ///
+ /// @note The use of the files is optional, but it must either be passed over
+ /// here or via @ref CompileAndLink, or both of the source code.
+ ///
+ bool LoadShaderFiles(const std::string& vert, const std::string& frag)
+ {
+ if (!kodi::vfs::FileExists(vert) || !m_pVP.LoadSource(vert))
+ {
+ kodi::Log(ADDON_LOG_ERROR, "%s: Failed to load '%s'", __func__, vert.c_str());
+ return false;
+ }
+
+ if (!kodi::vfs::FileExists(frag) || !m_pFP.LoadSource(frag))
+ {
+ kodi::Log(ADDON_LOG_ERROR, "%s: Failed to load '%s'", __func__, frag.c_str());
+ return false;
+ }
+
+ return true;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief To compile and link the shader to the GL interface.
+ ///
+ /// Optionally, additional source code can be transferred here, or it can be
+ /// used independently without any files
+ ///
+ /// @param[in] vertexExtraBegin [opt] To additionally add vextex source
+ /// code to the beginning of the loaded file
+ /// source code
+ /// @param[in] vertexExtraEnd [opt] To additionally add vextex source
+ /// code to the end of the loaded file
+ /// source code
+ /// @param[in] fragmentExtraBegin [opt] To additionally add fragment source
+ /// code to the beginning of the loaded file
+ /// source code
+ /// @param[in] fragmentExtraEnd [opt] To additionally add fragment source
+ /// code to the end of the loaded file
+ /// source code
+ /// @return true if compile was successed
+ ///
+ ///
+ /// @note In the case of a compile error, it will be written once into the Kodi
+ /// log and in addition to the console output to quickly detect the errors when
+ /// writing the damage.
+ ///
+ ///
+ bool CompileAndLink(const std::string& vertexExtraBegin = "",
+ const std::string& vertexExtraEnd = "",
+ const std::string& fragmentExtraBegin = "",
+ const std::string& fragmentExtraEnd = "")
+ {
+ GLint params[4];
+
+ // free resources
+ ShaderFree();
+ m_ok = false;
+
+ // compiled vertex shader
+ if (!m_pVP.Compile(vertexExtraBegin, vertexExtraEnd))
+ {
+ kodi::Log(ADDON_LOG_ERROR, "GL: Error compiling vertex shader");
+ return false;
+ }
+
+ // compile pixel shader
+ if (!m_pFP.Compile(fragmentExtraBegin, fragmentExtraEnd))
+ {
+ m_pVP.Free();
+ kodi::Log(ADDON_LOG_ERROR, "GL: Error compiling fragment shader");
+ return false;
+ }
+
+ // create program object
+ m_shaderProgram = glCreateProgram();
+ if (!m_shaderProgram)
+ {
+ kodi::Log(ADDON_LOG_ERROR, "CShaderProgram::%s: Failed to create GL program", __FUNCTION__);
+ ShaderFree();
+ return false;
+ }
+
+ // attach the vertex shader
+ glAttachShader(m_shaderProgram, m_pVP.Handle());
+ glAttachShader(m_shaderProgram, m_pFP.Handle());
+
+ // link the program
+ glLinkProgram(m_shaderProgram);
+ glGetProgramiv(m_shaderProgram, GL_LINK_STATUS, params);
+ if (params[0] != GL_TRUE)
+ {
+ GLchar log[LOG_SIZE];
+ glGetProgramInfoLog(m_shaderProgram, LOG_SIZE, nullptr, log);
+ kodi::Log(ADDON_LOG_ERROR, "CShaderProgram::%s: %s", __FUNCTION__, log);
+ fprintf(stderr, "CShaderProgram::%s: %s@n", __FUNCTION__, log);
+ ShaderFree();
+ return false;
+ }
+
+ m_validated = false;
+ m_ok = true;
+ OnCompiledAndLinked();
+ return true;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief To activate the shader and use it on the GPU.
+ ///
+ /// @return true if enable was successfully done
+ ///
+ ///
+ /// @note During this call, the @ref OnEnabled stored in the child is also
+ /// called
+ ///
+ bool EnableShader()
+ {
+ if (ShaderOK())
+ {
+ glUseProgram(m_shaderProgram);
+ if (OnEnabled())
+ {
+ if (!m_validated)
+ {
+ // validate the program
+ GLint params[4];
+ glValidateProgram(m_shaderProgram);
+ glGetProgramiv(m_shaderProgram, GL_VALIDATE_STATUS, params);
+ if (params[0] != GL_TRUE)
+ {
+ GLchar log[LOG_SIZE];
+ glGetProgramInfoLog(m_shaderProgram, LOG_SIZE, nullptr, log);
+ kodi::Log(ADDON_LOG_ERROR, "CShaderProgram::%s: %s", __FUNCTION__, log);
+ fprintf(stderr, "CShaderProgram::%s: %s\n", __FUNCTION__, log);
+ }
+ m_validated = true;
+ }
+ return true;
+ }
+ else
+ {
+ glUseProgram(0);
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief To deactivate the shader use on the GPU.
+ ///
+ void DisableShader()
+ {
+ if (ShaderOK())
+ {
+ glUseProgram(0);
+ OnDisabled();
+ }
+ }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief Used to check if shader has been loaded before.
+ ///
+ /// @return true if enable was successfully done
+ ///
+ /// @note The CompileAndLink call sets these values
+ ///
+ ATTR_FORCEINLINE bool ShaderOK() const { return m_ok; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief To get the vertex shader class used by Kodi at the addon.
+ ///
+ /// @return pointer to vertex shader class
+ ///
+ ATTR_FORCEINLINE CVertexShader& VertexShader() { return m_pVP; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief To get the fragment shader class used by Kodi at the addon.
+ ///
+ /// @return pointer to fragment shader class
+ ///
+ ATTR_FORCEINLINE CPixelShader& PixelShader() { return m_pFP; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief Used to get the definition created in the OpenGL itself.
+ ///
+ /// @return GLuint of GL shader program handler
+ ///
+ ATTR_FORCEINLINE GLuint ProgramHandle() { return m_shaderProgram; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @defgroup cpp_kodi_gui_helpers_gl_CShaderProgram_child Child Functions
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram
+ /// @brief @cpp_class{ kodi::gui::gl::CShaderProgram child functions }
+ ///
+ /// Functions that are added by parent in the child
+ /// @{
+ //==========================================================================
+ ///
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram_child
+ /// @brief Mandatory child function to set the necessary CPU to GPU data
+ ///
+ virtual void OnCompiledAndLinked() {}
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram_child
+ /// @brief Optional function to exchange data between CPU and GPU while
+ /// activating the shader
+ ///
+ /// @return true if enable was successfully done
+ ///
+ virtual bool OnEnabled() { return true; }
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_gui_helpers_gl_CShaderProgram_child
+ /// @brief Optional child function that may have to be performed when
+ /// switching off the shader
+ virtual void OnDisabled() {}
+ //--------------------------------------------------------------------------
+ /// @}
+
+private:
+ void ShaderFree()
+ {
+ if (m_shaderProgram)
+ glDeleteProgram(m_shaderProgram);
+ m_shaderProgram = 0;
+ m_ok = false;
+ }
+
+ CVertexShader m_pVP;
+ CPixelShader m_pFP;
+ GLuint m_shaderProgram = 0;
+ bool m_ok = false;
+ bool m_validated = false;
+};
+//------------------------------------------------------------------------
+
+} /* namespace gl */
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/input/ActionIDs.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/input/ActionIDs.h
new file mode 100644
index 0000000..4c816a4
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/input/ActionIDs.h
@@ -0,0 +1,11 @@
+/*
+ * Copyright (C) 2005-2020 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 "../../c-api/gui/input/action_ids.h"
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/input/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/gui/input/CMakeLists.txt
new file mode 100644
index 0000000..769cad2
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/input/CMakeLists.txt
@@ -0,0 +1,10 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ ActionIDs.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_gui_input)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/gui/renderHelper.h b/xbmc/addons/kodi-dev-kit/include/kodi/gui/renderHelper.h
new file mode 100644
index 0000000..f369205
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/gui/renderHelper.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2005-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 "../AddonBase.h"
+
+#ifdef __cplusplus
+
+namespace kodi
+{
+namespace gui
+{
+struct ATTR_DLL_LOCAL IRenderHelper
+{
+ virtual ~IRenderHelper() = default;
+ virtual bool Init() = 0;
+ virtual void Begin() = 0;
+ virtual void End() = 0;
+}; /* class IRenderHelper */
+} /* namespace gui */
+} /* namespace kodi */
+
+#if defined(WIN32) && defined(HAS_ANGLE)
+#include "gl/GLonDX.h"
+#else
+/*
+ * Default background GUI render helper class
+ */
+namespace kodi
+{
+namespace gui
+{
+struct ATTR_DLL_LOCAL CRenderHelperStub : public IRenderHelper
+{
+ bool Init() override { return true; }
+ void Begin() override {}
+ void End() override {}
+}; /* class CRenderHelperStub */
+
+using CRenderHelper = CRenderHelperStub;
+} /* namespace gui */
+} /* namespace kodi */
+#endif
+
+namespace kodi
+{
+namespace gui
+{
+
+/*
+ * Create render background handler, e.g. becomes on "Windows" Angle used
+ * to emulate GL.
+ *
+ * This only be used internal and not from addon's direct.
+ *
+ * Function defines here and not in CAddonBase because of a hen and egg problem.
+ */
+inline std::shared_ptr<IRenderHelper> ATTR_DLL_LOCAL GetRenderHelper()
+{
+ using namespace ::kodi::addon;
+ if (static_cast<CAddonBase*>(CPrivateBase::m_interface->addonBase)->m_renderHelper)
+ return static_cast<CAddonBase*>(CPrivateBase::m_interface->addonBase)->m_renderHelper;
+
+ std::shared_ptr<kodi::gui::IRenderHelper> renderHelper(new CRenderHelper());
+ if (!renderHelper->Init())
+ return nullptr;
+
+ static_cast<CAddonBase*>(CPrivateBase::m_interface->addonBase)->m_renderHelper =
+ renderHelper; // Hold on base for other types
+ return renderHelper;
+}
+
+} /* namespace gui */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/platform/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/platform/CMakeLists.txt
new file mode 100644
index 0000000..32a3910
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/platform/CMakeLists.txt
@@ -0,0 +1,9 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_platform)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/platform/android/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/platform/android/CMakeLists.txt
new file mode 100644
index 0000000..7ea3672
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/platform/android/CMakeLists.txt
@@ -0,0 +1,10 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ $<$<STREQUAL:${CORE_SYSTEM_NAME},android>:System.h>
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_platform_android)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/platform/android/System.h b/xbmc/addons/kodi-dev-kit/include/kodi/platform/android/System.h
new file mode 100644
index 0000000..b7eb32c
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/platform/android/System.h
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+
+/*---AUTO_GEN_PARSE<$$CORE_SYSTEM_NAME:android>---*/
+
+#pragma once
+
+#include "../../AddonBase.h"
+#include "../../c-api/platform/android/system.h"
+
+#ifdef __cplusplus
+namespace kodi
+{
+namespace platform
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_platform_CInterfaceAndroidSystem class CInterfaceAndroidSystem
+/// @ingroup cpp_kodi_platform
+/// @brief **Android platform specific functions**\n
+/// C++ class to query Android specific things in Kodi.
+///
+/// It has the header is @ref System.h "#include <kodi/platform/android/System.h>".
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/platform/android/System.h>
+///
+/// #if defined(ANDROID)
+/// kodi::platform::CInterfaceAndroidSystem system;
+/// if (system.GetSDKVersion() >= 23)
+/// {
+/// ...
+/// }
+/// #endif
+/// ~~~~~~~~~~~~~
+///
+class ATTR_DLL_LOCAL CInterfaceAndroidSystem
+{
+public:
+ CInterfaceAndroidSystem()
+ : m_interface(static_cast<AddonToKodiFuncTable_android_system*>(kodi::addon::GetInterface(
+ INTERFACE_ANDROID_SYSTEM_NAME, INTERFACE_ANDROID_SYSTEM_VERSION)))
+ {
+ }
+
+ //============================================================================
+ /// @ingroup cpp_kodi_platform_CInterfaceAndroidSystem
+ /// @brief Request an JNI env pointer for the calling thread.
+ ///
+ /// JNI env has to be controlled by kodi because of the underlying
+ /// threading concep.
+ ///
+ /// @return JNI env pointer for the calling thread
+ ///
+ inline void* GetJNIEnv()
+ {
+ if (m_interface)
+ return m_interface->get_jni_env();
+
+ return nullptr;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_platform_CInterfaceAndroidSystem
+ /// @brief Request the android sdk version to e.g. initialize <b>`JNIBase`</b>.
+ ///
+ /// @return Android SDK version
+ ///
+ inline int GetSDKVersion()
+ {
+ if (m_interface)
+ return m_interface->get_sdk_version();
+
+ return 0;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_platform_CInterfaceAndroidSystem
+ /// @brief Request the android main class name e.g. <b>`org.xbmc.kodi`</b>.
+ ///
+ /// @return package class name
+ ///
+ inline std::string GetClassName()
+ {
+ if (m_interface)
+ return m_interface->get_class_name();
+
+ return std::string();
+ }
+ //----------------------------------------------------------------------------
+
+private:
+ AddonToKodiFuncTable_android_system* m_interface;
+};
+//------------------------------------------------------------------------------
+
+} /* namespace platform */
+} /* namespace kodi */
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/tools/CMakeLists.txt b/xbmc/addons/kodi-dev-kit/include/kodi/tools/CMakeLists.txt
new file mode 100644
index 0000000..d81acab
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/tools/CMakeLists.txt
@@ -0,0 +1,14 @@
+# Auto generated CMakeLists.txt.
+# See xbmc/addons/kodi-dev-kit/tools/code-generator.py.
+
+set(HEADERS
+ DllHelper.h
+ EndTime.h
+ StringUtils.h
+ Thread.h
+ Timer.h
+)
+
+if(HEADERS)
+ core_add_devkit_header(kodi_tools)
+endif()
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/tools/DllHelper.h b/xbmc/addons/kodi-dev-kit/include/kodi/tools/DllHelper.h
new file mode 100644
index 0000000..2003171
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/tools/DllHelper.h
@@ -0,0 +1,211 @@
+/*
+ * 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
+
+#ifdef __cplusplus
+
+#include <string>
+
+#include <dlfcn.h>
+#include <kodi/AddonBase.h>
+#include <kodi/Filesystem.h>
+
+//==============================================================================
+/// @ingroup cpp_kodi_tools_CDllHelper
+/// @brief Macro to translate the given pointer value name of functions to
+/// requested function name.
+///
+/// @note This should always be used and does the work of
+/// @ref kodi::tools::CDllHelper::RegisterSymbol().
+///
+#define REGISTER_DLL_SYMBOL(functionPtr) \
+ kodi::tools::CDllHelper::RegisterSymbol(functionPtr, #functionPtr)
+//------------------------------------------------------------------------------
+
+namespace kodi
+{
+namespace tools
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_tools_CDllHelper class CDllHelper
+/// @ingroup cpp_kodi_tools
+/// @brief **Class to help with load of shared library functions**\n
+/// You can add them as parent to your class and to help with load of shared
+/// library functions.
+///
+/// @note To use on Windows must you also include [dlfcn-win32](https://github.com/dlfcn-win32/dlfcn-win32)
+/// on your addon!\n\n
+/// Furthermore, this allows the use of Android where the required library is
+/// copied to an EXE useable folder.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+///
+/// #include <kodi/tools/DllHelper.h>
+///
+/// ...
+/// class CMyInstance : public kodi::addon::CInstanceAudioDecoder,
+/// private kodi::tools::CDllHelper
+/// {
+/// public:
+/// CMyInstance(KODI_HANDLE instance, const std::string& kodiVersion);
+/// bool Start();
+///
+/// ...
+///
+/// // The pointers for on shared library exported functions
+/// int (*Init)();
+/// void (*Cleanup)();
+/// int (*GetLength)();
+/// };
+///
+/// CMyInstance::CMyInstance(KODI_HANDLE instance, const std::string& kodiVersion)
+/// : CInstanceAudioDecoder(instance, kodiVersion)
+/// {
+/// }
+///
+/// bool CMyInstance::Start()
+/// {
+/// std::string lib = kodi::GetAddonPath("myLib.so");
+/// if (!LoadDll(lib)) return false;
+/// if (!REGISTER_DLL_SYMBOL(Init)) return false;
+/// if (!REGISTER_DLL_SYMBOL(Cleanup)) return false;
+/// if (!REGISTER_DLL_SYMBOL(GetLength)) return false;
+///
+/// Init();
+/// return true;
+/// }
+/// ...
+/// ~~~~~~~~~~~~~
+///
+///@{
+class ATTR_DLL_LOCAL CDllHelper
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CDllHelper
+ /// @brief Class constructor.
+ ///
+ CDllHelper() = default;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CDllHelper
+ /// @brief Class destructor.
+ ///
+ virtual ~CDllHelper()
+ {
+ if (m_dll)
+ dlclose(m_dll);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CDllHelper
+ /// @brief Function to load requested library.
+ ///
+ /// @param[in] path The path with filename of shared library to load
+ /// @return true if load was successful done
+ ///
+ bool LoadDll(std::string path)
+ {
+#if defined(TARGET_ANDROID)
+ if (kodi::vfs::FileExists(path))
+ {
+ // Check already defined for "xbmcaltbinaddons", if yes no copy necassary.
+ std::string xbmcaltbinaddons =
+ kodi::vfs::TranslateSpecialProtocol("special://xbmcaltbinaddons/");
+ if (path.compare(0, xbmcaltbinaddons.length(), xbmcaltbinaddons) != 0)
+ {
+ bool doCopy = true;
+ std::string dstfile = xbmcaltbinaddons + kodi::vfs::GetFileName(path);
+
+ kodi::vfs::FileStatus dstFileStat;
+ if (kodi::vfs::StatFile(dstfile, dstFileStat))
+ {
+ kodi::vfs::FileStatus srcFileStat;
+ if (kodi::vfs::StatFile(path, srcFileStat))
+ {
+ if (dstFileStat.GetSize() == srcFileStat.GetSize() &&
+ dstFileStat.GetModificationTime() > srcFileStat.GetModificationTime())
+ doCopy = false;
+ }
+ }
+
+ if (doCopy)
+ {
+ kodi::Log(ADDON_LOG_DEBUG, "Caching '%s' to '%s'", path.c_str(), dstfile.c_str());
+ if (!kodi::vfs::CopyFile(path, dstfile))
+ {
+ kodi::Log(ADDON_LOG_ERROR, "Failed to cache '%s' to '%s'", path.c_str(),
+ dstfile.c_str());
+ return false;
+ }
+ }
+
+ path = dstfile;
+ }
+ }
+ else
+ {
+ return false;
+ }
+#endif
+
+ m_dll = dlopen(path.c_str(), RTLD_LOCAL | RTLD_LAZY);
+ if (m_dll == nullptr)
+ {
+ kodi::Log(ADDON_LOG_ERROR, "Unable to load %s", dlerror());
+ return false;
+ }
+ return true;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CDllHelper
+ /// @brief Function to register requested library symbol.
+ ///
+ /// @warning This function should not be used, use instead the macro
+ /// @ref REGISTER_DLL_SYMBOL to register the symbol pointer.
+ ///
+ ///
+ /// Use this always via Macro, e.g.:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// if (!REGISTER_DLL_SYMBOL(Init))
+ /// return false;
+ /// ~~~~~~~~~~~~~
+ ///
+ template <typename T>
+ bool RegisterSymbol(T& functionPtr, const char* strFunctionPtr)
+ {
+ functionPtr = reinterpret_cast<T>(dlsym(m_dll, strFunctionPtr));
+ if (functionPtr == nullptr)
+ {
+ kodi::Log(ADDON_LOG_ERROR, "Unable to assign function %s", dlerror());
+ return false;
+ }
+ return true;
+ }
+ //----------------------------------------------------------------------------
+
+private:
+ void* m_dll = nullptr;
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace tools */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/tools/EndTime.h b/xbmc/addons/kodi-dev-kit/include/kodi/tools/EndTime.h
new file mode 100644
index 0000000..729bf2b
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/tools/EndTime.h
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2005-2020 Team Kodi
+ * https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSE.md for more information.
+ */
+
+#pragma once
+
+#ifdef __cplusplus
+
+#include <chrono>
+
+namespace kodi
+{
+namespace tools
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_tools_CEndTime class CEndTime
+/// @ingroup cpp_kodi_tools
+/// @brief **Timeout check**\n
+/// Class which makes it easy to check if a specified amount of time has passed.
+///
+/// This code uses the support of platform-independent chrono system introduced
+/// with C++11.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/tools/EndTime.h>
+///
+/// class ATTR_DLL_LOCAL CExample
+/// {
+/// public:
+/// CExample()
+/// {
+/// TimerCall();
+/// }
+///
+/// void TimerCall()
+/// {
+/// fprintf(stderr, "Hello World\n");
+/// CEndTime timer(1000);
+///
+/// while (timer.MillisLeft())
+/// {
+/// if (timer.IsTimePast())
+/// {
+/// fprintf(stderr, "We timed out!\n");
+/// }
+/// std::this_thread::sleep_for(std::chrono::milliseconds(10));
+/// }
+/// }
+///
+/// };
+/// ~~~~~~~~~~~~~
+///
+///@{
+class CEndTime
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CEndTime
+ /// @brief Class constructor with no time to expiry set
+ ///
+ inline CEndTime() = default;
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CEndTime
+ /// @brief Class constructor to set future time when timer has expired
+ ///
+ /// @param[in] millisecondsIntoTheFuture the time in the future we cosider this timer as expired
+ ///
+ inline explicit CEndTime(unsigned int millisecondsIntoTheFuture)
+ : m_startTime(std::chrono::system_clock::now().time_since_epoch()),
+ m_totalWaitTime(std::chrono::milliseconds(millisecondsIntoTheFuture))
+ {
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CEndTime
+ /// @brief Set the time in the future we cosider this timer as expired
+ ///
+ /// @param[in] millisecondsIntoTheFuture the time in the future we cosider this timer as expired
+ ///
+ inline void Set(unsigned int millisecondsIntoTheFuture)
+ {
+ using namespace std::chrono;
+
+ m_startTime = system_clock::now().time_since_epoch();
+ m_totalWaitTime = milliseconds(millisecondsIntoTheFuture);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CEndTime
+ /// @brief Check if the expiry time has been reached
+ ///
+ /// @return True if the expiry amount of time has past, false otherwise
+ ///
+ inline bool IsTimePast() const
+ {
+ using namespace std::chrono;
+
+ // timer is infinite
+ if (m_totalWaitTime.count() == std::numeric_limits<unsigned int>::max())
+ return false;
+
+ if (m_totalWaitTime.count() == 0)
+ return true;
+ else
+ return (system_clock::now().time_since_epoch() - m_startTime) >= m_totalWaitTime;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CEndTime
+ /// @brief The amount of time left till this timer expires
+ ///
+ /// @return 0 if the expiry amount of time has past, the numbe rof milliseconds remaining otherwise
+ ///
+ inline unsigned int MillisLeft() const
+ {
+ using namespace std::chrono;
+
+ // timer is infinite
+ if (m_totalWaitTime.count() == std::numeric_limits<unsigned int>::max())
+ return std::numeric_limits<unsigned int>::max();
+
+ if (m_totalWaitTime.count() == 0)
+ return 0;
+
+ auto elapsed = system_clock::now().time_since_epoch() - m_startTime;
+
+ auto timeWaitedAlready = duration_cast<milliseconds>(elapsed).count();
+
+ if (timeWaitedAlready >= m_totalWaitTime.count())
+ return 0;
+
+ return static_cast<unsigned int>(m_totalWaitTime.count() - timeWaitedAlready);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CEndTime
+ /// @brief Consider this timer expired
+ ///
+ inline void SetExpired()
+ {
+ using namespace std::chrono;
+ m_totalWaitTime = milliseconds(0);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CEndTime
+ /// @brief Set this timer as never expiring
+ ///
+ inline void SetInfinite()
+ {
+ using namespace std::chrono;
+ m_totalWaitTime = milliseconds(std::numeric_limits<unsigned int>::max());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CEndTime
+ /// @brief Check if the timer has been set to infinite expiry
+ ///
+ /// @return True if the expiry has been set as infinite, false otherwise
+ ///
+ inline bool IsInfinite(void) const
+ {
+ return (m_totalWaitTime.count() == std::numeric_limits<unsigned int>::max());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CEndTime
+ /// @brief Get the initial timeout value this timer had
+ ///
+ /// @return The initial expiry amount of time this timer had in milliseconds
+ ///
+ inline unsigned int GetInitialTimeoutValue(void) const
+ {
+ auto value = std::chrono::duration_cast<std::chrono::milliseconds>(m_totalWaitTime);
+ return static_cast<unsigned int>(value.count());
+ }
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CEndTime
+ /// @brief Get the time this timer started
+ ///
+ /// @return The time this timer started in milliseconds since epoch
+ ///
+ inline uint64_t GetStartTime(void) const
+ {
+ auto value = std::chrono::duration_cast<std::chrono::milliseconds>(m_startTime);
+ return value.count();
+ }
+ //----------------------------------------------------------------------------
+
+private:
+ std::chrono::system_clock::duration m_startTime;
+ std::chrono::system_clock::duration m_totalWaitTime;
+};
+
+} /* namespace tools */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/tools/StringUtils.h b/xbmc/addons/kodi-dev-kit/include/kodi/tools/StringUtils.h
new file mode 100644
index 0000000..d1dede0
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/tools/StringUtils.h
@@ -0,0 +1,3086 @@
+/*
+ * Copyright (C) 2005-2020 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
+
+#ifdef __cplusplus
+
+#if !defined(NOMINMAX)
+#define NOMINMAX
+#endif
+
+#include <algorithm>
+#include <array>
+#include <cassert>
+#include <cinttypes>
+#include <cmath>
+#include <cstdarg>
+#include <cstring>
+#include <iomanip>
+#include <regex>
+#include <sstream>
+#include <string>
+#include <vector>
+
+// # of bytes for initial allocation for printf
+#define FORMAT_BLOCK_SIZE 512
+
+// macros for gcc, clang & others
+#ifndef PARAM1_PRINTF_FORMAT
+#ifdef __GNUC__
+// for use in functions that take printf format string as first parameter and additional printf parameters as second parameter
+// for example: int myprintf(const char* format, ...) PARAM1_PRINTF_FORMAT;
+#define PARAM1_PRINTF_FORMAT __attribute__((format(printf, 1, 2)))
+
+// for use in functions that take printf format string as second parameter and additional printf parameters as third parameter
+// for example: bool log_string(int logLevel, const char* format, ...) PARAM2_PRINTF_FORMAT;
+// note: all non-static class member functions take pointer to class object as hidden first parameter
+#define PARAM2_PRINTF_FORMAT __attribute__((format(printf, 2, 3)))
+
+// for use in functions that take printf format string as third parameter and additional printf parameters as fourth parameter
+// note: all non-static class member functions take pointer to class object as hidden first parameter
+// for example: class A { bool log_string(int logLevel, const char* functionName, const char* format, ...) PARAM3_PRINTF_FORMAT; };
+#define PARAM3_PRINTF_FORMAT __attribute__((format(printf, 3, 4)))
+
+// for use in functions that take printf format string as fourth parameter and additional printf parameters as fifth parameter
+// note: all non-static class member functions take pointer to class object as hidden first parameter
+// for example: class A { bool log_string(int logLevel, const char* functionName, int component, const char* format, ...) PARAM4_PRINTF_FORMAT; };
+#define PARAM4_PRINTF_FORMAT __attribute__((format(printf, 4, 5)))
+#else // ! __GNUC__
+#define PARAM1_PRINTF_FORMAT
+#define PARAM2_PRINTF_FORMAT
+#define PARAM3_PRINTF_FORMAT
+#define PARAM4_PRINTF_FORMAT
+#endif // ! __GNUC__
+#endif // PARAM1_PRINTF_FORMAT
+
+// macros for VC
+// VC check parameters only when "Code Analysis" is called
+#ifndef PRINTF_FORMAT_STRING
+#ifdef _MSC_VER
+#include <sal.h>
+
+// for use in any function that take printf format string and parameters
+// for example: bool log_string(int logLevel, PRINTF_FORMAT_STRING const char* format, ...);
+#define PRINTF_FORMAT_STRING _In_z_ _Printf_format_string_
+
+// specify that parameter must be zero-terminated string
+// for example: void SetName(IN_STRING const char* newName);
+#define IN_STRING _In_z_
+
+// specify that parameter must be zero-terminated string or NULL
+// for example: bool SetAdditionalName(IN_OPT_STRING const char* addName);
+#define IN_OPT_STRING _In_opt_z_
+#else // ! _MSC_VER
+#define PRINTF_FORMAT_STRING
+#define IN_STRING
+#define IN_OPT_STRING
+#endif // ! _MSC_VER
+#endif // PRINTF_FORMAT_STRING
+
+static constexpr wchar_t unicode_lowers[] = {
+ (wchar_t)0x0061, (wchar_t)0x0062, (wchar_t)0x0063, (wchar_t)0x0064, (wchar_t)0x0065,
+ (wchar_t)0x0066, (wchar_t)0x0067, (wchar_t)0x0068, (wchar_t)0x0069, (wchar_t)0x006A,
+ (wchar_t)0x006B, (wchar_t)0x006C, (wchar_t)0x006D, (wchar_t)0x006E, (wchar_t)0x006F,
+ (wchar_t)0x0070, (wchar_t)0x0071, (wchar_t)0x0072, (wchar_t)0x0073, (wchar_t)0x0074,
+ (wchar_t)0x0075, (wchar_t)0x0076, (wchar_t)0x0077, (wchar_t)0x0078, (wchar_t)0x0079,
+ (wchar_t)0x007A, (wchar_t)0x00E0, (wchar_t)0x00E1, (wchar_t)0x00E2, (wchar_t)0x00E3,
+ (wchar_t)0x00E4, (wchar_t)0x00E5, (wchar_t)0x00E6, (wchar_t)0x00E7, (wchar_t)0x00E8,
+ (wchar_t)0x00E9, (wchar_t)0x00EA, (wchar_t)0x00EB, (wchar_t)0x00EC, (wchar_t)0x00ED,
+ (wchar_t)0x00EE, (wchar_t)0x00EF, (wchar_t)0x00F0, (wchar_t)0x00F1, (wchar_t)0x00F2,
+ (wchar_t)0x00F3, (wchar_t)0x00F4, (wchar_t)0x00F5, (wchar_t)0x00F6, (wchar_t)0x00F8,
+ (wchar_t)0x00F9, (wchar_t)0x00FA, (wchar_t)0x00FB, (wchar_t)0x00FC, (wchar_t)0x00FD,
+ (wchar_t)0x00FE, (wchar_t)0x00FF, (wchar_t)0x0101, (wchar_t)0x0103, (wchar_t)0x0105,
+ (wchar_t)0x0107, (wchar_t)0x0109, (wchar_t)0x010B, (wchar_t)0x010D, (wchar_t)0x010F,
+ (wchar_t)0x0111, (wchar_t)0x0113, (wchar_t)0x0115, (wchar_t)0x0117, (wchar_t)0x0119,
+ (wchar_t)0x011B, (wchar_t)0x011D, (wchar_t)0x011F, (wchar_t)0x0121, (wchar_t)0x0123,
+ (wchar_t)0x0125, (wchar_t)0x0127, (wchar_t)0x0129, (wchar_t)0x012B, (wchar_t)0x012D,
+ (wchar_t)0x012F, (wchar_t)0x0131, (wchar_t)0x0133, (wchar_t)0x0135, (wchar_t)0x0137,
+ (wchar_t)0x013A, (wchar_t)0x013C, (wchar_t)0x013E, (wchar_t)0x0140, (wchar_t)0x0142,
+ (wchar_t)0x0144, (wchar_t)0x0146, (wchar_t)0x0148, (wchar_t)0x014B, (wchar_t)0x014D,
+ (wchar_t)0x014F, (wchar_t)0x0151, (wchar_t)0x0153, (wchar_t)0x0155, (wchar_t)0x0157,
+ (wchar_t)0x0159, (wchar_t)0x015B, (wchar_t)0x015D, (wchar_t)0x015F, (wchar_t)0x0161,
+ (wchar_t)0x0163, (wchar_t)0x0165, (wchar_t)0x0167, (wchar_t)0x0169, (wchar_t)0x016B,
+ (wchar_t)0x016D, (wchar_t)0x016F, (wchar_t)0x0171, (wchar_t)0x0173, (wchar_t)0x0175,
+ (wchar_t)0x0177, (wchar_t)0x017A, (wchar_t)0x017C, (wchar_t)0x017E, (wchar_t)0x0183,
+ (wchar_t)0x0185, (wchar_t)0x0188, (wchar_t)0x018C, (wchar_t)0x0192, (wchar_t)0x0199,
+ (wchar_t)0x01A1, (wchar_t)0x01A3, (wchar_t)0x01A5, (wchar_t)0x01A8, (wchar_t)0x01AD,
+ (wchar_t)0x01B0, (wchar_t)0x01B4, (wchar_t)0x01B6, (wchar_t)0x01B9, (wchar_t)0x01BD,
+ (wchar_t)0x01C6, (wchar_t)0x01C9, (wchar_t)0x01CC, (wchar_t)0x01CE, (wchar_t)0x01D0,
+ (wchar_t)0x01D2, (wchar_t)0x01D4, (wchar_t)0x01D6, (wchar_t)0x01D8, (wchar_t)0x01DA,
+ (wchar_t)0x01DC, (wchar_t)0x01DF, (wchar_t)0x01E1, (wchar_t)0x01E3, (wchar_t)0x01E5,
+ (wchar_t)0x01E7, (wchar_t)0x01E9, (wchar_t)0x01EB, (wchar_t)0x01ED, (wchar_t)0x01EF,
+ (wchar_t)0x01F3, (wchar_t)0x01F5, (wchar_t)0x01FB, (wchar_t)0x01FD, (wchar_t)0x01FF,
+ (wchar_t)0x0201, (wchar_t)0x0203, (wchar_t)0x0205, (wchar_t)0x0207, (wchar_t)0x0209,
+ (wchar_t)0x020B, (wchar_t)0x020D, (wchar_t)0x020F, (wchar_t)0x0211, (wchar_t)0x0213,
+ (wchar_t)0x0215, (wchar_t)0x0217, (wchar_t)0x0253, (wchar_t)0x0254, (wchar_t)0x0257,
+ (wchar_t)0x0258, (wchar_t)0x0259, (wchar_t)0x025B, (wchar_t)0x0260, (wchar_t)0x0263,
+ (wchar_t)0x0268, (wchar_t)0x0269, (wchar_t)0x026F, (wchar_t)0x0272, (wchar_t)0x0275,
+ (wchar_t)0x0283, (wchar_t)0x0288, (wchar_t)0x028A, (wchar_t)0x028B, (wchar_t)0x0292,
+ (wchar_t)0x03AC, (wchar_t)0x03AD, (wchar_t)0x03AE, (wchar_t)0x03AF, (wchar_t)0x03B1,
+ (wchar_t)0x03B2, (wchar_t)0x03B3, (wchar_t)0x03B4, (wchar_t)0x03B5, (wchar_t)0x03B6,
+ (wchar_t)0x03B7, (wchar_t)0x03B8, (wchar_t)0x03B9, (wchar_t)0x03BA, (wchar_t)0x03BB,
+ (wchar_t)0x03BC, (wchar_t)0x03BD, (wchar_t)0x03BE, (wchar_t)0x03BF, (wchar_t)0x03C0,
+ (wchar_t)0x03C1, (wchar_t)0x03C3, (wchar_t)0x03C4, (wchar_t)0x03C5, (wchar_t)0x03C6,
+ (wchar_t)0x03C7, (wchar_t)0x03C8, (wchar_t)0x03C9, (wchar_t)0x03CA, (wchar_t)0x03CB,
+ (wchar_t)0x03CC, (wchar_t)0x03CD, (wchar_t)0x03CE, (wchar_t)0x03E3, (wchar_t)0x03E5,
+ (wchar_t)0x03E7, (wchar_t)0x03E9, (wchar_t)0x03EB, (wchar_t)0x03ED, (wchar_t)0x03EF,
+ (wchar_t)0x0430, (wchar_t)0x0431, (wchar_t)0x0432, (wchar_t)0x0433, (wchar_t)0x0434,
+ (wchar_t)0x0435, (wchar_t)0x0436, (wchar_t)0x0437, (wchar_t)0x0438, (wchar_t)0x0439,
+ (wchar_t)0x043A, (wchar_t)0x043B, (wchar_t)0x043C, (wchar_t)0x043D, (wchar_t)0x043E,
+ (wchar_t)0x043F, (wchar_t)0x0440, (wchar_t)0x0441, (wchar_t)0x0442, (wchar_t)0x0443,
+ (wchar_t)0x0444, (wchar_t)0x0445, (wchar_t)0x0446, (wchar_t)0x0447, (wchar_t)0x0448,
+ (wchar_t)0x0449, (wchar_t)0x044A, (wchar_t)0x044B, (wchar_t)0x044C, (wchar_t)0x044D,
+ (wchar_t)0x044E, (wchar_t)0x044F, (wchar_t)0x0451, (wchar_t)0x0452, (wchar_t)0x0453,
+ (wchar_t)0x0454, (wchar_t)0x0455, (wchar_t)0x0456, (wchar_t)0x0457, (wchar_t)0x0458,
+ (wchar_t)0x0459, (wchar_t)0x045A, (wchar_t)0x045B, (wchar_t)0x045C, (wchar_t)0x045E,
+ (wchar_t)0x045F, (wchar_t)0x0461, (wchar_t)0x0463, (wchar_t)0x0465, (wchar_t)0x0467,
+ (wchar_t)0x0469, (wchar_t)0x046B, (wchar_t)0x046D, (wchar_t)0x046F, (wchar_t)0x0471,
+ (wchar_t)0x0473, (wchar_t)0x0475, (wchar_t)0x0477, (wchar_t)0x0479, (wchar_t)0x047B,
+ (wchar_t)0x047D, (wchar_t)0x047F, (wchar_t)0x0481, (wchar_t)0x0491, (wchar_t)0x0493,
+ (wchar_t)0x0495, (wchar_t)0x0497, (wchar_t)0x0499, (wchar_t)0x049B, (wchar_t)0x049D,
+ (wchar_t)0x049F, (wchar_t)0x04A1, (wchar_t)0x04A3, (wchar_t)0x04A5, (wchar_t)0x04A7,
+ (wchar_t)0x04A9, (wchar_t)0x04AB, (wchar_t)0x04AD, (wchar_t)0x04AF, (wchar_t)0x04B1,
+ (wchar_t)0x04B3, (wchar_t)0x04B5, (wchar_t)0x04B7, (wchar_t)0x04B9, (wchar_t)0x04BB,
+ (wchar_t)0x04BD, (wchar_t)0x04BF, (wchar_t)0x04C2, (wchar_t)0x04C4, (wchar_t)0x04C8,
+ (wchar_t)0x04CC, (wchar_t)0x04D1, (wchar_t)0x04D3, (wchar_t)0x04D5, (wchar_t)0x04D7,
+ (wchar_t)0x04D9, (wchar_t)0x04DB, (wchar_t)0x04DD, (wchar_t)0x04DF, (wchar_t)0x04E1,
+ (wchar_t)0x04E3, (wchar_t)0x04E5, (wchar_t)0x04E7, (wchar_t)0x04E9, (wchar_t)0x04EB,
+ (wchar_t)0x04EF, (wchar_t)0x04F1, (wchar_t)0x04F3, (wchar_t)0x04F5, (wchar_t)0x04F9,
+ (wchar_t)0x0561, (wchar_t)0x0562, (wchar_t)0x0563, (wchar_t)0x0564, (wchar_t)0x0565,
+ (wchar_t)0x0566, (wchar_t)0x0567, (wchar_t)0x0568, (wchar_t)0x0569, (wchar_t)0x056A,
+ (wchar_t)0x056B, (wchar_t)0x056C, (wchar_t)0x056D, (wchar_t)0x056E, (wchar_t)0x056F,
+ (wchar_t)0x0570, (wchar_t)0x0571, (wchar_t)0x0572, (wchar_t)0x0573, (wchar_t)0x0574,
+ (wchar_t)0x0575, (wchar_t)0x0576, (wchar_t)0x0577, (wchar_t)0x0578, (wchar_t)0x0579,
+ (wchar_t)0x057A, (wchar_t)0x057B, (wchar_t)0x057C, (wchar_t)0x057D, (wchar_t)0x057E,
+ (wchar_t)0x057F, (wchar_t)0x0580, (wchar_t)0x0581, (wchar_t)0x0582, (wchar_t)0x0583,
+ (wchar_t)0x0584, (wchar_t)0x0585, (wchar_t)0x0586, (wchar_t)0x10D0, (wchar_t)0x10D1,
+ (wchar_t)0x10D2, (wchar_t)0x10D3, (wchar_t)0x10D4, (wchar_t)0x10D5, (wchar_t)0x10D6,
+ (wchar_t)0x10D7, (wchar_t)0x10D8, (wchar_t)0x10D9, (wchar_t)0x10DA, (wchar_t)0x10DB,
+ (wchar_t)0x10DC, (wchar_t)0x10DD, (wchar_t)0x10DE, (wchar_t)0x10DF, (wchar_t)0x10E0,
+ (wchar_t)0x10E1, (wchar_t)0x10E2, (wchar_t)0x10E3, (wchar_t)0x10E4, (wchar_t)0x10E5,
+ (wchar_t)0x10E6, (wchar_t)0x10E7, (wchar_t)0x10E8, (wchar_t)0x10E9, (wchar_t)0x10EA,
+ (wchar_t)0x10EB, (wchar_t)0x10EC, (wchar_t)0x10ED, (wchar_t)0x10EE, (wchar_t)0x10EF,
+ (wchar_t)0x10F0, (wchar_t)0x10F1, (wchar_t)0x10F2, (wchar_t)0x10F3, (wchar_t)0x10F4,
+ (wchar_t)0x10F5, (wchar_t)0x1E01, (wchar_t)0x1E03, (wchar_t)0x1E05, (wchar_t)0x1E07,
+ (wchar_t)0x1E09, (wchar_t)0x1E0B, (wchar_t)0x1E0D, (wchar_t)0x1E0F, (wchar_t)0x1E11,
+ (wchar_t)0x1E13, (wchar_t)0x1E15, (wchar_t)0x1E17, (wchar_t)0x1E19, (wchar_t)0x1E1B,
+ (wchar_t)0x1E1D, (wchar_t)0x1E1F, (wchar_t)0x1E21, (wchar_t)0x1E23, (wchar_t)0x1E25,
+ (wchar_t)0x1E27, (wchar_t)0x1E29, (wchar_t)0x1E2B, (wchar_t)0x1E2D, (wchar_t)0x1E2F,
+ (wchar_t)0x1E31, (wchar_t)0x1E33, (wchar_t)0x1E35, (wchar_t)0x1E37, (wchar_t)0x1E39,
+ (wchar_t)0x1E3B, (wchar_t)0x1E3D, (wchar_t)0x1E3F, (wchar_t)0x1E41, (wchar_t)0x1E43,
+ (wchar_t)0x1E45, (wchar_t)0x1E47, (wchar_t)0x1E49, (wchar_t)0x1E4B, (wchar_t)0x1E4D,
+ (wchar_t)0x1E4F, (wchar_t)0x1E51, (wchar_t)0x1E53, (wchar_t)0x1E55, (wchar_t)0x1E57,
+ (wchar_t)0x1E59, (wchar_t)0x1E5B, (wchar_t)0x1E5D, (wchar_t)0x1E5F, (wchar_t)0x1E61,
+ (wchar_t)0x1E63, (wchar_t)0x1E65, (wchar_t)0x1E67, (wchar_t)0x1E69, (wchar_t)0x1E6B,
+ (wchar_t)0x1E6D, (wchar_t)0x1E6F, (wchar_t)0x1E71, (wchar_t)0x1E73, (wchar_t)0x1E75,
+ (wchar_t)0x1E77, (wchar_t)0x1E79, (wchar_t)0x1E7B, (wchar_t)0x1E7D, (wchar_t)0x1E7F,
+ (wchar_t)0x1E81, (wchar_t)0x1E83, (wchar_t)0x1E85, (wchar_t)0x1E87, (wchar_t)0x1E89,
+ (wchar_t)0x1E8B, (wchar_t)0x1E8D, (wchar_t)0x1E8F, (wchar_t)0x1E91, (wchar_t)0x1E93,
+ (wchar_t)0x1E95, (wchar_t)0x1EA1, (wchar_t)0x1EA3, (wchar_t)0x1EA5, (wchar_t)0x1EA7,
+ (wchar_t)0x1EA9, (wchar_t)0x1EAB, (wchar_t)0x1EAD, (wchar_t)0x1EAF, (wchar_t)0x1EB1,
+ (wchar_t)0x1EB3, (wchar_t)0x1EB5, (wchar_t)0x1EB7, (wchar_t)0x1EB9, (wchar_t)0x1EBB,
+ (wchar_t)0x1EBD, (wchar_t)0x1EBF, (wchar_t)0x1EC1, (wchar_t)0x1EC3, (wchar_t)0x1EC5,
+ (wchar_t)0x1EC7, (wchar_t)0x1EC9, (wchar_t)0x1ECB, (wchar_t)0x1ECD, (wchar_t)0x1ECF,
+ (wchar_t)0x1ED1, (wchar_t)0x1ED3, (wchar_t)0x1ED5, (wchar_t)0x1ED7, (wchar_t)0x1ED9,
+ (wchar_t)0x1EDB, (wchar_t)0x1EDD, (wchar_t)0x1EDF, (wchar_t)0x1EE1, (wchar_t)0x1EE3,
+ (wchar_t)0x1EE5, (wchar_t)0x1EE7, (wchar_t)0x1EE9, (wchar_t)0x1EEB, (wchar_t)0x1EED,
+ (wchar_t)0x1EEF, (wchar_t)0x1EF1, (wchar_t)0x1EF3, (wchar_t)0x1EF5, (wchar_t)0x1EF7,
+ (wchar_t)0x1EF9, (wchar_t)0x1F00, (wchar_t)0x1F01, (wchar_t)0x1F02, (wchar_t)0x1F03,
+ (wchar_t)0x1F04, (wchar_t)0x1F05, (wchar_t)0x1F06, (wchar_t)0x1F07, (wchar_t)0x1F10,
+ (wchar_t)0x1F11, (wchar_t)0x1F12, (wchar_t)0x1F13, (wchar_t)0x1F14, (wchar_t)0x1F15,
+ (wchar_t)0x1F20, (wchar_t)0x1F21, (wchar_t)0x1F22, (wchar_t)0x1F23, (wchar_t)0x1F24,
+ (wchar_t)0x1F25, (wchar_t)0x1F26, (wchar_t)0x1F27, (wchar_t)0x1F30, (wchar_t)0x1F31,
+ (wchar_t)0x1F32, (wchar_t)0x1F33, (wchar_t)0x1F34, (wchar_t)0x1F35, (wchar_t)0x1F36,
+ (wchar_t)0x1F37, (wchar_t)0x1F40, (wchar_t)0x1F41, (wchar_t)0x1F42, (wchar_t)0x1F43,
+ (wchar_t)0x1F44, (wchar_t)0x1F45, (wchar_t)0x1F51, (wchar_t)0x1F53, (wchar_t)0x1F55,
+ (wchar_t)0x1F57, (wchar_t)0x1F60, (wchar_t)0x1F61, (wchar_t)0x1F62, (wchar_t)0x1F63,
+ (wchar_t)0x1F64, (wchar_t)0x1F65, (wchar_t)0x1F66, (wchar_t)0x1F67, (wchar_t)0x1F80,
+ (wchar_t)0x1F81, (wchar_t)0x1F82, (wchar_t)0x1F83, (wchar_t)0x1F84, (wchar_t)0x1F85,
+ (wchar_t)0x1F86, (wchar_t)0x1F87, (wchar_t)0x1F90, (wchar_t)0x1F91, (wchar_t)0x1F92,
+ (wchar_t)0x1F93, (wchar_t)0x1F94, (wchar_t)0x1F95, (wchar_t)0x1F96, (wchar_t)0x1F97,
+ (wchar_t)0x1FA0, (wchar_t)0x1FA1, (wchar_t)0x1FA2, (wchar_t)0x1FA3, (wchar_t)0x1FA4,
+ (wchar_t)0x1FA5, (wchar_t)0x1FA6, (wchar_t)0x1FA7, (wchar_t)0x1FB0, (wchar_t)0x1FB1,
+ (wchar_t)0x1FD0, (wchar_t)0x1FD1, (wchar_t)0x1FE0, (wchar_t)0x1FE1, (wchar_t)0x24D0,
+ (wchar_t)0x24D1, (wchar_t)0x24D2, (wchar_t)0x24D3, (wchar_t)0x24D4, (wchar_t)0x24D5,
+ (wchar_t)0x24D6, (wchar_t)0x24D7, (wchar_t)0x24D8, (wchar_t)0x24D9, (wchar_t)0x24DA,
+ (wchar_t)0x24DB, (wchar_t)0x24DC, (wchar_t)0x24DD, (wchar_t)0x24DE, (wchar_t)0x24DF,
+ (wchar_t)0x24E0, (wchar_t)0x24E1, (wchar_t)0x24E2, (wchar_t)0x24E3, (wchar_t)0x24E4,
+ (wchar_t)0x24E5, (wchar_t)0x24E6, (wchar_t)0x24E7, (wchar_t)0x24E8, (wchar_t)0x24E9,
+ (wchar_t)0xFF41, (wchar_t)0xFF42, (wchar_t)0xFF43, (wchar_t)0xFF44, (wchar_t)0xFF45,
+ (wchar_t)0xFF46, (wchar_t)0xFF47, (wchar_t)0xFF48, (wchar_t)0xFF49, (wchar_t)0xFF4A,
+ (wchar_t)0xFF4B, (wchar_t)0xFF4C, (wchar_t)0xFF4D, (wchar_t)0xFF4E, (wchar_t)0xFF4F,
+ (wchar_t)0xFF50, (wchar_t)0xFF51, (wchar_t)0xFF52, (wchar_t)0xFF53, (wchar_t)0xFF54,
+ (wchar_t)0xFF55, (wchar_t)0xFF56, (wchar_t)0xFF57, (wchar_t)0xFF58, (wchar_t)0xFF59,
+ (wchar_t)0xFF5A};
+
+static const wchar_t unicode_uppers[] = {
+ (wchar_t)0x0041, (wchar_t)0x0042, (wchar_t)0x0043, (wchar_t)0x0044, (wchar_t)0x0045,
+ (wchar_t)0x0046, (wchar_t)0x0047, (wchar_t)0x0048, (wchar_t)0x0049, (wchar_t)0x004A,
+ (wchar_t)0x004B, (wchar_t)0x004C, (wchar_t)0x004D, (wchar_t)0x004E, (wchar_t)0x004F,
+ (wchar_t)0x0050, (wchar_t)0x0051, (wchar_t)0x0052, (wchar_t)0x0053, (wchar_t)0x0054,
+ (wchar_t)0x0055, (wchar_t)0x0056, (wchar_t)0x0057, (wchar_t)0x0058, (wchar_t)0x0059,
+ (wchar_t)0x005A, (wchar_t)0x00C0, (wchar_t)0x00C1, (wchar_t)0x00C2, (wchar_t)0x00C3,
+ (wchar_t)0x00C4, (wchar_t)0x00C5, (wchar_t)0x00C6, (wchar_t)0x00C7, (wchar_t)0x00C8,
+ (wchar_t)0x00C9, (wchar_t)0x00CA, (wchar_t)0x00CB, (wchar_t)0x00CC, (wchar_t)0x00CD,
+ (wchar_t)0x00CE, (wchar_t)0x00CF, (wchar_t)0x00D0, (wchar_t)0x00D1, (wchar_t)0x00D2,
+ (wchar_t)0x00D3, (wchar_t)0x00D4, (wchar_t)0x00D5, (wchar_t)0x00D6, (wchar_t)0x00D8,
+ (wchar_t)0x00D9, (wchar_t)0x00DA, (wchar_t)0x00DB, (wchar_t)0x00DC, (wchar_t)0x00DD,
+ (wchar_t)0x00DE, (wchar_t)0x0178, (wchar_t)0x0100, (wchar_t)0x0102, (wchar_t)0x0104,
+ (wchar_t)0x0106, (wchar_t)0x0108, (wchar_t)0x010A, (wchar_t)0x010C, (wchar_t)0x010E,
+ (wchar_t)0x0110, (wchar_t)0x0112, (wchar_t)0x0114, (wchar_t)0x0116, (wchar_t)0x0118,
+ (wchar_t)0x011A, (wchar_t)0x011C, (wchar_t)0x011E, (wchar_t)0x0120, (wchar_t)0x0122,
+ (wchar_t)0x0124, (wchar_t)0x0126, (wchar_t)0x0128, (wchar_t)0x012A, (wchar_t)0x012C,
+ (wchar_t)0x012E, (wchar_t)0x0049, (wchar_t)0x0132, (wchar_t)0x0134, (wchar_t)0x0136,
+ (wchar_t)0x0139, (wchar_t)0x013B, (wchar_t)0x013D, (wchar_t)0x013F, (wchar_t)0x0141,
+ (wchar_t)0x0143, (wchar_t)0x0145, (wchar_t)0x0147, (wchar_t)0x014A, (wchar_t)0x014C,
+ (wchar_t)0x014E, (wchar_t)0x0150, (wchar_t)0x0152, (wchar_t)0x0154, (wchar_t)0x0156,
+ (wchar_t)0x0158, (wchar_t)0x015A, (wchar_t)0x015C, (wchar_t)0x015E, (wchar_t)0x0160,
+ (wchar_t)0x0162, (wchar_t)0x0164, (wchar_t)0x0166, (wchar_t)0x0168, (wchar_t)0x016A,
+ (wchar_t)0x016C, (wchar_t)0x016E, (wchar_t)0x0170, (wchar_t)0x0172, (wchar_t)0x0174,
+ (wchar_t)0x0176, (wchar_t)0x0179, (wchar_t)0x017B, (wchar_t)0x017D, (wchar_t)0x0182,
+ (wchar_t)0x0184, (wchar_t)0x0187, (wchar_t)0x018B, (wchar_t)0x0191, (wchar_t)0x0198,
+ (wchar_t)0x01A0, (wchar_t)0x01A2, (wchar_t)0x01A4, (wchar_t)0x01A7, (wchar_t)0x01AC,
+ (wchar_t)0x01AF, (wchar_t)0x01B3, (wchar_t)0x01B5, (wchar_t)0x01B8, (wchar_t)0x01BC,
+ (wchar_t)0x01C4, (wchar_t)0x01C7, (wchar_t)0x01CA, (wchar_t)0x01CD, (wchar_t)0x01CF,
+ (wchar_t)0x01D1, (wchar_t)0x01D3, (wchar_t)0x01D5, (wchar_t)0x01D7, (wchar_t)0x01D9,
+ (wchar_t)0x01DB, (wchar_t)0x01DE, (wchar_t)0x01E0, (wchar_t)0x01E2, (wchar_t)0x01E4,
+ (wchar_t)0x01E6, (wchar_t)0x01E8, (wchar_t)0x01EA, (wchar_t)0x01EC, (wchar_t)0x01EE,
+ (wchar_t)0x01F1, (wchar_t)0x01F4, (wchar_t)0x01FA, (wchar_t)0x01FC, (wchar_t)0x01FE,
+ (wchar_t)0x0200, (wchar_t)0x0202, (wchar_t)0x0204, (wchar_t)0x0206, (wchar_t)0x0208,
+ (wchar_t)0x020A, (wchar_t)0x020C, (wchar_t)0x020E, (wchar_t)0x0210, (wchar_t)0x0212,
+ (wchar_t)0x0214, (wchar_t)0x0216, (wchar_t)0x0181, (wchar_t)0x0186, (wchar_t)0x018A,
+ (wchar_t)0x018E, (wchar_t)0x018F, (wchar_t)0x0190, (wchar_t)0x0193, (wchar_t)0x0194,
+ (wchar_t)0x0197, (wchar_t)0x0196, (wchar_t)0x019C, (wchar_t)0x019D, (wchar_t)0x019F,
+ (wchar_t)0x01A9, (wchar_t)0x01AE, (wchar_t)0x01B1, (wchar_t)0x01B2, (wchar_t)0x01B7,
+ (wchar_t)0x0386, (wchar_t)0x0388, (wchar_t)0x0389, (wchar_t)0x038A, (wchar_t)0x0391,
+ (wchar_t)0x0392, (wchar_t)0x0393, (wchar_t)0x0394, (wchar_t)0x0395, (wchar_t)0x0396,
+ (wchar_t)0x0397, (wchar_t)0x0398, (wchar_t)0x0399, (wchar_t)0x039A, (wchar_t)0x039B,
+ (wchar_t)0x039C, (wchar_t)0x039D, (wchar_t)0x039E, (wchar_t)0x039F, (wchar_t)0x03A0,
+ (wchar_t)0x03A1, (wchar_t)0x03A3, (wchar_t)0x03A4, (wchar_t)0x03A5, (wchar_t)0x03A6,
+ (wchar_t)0x03A7, (wchar_t)0x03A8, (wchar_t)0x03A9, (wchar_t)0x03AA, (wchar_t)0x03AB,
+ (wchar_t)0x038C, (wchar_t)0x038E, (wchar_t)0x038F, (wchar_t)0x03E2, (wchar_t)0x03E4,
+ (wchar_t)0x03E6, (wchar_t)0x03E8, (wchar_t)0x03EA, (wchar_t)0x03EC, (wchar_t)0x03EE,
+ (wchar_t)0x0410, (wchar_t)0x0411, (wchar_t)0x0412, (wchar_t)0x0413, (wchar_t)0x0414,
+ (wchar_t)0x0415, (wchar_t)0x0416, (wchar_t)0x0417, (wchar_t)0x0418, (wchar_t)0x0419,
+ (wchar_t)0x041A, (wchar_t)0x041B, (wchar_t)0x041C, (wchar_t)0x041D, (wchar_t)0x041E,
+ (wchar_t)0x041F, (wchar_t)0x0420, (wchar_t)0x0421, (wchar_t)0x0422, (wchar_t)0x0423,
+ (wchar_t)0x0424, (wchar_t)0x0425, (wchar_t)0x0426, (wchar_t)0x0427, (wchar_t)0x0428,
+ (wchar_t)0x0429, (wchar_t)0x042A, (wchar_t)0x042B, (wchar_t)0x042C, (wchar_t)0x042D,
+ (wchar_t)0x042E, (wchar_t)0x042F, (wchar_t)0x0401, (wchar_t)0x0402, (wchar_t)0x0403,
+ (wchar_t)0x0404, (wchar_t)0x0405, (wchar_t)0x0406, (wchar_t)0x0407, (wchar_t)0x0408,
+ (wchar_t)0x0409, (wchar_t)0x040A, (wchar_t)0x040B, (wchar_t)0x040C, (wchar_t)0x040E,
+ (wchar_t)0x040F, (wchar_t)0x0460, (wchar_t)0x0462, (wchar_t)0x0464, (wchar_t)0x0466,
+ (wchar_t)0x0468, (wchar_t)0x046A, (wchar_t)0x046C, (wchar_t)0x046E, (wchar_t)0x0470,
+ (wchar_t)0x0472, (wchar_t)0x0474, (wchar_t)0x0476, (wchar_t)0x0478, (wchar_t)0x047A,
+ (wchar_t)0x047C, (wchar_t)0x047E, (wchar_t)0x0480, (wchar_t)0x0490, (wchar_t)0x0492,
+ (wchar_t)0x0494, (wchar_t)0x0496, (wchar_t)0x0498, (wchar_t)0x049A, (wchar_t)0x049C,
+ (wchar_t)0x049E, (wchar_t)0x04A0, (wchar_t)0x04A2, (wchar_t)0x04A4, (wchar_t)0x04A6,
+ (wchar_t)0x04A8, (wchar_t)0x04AA, (wchar_t)0x04AC, (wchar_t)0x04AE, (wchar_t)0x04B0,
+ (wchar_t)0x04B2, (wchar_t)0x04B4, (wchar_t)0x04B6, (wchar_t)0x04B8, (wchar_t)0x04BA,
+ (wchar_t)0x04BC, (wchar_t)0x04BE, (wchar_t)0x04C1, (wchar_t)0x04C3, (wchar_t)0x04C7,
+ (wchar_t)0x04CB, (wchar_t)0x04D0, (wchar_t)0x04D2, (wchar_t)0x04D4, (wchar_t)0x04D6,
+ (wchar_t)0x04D8, (wchar_t)0x04DA, (wchar_t)0x04DC, (wchar_t)0x04DE, (wchar_t)0x04E0,
+ (wchar_t)0x04E2, (wchar_t)0x04E4, (wchar_t)0x04E6, (wchar_t)0x04E8, (wchar_t)0x04EA,
+ (wchar_t)0x04EE, (wchar_t)0x04F0, (wchar_t)0x04F2, (wchar_t)0x04F4, (wchar_t)0x04F8,
+ (wchar_t)0x0531, (wchar_t)0x0532, (wchar_t)0x0533, (wchar_t)0x0534, (wchar_t)0x0535,
+ (wchar_t)0x0536, (wchar_t)0x0537, (wchar_t)0x0538, (wchar_t)0x0539, (wchar_t)0x053A,
+ (wchar_t)0x053B, (wchar_t)0x053C, (wchar_t)0x053D, (wchar_t)0x053E, (wchar_t)0x053F,
+ (wchar_t)0x0540, (wchar_t)0x0541, (wchar_t)0x0542, (wchar_t)0x0543, (wchar_t)0x0544,
+ (wchar_t)0x0545, (wchar_t)0x0546, (wchar_t)0x0547, (wchar_t)0x0548, (wchar_t)0x0549,
+ (wchar_t)0x054A, (wchar_t)0x054B, (wchar_t)0x054C, (wchar_t)0x054D, (wchar_t)0x054E,
+ (wchar_t)0x054F, (wchar_t)0x0550, (wchar_t)0x0551, (wchar_t)0x0552, (wchar_t)0x0553,
+ (wchar_t)0x0554, (wchar_t)0x0555, (wchar_t)0x0556, (wchar_t)0x10A0, (wchar_t)0x10A1,
+ (wchar_t)0x10A2, (wchar_t)0x10A3, (wchar_t)0x10A4, (wchar_t)0x10A5, (wchar_t)0x10A6,
+ (wchar_t)0x10A7, (wchar_t)0x10A8, (wchar_t)0x10A9, (wchar_t)0x10AA, (wchar_t)0x10AB,
+ (wchar_t)0x10AC, (wchar_t)0x10AD, (wchar_t)0x10AE, (wchar_t)0x10AF, (wchar_t)0x10B0,
+ (wchar_t)0x10B1, (wchar_t)0x10B2, (wchar_t)0x10B3, (wchar_t)0x10B4, (wchar_t)0x10B5,
+ (wchar_t)0x10B6, (wchar_t)0x10B7, (wchar_t)0x10B8, (wchar_t)0x10B9, (wchar_t)0x10BA,
+ (wchar_t)0x10BB, (wchar_t)0x10BC, (wchar_t)0x10BD, (wchar_t)0x10BE, (wchar_t)0x10BF,
+ (wchar_t)0x10C0, (wchar_t)0x10C1, (wchar_t)0x10C2, (wchar_t)0x10C3, (wchar_t)0x10C4,
+ (wchar_t)0x10C5, (wchar_t)0x1E00, (wchar_t)0x1E02, (wchar_t)0x1E04, (wchar_t)0x1E06,
+ (wchar_t)0x1E08, (wchar_t)0x1E0A, (wchar_t)0x1E0C, (wchar_t)0x1E0E, (wchar_t)0x1E10,
+ (wchar_t)0x1E12, (wchar_t)0x1E14, (wchar_t)0x1E16, (wchar_t)0x1E18, (wchar_t)0x1E1A,
+ (wchar_t)0x1E1C, (wchar_t)0x1E1E, (wchar_t)0x1E20, (wchar_t)0x1E22, (wchar_t)0x1E24,
+ (wchar_t)0x1E26, (wchar_t)0x1E28, (wchar_t)0x1E2A, (wchar_t)0x1E2C, (wchar_t)0x1E2E,
+ (wchar_t)0x1E30, (wchar_t)0x1E32, (wchar_t)0x1E34, (wchar_t)0x1E36, (wchar_t)0x1E38,
+ (wchar_t)0x1E3A, (wchar_t)0x1E3C, (wchar_t)0x1E3E, (wchar_t)0x1E40, (wchar_t)0x1E42,
+ (wchar_t)0x1E44, (wchar_t)0x1E46, (wchar_t)0x1E48, (wchar_t)0x1E4A, (wchar_t)0x1E4C,
+ (wchar_t)0x1E4E, (wchar_t)0x1E50, (wchar_t)0x1E52, (wchar_t)0x1E54, (wchar_t)0x1E56,
+ (wchar_t)0x1E58, (wchar_t)0x1E5A, (wchar_t)0x1E5C, (wchar_t)0x1E5E, (wchar_t)0x1E60,
+ (wchar_t)0x1E62, (wchar_t)0x1E64, (wchar_t)0x1E66, (wchar_t)0x1E68, (wchar_t)0x1E6A,
+ (wchar_t)0x1E6C, (wchar_t)0x1E6E, (wchar_t)0x1E70, (wchar_t)0x1E72, (wchar_t)0x1E74,
+ (wchar_t)0x1E76, (wchar_t)0x1E78, (wchar_t)0x1E7A, (wchar_t)0x1E7C, (wchar_t)0x1E7E,
+ (wchar_t)0x1E80, (wchar_t)0x1E82, (wchar_t)0x1E84, (wchar_t)0x1E86, (wchar_t)0x1E88,
+ (wchar_t)0x1E8A, (wchar_t)0x1E8C, (wchar_t)0x1E8E, (wchar_t)0x1E90, (wchar_t)0x1E92,
+ (wchar_t)0x1E94, (wchar_t)0x1EA0, (wchar_t)0x1EA2, (wchar_t)0x1EA4, (wchar_t)0x1EA6,
+ (wchar_t)0x1EA8, (wchar_t)0x1EAA, (wchar_t)0x1EAC, (wchar_t)0x1EAE, (wchar_t)0x1EB0,
+ (wchar_t)0x1EB2, (wchar_t)0x1EB4, (wchar_t)0x1EB6, (wchar_t)0x1EB8, (wchar_t)0x1EBA,
+ (wchar_t)0x1EBC, (wchar_t)0x1EBE, (wchar_t)0x1EC0, (wchar_t)0x1EC2, (wchar_t)0x1EC4,
+ (wchar_t)0x1EC6, (wchar_t)0x1EC8, (wchar_t)0x1ECA, (wchar_t)0x1ECC, (wchar_t)0x1ECE,
+ (wchar_t)0x1ED0, (wchar_t)0x1ED2, (wchar_t)0x1ED4, (wchar_t)0x1ED6, (wchar_t)0x1ED8,
+ (wchar_t)0x1EDA, (wchar_t)0x1EDC, (wchar_t)0x1EDE, (wchar_t)0x1EE0, (wchar_t)0x1EE2,
+ (wchar_t)0x1EE4, (wchar_t)0x1EE6, (wchar_t)0x1EE8, (wchar_t)0x1EEA, (wchar_t)0x1EEC,
+ (wchar_t)0x1EEE, (wchar_t)0x1EF0, (wchar_t)0x1EF2, (wchar_t)0x1EF4, (wchar_t)0x1EF6,
+ (wchar_t)0x1EF8, (wchar_t)0x1F08, (wchar_t)0x1F09, (wchar_t)0x1F0A, (wchar_t)0x1F0B,
+ (wchar_t)0x1F0C, (wchar_t)0x1F0D, (wchar_t)0x1F0E, (wchar_t)0x1F0F, (wchar_t)0x1F18,
+ (wchar_t)0x1F19, (wchar_t)0x1F1A, (wchar_t)0x1F1B, (wchar_t)0x1F1C, (wchar_t)0x1F1D,
+ (wchar_t)0x1F28, (wchar_t)0x1F29, (wchar_t)0x1F2A, (wchar_t)0x1F2B, (wchar_t)0x1F2C,
+ (wchar_t)0x1F2D, (wchar_t)0x1F2E, (wchar_t)0x1F2F, (wchar_t)0x1F38, (wchar_t)0x1F39,
+ (wchar_t)0x1F3A, (wchar_t)0x1F3B, (wchar_t)0x1F3C, (wchar_t)0x1F3D, (wchar_t)0x1F3E,
+ (wchar_t)0x1F3F, (wchar_t)0x1F48, (wchar_t)0x1F49, (wchar_t)0x1F4A, (wchar_t)0x1F4B,
+ (wchar_t)0x1F4C, (wchar_t)0x1F4D, (wchar_t)0x1F59, (wchar_t)0x1F5B, (wchar_t)0x1F5D,
+ (wchar_t)0x1F5F, (wchar_t)0x1F68, (wchar_t)0x1F69, (wchar_t)0x1F6A, (wchar_t)0x1F6B,
+ (wchar_t)0x1F6C, (wchar_t)0x1F6D, (wchar_t)0x1F6E, (wchar_t)0x1F6F, (wchar_t)0x1F88,
+ (wchar_t)0x1F89, (wchar_t)0x1F8A, (wchar_t)0x1F8B, (wchar_t)0x1F8C, (wchar_t)0x1F8D,
+ (wchar_t)0x1F8E, (wchar_t)0x1F8F, (wchar_t)0x1F98, (wchar_t)0x1F99, (wchar_t)0x1F9A,
+ (wchar_t)0x1F9B, (wchar_t)0x1F9C, (wchar_t)0x1F9D, (wchar_t)0x1F9E, (wchar_t)0x1F9F,
+ (wchar_t)0x1FA8, (wchar_t)0x1FA9, (wchar_t)0x1FAA, (wchar_t)0x1FAB, (wchar_t)0x1FAC,
+ (wchar_t)0x1FAD, (wchar_t)0x1FAE, (wchar_t)0x1FAF, (wchar_t)0x1FB8, (wchar_t)0x1FB9,
+ (wchar_t)0x1FD8, (wchar_t)0x1FD9, (wchar_t)0x1FE8, (wchar_t)0x1FE9, (wchar_t)0x24B6,
+ (wchar_t)0x24B7, (wchar_t)0x24B8, (wchar_t)0x24B9, (wchar_t)0x24BA, (wchar_t)0x24BB,
+ (wchar_t)0x24BC, (wchar_t)0x24BD, (wchar_t)0x24BE, (wchar_t)0x24BF, (wchar_t)0x24C0,
+ (wchar_t)0x24C1, (wchar_t)0x24C2, (wchar_t)0x24C3, (wchar_t)0x24C4, (wchar_t)0x24C5,
+ (wchar_t)0x24C6, (wchar_t)0x24C7, (wchar_t)0x24C8, (wchar_t)0x24C9, (wchar_t)0x24CA,
+ (wchar_t)0x24CB, (wchar_t)0x24CC, (wchar_t)0x24CD, (wchar_t)0x24CE, (wchar_t)0x24CF,
+ (wchar_t)0xFF21, (wchar_t)0xFF22, (wchar_t)0xFF23, (wchar_t)0xFF24, (wchar_t)0xFF25,
+ (wchar_t)0xFF26, (wchar_t)0xFF27, (wchar_t)0xFF28, (wchar_t)0xFF29, (wchar_t)0xFF2A,
+ (wchar_t)0xFF2B, (wchar_t)0xFF2C, (wchar_t)0xFF2D, (wchar_t)0xFF2E, (wchar_t)0xFF2F,
+ (wchar_t)0xFF30, (wchar_t)0xFF31, (wchar_t)0xFF32, (wchar_t)0xFF33, (wchar_t)0xFF34,
+ (wchar_t)0xFF35, (wchar_t)0xFF36, (wchar_t)0xFF37, (wchar_t)0xFF38, (wchar_t)0xFF39,
+ (wchar_t)0xFF3A};
+
+namespace kodi
+{
+namespace tools
+{
+
+template<typename T, std::enable_if_t<!std::is_enum<T>::value, int> = 0>
+constexpr auto&& EnumToInt(T&& arg) noexcept
+{
+ return arg;
+}
+template<typename T, std::enable_if_t<std::is_enum<T>::value, int> = 0>
+constexpr auto EnumToInt(T&& arg) noexcept
+{
+ return static_cast<int>(arg);
+}
+
+//==============================================================================
+/// @defgroup cpp_kodi_tools_StringUtils_Defs Definitions, structures and enumerators
+/// @ingroup cpp_kodi_tools_StringUtils
+/// @brief **Parts used within string util functions**\n
+/// All to string functions associated data structures.
+///
+/// It is divided into individual modules that correspond to the respective
+/// types.
+///
+///
+///
+
+//==============================================================================
+/// @defgroup cpp_kodi_tools_StringUtils_Defs_TIME_FORMAT enum TIME_FORMAT
+/// @ingroup cpp_kodi_tools_StringUtils_Defs
+/// @brief TIME_FORMAT enum/bitmask used for formatting time strings.
+///
+/// Note the use of bitmasking, e.g. TIME_FORMAT_HH_MM_SS = TIME_FORMAT_HH | TIME_FORMAT_MM | TIME_FORMAT_SS
+/// @sa kodi::tools::StringUtils::SecondsToTimeString
+///
+/// @note For InfoLabels use the equivalent value listed (bold) on the
+/// description of each enum value.
+///
+/// <b>Example:</b> 3661 seconds => h=1, hh=01, m=1, mm=01, ss=01, hours=1, mins=61, secs=3661
+///
+///@{
+enum TIME_FORMAT
+{
+ /// Usually used as the fallback value if the format value is empty
+ TIME_FORMAT_GUESS = 0,
+
+ /// <b>ss</b> - seconds only
+ TIME_FORMAT_SS = 1,
+
+ /// <b>mm</b> - minutes only (2-digit)
+ TIME_FORMAT_MM = 2,
+
+ /// <b>mm:ss</b> - minutes and seconds
+ TIME_FORMAT_MM_SS = 3,
+
+ /// <b>hh</b> - hours only (2-digit)
+ TIME_FORMAT_HH = 4,
+
+ /// <b>hh:ss</b> - hours and seconds (this is not particularly useful)
+ TIME_FORMAT_HH_SS = 5,
+
+ /// <b>hh:mm</b> - hours and minutes
+ TIME_FORMAT_HH_MM = 6,
+
+ /// <b>hh:mm:ss</b> - hours, minutes and seconds
+ TIME_FORMAT_HH_MM_SS = 7,
+
+ /// <b>xx</b> - returns AM/PM for a 12-hour clock
+ TIME_FORMAT_XX = 8,
+
+ /// <b>hh:mm xx</b> - returns hours and minutes in a 12-hour clock format (AM/PM)
+ TIME_FORMAT_HH_MM_XX = 14,
+
+ /// <b>hh:mm:ss xx</b> - returns hours (2-digit), minutes and seconds in a 12-hour clock format (AM/PM)
+ TIME_FORMAT_HH_MM_SS_XX = 15,
+
+ /// <b>h</b> - hours only (1-digit)
+ TIME_FORMAT_H = 16,
+
+ /// <b>hh:mm:ss</b> - hours, minutes and seconds
+ TIME_FORMAT_H_MM_SS = 19,
+
+ /// <b>hh:mm:ss xx</b> - returns hours (1-digit), minutes and seconds in a 12-hour clock format (AM/PM)
+ TIME_FORMAT_H_MM_SS_XX = 27,
+
+ /// <b>secs</b> - total time in seconds
+ TIME_FORMAT_SECS = 32,
+
+ /// <b>mins</b> - total time in minutes
+ TIME_FORMAT_MINS = 64,
+
+ /// <b>hours</b> - total time in hours
+ TIME_FORMAT_HOURS = 128,
+
+ /// <b>m</b> - minutes only (1-digit)
+ TIME_FORMAT_M = 256
+};
+///@}
+//------------------------------------------------------------------------------
+
+//==============================================================================
+/// @defgroup cpp_kodi_tools_StringUtils class StringUtils
+/// @ingroup cpp_kodi_tools
+/// @brief **C++ class for processing strings**\n
+/// This class brings many different functions to edit, check or search texts.
+///
+/// Is intended to reduce any code work of C++ on addons and to have them faster
+/// to use.
+///
+/// All functions are static within the <b>`kodi::tools::StringUtils`</b> class.
+///
+///@{
+class StringUtils
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_StringUtils_Defs
+ /// @brief Defines a static empty <b>`std::string`</b>.
+ ///
+ static const std::string Empty;
+ //----------------------------------------------------------------------------
+
+ //----------------------------------------------------------------------------
+ /// @defgroup cpp_kodi_tools_StringUtils_FormatControl String format
+ /// @ingroup cpp_kodi_tools_StringUtils
+ /// @brief **Formatting functions**\n
+ /// Used to output the given values in newly formatted text using functions.
+ ///
+ /*!@{*/
+
+ //============================================================================
+ /// @brief Returns the C++ string pointed by given format. If format includes
+ /// format specifiers (subsequences beginning with %), the additional arguments
+ /// following format are formatted and inserted in the resulting string replacing
+ /// their respective specifiers.
+ ///
+ /// After the format parameter, the function expects at least as many additional
+ /// arguments as specified by format.
+ ///
+ /// @param[in] fmt The format of the text to process for output.
+ /// C string that contains the text to be written to the stream.
+ /// It can optionally contain embedded format specifiers that are
+ /// replaced by the values specified in subsequent additional
+ /// arguments and formatted as requested.
+ /// | specifier | Output | Example
+ /// |------------|----------------------------------------------------|------------
+ /// | d or i | Signed decimal integer | 392
+ /// | u | Unsigned decimal integer | 7235
+ /// | o | Unsigned octal | 610
+ /// | x | Unsigned hexadecimal integer | 7fa
+ /// | X | Unsigned hexadecimal integer (uppercase) | 7FA
+ /// | f | Decimal floating point, lowercase | 392.65
+ /// | F | Decimal floating point, uppercase | 392.65
+ /// | e | Scientific notation (mantissa/exponent), lowercase | 3.9265e+2
+ /// | E | Scientific notation (mantissa/exponent), uppercase | 3.9265E+2
+ /// | g | Use the shortest representation: %e or %f | 392.65
+ /// | G | Use the shortest representation: %E or %F | 392.65
+ /// | a | Hexadecimal floating point, lowercase | -0xc.90fep-2
+ /// | A | Hexadecimal floating point, uppercase | -0XC.90FEP-2
+ /// | c | Character | a
+ /// | s | String of characters | sample
+ /// | p | Pointer address | b8000000
+ /// | % | A % followed by another % character will write a single % to the stream. | %
+ /// The length sub-specifier modifies the length of the data type. This is a chart
+ /// showing the types used to interpret the corresponding arguments with and without
+ /// length specifier (if a different type is used, the proper type promotion or
+ /// conversion is performed, if allowed):
+ /// | length| d i | u o x X | f F e E g G a A | c | s | p | n |
+ /// |-------|---------------|-----------------------|-----------------|-------|---------|---------|-----------------|
+ /// | (none)| int | unsigned int | double | int | char* | void* | int* |
+ /// | hh | signed char | unsigned char | | | | | signed char* |
+ /// | h | short int | unsigned short int | | | | | short int* |
+ /// | l | long int | unsigned long int | | wint_t| wchar_t*| | long int* |
+ /// | ll | long long int | unsigned long long int| | | | | long long int* |
+ /// | j | intmax_t | uintmax_t | | | | | intmax_t* |
+ /// | z | size_t | size_t | | | | | size_t* |
+ /// | t | ptrdiff_t | ptrdiff_t | | | | | ptrdiff_t* |
+ /// | L | | | long double | | | | |
+ /// <b>Note:</b> that the c specifier takes an int (or wint_t) as argument, but performs the proper conversion to a char value
+ /// (or a wchar_t) before formatting it for output.
+ /// @param[in] ... <i>(additional arguments)</i>\n
+ /// Depending on the format string, the function may expect a
+ /// sequence of additional arguments, each containing a value
+ /// to be used to replace a format specifier in the format
+ /// string (or a pointer to a storage location, for n).\n
+ /// There should be at least as many of these arguments as the
+ /// number of values specified in the format specifiers.
+ /// Additional arguments are ignored by the function.
+ /// @return Formatted string
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string str = kodi::tools::StringUtils::Format("Hello {} {}", "World", 2020);
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::string Format(const char* fmt, ...)
+ {
+ va_list args;
+ va_start(args, fmt);
+ std::string str = FormatV(fmt, args);
+ va_end(args);
+
+ return str;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Returns the C++ wide string pointed by given format.
+ ///
+ /// @param[in] fmt The format of the text to process for output
+ /// (see @ref Format(const char* fmt, ...) for details).
+ /// @param[in] ... <i>(additional arguments)</i>\n
+ /// Depending on the format string, the function may expect a
+ /// sequence of additional arguments, each containing a value
+ /// to be used to replace a format specifier in the format
+ /// string (or a pointer to a storage location, for n).\n
+ /// There should be at least as many of these arguments as the
+ /// number of values specified in the format specifiers.
+ /// Additional arguments are ignored by the function.
+ /// @return Formatted string
+ ///
+ inline static std::wstring Format(const wchar_t* fmt, ...)
+ {
+ va_list args;
+ va_start(args, fmt);
+ std::wstring str = FormatV(fmt, args);
+ va_end(args);
+
+ return str;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Returns the C++ string pointed by given format list.
+ ///
+ /// @param[in] fmt The format of the text to process for output
+ /// (see @ref Format(const char* fmt, ...) for details).
+ /// @param[in] args A value identifying a variable arguments list initialized
+ /// with `va_start`.
+ /// @return Formatted string
+ ///
+ inline static std::string FormatV(PRINTF_FORMAT_STRING const char* fmt, va_list args)
+ {
+ if (!fmt || !fmt[0])
+ return "";
+
+ int size = FORMAT_BLOCK_SIZE;
+ va_list argCopy;
+
+ while (true)
+ {
+ char* cstr = reinterpret_cast<char*>(malloc(sizeof(char) * size));
+ if (!cstr)
+ return "";
+
+ va_copy(argCopy, args);
+ int nActual = vsnprintf(cstr, size, fmt, argCopy);
+ va_end(argCopy);
+
+ if (nActual > -1 && nActual < size) // We got a valid result
+ {
+ std::string str(cstr, nActual);
+ free(cstr);
+ return str;
+ }
+ free(cstr);
+#ifndef TARGET_WINDOWS
+ if (nActual > -1) // Exactly what we will need (glibc 2.1)
+ size = nActual + 1;
+ else // Let's try to double the size (glibc 2.0)
+ size *= 2;
+#else // TARGET_WINDOWS
+ va_copy(argCopy, args);
+ size = _vscprintf(fmt, argCopy);
+ va_end(argCopy);
+ if (size < 0)
+ return "";
+ else
+ size++; // increment for null-termination
+#endif // TARGET_WINDOWS
+ }
+
+ return ""; // unreachable
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Returns the C++ wide string pointed by given format list.
+ ///
+ /// @param[in] fmt The format of the text to process for output
+ /// (see @ref Format(const char* fmt, ...) for details).
+ /// @param[in] args A value identifying a variable arguments list initialized
+ /// with `va_start`.
+ /// @return Formatted string
+ ///
+ inline static std::wstring FormatV(PRINTF_FORMAT_STRING const wchar_t* fmt, va_list args)
+ {
+ if (!fmt || !fmt[0])
+ return L"";
+
+ int size = FORMAT_BLOCK_SIZE;
+ va_list argCopy;
+
+ while (true)
+ {
+ wchar_t* cstr = reinterpret_cast<wchar_t*>(malloc(sizeof(wchar_t) * size));
+ if (!cstr)
+ return L"";
+
+ va_copy(argCopy, args);
+ int nActual = vswprintf(cstr, size, fmt, argCopy);
+ va_end(argCopy);
+
+ if (nActual > -1 && nActual < size) // We got a valid result
+ {
+ std::wstring str(cstr, nActual);
+ free(cstr);
+ return str;
+ }
+ free(cstr);
+
+#ifndef TARGET_WINDOWS
+ if (nActual > -1) // Exactly what we will need (glibc 2.1)
+ size = nActual + 1;
+ else // Let's try to double the size (glibc 2.0)
+ size *= 2;
+#else // TARGET_WINDOWS
+ va_copy(argCopy, args);
+ size = _vscwprintf(fmt, argCopy);
+ va_end(argCopy);
+ if (size < 0)
+ return L"";
+ else
+ size++; // increment for null-termination
+#endif // TARGET_WINDOWS
+ }
+
+ return L"";
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Returns bytes in a human readable format using the smallest unit
+ /// that will fit `bytes` in at most three digits. The number of decimals are
+ /// adjusted with significance such that 'small' numbers will have more
+ /// decimals than larger ones.
+ ///
+ /// For example: 1024 bytes will be formatted as "1.00kB", 10240 bytes as
+ /// "10.0kB" and 102400 bytes as "100kB". See TestStringUtils for more
+ /// examples.
+ ///
+ /// Supported file sizes:
+ /// | Value | Short | Metric
+ /// |------------|-------|-----------
+ /// | 1 | B | byte
+ /// | 1024¹ | kB | kilobyte
+ /// | 1024² | MB | megabyte
+ /// | 1024³ | GB | gigabyte
+ /// | 1024 exp 4 | TB | terabyte
+ /// | 1024 exp 5 | PB | petabyte
+ /// | 1024 exp 6 | EB | exabyte
+ /// | 1024 exp 7 | ZB | zettabyte
+ /// | 1024 exp 8 | YB | yottabyte
+ ///
+ /// @param[in] bytes Bytes amount to return as human readable string
+ /// @return Size as string
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// EXPECT_STREQ("0B", kodi::tools::StringUtils::FormatFileSize(0).c_str());
+ ///
+ /// EXPECT_STREQ("999B", kodi::tools::StringUtils::FormatFileSize(999).c_str());
+ /// EXPECT_STREQ("0.98kB", kodi::tools::StringUtils::FormatFileSize(1000).c_str());
+ ///
+ /// EXPECT_STREQ("1.00kB", kodi::tools::StringUtils::FormatFileSize(1024).c_str());
+ /// EXPECT_STREQ("9.99kB", kodi::tools::StringUtils::FormatFileSize(10229).c_str());
+ ///
+ /// EXPECT_STREQ("10.1kB", kodi::tools::StringUtils::FormatFileSize(10387).c_str());
+ /// EXPECT_STREQ("99.9kB", kodi::tools::StringUtils::FormatFileSize(102297).c_str());
+ ///
+ /// EXPECT_STREQ("100kB", kodi::tools::StringUtils::FormatFileSize(102400).c_str());
+ /// EXPECT_STREQ("999kB", kodi::tools::StringUtils::FormatFileSize(1023431).c_str());
+ ///
+ /// EXPECT_STREQ("0.98MB", kodi::tools::StringUtils::FormatFileSize(1023897).c_str());
+ /// EXPECT_STREQ("0.98MB", kodi::tools::StringUtils::FormatFileSize(1024000).c_str());
+ ///
+ /// EXPECT_STREQ("5.30EB", kodi::tools::StringUtils::FormatFileSize(6115888293969133568).c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::string FormatFileSize(uint64_t bytes)
+ {
+ const std::array<std::string, 9> units{{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}};
+ if (bytes < 1000)
+ return Format("%" PRIu64 "B", bytes);
+
+ size_t i = 0;
+ double value = static_cast<double>(bytes);
+ while (i + 1 < units.size() && value >= 999.5)
+ {
+ ++i;
+ value /= 1024.0;
+ }
+ unsigned int decimals = value < 9.995 ? 2 : (value < 99.95 ? 1 : 0);
+ auto frmt = "%." + Format("%u", decimals) + "f%s";
+ return Format(frmt.c_str(), value, units[i].c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Convert the string of binary chars to the actual string.
+ ///
+ /// Convert the string representation of binary chars to the actual string.
+ /// For example <b>`\1\2\3`</b> is converted to a string with binary char
+ /// <b>`\1`</b>, <b>`\2`</b> and <b>`\3`</b>
+ ///
+ /// @param[in] in String to convert
+ /// @return Converted string
+ ///
+ inline static std::string BinaryStringToString(const std::string& in)
+ {
+ std::string out;
+ out.reserve(in.size() / 2);
+ for (const char *cur = in.c_str(), *end = cur + in.size(); cur != end; ++cur)
+ {
+ if (*cur == '\\')
+ {
+ ++cur;
+ if (cur == end)
+ {
+ break;
+ }
+ if (isdigit(*cur))
+ {
+ char* end;
+ unsigned long num = strtol(cur, &end, 10);
+ cur = end - 1;
+ out.push_back(static_cast<char>(num));
+ continue;
+ }
+ }
+ out.push_back(*cur);
+ }
+ return out;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Convert each character in the string to its hexadecimal
+ /// representation and return the concatenated result
+ ///
+ /// Example: "abc\n" -> "6162630a"
+ ///
+ /// @param[in] in String to convert
+ /// @return Converted string
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// EXPECT_STREQ("", kodi::tools::StringUtils::ToHexadecimal("").c_str());
+ /// EXPECT_STREQ("616263", kodi::tools::StringUtils::ToHexadecimal("abc").c_str());
+ /// std::string a{"a\0b\n", 4};
+ /// EXPECT_STREQ("6100620a", kodi::tools::StringUtils::ToHexadecimal(a).c_str());
+ /// std::string nul{"\0", 1};
+ /// EXPECT_STREQ("00", kodi::tools::StringUtils::ToHexadecimal(nul).c_str());
+ /// std::string ff{"\xFF", 1};
+ /// EXPECT_STREQ("ff", kodi::tools::StringUtils::ToHexadecimal(ff).c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::string ToHexadecimal(const std::string& in)
+ {
+ std::ostringstream ss;
+ ss << std::hex;
+ for (unsigned char ch : in)
+ {
+ ss << std::setw(2) << std::setfill('0') << static_cast<unsigned long>(ch);
+ }
+ return ss.str();
+ }
+ //----------------------------------------------------------------------------
+
+ /*!@}*/
+
+ //----------------------------------------------------------------------------
+ /// @defgroup cpp_kodi_tools_StringUtils_EditControl String edit
+ /// @ingroup cpp_kodi_tools_StringUtils
+ /// @brief **Edits given texts**\n
+ /// This is used to revise the respective strings and to get them in the desired format.
+ ///
+ /*!@{*/
+
+ //============================================================================
+ /// @brief Convert a string to uppercase.
+ ///
+ /// @param[in,out] str String to convert
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr = "TEST";
+ ///
+ /// std::string varstr = "TeSt";
+ /// kodi::tools::StringUtils::ToUpper(varstr);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static void ToUpper(std::string& str)
+ {
+ std::transform(str.begin(), str.end(), str.begin(), ::toupper);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Convert a 16bit wide string to uppercase.
+ ///
+ /// @param[in,out] str String to convert
+ ///
+ inline static void ToUpper(std::wstring& str)
+ {
+ transform(str.begin(), str.end(), str.begin(), toupperUnicode);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Convert a string to lowercase.
+ ///
+ /// @param[in,out] str String to convert
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr = "test";
+ ///
+ /// std::string varstr = "TeSt";
+ /// kodi::tools::StringUtils::ToLower(varstr);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static void ToLower(std::string& str)
+ {
+ transform(str.begin(), str.end(), str.begin(), ::tolower);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Convert a 16bit wide string to lowercase.
+ ///
+ /// @param[in,out] str String to convert
+ ///
+ inline static void ToLower(std::wstring& str)
+ {
+ transform(str.begin(), str.end(), str.begin(), tolowerUnicode);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Combine all numerical digits and give it as integer value.
+ ///
+ /// @param[in,out] str String to check for digits
+ /// @return All numerical digits fit together as integer value
+ ///
+ inline static int ReturnDigits(const std::string& str)
+ {
+ std::stringstream ss;
+ for (const auto& character : str)
+ {
+ if (isdigit(character))
+ ss << character;
+ }
+ return atoi(ss.str().c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Returns a string from start with givent count.
+ ///
+ /// @param[in] str String to use
+ /// @param[in] count Amount of characters to go from left
+ /// @return The left part string in amount of given count or complete if it
+ /// was higher.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr, varstr;
+ /// std::string origstr = "test";
+ ///
+ /// refstr = "";
+ /// varstr = kodi::tools::StringUtils::Left(origstr, 0);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ ///
+ /// refstr = "te";
+ /// varstr = kodi::tools::StringUtils::Left(origstr, 2);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ ///
+ /// refstr = "test";
+ /// varstr = kodi::tools::StringUtils::Left(origstr, 10);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::string Left(const std::string& str, size_t count)
+ {
+ count = std::max((size_t)0, std::min(count, str.size()));
+ return str.substr(0, count);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Get substring from mid of given string.
+ ///
+ /// @param[in] str String to get substring from
+ /// @param[in] first Position from where to start
+ /// @param[in] count [opt] length of position to get after start, default is
+ /// complete to end
+ /// @return The substring taken from middle of input string
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr, varstr;
+ /// std::string origstr = "test";
+ ///
+ /// refstr = "";
+ /// varstr = kodi::tools::StringUtils::Mid(origstr, 0, 0);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ ///
+ /// refstr = "te";
+ /// varstr = kodi::tools::StringUtils::Mid(origstr, 0, 2);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ ///
+ /// refstr = "test";
+ /// varstr = kodi::tools::StringUtils::Mid(origstr, 0, 10);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ ///
+ /// refstr = "st";
+ /// varstr = kodi::tools::StringUtils::Mid(origstr, 2);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ ///
+ /// refstr = "st";
+ /// varstr = kodi::tools::StringUtils::Mid(origstr, 2, 2);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ ///
+ /// refstr = "es";
+ /// varstr = kodi::tools::StringUtils::Mid(origstr, 1, 2);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::string Mid(const std::string& str,
+ size_t first,
+ size_t count = std::string::npos)
+ {
+ if (first + count > str.size())
+ count = str.size() - first;
+
+ if (first > str.size())
+ return std::string();
+
+ assert(first + count <= str.size());
+
+ return str.substr(first, count);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Returns a string from end with givent count.
+ ///
+ /// @param[in] str String to use
+ /// @param[in] count Amount of characters to go from right
+ /// @return The right part string in amount of given count or complete if it
+ /// was higher.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr, varstr;
+ /// std::string origstr = "test";
+ ///
+ /// refstr = "";
+ /// varstr = kodi::tools::StringUtils::Right(origstr, 0);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ ///
+ /// refstr = "st";
+ /// varstr = kodi::tools::StringUtils::Right(origstr, 2);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ ///
+ /// refstr = "test";
+ /// varstr = kodi::tools::StringUtils::Right(origstr, 10);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::string Right(const std::string& str, size_t count)
+ {
+ count = std::max((size_t)0, std::min(count, str.size()));
+ return str.substr(str.size() - count);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Trim a string with remove of not wanted spaces at begin and end
+ /// of string.
+ ///
+ /// @param[in,out] str String to trim, becomes also changed and given on
+ /// return
+ /// @return The changed string
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr = "test test";
+ ///
+ /// std::string varstr = " test test ";
+ /// kodi::tools::StringUtils::Trim(varstr);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::string& Trim(std::string& str)
+ {
+ TrimLeft(str);
+ return TrimRight(str);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Trim a string with remove of not wanted characters at begin and end
+ /// of string.
+ ///
+ /// @param[in,out] str String to trim, becomes also changed and given on
+ /// return
+ /// @param[in] chars Characters to use for trim
+ /// @return The changed string
+ ///
+ inline static std::string& Trim(std::string& str, const char* const chars)
+ {
+ TrimLeft(str, chars);
+ return TrimRight(str, chars);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Trim a string with remove of not wanted spaces at begin of string.
+ ///
+ /// @param[in,out] str String to trim, becomes also changed and given on
+ /// return
+ /// @return The changed string
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr = "test test ";
+ ///
+ /// std::string varstr = " test test ";
+ /// kodi::tools::StringUtils::TrimLeft(varstr);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::string& TrimLeft(std::string& str)
+ {
+ str.erase(str.begin(),
+ std::find_if(str.begin(), str.end(), [](char s) { return IsSpace(s) == 0; }));
+ return str;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Trim a string with remove of not wanted characters at begin of
+ /// string.
+ ///
+ /// @param[in,out] str String to trim, becomes also changed and given on
+ /// return
+ /// @param[in] chars Characters to use for trim
+ /// @return The changed string
+ ///
+ inline static std::string& TrimLeft(std::string& str, const char* const chars)
+ {
+ size_t nidx = str.find_first_not_of(chars);
+ str.erase(0, nidx);
+ return str;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Trim a string with remove of not wanted spaces at end of string.
+ ///
+ /// @param[in,out] str String to trim, becomes also changed and given on
+ /// return
+ /// @return The changed string
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr = " test test";
+ ///
+ /// std::string varstr = " test test ";
+ /// kodi::tools::StringUtils::TrimRight(varstr);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::string& TrimRight(std::string& str)
+ {
+ str.erase(std::find_if(str.rbegin(), str.rend(), [](char s) { return IsSpace(s) == 0; }).base(),
+ str.end());
+ return str;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Trim a string with remove of not wanted characters at end of
+ /// string.
+ ///
+ /// @param[in,out] str String to trim, becomes also changed and given on
+ /// return
+ /// @param[in] chars Characters to use for trim
+ /// @return The changed string
+ ///
+ inline static std::string& TrimRight(std::string& str, const char* const chars)
+ {
+ size_t nidx = str.find_last_not_of(chars);
+ str.erase(str.npos == nidx ? 0 : ++nidx);
+ return str;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Cleanup string by remove of duplicates of spaces and tabs.
+ ///
+ /// @param[in,out] str String to remove duplicates, becomes also changed and
+ /// given further on return
+ /// @return The changed string
+ ///
+ inline static std::string& RemoveDuplicatedSpacesAndTabs(std::string& str)
+ {
+ std::string::iterator it = str.begin();
+ bool onSpace = false;
+ while (it != str.end())
+ {
+ if (*it == '\t')
+ *it = ' ';
+
+ if (*it == ' ')
+ {
+ if (onSpace)
+ {
+ it = str.erase(it);
+ continue;
+ }
+ else
+ onSpace = true;
+ }
+ else
+ onSpace = false;
+
+ ++it;
+ }
+ return str;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Replace a character with another inside text string.
+ ///
+ /// @param[in] str String to replace within
+ /// @param[in] oldChar Character to search for replacement
+ /// @param[in] newChar New character to use for replacement
+ /// @return Amount of replaced characters
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr = "text text";
+ ///
+ /// std::string varstr = "test test";
+ /// EXPECT_EQ(kodi::tools::StringUtils::Replace(varstr, 's', 'x'), 2);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ ///
+ /// EXPECT_EQ(kodi::tools::StringUtils::Replace(varstr, 's', 'x'), 0);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static int Replace(std::string& str, char oldChar, char newChar)
+ {
+ int replacedChars = 0;
+ for (std::string::iterator it = str.begin(); it != str.end(); ++it)
+ {
+ if (*it == oldChar)
+ {
+ *it = newChar;
+ replacedChars++;
+ }
+ }
+
+ return replacedChars;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Replace a complete text with another inside text string.
+ ///
+ /// @param[in] str String to replace within
+ /// @param[in] oldStr String to search for replacement
+ /// @param[in] newStr New string to use for replacement
+ /// @return Amount of replaced text fields
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr = "text text";
+ ///
+ /// std::string varstr = "test test";
+ /// EXPECT_EQ(kodi::tools::StringUtils::Replace(varstr, "s", "x"), 2);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ ///
+ /// EXPECT_EQ(kodi::tools::StringUtils::Replace(varstr, "s", "x"), 0);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static int Replace(std::string& str, const std::string& oldStr, const std::string& newStr)
+ {
+ if (oldStr.empty())
+ return 0;
+
+ int replacedChars = 0;
+ size_t index = 0;
+
+ while (index < str.size() && (index = str.find(oldStr, index)) != std::string::npos)
+ {
+ str.replace(index, oldStr.size(), newStr);
+ index += newStr.size();
+ replacedChars++;
+ }
+
+ return replacedChars;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Replace a complete text with another inside 16bit wide text string.
+ ///
+ /// @param[in] str String to replace within
+ /// @param[in] oldStr String to search for replacement
+ /// @param[in] newStr New string to use for replacement
+ /// @return Amount of replaced text fields
+ ///
+ inline static int Replace(std::wstring& str,
+ const std::wstring& oldStr,
+ const std::wstring& newStr)
+ {
+ if (oldStr.empty())
+ return 0;
+
+ int replacedChars = 0;
+ size_t index = 0;
+
+ while (index < str.size() && (index = str.find(oldStr, index)) != std::string::npos)
+ {
+ str.replace(index, oldStr.size(), newStr);
+ index += newStr.size();
+ replacedChars++;
+ }
+
+ return replacedChars;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Transform characters to create a safe URL.
+ ///
+ /// @param[in] str The string to transform
+ /// @return The transformed string, with unsafe characters replaced by "_"
+ ///
+ /// Safe URLs are composed of the unreserved characters defined in
+ /// RFC 3986 section 2.3:
+ ///
+ /// ALPHA / DIGIT / "-" / "." / "_" / "~"
+ ///
+ /// Characters outside of this set will be replaced by "_".
+ ///
+ inline static std::string MakeSafeUrl(const std::string& str)
+ {
+ std::string safeUrl;
+
+ safeUrl.reserve(str.size());
+
+ std::transform(str.begin(), str.end(), std::back_inserter(safeUrl), [](char c) {
+ if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '-' ||
+ c == '.' || c == '_' || c == '~')
+ {
+ return c;
+ }
+ return '_';
+ });
+
+ return safeUrl;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Transform characters to create a safe, printable string.
+ ///
+ /// @param[in] str The string to transform
+ /// @return The transformed string, with unsafe characters replaced by " "
+ ///
+ /// Unsafe characters are defined as the non-printable ASCII characters
+ /// (character code 0-31).
+ ///
+ inline static std::string MakeSafeString(const std::string& str)
+ {
+ std::string safeString;
+
+ safeString.reserve(str.size());
+
+ std::transform(str.begin(), str.end(), std::back_inserter(safeString), [](char c) {
+ if (c < 0x20)
+ return ' ';
+
+ return c;
+ });
+
+ return safeString;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Removes a MAC address from a given string.
+ ///
+ /// @param[in] str The string containing a MAC address
+ /// @return The string without the MAC address (for chaining)
+ ///
+ inline static std::string RemoveMACAddress(const std::string& str)
+ {
+ std::regex re(R"mac([\(\[]?([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})[\)\]]?)mac");
+ return std::regex_replace(str, re, "", std::regex_constants::format_default);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Remove carriage return and line feeds on string ends.
+ ///
+ /// @param[in,out] str String where CR and LF becomes removed on end
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr, varstr;
+ ///
+ /// refstr = "test\r\nstring\nblah blah";
+ /// varstr = "test\r\nstring\nblah blah\n";
+ /// kodi::tools::StringUtils::RemoveCRLF(varstr);
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static void RemoveCRLF(std::string& strLine) { StringUtils::TrimRight(strLine, "\n\r"); }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Convert a word to a digit numerical string
+ ///
+ /// @param[in] str String to convert
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// std::string ref, var;
+ ///
+ /// ref = "8378 787464";
+ /// var = "test string";
+ /// kodi::tools::StringUtils::WordToDigits(var);
+ /// EXPECT_STREQ(ref.c_str(), var.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static void WordToDigits(std::string& word)
+ {
+ static const char word_to_letter[] = "22233344455566677778889999";
+ StringUtils::ToLower(word);
+ for (unsigned int i = 0; i < word.size(); ++i)
+ { // NB: This assumes ascii, which probably needs extending at some point.
+ char letter = word[i];
+ if ((letter >= 'a' && letter <= 'z')) // assume contiguous letter range
+ {
+ word[i] = word_to_letter[letter - 'a'];
+ }
+ else if (letter < '0' || letter > '9') // We want to keep 0-9!
+ {
+ word[i] = ' '; // replace everything else with a space
+ }
+ }
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Escapes the given string to be able to be used as a parameter.
+ ///
+ /// Escapes backslashes and double-quotes with an additional backslash and
+ /// adds double-quotes around the whole string.
+ ///
+ /// @param[in] param String to escape/paramify
+ /// @return Escaped/Paramified string
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// const char *input = "some, very \\ odd \"string\"";
+ /// const char *ref = "\"some, very \\\\ odd \\\"string\\\"\"";
+ ///
+ /// std::string result = kodi::tools::StringUtils::Paramify(input);
+ /// EXPECT_STREQ(ref, result.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::string Paramify(const std::string& param)
+ {
+ std::string result = param;
+ // escape backspaces
+ StringUtils::Replace(result, "\\", "\\\\");
+ // escape double quotes
+ StringUtils::Replace(result, "\"", "\\\"");
+
+ // add double quotes around the whole string
+ return "\"" + result + "\"";
+ }
+ //----------------------------------------------------------------------------
+
+ /*!@}*/
+
+ //----------------------------------------------------------------------------
+ /// @defgroup cpp_kodi_tools_StringUtils_CompareControl String compare
+ /// @ingroup cpp_kodi_tools_StringUtils
+ /// @brief **Check strings for the desired state**\n
+ /// With this, texts can be checked to see that they correspond to a required
+ /// format.
+ ///
+ /*!@{*/
+
+ //============================================================================
+ /// @brief Compare two strings with ignore of lower-/uppercase.
+ ///
+ /// @param[in] str1 C++ string to compare
+ /// @param[in] str2 C++ string to compare
+ /// @return True if the strings are equal, false otherwise
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr = "TeSt";
+ ///
+ /// EXPECT_TRUE(kodi::tools::StringUtils::EqualsNoCase(refstr, "TeSt"));
+ /// EXPECT_TRUE(kodi::tools::StringUtils::EqualsNoCase(refstr, "tEsT"));
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static bool EqualsNoCase(const std::string& str1, const std::string& str2)
+ {
+ // before we do the char-by-char comparison, first compare sizes of both strings.
+ // This led to a 33% improvement in benchmarking on average. (size() just returns a member of std::string)
+ if (str1.size() != str2.size())
+ return false;
+ return EqualsNoCase(str1.c_str(), str2.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Compare two strings with ignore of lower-/uppercase.
+ ///
+ /// @param[in] str1 C++ string to compare
+ /// @param[in] s2 C string to compare
+ /// @return True if the strings are equal, false otherwise
+ ///
+ inline static bool EqualsNoCase(const std::string& str1, const char* s2)
+ {
+ return EqualsNoCase(str1.c_str(), s2);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Compare two strings with ignore of lower-/uppercase.
+ ///
+ /// @param[in] s1 C string to compare
+ /// @param[in] s2 C string to compare
+ /// @return True if the strings are equal, false otherwise
+ ///
+ inline static bool EqualsNoCase(const char* s1, const char* s2)
+ {
+ char c2; // we need only one char outside the loop
+ do
+ {
+ const char c1 = *s1++; // const local variable should help compiler to optimize
+ c2 = *s2++;
+ // This includes the possibility that one of the characters is the null-terminator,
+ // which implies a string mismatch.
+ if (c1 != c2 && ::tolower(c1) != ::tolower(c2))
+ return false;
+ } while (c2 != '\0'); // At this point, we know c1 == c2, so there's no need to test them both.
+ return true;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Compare two strings with ignore of lower-/uppercase with given
+ /// size.
+ ///
+ /// Equal to @ref EqualsNoCase only that size can defined and on return the
+ /// difference between compared character becomes given.
+ ///
+ /// @param[in] str1 C++ string to compare
+ /// @param[in] str2 C++ string to compare
+ /// @param[in] n [opt] Length to check, 0 as default to make complete
+ /// @return 0 if equal, otherwise difference of failed character in string to
+ /// other ("a" - "b" = -1)
+ ///
+ inline static int CompareNoCase(const std::string& str1, const std::string& str2, size_t n = 0)
+ {
+ return CompareNoCase(str1.c_str(), str2.c_str(), n);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Compare two strings with ignore of lower-/uppercase with given
+ /// size.
+ ///
+ /// Equal to @ref EqualsNoCase only that size can defined and on return the
+ /// difference between compared character becomes given.
+ ///
+ /// @param[in] s1 C string to compare
+ /// @param[in] s2 C string to compare
+ /// @param[in] n [opt] Length to check, 0 as default to make complete
+ /// @return 0 if equal, otherwise difference of failed character in string to
+ /// other ("a" - "b" = -1)
+ ///
+ inline static int CompareNoCase(const char* s1, const char* s2, size_t n = 0)
+ {
+ char c2; // we need only one char outside the loop
+ size_t index = 0;
+ do
+ {
+ const char c1 = *s1++; // const local variable should help compiler to optimize
+ c2 = *s2++;
+ index++;
+ // This includes the possibility that one of the characters is the null-terminator,
+ // which implies a string mismatch.
+ if (c1 != c2 && ::tolower(c1) != ::tolower(c2))
+ return ::tolower(c1) - ::tolower(c2);
+ } while (c2 != '\0' &&
+ index != n); // At this point, we know c1 == c2, so there's no need to test them both.
+ return 0;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a string for the begin of another string.
+ ///
+ /// @param[in] str1 C++ string to be checked
+ /// @param[in] str2 C++ string with which text defined in str1 is checked at
+ /// the beginning
+ /// @return True if string started with asked text, false otherwise
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// bool ret;
+ /// std::string refstr = "test";
+ ///
+ /// ret = kodi::tools::StringUtils::StartsWith(refstr, "te");
+ /// fprintf(stderr, "Expect true for here and is '%s'\n", ret ? "true" : "false");
+ ///
+ /// ret = kodi::tools::StringUtils::StartsWith(refstr, "abc");
+ /// fprintf(stderr, "Expect false for here and is '%s'\n", ret ? "true" : "false");
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static bool StartsWith(const std::string& str1, const std::string& str2)
+ {
+ return str1.compare(0, str2.size(), str2) == 0;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a string for the begin of another string.
+ ///
+ /// @param[in] str1 C++ string to be checked
+ /// @param[in] s2 C string with which text defined in str1 is checked at
+ /// the beginning
+ /// @return True if string started with asked text, false otherwise
+ ///
+ inline static bool StartsWith(const std::string& str1, const char* s2)
+ {
+ return StartsWith(str1.c_str(), s2);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a string for the begin of another string.
+ ///
+ /// @param[in] s1 C string to be checked
+ /// @param[in] s2 C string with which text defined in str1 is checked at
+ /// the beginning
+ /// @return True if string started with asked text, false otherwise
+ ///
+ inline static bool StartsWith(const char* s1, const char* s2)
+ {
+ while (*s2 != '\0')
+ {
+ if (*s1 != *s2)
+ return false;
+ s1++;
+ s2++;
+ }
+ return true;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a string for the begin of another string by ignore of
+ /// upper-/lowercase.
+ ///
+ /// @param[in] str1 C++ string to be checked
+ /// @param[in] str2 C++ string with which text defined in str1 is checked at
+ /// the beginning
+ /// @return True if string started with asked text, false otherwise
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// bool ret;
+ /// std::string refstr = "test";
+ ///
+ /// ret = kodi::tools::StringUtils::StartsWithNoCase(refstr, "te");
+ /// fprintf(stderr, "Expect true for here and is '%s'\n", ret ? "true" : "false");
+ ///
+ /// ret = kodi::tools::StringUtils::StartsWithNoCase(refstr, "TEs");
+ /// fprintf(stderr, "Expect true for here and is '%s'\n", ret ? "true" : "false");
+ ///
+ /// ret = kodi::tools::StringUtils::StartsWithNoCase(refstr, "abc");
+ /// fprintf(stderr, "Expect false for here and is '%s'\n", ret ? "true" : "false");
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static bool StartsWithNoCase(const std::string& str1, const std::string& str2)
+ {
+ return StartsWithNoCase(str1.c_str(), str2.c_str());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a string for the begin of another string by ignore of
+ /// upper-/lowercase.
+ ///
+ /// @param[in] str1 C++ string to be checked
+ /// @param[in] s2 C string with which text defined in str1 is checked at
+ /// the beginning
+ /// @return True if string started with asked text, false otherwise
+ ///
+ inline static bool StartsWithNoCase(const std::string& str1, const char* s2)
+ {
+ return StartsWithNoCase(str1.c_str(), s2);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a string for the begin of another string by ignore of
+ /// upper-/lowercase.
+ ///
+ /// @param[in] s1 C string to be checked
+ /// @param[in] s2 C string with which text defined in str1 is checked at
+ /// the beginning
+ /// @return True if string started with asked text, false otherwise
+ ///
+ inline static bool StartsWithNoCase(const char* s1, const char* s2)
+ {
+ while (*s2 != '\0')
+ {
+ if (::tolower(*s1) != ::tolower(*s2))
+ return false;
+ s1++;
+ s2++;
+ }
+ return true;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a string for the ending of another string.
+ ///
+ /// @param[in] str1 C++ string to be checked
+ /// @param[in] str2 C++ string with which text defined in str1 is checked at
+ /// the ending
+ /// @return True if string ended with asked text, false otherwise
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// bool ret;
+ /// std::string refstr = "test";
+ ///
+ /// ret = kodi::tools::StringUtils::EndsWith(refstr, "st");
+ /// fprintf(stderr, "Expect true for here and is '%s'\n", ret ? "true" : "false");
+ ///
+ /// ret = kodi::tools::StringUtils::EndsWith(refstr, "abc");
+ /// fprintf(stderr, "Expect false for here and is '%s'\n", ret ? "true" : "false");
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static bool EndsWith(const std::string& str1, const std::string& str2)
+ {
+ if (str1.size() < str2.size())
+ return false;
+ return str1.compare(str1.size() - str2.size(), str2.size(), str2) == 0;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a string for the ending of another string.
+ ///
+ /// @param[in] str1 C++ string to be checked
+ /// @param[in] s2 C string with which text defined in str1 is checked at
+ /// the ending
+ /// @return True if string ended with asked text, false otherwise
+ ///
+ inline static bool EndsWith(const std::string& str1, const char* s2)
+ {
+ size_t len2 = strlen(s2);
+ if (str1.size() < len2)
+ return false;
+ return str1.compare(str1.size() - len2, len2, s2) == 0;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a string for the ending of another string by ignore of
+ /// upper-/lowercase.
+ ///
+ /// @param[in] str1 C++ string to be checked
+ /// @param[in] str2 C++ string with which text defined in str1 is checked at
+ /// the ending
+ /// @return True if string ended with asked text, false otherwise
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// bool ret;
+ /// std::string refstr = "test";
+ ///
+ /// ret = kodi::tools::StringUtils::EndsWithNoCase(refstr, "ST");
+ /// fprintf(stderr, "Expect true for here and is '%s'\n", ret ? "true" : "false");
+ ///
+ /// ret = kodi::tools::StringUtils::EndsWithNoCase(refstr, "ABC");
+ /// fprintf(stderr, "Expect false for here and is '%s'\n", ret ? "true" : "false");
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static bool EndsWithNoCase(const std::string& str1, const std::string& str2)
+ {
+ if (str1.size() < str2.size())
+ return false;
+ const char* s1 = str1.c_str() + str1.size() - str2.size();
+ const char* s2 = str2.c_str();
+ while (*s2 != '\0')
+ {
+ if (::tolower(*s1) != ::tolower(*s2))
+ return false;
+ s1++;
+ s2++;
+ }
+ return true;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a string for the ending of another string by ignore of
+ /// upper-/lowercase.
+ ///
+ /// @param[in] str1 C++ string to be checked
+ /// @param[in] s2 C string with which text defined in str1 is checked at
+ /// the ending
+ /// @return True if string ended with asked text, false otherwise
+ ///
+ inline static bool EndsWithNoCase(const std::string& str1, const char* s2)
+ {
+ size_t len2 = strlen(s2);
+ if (str1.size() < len2)
+ return false;
+ const char* s1 = str1.c_str() + str1.size() - len2;
+ while (*s2 != '\0')
+ {
+ if (::tolower(*s1) != ::tolower(*s2))
+ return false;
+ s1++;
+ s2++;
+ }
+ return true;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Compare two strings by his calculated alpha numeric values.
+ ///
+ /// @param[in] left Left string to compare with right
+ /// @param[in] right Right string to compare with left
+ /// @return Return about compare
+ /// - 0 if left and right the same
+ /// - -1 if right is longer
+ /// - 1 if left is longer
+ /// - < 0 if less equal
+ /// - > 0 if more equal
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// int64_t ref, var;
+ ///
+ /// ref = 0;
+ /// var = kodi::tools::StringUtils::AlphaNumericCompare(L"123abc", L"abc123");
+ /// EXPECT_LT(var, ref);
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static int64_t AlphaNumericCompare(const wchar_t* left, const wchar_t* right)
+ {
+ const wchar_t* l = left;
+ const wchar_t* r = right;
+ const wchar_t *ld, *rd;
+ wchar_t lc, rc;
+ int64_t lnum, rnum;
+ const std::collate<wchar_t>& coll = std::use_facet<std::collate<wchar_t>>(std::locale());
+ int cmp_res = 0;
+ while (*l != 0 && *r != 0)
+ {
+ // check if we have a numerical value
+ if (*l >= L'0' && *l <= L'9' && *r >= L'0' && *r <= L'9')
+ {
+ ld = l;
+ lnum = 0;
+ while (*ld >= L'0' && *ld <= L'9' && ld < l + 15)
+ { // compare only up to 15 digits
+ lnum *= 10;
+ lnum += *ld++ - '0';
+ }
+ rd = r;
+ rnum = 0;
+ while (*rd >= L'0' && *rd <= L'9' && rd < r + 15)
+ { // compare only up to 15 digits
+ rnum *= 10;
+ rnum += *rd++ - L'0';
+ }
+ // do we have numbers?
+ if (lnum != rnum)
+ { // yes - and they're different!
+ return lnum - rnum;
+ }
+ l = ld;
+ r = rd;
+ continue;
+ }
+ // do case less comparison
+ lc = *l;
+ if (lc >= L'A' && lc <= L'Z')
+ lc += L'a' - L'A';
+ rc = *r;
+ if (rc >= L'A' && rc <= L'Z')
+ rc += L'a' - L'A';
+
+ // ok, do a normal comparison, taking current locale into account. Add special case stuff (eg '(' characters)) in here later
+ if ((cmp_res = coll.compare(&lc, &lc + 1, &rc, &rc + 1)) != 0)
+ {
+ return cmp_res;
+ }
+ l++;
+ r++;
+ }
+ if (*r)
+ { // r is longer
+ return -1;
+ }
+ else if (*l)
+ { // l is longer
+ return 1;
+ }
+ return 0; // files are the same
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief UTF8 version of strlen
+ ///
+ /// Skips any non-starting bytes in the count, thus returning the number of
+ /// utf8 characters.
+ ///
+ /// @param[in] s c-string to find the length of.
+ /// @return The number of utf8 characters in the string.
+ ///
+ inline static size_t Utf8StringLength(const char* s)
+ {
+ size_t length = 0;
+ while (*s)
+ {
+ if ((*s++ & 0xC0) != 0x80)
+ length++;
+ }
+ return length;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Check given character is a space.
+ ///
+ /// Hack to check only first byte of UTF-8 character
+ /// without this hack "TrimX" functions failed on Win32 and OS X with UTF-8 strings
+ ///
+ /// @param[in] c Character to check
+ /// @return true if space, false otherwise
+ ///
+ inline static int IsSpace(char c) { return (c & 0x80) == 0 && ::isspace(c); }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks given pointer in string is a UTF8 letter.
+ ///
+ /// @param[in] str Given character values to check, must be minimum array of 2
+ /// @return return -1 if not, else return the utf8 char length.
+ ///
+ inline static int IsUTF8Letter(const unsigned char* str)
+ {
+ // reference:
+ // unicode -> utf8 table: http://www.utf8-chartable.de/
+ // latin characters in unicode: http://en.wikipedia.org/wiki/Latin_characters_in_Unicode
+ unsigned char ch = str[0];
+ if (!ch)
+ return -1;
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))
+ return 1;
+ if (!(ch & 0x80))
+ return -1;
+ unsigned char ch2 = str[1];
+ if (!ch2)
+ return -1;
+ // check latin 1 letter table: http://en.wikipedia.org/wiki/C1_Controls_and_Latin-1_Supplement
+ if (ch == 0xC3 && ch2 >= 0x80 && ch2 <= 0xBF && ch2 != 0x97 && ch2 != 0xB7)
+ return 2;
+ // check latin extended A table: http://en.wikipedia.org/wiki/Latin_Extended-A
+ if (ch >= 0xC4 && ch <= 0xC7 && ch2 >= 0x80 && ch2 <= 0xBF)
+ return 2;
+ // check latin extended B table: http://en.wikipedia.org/wiki/Latin_Extended-B
+ // and International Phonetic Alphabet: http://en.wikipedia.org/wiki/IPA_Extensions_(Unicode_block)
+ if (((ch == 0xC8 || ch == 0xC9) && ch2 >= 0x80 && ch2 <= 0xBF) ||
+ (ch == 0xCA && ch2 >= 0x80 && ch2 <= 0xAF))
+ return 2;
+ return -1;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Check whether a string is a natural number.
+ ///
+ /// Matches `[ \t]*[0-9]+[ \t]*`
+ ///
+ /// @param[in] str The string to check
+ /// @return true if the string is a natural number, false otherwise.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// EXPECT_TRUE(kodi::tools::StringUtils::IsNaturalNumber("10"));
+ /// EXPECT_TRUE(kodi::tools::StringUtils::IsNaturalNumber(" 10"));
+ /// EXPECT_TRUE(kodi::tools::StringUtils::IsNaturalNumber("0"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsNaturalNumber(" 1 0"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsNaturalNumber("1.0"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsNaturalNumber("1.1"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsNaturalNumber("0x1"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsNaturalNumber("blah"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsNaturalNumber("120 h"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsNaturalNumber(" "));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsNaturalNumber(""));
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static bool IsNaturalNumber(const std::string& str)
+ {
+ size_t i = 0, n = 0;
+ // allow whitespace,digits,whitespace
+ while (i < str.size() && isspace((unsigned char)str[i]))
+ i++;
+ while (i < str.size() && isdigit((unsigned char)str[i]))
+ {
+ i++;
+ n++;
+ }
+ while (i < str.size() && isspace((unsigned char)str[i]))
+ i++;
+ return i == str.size() && n > 0;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Check whether a string is an integer.
+ ///
+ /// Matches `[ \t]*[\-]*[0-9]+[ \t]*`
+ ///
+ /// @param str The string to check
+ /// @return true if the string is an integer, false otherwise.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// EXPECT_TRUE(kodi::tools::StringUtils::IsInteger("10"));
+ /// EXPECT_TRUE(kodi::tools::StringUtils::IsInteger(" -10"));
+ /// EXPECT_TRUE(kodi::tools::StringUtils::IsInteger("0"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsInteger(" 1 0"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsInteger("1.0"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsInteger("1.1"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsInteger("0x1"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsInteger("blah"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsInteger("120 h"));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsInteger(" "));
+ /// EXPECT_FALSE(kodi::tools::StringUtils::IsInteger(""));
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static bool IsInteger(const std::string& str)
+ {
+ size_t i = 0, n = 0;
+ // allow whitespace,-,digits,whitespace
+ while (i < str.size() && isspace((unsigned char)str[i]))
+ i++;
+ if (i < str.size() && str[i] == '-')
+ i++;
+ while (i < str.size() && isdigit((unsigned char)str[i]))
+ {
+ i++;
+ n++;
+ }
+ while (i < str.size() && isspace((unsigned char)str[i]))
+ i++;
+ return i == str.size() && n > 0;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a character is ascii number.
+ ///
+ /// @param[in] chr Single character to test
+ /// @return true if yes, false otherwise
+ ///
+ inline static bool IsAasciiDigit(char chr) // locale independent
+ {
+ return chr >= '0' && chr <= '9';
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a character is ascii hexadecimal number.
+ ///
+ /// @param[in] chr Single character to test
+ /// @return true if yes, false otherwise
+ ///
+ inline static bool IsAsciiXDigit(char chr) // locale independent
+ {
+ return (chr >= '0' && chr <= '9') || (chr >= 'a' && chr <= 'f') || (chr >= 'A' && chr <= 'F');
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Translate a character where defined as a numerical value (0-9)
+ /// string to right integer.
+ ///
+ /// @param[in] chr Single character to translate
+ /// @return
+ ///
+ inline static int AsciiDigitValue(char chr) // locale independent
+ {
+ if (!IsAasciiDigit(chr))
+ return -1;
+
+ return chr - '0';
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Translate a character where defined as a hexadecimal value string
+ /// to right integer.
+ ///
+ /// @param[in] chr Single character to translate
+ /// @return Corresponding integer value, e.g. character is "A" becomes
+ /// returned as a integer with 10.
+ ///
+ inline static int AsciiXDigitValue(char chr) // locale independent
+ {
+ int v = AsciiDigitValue(chr);
+ if (v >= 0)
+ return v;
+ if (chr >= 'a' && chr <= 'f')
+ return chr - 'a' + 10;
+ if (chr >= 'A' && chr <= 'F')
+ return chr - 'A' + 10;
+
+ return -1;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a character is ascii alphabetic lowercase.
+ ///
+ /// @param[in] chr Single character to test
+ /// @return True if ascii uppercase letter, false otherwise
+ ///
+ inline static bool IsAsciiUppercaseLetter(char chr) // locale independent
+ {
+ return (chr >= 'A' && chr <= 'Z');
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a character is ascii alphabetic lowercase.
+ ///
+ /// @param[in] chr Single character to test
+ /// @return True if ascii lowercase letter, false otherwise
+ ///
+ inline static bool IsAsciiLowercaseLetter(char chr) // locale independent
+ {
+ return (chr >= 'a' && chr <= 'z');
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Checks a character is within ascii alphabetic and numerical fields.
+ ///
+ /// @param[in] chr Single character to test
+ /// @return true if alphabetic / numerical ascii value
+ ///
+ inline static bool IsAsciiAlphaNum(char chr) // locale independent
+ {
+ return IsAsciiUppercaseLetter(chr) || IsAsciiLowercaseLetter(chr) || IsAasciiDigit(chr);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Check a string for another text.
+ ///
+ /// @param[in] str String to search for keywords
+ /// @param[in] keywords List of keywords to search in text
+ /// @return true if string contains word in list
+ ///
+ inline static bool ContainsKeyword(const std::string& str,
+ const std::vector<std::string>& keywords)
+ {
+ for (const auto& it : keywords)
+ {
+ if (str.find(it) != str.npos)
+ return true;
+ }
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ /*!@}*/
+
+ //----------------------------------------------------------------------------
+ /// @defgroup cpp_kodi_tools_StringUtils_SearchControl String search
+ /// @ingroup cpp_kodi_tools_StringUtils
+ /// @brief **To search a string**\n
+ /// Various functions are defined in here which allow you to search through a
+ /// text in different ways.
+ ///
+ /*!@{*/
+
+ //============================================================================
+ /// @brief Search for a single word within a text.
+ ///
+ /// @param[in] str String to search within
+ /// @param[in] wordLowerCase Word as lowercase to search
+ /// @return Position in string where word is found, -1 (std::string::npos) if
+ /// not found
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// size_t ref, var;
+ ///
+ /// // The name "string" is alone within text and becomes found on position 5
+ /// ref = 5;
+ /// var = kodi::tools::StringUtils::FindWords("test string", "string");
+ /// EXPECT_EQ(ref, var);
+ ///
+ /// // The 12 is included inside another word and then not found as it should alone (-1 return)
+ /// ref = -1;
+ /// var = kodi::tools::StringUtils::FindWords("apple2012", "12");
+ /// EXPECT_EQ(ref, var);
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static size_t FindWords(const char* str, const char* wordLowerCase)
+ {
+ // NOTE: This assumes word is lowercase!
+ const unsigned char* s = (const unsigned char*)str;
+ do
+ {
+ // start with a compare
+ const unsigned char* c = s;
+ const unsigned char* w = (const unsigned char*)wordLowerCase;
+ bool same = true;
+ while (same && *c && *w)
+ {
+ unsigned char lc = *c++;
+ if (lc >= 'A' && lc <= 'Z')
+ lc += 'a' - 'A';
+
+ if (lc != *w++) // different
+ same = false;
+ }
+ if (same && *w == 0) // only the same if word has been exhausted
+ return (const char*)s - str;
+
+ // otherwise, skip current word (composed by latin letters) or number
+ int l;
+ if (*s >= '0' && *s <= '9')
+ {
+ ++s;
+ while (*s >= '0' && *s <= '9')
+ ++s;
+ }
+ else if ((l = IsUTF8Letter(s)) > 0)
+ {
+ s += l;
+ while ((l = IsUTF8Letter(s)) > 0)
+ s += l;
+ }
+ else
+ ++s;
+ while (*s && *s == ' ')
+ s++;
+
+ // and repeat until we're done
+ } while (*s);
+
+ return std::string::npos;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Search a string for a given bracket and give its end position.
+ ///
+ /// @param[in] str String to search within
+ /// @param[in] opener Begin character to start search
+ /// @param[in] closer End character to end search
+ /// @param[in] startPos [opt] Position to start search in string, 0 as default
+ /// to start from begin
+ /// @return End position where found, -1 if failed
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// int ref, var;
+ ///
+ /// ref = 11;
+ /// var = kodi::tools::StringUtils::FindEndBracket("atest testbb test", 'a', 'b');
+ /// EXPECT_EQ(ref, var);
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static size_t FindEndBracket(const std::string& str,
+ char opener,
+ char closer,
+ size_t startPos = 0)
+ {
+ size_t blocks = 1;
+ for (size_t i = startPos; i < str.size(); i++)
+ {
+ if (str[i] == opener)
+ blocks++;
+ else if (str[i] == closer)
+ {
+ blocks--;
+ if (!blocks)
+ return i;
+ }
+ }
+
+ return std::string::npos;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Search a text and return the number of parts found as a number.
+ ///
+ /// @param[in] strInput Input string to search for
+ /// @param[in] strFind String to search in input
+ /// @return Amount how much the string is found
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// EXPECT_EQ(3, kodi::tools::StringUtils::FindNumber("aabcaadeaa", "aa"));
+ /// EXPECT_EQ(1, kodi::tools::StringUtils::FindNumber("aabcaadeaa", "b"));
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static int FindNumber(const std::string& strInput, const std::string& strFind)
+ {
+ size_t pos = strInput.find(strFind, 0);
+ int numfound = 0;
+ while (pos != std::string::npos)
+ {
+ numfound++;
+ pos = strInput.find(strFind, pos + 1);
+ }
+ return numfound;
+ }
+ //----------------------------------------------------------------------------
+
+ /*!@}*/
+
+ //----------------------------------------------------------------------------
+ /// @defgroup cpp_kodi_tools_StringUtils_ListControl String list
+ /// @ingroup cpp_kodi_tools_StringUtils
+ /// @brief **Creating lists using a string**\n
+ /// With this, either simple vectors or lists defined by templates can be given
+ /// for the respective divided text.
+ ///
+ /*!@{*/
+
+ //============================================================================
+ /// @brief Concatenates the elements of a specified array or the members of a
+ /// collection and uses the specified separator between each element or
+ /// member.
+ ///
+ /// @param[in] strings An array of objects whose string representations are
+ /// concatenated.
+ /// @param[in] delimiter Delimiter to be used to join the input string
+ /// @return A string consisting of the elements of values, separated by the
+ /// separator character.
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string refstr, varstr;
+ /// std::vector<std::string> strarray;
+ ///
+ /// strarray.emplace_back("a");
+ /// strarray.emplace_back("b");
+ /// strarray.emplace_back("c");
+ /// strarray.emplace_back("de");
+ /// strarray.emplace_back(",");
+ /// strarray.emplace_back("fg");
+ /// strarray.emplace_back(",");
+ /// refstr = "a,b,c,de,,,fg,,";
+ /// varstr = kodi::tools::StringUtils::Join(strarray, ",");
+ /// EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ template<typename CONTAINER>
+ inline static std::string Join(const CONTAINER& strings, const std::string& delimiter)
+ {
+ std::string result;
+ for (const auto& str : strings)
+ result += str + delimiter;
+
+ if (!result.empty())
+ result.erase(result.size() - delimiter.size());
+ return result;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Splits the given input string using the given delimiter into
+ /// separate strings.
+ ///
+ /// If the given input string is empty the result will be an empty array (not
+ /// an array containing an empty string).
+ ///
+ /// @param[in] input Input string to be split
+ /// @param[in] delimiter Delimiter to be used to split the input string
+ /// @param[in] iMaxStrings [opt] Maximum number of resulting split strings
+ /// @return List of splitted strings
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::vector<std::string> varresults;
+ ///
+ /// varresults = kodi::tools::StringUtils::Split("g,h,ij,k,lm,,n", ",");
+ /// EXPECT_STREQ("g", varresults.at(0).c_str());
+ /// EXPECT_STREQ("h", varresults.at(1).c_str());
+ /// EXPECT_STREQ("ij", varresults.at(2).c_str());
+ /// EXPECT_STREQ("k", varresults.at(3).c_str());
+ /// EXPECT_STREQ("lm", varresults.at(4).c_str());
+ /// EXPECT_STREQ("", varresults.at(5).c_str());
+ /// EXPECT_STREQ("n", varresults.at(6).c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::vector<std::string> Split(const std::string& input,
+ const std::string& delimiter,
+ unsigned int iMaxStrings = 0)
+ {
+ std::vector<std::string> result;
+ SplitTo(std::back_inserter(result), input, delimiter, iMaxStrings);
+ return result;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Splits the given input string using the given delimiter into
+ /// separate strings.
+ ///
+ /// If the given input string is empty the result will be an empty array (not
+ /// an array containing an empty string).
+ ///
+ /// @param[in] input Input string to be split
+ /// @param[in] delimiter Delimiter to be used to split the input string
+ /// @param[in] iMaxStrings [opt] Maximum number of resulting split strings
+ /// @return List of splitted strings
+ ///
+ inline static std::vector<std::string> Split(const std::string& input,
+ const char delimiter,
+ int iMaxStrings = 0)
+ {
+ std::vector<std::string> result;
+ SplitTo(std::back_inserter(result), input, delimiter, iMaxStrings);
+ return result;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Splits the given input string using the given delimiter into
+ /// separate strings.
+ ///
+ /// If the given input string is empty the result will be an empty array (not
+ /// an array containing an empty string).
+ ///
+ /// @param[in] input Input string to be split
+ /// @param[in] delimiters Delimiter strings to be used to split the input
+ /// strings
+ /// @return List of splitted strings
+ ///
+ inline static std::vector<std::string> Split(const std::string& input,
+ const std::vector<std::string>& delimiters)
+ {
+ std::vector<std::string> result;
+ SplitTo(std::back_inserter(result), input, delimiters);
+ return result;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Splits the given input string using the given delimiter into
+ /// separate strings.
+ ///
+ /// If the given input string is empty nothing will be put into the target
+ /// iterator.
+ ///
+ /// @param[in] d_first The beginning of the destination range
+ /// @param[in] input Input string to be split
+ /// @param[in] delimiter Delimiter to be used to split the input string
+ /// @param[in] iMaxStrings [opt] Maximum number of resulting split strings
+ /// @return Output iterator to the element in the destination range, one past
+ /// the last element that was put there
+ ///
+ template<typename OutputIt>
+ inline static OutputIt SplitTo(OutputIt d_first,
+ const std::string& input,
+ const std::string& delimiter,
+ unsigned int iMaxStrings = 0)
+ {
+ OutputIt dest = d_first;
+
+ if (input.empty())
+ return dest;
+ if (delimiter.empty())
+ {
+ *d_first++ = input;
+ return dest;
+ }
+
+ const size_t delimLen = delimiter.length();
+ size_t nextDelim;
+ size_t textPos = 0;
+ do
+ {
+ if (--iMaxStrings == 0)
+ {
+ *dest++ = input.substr(textPos);
+ break;
+ }
+ nextDelim = input.find(delimiter, textPos);
+ *dest++ = input.substr(textPos, nextDelim - textPos);
+ textPos = nextDelim + delimLen;
+ } while (nextDelim != std::string::npos);
+
+ return dest;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Splits the given input string using the given delimiter into
+ /// separate strings.
+ ///
+ /// If the given input string is empty nothing will be put into the target
+ /// iterator.
+ ///
+ /// @param[in] d_first The beginning of the destination range
+ /// @param[in] input Input string to be split
+ /// @param[in] delimiter Delimiter to be used to split the input string
+ /// @param[in] iMaxStrings [opt] Maximum number of resulting split strings
+ /// @return Output iterator to the element in the destination range, one past
+ /// the last element that was put there
+ ///
+ template<typename OutputIt>
+ inline static OutputIt SplitTo(OutputIt d_first,
+ const std::string& input,
+ const char delimiter,
+ int iMaxStrings = 0)
+ {
+ return SplitTo(d_first, input, std::string(1, delimiter), iMaxStrings);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Splits the given input string using the given delimiter into
+ /// separate strings.
+ ///
+ /// If the given input string is empty nothing will be put into the target
+ /// iterator.
+ ///
+ /// @param[in] d_first The beginning of the destination range
+ /// @param[in] input Input string to be split
+ /// @param[in] delimiters Delimiter strings to be used to split the input
+ /// strings
+ /// @return Output iterator to the element in the destination range, one past
+ /// the last element that was put there
+ ///
+ template<typename OutputIt>
+ inline static OutputIt SplitTo(OutputIt d_first,
+ const std::string& input,
+ const std::vector<std::string>& delimiters)
+ {
+ OutputIt dest = d_first;
+ if (input.empty())
+ return dest;
+
+ if (delimiters.empty())
+ {
+ *dest++ = input;
+ return dest;
+ }
+ std::string str = input;
+ for (size_t di = 1; di < delimiters.size(); di++)
+ StringUtils::Replace(str, delimiters[di], delimiters[0]);
+ return SplitTo(dest, str, delimiters[0]);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Splits the given input strings using the given delimiters into
+ /// further separate strings.
+ ///
+ /// If the given input string vector is empty the result will be an empty
+ /// array (not an array containing an empty string).
+ ///
+ /// Delimiter strings are applied in order, so once the (optional) maximum
+ /// number of items is produced no other delimiters are applied. This produces
+ /// different results to applying all delimiters at once e.g. "a/b#c/d"
+ /// becomes "a", "b#c", "d" rather than "a", "b", "c/d"
+ ///
+ /// @param[in] input Input vector of strings each to be split
+ /// @param[in] delimiters Delimiter strings to be used to split the input
+ /// strings
+ /// @param[in] iMaxStrings [opt] Maximum number of resulting split strings
+ /// @return List of splitted strings
+ ///
+ inline static std::vector<std::string> SplitMulti(const std::vector<std::string>& input,
+ const std::vector<std::string>& delimiters,
+ unsigned int iMaxStrings = 0)
+ {
+ if (input.empty())
+ return std::vector<std::string>();
+
+ std::vector<std::string> results(input);
+
+ if (delimiters.empty() || (iMaxStrings > 0 && iMaxStrings <= input.size()))
+ return results;
+
+ std::vector<std::string> strings1;
+ if (iMaxStrings == 0)
+ {
+ for (size_t di = 0; di < delimiters.size(); di++)
+ {
+ for (size_t i = 0; i < results.size(); i++)
+ {
+ std::vector<std::string> substrings = StringUtils::Split(results[i], delimiters[di]);
+ for (size_t j = 0; j < substrings.size(); j++)
+ strings1.push_back(substrings[j]);
+ }
+ results = strings1;
+ strings1.clear();
+ }
+ return results;
+ }
+
+ // Control the number of strings input is split into, keeping the original strings.
+ // Note iMaxStrings > input.size()
+ size_t iNew = iMaxStrings - results.size();
+ for (size_t di = 0; di < delimiters.size(); di++)
+ {
+ for (size_t i = 0; i < results.size(); i++)
+ {
+ if (iNew > 0)
+ {
+ std::vector<std::string> substrings =
+ StringUtils::Split(results[i], delimiters[di], static_cast<int>(iNew + 1));
+ iNew = iNew - substrings.size() + 1;
+ for (size_t j = 0; j < substrings.size(); j++)
+ strings1.push_back(substrings[j]);
+ }
+ else
+ strings1.push_back(results[i]);
+ }
+ results = strings1;
+ iNew = iMaxStrings - results.size();
+ strings1.clear();
+ if ((iNew <= 0))
+ break; //Stop trying any more delimiters
+ }
+ return results;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Split a string by the specified delimiters.
+ ///
+ /// Splits a string using one or more delimiting characters, ignoring empty
+ /// tokens.
+ ///
+ /// Differs from Split() in two ways:
+ /// 1. The delimiters are treated as individual characters, rather than a single delimiting string.
+ /// 2. Empty tokens are ignored.
+ ///
+ ///
+ /// @param[in] input String to split
+ /// @param[in] delimiters Delimiters
+ /// @return A vector of tokens
+ ///
+ inline static std::vector<std::string> Tokenize(const std::string& input,
+ const std::string& delimiters)
+ {
+ std::vector<std::string> tokens;
+ Tokenize(input, tokens, delimiters);
+ return tokens;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Tokenizing a string denotes splitting a string with respect to a
+ /// delimiter.
+ ///
+ /// @param[in] input String to split
+ /// @param[out] tokens A vector of tokens
+ /// @param[in] delimiters Delimiters
+ ///
+ inline static void Tokenize(const std::string& input,
+ std::vector<std::string>& tokens,
+ const std::string& delimiters)
+ {
+ tokens.clear();
+ // Skip delimiters at beginning.
+ std::string::size_type dataPos = input.find_first_not_of(delimiters);
+ while (dataPos != std::string::npos)
+ {
+ // Find next delimiter
+ const std::string::size_type nextDelimPos = input.find_first_of(delimiters, dataPos);
+ // Found a token, add it to the vector.
+ tokens.push_back(input.substr(dataPos, nextDelimPos - dataPos));
+ // Skip delimiters. Note the "not_of"
+ dataPos = input.find_first_not_of(delimiters, nextDelimPos);
+ }
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Tokenizing a string denotes splitting a string with respect to a
+ /// delimiter.
+ ///
+ /// @param[in] input String to split
+ /// @param[in] delimiter Delimiters
+ /// @return A vector of tokens
+ ///
+ inline static std::vector<std::string> Tokenize(const std::string& input, const char delimiter)
+ {
+ std::vector<std::string> tokens;
+ Tokenize(input, tokens, delimiter);
+ return tokens;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Tokenizing a string denotes splitting a string with respect to a
+ /// delimiter.
+ ///
+ /// @param[in] input String to split
+ /// @param[out] tokens List of
+ /// @param[in] delimiter Delimiters
+ ///
+ inline static void Tokenize(const std::string& input,
+ std::vector<std::string>& tokens,
+ const char delimiter)
+ {
+ tokens.clear();
+ // Skip delimiters at beginning.
+ std::string::size_type dataPos = input.find_first_not_of(delimiter);
+ while (dataPos != std::string::npos)
+ {
+ // Find next delimiter
+ const std::string::size_type nextDelimPos = input.find(delimiter, dataPos);
+ // Found a token, add it to the vector.
+ tokens.push_back(input.substr(dataPos, nextDelimPos - dataPos));
+ // Skip delimiters. Note the "not_of"
+ dataPos = input.find_first_not_of(delimiter, nextDelimPos);
+ }
+ }
+ //----------------------------------------------------------------------------
+
+ /*!@}*/
+
+ //----------------------------------------------------------------------------
+ /// @defgroup cpp_kodi_tools_StringUtils_TimeControl Time value processing
+ /// @ingroup cpp_kodi_tools_StringUtils
+ /// @brief **String time formats**\n
+ /// This is used to process the respective time formats in text fields.
+ /*!@{*/
+
+ //============================================================================
+ /// @brief Converts a time string to the respective integer value.
+ ///
+ /// @param[in] timeString String with time.\n
+ /// Following types are possible:
+ /// - "MM min" (integer number with "min" on end)
+ /// - "HH:MM:SS"
+ /// @return Time in seconds
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// EXPECT_EQ(77455, kodi::tools::StringUtils::TimeStringToSeconds("21:30:55"));
+ /// EXPECT_EQ(7*60, kodi::tools::StringUtils::TimeStringToSeconds("7 min"));
+ /// EXPECT_EQ(7*60, kodi::tools::StringUtils::TimeStringToSeconds("7 min\t"));
+ /// EXPECT_EQ(154*60, kodi::tools::StringUtils::TimeStringToSeconds(" 154 min"));
+ /// EXPECT_EQ(1*60+1, kodi::tools::StringUtils::TimeStringToSeconds("1:01"));
+ /// EXPECT_EQ(4*60+3, kodi::tools::StringUtils::TimeStringToSeconds("4:03"));
+ /// EXPECT_EQ(2*3600+4*60+3, kodi::tools::StringUtils::TimeStringToSeconds("2:04:03"));
+ /// EXPECT_EQ(2*3600+4*60+3, kodi::tools::StringUtils::TimeStringToSeconds(" 2:4:3"));
+ /// EXPECT_EQ(2*3600+4*60+3, kodi::tools::StringUtils::TimeStringToSeconds(" \t\t 02:04:03 \n "));
+ /// EXPECT_EQ(1*3600+5*60+2, kodi::tools::StringUtils::TimeStringToSeconds("01:05:02:04:03 \n "));
+ /// EXPECT_EQ(0, kodi::tools::StringUtils::TimeStringToSeconds("blah"));
+ /// EXPECT_EQ(0, kodi::tools::StringUtils::TimeStringToSeconds("ля-ля"));
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static long TimeStringToSeconds(const std::string& timeString)
+ {
+ std::string strCopy(timeString);
+ StringUtils::Trim(strCopy);
+ if (StringUtils::EndsWithNoCase(strCopy, " min"))
+ {
+ // this is imdb format of "XXX min"
+ return 60 * atoi(strCopy.c_str());
+ }
+ else
+ {
+ std::vector<std::string> secs = StringUtils::Split(strCopy, ':');
+ int timeInSecs = 0;
+ for (unsigned int i = 0; i < 3 && i < secs.size(); i++)
+ {
+ timeInSecs *= 60;
+ timeInSecs += atoi(secs[i].c_str());
+ }
+ return timeInSecs;
+ }
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Convert a time in seconds to a string based on the given time
+ /// format.
+ ///
+ /// @param[in] seconds time in seconds
+ /// @param[in] format [opt] The format we want the time in
+ /// @return The formatted time
+ ///
+ /// @sa TIME_FORMAT
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// std::string ref, var;
+ ///
+ /// ref = "21:30:55";
+ /// var = kodi::tools::StringUtils::SecondsToTimeString(77455);
+ /// EXPECT_STREQ(ref.c_str(), var.c_str());
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static std::string SecondsToTimeString(long seconds,
+ TIME_FORMAT format = TIME_FORMAT_GUESS)
+ {
+ bool isNegative = seconds < 0;
+ seconds = std::abs(seconds);
+
+ std::string strHMS;
+ if (format == TIME_FORMAT_SECS)
+ strHMS = std::to_string(seconds);
+ else if (format == TIME_FORMAT_MINS)
+ strHMS = std::to_string(lrintf(static_cast<float>(seconds) / 60.0f));
+ else if (format == TIME_FORMAT_HOURS)
+ strHMS = std::to_string(lrintf(static_cast<float>(seconds) / 3600.0f));
+ else if (format & TIME_FORMAT_M)
+ strHMS += std::to_string(seconds % 3600 / 60);
+ else
+ {
+ int hh = seconds / 3600;
+ seconds = seconds % 3600;
+ int mm = seconds / 60;
+ int ss = seconds % 60;
+
+ if (format == TIME_FORMAT_GUESS)
+ format = (hh >= 1) ? TIME_FORMAT_HH_MM_SS : TIME_FORMAT_MM_SS;
+ if (format & TIME_FORMAT_HH)
+ strHMS += StringUtils::Format("{:02}", hh);
+ else if (format & TIME_FORMAT_H)
+ strHMS += std::to_string(hh);
+ if (format & TIME_FORMAT_MM)
+ strHMS += StringUtils::Format(strHMS.empty() ? "{:02}" : ":{:02}", mm);
+ if (format & TIME_FORMAT_SS)
+ strHMS += StringUtils::Format(strHMS.empty() ? "{:02}" : ":{:02}", ss);
+ }
+
+ if (isNegative)
+ strHMS = "-" + strHMS;
+
+ return strHMS;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @brief Converts a string in the format YYYYMMDD to the corresponding
+ /// integer value.
+ ///
+ /// @param[in] dateString The date in the associated format, possible values
+ /// are:
+ /// - DD (for days only)
+ /// - MM-DD (for days with month)
+ /// - YYYY-MM-DD (for years, then month and last days)
+ /// @return Corresponding integer, e.g. "2020-12-24" return as integer value
+ /// 20201224
+ ///
+ ///
+ /// --------------------------------------------------------------------------
+ /// Example:
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/StringUtils.h>
+ ///
+ /// int ref, var;
+ ///
+ /// ref = 20120706;
+ /// var = kodi::tools::StringUtils::DateStringToYYYYMMDD("2012-07-06");
+ /// EXPECT_EQ(ref, var);
+ /// ~~~~~~~~~~~~~
+ ///
+ inline static int DateStringToYYYYMMDD(const std::string& dateString)
+ {
+ std::vector<std::string> days = StringUtils::Split(dateString, '-');
+ if (days.size() == 1)
+ return atoi(days[0].c_str());
+ else if (days.size() == 2)
+ return atoi(days[0].c_str()) * 100 + atoi(days[1].c_str());
+ else if (days.size() == 3)
+ return atoi(days[0].c_str()) * 10000 + atoi(days[1].c_str()) * 100 + atoi(days[2].c_str());
+ else
+ return -1;
+ }
+ //----------------------------------------------------------------------------
+
+ /*!@}*/
+
+private:
+ inline static int compareWchar(const void* a, const void* b)
+ {
+ if (*static_cast<const wchar_t*>(a) < *static_cast<const wchar_t*>(b))
+ return -1;
+ else if (*static_cast<const wchar_t*>(a) > *static_cast<const wchar_t*>(b))
+ return 1;
+ return 0;
+ }
+
+ inline static wchar_t tolowerUnicode(const wchar_t& c)
+ {
+ wchar_t* p =
+ static_cast<wchar_t*>(bsearch(&c, unicode_uppers, sizeof(unicode_uppers) / sizeof(wchar_t),
+ sizeof(wchar_t), compareWchar));
+ if (p)
+ return *(unicode_lowers + (p - unicode_uppers));
+
+ return c;
+ }
+
+ inline static wchar_t toupperUnicode(const wchar_t& c)
+ {
+ wchar_t* p =
+ static_cast<wchar_t*>(bsearch(&c, unicode_lowers, sizeof(unicode_lowers) / sizeof(wchar_t),
+ sizeof(wchar_t), compareWchar));
+ if (p)
+ return *(unicode_uppers + (p - unicode_lowers));
+
+ return c;
+ }
+
+ static uint32_t UTF8ToUnicode(const unsigned char* z, int nKey, unsigned char& bytes)
+ {
+ // Lookup table used decode the first byte of a multi-byte UTF8 character
+ // clang-format off
+ static const unsigned char utf8Trans1[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x00, 0x00,
+ };
+ // clang-format on
+
+ uint32_t c;
+ bytes = 0;
+ c = z[0];
+ if (c >= 0xc0)
+ {
+ c = utf8Trans1[c - 0xc0];
+ int index = 1;
+ while (index < nKey && (z[index] & 0xc0) == 0x80)
+ {
+ c = (c << 6) + (0x3f & z[index]);
+ index++;
+ }
+ if (c < 0x80 || (c & 0xFFFFF800) == 0xD800 || (c & 0xFFFFFFFE) == 0xFFFE)
+ c = 0xFFFD;
+ bytes = static_cast<unsigned char>(index - 1);
+ }
+ return c;
+ }
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace tools */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/tools/Thread.h b/xbmc/addons/kodi-dev-kit/include/kodi/tools/Thread.h
new file mode 100644
index 0000000..0510849
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/tools/Thread.h
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2005-2020 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
+
+#ifdef __cplusplus
+
+#include "../General.h"
+
+#include <chrono>
+#include <condition_variable>
+#include <future>
+#include <mutex>
+#include <thread>
+
+namespace kodi
+{
+namespace tools
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_tools_CThread class CThread
+/// @ingroup cpp_kodi_tools
+/// @brief **Helper class to represent threads of execution**\n
+/// An execution thread is a sequence of instructions that can run concurrently
+/// with other such sequences in multithreaded environments while sharing the
+/// same address space.
+///
+/// Is intended to reduce any code work of C++ on addons and to have them faster
+/// to use.
+///
+/// His code uses the support of platform-independent thread system introduced
+/// with C++11.
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/tools/Thread.h>
+/// #include <kodi/AddonBase.h>
+///
+/// class ATTR_DLL_LOCAL CTestAddon
+/// : public kodi::addon::CAddonBase,
+/// public kodi::tools::CThread
+/// {
+/// public:
+/// CTestAddon() = default;
+///
+/// ADDON_STATUS Create() override;
+///
+/// void Process() override;
+/// };
+///
+/// ADDON_STATUS CTestAddon::Create()
+/// {
+/// kodi::Log(ADDON_LOG_INFO, "Starting thread");
+/// CreateThread();
+///
+/// Sleep(4000);
+///
+/// kodi::Log(ADDON_LOG_INFO, "Stopping thread");
+/// // This added as example and also becomes stopped by class destructor
+/// StopThread();
+///
+/// return ADDON_STATUS_OK;
+/// }
+///
+/// void CTestAddon::Process()
+/// {
+/// kodi::Log(ADDON_LOG_INFO, "Thread started");
+///
+/// while (!m_threadStop)
+/// {
+/// kodi::Log(ADDON_LOG_INFO, "Hello World");
+/// Sleep(1000);
+/// }
+///
+/// kodi::Log(ADDON_LOG_INFO, "Thread ended");
+/// }
+///
+/// ADDONCREATOR(CTestAddon)
+/// ~~~~~~~~~~~~~
+///
+///@{
+class CThread
+{
+public:
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CThread
+ /// @brief Class constructor.
+ ///
+ CThread() : m_threadStop(false) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CThread
+ /// @brief Class destructor.
+ ///
+ virtual ~CThread()
+ {
+ StopThread();
+ if (m_thread != nullptr)
+ {
+ m_thread->detach();
+ delete m_thread;
+ }
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CThread
+ /// @brief Check auto delete is enabled on this thread class.
+ ///
+ /// @return true if auto delete is used, false otherwise
+ ///
+ bool IsAutoDelete() const { return m_autoDelete; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CThread
+ /// @brief Check caller is on this running thread.
+ ///
+ /// @return true if called from thread inside the class, false if from another
+ /// thread
+ ///
+ bool IsCurrentThread() const { return m_threadId == std::this_thread::get_id(); }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CThread
+ /// @brief Check thread inside this class is running and active.
+ ///
+ /// @note This function should be used from outside and not within process to
+ /// check thread is active. Use use atomic bool @ref m_threadStop for this.
+ ///
+ /// @return true if running, false if not
+ ///
+ bool IsRunning() const
+ {
+ if (m_thread != nullptr)
+ {
+ // it's possible that the thread exited on it's own without a call to StopThread. If so then
+ // the promise should be fulfilled.
+ std::future_status stat = m_future.wait_for(std::chrono::milliseconds(0));
+ // a status of 'ready' means the future contains the value so the thread has exited
+ // since the thread can't exit without setting the future.
+ if (stat == std::future_status::ready) // this is an indication the thread has exited.
+ return false;
+ return true; // otherwise the thread is still active.
+ }
+ else
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CThread
+ /// @brief Create a new thread defined by this class on child.
+ ///
+ /// This starts then @ref Process() where is available on the child by addon.
+ ///
+ /// @param[in] autoDelete To set thread to delete itself after end, default is
+ /// false
+ ///
+ void CreateThread(bool autoDelete = false)
+ {
+ if (m_thread != nullptr)
+ {
+ // if the thread exited on it's own, without a call to StopThread, then we can get here
+ // incorrectly. We should be able to determine this by checking the promise.
+ std::future_status stat = m_future.wait_for(std::chrono::milliseconds(0));
+ // a status of 'ready' means the future contains the value so the thread has exited
+ // since the thread can't exit without setting the future.
+ if (stat == std::future_status::ready) // this is an indication the thread has exited.
+ StopThread(true); // so let's just clean up
+ else
+ { // otherwise we have a problem.
+ kodi::Log(ADDON_LOG_FATAL, "%s - fatal error creating thread - old thread id not null",
+ __func__);
+ exit(1);
+ }
+ }
+
+ m_autoDelete = autoDelete;
+ m_threadStop = false;
+ m_startEvent.notify_all();
+ m_stopEvent.notify_all();
+
+ std::promise<bool> prom;
+ m_future = prom.get_future();
+
+ {
+ // The std::thread internals must be set prior to the lambda doing
+ // any work. This will cause the lambda to wait until m_thread
+ // is fully initialized. Interestingly, using a std::atomic doesn't
+ // have the appropriate memory barrier behavior to accomplish the
+ // same thing so a full system mutex needs to be used.
+ std::unique_lock<std::recursive_mutex> lock(m_threadMutex);
+ m_thread = new std::thread(
+ [](CThread* thread, std::promise<bool> promise) {
+ try
+ {
+ {
+ // Wait for the pThread->m_thread internals to be set. Otherwise we could
+ // get to a place where we're reading, say, the thread id inside this
+ // lambda's call stack prior to the thread that kicked off this lambda
+ // having it set. Once this lock is released, the CThread::Create function
+ // that kicked this off is done so everything should be set.
+ std::unique_lock<std::recursive_mutex> lock(thread->m_threadMutex);
+ }
+
+ thread->m_threadId = std::this_thread::get_id();
+ std::stringstream ss;
+ ss << thread->m_threadId;
+ std::string id = ss.str();
+ bool autodelete = thread->m_autoDelete;
+
+ kodi::Log(ADDON_LOG_DEBUG, "Thread %s start, auto delete: %s", id.c_str(),
+ (autodelete ? "true" : "false"));
+
+ thread->m_running = true;
+ thread->m_startEvent.notify_one();
+
+ thread->Process();
+
+ if (autodelete)
+ {
+ kodi::Log(ADDON_LOG_DEBUG, "Thread %s terminating (autodelete)", id.c_str());
+ delete thread;
+ thread = nullptr;
+ }
+ else
+ kodi::Log(ADDON_LOG_DEBUG, "Thread %s terminating", id.c_str());
+ }
+ catch (const std::exception& e)
+ {
+ kodi::Log(ADDON_LOG_DEBUG, "Thread Terminating with Exception: %s", e.what());
+ }
+ catch (...)
+ {
+ kodi::Log(ADDON_LOG_DEBUG, "Thread Terminating with Exception");
+ }
+
+ promise.set_value(true);
+ },
+ this, std::move(prom));
+
+ m_startEvent.wait(lock);
+ }
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CThread
+ /// @brief Stop a running thread.
+ ///
+ /// @param[in] wait As true (default) to wait until thread is finished and
+ /// stopped, as false the function return directly and thread
+ /// becomes independently stopped.
+ ///
+ void StopThread(bool wait = true)
+ {
+ std::unique_lock<std::recursive_mutex> lock(m_threadMutex);
+
+ if (m_threadStop)
+ return;
+
+ if (m_thread && !m_running)
+ m_startEvent.wait(lock);
+ m_running = false;
+ m_threadStop = true;
+ m_stopEvent.notify_one();
+
+ std::thread* lthread = m_thread;
+ if (lthread != nullptr && wait && !IsCurrentThread())
+ {
+ lock.unlock();
+ if (lthread->joinable())
+ lthread->join();
+ delete m_thread;
+ m_thread = nullptr;
+ m_threadId = std::thread::id();
+ }
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CThread
+ /// @brief Thread sleep with given amount of milliseconds.
+ ///
+ /// This makes a sleep in the thread with a given time value. If it is called
+ /// within the process itself, it is also checked whether the thread is
+ /// terminated and the sleep process is thereby interrupted.
+ ///
+ /// If the external point calls this, only a regular sleep is used, which runs
+ /// through completely.
+ ///
+ /// @param[in] milliseconds Time to sleep
+ ///
+ void Sleep(uint32_t milliseconds)
+ {
+ if (milliseconds > 10 && IsCurrentThread())
+ {
+ std::unique_lock<std::recursive_mutex> lock(m_threadMutex);
+ m_stopEvent.wait_for(lock, std::chrono::milliseconds(milliseconds));
+ }
+ else
+ {
+ std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
+ }
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CThread
+ /// @brief The function returns when the thread execution has completed or
+ /// timing is reached in milliseconds beforehand
+ ///
+ /// This synchronizes the moment this function returns with the completion of
+ /// all operations on the thread.
+ ///
+ /// @param[in] milliseconds Time to wait for join
+ ///
+ bool Join(unsigned int milliseconds)
+ {
+ std::unique_lock<std::recursive_mutex> lock(m_threadMutex);
+ std::thread* lthread = m_thread;
+ if (lthread != nullptr)
+ {
+ if (IsCurrentThread())
+ return false;
+
+ {
+ m_threadMutex.unlock(); // don't hold the thread lock while we're waiting
+ std::future_status stat = m_future.wait_for(std::chrono::milliseconds(milliseconds));
+ if (stat != std::future_status::ready)
+ return false;
+ m_threadMutex.lock();
+ }
+
+ // it's possible it's already joined since we released the lock above.
+ if (lthread->joinable())
+ m_thread->join();
+ return true;
+ }
+ else
+ return false;
+ }
+ //----------------------------------------------------------------------------
+
+protected:
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CThread
+ /// @brief The function to be added by the addon as a child to carry out the
+ /// process thread.
+ ///
+ /// Use @ref m_threadStop to check about active of thread and want stopped from
+ /// external place.
+ ///
+ /// @note This function is necessary and must be implemented by the addon.
+ ///
+ virtual void Process() = 0;
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CThread
+ /// @brief Atomic bool to indicate thread is active.
+ ///
+ /// This should be used in @ref Process() to check the activity of the thread and,
+ /// if true, to terminate the process.
+ ///
+ /// - <b>`false`</b>: Thread active and should be run
+ /// - <b>`true`</b>: Thread ends and should be stopped
+ ///
+ std::atomic<bool> m_threadStop;
+ //----------------------------------------------------------------------------
+
+private:
+ bool m_autoDelete = false;
+ bool m_running = false;
+ std::condition_variable_any m_stopEvent;
+ std::condition_variable_any m_startEvent;
+ std::recursive_mutex m_threadMutex;
+ std::thread::id m_threadId;
+ std::thread* m_thread = nullptr;
+ std::future<bool> m_future;
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace tools */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/tools/Timer.h b/xbmc/addons/kodi-dev-kit/include/kodi/tools/Timer.h
new file mode 100644
index 0000000..3214e67
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/tools/Timer.h
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2005-2020 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
+
+#ifdef __cplusplus
+
+#include "Thread.h"
+
+#include <functional>
+
+namespace kodi
+{
+namespace tools
+{
+
+//==============================================================================
+/// @defgroup cpp_kodi_tools_CTimer class CTimer
+/// @ingroup cpp_kodi_tools
+/// @brief **Time interval management**\n
+/// Class which enables a time interval to be called up by a given function or
+/// class by means of a thread.
+///
+/// His code uses the support of platform-independent thread system introduced
+/// with C++11.
+///
+///
+/// ----------------------------------------------------------------------------
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.cpp}
+/// #include <kodi/tools/Timer.h>
+///
+/// class ATTR_DLL_LOCAL CExample
+/// {
+/// public:
+/// CExample() : m_timer([this](){TimerCall();})
+/// {
+/// m_timer.Start(5000, true); // let call continuously all 5 seconds
+/// }
+///
+/// void TimerCall()
+/// {
+/// fprintf(stderr, "Hello World\n");
+/// }
+///
+/// private:
+/// kodi::tools::CTimer m_timer;
+/// };
+/// ~~~~~~~~~~~~~
+///
+///@{
+class CTimer : protected CThread
+{
+public:
+ class ITimerCallback;
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CTimer
+ /// @brief Class constructor to pass individual other class as callback.
+ ///
+ /// @param[in] callback Child class of parent @ref ITimerCallback with
+ /// implemented function @ref ITimerCallback::OnTimeout().
+ ///
+ explicit CTimer(kodi::tools::CTimer::ITimerCallback* callback)
+ : CTimer(std::bind(&ITimerCallback::OnTimeout, callback))
+ {
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CTimer
+ /// @brief Class constructor to pass individual function as callback.
+ ///
+ /// @param[in] callback Function to pass as callback about timeout.
+ ///
+ /// **Callback function style:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// void TimerCallback()
+ /// {
+ /// }
+ /// ~~~~~~~~~~~~~
+ explicit CTimer(std::function<void()> const& callback) : m_callback(callback) {}
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CTimer
+ /// @brief Class destructor.
+ ///
+ ~CTimer() override { Stop(true); }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CTimer
+ /// @brief Start the timer by given time in milliseconds to make his call
+ /// by arrive of them.
+ ///
+ /// If interval is activated, it calls the associated callback function
+ /// continuously in the given interval.
+ ///
+ /// @param[in] timeout Timeout in milliseconds
+ /// @param[in] interval [opt] To run continuously if true, false only one time
+ /// and default
+ /// @return True if successfully done, false if not (callback missing,
+ /// timeout = 0 or was already running.
+ ///
+ bool Start(uint64_t timeout, bool interval = false)
+ {
+ using namespace std::chrono;
+
+ if (m_callback == nullptr || timeout == 0 || IsRunning())
+ return false;
+
+ m_timeout = milliseconds(timeout);
+ m_interval = interval;
+
+ CreateThread();
+ return true;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CTimer
+ /// @brief Stop the timer if it is active.
+ ///
+ /// @param[in] wait [opt] Wait until timer is stopped, false is default and
+ /// call unblocked
+ /// @return True if timer was active and was stopped, false if already was
+ /// stopped.
+ ///
+ bool Stop(bool wait = false)
+ {
+ if (!IsRunning())
+ return false;
+
+ m_threadStop = true;
+ m_eventTimeout.notify_all();
+ StopThread(wait);
+
+ return true;
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CTimer
+ /// @brief Restart timer complete by stop and restart his thread again.
+ ///
+ /// @note Restart only possible as long the timer was running and not done his
+ /// work.
+ ///
+ /// @return True if start was successfully done, on error, or if was already
+ /// finished returned as false
+ ///
+ bool Restart()
+ {
+ using namespace std::chrono;
+
+ if (!IsRunning())
+ return false;
+
+ Stop(true);
+ return Start(duration_cast<milliseconds>(m_timeout).count(), m_interval);
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CTimer
+ /// @brief Restart the timer with new timeout without touch of his thread.
+ ///
+ /// @param[in] timeout Time as milliseconds to wait for next call
+ ///
+ void RestartAsync(uint64_t timeout)
+ {
+ using namespace std::chrono;
+
+ m_timeout = milliseconds(timeout);
+ const auto now = system_clock::now();
+ m_endTime = now.time_since_epoch() + m_timeout;
+ m_eventTimeout.notify_all();
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CTimer
+ /// @brief Check timer is still active to wait for next call.
+ ///
+ /// @return True if active, false if all his work done and no more running
+ ///
+ bool IsRunning() const { return CThread::IsRunning(); }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CTimer
+ /// @brief Get elapsed time as floating point of timer as seconds.
+ ///
+ /// @return Elapsed time
+ ///
+ float GetElapsedSeconds() const { return GetElapsedMilliseconds() / 1000.0f; }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @ingroup cpp_kodi_tools_CTimer
+ /// @brief Get elapsed time as floating point of timer as milliseconds.
+ ///
+ /// @return Elapsed time
+ ///
+ float GetElapsedMilliseconds() const
+ {
+ using namespace std::chrono;
+
+ if (!IsRunning())
+ return 0.0f;
+
+ const auto now = system_clock::now();
+ return static_cast<float>(duration_cast<milliseconds>(now.time_since_epoch() - (m_endTime - m_timeout)).count());
+ }
+ //----------------------------------------------------------------------------
+
+ //============================================================================
+ /// @defgroup cpp_kodi_tools_CTimer_CB class ITimerCallback
+ /// @ingroup cpp_kodi_tools_CTimer
+ /// @brief **Callback class of timer**\n
+ /// To give on constructor by @ref CTimer(kodi::tools::CTimer::ITimerCallback* callback)
+ ///
+ class ITimerCallback
+ {
+ public:
+ //==========================================================================
+ /// @ingroup cpp_kodi_tools_CTimer_CB
+ /// @brief Class destructor.
+ ///
+ virtual ~ITimerCallback() = default;
+ //--------------------------------------------------------------------------
+
+ //==========================================================================
+ /// @ingroup cpp_kodi_tools_CTimer_CB
+ /// @brief Callback function to implement if constructor @ref CTimer(kodi::tools::CTimer::ITimerCallback* callback)
+ /// is used and this as parent on related class
+ ///
+ /// ----------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.cpp}
+ /// #include <kodi/tools/Timer.h>
+ ///
+ /// class CExample : public kodi::tools::CTimer,
+ /// private kodi::tools::CTimer::ITimerCallback
+ /// {
+ /// public:
+ /// CExample() : kodi::tools::CTimer(this)
+ /// {
+ /// }
+ ///
+ /// void OnTimeout() override
+ /// {
+ /// // Some work
+ /// }
+ /// };
+ ///
+ /// ~~~~~~~~~~~~~
+ ///
+ virtual void OnTimeout() = 0;
+ //--------------------------------------------------------------------------
+ };
+ //----------------------------------------------------------------------------
+
+protected:
+ void Process() override
+ {
+ using namespace std::chrono;
+
+ while (!m_threadStop)
+ {
+ auto currentTime = system_clock::now();
+ m_endTime = currentTime.time_since_epoch() + m_timeout;
+
+ // wait the necessary time
+ std::mutex mutex;
+ std::unique_lock<std::mutex> lock(mutex);
+ const auto waitTime = duration_cast<milliseconds>(m_endTime - currentTime.time_since_epoch());
+ if (m_eventTimeout.wait_for(lock, waitTime) == std::cv_status::timeout)
+ {
+ currentTime = system_clock::now();
+ if (m_endTime.count() <= currentTime.time_since_epoch().count())
+ {
+ // execute OnTimeout() callback
+ m_callback();
+
+ // continue if this is an interval timer, or if it was restarted during callback
+ if (!m_interval && m_endTime.count() <= currentTime.time_since_epoch().count())
+ break;
+ }
+ }
+ }
+ }
+
+private:
+ bool m_interval = false;
+ std::function<void()> m_callback;
+ std::chrono::system_clock::duration m_timeout;
+ std::chrono::system_clock::duration m_endTime;
+ std::condition_variable_any m_eventTimeout;
+};
+///@}
+//------------------------------------------------------------------------------
+
+} /* namespace tools */
+} /* namespace kodi */
+
+#endif /* __cplusplus */
diff --git a/xbmc/addons/kodi-dev-kit/include/kodi/versions.h b/xbmc/addons/kodi-dev-kit/include/kodi/versions.h
new file mode 100644
index 0000000..c7f11c6
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/include/kodi/versions.h
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2016-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
+
+#ifndef KODI_VERSIONS_H
+#define KODI_VERSIONS_H
+
+#include <string.h>
+
+#define STR_HELPER(x) #x
+#define STR(x) STR_HELPER(x)
+
+/*
+ *------------------------------------------------------------------------------
+ * Some parts on headers are only be used for Kodi itself and internally (not
+ * for add-on development).
+ *
+ * For this reason also no doxygen part with "///" defined there.
+ * -----------------------------------------------------------------------------
+ */
+
+/*
+ * Versions of all add-on globals and instances are defined below.
+ *
+ * This is added here and not in related header to prevent not
+ * needed includes during compile. Also have it here a better
+ * overview.
+ */
+
+// Ignore clang here, as this must be good in overview and as the main reason,
+// because cmake uses this area in this form to perform its addon dependency
+// check.
+// clang-format off
+#define ADDON_GLOBAL_VERSION_MAIN "2.0.2"
+#define ADDON_GLOBAL_VERSION_MAIN_MIN "2.0.0"
+#define ADDON_GLOBAL_VERSION_MAIN_XML_ID "kodi.binary.global.main"
+#define ADDON_GLOBAL_VERSION_MAIN_DEPENDS "AddonBase.h" \
+ "addon-instance/" \
+ "c-api/addon_base.h"
+
+#define ADDON_GLOBAL_VERSION_GENERAL "1.0.5"
+#define ADDON_GLOBAL_VERSION_GENERAL_MIN "1.0.4"
+#define ADDON_GLOBAL_VERSION_GENERAL_XML_ID "kodi.binary.global.general"
+#define ADDON_GLOBAL_VERSION_GENERAL_DEPENDS "General.h"
+
+#define ADDON_GLOBAL_VERSION_GUI "5.15.0"
+#define ADDON_GLOBAL_VERSION_GUI_MIN "5.15.0"
+#define ADDON_GLOBAL_VERSION_GUI_XML_ID "kodi.binary.global.gui"
+#define ADDON_GLOBAL_VERSION_GUI_DEPENDS "c-api/gui/input/action_ids.h" \
+ "c-api/gui/" \
+ "gui/"
+
+#define ADDON_GLOBAL_VERSION_AUDIOENGINE "1.1.1"
+#define ADDON_GLOBAL_VERSION_AUDIOENGINE_MIN "1.1.0"
+#define ADDON_GLOBAL_VERSION_AUDIOENGINE_XML_ID "kodi.binary.global.audioengine"
+#define ADDON_GLOBAL_VERSION_AUDIOENGINE_DEPENDS "AudioEngine.h" \
+ "c-api/audio_engine.h"
+
+#define ADDON_GLOBAL_VERSION_FILESYSTEM "1.1.8"
+#define ADDON_GLOBAL_VERSION_FILESYSTEM_MIN "1.1.7"
+#define ADDON_GLOBAL_VERSION_FILESYSTEM_XML_ID "kodi.binary.global.filesystem"
+#define ADDON_GLOBAL_VERSION_FILESYSTEM_DEPENDS "Filesystem.h" \
+ "c-api/filesystem.h" \
+ "gui/gl/Shader.h" \
+ "tools/DllHelper.h"
+
+#define ADDON_GLOBAL_VERSION_NETWORK "1.0.4"
+#define ADDON_GLOBAL_VERSION_NETWORK_MIN "1.0.0"
+#define ADDON_GLOBAL_VERSION_NETWORK_XML_ID "kodi.binary.global.network"
+#define ADDON_GLOBAL_VERSION_NETWORK_DEPENDS "Network.h" \
+ "c-api/network.h"
+
+#define ADDON_GLOBAL_VERSION_TOOLS "1.0.4"
+#define ADDON_GLOBAL_VERSION_TOOLS_MIN "1.0.0"
+#define ADDON_GLOBAL_VERSION_TOOLS_XML_ID "kodi.binary.global.tools"
+#define ADDON_GLOBAL_VERSION_TOOLS_DEPENDS "tools/DllHelper.h" \
+ "tools/EndTime.h" \
+ "tools/StringUtils.h" \
+ "tools/Thread.h" \
+ "tools/Timer.h"
+
+#define ADDON_INSTANCE_VERSION_AUDIODECODER "4.0.0"
+#define ADDON_INSTANCE_VERSION_AUDIODECODER_MIN "4.0.0"
+#define ADDON_INSTANCE_VERSION_AUDIODECODER_XML_ID "kodi.binary.instance.audiodecoder"
+#define ADDON_INSTANCE_VERSION_AUDIODECODER_DEPENDS "c-api/addon-instance/audiodecoder.h" \
+ "addon-instance/AudioDecoder.h"
+
+#define ADDON_INSTANCE_VERSION_AUDIOENCODER "3.0.0"
+#define ADDON_INSTANCE_VERSION_AUDIOENCODER_MIN "3.0.0"
+#define ADDON_INSTANCE_VERSION_AUDIOENCODER_XML_ID "kodi.binary.instance.audioencoder"
+#define ADDON_INSTANCE_VERSION_AUDIOENCODER_DEPENDS "c-api/addon-instance/audioencoder.h" \
+ "addon-instance/AudioEncoder.h"
+
+#define ADDON_INSTANCE_VERSION_GAME "3.0.0"
+#define ADDON_INSTANCE_VERSION_GAME_MIN "3.0.0"
+#define ADDON_INSTANCE_VERSION_GAME_XML_ID "kodi.binary.instance.game"
+#define ADDON_INSTANCE_VERSION_GAME_DEPENDS "addon-instance/Game.h"
+
+#define ADDON_INSTANCE_VERSION_IMAGEDECODER "3.0.1"
+#define ADDON_INSTANCE_VERSION_IMAGEDECODER_MIN "3.0.0"
+#define ADDON_INSTANCE_VERSION_IMAGEDECODER_XML_ID "kodi.binary.instance.imagedecoder"
+#define ADDON_INSTANCE_VERSION_IMAGEDECODER_DEPENDS "c-api/addon-instance/imagedecoder.h" \
+ "addon-instance/ImageDecoder.h"
+
+#define ADDON_INSTANCE_VERSION_INPUTSTREAM "3.2.0"
+#define ADDON_INSTANCE_VERSION_INPUTSTREAM_MIN "3.2.0"
+#define ADDON_INSTANCE_VERSION_INPUTSTREAM_XML_ID "kodi.binary.instance.inputstream"
+#define ADDON_INSTANCE_VERSION_INPUTSTREAM_DEPENDS "c-api/addon-instance/inputstream.h" \
+ "c-api/addon-instance/inputstream/demux_packet.h" \
+ "c-api/addon-instance/inputstream/stream_codec.h" \
+ "c-api/addon-instance/inputstream/stream_constants.h" \
+ "c-api/addon-instance/inputstream/stream_crypto.h" \
+ "c-api/addon-instance/inputstream/timing_constants.h" \
+ "addon-instance/Inputstream.h" \
+ "addon-instance/inputstream/DemuxPacket.h" \
+ "addon-instance/inputstream/StreamCodec.h" \
+ "addon-instance/inputstream/StreamConstants.h" \
+ "addon-instance/inputstream/StreamCrypto.h" \
+ "addon-instance/inputstream/TimingConstants.h"
+
+#define ADDON_INSTANCE_VERSION_PERIPHERAL "2.0.0"
+#define ADDON_INSTANCE_VERSION_PERIPHERAL_MIN "2.0.0"
+#define ADDON_INSTANCE_VERSION_PERIPHERAL_XML_ID "kodi.binary.instance.peripheral"
+#define ADDON_INSTANCE_VERSION_PERIPHERAL_DEPENDS "addon-instance/Peripheral.h" \
+ "addon-instance/PeripheralUtils.h"
+
+#define ADDON_INSTANCE_VERSION_PVR "8.2.0"
+#define ADDON_INSTANCE_VERSION_PVR_MIN "8.2.0"
+#define ADDON_INSTANCE_VERSION_PVR_XML_ID "kodi.binary.instance.pvr"
+#define ADDON_INSTANCE_VERSION_PVR_DEPENDS "c-api/addon-instance/pvr.h" \
+ "c-api/addon-instance/pvr/pvr_providers.h" \
+ "c-api/addon-instance/pvr/pvr_channel_groups.h" \
+ "c-api/addon-instance/pvr/pvr_channels.h" \
+ "c-api/addon-instance/pvr/pvr_defines.h" \
+ "c-api/addon-instance/pvr/pvr_edl.h" \
+ "c-api/addon-instance/pvr/pvr_epg.h" \
+ "c-api/addon-instance/pvr/pvr_general.h" \
+ "c-api/addon-instance/pvr/pvr_menu_hook.h" \
+ "c-api/addon-instance/pvr/pvr_recordings.h" \
+ "c-api/addon-instance/pvr/pvr_stream.h" \
+ "c-api/addon-instance/pvr/pvr_timers.h" \
+ "addon-instance/PVR.h" \
+ "addon-instance/pvr/ChannelGroups.h" \
+ "addon-instance/pvr/Channels.h" \
+ "addon-instance/pvr/EDL.h" \
+ "addon-instance/pvr/EPG.h" \
+ "addon-instance/pvr/General.h" \
+ "addon-instance/pvr/MenuHook.h" \
+ "addon-instance/pvr/Recordings.h" \
+ "addon-instance/pvr/Stream.h" \
+ "addon-instance/pvr/Timers.h"
+
+#define ADDON_INSTANCE_VERSION_SCREENSAVER "2.2.0"
+#define ADDON_INSTANCE_VERSION_SCREENSAVER_MIN "2.2.0"
+#define ADDON_INSTANCE_VERSION_SCREENSAVER_XML_ID "kodi.binary.instance.screensaver"
+#define ADDON_INSTANCE_VERSION_SCREENSAVER_DEPENDS "c-api/addon-instance/screensaver.h" \
+ "addon-instance/Screensaver.h"
+
+#define ADDON_INSTANCE_VERSION_VFS "3.0.1"
+#define ADDON_INSTANCE_VERSION_VFS_MIN "3.0.1"
+#define ADDON_INSTANCE_VERSION_VFS_XML_ID "kodi.binary.instance.vfs"
+#define ADDON_INSTANCE_VERSION_VFS_DEPENDS "c-api/addon-instance/vfs.h" \
+ "addon-instance/VFS.h"
+
+#define ADDON_INSTANCE_VERSION_VISUALIZATION "4.0.0"
+#define ADDON_INSTANCE_VERSION_VISUALIZATION_MIN "4.0.0"
+#define ADDON_INSTANCE_VERSION_VISUALIZATION_XML_ID "kodi.binary.instance.visualization"
+#define ADDON_INSTANCE_VERSION_VISUALIZATION_DEPENDS "addon-instance/Visualization.h" \
+ "c-api/addon-instance/visualization.h"
+
+#define ADDON_INSTANCE_VERSION_VIDEOCODEC "2.0.3"
+#define ADDON_INSTANCE_VERSION_VIDEOCODEC_MIN "2.0.1"
+#define ADDON_INSTANCE_VERSION_VIDEOCODEC_XML_ID "kodi.binary.instance.videocodec"
+#define ADDON_INSTANCE_VERSION_VIDEOCODEC_DEPENDS "c-api/addon-instance/video_codec.h" \
+ "c-api/addon-instance/inputstream/stream_codec.h" \
+ "c-api/addon-instance/inputstream/stream_crypto.h" \
+ "addon-instance/VideoCodec.h" \
+ "addon-instance/inputstream/StreamCodec.h" \
+ "addon-instance/inputstream/StreamCrypto.h" \
+// clang-format on
+
+//==============================================================================
+/// @ingroup cpp_kodi_addon_addonbase_Defs
+/// @defgroup cpp_kodi_addon_addonbase_Defs_ADDON_TYPE enum ADDON_TYPE
+/// **The currently available instance types for Kodi add-ons**\n
+/// This identifies the associated API part on the interface in Kodi and in the
+/// addon.
+///
+/// \internal
+/// @note For add of new types take a new number on end. To change
+/// existing numbers can be make problems on already compiled add-ons.
+/// \endinternal
+///
+///@{
+typedef enum ADDON_TYPE
+{
+ /* addon global parts */
+ ADDON_GLOBAL_MAIN = 0,
+ ADDON_GLOBAL_GUI = 1,
+ ADDON_GLOBAL_AUDIOENGINE = 2,
+ ADDON_GLOBAL_GENERAL = 3,
+ ADDON_GLOBAL_NETWORK = 4,
+ ADDON_GLOBAL_FILESYSTEM = 5,
+ ADDON_GLOBAL_TOOLS = 6,
+ // Last used global id, used in loops to check versions.
+ // Need to change if new global type becomes added!
+ ADDON_GLOBAL_MAX = 6,
+
+ /* addon type instances */
+
+ /// Audio decoder instance, see @ref cpp_kodi_addon_audiodecoder "kodi::addon::CInstanceAudioDecoder"
+ ADDON_INSTANCE_AUDIODECODER = 102,
+
+ /// Audio encoder instance, see @ref cpp_kodi_addon_audioencoder "kodi::addon::CInstanceAudioEncoder"
+ ADDON_INSTANCE_AUDIOENCODER = 103,
+
+ /// Game instance, see @ref cpp_kodi_addon_game "kodi::addon::CInstanceGame"
+ ADDON_INSTANCE_GAME = 104,
+
+ /// Input stream instance, see @ref cpp_kodi_addon_inputstream "kodi::addon::CInstanceInputStream"
+ ADDON_INSTANCE_INPUTSTREAM = 105,
+
+ /// Peripheral instance, see @ref cpp_kodi_addon_peripheral "kodi::addon::CInstancePeripheral"
+ ADDON_INSTANCE_PERIPHERAL = 106,
+
+ /// Game instance, see @ref cpp_kodi_addon_pvr "kodi::addon::CInstancePVRClient"
+ ADDON_INSTANCE_PVR = 107,
+
+ /// PVR client instance, see @ref cpp_kodi_addon_screensaver "kodi::addon::CInstanceScreensaver"
+ ADDON_INSTANCE_SCREENSAVER = 108,
+
+ /// Music visualization instance, see @ref cpp_kodi_addon_visualization "kodi::addon::CInstanceVisualization"
+ ADDON_INSTANCE_VISUALIZATION = 109,
+
+ /// Virtual Filesystem (VFS) instance, see @ref cpp_kodi_addon_vfs "kodi::addon::CInstanceVFS"
+ ADDON_INSTANCE_VFS = 110,
+
+ /// Image Decoder instance, see @ref cpp_kodi_addon_imagedecoder "kodi::addon::CInstanceImageDecoder"
+ ADDON_INSTANCE_IMAGEDECODER = 111,
+
+ /// Video Decoder instance, see @ref cpp_kodi_addon_videocodec "kodi::addon::CInstanceVideoCodec"
+ ADDON_INSTANCE_VIDEOCODEC = 112,
+} ADDON_TYPE;
+///@}
+//------------------------------------------------------------------------------
+
+#ifdef __cplusplus
+extern "C" {
+namespace kodi {
+namespace addon {
+#endif
+
+///
+/// This is used from Kodi to get the active version of add-on parts.
+/// It is compiled in add-on and also in Kodi itself, with this can be Kodi
+/// compare the version from him with them on add-on.
+///
+/// @param[in] type The with 'enum ADDON_TYPE' type to ask
+/// @return version The current version of asked type
+///
+inline const char* GetTypeVersion(int type)
+{
+ /*
+ * #ifdef's below becomes set by cmake, no set by hand needed.
+ */
+ switch (type)
+ {
+ /* addon global parts */
+ case ADDON_GLOBAL_MAIN:
+ return ADDON_GLOBAL_VERSION_MAIN;
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_GLOBAL_VERSION_GENERAL_USED)
+ case ADDON_GLOBAL_GENERAL:
+ return ADDON_GLOBAL_VERSION_GENERAL;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_GLOBAL_VERSION_GUI_USED)
+ case ADDON_GLOBAL_GUI:
+ return ADDON_GLOBAL_VERSION_GUI;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_GLOBAL_VERSION_AUDIOENGINE_USED)
+ case ADDON_GLOBAL_AUDIOENGINE:
+ return ADDON_GLOBAL_VERSION_AUDIOENGINE;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_GLOBAL_VERSION_FILESYSTEM_USED)
+ case ADDON_GLOBAL_FILESYSTEM:
+ return ADDON_GLOBAL_VERSION_FILESYSTEM;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_GLOBAL_VERSION_NETWORK_USED)
+ case ADDON_GLOBAL_NETWORK:
+ return ADDON_GLOBAL_VERSION_NETWORK;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_GLOBAL_VERSION_TOOLS_USED)
+ case ADDON_GLOBAL_TOOLS:
+ return ADDON_GLOBAL_VERSION_TOOLS;
+#endif
+
+ /* addon type instances */
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_INSTANCE_VERSION_AUDIODECODER_USED)
+ case ADDON_INSTANCE_AUDIODECODER:
+ return ADDON_INSTANCE_VERSION_AUDIODECODER;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_INSTANCE_VERSION_AUDIOENCODER_USED)
+ case ADDON_INSTANCE_AUDIOENCODER:
+ return ADDON_INSTANCE_VERSION_AUDIOENCODER;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_INSTANCE_VERSION_GAME_USED)
+ case ADDON_INSTANCE_GAME:
+ return ADDON_INSTANCE_VERSION_GAME;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_INSTANCE_VERSION_IMAGEDECODER_USED)
+ case ADDON_INSTANCE_IMAGEDECODER:
+ return ADDON_INSTANCE_VERSION_IMAGEDECODER;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_INSTANCE_VERSION_INPUTSTREAM_USED)
+ case ADDON_INSTANCE_INPUTSTREAM:
+ return ADDON_INSTANCE_VERSION_INPUTSTREAM;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_INSTANCE_VERSION_PERIPHERAL_USED)
+ case ADDON_INSTANCE_PERIPHERAL:
+ return ADDON_INSTANCE_VERSION_PERIPHERAL;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_INSTANCE_VERSION_PVR_USED)
+ case ADDON_INSTANCE_PVR:
+ return ADDON_INSTANCE_VERSION_PVR;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_INSTANCE_VERSION_SCREENSAVER_USED)
+ case ADDON_INSTANCE_SCREENSAVER:
+ return ADDON_INSTANCE_VERSION_SCREENSAVER;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_INSTANCE_VERSION_VFS_USED)
+ case ADDON_INSTANCE_VFS:
+ return ADDON_INSTANCE_VERSION_VFS;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_INSTANCE_VERSION_VISUALIZATION_USED)
+ case ADDON_INSTANCE_VISUALIZATION:
+ return ADDON_INSTANCE_VERSION_VISUALIZATION;
+#endif
+#if !defined(BUILD_KODI_ADDON) || defined(ADDON_INSTANCE_VERSION_VIDEOCODEC_USED)
+ case ADDON_INSTANCE_VIDEOCODEC:
+ return ADDON_INSTANCE_VERSION_VIDEOCODEC;
+#endif
+ }
+ return "0.0.0";
+}
+
+///
+/// This is used from Kodi to get the minimum supported version of add-on parts.
+/// It is compiled in add-on and also in Kodi itself, with this can be Kodi
+/// compare the version from him with them on add-on.
+///
+/// @param[in] type The with 'enum ADDON_TYPE' type to ask
+/// @return version The minimum version of asked type
+///
+inline const char* GetTypeMinVersion(int type)
+{
+ switch (type)
+ {
+ /* addon global parts */
+ case ADDON_GLOBAL_MAIN:
+ return ADDON_GLOBAL_VERSION_MAIN_MIN;
+ case ADDON_GLOBAL_GUI:
+ return ADDON_GLOBAL_VERSION_GUI_MIN;
+ case ADDON_GLOBAL_GENERAL:
+ return ADDON_GLOBAL_VERSION_GENERAL_MIN;
+ case ADDON_GLOBAL_AUDIOENGINE:
+ return ADDON_GLOBAL_VERSION_AUDIOENGINE_MIN;
+ case ADDON_GLOBAL_FILESYSTEM:
+ return ADDON_GLOBAL_VERSION_FILESYSTEM_MIN;
+ case ADDON_GLOBAL_NETWORK:
+ return ADDON_GLOBAL_VERSION_NETWORK_MIN;
+ case ADDON_GLOBAL_TOOLS:
+ return ADDON_GLOBAL_VERSION_TOOLS_MIN;
+
+ /* addon type instances */
+ case ADDON_INSTANCE_AUDIODECODER:
+ return ADDON_INSTANCE_VERSION_AUDIODECODER_MIN;
+ case ADDON_INSTANCE_AUDIOENCODER:
+ return ADDON_INSTANCE_VERSION_AUDIOENCODER_MIN;
+ case ADDON_INSTANCE_GAME:
+ return ADDON_INSTANCE_VERSION_GAME_MIN;
+ case ADDON_INSTANCE_IMAGEDECODER:
+ return ADDON_INSTANCE_VERSION_IMAGEDECODER_MIN;
+ case ADDON_INSTANCE_INPUTSTREAM:
+ return ADDON_INSTANCE_VERSION_INPUTSTREAM_MIN;
+ case ADDON_INSTANCE_PERIPHERAL:
+ return ADDON_INSTANCE_VERSION_PERIPHERAL_MIN;
+ case ADDON_INSTANCE_PVR:
+ return ADDON_INSTANCE_VERSION_PVR_MIN;
+ case ADDON_INSTANCE_SCREENSAVER:
+ return ADDON_INSTANCE_VERSION_SCREENSAVER_MIN;
+ case ADDON_INSTANCE_VFS:
+ return ADDON_INSTANCE_VERSION_VFS_MIN;
+ case ADDON_INSTANCE_VISUALIZATION:
+ return ADDON_INSTANCE_VERSION_VISUALIZATION_MIN;
+ case ADDON_INSTANCE_VIDEOCODEC:
+ return ADDON_INSTANCE_VERSION_VIDEOCODEC_MIN;
+ }
+ return "0.0.0";
+}
+
+///
+/// Function used internally on add-on and in Kodi itself to get name
+/// about given type.
+///
+/// @param[in] type The with 'enum ADDON_TYPE' defined type to ask
+/// @return Name of the asked instance type
+///
+inline const char* GetTypeName(int type)
+{
+ switch (type)
+ {
+ /* addon global parts */
+ case ADDON_GLOBAL_MAIN:
+ return "Addon";
+ case ADDON_GLOBAL_GUI:
+ return "GUI";
+ case ADDON_GLOBAL_GENERAL:
+ return "General";
+ case ADDON_GLOBAL_AUDIOENGINE:
+ return "AudioEngine";
+ case ADDON_GLOBAL_FILESYSTEM:
+ return "Filesystem";
+ case ADDON_GLOBAL_NETWORK:
+ return "Network";
+ case ADDON_GLOBAL_TOOLS:
+ return "Tools";
+
+ /* addon type instances */
+ case ADDON_INSTANCE_AUDIODECODER:
+ return "AudioDecoder";
+ case ADDON_INSTANCE_AUDIOENCODER:
+ return "AudioEncoder";
+ case ADDON_INSTANCE_GAME:
+ return "Game";
+ case ADDON_INSTANCE_IMAGEDECODER:
+ return "ImageDecoder";
+ case ADDON_INSTANCE_INPUTSTREAM:
+ return "Inputstream";
+ case ADDON_INSTANCE_PERIPHERAL:
+ return "Peripheral";
+ case ADDON_INSTANCE_PVR:
+ return "PVR";
+ case ADDON_INSTANCE_SCREENSAVER:
+ return "ScreenSaver";
+ case ADDON_INSTANCE_VISUALIZATION:
+ return "Visualization";
+ case ADDON_INSTANCE_VIDEOCODEC:
+ return "VideoCodec";
+ }
+ return "unknown";
+}
+
+///
+/// Function used internally on add-on and in Kodi itself to get id number
+/// about given type name.
+///
+/// @param[in] name The type name string to ask
+/// @return Id number of the asked instance type
+///
+/// @warning String must be lower case here!
+///
+inline int GetTypeId(const char* name)
+{
+ if (name)
+ {
+ if (strcmp(name, "addon") == 0)
+ return ADDON_GLOBAL_MAIN;
+ else if (strcmp(name, "general") == 0)
+ return ADDON_GLOBAL_GENERAL;
+ else if (strcmp(name, "gui") == 0)
+ return ADDON_GLOBAL_GUI;
+ else if (strcmp(name, "audioengine") == 0)
+ return ADDON_GLOBAL_AUDIOENGINE;
+ else if (strcmp(name, "filesystem") == 0)
+ return ADDON_GLOBAL_FILESYSTEM;
+ else if (strcmp(name, "network") == 0)
+ return ADDON_GLOBAL_NETWORK;
+ else if (strcmp(name, "tools") == 0)
+ return ADDON_GLOBAL_TOOLS;
+ else if (strcmp(name, "audiodecoder") == 0)
+ return ADDON_INSTANCE_AUDIODECODER;
+ else if (strcmp(name, "audioencoder") == 0)
+ return ADDON_INSTANCE_AUDIOENCODER;
+ else if (strcmp(name, "game") == 0)
+ return ADDON_INSTANCE_GAME;
+ else if (strcmp(name, "imagedecoder") == 0)
+ return ADDON_INSTANCE_IMAGEDECODER;
+ else if (strcmp(name, "inputstream") == 0)
+ return ADDON_INSTANCE_INPUTSTREAM;
+ else if (strcmp(name, "peripheral") == 0)
+ return ADDON_INSTANCE_PERIPHERAL;
+ else if (strcmp(name, "pvr") == 0)
+ return ADDON_INSTANCE_PVR;
+ else if (strcmp(name, "screensaver") == 0)
+ return ADDON_INSTANCE_SCREENSAVER;
+ else if (strcmp(name, "vfs") == 0)
+ return ADDON_INSTANCE_VFS;
+ else if (strcmp(name, "visualization") == 0)
+ return ADDON_INSTANCE_VISUALIZATION;
+ else if (strcmp(name, "videocodec") == 0)
+ return ADDON_INSTANCE_VIDEOCODEC;
+ }
+ return -1;
+}
+
+#ifdef __cplusplus
+} /* namespace addon */
+} /* namespace kodi */
+} /* extern "C" */
+#endif
+
+#endif /* !KODI_VERSIONS_H */
diff --git a/xbmc/addons/kodi-dev-kit/tools/code-generator/code_generator.py b/xbmc/addons/kodi-dev-kit/tools/code-generator/code_generator.py
new file mode 100755
index 0000000..06cf810
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/tools/code-generator/code_generator.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2021 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.
+
+KODI_DIR = "../../../../../"
+DEVKIT_DIR = "xbmc/addons/kodi-dev-kit"
+
+# Global includes
+from optparse import OptionParser
+
+# Own includes
+from src.commitChanges import *
+from src.generateCMake__CMAKE_TREEDATA_COMMON_addon_dev_kit_txt import *
+from src.generateCMake__XBMC_ADDONS_KODIDEVKIT_INCLUDE_KODI_allfiles import *
+from src.helper_Log import *
+
+# ===============================================================================
+def GenerateCMakeParts(options):
+ Log.PrintGroupStart("Generate cmake parts")
+
+ # Generate Kodi's cmake system related include files to find related parts
+ GenerateCMake__XBMC_ADDONS_KODIDEVKIT_INCLUDE_KODI_all_files(options)
+ GenerateCMake__CMAKE_TREEDATA_COMMON_addon_dev_kit_txt(options)
+
+
+# ===============================================================================
+if __name__ == "__main__":
+ # parse command-line options
+ disc = """\
+This utility autogenerate the interface between Kodi and addon.
+It is currently still in the initial phase and will be expanded in the future.
+"""
+ parser = OptionParser(description=disc)
+ parser.add_option(
+ "-f",
+ "--force",
+ action="store_true",
+ dest="force",
+ default=False,
+ help="Force the generation of auto code",
+ )
+ parser.add_option(
+ "-d",
+ "--debug",
+ action="store_true",
+ dest="debug",
+ default=False,
+ help="Add debug identifiers to generated files",
+ )
+ parser.add_option(
+ "-c",
+ "--commit",
+ action="store_true",
+ dest="commit",
+ default=False,
+ help="Create automatic a git commit about API changes (WARNING: No hand edits should be present before!)",
+ )
+ (options, args) = parser.parse_args()
+
+ Log.Init(options)
+ Log.PrintMainStart(options)
+
+ ##----------------------------------------------------------------------------
+ # CMake generation
+ GenerateCMakeParts(options)
+
+ ##----------------------------------------------------------------------------
+ # Commit GIT changes generation (only makes work if '-c' option is used)
+ if options.commit:
+ Log.PrintGroupStart("Git update")
+ CommitChanges(options)
diff --git a/xbmc/addons/kodi-dev-kit/tools/code-generator/src/commitChanges.py b/xbmc/addons/kodi-dev-kit/tools/code-generator/src/commitChanges.py
new file mode 100644
index 0000000..896cca5
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/tools/code-generator/src/commitChanges.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2021 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.
+
+# Own includes
+from code_generator import DEVKIT_DIR, KODI_DIR
+from .generateCMake__CMAKE_TREEDATA_COMMON_addon_dev_kit_txt import *
+from .generateCMake__XBMC_ADDONS_KODIDEVKIT_INCLUDE_KODI_allfiles import *
+from .helper_Log import *
+
+# Global includes
+import importlib, os, subprocess
+
+git_found = importlib.find_loader("git") is not None
+if git_found:
+ from git import Repo
+
+
+def CommitChanges(options):
+ """
+ Do a git commit of the automatically changed locations of the dev-kit.
+ """
+ if not options.commit:
+ return
+ if not git_found:
+ Log.PrintFatal(
+ 'Needed "GitPython" module not present! To make commits need them be installed.'
+ )
+ quit(1)
+
+ Log.PrintBegin("Perform GIT update check")
+ Log.PrintResult(Result.SEE_BELOW)
+
+ contains_devkit_change = False
+ contains_external_change = False
+ changes_list = []
+
+ # Hack place one to get also new added files
+ subprocess.run(["git", "add", "-A"], check=True, stdout=subprocess.PIPE).stdout
+
+ r = Repo(KODI_DIR)
+ for x in r.index.diff("HEAD"):
+ if GenerateCMake__XBMC_ADDONS_KODIDEVKIT_INCLUDE_KODI_all_files_RelatedCheck(
+ x.b_path
+ ):
+ Log.PrintBegin(" - Changed file {}".format(x.b_path))
+ contains_devkit_change = True
+ changes_list.append(x.b_path)
+ Log.PrintResult(Result.NEW if x.new_file else Result.UPDATE)
+ elif GenerateCMake__CMAKE_TREEDATA_COMMON_addon_dev_kit_txt_RelatedCheck(
+ x.b_path
+ ):
+ Log.PrintBegin(" - Changed file {}".format(x.b_path))
+ contains_devkit_change = True
+ changes_list.append(x.b_path)
+ Log.PrintResult(Result.NEW if x.new_file else Result.UPDATE)
+ else:
+ Log.PrintBegin(" - Changed file {}".format(x.b_path))
+ Log.PrintFollow(" (Not auto update related)")
+ Log.PrintResult(Result.IGNORED)
+ contains_external_change = True
+
+ # Hack place two to reset about before
+ subprocess.run(["git", "reset", "HEAD"], check=True, stdout=subprocess.PIPE).stdout
+
+ Log.PrintBegin("Perform GIT commit")
+ if contains_devkit_change:
+ # Add changed or added files to git
+ for x in changes_list:
+ r.index.add(x)
+
+ commit_msg = (
+ "[auto][addons] devkit script update ({})\n"
+ "\n"
+ "This commit automatic generated by script '{}/tools/code-generator.py'.\n"
+ "\n"
+ "{}".format(
+ datetime.utcnow().strftime("%d/%m/%Y %H:%M:%S"),
+ DEVKIT_DIR,
+ open(Log.log_file).read(),
+ )
+ )
+ r.index.commit(commit_msg)
+ Log.PrintFollow(" ( Commit SHA256: {})".format(str(r.head.reference.commit)))
+ Log.PrintResult(Result.OK)
+ else:
+ Log.PrintResult(Result.ALREADY_DONE)
diff --git a/xbmc/addons/kodi-dev-kit/tools/code-generator/src/generateCMake__CMAKE_TREEDATA_COMMON_addon_dev_kit_txt.py b/xbmc/addons/kodi-dev-kit/tools/code-generator/src/generateCMake__CMAKE_TREEDATA_COMMON_addon_dev_kit_txt.py
new file mode 100644
index 0000000..20ed447
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/tools/code-generator/src/generateCMake__CMAKE_TREEDATA_COMMON_addon_dev_kit_txt.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2021 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.
+
+# Own includes
+from code_generator import DEVKIT_DIR, KODI_DIR
+from .helper_Log import *
+
+# Global includes
+import glob, os, re
+
+
+def GenerateCMake__CMAKE_TREEDATA_COMMON_addon_dev_kit_txt_RelatedCheck(filename):
+ """
+ This function is called by git update to be able to assign changed files to the dev kit.
+ """
+ return True if filename == "cmake/treedata/common/addon_dev_kit.txt" else False
+
+
+def GenerateCMake__CMAKE_TREEDATA_COMMON_addon_dev_kit_txt(options):
+ """
+ This function generate the "cmake/treedata/common/addon_dev_kit.txt"
+ by scan of related directories to use for addon interface build.
+ """
+ gen_file = "cmake/treedata/common/addon_dev_kit.txt"
+
+ Log.PrintBegin("Check for {}".format(gen_file))
+
+ scan_dir = "{}{}/include/kodi/**/CMakeLists.txt".format(KODI_DIR, DEVKIT_DIR)
+ parts = "# Auto generated {}.\n" "# See {}/tools/code-generator.py.\n\n".format(
+ gen_file, DEVKIT_DIR
+ )
+
+ for entry in glob.glob(scan_dir, recursive=True):
+ cmake_dir = entry.replace(KODI_DIR, "").replace("/CMakeLists.txt", "")
+ with open(entry) as search:
+ for line in search:
+ line = line.rstrip() # remove '\n' at end of line
+ m = re.search("^ *core_add_devkit_header\((.*)\)", line)
+ if m:
+ parts += "{} addons_kodi-dev-kit_include_{}\n".format(
+ cmake_dir, m.group(1)
+ )
+ break
+ file = "{}{}".format(KODI_DIR, gen_file)
+ present = os.path.isfile(file)
+ if not present or parts != open(file).read() or options.force:
+ with open(file, "w") as f:
+ f.write(parts)
+ Log.PrintResult(Result.NEW if not present else Result.UPDATE)
+ else:
+ Log.PrintResult(Result.ALREADY_DONE)
diff --git a/xbmc/addons/kodi-dev-kit/tools/code-generator/src/generateCMake__XBMC_ADDONS_KODIDEVKIT_INCLUDE_KODI_allfiles.py b/xbmc/addons/kodi-dev-kit/tools/code-generator/src/generateCMake__XBMC_ADDONS_KODIDEVKIT_INCLUDE_KODI_allfiles.py
new file mode 100644
index 0000000..c1000be
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/tools/code-generator/src/generateCMake__XBMC_ADDONS_KODIDEVKIT_INCLUDE_KODI_allfiles.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2021 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.
+
+# Own includes
+from code_generator import DEVKIT_DIR, KODI_DIR
+from .helper_Log import *
+
+# Global includes
+import glob, os, re
+
+
+def GenerateCMake__XBMC_ADDONS_KODIDEVKIT_INCLUDE_KODI_all_files_RelatedCheck(filename):
+ """
+ This function is called by git update to be able to assign changed files to the dev kit.
+ """
+ scan_dir = "{}{}/include/kodi/**/".format(KODI_DIR, DEVKIT_DIR)
+ dirs = sorted(glob.glob(scan_dir, recursive=True))
+ for dir in dirs:
+ source_dir = "{}CMakeLists.txt".format(dir.replace(KODI_DIR, ""))
+ if source_dir == filename:
+ return True
+
+ return False
+
+
+def GenerateCMake__XBMC_ADDONS_KODIDEVKIT_INCLUDE_KODI_all_files(options):
+ """
+ This function generate the "CMakeLists.txt" in xbmc/addons/kodi-dev-kit/include/kodi
+ and sub dirs by scan of available files
+ """
+ Log.PrintBegin(
+ "Generate CMakeLists.txt files in {}/include/kodi dirs".format(DEVKIT_DIR)
+ )
+ Log.PrintResult(Result.SEE_BELOW)
+
+ scan_dir = "{}{}/include/kodi/**/".format(KODI_DIR, DEVKIT_DIR)
+
+ found = False
+ dirs = sorted(glob.glob(scan_dir, recursive=True))
+ for dir in dirs:
+ source_dir = dir.replace(KODI_DIR, "")
+ Log.PrintBegin(" - Check {}CMakeLists.txt".format(source_dir))
+
+ # Scan for *.h
+ os_limits = []
+ header_configure = []
+ header_entry = []
+ src_parts = sorted(glob.glob("{}*.h*".format(dir), recursive=False))
+ for src_part in src_parts:
+ with open(src_part) as search:
+ for line in search:
+ line = line.rstrip() # remove '\n' at end of line
+ m = re.search("^\/\*---AUTO_GEN_PARSE<\$\$(.*):(.*)>---\*\/", line)
+ if m:
+ if m.group(1) == "CORE_SYSTEM_NAME":
+ if src_part.endswith(".in"):
+ Log.PrintResult(Result.FAILURE)
+ Log.PrintFatal(
+ 'File extensions with ".h.in" are currently not supported and require revision of Kodi\'s cmake system!'
+ )
+ exit(1)
+ """
+ NOTE: This code currently unused. About ".in" support need Kodi's cmake build system revised.
+ code = ''
+ for entry in m.group(2).split(","):
+ label = 'CORE_SYSTEM_NAME STREQUAL {}'.format(entry)
+ code += 'if({}'.format(label) if code == '' else ' OR\n {}'.format(label)
+ code += ')\n'
+ code += ' configure_file(${{CMAKE_SOURCE_DIR}}/{}\n'.format(src_part.replace(KODI_DIR, ''))
+ code += ' ${{CORE_BUILD_DIR}}/{} @ONLY)\n'.format(src_part.replace(KODI_DIR, '').replace('.in', ''))
+ code += 'endif()'
+ header_configure.append(code)
+ """
+ for entry in m.group(2).split(","):
+ entry = entry.strip()
+ if not entry in os_limits:
+ os_limits.append(entry)
+ header_entry.append(
+ "$<$<STREQUAL:${{CORE_SYSTEM_NAME}},{}>:{}>".format(
+ entry,
+ src_part.replace(dir, "").replace(".in", ""),
+ )
+ )
+ found = True
+ break
+ if not found:
+ header_entry.append(src_part.replace(dir, ""))
+ found = False
+
+ if len(os_limits) > 0:
+ Log.PrintFollow(
+ " (Contains limited OS header: {})".format(
+ ", ".join(map(str, os_limits))
+ )
+ )
+
+ cmake_cfg_text = (
+ "\n{}".format("".join("{}\n".format(entry) for entry in header_configure))
+ if len(header_configure) > 0
+ else ""
+ )
+ cmake_hdr_text = "set(HEADERS\n{})\n".format(
+ "".join(" {}\n".format(entry) for entry in header_entry)
+ )
+ cmake_part = (
+ source_dir[len(DEVKIT_DIR + "/include_") :].replace("/", "_").rstrip("_")
+ ) # Generate cmake sub part name
+
+ # Make final CMakeLists.txt
+ cmake_file = (
+ "# Auto generated CMakeLists.txt.\n"
+ "# See {}/tools/code-generator.py.\n"
+ "{}"
+ "\n"
+ "{}"
+ "\n"
+ "if(HEADERS)\n"
+ " core_add_devkit_header({})\n"
+ "endif()\n".format(DEVKIT_DIR, cmake_cfg_text, cmake_hdr_text, cmake_part)
+ )
+
+ file = "{}CMakeLists.txt".format(dir)
+ present = os.path.isfile(file)
+ if not present or cmake_file != open(file).read() or options.force:
+ with open(file, "w") as f:
+ f.write(cmake_file)
+ Log.PrintResult(Result.NEW if not present else Result.UPDATE)
+ else:
+ Log.PrintResult(Result.ALREADY_DONE)
diff --git a/xbmc/addons/kodi-dev-kit/tools/code-generator/src/helper_Log.py b/xbmc/addons/kodi-dev-kit/tools/code-generator/src/helper_Log.py
new file mode 100644
index 0000000..6279afa
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/tools/code-generator/src/helper_Log.py
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2021 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.
+
+from datetime import datetime
+import os
+
+
+class Result:
+ OK = 1
+ FAILURE = 2
+ UPDATE = 3
+ ALREADY_DONE = 4
+ NEW = 5
+ SEE_BELOW = 6
+ IGNORED = 7
+
+
+class Log:
+ log_file = "creation_log.txt"
+ current_cursor_pos = 0
+ terminal_columns = 120
+
+ # Class of different styles
+ class style:
+ BOLD = "\033[01m"
+ BLACK = "\033[30m"
+ RED = "\033[31m"
+ GREEN = "\033[32m"
+ YELLOW = "\033[33m"
+ BLUE = "\033[34m"
+ MAGENTA = "\033[35m"
+ CYAN = "\033[36m"
+ WHITE = "\033[37m"
+ UNDERLINE = "\033[4m"
+ RESET = "\033[0m"
+
+ def Init(options):
+ # Try to get terminal with, is optional and no matter if something fails
+ try:
+ columns, rows = os.get_terminal_size(0)
+ Log.terminal_columns = columns
+ except:
+ pass
+
+ if os.path.isfile(Log.log_file):
+ os.rename(Log.log_file, Log.log_file + ".old")
+
+ if options.debug:
+ print("DEBUG: Used command line options: {}".format(str(options)))
+
+ with open(Log.log_file, "w") as f:
+ f.write("Used call options: {}\n".format(str(options)))
+
+ def PrintMainStart(options):
+ print("┌{}┐".format("─" * (Log.terminal_columns - 2)))
+ text = "Auto generation of addon interface code"
+ print(
+ "│ {}{}{}{}{}│".format(
+ Log.style.BOLD,
+ Log.style.CYAN,
+ text,
+ Log.style.RESET,
+ " " * (Log.terminal_columns - len(text) - 3),
+ )
+ )
+ text = "Used options:"
+ print(
+ "│ {}{}{}{}{}{}│".format(
+ Log.style.BOLD,
+ Log.style.WHITE,
+ Log.style.UNDERLINE,
+ text,
+ Log.style.RESET,
+ " " * (Log.terminal_columns - len(text) - 3),
+ )
+ )
+ Log.__printUsedBooleanValueLine("force", options.force)
+ Log.__printUsedBooleanValueLine("debug", options.debug)
+ Log.__printUsedBooleanValueLine("commit", options.commit)
+ print("└{}┘".format("─" * (Log.terminal_columns - 2)))
+
+ def PrintGroupStart(text):
+ print("─" * Log.terminal_columns)
+ print(
+ "{}{} ...{}{}".format(
+ Log.style.CYAN,
+ text,
+ " " * (Log.terminal_columns - len(text) - 4),
+ Log.style.RESET,
+ )
+ )
+ with open(Log.log_file, "a") as f:
+ f.write("{}...\n".format(text))
+
+ def PrintBegin(text):
+ # datetime object containing current date and time
+ dt_string = datetime.utcnow().strftime("%d/%m/%Y %H:%M:%S")
+ Log.current_cursor_pos = len(text) + len(dt_string) + 3
+
+ print(
+ "[{}{}{}] {}{}{}{}".format(
+ Log.style.MAGENTA,
+ dt_string,
+ Log.style.RESET,
+ Log.style.WHITE,
+ Log.style.BOLD,
+ text,
+ Log.style.RESET,
+ ),
+ end="",
+ )
+ with open(Log.log_file, "a") as f:
+ f.write("[{}] {}: ".format(dt_string, text))
+
+ def PrintFollow(text):
+ Log.current_cursor_pos += len(text)
+
+ print(Log.style.CYAN + text + Log.style.RESET, end="")
+ with open(Log.log_file, "a") as f:
+ f.write("{} ".format(text))
+
+ def PrintResult(result_type, result_text=None):
+ text = ""
+ color = Log.style.WHITE
+
+ if result_type == Result.OK:
+ text = "OK"
+ color = Log.style.GREEN
+ elif result_type == Result.NEW:
+ text = "Created new"
+ color = Log.style.CYAN
+ elif result_type == Result.FAILURE:
+ text = "Failed"
+ color = Log.style.RED
+ elif result_type == Result.UPDATE:
+ text = "Updated"
+ color = Log.style.YELLOW
+ elif result_type == Result.ALREADY_DONE:
+ text = "Present and up to date"
+ color = Log.style.GREEN
+ elif result_type == Result.SEE_BELOW:
+ text = "See below"
+ color = Log.style.BLUE
+ elif result_type == Result.IGNORED:
+ text = "Ignored"
+ color = Log.style.YELLOW
+
+ print(
+ "{}{}{}{}".format(
+ color,
+ Log.style.BOLD,
+ text.rjust(Log.terminal_columns - Log.current_cursor_pos),
+ Log.style.RESET,
+ )
+ )
+ f = open(Log.log_file, "a")
+ f.write("{}\n".format(text))
+ if result_text:
+ print("Results of call before:{}\n".format(result_text))
+ f.write("Results of call before:{}\n".format(result_text))
+ f.close()
+
+ def PrintFatal(error_text):
+ # datetime object containing current date and time
+ dt_string = datetime.utcnow().strftime("%d/%m/%Y %H:%M:%S")
+ Log.current_cursor_pos = len(error_text) + len(dt_string) + 3
+
+ print(
+ "[{}{}{}] {}{}FATAL: {}{}".format(
+ Log.style.YELLOW,
+ dt_string,
+ Log.style.RESET,
+ Log.style.RED,
+ Log.style.BOLD,
+ Log.style.RESET,
+ error_text,
+ )
+ )
+ with open(Log.log_file, "a") as f:
+ f.write("[{}] {}\n".format(dt_string, error_text))
+
+ def __printUsedBooleanValueLine(name, value):
+ text = "--{} = {}{}".format(
+ name,
+ Log.style.RED + "yes" if value else Log.style.GREEN + "no",
+ Log.style.RESET,
+ )
+ print(
+ "│ {}{}{}{}{}│".format(
+ Log.style.BOLD,
+ Log.style.WHITE,
+ text,
+ Log.style.RESET,
+ " " * (Log.terminal_columns - len(text) + 6),
+ )
+ )
diff --git a/xbmc/addons/kodi-dev-kit/tools/debian-addon-package-test.sh b/xbmc/addons/kodi-dev-kit/tools/debian-addon-package-test.sh
new file mode 100755
index 0000000..d778954
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/tools/debian-addon-package-test.sh
@@ -0,0 +1,141 @@
+#!/bin/bash
+
+# This script is for purely testing purposes!
+#
+# It is meant to be able to test at binary addons the creation of a debian
+# package.
+#
+# The created files are below the source code folder.
+#
+# Example:
+# ./build-debian-addon-package.sh $HOME/your_path/screensaver.stars
+#
+# To remove generated code:
+# ./build-debian-addon-package.sh $HOME/your_path/screensaver.stars --clean
+#
+
+BASE_DIR=""
+REPO_DIR=""
+PACKAGEVERSION=""
+VERSION_MAIN=""
+VERSION_MINOR=""
+VERSION_REVISON=""
+GIT_REV=""
+DIST=""
+TAGREV=${TAGREV:-"0"}
+
+function usage {
+ echo "\
+--------------------------------------------------------------------------------
+
+This script builds a Kodi addon debian package from the given addon source.
+
+As value, the path to the addon must be given.
+In addition --clean can be used to remove the created debian files.
+
+WARNING: This script is for testing purposes only!
+
+--------------------------------------------------------------------------------"
+}
+
+function checkEnv {
+ echo "#------ build environment ------#"
+ echo "BASE_DIR: $BASE_DIR"
+ echo "REPO_DIR: $REPO_DIR"
+ getVersion
+ echo "VERSION_MAIN: $VERSION_MAIN"
+ echo "VERSION_MINOR: $VERSION_MINOR"
+ echo "VERSION_REVISON: $VERSION_REVISON"
+ if [ $GIT_REV ]; then
+ echo "GIT_REV: $GIT_REV"
+ fi
+ echo "TAGREV: $TAGREV"
+ echo "DIST: $DIST"
+ echo "ARCHS: $ARCHS"
+
+ echo "#-------------------------------#"
+}
+
+function getVersion {
+ if [ -d ${BASE_DIR}/.git ]; then
+ getGitRev
+ fi
+ PACKAGEVERSION=$(cat ${BASE_DIR}/$REPO_DIR/addon.xml.in | sed -n '/version/ s/.*version=\"\([0-9]\+\.[0-9]\+\.[0-9]\+\)\".*/\1/p' | awk 'NR == 1')
+ VERSION_MAIN=$(echo $PACKAGEVERSION | awk -F. '{print $1}')
+ VERSION_MINOR=$(echo $PACKAGEVERSION | awk -F. '{print $2}')
+ VERSION_REVISON=$(echo $PACKAGEVERSION | awk -F. '{print $3}')
+}
+
+function getGitRev {
+ cd $BASE_DIR || exit 1
+ GIT_REV=$(git log -1 --pretty=format:"%h")
+}
+
+function cleanup() {
+ echo "Starting to remove debian generated files"
+ cd ${BASE_DIR}
+ rm -rf obj-$ARCHS-linux-gnu
+ rm -rf debian/.debhelper
+ rm -rf debian/"kodi-"$(echo $REPO_DIR | tr . -)
+ rm -rf debian/"kodi-"$(echo $REPO_DIR | tr . -)-dbg
+ rm -rf debian/tmp
+ rm -f debian/changelog
+ rm -f debian/debhelper-build-stamp
+ rm -f debian/files
+ rm -f debian/*.log
+ rm -f debian/*.substvars
+}
+
+if [[ $1 = "-h" ]] || [[ $1 = "--help" ]]; then
+ echo "$0:"
+ usage
+ exit
+fi
+
+if [ ! $1 ] || [ ${1} = "--clean" ]; then
+ printf "$0:\nERROR: Addon source code must be given as the first parameter!\n"
+ usage
+ exit 1
+elif [ ! -d $1 ]; then
+ printf "$0:\nERROR: Given folder is not present or not a valid addon source!\n"
+ usage
+ exit 1
+fi
+
+ARCHS=$(uname -m)
+BASE_DIR=$(realpath ${1})
+REPO_DIR=$(basename ${1})
+DIST=$(lsb_release -cs)
+
+if [ ! -f ${BASE_DIR}/$REPO_DIR/addon.xml.in ]; then
+ echo "$0:
+ERROR: \"required $REPO_DIR/addon.xml.in\" not found!
+
+The base source dir and addon.xml.in containing directory names must be equal"
+ usage
+ exit 1
+fi
+
+checkEnv
+
+ORIGTARBALL="kodi-"$(echo $REPO_DIR | tr . -)"_${PACKAGEVERSION}.orig.tar.gz"
+
+if [[ ${2} = "--clean" ]]; then
+ cleanup
+ exit
+fi
+
+echo "Detected addon package version: ${PACKAGEVERSION}"
+
+sed -e "s/#PACKAGEVERSION#/${PACKAGEVERSION}/g" \
+ -e "s/#TAGREV#/${TAGREV}/g" \
+ -e "s/#DIST#/${DIST}/g" ${BASE_DIR}/debian/changelog.in > ${BASE_DIR}/debian/changelog
+
+echo "Create needed compressed source code file: ${BASE_DIR}/../${ORIGTARBALL}.tar.gz"
+
+# git archive --format=tar.gz -o ${BASE_DIR}/../${ORIGTARBALL} HEAD # Unused git, leaved as optional way
+EXCLUDE=$(cat .gitignore | sed -e '/#/d' -e '/^\s*$/d' -e 's/^/--exclude=/' -e 's/\/$//' | tr '\n' ' ')
+tar -zcvf ${BASE_DIR}/../${ORIGTARBALL} --exclude=.git $EXCLUDE * # Used to prevent on code changed for test a git commit
+
+echo "Building debian-source package for ${DIST}"
+dpkg-buildpackage -us -uc --diff-ignore="()$"
diff --git a/xbmc/addons/kodi-dev-kit/tools/doxygen-header-class-list-creator.py b/xbmc/addons/kodi-dev-kit/tools/doxygen-header-class-list-creator.py
new file mode 100755
index 0000000..0b0590e
--- /dev/null
+++ b/xbmc/addons/kodi-dev-kit/tools/doxygen-header-class-list-creator.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+
+from optparse import OptionParser
+import glob
+import io
+import os
+import re
+import sys
+
+def read_file(name, normalize=True):
+ """ Read a file. """
+ try:
+ with open(name, 'r', encoding='utf-8') as f:
+ # read the data
+ data = f.read()
+ if normalize:
+ # normalize line endings
+ data = data.replace("\r\n", "\n")
+ return data
+ except IOError as e:
+ (errno, strerror) = e.args
+ sys.stderr.write('Failed to read file ' + name + ': ' + strerror)
+ raise
+
+def write_file(name, data):
+ """ Write a file. """
+ try:
+ with open(name, 'w', encoding='utf-8') as f:
+ # write the data
+ if sys.version_info.major == 2:
+ f.write(data.decode('utf-8'))
+ else:
+ f.write(data)
+ except IOError as e:
+ (errno, strerror) = e.args
+ sys.stderr.write('Failed to write file ' + name + ': ' + strerror)
+ raise
+
+def auto_check_header(file):
+ groups_to_check = []
+
+ data = read_file(file)
+ if not 'addon_auto_check' in data:
+ return ''
+
+ for line in io.StringIO(data):
+ line = re.search(r"^.*\/\/\/.*@copydetails *(.*)(_header|_source)_addon_auto_check.*", line, flags=re.UNICODE)
+ if line and line.group(1):
+ group = line.group(1)
+ if group in groups_to_check:
+ continue
+
+ print(' - Found use with %s' % group)
+ groups_to_check.append(line.group(1))
+
+ return groups_to_check
+
+def parse_header(file, group, new_path=''):
+ header_sources = ''
+ header_addon = ''
+ source_addon = ''
+
+ data = read_file(file)
+ group_found = False
+ group_start = False
+ virtual_function_start = False
+ for line in io.StringIO(data):
+ if not group_found and 'defgroup ' + group in line:
+ group_found = True
+ continue
+ elif group_found and not group_start and '///@{' in line:
+ group_start = True
+ continue
+ elif group_start and '///@}' in line:
+ break
+ elif re.match(r'^.*//.*', line) or re.match(r'^.*//.*', line) or line == '\n' or not group_start:
+ continue
+
+ if re.match(r'^.*virtual.*', line):
+ virtual_function_start = True
+
+ if virtual_function_start:
+ header_sources += re.sub(r"^\s+", "", line, flags=re.UNICODE)
+
+ if virtual_function_start and re.match(r'^.*}.*', line):
+ virtual_function_start = False
+
+ if not group_found:
+ return ""
+
+ header_sources = header_sources.replace("\n", "")
+ header_sources = " ".join(re.split("\s+", header_sources, flags=re.UNICODE))
+ header_sources = header_sources.replace("}", "}\n")
+ header_sources = header_sources.replace("= 0;", "= 0;\n")
+ header_sources = header_sources.replace(",", ", ")
+
+ # Generate class header part of list
+ header_addon += '/// @defgroup ' + group + '_header_addon_auto_check Group header include\n'
+ header_addon += '/// @ingroup ' + group + '\n'
+ header_addon += '///@{\n'
+ header_addon += '/// *Header parts:*\n'
+ header_addon += '/// ~~~~~~~~~~~~~{.cpp}\n'
+ header_addon += '///\n'
+ for line in io.StringIO(header_sources):
+ line = re.search(r"^.*virtual.([A-Za-z1-9].*\(.*\))", line, flags=re.UNICODE)
+ if line:
+ header_addon += '/// ' + re.sub(' +', ' ', line.group(1)) + ' override;\n'
+ header_addon += '///\n'
+ header_addon += '/// ~~~~~~~~~~~~~\n'
+ header_addon += '///@}\n\n'
+
+ # Generate class source part of list
+ source_addon += '/// @defgroup ' + group + '_source_addon_auto_check Group source include\n'
+ source_addon += '/// @ingroup ' + group + '\n'
+ source_addon += '///@{\n'
+ source_addon += '/// *Source parts:*\n'
+ source_addon += '/// ~~~~~~~~~~~~~{.cpp}\n'
+ source_addon += '///\n'
+ for line in io.StringIO(header_sources):
+ line = line.replace("{", "\n{\n ")
+ line = line.replace("}", "\n}")
+ for line in io.StringIO(line + '\n'):
+ func = re.search(r"^.*(virtual *) *(.*) ([a-z|A-Z|0-9].*)(\(.*\))", line, flags=re.UNICODE)
+ if func:
+ source_addon += '/// ' + re.sub(' +', ' ', func.group(2) + ' CMyInstance::' + func.group(3) + func.group(4) + '\n')
+ else:
+ source_addon += '/// ' + line
+ if '= 0' in line:
+ source_addon += '/// {\n'
+ source_addon += '/// // Required in interface to have!\n'
+ source_addon += '/// // ...\n'
+ source_addon += '/// }\n'
+
+ source_addon += '/// ~~~~~~~~~~~~~\n'
+ source_addon += '///@}\n\n'
+
+ return header_addon + source_addon
+
+def print_error(msg):
+ print('Error: %s\nSee --help for usage.' % msg)
+
+# cannot be loaded as a module
+if __name__ != "__main__":
+ sys.stderr.write('This file cannot be loaded as a module!')
+ sys.exit()
+
+# parse command-line options
+disc = """
+This utility generate group list about addon header sources to add inside doxygen.
+"""
+
+parser = OptionParser(description=disc)
+parser.add_option(
+ '--header-file',
+ dest='headerfile',
+ metavar='DIR',
+ help='the to checked header [required]')
+parser.add_option(
+ '--doxygen-group',
+ dest='doxygengroup',
+ help='the to checked doxygen group inside header [required]')
+(options, args) = parser.parse_args()
+
+docs = ''
+groups_to_check = []
+
+# Check about use of helper docs
+if options.doxygengroup is None:
+ print('Scaning about used places...')
+ headers = glob.glob("../include/kodi/**/*.h", recursive=True)
+ for header in headers:
+ group = auto_check_header(header)
+ if group:
+ groups_to_check += group
+else:
+ groups_to_check.append(options.doxygengroup)
+
+# Generate the helper docs
+if options.headerfile is None:
+ headers = glob.glob("../include/kodi/**/*.h", recursive=True)
+ print('Parsing about docs:')
+ for header in headers:
+ print(' - %s' % header)
+ for group in groups_to_check:
+ docs += parse_header(header, group)
+else:
+ for group in groups_to_check:
+ docs += parse_header(options.headerfile, group)
+
+write_file("../include/groups.dox", docs)
diff --git a/xbmc/addons/settings/AddonSettings.cpp b/xbmc/addons/settings/AddonSettings.cpp
new file mode 100644
index 0000000..83bbdd9
--- /dev/null
+++ b/xbmc/addons/settings/AddonSettings.cpp
@@ -0,0 +1,1726 @@
+/*
+ * Copyright (C) 2017-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 "AddonSettings.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "addons/settings/SettingUrlEncodedString.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/SettingAddon.h"
+#include "settings/SettingConditions.h"
+#include "settings/SettingControl.h"
+#include "settings/SettingDateTime.h"
+#include "settings/SettingPath.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingSection.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+#include <vector>
+
+namespace
+{
+
+constexpr auto OldSettingValuesSeparator = "|";
+
+constexpr int UnknownSettingLabelIdStart = 100000;
+
+bool InfoBool(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CServiceBroker::GetGUI()->GetInfoManager().EvaluateBool(value, INFO::DEFAULT_CONTEXT);
+}
+
+template<class TSetting>
+SettingPtr InitializeFromOldSettingWithoutDefinition(ADDON::CAddonSettings& settings,
+ const std::string& settingId,
+ const typename TSetting::Value& defaultValue)
+{
+ std::shared_ptr<TSetting> setting =
+ std::make_shared<TSetting>(settingId, settings.GetSettingsManager());
+ setting->SetLevel(SettingLevel::Internal);
+ setting->SetVisible(false);
+ setting->SetDefault(defaultValue);
+
+ return setting;
+}
+
+template<>
+SettingPtr InitializeFromOldSettingWithoutDefinition<CSettingString>(
+ ADDON::CAddonSettings& settings,
+ const std::string& settingId,
+ const typename CSettingString::Value& defaultValue)
+{
+ std::shared_ptr<CSettingString> setting =
+ std::make_shared<CSettingString>(settingId, settings.GetSettingsManager());
+ setting->SetLevel(SettingLevel::Internal);
+ setting->SetVisible(false);
+ setting->SetDefault(defaultValue);
+ setting->SetAllowEmpty(true);
+
+ return setting;
+}
+
+template<class TSetting>
+SettingPtr AddSettingWithoutDefinition(ADDON::CAddonSettings& settings,
+ const std::string& settingId,
+ typename TSetting::Value defaultValue,
+ const Logger& logger)
+{
+ if (settingId.empty())
+ return nullptr;
+
+ // if necessary try to initialize the settings manager on-the-fly without any definitions
+ if (!settings.IsInitialized() && !settings.Initialize(CXBMCTinyXML(), true))
+ {
+ logger->warn("failed to initialize settings on-the-fly");
+ return nullptr;
+ }
+
+ // check if we need to add a section on-the-fly
+ auto sections = settings.GetSettingsManager()->GetSections();
+ SettingSectionPtr section;
+ if (sections.empty())
+ section =
+ std::make_shared<CSettingSection>(settings.GetAddonId(), settings.GetSettingsManager());
+ else
+ section = sections.back();
+
+ // check if we need to add a category on-the-fly
+ auto categories = section->GetCategories();
+ SettingCategoryPtr category;
+ if (categories.empty())
+ category = std::make_shared<CSettingCategory>("category0", settings.GetSettingsManager());
+ else
+ category = categories.back();
+
+ // check if we need to add a group on-the-fly
+ auto groups = category->GetGroups();
+ SettingGroupPtr group;
+ if (groups.empty())
+ group = std::make_shared<CSettingGroup>("0", settings.GetSettingsManager());
+ else
+ group = groups.back();
+
+ // create a new setting on-the-fly
+ auto setting =
+ InitializeFromOldSettingWithoutDefinition<TSetting>(settings, settingId, defaultValue);
+ if (setting == nullptr)
+ {
+ logger->warn("failed to create setting \"{}\" on-the-fly", settingId);
+ return nullptr;
+ }
+
+ // add the setting (and if necessary the section, category and/or group)
+ if (!settings.GetSettingsManager()->AddSetting(setting, section, category, group))
+ {
+ logger->warn("failed to add setting \"{}\" on-the-fly", settingId);
+ return nullptr;
+ }
+
+ return setting;
+}
+
+} // namespace
+
+namespace ADDON
+{
+
+CAddonSettings::CAddonSettings(const std::shared_ptr<const IAddon>& addon,
+ AddonInstanceId instanceId)
+ : CSettingsBase(),
+ m_addonId(addon->ID()),
+ m_addonPath(addon->Path()),
+ m_addonProfile(addon->Profile()),
+ m_instanceId(instanceId),
+ m_unidentifiedSettingId(0),
+ m_unknownSettingLabelId(UnknownSettingLabelIdStart),
+ m_logger(CServiceBroker::GetLogging().GetLogger(
+ StringUtils::Format("CAddonSettings[{}@{}]", m_instanceId, m_addonId)))
+{
+}
+
+std::shared_ptr<CSetting> CAddonSettings::CreateSetting(
+ const std::string& settingType,
+ const std::string& settingId,
+ CSettingsManager* settingsManager /* = nullptr */) const
+{
+ if (StringUtils::EqualsNoCase(settingType, "urlencodedstring"))
+ return std::make_shared<CSettingUrlEncodedString>(settingId, settingsManager);
+
+ return CSettingCreator::CreateSetting(settingType, settingId, settingsManager);
+}
+
+void CAddonSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ std::string actionData;
+ bool closeDialog = false;
+
+ // check if it's an action setting
+ if (setting->GetType() == SettingType::Action)
+ {
+ auto settingAction = std::dynamic_pointer_cast<const CSettingAction>(setting);
+ if (settingAction != nullptr && settingAction->HasData())
+ {
+ actionData = settingAction->GetData();
+ // replace $CWD with the url of the add-on
+ StringUtils::Replace(actionData, "$CWD", m_addonPath);
+ // replace $ID with the id of the add-on
+ StringUtils::Replace(actionData, "$ID", m_addonId);
+ }
+ }
+
+ // check if the setting control's is a button and its format is action
+ if (setting->GetControl()->GetType() == "button" &&
+ setting->GetControl()->GetFormat() == "action")
+ {
+ auto controlButton =
+ std::dynamic_pointer_cast<const CSettingControlButton>(setting->GetControl());
+ if (controlButton != nullptr)
+ {
+ if (actionData.empty() && controlButton->HasActionData())
+ actionData = controlButton->GetActionData();
+
+ closeDialog = controlButton->CloseDialog();
+ }
+ }
+
+ if (actionData.empty())
+ return;
+
+ if (closeDialog)
+ CGUIDialogAddonSettings::SaveAndClose();
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr, actionData);
+}
+
+bool CAddonSettings::AddInstanceSettings()
+{
+ if (GetSetting(ADDON_SETTING_INSTANCE_NAME_VALUE) ||
+ GetSetting(ADDON_SETTING_INSTANCE_ENABLED_VALUE))
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "CAddonSettings::{} - Add-on {} using instance setting values byself, Kodi's add ignored",
+ __func__, m_addonId);
+ return true;
+ }
+
+ auto mgr = GetSettingsManager();
+ if (!mgr)
+ return false;
+
+ auto sections = mgr->GetSections();
+ if (sections.empty())
+ return false;
+
+ SettingSectionPtr section = *sections.begin();
+
+ auto categories = section->GetCategories();
+ if (categories.empty())
+ return false;
+
+ SettingCategoryPtr category = *categories.begin();
+
+ auto groups = category->GetGroups();
+ auto itr = std::find_if(groups.begin(), groups.end(),
+ [](const SettingGroupPtr& group)
+ { return group->GetId() == ADDON_SETTING_INSTANCE_GROUP; });
+
+ SettingGroupPtr group;
+ if (itr != groups.end())
+ {
+ group = *itr;
+ }
+ else
+ {
+ group = std::make_shared<CSettingGroup>(ADDON_SETTING_INSTANCE_GROUP, mgr);
+ group->SetLabel(10017); // Add-on configuration
+ category->AddGroupToFront(group);
+ }
+
+ const std::shared_ptr<CSettingString> name =
+ std::make_shared<CSettingString>(ADDON_SETTING_INSTANCE_NAME_VALUE, 551, "", mgr); // Name
+ name->SetAllowEmpty(false);
+ name->SetControl(std::make_shared<CSettingControlEdit>());
+ if (!mgr->AddSetting(name, section, category, group))
+ return false;
+
+ const std::shared_ptr<CSettingBool> enabled = std::make_shared<CSettingBool>(
+ ADDON_SETTING_INSTANCE_ENABLED_VALUE, 305, true, mgr); // Enabled
+ enabled->SetControl(std::make_shared<CSettingControlCheckmark>());
+ if (!mgr->AddSetting(enabled, section, category, group))
+ return false;
+
+ return true;
+}
+
+bool CAddonSettings::Initialize(const CXBMCTinyXML& doc, bool allowEmpty /* = false */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (m_initialized)
+ return false;
+
+ // register custom setting types
+ InitializeSettingTypes();
+ // register custom setting controls
+ InitializeControls();
+
+ // conditions need to be initialized before the setting definitions
+ InitializeConditions();
+
+ // load the settings definitions
+ if (!InitializeDefinitions(doc) && !allowEmpty)
+ return false;
+
+ // Add internal settings to set values about instance set
+ if (m_instanceId > 0 && !AddInstanceSettings())
+ return false;
+
+ GetSettingsManager()->SetInitialized();
+
+ m_initialized = true;
+
+ return true;
+}
+
+bool CAddonSettings::Load(const CXBMCTinyXML& doc)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (!m_initialized)
+ return false;
+
+ // figure out the version of the setting definitions
+ uint32_t version = 0;
+ if (!ParseSettingVersion(doc, version))
+ {
+ m_logger->error("failed to determine setting values version");
+ return false;
+ }
+
+ std::map<std::string, std::string> settingValues;
+
+ // for new/"normal" setting values use the standard process
+ if (version != 0)
+ {
+ bool updated;
+ if (!LoadValuesFromXml(doc, updated))
+ return false;
+
+ // helper lambda for parsing a setting's ID and value from XML
+ auto parseSettingValue = [&settingValues](const TiXmlNode* setting,
+ const std::string& categoryId = "") {
+ // put together the setting ID
+ auto settingId = categoryId;
+ if (!settingId.empty())
+ settingId += ".";
+ auto id = setting->ToElement()->Attribute("id");
+ if (id)
+ settingId += id;
+
+ // parse the setting value
+ std::string settingValue;
+ if (setting->FirstChild())
+ settingValue = setting->FirstChild()->ValueStr();
+
+ // add the setting to the map
+ settingValues.emplace(std::make_pair(settingId, settingValue));
+ };
+
+ // check if there were any setting values without a definition
+ auto category = doc.RootElement()->FirstChild();
+ while (category != nullptr)
+ {
+ // check if this really is a category with setting elements
+ if (category->FirstChild() && category->FirstChild()->Type() == CXBMCTinyXML::TINYXML_ELEMENT)
+ {
+ const auto& categoryId = category->ValueStr();
+ auto setting = category->FirstChild();
+ while (setting != nullptr)
+ {
+ parseSettingValue(setting, categoryId);
+
+ setting = setting->NextSibling();
+ }
+ }
+ else
+ parseSettingValue(category);
+
+ category = category->NextSibling();
+ }
+ }
+ // for old setting values do it manually
+ else if (!LoadOldSettingValues(doc, settingValues))
+ {
+ m_logger->error("failed to determine setting values from old format");
+ return false;
+ }
+
+ // process all settings
+ for (const auto& setting : settingValues)
+ {
+ // ignore setting values without a setting identifier
+ if (setting.first.empty())
+ continue;
+
+ // try to find a matching setting
+ SettingPtr newSetting = GetSetting(setting.first);
+ if (newSetting == nullptr)
+ {
+ // create a hidden/internal string setting on-the-fly
+ newSetting = AddSettingWithoutDefinition<CSettingString>(*this, setting.first, setting.second,
+ m_logger);
+ }
+
+ // try to load the old setting value
+ if (!newSetting)
+ {
+ m_logger->error("had null newSetting for value \"{}\" for setting {}", setting.second,
+ setting.first);
+ }
+ else if (!newSetting->FromString(setting.second))
+ {
+ m_logger->warn("failed to load value \"{}\" for setting {}", setting.second, setting.first);
+ }
+ }
+
+ SetLoaded();
+
+ return true;
+}
+
+bool CAddonSettings::Save(CXBMCTinyXML& doc) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (!m_initialized)
+ return false;
+
+ if (!SaveValuesToXml(doc))
+ {
+ m_logger->error("failed to save settings");
+ return false;
+ }
+
+ return true;
+}
+
+bool CAddonSettings::HasSettings() const
+{
+ return IsInitialized() && GetSettingsManager()->HasSettings();
+}
+
+std::string CAddonSettings::GetSettingLabel(int label) const
+{
+ if (label < UnknownSettingLabelIdStart || label >= m_unknownSettingLabelId)
+ return "";
+
+ const auto labelIt = m_unknownSettingLabels.find(label);
+ if (labelIt == m_unknownSettingLabels.end())
+ return "";
+
+ return labelIt->second;
+}
+
+std::shared_ptr<CSetting> CAddonSettings::AddSetting(const std::string& settingId, bool value)
+{
+ return AddSettingWithoutDefinition<CSettingBool>(*this, settingId, value, m_logger);
+}
+
+std::shared_ptr<CSetting> CAddonSettings::AddSetting(const std::string& settingId, int value)
+{
+ return AddSettingWithoutDefinition<CSettingInt>(*this, settingId, value, m_logger);
+}
+
+std::shared_ptr<CSetting> CAddonSettings::AddSetting(const std::string& settingId, double value)
+{
+ return AddSettingWithoutDefinition<CSettingNumber>(*this, settingId, value, m_logger);
+}
+
+std::shared_ptr<CSetting> CAddonSettings::AddSetting(const std::string& settingId,
+ const std::string& value)
+{
+ return AddSettingWithoutDefinition<CSettingString>(*this, settingId, value, m_logger);
+}
+
+void CAddonSettings::InitializeSettingTypes()
+{
+ GetSettingsManager()->RegisterSettingType("addon", this);
+ GetSettingsManager()->RegisterSettingType("date", this);
+ GetSettingsManager()->RegisterSettingType("path", this);
+ GetSettingsManager()->RegisterSettingType("time", this);
+ GetSettingsManager()->RegisterSettingType("urlencodedstring", this);
+}
+
+void CAddonSettings::InitializeControls()
+{
+ GetSettingsManager()->RegisterSettingControl("toggle", this);
+ GetSettingsManager()->RegisterSettingControl("spinner", this);
+ GetSettingsManager()->RegisterSettingControl("edit", this);
+ GetSettingsManager()->RegisterSettingControl("button", this);
+ GetSettingsManager()->RegisterSettingControl("list", this);
+ GetSettingsManager()->RegisterSettingControl("slider", this);
+ GetSettingsManager()->RegisterSettingControl("range", this);
+ GetSettingsManager()->RegisterSettingControl("title", this);
+ GetSettingsManager()->RegisterSettingControl("colorbutton", this);
+}
+
+void CAddonSettings::InitializeConditions()
+{
+ CSettingConditions::Initialize();
+
+ // add basic conditions
+ const std::set<std::string>& simpleConditions = CSettingConditions::GetSimpleConditions();
+ for (const auto& condition : simpleConditions)
+ GetSettingsManager()->AddCondition(condition);
+
+ GetSettingsManager()->AddDynamicCondition("InfoBool", InfoBool);
+}
+
+bool CAddonSettings::InitializeDefinitions(const CXBMCTinyXML& doc)
+{
+ // figure out the version of the setting definitions
+ uint32_t version = 0;
+ if (!ParseSettingVersion(doc, version))
+ {
+ m_logger->error("failed to determine setting definitions version");
+ return false;
+ }
+
+ // for new/"normal" setting definitions use the standard process
+ if (version != 0)
+ return InitializeDefinitionsFromXml(doc);
+
+ // for old setting definitions do it manually
+ return InitializeFromOldSettingDefinitions(doc);
+}
+
+bool CAddonSettings::ParseSettingVersion(const CXBMCTinyXML& doc, uint32_t& version) const
+{
+ const TiXmlElement* root = doc.RootElement();
+ if (root == nullptr)
+ return false;
+
+ if (!StringUtils::EqualsNoCase(root->ValueStr(), SETTING_XML_ROOT))
+ {
+ m_logger->error("error reading setting definitions: no <settings> tag");
+ return false;
+ }
+
+ version = GetSettingsManager()->ParseVersion(root);
+ return true;
+}
+
+std::shared_ptr<CSettingGroup> CAddonSettings::ParseOldSettingElement(
+ const TiXmlElement* categoryElement,
+ const std::shared_ptr<CSettingCategory>& category,
+ std::set<std::string>& settingIds)
+{
+ // build a vector of settings from the same category
+ std::vector<std::shared_ptr<const CSetting>> categorySettings;
+
+ // prepare for settings with enable/visible conditions
+ struct SettingWithConditions
+ {
+ SettingPtr setting;
+ std::string enableCondition;
+ std::string visibleCondition;
+ SettingDependencies deps;
+ };
+ std::vector<SettingWithConditions> settingsWithConditions;
+
+ auto group = std::make_shared<CSettingGroup>("0", GetSettingsManager());
+ uint32_t groupId = 1;
+
+ // go through all settings in the category
+ const TiXmlElement* settingElement = categoryElement->FirstChildElement("setting");
+ while (settingElement != nullptr)
+ {
+ // read the possible attributes
+ const auto settingType = XMLUtils::GetAttribute(settingElement, "type");
+ const auto settingId = XMLUtils::GetAttribute(settingElement, "id");
+ const auto defaultValue = XMLUtils::GetAttribute(settingElement, "default");
+ const auto settingValues = XMLUtils::GetAttribute(settingElement, "values");
+ const auto settingLValues = StringUtils::Split(
+ XMLUtils::GetAttribute(settingElement, "lvalues"), OldSettingValuesSeparator);
+ int settingLabel = -1;
+ bool settingLabelParsed = ParseOldLabel(settingElement, settingId, settingLabel);
+
+ SettingPtr setting;
+ if (settingType == "sep" || settingType == "lsep")
+ {
+ // check if we need to create a new group
+ if (!group->GetSettings().empty())
+ {
+ // add the current group to the category
+ category->AddGroup(group);
+
+ // and create a new one
+ group.reset(new CSettingGroup(std::to_string(groupId), GetSettingsManager()));
+ groupId += 1;
+ }
+
+ if (settingType == "lsep" && settingLabelParsed)
+ group->SetLabel(settingLabel);
+ }
+ else if (settingId.empty() || settingType == "action")
+ {
+ if (settingType == "action")
+ setting = InitializeFromOldSettingAction(settingId, settingElement, defaultValue);
+ else
+ setting = InitializeFromOldSettingLabel();
+ }
+ else if (settingType == "bool")
+ setting = InitializeFromOldSettingBool(settingId, settingElement, defaultValue);
+ else if (settingType == "text" || settingType == "ipaddress")
+ setting = InitializeFromOldSettingTextIpAddress(settingId, settingType, settingElement,
+ defaultValue, settingLabel);
+ else if (settingType == "number")
+ setting =
+ InitializeFromOldSettingNumber(settingId, settingElement, defaultValue, settingLabel);
+ else if (settingType == "video" || settingType == "audio" || settingType == "image" ||
+ settingType == "executable" || settingType == "file" || settingType == "folder")
+ setting = InitializeFromOldSettingPath(settingId, settingType, settingElement, defaultValue,
+ settingLabel);
+ else if (settingType == "date")
+ setting = InitializeFromOldSettingDate(settingId, settingElement, defaultValue, settingLabel);
+ else if (settingType == "time")
+ setting = InitializeFromOldSettingTime(settingId, settingElement, defaultValue, settingLabel);
+ else if (settingType == "select")
+ setting = InitializeFromOldSettingSelect(settingId, settingElement, defaultValue,
+ settingLabel, settingValues, settingLValues);
+ else if (settingType == "addon")
+ setting =
+ InitializeFromOldSettingAddon(settingId, settingElement, defaultValue, settingLabel);
+ else if (settingType == "enum" || settingType == "labelenum")
+ setting = InitializeFromOldSettingEnums(settingId, settingType, settingElement, defaultValue,
+ settingValues, settingLValues);
+ else if (settingType == "fileenum")
+ setting =
+ InitializeFromOldSettingFileEnum(settingId, settingElement, defaultValue, settingValues);
+ else if (settingType == "rangeofnum")
+ setting = InitializeFromOldSettingRangeOfNum(settingId, settingElement, defaultValue);
+ else if (settingType == "slider")
+ setting = InitializeFromOldSettingSlider(settingId, settingElement, defaultValue);
+ else if (settingType.empty())
+ {
+ // setting definitions without a type are considered as "text" / strings but are hidden
+ setting = InitializeFromOldSettingTextIpAddress(settingId, "text", settingElement,
+ defaultValue, settingLabel);
+ setting->SetLevel(SettingLevel::Internal);
+ }
+ else
+ {
+ m_logger->warn("failed to parse old setting definition for \"{}\" of type \"{}\"", settingId,
+ settingType);
+ }
+
+ // process general properties
+ if (setting != nullptr)
+ {
+ // set the default level to be Basic
+ if (setting->GetLevel() != SettingLevel::Internal)
+ {
+ setting->SetLevel(SettingLevel::Basic);
+ }
+
+ // use the setting's ID if there's no label
+ if (settingLabel < 0)
+ {
+ settingLabel = m_unknownSettingLabelId;
+ m_unknownSettingLabelId += 1;
+
+ m_unknownSettingLabels.emplace(settingLabel, settingId);
+ }
+
+ // set the setting's label
+ setting->SetLabel(settingLabel);
+
+ // handle subsettings
+ bool isSubsetting = false;
+ if (settingElement->QueryBoolAttribute("subsetting", &isSubsetting) == TIXML_SUCCESS &&
+ isSubsetting)
+ {
+ // find the last non-subsetting in the current group and use that as the parent setting
+ const auto groupSettings = group->GetSettings();
+ const auto parentSetting = std::find_if(
+ groupSettings.crbegin(), groupSettings.crend(),
+ [](const SettingConstPtr& setting) { return setting->GetParent().empty(); });
+
+ if (parentSetting != groupSettings.crend())
+ {
+ if ((*parentSetting)->IsReference())
+ setting->SetParent((*parentSetting)->GetReferencedId());
+ else
+ setting->SetParent((*parentSetting)->GetId());
+ }
+ }
+
+ SettingWithConditions settingWithConditions;
+
+ // parse enable status
+ const auto conditionEnable = XMLUtils::GetAttribute(settingElement, "enable");
+ if (StringUtils::EqualsNoCase(conditionEnable, "true"))
+ setting->SetEnabled(true);
+ else if (StringUtils::EqualsNoCase(conditionEnable, "false"))
+ setting->SetEnabled(false);
+ else if (!conditionEnable.empty())
+ settingWithConditions.enableCondition = conditionEnable;
+
+ // parse visible status
+ const auto conditionVisible = XMLUtils::GetAttribute(settingElement, "visible");
+ if (StringUtils::EqualsNoCase(conditionVisible, "true"))
+ setting->SetVisible(true);
+ else if (StringUtils::EqualsNoCase(conditionVisible, "false"))
+ setting->SetVisible(false);
+ else if (!conditionVisible.empty())
+ settingWithConditions.visibleCondition = conditionVisible;
+
+ // check if there already is a setting with the setting identifier
+ if (settingIds.find(settingId) != settingIds.end())
+ {
+ // turn the setting into a reference setting
+ setting->MakeReference();
+ }
+ else
+ {
+ // add the setting's identifier to the list of all identifiers
+ settingIds.insert(setting->GetId());
+ }
+
+ if (!settingWithConditions.enableCondition.empty() ||
+ !settingWithConditions.visibleCondition.empty())
+ {
+ settingWithConditions.setting = setting;
+ settingsWithConditions.push_back(settingWithConditions);
+ }
+
+ // add the setting to the list of settings from the same category
+ categorySettings.push_back(setting);
+
+ // add the setting to the current group
+ group->AddSetting(setting);
+ }
+ else
+ {
+ // add a dummy setting for the group / separator to the list of settings from the same category
+ categorySettings.push_back(nullptr);
+ }
+
+ // look for the next setting
+ settingElement = settingElement->NextSiblingElement("setting");
+ }
+
+ // process settings with enable/visible conditions
+ for (auto setting : settingsWithConditions)
+ {
+ if (!setting.enableCondition.empty())
+ {
+ CSettingDependency dependencyEnable(SettingDependencyType::Enable, GetSettingsManager());
+ if (ParseOldCondition(setting.setting, categorySettings, setting.enableCondition,
+ dependencyEnable))
+ setting.deps.push_back(dependencyEnable);
+ else
+ {
+ m_logger->warn(
+ "failed to parse enable condition \"{}\" of old setting definition for \"{}\"",
+ setting.enableCondition, setting.setting->GetId());
+ }
+ }
+
+ if (!setting.visibleCondition.empty())
+ {
+ CSettingDependency dependencyVisible(SettingDependencyType::Visible, GetSettingsManager());
+ if (ParseOldCondition(setting.setting, categorySettings, setting.visibleCondition,
+ dependencyVisible))
+ setting.deps.push_back(dependencyVisible);
+ else
+ {
+ m_logger->warn(
+ "failed to parse visible condition \"{}\" of old setting definition for \"{}\"",
+ setting.visibleCondition, setting.setting->GetId());
+ }
+ }
+
+ // set dependencies
+ setting.setting->SetDependencies(setting.deps);
+ }
+
+ return group;
+}
+
+std::shared_ptr<CSettingCategory> CAddonSettings::ParseOldCategoryElement(
+ uint32_t& categoryId, const TiXmlElement* categoryElement, std::set<std::string>& settingIds)
+{
+ // create the category
+ auto category = std::make_shared<CSettingCategory>(StringUtils::Format("category{}", categoryId),
+ GetSettingsManager());
+ categoryId += 1;
+
+ // try to get the category's label and fall back to "General"
+ int categoryLabel = 128;
+ ParseOldLabel(categoryElement, g_localizeStrings.Get(categoryLabel), categoryLabel);
+ category->SetLabel(categoryLabel);
+
+ // prepare a setting group
+ auto group = ParseOldSettingElement(categoryElement, category, settingIds);
+
+ // add the group to the category
+ category->AddGroup(group);
+
+ return category;
+}
+
+bool CAddonSettings::InitializeFromOldSettingDefinitions(const CXBMCTinyXML& doc)
+{
+ m_logger->debug("trying to load setting definitions from old format...");
+
+ const TiXmlElement* root = doc.RootElement();
+ if (root == nullptr)
+ return false;
+
+ std::shared_ptr<CSettingSection> section =
+ std::make_shared<CSettingSection>(m_addonId, GetSettingsManager());
+
+ std::shared_ptr<CSettingCategory> category;
+ uint32_t categoryId = 0;
+
+ // Settings id set
+ std::set<std::string> settingIds;
+
+ // Special case for no category settings
+ section->AddCategory(ParseOldCategoryElement(categoryId, root, settingIds));
+
+ const TiXmlElement* categoryElement = root->FirstChildElement("category");
+ while (categoryElement != nullptr)
+ {
+ section->AddCategory(ParseOldCategoryElement(categoryId, categoryElement, settingIds));
+
+ // look for the next category
+ categoryElement = categoryElement->NextSiblingElement("category");
+ }
+
+ // add the section to the settingsmanager
+ GetSettingsManager()->AddSection(section);
+
+ return true;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingAction(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue)
+{
+ // parse the action attribute
+ std::string action = XMLUtils::GetAttribute(settingElement, "action");
+ // replace $CWD with the url of the add-on
+ StringUtils::Replace(action, "$CWD", m_addonPath);
+ // replace $ID with the id of the add-on
+ StringUtils::Replace(action, "$ID", m_addonId);
+
+ // prepare the setting's control
+ auto control = std::make_shared<CSettingControlButton>();
+ control->SetFormat("action");
+
+ SettingPtr setting = nullptr;
+ // action settings don't require a setting id
+ if (settingId.empty())
+ {
+ auto actionSettingId = StringUtils::Format("action{}", m_unidentifiedSettingId);
+ m_unidentifiedSettingId += 1;
+
+ auto settingAction = std::make_shared<CSettingAction>(actionSettingId, GetSettingsManager());
+ settingAction->SetData(action);
+
+ setting = settingAction;
+ }
+ else
+ {
+ // assume that the setting might store a value as a string
+ auto settingString = std::make_shared<CSettingString>(settingId, GetSettingsManager());
+ settingString->SetDefault(defaultValue);
+ settingString->SetAllowEmpty(true);
+
+ control->SetActionData(action);
+
+ setting = settingString;
+ }
+
+ // get any options
+ std::string option = XMLUtils::GetAttribute(settingElement, "option");
+ // handle the "close" option
+ if (StringUtils::EqualsNoCase(option, "close"))
+ control->SetCloseDialog(true);
+
+ setting->SetControl(control);
+
+ return setting;
+}
+
+std::shared_ptr<CSetting> CAddonSettings::InitializeFromOldSettingLabel()
+{
+ // label settings don't require a setting id
+ auto labelSettingId = StringUtils::Format("label{}", m_unidentifiedSettingId);
+ m_unidentifiedSettingId += 1;
+
+ auto settingLabel = std::make_shared<CSettingString>(labelSettingId, GetSettingsManager());
+
+ // create the setting's control
+ settingLabel->SetControl(std::make_shared<CSettingControlLabel>());
+
+ return settingLabel;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingBool(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue)
+{
+ auto setting = std::make_shared<CSettingBool>(settingId, GetSettingsManager());
+ if (setting->FromString(defaultValue))
+ setting->SetDefault(setting->GetValue());
+
+ setting->SetControl(std::make_shared<CSettingControlCheckmark>());
+
+ return setting;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingTextIpAddress(const std::string& settingId,
+ const std::string& settingType,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel)
+{
+ std::shared_ptr<CSettingString> setting;
+ auto control = std::make_shared<CSettingControlEdit>();
+ control->SetHeading(settingLabel);
+
+ // get any options
+ std::string option = XMLUtils::GetAttribute(settingElement, "option");
+
+ if (settingType == "ipaddress")
+ {
+ setting = std::make_shared<CSettingString>(settingId, GetSettingsManager());
+ control->SetFormat("ip");
+ }
+ else if (settingType == "text")
+ {
+
+ if (StringUtils::EqualsNoCase(option, "urlencoded"))
+ {
+ setting = std::make_shared<CSettingUrlEncodedString>(settingId, GetSettingsManager());
+ control->SetFormat("urlencoded");
+ }
+ else
+ {
+ setting = std::make_shared<CSettingString>(settingId, GetSettingsManager());
+ control->SetFormat("string");
+ control->SetHidden(StringUtils::EqualsNoCase(option, "hidden"));
+ }
+ }
+
+ setting->SetDefault(defaultValue);
+ setting->SetAllowEmpty(true);
+ setting->SetControl(control);
+
+ return setting;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingNumber(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel)
+{
+ auto setting = std::make_shared<CSettingInt>(settingId, GetSettingsManager());
+ if (setting->FromString(defaultValue))
+ setting->SetDefault(setting->GetValue());
+
+ auto control = std::make_shared<CSettingControlEdit>();
+ control->SetHeading(settingLabel);
+ control->SetFormat("integer");
+ setting->SetControl(control);
+
+ return setting;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingPath(const std::string& settingId,
+ const std::string& settingType,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel)
+{
+ auto setting = std::make_shared<CSettingPath>(settingId, GetSettingsManager());
+ setting->SetDefault(defaultValue);
+
+ // parse sources/shares
+ const auto source = XMLUtils::GetAttribute(settingElement, "source");
+ if (!source.empty())
+ setting->SetSources({source});
+
+ // setup masking
+ const auto audioMask = CServiceBroker::GetFileExtensionProvider().GetMusicExtensions();
+ const auto videoMask = CServiceBroker::GetFileExtensionProvider().GetVideoExtensions();
+ const auto imageMask = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ auto execMask = "";
+#if defined(TARGET_WINDOWS)
+ execMask = ".exe|.bat|.cmd|.py";
+#endif // defined(TARGET_WINDOWS)
+
+ std::string mask = XMLUtils::GetAttribute(settingElement, "mask");
+ if (!mask.empty())
+ {
+ // convert mask qualifiers
+ StringUtils::Replace(mask, "$AUDIO", audioMask);
+ StringUtils::Replace(mask, "$VIDEO", videoMask);
+ StringUtils::Replace(mask, "$IMAGE", imageMask);
+ StringUtils::Replace(mask, "$EXECUTABLE", execMask);
+ }
+ else
+ {
+ if (settingType == "video")
+ mask = videoMask;
+ else if (settingType == "audio")
+ mask = audioMask;
+ else if (settingType == "image")
+ mask = imageMask;
+ else if (settingType == "executable")
+ mask = execMask;
+ }
+ setting->SetMasking(mask);
+
+ // parse options
+ const auto option = XMLUtils::GetAttribute(settingElement, "option");
+ setting->SetWritable(StringUtils::EqualsNoCase(option, "writeable"));
+
+ auto control = std::make_shared<CSettingControlButton>();
+ if (settingType == "folder")
+ control->SetFormat("path");
+ else if (settingType == "image")
+ control->SetFormat("image");
+ else
+ {
+ control->SetFormat("file");
+
+ // parse the options
+ const auto options = StringUtils::Split(option, OldSettingValuesSeparator);
+ control->SetUseImageThumbs(std::find(options.cbegin(), options.cend(), "usethumbs") !=
+ options.cend());
+ control->SetUseFileDirectories(std::find(options.cbegin(), options.cend(), "treatasfolder") !=
+ options.cend());
+ }
+ control->SetHeading(settingLabel);
+ setting->SetControl(control);
+
+ return setting;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingDate(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel)
+{
+ auto setting = std::make_shared<CSettingDate>(settingId, GetSettingsManager());
+ if (setting->FromString(defaultValue))
+ setting->SetDefault(setting->GetValue());
+
+ auto control = std::make_shared<CSettingControlButton>();
+ control->SetFormat("date");
+ control->SetHeading(settingLabel);
+ setting->SetControl(control);
+
+ return setting;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingTime(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel)
+{
+ auto setting = std::make_shared<CSettingTime>(settingId, GetSettingsManager());
+ if (setting->FromString(defaultValue))
+ setting->SetDefault(setting->GetValue());
+
+ auto control = std::make_shared<CSettingControlButton>();
+ control->SetFormat("time");
+ control->SetHeading(settingLabel);
+ setting->SetControl(control);
+
+ return setting;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingSelect(
+ const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel,
+ const std::string& settingValues,
+ const std::vector<std::string>& settingLValues)
+{
+ // process values and lvalues
+ std::vector<std::string> values;
+ if (!settingLValues.empty())
+ values = settingLValues;
+ else
+ values = StringUtils::Split(settingValues, OldSettingValuesSeparator);
+
+ SettingPtr setting = nullptr;
+ if (!values.empty())
+ {
+ if (settingLValues.empty())
+ {
+ auto settingString = std::make_shared<CSettingString>(settingId, GetSettingsManager());
+ settingString->SetDefault(defaultValue);
+
+ StringSettingOptions options;
+ for (const auto& value : values)
+ options.push_back(StringSettingOption(value, value));
+ settingString->SetOptions(options);
+
+ setting = settingString;
+ }
+ else
+ {
+ auto settingInt = std::make_shared<CSettingInt>(settingId, GetSettingsManager());
+ if (settingInt->FromString(defaultValue))
+ settingInt->SetDefault(settingInt->GetValue());
+
+ TranslatableIntegerSettingOptions options;
+ for (uint32_t i = 0; i < values.size(); ++i)
+ options.push_back(TranslatableIntegerSettingOption(
+ static_cast<int>(strtol(values[i].c_str(), nullptr, 0)), i));
+ settingInt->SetTranslatableOptions(options);
+
+ setting = settingInt;
+ }
+ }
+ else
+ {
+ // parse sources/shares
+ const auto source = XMLUtils::GetAttribute(settingElement, "source");
+ if (!source.empty())
+ setting = InitializeFromOldSettingFileWithSource(settingId, settingElement, defaultValue,
+ settingValues);
+ else
+ m_logger->warn("failed to parse old setting definition for \"{}\" of type \"select\"",
+ settingId);
+ }
+
+ if (setting != nullptr)
+ {
+ auto control = std::make_shared<CSettingControlList>();
+ control->SetHeading(settingLabel);
+ control->SetFormat("string");
+ setting->SetControl(control);
+ }
+
+ return setting;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingAddon(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel)
+{
+ // get addon types
+ std::string addonTypeStr = XMLUtils::GetAttribute(settingElement, "addontype");
+ const auto addonTypesStr = StringUtils::Split(addonTypeStr, ",");
+ std::set<AddonType> addonTypes;
+ for (auto addonType : addonTypesStr)
+ {
+ auto type = ADDON::CAddonInfo::TranslateType(StringUtils::Trim(addonType));
+ if (type != ADDON::AddonType::UNKNOWN)
+ addonTypes.insert(type);
+ }
+
+ if (addonTypes.empty())
+ {
+ m_logger->error("missing addon type for addon setting \"{}\"", settingId);
+ return nullptr;
+ }
+
+ // TODO: support multiple addon types
+ if (addonTypes.size() > 1)
+ {
+ m_logger->error("multiple addon types are not supported (addon setting \"{}\")", settingId);
+ return nullptr;
+ }
+
+ // parse addon ids
+ auto addonIds = StringUtils::Split(defaultValue, ",");
+
+ // parse multiselect option
+ bool multiselect = false;
+ settingElement->QueryBoolAttribute("multiselect", &multiselect);
+
+ // sanity check
+ if (addonIds.size() > 1 && !multiselect)
+ {
+ m_logger->warn("multiple default addon ids on non-multiselect addon setting \"{}\"", settingId);
+ addonIds.erase(++addonIds.begin(), addonIds.end());
+ }
+
+ auto settingAddon = std::make_shared<CSettingAddon>(settingId, GetSettingsManager());
+ settingAddon->SetAddonType(*addonTypes.begin());
+
+ SettingPtr setting = settingAddon;
+ if (multiselect)
+ {
+ auto settingList =
+ std::make_shared<CSettingList>(settingId, settingAddon, GetSettingsManager());
+ settingList->SetDelimiter(",");
+ if (settingList->FromString(addonIds))
+ settingList->SetDefault(settingList->GetValue());
+
+ setting = settingList;
+ }
+ else if (!addonIds.empty())
+ settingAddon->SetDefault(addonIds.front());
+
+ auto control = std::make_shared<CSettingControlButton>();
+ control->SetFormat("addon");
+ control->SetHeading(settingLabel);
+ setting->SetControl(control);
+
+ return setting;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingEnums(
+ const std::string& settingId,
+ const std::string& settingType,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const std::string& settingValues,
+ const std::vector<std::string>& settingLValues)
+{
+ // process values and lvalues
+ std::vector<std::string> values;
+ if (!settingLValues.empty())
+ values = settingLValues;
+ else if (settingValues == "$HOURS")
+ {
+ for (uint32_t hour = 0; hour < 24; hour++)
+ values.push_back(
+ CDateTime(2000, 1, 1, hour, 0, 0).GetAsLocalizedTime(g_langInfo.GetTimeFormat(), false));
+ }
+ else
+ values = StringUtils::Split(settingValues, OldSettingValuesSeparator);
+
+ // process entries
+ const auto settingEntries = StringUtils::Split(XMLUtils::GetAttribute(settingElement, "entries"),
+ OldSettingValuesSeparator);
+
+ // process sort
+ bool sortAscending = false;
+ std::string sort = XMLUtils::GetAttribute(settingElement, "sort");
+ if (sort == "true" || sort == "yes")
+ sortAscending = true;
+
+ SettingPtr setting = nullptr;
+ if (settingType == "enum")
+ {
+ auto settingInt = std::make_shared<CSettingInt>(settingId, GetSettingsManager());
+
+ if (settingLValues.empty())
+ {
+ IntegerSettingOptions options;
+ for (uint32_t i = 0; i < values.size(); ++i)
+ {
+ std::string label = values[i];
+ int value = i;
+ if (settingEntries.size() > i)
+ value = static_cast<int>(strtol(settingEntries[i].c_str(), nullptr, 0));
+
+ options.push_back(IntegerSettingOption(label, value));
+ }
+
+ settingInt->SetOptions(options);
+ }
+ else
+ {
+ TranslatableIntegerSettingOptions options;
+ for (uint32_t i = 0; i < values.size(); ++i)
+ {
+ int label = static_cast<int>(strtol(values[i].c_str(), nullptr, 0));
+ int value = i;
+ if (settingEntries.size() > i)
+ value = static_cast<int>(strtol(settingEntries[i].c_str(), nullptr, 0));
+
+ options.push_back(TranslatableIntegerSettingOption(label, value));
+ }
+
+ settingInt->SetTranslatableOptions(options);
+ }
+
+ if (sortAscending)
+ settingInt->SetOptionsSort(SettingOptionsSort::Ascending);
+
+ // set the default value
+ if (settingInt->FromString(defaultValue))
+ settingInt->SetDefault(settingInt->GetValue());
+
+ setting = settingInt;
+ }
+ else
+ {
+ auto settingString = std::make_shared<CSettingString>(settingId, GetSettingsManager());
+
+ if (settingLValues.empty())
+ {
+ StringSettingOptions options;
+ for (uint32_t i = 0; i < values.size(); ++i)
+ {
+ std::string value = values[i];
+ if (settingEntries.size() > i)
+ value = settingEntries[i];
+
+ options.push_back(StringSettingOption(value, value));
+ }
+
+ settingString->SetOptions(options);
+ }
+ else
+ {
+ TranslatableStringSettingOptions options;
+ for (uint32_t i = 0; i < values.size(); ++i)
+ {
+ int label = static_cast<int>(strtol(values[i].c_str(), nullptr, 0));
+ std::string value = g_localizeStrings.GetAddonString(m_addonId, label);
+ if (settingEntries.size() > i)
+ value = settingEntries[i];
+
+ options.push_back(std::make_pair(label, value));
+ }
+
+ settingString->SetTranslatableOptions(options);
+ }
+
+ if (sortAscending)
+ settingString->SetOptionsSort(SettingOptionsSort::Ascending);
+
+ // set the default value
+ settingString->SetDefault(defaultValue);
+
+ setting = settingString;
+ }
+
+ auto control = std::make_shared<CSettingControlSpinner>();
+ control->SetFormat("string");
+ setting->SetControl(control);
+
+ return setting;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingFileEnum(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const std::string& settingValues)
+{
+ auto setting = InitializeFromOldSettingFileWithSource(settingId, settingElement, defaultValue,
+ settingValues);
+
+ auto control = std::make_shared<CSettingControlSpinner>();
+ control->SetFormat("string");
+ setting->SetControl(control);
+
+ return setting;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingRangeOfNum(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue)
+{
+ auto setting = std::make_shared<CSettingNumber>(settingId, GetSettingsManager());
+ if (setting->FromString(defaultValue))
+ setting->SetDefault(setting->GetValue());
+
+ // parse rangestart and rangeend
+ double rangeStart = 0.0, rangeEnd = 1.0;
+ settingElement->QueryDoubleAttribute("rangestart", &rangeStart);
+ settingElement->QueryDoubleAttribute("rangeend", &rangeEnd);
+ setting->SetMinimum(rangeStart);
+ setting->SetMaximum(rangeEnd);
+
+ // parse elements
+ uint32_t elements = 2;
+ settingElement->QueryUnsignedAttribute("elements", &elements);
+ if (elements > 1)
+ setting->SetStep((rangeEnd - rangeStart) / (elements - 1));
+
+ // parse valueformat
+ int valueFormat = -1;
+ settingElement->QueryIntAttribute("valueformat", &valueFormat);
+
+ auto control = std::make_shared<CSettingControlSpinner>();
+ control->SetFormat("string");
+ control->SetFormatLabel(valueFormat);
+ setting->SetControl(control);
+
+ return setting;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingSlider(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue)
+{
+ // parse range
+ double min = 0.0, max = 100.0, step = 1.0;
+ const auto range = StringUtils::Split(XMLUtils::GetAttribute(settingElement, "range"), ',');
+
+ if (range.size() > 1)
+ {
+ min = strtod(range[0].c_str(), nullptr);
+
+ if (range.size() > 2)
+ {
+ max = strtod(range[2].c_str(), nullptr);
+ step = strtod(range[1].c_str(), nullptr);
+ }
+ else
+ max = strtod(range[1].c_str(), nullptr);
+ }
+
+ // parse option
+ auto option = XMLUtils::GetAttribute(settingElement, "option");
+ if (option.empty() || StringUtils::EqualsNoCase(option, "float"))
+ {
+ auto setting = std::make_shared<CSettingNumber>(settingId, GetSettingsManager());
+ if (setting->FromString(defaultValue))
+ setting->SetDefault(setting->GetValue());
+
+ setting->SetMinimum(min);
+ setting->SetStep(step);
+ setting->SetMaximum(max);
+
+ auto control = std::make_shared<CSettingControlSlider>();
+ control->SetFormat("number");
+ control->SetPopup(false);
+ setting->SetControl(control);
+
+ return setting;
+ }
+
+ if (StringUtils::EqualsNoCase(option, "int") || StringUtils::EqualsNoCase(option, "percent"))
+ {
+ auto setting = std::make_shared<CSettingInt>(settingId, GetSettingsManager());
+ if (setting->FromString(defaultValue))
+ setting->SetDefault(setting->GetValue());
+
+ setting->SetMinimum(static_cast<int>(min));
+ setting->SetStep(static_cast<int>(step));
+ setting->SetMaximum(static_cast<int>(max));
+
+ auto control = std::make_shared<CSettingControlSlider>();
+ control->SetFormat(StringUtils::EqualsNoCase(option, "int") ? "integer" : "percentage");
+ control->SetPopup(false);
+ setting->SetControl(control);
+
+ return setting;
+ }
+
+ m_logger->warn("ignoring old setting definition for \"{}\" of type \"slider\" because of unknown "
+ "option \"{}\"",
+ settingId, option);
+
+ return nullptr;
+}
+
+SettingPtr CAddonSettings::InitializeFromOldSettingFileWithSource(
+ const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ std::string source)
+{
+ auto setting = std::make_shared<CSettingPath>(settingId, GetSettingsManager());
+ setting->SetDefault(defaultValue);
+
+ if (source.find("$PROFILE") != std::string::npos)
+ StringUtils::Replace(source, "$PROFILE", m_addonProfile);
+ else
+ source = URIUtils::AddFileToFolder(m_addonPath, source);
+
+ setting->SetSources({source});
+
+ // process the path/file mask
+ setting->SetMasking(XMLUtils::GetAttribute(settingElement, "mask"));
+
+ // process option
+ std::string option = XMLUtils::GetAttribute(settingElement, "option");
+ setting->SetHideExtension(StringUtils::EqualsNoCase(option, "hideext"));
+
+ setting->SetOptionsFiller(FileEnumSettingOptionsFiller);
+
+ return setting;
+}
+
+bool CAddonSettings::LoadOldSettingValues(const CXBMCTinyXML& doc,
+ std::map<std::string, std::string>& settings) const
+{
+ if (!doc.RootElement())
+ return false;
+
+ const TiXmlElement* category = doc.RootElement()->FirstChildElement("category");
+ if (category == nullptr)
+ category = doc.RootElement();
+
+ while (category != nullptr)
+ {
+ const TiXmlElement* setting = category->FirstChildElement("setting");
+ while (setting != nullptr)
+ {
+ const char* id = setting->Attribute("id");
+ const char* value = setting->Attribute("value");
+ if (id != nullptr && value != nullptr)
+ settings[id] = value;
+
+ setting = setting->NextSiblingElement("setting");
+ }
+
+ category = category->NextSiblingElement("category");
+ }
+
+ return !settings.empty();
+}
+
+bool CAddonSettings::ParseOldLabel(const TiXmlElement* element,
+ const std::string& settingId,
+ int& labelId)
+{
+ labelId = -1;
+ if (element == nullptr)
+ return false;
+
+ // label value as a string
+ std::string labelString;
+ element->QueryStringAttribute("label", &labelString);
+
+ bool parsed = !labelString.empty();
+
+ // try to parse the label as a pure number, i.e. a localized string
+ if (parsed)
+ {
+ char* endptr;
+ labelId = std::strtol(labelString.c_str(), &endptr, 10);
+ if (endptr == nullptr || *endptr == '\0')
+ return true;
+ }
+ // make sure the label string is not empty
+ else
+ labelString = " ";
+
+ labelId = m_unknownSettingLabelId;
+ m_unknownSettingLabelId += 1;
+ m_unknownSettingLabels.emplace(labelId, labelString);
+
+ return parsed;
+}
+
+bool CAddonSettings::ParseOldCondition(const std::shared_ptr<const CSetting>& setting,
+ const std::vector<std::shared_ptr<const CSetting>>& settings,
+ const std::string& condition,
+ CSettingDependency& dependeny) const
+{
+ if (setting == nullptr)
+ return false;
+
+ if (condition.empty())
+ return true;
+
+ // find the index of the setting in the list of all settings of the category
+ auto settingIt = std::find_if(settings.cbegin(), settings.cend(),
+ [setting](const SettingConstPtr& otherSetting) {
+ if (otherSetting == nullptr)
+ return false;
+
+ return setting->GetId() == otherSetting->GetId();
+ });
+ if (settingIt == settings.cend())
+ {
+ m_logger->warn("failed to parse old setting conditions \"{}\" for \"{}\"", condition,
+ setting->GetId());
+ return false;
+ }
+ int32_t currentSettingIndex = std::distance(settings.cbegin(), settingIt);
+
+ CSettingDependencyConditionCombinationPtr dependencyCombination;
+ std::vector<std::string> conditions;
+ if (condition.find('+') != std::string::npos)
+ {
+ StringUtils::Tokenize(condition, conditions, '+');
+ dependencyCombination = dependeny.And();
+ }
+ else
+ {
+ StringUtils::Tokenize(condition, conditions, '|');
+ dependencyCombination = dependeny.Or();
+ }
+
+ bool error = false;
+ for (const auto& cond : conditions)
+ {
+ ConditionExpression expression;
+ if (!ParseOldConditionExpression(cond, expression))
+ continue;
+
+ // determine the absolute setting index
+ int32_t absoluteSettingIndex = currentSettingIndex + expression.m_relativeSettingIndex;
+
+ // we cannot handle relative indices pointing to settings not belonging to the same category
+ if (absoluteSettingIndex < 0 || static_cast<size_t>(absoluteSettingIndex) >= settings.size())
+ {
+ m_logger->warn("cannot reference setting (relative index: {}; absolute index: {}) in another "
+ "category in old setting condition \"{}\" for \"{}\"",
+ expression.m_relativeSettingIndex, absoluteSettingIndex, cond,
+ setting->GetId());
+ error = true;
+ continue;
+ }
+
+ const SettingConstPtr& referencedSetting = settings.at(absoluteSettingIndex);
+ if (referencedSetting == nullptr)
+ {
+ m_logger->warn(
+ "cannot reference separator setting in old setting condition \"{}\" for \"{}\"", cond,
+ setting->GetId());
+ error = true;
+ continue;
+ }
+
+ // try to handle some odd cases where the setting is of type string but the comparison value references the index of the value in the list of options
+ if (referencedSetting->GetType() == SettingType::String &&
+ StringUtils::IsNaturalNumber(expression.m_value))
+ {
+ // try to parse the comparison value
+ size_t valueIndex = static_cast<size_t>(strtoul(expression.m_value.c_str(), nullptr, 10));
+
+ const auto referencedSettingString =
+ std::static_pointer_cast<const CSettingString>(referencedSetting);
+ switch (referencedSettingString->GetOptionsType())
+ {
+ case SettingOptionsType::Static:
+ {
+ const auto& options = referencedSettingString->GetOptions();
+ if (options.size() > valueIndex)
+ expression.m_value = options.at(valueIndex).value;
+ break;
+ }
+
+ case SettingOptionsType::StaticTranslatable:
+ {
+ const auto& options = referencedSettingString->GetTranslatableOptions();
+ if (options.size() > valueIndex)
+ expression.m_value = options.at(valueIndex).second;
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+
+ // add the condition to the value of the referenced setting
+ dependencyCombination->Add(std::make_shared<CSettingDependencyCondition>(
+ referencedSetting->GetId(), expression.m_value, expression.m_operator, expression.m_negated,
+ GetSettingsManager()));
+ }
+
+ // if the condition doesn't depend on other settings it might be an infobool expression
+ if (!error && dependencyCombination->GetOperations().empty() &&
+ dependencyCombination->GetValues().empty())
+ dependencyCombination->Add(std::make_shared<CSettingDependencyCondition>(
+ "InfoBool", condition, "", false, GetSettingsManager()));
+
+ return !error;
+}
+
+bool CAddonSettings::ParseOldConditionExpression(std::string str, ConditionExpression& expression)
+{
+ StringUtils::Trim(str);
+
+ size_t posOpen = str.find('(');
+ size_t posSep = str.find(',', posOpen);
+ size_t posClose = str.find(')', posSep);
+
+ if (posOpen == std::string::npos || posSep == std::string::npos || posClose == std::string::npos)
+ return false;
+
+ auto op = str.substr(0, posOpen);
+
+ // check if the operator is negated
+ expression.m_negated = StringUtils::StartsWith(op, "!");
+ if (expression.m_negated)
+ op = op.substr(1);
+
+ // parse the operator
+ if (StringUtils::EqualsNoCase(op, "eq"))
+ expression.m_operator = SettingDependencyOperator::Equals;
+ else if (StringUtils::EqualsNoCase(op, "gt"))
+ expression.m_operator = SettingDependencyOperator::GreaterThan;
+ else if (StringUtils::EqualsNoCase(op, "lt"))
+ expression.m_operator = SettingDependencyOperator::LessThan;
+ else
+ return false;
+
+ expression.m_relativeSettingIndex = static_cast<int32_t>(
+ strtol(str.substr(posOpen + 1, posSep - posOpen - 1).c_str(), nullptr, 10));
+ expression.m_value = str.substr(posSep + 1, posClose - posSep - 1);
+
+ return true;
+}
+
+void CAddonSettings::FileEnumSettingOptionsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ if (setting == nullptr)
+ return;
+
+ auto settingPath = std::dynamic_pointer_cast<const CSettingPath>(setting);
+ if (settingPath == nullptr)
+ return;
+
+ if (settingPath->GetSources().empty())
+ return;
+
+ const std::string& masking = settingPath->GetMasking(CServiceBroker::GetFileExtensionProvider());
+
+ // fetch the matching files/directories
+ CFileItemList items;
+ XFILE::CDirectory::GetDirectory(settingPath->GetSources().front(), items, masking,
+ XFILE::DIR_FLAG_NO_FILE_DIRS);
+
+ // process the matching files/directories
+ for (const auto& item : items)
+ {
+ if ((masking == "/" && item->m_bIsFolder) || !item->m_bIsFolder)
+ {
+ if (settingPath->HideExtension())
+ item->RemoveExtension();
+ list.emplace_back(item->GetLabel(), item->GetLabel());
+ }
+ }
+}
+
+} // namespace ADDON
diff --git a/xbmc/addons/settings/AddonSettings.h b/xbmc/addons/settings/AddonSettings.h
new file mode 100644
index 0000000..811ded1
--- /dev/null
+++ b/xbmc/addons/settings/AddonSettings.h
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2017-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 "addons/IAddon.h"
+#include "settings/SettingControl.h"
+#include "settings/SettingCreator.h"
+#include "settings/SettingsBase.h"
+#include "settings/lib/ISettingCallback.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+enum class SettingDependencyOperator;
+
+class CSettingCategory;
+class CSettingGroup;
+class CSettingDependency;
+class CXBMCTinyXML;
+
+struct StringSettingOption;
+
+namespace ADDON
+{
+
+class IAddon;
+class IAddonInstanceHandler;
+
+class CAddonSettings : public CSettingControlCreator,
+ public CSettingCreator,
+ public CSettingsBase,
+ public ISettingCallback
+{
+public:
+ CAddonSettings(const std::shared_ptr<const IAddon>& addon, AddonInstanceId instanceId);
+ ~CAddonSettings() override = default;
+
+ // specialization of CSettingsBase
+ bool Initialize() override { return false; }
+
+ // implementations of CSettingsBase
+ bool Load() override { return false; }
+ bool Save() override { return false; }
+
+ // specialization of CSettingCreator
+ std::shared_ptr<CSetting> CreateSetting(
+ const std::string& settingType,
+ const std::string& settingId,
+ CSettingsManager* settingsManager = nullptr) const override;
+
+ // implementation of ISettingCallback
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ const std::string& GetAddonId() const { return m_addonId; }
+
+ bool Initialize(const CXBMCTinyXML& doc, bool allowEmpty = false);
+ bool Load(const CXBMCTinyXML& doc);
+ bool Save(CXBMCTinyXML& doc) const;
+
+ bool HasSettings() const;
+
+ std::string GetSettingLabel(int label) const;
+
+ std::shared_ptr<CSetting> AddSetting(const std::string& settingId, bool value);
+ std::shared_ptr<CSetting> AddSetting(const std::string& settingId, int value);
+ std::shared_ptr<CSetting> AddSetting(const std::string& settingId, double value);
+ std::shared_ptr<CSetting> AddSetting(const std::string& settingId, const std::string& value);
+
+protected:
+ // specializations of CSettingsBase
+ void InitializeSettingTypes() override;
+ void InitializeControls() override;
+ void InitializeConditions() override;
+
+ // implementation of CSettingsBase
+ bool InitializeDefinitions() override { return false; }
+
+private:
+ bool AddInstanceSettings();
+ bool InitializeDefinitions(const CXBMCTinyXML& doc);
+
+ bool ParseSettingVersion(const CXBMCTinyXML& doc, uint32_t& version) const;
+
+ std::shared_ptr<CSettingGroup> ParseOldSettingElement(
+ const TiXmlElement* categoryElement,
+ const std::shared_ptr<CSettingCategory>& category,
+ std::set<std::string>& settingIds);
+
+ std::shared_ptr<CSettingCategory> ParseOldCategoryElement(uint32_t& categoryId,
+ const TiXmlElement* categoryElement,
+ std::set<std::string>& settingIds);
+
+ bool InitializeFromOldSettingDefinitions(const CXBMCTinyXML& doc);
+ std::shared_ptr<CSetting> InitializeFromOldSettingAction(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue);
+ std::shared_ptr<CSetting> InitializeFromOldSettingLabel();
+ std::shared_ptr<CSetting> InitializeFromOldSettingBool(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue);
+ std::shared_ptr<CSetting> InitializeFromOldSettingTextIpAddress(
+ const std::string& settingId,
+ const std::string& settingType,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel);
+ std::shared_ptr<CSetting> InitializeFromOldSettingNumber(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel);
+ std::shared_ptr<CSetting> InitializeFromOldSettingPath(const std::string& settingId,
+ const std::string& settingType,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel);
+ std::shared_ptr<CSetting> InitializeFromOldSettingDate(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel);
+ std::shared_ptr<CSetting> InitializeFromOldSettingTime(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel);
+ std::shared_ptr<CSetting> InitializeFromOldSettingSelect(
+ const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel,
+ const std::string& settingValues,
+ const std::vector<std::string>& settingLValues);
+ std::shared_ptr<CSetting> InitializeFromOldSettingAddon(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const int settingLabel);
+ std::shared_ptr<CSetting> InitializeFromOldSettingEnums(
+ const std::string& settingId,
+ const std::string& settingType,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const std::string& settingValues,
+ const std::vector<std::string>& settingLValues);
+ std::shared_ptr<CSetting> InitializeFromOldSettingFileEnum(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ const std::string& settingValues);
+ std::shared_ptr<CSetting> InitializeFromOldSettingRangeOfNum(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue);
+ std::shared_ptr<CSetting> InitializeFromOldSettingSlider(const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue);
+ std::shared_ptr<CSetting> InitializeFromOldSettingFileWithSource(
+ const std::string& settingId,
+ const TiXmlElement* settingElement,
+ const std::string& defaultValue,
+ std::string source);
+
+ bool LoadOldSettingValues(const CXBMCTinyXML& doc,
+ std::map<std::string, std::string>& settings) const;
+
+ struct ConditionExpression
+ {
+ SettingDependencyOperator m_operator;
+ bool m_negated;
+ int32_t m_relativeSettingIndex;
+ std::string m_value;
+ };
+
+ bool ParseOldLabel(const TiXmlElement* element, const std::string& settingId, int& labelId);
+ bool ParseOldCondition(const std::shared_ptr<const CSetting>& setting,
+ const std::vector<std::shared_ptr<const CSetting>>& settings,
+ const std::string& condition,
+ CSettingDependency& dependeny) const;
+ static bool ParseOldConditionExpression(std::string str, ConditionExpression& expression);
+
+ static void FileEnumSettingOptionsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+
+ // store these values so that we don't always have to access the weak pointer
+ const std::string m_addonId;
+ const std::string m_addonPath;
+ const std::string m_addonProfile;
+ const AddonInstanceId m_instanceId{ADDON_SETTINGS_ID};
+
+ uint32_t m_unidentifiedSettingId;
+ int m_unknownSettingLabelId;
+ std::map<int, std::string> m_unknownSettingLabels;
+
+ Logger m_logger;
+};
+
+} // namespace ADDON
diff --git a/xbmc/addons/settings/CMakeLists.txt b/xbmc/addons/settings/CMakeLists.txt
new file mode 100644
index 0000000..6774f68
--- /dev/null
+++ b/xbmc/addons/settings/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES AddonSettings.cpp
+ SettingUrlEncodedString.cpp)
+
+set(HEADERS AddonSettings.h
+ SettingUrlEncodedString.h)
+
+core_add_library(addons_settings)
diff --git a/xbmc/addons/settings/SettingUrlEncodedString.cpp b/xbmc/addons/settings/SettingUrlEncodedString.cpp
new file mode 100644
index 0000000..f09875f
--- /dev/null
+++ b/xbmc/addons/settings/SettingUrlEncodedString.cpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017-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 "SettingUrlEncodedString.h"
+
+#include "URL.h"
+#include "settings/lib/SettingsManager.h"
+
+namespace ADDON
+{
+
+CSettingUrlEncodedString::CSettingUrlEncodedString(
+ const std::string& id, CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingString(id, settingsManager)
+{ }
+
+CSettingUrlEncodedString::CSettingUrlEncodedString(
+ const std::string& id,
+ int label,
+ const std::string& value,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingString(id, label, value, settingsManager)
+{ }
+
+CSettingUrlEncodedString::CSettingUrlEncodedString(const std::string &id, const CSettingUrlEncodedString &setting)
+ : CSettingString(id, setting)
+{ }
+
+std::string CSettingUrlEncodedString::GetDecodedValue() const
+{
+ return CURL::Decode(CSettingString::GetValue());
+}
+
+bool CSettingUrlEncodedString::SetDecodedValue(const std::string &decodedValue)
+{
+ return CSettingString::SetValue(CURL::Encode(decodedValue));
+}
+
+} /* namespace ADDON */
diff --git a/xbmc/addons/settings/SettingUrlEncodedString.h b/xbmc/addons/settings/SettingUrlEncodedString.h
new file mode 100644
index 0000000..80bad4d
--- /dev/null
+++ b/xbmc/addons/settings/SettingUrlEncodedString.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017-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/Setting.h"
+
+class CSettingsManager;
+
+namespace ADDON
+{
+ class CSettingUrlEncodedString : public CSettingString
+ {
+ public:
+ CSettingUrlEncodedString(const std::string& id, CSettingsManager* settingsManager = nullptr);
+ CSettingUrlEncodedString(const std::string& id,
+ int label,
+ const std::string& value,
+ CSettingsManager* settingsManager = nullptr);
+ CSettingUrlEncodedString(const std::string &id, const CSettingUrlEncodedString &setting);
+ ~CSettingUrlEncodedString() override = default;
+
+ SettingPtr Clone(const std::string &id) const override { return std::make_shared<CSettingUrlEncodedString>(id, *this); }
+
+ std::string GetDecodedValue() const;
+ bool SetDecodedValue(const std::string& decodedValue);
+ };
+}
diff --git a/xbmc/addons/test/CMakeLists.txt b/xbmc/addons/test/CMakeLists.txt
new file mode 100644
index 0000000..3365972
--- /dev/null
+++ b/xbmc/addons/test/CMakeLists.txt
@@ -0,0 +1,6 @@
+set(SOURCES TestAddonBuilder.cpp
+ TestAddonDatabase.cpp
+ TestAddonInfoBuilder.cpp
+ TestAddonVersion.cpp)
+
+core_add_test_library(addons_test)
diff --git a/xbmc/addons/test/TestAddonBuilder.cpp b/xbmc/addons/test/TestAddonBuilder.cpp
new file mode 100644
index 0000000..08f6c6d
--- /dev/null
+++ b/xbmc/addons/test/TestAddonBuilder.cpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016-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 "addons/AddonBuilder.h"
+#include "addons/LanguageResource.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonInfoBuilder.h"
+#include "addons/addoninfo/AddonType.h"
+
+#include <gtest/gtest.h>
+
+using namespace ADDON;
+
+
+class TestAddonBuilder : public ::testing::Test
+{
+protected:
+ TestAddonBuilder() = default;
+};
+
+TEST_F(TestAddonBuilder, ShouldFailWhenEmpty)
+{
+ EXPECT_EQ(nullptr, CAddonBuilder::Generate(nullptr, AddonType::UNKNOWN));
+}
+
+TEST_F(TestAddonBuilder, ShouldBuildDependencyAddons)
+{
+ std::vector<DependencyInfo> deps;
+ deps.emplace_back("a", CAddonVersion("1.0.0"), CAddonVersion("1.0.10"), false);
+
+ CAddonInfoBuilderFromDB builder;
+ builder.SetId("aa");
+ builder.SetDependencies(deps);
+ CAddonType addonType(AddonType::UNKNOWN);
+ builder.SetExtensions(addonType);
+ AddonPtr addon = CAddonBuilder::Generate(builder.get(), AddonType::UNKNOWN);
+ EXPECT_EQ(deps, addon->GetDependencies());
+}
+
+TEST_F(TestAddonBuilder, ShouldReturnDerivedType)
+{
+ CAddonInfoBuilderFromDB builder;
+ builder.SetId("aa");
+ CAddonType addonType(AddonType::RESOURCE_LANGUAGE);
+ builder.SetExtensions(addonType);
+ auto addon = std::dynamic_pointer_cast<CLanguageResource>(
+ CAddonBuilder::Generate(builder.get(), AddonType::UNKNOWN));
+ EXPECT_NE(nullptr, addon);
+}
diff --git a/xbmc/addons/test/TestAddonDatabase.cpp b/xbmc/addons/test/TestAddonDatabase.cpp
new file mode 100644
index 0000000..d5557dc
--- /dev/null
+++ b/xbmc/addons/test/TestAddonDatabase.cpp
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016-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 "addons/AddonDatabase.h"
+#include "addons/addoninfo/AddonInfoBuilder.h"
+#include "filesystem/SpecialProtocol.h"
+#include "settings/AdvancedSettings.h"
+
+#include <set>
+#include <utility>
+
+#include <gtest/gtest.h>
+
+using namespace ADDON;
+
+
+class AddonDatabaseTest : public ::testing::Test
+{
+protected:
+ DatabaseSettings settings;
+ CAddonDatabase database;
+
+ void SetUp() override
+ {
+ settings.type = "sqlite3";
+ settings.name = "test";
+ settings.host = CSpecialProtocol::TranslatePath("special://temp/");
+
+ database.Connect("test", settings, true);
+
+ std::set<std::string> installed{"repository.a", "repository.b"};
+ database.SyncInstalled(installed, installed, std::set<std::string>());
+
+ std::vector<AddonInfoPtr> addons;
+ CreateAddon(addons, "foo.bar", "1.0.0");
+ database.SetRepoUpdateData("repository.a", {});
+ database.UpdateRepositoryContent("repository.a", CAddonVersion("1.0.0"), "test", addons);
+
+ addons.clear();
+ CreateAddon(addons, "foo.baz", "1.1.0");
+ database.SetRepoUpdateData("repository.b", {});
+ database.UpdateRepositoryContent("repository.b", CAddonVersion("1.0.0"), "test", addons);
+ }
+
+ void CreateAddon(std::vector<AddonInfoPtr>& addons, std::string id, const std::string& version)
+ {
+ CAddonInfoBuilderFromDB builder;
+ builder.SetId(std::move(id));
+ builder.SetVersion(CAddonVersion(version));
+ addons.push_back(builder.get());
+ }
+
+ void TearDown() override
+ {
+ database.Close();
+ }
+};
+
+
+TEST_F(AddonDatabaseTest, TestFindById)
+{
+ VECADDONS addons;
+ EXPECT_TRUE(database.FindByAddonId("foo.baz", addons));
+ EXPECT_EQ(1U, addons.size());
+ EXPECT_EQ(addons.at(0)->ID(), "foo.baz");
+ EXPECT_EQ(addons.at(0)->Version().asString(), "1.1.0");
+ EXPECT_EQ(addons.at(0)->Origin(), "repository.b");
+}
+
+TEST_F(AddonDatabaseTest, TestFindByNonExistingId)
+{
+ VECADDONS addons;
+ EXPECT_TRUE(database.FindByAddonId("does.not.exist", addons));
+ EXPECT_EQ(0U, addons.size());
+}
diff --git a/xbmc/addons/test/TestAddonInfoBuilder.cpp b/xbmc/addons/test/TestAddonInfoBuilder.cpp
new file mode 100644
index 0000000..294d930
--- /dev/null
+++ b/xbmc/addons/test/TestAddonInfoBuilder.cpp
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016-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 "addons/Repository.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonInfoBuilder.h"
+#include "addons/addoninfo/AddonType.h"
+#include "utils/XBMCTinyXML.h"
+
+#include <set>
+
+#include <gtest/gtest.h>
+
+using namespace ADDON;
+
+const std::string addonXML = R"xml(
+<addon id="metadata.blablabla.org"
+ name="The Bla Bla Bla Addon"
+ version="1.2.3"
+ provider-name="Team Kodi">
+ <requires>
+ <import addon="xbmc.metadata" version="2.1.0"/>
+ <import addon="metadata.common.imdb.com" minversion="2.9.2" version="2.9.2"/>
+ <import addon="metadata.common.themoviedb.org" minversion="3.1.0" version="3.1.0"/>
+ <import addon="plugin.video.youtube" minversion="4.4.0" version="4.4.10" optional="true"/>
+ </requires>
+ <extension point="xbmc.metadata.scraper.movies"
+ language="en"
+ library="blablabla.xml"/>
+ <extension point="xbmc.python.module"
+ library="lib.so"/>
+ <extension point="kodi.addon.metadata">
+ <summary lang="en">Summary bla bla bla</summary>
+ <description lang="en">Description bla bla bla</description>
+ <disclaimer lang="en">Disclaimer bla bla bla</disclaimer>
+ <platform>all</platform>
+ <language>marsian</language>
+ <license>GPL v2.0</license>
+ <forum>https://forum.kodi.tv</forum>
+ <website>https://kodi.tv</website>
+ <email>a@a.dummy</email>
+ <source>https://github.com/xbmc/xbmc</source>
+ </extension>
+</addon>
+)xml";
+
+class TestAddonInfoBuilder : public ::testing::Test
+{
+protected:
+ TestAddonInfoBuilder() = default;
+};
+
+TEST_F(TestAddonInfoBuilder, ShouldFailWhenIdIsNotSet)
+{
+ AddonInfoPtr addon = CAddonInfoBuilder::Generate("", AddonType::UNKNOWN);
+ EXPECT_EQ(nullptr, addon);
+}
+
+TEST_F(TestAddonInfoBuilder, TestGenerate_Id_Type)
+{
+ AddonInfoPtr addon = CAddonInfoBuilder::Generate("foo.baz", AddonType::VISUALIZATION);
+ EXPECT_NE(nullptr, addon);
+ EXPECT_EQ(addon->ID(), "foo.baz");
+ EXPECT_EQ(addon->MainType(), AddonType::VISUALIZATION);
+ EXPECT_TRUE(addon->HasType(AddonType::VISUALIZATION));
+ EXPECT_FALSE(addon->HasType(AddonType::SCREENSAVER));
+}
+
+TEST_F(TestAddonInfoBuilder, TestGenerate_Repo)
+{
+ CXBMCTinyXML doc;
+ EXPECT_TRUE(doc.Parse(addonXML));
+ ASSERT_NE(nullptr, doc.RootElement());
+
+ RepositoryDirInfo repo;
+ AddonInfoPtr addon = CAddonInfoBuilder::Generate(doc.RootElement(), repo);
+ ASSERT_NE(nullptr, addon);
+ EXPECT_EQ(addon->ID(), "metadata.blablabla.org");
+
+ EXPECT_EQ(addon->MainType(), AddonType::SCRAPER_MOVIES);
+ EXPECT_TRUE(addon->HasType(AddonType::SCRAPER_MOVIES));
+ EXPECT_EQ(addon->Type(AddonType::SCRAPER_MOVIES)->LibName(), "blablabla.xml");
+ EXPECT_EQ(addon->Type(AddonType::SCRAPER_MOVIES)->GetValue("@language").asString(), "en");
+
+ EXPECT_TRUE(addon->HasType(AddonType::SCRIPT_MODULE));
+ EXPECT_EQ(addon->Type(AddonType::SCRIPT_MODULE)->LibName(), "lib.so");
+ EXPECT_FALSE(addon->HasType(AddonType::SCRAPER_ARTISTS));
+
+ EXPECT_EQ(addon->Name(), "The Bla Bla Bla Addon");
+ EXPECT_EQ(addon->Author(), "Team Kodi");
+ EXPECT_EQ(addon->Version().asString(), "1.2.3");
+
+ EXPECT_EQ(addon->Summary(), "Summary bla bla bla");
+ EXPECT_EQ(addon->Description(), "Description bla bla bla");
+ EXPECT_EQ(addon->Disclaimer(), "Disclaimer bla bla bla");
+ EXPECT_EQ(addon->License(), "GPL v2.0");
+ EXPECT_EQ(addon->Forum(), "https://forum.kodi.tv");
+ EXPECT_EQ(addon->Website(), "https://kodi.tv");
+ EXPECT_EQ(addon->EMail(), "a@a.dummy");
+ EXPECT_EQ(addon->Source(), "https://github.com/xbmc/xbmc");
+
+ const std::vector<DependencyInfo>& dependencies = addon->GetDependencies();
+ ASSERT_EQ(dependencies.size(), (long unsigned int)4);
+ EXPECT_EQ(dependencies[0].id, "xbmc.metadata");
+ EXPECT_EQ(dependencies[0].optional, false);
+ EXPECT_EQ(dependencies[0].versionMin.asString(), "2.1.0");
+ EXPECT_EQ(dependencies[0].version.asString(), "2.1.0");
+ EXPECT_EQ(dependencies[1].id, "metadata.common.imdb.com");
+ EXPECT_EQ(dependencies[1].optional, false);
+ EXPECT_EQ(dependencies[1].versionMin.asString(), "2.9.2");
+ EXPECT_EQ(dependencies[1].version.asString(), "2.9.2");
+ EXPECT_EQ(dependencies[2].id, "metadata.common.themoviedb.org");
+ EXPECT_EQ(dependencies[2].optional, false);
+ EXPECT_EQ(dependencies[2].versionMin.asString(), "3.1.0");
+ EXPECT_EQ(dependencies[2].version.asString(), "3.1.0");
+ EXPECT_EQ(dependencies[3].id, "plugin.video.youtube");
+ EXPECT_EQ(dependencies[3].optional, true);
+ EXPECT_EQ(dependencies[3].versionMin.asString(), "4.4.0");
+ EXPECT_EQ(dependencies[3].version.asString(), "4.4.10");
+
+ auto info = addon->ExtraInfo().find("language");
+ ASSERT_NE(info, addon->ExtraInfo().end());
+ EXPECT_EQ(info->second, "marsian");
+}
+
+TEST_F(TestAddonInfoBuilder, TestGenerate_DBEntry)
+{
+ CAddonInfoBuilderFromDB builder;
+ builder.SetId("video.blablabla.org");
+ builder.SetVersion(CAddonVersion("1.2.3"));
+ CAddonType addonType(AddonType::PLUGIN);
+ addonType.Insert("provides", "video audio");
+ builder.SetExtensions(addonType);
+ builder.SetName("The Bla Bla Bla Addon");
+ builder.SetAuthor("Team Kodi");
+ builder.SetSummary("Summary bla bla bla");
+ builder.SetDescription("Description bla bla bla");
+ builder.SetDisclaimer("Disclaimer bla bla bla");
+ builder.SetLicense("GPL v2.0");
+ builder.SetForum("https://forum.kodi.tv");
+ builder.SetWebsite("https://kodi.tv");
+ builder.SetEMail("a@a.dummy");
+ builder.SetSource("https://github.com/xbmc/xbmc");
+ InfoMap extrainfo;
+ extrainfo["language"] = "marsian";
+ builder.SetExtrainfo(extrainfo);
+
+ AddonInfoPtr addon = builder.get();
+ ASSERT_NE(nullptr, addon);
+ EXPECT_EQ(addon->ID(), "video.blablabla.org");
+
+ EXPECT_EQ(addon->MainType(), AddonType::PLUGIN);
+ EXPECT_TRUE(addon->HasType(AddonType::PLUGIN));
+ EXPECT_TRUE(addon->HasType(AddonType::VIDEO));
+ EXPECT_TRUE(addon->HasType(AddonType::AUDIO));
+ EXPECT_FALSE(addon->HasType(AddonType::GAME));
+
+ EXPECT_EQ(addon->Name(), "The Bla Bla Bla Addon");
+ EXPECT_EQ(addon->Author(), "Team Kodi");
+ EXPECT_EQ(addon->Version().asString(), "1.2.3");
+
+ EXPECT_EQ(addon->Summary(), "Summary bla bla bla");
+ EXPECT_EQ(addon->Description(), "Description bla bla bla");
+ EXPECT_EQ(addon->Disclaimer(), "Disclaimer bla bla bla");
+ EXPECT_EQ(addon->License(), "GPL v2.0");
+ EXPECT_EQ(addon->Forum(), "https://forum.kodi.tv");
+ EXPECT_EQ(addon->Website(), "https://kodi.tv");
+ EXPECT_EQ(addon->EMail(), "a@a.dummy");
+ EXPECT_EQ(addon->Source(), "https://github.com/xbmc/xbmc");
+
+ auto info = addon->ExtraInfo().find("language");
+ ASSERT_NE(info, addon->ExtraInfo().end());
+ EXPECT_EQ(info->second, "marsian");
+}
diff --git a/xbmc/addons/test/TestAddonVersion.cpp b/xbmc/addons/test/TestAddonVersion.cpp
new file mode 100644
index 0000000..0397792
--- /dev/null
+++ b/xbmc/addons/test/TestAddonVersion.cpp
@@ -0,0 +1,271 @@
+/*
+ * 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 "addons/AddonVersion.h"
+
+#include <gtest/gtest.h>
+
+using namespace ADDON;
+
+class TestAddonVersion : public testing::Test
+{
+public:
+ TestAddonVersion()
+ : v1_0("1.0"),
+ v1_00("1.00"),
+ v1_0_0("1.0.0"),
+ v1_1("1.1"),
+ v1_01("1.01"),
+ v1_0_1("1.0.1"),
+ e1_v1_0_0("1:1.0.0"),
+ e1_v1_0_1("1:1.0.1"),
+ e2_v1_0_0("2:1.0.0"),
+ e1_v1_0_0_r1("1:1.0.0-1"),
+ e1_v1_0_1_r1("1:1.0.1-1"),
+ e1_v1_0_0_r2("1:1.0.0-2"),
+ v1_0_0_beta("1.0.0~beta"),
+ v1_0_0_alpha("1.0.0~alpha"),
+ v1_0_0_alpha2("1.0.0~alpha2"),
+ v1_0_0_alpha3("1.0.0~alpha3"),
+ v1_0_0_alpha10("1.0.0~alpha10")
+ {
+ }
+
+ CAddonVersion v1_0;
+ CAddonVersion v1_00;
+ CAddonVersion v1_0_0;
+ CAddonVersion v1_1;
+ CAddonVersion v1_01;
+ CAddonVersion v1_0_1;
+ CAddonVersion e1_v1_0_0;
+ CAddonVersion e1_v1_0_1;
+ CAddonVersion e2_v1_0_0;
+ CAddonVersion e1_v1_0_0_r1;
+ CAddonVersion e1_v1_0_1_r1;
+ CAddonVersion e1_v1_0_0_r2;
+ CAddonVersion v1_0_0_beta;
+ CAddonVersion v1_0_0_alpha;
+ CAddonVersion v1_0_0_alpha2;
+ CAddonVersion v1_0_0_alpha3;
+ CAddonVersion v1_0_0_alpha10;
+};
+
+TEST_F(TestAddonVersion, Constructor)
+{
+ EXPECT_EQ(v1_0.Upstream(), "1.0");
+ EXPECT_EQ(v1_0.Epoch(), 0);
+ EXPECT_TRUE(v1_0.Revision().empty());
+
+ EXPECT_EQ(v1_00.Upstream(), "1.00");
+ EXPECT_EQ(v1_00.Epoch(), 0);
+ EXPECT_TRUE(v1_00.Revision().empty());
+
+ EXPECT_EQ(v1_0_0.Upstream(), "1.0.0");
+ EXPECT_EQ(v1_0_0.Epoch(), 0);
+ EXPECT_TRUE(v1_0_0.Revision().empty());
+
+ EXPECT_EQ(v1_1.Upstream(), "1.1");
+ EXPECT_EQ(v1_1.Epoch(), 0);
+ EXPECT_TRUE(v1_1.Revision().empty());
+
+ EXPECT_EQ(v1_01.Upstream(), "1.01");
+ EXPECT_EQ(v1_01.Epoch(), 0);
+ EXPECT_TRUE(v1_01.Revision().empty());
+
+ EXPECT_EQ(v1_0_1.Upstream(), "1.0.1");
+ EXPECT_EQ(v1_0_1.Epoch(), 0);
+ EXPECT_TRUE(v1_0_1.Revision().empty());
+
+ EXPECT_EQ(e1_v1_0_0.Upstream(), "1.0.0");
+ EXPECT_EQ(e1_v1_0_0.Epoch(), 1);
+ EXPECT_TRUE(e1_v1_0_0.Revision().empty());
+
+ EXPECT_EQ(e1_v1_0_1.Upstream(), "1.0.1");
+ EXPECT_EQ(e1_v1_0_1.Epoch(), 1);
+ EXPECT_TRUE(e1_v1_0_1.Revision().empty());
+
+ EXPECT_EQ(e2_v1_0_0.Upstream(), "1.0.0");
+ EXPECT_EQ(e2_v1_0_0.Epoch(), 2);
+ EXPECT_TRUE(e2_v1_0_0.Revision().empty());
+
+ EXPECT_EQ(e1_v1_0_0_r1.Upstream(), "1.0.0");
+ EXPECT_EQ(e1_v1_0_0_r1.Epoch(), 1);
+ EXPECT_EQ(e1_v1_0_0_r1.Revision(), "1");
+
+ EXPECT_EQ(e1_v1_0_1_r1.Upstream(), "1.0.1");
+ EXPECT_EQ(e1_v1_0_1_r1.Epoch(), 1);
+ EXPECT_EQ(e1_v1_0_1_r1.Revision(), "1");
+
+ EXPECT_EQ(e1_v1_0_0_r2.Upstream(), "1.0.0");
+ EXPECT_EQ(e1_v1_0_0_r2.Epoch(), 1);
+ EXPECT_EQ(e1_v1_0_0_r2.Revision(), "2");
+
+ EXPECT_EQ(v1_0_0_beta.Upstream(), "1.0.0~beta");
+ EXPECT_EQ(v1_0_0_beta.Epoch(), 0);
+ EXPECT_TRUE(v1_0_0_beta.Revision().empty());
+
+ EXPECT_EQ(v1_0_0_alpha.Upstream(), "1.0.0~alpha");
+ EXPECT_EQ(v1_0_0_alpha.Epoch(), 0);
+ EXPECT_TRUE(v1_0_0_alpha.Revision().empty());
+
+ EXPECT_EQ(v1_0_0_alpha2.Upstream(), "1.0.0~alpha2");
+ EXPECT_EQ(v1_0_0_alpha2.Epoch(), 0);
+ EXPECT_TRUE(v1_0_0_alpha2.Revision().empty());
+
+ EXPECT_EQ(v1_0_0_alpha3.Upstream(), "1.0.0~alpha3");
+ EXPECT_EQ(v1_0_0_alpha3.Epoch(), 0);
+ EXPECT_TRUE(v1_0_0_alpha3.Revision().empty());
+
+ EXPECT_EQ(v1_0_0_alpha10.Upstream(), "1.0.0~alpha10");
+ EXPECT_EQ(v1_0_0_alpha10.Epoch(), 0);
+ EXPECT_TRUE(v1_0_0_alpha10.Revision().empty());
+}
+
+TEST_F(TestAddonVersion, asString)
+{
+ EXPECT_EQ(v1_0.asString(), "1.0");
+ EXPECT_EQ(v1_00.asString(), "1.00");
+ EXPECT_EQ(v1_0_0.asString(), "1.0.0");
+ EXPECT_EQ(v1_1.asString(), "1.1");
+ EXPECT_EQ(v1_01.asString(), "1.01");
+ EXPECT_EQ(v1_0_1.asString(), "1.0.1");
+ EXPECT_EQ(e1_v1_0_0.asString(), "1:1.0.0");
+ EXPECT_EQ(e1_v1_0_1.asString(), "1:1.0.1");
+ EXPECT_EQ(e2_v1_0_0.asString(), "2:1.0.0");
+ EXPECT_EQ(e1_v1_0_0_r1.asString(), "1:1.0.0-1");
+ EXPECT_EQ(e1_v1_0_1_r1.asString(), "1:1.0.1-1");
+ EXPECT_EQ(e1_v1_0_0_r2.asString(), "1:1.0.0-2");
+ EXPECT_EQ(v1_0_0_beta.asString(), "1.0.0~beta");
+ EXPECT_EQ(v1_0_0_alpha.asString(), "1.0.0~alpha");
+ EXPECT_EQ(v1_0_0_alpha2.asString(), "1.0.0~alpha2");
+ EXPECT_EQ(v1_0_0_alpha3.asString(), "1.0.0~alpha3");
+ EXPECT_EQ(v1_0_0_alpha10.asString(), "1.0.0~alpha10");
+}
+
+TEST_F(TestAddonVersion, Equals)
+{
+ EXPECT_EQ(v1_0, CAddonVersion("1.0"));
+ EXPECT_EQ(v1_00, CAddonVersion("1.00"));
+ EXPECT_EQ(v1_0_0, CAddonVersion("1.0.0"));
+ EXPECT_EQ(v1_1, CAddonVersion("1.1"));
+ EXPECT_EQ(v1_01, CAddonVersion("1.01"));
+ EXPECT_EQ(v1_0_1, CAddonVersion("1.0.1"));
+ EXPECT_EQ(e1_v1_0_0, CAddonVersion("1:1.0.0"));
+ EXPECT_EQ(e1_v1_0_1, CAddonVersion("1:1.0.1"));
+ EXPECT_EQ(e2_v1_0_0, CAddonVersion("2:1.0.0"));
+ EXPECT_EQ(e1_v1_0_0_r1, CAddonVersion("1:1.0.0-1"));
+ EXPECT_EQ(e1_v1_0_1_r1, CAddonVersion("1:1.0.1-1"));
+ EXPECT_EQ(e1_v1_0_0_r2, CAddonVersion("1:1.0.0-2"));
+ EXPECT_EQ(v1_0_0_beta, CAddonVersion("1.0.0~beta"));
+ EXPECT_EQ(v1_0_0_alpha, CAddonVersion("1.0.0~alpha"));
+ EXPECT_EQ(v1_0_0_alpha2, CAddonVersion("1.0.0~alpha2"));
+ EXPECT_EQ(v1_0_0_alpha3, CAddonVersion("1.0.0~alpha3"));
+ EXPECT_EQ(v1_0_0_alpha10, CAddonVersion("1.0.0~alpha10"));
+}
+
+TEST_F(TestAddonVersion, LessThan)
+{
+ EXPECT_LT(v1_0, v1_0_0);
+ EXPECT_LT(v1_0, v1_1);
+ EXPECT_LT(v1_0, v1_01);
+ EXPECT_LT(v1_0, v1_0_1);
+
+ EXPECT_LT(v1_00, v1_0_0);
+ EXPECT_LT(v1_00, v1_1);
+ EXPECT_LT(v1_00, v1_01);
+ EXPECT_LT(v1_00, v1_0_1);
+
+ EXPECT_LT(v1_0_0, v1_1);
+ EXPECT_LT(v1_0_0, v1_01);
+ EXPECT_LT(v1_0_0, v1_0_1);
+
+ EXPECT_LT(v1_0_1, v1_01);
+ EXPECT_LT(v1_0_1, v1_1);
+
+ // epochs
+ EXPECT_LT(v1_0_0, e1_v1_0_0);
+ EXPECT_LT(v1_0_0, e1_v1_0_1);
+ EXPECT_LT(v1_0_0, e2_v1_0_0);
+ EXPECT_LT(v1_0_1, e1_v1_0_1);
+ EXPECT_LT(v1_0_1, e2_v1_0_0);
+
+ EXPECT_LT(e1_v1_0_0, e1_v1_0_1);
+ EXPECT_LT(e1_v1_0_0, e2_v1_0_0);
+ EXPECT_LT(e1_v1_0_1, e2_v1_0_0);
+
+ // revisions
+ EXPECT_LT(e1_v1_0_0, e1_v1_0_0_r1);
+ EXPECT_LT(e1_v1_0_0, e1_v1_0_1_r1);
+ EXPECT_LT(e1_v1_0_0, e1_v1_0_0_r2);
+ EXPECT_LT(e1_v1_0_1, e1_v1_0_1_r1);
+ EXPECT_LT(e1_v1_0_0_r1, e1_v1_0_1);
+ EXPECT_LT(e1_v1_0_0_r1, e1_v1_0_1_r1);
+ EXPECT_LT(e1_v1_0_0_r1, e1_v1_0_0_r2);
+ EXPECT_LT(e1_v1_0_0_r2, e1_v1_0_1);
+ EXPECT_LT(e1_v1_0_0_r2, e1_v1_0_1_r1);
+ EXPECT_LT(e1_v1_0_1_r1, e2_v1_0_0);
+
+ // alpha, beta
+ EXPECT_LT(v1_0_0_beta, v1_0_0);
+ EXPECT_LT(v1_0_0_alpha, v1_0_0);
+ EXPECT_LT(v1_0_0_alpha, v1_0_0_beta);
+ EXPECT_LT(v1_0_0_alpha, v1_0_0_alpha2);
+ EXPECT_LT(v1_0_0_alpha, v1_0_0_alpha3);
+ EXPECT_LT(v1_0_0_alpha, v1_0_0_alpha10);
+ EXPECT_LT(v1_0_0_alpha2, v1_0_0);
+ EXPECT_LT(v1_0_0_alpha2, v1_0_0_beta);
+ EXPECT_LT(v1_0_0_alpha2, v1_0_0_alpha3);
+ EXPECT_LT(v1_0_0_alpha2, v1_0_0_alpha10);
+ EXPECT_LT(v1_0_0_alpha3, v1_0_0);
+ EXPECT_LT(v1_0_0_alpha3, v1_0_0_beta);
+ EXPECT_LT(v1_0_0_alpha3, v1_0_0_alpha10);
+ EXPECT_LT(v1_0_0_alpha10, v1_0_0);
+ EXPECT_LT(v1_0_0_alpha10, v1_0_0_beta);
+
+ // pep-0440/local-version-identifiers
+ // ref: https://www.python.org/dev/peps/pep-0440/#local-version-identifiers
+ // Python addons use this kind of versioning particularly for script.module
+ // addons. The "same" version number may exist in different branches or
+ // targeting different kodi versions while keeping consistency with the
+ // upstream module version. The addon version available in upper repos
+ // (let's say matrix) must have a higher version than the one stored in
+ // lower branches (e.g. leia) so that users receive the addon update
+ // when upgrading kodi.
+ // So, for instance, we use version x.x.x or version x.x.x+kodiversion.r to
+ // refer to the same upstream version x.x.x of the module.
+ // Eg: script.module.foo-1.0.0 or script.module.foo-1.0.0+leia.1 for upstream
+ // module foo (version 1.0.0) available for leia; and
+ // script.module.foo-1.0.0+matrix.1 for upstream module foo (1.0.0) for matrix.
+ // In summary, 1.0.0 or 1.0.0+leia.1 must be < than 1.0.0+matrix.1
+ // tests below assure this won't get broken inadvertently
+ EXPECT_LT(CAddonVersion("1.0.0"), CAddonVersion("1.0.0+matrix.1"));
+ EXPECT_LT(CAddonVersion("1.0.0+leia.1"), CAddonVersion("1.0.0+matrix.1"));
+ EXPECT_LT(CAddonVersion("1.0.0+matrix.1"), CAddonVersion("1.0.0+matrix.2"));
+ EXPECT_LT(CAddonVersion("1.0.0+matrix.1"), CAddonVersion("1.0.1+matrix.1"));
+ EXPECT_LT(CAddonVersion("1.0.0+matrix.1"), CAddonVersion("1.1.0+matrix.1"));
+ EXPECT_LT(CAddonVersion("1.0.0+matrix.1"), CAddonVersion("2.0.0+matrix.1"));
+ EXPECT_LT(CAddonVersion("1.0.0+matrix.1"), CAddonVersion("1.0.0.1"));
+ EXPECT_LT(CAddonVersion("1.0.0+Leia.1"), CAddonVersion("1.0.0+matrix.1"));
+ EXPECT_LT(CAddonVersion("1.0.0+leia.1"), CAddonVersion("1.0.0+Matrix.1"));
+}
+
+TEST_F(TestAddonVersion, Equivalent)
+{
+ EXPECT_FALSE(v1_0 != v1_00);
+ EXPECT_FALSE(v1_0 < v1_00);
+ EXPECT_FALSE(v1_0 > v1_00);
+ EXPECT_TRUE(v1_0 == v1_00);
+
+ EXPECT_FALSE(v1_01 != v1_1);
+ EXPECT_FALSE(v1_01 < v1_1);
+ EXPECT_FALSE(v1_01 > v1_1);
+ EXPECT_TRUE(v1_01 == v1_1);
+
+ // pep-0440/local-version-identifiers
+ EXPECT_TRUE(CAddonVersion("1.0.0+leia.1") == CAddonVersion("1.0.0+Leia.1"));
+}
diff --git a/xbmc/application/AppEnvironment.cpp b/xbmc/application/AppEnvironment.cpp
new file mode 100644
index 0000000..32d4c4f
--- /dev/null
+++ b/xbmc/application/AppEnvironment.cpp
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2005-2022 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 "AppEnvironment.h"
+
+#include "ServiceBroker.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+
+void CAppEnvironment::SetUp(const std::shared_ptr<CAppParams>& appParams)
+{
+ CServiceBroker::RegisterAppParams(appParams);
+ CServiceBroker::CreateLogging();
+ const auto settingsComponent = std::make_shared<CSettingsComponent>();
+ settingsComponent->Initialize();
+ CServiceBroker::RegisterSettingsComponent(settingsComponent);
+}
+
+void CAppEnvironment::TearDown()
+{
+ CServiceBroker::GetLogging().UnregisterFromSettings();
+ CServiceBroker::GetSettingsComponent()->Deinitialize();
+ CServiceBroker::UnregisterSettingsComponent();
+ CServiceBroker::GetLogging().Deinitialize();
+ CServiceBroker::DestroyLogging();
+ CServiceBroker::UnregisterAppParams();
+}
diff --git a/xbmc/application/AppEnvironment.h b/xbmc/application/AppEnvironment.h
new file mode 100644
index 0000000..d301866
--- /dev/null
+++ b/xbmc/application/AppEnvironment.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2005-2022 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 <memory>
+
+class CAppParams;
+
+class CAppEnvironment
+{
+public:
+ static void SetUp(const std::shared_ptr<CAppParams>& appParams);
+ static void TearDown();
+};
diff --git a/xbmc/application/AppInboundProtocol.cpp b/xbmc/application/AppInboundProtocol.cpp
new file mode 100644
index 0000000..7a1c80d
--- /dev/null
+++ b/xbmc/application/AppInboundProtocol.cpp
@@ -0,0 +1,31 @@
+/*
+ * 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 "AppInboundProtocol.h"
+
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+
+CAppInboundProtocol::CAppInboundProtocol(CApplication &app) : m_pApp(app)
+{
+
+}
+
+bool CAppInboundProtocol::OnEvent(XBMC_Event &event)
+{
+ return m_pApp.OnEvent(event);
+}
+
+void CAppInboundProtocol::SetRenderGUI(bool renderGUI)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->SetRenderGUI(renderGUI);
+}
diff --git a/xbmc/application/AppInboundProtocol.h b/xbmc/application/AppInboundProtocol.h
new file mode 100644
index 0000000..b0489e5
--- /dev/null
+++ b/xbmc/application/AppInboundProtocol.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "windowing/XBMC_events.h"
+
+class CApplication;
+
+class CAppInboundProtocol
+{
+public:
+ CAppInboundProtocol(CApplication &app);
+ bool OnEvent(XBMC_Event &event);
+ void SetRenderGUI(bool renderGUI);
+
+protected:
+ CApplication &m_pApp;
+};
diff --git a/xbmc/application/AppParamParser.cpp b/xbmc/application/AppParamParser.cpp
new file mode 100644
index 0000000..ca9cd2a
--- /dev/null
+++ b/xbmc/application/AppParamParser.cpp
@@ -0,0 +1,118 @@
+/*
+ * 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 "AppParamParser.h"
+
+#include "CompileInfo.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "application/AppParams.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/log.h"
+
+#include <iostream>
+#include <stdlib.h>
+#include <string>
+
+namespace
+{
+
+constexpr const char* versionText =
+ R"""({0} Media Center {1}
+Copyright (C) {2} Team {0} - http://kodi.tv
+)""";
+
+constexpr const char* helpText =
+ R"""(Usage: {0} [OPTION]... [FILE]...
+
+Arguments:
+ -fs Runs {1} in full screen
+ --standalone {1} runs in a stand alone environment without a window
+ manager and supporting applications. For example, that
+ enables network settings.
+ -p or --portable {1} will look for configurations in install folder instead of ~/.{0}
+ --debug Enable debug logging
+ --version Print version information
+ --test Enable test mode. [FILE] required.
+ --settings=<filename> Loads specified file after advancedsettings.xml replacing any settings specified
+ specified file must exist in special://xbmc/system/
+)""";
+
+} // namespace
+
+CAppParamParser::CAppParamParser() : m_params(std::make_shared<CAppParams>())
+{
+}
+
+void CAppParamParser::Parse(const char* const* argv, int nArgs)
+{
+ std::vector<std::string> args;
+ args.reserve(nArgs);
+
+ for (int i = 0; i < nArgs; i++)
+ {
+ args.emplace_back(argv[i]);
+ if (i > 0)
+ ParseArg(argv[i]);
+ }
+
+ if (nArgs > 1)
+ {
+ // testmode is only valid if at least one item to play was given
+ if (m_params->GetPlaylist().IsEmpty())
+ m_params->SetTestMode(false);
+ }
+
+ // Record raw paramerters
+ m_params->SetRawArgs(std::move(args));
+}
+
+void CAppParamParser::DisplayVersion()
+{
+ std::cout << StringUtils::Format(versionText, CSysInfo::GetAppName(), CSysInfo::GetVersion(),
+ CCompileInfo::GetCopyrightYears());
+ exit(0);
+}
+
+void CAppParamParser::DisplayHelp()
+{
+ std::string lcAppName = CSysInfo::GetAppName();
+ StringUtils::ToLower(lcAppName);
+
+ std::cout << StringUtils::Format(helpText, lcAppName, CSysInfo::GetAppName());
+}
+
+void CAppParamParser::ParseArg(const std::string &arg)
+{
+ if (arg == "-fs" || arg == "--fullscreen")
+ m_params->SetStartFullScreen(true);
+ else if (arg == "-h" || arg == "--help")
+ {
+ DisplayHelp();
+ exit(0);
+ }
+ else if (arg == "-v" || arg == "--version")
+ DisplayVersion();
+ else if (arg == "--standalone")
+ m_params->SetStandAlone(true);
+ else if (arg == "-p" || arg == "--portable")
+ m_params->SetPlatformDirectories(false);
+ else if (arg == "--debug")
+ m_params->SetLogLevel(LOG_LEVEL_DEBUG);
+ else if (arg == "--test")
+ m_params->SetTestMode(true);
+ else if (arg.substr(0, 11) == "--settings=")
+ m_params->SetSettingsFile(arg.substr(11));
+ else if (arg.length() != 0 && arg[0] != '-')
+ {
+ const CFileItemPtr item = std::make_shared<CFileItem>(arg);
+ item->SetPath(arg);
+ m_params->GetPlaylist().Add(item);
+ }
+}
diff --git a/xbmc/application/AppParamParser.h b/xbmc/application/AppParamParser.h
new file mode 100644
index 0000000..44907f8
--- /dev/null
+++ b/xbmc/application/AppParamParser.h
@@ -0,0 +1,34 @@
+/*
+ * 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 <memory>
+#include <string>
+
+class CAppParams;
+
+class CAppParamParser
+{
+public:
+ CAppParamParser();
+ virtual ~CAppParamParser() = default;
+
+ void Parse(const char* const* argv, int nArgs);
+
+ std::shared_ptr<CAppParams> GetAppParams() const { return m_params; }
+
+protected:
+ virtual void ParseArg(const std::string& arg);
+ virtual void DisplayHelp();
+
+private:
+ void DisplayVersion();
+
+ std::shared_ptr<CAppParams> m_params;
+};
diff --git a/xbmc/application/AppParams.cpp b/xbmc/application/AppParams.cpp
new file mode 100644
index 0000000..97c09e3
--- /dev/null
+++ b/xbmc/application/AppParams.cpp
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2005-2022 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 "AppParams.h"
+
+#include "FileItem.h"
+
+CAppParams::CAppParams() : m_playlist(std::make_unique<CFileItemList>())
+{
+}
+
+void CAppParams::SetRawArgs(std::vector<std::string> args)
+{
+ m_rawArgs = std::move(args);
+}
diff --git a/xbmc/application/AppParams.h b/xbmc/application/AppParams.h
new file mode 100644
index 0000000..4452cd7
--- /dev/null
+++ b/xbmc/application/AppParams.h
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2005-202 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 "commons/ilog.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItemList;
+
+class CAppParams
+{
+public:
+ CAppParams();
+ virtual ~CAppParams() = default;
+
+ int GetLogLevel() const { return m_logLevel; }
+ void SetLogLevel(int logLevel) { m_logLevel = logLevel; }
+
+ bool IsStartFullScreen() const { return m_startFullScreen; }
+ void SetStartFullScreen(bool startFullScreen) { m_startFullScreen = startFullScreen; }
+
+ bool IsStandAlone() const { return m_standAlone; }
+ void SetStandAlone(bool standAlone) { m_standAlone = standAlone; }
+
+ bool HasPlatformDirectories() const { return m_platformDirectories; }
+ void SetPlatformDirectories(bool platformDirectories)
+ {
+ m_platformDirectories = platformDirectories;
+ }
+
+ bool IsTestMode() const { return m_testmode; }
+ void SetTestMode(bool testMode) { m_testmode = testMode; }
+
+ const std::string& GetSettingsFile() const { return m_settingsFile; }
+ void SetSettingsFile(const std::string& settingsFile) { m_settingsFile = settingsFile; }
+
+ const std::string& GetWindowing() const { return m_windowing; }
+ void SetWindowing(const std::string& windowing) { m_windowing = windowing; }
+
+ const std::string& GetLogTarget() const { return m_logTarget; }
+ void SetLogTarget(const std::string& logTarget) { m_logTarget = logTarget; }
+
+ CFileItemList& GetPlaylist() const { return *m_playlist; }
+
+ /*!
+ * \brief Get the raw command-line arguments
+ *
+ * Note: Raw arguments are currently not used by Kodi, but they will be
+ * useful if/when ROS 2 support is ever merged.
+ *
+ * \return The arguments. Note that the leading argument is the executable
+ * path name.
+ */
+ const std::vector<std::string>& GetRawArgs() const { return m_rawArgs; }
+
+ /*!
+ * \brief Set the raw command-line arguments
+ *
+ * \args The arguments. Note that the leading argument is the executable path
+ * name.
+ */
+ void SetRawArgs(std::vector<std::string> args);
+
+private:
+ int m_logLevel{LOG_LEVEL_NORMAL};
+
+ bool m_startFullScreen{false};
+ bool m_standAlone{false};
+ bool m_platformDirectories{true};
+ bool m_testmode{false};
+
+ std::string m_settingsFile;
+ std::string m_windowing;
+ std::string m_logTarget;
+
+ std::unique_ptr<CFileItemList> m_playlist;
+
+ // The raw command-line arguments
+ std::vector<std::string> m_rawArgs;
+};
diff --git a/xbmc/application/Application.cpp b/xbmc/application/Application.cpp
new file mode 100644
index 0000000..0765cf3
--- /dev/null
+++ b/xbmc/application/Application.cpp
@@ -0,0 +1,3693 @@
+/*
+ * 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 "Application.h"
+
+#include "Autorun.h"
+#include "CompileInfo.h"
+#include "GUIInfoManager.h"
+#include "HDRStatus.h"
+#include "LangInfo.h"
+#include "PlayListPlayer.h"
+#include "ServiceManager.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/RepositoryUpdater.h"
+#include "addons/Service.h"
+#include "addons/Skin.h"
+#include "addons/VFSEntry.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "application/AppInboundProtocol.h"
+#include "application/AppParams.h"
+#include "application/ApplicationActionListeners.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationPowerHandling.h"
+#include "application/ApplicationSkinHandling.h"
+#include "application/ApplicationStackHelper.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "cores/AudioEngine/Engines/ActiveAE/ActiveAE.h"
+#include "cores/IPlayer.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogCache.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+#include "filesystem/File.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIControlProfiler.h"
+#include "guilib/GUIFontManager.h"
+#include "guilib/StereoscopicsManager.h"
+#include "guilib/TextureManager.h"
+#include "interfaces/builtins/Builtins.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/tags/MusicInfoTag.h"
+#include "network/EventServer.h"
+#include "network/Network.h"
+#include "platform/Environment.h"
+#include "playlists/PlayListFactory.h"
+#include "threads/SystemClock.h"
+#include "utils/ContentUtils.h"
+#include "utils/JobManager.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/Screenshot.h"
+#include "utils/Variant.h"
+#include "video/Bookmark.h"
+#include "video/VideoLibraryQueue.h"
+
+#ifdef HAS_PYTHON
+#include "interfaces/python/XBPython.h"
+#endif
+#include "GUILargeTextureManager.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "SectionLoader.h"
+#include "SeekHandler.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "cores/DllLoader/DllLoaderContainer.h"
+#include "filesystem/Directory.h"
+#include "filesystem/DirectoryCache.h"
+#include "filesystem/DllLibCurl.h"
+#include "filesystem/PluginDirectory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIAudioManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/InertialScrollingHandler.h"
+#include "input/KeyboardLayoutManager.h"
+#include "input/actions/ActionTranslator.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/ThreadMessage.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "playlists/PlayList.h"
+#include "playlists/SmartPlayList.h"
+#include "powermanagement/PowerManager.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/DisplaySettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "speech/ISpeechRecognition.h"
+#include "threads/SingleLock.h"
+#include "utils/CPUInfo.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/RegExp.h"
+#include "utils/SystemInfo.h"
+#include "utils/TimeUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+#include "windowing/WindowSystemFactory.h"
+
+#include <cmath>
+
+#ifdef HAS_UPNP
+#include "network/upnp/UPnP.h"
+#include "filesystem/UPnPDirectory.h"
+#endif
+#if defined(TARGET_POSIX) && defined(HAS_FILESYSTEM_SMB)
+#include "platform/posix/filesystem/SMBFile.h"
+#endif
+#ifdef HAS_FILESYSTEM_NFS
+#include "filesystem/NFSFile.h"
+#endif
+#include "PartyModeManager.h"
+#include "network/ZeroconfBrowser.h"
+#ifndef TARGET_POSIX
+#include "platform/win32/threads/Win32Exception.h"
+#endif
+#include "interfaces/json-rpc/JSONRPC.h"
+#include "interfaces/AnnouncementManager.h"
+#include "peripherals/Peripherals.h"
+#include "music/infoscanner/MusicInfoScanner.h"
+#include "music/MusicUtils.h"
+#include "music/MusicThumbLoader.h"
+
+// Windows includes
+#include "guilib/GUIWindowManager.h"
+#include "video/PlayerController.h"
+
+// Dialog includes
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogSimpleMenu.h"
+#include "video/dialogs/GUIDialogVideoBookmarks.h"
+
+// PVR related include Files
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsPowerManagement.h"
+
+#ifdef TARGET_WINDOWS
+#include "win32util.h"
+#endif
+
+#ifdef TARGET_DARWIN_OSX
+#include "platform/darwin/osx/CocoaInterface.h"
+#include "platform/darwin/osx/XBMCHelper.h"
+#endif
+#ifdef TARGET_DARWIN
+#include "platform/darwin/DarwinUtils.h"
+#endif
+
+#ifdef HAS_DVD_DRIVE
+#include <cdio/logging.h>
+#endif
+
+#include "DatabaseManager.h"
+#include "input/InputManager.h"
+#include "storage/MediaManager.h"
+#include "utils/AlarmClock.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#ifdef TARGET_POSIX
+#include "platform/posix/XHandle.h"
+#include "platform/posix/PlatformPosix.h"
+#endif
+
+#if defined(TARGET_ANDROID)
+#include "platform/android/activity/XBMCApp.h"
+#endif
+
+#ifdef TARGET_WINDOWS
+#include "platform/Environment.h"
+#endif
+
+//TODO: XInitThreads
+#ifdef HAVE_X11
+#include <X11/Xlib.h>
+#endif
+
+#include "FileItem.h"
+#include "addons/AddonSystemSettings.h"
+#include "cores/FFmpeg.h"
+#include "pictures/GUIWindowSlideShow.h"
+#include "utils/CharsetConverter.h"
+
+#include <mutex>
+
+using namespace ADDON;
+using namespace XFILE;
+#ifdef HAS_DVD_DRIVE
+using namespace MEDIA_DETECT;
+#endif
+using namespace VIDEO;
+using namespace MUSIC_INFO;
+using namespace EVENTSERVER;
+using namespace JSONRPC;
+using namespace PVR;
+using namespace PERIPHERALS;
+using namespace KODI;
+using namespace KODI::MESSAGING;
+using namespace ActiveAE;
+
+using namespace XbmcThreads;
+using namespace std::chrono_literals;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+using namespace std::chrono_literals;
+
+#define MAX_FFWD_SPEED 5
+
+CApplication::CApplication(void)
+ :
+#ifdef HAS_DVD_DRIVE
+ m_Autorun(new CAutorun()),
+#endif
+ m_pInertialScrollingHandler(new CInertialScrollingHandler()),
+ m_WaitingExternalCalls(0)
+{
+ TiXmlBase::SetCondenseWhiteSpace(false);
+
+#ifdef HAVE_X11
+ XInitThreads();
+#endif
+
+ // register application components
+ RegisterComponent(std::make_shared<CApplicationActionListeners>(m_critSection));
+ RegisterComponent(std::make_shared<CApplicationPlayer>());
+ RegisterComponent(std::make_shared<CApplicationPowerHandling>());
+ RegisterComponent(std::make_shared<CApplicationSkinHandling>(this, this, m_bInitializing));
+ RegisterComponent(std::make_shared<CApplicationVolumeHandling>());
+ RegisterComponent(std::make_shared<CApplicationStackHelper>());
+}
+
+CApplication::~CApplication(void)
+{
+ DeregisterComponent(typeid(CApplicationStackHelper));
+ DeregisterComponent(typeid(CApplicationVolumeHandling));
+ DeregisterComponent(typeid(CApplicationSkinHandling));
+ DeregisterComponent(typeid(CApplicationPowerHandling));
+ DeregisterComponent(typeid(CApplicationPlayer));
+ DeregisterComponent(typeid(CApplicationActionListeners));
+}
+
+bool CApplication::OnEvent(XBMC_Event& newEvent)
+{
+ std::unique_lock<CCriticalSection> lock(m_portSection);
+ m_portEvents.push_back(newEvent);
+ return true;
+}
+
+void CApplication::HandlePortEvents()
+{
+ std::unique_lock<CCriticalSection> lock(m_portSection);
+ while (!m_portEvents.empty())
+ {
+ auto newEvent = m_portEvents.front();
+ m_portEvents.pop_front();
+ CSingleExit lock(m_portSection);
+ switch(newEvent.type)
+ {
+ case XBMC_QUIT:
+ if (!m_bStop)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+ break;
+ case XBMC_VIDEORESIZE:
+ if (CServiceBroker::GetGUI()->GetWindowManager().Initialized())
+ {
+ if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen)
+ {
+ CServiceBroker::GetWinSystem()->GetGfxContext().ApplyWindowResize(newEvent.resize.w, newEvent.resize.h);
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetInt(CSettings::SETTING_WINDOW_WIDTH, newEvent.resize.w);
+ settings->SetInt(CSettings::SETTING_WINDOW_HEIGHT, newEvent.resize.h);
+ settings->Save();
+ }
+#ifdef TARGET_WINDOWS
+ else
+ {
+ // this may occurs when OS tries to resize application window
+ //CDisplaySettings::GetInstance().SetCurrentResolution(RES_DESKTOP, true);
+ //auto& gfxContext = CServiceBroker::GetWinSystem()->GetGfxContext();
+ //gfxContext.SetVideoResolution(gfxContext.GetVideoResolution(), true);
+ // try to resize window back to it's full screen size
+ //! TODO: DX windowing should emit XBMC_FULLSCREEN_UPDATE instead with the proper dimensions
+ //! and position to avoid the ifdef in common code
+ auto& res_info = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP);
+ CServiceBroker::GetWinSystem()->ResizeWindow(res_info.iScreenWidth, res_info.iScreenHeight, 0, 0);
+ }
+#endif
+ }
+ break;
+ case XBMC_FULLSCREEN_UPDATE:
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen)
+ {
+ CServiceBroker::GetWinSystem()->ResizeWindow(newEvent.resize.w, newEvent.resize.h,
+ newEvent.move.x, newEvent.move.y);
+ }
+ break;
+ }
+ case XBMC_VIDEOMOVE:
+ {
+ CServiceBroker::GetWinSystem()->OnMove(newEvent.move.x, newEvent.move.y);
+ }
+ break;
+ case XBMC_MODECHANGE:
+ CServiceBroker::GetWinSystem()->GetGfxContext().ApplyModeChange(newEvent.mode.res);
+ break;
+ case XBMC_USEREVENT:
+ CServiceBroker::GetAppMessenger()->PostMsg(static_cast<uint32_t>(newEvent.user.code));
+ break;
+ case XBMC_SETFOCUS:
+ {
+ // Reset the screensaver
+ const auto appPower = GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+ appPower->WakeUpScreenSaverAndDPMS();
+ // Send a mouse motion event with no dx,dy for getting the current guiitem selected
+ OnAction(CAction(ACTION_MOUSE_MOVE, 0, static_cast<float>(newEvent.focus.x), static_cast<float>(newEvent.focus.y), 0, 0));
+ break;
+ }
+ default:
+ CServiceBroker::GetInputManager().OnEvent(newEvent);
+ }
+ }
+}
+
+extern "C" void __stdcall init_emu_environ();
+extern "C" void __stdcall update_emu_environ();
+extern "C" void __stdcall cleanup_emu_environ();
+
+bool CApplication::Create()
+{
+ m_bStop = false;
+
+ RegisterSettings();
+
+ CServiceBroker::RegisterCPUInfo(CCPUInfo::GetCPUInfo());
+
+ // Register JobManager service
+ CServiceBroker::RegisterJobManager(std::make_shared<CJobManager>());
+
+ // Announcement service
+ m_pAnnouncementManager = std::make_shared<ANNOUNCEMENT::CAnnouncementManager>();
+ m_pAnnouncementManager->Start();
+ CServiceBroker::RegisterAnnouncementManager(m_pAnnouncementManager);
+
+ const auto appMessenger = std::make_shared<CApplicationMessenger>();
+ CServiceBroker::RegisterAppMessenger(appMessenger);
+
+ const auto keyboardLayoutManager = std::make_shared<CKeyboardLayoutManager>();
+ CServiceBroker::RegisterKeyboardLayoutManager(keyboardLayoutManager);
+
+ m_ServiceManager.reset(new CServiceManager());
+
+ if (!m_ServiceManager->InitStageOne())
+ {
+ return false;
+ }
+
+ // here we register all global classes for the CApplicationMessenger,
+ // after that we can send messages to the corresponding modules
+ appMessenger->RegisterReceiver(this);
+ appMessenger->RegisterReceiver(&CServiceBroker::GetPlaylistPlayer());
+ appMessenger->SetGUIThread(CThread::GetCurrentThreadId());
+ appMessenger->SetProcessThread(CThread::GetCurrentThreadId());
+
+ // copy required files
+ CUtil::CopyUserDataIfNeeded("special://masterprofile/", "RssFeeds.xml");
+ CUtil::CopyUserDataIfNeeded("special://masterprofile/", "favourites.xml");
+ CUtil::CopyUserDataIfNeeded("special://masterprofile/", "Lircmap.xml");
+
+ CServiceBroker::GetLogging().Initialize(CSpecialProtocol::TranslatePath("special://logpath"));
+
+#ifdef TARGET_POSIX //! @todo Win32 has no special://home/ mapping by default, so we
+ //! must create these here. Ideally this should be using special://home/ and
+ //! be platform agnostic (i.e. unify the InitDirectories*() functions)
+ if (!CServiceBroker::GetAppParams()->HasPlatformDirectories())
+#endif
+ {
+ CDirectory::Create("special://xbmc/addons");
+ }
+
+ // Init our DllLoaders emu env
+ init_emu_environ();
+
+ PrintStartupLog();
+
+ // initialize network protocols
+ avformat_network_init();
+ // set avutil callback
+ av_log_set_callback(ff_avutil_log);
+
+ CLog::Log(LOGINFO, "loading settings");
+ const auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent->Load())
+ return false;
+
+ CLog::Log(LOGINFO, "creating subdirectories");
+ const std::shared_ptr<CProfileManager> profileManager = settingsComponent->GetProfileManager();
+ const std::shared_ptr<CSettings> settings = settingsComponent->GetSettings();
+ CLog::Log(LOGINFO, "userdata folder: {}",
+ CURL::GetRedacted(profileManager->GetProfileUserDataFolder()));
+ CLog::Log(LOGINFO, "recording folder: {}",
+ CURL::GetRedacted(settings->GetString(CSettings::SETTING_AUDIOCDS_RECORDINGPATH)));
+ CLog::Log(LOGINFO, "screenshots folder: {}",
+ CURL::GetRedacted(settings->GetString(CSettings::SETTING_DEBUG_SCREENSHOTPATH)));
+ CDirectory::Create(profileManager->GetUserDataFolder());
+ CDirectory::Create(profileManager->GetProfileUserDataFolder());
+ profileManager->CreateProfileFolders();
+
+ update_emu_environ();//apply the GUI settings
+
+ // application inbound service
+ m_pAppPort = std::make_shared<CAppInboundProtocol>(*this);
+ CServiceBroker::RegisterAppPort(m_pAppPort);
+
+ if (!m_ServiceManager->InitStageTwo(
+ settingsComponent->GetProfileManager()->GetProfileUserDataFolder()))
+ {
+ return false;
+ }
+
+ m_pActiveAE.reset(new ActiveAE::CActiveAE());
+ CServiceBroker::RegisterAE(m_pActiveAE.get());
+
+ // initialize m_replayGainSettings
+ GetComponent<CApplicationVolumeHandling>()->CacheReplayGainSettings(*settings);
+
+ // load the keyboard layouts
+ if (!keyboardLayoutManager->Load())
+ {
+ CLog::Log(LOGFATAL, "CApplication::Create: Unable to load keyboard layouts");
+ return false;
+ }
+
+ // set user defined CA trust bundle
+ std::string caCert =
+ CSpecialProtocol::TranslatePath(settingsComponent->GetAdvancedSettings()->m_caTrustFile);
+ if (!caCert.empty())
+ {
+ if (XFILE::CFile::Exists(caCert))
+ {
+ CEnvironment::setenv("SSL_CERT_FILE", caCert, 1);
+ CLog::Log(LOGDEBUG, "CApplication::Create - SSL_CERT_FILE: {}", caCert);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "CApplication::Create - Error reading SSL_CERT_FILE: {} -> ignored",
+ caCert);
+ }
+ }
+
+ CUtil::InitRandomSeed();
+
+ m_lastRenderTime = std::chrono::steady_clock::now();
+ return true;
+}
+
+bool CApplication::CreateGUI()
+{
+ m_frameMoveGuard.lock();
+
+ const auto appPower = GetComponent<CApplicationPowerHandling>();
+ appPower->SetRenderGUI(true);
+
+ auto windowSystems = KODI::WINDOWING::CWindowSystemFactory::GetWindowSystems();
+
+ const std::string& windowing = CServiceBroker::GetAppParams()->GetWindowing();
+
+ if (!windowing.empty())
+ windowSystems = {windowing};
+
+ for (auto& windowSystem : windowSystems)
+ {
+ CLog::Log(LOGDEBUG, "CApplication::{} - trying to init {} windowing system", __FUNCTION__,
+ windowSystem);
+ m_pWinSystem = KODI::WINDOWING::CWindowSystemFactory::CreateWindowSystem(windowSystem);
+
+ if (!m_pWinSystem)
+ continue;
+
+ if (!windowing.empty() && windowing != windowSystem)
+ continue;
+
+ CServiceBroker::RegisterWinSystem(m_pWinSystem.get());
+
+ if (!m_pWinSystem->InitWindowSystem())
+ {
+ CLog::Log(LOGDEBUG, "CApplication::{} - unable to init {} windowing system", __FUNCTION__,
+ windowSystem);
+ m_pWinSystem->DestroyWindowSystem();
+ m_pWinSystem.reset();
+ CServiceBroker::UnregisterWinSystem();
+ continue;
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "CApplication::{} - using the {} windowing system", __FUNCTION__,
+ windowSystem);
+ break;
+ }
+ }
+
+ if (!m_pWinSystem)
+ {
+ CLog::Log(LOGFATAL, "CApplication::{} - unable to init windowing system", __FUNCTION__);
+ CServiceBroker::UnregisterWinSystem();
+ return false;
+ }
+
+ // Retrieve the matching resolution based on GUI settings
+ bool sav_res = false;
+ CDisplaySettings::GetInstance().SetCurrentResolution(CDisplaySettings::GetInstance().GetDisplayResolution());
+ CLog::Log(LOGINFO, "Checking resolution {}",
+ CDisplaySettings::GetInstance().GetCurrentResolution());
+ if (!CServiceBroker::GetWinSystem()->GetGfxContext().IsValidResolution(CDisplaySettings::GetInstance().GetCurrentResolution()))
+ {
+ CLog::Log(LOGINFO, "Setting safe mode {}", RES_DESKTOP);
+ // defer saving resolution after window was created
+ CDisplaySettings::GetInstance().SetCurrentResolution(RES_DESKTOP);
+ sav_res = true;
+ }
+
+ // update the window resolution
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ CServiceBroker::GetWinSystem()->SetWindowResolution(settings->GetInt(CSettings::SETTING_WINDOW_WIDTH), settings->GetInt(CSettings::SETTING_WINDOW_HEIGHT));
+
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_startFullScreen && CDisplaySettings::GetInstance().GetCurrentResolution() == RES_WINDOW)
+ {
+ // defer saving resolution after window was created
+ CDisplaySettings::GetInstance().SetCurrentResolution(RES_DESKTOP);
+ sav_res = true;
+ }
+
+ if (!CServiceBroker::GetWinSystem()->GetGfxContext().IsValidResolution(CDisplaySettings::GetInstance().GetCurrentResolution()))
+ {
+ // Oh uh - doesn't look good for starting in their wanted screenmode
+ CLog::Log(LOGERROR, "The screen resolution requested is not valid, resetting to a valid mode");
+ CDisplaySettings::GetInstance().SetCurrentResolution(RES_DESKTOP);
+ sav_res = true;
+ }
+ if (!InitWindow())
+ {
+ return false;
+ }
+
+ // Set default screen saver mode
+ auto screensaverModeSetting = std::static_pointer_cast<CSettingString>(settings->GetSetting(CSettings::SETTING_SCREENSAVER_MODE));
+ // Can only set this after windowing has been initialized since it depends on it
+ if (CServiceBroker::GetWinSystem()->GetOSScreenSaver())
+ {
+ // If OS has a screen saver, use it by default
+ screensaverModeSetting->SetDefault("");
+ }
+ else
+ {
+ // If OS has no screen saver, use Kodi one by default
+ screensaverModeSetting->SetDefault("screensaver.xbmc.builtin.dim");
+ }
+
+ if (sav_res)
+ CDisplaySettings::GetInstance().SetCurrentResolution(RES_DESKTOP, true);
+
+ m_pGUI.reset(new CGUIComponent());
+ m_pGUI->Init();
+
+ // Splash requires gui component!!
+ CServiceBroker::GetRenderSystem()->ShowSplash("");
+
+ // The key mappings may already have been loaded by a peripheral
+ CLog::Log(LOGINFO, "load keymapping");
+ if (!CServiceBroker::GetInputManager().LoadKeymaps())
+ return false;
+
+ RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ CLog::Log(LOGINFO, "GUI format {}x{}, Display {}", info.iWidth, info.iHeight, info.strMode);
+
+ return true;
+}
+
+bool CApplication::InitWindow(RESOLUTION res)
+{
+ if (res == RES_INVALID)
+ res = CDisplaySettings::GetInstance().GetCurrentResolution();
+
+ bool bFullScreen = res != RES_WINDOW;
+ if (!CServiceBroker::GetWinSystem()->CreateNewWindow(CSysInfo::GetAppName(),
+ bFullScreen, CDisplaySettings::GetInstance().GetResolutionInfo(res)))
+ {
+ CLog::Log(LOGFATAL, "CApplication::Create: Unable to create window");
+ return false;
+ }
+
+ if (!CServiceBroker::GetRenderSystem()->InitRenderSystem())
+ {
+ CLog::Log(LOGFATAL, "CApplication::Create: Unable to init rendering system");
+ return false;
+ }
+ // set GUI res and force the clear of the screen
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(res, false);
+ return true;
+}
+
+bool CApplication::Initialize()
+{
+ m_pActiveAE->Start();
+ // restore AE's previous volume state
+
+ const auto appVolume = GetComponent<CApplicationVolumeHandling>();
+ const auto level = appVolume->GetVolumeRatio();
+ const auto muted = appVolume->IsMuted();
+ appVolume->SetHardwareVolume(level);
+ CServiceBroker::GetActiveAE()->SetMute(muted);
+
+#if defined(HAS_DVD_DRIVE) && !defined(TARGET_WINDOWS) // somehow this throws an "unresolved external symbol" on win32
+ // turn off cdio logging
+ cdio_loglevel_default = CDIO_LOG_ERROR;
+#endif
+
+ // load the language and its translated strings
+ if (!LoadLanguage(false))
+ return false;
+
+ // load media manager sources (e.g. root addon type sources depend on language strings to be available)
+ CServiceBroker::GetMediaManager().LoadSources();
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ profileManager->GetEventLog().Add(EventPtr(new CNotificationEvent(
+ StringUtils::Format(g_localizeStrings.Get(177), g_sysinfo.GetAppName()),
+ StringUtils::Format(g_localizeStrings.Get(178), g_sysinfo.GetAppName()),
+ "special://xbmc/media/icon256x256.png", EventLevel::Basic)));
+
+ m_ServiceManager->GetNetwork().WaitForNet();
+
+ // initialize (and update as needed) our databases
+ CDatabaseManager &databaseManager = m_ServiceManager->GetDatabaseManager();
+
+ CEvent event(true);
+ CServiceBroker::GetJobManager()->Submit([&databaseManager, &event]() {
+ databaseManager.Initialize();
+ event.Set();
+ });
+
+ std::string localizedStr = g_localizeStrings.Get(24150);
+ int iDots = 1;
+ while (!event.Wait(1000ms))
+ {
+ if (databaseManager.IsUpgrading())
+ CServiceBroker::GetRenderSystem()->ShowSplash(std::string(iDots, ' ') + localizedStr + std::string(iDots, '.'));
+
+ if (iDots == 3)
+ iDots = 1;
+ else
+ ++iDots;
+ }
+ CServiceBroker::GetRenderSystem()->ShowSplash("");
+
+ // Initialize GUI font manager to build/update fonts cache
+ //! @todo Move GUIFontManager into service broker and drop the global reference
+ event.Reset();
+ GUIFontManager& guiFontManager = g_fontManager;
+ CServiceBroker::GetJobManager()->Submit([&guiFontManager, &event]() {
+ guiFontManager.Initialize();
+ event.Set();
+ });
+ localizedStr = g_localizeStrings.Get(39175);
+ iDots = 1;
+ while (!event.Wait(1000ms))
+ {
+ if (g_fontManager.IsUpdating())
+ CServiceBroker::GetRenderSystem()->ShowSplash(std::string(iDots, ' ') + localizedStr +
+ std::string(iDots, '.'));
+
+ if (iDots == 3)
+ iDots = 1;
+ else
+ ++iDots;
+ }
+ CServiceBroker::GetRenderSystem()->ShowSplash("");
+
+ // GUI depends on seek handler
+ GetComponent<CApplicationPlayer>()->GetSeekHandler().Configure();
+
+ const auto skinHandling = GetComponent<CApplicationSkinHandling>();
+
+ bool uiInitializationFinished = false;
+
+ if (CServiceBroker::GetGUI()->GetWindowManager().Initialized())
+ {
+ const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ CServiceBroker::GetGUI()->GetWindowManager().CreateWindows();
+
+ skinHandling->m_confirmSkinChange = false;
+
+ std::vector<AddonInfoPtr> incompatibleAddons;
+ event.Reset();
+
+ // Addon migration
+ if (CServiceBroker::GetAddonMgr().GetIncompatibleEnabledAddonInfos(incompatibleAddons))
+ {
+ if (CAddonSystemSettings::GetInstance().GetAddonAutoUpdateMode() == AUTO_UPDATES_ON)
+ {
+ CServiceBroker::GetJobManager()->Submit(
+ [&event, &incompatibleAddons]() {
+ if (CServiceBroker::GetRepositoryUpdater().CheckForUpdates())
+ CServiceBroker::GetRepositoryUpdater().Await();
+
+ incompatibleAddons = CServiceBroker::GetAddonMgr().MigrateAddons();
+ event.Set();
+ },
+ CJob::PRIORITY_DEDICATED);
+ localizedStr = g_localizeStrings.Get(24151);
+ iDots = 1;
+ while (!event.Wait(1000ms))
+ {
+ CServiceBroker::GetRenderSystem()->ShowSplash(std::string(iDots, ' ') + localizedStr +
+ std::string(iDots, '.'));
+ if (iDots == 3)
+ iDots = 1;
+ else
+ ++iDots;
+ }
+ m_incompatibleAddons = incompatibleAddons;
+ }
+ else
+ {
+ // If no update is active disable all incompatible addons during start
+ m_incompatibleAddons =
+ CServiceBroker::GetAddonMgr().DisableIncompatibleAddons(incompatibleAddons);
+ }
+ }
+
+ // Start splashscreen and load skin
+ CServiceBroker::GetRenderSystem()->ShowSplash("");
+ skinHandling->m_confirmSkinChange = true;
+
+ auto setting = settings->GetSetting(CSettings::SETTING_LOOKANDFEEL_SKIN);
+ if (!setting)
+ {
+ CLog::Log(LOGFATAL, "Failed to load setting for: {}", CSettings::SETTING_LOOKANDFEEL_SKIN);
+ return false;
+ }
+
+ CServiceBroker::RegisterTextureCache(std::make_shared<CTextureCache>());
+
+ std::string skinId = settings->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN);
+ if (!skinHandling->LoadSkin(skinId))
+ {
+ CLog::Log(LOGERROR, "Failed to load skin '{}'", skinId);
+ std::string defaultSkin =
+ std::static_pointer_cast<const CSettingString>(setting)->GetDefault();
+ if (!skinHandling->LoadSkin(defaultSkin))
+ {
+ CLog::Log(LOGFATAL, "Default skin '{}' could not be loaded! Terminating..", defaultSkin);
+ return false;
+ }
+ }
+
+ // initialize splash window after splash screen disappears
+ // because we need a real window in the background which gets
+ // rendered while we load the main window or enter the master lock key
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SPLASH);
+
+ if (settings->GetBool(CSettings::SETTING_MASTERLOCK_STARTUPLOCK) &&
+ profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE &&
+ !profileManager->GetMasterProfile().getLockCode().empty())
+ {
+ g_passwordManager.CheckStartUpLock();
+ }
+
+ // check if we should use the login screen
+ if (profileManager->UsingLoginScreen())
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_LOGIN_SCREEN);
+ }
+ else
+ {
+ // activate the configured start window
+ int firstWindow = g_SkinInfo->GetFirstWindow();
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(firstWindow);
+
+ if (CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_STARTUP_ANIM))
+ {
+ CLog::Log(LOGWARNING, "CApplication::Initialize - startup.xml taints init process");
+ }
+
+ // the startup window is considered part of the initialization as it most likely switches to the final window
+ uiInitializationFinished = firstWindow != WINDOW_STARTUP_ANIM;
+ }
+ }
+ else //No GUI Created
+ {
+ uiInitializationFinished = true;
+ }
+
+ CJSONRPC::Initialize();
+
+ CServiceBroker::RegisterSpeechRecognition(speech::ISpeechRecognition::CreateInstance());
+
+ if (!m_ServiceManager->InitStageThree(profileManager))
+ {
+ CLog::Log(LOGERROR, "Application - Init3 failed");
+ }
+
+ g_sysinfo.Refresh();
+
+ CLog::Log(LOGINFO, "removing tempfiles");
+ CUtil::RemoveTempFiles();
+
+ if (!profileManager->UsingLoginScreen())
+ {
+ UpdateLibraries();
+ SetLoggingIn(false);
+ }
+
+ m_slowTimer.StartZero();
+
+ // register action listeners
+ const auto appListener = GetComponent<CApplicationActionListeners>();
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ appListener->RegisterActionListener(&appPlayer->GetSeekHandler());
+ appListener->RegisterActionListener(&CPlayerController::GetInstance());
+
+ CServiceBroker::GetRepositoryUpdater().Start();
+ if (!profileManager->UsingLoginScreen())
+ CServiceBroker::GetServiceAddons().Start();
+
+ CLog::Log(LOGINFO, "initialize done");
+
+ const auto appPower = GetComponent<CApplicationPowerHandling>();
+ appPower->CheckOSScreenSaverInhibitionSetting();
+ // reset our screensaver (starts timers etc.)
+ appPower->ResetScreenSaver();
+
+ // if the user interfaces has been fully initialized let everyone know
+ if (uiInitializationFinished)
+ {
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UI_READY);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+
+ return true;
+}
+
+bool CApplication::OnSettingsSaving() const
+{
+ // don't save settings when we're busy stopping the application
+ // a lot of screens try to save settings on deinit and deinit is
+ // called for every screen when the application is stopping
+ return !m_bStop;
+}
+
+void CApplication::Render()
+{
+ // do not render if we are stopped or in background
+ if (m_bStop)
+ return;
+
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ const auto appPower = GetComponent<CApplicationPowerHandling>();
+
+ bool hasRendered = false;
+
+ // Whether externalplayer is playing and we're unfocused
+ bool extPlayerActive = appPlayer->IsExternalPlaying() && !m_AppFocused;
+
+ if (!extPlayerActive && CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenVideo() &&
+ !appPlayer->IsPausedPlayback())
+ {
+ appPower->ResetScreenSaver();
+ }
+
+ if (!CServiceBroker::GetRenderSystem()->BeginRender())
+ return;
+
+ // render gui layer
+ if (appPower->GetRenderGUI() && !m_skipGuiRender)
+ {
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode())
+ {
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetStereoView(RENDER_STEREO_VIEW_LEFT);
+ hasRendered |= CServiceBroker::GetGUI()->GetWindowManager().Render();
+
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() != RENDER_STEREO_MODE_MONO)
+ {
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetStereoView(RENDER_STEREO_VIEW_RIGHT);
+ hasRendered |= CServiceBroker::GetGUI()->GetWindowManager().Render();
+ }
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetStereoView(RENDER_STEREO_VIEW_OFF);
+ }
+ else
+ {
+ hasRendered |= CServiceBroker::GetGUI()->GetWindowManager().Render();
+ }
+ // execute post rendering actions (finalize window closing)
+ CServiceBroker::GetGUI()->GetWindowManager().AfterRender();
+
+ m_lastRenderTime = std::chrono::steady_clock::now();
+ }
+
+ // render video layer
+ CServiceBroker::GetGUI()->GetWindowManager().RenderEx();
+
+ CServiceBroker::GetRenderSystem()->EndRender();
+
+ // reset our info cache - we do this at the end of Render so that it is
+ // fresh for the next process(), or after a windowclose animation (where process()
+ // isn't called)
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ infoMgr.ResetCache();
+ infoMgr.GetInfoProviders().GetGUIControlsInfoProvider().ResetContainerMovingCache();
+
+ if (hasRendered)
+ {
+ infoMgr.GetInfoProviders().GetSystemInfoProvider().UpdateFPS();
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().Flip(hasRendered,
+ appPlayer->IsRenderingVideoLayer());
+
+ CTimeUtils::UpdateFrameTime(hasRendered);
+}
+
+bool CApplication::OnAction(const CAction &action)
+{
+ // special case for switching between GUI & fullscreen mode.
+ if (action.GetID() == ACTION_SHOW_GUI)
+ { // Switch to fullscreen mode if we can
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ {
+ if (gui->GetWindowManager().SwitchToFullScreen())
+ {
+ GetComponent<CApplicationPowerHandling>()->m_navigationTimer.StartZero();
+ return true;
+ }
+ }
+ }
+
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+
+ if (action.GetID() == ACTION_TOGGLE_FULLSCREEN)
+ {
+ CServiceBroker::GetWinSystem()->GetGfxContext().ToggleFullScreen();
+ appPlayer->TriggerUpdateResolution();
+ return true;
+ }
+
+ if (action.IsMouse())
+ CServiceBroker::GetInputManager().SetMouseActive(true);
+
+ if (action.GetID() == ACTION_CREATE_EPISODE_BOOKMARK)
+ {
+ CGUIDialogVideoBookmarks::OnAddEpisodeBookmark();
+ }
+ if (action.GetID() == ACTION_CREATE_BOOKMARK)
+ {
+ CGUIDialogVideoBookmarks::OnAddBookmark();
+ }
+
+ // The action PLAYPAUSE behaves as ACTION_PAUSE if we are currently
+ // playing or ACTION_PLAYER_PLAY if we are seeking (FF/RW) or not playing.
+ if (action.GetID() == ACTION_PLAYER_PLAYPAUSE)
+ {
+ CGUIWindowSlideShow* pSlideShow = CServiceBroker::GetGUI()->
+ GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if ((appPlayer->IsPlaying() && appPlayer->GetPlaySpeed() == 1) ||
+ (pSlideShow && pSlideShow->InSlideShow() && !pSlideShow->IsPaused()))
+ return OnAction(CAction(ACTION_PAUSE));
+ else
+ return OnAction(CAction(ACTION_PLAYER_PLAY));
+ }
+
+ //if the action would start or stop inertial scrolling
+ //by gesture - bypass the normal OnAction handler of current window
+ if( !m_pInertialScrollingHandler->CheckForInertialScrolling(&action) )
+ {
+ // in normal case
+ // just pass the action to the current window and let it handle it
+ if (CServiceBroker::GetGUI()->GetWindowManager().OnAction(action))
+ {
+ GetComponent<CApplicationPowerHandling>()->ResetNavigationTimer();
+ return true;
+ }
+ }
+
+ // handle extra global presses
+
+ // notify action listeners
+ if (GetComponent<CApplicationActionListeners>()->NotifyActionListeners(action))
+ return true;
+
+ // screenshot : take a screenshot :)
+ if (action.GetID() == ACTION_TAKE_SCREENSHOT)
+ {
+ CScreenShot::TakeScreenshot();
+ return true;
+ }
+ // Display HDR : toggle HDR on/off
+ if (action.GetID() == ACTION_HDR_TOGGLE)
+ {
+ // Only enables manual HDR toggle if no video is playing or auto HDR switch is disabled
+ if (appPlayer->IsPlayingVideo() &&
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CServiceBroker::GetWinSystem()->SETTING_WINSYSTEM_IS_HDR_DISPLAY))
+ return true;
+
+ HDR_STATUS hdrStatus = CServiceBroker::GetWinSystem()->ToggleHDR();
+
+ if (hdrStatus == HDR_STATUS::HDR_OFF)
+ {
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(34220),
+ g_localizeStrings.Get(34221));
+ }
+ else if (hdrStatus == HDR_STATUS::HDR_ON)
+ {
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(34220),
+ g_localizeStrings.Get(34222));
+ }
+ return true;
+ }
+ // Tone Mapping : switch to next tone map method
+ if (action.GetID() == ACTION_CYCLE_TONEMAP_METHOD)
+ {
+ // Only enables tone mapping switch if display is not HDR capable or HDR is not enabled
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CServiceBroker::GetWinSystem()->SETTING_WINSYSTEM_IS_HDR_DISPLAY) &&
+ CServiceBroker::GetWinSystem()->IsHDRDisplay())
+ return true;
+
+ if (appPlayer->IsPlayingVideo())
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_ToneMapMethod = static_cast<ETONEMAPMETHOD>(static_cast<int>(vs.m_ToneMapMethod) + 1);
+ if (vs.m_ToneMapMethod >= VS_TONEMAPMETHOD_MAX)
+ vs.m_ToneMapMethod =
+ static_cast<ETONEMAPMETHOD>(static_cast<int>(VS_TONEMAPMETHOD_OFF) + 1);
+
+ appPlayer->SetVideoSettings(vs);
+
+ int code = 0;
+ switch (vs.m_ToneMapMethod)
+ {
+ case VS_TONEMAPMETHOD_REINHARD:
+ code = 36555;
+ break;
+ case VS_TONEMAPMETHOD_ACES:
+ code = 36557;
+ break;
+ case VS_TONEMAPMETHOD_HABLE:
+ code = 36558;
+ break;
+ default:
+ throw std::logic_error("Tonemapping method not found. Did you forget to add a mapping?");
+ }
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(34224),
+ g_localizeStrings.Get(code), 1000, false, 500);
+ }
+ return true;
+ }
+ // built in functions : execute the built-in
+ if (action.GetID() == ACTION_BUILT_IN_FUNCTION)
+ {
+ if (!CBuiltins::GetInstance().IsSystemPowerdownCommand(action.GetName()) ||
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::PowerManagement>().CanSystemPowerdown())
+ {
+ CBuiltins::GetInstance().Execute(action.GetName());
+ GetComponent<CApplicationPowerHandling>()->ResetNavigationTimer();
+ }
+ return true;
+ }
+
+ // reload keymaps
+ if (action.GetID() == ACTION_RELOAD_KEYMAPS)
+ CServiceBroker::GetInputManager().ReloadKeymaps();
+
+ // show info : Shows the current video or song information
+ if (action.GetID() == ACTION_SHOW_INFO)
+ {
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().ToggleShowInfo();
+ return true;
+ }
+
+ if (action.GetID() == ACTION_SET_RATING && appPlayer->IsPlayingAudio())
+ {
+ int userrating = MUSIC_UTILS::ShowSelectRatingDialog(m_itemCurrentFile->GetMusicInfoTag()->GetUserrating());
+ if (userrating < 0) // Nothing selected, so user rating unchanged
+ return true;
+ userrating = std::min(userrating, 10);
+ if (userrating != m_itemCurrentFile->GetMusicInfoTag()->GetUserrating())
+ {
+ m_itemCurrentFile->GetMusicInfoTag()->SetUserrating(userrating);
+ // Mirror changes to GUI item
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*m_itemCurrentFile);
+
+ // Asynchronously update song userrating in music library
+ MUSIC_UTILS::UpdateSongRatingJob(m_itemCurrentFile, userrating);
+
+ // Tell all windows (e.g. playlistplayer, media windows) to update the fileitem
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_itemCurrentFile);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+ return true;
+ }
+
+ else if ((action.GetID() == ACTION_INCREASE_RATING || action.GetID() == ACTION_DECREASE_RATING) &&
+ appPlayer->IsPlayingAudio())
+ {
+ int userrating = m_itemCurrentFile->GetMusicInfoTag()->GetUserrating();
+ bool needsUpdate(false);
+ if (userrating > 0 && action.GetID() == ACTION_DECREASE_RATING)
+ {
+ m_itemCurrentFile->GetMusicInfoTag()->SetUserrating(userrating - 1);
+ needsUpdate = true;
+ }
+ else if (userrating < 10 && action.GetID() == ACTION_INCREASE_RATING)
+ {
+ m_itemCurrentFile->GetMusicInfoTag()->SetUserrating(userrating + 1);
+ needsUpdate = true;
+ }
+ if (needsUpdate)
+ {
+ // Mirror changes to current GUI item
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*m_itemCurrentFile);
+
+ // Asynchronously update song userrating in music library
+ MUSIC_UTILS::UpdateSongRatingJob(m_itemCurrentFile, m_itemCurrentFile->GetMusicInfoTag()->GetUserrating());
+
+ // send a message to all windows to tell them to update the fileitem (eg playlistplayer, media windows)
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_itemCurrentFile);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+
+ return true;
+ }
+ else if ((action.GetID() == ACTION_INCREASE_RATING || action.GetID() == ACTION_DECREASE_RATING) &&
+ appPlayer->IsPlayingVideo())
+ {
+ int rating = m_itemCurrentFile->GetVideoInfoTag()->m_iUserRating;
+ bool needsUpdate(false);
+ if (rating > 1 && action.GetID() == ACTION_DECREASE_RATING)
+ {
+ m_itemCurrentFile->GetVideoInfoTag()->m_iUserRating = rating - 1;
+ needsUpdate = true;
+ }
+ else if (rating < 10 && action.GetID() == ACTION_INCREASE_RATING)
+ {
+ m_itemCurrentFile->GetVideoInfoTag()->m_iUserRating = rating + 1;
+ needsUpdate = true;
+ }
+ if (needsUpdate)
+ {
+ // Mirror changes to GUI item
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*m_itemCurrentFile);
+
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ db.SetVideoUserRating(m_itemCurrentFile->GetVideoInfoTag()->m_iDbId,
+ m_itemCurrentFile->GetVideoInfoTag()->m_iUserRating,
+ m_itemCurrentFile->GetVideoInfoTag()->m_type);
+ db.Close();
+ }
+ // send a message to all windows to tell them to update the fileitem (eg playlistplayer, media windows)
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_itemCurrentFile);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+ return true;
+ }
+
+ // Now check with the playlist player if action can be handled.
+ // In case of ACTION_PREV_ITEM, we only allow the playlist player to take it if we're less than ACTION_PREV_ITEM_THRESHOLD seconds into playback.
+ if (!(action.GetID() == ACTION_PREV_ITEM && appPlayer->CanSeek() &&
+ GetTime() > ACTION_PREV_ITEM_THRESHOLD))
+ {
+ if (CServiceBroker::GetPlaylistPlayer().OnAction(action))
+ return true;
+ }
+
+ // Now check with the player if action can be handled.
+ bool bIsPlayingPVRChannel = (CServiceBroker::GetPVRManager().IsStarted() &&
+ CurrentFileItem().IsPVRChannel());
+
+ bool bNotifyPlayer = false;
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_VIDEO)
+ bNotifyPlayer = true;
+ else if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_GAME)
+ bNotifyPlayer = true;
+ else if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VISUALISATION && bIsPlayingPVRChannel)
+ bNotifyPlayer = true;
+ else if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_DIALOG_VIDEO_OSD ||
+ (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_DIALOG_MUSIC_OSD && bIsPlayingPVRChannel))
+ {
+ switch (action.GetID())
+ {
+ case ACTION_NEXT_ITEM:
+ case ACTION_PREV_ITEM:
+ case ACTION_CHANNEL_UP:
+ case ACTION_CHANNEL_DOWN:
+ bNotifyPlayer = true;
+ break;
+ default:
+ break;
+ }
+ }
+ else if (action.GetID() == ACTION_STOP)
+ bNotifyPlayer = true;
+
+ if (bNotifyPlayer)
+ {
+ if (appPlayer->OnAction(action))
+ return true;
+ }
+
+ // stop : stops playing current audio song
+ if (action.GetID() == ACTION_STOP)
+ {
+ StopPlaying();
+ return true;
+ }
+
+ // In case the playlist player nor the player didn't handle PREV_ITEM, because we are past the ACTION_PREV_ITEM_THRESHOLD secs limit.
+ // If so, we just jump to the start of the track.
+ if (action.GetID() == ACTION_PREV_ITEM && appPlayer->CanSeek())
+ {
+ SeekTime(0);
+ appPlayer->SetPlaySpeed(1);
+ return true;
+ }
+
+ // forward action to graphic context and see if it can handle it
+ if (CServiceBroker::GetGUI()->GetStereoscopicsManager().OnAction(action))
+ return true;
+
+ if (appPlayer->IsPlaying())
+ {
+ // forward channel switches to the player - he knows what to do
+ if (action.GetID() == ACTION_CHANNEL_UP || action.GetID() == ACTION_CHANNEL_DOWN)
+ {
+ appPlayer->OnAction(action);
+ return true;
+ }
+
+ // pause : toggle pause action
+ if (action.GetID() == ACTION_PAUSE)
+ {
+ appPlayer->Pause();
+ // go back to normal play speed on unpause
+ if (!appPlayer->IsPaused() && appPlayer->GetPlaySpeed() != 1)
+ appPlayer->SetPlaySpeed(1);
+
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetAudioManager().Enable(appPlayer->IsPaused());
+ return true;
+ }
+ // play: unpause or set playspeed back to normal
+ if (action.GetID() == ACTION_PLAYER_PLAY)
+ {
+ // if currently paused - unpause
+ if (appPlayer->IsPaused())
+ return OnAction(CAction(ACTION_PAUSE));
+ // if we do a FF/RW then go back to normal speed
+ if (appPlayer->GetPlaySpeed() != 1)
+ appPlayer->SetPlaySpeed(1);
+ return true;
+ }
+ if (!appPlayer->IsPaused())
+ {
+ if (action.GetID() == ACTION_PLAYER_FORWARD || action.GetID() == ACTION_PLAYER_REWIND)
+ {
+ float playSpeed = appPlayer->GetPlaySpeed();
+
+ if (action.GetID() == ACTION_PLAYER_REWIND && (playSpeed == 1)) // Enables Rewinding
+ playSpeed *= -2;
+ else if (action.GetID() == ACTION_PLAYER_REWIND && playSpeed > 1) //goes down a notch if you're FFing
+ playSpeed /= 2;
+ else if (action.GetID() == ACTION_PLAYER_FORWARD && playSpeed < 1) //goes up a notch if you're RWing
+ playSpeed /= 2;
+ else
+ playSpeed *= 2;
+
+ if (action.GetID() == ACTION_PLAYER_FORWARD && playSpeed == -1) //sets iSpeed back to 1 if -1 (didn't plan for a -1)
+ playSpeed = 1;
+ if (playSpeed > 32 || playSpeed < -32)
+ playSpeed = 1;
+
+ appPlayer->SetPlaySpeed(playSpeed);
+ return true;
+ }
+ else if ((action.GetAmount() || appPlayer->GetPlaySpeed() != 1) &&
+ (action.GetID() == ACTION_ANALOG_REWIND || action.GetID() == ACTION_ANALOG_FORWARD))
+ {
+ // calculate the speed based on the amount the button is held down
+ int iPower = (int)(action.GetAmount() * MAX_FFWD_SPEED + 0.5f);
+ // amount can be negative, for example rewind and forward share the same axis
+ iPower = std::abs(iPower);
+ // returns 0 -> MAX_FFWD_SPEED
+ int iSpeed = 1 << iPower;
+ if (iSpeed != 1 && action.GetID() == ACTION_ANALOG_REWIND)
+ iSpeed = -iSpeed;
+ appPlayer->SetPlaySpeed(static_cast<float>(iSpeed));
+ if (iSpeed == 1)
+ CLog::Log(LOGDEBUG,"Resetting playspeed");
+ return true;
+ }
+ }
+ // allow play to unpause
+ else
+ {
+ if (action.GetID() == ACTION_PLAYER_PLAY)
+ {
+ // unpause, and set the playspeed back to normal
+ appPlayer->Pause();
+
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetAudioManager().Enable(appPlayer->IsPaused());
+
+ appPlayer->SetPlaySpeed(1);
+ return true;
+ }
+ }
+ }
+
+
+ if (action.GetID() == ACTION_SWITCH_PLAYER)
+ {
+ const CPlayerCoreFactory &playerCoreFactory = m_ServiceManager->GetPlayerCoreFactory();
+
+ if (appPlayer->IsPlaying())
+ {
+ std::vector<std::string> players;
+ CFileItem item(*m_itemCurrentFile.get());
+ playerCoreFactory.GetPlayers(item, players);
+ std::string player = playerCoreFactory.SelectPlayerDialog(players);
+ if (!player.empty())
+ {
+ item.SetStartOffset(CUtil::ConvertSecsToMilliSecs(GetTime()));
+ PlayFile(item, player, true);
+ }
+ }
+ else
+ {
+ std::vector<std::string> players;
+ playerCoreFactory.GetRemotePlayers(players);
+ std::string player = playerCoreFactory.SelectPlayerDialog(players);
+ if (!player.empty())
+ {
+ PlayFile(CFileItem(), player, false);
+ }
+ }
+ }
+
+ if (CServiceBroker::GetPeripherals().OnAction(action))
+ return true;
+
+ if (action.GetID() == ACTION_MUTE)
+ {
+ const auto appVolume = GetComponent<CApplicationVolumeHandling>();
+ appVolume->ToggleMute();
+ appVolume->ShowVolumeBar(&action);
+ return true;
+ }
+
+ if (action.GetID() == ACTION_TOGGLE_DIGITAL_ANALOG)
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ bool passthrough = settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH);
+ settings->SetBool(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH, !passthrough);
+
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SETTINGS_SYSTEM)
+ {
+ CGUIMessage msg(GUI_MSG_WINDOW_INIT, 0,0,WINDOW_INVALID,CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+ return true;
+ }
+
+ // Check for global volume control
+ if ((action.GetAmount() && (action.GetID() == ACTION_VOLUME_UP || action.GetID() == ACTION_VOLUME_DOWN)) || action.GetID() == ACTION_VOLUME_SET)
+ {
+ const auto appVolume = GetComponent<CApplicationVolumeHandling>();
+ if (!appPlayer->IsPassthrough())
+ {
+ if (appVolume->IsMuted())
+ appVolume->UnMute();
+ float volume = appVolume->GetVolumeRatio();
+ int volumesteps = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_AUDIOOUTPUT_VOLUMESTEPS);
+ // sanity check
+ if (volumesteps == 0)
+ volumesteps = 90;
+
+// Android has steps based on the max available volume level
+#if defined(TARGET_ANDROID)
+ float step = (CApplicationVolumeHandling::VOLUME_MAXIMUM -
+ CApplicationVolumeHandling::VOLUME_MINIMUM) /
+ CXBMCApp::GetMaxSystemVolume();
+#else
+ float step = (CApplicationVolumeHandling::VOLUME_MAXIMUM -
+ CApplicationVolumeHandling::VOLUME_MINIMUM) /
+ volumesteps;
+
+ if (action.GetRepeat())
+ step *= action.GetRepeat() * 50; // 50 fps
+#endif
+ if (action.GetID() == ACTION_VOLUME_UP)
+ volume += action.GetAmount() * action.GetAmount() * step;
+ else if (action.GetID() == ACTION_VOLUME_DOWN)
+ volume -= action.GetAmount() * action.GetAmount() * step;
+ else
+ volume = action.GetAmount() * step;
+ if (volume != appVolume->GetVolumeRatio())
+ appVolume->SetVolume(volume, false);
+ }
+ // show visual feedback of volume or passthrough indicator
+ appVolume->ShowVolumeBar(&action);
+ return true;
+ }
+
+ if (action.GetID() == ACTION_GUIPROFILE_BEGIN)
+ {
+ CGUIControlProfiler::Instance().SetOutputFile(CSpecialProtocol::TranslatePath("special://home/guiprofiler.xml"));
+ CGUIControlProfiler::Instance().Start();
+ return true;
+ }
+ if (action.GetID() == ACTION_SHOW_PLAYLIST)
+ {
+ const PLAYLIST::Id playlistId = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+ if (playlistId == PLAYLIST::TYPE_VIDEO &&
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_VIDEO_PLAYLIST)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_PLAYLIST);
+ }
+ else if (playlistId == PLAYLIST::TYPE_MUSIC &&
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() !=
+ WINDOW_MUSIC_PLAYLIST)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST);
+ }
+ return true;
+ }
+ return false;
+}
+
+int CApplication::GetMessageMask()
+{
+ return TMSG_MASK_APPLICATION;
+}
+
+void CApplication::OnApplicationMessage(ThreadMessage* pMsg)
+{
+ uint32_t msg = pMsg->dwMessage;
+ if (msg == TMSG_SYSTEM_POWERDOWN)
+ {
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::PowerManagement>().CanSystemPowerdown())
+ msg = pMsg->param1; // perform requested shutdown action
+ else
+ return; // no shutdown
+ }
+
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+
+ switch (msg)
+ {
+ case TMSG_POWERDOWN:
+ if (Stop(EXITCODE_POWERDOWN))
+ CServiceBroker::GetPowerManager().Powerdown();
+ break;
+
+ case TMSG_QUIT:
+ Stop(EXITCODE_QUIT);
+ break;
+
+ case TMSG_SHUTDOWN:
+ GetComponent<CApplicationPowerHandling>()->HandleShutdownMessage();
+ break;
+
+ case TMSG_RENDERER_FLUSH:
+ appPlayer->FlushRenderer();
+ break;
+
+ case TMSG_HIBERNATE:
+ CServiceBroker::GetPowerManager().Hibernate();
+ break;
+
+ case TMSG_SUSPEND:
+ CServiceBroker::GetPowerManager().Suspend();
+ break;
+
+ case TMSG_RESTART:
+ case TMSG_RESET:
+ if (Stop(EXITCODE_REBOOT))
+ CServiceBroker::GetPowerManager().Reboot();
+ break;
+
+ case TMSG_RESTARTAPP:
+#if defined(TARGET_WINDOWS) || defined(TARGET_LINUX)
+ Stop(EXITCODE_RESTARTAPP);
+#endif
+ break;
+
+ case TMSG_INHIBITIDLESHUTDOWN:
+ GetComponent<CApplicationPowerHandling>()->InhibitIdleShutdown(pMsg->param1 != 0);
+ break;
+
+ case TMSG_INHIBITSCREENSAVER:
+ GetComponent<CApplicationPowerHandling>()->InhibitScreenSaver(pMsg->param1 != 0);
+ break;
+
+ case TMSG_ACTIVATESCREENSAVER:
+ GetComponent<CApplicationPowerHandling>()->ActivateScreenSaver();
+ break;
+
+ case TMSG_RESETSCREENSAVER:
+ GetComponent<CApplicationPowerHandling>()->m_bResetScreenSaver = true;
+ break;
+
+ case TMSG_VOLUME_SHOW:
+ {
+ CAction action(pMsg->param1);
+ GetComponent<CApplicationVolumeHandling>()->ShowVolumeBar(&action);
+ }
+ break;
+
+#ifdef TARGET_ANDROID
+ case TMSG_DISPLAY_SETUP:
+ // We might come from a refresh rate switch destroying the native window; use the context resolution
+ *static_cast<bool*>(pMsg->lpVoid) = InitWindow(CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution());
+ GetComponent<CApplicationPowerHandling>()->SetRenderGUI(true);
+ break;
+
+ case TMSG_DISPLAY_DESTROY:
+ *static_cast<bool*>(pMsg->lpVoid) = CServiceBroker::GetWinSystem()->DestroyWindow();
+ GetComponent<CApplicationPowerHandling>()->SetRenderGUI(false);
+ break;
+#endif
+
+ case TMSG_START_ANDROID_ACTIVITY:
+ {
+#if defined(TARGET_ANDROID)
+ if (pMsg->params.size())
+ {
+ CXBMCApp::StartActivity(pMsg->params[0], pMsg->params.size() > 1 ? pMsg->params[1] : "",
+ pMsg->params.size() > 2 ? pMsg->params[2] : "",
+ pMsg->params.size() > 3 ? pMsg->params[3] : "",
+ pMsg->params.size() > 4 ? pMsg->params[4] : "",
+ pMsg->params.size() > 5 ? pMsg->params[5] : "",
+ pMsg->params.size() > 6 ? pMsg->params[6] : "",
+ pMsg->params.size() > 7 ? pMsg->params[7] : "",
+ pMsg->params.size() > 8 ? pMsg->params[8] : "");
+ }
+#endif
+ }
+ break;
+
+ case TMSG_NETWORKMESSAGE:
+ m_ServiceManager->GetNetwork().NetworkMessage(static_cast<CNetworkBase::EMESSAGE>(pMsg->param1),
+ pMsg->param2);
+ break;
+
+ case TMSG_SETLANGUAGE:
+ SetLanguage(pMsg->strParam);
+ break;
+
+
+ case TMSG_SWITCHTOFULLSCREEN:
+ {
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetWindowManager().SwitchToFullScreen(true);
+ break;
+ }
+ case TMSG_VIDEORESIZE:
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_VIDEORESIZE;
+ newEvent.resize.w = pMsg->param1;
+ newEvent.resize.h = pMsg->param2;
+ OnEvent(newEvent);
+ CServiceBroker::GetGUI()->GetWindowManager().MarkDirty();
+ }
+ break;
+
+ case TMSG_SETVIDEORESOLUTION:
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(static_cast<RESOLUTION>(pMsg->param1), pMsg->param2 == 1);
+ break;
+
+ case TMSG_TOGGLEFULLSCREEN:
+ CServiceBroker::GetWinSystem()->GetGfxContext().ToggleFullScreen();
+ appPlayer->TriggerUpdateResolution();
+ break;
+
+ case TMSG_MINIMIZE:
+ CServiceBroker::GetWinSystem()->Minimize();
+ break;
+
+ case TMSG_EXECUTE_OS:
+ // Suspend AE temporarily so exclusive or hog-mode sinks
+ // don't block external player's access to audio device
+ IAE *audioengine;
+ audioengine = CServiceBroker::GetActiveAE();
+ if (audioengine)
+ {
+ if (!audioengine->Suspend())
+ {
+ CLog::Log(LOGINFO, "{}: Failed to suspend AudioEngine before launching external program",
+ __FUNCTION__);
+ }
+ }
+#if defined(TARGET_DARWIN)
+ CLog::Log(LOGINFO, "ExecWait is not implemented on this platform");
+#elif defined(TARGET_POSIX)
+ CUtil::RunCommandLine(pMsg->strParam, (pMsg->param1 == 1));
+#elif defined(TARGET_WINDOWS)
+ CWIN32Util::XBMCShellExecute(pMsg->strParam.c_str(), (pMsg->param1 == 1));
+#endif
+ // Resume AE processing of XBMC native audio
+ if (audioengine)
+ {
+ if (!audioengine->Resume())
+ {
+ CLog::Log(LOGFATAL, "{}: Failed to restart AudioEngine after return from external player",
+ __FUNCTION__);
+ }
+ }
+ break;
+
+ case TMSG_EXECUTE_SCRIPT:
+ CScriptInvocationManager::GetInstance().ExecuteAsync(pMsg->strParam);
+ break;
+
+ case TMSG_EXECUTE_BUILT_IN:
+ CBuiltins::GetInstance().Execute(pMsg->strParam);
+ break;
+
+ case TMSG_PICTURE_SHOW:
+ {
+ CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (!pSlideShow) return;
+
+ // stop playing file
+ if (appPlayer->IsPlayingVideo())
+ StopPlaying();
+
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_VIDEO)
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+
+ const auto appPower = GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+ appPower->WakeUpScreenSaverAndDPMS();
+
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_SLIDESHOW)
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SLIDESHOW);
+ if (URIUtils::IsZIP(pMsg->strParam) || URIUtils::IsRAR(pMsg->strParam)) // actually a cbz/cbr
+ {
+ CFileItemList items;
+ CURL pathToUrl;
+ if (URIUtils::IsZIP(pMsg->strParam))
+ pathToUrl = URIUtils::CreateArchivePath("zip", CURL(pMsg->strParam), "");
+ else
+ pathToUrl = URIUtils::CreateArchivePath("rar", CURL(pMsg->strParam), "");
+
+ CUtil::GetRecursiveListing(pathToUrl.Get(), items, CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), XFILE::DIR_FLAG_NO_FILE_DIRS);
+ if (items.Size() > 0)
+ {
+ pSlideShow->Reset();
+ for (int i = 0; i<items.Size(); ++i)
+ {
+ pSlideShow->Add(items[i].get());
+ }
+ pSlideShow->Select(items[0]->GetPath());
+ }
+ }
+ else
+ {
+ CFileItem item(pMsg->strParam, false);
+ pSlideShow->Reset();
+ pSlideShow->Add(&item);
+ pSlideShow->Select(pMsg->strParam);
+ }
+ }
+ break;
+
+ case TMSG_PICTURE_SLIDESHOW:
+ {
+ CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (!pSlideShow) return;
+
+ if (appPlayer->IsPlayingVideo())
+ StopPlaying();
+
+ pSlideShow->Reset();
+
+ CFileItemList items;
+ std::string strPath = pMsg->strParam;
+ std::string extensions = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ if (pMsg->param1)
+ extensions += "|.tbn";
+ CUtil::GetRecursiveListing(strPath, items, extensions);
+
+ if (items.Size() > 0)
+ {
+ for (int i = 0; i<items.Size(); ++i)
+ pSlideShow->Add(items[i].get());
+ pSlideShow->StartSlideShow(); //Start the slideshow!
+ }
+
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_SLIDESHOW)
+ {
+ if (items.Size() == 0)
+ {
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_SCREENSAVER_MODE, "screensaver.xbmc.builtin.dim");
+ GetComponent<CApplicationPowerHandling>()->ActivateScreenSaver();
+ }
+ else
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SLIDESHOW);
+ }
+
+ }
+ break;
+
+ case TMSG_LOADPROFILE:
+ {
+ const int profile = pMsg->param1;
+ if (profile >= 0)
+ CServiceBroker::GetSettingsComponent()->GetProfileManager()->LoadProfile(static_cast<unsigned int>(profile));
+ }
+
+ break;
+
+ case TMSG_EVENT:
+ {
+ if (pMsg->lpVoid)
+ {
+ XBMC_Event* event = static_cast<XBMC_Event*>(pMsg->lpVoid);
+ OnEvent(*event);
+ delete event;
+ }
+ }
+ break;
+
+ case TMSG_UPDATE_PLAYER_ITEM:
+ {
+ std::unique_ptr<CFileItem> item{static_cast<CFileItem*>(pMsg->lpVoid)};
+ if (item)
+ {
+ m_itemCurrentFile->UpdateInfo(*item);
+ CServiceBroker::GetGUI()->GetInfoManager().UpdateCurrentItem(*m_itemCurrentFile);
+ }
+ }
+ break;
+
+ default:
+ CLog::Log(LOGERROR, "{}: Unhandled threadmessage sent, {}", __FUNCTION__, msg);
+ break;
+ }
+}
+
+void CApplication::LockFrameMoveGuard()
+{
+ ++m_WaitingExternalCalls;
+ m_frameMoveGuard.lock();
+ ++m_ProcessedExternalCalls;
+ CServiceBroker::GetWinSystem()->GetGfxContext().lock();
+};
+
+void CApplication::UnlockFrameMoveGuard()
+{
+ --m_WaitingExternalCalls;
+ CServiceBroker::GetWinSystem()->GetGfxContext().unlock();
+ m_frameMoveGuard.unlock();
+};
+
+void CApplication::FrameMove(bool processEvents, bool processGUI)
+{
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ bool renderGUI = GetComponent<CApplicationPowerHandling>()->GetRenderGUI();
+ if (processEvents)
+ {
+ // currently we calculate the repeat time (ie time from last similar keypress) just global as fps
+ float frameTime = m_frameTime.GetElapsedSeconds();
+ m_frameTime.StartZero();
+ // never set a frametime less than 2 fps to avoid problems when debugging and on breaks
+ if (frameTime > 0.5f)
+ frameTime = 0.5f;
+
+ if (processGUI && renderGUI)
+ {
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ // check if there are notifications to display
+ CGUIDialogKaiToast *toast = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogKaiToast>(WINDOW_DIALOG_KAI_TOAST);
+ if (toast && toast->DoWork())
+ {
+ if (!toast->IsDialogRunning())
+ {
+ toast->Open();
+ }
+ }
+ }
+
+ HandlePortEvents();
+ CServiceBroker::GetInputManager().Process(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog(), frameTime);
+
+ if (processGUI && renderGUI)
+ {
+ m_pInertialScrollingHandler->ProcessInertialScroll(frameTime);
+ appPlayer->GetSeekHandler().FrameMove();
+ }
+
+ // Open the door for external calls e.g python exactly here.
+ // Window size can be between 2 and 10ms and depends on number of continuous requests
+ if (m_WaitingExternalCalls)
+ {
+ CSingleExit ex(CServiceBroker::GetWinSystem()->GetGfxContext());
+ m_frameMoveGuard.unlock();
+
+ // Calculate a window size between 2 and 10ms, 4 continuous requests let the window grow by 1ms
+ // When not playing video we allow it to increase to 80ms
+ unsigned int max_sleep = 10;
+ if (!appPlayer->IsPlayingVideo() || appPlayer->IsPausedPlayback())
+ max_sleep = 80;
+ unsigned int sleepTime = std::max(static_cast<unsigned int>(2), std::min(m_ProcessedExternalCalls >> 2, max_sleep));
+ KODI::TIME::Sleep(std::chrono::milliseconds(sleepTime));
+ m_frameMoveGuard.lock();
+ m_ProcessedExternalDecay = 5;
+ }
+ if (m_ProcessedExternalDecay && --m_ProcessedExternalDecay == 0)
+ m_ProcessedExternalCalls = 0;
+ }
+
+ if (processGUI && renderGUI)
+ {
+ m_skipGuiRender = false;
+
+ /*! @todo look into the possibility to use this for GBM
+ int fps = 0;
+
+ // This code reduces rendering fps of the GUI layer when playing videos in fullscreen mode
+ // it makes only sense on architectures with multiple layers
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenVideo() && !m_appPlayer.IsPausedPlayback() && m_appPlayer.IsRenderingVideoLayer())
+ fps = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_LIMITGUIUPDATE);
+
+ auto now = std::chrono::steady_clock::now();
+
+ auto frameTime = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastRenderTime).count();
+ if (fps > 0 && frameTime * fps < 1000)
+ m_skipGuiRender = true;
+ */
+
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_guiSmartRedraw && m_guiRefreshTimer.IsTimePast())
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_REFRESH_TIMER, 0, 0);
+ m_guiRefreshTimer.Set(500ms);
+ }
+
+ if (!m_bStop)
+ {
+ if (!m_skipGuiRender)
+ CServiceBroker::GetGUI()->GetWindowManager().Process(CTimeUtils::GetFrameTime());
+ }
+ CServiceBroker::GetGUI()->GetWindowManager().FrameMove();
+ }
+
+ appPlayer->FrameMove();
+
+ // this will go away when render systems gets its own thread
+ CServiceBroker::GetWinSystem()->DriveRenderLoop();
+}
+
+
+void CApplication::ResetCurrentItem()
+{
+ m_itemCurrentFile->Reset();
+ if (m_pGUI)
+ m_pGUI->GetInfoManager().ResetCurrentItem();
+}
+
+int CApplication::Run()
+{
+ CLog::Log(LOGINFO, "Running the application...");
+
+ std::chrono::time_point<std::chrono::steady_clock> lastFrameTime;
+ std::chrono::milliseconds frameTime;
+ const unsigned int noRenderFrameTime = 15; // Simulates ~66fps
+
+ CFileItemList& playlist = CServiceBroker::GetAppParams()->GetPlaylist();
+ if (playlist.Size() > 0)
+ {
+ CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_MUSIC, playlist);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_PLAYLISTPLAYER_PLAY, -1);
+ }
+
+ // Run the app
+ while (!m_bStop)
+ {
+ // Animate and render a frame
+
+ lastFrameTime = std::chrono::steady_clock::now();
+ Process();
+
+ bool renderGUI = GetComponent<CApplicationPowerHandling>()->GetRenderGUI();
+ if (!m_bStop)
+ {
+ FrameMove(true, renderGUI);
+ }
+
+ if (renderGUI && !m_bStop)
+ {
+ Render();
+ }
+ else if (!renderGUI)
+ {
+ auto now = std::chrono::steady_clock::now();
+ frameTime = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastFrameTime);
+ if (frameTime.count() < noRenderFrameTime)
+ KODI::TIME::Sleep(std::chrono::milliseconds(noRenderFrameTime - frameTime.count()));
+ }
+ }
+
+ Cleanup();
+
+ CLog::Log(LOGINFO, "Exiting the application...");
+ return m_ExitCode;
+}
+
+bool CApplication::Cleanup()
+{
+ try
+ {
+ ResetCurrentItem();
+ StopPlaying();
+
+ if (m_ServiceManager)
+ m_ServiceManager->DeinitStageThree();
+
+ CServiceBroker::UnregisterSpeechRecognition();
+
+ CLog::Log(LOGINFO, "unload skin");
+ GetComponent<CApplicationSkinHandling>()->UnloadSkin();
+
+ CServiceBroker::UnregisterTextureCache();
+
+ // stop all remaining scripts; must be done after skin has been unloaded,
+ // not before some windows still need it when deinitializing during skin
+ // unloading
+ CScriptInvocationManager::GetInstance().Uninitialize();
+
+ const auto appPower = GetComponent<CApplicationPowerHandling>();
+ appPower->m_globalScreensaverInhibitor.Release();
+ appPower->m_screensaverInhibitor.Release();
+
+ CRenderSystemBase *renderSystem = CServiceBroker::GetRenderSystem();
+ if (renderSystem)
+ renderSystem->DestroyRenderSystem();
+
+ CWinSystemBase *winSystem = CServiceBroker::GetWinSystem();
+ if (winSystem)
+ winSystem->DestroyWindow();
+
+ if (m_pGUI)
+ m_pGUI->GetWindowManager().DestroyWindows();
+
+ CLog::Log(LOGINFO, "unload sections");
+
+ // Shutdown as much as possible of the
+ // application, to reduce the leaks dumped
+ // to the vc output window before calling
+ // _CrtDumpMemoryLeaks(). Most of the leaks
+ // shown are no real leaks, as parts of the app
+ // are still allocated.
+
+ g_localizeStrings.Clear();
+ g_LangCodeExpander.Clear();
+ g_charsetConverter.clear();
+ g_directoryCache.Clear();
+ //CServiceBroker::GetInputManager().ClearKeymaps(); //! @todo
+ CEventServer::RemoveInstance();
+ DllLoaderContainer::Clear();
+ CServiceBroker::GetPlaylistPlayer().Clear();
+
+ if (m_ServiceManager)
+ m_ServiceManager->DeinitStageTwo();
+
+#ifdef TARGET_POSIX
+ CXHandle::DumpObjectTracker();
+
+#ifdef HAS_DVD_DRIVE
+ CLibcdio::ReleaseInstance();
+#endif
+#endif
+#ifdef _CRTDBG_MAP_ALLOC
+ _CrtDumpMemoryLeaks();
+ while(1); // execution ends
+#endif
+
+ if (m_pGUI)
+ {
+ m_pGUI->Deinit();
+ m_pGUI.reset();
+ }
+
+ if (winSystem)
+ {
+ winSystem->DestroyWindowSystem();
+ CServiceBroker::UnregisterWinSystem();
+ winSystem = nullptr;
+ m_pWinSystem.reset();
+ }
+
+ // Cleanup was called more than once on exit during my tests
+ if (m_ServiceManager)
+ {
+ m_ServiceManager->DeinitStageOne();
+ m_ServiceManager.reset();
+ }
+
+ CServiceBroker::UnregisterKeyboardLayoutManager();
+
+ CServiceBroker::UnregisterAppMessenger();
+
+ CServiceBroker::UnregisterAnnouncementManager();
+ m_pAnnouncementManager->Deinitialize();
+ m_pAnnouncementManager.reset();
+
+ CServiceBroker::UnregisterJobManager();
+ CServiceBroker::UnregisterCPUInfo();
+
+ UnregisterSettings();
+
+ m_bInitializing = true;
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CApplication::Cleanup()");
+ return false;
+ }
+}
+
+bool CApplication::Stop(int exitCode)
+{
+#if defined(TARGET_ANDROID)
+ // Note: On Android, the app must be stopped asynchronously, once Android has
+ // signalled that the app shall be destroyed. See android_main() implementation.
+ if (!CXBMCApp::Get().Stop(exitCode))
+ return false;
+#endif
+
+ CLog::Log(LOGINFO, "Stopping the application...");
+
+ bool success = true;
+
+ CLog::Log(LOGINFO, "Stopping player");
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ appPlayer->ClosePlayer();
+
+ {
+ // close inbound port
+ CServiceBroker::UnregisterAppPort();
+ XbmcThreads::EndTime<> timer(1000ms);
+ while (m_pAppPort.use_count() > 1)
+ {
+ KODI::TIME::Sleep(100ms);
+ if (timer.IsTimePast())
+ {
+ CLog::Log(LOGERROR, "CApplication::Stop - CAppPort still in use, app may crash");
+ break;
+ }
+ }
+ m_pAppPort.reset();
+ }
+
+ try
+ {
+ m_frameMoveGuard.unlock();
+
+ CVariant vExitCode(CVariant::VariantTypeObject);
+ vExitCode["exitcode"] = exitCode;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::System, "OnQuit", vExitCode);
+
+ // Abort any active screensaver
+ GetComponent<CApplicationPowerHandling>()->WakeUpScreenSaverAndDPMS();
+
+ g_alarmClock.StopThread();
+
+ CLog::Log(LOGINFO, "Storing total System Uptime");
+ g_sysinfo.SetTotalUptime(g_sysinfo.GetTotalUptime() + (int)(CTimeUtils::GetFrameTime() / 60000));
+
+ // Update the settings information (volume, uptime etc. need saving)
+ if (CFile::Exists(CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetSettingsFile()))
+ {
+ CLog::Log(LOGINFO, "Saving settings");
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ }
+ else
+ CLog::Log(LOGINFO, "Not saving settings (settings.xml is not present)");
+
+ // kodi may crash or deadlock during exit (shutdown / reboot) due to
+ // either a bug in core or misbehaving addons. so try saving
+ // skin settings early
+ CLog::Log(LOGINFO, "Saving skin settings");
+ if (g_SkinInfo != nullptr)
+ g_SkinInfo->SaveSettings();
+
+ m_bStop = true;
+ // Add this here to keep the same ordering behaviour for now
+ // Needs cleaning up
+ CServiceBroker::GetAppMessenger()->Stop();
+ m_AppFocused = false;
+ m_ExitCode = exitCode;
+ CLog::Log(LOGINFO, "Stopping all");
+
+ // cancel any jobs from the jobmanager
+ CServiceBroker::GetJobManager()->CancelJobs();
+
+ // stop scanning before we kill the network and so on
+ if (CMusicLibraryQueue::GetInstance().IsRunning())
+ CMusicLibraryQueue::GetInstance().CancelAllJobs();
+
+ if (CVideoLibraryQueue::GetInstance().IsRunning())
+ CVideoLibraryQueue::GetInstance().CancelAllJobs();
+
+ CServiceBroker::GetAppMessenger()->Cleanup();
+
+ m_ServiceManager->GetNetwork().NetworkMessage(CNetworkBase::SERVICES_DOWN, 0);
+
+#ifdef HAS_ZEROCONF
+ if(CZeroconfBrowser::IsInstantiated())
+ {
+ CLog::Log(LOGINFO, "Stopping zeroconf browser");
+ CZeroconfBrowser::GetInstance()->Stop();
+ CZeroconfBrowser::ReleaseInstance();
+ }
+#endif
+
+ for (const auto& vfsAddon : CServiceBroker::GetVFSAddonCache().GetAddonInstances())
+ vfsAddon->DisconnectAll();
+
+#if defined(TARGET_POSIX) && defined(HAS_FILESYSTEM_SMB)
+ smb.Deinit();
+#endif
+
+#if defined(TARGET_DARWIN_OSX)
+ if (XBMCHelper::GetInstance().IsAlwaysOn() == false)
+ XBMCHelper::GetInstance().Stop();
+#endif
+
+ // Stop services before unloading Python
+ CServiceBroker::GetServiceAddons().Stop();
+
+ // Stop any other python scripts that may be looping waiting for monitor.abortRequested()
+ CScriptInvocationManager::GetInstance().StopRunningScripts();
+
+ // unregister action listeners
+ const auto appListener = GetComponent<CApplicationActionListeners>();
+ appListener->UnregisterActionListener(&GetComponent<CApplicationPlayer>()->GetSeekHandler());
+ appListener->UnregisterActionListener(&CPlayerController::GetInstance());
+
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetAudioManager().DeInitialize();
+
+ // shutdown the AudioEngine
+ CServiceBroker::UnregisterAE();
+ m_pActiveAE->Shutdown();
+ m_pActiveAE.reset();
+
+ CLog::Log(LOGINFO, "Application stopped");
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CApplication::Stop()");
+ success = false;
+ }
+
+ cleanup_emu_environ();
+
+ KODI::TIME::Sleep(200ms);
+
+ return success;
+}
+
+namespace
+{
+class CCreateAndLoadPlayList : public IRunnable
+{
+public:
+ CCreateAndLoadPlayList(CFileItem& item, std::unique_ptr<PLAYLIST::CPlayList>& playlist)
+ : m_item(item), m_playlist(playlist)
+ {
+ }
+
+ void Run() override
+ {
+ const std::unique_ptr<PLAYLIST::CPlayList> playlist(PLAYLIST::CPlayListFactory::Create(m_item));
+ if (playlist)
+ {
+ if (playlist->Load(m_item.GetPath()))
+ *m_playlist = *playlist;
+ }
+ }
+
+private:
+ CFileItem& m_item;
+ std::unique_ptr<PLAYLIST::CPlayList>& m_playlist;
+};
+} // namespace
+
+bool CApplication::PlayMedia(CFileItem& item, const std::string& player, PLAYLIST::Id playlistId)
+{
+ // if the item is a plugin we need to resolve the plugin paths
+ if (URIUtils::HasPluginPath(item) && !XFILE::CPluginDirectory::GetResolvedPluginResult(item))
+ return false;
+
+ if (item.IsSmartPlayList())
+ {
+ CFileItemList items;
+ CUtil::GetRecursiveListing(item.GetPath(), items, "", DIR_FLAG_NO_FILE_DIRS);
+ if (items.Size())
+ {
+ CSmartPlaylist smartpl;
+ //get name and type of smartplaylist, this will always succeed as GetDirectory also did this.
+ smartpl.OpenAndReadName(item.GetURL());
+ PLAYLIST::CPlayList playlist;
+ playlist.Add(items);
+ PLAYLIST::Id smartplPlaylistId = PLAYLIST::TYPE_VIDEO;
+
+ if (smartpl.GetType() == "songs" || smartpl.GetType() == "albums" ||
+ smartpl.GetType() == "artists")
+ smartplPlaylistId = PLAYLIST::TYPE_MUSIC;
+
+ return ProcessAndStartPlaylist(smartpl.GetName(), playlist, smartplPlaylistId);
+ }
+ }
+ else if (item.IsPlayList() || item.IsInternetStream())
+ {
+ // Not owner. Dialog auto-deletes itself.
+ CGUIDialogCache* dlgCache =
+ new CGUIDialogCache(5s, g_localizeStrings.Get(10214), item.GetLabel());
+
+ //is or could be a playlist
+ std::unique_ptr<PLAYLIST::CPlayList> playlist;
+ CCreateAndLoadPlayList getPlaylist(item, playlist);
+ bool cancelled = !CGUIDialogBusy::Wait(&getPlaylist, 100, true);
+
+ if (dlgCache)
+ {
+ dlgCache->Close();
+ if (dlgCache->IsCanceled())
+ cancelled = true;
+ }
+
+ if (cancelled)
+ return true;
+
+ if (playlist)
+ {
+
+ if (playlistId != PLAYLIST::TYPE_NONE)
+ {
+ int track=0;
+ if (item.HasProperty("playlist_starting_track"))
+ track = (int)item.GetProperty("playlist_starting_track").asInteger();
+ return ProcessAndStartPlaylist(item.GetPath(), *playlist, playlistId, track);
+ }
+ else
+ {
+ CLog::Log(LOGWARNING,
+ "CApplication::PlayMedia called to play a playlist {} but no idea which playlist "
+ "to use, playing first item",
+ item.GetPath());
+ if (playlist->size())
+ return PlayFile(*(*playlist)[0], "", false);
+ }
+ }
+ }
+ else if (item.IsPVR())
+ {
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayMedia(item);
+ }
+
+ CURL path(item.GetPath());
+ if (path.GetProtocol() == "game")
+ {
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(path.GetHostName(), addon, AddonType::GAMEDLL,
+ OnlyEnabled::CHOICE_YES))
+ {
+ CFileItem addonItem(addon);
+ return PlayFile(addonItem, player, false);
+ }
+ }
+
+ //nothing special just play
+ return PlayFile(item, player, false);
+}
+
+// PlayStack()
+// For playing a multi-file video. Particularly inefficient
+// on startup, as we are required to calculate the length
+// of each video, so we open + close each one in turn.
+// A faster calculation of video time would improve this
+// substantially.
+// return value: same with PlayFile()
+bool CApplication::PlayStack(CFileItem& item, bool bRestart)
+{
+ const auto stackHelper = GetComponent<CApplicationStackHelper>();
+ if (!stackHelper->InitializeStack(item))
+ return false;
+
+ int startoffset = stackHelper->InitializeStackStartPartAndOffset(item);
+
+ CFileItem selectedStackPart = stackHelper->GetCurrentStackPartFileItem();
+ selectedStackPart.SetStartOffset(startoffset);
+
+ if (item.HasProperty("savedplayerstate"))
+ {
+ selectedStackPart.SetProperty("savedplayerstate", item.GetProperty("savedplayerstate")); // pass on to part
+ item.ClearProperty("savedplayerstate");
+ }
+
+ return PlayFile(selectedStackPart, "", true);
+}
+
+bool CApplication::PlayFile(CFileItem item, const std::string& player, bool bRestart)
+{
+ // Ensure the MIME type has been retrieved for http:// and shout:// streams
+ if (item.GetMimeType().empty())
+ item.FillInMimeType();
+
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ const auto stackHelper = GetComponent<CApplicationStackHelper>();
+
+ if (!bRestart)
+ {
+ // bRestart will be true when called from PlayStack(), skipping this block
+ appPlayer->SetPlaySpeed(1);
+
+ m_nextPlaylistItem = -1;
+ stackHelper->Clear();
+
+ if (item.IsVideo())
+ CUtil::ClearSubtitles();
+ }
+
+ if (item.IsDiscStub())
+ {
+ return CServiceBroker::GetMediaManager().playStubFile(item);
+ }
+
+ if (item.IsPlayList())
+ return false;
+
+ // if the item is a plugin we need to resolve the plugin paths
+ if (URIUtils::HasPluginPath(item) && !XFILE::CPluginDirectory::GetResolvedPluginResult(item))
+ return false;
+
+#ifdef HAS_UPNP
+ if (URIUtils::IsUPnP(item.GetPath()))
+ {
+ if (!XFILE::CUPnPDirectory::GetResource(item.GetURL(), item))
+ return false;
+ }
+#endif
+
+ // if we have a stacked set of files, we need to setup our stack routines for
+ // "seamless" seeking and total time of the movie etc.
+ // will recall with restart set to true
+ if (item.IsStack())
+ return PlayStack(item, bRestart);
+
+ CPlayerOptions options;
+
+ if (item.HasProperty("StartPercent"))
+ {
+ options.startpercent = item.GetProperty("StartPercent").asDouble();
+ item.SetStartOffset(0);
+ }
+
+ options.starttime = CUtil::ConvertMilliSecsToSecs(item.GetStartOffset());
+
+ if (bRestart)
+ {
+ // have to be set here due to playstack using this for starting the file
+ if (item.HasVideoInfoTag())
+ options.state = item.GetVideoInfoTag()->GetResumePoint().playerState;
+ }
+ if (!bRestart || stackHelper->IsPlayingISOStack())
+ {
+ // the following code block is only applicable when bRestart is false OR to ISO stacks
+
+ if (item.IsVideo())
+ {
+ // open the d/b and retrieve the bookmarks for the current movie
+ CVideoDatabase dbs;
+ dbs.Open();
+
+ std::string path = item.GetPath();
+ std::string videoInfoTagPath(item.GetVideoInfoTag()->m_strFileNameAndPath);
+ if (videoInfoTagPath.find("removable://") == 0 || item.IsVideoDb())
+ path = videoInfoTagPath;
+ dbs.LoadVideoInfo(path, *item.GetVideoInfoTag());
+
+ if (item.HasProperty("savedplayerstate"))
+ {
+ options.starttime = CUtil::ConvertMilliSecsToSecs(item.GetStartOffset());
+ options.state = item.GetProperty("savedplayerstate").asString();
+ item.ClearProperty("savedplayerstate");
+ }
+ else if (item.GetStartOffset() == STARTOFFSET_RESUME)
+ {
+ options.starttime = 0.0;
+ if (item.IsResumePointSet())
+ {
+ options.starttime = item.GetCurrentResumeTime();
+ if (item.HasVideoInfoTag())
+ options.state = item.GetVideoInfoTag()->GetResumePoint().playerState;
+ }
+ else
+ {
+ CBookmark bookmark;
+ std::string path = item.GetPath();
+ if (item.HasVideoInfoTag() && StringUtils::StartsWith(item.GetVideoInfoTag()->m_strFileNameAndPath, "removable://"))
+ path = item.GetVideoInfoTag()->m_strFileNameAndPath;
+ else if (item.HasProperty("original_listitem_url") && URIUtils::IsPlugin(item.GetProperty("original_listitem_url").asString()))
+ path = item.GetProperty("original_listitem_url").asString();
+ if (dbs.GetResumeBookMark(path, bookmark))
+ {
+ options.starttime = bookmark.timeInSeconds;
+ options.state = bookmark.playerState;
+ }
+ }
+
+ if (options.starttime == 0.0 && item.HasVideoInfoTag())
+ {
+ // No resume point is set, but check if this item is part of a multi-episode file
+ const CVideoInfoTag *tag = item.GetVideoInfoTag();
+
+ if (tag->m_iBookmarkId > 0)
+ {
+ CBookmark bookmark;
+ dbs.GetBookMarkForEpisode(*tag, bookmark);
+ options.starttime = bookmark.timeInSeconds;
+ options.state = bookmark.playerState;
+ }
+ }
+ }
+ else if (item.HasVideoInfoTag())
+ {
+ const CVideoInfoTag *tag = item.GetVideoInfoTag();
+
+ if (tag->m_iBookmarkId > 0)
+ {
+ CBookmark bookmark;
+ dbs.GetBookMarkForEpisode(*tag, bookmark);
+ options.starttime = bookmark.timeInSeconds;
+ options.state = bookmark.playerState;
+ }
+ }
+
+ dbs.Close();
+ }
+ }
+
+ // a disc image might be Blu-Ray disc
+ if (!(options.startpercent > 0.0 || options.starttime > 0.0) &&
+ (item.IsBDFile() || item.IsDiscImage()))
+ {
+ //check if we must show the simplified bd menu
+ if (!CGUIDialogSimpleMenu::ShowPlaySelection(item))
+ return true;
+ }
+
+ // this really aught to be inside !bRestart, but since PlayStack
+ // uses that to init playback, we have to keep it outside
+ const PLAYLIST::Id playlistId = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+ if (item.IsAudio() && playlistId == PLAYLIST::TYPE_MUSIC)
+ { // playing from a playlist by the looks
+ // don't switch to fullscreen if we are not playing the first item...
+ options.fullscreen = !CServiceBroker::GetPlaylistPlayer().HasPlayedFirstFile() &&
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICFILES_SELECTACTION) &&
+ !CMediaSettings::GetInstance().DoesMediaStartWindowed();
+ }
+ else if (item.IsVideo() && playlistId == PLAYLIST::TYPE_VIDEO &&
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId).size() > 1)
+ { // playing from a playlist by the looks
+ // don't switch to fullscreen if we are not playing the first item...
+ options.fullscreen = !CServiceBroker::GetPlaylistPlayer().HasPlayedFirstFile() &&
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreenOnMovieStart &&
+ !CMediaSettings::GetInstance().DoesMediaStartWindowed();
+ }
+ else if (stackHelper->IsPlayingRegularStack())
+ {
+ //! @todo - this will fail if user seeks back to first file in stack
+ if (stackHelper->GetCurrentPartNumber() == 0 ||
+ stackHelper->GetRegisteredStack(item)->GetStartOffset() != 0)
+ options.fullscreen = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->
+ m_fullScreenOnMovieStart && !CMediaSettings::GetInstance().DoesMediaStartWindowed();
+ else
+ options.fullscreen = false;
+ }
+ else
+ options.fullscreen = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->
+ m_fullScreenOnMovieStart && !CMediaSettings::GetInstance().DoesMediaStartWindowed();
+
+ // stereo streams may have lower quality, i.e. 32bit vs 16 bit
+ options.preferStereo = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoPreferStereoStream &&
+ CServiceBroker::GetActiveAE()->HasStereoAudioChannelCount();
+
+ // reset VideoStartWindowed as it's a temp setting
+ CMediaSettings::GetInstance().SetMediaStartWindowed(false);
+
+ {
+ // for playing a new item, previous playing item's callback may already
+ // pushed some delay message into the threadmessage list, they are not
+ // expected be processed after or during the new item playback starting.
+ // so we clean up previous playing item's playback callback delay messages here.
+ int previousMsgsIgnoredByNewPlaying[] = {
+ GUI_MSG_PLAYBACK_STARTED,
+ GUI_MSG_PLAYBACK_ENDED,
+ GUI_MSG_PLAYBACK_STOPPED,
+ GUI_MSG_PLAYLIST_CHANGED,
+ GUI_MSG_PLAYLISTPLAYER_STOPPED,
+ GUI_MSG_PLAYLISTPLAYER_STARTED,
+ GUI_MSG_PLAYLISTPLAYER_CHANGED,
+ GUI_MSG_QUEUE_NEXT_ITEM,
+ 0
+ };
+ int dMsgCount = CServiceBroker::GetGUI()->GetWindowManager().RemoveThreadMessageByMessageIds(&previousMsgsIgnoredByNewPlaying[0]);
+ if (dMsgCount > 0)
+ CLog::LogF(LOGDEBUG, "Ignored {} playback thread messages", dMsgCount);
+ }
+
+ const auto appVolume = GetComponent<CApplicationVolumeHandling>();
+ appPlayer->OpenFile(item, options, m_ServiceManager->GetPlayerCoreFactory(), player, *this);
+ appPlayer->SetVolume(appVolume->GetVolumeRatio());
+ appPlayer->SetMute(appVolume->IsMuted());
+
+#if !defined(TARGET_POSIX)
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetAudioManager().Enable(false);
+#endif
+
+ if (item.HasPVRChannelInfoTag())
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE);
+
+ return true;
+}
+
+void CApplication::PlaybackCleanup()
+{
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ const auto stackHelper = GetComponent<CApplicationStackHelper>();
+
+ if (!appPlayer->IsPlaying())
+ {
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui)
+ CServiceBroker::GetGUI()->GetAudioManager().Enable(true);
+ appPlayer->OpenNext(m_ServiceManager->GetPlayerCoreFactory());
+ }
+
+ if (!appPlayer->IsPlayingVideo())
+ {
+ if(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_VIDEO ||
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_GAME)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ }
+ else
+ {
+ // resets to res_desktop or look&feel resolution (including refreshrate)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetFullScreenVideo(false);
+ }
+#ifdef TARGET_DARWIN_EMBEDDED
+ CDarwinUtils::SetScheduling(false);
+#endif
+ }
+
+ const auto appPower = GetComponent<CApplicationPowerHandling>();
+
+ if (!appPlayer->IsPlayingAudio() &&
+ CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_NONE &&
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VISUALISATION)
+ {
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); // save vis settings
+ appPower->WakeUpScreenSaverAndDPMS();
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ }
+
+ // DVD ejected while playing in vis ?
+ if (!appPlayer->IsPlayingAudio() &&
+ (m_itemCurrentFile->IsCDDA() || m_itemCurrentFile->IsOnDVD()) &&
+ !CServiceBroker::GetMediaManager().IsDiscInDrive() &&
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VISUALISATION)
+ {
+ // yes, disable vis
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save(); // save vis settings
+ appPower->WakeUpScreenSaverAndDPMS();
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ }
+
+ if (!appPlayer->IsPlaying())
+ {
+ stackHelper->Clear();
+ appPlayer->ResetPlayer();
+ }
+
+ if (CServiceBroker::GetAppParams()->IsTestMode())
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+}
+
+bool CApplication::IsPlayingFullScreenVideo() const
+{
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ return appPlayer->IsPlayingVideo() &&
+ CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenVideo();
+}
+
+bool CApplication::IsFullScreen()
+{
+ return IsPlayingFullScreenVideo() ||
+ (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VISUALISATION) ||
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW;
+}
+
+void CApplication::StopPlaying()
+{
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+
+ if (gui)
+ {
+ int iWin = gui->GetWindowManager().GetActiveWindow();
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ {
+ appPlayer->ClosePlayer();
+
+ // turn off visualisation window when stopping
+ if ((iWin == WINDOW_VISUALISATION ||
+ iWin == WINDOW_FULLSCREEN_VIDEO ||
+ iWin == WINDOW_FULLSCREEN_GAME) &&
+ !m_bStop)
+ gui->GetWindowManager().PreviousWindow();
+
+ g_partyModeManager.Disable();
+ }
+ }
+}
+
+bool CApplication::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (message.GetParam1()==GUI_MSG_REMOVED_MEDIA)
+ {
+ // Update general playlist: Remove DVD playlist items
+ int nRemoved = CServiceBroker::GetPlaylistPlayer().RemoveDVDItems();
+ if ( nRemoved > 0 )
+ {
+ CGUIMessage msg( GUI_MSG_PLAYLIST_CHANGED, 0, 0 );
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage( msg );
+ }
+ // stop the file if it's on dvd (will set the resume point etc)
+ if (m_itemCurrentFile->IsOnDVD())
+ StopPlaying();
+ }
+ else if (message.GetParam1() == GUI_MSG_UI_READY)
+ {
+ // remove splash window
+ CServiceBroker::GetGUI()->GetWindowManager().Delete(WINDOW_SPLASH);
+
+ // show the volumebar if the volume is muted
+ const auto appVolume = GetComponent<CApplicationVolumeHandling>();
+ if (appVolume->IsMuted() ||
+ appVolume->GetVolumeRatio() <= CApplicationVolumeHandling::VOLUME_MINIMUM)
+ appVolume->ShowVolumeBar();
+
+ if (!m_incompatibleAddons.empty())
+ {
+ // filter addons that are not dependencies
+ std::vector<std::string> disabledAddonNames;
+ for (const auto& addoninfo : m_incompatibleAddons)
+ {
+ if (!CAddonType::IsDependencyType(addoninfo->MainType()))
+ disabledAddonNames.emplace_back(addoninfo->Name());
+ }
+
+ // migration (incompatible addons) dialog
+ auto addonList = StringUtils::Join(disabledAddonNames, ", ");
+ auto msg = StringUtils::Format(g_localizeStrings.Get(24149), addonList);
+ HELPERS::ShowOKDialogText(CVariant{24148}, CVariant{std::move(msg)});
+ m_incompatibleAddons.clear();
+ }
+
+ // show info dialog about moved configuration files if needed
+ ShowAppMigrationMessage();
+
+ // offer enabling addons at kodi startup that are disabled due to
+ // e.g. os package manager installation on linux
+ ConfigureAndEnableAddons();
+
+ m_bInitializing = false;
+
+ if (message.GetSenderId() == WINDOW_SETTINGS_PROFILES)
+ GetComponent<CApplicationSkinHandling>()->ReloadSkin(false);
+ }
+ else if (message.GetParam1() == GUI_MSG_UPDATE_ITEM && message.GetItem())
+ {
+ CFileItemPtr item = std::static_pointer_cast<CFileItem>(message.GetItem());
+ if (m_itemCurrentFile->IsSamePath(item.get()))
+ {
+ m_itemCurrentFile->UpdateInfo(*item);
+ CServiceBroker::GetGUI()->GetInfoManager().UpdateCurrentItem(*item);
+ }
+ }
+ }
+ break;
+
+ case GUI_MSG_PLAYBACK_STARTED:
+ {
+#ifdef TARGET_DARWIN_EMBEDDED
+ // @TODO move this away to platform code
+ CDarwinUtils::SetScheduling(GetComponent<CApplicationPlayer>()->IsPlayingVideo());
+#endif
+ PLAYLIST::CPlayList playList = CServiceBroker::GetPlaylistPlayer().GetPlaylist(
+ CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist());
+
+ // Update our infoManager with the new details etc.
+ if (m_nextPlaylistItem >= 0)
+ {
+ // playing an item which is not in the list - player might be stopped already
+ // so do nothing
+ if (playList.size() <= m_nextPlaylistItem)
+ return true;
+
+ // we've started a previously queued item
+ CFileItemPtr item = playList[m_nextPlaylistItem];
+ // update the playlist manager
+ int currentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ int param = ((currentSong & 0xffff) << 16) | (m_nextPlaylistItem & 0xffff);
+ CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_CHANGED, 0, 0, CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist(), param, item);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(m_nextPlaylistItem);
+ m_itemCurrentFile.reset(new CFileItem(*item));
+ }
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*m_itemCurrentFile);
+ g_partyModeManager.OnSongChange(true);
+
+#ifdef HAS_PYTHON
+ // informs python script currently running playback has started
+ // (does nothing if python is not loaded)
+ CServiceBroker::GetXBPython().OnPlayBackStarted(*m_itemCurrentFile);
+#endif
+
+ CVariant param;
+ param["player"]["speed"] = 1;
+ param["player"]["playerid"] = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPlay",
+ m_itemCurrentFile, param);
+
+ // we don't want a busy dialog when switching channels
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ if (!m_itemCurrentFile->IsLiveTV() ||
+ (!appPlayer->IsPlayingVideo() && !appPlayer->IsPlayingAudio()))
+ {
+ CGUIDialogBusy* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogBusy>(
+ WINDOW_DIALOG_BUSY);
+ if (dialog && !dialog->IsDialogRunning())
+ dialog->WaitOnEvent(m_playerEvent);
+ }
+
+ return true;
+ }
+ break;
+
+ case GUI_MSG_QUEUE_NEXT_ITEM:
+ {
+ // Check to see if our playlist player has a new item for us,
+ // and if so, we check whether our current player wants the file
+ int iNext = CServiceBroker::GetPlaylistPlayer().GetNextSong();
+ PLAYLIST::CPlayList& playlist = CServiceBroker::GetPlaylistPlayer().GetPlaylist(
+ CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist());
+ if (iNext < 0 || iNext >= playlist.size())
+ {
+ GetComponent<CApplicationPlayer>()->OnNothingToQueueNotify();
+ return true; // nothing to do
+ }
+
+ // ok, grab the next song
+ CFileItem file(*playlist[iNext]);
+ // handle plugin://
+ CURL url(file.GetDynPath());
+ if (url.IsProtocol("plugin"))
+ XFILE::CPluginDirectory::GetPluginResult(url.Get(), file, false);
+
+ // Don't queue if next media type is different from current one
+ bool bNothingToQueue = false;
+
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ if (!file.IsVideo() && appPlayer->IsPlayingVideo())
+ bNothingToQueue = true;
+ else if ((!file.IsAudio() || file.IsVideo()) && appPlayer->IsPlayingAudio())
+ bNothingToQueue = true;
+
+ if (bNothingToQueue)
+ {
+ appPlayer->OnNothingToQueueNotify();
+ return true;
+ }
+
+#ifdef HAS_UPNP
+ if (URIUtils::IsUPnP(file.GetDynPath()))
+ {
+ if (!XFILE::CUPnPDirectory::GetResource(file.GetDynURL(), file))
+ return true;
+ }
+#endif
+
+ // ok - send the file to the player, if it accepts it
+ if (appPlayer->QueueNextFile(file))
+ {
+ // player accepted the next file
+ m_nextPlaylistItem = iNext;
+ }
+ else
+ {
+ /* Player didn't accept next file: *ALWAYS* advance playlist in this case so the player can
+ queue the next (if it wants to) and it doesn't keep looping on this song */
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(iNext);
+ }
+
+ return true;
+ }
+ break;
+
+ case GUI_MSG_PLAY_TRAILER:
+ {
+ const CFileItem* item = dynamic_cast<CFileItem*>(message.GetItem().get());
+ if (item == nullptr)
+ {
+ CLog::LogF(LOGERROR, "Supplied item is not a CFileItem! Trailer cannot be played.");
+ return false;
+ }
+
+ std::unique_ptr<CFileItem> trailerItem =
+ ContentUtils::GeneratePlayableTrailerItem(*item, g_localizeStrings.Get(20410));
+
+ if (item->IsPlayList())
+ {
+ std::unique_ptr<CFileItemList> fileitemList = std::make_unique<CFileItemList>();
+ fileitemList->Add(std::move(trailerItem));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, -1, -1,
+ static_cast<void*>(fileitemList.release()));
+ }
+ else
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 1, 0,
+ static_cast<void*>(trailerItem.release()));
+ }
+ break;
+ }
+
+ case GUI_MSG_PLAYBACK_STOPPED:
+ m_playerEvent.Set();
+ ResetCurrentItem();
+ PlaybackCleanup();
+#ifdef HAS_PYTHON
+ CServiceBroker::GetXBPython().OnPlayBackStopped();
+#endif
+ return true;
+
+ case GUI_MSG_PLAYBACK_ENDED:
+ {
+ m_playerEvent.Set();
+ const auto stackHelper = GetComponent<CApplicationStackHelper>();
+ if (stackHelper->IsPlayingRegularStack() && stackHelper->HasNextStackPartFileItem())
+ { // just play the next item in the stack
+ PlayFile(stackHelper->SetNextStackPartCurrentFileItem(), "", true);
+ return true;
+ }
+ ResetCurrentItem();
+ if (!CServiceBroker::GetPlaylistPlayer().PlayNext(1, true))
+ GetComponent<CApplicationPlayer>()->ClosePlayer();
+
+ PlaybackCleanup();
+
+#ifdef HAS_PYTHON
+ CServiceBroker::GetXBPython().OnPlayBackEnded();
+#endif
+ return true;
+ }
+
+ case GUI_MSG_PLAYLISTPLAYER_STOPPED:
+ ResetCurrentItem();
+ if (GetComponent<CApplicationPlayer>()->IsPlaying())
+ StopPlaying();
+ PlaybackCleanup();
+ return true;
+
+ case GUI_MSG_PLAYBACK_AVSTARTED:
+ m_playerEvent.Set();
+#ifdef HAS_PYTHON
+ // informs python script currently running playback has started
+ // (does nothing if python is not loaded)
+ CServiceBroker::GetXBPython().OnAVStarted(*m_itemCurrentFile);
+#endif
+ return true;
+
+ case GUI_MSG_PLAYBACK_AVCHANGE:
+#ifdef HAS_PYTHON
+ // informs python script currently running playback has started
+ // (does nothing if python is not loaded)
+ CServiceBroker::GetXBPython().OnAVChange();
+#endif
+ return true;
+
+ case GUI_MSG_PLAYBACK_ERROR:
+ HELPERS::ShowOKDialogText(CVariant{16026}, CVariant{16027});
+ return true;
+
+ case GUI_MSG_PLAYLISTPLAYER_STARTED:
+ case GUI_MSG_PLAYLISTPLAYER_CHANGED:
+ {
+ return true;
+ }
+ break;
+ case GUI_MSG_FULLSCREEN:
+ { // Switch to fullscreen, if we can
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetWindowManager().SwitchToFullScreen();
+
+ return true;
+ }
+ break;
+ case GUI_MSG_EXECUTE:
+ if (message.GetNumStringParams())
+ return ExecuteXBMCAction(message.GetStringParam(), message.GetItem());
+ break;
+ }
+ return false;
+}
+
+bool CApplication::ExecuteXBMCAction(std::string actionStr, const CGUIListItemPtr &item /* = NULL */)
+{
+ // see if it is a user set string
+
+ //We don't know if there is unsecure information in this yet, so we
+ //postpone any logging
+ const std::string in_actionStr(actionStr);
+ if (item)
+ actionStr = GUILIB::GUIINFO::CGUIInfoLabel::GetItemLabel(actionStr, item.get());
+ else
+ actionStr = GUILIB::GUIINFO::CGUIInfoLabel::GetLabel(actionStr, INFO::DEFAULT_CONTEXT);
+
+ // user has asked for something to be executed
+ if (CBuiltins::GetInstance().HasCommand(actionStr))
+ {
+ if (!CBuiltins::GetInstance().IsSystemPowerdownCommand(actionStr) ||
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::PowerManagement>().CanSystemPowerdown())
+ CBuiltins::GetInstance().Execute(actionStr);
+ }
+ else
+ {
+ // try translating the action from our ButtonTranslator
+ unsigned int actionID;
+ if (CActionTranslator::TranslateString(actionStr, actionID))
+ {
+ OnAction(CAction(actionID));
+ return true;
+ }
+ CFileItem item(actionStr, false);
+#ifdef HAS_PYTHON
+ if (item.IsPythonScript())
+ { // a python script
+ CScriptInvocationManager::GetInstance().ExecuteAsync(item.GetPath());
+ }
+ else
+#endif
+ if (item.IsAudio() || item.IsVideo() || item.IsGame())
+ { // an audio or video file
+ PlayFile(item, "");
+ }
+ else
+ {
+ //At this point we have given up to translate, so even though
+ //there may be insecure information, we log it.
+ CLog::LogF(LOGDEBUG, "Tried translating, but failed to understand {}", in_actionStr);
+ return false;
+ }
+ }
+ return true;
+}
+
+// inform the user that the configuration data has moved from old XBMC location
+// to new Kodi location - if applicable
+void CApplication::ShowAppMigrationMessage()
+{
+ // .kodi_migration_complete will be created from the installer/packaging
+ // once an old XBMC configuration was moved to the new Kodi location
+ // if this is the case show the migration info to the user once which
+ // tells him to have a look into the wiki where the move of configuration
+ // is further explained.
+ if (CFile::Exists("special://home/.kodi_data_was_migrated") &&
+ !CFile::Exists("special://home/.kodi_migration_info_shown"))
+ {
+ HELPERS::ShowOKDialogText(CVariant{24128}, CVariant{24129});
+ CFile tmpFile;
+ // create the file which will prevent this dialog from appearing in the future
+ tmpFile.OpenForWrite("special://home/.kodi_migration_info_shown");
+ tmpFile.Close();
+ }
+}
+
+void CApplication::ConfigureAndEnableAddons()
+{
+ std::vector<std::shared_ptr<IAddon>>
+ disabledAddons; /*!< Installed addons, but not auto-enabled via manifest */
+
+ auto& addonMgr = CServiceBroker::GetAddonMgr();
+
+ if (addonMgr.GetDisabledAddons(disabledAddons) && !disabledAddons.empty())
+ {
+ // this applies to certain platforms only:
+ // look at disabled addons with disabledReason == NONE, usually those are installed via package managers or manually.
+ // also try to enable add-ons with disabledReason == INCOMPATIBLE at startup for all platforms.
+
+ bool isConfigureAddonsAtStartupEnabled =
+ m_ServiceManager->GetPlatform().IsConfigureAddonsAtStartupEnabled();
+
+ for (const auto& addon : disabledAddons)
+ {
+ if (addonMgr.IsAddonDisabledWithReason(addon->ID(), ADDON::AddonDisabledReason::INCOMPATIBLE))
+ {
+ auto addonInfo = addonMgr.GetAddonInfo(addon->ID(), AddonType::UNKNOWN);
+ if (addonInfo && addonMgr.IsCompatible(addonInfo))
+ {
+ CLog::Log(LOGDEBUG, "CApplication::{}: enabling the compatible version of [{}].",
+ __FUNCTION__, addon->ID());
+ addonMgr.EnableAddon(addon->ID());
+ }
+ continue;
+ }
+
+ if (addonMgr.IsAddonDisabledExcept(addon->ID(), ADDON::AddonDisabledReason::NONE) ||
+ CAddonType::IsDependencyType(addon->MainType()))
+ {
+ continue;
+ }
+
+ if (isConfigureAddonsAtStartupEnabled)
+ {
+ if (HELPERS::ShowYesNoDialogLines(CVariant{24039}, // Disabled add-ons
+ CVariant{24059}, // Would you like to enable this add-on?
+ CVariant{addon->Name()}) == DialogResponse::CHOICE_YES)
+ {
+ if (addon->CanHaveAddonOrInstanceSettings())
+ {
+ if (CGUIDialogAddonSettings::ShowForAddon(addon))
+ {
+ // only enable if settings dialog hasn't been cancelled
+ addonMgr.EnableAddon(addon->ID());
+ }
+ }
+ else
+ {
+ addonMgr.EnableAddon(addon->ID());
+ }
+ }
+ else
+ {
+ // user chose not to configure/enable so we're not asking anymore
+ addonMgr.UpdateDisabledReason(addon->ID(), ADDON::AddonDisabledReason::USER);
+ }
+ }
+ }
+ }
+}
+
+void CApplication::Process()
+{
+ // dispatch the messages generated by python or other threads to the current window
+ CServiceBroker::GetGUI()->GetWindowManager().DispatchThreadMessages();
+
+ // process messages which have to be send to the gui
+ // (this can only be done after CServiceBroker::GetGUI()->GetWindowManager().Render())
+ CServiceBroker::GetAppMessenger()->ProcessWindowMessages();
+
+ // handle any active scripts
+
+ {
+ // Allow processing of script threads to let them shut down properly.
+ CSingleExit ex(CServiceBroker::GetWinSystem()->GetGfxContext());
+ m_frameMoveGuard.unlock();
+ CScriptInvocationManager::GetInstance().Process();
+ m_frameMoveGuard.lock();
+ }
+
+ // process messages, even if a movie is playing
+ CServiceBroker::GetAppMessenger()->ProcessMessages();
+ if (m_bStop) return; //we're done, everything has been unloaded
+
+ // update sound
+ GetComponent<CApplicationPlayer>()->DoAudioWork();
+
+ // do any processing that isn't needed on each run
+ if( m_slowTimer.GetElapsedMilliseconds() > 500 )
+ {
+ m_slowTimer.Reset();
+ ProcessSlow();
+ }
+}
+
+// We get called every 500ms
+void CApplication::ProcessSlow()
+{
+ // process skin resources (skin timers)
+ GetComponent<CApplicationSkinHandling>()->ProcessSkin();
+
+ CServiceBroker::GetPowerManager().ProcessEvents();
+
+#if defined(TARGET_DARWIN_OSX) && defined(SDL_FOUND)
+ // There is an issue on OS X that several system services ask the cursor to become visible
+ // during their startup routines. Given that we can't control this, we hack it in by
+ // forcing the
+ if (CServiceBroker::GetWinSystem()->IsFullScreen())
+ { // SDL thinks it's hidden
+ Cocoa_HideMouse();
+ }
+#endif
+
+ // Temporarily pause pausable jobs when viewing video/picture
+ int currentWindow = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ if (CurrentFileItem().IsVideo() ||
+ CurrentFileItem().IsPicture() ||
+ currentWindow == WINDOW_FULLSCREEN_VIDEO ||
+ currentWindow == WINDOW_FULLSCREEN_GAME ||
+ currentWindow == WINDOW_SLIDESHOW)
+ {
+ CServiceBroker::GetJobManager()->PauseJobs();
+ }
+ else
+ {
+ CServiceBroker::GetJobManager()->UnPauseJobs();
+ }
+
+ // Check if we need to activate the screensaver / DPMS.
+ const auto appPower = GetComponent<CApplicationPowerHandling>();
+ appPower->CheckScreenSaverAndDPMS();
+
+ // Check if we need to shutdown (if enabled).
+#if defined(TARGET_DARWIN)
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNTIME) &&
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen)
+#else
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNTIME))
+#endif
+ {
+ appPower->CheckShutdown();
+ }
+
+#if defined(TARGET_POSIX)
+ if (CPlatformPosix::TestQuitFlag())
+ {
+ CLog::Log(LOGINFO, "Quitting due to POSIX signal");
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+ }
+#endif
+
+ // check if we should restart the player
+ CheckDelayedPlayerRestart();
+
+ // check if we can unload any unreferenced dlls or sections
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlayingVideo())
+ CSectionLoader::UnloadDelayed();
+
+#ifdef TARGET_ANDROID
+ // Pass the slow loop to droid
+ CXBMCApp::Get().ProcessSlow();
+#endif
+
+ // check for any idle curl connections
+ g_curlInterface.CheckIdle();
+
+ CServiceBroker::GetGUI()->GetLargeTextureManager().CleanupUnusedImages();
+
+ CServiceBroker::GetGUI()->GetTextureManager().FreeUnusedTextures(5000);
+
+#ifdef HAS_DVD_DRIVE
+ // checks whats in the DVD drive and tries to autostart the content (xbox games, dvd, cdda, avi files...)
+ if (!appPlayer->IsPlayingVideo())
+ m_Autorun->HandleAutorun();
+#endif
+
+ // update upnp server/renderer states
+#ifdef HAS_UPNP
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_UPNP) && UPNP::CUPnP::IsInstantiated())
+ UPNP::CUPnP::GetInstance()->UpdateState();
+#endif
+
+#if defined(TARGET_POSIX) && defined(HAS_FILESYSTEM_SMB)
+ smb.CheckIfIdle();
+#endif
+
+#ifdef HAS_FILESYSTEM_NFS
+ gNfsConnection.CheckIfIdle();
+#endif
+
+ for (const auto& vfsAddon : CServiceBroker::GetVFSAddonCache().GetAddonInstances())
+ vfsAddon->ClearOutIdle();
+
+ CServiceBroker::GetMediaManager().ProcessEvents();
+
+ // if we don't render the gui there's no reason to start the screensaver.
+ // that way the screensaver won't kick in if we maximize the XBMC window
+ // after the screensaver start time.
+ if (!appPower->GetRenderGUI())
+ appPower->ResetScreenSaverTimer();
+}
+
+void CApplication::DelayedPlayerRestart()
+{
+ m_restartPlayerTimer.StartZero();
+}
+
+void CApplication::CheckDelayedPlayerRestart()
+{
+ if (m_restartPlayerTimer.GetElapsedSeconds() > 3)
+ {
+ m_restartPlayerTimer.Stop();
+ m_restartPlayerTimer.Reset();
+ Restart(true);
+ }
+}
+
+void CApplication::Restart(bool bSamePosition)
+{
+ // this function gets called when the user changes a setting (like noninterleaved)
+ // and which means we gotta close & reopen the current playing file
+
+ // first check if we're playing a file
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlayingVideo() && !appPlayer->IsPlayingAudio())
+ return ;
+
+ if (!appPlayer->HasPlayer())
+ return ;
+
+ // do we want to return to the current position in the file
+ if (!bSamePosition)
+ {
+ // no, then just reopen the file and start at the beginning
+ PlayFile(*m_itemCurrentFile, "", true);
+ return ;
+ }
+
+ // else get current position
+ double time = GetTime();
+
+ // get player state, needed for dvd's
+ std::string state = appPlayer->GetPlayerState();
+
+ // set the requested starttime
+ m_itemCurrentFile->SetStartOffset(CUtil::ConvertSecsToMilliSecs(time));
+
+ // reopen the file
+ if (PlayFile(*m_itemCurrentFile, "", true))
+ appPlayer->SetPlayerState(state);
+}
+
+const std::string& CApplication::CurrentFile()
+{
+ return m_itemCurrentFile->GetPath();
+}
+
+std::shared_ptr<CFileItem> CApplication::CurrentFileItemPtr()
+{
+ return m_itemCurrentFile;
+}
+
+CFileItem& CApplication::CurrentFileItem()
+{
+ return *m_itemCurrentFile;
+}
+
+const CFileItem& CApplication::CurrentUnstackedItem()
+{
+ const auto stackHelper = GetComponent<CApplicationStackHelper>();
+
+ if (stackHelper->IsPlayingISOStack() || stackHelper->IsPlayingRegularStack())
+ return stackHelper->GetCurrentStackPartFileItem();
+ else
+ return *m_itemCurrentFile;
+}
+
+// Returns the total time in seconds of the current media. Fractional
+// portions of a second are possible - but not necessarily supported by the
+// player class. This returns a double to be consistent with GetTime() and
+// SeekTime().
+double CApplication::GetTotalTime() const
+{
+ double rc = 0.0;
+
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ const auto stackHelper = GetComponent<CApplicationStackHelper>();
+
+ if (appPlayer->IsPlaying())
+ {
+ if (stackHelper->IsPlayingRegularStack())
+ rc = stackHelper->GetStackTotalTimeMs() * 0.001;
+ else
+ rc = appPlayer->GetTotalTime() * 0.001;
+ }
+
+ return rc;
+}
+
+// Returns the current time in seconds of the currently playing media.
+// Fractional portions of a second are possible. This returns a double to
+// be consistent with GetTotalTime() and SeekTime().
+double CApplication::GetTime() const
+{
+ double rc = 0.0;
+
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ const auto stackHelper = GetComponent<CApplicationStackHelper>();
+
+ if (appPlayer->IsPlaying())
+ {
+ if (stackHelper->IsPlayingRegularStack())
+ {
+ uint64_t startOfCurrentFile = stackHelper->GetCurrentStackPartStartTimeMs();
+ rc = (startOfCurrentFile + appPlayer->GetTime()) * 0.001;
+ }
+ else
+ rc = appPlayer->GetTime() * 0.001;
+ }
+
+ return rc;
+}
+
+// Sets the current position of the currently playing media to the specified
+// time in seconds. Fractional portions of a second are valid. The passed
+// time is the time offset from the beginning of the file as opposed to a
+// delta from the current position. This method accepts a double to be
+// consistent with GetTime() and GetTotalTime().
+void CApplication::SeekTime( double dTime )
+{
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ const auto stackHelper = GetComponent<CApplicationStackHelper>();
+
+ if (appPlayer->IsPlaying() && (dTime >= 0.0))
+ {
+ if (!appPlayer->CanSeek())
+ return;
+
+ if (stackHelper->IsPlayingRegularStack())
+ {
+ // find the item in the stack we are seeking to, and load the new
+ // file if necessary, and calculate the correct seek within the new
+ // file. Otherwise, just fall through to the usual routine if the
+ // time is higher than our total time.
+ int partNumberToPlay =
+ stackHelper->GetStackPartNumberAtTimeMs(static_cast<uint64_t>(dTime * 1000.0));
+ uint64_t startOfNewFile = stackHelper->GetStackPartStartTimeMs(partNumberToPlay);
+ if (partNumberToPlay == stackHelper->GetCurrentPartNumber())
+ appPlayer->SeekTime(static_cast<uint64_t>(dTime * 1000.0) - startOfNewFile);
+ else
+ { // seeking to a new file
+ stackHelper->SetStackPartCurrentFileItem(partNumberToPlay);
+ CFileItem* item = new CFileItem(stackHelper->GetCurrentStackPartFileItem());
+ item->SetStartOffset(static_cast<uint64_t>(dTime * 1000.0) - startOfNewFile);
+ // don't just call "PlayFile" here, as we are quite likely called from the
+ // player thread, so we won't be able to delete ourselves.
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 1, 0, static_cast<void*>(item));
+ }
+ return;
+ }
+ // convert to milliseconds and perform seek
+ appPlayer->SeekTime(static_cast<int64_t>(dTime * 1000.0));
+ }
+}
+
+float CApplication::GetPercentage() const
+{
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ const auto stackHelper = GetComponent<CApplicationStackHelper>();
+
+ if (appPlayer->IsPlaying())
+ {
+ if (appPlayer->GetTotalTime() == 0 && appPlayer->IsPlayingAudio() &&
+ m_itemCurrentFile->HasMusicInfoTag())
+ {
+ const CMusicInfoTag& tag = *m_itemCurrentFile->GetMusicInfoTag();
+ if (tag.GetDuration() > 0)
+ return (float)(GetTime() / tag.GetDuration() * 100);
+ }
+
+ if (stackHelper->IsPlayingRegularStack())
+ {
+ double totalTime = GetTotalTime();
+ if (totalTime > 0.0)
+ return (float)(GetTime() / totalTime * 100);
+ }
+ else
+ return appPlayer->GetPercentage();
+ }
+ return 0.0f;
+}
+
+float CApplication::GetCachePercentage() const
+{
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ const auto stackHelper = GetComponent<CApplicationStackHelper>();
+
+ if (appPlayer->IsPlaying())
+ {
+ // Note that the player returns a relative cache percentage and we want an absolute percentage
+ if (stackHelper->IsPlayingRegularStack())
+ {
+ float stackedTotalTime = (float) GetTotalTime();
+ // We need to take into account the stack's total time vs. currently playing file's total time
+ if (stackedTotalTime > 0.0f)
+ return std::min(100.0f,
+ GetPercentage() + (appPlayer->GetCachePercentage() *
+ appPlayer->GetTotalTime() * 0.001f / stackedTotalTime));
+ }
+ else
+ return std::min(100.0f, appPlayer->GetPercentage() + appPlayer->GetCachePercentage());
+ }
+ return 0.0f;
+}
+
+void CApplication::SeekPercentage(float percent)
+{
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ const auto stackHelper = GetComponent<CApplicationStackHelper>();
+
+ if (appPlayer->IsPlaying() && (percent >= 0.0f))
+ {
+ if (!appPlayer->CanSeek())
+ return;
+ if (stackHelper->IsPlayingRegularStack())
+ SeekTime(static_cast<double>(percent) * 0.01 * GetTotalTime());
+ else
+ appPlayer->SeekPercentage(percent);
+ }
+}
+
+std::string CApplication::GetCurrentPlayer()
+{
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ return appPlayer->GetCurrentPlayer();
+}
+
+void CApplication::UpdateLibraries()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_UPDATEONSTARTUP))
+ {
+ CLog::LogF(LOGINFO, "Starting video library startup scan");
+ CVideoLibraryQueue::GetInstance().ScanLibrary(
+ "", false, !settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_BACKGROUNDUPDATE));
+ }
+
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_UPDATEONSTARTUP))
+ {
+ CLog::LogF(LOGINFO, "Starting music library startup scan");
+ CMusicLibraryQueue::GetInstance().ScanLibrary(
+ "", MUSIC_INFO::CMusicInfoScanner::SCAN_NORMAL,
+ !settings->GetBool(CSettings::SETTING_MUSICLIBRARY_BACKGROUNDUPDATE));
+ }
+}
+
+void CApplication::UpdateCurrentPlayArt()
+{
+ const auto appPlayer = GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlayingAudio())
+ return;
+ //Clear and reload the art for the currently playing item to show updated art on OSD
+ m_itemCurrentFile->ClearArt();
+ CMusicThumbLoader loader;
+ loader.LoadItem(m_itemCurrentFile.get());
+ // Mirror changes to GUI item
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*m_itemCurrentFile);
+}
+
+bool CApplication::ProcessAndStartPlaylist(const std::string& strPlayList,
+ PLAYLIST::CPlayList& playlist,
+ PLAYLIST::Id playlistId,
+ int track)
+{
+ CLog::Log(LOGDEBUG, "CApplication::ProcessAndStartPlaylist({}, {})", strPlayList, playlistId);
+
+ // initial exit conditions
+ // no songs in playlist just return
+ if (playlist.size() == 0)
+ return false;
+
+ // illegal playlist
+ if (playlistId == PLAYLIST::TYPE_NONE || playlistId == PLAYLIST::TYPE_PICTURE)
+ return false;
+
+ // setup correct playlist
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(playlistId);
+
+ // if the playlist contains an internet stream, this file will be used
+ // to generate a thumbnail for musicplayer.cover
+ m_strPlayListFile = strPlayList;
+
+ // add the items to the playlist player
+ CServiceBroker::GetPlaylistPlayer().Add(playlistId, playlist);
+
+ // if we have a playlist
+ if (CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId).size())
+ {
+ // start playing it
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(playlistId);
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().Play(track, "");
+ return true;
+ }
+ return false;
+}
+
+bool CApplication::GetRenderGUI() const
+{
+ return GetComponent<CApplicationPowerHandling>()->GetRenderGUI();
+}
+
+bool CApplication::SetLanguage(const std::string &strLanguage)
+{
+ // nothing to be done if the language hasn't changed
+ if (strLanguage == CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_LANGUAGE))
+ return true;
+
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_LOCALE_LANGUAGE, strLanguage);
+}
+
+bool CApplication::LoadLanguage(bool reload)
+{
+ // load the configured language
+ if (!g_langInfo.SetLanguage("", reload))
+ return false;
+
+ // set the proper audio and subtitle languages
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ g_langInfo.SetAudioLanguage(settings->GetString(CSettings::SETTING_LOCALE_AUDIOLANGUAGE));
+ g_langInfo.SetSubtitleLanguage(settings->GetString(CSettings::SETTING_LOCALE_SUBTITLELANGUAGE));
+
+ return true;
+}
+
+void CApplication::SetLoggingIn(bool switchingProfiles)
+{
+ // don't save skin settings on unloading when logging into another profile
+ // because in that case we have already loaded the new profile and
+ // would therefore write the previous skin's settings into the new profile
+ // instead of into the previous one
+ GetComponent<CApplicationSkinHandling>()->m_saveSkinOnUnloading = !switchingProfiles;
+}
+
+void CApplication::PrintStartupLog()
+{
+ CLog::Log(LOGINFO, "-----------------------------------------------------------------------");
+ CLog::Log(LOGINFO, "Starting {} ({}). Platform: {} {} {}-bit", CSysInfo::GetAppName(),
+ CSysInfo::GetVersion(), g_sysinfo.GetBuildTargetPlatformName(),
+ g_sysinfo.GetBuildTargetCpuFamily(), g_sysinfo.GetXbmcBitness());
+
+ std::string buildType;
+#if defined(_DEBUG)
+ buildType = "Debug";
+#elif defined(NDEBUG)
+ buildType = "Release";
+#else
+ buildType = "Unknown";
+#endif
+
+ CLog::Log(LOGINFO, "Using {} {} x{}", buildType, CSysInfo::GetAppName(),
+ g_sysinfo.GetXbmcBitness());
+ CLog::Log(LOGINFO, "{} compiled {} by {} for {} {} {}-bit {} ({})", CSysInfo::GetAppName(),
+ CSysInfo::GetBuildDate(), g_sysinfo.GetUsedCompilerNameAndVer(),
+ g_sysinfo.GetBuildTargetPlatformName(), g_sysinfo.GetBuildTargetCpuFamily(),
+ g_sysinfo.GetXbmcBitness(), g_sysinfo.GetBuildTargetPlatformVersionDecoded(),
+ g_sysinfo.GetBuildTargetPlatformVersion());
+
+ std::string deviceModel(g_sysinfo.GetModelName());
+ if (!g_sysinfo.GetManufacturerName().empty())
+ deviceModel = g_sysinfo.GetManufacturerName() + " " +
+ (deviceModel.empty() ? std::string("device") : deviceModel);
+ if (!deviceModel.empty())
+ CLog::Log(LOGINFO, "Running on {} with {}, kernel: {} {} {}-bit version {}", deviceModel,
+ g_sysinfo.GetOsPrettyNameWithVersion(), g_sysinfo.GetKernelName(),
+ g_sysinfo.GetKernelCpuFamily(), g_sysinfo.GetKernelBitness(),
+ g_sysinfo.GetKernelVersionFull());
+ else
+ CLog::Log(LOGINFO, "Running on {}, kernel: {} {} {}-bit version {}",
+ g_sysinfo.GetOsPrettyNameWithVersion(), g_sysinfo.GetKernelName(),
+ g_sysinfo.GetKernelCpuFamily(), g_sysinfo.GetKernelBitness(),
+ g_sysinfo.GetKernelVersionFull());
+
+ CLog::Log(LOGINFO, "FFmpeg version/source: {}", av_version_info());
+
+ std::string cpuModel(CServiceBroker::GetCPUInfo()->GetCPUModel());
+ if (!cpuModel.empty())
+ {
+ CLog::Log(LOGINFO, "Host CPU: {}, {} core{} available", cpuModel,
+ CServiceBroker::GetCPUInfo()->GetCPUCount(),
+ (CServiceBroker::GetCPUInfo()->GetCPUCount() == 1) ? "" : "s");
+ }
+ else
+ CLog::Log(LOGINFO, "{} CPU core{} available", CServiceBroker::GetCPUInfo()->GetCPUCount(),
+ (CServiceBroker::GetCPUInfo()->GetCPUCount() == 1) ? "" : "s");
+
+ // Any system info logging that is unique to a platform
+ m_ServiceManager->GetPlatform().PlatformSyslog();
+
+#if defined(__arm__) || defined(__aarch64__)
+ CLog::Log(LOGINFO, "ARM Features: Neon {}",
+ (CServiceBroker::GetCPUInfo()->GetCPUFeatures() & CPU_FEATURE_NEON) ? "enabled"
+ : "disabled");
+#endif
+ CSpecialProtocol::LogPaths();
+
+#ifdef HAS_WEB_SERVER
+ CLog::Log(LOGINFO, "Webserver extra whitelist paths: {}",
+ StringUtils::Join(CCompileInfo::GetWebserverExtraWhitelist(), ", "));
+#endif
+
+ // Check, whether libkodi.so was reused (happens on Android, where the system does not unload
+ // the lib on activity end, but keeps it loaded (as long as there is enough memory) and reuses
+ // it on next activity start.
+ static bool firstRun = true;
+
+ CLog::Log(LOGINFO, "The executable running is: {}{}", CUtil::ResolveExecutablePath(),
+ firstRun ? "" : " [reused]");
+
+ firstRun = false;
+
+ std::string hostname("[unknown]");
+ m_ServiceManager->GetNetwork().GetHostName(hostname);
+ CLog::Log(LOGINFO, "Local hostname: {}", hostname);
+ std::string lowerAppName = CCompileInfo::GetAppName();
+ StringUtils::ToLower(lowerAppName);
+ CLog::Log(LOGINFO, "Log File is located: {}.log",
+ CSpecialProtocol::TranslatePath("special://logpath/" + lowerAppName));
+ CRegExp::LogCheckUtf8Support();
+ CLog::Log(LOGINFO, "-----------------------------------------------------------------------");
+}
+
+void CApplication::CloseNetworkShares()
+{
+ CLog::Log(LOGDEBUG,"CApplication::CloseNetworkShares: Closing all network shares");
+
+#if defined(HAS_FILESYSTEM_SMB) && !defined(TARGET_WINDOWS)
+ smb.Deinit();
+#endif
+
+#ifdef HAS_FILESYSTEM_NFS
+ gNfsConnection.Deinit();
+#endif
+
+ for (const auto& vfsAddon : CServiceBroker::GetVFSAddonCache().GetAddonInstances())
+ vfsAddon->DisconnectAll();
+}
diff --git a/xbmc/application/Application.h b/xbmc/application/Application.h
new file mode 100644
index 0000000..a37dcf7
--- /dev/null
+++ b/xbmc/application/Application.h
@@ -0,0 +1,257 @@
+/*
+ * 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 "application/ApplicationComponents.h"
+#include "application/ApplicationEnums.h"
+#include "application/ApplicationPlayerCallback.h"
+#include "application/ApplicationSettingsHandling.h"
+#include "guilib/IMsgTargetCallback.h"
+#include "guilib/IWindowManagerCallback.h"
+#include "messaging/IMessageTarget.h"
+#include "playlists/PlayListTypes.h"
+#include "threads/SystemClock.h"
+#include "utils/GlobalsHandling.h"
+#include "utils/Stopwatch.h"
+#include "windowing/Resolution.h"
+#include "windowing/XBMC_events.h"
+
+#include <atomic>
+#include <chrono>
+#include <deque>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CAction;
+class CAppInboundProtocol;
+class CBookmark;
+class CFileItem;
+class CFileItemList;
+class CGUIComponent;
+class CInertialScrollingHandler;
+class CKey;
+class CSeekHandler;
+class CServiceManager;
+class CSettingsComponent;
+class CSplash;
+class CWinSystemBase;
+
+namespace ADDON
+{
+ class CSkinInfo;
+ class IAddon;
+ typedef std::shared_ptr<IAddon> AddonPtr;
+ class CAddonInfo;
+}
+
+namespace ANNOUNCEMENT
+{
+ class CAnnouncementManager;
+}
+
+namespace MEDIA_DETECT
+{
+ class CAutorun;
+}
+
+namespace PLAYLIST
+{
+ class CPlayList;
+}
+
+namespace ActiveAE
+{
+ class CActiveAE;
+}
+
+namespace VIDEO
+{
+ class CVideoInfoScanner;
+}
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoScanner;
+}
+
+class CApplication : public IWindowManagerCallback,
+ public IMsgTargetCallback,
+ public KODI::MESSAGING::IMessageTarget,
+ public CApplicationComponents,
+ public CApplicationPlayerCallback,
+ public CApplicationSettingsHandling
+{
+friend class CAppInboundProtocol;
+
+public:
+
+ // If playback time of current item is greater than this value, ACTION_PREV_ITEM will seek to start
+ // of currently playing item, otherwise it will seek to start of the previous item in playlist
+ static const unsigned int ACTION_PREV_ITEM_THRESHOLD = 3; // seconds;
+
+ CApplication(void);
+ ~CApplication(void) override;
+
+ bool Create();
+ bool Initialize();
+ int Run();
+ bool Cleanup();
+
+ void FrameMove(bool processEvents, bool processGUI = true) override;
+ void Render() override;
+
+ bool IsInitialized() const { return !m_bInitializing; }
+ bool IsStopping() const { return m_bStop; }
+
+ bool CreateGUI();
+ bool InitWindow(RESOLUTION res = RES_INVALID);
+
+ bool Stop(int exitCode);
+ const std::string& CurrentFile();
+ CFileItem& CurrentFileItem();
+ std::shared_ptr<CFileItem> CurrentFileItemPtr();
+ const CFileItem& CurrentUnstackedItem();
+ bool OnMessage(CGUIMessage& message) override;
+ std::string GetCurrentPlayer();
+
+ int GetMessageMask() override;
+ void OnApplicationMessage(KODI::MESSAGING::ThreadMessage* pMsg) override;
+
+ bool PlayMedia(CFileItem& item, const std::string& player, PLAYLIST::Id playlistId);
+ bool ProcessAndStartPlaylist(const std::string& strPlayList,
+ PLAYLIST::CPlayList& playlist,
+ PLAYLIST::Id playlistId,
+ int track = 0);
+ bool PlayFile(CFileItem item, const std::string& player, bool bRestart = false);
+ void StopPlaying();
+ void Restart(bool bSamePosition = true);
+ void DelayedPlayerRestart();
+ void CheckDelayedPlayerRestart();
+ bool IsPlayingFullScreenVideo() const;
+ bool IsFullScreen();
+ bool OnAction(const CAction &action);
+ void CloseNetworkShares();
+
+ void ConfigureAndEnableAddons();
+ void ShowAppMigrationMessage();
+ void Process() override;
+ void ProcessSlow();
+ /*!
+ \brief Returns the total time in fractional seconds of the currently playing media
+
+ Beware that this method returns fractional seconds whereas IPlayer::GetTotalTime() returns milliseconds.
+ */
+ double GetTotalTime() const;
+ /*!
+ \brief Returns the current time in fractional seconds of the currently playing media
+
+ Beware that this method returns fractional seconds whereas IPlayer::GetTime() returns milliseconds.
+ */
+ double GetTime() const;
+ float GetPercentage() const;
+
+ // Get the percentage of data currently cached/buffered (aq/vq + FileCache) from the input stream if applicable.
+ float GetCachePercentage() const;
+
+ void SeekPercentage(float percent);
+ void SeekTime( double dTime = 0.0 );
+
+ void UpdateLibraries();
+
+ void UpdateCurrentPlayArt();
+
+ bool ExecuteXBMCAction(std::string action, const CGUIListItemPtr &item = NULL);
+
+#ifdef HAS_DVD_DRIVE
+ std::unique_ptr<MEDIA_DETECT::CAutorun> m_Autorun;
+#endif
+
+ std::string m_strPlayListFile;
+
+ bool IsAppFocused() const { return m_AppFocused; }
+
+ bool GetRenderGUI() const override;
+
+ bool SetLanguage(const std::string &strLanguage);
+ bool LoadLanguage(bool reload);
+
+ void SetLoggingIn(bool switchingProfiles);
+
+ std::unique_ptr<CServiceManager> m_ServiceManager;
+
+ /*!
+ \brief Locks calls from outside kodi (e.g. python) until framemove is processed.
+ */
+ void LockFrameMoveGuard();
+
+ /*!
+ \brief Unlocks calls from outside kodi (e.g. python).
+ */
+ void UnlockFrameMoveGuard();
+
+protected:
+ bool OnSettingsSaving() const override;
+ void PlaybackCleanup();
+
+ // inbound protocol
+ bool OnEvent(XBMC_Event& newEvent);
+
+ std::shared_ptr<ANNOUNCEMENT::CAnnouncementManager> m_pAnnouncementManager;
+ std::unique_ptr<CGUIComponent> m_pGUI;
+ std::unique_ptr<CWinSystemBase> m_pWinSystem;
+ std::unique_ptr<ActiveAE::CActiveAE> m_pActiveAE;
+ std::shared_ptr<CAppInboundProtocol> m_pAppPort;
+ std::deque<XBMC_Event> m_portEvents;
+ CCriticalSection m_portSection;
+
+ // timer information
+ CStopWatch m_restartPlayerTimer;
+ CStopWatch m_frameTime;
+ CStopWatch m_slowTimer;
+ XbmcThreads::EndTime<> m_guiRefreshTimer;
+
+ std::string m_prevMedia;
+ bool m_bInitializing = true;
+
+ int m_nextPlaylistItem = -1;
+
+ std::chrono::time_point<std::chrono::steady_clock> m_lastRenderTime;
+ bool m_skipGuiRender = false;
+
+ std::unique_ptr<MUSIC_INFO::CMusicInfoScanner> m_musicInfoScanner;
+
+ bool PlayStack(CFileItem& item, bool bRestart);
+
+ void HandlePortEvents();
+
+ std::unique_ptr<CInertialScrollingHandler> m_pInertialScrollingHandler;
+
+ std::vector<std::shared_ptr<ADDON::CAddonInfo>>
+ m_incompatibleAddons; /*!< Result of addon migration (incompatible addon infos) */
+
+public:
+ bool m_bStop{false};
+ bool m_AppFocused{true};
+
+private:
+ void PrintStartupLog();
+ void ResetCurrentItem();
+
+ mutable CCriticalSection m_critSection; /*!< critical section for all changes to this class, except for changes to triggers */
+
+ CCriticalSection m_frameMoveGuard; /*!< critical section for synchronizing GUI actions from inside and outside (python) */
+ std::atomic_uint m_WaitingExternalCalls; /*!< counts threads which are waiting to be processed in FrameMove */
+ unsigned int m_ProcessedExternalCalls = 0; /*!< counts calls which are processed during one "door open" cycle in FrameMove */
+ unsigned int m_ProcessedExternalDecay = 0; /*!< counts to close door after a few frames of no python activity */
+ int m_ExitCode{EXITCODE_QUIT};
+};
+
+XBMC_GLOBAL_REF(CApplication,g_application);
+#define g_application XBMC_GLOBAL_USE(CApplication)
diff --git a/xbmc/application/ApplicationActionListeners.cpp b/xbmc/application/ApplicationActionListeners.cpp
new file mode 100644
index 0000000..83c798e
--- /dev/null
+++ b/xbmc/application/ApplicationActionListeners.cpp
@@ -0,0 +1,48 @@
+/*
+ * 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 "ApplicationActionListeners.h"
+
+#include "interfaces/IActionListener.h"
+#include "threads/CriticalSection.h"
+
+#include <algorithm>
+#include <mutex>
+
+CApplicationActionListeners::CApplicationActionListeners(CCriticalSection& section)
+ : m_critSection(section)
+{
+}
+
+void CApplicationActionListeners::RegisterActionListener(IActionListener* listener)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = std::find(m_actionListeners.begin(), m_actionListeners.end(), listener);
+ if (it == m_actionListeners.end())
+ m_actionListeners.push_back(listener);
+}
+
+void CApplicationActionListeners::UnregisterActionListener(IActionListener* listener)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto it = std::find(m_actionListeners.begin(), m_actionListeners.end(), listener);
+ if (it != m_actionListeners.end())
+ m_actionListeners.erase(it);
+}
+
+bool CApplicationActionListeners::NotifyActionListeners(const CAction& action) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& listener : m_actionListeners)
+ {
+ if (listener->OnAction(action))
+ return true;
+ }
+
+ return false;
+}
diff --git a/xbmc/application/ApplicationActionListeners.h b/xbmc/application/ApplicationActionListeners.h
new file mode 100644
index 0000000..8f72dd4
--- /dev/null
+++ b/xbmc/application/ApplicationActionListeners.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "application/IApplicationComponent.h"
+
+#include <vector>
+
+class CAction;
+class CApplication;
+class CCriticalSection;
+class IActionListener;
+
+/*!
+ * \brief Class handling application support for action listeners.
+ */
+
+class CApplicationActionListeners : public IApplicationComponent
+{
+ friend class CApplication;
+
+public:
+ CApplicationActionListeners(CCriticalSection& sect);
+
+ /*!
+ \brief Register an action listener.
+ \param listener The listener to register
+ */
+ void RegisterActionListener(IActionListener* listener);
+ /*!
+ \brief Unregister an action listener.
+ \param listener The listener to unregister
+ */
+ void UnregisterActionListener(IActionListener* listener);
+
+protected:
+ /*!
+ \brief Delegates the action to all registered action handlers.
+ \param action The action
+ \return true, if the action was taken by one of the action listener.
+ */
+ bool NotifyActionListeners(const CAction& action) const;
+
+ std::vector<IActionListener*> m_actionListeners;
+
+ CCriticalSection& m_critSection;
+};
diff --git a/xbmc/application/ApplicationComponents.h b/xbmc/application/ApplicationComponents.h
new file mode 100644
index 0000000..33ae0d3
--- /dev/null
+++ b/xbmc/application/ApplicationComponents.h
@@ -0,0 +1,15 @@
+/*
+ * Copyright (C) 2022 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 "application/IApplicationComponent.h"
+#include "utils/ComponentContainer.h"
+
+//! \brief Convenience alias for application components.
+using CApplicationComponents = CComponentContainer<IApplicationComponent>;
diff --git a/xbmc/application/ApplicationEnums.h b/xbmc/application/ApplicationEnums.h
new file mode 100644
index 0000000..3afa41e
--- /dev/null
+++ b/xbmc/application/ApplicationEnums.h
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+enum StartupAction
+{
+ STARTUP_ACTION_NONE = 0,
+ STARTUP_ACTION_PLAY_TV,
+ STARTUP_ACTION_PLAY_RADIO
+};
+
+// Do not change the numbers; external scripts depend on them
+enum
+{
+ EXITCODE_QUIT = 0,
+ EXITCODE_POWERDOWN = 64,
+ EXITCODE_RESTARTAPP = 65,
+ EXITCODE_REBOOT = 66,
+};
diff --git a/xbmc/application/ApplicationPlayer.cpp b/xbmc/application/ApplicationPlayer.cpp
new file mode 100644
index 0000000..ef571a6
--- /dev/null
+++ b/xbmc/application/ApplicationPlayer.cpp
@@ -0,0 +1,1061 @@
+/*
+ * 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 "ApplicationPlayer.h"
+
+#include "ServiceBroker.h"
+#include "cores/DataCacheCore.h"
+#include "cores/IPlayer.h"
+#include "cores/VideoPlayer/VideoPlayer.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+std::shared_ptr<const IPlayer> CApplicationPlayer::GetInternal() const
+{
+ std::unique_lock<CCriticalSection> lock(m_playerLock);
+ return m_pPlayer;
+}
+
+std::shared_ptr<IPlayer> CApplicationPlayer::GetInternal()
+{
+ std::unique_lock<CCriticalSection> lock(m_playerLock);
+ return m_pPlayer;
+}
+
+void CApplicationPlayer::ClosePlayer()
+{
+ m_nextItem.pItem.reset();
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ CloseFile();
+ ResetPlayer();
+ }
+}
+
+void CApplicationPlayer::ResetPlayer()
+{
+ // we need to do this directly on the member
+ std::unique_lock<CCriticalSection> lock(m_playerLock);
+ m_pPlayer.reset();
+}
+
+void CApplicationPlayer::CloseFile(bool reopen)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ player->CloseFile(reopen);
+ }
+}
+
+void CApplicationPlayer::CreatePlayer(const CPlayerCoreFactory &factory, const std::string &player, IPlayerCallback& callback)
+{
+ std::unique_lock<CCriticalSection> lock(m_playerLock);
+ if (!m_pPlayer)
+ {
+ CDataCacheCore::GetInstance().Reset();
+ m_pPlayer = factory.CreatePlayer(player, callback);
+ }
+}
+
+std::string CApplicationPlayer::GetCurrentPlayer() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ {
+ return player->m_name;
+ }
+ return "";
+}
+
+bool CApplicationPlayer::OpenFile(const CFileItem& item, const CPlayerOptions& options,
+ const CPlayerCoreFactory &factory,
+ const std::string &playerName, IPlayerCallback& callback)
+{
+ // get player type
+ std::string newPlayer;
+ if (!playerName.empty())
+ newPlayer = playerName;
+ else
+ newPlayer = factory.GetDefaultPlayer(item);
+
+ // check if we need to close current player
+ // VideoPlayer can open a new file while playing
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player && player->IsPlaying())
+ {
+ bool needToClose = false;
+
+ if (item.IsDiscImage() || item.IsDVDFile())
+ needToClose = true;
+
+ if (player->m_name != newPlayer)
+ needToClose = true;
+
+ if (player->m_type != "video")
+ needToClose = true;
+
+ if (needToClose)
+ {
+ m_nextItem.pItem = std::make_shared<CFileItem>(item);
+ m_nextItem.options = options;
+ m_nextItem.playerName = newPlayer;
+ m_nextItem.callback = &callback;
+
+ CloseFile();
+ if (player->m_name != newPlayer)
+ {
+ std::unique_lock<CCriticalSection> lock(m_playerLock);
+ m_pPlayer.reset();
+ }
+ return true;
+ }
+ }
+ else if (player && player->m_name != newPlayer)
+ {
+ CloseFile();
+ {
+ std::unique_lock<CCriticalSection> lock(m_playerLock);
+ m_pPlayer.reset();
+ player.reset();
+ }
+ }
+
+ if (!player)
+ {
+ CreatePlayer(factory, newPlayer, callback);
+ player = GetInternal();
+ if (!player)
+ return false;
+ }
+
+ bool ret = player->OpenFile(item, options);
+
+ m_nextItem.pItem.reset();
+
+ // reset caching timers
+ m_audioStreamUpdate.SetExpired();
+ m_videoStreamUpdate.SetExpired();
+ m_subtitleStreamUpdate.SetExpired();
+
+ return ret;
+}
+
+void CApplicationPlayer::OpenNext(const CPlayerCoreFactory &factory)
+{
+ if (m_nextItem.pItem)
+ {
+ OpenFile(*m_nextItem.pItem, m_nextItem.options,
+ factory,
+ m_nextItem.playerName, *m_nextItem.callback);
+ m_nextItem.pItem.reset();
+ }
+}
+
+bool CApplicationPlayer::HasPlayer() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ return player != nullptr;
+}
+
+int CApplicationPlayer::GetChapter() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->GetChapter();
+ else
+ return -1;
+}
+
+int CApplicationPlayer::GetChapterCount() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->GetChapterCount();
+ else
+ return 0;
+}
+
+void CApplicationPlayer::GetChapterName(std::string& strChapterName, int chapterIdx) const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ player->GetChapterName(strChapterName, chapterIdx);
+}
+
+int64_t CApplicationPlayer::GetChapterPos(int chapterIdx) const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->GetChapterPos(chapterIdx);
+
+ return -1;
+}
+
+bool CApplicationPlayer::HasAudio() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ return (player && player->HasAudio());
+}
+
+bool CApplicationPlayer::HasVideo() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ return (player && player->HasVideo());
+}
+
+bool CApplicationPlayer::HasGame() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ return (player && player->HasGame());
+}
+
+PLAYLIST::Id CApplicationPlayer::GetPreferredPlaylist() const
+{
+ if (IsPlayingVideo())
+ return PLAYLIST::TYPE_VIDEO;
+
+ if (IsPlayingAudio())
+ return PLAYLIST::TYPE_MUSIC;
+
+ return PLAYLIST::TYPE_NONE;
+}
+
+bool CApplicationPlayer::HasRDS() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ return (player && player->HasRDS());
+}
+
+bool CApplicationPlayer::IsPaused() const
+{
+ return (GetPlaySpeed() == 0);
+}
+
+bool CApplicationPlayer::IsPlaying() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ return (player && player->IsPlaying());
+}
+
+bool CApplicationPlayer::IsPausedPlayback() const
+{
+ return (IsPlaying() && (GetPlaySpeed() == 0));
+}
+
+bool CApplicationPlayer::IsPlayingAudio() const
+{
+ return (IsPlaying() && !HasVideo() && HasAudio());
+}
+
+bool CApplicationPlayer::IsPlayingVideo() const
+{
+ return (IsPlaying() && HasVideo());
+}
+
+bool CApplicationPlayer::IsPlayingGame() const
+{
+ return (IsPlaying() && HasGame());
+}
+
+bool CApplicationPlayer::IsPlayingRDS() const
+{
+ return (IsPlaying() && HasRDS());
+}
+
+void CApplicationPlayer::Pause()
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ player->Pause();
+ }
+}
+
+void CApplicationPlayer::SetMute(bool bOnOff)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SetMute(bOnOff);
+}
+
+void CApplicationPlayer::SetVolume(float volume)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SetVolume(volume);
+}
+
+void CApplicationPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->Seek(bPlus, bLargeStep, bChapterOverride);
+}
+
+void CApplicationPlayer::SeekPercentage(float fPercent)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SeekPercentage(fPercent);
+}
+
+bool CApplicationPlayer::IsPassthrough() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ return (player && player->IsPassthrough());
+}
+
+bool CApplicationPlayer::CanSeek() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ return (player && player->CanSeek());
+}
+
+bool CApplicationPlayer::SeekScene(bool bPlus)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ return (player && player->SeekScene(bPlus));
+}
+
+void CApplicationPlayer::SeekTime(int64_t iTime)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SeekTime(iTime);
+}
+
+void CApplicationPlayer::SeekTimeRelative(int64_t iTime)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ // use relative seeking if implemented by player
+ if (!player->SeekTimeRelative(iTime))
+ {
+ int64_t abstime = GetTime() + iTime;
+ player->SeekTime(abstime);
+ }
+ }
+}
+
+int64_t CApplicationPlayer::GetTime() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return CDataCacheCore::GetInstance().GetPlayTime();
+ else
+ return 0;
+}
+
+int64_t CApplicationPlayer::GetMinTime() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return CDataCacheCore::GetInstance().GetMinTime();
+ else
+ return 0;
+}
+
+int64_t CApplicationPlayer::GetMaxTime() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return CDataCacheCore::GetInstance().GetMaxTime();
+ else
+ return 0;
+}
+
+time_t CApplicationPlayer::GetStartTime() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return CDataCacheCore::GetInstance().GetStartTime();
+ else
+ return 0;
+}
+
+int64_t CApplicationPlayer::GetTotalTime() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ {
+ int64_t total = CDataCacheCore::GetInstance().GetMaxTime() - CDataCacheCore::GetInstance().GetMinTime();
+ return total;
+ }
+ else
+ return 0;
+}
+
+bool CApplicationPlayer::IsCaching() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ return (player && player->IsCaching());
+}
+
+bool CApplicationPlayer::IsInMenu() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ return (player && player->IsInMenu());
+}
+
+MenuType CApplicationPlayer::GetSupportedMenuType() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (!player)
+ {
+ return MenuType::NONE;
+ }
+ return player->GetSupportedMenuType();
+}
+
+int CApplicationPlayer::GetCacheLevel() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->GetCacheLevel();
+ else
+ return 0;
+}
+
+int CApplicationPlayer::GetSubtitleCount() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->GetSubtitleCount();
+ else
+ return 0;
+}
+
+int CApplicationPlayer::GetAudioStream()
+{
+ if (!m_audioStreamUpdate.IsTimePast())
+ return m_iAudioStream;
+
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ m_iAudioStream = player->GetAudioStream();
+ m_audioStreamUpdate.Set(1000ms);
+ return m_iAudioStream;
+ }
+ else
+ return 0;
+}
+
+int CApplicationPlayer::GetSubtitle()
+{
+ if (!m_subtitleStreamUpdate.IsTimePast())
+ return m_iSubtitleStream;
+
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ m_iSubtitleStream = player->GetSubtitle();
+ m_subtitleStreamUpdate.Set(1000ms);
+ return m_iSubtitleStream;
+ }
+ else
+ return 0;
+}
+
+bool CApplicationPlayer::GetSubtitleVisible() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ return player && player->GetSubtitleVisible();
+}
+
+bool CApplicationPlayer::CanPause() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ return (player && player->CanPause());
+}
+
+bool CApplicationPlayer::HasTeletextCache() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->HasTeletextCache();
+ else
+ return false;
+}
+
+std::shared_ptr<TextCacheStruct_t> CApplicationPlayer::GetTeletextCache()
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ return player->GetTeletextCache();
+ else
+ return {};
+}
+
+float CApplicationPlayer::GetPercentage() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ {
+ float fPercent = CDataCacheCore::GetInstance().GetPlayPercentage();
+ return std::max(0.0f, std::min(fPercent, 100.0f));
+ }
+ else
+ return 0.0;
+}
+
+float CApplicationPlayer::GetCachePercentage() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->GetCachePercentage();
+ else
+ return 0.0;
+}
+
+void CApplicationPlayer::SetSpeed(float speed)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SetSpeed(speed);
+}
+
+void CApplicationPlayer::SetTempo(float tempo)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SetTempo(tempo);
+}
+
+void CApplicationPlayer::FrameAdvance(int frames)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->FrameAdvance(frames);
+}
+
+void CApplicationPlayer::DoAudioWork()
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->DoAudioWork();
+}
+
+std::string CApplicationPlayer::GetPlayerState()
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ return player->GetPlayerState();
+ else
+ return "";
+}
+
+bool CApplicationPlayer::QueueNextFile(const CFileItem &file)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ return (player && player->QueueNextFile(file));
+}
+
+bool CApplicationPlayer::SetPlayerState(const std::string& state)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ return (player && player->SetPlayerState(state));
+}
+
+void CApplicationPlayer::OnNothingToQueueNotify()
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->OnNothingToQueueNotify();
+}
+
+void CApplicationPlayer::GetVideoStreamInfo(int streamId, VideoStreamInfo& info) const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ player->GetVideoStreamInfo(streamId, info);
+}
+
+void CApplicationPlayer::GetAudioStreamInfo(int index, AudioStreamInfo& info) const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ player->GetAudioStreamInfo(index, info);
+}
+
+int CApplicationPlayer::GetPrograms(std::vector<ProgramInfo> &programs)
+{
+ int ret = 0;
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ ret = player->GetPrograms(programs);
+ return ret;
+}
+
+void CApplicationPlayer::SetProgram(int progId)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SetProgram(progId);
+}
+
+int CApplicationPlayer::GetProgramsCount() const
+{
+ int ret = 0;
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ ret = player->GetProgramsCount();
+ return ret;
+}
+
+bool CApplicationPlayer::OnAction(const CAction &action)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ return (player && player->OnAction(action));
+}
+
+int CApplicationPlayer::GetAudioStreamCount() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->GetAudioStreamCount();
+ else
+ return 0;
+}
+
+int CApplicationPlayer::GetVideoStream()
+{
+ if (!m_videoStreamUpdate.IsTimePast())
+ return m_iVideoStream;
+
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ m_iVideoStream = player->GetVideoStream();
+ m_videoStreamUpdate.Set(1000ms);
+ return m_iVideoStream;
+ }
+ else
+ return 0;
+}
+
+int CApplicationPlayer::GetVideoStreamCount() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->GetVideoStreamCount();
+ else
+ return 0;
+}
+
+void CApplicationPlayer::SetAudioStream(int iStream)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ player->SetAudioStream(iStream);
+ m_iAudioStream = iStream;
+ m_audioStreamUpdate.Set(1000ms);
+ }
+}
+
+void CApplicationPlayer::GetSubtitleStreamInfo(int index, SubtitleStreamInfo& info) const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ player->GetSubtitleStreamInfo(index, info);
+}
+
+void CApplicationPlayer::SetSubtitle(int iStream)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ player->SetSubtitle(iStream);
+ m_iSubtitleStream = iStream;
+ m_subtitleStreamUpdate.Set(1000ms);
+ }
+}
+
+void CApplicationPlayer::SetSubtitleVisible(bool bVisible)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ player->SetSubtitleVisible(bVisible);
+ }
+}
+
+void CApplicationPlayer::SetSubtitleVerticalPosition(int value, bool save)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ player->SetSubtitleVerticalPosition(value, save);
+ }
+}
+
+void CApplicationPlayer::SetTime(int64_t time)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ return player->SetTime(time);
+}
+
+void CApplicationPlayer::SetTotalTime(int64_t time)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SetTotalTime(time);
+}
+
+void CApplicationPlayer::SetVideoStream(int iStream)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ player->SetVideoStream(iStream);
+ m_iVideoStream = iStream;
+ m_videoStreamUpdate.Set(1000ms);
+ }
+}
+
+void CApplicationPlayer::AddSubtitle(const std::string& strSubPath)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->AddSubtitle(strSubPath);
+}
+
+void CApplicationPlayer::SetSubTitleDelay(float fValue)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SetSubTitleDelay(fValue);
+}
+
+void CApplicationPlayer::SetAVDelay(float fValue)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SetAVDelay(fValue);
+}
+
+void CApplicationPlayer::SetDynamicRangeCompression(long drc)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SetDynamicRangeCompression(drc);
+}
+
+void CApplicationPlayer::LoadPage(int p, int sp, unsigned char* buffer)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->LoadPage(p, sp, buffer);
+}
+
+void CApplicationPlayer::GetAudioCapabilities(std::vector<int>& audioCaps) const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ player->GetAudioCapabilities(audioCaps);
+}
+
+void CApplicationPlayer::GetSubtitleCapabilities(std::vector<int>& subCaps) const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ player->GetSubtitleCapabilities(subCaps);
+}
+
+int CApplicationPlayer::SeekChapter(int iChapter)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ return player->SeekChapter(iChapter);
+ else
+ return 0;
+}
+
+void CApplicationPlayer::SetPlaySpeed(float speed)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (!player)
+ return;
+
+ if (!IsPlayingAudio() && !IsPlayingVideo())
+ return ;
+
+ SetSpeed(speed);
+}
+
+float CApplicationPlayer::GetPlaySpeed() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ {
+ return CDataCacheCore::GetInstance().GetSpeed();
+ }
+ else
+ return 0;
+}
+
+float CApplicationPlayer::GetPlayTempo() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ {
+ return CDataCacheCore::GetInstance().GetTempo();
+ }
+ else
+ return 0;
+}
+
+bool CApplicationPlayer::SupportsTempo() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->SupportsTempo();
+ else
+ return false;
+}
+
+void CApplicationPlayer::FrameMove()
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ if (CDataCacheCore::GetInstance().IsPlayerStateChanged())
+ // CApplicationMessenger would be overhead because we are already in gui thread
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_STATE_CHANGED);
+ }
+}
+
+void CApplicationPlayer::Render(bool clear, uint32_t alpha, bool gui)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->Render(clear, alpha, gui);
+}
+
+void CApplicationPlayer::FlushRenderer()
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->FlushRenderer();
+}
+
+void CApplicationPlayer::SetRenderViewMode(int mode, float zoom, float par, float shift, bool stretch)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->SetRenderViewMode(mode, zoom, par, shift, stretch);
+}
+
+float CApplicationPlayer::GetRenderAspectRatio() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->GetRenderAspectRatio();
+ else
+ return 1.0;
+}
+
+void CApplicationPlayer::TriggerUpdateResolution()
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->TriggerUpdateResolution();
+}
+
+bool CApplicationPlayer::IsRenderingVideo() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->IsRenderingVideo();
+ else
+ return false;
+}
+
+bool CApplicationPlayer::IsRenderingGuiLayer() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return CServiceBroker::GetDataCacheCore().GetGuiRender();
+ else
+ return false;
+}
+
+bool CApplicationPlayer::IsRenderingVideoLayer() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return CServiceBroker::GetDataCacheCore().GetVideoRender();
+ else
+ return false;
+}
+
+bool CApplicationPlayer::Supports(EINTERLACEMETHOD method) const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->Supports(method);
+ else
+ return false;
+}
+
+EINTERLACEMETHOD CApplicationPlayer::GetDeinterlacingMethodDefault() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->GetDeinterlacingMethodDefault();
+ else
+ return EINTERLACEMETHOD::VS_INTERLACEMETHOD_NONE;
+}
+
+bool CApplicationPlayer::Supports(ESCALINGMETHOD method) const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->Supports(method);
+ else
+ return false;
+}
+
+bool CApplicationPlayer::Supports(ERENDERFEATURE feature) const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->Supports(feature);
+ else
+ return false;
+}
+
+unsigned int CApplicationPlayer::RenderCaptureAlloc()
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ return player->RenderCaptureAlloc();
+ else
+ return 0;
+}
+
+void CApplicationPlayer::RenderCapture(unsigned int captureId, unsigned int width, unsigned int height, int flags)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->RenderCapture(captureId, width, height, flags);
+}
+
+void CApplicationPlayer::RenderCaptureRelease(unsigned int captureId)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ player->RenderCaptureRelease(captureId);
+}
+
+bool CApplicationPlayer::RenderCaptureGetPixels(unsigned int captureId, unsigned int millis, uint8_t *buffer, unsigned int size)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ return player->RenderCaptureGetPixels(captureId, millis, buffer, size);
+ else
+ return false;
+}
+
+bool CApplicationPlayer::IsExternalPlaying() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ {
+ if (player->IsPlaying() && player->m_type == "external")
+ return true;
+ }
+ return false;
+}
+
+bool CApplicationPlayer::IsRemotePlaying() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ {
+ if (player->IsPlaying() && player->m_type == "remote")
+ return true;
+ }
+ return false;
+}
+
+CVideoSettings CApplicationPlayer::GetVideoSettings() const
+{
+ std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ {
+ return player->GetVideoSettings();
+ }
+ return CVideoSettings();
+}
+
+void CApplicationPlayer::SetVideoSettings(CVideoSettings& settings)
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ if (player)
+ {
+ return player->SetVideoSettings(settings);
+ }
+}
+
+CSeekHandler& CApplicationPlayer::GetSeekHandler()
+{
+ return m_seekHandler;
+}
+
+const CSeekHandler& CApplicationPlayer::GetSeekHandler() const
+{
+ return m_seekHandler;
+}
+
+void CApplicationPlayer::SetUpdateStreamDetails()
+{
+ std::shared_ptr<IPlayer> player = GetInternal();
+ CVideoPlayer* vp = dynamic_cast<CVideoPlayer*>(player.get());
+ if (vp)
+ vp->SetUpdateStreamDetails();
+}
+
+bool CApplicationPlayer::HasGameAgent() const
+{
+ const std::shared_ptr<const IPlayer> player = GetInternal();
+ if (player)
+ return player->HasGameAgent();
+
+ return false;
+}
+
+int CApplicationPlayer::GetSubtitleDelay() const
+{
+ // converts subtitle delay to a percentage
+ const auto& advSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+ const auto delay = this->GetVideoSettings().m_SubtitleDelay;
+ const auto range = advSettings->m_videoSubsDelayRange;
+ return static_cast<int>(0.5f + (delay + range) / (2.f * range) * 100.0f);
+}
+
+int CApplicationPlayer::GetAudioDelay() const
+{
+ // converts audio delay to a percentage
+ const auto& advSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+ const auto delay = this->GetVideoSettings().m_AudioDelay;
+ const auto range = advSettings->m_videoAudioDelayRange;
+ return static_cast<int>(0.5f + (delay + range) / (2.f * range) * 100.0f);
+}
diff --git a/xbmc/application/ApplicationPlayer.h b/xbmc/application/ApplicationPlayer.h
new file mode 100644
index 0000000..f760936
--- /dev/null
+++ b/xbmc/application/ApplicationPlayer.h
@@ -0,0 +1,205 @@
+/*
+ * 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 "SeekHandler.h"
+#include "application/IApplicationComponent.h"
+#include "cores/IPlayer.h"
+#include "cores/MenuType.h"
+#include "playlists/PlayListTypes.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CAction;
+class CPlayerCoreFactory;
+class CPlayerOptions;
+class CStreamDetails;
+
+struct AudioStreamInfo;
+struct VideoStreamInfo;
+struct SubtitleStreamInfo;
+struct TextCacheStruct_t;
+
+class CApplicationPlayer : public IApplicationComponent
+{
+public:
+ CApplicationPlayer() = default;
+
+ // player management
+ void ClosePlayer();
+ void ResetPlayer();
+ std::string GetCurrentPlayer() const;
+ float GetPlaySpeed() const;
+ float GetPlayTempo() const;
+ bool HasPlayer() const;
+ bool OpenFile(const CFileItem& item, const CPlayerOptions& options,
+ const CPlayerCoreFactory &factory,
+ const std::string &playerName, IPlayerCallback& callback);
+ void OpenNext(const CPlayerCoreFactory &factory);
+ void SetPlaySpeed(float speed);
+ void SetTempo(float tempo);
+ void FrameAdvance(int frames);
+
+ void FrameMove();
+ void Render(bool clear, uint32_t alpha = 255, bool gui = true);
+ void FlushRenderer();
+ void SetRenderViewMode(int mode, float zoom, float par, float shift, bool stretch);
+ float GetRenderAspectRatio() const;
+ void TriggerUpdateResolution();
+ bool IsRenderingVideo() const;
+ bool IsRenderingGuiLayer() const;
+ bool IsRenderingVideoLayer() const;
+ bool Supports(EINTERLACEMETHOD method) const;
+ EINTERLACEMETHOD GetDeinterlacingMethodDefault() const;
+ bool Supports(ESCALINGMETHOD method) const;
+ bool Supports(ERENDERFEATURE feature) const;
+ unsigned int RenderCaptureAlloc();
+ void RenderCapture(unsigned int captureId, unsigned int width, unsigned int height, int flags = 0);
+ void RenderCaptureRelease(unsigned int captureId);
+ bool RenderCaptureGetPixels(unsigned int captureId, unsigned int millis, uint8_t *buffer, unsigned int size);
+ bool IsExternalPlaying() const;
+ bool IsRemotePlaying() const;
+
+ // proxy calls
+ void AddSubtitle(const std::string& strSubPath);
+ bool CanPause() const;
+ bool CanSeek() const;
+ void DoAudioWork();
+ int GetAudioDelay() const;
+ void GetAudioCapabilities(std::vector<int>& audioCaps) const;
+ int GetAudioStream();
+ int GetAudioStreamCount() const;
+ void GetAudioStreamInfo(int index, AudioStreamInfo& info) const;
+ int GetCacheLevel() const;
+ float GetCachePercentage() const;
+ int GetChapterCount() const;
+ int GetChapter() const;
+ void GetChapterName(std::string& strChapterName, int chapterIdx = -1) const;
+ int64_t GetChapterPos(int chapterIdx = -1) const;
+ float GetPercentage() const;
+ std::string GetPlayerState();
+ PLAYLIST::Id GetPreferredPlaylist() const;
+ int GetSubtitleDelay() const;
+ int GetSubtitle();
+ void GetSubtitleCapabilities(std::vector<int>& subCaps) const;
+ int GetSubtitleCount() const;
+ void GetSubtitleStreamInfo(int index, SubtitleStreamInfo& info) const;
+ bool GetSubtitleVisible() const;
+ bool HasTeletextCache() const;
+ std::shared_ptr<TextCacheStruct_t> GetTeletextCache();
+ int64_t GetTime() const;
+ int64_t GetMinTime() const;
+ int64_t GetMaxTime() const;
+ time_t GetStartTime() const;
+ int64_t GetTotalTime() const;
+ int GetVideoStream();
+ int GetVideoStreamCount() const;
+ void GetVideoStreamInfo(int streamId, VideoStreamInfo& info) const;
+ int GetPrograms(std::vector<ProgramInfo>& programs);
+ void SetProgram(int progId);
+ int GetProgramsCount() const;
+ bool HasAudio() const;
+
+ /*!
+ * \brief Get the supported menu type
+ * \return The supported menu type
+ */
+ MenuType GetSupportedMenuType() const;
+
+ bool HasVideo() const;
+ bool HasGame() const;
+ bool HasRDS() const;
+ bool IsCaching() const;
+ bool IsInMenu() const;
+ bool IsPaused() const;
+ bool IsPausedPlayback() const;
+ bool IsPassthrough() const;
+ bool IsPlaying() const;
+ bool IsPlayingAudio() const;
+ bool IsPlayingVideo() const;
+ bool IsPlayingGame() const;
+ bool IsPlayingRDS() const;
+ void LoadPage(int p, int sp, unsigned char* buffer);
+ bool OnAction(const CAction &action);
+ void OnNothingToQueueNotify();
+ void Pause();
+ bool QueueNextFile(const CFileItem &file);
+ void Seek(bool bPlus = true, bool bLargeStep = false, bool bChapterOverride = false);
+ int SeekChapter(int iChapter);
+ void SeekPercentage(float fPercent = 0);
+ bool SeekScene(bool bPlus = true);
+ void SeekTime(int64_t iTime = 0);
+ void SeekTimeRelative(int64_t iTime = 0);
+ void SetAudioStream(int iStream);
+ void SetAVDelay(float fValue = 0.0f);
+ void SetDynamicRangeCompression(long drc);
+ void SetMute(bool bOnOff);
+ bool SetPlayerState(const std::string& state);
+ void SetSubtitle(int iStream);
+ void SetSubTitleDelay(float fValue = 0.0f);
+ void SetSubtitleVisible(bool bVisible);
+
+ /*!
+ * \brief Set the subtitle vertical position,
+ * it depends on current screen resolution
+ * \param value The subtitle position in pixels
+ * \param save If true, the value will be saved to resolution info
+ */
+ void SetSubtitleVerticalPosition(const int value, bool save);
+
+ void SetTime(int64_t time);
+ void SetTotalTime(int64_t time);
+ void SetVideoStream(int iStream);
+ void SetVolume(float volume);
+ void SetSpeed(float speed);
+ bool SupportsTempo() const;
+
+ CVideoSettings GetVideoSettings() const;
+ void SetVideoSettings(CVideoSettings& settings);
+
+ CSeekHandler& GetSeekHandler();
+ const CSeekHandler& GetSeekHandler() const;
+
+ void SetUpdateStreamDetails();
+
+ /*!
+ * \copydoc IPlayer::HasGameAgent
+ */
+ bool HasGameAgent() const;
+
+private:
+ std::shared_ptr<const IPlayer> GetInternal() const;
+ std::shared_ptr<IPlayer> GetInternal();
+ void CreatePlayer(const CPlayerCoreFactory &factory, const std::string &player, IPlayerCallback& callback);
+ void CloseFile(bool reopen = false);
+
+ std::shared_ptr<IPlayer> m_pPlayer;
+ mutable CCriticalSection m_playerLock;
+ CSeekHandler m_seekHandler;
+
+ // cache player state
+ XbmcThreads::EndTime<> m_audioStreamUpdate;
+ int m_iAudioStream;
+ XbmcThreads::EndTime<> m_videoStreamUpdate;
+ int m_iVideoStream;
+ XbmcThreads::EndTime<> m_subtitleStreamUpdate;
+ int m_iSubtitleStream;
+
+ struct SNextItem
+ {
+ std::shared_ptr<CFileItem> pItem;
+ CPlayerOptions options = {};
+ std::string playerName;
+ IPlayerCallback *callback = nullptr;
+ } m_nextItem;
+};
diff --git a/xbmc/application/ApplicationPlayerCallback.cpp b/xbmc/application/ApplicationPlayerCallback.cpp
new file mode 100644
index 0000000..0dbb4b9
--- /dev/null
+++ b/xbmc/application/ApplicationPlayerCallback.cpp
@@ -0,0 +1,346 @@
+/*
+ * 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 "ApplicationPlayerCallback.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationStackHelper.h"
+#include "cores/DataCacheCore.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/StereoscopicsManager.h"
+#include "interfaces/AnnouncementManager.h"
+#include "interfaces/json-rpc/JSONUtils.h"
+#include "interfaces/python/XBPython.h"
+#include "profiles/ProfileManager.h"
+#include "pvr/PVRManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SaveFileStateJob.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoInfoTag.h"
+
+CApplicationPlayerCallback::CApplicationPlayerCallback()
+ : m_itemCurrentFile(new CFileItem), m_playerEvent(true, true)
+{
+}
+
+void CApplicationPlayerCallback::OnPlayBackEnded()
+{
+ CLog::LogF(LOGDEBUG, "CApplicationPlayerCallback::OnPlayBackEnded");
+
+ CServiceBroker::GetPVRManager().OnPlaybackEnded(*m_itemCurrentFile);
+
+ CVariant data(CVariant::VariantTypeObject);
+ data["end"] = true;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnStop",
+ m_itemCurrentFile, data);
+
+ CGUIMessage msg(GUI_MSG_PLAYBACK_ENDED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CApplicationPlayerCallback::OnPlayBackStarted(const CFileItem& file)
+{
+ CLog::LogF(LOGDEBUG, "CApplication::OnPlayBackStarted");
+
+ // check if VideoPlayer should set file item stream details from its current streams
+ const bool isBlu_dvd_image_or_stream = (URIUtils::IsBluray(file.GetPath()) || file.IsDVDFile() ||
+ file.IsDiscImage() || file.IsInternetStream());
+
+ const bool hasNoStreamDetails =
+ (!file.HasVideoInfoTag() || !file.GetVideoInfoTag()->HasStreamDetails());
+
+ if (file.GetProperty("get_stream_details_from_player").asBoolean() ||
+ (hasNoStreamDetails && isBlu_dvd_image_or_stream))
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->SetUpdateStreamDetails();
+ }
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto stackHelper = components.GetComponent<CApplicationStackHelper>();
+ if (stackHelper->IsPlayingISOStack() || stackHelper->IsPlayingRegularStack())
+ m_itemCurrentFile.reset(new CFileItem(*stackHelper->GetRegisteredStack(file)));
+ else
+ m_itemCurrentFile.reset(new CFileItem(file));
+
+ /* When playing video pause any low priority jobs, they will be unpaused when playback stops.
+ * This should speed up player startup for files on internet filesystems (eg. webdav) and
+ * increase performance on low powered systems (Atom/ARM).
+ */
+ if (file.IsVideo() || file.IsGame())
+ {
+ CServiceBroker::GetJobManager()->PauseJobs();
+ }
+
+ CServiceBroker::GetPVRManager().OnPlaybackStarted(*m_itemCurrentFile);
+ stackHelper->OnPlayBackStarted(file);
+
+ m_playerEvent.Reset();
+
+ CGUIMessage msg(GUI_MSG_PLAYBACK_STARTED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CApplicationPlayerCallback::OnPlayerCloseFile(const CFileItem& file,
+ const CBookmark& bookmarkParam)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto stackHelper = components.GetComponent<CApplicationStackHelper>();
+
+ std::unique_lock<CCriticalSection> lock(stackHelper->m_critSection);
+
+ CFileItem fileItem(file);
+ CBookmark bookmark = bookmarkParam;
+ CBookmark resumeBookmark;
+ bool playCountUpdate = false;
+ float percent = 0.0f;
+
+ // Make sure we don't reset existing bookmark etc. on eg. player start failure
+ if (bookmark.timeInSeconds == 0.0)
+ return;
+
+ if (stackHelper->GetRegisteredStack(fileItem) != nullptr &&
+ stackHelper->GetRegisteredStackTotalTimeMs(fileItem) > 0)
+ {
+ // regular stack case: we have to save the bookmark on the stack
+ fileItem = *stackHelper->GetRegisteredStack(file);
+ // the bookmark coming from the player is only relative to the current part, thus needs to be corrected with these attributes (start time will be 0 for non-stackparts)
+ bookmark.timeInSeconds += stackHelper->GetRegisteredStackPartStartTimeMs(file) / 1000.0;
+ if (stackHelper->GetRegisteredStackTotalTimeMs(file) > 0)
+ bookmark.totalTimeInSeconds = stackHelper->GetRegisteredStackTotalTimeMs(file) / 1000.0;
+ bookmark.partNumber = stackHelper->GetRegisteredStackPartNumber(file);
+ }
+
+ percent = bookmark.timeInSeconds / bookmark.totalTimeInSeconds * 100;
+
+ const std::shared_ptr<CAdvancedSettings> advancedSettings =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ if ((fileItem.IsAudio() && advancedSettings->m_audioPlayCountMinimumPercent > 0 &&
+ percent >= advancedSettings->m_audioPlayCountMinimumPercent) ||
+ (fileItem.IsVideo() && advancedSettings->m_videoPlayCountMinimumPercent > 0 &&
+ percent >= advancedSettings->m_videoPlayCountMinimumPercent))
+ {
+ playCountUpdate = true;
+ }
+
+ if (advancedSettings->m_videoIgnorePercentAtEnd > 0 &&
+ bookmark.totalTimeInSeconds - bookmark.timeInSeconds <
+ 0.01 * static_cast<double>(advancedSettings->m_videoIgnorePercentAtEnd) *
+ bookmark.totalTimeInSeconds)
+ {
+ resumeBookmark.timeInSeconds = -1.0;
+ }
+ else if (bookmark.timeInSeconds > advancedSettings->m_videoIgnoreSecondsAtStart)
+ {
+ resumeBookmark = bookmark;
+ if (stackHelper->GetRegisteredStack(file) != nullptr)
+ {
+ // also update video info tag with total time
+ fileItem.GetVideoInfoTag()->m_streamDetails.SetVideoDuration(
+ 0, resumeBookmark.totalTimeInSeconds);
+ }
+ }
+ else
+ {
+ resumeBookmark.timeInSeconds = 0.0;
+ }
+
+ if (CServiceBroker::GetSettingsComponent()
+ ->GetProfileManager()
+ ->GetCurrentProfile()
+ .canWriteDatabases())
+ {
+ CSaveFileState::DoWork(fileItem, resumeBookmark, playCountUpdate);
+ }
+}
+
+void CApplicationPlayerCallback::OnPlayBackPaused()
+{
+#ifdef HAS_PYTHON
+ CServiceBroker::GetXBPython().OnPlayBackPaused();
+#endif
+
+ CVariant param;
+ param["player"]["speed"] = 0;
+ param["player"]["playerid"] = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPause",
+ m_itemCurrentFile, param);
+}
+
+void CApplicationPlayerCallback::OnPlayBackResumed()
+{
+#ifdef HAS_PYTHON
+ CServiceBroker::GetXBPython().OnPlayBackResumed();
+#endif
+
+ CVariant param;
+ param["player"]["speed"] = 1;
+ param["player"]["playerid"] = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnResume",
+ m_itemCurrentFile, param);
+}
+
+void CApplicationPlayerCallback::OnPlayBackStopped()
+{
+ CLog::LogF(LOGDEBUG, "CApplication::OnPlayBackStopped");
+
+ CServiceBroker::GetPVRManager().OnPlaybackStopped(*m_itemCurrentFile);
+
+ CVariant data(CVariant::VariantTypeObject);
+ data["end"] = false;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnStop",
+ m_itemCurrentFile, data);
+
+ CGUIMessage msg(GUI_MSG_PLAYBACK_STOPPED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CApplicationPlayerCallback::OnPlayBackError()
+{
+ //@todo Playlists can be continued by calling OnPlaybackEnded instead
+ // open error dialog
+ CGUIMessage msg(GUI_MSG_PLAYBACK_ERROR, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ OnPlayBackStopped();
+}
+
+void CApplicationPlayerCallback::OnQueueNextItem()
+{
+ CLog::LogF(LOGDEBUG, "CApplication::OnQueueNextItem");
+
+ // informs python script currently running that we are requesting the next track
+ // (does nothing if python is not loaded)
+#ifdef HAS_PYTHON
+ CServiceBroker::GetXBPython().OnQueueNextItem(); // currently unimplemented
+#endif
+
+ CGUIMessage msg(GUI_MSG_QUEUE_NEXT_ITEM, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CApplicationPlayerCallback::OnPlayBackSeek(int64_t iTime, int64_t seekOffset)
+{
+#ifdef HAS_PYTHON
+ CServiceBroker::GetXBPython().OnPlayBackSeek(static_cast<int>(iTime),
+ static_cast<int>(seekOffset));
+#endif
+
+ CVariant param;
+ JSONRPC::CJSONUtils::MillisecondsToTimeObject(iTime, param["player"]["time"]);
+ JSONRPC::CJSONUtils::MillisecondsToTimeObject(seekOffset, param["player"]["seekoffset"]);
+ param["player"]["playerid"] = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ param["player"]["speed"] = static_cast<int>(appPlayer->GetPlaySpeed());
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnSeek",
+ m_itemCurrentFile, param);
+
+ CDataCacheCore::GetInstance().SeekFinished(static_cast<int>(seekOffset));
+}
+
+void CApplicationPlayerCallback::OnPlayBackSeekChapter(int iChapter)
+{
+#ifdef HAS_PYTHON
+ CServiceBroker::GetXBPython().OnPlayBackSeekChapter(iChapter);
+#endif
+}
+
+void CApplicationPlayerCallback::OnPlayBackSpeedChanged(int iSpeed)
+{
+#ifdef HAS_PYTHON
+ CServiceBroker::GetXBPython().OnPlayBackSpeedChanged(iSpeed);
+#endif
+
+ CVariant param;
+ param["player"]["speed"] = iSpeed;
+ param["player"]["playerid"] = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnSpeedChanged",
+ m_itemCurrentFile, param);
+}
+
+void CApplicationPlayerCallback::OnAVChange()
+{
+ CLog::LogF(LOGDEBUG, "CApplication::OnAVChange");
+
+ CServiceBroker::GetGUI()->GetStereoscopicsManager().OnStreamChange();
+
+ CGUIMessage msg(GUI_MSG_PLAYBACK_AVCHANGE, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+
+ CVariant param;
+ param["player"]["speed"] = 1;
+ param["player"]["playerid"] = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnAVChange",
+ m_itemCurrentFile, param);
+}
+
+void CApplicationPlayerCallback::OnAVStarted(const CFileItem& file)
+{
+ CLog::LogF(LOGDEBUG, "CApplication::OnAVStarted");
+
+ CGUIMessage msg(GUI_MSG_PLAYBACK_AVSTARTED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+
+ CVariant param;
+ param["player"]["speed"] = 1;
+ param["player"]["playerid"] = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnAVStart",
+ m_itemCurrentFile, param);
+}
+
+void CApplicationPlayerCallback::RequestVideoSettings(const CFileItem& fileItem)
+{
+ CVideoDatabase dbs;
+ if (dbs.Open())
+ {
+ CLog::Log(LOGDEBUG, "Loading settings for {}", CURL::GetRedacted(fileItem.GetPath()));
+
+ // Load stored settings if they exist, otherwise use default
+ CVideoSettings vs;
+ if (!dbs.GetVideoSettings(fileItem, vs))
+ vs = CMediaSettings::GetInstance().GetDefaultVideoSettings();
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->SetVideoSettings(vs);
+
+ dbs.Close();
+ }
+}
+
+void CApplicationPlayerCallback::StoreVideoSettings(const CFileItem& fileItem,
+ const CVideoSettings& vs)
+{
+ CVideoDatabase dbs;
+ if (dbs.Open())
+ {
+ if (vs != CMediaSettings::GetInstance().GetDefaultVideoSettings())
+ {
+ dbs.SetVideoSettings(fileItem, vs);
+ }
+ else
+ {
+ dbs.EraseVideoSettings(fileItem);
+ }
+ dbs.Close();
+ }
+}
diff --git a/xbmc/application/ApplicationPlayerCallback.h b/xbmc/application/ApplicationPlayerCallback.h
new file mode 100644
index 0000000..8cfd40b
--- /dev/null
+++ b/xbmc/application/ApplicationPlayerCallback.h
@@ -0,0 +1,43 @@
+/*
+ * 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/IPlayerCallback.h"
+#include "threads/Event.h"
+
+#include <memory>
+
+class CApplicationStackHelper;
+class CFileItem;
+
+class CApplicationPlayerCallback : public IPlayerCallback
+{
+public:
+ CApplicationPlayerCallback();
+
+ void OnPlayBackEnded() override;
+ void OnPlayBackStarted(const CFileItem& file) override;
+ void OnPlayerCloseFile(const CFileItem& file, const CBookmark& bookmark) override;
+ void OnPlayBackPaused() override;
+ void OnPlayBackResumed() override;
+ void OnPlayBackStopped() override;
+ void OnPlayBackError() override;
+ void OnQueueNextItem() override;
+ void OnPlayBackSeek(int64_t iTime, int64_t seekOffset) override;
+ void OnPlayBackSeekChapter(int iChapter) override;
+ void OnPlayBackSpeedChanged(int iSpeed) override;
+ void OnAVChange() override;
+ void OnAVStarted(const CFileItem& file) override;
+ void RequestVideoSettings(const CFileItem& fileItem) override;
+ void StoreVideoSettings(const CFileItem& fileItem, const CVideoSettings& vs) override;
+
+protected:
+ std::shared_ptr<CFileItem> m_itemCurrentFile; //!< Currently playing file
+ CEvent m_playerEvent;
+};
diff --git a/xbmc/application/ApplicationPowerHandling.cpp b/xbmc/application/ApplicationPowerHandling.cpp
new file mode 100644
index 0000000..ea182d5
--- /dev/null
+++ b/xbmc/application/ApplicationPowerHandling.cpp
@@ -0,0 +1,598 @@
+/*
+ * 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 "ApplicationPowerHandling.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "music/MusicLibraryQueue.h"
+#include "powermanagement/DPMSSupport.h"
+#include "powermanagement/PowerTypes.h"
+#include "profiles/ProfileManager.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsPowerManagement.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/AlarmClock.h"
+#include "utils/log.h"
+#include "video/VideoLibraryQueue.h"
+#include "windowing/WinSystem.h"
+
+void CApplicationPowerHandling::ResetScreenSaver()
+{
+ // reset our timers
+ m_shutdownTimer.StartZero();
+
+ // screen saver timer is reset only if we're not already in screensaver or
+ // DPMS mode
+ if ((!m_screensaverActive && m_iScreenSaveLock == 0) && !m_dpmsIsActive)
+ ResetScreenSaverTimer();
+}
+
+void CApplicationPowerHandling::ResetScreenSaverTimer()
+{
+ m_screenSaverTimer.StartZero();
+}
+
+void CApplicationPowerHandling::ResetSystemIdleTimer()
+{
+ // reset system idle timer
+ m_idleTimer.StartZero();
+}
+
+void CApplicationPowerHandling::ResetNavigationTimer()
+{
+ m_navigationTimer.StartZero();
+}
+
+void CApplicationPowerHandling::SetRenderGUI(bool renderGUI)
+{
+ if (renderGUI && !m_renderGUI)
+ {
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ CServiceBroker::GetGUI()->GetWindowManager().MarkDirty();
+ }
+ m_renderGUI = renderGUI;
+}
+
+void CApplicationPowerHandling::StopScreenSaverTimer()
+{
+ m_screenSaverTimer.Stop();
+}
+
+bool CApplicationPowerHandling::ToggleDPMS(bool manual)
+{
+ auto winSystem = CServiceBroker::GetWinSystem();
+ if (!winSystem)
+ return false;
+
+ std::shared_ptr<CDPMSSupport> dpms = winSystem->GetDPMSManager();
+ if (!dpms)
+ return false;
+
+ if (manual || (m_dpmsIsManual == manual))
+ {
+ if (m_dpmsIsActive)
+ {
+ m_dpmsIsActive = false;
+ m_dpmsIsManual = false;
+ SetRenderGUI(true);
+ CheckOSScreenSaverInhibitionSetting();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::GUI, "OnDPMSDeactivated");
+ return dpms->DisablePowerSaving();
+ }
+ else
+ {
+ if (dpms->EnablePowerSaving(dpms->GetSupportedModes()[0]))
+ {
+ m_dpmsIsActive = true;
+ m_dpmsIsManual = manual;
+ SetRenderGUI(false);
+ CheckOSScreenSaverInhibitionSetting();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::GUI, "OnDPMSActivated");
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+bool CApplicationPowerHandling::WakeUpScreenSaverAndDPMS(bool bPowerOffKeyPressed /* = false */)
+{
+ bool result = false;
+
+ // First reset DPMS, if active
+ if (m_dpmsIsActive)
+ {
+ if (m_dpmsIsManual)
+ return false;
+ //! @todo if screensaver lock is specified but screensaver is not active
+ //! (DPMS came first), activate screensaver now.
+ ToggleDPMS(false);
+ ResetScreenSaverTimer();
+ result = !m_screensaverActive || WakeUpScreenSaver(bPowerOffKeyPressed);
+ }
+ else if (m_screensaverActive)
+ result = WakeUpScreenSaver(bPowerOffKeyPressed);
+
+ if (result)
+ {
+ // allow listeners to ignore the deactivation if it precedes a powerdown/suspend etc
+ CVariant data(CVariant::VariantTypeObject);
+ data["shuttingdown"] = bPowerOffKeyPressed;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::GUI,
+ "OnScreensaverDeactivated", data);
+ }
+
+ return result;
+}
+
+bool CApplicationPowerHandling::WakeUpScreenSaver(bool bPowerOffKeyPressed /* = false */)
+{
+ if (m_iScreenSaveLock == 2)
+ return false;
+
+ // if Screen saver is active
+ if (m_screensaverActive && !m_screensaverIdInUse.empty())
+ {
+ if (m_iScreenSaveLock == 0)
+ {
+ const std::shared_ptr<CProfileManager> profileManager =
+ CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ if (profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE &&
+ (profileManager->UsingLoginScreen() ||
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MASTERLOCK_STARTUPLOCK)) &&
+ profileManager->GetCurrentProfile().getLockMode() != LOCK_MODE_EVERYONE &&
+ m_screensaverIdInUse != "screensaver.xbmc.builtin.dim" &&
+ m_screensaverIdInUse != "screensaver.xbmc.builtin.black" &&
+ m_screensaverIdInUse != "visualization")
+ {
+ m_iScreenSaveLock = 2;
+ CGUIMessage msg(GUI_MSG_CHECK_LOCK, 0, 0);
+
+ CGUIWindow* pWindow =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_SCREENSAVER);
+ if (pWindow)
+ pWindow->OnMessage(msg);
+ }
+ }
+ if (m_iScreenSaveLock == -1)
+ {
+ m_iScreenSaveLock = 0;
+ return true;
+ }
+
+ // disable screensaver
+ m_screensaverActive = false;
+ m_iScreenSaveLock = 0;
+ ResetScreenSaverTimer();
+
+ if (m_screensaverIdInUse == "visualization")
+ {
+ // we can just continue as usual from vis mode
+ return false;
+ }
+ else if (m_screensaverIdInUse == "screensaver.xbmc.builtin.dim" ||
+ m_screensaverIdInUse == "screensaver.xbmc.builtin.black" ||
+ m_screensaverIdInUse.empty())
+ {
+ return true;
+ }
+ else
+ { // we're in screensaver window
+ if (m_pythonScreenSaver)
+ {
+// What sound does a python screensaver make?
+#define SCRIPT_ALARM "sssssscreensaver"
+#define SCRIPT_TIMEOUT 15 // seconds
+
+ /* FIXME: This is a hack but a proper fix is non-trivial. Basically this code
+ * makes sure the addon gets terminated after we've moved out of the screensaver window.
+ * If we don't do this, we may simply lockup.
+ */
+ g_alarmClock.Start(SCRIPT_ALARM, SCRIPT_TIMEOUT,
+ "StopScript(" + m_pythonScreenSaver->LibPath() + ")", true, false);
+ m_pythonScreenSaver.reset();
+ }
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SCREENSAVER)
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow(); // show the previous window
+ else if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_STOP)));
+ }
+ return true;
+ }
+ else
+ return false;
+}
+
+void CApplicationPowerHandling::CheckOSScreenSaverInhibitionSetting()
+{
+ // Kodi screen saver overrides OS one: always inhibit OS screen saver then
+ // except when DPMS is active (inhibiting the screen saver then might also
+ // disable DPMS again)
+ if (!m_dpmsIsActive &&
+ !CServiceBroker::GetSettingsComponent()
+ ->GetSettings()
+ ->GetString(CSettings::SETTING_SCREENSAVER_MODE)
+ .empty() &&
+ CServiceBroker::GetWinSystem()->GetOSScreenSaver())
+ {
+ if (!m_globalScreensaverInhibitor)
+ {
+ m_globalScreensaverInhibitor =
+ CServiceBroker::GetWinSystem()->GetOSScreenSaver()->CreateInhibitor();
+ }
+ }
+ else if (m_globalScreensaverInhibitor)
+ {
+ m_globalScreensaverInhibitor.Release();
+ }
+}
+
+void CApplicationPowerHandling::CheckScreenSaverAndDPMS()
+{
+ bool maybeScreensaver = true;
+ if (m_dpmsIsActive)
+ maybeScreensaver = false;
+ else if (m_screensaverActive)
+ maybeScreensaver = false;
+ else if (CServiceBroker::GetSettingsComponent()
+ ->GetSettings()
+ ->GetString(CSettings::SETTING_SCREENSAVER_MODE)
+ .empty())
+ maybeScreensaver = false;
+
+ auto winSystem = CServiceBroker::GetWinSystem();
+ if (!winSystem)
+ return;
+
+ std::shared_ptr<CDPMSSupport> dpms = winSystem->GetDPMSManager();
+
+ bool maybeDPMS = true;
+ if (m_dpmsIsActive)
+ maybeDPMS = false;
+ else if (!dpms || !dpms->IsSupported())
+ maybeDPMS = false;
+ else if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_POWERMANAGEMENT_DISPLAYSOFF) <= 0)
+ maybeDPMS = false;
+
+ // whether the current state of the application should be regarded as active even when there is no
+ // explicit user activity such as input
+ bool haveIdleActivity = false;
+
+ if (m_bResetScreenSaver)
+ {
+ m_bResetScreenSaver = false;
+ haveIdleActivity = true;
+ }
+
+ // When inhibit screensaver is enabled prevent screensaver from kicking in
+ if (m_bInhibitScreenSaver)
+ haveIdleActivity = true;
+
+ // Are we playing a video and it is not paused?
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer && appPlayer->IsPlayingVideo() && !appPlayer->IsPaused())
+ haveIdleActivity = true;
+
+ // Are we playing some music in fullscreen vis?
+ else if (appPlayer && appPlayer->IsPlayingAudio() &&
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VISUALISATION &&
+ !CServiceBroker::GetSettingsComponent()
+ ->GetSettings()
+ ->GetString(CSettings::SETTING_MUSICPLAYER_VISUALISATION)
+ .empty())
+ {
+ haveIdleActivity = true;
+ }
+
+ // Handle OS screen saver state
+ if (haveIdleActivity && CServiceBroker::GetWinSystem()->GetOSScreenSaver())
+ {
+ // Always inhibit OS screen saver during these kinds of activities
+ if (!m_screensaverInhibitor)
+ {
+ m_screensaverInhibitor =
+ CServiceBroker::GetWinSystem()->GetOSScreenSaver()->CreateInhibitor();
+ }
+ }
+ else if (m_screensaverInhibitor)
+ {
+ m_screensaverInhibitor.Release();
+ }
+
+ // Has the screen saver window become active?
+ if (maybeScreensaver &&
+ CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_SCREENSAVER))
+ {
+ m_screensaverActive = true;
+ maybeScreensaver = false;
+ }
+
+ if (m_screensaverActive && haveIdleActivity)
+ {
+ WakeUpScreenSaverAndDPMS();
+ return;
+ }
+
+ if (!maybeScreensaver && !maybeDPMS)
+ return; // Nothing to do.
+
+ // See if we need to reset timer.
+ if (haveIdleActivity)
+ {
+ ResetScreenSaverTimer();
+ return;
+ }
+
+ float elapsed = m_screenSaverTimer.IsRunning() ? m_screenSaverTimer.GetElapsedSeconds() : 0.f;
+
+ // DPMS has priority (it makes the screensaver not needed)
+ if (maybeDPMS && elapsed > CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_POWERMANAGEMENT_DISPLAYSOFF) *
+ 60)
+ {
+ ToggleDPMS(false);
+ WakeUpScreenSaver();
+ }
+ else if (maybeScreensaver &&
+ elapsed > CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_SCREENSAVER_TIME) *
+ 60)
+ {
+ ActivateScreenSaver();
+ }
+}
+
+// activate the screensaver.
+// if forceType is true, we ignore the various conditions that can alter
+// the type of screensaver displayed
+void CApplicationPowerHandling::ActivateScreenSaver(bool forceType /*= false */)
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer && appPlayer->IsPlayingAudio() &&
+ settings->GetBool(CSettings::SETTING_SCREENSAVER_USEMUSICVISINSTEAD) &&
+ !settings->GetString(CSettings::SETTING_MUSICPLAYER_VISUALISATION).empty())
+ { // just activate the visualisation if user toggled the usemusicvisinstead option
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VISUALISATION);
+ return;
+ }
+
+ m_screensaverActive = true;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::GUI, "OnScreensaverActivated");
+
+ // disable screensaver lock from the login screen
+ m_iScreenSaveLock =
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_LOGIN_SCREEN ? 1 : 0;
+
+ m_screensaverIdInUse = settings->GetString(CSettings::SETTING_SCREENSAVER_MODE);
+
+ if (!forceType)
+ {
+ if (m_screensaverIdInUse == "screensaver.xbmc.builtin.dim" ||
+ m_screensaverIdInUse == "screensaver.xbmc.builtin.black" || m_screensaverIdInUse.empty())
+ {
+ return;
+ }
+
+ // Enforce Dim for special cases.
+ bool bUseDim = false;
+ if (CServiceBroker::GetGUI()->GetWindowManager().HasModalDialog(true))
+ bUseDim = true;
+ else if (appPlayer && appPlayer->IsPlayingVideo() &&
+ settings->GetBool(CSettings::SETTING_SCREENSAVER_USEDIMONPAUSE))
+ bUseDim = true;
+ else if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().IsRunningChannelScan())
+ bUseDim = true;
+
+ if (bUseDim)
+ m_screensaverIdInUse = "screensaver.xbmc.builtin.dim";
+ }
+
+ if (m_screensaverIdInUse == "screensaver.xbmc.builtin.dim" ||
+ m_screensaverIdInUse == "screensaver.xbmc.builtin.black" || m_screensaverIdInUse.empty())
+ {
+ return;
+ }
+ else if (CServiceBroker::GetAddonMgr().GetAddon(m_screensaverIdInUse, m_pythonScreenSaver,
+ ADDON::AddonType::SCREENSAVER,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ std::string libPath = m_pythonScreenSaver->LibPath();
+ if (CScriptInvocationManager::GetInstance().HasLanguageInvoker(libPath))
+ {
+ CLog::Log(LOGDEBUG, "using python screensaver add-on {}", m_screensaverIdInUse);
+
+ // Don't allow a previously-scheduled alarm to kill our new screensaver
+ g_alarmClock.Stop(SCRIPT_ALARM, true);
+
+ if (!CScriptInvocationManager::GetInstance().Stop(libPath))
+ CScriptInvocationManager::GetInstance().ExecuteAsync(
+ libPath,
+ ADDON::AddonPtr(new ADDON::CAddon(dynamic_cast<ADDON::CAddon&>(*m_pythonScreenSaver))));
+ return;
+ }
+ m_pythonScreenSaver.reset();
+ }
+
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SCREENSAVER);
+}
+
+void CApplicationPowerHandling::InhibitScreenSaver(bool inhibit)
+{
+ m_bInhibitScreenSaver = inhibit;
+}
+
+bool CApplicationPowerHandling::IsScreenSaverInhibited() const
+{
+ return m_bInhibitScreenSaver;
+}
+
+// Global Idle Time in Seconds
+// idle time will be reset if on any OnKey()
+// int return: system Idle time in seconds! 0 is no idle!
+int CApplicationPowerHandling::GlobalIdleTime()
+{
+ if (!m_idleTimer.IsRunning())
+ m_idleTimer.StartZero();
+ return (int)m_idleTimer.GetElapsedSeconds();
+}
+
+float CApplicationPowerHandling::NavigationIdleTime()
+{
+ if (!m_navigationTimer.IsRunning())
+ m_navigationTimer.StartZero();
+ return m_navigationTimer.GetElapsedSeconds();
+}
+
+void CApplicationPowerHandling::StopShutdownTimer()
+{
+ m_shutdownTimer.Stop();
+}
+
+void CApplicationPowerHandling::ResetShutdownTimers()
+{
+ // reset system shutdown timer
+ m_shutdownTimer.StartZero();
+
+ // delete custom shutdown timer
+ if (g_alarmClock.HasAlarm("shutdowntimer"))
+ g_alarmClock.Stop("shutdowntimer", true);
+}
+
+void CApplicationPowerHandling::HandleShutdownMessage()
+{
+ switch (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNSTATE))
+ {
+ case POWERSTATE_SHUTDOWN:
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_POWERDOWN);
+ break;
+
+ case POWERSTATE_SUSPEND:
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SUSPEND);
+ break;
+
+ case POWERSTATE_HIBERNATE:
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_HIBERNATE);
+ break;
+
+ case POWERSTATE_QUIT:
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+ break;
+
+ case POWERSTATE_MINIMIZE:
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MINIMIZE);
+ break;
+
+ default:
+ CLog::Log(LOGERROR, "{}: No valid shutdownstate matched", __FUNCTION__);
+ break;
+ }
+}
+
+void CApplicationPowerHandling::CheckShutdown()
+{
+ // first check if we should reset the timer
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer)
+ return;
+
+ if (m_bInhibitIdleShutdown || appPlayer->IsPlaying() ||
+ appPlayer->IsPausedPlayback() // is something playing?
+ || CMusicLibraryQueue::GetInstance().IsRunning() ||
+ CVideoLibraryQueue::GetInstance().IsRunning() ||
+ CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(
+ WINDOW_DIALOG_PROGRESS) // progress dialog is onscreen
+ ||
+ !CServiceBroker::GetPVRManager().Get<PVR::GUI::PowerManagement>().CanSystemPowerdown(false))
+ {
+ m_shutdownTimer.StartZero();
+ return;
+ }
+
+ float elapsed = m_shutdownTimer.IsRunning() ? m_shutdownTimer.GetElapsedSeconds() : 0.f;
+ if (elapsed > CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNTIME) *
+ 60)
+ {
+ // Since it is a sleep instead of a shutdown, let's set everything to reset when we wake up.
+ m_shutdownTimer.Stop();
+
+ // Sleep the box
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SHUTDOWN);
+ }
+}
+
+void CApplicationPowerHandling::InhibitIdleShutdown(bool inhibit)
+{
+ m_bInhibitIdleShutdown = inhibit;
+}
+
+bool CApplicationPowerHandling::IsIdleShutdownInhibited() const
+{
+ return m_bInhibitIdleShutdown;
+}
+
+bool CApplicationPowerHandling::OnSettingChanged(const CSetting& setting)
+{
+ const std::string& settingId = setting.GetId();
+
+ if (settingId == CSettings::SETTING_SCREENSAVER_MODE)
+ {
+ CheckOSScreenSaverInhibitionSetting();
+ }
+ else
+ return false;
+
+ return true;
+}
+
+bool CApplicationPowerHandling::OnSettingAction(const CSetting& setting)
+{
+ const std::string& settingId = setting.GetId();
+
+ if (settingId == CSettings::SETTING_SCREENSAVER_PREVIEW)
+ ActivateScreenSaver(true);
+ else if (settingId == CSettings::SETTING_SCREENSAVER_SETTINGS)
+ {
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_SCREENSAVER_MODE),
+ addon, ADDON::AddonType::SCREENSAVER, ADDON::OnlyEnabled::CHOICE_YES))
+ CGUIDialogAddonSettings::ShowForAddon(addon);
+ }
+ else
+ return false;
+
+ return true;
+}
diff --git a/xbmc/application/ApplicationPowerHandling.h b/xbmc/application/ApplicationPowerHandling.h
new file mode 100644
index 0000000..9d78c6e
--- /dev/null
+++ b/xbmc/application/ApplicationPowerHandling.h
@@ -0,0 +1,116 @@
+/*
+ * 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 "application/IApplicationComponent.h"
+
+#ifdef TARGET_WINDOWS
+#include "powermanagement/WinIdleTimer.h"
+#endif
+#include "utils/Stopwatch.h"
+#include "windowing/OSScreenSaver.h"
+
+#include <string>
+
+namespace ADDON
+{
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+} // namespace ADDON
+
+class CApplication;
+class CSetting;
+
+/*!
+ * \brief Class handling application support for screensavers, dpms and shutdown timers.
+ */
+
+class CApplicationPowerHandling : public IApplicationComponent
+{
+ friend class CApplication;
+
+public:
+ bool IsInScreenSaver() const { return m_screensaverActive; }
+ bool IsScreenSaverInhibited() const;
+ void ResetScreenSaver();
+ void SetScreenSaverLockFailed() { m_iScreenSaveLock = -1; }
+ void SetScreenSaverUnlocked() { m_iScreenSaveLock = 1; }
+ void StopScreenSaverTimer();
+ std::string ScreensaverIdInUse() const { return m_screensaverIdInUse; }
+
+ bool GetRenderGUI() const { return m_renderGUI; }
+ void SetRenderGUI(bool renderGUI);
+
+ int GlobalIdleTime();
+ void ResetSystemIdleTimer();
+ bool IsIdleShutdownInhibited() const;
+
+ void ResetShutdownTimers();
+ void StopShutdownTimer();
+
+ void ResetNavigationTimer();
+
+ bool IsDPMSActive() const { return m_dpmsIsActive; }
+ bool ToggleDPMS(bool manual);
+
+ // Wakes up from the screensaver and / or DPMS. Returns true if woken up.
+ bool WakeUpScreenSaverAndDPMS(bool bPowerOffKeyPressed = false);
+
+ bool OnSettingChanged(const CSetting& setting);
+ bool OnSettingAction(const CSetting& setting);
+
+protected:
+ void ActivateScreenSaver(bool forceType = false);
+ void CheckOSScreenSaverInhibitionSetting();
+ // Checks whether the screensaver and / or DPMS should become active.
+ void CheckScreenSaverAndDPMS();
+ void InhibitScreenSaver(bool inhibit);
+ void ResetScreenSaverTimer();
+ bool WakeUpScreenSaver(bool bPowerOffKeyPressed = false);
+
+ void InhibitIdleShutdown(bool inhibit);
+
+ /*! \brief Helper method to determine how to handle TMSG_SHUTDOWN
+ */
+ void HandleShutdownMessage();
+ void CheckShutdown();
+
+ float NavigationIdleTime();
+
+ bool m_renderGUI{false};
+
+ bool m_bInhibitScreenSaver = false;
+ bool m_bResetScreenSaver = false;
+ ADDON::AddonPtr
+ m_pythonScreenSaver; // @warning: Fallback for Python interface, for binaries not needed!
+ bool m_screensaverActive = false;
+ // -1 = failed, 0 = locked, 1 = unlocked, 2 = check in progress
+ int m_iScreenSaveLock = 0;
+ std::string m_screensaverIdInUse;
+
+ bool m_dpmsIsActive = false;
+ bool m_dpmsIsManual = false;
+
+ bool m_bInhibitIdleShutdown = false;
+ CStopWatch m_navigationTimer;
+ CStopWatch m_shutdownTimer;
+
+#ifdef TARGET_WINDOWS
+ CWinIdleTimer m_idleTimer;
+ CWinIdleTimer m_screenSaverTimer;
+#else
+ CStopWatch m_idleTimer;
+ CStopWatch m_screenSaverTimer;
+#endif
+
+ // OS screen saver inhibitor that is always active if user selected a Kodi screen saver
+ KODI::WINDOWING::COSScreenSaverInhibitor m_globalScreensaverInhibitor;
+ // Inhibitor that is active e.g. during video playback
+ KODI::WINDOWING::COSScreenSaverInhibitor m_screensaverInhibitor;
+};
diff --git a/xbmc/application/ApplicationSettingsHandling.cpp b/xbmc/application/ApplicationSettingsHandling.cpp
new file mode 100644
index 0000000..0a2c1b1
--- /dev/null
+++ b/xbmc/application/ApplicationSettingsHandling.cpp
@@ -0,0 +1,214 @@
+/*
+ * 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 "ApplicationSettingsHandling.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationPowerHandling.h"
+#include "application/ApplicationSkinHandling.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingsManager.h"
+#if defined(TARGET_DARWIN_OSX)
+#include "utils/StringUtils.h"
+#endif
+
+namespace
+{
+bool IsPlaying(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return data ? static_cast<CApplicationPlayer*>(data)->IsPlaying() : false;
+}
+} // namespace
+
+void CApplicationSettingsHandling::RegisterSettings()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ CSettingsManager* settingsMgr = settings->GetSettingsManager();
+
+ settingsMgr->RegisterSettingsHandler(this);
+
+ settingsMgr->RegisterCallback(this, {CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH,
+ CSettings::SETTING_LOOKANDFEEL_SKIN,
+ CSettings::SETTING_LOOKANDFEEL_SKINSETTINGS,
+ CSettings::SETTING_LOOKANDFEEL_FONT,
+ CSettings::SETTING_LOOKANDFEEL_SKINTHEME,
+ CSettings::SETTING_LOOKANDFEEL_SKINCOLORS,
+ CSettings::SETTING_LOOKANDFEEL_SKINZOOM,
+ CSettings::SETTING_MUSICPLAYER_REPLAYGAINPREAMP,
+ CSettings::SETTING_MUSICPLAYER_REPLAYGAINNOGAINPREAMP,
+ CSettings::SETTING_MUSICPLAYER_REPLAYGAINTYPE,
+ CSettings::SETTING_MUSICPLAYER_REPLAYGAINAVOIDCLIPPING,
+ CSettings::SETTING_SCRAPERS_MUSICVIDEOSDEFAULT,
+ CSettings::SETTING_SCREENSAVER_MODE,
+ CSettings::SETTING_SCREENSAVER_PREVIEW,
+ CSettings::SETTING_SCREENSAVER_SETTINGS,
+ CSettings::SETTING_AUDIOCDS_SETTINGS,
+ CSettings::SETTING_VIDEOSCREEN_GUICALIBRATION,
+ CSettings::SETTING_VIDEOSCREEN_TESTPATTERN,
+ CSettings::SETTING_VIDEOPLAYER_USEMEDIACODEC,
+ CSettings::SETTING_VIDEOPLAYER_USEMEDIACODECSURFACE,
+ CSettings::SETTING_AUDIOOUTPUT_VOLUMESTEPS,
+ CSettings::SETTING_SOURCE_VIDEOS,
+ CSettings::SETTING_SOURCE_MUSIC,
+ CSettings::SETTING_SOURCE_PICTURES,
+ CSettings::SETTING_VIDEOSCREEN_FAKEFULLSCREEN});
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer)
+ return;
+
+ settingsMgr->RegisterCallback(
+ &appPlayer->GetSeekHandler(),
+ {CSettings::SETTING_VIDEOPLAYER_SEEKDELAY, CSettings::SETTING_VIDEOPLAYER_SEEKSTEPS,
+ CSettings::SETTING_MUSICPLAYER_SEEKDELAY, CSettings::SETTING_MUSICPLAYER_SEEKSTEPS});
+
+ settingsMgr->AddDynamicCondition("isplaying", IsPlaying, appPlayer.get());
+
+ settings->RegisterSubSettings(this);
+}
+
+void CApplicationSettingsHandling::UnregisterSettings()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ CSettingsManager* settingsMgr = settings->GetSettingsManager();
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer)
+ return;
+
+ settings->UnregisterSubSettings(this);
+ settingsMgr->RemoveDynamicCondition("isplaying");
+ settingsMgr->UnregisterCallback(&appPlayer->GetSeekHandler());
+ settingsMgr->UnregisterCallback(this);
+ settingsMgr->UnregisterSettingsHandler(this);
+}
+
+void CApplicationSettingsHandling::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (!setting)
+ return;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appSkin = components.GetComponent<CApplicationSkinHandling>();
+ if (appSkin->OnSettingChanged(*setting))
+ return;
+
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ if (appVolume->OnSettingChanged(*setting))
+ return;
+
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (appPower->OnSettingChanged(*setting))
+ return;
+
+ const std::string& settingId = setting->GetId();
+
+ if (settingId == CSettings::SETTING_VIDEOSCREEN_FAKEFULLSCREEN)
+ {
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot())
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(), true);
+ }
+ else if (settingId == CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH)
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_RESTART);
+ }
+}
+
+void CApplicationSettingsHandling::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (!setting)
+ return;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (appPower->OnSettingAction(*setting))
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_LOOKANDFEEL_SKINSETTINGS)
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SKIN_SETTINGS);
+ else if (settingId == CSettings::SETTING_AUDIOCDS_SETTINGS)
+ {
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_AUDIOCDS_ENCODER),
+ addon, ADDON::AddonType::AUDIOENCODER, ADDON::OnlyEnabled::CHOICE_YES))
+ CGUIDialogAddonSettings::ShowForAddon(addon);
+ }
+ else if (settingId == CSettings::SETTING_VIDEOSCREEN_GUICALIBRATION)
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SCREEN_CALIBRATION);
+ else if (settingId == CSettings::SETTING_SOURCE_VIDEOS)
+ {
+ std::vector<std::string> params{"library://video/files.xml", "return"};
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_NAV, params);
+ }
+ else if (settingId == CSettings::SETTING_SOURCE_MUSIC)
+ {
+ std::vector<std::string> params{"library://music/files.xml", "return"};
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_NAV, params);
+ }
+ else if (settingId == CSettings::SETTING_SOURCE_PICTURES)
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_PICTURES);
+}
+
+bool CApplicationSettingsHandling::OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode)
+{
+ if (!setting)
+ return false;
+
+#if defined(TARGET_DARWIN_OSX)
+ if (setting->GetId() == CSettings::SETTING_AUDIOOUTPUT_AUDIODEVICE)
+ {
+ std::shared_ptr<CSettingString> audioDevice = std::static_pointer_cast<CSettingString>(setting);
+ // Gotham and older didn't enumerate audio devices per stream on osx
+ // add stream0 per default which should be ok for all old settings.
+ if (!StringUtils::EqualsNoCase(audioDevice->GetValue(), "DARWINOSX:default") &&
+ StringUtils::FindWords(audioDevice->GetValue().c_str(), ":stream") == std::string::npos)
+ {
+ std::string newSetting = audioDevice->GetValue();
+ newSetting += ":stream0";
+ return audioDevice->SetValue(newSetting);
+ }
+ }
+#endif
+
+ return false;
+}
+
+bool CApplicationSettingsHandling::Load(const TiXmlNode* settings)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ return appVolume->Load(settings);
+}
+
+bool CApplicationSettingsHandling::Save(TiXmlNode* settings) const
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ return appVolume->Save(settings);
+}
diff --git a/xbmc/application/ApplicationSettingsHandling.h b/xbmc/application/ApplicationSettingsHandling.h
new file mode 100644
index 0000000..5dedd8f
--- /dev/null
+++ b/xbmc/application/ApplicationSettingsHandling.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "settings/ISubSettings.h"
+#include "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+
+/*!
+ * \brief Class handling application support for settings.
+ */
+
+class CApplicationSettingsHandling : public ISettingCallback,
+ public ISettingsHandler,
+ public ISubSettings
+{
+protected:
+ void RegisterSettings();
+ void UnregisterSettings();
+
+ bool Load(const TiXmlNode* settings) override;
+ bool Save(TiXmlNode* settings) const override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+ bool OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode) override;
+};
diff --git a/xbmc/application/ApplicationSkinHandling.cpp b/xbmc/application/ApplicationSkinHandling.cpp
new file mode 100644
index 0000000..edca76b
--- /dev/null
+++ b/xbmc/application/ApplicationSkinHandling.cpp
@@ -0,0 +1,514 @@
+/*
+ * 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 "ApplicationSkinHandling.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUILargeTextureManager.h"
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonVersion.h"
+#include "addons/Skin.h"
+#include "addons/addoninfo/AddonType.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogButtonMenu.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogSubMenu.h"
+#include "filesystem/Directory.h"
+#include "filesystem/DirectoryCache.h"
+#include "guilib/GUIAudioManager.h"
+#include "guilib/GUIColorManager.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIFontManager.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/StereoscopicsManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SkinSettings.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+#include "video/dialogs/GUIDialogFullScreenInfo.h"
+
+using namespace KODI::MESSAGING;
+
+CApplicationSkinHandling::CApplicationSkinHandling(IMsgTargetCallback* msgCb,
+ IWindowManagerCallback* wCb,
+ bool& bInitializing)
+ : m_msgCb(msgCb), m_wCb(wCb), m_bInitializing(bInitializing)
+{
+}
+
+bool CApplicationSkinHandling::LoadSkin(const std::string& skinID)
+{
+ std::shared_ptr<ADDON::CSkinInfo> skin;
+ {
+ ADDON::AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(skinID, addon, ADDON::AddonType::SKIN,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ return false;
+ skin = std::static_pointer_cast<ADDON::CSkinInfo>(addon);
+ }
+
+ // store player and rendering state
+ bool bPreviousPlayingState = false;
+
+ enum class RENDERING_STATE
+ {
+ NONE,
+ VIDEO,
+ GAME,
+ } previousRenderingState = RENDERING_STATE::NONE;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer && appPlayer->IsPlayingVideo())
+ {
+ bPreviousPlayingState = !appPlayer->IsPausedPlayback();
+ if (bPreviousPlayingState)
+ appPlayer->Pause();
+ appPlayer->FlushRenderer();
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_VIDEO)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_HOME);
+ previousRenderingState = RENDERING_STATE::VIDEO;
+ }
+ else if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() ==
+ WINDOW_FULLSCREEN_GAME)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_HOME);
+ previousRenderingState = RENDERING_STATE::GAME;
+ }
+ }
+
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ // store current active window with its focused control
+ int currentWindowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ int currentFocusedControlID = -1;
+ if (currentWindowID != WINDOW_INVALID)
+ {
+ CGUIWindow* pWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(currentWindowID);
+ if (pWindow)
+ currentFocusedControlID = pWindow->GetFocusedControlID();
+ }
+
+ UnloadSkin();
+
+ skin->Start();
+
+ // migrate any skin-specific settings that are still stored in guisettings.xml
+ CSkinSettings::GetInstance().MigrateSettings(skin);
+
+ // check if the skin has been properly loaded and if it has a Home.xml
+ if (!skin->HasSkinFile("Home.xml"))
+ {
+ CLog::Log(LOGERROR, "failed to load requested skin '{}'", skin->ID());
+ return false;
+ }
+
+ CLog::Log(LOGINFO, " load skin from: {} (version: {})", skin->Path(),
+ skin->Version().asString());
+ g_SkinInfo = skin;
+
+ CLog::Log(LOGINFO, " load fonts for skin...");
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetMediaDir(skin->Path());
+ g_directoryCache.ClearSubPaths(skin->Path());
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ CServiceBroker::GetGUI()->GetColorManager().Load(
+ settings->GetString(CSettings::SETTING_LOOKANDFEEL_SKINCOLORS));
+
+ g_SkinInfo->LoadIncludes();
+
+ g_fontManager.LoadFonts(settings->GetString(CSettings::SETTING_LOOKANDFEEL_FONT));
+
+ // load in the skin strings
+ std::string langPath = URIUtils::AddFileToFolder(skin->Path(), "language");
+ URIUtils::AddSlashAtEnd(langPath);
+
+ g_localizeStrings.LoadSkinStrings(langPath,
+ settings->GetString(CSettings::SETTING_LOCALE_LANGUAGE));
+ g_SkinInfo->LoadTimers();
+
+ const auto start = std::chrono::steady_clock::now();
+
+ CLog::Log(LOGINFO, " load new skin...");
+
+ // Load custom windows
+ LoadCustomWindows();
+
+ const auto end = std::chrono::steady_clock::now();
+ std::chrono::duration<double, std::milli> duration = end - start;
+
+ CLog::Log(LOGDEBUG, "Load Skin XML: {:.2f} ms", duration.count());
+
+ CLog::Log(LOGINFO, " initialize new skin...");
+ CServiceBroker::GetGUI()->GetWindowManager().AddMsgTarget(m_msgCb);
+ CServiceBroker::GetGUI()->GetWindowManager().AddMsgTarget(&CServiceBroker::GetPlaylistPlayer());
+ CServiceBroker::GetGUI()->GetWindowManager().AddMsgTarget(&g_fontManager);
+ CServiceBroker::GetGUI()->GetWindowManager().AddMsgTarget(
+ &CServiceBroker::GetGUI()->GetStereoscopicsManager());
+ CServiceBroker::GetGUI()->GetWindowManager().SetCallback(*m_wCb);
+
+ //@todo should be done by GUIComponents
+ CServiceBroker::GetGUI()->GetWindowManager().Initialize();
+ CServiceBroker::GetGUI()->GetAudioManager().Enable(true);
+ CServiceBroker::GetGUI()->GetAudioManager().Load();
+ CServiceBroker::GetTextureCache()->Initialize();
+
+ if (g_SkinInfo->HasSkinFile("DialogFullScreenInfo.xml"))
+ CServiceBroker::GetGUI()->GetWindowManager().Add(new CGUIDialogFullScreenInfo);
+
+ CLog::Log(LOGINFO, " skin loaded...");
+
+ // leave the graphics lock
+ lock.unlock();
+
+ // restore active window
+ if (currentWindowID != WINDOW_INVALID)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(currentWindowID);
+ if (currentFocusedControlID != -1)
+ {
+ CGUIWindow* pWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(currentWindowID);
+ if (pWindow && pWindow->HasSaveLastControl())
+ {
+ CGUIMessage msg(GUI_MSG_SETFOCUS, currentWindowID, currentFocusedControlID, 0);
+ pWindow->OnMessage(msg);
+ }
+ }
+ }
+
+ // restore player and rendering state
+ if (appPlayer && appPlayer->IsPlayingVideo())
+ {
+ if (bPreviousPlayingState)
+ appPlayer->Pause();
+
+ switch (previousRenderingState)
+ {
+ case RENDERING_STATE::VIDEO:
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_FULLSCREEN_VIDEO);
+ break;
+ case RENDERING_STATE::GAME:
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_FULLSCREEN_GAME);
+ break;
+ default:
+ break;
+ }
+ }
+
+ return true;
+}
+
+void CApplicationSkinHandling::UnloadSkin()
+{
+ if (g_SkinInfo != nullptr && m_saveSkinOnUnloading)
+ g_SkinInfo->SaveSettings();
+ else if (!m_saveSkinOnUnloading)
+ m_saveSkinOnUnloading = true;
+
+ if (g_SkinInfo)
+ g_SkinInfo->Unload();
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ {
+ gui->GetAudioManager().Enable(false);
+
+ gui->GetWindowManager().DeInitialize();
+ CServiceBroker::GetTextureCache()->Deinitialize();
+
+ // remove the skin-dependent window
+ gui->GetWindowManager().Delete(WINDOW_DIALOG_FULLSCREEN_INFO);
+
+ gui->GetTextureManager().Cleanup();
+ gui->GetLargeTextureManager().CleanupUnusedImages(true);
+
+ g_fontManager.Clear();
+
+ gui->GetColorManager().Clear();
+
+ gui->GetInfoManager().Clear();
+ }
+
+ // The g_SkinInfo shared_ptr ought to be reset here
+ // but there are too many places it's used without checking for nullptr
+ // and as a result a race condition on exit can cause a crash.
+ CLog::Log(LOGINFO, "Unloaded skin");
+}
+
+bool CApplicationSkinHandling::LoadCustomWindows()
+{
+ // Start from wherever home.xml is
+ std::vector<std::string> vecSkinPath;
+ g_SkinInfo->GetSkinPaths(vecSkinPath);
+
+ for (const auto& skinPath : vecSkinPath)
+ {
+ CLog::Log(LOGINFO, "Loading custom window XMLs from skin path {}", skinPath);
+
+ CFileItemList items;
+ if (XFILE::CDirectory::GetDirectory(skinPath, items, ".xml", XFILE::DIR_FLAG_NO_FILE_DIRS))
+ {
+ for (const auto& item : items)
+ {
+ if (item->m_bIsFolder)
+ continue;
+
+ std::string skinFile = URIUtils::GetFileName(item->GetPath());
+ if (StringUtils::StartsWithNoCase(skinFile, "custom"))
+ {
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(item->GetPath()))
+ {
+ CLog::Log(LOGERROR, "Unable to load custom window XML {}. Line {}\n{}", item->GetPath(),
+ xmlDoc.ErrorRow(), xmlDoc.ErrorDesc());
+ continue;
+ }
+
+ // Root element should be <window>
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ std::string strValue = pRootElement->Value();
+ if (!StringUtils::EqualsNoCase(strValue, "window"))
+ {
+ CLog::Log(LOGERROR, "No <window> root element found for custom window in {}", skinFile);
+ continue;
+ }
+
+ int id = WINDOW_INVALID;
+
+ // Read the type attribute or element to get the window type to create
+ // If no type is specified, create a CGUIWindow as default
+ std::string strType;
+ if (pRootElement->Attribute("type"))
+ strType = pRootElement->Attribute("type");
+ else
+ {
+ const TiXmlNode* pType = pRootElement->FirstChild("type");
+ if (pType && pType->FirstChild())
+ strType = pType->FirstChild()->Value();
+ }
+
+ // Read the id attribute or element to get the window id
+ if (!pRootElement->Attribute("id", &id))
+ {
+ const TiXmlNode* pType = pRootElement->FirstChild("id");
+ if (pType && pType->FirstChild())
+ id = atol(pType->FirstChild()->Value());
+ }
+
+ int windowId = id + WINDOW_HOME;
+ if (id == WINDOW_INVALID ||
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow(windowId))
+ {
+ // No id specified or id already in use
+ CLog::Log(LOGERROR, "No id specified or id already in use for custom window in {}",
+ skinFile);
+ continue;
+ }
+
+ CGUIWindow* pWindow = nullptr;
+ bool hasVisibleCondition = false;
+
+ if (StringUtils::EqualsNoCase(strType, "dialog"))
+ {
+ DialogModalityType modality = DialogModalityType::MODAL;
+ hasVisibleCondition = pRootElement->FirstChildElement("visible") != nullptr;
+ // By default dialogs that have visible conditions are considered modeless unless explicitly
+ // set to "modal" by the skinner using the "modality" attribute in the root XML element of the window
+ if (hasVisibleCondition &&
+ (!pRootElement->Attribute("modality") ||
+ !StringUtils::EqualsNoCase(pRootElement->Attribute("modality"), "modal")))
+ modality = DialogModalityType::MODELESS;
+
+ pWindow = new CGUIDialog(windowId, skinFile, modality);
+ }
+ else if (StringUtils::EqualsNoCase(strType, "submenu"))
+ {
+ pWindow = new CGUIDialogSubMenu(windowId, skinFile);
+ }
+ else if (StringUtils::EqualsNoCase(strType, "buttonmenu"))
+ {
+ pWindow = new CGUIDialogButtonMenu(windowId, skinFile);
+ }
+ else
+ {
+ pWindow = new CGUIWindow(windowId, skinFile);
+ }
+
+ if (!pWindow)
+ {
+ CLog::Log(LOGERROR, "Failed to create custom window from {}", skinFile);
+ continue;
+ }
+
+ pWindow->SetCustom(true);
+
+ // Determining whether our custom dialog is modeless (visible condition is present)
+ // will be done on load. Therefore we need to initialize the custom dialog on gui init.
+ pWindow->SetLoadType(hasVisibleCondition ? CGUIWindow::LOAD_ON_GUI_INIT
+ : CGUIWindow::KEEP_IN_MEMORY);
+
+ CServiceBroker::GetGUI()->GetWindowManager().AddCustomWindow(pWindow);
+ }
+ }
+ }
+ }
+ return true;
+}
+
+void CApplicationSkinHandling::ReloadSkin(bool confirm)
+{
+ if (!g_SkinInfo || m_bInitializing)
+ return; // Don't allow reload before skin is loaded by system
+
+ std::string oldSkin = g_SkinInfo->ID();
+
+ CGUIMessage msg(GUI_MSG_LOAD_SKIN, -1,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::string newSkin = settings->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN);
+ if (LoadSkin(newSkin))
+ {
+ /* The Reset() or SetString() below will cause recursion, so the m_confirmSkinChange boolean is set so as to not prompt the
+ user as to whether they want to keep the current skin. */
+ if (confirm && m_confirmSkinChange)
+ {
+ if (HELPERS::ShowYesNoDialogText(CVariant{13123}, CVariant{13111}, CVariant{""}, CVariant{""},
+ 10000) != HELPERS::DialogResponse::CHOICE_YES)
+ {
+ m_confirmSkinChange = false;
+ settings->SetString(CSettings::SETTING_LOOKANDFEEL_SKIN, oldSkin);
+ }
+ else
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_STARTUP_ANIM);
+ }
+ }
+ else
+ {
+ // skin failed to load - we revert to the default only if we didn't fail loading the default
+ auto setting = settings->GetSetting(CSettings::SETTING_LOOKANDFEEL_SKIN);
+ if (!setting)
+ {
+ CLog::Log(LOGFATAL, "Failed to load setting for: {}", CSettings::SETTING_LOOKANDFEEL_SKIN);
+ return;
+ }
+
+ std::string defaultSkin = std::static_pointer_cast<CSettingString>(setting)->GetDefault();
+ if (newSkin != defaultSkin)
+ {
+ m_confirmSkinChange = false;
+ setting->Reset();
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(24102),
+ g_localizeStrings.Get(24103));
+ }
+ }
+ m_confirmSkinChange = true;
+}
+
+bool CApplicationSkinHandling::OnSettingChanged(const CSetting& setting)
+{
+ const std::string& settingId = setting.GetId();
+
+ if (settingId == CSettings::SETTING_LOOKANDFEEL_SKIN ||
+ settingId == CSettings::SETTING_LOOKANDFEEL_FONT ||
+ settingId == CSettings::SETTING_LOOKANDFEEL_SKINTHEME ||
+ settingId == CSettings::SETTING_LOOKANDFEEL_SKINCOLORS)
+ {
+ // check if we should ignore this change event due to changing skins in which case we have to
+ // change several settings and each one of them could lead to a complete skin reload which would
+ // result in multiple skin reloads. Therefore we manually specify to ignore specific settings
+ // which are going to be changed.
+ if (m_ignoreSkinSettingChanges)
+ return true;
+
+ // if the skin changes and the current color/theme/font is not the default one, reset
+ // the it to the default value
+ if (settingId == CSettings::SETTING_LOOKANDFEEL_SKIN)
+ {
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ SettingPtr skinRelatedSetting =
+ settings->GetSetting(CSettings::SETTING_LOOKANDFEEL_SKINCOLORS);
+ if (!skinRelatedSetting->IsDefault())
+ {
+ m_ignoreSkinSettingChanges = true;
+ skinRelatedSetting->Reset();
+ }
+
+ skinRelatedSetting = settings->GetSetting(CSettings::SETTING_LOOKANDFEEL_SKINTHEME);
+ if (!skinRelatedSetting->IsDefault())
+ {
+ m_ignoreSkinSettingChanges = true;
+ skinRelatedSetting->Reset();
+ }
+
+ skinRelatedSetting = settings->GetSetting(CSettings::SETTING_LOOKANDFEEL_FONT);
+ if (!skinRelatedSetting->IsDefault())
+ {
+ m_ignoreSkinSettingChanges = true;
+ skinRelatedSetting->Reset();
+ }
+ }
+ else if (settingId == CSettings::SETTING_LOOKANDFEEL_SKINTHEME)
+ {
+ std::shared_ptr<CSettingString> skinColorsSetting = std::static_pointer_cast<CSettingString>(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(
+ CSettings::SETTING_LOOKANDFEEL_SKINCOLORS));
+ m_ignoreSkinSettingChanges = true;
+
+ // we also need to adjust the skin color setting
+ std::string colorTheme = static_cast<const CSettingString&>(setting).GetValue();
+ URIUtils::RemoveExtension(colorTheme);
+ if (setting.IsDefault() || StringUtils::EqualsNoCase(colorTheme, "Textures"))
+ skinColorsSetting->Reset();
+ else
+ skinColorsSetting->SetValue(colorTheme);
+ }
+
+ m_ignoreSkinSettingChanges = false;
+
+ if (g_SkinInfo)
+ {
+ // now we can finally reload skins
+ std::string builtin("ReloadSkin");
+ if (settingId == CSettings::SETTING_LOOKANDFEEL_SKIN && m_confirmSkinChange)
+ builtin += "(confirm)";
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr, builtin);
+ }
+ }
+ else if (settingId == CSettings::SETTING_LOOKANDFEEL_SKINZOOM)
+ {
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_WINDOW_RESIZE);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+ else
+ return false;
+
+ return true;
+}
+
+void CApplicationSkinHandling::ProcessSkin() const
+{
+ if (g_SkinInfo != nullptr)
+ g_SkinInfo->ProcessTimers();
+}
diff --git a/xbmc/application/ApplicationSkinHandling.h b/xbmc/application/ApplicationSkinHandling.h
new file mode 100644
index 0000000..fc5c3d0
--- /dev/null
+++ b/xbmc/application/ApplicationSkinHandling.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "application/IApplicationComponent.h"
+
+#include <string>
+
+class CApplication;
+class CSetting;
+class IMsgTargetCallback;
+class IWindowManagerCallback;
+
+/*!
+ * \brief Class handling application support for skin management.
+ */
+class CApplicationSkinHandling : public IApplicationComponent
+{
+ friend class CApplication;
+
+public:
+ CApplicationSkinHandling(IMsgTargetCallback* msgCb,
+ IWindowManagerCallback* wCb,
+ bool& bInitializing);
+
+ void UnloadSkin();
+
+ bool OnSettingChanged(const CSetting& setting);
+ void ReloadSkin(bool confirm = false);
+
+protected:
+ bool LoadSkin(const std::string& skinID);
+ bool LoadCustomWindows();
+
+ /*!
+ * \brief Called by the application main/render thread for processing
+ * operations belonging to the skin.
+ */
+ void ProcessSkin() const;
+
+ bool m_saveSkinOnUnloading = true;
+ bool m_confirmSkinChange = true;
+ bool m_ignoreSkinSettingChanges = false;
+ IMsgTargetCallback* m_msgCb;
+ IWindowManagerCallback* m_wCb;
+ bool& m_bInitializing;
+};
diff --git a/xbmc/application/ApplicationStackHelper.cpp b/xbmc/application/ApplicationStackHelper.cpp
new file mode 100644
index 0000000..a1e8a0e
--- /dev/null
+++ b/xbmc/application/ApplicationStackHelper.cpp
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2017-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 "ApplicationStackHelper.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "Util.h"
+#include "cores/VideoPlayer/DVDFileInfo.h"
+#include "filesystem/StackDirectory.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <utility>
+
+using namespace XFILE;
+
+CApplicationStackHelper::CApplicationStackHelper(void)
+ : m_currentStack(new CFileItemList)
+{
+}
+
+void CApplicationStackHelper::Clear()
+{
+ m_currentStackPosition = 0;
+ m_currentStack->Clear();
+}
+
+void CApplicationStackHelper::OnPlayBackStarted(const CFileItem& item)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // time to clean up stack map
+ if (!HasRegisteredStack(item))
+ m_stackmap.clear();
+ else
+ {
+ auto stack = GetRegisteredStack(item);
+ Stackmap::iterator itr = m_stackmap.begin();
+ while (itr != m_stackmap.end())
+ {
+ if (itr->second->m_pStack != stack)
+ {
+ itr = m_stackmap.erase(itr);
+ }
+ else
+ {
+ ++itr;
+ }
+ }
+ }
+}
+
+bool CApplicationStackHelper::InitializeStack(const CFileItem & item)
+{
+ if (!item.IsStack())
+ return false;
+
+ auto stack = std::make_shared<CFileItem>(item);
+
+ Clear();
+ // read and determine kind of stack
+ CStackDirectory dir;
+ if (!dir.GetDirectory(item.GetURL(), *m_currentStack) || m_currentStack->IsEmpty())
+ return false;
+ for (int i = 0; i < m_currentStack->Size(); i++)
+ {
+ // keep cross-references between stack parts and the stack
+ SetRegisteredStack(GetStackPartFileItem(i), stack);
+ SetRegisteredStackPartNumber(GetStackPartFileItem(i), i);
+ }
+ m_currentStackIsDiscImageStack = CFileItem(CStackDirectory::GetFirstStackedFile(item.GetPath()), false).IsDiscImage();
+
+ return true;
+}
+
+int CApplicationStackHelper::InitializeStackStartPartAndOffset(const CFileItem& item)
+{
+ CVideoDatabase dbs;
+ int64_t startoffset = 0;
+
+ // case 1: stacked ISOs
+ if (m_currentStackIsDiscImageStack)
+ {
+ // first assume values passed to the stack
+ int selectedFile = item.m_lStartPartNumber;
+ startoffset = item.GetStartOffset();
+
+ // check if we instructed the stack to resume from default
+ if (startoffset == STARTOFFSET_RESUME) // selected file is not specified, pick the 'last' resume point
+ {
+ if (dbs.Open())
+ {
+ CBookmark bookmark;
+ std::string path = item.GetPath();
+ if (item.HasProperty("original_listitem_url") && URIUtils::IsPlugin(item.GetProperty("original_listitem_url").asString()))
+ path = item.GetProperty("original_listitem_url").asString();
+ if (dbs.GetResumeBookMark(path, bookmark))
+ {
+ startoffset = CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds);
+ selectedFile = bookmark.partNumber;
+ }
+ dbs.Close();
+ }
+ else
+ CLog::LogF(LOGERROR, "Cannot open VideoDatabase");
+ }
+
+ // make sure that the selected part is within the boundaries
+ if (selectedFile <= 0)
+ {
+ CLog::LogF(LOGWARNING, "Selected part {} out of range, playing part 1", selectedFile);
+ selectedFile = 1;
+ }
+ else if (selectedFile > m_currentStack->Size())
+ {
+ CLog::LogF(LOGWARNING, "Selected part {} out of range, playing part {}", selectedFile,
+ m_currentStack->Size());
+ selectedFile = m_currentStack->Size();
+ }
+
+ // set startoffset in selected item, track stack item for updating purposes, and finally play disc part
+ m_currentStackPosition = selectedFile - 1;
+ startoffset = startoffset > 0 ? STARTOFFSET_RESUME : 0;
+ }
+ // case 2: all other stacks
+ else
+ {
+ // see if we have the info in the database
+ //! @todo If user changes the time speed (FPS via framerate conversion stuff)
+ //! then these times will be wrong.
+ //! Also, this is really just a hack for the slow load up times we have
+ //! A much better solution is a fast reader of FPS and fileLength
+ //! that we can use on a file to get it's time.
+ std::vector<uint64_t> times;
+ bool haveTimes(false);
+
+ if (dbs.Open())
+ {
+ haveTimes = dbs.GetStackTimes(item.GetPath(), times);
+ dbs.Close();
+ }
+
+ // calculate the total time of the stack
+ uint64_t totalTimeMs = 0;
+ for (int i = 0; i < m_currentStack->Size(); i++)
+ {
+ if (haveTimes)
+ {
+ // set end time in every part
+ GetStackPartFileItem(i).SetEndOffset(times[i]);
+ }
+ else
+ {
+ int duration;
+ if (!CDVDFileInfo::GetFileDuration(GetStackPartFileItem(i).GetPath(), duration))
+ {
+ m_currentStack->Clear();
+ return false;
+ }
+ totalTimeMs += duration;
+ // set end time in every part
+ GetStackPartFileItem(i).SetEndOffset(totalTimeMs);
+ times.push_back(totalTimeMs);
+ }
+ // set start time in every part
+ SetRegisteredStackPartStartTimeMs(GetStackPartFileItem(i), GetStackPartStartTimeMs(i));
+ }
+ // set total time in every part
+ totalTimeMs = GetStackTotalTimeMs();
+ for (int i = 0; i < m_currentStack->Size(); i++)
+ SetRegisteredStackTotalTimeMs(GetStackPartFileItem(i), totalTimeMs);
+
+ uint64_t msecs = item.GetStartOffset();
+
+ if (!haveTimes || item.GetStartOffset() == STARTOFFSET_RESUME)
+ {
+ if (dbs.Open())
+ {
+ // have our times now, so update the dB
+ if (!haveTimes && !times.empty())
+ dbs.SetStackTimes(item.GetPath(), times);
+
+ if (item.GetStartOffset() == STARTOFFSET_RESUME)
+ {
+ // can only resume seek here, not dvdstate
+ CBookmark bookmark;
+ std::string path = item.GetPath();
+ if (item.HasProperty("original_listitem_url") && URIUtils::IsPlugin(item.GetProperty("original_listitem_url").asString()))
+ path = item.GetProperty("original_listitem_url").asString();
+ if (dbs.GetResumeBookMark(path, bookmark))
+ msecs = static_cast<uint64_t>(bookmark.timeInSeconds * 1000);
+ else
+ msecs = 0;
+ }
+ dbs.Close();
+ }
+ }
+
+ m_currentStackPosition = GetStackPartNumberAtTimeMs(msecs);
+ startoffset = msecs - GetStackPartStartTimeMs(m_currentStackPosition);
+ }
+ return startoffset;
+}
+
+bool CApplicationStackHelper::IsPlayingISOStack() const
+{
+ return m_currentStack->Size() > 0 && m_currentStackIsDiscImageStack;
+}
+
+bool CApplicationStackHelper::IsPlayingRegularStack() const
+{
+ return m_currentStack->Size() > 0 && !m_currentStackIsDiscImageStack;
+}
+
+bool CApplicationStackHelper::HasNextStackPartFileItem() const
+{
+ return m_currentStackPosition < m_currentStack->Size() - 1;
+}
+
+uint64_t CApplicationStackHelper::GetStackPartEndTimeMs(int partNumber) const
+{
+ return GetStackPartFileItem(partNumber).GetEndOffset();
+}
+
+uint64_t CApplicationStackHelper::GetStackTotalTimeMs() const
+{
+ return GetStackPartEndTimeMs(m_currentStack->Size() - 1);
+}
+
+int CApplicationStackHelper::GetStackPartNumberAtTimeMs(uint64_t msecs)
+{
+ if (msecs > 0)
+ {
+ // work out where to seek to
+ for (int partNumber = 0; partNumber < m_currentStack->Size(); partNumber++)
+ {
+ if (msecs < GetStackPartEndTimeMs(partNumber))
+ return partNumber;
+ }
+ }
+ return 0;
+}
+
+void CApplicationStackHelper::ClearAllRegisteredStackInformation()
+{
+ m_stackmap.clear();
+}
+
+std::shared_ptr<const CFileItem> CApplicationStackHelper::GetRegisteredStack(
+ const CFileItem& item) const
+{
+ return GetStackPartInformation(item.GetPath())->m_pStack;
+}
+
+bool CApplicationStackHelper::HasRegisteredStack(const CFileItem& item) const
+{
+ const auto it = m_stackmap.find(item.GetPath());
+ return it != m_stackmap.end() && it->second != nullptr;
+}
+
+void CApplicationStackHelper::SetRegisteredStack(const CFileItem& item,
+ std::shared_ptr<CFileItem> stackItem)
+{
+ GetStackPartInformation(item.GetPath())->m_pStack = std::move(stackItem);
+}
+
+CFileItem& CApplicationStackHelper::GetStackPartFileItem(int partNumber)
+{
+ return *(*m_currentStack)[partNumber];
+}
+
+const CFileItem& CApplicationStackHelper::GetStackPartFileItem(int partNumber) const
+{
+ return *(*m_currentStack)[partNumber];
+}
+
+int CApplicationStackHelper::GetRegisteredStackPartNumber(const CFileItem& item)
+{
+ return GetStackPartInformation(item.GetPath())->m_lStackPartNumber;
+}
+
+void CApplicationStackHelper::SetRegisteredStackPartNumber(const CFileItem& item, int partNumber)
+{
+ GetStackPartInformation(item.GetPath())->m_lStackPartNumber = partNumber;
+}
+
+uint64_t CApplicationStackHelper::GetRegisteredStackPartStartTimeMs(const CFileItem& item) const
+{
+ return GetStackPartInformation(item.GetPath())->m_lStackPartStartTimeMs;
+}
+
+void CApplicationStackHelper::SetRegisteredStackPartStartTimeMs(const CFileItem& item, uint64_t startTime)
+{
+ GetStackPartInformation(item.GetPath())->m_lStackPartStartTimeMs = startTime;
+}
+
+uint64_t CApplicationStackHelper::GetRegisteredStackTotalTimeMs(const CFileItem& item) const
+{
+ return GetStackPartInformation(item.GetPath())->m_lStackTotalTimeMs;
+}
+
+void CApplicationStackHelper::SetRegisteredStackTotalTimeMs(const CFileItem& item, uint64_t totalTime)
+{
+ GetStackPartInformation(item.GetPath())->m_lStackTotalTimeMs = totalTime;
+}
+
+CApplicationStackHelper::StackPartInformationPtr CApplicationStackHelper::GetStackPartInformation(
+ const std::string& key)
+{
+ if (m_stackmap.count(key) == 0)
+ {
+ StackPartInformationPtr value(new StackPartInformation());
+ m_stackmap[key] = value;
+ }
+ return m_stackmap[key];
+}
+
+CApplicationStackHelper::StackPartInformationPtr CApplicationStackHelper::GetStackPartInformation(
+ const std::string& key) const
+{
+ const auto it = m_stackmap.find(key);
+ if (it == m_stackmap.end())
+ return std::make_shared<StackPartInformation>();
+ return it->second;
+}
diff --git a/xbmc/application/ApplicationStackHelper.h b/xbmc/application/ApplicationStackHelper.h
new file mode 100644
index 0000000..6af0044
--- /dev/null
+++ b/xbmc/application/ApplicationStackHelper.h
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2017-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 "application/IApplicationComponent.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+class CFileItem;
+class CFileItemList;
+
+class CApplicationStackHelper : public IApplicationComponent
+{
+public:
+ CApplicationStackHelper(void);
+ ~CApplicationStackHelper() = default;
+
+ void Clear();
+ void OnPlayBackStarted(const CFileItem& item);
+
+ /*!
+ \brief Initialize stack
+ \param item the FileItem object that is the stack
+ */
+ bool InitializeStack(const CFileItem& item);
+
+ /*!
+ \brief Initialize stack times for each part, start & end, total time, and current part number if resume offset is specified.
+ \param item the FileItem object that is the stack
+ */
+ int InitializeStackStartPartAndOffset(const CFileItem& item);
+
+ /*!
+ \brief returns the current part number
+ */
+ int GetCurrentPartNumber() const { return m_currentStackPosition; }
+
+ /*!
+ \brief Returns true if Application is currently playing an ISO stack
+ */
+ bool IsPlayingISOStack() const;
+
+ /*!
+ \brief Returns true if Application is currently playing a Regular (non-ISO) stack
+ */
+ bool IsPlayingRegularStack() const;
+
+ /*!
+ \brief returns true if there is a next part available
+ */
+ bool HasNextStackPartFileItem() const;
+
+ /*!
+ \brief sets the next stack part as the current and returns a reference to it
+ */
+ const CFileItem& SetNextStackPartCurrentFileItem()
+ {
+ return GetStackPartFileItem(++m_currentStackPosition);
+ }
+
+ /*!
+ \brief sets a given stack part as the current and returns a reference to it
+ \param partNumber the number of the part that needs to become the current one
+ */
+ const CFileItem& SetStackPartCurrentFileItem(int partNumber)
+ {
+ return GetStackPartFileItem(m_currentStackPosition = partNumber);
+ }
+
+ /*!
+ \brief Returns the FileItem currently playing back as part of a (non-ISO) stack playback
+ */
+ const CFileItem& GetCurrentStackPartFileItem() const
+ {
+ return GetStackPartFileItem(m_currentStackPosition);
+ }
+
+ /*!
+ \brief Returns the end time of a FileItem part of a (non-ISO) stack playback
+ \param partNumber the requested part number in the stack
+ */
+ uint64_t GetStackPartEndTimeMs(int partNumber) const;
+
+ /*!
+ \brief Returns the start time of a FileItem part of a (non-ISO) stack playback
+ \param partNumber the requested part number in the stack
+ */
+ uint64_t GetStackPartStartTimeMs(int partNumber) const { return (partNumber > 0) ? GetStackPartEndTimeMs(partNumber - 1) : 0; }
+
+ /*!
+ \brief Returns the start time of the current FileItem part of a (non-ISO) stack playback
+ */
+ uint64_t GetCurrentStackPartStartTimeMs() const { return GetStackPartStartTimeMs(m_currentStackPosition); }
+
+ /*!
+ \brief Returns the total time of a (non-ISO) stack playback
+ */
+ uint64_t GetStackTotalTimeMs() const;
+
+ /*!
+ \brief Returns the stack part number corresponding to the given timestamp in a (non-ISO) stack playback
+ \param msecs the requested timestamp in the stack (in milliseconds)
+ */
+ int GetStackPartNumberAtTimeMs(uint64_t msecs);
+
+ // Stack information registration methods
+
+ /*!
+ \brief Clear all entries in the item-stack map. To be called upon playback stopped.
+ */
+ void ClearAllRegisteredStackInformation();
+
+ /*!
+ \brief Returns a smart pointer to the stack CFileItem.
+ */
+ std::shared_ptr<const CFileItem> GetRegisteredStack(const CFileItem& item) const;
+
+ /*!
+ \brief Returns true if there is a registered stack for the given CFileItem part.
+ \param item the reference to the item that is part of a stack
+ */
+ bool HasRegisteredStack(const CFileItem& item) const;
+
+ /*!
+ \brief Stores a smart pointer to the stack CFileItem in the item-stack map.
+ \param item the reference to the item that is part of a stack
+ \param stackItem the smart pointer to the stack CFileItem
+ */
+ void SetRegisteredStack(const CFileItem& item, std::shared_ptr<CFileItem> stackItem);
+
+ /*!
+ \brief Returns the part number of the part in the parameter
+ \param item the reference to the item that is part of a stack
+ */
+ int GetRegisteredStackPartNumber(const CFileItem& item);
+
+ /*!
+ \brief Stores the part number in the item-stack map.
+ \param item the reference to the item that is part of a stack
+ \param partNumber the part number of the part in other parameter
+ */
+ void SetRegisteredStackPartNumber(const CFileItem& item, int partNumber);
+
+ /*!
+ \brief Returns the start time of the part in the parameter
+ \param item the reference to the item that is part of a stack
+ */
+ uint64_t GetRegisteredStackPartStartTimeMs(const CFileItem& item) const;
+
+ /*!
+ \brief Stores the part start time in the item-stack map.
+ \param item the reference to the item that is part of a stack
+ \param startTime the start time of the part in other parameter
+ */
+ void SetRegisteredStackPartStartTimeMs(const CFileItem& item, uint64_t startTimeMs);
+
+ /*!
+ \brief Returns the total time of the stack associated to the part in the parameter
+ \param item the reference to the item that is part of a stack
+ */
+ uint64_t GetRegisteredStackTotalTimeMs(const CFileItem& item) const;
+
+ /*!
+ \brief Stores the stack's total time associated to the part in the item-stack map.
+ \param item the reference to the item that is part of a stack
+ \param totalTime the total time of the stack
+ */
+ void SetRegisteredStackTotalTimeMs(const CFileItem& item, uint64_t totalTimeMs);
+
+ CCriticalSection m_critSection;
+
+protected:
+ /*!
+ \brief Returns a FileItem part of a (non-ISO) stack playback
+ \param partNumber the requested part number in the stack
+ */
+ CFileItem& GetStackPartFileItem(int partNumber);
+ const CFileItem& GetStackPartFileItem(int partNumber) const;
+
+ class StackPartInformation
+ {
+ public:
+ StackPartInformation()
+ {
+ m_lStackPartNumber = 0;
+ m_lStackPartStartTimeMs = 0;
+ m_lStackTotalTimeMs = 0;
+ };
+ uint64_t m_lStackPartStartTimeMs;
+ uint64_t m_lStackTotalTimeMs;
+ int m_lStackPartNumber;
+ std::shared_ptr<CFileItem> m_pStack;
+ };
+
+ typedef std::shared_ptr<StackPartInformation> StackPartInformationPtr;
+ typedef std::map<std::string, StackPartInformationPtr> Stackmap;
+ Stackmap m_stackmap;
+ StackPartInformationPtr GetStackPartInformation(const std::string& key);
+ StackPartInformationPtr GetStackPartInformation(const std::string& key) const;
+
+ std::unique_ptr<CFileItemList> m_currentStack;
+ int m_currentStackPosition = 0;
+ bool m_currentStackIsDiscImageStack = false;
+};
diff --git a/xbmc/application/ApplicationVolumeHandling.cpp b/xbmc/application/ApplicationVolumeHandling.cpp
new file mode 100644
index 0000000..25229be
--- /dev/null
+++ b/xbmc/application/ApplicationVolumeHandling.cpp
@@ -0,0 +1,201 @@
+/*
+ * 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 "ApplicationVolumeHandling.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "dialogs/GUIDialogVolumeBar.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "interfaces/AnnouncementManager.h"
+#include "peripherals/Peripherals.h"
+#include "settings/Settings.h"
+#include "settings/lib/Setting.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+
+#include <tinyxml.h>
+
+float CApplicationVolumeHandling::GetVolumePercent() const
+{
+ // converts the hardware volume to a percentage
+ return m_volumeLevel * 100.0f;
+}
+
+float CApplicationVolumeHandling::GetVolumeRatio() const
+{
+ return m_volumeLevel;
+}
+
+void CApplicationVolumeHandling::SetHardwareVolume(float hardwareVolume)
+{
+ m_volumeLevel = std::clamp(hardwareVolume, VOLUME_MINIMUM, VOLUME_MAXIMUM);
+
+ IAE* ae = CServiceBroker::GetActiveAE();
+ if (ae)
+ ae->SetVolume(m_volumeLevel);
+}
+
+void CApplicationVolumeHandling::VolumeChanged()
+{
+ CVariant data(CVariant::VariantTypeObject);
+ data["volume"] = static_cast<int>(std::lroundf(GetVolumePercent()));
+ data["muted"] = m_muted;
+ const auto announcementMgr = CServiceBroker::GetAnnouncementManager();
+ announcementMgr->Announce(ANNOUNCEMENT::Application, "OnVolumeChanged", data);
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ // if player has volume control, set it.
+ if (appPlayer)
+ {
+ appPlayer->SetVolume(m_volumeLevel);
+ appPlayer->SetMute(m_muted);
+ }
+}
+
+void CApplicationVolumeHandling::ShowVolumeBar(const CAction* action)
+{
+ const auto& wm = CServiceBroker::GetGUI()->GetWindowManager();
+ auto* volumeBar = wm.GetWindow<CGUIDialogVolumeBar>(WINDOW_DIALOG_VOLUME_BAR);
+ if (volumeBar != nullptr && volumeBar->IsVolumeBarEnabled())
+ {
+ volumeBar->Open();
+ if (action)
+ volumeBar->OnAction(*action);
+ }
+}
+
+bool CApplicationVolumeHandling::IsMuted() const
+{
+ if (CServiceBroker::GetPeripherals().IsMuted())
+ return true;
+ IAE* ae = CServiceBroker::GetActiveAE();
+ if (ae)
+ return ae->IsMuted();
+ return true;
+}
+
+void CApplicationVolumeHandling::ToggleMute(void)
+{
+ if (m_muted)
+ UnMute();
+ else
+ Mute();
+}
+
+void CApplicationVolumeHandling::SetMute(bool mute)
+{
+ if (m_muted != mute)
+ {
+ ToggleMute();
+ m_muted = mute;
+ }
+}
+
+void CApplicationVolumeHandling::Mute()
+{
+ if (CServiceBroker::GetPeripherals().Mute())
+ return;
+
+ IAE* ae = CServiceBroker::GetActiveAE();
+ if (ae)
+ ae->SetMute(true);
+ m_muted = true;
+ VolumeChanged();
+}
+
+void CApplicationVolumeHandling::UnMute()
+{
+ if (CServiceBroker::GetPeripherals().UnMute())
+ return;
+
+ IAE* ae = CServiceBroker::GetActiveAE();
+ if (ae)
+ ae->SetMute(false);
+ m_muted = false;
+ VolumeChanged();
+}
+
+void CApplicationVolumeHandling::SetVolume(float iValue, bool isPercentage)
+{
+ float hardwareVolume = iValue;
+
+ if (isPercentage)
+ hardwareVolume /= 100.0f;
+
+ SetHardwareVolume(hardwareVolume);
+ VolumeChanged();
+}
+
+void CApplicationVolumeHandling::CacheReplayGainSettings(const CSettings& settings)
+{
+ // initialize m_replayGainSettings
+ m_replayGainSettings.iType = settings.GetInt(CSettings::SETTING_MUSICPLAYER_REPLAYGAINTYPE);
+ m_replayGainSettings.iPreAmp = settings.GetInt(CSettings::SETTING_MUSICPLAYER_REPLAYGAINPREAMP);
+ m_replayGainSettings.iNoGainPreAmp =
+ settings.GetInt(CSettings::SETTING_MUSICPLAYER_REPLAYGAINNOGAINPREAMP);
+ m_replayGainSettings.bAvoidClipping =
+ settings.GetBool(CSettings::SETTING_MUSICPLAYER_REPLAYGAINAVOIDCLIPPING);
+}
+
+bool CApplicationVolumeHandling::Load(const TiXmlNode* settings)
+{
+ if (!settings)
+ return false;
+
+ const TiXmlElement* audioElement = settings->FirstChildElement("audio");
+ if (audioElement)
+ {
+ XMLUtils::GetBoolean(audioElement, "mute", m_muted);
+ if (!XMLUtils::GetFloat(audioElement, "fvolumelevel", m_volumeLevel, VOLUME_MINIMUM,
+ VOLUME_MAXIMUM))
+ m_volumeLevel = VOLUME_MAXIMUM;
+ }
+
+ return true;
+}
+
+bool CApplicationVolumeHandling::Save(TiXmlNode* settings) const
+{
+ if (!settings)
+ return false;
+
+ TiXmlElement volumeNode("audio");
+ TiXmlNode* audioNode = settings->InsertEndChild(volumeNode);
+ if (!audioNode)
+ return false;
+
+ XMLUtils::SetBoolean(audioNode, "mute", m_muted);
+ XMLUtils::SetFloat(audioNode, "fvolumelevel", m_volumeLevel);
+
+ return true;
+}
+
+bool CApplicationVolumeHandling::OnSettingChanged(const CSetting& setting)
+{
+ const std::string& settingId = setting.GetId();
+
+ if (StringUtils::EqualsNoCase(settingId, CSettings::SETTING_MUSICPLAYER_REPLAYGAINTYPE))
+ m_replayGainSettings.iType = static_cast<const CSettingInt&>(setting).GetValue();
+ else if (StringUtils::EqualsNoCase(settingId, CSettings::SETTING_MUSICPLAYER_REPLAYGAINPREAMP))
+ m_replayGainSettings.iPreAmp = static_cast<const CSettingInt&>(setting).GetValue();
+ else if (StringUtils::EqualsNoCase(settingId,
+ CSettings::SETTING_MUSICPLAYER_REPLAYGAINNOGAINPREAMP))
+ m_replayGainSettings.iNoGainPreAmp = static_cast<const CSettingInt&>(setting).GetValue();
+ else if (StringUtils::EqualsNoCase(settingId,
+ CSettings::SETTING_MUSICPLAYER_REPLAYGAINAVOIDCLIPPING))
+ m_replayGainSettings.bAvoidClipping = static_cast<const CSettingBool&>(setting).GetValue();
+ else
+ return false;
+
+ return true;
+}
diff --git a/xbmc/application/ApplicationVolumeHandling.h b/xbmc/application/ApplicationVolumeHandling.h
new file mode 100644
index 0000000..d417227
--- /dev/null
+++ b/xbmc/application/ApplicationVolumeHandling.h
@@ -0,0 +1,71 @@
+/*
+ * 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 "application/IApplicationComponent.h"
+
+class CAction;
+class CApplication;
+class CSetting;
+class CSettings;
+class TiXmlNode;
+
+/*!
+ * \brief Class handling application support for audio volume management.
+ */
+class CApplicationVolumeHandling : public IApplicationComponent
+{
+ friend class CApplication;
+
+public:
+ // replay gain settings struct for quick access by the player multiple
+ // times per second (saves doing settings lookup)
+ struct ReplayGainSettings
+ {
+ int iPreAmp;
+ int iNoGainPreAmp;
+ int iType;
+ bool bAvoidClipping;
+ };
+
+ float GetVolumePercent() const;
+ float GetVolumeRatio() const;
+ bool IsMuted() const;
+
+ void SetVolume(float iValue, bool isPercentage = true);
+ void SetMute(bool mute);
+ void ToggleMute(void);
+
+ const ReplayGainSettings& GetReplayGainSettings() const { return m_replayGainSettings; }
+
+ static constexpr float VOLUME_MINIMUM = 0.0f; // -60dB
+ static constexpr float VOLUME_MAXIMUM = 1.0f; // 0dB
+ static constexpr float VOLUME_DYNAMIC_RANGE = 90.0f; // 60dB
+
+ bool Load(const TiXmlNode* settings);
+ bool Save(TiXmlNode* settings) const;
+ bool OnSettingChanged(const CSetting& setting);
+
+protected:
+ bool IsMutedInternal() const { return m_muted; }
+ void ShowVolumeBar(const CAction* action = nullptr);
+
+ void CacheReplayGainSettings(const CSettings& settings);
+
+ void Mute();
+ void UnMute();
+
+ void SetHardwareVolume(float hardwareVolume);
+
+ void VolumeChanged();
+
+ bool m_muted = false;
+ float m_volumeLevel = VOLUME_MAXIMUM;
+ ReplayGainSettings m_replayGainSettings;
+};
diff --git a/xbmc/application/CMakeLists.txt b/xbmc/application/CMakeLists.txt
new file mode 100644
index 0000000..2fd3dbe
--- /dev/null
+++ b/xbmc/application/CMakeLists.txt
@@ -0,0 +1,29 @@
+set(SOURCES AppEnvironment.cpp
+ AppInboundProtocol.cpp
+ Application.cpp
+ ApplicationActionListeners.cpp
+ ApplicationPlayer.cpp
+ ApplicationPlayerCallback.cpp
+ ApplicationPowerHandling.cpp
+ ApplicationSettingsHandling.cpp
+ ApplicationSkinHandling.cpp
+ ApplicationStackHelper.cpp
+ ApplicationVolumeHandling.cpp
+ AppParamParser.cpp
+ AppParams.cpp)
+
+set(HEADERS AppEnvironment.h
+ AppInboundProtocol.h
+ Application.h
+ ApplicationActionListeners.h
+ ApplicationPlayer.h
+ ApplicationPlayerCallback.h
+ ApplicationPowerHandling.h
+ ApplicationSettingsHandling.h
+ ApplicationSkinHandling.h
+ ApplicationStackHelper.h
+ ApplicationVolumeHandling.h
+ AppParamParser.h
+ AppParams.h)
+
+core_add_library(application)
diff --git a/xbmc/application/IApplicationComponent.h b/xbmc/application/IApplicationComponent.h
new file mode 100644
index 0000000..da23d3e
--- /dev/null
+++ b/xbmc/application/IApplicationComponent.h
@@ -0,0 +1,16 @@
+/*
+ * Copyright (C) 2022 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
+
+//! \brief Base class for application components.
+class IApplicationComponent
+{
+public:
+ virtual ~IApplicationComponent() = default;
+};
diff --git a/xbmc/cdrip/CDDARipJob.cpp b/xbmc/cdrip/CDDARipJob.cpp
new file mode 100644
index 0000000..d8cbab9
--- /dev/null
+++ b/xbmc/cdrip/CDDARipJob.cpp
@@ -0,0 +1,259 @@
+/*
+ * 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 "CDDARipJob.h"
+
+#include "Encoder.h"
+#include "EncoderAddon.h"
+#include "EncoderFFmpeg.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/log.h"
+
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/CharsetConverter.h"
+#endif
+
+using namespace ADDON;
+using namespace MUSIC_INFO;
+using namespace XFILE;
+using namespace KODI::CDRIP;
+
+CCDDARipJob::CCDDARipJob(const std::string& input,
+ const std::string& output,
+ const CMusicInfoTag& tag,
+ int encoder,
+ bool eject,
+ unsigned int rate,
+ unsigned int channels,
+ unsigned int bps)
+ : m_rate(rate),
+ m_channels(channels),
+ m_bps(bps),
+ m_tag(tag),
+ m_input(input),
+ m_output(CUtil::MakeLegalPath(output)),
+ m_eject(eject),
+ m_encoder(encoder)
+{
+}
+
+CCDDARipJob::~CCDDARipJob() = default;
+
+bool CCDDARipJob::DoWork()
+{
+ CLog::Log(LOGINFO, "CCDDARipJob::{} - Start ripping track {} to {}", __func__, m_input, m_output);
+
+ // if we are ripping to a samba share, rip it to hd first and then copy it to the share
+ CFileItem file(m_output, false);
+ if (file.IsRemote())
+ m_output = SetupTempFile();
+
+ if (m_output.empty())
+ {
+ CLog::Log(LOGERROR, "CCDDARipJob::{} - Error opening file", __func__);
+ return false;
+ }
+
+ // init ripper
+ CFile reader;
+ std::unique_ptr<CEncoder> encoder{};
+ if (!reader.Open(m_input, READ_CACHED) || !(encoder = SetupEncoder(reader)))
+ {
+ CLog::Log(LOGERROR, "CCDDARipJob::{} - Opening failed", __func__);
+ return false;
+ }
+
+ // setup the progress dialog
+ CGUIDialogExtendedProgressBar* pDlgProgress =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(
+ WINDOW_DIALOG_EXT_PROGRESS);
+ CGUIDialogProgressBarHandle* handle = pDlgProgress->GetHandle(g_localizeStrings.Get(605));
+
+ int iTrack = atoi(m_input.substr(13, m_input.size() - 13 - 5).c_str());
+ std::string strLine0 =
+ StringUtils::Format("{:02}. {} - {}", iTrack, m_tag.GetArtistString(), m_tag.GetTitle());
+ handle->SetText(strLine0);
+
+ // start ripping
+ int percent = 0;
+ int oldpercent = 0;
+ bool cancelled(false);
+ int result;
+ while (!cancelled && (result = RipChunk(reader, encoder, percent)) == 0)
+ {
+ cancelled = ShouldCancel(percent, 100);
+ if (percent > oldpercent)
+ {
+ oldpercent = percent;
+ handle->SetPercentage(static_cast<float>(percent));
+ }
+ }
+
+ // close encoder ripper
+ encoder->EncoderClose();
+ encoder.reset();
+ reader.Close();
+
+ if (file.IsRemote() && !cancelled && result == 2)
+ {
+ // copy the ripped track to the share
+ if (!CFile::Copy(m_output, file.GetPath()))
+ {
+ CLog::Log(LOGERROR, "CCDDARipJob::{} - Error copying file from {} to {}", __func__, m_output,
+ file.GetPath());
+ CFile::Delete(m_output);
+ return false;
+ }
+ // delete cached file
+ CFile::Delete(m_output);
+ }
+
+ if (cancelled)
+ {
+ CLog::Log(LOGWARNING, "CCDDARipJob::{} - User Cancelled CDDA Rip", __func__);
+ CFile::Delete(m_output);
+ }
+ else if (result == 1)
+ CLog::Log(LOGERROR, "CCDDARipJob::{} - Error ripping {}", __func__, m_input);
+ else if (result < 0)
+ CLog::Log(LOGERROR, "CCDDARipJob::{} - Error encoding {}", __func__, m_input);
+ else
+ {
+ CLog::Log(LOGINFO, "CCDDARipJob::{} - Finished ripping {}", __func__, m_input);
+ if (m_eject)
+ {
+ CLog::Log(LOGINFO, "CCDDARipJob::{} - Ejecting CD", __func__);
+ CServiceBroker::GetMediaManager().EjectTray();
+ }
+ }
+
+ handle->MarkFinished();
+
+ return !cancelled && result == 2;
+}
+
+int CCDDARipJob::RipChunk(CFile& reader, const std::unique_ptr<CEncoder>& encoder, int& percent)
+{
+ percent = 0;
+
+ uint8_t stream[1024];
+
+ // get data
+ ssize_t result = reader.Read(stream, 1024);
+
+ // return if rip is done or on some kind of error
+ if (result <= 0)
+ return 1;
+
+ // encode data
+ ssize_t encres = encoder->EncoderEncode(stream, result);
+
+ // Get progress indication
+ percent = static_cast<int>(reader.GetPosition() * 100 / reader.GetLength());
+
+ if (reader.GetPosition() == reader.GetLength())
+ return 2;
+
+ return -(1 - encres);
+}
+
+std::unique_ptr<CEncoder> CCDDARipJob::SetupEncoder(CFile& reader)
+{
+ std::unique_ptr<CEncoder> encoder;
+ const std::string audioEncoder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_AUDIOCDS_ENCODER);
+ if (audioEncoder == "audioencoder.kodi.builtin.aac" ||
+ audioEncoder == "audioencoder.kodi.builtin.wma")
+ {
+ encoder = std::make_unique<CEncoderFFmpeg>();
+ }
+ else
+ {
+ const AddonInfoPtr addonInfo =
+ CServiceBroker::GetAddonMgr().GetAddonInfo(audioEncoder, AddonType::AUDIOENCODER);
+ if (addonInfo)
+ {
+ encoder = std::make_unique<CEncoderAddon>(addonInfo);
+ }
+ }
+ if (!encoder)
+ return std::unique_ptr<CEncoder>{};
+
+ // we have to set the tags before we init the Encoder
+ const std::string strTrack = StringUtils::Format(
+ "{}", std::stol(m_input.substr(13, m_input.size() - 13 - 5), nullptr, 10));
+
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ encoder->SetComment(std::string("Ripped with ") + CSysInfo::GetAppName());
+ encoder->SetArtist(StringUtils::Join(m_tag.GetArtist(), itemSeparator));
+ encoder->SetTitle(m_tag.GetTitle());
+ encoder->SetAlbum(m_tag.GetAlbum());
+ encoder->SetAlbumArtist(StringUtils::Join(m_tag.GetAlbumArtist(), itemSeparator));
+ encoder->SetGenre(StringUtils::Join(m_tag.GetGenre(), itemSeparator));
+ encoder->SetTrack(strTrack);
+ encoder->SetTrackLength(static_cast<int>(reader.GetLength()));
+ encoder->SetYear(m_tag.GetYearString());
+
+ // init encoder
+ if (!encoder->EncoderInit(m_output, m_channels, m_rate, m_bps))
+ encoder.reset();
+
+ return encoder;
+}
+
+std::string CCDDARipJob::SetupTempFile()
+{
+ char tmp[MAX_PATH + 1];
+#if defined(TARGET_WINDOWS)
+ using namespace KODI::PLATFORM::WINDOWS;
+ wchar_t tmpW[MAX_PATH];
+ GetTempFileName(ToW(CSpecialProtocol::TranslatePath("special://temp/")).c_str(), L"riptrack", 0,
+ tmpW);
+ auto tmpString = FromW(tmpW);
+ strncpy_s(tmp, tmpString.length(), tmpString.c_str(), MAX_PATH);
+#else
+ int fd;
+ strncpy(tmp, CSpecialProtocol::TranslatePath("special://temp/riptrackXXXXXX").c_str(), MAX_PATH);
+ if ((fd = mkstemp(tmp)) == -1)
+ tmp[0] = '\0';
+ if (fd != -1)
+ close(fd);
+#endif
+ return tmp;
+}
+
+bool CCDDARipJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) == 0)
+ {
+ const CCDDARipJob* rjob = dynamic_cast<const CCDDARipJob*>(job);
+ if (rjob)
+ {
+ return m_input == rjob->m_input && m_output == rjob->m_output;
+ }
+ }
+ return false;
+}
diff --git a/xbmc/cdrip/CDDARipJob.h b/xbmc/cdrip/CDDARipJob.h
new file mode 100644
index 0000000..11d92eb
--- /dev/null
+++ b/xbmc/cdrip/CDDARipJob.h
@@ -0,0 +1,92 @@
+/*
+ * 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 "music/tags/MusicInfoTag.h"
+#include "utils/Job.h"
+
+namespace XFILE
+{
+class CFile;
+}
+
+namespace KODI
+{
+namespace CDRIP
+{
+
+class CEncoder;
+
+class CCDDARipJob : public CJob
+{
+public:
+ /*!
+ * \brief Construct a ripper job
+ *
+ * \param[in] input The input file url
+ * \param[in] output The output file url
+ * \param[in] tag The music tag to attach to track
+ * \param[in] encoder The encoder to use. See Encoder.h
+ * \param[in] eject Should we eject tray on finish?
+ * \param[in] rate The sample rate of the input
+ * \param[in] channels Number of audio channels in input
+ * \param[in] bps The bits per sample for input
+ */
+ CCDDARipJob(const std::string& input,
+ const std::string& output,
+ const MUSIC_INFO::CMusicInfoTag& tag,
+ int encoder,
+ bool eject = false,
+ unsigned int rate = 44100,
+ unsigned int channels = 2,
+ unsigned int bps = 16);
+
+ ~CCDDARipJob() override;
+
+ const char* GetType() const override { return "cdrip"; }
+ bool operator==(const CJob* job) const override;
+ bool DoWork() override;
+ std::string GetOutput() const { return m_output; }
+
+protected:
+ /*!
+ * \brief Setup the audio encoder
+ */
+ std::unique_ptr<CEncoder> SetupEncoder(XFILE::CFile& reader);
+
+ /*!
+ * \brief Helper used if output is a remote url
+ */
+ std::string SetupTempFile();
+
+ /*!
+ * \brief Rip a chunk of audio
+ *
+ * \param[in] reader The input reader
+ * \param[in] encoder The audio encoder
+ * \param[out] percent The percentage completed on return
+ * \return 0 (CDDARIP_OK) if everything went okay, or
+ * a positive error code from the reader, or
+ * -1 if the encoder failed
+ * \sa CCDDARipper::GetData, CEncoder::Encode
+ */
+ int RipChunk(XFILE::CFile& reader, const std::unique_ptr<CEncoder>& encoder, int& percent);
+
+ unsigned int m_rate; //< The sample rate of the input file
+ unsigned int m_channels; //< The number of channels in input file
+ unsigned int m_bps; //< The bits per sample of input
+ MUSIC_INFO::CMusicInfoTag m_tag; //< Music tag to attach to output file
+ std::string m_input; //< The input url
+ std::string m_output; //< The output url
+ bool m_eject; //< Should we eject tray when we are finished?
+ int m_encoder; //< The audio encoder
+};
+
+} /* namespace CDRIP */
+} /* namespace KODI */
diff --git a/xbmc/cdrip/CDDARipper.cpp b/xbmc/cdrip/CDDARipper.cpp
new file mode 100644
index 0000000..fdea7b1
--- /dev/null
+++ b/xbmc/cdrip/CDDARipper.cpp
@@ -0,0 +1,326 @@
+/*
+ * 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 "CDDARipper.h"
+
+#include "CDDARipJob.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/CDDADirectory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicDbUrl.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/infoscanner/MusicInfoScanner.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/tags/MusicInfoTagLoaderFactory.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/SettingPath.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "storage/MediaManager.h"
+#include "utils/LabelFormatter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace ADDON;
+using namespace XFILE;
+using namespace MUSIC_INFO;
+using namespace KODI::MESSAGING;
+using namespace KODI::CDRIP;
+
+CCDDARipper& CCDDARipper::GetInstance()
+{
+ static CCDDARipper sRipper;
+ return sRipper;
+}
+
+CCDDARipper::CCDDARipper() : CJobQueue(false, 1) //enforce fifo and non-parallel processing
+{
+}
+
+CCDDARipper::~CCDDARipper() = default;
+
+// rip a single track from cd
+bool CCDDARipper::RipTrack(CFileItem* pItem)
+{
+ // don't rip non cdda items
+ if (!URIUtils::HasExtension(pItem->GetPath(), ".cdda"))
+ {
+ CLog::Log(LOGDEBUG, "CCDDARipper::{} - File '{}' is not a cdda track", __func__,
+ pItem->GetPath());
+ return false;
+ }
+
+ // construct directory where the track is stored
+ std::string strDirectory;
+ int legalType;
+ if (!CreateAlbumDir(*pItem->GetMusicInfoTag(), strDirectory, legalType))
+ return false;
+
+ std::string strFile = URIUtils::AddFileToFolder(
+ strDirectory, CUtil::MakeLegalFileName(GetTrackName(pItem), legalType));
+
+ AddJob(new CCDDARipJob(pItem->GetPath(), strFile, *pItem->GetMusicInfoTag(),
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_AUDIOCDS_ENCODER)));
+
+ return true;
+}
+
+bool CCDDARipper::RipCD()
+{
+ // return here if cd is not a CDDA disc
+ MEDIA_DETECT::CCdInfo* pInfo = CServiceBroker::GetMediaManager().GetCdInfo();
+ if (pInfo == nullptr || !pInfo->IsAudio(1))
+ {
+ CLog::Log(LOGDEBUG, "CCDDARipper::{} - CD is not an audio cd", __func__);
+ return false;
+ }
+
+ // get cd cdda contents
+ CFileItemList vecItems;
+ XFILE::CCDDADirectory directory;
+ directory.GetDirectory(CURL("cdda://local/"), vecItems);
+
+ // get cddb info
+ for (int i = 0; i < vecItems.Size(); ++i)
+ {
+ CFileItemPtr pItem = vecItems[i];
+ CMusicInfoTagLoaderFactory factory;
+ std::unique_ptr<IMusicInfoTagLoader> pLoader(factory.CreateLoader(*pItem));
+ if (nullptr != pLoader)
+ {
+ pLoader->Load(pItem->GetPath(), *pItem->GetMusicInfoTag()); // get tag from file
+ if (!pItem->GetMusicInfoTag()->Loaded())
+ break; // No CDDB info available
+ }
+ }
+
+ // construct directory where the tracks are stored
+ std::string strDirectory;
+ int legalType;
+ if (!CreateAlbumDir(*vecItems[0]->GetMusicInfoTag(), strDirectory, legalType))
+ return false;
+
+ // rip all tracks one by one
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ for (int i = 0; i < vecItems.Size(); i++)
+ {
+ CFileItemPtr item = vecItems[i];
+
+ // construct filename
+ std::string strFile = URIUtils::AddFileToFolder(
+ strDirectory, CUtil::MakeLegalFileName(GetTrackName(item.get()), legalType));
+
+ // don't rip non cdda items
+ if (item->GetPath().find(".cdda") == std::string::npos)
+ continue;
+
+ bool eject =
+ settings->GetBool(CSettings::SETTING_AUDIOCDS_EJECTONRIP) && i == vecItems.Size() - 1;
+ AddJob(new CCDDARipJob(item->GetPath(), strFile, *item->GetMusicInfoTag(),
+ settings->GetInt(CSettings::SETTING_AUDIOCDS_ENCODER), eject));
+ }
+
+ return true;
+}
+
+bool CCDDARipper::CreateAlbumDir(const MUSIC_INFO::CMusicInfoTag& infoTag,
+ std::string& strDirectory,
+ int& legalType)
+{
+ std::shared_ptr<CSettingPath> recordingpathSetting = std::static_pointer_cast<CSettingPath>(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(
+ CSettings::SETTING_AUDIOCDS_RECORDINGPATH));
+ if (recordingpathSetting != nullptr)
+ {
+ strDirectory = recordingpathSetting->GetValue();
+ if (strDirectory.empty())
+ {
+ if (CGUIControlButtonSetting::GetPath(recordingpathSetting, &g_localizeStrings))
+ strDirectory = recordingpathSetting->GetValue();
+ }
+ }
+ URIUtils::AddSlashAtEnd(strDirectory);
+
+ if (strDirectory.size() < 3)
+ {
+ // no rip path has been set, show error
+ CLog::Log(LOGERROR, "CCDDARipper::{} - Required path has not been set", __func__);
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{608});
+ return false;
+ }
+
+ legalType = LEGAL_NONE;
+ CFileItem ripPath(strDirectory, true);
+ if (ripPath.IsSmb())
+ legalType = LEGAL_WIN32_COMPAT;
+#ifdef TARGET_WINDOWS
+ if (ripPath.IsHD())
+ legalType = LEGAL_WIN32_COMPAT;
+#endif
+
+ std::string strAlbumDir = GetAlbumDirName(infoTag);
+
+ if (!strAlbumDir.empty())
+ {
+ strDirectory = URIUtils::AddFileToFolder(strDirectory, strAlbumDir);
+ URIUtils::AddSlashAtEnd(strDirectory);
+ }
+
+ strDirectory = CUtil::MakeLegalPath(strDirectory, legalType);
+
+ // Create directory if it doesn't exist
+ if (!CUtil::CreateDirectoryEx(strDirectory))
+ {
+ CLog::Log(LOGERROR, "CCDDARipper::{} - Unable to create directory '{}'", __func__,
+ strDirectory);
+ return false;
+ }
+
+ return true;
+}
+
+std::string CCDDARipper::GetAlbumDirName(const MUSIC_INFO::CMusicInfoTag& infoTag)
+{
+ std::string strAlbumDir;
+
+ // use audiocds.trackpathformat setting to format
+ // directory name where CD tracks will be stored,
+ // use only format part ending at the last '/'
+ strAlbumDir = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_AUDIOCDS_TRACKPATHFORMAT);
+ size_t pos = strAlbumDir.find_last_of("/\\");
+ if (pos == std::string::npos)
+ return ""; // no directory
+
+ strAlbumDir = strAlbumDir.substr(0, pos);
+
+ // replace %A with album artist name
+ if (strAlbumDir.find("%A") != std::string::npos)
+ {
+ std::string strAlbumArtist = infoTag.GetAlbumArtistString();
+ if (strAlbumArtist.empty())
+ strAlbumArtist = infoTag.GetArtistString();
+ if (strAlbumArtist.empty())
+ strAlbumArtist = "Unknown Artist";
+ else
+ StringUtils::Replace(strAlbumArtist, '/', '_');
+ StringUtils::Replace(strAlbumDir, "%A", strAlbumArtist);
+ }
+
+ // replace %B with album title
+ if (strAlbumDir.find("%B") != std::string::npos)
+ {
+ std::string strAlbum = infoTag.GetAlbum();
+ if (strAlbum.empty())
+ strAlbum = StringUtils::Format("Unknown Album {}",
+ CDateTime::GetCurrentDateTime().GetAsLocalizedDateTime());
+ else
+ StringUtils::Replace(strAlbum, '/', '_');
+ StringUtils::Replace(strAlbumDir, "%B", strAlbum);
+ }
+
+ // replace %G with genre
+ if (strAlbumDir.find("%G") != std::string::npos)
+ {
+ std::string strGenre = StringUtils::Join(
+ infoTag.GetGenre(),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ if (strGenre.empty())
+ strGenre = "Unknown Genre";
+ else
+ StringUtils::Replace(strGenre, '/', '_');
+ StringUtils::Replace(strAlbumDir, "%G", strGenre);
+ }
+
+ // replace %Y with year
+ if (strAlbumDir.find("%Y") != std::string::npos)
+ {
+ std::string strYear = infoTag.GetYearString();
+ if (strYear.empty())
+ strYear = "Unknown Year";
+ else
+ StringUtils::Replace(strYear, '/', '_');
+ StringUtils::Replace(strAlbumDir, "%Y", strYear);
+ }
+
+ return strAlbumDir;
+}
+
+std::string CCDDARipper::GetTrackName(CFileItem* item)
+{
+ // get track number from "cdda://local/01.cdda"
+ int trackNumber = atoi(item->GetPath().substr(13, item->GetPath().size() - 13 - 5).c_str());
+
+ // Format up our ripped file label
+ CFileItem destItem(*item);
+ destItem.SetLabel("");
+
+ // get track file name format from audiocds.trackpathformat setting,
+ // use only format part starting from the last '/'
+ std::string strFormat = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_AUDIOCDS_TRACKPATHFORMAT);
+ size_t pos = strFormat.find_last_of("/\\");
+ if (pos != std::string::npos)
+ strFormat.erase(0, pos + 1);
+
+ CLabelFormatter formatter(strFormat, "");
+ formatter.FormatLabel(&destItem);
+
+ // grab the label to use it as our ripped filename
+ std::string track = destItem.GetLabel();
+ if (track.empty())
+ track = StringUtils::Format("{}{:02}", "Track-", trackNumber);
+
+ const std::string encoder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_AUDIOCDS_ENCODER);
+ const AddonInfoPtr addonInfo =
+ CServiceBroker::GetAddonMgr().GetAddonInfo(encoder, AddonType::AUDIOENCODER);
+ if (addonInfo)
+ track += addonInfo->Type(AddonType::AUDIOENCODER)->GetValue("@extension").asString();
+
+ return track;
+}
+
+void CCDDARipper::OnJobComplete(unsigned int jobID, bool success, CJob* job)
+{
+ if (success)
+ {
+ if (CJobQueue::QueueEmpty())
+ {
+ std::string dir = URIUtils::GetDirectory(static_cast<CCDDARipJob*>(job)->GetOutput());
+ bool unimportant;
+ int source = CUtil::GetMatchingSource(
+ dir, *CMediaSourceSettings::GetInstance().CMediaSourceSettings::GetSources("music"),
+ unimportant);
+
+ CMusicDatabase database;
+ database.Open();
+ if (source >= 0 && database.InsideScannedPath(dir))
+ CMusicLibraryQueue::GetInstance().ScanLibrary(
+ dir, MUSIC_INFO::CMusicInfoScanner::SCAN_NORMAL, false);
+
+ database.Close();
+ }
+ return CJobQueue::OnJobComplete(jobID, success, job);
+ }
+
+ CancelJobs();
+}
diff --git a/xbmc/cdrip/CDDARipper.h b/xbmc/cdrip/CDDARipper.h
new file mode 100644
index 0000000..0f61b7c
--- /dev/null
+++ b/xbmc/cdrip/CDDARipper.h
@@ -0,0 +1,102 @@
+/*
+ * 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 "utils/JobManager.h"
+
+#include <string>
+
+class CFileItem;
+
+namespace MUSIC_INFO
+{
+class CMusicInfoTag;
+}
+
+namespace KODI
+{
+namespace CDRIP
+{
+
+/*!
+ * \brief Rip an entire CD or a single track
+ *
+ * The CCDDARipper class is used to rip an entire CD or just a single track.
+ * Tracks are stored in a folder constructed from two user settings: audiocds.recordingpath and
+ * audiocds.trackpathformat. The former is the absolute file system path for the root folder
+ * where ripped music is stored, and the latter specifies the format for the album subfolder and
+ * for the track file name.
+ * Format used to encode ripped tracks is defined by the audiocds.encoder user setting, and
+ * there are several choices: wav, ogg vorbis and mp3.
+ */
+class CCDDARipper : public CJobQueue
+{
+public:
+ /*!
+ * \brief The only way through which the global instance of the CDDARipper should be accessed.
+ *
+ * \return the global instance.
+ */
+ static CCDDARipper& GetInstance();
+
+ /*!
+ * \brief Rip a single track
+ *
+ * \param[in] pItem CFileItem representing a track to rip
+ * \return true if success, false if failure
+ */
+ bool RipTrack(CFileItem* pItem);
+
+ /*!
+ * \brief Rip an entire CD
+ *
+ * \return true if success, false if failure
+ */
+ bool RipCD();
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob* job) override;
+
+private:
+ // private construction and no assignments
+ CCDDARipper();
+ CCDDARipper(const CCDDARipper&) = delete;
+ ~CCDDARipper() override;
+ CCDDARipper const& operator=(CCDDARipper const&) = delete;
+
+ /*!
+ * \brief Create folder where CD tracks will be stored
+ *
+ * \param[in] infoTag music info tags for the CD, used to format album name
+ * \param[out] strDirectory full path of the created folder
+ * \param[out] legalType created directory type (see LEGAL_... constants)
+ * \return true if success, false if failure
+ */
+ bool CreateAlbumDir(const MUSIC_INFO::CMusicInfoTag& infoTag,
+ std::string& strDirectory,
+ int& legalType);
+
+ /*!
+ * \brief Return formatted album subfolder for rip path
+ *
+ * \param infoTag music info tags for the CD, used to format album name
+ * \return album subfolder path name
+ */
+ std::string GetAlbumDirName(const MUSIC_INFO::CMusicInfoTag& infoTag);
+
+ /*!
+ * \brief Return file name for the track
+ *
+ * \param[in] item CFileItem representing a track
+ * \return track file name
+ */
+ std::string GetTrackName(CFileItem* item);
+};
+
+} /* namespace CDRIP */
+} /* namespace KODI */
diff --git a/xbmc/cdrip/CMakeLists.txt b/xbmc/cdrip/CMakeLists.txt
new file mode 100644
index 0000000..4e591d2
--- /dev/null
+++ b/xbmc/cdrip/CMakeLists.txt
@@ -0,0 +1,17 @@
+set(SOURCES CDDARipJob.cpp
+ Encoder.cpp
+ EncoderAddon.cpp
+ EncoderFFmpeg.cpp)
+
+set(HEADERS CDDARipJob.h
+ Encoder.h
+ EncoderAddon.h
+ EncoderFFmpeg.h
+ IEncoder.h)
+
+if(ENABLE_OPTICAL)
+ list(APPEND SOURCES CDDARipper.cpp)
+ list(APPEND HEADERS CDDARipper.h)
+endif()
+
+core_add_library(cdrip)
diff --git a/xbmc/cdrip/Encoder.cpp b/xbmc/cdrip/Encoder.cpp
new file mode 100644
index 0000000..fa78f84
--- /dev/null
+++ b/xbmc/cdrip/Encoder.cpp
@@ -0,0 +1,159 @@
+/*
+ * 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 "Encoder.h"
+
+#include "filesystem/File.h"
+#include "utils/log.h"
+
+#include <string.h>
+#include <utility>
+
+using namespace KODI::CDRIP;
+
+CEncoder::CEncoder() = default;
+
+CEncoder::~CEncoder()
+{
+ FileClose();
+}
+
+bool CEncoder::EncoderInit(const std::string& strFile, int iInChannels, int iInRate, int iInBits)
+{
+ m_dwWriteBufferPointer = 0;
+ m_strFile = strFile;
+ m_iInChannels = iInChannels;
+ m_iInSampleRate = iInRate;
+ m_iInBitsPerSample = iInBits;
+
+ if (!FileCreate(strFile))
+ {
+ CLog::Log(LOGERROR, "CEncoder::{} - Cannot open file: {}", __func__, strFile);
+ return false;
+ }
+
+ return Init();
+}
+
+ssize_t CEncoder::EncoderEncode(uint8_t* pbtStream, size_t nNumBytesRead)
+{
+ const int iBytes = Encode(pbtStream, nNumBytesRead);
+ if (iBytes < 0)
+ {
+ CLog::Log(LOGERROR, "CEncoder::{} - Internal encoder error: {}", __func__, iBytes);
+ return 0;
+ }
+ return 1;
+}
+
+bool CEncoder::EncoderClose()
+{
+ if (!Close())
+ return false;
+
+ FlushStream();
+ FileClose();
+
+ return true;
+}
+
+bool CEncoder::FileCreate(const std::string& filename)
+{
+ m_file = std::make_unique<XFILE::CFile>();
+ if (m_file)
+ return m_file->OpenForWrite(filename, true);
+ return false;
+}
+
+bool CEncoder::FileClose()
+{
+ if (m_file)
+ {
+ m_file->Close();
+ m_file.reset();
+ }
+ return true;
+}
+
+// return total bytes written, or -1 on error
+ssize_t CEncoder::FileWrite(const uint8_t* pBuffer, size_t iBytes)
+{
+ if (!m_file)
+ return -1;
+
+ const ssize_t dwBytesWritten = m_file->Write(pBuffer, iBytes);
+ if (dwBytesWritten <= 0)
+ return -1;
+
+ return dwBytesWritten;
+}
+
+ssize_t CEncoder::Seek(ssize_t iFilePosition, int iWhence)
+{
+ if (!m_file)
+ return -1;
+ FlushStream();
+ return m_file->Seek(iFilePosition, iWhence);
+}
+
+// write the stream to our writebuffer, and write the buffer to disk if it's full
+ssize_t CEncoder::Write(const uint8_t* pBuffer, size_t iBytes)
+{
+ if ((WRITEBUFFER_SIZE - m_dwWriteBufferPointer) > iBytes)
+ {
+ // writebuffer is big enough to fit data
+ memcpy(m_btWriteBuffer + m_dwWriteBufferPointer, pBuffer, iBytes);
+ m_dwWriteBufferPointer += iBytes;
+ return iBytes;
+ }
+ else
+ {
+ // buffer is not big enough to fit data
+ if (m_dwWriteBufferPointer == 0)
+ {
+ // nothing in our buffer, just write the entire pBuffer to disk
+ return FileWrite(pBuffer, iBytes);
+ }
+
+ const size_t dwBytesRemaining = iBytes - (WRITEBUFFER_SIZE - m_dwWriteBufferPointer);
+ // fill up our write buffer and write it to disk
+ memcpy(m_btWriteBuffer + m_dwWriteBufferPointer, pBuffer,
+ (WRITEBUFFER_SIZE - m_dwWriteBufferPointer));
+ FileWrite(m_btWriteBuffer, WRITEBUFFER_SIZE);
+ m_dwWriteBufferPointer = 0;
+
+ // pbtRemaining = pBuffer + bytesWritten
+ const uint8_t* pbtRemaining = pBuffer + (iBytes - dwBytesRemaining);
+ if (dwBytesRemaining > WRITEBUFFER_SIZE)
+ {
+ // data is not going to fit in our buffer, just write it to disk
+ if (FileWrite(pbtRemaining, dwBytesRemaining) == -1)
+ return -1;
+ return iBytes;
+ }
+ else
+ {
+ // copy remaining bytes to our currently empty writebuffer
+ memcpy(m_btWriteBuffer, pbtRemaining, dwBytesRemaining);
+ m_dwWriteBufferPointer = dwBytesRemaining;
+ return iBytes;
+ }
+ }
+}
+
+// flush the contents of our writebuffer
+ssize_t CEncoder::FlushStream()
+{
+ if (m_dwWriteBufferPointer == 0)
+ return 0;
+
+ const ssize_t iResult = FileWrite(m_btWriteBuffer, m_dwWriteBufferPointer);
+ m_dwWriteBufferPointer = 0;
+
+ return iResult;
+}
diff --git a/xbmc/cdrip/Encoder.h b/xbmc/cdrip/Encoder.h
new file mode 100644
index 0000000..644296d
--- /dev/null
+++ b/xbmc/cdrip/Encoder.h
@@ -0,0 +1,67 @@
+/*
+ * 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 "IEncoder.h"
+
+#include <memory>
+#include <stdint.h>
+#include <stdio.h>
+#include <string>
+
+namespace XFILE
+{
+class CFile;
+}
+
+namespace KODI
+{
+namespace CDRIP
+{
+
+constexpr size_t WRITEBUFFER_SIZE = 131072; // 128k buffer
+
+class CEncoder : public IEncoder
+{
+public:
+ CEncoder();
+ virtual ~CEncoder();
+
+ bool EncoderInit(const std::string& strFile, int iInChannels, int iInRate, int iInBits);
+ ssize_t EncoderEncode(uint8_t* pbtStream, size_t nNumBytesRead);
+ bool EncoderClose();
+
+ void SetComment(const std::string& str) { m_strComment = str; }
+ void SetArtist(const std::string& str) { m_strArtist = str; }
+ void SetTitle(const std::string& str) { m_strTitle = str; }
+ void SetAlbum(const std::string& str) { m_strAlbum = str; }
+ void SetAlbumArtist(const std::string& str) { m_strAlbumArtist = str; }
+ void SetGenre(const std::string& str) { m_strGenre = str; }
+ void SetTrack(const std::string& str) { m_strTrack = str; }
+ void SetTrackLength(int length) { m_iTrackLength = length; }
+ void SetYear(const std::string& str) { m_strYear = str; }
+
+protected:
+ virtual ssize_t Write(const uint8_t* pBuffer, size_t iBytes);
+ virtual ssize_t Seek(ssize_t iFilePosition, int iWhence);
+
+private:
+ bool FileCreate(const std::string& filename);
+ bool FileClose();
+ ssize_t FileWrite(const uint8_t* pBuffer, size_t iBytes);
+ ssize_t FlushStream();
+
+ std::unique_ptr<XFILE::CFile> m_file;
+
+ uint8_t m_btWriteBuffer[WRITEBUFFER_SIZE]; // 128k buffer for writing to disc
+ size_t m_dwWriteBufferPointer{0};
+};
+
+} /* namespace CDRIP */
+} /* namespace KODI */
diff --git a/xbmc/cdrip/EncoderAddon.cpp b/xbmc/cdrip/EncoderAddon.cpp
new file mode 100644
index 0000000..4d250ad
--- /dev/null
+++ b/xbmc/cdrip/EncoderAddon.cpp
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2013 Arne Morten Kvarving
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "EncoderAddon.h"
+
+using namespace ADDON;
+using namespace KODI::CDRIP;
+
+CEncoderAddon::CEncoderAddon(const AddonInfoPtr& addonInfo)
+ : IAddonInstanceHandler(ADDON_INSTANCE_AUDIOENCODER, addonInfo)
+{
+ // Create "C" interface structures, used as own parts to prevent API problems on update
+ m_ifc.audioencoder = new AddonInstance_AudioEncoder();
+ m_ifc.audioencoder->toAddon = new KodiToAddonFuncTable_AudioEncoder();
+ m_ifc.audioencoder->toKodi = new AddonToKodiFuncTable_AudioEncoder();
+ m_ifc.audioencoder->toKodi->kodiInstance = this;
+ m_ifc.audioencoder->toKodi->write = cb_write;
+ m_ifc.audioencoder->toKodi->seek = cb_seek;
+}
+
+CEncoderAddon::~CEncoderAddon()
+{
+ // Delete "C" interface structures
+ delete m_ifc.audioencoder->toKodi;
+ delete m_ifc.audioencoder->toAddon;
+ delete m_ifc.audioencoder;
+}
+
+bool CEncoderAddon::Init()
+{
+ if (CreateInstance() != ADDON_STATUS_OK || !m_ifc.audioencoder->toAddon->start)
+ return false;
+
+ KODI_ADDON_AUDIOENCODER_INFO_TAG tag{};
+ tag.channels = m_iInChannels;
+ tag.samplerate = m_iInSampleRate;
+ tag.bits_per_sample = m_iInBitsPerSample;
+ tag.track_length = m_iTrackLength;
+ tag.title = m_strTitle.c_str();
+ tag.artist = m_strArtist.c_str();
+ tag.album_artist = m_strAlbumArtist.c_str();
+ tag.album = m_strAlbum.c_str();
+ tag.release_date = m_strYear.c_str();
+ tag.track = atoi(m_strTrack.c_str());
+ tag.genre = m_strGenre.c_str();
+ tag.comment = m_strComment.c_str();
+
+ return m_ifc.audioencoder->toAddon->start(m_ifc.hdl, &tag);
+}
+
+ssize_t CEncoderAddon::Encode(uint8_t* pbtStream, size_t nNumBytesRead)
+{
+ if (m_ifc.audioencoder->toAddon->encode)
+ return m_ifc.audioencoder->toAddon->encode(m_ifc.hdl, pbtStream, nNumBytesRead);
+ return 0;
+}
+
+bool CEncoderAddon::Close()
+{
+ bool ret = false;
+ if (m_ifc.audioencoder->toAddon->finish)
+ ret = m_ifc.audioencoder->toAddon->finish(m_ifc.hdl);
+
+ DestroyInstance();
+
+ return ret;
+}
+
+ssize_t CEncoderAddon::Write(const uint8_t* data, size_t len)
+{
+ return CEncoder::Write(data, len);
+}
+
+ssize_t CEncoderAddon::Seek(ssize_t pos, int whence)
+{
+ return CEncoder::Seek(pos, whence);
+}
+
+ssize_t CEncoderAddon::cb_write(KODI_HANDLE kodiInstance, const uint8_t* data, size_t len)
+{
+ if (!kodiInstance || !data)
+ return -1;
+ return static_cast<CEncoderAddon*>(kodiInstance)->Write(data, len);
+}
+
+ssize_t CEncoderAddon::cb_seek(KODI_HANDLE kodiInstance, ssize_t pos, int whence)
+{
+ if (!kodiInstance)
+ return -1;
+ return static_cast<CEncoderAddon*>(kodiInstance)->Seek(pos, whence);
+}
diff --git a/xbmc/cdrip/EncoderAddon.h b/xbmc/cdrip/EncoderAddon.h
new file mode 100644
index 0000000..6721d28
--- /dev/null
+++ b/xbmc/cdrip/EncoderAddon.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 Arne Morten Kvarving
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "Encoder.h"
+#include "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/AudioEncoder.h"
+
+namespace KODI
+{
+namespace CDRIP
+{
+
+class CEncoderAddon : public CEncoder, public ADDON::IAddonInstanceHandler
+{
+public:
+ explicit CEncoderAddon(const ADDON::AddonInfoPtr& addonInfo);
+ ~CEncoderAddon() override;
+
+ // Child functions related to IEncoder within CEncoder
+ bool Init() override;
+ ssize_t Encode(uint8_t* pbtStream, size_t nNumBytesRead) override;
+ bool Close() override;
+
+ // Addon callback functions
+ ssize_t Write(const uint8_t* data, size_t len) override;
+ ssize_t Seek(ssize_t pos, int whence) override;
+
+private:
+ // Currently needed addon interface parts
+ //@{
+ static ssize_t cb_write(KODI_HANDLE kodiInstance, const uint8_t* data, size_t len);
+ static ssize_t cb_seek(KODI_HANDLE kodiInstance, ssize_t pos, int whence);
+ //@}
+};
+
+} /* namespace CDRIP */
+} /* namespace KODI */
diff --git a/xbmc/cdrip/EncoderFFmpeg.cpp b/xbmc/cdrip/EncoderFFmpeg.cpp
new file mode 100644
index 0000000..2867b7c
--- /dev/null
+++ b/xbmc/cdrip/EncoderFFmpeg.cpp
@@ -0,0 +1,413 @@
+/*
+ * 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 "EncoderFFmpeg.h"
+
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace KODI::CDRIP;
+
+namespace
+{
+
+struct EncoderException : public std::exception
+{
+ std::string s;
+ template<typename... Args>
+ EncoderException(const std::string& fmt, Args&&... args)
+ : s(StringUtils::Format(fmt, std::forward<Args>(args)...))
+ {
+ }
+ ~EncoderException() throw() {} // Updated
+ const char* what() const throw() { return s.c_str(); }
+};
+
+} /* namespace */
+
+bool CEncoderFFmpeg::Init()
+{
+ try
+ {
+ ADDON::AddonPtr addon;
+ const std::string addonId = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_AUDIOCDS_ENCODER);
+ bool success =
+ CServiceBroker::GetAddonMgr().GetAddon(addonId, addon, ADDON::OnlyEnabled::CHOICE_YES);
+ int bitrate;
+ if (success && addon)
+ {
+ addon->GetSettingInt("bitrate", bitrate);
+ bitrate *= 1000; /* Multiply as on settings as kbps */
+ }
+ else
+ {
+ throw EncoderException("Could not get add-on: {}", addonId);
+ }
+
+ // Hack fix about PTS on generated files.
+ // - AAC need to multiply with sample rate
+ // - Note: Within Kodi it can still played without use of sample rate, only becomes by VLC the problem visible,
+ // - WMA need only the multiply with 1000
+ if (addonId == "audioencoder.kodi.builtin.aac")
+ m_samplesCountMultiply = m_iInSampleRate;
+ else if (addonId == "audioencoder.kodi.builtin.wma")
+ m_samplesCountMultiply = 1000;
+ else
+ throw EncoderException("Internal add-on id \"{}\" not known as usable", addonId);
+
+ const std::string filename = URIUtils::GetFileName(m_strFile);
+
+ m_formatCtx = avformat_alloc_context();
+ if (!m_formatCtx)
+ throw EncoderException("Could not allocate output format context");
+
+ m_bcBuffer = static_cast<uint8_t*>(av_malloc(BUFFER_SIZE + AV_INPUT_BUFFER_PADDING_SIZE));
+ if (!m_bcBuffer)
+ throw EncoderException("Could not allocate buffer");
+
+ m_formatCtx->pb = avio_alloc_context(m_bcBuffer, BUFFER_SIZE, AVIO_FLAG_WRITE, this, nullptr,
+ avio_write_callback, avio_seek_callback);
+ if (!m_formatCtx->pb)
+ throw EncoderException("Failed to allocate ByteIOContext");
+
+ /* Guess the desired container format based on the file extension. */
+ m_formatCtx->oformat = av_guess_format(nullptr, filename.c_str(), nullptr);
+ if (!m_formatCtx->oformat)
+ throw EncoderException("Could not find output file format");
+
+ m_formatCtx->url = av_strdup(filename.c_str());
+ if (!m_formatCtx->url)
+ throw EncoderException("Could not allocate url");
+
+ /* Find the encoder to be used by its name. */
+ AVCodec* codec = avcodec_find_encoder(m_formatCtx->oformat->audio_codec);
+ if (!codec)
+ throw EncoderException("Unable to find a suitable FFmpeg encoder");
+
+ /* Create a new audio stream in the output file container. */
+ m_stream = avformat_new_stream(m_formatCtx, nullptr);
+ if (!m_stream)
+ throw EncoderException("Failed to allocate AVStream context");
+
+ m_codecCtx = avcodec_alloc_context3(codec);
+ if (!m_codecCtx)
+ throw EncoderException("Failed to allocate the encoder context");
+
+ /* Set the basic encoder parameters.
+ * The input file's sample rate is used to avoid a sample rate conversion. */
+ m_codecCtx->channels = m_iInChannels;
+ m_codecCtx->channel_layout = av_get_default_channel_layout(m_iInChannels);
+ m_codecCtx->sample_rate = m_iInSampleRate;
+ m_codecCtx->sample_fmt = codec->sample_fmts[0];
+ m_codecCtx->bit_rate = bitrate;
+
+ /* Allow experimental encoders (like FFmpeg builtin AAC encoder) */
+ m_codecCtx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
+
+ /* Set the sample rate for the container. */
+ m_codecCtx->time_base.num = 1;
+ m_codecCtx->time_base.den = m_iInSampleRate;
+
+ /* Some container formats (like MP4) require global headers to be present.
+ * Mark the encoder so that it behaves accordingly. */
+ if (m_formatCtx->oformat->flags & AVFMT_GLOBALHEADER)
+ m_codecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+
+ int err = avcodec_open2(m_codecCtx, codec, nullptr);
+ if (err < 0)
+ throw EncoderException("Failed to open the codec {} (error '{}')",
+ codec->long_name ? codec->long_name : codec->name,
+ FFmpegErrorToString(err));
+
+ err = avcodec_parameters_from_context(m_stream->codecpar, m_codecCtx);
+ if (err < 0)
+ throw EncoderException("Failed to copy encoder parameters to output stream (error '{}')",
+ FFmpegErrorToString(err));
+
+ m_inFormat = GetInputFormat(m_iInBitsPerSample);
+ m_outFormat = m_codecCtx->sample_fmt;
+ m_needConversion = (m_outFormat != m_inFormat);
+
+ /* calculate how many bytes we need per frame */
+ m_neededFrames = m_codecCtx->frame_size;
+ m_neededBytes =
+ av_samples_get_buffer_size(nullptr, m_iInChannels, m_neededFrames, m_inFormat, 0);
+ m_buffer = static_cast<uint8_t*>(av_malloc(m_neededBytes));
+ m_bufferSize = 0;
+
+ m_bufferFrame = av_frame_alloc();
+ if (!m_bufferFrame || !m_buffer)
+ throw EncoderException("Failed to allocate necessary buffers");
+
+ m_bufferFrame->nb_samples = m_codecCtx->frame_size;
+ m_bufferFrame->format = m_inFormat;
+ m_bufferFrame->channel_layout = m_codecCtx->channel_layout;
+ m_bufferFrame->sample_rate = m_codecCtx->sample_rate;
+
+ err = av_frame_get_buffer(m_bufferFrame, 0);
+ if (err < 0)
+ throw EncoderException("Could not allocate output frame samples (error '{}')",
+ FFmpegErrorToString(err));
+
+ avcodec_fill_audio_frame(m_bufferFrame, m_iInChannels, m_inFormat, m_buffer, m_neededBytes, 0);
+
+ if (m_needConversion)
+ {
+ m_swrCtx = swr_alloc_set_opts(nullptr, m_codecCtx->channel_layout, m_outFormat,
+ m_codecCtx->sample_rate, m_codecCtx->channel_layout, m_inFormat,
+ m_codecCtx->sample_rate, 0, nullptr);
+ if (!m_swrCtx || swr_init(m_swrCtx) < 0)
+ throw EncoderException("Failed to initialize the resampler");
+
+ m_resampledBufferSize =
+ av_samples_get_buffer_size(nullptr, m_iInChannels, m_neededFrames, m_outFormat, 0);
+ m_resampledBuffer = static_cast<uint8_t*>(av_malloc(m_resampledBufferSize));
+ m_resampledFrame = av_frame_alloc();
+ if (!m_resampledBuffer || !m_resampledFrame)
+ throw EncoderException("Failed to allocate a frame for resampling");
+
+ m_resampledFrame->nb_samples = m_neededFrames;
+ m_resampledFrame->format = m_outFormat;
+ m_resampledFrame->channel_layout = m_codecCtx->channel_layout;
+ m_resampledFrame->sample_rate = m_codecCtx->sample_rate;
+
+ err = av_frame_get_buffer(m_resampledFrame, 0);
+ if (err < 0)
+ throw EncoderException("Could not allocate output resample frame samples (error '{}')",
+ FFmpegErrorToString(err));
+
+ avcodec_fill_audio_frame(m_resampledFrame, m_iInChannels, m_outFormat, m_resampledBuffer,
+ m_resampledBufferSize, 0);
+ }
+
+ /* set the tags */
+ SetTag("album", m_strAlbum);
+ SetTag("album_artist", m_strArtist);
+ SetTag("genre", m_strGenre);
+ SetTag("title", m_strTitle);
+ SetTag("track", m_strTrack);
+ SetTag("encoder", CSysInfo::GetAppName() + " FFmpeg Encoder");
+
+ /* write the header */
+ err = avformat_write_header(m_formatCtx, nullptr);
+ if (err != 0)
+ throw EncoderException("Failed to write the header (error '{}')", FFmpegErrorToString(err));
+
+ CLog::Log(LOGDEBUG, "CEncoderFFmpeg::{} - Successfully initialized with muxer {} and codec {}",
+ __func__,
+ m_formatCtx->oformat->long_name ? m_formatCtx->oformat->long_name
+ : m_formatCtx->oformat->name,
+ codec->long_name ? codec->long_name : codec->name);
+ }
+ catch (EncoderException& caught)
+ {
+ CLog::Log(LOGERROR, "CEncoderFFmpeg::{} - {}", __func__, caught.what());
+
+ av_freep(&m_buffer);
+ av_frame_free(&m_bufferFrame);
+ swr_free(&m_swrCtx);
+ av_frame_free(&m_resampledFrame);
+ av_freep(&m_resampledBuffer);
+ av_free(m_bcBuffer);
+ avcodec_free_context(&m_codecCtx);
+ if (m_formatCtx)
+ {
+ av_freep(&m_formatCtx->pb);
+ avformat_free_context(m_formatCtx);
+ }
+ return false;
+ }
+
+ return true;
+}
+
+void CEncoderFFmpeg::SetTag(const std::string& tag, const std::string& value)
+{
+ av_dict_set(&m_formatCtx->metadata, tag.c_str(), value.c_str(), 0);
+}
+
+int CEncoderFFmpeg::avio_write_callback(void* opaque, uint8_t* buf, int buf_size)
+{
+ CEncoderFFmpeg* enc = static_cast<CEncoderFFmpeg*>(opaque);
+ if (enc->Write(buf, buf_size) != buf_size)
+ {
+ CLog::Log(LOGERROR, "CEncoderFFmpeg::{} - Error writing FFmpeg buffer to file", __func__);
+ return -1;
+ }
+ return buf_size;
+}
+
+int64_t CEncoderFFmpeg::avio_seek_callback(void* opaque, int64_t offset, int whence)
+{
+ CEncoderFFmpeg* enc = static_cast<CEncoderFFmpeg*>(opaque);
+ return enc->Seek(offset, whence);
+}
+
+ssize_t CEncoderFFmpeg::Encode(uint8_t* pbtStream, size_t nNumBytesRead)
+{
+ while (nNumBytesRead > 0)
+ {
+ size_t space = m_neededBytes - m_bufferSize;
+ size_t copy = nNumBytesRead > space ? space : nNumBytesRead;
+
+ memcpy(&m_buffer[m_bufferSize], pbtStream, copy);
+ m_bufferSize += copy;
+ pbtStream += copy;
+ nNumBytesRead -= copy;
+
+ /* only write full packets */
+ if (m_bufferSize == m_neededBytes)
+ {
+ if (!WriteFrame())
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+bool CEncoderFFmpeg::WriteFrame()
+{
+ int err = AVERROR_UNKNOWN;
+ AVPacket* pkt = av_packet_alloc();
+ if (!pkt)
+ {
+ CLog::Log(LOGERROR, "CEncoderFFmpeg::{} - av_packet_alloc failed: {}", __func__,
+ strerror(errno));
+ return false;
+ }
+
+ try
+ {
+ AVFrame* frame;
+ if (m_needConversion)
+ {
+ //! @bug libavresample isn't const correct
+ if (swr_convert(m_swrCtx, m_resampledFrame->data, m_neededFrames,
+ const_cast<const uint8_t**>(m_bufferFrame->extended_data),
+ m_neededFrames) < 0)
+ throw EncoderException("Error resampling audio");
+
+ frame = m_resampledFrame;
+ }
+ else
+ frame = m_bufferFrame;
+
+ if (frame)
+ {
+ /* To fix correct length on wma files */
+ frame->pts = m_samplesCount;
+ m_samplesCount += frame->nb_samples * m_samplesCountMultiply / m_codecCtx->time_base.den;
+ }
+
+ m_bufferSize = 0;
+ err = avcodec_send_frame(m_codecCtx, frame);
+ if (err < 0)
+ throw EncoderException("Error sending a frame for encoding (error '{}')", __func__,
+ FFmpegErrorToString(err));
+
+ while (err >= 0)
+ {
+ err = avcodec_receive_packet(m_codecCtx, pkt);
+ if (err == AVERROR(EAGAIN) || err == AVERROR_EOF)
+ {
+ av_packet_free(&pkt);
+ return (err == AVERROR(EAGAIN)) ? false : true;
+ }
+ else if (err < 0)
+ {
+ throw EncoderException("Error during encoding (error '{}')", __func__,
+ FFmpegErrorToString(err));
+ }
+
+ err = av_write_frame(m_formatCtx, pkt);
+ if (err < 0)
+ throw EncoderException("Failed to write the frame data (error '{}')", __func__,
+ FFmpegErrorToString(err));
+
+ av_packet_unref(pkt);
+ }
+ }
+ catch (EncoderException& caught)
+ {
+ CLog::Log(LOGERROR, "CEncoderFFmpeg::{} - {}", __func__, caught.what());
+ }
+
+ av_packet_free(&pkt);
+
+ return (err) ? false : true;
+}
+
+bool CEncoderFFmpeg::Close()
+{
+ if (m_formatCtx)
+ {
+ /* if there is anything still in the buffer */
+ if (m_bufferSize > 0)
+ {
+ /* zero the unused space so we dont encode random junk */
+ memset(&m_buffer[m_bufferSize], 0, m_neededBytes - m_bufferSize);
+ /* write any remaining data */
+ WriteFrame();
+ }
+
+ /* Flush if needed */
+ av_freep(&m_buffer);
+ av_frame_free(&m_bufferFrame);
+ swr_free(&m_swrCtx);
+ av_frame_free(&m_resampledFrame);
+ av_freep(&m_resampledBuffer);
+ m_needConversion = false;
+
+ WriteFrame();
+
+ /* write the trailer */
+ av_write_trailer(m_formatCtx);
+
+ /* cleanup */
+ av_free(m_bcBuffer);
+ avcodec_free_context(&m_codecCtx);
+ av_freep(&m_formatCtx->pb);
+ avformat_free_context(m_formatCtx);
+ }
+
+ m_bufferSize = 0;
+
+ return true;
+}
+
+AVSampleFormat CEncoderFFmpeg::GetInputFormat(int inBitsPerSample)
+{
+ switch (inBitsPerSample)
+ {
+ case 8:
+ return AV_SAMPLE_FMT_U8;
+ case 16:
+ return AV_SAMPLE_FMT_S16;
+ case 32:
+ return AV_SAMPLE_FMT_S32;
+ default:
+ throw EncoderException("Invalid input bits per sample");
+ }
+}
+
+std::string CEncoderFFmpeg::FFmpegErrorToString(int err)
+{
+ std::string text;
+ text.reserve(AV_ERROR_MAX_STRING_SIZE);
+ av_strerror(err, text.data(), AV_ERROR_MAX_STRING_SIZE);
+ return text;
+}
diff --git a/xbmc/cdrip/EncoderFFmpeg.h b/xbmc/cdrip/EncoderFFmpeg.h
new file mode 100644
index 0000000..39112ba
--- /dev/null
+++ b/xbmc/cdrip/EncoderFFmpeg.h
@@ -0,0 +1,74 @@
+/*
+ * 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 "Encoder.h"
+
+extern "C"
+{
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libswresample/swresample.h>
+}
+
+namespace KODI
+{
+namespace CDRIP
+{
+
+class CEncoderFFmpeg : public CEncoder
+{
+public:
+ CEncoderFFmpeg() = default;
+ ~CEncoderFFmpeg() override = default;
+
+ bool Init() override;
+ ssize_t Encode(uint8_t* pbtStream, size_t nNumBytesRead) override;
+ bool Close() override;
+
+private:
+ static int avio_write_callback(void* opaque, uint8_t* buf, int buf_size);
+ static int64_t avio_seek_callback(void* opaque, int64_t offset, int whence);
+
+ void SetTag(const std::string& tag, const std::string& value);
+ bool WriteFrame();
+ AVSampleFormat GetInputFormat(int inBitsPerSample);
+ std::string FFmpegErrorToString(int err);
+
+ AVFormatContext* m_formatCtx{nullptr};
+ AVCodecContext* m_codecCtx{nullptr};
+ SwrContext* m_swrCtx{nullptr};
+ AVStream* m_stream{nullptr};
+ AVSampleFormat m_inFormat;
+ AVSampleFormat m_outFormat;
+
+ /* From libavformat/avio.h:
+ * The buffer size is very important for performance.
+ * For protocols with fixed blocksize it should be set to this
+ * blocksize.
+ * For others a typical size is a cache page, e.g. 4kb.
+ */
+ static constexpr size_t BUFFER_SIZE = 4096;
+ uint8_t* m_bcBuffer{nullptr};
+
+ unsigned int m_neededFrames{0};
+ size_t m_neededBytes{0};
+ uint8_t* m_buffer{nullptr};
+ size_t m_bufferSize{0};
+ AVFrame* m_bufferFrame{nullptr};
+ uint8_t* m_resampledBuffer{nullptr};
+ size_t m_resampledBufferSize{0};
+ AVFrame* m_resampledFrame{nullptr};
+ bool m_needConversion{false};
+ int64_t m_samplesCount{0};
+ int64_t m_samplesCountMultiply{1000};
+};
+
+} /* namespace CDRIP */
+} /* namespace KODI */
diff --git a/xbmc/cdrip/IEncoder.h b/xbmc/cdrip/IEncoder.h
new file mode 100644
index 0000000..de484aa
--- /dev/null
+++ b/xbmc/cdrip/IEncoder.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014-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 <stdint.h>
+#include <string>
+
+#include "PlatformDefs.h" // for ssize_t
+
+namespace KODI
+{
+namespace CDRIP
+{
+
+class IEncoder
+{
+public:
+ virtual ~IEncoder() = default;
+ virtual bool Init() = 0;
+ virtual ssize_t Encode(uint8_t* pbtStream, size_t nNumBytesRead) = 0;
+ virtual bool Close() = 0;
+
+ // tag info
+ std::string m_strComment;
+ std::string m_strArtist;
+ std::string m_strAlbumArtist;
+ std::string m_strTitle;
+ std::string m_strAlbum;
+ std::string m_strGenre;
+ std::string m_strTrack;
+ std::string m_strYear;
+ std::string m_strFile;
+ int m_iTrackLength = 0;
+ int m_iInChannels = 0;
+ int m_iInSampleRate = 0;
+ int m_iInBitsPerSample = 0;
+};
+
+} /* namespace CDRIP */
+} /* namespace KODI */
diff --git a/xbmc/commons/Buffer.h b/xbmc/commons/Buffer.h
new file mode 100644
index 0000000..98085ea
--- /dev/null
+++ b/xbmc/commons/Buffer.h
@@ -0,0 +1,254 @@
+/*
+ * 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 <memory>
+#include <string.h>
+#include <string>
+
+namespace XbmcCommons
+{
+ class BufferException final
+ {
+ std::string message;
+
+ public:
+ explicit BufferException(const char* message_) : message(message_) {}
+ };
+
+ /**
+ * This class is based on the java java.nio.Buffer class however, it
+ * does not implement the 'mark' functionality.
+ *
+ * [ the following is borrowed from the javadocs for java.nio.Buffer
+ * where it applies to this class]:
+ *
+ * A buffer is a linear, finite sequence of elements of a unspecified types.
+ * Aside from its content, the essential properties of a buffer are its capacity,
+ * limit, and position:
+ * A buffer's capacity is the number of elements it contains. The capacity
+ * of a buffer is never negative and never changes.
+ *
+ * A buffer's limit is the index of the first element that should not be
+ * read or written. A buffer's limit is never negative and is never greater
+ * than its capacity.
+ *
+ * A buffer's position is the index of the next element to be read or written.
+ * A buffer's position is never negative and is never greater than its limit.
+ *
+ * Invariants:
+ *
+ * The following invariant holds for the mark, position, limit, and capacity values:
+ *
+ * 0 <= mark <= position <= limit <= capacity
+ *
+ * A newly-created buffer always has a position of zero and a limit set to the
+ * capacity. The initial content of a buffer is, in general, undefined.
+ *
+ * Example:
+ * Buffer buffer(1024);
+ * buffer.putInt(1).putString("hello there").putLongLong( ((long long)2)^40 );
+ * buffer.flip();
+ * std::cout << "buffer contents:" << buffer.getInt() << ", ";
+ * std::cout << buffer.getCharPointerDirect() << ", ";
+ * std::cout << buffer.getLongLong() << std::endl;
+ *
+ * Note: the 'gets' are sensitive to the order-of-operations. Therefore, while
+ * the above is correct, it would be wrong to chain the output as follows:
+ *
+ * std::cout << "buffer contents:" << buffer.getInt() << ", " << std::cout
+ * << buffer.getCharPointerDirect() << ", " << buffer.getLongLong()
+ * << std::endl;
+ *
+ * This would result in the get's executing from right to left and therefore would
+ * produce totally erroneous results. This is also a problem when the values are
+ * passed to a method as in:
+ *
+ * printf("buffer contents: %d, \"%s\", %ll\n", buffer.getInt(),
+ * buffer.getCharPointerDirect(), buffer.getLongLong());
+ *
+ * This would also produce erroneous results as they get's will be evaluated
+ * from right to left in the parameter list of printf.
+ */
+ class Buffer
+ {
+ std::shared_ptr<unsigned char> bufferRef;
+ unsigned char* buffer = nullptr;
+ size_t mposition = 0;
+ size_t mcapacity = 0;
+ size_t mlimit = 0;
+
+ inline void check(size_t count) const
+ {
+ if ((mposition + count) > mlimit)
+ throw BufferException("Buffer buffer overflow: Cannot add more data to the Buffer's buffer.");
+ }
+
+ public:
+ /**
+ * Construct an uninitialized buffer instance, perhaps as an lvalue.
+ */
+ inline Buffer() { clear(); }
+
+ /**
+ * Construct a buffer given an externally managed memory buffer.
+ * The ownership of the buffer is assumed to be the code that called
+ * this constructor, therefore the Buffer destructor will not free it.
+ *
+ * The newly constructed buffer is considered empty and is ready to
+ * have data written into it.
+ *
+ * If you want to read from the buffer you just created, you can use:
+ *
+ * Buffer b = Buffer(buf,bufSize).forward(bufSize).flip();
+ */
+ inline Buffer(void* buffer_, size_t bufferSize) : buffer((unsigned char*)buffer_), mcapacity(bufferSize)
+ {
+ clear();
+ }
+
+ /**
+ * Construct a buffer buffer using the size buffer provided. The
+ * buffer will be internally managed and potentially shared with
+ * other Buffer instances. It will be freed upon destruction of
+ * the last Buffer that references it.
+ */
+ inline explicit Buffer(size_t bufferSize) : buffer(bufferSize ? new unsigned char[bufferSize] : NULL), mcapacity(bufferSize)
+ {
+ clear();
+ bufferRef.reset(buffer, std::default_delete<unsigned char[]>());
+ }
+
+ /**
+ * Copy another buffer. This is a "shallow copy" and therefore
+ * shares the underlying data buffer with the Buffer it is a copy
+ * of. Changes made to the data through this buffer will be seen
+ * in the source buffer and vice/vrs. However, each buffer maintains
+ * its own indexing.
+ */
+ inline Buffer(const Buffer& buf) = default;
+
+ /**
+ * Copy another buffer. This is a "shallow copy" and therefore
+ * shares the underlying data buffer with the Buffer it is a copy
+ * of. Changes made to the data through this buffer will be seen
+ * in the source buffer and vice/vrs. However, each buffer maintains
+ * its own indexing.
+ */
+ inline Buffer& operator=(const Buffer& buf)
+ {
+ buffer = buf.buffer;
+ bufferRef = buf.bufferRef;
+ mcapacity = buf.mcapacity;
+ mlimit = buf.mlimit;
+ return *this;
+ }
+
+ inline Buffer& allocate(size_t bufferSize)
+ {
+ buffer = bufferSize ? new unsigned char[bufferSize] : NULL;
+ bufferRef.reset(buffer, std::default_delete<unsigned char[]>());
+ mcapacity = bufferSize;
+ clear();
+ return *this;
+ }
+
+ /**
+ * Flips this buffer. The limit is set to the current position
+ * and then the position is set to zero.
+ *
+ * After a sequence of channel-read or put operations, invoke this
+ * method to prepare for a sequence of channel-write or relative
+ * get operations. For example:
+ *
+ * buf.put(magic); // Prepend header
+ * in.read(buf); // Read data into rest of buffer
+ * buf.flip(); // Flip buffer
+ * out.write(buf); // Write header + data to channel
+ *
+ * This is used to prepare the Buffer for reading from after
+ * it has been written to.
+ */
+ inline Buffer& flip() { mlimit = mposition; mposition = 0; return *this; }
+
+ /**
+ *Clears this buffer. The position is set to zero, the limit
+ * is set to the capacity.
+ *
+ * Invoke this method before using a sequence of channel-read
+ * or put operations to fill this buffer. For example:
+ *
+ * buf.clear(); // Prepare buffer for reading
+ * in.read(buf); // Read data
+ *
+ * This method does not actually erase the data in the buffer,
+ * but it is named as if it did because it will most often be used
+ * in situations in which that might as well be the case.
+ */
+ inline Buffer& clear() { mlimit = mcapacity; mposition = 0; return *this; }
+
+ /**
+ * This method resets the position to the beginning of the buffer
+ * so that it can be either reread or written to all over again.
+ */
+ inline Buffer& rewind() { mposition = 0; return *this; }
+
+ /**
+ * This method provides for the remaining number of bytes
+ * that can be read out of the buffer or written into the
+ * buffer before it's finished.
+ */
+ inline size_t remaining() const { return mlimit - mposition; }
+
+ inline Buffer& put(const void* src, size_t bytes)
+ { check(bytes); memcpy( buffer + mposition, src, bytes); mposition += bytes; return *this; }
+ inline Buffer& get(void* dest, size_t bytes)
+ { check(bytes); memcpy( dest, buffer + mposition, bytes); mposition += bytes; return *this; }
+
+ inline unsigned char* data() const { return buffer; }
+ inline unsigned char* curPosition() const { return buffer + mposition; }
+ inline Buffer& setPosition(size_t position) { mposition = position; return *this; }
+ inline Buffer& forward(size_t positionIncrement)
+ { check(positionIncrement); mposition += positionIncrement; return *this; }
+
+ inline size_t limit() const { return mlimit; }
+ inline size_t capacity() const { return mcapacity; }
+ inline size_t position() const { return mposition; }
+
+#define DEFAULTBUFFERRELATIVERW(name,type) \
+ inline Buffer& put##name(const type & val) { return put(&val, sizeof(type)); } \
+ inline type get##name() { type ret; get(&ret, sizeof(type)); return ret; }
+
+ DEFAULTBUFFERRELATIVERW(Bool,bool);
+ DEFAULTBUFFERRELATIVERW(Int,int);
+ DEFAULTBUFFERRELATIVERW(Char,char);
+ DEFAULTBUFFERRELATIVERW(Long,long);
+ DEFAULTBUFFERRELATIVERW(Float,float);
+ DEFAULTBUFFERRELATIVERW(Double,double);
+ DEFAULTBUFFERRELATIVERW(Pointer,void*);
+ DEFAULTBUFFERRELATIVERW(LongLong,long long);
+#undef DEFAULTBUFFERRELATIVERW
+
+ inline Buffer& putString(const char* str) { size_t len = strlen(str) + 1; check(len); put(str, len); return (*this); }
+ inline Buffer& putString(const std::string& str) { size_t len = str.length() + 1; check(len); put(str.c_str(), len); return (*this); }
+
+ inline std::string getString() { std::string ret((const char*)(buffer + mposition)); size_t len = ret.length() + 1; check(len); mposition += len; return ret; }
+ inline std::string getString(size_t length)
+ {
+ check(length);
+ std::string ret((const char*)(buffer + mposition),length);
+ mposition += length;
+ return ret;
+ }
+ inline char* getCharPointerDirect() { char* ret = (char*)(buffer + mposition); size_t len = strlen(ret) + 1; check(len); mposition += len; return ret; }
+
+ };
+
+}
+
diff --git a/xbmc/commons/CMakeLists.txt b/xbmc/commons/CMakeLists.txt
new file mode 100644
index 0000000..ff6245d
--- /dev/null
+++ b/xbmc/commons/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES Exception.cpp)
+
+set(HEADERS Buffer.h
+ Exception.h
+ ilog.h)
+
+core_add_library(commons)
diff --git a/xbmc/commons/Exception.cpp b/xbmc/commons/Exception.cpp
new file mode 100644
index 0000000..b9bd2e5
--- /dev/null
+++ b/xbmc/commons/Exception.cpp
@@ -0,0 +1,22 @@
+/*
+ * 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 "Exception.h"
+
+#include "utils/log.h"
+
+namespace XbmcCommons
+{
+ Exception::~Exception() = default;
+
+ void Exception::LogThrowMessage(const char* prefix) const
+ {
+ CLog::Log(LOGERROR, "EXCEPTION Thrown ({}) : {}", classname, message);
+ }
+}
+
diff --git a/xbmc/commons/Exception.h b/xbmc/commons/Exception.h
new file mode 100644
index 0000000..ea2c9bc
--- /dev/null
+++ b/xbmc/commons/Exception.h
@@ -0,0 +1,113 @@
+/*
+ * 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
+
+//---------------------------------------------------------
+// This include should be moved to commons but even as it is,
+// it wont cause a linker circular dependency since it's just
+// a header.
+#include "utils/StringUtils.h"
+//---------------------------------------------------------
+#include <stdarg.h>
+
+
+#ifdef __GNUC__
+// The 'this' pointer counts as a parameter on member methods.
+#define XBMCCOMMONS_ATTRIB_EXCEPTION_FORMAT __attribute__((format(printf,2,3)))
+#else
+#define XBMCCOMMONS_ATTRIB_EXCEPTION_FORMAT
+#endif
+
+#define XBMCCOMMONS_COPYVARARGS(fmt) va_list argList; va_start(argList, fmt); Set(fmt, argList); va_end(argList)
+#define XBMCCOMMONS_STANDARD_EXCEPTION(E) \
+ class E : public XbmcCommons::Exception \
+ { \
+ public: \
+ inline E(const char* message,...) XBMCCOMMONS_ATTRIB_EXCEPTION_FORMAT : Exception(#E) { XBMCCOMMONS_COPYVARARGS(message); } \
+ \
+ inline E(const E& other) : Exception(other) {} \
+ }
+
+namespace XbmcCommons
+{
+ /**
+ * This class a superclass for exceptions that want to utilize some
+ * utility functionality including autologging with the specific
+ * exception name.
+ */
+ class Exception
+ {
+ private:
+
+ std::string classname;
+ std::string message;
+
+ protected:
+
+ inline explicit Exception(const char* classname_) : classname(classname_) { }
+ inline Exception(const char* classname_, const char* message_) : classname(classname_), message(message_) { }
+ inline Exception(const Exception& other) = default;
+
+ /**
+ * This method is called from the constructor of subclasses. It
+ * will set the message from varargs as well as call log message
+ */
+ inline void Set(const char* fmt, va_list& argList)
+ {
+ message = StringUtils::FormatV(fmt, argList);
+ }
+
+ /**
+ * This message can be called from the constructor of subclasses.
+ * It will set the message and log the throwing.
+ */
+ inline void SetMessage(const char* fmt, ...) XBMCCOMMONS_ATTRIB_EXCEPTION_FORMAT
+ {
+ // calls 'set'
+ XBMCCOMMONS_COPYVARARGS(fmt);
+ }
+
+ inline void setClassname(const char* cn) { classname = cn; }
+
+ public:
+ virtual ~Exception();
+
+ virtual void LogThrowMessage(const char* prefix = NULL) const;
+
+ inline virtual const char* GetExMessage() const { return message.c_str(); }
+ };
+
+ /**
+ * This class forms the base class for unchecked exceptions. Unchecked exceptions
+ * are those that really shouldn't be handled explicitly. For example, on windows
+ * when a access violation is converted to a win32_exception, there's nothing
+ * that can be done in most code. The outer most stack frame might try to
+ * do some error logging prior to shutting down, but that's really it.
+ */
+ XBMCCOMMONS_STANDARD_EXCEPTION(UncheckedException);
+
+/**
+ * In cases where you catch(...){} you will (may) inadvertently be
+ * catching UncheckedException's. Therefore this macro will allow
+ * you to do something equivalent to:
+ * catch (anything except UncheckedException) {}
+ *
+ * In order to avoid catching UncheckedException, use the macro as follows:
+ *
+ * try { ... }
+ * XBMCCOMMONS_HANDLE_UNCHECKED
+ * catch(...){ ... }
+ */
+// Yes. I recognize that the name of this macro is an oxymoron.
+#define XBMCCOMMONS_HANDLE_UNCHECKED \
+ catch (const XbmcCommons::UncheckedException& ) { throw; } \
+ catch (const XbmcCommons::UncheckedException* ) { throw; }
+
+}
+
diff --git a/xbmc/commons/ilog.h b/xbmc/commons/ilog.h
new file mode 100644
index 0000000..10b3880
--- /dev/null
+++ b/xbmc/commons/ilog.h
@@ -0,0 +1,46 @@
+/*
+ * 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
+
+#define LOG_LEVEL_NONE -1 // nothing at all is logged
+#define LOG_LEVEL_NORMAL 0 // shows notice, error, severe and fatal
+#define LOG_LEVEL_DEBUG 1 // shows all
+#define LOG_LEVEL_DEBUG_FREEMEM 2 // shows all + shows freemem on screen
+#define LOG_LEVEL_MAX LOG_LEVEL_DEBUG_FREEMEM
+
+// ones we use in the code
+#define LOGDEBUG 0
+#define LOGINFO 1
+#define LOGWARNING 2
+#define LOGERROR 3
+#define LOGFATAL 4
+#define LOGNONE 5
+
+// extra masks - from bit 5
+#define LOGMASKBIT 5
+#define LOGMASK ((1 << LOGMASKBIT) - 1)
+
+#define LOGSAMBA (1 << (LOGMASKBIT + 0))
+#define LOGCURL (1 << (LOGMASKBIT + 1))
+#define LOGFFMPEG (1 << (LOGMASKBIT + 2))
+#define LOGDBUS (1 << (LOGMASKBIT + 4))
+#define LOGJSONRPC (1 << (LOGMASKBIT + 5))
+#define LOGAUDIO (1 << (LOGMASKBIT + 6))
+#define LOGAIRTUNES (1 << (LOGMASKBIT + 7))
+#define LOGUPNP (1 << (LOGMASKBIT + 8))
+#define LOGCEC (1 << (LOGMASKBIT + 9))
+#define LOGVIDEO (1 << (LOGMASKBIT + 10))
+#define LOGWEBSERVER (1 << (LOGMASKBIT + 11))
+#define LOGDATABASE (1 << (LOGMASKBIT + 12))
+#define LOGAVTIMING (1 << (LOGMASKBIT + 13))
+#define LOGWINDOWING (1 << (LOGMASKBIT + 14))
+#define LOGPVR (1 << (LOGMASKBIT + 15))
+#define LOGEPG (1 << (LOGMASKBIT + 16))
+#define LOGANNOUNCE (1 << (LOGMASKBIT + 17))
+#define LOGWSDISCOVERY (1 << (LOGMASKBIT + 18))
diff --git a/xbmc/contrib/kissfft/CMakeLists.txt b/xbmc/contrib/kissfft/CMakeLists.txt
new file mode 100644
index 0000000..e11a6f8
--- /dev/null
+++ b/xbmc/contrib/kissfft/CMakeLists.txt
@@ -0,0 +1,10 @@
+if(ENABLE_INTERNAL_KISSFFT)
+ set(SOURCES kiss_fft.c
+ kiss_fftr.c)
+
+ set(HEADERS _kiss_fft_guts.h
+ kiss_fft.h
+ kiss_fftr.h)
+
+ core_add_library(kissfft)
+endif()
diff --git a/xbmc/contrib/kissfft/COPYING b/xbmc/contrib/kissfft/COPYING
new file mode 100644
index 0000000..6b4b622
--- /dev/null
+++ b/xbmc/contrib/kissfft/COPYING
@@ -0,0 +1,11 @@
+Copyright (c) 2003-2010 Mark Borgerding . All rights reserved.
+
+KISS FFT is provided under:
+
+ SPDX-License-Identifier: BSD-3-Clause
+
+Being under the terms of the BSD 3-clause "New" or "Revised" License,
+according with:
+
+ LICENSES/BSD-3-Clause
+
diff --git a/xbmc/contrib/kissfft/_kiss_fft_guts.h b/xbmc/contrib/kissfft/_kiss_fft_guts.h
new file mode 100644
index 0000000..0a2feee
--- /dev/null
+++ b/xbmc/contrib/kissfft/_kiss_fft_guts.h
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+/* kiss_fft.h
+ defines kiss_fft_scalar as either short or a float type
+ and defines
+ typedef struct { kiss_fft_scalar r; kiss_fft_scalar i; }kiss_fft_cpx; */
+#include "kiss_fft.h"
+#include <limits.h>
+
+#define MAXFACTORS 32
+/* e.g. an fft of length 128 has 4 factors
+ as far as kissfft is concerned
+ 4*4*4*2
+ */
+
+struct kiss_fft_state{
+ int nfft;
+ int inverse;
+ int factors[2*MAXFACTORS];
+ kiss_fft_cpx twiddles[1];
+};
+
+/*
+ Explanation of macros dealing with complex math:
+
+ C_MUL(m,a,b) : m = a*b
+ C_FIXDIV( c , div ) : if a fixed point impl., c /= div. noop otherwise
+ C_SUB( res, a,b) : res = a - b
+ C_SUBFROM( res , a) : res -= a
+ C_ADDTO( res , a) : res += a
+ * */
+#ifdef FIXED_POINT
+#if (FIXED_POINT==32)
+# define FRACBITS 31
+# define SAMPPROD int64_t
+#define SAMP_MAX 2147483647
+#else
+# define FRACBITS 15
+# define SAMPPROD int32_t
+#define SAMP_MAX 32767
+#endif
+
+#define SAMP_MIN -SAMP_MAX
+
+#if defined(CHECK_OVERFLOW)
+# define CHECK_OVERFLOW_OP(a,op,b) \
+ if ( (SAMPPROD)(a) op (SAMPPROD)(b) > SAMP_MAX || (SAMPPROD)(a) op (SAMPPROD)(b) < SAMP_MIN ) { \
+ fprintf(stderr,"WARNING:overflow @ " __FILE__ "(%d): (%d " #op" %d) = %ld\n",__LINE__,(a),(b),(SAMPPROD)(a) op (SAMPPROD)(b) ); }
+#endif
+
+
+# define smul(a,b) ( (SAMPPROD)(a)*(b) )
+# define sround( x ) (kiss_fft_scalar)( ( (x) + (1<<(FRACBITS-1)) ) >> FRACBITS )
+
+# define S_MUL(a,b) sround( smul(a,b) )
+
+# define C_MUL(m,a,b) \
+ do{ (m).r = sround( smul((a).r,(b).r) - smul((a).i,(b).i) ); \
+ (m).i = sround( smul((a).r,(b).i) + smul((a).i,(b).r) ); }while(0)
+
+# define DIVSCALAR(x,k) \
+ (x) = sround( smul( x, SAMP_MAX/k ) )
+
+# define C_FIXDIV(c,div) \
+ do { DIVSCALAR( (c).r , div); \
+ DIVSCALAR( (c).i , div); }while (0)
+
+# define C_MULBYSCALAR( c, s ) \
+ do{ (c).r = sround( smul( (c).r , s ) ) ;\
+ (c).i = sround( smul( (c).i , s ) ) ; }while(0)
+
+#else /* not FIXED_POINT*/
+
+# define S_MUL(a,b) ( (a)*(b) )
+#define C_MUL(m,a,b) \
+ do{ (m).r = (a).r*(b).r - (a).i*(b).i;\
+ (m).i = (a).r*(b).i + (a).i*(b).r; }while(0)
+# define C_FIXDIV(c,div) /* NOOP */
+# define C_MULBYSCALAR( c, s ) \
+ do{ (c).r *= (s);\
+ (c).i *= (s); }while(0)
+#endif
+
+#ifndef CHECK_OVERFLOW_OP
+# define CHECK_OVERFLOW_OP(a,op,b) /* noop */
+#endif
+
+#define C_ADD( res, a,b)\
+ do { \
+ CHECK_OVERFLOW_OP((a).r,+,(b).r)\
+ CHECK_OVERFLOW_OP((a).i,+,(b).i)\
+ (res).r=(a).r+(b).r; (res).i=(a).i+(b).i; \
+ }while(0)
+#define C_SUB( res, a,b)\
+ do { \
+ CHECK_OVERFLOW_OP((a).r,-,(b).r)\
+ CHECK_OVERFLOW_OP((a).i,-,(b).i)\
+ (res).r=(a).r-(b).r; (res).i=(a).i-(b).i; \
+ }while(0)
+#define C_ADDTO( res , a)\
+ do { \
+ CHECK_OVERFLOW_OP((res).r,+,(a).r)\
+ CHECK_OVERFLOW_OP((res).i,+,(a).i)\
+ (res).r += (a).r; (res).i += (a).i;\
+ }while(0)
+
+#define C_SUBFROM( res , a)\
+ do {\
+ CHECK_OVERFLOW_OP((res).r,-,(a).r)\
+ CHECK_OVERFLOW_OP((res).i,-,(a).i)\
+ (res).r -= (a).r; (res).i -= (a).i; \
+ }while(0)
+
+
+#ifdef FIXED_POINT
+# define KISS_FFT_COS(phase) floor(.5+SAMP_MAX * cos (phase))
+# define KISS_FFT_SIN(phase) floor(.5+SAMP_MAX * sin (phase))
+# define HALF_OF(x) ((x)>>1)
+#elif defined(USE_SIMD)
+# define KISS_FFT_COS(phase) _mm_set1_ps( cos(phase) )
+# define KISS_FFT_SIN(phase) _mm_set1_ps( sin(phase) )
+# define HALF_OF(x) ((x)*_mm_set1_ps(.5))
+#else
+# define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase)
+# define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase)
+# define HALF_OF(x) ((x)*.5f)
+#endif
+
+#define kf_cexp(x,phase) \
+ do{ \
+ (x)->r = KISS_FFT_COS(phase);\
+ (x)->i = KISS_FFT_SIN(phase);\
+ }while(0)
+
+
+/* a debugging function */
+#define pcpx(c)\
+ fprintf(stderr,"%g + %gi\n",(double)((c)->r),(double)((c)->i) )
+
+
+#ifdef KISS_FFT_USE_ALLOCA
+// define this to allow use of alloca instead of malloc for temporary buffers
+// Temporary buffers are used in two case:
+// 1. FFT sizes that have "bad" factors. i.e. not 2,3 and 5
+// 2. "in-place" FFTs. Notice the quotes, since kissfft does not really do an in-place transform.
+#include <alloca.h>
+#define KISS_FFT_TMP_ALLOC(nbytes) alloca(nbytes)
+#define KISS_FFT_TMP_FREE(ptr)
+#else
+#define KISS_FFT_TMP_ALLOC(nbytes) KISS_FFT_MALLOC(nbytes)
+#define KISS_FFT_TMP_FREE(ptr) KISS_FFT_FREE(ptr)
+#endif
diff --git a/xbmc/contrib/kissfft/kiss_fft.c b/xbmc/contrib/kissfft/kiss_fft.c
new file mode 100644
index 0000000..1db72b5
--- /dev/null
+++ b/xbmc/contrib/kissfft/kiss_fft.c
@@ -0,0 +1,401 @@
+/*
+ * Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#include "_kiss_fft_guts.h"
+/* The guts header contains all the multiplication and addition macros that are defined for
+ fixed or floating point complex numbers. It also declares the kf_ internal functions.
+ */
+
+static void kf_bfly2(
+ kiss_fft_cpx * Fout,
+ const size_t fstride,
+ const kiss_fft_cfg st,
+ int m
+ )
+{
+ kiss_fft_cpx * Fout2;
+ kiss_fft_cpx * tw1 = st->twiddles;
+ kiss_fft_cpx t;
+ Fout2 = Fout + m;
+ do{
+ C_FIXDIV(*Fout,2); C_FIXDIV(*Fout2,2);
+
+ C_MUL (t, *Fout2 , *tw1);
+ tw1 += fstride;
+ C_SUB( *Fout2 , *Fout , t );
+ C_ADDTO( *Fout , t );
+ ++Fout2;
+ ++Fout;
+ }while (--m);
+}
+
+static void kf_bfly4(
+ kiss_fft_cpx * Fout,
+ const size_t fstride,
+ const kiss_fft_cfg st,
+ const size_t m
+ )
+{
+ kiss_fft_cpx *tw1,*tw2,*tw3;
+ kiss_fft_cpx scratch[6];
+ size_t k=m;
+ const size_t m2=2*m;
+ const size_t m3=3*m;
+
+
+ tw3 = tw2 = tw1 = st->twiddles;
+
+ do {
+ C_FIXDIV(*Fout,4); C_FIXDIV(Fout[m],4); C_FIXDIV(Fout[m2],4); C_FIXDIV(Fout[m3],4);
+
+ C_MUL(scratch[0],Fout[m] , *tw1 );
+ C_MUL(scratch[1],Fout[m2] , *tw2 );
+ C_MUL(scratch[2],Fout[m3] , *tw3 );
+
+ C_SUB( scratch[5] , *Fout, scratch[1] );
+ C_ADDTO(*Fout, scratch[1]);
+ C_ADD( scratch[3] , scratch[0] , scratch[2] );
+ C_SUB( scratch[4] , scratch[0] , scratch[2] );
+ C_SUB( Fout[m2], *Fout, scratch[3] );
+ tw1 += fstride;
+ tw2 += fstride*2;
+ tw3 += fstride*3;
+ C_ADDTO( *Fout , scratch[3] );
+
+ if(st->inverse) {
+ Fout[m].r = scratch[5].r - scratch[4].i;
+ Fout[m].i = scratch[5].i + scratch[4].r;
+ Fout[m3].r = scratch[5].r + scratch[4].i;
+ Fout[m3].i = scratch[5].i - scratch[4].r;
+ }else{
+ Fout[m].r = scratch[5].r + scratch[4].i;
+ Fout[m].i = scratch[5].i - scratch[4].r;
+ Fout[m3].r = scratch[5].r - scratch[4].i;
+ Fout[m3].i = scratch[5].i + scratch[4].r;
+ }
+ ++Fout;
+ }while(--k);
+}
+
+static void kf_bfly3(
+ kiss_fft_cpx * Fout,
+ const size_t fstride,
+ const kiss_fft_cfg st,
+ size_t m
+ )
+{
+ size_t k=m;
+ const size_t m2 = 2*m;
+ kiss_fft_cpx *tw1,*tw2;
+ kiss_fft_cpx scratch[5];
+ kiss_fft_cpx epi3;
+ epi3 = st->twiddles[fstride*m];
+
+ tw1=tw2=st->twiddles;
+
+ do{
+ C_FIXDIV(*Fout,3); C_FIXDIV(Fout[m],3); C_FIXDIV(Fout[m2],3);
+
+ C_MUL(scratch[1],Fout[m] , *tw1);
+ C_MUL(scratch[2],Fout[m2] , *tw2);
+
+ C_ADD(scratch[3],scratch[1],scratch[2]);
+ C_SUB(scratch[0],scratch[1],scratch[2]);
+ tw1 += fstride;
+ tw2 += fstride*2;
+
+ Fout[m].r = Fout->r - HALF_OF(scratch[3].r);
+ Fout[m].i = Fout->i - HALF_OF(scratch[3].i);
+
+ C_MULBYSCALAR( scratch[0] , epi3.i );
+
+ C_ADDTO(*Fout,scratch[3]);
+
+ Fout[m2].r = Fout[m].r + scratch[0].i;
+ Fout[m2].i = Fout[m].i - scratch[0].r;
+
+ Fout[m].r -= scratch[0].i;
+ Fout[m].i += scratch[0].r;
+
+ ++Fout;
+ }while(--k);
+}
+
+static void kf_bfly5(
+ kiss_fft_cpx * Fout,
+ const size_t fstride,
+ const kiss_fft_cfg st,
+ int m
+ )
+{
+ kiss_fft_cpx *Fout0,*Fout1,*Fout2,*Fout3,*Fout4;
+ int u;
+ kiss_fft_cpx scratch[13];
+ kiss_fft_cpx * twiddles = st->twiddles;
+ kiss_fft_cpx *tw;
+ kiss_fft_cpx ya,yb;
+ ya = twiddles[fstride*m];
+ yb = twiddles[fstride*2*m];
+
+ Fout0=Fout;
+ Fout1=Fout0+m;
+ Fout2=Fout0+2*m;
+ Fout3=Fout0+3*m;
+ Fout4=Fout0+4*m;
+
+ tw=st->twiddles;
+ for ( u=0; u<m; ++u ) {
+ C_FIXDIV( *Fout0,5); C_FIXDIV( *Fout1,5); C_FIXDIV( *Fout2,5); C_FIXDIV( *Fout3,5); C_FIXDIV( *Fout4,5);
+ scratch[0] = *Fout0;
+
+ C_MUL(scratch[1] ,*Fout1, tw[u*fstride]);
+ C_MUL(scratch[2] ,*Fout2, tw[2*u*fstride]);
+ C_MUL(scratch[3] ,*Fout3, tw[3*u*fstride]);
+ C_MUL(scratch[4] ,*Fout4, tw[4*u*fstride]);
+
+ C_ADD( scratch[7],scratch[1],scratch[4]);
+ C_SUB( scratch[10],scratch[1],scratch[4]);
+ C_ADD( scratch[8],scratch[2],scratch[3]);
+ C_SUB( scratch[9],scratch[2],scratch[3]);
+
+ Fout0->r += scratch[7].r + scratch[8].r;
+ Fout0->i += scratch[7].i + scratch[8].i;
+
+ scratch[5].r = scratch[0].r + S_MUL(scratch[7].r,ya.r) + S_MUL(scratch[8].r,yb.r);
+ scratch[5].i = scratch[0].i + S_MUL(scratch[7].i,ya.r) + S_MUL(scratch[8].i,yb.r);
+
+ scratch[6].r = S_MUL(scratch[10].i,ya.i) + S_MUL(scratch[9].i,yb.i);
+ scratch[6].i = -S_MUL(scratch[10].r,ya.i) - S_MUL(scratch[9].r,yb.i);
+
+ C_SUB(*Fout1,scratch[5],scratch[6]);
+ C_ADD(*Fout4,scratch[5],scratch[6]);
+
+ scratch[11].r = scratch[0].r + S_MUL(scratch[7].r,yb.r) + S_MUL(scratch[8].r,ya.r);
+ scratch[11].i = scratch[0].i + S_MUL(scratch[7].i,yb.r) + S_MUL(scratch[8].i,ya.r);
+ scratch[12].r = - S_MUL(scratch[10].i,yb.i) + S_MUL(scratch[9].i,ya.i);
+ scratch[12].i = S_MUL(scratch[10].r,yb.i) - S_MUL(scratch[9].r,ya.i);
+
+ C_ADD(*Fout2,scratch[11],scratch[12]);
+ C_SUB(*Fout3,scratch[11],scratch[12]);
+
+ ++Fout0;++Fout1;++Fout2;++Fout3;++Fout4;
+ }
+}
+
+/* perform the butterfly for one stage of a mixed radix FFT */
+static void kf_bfly_generic(
+ kiss_fft_cpx * Fout,
+ const size_t fstride,
+ const kiss_fft_cfg st,
+ int m,
+ int p
+ )
+{
+ int u,k,q1,q;
+ kiss_fft_cpx * twiddles = st->twiddles;
+ kiss_fft_cpx t;
+ int Norig = st->nfft;
+
+ kiss_fft_cpx * scratch = (kiss_fft_cpx*)KISS_FFT_TMP_ALLOC(sizeof(kiss_fft_cpx)*p);
+
+ for ( u=0; u<m; ++u ) {
+ k=u;
+ for ( q1=0 ; q1<p ; ++q1 ) {
+ scratch[q1] = Fout[ k ];
+ C_FIXDIV(scratch[q1],p);
+ k += m;
+ }
+
+ k=u;
+ for ( q1=0 ; q1<p ; ++q1 ) {
+ int twidx=0;
+ Fout[ k ] = scratch[0];
+ for (q=1;q<p;++q ) {
+ twidx += fstride * k;
+ if (twidx>=Norig) twidx-=Norig;
+ C_MUL(t,scratch[q] , twiddles[twidx] );
+ C_ADDTO( Fout[ k ] ,t);
+ }
+ k += m;
+ }
+ }
+ KISS_FFT_TMP_FREE(scratch);
+}
+
+static
+void kf_work(
+ kiss_fft_cpx * Fout,
+ const kiss_fft_cpx * f,
+ const size_t fstride,
+ int in_stride,
+ int * factors,
+ const kiss_fft_cfg st
+ )
+{
+ kiss_fft_cpx * Fout_beg=Fout;
+ const int p=*factors++; /* the radix */
+ const int m=*factors++; /* stage's fft length/p */
+ const kiss_fft_cpx * Fout_end = Fout + p*m;
+
+#ifdef _OPENMP
+ // use openmp extensions at the
+ // top-level (not recursive)
+ if (fstride==1 && p<=5)
+ {
+ int k;
+
+ // execute the p different work units in different threads
+# pragma omp parallel for
+ for (k=0;k<p;++k)
+ kf_work( Fout +k*m, f+ fstride*in_stride*k,fstride*p,in_stride,factors,st);
+ // all threads have joined by this point
+
+ switch (p) {
+ case 2: kf_bfly2(Fout,fstride,st,m); break;
+ case 3: kf_bfly3(Fout,fstride,st,m); break;
+ case 4: kf_bfly4(Fout,fstride,st,m); break;
+ case 5: kf_bfly5(Fout,fstride,st,m); break;
+ default: kf_bfly_generic(Fout,fstride,st,m,p); break;
+ }
+ return;
+ }
+#endif
+
+ if (m==1) {
+ do{
+ *Fout = *f;
+ f += fstride*in_stride;
+ }while(++Fout != Fout_end );
+ }else{
+ do{
+ // recursive call:
+ // DFT of size m*p performed by doing
+ // p instances of smaller DFTs of size m,
+ // each one takes a decimated version of the input
+ kf_work( Fout , f, fstride*p, in_stride, factors,st);
+ f += fstride*in_stride;
+ }while( (Fout += m) != Fout_end );
+ }
+
+ Fout=Fout_beg;
+
+ // recombine the p smaller DFTs
+ switch (p) {
+ case 2: kf_bfly2(Fout,fstride,st,m); break;
+ case 3: kf_bfly3(Fout,fstride,st,m); break;
+ case 4: kf_bfly4(Fout,fstride,st,m); break;
+ case 5: kf_bfly5(Fout,fstride,st,m); break;
+ default: kf_bfly_generic(Fout,fstride,st,m,p); break;
+ }
+}
+
+/* facbuf is populated by p1,m1,p2,m2, ...
+ where
+ p[i] * m[i] = m[i-1]
+ m0 = n */
+static
+void kf_factor(int n,int * facbuf)
+{
+ int p=4;
+ double floor_sqrt;
+ floor_sqrt = floor( sqrt((double)n) );
+
+ /*factor out powers of 4, powers of 2, then any remaining primes */
+ do {
+ while (n % p) {
+ switch (p) {
+ case 4: p = 2; break;
+ case 2: p = 3; break;
+ default: p += 2; break;
+ }
+ if (p > floor_sqrt)
+ p = n; /* no more factors, skip to end */
+ }
+ n /= p;
+ *facbuf++ = p;
+ *facbuf++ = n;
+ } while (n > 1);
+}
+
+/*
+ *
+ * User-callable function to allocate all necessary storage space for the fft.
+ *
+ * The return value is a contiguous block of memory, allocated with malloc. As such,
+ * It can be freed with free(), rather than a kiss_fft-specific function.
+ * */
+kiss_fft_cfg kiss_fft_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem )
+{
+ kiss_fft_cfg st=NULL;
+ size_t memneeded = sizeof(struct kiss_fft_state)
+ + sizeof(kiss_fft_cpx)*(nfft-1); /* twiddle factors*/
+
+ if ( lenmem==NULL ) {
+ st = ( kiss_fft_cfg)KISS_FFT_MALLOC( memneeded );
+ }else{
+ if (mem != NULL && *lenmem >= memneeded)
+ st = (kiss_fft_cfg)mem;
+ *lenmem = memneeded;
+ }
+ if (st) {
+ int i;
+ st->nfft=nfft;
+ st->inverse = inverse_fft;
+
+ for (i=0;i<nfft;++i) {
+ const double pi=3.141592653589793238462643383279502884197169399375105820974944;
+ double phase = -2*pi*i / nfft;
+ if (st->inverse)
+ phase *= -1;
+ kf_cexp(st->twiddles+i, phase );
+ }
+
+ kf_factor(nfft,st->factors);
+ }
+ return st;
+}
+
+
+void kiss_fft_stride(kiss_fft_cfg st,const kiss_fft_cpx *fin,kiss_fft_cpx *fout,int in_stride)
+{
+ if (fin == fout) {
+ //NOTE: this is not really an in-place FFT algorithm.
+ //It just performs an out-of-place FFT into a temp buffer
+ kiss_fft_cpx * tmpbuf = (kiss_fft_cpx*)KISS_FFT_TMP_ALLOC( sizeof(kiss_fft_cpx)*st->nfft);
+ kf_work(tmpbuf,fin,1,in_stride, st->factors,st);
+ memcpy(fout,tmpbuf,sizeof(kiss_fft_cpx)*st->nfft);
+ KISS_FFT_TMP_FREE(tmpbuf);
+ }else{
+ kf_work( fout, fin, 1,in_stride, st->factors,st );
+ }
+}
+
+void kiss_fft(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout)
+{
+ kiss_fft_stride(cfg,fin,fout,1);
+}
+
+
+void kiss_fft_cleanup(void)
+{
+ // nothing needed any more
+}
+
+int kiss_fft_next_fast_size(int n)
+{
+ while(1) {
+ int m=n;
+ while ( (m%2) == 0 ) m/=2;
+ while ( (m%3) == 0 ) m/=3;
+ while ( (m%5) == 0 ) m/=5;
+ if (m<=1)
+ break; /* n is completely factorable by twos, threes, and fives */
+ n++;
+ }
+ return n;
+}
diff --git a/xbmc/contrib/kissfft/kiss_fft.h b/xbmc/contrib/kissfft/kiss_fft.h
new file mode 100644
index 0000000..9db7f92
--- /dev/null
+++ b/xbmc/contrib/kissfft/kiss_fft.h
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2003-2010, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#ifndef KISS_FFT_H
+#define KISS_FFT_H
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ ATTENTION!
+ If you would like a :
+ -- a utility that will handle the caching of fft objects
+ -- real-only (no imaginary time component ) FFT
+ -- a multi-dimensional FFT
+ -- a command-line utility to perform ffts
+ -- a command-line utility to perform fast-convolution filtering
+
+ Then see kfc.h kiss_fftr.h kiss_fftnd.h fftutil.c kiss_fastfir.c
+ in the tools/ directory.
+*/
+
+#ifdef USE_SIMD
+# include <xmmintrin.h>
+# define kiss_fft_scalar __m128
+#define KISS_FFT_MALLOC(nbytes) _mm_malloc(nbytes,16)
+#define KISS_FFT_FREE _mm_free
+#else
+#define KISS_FFT_MALLOC malloc
+#define KISS_FFT_FREE free
+#endif
+
+
+#ifdef FIXED_POINT
+#include <sys/types.h>
+# if (FIXED_POINT == 32)
+# define kiss_fft_scalar int32_t
+# else
+# define kiss_fft_scalar int16_t
+# endif
+#else
+# ifndef kiss_fft_scalar
+/* default is float */
+# define kiss_fft_scalar float
+# endif
+#endif
+
+typedef struct {
+ kiss_fft_scalar r;
+ kiss_fft_scalar i;
+}kiss_fft_cpx;
+
+typedef struct kiss_fft_state* kiss_fft_cfg;
+
+/*
+ * kiss_fft_alloc
+ *
+ * Initialize a FFT (or IFFT) algorithm's cfg/state buffer.
+ *
+ * typical usage: kiss_fft_cfg mycfg=kiss_fft_alloc(1024,0,NULL,NULL);
+ *
+ * The return value from fft_alloc is a cfg buffer used internally
+ * by the fft routine or NULL.
+ *
+ * If lenmem is NULL, then kiss_fft_alloc will allocate a cfg buffer using malloc.
+ * The returned value should be free()d when done to avoid memory leaks.
+ *
+ * The state can be placed in a user supplied buffer 'mem':
+ * If lenmem is not NULL and mem is not NULL and *lenmem is large enough,
+ * then the function places the cfg in mem and the size used in *lenmem
+ * and returns mem.
+ *
+ * If lenmem is not NULL and ( mem is NULL or *lenmem is not large enough),
+ * then the function returns NULL and places the minimum cfg
+ * buffer size in *lenmem.
+ * */
+
+kiss_fft_cfg kiss_fft_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem);
+
+/*
+ * kiss_fft(cfg,in_out_buf)
+ *
+ * Perform an FFT on a complex input buffer.
+ * for a forward FFT,
+ * fin should be f[0] , f[1] , ... ,f[nfft-1]
+ * fout will be F[0] , F[1] , ... ,F[nfft-1]
+ * Note that each element is complex and can be accessed like
+ f[k].r and f[k].i
+ * */
+void kiss_fft(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout);
+
+/*
+ A more generic version of the above function. It reads its input from every Nth sample.
+ * */
+void kiss_fft_stride(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout,int fin_stride);
+
+/* If kiss_fft_alloc allocated a buffer, it is one contiguous
+ buffer and can be simply free()d when no longer needed*/
+#define kiss_fft_free free
+
+/*
+ Cleans up some memory that gets managed internally. Not necessary to call, but it might clean up
+ your compiler output to call this before you exit.
+*/
+void kiss_fft_cleanup(void);
+
+
+/*
+ * Returns the smallest integer k, such that k>=n and k has only "fast" factors (2,3,5)
+ */
+int kiss_fft_next_fast_size(int n);
+
+/* for real ffts, we need an even size */
+#define kiss_fftr_next_fast_size_real(n) \
+ (kiss_fft_next_fast_size( ((n)+1)>>1)<<1)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/xbmc/contrib/kissfft/kiss_fftr.c b/xbmc/contrib/kissfft/kiss_fftr.c
new file mode 100644
index 0000000..e9d3fe9
--- /dev/null
+++ b/xbmc/contrib/kissfft/kiss_fftr.c
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#include "kiss_fftr.h"
+
+#include "_kiss_fft_guts.h"
+
+struct kiss_fftr_state{
+ kiss_fft_cfg substate;
+ kiss_fft_cpx * tmpbuf;
+ kiss_fft_cpx * super_twiddles;
+#ifdef USE_SIMD
+ void * pad;
+#endif
+};
+
+kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem)
+{
+ int i;
+ kiss_fftr_cfg st = NULL;
+ size_t subsize, memneeded;
+
+ if (nfft & 1) {
+ fprintf(stderr,"Real FFT optimization must be even.\n");
+ return NULL;
+ }
+ nfft >>= 1;
+
+ kiss_fft_alloc (nfft, inverse_fft, NULL, &subsize);
+ memneeded = sizeof(struct kiss_fftr_state) + subsize + sizeof(kiss_fft_cpx) * ( nfft * 3 / 2);
+
+ if (lenmem == NULL) {
+ st = (kiss_fftr_cfg) KISS_FFT_MALLOC (memneeded);
+ } else {
+ if (*lenmem >= memneeded)
+ st = (kiss_fftr_cfg) mem;
+ *lenmem = memneeded;
+ }
+ if (!st)
+ return NULL;
+
+ st->substate = (kiss_fft_cfg) (st + 1); /*just beyond kiss_fftr_state struct */
+ st->tmpbuf = (kiss_fft_cpx *) (((char *) st->substate) + subsize);
+ st->super_twiddles = st->tmpbuf + nfft;
+ kiss_fft_alloc(nfft, inverse_fft, st->substate, &subsize);
+
+ for (i = 0; i < nfft/2; ++i) {
+ double phase =
+ -3.14159265358979323846264338327 * ((double) (i+1) / nfft + .5);
+ if (inverse_fft)
+ phase *= -1;
+ kf_cexp (st->super_twiddles+i,phase);
+ }
+ return st;
+}
+
+void kiss_fftr(kiss_fftr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata)
+{
+ /* input buffer timedata is stored row-wise */
+ int k,ncfft;
+ kiss_fft_cpx fpnk,fpk,f1k,f2k,tw,tdc;
+
+ if ( st->substate->inverse) {
+ fprintf(stderr,"kiss fft usage error: improper alloc\n");
+ exit(1);
+ }
+
+ ncfft = st->substate->nfft;
+
+ /*perform the parallel fft of two real signals packed in real,imag*/
+ kiss_fft( st->substate , (const kiss_fft_cpx*)timedata, st->tmpbuf );
+ /* The real part of the DC element of the frequency spectrum in st->tmpbuf
+ * contains the sum of the even-numbered elements of the input time sequence
+ * The imag part is the sum of the odd-numbered elements
+ *
+ * The sum of tdc.r and tdc.i is the sum of the input time sequence.
+ * yielding DC of input time sequence
+ * The difference of tdc.r - tdc.i is the sum of the input (dot product) [1,-1,1,-1...
+ * yielding Nyquist bin of input time sequence
+ */
+
+ tdc.r = st->tmpbuf[0].r;
+ tdc.i = st->tmpbuf[0].i;
+ C_FIXDIV(tdc,2);
+ CHECK_OVERFLOW_OP(tdc.r ,+, tdc.i);
+ CHECK_OVERFLOW_OP(tdc.r ,-, tdc.i);
+ freqdata[0].r = tdc.r + tdc.i;
+ freqdata[ncfft].r = tdc.r - tdc.i;
+#ifdef USE_SIMD
+ freqdata[ncfft].i = freqdata[0].i = _mm_set1_ps(0);
+#else
+ freqdata[ncfft].i = freqdata[0].i = 0;
+#endif
+
+ for ( k=1;k <= ncfft/2 ; ++k ) {
+ fpk = st->tmpbuf[k];
+ fpnk.r = st->tmpbuf[ncfft-k].r;
+ fpnk.i = - st->tmpbuf[ncfft-k].i;
+ C_FIXDIV(fpk,2);
+ C_FIXDIV(fpnk,2);
+
+ C_ADD( f1k, fpk , fpnk );
+ C_SUB( f2k, fpk , fpnk );
+ C_MUL( tw , f2k , st->super_twiddles[k-1]);
+
+ freqdata[k].r = HALF_OF(f1k.r + tw.r);
+ freqdata[k].i = HALF_OF(f1k.i + tw.i);
+ freqdata[ncfft-k].r = HALF_OF(f1k.r - tw.r);
+ freqdata[ncfft-k].i = HALF_OF(tw.i - f1k.i);
+ }
+}
+
+void kiss_fftri(kiss_fftr_cfg st,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata)
+{
+ /* input buffer timedata is stored row-wise */
+ int k, ncfft;
+
+ if (st->substate->inverse == 0) {
+ fprintf (stderr, "kiss fft usage error: improper alloc\n");
+ exit (1);
+ }
+
+ ncfft = st->substate->nfft;
+
+ st->tmpbuf[0].r = freqdata[0].r + freqdata[ncfft].r;
+ st->tmpbuf[0].i = freqdata[0].r - freqdata[ncfft].r;
+ C_FIXDIV(st->tmpbuf[0],2);
+
+ for (k = 1; k <= ncfft / 2; ++k) {
+ kiss_fft_cpx fk, fnkc, fek, fok, tmp;
+ fk = freqdata[k];
+ fnkc.r = freqdata[ncfft - k].r;
+ fnkc.i = -freqdata[ncfft - k].i;
+ C_FIXDIV( fk , 2 );
+ C_FIXDIV( fnkc , 2 );
+
+ C_ADD (fek, fk, fnkc);
+ C_SUB (tmp, fk, fnkc);
+ C_MUL (fok, tmp, st->super_twiddles[k-1]);
+ C_ADD (st->tmpbuf[k], fek, fok);
+ C_SUB (st->tmpbuf[ncfft - k], fek, fok);
+#ifdef USE_SIMD
+ st->tmpbuf[ncfft - k].i *= _mm_set1_ps(-1.0);
+#else
+ st->tmpbuf[ncfft - k].i *= -1;
+#endif
+ }
+ kiss_fft (st->substate, st->tmpbuf, (kiss_fft_cpx *) timedata);
+}
diff --git a/xbmc/contrib/kissfft/kiss_fftr.h b/xbmc/contrib/kissfft/kiss_fftr.h
new file mode 100644
index 0000000..f7586be
--- /dev/null
+++ b/xbmc/contrib/kissfft/kiss_fftr.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2003-2004, Mark Borgerding. All rights reserved.
+ * This file is part of KISS FFT - https://github.com/mborgerding/kissfft
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See COPYING file for more information.
+ */
+
+#ifndef KISS_FTR_H
+#define KISS_FTR_H
+
+#include "kiss_fft.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+/*
+
+ Real optimized version can save about 45% cpu time vs. complex fft of a real seq.
+
+
+
+ */
+
+typedef struct kiss_fftr_state *kiss_fftr_cfg;
+
+
+kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem, size_t * lenmem);
+/*
+ nfft must be even
+
+ If you don't care to allocate space, use mem = lenmem = NULL
+*/
+
+
+void kiss_fftr(kiss_fftr_cfg cfg,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata);
+/*
+ input timedata has nfft scalar points
+ output freqdata has nfft/2+1 complex points
+*/
+
+void kiss_fftri(kiss_fftr_cfg cfg,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata);
+/*
+ input freqdata has nfft/2+1 complex points
+ output timedata has nfft scalar points
+*/
+
+#define kiss_fftr_free free
+
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/xbmc/cores/AudioEngine/AEResampleFactory.cpp b/xbmc/cores/AudioEngine/AEResampleFactory.cpp
new file mode 100644
index 0000000..224910f
--- /dev/null
+++ b/xbmc/cores/AudioEngine/AEResampleFactory.cpp
@@ -0,0 +1,20 @@
+/*
+ * 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 "AEResampleFactory.h"
+#include "cores/AudioEngine/Engines/ActiveAE/ActiveAEResampleFFMPEG.h"
+
+namespace ActiveAE
+{
+
+IAEResample *CAEResampleFactory::Create(uint32_t flags /* = 0 */)
+{
+ return new CActiveAEResampleFFMPEG();
+}
+
+}
diff --git a/xbmc/cores/AudioEngine/AEResampleFactory.h b/xbmc/cores/AudioEngine/AEResampleFactory.h
new file mode 100644
index 0000000..e2a7b19
--- /dev/null
+++ b/xbmc/cores/AudioEngine/AEResampleFactory.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "cores/AudioEngine/Interfaces/AEResample.h"
+
+class IAEResample;
+
+namespace ActiveAE
+{
+
+/**
+ * Bit options to pass to CAEResampleFactory::Create
+ */
+enum AEResampleFactoryOptions
+{
+ /* This is a quick resample job (e.g. resample a single noise packet) and may not be worth using GPU acceleration */
+ AERESAMPLEFACTORY_QUICK_RESAMPLE = 0x01
+};
+
+class CAEResampleFactory
+{
+public:
+ static IAEResample *Create(uint32_t flags = 0U);
+};
+
+}
diff --git a/xbmc/cores/AudioEngine/AESinkFactory.cpp b/xbmc/cores/AudioEngine/AESinkFactory.cpp
new file mode 100644
index 0000000..d86a83e
--- /dev/null
+++ b/xbmc/cores/AudioEngine/AESinkFactory.cpp
@@ -0,0 +1,114 @@
+/*
+ * 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 "AESinkFactory.h"
+
+#include "Interfaces/AESink.h"
+#include "ServiceBroker.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace AE;
+
+std::map<std::string, AESinkRegEntry> CAESinkFactory::m_AESinkRegEntry;
+
+void CAESinkFactory::RegisterSink(const AESinkRegEntry& regEntry)
+{
+ m_AESinkRegEntry[regEntry.sinkName] = regEntry;
+
+ IAE *ae = CServiceBroker::GetActiveAE();
+ if (ae)
+ ae->DeviceChange();
+}
+
+void CAESinkFactory::ClearSinks()
+{
+ m_AESinkRegEntry.clear();
+}
+
+bool CAESinkFactory::HasSinks()
+{
+ return !m_AESinkRegEntry.empty();
+}
+
+void CAESinkFactory::ParseDevice(std::string &device, std::string &driver)
+{
+ int pos = device.find_first_of(':');
+ bool found = false;
+ if (pos > 0)
+ {
+ driver = device.substr(0, pos);
+
+ for (const auto& reg : m_AESinkRegEntry)
+ {
+ if (!StringUtils::EqualsNoCase(driver, reg.second.sinkName))
+ continue;
+
+ device = device.substr(pos + 1, device.length() - pos - 1);
+ found = true;
+ }
+ }
+
+ if (!found)
+ driver.clear();
+}
+
+IAESink *CAESinkFactory::Create(std::string &device, AEAudioFormat &desiredFormat)
+{
+ // extract the driver from the device string if it exists
+ std::string driver;
+ ParseDevice(device, driver);
+
+ AEAudioFormat tmpFormat = desiredFormat;
+ IAESink *sink;
+ std::string tmpDevice = device;
+
+ for (const auto& reg : m_AESinkRegEntry)
+ {
+ if (driver != reg.second.sinkName)
+ continue;
+
+ sink = reg.second.createFunc(tmpDevice, tmpFormat);
+ if (sink)
+ {
+ desiredFormat = tmpFormat;
+ return sink;
+ }
+ }
+ return nullptr;
+}
+
+void CAESinkFactory::EnumerateEx(std::vector<AESinkInfo>& list,
+ bool force,
+ const std::string& driver)
+{
+ AESinkInfo info;
+
+ for (const auto& reg : m_AESinkRegEntry)
+ {
+ if (!driver.empty() && driver != reg.second.sinkName)
+ continue;
+
+ info.m_deviceInfoList.clear();
+ info.m_sinkName = reg.second.sinkName;
+ reg.second.enumerateFunc(info.m_deviceInfoList, force);
+ if (!info.m_deviceInfoList.empty())
+ list.push_back(info);
+ }
+}
+
+void CAESinkFactory::Cleanup()
+{
+ for (const auto& reg : m_AESinkRegEntry)
+ {
+ if (reg.second.cleanupFunc)
+ reg.second.cleanupFunc();
+ }
+}
diff --git a/xbmc/cores/AudioEngine/AESinkFactory.h b/xbmc/cores/AudioEngine/AESinkFactory.h
new file mode 100644
index 0000000..e8e069f
--- /dev/null
+++ b/xbmc/cores/AudioEngine/AESinkFactory.h
@@ -0,0 +1,58 @@
+/*
+ * 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 "Utils/AEAudioFormat.h"
+#include "Utils/AEDeviceInfo.h"
+
+#include <map>
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+class IAESink;
+
+namespace AE
+{
+
+struct AESinkInfo
+{
+ std::string m_sinkName;
+ AEDeviceInfoList m_deviceInfoList;
+};
+
+typedef IAESink* (*CreateSink)(std::string &device, AEAudioFormat &desiredFormat);
+typedef void (*Enumerate)(AEDeviceInfoList &list, bool force);
+typedef void (*Cleanup)();
+
+struct AESinkRegEntry
+{
+ std::string sinkName;
+ CreateSink createFunc = nullptr;
+ Enumerate enumerateFunc = nullptr;
+ Cleanup cleanupFunc = nullptr;
+};
+
+class CAESinkFactory
+{
+public:
+ static void RegisterSink(const AESinkRegEntry& regEntry);
+ static void ClearSinks();
+ static bool HasSinks();
+
+ static void ParseDevice(std::string &device, std::string &driver);
+ static IAESink *Create(std::string &device, AEAudioFormat &desiredFormat);
+ static void EnumerateEx(std::vector<AESinkInfo>& list, bool force, const std::string& driver);
+ static void Cleanup();
+
+protected:
+ static std::map<std::string, AESinkRegEntry> m_AESinkRegEntry;
+};
+
+}
diff --git a/xbmc/cores/AudioEngine/CMakeLists.txt b/xbmc/cores/AudioEngine/CMakeLists.txt
new file mode 100644
index 0000000..c5d4d97
--- /dev/null
+++ b/xbmc/cores/AudioEngine/CMakeLists.txt
@@ -0,0 +1,166 @@
+set(SOURCES AEResampleFactory.cpp
+ AESinkFactory.cpp
+ Encoders/AEEncoderFFmpeg.cpp
+ Engines/ActiveAE/ActiveAE.cpp
+ Engines/ActiveAE/ActiveAEBuffer.cpp
+ Engines/ActiveAE/ActiveAEFilter.cpp
+ Engines/ActiveAE/ActiveAESink.cpp
+ Engines/ActiveAE/ActiveAEStream.cpp
+ Engines/ActiveAE/ActiveAESound.cpp
+ Engines/ActiveAE/ActiveAESettings.cpp
+ Utils/AEBitstreamPacker.cpp
+ Utils/AEChannelInfo.cpp
+ Utils/AEDeviceInfo.cpp
+ Utils/AELimiter.cpp
+ Utils/AEPackIEC61937.cpp
+ Utils/AEStreamInfo.cpp
+ Utils/AEUtil.cpp)
+
+set(HEADERS AEResampleFactory.h
+ AESinkFactory.h
+ Encoders/AEEncoderFFmpeg.h
+ Engines/ActiveAE/ActiveAE.h
+ Engines/ActiveAE/ActiveAEBuffer.h
+ Engines/ActiveAE/ActiveAEFilter.h
+ Engines/ActiveAE/ActiveAESink.h
+ Engines/ActiveAE/ActiveAESound.h
+ Engines/ActiveAE/ActiveAEStream.h
+ Engines/ActiveAE/ActiveAESettings.h
+ Interfaces/AE.h
+ Interfaces/AEEncoder.h
+ Interfaces/AEResample.h
+ Interfaces/AESink.h
+ Interfaces/AESound.h
+ Interfaces/AEStream.h
+ Interfaces/IAudioCallback.h
+ Interfaces/ThreadedAE.h
+ Utils/AEAudioFormat.h
+ Utils/AEBitstreamPacker.h
+ Utils/AEChannelData.h
+ Utils/AEChannelInfo.h
+ Utils/AEDeviceInfo.h
+ Utils/AELimiter.h
+ Utils/AEPackIEC61937.h
+ Utils/AERingBuffer.h
+ Utils/AEStreamData.h
+ Utils/AEStreamInfo.h
+ Utils/AEUtil.h)
+
+if(ALSA_FOUND)
+ list(APPEND SOURCES Sinks/AESinkALSA.cpp
+ Utils/AEELDParser.cpp)
+ list(APPEND HEADERS Sinks/AESinkALSA.h
+ Utils/AEELDParser.h)
+
+ if(NOT "x11" IN_LIST CORE_PLATFORM_NAME_LC)
+ list(APPEND SOURCES Sinks/alsa/ALSAHControlMonitor.cpp)
+ list(APPEND HEADERS Sinks/alsa/ALSAHControlMonitor.h)
+ endif()
+
+ if(UDEV_FOUND)
+ list(APPEND SOURCES Sinks/alsa/ALSADeviceMonitor.cpp)
+ list(APPEND HEADERS Sinks/alsa/ALSADeviceMonitor.h)
+ endif()
+endif()
+
+if(PULSEAUDIO_FOUND)
+ list(APPEND SOURCES Sinks/AESinkPULSE.cpp)
+ list(APPEND HEADERS Sinks/AESinkPULSE.h)
+endif()
+
+if(PIPEWIRE_FOUND)
+ list(APPEND SOURCES Sinks/pipewire/AESinkPipewire.cpp
+ Sinks/pipewire/Pipewire.cpp
+ Sinks/pipewire/PipewireContext.cpp
+ Sinks/pipewire/PipewireCore.cpp
+ Sinks/pipewire/PipewireNode.cpp
+ Sinks/pipewire/PipewireProxy.cpp
+ Sinks/pipewire/PipewireRegistry.cpp
+ Sinks/pipewire/PipewireStream.cpp
+ Sinks/pipewire/PipewireThreadLoop.cpp)
+ list(APPEND HEADERS Sinks/pipewire/AESinkPipewire.h
+ Sinks/pipewire/Pipewire.h
+ Sinks/pipewire/PipewireContext.h
+ Sinks/pipewire/PipewireCore.h
+ Sinks/pipewire/PipewireNode.h
+ Sinks/pipewire/PipewireProxy.h
+ Sinks/pipewire/PipewireRegistry.h
+ Sinks/pipewire/PipewireStream.h
+ Sinks/pipewire/PipewireThreadLoop.h)
+endif()
+
+if(SNDIO_FOUND)
+ list(APPEND SOURCES Sinks/AESinkSNDIO.cpp)
+ list(APPEND HEADERS Sinks/AESinkSNDIO.h)
+endif()
+
+if(FFMPEG_FOUND)
+ list(APPEND SOURCES Engines/ActiveAE/ActiveAEResampleFFMPEG.cpp)
+ list(APPEND HEADERS Engines/ActiveAE/ActiveAEResampleFFMPEG.h)
+endif()
+
+if(CORE_SYSTEM_NAME MATCHES windows)
+ list(APPEND SOURCES Sinks/AESinkWASAPI.cpp
+ Sinks/windows/AESinkFactoryWin.cpp)
+ list(APPEND HEADERS Sinks/AESinkWASAPI.h
+ Sinks/windows/AESinkFactoryWin.h)
+ if(CORE_SYSTEM_NAME STREQUAL windowsstore)
+ list(APPEND SOURCES Sinks/AESinkXAudio.cpp
+ Sinks/windows/AESinkFactoryWin10.cpp)
+ list(APPEND SOURCES Sinks/AESinkXAudio.h)
+ elseif(CORE_SYSTEM_NAME STREQUAL windows)
+ list(APPEND SOURCES Sinks/AESinkDirectSound.cpp
+ Sinks/windows/AESinkFactoryWin32.cpp)
+ list(APPEND SOURCES Sinks/AESinkDirectSound.h)
+ endif()
+endif()
+
+if(CORE_SYSTEM_NAME STREQUAL osx)
+ list(APPEND SOURCES Sinks/AESinkDARWINOSX.cpp
+ Sinks/darwin/CoreAudioHelpers.cpp
+ Sinks/osx/AEDeviceEnumerationOSX.cpp
+ Sinks/osx/CoreAudioChannelLayout.cpp
+ Sinks/osx/CoreAudioDevice.cpp
+ Sinks/osx/CoreAudioHardware.cpp
+ Sinks/osx/CoreAudioStream.cpp)
+ list(APPEND HEADERS Sinks/AESinkDARWINOSX.h
+ Sinks/darwin/CoreAudioHelpers.h
+ Sinks/osx/AEDeviceEnumerationOSX.h
+ Sinks/osx/CoreAudioChannelLayout.h
+ Sinks/osx/CoreAudioDevice.h
+ Sinks/osx/CoreAudioHardware.h
+ Sinks/osx/CoreAudioStream.h)
+endif()
+
+if(CORE_SYSTEM_NAME STREQUAL darwin_embedded)
+ list(APPEND SOURCES Sinks/darwin/CoreAudioHelpers.cpp)
+ list(APPEND HEADERS Sinks/darwin/CoreAudioHelpers.h)
+ if("ios" IN_LIST CORE_PLATFORM_NAME_LC)
+ list(APPEND SOURCES Sinks/AESinkDARWINIOS.mm)
+ list(APPEND HEADERS Sinks/AESinkDARWINIOS.h)
+ elseif("tvos" IN_LIST CORE_PLATFORM_NAME_LC)
+ list(APPEND SOURCES Sinks/AESinkDARWINTVOS.mm)
+ list(APPEND HEADERS Sinks/AESinkDARWINTVOS.h)
+ endif()
+endif()
+
+if(CORE_SYSTEM_NAME STREQUAL android)
+ list(APPEND SOURCES Sinks/AESinkAUDIOTRACK.cpp)
+ list(APPEND HEADERS Sinks/AESinkAUDIOTRACK.h)
+endif()
+
+if(CORE_SYSTEM_NAME STREQUAL freebsd)
+ list(APPEND SOURCES Sinks/AESinkOSS.cpp)
+ list(APPEND HEADERS Sinks/AESinkOSS.h)
+endif()
+
+core_add_library(audioengine)
+target_include_directories(${CORE_LIBRARY} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+if(NOT CORE_SYSTEM_NAME STREQUAL windows AND NOT CORE_SYSTEM_NAME STREQUAL windowsstore)
+ if(HAVE_SSE)
+ target_compile_options(${CORE_LIBRARY} PRIVATE -msse)
+ endif()
+ if(HAVE_SSE2)
+ target_compile_options(${CORE_LIBRARY} PRIVATE -msse2)
+ endif()
+endif()
diff --git a/xbmc/cores/AudioEngine/Encoders/AEEncoderFFmpeg.cpp b/xbmc/cores/AudioEngine/Encoders/AEEncoderFFmpeg.cpp
new file mode 100644
index 0000000..09bb26a
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Encoders/AEEncoderFFmpeg.cpp
@@ -0,0 +1,322 @@
+/*
+ * 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.
+ */
+
+#define AC3_ENCODE_BITRATE 640000
+#define DTS_ENCODE_BITRATE 1411200
+
+#include "cores/AudioEngine/Encoders/AEEncoderFFmpeg.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "ServiceBroker.h"
+#include "utils/log.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include <string.h>
+#include <cassert>
+
+CAEEncoderFFmpeg::CAEEncoderFFmpeg() : m_CodecCtx(NULL), m_SwrCtx(NULL)
+{
+}
+
+CAEEncoderFFmpeg::~CAEEncoderFFmpeg()
+{
+ Reset();
+ swr_free(&m_SwrCtx);
+ avcodec_free_context(&m_CodecCtx);
+}
+
+bool CAEEncoderFFmpeg::IsCompatible(const AEAudioFormat& format)
+{
+ if (!m_CodecCtx)
+ return false;
+
+ bool match = (
+ format.m_dataFormat == m_CurrentFormat.m_dataFormat &&
+ format.m_sampleRate == m_CurrentFormat.m_sampleRate
+ );
+
+ if (match)
+ {
+ CAEChannelInfo layout;
+ BuildChannelLayout(AV_CH_LAYOUT_5POINT1_BACK, layout); /* hard coded for AC3 & DTS currently */
+ match = (m_CurrentFormat.m_channelLayout == layout);
+ }
+
+ return match;
+}
+
+unsigned int CAEEncoderFFmpeg::BuildChannelLayout(const int64_t ffmap, CAEChannelInfo& layout)
+{
+ /* build the channel layout and count the channels */
+ layout.Reset();
+ if (ffmap & AV_CH_FRONT_LEFT ) layout += AE_CH_FL ;
+ if (ffmap & AV_CH_FRONT_RIGHT ) layout += AE_CH_FR ;
+ if (ffmap & AV_CH_FRONT_CENTER ) layout += AE_CH_FC ;
+ if (ffmap & AV_CH_LOW_FREQUENCY ) layout += AE_CH_LFE ;
+ if (ffmap & AV_CH_BACK_LEFT ) layout += AE_CH_BL ;
+ if (ffmap & AV_CH_BACK_RIGHT ) layout += AE_CH_BR ;
+ if (ffmap & AV_CH_FRONT_LEFT_OF_CENTER ) layout += AE_CH_FLOC;
+ if (ffmap & AV_CH_FRONT_RIGHT_OF_CENTER) layout += AE_CH_FROC;
+ if (ffmap & AV_CH_BACK_CENTER ) layout += AE_CH_BC ;
+ if (ffmap & AV_CH_SIDE_LEFT ) layout += AE_CH_SL ;
+ if (ffmap & AV_CH_SIDE_RIGHT ) layout += AE_CH_SR ;
+ if (ffmap & AV_CH_TOP_CENTER ) layout += AE_CH_TC ;
+ if (ffmap & AV_CH_TOP_FRONT_LEFT ) layout += AE_CH_TFL ;
+ if (ffmap & AV_CH_TOP_FRONT_CENTER ) layout += AE_CH_TFC ;
+ if (ffmap & AV_CH_TOP_FRONT_RIGHT ) layout += AE_CH_TFR ;
+ if (ffmap & AV_CH_TOP_BACK_LEFT ) layout += AE_CH_TBL ;
+ if (ffmap & AV_CH_TOP_BACK_CENTER ) layout += AE_CH_TBC ;
+ if (ffmap & AV_CH_TOP_BACK_RIGHT ) layout += AE_CH_TBR ;
+
+ return layout.Count();
+}
+
+bool CAEEncoderFFmpeg::Initialize(AEAudioFormat &format, bool allow_planar_input)
+{
+ Reset();
+
+ bool ac3 = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_AUDIOOUTPUT_AC3PASSTHROUGH);
+
+ AVCodec *codec = NULL;
+
+ /* fallback to ac3 if we support it, we might not have DTS support */
+ if (ac3)
+ {
+ m_CodecName = "AC3";
+ m_CodecID = AV_CODEC_ID_AC3;
+ m_BitRate = AC3_ENCODE_BITRATE;
+ codec = avcodec_find_encoder(m_CodecID);
+ }
+
+ /* check we got the codec */
+ if (!codec)
+ return false;
+
+ m_CodecCtx = avcodec_alloc_context3(codec);
+ if (!m_CodecCtx)
+ return false;
+
+ m_CodecCtx->bit_rate = m_BitRate;
+ m_CodecCtx->sample_rate = format.m_sampleRate;
+ m_CodecCtx->channel_layout = AV_CH_LAYOUT_5POINT1_BACK;
+
+ /* select a suitable data format */
+ if (codec->sample_fmts)
+ {
+ bool hasFloat = false;
+ bool hasDouble = false;
+ bool hasS32 = false;
+ bool hasS16 = false;
+ bool hasU8 = false;
+ bool hasFloatP = false;
+ bool hasUnknownFormat = false;
+
+ for(int i = 0; codec->sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i)
+ {
+ switch (codec->sample_fmts[i])
+ {
+ case AV_SAMPLE_FMT_FLT: hasFloat = true; break;
+ case AV_SAMPLE_FMT_DBL: hasDouble = true; break;
+ case AV_SAMPLE_FMT_S32: hasS32 = true; break;
+ case AV_SAMPLE_FMT_S16: hasS16 = true; break;
+ case AV_SAMPLE_FMT_U8 : hasU8 = true; break;
+ case AV_SAMPLE_FMT_FLTP:
+ if (allow_planar_input)
+ hasFloatP = true;
+ else
+ hasUnknownFormat = true;
+ break;
+ case AV_SAMPLE_FMT_NONE: return false;
+ default: hasUnknownFormat = true; break;
+ }
+ }
+
+ if (hasFloat)
+ {
+ m_CodecCtx->sample_fmt = AV_SAMPLE_FMT_FLT;
+ format.m_dataFormat = AE_FMT_FLOAT;
+ }
+ else if (hasFloatP)
+ {
+ m_CodecCtx->sample_fmt = AV_SAMPLE_FMT_FLTP;
+ format.m_dataFormat = AE_FMT_FLOATP;
+ }
+ else if (hasDouble)
+ {
+ m_CodecCtx->sample_fmt = AV_SAMPLE_FMT_DBL;
+ format.m_dataFormat = AE_FMT_DOUBLE;
+ }
+ else if (hasS32)
+ {
+ m_CodecCtx->sample_fmt = AV_SAMPLE_FMT_S32;
+ format.m_dataFormat = AE_FMT_S32NE;
+ }
+ else if (hasS16)
+ {
+ m_CodecCtx->sample_fmt = AV_SAMPLE_FMT_S16;
+ format.m_dataFormat = AE_FMT_S16NE;
+ }
+ else if (hasU8)
+ {
+ m_CodecCtx->sample_fmt = AV_SAMPLE_FMT_U8;
+ format.m_dataFormat = AE_FMT_U8;
+ }
+ else if (hasUnknownFormat)
+ {
+ m_CodecCtx->sample_fmt = codec->sample_fmts[0];
+ format.m_dataFormat = AE_FMT_FLOAT;
+ m_NeedConversion = true;
+ CLog::Log(LOGINFO,
+ "CAEEncoderFFmpeg::Initialize - Unknown audio format, it will be resampled.");
+ }
+ else
+ {
+ CLog::Log(
+ LOGERROR,
+ "CAEEncoderFFmpeg::Initialize - Unable to find a suitable data format for the codec ({})",
+ m_CodecName);
+ avcodec_free_context(&m_CodecCtx);
+ return false;
+ }
+ }
+
+ m_CodecCtx->channels = BuildChannelLayout(m_CodecCtx->channel_layout, m_Layout);
+
+ /* open the codec */
+ if (avcodec_open2(m_CodecCtx, codec, NULL))
+ {
+ avcodec_free_context(&m_CodecCtx);
+ return false;
+ }
+
+ format.m_frames = m_CodecCtx->frame_size;
+ format.m_frameSize = m_CodecCtx->channels * (CAEUtil::DataFormatToBits(format.m_dataFormat) >> 3);
+ format.m_channelLayout = m_Layout;
+
+ m_CurrentFormat = format;
+ m_NeededFrames = format.m_frames;
+ m_OutputRatio = (double)m_NeededFrames / m_OutputSize;
+ m_SampleRateMul = 1.0 / (double)m_CodecCtx->sample_rate;
+
+ if (m_NeedConversion)
+ {
+ m_SwrCtx = swr_alloc_set_opts(NULL,
+ m_CodecCtx->channel_layout, m_CodecCtx->sample_fmt, m_CodecCtx->sample_rate,
+ m_CodecCtx->channel_layout, AV_SAMPLE_FMT_FLT, m_CodecCtx->sample_rate,
+ 0, NULL);
+ if (!m_SwrCtx || swr_init(m_SwrCtx) < 0)
+ {
+ CLog::Log(LOGERROR, "CAEEncoderFFmpeg::Initialize - Failed to initialise resampler.");
+ swr_free(&m_SwrCtx);
+ avcodec_free_context(&m_CodecCtx);
+ return false;
+ }
+ }
+ CLog::Log(LOGINFO, "CAEEncoderFFmpeg::Initialize - {} encoder ready", m_CodecName);
+ return true;
+}
+
+void CAEEncoderFFmpeg::Reset()
+{
+ m_BufferSize = 0;
+}
+
+unsigned int CAEEncoderFFmpeg::GetBitRate()
+{
+ return m_BitRate;
+}
+
+AVCodecID CAEEncoderFFmpeg::GetCodecID()
+{
+ return m_CodecID;
+}
+
+unsigned int CAEEncoderFFmpeg::GetFrames()
+{
+ return m_NeededFrames;
+}
+
+int CAEEncoderFFmpeg::Encode(uint8_t *in, int in_size, uint8_t *out, int out_size)
+{
+ int got_output;
+ AVFrame *frame;
+
+ if (!m_CodecCtx)
+ return 0;
+
+ /* allocate the input frame
+ * sadly, we have to alloc/dealloc it everytime since we have no guarantee the
+ * data argument will be constant over iterated calls and the frame needs to
+ * setup pointers inside data */
+ frame = av_frame_alloc();
+ if (!frame)
+ return 0;
+
+ frame->nb_samples = m_CodecCtx->frame_size;
+ frame->format = m_CodecCtx->sample_fmt;
+ frame->channel_layout = m_CodecCtx->channel_layout;
+ frame->channels = m_CodecCtx->channels;
+
+ avcodec_fill_audio_frame(frame, m_CodecCtx->channels, m_CodecCtx->sample_fmt,
+ in, in_size, 0);
+
+ /* initialize the output packet */
+ AVPacket* pkt = av_packet_alloc();
+ if (!pkt)
+ {
+ CLog::Log(LOGERROR, "CAEEncoderFFmpeg::{} - av_packet_alloc failed: {}", __FUNCTION__,
+ strerror(errno));
+ av_frame_free(&frame);
+ return 0;
+ }
+
+ pkt->size = out_size;
+ pkt->data = out;
+
+ /* encode it */
+ int ret = avcodec_encode_audio2(m_CodecCtx, pkt, frame, &got_output);
+
+ int size = pkt->size;
+
+ /* free temporary data */
+ av_frame_free(&frame);
+
+ /* free the packet */
+ av_packet_free(&pkt);
+
+ if (ret < 0 || !got_output)
+ {
+ CLog::Log(LOGERROR, "CAEEncoderFFmpeg::Encode - Encoding failed");
+ return 0;
+ }
+
+ /* return the number of frames used */
+ return size;
+}
+
+
+int CAEEncoderFFmpeg::GetData(uint8_t **data)
+{
+ int size;
+ *data = m_Buffer;
+ size = m_BufferSize;
+ m_BufferSize = 0;
+ return size;
+}
+
+double CAEEncoderFFmpeg::GetDelay(unsigned int bufferSize)
+{
+ if (!m_CodecCtx)
+ return 0;
+
+ int frames = m_CodecCtx->delay;
+ if (m_BufferSize)
+ frames += m_NeededFrames;
+
+ return ((double)frames + ((double)bufferSize * m_OutputRatio)) * m_SampleRateMul;
+}
+
diff --git a/xbmc/cores/AudioEngine/Encoders/AEEncoderFFmpeg.h b/xbmc/cores/AudioEngine/Encoders/AEEncoderFFmpeg.h
new file mode 100644
index 0000000..3274d3a
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Encoders/AEEncoderFFmpeg.h
@@ -0,0 +1,55 @@
+/*
+ * 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 "cores/AudioEngine/Interfaces/AEEncoder.h"
+
+extern "C" {
+#include <libswresample/swresample.h>
+}
+
+/* ffmpeg re-defines this, so undef it to squash the warning */
+#undef restrict
+
+class CAEEncoderFFmpeg: public IAEEncoder
+{
+public:
+ CAEEncoderFFmpeg();
+ ~CAEEncoderFFmpeg() override;
+
+ bool IsCompatible(const AEAudioFormat& format) override;
+ bool Initialize(AEAudioFormat &format, bool allow_planar_input = false) override;
+ void Reset() override;
+
+ unsigned int GetBitRate() override;
+ AVCodecID GetCodecID() override;
+ unsigned int GetFrames() override;
+
+ int Encode(uint8_t *in, int in_size, uint8_t *out, int out_size) override;
+ int GetData(uint8_t **data) override;
+ double GetDelay(unsigned int bufferSize) override;
+private:
+ unsigned int BuildChannelLayout(const int64_t ffmap, CAEChannelInfo& layout);
+
+ std::string m_CodecName;
+ AVCodecID m_CodecID;
+ unsigned int m_BitRate = 0;
+ AEAudioFormat m_CurrentFormat;
+ AVCodecContext *m_CodecCtx;
+ SwrContext *m_SwrCtx;
+ CAEChannelInfo m_Layout;
+ uint8_t m_Buffer[8 + AV_INPUT_BUFFER_MIN_SIZE];
+ int m_BufferSize = 0;
+ int m_OutputSize = 0;
+ double m_OutputRatio = 0.0;
+ double m_SampleRateMul = 0.0;
+ unsigned int m_NeededFrames = 0;
+ bool m_NeedConversion = false;
+};
+
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAE.cpp b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAE.cpp
new file mode 100644
index 0000000..9053603
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAE.cpp
@@ -0,0 +1,3498 @@
+/*
+ * 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 "ActiveAE.h"
+
+#include <mutex>
+
+using namespace AE;
+using namespace ActiveAE;
+#include "ActiveAESettings.h"
+#include "ActiveAESound.h"
+#include "ActiveAEStream.h"
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/Interfaces/IAudioCallback.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "cores/AudioEngine/Utils/AEStreamData.h"
+#include "cores/AudioEngine/Utils/AEStreamInfo.h"
+#include "cores/AudioEngine/AEResampleFactory.h"
+#include "cores/AudioEngine/Encoders/AEEncoderFFmpeg.h"
+
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "windowing/WinSystem.h"
+#include "utils/log.h"
+
+using namespace std::chrono_literals;
+
+namespace
+{
+constexpr float MAX_CACHE_LEVEL = 0.4f; // total cache time of stream in seconds;
+constexpr float MAX_WATER_LEVEL = 0.2f; // buffered time after stream stages in seconds;
+constexpr double MAX_BUFFER_TIME = 0.1; // max time of a buffer in seconds;
+} // unnamed namespace
+
+void CEngineStats::Reset(unsigned int sampleRate, bool pcm)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ m_sinkDelay.SetDelay(0.0);
+ m_sinkSampleRate = sampleRate;
+ m_bufferedSamples = 0;
+ m_suspended = false;
+ m_pcmOutput = pcm;
+}
+
+void CEngineStats::UpdateSinkDelay(const AEDelayStatus& status, int samples)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ m_sinkDelay = status;
+ if (samples > m_bufferedSamples)
+ {
+ CLog::Log(LOGERROR, "CEngineStats::UpdateSinkDelay - inconsistency in buffer time");
+ }
+ else
+ m_bufferedSamples -= samples;
+}
+
+void CEngineStats::AddSamples(int samples, const std::list<CActiveAEStream*>& streams)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ m_bufferedSamples += samples;
+
+ for (auto stream : streams)
+ {
+ UpdateStream(stream);
+ }
+}
+
+void CEngineStats::GetDelay(AEDelayStatus& status)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ status = m_sinkDelay;
+ if (m_pcmOutput)
+ status.delay += (double)m_bufferedSamples / m_sinkSampleRate;
+ else
+ status.delay +=
+ static_cast<double>(m_bufferedSamples) * m_sinkFormat.m_streamInfo.GetDuration() / 1000;
+}
+
+void CEngineStats::AddStream(unsigned int streamid)
+{
+ StreamStats stream;
+ stream.m_streamId = streamid;
+ stream.m_bufferedTime = 0;
+ stream.m_resampleRatio = 1.0;
+ stream.m_syncError = 0;
+ stream.m_syncState = CAESyncInfo::AESyncState::SYNC_OFF;
+ m_streamStats.push_back(stream);
+}
+
+void CEngineStats::RemoveStream(unsigned int streamid)
+{
+ for (auto it = m_streamStats.begin(); it != m_streamStats.end(); ++it)
+ {
+ if (it->m_streamId == streamid)
+ {
+ m_streamStats.erase(it);
+ return;
+ }
+ }
+}
+
+void CEngineStats::UpdateStream(CActiveAEStream *stream)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ for (auto &str : m_streamStats)
+ {
+ if (str.m_streamId == stream->m_id)
+ {
+ float delay = 0;
+ str.m_syncState = stream->m_syncState;
+ str.m_syncError = stream->m_syncError.GetLastError(str.m_errorTime);
+ if (stream->m_processingBuffers)
+ {
+ str.m_resampleRatio = stream->m_processingBuffers->GetRR();
+ delay += stream->m_processingBuffers->GetDelay();
+ }
+ else
+ {
+ str.m_resampleRatio = 1.0;
+ }
+
+ std::unique_lock<CCriticalSection> lock(stream->m_statsLock);
+ std::deque<CSampleBuffer*>::iterator itBuf;
+ for(itBuf=stream->m_processingSamples.begin(); itBuf!=stream->m_processingSamples.end(); ++itBuf)
+ {
+ if (m_pcmOutput)
+ delay += (float)(*itBuf)->pkt->nb_samples / (*itBuf)->pkt->config.sample_rate;
+ else
+ delay += static_cast<float>(m_sinkFormat.m_streamInfo.GetDuration() / 1000.0);
+ }
+ str.m_bufferedTime = static_cast<double>(delay);
+ stream->m_bufferedTime = 0;
+ break;
+ }
+ }
+}
+
+// this is used to sync a/v so we need to add sink latency here
+void CEngineStats::GetDelay(AEDelayStatus& status, CActiveAEStream *stream)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ status = m_sinkDelay;
+ status.delay += static_cast<double>(m_sinkLatency);
+ if (m_pcmOutput)
+ status.delay += (double)m_bufferedSamples / m_sinkSampleRate;
+ else
+ status.delay +=
+ static_cast<double>(m_bufferedSamples) * m_sinkFormat.m_streamInfo.GetDuration() / 1000;
+
+ if (!m_pcmOutput && m_sinkNeedIecPack &&
+ m_sinkFormat.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD)
+ {
+ // take into account MAT packer latency (half duration of MAT frame)
+ status.delay += m_sinkFormat.m_streamInfo.GetDuration() / 1000 / 2;
+ }
+
+ for (auto &str : m_streamStats)
+ {
+ if (str.m_streamId == stream->m_id)
+ {
+ std::unique_lock<CCriticalSection> lock(stream->m_statsLock);
+ float buffertime = static_cast<float>(str.m_bufferedTime) + stream->m_bufferedTime;
+ status.delay += static_cast<double>(buffertime) / str.m_resampleRatio;
+ return;
+ }
+ }
+}
+
+// this is used to sync a/v so we need to add sink latency here
+void CEngineStats::GetSyncInfo(CAESyncInfo& info, CActiveAEStream *stream)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ AEDelayStatus status;
+ status = m_sinkDelay;
+ if (m_pcmOutput)
+ status.delay += (double)m_bufferedSamples / m_sinkSampleRate;
+ else
+ status.delay +=
+ static_cast<double>(m_bufferedSamples) * m_sinkFormat.m_streamInfo.GetDuration() / 1000;
+
+ status.delay += static_cast<double>(m_sinkLatency);
+
+ if (!m_pcmOutput && m_sinkNeedIecPack &&
+ m_sinkFormat.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD)
+ {
+ // take into account MAT packer latency (half duration of MAT frame)
+ status.delay += m_sinkFormat.m_streamInfo.GetDuration() / 1000 / 2;
+ }
+
+ for (auto &str : m_streamStats)
+ {
+ if (str.m_streamId == stream->m_id)
+ {
+ std::unique_lock<CCriticalSection> lock(stream->m_statsLock);
+ float buffertime = static_cast<float>(str.m_bufferedTime) + stream->m_bufferedTime;
+ status.delay += static_cast<double>(buffertime) / str.m_resampleRatio;
+ info.delay = status.GetDelay();
+ info.error = str.m_syncError;
+ info.errortime = str.m_errorTime;
+ info.state = str.m_syncState;
+ info.rr = str.m_resampleRatio;
+ return;
+ }
+ }
+}
+
+float CEngineStats::GetCacheTime(CActiveAEStream *stream)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ float delay = 0;
+
+ for (auto &str : m_streamStats)
+ {
+ if (str.m_streamId == stream->m_id)
+ {
+ std::unique_lock<CCriticalSection> lock(stream->m_statsLock);
+ float buffertime = static_cast<float>(str.m_bufferedTime) + stream->m_bufferedTime;
+ delay += buffertime / static_cast<float>(str.m_resampleRatio);
+ break;
+ }
+ }
+ return delay;
+}
+
+float CEngineStats::GetCacheTotal()
+{
+ return MAX_CACHE_LEVEL;
+}
+
+float CEngineStats::GetMaxDelay() const
+{
+ return MAX_CACHE_LEVEL + MAX_WATER_LEVEL + m_sinkCacheTotal;
+}
+
+float CEngineStats::GetWaterLevel()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ if (m_pcmOutput)
+ return static_cast<float>(m_bufferedSamples) / m_sinkSampleRate;
+ else
+ return static_cast<float>(m_bufferedSamples * m_sinkFormat.m_streamInfo.GetDuration()) / 1000;
+}
+
+void CEngineStats::SetSuspended(bool state)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ m_suspended = state;
+}
+
+bool CEngineStats::IsSuspended()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ return m_suspended;
+}
+
+void CEngineStats::SetCurrentSinkFormat(const AEAudioFormat& SinkFormat)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ m_sinkFormat = SinkFormat;
+}
+
+AEAudioFormat CEngineStats::GetCurrentSinkFormat()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ return m_sinkFormat;
+}
+
+CActiveAE::CActiveAE() :
+ CThread("ActiveAE"),
+ m_controlPort("OutputControlPort", &m_inMsgEvent, &m_outMsgEvent),
+ m_dataPort("OutputDataPort", &m_inMsgEvent, &m_outMsgEvent),
+ m_sink(&m_outMsgEvent)
+{
+ m_sinkBuffers = NULL;
+ m_silenceBuffers = NULL;
+ m_encoderBuffers = NULL;
+ m_vizBuffers = NULL;
+ m_vizBuffersInput = NULL;
+ m_volume = 1.0;
+ m_volumeScaled = 1.0;
+ m_aeVolume = 1.0;
+ m_muted = false;
+ m_aeMuted = false;
+ m_mode = MODE_PCM;
+ m_encoder = NULL;
+ m_vizInitialized = false;
+ m_sinkHasVolume = false;
+ m_aeGUISoundForce = false;
+ m_stats.Reset(44100, true);
+ m_streamIdGen = 0;
+
+ m_settingsHandler.reset(new CActiveAESettings(*this));
+}
+
+CActiveAE::~CActiveAE()
+{
+ m_settingsHandler.reset();
+
+ Dispose();
+}
+
+void CActiveAE::Dispose()
+{
+ if (m_isWinSysReg)
+ {
+ CWinSystemBase *winsystem = CServiceBroker::GetWinSystem();
+ if (winsystem)
+ winsystem->Unregister(this);
+ m_isWinSysReg = false;
+ }
+
+ m_bStop = true;
+ m_outMsgEvent.Set();
+ StopThread();
+ m_controlPort.Purge();
+ m_dataPort.Purge();
+ m_sink.Dispose();
+}
+
+//-----------------------------------------------------------------------------
+// Behavior
+//-----------------------------------------------------------------------------
+
+enum AE_STATES
+{
+ AE_TOP = 0, // 0
+ AE_TOP_WAIT_PRECOND, // 1
+ AE_TOP_ERROR, // 2
+ AE_TOP_UNCONFIGURED, // 3
+ AE_TOP_RECONFIGURING, // 4
+ AE_TOP_CONFIGURED, // 5
+ AE_TOP_CONFIGURED_SUSPEND, // 6
+ AE_TOP_CONFIGURED_IDLE, // 6
+ AE_TOP_CONFIGURED_PLAY, // 7
+};
+
+int AE_parentStates[] = {
+ -1,
+ 0, //TOP_ERROR
+ 0, //AE_TOP_WAIT_PRECOND
+ 0, //TOP_UNCONFIGURED
+ 0, //TOP_CONFIGURED
+ 0, //TOP_RECONFIGURING
+ 5, //TOP_CONFIGURED_SUSPEND
+ 5, //TOP_CONFIGURED_IDLE
+ 5, //TOP_CONFIGURED_PLAY
+};
+
+void CActiveAE::StateMachine(int signal, Protocol *port, Message *msg)
+{
+ for (int state = m_state; ; state = AE_parentStates[state])
+ {
+ switch (state)
+ {
+ case AE_TOP: // TOP
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case CActiveAEControlProtocol::GETSTATE:
+ msg->Reply(CActiveAEControlProtocol::ACC, &m_state, sizeof(m_state));
+ return;
+ case CActiveAEControlProtocol::VOLUME:
+ m_volume = *(float*)msg->data;
+ m_volumeScaled = CAEUtil::GainToScale(CAEUtil::PercentToGain(m_volume));
+ if (m_sinkHasVolume)
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::VOLUME, &m_volume, sizeof(float));
+ return;
+ case CActiveAEControlProtocol::MUTE:
+ m_muted = *(bool*)msg->data;
+ return;
+ case CActiveAEControlProtocol::KEEPCONFIG:
+ m_extKeepConfig = std::chrono::milliseconds(*reinterpret_cast<unsigned int*>(msg->data));
+ return;
+ case CActiveAEControlProtocol::DISPLAYRESET:
+ return;
+ case CActiveAEControlProtocol::APPFOCUSED:
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::APPFOCUSED, msg->data, sizeof(bool));
+ return;
+ case CActiveAEControlProtocol::STREAMRESAMPLEMODE:
+ MsgStreamParameter *par;
+ par = reinterpret_cast<MsgStreamParameter*>(msg->data);
+ if (par->stream)
+ {
+ par->stream->m_resampleMode = par->parameter.int_par;
+ par->stream->m_resampleIntegral = 0.0;
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case CActiveAEDataProtocol::NEWSOUND:
+ CActiveAESound *sound;
+ sound = *(CActiveAESound**)msg->data;
+ if (sound)
+ {
+ m_sounds.push_back(sound);
+ ResampleSounds();
+ }
+ return;
+ case CActiveAEDataProtocol::FREESTREAM:
+ MsgStreamFree *msgStreamFree;
+ msgStreamFree = *(MsgStreamFree**)msg->data;
+ DiscardStream(msgStreamFree->stream);
+ msg->Reply(CActiveAEDataProtocol::ACC);
+ return;
+ case CActiveAEDataProtocol::FREESOUND:
+ sound = *(CActiveAESound**)msg->data;
+ DiscardSound(sound);
+ return;
+ case CActiveAEDataProtocol::DRAINSTREAM:
+ CActiveAEStream *stream;
+ stream = *(CActiveAEStream**)msg->data;
+ stream->m_drain = true;
+ stream->m_processingBuffers->SetDrain(true);
+ msg->Reply(CActiveAEDataProtocol::ACC);
+ stream->m_streamPort->SendInMessage(CActiveAEDataProtocol::STREAMDRAINED);
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_sink.m_dataPort)
+ {
+ switch (signal)
+ {
+ case CSinkDataProtocol::RETURNSAMPLE:
+ CSampleBuffer **buffer;
+ buffer = (CSampleBuffer**)msg->data;
+ if (buffer)
+ {
+ (*buffer)->Return();
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ {
+ std::string portName = port == NULL ? "timer" : port->portName;
+ CLog::Log(LOGWARNING, "CActiveAE::{} - signal: {} from port: {} not handled for state: {}",
+ __FUNCTION__, signal, portName, m_state);
+ }
+ return;
+
+ case AE_TOP_WAIT_PRECOND:
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case CActiveAEControlProtocol::INIT:
+ LoadSettings();
+ if (!m_settings.device.empty() && CAESinkFactory::HasSinks())
+ {
+ m_state = AE_TOP_UNCONFIGURED;
+ m_bStateMachineSelfTrigger = true;
+ }
+ else
+ {
+ // Application can't handle error case and work without an AE
+ msg->Reply(CActiveAEControlProtocol::ACC);
+ }
+ return;
+
+ case CActiveAEControlProtocol::DEVICECHANGE:
+ case CActiveAEControlProtocol::DEVICECOUNTCHANGE:
+ LoadSettings();
+ if (!m_settings.device.empty() && CAESinkFactory::HasSinks())
+ {
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::INIT);
+ }
+ return;
+
+ default:
+ break;
+ }
+ }
+ break;
+
+ case AE_TOP_ERROR:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case CActiveAEControlProtocol::TIMEOUT:
+ m_extError = false;
+ LoadSettings();
+ Configure();
+ if (!m_extError)
+ {
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ }
+ else
+ {
+ m_state = AE_TOP_ERROR;
+ m_extTimeout = 500ms;
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case AE_TOP_UNCONFIGURED:
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case CActiveAEControlProtocol::INIT:
+ m_extError = false;
+ m_sink.EnumerateSinkList(false, "");
+ LoadSettings();
+ Configure();
+ if (!m_isWinSysReg)
+ {
+ CWinSystemBase *winsystem = CServiceBroker::GetWinSystem();
+ if (winsystem)
+ {
+ winsystem->Register(this);
+ m_isWinSysReg = true;
+ }
+ }
+ msg->Reply(CActiveAEControlProtocol::ACC);
+ if (!m_extError)
+ {
+ m_state = AE_TOP_CONFIGURED_IDLE;
+ m_extTimeout = 0ms;
+ }
+ else
+ {
+ m_state = AE_TOP_ERROR;
+ m_extTimeout = 500ms;
+ }
+ return;
+
+ default:
+ break;
+ }
+ }
+ break;
+
+ case AE_TOP_RECONFIGURING:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case CActiveAEControlProtocol::TIMEOUT:
+ // drain
+ if (RunStages())
+ {
+ m_extTimeout = 0ms;
+ return;
+ }
+ if (!m_sinkBuffers->m_inputSamples.empty() || !m_sinkBuffers->m_outputSamples.empty())
+ {
+ m_extTimeout = 100ms;
+ return;
+ }
+ if (NeedReconfigureSink())
+ DrainSink();
+
+ if (!m_extError)
+ Configure();
+ if (!m_extError)
+ {
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ }
+ else
+ {
+ m_state = AE_TOP_ERROR;
+ m_extTimeout = 500ms;
+ }
+ m_extDeferData = false;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case AE_TOP_CONFIGURED:
+ if (port == &m_controlPort)
+ {
+ bool streaming;
+ switch (signal)
+ {
+ case CActiveAEControlProtocol::RECONFIGURE:
+ {
+ if (m_streams.empty())
+ {
+ streaming = false;
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::STREAMING, &streaming, sizeof(bool));
+ }
+ LoadSettings();
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::SETNOISETYPE, &m_settings.streamNoise, sizeof(bool));
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::SETSILENCETIMEOUT,
+ &m_settings.silenceTimeoutMinutes, sizeof(int));
+ ChangeResamplers();
+ if (!NeedReconfigureBuffers() && !NeedReconfigureSink())
+ return;
+ m_state = AE_TOP_RECONFIGURING;
+ m_extTimeout = 0ms;
+ // don't accept any data until we are reconfigured
+ m_extDeferData = true;
+ return;
+ }
+ case CActiveAEControlProtocol::SUSPEND:
+ UnconfigureSink();
+ m_stats.SetSuspended(true);
+ m_state = AE_TOP_CONFIGURED_SUSPEND;
+ m_extDeferData = true;
+ m_extSuspended = true;
+ return;
+ case CActiveAEControlProtocol::DISPLAYLOST:
+ if (m_sink.GetDeviceType(m_mode == MODE_PCM ? m_settings.device : m_settings.passthroughdevice) == AE_DEVTYPE_HDMI)
+ {
+ UnconfigureSink();
+ m_stats.SetSuspended(true);
+ m_state = AE_TOP_CONFIGURED_SUSPEND;
+ m_extDeferData = true;
+ }
+ msg->Reply(CActiveAEControlProtocol::ACC);
+ return;
+ case CActiveAEControlProtocol::DEVICECHANGE:
+ time_t now;
+ time(&now);
+ CLog::Log(LOGDEBUG,"CActiveAE - device change event");
+ while (!m_extLastDeviceChange.empty() && (now - m_extLastDeviceChange.front() > 0))
+ {
+ m_extLastDeviceChange.pop();
+ }
+ if (m_extLastDeviceChange.size() > 2)
+ {
+ CLog::Log(LOGWARNING, "CActiveAE - received {} device change events within one second",
+ m_extLastDeviceChange.size());
+ return;
+ }
+ m_extLastDeviceChange.push(now);
+ UnconfigureSink();
+ m_controlPort.PurgeOut(CActiveAEControlProtocol::DEVICECHANGE);
+ m_sink.EnumerateSinkList(true, "");
+ LoadSettings();
+ m_extError = false;
+ Configure();
+ if (!m_extError)
+ {
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ }
+ else
+ {
+ m_state = AE_TOP_ERROR;
+ m_extTimeout = 500ms;
+ }
+ return;
+ case CActiveAEControlProtocol::DEVICECOUNTCHANGE:
+ const char* param;
+ param = reinterpret_cast<const char*>(msg->data);
+ CLog::Log(LOGDEBUG, "CActiveAE - device count change event from driver: {}", param);
+ m_sink.EnumerateSinkList(true, param);
+ if (!m_sink.DeviceExist(m_settings.driver, m_currDevice))
+ {
+ UnconfigureSink();
+ LoadSettings();
+ m_extError = false;
+ Configure();
+ if (!m_extError)
+ {
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ }
+ else
+ {
+ m_state = AE_TOP_ERROR;
+ m_extTimeout = 500ms;
+ }
+ }
+ return;
+ case CActiveAEControlProtocol::PAUSESTREAM:
+ CActiveAEStream *stream;
+ stream = *(CActiveAEStream**)msg->data;
+ if (!stream->m_paused && m_streams.size() == 1)
+ {
+ FlushEngine();
+ streaming = false;
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::STREAMING, &streaming, sizeof(bool));
+ }
+ stream->m_paused = true;
+ return;
+ case CActiveAEControlProtocol::RESUMESTREAM:
+ stream = *(CActiveAEStream**)msg->data;
+ if (stream->m_paused)
+ stream->m_syncState = CAESyncInfo::AESyncState::SYNC_START;
+ stream->m_paused = false;
+ streaming = true;
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::STREAMING, &streaming, sizeof(bool));
+ m_extTimeout = 0ms;
+ return;
+ case CActiveAEControlProtocol::FLUSHSTREAM:
+ stream = *(CActiveAEStream**)msg->data;
+ SFlushStream(stream);
+ msg->Reply(CActiveAEControlProtocol::ACC);
+ m_extTimeout = 0ms;
+ return;
+ case CActiveAEControlProtocol::STREAMAMP:
+ MsgStreamParameter *par;
+ par = reinterpret_cast<MsgStreamParameter*>(msg->data);
+ par->stream->m_limiter.SetAmplification(par->parameter.float_par);
+ par->stream->m_amplify = par->parameter.float_par;
+ return;
+ case CActiveAEControlProtocol::STREAMVOLUME:
+ par = reinterpret_cast<MsgStreamParameter*>(msg->data);
+ par->stream->m_volume = par->parameter.float_par;
+ return;
+ case CActiveAEControlProtocol::STREAMRGAIN:
+ par = reinterpret_cast<MsgStreamParameter*>(msg->data);
+ par->stream->m_rgain = par->parameter.float_par;
+ return;
+ case CActiveAEControlProtocol::STREAMRESAMPLERATIO:
+ par = reinterpret_cast<MsgStreamParameter*>(msg->data);
+ if (par->stream->m_processingBuffers)
+ {
+ par->stream->m_processingBuffers->SetRR(par->parameter.double_par, m_settings.atempoThreshold);
+ }
+ return;
+ case CActiveAEControlProtocol::STREAMFFMPEGINFO:
+ MsgStreamFFmpegInfo *info;
+ info = reinterpret_cast<MsgStreamFFmpegInfo*>(msg->data);
+ info->stream->m_profile = info->profile;
+ info->stream->m_matrixEncoding = info->matrix_encoding;
+ info->stream->m_audioServiceType = info->audio_service_type;
+ return;
+ case CActiveAEControlProtocol::STREAMFADE:
+ MsgStreamFade *fade;
+ fade = reinterpret_cast<MsgStreamFade*>(msg->data);
+ fade->stream->m_fadingBase = fade->from;
+ fade->stream->m_fadingTarget = fade->target;
+ fade->stream->m_fadingTime = fade->millis;
+ fade->stream->m_fadingSamples = -1;
+ return;
+ case CActiveAEControlProtocol::STOPSOUND:
+ CActiveAESound *sound;
+ sound = *(CActiveAESound**)msg->data;
+ SStopSound(sound);
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case CActiveAEDataProtocol::PLAYSOUND:
+ CActiveAESound *sound;
+ sound = *(CActiveAESound**)msg->data;
+ if (sound)
+ {
+ if (m_settings.guisoundmode == AE_SOUND_OFF ||
+ (m_settings.guisoundmode == AE_SOUND_IDLE && !m_streams.empty()))
+ return;
+
+ SoundState st = {sound, 0};
+ m_sounds_playing.push_back(st);
+ m_extTimeout = 0ms;
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ }
+ return;
+ case CActiveAEDataProtocol::NEWSTREAM:
+ MsgStreamNew *streamMsg;
+ CActiveAEStream *stream;
+ streamMsg = reinterpret_cast<MsgStreamNew*>(msg->data);
+ stream = CreateStream(streamMsg);
+ if(stream)
+ {
+ msg->Reply(CActiveAEDataProtocol::ACC, &stream, sizeof(CActiveAEStream*));
+ LoadSettings();
+ Configure();
+ if (!m_extError)
+ {
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ }
+ else
+ {
+ m_state = AE_TOP_ERROR;
+ m_extTimeout = 500ms;
+ }
+ }
+ else
+ msg->Reply(CActiveAEDataProtocol::ERR);
+ return;
+ case CActiveAEDataProtocol::STREAMSAMPLE:
+ MsgStreamSample *msgData;
+ CSampleBuffer *samples;
+ msgData = reinterpret_cast<MsgStreamSample*>(msg->data);
+ samples = msgData->stream->m_processingSamples.front();
+ msgData->stream->m_processingSamples.pop_front();
+ if (samples != msgData->buffer)
+ CLog::Log(LOGERROR, "CActiveAE - inconsistency in stream sample message");
+ if (msgData->buffer->pkt->nb_samples == 0)
+ msgData->buffer->Return();
+ else
+ msgData->stream->m_processingBuffers->m_inputSamples.push_back(msgData->buffer);
+ m_extTimeout = 0ms;
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ return;
+ case CActiveAEDataProtocol::FREESTREAM:
+ MsgStreamFree *msgStreamFree;
+ msgStreamFree = reinterpret_cast<MsgStreamFree*>(msg->data);
+ DiscardStream(msgStreamFree->stream);
+ msg->Reply(CActiveAEDataProtocol::ACC);
+ if (m_streams.empty())
+ {
+ if (m_extKeepConfig > 0ms)
+ m_extDrainTimer.Set(m_extKeepConfig);
+ else
+ {
+ AEDelayStatus status;
+ m_stats.GetDelay(status);
+ if (msgStreamFree->finish)
+ m_extDrainTimer.Set(
+ std::chrono::milliseconds(static_cast<int>(status.GetDelay() * 1000)));
+ else
+ m_extDrainTimer.Set(
+ std::chrono::milliseconds(static_cast<int>(status.GetDelay() * 1000)) + 1s);
+ }
+ m_extDrain = true;
+ }
+ m_extTimeout = 0ms;
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ return;
+ case CActiveAEDataProtocol::DRAINSTREAM:
+ stream = *(CActiveAEStream**)msg->data;
+ stream->m_drain = true;
+ stream->m_processingBuffers->SetDrain(true);
+ m_extTimeout = 0ms;
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ msg->Reply(CActiveAEDataProtocol::ACC);
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_sink.m_dataPort)
+ {
+ switch (signal)
+ {
+ case CSinkDataProtocol::RETURNSAMPLE:
+ CSampleBuffer **buffer;
+ buffer = (CSampleBuffer**)msg->data;
+ if (buffer)
+ {
+ (*buffer)->Return();
+ }
+ m_extTimeout = 0ms;
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case AE_TOP_CONFIGURED_SUSPEND:
+ if (port == &m_controlPort)
+ {
+ bool displayReset = false;
+ switch (signal)
+ {
+ case CActiveAEControlProtocol::DISPLAYRESET:
+ if (m_extSuspended)
+ return;
+ CLog::Log(LOGDEBUG,"CActiveAE - display reset event");
+ displayReset = true;
+ [[fallthrough]];
+ case CActiveAEControlProtocol::INIT:
+ m_extError = false;
+ m_extSuspended = false;
+ if (!displayReset)
+ {
+ m_controlPort.PurgeOut(CActiveAEControlProtocol::DEVICECHANGE);
+ m_controlPort.PurgeOut(CActiveAEControlProtocol::DEVICECOUNTCHANGE);
+ m_sink.EnumerateSinkList(true, "");
+ LoadSettings();
+ }
+ Configure();
+ if (!displayReset)
+ msg->Reply(CActiveAEControlProtocol::ACC);
+ if (!m_extError)
+ {
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ }
+ else
+ {
+ m_state = AE_TOP_ERROR;
+ m_extTimeout = 500ms;
+ }
+ m_stats.SetSuspended(false);
+ m_extDeferData = false;
+ return;
+ case CActiveAEControlProtocol::DEVICECHANGE:
+ case CActiveAEControlProtocol::DEVICECOUNTCHANGE:
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_sink.m_dataPort)
+ {
+ switch (signal)
+ {
+ case CSinkDataProtocol::RETURNSAMPLE:
+ CSampleBuffer **buffer;
+ buffer = (CSampleBuffer**)msg->data;
+ if (buffer)
+ {
+ (*buffer)->Return();
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case CActiveAEControlProtocol::TIMEOUT:
+ m_extTimeout = 1000ms;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case AE_TOP_CONFIGURED_IDLE:
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case CActiveAEControlProtocol::RESUMESTREAM:
+ CActiveAEStream *stream;
+ stream = *(CActiveAEStream**)msg->data;
+ stream->m_paused = false;
+ stream->m_syncState = CAESyncInfo::AESyncState::SYNC_START;
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ return;
+ case CActiveAEControlProtocol::FLUSHSTREAM:
+ stream = *(CActiveAEStream**)msg->data;
+ SFlushStream(stream);
+ msg->Reply(CActiveAEControlProtocol::ACC);
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case CActiveAEControlProtocol::TIMEOUT:
+ ResampleSounds();
+ ClearDiscardedBuffers();
+ if (m_extDrain)
+ {
+ if (m_extDrainTimer.IsTimePast())
+ {
+ Configure();
+ if (!m_extError)
+ {
+ m_state = AE_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ }
+ else
+ {
+ m_state = AE_TOP_ERROR;
+ m_extTimeout = 500ms;
+ }
+ }
+ else
+ m_extTimeout = m_extDrainTimer.GetTimeLeft();
+ }
+ else
+ m_extTimeout = 5000ms;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case AE_TOP_CONFIGURED_PLAY:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case CActiveAEControlProtocol::TIMEOUT:
+ if (m_extError)
+ {
+ m_state = AE_TOP_ERROR;
+ m_extTimeout = 100ms;
+ return;
+ }
+ if (RunStages())
+ {
+ m_extTimeout = 0ms;
+ return;
+ }
+ if (!m_extDrain && HasWork())
+ {
+ ClearDiscardedBuffers();
+ m_extTimeout = 100ms;
+ return;
+ }
+ m_extTimeout = 0ms;
+ m_state = AE_TOP_CONFIGURED_IDLE;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ default: // we are in no state, should not happen
+ CLog::Log(LOGERROR, "CActiveAE::{} - no valid state: {}", __FUNCTION__, m_state);
+ return;
+ }
+ } // for
+}
+
+void CActiveAE::Process()
+{
+ Message *msg = NULL;
+ Protocol *port = NULL;
+ bool gotMsg;
+ XbmcThreads::EndTime<> timer;
+
+ m_state = AE_TOP_WAIT_PRECOND;
+ m_extTimeout = 1000ms;
+ m_bStateMachineSelfTrigger = false;
+ m_extDrain = false;
+ m_extDeferData = false;
+ m_extKeepConfig = 0ms;
+
+ // start sink
+ m_sink.Start();
+
+ while (!m_bStop)
+ {
+ gotMsg = false;
+ timer.Set(m_extTimeout);
+
+ if (m_bStateMachineSelfTrigger)
+ {
+ m_bStateMachineSelfTrigger = false;
+ // self trigger state machine
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ continue;
+ }
+ // check control port
+ else if (m_controlPort.ReceiveOutMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_controlPort;
+ }
+ // check sink data port
+ else if (m_sink.m_dataPort.ReceiveInMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_sink.m_dataPort;
+ }
+ else if (!m_extDeferData)
+ {
+ // check data port
+ if (m_dataPort.ReceiveOutMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_dataPort;
+ }
+ // stream data ports
+ else
+ {
+ std::list<CActiveAEStream*>::iterator it;
+ for(it=m_streams.begin(); it!=m_streams.end(); ++it)
+ {
+ if((*it)->m_streamPort->ReceiveOutMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_dataPort;
+ break;
+ }
+ }
+ }
+ }
+
+ if (gotMsg)
+ {
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ continue;
+ }
+
+ // wait for message
+ else if (m_outMsgEvent.Wait(m_extTimeout))
+ {
+ m_extTimeout = timer.GetTimeLeft();
+ continue;
+ }
+ // time out
+ else
+ {
+ msg = m_controlPort.GetMessage();
+ msg->signal = CActiveAEControlProtocol::TIMEOUT;
+ port = 0;
+ // signal timeout to state machine
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ }
+ }
+}
+
+AEAudioFormat CActiveAE::GetInputFormat(AEAudioFormat *desiredFmt)
+{
+ AEAudioFormat inputFormat;
+
+ if (m_streams.empty())
+ {
+ inputFormat.m_dataFormat = AE_FMT_FLOAT;
+ inputFormat.m_sampleRate = 44100;
+ inputFormat.m_channelLayout = AE_CH_LAYOUT_2_0;
+ inputFormat.m_frames = 0;
+ inputFormat.m_frameSize = 0;
+ }
+ // force input format after unpausing slave
+ else if (desiredFmt != NULL)
+ {
+ inputFormat = *desiredFmt;
+ }
+ // keep format when having multiple streams
+ else if (m_streams.size() > 1 && m_silenceBuffers == NULL)
+ {
+ inputFormat = m_inputFormat;
+ }
+ else
+ {
+ inputFormat = m_streams.front()->m_format;
+ m_inputFormat = inputFormat;
+ }
+
+ return inputFormat;
+}
+
+void CActiveAE::Configure(AEAudioFormat *desiredFmt)
+{
+ bool initSink = false;
+
+ AEAudioFormat sinkInputFormat, inputFormat;
+ AEAudioFormat oldInternalFormat = m_internalFormat;
+ AEAudioFormat oldSinkRequestFormat = m_sinkRequestFormat;
+
+ inputFormat = GetInputFormat(desiredFmt);
+
+ m_sinkRequestFormat = inputFormat;
+ ApplySettingsToFormat(m_sinkRequestFormat, m_settings, (int*)&m_mode);
+ m_extKeepConfig = 0ms;
+
+ std::string device = (m_sinkRequestFormat.m_dataFormat == AE_FMT_RAW) ? m_settings.passthroughdevice : m_settings.device;
+ std::string driver;
+ CAESinkFactory::ParseDevice(device, driver);
+ if ((!CompareFormat(m_sinkRequestFormat, m_sinkFormat) && !CompareFormat(m_sinkRequestFormat, oldSinkRequestFormat)) ||
+ m_currDevice.compare(device) != 0 ||
+ m_settings.driver.compare(driver) != 0)
+ {
+ FlushEngine();
+ if (!InitSink())
+ return;
+ m_settings.driver = driver;
+ m_currDevice = device;
+ initSink = true;
+ m_stats.Reset(m_sinkFormat.m_sampleRate, m_mode == MODE_PCM);
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::VOLUME, &m_volume, sizeof(float));
+
+ if (m_sinkRequestFormat.m_dataFormat != AE_FMT_RAW)
+ {
+ // limit buffer size in case of sink returns large buffer
+ double buffertime = (double)m_sinkFormat.m_frames / m_sinkFormat.m_sampleRate;
+ if (buffertime > MAX_BUFFER_TIME)
+ {
+ CLog::Log(LOGWARNING,
+ "ActiveAE::{} - sink returned large period time of {} ms, reducing to {} ms",
+ __FUNCTION__, (int)(buffertime * 1000), (int)(MAX_BUFFER_TIME * 1000));
+ m_sinkFormat.m_frames = MAX_BUFFER_TIME * m_sinkFormat.m_sampleRate;
+ }
+ }
+ }
+
+ if (m_silenceBuffers)
+ {
+ m_discardBufferPools.push_back(m_silenceBuffers);
+ m_silenceBuffers = NULL;
+ }
+
+ // buffers for driving gui sounds if no streams are active
+ if (m_streams.empty())
+ {
+ inputFormat = m_sinkFormat;
+ if (m_sinkFormat.m_channelLayout.Count() > m_sinkRequestFormat.m_channelLayout.Count())
+ {
+ inputFormat.m_channelLayout = m_sinkRequestFormat.m_channelLayout;
+ inputFormat.m_channelLayout.ResolveChannels(m_sinkFormat.m_channelLayout);
+ }
+ inputFormat.m_dataFormat = AE_FMT_FLOAT;
+ inputFormat.m_frameSize = inputFormat.m_channelLayout.Count() *
+ (CAEUtil::DataFormatToBits(inputFormat.m_dataFormat) >> 3);
+ m_silenceBuffers = new CActiveAEBufferPool(inputFormat);
+ m_silenceBuffers->Create(MAX_WATER_LEVEL*1000);
+ sinkInputFormat = inputFormat;
+ m_internalFormat = inputFormat;
+
+ bool streaming = false;
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::STREAMING, &streaming, sizeof(bool));
+
+ delete m_encoder;
+ m_encoder = NULL;
+
+ if (m_encoderBuffers)
+ {
+ m_discardBufferPools.push_back(m_encoderBuffers);
+ m_encoderBuffers = NULL;
+ }
+ if (m_vizBuffers)
+ {
+ m_discardBufferPools.push_back(m_vizBuffers);
+ m_vizBuffers = NULL;
+ }
+ if (m_vizBuffersInput)
+ {
+ m_discardBufferPools.push_back(m_vizBuffersInput);
+ m_vizBuffersInput = NULL;
+ }
+ }
+ // resample buffers for streams
+ else
+ {
+ bool streaming = true;
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::STREAMING, &streaming, sizeof(bool));
+
+ AEAudioFormat outputFormat;
+ if (m_mode == MODE_RAW)
+ {
+ inputFormat.m_frames = m_sinkFormat.m_frames;
+ outputFormat = inputFormat;
+ sinkInputFormat = m_sinkFormat;
+ }
+ // transcode everything with more than 2 channels
+ else if (m_mode == MODE_TRANSCODE)
+ {
+ outputFormat = inputFormat;
+ outputFormat.m_dataFormat = AE_FMT_FLOATP;
+ outputFormat.m_sampleRate = 48000;
+
+ // setup encoder
+ if (!m_encoder)
+ {
+ m_encoder = new CAEEncoderFFmpeg();
+ m_encoder->Initialize(outputFormat, true);
+ m_encoderFormat = outputFormat;
+ }
+ else
+ outputFormat = m_encoderFormat;
+
+ outputFormat.m_channelLayout = m_encoderFormat.m_channelLayout;
+ outputFormat.m_frames = m_encoderFormat.m_frames;
+
+ // encoder buffer
+ if (m_encoder->GetCodecID() == AV_CODEC_ID_AC3)
+ {
+ AEAudioFormat format;
+ format.m_channelLayout += AE_CH_FC;
+ format.m_dataFormat = AE_FMT_RAW;
+ format.m_sampleRate = 48000;
+ format.m_channelLayout = AE_CH_LAYOUT_2_0;
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_AC3;
+ format.m_streamInfo.m_channels = 2;
+ format.m_streamInfo.m_sampleRate = 48000;
+ format.m_streamInfo.m_ac3FrameSize = m_encoderFormat.m_frames;
+ //! @todo implement
+ if (m_encoderBuffers && initSink)
+ {
+ m_discardBufferPools.push_back(m_encoderBuffers);
+ m_encoderBuffers = NULL;
+ }
+ if (!m_encoderBuffers)
+ {
+ m_encoderBuffers = new CActiveAEBufferPool(format);
+ m_encoderBuffers->Create(MAX_WATER_LEVEL*1000);
+ }
+ }
+
+ sinkInputFormat = m_sinkFormat;
+ }
+ else
+ {
+ outputFormat = m_sinkFormat;
+ outputFormat.m_dataFormat = AE_IS_PLANAR(outputFormat.m_dataFormat) ? AE_FMT_FLOATP : AE_FMT_FLOAT;
+ outputFormat.m_frameSize = outputFormat.m_channelLayout.Count() *
+ (CAEUtil::DataFormatToBits(outputFormat.m_dataFormat) >> 3);
+
+ // due to channel ordering of the driver, a sink may return more channels than
+ // requested, i.e. 2.1 request returns FL,FR,BL,BR,FC,LFE for ALSA
+ // in this case we need to downmix to requested format
+ if (m_sinkFormat.m_channelLayout.Count() > m_sinkRequestFormat.m_channelLayout.Count())
+ {
+ outputFormat.m_channelLayout = m_sinkRequestFormat.m_channelLayout;
+ outputFormat.m_channelLayout.ResolveChannels(m_sinkFormat.m_channelLayout);
+ }
+
+ // internally we use ffmpeg layouts, means that layout won't change in resample
+ // stage. preserve correct layout for sink stage where remapping is done
+ uint64_t avlayout = CAEUtil::GetAVChannelLayout(outputFormat.m_channelLayout);
+ outputFormat.m_channelLayout = CAEUtil::GetAEChannelLayout(avlayout);
+
+ //! @todo adjust to decoder
+ sinkInputFormat = outputFormat;
+ }
+ m_internalFormat = outputFormat;
+
+ std::list<CActiveAEStream*>::iterator it;
+ for(it=m_streams.begin(); it!=m_streams.end(); ++it)
+ {
+ if (!(*it)->m_inputBuffers)
+ {
+ // align input buffers with period of sink or encoder
+ (*it)->m_format.m_frames = m_internalFormat.m_frames * ((float)(*it)->m_format.m_sampleRate / m_internalFormat.m_sampleRate);
+
+ // create buffer pool
+ (*it)->m_inputBuffers = new CActiveAEBufferPool((*it)->m_format);
+ (*it)->m_inputBuffers->Create(MAX_CACHE_LEVEL*1000);
+ (*it)->m_streamSpace = (*it)->m_format.m_frameSize * (*it)->m_format.m_frames;
+
+ // if input format does not follow ffmpeg channel mask, we may need to remap channels
+ (*it)->InitRemapper();
+ }
+ if (initSink && (*it)->m_processingBuffers)
+ {
+ (*it)->m_processingBuffers->Flush();
+ m_discardBufferPools.push_back((*it)->m_processingBuffers->GetResampleBuffers());
+ m_discardBufferPools.push_back((*it)->m_processingBuffers->GetAtempoBuffers());
+ delete (*it)->m_processingBuffers;
+ (*it)->m_processingBuffers = nullptr;
+ }
+ if (!(*it)->m_processingBuffers)
+ {
+ (*it)->m_processingBuffers = new CActiveAEStreamBuffers((*it)->m_inputBuffers->m_format, outputFormat, m_settings.resampleQuality);
+ (*it)->m_processingBuffers->ForceResampler((*it)->m_forceResampler);
+
+ (*it)->m_processingBuffers->Create(MAX_CACHE_LEVEL*1000, false, m_settings.stereoupmix, m_settings.normalizelevels);
+ }
+ if (m_mode == MODE_TRANSCODE || m_streams.size() > 1)
+ (*it)->m_processingBuffers->FillBuffer();
+
+ // amplification
+ (*it)->m_limiter.SetSamplerate(outputFormat.m_sampleRate);
+ }
+
+ // update buffered time of streams
+ m_stats.AddSamples(0, m_streams);
+
+ // buffers for viz
+ if (!(inputFormat.m_dataFormat == AE_FMT_RAW))
+ {
+ if (initSink && m_vizBuffers)
+ {
+ m_discardBufferPools.push_back(m_vizBuffers);
+ m_vizBuffers = NULL;
+ m_discardBufferPools.push_back(m_vizBuffersInput);
+ m_vizBuffersInput = NULL;
+ }
+ if (!m_vizBuffers && !m_audioCallback.empty())
+ {
+ AEAudioFormat vizFormat = m_internalFormat;
+ vizFormat.m_channelLayout = AE_CH_LAYOUT_2_0;
+ vizFormat.m_dataFormat = AE_FMT_FLOAT;
+ vizFormat.m_sampleRate = 44100;
+ vizFormat.m_frames =
+ m_internalFormat.m_frames *
+ (static_cast<float>(vizFormat.m_sampleRate) / m_internalFormat.m_sampleRate);
+
+ // input buffers
+ m_vizBuffersInput = new CActiveAEBufferPool(m_internalFormat);
+ m_vizBuffersInput->Create(2000 + m_stats.GetMaxDelay() * 1000);
+
+ // resample buffers
+ m_vizBuffers = new CActiveAEBufferPoolResample(m_internalFormat, vizFormat, m_settings.resampleQuality);
+ //! @todo use cache of sync + water level
+ m_vizBuffers->Create(2000 + m_stats.GetMaxDelay() * 1000, false, false);
+ m_vizInitialized = false;
+ }
+ }
+
+ // buffers need to sync
+ m_silenceBuffers = new CActiveAEBufferPool(outputFormat);
+ m_silenceBuffers->Create(500);
+ }
+
+ // resample buffers for sink
+ if (m_sinkBuffers &&
+ (!CompareFormat(m_sinkBuffers->m_format,m_sinkFormat) ||
+ !CompareFormat(m_sinkBuffers->m_inputFormat, sinkInputFormat) ||
+ m_sinkBuffers->m_format.m_frames != m_sinkFormat.m_frames))
+ {
+ m_discardBufferPools.push_back(m_sinkBuffers);
+ m_sinkBuffers = NULL;
+ }
+ if (!m_sinkBuffers)
+ {
+ m_sinkBuffers = new CActiveAEBufferPoolResample(sinkInputFormat, m_sinkFormat, m_settings.resampleQuality);
+ m_sinkBuffers->Create(MAX_WATER_LEVEL*1000, true, false);
+ }
+
+ // reset gui sounds
+ if (!CompareFormat(oldInternalFormat, m_internalFormat))
+ {
+ if (m_settings.guisoundmode == AE_SOUND_ALWAYS ||
+ (m_settings.guisoundmode == AE_SOUND_IDLE && m_streams.empty()) ||
+ m_aeGUISoundForce)
+ {
+ std::vector<CActiveAESound*>::iterator it;
+ for (it = m_sounds.begin(); it != m_sounds.end(); ++it)
+ {
+ (*it)->SetConverted(false);
+ }
+ }
+ m_sounds_playing.clear();
+ }
+
+ ClearDiscardedBuffers();
+ m_extDrain = false;
+}
+
+CActiveAEStream* CActiveAE::CreateStream(MsgStreamNew *streamMsg)
+{
+ // we only can handle a single pass through stream
+ bool hasRawStream = false;
+ bool hasStream = false;
+ std::list<CActiveAEStream*>::iterator it;
+ for(it = m_streams.begin(); it != m_streams.end(); ++it)
+ {
+ if ((*it)->IsDrained())
+ continue;
+ if ((*it)->m_format.m_dataFormat == AE_FMT_RAW)
+ hasRawStream = true;
+ hasStream = true;
+ }
+ if (hasRawStream || (hasStream && (streamMsg->format.m_dataFormat == AE_FMT_RAW)))
+ {
+ return NULL;
+ }
+
+ // create the stream
+ CActiveAEStream *stream;
+ stream = new CActiveAEStream(&streamMsg->format, m_streamIdGen++, this);
+ stream->m_streamPort = new CActiveAEDataProtocol("stream",
+ &stream->m_inMsgEvent, &m_outMsgEvent);
+
+ // create buffer pool
+ stream->m_inputBuffers = NULL; // create in Configure when we know the sink format
+ stream->m_processingBuffers = NULL; // create in Configure when we know the sink format
+ stream->m_fadingSamples = 0;
+ stream->m_started = false;
+ stream->m_resampleMode = 0;
+ stream->m_syncState = CAESyncInfo::AESyncState::SYNC_OFF;
+
+ if (streamMsg->options & AESTREAM_PAUSED)
+ {
+ stream->m_paused = true;
+ stream->m_streamIsBuffering = true;
+ }
+
+ if (streamMsg->options & AESTREAM_FORCE_RESAMPLE)
+ stream->m_forceResampler = true;
+
+ stream->m_pClock = streamMsg->clock;
+
+ m_streams.push_back(stream);
+ m_stats.AddStream(stream->m_id);
+
+ return stream;
+}
+
+void CActiveAE::DiscardStream(CActiveAEStream *stream)
+{
+ std::list<CActiveAEStream*>::iterator it;
+ for (it=m_streams.begin(); it!=m_streams.end(); )
+ {
+ if (stream == (*it))
+ {
+ while (!(*it)->m_processingSamples.empty())
+ {
+ (*it)->m_processingSamples.front()->Return();
+ (*it)->m_processingSamples.pop_front();
+ }
+ if ((*it)->m_inputBuffers)
+ m_discardBufferPools.push_back((*it)->m_inputBuffers);
+ if ((*it)->m_processingBuffers)
+ {
+ (*it)->m_processingBuffers->Flush();
+ m_discardBufferPools.push_back((*it)->m_processingBuffers->GetResampleBuffers());
+ m_discardBufferPools.push_back((*it)->m_processingBuffers->GetAtempoBuffers());
+ }
+ delete (*it)->m_processingBuffers;
+ CLog::Log(LOGDEBUG, "CActiveAE::DiscardStream - audio stream deleted");
+ m_stats.RemoveStream((*it)->m_id);
+ delete (*it)->m_streamPort;
+ delete (*it);
+ it = m_streams.erase(it);
+ }
+ else
+ ++it;
+ }
+
+ ClearDiscardedBuffers();
+}
+
+void CActiveAE::SFlushStream(CActiveAEStream *stream)
+{
+ while (!stream->m_processingSamples.empty())
+ {
+ stream->m_processingSamples.front()->Return();
+ stream->m_processingSamples.pop_front();
+ }
+ stream->m_processingBuffers->Flush();
+ stream->m_streamPort->Purge();
+ stream->m_bufferedTime = 0.0;
+ stream->m_paused = false;
+ stream->m_syncState = CAESyncInfo::AESyncState::SYNC_START;
+ stream->m_syncError.Flush();
+ stream->ResetFreeBuffers();
+
+ // flush the engine if we only have a single stream
+ if (m_streams.size() == 1)
+ {
+ FlushEngine();
+ }
+
+ m_stats.UpdateStream(stream);
+}
+
+void CActiveAE::FlushEngine()
+{
+ if (m_sinkBuffers)
+ m_sinkBuffers->Flush();
+ if (m_vizBuffers)
+ m_vizBuffers->Flush();
+
+ // send message to sink
+ Message *reply;
+ if (m_sink.m_controlPort.SendOutMessageSync(CSinkControlProtocol::FLUSH,
+ &reply, 2000))
+ {
+ bool success = reply->signal == CSinkControlProtocol::ACC;
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - returned error on flush", __FUNCTION__);
+ m_extError = true;
+ }
+ reply->Release();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - failed to flush", __FUNCTION__);
+ m_extError = true;
+ }
+ m_stats.Reset(m_sinkFormat.m_sampleRate, m_mode == MODE_PCM);
+}
+
+void CActiveAE::ClearDiscardedBuffers()
+{
+ auto it = m_discardBufferPools.begin();
+ while (it != m_discardBufferPools.end())
+ {
+ CActiveAEBufferPoolResample *rbuf = dynamic_cast<CActiveAEBufferPoolResample*>(*it);
+ if (rbuf)
+ {
+ rbuf->Flush();
+ }
+ // if all buffers have returned, we can delete the buffer pool
+ if ((*it)->m_allSamples.size() == (*it)->m_freeSamples.size())
+ {
+ delete (*it);
+ CLog::Log(LOGDEBUG, "CActiveAE::ClearDiscardedBuffers - buffer pool deleted");
+ it = m_discardBufferPools.erase(it);
+ }
+ else
+ ++it;
+ }
+}
+
+void CActiveAE::SStopSound(CActiveAESound *sound)
+{
+ std::list<SoundState>::iterator it;
+ for (it=m_sounds_playing.begin(); it!=m_sounds_playing.end(); ++it)
+ {
+ if (it->sound == sound)
+ {
+ if (sound->GetChannel() != AE_CH_NULL)
+ m_aeGUISoundForce = false;
+ m_sounds_playing.erase(it);
+ return;
+ }
+ }
+}
+
+void CActiveAE::DiscardSound(CActiveAESound *sound)
+{
+ SStopSound(sound);
+
+ std::vector<CActiveAESound*>::iterator it;
+ for (it=m_sounds.begin(); it!=m_sounds.end(); ++it)
+ {
+ if ((*it) == sound)
+ {
+ m_sounds.erase(it);
+ delete sound;
+ return;
+ }
+ }
+}
+
+void CActiveAE::ChangeResamplers()
+{
+ std::list<CActiveAEStream*>::iterator it;
+ for(it=m_streams.begin(); it!=m_streams.end(); ++it)
+ {
+ (*it)->m_processingBuffers->ConfigureResampler(m_settings.normalizelevels, m_settings.stereoupmix, m_settings.resampleQuality);
+ }
+}
+
+void CActiveAE::ApplySettingsToFormat(AEAudioFormat& format,
+ const AudioSettings& settings,
+ int* mode)
+{
+ int oldMode = m_mode;
+ if (mode)
+ *mode = MODE_PCM;
+
+ // raw pass through
+ if (format.m_dataFormat == AE_FMT_RAW)
+ {
+ if (mode)
+ *mode = MODE_RAW;
+ }
+ // transcode
+ else if (settings.channels <= AE_CH_LAYOUT_2_0 && // no multichannel pcm
+ settings.passthrough &&
+ settings.ac3passthrough &&
+ settings.ac3transcode &&
+ !m_streams.empty() &&
+ (format.m_channelLayout.Count() > 2 || settings.stereoupmix))
+ {
+ format.m_dataFormat = AE_FMT_RAW;
+ format.m_sampleRate = 48000;
+ format.m_channelLayout = AE_CH_LAYOUT_2_0;
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_AC3;
+ format.m_streamInfo.m_channels = 2;
+ format.m_streamInfo.m_sampleRate = 48000;
+ if (mode)
+ *mode = MODE_TRANSCODE;
+ }
+ else
+ {
+ format.m_dataFormat = AE_IS_PLANAR(format.m_dataFormat) ? AE_FMT_FLOATP : AE_FMT_FLOAT;
+ // consider user channel layout for those cases
+ // 1. input stream is multichannel
+ // 2. stereo upmix is selected
+ // 3. fixed mode
+ if ((format.m_channelLayout.Count() > 2) ||
+ settings.stereoupmix ||
+ (settings.config == AE_CONFIG_FIXED))
+ {
+ CAEChannelInfo stdLayout;
+ switch (settings.channels)
+ {
+ default:
+ case 0: stdLayout = AE_CH_LAYOUT_2_0; break;
+ case 1: stdLayout = AE_CH_LAYOUT_2_0; break;
+ case 2: stdLayout = AE_CH_LAYOUT_2_1; break;
+ case 3: stdLayout = AE_CH_LAYOUT_3_0; break;
+ case 4: stdLayout = AE_CH_LAYOUT_3_1; break;
+ case 5: stdLayout = AE_CH_LAYOUT_4_0; break;
+ case 6: stdLayout = AE_CH_LAYOUT_4_1; break;
+ case 7: stdLayout = AE_CH_LAYOUT_5_0; break;
+ case 8: stdLayout = AE_CH_LAYOUT_5_1; break;
+ case 9: stdLayout = AE_CH_LAYOUT_7_0; break;
+ case 10: stdLayout = AE_CH_LAYOUT_7_1; break;
+ }
+
+ if (m_settings.config == AE_CONFIG_FIXED || (settings.stereoupmix && format.m_channelLayout.Count() <= 2))
+ format.m_channelLayout = stdLayout;
+ else if ((m_extKeepConfig > 0ms) && (settings.config == AE_CONFIG_AUTO) &&
+ (oldMode != MODE_RAW))
+ format.m_channelLayout = m_internalFormat.m_channelLayout;
+ else
+ {
+ if (stdLayout == AE_CH_LAYOUT_5_0 || stdLayout == AE_CH_LAYOUT_5_1)
+ {
+ std::vector<CAEChannelInfo> alts;
+ alts.push_back(stdLayout);
+ stdLayout.ReplaceChannel(AE_CH_BL, AE_CH_SL);
+ stdLayout.ReplaceChannel(AE_CH_BR, AE_CH_SR);
+ alts.push_back(stdLayout);
+ int bestMatch = format.m_channelLayout.BestMatch(alts);
+ stdLayout = alts[bestMatch];
+ }
+ format.m_channelLayout.ResolveChannels(stdLayout);
+ }
+ }
+ // don't change from multi to stereo in AUTO mode
+ else if ((settings.config == AE_CONFIG_AUTO) &&
+ m_stats.GetWaterLevel() > 0 && m_internalFormat.m_channelLayout.Count() > 2)
+ {
+ format.m_channelLayout = m_internalFormat.m_channelLayout;
+ }
+
+ if (m_sink.GetDeviceType(m_settings.device) == AE_DEVTYPE_IEC958)
+ {
+ if (format.m_sampleRate > m_settings.samplerate)
+ {
+ format.m_sampleRate = m_settings.samplerate;
+ CLog::Log(LOGINFO, "CActiveAE::ApplySettings - limit samplerate for SPDIF to {}",
+ format.m_sampleRate);
+ }
+ format.m_channelLayout = AE_CH_LAYOUT_2_0;
+ }
+
+ if (m_settings.config == AE_CONFIG_FIXED)
+ {
+ format.m_sampleRate = m_settings.samplerate;
+ format.m_dataFormat = AE_FMT_FLOAT;
+ CLog::Log(LOGINFO, "CActiveAE::ApplySettings - Forcing samplerate to {}",
+ format.m_sampleRate);
+ }
+
+ // sinks may not support mono
+ if (format.m_channelLayout.Count() == 1)
+ {
+ format.m_channelLayout = AE_CH_LAYOUT_2_0;
+ }
+ }
+}
+
+bool CActiveAE::NeedReconfigureBuffers()
+{
+ AEAudioFormat newFormat = GetInputFormat();
+ ApplySettingsToFormat(newFormat, m_settings);
+
+ return newFormat.m_dataFormat != m_sinkRequestFormat.m_dataFormat ||
+ newFormat.m_channelLayout != m_sinkRequestFormat.m_channelLayout ||
+ newFormat.m_sampleRate != m_sinkRequestFormat.m_sampleRate;
+}
+
+bool CActiveAE::NeedReconfigureSink()
+{
+ AEAudioFormat newFormat = GetInputFormat();
+ ApplySettingsToFormat(newFormat, m_settings);
+
+ std::string device = (newFormat.m_dataFormat == AE_FMT_RAW) ? m_settings.passthroughdevice : m_settings.device;
+ std::string driver;
+ CAESinkFactory::ParseDevice(device, driver);
+
+ return !CompareFormat(newFormat, m_sinkFormat) ||
+ m_currDevice.compare(device) != 0 ||
+ m_settings.driver.compare(driver) != 0;
+}
+
+bool CActiveAE::InitSink()
+{
+ SinkConfig config;
+ config.format = m_sinkRequestFormat;
+ config.stats = &m_stats;
+ config.device = (m_sinkRequestFormat.m_dataFormat == AE_FMT_RAW) ? &m_settings.passthroughdevice :
+ &m_settings.device;
+
+ // send message to sink
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::SETNOISETYPE, &m_settings.streamNoise, sizeof(bool));
+ m_sink.m_controlPort.SendOutMessage(CSinkControlProtocol::SETSILENCETIMEOUT,
+ &m_settings.silenceTimeoutMinutes, sizeof(int));
+
+ Message *reply;
+ if (m_sink.m_controlPort.SendOutMessageSync(CSinkControlProtocol::CONFIGURE,
+ &reply,
+ 5000,
+ &config, sizeof(config)))
+ {
+ bool success = reply->signal == CSinkControlProtocol::ACC;
+ if (!success)
+ {
+ reply->Release();
+ CLog::Log(LOGERROR, "ActiveAE::{} - returned error", __FUNCTION__);
+ m_extError = true;
+ return false;
+ }
+ SinkReply *data;
+ data = reinterpret_cast<SinkReply*>(reply->data);
+ if (data)
+ {
+ m_sinkFormat = data->format;
+ m_sinkHasVolume = data->hasVolume;
+ m_stats.SetSinkCacheTotal(data->cacheTotal);
+ m_stats.SetSinkLatency(data->latency);
+ m_stats.SetCurrentSinkFormat(m_sinkFormat);
+ m_stats.SetSinkNeedIec(m_sink.NeedIecPack());
+ }
+ reply->Release();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - failed to init", __FUNCTION__);
+ m_stats.SetSinkCacheTotal(0);
+ m_stats.SetSinkLatency(0);
+ AEAudioFormat invalidFormat;
+ invalidFormat.m_dataFormat = AE_FMT_INVALID;
+ m_stats.SetCurrentSinkFormat(invalidFormat);
+ m_extError = true;
+ return false;
+ }
+
+ m_inMsgEvent.Reset();
+ return true;
+}
+
+void CActiveAE::DrainSink()
+{
+ // send message to sink
+ Message *reply;
+ if (m_sink.m_dataPort.SendOutMessageSync(CSinkDataProtocol::DRAIN,
+ &reply,
+ 2000))
+ {
+ bool success = reply->signal == CSinkDataProtocol::ACC;
+ if (!success)
+ {
+ reply->Release();
+ CLog::Log(LOGERROR, "ActiveAE::{} - returned error on drain", __FUNCTION__);
+ m_extError = true;
+ return;
+ }
+ reply->Release();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - failed to drain", __FUNCTION__);
+ m_extError = true;
+ return;
+ }
+}
+
+void CActiveAE::UnconfigureSink()
+{
+ // send message to sink
+ Message *reply;
+ if (m_sink.m_controlPort.SendOutMessageSync(CSinkControlProtocol::UNCONFIGURE,
+ &reply,
+ 2000))
+ {
+ bool success = reply->signal == CSinkControlProtocol::ACC;
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - returned error", __FUNCTION__);
+ m_extError = true;
+ }
+ reply->Release();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - failed to unconfigure", __FUNCTION__);
+ m_extError = true;
+ }
+
+ // make sure we open sink on next configure
+ m_currDevice = "";
+
+ m_inMsgEvent.Reset();
+}
+
+
+bool CActiveAE::RunStages()
+{
+ bool busy = false;
+
+ // serve input streams
+ std::list<CActiveAEStream*>::iterator it;
+ for (it = m_streams.begin(); it != m_streams.end(); ++it)
+ {
+ if ((*it)->m_processingBuffers && !(*it)->m_paused)
+ busy = (*it)->m_processingBuffers->ProcessBuffers();
+
+ if ((*it)->m_streamIsBuffering &&
+ (*it)->m_processingBuffers &&
+ ((*it)->m_processingBuffers->HasInputLevel(50)))
+ {
+ std::unique_lock<CCriticalSection> lock((*it)->m_streamLock);
+ (*it)->m_streamIsBuffering = false;
+ }
+
+ // provide buffers to stream
+ float time = m_stats.GetCacheTime((*it));
+ CSampleBuffer *buffer;
+ if (!(*it)->m_drain)
+ {
+ float buftime = (float)(*it)->m_inputBuffers->m_format.m_frames / (*it)->m_inputBuffers->m_format.m_sampleRate;
+ if ((*it)->m_inputBuffers->m_format.m_dataFormat == AE_FMT_RAW)
+ buftime = (*it)->m_inputBuffers->m_format.m_streamInfo.GetDuration() / 1000;
+ while ((time < MAX_CACHE_LEVEL || (*it)->m_streamIsBuffering) &&
+ !(*it)->m_inputBuffers->m_freeSamples.empty())
+ {
+ buffer = (*it)->m_inputBuffers->GetFreeBuffer();
+ (*it)->m_processingSamples.push_back(buffer);
+ (*it)->m_streamPort->SendInMessage(CActiveAEDataProtocol::STREAMBUFFER, &buffer, sizeof(CSampleBuffer*));
+ (*it)->IncFreeBuffers();
+ time += buftime;
+ }
+ }
+ else
+ {
+ if ((*it)->m_processingBuffers->IsDrained() &&
+ (*it)->m_processingSamples.empty())
+ {
+ (*it)->m_streamPort->SendInMessage(CActiveAEDataProtocol::STREAMDRAINED);
+ (*it)->m_drain = false;
+ (*it)->m_processingBuffers->SetDrain(false);
+ (*it)->m_started = false;
+
+ // set variables being polled via stream interface
+ std::unique_lock<CCriticalSection> lock((*it)->m_streamLock);
+ if ((*it)->m_streamSlave)
+ {
+ CActiveAEStream *slave = (CActiveAEStream*)((*it)->m_streamSlave);
+ slave->m_paused = false;
+
+ //! @todo find better solution for this gapless bites audiophile
+ if (m_settings.config == AE_CONFIG_MATCH)
+ Configure(&slave->m_format);
+
+ (*it)->m_streamSlave = NULL;
+ }
+ (*it)->m_streamDrained = true;
+ (*it)->m_streamDraining = false;
+ (*it)->m_streamFading = false;
+ }
+ }
+ }
+
+ // TrueHD is very jumpy, meaning the frames don't come in equidistantly. They are only smoothed
+ // at the end when the IEC packing happens. Therefore adjust earlier.
+ const bool ignoreWL =
+ (m_mode == MODE_RAW && m_sinkFormat.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD);
+
+ if ((m_stats.GetWaterLevel() < (MAX_WATER_LEVEL + 0.0001f) || ignoreWL) &&
+ (m_mode != MODE_TRANSCODE || (m_encoderBuffers && !m_encoderBuffers->m_freeSamples.empty())))
+ {
+ // calculate sync error
+ for (it = m_streams.begin(); it != m_streams.end(); ++it)
+ {
+ if ((*it)->m_paused || !(*it)->m_started || !(*it)->m_processingBuffers || !(*it)->m_pClock)
+ continue;
+
+ if ((*it)->m_processingBuffers->m_outputSamples.empty())
+ continue;
+
+ CSampleBuffer *buf = (*it)->m_processingBuffers->m_outputSamples.front();
+ if (buf->timestamp)
+ {
+ AEDelayStatus status;
+ m_stats.GetDelay(status);
+ double pts = buf->timestamp - (buf->pkt_start_offset * 1000 / buf->pkt->config.sample_rate);
+ double delay = status.GetDelay() * 1000;
+ double playingPts = pts - delay;
+ double maxError = ((*it)->m_syncState == CAESyncInfo::SYNC_INSYNC) ? 1000 : 5000;
+ double error = playingPts - (*it)->m_pClock->GetClock();
+ if (error > maxError)
+ {
+ CLog::Log(LOGWARNING, "ActiveAE - large audio sync error: {:f}", error);
+ error = maxError;
+ }
+ else if (error < -maxError)
+ {
+ CLog::Log(LOGWARNING, "ActiveAE - large audio sync error: {:f}", error);
+ error = -maxError;
+ }
+ (*it)->m_syncError.Add(error);
+ }
+ }
+
+ // mix streams and sounds sounds
+ if (m_mode != MODE_RAW)
+ {
+ CSampleBuffer *out = NULL;
+ if (!m_sounds_playing.empty() && m_streams.empty())
+ {
+ if (m_silenceBuffers && !m_silenceBuffers->m_freeSamples.empty())
+ {
+ out = m_silenceBuffers->GetFreeBuffer();
+ for (int i=0; i<out->pkt->planes; i++)
+ {
+ memset(out->pkt->data[i], 0, out->pkt->linesize);
+ }
+ out->pkt->nb_samples = out->pkt->max_nb_samples;
+ }
+ }
+
+ // mix streams
+ std::list<CActiveAEStream*>::iterator it;
+
+ // if we deal with more than a single stream, all streams
+ // must provide samples for mixing
+ bool allStreamsReady = true;
+ for (it = m_streams.begin(); it != m_streams.end(); ++it)
+ {
+ if ((*it)->m_paused || !(*it)->m_started || !(*it)->m_processingBuffers)
+ continue;
+
+ if ((*it)->m_processingBuffers->m_outputSamples.empty())
+ allStreamsReady = false;
+ }
+
+ bool needClamp = false;
+ for (it = m_streams.begin(); it != m_streams.end() && allStreamsReady; ++it)
+ {
+ if ((*it)->m_paused || !(*it)->m_processingBuffers)
+ continue;
+
+ if (!(*it)->m_processingBuffers->m_outputSamples.empty())
+ {
+ CSampleBuffer *tmp = SyncStream(*it);
+ m_stats.UpdateStream(*it);
+ if (tmp)
+ {
+ if (!out)
+ out = tmp;
+ continue;
+ }
+
+ (*it)->m_started = true;
+
+ if (!out)
+ {
+ out = (*it)->m_processingBuffers->m_outputSamples.front();
+ (*it)->m_processingBuffers->m_outputSamples.pop_front();
+
+ int nb_floats = out->pkt->nb_samples * out->pkt->config.channels / out->pkt->planes;
+ int nb_loops = 1;
+ float fadingStep = 0.0f;
+
+ // fading
+ if ((*it)->m_fadingSamples == -1)
+ {
+ (*it)->m_fadingSamples = m_internalFormat.m_sampleRate * (float)(*it)->m_fadingTime / 1000.0f;
+ if ((*it)->m_fadingSamples > 0)
+ (*it)->m_volume = (*it)->m_fadingBase;
+ else
+ {
+ (*it)->m_volume = (*it)->m_fadingTarget;
+ std::unique_lock<CCriticalSection> lock((*it)->m_streamLock);
+ (*it)->m_streamFading = false;
+ }
+ }
+ if ((*it)->m_fadingSamples > 0)
+ {
+ nb_floats = out->pkt->config.channels / out->pkt->planes;
+ nb_loops = out->pkt->nb_samples;
+ float delta = (*it)->m_fadingTarget - (*it)->m_fadingBase;
+ int samples = m_internalFormat.m_sampleRate * (float)(*it)->m_fadingTime / 1000.0f;
+ fadingStep = delta / samples;
+ }
+
+ // for stream amplification,
+ // turned off downmix normalization,
+ // or if sink format is float (in order to prevent from clipping)
+ // we need to run on a per sample basis
+ if ((*it)->m_amplify != 1.0f || !(*it)->m_processingBuffers->DoesNormalize() ||
+ (m_sinkFormat.m_dataFormat == AE_FMT_FLOAT))
+ {
+ nb_floats = out->pkt->config.channels / out->pkt->planes;
+ nb_loops = out->pkt->nb_samples;
+ }
+
+ for(int i=0; i<nb_loops; i++)
+ {
+ if ((*it)->m_fadingSamples > 0)
+ {
+ (*it)->m_volume += fadingStep;
+ (*it)->m_fadingSamples--;
+
+ if ((*it)->m_fadingSamples == 0)
+ {
+ // set variables being polled via stream interface
+ std::unique_lock<CCriticalSection> lock((*it)->m_streamLock);
+ (*it)->m_streamFading = false;
+ }
+ }
+
+ // volume for stream
+ float volume = (*it)->m_volume * (*it)->m_rgain;
+ if(nb_loops > 1)
+ volume *= (*it)->m_limiter.Run((float**)out->pkt->data, out->pkt->config.channels, i*nb_floats, out->pkt->planes > 1);
+
+ for(int j=0; j<out->pkt->planes; j++)
+ {
+#if defined(HAVE_SSE) && defined(__SSE__)
+ CAEUtil::SSEMulArray((float*)out->pkt->data[j]+i*nb_floats, volume, nb_floats);
+#else
+ float* fbuffer = (float*) out->pkt->data[j]+i*nb_floats;
+ for (int k = 0; k < nb_floats; ++k)
+ {
+ fbuffer[k] *= volume;
+ }
+#endif
+ }
+ }
+ }
+ else
+ {
+ CSampleBuffer *mix = NULL;
+ mix = (*it)->m_processingBuffers->m_outputSamples.front();
+ (*it)->m_processingBuffers->m_outputSamples.pop_front();
+
+ int nb_floats = mix->pkt->nb_samples * mix->pkt->config.channels / mix->pkt->planes;
+ int nb_loops = 1;
+ float fadingStep = 0.0f;
+
+ // fading
+ if ((*it)->m_fadingSamples == -1)
+ {
+ (*it)->m_fadingSamples = m_internalFormat.m_sampleRate * (float)(*it)->m_fadingTime / 1000.0f;
+ (*it)->m_volume = (*it)->m_fadingBase;
+ }
+ if ((*it)->m_fadingSamples > 0)
+ {
+ nb_floats = mix->pkt->config.channels / mix->pkt->planes;
+ nb_loops = mix->pkt->nb_samples;
+ float delta = (*it)->m_fadingTarget - (*it)->m_fadingBase;
+ int samples = m_internalFormat.m_sampleRate * (float)(*it)->m_fadingTime / 1000.0f;
+ fadingStep = delta / samples;
+ }
+
+ // for streams amplification of turned off downmix normalization
+ // we need to run on a per sample basis
+ if ((*it)->m_amplify != 1.0f || !(*it)->m_processingBuffers->DoesNormalize())
+ {
+ nb_floats = out->pkt->config.channels / out->pkt->planes;
+ nb_loops = out->pkt->nb_samples;
+ }
+
+ for(int i=0; i<nb_loops; i++)
+ {
+ if ((*it)->m_fadingSamples > 0)
+ {
+ (*it)->m_volume += fadingStep;
+ (*it)->m_fadingSamples--;
+
+ if ((*it)->m_fadingSamples == 0)
+ {
+ // set variables being polled via stream interface
+ std::unique_lock<CCriticalSection> lock((*it)->m_streamLock);
+ (*it)->m_streamFading = false;
+ }
+ }
+
+ // volume for stream
+ float volume = (*it)->m_volume * (*it)->m_rgain;
+ if(nb_loops > 1)
+ volume *= (*it)->m_limiter.Run((float**)mix->pkt->data, mix->pkt->config.channels, i*nb_floats, mix->pkt->planes > 1);
+
+ for(int j=0; j<out->pkt->planes && j<mix->pkt->planes; j++)
+ {
+ float *dst = (float*)out->pkt->data[j]+i*nb_floats;
+ float *src = (float*)mix->pkt->data[j]+i*nb_floats;
+#if defined(HAVE_SSE) && defined(__SSE__)
+ CAEUtil::SSEMulAddArray(dst, src, volume, nb_floats);
+ for (int k = 0; k < nb_floats; ++k)
+ {
+ if (fabs(dst[k]) > 1.0f)
+ {
+ needClamp = true;
+ break;
+ }
+ }
+#else
+ for (int k = 0; k < nb_floats; ++k)
+ {
+ dst[k] += src[k] * volume;
+ if (fabs(dst[k]) > 1.0f)
+ needClamp = true;
+ }
+#endif
+ }
+ }
+ mix->Return();
+ }
+ busy = true;
+ }
+ }// for
+
+ // finally clamp samples
+ if (out && needClamp)
+ {
+ int nb_floats = out->pkt->nb_samples * out->pkt->config.channels / out->pkt->planes;
+ for (int i=0; i<out->pkt->planes; i++)
+ {
+ CAEUtil::ClampArray((float*)out->pkt->data[i], nb_floats);
+ }
+ }
+
+ // process output buffer, gui sounds, encode, viz
+ if (out)
+ {
+ // viz
+ {
+ std::unique_lock<CCriticalSection> lock(m_vizLock);
+ if (!m_audioCallback.empty() && !m_streams.empty())
+ {
+ if (!m_vizInitialized || !m_vizBuffers)
+ {
+ Configure();
+ for (auto& it : m_audioCallback)
+ it->OnInitialize(2, m_vizBuffers->m_format.m_sampleRate, 32);
+ m_vizInitialized = true;
+ }
+
+ if (!m_vizBuffersInput->m_freeSamples.empty())
+ {
+ // copy the samples into the viz input buffer
+ CSampleBuffer *viz = m_vizBuffersInput->GetFreeBuffer();
+ int samples = out->pkt->nb_samples;
+ int bytes = samples * out->pkt->config.channels / out->pkt->planes * out->pkt->bytes_per_sample;
+ for(int i= 0; i < out->pkt->planes; i++)
+ {
+ memcpy(viz->pkt->data[i], out->pkt->data[i], bytes);
+ }
+ viz->pkt->nb_samples = samples;
+ m_vizBuffers->m_inputSamples.push_back(viz);
+ }
+ else
+ CLog::Log(LOGWARNING, "ActiveAE::{} - viz ran out of free buffers", __FUNCTION__);
+ AEDelayStatus status;
+ m_stats.GetDelay(status);
+ int64_t now = std::chrono::steady_clock::now().time_since_epoch().count();
+ int64_t timestamp = now + status.GetDelay() * 1000;
+ busy |= m_vizBuffers->ResampleBuffers(timestamp);
+ while(!m_vizBuffers->m_outputSamples.empty())
+ {
+ CSampleBuffer *buf = m_vizBuffers->m_outputSamples.front();
+ if ((now - buf->timestamp) < 0)
+ break;
+ else
+ {
+ unsigned int samples = static_cast<unsigned int>(buf->pkt->nb_samples) *
+ buf->pkt->config.channels / buf->pkt->planes;
+ for (auto& it : m_audioCallback)
+ it->OnAudioData((float*)(buf->pkt->data[0]), samples);
+ buf->Return();
+ m_vizBuffers->m_outputSamples.pop_front();
+ }
+ }
+ }
+ else if (m_vizBuffers)
+ m_vizBuffers->Flush();
+ }
+
+ // mix gui sounds
+ MixSounds(*(out->pkt));
+ if (!m_sinkHasVolume || m_muted)
+ Deamplify(*(out->pkt));
+
+ if (m_mode == MODE_TRANSCODE && m_encoder)
+ {
+ CSampleBuffer *buf = nullptr;
+ if (out->pkt->nb_samples)
+ {
+ buf = m_encoderBuffers->GetFreeBuffer();
+ buf->pkt->nb_samples = m_encoder->Encode(out->pkt->data[0], out->pkt->planes*out->pkt->linesize,
+ buf->pkt->data[0], buf->pkt->planes*buf->pkt->linesize);
+
+ // set pts of last sample
+ buf->pkt_start_offset = buf->pkt->nb_samples;
+ buf->timestamp = out->timestamp;
+ }
+
+ out->Return();
+ out = buf;
+ }
+ busy = true;
+ }
+
+ // update stats
+ if(out)
+ {
+ int samples = (m_mode == MODE_TRANSCODE) ? 1 : out->pkt->nb_samples;
+ m_stats.AddSamples(samples, m_streams);
+ m_sinkBuffers->m_inputSamples.push_back(out);
+ }
+ }
+ // pass through
+ else
+ {
+ std::list<CActiveAEStream*>::iterator it;
+ CSampleBuffer *buffer;
+ for (it = m_streams.begin(); it != m_streams.end(); ++it)
+ {
+ if (!(*it)->m_processingBuffers->m_outputSamples.empty() && !(*it)->m_paused)
+ {
+ (*it)->m_started = true;
+ buffer = SyncStream(*it);
+ m_stats.UpdateStream(*it);
+ if (!buffer)
+ {
+ buffer = (*it)->m_processingBuffers->m_outputSamples.front();
+ (*it)->m_processingBuffers->m_outputSamples.pop_front();
+ }
+ m_stats.AddSamples(1, m_streams);
+ m_sinkBuffers->m_inputSamples.push_back(buffer);
+ }
+ }
+ }
+ }
+
+ // serve sink buffers
+ busy |= m_sinkBuffers->ResampleBuffers();
+ while(!m_sinkBuffers->m_outputSamples.empty())
+ {
+ CSampleBuffer *out = NULL;
+ out = m_sinkBuffers->m_outputSamples.front();
+ m_sinkBuffers->m_outputSamples.pop_front();
+ m_sink.m_dataPort.SendOutMessage(CSinkDataProtocol::SAMPLE,
+ &out, sizeof(CSampleBuffer*));
+ busy = true;
+ }
+
+ return busy;
+}
+
+bool CActiveAE::HasWork()
+{
+ if (!m_sounds_playing.empty())
+ return true;
+ if (!m_sinkBuffers->m_inputSamples.empty())
+ return true;
+ if (!m_sinkBuffers->m_outputSamples.empty())
+ return true;
+
+ std::list<CActiveAEStream*>::iterator it;
+ for (it = m_streams.begin(); it != m_streams.end(); ++it)
+ {
+ if (!(*it)->m_processingBuffers->HasWork())
+ return true;
+ if (!(*it)->m_processingSamples.empty())
+ return true;
+ }
+
+ return false;
+}
+
+CSampleBuffer* CActiveAE::SyncStream(CActiveAEStream *stream)
+{
+ CSampleBuffer *ret = NULL;
+
+ if (!stream->m_pClock)
+ return ret;
+
+ if (stream->m_syncState == CAESyncInfo::AESyncState::SYNC_START)
+ {
+ stream->m_syncState = CAESyncInfo::AESyncState::SYNC_MUTE;
+ stream->m_syncError.Flush(100ms);
+ stream->m_processingBuffers->SetRR(1.0, m_settings.atempoThreshold);
+ stream->m_resampleIntegral = 0;
+ CLog::Log(LOGDEBUG,"ActiveAE - start sync of audio stream");
+ }
+
+ double error;
+ double threshold = 100;
+ if (stream->m_resampleMode)
+ {
+ threshold *= 2;
+ if (stream->m_pClock)
+ {
+ double clockspeed = stream->m_pClock->GetClockSpeed();
+ if (clockspeed >= 1.05 || clockspeed <= 0.95)
+ threshold *= 5;
+ }
+ }
+
+ std::chrono::milliseconds timeout = (stream->m_syncState != CAESyncInfo::AESyncState::SYNC_INSYNC)
+ ? 100ms
+ : stream->GetErrorInterval();
+ bool newerror = stream->m_syncError.Get(error, timeout);
+
+ if (newerror && fabs(error) > threshold && stream->m_syncState == CAESyncInfo::AESyncState::SYNC_INSYNC)
+ {
+ stream->m_syncState = CAESyncInfo::AESyncState::SYNC_ADJUST;
+ stream->m_processingBuffers->SetRR(1.0, m_settings.atempoThreshold);
+ stream->m_resampleIntegral = 0;
+ stream->m_lastSyncError = error;
+ CLog::Log(LOGDEBUG, "ActiveAE::SyncStream - average error {:f} above threshold of {:f}", error,
+ threshold);
+ }
+ else if (newerror && stream->m_syncState == CAESyncInfo::AESyncState::SYNC_MUTE)
+ {
+ stream->m_syncState = CAESyncInfo::AESyncState::SYNC_ADJUST;
+ stream->m_lastSyncError = error;
+ CLog::Log(LOGDEBUG, "ActiveAE::SyncStream - average error of {:f}, start adjusting", error);
+ }
+
+ if (stream->m_syncState == CAESyncInfo::AESyncState::SYNC_MUTE)
+ {
+ CSampleBuffer *buf = stream->m_processingBuffers->m_outputSamples.front();
+ if (m_mode == MODE_RAW)
+ {
+ buf->pkt->nb_samples = 0;
+ buf->pkt->pause_burst_ms = stream->m_processingBuffers->m_inputFormat.m_streamInfo.GetDuration();
+ }
+ else
+ {
+ for(int i=0; i<buf->pkt->planes; i++)
+ {
+ memset(buf->pkt->data[i], 0, buf->pkt->linesize);
+ }
+ }
+ }
+ else if (stream->m_syncState == CAESyncInfo::AESyncState::SYNC_ADJUST)
+ {
+ if (error > 0)
+ {
+ ret = m_silenceBuffers->GetFreeBuffer();
+ if (ret)
+ {
+ ret->pkt->nb_samples = 0;
+ ret->pkt->pause_burst_ms = 0;
+ int framesToDelay = error / 1000 * ret->pkt->config.sample_rate;
+ if (framesToDelay > ret->pkt->max_nb_samples)
+ framesToDelay = ret->pkt->max_nb_samples;
+ if (m_mode == MODE_TRANSCODE)
+ {
+ if (framesToDelay > (int) (m_encoderFormat.m_frames / 2))
+ framesToDelay = m_encoderFormat.m_frames;
+ else
+ framesToDelay = 0;
+ }
+ ret->pkt->nb_samples = framesToDelay;
+ if (m_mode == MODE_RAW)
+ {
+ ret->pkt->nb_samples = 0;
+ ret->pkt->pause_burst_ms = error;
+ if (error > stream->m_format.m_streamInfo.GetDuration())
+ ret->pkt->pause_burst_ms = stream->m_format.m_streamInfo.GetDuration();
+
+ stream->m_syncError.Correction(-ret->pkt->pause_burst_ms);
+ error -= ret->pkt->pause_burst_ms;
+ }
+ else
+ {
+ stream->m_syncError.Correction(-framesToDelay*1000/ret->pkt->config.sample_rate);
+ error -= framesToDelay*1000/ret->pkt->config.sample_rate;
+ for(int i=0; i<ret->pkt->planes; i++)
+ {
+ memset(ret->pkt->data[i], 0, ret->pkt->linesize);
+ }
+ }
+
+ if ((ret->pkt->nb_samples == 0) && (ret->pkt->pause_burst_ms == 0))
+ {
+ ret->Return();
+ ret = nullptr;
+ }
+ }
+ }
+ else
+ {
+ CSampleBuffer *buf = stream->m_processingBuffers->m_outputSamples.front();
+ int framesToSkip = -error / 1000 * buf->pkt->config.sample_rate;
+ if (framesToSkip > buf->pkt->nb_samples)
+ framesToSkip = buf->pkt->nb_samples;
+ if (m_mode == MODE_TRANSCODE)
+ {
+ if (framesToSkip > (int) (m_encoderFormat.m_frames / 2))
+ framesToSkip = buf->pkt->nb_samples;
+ else
+ framesToSkip = 0;
+ }
+ if (m_mode == MODE_RAW)
+ {
+ if (-error > stream->m_format.m_streamInfo.GetDuration() / 2)
+ {
+ stream->m_syncError.Correction(stream->m_format.m_streamInfo.GetDuration());
+ error += stream->m_format.m_streamInfo.GetDuration();
+ buf->pkt->nb_samples = 0;
+ }
+ }
+ else
+ {
+ int bytesToSkip = framesToSkip * buf->pkt->bytes_per_sample *
+ buf->pkt->config.channels / buf->pkt->planes;
+ for (int i=0; i<buf->pkt->planes; i++)
+ {
+ memmove(buf->pkt->data[i], buf->pkt->data[i]+bytesToSkip, buf->pkt->linesize - bytesToSkip);
+ }
+ buf->pkt->nb_samples -= framesToSkip;
+ stream->m_syncError.Correction((double)framesToSkip * 1000 / buf->pkt->config.sample_rate);
+ error += (double)framesToSkip * 1000 / buf->pkt->config.sample_rate;
+ }
+ }
+
+ if (fabs(error) < 30)
+ {
+ if (stream->m_lastSyncError > threshold * 2)
+ {
+ stream->m_syncState = CAESyncInfo::AESyncState::SYNC_MUTE;
+ stream->m_syncError.Flush(100ms);
+ CLog::Log(LOGDEBUG, "ActiveAE::SyncStream - average error {:f}, last average error: {:f}",
+ error, stream->m_lastSyncError);
+ stream->m_lastSyncError = error;
+ }
+ else
+ {
+ stream->m_syncState = CAESyncInfo::AESyncState::SYNC_INSYNC;
+ stream->m_syncError.Flush(1000ms);
+ stream->m_resampleIntegral = 0;
+ stream->m_processingBuffers->SetRR(1.0, m_settings.atempoThreshold);
+ CLog::Log(LOGDEBUG, "ActiveAE::SyncStream - average error {:f} below threshold of {:f}",
+ error, 30.0);
+ }
+ }
+
+ return ret;
+ }
+
+ if (!newerror || stream->m_syncState != CAESyncInfo::AESyncState::SYNC_INSYNC)
+ return ret;
+
+ if (stream->m_resampleMode)
+ {
+ if (stream->m_processingBuffers)
+ {
+ stream->m_processingBuffers->SetRR(stream->CalcResampleRatio(error), m_settings.atempoThreshold);
+ }
+ }
+ else if (stream->m_processingBuffers)
+ {
+ stream->m_processingBuffers->SetRR(1.0, m_settings.atempoThreshold);
+ }
+
+ stream->m_syncError.SetErrorInterval(stream->GetErrorInterval());
+
+ return ret;
+}
+
+void CActiveAE::MixSounds(CSoundPacket &dstSample)
+{
+ if (m_sounds_playing.empty())
+ return;
+
+ float volume;
+ float *out;
+ float *sample_buffer;
+ int max_samples = dstSample.nb_samples;
+
+ std::list<SoundState>::iterator it;
+ for (it = m_sounds_playing.begin(); it != m_sounds_playing.end(); )
+ {
+ if (!it->sound->IsConverted())
+ ResampleSound(it->sound);
+ int available_samples = it->sound->GetSound(false)->nb_samples - it->samples_played;
+ int mix_samples = std::min(max_samples, available_samples);
+ int start = it->samples_played *
+ av_get_bytes_per_sample(it->sound->GetSound(false)->config.fmt) *
+ it->sound->GetSound(false)->config.channels /
+ it->sound->GetSound(false)->planes;
+
+ for(int j=0; j<dstSample.planes; j++)
+ {
+ volume = it->sound->GetVolume();
+ out = (float*)dstSample.data[j];
+ sample_buffer = (float*)(it->sound->GetSound(false)->data[j]+start);
+ int nb_floats = mix_samples * dstSample.config.channels / dstSample.planes;
+#if defined(HAVE_SSE) && defined(__SSE__)
+ CAEUtil::SSEMulAddArray(out, sample_buffer, volume, nb_floats);
+#else
+ for (int k = 0; k < nb_floats; ++k)
+ *out++ += *sample_buffer++ * volume;
+#endif
+ }
+
+ it->samples_played += mix_samples;
+
+ // no more frames, so remove it from the list
+ if (it->samples_played >= it->sound->GetSound(false)->nb_samples)
+ {
+ it = m_sounds_playing.erase(it);
+ continue;
+ }
+ ++it;
+ }
+}
+
+void CActiveAE::Deamplify(CSoundPacket &dstSample)
+{
+ if (m_volumeScaled < 1.0f || m_muted)
+ {
+ int nb_floats = dstSample.nb_samples * dstSample.config.channels / dstSample.planes;
+ float volume = m_muted ? 0.0f : m_volumeScaled;
+
+ for(int j=0; j<dstSample.planes; j++)
+ {
+ float* buffer = reinterpret_cast<float*>(dstSample.data[j]);
+#if defined(HAVE_SSE) && defined(__SSE__)
+ CAEUtil::SSEMulArray(buffer, volume, nb_floats);
+#else
+ float *fbuffer = buffer;
+ for (int i = 0; i < nb_floats; i++)
+ *fbuffer++ *= volume;
+#endif
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Configuration
+//-----------------------------------------------------------------------------
+
+void CActiveAE::LoadSettings()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ m_settings.device = settings->GetString(CSettings::SETTING_AUDIOOUTPUT_AUDIODEVICE);
+ m_settings.passthroughdevice = settings->GetString(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGHDEVICE);
+
+ m_settings.config = settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CONFIG);
+ m_settings.channels = (m_sink.GetDeviceType(m_settings.device) == AE_DEVTYPE_IEC958) ? AE_CH_LAYOUT_2_0 : settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CHANNELS);
+ m_settings.samplerate = settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_SAMPLERATE);
+
+ m_settings.stereoupmix = IsSettingVisible(CSettings::SETTING_AUDIOOUTPUT_STEREOUPMIX) ? settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_STEREOUPMIX) : false;
+ m_settings.normalizelevels = !settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_MAINTAINORIGINALVOLUME);
+ m_settings.guisoundmode = settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_GUISOUNDMODE);
+
+ m_settings.passthrough = m_settings.config == AE_CONFIG_FIXED ? false : settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH);
+ if (!m_sink.HasPassthroughDevice())
+ m_settings.passthrough = false;
+ m_settings.ac3passthrough = settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_AC3PASSTHROUGH);
+ m_settings.ac3transcode = settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_AC3TRANSCODE);
+ m_settings.eac3passthrough = settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_EAC3PASSTHROUGH);
+ m_settings.truehdpassthrough = settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_TRUEHDPASSTHROUGH);
+ m_settings.dtspassthrough = settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_DTSPASSTHROUGH);
+ m_settings.dtshdpassthrough = settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_DTSHDPASSTHROUGH);
+ m_settings.usesdtscorefallback =
+ settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_DTSHDCOREFALLBACK);
+
+ m_settings.resampleQuality = static_cast<AEQuality>(settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_PROCESSQUALITY));
+ m_settings.atempoThreshold = settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_ATEMPOTHRESHOLD) / 100.0;
+ m_settings.streamNoise = settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_STREAMNOISE);
+ m_settings.silenceTimeoutMinutes = settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_STREAMSILENCE);
+}
+
+void CActiveAE::Start()
+{
+ Create();
+ Message *reply;
+ if (m_controlPort.SendOutMessageSync(CActiveAEControlProtocol::INIT,
+ &reply,
+ 10000))
+ {
+ bool success = reply->signal == CActiveAEControlProtocol::ACC;
+ reply->Release();
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - returned error", __FUNCTION__);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - failed to init", __FUNCTION__);
+ }
+
+ m_inMsgEvent.Reset();
+}
+
+void CActiveAE::EnumerateOutputDevices(AEDeviceList &devices, bool passthrough)
+{
+ m_sink.EnumerateOutputDevices(devices, passthrough);
+}
+
+void CActiveAE::OnSettingsChange()
+{
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::RECONFIGURE);
+}
+
+bool CActiveAE::SupportsRaw(AEAudioFormat &format)
+{
+ // check if passthrough is enabled
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH))
+ return false;
+
+ // fixed config disabled passthrough
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_AUDIOOUTPUT_CONFIG) == AE_CONFIG_FIXED)
+ return false;
+
+ // check if the format is enabled in settings
+ if (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_AC3 && !m_settings.ac3passthrough)
+ return false;
+ if (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTS_512 && !m_settings.dtspassthrough)
+ return false;
+ if (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTS_1024 && !m_settings.dtspassthrough)
+ return false;
+ if (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTS_2048 && !m_settings.dtspassthrough)
+ return false;
+ if (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTSHD_CORE && !m_settings.dtspassthrough)
+ return false;
+ if (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_EAC3 && !m_settings.eac3passthrough)
+ return false;
+ if (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD && !m_settings.truehdpassthrough)
+ return false;
+ if (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTSHD && !m_settings.dtshdpassthrough)
+ return false;
+ if (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTSHD_MA && !m_settings.dtshdpassthrough)
+ return false;
+
+ if (!m_sink.SupportsFormat(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGHDEVICE), format))
+ return false;
+
+ return true;
+}
+
+bool CActiveAE::UsesDtsCoreFallback()
+{
+ return m_settings.usesdtscorefallback;
+}
+
+bool CActiveAE::SupportsSilenceTimeout()
+{
+ return true;
+}
+
+bool CActiveAE::HasStereoAudioChannelCount()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ const std::string device = settings->GetString(CSettings::SETTING_AUDIOOUTPUT_AUDIODEVICE);
+ int numChannels = (m_sink.GetDeviceType(device) == AE_DEVTYPE_IEC958) ? AE_CH_LAYOUT_2_0 : settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CHANNELS);
+ bool passthrough = settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CONFIG) == AE_CONFIG_FIXED ? false : settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH);
+ return numChannels == AE_CH_LAYOUT_2_0 && !passthrough;
+}
+
+bool CActiveAE::HasHDAudioChannelCount()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ const std::string device = settings->GetString(CSettings::SETTING_AUDIOOUTPUT_AUDIODEVICE);
+ int numChannels = (m_sink.GetDeviceType(device) == AE_DEVTYPE_IEC958) ? AE_CH_LAYOUT_2_0 : settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CHANNELS);
+ return numChannels > AE_CH_LAYOUT_5_1;
+}
+
+bool CActiveAE::SupportsQualityLevel(enum AEQuality level)
+{
+ if (level == AE_QUALITY_LOW || level == AE_QUALITY_MID || level == AE_QUALITY_HIGH)
+ return true;
+
+ return false;
+}
+
+bool CActiveAE::IsSettingVisible(const std::string &settingId)
+{
+ if (settingId == CSettings::SETTING_AUDIOOUTPUT_SAMPLERATE)
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (m_sink.GetDeviceType(settings->GetString(CSettings::SETTING_AUDIOOUTPUT_AUDIODEVICE)) == AE_DEVTYPE_IEC958)
+ return true;
+ if (settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CONFIG) == AE_CONFIG_FIXED)
+ return true;
+ }
+ else if (settingId == CSettings::SETTING_AUDIOOUTPUT_CHANNELS)
+ {
+ if (m_sink.GetDeviceType(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_AUDIOOUTPUT_AUDIODEVICE)) != AE_DEVTYPE_IEC958)
+ return true;
+ }
+ else if (settingId == CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH)
+ {
+ if (m_sink.HasPassthroughDevice() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_AUDIOOUTPUT_CONFIG) != AE_CONFIG_FIXED)
+ return true;
+ }
+ else if (settingId == CSettings::SETTING_AUDIOOUTPUT_DTSPASSTHROUGH)
+ {
+ AEAudioFormat format;
+ format.m_dataFormat = AE_FMT_RAW;
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTS_512;
+ format.m_sampleRate = 48000;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (m_sink.SupportsFormat(settings->GetString(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGHDEVICE), format) &&
+ settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CONFIG) != AE_CONFIG_FIXED)
+ return true;
+ }
+ else if (settingId == CSettings::SETTING_AUDIOOUTPUT_TRUEHDPASSTHROUGH)
+ {
+ AEAudioFormat format;
+ format.m_dataFormat = AE_FMT_RAW;
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_TRUEHD;
+ format.m_streamInfo.m_sampleRate = 192000;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (m_sink.SupportsFormat(settings->GetString(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGHDEVICE), format) &&
+ settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CONFIG) != AE_CONFIG_FIXED)
+ return true;
+ }
+ else if (settingId == CSettings::SETTING_AUDIOOUTPUT_DTSHDPASSTHROUGH)
+ {
+ AEAudioFormat format;
+ format.m_dataFormat = AE_FMT_RAW;
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (m_sink.SupportsFormat(settings->GetString(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGHDEVICE), format) &&
+ settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CONFIG) != AE_CONFIG_FIXED)
+ return true;
+ }
+ else if (settingId == CSettings::SETTING_AUDIOOUTPUT_EAC3PASSTHROUGH)
+ {
+ AEAudioFormat format;
+ format.m_dataFormat = AE_FMT_RAW;
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_EAC3;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (m_sink.SupportsFormat(settings->GetString(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGHDEVICE), format) &&
+ settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CONFIG) != AE_CONFIG_FIXED)
+ return true;
+ }
+ else if (settingId == CSettings::SETTING_AUDIOOUTPUT_STEREOUPMIX)
+ {
+ if (m_sink.HasPassthroughDevice() ||
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_AUDIOOUTPUT_CHANNELS) > AE_CH_LAYOUT_2_0)
+ return true;
+ }
+ else if (settingId == CSettings::SETTING_AUDIOOUTPUT_AC3TRANSCODE)
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (m_sink.HasPassthroughDevice() &&
+ settings->GetBool(CSettings::SETTING_AUDIOOUTPUT_AC3PASSTHROUGH) &&
+ settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CONFIG) != AE_CONFIG_FIXED &&
+ (settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_CHANNELS) <= AE_CH_LAYOUT_2_0 || m_sink.GetDeviceType(settings->GetString(CSettings::SETTING_AUDIOOUTPUT_AUDIODEVICE)) == AE_DEVTYPE_IEC958))
+ return true;
+ }
+ return false;
+}
+
+void CActiveAE::Shutdown()
+{
+ Dispose();
+}
+
+bool CActiveAE::Suspend()
+{
+ return m_controlPort.SendOutMessage(CActiveAEControlProtocol::SUSPEND);
+}
+
+bool CActiveAE::Resume()
+{
+ Message *reply;
+ if (m_controlPort.SendOutMessageSync(CActiveAEControlProtocol::INIT,
+ &reply,
+ 5000))
+ {
+ bool success = reply->signal == CActiveAEControlProtocol::ACC;
+ reply->Release();
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - returned error", __FUNCTION__);
+ return false;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - failed to init", __FUNCTION__);
+ return false;
+ }
+
+ m_inMsgEvent.Reset();
+ return true;
+}
+
+bool CActiveAE::IsSuspended()
+{
+ return m_stats.IsSuspended();
+}
+
+float CActiveAE::GetVolume()
+{
+ return m_aeVolume;
+}
+
+void CActiveAE::SetVolume(const float volume)
+{
+ m_aeVolume = std::max( 0.0f, std::min(1.0f, volume));
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::VOLUME, &m_aeVolume, sizeof(float));
+}
+
+void CActiveAE::SetMute(const bool enabled)
+{
+ m_aeMuted = enabled;
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::MUTE, &m_aeMuted, sizeof(bool));
+}
+
+bool CActiveAE::IsMuted()
+{
+ return m_aeMuted;
+}
+
+void CActiveAE::KeepConfiguration(unsigned int millis)
+{
+ unsigned int timeMs = millis;
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::KEEPCONFIG, &timeMs, sizeof(unsigned int));
+}
+
+void CActiveAE::DeviceChange()
+{
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::DEVICECHANGE);
+}
+
+void CActiveAE::DeviceCountChange(const std::string& driver)
+{
+ const char* name = driver.c_str();
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::DEVICECOUNTCHANGE, name,
+ driver.length() + 1);
+}
+
+bool CActiveAE::GetCurrentSinkFormat(AEAudioFormat &SinkFormat)
+{
+ SinkFormat = m_stats.GetCurrentSinkFormat();
+ return true;
+}
+
+void CActiveAE::OnLostDisplay()
+{
+ Message *reply;
+ if (m_controlPort.SendOutMessageSync(CActiveAEControlProtocol::DISPLAYLOST,
+ &reply,
+ 5000))
+ {
+ bool success = reply->signal == CActiveAEControlProtocol::ACC;
+ reply->Release();
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - returned error", __FUNCTION__);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "ActiveAE::{} - timed out", __FUNCTION__);
+ }
+}
+
+void CActiveAE::OnResetDisplay()
+{
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::DISPLAYRESET);
+}
+
+void CActiveAE::OnAppFocusChange(bool focus)
+{
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::APPFOCUSED, &focus, sizeof(focus));
+}
+
+//-----------------------------------------------------------------------------
+// Utils
+//-----------------------------------------------------------------------------
+
+uint8_t **CActiveAE::AllocSoundSample(SampleConfig &config, int &samples, int &bytes_per_sample, int &planes, int &linesize)
+{
+ uint8_t **buffer;
+ planes = av_sample_fmt_is_planar(config.fmt) ? config.channels : 1;
+ buffer = new uint8_t*[planes];
+
+ // align buffer to 16 in order to be compatible with sse in CAEConvert
+ av_samples_alloc(buffer, &linesize, config.channels,
+ samples, config.fmt, 16);
+ bytes_per_sample = av_get_bytes_per_sample(config.fmt);
+ return buffer;
+}
+
+void CActiveAE::FreeSoundSample(uint8_t **data)
+{
+ av_freep(data);
+ delete [] data;
+}
+
+bool CActiveAE::CompareFormat(const AEAudioFormat& lhs, const AEAudioFormat& rhs)
+{
+ if (lhs.m_channelLayout != rhs.m_channelLayout ||
+ lhs.m_dataFormat != rhs.m_dataFormat ||
+ lhs.m_sampleRate != rhs.m_sampleRate)
+ return false;
+ else if (lhs.m_dataFormat == AE_FMT_RAW && rhs.m_dataFormat == AE_FMT_RAW &&
+ lhs.m_streamInfo.m_type != rhs.m_streamInfo.m_type)
+ return false;
+ else
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// GUI Sounds
+//-----------------------------------------------------------------------------
+
+/**
+ * load sound from an audio file and store original format
+ * register the sound in ActiveAE
+ * later when the engine is idle it will convert the sound to sink format
+ */
+
+IAE::SoundPtr CActiveAE::MakeSound(const std::string& file)
+{
+ AVFormatContext *fmt_ctx = nullptr;
+ AVCodecContext *dec_ctx = nullptr;
+ AVIOContext *io_ctx;
+ AVInputFormat *io_fmt = nullptr;
+ AVCodec *dec = nullptr;
+ SampleConfig config;
+
+ // No custom deleter until sound is registered
+ auto sound = std::make_unique<CActiveAESound>(file, this);
+ if (!sound->Prepare())
+ {
+ return nullptr;
+ }
+ int fileSize = sound->GetFileSize();
+
+ int bufferSize = 4096;
+ int blockSize = sound->GetChunkSize();
+ if (blockSize > 1)
+ bufferSize = blockSize;
+
+ fmt_ctx = avformat_alloc_context();
+ unsigned char* buffer = (unsigned char*)av_malloc(bufferSize);
+ io_ctx = avio_alloc_context(buffer, bufferSize, 0, sound.get(), CActiveAESound::Read, NULL,
+ CActiveAESound::Seek);
+
+ io_ctx->max_packet_size = bufferSize;
+
+ if (!sound->IsSeekPossible())
+ {
+ io_ctx->seekable = 0;
+ io_ctx->max_packet_size = 0;
+ }
+
+ fmt_ctx->pb = io_ctx;
+
+ av_probe_input_buffer(io_ctx, &io_fmt, file.c_str(), nullptr, 0, 0);
+ if (!io_fmt)
+ {
+ avformat_close_input(&fmt_ctx);
+ if (io_ctx)
+ {
+ av_freep(&io_ctx->buffer);
+ av_freep(&io_ctx);
+ }
+ return nullptr;
+ }
+
+ // find decoder
+ if (avformat_open_input(&fmt_ctx, file.c_str(), nullptr, nullptr) == 0)
+ {
+ fmt_ctx->flags |= AVFMT_FLAG_NOPARSE;
+ if (avformat_find_stream_info(fmt_ctx, nullptr) >= 0)
+ {
+ AVCodecID codecId = fmt_ctx->streams[0]->codecpar->codec_id;
+ dec = avcodec_find_decoder(codecId);
+ config.sample_rate = fmt_ctx->streams[0]->codecpar->sample_rate;
+ config.channels = fmt_ctx->streams[0]->codecpar->channels;
+ config.channel_layout = fmt_ctx->streams[0]->codecpar->channel_layout;
+ }
+ }
+ if (dec == nullptr)
+ {
+ avformat_close_input(&fmt_ctx);
+ if (io_ctx)
+ {
+ av_freep(&io_ctx->buffer);
+ av_freep(&io_ctx);
+ }
+ return nullptr;
+ }
+
+ dec_ctx = avcodec_alloc_context3(dec);
+ dec_ctx->sample_rate = config.sample_rate;
+ dec_ctx->channels = config.channels;
+ if (!config.channel_layout)
+ config.channel_layout = av_get_default_channel_layout(config.channels);
+ dec_ctx->channel_layout = config.channel_layout;
+
+ AVPacket* avpkt = av_packet_alloc();
+ if (!avpkt)
+ CLog::Log(LOGERROR, "CActiveAE::{} - av_packet_alloc failed: {}", __FUNCTION__,
+ strerror(errno));
+
+ AVFrame* decoded_frame = av_frame_alloc();
+ if (!decoded_frame)
+ CLog::Log(LOGERROR, "CActiveAE::{} - av_frame_alloc failed: {}", __FUNCTION__, strerror(errno));
+
+ bool error = false;
+
+ if (avpkt && decoded_frame && avcodec_open2(dec_ctx, dec, nullptr) >= 0)
+ {
+ bool init = false;
+
+ // decode until eof
+ int ret;
+ while (av_read_frame(fmt_ctx, avpkt) >= 0 && !error)
+ {
+ ret = avcodec_send_packet(dec_ctx, avpkt);
+ if (ret < 0)
+ {
+ error = true;
+ break;
+ }
+
+ while ((ret = avcodec_receive_frame(dec_ctx, decoded_frame)) == 0)
+ {
+ if (!init)
+ {
+ int samples = fileSize / av_get_bytes_per_sample(dec_ctx->sample_fmt) / config.channels;
+ config.fmt = dec_ctx->sample_fmt;
+ config.bits_per_sample = dec_ctx->bits_per_coded_sample;
+ sound->InitSound(true, config, samples);
+ init = true;
+ }
+ sound->StoreSound(true, decoded_frame->extended_data,
+ decoded_frame->nb_samples, decoded_frame->linesize[0]);
+ }
+ av_packet_unref(avpkt);
+
+ if (ret < 0 && ret != AVERROR(EAGAIN))
+ {
+ error = true;
+ break;
+ }
+ }
+ ret = avcodec_send_packet(dec_ctx, nullptr);
+ while ((ret = avcodec_receive_frame(dec_ctx, decoded_frame)) != AVERROR_EOF)
+ {
+ if (ret == 0)
+ {
+ sound->StoreSound(true, decoded_frame->extended_data,
+ decoded_frame->nb_samples, decoded_frame->linesize[0]);
+ }
+ else
+ {
+ error = true;
+ break;
+ }
+ }
+ }
+
+ av_packet_free(&avpkt);
+ av_frame_free(&decoded_frame);
+ avcodec_free_context(&dec_ctx);
+ avformat_close_input(&fmt_ctx);
+ if (io_ctx)
+ {
+ av_freep(&io_ctx->buffer);
+ av_freep(&io_ctx);
+ }
+
+ if (error)
+ {
+ return nullptr;
+ }
+
+ sound->Finish();
+
+ // register sound
+ m_dataPort.SendOutMessage(CActiveAEDataProtocol::NEWSOUND, &sound, sizeof(CActiveAESound*));
+
+ // transfer ownership to std::unique_ptr that unregisters the sound
+ return {sound.release(), IAESoundDeleter(*this)};
+}
+
+void CActiveAE::FreeSound(IAESound *sound)
+{
+ m_dataPort.SendOutMessage(CActiveAEDataProtocol::FREESOUND, &sound, sizeof(CActiveAESound*));
+}
+
+void CActiveAE::PlaySound(CActiveAESound *sound)
+{
+ m_dataPort.SendOutMessage(CActiveAEDataProtocol::PLAYSOUND, &sound, sizeof(CActiveAESound*));
+}
+
+void CActiveAE::StopSound(CActiveAESound *sound)
+{
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::STOPSOUND, &sound, sizeof(CActiveAESound*));
+}
+
+/**
+ * resample sounds to destination format for mixing
+ * destination format is either format of stream or
+ * default sink format when no stream is playing
+ */
+void CActiveAE::ResampleSounds()
+{
+ if ((m_settings.guisoundmode == AE_SOUND_OFF ||
+ (m_settings.guisoundmode == AE_SOUND_IDLE && !m_streams.empty())) &&
+ !m_aeGUISoundForce)
+ return;
+
+ std::vector<CActiveAESound*>::iterator it;
+ for (it = m_sounds.begin(); it != m_sounds.end(); ++it)
+ {
+ if (!(*it)->IsConverted())
+ {
+ ResampleSound(*it);
+ // only do one sound, then yield to main loop
+ break;
+ }
+ }
+}
+
+bool CActiveAE::ResampleSound(CActiveAESound *sound)
+{
+ SampleConfig orig_config, dst_config;
+ uint8_t **dst_buffer;
+ int dst_samples;
+
+ if (m_mode == MODE_RAW || m_internalFormat.m_dataFormat == AE_FMT_INVALID)
+ return false;
+
+ if (!sound->GetSound(true))
+ return false;
+
+ orig_config = sound->GetSound(true)->config;
+
+ dst_config.channel_layout = CAEUtil::GetAVChannelLayout(m_internalFormat.m_channelLayout);
+ dst_config.channels = m_internalFormat.m_channelLayout.Count();
+ dst_config.sample_rate = m_internalFormat.m_sampleRate;
+ dst_config.fmt = CAEUtil::GetAVSampleFormat(m_internalFormat.m_dataFormat);
+ dst_config.bits_per_sample = CAEUtil::DataFormatToUsedBits(m_internalFormat.m_dataFormat);
+ dst_config.dither_bits = CAEUtil::DataFormatToDitherBits(m_internalFormat.m_dataFormat);
+
+ AEChannel testChannel = sound->GetChannel();
+ CAEChannelInfo outChannels;
+ if (sound->GetSound(true)->config.channels == 1 && testChannel != AE_CH_NULL)
+ {
+ for (unsigned int out=0; out < m_internalFormat.m_channelLayout.Count(); out++)
+ {
+ if (m_internalFormat.m_channelLayout[out] == AE_CH_FC && testChannel != AE_CH_FC) /// To become center clear on position test ??????
+ outChannels += AE_CH_FL;
+ else if (m_internalFormat.m_channelLayout[out] == testChannel)
+ outChannels += AE_CH_FC;
+ else
+ outChannels += m_internalFormat.m_channelLayout[out];
+ }
+ }
+
+ IAEResample *resampler = CAEResampleFactory::Create(AERESAMPLEFACTORY_QUICK_RESAMPLE);
+
+ resampler->Init(dst_config, orig_config,
+ false,
+ true,
+ M_SQRT1_2,
+ outChannels.Count() > 0 ? &outChannels : nullptr,
+ m_settings.resampleQuality,
+ false);
+
+ dst_samples = resampler->CalcDstSampleCount(sound->GetSound(true)->nb_samples,
+ m_internalFormat.m_sampleRate,
+ orig_config.sample_rate);
+
+ dst_buffer = sound->InitSound(false, dst_config, dst_samples);
+ if (!dst_buffer)
+ {
+ delete resampler;
+ return false;
+ }
+ int samples = resampler->Resample(dst_buffer, dst_samples,
+ sound->GetSound(true)->data,
+ sound->GetSound(true)->nb_samples,
+ 1.0);
+
+ sound->GetSound(false)->nb_samples = samples;
+
+ delete resampler;
+ sound->SetConverted(true);
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Streams
+//-----------------------------------------------------------------------------
+
+IAE::StreamPtr CActiveAE::MakeStream(AEAudioFormat& audioFormat,
+ unsigned int options,
+ IAEClockCallback* clock)
+{
+ if (audioFormat.m_dataFormat <= AE_FMT_INVALID || audioFormat.m_dataFormat >= AE_FMT_MAX)
+ {
+ return nullptr;
+ }
+
+ if (IsSuspended())
+ return NULL;
+
+ //! @todo pass number of samples in audio packet
+
+ AEAudioFormat format = audioFormat;
+ format.m_frames = format.m_sampleRate / 10;
+
+ if (format.m_dataFormat != AE_FMT_RAW)
+ {
+ format.m_frameSize = format.m_channelLayout.Count() *
+ (CAEUtil::DataFormatToBits(format.m_dataFormat) >> 3);
+ }
+ else
+ format.m_frameSize = 1;
+
+ MsgStreamNew msg;
+ msg.format = format;
+ msg.options = options;
+ msg.clock = clock;
+
+ Message *reply;
+ if (m_dataPort.SendOutMessageSync(CActiveAEDataProtocol::NEWSTREAM,
+ &reply,10000,
+ &msg, sizeof(MsgStreamNew)))
+ {
+ bool success = reply->signal == CActiveAEControlProtocol::ACC;
+ if (success)
+ {
+ IAE::StreamPtr stream(*reinterpret_cast<CActiveAEStream**>(reply->data),
+ IAEStreamDeleter(*this));
+ reply->Release();
+ return stream;
+ }
+ reply->Release();
+ }
+
+ CLog::Log(LOGERROR, "ActiveAE::{} - could not create stream", __FUNCTION__);
+ return NULL;
+}
+
+bool CActiveAE::FreeStream(IAEStream *stream, bool finish)
+{
+ MsgStreamFree msg;
+ msg.stream = static_cast<CActiveAEStream*>(stream);
+ msg.finish = finish;
+
+ Message *reply;
+ if (m_dataPort.SendOutMessageSync(CActiveAEDataProtocol::FREESTREAM,
+ &reply,1000,
+ &msg, sizeof(MsgStreamFree)))
+ {
+ bool success = reply->signal == CActiveAEControlProtocol::ACC;
+ reply->Release();
+ if (success)
+ {
+ return true;
+ }
+ }
+ CLog::Log(LOGERROR, "CActiveAE::FreeStream - failed");
+ return false;
+}
+
+void CActiveAE::FlushStream(CActiveAEStream *stream)
+{
+ Message *reply;
+ if (m_controlPort.SendOutMessageSync(CActiveAEControlProtocol::FLUSHSTREAM,
+ &reply,1000,
+ &stream, sizeof(CActiveAEStream*)))
+ {
+ bool success = reply->signal == CActiveAEControlProtocol::ACC;
+ reply->Release();
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "CActiveAE::FlushStream - failed");
+ }
+ }
+}
+
+void CActiveAE::PauseStream(CActiveAEStream *stream, bool pause)
+{
+ //! @todo pause sink, needs api change
+ if (pause)
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::PAUSESTREAM,
+ &stream, sizeof(CActiveAEStream*));
+ else
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::RESUMESTREAM,
+ &stream, sizeof(CActiveAEStream*));
+}
+
+void CActiveAE::SetStreamAmplification(CActiveAEStream *stream, float amplify)
+{
+ MsgStreamParameter msg;
+ msg.stream = stream;
+ msg.parameter.float_par = amplify;
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::STREAMAMP,
+ &msg, sizeof(MsgStreamParameter));
+}
+
+void CActiveAE::SetStreamReplaygain(CActiveAEStream *stream, float rgain)
+{
+ MsgStreamParameter msg;
+ msg.stream = stream;
+ msg.parameter.float_par = rgain;
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::STREAMRGAIN,
+ &msg, sizeof(MsgStreamParameter));
+}
+
+void CActiveAE::SetStreamVolume(CActiveAEStream *stream, float volume)
+{
+ MsgStreamParameter msg;
+ msg.stream = stream;
+ msg.parameter.float_par = volume;
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::STREAMVOLUME,
+ &msg, sizeof(MsgStreamParameter));
+}
+
+void CActiveAE::SetStreamResampleRatio(CActiveAEStream *stream, double ratio)
+{
+ MsgStreamParameter msg;
+ msg.stream = stream;
+ msg.parameter.double_par = ratio;
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::STREAMRESAMPLERATIO,
+ &msg, sizeof(MsgStreamParameter));
+}
+
+void CActiveAE::SetStreamResampleMode(CActiveAEStream *stream, int mode)
+{
+ MsgStreamParameter msg;
+ msg.stream = stream;
+ msg.parameter.int_par = mode;
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::STREAMRESAMPLEMODE,
+ &msg, sizeof(MsgStreamParameter));
+}
+
+void CActiveAE::SetStreamFFmpegInfo(CActiveAEStream *stream, int profile, enum AVMatrixEncoding matrix_encoding, enum AVAudioServiceType audio_service_type)
+{
+ MsgStreamFFmpegInfo msg;
+ msg.stream = stream;
+ msg.profile = profile;
+ msg.matrix_encoding = matrix_encoding;
+ msg.audio_service_type = audio_service_type;
+
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::STREAMFFMPEGINFO, &msg, sizeof(MsgStreamFFmpegInfo));
+}
+
+void CActiveAE::SetStreamFade(CActiveAEStream *stream, float from, float target, unsigned int millis)
+{
+ MsgStreamFade msg;
+ msg.stream = stream;
+ msg.from = from;
+ msg.target = target;
+ msg.millis = millis;
+ m_controlPort.SendOutMessage(CActiveAEControlProtocol::STREAMFADE,
+ &msg, sizeof(MsgStreamFade));
+}
+
+void CActiveAE::RegisterAudioCallback(IAudioCallback* pCallback)
+{
+ std::unique_lock<CCriticalSection> lock(m_vizLock);
+ m_audioCallback.push_back(pCallback);
+ m_vizInitialized = false;
+}
+
+void CActiveAE::UnregisterAudioCallback(IAudioCallback* pCallback)
+{
+ std::unique_lock<CCriticalSection> lock(m_vizLock);
+ auto it = std::find(m_audioCallback.begin(), m_audioCallback.end(), pCallback);
+ if (it != m_audioCallback.end())
+ m_audioCallback.erase(it);
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAE.h b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAE.h
new file mode 100644
index 0000000..c396c18
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAE.h
@@ -0,0 +1,408 @@
+/*
+ * 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 "ActiveAESink.h"
+#include "cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.h"
+#include "cores/AudioEngine/Interfaces/AESound.h"
+#include "cores/AudioEngine/Interfaces/AEStream.h"
+#include "guilib/DispResource.h"
+#include "threads/SystemClock.h"
+#include "threads/Thread.h"
+
+#include <list>
+#include <queue>
+#include <string>
+#include <utility>
+#include <vector>
+
+// ffmpeg
+extern "C" {
+#include <libavformat/avformat.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/avutil.h>
+}
+
+class IAESink;
+class IAEEncoder;
+
+namespace ActiveAE
+{
+
+class CActiveAESound;
+class CActiveAEStream;
+class CActiveAESettings;
+
+struct AudioSettings
+{
+ std::string device;
+ std::string driver;
+ std::string passthroughdevice;
+ int channels;
+ bool ac3passthrough;
+ bool ac3transcode;
+ bool eac3passthrough;
+ bool dtspassthrough;
+ bool truehdpassthrough;
+ bool dtshdpassthrough;
+ bool usesdtscorefallback;
+ bool stereoupmix;
+ bool normalizelevels;
+ bool passthrough;
+ int config;
+ int guisoundmode;
+ unsigned int samplerate;
+ AEQuality resampleQuality;
+ double atempoThreshold;
+ bool streamNoise;
+ int silenceTimeoutMinutes;
+};
+
+class CActiveAEControlProtocol : public Protocol
+{
+public:
+ CActiveAEControlProtocol(std::string name, CEvent* inEvent, CEvent* outEvent)
+ : Protocol(std::move(name), inEvent, outEvent)
+ {
+ }
+ enum OutSignal
+ {
+ INIT = 0,
+ RECONFIGURE,
+ SUSPEND,
+ DEVICECHANGE,
+ DEVICECOUNTCHANGE,
+ MUTE,
+ VOLUME,
+ PAUSESTREAM,
+ RESUMESTREAM,
+ FLUSHSTREAM,
+ STREAMRGAIN,
+ STREAMVOLUME,
+ STREAMAMP,
+ STREAMRESAMPLERATIO,
+ STREAMRESAMPLEMODE,
+ STREAMFADE,
+ STREAMFFMPEGINFO,
+ STOPSOUND,
+ GETSTATE,
+ DISPLAYLOST,
+ DISPLAYRESET,
+ APPFOCUSED,
+ KEEPCONFIG,
+ TIMEOUT,
+ };
+ enum InSignal
+ {
+ ACC,
+ ERR,
+ STATS,
+ };
+};
+
+class CActiveAEDataProtocol : public Protocol
+{
+public:
+ CActiveAEDataProtocol(std::string name, CEvent* inEvent, CEvent* outEvent)
+ : Protocol(std::move(name), inEvent, outEvent)
+ {
+ }
+ enum OutSignal
+ {
+ NEWSOUND = 0,
+ PLAYSOUND,
+ FREESOUND,
+ NEWSTREAM,
+ FREESTREAM,
+ STREAMSAMPLE,
+ DRAINSTREAM,
+ };
+ enum InSignal
+ {
+ ACC,
+ ERR,
+ STREAMBUFFER,
+ STREAMDRAINED,
+ };
+};
+
+struct MsgStreamNew
+{
+ AEAudioFormat format;
+ unsigned int options;
+ IAEClockCallback *clock;
+};
+
+struct MsgStreamFree
+{
+ CActiveAEStream *stream;
+ bool finish; // if true switch back to gui sound mode
+};
+
+struct MsgStreamSample
+{
+ CSampleBuffer *buffer;
+ CActiveAEStream *stream;
+};
+
+struct MsgStreamParameter
+{
+ CActiveAEStream *stream;
+ union
+ {
+ float float_par;
+ double double_par;
+ int int_par;
+ } parameter;
+};
+
+struct MsgStreamFade
+{
+ CActiveAEStream *stream;
+ float from;
+ float target;
+ unsigned int millis;
+};
+
+struct MsgStreamFFmpegInfo
+{
+ CActiveAEStream *stream;
+ int profile;
+ enum AVMatrixEncoding matrix_encoding;
+ enum AVAudioServiceType audio_service_type;
+};
+
+class CEngineStats
+{
+public:
+ void Reset(unsigned int sampleRate, bool pcm);
+ void UpdateSinkDelay(const AEDelayStatus& status, int samples);
+ void AddSamples(int samples, const std::list<CActiveAEStream*>& streams);
+ void GetDelay(AEDelayStatus& status);
+ void AddStream(unsigned int streamid);
+ void RemoveStream(unsigned int streamid);
+ void UpdateStream(CActiveAEStream *stream);
+ void GetDelay(AEDelayStatus& status, CActiveAEStream *stream);
+ void GetSyncInfo(CAESyncInfo& info, CActiveAEStream *stream);
+ float GetCacheTime(CActiveAEStream *stream);
+ float GetCacheTotal();
+ float GetMaxDelay() const;
+ float GetWaterLevel();
+ void SetSuspended(bool state);
+ void SetCurrentSinkFormat(const AEAudioFormat& SinkFormat);
+ void SetSinkCacheTotal(float time) { m_sinkCacheTotal = time; }
+ void SetSinkLatency(float time) { m_sinkLatency = time; }
+ void SetSinkNeedIec(bool needIEC) { m_sinkNeedIecPack = needIEC; }
+ bool IsSuspended();
+ AEAudioFormat GetCurrentSinkFormat();
+protected:
+ float m_sinkCacheTotal;
+ float m_sinkLatency;
+ int m_bufferedSamples;
+ unsigned int m_sinkSampleRate;
+ AEDelayStatus m_sinkDelay;
+ bool m_suspended;
+ AEAudioFormat m_sinkFormat;
+ bool m_pcmOutput;
+ bool m_sinkNeedIecPack{false};
+ CCriticalSection m_lock;
+ struct StreamStats
+ {
+ unsigned int m_streamId;
+ double m_bufferedTime;
+ double m_resampleRatio;
+ double m_syncError;
+ unsigned int m_errorTime;
+ CAESyncInfo::AESyncState m_syncState;
+ };
+ std::vector<StreamStats> m_streamStats;
+};
+
+class CActiveAE : public IAE, public IDispResource, private CThread
+{
+protected:
+ friend class CActiveAESound;
+ friend class CActiveAEStream;
+ friend class CSoundPacket;
+ friend class CActiveAEBufferPoolResample;
+
+public:
+ CActiveAE();
+ ~CActiveAE() override;
+ void Start() override;
+ void Shutdown() override;
+ bool Suspend() override;
+ bool Resume() override;
+ bool IsSuspended() override;
+ void OnSettingsChange();
+
+ float GetVolume() override;
+ void SetVolume(const float volume) override;
+ void SetMute(const bool enabled) override;
+ bool IsMuted() override;
+
+ /* returns a new stream for data in the specified format */
+ IAE::StreamPtr MakeStream(AEAudioFormat& audioFormat,
+ unsigned int options = 0,
+ IAEClockCallback* clock = NULL) override;
+
+ /* returns a new sound object */
+ IAE::SoundPtr MakeSound(const std::string& file) override;
+
+ void EnumerateOutputDevices(AEDeviceList &devices, bool passthrough) override;
+ bool SupportsRaw(AEAudioFormat &format) override;
+ bool SupportsSilenceTimeout() override;
+ bool UsesDtsCoreFallback() override;
+ bool HasStereoAudioChannelCount() override;
+ bool HasHDAudioChannelCount() override;
+ bool SupportsQualityLevel(enum AEQuality level) override;
+ bool IsSettingVisible(const std::string &settingId) override;
+ void KeepConfiguration(unsigned int millis) override;
+ void DeviceChange() override;
+ void DeviceCountChange(const std::string& driver) override;
+ bool GetCurrentSinkFormat(AEAudioFormat &SinkFormat) override;
+
+ void RegisterAudioCallback(IAudioCallback* pCallback) override;
+ void UnregisterAudioCallback(IAudioCallback* pCallback) override;
+
+ void OnLostDisplay() override;
+ void OnResetDisplay() override;
+ void OnAppFocusChange(bool focus) override;
+
+private:
+ bool FreeStream(IAEStream* stream, bool finish) override;
+ void FreeSound(IAESound* sound) override;
+
+protected:
+ void PlaySound(CActiveAESound *sound);
+ static uint8_t **AllocSoundSample(SampleConfig &config, int &samples, int &bytes_per_sample, int &planes, int &linesize);
+ static void FreeSoundSample(uint8_t **data);
+ void GetDelay(AEDelayStatus& status, CActiveAEStream *stream) { m_stats.GetDelay(status, stream); }
+ void GetSyncInfo(CAESyncInfo& info, CActiveAEStream *stream) { m_stats.GetSyncInfo(info, stream); }
+ float GetCacheTime(CActiveAEStream *stream) { return m_stats.GetCacheTime(stream); }
+ float GetCacheTotal() { return m_stats.GetCacheTotal(); }
+ float GetMaxDelay() { return m_stats.GetMaxDelay(); }
+ void FlushStream(CActiveAEStream *stream);
+ void PauseStream(CActiveAEStream *stream, bool pause);
+ void StopSound(CActiveAESound *sound);
+ void SetStreamAmplification(CActiveAEStream *stream, float amplify);
+ void SetStreamReplaygain(CActiveAEStream *stream, float rgain);
+ void SetStreamVolume(CActiveAEStream *stream, float volume);
+ void SetStreamResampleRatio(CActiveAEStream *stream, double ratio);
+ void SetStreamResampleMode(CActiveAEStream *stream, int mode);
+ void SetStreamFFmpegInfo(CActiveAEStream *stream, int profile, enum AVMatrixEncoding matrix_encoding, enum AVAudioServiceType audio_service_type);
+ void SetStreamFade(CActiveAEStream *stream, float from, float target, unsigned int millis);
+
+protected:
+ void Process() override;
+ void StateMachine(int signal, Protocol *port, Message *msg);
+ bool InitSink();
+ void DrainSink();
+ void UnconfigureSink();
+ void Dispose();
+ void LoadSettings();
+ bool NeedReconfigureBuffers();
+ bool NeedReconfigureSink();
+ void ApplySettingsToFormat(AEAudioFormat& format,
+ const AudioSettings& settings,
+ int* mode = NULL);
+ void Configure(AEAudioFormat *desiredFmt = NULL);
+ AEAudioFormat GetInputFormat(AEAudioFormat *desiredFmt = NULL);
+ CActiveAEStream* CreateStream(MsgStreamNew *streamMsg);
+ void DiscardStream(CActiveAEStream *stream);
+ void SFlushStream(CActiveAEStream *stream);
+ void FlushEngine();
+ void ClearDiscardedBuffers();
+ void SStopSound(CActiveAESound *sound);
+ void DiscardSound(CActiveAESound *sound);
+ void ChangeResamplers();
+
+ bool RunStages();
+ bool HasWork();
+ CSampleBuffer* SyncStream(CActiveAEStream *stream);
+
+ void ResampleSounds();
+ bool ResampleSound(CActiveAESound *sound);
+ void MixSounds(CSoundPacket &dstSample);
+ void Deamplify(CSoundPacket &dstSample);
+
+ bool CompareFormat(const AEAudioFormat& lhs, const AEAudioFormat& rhs);
+
+ CEvent m_inMsgEvent;
+ CEvent m_outMsgEvent;
+ CActiveAEControlProtocol m_controlPort;
+ CActiveAEDataProtocol m_dataPort;
+ int m_state;
+ bool m_bStateMachineSelfTrigger;
+ std::chrono::milliseconds m_extTimeout;
+ bool m_extError;
+ bool m_extDrain;
+ XbmcThreads::EndTime<> m_extDrainTimer;
+ std::chrono::milliseconds m_extKeepConfig;
+ bool m_extDeferData;
+ std::queue<time_t> m_extLastDeviceChange;
+ bool m_extSuspended = false;
+ bool m_isWinSysReg = false;
+
+ enum
+ {
+ MODE_RAW,
+ MODE_TRANSCODE,
+ MODE_PCM
+ }m_mode;
+
+ CActiveAESink m_sink;
+ AEAudioFormat m_sinkFormat;
+ AEAudioFormat m_sinkRequestFormat;
+ AEAudioFormat m_encoderFormat;
+ AEAudioFormat m_internalFormat;
+ AEAudioFormat m_inputFormat;
+ AudioSettings m_settings;
+ CEngineStats m_stats;
+ IAEEncoder *m_encoder;
+ std::string m_currDevice;
+ std::unique_ptr<CActiveAESettings> m_settingsHandler;
+
+ // buffers
+ CActiveAEBufferPoolResample *m_sinkBuffers;
+ CActiveAEBufferPoolResample *m_vizBuffers;
+ CActiveAEBufferPool *m_vizBuffersInput;
+ CActiveAEBufferPool *m_silenceBuffers; // needed to drive gui sounds if we have no streams
+ CActiveAEBufferPool *m_encoderBuffers;
+
+ // streams
+ std::list<CActiveAEStream*> m_streams;
+ std::list<CActiveAEBufferPool*> m_discardBufferPools;
+ unsigned int m_streamIdGen;
+
+ // gui sounds
+ struct SoundState
+ {
+ CActiveAESound *sound;
+ int samples_played;
+ };
+ std::list<SoundState> m_sounds_playing;
+ std::vector<CActiveAESound*> m_sounds;
+
+ float m_volume; // volume on a 0..1 scale corresponding to a proportion along the dB scale
+ float m_volumeScaled; // multiplier to scale samples in order to achieve the volume specified in m_volume
+ bool m_muted;
+ bool m_sinkHasVolume;
+
+ // viz
+ std::vector<IAudioCallback*> m_audioCallback;
+ bool m_vizInitialized;
+ CCriticalSection m_vizLock;
+
+ // polled via the interface
+ float m_aeVolume;
+ bool m_aeMuted;
+ bool m_aeGUISoundForce;
+};
+};
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.cpp b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.cpp
new file mode 100644
index 0000000..7768a62
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.cpp
@@ -0,0 +1,703 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ActiveAEBuffer.h"
+
+#include "ActiveAE.h"
+#include "ActiveAEFilter.h"
+#include "cores/AudioEngine/AEResampleFactory.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+
+using namespace ActiveAE;
+
+CSoundPacket::CSoundPacket(const SampleConfig& conf, int samples) : config(conf)
+{
+ data = CActiveAE::AllocSoundSample(config, samples, bytes_per_sample, planes, linesize);
+ max_nb_samples = samples;
+ nb_samples = 0;
+ pause_burst_ms = 0;
+}
+
+CSoundPacket::~CSoundPacket()
+{
+ if (data)
+ CActiveAE::FreeSoundSample(data);
+}
+
+CSampleBuffer::~CSampleBuffer()
+{
+ delete pkt;
+}
+
+CSampleBuffer* CSampleBuffer::Acquire()
+{
+ refCount++;
+ return this;
+}
+
+void CSampleBuffer::Return()
+{
+ refCount--;
+ if (pool && refCount <= 0)
+ pool->ReturnBuffer(this);
+}
+
+CActiveAEBufferPool::CActiveAEBufferPool(const AEAudioFormat& format) : m_format(format)
+{
+ if (m_format.m_dataFormat == AE_FMT_RAW)
+ {
+ m_format.m_frameSize = 1;
+ m_format.m_frames = 61440;
+ m_format.m_channelLayout.Reset();
+ m_format.m_channelLayout += AE_CH_FC;
+ }
+}
+
+CActiveAEBufferPool::~CActiveAEBufferPool()
+{
+ CSampleBuffer *buffer;
+ while(!m_allSamples.empty())
+ {
+ buffer = m_allSamples.front();
+ m_allSamples.pop_front();
+ delete buffer;
+ }
+}
+
+CSampleBuffer* CActiveAEBufferPool::GetFreeBuffer()
+{
+ CSampleBuffer* buf = NULL;
+
+ if (!m_freeSamples.empty())
+ {
+ buf = m_freeSamples.front();
+ m_freeSamples.pop_front();
+ buf->refCount = 1;
+ buf->centerMixLevel = M_SQRT1_2;
+ }
+ return buf;
+}
+
+void CActiveAEBufferPool::ReturnBuffer(CSampleBuffer *buffer)
+{
+ buffer->pkt->nb_samples = 0;
+ buffer->pkt->pause_burst_ms = 0;
+ m_freeSamples.push_back(buffer);
+}
+
+bool CActiveAEBufferPool::Create(unsigned int totaltime)
+{
+ CSampleBuffer *buffer;
+ SampleConfig config;
+ config.fmt = CAEUtil::GetAVSampleFormat(m_format.m_dataFormat);
+ config.bits_per_sample = CAEUtil::DataFormatToUsedBits(m_format.m_dataFormat);
+ config.dither_bits = CAEUtil::DataFormatToDitherBits(m_format.m_dataFormat);
+ config.channels = m_format.m_channelLayout.Count();
+ config.sample_rate = m_format.m_sampleRate;
+ config.channel_layout = CAEUtil::GetAVChannelLayout(m_format.m_channelLayout);
+
+ unsigned int time = 0;
+ unsigned int buffertime = (m_format.m_frames*1000) / m_format.m_sampleRate;
+ if (m_format.m_dataFormat == AE_FMT_RAW)
+ {
+ buffertime = m_format.m_streamInfo.GetDuration();
+ }
+ unsigned int n = 0;
+ while (time < totaltime || n < 5)
+ {
+ buffer = new CSampleBuffer();
+ buffer->pool = this;
+ buffer->pkt = new CSoundPacket(config, m_format.m_frames);
+
+ m_allSamples.push_back(buffer);
+ m_freeSamples.push_back(buffer);
+ time += buffertime;
+ n++;
+ }
+
+ return true;
+}
+
+// ----------------------------------------------------------------------------------
+// Resample
+// ----------------------------------------------------------------------------------
+
+CActiveAEBufferPoolResample::CActiveAEBufferPoolResample(const AEAudioFormat& inputFormat,
+ const AEAudioFormat& outputFormat,
+ AEQuality quality)
+ : CActiveAEBufferPool(outputFormat), m_inputFormat(inputFormat)
+{
+ if (m_inputFormat.m_dataFormat == AE_FMT_RAW)
+ {
+ m_format.m_frameSize = 1;
+ m_format.m_frames = 61440;
+ m_inputFormat.m_channelLayout.Reset();
+ m_inputFormat.m_channelLayout += AE_CH_FC;
+ }
+ m_resampleQuality = quality;
+}
+
+CActiveAEBufferPoolResample::~CActiveAEBufferPoolResample()
+{
+ Flush();
+
+ delete m_resampler;
+}
+
+bool CActiveAEBufferPoolResample::Create(unsigned int totaltime, bool remap, bool upmix, bool normalize)
+{
+ CActiveAEBufferPool::Create(totaltime);
+
+ m_remap = remap;
+ m_stereoUpmix = upmix;
+
+ m_normalize = true;
+ if ((m_format.m_channelLayout.Count() < m_inputFormat.m_channelLayout.Count() && !normalize))
+ m_normalize = false;
+
+ if (m_inputFormat.m_channelLayout != m_format.m_channelLayout ||
+ m_inputFormat.m_sampleRate != m_format.m_sampleRate ||
+ m_inputFormat.m_dataFormat != m_format.m_dataFormat ||
+ m_changeResampler)
+ {
+ ChangeResampler();
+ }
+ return true;
+}
+
+void CActiveAEBufferPoolResample::ChangeResampler()
+{
+ if (m_resampler)
+ {
+ delete m_resampler;
+ m_resampler = NULL;
+ }
+
+ m_resampler = CAEResampleFactory::Create();
+
+ SampleConfig dstConfig, srcConfig;
+ dstConfig.channel_layout = CAEUtil::GetAVChannelLayout(m_format.m_channelLayout);
+ dstConfig.channels = m_format.m_channelLayout.Count();
+ dstConfig.sample_rate = m_format.m_sampleRate;
+ dstConfig.fmt = CAEUtil::GetAVSampleFormat(m_format.m_dataFormat);
+ dstConfig.bits_per_sample = CAEUtil::DataFormatToUsedBits(m_format.m_dataFormat);
+ dstConfig.dither_bits = CAEUtil::DataFormatToDitherBits(m_format.m_dataFormat);
+
+ srcConfig.channel_layout = CAEUtil::GetAVChannelLayout(m_inputFormat.m_channelLayout);
+ srcConfig.channels = m_inputFormat.m_channelLayout.Count();
+ srcConfig.sample_rate = m_inputFormat.m_sampleRate;
+ srcConfig.fmt = CAEUtil::GetAVSampleFormat(m_inputFormat.m_dataFormat);
+ srcConfig.bits_per_sample = CAEUtil::DataFormatToUsedBits(m_inputFormat.m_dataFormat);
+ srcConfig.dither_bits = CAEUtil::DataFormatToDitherBits(m_inputFormat.m_dataFormat);
+
+ m_resampler->Init(dstConfig, srcConfig,
+ m_stereoUpmix,
+ m_normalize,
+ m_centerMixLevel,
+ m_remap ? &m_format.m_channelLayout : nullptr,
+ m_resampleQuality,
+ m_forceResampler);
+
+ m_changeResampler = false;
+}
+
+bool CActiveAEBufferPoolResample::ResampleBuffers(int64_t timestamp)
+{
+ bool busy = false;
+ CSampleBuffer *in;
+
+ if (!m_resampler)
+ {
+ if (m_changeResampler)
+ {
+ if (m_changeResampler)
+ ChangeResampler();
+ return true;
+ }
+ while(!m_inputSamples.empty())
+ {
+ in = m_inputSamples.front();
+ m_inputSamples.pop_front();
+ if (timestamp)
+ {
+ in->timestamp = timestamp;
+ }
+ m_outputSamples.push_back(in);
+ busy = true;
+ }
+ }
+ else if (m_procSample || !m_freeSamples.empty())
+ {
+ int free_samples;
+ if (m_procSample)
+ free_samples = m_procSample->pkt->max_nb_samples - m_procSample->pkt->nb_samples;
+ else
+ free_samples = m_format.m_frames;
+
+ bool skipInput = false;
+ // avoid that ffmpeg resample buffer grows too large
+ if (!m_resampler->WantsNewSamples(free_samples) && !m_empty)
+ skipInput = true;
+
+ bool hasInput = !m_inputSamples.empty();
+
+ if (hasInput || skipInput || m_drain || m_changeResampler)
+ {
+ if (!m_procSample)
+ {
+ m_procSample = GetFreeBuffer();
+ }
+
+ if (hasInput && !skipInput && !m_changeResampler)
+ {
+ in = m_inputSamples.front();
+ if (in->centerMixLevel != m_centerMixLevel)
+ {
+ m_centerMixLevel = in->centerMixLevel;
+ m_changeResampler = true;
+ in = nullptr;
+ }
+ else
+ m_inputSamples.pop_front();
+ }
+ else
+ in = nullptr;
+
+ int start = m_procSample->pkt->nb_samples *
+ m_procSample->pkt->bytes_per_sample *
+ m_procSample->pkt->config.channels /
+ m_procSample->pkt->planes;
+
+ for(int i=0; i<m_procSample->pkt->planes; i++)
+ {
+ m_planes[i] = m_procSample->pkt->data[i] + start;
+ }
+
+ int out_samples = m_resampler->Resample(m_planes,
+ m_procSample->pkt->max_nb_samples - m_procSample->pkt->nb_samples,
+ in ? in->pkt->data : NULL,
+ in ? in->pkt->nb_samples : 0,
+ m_resampleRatio);
+ // in case of error, trigger re-create of resampler
+ if (out_samples < 0)
+ {
+ out_samples = 0;
+ m_changeResampler = true;
+ }
+
+ m_procSample->pkt->nb_samples += out_samples;
+ busy = true;
+ m_empty = (out_samples == 0);
+
+ if (in)
+ {
+ if (!timestamp)
+ {
+ if (in->timestamp)
+ m_lastSamplePts = in->timestamp;
+ else
+ in->pkt_start_offset = 0;
+ }
+ else
+ {
+ m_lastSamplePts = timestamp;
+ in->pkt_start_offset = 0;
+ }
+
+ // pts of last sample we added to the buffer
+ m_lastSamplePts +=
+ (in->pkt->nb_samples - in->pkt_start_offset) * 1000 / in->pkt->config.sample_rate;
+ }
+
+ // calculate pts for last sample in m_procSample
+ int bufferedSamples = m_resampler->GetBufferedSamples();
+ m_procSample->pkt_start_offset = m_procSample->pkt->nb_samples;
+ m_procSample->timestamp = m_lastSamplePts - bufferedSamples * 1000 / m_format.m_sampleRate;
+
+ if ((m_drain || m_changeResampler) && m_empty)
+ {
+ if (m_fillPackets && m_procSample->pkt->nb_samples != 0)
+ {
+ // pad with zero
+ start = m_procSample->pkt->nb_samples *
+ m_procSample->pkt->bytes_per_sample *
+ m_procSample->pkt->config.channels /
+ m_procSample->pkt->planes;
+ for(int i=0; i<m_procSample->pkt->planes; i++)
+ {
+ memset(m_procSample->pkt->data[i]+start, 0, m_procSample->pkt->linesize-start);
+ }
+ }
+
+ // check if draining is finished
+ if (m_drain && m_procSample->pkt->nb_samples == 0)
+ {
+ m_procSample->Return();
+ busy = false;
+ }
+ else
+ m_outputSamples.push_back(m_procSample);
+
+ m_procSample = NULL;
+ if (m_changeResampler)
+ ChangeResampler();
+ }
+ // some methods like encode require completely filled packets
+ else if (!m_fillPackets || (m_procSample->pkt->nb_samples == m_procSample->pkt->max_nb_samples))
+ {
+ m_outputSamples.push_back(m_procSample);
+ m_procSample = NULL;
+ }
+
+ if (in)
+ in->Return();
+ }
+ }
+ return busy;
+}
+
+void CActiveAEBufferPoolResample::ConfigureResampler(bool normalizelevels, bool stereoupmix, AEQuality quality)
+{
+ bool normalize = true;
+ if ((m_format.m_channelLayout.Count() < m_inputFormat.m_channelLayout.Count()) && !normalizelevels)
+ {
+ normalize = false;
+ }
+
+ if (m_normalize != normalize || m_resampleQuality != quality)
+ {
+ m_changeResampler = true;
+ }
+
+ m_resampleQuality = quality;
+ m_normalize = normalize;
+}
+
+float CActiveAEBufferPoolResample::GetDelay()
+{
+ float delay = 0;
+ std::deque<CSampleBuffer*>::iterator itBuf;
+
+ if (m_procSample)
+ delay += (float)m_procSample->pkt->nb_samples / m_procSample->pkt->config.sample_rate;
+
+ for(itBuf=m_inputSamples.begin(); itBuf!=m_inputSamples.end(); ++itBuf)
+ {
+ delay += (float)(*itBuf)->pkt->nb_samples / (*itBuf)->pkt->config.sample_rate;
+ }
+
+ for(itBuf=m_outputSamples.begin(); itBuf!=m_outputSamples.end(); ++itBuf)
+ {
+ delay += (float)(*itBuf)->pkt->nb_samples / (*itBuf)->pkt->config.sample_rate;
+ }
+
+ if (m_resampler)
+ {
+ int samples = m_resampler->GetBufferedSamples();
+ delay += (float)samples / m_format.m_sampleRate;
+ }
+
+ return delay;
+}
+
+void CActiveAEBufferPoolResample::Flush()
+{
+ if (m_procSample)
+ {
+ m_procSample->Return();
+ m_procSample = NULL;
+ }
+ while (!m_inputSamples.empty())
+ {
+ m_inputSamples.front()->Return();
+ m_inputSamples.pop_front();
+ }
+ while (!m_outputSamples.empty())
+ {
+ m_outputSamples.front()->Return();
+ m_outputSamples.pop_front();
+ }
+ if (m_resampler)
+ ChangeResampler();
+}
+
+void CActiveAEBufferPoolResample::SetDrain(bool drain)
+{
+ m_drain = drain;
+}
+
+void CActiveAEBufferPoolResample::SetRR(double rr)
+{
+ m_resampleRatio = rr;
+}
+
+double CActiveAEBufferPoolResample::GetRR() const
+{
+ return m_resampleRatio;
+}
+
+void CActiveAEBufferPoolResample::FillBuffer()
+{
+ m_fillPackets = true;
+}
+
+bool CActiveAEBufferPoolResample::DoesNormalize() const
+{
+ return m_normalize;
+}
+
+void CActiveAEBufferPoolResample::ForceResampler(bool force)
+{
+ m_forceResampler = force;
+}
+
+
+// ----------------------------------------------------------------------------------
+// Atempo
+// ----------------------------------------------------------------------------------
+
+CActiveAEBufferPoolAtempo::CActiveAEBufferPoolAtempo(const AEAudioFormat& format) : CActiveAEBufferPool(format)
+{
+ m_drain = false;
+ m_empty = true;
+ m_tempo = 1.0;
+ m_changeFilter = false;
+ m_procSample = nullptr;
+}
+
+CActiveAEBufferPoolAtempo::~CActiveAEBufferPoolAtempo()
+{
+ Flush();
+}
+
+bool CActiveAEBufferPoolAtempo::Create(unsigned int totaltime)
+{
+ CActiveAEBufferPool::Create(totaltime);
+
+ m_pTempoFilter.reset(new CActiveAEFilter());
+ m_pTempoFilter->Init(CAEUtil::GetAVSampleFormat(m_format.m_dataFormat), m_format.m_sampleRate, CAEUtil::GetAVChannelLayout(m_format.m_channelLayout));
+
+ return true;
+}
+
+void CActiveAEBufferPoolAtempo::ChangeFilter()
+{
+ m_pTempoFilter->SetTempo(m_tempo);
+ m_changeFilter = false;
+}
+
+bool CActiveAEBufferPoolAtempo::ProcessBuffers()
+{
+ bool busy = false;
+ CSampleBuffer *in;
+
+ if (!m_pTempoFilter->IsActive())
+ {
+ if (m_changeFilter)
+ {
+ if (m_changeFilter)
+ ChangeFilter();
+ return true;
+ }
+ while(!m_inputSamples.empty())
+ {
+ in = m_inputSamples.front();
+ m_inputSamples.pop_front();
+ m_outputSamples.push_back(in);
+ busy = true;
+ }
+ }
+ else if (m_procSample || !m_freeSamples.empty())
+ {
+ bool skipInput = false;
+
+ // avoid that bufferscr grows too large
+ if (!m_pTempoFilter->NeedData())
+ skipInput = true;
+
+ bool hasInput = !m_inputSamples.empty();
+
+ if (hasInput || skipInput || m_drain || m_changeFilter)
+ {
+ if (!m_procSample)
+ {
+ m_procSample = GetFreeBuffer();
+ }
+
+ if (hasInput && !skipInput && !m_changeFilter)
+ {
+ in = m_inputSamples.front();
+ m_inputSamples.pop_front();
+ }
+ else
+ in = nullptr;
+
+ int start = m_procSample->pkt->nb_samples *
+ m_procSample->pkt->bytes_per_sample *
+ m_procSample->pkt->config.channels /
+ m_procSample->pkt->planes;
+
+ for (int i=0; i<m_procSample->pkt->planes; i++)
+ {
+ m_planes[i] = m_procSample->pkt->data[i] + start;
+ }
+
+ int out_samples = m_pTempoFilter->ProcessFilter(m_planes,
+ m_procSample->pkt->max_nb_samples - m_procSample->pkt->nb_samples,
+ in ? in->pkt->data : nullptr,
+ in ? in->pkt->nb_samples : 0,
+ in ? in->pkt->linesize * in->pkt->planes : 0);
+
+ // in case of error, trigger re-create of filter
+ if (out_samples < 0)
+ {
+ out_samples = 0;
+ m_changeFilter = true;
+ }
+
+ m_procSample->pkt->nb_samples += out_samples;
+ busy = true;
+ m_empty = m_pTempoFilter->IsEof();
+
+ if (in)
+ {
+ if (in->timestamp)
+ m_lastSamplePts = in->timestamp;
+ else
+ in->pkt_start_offset = 0;
+
+ // pts of last sample we added to the buffer
+ m_lastSamplePts += (in->pkt->nb_samples-in->pkt_start_offset) * 1000 / m_format.m_sampleRate;
+ }
+
+ // calculate pts for last sample in m_procSample
+ int bufferedSamples = m_pTempoFilter->GetBufferedSamples();
+ m_procSample->pkt_start_offset = m_procSample->pkt->nb_samples;
+ m_procSample->timestamp = m_lastSamplePts - bufferedSamples * 1000 / m_format.m_sampleRate;
+
+ if ((m_drain || m_changeFilter) && m_empty)
+ {
+ if (m_fillPackets && m_procSample->pkt->nb_samples != 0)
+ {
+ // pad with zero
+ start = m_procSample->pkt->nb_samples *
+ m_procSample->pkt->bytes_per_sample *
+ m_procSample->pkt->config.channels /
+ m_procSample->pkt->planes;
+ for (int i=0; i<m_procSample->pkt->planes; i++)
+ {
+ memset(m_procSample->pkt->data[i]+start, 0, m_procSample->pkt->linesize-start);
+ }
+ }
+
+ // check if draining is finished
+ if (m_drain && m_procSample->pkt->nb_samples == 0)
+ {
+ m_procSample->Return();
+ busy = false;
+ }
+ else
+ m_outputSamples.push_back(m_procSample);
+
+ m_procSample = nullptr;
+
+ if (m_changeFilter)
+ {
+ ChangeFilter();
+ }
+ }
+ // some methods like encode require completely filled packets
+ else if (!m_fillPackets || (m_procSample->pkt->nb_samples == m_procSample->pkt->max_nb_samples))
+ {
+ m_outputSamples.push_back(m_procSample);
+ m_procSample = nullptr;
+ }
+
+ if (in)
+ in->Return();
+ }
+ }
+ return busy;
+}
+
+void CActiveAEBufferPoolAtempo::Flush()
+{
+ if (m_procSample)
+ {
+ m_procSample->Return();
+ m_procSample = nullptr;
+ }
+ while (!m_inputSamples.empty())
+ {
+ m_inputSamples.front()->Return();
+ m_inputSamples.pop_front();
+ }
+ while (!m_outputSamples.empty())
+ {
+ m_outputSamples.front()->Return();
+ m_outputSamples.pop_front();
+ }
+ if (m_pTempoFilter)
+ ChangeFilter();
+}
+
+float CActiveAEBufferPoolAtempo::GetDelay()
+{
+ float delay = 0;
+
+ if (m_procSample)
+ delay += (float)m_procSample->pkt->nb_samples / m_procSample->pkt->config.sample_rate;
+
+ for (auto &buf : m_inputSamples)
+ {
+ delay += (float)buf->pkt->nb_samples / buf->pkt->config.sample_rate;
+ }
+
+ for (auto &buf : m_outputSamples)
+ {
+ delay += (float)buf->pkt->nb_samples / buf->pkt->config.sample_rate;
+ }
+
+ if (m_pTempoFilter->IsActive())
+ {
+ int samples = m_pTempoFilter->GetBufferedSamples();
+ delay += (float)samples / m_format.m_sampleRate;
+ }
+
+ return delay;
+}
+
+void CActiveAEBufferPoolAtempo::SetTempo(float tempo)
+{
+ if (tempo > 2.0f)
+ tempo = 2.0;
+ else if (tempo < 0.5f)
+ tempo = 0.5;
+
+ if (tempo != m_tempo)
+ m_changeFilter = true;
+
+ m_tempo = tempo;
+}
+
+float CActiveAEBufferPoolAtempo::GetTempo() const
+{
+ return m_tempo;
+}
+
+void CActiveAEBufferPoolAtempo::FillBuffer()
+{
+ m_fillPackets = true;
+}
+
+void CActiveAEBufferPoolAtempo::SetDrain(bool drain)
+{
+ m_drain = drain;
+ if (!m_drain)
+ m_changeFilter = true;
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.h b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.h
new file mode 100644
index 0000000..423b59e
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.h
@@ -0,0 +1,147 @@
+/*
+ * 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 "cores/AudioEngine/Utils/AEAudioFormat.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include <cmath>
+#include <deque>
+#include <memory>
+
+extern "C" {
+#include <libavutil/avutil.h>
+#include <libswresample/swresample.h>
+}
+
+namespace ActiveAE
+{
+
+/**
+ * the variables here follow ffmpeg naming
+ */
+class CSoundPacket
+{
+public:
+ CSoundPacket(const SampleConfig& conf, int samples);
+ ~CSoundPacket();
+ uint8_t **data; // array with pointers to planes of data
+ SampleConfig config;
+ int bytes_per_sample; // bytes per sample and per channel
+ int linesize; // see ffmpeg, required for planar formats
+ int planes; // 1 for non planar formats, #channels for planar
+ int nb_samples; // number of frames used
+ int max_nb_samples; // max number of frames this packet can hold
+ int pause_burst_ms;
+};
+
+class CActiveAEBufferPool;
+
+class CSampleBuffer
+{
+public:
+ CSampleBuffer() = default;
+ ~CSampleBuffer();
+ CSampleBuffer *Acquire();
+ void Return();
+ CSoundPacket *pkt = nullptr;
+ CActiveAEBufferPool *pool = nullptr;
+ int64_t timestamp;
+ int pkt_start_offset = 0;
+ int refCount = 0;
+ double centerMixLevel;
+};
+
+class CActiveAEBufferPool
+{
+public:
+ explicit CActiveAEBufferPool(const AEAudioFormat& format);
+ virtual ~CActiveAEBufferPool();
+ virtual bool Create(unsigned int totaltime);
+ CSampleBuffer *GetFreeBuffer();
+ void ReturnBuffer(CSampleBuffer *buffer);
+ AEAudioFormat m_format;
+ std::deque<CSampleBuffer*> m_allSamples;
+ std::deque<CSampleBuffer*> m_freeSamples;
+};
+
+class IAEResample;
+
+class CActiveAEBufferPoolResample : public CActiveAEBufferPool
+{
+public:
+ CActiveAEBufferPoolResample(const AEAudioFormat& inputFormat, const AEAudioFormat& outputFormat, AEQuality quality);
+ ~CActiveAEBufferPoolResample() override;
+ using CActiveAEBufferPool::Create;
+ bool Create(unsigned int totaltime, bool remap, bool upmix, bool normalize = true);
+ bool ResampleBuffers(int64_t timestamp = 0);
+ void ConfigureResampler(bool normalizelevels, bool stereoupmix, AEQuality quality);
+ float GetDelay();
+ void Flush();
+ void SetDrain(bool drain);
+ void SetRR(double rr);
+ double GetRR() const;
+ void FillBuffer();
+ bool DoesNormalize() const;
+ void ForceResampler(bool force);
+ AEAudioFormat m_inputFormat;
+ std::deque<CSampleBuffer*> m_inputSamples;
+ std::deque<CSampleBuffer*> m_outputSamples;
+
+protected:
+ void ChangeResampler();
+
+ uint8_t *m_planes[16];
+ bool m_empty = true;
+ bool m_drain = false;
+ int64_t m_lastSamplePts = 0;
+ bool m_remap = false;
+ CSampleBuffer *m_procSample = nullptr;
+ IAEResample *m_resampler = nullptr;
+ double m_resampleRatio = 1.0;
+ double m_centerMixLevel = M_SQRT1_2;
+ bool m_fillPackets = false;
+ bool m_normalize = true;
+ bool m_changeResampler = false;
+ bool m_forceResampler = false;
+ AEQuality m_resampleQuality;
+ bool m_stereoUpmix = false;
+};
+
+class CActiveAEFilter;
+
+class CActiveAEBufferPoolAtempo : public CActiveAEBufferPool
+{
+public:
+ explicit CActiveAEBufferPoolAtempo(const AEAudioFormat& format);
+ ~CActiveAEBufferPoolAtempo() override;
+ bool Create(unsigned int totaltime) override;
+ bool ProcessBuffers();
+ float GetDelay();
+ void Flush();
+ void SetTempo(float tempo);
+ float GetTempo() const;
+ void FillBuffer();
+ void SetDrain(bool drain);
+ std::deque<CSampleBuffer*> m_inputSamples;
+ std::deque<CSampleBuffer*> m_outputSamples;
+
+protected:
+ void ChangeFilter();
+ std::unique_ptr<CActiveAEFilter> m_pTempoFilter;
+ uint8_t *m_planes[16];
+ CSampleBuffer *m_procSample;
+ bool m_empty;
+ bool m_drain;
+ bool m_changeFilter;
+ float m_tempo;
+ int64_t m_lastSamplePts;
+ bool m_fillPackets;
+};
+
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEFilter.cpp b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEFilter.cpp
new file mode 100644
index 0000000..26b669a
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEFilter.cpp
@@ -0,0 +1,351 @@
+/*
+ * 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 "ActiveAEFilter.h"
+#include "utils/log.h"
+#include "utils/StringUtils.h"
+#include <algorithm>
+
+extern "C" {
+#include <libavfilter/avfilter.h>
+#include <libavcodec/avcodec.h>
+#include <libavfilter/buffersink.h>
+#include <libavfilter/buffersrc.h>
+#include <libswresample/swresample.h>
+}
+
+using namespace ActiveAE;
+
+CActiveAEFilter::CActiveAEFilter()
+{
+ m_pFilterGraph = nullptr;
+ m_pFilterCtxIn = nullptr;
+ m_pFilterCtxOut = nullptr;
+ m_pOutFrame = nullptr;
+ m_pConvertCtx = nullptr;
+ m_pConvertFrame = nullptr;
+ m_needConvert = false;
+}
+
+CActiveAEFilter::~CActiveAEFilter()
+{
+ CloseFilter();
+}
+
+void CActiveAEFilter::Init(AVSampleFormat fmt, int sampleRate, uint64_t channelLayout)
+{
+ m_sampleFormat = fmt;
+ m_sampleRate = sampleRate;
+ m_channelLayout = channelLayout;
+ m_tempo = 1.0;
+ m_SamplesIn = 0;
+ m_SamplesOut = 0;
+}
+
+bool CActiveAEFilter::SetTempo(float tempo)
+{
+ m_tempo = tempo;
+ if (m_tempo == 1.0f)
+ {
+ CloseFilter();
+ return true;
+ }
+
+ if (!CreateFilterGraph())
+ return false;
+
+ if (!CreateAtempoFilter())
+ {
+ CloseFilter();
+ return false;
+ }
+
+ m_SamplesIn = 0;
+ m_SamplesOut = 0;
+ return true;
+}
+
+bool CActiveAEFilter::CreateFilterGraph()
+{
+ CloseFilter();
+
+ m_pFilterGraph = avfilter_graph_alloc();
+ if (!m_pFilterGraph)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::CreateFilterGraph - unable to alloc filter graph");
+ return false;
+ }
+
+ const AVFilter* srcFilter = avfilter_get_by_name("abuffer");
+ const AVFilter* outFilter = avfilter_get_by_name("abuffersink");
+
+ std::string args = StringUtils::Format(
+ "time_base=1/{}:sample_rate={}:sample_fmt={}:channel_layout={}", m_sampleRate, m_sampleRate,
+ av_get_sample_fmt_name(m_sampleFormat), m_channelLayout);
+
+ int ret = avfilter_graph_create_filter(&m_pFilterCtxIn, srcFilter, "in", args.c_str(), NULL, m_pFilterGraph);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::CreateFilterGraph - avfilter_graph_create_filter: src");
+ return false;
+ }
+
+ ret = avfilter_graph_create_filter(&m_pFilterCtxOut, outFilter, "out", NULL, NULL, m_pFilterGraph);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::CreateFilterGraph - avfilter_graph_create_filter: out");
+ return false;
+ }
+
+ m_pOutFrame = av_frame_alloc();
+
+ return true;
+}
+
+bool CActiveAEFilter::CreateAtempoFilter()
+{
+ const AVFilter *atempo;
+
+ atempo = avfilter_get_by_name("atempo");
+ m_pFilterCtxAtempo = avfilter_graph_alloc_filter(m_pFilterGraph, atempo, "atempo");
+ std::string args = StringUtils::Format("tempo={:f}", m_tempo);
+ int ret = avfilter_init_str(m_pFilterCtxAtempo, args.c_str());
+
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::CreateAtempoFilter - avfilter_init_str failed");
+ return false;
+ }
+
+ ret = avfilter_link(m_pFilterCtxIn, 0, m_pFilterCtxAtempo, 0);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::CreateAtempoFilter - avfilter_link failed for in filter");
+ return false;
+ }
+
+ ret = avfilter_link(m_pFilterCtxAtempo, 0, m_pFilterCtxOut, 0);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::CreateAtempoFilter - avfilter_link failed for out filter");
+ return false;
+ }
+
+ ret = avfilter_graph_config(m_pFilterGraph, NULL);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::CreateAtempoFilter - avfilter_graph_config failed");
+ return false;
+ }
+
+ m_needConvert = false;
+ if (m_pFilterCtxAtempo->outputs[0]->format != m_sampleFormat)
+ {
+ m_needConvert = true;
+ m_pConvertCtx = swr_alloc();
+ m_pConvertFrame = av_frame_alloc();
+ }
+
+ m_hasData = false;
+ m_needData = true;
+ m_filterEof = false;
+ m_started = false;
+ m_ptsInitialized = false;
+
+ return true;
+}
+
+void CActiveAEFilter::CloseFilter()
+{
+ if (m_pFilterGraph)
+ {
+ avfilter_graph_free(&m_pFilterGraph);
+
+ m_pFilterCtxIn = nullptr;
+ m_pFilterCtxOut = nullptr;
+ }
+
+ if (m_pOutFrame)
+ av_frame_free(&m_pOutFrame);
+
+ if (m_pConvertFrame)
+ av_frame_free(&m_pConvertFrame);
+
+ if (m_pConvertCtx)
+ swr_free(&m_pConvertCtx);
+
+ m_SamplesIn = 0;
+ m_SamplesOut = 0;
+
+ m_ptsInitialized = false;
+}
+
+int CActiveAEFilter::ProcessFilter(uint8_t **dst_buffer, int dst_samples, uint8_t **src_buffer, int src_samples, int src_bufsize)
+{
+ if (m_filterEof)
+ {
+ if (src_samples)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::ProcessFilter - adding data while already eof");
+ return -1;
+ }
+ return 0;
+ }
+
+ int result;
+
+ if (src_samples)
+ {
+ AVFrame *frame = av_frame_alloc();
+ if (!frame)
+ return -1;
+
+ int channels = av_get_channel_layout_nb_channels(m_channelLayout);
+
+ frame->channel_layout = m_channelLayout;
+ frame->channels = channels;
+ frame->sample_rate = m_sampleRate;
+ frame->nb_samples = src_samples;
+ frame->format = m_sampleFormat;
+ if (!m_ptsInitialized)
+ {
+ frame->pts = 0;
+ m_ptsInitialized = true;
+ }
+
+ m_SamplesIn += src_samples;
+
+ result = avcodec_fill_audio_frame(frame, channels, m_sampleFormat,
+ src_buffer[0], src_bufsize, 16);
+ if (result < 0)
+ {
+ av_frame_free(&frame);
+ CLog::Log(LOGERROR, "CActiveAEFilter::ProcessFilter - avcodec_fill_audio_frame failed");
+ m_filterEof = true;
+ return -1;
+ }
+
+ result = av_buffersrc_write_frame(m_pFilterCtxIn, frame);
+ av_frame_free(&frame);
+ if (result < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::ProcessFilter - av_buffersrc_add_frame failed");
+ m_filterEof = true;
+ return -1;
+ }
+
+ m_started = true;
+ }
+ else if (!m_filterEof && m_needData)
+ {
+ result = av_buffersrc_write_frame(m_pFilterCtxIn, nullptr);
+ if (result < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::ProcessFilter - av_buffersrc_add_frame");
+ m_filterEof = true;
+ return -1;
+ }
+ }
+
+ if (!m_hasData && m_started)
+ {
+ m_needData = false;
+ AVFrame *outFrame = m_needConvert ? m_pConvertFrame : m_pOutFrame;
+
+ result = av_buffersink_get_frame(m_pFilterCtxOut, outFrame);
+
+ if (result == AVERROR(EAGAIN))
+ {
+ m_needData = true;
+ return 0;
+ }
+ else if (result == AVERROR_EOF)
+ {
+ result = av_buffersink_get_frame(m_pFilterCtxOut, outFrame);
+ m_filterEof = true;
+ if (result < 0)
+ return 0;
+ }
+ else if (result < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::ProcessFilter - av_buffersink_get_frame");
+ m_filterEof = true;
+ return -1;
+ }
+
+ m_SamplesOut = outFrame->pts;
+
+ if (m_needConvert)
+ {
+ av_frame_unref(m_pOutFrame);
+ m_pOutFrame->format = m_sampleFormat;
+ m_pOutFrame->channel_layout = m_channelLayout;
+ m_pOutFrame->sample_rate = m_sampleRate;
+ result = swr_convert_frame(m_pConvertCtx, m_pOutFrame, m_pConvertFrame);
+ av_frame_unref(m_pConvertFrame);
+ if (result < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEFilter::ProcessFilter - swr_convert_frame failed");
+ m_filterEof = true;
+ return -1;
+ }
+ }
+
+ m_hasData = true;
+ m_sampleOffset = 0;
+ }
+
+ if (m_hasData)
+ {
+ int channels = av_get_channel_layout_nb_channels(m_channelLayout);
+ int planes = av_sample_fmt_is_planar(m_sampleFormat) ? channels : 1;
+ int samples = std::min(dst_samples, m_pOutFrame->nb_samples - m_sampleOffset);
+ int bytes = samples * av_get_bytes_per_sample(m_sampleFormat) * channels / planes;
+ int bytesOffset = m_sampleOffset * av_get_bytes_per_sample(m_sampleFormat) * channels / planes;
+ for (int i=0; i<planes; i++)
+ {
+ memcpy(dst_buffer[i], m_pOutFrame->extended_data[i] + bytesOffset, bytes);
+ }
+ m_sampleOffset += samples;
+
+ if (m_sampleOffset >= m_pOutFrame->nb_samples)
+ {
+ av_frame_unref(m_pOutFrame);
+ m_hasData = false;
+ }
+
+ return samples;
+ }
+
+ return 0;
+}
+
+bool CActiveAEFilter::IsEof() const
+{
+ return m_filterEof;
+}
+
+bool CActiveAEFilter::NeedData() const
+{
+ return m_needData;
+}
+
+bool CActiveAEFilter::IsActive() const
+{
+ return m_pFilterGraph != nullptr && !m_filterEof;
+}
+
+int CActiveAEFilter::GetBufferedSamples() const
+{
+ int ret = m_SamplesIn - (m_SamplesOut * m_tempo);
+ if (m_hasData)
+ {
+ ret += (m_pOutFrame->nb_samples - m_sampleOffset);
+ }
+ return ret;
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEFilter.h b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEFilter.h
new file mode 100644
index 0000000..8cb15e7
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEFilter.h
@@ -0,0 +1,61 @@
+/*
+ * 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
+
+extern "C" {
+#include <libavfilter/avfilter.h>
+#include <libavutil/frame.h>
+}
+
+struct SwrContext;
+
+namespace ActiveAE
+{
+
+class CActiveAEFilter
+{
+public:
+ CActiveAEFilter();
+ virtual ~CActiveAEFilter();
+ void Init(AVSampleFormat fmt, int sampleRate, uint64_t channelLayout);
+ int ProcessFilter(uint8_t **dst_buffer, int dst_samples, uint8_t **src_buffer, int src_samples, int src_bufsize);
+ bool SetTempo(float tempo);
+ bool NeedData() const;
+ bool IsEof() const;
+ bool IsActive() const;
+ int GetBufferedSamples() const;
+
+protected:
+ bool CreateFilterGraph();
+ bool CreateAtempoFilter();
+ void CloseFilter();
+
+ AVSampleFormat m_sampleFormat;
+ int m_sampleRate;
+ uint64_t m_channelLayout;
+ AVFilterGraph* m_pFilterGraph;
+ AVFilterContext* m_pFilterCtxIn;
+ AVFilterContext* m_pFilterCtxOut;
+ AVFilterContext* m_pFilterCtxAtempo;
+ AVFrame* m_pOutFrame;
+ SwrContext* m_pConvertCtx;
+ AVFrame* m_pConvertFrame;
+ bool m_needConvert;
+ float m_tempo;
+ bool m_filterEof;
+ bool m_started;
+ bool m_hasData;
+ bool m_needData;
+ bool m_ptsInitialized;
+ int m_sampleOffset;
+ int64_t m_SamplesIn;
+ int64_t m_SamplesOut;
+};
+
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEResampleFFMPEG.cpp b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEResampleFFMPEG.cpp
new file mode 100644
index 0000000..bfef837
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEResampleFFMPEG.cpp
@@ -0,0 +1,277 @@
+/*
+ * 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 "cores/AudioEngine/Utils/AEUtil.h"
+#include "ActiveAEResampleFFMPEG.h"
+#include "utils/log.h"
+
+extern "C" {
+#include <libavutil/channel_layout.h>
+#include <libavutil/opt.h>
+#include <libswresample/swresample.h>
+}
+
+using namespace ActiveAE;
+
+CActiveAEResampleFFMPEG::CActiveAEResampleFFMPEG()
+{
+ m_pContext = NULL;
+ m_doesResample = false;
+}
+
+CActiveAEResampleFFMPEG::~CActiveAEResampleFFMPEG()
+{
+ swr_free(&m_pContext);
+}
+
+bool CActiveAEResampleFFMPEG::Init(SampleConfig dstConfig, SampleConfig srcConfig, bool upmix, bool normalize, double centerMix,
+ CAEChannelInfo *remapLayout, AEQuality quality, bool force_resample)
+{
+ m_dst_chan_layout = dstConfig.channel_layout;
+ m_dst_channels = dstConfig.channels;
+ m_dst_rate = dstConfig.sample_rate;
+ m_dst_fmt = dstConfig.fmt;
+ m_dst_bits = dstConfig.bits_per_sample;
+ m_dst_dither_bits = dstConfig.dither_bits;
+ m_src_chan_layout = srcConfig.channel_layout;
+ m_src_channels = srcConfig.channels;
+ m_src_rate = srcConfig.sample_rate;
+ m_src_fmt = srcConfig.fmt;
+ m_src_bits = srcConfig.bits_per_sample;
+ m_src_dither_bits = srcConfig.dither_bits;
+
+ if (m_src_rate != m_dst_rate)
+ m_doesResample = true;
+
+ if (m_dst_chan_layout == 0)
+ m_dst_chan_layout = av_get_default_channel_layout(m_dst_channels);
+ if (m_src_chan_layout == 0)
+ m_src_chan_layout = av_get_default_channel_layout(m_src_channels);
+
+ m_pContext = swr_alloc_set_opts(NULL, m_dst_chan_layout, m_dst_fmt, m_dst_rate,
+ m_src_chan_layout, m_src_fmt, m_src_rate,
+ 0, NULL);
+
+ if (!m_pContext)
+ {
+ CLog::Log(LOGERROR, "CActiveAEResampleFFMPEG::Init - create context failed");
+ return false;
+ }
+
+ if(quality == AE_QUALITY_HIGH)
+ {
+ av_opt_set_double(m_pContext, "cutoff", 1.0, 0);
+ av_opt_set_int(m_pContext,"filter_size", 256, 0);
+ }
+ else if(quality == AE_QUALITY_MID)
+ {
+ // 0.97 is default cutoff so use (1.0 - 0.97) / 2.0 + 0.97
+ av_opt_set_double(m_pContext, "cutoff", 0.985, 0);
+ av_opt_set_int(m_pContext,"filter_size", 64, 0);
+ }
+ else if(quality == AE_QUALITY_LOW)
+ {
+ av_opt_set_double(m_pContext, "cutoff", 0.97, 0);
+ av_opt_set_int(m_pContext,"filter_size", 32, 0);
+ }
+
+ if (m_dst_fmt == AV_SAMPLE_FMT_S32 || m_dst_fmt == AV_SAMPLE_FMT_S32P)
+ {
+ av_opt_set_int(m_pContext, "output_sample_bits", m_dst_bits, 0);
+ }
+
+ // tell resampler to clamp float values
+ // not required for sink stage (remapLayout == true)
+ if ((m_dst_fmt == AV_SAMPLE_FMT_FLT || m_dst_fmt == AV_SAMPLE_FMT_FLTP) &&
+ (m_src_fmt == AV_SAMPLE_FMT_FLT || m_src_fmt == AV_SAMPLE_FMT_FLTP) &&
+ !remapLayout && normalize)
+ {
+ av_opt_set_double(m_pContext, "rematrix_maxval", 1.0, 0);
+ }
+
+ av_opt_set_double(m_pContext, "center_mix_level", centerMix, 0);
+
+ if (remapLayout)
+ {
+ // one-to-one mapping of channels
+ // remapLayout is the layout of the sink, if the channel is in our src layout
+ // the channel is mapped by setting coef 1.0
+ memset(m_rematrix, 0, sizeof(m_rematrix));
+ m_dst_chan_layout = 0;
+ for (unsigned int out=0; out<remapLayout->Count(); out++)
+ {
+ m_dst_chan_layout += ((uint64_t)1) << out;
+ int idx = CAEUtil::GetAVChannelIndex((*remapLayout)[out], m_src_chan_layout);
+ if (idx >= 0)
+ {
+ m_rematrix[out][idx] = 1.0;
+ }
+ }
+
+ av_opt_set_int(m_pContext, "out_channel_count", m_dst_channels, 0);
+ av_opt_set_int(m_pContext, "out_channel_layout", m_dst_chan_layout, 0);
+
+ if (swr_set_matrix(m_pContext, (const double*)m_rematrix, AE_CH_MAX) < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEResampleFFMPEG::Init - setting channel matrix failed");
+ return false;
+ }
+ }
+ // stereo upmix
+ else if (upmix && m_src_channels == 2 && m_dst_channels > 2)
+ {
+ memset(m_rematrix, 0, sizeof(m_rematrix));
+ for (int out=0; out<m_dst_channels; out++)
+ {
+ uint64_t out_chan = av_channel_layout_extract_channel(m_dst_chan_layout, out);
+ switch(out_chan)
+ {
+ case AV_CH_FRONT_LEFT:
+ case AV_CH_BACK_LEFT:
+ case AV_CH_SIDE_LEFT:
+ m_rematrix[out][0] = 1.0;
+ break;
+ case AV_CH_FRONT_RIGHT:
+ case AV_CH_BACK_RIGHT:
+ case AV_CH_SIDE_RIGHT:
+ m_rematrix[out][1] = 1.0;
+ break;
+ case AV_CH_FRONT_CENTER:
+ m_rematrix[out][0] = 0.5;
+ m_rematrix[out][1] = 0.5;
+ break;
+ case AV_CH_LOW_FREQUENCY:
+ m_rematrix[out][0] = 0.5;
+ m_rematrix[out][1] = 0.5;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (swr_set_matrix(m_pContext, (const double*)m_rematrix, AE_CH_MAX) < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEResampleFFMPEG::Init - setting channel matrix failed");
+ return false;
+ }
+ }
+
+ if(swr_init(m_pContext) < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEResampleFFMPEG::Init - init resampler failed");
+ return false;
+ }
+ return true;
+}
+
+int CActiveAEResampleFFMPEG::Resample(uint8_t **dst_buffer, int dst_samples, uint8_t **src_buffer, int src_samples, double ratio)
+{
+ int delta = 0;
+ int distance = 0;
+ if (ratio != 1.0)
+ {
+ delta = (src_samples*ratio-src_samples)*m_dst_rate/m_src_rate;
+ distance = src_samples*m_dst_rate/m_src_rate;
+ m_doesResample = true;
+ }
+
+ if (m_doesResample)
+ {
+ if (swr_set_compensation(m_pContext, delta, distance) < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEResampleFFMPEG::Resample - set compensation failed");
+ return -1;
+ }
+ }
+
+ //! @bug libavresample isn't const correct
+ int ret = swr_convert(m_pContext, dst_buffer, dst_samples, const_cast<const uint8_t**>(src_buffer), src_samples);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CActiveAEResampleFFMPEG::Resample - resample failed");
+ return -1;
+ }
+
+ // special handling for S24 formats which are carried in S32
+ if (m_dst_fmt == AV_SAMPLE_FMT_S32 || m_dst_fmt == AV_SAMPLE_FMT_S32P)
+ {
+ // S24NE3
+ if (m_dst_bits == 24 && m_dst_dither_bits == -8)
+ {
+ int planes = av_sample_fmt_is_planar(m_dst_fmt) ? m_dst_channels : 1;
+ int samples = ret * m_dst_channels / planes;
+ uint8_t *src, *dst;
+ for (int i=0; i<planes; i++)
+ {
+ src = dst = dst_buffer[i];
+ for (int j=0; j<samples; j++)
+ {
+#ifndef WORDS_BIGENDIAN
+ src++;
+#endif
+ *dst++ = *src++;
+ *dst++ = *src++;
+ *dst++ = *src++;
+#ifdef WORDS_BIGENDIAN
+ src++;
+#endif
+ }
+ }
+ }
+ // shift bits if destination format requires it, swr_resamples aligns to the left
+ // Example:
+ // ALSA uses SNE24NE that means 24 bit load in 32 bit package and 0 dither bits
+ // WASAPI uses SNE24NEMSB which is 24 bit load in 32 bit package and 8 dither bits
+ // dither bits are always assumed from the right
+ // FFmpeg internally calculates with S24NEMSB which means, that we need to shift the
+ // data 8 bits to the right in order to get the correct alignment of 0 dither bits
+ // if we want to use ALSA as output. For WASAPI nothing had to be done.
+ // SNE24NEMSB 1 1 1 0 >> 8 = 0 1 1 1 = SNE24NE
+ else if (m_dst_bits != 32 && (m_dst_dither_bits + m_dst_bits) != 32)
+ {
+ int planes = av_sample_fmt_is_planar(m_dst_fmt) ? m_dst_channels : 1;
+ int samples = ret * m_dst_channels / planes;
+ for (int i=0; i<planes; i++)
+ {
+ uint32_t* buf = (uint32_t*)dst_buffer[i];
+ for (int j=0; j<samples; j++)
+ {
+ *buf = *buf >> (32 - m_dst_bits - m_dst_dither_bits);
+ buf++;
+ }
+ }
+ }
+ }
+ return ret;
+}
+
+int64_t CActiveAEResampleFFMPEG::GetDelay(int64_t base)
+{
+ return swr_get_delay(m_pContext, base);
+}
+
+int CActiveAEResampleFFMPEG::GetBufferedSamples()
+{
+ return av_rescale_rnd(swr_get_delay(m_pContext, m_src_rate),
+ m_dst_rate, m_src_rate, AV_ROUND_UP);
+}
+
+int CActiveAEResampleFFMPEG::CalcDstSampleCount(int src_samples, int dst_rate, int src_rate)
+{
+ return av_rescale_rnd(src_samples, dst_rate, src_rate, AV_ROUND_UP);
+}
+
+int CActiveAEResampleFFMPEG::GetSrcBufferSize(int samples)
+{
+ return av_samples_get_buffer_size(NULL, m_src_channels, samples, m_src_fmt, 1);
+}
+
+int CActiveAEResampleFFMPEG::GetDstBufferSize(int samples)
+{
+ return av_samples_get_buffer_size(NULL, m_dst_channels, samples, m_dst_fmt, 1);
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEResampleFFMPEG.h b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEResampleFFMPEG.h
new file mode 100644
index 0000000..5fbce37
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEResampleFFMPEG.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "cores/AudioEngine/Utils/AEChannelInfo.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "cores/AudioEngine/Interfaces/AEResample.h"
+
+extern "C" {
+#include <libavutil/samplefmt.h>
+}
+
+struct SwrContext;
+
+namespace ActiveAE
+{
+
+class CActiveAEResampleFFMPEG : public IAEResample
+{
+public:
+ const char *GetName() override { return "ActiveAEResampleFFMPEG"; }
+ CActiveAEResampleFFMPEG();
+ ~CActiveAEResampleFFMPEG() override;
+ bool Init(SampleConfig dstConfig, SampleConfig srcConfig, bool upmix, bool normalize, double centerMix,
+ CAEChannelInfo *remapLayout, AEQuality quality, bool force_resample) override;
+ int Resample(uint8_t **dst_buffer, int dst_samples, uint8_t **src_buffer, int src_samples, double ratio) override;
+ int64_t GetDelay(int64_t base) override;
+ int GetBufferedSamples() override;
+ bool WantsNewSamples(int samples) override { return GetBufferedSamples() <= samples * 2; }
+ int CalcDstSampleCount(int src_samples, int dst_rate, int src_rate) override;
+ int GetSrcBufferSize(int samples) override;
+ int GetDstBufferSize(int samples) override;
+
+protected:
+ bool m_loaded;
+ bool m_doesResample;
+ uint64_t m_src_chan_layout, m_dst_chan_layout;
+ int m_src_rate, m_dst_rate;
+ int m_src_channels, m_dst_channels;
+ AVSampleFormat m_src_fmt, m_dst_fmt;
+ int m_src_bits, m_dst_bits;
+ int m_src_dither_bits, m_dst_dither_bits;
+ SwrContext *m_pContext;
+ double m_rematrix[AE_CH_MAX][AE_CH_MAX];
+};
+
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESettings.cpp b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESettings.cpp
new file mode 100644
index 0000000..5f9a1fb
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESettings.cpp
@@ -0,0 +1,191 @@
+/*
+ * 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 "ActiveAESettings.h"
+
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/Engines/ActiveAE/ActiveAE.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/StringUtils.h"
+
+#include <mutex>
+
+namespace ActiveAE
+{
+
+CActiveAESettings* CActiveAESettings::m_instance = nullptr;
+
+CActiveAESettings::CActiveAESettings(CActiveAE &ae) : m_audioEngine(ae)
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ std::unique_lock<CCriticalSection> lock(m_cs);
+ m_instance = this;
+
+ std::set<std::string> settingSet;
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_CONFIG);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_SAMPLERATE);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_CHANNELS);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_PROCESSQUALITY);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_ATEMPOTHRESHOLD);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_GUISOUNDMODE);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_STEREOUPMIX);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_AC3PASSTHROUGH);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_AC3TRANSCODE);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_EAC3PASSTHROUGH);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_DTSPASSTHROUGH);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_TRUEHDPASSTHROUGH);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_DTSHDPASSTHROUGH);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_AUDIODEVICE);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGHDEVICE);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_STREAMSILENCE);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_STREAMNOISE);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_MAINTAINORIGINALVOLUME);
+ settingSet.insert(CSettings::SETTING_AUDIOOUTPUT_DTSHDCOREFALLBACK);
+ settings->GetSettingsManager()->RegisterCallback(this, settingSet);
+
+ settings->GetSettingsManager()->RegisterSettingOptionsFiller("aequalitylevels", SettingOptionsAudioQualityLevelsFiller);
+ settings->GetSettingsManager()->RegisterSettingOptionsFiller("audiodevices", SettingOptionsAudioDevicesFiller);
+ settings->GetSettingsManager()->RegisterSettingOptionsFiller("audiodevicespassthrough", SettingOptionsAudioDevicesPassthroughFiller);
+ settings->GetSettingsManager()->RegisterSettingOptionsFiller("audiostreamsilence", SettingOptionsAudioStreamsilenceFiller);
+}
+
+CActiveAESettings::~CActiveAESettings()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ std::unique_lock<CCriticalSection> lock(m_cs);
+ settings->GetSettingsManager()->UnregisterSettingOptionsFiller("aequalitylevels");
+ settings->GetSettingsManager()->UnregisterSettingOptionsFiller("audiodevices");
+ settings->GetSettingsManager()->UnregisterSettingOptionsFiller("audiodevicespassthrough");
+ settings->GetSettingsManager()->UnregisterSettingOptionsFiller("audiostreamsilence");
+ settings->GetSettingsManager()->UnregisterCallback(this);
+ m_instance = nullptr;
+}
+
+void CActiveAESettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+ m_instance->m_audioEngine.OnSettingsChange();
+}
+
+void CActiveAESettings::SettingOptionsAudioDevicesFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ SettingOptionsAudioDevicesFillerGeneral(setting, list, current, false);
+}
+
+void CActiveAESettings::SettingOptionsAudioDevicesPassthroughFiller(
+ const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ SettingOptionsAudioDevicesFillerGeneral(setting, list, current, true);
+}
+
+void CActiveAESettings::SettingOptionsAudioQualityLevelsFiller(
+ const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ std::unique_lock<CCriticalSection> lock(m_instance->m_cs);
+
+ if (m_instance->m_audioEngine.SupportsQualityLevel(AE_QUALITY_LOW))
+ list.emplace_back(g_localizeStrings.Get(13506), AE_QUALITY_LOW);
+ if (m_instance->m_audioEngine.SupportsQualityLevel(AE_QUALITY_MID))
+ list.emplace_back(g_localizeStrings.Get(13507), AE_QUALITY_MID);
+ if (m_instance->m_audioEngine.SupportsQualityLevel(AE_QUALITY_HIGH))
+ list.emplace_back(g_localizeStrings.Get(13508), AE_QUALITY_HIGH);
+ if (m_instance->m_audioEngine.SupportsQualityLevel(AE_QUALITY_REALLYHIGH))
+ list.emplace_back(g_localizeStrings.Get(13509), AE_QUALITY_REALLYHIGH);
+ if (m_instance->m_audioEngine.SupportsQualityLevel(AE_QUALITY_GPU))
+ list.emplace_back(g_localizeStrings.Get(38010), AE_QUALITY_GPU);
+}
+
+void CActiveAESettings::SettingOptionsAudioStreamsilenceFiller(
+ const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ std::unique_lock<CCriticalSection> lock(m_instance->m_cs);
+
+ list.emplace_back(g_localizeStrings.Get(20422),
+ XbmcThreads::EndTime<std::chrono::minutes>::Max().count());
+ list.emplace_back(g_localizeStrings.Get(13551), 0);
+
+ if (m_instance->m_audioEngine.SupportsSilenceTimeout())
+ {
+ list.emplace_back(StringUtils::Format(g_localizeStrings.Get(13554), 1), 1);
+ for (int i = 2; i <= 10; i++)
+ {
+ list.emplace_back(StringUtils::Format(g_localizeStrings.Get(13555), i), i);
+ }
+ }
+}
+
+bool CActiveAESettings::IsSettingVisible(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL || value.empty())
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_instance->m_cs);
+ if (!m_instance)
+ return false;
+
+ return m_instance->m_audioEngine.IsSettingVisible(value);
+}
+
+void CActiveAESettings::SettingOptionsAudioDevicesFillerGeneral(
+ const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ bool passthrough)
+{
+ current = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ std::string firstDevice;
+
+ std::unique_lock<CCriticalSection> lock(m_instance->m_cs);
+
+ bool foundValue = false;
+ AEDeviceList sinkList;
+ m_instance->m_audioEngine.EnumerateOutputDevices(sinkList, passthrough);
+ if (sinkList.empty())
+ list.emplace_back("Error - no devices found", "error");
+ else
+ {
+ for (AEDeviceList::const_iterator sink = sinkList.begin(); sink != sinkList.end(); ++sink)
+ {
+ if (sink == sinkList.begin())
+ firstDevice = sink->second;
+
+ list.emplace_back(sink->first, sink->second);
+
+ if (StringUtils::EqualsNoCase(current, sink->second))
+ foundValue = true;
+ }
+ }
+
+ if (!foundValue)
+ current = firstDevice;
+}
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESettings.h b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESettings.h
new file mode 100644
index 0000000..7f814d0
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESettings.h
@@ -0,0 +1,69 @@
+/*
+ * 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 "settings/lib/ISettingCallback.h"
+#include "threads/CriticalSection.h"
+
+#include <atomic>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CSetting;
+class CAEStreamInfo;
+struct IntegerSettingOption;
+struct StringSettingOption;
+
+namespace ActiveAE
+{
+class CActiveAE;
+
+class CActiveAESettings : public ISettingCallback
+{
+public:
+ CActiveAESettings(CActiveAE &ae);
+ ~CActiveAESettings() override;
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ static void SettingOptionsAudioDevicesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsAudioDevicesPassthroughFiller(
+ const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsAudioQualityLevelsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void SettingOptionsAudioStreamsilenceFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static bool IsSettingVisible(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+protected:
+ static void SettingOptionsAudioDevicesFillerGeneral(
+ const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ bool passthrough);
+
+ CActiveAE &m_audioEngine;
+ CCriticalSection m_cs;
+ static CActiveAESettings* m_instance;
+};
+};
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESink.cpp b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESink.cpp
new file mode 100644
index 0000000..477129f
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESink.cpp
@@ -0,0 +1,1175 @@
+/*
+ * 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 "ActiveAESink.h"
+
+#include "ActiveAE.h"
+#include "cores/AudioEngine/AEResampleFactory.h"
+#include "cores/AudioEngine/Utils/AEBitstreamPacker.h"
+#include "cores/AudioEngine/Utils/AEStreamInfo.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "utils/EndianSwap.h"
+#include "utils/MemUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <new> // for std::bad_alloc
+#include <sstream>
+
+using namespace AE;
+using namespace ActiveAE;
+using namespace std::chrono_literals;
+
+CActiveAESink::CActiveAESink(CEvent *inMsgEvent) :
+ CThread("AESink"),
+ m_controlPort("SinkControlPort", inMsgEvent, &m_outMsgEvent),
+ m_dataPort("SinkDataPort", inMsgEvent, &m_outMsgEvent)
+{
+ m_inMsgEvent = inMsgEvent;
+ m_sink = nullptr;
+ m_stats = nullptr;
+ m_volume = 0.0;
+ m_packer = nullptr;
+ m_streamNoise = true;
+}
+
+void CActiveAESink::Start()
+{
+ if (!IsRunning())
+ {
+ Create();
+ SetPriority(ThreadPriority::ABOVE_NORMAL);
+ }
+}
+
+void CActiveAESink::Dispose()
+{
+ m_bStop = true;
+ m_outMsgEvent.Set();
+ StopThread();
+ m_controlPort.Purge();
+ m_dataPort.Purge();
+
+ if (m_sink)
+ {
+ m_sink->Drain();
+ m_sink->Deinitialize();
+ delete m_sink;
+ m_sink = nullptr;
+ }
+
+ delete m_sampleOfSilence.pkt;
+ m_sampleOfSilence.pkt = nullptr;
+
+ delete m_packer;
+ m_packer = nullptr;
+
+ CAESinkFactory::Cleanup();
+}
+
+AEDeviceType CActiveAESink::GetDeviceType(const std::string &device)
+{
+ std::string dev = device;
+ std::string dri;
+ CAESinkFactory::ParseDevice(dev, dri);
+ for (auto itt = m_sinkInfoList.begin(); itt != m_sinkInfoList.end(); ++itt)
+ {
+ for (AEDeviceInfoList::iterator itt2 = itt->m_deviceInfoList.begin(); itt2 != itt->m_deviceInfoList.end(); ++itt2)
+ {
+ CAEDeviceInfo& info = *itt2;
+ if (info.m_deviceName == dev)
+ return info.m_deviceType;
+ }
+ }
+ return AE_DEVTYPE_PCM;
+}
+
+bool CActiveAESink::HasPassthroughDevice()
+{
+ for (auto itt = m_sinkInfoList.begin(); itt != m_sinkInfoList.end(); ++itt)
+ {
+ for (AEDeviceInfoList::iterator itt2 = itt->m_deviceInfoList.begin(); itt2 != itt->m_deviceInfoList.end(); ++itt2)
+ {
+ CAEDeviceInfo& info = *itt2;
+ if (info.m_deviceType != AE_DEVTYPE_PCM && !info.m_streamTypes.empty())
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CActiveAESink::SupportsFormat(const std::string &device, AEAudioFormat &format)
+{
+ std::string dev = device;
+ std::string dri;
+
+ CAESinkFactory::ParseDevice(dev, dri);
+ for (auto itt = m_sinkInfoList.begin(); itt != m_sinkInfoList.end(); ++itt)
+ {
+ if (dri == itt->m_sinkName)
+ {
+ for (auto itt2 = itt->m_deviceInfoList.begin(); itt2 != itt->m_deviceInfoList.end(); ++itt2)
+ {
+ CAEDeviceInfo& info = *itt2;
+ if (info.m_deviceName == dev)
+ {
+ bool isRaw = format.m_dataFormat == AE_FMT_RAW;
+ bool formatExists = false;
+
+ // PCM sample rate
+ unsigned int samplerate = format.m_sampleRate;
+
+ if (isRaw && info.m_wantsIECPassthrough)
+ {
+ switch (format.m_streamInfo.m_type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ samplerate = 192000;
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ if (format.m_streamInfo.m_sampleRate == 48000 || format.m_streamInfo.m_sampleRate == 96000 || format.m_streamInfo.m_sampleRate == 192000)
+ samplerate = 192000;
+ else
+ samplerate = 176400;
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ samplerate = 192000;
+ break;
+
+ default:
+ break;
+ }
+ AEDataTypeList::iterator iit3;
+ iit3 = find(info.m_streamTypes.begin(), info.m_streamTypes.end(), format.m_streamInfo.m_type);
+ formatExists = (iit3 != info.m_streamTypes.end());
+ }
+ else if (isRaw && !info.m_wantsIECPassthrough)
+ {
+ samplerate = 48000;
+ AEDataTypeList::iterator iit3;
+ iit3 = find(info.m_streamTypes.begin(), info.m_streamTypes.end(), format.m_streamInfo.m_type);
+ formatExists = (iit3 != info.m_streamTypes.end());
+ }
+ else // PCM case
+ {
+ AEDataFormatList::iterator itt3;
+ itt3 = find(info.m_dataFormats.begin(), info.m_dataFormats.end(), format.m_dataFormat);
+ formatExists = (itt3 != info.m_dataFormats.end());
+ }
+
+ // check if samplerate is available
+ if (formatExists)
+ {
+ AESampleRateList::iterator itt4;
+ itt4 = find(info.m_sampleRates.begin(), info.m_sampleRates.end(), samplerate);
+ return itt4 != info.m_sampleRates.end();
+ }
+ else // format is not existent
+ {
+ return false;
+ }
+ }
+ }
+ }
+ }
+ return false;
+}
+
+bool CActiveAESink::NeedIECPacking()
+{
+ std::string dev = m_device;
+ std::string dri;
+
+ CAESinkFactory::ParseDevice(dev, dri);
+ for (auto itt = m_sinkInfoList.begin(); itt != m_sinkInfoList.end(); ++itt)
+ {
+ if (dri == itt->m_sinkName)
+ {
+ for (auto itt2 = itt->m_deviceInfoList.begin(); itt2 != itt->m_deviceInfoList.end(); ++itt2)
+ {
+ CAEDeviceInfo& info = *itt2;
+ if (info.m_deviceName == dev)
+ {
+ return info.m_wantsIECPassthrough;
+ }
+ }
+ }
+ }
+ return true;
+}
+
+bool CActiveAESink::DeviceExist(std::string driver, const std::string& device)
+{
+ if (driver.empty() && m_sink)
+ driver = m_sink->GetName();
+
+ for (const auto& itt : m_sinkInfoList)
+ {
+ if (itt.m_sinkName != driver)
+ continue;
+
+ for (const CAEDeviceInfo& info : itt.m_deviceInfoList)
+ {
+ if (info.m_deviceName == device)
+ return true;
+ }
+ }
+ return false;
+}
+
+enum SINK_STATES
+{
+ S_TOP = 0, // 0
+ S_TOP_UNCONFIGURED, // 1
+ S_TOP_CONFIGURED, // 2
+ S_TOP_CONFIGURED_SUSPEND, // 3
+ S_TOP_CONFIGURED_IDLE, // 4
+ S_TOP_CONFIGURED_PLAY, // 5
+ S_TOP_CONFIGURED_SILENCE, // 6
+};
+
+int SINK_parentStates[] = {
+ -1,
+ 0, //TOP_UNCONFIGURED
+ 0, //TOP_CONFIGURED
+ 2, //TOP_CONFIGURED_SUSPEND
+ 2, //TOP_CONFIGURED_IDLE
+ 2, //TOP_CONFIGURED_PLAY
+ 2, //TOP_CONFIGURED_SILENCE
+};
+
+void CActiveAESink::StateMachine(int signal, Protocol *port, Message *msg)
+{
+ for (int state = m_state; ; state = SINK_parentStates[state])
+ {
+ switch (state)
+ {
+ case S_TOP: // TOP
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case CSinkControlProtocol::CONFIGURE:
+ SinkConfig *data;
+ data = reinterpret_cast<SinkConfig*>(msg->data);
+ if (data)
+ {
+ m_requestedFormat = data->format;
+ m_stats = data->stats;
+ m_device = *(data->device);
+ }
+ m_extError = false;
+ m_extSilenceTimer.Set(0ms);
+ m_extStreaming = false;
+ ReturnBuffers();
+ OpenSink();
+
+ if (!m_extError)
+ {
+ SinkReply reply;
+ reply.format = m_sinkFormat;
+ //! @todo
+ //! use max raw packet size, for now use max size of an IEC packed packet
+ //! maxIECPpacket > maxRawPacket
+ //! for raw packets frameSize is set to 1
+ if (m_requestedFormat.m_dataFormat == AE_FMT_RAW)
+ {
+ reply.format.m_frames = 61440;
+ }
+ reply.cacheTotal = m_sink->GetCacheTotal();
+ reply.latency = m_sink->GetLatency();
+ reply.hasVolume = m_sink->HasVolume();
+ m_state = S_TOP_CONFIGURED_IDLE;
+ m_extTimeout = 10s;
+ m_sinkLatency = (int64_t)(reply.latency * 1000);
+ msg->Reply(CSinkControlProtocol::ACC, &reply, sizeof(SinkReply));
+ }
+ else
+ {
+ m_state = S_TOP_UNCONFIGURED;
+ msg->Reply(CSinkControlProtocol::ERR);
+ }
+ return;
+
+ case CSinkControlProtocol::UNCONFIGURE:
+ ReturnBuffers();
+ if (m_sink)
+ {
+ m_sink->Drain();
+ m_sink->Deinitialize();
+ delete m_sink;
+ m_sink = nullptr;
+ }
+ m_state = S_TOP_UNCONFIGURED;
+ msg->Reply(CSinkControlProtocol::ACC);
+ return;
+
+ case CSinkControlProtocol::FLUSH:
+ ReturnBuffers();
+ msg->Reply(CSinkControlProtocol::ACC);
+ return;
+
+ case CSinkControlProtocol::APPFOCUSED:
+ m_extAppFocused = *(bool*)msg->data;
+ SetSilenceTimer();
+ m_extTimeout = 0ms;
+ return;
+
+ case CSinkControlProtocol::STREAMING:
+ m_extStreaming = *(bool*)msg->data;
+ return;
+
+ case CSinkControlProtocol::SETSILENCETIMEOUT:
+ m_silenceTimeOut = std::chrono::minutes(*reinterpret_cast<int*>(msg->data));
+ return;
+
+ case CSinkControlProtocol::SETNOISETYPE:
+ m_streamNoise = *(bool*)msg->data;
+ return;
+
+ default:
+ break;
+ }
+ }
+ else if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case CSinkDataProtocol::DRAIN:
+ msg->Reply(CSinkDataProtocol::ACC);
+ m_state = S_TOP_UNCONFIGURED;
+ m_extTimeout = 0ms;
+ return;
+ default:
+ break;
+ }
+ }
+ {
+ std::string portName = port == nullptr ? "timer" : port->portName;
+ CLog::Log(LOGWARNING,
+ "CActiveAESink::{} - signal: {} form port: {} not handled for state: {}",
+ __FUNCTION__, signal, portName, m_state);
+ }
+ return;
+
+ case S_TOP_UNCONFIGURED:
+ if (port == nullptr) // timeout
+ {
+ switch (signal)
+ {
+ case CSinkControlProtocol::TIMEOUT:
+ m_extTimeout = 1000ms;
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case CSinkDataProtocol::SAMPLE:
+ CSampleBuffer *samples;
+ samples = *((CSampleBuffer**)msg->data);
+ CThread::Sleep(std::chrono::milliseconds(1000 * samples->pkt->nb_samples /
+ samples->pkt->config.sample_rate));
+ msg->Reply(CSinkDataProtocol::RETURNSAMPLE, &samples, sizeof(CSampleBuffer*));
+ m_extTimeout = 0ms;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case S_TOP_CONFIGURED:
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case CSinkControlProtocol::STREAMING:
+ m_extStreaming = *(bool*)msg->data;
+ SetSilenceTimer();
+ if (!m_extSilenceTimer.IsTimePast())
+ {
+ m_state = S_TOP_CONFIGURED_SILENCE;
+ }
+ m_extTimeout = 0ms;
+ return;
+ case CSinkControlProtocol::VOLUME:
+ m_volume = *(float*)msg->data;
+ m_sink->SetVolume(m_volume);
+ return;
+
+ case CSinkControlProtocol::SETNOISETYPE:
+ {
+ bool streamNoise = *(bool*)msg->data;
+ if (streamNoise != m_streamNoise)
+ {
+ m_streamNoise = streamNoise;
+ GenerateNoise();
+ }
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case CSinkDataProtocol::DRAIN:
+ m_sink->Drain();
+ msg->Reply(CSinkDataProtocol::ACC);
+ m_state = S_TOP_CONFIGURED_IDLE;
+ m_extTimeout = 10s;
+ return;
+ case CSinkDataProtocol::SAMPLE:
+ CSampleBuffer *samples;
+ unsigned int delay;
+ samples = *((CSampleBuffer**)msg->data);
+ delay = OutputSamples(samples);
+ msg->Reply(CSinkDataProtocol::RETURNSAMPLE, &samples, sizeof(CSampleBuffer*));
+ if (m_extError)
+ {
+ m_sink->Deinitialize();
+ delete m_sink;
+ m_sink = nullptr;
+ m_state = S_TOP_CONFIGURED_SUSPEND;
+ m_extTimeout = 0ms;
+ }
+ else
+ {
+ m_state = S_TOP_CONFIGURED_PLAY;
+ m_extTimeout = std::chrono::milliseconds(delay / 2);
+ m_extSilenceTimer.Set(m_extSilenceTimeout);
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case S_TOP_CONFIGURED_SUSPEND:
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case CSinkControlProtocol::STREAMING:
+ m_extStreaming = *(bool*)msg->data;
+ SetSilenceTimer();
+ m_extTimeout = 0ms;
+ return;
+ case CSinkControlProtocol::VOLUME:
+ m_volume = *(float*)msg->data;
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case CSinkDataProtocol::SAMPLE:
+ m_extError = false;
+ OpenSink();
+ if (!m_extError)
+ {
+ OutputSamples(&m_sampleOfSilence);
+ m_state = S_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ m_bStateMachineSelfTrigger = true;
+ }
+ else
+ {
+ m_state = S_TOP_UNCONFIGURED;
+ }
+ return;
+ case CSinkDataProtocol::DRAIN:
+ msg->Reply(CSinkDataProtocol::ACC);
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == nullptr) // timeout
+ {
+ switch (signal)
+ {
+ case CSinkControlProtocol::TIMEOUT:
+ m_extTimeout = 10s;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case S_TOP_CONFIGURED_IDLE:
+ if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case CSinkDataProtocol::SAMPLE:
+ OutputSamples(&m_sampleOfSilence);
+ m_state = S_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ m_bStateMachineSelfTrigger = true;
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == nullptr) // timeout
+ {
+ switch (signal)
+ {
+ case CSinkControlProtocol::TIMEOUT:
+ m_sink->Deinitialize();
+ delete m_sink;
+ m_sink = nullptr;
+ m_state = S_TOP_CONFIGURED_SUSPEND;
+ m_extTimeout = 10s;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case S_TOP_CONFIGURED_PLAY:
+ if (port == nullptr) // timeout
+ {
+ switch (signal)
+ {
+ case CSinkControlProtocol::TIMEOUT:
+ if (!m_extSilenceTimer.IsTimePast())
+ {
+ m_state = S_TOP_CONFIGURED_SILENCE;
+ m_extTimeout = 0ms;
+ }
+ else
+ {
+ m_sink->Drain();
+ m_state = S_TOP_CONFIGURED_IDLE;
+ if (m_extAppFocused)
+ m_extTimeout = 10s;
+ else
+ m_extTimeout = 0ms;
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case S_TOP_CONFIGURED_SILENCE:
+ if (port == nullptr) // timeout
+ {
+ switch (signal)
+ {
+ case CSinkControlProtocol::TIMEOUT:
+ OutputSamples(&m_sampleOfSilence);
+ if (m_extError)
+ {
+ m_sink->Deinitialize();
+ delete m_sink;
+ m_sink = nullptr;
+ m_state = S_TOP_CONFIGURED_SUSPEND;
+ }
+ else
+ m_state = S_TOP_CONFIGURED_PLAY;
+ m_extTimeout = 0ms;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ default: // we are in no state, should not happen
+ CLog::Log(LOGERROR, "CActiveAESink::{} - no valid state: {}", __FUNCTION__, m_state);
+ return;
+ }
+ } // for
+}
+
+void CActiveAESink::Process()
+{
+ Message *msg = nullptr;
+ Protocol *port = nullptr;
+ bool gotMsg;
+ XbmcThreads::EndTime<> timer;
+
+ m_state = S_TOP_UNCONFIGURED;
+ m_extTimeout = 1000ms;
+ m_bStateMachineSelfTrigger = false;
+ m_extAppFocused = true;
+
+ while (!m_bStop)
+ {
+ gotMsg = false;
+ timer.Set(m_extTimeout);
+
+ if (m_bStateMachineSelfTrigger)
+ {
+ m_bStateMachineSelfTrigger = false;
+ // self trigger state machine
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = nullptr;
+ }
+ continue;
+ }
+ // check control port
+ else if (m_controlPort.ReceiveOutMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_controlPort;
+ }
+ // check data port
+ else if (m_dataPort.ReceiveOutMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_dataPort;
+ }
+
+ if (gotMsg)
+ {
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = nullptr;
+ }
+ continue;
+ }
+
+ // wait for message
+ else if (m_outMsgEvent.Wait(m_extTimeout))
+ {
+ m_extTimeout = timer.GetTimeLeft();
+ continue;
+ }
+ // time out
+ else
+ {
+ msg = m_controlPort.GetMessage();
+ msg->signal = CSinkControlProtocol::TIMEOUT;
+ port = 0;
+ // signal timeout to state machine
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = nullptr;
+ }
+ }
+ }
+}
+
+void CActiveAESink::EnumerateSinkList(bool force, std::string driver)
+{
+ if (!m_sinkInfoList.empty() && !force)
+ return;
+
+ if (!CAESinkFactory::HasSinks())
+ return;
+
+ std::vector<AE::AESinkInfo> tmpList(m_sinkInfoList);
+
+ unsigned int c_retry = 4;
+ m_sinkInfoList.clear();
+
+ if (!driver.empty())
+ {
+ for (auto const& info : tmpList)
+ {
+ if (info.m_sinkName != driver)
+ m_sinkInfoList.push_back(info);
+ }
+ }
+
+ CAESinkFactory::EnumerateEx(m_sinkInfoList, false, driver);
+ while (m_sinkInfoList.empty() && c_retry > 0)
+ {
+ CLog::Log(LOGINFO, "No Devices found - retry: {}", c_retry);
+ CThread::Sleep(1500ms);
+ c_retry--;
+ // retry the enumeration
+ CAESinkFactory::EnumerateEx(m_sinkInfoList, true, driver);
+ }
+ CLog::Log(LOGINFO, "Found {} Lists of Devices", m_sinkInfoList.size());
+ PrintSinks(driver);
+}
+
+void CActiveAESink::PrintSinks(std::string& driver)
+{
+ for (auto itt = m_sinkInfoList.begin(); itt != m_sinkInfoList.end(); ++itt)
+ {
+ if (!driver.empty() && itt->m_sinkName != driver)
+ continue;
+
+ CLog::Log(LOGINFO, "Enumerated {} devices:", itt->m_sinkName);
+ int count = 0;
+ for (auto itt2 = itt->m_deviceInfoList.begin(); itt2 != itt->m_deviceInfoList.end(); ++itt2)
+ {
+ CLog::Log(LOGINFO, " Device {}", ++count);
+ CAEDeviceInfo& info = *itt2;
+ std::stringstream ss((std::string)info);
+ std::string line;
+ while(std::getline(ss, line, '\n'))
+ CLog::Log(LOGINFO, " {}", line);
+ }
+ }
+}
+
+void CActiveAESink::EnumerateOutputDevices(AEDeviceList &devices, bool passthrough)
+{
+ EnumerateSinkList(false, "");
+
+ for (auto itt = m_sinkInfoList.begin(); itt != m_sinkInfoList.end(); ++itt)
+ {
+ AESinkInfo sinkInfo = *itt;
+ for (AEDeviceInfoList::iterator itt2 = sinkInfo.m_deviceInfoList.begin(); itt2 != sinkInfo.m_deviceInfoList.end(); ++itt2)
+ {
+ CAEDeviceInfo devInfo = *itt2;
+ if (passthrough && devInfo.m_deviceType == AE_DEVTYPE_PCM)
+ continue;
+
+ // filters devices that should not be shown in the list
+ // of AUDIO DEVICES or AUDIO PASSTHROUGH DEVICES
+ // according to the capabilities informed by each sink
+ if (devInfo.m_onlyPassthrough && !passthrough)
+ continue;
+
+ if (devInfo.m_onlyPCM && passthrough)
+ continue;
+
+ std::string device = sinkInfo.m_sinkName + ":" + devInfo.m_deviceName;
+
+ std::stringstream ss;
+
+ /* add the sink name if we have more then one sink type */
+ if (m_sinkInfoList.size() > 1)
+ ss << sinkInfo.m_sinkName << ": ";
+
+ ss << devInfo.m_displayName;
+ if (!devInfo.m_displayNameExtra.empty())
+ ss << ", " << devInfo.m_displayNameExtra;
+
+ devices.push_back(AEDevice(ss.str(), device));
+ }
+ }
+}
+
+void CActiveAESink::GetDeviceFriendlyName(const std::string& device)
+{
+ m_deviceFriendlyName = "Device not found";
+ /* Match the device and find its friendly name */
+ for (auto itt = m_sinkInfoList.begin(); itt != m_sinkInfoList.end(); ++itt)
+ {
+ AESinkInfo sinkInfo = *itt;
+ for (AEDeviceInfoList::iterator itt2 = sinkInfo.m_deviceInfoList.begin(); itt2 != sinkInfo.m_deviceInfoList.end(); ++itt2)
+ {
+ CAEDeviceInfo& devInfo = *itt2;
+ if (devInfo.m_deviceName == device)
+ {
+ m_deviceFriendlyName = devInfo.m_displayName;
+ break;
+ }
+ }
+ }
+}
+
+void CActiveAESink::OpenSink()
+{
+ // we need a copy of m_device here because ParseDevice and CreateDevice write back
+ // into this variable
+ std::string device = m_device;
+ std::string driver;
+ bool passthrough = (m_requestedFormat.m_dataFormat == AE_FMT_RAW);
+
+ CAESinkFactory::ParseDevice(device, driver);
+ if (driver.empty() && m_sink)
+ driver = m_sink->GetName();
+
+ // iec packing or raw
+ if (passthrough)
+ {
+ m_needIecPack = NeedIECPacking();
+ if (m_needIecPack)
+ {
+ m_packer = new CAEBitstreamPacker();
+ m_requestedFormat.m_sampleRate = CAEBitstreamPacker::GetOutputRate(m_requestedFormat.m_streamInfo);
+ m_requestedFormat.m_channelLayout = CAEBitstreamPacker::GetOutputChannelMap(m_requestedFormat.m_streamInfo);
+ }
+ }
+
+ CLog::Log(LOGINFO, "CActiveAESink::OpenSink - initialize sink");
+
+ if (m_sink)
+ {
+ m_sink->Drain();
+ m_sink->Deinitialize();
+ delete m_sink;
+ m_sink = nullptr;
+ }
+
+ // get the display name of the device
+ GetDeviceFriendlyName(device);
+
+ // if we already have a driver, prepend it to the device string
+ if (!driver.empty())
+ device = driver + ":" + device;
+
+ // WARNING: this changes format and does not use passthrough
+ m_sinkFormat = m_requestedFormat;
+ CLog::Log(LOGDEBUG, "CActiveAESink::OpenSink - trying to open device {}", device);
+ m_sink = CAESinkFactory::Create(device, m_sinkFormat);
+
+ // try first device in out list
+ if (!m_sink && !m_sinkInfoList.empty())
+ {
+ driver = m_sinkInfoList.front().m_sinkName;
+ device = m_sinkInfoList.front().m_deviceInfoList.front().m_deviceName;
+ GetDeviceFriendlyName(device);
+ if (!driver.empty())
+ device = driver + ":" + device;
+ m_sinkFormat = m_requestedFormat;
+ CLog::Log(LOGDEBUG, "CActiveAESink::OpenSink - trying to open device {}", device);
+ m_sink = CAESinkFactory::Create(device, m_sinkFormat);
+ }
+
+ if (!m_sink)
+ {
+ CLog::Log(LOGERROR, "CActiveAESink::OpenSink - no sink was returned");
+ m_extError = true;
+ return;
+ }
+
+ m_sink->SetVolume(m_volume);
+
+#ifdef WORDS_BIGENDIAN
+ if (m_sinkFormat.m_dataFormat == AE_FMT_S16BE)
+ m_sinkFormat.m_dataFormat = AE_FMT_S16NE;
+ else if (m_sinkFormat.m_dataFormat == AE_FMT_S32BE)
+ m_sinkFormat.m_dataFormat = AE_FMT_S32NE;
+#else
+ if (m_sinkFormat.m_dataFormat == AE_FMT_S16LE)
+ m_sinkFormat.m_dataFormat = AE_FMT_S16NE;
+ else if (m_sinkFormat.m_dataFormat == AE_FMT_S32LE)
+ m_sinkFormat.m_dataFormat = AE_FMT_S32NE;
+#endif
+
+ CLog::Log(LOGDEBUG, "CActiveAESink::OpenSink - {} Initialized:", m_sink->GetName());
+ CLog::Log(LOGDEBUG, " Output Device : {}", m_deviceFriendlyName);
+ CLog::Log(LOGDEBUG, " Sample Rate : {}", m_sinkFormat.m_sampleRate);
+ CLog::Log(LOGDEBUG, " Sample Format : {}", CAEUtil::DataFormatToStr(m_sinkFormat.m_dataFormat));
+ CLog::Log(LOGDEBUG, " Channel Count : {}", m_sinkFormat.m_channelLayout.Count());
+ CLog::Log(LOGDEBUG, " Channel Layout: {}", ((std::string)m_sinkFormat.m_channelLayout));
+ CLog::Log(LOGDEBUG, " Frames : {}", m_sinkFormat.m_frames);
+ CLog::Log(LOGDEBUG, " Frame Size : {}", m_sinkFormat.m_frameSize);
+
+ // init sample of silence
+ SampleConfig config;
+ config.fmt = CAEUtil::GetAVSampleFormat(m_sinkFormat.m_dataFormat);
+ config.bits_per_sample = CAEUtil::DataFormatToUsedBits(m_sinkFormat.m_dataFormat);
+ config.dither_bits = CAEUtil::DataFormatToDitherBits(m_sinkFormat.m_dataFormat);
+ config.channel_layout = CAEUtil::GetAVChannelLayout(m_sinkFormat.m_channelLayout);
+ config.channels = m_sinkFormat.m_channelLayout.Count();
+ config.sample_rate = m_sinkFormat.m_sampleRate;
+
+ // init sample of silence/noise
+ delete m_sampleOfSilence.pkt;
+ m_sampleOfSilence.pkt = new CSoundPacket(config, m_sinkFormat.m_frames);
+ m_sampleOfSilence.pkt->nb_samples = m_sampleOfSilence.pkt->max_nb_samples;
+ if (!passthrough)
+ GenerateNoise();
+ else
+ {
+ m_sampleOfSilence.pkt->nb_samples = 0;
+ m_sampleOfSilence.pkt->pause_burst_ms = m_sinkFormat.m_streamInfo.GetDuration();
+ }
+
+ m_swapState = CHECK_SWAP;
+}
+
+void CActiveAESink::ReturnBuffers()
+{
+ Message *msg = nullptr;
+ CSampleBuffer *samples;
+ while (m_dataPort.ReceiveOutMessage(&msg))
+ {
+ if (msg->signal == CSinkDataProtocol::SAMPLE)
+ {
+ samples = *((CSampleBuffer**)msg->data);
+ msg->Reply(CSinkDataProtocol::RETURNSAMPLE, &samples, sizeof(CSampleBuffer*));
+ }
+ msg->Release();
+ }
+}
+
+unsigned int CActiveAESink::OutputSamples(CSampleBuffer* samples)
+{
+ uint8_t **buffer = samples->pkt->data;
+ uint8_t *packBuffer;
+ unsigned int frames = samples->pkt->nb_samples;
+ unsigned int totalFrames = frames;
+ unsigned int maxFrames;
+ int retry = 0;
+ unsigned int written = 0;
+ uint8_t* p_mergeBuffer = nullptr;
+ AEDelayStatus status;
+
+ if (m_requestedFormat.m_dataFormat == AE_FMT_RAW)
+ {
+ bool skipSwap = false;
+ if (m_needIecPack)
+ {
+ if (frames > 0)
+ {
+ m_packer->Reset();
+ if (m_sinkFormat.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD)
+ {
+ if (frames == 61440)
+ {
+ for (int i = 0, of = 0; i < 12; i++)
+ {
+ // calculates length of each audio unit using raw data of stream
+ const uint16_t len = ((*(buffer[0] + of) & 0x0F) << 8 | *(buffer[0] + of + 1)) << 1;
+
+ m_packer->Pack(m_sinkFormat.m_streamInfo, buffer[0] + of, len);
+ of += len;
+ }
+ }
+ else
+ {
+ m_extError = true;
+ CLog::Log(LOGERROR, "CActiveAESink::OutputSamples - incomplete TrueHD buffer");
+ return 0;
+ }
+ }
+ else
+ m_packer->Pack(m_sinkFormat.m_streamInfo, buffer[0], frames);
+ }
+ else if (samples->pkt->pause_burst_ms > 0)
+ {
+ // construct a pause burst if we have already output valid audio
+ bool burst = m_extStreaming && (m_packer->GetBuffer()[0] != 0);
+ if (!m_packer->PackPause(m_sinkFormat.m_streamInfo, samples->pkt->pause_burst_ms, burst))
+ skipSwap = true;
+ }
+ else
+ m_packer->Reset();
+
+ unsigned int size = m_packer->GetSize();
+ packBuffer = m_packer->GetBuffer();
+ buffer = &packBuffer;
+ totalFrames = size / m_sinkFormat.m_frameSize;
+ frames = totalFrames;
+
+ switch(m_swapState)
+ {
+ case SKIP_SWAP:
+ break;
+ case NEED_BYTESWAP:
+ if (!skipSwap)
+ Endian_Swap16_buf((uint16_t *)buffer[0], (uint16_t *)buffer[0], size / 2);
+ break;
+ case CHECK_SWAP:
+ SwapInit(samples);
+ if (m_swapState == NEED_BYTESWAP)
+ Endian_Swap16_buf((uint16_t *)buffer[0], (uint16_t *)buffer[0], size / 2);
+ break;
+ default:
+ break;
+ }
+ }
+ else // Android IEC packer (RAW)
+ {
+ if (m_sinkFormat.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD && frames == 61440)
+ {
+ if (m_mergeBuffer.empty())
+ m_mergeBuffer.resize(MAX_IEC61937_PACKET);
+
+ p_mergeBuffer = m_mergeBuffer.data();
+ unsigned int size = 0;
+
+ for (int i = 0, of = 0; i < 24; i++)
+ {
+ // calculates length of each audio unit using raw data of stream
+ const uint16_t len = ((*(buffer[0] + of) & 0x0F) << 8 | *(buffer[0] + of + 1)) << 1;
+
+ memcpy(m_mergeBuffer.data() + of, buffer[0] + of, len);
+ size += len;
+ of += len;
+ }
+
+ buffer = &p_mergeBuffer;
+ totalFrames = size / m_sinkFormat.m_frameSize; // m_frameSize = 1
+ frames = totalFrames;
+ }
+ if (samples->pkt->pause_burst_ms > 0)
+ {
+ m_sink->AddPause(samples->pkt->pause_burst_ms);
+ m_sink->GetDelay(status);
+ m_stats->UpdateSinkDelay(status, samples->pool ? 1 : 0);
+ return status.delay * 1000;
+ }
+ }
+ }
+
+ int framesOrPackets;
+
+ while (frames > 0)
+ {
+ maxFrames = std::min(frames, m_sinkFormat.m_frames);
+ written = m_sink->AddPackets(buffer, maxFrames, totalFrames - frames);
+ if (written == 0)
+ {
+ CThread::Sleep(
+ std::chrono::milliseconds(500 * m_sinkFormat.m_frames / m_sinkFormat.m_sampleRate));
+ retry++;
+ if (retry > 4)
+ {
+ m_extError = true;
+ CLog::Log(LOGERROR, "CActiveAESink::OutputSamples - failed");
+ status.SetDelay(0);
+ framesOrPackets = frames;
+ if (m_requestedFormat.m_dataFormat == AE_FMT_RAW)
+ framesOrPackets = 1;
+ m_stats->UpdateSinkDelay(status, samples->pool ? framesOrPackets : 0);
+ return 0;
+ }
+ else
+ continue;
+ }
+ else if (written > maxFrames)
+ {
+ m_extError = true;
+ CLog::Log(LOGERROR, "CActiveAESink::OutputSamples - sink returned error");
+ status.SetDelay(0);
+ framesOrPackets = frames;
+ if (m_requestedFormat.m_dataFormat == AE_FMT_RAW)
+ framesOrPackets = 1;
+ m_stats->UpdateSinkDelay(status, samples->pool ? framesOrPackets : 0);
+ return 0;
+ }
+ frames -= written;
+
+ m_sink->GetDelay(status);
+
+ if (m_requestedFormat.m_dataFormat != AE_FMT_RAW)
+ m_stats->UpdateSinkDelay(status, samples->pool ? written : 0);
+ }
+
+ if (m_requestedFormat.m_dataFormat == AE_FMT_RAW)
+ m_stats->UpdateSinkDelay(status, samples->pool ? 1 : 0);
+
+ return status.delay * 1000;
+}
+
+void CActiveAESink::SwapInit(CSampleBuffer* samples)
+{
+ if ((m_requestedFormat.m_dataFormat == AE_FMT_RAW) && CAEUtil::S16NeedsByteSwap(AE_FMT_S16NE, m_sinkFormat.m_dataFormat))
+ {
+ m_swapState = NEED_BYTESWAP;
+ }
+ else
+ m_swapState = SKIP_SWAP;
+}
+
+#define PI 3.1415926536f
+
+void CActiveAESink::GenerateNoise()
+{
+ int nb_floats = m_sampleOfSilence.pkt->max_nb_samples;
+ nb_floats *= m_sampleOfSilence.pkt->config.channels;
+ size_t size = nb_floats*sizeof(float);
+
+ float *noise = static_cast<float*>(KODI::MEMORY::AlignedMalloc(size, 32));
+ if (!noise)
+ throw std::bad_alloc();
+
+ if (!m_streamNoise)
+ memset(noise, 0, size);
+ else
+ {
+ float R1, R2;
+ for(int i = 0; i < nb_floats; i++)
+ {
+ do
+ {
+ R1 = (float) rand() / (float) RAND_MAX;
+ R2 = (float) rand() / (float) RAND_MAX;
+ }
+ while(R1 == 0.0f);
+
+ noise[i] = sqrt( -2.0f * log( R1 )) * cos( 2.0f * PI * R2 ) * 0.00001f;
+ }
+ }
+
+ SampleConfig config = m_sampleOfSilence.pkt->config;
+ IAEResample *resampler = CAEResampleFactory::Create(AERESAMPLEFACTORY_QUICK_RESAMPLE);
+
+ SampleConfig dstConfig, srcConfig;
+ dstConfig.channel_layout = config.channel_layout;
+ dstConfig.channels = config.channels;
+ dstConfig.sample_rate = config.sample_rate;
+ dstConfig.fmt = config.fmt;
+ dstConfig.bits_per_sample = config.bits_per_sample;
+ dstConfig.dither_bits = config.dither_bits;
+
+ srcConfig.channel_layout = config.channel_layout;
+ srcConfig.channels = config.channels;
+ srcConfig.sample_rate = config.sample_rate;
+ srcConfig.fmt = AV_SAMPLE_FMT_FLT;
+ srcConfig.bits_per_sample = CAEUtil::DataFormatToUsedBits(m_sinkFormat.m_dataFormat);
+ srcConfig.dither_bits = CAEUtil::DataFormatToDitherBits(m_sinkFormat.m_dataFormat);
+
+ resampler->Init(dstConfig, srcConfig,
+ false, false, M_SQRT1_2, nullptr, AE_QUALITY_UNKNOWN, false);
+
+ resampler->Resample(m_sampleOfSilence.pkt->data, m_sampleOfSilence.pkt->max_nb_samples,
+ (uint8_t**)&noise, m_sampleOfSilence.pkt->max_nb_samples, 1.0);
+
+ KODI::MEMORY::AlignedFree(noise);
+ delete resampler;
+}
+
+void CActiveAESink::SetSilenceTimer()
+{
+ if (m_extStreaming)
+ m_extSilenceTimeout = XbmcThreads::EndTime<decltype(m_extSilenceTimeout)>::Max();
+ else if (m_extAppFocused) // handles no playback/GUI and playback in pause and seek
+ {
+ // only true with AudioTrack RAW + passthrough + TrueHD or EAC3 (DD+)
+ const bool noSilenceOnPause =
+ !m_needIecPack && m_requestedFormat.m_dataFormat == AE_FMT_RAW &&
+ (m_sinkFormat.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD ||
+ m_sinkFormat.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_EAC3);
+
+ m_extSilenceTimeout = (noSilenceOnPause) ? 0ms : m_silenceTimeOut;
+ }
+ else
+ {
+ m_extSilenceTimeout = 0ms;
+ }
+
+ m_extSilenceTimer.Set(m_extSilenceTimeout);
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESink.h b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESink.h
new file mode 100644
index 0000000..ee1253b
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESink.h
@@ -0,0 +1,158 @@
+/*
+ * 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 "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "cores/AudioEngine/Interfaces/AESink.h"
+#include "threads/Event.h"
+#include "threads/SystemClock.h"
+#include "threads/Thread.h"
+#include "utils/ActorProtocol.h"
+
+#include <utility>
+
+class CAEBitstreamPacker;
+
+namespace ActiveAE
+{
+using namespace Actor;
+
+class CEngineStats;
+
+struct SinkConfig
+{
+ AEAudioFormat format;
+ CEngineStats *stats;
+ const std::string *device;
+};
+
+struct SinkReply
+{
+ AEAudioFormat format;
+ float cacheTotal;
+ float latency;
+ bool hasVolume;
+};
+
+class CSinkControlProtocol : public Protocol
+{
+public:
+ CSinkControlProtocol(std::string name, CEvent* inEvent, CEvent* outEvent)
+ : Protocol(std::move(name), inEvent, outEvent)
+ {
+ }
+ enum OutSignal
+ {
+ CONFIGURE,
+ UNCONFIGURE,
+ STREAMING,
+ APPFOCUSED,
+ VOLUME,
+ FLUSH,
+ TIMEOUT,
+ SETSILENCETIMEOUT,
+ SETNOISETYPE,
+ };
+ enum InSignal
+ {
+ ACC,
+ ERR,
+ STATS,
+ };
+};
+
+class CSinkDataProtocol : public Protocol
+{
+public:
+ CSinkDataProtocol(std::string name, CEvent* inEvent, CEvent* outEvent)
+ : Protocol(std::move(name), inEvent, outEvent)
+ {
+ }
+ enum OutSignal
+ {
+ SAMPLE = 0,
+ DRAIN,
+ };
+ enum InSignal
+ {
+ RETURNSAMPLE,
+ ACC,
+ };
+};
+
+class CActiveAESink : private CThread
+{
+public:
+ explicit CActiveAESink(CEvent *inMsgEvent);
+ void EnumerateSinkList(bool force, std::string driver);
+ void EnumerateOutputDevices(AEDeviceList &devices, bool passthrough);
+ void Start();
+ void Dispose();
+ AEDeviceType GetDeviceType(const std::string &device);
+ bool HasPassthroughDevice();
+ bool SupportsFormat(const std::string &device, AEAudioFormat &format);
+ bool DeviceExist(std::string driver, const std::string& device);
+ bool NeedIecPack() const { return m_needIecPack; }
+ CSinkControlProtocol m_controlPort;
+ CSinkDataProtocol m_dataPort;
+
+protected:
+ void Process() override;
+ void StateMachine(int signal, Protocol *port, Message *msg);
+ void PrintSinks(std::string& driver);
+ void GetDeviceFriendlyName(const std::string& device);
+ void OpenSink();
+ void ReturnBuffers();
+ void SetSilenceTimer();
+ bool NeedIECPacking();
+
+ unsigned int OutputSamples(CSampleBuffer* samples);
+ void SwapInit(CSampleBuffer* samples);
+
+ void GenerateNoise();
+
+ CEvent m_outMsgEvent;
+ CEvent *m_inMsgEvent;
+ int m_state;
+ bool m_bStateMachineSelfTrigger;
+ std::chrono::milliseconds m_extTimeout;
+ std::chrono::minutes m_silenceTimeOut{std::chrono::minutes::zero()};
+ bool m_extError;
+ std::chrono::milliseconds m_extSilenceTimeout;
+ bool m_extAppFocused;
+ bool m_extStreaming;
+ XbmcThreads::EndTime<> m_extSilenceTimer;
+
+ CSampleBuffer m_sampleOfSilence;
+ enum
+ {
+ CHECK_SWAP,
+ NEED_CONVERT,
+ NEED_BYTESWAP,
+ SKIP_SWAP,
+ } m_swapState;
+
+ std::vector<uint8_t> m_mergeBuffer;
+
+ std::string m_deviceFriendlyName;
+ std::string m_device;
+ std::vector<AE::AESinkInfo> m_sinkInfoList;
+ IAESink *m_sink;
+ AEAudioFormat m_sinkFormat, m_requestedFormat;
+ CEngineStats *m_stats;
+ float m_volume;
+ int m_sinkLatency;
+ CAEBitstreamPacker *m_packer;
+ bool m_needIecPack{false};
+ bool m_streamNoise;
+};
+
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESound.cpp b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESound.cpp
new file mode 100644
index 0000000..ee1ed70
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESound.cpp
@@ -0,0 +1,158 @@
+/*
+ * 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 "ActiveAESound.h"
+
+#include "ActiveAE.h"
+#include "cores/AudioEngine/Interfaces/AESound.h"
+#include "filesystem/File.h"
+#include "utils/log.h"
+
+extern "C" {
+#include <libavutil/avutil.h>
+}
+
+using namespace ActiveAE;
+using namespace XFILE;
+
+CActiveAESound::CActiveAESound(const std::string &filename, CActiveAE *ae) :
+ IAESound (filename),
+ m_filename (filename),
+ m_volume (1.0f ),
+ m_channel (AE_CH_NULL)
+{
+ m_orig_sound = NULL;
+ m_dst_sound = NULL;
+ m_pFile = NULL;
+ m_isSeekPossible = false;
+ m_fileSize = 0;
+ m_isConverted = false;
+ m_activeAE = ae;
+}
+
+CActiveAESound::~CActiveAESound()
+{
+ delete m_orig_sound;
+ delete m_dst_sound;
+ Finish();
+}
+
+void CActiveAESound::Play()
+{
+ m_activeAE->PlaySound(this);
+
+}
+
+void CActiveAESound::Stop()
+{
+ m_activeAE->StopSound(this);
+}
+
+bool CActiveAESound::IsPlaying()
+{
+ //! @todo implement
+ return false;
+}
+
+uint8_t** CActiveAESound::InitSound(bool orig, SampleConfig config, int nb_samples)
+{
+ CSoundPacket **info;
+ if (orig)
+ info = &m_orig_sound;
+ else
+ info = &m_dst_sound;
+
+ delete *info;
+ *info = new CSoundPacket(config, nb_samples);
+
+ (*info)->nb_samples = 0;
+ m_isConverted = false;
+ return (*info)->data;
+}
+
+bool CActiveAESound::StoreSound(bool orig, uint8_t **buffer, int samples, int linesize)
+{
+ CSoundPacket **info;
+ if (orig)
+ info = &m_orig_sound;
+ else
+ info = &m_dst_sound;
+
+ if ((*info)->nb_samples + samples > (*info)->max_nb_samples)
+ {
+ CLog::Log(LOGERROR, "CActiveAESound::StoreSound - exceeded max samples");
+ return false;
+ }
+
+ int bytes_to_copy = samples * (*info)->bytes_per_sample * (*info)->config.channels;
+ bytes_to_copy /= (*info)->planes;
+ int start = (*info)->nb_samples * (*info)->bytes_per_sample * (*info)->config.channels;
+ start /= (*info)->planes;
+
+ for (int i=0; i<(*info)->planes; i++)
+ {
+ memcpy((*info)->data[i]+start, buffer[i], bytes_to_copy);
+ }
+ (*info)->nb_samples += samples;
+
+ return true;
+}
+
+CSoundPacket *CActiveAESound::GetSound(bool orig)
+{
+ if (orig)
+ return m_orig_sound;
+ else
+ return m_dst_sound;
+}
+
+bool CActiveAESound::Prepare()
+{
+ unsigned int flags = READ_TRUNCATED | READ_CHUNKED;
+ m_pFile = new CFile();
+
+ if (!m_pFile->Open(m_filename, flags))
+ {
+ delete m_pFile;
+ m_pFile = NULL;
+ return false;
+ }
+ m_isSeekPossible = m_pFile->IoControl(IOCTRL_SEEK_POSSIBLE, NULL) != 0;
+ m_fileSize = m_pFile->GetLength();
+ return true;
+}
+
+void CActiveAESound::Finish()
+{
+ delete m_pFile;
+ m_pFile = NULL;
+}
+
+int CActiveAESound::GetChunkSize()
+{
+ return m_pFile->GetChunkSize();
+}
+
+int CActiveAESound::Read(void *h, uint8_t* buf, int size)
+{
+ CFile *pFile = static_cast<CActiveAESound*>(h)->m_pFile;
+ int len = pFile->Read(buf, size);
+ if (len == 0)
+ return AVERROR_EOF;
+ else
+ return len;
+}
+
+int64_t CActiveAESound::Seek(void *h, int64_t pos, int whence)
+{
+ CFile* pFile = static_cast<CActiveAESound*>(h)->m_pFile;
+ if(whence == AVSEEK_SIZE)
+ return pFile->GetLength();
+ else
+ return pFile->Seek(pos, whence & ~AVSEEK_FORCE);
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESound.h b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESound.h
new file mode 100644
index 0000000..0bde021
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAESound.h
@@ -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.
+ */
+
+#pragma once
+
+#include "cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.h"
+#include "cores/AudioEngine/Interfaces/AESound.h"
+
+class DllAvUtil;
+
+namespace XFILE
+{
+class CFile;
+}
+
+namespace ActiveAE
+{
+
+class CActiveAE;
+
+class CActiveAESound : public IAESound
+{
+public:
+ CActiveAESound (const std::string &filename, CActiveAE *ae);
+ ~CActiveAESound() override;
+
+ void Play() override;
+ void Stop() override;
+ bool IsPlaying() override;
+
+ void SetChannel(AEChannel channel) override { m_channel = channel; }
+ AEChannel GetChannel() override { return m_channel; }
+ void SetVolume(float volume) override { m_volume = std::max(0.0f, std::min(1.0f, volume)); }
+ float GetVolume() override { return m_volume; }
+
+ uint8_t** InitSound(bool orig, SampleConfig config, int nb_samples);
+ bool StoreSound(bool orig, uint8_t **buffer, int samples, int linesize);
+ CSoundPacket *GetSound(bool orig);
+
+ bool IsConverted() { return m_isConverted; }
+ void SetConverted(bool state) { m_isConverted = state; }
+
+ bool Prepare();
+ void Finish();
+ int GetChunkSize();
+ int GetFileSize() { return m_fileSize; }
+ bool IsSeekPossible() { return m_isSeekPossible; }
+
+ static int Read(void *h, uint8_t* buf, int size);
+ static int64_t Seek(void *h, int64_t pos, int whence);
+
+protected:
+ CActiveAE *m_activeAE;
+ std::string m_filename;
+ XFILE::CFile *m_pFile;
+ bool m_isSeekPossible;
+ int m_fileSize;
+ float m_volume;
+ AEChannel m_channel;
+
+ CSoundPacket *m_orig_sound;
+ CSoundPacket *m_dst_sound;
+
+ bool m_isConverted;
+};
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEStream.cpp b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEStream.cpp
new file mode 100644
index 0000000..858b0f2
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEStream.cpp
@@ -0,0 +1,797 @@
+/*
+ * 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 "ActiveAEStream.h"
+
+#include "ActiveAE.h"
+#include "cores/AudioEngine/AEResampleFactory.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace ActiveAE;
+using namespace std::chrono_literals;
+
+CActiveAEStream::CActiveAEStream(AEAudioFormat* format, unsigned int streamid, CActiveAE* ae)
+ : m_format(*format)
+{
+ m_activeAE = ae;
+ m_id = streamid;
+ m_bufferedTime = 0;
+ m_currentBuffer = NULL;
+ m_drain = false;
+ m_paused = false;
+ m_rgain = 1.0;
+ m_volume = 1.0;
+ SetVolume(1.0);
+ m_amplify = 1.0;
+ m_streamSpace = m_format.m_frameSize * m_format.m_frames;
+ m_streamDraining = false;
+ m_streamDrained = false;
+ m_streamFading = false;
+ m_streamFreeBuffers = 0;
+ m_streamIsBuffering = false;
+ m_streamIsFlushed = false;
+ m_streamSlave = NULL;
+ m_leftoverBuffer = new uint8_t[m_format.m_frameSize];
+ m_leftoverBytes = 0;
+ m_forceResampler = false;
+ m_remapper = NULL;
+ m_remapBuffer = NULL;
+ m_streamResampleRatio = 1.0;
+ m_streamResampleMode = 0;
+ m_profile = 0;
+ m_matrixEncoding = AV_MATRIX_ENCODING_NONE;
+ m_audioServiceType = AV_AUDIO_SERVICE_TYPE_MAIN;
+ m_pClock = NULL;
+ m_lastPts = 0;
+ m_lastPtsJump = 0;
+ m_clockSpeed = 1.0;
+}
+
+CActiveAEStream::~CActiveAEStream()
+{
+ delete [] m_leftoverBuffer;
+ delete m_remapper;
+ delete m_remapBuffer;
+}
+
+void CActiveAEStream::IncFreeBuffers()
+{
+ std::unique_lock<CCriticalSection> lock(m_streamLock);
+ m_streamFreeBuffers++;
+}
+
+void CActiveAEStream::DecFreeBuffers()
+{
+ std::unique_lock<CCriticalSection> lock(m_streamLock);
+ m_streamFreeBuffers--;
+}
+
+void CActiveAEStream::ResetFreeBuffers()
+{
+ std::unique_lock<CCriticalSection> lock(m_streamLock);
+ m_streamFreeBuffers = 0;
+}
+
+void CActiveAEStream::InitRemapper()
+{
+ // check if input format follows ffmpeg channel mask
+ bool needRemap = false;
+ unsigned int avLast, avCur = 0;
+ for(unsigned int i=0; i<m_format.m_channelLayout.Count(); i++)
+ {
+ avLast = avCur;
+ avCur = CAEUtil::GetAVChannel(m_format.m_channelLayout[i]);
+ if(avCur < avLast)
+ {
+ needRemap = true;
+ break;
+ }
+ }
+
+ if(needRemap)
+ {
+ CLog::Log(LOGDEBUG, "CActiveAEStream::{} - initialize remapper", __FUNCTION__);
+
+ m_remapper = CAEResampleFactory::Create();
+ uint64_t avLayout = CAEUtil::GetAVChannelLayout(m_format.m_channelLayout);
+
+ // build layout according to ffmpeg channel order
+ // we need this for reference
+ CAEChannelInfo ffmpegLayout;
+ ffmpegLayout.Reset();
+ int idx = 0;
+ for(unsigned int i=0; i<m_format.m_channelLayout.Count(); i++)
+ {
+ for(unsigned int j=0; j<m_format.m_channelLayout.Count(); j++)
+ {
+ idx = CAEUtil::GetAVChannelIndex(m_format.m_channelLayout[j], avLayout);
+ if (idx == (int)i)
+ {
+ ffmpegLayout += m_format.m_channelLayout[j];
+ break;
+ }
+ }
+ }
+
+ // build remap layout we can pass to resampler as destination layout
+ CAEChannelInfo remapLayout;
+ remapLayout.Reset();
+ for(unsigned int i=0; i<m_format.m_channelLayout.Count(); i++)
+ {
+ for(unsigned int j=0; j<m_format.m_channelLayout.Count(); j++)
+ {
+ idx = CAEUtil::GetAVChannelIndex(m_format.m_channelLayout[j], avLayout);
+ if (idx == (int)i)
+ {
+ remapLayout += ffmpegLayout[j];
+ break;
+ }
+ }
+ }
+
+ // initialize resampler for only doing remapping
+ SampleConfig dstConfig, srcConfig;
+ dstConfig.channel_layout = avLayout;
+ dstConfig.channels = m_format.m_channelLayout.Count();
+ dstConfig.sample_rate = m_format.m_sampleRate;
+ dstConfig.fmt = CAEUtil::GetAVSampleFormat(m_format.m_dataFormat);
+ dstConfig.bits_per_sample = CAEUtil::DataFormatToUsedBits(m_format.m_dataFormat);
+ dstConfig.dither_bits = CAEUtil::DataFormatToDitherBits(m_format.m_dataFormat);
+
+ srcConfig.channel_layout = avLayout;
+ srcConfig.channels = m_format.m_channelLayout.Count();
+ srcConfig.sample_rate = m_format.m_sampleRate;
+ srcConfig.fmt = CAEUtil::GetAVSampleFormat(m_format.m_dataFormat);
+ srcConfig.bits_per_sample = CAEUtil::DataFormatToUsedBits(m_format.m_dataFormat);
+ srcConfig.dither_bits = CAEUtil::DataFormatToDitherBits(m_format.m_dataFormat);
+
+ m_remapper->Init(dstConfig, srcConfig,
+ false,
+ false,
+ M_SQRT1_2,
+ &remapLayout,
+ AE_QUALITY_LOW, // not used for remapping
+ false);
+
+ // extra sound packet, we can't resample to the same buffer
+ m_remapBuffer = new CSoundPacket(m_inputBuffers->m_allSamples[0]->pkt->config, m_inputBuffers->m_allSamples[0]->pkt->max_nb_samples);
+ }
+}
+
+void CActiveAEStream::RemapBuffer()
+{
+ if(m_remapper)
+ {
+ int samples = m_remapper->Resample(m_remapBuffer->data, m_remapBuffer->max_nb_samples,
+ m_currentBuffer->pkt->data, m_currentBuffer->pkt->nb_samples,
+ 1.0);
+
+ if (samples != m_currentBuffer->pkt->nb_samples)
+ {
+ CLog::Log(LOGERROR, "CActiveAEStream::{} - error remapping", __FUNCTION__);
+ }
+
+ // swap sound packets
+ CSoundPacket *tmp = m_currentBuffer->pkt;
+ m_currentBuffer->pkt = m_remapBuffer;
+ m_remapBuffer = tmp;
+ }
+}
+
+double CActiveAEStream::CalcResampleRatio(double error)
+{
+ //reset the integral on big errors, failsafe
+ if (fabs(error) > 1000)
+ m_resampleIntegral = 0;
+ else if (fabs(error) > 5)
+ m_resampleIntegral += error / 1000 / 50;
+
+ double proportional = 0.0;
+
+ double proportionaldiv = 2.0;
+ proportional = error / GetErrorInterval().count() / proportionaldiv;
+
+ double clockspeed = 1.0;
+ if (m_pClock)
+ {
+ clockspeed = m_pClock->GetClockSpeed();
+ if (m_clockSpeed != clockspeed)
+ m_resampleIntegral = 0;
+ m_clockSpeed = clockspeed;
+ }
+
+ double ret = 1.0 / clockspeed + proportional + m_resampleIntegral;
+ //CLog::Log(LOGINFO,"----- error: {:f}, rr: {:f}, prop: {:f}, int: {:f}",
+ // error, ret, proportional, m_resampleIntegral);
+ return ret;
+}
+
+std::chrono::milliseconds CActiveAEStream::GetErrorInterval()
+{
+ std::chrono::milliseconds ret = m_errorInterval;
+ double rr = m_processingBuffers->GetRR();
+ if (rr > 1.02 || rr < 0.98)
+ ret *= 3;
+ return ret;
+}
+
+unsigned int CActiveAEStream::GetSpace()
+{
+ std::unique_lock<CCriticalSection> lock(m_streamLock);
+ if (m_format.m_dataFormat == AE_FMT_RAW)
+ return m_streamFreeBuffers;
+ else
+ return m_streamFreeBuffers * m_streamSpace;
+}
+
+unsigned int CActiveAEStream::AddData(const uint8_t* const *data, unsigned int offset, unsigned int frames, ExtData *extData)
+{
+ Message *msg;
+ unsigned int copied = 0;
+ int sourceFrames = frames;
+ const uint8_t* const *buf = data;
+ double pts = 0;
+
+ if (extData)
+ {
+ pts = extData->pts;
+ }
+
+ m_streamIsFlushed = false;
+
+ while(copied < frames)
+ {
+ sourceFrames = frames - copied;
+
+ if (m_currentBuffer)
+ {
+ int start = m_currentBuffer->pkt->nb_samples *
+ m_currentBuffer->pkt->bytes_per_sample *
+ m_currentBuffer->pkt->config.channels /
+ m_currentBuffer->pkt->planes;
+
+ int freeSpace = m_currentBuffer->pkt->max_nb_samples - m_currentBuffer->pkt->nb_samples;
+ int minFrames = std::min(freeSpace, sourceFrames);
+ int planes = m_currentBuffer->pkt->planes;
+ int bufOffset = (offset + copied)*m_format.m_frameSize/planes;
+
+ if (!copied)
+ {
+ if (pts < m_lastPts)
+ {
+ if (m_lastPtsJump != 0)
+ {
+ auto diff = std::chrono::milliseconds(static_cast<int>(pts - m_lastPtsJump));
+ if (diff > m_errorInterval)
+ {
+ diff += 1s;
+ diff = std::min(diff, 6000ms);
+ CLog::Log(LOGINFO,
+ "CActiveAEStream::AddData - messy timestamps, increasing interval for "
+ "measuring average error to {} ms",
+ diff.count());
+ m_errorInterval = diff;
+ }
+ }
+ m_lastPtsJump = pts;
+ }
+ m_lastPts = pts;
+ m_currentBuffer->timestamp = pts;
+ m_currentBuffer->pkt_start_offset = m_currentBuffer->pkt->nb_samples;
+ }
+
+ for (int i=0; i<planes; i++)
+ {
+ memcpy(m_currentBuffer->pkt->data[i]+start, buf[i]+bufOffset, minFrames*m_format.m_frameSize/planes);
+ }
+ copied += minFrames;
+
+ if (extData && extData->hasDownmix)
+ m_currentBuffer->centerMixLevel = extData->centerMixLevel;
+
+ bool rawPktComplete = false;
+ {
+ std::unique_lock<CCriticalSection> lock(m_statsLock);
+ if (m_format.m_dataFormat != AE_FMT_RAW)
+ {
+ m_currentBuffer->pkt->nb_samples += minFrames;
+ m_bufferedTime +=
+ static_cast<float>(minFrames) / m_currentBuffer->pkt->config.sample_rate;
+ }
+ else
+ {
+ m_bufferedTime += static_cast<float>(m_format.m_streamInfo.GetDuration()) / 1000;
+ m_currentBuffer->pkt->nb_samples += minFrames;
+ rawPktComplete = true;
+ }
+ }
+
+ if (m_currentBuffer->pkt->nb_samples == m_currentBuffer->pkt->max_nb_samples || rawPktComplete)
+ {
+ MsgStreamSample msgData;
+ msgData.buffer = m_currentBuffer;
+ msgData.stream = this;
+ RemapBuffer();
+ m_streamPort->SendOutMessage(CActiveAEDataProtocol::STREAMSAMPLE, &msgData, sizeof(MsgStreamSample));
+ m_currentBuffer = nullptr;
+ }
+ continue;
+ }
+ else if (m_streamPort->ReceiveInMessage(&msg))
+ {
+ if (msg->signal == CActiveAEDataProtocol::STREAMBUFFER)
+ {
+ m_currentBuffer = *((CSampleBuffer**)msg->data);
+ m_currentBuffer->timestamp = 0;
+ m_currentBuffer->pkt->nb_samples = 0;
+ m_currentBuffer->pkt->pause_burst_ms = 0;
+ msg->Release();
+ DecFreeBuffers();
+ continue;
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "CActiveAEStream::AddData - unknown signal");
+ msg->Release();
+ break;
+ }
+ }
+ if (!m_inMsgEvent.Wait(200ms))
+ break;
+ }
+ return copied;
+}
+
+double CActiveAEStream::GetDelay()
+{
+ AEDelayStatus status;
+ m_activeAE->GetDelay(status, this);
+ return status.GetDelay();
+}
+
+CAESyncInfo CActiveAEStream::GetSyncInfo()
+{
+ CAESyncInfo info;
+ m_activeAE->GetSyncInfo(info, this);
+ return info;
+}
+
+bool CActiveAEStream::IsBuffering()
+{
+ std::unique_lock<CCriticalSection> lock(m_streamLock);
+ return m_streamIsBuffering;
+}
+
+double CActiveAEStream::GetCacheTime()
+{
+ return static_cast<double>(m_activeAE->GetCacheTime(this));
+}
+
+double CActiveAEStream::GetCacheTotal()
+{
+ return static_cast<double>(m_activeAE->GetCacheTotal());
+}
+
+double CActiveAEStream::GetMaxDelay()
+{
+ return static_cast<double>(m_activeAE->GetMaxDelay());
+}
+
+void CActiveAEStream::Pause()
+{
+ m_activeAE->PauseStream(this, true);
+}
+
+void CActiveAEStream::Resume()
+{
+ m_activeAE->PauseStream(this, false);
+}
+
+void CActiveAEStream::Drain(bool wait)
+{
+ Message *msg;
+ CActiveAEStream *stream = this;
+
+ m_streamDraining = true;
+ m_streamDrained = false;
+
+ Message *reply;
+ if (m_streamPort->SendOutMessageSync(CActiveAEDataProtocol::DRAINSTREAM,
+ &reply,2000,
+ &stream, sizeof(CActiveAEStream*)))
+ {
+ bool success = reply->signal == CActiveAEDataProtocol::ACC ? true : false;
+ reply->Release();
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "CActiveAEStream::Drain - no acc");
+ }
+ }
+
+ if (m_currentBuffer)
+ {
+ MsgStreamSample msgData;
+ msgData.buffer = m_currentBuffer;
+ msgData.stream = this;
+ RemapBuffer();
+ m_streamPort->SendOutMessage(CActiveAEDataProtocol::STREAMSAMPLE, &msgData, sizeof(MsgStreamSample));
+ m_currentBuffer = NULL;
+ }
+
+ if (wait)
+ Resume();
+
+ XbmcThreads::EndTime<> timer(2000ms);
+ while (!timer.IsTimePast())
+ {
+ if (m_streamPort->ReceiveInMessage(&msg))
+ {
+ if (msg->signal == CActiveAEDataProtocol::STREAMBUFFER)
+ {
+ MsgStreamSample msgData;
+ msgData.stream = this;
+ msgData.buffer = *((CSampleBuffer**)msg->data);
+ msg->Reply(CActiveAEDataProtocol::STREAMSAMPLE, &msgData, sizeof(MsgStreamSample));
+ DecFreeBuffers();
+ continue;
+ }
+ else if (msg->signal == CActiveAEDataProtocol::STREAMDRAINED)
+ {
+ msg->Release();
+ return;
+ }
+ }
+ else if (!wait)
+ return;
+
+ m_inMsgEvent.Wait(timer.GetTimeLeft());
+ }
+ CLog::Log(LOGERROR, "CActiveAEStream::Drain - timeout out");
+}
+
+bool CActiveAEStream::IsDraining()
+{
+ std::unique_lock<CCriticalSection> lock(m_streamLock);
+ return m_streamDraining;
+}
+
+bool CActiveAEStream::IsDrained()
+{
+ std::unique_lock<CCriticalSection> lock(m_streamLock);
+ return m_streamDrained;
+}
+
+void CActiveAEStream::Flush()
+{
+ if (!m_streamIsFlushed)
+ {
+ m_currentBuffer = NULL;
+ m_leftoverBytes = 0;
+ m_activeAE->FlushStream(this);
+ m_streamIsFlushed = true;
+ }
+}
+
+float CActiveAEStream::GetAmplification()
+{
+ return m_streamAmplify;
+}
+
+void CActiveAEStream::SetAmplification(float amplify)
+{
+ m_streamAmplify = amplify;
+ m_activeAE->SetStreamAmplification(this, m_streamAmplify);
+}
+
+float CActiveAEStream::GetReplayGain()
+{
+ return m_streamRgain;
+}
+
+void CActiveAEStream::SetReplayGain(float factor)
+{
+ m_streamRgain = std::max( 0.0f, factor);
+ m_activeAE->SetStreamReplaygain(this, m_streamRgain);
+}
+
+float CActiveAEStream::GetVolume()
+{
+ return m_streamVolume;
+}
+
+void CActiveAEStream::SetVolume(float volume)
+{
+ m_streamVolume = std::max( 0.0f, std::min(1.0f, volume));
+ m_activeAE->SetStreamVolume(this, m_streamVolume);
+}
+
+double CActiveAEStream::GetResampleRatio()
+{
+ return m_streamResampleRatio;
+}
+
+void CActiveAEStream::SetResampleRatio(double ratio)
+{
+ if (ratio != m_streamResampleRatio)
+ m_activeAE->SetStreamResampleRatio(this, ratio);
+ m_streamResampleRatio = ratio;
+}
+
+void CActiveAEStream::SetResampleMode(int mode)
+{
+ if (mode != m_streamResampleMode)
+ m_activeAE->SetStreamResampleMode(this, mode);
+ m_streamResampleMode = mode;
+}
+
+void CActiveAEStream::SetFFmpegInfo(int profile, enum AVMatrixEncoding matrix_encoding, enum AVAudioServiceType audio_service_type)
+{
+ m_activeAE->SetStreamFFmpegInfo(this, profile, matrix_encoding, audio_service_type);
+}
+
+void CActiveAEStream::FadeVolume(float from, float target, unsigned int time)
+{
+ if (time == 0 || (m_format.m_dataFormat == AE_FMT_RAW))
+ return;
+
+ m_streamFading = true;
+ m_activeAE->SetStreamFade(this, from, target, time);
+}
+
+bool CActiveAEStream::IsFading()
+{
+ std::unique_lock<CCriticalSection> lock(m_streamLock);
+ return m_streamFading;
+}
+
+unsigned int CActiveAEStream::GetFrameSize() const
+{
+ return m_format.m_frameSize;
+}
+
+unsigned int CActiveAEStream::GetChannelCount() const
+{
+ return m_format.m_channelLayout.Count();
+}
+
+unsigned int CActiveAEStream::GetSampleRate() const
+{
+ return m_format.m_sampleRate;
+}
+
+enum AEDataFormat CActiveAEStream::GetDataFormat() const
+{
+ return m_format.m_dataFormat;
+}
+
+void CActiveAEStream::RegisterAudioCallback(IAudioCallback* pCallback)
+{
+}
+
+void CActiveAEStream::UnRegisterAudioCallback()
+{
+}
+
+void CActiveAEStream::RegisterSlave(IAEStream *slave)
+{
+ std::unique_lock<CCriticalSection> lock(m_streamLock);
+ m_streamSlave = slave;
+}
+
+//------------------------------------------------------------------------------
+// CActiveAEStreamBuffers
+//------------------------------------------------------------------------------
+
+CActiveAEStreamBuffers::CActiveAEStreamBuffers(const AEAudioFormat& inputFormat,
+ const AEAudioFormat& outputFormat,
+ AEQuality quality)
+ : m_inputFormat(inputFormat)
+{
+ m_resampleBuffers = new CActiveAEBufferPoolResample(inputFormat, outputFormat, quality);
+ m_atempoBuffers = new CActiveAEBufferPoolAtempo(outputFormat);
+}
+
+CActiveAEStreamBuffers::~CActiveAEStreamBuffers()
+{
+ delete m_resampleBuffers;
+ delete m_atempoBuffers;
+}
+
+bool CActiveAEStreamBuffers::HasInputLevel(int level)
+{
+ if ((m_inputSamples.size() + m_resampleBuffers->m_inputSamples.size()) >
+ (m_resampleBuffers->m_allSamples.size() * level / 100))
+ return true;
+ else
+ return false;
+}
+
+bool CActiveAEStreamBuffers::Create(unsigned int totaltime, bool remap, bool upmix, bool normalize)
+{
+ if (!m_resampleBuffers->Create(totaltime, remap, upmix, normalize))
+ return false;
+
+ if (!m_atempoBuffers->Create(totaltime))
+ return false;
+
+ return true;
+}
+
+void CActiveAEStreamBuffers::SetExtraData(int profile, enum AVMatrixEncoding matrix_encoding, enum AVAudioServiceType audio_service_type)
+{
+ /*! @todo Implement set dsp config with new AudioDSP buffer implementation */
+}
+
+bool CActiveAEStreamBuffers::ProcessBuffers()
+{
+ bool busy = false;
+ CSampleBuffer *buf;
+
+ while (!m_inputSamples.empty())
+ {
+ buf = m_inputSamples.front();
+ m_inputSamples.pop_front();
+ m_resampleBuffers->m_inputSamples.push_back(buf);
+ busy = true;
+ }
+
+ busy |= m_resampleBuffers->ResampleBuffers();
+
+ while (!m_resampleBuffers->m_outputSamples.empty())
+ {
+ buf = m_resampleBuffers->m_outputSamples.front();
+ m_resampleBuffers->m_outputSamples.pop_front();
+ m_atempoBuffers->m_inputSamples.push_back(buf);
+ busy = true;
+ }
+
+ busy |= m_atempoBuffers->ProcessBuffers();
+
+ while (!m_atempoBuffers->m_outputSamples.empty())
+ {
+ buf = m_atempoBuffers->m_outputSamples.front();
+ m_atempoBuffers->m_outputSamples.pop_front();
+ m_outputSamples.push_back(buf);
+ busy = true;
+ }
+
+ return busy;
+}
+
+void CActiveAEStreamBuffers::ConfigureResampler(bool normalizelevels, bool stereoupmix, AEQuality quality)
+{
+ m_resampleBuffers->ConfigureResampler(normalizelevels, stereoupmix, quality);
+}
+
+float CActiveAEStreamBuffers::GetDelay()
+{
+ float delay = 0;
+
+ for (auto &buf : m_inputSamples)
+ {
+ delay += (float)buf->pkt->nb_samples / buf->pkt->config.sample_rate;
+ }
+
+ delay += m_resampleBuffers->GetDelay();
+ delay += m_atempoBuffers->GetDelay();
+
+ for (auto &buf : m_outputSamples)
+ {
+ delay += (float)buf->pkt->nb_samples / buf->pkt->config.sample_rate;
+ }
+
+ return delay;
+}
+
+void CActiveAEStreamBuffers::Flush()
+{
+ m_resampleBuffers->Flush();
+ m_atempoBuffers->Flush();
+
+ while (!m_inputSamples.empty())
+ {
+ m_inputSamples.front()->Return();
+ m_inputSamples.pop_front();
+ }
+ while (!m_outputSamples.empty())
+ {
+ m_outputSamples.front()->Return();
+ m_outputSamples.pop_front();
+ }
+}
+
+void CActiveAEStreamBuffers::SetDrain(bool drain)
+{
+ m_resampleBuffers->SetDrain(drain);
+ m_atempoBuffers->SetDrain(drain);
+}
+
+bool CActiveAEStreamBuffers::IsDrained()
+{
+ if (m_resampleBuffers->m_inputSamples.empty() &&
+ m_resampleBuffers->m_outputSamples.empty() &&
+ m_atempoBuffers->m_inputSamples.empty() &&
+ m_atempoBuffers->m_outputSamples.empty() &&
+ m_inputSamples.empty() &&
+ m_outputSamples.empty())
+ return true;
+ else
+ return false;
+}
+
+void CActiveAEStreamBuffers::SetRR(double rr, double atempoThreshold)
+{
+ if (fabs(rr - 1.0) < atempoThreshold)
+ {
+ m_resampleBuffers->SetRR(rr);
+ m_atempoBuffers->SetTempo(1.0);
+ }
+ else
+ {
+ m_resampleBuffers->SetRR(1.0);
+ m_atempoBuffers->SetTempo(1.0/rr);
+ }
+}
+
+double CActiveAEStreamBuffers::GetRR()
+{
+ double tempo = m_resampleBuffers->GetRR();
+ tempo /= static_cast<double>(m_atempoBuffers->GetTempo());
+ return tempo;
+}
+
+void CActiveAEStreamBuffers::FillBuffer()
+{
+ m_resampleBuffers->FillBuffer();
+ m_atempoBuffers->FillBuffer();
+}
+
+bool CActiveAEStreamBuffers::DoesNormalize()
+{
+ return m_resampleBuffers->DoesNormalize();
+}
+
+void CActiveAEStreamBuffers::ForceResampler(bool force)
+{
+ m_resampleBuffers->ForceResampler(force);
+}
+
+CActiveAEBufferPool* CActiveAEStreamBuffers::GetResampleBuffers()
+{
+ CActiveAEBufferPool *ret = m_resampleBuffers;
+ m_resampleBuffers = nullptr;
+ return ret;
+}
+
+CActiveAEBufferPool* CActiveAEStreamBuffers::GetAtempoBuffers()
+{
+ CActiveAEBufferPool *ret = m_atempoBuffers;
+ m_atempoBuffers = nullptr;
+ return ret;
+}
+
+bool CActiveAEStreamBuffers::HasWork()
+{
+ if (!m_inputSamples.empty())
+ return true;
+ if (!m_outputSamples.empty())
+ return true;
+ if (!m_resampleBuffers->m_inputSamples.empty())
+ return true;
+ if (!m_resampleBuffers->m_outputSamples.empty())
+ return true;
+ if (!m_atempoBuffers->m_inputSamples.empty())
+ return true;
+ if (!m_atempoBuffers->m_outputSamples.empty())
+ return true;
+
+ return false;
+}
diff --git a/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEStream.h b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEStream.h
new file mode 100644
index 0000000..b7502d5
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Engines/ActiveAE/ActiveAEStream.h
@@ -0,0 +1,246 @@
+/*
+ * 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 "cores/AudioEngine/Engines/ActiveAE/ActiveAE.h"
+#include "cores/AudioEngine/Engines/ActiveAE/ActiveAEBuffer.h"
+#include "cores/AudioEngine/Interfaces/AEStream.h"
+#include "cores/AudioEngine/Utils/AEAudioFormat.h"
+#include "cores/AudioEngine/Utils/AELimiter.h"
+#include "threads/Event.h"
+
+#include <atomic>
+#include <deque>
+
+namespace ActiveAE
+{
+class CActiveAE;
+
+class CSyncError
+{
+public:
+ CSyncError()
+ {
+ Flush();
+ }
+ void Add(double error)
+ {
+ m_buffer += error;
+ m_count++;
+ }
+
+ void Flush(std::chrono::milliseconds interval = std::chrono::milliseconds(100))
+ {
+ m_buffer = 0.0;
+ m_lastError = 0.0;
+ m_count = 0;
+ m_timer.Set(interval);
+ }
+
+ void SetErrorInterval(std::chrono::milliseconds interval = std::chrono::milliseconds(100))
+ {
+ m_buffer = 0.0;
+ m_count = 0;
+ m_timer.Set(interval);
+ }
+
+ bool Get(double& error, std::chrono::milliseconds interval = std::chrono::milliseconds(100))
+ {
+ if(m_timer.IsTimePast())
+ {
+ error = Get();
+ Flush(interval);
+ m_lastError = error;
+ return true;
+ }
+ else
+ {
+ error = m_lastError;
+ return false;
+ }
+ }
+
+ double GetLastError(unsigned int &time)
+ {
+ time = m_timer.GetStartTime().time_since_epoch().count();
+ return m_lastError;
+ }
+
+ void Correction(double correction)
+ {
+ m_lastError += correction;
+ }
+
+protected:
+ double Get() const
+ {
+ if(m_count)
+ return m_buffer / m_count;
+ else
+ return 0.0;
+ }
+ double m_buffer;
+ double m_lastError;
+ int m_count;
+ XbmcThreads::EndTime<> m_timer;
+};
+
+class CActiveAEStreamBuffers
+{
+public:
+ CActiveAEStreamBuffers(const AEAudioFormat& inputFormat, const AEAudioFormat& outputFormat, AEQuality quality);
+ virtual ~CActiveAEStreamBuffers();
+ bool Create(unsigned int totaltime, bool remap, bool upmix, bool normalize = true);
+ void SetExtraData(int profile, enum AVMatrixEncoding matrix_encoding, enum AVAudioServiceType audio_service_type);
+ bool ProcessBuffers();
+ void ConfigureResampler(bool normalizelevels, bool stereoupmix, AEQuality quality);
+ bool HasInputLevel(int level);
+ float GetDelay();
+ void Flush();
+ void SetDrain(bool drain);
+ bool IsDrained();
+ void SetRR(double rr, double atempoThreshold);
+ double GetRR();
+ void FillBuffer();
+ bool DoesNormalize();
+ void ForceResampler(bool force);
+ bool HasWork();
+ CActiveAEBufferPool *GetResampleBuffers();
+ CActiveAEBufferPool *GetAtempoBuffers();
+
+ AEAudioFormat m_inputFormat;
+ std::deque<CSampleBuffer*> m_outputSamples;
+ std::deque<CSampleBuffer*> m_inputSamples;
+
+protected:
+ CActiveAEBufferPoolResample *m_resampleBuffers;
+ CActiveAEBufferPoolAtempo *m_atempoBuffers;
+
+private:
+ CActiveAEStreamBuffers(const CActiveAEStreamBuffers&) = delete;
+ CActiveAEStreamBuffers& operator=(const CActiveAEStreamBuffers&) = delete;
+};
+
+class CActiveAEStream : public IAEStream
+{
+protected:
+ friend class CActiveAE;
+ friend class CEngineStats;
+ CActiveAEStream(AEAudioFormat *format, unsigned int streamid, CActiveAE *ae);
+ ~CActiveAEStream() override;
+ void FadingFinished();
+ void IncFreeBuffers();
+ void DecFreeBuffers();
+ void ResetFreeBuffers();
+ void InitRemapper();
+ void RemapBuffer();
+ double CalcResampleRatio(double error);
+ std::chrono::milliseconds GetErrorInterval();
+
+public:
+ unsigned int GetSpace() override;
+ unsigned int AddData(const uint8_t* const *data, unsigned int offset, unsigned int frames, ExtData *extData) override;
+ double GetDelay() override;
+ CAESyncInfo GetSyncInfo() override;
+ bool IsBuffering() override;
+ double GetCacheTime() override;
+ double GetCacheTotal() override;
+ double GetMaxDelay() override;
+
+ void Pause() override;
+ void Resume() override;
+ void Drain(bool wait) override;
+ bool IsDraining() override;
+ bool IsDrained() override;
+ void Flush() override;
+
+ float GetVolume() override;
+ float GetReplayGain() override;
+ float GetAmplification() override;
+ void SetVolume(float volume) override;
+ void SetReplayGain(float factor) override;
+ void SetAmplification(float amplify) override;
+ void SetFFmpegInfo(int profile, enum AVMatrixEncoding matrix_encoding, enum AVAudioServiceType audio_service_type) override;
+
+ unsigned int GetFrameSize() const override;
+ unsigned int GetChannelCount() const override;
+
+ unsigned int GetSampleRate() const override ;
+ enum AEDataFormat GetDataFormat() const override;
+
+ double GetResampleRatio() override;
+ void SetResampleRatio(double ratio) override;
+ void SetResampleMode(int mode) override;
+ void RegisterAudioCallback(IAudioCallback* pCallback) override;
+ void UnRegisterAudioCallback() override;
+ void FadeVolume(float from, float to, unsigned int time) override;
+ bool IsFading() override;
+ void RegisterSlave(IAEStream *stream) override;
+
+protected:
+
+ CActiveAE *m_activeAE;
+ unsigned int m_id;
+ AEAudioFormat m_format;
+ float m_streamVolume;
+ float m_streamRgain;
+ float m_streamAmplify;
+ double m_streamResampleRatio;
+ int m_streamResampleMode;
+ unsigned int m_streamSpace;
+ bool m_streamDraining;
+ bool m_streamDrained;
+ bool m_streamFading;
+ int m_streamFreeBuffers;
+ bool m_streamIsBuffering;
+ bool m_streamIsFlushed;
+ IAEStream *m_streamSlave;
+ CCriticalSection m_streamLock;
+ CCriticalSection m_statsLock;
+ uint8_t *m_leftoverBuffer;
+ int m_leftoverBytes;
+ CSampleBuffer *m_currentBuffer;
+ CSoundPacket *m_remapBuffer;
+ IAEResample *m_remapper;
+ double m_lastPts;
+ double m_lastPtsJump;
+ std::chrono::milliseconds m_errorInterval{1000};
+
+ // only accessed by engine
+ CActiveAEBufferPool *m_inputBuffers;
+ CActiveAEStreamBuffers *m_processingBuffers;
+ std::deque<CSampleBuffer*> m_processingSamples;
+ CActiveAEDataProtocol *m_streamPort;
+ CEvent m_inMsgEvent;
+ bool m_drain;
+ bool m_paused;
+ bool m_started;
+ CAELimiter m_limiter;
+ float m_volume;
+ float m_rgain;
+ float m_amplify;
+ float m_bufferedTime;
+ int m_fadingSamples;
+ float m_fadingBase;
+ float m_fadingTarget;
+ int m_fadingTime;
+ int m_profile;
+ int m_resampleMode;
+ double m_resampleIntegral;
+ double m_clockSpeed;
+ enum AVMatrixEncoding m_matrixEncoding;
+ enum AVAudioServiceType m_audioServiceType;
+ bool m_forceResampler;
+ IAEClockCallback *m_pClock;
+ CSyncError m_syncError;
+ double m_lastSyncError;
+ CAESyncInfo::AESyncState m_syncState;
+};
+}
+
diff --git a/xbmc/cores/AudioEngine/Interfaces/AE.h b/xbmc/cores/AudioEngine/Interfaces/AE.h
new file mode 100644
index 0000000..a8562ea
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Interfaces/AE.h
@@ -0,0 +1,323 @@
+/*
+ * 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 "cores/AudioEngine/Utils/AEAudioFormat.h"
+
+#include <cassert>
+#include <list>
+#include <map>
+#include <memory>
+#include <utility>
+#include <vector>
+
+extern "C" {
+#include <libavutil/samplefmt.h>
+}
+
+typedef std::pair<std::string, std::string> AEDevice;
+typedef std::vector<AEDevice> AEDeviceList;
+
+/* forward declarations */
+class IAEStream;
+class IAEStreamDeleter;
+class IAESound;
+class IAESoundDeleter;
+class IAEPacketizer;
+class IAudioCallback;
+class IAEClockCallback;
+class CAEStreamInfo;
+
+namespace ADDON
+{
+struct Interface_AudioEngine;
+}
+
+/* sound options */
+#define AE_SOUND_OFF 0 /*! disable sounds */
+#define AE_SOUND_IDLE 1 /*! only play sounds while no streams are running */
+#define AE_SOUND_ALWAYS 2 /*! always play sounds */
+
+/* config options */
+#define AE_CONFIG_FIXED 1
+#define AE_CONFIG_AUTO 2
+#define AE_CONFIG_MATCH 3
+
+enum AEQuality
+{
+ AE_QUALITY_UNKNOWN = -1, /*! Unset, unknown or incorrect quality level */
+ AE_QUALITY_DEFAULT = 0, /*! Engine's default quality level */
+
+ /* Basic quality levels */
+ AE_QUALITY_LOW = 20, /*! Low quality level */
+ AE_QUALITY_MID = 30, /*! Standard quality level */
+ AE_QUALITY_HIGH = 50, /*! Best sound processing quality */
+
+ /* Optional quality levels */
+ AE_QUALITY_REALLYHIGH = 100, /*! Uncompromised optional quality level,
+ usually with unmeasurable and unnoticeable improvement */
+ AE_QUALITY_GPU = 101, /*! GPU acceleration */
+};
+
+struct SampleConfig
+{
+ AVSampleFormat fmt;
+ uint64_t channel_layout;
+ int channels;
+ int sample_rate;
+ int bits_per_sample;
+ int dither_bits;
+};
+
+/*!
+ * \brief IAE Interface
+ */
+class IAE
+{
+protected:
+
+ IAE() = default;
+ virtual ~IAE() = default;
+
+ /*!
+ * \brief Initializes the AudioEngine, called by CFactory when it is time to initialize the audio engine.
+ *
+ * Do not call this directly, CApplication will call this when it is ready
+ */
+ virtual void Start() = 0;
+public:
+ using StreamPtr = std::unique_ptr<IAEStream, IAEStreamDeleter>;
+ using SoundPtr = std::unique_ptr<IAESound, IAESoundDeleter>;
+
+ /*!
+ * \brief Called when the application needs to terminate the engine
+ */
+ virtual void Shutdown() { }
+
+ /*!
+ * \brief Suspends output and de-initializes sink
+ *
+ * Used to avoid conflicts with external players or to reduce power consumption
+ *
+ * \return True if successful
+ */
+ virtual bool Suspend() = 0;
+
+ /*!
+ * \brief Resumes output and re-initializes sink
+ *
+ * Used to resume output from Suspend() state above
+ *
+ * \return True if successful
+ */
+ virtual bool Resume() = 0;
+
+ /*!
+ * \brief Get the current Suspend() state
+ *
+ * Used by players to determine if audio is being processed
+ * Default is true so players drop audio or pause if engine unloaded
+ *
+ * \return True if processing suspended
+ */
+ virtual bool IsSuspended() {return true;}
+
+ /*!
+ * \brief Returns the current master volume level of the AudioEngine
+ *
+ * \return The volume level between 0.0 and 1.0
+ */
+ virtual float GetVolume() = 0;
+
+ /*!
+ * \brief Sets the master volume level of the AudioEngine
+ *
+ * \param volume The new volume level between 0.0 and 1.0
+ */
+ virtual void SetVolume(const float volume) = 0;
+
+ /*!
+ * \brief Set the mute state (does not affect volume level value)
+ *
+ * \param enabled The mute state
+ */
+ virtual void SetMute(const bool enabled) = 0;
+
+ /*!
+ * \brief Get the current mute state
+ *
+ * \return The current mute state
+ */
+ virtual bool IsMuted() = 0;
+
+ /*!
+ * \brief Creates and returns a new IAEStream in the format specified, this function should never fail
+ *
+ * The cleanup behaviour can be modified with the IAEStreamDeleter::setFinish method.
+ * Per default the behaviour is the same as calling FreeStream with true.
+ *
+ * \param audioFormat
+ * \param options A bit field of stream options (see: enum AEStreamOptions)
+ * \return a new IAEStream that will accept data in the requested format
+ */
+ virtual StreamPtr MakeStream(AEAudioFormat& audioFormat,
+ unsigned int options = 0,
+ IAEClockCallback* clock = NULL) = 0;
+
+ /*!
+ * \brief Creates a new IAESound that is ready to play the specified file
+ *
+ * \param file The WAV file to load, this supports XBMC's VFS
+ * \return A new IAESound if the file could be loaded, otherwise NULL
+ */
+ virtual SoundPtr MakeSound(const std::string& file) = 0;
+
+ /*!
+ * \brief Enumerate the supported audio output devices
+ *
+ * \param devices The device list to append supported devices to
+ * \param passthrough True if only passthrough devices are wanted
+ */
+ virtual void EnumerateOutputDevices(AEDeviceList &devices, bool passthrough) = 0;
+
+ /*!
+ * \brief Returns true if the AudioEngine supports AE_FMT_RAW streams for use with formats such as IEC61937
+ *
+ * \see CAEPackIEC61937::CAEPackIEC61937()
+ *
+ * \returns true if the AudioEngine is capable of RAW output
+ */
+ virtual bool SupportsRaw(AEAudioFormat &format) { return false; }
+
+ /*!
+ * \brief Returns true if the AudioEngine supports drain mode which is not streaming silence when idle
+ *
+ * \returns true if the AudioEngine is capable of drain mode
+ */
+ virtual bool SupportsSilenceTimeout() { return false; }
+
+ /*!
+ * \brief Returns true if the AudioEngine is currently configured to extract the DTS Core from DTS-HD streams
+ *
+ * \returns true if the AudioEngine is currently configured to extract the DTS Core from DTS-HD streams
+ */
+ virtual bool UsesDtsCoreFallback() { return false; }
+
+ /*!
+ * \brief Returns true if the AudioEngine is currently configured for stereo audio
+ *
+ * \returns true if the AudioEngine is currently configured for stereo audio
+ */
+ virtual bool HasStereoAudioChannelCount() { return false; }
+
+ /*!
+ * \brief Returns true if the AudioEngine is currently configured for HD audio (more than 5.1)
+ *
+ * \returns true if the AudioEngine is currently configured for HD audio (more than 5.1)
+ */
+ virtual bool HasHDAudioChannelCount() { return true; }
+
+ virtual void RegisterAudioCallback(IAudioCallback* pCallback) {}
+
+ virtual void UnregisterAudioCallback(IAudioCallback* pCallback) {}
+
+ /*!
+ * \brief Returns true if AudioEngine supports specified quality level
+ *
+ * \return true if specified quality level is supported, otherwise false
+ */
+ virtual bool SupportsQualityLevel(enum AEQuality level) { return false; }
+
+ /*!
+ * \brief AE decides whether this settings should be displayed
+ *
+ * \return true if AudioEngine wants to display this setting
+ */
+ virtual bool IsSettingVisible(const std::string &settingId) {return false; }
+
+ /*!
+ * \brief Instruct AE to keep configuration for a specified time
+ *
+ * \param millis time for which old configuration should be kept
+ */
+ virtual void KeepConfiguration(unsigned int millis) {}
+
+ /*!
+ * \brief Instruct AE to re-initialize, e.g. after ELD change event
+ */
+ virtual void DeviceChange() {}
+
+ /*!
+ * \brief Instruct AE to re-initialize, e.g. after ELD change event
+ */
+ virtual void DeviceCountChange(const std::string& driver) {}
+
+ /*!
+ * \brief Get the current sink data format
+ *
+ * \param Current sink data format. For more details see AEAudioFormat.
+ * \return Returns true on success, else false.
+ */
+ virtual bool GetCurrentSinkFormat(AEAudioFormat &SinkFormat) { return false; }
+
+private:
+ friend class IAEStreamDeleter;
+ friend class IAESoundDeleter;
+ friend struct ADDON::Interface_AudioEngine;
+
+ /*!
+ * \brief This method will remove the specified stream from the engine.
+ *
+ * For OSX/IOS this is essential to reconfigure the audio output.
+ *
+ * \param stream The stream to be altered
+ * \param finish if true AE will switch back to gui sound mode (if this is last stream)
+ * \return true on success, else false.
+ */
+ virtual bool FreeStream(IAEStream* stream, bool finish) = 0;
+
+ /*!
+ * \brief Free the supplied IAESound object
+ *
+ * \param sound The IAESound object to free
+ */
+ virtual void FreeSound(IAESound* sound) = 0;
+};
+
+class IAEStreamDeleter
+{
+private:
+ IAE* m_iae;
+ bool m_finish;
+
+public:
+ IAEStreamDeleter() : m_iae(nullptr), m_finish(true) {}
+ explicit IAEStreamDeleter(IAE& iae) : m_iae(&iae), m_finish{true} {}
+ void setFinish(bool finish) { m_finish = finish; }
+ void operator()(IAEStream* stream)
+ {
+ assert(m_iae);
+ m_iae->FreeStream(stream, m_finish);
+ }
+};
+
+class IAESoundDeleter
+{
+private:
+ IAE* m_iae;
+
+public:
+ IAESoundDeleter() : m_iae(nullptr) {}
+ explicit IAESoundDeleter(IAE& iae) : m_iae(&iae) {}
+ void operator()(IAESound* sound)
+ {
+ assert(m_iae);
+ m_iae->FreeSound(sound);
+ }
+};
diff --git a/xbmc/cores/AudioEngine/Interfaces/AEEncoder.h b/xbmc/cores/AudioEngine/Interfaces/AEEncoder.h
new file mode 100644
index 0000000..5c207be
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Interfaces/AEEncoder.h
@@ -0,0 +1,95 @@
+/*
+ * 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 "cores/AudioEngine/Utils/AEAudioFormat.h"
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+}
+
+/**
+ * IAEEncoder interface for on the fly audio compression
+ */
+class IAEEncoder
+{
+public:
+ /**
+ * Constructor
+ */
+ IAEEncoder() = default;
+
+ /**
+ * Destructor
+ */
+ virtual ~IAEEncoder() = default;
+
+ /**
+ * Return true if the supplied format is compatible with the current open encoder.
+ * @param format the format to compare
+ * @return true if compatible, false if not
+ */
+ virtual bool IsCompatible(const AEAudioFormat& format) = 0;
+
+ /**
+ * Called to setup the encoder to accept data in the specified format
+ * @param format the desired audio format, may be changed to suit the encoder
+ * @param allow_planar_input allow engine to use with planar formats
+ * @return true on success, false on failure
+ */
+ virtual bool Initialize(AEAudioFormat &format, bool allow_planar_input = false) = 0;
+
+ /**
+ * Reset the encoder for new data
+ */
+ virtual void Reset() = 0;
+
+ /**
+ * Returns the bitrate of the encoder
+ * @return bit rate in bits per second
+ */
+ virtual unsigned int GetBitRate() = 0;
+
+ /**
+ * Returns the AVCodecID of the encoder
+ * @return the ffmpeg codec id
+ */
+ virtual AVCodecID GetCodecID() = 0;
+
+ /**
+ * Return the number of frames needed to encode
+ * @return number of frames (frames * channels = samples * bits per sample = bytes)
+ */
+ virtual unsigned int GetFrames() = 0;
+
+ /**
+ * Encodes the supplied samples into a provided buffer
+ * @param in the PCM samples encoder requested format
+ * @param in_size input buffer size
+ * @param output buffer
+ * @param out_size output buffer size
+ * @return size of encoded data
+ */
+ virtual int Encode (uint8_t *in, int in_size, uint8_t *out, int out_size) = 0;
+
+ /**
+ * Get the encoded data
+ * @param data return pointer to the buffer with the current encoded block
+ * @return the size in bytes of *data
+ */
+ virtual int GetData(uint8_t **data) = 0;
+
+ /**
+ * Get the delay in seconds
+ * @param bufferSize how much encoded data the caller has buffered to add to the delay
+ * @return the delay in seconds including any un-fetched encoded data
+ */
+ virtual double GetDelay(unsigned int bufferSize) = 0;
+};
+
diff --git a/xbmc/cores/AudioEngine/Interfaces/AEResample.h b/xbmc/cores/AudioEngine/Interfaces/AEResample.h
new file mode 100644
index 0000000..8b27b32
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Interfaces/AEResample.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "cores/AudioEngine/Interfaces/AE.h"
+
+namespace ActiveAE
+{
+
+class IAEResample
+{
+public:
+ // return the name of this sync for logging
+ virtual const char *GetName() = 0;
+ IAEResample() = default;
+ virtual ~IAEResample() = default;
+ virtual bool Init(SampleConfig dstConfig, SampleConfig srcConfig, bool upmix, bool normalize, double centerMix,
+ CAEChannelInfo *remapLayout, AEQuality quality, bool force_resample) = 0;
+ virtual int Resample(uint8_t **dst_buffer, int dst_samples, uint8_t **src_buffer, int src_samples, double ratio) = 0;
+ virtual int64_t GetDelay(int64_t base) = 0;
+ virtual int GetBufferedSamples() = 0;
+ virtual bool WantsNewSamples(int samples) = 0;
+ virtual int CalcDstSampleCount(int src_samples, int dst_rate, int src_rate) = 0;
+ virtual int GetSrcBufferSize(int samples) = 0;
+ virtual int GetDstBufferSize(int samples) = 0;
+};
+
+}
diff --git a/xbmc/cores/AudioEngine/Interfaces/AESink.h b/xbmc/cores/AudioEngine/Interfaces/AESink.h
new file mode 100644
index 0000000..5637c69
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Interfaces/AESink.h
@@ -0,0 +1,85 @@
+/*
+ * 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 "cores/AudioEngine/Interfaces/AE.h" // for typedef's used in derived classes
+#include "cores/AudioEngine/Utils/AEAudioFormat.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+
+#include <stdint.h>
+#include <string>
+
+class IAESink
+{
+public:
+ /* return the name of this sync for logging */
+ virtual const char *GetName() = 0;
+
+ IAESink() = default;
+ virtual ~IAESink() = default;
+
+ /*
+ The sink does NOT have to honour anything in the format struct or the device
+ if however it does not honour what is requested, it MUST update device/format
+ with what it does support.
+ */
+ virtual bool Initialize (AEAudioFormat &format, std::string &device) = 0;
+
+ /*
+ Deinitialize the sink for destruction
+ */
+ virtual void Deinitialize() = 0;
+
+ /*
+ This method returns the total time in seconds of the cache.
+ */
+ virtual double GetCacheTotal() = 0;
+
+ /*
+ This method returns latency of hardware.
+ */
+ virtual double GetLatency() { return 0.0; }
+
+ /*!
+ * @brief Adds packets to be sent out, this routine MUST block or sleep.
+ * @param data array of pointers to planes holding audio data
+ * @param frames number of audio frames in data
+ * @param offset offset in frames where audio data starts
+ * @return number of frames consumed by the sink
+ */
+ virtual unsigned int AddPackets(uint8_t **data, unsigned int frames, unsigned int offset) = 0;
+
+ /*!
+ * @brief instruct the sink to add a pause
+ * @param millis ms to pause
+ */
+ virtual void AddPause(unsigned int millis) {}
+
+ /*!
+ * @brief Return a timestamped status structure with delay and sink info
+ * @param status structure filled with sink status
+ */
+ virtual void GetDelay(AEDelayStatus& status) = 0;
+
+ /*
+ Drain the sink
+ */
+ virtual void Drain() {}
+
+ /*
+ Indicates if sink can handle volume control.
+ */
+ virtual bool HasVolume() { return false; }
+
+ /*
+ This method sets the volume control, volume ranges from 0.0 to 1.0.
+ */
+ virtual void SetVolume(float volume) {}
+};
+
diff --git a/xbmc/cores/AudioEngine/Interfaces/AESound.h b/xbmc/cores/AudioEngine/Interfaces/AESound.h
new file mode 100644
index 0000000..6066fa1
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Interfaces/AESound.h
@@ -0,0 +1,44 @@
+/*
+ * 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 "cores/AudioEngine/Utils/AEAudioFormat.h"
+
+#include <string>
+
+class IAESound
+{
+protected:
+ friend class IAE;
+ explicit IAESound(const std::string &filename) {}
+ virtual ~IAESound() = default;
+
+public:
+ /* play the sound this object represents */
+ virtual void Play() = 0;
+
+ /* stop playing the sound this object represents */
+ virtual void Stop() = 0;
+
+ /* return true if the sound is currently playing */
+ virtual bool IsPlaying() = 0;
+
+ /* set the playback channel of this sound, AE_CH_NULL for all */
+ virtual void SetChannel(AEChannel channel) = 0;
+
+ /* get the current playback channel of this sound, AE_CH_NULL for all */
+ virtual AEChannel GetChannel() = 0;
+
+ /* set the playback volume of this sound */
+ virtual void SetVolume(float volume) = 0;
+
+ /* get the current playback volume of this sound */
+ virtual float GetVolume() = 0;
+};
+
diff --git a/xbmc/cores/AudioEngine/Interfaces/AEStream.h b/xbmc/cores/AudioEngine/Interfaces/AEStream.h
new file mode 100644
index 0000000..c0f20f6
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Interfaces/AEStream.h
@@ -0,0 +1,272 @@
+/*
+ * 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 "cores/AudioEngine/Utils/AEAudioFormat.h"
+#include <stdint.h>
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+}
+
+class IAudioCallback;
+
+/**
+ * Callback interface for VideoPlayer clock needed by AE for sync
+ */
+class IAEClockCallback
+{
+public:
+ virtual ~IAEClockCallback() = default;
+ virtual double GetClock() = 0;
+ virtual double GetClockSpeed() { return 1.0; }
+};
+
+class CAESyncInfo
+{
+public:
+ double delay;
+ double error;
+ double rr;
+ unsigned int errortime;
+ enum AESyncState
+ {
+ SYNC_OFF,
+ SYNC_INSYNC,
+ SYNC_START,
+ SYNC_MUTE,
+ SYNC_ADJUST
+ };
+ AESyncState state;
+};
+
+/**
+ * IAEStream Stream Interface for streaming audio
+ */
+class IAEStream
+{
+protected:
+ friend class IAE;
+ IAEStream() = default;
+ virtual ~IAEStream() = default;
+
+public:
+ struct ExtData
+ {
+ double pts = 0;
+ bool hasDownmix = false;
+ double centerMixLevel = 1;
+ };
+
+public:
+ /**
+ * Returns the amount of space available in the stream
+ * @return The number of bytes AddData will consume
+ */
+ virtual unsigned int GetSpace() = 0;
+
+ /**
+ * Add planar or interleaved PCM data to the stream
+ * @param data array of pointers to the planes
+ * @param offset to frame in frames
+ * @param frames number of frames
+ * @param pts timestamp
+ * @return The number of frames consumed
+ */
+ virtual unsigned int AddData(const uint8_t* const *data, unsigned int offset, unsigned int frames, ExtData *extData) = 0;
+
+ /**
+ * Returns the time in seconds that it will take
+ * for the next added packet to be heard from the speakers.
+ * @return seconds
+ */
+ virtual double GetDelay() = 0;
+
+ /**
+ * Returns info about audio to clock synchronization
+ * @return CAESyncInfo
+ */
+ virtual CAESyncInfo GetSyncInfo() = 0;
+
+ /**
+ * Returns if the stream is buffering
+ * @return True if the stream is buffering
+ */
+ virtual bool IsBuffering() = 0;
+
+ /**
+ * Returns the time in seconds of the stream's
+ * cached audio samples. Engine buffers excluded.
+ * @return seconds
+ */
+ virtual double GetCacheTime() = 0;
+
+ /**
+ * Returns the total time in seconds of the cache
+ * @return seconds
+ */
+ virtual double GetCacheTotal() = 0;
+
+ /**
+ * Returns the total time in seconds of maximum delay
+ * @return seconds
+ */
+ virtual double GetMaxDelay() = 0;
+
+ /**
+ * Pauses the stream playback
+ */
+ virtual void Pause() = 0;
+
+ /**
+ * Resumes the stream after pausing
+ */
+ virtual void Resume() = 0;
+
+ /**
+ * Start draining the stream
+ * @note Once called AddData will not consume more data.
+ */
+ virtual void Drain(bool wait) = 0;
+
+ /**
+ * Returns true if the is stream draining
+ */
+ virtual bool IsDraining() = 0;
+
+ /**
+ * Returns true if the is stream has finished draining
+ */
+ virtual bool IsDrained() = 0;
+
+ /**
+ * Flush all buffers dropping the audio data
+ */
+ virtual void Flush() = 0;
+
+ /**
+ * Return the stream's current volume level
+ * @return The volume level between 0.0 and 1.0
+ */
+ virtual float GetVolume() = 0;
+
+ /**
+ * Set the stream's volume level
+ * @param volume The new volume level between 0.0 and 1.0
+ */
+ virtual void SetVolume(float volume) = 0;
+
+ /**
+ * Returns the stream's current replay gain factor
+ * @return The replay gain factor between 0.0 and 1.0
+ */
+ virtual float GetReplayGain() = 0;
+
+ /**
+ * Sets the stream's replay gain factor, this is used by formats such as MP3 that have attenuation information in their streams
+ * @param factor The replay gain factor
+ */
+ virtual void SetReplayGain(float factor) = 0;
+
+ /**
+ * Gets the stream's volume amplification in linear units.
+ * @return The volume amplification factor between 1.0 and 1000.0
+ */
+ virtual float GetAmplification() = 0;
+
+ /**
+ * Sets the stream's volume amplification in linear units.
+ * @param The volume amplification factor between 1.0 and 1000.0
+ */
+ virtual void SetAmplification(float amplify) = 0;
+
+ /**
+ * Sets the stream ffmpeg information if present.
+ + @param profile
+ * @param matrix_encoding
+ * @param audio_service_type
+ */
+ virtual void SetFFmpegInfo(int profile, enum AVMatrixEncoding matrix_encoding, enum AVAudioServiceType audio_service_type) = 0;
+
+ /**
+ * Returns the size of one audio frame in bytes (channelCount * resolution)
+ * @return The size in bytes of one frame
+ */
+ virtual unsigned int GetFrameSize() const = 0;
+
+ /**
+ * Returns the number of channels the stream is configured to accept
+ * @return The channel count
+ */
+ virtual unsigned int GetChannelCount() const = 0;
+
+ /**
+ * Returns the stream's sample rate, if the stream is using a dynamic sample rate, this value will NOT reflect any changes made by calls to SetResampleRatio()
+ * @return The stream's sample rate (eg, 48000)
+ */
+ virtual unsigned int GetSampleRate() const = 0;
+
+ /**
+ * Return the data format the stream has been configured with
+ * @return The stream's data format (eg, AE_FMT_S16LE)
+ */
+ virtual enum AEDataFormat GetDataFormat() const = 0;
+
+ /**
+ * Return the resample ratio
+ * @note This will return an undefined value if the stream is not resampling
+ * @return the current resample ratio or undefined if the stream is not resampling
+ */
+ virtual double GetResampleRatio() = 0;
+
+ /**
+ * Sets the resample ratio
+ * @note This function may return false if the stream is not resampling, if you wish to use this be sure to set the AESTREAM_FORCE_RESAMPLE option
+ * @param ratio the new sample rate ratio, calculated by ((double)desiredRate / (double)GetSampleRate())
+ */
+ virtual void SetResampleRatio(double ratio) = 0;
+
+ /**
+ * Sets the resamplling on/ff
+ */
+ virtual void SetResampleMode(int mode) = 0;
+
+ /**
+ * Registers the audio callback to call with each block of data, this is used by Audio Visualizations
+ * @warning Currently the callbacks require stereo float data in blocks of 512 samples, any deviation from this may crash XBMC, or cause junk to be rendered
+ * @param pCallback The callback
+ */
+ virtual void RegisterAudioCallback(IAudioCallback* pCallback) = 0;
+
+ /**
+ * Unregisters the current audio callback
+ */
+ virtual void UnRegisterAudioCallback() = 0;
+
+ /**
+ * Fade the volume level over the specified time
+ * @param from The volume level to fade from (0.0f-1.0f) - See notes
+ * @param target The volume level to fade to (0.0f-1.0f)
+ * @param time The amount of time in milliseconds for the fade to occur
+ * @note The from parameter does not set the streams volume, it is only used to calculate the fade time properly
+ */
+ virtual void FadeVolume(float from, float target, unsigned int time) {} /* FIXME: once all the engines have these new methods */
+
+ /**
+ * Returns if a fade is still running
+ * @return true if a fade is in progress, otherwise false
+ */
+ virtual bool IsFading() { return false; }
+
+ /**
+ * Slave a stream to resume when this stream has drained
+ */
+ virtual void RegisterSlave(IAEStream *stream) = 0;
+};
+
diff --git a/xbmc/cores/AudioEngine/Interfaces/IAudioCallback.h b/xbmc/cores/AudioEngine/Interfaces/IAudioCallback.h
new file mode 100644
index 0000000..d8759d5
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Interfaces/IAudioCallback.h
@@ -0,0 +1,23 @@
+/*
+ * 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
+
+// IAudioCallback.h: interface for the IAudioCallback class.
+//
+//////////////////////////////////////////////////////////////////////
+
+class IAudioCallback
+{
+public:
+ IAudioCallback() = default;
+ virtual ~IAudioCallback() = default;
+ virtual void OnInitialize(int iChannels, int iSamplesPerSec, int iBitsPerSample) = 0;
+ virtual void OnAudioData(const float* pAudioData, unsigned int iAudioDataLength) = 0;
+};
+
diff --git a/xbmc/cores/AudioEngine/Interfaces/ThreadedAE.h b/xbmc/cores/AudioEngine/Interfaces/ThreadedAE.h
new file mode 100644
index 0000000..336b200
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Interfaces/ThreadedAE.h
@@ -0,0 +1,20 @@
+/*
+ * 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 "AE.h"
+#include "threads/IRunnable.h"
+
+class IThreadedAE : public IAE, public IRunnable
+{
+public:
+ virtual void Run () = 0;
+ virtual void Stop() = 0;
+};
+
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkALSA.cpp b/xbmc/cores/AudioEngine/Sinks/AESinkALSA.cpp
new file mode 100644
index 0000000..3e7b7b2
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkALSA.cpp
@@ -0,0 +1,1691 @@
+/*
+ * 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 "AESinkALSA.h"
+
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.h"
+#ifndef HAVE_X11
+#include "cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.h"
+#endif
+#include "cores/AudioEngine/Utils/AEELDParser.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "platform/Platform.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <limits.h>
+#include <set>
+#include <sstream>
+#include <stdint.h>
+#include <string>
+
+#include <sys/utsname.h>
+
+using namespace std::chrono_literals;
+
+#define ALSA_OPTIONS (SND_PCM_NO_AUTO_FORMAT | SND_PCM_NO_AUTO_CHANNELS | SND_PCM_NO_AUTO_RESAMPLE)
+
+#define ALSA_MAX_CHANNELS 16
+static enum AEChannel LegacyALSAChannelMap[ALSA_MAX_CHANNELS + 1] = {
+ AE_CH_FL , AE_CH_FR , AE_CH_BL , AE_CH_BR , AE_CH_FC , AE_CH_LFE , AE_CH_SL , AE_CH_SR ,
+ AE_CH_UNKNOWN1, AE_CH_UNKNOWN2, AE_CH_UNKNOWN3, AE_CH_UNKNOWN4, AE_CH_UNKNOWN5, AE_CH_UNKNOWN6, AE_CH_UNKNOWN7, AE_CH_UNKNOWN8, /* for p16v devices */
+ AE_CH_NULL
+};
+
+static enum AEChannel LegacyALSAChannelMap51Wide[ALSA_MAX_CHANNELS + 1] = {
+ AE_CH_FL , AE_CH_FR , AE_CH_SL , AE_CH_SR , AE_CH_FC , AE_CH_LFE , AE_CH_BL , AE_CH_BR ,
+ AE_CH_UNKNOWN1, AE_CH_UNKNOWN2, AE_CH_UNKNOWN3, AE_CH_UNKNOWN4, AE_CH_UNKNOWN5, AE_CH_UNKNOWN6, AE_CH_UNKNOWN7, AE_CH_UNKNOWN8, /* for p16v devices */
+ AE_CH_NULL
+};
+
+static enum AEChannel ALSAChannelMapPassthrough[ALSA_MAX_CHANNELS + 1] = {
+ AE_CH_RAW , AE_CH_RAW , AE_CH_RAW , AE_CH_RAW , AE_CH_RAW , AE_CH_RAW , AE_CH_RAW , AE_CH_RAW ,
+ AE_CH_UNKNOWN1, AE_CH_UNKNOWN2, AE_CH_UNKNOWN3, AE_CH_UNKNOWN4, AE_CH_UNKNOWN5, AE_CH_UNKNOWN6, AE_CH_UNKNOWN7, AE_CH_UNKNOWN8, /* for p16v devices */
+ AE_CH_NULL
+};
+
+static unsigned int ALSASampleRateList[] =
+{
+ 5512,
+ 8000,
+ 11025,
+ 16000,
+ 22050,
+ 32000,
+ 44100,
+ 48000,
+ 64000,
+ 88200,
+ 96000,
+ 176400,
+ 192000,
+ 384000,
+ 0
+};
+
+namespace
+{
+struct SndConfigDeleter
+{
+ void operator()(snd_config_t* p) { snd_config_delete(p); }
+};
+
+inline std::unique_ptr<snd_config_t, SndConfigDeleter> SndConfigCopy(snd_config_t* original)
+{
+ snd_config_t* config;
+ snd_config_copy(&config, original);
+ return std::unique_ptr<snd_config_t, SndConfigDeleter>(config, SndConfigDeleter());
+}
+} // namespace
+
+CAESinkALSA::CAESinkALSA() :
+ m_pcm(NULL)
+{
+ /* ensure that ALSA has been initialized */
+ if (!snd_config)
+ snd_config_update();
+}
+
+CAESinkALSA::~CAESinkALSA()
+{
+ Deinitialize();
+}
+
+void CAESinkALSA::Register()
+{
+ AE::AESinkRegEntry entry;
+ entry.sinkName = "ALSA";
+ entry.createFunc = CAESinkALSA::Create;
+ entry.enumerateFunc = CAESinkALSA::EnumerateDevicesEx;
+ entry.cleanupFunc = CAESinkALSA::Cleanup;
+ AE::CAESinkFactory::RegisterSink(entry);
+}
+
+IAESink* CAESinkALSA::Create(std::string &device, AEAudioFormat& desiredFormat)
+{
+ IAESink* sink = new CAESinkALSA();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+
+inline CAEChannelInfo CAESinkALSA::GetChannelLayoutRaw(const AEAudioFormat& format)
+{
+ unsigned int count = 0;
+
+ switch (format.m_streamInfo.m_type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ count = 8;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ count = 2;
+ break;
+ default:
+ count = 0;
+ break;
+ }
+
+ CAEChannelInfo info;
+ for (unsigned int i = 0; i < count; ++i)
+ info += ALSAChannelMapPassthrough[i];
+
+ return info;
+}
+
+inline CAEChannelInfo CAESinkALSA::GetChannelLayoutLegacy(const AEAudioFormat& format, unsigned int minChannels, unsigned int maxChannels)
+{
+ enum AEChannel* channelMap = LegacyALSAChannelMap;
+ unsigned int count = 0;
+
+ if (format.m_dataFormat == AE_FMT_RAW)
+ return GetChannelLayoutRaw(format);
+
+ // According to CEA-861-D only RL and RR are known. In case of a format having SL and SR channels
+ // but no BR BL channels, we use the wide map in order to open only the num of channels really
+ // needed.
+ if (format.m_channelLayout.HasChannel(AE_CH_SL) && !format.m_channelLayout.HasChannel(AE_CH_BL))
+ {
+ channelMap = LegacyALSAChannelMap51Wide;
+ }
+ for (unsigned int c = 0; c < 8; ++c)
+ {
+ for (unsigned int i = 0; i < format.m_channelLayout.Count(); ++i)
+ {
+ if (format.m_channelLayout[i] == channelMap[c])
+ {
+ count = c + 1;
+ break;
+ }
+ }
+ }
+ count = std::max(count, minChannels);
+ count = std::min(count, maxChannels);
+
+ CAEChannelInfo info;
+ for (unsigned int i = 0; i < count; ++i)
+ info += channelMap[i];
+
+ return info;
+}
+
+inline CAEChannelInfo CAESinkALSA::GetChannelLayout(const AEAudioFormat& format, unsigned int channels)
+{
+ CAEChannelInfo info;
+ std::string alsaMapStr("none");
+
+ if (format.m_dataFormat == AE_FMT_RAW)
+ {
+ info = GetChannelLayoutRaw(format);
+ }
+ else
+ {
+ /* ask for the actual map */
+ snd_pcm_chmap_t* actualMap = snd_pcm_get_chmap(m_pcm);
+ if (actualMap)
+ {
+ alsaMapStr = ALSAchmapToString(actualMap);
+
+ info = ALSAchmapToAEChannelMap(actualMap);
+
+ /* "fake" a compatible map if it is more suitable for AE */
+ if (!info.ContainsChannels(format.m_channelLayout))
+ {
+ CAEChannelInfo infoAlternate = GetAlternateLayoutForm(info);
+ if (infoAlternate.Count())
+ {
+ std::vector<CAEChannelInfo> alts;
+ alts.push_back(info);
+ alts.push_back(infoAlternate);
+ if (format.m_channelLayout.BestMatch(alts) == 1)
+ info = infoAlternate;
+ }
+ }
+
+ /* add empty channels as needed (with e.g. FL,FR,LFE in 4ch) */
+ while (info.Count() < channels)
+ info += AE_CH_UNKNOWN1;
+
+ free(actualMap);
+ }
+ else
+ {
+ info = GetChannelLayoutLegacy(format, channels, channels);
+ }
+ }
+
+ CLog::Log(LOGDEBUG,
+ "CAESinkALSA::GetChannelLayout - Input Channel Count: {} Output Channel Count: {}",
+ format.m_channelLayout.Count(), info.Count());
+ CLog::Log(LOGDEBUG, "CAESinkALSA::GetChannelLayout - Requested Layout: {}",
+ std::string(format.m_channelLayout));
+ CLog::Log(LOGDEBUG, "CAESinkALSA::GetChannelLayout - Got Layout: {} (ALSA: {})",
+ std::string(info), alsaMapStr);
+
+ return info;
+}
+
+AEChannel CAESinkALSA::ALSAChannelToAEChannel(unsigned int alsaChannel)
+{
+ AEChannel aeChannel;
+ switch (alsaChannel)
+ {
+ case SND_CHMAP_FL: aeChannel = AE_CH_FL; break;
+ case SND_CHMAP_FR: aeChannel = AE_CH_FR; break;
+ case SND_CHMAP_FC: aeChannel = AE_CH_FC; break;
+ case SND_CHMAP_LFE: aeChannel = AE_CH_LFE; break;
+ case SND_CHMAP_RL: aeChannel = AE_CH_BL; break;
+ case SND_CHMAP_RR: aeChannel = AE_CH_BR; break;
+ case SND_CHMAP_FLC: aeChannel = AE_CH_FLOC; break;
+ case SND_CHMAP_FRC: aeChannel = AE_CH_FROC; break;
+ case SND_CHMAP_RC: aeChannel = AE_CH_BC; break;
+ case SND_CHMAP_SL: aeChannel = AE_CH_SL; break;
+ case SND_CHMAP_SR: aeChannel = AE_CH_SR; break;
+ case SND_CHMAP_TFL: aeChannel = AE_CH_TFL; break;
+ case SND_CHMAP_TFR: aeChannel = AE_CH_TFR; break;
+ case SND_CHMAP_TFC: aeChannel = AE_CH_TFC; break;
+ case SND_CHMAP_TC: aeChannel = AE_CH_TC; break;
+ case SND_CHMAP_TRL: aeChannel = AE_CH_TBL; break;
+ case SND_CHMAP_TRR: aeChannel = AE_CH_TBR; break;
+ case SND_CHMAP_TRC: aeChannel = AE_CH_TBC; break;
+ case SND_CHMAP_RLC: aeChannel = AE_CH_BLOC; break;
+ case SND_CHMAP_RRC: aeChannel = AE_CH_BROC; break;
+ default: aeChannel = AE_CH_UNKNOWN1; break;
+ }
+ return aeChannel;
+}
+
+unsigned int CAESinkALSA::AEChannelToALSAChannel(AEChannel aeChannel)
+{
+ unsigned int alsaChannel;
+ switch (aeChannel)
+ {
+ case AE_CH_FL: alsaChannel = SND_CHMAP_FL; break;
+ case AE_CH_FR: alsaChannel = SND_CHMAP_FR; break;
+ case AE_CH_FC: alsaChannel = SND_CHMAP_FC; break;
+ case AE_CH_LFE: alsaChannel = SND_CHMAP_LFE; break;
+ case AE_CH_BL: alsaChannel = SND_CHMAP_RL; break;
+ case AE_CH_BR: alsaChannel = SND_CHMAP_RR; break;
+ case AE_CH_FLOC: alsaChannel = SND_CHMAP_FLC; break;
+ case AE_CH_FROC: alsaChannel = SND_CHMAP_FRC; break;
+ case AE_CH_BC: alsaChannel = SND_CHMAP_RC; break;
+ case AE_CH_SL: alsaChannel = SND_CHMAP_SL; break;
+ case AE_CH_SR: alsaChannel = SND_CHMAP_SR; break;
+ case AE_CH_TFL: alsaChannel = SND_CHMAP_TFL; break;
+ case AE_CH_TFR: alsaChannel = SND_CHMAP_TFR; break;
+ case AE_CH_TFC: alsaChannel = SND_CHMAP_TFC; break;
+ case AE_CH_TC: alsaChannel = SND_CHMAP_TC; break;
+ case AE_CH_TBL: alsaChannel = SND_CHMAP_TRL; break;
+ case AE_CH_TBR: alsaChannel = SND_CHMAP_TRR; break;
+ case AE_CH_TBC: alsaChannel = SND_CHMAP_TRC; break;
+ case AE_CH_BLOC: alsaChannel = SND_CHMAP_RLC; break;
+ case AE_CH_BROC: alsaChannel = SND_CHMAP_RRC; break;
+ default: alsaChannel = SND_CHMAP_UNKNOWN; break;
+ }
+ return alsaChannel;
+}
+
+CAEChannelInfo CAESinkALSA::ALSAchmapToAEChannelMap(snd_pcm_chmap_t* alsaMap)
+{
+ CAEChannelInfo info;
+
+ for (unsigned int i = 0; i < alsaMap->channels; i++)
+ info += ALSAChannelToAEChannel(alsaMap->pos[i]);
+
+ return info;
+}
+
+snd_pcm_chmap_t* CAESinkALSA::AEChannelMapToALSAchmap(const CAEChannelInfo& info)
+{
+ int AECount = info.Count();
+ snd_pcm_chmap_t* alsaMap = (snd_pcm_chmap_t*)malloc(sizeof(snd_pcm_chmap_t) + AECount * sizeof(int));
+
+ alsaMap->channels = AECount;
+
+ for (int i = 0; i < AECount; i++)
+ alsaMap->pos[i] = AEChannelToALSAChannel(info[i]);
+
+ return alsaMap;
+}
+
+snd_pcm_chmap_t* CAESinkALSA::CopyALSAchmap(snd_pcm_chmap_t* alsaMap)
+{
+ snd_pcm_chmap_t* copyMap = (snd_pcm_chmap_t*)malloc(sizeof(snd_pcm_chmap_t) + alsaMap->channels * sizeof(int));
+
+ copyMap->channels = alsaMap->channels;
+ memcpy(copyMap->pos, alsaMap->pos, alsaMap->channels * sizeof(int));
+
+ return copyMap;
+}
+
+std::string CAESinkALSA::ALSAchmapToString(snd_pcm_chmap_t* alsaMap)
+{
+ char buf[128] = {};
+ //! @bug ALSA bug - buffer overflow by a factor of 2 is possible
+ //! http://mailman.alsa-project.org/pipermail/alsa-devel/2014-December/085815.html
+ int err = snd_pcm_chmap_print(alsaMap, sizeof(buf) / 2, buf);
+ if (err < 0)
+ return "Error";
+ return std::string(buf);
+}
+
+CAEChannelInfo CAESinkALSA::GetAlternateLayoutForm(const CAEChannelInfo& info)
+{
+ CAEChannelInfo altLayout;
+
+ /* only handle symmetrical layouts */
+ if (info.HasChannel(AE_CH_BL) == info.HasChannel(AE_CH_BR) &&
+ info.HasChannel(AE_CH_SL) == info.HasChannel(AE_CH_SR) &&
+ info.HasChannel(AE_CH_BLOC) == info.HasChannel(AE_CH_BROC))
+ {
+ /* CEA-861-D used by HDMI 1.x has 7.1 as back+back-x-of-center, not
+ * side+back. Mangle it here. */
+ if (info.HasChannel(AE_CH_SL) && info.HasChannel(AE_CH_BL) && !info.HasChannel(AE_CH_BLOC))
+ {
+ altLayout = info;
+ altLayout.ReplaceChannel(AE_CH_BL, AE_CH_BLOC);
+ altLayout.ReplaceChannel(AE_CH_BR, AE_CH_BROC);
+ altLayout.ReplaceChannel(AE_CH_SL, AE_CH_BL);
+ altLayout.ReplaceChannel(AE_CH_SR, AE_CH_BR);
+ }
+ /* same in reverse */
+ else if (!info.HasChannel(AE_CH_SL) && info.HasChannel(AE_CH_BL) && info.HasChannel(AE_CH_BLOC))
+ {
+ altLayout = info;
+ altLayout.ReplaceChannel(AE_CH_BL, AE_CH_SL);
+ altLayout.ReplaceChannel(AE_CH_BR, AE_CH_SR);
+ altLayout.ReplaceChannel(AE_CH_BLOC, AE_CH_BL);
+ altLayout.ReplaceChannel(AE_CH_BROC, AE_CH_BR);
+ }
+ /* We have side speakers but no back speakers, allow map to back
+ * speakers. */
+ else if (info.HasChannel(AE_CH_SL) && !info.HasChannel(AE_CH_BL))
+ {
+ altLayout = info;
+ altLayout.ReplaceChannel(AE_CH_SL, AE_CH_BL);
+ altLayout.ReplaceChannel(AE_CH_SR, AE_CH_BR);
+ }
+ /* reverse */
+ else if (!info.HasChannel(AE_CH_SL) && info.HasChannel(AE_CH_BL))
+ {
+ altLayout = info;
+ altLayout.ReplaceChannel(AE_CH_BL, AE_CH_SL);
+ altLayout.ReplaceChannel(AE_CH_BR, AE_CH_SR);
+ }
+ }
+ return altLayout;
+}
+
+snd_pcm_chmap_t* CAESinkALSA::SelectALSAChannelMap(const CAEChannelInfo& info)
+{
+ snd_pcm_chmap_t* chmap = NULL;
+ snd_pcm_chmap_query_t** supportedMaps;
+
+ supportedMaps = snd_pcm_query_chmaps(m_pcm);
+
+ if (!supportedMaps)
+ return NULL;
+
+ CAEChannelInfo infoAlternate = GetAlternateLayoutForm(info);
+
+ /* for efficiency, first try to find an exact match, and only then fallback
+ * to searching for less perfect matches */
+ int i = 0;
+ for (snd_pcm_chmap_query_t* supportedMap = supportedMaps[i++];
+ supportedMap; supportedMap = supportedMaps[i++])
+ {
+ if (supportedMap->map.channels == info.Count())
+ {
+ CAEChannelInfo candidate = ALSAchmapToAEChannelMap(&supportedMap->map);
+ const CAEChannelInfo* selectedInfo = &info;
+
+ if (!candidate.ContainsChannels(info) || !info.ContainsChannels(candidate))
+ {
+ selectedInfo = &infoAlternate;
+ if (!candidate.ContainsChannels(infoAlternate) || !infoAlternate.ContainsChannels(candidate))
+ continue;
+ }
+
+ if (supportedMap->type == SND_CHMAP_TYPE_VAR)
+ {
+ /* device supports the AE map directly */
+ chmap = AEChannelMapToALSAchmap(*selectedInfo);
+ break;
+ }
+ else
+ {
+ /* device needs 1:1 remapping */
+ chmap = CopyALSAchmap(&supportedMap->map);
+ break;
+ }
+ }
+ }
+
+ /* if no exact chmap was found, fallback to best-effort */
+ if (!chmap)
+ {
+ CAEChannelInfo allChannels;
+ std::vector<CAEChannelInfo> supportedMapsAE;
+
+ /* Convert the ALSA maps to AE maps. */
+ int i = 0;
+ for (snd_pcm_chmap_query_t* supportedMap = supportedMaps[i++];
+ supportedMap; supportedMap = supportedMaps[i++])
+ supportedMapsAE.push_back(ALSAchmapToAEChannelMap(&supportedMap->map));
+
+ int score = 0;
+ int best = info.BestMatch(supportedMapsAE, &score);
+
+ /* see if we find a better result with the alternate form */
+ if (infoAlternate.Count() && score < 0)
+ {
+ int scoreAlt = 0;
+ int bestAlt = infoAlternate.BestMatch(supportedMapsAE, &scoreAlt);
+ if (scoreAlt > score)
+ best = bestAlt;
+ }
+
+ if (best > 0)
+ chmap = CopyALSAchmap(&supportedMaps[best]->map);
+ }
+
+ if (chmap && CServiceBroker::GetLogging().CanLogComponent(LOGAUDIO))
+ CLog::Log(LOGDEBUG, "CAESinkALSA::SelectALSAChannelMap - Selected ALSA map \"{}\"",
+ ALSAchmapToString(chmap));
+
+ snd_pcm_free_chmaps(supportedMaps);
+ return chmap;
+}
+
+void CAESinkALSA::GetAESParams(const AEAudioFormat& format, std::string& params)
+{
+ if (m_passthrough)
+ params = "AES0=0x06";
+ else
+ params = "AES0=0x04";
+
+ params += ",AES1=0x82,AES2=0x00";
+
+ if (m_passthrough && format.m_channelLayout.Count() == 8) params += ",AES3=0x09";
+ else if (format.m_sampleRate == 192000) params += ",AES3=0x0e";
+ else if (format.m_sampleRate == 176400) params += ",AES3=0x0c";
+ else if (format.m_sampleRate == 96000) params += ",AES3=0x0a";
+ else if (format.m_sampleRate == 88200) params += ",AES3=0x08";
+ else if (format.m_sampleRate == 48000) params += ",AES3=0x02";
+ else if (format.m_sampleRate == 44100) params += ",AES3=0x00";
+ else if (format.m_sampleRate == 32000) params += ",AES3=0x03";
+ else params += ",AES3=0x01";
+}
+
+bool CAESinkALSA::Initialize(AEAudioFormat &format, std::string &device)
+{
+ m_initDevice = device;
+ m_initFormat = format;
+ ALSAConfig inconfig, outconfig;
+ inconfig.format = format.m_dataFormat;
+ inconfig.sampleRate = format.m_sampleRate;
+
+ /*
+ * We can't use the better GetChannelLayout() at this point as the device
+ * is not opened yet, and we need inconfig.channels to select the correct
+ * device... Legacy layouts should be accurate enough for device selection
+ * in all cases, though.
+ */
+ inconfig.channels = GetChannelLayoutLegacy(format, 2, 8).Count();
+
+ /* if we are raw, correct the data format */
+ if (format.m_dataFormat == AE_FMT_RAW)
+ {
+ inconfig.format = AE_FMT_S16NE;
+ m_passthrough = true;
+ }
+ else
+ {
+ m_passthrough = false;
+ }
+
+ if (inconfig.channels == 0)
+ {
+ CLog::Log(LOGERROR, "CAESinkALSA::Initialize - Unable to open the requested channel layout");
+ return false;
+ }
+
+ AEDeviceType devType = AEDeviceTypeFromName(device);
+
+ std::string AESParams;
+ /* digital interfaces should have AESx set, though in practice most
+ * receivers don't care */
+ if (m_passthrough || devType == AE_DEVTYPE_HDMI || devType == AE_DEVTYPE_IEC958)
+ GetAESParams(format, AESParams);
+
+ CLog::Log(LOGINFO, "CAESinkALSA::Initialize - Attempting to open device \"{}\"", device);
+
+ /* get the sound config */
+ std::unique_ptr<snd_config_t, SndConfigDeleter> config = SndConfigCopy(snd_config);
+
+ if (!OpenPCMDevice(device, AESParams, inconfig.channels, &m_pcm, config.get()))
+ {
+ CLog::Log(LOGERROR, "CAESinkALSA::Initialize - failed to initialize device \"{}\"", device);
+ return false;
+ }
+
+ /* get the actual device name that was used */
+ device = snd_pcm_name(m_pcm);
+ m_device = device;
+
+ CLog::Log(LOGINFO, "CAESinkALSA::Initialize - Opened device \"{}\"", device);
+
+ snd_pcm_chmap_t* selectedChmap = NULL;
+ if (!m_passthrough)
+ {
+ selectedChmap = SelectALSAChannelMap(format.m_channelLayout);
+ if (selectedChmap)
+ {
+ /* update wanted channel count according to the selected map */
+ inconfig.channels = selectedChmap->channels;
+ }
+ }
+
+ if (!InitializeHW(inconfig, outconfig) || !InitializeSW(outconfig))
+ {
+ free(selectedChmap);
+ return false;
+ }
+
+ if (selectedChmap)
+ {
+ /* failure is OK, that likely just means the selected chmap is fixed already */
+ snd_pcm_set_chmap(m_pcm, selectedChmap);
+ free(selectedChmap);
+ }
+
+ // we want it blocking
+ snd_pcm_nonblock(m_pcm, 0);
+ snd_pcm_prepare (m_pcm);
+
+ if (m_passthrough && inconfig.channels != outconfig.channels)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA::Initialize - could not open required number of channels");
+ return false;
+ }
+ // adjust format to the configuration we got
+ format.m_channelLayout = GetChannelLayout(format, outconfig.channels);
+ // we might end up with an unusable channel layout that contains only UNKNOWN
+ // channels, let's do a sanity check.
+ if (!format.m_channelLayout.IsLayoutValid())
+ return false;
+
+ format.m_sampleRate = outconfig.sampleRate;
+ format.m_frames = outconfig.periodSize;
+ format.m_frameSize = outconfig.frameSize;
+ format.m_dataFormat = outconfig.format;
+
+ m_format = format;
+ m_formatSampleRateMul = 1.0 / (double)m_format.m_sampleRate;
+
+ return true;
+}
+
+snd_pcm_format_t CAESinkALSA::AEFormatToALSAFormat(const enum AEDataFormat format)
+{
+ if (format == AE_FMT_RAW)
+ return SND_PCM_FORMAT_S16;
+
+ switch (format)
+ {
+ case AE_FMT_U8 : return SND_PCM_FORMAT_U8;
+ case AE_FMT_S16NE : return SND_PCM_FORMAT_S16;
+ case AE_FMT_S16LE : return SND_PCM_FORMAT_S16_LE;
+ case AE_FMT_S16BE : return SND_PCM_FORMAT_S16_BE;
+ case AE_FMT_S24NE4: return SND_PCM_FORMAT_S24;
+#ifdef __BIG_ENDIAN__
+ case AE_FMT_S24NE3: return SND_PCM_FORMAT_S24_3BE;
+#else
+ case AE_FMT_S24NE3: return SND_PCM_FORMAT_S24_3LE;
+#endif
+ case AE_FMT_S32NE : return SND_PCM_FORMAT_S32;
+ case AE_FMT_FLOAT : return SND_PCM_FORMAT_FLOAT;
+
+ default:
+ return SND_PCM_FORMAT_UNKNOWN;
+ }
+}
+
+bool CAESinkALSA::InitializeHW(const ALSAConfig &inconfig, ALSAConfig &outconfig)
+{
+ snd_pcm_hw_params_t *hw_params;
+
+ snd_pcm_hw_params_alloca(&hw_params);
+ memset(hw_params, 0, snd_pcm_hw_params_sizeof());
+
+ snd_pcm_hw_params_any(m_pcm, hw_params);
+ snd_pcm_hw_params_set_access(m_pcm, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
+
+ unsigned int sampleRate = inconfig.sampleRate;
+ snd_pcm_hw_params_set_rate_near (m_pcm, hw_params, &sampleRate, NULL);
+
+ unsigned int channelCount = inconfig.channels;
+ /* select a channel count >=wanted, or otherwise the highest available */
+ if (snd_pcm_hw_params_set_channels_min(m_pcm, hw_params, &channelCount) == 0)
+ snd_pcm_hw_params_set_channels_first(m_pcm, hw_params, &channelCount);
+ else
+ snd_pcm_hw_params_set_channels_last(m_pcm, hw_params, &channelCount);
+
+ /* ensure we opened X channels or more */
+ if (inconfig.channels > channelCount)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA::InitializeHW - Unable to open the required number of channels");
+ }
+
+ /* update outconfig */
+ outconfig.channels = channelCount;
+
+ snd_pcm_format_t fmt = AEFormatToALSAFormat(inconfig.format);
+ outconfig.format = inconfig.format;
+
+ if (fmt == SND_PCM_FORMAT_UNKNOWN)
+ {
+ /* if we dont support the requested format, fallback to float */
+ fmt = SND_PCM_FORMAT_FLOAT;
+ outconfig.format = AE_FMT_FLOAT;
+ }
+
+ snd_pcm_hw_params_t *hw_params_copy;
+ snd_pcm_hw_params_alloca(&hw_params_copy);
+ snd_pcm_hw_params_copy(hw_params_copy, hw_params); // copy what we have
+
+ /* try the data format */
+ if (snd_pcm_hw_params_set_format(m_pcm, hw_params, fmt) < 0)
+ {
+ /* if the chosen format is not supported, try each one in descending order */
+ CLog::Log(LOGINFO,
+ "CAESinkALSA::InitializeHW - Your hardware does not support {}, trying other formats",
+ CAEUtil::DataFormatToStr(outconfig.format));
+ for (enum AEDataFormat i = AE_FMT_MAX; i > AE_FMT_INVALID; i = (enum AEDataFormat)((int)i - 1))
+ {
+ if (i == AE_FMT_RAW || i == AE_FMT_MAX)
+ continue;
+
+ if (m_passthrough && i != AE_FMT_S16BE && i != AE_FMT_S16LE)
+ continue;
+
+ fmt = AEFormatToALSAFormat(i);
+ if (fmt == SND_PCM_FORMAT_UNKNOWN)
+ continue;
+
+ snd_pcm_hw_params_copy(hw_params, hw_params_copy); // restore from copy
+ if (snd_pcm_hw_params_set_format(m_pcm, hw_params, fmt) < 0)
+ {
+ fmt = SND_PCM_FORMAT_UNKNOWN;
+ continue;
+ }
+
+ int fmtBits = CAEUtil::DataFormatToBits(i);
+ int bits = snd_pcm_hw_params_get_sbits(hw_params);
+
+ // skip bits check when alsa reports invalid sbits value
+ if (bits > 0 && bits != fmtBits)
+ {
+ /* if we opened in 32bit and only have 24bits, signal it accordingly */
+ if (fmt == SND_PCM_FORMAT_S32 && bits == 24)
+ i = AE_FMT_S24NE4MSB;
+ else if (fmt == SND_PCM_FORMAT_S24 && bits == 24)
+ i = AE_FMT_S24NE4;
+ else
+ continue;
+ }
+
+ /* record that the format fell back to X */
+ outconfig.format = i;
+ CLog::Log(LOGINFO, "CAESinkALSA::InitializeHW - Using data format {}",
+ CAEUtil::DataFormatToStr(outconfig.format));
+ break;
+ }
+
+ /* if we failed to find a valid output format */
+ if (fmt == SND_PCM_FORMAT_UNKNOWN)
+ {
+ CLog::Log(LOGERROR, "CAESinkALSA::InitializeHW - Unable to find a suitable output format");
+ return false;
+ }
+ }
+
+ snd_pcm_uframes_t periodSize, bufferSize;
+ snd_pcm_hw_params_get_buffer_size_max(hw_params, &bufferSize);
+ snd_pcm_hw_params_get_period_size_max(hw_params, &periodSize, NULL);
+
+ /*
+ We want to make sure, that we have max 200 ms Buffer with
+ a periodSize of approx 50 ms. Choosing a higher bufferSize
+ will cause problems with menu sounds. Buffer will be increased
+ after those are fixed.
+ */
+ periodSize = std::min(periodSize, (snd_pcm_uframes_t) sampleRate / 20);
+ bufferSize = std::min(bufferSize, (snd_pcm_uframes_t) sampleRate / 5);
+
+ /*
+ According to upstream we should set buffer size first - so make sure it is always at least
+ 4x period size to not get underruns (some systems seem to have issues with only 2 periods)
+ */
+ periodSize = std::min(periodSize, bufferSize / 4);
+
+ CLog::Log(LOGDEBUG, "CAESinkALSA::InitializeHW - Request: periodSize {}, bufferSize {}",
+ periodSize, bufferSize);
+
+ snd_pcm_hw_params_copy(hw_params_copy, hw_params); // copy what we have and is already working
+
+ // Make sure to not initialize too large to not cause underruns
+ snd_pcm_uframes_t periodSizeMax = bufferSize / 3;
+ if(snd_pcm_hw_params_set_period_size_max(m_pcm, hw_params_copy, &periodSizeMax, NULL) != 0)
+ {
+ snd_pcm_hw_params_copy(hw_params_copy, hw_params); // restore working copy
+ CLog::Log(LOGDEBUG, "CAESinkALSA::InitializeHW - Request: Failed to limit periodSize to {}",
+ periodSizeMax);
+ }
+
+ // first trying bufferSize, PeriodSize
+ // for more info see here:
+ // http://mailman.alsa-project.org/pipermail/alsa-devel/2009-September/021069.html
+ // the last three tries are done as within pulseaudio
+
+ // backup periodSize and bufferSize first. Restore them after every failed try
+ snd_pcm_uframes_t periodSizeTemp, bufferSizeTemp;
+ periodSizeTemp = periodSize;
+ bufferSizeTemp = bufferSize;
+ if (snd_pcm_hw_params_set_buffer_size_near(m_pcm, hw_params_copy, &bufferSize) != 0
+ || snd_pcm_hw_params_set_period_size_near(m_pcm, hw_params_copy, &periodSize, NULL) != 0
+ || snd_pcm_hw_params(m_pcm, hw_params_copy) != 0)
+ {
+ bufferSize = bufferSizeTemp;
+ periodSize = periodSizeTemp;
+ // retry with PeriodSize, bufferSize
+ snd_pcm_hw_params_copy(hw_params_copy, hw_params); // restore working copy
+ if (snd_pcm_hw_params_set_period_size_near(m_pcm, hw_params_copy, &periodSize, NULL) != 0
+ || snd_pcm_hw_params_set_buffer_size_near(m_pcm, hw_params_copy, &bufferSize) != 0
+ || snd_pcm_hw_params(m_pcm, hw_params_copy) != 0)
+ {
+ // try only periodSize
+ periodSize = periodSizeTemp;
+ snd_pcm_hw_params_copy(hw_params_copy, hw_params); // restore working copy
+ if(snd_pcm_hw_params_set_period_size_near(m_pcm, hw_params_copy, &periodSize, NULL) != 0
+ || snd_pcm_hw_params(m_pcm, hw_params_copy) != 0)
+ {
+ // try only BufferSize
+ bufferSize = bufferSizeTemp;
+ snd_pcm_hw_params_copy(hw_params_copy, hw_params); // restore working copy
+ if (snd_pcm_hw_params_set_buffer_size_near(m_pcm, hw_params_copy, &bufferSize) != 0
+ || snd_pcm_hw_params(m_pcm, hw_params_copy) != 0)
+ {
+ // set default that Alsa would choose
+ CLog::Log(LOGWARNING, "CAESinkAlsa::InitializeHW - Using default alsa values - set failed");
+ if (snd_pcm_hw_params(m_pcm, hw_params) != 0)
+ {
+ CLog::Log(LOGDEBUG, "CAESinkALSA::InitializeHW - Could not init a valid sink");
+ return false;
+ }
+ }
+ }
+ // reread values when alsa default was kept
+ snd_pcm_get_params(m_pcm, &bufferSize, &periodSize);
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "CAESinkALSA::InitializeHW - Got: periodSize {}, bufferSize {}", periodSize,
+ bufferSize);
+
+ /* set the format parameters */
+ outconfig.sampleRate = sampleRate;
+
+ /* if periodSize is too small Audio Engine might starve */
+ m_fragmented = false;
+ unsigned int fragments = 1;
+ if (periodSize < AE_MIN_PERIODSIZE)
+ {
+ fragments = std::ceil((double) AE_MIN_PERIODSIZE / periodSize);
+ CLog::Log(LOGDEBUG, "Audio Driver reports too low periodSize {} - will use {} fragments",
+ (int)periodSize, (int)fragments);
+ m_fragmented = true;
+ }
+
+ m_originalPeriodSize = periodSize;
+ outconfig.periodSize = fragments * periodSize;
+ outconfig.frameSize = snd_pcm_frames_to_bytes(m_pcm, 1);
+
+ m_bufferSize = (unsigned int)bufferSize;
+ m_timeout = std::ceil((double)(bufferSize * 1000) / (double)sampleRate);
+
+ CLog::Log(LOGDEBUG, "CAESinkALSA::InitializeHW - Setting timeout to {} ms", m_timeout);
+
+ return true;
+}
+
+bool CAESinkALSA::InitializeSW(const ALSAConfig &inconfig)
+{
+ snd_pcm_sw_params_t *sw_params;
+ snd_pcm_uframes_t boundary;
+
+ snd_pcm_sw_params_alloca(&sw_params);
+ memset(sw_params, 0, snd_pcm_sw_params_sizeof());
+
+ snd_pcm_sw_params_current (m_pcm, sw_params);
+ snd_pcm_sw_params_set_start_threshold (m_pcm, sw_params, INT_MAX);
+ snd_pcm_sw_params_set_silence_threshold(m_pcm, sw_params, 0);
+ snd_pcm_sw_params_get_boundary (sw_params, &boundary);
+ snd_pcm_sw_params_set_silence_size (m_pcm, sw_params, boundary);
+ snd_pcm_sw_params_set_avail_min (m_pcm, sw_params, inconfig.periodSize);
+
+ if (snd_pcm_sw_params(m_pcm, sw_params) < 0)
+ {
+ CLog::Log(LOGERROR, "CAESinkALSA::InitializeSW - Failed to set the parameters");
+ return false;
+ }
+
+ return true;
+}
+
+void CAESinkALSA::Deinitialize()
+{
+ if (m_pcm)
+ {
+ Stop();
+ snd_pcm_close(m_pcm);
+ m_pcm = NULL;
+ }
+}
+
+void CAESinkALSA::Stop()
+{
+ if (!m_pcm)
+ return;
+ snd_pcm_drop(m_pcm);
+}
+
+void CAESinkALSA::GetDelay(AEDelayStatus& status)
+{
+ if (!m_pcm)
+ {
+ status.SetDelay(0);
+ return;
+ }
+ snd_pcm_sframes_t frames = 0;
+ snd_pcm_delay(m_pcm, &frames);
+
+ if (frames < 0)
+ {
+ snd_pcm_forward(m_pcm, -frames);
+ frames = 0;
+ }
+
+ status.SetDelay((double)frames * m_formatSampleRateMul);
+}
+
+double CAESinkALSA::GetCacheTotal()
+{
+ return (double)m_bufferSize * m_formatSampleRateMul;
+}
+
+unsigned int CAESinkALSA::AddPackets(uint8_t **data, unsigned int frames, unsigned int offset)
+{
+ if (!m_pcm)
+ {
+ CLog::Log(LOGERROR, "CAESinkALSA - Tried to add packets without a sink");
+ return INT_MAX;
+ }
+
+ void *buffer = data[0]+offset*m_format.m_frameSize;
+ unsigned int amount = 0;
+ int64_t data_left = (int64_t) frames;
+ int frames_written = 0;
+
+ while (data_left > 0)
+ {
+ if (m_fragmented)
+ amount = std::min((unsigned int) data_left, m_originalPeriodSize);
+ else // take care as we can come here a second time if the sink does not eat all data
+ amount = (unsigned int) data_left;
+
+ int ret = snd_pcm_writei(m_pcm, buffer, amount);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CAESinkALSA - snd_pcm_writei({}) {} - trying to recover", ret,
+ snd_strerror(ret));
+ ret = snd_pcm_recover(m_pcm, ret, 1);
+ if(ret < 0)
+ {
+ HandleError("snd_pcm_writei(1)", ret);
+ ret = snd_pcm_writei(m_pcm, buffer, amount);
+ if (ret < 0)
+ {
+ HandleError("snd_pcm_writei(2)", ret);
+ ret = 0;
+ }
+ }
+ }
+
+ if ( ret > 0 && snd_pcm_state(m_pcm) == SND_PCM_STATE_PREPARED)
+ snd_pcm_start(m_pcm);
+
+ if (ret <= 0)
+ break;
+
+ frames_written += ret;
+ data_left -= ret;
+ buffer = data[0]+offset*m_format.m_frameSize + frames_written*m_format.m_frameSize;
+ }
+ return frames_written;
+}
+
+void CAESinkALSA::HandleError(const char* name, int err)
+{
+ switch(err)
+ {
+ case -EPIPE:
+ CLog::Log(LOGERROR, "CAESinkALSA::HandleError({}) - underrun", name);
+ if ((err = snd_pcm_prepare(m_pcm)) < 0)
+ CLog::Log(LOGERROR, "CAESinkALSA::HandleError({}) - snd_pcm_prepare returned {} ({})", name,
+ err, snd_strerror(err));
+ break;
+
+ case -ESTRPIPE:
+ CLog::Log(LOGINFO, "CAESinkALSA::HandleError({}) - Resuming after suspend", name);
+
+ /* try to resume the stream */
+ while((err = snd_pcm_resume(m_pcm)) == -EAGAIN)
+ KODI::TIME::Sleep(1ms);
+
+ /* if the hardware doesn't support resume, prepare the stream */
+ if (err == -ENOSYS)
+ if ((err = snd_pcm_prepare(m_pcm)) < 0)
+ CLog::Log(LOGERROR, "CAESinkALSA::HandleError({}) - snd_pcm_prepare returned {} ({})",
+ name, err, snd_strerror(err));
+ break;
+
+ default:
+ CLog::Log(LOGERROR, "CAESinkALSA::HandleError({}) - snd_pcm_writei returned {} ({})", name,
+ err, snd_strerror(err));
+ break;
+ }
+}
+
+void CAESinkALSA::Drain()
+{
+ if (!m_pcm)
+ return;
+
+ snd_pcm_drain(m_pcm);
+ snd_pcm_prepare(m_pcm);
+}
+
+void CAESinkALSA::AppendParams(std::string &device, const std::string &params)
+{
+ /* Note: escaping, e.g. "plug:'something:X=y'" isn't handled,
+ * but it is not normally encountered at this point. */
+
+ device += (device.find(':') == std::string::npos) ? ':' : ',';
+ device += params;
+}
+
+bool CAESinkALSA::TryDevice(const std::string &name, snd_pcm_t **pcmp, snd_config_t *lconf)
+{
+ /* Check if this device was already open (e.g. when checking for supported
+ * channel count in EnumerateDevice()) */
+ if (*pcmp)
+ {
+ if (name == snd_pcm_name(*pcmp))
+ return true;
+
+ snd_pcm_close(*pcmp);
+ *pcmp = NULL;
+ }
+
+ int err = snd_pcm_open_lconf(pcmp, name.c_str(), SND_PCM_STREAM_PLAYBACK, ALSA_OPTIONS, lconf);
+ if (err < 0)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA - Unable to open device \"{}\" for playback", name);
+ }
+
+ return err == 0;
+}
+
+bool CAESinkALSA::TryDeviceWithParams(const std::string &name, const std::string &params, snd_pcm_t **pcmp, snd_config_t *lconf)
+{
+ if (!params.empty())
+ {
+ std::string nameWithParams = name;
+ AppendParams(nameWithParams, params);
+ if (TryDevice(nameWithParams, pcmp, lconf))
+ return true;
+ }
+
+ /* Try the variant without extra parameters.
+ * Custom devices often do not take the AESx parameters, for example.
+ */
+ return TryDevice(name, pcmp, lconf);
+}
+
+bool CAESinkALSA::OpenPCMDevice(const std::string &name, const std::string &params, int channels, snd_pcm_t **pcmp, snd_config_t *lconf)
+{
+ /* Special name denoting surroundXX mangling. This is needed for some
+ * devices for multichannel to work. */
+ if (name == "@" || name.substr(0, 2) == "@:")
+ {
+ std::string openName = name.substr(1);
+
+ /* These device names allow alsa-lib to perform special routing if needed
+ * for multichannel to work with the audio hardware.
+ * Fall through in switch() so that devices with more channels are
+ * added as fallback. */
+ switch (channels)
+ {
+ case 3:
+ case 4:
+ if (TryDeviceWithParams("surround40" + openName, params, pcmp, lconf))
+ return true;
+ [[fallthrough]];
+ case 5:
+ case 6:
+ if (TryDeviceWithParams("surround51" + openName, params, pcmp, lconf))
+ return true;
+ [[fallthrough]];
+ case 7:
+ case 8:
+ if (TryDeviceWithParams("surround71" + openName, params, pcmp, lconf))
+ return true;
+ }
+
+ /* Try "sysdefault" and "default" (they provide dmix if needed, and route
+ * audio to all extra channels on subdeviced cards),
+ * unless the selected devices is not DEV=0 of the card, in which case
+ * "sysdefault" and "default" would point to another device.
+ * "sysdefault" is a newish device name that won't be overwritten in case
+ * system configuration redefines "default". "default" is still tried
+ * because "sysdefault" is rather new. */
+ size_t devPos = openName.find(",DEV=");
+ if (devPos == std::string::npos || (devPos + 5 < openName.size() && openName[devPos+5] == '0'))
+ {
+ /* "sysdefault" and "default" do not have "DEV=0", drop it */
+ std::string nameWithoutDev = openName;
+ if (devPos != std::string::npos)
+ nameWithoutDev.erase(nameWithoutDev.begin() + devPos, nameWithoutDev.begin() + devPos + 6);
+
+ if (TryDeviceWithParams("sysdefault" + nameWithoutDev, params, pcmp, lconf)
+ || TryDeviceWithParams("default" + nameWithoutDev, params, pcmp, lconf))
+ return true;
+ }
+
+ /* Try "front" (no dmix, no audio in other channels on subdeviced cards) */
+ if (TryDeviceWithParams("front" + openName, params, pcmp, lconf))
+ return true;
+
+ }
+ else
+ {
+ /* Non-surroundXX device, just add it */
+ if (TryDeviceWithParams(name, params, pcmp, lconf))
+ return true;
+ }
+
+ return false;
+}
+
+void CAESinkALSA::EnumerateDevicesEx(AEDeviceInfoList &list, bool force)
+{
+#if defined(HAVE_LIBUDEV)
+ const auto deviceMonitor = CServiceBroker::GetPlatform().GetService<CALSADeviceMonitor>();
+ deviceMonitor->Start();
+#endif
+
+ /* ensure that ALSA has been initialized */
+ snd_lib_error_set_handler(sndLibErrorHandler);
+ if(!snd_config || force)
+ {
+ if(force)
+ snd_config_update_free_global();
+
+ snd_config_update();
+ }
+
+ std::unique_ptr<snd_config_t, SndConfigDeleter> config = SndConfigCopy(snd_config);
+
+#if !defined(HAVE_X11)
+ const auto controlMonitor = CServiceBroker::GetPlatform().GetService<CALSAHControlMonitor>();
+ controlMonitor->Clear();
+#endif
+
+ /* Always enumerate the default device.
+ * Note: If "default" is a stereo device, EnumerateDevice()
+ * will automatically add "@" instead to enable surroundXX mangling.
+ * We don't want to do that if "default" can handle multichannel
+ * itself (e.g. in case of a pulseaudio server). */
+ EnumerateDevice(list, "default", "", config.get());
+
+ void **hints;
+
+ if (snd_device_name_hint(-1, "pcm", &hints) < 0)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA - Unable to get a list of devices");
+ return;
+ }
+
+ std::string defaultDescription;
+
+ for (void** hint = hints; *hint != NULL; ++hint)
+ {
+ char *io = snd_device_name_get_hint(*hint, "IOID");
+ char *name = snd_device_name_get_hint(*hint, "NAME");
+ char *desc = snd_device_name_get_hint(*hint, "DESC");
+ if ((!io || strcmp(io, "Output") == 0) && name
+ && strcmp(name, "null") != 0)
+ {
+ std::string baseName = std::string(name);
+ baseName = baseName.substr(0, baseName.find(':'));
+
+ if (strcmp(name, "default") == 0)
+ {
+ /* added already, but lets get the description if we have one */
+ if (desc)
+ defaultDescription = desc;
+ }
+ else if (baseName == "front")
+ {
+ /* Enumerate using the surroundXX mangling */
+ /* do not enumerate basic "front", it is already handled
+ * by the default "@" entry added in the very beginning */
+ if (strcmp(name, "front") != 0)
+ EnumerateDevice(list, std::string("@") + (name + 5), desc ? desc : name, config.get());
+ }
+
+ /* Do not enumerate "default", it is already enumerated above. */
+
+ /* Do not enumerate the surroundXX devices, those are always accompanied
+ * with a "front" device and it is handled above as "@". The below
+ * devices plus sysdefault will be automatically used if available
+ * for a "@" device.
+ * sysdefault devices are enumerated as not all cards have front/surround
+ * devices. For cards with front/surround devices the sysdefault
+ * entry will be removed in a second pass after enumeration.
+ */
+
+ /* Ubuntu has patched their alsa-lib so that "defaults.namehint.extended"
+ * defaults to "on" instead of upstream "off", causing lots of unwanted
+ * extra devices (many of which are not actually routed properly) to be
+ * found by the enumeration process. Skip them as well ("hw", "dmix",
+ * "plughw", "dsnoop"). */
+
+ else if (baseName != "default"
+ && baseName != "surround40"
+ && baseName != "surround41"
+ && baseName != "surround50"
+ && baseName != "surround51"
+ && baseName != "surround71"
+ && baseName != "hw"
+ && baseName != "dmix"
+ && baseName != "plughw"
+ && baseName != "dsnoop")
+ {
+ EnumerateDevice(list, name, desc ? desc : name, config.get());
+ }
+ }
+ free(io);
+ free(name);
+ free(desc);
+ }
+ snd_device_name_free_hint(hints);
+
+#if !defined(HAVE_X11)
+ controlMonitor->Start();
+#endif
+
+ /* set the displayname for default device */
+ if (!list.empty() && list[0].m_deviceName == "default")
+ {
+ /* If we have one from a hint (DESC), use it */
+ if (!defaultDescription.empty())
+ list[0].m_displayName = defaultDescription;
+ /* Otherwise use the discovered name or (unlikely) "Default" */
+ else if (list[0].m_displayName.empty())
+ list[0].m_displayName = "Default";
+ }
+
+ /* cards with surround entries where sysdefault should be removed */
+ std::set<std::string> cardsWithSurround;
+
+ for (AEDeviceInfoList::iterator it1 = list.begin(); it1 != list.end(); ++it1)
+ {
+ std::string baseName = it1->m_deviceName.substr(0, it1->m_deviceName.find(':'));
+ std::string card = GetParamFromName(it1->m_deviceName, "CARD");
+ if (baseName == "@" && !card.empty())
+ cardsWithSurround.insert(card);
+ }
+
+ if (!cardsWithSurround.empty())
+ {
+ /* remove sysdefault entries where we already have a surround entry */
+ AEDeviceInfoList::iterator iter = list.begin();
+ while (iter != list.end())
+ {
+ std::string baseName = iter->m_deviceName.substr(0, iter->m_deviceName.find(':'));
+ std::string card = GetParamFromName(iter->m_deviceName, "CARD");
+ if (baseName == "sysdefault" && cardsWithSurround.find(card) != cardsWithSurround.end())
+ iter = list.erase(iter);
+ else
+ ++iter;
+ }
+ }
+
+ /* lets check uniqueness, we may need to append DEV or CARD to DisplayName */
+ /* If even a single device of card/dev X clashes with Y, add suffixes to
+ * all devices of both them, for clarity. */
+
+ /* clashing card names, e.g. "NVidia", "NVidia_2" */
+ std::set<std::string> cardsToAppend;
+
+ /* clashing basename + cardname combinations, e.g. ("hdmi","Nvidia") */
+ std::set<std::pair<std::string, std::string> > devsToAppend;
+
+ for (AEDeviceInfoList::iterator it1 = list.begin(); it1 != list.end(); ++it1)
+ {
+ for (AEDeviceInfoList::iterator it2 = it1+1; it2 != list.end(); ++it2)
+ {
+ if (it1->m_displayName == it2->m_displayName
+ && it1->m_displayNameExtra == it2->m_displayNameExtra)
+ {
+ /* something needs to be done */
+ std::string cardString1 = GetParamFromName(it1->m_deviceName, "CARD");
+ std::string cardString2 = GetParamFromName(it2->m_deviceName, "CARD");
+
+ if (cardString1 != cardString2)
+ {
+ /* card name differs, add identifiers to all devices */
+ cardsToAppend.insert(cardString1);
+ cardsToAppend.insert(cardString2);
+ continue;
+ }
+
+ std::string devString1 = GetParamFromName(it1->m_deviceName, "DEV");
+ std::string devString2 = GetParamFromName(it2->m_deviceName, "DEV");
+
+ if (devString1 != devString2)
+ {
+ /* device number differs, add identifiers to all such devices */
+ devsToAppend.insert(std::make_pair(it1->m_deviceName.substr(0, it1->m_deviceName.find(':')), cardString1));
+ devsToAppend.insert(std::make_pair(it2->m_deviceName.substr(0, it2->m_deviceName.find(':')), cardString2));
+ continue;
+ }
+
+ /* if we got here, the configuration is really weird, just append the whole device string */
+ it1->m_displayName += " (" + it1->m_deviceName + ")";
+ it2->m_displayName += " (" + it2->m_deviceName + ")";
+ }
+ }
+ }
+
+ for (std::set<std::string>::iterator it = cardsToAppend.begin();
+ it != cardsToAppend.end(); ++it)
+ {
+ for (AEDeviceInfoList::iterator itl = list.begin(); itl != list.end(); ++itl)
+ {
+ std::string cardString = GetParamFromName(itl->m_deviceName, "CARD");
+ if (cardString == *it)
+ /* "HDA NVidia (NVidia)", "HDA NVidia (NVidia_2)", ... */
+ itl->m_displayName += " (" + cardString + ")";
+ }
+ }
+
+ for (std::set<std::pair<std::string, std::string> >::iterator it = devsToAppend.begin();
+ it != devsToAppend.end(); ++it)
+ {
+ for (AEDeviceInfoList::iterator itl = list.begin(); itl != list.end(); ++itl)
+ {
+ std::string baseName = itl->m_deviceName.substr(0, itl->m_deviceName.find(':'));
+ std::string cardString = GetParamFromName(itl->m_deviceName, "CARD");
+ if (baseName == it->first && cardString == it->second)
+ {
+ std::string devString = GetParamFromName(itl->m_deviceName, "DEV");
+ /* "HDMI #0", "HDMI #1" ... */
+ itl->m_displayNameExtra += " #" + devString;
+ }
+ }
+ }
+}
+
+AEDeviceType CAESinkALSA::AEDeviceTypeFromName(const std::string &name)
+{
+ if (name.substr(0, 4) == "hdmi")
+ return AE_DEVTYPE_HDMI;
+ else if (name.substr(0, 6) == "iec958" || name.substr(0, 5) == "spdif")
+ return AE_DEVTYPE_IEC958;
+
+ return AE_DEVTYPE_PCM;
+}
+
+std::string CAESinkALSA::GetParamFromName(const std::string &name, const std::string &param)
+{
+ /* name = "hdmi:CARD=x,DEV=y" param = "CARD" => return "x" */
+ size_t parPos = name.find(param + '=');
+ if (parPos != std::string::npos)
+ {
+ parPos += param.size() + 1;
+ return name.substr(parPos, name.find_first_of(",'\"", parPos)-parPos);
+ }
+
+ return "";
+}
+
+void CAESinkALSA::EnumerateDevice(AEDeviceInfoList &list, const std::string &device, const std::string &description, snd_config_t *config)
+{
+ snd_pcm_t *pcmhandle = NULL;
+ if (!OpenPCMDevice(device, "", ALSA_MAX_CHANNELS, &pcmhandle, config))
+ return;
+
+ snd_pcm_info_t *pcminfo;
+ snd_pcm_info_alloca(&pcminfo);
+ memset(pcminfo, 0, snd_pcm_info_sizeof());
+
+ int err = snd_pcm_info(pcmhandle, pcminfo);
+ if (err < 0)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA - Unable to get pcm_info for \"{}\"", device);
+ snd_pcm_close(pcmhandle);
+ }
+
+ int cardNr = snd_pcm_info_get_card(pcminfo);
+
+ CAEDeviceInfo info;
+ info.m_deviceName = device;
+ info.m_deviceType = AEDeviceTypeFromName(device);
+
+ if (cardNr >= 0)
+ {
+ /* "HDA NVidia", "HDA Intel", "HDA ATI HDMI", "SB Live! 24-bit External", ... */
+ char *cardName;
+ if (snd_card_get_name(cardNr, &cardName) == 0)
+ {
+ info.m_displayName = cardName;
+ free(cardName);
+ }
+
+ if (info.m_deviceType == AE_DEVTYPE_HDMI && info.m_displayName.size() > 5 &&
+ info.m_displayName.substr(info.m_displayName.size()-5) == " HDMI")
+ {
+ /* We already know this is HDMI, strip it */
+ info.m_displayName.erase(info.m_displayName.size()-5);
+ }
+
+ /* "CONEXANT Analog", "USB Audio", "HDMI 0", "ALC889 Digital" ... */
+ std::string pcminfoName = snd_pcm_info_get_name(pcminfo);
+
+ /*
+ * Filter "USB Audio", in those cases snd_card_get_name() is more
+ * meaningful already
+ */
+ if (pcminfoName != "USB Audio")
+ info.m_displayNameExtra = pcminfoName;
+
+ if (info.m_deviceType == AE_DEVTYPE_HDMI)
+ {
+ /* replace, this was likely "HDMI 0" */
+ info.m_displayNameExtra = "HDMI";
+
+ int dev = snd_pcm_info_get_device(pcminfo);
+
+ if (dev >= 0)
+ {
+ /* lets see if we can get ELD info */
+
+ snd_ctl_t *ctlhandle;
+ std::stringstream sstr;
+ sstr << "hw:" << cardNr;
+ std::string strHwName = sstr.str();
+
+ if (snd_ctl_open_lconf(&ctlhandle, strHwName.c_str(), 0, config) == 0)
+ {
+ snd_hctl_t *hctl;
+ if (snd_hctl_open_ctl(&hctl, ctlhandle) == 0)
+ {
+ snd_hctl_load(hctl);
+ bool badHDMI = false;
+
+#if !defined(HAVE_X11)
+ /* add ELD to monitoring */
+ const auto controlMonitor =
+ CServiceBroker::GetPlatform().GetService<CALSAHControlMonitor>();
+ controlMonitor->Add(strHwName, SND_CTL_ELEM_IFACE_PCM, dev, "ELD");
+#endif
+
+ if (!GetELD(hctl, dev, info, badHDMI))
+ CLog::Log(LOGDEBUG,
+ "CAESinkALSA - Unable to obtain ELD information for device \"{}\" (not "
+ "supported by device, or kernel older than 3.2)",
+ device);
+
+ /* snd_hctl_close also closes ctlhandle */
+ snd_hctl_close(hctl);
+
+ if (badHDMI)
+ {
+ /*
+ * Warn about disconnected devices, but keep them enabled
+ * Detection can go wrong on Intel, Nvidia and on all
+ * AMD (fglrx) hardware, so it is not safe to close those
+ * handles
+ */
+ CLog::Log(LOGDEBUG,
+ "CAESinkALSA - HDMI device \"{}\" may be unconnected (no ELD data)",
+ device);
+ }
+ }
+ else
+ {
+ snd_ctl_close(ctlhandle);
+ }
+ }
+ }
+ }
+ else if (info.m_deviceType == AE_DEVTYPE_IEC958)
+ {
+ /* append instead of replace, pcminfoName is useful for S/PDIF */
+ if (!info.m_displayNameExtra.empty())
+ info.m_displayNameExtra += ' ';
+ info.m_displayNameExtra += "S/PDIF";
+
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ info.m_dataFormats.push_back(AE_FMT_RAW);
+ }
+ else if (info.m_displayNameExtra.empty())
+ {
+ /* for USB audio, it gets a bit confusing as there is
+ * - "SB Live! 24-bit External"
+ * - "SB Live! 24-bit External, S/PDIF"
+ * so add "Analog" qualifier to the first one */
+ info.m_displayNameExtra = "Analog";
+ }
+
+ /* "default" is a device that will be used for all inputs, while
+ * "@" will be mangled to front/default/surroundXX as necessary */
+ if (device == "@" || device == "default")
+ {
+ /* Make it "Default (whatever)" */
+ info.m_displayName = "Default (" + info.m_displayName + (info.m_displayNameExtra.empty() ? "" : " " + info.m_displayNameExtra + ")");
+ info.m_displayNameExtra = "";
+ }
+
+ }
+ else
+ {
+ /* virtual devices: "default", "pulse", ... */
+ /* description can be e.g. "PulseAudio Sound Server" - for hw devices it is
+ * normally uninteresting, like "HDMI Audio Output" or "Default Audio Device",
+ * so we only use it for virtual devices that have no better display name */
+ info.m_displayName = description;
+ }
+
+ snd_pcm_hw_params_t *hwparams;
+ snd_pcm_hw_params_alloca(&hwparams);
+ memset(hwparams, 0, snd_pcm_hw_params_sizeof());
+
+ /* ensure we can get a playback configuration for the device */
+ if (snd_pcm_hw_params_any(pcmhandle, hwparams) < 0)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA - No playback configurations available for device \"{}\"",
+ device);
+ snd_pcm_close(pcmhandle);
+ return;
+ }
+
+ /* detect the available sample rates */
+ for (unsigned int *rate = ALSASampleRateList; *rate != 0; ++rate)
+ if (snd_pcm_hw_params_test_rate(pcmhandle, hwparams, *rate, 0) >= 0)
+ info.m_sampleRates.push_back(*rate);
+
+ /* detect the channels available */
+ int channels = 0;
+ for (int i = ALSA_MAX_CHANNELS; i >= 1; --i)
+ {
+ /* Reopen the device if needed on the special "surroundXX" cases */
+ if (info.m_deviceType == AE_DEVTYPE_PCM && (i == 8 || i == 6 || i == 4))
+ OpenPCMDevice(device, "", i, &pcmhandle, config);
+
+ if (snd_pcm_hw_params_test_channels(pcmhandle, hwparams, i) >= 0)
+ {
+ channels = i;
+ break;
+ }
+ }
+
+ if (device == "default" && channels == 2)
+ {
+ /* This looks like the ALSA standard default stereo dmix device, we
+ * probably want to use "@" instead to get surroundXX. */
+ snd_pcm_close(pcmhandle);
+ EnumerateDevice(list, "@", description, config);
+ return;
+ }
+
+ CAEChannelInfo alsaChannels;
+ snd_pcm_chmap_query_t** alsaMaps = snd_pcm_query_chmaps(pcmhandle);
+ bool useEldChannels = (info.m_channels.Count() > 0);
+ if (alsaMaps)
+ {
+ int i = 0;
+ for (snd_pcm_chmap_query_t* alsaMap = alsaMaps[i++];
+ alsaMap; alsaMap = alsaMaps[i++])
+ {
+ CAEChannelInfo AEmap = ALSAchmapToAEChannelMap(&alsaMap->map);
+ alsaChannels.AddMissingChannels(AEmap);
+ if (!useEldChannels)
+ info.m_channels.AddMissingChannels(AEmap);
+ }
+ snd_pcm_free_chmaps(alsaMaps);
+ }
+ else
+ {
+ for (int i = 0; i < channels; ++i)
+ {
+ if (!info.m_channels.HasChannel(LegacyALSAChannelMap[i]))
+ info.m_channels += LegacyALSAChannelMap[i];
+ alsaChannels += LegacyALSAChannelMap[i];
+ }
+ }
+
+ /* remove the channels from m_channels that we cant use */
+ info.m_channels.ResolveChannels(alsaChannels);
+
+ /* detect the PCM sample formats that are available */
+ for (enum AEDataFormat i = AE_FMT_MAX; i > AE_FMT_INVALID; i = (enum AEDataFormat)((int)i - 1))
+ {
+ if (i == AE_FMT_RAW || i == AE_FMT_MAX)
+ continue;
+ snd_pcm_format_t fmt = AEFormatToALSAFormat(i);
+ if (fmt == SND_PCM_FORMAT_UNKNOWN)
+ continue;
+
+ if (snd_pcm_hw_params_test_format(pcmhandle, hwparams, fmt) >= 0)
+ info.m_dataFormats.push_back(i);
+ }
+
+ if (info.m_deviceType == AE_DEVTYPE_HDMI)
+ {
+ // we don't trust ELD information and push back our supported formats explicitly
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_MA);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_EAC3);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_TRUEHD);
+
+ // indicate that we can do AE_FMT_RAW
+ info.m_dataFormats.push_back(AE_FMT_RAW);
+ }
+
+ snd_pcm_close(pcmhandle);
+ info.m_wantsIECPassthrough = true;
+ list.push_back(info);
+}
+
+bool CAESinkALSA::GetELD(snd_hctl_t *hctl, int device, CAEDeviceInfo& info, bool& badHDMI)
+{
+ badHDMI = false;
+
+ snd_ctl_elem_id_t *id;
+ snd_ctl_elem_info_t *einfo;
+ snd_ctl_elem_value_t *control;
+ snd_hctl_elem_t *elem;
+
+ snd_ctl_elem_id_alloca(&id);
+ memset(id, 0, snd_ctl_elem_id_sizeof());
+
+ snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_PCM);
+ snd_ctl_elem_id_set_name (id, "ELD" );
+ snd_ctl_elem_id_set_device (id, device);
+ elem = snd_hctl_find_elem(hctl, id);
+ if (!elem)
+ return false;
+
+ snd_ctl_elem_info_alloca(&einfo);
+ memset(einfo, 0, snd_ctl_elem_info_sizeof());
+
+ if (snd_hctl_elem_info(elem, einfo) < 0)
+ return false;
+
+ if (!snd_ctl_elem_info_is_readable(einfo))
+ return false;
+
+ if (snd_ctl_elem_info_get_type(einfo) != SND_CTL_ELEM_TYPE_BYTES)
+ return false;
+
+ snd_ctl_elem_value_alloca(&control);
+ memset(control, 0, snd_ctl_elem_value_sizeof());
+
+ if (snd_hctl_elem_read(elem, control) < 0)
+ return false;
+
+ int dataLength = snd_ctl_elem_info_get_count(einfo);
+ /* if there is no ELD data, then its a bad HDMI device, either nothing attached OR an invalid nVidia HDMI device
+ * OR the driver doesn't properly support ELD (notably ATI/AMD, 2012-05) */
+ if (!dataLength)
+ badHDMI = true;
+ else
+ CAEELDParser::Parse(
+ (const uint8_t*)snd_ctl_elem_value_get_bytes(control),
+ dataLength,
+ info
+ );
+
+ info.m_deviceType = AE_DEVTYPE_HDMI;
+ return true;
+}
+
+void CAESinkALSA::sndLibErrorHandler(const char *file, int line, const char *function, int err, const char *fmt, ...)
+{
+ if (!CServiceBroker::GetLogging().CanLogComponent(LOGAUDIO))
+ return;
+
+ va_list arg;
+ va_start(arg, fmt);
+ char *errorStr;
+ if (vasprintf(&errorStr, fmt, arg) >= 0)
+ {
+ CLog::Log(LOGINFO, "CAESinkALSA - ALSA: {}:{}:({}) {}{}{}", file, line, function, errorStr,
+ err ? ": " : "", err ? snd_strerror(err) : "");
+ free(errorStr);
+ }
+ va_end(arg);
+}
+
+void CAESinkALSA::Cleanup()
+{
+#if HAVE_LIBUDEV
+ const auto deviceMonitor = CServiceBroker::GetPlatform().GetService<CALSADeviceMonitor>();
+ deviceMonitor->Stop();
+#endif
+
+#if !defined(HAVE_X11)
+ const auto controlMonitor = CServiceBroker::GetPlatform().GetService<CALSAHControlMonitor>();
+ controlMonitor->Clear();
+#endif
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkALSA.h b/xbmc/cores/AudioEngine/Sinks/AESinkALSA.h
new file mode 100644
index 0000000..3ab9bd3
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkALSA.h
@@ -0,0 +1,100 @@
+/*
+ * 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 "cores/AudioEngine/Interfaces/AESink.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+#include "threads/CriticalSection.h"
+
+#include <stdint.h>
+
+#include <alsa/asoundlib.h>
+
+#define AE_MIN_PERIODSIZE 256
+
+class CAESinkALSA : public IAESink
+{
+public:
+ const char *GetName() override { return "ALSA"; }
+
+ CAESinkALSA();
+ ~CAESinkALSA() override;
+
+ static void Register();
+ static IAESink* Create(std::string &device, AEAudioFormat &desiredFormat);
+ static void EnumerateDevicesEx(AEDeviceInfoList &list, bool force = false);
+ static void Cleanup();
+
+ bool Initialize(AEAudioFormat &format, std::string &device) override;
+ void Deinitialize() override;
+
+ virtual void Stop ();
+ void GetDelay(AEDelayStatus& status) override;
+ double GetCacheTotal() override;
+ unsigned int AddPackets(uint8_t **data, unsigned int frames, unsigned int offset) override;
+ void Drain() override;
+
+private:
+ CAEChannelInfo GetChannelLayoutRaw(const AEAudioFormat& format);
+ CAEChannelInfo GetChannelLayoutLegacy(const AEAudioFormat& format, unsigned int minChannels, unsigned int maxChannels);
+ CAEChannelInfo GetChannelLayout(const AEAudioFormat& format, unsigned int channels);
+
+ static AEChannel ALSAChannelToAEChannel(unsigned int alsaChannel);
+ static unsigned int AEChannelToALSAChannel(AEChannel aeChannel);
+ static CAEChannelInfo ALSAchmapToAEChannelMap(snd_pcm_chmap_t* alsaMap);
+ static snd_pcm_chmap_t* AEChannelMapToALSAchmap(const CAEChannelInfo& info);
+ static snd_pcm_chmap_t* CopyALSAchmap(snd_pcm_chmap_t* alsaMap);
+ static std::string ALSAchmapToString(snd_pcm_chmap_t* alsaMap);
+ static CAEChannelInfo GetAlternateLayoutForm(const CAEChannelInfo& info);
+ snd_pcm_chmap_t* SelectALSAChannelMap(const CAEChannelInfo& info);
+
+ void GetAESParams(const AEAudioFormat& format, std::string& params);
+ void HandleError(const char* name, int err);
+
+ std::string m_initDevice;
+ AEAudioFormat m_initFormat;
+ AEAudioFormat m_format;
+ unsigned int m_bufferSize = 0;
+ double m_formatSampleRateMul = 0.0;
+ bool m_passthrough = false;
+ std::string m_device;
+ snd_pcm_t *m_pcm;
+ int m_timeout = 0;
+ // support fragmentation, e.g. looping in the sink to get a certain amount of data onto the device
+ bool m_fragmented = false;
+ unsigned int m_originalPeriodSize = AE_MIN_PERIODSIZE;
+
+ struct ALSAConfig
+ {
+ unsigned int sampleRate;
+ unsigned int periodSize;
+ unsigned int frameSize;
+ unsigned int channels;
+ AEDataFormat format;
+ };
+
+ static snd_pcm_format_t AEFormatToALSAFormat(const enum AEDataFormat format);
+
+ bool InitializeHW(const ALSAConfig &inconfig, ALSAConfig &outconfig);
+ bool InitializeSW(const ALSAConfig &inconfig);
+
+ static void AppendParams(std::string &device, const std::string &params);
+ static bool TryDevice(const std::string &name, snd_pcm_t **pcmp, snd_config_t *lconf);
+ static bool TryDeviceWithParams(const std::string &name, const std::string &params, snd_pcm_t **pcmp, snd_config_t *lconf);
+ static bool OpenPCMDevice(const std::string &name, const std::string &params, int channels, snd_pcm_t **pcmp, snd_config_t *lconf);
+
+ static AEDeviceType AEDeviceTypeFromName(const std::string &name);
+ static std::string GetParamFromName(const std::string &name, const std::string &param);
+ static void EnumerateDevice(AEDeviceInfoList &list, const std::string &device, const std::string &description, snd_config_t *config);
+ static bool SoundDeviceExists(const std::string& device);
+ static bool GetELD(snd_hctl_t *hctl, int device, CAEDeviceInfo& info, bool& badHDMI);
+
+ static void sndLibErrorHandler(const char *file, int line, const char *function, int err, const char *fmt, ...);
+};
+
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkAUDIOTRACK.cpp b/xbmc/cores/AudioEngine/Sinks/AESinkAUDIOTRACK.cpp
new file mode 100644
index 0000000..a7fb55c
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkAUDIOTRACK.cpp
@@ -0,0 +1,1286 @@
+/*
+ * 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 "AESinkAUDIOTRACK.h"
+
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "settings/Settings.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+
+#include "platform/android/activity/XBMCApp.h"
+
+#include <androidjni/AudioFormat.h>
+#include <androidjni/AudioManager.h>
+#include <androidjni/AudioTrack.h>
+#include <androidjni/Build.h>
+#include <unistd.h>
+
+// This is an alternative to the linear weighted delay smoothing
+// advantages: only one history value needs to be stored
+// in tests the linear weighted average smoother yield better results
+//#define AT_USE_EXPONENTIAL_AVERAGING 1
+
+using namespace jni;
+
+using namespace std::chrono_literals;
+
+// those are empirical values while the HD buffer
+// is the max TrueHD package
+const unsigned int MAX_RAW_AUDIO_BUFFER_HD = 61440;
+const unsigned int MAX_RAW_AUDIO_BUFFER = 16384;
+const unsigned int MOVING_AVERAGE_MAX_MEMBERS = 3;
+const uint64_t UINT64_LOWER_BYTES = 0x00000000FFFFFFFF;
+const uint64_t UINT64_UPPER_BYTES = 0xFFFFFFFF00000000;
+
+static const AEChannel KnownChannels[] = {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE,
+ AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR,
+ AE_CH_BC, AE_CH_BLOC, AE_CH_BROC, AE_CH_NULL};
+
+static int AEStreamFormatToATFormat(const CAEStreamInfo::DataType& dt)
+{
+ switch (dt)
+ {
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ return CJNIAudioFormat::ENCODING_AC3;
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ return CJNIAudioFormat::ENCODING_DTS;
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ return CJNIAudioFormat::ENCODING_DTS_HD;
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ return CJNIAudioFormat::ENCODING_E_AC3;
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ return CJNIAudioFormat::ENCODING_DOLBY_TRUEHD;
+ default:
+ return CJNIAudioFormat::ENCODING_PCM_16BIT;
+ }
+}
+
+static AEChannel AUDIOTRACKChannelToAEChannel(int atChannel)
+{
+ AEChannel aeChannel;
+
+ /* cannot use switch since CJNIAudioFormat is populated at runtime */
+
+ if (atChannel == CJNIAudioFormat::CHANNEL_OUT_FRONT_LEFT) aeChannel = AE_CH_FL;
+ else if (atChannel == CJNIAudioFormat::CHANNEL_OUT_FRONT_RIGHT) aeChannel = AE_CH_FR;
+ else if (atChannel == CJNIAudioFormat::CHANNEL_OUT_FRONT_CENTER) aeChannel = AE_CH_FC;
+ else if (atChannel == CJNIAudioFormat::CHANNEL_OUT_LOW_FREQUENCY) aeChannel = AE_CH_LFE;
+ else if (atChannel == CJNIAudioFormat::CHANNEL_OUT_BACK_LEFT) aeChannel = AE_CH_BL;
+ else if (atChannel == CJNIAudioFormat::CHANNEL_OUT_BACK_RIGHT) aeChannel = AE_CH_BR;
+ else if (atChannel == CJNIAudioFormat::CHANNEL_OUT_SIDE_LEFT) aeChannel = AE_CH_SL;
+ else if (atChannel == CJNIAudioFormat::CHANNEL_OUT_SIDE_RIGHT) aeChannel = AE_CH_SR;
+ else if (atChannel == CJNIAudioFormat::CHANNEL_OUT_FRONT_LEFT_OF_CENTER) aeChannel = AE_CH_FLOC;
+ else if (atChannel == CJNIAudioFormat::CHANNEL_OUT_FRONT_RIGHT_OF_CENTER) aeChannel = AE_CH_FROC;
+ else if (atChannel == CJNIAudioFormat::CHANNEL_OUT_BACK_CENTER) aeChannel = AE_CH_BC;
+ else aeChannel = AE_CH_UNKNOWN1;
+
+ return aeChannel;
+}
+
+static int AEChannelToAUDIOTRACKChannel(AEChannel aeChannel)
+{
+ int atChannel;
+ switch (aeChannel)
+ {
+ case AE_CH_FL: atChannel = CJNIAudioFormat::CHANNEL_OUT_FRONT_LEFT; break;
+ case AE_CH_FR: atChannel = CJNIAudioFormat::CHANNEL_OUT_FRONT_RIGHT; break;
+ case AE_CH_FC: atChannel = CJNIAudioFormat::CHANNEL_OUT_FRONT_CENTER; break;
+ case AE_CH_LFE: atChannel = CJNIAudioFormat::CHANNEL_OUT_LOW_FREQUENCY; break;
+ case AE_CH_BL: atChannel = CJNIAudioFormat::CHANNEL_OUT_BACK_LEFT; break;
+ case AE_CH_BR: atChannel = CJNIAudioFormat::CHANNEL_OUT_BACK_RIGHT; break;
+ case AE_CH_SL: atChannel = CJNIAudioFormat::CHANNEL_OUT_SIDE_LEFT; break;
+ case AE_CH_SR: atChannel = CJNIAudioFormat::CHANNEL_OUT_SIDE_RIGHT; break;
+ case AE_CH_BC: atChannel = CJNIAudioFormat::CHANNEL_OUT_BACK_CENTER; break;
+ case AE_CH_FLOC: atChannel = CJNIAudioFormat::CHANNEL_OUT_FRONT_LEFT_OF_CENTER; break;
+ case AE_CH_FROC: atChannel = CJNIAudioFormat::CHANNEL_OUT_FRONT_RIGHT_OF_CENTER; break;
+ default: atChannel = CJNIAudioFormat::CHANNEL_INVALID; break;
+ }
+ return atChannel;
+}
+
+static CAEChannelInfo AUDIOTRACKChannelMaskToAEChannelMap(int atMask)
+{
+ CAEChannelInfo info;
+
+ int mask = 0x1;
+ for (unsigned int i = 0; i < sizeof(int32_t) * 8; i++)
+ {
+ if (atMask & mask)
+ info += AUDIOTRACKChannelToAEChannel(mask);
+ mask <<= 1;
+ }
+
+ return info;
+}
+
+static int AEChannelMapToAUDIOTRACKChannelMask(CAEChannelInfo info)
+{
+ info.ResolveChannels(CAEChannelInfo(KnownChannels));
+
+ // Detect layouts with 6 channels including one LFE channel
+ // We currently support the following layouts:
+ // 5.1 FL+FR+FC+LFE+BL+BR
+ // 5.1(side) FL+FR+FC+LFE+SL+SR
+ // According to CEA-861-D only RR and RL are defined
+ // Therefore we let Android decide about the 5.1 mapping
+ // For 8 channel layouts including one LFE channel
+ // we leave the same decision to Android
+ if (info.Count() == 6 && info.HasChannel(AE_CH_LFE))
+ return CJNIAudioFormat::CHANNEL_OUT_5POINT1;
+
+ if (info.Count() == 8 && info.HasChannel(AE_CH_LFE))
+ return CJNIAudioFormat::CHANNEL_OUT_7POINT1_SURROUND;
+
+ int atMask = 0;
+
+ for (unsigned int i = 0; i < info.Count(); i++)
+ atMask |= AEChannelToAUDIOTRACKChannel(info[i]);
+
+ return atMask;
+}
+
+jni::CJNIAudioTrack *CAESinkAUDIOTRACK::CreateAudioTrack(int stream, int sampleRate, int channelMask, int encoding, int bufferSize)
+{
+ jni::CJNIAudioTrack *jniAt = NULL;
+
+ try
+ {
+ CJNIAudioAttributesBuilder attrBuilder;
+ attrBuilder.setUsage(CJNIAudioAttributes::USAGE_MEDIA);
+ attrBuilder.setContentType(CJNIAudioAttributes::CONTENT_TYPE_MUSIC);
+
+ CJNIAudioFormatBuilder fmtBuilder;
+ fmtBuilder.setChannelMask(channelMask);
+ fmtBuilder.setEncoding(encoding);
+ fmtBuilder.setSampleRate(sampleRate);
+
+ jniAt = new CJNIAudioTrack(attrBuilder.build(),
+ fmtBuilder.build(),
+ bufferSize,
+ CJNIAudioTrack::MODE_STREAM,
+ CJNIAudioManager::AUDIO_SESSION_ID_GENERATE);
+ }
+ catch (const std::invalid_argument& e)
+ {
+ CLog::Log(LOGINFO, "AESinkAUDIOTRACK - AudioTrack creation (channelMask {:#08x}): {}",
+ channelMask, e.what());
+ }
+
+ return jniAt;
+}
+
+int CAESinkAUDIOTRACK::AudioTrackWrite(char* audioData, int offsetInBytes, int sizeInBytes)
+{
+ int written = 0;
+ if (m_jniAudioFormat == CJNIAudioFormat::ENCODING_PCM_FLOAT)
+ {
+ if (m_floatbuf.size() != (sizeInBytes - offsetInBytes) / sizeof(float))
+ m_floatbuf.resize((sizeInBytes - offsetInBytes) / sizeof(float));
+ memcpy(m_floatbuf.data(), audioData + offsetInBytes, sizeInBytes - offsetInBytes);
+ written = m_at_jni->write(m_floatbuf, 0, (sizeInBytes - offsetInBytes) / sizeof(float), CJNIAudioTrack::WRITE_BLOCKING);
+ written *= sizeof(float);
+ }
+ else if (m_jniAudioFormat == CJNIAudioFormat::ENCODING_IEC61937)
+ {
+ if (m_shortbuf.size() != (sizeInBytes - offsetInBytes) / sizeof(int16_t))
+ m_shortbuf.resize((sizeInBytes - offsetInBytes) / sizeof(int16_t));
+ memcpy(m_shortbuf.data(), audioData + offsetInBytes, sizeInBytes - offsetInBytes);
+ if (CJNIBase::GetSDKVersion() >= 23)
+ written = m_at_jni->write(m_shortbuf, 0, (sizeInBytes - offsetInBytes) / sizeof(int16_t), CJNIAudioTrack::WRITE_BLOCKING);
+ else
+ written = m_at_jni->write(m_shortbuf, 0, (sizeInBytes - offsetInBytes) / sizeof(int16_t));
+ written *= sizeof(uint16_t);
+ }
+ else
+ {
+ if (static_cast<int>(m_charbuf.size()) != (sizeInBytes - offsetInBytes))
+ m_charbuf.resize(sizeInBytes - offsetInBytes);
+ memcpy(m_charbuf.data(), audioData + offsetInBytes, sizeInBytes - offsetInBytes);
+ if (CJNIBase::GetSDKVersion() >= 23)
+ written = m_at_jni->write(m_charbuf, 0, sizeInBytes - offsetInBytes, CJNIAudioTrack::WRITE_BLOCKING);
+ else
+ written = m_at_jni->write(m_charbuf, 0, sizeInBytes - offsetInBytes);
+ }
+
+ return written;
+}
+
+int CAESinkAUDIOTRACK::AudioTrackWrite(char* audioData, int sizeInBytes, int64_t timestamp)
+{
+ int written = 0;
+ std::vector<char> buf;
+ buf.reserve(sizeInBytes);
+ memcpy(buf.data(), audioData, sizeInBytes);
+
+ CJNIByteBuffer bytebuf = CJNIByteBuffer::wrap(buf);
+ written = m_at_jni->write(bytebuf.get_raw(), sizeInBytes, CJNIAudioTrack::WRITE_BLOCKING, timestamp);
+
+ return written;
+}
+
+CAEDeviceInfo CAESinkAUDIOTRACK::m_info;
+CAEDeviceInfo CAESinkAUDIOTRACK::m_info_iec;
+CAEDeviceInfo CAESinkAUDIOTRACK::m_info_raw;
+bool CAESinkAUDIOTRACK::m_hasIEC = false;
+std::set<unsigned int> CAESinkAUDIOTRACK::m_sink_sampleRates;
+bool CAESinkAUDIOTRACK::m_sinkSupportsFloat = false;
+bool CAESinkAUDIOTRACK::m_sinkSupportsMultiChannelFloat = false;
+bool CAESinkAUDIOTRACK::m_passthrough_use_eac3 = false;
+
+////////////////////////////////////////////////////////////////////////////////////////////
+CAESinkAUDIOTRACK::CAESinkAUDIOTRACK()
+{
+ m_alignedS16 = NULL;
+ m_sink_frameSize = 0;
+ m_encoding = CJNIAudioFormat::ENCODING_PCM_16BIT;
+ m_audiotrackbuffer_sec = 0.0;
+ m_audiotrackbuffer_sec_orig = 0.0;
+ m_at_jni = NULL;
+ m_duration_written = 0;
+ m_headPos = 0;
+ m_stuckCounter = 0;
+ m_headPosOld = 0;
+ m_timestampPos = 0;
+ m_sink_sampleRate = 0;
+ m_passthrough = false;
+ m_min_buffer_size = 0;
+}
+
+CAESinkAUDIOTRACK::~CAESinkAUDIOTRACK()
+{
+ Deinitialize();
+}
+
+bool CAESinkAUDIOTRACK::VerifySinkConfiguration(int sampleRate,
+ int channelMask,
+ int encoding,
+ bool isRaw)
+{
+ int minBufferSize = CJNIAudioTrack::getMinBufferSize(sampleRate, channelMask, encoding);
+ bool supported = (minBufferSize > 0);
+
+ // make sure to have enough buffer as minimum might not be enough to open
+ if (!isRaw)
+ minBufferSize *= 2;
+
+ if (supported)
+ {
+ jni::CJNIAudioTrack* jniAt = CreateAudioTrack(CJNIAudioManager::STREAM_MUSIC, sampleRate,
+ channelMask, encoding, minBufferSize);
+ supported = (jniAt && jniAt->getState() == CJNIAudioTrack::STATE_INITIALIZED);
+ if (supported)
+ {
+ jniAt->pause();
+ jniAt->flush();
+ }
+
+ if (jniAt)
+ {
+ jniAt->release();
+ delete jniAt;
+ }
+ }
+ CLog::Log(LOGDEBUG, "VerifySinkConfiguration samplerate: {} mask: {} encoding: {} success: {}",
+ sampleRate, channelMask, encoding, supported ? "true" : "false");
+ return supported;
+}
+
+
+bool CAESinkAUDIOTRACK::IsSupported(int sampleRateInHz, int channelConfig, int encoding)
+{
+ int ret = CJNIAudioTrack::getMinBufferSize( sampleRateInHz, channelConfig, encoding);
+ return (ret > 0);
+}
+
+bool CAESinkAUDIOTRACK::Initialize(AEAudioFormat &format, std::string &device)
+{
+
+ // try to recover used device
+ if (!m_hasIEC)
+ m_info = m_info_raw;
+ else if (device == "Default" && !m_info.m_wantsIECPassthrough)
+ m_info = m_info_raw;
+ else if (device == "AudioTrack (RAW)")
+ m_info = m_info_raw;
+ else
+ m_info = m_info_iec;
+
+ m_format = format;
+ m_headPos = 0;
+ m_stuckCounter = 0;
+ m_headPosOld = 0;
+ m_timestampPos = 0;
+ m_linearmovingaverage.clear();
+ m_pause_ms = 0.0;
+ CLog::Log(LOGDEBUG,
+ "CAESinkAUDIOTRACK::Initialize requested: sampleRate {}; format: {}; channels: {}",
+ format.m_sampleRate, CAEUtil::DataFormatToStr(format.m_dataFormat),
+ format.m_channelLayout.Count());
+
+ int stream = CJNIAudioManager::STREAM_MUSIC;
+ m_encoding = CJNIAudioFormat::ENCODING_PCM_16BIT;
+
+ // If the device supports EAC3 passthrough, but not basic AC3 patthrough, send it as EAC3 (which is AC3 compatible) instead
+ if (!m_info.m_wantsIECPassthrough)
+ {
+ if ((m_format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_AC3) && m_passthrough_use_eac3)
+ m_format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_EAC3;
+ }
+
+ uint32_t distance = UINT32_MAX; // max upper distance, update at least ones to use one of our samplerates
+ for (auto& s : m_sink_sampleRates)
+ {
+ // prefer best match or alternatively something that divides nicely and
+ // is not too far away
+ uint32_t d = std::abs((int)m_format.m_sampleRate - (int)s) + 8 * (s > m_format.m_sampleRate ? (s % m_format.m_sampleRate) : (m_format.m_sampleRate % s));
+ if (d < distance)
+ {
+ m_sink_sampleRate = s;
+ distance = d;
+ CLog::Log(LOGDEBUG, "Updated SampleRate: {} Distance: {}", m_sink_sampleRate, d);
+ }
+ }
+
+ if (m_format.m_dataFormat == AE_FMT_RAW)
+ {
+ m_passthrough = true;
+ m_encoding = AEStreamFormatToATFormat(m_format.m_streamInfo.m_type);
+ m_format.m_channelLayout = AE_CH_LAYOUT_2_0;
+
+ if (m_format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTSHD_MA ||
+ m_format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD)
+ {
+ m_format.m_channelLayout = AE_CH_LAYOUT_7_1;
+ }
+
+ // EAC3 needs real samplerate not the modulation
+ if (m_format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_EAC3)
+ m_sink_sampleRate = m_format.m_streamInfo.m_sampleRate;
+
+ if (m_info.m_wantsIECPassthrough)
+ {
+ m_format.m_dataFormat = AE_FMT_S16LE;
+ if (m_format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTSHD ||
+ m_format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTSHD_MA ||
+ m_format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD)
+ m_sink_sampleRate = 192000;
+
+ // new Android N format
+ if (CJNIAudioFormat::ENCODING_IEC61937 != -1)
+ {
+ m_encoding = CJNIAudioFormat::ENCODING_IEC61937;
+ // this will be sent tunneled, therefore the IEC path needs e.g.
+ // 4 * m_format.m_streamInfo.m_sampleRate
+ if (m_format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_EAC3)
+ m_sink_sampleRate = m_format.m_sampleRate;
+ }
+
+ // we are running on an old android version
+ // that does neither know AC3, DTS or whatever
+ // we will fallback to 16BIT passthrough
+ if (m_encoding == -1)
+ {
+ m_format.m_channelLayout = AE_CH_LAYOUT_2_0;
+ m_format.m_sampleRate = m_sink_sampleRate;
+ m_encoding = CJNIAudioFormat::ENCODING_PCM_16BIT;
+ CLog::Log(LOGDEBUG, "Fallback to PCM passthrough mode - this might not work!");
+ }
+ }
+ }
+ else
+ {
+ m_passthrough = false;
+ m_format.m_sampleRate = m_sink_sampleRate;
+ if (m_sinkSupportsMultiChannelFloat)
+ {
+ m_encoding = CJNIAudioFormat::ENCODING_PCM_FLOAT;
+ m_format.m_dataFormat = AE_FMT_FLOAT;
+ }
+ else if (m_sinkSupportsFloat && m_format.m_channelLayout.Count() == 2)
+ {
+ m_encoding = CJNIAudioFormat::ENCODING_PCM_FLOAT;
+ m_format.m_dataFormat = AE_FMT_FLOAT;
+ }
+ else
+ {
+ m_encoding = CJNIAudioFormat::ENCODING_PCM_16BIT;
+ m_format.m_dataFormat = AE_FMT_S16LE;
+ }
+ }
+
+ int atChannelMask = AEChannelMapToAUDIOTRACKChannelMask(m_format.m_channelLayout);
+ m_format.m_channelLayout = AUDIOTRACKChannelMaskToAEChannelMap(atChannelMask);
+ if (m_encoding == CJNIAudioFormat::ENCODING_IEC61937)
+ {
+ // keep above channel output if we do IEC61937 and got DTSHD or TrueHD by AudioEngine
+ if (m_format.m_streamInfo.m_type != CAEStreamInfo::STREAM_TYPE_DTSHD_MA && m_format.m_streamInfo.m_type != CAEStreamInfo::STREAM_TYPE_TRUEHD)
+ atChannelMask = CJNIAudioFormat::CHANNEL_OUT_STEREO;
+ }
+
+ bool retried = false;
+ while (!m_at_jni)
+ {
+ CLog::Log(LOGINFO, "Trying to open: samplerate: {}, channelMask: {}, encoding: {}",
+ m_sink_sampleRate, atChannelMask, m_encoding);
+ int min_buffer = CJNIAudioTrack::getMinBufferSize(m_sink_sampleRate,
+ atChannelMask,
+ m_encoding);
+
+ if (min_buffer < 0)
+ {
+ CLog::Log(LOGERROR,
+ "Minimum Buffer Size was: {} - disable passthrough (?) your hw does not support it",
+ min_buffer);
+ return false;
+ }
+
+ m_min_buffer_size = (unsigned int) min_buffer;
+ CLog::Log(LOGINFO, "Minimum size we need for stream: {} Bytes", m_min_buffer_size);
+ double rawlength_in_seconds = 0.0;
+ int multiplier = 1;
+ unsigned int ac3FrameSize = 1;
+ if (m_passthrough && !m_info.m_wantsIECPassthrough)
+ {
+ switch (m_format.m_streamInfo.m_type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ m_min_buffer_size = MAX_RAW_AUDIO_BUFFER_HD;
+ m_format.m_frames = m_min_buffer_size;
+ rawlength_in_seconds = 8 * m_format.m_streamInfo.GetDuration() / 1000; // on average
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ // normal frame is max 2012 bytes + 2764 sub frame
+ m_min_buffer_size = 66432; //according to the buffer model of ISO/IEC13818-1
+ m_format.m_frames = m_min_buffer_size;
+ rawlength_in_seconds = 8 * m_format.m_streamInfo.GetDuration() / 1000; // average value
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ // max 2012 bytes
+ // depending on sample rate between 156 ms and 312 ms
+ m_min_buffer_size = 16 * 2012;
+ m_format.m_frames = m_min_buffer_size;
+ rawlength_in_seconds = 16 * m_format.m_streamInfo.GetDuration() / 1000;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ m_min_buffer_size = 8 * 5462;
+ m_format.m_frames = m_min_buffer_size;
+ rawlength_in_seconds = 8 * m_format.m_streamInfo.GetDuration() / 1000;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ ac3FrameSize = m_format.m_streamInfo.m_ac3FrameSize;
+ if (ac3FrameSize == 0)
+ ac3FrameSize = 1536; // fallback if not set, e.g. Transcoding
+ m_min_buffer_size = std::max(m_min_buffer_size * 3, ac3FrameSize * 8);
+ m_format.m_frames = m_min_buffer_size;
+ multiplier = m_min_buffer_size / ac3FrameSize; // int division is wanted
+ rawlength_in_seconds = multiplier * m_format.m_streamInfo.GetDuration() / 1000;
+ break;
+ // EAC3 is currently not supported
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ m_min_buffer_size = 2 * 10752; // least common multiple of 1792 and 1536
+ m_format.m_frames = m_min_buffer_size; // needs testing
+ rawlength_in_seconds = 8 * m_format.m_streamInfo.GetDuration() / 1000;
+ break;
+ default:
+ m_min_buffer_size = MAX_RAW_AUDIO_BUFFER;
+ m_format.m_frames = m_min_buffer_size;
+ rawlength_in_seconds = 0.4;
+ break;
+ }
+
+ CLog::Log(LOGDEBUG, "Opening Passthrough RAW Format: {} Sink SampleRate: {}",
+ CAEUtil::StreamTypeToStr(m_format.m_streamInfo.m_type), m_sink_sampleRate);
+ m_format.m_frameSize = 1;
+ m_sink_frameSize = m_format.m_frameSize;
+ }
+ else
+ {
+ m_format.m_frameSize = m_format.m_channelLayout.Count() * (CAEUtil::DataFormatToBits(m_format.m_dataFormat) / 8);
+ m_sink_frameSize = m_format.m_frameSize;
+ // aim at max 200 ms buffer and 50 ms periods but at least two periods of min_buffer
+ // make sure periods are actually not smaller than 32 ms (32, cause 32 * 2 = 64)
+ // but also lower than 64 ms
+ // which is large enough to not cause CPU hogging in case 32 ms periods are used
+ m_audiotrackbuffer_sec =
+ static_cast<double>(m_min_buffer_size) / (m_sink_frameSize * m_sink_sampleRate);
+ // the period calculation starts
+ // after the buffer division to get even division results
+ int c = 1;
+ if (m_audiotrackbuffer_sec > 0.25)
+ {
+ CLog::Log(LOGWARNING,
+ "Audiobuffer is already very large {:f} ms - Reducing to a sensible value",
+ 1000.0 * m_audiotrackbuffer_sec);
+ int buffer_frames = m_sink_sampleRate / 4; // 250 ms
+ m_min_buffer_size = buffer_frames * m_sink_frameSize;
+ c = 5; // 50 ms
+ }
+ // update potential new buffertime
+ m_audiotrackbuffer_sec =
+ static_cast<double>(m_min_buffer_size) / (m_sink_frameSize * m_sink_sampleRate);
+ constexpr double max_time = 0.064;
+ constexpr double min_time = 0.032;
+ constexpr double target_duration = 0.128;
+
+ while (m_audiotrackbuffer_sec < target_duration)
+ {
+ m_min_buffer_size += min_buffer;
+ c++;
+ m_audiotrackbuffer_sec =
+ static_cast<double>(m_min_buffer_size) / (m_sink_frameSize * m_sink_sampleRate);
+ }
+ unsigned int period_size = m_min_buffer_size / c;
+ double period_time =
+ static_cast<double>(period_size) / (m_sink_frameSize * m_sink_sampleRate);
+
+ // This will result in minimum 32 ms
+ while (period_time >= max_time)
+ {
+ period_time /= 2;
+ period_size /= 2;
+ }
+ // If the audio track API gave us very low values increase them
+ // In this case the first loop would not have been run at all
+ while (period_time < min_time)
+ {
+ period_size *= 2;
+ period_time *= 2;
+ }
+ m_format.m_frames = static_cast<int>(period_size / m_format.m_frameSize);
+
+ CLog::Log(LOGINFO,
+ "Audiotrack buffer params are: period time = {:.3f} ms, period size = "
+ "{} bytes, num periods = {}",
+ period_time * 1000, period_size, m_min_buffer_size / period_size);
+ }
+
+ if (m_passthrough && !m_info.m_wantsIECPassthrough)
+ m_audiotrackbuffer_sec = rawlength_in_seconds;
+
+ CLog::Log(LOGINFO,
+ "Created Audiotrackbuffer with playing time of {:f} ms min buffer size: {} bytes",
+ m_audiotrackbuffer_sec * 1000, m_min_buffer_size);
+
+ m_audiotrackbuffer_sec_orig = m_audiotrackbuffer_sec;
+
+ m_jniAudioFormat = m_encoding;
+ m_at_jni = CreateAudioTrack(stream, m_sink_sampleRate, atChannelMask,
+ m_encoding, m_min_buffer_size);
+
+ if (!IsInitialized())
+ {
+ if (!m_passthrough)
+ {
+ if (atChannelMask != CJNIAudioFormat::CHANNEL_OUT_STEREO &&
+ atChannelMask != CJNIAudioFormat::CHANNEL_OUT_5POINT1)
+ {
+ atChannelMask = CJNIAudioFormat::CHANNEL_OUT_5POINT1;
+ CLog::Log(LOGDEBUG, "AESinkAUDIOTRACK - Retrying multichannel playback with a 5.1 layout");
+ continue;
+ }
+ else if (atChannelMask != CJNIAudioFormat::CHANNEL_OUT_STEREO)
+ {
+ atChannelMask = CJNIAudioFormat::CHANNEL_OUT_STEREO;
+ CLog::Log(LOGDEBUG, "AESinkAUDIOTRACK - Retrying with a stereo layout");
+ continue;
+ }
+ }
+ else
+ {
+ if (!retried)
+ {
+ retried = true;
+ CLog::Log(LOGWARNING, "AESinkAUDIOTRACK - Unable to open PT device - will retry once");
+ // Seems that some devices don't properly implement pause + flush, which during seek
+ // might open the device too fast - let's retry
+ usleep(200 * 1000);
+ continue;
+ }
+ }
+ CLog::Log(LOGERROR, "AESinkAUDIOTRACK - Unable to create AudioTrack");
+ Deinitialize();
+ return false;
+ }
+ const char* method = m_passthrough ? (m_info.m_wantsIECPassthrough ? "IEC (PT)" : "RAW (PT)") : "PCM";
+ CLog::Log(LOGINFO,
+ "CAESinkAUDIOTRACK::Initializing with: m_sampleRate: {} format: {} (AE) method: {} "
+ "stream-type: {} min_buffer_size: {} m_frames: {} m_frameSize: {} channels: {}",
+ m_sink_sampleRate, CAEUtil::DataFormatToStr(m_format.m_dataFormat), method,
+ m_passthrough ? CAEUtil::StreamTypeToStr(m_format.m_streamInfo.m_type) : "PCM-STREAM",
+ m_min_buffer_size, m_format.m_frames, m_format.m_frameSize,
+ m_format.m_channelLayout.Count());
+ }
+ format = m_format;
+
+ return true;
+}
+
+void CAESinkAUDIOTRACK::Deinitialize()
+{
+ CLog::Log(LOGDEBUG, "CAESinkAUDIOTRACK::Deinitialize");
+
+ if (!m_at_jni)
+ return;
+
+ if (IsInitialized())
+ {
+ m_at_jni->pause();
+ m_at_jni->flush();
+ }
+ m_at_jni->release();
+
+ m_duration_written = 0;
+ m_headPos = 0;
+ m_headPosOld = 0;
+ m_timestampPos = 0;
+ m_stampTimer.SetExpired();
+
+ m_linearmovingaverage.clear();
+
+ delete m_at_jni;
+ m_at_jni = NULL;
+ m_delay = 0.0;
+ m_hw_delay = 0.0;
+}
+
+bool CAESinkAUDIOTRACK::IsInitialized()
+{
+ return (m_at_jni && m_at_jni->getState() == CJNIAudioTrack::STATE_INITIALIZED);
+}
+
+void CAESinkAUDIOTRACK::GetDelay(AEDelayStatus& status)
+{
+ if (!m_at_jni)
+ {
+ status.SetDelay(0);
+ return;
+ }
+
+ bool usesAdvancedLogging = CServiceBroker::GetLogging().CanLogComponent(LOGAUDIO);
+ // In their infinite wisdom, Google decided to make getPlaybackHeadPosition
+ // return a 32bit "int" that you should "interpret as unsigned." As such,
+ // for wrap safety, we need to do all ops on it in 32bit integer math.
+
+ uint32_t head_pos = (uint32_t)m_at_jni->getPlaybackHeadPosition();
+
+ // Wraparound
+ if ((uint32_t)(m_headPos & UINT64_LOWER_BYTES) > head_pos) // need to compute wraparound
+ m_headPos += (1ULL << 32); // add wraparound, e.g. 0x0000 FFFF FFFF -> 0x0001 FFFF FFFF
+ // clear lower 32 bit values, e.g. 0x0001 FFFF FFFF -> 0x0001 0000 0000
+ // and add head_pos which wrapped around, e.g. 0x0001 0000 0000 -> 0x0001 0000 0004
+ m_headPos = (m_headPos & UINT64_UPPER_BYTES) | (uint64_t)head_pos;
+ // check if sink is stuck
+ if (m_headPos == m_headPosOld)
+ m_stuckCounter++;
+ else
+ {
+ m_stuckCounter = 0;
+ m_headPosOld = m_headPos;
+ }
+
+ double gone = static_cast<double>(m_headPos) / m_sink_sampleRate;
+
+ // if sink is run dry without buffer time written anymore
+ if (gone > m_duration_written)
+ gone = m_duration_written;
+
+ double delay = m_duration_written - gone;
+
+ if (m_stampTimer.IsTimePast())
+ {
+ if (!m_at_jni->getTimestamp(m_timestamp))
+ {
+ CLog::Log(LOGDEBUG, "Could not acquire timestamp");
+ m_stampTimer.Set(100ms);
+ }
+ else
+ {
+ // check if frameposition is valid and nano timer less than 50 ms outdated
+ if (m_timestamp.get_framePosition() > 0 &&
+ (CurrentHostCounter() - m_timestamp.get_nanoTime()) < 50 * 1000 * 1000)
+ m_stampTimer.Set(1000ms);
+ else
+ m_stampTimer.Set(100ms);
+ }
+ }
+ // check if last value was received less than 2 seconds ago
+ if (m_timestamp.get_framePosition() > 0 &&
+ (CurrentHostCounter() - m_timestamp.get_nanoTime()) < 2 * 1000 * 1000 * 1000)
+ {
+ if (usesAdvancedLogging)
+ {
+ CLog::Log(LOGINFO, "Framecounter: {} Time: {} Current-Time: {}",
+ (m_timestamp.get_framePosition() & UINT64_LOWER_BYTES), m_timestamp.get_nanoTime(),
+ CurrentHostCounter());
+ }
+ uint64_t delta = static_cast<uint64_t>(CurrentHostCounter() - m_timestamp.get_nanoTime());
+ uint64_t stamphead =
+ static_cast<uint64_t>(m_timestamp.get_framePosition() & UINT64_LOWER_BYTES) +
+ delta * m_sink_sampleRate / 1000000000.0;
+ // wrap around
+ // e.g. 0xFFFFFFFFFFFF0123 -> 0x0000000000002478
+ // because we only query each second the simple smaller comparison won't suffice
+ // as delay can fluctuate minimally
+ if (stamphead < m_timestampPos && (m_timestampPos - stamphead) > 0x7FFFFFFFFFFFFFFFULL)
+ {
+ uint64_t stamp = m_timestampPos;
+ stamp += (1ULL << 32);
+ stamphead = (stamp & UINT64_UPPER_BYTES) | stamphead;
+ CLog::Log(LOGDEBUG, "Wraparound happened old: {} new: {}", m_timestampPos, stamphead);
+ }
+ m_timestampPos = stamphead;
+
+ double playtime = m_timestampPos / static_cast<double>(m_sink_sampleRate);
+
+ if (usesAdvancedLogging)
+ {
+ CLog::Log(LOGINFO,
+ "Delay - Timestamp: {} (ms) delta: {} (ms) playtime: {} (ms) Duration: {} ms",
+ 1000.0 * (m_duration_written - playtime), delta / 1000000.0, playtime * 1000,
+ m_duration_written * 1000);
+ CLog::Log(LOGINFO, "Head-Position {} Timestamp Position {} Delay-Offset: {} ms", m_headPos,
+ m_timestampPos,
+ 1000.0 * (static_cast<int64_t>(m_headPos - m_timestampPos)) / m_sink_sampleRate);
+ }
+ double hw_delay = m_duration_written - playtime;
+ // correct by subtracting above measured delay, if lower delay gets automatically reduced
+ hw_delay -= delay;
+ // sometimes at the beginning of the stream m_timestampPos is more accurate and ahead of
+ // m_headPos - don't use the computed value then and wait
+ if (hw_delay > -1.0 && hw_delay < 1.0)
+ m_hw_delay = hw_delay;
+ else
+ m_hw_delay = 0.0;
+ if (usesAdvancedLogging)
+ {
+ CLog::Log(LOGINFO, "HW-Delay (1): {} ms", hw_delay * 1000);
+ }
+ }
+
+ delay += m_hw_delay;
+
+ if (usesAdvancedLogging)
+ {
+ CLog::Log(LOGINFO, "Combined Delay: {} ms", delay * 1000);
+ }
+ if (delay < 0.0)
+ delay = 0.0;
+
+ // the RAW hack for simulating pause bursts should not come
+ // into the way of hw delay
+ if (m_pause_ms > 0.0)
+ {
+ double difference = (m_audiotrackbuffer_sec - delay) * 1000;
+ if (usesAdvancedLogging)
+ {
+ CLog::Log(LOGINFO, "Faking Pause-Bursts in Delay - returning smoothed {} ms Original {} ms",
+ m_audiotrackbuffer_sec * 1000, delay * 1000);
+ CLog::Log(LOGINFO, "Difference: {} ms m_pause_ms {}", difference, m_pause_ms);
+ }
+ // buffer not yet reached
+ if (difference > 0.0)
+ delay = m_audiotrackbuffer_sec;
+ else
+ {
+ CLog::Log(LOGINFO, "Resetting pause bursts as buffer level was reached! (2)");
+ m_pause_ms = 0.0;
+ }
+ }
+
+ const double d = GetMovingAverageDelay(delay);
+
+ // Audiotrack is caching more than we thought it would
+ if (d > m_audiotrackbuffer_sec)
+ m_audiotrackbuffer_sec = d;
+
+ // track delay in local member
+ m_delay = d;
+ if (usesAdvancedLogging)
+ {
+ CLog::Log(LOGINFO, "Delay Current: {:f} ms", d * 1000);
+ }
+ status.SetDelay(d);
+}
+
+double CAESinkAUDIOTRACK::GetLatency()
+{
+ return 0.0;
+}
+
+double CAESinkAUDIOTRACK::GetCacheTotal()
+{
+ // total amount that the audio sink can buffer in units of seconds
+ return m_audiotrackbuffer_sec;
+}
+
+// this method is supposed to block until all frames are written to the device buffer
+// when it returns ActiveAESink will take the next buffer out of a queue
+unsigned int CAESinkAUDIOTRACK::AddPackets(uint8_t **data, unsigned int frames, unsigned int offset)
+{
+ if (!IsInitialized())
+ return INT_MAX;
+
+ // If the sink did not move twice the buffer size in time it was opened
+ // take action. Some sinks open with e.g. 128 ms nicely but under the
+ // hood need a bit more samples to start moving on sink start.
+ // Simple equation: N x stime packages in ms > 2 configured audiotrack_buffer in ms
+ // will result in the error condition triggering.
+
+ const bool isRawPt = m_passthrough && !m_info.m_wantsIECPassthrough;
+ if (!isRawPt)
+ {
+ const double max_stuck_delay_ms = m_audiotrackbuffer_sec_orig * 2000.0;
+ const double stime_ms = 1000.0 * frames / m_format.m_sampleRate;
+
+ if (m_stuckCounter * stime_ms > max_stuck_delay_ms)
+ {
+ CLog::Log(LOGERROR, "Sink got stuck with {:f} ms - ask AE for reopening", max_stuck_delay_ms);
+ usleep(max_stuck_delay_ms * 1000);
+ return INT_MAX;
+ }
+ }
+
+ // for debugging only - can be removed if everything is really stable
+ uint64_t startTime = CurrentHostCounter();
+
+ uint8_t *buffer = data[0]+offset*m_format.m_frameSize;
+ uint8_t *out_buf = buffer;
+ int size = frames * m_format.m_frameSize;
+
+ // write as many frames of audio as we can fit into our internal buffer.
+ int written = 0;
+ int loop_written = 0;
+ if (frames)
+ {
+ if (m_at_jni->getPlayState() != CJNIAudioTrack::PLAYSTATE_PLAYING)
+ m_at_jni->play();
+
+ bool retried = false;
+ int size_left = size;
+ while (written < size)
+ {
+ loop_written = AudioTrackWrite((char*)out_buf, 0, size_left);
+ written += loop_written;
+ size_left -= loop_written;
+
+ if (loop_written < 0)
+ {
+ CLog::Log(LOGERROR, "CAESinkAUDIOTRACK::AddPackets write returned error: {}",
+ loop_written);
+ return INT_MAX;
+ }
+
+ // if we could not add any data - sleep a bit and retry
+ if (loop_written == 0)
+ {
+ if (!retried)
+ {
+ retried = true;
+ double sleep_time = 0;
+ if (m_passthrough && !m_info.m_wantsIECPassthrough)
+ {
+ sleep_time = m_format.m_streamInfo.GetDuration();
+ usleep(sleep_time * 1000);
+ }
+ else
+ {
+ sleep_time = 1000.0 * m_format.m_frames / m_format.m_sampleRate;
+ usleep(sleep_time * 1000);
+ }
+ bool playing = m_at_jni->getPlayState() == CJNIAudioTrack::PLAYSTATE_PLAYING;
+ CLog::Log(LOGDEBUG, "Retried to write onto the sink - slept: {:f} playing: {}",
+ sleep_time, playing ? "yes" : "no");
+ continue;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Repeatedly tried to write onto the sink - giving up");
+ break;
+ }
+ }
+ retried = false; // at least one time there was more than zero data written
+ if (m_passthrough && !m_info.m_wantsIECPassthrough)
+ {
+ if (written == size)
+ m_duration_written += m_format.m_streamInfo.GetDuration() / 1000;
+ else
+ {
+ CLog::Log(LOGDEBUG, "Error writing full package to sink, left: {}", size_left);
+ // Let AE wait some ms to come back
+ unsigned int written_frames = (unsigned int) (written/m_format.m_frameSize);
+ return written_frames;
+ }
+ }
+ else
+ {
+ double duration =
+ (static_cast<double>(loop_written) / m_format.m_frameSize) / m_format.m_sampleRate;
+ m_duration_written += duration;
+ }
+
+ // just try again to care for fragmentation
+ if (written < size)
+ out_buf = out_buf + loop_written;
+
+ loop_written = 0;
+ }
+ }
+ unsigned int written_frames = static_cast<unsigned int>(written / m_format.m_frameSize);
+ double time_to_add_ms = 1000.0 * (CurrentHostCounter() - startTime) / CurrentHostFrequency();
+ if (m_passthrough && !m_info.m_wantsIECPassthrough)
+ {
+ // AT does not consume in a blocking way - it runs ahead and blocks
+ // exactly once with the last package for some 100 ms
+ double extra_sleep = 0.0;
+ if (time_to_add_ms < m_format.m_streamInfo.GetDuration())
+ extra_sleep = (m_format.m_streamInfo.GetDuration() - time_to_add_ms) / 2;
+
+ // if there is still place, just add it without blocking
+ if (m_delay < (m_audiotrackbuffer_sec - (m_format.m_streamInfo.GetDuration() / 1000.0)))
+ extra_sleep = 0;
+
+ if (m_pause_ms > 0.0)
+ {
+ extra_sleep = 0;
+ m_pause_ms -= m_format.m_streamInfo.GetDuration();
+ if (m_pause_ms <= 0.0)
+ {
+ m_pause_ms = 0.0;
+ CLog::Log(LOGINFO, "Resetting pause bursts as buffer level was reached! (1)");
+ }
+ }
+
+ usleep(extra_sleep * 1000);
+ }
+ else
+ {
+ // waiting should only be done if sink is not run dry
+ double period_time = m_format.m_frames / static_cast<double>(m_sink_sampleRate);
+ if (m_delay >= (m_audiotrackbuffer_sec - period_time))
+ {
+ double time_should_ms = 1000.0 * written_frames / m_format.m_sampleRate;
+ double time_off = time_should_ms - time_to_add_ms;
+ if (time_off > 0)
+ usleep(time_off * 500); // sleep half the error on average away
+ }
+ }
+
+ return written_frames;
+}
+
+void CAESinkAUDIOTRACK::AddPause(unsigned int millis)
+{
+ if (!m_at_jni)
+ return;
+
+ // just sleep out the frames
+ if (m_at_jni->getPlayState() != CJNIAudioTrack::PLAYSTATE_PAUSED)
+ m_at_jni->pause();
+
+ // This is a mixture to get it right between
+ // blocking, sleeping roughly and GetDelay smoothing
+ // In short: Shit in, shit out
+ usleep(millis * 1000);
+ m_pause_ms += millis;
+}
+
+void CAESinkAUDIOTRACK::Drain()
+{
+ if (!m_at_jni)
+ return;
+
+ CLog::Log(LOGDEBUG, "Draining Audio");
+ if (IsInitialized())
+ {
+ m_at_jni->stop();
+ // stay ready
+ m_at_jni->pause();
+ }
+ m_duration_written = 0;
+ m_headPos = 0;
+ m_stuckCounter = 0;
+ m_timestampPos = 0;
+ m_linearmovingaverage.clear();
+ m_stampTimer.SetExpired();
+ m_pause_ms = 0.0;
+}
+
+void CAESinkAUDIOTRACK::Register()
+{
+ AE::AESinkRegEntry entry;
+ entry.sinkName = "AUDIOTRACK";
+ entry.createFunc = CAESinkAUDIOTRACK::Create;
+ entry.enumerateFunc = CAESinkAUDIOTRACK::EnumerateDevicesEx;
+ AE::CAESinkFactory::RegisterSink(entry);
+}
+
+IAESink* CAESinkAUDIOTRACK::Create(std::string &device, AEAudioFormat& desiredFormat)
+{
+ IAESink* sink = new CAESinkAUDIOTRACK();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+
+void CAESinkAUDIOTRACK::EnumerateDevicesEx(AEDeviceInfoList &list, bool force)
+{
+ // Clear everything
+ m_info.m_channels.Reset();
+ m_info.m_dataFormats.clear();
+ m_info.m_sampleRates.clear();
+ m_info.m_streamTypes.clear();
+ m_sink_sampleRates.clear();
+
+ m_info.m_deviceType = AE_DEVTYPE_PCM;
+ m_info.m_deviceName = "AudioTrack (IEC)";
+ m_info.m_displayName = "AudioTrack (IEC)";
+ m_info.m_displayNameExtra = "Kodi IEC packer (recommended)";
+
+ // Query IEC capabilities
+ bool isRaw = false;
+ if (CJNIAudioFormat::ENCODING_IEC61937 != -1)
+ {
+ UpdateAvailablePCMCapabilities();
+ UpdateAvailablePassthroughCapabilities(isRaw);
+
+ if (!m_info.m_streamTypes.empty())
+ {
+ m_info_iec = m_info;
+ list.push_back(m_info_iec);
+ m_hasIEC = true;
+ }
+ }
+
+ // Query RAW capabilities
+ isRaw = true;
+ m_info.m_deviceName = "AudioTrack (RAW)";
+ m_info.m_displayName = "AudioTrack (RAW)";
+ m_info.m_displayNameExtra = "Android IEC packer";
+ UpdateAvailablePCMCapabilities();
+ UpdateAvailablePassthroughCapabilities(isRaw);
+ m_info_raw = m_info;
+
+ // no need to display two PCM sinks - as they are the same
+ if (!list.empty())
+ m_info_raw.m_onlyPassthrough = true;
+
+ list.push_back(m_info_raw);
+}
+
+void CAESinkAUDIOTRACK::UpdateAvailablePassthroughCapabilities(bool isRaw)
+{
+ m_info.m_deviceType = AE_DEVTYPE_HDMI;
+ m_info.m_wantsIECPassthrough = false;
+ m_info.m_dataFormats.push_back(AE_FMT_RAW);
+ m_info.m_streamTypes.clear();
+ if (isRaw)
+ {
+ bool canDoAC3 = false;
+
+ if (CJNIAudioFormat::ENCODING_AC3 != -1)
+ {
+ if (VerifySinkConfiguration(48000, CJNIAudioFormat::CHANNEL_OUT_STEREO,
+ CJNIAudioFormat::ENCODING_AC3, true))
+ {
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ CLog::Log(LOGDEBUG, "Firmware implements AC3 RAW");
+
+ canDoAC3 = true;
+ }
+ }
+
+ // EAC3 working on shield, broken on FireTV
+ if (CJNIAudioFormat::ENCODING_E_AC3 != -1)
+ {
+ if (VerifySinkConfiguration(48000, CJNIAudioFormat::CHANNEL_OUT_STEREO,
+ CJNIAudioFormat::ENCODING_E_AC3, true))
+ {
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_EAC3);
+ CLog::Log(LOGDEBUG, "Firmware implements EAC3 RAW");
+
+ if (!canDoAC3)
+ m_passthrough_use_eac3 = true;
+ }
+ }
+
+ if (CJNIAudioFormat::ENCODING_DTS != -1)
+ {
+ if (VerifySinkConfiguration(48000, CJNIAudioFormat::CHANNEL_OUT_STEREO,
+ CJNIAudioFormat::ENCODING_DTS, true))
+ {
+ CLog::Log(LOGDEBUG, "Firmware implements DTS RAW");
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ }
+ }
+
+ if (CJNIAudioManager::GetSDKVersion() >= 23)
+ {
+ if (CJNIAudioFormat::ENCODING_DTS_HD != -1)
+ {
+ if (VerifySinkConfiguration(48000, AEChannelMapToAUDIOTRACKChannelMask(AE_CH_LAYOUT_7_1),
+ CJNIAudioFormat::ENCODING_DTS_HD, true))
+ {
+ CLog::Log(LOGDEBUG, "Firmware implements DTS-HD RAW");
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD);
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_MA);
+ }
+ }
+ if (CJNIAudioFormat::ENCODING_DOLBY_TRUEHD != -1)
+ {
+ if (VerifySinkConfiguration(48000, AEChannelMapToAUDIOTRACKChannelMask(AE_CH_LAYOUT_7_1),
+ CJNIAudioFormat::ENCODING_DOLBY_TRUEHD, true))
+ {
+ CLog::Log(LOGDEBUG, "Firmware implements TrueHD RAW");
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_TRUEHD);
+ }
+ }
+ }
+ }
+ else
+ {
+ // Android v24 and backports can do real IEC API
+ if (CJNIAudioFormat::ENCODING_IEC61937 != -1)
+ {
+ // check if we support opening an IEC sink at all:
+ bool supports_iec = VerifySinkConfiguration(48000, CJNIAudioFormat::CHANNEL_OUT_STEREO,
+ CJNIAudioFormat::ENCODING_IEC61937);
+ if (supports_iec)
+ {
+ bool supports_192khz = m_sink_sampleRates.find(192000) != m_sink_sampleRates.end();
+ m_info.m_wantsIECPassthrough = true;
+ m_info.m_streamTypes.clear();
+ m_info.m_dataFormats.push_back(AE_FMT_RAW);
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ CLog::Log(LOGDEBUG, "AESinkAUDIOTrack: Using IEC PT mode: {}",
+ CJNIAudioFormat::ENCODING_IEC61937);
+ CLog::Log(LOGDEBUG, "AC3 and DTS via IEC61937 is supported");
+ if (supports_192khz)
+ {
+ // Check for IEC 2 channel 192 khz PT DTS-HD-HR and E-AC3
+ if (VerifySinkConfiguration(192000, CJNIAudioFormat::CHANNEL_OUT_STEREO,
+ CJNIAudioFormat::ENCODING_IEC61937))
+ {
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_EAC3);
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD);
+ CLog::Log(LOGDEBUG, "E-AC3 and DTSHD-HR via IEC61937 is supported");
+ }
+ // Check for IEC 8 channel 192 khz PT DTS-HD-MA and TrueHD
+ int atChannelMask = AEChannelMapToAUDIOTRACKChannelMask(AE_CH_LAYOUT_7_1);
+ if (VerifySinkConfiguration(192000, atChannelMask, CJNIAudioFormat::ENCODING_IEC61937))
+ {
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_MA);
+ m_info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_TRUEHD);
+ CLog::Log(LOGDEBUG, "DTSHD-MA and TrueHD via IEC61937 is supported");
+ }
+ }
+ }
+ }
+ }
+}
+
+void CAESinkAUDIOTRACK::UpdateAvailablePCMCapabilities()
+{
+ m_info.m_channels = KnownChannels;
+
+ // default fallback format
+ m_info.m_dataFormats.push_back(AE_FMT_S16LE);
+ unsigned int native_sampleRate = CJNIAudioTrack::getNativeOutputSampleRate(CJNIAudioManager::STREAM_MUSIC);
+ m_sink_sampleRates.insert(native_sampleRate);
+
+ int encoding = CJNIAudioFormat::ENCODING_PCM_16BIT;
+ m_sinkSupportsFloat = VerifySinkConfiguration(native_sampleRate, CJNIAudioFormat::CHANNEL_OUT_STEREO, CJNIAudioFormat::ENCODING_PCM_FLOAT);
+ // Only try for Android 7 or later - there are a lot of old devices that open successfully
+ // but won't work correctly under the hood (famouse example: old FireTV)
+ // As even newish devices like Android Chromecast don't do it properly - just disable it ... and use 16 bit Integer
+ //if (CJNIAudioManager::GetSDKVersion() > 23)
+ // m_sinkSupportsMultiChannelFloat = VerifySinkConfiguration(native_sampleRate, CJNIAudioFormat::CHANNEL_OUT_7POINT1_SURROUND, CJNIAudioFormat::ENCODING_PCM_FLOAT);
+
+ if (m_sinkSupportsFloat)
+ {
+ encoding = CJNIAudioFormat::ENCODING_PCM_FLOAT;
+ m_info.m_dataFormats.push_back(AE_FMT_FLOAT);
+ CLog::Log(LOGINFO, "Float is supported");
+ }
+ if (m_sinkSupportsMultiChannelFloat)
+ {
+ CLog::Log(LOGINFO, "Multi channel Float is supported");
+ }
+
+ int test_sample[] = { 32000, 44100, 48000, 88200, 96000, 176400, 192000 };
+ int test_sample_sz = sizeof(test_sample) / sizeof(int);
+
+ for (int i = 0; i < test_sample_sz; ++i)
+ {
+ if (IsSupported(test_sample[i], CJNIAudioFormat::CHANNEL_OUT_STEREO, encoding))
+ {
+ m_sink_sampleRates.insert(test_sample[i]);
+ CLog::Log(LOGDEBUG, "AESinkAUDIOTRACK - {} supported", test_sample[i]);
+ }
+ }
+ std::copy(m_sink_sampleRates.begin(), m_sink_sampleRates.end(), std::back_inserter(m_info.m_sampleRates));
+}
+
+double CAESinkAUDIOTRACK::GetMovingAverageDelay(double newestdelay)
+{
+#if defined AT_USE_EXPONENTIAL_AVERAGING
+ double old = 0.0;
+ if (m_linearmovingaverage.empty()) // just for creating one space in list
+ m_linearmovingaverage.push_back(newestdelay);
+ else
+ old = m_linearmovingaverage.front();
+
+ const double alpha = 0.3;
+ const double beta = 0.7;
+
+ double d = alpha * newestdelay + beta * old;
+ m_linearmovingaverage.at(0) = d;
+
+ return d;
+#endif
+
+ m_linearmovingaverage.push_back(newestdelay);
+
+ // new values are in the back, old values are in the front
+ // oldest value is removed if elements > MOVING_AVERAGE_MAX_MEMBERS
+ // removing first element of a vector sucks - I know that
+ // but hey - 10 elements - not 1 million
+ size_t size = m_linearmovingaverage.size();
+ if (size > MOVING_AVERAGE_MAX_MEMBERS)
+ {
+ m_linearmovingaverage.pop_front();
+ size--;
+ }
+ // m_{LWMA}^{(n)}(t) = \frac{2}{n (n+1)} \sum_{i=1}^n i \; x(t-n+i)
+ const double denom = 2.0 / (size * (size + 1));
+ double sum = 0.0;
+ for (size_t i = 0; i < m_linearmovingaverage.size(); i++)
+ sum += (i + 1) * m_linearmovingaverage.at(i);
+
+ return sum * denom;
+}
+
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkAUDIOTRACK.h b/xbmc/cores/AudioEngine/Sinks/AESinkAUDIOTRACK.h
new file mode 100644
index 0000000..6e9ec76
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkAUDIOTRACK.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 "cores/AudioEngine/Interfaces/AESink.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "threads/Thread.h"
+
+#include <deque>
+#include <set>
+
+#include <androidjni/AudioTrack.h>
+
+class CAESinkAUDIOTRACK : public IAESink
+{
+public:
+ const char* GetName() override { return "AUDIOTRACK"; }
+
+ CAESinkAUDIOTRACK();
+ ~CAESinkAUDIOTRACK() override;
+
+ bool Initialize(AEAudioFormat& format, std::string& device) override;
+ void Deinitialize() override;
+ bool IsInitialized();
+
+ void GetDelay(AEDelayStatus& status) override;
+ double GetLatency() override;
+ double GetCacheTotal() override;
+ unsigned int AddPackets(uint8_t** data, unsigned int frames, unsigned int offset) override;
+ void AddPause(unsigned int millis) override;
+ void Drain() override;
+ static void EnumerateDevicesEx(AEDeviceInfoList &list, bool force = false);
+ static void Register();
+ static IAESink* Create(std::string &device, AEAudioFormat &desiredFormat);
+
+protected:
+ static jni::CJNIAudioTrack *CreateAudioTrack(int stream, int sampleRate, int channelMask, int encoding, int bufferSize);
+ static bool IsSupported(int sampleRateInHz, int channelConfig, int audioFormat);
+ static bool VerifySinkConfiguration(int sampleRate,
+ int channelMask,
+ int encoding,
+ bool isRaw = false);
+ static void UpdateAvailablePCMCapabilities();
+ static void UpdateAvailablePassthroughCapabilities(bool isRaw = false);
+
+ int AudioTrackWrite(char* audioData, int offsetInBytes, int sizeInBytes);
+ int AudioTrackWrite(char* audioData, int sizeInBytes, int64_t timestamp);
+
+private:
+ jni::CJNIAudioTrack *m_at_jni;
+ int m_jniAudioFormat;
+
+ double m_duration_written;
+ unsigned int m_min_buffer_size;
+ uint64_t m_headPos;
+ uint64_t m_headPosOld;
+ uint32_t m_stuckCounter;
+ uint64_t m_timestampPos = 0;
+ // Moving Average computes the weighted average delay over
+ // a fixed size of delay values - current size: 20 values
+ double GetMovingAverageDelay(double newestdelay);
+
+ // We maintain our linear weighted average delay counter in here
+ // The n-th value (timely oldest value) is weighted with 1/n
+ // the newest value gets a weight of 1
+ std::deque<double> m_linearmovingaverage;
+
+ static CAEDeviceInfo m_info;
+ static CAEDeviceInfo m_info_raw;
+ static CAEDeviceInfo m_info_iec;
+ static bool m_hasIEC;
+ static std::set<unsigned int> m_sink_sampleRates;
+ static bool m_sinkSupportsFloat;
+ static bool m_sinkSupportsMultiChannelFloat;
+ static bool m_passthrough_use_eac3;
+
+ AEAudioFormat m_format;
+ int16_t *m_alignedS16;
+ unsigned int m_sink_frameSize;
+ unsigned int m_sink_sampleRate;
+ bool m_passthrough;
+ double m_audiotrackbuffer_sec;
+ double m_audiotrackbuffer_sec_orig;
+ int m_encoding;
+ double m_pause_ms = 0.0;
+ double m_delay = 0.0;
+ double m_hw_delay = 0.0;
+ CJNIAudioTimestamp m_timestamp;
+ XbmcThreads::EndTime<> m_stampTimer;
+
+ std::vector<float> m_floatbuf;
+ std::vector<int16_t> m_shortbuf;
+ std::vector<char> m_charbuf;
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkDARWINIOS.h b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINIOS.h
new file mode 100644
index 0000000..708b0f6
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINIOS.h
@@ -0,0 +1,56 @@
+/*
+ * 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 CAESinkDARWINIOS : public IAESink
+{
+public:
+ const char* GetName() override { return "DARWINIOS"; }
+
+ CAESinkDARWINIOS();
+ ~CAESinkDARWINIOS() 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/AESinkDARWINIOS.mm b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINIOS.mm
new file mode 100644
index 0000000..34b52fa
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINIOS.mm
@@ -0,0 +1,766 @@
+/*
+ * 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 "cores/AudioEngine/Sinks/AESinkDARWINIOS.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 "threads/SystemClock.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+#include <sstream>
+
+#include <AudioToolbox/AudioToolbox.h>
+
+using namespace std::chrono_literals;
+
+#define CA_MAX_CHANNELS 8
+static enum AEChannel CAChannelMap[CA_MAX_CHANNELS + 1] = {
+ AE_CH_FL , AE_CH_FR , AE_CH_BL , AE_CH_BR , AE_CH_FC , AE_CH_LFE , AE_CH_SL , AE_CH_SR ,
+ AE_CH_NULL
+};
+
+/***************************************************************************************/
+/***************************************************************************************/
+#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
+
+/***************************************************************************************/
+/***************************************************************************************/
+class CAAudioUnitSink
+{
+ public:
+ CAAudioUnitSink() = default;
+ ~CAAudioUnitSink();
+
+ bool open(AudioStreamBasicDescription outputFormat);
+ bool close();
+ bool play(bool mute);
+ bool mute(bool mute);
+ bool pause();
+ void drain();
+ void getDelay(AEDelayStatus& status);
+ double cacheSize();
+ unsigned int write(uint8_t *data, unsigned int byte_count);
+ unsigned int chunkSize() { return m_bufferDuration * m_sampleRate; }
+ unsigned int getRealisedSampleRate() { return m_outputFormat.mSampleRate; }
+ static Float64 getCoreAudioRealisedSampleRate();
+
+ private:
+ void setCoreAudioBuffersize();
+ bool setCoreAudioInputFormat();
+ void setCoreAudioPreferredSampleRate();
+ bool setupAudio();
+ bool checkAudioRoute();
+ bool checkSessionProperties();
+ bool activateAudioSession();
+ void deactivateAudioSession();
+
+ // callbacks
+ static void sessionPropertyCallback(void *inClientData,
+ AudioSessionPropertyID inID, UInt32 inDataSize, const void *inData);
+
+ static OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags,
+ const AudioTimeStamp *inTimeStamp, UInt32 inOutputBusNumber, UInt32 inNumberFrames,
+ AudioBufferList *ioData);
+
+ bool m_setup;
+ bool m_activated = false;
+ AudioUnit m_audioUnit;
+ AudioStreamBasicDescription m_outputFormat;
+ AERingBuffer* m_buffer = nullptr;
+
+ bool m_mute;
+ Float32 m_outputVolume;
+ Float32 m_outputLatency;
+ Float32 m_bufferDuration;
+
+ unsigned int m_sampleRate;
+ unsigned int m_frameSize;
+
+ bool m_playing = false;
+ volatile bool m_started = false;
+
+ CAESpinSection m_render_section;
+ volatile int64_t m_render_timestamp = 0;
+ volatile uint32_t m_render_frames = 0;
+};
+
+CAAudioUnitSink::~CAAudioUnitSink()
+{
+ close();
+}
+
+bool CAAudioUnitSink::open(AudioStreamBasicDescription outputFormat)
+{
+ m_mute = false;
+ m_setup = false;
+ m_outputFormat = outputFormat;
+ m_outputLatency = 0.0;
+ m_bufferDuration= 0.0;
+ m_outputVolume = 1.0;
+ m_sampleRate = (unsigned int)outputFormat.mSampleRate;
+ m_frameSize = outputFormat.mChannelsPerFrame * outputFormat.mBitsPerChannel / 8;
+
+ /* TODO: Reduce the size of this buffer, pre-calculate the size based on how large
+ the buffers are that CA calls us with in the renderCallback - perhaps call
+ the checkSessionProperties() before running this? */
+ m_buffer = new AERingBuffer(16384);
+
+ return setupAudio();
+}
+
+bool CAAudioUnitSink::close()
+{
+ deactivateAudioSession();
+
+ delete m_buffer;
+ m_buffer = NULL;
+
+ m_started = false;
+ return true;
+}
+
+bool CAAudioUnitSink::play(bool mute)
+{
+ if (!m_playing)
+ {
+ if (activateAudioSession())
+ {
+ CAAudioUnitSink::mute(mute);
+ m_playing = !AudioOutputUnitStart(m_audioUnit);
+ }
+ }
+
+ return m_playing;
+}
+
+bool CAAudioUnitSink::mute(bool mute)
+{
+ m_mute = mute;
+
+ return true;
+}
+
+bool CAAudioUnitSink::pause()
+{
+ if (m_playing)
+ m_playing = AudioOutputUnitStop(m_audioUnit);
+
+ return m_playing;
+}
+
+void CAAudioUnitSink::getDelay(AEDelayStatus& status)
+{
+ CAESpinLock lock(m_render_section);
+ do
+ {
+ status.delay = (double)m_buffer->GetReadSize() / m_frameSize;
+ status.delay += (double)m_render_frames;
+ status.tick = m_render_timestamp;
+ } while(lock.retry());
+
+ status.delay /= m_sampleRate;
+ status.delay += static_cast<double>(m_bufferDuration + m_outputLatency);
+}
+
+double CAAudioUnitSink::cacheSize()
+{
+ return (double)m_buffer->GetMaxSize() / (double)(m_frameSize * m_sampleRate);
+}
+
+CCriticalSection mutex;
+XbmcThreads::ConditionVariable condVar;
+
+unsigned int CAAudioUnitSink::write(uint8_t *data, unsigned int frames)
+{
+ if (m_buffer->GetWriteSize() < frames * m_frameSize)
+ { // no space to write - wait for a bit
+ std::unique_lock<CCriticalSection> lock(mutex);
+ auto timeout = std::chrono::milliseconds(900 * frames / m_sampleRate);
+ if (!m_started)
+ timeout = 4500ms;
+
+ // we are using a timer here for being 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, "{} engine didn't start in {} ms!", __FUNCTION__, timeout.count());
+ return INT_MAX;
+ }
+ }
+
+ unsigned int write_frames = std::min(frames, m_buffer->GetWriteSize() / m_frameSize);
+ if (write_frames)
+ m_buffer->Write(data, write_frames * m_frameSize);
+
+ return write_frames;
+}
+
+void CAAudioUnitSink::drain()
+{
+ unsigned int bytes = m_buffer->GetReadSize();
+ unsigned int totalBytes = bytes;
+ int maxNumTimeouts = 3;
+ auto timeout = std::chrono::milliseconds(900 * bytes / (m_sampleRate * m_frameSize));
+ while (bytes && maxNumTimeouts > 0)
+ {
+ std::unique_lock<CCriticalSection> lock(mutex);
+ XbmcThreads::EndTime<> timer(timeout);
+ condVar.wait(mutex, timeout);
+
+ bytes = m_buffer->GetReadSize();
+ // if we timeout and don't
+ // consume bytes - decrease maxNumTimeouts
+ if (timer.IsTimePast() && bytes == totalBytes)
+ maxNumTimeouts--;
+ totalBytes = bytes;
+ }
+}
+
+void CAAudioUnitSink::setCoreAudioBuffersize()
+{
+#if !TARGET_IPHONE_SIMULATOR
+ // set the buffer size, this affects the number of samples
+ // that get rendered every time the audio callback is fired.
+ Float32 preferredBufferSize = 512 * m_outputFormat.mChannelsPerFrame / m_outputFormat.mSampleRate;
+ CLog::Log(LOGINFO, "{} setting buffer duration to {:f}", __PRETTY_FUNCTION__,
+ preferredBufferSize);
+ OSStatus status = AudioSessionSetProperty(kAudioSessionProperty_PreferredHardwareIOBufferDuration,
+ sizeof(preferredBufferSize), &preferredBufferSize);
+ if (status != noErr)
+ CLog::Log(LOGWARNING, "{} preferredBufferSize couldn't be set (error: {})", __PRETTY_FUNCTION__,
+ (int)status);
+#endif
+}
+
+bool CAAudioUnitSink::setCoreAudioInputFormat()
+{
+ // Set the output stream format
+ UInt32 ioDataSize = sizeof(AudioStreamBasicDescription);
+ OSStatus status = AudioUnitSetProperty(m_audioUnit, kAudioUnitProperty_StreamFormat,
+ kAudioUnitScope_Input, 0, &m_outputFormat, ioDataSize);
+ if (status != noErr)
+ {
+ CLog::Log(LOGERROR, "{} error setting stream format on audioUnit (error: {})",
+ __PRETTY_FUNCTION__, (int)status);
+ return false;
+ }
+ return true;
+}
+
+void CAAudioUnitSink::setCoreAudioPreferredSampleRate()
+{
+ Float64 preferredSampleRate = m_outputFormat.mSampleRate;
+ CLog::Log(LOGINFO, "{} requesting hw samplerate {:f}", __PRETTY_FUNCTION__, preferredSampleRate);
+ OSStatus status = AudioSessionSetProperty(kAudioSessionProperty_PreferredHardwareSampleRate,
+ sizeof(preferredSampleRate), &preferredSampleRate);
+ if (status != noErr)
+ CLog::Log(LOGWARNING, "{} preferredSampleRate couldn't be set (error: {})", __PRETTY_FUNCTION__,
+ (int)status);
+}
+
+Float64 CAAudioUnitSink::getCoreAudioRealisedSampleRate()
+{
+ Float64 outputSampleRate = 0.0;
+ UInt32 ioDataSize = sizeof(outputSampleRate);
+ if (AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareSampleRate,
+ &ioDataSize, &outputSampleRate) != noErr)
+ CLog::Log(LOGERROR, "{}: error getting CurrentHardwareSampleRate", __FUNCTION__);
+ return outputSampleRate;
+}
+
+bool CAAudioUnitSink::setupAudio()
+{
+ OSStatus status = noErr;
+ if (m_setup && m_audioUnit)
+ return true;
+
+ AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange,
+ sessionPropertyCallback, this);
+
+ AudioSessionAddPropertyListener(kAudioSessionProperty_CurrentHardwareOutputVolume,
+ sessionPropertyCallback, this);
+
+ // 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(NULL, &description);
+ status = AudioComponentInstanceNew(component, &m_audioUnit);
+ if (status != noErr)
+ {
+ CLog::Log(LOGERROR, "{} error creating audioUnit (error: {})", __PRETTY_FUNCTION__,
+ (int)status);
+ return false;
+ }
+
+ setCoreAudioPreferredSampleRate();
+
+ // Get the output samplerate for knowing what was setup in reality
+ Float64 realisedSampleRate = getCoreAudioRealisedSampleRate();
+ if (m_outputFormat.mSampleRate != realisedSampleRate)
+ {
+ CLog::Log(LOGINFO,
+ "{} couldn't set requested samplerate {}, coreaudio will resample to {} instead",
+ __PRETTY_FUNCTION__, (int)m_outputFormat.mSampleRate, (int)realisedSampleRate);
+ // if we don't ca to resample - but instead let activeae resample -
+ // reflect the realised samplerate to the outputformat here
+ // well maybe it is handy in the future - as of writing this
+ // ca was about 6 times faster then activeae ;)
+ //m_outputFormat.mSampleRate = realisedSampleRate;
+ //m_sampleRate = realisedSampleRate;
+ }
+
+ setCoreAudioBuffersize();
+ if (!setCoreAudioInputFormat())
+ return false;
+
+ // Attach a render callback on the unit
+ AURenderCallbackStruct callbackStruct = {};
+ callbackStruct.inputProc = renderCallback;
+ callbackStruct.inputProcRefCon = this;
+ status = AudioUnitSetProperty(m_audioUnit,
+ kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input,
+ 0, &callbackStruct, sizeof(callbackStruct));
+ if (status != noErr)
+ {
+ CLog::Log(LOGERROR, "{} error setting render callback for audioUnit (error: {})",
+ __PRETTY_FUNCTION__, (int)status);
+ return false;
+ }
+
+ status = AudioUnitInitialize(m_audioUnit);
+ if (status != noErr)
+ {
+ CLog::Log(LOGERROR, "{} error initializing audioUnit (error: {})", __PRETTY_FUNCTION__,
+ (int)status);
+ return false;
+ }
+
+ checkSessionProperties();
+
+ m_setup = true;
+ std::string formatString;
+ CLog::Log(LOGINFO, "{} setup audio format: {}", __PRETTY_FUNCTION__,
+ StreamDescriptionToString(m_outputFormat, formatString));
+
+ return m_setup;
+}
+
+bool CAAudioUnitSink::checkAudioRoute()
+{
+ // why do we need to know the audio route ?
+ CFStringRef route;
+ UInt32 propertySize = sizeof(CFStringRef);
+ if (AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &route) != noErr)
+ return false;
+
+ return true;
+}
+
+bool CAAudioUnitSink::checkSessionProperties()
+{
+ checkAudioRoute();
+
+ UInt32 ioDataSize;
+ ioDataSize = sizeof(m_outputVolume);
+ if (AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareOutputVolume,
+ &ioDataSize, &m_outputVolume) != noErr)
+ CLog::Log(LOGERROR, "{}: error getting CurrentHardwareOutputVolume", __FUNCTION__);
+
+ ioDataSize = sizeof(m_outputLatency);
+ if (AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareOutputLatency,
+ &ioDataSize, &m_outputLatency) != noErr)
+ CLog::Log(LOGERROR, "{}: error getting CurrentHardwareOutputLatency", __FUNCTION__);
+
+ ioDataSize = sizeof(m_bufferDuration);
+ if (AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareIOBufferDuration,
+ &ioDataSize, &m_bufferDuration) != noErr)
+ CLog::Log(LOGERROR, "{}: error getting CurrentHardwareIOBufferDuration", __FUNCTION__);
+
+ CLog::Log(LOGDEBUG, "{}: volume = {:f}, latency = {:f}, buffer = {:f}", __FUNCTION__,
+ m_outputVolume, m_outputLatency, m_bufferDuration);
+ return true;
+}
+
+bool CAAudioUnitSink::activateAudioSession()
+{
+ if (!m_activated)
+ {
+ if (checkAudioRoute() && setupAudio())
+ m_activated = true;
+ }
+
+ return m_activated;
+}
+
+void CAAudioUnitSink::deactivateAudioSession()
+{
+ if (m_activated)
+ {
+ pause();
+ AudioUnitUninitialize(m_audioUnit);
+ AudioComponentInstanceDispose(m_audioUnit), m_audioUnit = NULL;
+ AudioSessionRemovePropertyListenerWithUserData(kAudioSessionProperty_AudioRouteChange,
+ sessionPropertyCallback, this);
+ AudioSessionRemovePropertyListenerWithUserData(kAudioSessionProperty_CurrentHardwareOutputVolume,
+ sessionPropertyCallback, this);
+
+ m_setup = false;
+ m_activated = false;
+ }
+}
+
+void CAAudioUnitSink::sessionPropertyCallback(void *inClientData,
+ AudioSessionPropertyID inID, UInt32 inDataSize, const void *inData)
+{
+ CAAudioUnitSink *sink = (CAAudioUnitSink*)inClientData;
+
+ if (inID == kAudioSessionProperty_AudioRouteChange)
+ {
+ if (sink->checkAudioRoute())
+ sink->checkSessionProperties();
+ }
+ else if (inID == kAudioSessionProperty_CurrentHardwareOutputVolume)
+ {
+ if (inData && inDataSize == 4)
+ sink->m_outputVolume = *(float*)inData;
+ }
+}
+
+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: {}flow ({} vs {} 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++)
+ {
+ // buffers come from CA already zero'd, so just copy what is wanted
+ unsigned int wanted = ioData->mBuffers[i].mDataByteSize;
+ unsigned int bytes = std::min(sink->m_buffer->GetReadSize(), wanted);
+ sink->m_buffer->Read((unsigned char*)ioData->mBuffers[i].mData, bytes);
+ LogLevel(bytes, wanted);
+
+ if (bytes == 0)
+ *ioActionFlags |= kAudioUnitRenderAction_OutputIsSilence;
+ }
+
+ sink->m_render_timestamp = inTimeStamp->mHostTime;
+ sink->m_render_frames = inNumberFrames;
+ sink->m_render_section.leave();
+ // tell the sink we're good for more data
+ condVar.notifyAll();
+
+ return noErr;
+}
+
+/***************************************************************************************/
+/***************************************************************************************/
+static void EnumerateDevices(AEDeviceInfoList &list)
+{
+ CAEDeviceInfo device;
+
+ device.m_deviceName = "default";
+ device.m_displayName = "Default";
+ device.m_displayNameExtra = "";
+ // TODO screen changing on ios needs to call
+ // devices changed once this is available in active
+ if (false)
+ {
+ device.m_deviceType = AE_DEVTYPE_IEC958; //allow passthrough for tvout
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ device.m_dataFormats.push_back(AE_FMT_RAW);
+ }
+ else
+ device.m_deviceType = AE_DEVTYPE_PCM;
+
+ // add channel info
+ CAEChannelInfo channel_info;
+ for (UInt32 chan = 0; chan < 2; ++chan)
+ {
+ if (!device.m_channels.HasChannel(CAChannelMap[chan]))
+ device.m_channels += CAChannelMap[chan];
+ channel_info += CAChannelMap[chan];
+ }
+
+ // there are more supported ( one of those 2 gets resampled
+ // by coreaudio anyway) - but for keeping it save ignore
+ // the others...
+ device.m_sampleRates.push_back(44100);
+ device.m_sampleRates.push_back(48000);
+
+ device.m_dataFormats.push_back(AE_FMT_S16LE);
+ //device.m_dataFormats.push_back(AE_FMT_S24LE3);
+ //device.m_dataFormats.push_back(AE_FMT_S32LE);
+ device.m_dataFormats.push_back(AE_FMT_FLOAT);
+ device.m_wantsIECPassthrough = true;
+
+ CLog::Log(LOGDEBUG, "EnumerateDevices:Device({})", device.m_deviceName);
+
+ list.push_back(device);
+}
+
+/***************************************************************************************/
+/***************************************************************************************/
+AEDeviceInfoList CAESinkDARWINIOS::m_devices;
+
+CAESinkDARWINIOS::CAESinkDARWINIOS()
+: m_audioSink(NULL)
+{
+}
+
+void CAESinkDARWINIOS::Register()
+{
+ AE::AESinkRegEntry reg;
+ reg.sinkName = "DARWINIOS";
+ reg.createFunc = CAESinkDARWINIOS::Create;
+ reg.enumerateFunc = CAESinkDARWINIOS::EnumerateDevicesEx;
+ AE::CAESinkFactory::RegisterSink(reg);
+}
+
+IAESink* CAESinkDARWINIOS::Create(std::string &device, AEAudioFormat &desiredFormat)
+{
+ IAESink *sink = new CAESinkDARWINIOS();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+
+bool CAESinkDARWINIOS::Initialize(AEAudioFormat &format, std::string &device)
+{
+ bool found = false;
+ bool forceRaw = 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 = {};
+
+ if (format.m_dataFormat == AE_FMT_FLOAT)
+ audioFormat.mFormatFlags |= kLinearPCMFormatFlagIsFloat;
+ else// this will be selected when AE wants AC3 or DTS or anything other then float
+ {
+ audioFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger;
+ if (format.m_dataFormat == AE_FMT_RAW)
+ forceRaw = true;
+ format.m_dataFormat = AE_FMT_S16LE;
+ }
+
+ format.m_channelLayout = m_info.m_channels;
+ format.m_frameSize = format.m_channelLayout.Count() * (CAEUtil::DataFormatToBits(format.m_dataFormat) >> 3);
+
+
+ audioFormat.mFormatID = kAudioFormatLinearPCM;
+ switch(format.m_sampleRate)
+ {
+ case 11025:
+ case 22050:
+ case 44100:
+ case 88200:
+ case 176400:
+ 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 (forceRaw)//make sure input and output samplerate match for preventing resampling
+ audioFormat.mSampleRate = CAAudioUnitSink::getCoreAudioRealisedSampleRate();
+
+ audioFormat.mFramesPerPacket = 1;
+ audioFormat.mChannelsPerFrame= 2;// ios only supports 2 channels
+ audioFormat.mBitsPerChannel = CAEUtil::DataFormatToBits(format.m_dataFormat);
+ audioFormat.mBytesPerFrame = format.m_frameSize;
+ audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame * audioFormat.mFramesPerPacket;
+ audioFormat.mFormatFlags |= kLinearPCMFormatFlagIsPacked;
+
+#if DO_440HZ_TONE_TEST
+ SineWaveGeneratorInitWithFrequency(&m_SineWaveGenerator, 440.0, audioFormat.mSampleRate);
+#endif
+
+ m_audioSink = new CAAudioUnitSink;
+ m_audioSink->open(audioFormat);
+
+ format.m_frames = m_audioSink->chunkSize();
+ // reset to the realised samplerate
+ format.m_sampleRate = m_audioSink->getRealisedSampleRate();
+ m_format = format;
+
+ m_audioSink->play(false);
+
+ return true;
+}
+
+void CAESinkDARWINIOS::Deinitialize()
+{
+ delete m_audioSink;
+ m_audioSink = NULL;
+}
+
+void CAESinkDARWINIOS::GetDelay(AEDelayStatus& status)
+{
+ if (m_audioSink)
+ m_audioSink->getDelay(status);
+ else
+ status.SetDelay(0.0);
+}
+
+double CAESinkDARWINIOS::GetCacheTotal()
+{
+ if (m_audioSink)
+ return m_audioSink->cacheSize();
+ return 0.0;
+}
+
+unsigned int CAESinkDARWINIOS::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 = (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);
+ return 0;
+}
+
+void CAESinkDARWINIOS::Drain()
+{
+ if (m_audioSink)
+ m_audioSink->drain();
+}
+
+bool CAESinkDARWINIOS::HasVolume()
+{
+ return false;
+}
+
+void CAESinkDARWINIOS::EnumerateDevicesEx(AEDeviceInfoList &list, bool force)
+{
+ m_devices.clear();
+ EnumerateDevices(m_devices);
+ list = m_devices;
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkDARWINOSX.cpp b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINOSX.cpp
new file mode 100644
index 0000000..b2a0280
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINOSX.cpp
@@ -0,0 +1,556 @@
+/*
+ * 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 "cores/AudioEngine/Sinks/AESinkDARWINOSX.h"
+
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h"
+#include "cores/AudioEngine/Sinks/osx/AEDeviceEnumerationOSX.h"
+#include "cores/AudioEngine/Sinks/osx/CoreAudioHardware.h"
+#include "cores/AudioEngine/Utils/AERingBuffer.h"
+#include "threads/SystemClock.h"
+#include "utils/MemUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+static void EnumerateDevices(CADeviceList &list)
+{
+ std::string defaultDeviceName;
+ CCoreAudioHardware::GetOutputDeviceName(defaultDeviceName);
+ AudioDeviceID defaultID = CCoreAudioHardware::GetDefaultOutputDevice();
+
+ CoreAudioDeviceList deviceIDList;
+ CCoreAudioHardware::GetOutputDevices(&deviceIDList);
+ while (!deviceIDList.empty())
+ {
+ AudioDeviceID deviceID = deviceIDList.front();
+
+ AEDeviceEnumerationOSX devEnum(deviceID);
+ CADeviceList listForDevice = devEnum.GetDeviceInfoList();
+ for (UInt32 devIdx = 0; devIdx < listForDevice.size(); devIdx++)
+ list.push_back(listForDevice[devIdx]);
+
+ //in the first place of the list add the default device
+ //with name "default" - if this is selected
+ //we will output to whatever osx claims to be default
+ //(allows transition from headphones to speaker and stuff
+ //like that)
+ //fixme taking the first stream device is wrong here
+ //we rather might need the concatenation of all streams *sucks*
+ if(defaultID == deviceID && defaultDeviceName == devEnum.GetMasterDeviceName())
+ {
+ struct CADeviceInstance deviceInstance;
+ deviceInstance.audioDeviceId = deviceID;
+ deviceInstance.streamIndex = INT_MAX;//don't limit streamidx for the raw device
+ deviceInstance.sourceId = INT_MAX;
+ CAEDeviceInfo firstDevice = listForDevice.front().second;
+ firstDevice.m_deviceName = "default";
+ firstDevice.m_displayName = "Default";
+ firstDevice.m_displayNameExtra = defaultDeviceName;
+ list.insert(list.begin(), std::make_pair(deviceInstance, firstDevice));
+ }
+
+ deviceIDList.pop_front();
+ }
+}
+
+/* static, threadsafe access to the device list */
+static CADeviceList s_devices;
+static CCriticalSection s_devicesLock;
+
+static void EnumerateDevices()
+{
+ CADeviceList devices;
+ EnumerateDevices(devices);
+ {
+ std::unique_lock<CCriticalSection> lock(s_devicesLock);
+ s_devices = devices;
+ }
+}
+
+static CADeviceList GetDevices()
+{
+ CADeviceList list;
+ {
+ std::unique_lock<CCriticalSection> lock(s_devicesLock);
+ list = s_devices;
+ }
+ return list;
+}
+
+OSStatus deviceChangedCB(AudioObjectID inObjectID,
+ UInt32 inNumberAddresses,
+ const AudioObjectPropertyAddress inAddresses[],
+ void* inClientData)
+{
+ bool deviceChanged = false;
+ static AudioDeviceID oldDefaultDevice = 0;
+ AudioDeviceID currentDefaultOutputDevice = 0;
+
+ for (unsigned int i = 0; i < inNumberAddresses; i++)
+ {
+ switch (inAddresses[i].mSelector)
+ {
+ case kAudioHardwarePropertyDefaultOutputDevice:
+ currentDefaultOutputDevice = CCoreAudioHardware::GetDefaultOutputDevice();
+ // This listener is called on every change of the hardware
+ // device. So check if the default device has really changed.
+ if (oldDefaultDevice != currentDefaultOutputDevice)
+ {
+ deviceChanged = true;
+ oldDefaultDevice = currentDefaultOutputDevice;
+ }
+ break;
+ default:
+ deviceChanged = true;
+ break;
+ }
+ if (deviceChanged)
+ break;
+ }
+
+ if (deviceChanged)
+ {
+ CLog::Log(LOGDEBUG, "CoreAudio: audiodevicelist changed - reenumerating");
+ IAE* ae = CServiceBroker::GetActiveAE();
+ if (ae)
+ ae->DeviceChange();
+ CLog::Log(LOGDEBUG, "CoreAudio: audiodevicelist changed - done");
+ }
+ return noErr;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////
+CAESinkDARWINOSX::CAESinkDARWINOSX()
+{
+ // By default, kAudioHardwarePropertyRunLoop points at the process's main thread on SnowLeopard,
+ // If your process lacks such a run loop, you can set kAudioHardwarePropertyRunLoop to NULL which
+ // tells the HAL to run its own thread for notifications (which was the default prior to SnowLeopard).
+ // So tell the HAL to use its own thread for similar behavior under all supported versions of OSX.
+ CFRunLoopRef theRunLoop = NULL;
+ AudioObjectPropertyAddress theAddress = {
+ kAudioHardwarePropertyRunLoop,
+ kAudioObjectPropertyScopeGlobal,
+ kAudioObjectPropertyElementMaster
+ };
+ OSStatus theError = AudioObjectSetPropertyData(kAudioObjectSystemObject,
+ &theAddress, 0, NULL, sizeof(CFRunLoopRef), &theRunLoop);
+ if (theError != noErr)
+ {
+ CLog::Log(LOGERROR, "CCoreAudioAE::constructor: kAudioHardwarePropertyRunLoop error.");
+ }
+ CCoreAudioDevice::RegisterDeviceChangedCB(true, deviceChangedCB, this);
+ CCoreAudioDevice::RegisterDefaultOutputDeviceChangedCB(true, deviceChangedCB, this);
+}
+
+CAESinkDARWINOSX::~CAESinkDARWINOSX()
+{
+ CCoreAudioDevice::RegisterDeviceChangedCB(false, deviceChangedCB, this);
+ CCoreAudioDevice::RegisterDefaultOutputDeviceChangedCB(false, deviceChangedCB, this);
+}
+
+void CAESinkDARWINOSX::Register()
+{
+ AE::AESinkRegEntry reg;
+ reg.sinkName = "DARWINOSX";
+ reg.createFunc = CAESinkDARWINOSX::Create;
+ reg.enumerateFunc = CAESinkDARWINOSX::EnumerateDevicesEx;
+ AE::CAESinkFactory::RegisterSink(reg);
+}
+
+IAESink* CAESinkDARWINOSX::Create(std::string &device, AEAudioFormat &desiredFormat)
+{
+ IAESink *sink = new CAESinkDARWINOSX();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+
+bool CAESinkDARWINOSX::Initialize(AEAudioFormat &format, std::string &device)
+{
+ AudioDeviceID deviceID = 0;
+ UInt32 requestedStreamIndex = INT_MAX;
+ UInt32 requestedSourceId = INT_MAX;
+ bool passthrough = false;
+
+ // this sink needs IEC packing for RAW data
+ if (format.m_dataFormat == AE_FMT_RAW)
+ {
+ format.m_dataFormat= AE_FMT_S16NE;
+ passthrough = true;
+ }
+
+ CADeviceList devices = GetDevices();
+ if (StringUtils::EqualsNoCase(device, "default"))
+ {
+ CCoreAudioHardware::GetOutputDeviceName(device);
+ deviceID = CCoreAudioHardware::GetDefaultOutputDevice();
+ CLog::Log(LOGINFO, "{}: Opening default device {}", __PRETTY_FUNCTION__, device);
+ }
+ else
+ {
+ for (size_t i = 0; i < devices.size(); i++)
+ {
+ if (device == devices[i].second.m_deviceName)
+ {
+ const struct CADeviceInstance &deviceInstance = devices[i].first;
+ deviceID = deviceInstance.audioDeviceId;
+ requestedStreamIndex = deviceInstance.streamIndex;
+ requestedSourceId = deviceInstance.sourceId;
+ if (requestedStreamIndex != INT_MAX)
+ CLog::Log(LOGINFO, "{} pseudo device - requesting stream {}", __FUNCTION__,
+ (unsigned int)requestedStreamIndex);
+ if (requestedSourceId != INT_MAX)
+ CLog::Log(LOGINFO, "{} device - requesting audiosource {}", __FUNCTION__,
+ (unsigned int)requestedSourceId);
+ break;
+ }
+ }
+ }
+
+ if (!deviceID)
+ {
+ CLog::Log(LOGERROR, "{}: Unable to find device {}", __FUNCTION__, device);
+ return false;
+ }
+
+ AEDeviceEnumerationOSX devEnum(deviceID);
+ AudioStreamBasicDescription outputFormat = {};
+ AudioStreamID outputStream = 0;
+ UInt32 numOutputChannels = 0;
+ m_planes = 1;
+ // after FindSuitableFormatForStream requestedStreamIndex will have a valid index and no INT_MAX anymore ...
+ if (devEnum.FindSuitableFormatForStream(requestedStreamIndex, format, false, outputFormat, outputStream))
+ {
+ numOutputChannels = outputFormat.mChannelsPerFrame;
+
+ if (devEnum.IsPlanar())
+ {
+ numOutputChannels = std::min((size_t)format.m_channelLayout.Count(), (size_t)devEnum.GetNumPlanes());
+ m_planes = numOutputChannels;
+ CLog::Log(LOGDEBUG, "{} Found planar audio with {} channels using {} of them.", __FUNCTION__,
+ (unsigned int)devEnum.GetNumPlanes(), (unsigned int)numOutputChannels);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{}, Unable to find suitable stream", __FUNCTION__);
+ return false;
+ }
+
+ AudioStreamBasicDescription outputFormatVirt = {};
+ AudioStreamID outputStreamVirt = 0;
+ UInt32 numOutputChannelsVirt = 0;
+ if (passthrough)
+ {
+ if (devEnum.FindSuitableFormatForStream(requestedStreamIndex, format, true, outputFormatVirt, outputStreamVirt))
+ {
+ numOutputChannelsVirt = outputFormatVirt.mChannelsPerFrame;
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{}, Unable to find suitable virtual stream", __FUNCTION__);
+ //return false;
+ numOutputChannelsVirt = 0;
+ }
+ }
+
+ /* Update our AE format */
+ format.m_sampleRate = outputFormat.mSampleRate;
+
+ m_outputBufferIndex = requestedStreamIndex;
+
+ // if we are in passthrough but didn't have a matching
+ // virtual format - enable bitstream which deals with
+ // backconverting from float to 16bit
+ if (passthrough && numOutputChannelsVirt == 0)
+ {
+ m_outputBitstream = true;
+ CLog::Log(LOGDEBUG, "{}: Bitstream passthrough with float -> int16 conversion enabled",
+ __FUNCTION__);
+ }
+
+ std::string formatString;
+ CLog::Log(LOGDEBUG, "{}: Selected stream[{}] - id: {:#04X}, Physical Format: {} {}", __FUNCTION__,
+ (unsigned int)m_outputBufferIndex, (unsigned int)outputStream,
+ StreamDescriptionToString(outputFormat, formatString),
+ passthrough ? "passthrough" : "");
+
+ m_device.Open(deviceID);
+ SetHogMode(passthrough);
+
+ // Configure the output stream object
+ m_outputStream.Open(outputStream);
+
+ AudioStreamBasicDescription virtualFormat, previousPhysicalFormat;
+ m_outputStream.GetVirtualFormat(&virtualFormat);
+ m_outputStream.GetPhysicalFormat(&previousPhysicalFormat);
+ CLog::Log(LOGDEBUG, "{}: Previous Virtual Format: {}", __FUNCTION__,
+ StreamDescriptionToString(virtualFormat, formatString));
+ CLog::Log(LOGDEBUG, "{}: Previous Physical Format: {}", __FUNCTION__,
+ StreamDescriptionToString(previousPhysicalFormat, formatString));
+
+ m_outputStream.SetPhysicalFormat(&outputFormat); // Set the active format (the old one will be reverted when we close)
+ if (passthrough && numOutputChannelsVirt > 0)
+ m_outputStream.SetVirtualFormat(&outputFormatVirt);
+
+ m_outputStream.GetVirtualFormat(&virtualFormat);
+ CLog::Log(LOGDEBUG, "{}: New Virtual Format: {}", __FUNCTION__,
+ StreamDescriptionToString(virtualFormat, formatString));
+ CLog::Log(LOGDEBUG, "{}: New Physical Format: {}", __FUNCTION__,
+ StreamDescriptionToString(outputFormat, formatString));
+
+ if (requestedSourceId != INT_MAX && !m_device.SetDataSource(requestedSourceId))
+ CLog::Log(LOGERROR, "{}: Error setting requested audio source.", __FUNCTION__);
+
+ m_latentFrames = m_device.GetNumLatencyFrames();
+ m_latentFrames += m_outputStream.GetNumLatencyFrames();
+
+ // update the channel map based on the new stream format
+ devEnum.GetAEChannelMap(format.m_channelLayout, numOutputChannels);
+
+ //! @todo Should we use the virtual format to determine our data format?
+ format.m_frameSize = format.m_channelLayout.Count() * (CAEUtil::DataFormatToBits(format.m_dataFormat) >> 3);
+ format.m_frames = m_device.GetBufferSize();
+
+ m_frameSizePerPlane = format.m_frameSize / m_planes;
+ m_framesPerSecond = format.m_sampleRate;
+
+ unsigned int num_buffers = 4;
+ m_buffer = new AERingBuffer(num_buffers * format.m_frames * m_frameSizePerPlane, m_planes);
+ CLog::Log(LOGDEBUG, "{}: using buffer size: {} ({:f} ms)", __FUNCTION__, m_buffer->GetMaxSize(),
+ (float)m_buffer->GetMaxSize() / (m_framesPerSecond * m_frameSizePerPlane));
+
+ if (!passthrough)
+ format.m_dataFormat = (m_planes > 1) ? AE_FMT_FLOATP : AE_FMT_FLOAT;
+
+ // Register for data request callbacks from the driver and start
+ m_device.AddIOProc(renderCallback, this);
+ m_device.Start();
+ return true;
+}
+
+void CAESinkDARWINOSX::SetHogMode(bool on)
+{
+ //! @todo Auto hogging sets this for us. Figure out how/when to turn it off or use it
+ //! It appears that leaving this set will also restore the previous stream format when the
+ //! Application exits. If auto hogging is set and we try to set hog mode, we will deadlock
+ //! From the SDK docs: "If the AudioDevice is in a non-mixable mode, the HAL will automatically take hog mode on behalf of the first process to start an IOProc."
+ //!
+ //! Lock down the device. This MUST be done PRIOR to switching to a non-mixable format, if it is done at all
+ //! If it is attempted after the format change, there is a high likelihood of a deadlock
+ //! We may need to do this sooner to enable mix-disable (i.e. before setting the stream format)
+ if (on)
+ {
+ // Auto-Hog does not always un-hog the device when changing back to a mixable mode.
+ // Handle this on our own until it is fixed.
+ CCoreAudioHardware::SetAutoHogMode(false);
+ bool autoHog = CCoreAudioHardware::GetAutoHogMode();
+ CLog::Log(LOGDEBUG,
+ " CoreAudioRenderer::InitializeEncoded: "
+ "Auto 'hog' mode is set to '{}'.",
+ autoHog ? "On" : "Off");
+ if (autoHog)
+ return;
+ }
+ m_device.SetHogStatus(on);
+ m_device.SetMixingSupport(!on);
+}
+
+void CAESinkDARWINOSX::Deinitialize()
+{
+ m_device.Stop();
+ m_device.RemoveIOProc();
+
+ m_outputStream.Close();
+ m_device.Close();
+ if (m_buffer)
+ {
+ delete m_buffer;
+ m_buffer = NULL;
+ }
+ m_outputBufferIndex = 0;
+ m_outputBitstream = false;
+ m_planes = 1;
+
+ m_started = false;
+}
+
+void CAESinkDARWINOSX::GetDelay(AEDelayStatus& status)
+{
+ /* lockless way of guaranteeing consistency of tick/delay/buffer,
+ * this work since render callback is short and quick and higher
+ * priority compared to this thread, unsigned int are assumed
+ * aligned and having atomic read/write */
+ unsigned int size;
+ CAESpinLock lock(m_render_locker);
+ do
+ {
+ status.tick = m_render_tick;
+ status.delay = m_render_delay;
+ if(m_buffer)
+ size = m_buffer->GetReadSize();
+ else
+ size = 0;
+
+ } while(lock.retry());
+
+ status.delay += (double)size / (double)m_frameSizePerPlane / (double)m_framesPerSecond;
+ status.delay += (double)m_latentFrames / (double)m_framesPerSecond;
+}
+
+double CAESinkDARWINOSX::GetCacheTotal()
+{
+ return (double)m_buffer->GetMaxSize() / (double)(m_frameSizePerPlane * m_framesPerSecond);
+}
+
+CCriticalSection mutex;
+XbmcThreads::ConditionVariable condVar;
+
+unsigned int CAESinkDARWINOSX::AddPackets(uint8_t **data, unsigned int frames, unsigned int offset)
+{
+ if (m_buffer->GetWriteSize() < frames * m_frameSizePerPlane)
+ { // no space to write - wait for a bit
+ std::unique_lock<CCriticalSection> lock(mutex);
+ auto timeout = std::chrono::milliseconds(900 * frames / m_framesPerSecond);
+ if (!m_started)
+ timeout = 4500ms;
+
+ // we are using a timer here for being 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, "{} engine didn't start in {} ms!", __FUNCTION__, timeout.count());
+ return INT_MAX;
+ }
+ }
+
+ unsigned int write_frames = std::min(frames, m_buffer->GetWriteSize() / m_frameSizePerPlane);
+ if (write_frames)
+ {
+ for (unsigned int i = 0; i < m_buffer->NumPlanes(); i++)
+ m_buffer->Write(data[i] + offset * m_frameSizePerPlane, write_frames * m_frameSizePerPlane, i);
+ }
+ return write_frames;
+}
+
+void CAESinkDARWINOSX::Drain()
+{
+ int bytes = m_buffer->GetReadSize();
+ int totalBytes = bytes;
+ int maxNumTimeouts = 3;
+ auto timeout = std::chrono::milliseconds(900 * bytes / (m_framesPerSecond * m_frameSizePerPlane));
+ while (bytes && maxNumTimeouts > 0)
+ {
+ std::unique_lock<CCriticalSection> lock(mutex);
+ XbmcThreads::EndTime<> timer(timeout);
+ condVar.wait(mutex, timeout);
+
+ bytes = m_buffer->GetReadSize();
+ // if we timeout and don't
+ // consume bytes - decrease maxNumTimeouts
+ if (timer.IsTimePast() && bytes == totalBytes)
+ maxNumTimeouts--;
+ totalBytes = bytes;
+ }
+}
+
+void CAESinkDARWINOSX::EnumerateDevicesEx(AEDeviceInfoList &list, bool force)
+{
+ EnumerateDevices();
+ list.clear();
+ for (CADeviceList::const_iterator i = s_devices.begin(); i != s_devices.end(); ++i)
+ list.push_back(i->second);
+}
+
+inline void LogLevel(unsigned int got, unsigned int wanted)
+{
+ static unsigned int lastReported = INT_MAX;
+ if (got != wanted)
+ {
+ if (got != lastReported)
+ {
+ CLog::Log(LOGWARNING, "DARWINOSX: {}flow ({} vs {} bytes)", got > wanted ? "over" : "under",
+ got, wanted);
+ lastReported = got;
+ }
+ }
+ else
+ lastReported = INT_MAX; // indicate we were good at least once
+}
+
+OSStatus CAESinkDARWINOSX::renderCallback(AudioDeviceID inDevice, const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime, void* inClientData)
+{
+ CAESinkDARWINOSX *sink = (CAESinkDARWINOSX*)inClientData;
+
+ sink->m_render_locker.enter(); /* grab lock */
+ sink->m_started = true;
+ if (outOutputData->mNumberBuffers)
+ {
+ //planar always starts at outputbuffer/streamidx 0
+ unsigned int startIdx = sink->m_buffer->NumPlanes() == 1 ? sink->m_outputBufferIndex : 0;
+ unsigned int endIdx = startIdx + sink->m_buffer->NumPlanes();
+
+ /* NOTE: We assume that the buffers are all the same size... */
+ if (sink->m_outputBitstream)
+ {
+ /* HACK for bitstreaming AC3/DTS via PCM.
+ We reverse the float->S16LE conversion done in the stream or device */
+ static const float mul = 1.0f / (INT16_MAX + 1);
+
+ size_t wanted = outOutputData->mBuffers[0].mDataByteSize / sizeof(float) * sizeof(int16_t);
+ size_t bytes = std::min((size_t)sink->m_buffer->GetReadSize(), wanted);
+ for (unsigned int j = 0; j < bytes / sizeof(int16_t); j++)
+ {
+ for (unsigned int i = startIdx; i < endIdx; i++)
+ {
+ int16_t src = 0;
+ sink->m_buffer->Read((unsigned char *)&src, sizeof(int16_t), i);
+ if (i < outOutputData->mNumberBuffers && outOutputData->mBuffers[i].mData)
+ {
+ float *dest = (float *)outOutputData->mBuffers[i].mData;
+ dest[j] = src * mul;
+ }
+ }
+ }
+ LogLevel(bytes, wanted);
+ }
+ else
+ {
+ /* buffers appear to come from CA already zero'd, so just copy what is wanted */
+ unsigned int wanted = outOutputData->mBuffers[0].mDataByteSize;
+ unsigned int bytes = std::min(sink->m_buffer->GetReadSize(), wanted);
+ for (unsigned int i = startIdx; i < endIdx; i++)
+ {
+ if (i < outOutputData->mNumberBuffers && outOutputData->mBuffers[i].mData)
+ sink->m_buffer->Read((unsigned char *)outOutputData->mBuffers[i].mData, bytes, i);
+ else
+ sink->m_buffer->Read(NULL, bytes, i);
+ }
+ LogLevel(bytes, wanted);
+ }
+
+ // tell the sink we're good for more data
+ condVar.notifyAll();
+ }
+
+ sink->m_render_delay = (double)(inOutputTime->mHostTime - inNow->mHostTime) / CurrentHostFrequency();
+ sink->m_render_tick = inNow->mHostTime;
+ sink->m_render_locker.leave();
+ return noErr;
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkDARWINOSX.h b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINOSX.h
new file mode 100644
index 0000000..826ccf4
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINOSX.h
@@ -0,0 +1,62 @@
+/*
+ * 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/Sinks/osx/CoreAudioDevice.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+
+class AERingBuffer;
+struct AEDelayStatus;
+
+class CAESinkDARWINOSX : public IAESink
+{
+public:
+ const char* GetName() override { return "DARWINOSX"; }
+
+ CAESinkDARWINOSX();
+ ~CAESinkDARWINOSX() override;
+
+ 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;
+
+private:
+ static OSStatus renderCallback(AudioDeviceID inDevice, const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime, void* inClientData);
+ void SetHogMode(bool on);
+
+ CAEDeviceInfo m_info;
+
+ CCoreAudioDevice m_device;
+ CCoreAudioStream m_outputStream;
+ unsigned int m_latentFrames = 0;
+ unsigned int m_outputBufferIndex = 0;
+
+ bool m_outputBitstream =
+ false; ///< true if we're bistreaming into a LinearPCM stream rather than AC3 stream.
+ unsigned int m_planes = 1; ///< number of audio planes (1 if non-planar)
+ unsigned int m_frameSizePerPlane = 0; ///< frame size (per plane) in bytes
+ unsigned int m_framesPerSecond = 0; ///< sample rate
+
+ AERingBuffer* m_buffer = nullptr;
+ volatile bool m_started =
+ false; // set once we get a callback from CoreAudio, which can take a little while.
+
+ CAESpinSection m_render_locker;
+ volatile int64_t m_render_tick = 0;
+ volatile double m_render_delay = 0.0;
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.h b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.h
new file mode 100644
index 0000000..8a29dde
--- /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 = nullptr;
+#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 0000000..955e636
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.mm
@@ -0,0 +1,898 @@
+/*
+ * 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 "threads/SystemClock.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+
+#include "platform/darwin/DarwinUtils.h"
+
+#include <mutex>
+#include <sstream>
+
+#import <AVFoundation/AVAudioSession.h>
+#include <AudioToolbox/AudioToolbox.h>
+#include <unistd.h>
+
+using namespace std::chrono_literals;
+
+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(LOGINFO, "{} audio route = {}", __PRETTY_FUNCTION__, route.empty() ? "NONE" : route);
+
+ AVAudioSession* mySession = [AVAudioSession sharedInstance];
+
+ CLog::Log(LOGINFO, "{} sampleRate {:f}", __PRETTY_FUNCTION__, [mySession sampleRate]);
+ CLog::Log(LOGINFO, "{} outputLatency {:f}", __PRETTY_FUNCTION__, [mySession outputLatency]);
+ CLog::Log(LOGINFO, "{} IOBufferDuration {:f}", __PRETTY_FUNCTION__, [mySession IOBufferDuration]);
+ CLog::Log(LOGINFO, "{} outputNumberOfChannels {}", __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 Digital 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(LOGINFO, "{} maximumOutputNumberOfChannels {}", __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 {}", 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: {}", 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, "{} setPreferredOutputNumberOfChannels failed", __PRETTY_FUNCTION__);
+
+ // change the sameple rate
+ err = nil;
+ [mySession setPreferredSampleRate:samplerate error:&err];
+ if (err != nil)
+ CLog::Log(LOGWARNING, "{} setPreferredSampleRate failed", __PRETTY_FUNCTION__);
+
+ // change the i/o buffer duration
+ err = nil;
+ [mySession setPreferredIOBufferDuration:bufferseconds error:&err];
+ if (err != nil)
+ CLog::Log(LOGWARNING, "{} setPreferredIOBufferDuration failed", __PRETTY_FUNCTION__);
+
+ // reactivate the session
+ err = nil;
+ if (![mySession setActive:YES error:&err])
+ CLog::Log(LOGWARNING, "AVAudioSession setActive YES failed: {}", 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 {}, is {}", 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 = false;
+ AudioUnit m_audioUnit;
+ AudioStreamBasicDescription m_outputFormat;
+ AERingBuffer* m_buffer = nullptr;
+
+ 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_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 = {};
+ 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 += static_cast<double>(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
+ std::unique_lock<CCriticalSection> lock(mutex);
+ auto timeout = std::chrono::milliseconds(900 * frames / m_sampleRate);
+ if (!m_started)
+ timeout = 4500ms;
+
+ // we are using a timer here for being 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, "{} engine didn't start in {} ms!", __FUNCTION__, timeout.count());
+ 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;
+ auto timeout = std::chrono::milliseconds(static_cast<int>(buffertime()));
+
+ while (bytes && maxNumTimeouts > 0)
+ {
+ std::unique_lock<CCriticalSection> 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, "{} error creating audioUnit (error: {})", __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(LOGINFO, "{} setting channels {}", __PRETTY_FUNCTION__, channels);
+ CLog::Log(LOGINFO, "{} setting samplerate {:f}", __PRETTY_FUNCTION__, samplerate);
+ CLog::Log(LOGINFO, "{} setting buffer duration to {:f}", __PRETTY_FUNCTION__, bufferseconds);
+ setAVAudioSessionProperties(bufferseconds, samplerate, channels);
+
+ // Get the real output samplerate, the requested might not available
+ Float64 realisedSampleRate = [[AVAudioSession sharedInstance] sampleRate];
+ if (m_outputFormat.mSampleRate != realisedSampleRate)
+ {
+ CLog::Log(LOGINFO,
+ "{} couldn't set requested samplerate {}, AudioUnit will resample to {} 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, "{} error setting stream format on audioUnit (error: {})",
+ __PRETTY_FUNCTION__, static_cast<int>(status));
+ return false;
+ }
+
+ // Attach a render callback on the unit
+ AURenderCallbackStruct callbackStruct = {};
+ callbackStruct.inputProc = renderCallback;
+ callbackStruct.inputProcRefCon = this;
+ status = AudioUnitSetProperty(m_audioUnit, kAudioUnitProperty_SetRenderCallback,
+ kAudioUnitScope_Input, 0, &callbackStruct, sizeof(callbackStruct));
+ if (status != noErr)
+ {
+ CLog::Log(LOGERROR, "{} error setting render callback for AudioUnit (error: {})",
+ __PRETTY_FUNCTION__, static_cast<int>(status));
+ return false;
+ }
+
+ status = AudioUnitInitialize(m_audioUnit);
+ if (status != noErr)
+ {
+ CLog::Log(LOGERROR, "{} error initializing AudioUnit (error: {})", __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(LOGINFO, "{} total latency = {:f}", __PRETTY_FUNCTION__, m_totalLatency);
+
+ m_setup = true;
+ std::string formatString;
+ CLog::Log(LOGINFO, "{} setup audio format: {}", __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: {}flow ({} vs {} 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({})", device.m_deviceName);
+
+ list.push_back(device);
+}
+
+#pragma mark - AEDeviceInfoList
+/***************************************************************************************/
+/***************************************************************************************/
+AEDeviceInfoList CAESinkDARWINTVOS::m_devices;
+
+CAESinkDARWINTVOS::CAESinkDARWINTVOS()
+{
+}
+
+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 = {};
+ 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 Digital 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 Digital 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, "{}: AudioStreamBasicDescription: {} {}", __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/AudioEngine/Sinks/AESinkDirectSound.cpp b/xbmc/cores/AudioEngine/Sinks/AESinkDirectSound.cpp
new file mode 100644
index 0000000..653e5c4
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkDirectSound.cpp
@@ -0,0 +1,758 @@
+/*
+ * 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.
+ */
+
+#define INITGUID
+
+
+#include "AESinkDirectSound.h"
+
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Sinks/windows/AESinkFactoryWin.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "utils/StringUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include "platform/win32/CharsetConverter.h"
+
+#include <algorithm>
+#include <list>
+#include <mutex>
+
+#include <Audioclient.h>
+#include <Mmreg.h>
+#include <Rpc.h>
+#include <initguid.h>
+
+// include order is important here
+// clang-format off
+#include <mmdeviceapi.h>
+#include <Functiondiscoverykeys_devpkey.h>
+// clang-format on
+
+#pragma comment(lib, "Rpcrt4.lib")
+
+extern HWND g_hWnd;
+
+DEFINE_GUID( _KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, WAVE_FORMAT_IEEE_FLOAT, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 );
+DEFINE_GUID( _KSDATAFORMAT_SUBTYPE_DOLBY_AC3_SPDIF, WAVE_FORMAT_DOLBY_AC3_SPDIF, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 );
+
+extern const char *WASAPIErrToStr(HRESULT err);
+#define EXIT_ON_FAILURE(hr, reason) \
+ if (FAILED(hr)) \
+ { \
+ CLog::LogF(LOGERROR, reason " - HRESULT = {} ErrorMessage = {}", hr, WASAPIErrToStr(hr)); \
+ goto failed; \
+ }
+
+#define DS_SPEAKER_COUNT 8
+static const unsigned int DSChannelOrder[] = {SPEAKER_FRONT_LEFT, SPEAKER_FRONT_RIGHT, SPEAKER_FRONT_CENTER, SPEAKER_LOW_FREQUENCY, SPEAKER_BACK_LEFT, SPEAKER_BACK_RIGHT, SPEAKER_SIDE_LEFT, SPEAKER_SIDE_RIGHT};
+static const enum AEChannel AEChannelNamesDS[] = {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_BL, AE_CH_BR, AE_CH_SL, AE_CH_SR, AE_CH_NULL};
+
+using namespace Microsoft::WRL;
+
+struct DSDevice
+{
+ std::string name;
+ LPGUID lpGuid;
+};
+
+static BOOL CALLBACK DSEnumCallback(LPGUID lpGuid, LPCTSTR lpcstrDescription, LPCTSTR lpcstrModule, LPVOID lpContext)
+{
+ DSDevice dev;
+ std::list<DSDevice> &enumerator = *static_cast<std::list<DSDevice>*>(lpContext);
+
+ dev.name = KODI::PLATFORM::WINDOWS::FromW(lpcstrDescription);
+
+ dev.lpGuid = lpGuid;
+
+ if (lpGuid)
+ enumerator.push_back(dev);
+
+ return TRUE;
+}
+
+CAESinkDirectSound::CAESinkDirectSound() :
+ m_pBuffer (nullptr),
+ m_pDSound (nullptr),
+ m_encodedFormat (AE_FMT_INVALID),
+ m_AvgBytesPerSec(0 ),
+ m_dwChunkSize (0 ),
+ m_dwFrameSize (0 ),
+ m_dwBufferLen (0 ),
+ m_BufferOffset (0 ),
+ m_CacheLen (0 ),
+ m_BufferTimeouts(0 ),
+ m_running (false),
+ m_initialized (false),
+ m_isDirtyDS (false)
+{
+ m_channelLayout.Reset();
+}
+
+CAESinkDirectSound::~CAESinkDirectSound()
+{
+ Deinitialize();
+}
+
+void CAESinkDirectSound::Register()
+{
+ AE::AESinkRegEntry reg;
+ reg.sinkName = "DIRECTSOUND";
+ reg.createFunc = CAESinkDirectSound::Create;
+ reg.enumerateFunc = CAESinkDirectSound::EnumerateDevicesEx;
+ AE::CAESinkFactory::RegisterSink(reg);
+}
+
+IAESink* CAESinkDirectSound::Create(std::string &device, AEAudioFormat &desiredFormat)
+{
+ IAESink *sink = new CAESinkDirectSound();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+
+bool CAESinkDirectSound::Initialize(AEAudioFormat &format, std::string &device)
+{
+ if (m_initialized)
+ return false;
+
+ LPGUID deviceGUID = nullptr;
+ RPC_WSTR wszUuid = nullptr;
+ HRESULT hr = E_FAIL;
+ std::string strDeviceGUID = device;
+ std::list<DSDevice> DSDeviceList;
+ std::string deviceFriendlyName;
+ DirectSoundEnumerate(DSEnumCallback, &DSDeviceList);
+
+ if(StringUtils::EndsWithNoCase(device, std::string("default")))
+ strDeviceGUID = GetDefaultDevice();
+
+ for (std::list<DSDevice>::iterator itt = DSDeviceList.begin(); itt != DSDeviceList.end(); ++itt)
+ {
+ if ((*itt).lpGuid)
+ {
+ hr = (UuidToString((*itt).lpGuid, &wszUuid));
+ std::string sztmp = KODI::PLATFORM::WINDOWS::FromW(reinterpret_cast<wchar_t*>(wszUuid));
+ std::string szGUID = "{" + std::string(sztmp.begin(), sztmp.end()) + "}";
+ if (StringUtils::CompareNoCase(szGUID, strDeviceGUID) == 0)
+ {
+ deviceGUID = (*itt).lpGuid;
+ deviceFriendlyName = (*itt).name.c_str();
+ break;
+ }
+ }
+ if (hr == RPC_S_OK) RpcStringFree(&wszUuid);
+ }
+
+ hr = DirectSoundCreate(deviceGUID, m_pDSound.ReleaseAndGetAddressOf(), nullptr);
+
+ if (FAILED(hr))
+ {
+ CLog::LogF(
+ LOGERROR,
+ "Failed to create the DirectSound device {} with error {}, trying the default device.",
+ deviceFriendlyName, dserr2str(hr));
+
+ hr = DirectSoundCreate(nullptr, m_pDSound.ReleaseAndGetAddressOf(), nullptr);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Failed to create the default DirectSound device with error {}.",
+ dserr2str(hr));
+ return false;
+ }
+ }
+
+ /* Dodge the null handle on first init by using desktop handle */
+ HWND tmp_hWnd = g_hWnd == nullptr ? GetDesktopWindow() : g_hWnd;
+ CLog::LogF(LOGDEBUG, "Using Window handle: {}", fmt::ptr(tmp_hWnd));
+
+ hr = m_pDSound->SetCooperativeLevel(tmp_hWnd, DSSCL_PRIORITY);
+
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Failed to create the DirectSound device cooperative level.");
+ CLog::LogF(LOGERROR, "DSErr: {}", dserr2str(hr));
+ m_pDSound = nullptr;
+ return false;
+ }
+
+ // clamp samplerate between 44100 and 192000
+ if (format.m_sampleRate < 44100)
+ format.m_sampleRate = 44100;
+
+ if (format.m_sampleRate > 192000)
+ format.m_sampleRate = 192000;
+
+ // fill waveformatex
+ WAVEFORMATEXTENSIBLE wfxex = {};
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE)-sizeof(WAVEFORMATEX);
+ wfxex.Format.nChannels = format.m_channelLayout.Count();
+ wfxex.Format.nSamplesPerSec = format.m_sampleRate;
+ if (format.m_dataFormat == AE_FMT_RAW)
+ {
+ wfxex.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
+ wfxex.Format.wFormatTag = WAVE_FORMAT_DOLBY_AC3_SPDIF;
+ wfxex.SubFormat = _KSDATAFORMAT_SUBTYPE_DOLBY_AC3_SPDIF;
+ wfxex.Format.wBitsPerSample = 16;
+ wfxex.Format.nChannels = 2;
+ }
+ else
+ {
+ wfxex.dwChannelMask = SpeakerMaskFromAEChannels(format.m_channelLayout);
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.SubFormat = _KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+ wfxex.Format.wBitsPerSample = 32;
+ }
+
+ wfxex.Samples.wValidBitsPerSample = wfxex.Format.wBitsPerSample;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+
+ m_AvgBytesPerSec = wfxex.Format.nAvgBytesPerSec;
+
+ unsigned int uiFrameCount = (int)(format.m_sampleRate * 0.015); //default to 15ms chunks
+ m_dwFrameSize = wfxex.Format.nBlockAlign;
+ m_dwChunkSize = m_dwFrameSize * uiFrameCount;
+ m_dwBufferLen = m_dwChunkSize * 12; //180ms total buffer
+
+ // fill in the secondary sound buffer descriptor
+ DSBUFFERDESC dsbdesc = {};
+ dsbdesc.dwSize = sizeof(DSBUFFERDESC);
+ dsbdesc.dwFlags = DSBCAPS_GETCURRENTPOSITION2 /** Better position accuracy */
+ | DSBCAPS_TRUEPLAYPOSITION /** Vista+ accurate position */
+ | DSBCAPS_GLOBALFOCUS; /** Allows background playing */
+
+ dsbdesc.dwBufferBytes = m_dwBufferLen;
+ dsbdesc.lpwfxFormat = (WAVEFORMATEX *)&wfxex;
+
+ // now create the stream buffer
+ HRESULT res = m_pDSound->CreateSoundBuffer(&dsbdesc, m_pBuffer.ReleaseAndGetAddressOf(), nullptr);
+ if (res != DS_OK)
+ {
+ if (dsbdesc.dwFlags & DSBCAPS_LOCHARDWARE)
+ {
+ CLog::LogF(LOGDEBUG, "Couldn't create secondary buffer ({}). Trying without LOCHARDWARE.",
+ dserr2str(res));
+ // Try without DSBCAPS_LOCHARDWARE
+ dsbdesc.dwFlags &= ~DSBCAPS_LOCHARDWARE;
+ res = m_pDSound->CreateSoundBuffer(&dsbdesc, m_pBuffer.ReleaseAndGetAddressOf(), nullptr);
+ }
+ if (res != DS_OK)
+ {
+ m_pBuffer = nullptr;
+ CLog::LogF(LOGERROR, "cannot create secondary buffer ({})", dserr2str(res));
+ return false;
+ }
+ }
+ CLog::LogF(LOGDEBUG, "secondary buffer created");
+
+ m_pBuffer->Stop();
+
+ AEChannelsFromSpeakerMask(wfxex.dwChannelMask);
+ format.m_channelLayout = m_channelLayout;
+ m_encodedFormat = format.m_dataFormat;
+ format.m_frames = uiFrameCount;
+ format.m_frameSize = ((format.m_dataFormat == AE_FMT_RAW) ? (wfxex.Format.wBitsPerSample >> 3) : sizeof(float)) * format.m_channelLayout.Count();
+ format.m_dataFormat = (format.m_dataFormat == AE_FMT_RAW) ? AE_FMT_S16NE : AE_FMT_FLOAT;
+
+ m_format = format;
+ m_device = device;
+
+ m_BufferOffset = 0;
+ m_CacheLen = 0;
+ m_initialized = true;
+ m_isDirtyDS = false;
+
+ CLog::LogF(LOGDEBUG, "Initializing DirectSound with the following parameters:");
+ CLog::Log(LOGDEBUG, " Audio Device : {}", ((std::string)deviceFriendlyName));
+ CLog::Log(LOGDEBUG, " Sample Rate : {}", wfxex.Format.nSamplesPerSec);
+ CLog::Log(LOGDEBUG, " Sample Format : {}", CAEUtil::DataFormatToStr(format.m_dataFormat));
+ CLog::Log(LOGDEBUG, " Bits Per Sample : {}", wfxex.Format.wBitsPerSample);
+ CLog::Log(LOGDEBUG, " Valid Bits/Samp : {}", wfxex.Samples.wValidBitsPerSample);
+ CLog::Log(LOGDEBUG, " Channel Count : {}", wfxex.Format.nChannels);
+ CLog::Log(LOGDEBUG, " Block Align : {}", wfxex.Format.nBlockAlign);
+ CLog::Log(LOGDEBUG, " Avg. Bytes Sec : {}", wfxex.Format.nAvgBytesPerSec);
+ CLog::Log(LOGDEBUG, " Samples/Block : {}", wfxex.Samples.wSamplesPerBlock);
+ CLog::Log(LOGDEBUG, " Format cBSize : {}", wfxex.Format.cbSize);
+ CLog::Log(LOGDEBUG, " Channel Layout : {}", ((std::string)format.m_channelLayout));
+ CLog::Log(LOGDEBUG, " Channel Mask : {}", wfxex.dwChannelMask);
+ CLog::Log(LOGDEBUG, " Frames : {}", format.m_frames);
+ CLog::Log(LOGDEBUG, " Frame Size : {}", format.m_frameSize);
+
+ return true;
+}
+
+void CAESinkDirectSound::Deinitialize()
+{
+ if (!m_initialized)
+ return;
+
+ CLog::LogF(LOGDEBUG, "Cleaning up");
+
+ if (m_pBuffer)
+ {
+ m_pBuffer->Stop();
+ }
+
+ m_initialized = false;
+ m_pBuffer = nullptr;
+ m_pDSound = nullptr;
+ m_BufferOffset = 0;
+ m_CacheLen = 0;
+ m_dwChunkSize = 0;
+ m_dwBufferLen = 0;
+}
+
+unsigned int CAESinkDirectSound::AddPackets(uint8_t **data, unsigned int frames, unsigned int offset)
+{
+ if (!m_initialized)
+ return 0;
+
+ DWORD total = m_dwFrameSize * frames;
+ DWORD len = total;
+ unsigned char* pBuffer = (unsigned char*)data[0]+offset*m_format.m_frameSize;
+
+ DWORD bufferStatus = 0;
+ if (m_pBuffer->GetStatus(&bufferStatus) != DS_OK)
+ {
+ CLog::LogF(LOGERROR, "GetStatus() failed");
+ return 0;
+ }
+ if (bufferStatus & DSBSTATUS_BUFFERLOST)
+ {
+ CLog::LogF(LOGDEBUG, "Buffer allocation was lost. Restoring buffer.");
+ m_pBuffer->Restore();
+ }
+
+ while (GetSpace() < total)
+ {
+ if(m_isDirtyDS)
+ return INT_MAX;
+ else
+ {
+ KODI::TIME::Sleep(
+ std::chrono::milliseconds(static_cast<int>(total * 1000 / m_AvgBytesPerSec)));
+ }
+ }
+
+ while (len)
+ {
+ void* start = nullptr, *startWrap = nullptr;
+ DWORD size = 0, sizeWrap = 0;
+ if (m_BufferOffset >= m_dwBufferLen) // Wrap-around manually
+ m_BufferOffset = 0;
+ DWORD dwWriteBytes = std::min((int)m_dwChunkSize, (int)len);
+ HRESULT res = m_pBuffer->Lock(m_BufferOffset, dwWriteBytes, &start, &size, &startWrap, &sizeWrap, 0);
+ if (DS_OK != res)
+ {
+ CLog::LogF(LOGERROR, "Unable to lock buffer at offset {}. HRESULT: {:#08x}", m_BufferOffset,
+ res);
+ m_isDirtyDS = true;
+ return INT_MAX;
+ }
+
+ memcpy(start, pBuffer, size);
+
+ pBuffer += size;
+ len -= size;
+
+ m_BufferOffset += size;
+ if (startWrap) // Write-region wraps to beginning of buffer
+ {
+ memcpy(startWrap, pBuffer, sizeWrap);
+ m_BufferOffset = sizeWrap;
+
+ pBuffer += sizeWrap;
+ len -= sizeWrap;
+ }
+
+ m_CacheLen += size + sizeWrap; // This data is now in the cache
+ m_pBuffer->Unlock(start, size, startWrap, sizeWrap);
+ }
+
+ CheckPlayStatus();
+
+ return (total - len) / m_dwFrameSize; // Frames used
+}
+
+void CAESinkDirectSound::Stop()
+{
+ if (m_pBuffer)
+ m_pBuffer->Stop();
+}
+
+void CAESinkDirectSound::Drain()
+{
+ if (!m_initialized || m_isDirtyDS)
+ return;
+
+ m_pBuffer->Stop();
+ HRESULT res = m_pBuffer->SetCurrentPosition(0);
+ if (DS_OK != res)
+ {
+ CLog::LogF(LOGERROR,
+ "SetCurrentPosition failed. Unable to determine buffer status. HRESULT = {:#08x}",
+ res);
+ m_isDirtyDS = true;
+ return;
+ }
+ m_BufferOffset = 0;
+ UpdateCacheStatus();
+}
+
+void CAESinkDirectSound::GetDelay(AEDelayStatus& status)
+{
+ if (!m_initialized)
+ {
+ status.SetDelay(0);
+ return;
+ }
+
+ /* Make sure we know how much data is in the cache */
+ if (!UpdateCacheStatus())
+ m_isDirtyDS = true;
+
+ /** returns current cached data duration in seconds */
+ status.SetDelay((double)m_CacheLen / (double)m_AvgBytesPerSec);
+}
+
+double CAESinkDirectSound::GetCacheTotal()
+{
+ /** returns total cache capacity in seconds */
+ return (double)m_dwBufferLen / (double)m_AvgBytesPerSec;
+}
+
+void CAESinkDirectSound::EnumerateDevicesEx(AEDeviceInfoList &deviceInfoList, bool force)
+{
+ CAEDeviceInfo deviceInfo;
+
+ ComPtr<IMMDeviceEnumerator> pEnumerator = nullptr;
+ ComPtr<IMMDeviceCollection> pEnumDevices = nullptr;
+ UINT uiCount = 0;
+
+ HRESULT hr;
+
+ std::string strDD = GetDefaultDevice();
+
+ /* Windows Vista or later - supporting WASAPI device probing */
+ hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, reinterpret_cast<void**>(pEnumerator.GetAddressOf()));
+ EXIT_ON_FAILURE(hr, "Could not allocate WASAPI device enumerator.")
+
+ hr = pEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, pEnumDevices.GetAddressOf());
+ EXIT_ON_FAILURE(hr, "Retrieval of audio endpoint enumeration failed.")
+
+ hr = pEnumDevices->GetCount(&uiCount);
+ EXIT_ON_FAILURE(hr, "Retrieval of audio endpoint count failed.")
+
+ for (UINT i = 0; i < uiCount; i++)
+ {
+ ComPtr<IMMDevice> pDevice = nullptr;
+ ComPtr<IPropertyStore> pProperty = nullptr;
+ PROPVARIANT varName;
+ PropVariantInit(&varName);
+
+ deviceInfo.m_channels.Reset();
+ deviceInfo.m_dataFormats.clear();
+ deviceInfo.m_sampleRates.clear();
+
+ hr = pEnumDevices->Item(i, pDevice.GetAddressOf());
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Retrieval of DirectSound endpoint failed.");
+ goto failed;
+ }
+
+ hr = pDevice->OpenPropertyStore(STGM_READ, pProperty.GetAddressOf());
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Retrieval of DirectSound endpoint properties failed.");
+ goto failed;
+ }
+
+ hr = pProperty->GetValue(PKEY_Device_FriendlyName, &varName);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Retrieval of DirectSound endpoint device name failed.");
+ goto failed;
+ }
+
+ std::string strFriendlyName = KODI::PLATFORM::WINDOWS::FromW(varName.pwszVal);
+ PropVariantClear(&varName);
+
+ hr = pProperty->GetValue(PKEY_AudioEndpoint_GUID, &varName);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Retrieval of DirectSound endpoint GUID failed.");
+ goto failed;
+ }
+
+ std::string strDevName = KODI::PLATFORM::WINDOWS::FromW(varName.pwszVal);
+ PropVariantClear(&varName);
+
+ hr = pProperty->GetValue(PKEY_AudioEndpoint_FormFactor, &varName);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Retrieval of DirectSound endpoint form factor failed.");
+ goto failed;
+ }
+ std::string strWinDevType = winEndpoints[(EndpointFormFactor)varName.uiVal].winEndpointType;
+ AEDeviceType aeDeviceType = winEndpoints[(EndpointFormFactor)varName.uiVal].aeDeviceType;
+
+ PropVariantClear(&varName);
+
+ /* In shared mode Windows tells us what format the audio must be in. */
+ ComPtr<IAudioClient> pClient;
+ hr = pDevice->Activate(IID_IAudioClient, CLSCTX_ALL, nullptr, reinterpret_cast<void**>(pClient.GetAddressOf()));
+ EXIT_ON_FAILURE(hr, "Activate device failed.")
+
+ //hr = pClient->GetMixFormat(&pwfxex);
+ hr = pProperty->GetValue(PKEY_AudioEngine_DeviceFormat, &varName);
+ if (SUCCEEDED(hr) && varName.blob.cbSize > 0)
+ {
+ WAVEFORMATEX* smpwfxex = (WAVEFORMATEX*)varName.blob.pBlobData;
+ deviceInfo.m_channels = layoutsByChCount[std::max(std::min(smpwfxex->nChannels, (WORD) DS_SPEAKER_COUNT), (WORD) 2)];
+ deviceInfo.m_dataFormats.push_back(AEDataFormat(AE_FMT_FLOAT));
+ if (aeDeviceType != AE_DEVTYPE_PCM)
+ {
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ // DTS is played with the same infrastructure as AC3
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ // signal that we can doe AE_FMT_RAW
+ deviceInfo.m_dataFormats.push_back(AE_FMT_RAW);
+ }
+ deviceInfo.m_sampleRates.push_back(std::min(smpwfxex->nSamplesPerSec, (DWORD) 192000));
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Getting DeviceFormat failed ({})", WASAPIErrToStr(hr));
+ }
+
+ deviceInfo.m_deviceName = strDevName;
+ deviceInfo.m_displayName = strWinDevType.append(strFriendlyName);
+ deviceInfo.m_displayNameExtra = std::string("DIRECTSOUND: ").append(strFriendlyName);
+ deviceInfo.m_deviceType = aeDeviceType;
+
+ deviceInfo.m_wantsIECPassthrough = true;
+ deviceInfoList.push_back(deviceInfo);
+
+ // add the default device with m_deviceName = default
+ if(strDD == strDevName)
+ {
+ deviceInfo.m_deviceName = std::string("default");
+ deviceInfo.m_displayName = std::string("default");
+ deviceInfo.m_displayNameExtra = std::string("");
+ deviceInfo.m_wantsIECPassthrough = true;
+ deviceInfoList.push_back(deviceInfo);
+ }
+ }
+
+ return;
+
+failed:
+
+ if (FAILED(hr))
+ CLog::LogF(LOGERROR, "Failed to enumerate WASAPI endpoint devices ({}).", WASAPIErrToStr(hr));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+void CAESinkDirectSound::CheckPlayStatus()
+{
+ DWORD status = 0;
+ if (m_pBuffer->GetStatus(&status) != DS_OK)
+ {
+ CLog::LogF(LOGERROR, "GetStatus() failed");
+ return;
+ }
+
+ if (!(status & DSBSTATUS_PLAYING) && m_CacheLen != 0) // If we have some data, see if we can start playback
+ {
+ HRESULT hr = m_pBuffer->Play(0, 0, DSBPLAY_LOOPING);
+ CLog::LogF(LOGDEBUG, "Resuming Playback");
+ if (FAILED(hr))
+ CLog::LogF(LOGERROR, "Failed to play the DirectSound buffer: {}", dserr2str(hr));
+ }
+}
+
+bool CAESinkDirectSound::UpdateCacheStatus()
+{
+ std::unique_lock<CCriticalSection> lock(m_runLock);
+
+ DWORD playCursor = 0, writeCursor = 0;
+ HRESULT res = m_pBuffer->GetCurrentPosition(&playCursor, &writeCursor); // Get the current playback and safe write positions
+ if (DS_OK != res)
+ {
+ CLog::LogF(LOGERROR,
+ "GetCurrentPosition failed. Unable to determine buffer status. HRESULT = {:#08x}",
+ res);
+ m_isDirtyDS = true;
+ return false;
+ }
+
+ // Check the state of the ring buffer (P->O->W == underrun)
+ // These are the logical situations that can occur
+ // O: CurrentOffset W: WriteCursor P: PlayCursor
+ // | | | | | | | | | |
+ // ***O----W----P***** < underrun P > W && O < W (1)
+ // | | | | | | | | | |
+ // ---P****O----W----- < underrun O > P && O < W (2)
+ // | | | | | | | | | |
+ // ---W----P****O----- < underrun P > W && P < O (3)
+ // | | | | | | | | | |
+ // ***W****O----P***** P > W && P > O (4)
+ // | | | | | | | | | |
+ // ---P****W****O----- P < W && O > W (5)
+ // | | | | | | | | | |
+ // ***O----P****W***** P < W && O < P (6)
+
+ // Check for underruns
+ if ((playCursor > writeCursor && m_BufferOffset < writeCursor) || // (1)
+ (playCursor < m_BufferOffset && m_BufferOffset < writeCursor) || // (2)
+ (playCursor > writeCursor && playCursor < m_BufferOffset)) // (3)
+ {
+ CLog::Log(LOGWARNING, "CWin32DirectSound::GetSpace - buffer underrun - W:{}, P:{}, O:{}.",
+ writeCursor, playCursor, m_BufferOffset);
+ m_BufferOffset = writeCursor; // Catch up
+ //m_pBuffer->Stop(); // Wait until someone gives us some data to restart playback (prevents glitches)
+ m_BufferTimeouts++;
+ if (m_BufferTimeouts > 10)
+ {
+ m_isDirtyDS = true;
+ return false;
+ }
+ }
+ else
+ m_BufferTimeouts = 0;
+
+ // Calculate available space in the ring buffer
+ if (playCursor == m_BufferOffset && m_BufferOffset == writeCursor) // Playback is stopped and we are all at the same place
+ m_CacheLen = 0;
+ else if (m_BufferOffset > playCursor)
+ m_CacheLen = m_BufferOffset - playCursor;
+ else
+ m_CacheLen = m_dwBufferLen - (playCursor - m_BufferOffset);
+
+ return true;
+}
+
+unsigned int CAESinkDirectSound::GetSpace()
+{
+ std::unique_lock<CCriticalSection> lock(m_runLock);
+ if (!UpdateCacheStatus())
+ m_isDirtyDS = true;
+ unsigned int space = m_dwBufferLen - m_CacheLen;
+
+ // We can never allow the internal buffers to fill up complete
+ // as we get confused between if the buffer is full or empty
+ // so never allow the last chunk to be added
+ if (space > m_dwChunkSize)
+ return space - m_dwChunkSize;
+ else
+ return 0;
+}
+
+void CAESinkDirectSound::AEChannelsFromSpeakerMask(DWORD speakers)
+{
+ m_channelLayout.Reset();
+
+ for (int i = 0; i < DS_SPEAKER_COUNT; i++)
+ {
+ if (speakers & DSChannelOrder[i])
+ m_channelLayout += AEChannelNamesDS[i];
+ }
+}
+
+DWORD CAESinkDirectSound::SpeakerMaskFromAEChannels(const CAEChannelInfo &channels)
+{
+ DWORD mask = 0;
+
+ for (unsigned int i = 0; i < channels.Count(); i++)
+ {
+ for (unsigned int j = 0; j < DS_SPEAKER_COUNT; j++)
+ if (channels[i] == AEChannelNamesDS[j])
+ mask |= DSChannelOrder[j];
+ }
+
+ return mask;
+}
+
+const char *CAESinkDirectSound::dserr2str(int err)
+{
+ switch (err)
+ {
+ case DS_OK: return "DS_OK";
+ case DS_NO_VIRTUALIZATION: return "DS_NO_VIRTUALIZATION";
+ case DSERR_ALLOCATED: return "DS_NO_VIRTUALIZATION";
+ case DSERR_CONTROLUNAVAIL: return "DSERR_CONTROLUNAVAIL";
+ case DSERR_INVALIDPARAM: return "DSERR_INVALIDPARAM";
+ case DSERR_INVALIDCALL: return "DSERR_INVALIDCALL";
+ case DSERR_GENERIC: return "DSERR_GENERIC";
+ case DSERR_PRIOLEVELNEEDED: return "DSERR_PRIOLEVELNEEDED";
+ case DSERR_OUTOFMEMORY: return "DSERR_OUTOFMEMORY";
+ case DSERR_BADFORMAT: return "DSERR_BADFORMAT";
+ case DSERR_UNSUPPORTED: return "DSERR_UNSUPPORTED";
+ case DSERR_NODRIVER: return "DSERR_NODRIVER";
+ case DSERR_ALREADYINITIALIZED: return "DSERR_ALREADYINITIALIZED";
+ case DSERR_NOAGGREGATION: return "DSERR_NOAGGREGATION";
+ case DSERR_BUFFERLOST: return "DSERR_BUFFERLOST";
+ case DSERR_OTHERAPPHASPRIO: return "DSERR_OTHERAPPHASPRIO";
+ case DSERR_UNINITIALIZED: return "DSERR_UNINITIALIZED";
+ case DSERR_NOINTERFACE: return "DSERR_NOINTERFACE";
+ case DSERR_ACCESSDENIED: return "DSERR_ACCESSDENIED";
+ case DSERR_BUFFERTOOSMALL: return "DSERR_BUFFERTOOSMALL";
+ case DSERR_DS8_REQUIRED: return "DSERR_DS8_REQUIRED";
+ case DSERR_SENDLOOP: return "DSERR_SENDLOOP";
+ case DSERR_BADSENDBUFFERGUID: return "DSERR_BADSENDBUFFERGUID";
+ case DSERR_OBJECTNOTFOUND: return "DSERR_OBJECTNOTFOUND";
+ case DSERR_FXUNAVAILABLE: return "DSERR_FXUNAVAILABLE";
+ default: return "unknown";
+ }
+}
+
+std::string CAESinkDirectSound::GetDefaultDevice()
+{
+ HRESULT hr;
+ ComPtr<IMMDeviceEnumerator> pEnumerator = nullptr;
+ ComPtr<IMMDevice> pDevice = nullptr;
+ ComPtr<IPropertyStore> pProperty = nullptr;
+ PROPVARIANT varName;
+ std::string strDevName = "default";
+ AEDeviceType aeDeviceType;
+
+ hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, reinterpret_cast<void**>(pEnumerator.GetAddressOf()));
+ EXIT_ON_FAILURE(hr, "Could not allocate WASAPI device enumerator")
+
+ hr = pEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, pDevice.GetAddressOf());
+ EXIT_ON_FAILURE(hr, "Retrieval of audio endpoint enumeration failed.")
+
+ hr = pDevice->OpenPropertyStore(STGM_READ, pProperty.GetAddressOf());
+ EXIT_ON_FAILURE(hr, "Retrieval of DirectSound endpoint properties failed.")
+
+ PropVariantInit(&varName);
+ hr = pProperty->GetValue(PKEY_AudioEndpoint_FormFactor, &varName);
+ EXIT_ON_FAILURE(hr, "Retrieval of DirectSound endpoint form factor failed.")
+
+ aeDeviceType = winEndpoints[(EndpointFormFactor)varName.uiVal].aeDeviceType;
+ PropVariantClear(&varName);
+
+ hr = pProperty->GetValue(PKEY_AudioEndpoint_GUID, &varName);
+ EXIT_ON_FAILURE(hr, "Retrieval of DirectSound endpoint GUID failed")
+
+ strDevName = KODI::PLATFORM::WINDOWS::FromW(varName.pwszVal);
+ PropVariantClear(&varName);
+
+failed:
+
+ return strDevName;
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkDirectSound.h b/xbmc/cores/AudioEngine/Sinks/AESinkDirectSound.h
new file mode 100644
index 0000000..6eabca4
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkDirectSound.h
@@ -0,0 +1,73 @@
+/*
+ * 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 "cores/AudioEngine/Interfaces/AESink.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+#include "threads/CriticalSection.h"
+
+#include <stdint.h>
+
+#include <mmsystem.h> /* Microsoft can't write standalone headers */
+#include <DSound.h> /* Microsoft can't write standalone headers */
+#include <wrl/client.h>
+
+class CAESinkDirectSound : public IAESink
+{
+public:
+ virtual const char *GetName() { return "DIRECTSOUND"; }
+
+ CAESinkDirectSound();
+ virtual ~CAESinkDirectSound();
+
+ static void Register();
+ static IAESink* Create(std::string &device, AEAudioFormat &desiredFormat);
+
+ virtual bool Initialize(AEAudioFormat &format, std::string &device);
+ virtual void Deinitialize();
+
+ virtual void Stop();
+ virtual void Drain();
+ virtual void GetDelay(AEDelayStatus& status);
+ virtual double GetCacheTotal();
+ virtual unsigned int AddPackets(uint8_t **data, unsigned int frames, unsigned int offset);
+
+ static std::string GetDefaultDevice();
+ static void EnumerateDevicesEx (AEDeviceInfoList &deviceInfoList, bool force = false);
+private:
+ void AEChannelsFromSpeakerMask(DWORD speakers);
+ DWORD SpeakerMaskFromAEChannels(const CAEChannelInfo &channels);
+ void CheckPlayStatus();
+ bool UpdateCacheStatus();
+ unsigned int GetSpace();
+ const char *dserr2str(int err);
+
+ Microsoft::WRL::ComPtr<IDirectSoundBuffer> m_pBuffer;
+ Microsoft::WRL::ComPtr<IDirectSound> m_pDSound;
+
+ AEAudioFormat m_format;
+ enum AEDataFormat m_encodedFormat;
+ CAEChannelInfo m_channelLayout;
+ std::string m_device;
+
+ unsigned int m_AvgBytesPerSec;
+
+ unsigned int m_dwChunkSize;
+ unsigned int m_dwFrameSize;
+ unsigned int m_dwBufferLen;
+
+ unsigned int m_BufferOffset;
+ unsigned int m_CacheLen;
+ unsigned int m_BufferTimeouts;
+
+ bool m_running;
+ bool m_initialized;
+ bool m_isDirtyDS;
+ CCriticalSection m_runLock;
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkOSS.cpp b/xbmc/cores/AudioEngine/Sinks/AESinkOSS.cpp
new file mode 100644
index 0000000..b319317
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkOSS.cpp
@@ -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.
+ */
+
+#include "AESinkOSS.h"
+#include <stdint.h>
+#include <limits.h>
+#include <unistd.h>
+
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "utils/log.h"
+#include "threads/SingleLock.h"
+#include <sstream>
+
+#include <sys/ioctl.h>
+#include <sys/fcntl.h>
+
+#if defined(OSS4) || defined(TARGET_FREEBSD)
+ #include <sys/soundcard.h>
+#else
+ #include <linux/soundcard.h>
+#endif
+
+#define OSS_FRAMES 256
+
+static enum AEChannel OSSChannelMap[9] =
+ {AE_CH_FL, AE_CH_FR, AE_CH_BL, AE_CH_BR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_NULL};
+
+#if defined(SNDCTL_SYSINFO) && defined(SNDCTL_CARDINFO)
+static int OSSSampleRateList[] =
+{
+ 5512,
+ 8000,
+ 11025,
+ 16000,
+ 22050,
+ 32000,
+ 44100,
+ 48000,
+ 64000,
+ 88200,
+ 96000,
+ 176400,
+ 192000,
+ 384000,
+ 0
+};
+#endif
+
+CAESinkOSS::CAESinkOSS()
+{
+ m_fd = 0;
+}
+
+CAESinkOSS::~CAESinkOSS()
+{
+ Deinitialize();
+}
+
+void CAESinkOSS::Register()
+{
+ AE::AESinkRegEntry entry;
+ entry.sinkName = "OSS";
+ entry.createFunc = CAESinkOSS::Create;
+ entry.enumerateFunc = CAESinkOSS::EnumerateDevicesEx;
+ AE::CAESinkFactory::RegisterSink(entry);
+}
+
+IAESink* CAESinkOSS::Create(std::string &device, AEAudioFormat& desiredFormat)
+{
+ IAESink* sink = new CAESinkOSS();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+
+std::string CAESinkOSS::GetDeviceUse(const AEAudioFormat& format, const std::string &device)
+{
+#ifdef OSS4
+ if (AE_IS_RAW(format.m_dataFormat))
+ {
+ if (device.find_first_of('/') != 0)
+ return "/dev/dsp_ac3";
+ return device;
+ }
+
+ if (device.find_first_of('/') != 0)
+ return "/dev/dsp_multich";
+#else
+ if (device.find_first_of('/') != 0)
+ return "/dev/dsp";
+#endif
+
+ return device;
+}
+
+bool CAESinkOSS::Initialize(AEAudioFormat &format, std::string &device)
+{
+ m_initFormat = format;
+ format.m_channelLayout = GetChannelLayout(format);
+ device = GetDeviceUse(format, device);
+
+#ifdef __linux__
+ /* try to open in exclusive mode first (no software mixing) */
+ m_fd = open(device.c_str(), O_WRONLY | O_EXCL, 0);
+ if (m_fd == -1)
+#endif
+ m_fd = open(device.c_str(), O_WRONLY, 0);
+ if (m_fd == -1)
+ {
+ CLog::Log(LOGERROR, "CAESinkOSS::Initialize - Failed to open the audio device: {}", device);
+ return false;
+ }
+
+ int format_mask;
+ if (ioctl(m_fd, SNDCTL_DSP_GETFMTS, &format_mask) == -1)
+ {
+ close(m_fd);
+ CLog::Log(LOGERROR, "CAESinkOSS::Initialize - Failed to get supported formats, assuming AFMT_S16_NE");
+ return false;
+ }
+
+#ifdef OSS4
+ bool useCooked = true;
+#endif
+
+ int oss_fmt = 0;
+#ifdef AFMT_FLOAT
+ if ((format.m_dataFormat == AE_FMT_FLOAT) && (format_mask & AFMT_FLOAT ))
+ oss_fmt = AFMT_FLOAT;
+ else
+#endif
+#ifdef AFMT_S32_NE
+ if ((format.m_dataFormat == AE_FMT_S32NE) && (format_mask & AFMT_S32_NE))
+ oss_fmt = AFMT_S32_NE;
+ else if ((format.m_dataFormat == AE_FMT_S32BE) && (format_mask & AFMT_S32_BE))
+ oss_fmt = AFMT_S32_BE;
+ else if ((format.m_dataFormat == AE_FMT_S32LE) && (format_mask & AFMT_S32_LE))
+ oss_fmt = AFMT_S32_LE;
+ else
+#endif
+ if ((format.m_dataFormat == AE_FMT_S16NE) && (format_mask & AFMT_S16_NE))
+ oss_fmt = AFMT_S16_NE;
+ else if ((format.m_dataFormat == AE_FMT_S16BE) && (format_mask & AFMT_S16_BE))
+ oss_fmt = AFMT_S16_BE;
+ else if ((format.m_dataFormat == AE_FMT_S16LE) && (format_mask & AFMT_S16_LE))
+ oss_fmt = AFMT_S16_LE;
+ else if ((format.m_dataFormat == AE_FMT_U8 ) && (format_mask & AFMT_U8 ))
+ oss_fmt = AFMT_U8;
+ else if (((format.m_dataFormat == AE_FMT_RAW) ) && (format_mask & AFMT_AC3 ))
+ {
+ oss_fmt = AFMT_AC3;
+ format.m_dataFormat = AE_FMT_S16NE;
+ }
+ else if (format.m_dataFormat == AE_FMT_RAW)
+ {
+ close(m_fd);
+ CLog::Log(LOGERROR, "CAESinkOSS::Initialize - Failed to find a suitable RAW output format");
+ return false;
+ }
+ else
+ {
+ CLog::Log(LOGINFO,
+ "CAESinkOSS::Initialize - Your hardware does not support {}, trying other formats",
+ CAEUtil::DataFormatToStr(format.m_dataFormat));
+
+ /* fallback to the best supported format */
+#ifdef AFMT_FLOAT
+ if (format_mask & AFMT_FLOAT )
+ {
+ oss_fmt = AFMT_FLOAT;
+ format.m_dataFormat = AE_FMT_FLOAT;
+ }
+ else
+#endif
+#ifdef AFMT_S32_NE
+ if (format_mask & AFMT_S32_NE)
+ {
+ oss_fmt = AFMT_S32_NE;
+ format.m_dataFormat = AE_FMT_S32NE;
+ }
+ else if (format_mask & AFMT_S32_BE)
+ {
+ oss_fmt = AFMT_S32_BE;
+ format.m_dataFormat = AE_FMT_S32BE;
+ }
+ else if (format_mask & AFMT_S32_LE)
+ {
+ oss_fmt = AFMT_S32_LE;
+ format.m_dataFormat = AE_FMT_S32LE;
+ }
+ else
+#endif
+ if (format_mask & AFMT_S16_NE)
+ {
+ oss_fmt = AFMT_S16_NE;
+ format.m_dataFormat = AE_FMT_S16NE;
+ }
+ else if (format_mask & AFMT_S16_BE)
+ {
+ oss_fmt = AFMT_S16_BE;
+ format.m_dataFormat = AE_FMT_S16BE;
+ }
+ else if (format_mask & AFMT_S16_LE)
+ {
+ oss_fmt = AFMT_S16_LE;
+ format.m_dataFormat = AE_FMT_S16LE;
+ }
+ else if (format_mask & AFMT_U8 )
+ {
+ oss_fmt = AFMT_U8;
+ format.m_dataFormat = AE_FMT_U8;
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "CAESinkOSS::Initialize - Failed to find a suitable native output format, will try to use AE_FMT_S16NE anyway");
+ oss_fmt = AFMT_S16_NE;
+ format.m_dataFormat = AE_FMT_S16NE;
+#ifdef OSS4
+ /* dont use cooked if we did not find a native format, OSS might be able to convert */
+ useCooked = false;
+#endif
+ }
+ }
+
+#ifdef OSS4
+ if (useCooked)
+ {
+ int oss_cooked = 1;
+ if (ioctl(m_fd, SNDCTL_DSP_COOKEDMODE, &oss_cooked) == -1)
+ CLog::Log(LOGWARNING, "CAESinkOSS::Initialize - Failed to set cooked mode");
+ }
+#endif
+
+ if (ioctl(m_fd, SNDCTL_DSP_SETFMT, &oss_fmt) == -1)
+ {
+ close(m_fd);
+ CLog::Log(LOGERROR, "CAESinkOSS::Initialize - Failed to set the data format ({})",
+ CAEUtil::DataFormatToStr(format.m_dataFormat));
+ return false;
+ }
+
+ /* find the number we need to open to access the channels we need */
+ bool found = false;
+ int oss_ch = 0;
+ for (int ch = format.m_channelLayout.Count(); ch < 9; ++ch)
+ {
+ oss_ch = ch;
+ if (ioctl(m_fd, SNDCTL_DSP_CHANNELS, &oss_ch) != -1 && oss_ch >= (int)format.m_channelLayout.Count())
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ CLog::Log(LOGWARNING, "CAESinkOSS::Initialize - Failed to access the number of channels required, falling back");
+
+#if defined(TARGET_FREEBSD)
+ /* fix hdmi 8 channels order */
+ if ((oss_fmt != AFMT_AC3) && 8 == oss_ch)
+ {
+ unsigned long long order = 0x0000000087346521ULL;
+
+ if (ioctl(m_fd, SNDCTL_DSP_SET_CHNORDER, &order) == -1)
+ CLog::Log(LOGWARNING, "CAESinkOSS::Initialize - Failed to set the channel order");
+ }
+#elif defined(OSS4)
+ unsigned long long order = 0;
+
+ for (unsigned int i = 0; i < format.m_channelLayout.Count(); ++i)
+ switch (format.m_channelLayout[i])
+ {
+ case AE_CH_FL : order = (order << 4) | CHID_L ; break;
+ case AE_CH_FR : order = (order << 4) | CHID_R ; break;
+ case AE_CH_FC : order = (order << 4) | CHID_C ; break;
+ case AE_CH_LFE: order = (order << 4) | CHID_LFE; break;
+ case AE_CH_SL : order = (order << 4) | CHID_LS ; break;
+ case AE_CH_SR : order = (order << 4) | CHID_RS ; break;
+ case AE_CH_BL : order = (order << 4) | CHID_LR ; break;
+ case AE_CH_BR : order = (order << 4) | CHID_RR ; break;
+
+ default:
+ continue;
+ }
+
+ if (ioctl(m_fd, SNDCTL_DSP_SET_CHNORDER, &order) == -1)
+ {
+ if (ioctl(m_fd, SNDCTL_DSP_GET_CHNORDER, &order) == -1)
+ {
+ CLog::Log(LOGWARNING, "CAESinkOSS::Initialize - Failed to get the channel order, assuming CHNORDER_NORMAL");
+ }
+ }
+#endif
+
+ int tmp = (CAEUtil::DataFormatToBits(format.m_dataFormat) >> 3) * format.m_channelLayout.Count() * OSS_FRAMES;
+ int pos = 0;
+ while ((tmp & 0x1) == 0x0)
+ {
+ tmp = tmp >> 1;
+ ++pos;
+ }
+
+ int oss_frag = (4 << 16) | pos;
+ if (ioctl(m_fd, SNDCTL_DSP_SETFRAGMENT, &oss_frag) == -1)
+ CLog::Log(LOGWARNING, "CAESinkOSS::Initialize - Failed to set the fragment size");
+
+ int oss_sr = format.m_sampleRate;
+ if (ioctl(m_fd, SNDCTL_DSP_SPEED, &oss_sr) == -1)
+ {
+ close(m_fd);
+ CLog::Log(LOGERROR, "CAESinkOSS::Initialize - Failed to set the sample rate");
+ return false;
+ }
+
+ audio_buf_info bi;
+ if (ioctl(m_fd, SNDCTL_DSP_GETOSPACE, &bi) == -1)
+ {
+ close(m_fd);
+ CLog::Log(LOGERROR, "CAESinkOSS::Initialize - Failed to get the output buffer size");
+ return false;
+ }
+
+ format.m_sampleRate = oss_sr;
+ format.m_frameSize = (CAEUtil::DataFormatToBits(format.m_dataFormat) >> 3) * format.m_channelLayout.Count();
+ format.m_frames = bi.fragsize / format.m_frameSize;
+
+ m_device = device;
+ m_format = format;
+ return true;
+}
+
+void CAESinkOSS::Deinitialize()
+{
+ Stop();
+
+ if (m_fd != -1)
+ close(m_fd);
+}
+
+inline CAEChannelInfo CAESinkOSS::GetChannelLayout(const AEAudioFormat& format)
+{
+ unsigned int count = 0;
+
+ if (format.m_dataFormat == AE_FMT_RAW)
+ {
+ switch (format.m_streamInfo.m_type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ count = 8;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ count = 2;
+ break;
+ default:
+ count = 0;
+ break;
+ }
+ }
+ else
+ {
+ for (unsigned int c = 0; c < 8; ++c)
+ for (unsigned int i = 0; i < format.m_channelLayout.Count(); ++i)
+ if (format.m_channelLayout[i] == OSSChannelMap[c])
+ {
+ count = c + 1;
+ break;
+ }
+ }
+
+ CAEChannelInfo info;
+ for (unsigned int i = 0; i < count; ++i)
+ info += OSSChannelMap[i];
+
+ return info;
+}
+
+void CAESinkOSS::Stop()
+{
+#ifdef SNDCTL_DSP_RESET
+ if (m_fd != -1)
+ ioctl(m_fd, SNDCTL_DSP_RESET, NULL);
+#endif
+}
+
+void CAESinkOSS::GetDelay(AEDelayStatus& status)
+{
+ if (m_fd == -1)
+ {
+ status.SetDelay(0);
+ return;
+ }
+
+ int delay;
+ if (ioctl(m_fd, SNDCTL_DSP_GETODELAY, &delay) == -1)
+ {
+ status.SetDelay(0);
+ return;
+ }
+
+ status.SetDelay((double)delay / (m_format.m_frameSize * m_format.m_sampleRate));
+}
+
+unsigned int CAESinkOSS::AddPackets(uint8_t **data, unsigned int frames, unsigned int offset)
+{
+ int size = frames * m_format.m_frameSize;
+ if (m_fd == -1)
+ {
+ CLog::Log(LOGERROR, "CAESinkOSS::AddPackets - Failed to write");
+ return INT_MAX;
+ }
+
+ void *buffer = data[0]+offset*m_format.m_frameSize;
+ int wrote = write(m_fd, buffer, size);
+ if (wrote < 0)
+ {
+ CLog::Log(LOGERROR, "CAESinkOSS::AddPackets - Failed to write");
+ return INT_MAX;
+ }
+
+ return wrote / m_format.m_frameSize;
+}
+
+void CAESinkOSS::Drain()
+{
+ if (m_fd == -1)
+ return;
+
+ if(ioctl(m_fd, SNDCTL_DSP_SYNC, NULL) == -1)
+ {
+ CLog::Log(LOGERROR, "CAESinkOSS::Drain - Draining the Sink failed");
+ }
+}
+
+void CAESinkOSS::EnumerateDevicesEx(AEDeviceInfoList &list, bool force)
+{
+ int mixerfd;
+ const char * mixerdev = "/dev/mixer";
+
+ if ((mixerfd = open(mixerdev, O_RDWR, 0)) == -1)
+ {
+ CLog::Log(LOGINFO, "CAESinkOSS::EnumerateDevicesEx - No OSS mixer device present: {}",
+ mixerdev);
+ return;
+ }
+
+#if defined(SNDCTL_SYSINFO) && defined(SNDCTL_CARDINFO)
+ oss_sysinfo sysinfo;
+ if (ioctl(mixerfd, SNDCTL_SYSINFO, &sysinfo) == -1)
+ {
+ // hardware not supported
+ // OSSv4 required ?
+ close(mixerfd);
+ return;
+ }
+
+ for (int i = 0; i < sysinfo.numcards; ++i)
+ {
+ std::stringstream devicepath;
+ std::stringstream devicename;
+ CAEDeviceInfo info;
+ oss_card_info cardinfo;
+
+ devicepath << "/dev/dsp" << i;
+ info.m_deviceName = devicepath.str();
+
+ cardinfo.card = i;
+ if (ioctl(mixerfd, SNDCTL_CARDINFO, &cardinfo) == -1)
+ break;
+
+ devicename << cardinfo.shortname << " " << cardinfo.longname;
+ info.m_displayName = devicename.str();
+
+ info.m_dataFormats.push_back(AE_FMT_S16NE);
+ info.m_dataFormats.push_back(AE_FMT_S32NE);
+ if (info.m_displayName.find("HDMI") != std::string::npos
+ || info.m_displayName.find("DisplayPort") != std::string::npos)
+ {
+ info.m_deviceType = AE_DEVTYPE_HDMI;
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_EAC3);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_MA);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_TRUEHD);
+ info.m_dataFormats.push_back(AE_FMT_RAW);
+ }
+ else if (info.m_displayName.find("Digital") != std::string::npos)
+ {
+ info.m_deviceType = AE_DEVTYPE_IEC958;
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ info.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ info.m_dataFormats.push_back(AE_FMT_RAW);
+ }
+ else
+ {
+ info.m_deviceType = AE_DEVTYPE_PCM;
+ }
+
+ oss_audioinfo ainfo = {};
+ ainfo.dev = i;
+ if (ioctl(mixerfd, SNDCTL_AUDIOINFO, &ainfo) != -1) {
+#if 0
+ if (ainfo.oformats & AFMT_S32_LE)
+ info.m_dataFormats.push_back(AE_FMT_S32LE);
+ if (ainfo.oformats & AFMT_S16_LE)
+ info.m_dataFormats.push_back(AE_FMT_S16LE);
+#endif
+ for (int j = 0;
+ j < ainfo.max_channels && AE_CH_NULL != OSSChannelMap[j];
+ ++j)
+ info.m_channels += OSSChannelMap[j];
+
+ for (int *rate = OSSSampleRateList; *rate != 0; ++rate)
+ if (*rate >= ainfo.min_rate && *rate <= ainfo.max_rate)
+ info.m_sampleRates.push_back(*rate);
+ }
+ info.m_wantsIECPassthrough = true;
+ list.push_back(info);
+ }
+#endif
+ close(mixerfd);
+}
+
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkOSS.h b/xbmc/cores/AudioEngine/Sinks/AESinkOSS.h
new file mode 100644
index 0000000..98762da
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkOSS.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "cores/AudioEngine/Interfaces/AESink.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+#include "threads/CriticalSection.h"
+
+#include <stdint.h>
+
+class CAESinkOSS : public IAESink
+{
+public:
+ const char *GetName() override { return "OSS"; }
+
+ CAESinkOSS();
+ ~CAESinkOSS() override;
+
+ static void Register();
+ static IAESink* Create(std::string &device, AEAudioFormat &desiredFormat);
+ static void EnumerateDevicesEx(AEDeviceInfoList &list, bool force = false);
+
+ bool Initialize(AEAudioFormat &format, std::string &device) override;
+ void Deinitialize() override;
+
+ virtual void Stop();
+ void GetDelay(AEDelayStatus& status) override;
+ double GetCacheTotal() override { return 0.0; } /* FIXME */
+ unsigned int AddPackets(uint8_t **data, unsigned int frames, unsigned int offset) override;
+ void Drain() override;
+private:
+ int m_fd;
+ std::string m_device;
+ AEAudioFormat m_initFormat;
+ AEAudioFormat m_format;
+
+ CAEChannelInfo GetChannelLayout(const AEAudioFormat& format);
+ std::string GetDeviceUse(const AEAudioFormat& format, const std::string &device);
+};
+
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkPULSE.cpp b/xbmc/cores/AudioEngine/Sinks/AESinkPULSE.cpp
new file mode 100644
index 0000000..77add8d
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkPULSE.cpp
@@ -0,0 +1,1297 @@
+/*
+ * 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 "AESinkPULSE.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "threads/SingleLock.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <array>
+#include <mutex>
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+class CDriverMonitor
+{
+public:
+ CDriverMonitor() = default;
+ virtual ~CDriverMonitor();
+ bool Start();
+ bool IsInitialized();
+
+ CCriticalSection m_sec;
+
+protected:
+ pa_context* m_pContext = nullptr;
+ pa_threaded_mainloop* m_pMainLoop = nullptr;
+ bool m_isInit = false;
+};
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+static const char *ContextStateToString(pa_context_state s)
+{
+ switch (s)
+ {
+ case PA_CONTEXT_UNCONNECTED:
+ return "unconnected";
+ case PA_CONTEXT_CONNECTING:
+ return "connecting";
+ case PA_CONTEXT_AUTHORIZING:
+ return "authorizing";
+ case PA_CONTEXT_SETTING_NAME:
+ return "setting name";
+ case PA_CONTEXT_READY:
+ return "ready";
+ case PA_CONTEXT_FAILED:
+ return "failed";
+ case PA_CONTEXT_TERMINATED:
+ return "terminated";
+ default:
+ return "none";
+ }
+}
+
+static const char *StreamStateToString(pa_stream_state s)
+{
+ switch(s)
+ {
+ case PA_STREAM_UNCONNECTED:
+ return "unconnected";
+ case PA_STREAM_CREATING:
+ return "creating";
+ case PA_STREAM_READY:
+ return "ready";
+ case PA_STREAM_FAILED:
+ return "failed";
+ case PA_STREAM_TERMINATED:
+ return "terminated";
+ default:
+ return "none";
+ }
+}
+
+static pa_sample_format AEStreamFormatToPulseFormat(CAEStreamInfo::DataType type)
+{
+ switch (type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ return PA_SAMPLE_S16NE;
+
+ default:
+ return PA_SAMPLE_INVALID;
+ }
+}
+
+static pa_sample_format AEFormatToPulseFormat(AEDataFormat format)
+{
+ switch (format)
+ {
+ case AE_FMT_U8 : return PA_SAMPLE_U8;
+ case AE_FMT_S16NE : return PA_SAMPLE_S16NE;
+ case AE_FMT_S24NE3 : return PA_SAMPLE_S24NE;
+ case AE_FMT_S24NE4 : return PA_SAMPLE_S24_32NE;
+ case AE_FMT_S32NE : return PA_SAMPLE_S32NE;
+ case AE_FMT_FLOAT : return PA_SAMPLE_FLOAT32;
+
+ default:
+ return PA_SAMPLE_INVALID;
+ }
+}
+
+static pa_encoding AEStreamFormatToPulseEncoding(CAEStreamInfo::DataType type)
+{
+ switch (type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ return PA_ENCODING_AC3_IEC61937;
+
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ return PA_ENCODING_DTS_IEC61937;
+
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ return PA_ENCODING_EAC3_IEC61937;
+
+ default:
+ return PA_ENCODING_INVALID;
+ }
+}
+
+static pa_encoding AEFormatToPulseEncoding(AEDataFormat format)
+{
+ switch (format)
+ {
+ case AE_FMT_RAW:
+ return PA_ENCODING_INVALID;
+
+ default:
+ return PA_ENCODING_PCM;
+ }
+}
+
+namespace
+{
+
+// clang-format off
+constexpr std::array<AEDataFormat, 6> defaultDataFormats = {
+ AE_FMT_U8,
+ AE_FMT_S16NE,
+ AE_FMT_S24NE3,
+ AE_FMT_S24NE4,
+ AE_FMT_S32NE,
+ AE_FMT_FLOAT
+};
+
+constexpr std::array<unsigned int, 14> defaultSampleRates = {
+ 5512,
+ 8000,
+ 11025,
+ 16000,
+ 22050,
+ 32000,
+ 44100,
+ 48000,
+ 64000,
+ 88200,
+ 96000,
+ 176400,
+ 192000,
+ 384000
+};
+// clang-format on
+
+} // namespace
+
+/* Static callback functions */
+
+static void ContextStateCallback(pa_context *c, void *userdata)
+{
+ pa_threaded_mainloop* m = static_cast<pa_threaded_mainloop*>(userdata);
+ switch (pa_context_get_state(c))
+ {
+ case PA_CONTEXT_READY:
+ case PA_CONTEXT_TERMINATED:
+ case PA_CONTEXT_UNCONNECTED:
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ case PA_CONTEXT_FAILED:
+ pa_threaded_mainloop_signal(m, 0);
+ break;
+ }
+}
+
+static void StreamStateCallback(pa_stream *s, void *userdata)
+{
+ pa_threaded_mainloop* m = static_cast<pa_threaded_mainloop*>(userdata);
+ switch (pa_stream_get_state(s))
+ {
+ case PA_STREAM_UNCONNECTED:
+ case PA_STREAM_CREATING:
+ case PA_STREAM_READY:
+ case PA_STREAM_FAILED:
+ case PA_STREAM_TERMINATED:
+ pa_threaded_mainloop_signal(m, 0);
+ break;
+ }
+}
+
+static void StreamRequestCallback(pa_stream *s, size_t length, void *userdata)
+{
+ CAESinkPULSE* p = static_cast<CAESinkPULSE*>(userdata);
+ if (!p)
+ return;
+
+ pa_threaded_mainloop* m = p->GetInternalMainLoop();
+ // pulse always tells us the total number of bytes
+ // we can add.
+ p->m_requestedBytes = static_cast<int>(length);
+ pa_threaded_mainloop_signal(m, 0);
+}
+
+static void StreamLatencyUpdateCallback(pa_stream *s, void *userdata)
+{
+ pa_threaded_mainloop* m = static_cast<pa_threaded_mainloop*>(userdata);
+ pa_threaded_mainloop_signal(m, 0);
+}
+
+
+static void SinkInputInfoCallback(pa_context *c, const pa_sink_input_info *i, int eol, void *userdata)
+{
+ CAESinkPULSE *p = static_cast<CAESinkPULSE*>(userdata);
+ if (!p || !p->IsInitialized())
+ return;
+
+ if(i && i->has_volume && !i->corked)
+ p->UpdateInternalVolume(&(i->volume));
+}
+
+static void SinkCallback(pa_context* c,
+ pa_subscription_event_type_t t,
+ uint32_t idx,
+ void* userdata)
+{
+ CDriverMonitor* p = static_cast<CDriverMonitor*>(userdata);
+ if (!p)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(p->m_sec);
+ if (p->IsInitialized())
+ {
+ if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK)
+ {
+ if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW)
+ {
+ CLog::Log(LOGDEBUG, "Sink appeared");
+ CServiceBroker::GetActiveAE()->DeviceCountChange("PULSE");
+ }
+ else if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE)
+ {
+ CLog::Log(LOGDEBUG, "Sink removed");
+ CServiceBroker::GetActiveAE()->DeviceCountChange("PULSE");
+ }
+ else if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_CHANGE)
+ {
+ CLog::Log(LOGDEBUG, "Sink changed");
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Not subscribed to Event: {}", static_cast<int>(t));
+ }
+ }
+}
+
+static void SinkChangedCallback(pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata)
+{
+ CAESinkPULSE* p = static_cast<CAESinkPULSE*>(userdata);
+ if(!p)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(p->m_sec);
+ if (p->IsInitialized())
+ {
+ if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK_INPUT)
+ {
+ // when we get a sink input event volume might have changed
+ if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_CHANGE)
+ {
+ if (idx != pa_stream_get_index(p->GetInternalStream()))
+ return;
+
+ // we need to leave the lock as we trigger a second callback
+ CSingleExit exitlock(p->m_sec);
+ pa_operation* op = pa_context_get_sink_input_info(c, idx, SinkInputInfoCallback, p);
+ if (op == NULL)
+ CLog::Log(LOGERROR, "PulseAudio: Failed to sync volume");
+ else
+ pa_operation_unref(op);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Not subscribed to Event: {}", static_cast<int>(t));
+ }
+ }
+}
+
+struct SinkInfoStruct
+{
+ AEDeviceInfoList *list;
+ bool isHWDevice;
+ bool isNWDevice;
+ bool isBTDevice;
+ bool device_found;
+ pa_threaded_mainloop *mainloop;
+ int samplerate;
+ pa_channel_map map;
+ SinkInfoStruct()
+ {
+ list = nullptr;
+ isHWDevice = false;
+ isNWDevice = false;
+ isBTDevice = false;
+ device_found = true;
+ mainloop = NULL; //called into C
+ samplerate = 0;
+ pa_channel_map_init(&map);
+ }
+};
+
+struct ModuleInfoStruct
+{
+ pa_threaded_mainloop *mainloop;
+ bool hasAllowPT;
+ ModuleInfoStruct()
+ {
+ mainloop = NULL; //called into C
+ hasAllowPT = false;
+ }
+};
+
+static void SinkInfoCallback(pa_context *c, const pa_sink_info *i, int eol, void *userdata)
+{
+ SinkInfoStruct *sinkStruct = static_cast<SinkInfoStruct*>(userdata);
+ if (!sinkStruct)
+ return;
+
+ if(i)
+ {
+ if (i->flags)
+ {
+ if (i->flags & PA_SINK_HARDWARE)
+ sinkStruct->isHWDevice = true;
+
+ if (i->flags & PA_SINK_NETWORK)
+ sinkStruct->isNWDevice = true;
+
+ sinkStruct->isBTDevice =
+ StringUtils::EndsWithNoCase(std::string(i->name), std::string("a2dp_sink"));
+ if (sinkStruct->isBTDevice)
+ CLog::Log(LOGINFO, "Found BT Device - will adjust buffers to larger values");
+
+ sinkStruct->samplerate = i->sample_spec.rate;
+ sinkStruct->device_found = true;
+ sinkStruct->map = i->channel_map;
+ }
+ }
+ pa_threaded_mainloop_signal(sinkStruct->mainloop, 0);
+}
+
+static AEChannel PAChannelToAEChannel(pa_channel_position_t channel)
+{
+ AEChannel ae_channel;
+ switch (channel)
+ {
+ case PA_CHANNEL_POSITION_FRONT_LEFT: ae_channel = AE_CH_FL; break;
+ case PA_CHANNEL_POSITION_FRONT_RIGHT: ae_channel = AE_CH_FR; break;
+ case PA_CHANNEL_POSITION_FRONT_CENTER: ae_channel = AE_CH_FC; break;
+ case PA_CHANNEL_POSITION_LFE: ae_channel = AE_CH_LFE; break;
+ case PA_CHANNEL_POSITION_REAR_LEFT: ae_channel = AE_CH_BL; break;
+ case PA_CHANNEL_POSITION_REAR_RIGHT: ae_channel = AE_CH_BR; break;
+ case PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER: ae_channel = AE_CH_FLOC; break;
+ case PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER: ae_channel = AE_CH_FROC; break;
+ case PA_CHANNEL_POSITION_REAR_CENTER: ae_channel = AE_CH_BC; break;
+ case PA_CHANNEL_POSITION_SIDE_LEFT: ae_channel = AE_CH_SL; break;
+ case PA_CHANNEL_POSITION_SIDE_RIGHT: ae_channel = AE_CH_SR; break;
+ case PA_CHANNEL_POSITION_TOP_FRONT_LEFT: ae_channel = AE_CH_TFL; break;
+ case PA_CHANNEL_POSITION_TOP_FRONT_RIGHT: ae_channel = AE_CH_TFR; break;
+ case PA_CHANNEL_POSITION_TOP_FRONT_CENTER: ae_channel = AE_CH_TFC; break;
+ case PA_CHANNEL_POSITION_TOP_CENTER: ae_channel = AE_CH_TC; break;
+ case PA_CHANNEL_POSITION_TOP_REAR_LEFT: ae_channel = AE_CH_TBL; break;
+ case PA_CHANNEL_POSITION_TOP_REAR_RIGHT: ae_channel = AE_CH_TBR; break;
+ case PA_CHANNEL_POSITION_TOP_REAR_CENTER: ae_channel = AE_CH_TBC; break;
+ default: ae_channel = AE_CH_NULL; break;
+ }
+ return ae_channel;
+}
+
+static pa_channel_position_t AEChannelToPAChannel(AEChannel ae_channel)
+{
+ pa_channel_position_t pa_channel;
+ switch (ae_channel)
+ {
+ case AE_CH_FL: pa_channel = PA_CHANNEL_POSITION_FRONT_LEFT; break;
+ case AE_CH_FR: pa_channel = PA_CHANNEL_POSITION_FRONT_RIGHT; break;
+ case AE_CH_FC: pa_channel = PA_CHANNEL_POSITION_FRONT_CENTER; break;
+ case AE_CH_LFE: pa_channel = PA_CHANNEL_POSITION_LFE; break;
+ case AE_CH_BL: pa_channel = PA_CHANNEL_POSITION_REAR_LEFT; break;
+ case AE_CH_BR: pa_channel = PA_CHANNEL_POSITION_REAR_RIGHT; break;
+ case AE_CH_FLOC: pa_channel = PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER; break;
+ case AE_CH_FROC: pa_channel = PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER; break;
+ case AE_CH_BC: pa_channel = PA_CHANNEL_POSITION_REAR_CENTER; break;
+ case AE_CH_SL: pa_channel = PA_CHANNEL_POSITION_SIDE_LEFT; break;
+ case AE_CH_SR: pa_channel = PA_CHANNEL_POSITION_SIDE_RIGHT; break;
+ case AE_CH_TFL: pa_channel = PA_CHANNEL_POSITION_TOP_FRONT_LEFT; break;
+ case AE_CH_TFR: pa_channel = PA_CHANNEL_POSITION_TOP_FRONT_RIGHT; break;
+ case AE_CH_TFC: pa_channel = PA_CHANNEL_POSITION_TOP_FRONT_CENTER; break;
+ case AE_CH_TC: pa_channel = PA_CHANNEL_POSITION_TOP_CENTER; break;
+ case AE_CH_TBL: pa_channel = PA_CHANNEL_POSITION_TOP_REAR_LEFT; break;
+ case AE_CH_TBR: pa_channel = PA_CHANNEL_POSITION_TOP_REAR_RIGHT; break;
+ case AE_CH_TBC: pa_channel = PA_CHANNEL_POSITION_TOP_REAR_CENTER; break;
+ default: pa_channel = PA_CHANNEL_POSITION_INVALID; break;
+ }
+ return pa_channel;
+}
+
+static pa_channel_map AEChannelMapToPAChannel(const CAEChannelInfo& info)
+{
+ pa_channel_map map;
+ pa_channel_map_init(&map);
+ pa_channel_position_t pos;
+ for (unsigned int i = 0; i < info.Count(); ++i)
+ {
+ pos = AEChannelToPAChannel(info[i]);
+ if(pos != PA_CHANNEL_POSITION_INVALID)
+ {
+ // remember channel name and increase channel count
+ map.map[map.channels++] = pos;
+ }
+ }
+ return map;
+}
+
+static CAEChannelInfo PAChannelToAEChannelMap(const pa_channel_map& channels)
+{
+ CAEChannelInfo info;
+ AEChannel ch;
+ info.Reset();
+ for (unsigned int i=0; i<channels.channels; i++)
+ {
+ ch = PAChannelToAEChannel(channels.map[i]);
+ if(ch != AE_CH_NULL)
+ info += ch;
+ }
+ return info;
+}
+
+static void ModuleInfoCallback(pa_context* c, const pa_module_info *i, int eol, void *userdata)
+{
+ ModuleInfoStruct *mis = static_cast<ModuleInfoStruct*>(userdata);
+ if (!mis)
+ return;
+
+ if (i)
+ {
+ if (strcmp(i->name, "module-allow-passthrough") == 0)
+ mis->hasAllowPT = true;
+ }
+ pa_threaded_mainloop_signal(mis->mainloop, 0);
+}
+
+static void SinkInfoRequestCallback(pa_context *c, const pa_sink_info *i, int eol, void *userdata)
+{
+
+ SinkInfoStruct *sinkStruct = static_cast<SinkInfoStruct*>(userdata);
+ if (!sinkStruct)
+ return;
+
+ if(sinkStruct->list->empty())
+ {
+ //add a default device first
+ CAEDeviceInfo defaultDevice;
+ defaultDevice.m_deviceName = std::string("Default");
+ defaultDevice.m_displayName = std::string("Default");
+ defaultDevice.m_displayNameExtra = std::string("Default Output Device (PULSEAUDIO)");
+ defaultDevice.m_dataFormats.insert(defaultDevice.m_dataFormats.end(),
+ defaultDataFormats.begin(), defaultDataFormats.end());
+ defaultDevice.m_channels = CAEChannelInfo(AE_CH_LAYOUT_2_0);
+ defaultDevice.m_sampleRates.assign(defaultSampleRates.begin(), defaultSampleRates.end());
+ defaultDevice.m_deviceType = AE_DEVTYPE_PCM;
+ defaultDevice.m_wantsIECPassthrough = true;
+ sinkStruct->list->push_back(defaultDevice);
+ }
+ if (i && i->name)
+ {
+ CAEDeviceInfo device;
+ bool valid = true;
+ device.m_deviceName = std::string(i->name);
+ device.m_displayName = std::string(i->description);
+ if (i->active_port && i->active_port->description)
+ device.m_displayNameExtra = std::string((i->active_port->description)).append(" (PULSEAUDIO)");
+ else
+ device.m_displayNameExtra = std::string((i->description)).append(" (PULSEAUDIO)");
+ unsigned int device_type = AE_DEVTYPE_PCM; //0
+
+ device.m_channels = PAChannelToAEChannelMap(i->channel_map);
+
+ // Don't add devices that would not have a channel map
+ if(device.m_channels.Count() == 0)
+ valid = false;
+
+ device.m_sampleRates.assign(defaultSampleRates.begin(), defaultSampleRates.end());
+
+ for (unsigned int j = 0; j < i->n_formats; j++)
+ {
+ switch(i->formats[j]->encoding)
+ {
+ case PA_ENCODING_AC3_IEC61937:
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ device_type = AE_DEVTYPE_IEC958;
+ break;
+ case PA_ENCODING_DTS_IEC61937:
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ device_type = AE_DEVTYPE_IEC958;
+ break;
+ case PA_ENCODING_EAC3_IEC61937:
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_EAC3);
+ device_type = AE_DEVTYPE_IEC958;
+ break;
+ case PA_ENCODING_PCM:
+ device.m_dataFormats.insert(device.m_dataFormats.end(), defaultDataFormats.begin(),
+ defaultDataFormats.end());
+ break;
+ default:
+ break;
+ }
+ }
+ // passthrough is only working when device has Stereo channel config
+ if (device_type > AE_DEVTYPE_PCM && device.m_channels.Count() == 2)
+ {
+ device.m_deviceType = AE_DEVTYPE_IEC958;
+ device.m_dataFormats.push_back(AE_FMT_RAW);
+ }
+ else
+ device.m_deviceType = AE_DEVTYPE_PCM;
+
+ device.m_wantsIECPassthrough = true;
+
+ if(valid)
+ {
+ CLog::Log(LOGDEBUG, "PulseAudio: Found {} with devicestring {}", device.m_displayName,
+ device.m_deviceName);
+ sinkStruct->list->push_back(device);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "PulseAudio: Skipped {} with devicestring {}", device.m_displayName,
+ device.m_deviceName);
+ }
+ }
+ pa_threaded_mainloop_signal(sinkStruct->mainloop, 0);
+}
+
+static bool SetupContext(const char* host,
+ const char* appname,
+ pa_context** context,
+ pa_threaded_mainloop** mainloop)
+{
+ if ((*mainloop = pa_threaded_mainloop_new()) == nullptr)
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Failed to allocate main loop");
+ return false;
+ }
+
+ if (((*context) = pa_context_new(pa_threaded_mainloop_get_api(*mainloop), appname)) == nullptr)
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Failed to allocate context");
+ return false;
+ }
+
+ pa_context_set_state_callback(*context, ContextStateCallback, *mainloop);
+
+ if (pa_context_connect(*context, host, (pa_context_flags_t)0, nullptr) < 0)
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Failed to connect context");
+ return false;
+ }
+ pa_threaded_mainloop_lock(*mainloop);
+
+ if (pa_threaded_mainloop_start(*mainloop) < 0)
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Failed to start MainLoop");
+ pa_threaded_mainloop_unlock(*mainloop);
+ return false;
+ }
+
+ /* Wait until the context is ready */
+ do
+ {
+ pa_threaded_mainloop_wait(*mainloop);
+ CLog::Log(LOGDEBUG, "PulseAudio: Context {}",
+ ContextStateToString(pa_context_get_state(*context)));
+ } while (pa_context_get_state(*context) != PA_CONTEXT_READY &&
+ pa_context_get_state(*context) != PA_CONTEXT_FAILED);
+
+ if (pa_context_get_state(*context) == PA_CONTEXT_FAILED)
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Waited for the Context but it failed");
+ pa_threaded_mainloop_unlock(*mainloop);
+ return false;
+ }
+
+ pa_threaded_mainloop_unlock(*mainloop);
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+CDriverMonitor::~CDriverMonitor()
+{
+ m_isInit = false;
+
+ if (m_pMainLoop)
+ pa_threaded_mainloop_stop(m_pMainLoop);
+
+ if (m_pContext)
+ {
+ pa_context_disconnect(m_pContext);
+ pa_context_unref(m_pContext);
+ m_pContext = nullptr;
+ }
+
+ if (m_pMainLoop)
+ {
+ pa_threaded_mainloop_free(m_pMainLoop);
+ m_pMainLoop = nullptr;
+ }
+}
+
+bool CDriverMonitor::IsInitialized()
+{
+ return m_isInit;
+}
+
+bool CDriverMonitor::Start()
+{
+ if (!SetupContext(nullptr, "KodiDriver", &m_pContext, &m_pMainLoop))
+ {
+ CLog::Log(LOGINFO, "PulseAudio might not be running. Context was not created.");
+ return false;
+ }
+
+ pa_threaded_mainloop_lock(m_pMainLoop);
+
+ m_isInit = true;
+
+ std::unique_lock<CCriticalSection> lock(m_sec);
+ // Register Callback for Sink changes
+ pa_context_set_subscribe_callback(m_pContext, SinkCallback, this);
+ const pa_subscription_mask_t mask = pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK);
+ pa_operation* op = pa_context_subscribe(m_pContext, mask, nullptr, this);
+ if (op != nullptr)
+ pa_operation_unref(op);
+
+ pa_threaded_mainloop_unlock(m_pMainLoop);
+
+ return true;
+}
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/* PulseAudio class memberfunctions*/
+
+std::unique_ptr<CDriverMonitor> CAESinkPULSE::m_pMonitor;
+
+bool CAESinkPULSE::Register()
+{
+ // check if pulseaudio is actually available
+ pa_simple *s;
+ pa_sample_spec ss;
+ ss.format = PA_SAMPLE_S16NE;
+ ss.channels = 2;
+ ss.rate = 44100;
+ s = pa_simple_new(NULL, "Kodi-Tester", PA_STREAM_PLAYBACK, NULL, "Test", &ss, NULL, NULL, NULL);
+ if (!s)
+ {
+ CLog::Log(LOGINFO, "PulseAudio: Server not running");
+ return false;
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "PulseAudio: Server found running - will try to use Pulse");
+ pa_simple_free(s);
+ }
+
+ m_pMonitor.reset(new CDriverMonitor());
+ m_pMonitor->Start();
+
+ AE::AESinkRegEntry entry;
+ entry.sinkName = "PULSE";
+ entry.createFunc = CAESinkPULSE::Create;
+ entry.enumerateFunc = CAESinkPULSE::EnumerateDevicesEx;
+ entry.cleanupFunc = CAESinkPULSE::Cleanup;
+ AE::CAESinkFactory::RegisterSink(entry);
+ return true;
+}
+
+IAESink* CAESinkPULSE::Create(std::string &device, AEAudioFormat& desiredFormat)
+{
+ IAESink* sink = new CAESinkPULSE();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+CAESinkPULSE::CAESinkPULSE()
+{
+ m_IsAllocated = false;
+ m_passthrough = false;
+ m_MainLoop = NULL;
+ m_BytesPerSecond = 0;
+ m_BufferSize = 0;
+ m_Channels = 0;
+ m_maxLatency = 0.0;
+ m_Stream = NULL;
+ m_Context = NULL;
+ m_IsStreamPaused = false;
+ m_volume_needs_update = false;
+ m_periodSize = 0;
+ pa_cvolume_init(&m_Volume);
+}
+
+CAESinkPULSE::~CAESinkPULSE()
+{
+ Deinitialize();
+}
+
+bool CAESinkPULSE::Initialize(AEAudioFormat &format, std::string &device)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_sec);
+ m_IsAllocated = false;
+ }
+ m_passthrough = false;
+ m_BytesPerSecond = 0;
+ m_BufferSize = 0;
+ m_Channels = 0;
+ m_maxLatency = 0.0;
+ m_Stream = NULL;
+ m_Context = NULL;
+ m_periodSize = 0;
+
+ if (!SetupContext(NULL, "KodiSink", &m_Context, &m_MainLoop))
+ {
+ CLog::Log(LOGINFO, "PulseAudio might not be running. Context was not created.");
+ Deinitialize();
+ return false;
+ }
+
+ pa_threaded_mainloop_lock(m_MainLoop);
+
+ struct pa_channel_map map;
+ pa_channel_map_init(&map);
+
+ // PULSE cannot cope with e.g. planar formats so we fall back to FLOAT
+ // when we receive an invalid pulse format
+ pa_sample_format pa_fmt;
+ // PA can only handle IEC packed RAW format if we get a RAW format
+ if (format.m_dataFormat == AE_FMT_RAW)
+ {
+ pa_fmt = AEStreamFormatToPulseFormat(format.m_streamInfo.m_type);
+ m_passthrough = true;
+ }
+ else
+ pa_fmt = AEFormatToPulseFormat(format.m_dataFormat);
+
+ if (pa_fmt == PA_SAMPLE_INVALID)
+ {
+ CLog::Log(LOGDEBUG, "PULSE does not support format: {} - will fallback to AE_FMT_FLOAT",
+ CAEUtil::DataFormatToStr(format.m_dataFormat));
+ format.m_dataFormat = AE_FMT_FLOAT;
+ pa_fmt = PA_SAMPLE_FLOAT32;
+ m_passthrough = false;
+ }
+ // store information about current sink
+ SinkInfoStruct sinkStruct;
+ sinkStruct.mainloop = m_MainLoop;
+ sinkStruct.device_found = false;
+
+ // get real sample rate of the device we want to open - to avoid resampling
+ bool isDefaultDevice = false;
+ if(StringUtils::EndsWithNoCase(device, std::string("default")))
+ isDefaultDevice = true;
+
+ WaitForOperation(pa_context_get_sink_info_by_name(m_Context, isDefaultDevice ? NULL : device.c_str(), SinkInfoCallback, &sinkStruct), m_MainLoop, "Get Sink Info");
+ // only check if the device is existing - don't alter the sample rate
+ if (!sinkStruct.device_found)
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Sink {} not found", device);
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ Deinitialize();
+ return false;
+ }
+
+ if(m_passthrough)
+ {
+ map.channels = 2;
+ format.m_channelLayout = AE_CH_LAYOUT_2_0;
+ }
+ else
+ {
+ map = AEChannelMapToPAChannel(format.m_channelLayout);
+ format.m_channelLayout = PAChannelToAEChannelMap(map);
+ }
+ m_Channels = format.m_channelLayout.Count();
+
+ // Pulse can resample everything between 5 khz and 384 khz (since 9.0)
+ unsigned int max_pulse_sample_rate = 384000U;
+ format.m_sampleRate = std::max(5512U, std::min(format.m_sampleRate, max_pulse_sample_rate));
+
+ pa_format_info *info[1];
+ info[0] = pa_format_info_new();
+ if (m_passthrough)
+ info[0]->encoding = AEStreamFormatToPulseEncoding(format.m_streamInfo.m_type);
+ else
+ info[0]->encoding = AEFormatToPulseEncoding(format.m_dataFormat);
+
+ if (info[0]->encoding == PA_ENCODING_INVALID)
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Invalid Encoding");
+ pa_format_info_free(info[0]);
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ Deinitialize();
+ return false;
+ }
+
+ if(!m_passthrough)
+ {
+ pa_format_info_set_sample_format(info[0], pa_fmt);
+ pa_format_info_set_channel_map(info[0], &map);
+ }
+ pa_format_info_set_channels(info[0], m_Channels);
+
+ // PA requires the original encoded rate in order to do EAC3
+ unsigned int samplerate = format.m_sampleRate;
+ if (m_passthrough && (info[0]->encoding == PA_ENCODING_EAC3_IEC61937))
+ {
+ // this is only used internally for PA to use EAC3
+ samplerate = format.m_streamInfo.m_sampleRate;
+ }
+
+ pa_format_info_set_rate(info[0], samplerate);
+
+ if (!pa_format_info_valid(info[0]))
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Invalid format info");
+ pa_format_info_free(info[0]);
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ Deinitialize();
+ return false;
+ }
+
+ pa_sample_spec spec;
+ pa_format_info_to_sample_spec(info[0], &spec, NULL);
+ if (!pa_sample_spec_valid(&spec))
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Invalid sample spec");
+ pa_format_info_free(info[0]);
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ Deinitialize();
+ return false;
+ }
+
+ m_BytesPerSecond = pa_bytes_per_second(&spec);
+ unsigned int frameSize = pa_frame_size(&spec);
+
+ m_Stream = pa_stream_new_extended(m_Context, "kodi audio stream", info, 1, NULL);
+ pa_format_info_free(info[0]);
+
+ if (m_Stream == NULL)
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Could not create a stream");
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ Deinitialize();
+ return false;
+ }
+
+ pa_stream_set_state_callback(m_Stream, StreamStateCallback, m_MainLoop);
+ pa_stream_set_write_callback(m_Stream, StreamRequestCallback, this);
+ pa_stream_set_latency_update_callback(m_Stream, StreamLatencyUpdateCallback, m_MainLoop);
+
+ // default buffer construction
+ // align with AE's max buffer
+ unsigned int latency = m_BytesPerSecond / 2.5; // 400 ms
+ unsigned int process_time = latency / 4; // 100 ms
+ if (sinkStruct.isHWDevice && !sinkStruct.isNWDevice && !sinkStruct.isBTDevice)
+ {
+ // on hw devices buffers can be further reduced
+ // 200ms max latency
+ // 50ms min packet size
+ latency = m_BytesPerSecond / 5;
+ process_time = latency / 4;
+ }
+
+ pa_buffer_attr buffer_attr;
+ buffer_attr.fragsize = latency;
+ buffer_attr.maxlength = (uint32_t) -1;
+ buffer_attr.minreq = process_time;
+ buffer_attr.prebuf = (uint32_t) -1;
+ buffer_attr.tlength = latency;
+ int flags = (PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY);
+
+ if (m_passthrough)
+ flags |= PA_STREAM_PASSTHROUGH;
+
+ if (pa_stream_connect_playback(m_Stream, isDefaultDevice ? NULL : device.c_str(), &buffer_attr, (pa_stream_flags) flags, NULL, NULL) < 0)
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Failed to connect stream to output");
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ Deinitialize();
+ return false;
+ }
+
+ /* Wait until the stream is ready */
+ do
+ {
+ pa_threaded_mainloop_wait(m_MainLoop);
+ CLog::Log(LOGDEBUG, "PulseAudio: Stream {}",
+ StreamStateToString(pa_stream_get_state(m_Stream)));
+ }
+ while (pa_stream_get_state(m_Stream) != PA_STREAM_READY && pa_stream_get_state(m_Stream) != PA_STREAM_FAILED);
+
+ if (pa_stream_get_state(m_Stream) == PA_STREAM_FAILED)
+ {
+ CLog::Log(LOGERROR, "PulseAudio: Waited for the stream but it failed");
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ Deinitialize();
+ return false;
+ }
+
+ const pa_buffer_attr *a;
+
+ if (!(a = pa_stream_get_buffer_attr(m_Stream)))
+ {
+ CLog::Log(LOGERROR, "PulseAudio: {}", pa_strerror(pa_context_errno(m_Context)));
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ Deinitialize();
+ return false;
+ }
+ else
+ {
+ unsigned int packetSize = a->minreq;
+ m_BufferSize = a->tlength;
+ m_periodSize = a->minreq;
+
+ format.m_frames = packetSize / frameSize;
+ m_maxLatency = static_cast<double>(m_BufferSize) / m_BytesPerSecond;
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_sec);
+ // Register Callback for Sink changes
+ pa_context_set_subscribe_callback(m_Context, SinkChangedCallback, this);
+ const pa_subscription_mask_t mask = pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK_INPUT);
+ pa_operation *op = pa_context_subscribe(m_Context, mask, NULL, this);
+ if (op != NULL)
+ pa_operation_unref(op);
+ }
+
+ pa_threaded_mainloop_unlock(m_MainLoop);
+
+ format.m_frameSize = frameSize;
+ m_format = format;
+ format.m_dataFormat = m_passthrough ? AE_FMT_S16NE : format.m_dataFormat;
+
+ CLog::Log(LOGINFO,
+ "PulseAudio: Opened device {} in {} mode with Buffersize {} ms Periodsize {} ms",
+ device, m_passthrough ? "passthrough" : "pcm",
+ static_cast<unsigned int>(1000.0 * m_BufferSize / m_BytesPerSecond),
+ static_cast<unsigned int>(1000.0 * m_periodSize / m_BytesPerSecond));
+
+ // Cork stream will resume when adding first package
+ Pause(true);
+ {
+ std::unique_lock<CCriticalSection> lock(m_sec);
+ m_IsAllocated = true;
+ }
+ return true;
+}
+
+void CAESinkPULSE::Deinitialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_sec);
+ m_IsAllocated = false;
+ m_passthrough = false;
+ m_periodSize = 0;
+ m_requestedBytes = 0;
+ m_maxLatency = 0.0;
+
+ if (m_Stream)
+ {
+ CSingleExit exit(m_sec);
+ pa_threaded_mainloop_lock(m_MainLoop);
+ WaitForOperation(pa_stream_flush(m_Stream, NULL, NULL), m_MainLoop, "Flush");
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ }
+
+ {
+ CSingleExit exit(m_sec);
+ if (m_MainLoop)
+ pa_threaded_mainloop_stop(m_MainLoop);
+ }
+
+ if (m_Stream)
+ {
+ pa_stream_disconnect(m_Stream);
+ pa_stream_unref(m_Stream);
+ m_Stream = NULL;
+ m_IsStreamPaused = false;
+ }
+
+ if (m_Context)
+ {
+ pa_context_disconnect(m_Context);
+ pa_context_unref(m_Context);
+ m_Context = NULL;
+ }
+
+ if (m_MainLoop)
+ {
+ pa_threaded_mainloop_free(m_MainLoop);
+ m_MainLoop = NULL;
+ }
+}
+
+void CAESinkPULSE::GetDelay(AEDelayStatus& status)
+{
+ if (!m_IsAllocated)
+ {
+ status.SetDelay(0);
+ return;
+ }
+
+ pa_threaded_mainloop_lock(m_MainLoop);
+ pa_usec_t r_usec;
+ int negative;
+
+ if (pa_stream_get_latency(m_Stream, &r_usec, &negative) < 0)
+ r_usec = 0;
+
+ double delay = r_usec / 1000000.0;
+ if (delay > m_maxLatency)
+ m_maxLatency = delay;
+
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ status.SetDelay(delay);
+}
+
+double CAESinkPULSE::GetCacheTotal()
+{
+ return m_maxLatency;
+}
+
+unsigned int CAESinkPULSE::AddPackets(uint8_t **data, unsigned int frames, unsigned int offset)
+{
+ if (!m_IsAllocated)
+ return 0;
+
+ if (m_IsStreamPaused)
+ {
+ Pause(false);
+ }
+
+ pa_threaded_mainloop_lock(m_MainLoop);
+
+ unsigned int available = frames * m_format.m_frameSize;
+ unsigned int length = m_periodSize;
+ void *buffer = data[0]+offset*m_format.m_frameSize;
+ auto wait_time =
+ std::chrono::duration<double>(static_cast<double>(m_BufferSize) / m_BytesPerSecond);
+ XbmcThreads::EndTime<std::chrono::duration<double>> timer(wait_time);
+ // we don't want to block forever - if timer expires pa_stream_write will
+ // fail - therefore we don't care and just return 0;
+ while (!timer.IsTimePast())
+ {
+ if (m_requestedBytes > 0)
+ break;
+ pa_threaded_mainloop_wait(m_MainLoop);
+ }
+
+ if (timer.IsTimePast())
+ {
+ CLog::Log(LOGERROR, "Sink Timer expired for more than buffer time: {}s", wait_time.count());
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ return 0;
+ }
+
+ length = std::min(length, available);
+ int error = pa_stream_write(m_Stream, buffer, length, NULL, 0, PA_SEEK_RELATIVE);
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ if (error)
+ {
+ CLog::Log(LOGERROR, "CAESinkPULSE::AddPackets - pa_stream_write failed: {}", error);
+ return 0;
+ }
+
+ // subtract here, as we might come back earlier than our callback and there is
+ // still space in the buffer to write another time
+ m_requestedBytes -= length;
+
+ unsigned int res = length / m_format.m_frameSize;
+
+ return res;
+}
+
+void CAESinkPULSE::Drain()
+{
+ if (!m_IsAllocated)
+ return;
+
+ pa_threaded_mainloop_lock(m_MainLoop);
+ WaitForOperation(pa_stream_drain(m_Stream, NULL, NULL), m_MainLoop, "Drain");
+ WaitForOperation(pa_stream_cork(m_Stream, 1, NULL, NULL), m_MainLoop, "Pause");
+ m_IsStreamPaused = true;
+ pa_threaded_mainloop_unlock(m_MainLoop);
+}
+
+// This is a helper to get stream info during the PA callbacks
+// it shall never be called from real outside
+pa_stream* CAESinkPULSE::GetInternalStream()
+{
+ return m_Stream;
+}
+
+// This is a helper to use the internal mainloop from another thread, e.g. a RequestCallback
+// it is shipped via the userdata. Don't use it for other purposes than signalling
+pa_threaded_mainloop* CAESinkPULSE::GetInternalMainLoop()
+{
+ return m_MainLoop;
+}
+
+void CAESinkPULSE::UpdateInternalVolume(const pa_cvolume* nVol)
+{
+ if (!nVol)
+ return;
+
+ pa_volume_t o_vol = pa_cvolume_avg(&m_Volume);
+ pa_volume_t n_vol = pa_cvolume_avg(nVol);
+
+ if (o_vol != n_vol)
+ {
+ pa_cvolume_set(&m_Volume, m_Channels, n_vol);
+ m_volume_needs_update = true;
+ }
+}
+
+void CAESinkPULSE::SetVolume(float volume)
+{
+ if (m_IsAllocated && !m_passthrough)
+ {
+ pa_threaded_mainloop_lock(m_MainLoop);
+ // clamp possibly too large / low values
+ float per_cent_volume = std::max(0.0f, std::min(volume, 1.0f));
+
+ if (m_volume_needs_update)
+ {
+ m_volume_needs_update = false;
+ pa_volume_t n_vol = pa_cvolume_avg(&m_Volume);
+ n_vol = std::min(n_vol, PA_VOLUME_NORM);
+ per_cent_volume = static_cast<float>(n_vol) / PA_VOLUME_NORM;
+ // only update internal volume
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume(per_cent_volume, false);
+ return;
+ }
+
+ pa_volume_t pavolume = per_cent_volume * PA_VOLUME_NORM;
+ unsigned int sink_input_idx = pa_stream_get_index(m_Stream);
+
+ if ( pavolume <= 0 )
+ pa_cvolume_mute(&m_Volume, m_Channels);
+ else
+ pa_cvolume_set(&m_Volume, m_Channels, pavolume);
+
+ pa_operation *op = pa_context_set_sink_input_volume(m_Context, sink_input_idx, &m_Volume, NULL, NULL);
+ if (op == NULL)
+ CLog::Log(LOGERROR, "PulseAudio: Failed to set volume");
+ else
+ pa_operation_unref(op);
+
+ pa_threaded_mainloop_unlock(m_MainLoop);
+ }
+}
+
+void CAESinkPULSE::EnumerateDevicesEx(AEDeviceInfoList &list, bool force)
+{
+ pa_context *context;
+ pa_threaded_mainloop *mainloop;
+
+ if (!SetupContext(NULL, "KodiSink", &context, &mainloop))
+ {
+ CLog::Log(LOGINFO, "PulseAudio might not be running. Context was not created.");
+ return;
+ }
+
+ pa_threaded_mainloop_lock(mainloop);
+
+ SinkInfoStruct sinkStruct;
+ sinkStruct.mainloop = mainloop;
+ sinkStruct.list = &list;
+
+ ModuleInfoStruct mis;
+ mis.mainloop = mainloop;
+
+ WaitForOperation(pa_context_get_module_info_list(context, ModuleInfoCallback, &mis), mainloop, "Check PA Modules");
+ if (!mis.hasAllowPT)
+ {
+ CLog::Log(LOGWARNING, "Pulseaudio module module-allow-passthrough not loaded - opening PT devices might fail");
+ }
+ WaitForOperation(pa_context_get_sink_info_list(context, SinkInfoRequestCallback, &sinkStruct), mainloop, "EnumerateAudioSinks");
+
+ pa_threaded_mainloop_unlock(mainloop);
+
+ if (mainloop)
+ pa_threaded_mainloop_stop(mainloop);
+
+ if (context)
+ {
+ pa_context_disconnect(context);
+ pa_context_unref(context);
+ context = NULL;
+ }
+
+ if (mainloop)
+ {
+ pa_threaded_mainloop_free(mainloop);
+ mainloop = NULL;
+ }
+}
+
+bool CAESinkPULSE::IsInitialized()
+{
+ std::unique_lock<CCriticalSection> lock(m_sec);
+ return m_IsAllocated;
+}
+
+void CAESinkPULSE::Pause(bool pause)
+{
+ pa_threaded_mainloop_lock(m_MainLoop);
+
+ if (!WaitForOperation(pa_stream_cork(m_Stream, pause ? 1 : 0, NULL, NULL), m_MainLoop, pause ? "Pause" : "Resume"))
+ pause = !pause;
+
+ m_IsStreamPaused = pause;
+ pa_threaded_mainloop_unlock(m_MainLoop);
+}
+
+inline bool CAESinkPULSE::WaitForOperation(pa_operation *op, pa_threaded_mainloop *mainloop, const char *LogEntry = "")
+{
+ if (op == NULL)
+ return false;
+
+ bool success = true;
+
+ while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
+ pa_threaded_mainloop_wait(mainloop);
+
+ if (pa_operation_get_state(op) != PA_OPERATION_DONE)
+ {
+ CLog::Log(LOGERROR, "PulseAudio: {} Operation failed", LogEntry);
+ success = false;
+ }
+
+ pa_operation_unref(op);
+ return success;
+}
+
+void CAESinkPULSE::Cleanup()
+{
+ m_pMonitor.reset();
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkPULSE.h b/xbmc/cores/AudioEngine/Sinks/AESinkPULSE.h
new file mode 100644
index 0000000..18aeb32
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkPULSE.h
@@ -0,0 +1,81 @@
+/*
+ * 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 "cores/AudioEngine/Interfaces/AESink.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "threads/Thread.h"
+
+#include <atomic>
+#include <memory>
+
+#include <pulse/pulseaudio.h>
+#include <pulse/simple.h>
+
+class CDriverMonitor;
+
+class CAESinkPULSE : public IAESink
+{
+public:
+ const char *GetName() override { return "PULSE"; }
+
+ CAESinkPULSE();
+ ~CAESinkPULSE() override;
+
+ static bool Register();
+ static IAESink* Create(std::string &device, AEAudioFormat &desiredFormat);
+ static void EnumerateDevicesEx(AEDeviceInfoList &list, bool force = false);
+ static void Cleanup();
+
+ bool Initialize(AEAudioFormat &format, std::string &device) override;
+ void Deinitialize() override;
+
+ virtual double GetDelay() { return 0.0; }
+ 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 { return true; }
+ void SetVolume(float volume) override;
+
+ bool IsInitialized();
+ void UpdateInternalVolume(const pa_cvolume* nVol);
+ pa_stream* GetInternalStream();
+ pa_threaded_mainloop* GetInternalMainLoop();
+ CCriticalSection m_sec;
+ std::atomic<int> m_requestedBytes = 0;
+
+private:
+ void Pause(bool pause);
+ static inline bool WaitForOperation(pa_operation *op, pa_threaded_mainloop *mainloop, const char *LogEntry);
+
+ bool m_IsAllocated;
+ bool m_passthrough;
+ bool m_IsStreamPaused;
+
+ AEAudioFormat m_format;
+ unsigned int m_BytesPerSecond;
+ unsigned int m_BufferSize;
+ unsigned int m_Channels;
+ double m_maxLatency;
+
+ pa_stream *m_Stream;
+ pa_cvolume m_Volume;
+ bool m_volume_needs_update;
+ uint32_t m_periodSize;
+
+ pa_context *m_Context;
+ pa_threaded_mainloop *m_MainLoop;
+
+ static std::unique_ptr<CDriverMonitor> m_pMonitor;
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkSNDIO.cpp b/xbmc/cores/AudioEngine/Sinks/AESinkSNDIO.cpp
new file mode 100644
index 0000000..3ffb37f
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkSNDIO.cpp
@@ -0,0 +1,316 @@
+/* -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2016-2017 Tobias Kortkamp <t@tobik.me>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "AESinkSNDIO.h"
+
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "utils/log.h"
+
+#include <sys/param.h>
+
+#ifndef nitems
+#define nitems(x) (sizeof((x))/sizeof((x)[0]))
+#endif
+
+static enum AEChannel channelMap[] =
+{
+ AE_CH_FL,
+ AE_CH_FR,
+ AE_CH_BL,
+ AE_CH_BR,
+ AE_CH_FC,
+ AE_CH_LFE,
+ AE_CH_SL,
+ AE_CH_SR,
+};
+
+struct sndio_formats
+{
+ AEDataFormat fmt;
+ unsigned int bits;
+ unsigned int bps;
+ unsigned int sig;
+ unsigned int le;
+ unsigned int msb;
+};
+
+static struct sndio_formats formats[] =
+{
+ { AE_FMT_S32NE, 32, 4, 1, SIO_LE_NATIVE, 1 },
+ { AE_FMT_S32LE, 32, 4, 1, 1, 1 },
+ { AE_FMT_S32BE, 32, 4, 1, 0, 1 },
+
+ { AE_FMT_S24NE4, 24, 4, 1, SIO_LE_NATIVE, 0 },
+ { AE_FMT_S24NE4, 24, 4, 1, SIO_LE_NATIVE, 1 },
+ { AE_FMT_S24NE3, 24, 3, 1, SIO_LE_NATIVE, 0 },
+ { AE_FMT_S24NE3, 24, 3, 1, SIO_LE_NATIVE, 1 },
+
+ { AE_FMT_S16NE, 16, 2, 1, SIO_LE_NATIVE, 1 },
+ { AE_FMT_S16NE, 16, 2, 1, SIO_LE_NATIVE, 0 },
+ { AE_FMT_S16LE, 16, 2, 1, 1, 1 },
+ { AE_FMT_S16LE, 16, 2, 1, 1, 0 },
+ { AE_FMT_S16BE, 16, 2, 1, 0, 1 },
+ { AE_FMT_S16BE, 16, 2, 1, 0, 0 },
+
+ { AE_FMT_U8, 8, 1, 0, 0, 0 },
+ { AE_FMT_U8, 8, 1, 0, 0, 1 },
+ { AE_FMT_U8, 8, 1, 0, 1, 0 },
+ { AE_FMT_U8, 8, 1, 0, 1, 1 },
+};
+
+static AEDataFormat lookupDataFormat(unsigned int bits, unsigned int bps,
+ unsigned int sig, unsigned int le, unsigned int msb)
+{
+ for (const sndio_formats& format : formats)
+ {
+ if (bits == format.bits &&
+ bps == format.bps &&
+ sig == format.sig &&
+ le == format.le &&
+ msb == format.msb)
+ {
+ return format.fmt;
+ }
+ }
+ return AE_FMT_INVALID;
+}
+
+void CAESinkSNDIO::AudioFormatToPar(AEAudioFormat& format)
+{
+ sio_initpar(&m_par);
+
+ m_par.rate = format.m_sampleRate;
+ m_par.xrun = SIO_IGNORE;
+ m_par.pchan = format.m_channelLayout.Count();
+
+ for (const sndio_formats& f : formats)
+ {
+ if (f.fmt == format.m_dataFormat)
+ {
+ m_par.bits = f.bits;
+ m_par.sig = f.sig;
+ m_par.le = f.le;
+ m_par.msb = f.msb;
+ m_par.bps = f.bps;
+ return;
+ }
+ }
+
+ /* Default to AE_FMT_S16NE */
+ m_par.bits = 16;
+ m_par.bps = 2;
+ m_par.sig = 1;
+ m_par.le = SIO_LE_NATIVE;
+}
+
+bool CAESinkSNDIO::ParToAudioFormat(AEAudioFormat& format)
+{
+ AEDataFormat dataFormat = lookupDataFormat(m_par.bits, m_par.bps, m_par.sig, m_par.le, m_par.msb);
+ if (dataFormat == AE_FMT_INVALID)
+ {
+ CLog::Log(LOGERROR, "CAESinkSNDIO::ParToAudioFormat - invalid data format");
+ return false;
+ }
+
+ if (m_par.pchan > nitems(channelMap))
+ {
+ CLog::Log(LOGERROR, "CAESinkSNDIO::ParToAudioFormat - too many channels: {}", m_par.pchan);
+ return false;
+ }
+
+ CAEChannelInfo info;
+ for (unsigned int i = 0; i < m_par.pchan; i++)
+ info += channelMap[i];
+ format.m_channelLayout = info;
+ format.m_dataFormat = dataFormat;
+ format.m_sampleRate = m_par.rate;
+ format.m_frameSize = m_par.bps * m_par.pchan;
+ format.m_frames = m_par.bufsz / format.m_frameSize;
+
+ return true;
+}
+
+CAESinkSNDIO::CAESinkSNDIO()
+{
+ m_hdl = nullptr;
+}
+
+CAESinkSNDIO::~CAESinkSNDIO()
+{
+ Deinitialize();
+}
+
+void CAESinkSNDIO::Register()
+{
+ AE::AESinkRegEntry entry;
+ entry.sinkName = "SNDIO";
+ entry.createFunc = CAESinkSNDIO::Create;
+ entry.enumerateFunc = CAESinkSNDIO::EnumerateDevicesEx;
+ AE::CAESinkFactory::RegisterSink(entry);
+}
+
+IAESink* CAESinkSNDIO::Create(std::string &device, AEAudioFormat& desiredFormat)
+{
+ IAESink* sink = new CAESinkSNDIO();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+
+bool CAESinkSNDIO::Initialize(AEAudioFormat &format, std::string &device)
+{
+ if ((m_hdl = sio_open(SIO_DEVANY, SIO_PLAY, 0)) == nullptr)
+ {
+ CLog::Log(LOGERROR, "CAESinkSNDIO::Initialize - Failed to open device");
+ return false;
+ }
+
+ AudioFormatToPar(format);
+ if (!sio_setpar(m_hdl, &m_par) ||
+ !sio_getpar(m_hdl, &m_par) ||
+ !ParToAudioFormat(format))
+ {
+ CLog::Log(LOGERROR, "CAESinkSNDIO::Initialize - could not negotiate parameters");
+ return false;
+ }
+
+ m_played = m_written = 0;
+
+ sio_onmove(m_hdl, CAESinkSNDIO::OnmoveCb, this);
+
+ if (!sio_start(m_hdl))
+ {
+ CLog::Log(LOGERROR, "CAESinkSNDIO::Initialize - sio_start failed");
+ return false;
+ }
+
+ return true;
+}
+
+void CAESinkSNDIO::Deinitialize()
+{
+ if (m_hdl != nullptr)
+ {
+ sio_close(m_hdl);
+ m_hdl = nullptr;
+ }
+}
+
+void CAESinkSNDIO::Stop()
+{
+ if (!m_hdl)
+ return;
+
+ if (!sio_stop(m_hdl))
+ CLog::Log(LOGERROR, "CAESinkSNDIO::Stop - Failed");
+
+ m_written = m_played = 0;
+}
+
+void CAESinkSNDIO::OnmoveCb(void *arg, int delta) {
+ CAESinkSNDIO* self = static_cast<CAESinkSNDIO*>(arg);
+ self->m_played += delta;
+}
+
+void CAESinkSNDIO::GetDelay(AEDelayStatus& status)
+{
+ unsigned int frameSize = m_par.bps * m_par.pchan;
+ double delay = 1.0 * ((m_written / frameSize) - m_played) / m_par.rate;
+ status.SetDelay(delay);
+}
+
+unsigned int CAESinkSNDIO::AddPackets(uint8_t **data, unsigned int frames, unsigned int offset)
+{
+ if (!m_hdl)
+ return INT_MAX;
+
+ unsigned int frameSize = m_par.bps * m_par.pchan;
+ size_t size = frames * frameSize;
+ void *buffer = data[0] + offset * frameSize;
+ size_t wrote = sio_write(m_hdl, buffer, size);
+ m_written += wrote;
+ return wrote / frameSize;
+}
+
+void CAESinkSNDIO::Drain()
+{
+ if(!m_hdl)
+ return;
+
+ if (!sio_stop(m_hdl) || !sio_start(m_hdl))
+ CLog::Log(LOGERROR, "CAESinkSNDIO::Drain - failed");
+
+ m_written = m_played = 0;
+}
+
+void CAESinkSNDIO::EnumerateDevicesEx(AEDeviceInfoList &list, bool force)
+{
+ struct sio_hdl *hdl;
+ struct sio_cap cap;
+
+ if ((hdl = sio_open(SIO_DEVANY, SIO_PLAY, 0)) == nullptr)
+ {
+ CLog::Log(LOGERROR, "CAESinkSNDIO::EnumerateDevicesEx - sio_open");
+ return;
+ }
+
+ if (!sio_getcap(hdl, &cap))
+ {
+ CLog::Log(LOGERROR, "CAESinkSNDIO::EnumerateDevicesEx - sio_getcap");
+ return;
+ }
+
+ sio_close(hdl);
+ hdl = nullptr;
+
+ for (unsigned int i = 0; i < cap.nconf; i++)
+ {
+ CAEDeviceInfo info;
+ sio_cap::sio_conf conf = cap.confs[i];
+
+ info.m_deviceName = SIO_DEVANY;
+ info.m_displayName = "sndio";
+ info.m_displayNameExtra = "#" + std::to_string(i);
+ info.m_deviceType = AE_DEVTYPE_PCM;
+ info.m_wantsIECPassthrough = false;
+
+ unsigned int maxchan = 0;
+ for (unsigned int j = 0; j < SIO_NCHAN; j++)
+ {
+ if (conf.pchan & (1 << j))
+ maxchan = MAX(maxchan, cap.pchan[j]);
+ }
+
+ maxchan = MIN(maxchan, nitems(channelMap));
+ for (unsigned int j = 0; j < maxchan; j++)
+ info.m_channels += channelMap[j];
+
+ for (unsigned int j = 0; j < SIO_NRATE; j++)
+ {
+ if (conf.rate & (1 << j))
+ {
+ info.m_sampleRates.push_back(cap.rate[j]);
+ }
+ }
+
+ for (unsigned int j = 0; j < SIO_NENC; j++)
+ {
+ if (conf.enc & (1 << j))
+ {
+ AEDataFormat format = lookupDataFormat(cap.enc[j].bits, cap.enc[j].bps, cap.enc[j].sig, cap.enc[j].le, cap.enc[j].msb);
+ if (format != AE_FMT_INVALID)
+ info.m_dataFormats.push_back(format);
+ }
+ }
+
+ list.push_back(info);
+ }
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkSNDIO.h b/xbmc/cores/AudioEngine/Sinks/AESinkSNDIO.h
new file mode 100644
index 0000000..668a3b7
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkSNDIO.h
@@ -0,0 +1,49 @@
+/*
+ * 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 "cores/AudioEngine/Interfaces/AESink.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+#include "threads/CriticalSection.h"
+
+#include <stdint.h>
+
+#include <sndio.h>
+
+class CAESinkSNDIO : public IAESink
+{
+public:
+ const char *GetName() override { return "sndio"; }
+
+ CAESinkSNDIO();
+ ~CAESinkSNDIO() override;
+
+ static void Register();
+ static IAESink* Create(std::string &device, AEAudioFormat &desiredFormat);
+ static void EnumerateDevicesEx(AEDeviceInfoList &list, bool force = false);
+
+ bool Initialize(AEAudioFormat &format, std::string &device) override;
+ void Deinitialize() override;
+
+ virtual void Stop();
+ void GetDelay(AEDelayStatus& status) override;
+ double GetCacheTotal() override { return 0.0; }
+ unsigned int AddPackets(uint8_t **data, unsigned int frames, unsigned int offset) override;
+ void Drain() override;
+private:
+ void AudioFormatToPar(AEAudioFormat& format);
+ bool ParToAudioFormat(AEAudioFormat& format);
+ static void OnmoveCb(void *arg, int delta);
+
+ struct sio_hdl *m_hdl;
+ struct sio_par m_par;
+ ssize_t m_played;
+ ssize_t m_written;
+};
+
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkWASAPI.cpp b/xbmc/cores/AudioEngine/Sinks/AESinkWASAPI.cpp
new file mode 100644
index 0000000..cbbf084
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkWASAPI.cpp
@@ -0,0 +1,1002 @@
+/*
+ * 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 "AESinkWASAPI.h"
+
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/TimeUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <stdint.h>
+
+#include <Audioclient.h>
+#include <Mmreg.h>
+
+#ifdef TARGET_WINDOWS_DESKTOP
+# pragma comment(lib, "Avrt.lib")
+#endif // TARGET_WINDOWS_DESKTOP
+
+const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);
+const IID IID_IAudioClock = __uuidof(IAudioClock);
+DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14);
+DEFINE_PROPERTYKEY(PKEY_Device_EnumeratorName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 24);
+
+extern const char *WASAPIErrToStr(HRESULT err);
+#define EXIT_ON_FAILURE(hr, reason) \
+ if (FAILED(hr)) \
+ { \
+ CLog::LogF(LOGERROR, reason " - HRESULT = {} ErrorMessage = {}", hr, WASAPIErrToStr(hr)); \
+ goto failed; \
+ }
+
+template<class T>
+inline void SafeRelease(T **ppT)
+{
+ if (*ppT)
+ {
+ (*ppT)->Release();
+ *ppT = nullptr;
+ }
+}
+
+using namespace Microsoft::WRL;
+
+CAESinkWASAPI::CAESinkWASAPI()
+{
+ m_channelLayout.Reset();
+}
+
+CAESinkWASAPI::~CAESinkWASAPI()
+{
+}
+
+void CAESinkWASAPI::Register()
+{
+ AE::AESinkRegEntry reg;
+ reg.sinkName = "WASAPI";
+ reg.createFunc = CAESinkWASAPI::Create;
+ reg.enumerateFunc = CAESinkWASAPI::EnumerateDevicesEx;
+ AE::CAESinkFactory::RegisterSink(reg);
+}
+
+IAESink* CAESinkWASAPI::Create(std::string &device, AEAudioFormat &desiredFormat)
+{
+ IAESink *sink = new CAESinkWASAPI();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+
+bool CAESinkWASAPI::Initialize(AEAudioFormat &format, std::string &device)
+{
+ if (m_initialized)
+ return false;
+
+ m_device = device;
+ bool bdefault = false;
+ HRESULT hr = S_FALSE;
+
+ /* Save requested format */
+ /* Clear returned format */
+ sinkReqFormat = format.m_dataFormat;
+ sinkRetFormat = AE_FMT_INVALID;
+
+ if(StringUtils::EndsWithNoCase(device, std::string("default")))
+ bdefault = true;
+
+ if(!bdefault)
+ {
+ hr = CAESinkFactoryWin::ActivateWASAPIDevice(device, &m_pDevice);
+ EXIT_ON_FAILURE(hr, "Retrieval of WASAPI endpoint failed.")
+ }
+
+ if (!m_pDevice)
+ {
+ if(!bdefault)
+ {
+ CLog::LogF(LOGINFO,
+ "Could not locate the device named \"{}\" in the list of WASAPI endpoint devices. "
+ " Trying the default device...",
+ device);
+ }
+
+ std::string defaultId = CAESinkFactoryWin::GetDefaultDeviceId();
+ if (defaultId.empty())
+ {
+ CLog::LogF(LOGINFO, "Could not locate the default device id in the list of WASAPI endpoint devices.");
+ goto failed;
+ }
+
+ hr = CAESinkFactoryWin::ActivateWASAPIDevice(defaultId, &m_pDevice);
+ EXIT_ON_FAILURE(hr, "Could not retrieve the default WASAPI audio endpoint.")
+
+ device = defaultId;
+ }
+
+ hr = m_pDevice->Activate(m_pAudioClient.ReleaseAndGetAddressOf());
+ EXIT_ON_FAILURE(hr, "Activating the WASAPI endpoint device failed.")
+
+ if (!InitializeExclusive(format))
+ {
+ CLog::LogF(LOGINFO, "Could not Initialize Exclusive with that format");
+ goto failed;
+ }
+
+ /* get the buffer size and calculate the frames for AE */
+ m_pAudioClient->GetBufferSize(&m_uiBufferLen);
+
+ format.m_frames = m_uiBufferLen;
+ m_format = format;
+ sinkRetFormat = format.m_dataFormat;
+
+ hr = m_pAudioClient->GetService(IID_IAudioRenderClient, reinterpret_cast<void**>(m_pRenderClient.ReleaseAndGetAddressOf()));
+ EXIT_ON_FAILURE(hr, "Could not initialize the WASAPI render client interface.")
+
+ hr = m_pAudioClient->GetService(IID_IAudioClock, reinterpret_cast<void**>(m_pAudioClock.ReleaseAndGetAddressOf()));
+ EXIT_ON_FAILURE(hr, "Could not initialize the WASAPI audio clock interface.")
+
+ hr = m_pAudioClock->GetFrequency(&m_clockFreq);
+ EXIT_ON_FAILURE(hr, "Retrieval of IAudioClock::GetFrequency failed.")
+
+ m_needDataEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
+ hr = m_pAudioClient->SetEventHandle(m_needDataEvent);
+ EXIT_ON_FAILURE(hr, "Could not set the WASAPI event handler.");
+
+ m_initialized = true;
+ m_isDirty = false;
+
+ // allow feeding less samples than buffer size
+ // if the device is opened exclusive and event driven, provided samples must match buffersize
+ // ActiveAE tries to align provided samples with buffer size but cannot guarantee (e.g. transcoding)
+ // this can be avoided by dropping the event mode which has not much benefit; SoftAE polls anyway
+ m_buffer.resize(format.m_frames * format.m_frameSize);
+ m_bufferPtr = 0;
+
+ return true;
+
+failed:
+ CLog::LogF(LOGERROR, "WASAPI initialization failed.");
+ SafeRelease(&m_pDevice);
+ if(m_needDataEvent)
+ {
+ CloseHandle(m_needDataEvent);
+ m_needDataEvent = 0;
+ }
+
+ return false;
+}
+
+void CAESinkWASAPI::Deinitialize()
+{
+ if (!m_initialized && !m_isDirty)
+ return;
+
+ if (m_running)
+ {
+ try
+ {
+ m_pAudioClient->Stop(); //stop the audio output
+ m_pAudioClient->Reset(); //flush buffer and reset audio clock stream position
+ m_sinkFrames = 0;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGDEBUG, "Invalidated AudioClient - Releasing");
+ }
+ }
+ m_running = false;
+
+ CloseHandle(m_needDataEvent);
+
+ m_pRenderClient = nullptr;
+ m_pAudioClient = nullptr;
+ m_pAudioClock = nullptr;
+ SafeRelease(&m_pDevice);
+
+ m_initialized = false;
+
+ m_bufferPtr = 0;
+}
+
+/**
+ * @brief rescale uint64_t without overflowing on large values
+ */
+static uint64_t rescale_u64(uint64_t val, uint64_t num, uint64_t den)
+{
+ return ((val / den) * num) + (((val % den) * num) / den);
+}
+
+
+void CAESinkWASAPI::GetDelay(AEDelayStatus& status)
+{
+ HRESULT hr;
+ uint64_t pos, tick;
+ int retries = 0;
+
+ if (!m_initialized)
+ goto failed;
+
+ do {
+ hr = m_pAudioClock->GetPosition(&pos, &tick);
+ } while (hr != S_OK && ++retries < 100);
+ EXIT_ON_FAILURE(hr, "Retrieval of IAudioClock::GetPosition failed.")
+
+ status.delay = (double)(m_sinkFrames + m_bufferPtr) / m_format.m_sampleRate - (double)pos / m_clockFreq;
+ status.tick = rescale_u64(tick, CurrentHostFrequency(), 10000000); /* convert from 100ns back to qpc ticks */
+ return;
+failed:
+ status.SetDelay(0);
+}
+
+double CAESinkWASAPI::GetCacheTotal()
+{
+ if (!m_initialized)
+ return 0.0;
+
+ return m_sinkLatency;
+}
+
+unsigned int CAESinkWASAPI::AddPackets(uint8_t **data, unsigned int frames, unsigned int offset)
+{
+ if (!m_initialized)
+ return 0;
+
+ HRESULT hr;
+ BYTE *buf;
+ DWORD flags = 0;
+
+#ifndef _DEBUG
+ LARGE_INTEGER timerStart;
+ LARGE_INTEGER timerStop;
+ LARGE_INTEGER timerFreq;
+#endif
+
+ unsigned int NumFramesRequested = m_format.m_frames;
+ unsigned int FramesToCopy = std::min(m_format.m_frames - m_bufferPtr, frames);
+ uint8_t *buffer = data[0]+offset*m_format.m_frameSize;
+ if (m_bufferPtr != 0 || frames != m_format.m_frames)
+ {
+ memcpy(m_buffer.data() + m_bufferPtr * m_format.m_frameSize, buffer,
+ FramesToCopy * m_format.m_frameSize);
+ m_bufferPtr += FramesToCopy;
+ if (m_bufferPtr != m_format.m_frames)
+ return frames;
+ }
+
+ if (!m_running) //first time called, pre-fill buffer then start audio client
+ {
+ hr = m_pAudioClient->Reset();
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, " AudioClient reset failed due to {}", WASAPIErrToStr(hr));
+ return 0;
+ }
+ hr = m_pRenderClient->GetBuffer(NumFramesRequested, &buf);
+ if (FAILED(hr))
+ {
+ #ifdef _DEBUG
+ CLog::LogF(LOGERROR, "GetBuffer failed due to {}", WASAPIErrToStr(hr));
+#endif
+ m_isDirty = true; //flag new device or re-init needed
+ return INT_MAX;
+ }
+
+ memset(buf, 0, NumFramesRequested * m_format.m_frameSize); //fill buffer with silence
+
+ hr = m_pRenderClient->ReleaseBuffer(NumFramesRequested, flags); //pass back to audio driver
+ if (FAILED(hr))
+ {
+ #ifdef _DEBUG
+ CLog::LogF(LOGDEBUG, "ReleaseBuffer failed due to {}.", WASAPIErrToStr(hr));
+#endif
+ m_isDirty = true; //flag new device or re-init needed
+ return INT_MAX;
+ }
+ m_sinkFrames += NumFramesRequested;
+
+ hr = m_pAudioClient->Start(); //start the audio driver running
+ if (FAILED(hr))
+ CLog::LogF(LOGERROR, "AudioClient Start Failed");
+ m_running = true; //signal that we're processing frames
+ return 0U;
+ }
+
+#ifndef _DEBUG
+ /* Get clock time for latency checks */
+ QueryPerformanceFrequency(&timerFreq);
+ QueryPerformanceCounter(&timerStart);
+#endif
+
+ /* Wait for Audio Driver to tell us it's got a buffer available */
+ DWORD eventAudioCallback;
+ eventAudioCallback = WaitForSingleObject(m_needDataEvent, 1100);
+
+ if(eventAudioCallback != WAIT_OBJECT_0 || !&buf)
+ {
+ CLog::LogF(LOGERROR, "Endpoint Buffer timed out");
+ return INT_MAX;
+ }
+
+ if (!m_running)
+ return 0;
+
+#ifndef _DEBUG
+ QueryPerformanceCounter(&timerStop);
+ LONGLONG timerDiff = timerStop.QuadPart - timerStart.QuadPart;
+ double timerElapsed = (double) timerDiff * 1000.0 / (double) timerFreq.QuadPart;
+ m_avgTimeWaiting += (timerElapsed - m_avgTimeWaiting) * 0.5;
+
+ if (m_avgTimeWaiting < 3.0)
+ {
+ CLog::LogF(LOGDEBUG, "Possible AQ Loss: Avg. Time Waiting for Audio Driver callback : {}msec",
+ (int)m_avgTimeWaiting);
+ }
+#endif
+
+ hr = m_pRenderClient->GetBuffer(NumFramesRequested, &buf);
+ if (FAILED(hr))
+ {
+#ifdef _DEBUG
+ CLog::LogF(LOGERROR, "GetBuffer failed due to {}", WASAPIErrToStr(hr));
+#endif
+ return INT_MAX;
+ }
+
+ // fill buffer
+ memcpy(buf, m_bufferPtr == 0 ? buffer : m_buffer.data(),
+ NumFramesRequested * m_format.m_frameSize);
+ m_bufferPtr = 0;
+
+ hr = m_pRenderClient->ReleaseBuffer(NumFramesRequested, flags); //pass back to audio driver
+ if (FAILED(hr))
+ {
+#ifdef _DEBUG
+ CLog::LogF(LOGDEBUG, "ReleaseBuffer failed due to {}.", WASAPIErrToStr(hr));
+#endif
+ return INT_MAX;
+ }
+ m_sinkFrames += NumFramesRequested;
+
+ if (FramesToCopy != frames)
+ {
+ m_bufferPtr = frames-FramesToCopy;
+ memcpy(m_buffer.data(), buffer + FramesToCopy * m_format.m_frameSize,
+ m_bufferPtr * m_format.m_frameSize);
+ }
+
+ return frames;
+}
+
+void CAESinkWASAPI::EnumerateDevicesEx(AEDeviceInfoList &deviceInfoList, bool force)
+{
+ CAEDeviceInfo deviceInfo;
+ CAEChannelInfo deviceChannels;
+ bool add192 = false;
+ bool add48 = false;
+
+ WAVEFORMATEXTENSIBLE wfxex = {};
+ HRESULT hr;
+
+ const bool onlyPT = (CSysInfo::GetWindowsDeviceFamily() == CSysInfo::WindowsDeviceFamily::Xbox);
+
+ for(RendererDetail& details : CAESinkFactoryWin::GetRendererDetails())
+ {
+ deviceInfo.m_channels.Reset();
+ deviceInfo.m_dataFormats.clear();
+ deviceInfo.m_sampleRates.clear();
+ deviceChannels.Reset();
+
+ for (unsigned int c = 0; c < WASAPI_SPEAKER_COUNT; c++)
+ {
+ if (details.uiChannelMask & WASAPIChannelOrder[c])
+ deviceChannels += AEChannelNames[c];
+ }
+
+ IAEWASAPIDevice* pDevice;
+ hr = CAESinkFactoryWin::ActivateWASAPIDevice(details.strDeviceId, &pDevice);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Retrieval of WASAPI endpoint failed.");
+ goto failed;
+ }
+
+ ComPtr<IAudioClient> pClient = nullptr;
+ hr = pDevice->Activate(pClient.GetAddressOf());
+ if (SUCCEEDED(hr))
+ {
+ /* Test format DTS-HD-HR */
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE)-sizeof(WAVEFORMATEX);
+ wfxex.Format.nSamplesPerSec = 192000;
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_5POINT1;
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD;
+ wfxex.Format.wBitsPerSample = 16;
+ wfxex.Samples.wValidBitsPerSample = 16;
+ wfxex.Format.nChannels = 2;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+ hr = pClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &wfxex.Format, NULL);
+ if (hr == AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED)
+ {
+ CLog::LogF(LOGINFO,
+ "Exclusive mode is not allowed on device \"{}\", check device settings.",
+ details.strDescription);
+ SafeRelease(&pDevice);
+ continue;
+ }
+ if (SUCCEEDED(hr) || details.eDeviceType == AE_DEVTYPE_HDMI)
+ {
+ if(FAILED(hr))
+ {
+ CLog::LogF(LOGINFO, "stream type \"{}\" on device \"{}\" seems to be not supported.",
+ CAEUtil::StreamTypeToStr(CAEStreamInfo::STREAM_TYPE_DTSHD),
+ details.strDescription);
+ }
+
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD);
+ add192 = true;
+ }
+
+ /* Test format DTS-HD */
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE)-sizeof(WAVEFORMATEX);
+ wfxex.Format.nSamplesPerSec = 192000;
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD;
+ wfxex.Format.wBitsPerSample = 16;
+ wfxex.Samples.wValidBitsPerSample = 16;
+ wfxex.Format.nChannels = 8;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+ hr = pClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &wfxex.Format, NULL);
+ if (hr == AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED)
+ {
+ CLog::LogF(LOGINFO,
+ "Exclusive mode is not allowed on device \"{}\", check device settings.",
+ details.strDescription);
+ SafeRelease(&pDevice);
+ continue;
+ }
+ if (SUCCEEDED(hr) || details.eDeviceType == AE_DEVTYPE_HDMI)
+ {
+ if(FAILED(hr))
+ {
+ CLog::LogF(LOGINFO, "stream type \"{}\" on device \"{}\" seems to be not supported.",
+ CAEUtil::StreamTypeToStr(CAEStreamInfo::STREAM_TYPE_DTSHD_MA),
+ details.strDescription);
+ }
+
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_MA);
+ add192 = true;
+ }
+
+ /* Test format Dolby TrueHD */
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP;
+ hr = pClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &wfxex.Format, NULL);
+ if (SUCCEEDED(hr) || details.eDeviceType == AE_DEVTYPE_HDMI)
+ {
+ if(FAILED(hr))
+ {
+ CLog::LogF(LOGINFO, "stream type \"{}\" on device \"{}\" seems to be not supported.",
+ CAEUtil::StreamTypeToStr(CAEStreamInfo::STREAM_TYPE_TRUEHD),
+ details.strDescription);
+ }
+
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_TRUEHD);
+ add192 = true;
+ }
+
+ /* Test format Dolby EAC3 */
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS;
+ wfxex.Format.nChannels = 2;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+ hr = pClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &wfxex.Format, NULL);
+ if (SUCCEEDED(hr) || details.eDeviceType == AE_DEVTYPE_HDMI)
+ {
+ if(FAILED(hr))
+ {
+ CLog::LogF(LOGINFO, "stream type \"{}\" on device \"{}\" seems to be not supported.",
+ CAEUtil::StreamTypeToStr(CAEStreamInfo::STREAM_TYPE_EAC3),
+ details.strDescription);
+ }
+
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_EAC3);
+ add192 = true;
+ }
+
+ /* Test format DTS */
+ wfxex.Format.nSamplesPerSec = 48000;
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_5POINT1;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DTS;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+ hr = pClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &wfxex.Format, NULL);
+ if (SUCCEEDED(hr) || details.eDeviceType == AE_DEVTYPE_HDMI)
+ {
+ if(FAILED(hr))
+ {
+ CLog::LogF(LOGINFO, "stream type \"{}\" on device \"{}\" seems to be not supported.",
+ "STREAM_TYPE_DTS", details.strDescription);
+ }
+
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ add48 = true;
+ }
+
+ /* Test format Dolby AC3 */
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL;
+ hr = pClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &wfxex.Format, NULL);
+ if (SUCCEEDED(hr) || details.eDeviceType == AE_DEVTYPE_HDMI)
+ {
+ if(FAILED(hr))
+ {
+ CLog::LogF(LOGINFO, "stream type \"{}\" on device \"{}\" seems to be not supported.",
+ CAEUtil::StreamTypeToStr(CAEStreamInfo::STREAM_TYPE_AC3),
+ details.strDescription);
+ }
+
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ add48 = true;
+ }
+
+ /* Test format for PCM format iteration */
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE)-sizeof(WAVEFORMATEX);
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_STEREO;
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+
+ for (int p = AE_FMT_FLOAT; p > AE_FMT_INVALID; p--)
+ {
+ if (p < AE_FMT_FLOAT)
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+ wfxex.Format.wBitsPerSample = CAEUtil::DataFormatToBits((AEDataFormat) p);
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+ if (p == AE_FMT_S24NE4MSB)
+ {
+ wfxex.Samples.wValidBitsPerSample = 24;
+ }
+ else if (p <= AE_FMT_S24NE4 && p >= AE_FMT_S24BE4)
+ {
+ // not supported
+ continue;
+ }
+ else
+ {
+ wfxex.Samples.wValidBitsPerSample = wfxex.Format.wBitsPerSample;
+ }
+
+ hr = pClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &wfxex.Format, NULL);
+ if (SUCCEEDED(hr))
+ deviceInfo.m_dataFormats.push_back((AEDataFormat) p);
+ }
+
+ /* Test format for sample rate iteration */
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE)-sizeof(WAVEFORMATEX);
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_STEREO;
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+ wfxex.Format.wBitsPerSample = 16;
+ wfxex.Samples.wValidBitsPerSample = 16;
+ wfxex.Format.nChannels = 2;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+
+ for (int j = 0; j < WASAPISampleRateCount; j++)
+ {
+ wfxex.Format.nSamplesPerSec = WASAPISampleRates[j];
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+ hr = pClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &wfxex.Format, NULL);
+ if (SUCCEEDED(hr))
+ deviceInfo.m_sampleRates.push_back(WASAPISampleRates[j]);
+ else if (wfxex.Format.nSamplesPerSec == 192000 && add192)
+ {
+ deviceInfo.m_sampleRates.push_back(WASAPISampleRates[j]);
+ CLog::LogF(LOGINFO, "sample rate 192khz on device \"{}\" seems to be not supported.",
+ details.strDescription);
+ }
+ else if (wfxex.Format.nSamplesPerSec == 48000 && add48)
+ {
+ deviceInfo.m_sampleRates.push_back(WASAPISampleRates[j]);
+ CLog::LogF(LOGINFO, "sample rate 48khz on device \"{}\" seems to be not supported.",
+ details.strDescription);
+ }
+ }
+ pClient = nullptr;
+ }
+ else
+ {
+ CLog::LogF(LOGDEBUG, "Failed to activate device for passthrough capability testing.");
+ }
+
+ deviceInfo.m_deviceName = details.strDeviceId;
+ deviceInfo.m_displayName = details.strWinDevType.append(details.strDescription);
+ deviceInfo.m_displayNameExtra = std::string("WASAPI: ").append(details.strDescription);
+ deviceInfo.m_deviceType = details.eDeviceType;
+ deviceInfo.m_channels = deviceChannels;
+
+ /* Store the device info */
+ deviceInfo.m_wantsIECPassthrough = true;
+ deviceInfo.m_onlyPassthrough = onlyPT;
+
+ if (!deviceInfo.m_streamTypes.empty())
+ deviceInfo.m_dataFormats.push_back(AE_FMT_RAW);
+
+ deviceInfoList.push_back(deviceInfo);
+
+ if (details.bDefault)
+ {
+ deviceInfo.m_deviceName = std::string("default");
+ deviceInfo.m_displayName = std::string("default");
+ deviceInfo.m_displayNameExtra = std::string("");
+ deviceInfo.m_wantsIECPassthrough = true;
+ deviceInfo.m_onlyPassthrough = onlyPT;
+ deviceInfoList.push_back(deviceInfo);
+ }
+
+ SafeRelease(&pDevice);
+ }
+ return;
+
+failed:
+
+ if (FAILED(hr))
+ CLog::LogF(LOGERROR, "Failed to enumerate WASAPI endpoint devices ({}).", WASAPIErrToStr(hr));
+}
+
+//Private utility functions////////////////////////////////////////////////////
+
+void CAESinkWASAPI::BuildWaveFormatExtensibleIEC61397(AEAudioFormat &format, WAVEFORMATEXTENSIBLE_IEC61937 &wfxex)
+{
+ /* Fill the common structure */
+ CAESinkFactoryWin::BuildWaveFormatExtensible(format, wfxex.FormatExt);
+
+ /* Code below kept for future use - preferred for later Windows versions */
+ /* but can cause problems on older Windows versions and drivers */
+ /*
+ wfxex.FormatExt.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE_IEC61937)-sizeof(WAVEFORMATEX);
+ wfxex.dwEncodedChannelCount = format.m_channelLayout.Count();
+ wfxex.dwEncodedSamplesPerSec = bool(format.m_dataFormat == AE_FMT_TRUEHD ||
+ format.m_dataFormat == AE_FMT_DTSHD ||
+ format.m_dataFormat == AE_FMT_EAC3) ? 96000L : 48000L;
+ wfxex.dwAverageBytesPerSec = 0; //Ignored */
+}
+
+bool CAESinkWASAPI::InitializeExclusive(AEAudioFormat &format)
+{
+ WAVEFORMATEXTENSIBLE_IEC61937 wfxex_iec61937;
+ WAVEFORMATEXTENSIBLE &wfxex = wfxex_iec61937.FormatExt;
+
+ if (format.m_dataFormat <= AE_FMT_FLOAT)
+ CAESinkFactoryWin::BuildWaveFormatExtensible(format, wfxex);
+ else if (format.m_dataFormat == AE_FMT_RAW)
+ BuildWaveFormatExtensibleIEC61397(format, wfxex_iec61937);
+ else
+ {
+ // planar formats are currently not supported by this sink
+ format.m_dataFormat = AE_FMT_FLOAT;
+ CAESinkFactoryWin::BuildWaveFormatExtensible(format, wfxex);
+ }
+
+ // Prevents NULL speaker mask. To do: debug exact cause.
+ // When this happens requested AE format is AE_FMT_FLOAT + channel layout
+ // RAW, RAW, RAW... (6 channels). Only happens at end of playback PT
+ // stream, force to defaults does not affect functionality or user
+ // experience. Only avoids crash.
+ if (!wfxex.dwChannelMask && format.m_dataFormat <= AE_FMT_FLOAT)
+ {
+ CLog::LogF(LOGWARNING, "NULL Channel Mask detected. Default values are enforced.");
+ format.m_sampleRate = 0; // force defaults in following code
+ }
+
+ /* Test for incomplete format and provide defaults */
+ if (format.m_sampleRate == 0 ||
+ format.m_channelLayout == CAEChannelInfo(nullptr) ||
+ format.m_dataFormat <= AE_FMT_INVALID ||
+ format.m_dataFormat >= AE_FMT_MAX ||
+ format.m_channelLayout.Count() == 0)
+ {
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.Format.nChannels = 2;
+ wfxex.Format.nSamplesPerSec = 44100L;
+ wfxex.Format.wBitsPerSample = 16;
+ wfxex.Format.nBlockAlign = 4;
+ wfxex.Samples.wValidBitsPerSample = 16;
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nBlockAlign * wfxex.Format.nSamplesPerSec;
+ wfxex.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+ }
+
+ HRESULT hr = m_pAudioClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &wfxex.Format, NULL);
+
+ int closestMatch = 0;
+ unsigned int requestedChannels = 0;
+ unsigned int noOfCh = 0;
+ uint64_t desired_map = 0;
+ bool matchNoChannelsOnly = false;
+
+ if (SUCCEEDED(hr))
+ {
+ CLog::LogF(LOGINFO, "Format is Supported - will attempt to Initialize");
+ goto initialize;
+ }
+ else if (hr != AUDCLNT_E_UNSUPPORTED_FORMAT) //It failed for a reason unrelated to an unsupported format.
+ {
+ CLog::LogF(LOGERROR, "IsFormatSupported failed ({})", WASAPIErrToStr(hr));
+ return false;
+ }
+ else if (format.m_dataFormat == AE_FMT_RAW) //No sense in trying other formats for passthrough.
+ return false;
+
+ CLog::LogFC(LOGDEBUG, LOGAUDIO,
+ "IsFormatSupported failed ({}) - trying to find a compatible format",
+ WASAPIErrToStr(hr));
+
+ requestedChannels = wfxex.Format.nChannels;
+ desired_map = CAESinkFactoryWin::SpeakerMaskFromAEChannels(format.m_channelLayout);
+
+ /* The requested format is not supported by the device. Find something that works */
+ CLog::Log(LOGDEBUG,
+ "AESinkWASAPI: Input channels are [{}] - Trying to find a matching output layout",
+ std::string(format.m_channelLayout));
+ for (int layout = -1; layout <= (int)ARRAYSIZE(layoutsList); layout++)
+ {
+ // if requested layout is not supported, try standard layouts which contain
+ // at least the same channels as the input source
+ // as the last resort try stereo
+ if (layout == ARRAYSIZE(layoutsList))
+ {
+ if (matchNoChannelsOnly)
+ {
+ wfxex.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
+ wfxex.Format.nChannels = 2;
+ }
+ else
+ {
+ matchNoChannelsOnly = true;
+ layout = -1;
+ CLog::Log(LOGWARNING, "AESinkWASAPI: Match only number of audio channels as fallback");
+ continue;
+ }
+ }
+ else if (layout >= 0)
+ {
+ wfxex.dwChannelMask = CAESinkFactoryWin::ChLayoutToChMask(layoutsList[layout], &noOfCh);
+ wfxex.Format.nChannels = noOfCh;
+ int res = desired_map & wfxex.dwChannelMask;
+ if (matchNoChannelsOnly)
+ {
+ if (noOfCh < requestedChannels)
+ continue; // number of channels doesn't match requested channels
+ }
+ else
+ {
+ if (res != desired_map)
+ continue; // output channel layout doesn't match input channels
+ }
+ }
+ CAEChannelInfo foundChannels;
+ CAESinkFactoryWin::AEChannelsFromSpeakerMask(foundChannels, wfxex.dwChannelMask);
+ CLog::Log(LOGDEBUG, "AESinkWASAPI: Trying matching channel layout [{}]",
+ std::string(foundChannels));
+
+ for (int j = 0; j < sizeof(testFormats)/sizeof(sampleFormat); j++)
+ {
+ closestMatch = -1;
+
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.SubFormat = testFormats[j].subFormat;
+ wfxex.Format.wBitsPerSample = testFormats[j].bitsPerSample;
+ wfxex.Samples.wValidBitsPerSample = testFormats[j].validBitsPerSample;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+
+ for (int i = 0 ; i < WASAPISampleRateCount; i++)
+ {
+ wfxex.Format.nSamplesPerSec = WASAPISampleRates[i];
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+
+ /* Trace format match iteration loop via log */
+#if 0
+ CLog::Log(LOGDEBUG, "WASAPI: Trying Format: {}, {}, {}, {}", CAEUtil::DataFormatToStr(testFormats[j].subFormatType),
+ wfxex.Format.nSamplesPerSec,
+ wfxex.Format.wBitsPerSample,
+ wfxex.Samples.wValidBitsPerSample);
+#endif
+
+ hr = m_pAudioClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, &wfxex.Format, NULL);
+
+ if (SUCCEEDED(hr))
+ {
+ /* If the current sample rate matches the source then stop looking and use it */
+ if ((WASAPISampleRates[i] == format.m_sampleRate) && (testFormats[j].subFormatType <= format.m_dataFormat))
+ goto initialize;
+ /* If this rate is closer to the source then the previous one, save it */
+ else if (closestMatch < 0 || abs((int)WASAPISampleRates[i] - (int)format.m_sampleRate) < abs((int)WASAPISampleRates[closestMatch] - (int)format.m_sampleRate))
+ closestMatch = i;
+ }
+ else if (hr != AUDCLNT_E_UNSUPPORTED_FORMAT)
+ CLog::LogF(LOGERROR, "IsFormatSupported failed ({})", WASAPIErrToStr(hr));
+ }
+
+ if (closestMatch >= 0)
+ {
+ wfxex.Format.nSamplesPerSec = WASAPISampleRates[closestMatch];
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+ goto initialize;
+ }
+ }
+ CLog::Log(LOGDEBUG, "AESinkWASAPI: Format [{}] not supported by driver",
+ std::string(foundChannels));
+ }
+
+ CLog::LogF(LOGERROR, "Unable to locate a supported output format for the device. "
+ "Check the speaker settings in the control panel.");
+
+ /* We couldn't find anything supported. This should never happen */
+ /* unless the user set the wrong speaker setting in the control panel */
+ return false;
+
+initialize:
+
+ CAESinkFactoryWin::AEChannelsFromSpeakerMask(m_channelLayout, wfxex.dwChannelMask);
+ format.m_channelLayout = m_channelLayout;
+
+ /* When the stream is raw, the values in the format structure are set to the link */
+ /* parameters, so store the encoded stream values here for the IsCompatible function */
+ m_encodedChannels = wfxex.Format.nChannels;
+ m_encodedSampleRate = (format.m_dataFormat == AE_FMT_RAW) ? format.m_streamInfo.m_sampleRate : format.m_sampleRate;
+ wfxex_iec61937.dwEncodedChannelCount = wfxex.Format.nChannels;
+ wfxex_iec61937.dwEncodedSamplesPerSec = m_encodedSampleRate;
+
+ /* Set up returned sink format for engine */
+ if (format.m_dataFormat != AE_FMT_RAW)
+ {
+ if (wfxex.Format.wBitsPerSample == 32)
+ {
+ if (wfxex.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
+ format.m_dataFormat = AE_FMT_FLOAT;
+ else if (wfxex.Samples.wValidBitsPerSample == 32)
+ format.m_dataFormat = AE_FMT_S32NE;
+ else
+ format.m_dataFormat = AE_FMT_S24NE4MSB;
+ }
+ else if (wfxex.Format.wBitsPerSample == 24)
+ format.m_dataFormat = AE_FMT_S24NE3;
+ else
+ format.m_dataFormat = AE_FMT_S16NE;
+ }
+
+ format.m_sampleRate = wfxex.Format.nSamplesPerSec; //PCM: Sample rate. RAW: Link speed
+ format.m_frameSize = (wfxex.Format.wBitsPerSample >> 3) * wfxex.Format.nChannels;
+
+ REFERENCE_TIME audioSinkBufferDurationMsec, hnsLatency;
+
+ audioSinkBufferDurationMsec = (REFERENCE_TIME)500000;
+ if (IsUSBDevice())
+ {
+ CLog::LogF(LOGDEBUG, "detected USB device, increasing buffer size");
+ audioSinkBufferDurationMsec = (REFERENCE_TIME)1000000;
+ }
+ audioSinkBufferDurationMsec = (REFERENCE_TIME)((audioSinkBufferDurationMsec / format.m_frameSize) * format.m_frameSize); //even number of frames
+
+ if (format.m_dataFormat == AE_FMT_RAW)
+ format.m_dataFormat = AE_FMT_S16NE;
+
+ hr = m_pAudioClient->Initialize(AUDCLNT_SHAREMODE_EXCLUSIVE, AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST,
+ audioSinkBufferDurationMsec, audioSinkBufferDurationMsec, &wfxex.Format, NULL);
+
+ if (hr == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED)
+ {
+ /* WASAPI requires aligned buffer */
+ /* Get the next aligned frame */
+ hr = m_pAudioClient->GetBufferSize(&m_uiBufferLen);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "GetBufferSize Failed : {}", WASAPIErrToStr(hr));
+ return false;
+ }
+
+ audioSinkBufferDurationMsec = (REFERENCE_TIME) ((10000.0 * 1000 / wfxex.Format.nSamplesPerSec * m_uiBufferLen) + 0.5);
+
+ /* Release the previous allocations */
+ /* Create a new audio client */
+ hr = m_pDevice->Activate(m_pAudioClient.ReleaseAndGetAddressOf());
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Device Activation Failed : {}", WASAPIErrToStr(hr));
+ return false;
+ }
+
+ /* Open the stream and associate it with an audio session */
+ hr = m_pAudioClient->Initialize(AUDCLNT_SHAREMODE_EXCLUSIVE, AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST,
+ audioSinkBufferDurationMsec, audioSinkBufferDurationMsec, &wfxex.Format, NULL);
+ }
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Failed to initialize WASAPI in exclusive mode {} - ({}).", HRESULT(hr),
+ WASAPIErrToStr(hr));
+ CLog::Log(LOGDEBUG, " Sample Rate : {}", wfxex.Format.nSamplesPerSec);
+ CLog::Log(LOGDEBUG, " Sample Format : {}", CAEUtil::DataFormatToStr(format.m_dataFormat));
+ CLog::Log(LOGDEBUG, " Bits Per Sample : {}", wfxex.Format.wBitsPerSample);
+ CLog::Log(LOGDEBUG, " Valid Bits/Samp : {}", wfxex.Samples.wValidBitsPerSample);
+ CLog::Log(LOGDEBUG, " Channel Count : {}", wfxex.Format.nChannels);
+ CLog::Log(LOGDEBUG, " Block Align : {}", wfxex.Format.nBlockAlign);
+ CLog::Log(LOGDEBUG, " Avg. Bytes Sec : {}", wfxex.Format.nAvgBytesPerSec);
+ CLog::Log(LOGDEBUG, " Samples/Block : {}", wfxex.Samples.wSamplesPerBlock);
+ CLog::Log(LOGDEBUG, " Format cBSize : {}", wfxex.Format.cbSize);
+ CLog::Log(LOGDEBUG, " Channel Layout : {}", ((std::string)format.m_channelLayout));
+ CLog::Log(LOGDEBUG, " Enc. Channels : {}", wfxex_iec61937.dwEncodedChannelCount);
+ CLog::Log(LOGDEBUG, " Enc. Samples/Sec: {}", wfxex_iec61937.dwEncodedSamplesPerSec);
+ CLog::Log(LOGDEBUG, " Channel Mask : {}", wfxex.dwChannelMask);
+ CLog::Log(LOGDEBUG, " Periodicty : {}", audioSinkBufferDurationMsec);
+ return false;
+ }
+
+ /* Latency of WASAPI buffers in event-driven mode is equal to the returned value */
+ /* of GetStreamLatency converted from 100ns intervals to seconds then multiplied */
+ /* by two as there are two equally-sized buffers and playback starts when the */
+ /* second buffer is filled. Multiplying the returned 100ns intervals by 0.0000002 */
+ /* is handles both the unit conversion and twin buffers. */
+ hr = m_pAudioClient->GetStreamLatency(&hnsLatency);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "GetStreamLatency Failed : {}", WASAPIErrToStr(hr));
+ return false;
+ }
+
+ m_sinkLatency = hnsLatency * 0.0000002;
+
+ CLog::LogF(LOGINFO, "WASAPI Exclusive Mode Sink Initialized using: {}, {}, {}",
+ CAEUtil::DataFormatToStr(format.m_dataFormat), wfxex.Format.nSamplesPerSec,
+ wfxex.Format.nChannels);
+ return true;
+}
+
+void CAESinkWASAPI::Drain()
+{
+ if(!m_pAudioClient)
+ return;
+
+ AEDelayStatus status;
+ GetDelay(status);
+
+ KODI::TIME::Sleep(std::chrono::milliseconds(static_cast<int>(status.GetDelay() * 500)));
+
+ if (m_running)
+ {
+ try
+ {
+ m_pAudioClient->Stop(); //stop the audio output
+ m_pAudioClient->Reset(); //flush buffer and reset audio clock stream position
+ m_sinkFrames = 0;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGDEBUG, "Invalidated AudioClient - Releasing");
+ }
+ }
+ m_running = false;
+}
+
+bool CAESinkWASAPI::IsUSBDevice()
+{
+ return m_pDevice && m_pDevice->IsUSBDevice();
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkWASAPI.h b/xbmc/cores/AudioEngine/Sinks/AESinkWASAPI.h
new file mode 100644
index 0000000..30826de
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkWASAPI.h
@@ -0,0 +1,76 @@
+/*
+ * 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 "cores/AudioEngine/Interfaces/AESink.h"
+#include "cores/AudioEngine/Sinks/windows/AESinkFactoryWin.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+
+#include <stdint.h>
+#include <vector>
+
+#include <Audioclient.h>
+#include <mmdeviceapi.h>
+#include <wrl/client.h>
+
+class CAESinkWASAPI : public IAESink
+{
+public:
+ CAESinkWASAPI();
+ virtual ~CAESinkWASAPI();
+
+ static void Register();
+ static IAESink* Create(std::string &device, AEAudioFormat &desiredFormat);
+ static void EnumerateDevicesEx(AEDeviceInfoList &deviceInfoList, bool force = false);
+
+ // IAESink overrides
+ const char *GetName() override { return "WASAPI"; }
+ 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;
+
+private:
+ bool InitializeExclusive(AEAudioFormat &format);
+ static void BuildWaveFormatExtensibleIEC61397(AEAudioFormat &format, WAVEFORMATEXTENSIBLE_IEC61937 &wfxex);
+ bool IsUSBDevice();
+
+ HANDLE m_needDataEvent{0};
+ IAEWASAPIDevice* m_pDevice{nullptr};
+ Microsoft::WRL::ComPtr<IAudioClient> m_pAudioClient;
+ Microsoft::WRL::ComPtr<IAudioRenderClient> m_pRenderClient;
+ Microsoft::WRL::ComPtr<IAudioClock> m_pAudioClock;
+
+ AEAudioFormat m_format{};
+ unsigned int m_encodedChannels{0};
+ unsigned int m_encodedSampleRate{0};
+ CAEChannelInfo m_channelLayout;
+ std::string m_device;
+
+ enum AEDataFormat sinkReqFormat = AE_FMT_INVALID;
+ enum AEDataFormat sinkRetFormat = AE_FMT_INVALID;
+
+ bool m_running{false};
+ bool m_initialized{false};
+ bool m_isSuspended{false}; // sink is in a suspended state - release audio device
+ bool m_isDirty{false}; // sink output failed - needs re-init or new device
+
+ // time between next buffer of data from SoftAE and driver call for data
+ double m_avgTimeWaiting{50.0};
+ double m_sinkLatency{0.0}; // time in seconds of total duration of the two WASAPI buffers
+
+ unsigned int m_uiBufferLen{0}; // wasapi endpoint buffer size, in frames
+ uint64_t m_sinkFrames{0};
+ uint64_t m_clockFreq{0};
+
+ std::vector<uint8_t> m_buffer;
+ int m_bufferPtr{0};
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkXAudio.cpp b/xbmc/cores/AudioEngine/Sinks/AESinkXAudio.cpp
new file mode 100644
index 0000000..c7c40ab
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkXAudio.cpp
@@ -0,0 +1,890 @@
+/*
+ * 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 "AESinkXAudio.h"
+
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Sinks/windows/AESinkFactoryWin.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include "platform/win10/AsyncHelpers.h"
+#include "platform/win32/CharsetConverter.h"
+
+#include <algorithm>
+#include <stdint.h>
+
+#include <ksmedia.h>
+#include <mfapi.h>
+#include <mmdeviceapi.h>
+#include <mmreg.h>
+#include <wrl/implements.h>
+
+using namespace Microsoft::WRL;
+
+extern const char *WASAPIErrToStr(HRESULT err);
+
+#define EXIT_ON_FAILURE(hr, reason, ...) \
+ if (FAILED(hr)) \
+ { \
+ CLog::Log(LOGERROR, reason " - {}", __VA_ARGS__, WASAPIErrToStr(hr)); \
+ goto failed; \
+ }
+#define XAUDIO_BUFFERS_IN_QUEUE 2
+
+template <class TVoice>
+inline void SafeDestroyVoice(TVoice **ppVoice)
+{
+ if (*ppVoice)
+ {
+ (*ppVoice)->DestroyVoice();
+ *ppVoice = nullptr;
+ }
+}
+
+/// ----------------- CAESinkXAudio ------------------------
+
+CAESinkXAudio::CAESinkXAudio() :
+ m_masterVoice(nullptr),
+ m_sourceVoice(nullptr),
+ m_encodedChannels(0),
+ m_encodedSampleRate(0),
+ sinkReqFormat(AE_FMT_INVALID),
+ sinkRetFormat(AE_FMT_INVALID),
+ m_AvgBytesPerSec(0),
+ m_dwChunkSize(0),
+ m_dwFrameSize(0),
+ m_dwBufferLen(0),
+ m_sinkFrames(0),
+ m_framesInBuffers(0),
+ m_running(false),
+ m_initialized(false),
+ m_isSuspended(false),
+ m_isDirty(false),
+ m_uiBufferLen(0),
+ m_avgTimeWaiting(50)
+{
+ m_channelLayout.Reset();
+
+ HRESULT hr = XAudio2Create(m_xAudio2.ReleaseAndGetAddressOf(), 0);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "XAudio initialization failed.");
+ }
+#ifdef _DEBUG
+ else
+ {
+ XAUDIO2_DEBUG_CONFIGURATION config = {};
+ config.BreakMask = XAUDIO2_LOG_ERRORS | XAUDIO2_LOG_WARNINGS | XAUDIO2_LOG_API_CALLS | XAUDIO2_LOG_STREAMING;
+ config.TraceMask = XAUDIO2_LOG_ERRORS | XAUDIO2_LOG_WARNINGS | XAUDIO2_LOG_API_CALLS | XAUDIO2_LOG_STREAMING;
+ config.LogTiming = true;
+ config.LogFunctionName = true;
+ m_xAudio2->SetDebugConfiguration(&config, 0);
+ }
+#endif // _DEBUG
+}
+
+CAESinkXAudio::~CAESinkXAudio()
+{
+ if (m_xAudio2)
+ m_xAudio2.Reset();
+}
+
+void CAESinkXAudio::Register()
+{
+ AE::AESinkRegEntry reg;
+ reg.sinkName = "XAUDIO";
+ reg.createFunc = CAESinkXAudio::Create;
+ reg.enumerateFunc = CAESinkXAudio::EnumerateDevicesEx;
+ AE::CAESinkFactory::RegisterSink(reg);
+}
+
+IAESink* CAESinkXAudio::Create(std::string &device, AEAudioFormat &desiredFormat)
+{
+ IAESink *sink = new CAESinkXAudio();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+
+bool CAESinkXAudio::Initialize(AEAudioFormat &format, std::string &device)
+{
+ if (m_initialized)
+ return false;
+
+ m_device = device;
+ bool bdefault = false;
+ HRESULT hr = S_OK;
+
+ /* Save requested format */
+ /* Clear returned format */
+ sinkReqFormat = format.m_dataFormat;
+ sinkRetFormat = AE_FMT_INVALID;
+
+ if (!InitializeInternal(device, format))
+ {
+ CLog::Log(LOGINFO, __FUNCTION__": Could not Initialize voices with that format");
+ goto failed;
+ }
+
+ format.m_frames = m_uiBufferLen;
+ m_format = format;
+ sinkRetFormat = format.m_dataFormat;
+
+ m_initialized = true;
+ m_isDirty = false;
+
+ return true;
+
+failed:
+ CLog::Log(LOGERROR, __FUNCTION__": XAudio initialization failed.");
+ return true;
+}
+
+void CAESinkXAudio::Deinitialize()
+{
+ if (!m_initialized && !m_isDirty)
+ return;
+
+ if (m_running)
+ {
+ try
+ {
+ m_sourceVoice->Stop();
+ m_sourceVoice->FlushSourceBuffers();
+ m_sinkFrames = 0;
+ m_framesInBuffers = 0;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGDEBUG, "{}: Invalidated source voice - Releasing", __FUNCTION__);
+ }
+ }
+ m_running = false;
+
+ SafeDestroyVoice(&m_sourceVoice);
+ SafeDestroyVoice(&m_masterVoice);
+
+ m_initialized = false;
+}
+
+/**
+ * @brief rescale uint64_t without overflowing on large values
+ */
+static uint64_t rescale_u64(uint64_t val, uint64_t num, uint64_t den)
+{
+ return ((val / den) * num) + (((val % den) * num) / den);
+}
+
+void CAESinkXAudio::GetDelay(AEDelayStatus& status)
+{
+ HRESULT hr = S_OK;
+ uint64_t pos = 0, tick = 0;
+ int retries = 0;
+
+ if (!m_initialized)
+ {
+ status.SetDelay(0.0);
+ return;
+ }
+
+ XAUDIO2_VOICE_STATE state;
+ m_sourceVoice->GetState(&state, 0);
+
+ double delay = (double)(m_sinkFrames - state.SamplesPlayed) / m_format.m_sampleRate;
+ status.SetDelay(delay);
+ return;
+}
+
+double CAESinkXAudio::GetCacheTotal()
+{
+ if (!m_initialized)
+ return 0.0;
+
+ return XAUDIO_BUFFERS_IN_QUEUE * m_format.m_frames / (double)m_format.m_sampleRate;
+}
+
+double CAESinkXAudio::GetLatency()
+{
+ if (!m_initialized || !m_xAudio2)
+ return 0.0;
+
+ XAUDIO2_PERFORMANCE_DATA perfData;
+ m_xAudio2->GetPerformanceData(&perfData);
+
+ return perfData.CurrentLatencyInSamples / (double) m_format.m_sampleRate;
+}
+
+unsigned int CAESinkXAudio::AddPackets(uint8_t **data, unsigned int frames, unsigned int offset)
+{
+ if (!m_initialized)
+ return 0;
+
+ HRESULT hr = S_OK;
+ DWORD flags = 0;
+
+#ifndef _DEBUG
+ LARGE_INTEGER timerStart;
+ LARGE_INTEGER timerStop;
+ LARGE_INTEGER timerFreq;
+#endif
+ size_t dataLenght = frames * m_format.m_frameSize;
+
+ struct buffer_ctx *ctx = new buffer_ctx;
+ ctx->data = new uint8_t[dataLenght];
+ ctx->frames = frames;
+ ctx->sink = this;
+ memcpy(ctx->data, data[0] + offset * m_format.m_frameSize, dataLenght);
+
+ XAUDIO2_BUFFER xbuffer = {};
+ xbuffer.AudioBytes = dataLenght;
+ xbuffer.pAudioData = ctx->data;
+ xbuffer.pContext = ctx;
+
+ if (!m_running) //first time called, pre-fill buffer then start voice
+ {
+ m_sourceVoice->Stop();
+ hr = m_sourceVoice->SubmitSourceBuffer(&xbuffer);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "voice submit buffer failed due to {}", WASAPIErrToStr(hr));
+ delete ctx;
+ return 0;
+ }
+ hr = m_sourceVoice->Start(0, XAUDIO2_COMMIT_NOW);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "voice start failed due to {}", WASAPIErrToStr(hr));
+ m_isDirty = true; //flag new device or re-init needed
+ delete ctx;
+ return INT_MAX;
+ }
+ m_sinkFrames += frames;
+ m_framesInBuffers += frames;
+ m_running = true; //signal that we're processing frames
+ return frames;
+ }
+
+#ifndef _DEBUG
+ /* Get clock time for latency checks */
+ QueryPerformanceFrequency(&timerFreq);
+ QueryPerformanceCounter(&timerStart);
+#endif
+
+ /* Wait for Audio Driver to tell us it's got a buffer available */
+ //XAUDIO2_VOICE_STATE state;
+ //while (m_sourceVoice->GetState(&state), state.BuffersQueued >= XAUDIO_BUFFERS_IN_QUEUE)
+ while (m_format.m_frames * XAUDIO_BUFFERS_IN_QUEUE <= m_framesInBuffers.load())
+ {
+ DWORD eventAudioCallback;
+ eventAudioCallback = WaitForSingleObjectEx(m_voiceCallback.mBufferEnd.get(), 1100, TRUE);
+ if (eventAudioCallback != WAIT_OBJECT_0)
+ {
+ CLog::LogF(LOGERROR, "voice buffer timed out");
+ delete ctx;
+ return INT_MAX;
+ }
+ }
+
+ if (!m_running)
+ return 0;
+
+#ifndef _DEBUG
+ QueryPerformanceCounter(&timerStop);
+ LONGLONG timerDiff = timerStop.QuadPart - timerStart.QuadPart;
+ double timerElapsed = (double) timerDiff * 1000.0 / (double) timerFreq.QuadPart;
+ m_avgTimeWaiting += (timerElapsed - m_avgTimeWaiting) * 0.5;
+
+ if (m_avgTimeWaiting < 3.0)
+ {
+ CLog::LogF(LOGDEBUG, "Possible AQ Loss: Avg. Time Waiting for Audio Driver callback : {}msec",
+ (int)m_avgTimeWaiting);
+ }
+#endif
+
+ hr = m_sourceVoice->SubmitSourceBuffer(&xbuffer);
+ if (FAILED(hr))
+ {
+ #ifdef _DEBUG
+ CLog::LogF(LOGERROR, "submiting buffer failed due to {}", WASAPIErrToStr(hr));
+#endif
+ delete ctx;
+ return INT_MAX;
+ }
+
+ m_sinkFrames += frames;
+ m_framesInBuffers += frames;
+
+ return frames;
+}
+
+void CAESinkXAudio::EnumerateDevicesEx(AEDeviceInfoList &deviceInfoList, bool force)
+{
+ HRESULT hr = S_OK, hr2 = S_OK;
+ CAEDeviceInfo deviceInfo;
+ CAEChannelInfo deviceChannels;
+ WAVEFORMATEXTENSIBLE wfxex = {};
+ bool add192 = false;
+
+ UINT32 eflags = 0;// XAUDIO2_DEBUG_ENGINE;
+
+ IXAudio2MasteringVoice* mMasterVoice = nullptr;
+ IXAudio2SourceVoice* mSourceVoice = nullptr;
+ Microsoft::WRL::ComPtr<IXAudio2> xaudio2;
+ hr = XAudio2Create(xaudio2.ReleaseAndGetAddressOf(), eflags);
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGDEBUG, __FUNCTION__": Failed to activate XAudio for capability testing.");
+ goto failed;
+ }
+
+ for(RendererDetail& details : CAESinkFactoryWin::GetRendererDetails())
+ {
+ deviceInfo.m_channels.Reset();
+ deviceInfo.m_dataFormats.clear();
+ deviceInfo.m_sampleRates.clear();
+
+ std::wstring deviceId = KODI::PLATFORM::WINDOWS::ToW(details.strDeviceId);
+
+ /* Test format DTS-HD-MA */
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
+ wfxex.Format.nSamplesPerSec = 192000;
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD;
+ wfxex.Format.wBitsPerSample = 16;
+ wfxex.Samples.wValidBitsPerSample = 16;
+ wfxex.Format.nChannels = 8;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+
+ hr2 = xaudio2->CreateMasteringVoice(&mMasterVoice, wfxex.Format.nChannels, wfxex.Format.nSamplesPerSec,
+ 0, deviceId.c_str(), nullptr, AudioCategory_Media);
+ hr = xaudio2->CreateSourceVoice(&mSourceVoice, &wfxex.Format);
+
+ if (FAILED(hr))
+ {
+ CLog::Log(
+ LOGINFO, __FUNCTION__ ": stream type \"{}\" on device \"{}\" seems to be not supported.",
+ CAEUtil::StreamTypeToStr(CAEStreamInfo::STREAM_TYPE_DTSHD_MA), details.strDescription);
+ }
+ else
+ {
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_MA);
+ add192 = true;
+ }
+ SafeDestroyVoice(&mSourceVoice);
+
+ /* Test format DTS-HD-HR */
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
+ wfxex.Format.nSamplesPerSec = 192000;
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_5POINT1;
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD;
+ wfxex.Format.wBitsPerSample = 16;
+ wfxex.Samples.wValidBitsPerSample = 16;
+ wfxex.Format.nChannels = 2;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+
+ hr2 = xaudio2->CreateMasteringVoice(&mMasterVoice, wfxex.Format.nChannels, wfxex.Format.nSamplesPerSec,
+ 0, deviceId.c_str(), nullptr, AudioCategory_Media);
+ hr = xaudio2->CreateSourceVoice(&mSourceVoice, &wfxex.Format);
+
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGINFO,
+ __FUNCTION__ ": stream type \"{}\" on device \"{}\" seems to be not supported.",
+ CAEUtil::StreamTypeToStr(CAEStreamInfo::STREAM_TYPE_DTSHD), details.strDescription);
+ }
+ else
+ {
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD);
+ add192 = true;
+ }
+ SafeDestroyVoice(&mSourceVoice);
+
+ /* Test format Dolby TrueHD */
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP;
+ wfxex.Format.nChannels = 8;
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
+
+ hr = xaudio2->CreateSourceVoice(&mSourceVoice, &wfxex.Format);
+ if (FAILED(hr))
+ {
+ CLog::Log(
+ LOGINFO, __FUNCTION__ ": stream type \"{}\" on device \"{}\" seems to be not supported.",
+ CAEUtil::StreamTypeToStr(CAEStreamInfo::STREAM_TYPE_TRUEHD), details.strDescription);
+ }
+ else
+ {
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_TRUEHD);
+ add192 = true;
+ }
+
+ /* Test format Dolby EAC3 */
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS;
+ wfxex.Format.nChannels = 2;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+
+ SafeDestroyVoice(&mSourceVoice);
+ SafeDestroyVoice(&mMasterVoice);
+ hr2 = xaudio2->CreateMasteringVoice(&mMasterVoice, wfxex.Format.nChannels, wfxex.Format.nSamplesPerSec,
+ 0, deviceId.c_str(), nullptr, AudioCategory_Media);
+ hr = xaudio2->CreateSourceVoice(&mSourceVoice, &wfxex.Format);
+
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGINFO,
+ __FUNCTION__ ": stream type \"{}\" on device \"{}\" seems to be not supported.",
+ CAEUtil::StreamTypeToStr(CAEStreamInfo::STREAM_TYPE_EAC3), details.strDescription);
+ }
+ else
+ {
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_EAC3);
+ add192 = true;
+ }
+
+ /* Test format DTS */
+ wfxex.Format.nSamplesPerSec = 48000;
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_5POINT1;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DTS;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+
+ SafeDestroyVoice(&mSourceVoice);
+ SafeDestroyVoice(&mMasterVoice);
+ hr2 = xaudio2->CreateMasteringVoice(&mMasterVoice, wfxex.Format.nChannels, wfxex.Format.nSamplesPerSec,
+ 0, deviceId.c_str(), nullptr, AudioCategory_Media);
+ hr = xaudio2->CreateSourceVoice(&mSourceVoice, &wfxex.Format);
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGINFO,
+ __FUNCTION__ ": stream type \"{}\" on device \"{}\" seems to be not supported.",
+ "STREAM_TYPE_DTS", details.strDescription);
+ }
+ else
+ {
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ }
+ SafeDestroyVoice(&mSourceVoice);
+
+ /* Test format Dolby AC3 */
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL;
+
+ hr = xaudio2->CreateSourceVoice(&mSourceVoice, &wfxex.Format);
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGINFO,
+ __FUNCTION__ ": stream type \"{}\" on device \"{}\" seems to be not supported.",
+ CAEUtil::StreamTypeToStr(CAEStreamInfo::STREAM_TYPE_AC3), details.strDescription);
+ }
+ else
+ {
+ deviceInfo.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ }
+
+ /* Test format for PCM format iteration */
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_STEREO;
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+
+ for (int p = AE_FMT_FLOAT; p > AE_FMT_INVALID; p--)
+ {
+ if (p < AE_FMT_FLOAT)
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+ wfxex.Format.wBitsPerSample = CAEUtil::DataFormatToBits((AEDataFormat)p);
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+ if (p == AE_FMT_S24NE4MSB)
+ {
+ wfxex.Samples.wValidBitsPerSample = 24;
+ }
+ else if (p <= AE_FMT_S24NE4 && p >= AE_FMT_S24BE4)
+ {
+ // not supported
+ continue;
+ }
+ else
+ {
+ wfxex.Samples.wValidBitsPerSample = wfxex.Format.wBitsPerSample;
+ }
+
+ SafeDestroyVoice(&mSourceVoice);
+ hr = xaudio2->CreateSourceVoice(&mSourceVoice, &wfxex.Format);
+
+ if (SUCCEEDED(hr))
+ deviceInfo.m_dataFormats.push_back((AEDataFormat)p);
+ }
+
+ /* Test format for sample rate iteration */
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_STEREO;
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+ wfxex.Format.wBitsPerSample = 16;
+ wfxex.Samples.wValidBitsPerSample = 16;
+ wfxex.Format.nChannels = 2;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+
+ for (int j = 0; j < WASAPISampleRateCount; j++)
+ {
+ SafeDestroyVoice(&mSourceVoice);
+ SafeDestroyVoice(&mMasterVoice);
+
+ wfxex.Format.nSamplesPerSec = WASAPISampleRates[j];
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+
+ hr2 = xaudio2->CreateMasteringVoice(&mMasterVoice, wfxex.Format.nChannels, wfxex.Format.nSamplesPerSec,
+ 0, deviceId.c_str(), nullptr, AudioCategory_Media);
+ hr = xaudio2->CreateSourceVoice(&mSourceVoice, &wfxex.Format);
+ if (SUCCEEDED(hr))
+ deviceInfo.m_sampleRates.push_back(WASAPISampleRates[j]);
+ else if (wfxex.Format.nSamplesPerSec == 192000 && add192)
+ {
+ deviceInfo.m_sampleRates.push_back(WASAPISampleRates[j]);
+ CLog::Log(LOGINFO,
+ __FUNCTION__ ": sample rate 192khz on device \"{}\" seems to be not supported.",
+ details.strDescription);
+ }
+ }
+ SafeDestroyVoice(&mSourceVoice);
+ SafeDestroyVoice(&mMasterVoice);
+
+ deviceInfo.m_deviceName = details.strDeviceId;
+ deviceInfo.m_displayName = details.strWinDevType.append(details.strDescription);
+ deviceInfo.m_displayNameExtra = std::string("XAudio: ").append(details.strDescription);
+ deviceInfo.m_deviceType = details.eDeviceType;
+ deviceInfo.m_channels = layoutsByChCount[details.nChannels];
+
+ /* Store the device info */
+ deviceInfo.m_wantsIECPassthrough = true;
+ deviceInfo.m_onlyPCM = true;
+
+ if (!deviceInfo.m_streamTypes.empty())
+ deviceInfo.m_dataFormats.push_back(AE_FMT_RAW);
+
+ deviceInfoList.push_back(deviceInfo);
+
+ if (details.bDefault)
+ {
+ deviceInfo.m_deviceName = std::string("default");
+ deviceInfo.m_displayName = std::string("default");
+ deviceInfo.m_displayNameExtra = std::string("");
+ deviceInfo.m_wantsIECPassthrough = true;
+ deviceInfo.m_onlyPCM = true;
+ deviceInfoList.push_back(deviceInfo);
+ }
+ }
+
+failed:
+
+ if (FAILED(hr))
+ CLog::Log(LOGERROR, __FUNCTION__ ": Failed to enumerate XAudio endpoint devices ({}).",
+ WASAPIErrToStr(hr));
+}
+
+/// ------------------- Private utility functions -----------------------------------
+
+bool CAESinkXAudio::InitializeInternal(std::string deviceId, AEAudioFormat &format)
+{
+ std::wstring device = KODI::PLATFORM::WINDOWS::ToW(deviceId);
+ WAVEFORMATEXTENSIBLE wfxex = {};
+
+ if ( format.m_dataFormat <= AE_FMT_FLOAT
+ || format.m_dataFormat == AE_FMT_RAW)
+ CAESinkFactoryWin::BuildWaveFormatExtensible(format, wfxex);
+ else
+ {
+ // planar formats are currently not supported by this sink
+ format.m_dataFormat = AE_FMT_FLOAT;
+ CAESinkFactoryWin::BuildWaveFormatExtensible(format, wfxex);
+ }
+
+ /* Test for incomplete format and provide defaults */
+ if (format.m_sampleRate == 0 ||
+ format.m_channelLayout == CAEChannelInfo(nullptr) ||
+ format.m_dataFormat <= AE_FMT_INVALID ||
+ format.m_dataFormat >= AE_FMT_MAX ||
+ format.m_channelLayout.Count() == 0)
+ {
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.Format.nChannels = 2;
+ wfxex.Format.nSamplesPerSec = 44100L;
+ wfxex.Format.wBitsPerSample = 16;
+ wfxex.Format.nBlockAlign = 4;
+ wfxex.Samples.wValidBitsPerSample = 16;
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nBlockAlign * wfxex.Format.nSamplesPerSec;
+ wfxex.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
+ }
+
+ bool bdefault = StringUtils::EndsWithNoCase(deviceId, std::string("default"));
+
+ HRESULT hr;
+ IXAudio2MasteringVoice* pMasterVoice = nullptr;
+
+ if (!bdefault)
+ {
+ hr = m_xAudio2->CreateMasteringVoice(&pMasterVoice, wfxex.Format.nChannels, wfxex.Format.nSamplesPerSec,
+ 0, device.c_str(), nullptr, AudioCategory_Media);
+ }
+
+ if (!pMasterVoice)
+ {
+ if (!bdefault)
+ {
+ CLog::Log(LOGINFO,
+ __FUNCTION__ ": Could not locate the device named \"{}\" in the list of Xaudio "
+ "endpoint devices. Trying the default device...",
+ KODI::PLATFORM::WINDOWS::FromW(device));
+ }
+
+ // smartphone issue: providing device ID (even default ID) causes E_NOINTERFACE result
+ // workaround: device = nullptr will initialize default audio endpoint
+ hr = m_xAudio2->CreateMasteringVoice(&pMasterVoice, wfxex.Format.nChannels, wfxex.Format.nSamplesPerSec,
+ 0, 0, nullptr, AudioCategory_Media);
+ if (FAILED(hr) || !pMasterVoice)
+ {
+ CLog::Log(LOGINFO,
+ __FUNCTION__ ": Could not retrieve the default XAudio audio endpoint ({}).",
+ WASAPIErrToStr(hr));
+ return false;
+ }
+ }
+
+ m_masterVoice = pMasterVoice;
+
+ int closestMatch = 0;
+ unsigned int requestedChannels = 0;
+ unsigned int noOfCh = 0;
+
+ hr = m_xAudio2->CreateSourceVoice(&m_sourceVoice, &wfxex.Format, 0, XAUDIO2_DEFAULT_FREQ_RATIO, &m_voiceCallback);
+ if (SUCCEEDED(hr))
+ {
+ CLog::Log(LOGINFO, __FUNCTION__": Format is Supported - will attempt to Initialize");
+ goto initialize;
+ }
+
+ if (format.m_dataFormat == AE_FMT_RAW) //No sense in trying other formats for passthrough.
+ return false;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGAUDIO))
+ CLog::Log(LOGDEBUG,
+ __FUNCTION__ ": CreateSourceVoice failed ({}) - trying to find a compatible format",
+ WASAPIErrToStr(hr));
+
+ requestedChannels = wfxex.Format.nChannels;
+
+ /* The requested format is not supported by the device. Find something that works */
+ for (int layout = -1; layout <= (int)ARRAYSIZE(layoutsList); layout++)
+ {
+ // if requested layout is not supported, try standard layouts with at least
+ // the number of channels as requested
+ // as the last resort try stereo
+ if (layout == ARRAYSIZE(layoutsList))
+ {
+ wfxex.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
+ wfxex.Format.nChannels = 2;
+ }
+ else if (layout >= 0)
+ {
+ wfxex.dwChannelMask = CAESinkFactoryWin::ChLayoutToChMask(layoutsList[layout], &noOfCh);
+ wfxex.Format.nChannels = noOfCh;
+ if (noOfCh < requestedChannels)
+ continue;
+ }
+
+ for (int j = 0; j < sizeof(testFormats)/sizeof(sampleFormat); j++)
+ {
+ closestMatch = -1;
+
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.SubFormat = testFormats[j].subFormat;
+ wfxex.Format.wBitsPerSample = testFormats[j].bitsPerSample;
+ wfxex.Samples.wValidBitsPerSample = testFormats[j].validBitsPerSample;
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+
+ for (int i = 0 ; i < WASAPISampleRateCount; i++)
+ {
+ wfxex.Format.nSamplesPerSec = WASAPISampleRates[i];
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+
+ hr = m_xAudio2->CreateMasteringVoice(&m_masterVoice, wfxex.Format.nChannels, wfxex.Format.nSamplesPerSec,
+ 0, device.c_str(), nullptr, AudioCategory_Media);
+ if (SUCCEEDED(hr))
+ {
+ hr = m_xAudio2->CreateSourceVoice(&m_sourceVoice, &wfxex.Format, 0, XAUDIO2_DEFAULT_FREQ_RATIO, &m_voiceCallback);
+ if (SUCCEEDED(hr))
+ {
+ /* If the current sample rate matches the source then stop looking and use it */
+ if ((WASAPISampleRates[i] == format.m_sampleRate) && (testFormats[j].subFormatType <= format.m_dataFormat))
+ goto initialize;
+ /* If this rate is closer to the source then the previous one, save it */
+ else if (closestMatch < 0 || abs((int)WASAPISampleRates[i] - (int)format.m_sampleRate) < abs((int)WASAPISampleRates[closestMatch] - (int)format.m_sampleRate))
+ closestMatch = i;
+ }
+ }
+
+ if (FAILED(hr))
+ CLog::Log(LOGERROR, __FUNCTION__ ": creating voices failed ({})", WASAPIErrToStr(hr));
+ }
+
+ if (closestMatch >= 0)
+ {
+ wfxex.Format.nSamplesPerSec = WASAPISampleRates[closestMatch];
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+ goto initialize;
+ }
+ }
+ }
+
+ CLog::Log(LOGERROR, __FUNCTION__": Unable to locate a supported output format for the device. Check the speaker settings in the control panel.");
+
+ /* We couldn't find anything supported. This should never happen */
+ /* unless the user set the wrong speaker setting in the control panel */
+ return false;
+
+initialize:
+
+ CAESinkFactoryWin::AEChannelsFromSpeakerMask(m_channelLayout, wfxex.dwChannelMask);
+ format.m_channelLayout = m_channelLayout;
+
+ /* When the stream is raw, the values in the format structure are set to the link */
+ /* parameters, so store the encoded stream values here for the IsCompatible function */
+ m_encodedChannels = wfxex.Format.nChannels;
+ m_encodedSampleRate = (format.m_dataFormat == AE_FMT_RAW) ? format.m_streamInfo.m_sampleRate : format.m_sampleRate;
+
+ /* Set up returned sink format for engine */
+ if (format.m_dataFormat != AE_FMT_RAW)
+ {
+ if (wfxex.Format.wBitsPerSample == 32)
+ {
+ if (wfxex.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
+ format.m_dataFormat = AE_FMT_FLOAT;
+ else if (wfxex.Samples.wValidBitsPerSample == 32)
+ format.m_dataFormat = AE_FMT_S32NE;
+ else
+ format.m_dataFormat = AE_FMT_S24NE4MSB;
+ }
+ else if (wfxex.Format.wBitsPerSample == 24)
+ format.m_dataFormat = AE_FMT_S24NE3;
+ else
+ format.m_dataFormat = AE_FMT_S16NE;
+ }
+
+ format.m_sampleRate = wfxex.Format.nSamplesPerSec; //PCM: Sample rate. RAW: Link speed
+ format.m_frameSize = (wfxex.Format.wBitsPerSample >> 3) * wfxex.Format.nChannels;
+
+ if (format.m_dataFormat == AE_FMT_RAW)
+ format.m_dataFormat = AE_FMT_S16NE;
+
+ hr = m_sourceVoice->Start(0, XAUDIO2_COMMIT_NOW);
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGERROR, __FUNCTION__ ": Voice start failed : {}", WASAPIErrToStr(hr));
+ CLog::Log(LOGDEBUG, " Sample Rate : {}", wfxex.Format.nSamplesPerSec);
+ CLog::Log(LOGDEBUG, " Sample Format : {}", CAEUtil::DataFormatToStr(format.m_dataFormat));
+ CLog::Log(LOGDEBUG, " Bits Per Sample : {}", wfxex.Format.wBitsPerSample);
+ CLog::Log(LOGDEBUG, " Valid Bits/Samp : {}", wfxex.Samples.wValidBitsPerSample);
+ CLog::Log(LOGDEBUG, " Channel Count : {}", wfxex.Format.nChannels);
+ CLog::Log(LOGDEBUG, " Block Align : {}", wfxex.Format.nBlockAlign);
+ CLog::Log(LOGDEBUG, " Avg. Bytes Sec : {}", wfxex.Format.nAvgBytesPerSec);
+ CLog::Log(LOGDEBUG, " Samples/Block : {}", wfxex.Samples.wSamplesPerBlock);
+ CLog::Log(LOGDEBUG, " Format cBSize : {}", wfxex.Format.cbSize);
+ CLog::Log(LOGDEBUG, " Channel Layout : {}", ((std::string)format.m_channelLayout));
+ CLog::Log(LOGDEBUG, " Channel Mask : {}", wfxex.dwChannelMask);
+ return false;
+ }
+
+ XAUDIO2_PERFORMANCE_DATA perfData;
+ m_xAudio2->GetPerformanceData(&perfData);
+ if (!perfData.TotalSourceVoiceCount)
+ {
+ CLog::Log(LOGERROR, __FUNCTION__ ": GetPerformanceData Failed : {}", WASAPIErrToStr(hr));
+ return false;
+ }
+
+ m_uiBufferLen = (int)(format.m_sampleRate * 0.02);
+ m_dwFrameSize = wfxex.Format.nBlockAlign;
+ m_dwChunkSize = m_dwFrameSize * m_uiBufferLen;
+ m_dwBufferLen = m_dwChunkSize * 4;
+ m_AvgBytesPerSec = wfxex.Format.nAvgBytesPerSec;
+
+ CLog::Log(LOGINFO, __FUNCTION__ ": XAudio Sink Initialized using: {}, {}, {}",
+ CAEUtil::DataFormatToStr(format.m_dataFormat), wfxex.Format.nSamplesPerSec,
+ wfxex.Format.nChannels);
+
+ m_sourceVoice->Stop();
+
+ return true;
+}
+
+void CAESinkXAudio::Drain()
+{
+ if(!m_sourceVoice)
+ return;
+
+ AEDelayStatus status;
+ GetDelay(status);
+
+ KODI::TIME::Sleep(std::chrono::milliseconds(static_cast<int>(status.GetDelay() * 500)));
+
+ if (m_running)
+ {
+ try
+ {
+ m_sourceVoice->Stop();
+ m_sourceVoice->FlushSourceBuffers();
+ m_sinkFrames = 0;
+ m_framesInBuffers = 0;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGDEBUG, "{}: Invalidated source voice - Releasing", __FUNCTION__);
+ }
+ }
+ m_running = false;
+}
+
+bool CAESinkXAudio::IsUSBDevice()
+{
+#if 0 // TODO
+ IPropertyStore *pProperty = nullptr;
+ PROPVARIANT varName;
+ PropVariantInit(&varName);
+ bool ret = false;
+
+ HRESULT hr = m_pDevice->OpenPropertyStore(STGM_READ, &pProperty);
+ if (!SUCCEEDED(hr))
+ return ret;
+ hr = pProperty->GetValue(PKEY_Device_EnumeratorName, &varName);
+
+ std::string str = localWideToUtf(varName.pwszVal);
+ StringUtils::ToUpper(str);
+ ret = (str == "USB");
+ PropVariantClear(&varName);
+ if (pProperty)
+ pProperty->Release();
+#endif
+ return false;
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkXAudio.h b/xbmc/cores/AudioEngine/Sinks/AESinkXAudio.h
new file mode 100644
index 0000000..7c34e83
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkXAudio.h
@@ -0,0 +1,131 @@
+/*
+ * 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 "cores/AudioEngine/Interfaces/AESink.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+
+#include <stdint.h>
+
+#include <mmdeviceapi.h>
+#include <ppltasks.h>
+#include <wrl/implements.h>
+#include <x3daudio.h>
+#include <xapofx.h>
+#include <xaudio2.h>
+#include <xaudio2fx.h>
+#pragma comment(lib,"xaudio2.lib")
+
+class CAESinkXAudio : public IAESink
+{
+public:
+ virtual const char *GetName() { return "XAudio"; }
+
+ CAESinkXAudio();
+ virtual ~CAESinkXAudio();
+
+ static void Register();
+ 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;
+ double GetLatency() override;
+ unsigned int AddPackets(uint8_t **data, unsigned int frames, unsigned int offset) override;
+ void Drain() override;
+
+ static void EnumerateDevicesEx(AEDeviceInfoList &deviceInfoList, bool force = false);
+
+private:
+ struct buffer_ctx
+ {
+ uint8_t *data;
+ uint32_t frames;
+ CAESinkXAudio* sink;
+
+ ~buffer_ctx()
+ {
+ delete[] data;
+ sink->m_framesInBuffers -= frames;
+ sink = nullptr;
+ }
+ };
+
+ struct VoiceCallback : public IXAudio2VoiceCallback
+ {
+ VoiceCallback()
+ {
+ mBufferEnd.reset(CreateEventEx(nullptr, nullptr, 0, EVENT_MODIFY_STATE | SYNCHRONIZE));
+ if (!mBufferEnd)
+ {
+ throw std::exception("CreateEvent");
+ }
+ }
+ virtual ~VoiceCallback() { }
+
+ STDMETHOD_(void, OnVoiceProcessingPassStart) (UINT32) override {}
+ STDMETHOD_(void, OnVoiceProcessingPassEnd)() override {}
+ STDMETHOD_(void, OnStreamEnd)() override {}
+ STDMETHOD_(void, OnBufferStart)(void*) override {}
+ STDMETHOD_(void, OnBufferEnd)(void* context) override
+ {
+ SetEvent(mBufferEnd.get());
+ struct buffer_ctx *ctx = static_cast<struct buffer_ctx*>(context);
+ delete ctx;
+ }
+
+ STDMETHOD_(void, OnLoopEnd)(void*) override {}
+ STDMETHOD_(void, OnVoiceError)(void*, HRESULT) override {}
+
+ struct handle_closer
+ {
+ void operator()(HANDLE h) const
+ {
+ assert(h != INVALID_HANDLE_VALUE);
+ if (h)
+ CloseHandle(h);
+ }
+ };
+ std::unique_ptr<void, handle_closer> mBufferEnd;
+ };
+
+ bool InitializeInternal(std::string deviceId, AEAudioFormat &format);
+ bool IsUSBDevice();
+
+ Microsoft::WRL::ComPtr<IXAudio2> m_xAudio2;
+ IXAudio2MasteringVoice* m_masterVoice;
+ IXAudio2SourceVoice* m_sourceVoice;
+ VoiceCallback m_voiceCallback;
+
+ AEAudioFormat m_format;
+ unsigned int m_encodedChannels;
+ unsigned int m_encodedSampleRate;
+ CAEChannelInfo m_channelLayout;
+ std::string m_device;
+
+ enum AEDataFormat sinkReqFormat;
+ enum AEDataFormat sinkRetFormat;
+
+ unsigned int m_uiBufferLen; /* xaudio endpoint buffer size, in frames */
+ unsigned int m_AvgBytesPerSec;
+ unsigned int m_dwChunkSize;
+ unsigned int m_dwFrameSize;
+ unsigned int m_dwBufferLen;
+ uint64_t m_sinkFrames;
+ std::atomic<uint16_t> m_framesInBuffers;
+
+ double m_avgTimeWaiting; /* time between next buffer of data from SoftAE and driver call for data */
+
+ bool m_running;
+ bool m_initialized;
+ bool m_isSuspended; /* sink is in a suspended state - release audio device */
+ bool m_isDirty; /* sink output failed - needs re-init or new device */
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.cpp b/xbmc/cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.cpp
new file mode 100644
index 0000000..2d2bd78
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.cpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2014-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 "ALSADeviceMonitor.h"
+
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/Engines/ActiveAE/ActiveAE.h"
+#include "utils/log.h"
+
+#include "platform/linux/FDEventMonitor.h"
+
+#include <libudev.h>
+
+CALSADeviceMonitor::CALSADeviceMonitor() :
+ m_udev(NULL),
+ m_udevMonitor(NULL)
+{
+}
+
+CALSADeviceMonitor::~CALSADeviceMonitor()
+{
+ Stop();
+}
+
+void CALSADeviceMonitor::Start()
+{
+ int err;
+
+ if (!m_udev)
+ {
+ m_udev = udev_new();
+ if (!m_udev)
+ {
+ CLog::Log(LOGWARNING, "CALSADeviceMonitor::Start - Unable to open udev handle");
+ return;
+ }
+
+ m_udevMonitor = udev_monitor_new_from_netlink(m_udev, "udev");
+ if (!m_udevMonitor)
+ {
+ CLog::Log(LOGERROR, "CALSADeviceMonitor::Start - udev_monitor_new_from_netlink() failed");
+ goto err_unref_udev;
+ }
+
+ err = udev_monitor_filter_add_match_subsystem_devtype(m_udevMonitor, "sound", NULL);
+ if (err)
+ {
+ CLog::Log(LOGERROR, "CALSADeviceMonitor::Start - udev_monitor_filter_add_match_subsystem_devtype() failed");
+ goto err_unref_monitor;
+ }
+
+ err = udev_monitor_enable_receiving(m_udevMonitor);
+ if (err)
+ {
+ CLog::Log(LOGERROR, "CALSADeviceMonitor::Start - udev_monitor_enable_receiving() failed");
+ goto err_unref_monitor;
+ }
+
+ const auto eventMonitor = CServiceBroker::GetPlatform().GetService<CFDEventMonitor>();
+ eventMonitor->AddFD(CFDEventMonitor::MonitoredFD(udev_monitor_get_fd(m_udevMonitor), POLLIN,
+ FDEventCallback, m_udevMonitor),
+ m_fdMonitorId);
+ }
+
+ return;
+
+err_unref_monitor:
+ udev_monitor_unref(m_udevMonitor);
+ m_udevMonitor = NULL;
+err_unref_udev:
+ udev_unref(m_udev);
+ m_udev = NULL;
+}
+
+void CALSADeviceMonitor::Stop()
+{
+ if (m_udev)
+ {
+ const auto eventMonitor = CServiceBroker::GetPlatform().GetService<CFDEventMonitor>();
+ eventMonitor->RemoveFD(m_fdMonitorId);
+
+ udev_monitor_unref(m_udevMonitor);
+ m_udevMonitor = NULL;
+ udev_unref(m_udev);
+ m_udev = NULL;
+ }
+}
+
+void CALSADeviceMonitor::FDEventCallback(int id, int fd, short revents, void *data)
+{
+ struct udev_monitor *udevMonitor = (struct udev_monitor *)data;
+ bool audioDevicesChanged = false;
+ struct udev_device *device;
+
+ while ((device = udev_monitor_receive_device(udevMonitor)) != NULL)
+ {
+ const char* action = udev_device_get_action(device);
+ const char* soundInitialized = udev_device_get_property_value(device, "SOUND_INITIALIZED");
+
+ if (!action || !soundInitialized)
+ continue;
+
+ /* cardX devices emit a "change" event when ready (i.e. all subdevices added) */
+ if (strcmp(action, "change") == 0)
+ {
+ CLog::Log(LOGDEBUG, "CALSADeviceMonitor - ALSA card added (\"{}\", \"{}\")",
+ udev_device_get_syspath(device), udev_device_get_devpath(device));
+ audioDevicesChanged = true;
+ }
+ else if (strcmp(action, "remove") == 0)
+ {
+ CLog::Log(LOGDEBUG, "CALSADeviceMonitor - ALSA card removed");
+ audioDevicesChanged = true;
+ }
+
+ udev_device_unref(device);
+ }
+
+ if (audioDevicesChanged)
+ {
+ CServiceBroker::GetActiveAE()->DeviceChange();
+ }
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.h b/xbmc/cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.h
new file mode 100644
index 0000000..3bb8615
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2014-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 "platform/Platform.h"
+
+#include <string>
+#include <vector>
+
+#include <alsa/asoundlib.h>
+
+class CALSADeviceMonitor : public IPlatformService
+{
+public:
+ CALSADeviceMonitor();
+ ~CALSADeviceMonitor();
+
+ void Start();
+ void Stop();
+
+private:
+ static void FDEventCallback(int id, int fd, short revents, void *data);
+
+ int m_fdMonitorId = 0;
+
+ struct udev *m_udev;
+ struct udev_monitor* m_udevMonitor;
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.cpp b/xbmc/cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.cpp
new file mode 100644
index 0000000..5572920
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.cpp
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2014-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 "ALSAHControlMonitor.h"
+
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/Engines/ActiveAE/ActiveAE.h"
+#include "utils/log.h"
+
+#include "platform/linux/FDEventMonitor.h"
+
+CALSAHControlMonitor::CALSAHControlMonitor() = default;
+
+CALSAHControlMonitor::~CALSAHControlMonitor()
+{
+ Clear();
+}
+
+bool CALSAHControlMonitor::Add(const std::string& ctlHandleName,
+ snd_ctl_elem_iface_t interface,
+ unsigned int device,
+ const std::string& name)
+{
+ snd_hctl_t *hctl = GetHandle(ctlHandleName);
+
+ if (!hctl)
+ {
+ return false;
+ }
+
+ snd_ctl_elem_id_t *id;
+
+ snd_ctl_elem_id_alloca(&id);
+
+ snd_ctl_elem_id_set_interface(id, interface);
+ snd_ctl_elem_id_set_name (id, name.c_str());
+ snd_ctl_elem_id_set_device (id, device);
+
+ snd_hctl_elem_t *elem = snd_hctl_find_elem(hctl, id);
+
+ if (!elem)
+ {
+ PutHandle(ctlHandleName);
+ return false;
+ }
+
+ snd_hctl_elem_set_callback(elem, HCTLCallback);
+
+ return true;
+}
+
+void CALSAHControlMonitor::Clear()
+{
+ Stop();
+
+ for (std::map<std::string, CTLHandle>::iterator it = m_ctlHandles.begin();
+ it != m_ctlHandles.end(); ++it)
+ {
+ snd_hctl_close(it->second.handle);
+ }
+ m_ctlHandles.clear();
+}
+
+void CALSAHControlMonitor::Start()
+{
+ assert(m_fdMonitorIds.empty());
+
+ std::vector<struct pollfd> pollfds;
+ std::vector<CFDEventMonitor::MonitoredFD> monitoredFDs;
+
+ for (std::map<std::string, CTLHandle>::iterator it = m_ctlHandles.begin();
+ it != m_ctlHandles.end(); ++it)
+ {
+ pollfds.resize(snd_hctl_poll_descriptors_count(it->second.handle));
+ int fdcount = snd_hctl_poll_descriptors(it->second.handle, &pollfds[0], pollfds.size());
+
+ for (int j = 0; j < fdcount; ++j)
+ {
+ monitoredFDs.emplace_back(pollfds[j].fd, pollfds[j].events, FDEventCallback, it->second.handle);
+ }
+ }
+
+ const auto eventMonitor = CServiceBroker::GetPlatform().GetService<CFDEventMonitor>();
+ eventMonitor->AddFDs(monitoredFDs, m_fdMonitorIds);
+}
+
+
+void CALSAHControlMonitor::Stop()
+{
+ const auto eventMonitor = CServiceBroker::GetPlatform().GetService<CFDEventMonitor>();
+ eventMonitor->RemoveFDs(m_fdMonitorIds);
+
+ m_fdMonitorIds.clear();
+}
+
+int CALSAHControlMonitor::HCTLCallback(snd_hctl_elem_t *elem, unsigned int mask)
+{
+ /* _REMOVE is a special value instead of a bit and must be checked first */
+ if (mask == SND_CTL_EVENT_MASK_REMOVE)
+ {
+ /* Either the device was removed (which is handled in ALSADeviceMonitor instead)
+ * or snd_hctl_close() got called. */
+ return 0;
+ }
+
+ if (mask & SND_CTL_EVENT_MASK_VALUE)
+ {
+ CLog::Log(LOGDEBUG, "CALSAHControlMonitor - Monitored ALSA hctl value changed");
+
+ /*
+ * Currently we just re-enumerate on any change.
+ * Custom callbacks for handling other control monitoring may be implemented when needed.
+ */
+ CServiceBroker::GetActiveAE()->DeviceChange();
+ }
+
+ return 0;
+}
+
+void CALSAHControlMonitor::FDEventCallback(int id, int fd, short revents, void *data)
+{
+ /* Run ALSA event handling when the FD has events */
+ snd_hctl_t *hctl = (snd_hctl_t *)data;
+ snd_hctl_handle_events(hctl);
+}
+
+snd_hctl_t* CALSAHControlMonitor::GetHandle(const std::string& ctlHandleName)
+{
+ if (!m_ctlHandles.count(ctlHandleName))
+ {
+ snd_hctl_t *hctl;
+
+ if (snd_hctl_open(&hctl, ctlHandleName.c_str(), 0) != 0)
+ {
+ CLog::Log(LOGWARNING, "CALSAHControlMonitor::GetHandle - snd_hctl_open() failed for \"{}\"",
+ ctlHandleName);
+ return NULL;
+ }
+ if (snd_hctl_load(hctl) != 0)
+ {
+ CLog::Log(LOGERROR, "CALSAHControlMonitor::GetHandle - snd_hctl_load() failed for \"{}\"",
+ ctlHandleName);
+ snd_hctl_close(hctl);
+ return NULL;
+ }
+
+ snd_hctl_nonblock(hctl, 1);
+
+ m_ctlHandles[ctlHandleName] = CTLHandle(hctl);
+ }
+
+ m_ctlHandles[ctlHandleName].useCount++;
+ return m_ctlHandles[ctlHandleName].handle;
+}
+
+void CALSAHControlMonitor::PutHandle(const std::string& ctlHandleName)
+{
+ if (--m_ctlHandles[ctlHandleName].useCount == 0)
+ {
+ snd_hctl_close(m_ctlHandles[ctlHandleName].handle);
+ m_ctlHandles.erase(ctlHandleName);
+ }
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.h b/xbmc/cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.h
new file mode 100644
index 0000000..165f375
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2014-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 "platform/Platform.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <alsa/asoundlib.h>
+
+class CALSAHControlMonitor : public IPlatformService
+{
+public:
+ CALSAHControlMonitor();
+ ~CALSAHControlMonitor();
+
+ bool Add(const std::string& ctlHandleName,
+ snd_ctl_elem_iface_t interface,
+ unsigned int device,
+ const std::string& name);
+
+ void Clear();
+
+ void Start();
+ void Stop();
+
+private:
+ static int HCTLCallback(snd_hctl_elem_t *elem, unsigned int mask);
+ static void FDEventCallback(int id, int fd, short revents, void *data);
+
+ snd_hctl_t* GetHandle(const std::string& ctlHandleName);
+ void PutHandle(const std::string& ctlHandleName);
+
+ struct CTLHandle
+ {
+ snd_hctl_t *handle;
+ int useCount = 0;
+
+ explicit CTLHandle(snd_hctl_t *handle_) : handle(handle_) {}
+ CTLHandle() : handle(NULL) {}
+ };
+
+ std::map<std::string, CTLHandle> m_ctlHandles;
+
+ std::vector<int> m_fdMonitorIds;
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.cpp b/xbmc/cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.cpp
new file mode 100644
index 0000000..2ccab00
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.cpp
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011-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 "CoreAudioHelpers.h"
+
+#include <sstream>
+
+// Helper Functions
+std::string GetError(OSStatus error)
+{
+ char buffer[128] = {};
+
+ *(UInt32 *)(buffer + 1) = CFSwapInt32HostToBig(error);
+ if (isprint(buffer[1]) && isprint(buffer[2]) &&
+ isprint(buffer[3]) && isprint(buffer[4]))
+ {
+ buffer[0] = buffer[5] = '\'';
+ buffer[6] = '\0';
+ }
+ else
+ {
+ // no, format it as an integer
+ sprintf(buffer, "%d", (int)error);
+ }
+
+ return std::string(buffer);
+}
+
+const char* StreamDescriptionToString(AudioStreamBasicDescription desc, std::string &str)
+{
+ char fourCC[5] = {
+ (char)((desc.mFormatID >> 24) & 0xFF),
+ (char)((desc.mFormatID >> 16) & 0xFF),
+ (char)((desc.mFormatID >> 8) & 0xFF),
+ (char) (desc.mFormatID & 0xFF),
+ 0
+ };
+
+ std::stringstream sstr;
+ switch (desc.mFormatID)
+ {
+ case kAudioFormatLinearPCM:
+ sstr << "["
+ << fourCC
+ << "] "
+ << ((desc.mFormatFlags & kAudioFormatFlagIsNonMixable) ? "" : "Mixable " )
+ << ((desc.mFormatFlags & kAudioFormatFlagIsNonInterleaved) ? "Non-" : "" )
+ << "Interleaved "
+ << desc.mChannelsPerFrame
+ << " Channel "
+ << desc.mBitsPerChannel
+ << "-bit "
+ << ((desc.mFormatFlags & kAudioFormatFlagIsFloat) ? "Floating Point " : "Signed Integer ")
+ << ((desc.mFormatFlags & kAudioFormatFlagIsBigEndian) ? "BE" : "LE")
+ << " ("
+ << (UInt32)desc.mSampleRate
+ << "Hz)";
+ str = sstr.str();
+ break;
+ case kAudioFormatAC3:
+ sstr << "["
+ << fourCC
+ << "] "
+ << ((desc.mFormatFlags & kAudioFormatFlagIsBigEndian) ? "BE" : "LE")
+ << " AC-3/DTS ("
+ << (UInt32)desc.mSampleRate
+ << "Hz)";
+ str = sstr.str();
+ break;
+ case kAudioFormat60958AC3:
+ sstr << "["
+ << fourCC
+ << "] AC-3/DTS for S/PDIF "
+ << ((desc.mFormatFlags & kAudioFormatFlagIsBigEndian) ? "BE" : "LE")
+ << " ("
+ << (UInt32)desc.mSampleRate
+ << "Hz)";
+ str = sstr.str();
+ break;
+ default:
+ sstr << "["
+ << fourCC
+ << "]";
+ break;
+ }
+ return str.c_str();
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h b/xbmc/cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h
new file mode 100644
index 0000000..082c8e1
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) 2011-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 <string>
+
+#include <AudioToolbox/AudioToolbox.h>
+
+// Helper Functions
+std::string GetError(OSStatus error);
+const char* StreamDescriptionToString(AudioStreamBasicDescription desc, std::string &str);
diff --git a/xbmc/cores/AudioEngine/Sinks/osx/AEDeviceEnumerationOSX.cpp b/xbmc/cores/AudioEngine/Sinks/osx/AEDeviceEnumerationOSX.cpp
new file mode 100644
index 0000000..4074ee7
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/osx/AEDeviceEnumerationOSX.cpp
@@ -0,0 +1,837 @@
+/*
+ * Copyright (C) 2014-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 "AEDeviceEnumerationOSX.h"
+
+#include "cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h"
+#include "cores/AudioEngine/Sinks/osx/CoreAudioChannelLayout.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <sstream>
+
+#define CA_MAX_CHANNELS 72
+// default channel map - in case it can't be fetched from the device
+static enum AEChannel CAChannelMap[CA_MAX_CHANNELS + 1] = {
+ AE_CH_FL , AE_CH_FR , AE_CH_BL , AE_CH_BR , AE_CH_FC , AE_CH_LFE , AE_CH_SL , AE_CH_SR ,
+ AE_CH_UNKNOWN1 , AE_CH_UNKNOWN2 , AE_CH_UNKNOWN3 , AE_CH_UNKNOWN4 ,
+ AE_CH_UNKNOWN5 , AE_CH_UNKNOWN6 , AE_CH_UNKNOWN7 , AE_CH_UNKNOWN8 ,
+ AE_CH_UNKNOWN9 , AE_CH_UNKNOWN10 , AE_CH_UNKNOWN11 , AE_CH_UNKNOWN12 ,
+ AE_CH_UNKNOWN13 , AE_CH_UNKNOWN14 , AE_CH_UNKNOWN15 , AE_CH_UNKNOWN16 ,
+ AE_CH_UNKNOWN17 , AE_CH_UNKNOWN18 , AE_CH_UNKNOWN19 , AE_CH_UNKNOWN20 ,
+ AE_CH_UNKNOWN21 , AE_CH_UNKNOWN22 , AE_CH_UNKNOWN23 , AE_CH_UNKNOWN24 ,
+ AE_CH_UNKNOWN25 , AE_CH_UNKNOWN26 , AE_CH_UNKNOWN27 , AE_CH_UNKNOWN28 ,
+ AE_CH_UNKNOWN29 , AE_CH_UNKNOWN30 , AE_CH_UNKNOWN31 , AE_CH_UNKNOWN32 ,
+ AE_CH_UNKNOWN33 , AE_CH_UNKNOWN34 , AE_CH_UNKNOWN35 , AE_CH_UNKNOWN36 ,
+ AE_CH_UNKNOWN37 , AE_CH_UNKNOWN38 , AE_CH_UNKNOWN39 , AE_CH_UNKNOWN40 ,
+ AE_CH_UNKNOWN41 , AE_CH_UNKNOWN42 , AE_CH_UNKNOWN43 , AE_CH_UNKNOWN44 ,
+ AE_CH_UNKNOWN45 , AE_CH_UNKNOWN46 , AE_CH_UNKNOWN47 , AE_CH_UNKNOWN48 ,
+ AE_CH_UNKNOWN49 , AE_CH_UNKNOWN50 , AE_CH_UNKNOWN51 , AE_CH_UNKNOWN52 ,
+ AE_CH_UNKNOWN53 , AE_CH_UNKNOWN54 , AE_CH_UNKNOWN55 , AE_CH_UNKNOWN56 ,
+ AE_CH_UNKNOWN57 , AE_CH_UNKNOWN58 , AE_CH_UNKNOWN59 , AE_CH_UNKNOWN60 ,
+ AE_CH_UNKNOWN61 , AE_CH_UNKNOWN62 , AE_CH_UNKNOWN63 , AE_CH_UNKNOWN64 ,
+
+ AE_CH_NULL
+};
+
+AEDeviceEnumerationOSX::AEDeviceEnumerationOSX(AudioDeviceID deviceID)
+: m_deviceID(deviceID)
+, m_isPlanar(false)
+, m_caDevice(deviceID)
+{
+ Enumerate();
+}
+
+bool AEDeviceEnumerationOSX::Enumerate()
+{
+ AudioStreamIdList streamList;
+ bool isDigital = isDigitalDevice();
+ bool ret = false;
+ UInt32 transportType = m_caDevice.GetTransportType();
+ m_caStreamInfos.clear();
+ m_isPlanar = true;
+ m_deviceName = m_caDevice.GetName();
+
+ if (m_caDevice.GetStreams(&streamList))
+ {
+ for (UInt32 streamIdx = 0; streamIdx < streamList.size(); streamIdx++)
+ {
+ caStreamInfo info;
+ info.streamID = streamList[streamIdx];
+ info.numChannels = m_caDevice.GetNumChannelsOfStream(streamIdx);
+ // one stream with num channels other then 1 is enough to make this device non-planar
+ if (info.numChannels != 1)
+ m_isPlanar = false;
+
+ CCoreAudioStream::GetAvailablePhysicalFormats(streamList[streamIdx], &info.formatList);
+
+ CCoreAudioStream::GetAvailableVirtualFormats(streamList[streamIdx], &info.formatListVirt);
+
+ hasPassthroughOrDigitalFormats(info.formatList, info.hasPassthroughFormats, info.isDigital);
+
+ info.isDigital |= isDigital;
+ info.deviceType = getDeviceType(info.hasPassthroughFormats, info.isDigital, info.numChannels, transportType);
+ m_caStreamInfos.push_back(info);
+ }
+ ret = true;
+ }
+
+ return ret;
+}
+
+// device/stream is digital if the transportType is digital
+// or the devicename suggests that it is digital
+bool AEDeviceEnumerationOSX::isDigitalDevice() const
+{
+ bool isDigital = m_caDevice.IsDigital();
+
+ // if it is no digital stream per definition
+ // check if the device name suggests that it is digital
+ // (some hackintonshs are not so smart in announcing correct
+ // ca devices ...
+ if (!isDigital)
+ {
+ std::string devNameLower = m_caDevice.GetName();
+ StringUtils::ToLower(devNameLower);
+ isDigital = devNameLower.find("digital") != std::string::npos;
+ }
+ return isDigital;
+}
+
+void AEDeviceEnumerationOSX::hasPassthroughOrDigitalFormats(const StreamFormatList &formatList, bool &hasPassthroughFormats, bool &hasDigitalFormat) const
+{
+ hasDigitalFormat = false;
+ hasPassthroughFormats = false;
+
+ for(UInt32 formatIdx = 0; formatIdx < formatList.size(); formatIdx++)
+ {
+ const AudioStreamBasicDescription &desc = formatList[formatIdx].mFormat;
+ if (desc.mFormatID == kAudioFormatAC3 || desc.mFormatID == kAudioFormat60958AC3)
+ {
+ hasDigitalFormat = true;
+ hasPassthroughFormats = true;
+ break;
+ }
+ else
+ {
+ // PassthroughFormat 2ch 16bit LE 48000Hz / 192000Hz */
+ if (desc.mBitsPerChannel == 16 &&
+ !(desc.mFormatFlags & kAudioFormatFlagIsBigEndian) &&
+ desc.mChannelsPerFrame == 2 &&
+ (desc.mSampleRate == 48000 || desc.mSampleRate == 192000))
+ {
+ hasPassthroughFormats = true;
+ }
+ }
+ }
+}
+
+enum AEDeviceType AEDeviceEnumerationOSX::getDeviceType(bool hasPassthroughFormats,
+ bool isDigital,
+ UInt32 numChannels,
+ UInt32 transportType) const
+{
+ // flag indicating that the device name "sounds" like HDMI
+ bool hasHdmiName = m_deviceName.find("HDMI") != std::string::npos;
+ // flag indicating that the device name "sounds" like DisplayPort
+ bool hasDisplayPortName = m_deviceName.find("DisplayPort") != std::string::npos;
+ enum AEDeviceType deviceType = AE_DEVTYPE_PCM;//default
+
+ // decide the type of the device based on the discovered information
+ // in the streams
+ // device defaults to PCM (see start of the while loop)
+ // it can be HDMI, DisplayPort or Optical
+ // for all of those types it needs to support
+ // passthroughformats and needs to be a digital port
+ if (hasPassthroughFormats && isDigital)
+ {
+ // if the max number of channels was more then 2
+ // this can be HDMI or DisplayPort or Thunderbolt
+ if (numChannels > 2)
+ {
+ // either the devicename suggests its HDMI
+ // or CA reported the transportType as HDMI
+ if (hasHdmiName || transportType == kIOAudioDeviceTransportTypeHdmi)
+ deviceType = AE_DEVTYPE_HDMI;
+
+ // either the devicename suggests its DisplayPort
+ // or CA reported the transportType as DisplayPort or Thunderbolt
+ if (hasDisplayPortName ||
+ transportType == kIOAudioDeviceTransportTypeDisplayPort ||
+ transportType == kIOAudioDeviceTransportTypeThunderbolt)
+ deviceType = AE_DEVTYPE_DP;
+ }
+ else// treat all other digital passthrough devices as optical
+ deviceType = AE_DEVTYPE_IEC958;
+
+ //treat all other digital devices as HDMI to let options open to the user
+ if (deviceType == AE_DEVTYPE_PCM)
+ deviceType = AE_DEVTYPE_HDMI;
+ }
+
+ // devicename based overwrites from former code - maybe FIXME at some point when we
+ // are sure that the upper detection does its job in all[tm] use cases
+ if (hasHdmiName)
+ deviceType = AE_DEVTYPE_HDMI;
+ if (hasDisplayPortName)
+ deviceType = AE_DEVTYPE_DP;
+
+ return deviceType;
+}
+
+CADeviceList AEDeviceEnumerationOSX::GetDeviceInfoList() const
+{
+ CADeviceList list;
+ UInt32 numDevices = m_caStreamInfos.size();
+ if (m_isPlanar)
+ numDevices = 1;
+
+ for (UInt32 streamIdx = 0; streamIdx < numDevices; streamIdx++)
+ {
+ CAEDeviceInfo deviceInfo;
+ struct CADeviceInstance devInstance;
+ devInstance.audioDeviceId = m_deviceID;
+ devInstance.streamIndex = streamIdx;
+ devInstance.sourceId = INT_MAX;//don't set audio source by default
+
+ deviceInfo.m_deviceName = getDeviceNameForStream(streamIdx);
+ deviceInfo.m_displayName = m_deviceName;
+ deviceInfo.m_displayNameExtra = getExtraDisplayNameForStream(streamIdx);
+ deviceInfo.m_channels = getChannelInfoForStream(streamIdx);
+ deviceInfo.m_sampleRates = getSampleRateListForStream(streamIdx);
+ deviceInfo.m_dataFormats = getFormatListForStream(streamIdx);
+ deviceInfo.m_streamTypes = getTypeListForStream(streamIdx);
+ deviceInfo.m_deviceType = m_caStreamInfos[streamIdx].deviceType;
+ deviceInfo.m_wantsIECPassthrough = true;
+
+ CoreAudioDataSourceList sourceList;
+ // if this enumerator contains multiple devices with more then 1 source we add :source suffixes to the
+ // device names and overwrite the extraname with the source name
+ if (numDevices == 1 && m_caDevice.GetDataSources(&sourceList) && sourceList.size() > 1)
+ {
+ for (unsigned sourceIdx = 0; sourceIdx < sourceList.size(); sourceIdx++)
+ {
+ std::stringstream sourceIdxStr;
+ sourceIdxStr << sourceIdx;
+ deviceInfo.m_deviceName = getDeviceNameForStream(streamIdx) + ":source" + sourceIdxStr.str();
+ deviceInfo.m_displayNameExtra = m_caDevice.GetDataSourceName(sourceList[sourceIdx]);
+ devInstance.sourceId = sourceList[sourceIdx];
+ list.push_back(std::make_pair(devInstance, deviceInfo));
+ }
+ }
+ else
+ list.push_back(std::make_pair(devInstance, deviceInfo));
+ }
+ return list;
+}
+
+unsigned int AEDeviceEnumerationOSX::GetNumPlanes() const
+{
+ if (m_isPlanar)
+ return m_caStreamInfos.size();
+ else
+ return 1;//interleaved - one plane
+}
+
+bool AEDeviceEnumerationOSX::hasSampleRate(const AESampleRateList &list, const unsigned int samplerate) const
+{
+ for (size_t i = 0; i < list.size(); ++i)
+ {
+ if (list[i] == samplerate)
+ return true;
+ }
+ return false;
+}
+
+bool AEDeviceEnumerationOSX::hasDataFormat(const AEDataFormatList &list, const enum AEDataFormat format) const
+{
+ for (size_t i = 0; i < list.size(); ++i)
+ {
+ if (list[i] == format)
+ return true;
+ }
+ return false;
+}
+
+bool AEDeviceEnumerationOSX::hasDataType(const AEDataTypeList &list, CAEStreamInfo::DataType type) const
+{
+ for (size_t i = 0; i < list.size(); ++i)
+ {
+ if (list[i] == type)
+ return true;
+ }
+ return false;
+}
+
+AEDataFormatList AEDeviceEnumerationOSX::getFormatListForStream(UInt32 streamIdx) const
+{
+ AEDataFormatList returnDataFormatList;
+ if (streamIdx >= m_caStreamInfos.size())
+ return returnDataFormatList;
+
+ // check the streams
+ const StreamFormatList &formatList = m_caStreamInfos[streamIdx].formatList;
+ for(UInt32 formatIdx = 0; formatIdx < formatList.size(); formatIdx++)
+ {
+ AudioStreamBasicDescription formatDesc = formatList[formatIdx].mFormat;
+ AEDataFormatList aeFormatList = caFormatToAE(formatDesc, m_caStreamInfos[streamIdx].isDigital);
+ for (UInt32 formatListIdx = 0; formatListIdx < aeFormatList.size(); formatListIdx++)
+ {
+ if (!hasDataFormat(returnDataFormatList, aeFormatList[formatListIdx]))
+ returnDataFormatList.push_back(aeFormatList[formatListIdx]);
+ }
+ }
+ return returnDataFormatList;
+}
+
+AEDataTypeList AEDeviceEnumerationOSX::getTypeListForStream(UInt32 streamIdx) const
+{
+ AEDataTypeList returnDataTypeList;
+ if (streamIdx >= m_caStreamInfos.size())
+ return returnDataTypeList;
+
+ // check the streams
+ const StreamFormatList &formatList = m_caStreamInfos[streamIdx].formatList;
+ for(UInt32 formatIdx = 0; formatIdx < formatList.size(); formatIdx++)
+ {
+ AudioStreamBasicDescription formatDesc = formatList[formatIdx].mFormat;
+ AEDataTypeList aeTypeList = caFormatToAEType(formatDesc, m_caStreamInfos[streamIdx].isDigital);
+ for (UInt32 typeListIdx = 0; typeListIdx < aeTypeList.size(); typeListIdx++)
+ {
+ if (!hasDataType(returnDataTypeList, aeTypeList[typeListIdx]))
+ returnDataTypeList.push_back(aeTypeList[typeListIdx]);
+ }
+ }
+ return returnDataTypeList;
+}
+
+CAEChannelInfo AEDeviceEnumerationOSX::getChannelInfoForStream(UInt32 streamIdx) const
+{
+ CAEChannelInfo channelInfo;
+ if (streamIdx >= m_caStreamInfos.size())
+ return channelInfo;
+
+ if (m_isPlanar)
+ {
+ //get channel map to match the devices channel layout as set in audio-midi-setup
+ GetAEChannelMap(channelInfo, GetNumPlanes());
+ }
+ else
+ {
+ //get channel map to match the devices channel layout as set in audio-midi-setup
+ GetAEChannelMap(channelInfo, m_caDevice.GetNumChannelsOfStream(streamIdx));
+ }
+ return channelInfo;
+}
+
+AEDataFormatList AEDeviceEnumerationOSX::caFormatToAE(const AudioStreamBasicDescription &formatDesc, bool isDigital) const
+{
+ AEDataFormatList formatList;
+ // add stream format info
+ switch (formatDesc.mFormatID)
+ {
+ case kAudioFormatAC3:
+ case kAudioFormat60958AC3:
+ break;
+ default:
+ switch(formatDesc.mBitsPerChannel)
+ {
+ case 16:
+ if (formatDesc.mFormatFlags & kAudioFormatFlagIsBigEndian)
+ {
+ formatList.push_back(AE_FMT_S16BE);
+ }
+ else
+ {
+ formatList.push_back(AE_FMT_S16LE);
+ }
+ formatList.push_back(AE_FMT_S16NE); // if we support either LE or BE - we always support NE too...
+ break;
+ case 24:
+ if (formatDesc.mFormatFlags & kAudioFormatFlagIsBigEndian)
+ {
+ formatList.push_back(AE_FMT_S24BE3);
+ }
+ else
+ {
+ formatList.push_back(AE_FMT_S24LE3);
+ }
+ formatList.push_back(AE_FMT_S24NE3); // if we support either LE or BE - we always support NE too...
+ break;
+ case 32:
+ if (formatDesc.mFormatFlags & kAudioFormatFlagIsFloat)
+ {
+ formatList.push_back(AE_FMT_FLOAT);
+ }
+ else
+ {
+ if (formatDesc.mFormatFlags & kAudioFormatFlagIsBigEndian)
+ {
+ formatList.push_back(AE_FMT_S32BE);
+ }
+ else
+ {
+ formatList.push_back(AE_FMT_S32LE);
+ }
+ formatList.push_back(AE_FMT_S32NE); // if we support either LE or BE - we always support NE too...
+ }
+ break;
+ }
+ break;
+ }
+ return formatList;
+}
+
+AEDataTypeList AEDeviceEnumerationOSX::caFormatToAEType(const AudioStreamBasicDescription &formatDesc, bool isDigital) const
+{
+ AEDataTypeList typeList;
+ // add stream format info
+ switch (formatDesc.mFormatID)
+ {
+ case kAudioFormatAC3:
+ case kAudioFormat60958AC3:
+ typeList.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ typeList.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ typeList.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ typeList.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ typeList.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ break;
+ default:
+ switch(formatDesc.mBitsPerChannel)
+ {
+ case 16:
+ if (formatDesc.mFormatFlags & kAudioFormatFlagIsBigEndian)
+ ;
+ else
+ {
+ /* Passthrough is possible with a 2ch digital output */
+ if (formatDesc.mChannelsPerFrame == 2 && isDigital)
+ {
+ if (formatDesc.mSampleRate == 48000)
+ {
+ typeList.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ typeList.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ typeList.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ typeList.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ typeList.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+ }
+ else if (formatDesc.mSampleRate == 192000)
+ {
+ typeList.push_back(CAEStreamInfo::STREAM_TYPE_EAC3);
+ }
+ }
+ }
+ break;
+ }
+ break;
+ }
+ return typeList;
+}
+
+AESampleRateList AEDeviceEnumerationOSX::getSampleRateListForStream(UInt32 streamIdx) const
+{
+ AESampleRateList returnSampleRateList;
+ if (streamIdx >= m_caStreamInfos.size())
+ return returnSampleRateList;
+
+ // check the streams
+ const StreamFormatList &formatList = m_caStreamInfos[streamIdx].formatList;
+ for(UInt32 formatIdx = 0; formatIdx < formatList.size(); formatIdx++)
+ {
+ AudioStreamBasicDescription formatDesc = formatList[formatIdx].mFormat;
+ // add sample rate info
+ // for devices which return kAudioStreamAnyRatee
+ // we add 44.1khz and 48khz - user can use
+ // the "fixed" audio config to force one of them
+ if (formatDesc.mSampleRate == kAudioStreamAnyRate)
+ {
+ CLog::Log(LOGINFO, "{} reported samplerate is kAudioStreamAnyRate adding 44.1khz and 48khz",
+ __FUNCTION__);
+ formatDesc.mSampleRate = 44100;
+ if (!hasSampleRate(returnSampleRateList, formatDesc.mSampleRate))
+ returnSampleRateList.push_back(formatDesc.mSampleRate);
+ formatDesc.mSampleRate = 48000;
+ }
+
+ if (!hasSampleRate(returnSampleRateList, formatDesc.mSampleRate))
+ returnSampleRateList.push_back(formatDesc.mSampleRate);
+ }
+
+ return returnSampleRateList;
+}
+
+std::string AEDeviceEnumerationOSX::getDeviceNameForStream(UInt32 streamIdx) const
+{
+ std::stringstream deviceIdStr;
+ deviceIdStr << m_deviceID;
+
+ // device name is the devicename from coreaudio + the device id to make it unique
+ // for example devicename could be DisplayPort and couldn't be distinguished if multiple
+ // DisplayPort devices are available
+ std::string deviceName = m_deviceName + "-" + deviceIdStr.str();
+ if (m_isPlanar)// planar devices are saved as :stream0
+ {
+ deviceName += ":stream0";
+ }
+ else
+ {
+ std::stringstream streamIdxStr;
+ streamIdxStr << streamIdx;
+ deviceName += ":stream" + streamIdxStr.str();
+ }
+ return deviceName;
+}
+
+std::string AEDeviceEnumerationOSX::getExtraDisplayNameForStream(UInt32 streamIdx) const
+{
+ // for distinguishing the streams inside one device we add
+ // the corresponding channels to the extraDisplayName
+ // planar devices are ignored here as their streams are
+ // the channels not different subdevices
+ if (m_caStreamInfos.size() > 1 && !m_isPlanar)
+ {
+ // build a string with the channels for this stream
+ UInt32 startChannel = 0;
+ CCoreAudioStream::GetStartingChannelInDevice(m_caStreamInfos[streamIdx].streamID, startChannel);
+ UInt32 numChannels = m_caDevice.GetNumChannelsOfStream(streamIdx);
+ std::stringstream extraName;
+ extraName << "Channels ";
+ extraName << startChannel;
+ extraName << " - ";
+ extraName << startChannel + numChannels - 1;
+ CLog::Log(LOGINFO,
+ "{} adding stream {} as pseudo device with start channel {} and {} channels total",
+ __FUNCTION__, (unsigned int)streamIdx, (unsigned int)startChannel,
+ (unsigned int)numChannels);
+ return extraName.str();
+ }
+
+ //for all other devices use the datasource as extraname
+ return m_caDevice.GetCurrentDataSourceName();
+}
+
+float AEDeviceEnumerationOSX::scoreSampleRate(Float64 destinationRate, unsigned int sourceRate) const
+{
+ float score = 0;
+ double intPortion;
+ double fracPortion = modf(destinationRate / sourceRate, &intPortion);
+
+ score += (1 - fracPortion) * 1000; // prefer sample rates that are multiples of the source sample rate
+ score += (intPortion == 1.0) ? 500 : 0; // prefer exact matches over other multiples
+ score += (intPortion > 1 && intPortion < 100) ? (100 - intPortion) / 100 * 100 : 0; // prefer smaller multiples otherwise
+
+ return score;
+}
+
+float AEDeviceEnumerationOSX::ScoreFormat(const AudioStreamBasicDescription &formatDesc, const AEAudioFormat &format) const
+{
+ float score = 0;
+
+ if (format.m_streamInfo.m_type != CAEStreamInfo::STREAM_TYPE_NULL)
+ {
+ if (formatDesc.mBitsPerChannel != 16)
+ return score;
+ if (formatDesc.mSampleRate != format.m_sampleRate)
+ return score;
+ if (formatDesc.mChannelsPerFrame != format.m_channelLayout.Count())
+ return score;
+ score += 5;
+
+ if (formatDesc.mFormatID == kAudioFormat60958AC3 ||
+ formatDesc.mFormatID == 'IAC3' ||
+ formatDesc.mFormatID == kAudioFormatAC3)
+ {
+ score += 1;
+ }
+ }
+ // non-passthrough, whatever works is fine
+ else if (formatDesc.mFormatID == kAudioFormatLinearPCM)
+ {
+ score += scoreSampleRate(formatDesc.mSampleRate, format.m_sampleRate);
+
+ if (formatDesc.mChannelsPerFrame == format.m_channelLayout.Count())
+ score += 5;
+ else if (formatDesc.mChannelsPerFrame > format.m_channelLayout.Count())
+ score += 1;
+ if (format.m_dataFormat == AE_FMT_FLOAT || format.m_dataFormat == AE_FMT_FLOATP)
+ { // for float, prefer the highest bitdepth we have
+ if (formatDesc.mBitsPerChannel >= 16)
+ score += (formatDesc.mBitsPerChannel / 8);
+ }
+ else
+ {
+ if (formatDesc.mBitsPerChannel == CAEUtil::DataFormatToBits(format.m_dataFormat))
+ score += 5;
+ else if (formatDesc.mBitsPerChannel > CAEUtil::DataFormatToBits(format.m_dataFormat))
+ score += 1;
+ }
+ }
+
+ return score;
+}
+
+bool AEDeviceEnumerationOSX::FindSuitableFormatForStream(UInt32 &streamIdx, const AEAudioFormat &format, bool virt, AudioStreamBasicDescription &outputFormat, AudioStreamID &outputStream) const
+{
+ CLog::Log(LOGDEBUG, "{}: Finding stream for format {}", __FUNCTION__,
+ CAEUtil::DataFormatToStr(format.m_dataFormat));
+
+ bool formatFound = false;
+ float outputScore = 0;
+ UInt32 streamIdxStart = streamIdx;
+ UInt32 streamIdxEnd = streamIdx + 1;
+ UInt32 streamIdxCurrent = streamIdx;
+
+ if (streamIdx == INT_MAX)
+ {
+ streamIdxStart = 0;
+ streamIdxEnd = m_caStreamInfos.size();
+ streamIdxCurrent = 0;
+ }
+
+ if (streamIdxCurrent >= m_caStreamInfos.size())
+ return false;
+
+ // loop over all streams or over given streams (depends on initial value of param streamIdx
+ for(streamIdxCurrent = streamIdxStart; streamIdxCurrent < streamIdxEnd; streamIdxCurrent++)
+ {
+
+ // Probe physical or virtual formats
+ const StreamFormatList *formats = &m_caStreamInfos[streamIdxCurrent].formatList;
+ if (virt)
+ formats = &m_caStreamInfos[streamIdxCurrent].formatListVirt;
+
+ for (StreamFormatList::const_iterator j = formats->begin(); j != formats->end(); ++j)
+ {
+ AudioStreamBasicDescription formatDesc = j->mFormat;
+
+ // for devices with kAudioStreamAnyRate
+ // assume that the user uses a fixed config
+ // and knows what he is doing - so we use
+ // the requested samplerate here
+ if (formatDesc.mSampleRate == kAudioStreamAnyRate)
+ formatDesc.mSampleRate = format.m_sampleRate;
+
+ float score = ScoreFormat(formatDesc, format);
+
+ std::string formatString;
+ if (!virt)
+ CLog::Log(LOGDEBUG, "{}: Physical Format: {} rated {:f}", __FUNCTION__,
+ StreamDescriptionToString(formatDesc, formatString), score);
+ else
+ CLog::Log(LOGDEBUG, "{}: Virtual Format: {} rated {:f}", __FUNCTION__,
+ StreamDescriptionToString(formatDesc, formatString), score);
+
+ if (score > outputScore)
+ {
+ outputScore = score;
+ outputFormat = formatDesc;
+ outputStream = m_caStreamInfos[streamIdxCurrent].streamID;
+ streamIdx = streamIdxCurrent;// return the streamIdx for the caller
+ formatFound = true;
+ }
+ }
+ }
+
+ if (m_isPlanar)
+ outputFormat.mChannelsPerFrame = std::min((size_t)format.m_channelLayout.Count(), m_caStreamInfos.size());
+
+ return formatFound;
+}
+
+// map coraudio channel labels to activeae channel labels
+enum AEChannel AEDeviceEnumerationOSX::caChannelToAEChannel(const AudioChannelLabel &CAChannelLabel) const
+{
+ enum AEChannel ret = AE_CH_NULL;
+ static unsigned int unknownChannel = AE_CH_UNKNOWN1;
+ switch(CAChannelLabel)
+ {
+ case kAudioChannelLabel_Left:
+ ret = AE_CH_FL;
+ break;
+ case kAudioChannelLabel_Right:
+ ret = AE_CH_FR;
+ break;
+ case kAudioChannelLabel_Center:
+ ret = AE_CH_FC;
+ break;
+ case kAudioChannelLabel_LFEScreen:
+ ret = AE_CH_LFE;
+ break;
+ case kAudioChannelLabel_LeftSurroundDirect:
+ ret = AE_CH_SL;
+ break;
+ case kAudioChannelLabel_RightSurroundDirect:
+ ret = AE_CH_SR;
+ break;
+ case kAudioChannelLabel_LeftCenter:
+ ret = AE_CH_FLOC;
+ break;
+ case kAudioChannelLabel_RightCenter:
+ ret = AE_CH_FROC;
+ break;
+ case kAudioChannelLabel_CenterSurround:
+ ret = AE_CH_TC;
+ break;
+ case kAudioChannelLabel_LeftSurround:
+ ret = AE_CH_SL;
+ break;
+ case kAudioChannelLabel_RightSurround:
+ ret = AE_CH_SR;
+ break;
+ case kAudioChannelLabel_VerticalHeightLeft:
+ ret = AE_CH_TFL;
+ break;
+ case kAudioChannelLabel_VerticalHeightRight:
+ ret = AE_CH_TFR;
+ break;
+ case kAudioChannelLabel_VerticalHeightCenter:
+ ret = AE_CH_TFC;
+ break;
+ case kAudioChannelLabel_TopCenterSurround:
+ ret = AE_CH_TC;
+ break;
+ case kAudioChannelLabel_TopBackLeft:
+ ret = AE_CH_TBL;
+ break;
+ case kAudioChannelLabel_TopBackRight:
+ ret = AE_CH_TBR;
+ break;
+ case kAudioChannelLabel_TopBackCenter:
+ ret = AE_CH_TBC;
+ break;
+ case kAudioChannelLabel_RearSurroundLeft:
+ ret = AE_CH_BL;
+ break;
+ case kAudioChannelLabel_RearSurroundRight:
+ ret = AE_CH_BR;
+ break;
+ case kAudioChannelLabel_LeftWide:
+ ret = AE_CH_BLOC;
+ break;
+ case kAudioChannelLabel_RightWide:
+ ret = AE_CH_BROC;
+ break;
+ case kAudioChannelLabel_LFE2:
+ ret = AE_CH_LFE;
+ break;
+ case kAudioChannelLabel_LeftTotal:
+ ret = AE_CH_FL;
+ break;
+ case kAudioChannelLabel_RightTotal:
+ ret = AE_CH_FR;
+ break;
+ case kAudioChannelLabel_HearingImpaired:
+ ret = AE_CH_FC;
+ break;
+ case kAudioChannelLabel_Narration:
+ ret = AE_CH_FC;
+ break;
+ case kAudioChannelLabel_Mono:
+ ret = AE_CH_FC;
+ break;
+ case kAudioChannelLabel_DialogCentricMix:
+ ret = AE_CH_FC;
+ break;
+ case kAudioChannelLabel_CenterSurroundDirect:
+ ret = AE_CH_TC;
+ break;
+ case kAudioChannelLabel_Haptic:
+ ret = AE_CH_FC;
+ break;
+ default:
+ ret = (enum AEChannel)unknownChannel++;
+ }
+ if (unknownChannel == AE_CH_MAX)
+ unknownChannel = AE_CH_UNKNOWN1;
+
+ return ret;
+}
+
+//Note: in multichannel mode CA will either pull 2 channels of data (stereo) or 6/8 channels of data
+//(every speaker setup with more then 2 speakers). The difference between the number of real speakers
+//and 6/8 channels needs to be padded with unknown channels so that the sample size fits 6/8 channels
+//
+//device [in] - the device whose channel layout should be used
+//channelMap [in/out] - if filled it will it indicates that we are called from initialize and we log the requested map, out returns the channelMap for device
+//channelsPerFrame [in] - the number of channels this device is configured to (e.x. 2 or 6/8)
+void AEDeviceEnumerationOSX::GetAEChannelMap(CAEChannelInfo &channelMap, unsigned int channelsPerFrame) const
+{
+ CCoreAudioChannelLayout calayout;
+ bool logMapping = channelMap.Count() > 0; // only log if the engine requests a layout during init
+ bool mapAvailable = false;
+ unsigned int numberChannelsInDeviceLayout = CA_MAX_CHANNELS; // default number of channels from CAChannelMap
+ AudioChannelLayout *layout = NULL;
+
+ // try to fetch either the multichannel or the stereo channel layout from the device
+ if (channelsPerFrame == 2 || channelMap.Count() == 2)
+ mapAvailable = m_caDevice.GetPreferredChannelLayoutForStereo(calayout);
+ else
+ mapAvailable = m_caDevice.GetPreferredChannelLayout(calayout);
+
+ // if a map was fetched - check if it is usable
+ if (mapAvailable)
+ {
+ layout = calayout;
+ if (layout == NULL || layout->mChannelLayoutTag != kAudioChannelLayoutTag_UseChannelDescriptions)
+ mapAvailable = false;// wrong map format
+ else
+ numberChannelsInDeviceLayout = layout->mNumberChannelDescriptions;
+ }
+
+ // start the mapping action
+ // the number of channels to be added to the outgoing channelmap
+ // this is CA_MAX_CHANNELS at max and might be lower for some output devices (channelsPerFrame)
+ unsigned int numChannelsToMap = std::min((unsigned int)CA_MAX_CHANNELS, (unsigned int)channelsPerFrame);
+
+ // if there was a map fetched we force the number of
+ // channels to map to channelsPerFrame (this allows mapping
+ // of more then CA_MAX_CHANNELS if needed)
+ if (mapAvailable)
+ numChannelsToMap = channelsPerFrame;
+
+ std::string layoutStr;
+
+ if (logMapping)
+ {
+ CLog::Log(LOGDEBUG, "{} Engine requests layout {}", __FUNCTION__, ((std::string)channelMap));
+
+ if (mapAvailable)
+ CLog::Log(LOGDEBUG, "{} trying to map to {} layout: {}", __FUNCTION__,
+ channelsPerFrame == 2 ? "stereo" : "multichannel",
+ calayout.ChannelLayoutToString(*layout, layoutStr));
+ else
+ CLog::Log(LOGDEBUG, "{} no map available - using static multichannel map layout",
+ __FUNCTION__);
+ }
+
+ channelMap.Reset();// start with an empty map
+
+ for (unsigned int channel = 0; channel < numChannelsToMap; channel++)
+ {
+ // we only try to map channels which are defined in the device layout
+ enum AEChannel currentChannel;
+ if (channel < numberChannelsInDeviceLayout)
+ {
+ // get the channel from the fetched map
+ if (mapAvailable)
+ currentChannel = caChannelToAEChannel(layout->mChannelDescriptions[channel].mChannelLabel);
+ else// get the channel from the default map
+ currentChannel = CAChannelMap[channel];
+
+ }
+ else// fill with unknown channels
+ currentChannel = caChannelToAEChannel(kAudioChannelLabel_Unknown);
+
+ if(!channelMap.HasChannel(currentChannel))// only add if not already added
+ channelMap += currentChannel;
+ }
+
+ if (logMapping)
+ CLog::Log(LOGDEBUG, "{} mapped channels to layout {}", __FUNCTION__, ((std::string)channelMap));
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/osx/AEDeviceEnumerationOSX.h b/xbmc/cores/AudioEngine/Sinks/osx/AEDeviceEnumerationOSX.h
new file mode 100644
index 0000000..7baae8f
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/osx/AEDeviceEnumerationOSX.h
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2014-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/Sinks/osx/CoreAudioDevice.h"
+#include "cores/AudioEngine/Utils/AEAudioFormat.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+struct CADeviceInstance
+{
+ AudioDeviceID audioDeviceId;
+ unsigned int streamIndex;
+ unsigned int sourceId;
+};
+typedef std::vector< std::pair<struct CADeviceInstance, CAEDeviceInfo> > CADeviceList;
+
+//Hierarchy:
+// Device
+// - 1..n streams
+// 1..n formats
+// - 0..n sources
+//on non planar devices we have numstreams * numsources devices for our list
+//on planar devices we have 1 * numsources devices for our list
+class AEDeviceEnumerationOSX
+{
+public:
+ /*!
+ * @brief C'tor - initialises the Enumerator and calls Enumerate
+ * @param deviceID - the CoreAudio Device ID which will be the base of the enumerated device list
+ */
+ AEDeviceEnumerationOSX(AudioDeviceID deviceID);
+ // d'tor
+ ~AEDeviceEnumerationOSX() = default;
+
+ /*!
+ * @brief Gets the device list which was enumerated by the last call to Enumerate
+ * (which is also called in c'tor).
+ *
+ * @return Returns the device list.
+ */
+ CADeviceList GetDeviceInfoList() const;
+
+ /*!
+ * @brief Fetches all metadata from the CoreAudio device which is needed to generate a proper DeviceList for AE
+ * This method is always called from C'tor but can be called multiple times if the streams of a device
+ * changed. This fills m_caStreamInfos.
+ * After this call - GetDeviceInfoList will reflect the Enumerated metadata.
+ * @return false when streamlist couldn't be fetched from device - else true
+ */
+ bool Enumerate();
+
+ /*!
+ * @brief Returns the number of Planes for a device. This will be 1 for non-planar devices and > 1 for planar devices.
+ * @return Number of planes for this device.
+ */
+ unsigned int GetNumPlanes() const;
+
+ /*!
+ * @brief Checks if the m_deviceID belongs to a planar or non-planar device.
+ * @return true if m_deviceID belongs to a planar device - else false.
+ */
+ bool IsPlanar() const { return m_isPlanar; }
+
+ /*!
+ * @brief Tries to find a suitable CoreAudio format which matches the given AEAudioFormat as close as possible.
+ * @param streamIdx [in/out] - if streamIdx != INT_MAX only formats of the given streamIdx are checked
+ * if streamIdx == INT_MAX - formats of all streams in the device are considered
+ * On success this parameter returns the selected streamIdx.
+ *
+ * @param format [in] - the requested AE format which should be matched to the stream formats of CA
+ * @param outputFormat [out] - the found CA format which matches best to the requested AE format
+ * @param outputStream [out] - the coreaudio streamid which contains the coreaudio format returned in outputFormat
+ * @return true if a matching corea audio format was found - else false
+ */
+ bool FindSuitableFormatForStream(UInt32 &streamIdx, const AEAudioFormat &format, bool virt,
+ AudioStreamBasicDescription &outputFormat,
+ AudioStreamID &outputStream) const;
+
+ /*!
+ * @brief Returns the device name which belongs to m_deviceID without any stream/source suffixes
+ * @return the CA device name
+ */
+ std::string GetMasterDeviceName() const { return m_deviceName; }
+
+ /*!
+ * @brief Tries to return a proper channelmap from CA in a format AE understands
+ * @param channelMap [in/out] - returns the found channelmap in AE format
+ * if initialised with a map the number of channels is used to determine
+ * if stereo or multichannel map should be fetched
+ * @param channelsPerFrame [int] - the number of channels which should be mapped
+ * (also decides if stereo or multichannel map is fetched similar to channelMap param)
+ */
+ void GetAEChannelMap(CAEChannelInfo &channelMap, unsigned int channelsPerFrame) const;
+
+ /*!
+ * @brief Scores a format based on:
+ * 1. Matching passthrough characteristics (i.e. passthrough flag)
+ * 2. Matching sample rate.
+ * 3. Matching bits per channel (or higher).
+ * 4. Matching number of channels (or higher).
+ *
+ * @param formatDesc [in] - The CA FormatDescription which should be scored
+ * @param format [in] - the AE format which should be matched as good as possible
+ * @return - the score of formatDesc - higher scores indicate better matching to "format"
+ * (scores > 10000 indicate passthrough formats)
+ */
+ // public because its used in unit tests ...
+ float ScoreFormat(const AudioStreamBasicDescription &formatDesc, const AEAudioFormat &format) const;
+
+private:
+
+ /*!
+ * @brief Checks if this is a digital device based on CA transportType or name
+ * @return - true if this is a digital device - else false.
+ */
+ bool isDigitalDevice() const;
+
+ /*!
+ * @brief Checks if there are passthrough formats or digital formats
+ * (the latter are passthrough formats with dedicated format config like AC3/DTS)
+ * @param formatList [in] - the format list to be evaluated
+ * @param hasPassthroughFormats [out] - true if there were passthrough formats in the list
+ * @param hasDigitalFormat [out] - true if there were dedicated passthrough formats in the list
+ */
+ void hasPassthroughOrDigitalFormats(const StreamFormatList &formatList,
+ bool &hasPassthroughFormats,
+ bool &hasDigitalFormat) const;
+
+ /*!
+ * @brief Gets the AE devicetype for this device based the given criteria
+ * @param hasPassthroughFormats [in] - flag indicating that the device has passthrough formats
+ * @param isDigital [in] - flag indicating that the device is digital
+ * @param numChannels [in] - the number of channels of the device
+ * @param transportType [in] - the transportType of the device
+ * @return the AE devicetype
+ */
+ enum AEDeviceType getDeviceType(bool hasPassthroughFormats, bool isDigital,
+ UInt32 numChannels, UInt32 transportType) const;
+
+ /*!
+ * @brief Fetches all ca streams from the ca device and fills m_channelsPerStream and m_streams
+ */
+ void fillStreamList();
+
+ /*!
+ * @brief Scores a samplerate based on:
+ * 1. Prefer exact match
+ * 2. Prefer exact multiple of source samplerate and prefer the lowest
+ *
+ * @param destinationRate [in] - the destination samplerate to score
+ * @param sourceRate [in] - the sourceRate of the audio format - this is the samplerate the score is based on
+ * @return the score
+ */
+ float scoreSampleRate(Float64 destinationRate, unsigned int sourceRate) const;
+
+ bool hasSampleRate(const AESampleRateList &list, const unsigned int samplerate) const;
+ bool hasDataFormat(const AEDataFormatList &list, const enum AEDataFormat format) const;
+ bool hasDataType(const AEDataTypeList &list, CAEStreamInfo::DataType type) const;
+
+ /*!
+ * @brief Converts a CA format description to a list of AEFormat descriptions (as one format can result
+ * in more then 1 AE format - e.x. AC3 ca format results in AC3 and DTS AE format
+ *
+ * @param formatDesc [in] - The CA format description to be converted
+ * @param isDigital [in] - Flag indicating if the parent stream of formatDesc is digital
+ * (for allowing bitstreaming without dedicated AC3 format in CA)
+ * @return The list of converted AE formats.
+ */
+ AEDataFormatList caFormatToAE(const AudioStreamBasicDescription &formatDesc, bool isDigital) const;
+ AEDataTypeList caFormatToAEType(const AudioStreamBasicDescription &formatDesc, bool isDigital) const;
+
+
+ /*!
+ * @brief Convert a CA channel label to an AE channel.
+ * @param CAChannelLabel - the CA channel label to be converted
+ * @return the corresponding AEChannel
+ */
+ enum AEChannel caChannelToAEChannel(const AudioChannelLabel &CAChannelLabel) const;
+
+ // for filling out the AEDeviceInfo object based on
+ // the data gathered on the last call to Enumerate()
+ /*!
+ * @brief Returns all AE formats for the CA stream at the given index
+ * @param streamIdx [in] - index into m_caStreamInfos
+ * @return - the list of AE formats in that stream.
+ */
+ AEDataFormatList getFormatListForStream(UInt32 streamIdx) const;
+
+ AEDataTypeList getTypeListForStream(UInt32 streamIdx) const;
+
+ /*!
+ * @brief Returns the AE channelinfo/channel map for the CA stream at the given index
+ * @param streamIdx [in] - index into m_caStreamInfos
+ * @return - the of AE channel info for that stream.
+ */
+ CAEChannelInfo getChannelInfoForStream(UInt32 streamIdx) const;
+
+ /*!
+ * @brief Returns the AE samplerates for the CA stream at the given index
+ * @param streamIdx [in] - index into m_caStreamInfos
+ * @return - the list of AE samplerates for that stream.
+ */
+ AESampleRateList getSampleRateListForStream(UInt32 streamIdx) const;
+
+ /*!
+ * @brief Returns the AE device name for the CA stream at the given index
+ * @param streamIdx [in] - index into m_caStreamInfos
+ * @return - The devicename for that stream
+ */
+ std::string getDeviceNameForStream(UInt32 streamIdx) const;
+
+ /*!
+ * @brief - Returns the extra displayname shown to the User in addition to getDisplayNameForStream
+ * for the CA stream at the given index
+ * @param streamIdx [in] - index into m_caStreamInfos
+ * @return - The extra displayname for that stream (might be empty)
+ */
+ std::string getExtraDisplayNameForStream(UInt32 streamIdx) const;
+
+ AudioDeviceID m_deviceID;
+ bool m_isPlanar;
+ std::string m_deviceName;
+
+ typedef struct
+ {
+ AudioStreamID streamID;
+ StreamFormatList formatList;
+ StreamFormatList formatListVirt;
+ UInt32 numChannels;
+ bool isDigital;
+ bool hasPassthroughFormats;
+ enum AEDeviceType deviceType;
+ } caStreamInfo;
+ std::vector<caStreamInfo> m_caStreamInfos;
+ CCoreAudioDevice m_caDevice;
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioChannelLayout.cpp b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioChannelLayout.cpp
new file mode 100644
index 0000000..8c2446f
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioChannelLayout.cpp
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2011-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 "CoreAudioChannelLayout.h"
+
+#include <AudioToolbox/AudioToolbox.h>
+
+#define MAX_CHANNEL_LABEL 45
+
+const char* g_ChannelLabels[] =
+{
+ "Unused", // kAudioChannelLabel_Unused
+ "Left", // kAudioChannelLabel_Left
+ "Right", // kAudioChannelLabel_Right
+ "Center", // kAudioChannelLabel_Center
+ "LFE", // kAudioChannelLabel_LFEScreen
+ "Side Left", // kAudioChannelLabel_LeftSurround
+ "Side Right", // kAudioChannelLabel_RightSurround
+ "Left Center", // kAudioChannelLabel_LeftCenter
+ "Right Center", // kAudioChannelLabel_RightCenter
+ "Back Center", // kAudioChannelLabel_CenterSurround
+ "Back Left", // kAudioChannelLabel_LeftSurroundDirect
+ "Back Right", // kAudioChannelLabel_RightSurroundDirect
+ "Top Center", // kAudioChannelLabel_TopCenterSurround
+ "Vertical Back Left", // kAudioChannelLabel_VerticalHeightLeft
+ "Vertical Back Center", // kAudioChannelLabel_VerticalHeightCenter
+ "Vertical Back Right", // kAudioChannelLabel_VerticalHeightRight
+ "Top Back Left", // kAudioChannelLabel_VerticalHeightLeft
+ "Top Back Center", // kAudioChannelLabel_VerticalHeightCenter
+ "Top Back Right", // kAudioChannelLabel_VerticalHeightRight
+
+ // gap
+
+ "unused19", "unused20", "unused21", "unused22", "unused23", "unused24", "unused25",
+ "unused26", "unused27", "unused28", "unused29", "unused30", "unused31", "unused32",
+
+ "Rear Left", // kAudioChannelLabel_RearSurroundLeft
+ "Rear Right", // kAudioChannelLabel_RearSurroundRight
+ "Left Wide", // kAudioChannelLabel_LeftWide
+ "Right Wide", // kAudioChannelLabel_RightWide
+ "LFE2", // kAudioChannelLabel_LFE2
+ "Left Total", // kAudioChannelLabel_LeftTotal
+ "Right Total", // kAudioChannelLabel_RightTotal
+ "HearingImpaired", // kAudioChannelLabel_HearingImpaired
+ "Narration", // kAudioChannelLabel_Narration
+ "Mono", // kAudioChannelLabel_Mono
+ "DialogCentricMix", // kAudioChannelLabel_DialogCentricMix
+ "CenterSurroundDirect", // kAudioChannelLabel_CenterSurroundDirect
+ "Haptic", // kAudioChannelLabel_Haptic
+
+};
+
+CCoreAudioChannelLayout::CCoreAudioChannelLayout() :
+ m_pLayout(NULL)
+{
+}
+
+CCoreAudioChannelLayout::CCoreAudioChannelLayout(AudioChannelLayout& layout) :
+m_pLayout(NULL)
+{
+ CopyLayout(layout);
+}
+
+CCoreAudioChannelLayout::~CCoreAudioChannelLayout()
+{
+ free(m_pLayout);
+}
+
+bool CCoreAudioChannelLayout::CopyLayout(AudioChannelLayout& layout)
+{
+ enum {
+ kVariableLengthArray_deprecated = 1
+ };
+
+ free(m_pLayout);
+ m_pLayout = NULL;
+
+ // This method always produces a layout with a ChannelDescriptions structure
+
+ OSStatus ret = 0;
+ UInt32 channels = GetChannelCountForLayout(layout);
+ UInt32 size = sizeof(AudioChannelLayout) + (channels - kVariableLengthArray_deprecated) * sizeof(AudioChannelDescription);
+
+ if (layout.mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelDescriptions)
+ {
+ // We can copy the whole layout
+ m_pLayout = (AudioChannelLayout*)malloc(size);
+ memcpy(m_pLayout, &layout, size);
+ }
+ else if (layout.mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelBitmap)
+ {
+ // Deconstruct the bitmap to get the layout
+ UInt32 propSize = 0;
+ AudioFormatGetPropertyInfo(kAudioFormatProperty_ChannelLayoutForBitmap, sizeof(layout.mChannelBitmap), &layout.mChannelBitmap, &propSize);
+ m_pLayout = (AudioChannelLayout*)malloc(propSize);
+ ret = AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForBitmap, sizeof(layout.mChannelBitmap), &layout.mChannelBitmap, &propSize, m_pLayout);
+ m_pLayout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions;
+ }
+ else
+ {
+ // Convert the known layout to a custom layout
+ UInt32 propSize = 0;
+ AudioFormatGetPropertyInfo(kAudioFormatProperty_ChannelLayoutForTag,
+ sizeof(layout.mChannelLayoutTag), &layout.mChannelLayoutTag, &propSize);
+ m_pLayout = (AudioChannelLayout*)malloc(propSize);
+ ret = AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForTag,
+ sizeof(layout.mChannelLayoutTag), &layout.mChannelLayoutTag, &propSize, m_pLayout);
+ m_pLayout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions;
+ }
+
+ return (ret == noErr);
+}
+
+bool CCoreAudioChannelLayout::CopyLayoutForStereo(UInt32 layout[2])
+{
+ enum {
+ kVariableLengthArray_deprecated = 1
+ };
+
+ free(m_pLayout);
+ m_pLayout = NULL;
+
+ UInt32 channels = 2;
+ UInt32 size = sizeof(AudioChannelLayout) + (channels - kVariableLengthArray_deprecated) * sizeof(AudioChannelDescription);
+
+ m_pLayout = (AudioChannelLayout*)malloc(size);
+ m_pLayout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions;
+ m_pLayout->mNumberChannelDescriptions = 2;//stereo
+
+ AudioChannelDescription desc;
+ desc.mChannelFlags = kAudioChannelFlags_AllOff;
+ memset(desc.mCoordinates, 0, sizeof(desc.mCoordinates));
+
+ desc.mChannelLabel = layout[0];// label for channel 1
+ m_pLayout->mChannelDescriptions[0] = desc;
+
+ desc.mChannelLabel = layout[1];// label for channel 2
+ m_pLayout->mChannelDescriptions[1] = desc;
+ return true;
+}
+
+UInt32 CCoreAudioChannelLayout::GetChannelCountForLayout(AudioChannelLayout& layout)
+{
+ UInt32 channels = 0;
+ if (layout.mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelBitmap)
+ {
+ // Channels are in fixed-order('USB Order'), any combination
+ UInt32 bitmap = layout.mChannelBitmap;
+ for (UInt32 c = 0; c < (sizeof(layout.mChannelBitmap) << 3); c++)
+ {
+ if (bitmap & 0x1)
+ channels++;
+ bitmap >>= 1;
+ }
+ }
+ else if (layout.mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelDescriptions)
+ {
+ // Channels are in any order, any combination
+ channels = layout.mNumberChannelDescriptions;
+ }
+ else
+ {
+ // Channels are in a predefined order and combination
+ channels = AudioChannelLayoutTag_GetNumberOfChannels(layout.mChannelLayoutTag);
+ }
+
+ return channels;
+}
+
+const char* CCoreAudioChannelLayout::ChannelLabelToString(UInt32 label)
+{
+ if (label > MAX_CHANNEL_LABEL)
+ return "Unknown";
+ return g_ChannelLabels[label];
+}
+
+const char* CCoreAudioChannelLayout::ChannelLayoutToString(AudioChannelLayout& layout, std::string& str)
+{
+ AudioChannelLayout* pLayout = NULL;
+
+ if (layout.mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelDescriptions)
+ {
+ pLayout = &layout;
+ }
+ else if (layout.mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelBitmap)
+ {
+ // Deconstruct the bitmap to get the layout
+ UInt32 propSize = 0;
+ AudioFormatGetPropertyInfo(kAudioFormatProperty_ChannelLayoutForBitmap,
+ sizeof(layout.mChannelBitmap), &layout.mChannelBitmap, &propSize);
+ pLayout = (AudioChannelLayout*)calloc(propSize, 1);
+ AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForBitmap,
+ sizeof(layout.mChannelBitmap), &layout.mChannelBitmap, &propSize, pLayout);
+ }
+ else
+ {
+ // Predefined layout 'tag'
+ UInt32 propSize = 0;
+ AudioFormatGetPropertyInfo(kAudioFormatProperty_ChannelLayoutForTag,
+ sizeof(layout.mChannelLayoutTag), &layout.mChannelLayoutTag, &propSize);
+ pLayout = (AudioChannelLayout*)calloc(propSize, 1);
+ AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForTag,
+ sizeof(layout.mChannelLayoutTag), &layout.mChannelLayoutTag, &propSize, pLayout);
+ }
+
+ for (UInt32 c = 0; c < pLayout->mNumberChannelDescriptions; c++)
+ {
+ str += "[";
+ str += ChannelLabelToString(pLayout->mChannelDescriptions[c].mChannelLabel);
+ if (pLayout->mChannelDescriptions[c].mChannelLabel > MAX_CHANNEL_LABEL)
+ {
+ str += "(" + std::to_string(pLayout->mChannelDescriptions[c].mChannelLabel) + ")";
+ }
+ str += "] ";
+ }
+
+ if (layout.mChannelLayoutTag != kAudioChannelLayoutTag_UseChannelDescriptions)
+ free(pLayout);
+
+ return str.c_str();
+}
+
+bool CCoreAudioChannelLayout::AllChannelUnknown()
+{
+ AudioChannelLayout* pLayout = NULL;
+
+ if (!m_pLayout)
+ return false;
+
+ if (m_pLayout->mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelDescriptions)
+ {
+ pLayout = m_pLayout;
+ }
+ else if (m_pLayout->mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelBitmap)
+ {
+ // Deconstruct the bitmap to get the layout
+ UInt32 propSize = 0;
+ AudioFormatGetPropertyInfo(kAudioFormatProperty_ChannelLayoutForBitmap,
+ sizeof(m_pLayout->mChannelBitmap), &m_pLayout->mChannelBitmap, &propSize);
+ pLayout = (AudioChannelLayout*)calloc(propSize, 1);
+ AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForBitmap,
+ sizeof(m_pLayout->mChannelBitmap), &m_pLayout->mChannelBitmap, &propSize, pLayout);
+ }
+ else
+ {
+ // Predefined layout 'tag'
+ UInt32 propSize = 0;
+ AudioFormatGetPropertyInfo(kAudioFormatProperty_ChannelLayoutForTag,
+ sizeof(m_pLayout->mChannelLayoutTag), &m_pLayout->mChannelLayoutTag, &propSize);
+ pLayout = (AudioChannelLayout*)calloc(propSize, 1);
+ AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForTag,
+ sizeof(m_pLayout->mChannelLayoutTag), &m_pLayout->mChannelLayoutTag, &propSize, pLayout);
+ }
+
+ for (UInt32 c = 0; c < pLayout->mNumberChannelDescriptions; c++)
+ {
+ if (pLayout->mChannelDescriptions[c].mChannelLabel != kAudioChannelLabel_Unknown)
+ {
+ return false;
+ }
+ }
+
+ if (m_pLayout->mChannelLayoutTag != kAudioChannelLayoutTag_UseChannelDescriptions)
+ free(pLayout);
+
+ return true;
+}
+
diff --git a/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioChannelLayout.h b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioChannelLayout.h
new file mode 100644
index 0000000..a055b25
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioChannelLayout.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2011-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 <list>
+#include <string>
+#include <vector>
+
+#include <CoreAudio/CoreAudio.h>
+
+typedef std::vector<SInt32> CoreAudioChannelList;
+typedef std::list<AudioChannelLayoutTag> AudioChannelLayoutList;
+
+const AudioChannelLayoutTag g_LayoutMap[] =
+{
+ kAudioChannelLayoutTag_Stereo, // PCM_LAYOUT_2_0 = 0,
+ kAudioChannelLayoutTag_Stereo, // PCM_LAYOUT_2_0 = 0,
+ kAudioChannelLayoutTag_DVD_4, // PCM_LAYOUT_2_1,
+ kAudioChannelLayoutTag_MPEG_3_0_A, // PCM_LAYOUT_3_0,
+ kAudioChannelLayoutTag_DVD_10, // PCM_LAYOUT_3_1,
+ kAudioChannelLayoutTag_DVD_3, // PCM_LAYOUT_4_0,
+ kAudioChannelLayoutTag_DVD_6, // PCM_LAYOUT_4_1,
+ kAudioChannelLayoutTag_MPEG_5_0_A, // PCM_LAYOUT_5_0,
+ kAudioChannelLayoutTag_MPEG_5_1_A, // PCM_LAYOUT_5_1,
+ kAudioChannelLayoutTag_AudioUnit_7_0, // PCM_LAYOUT_7_0, ** This layout may be incorrect...no content to testß˚ **
+ kAudioChannelLayoutTag_MPEG_7_1_A, // PCM_LAYOUT_7_1
+};
+
+const AudioChannelLabel g_LabelMap[] =
+{
+ kAudioChannelLabel_Unused, // PCM_FRONT_LEFT,
+ kAudioChannelLabel_Left, // PCM_FRONT_LEFT,
+ kAudioChannelLabel_Right, // PCM_FRONT_RIGHT,
+ kAudioChannelLabel_Center, // PCM_FRONT_CENTER,
+ kAudioChannelLabel_LFEScreen, // PCM_LOW_FREQUENCY,
+ kAudioChannelLabel_LeftSurroundDirect, // PCM_BACK_LEFT, *** This is incorrect, but has been changed to match VideoPlayer
+ kAudioChannelLabel_RightSurroundDirect, // PCM_BACK_RIGHT, *** This is incorrect, but has been changed to match VideoPlayer
+ kAudioChannelLabel_LeftCenter, // PCM_FRONT_LEFT_OF_CENTER,
+ kAudioChannelLabel_RightCenter, // PCM_FRONT_RIGHT_OF_CENTER,
+ kAudioChannelLabel_CenterSurround, // PCM_BACK_CENTER,
+ kAudioChannelLabel_LeftSurround, // PCM_SIDE_LEFT, *** This is incorrect, but has been changed to match VideoPlayer
+ kAudioChannelLabel_RightSurround, // PCM_SIDE_RIGHT, *** This is incorrect, but has been changed to match VideoPlayer
+ kAudioChannelLabel_VerticalHeightLeft, // PCM_TOP_FRONT_LEFT,
+ kAudioChannelLabel_VerticalHeightRight, // PCM_TOP_FRONT_RIGHT,
+ kAudioChannelLabel_VerticalHeightCenter, // PCM_TOP_FRONT_CENTER,
+ kAudioChannelLabel_TopCenterSurround, // PCM_TOP_CENTER,
+ kAudioChannelLabel_TopBackLeft, // PCM_TOP_BACK_LEFT,
+ kAudioChannelLabel_TopBackRight, // PCM_TOP_BACK_RIGHT,
+ kAudioChannelLabel_TopBackCenter // PCM_TOP_BACK_CENTER
+};
+
+class CCoreAudioChannelLayout
+{
+public:
+ CCoreAudioChannelLayout();
+ CCoreAudioChannelLayout(AudioChannelLayout &layout);
+ virtual ~CCoreAudioChannelLayout();
+
+ operator AudioChannelLayout*() {return m_pLayout;}
+
+ bool CopyLayout(AudioChannelLayout &layout);
+ bool CopyLayoutForStereo(UInt32 layout[2]);
+ static UInt32 GetChannelCountForLayout(AudioChannelLayout &layout);
+ static const char* ChannelLabelToString(UInt32 label);
+ static const char* ChannelLayoutToString(AudioChannelLayout &layout, std::string &str);
+ bool AllChannelUnknown();
+protected:
+ AudioChannelLayout* m_pLayout;
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioDevice.cpp b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioDevice.cpp
new file mode 100644
index 0000000..69f7580
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioDevice.cpp
@@ -0,0 +1,938 @@
+/*
+ * Copyright (C) 2011-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 "CoreAudioDevice.h"
+
+#include "CoreAudioChannelLayout.h"
+#include "CoreAudioHardware.h"
+#include "cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h"
+#include "utils/log.h"
+
+#include "platform/darwin/DarwinUtils.h"
+
+#include <unistd.h>
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// CCoreAudioDevice
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+CCoreAudioDevice::CCoreAudioDevice(AudioDeviceID deviceId) : m_DeviceId(deviceId)
+{
+}
+
+CCoreAudioDevice::~CCoreAudioDevice()
+{
+ Close();
+}
+
+bool CCoreAudioDevice::Open(AudioDeviceID deviceId)
+{
+ m_DeviceId = deviceId;
+ m_BufferSizeRestore = GetBufferSize();
+ return true;
+}
+
+void CCoreAudioDevice::Close()
+{
+ if (!m_DeviceId)
+ return;
+
+ // Stop the device if it was started
+ Stop();
+
+ // Unregister the IOProc if we have one
+ RemoveIOProc();
+
+ SetHogStatus(false);
+ CCoreAudioHardware::SetAutoHogMode(false);
+
+ if (m_MixerRestore > -1) // We changed the mixer status
+ SetMixingSupport((m_MixerRestore ? true : false));
+ m_MixerRestore = -1;
+
+ if (m_SampleRateRestore != 0.0)
+ SetNominalSampleRate(m_SampleRateRestore);
+
+ if (m_BufferSizeRestore && m_BufferSizeRestore != GetBufferSize())
+ {
+ SetBufferSize(m_BufferSizeRestore);
+ m_BufferSizeRestore = 0;
+ }
+
+ m_IoProc = NULL;
+ m_DeviceId = 0;
+ m_ObjectListenerProc = NULL;
+}
+
+void CCoreAudioDevice::Start()
+{
+ if (!m_DeviceId || m_Started)
+ return;
+
+ OSStatus ret = AudioDeviceStart(m_DeviceId, m_IoProc);
+ if (ret)
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::Start: "
+ "Unable to start device. Error = {}",
+ GetError(ret));
+ else
+ m_Started = true;
+}
+
+void CCoreAudioDevice::Stop()
+{
+ if (!m_DeviceId || !m_Started)
+ return;
+
+ OSStatus ret = AudioDeviceStop(m_DeviceId, m_IoProc);
+ if (ret)
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::Stop: "
+ "Unable to stop device. Error = {}",
+ GetError(ret));
+ m_Started = false;
+}
+
+void CCoreAudioDevice::RemoveObjectListenerProc(AudioObjectPropertyListenerProc callback, void* pClientData)
+{
+ if (!m_DeviceId)
+ return;
+
+ AudioObjectPropertyAddress audioProperty;
+ audioProperty.mSelector = kAudioObjectPropertySelectorWildcard;
+ audioProperty.mScope = kAudioObjectPropertyScopeWildcard;
+ audioProperty.mElement = kAudioObjectPropertyElementWildcard;
+
+ OSStatus ret = AudioObjectRemovePropertyListener(m_DeviceId, &audioProperty, callback, pClientData);
+ if (ret)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::RemoveObjectListenerProc: "
+ "Unable to set ObjectListener callback. Error = {}",
+ GetError(ret));
+ }
+ m_ObjectListenerProc = NULL;
+}
+
+bool CCoreAudioDevice::SetObjectListenerProc(AudioObjectPropertyListenerProc callback, void* pClientData)
+{
+ // Allow only one ObjectListener at a time
+ if (!m_DeviceId || m_ObjectListenerProc)
+ return false;
+
+ AudioObjectPropertyAddress audioProperty;
+ audioProperty.mSelector = kAudioObjectPropertySelectorWildcard;
+ audioProperty.mScope = kAudioObjectPropertyScopeWildcard;
+ audioProperty.mElement = kAudioObjectPropertyElementWildcard;
+
+ OSStatus ret = AudioObjectAddPropertyListener(m_DeviceId, &audioProperty, callback, pClientData);
+
+ if (ret)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::SetObjectListenerProc: "
+ "Unable to remove ObjectListener callback. Error = {}",
+ GetError(ret));
+ return false;
+ }
+
+ m_ObjectListenerProc = callback;
+ return true;
+}
+bool CCoreAudioDevice::AddIOProc(AudioDeviceIOProc ioProc, void* pCallbackData)
+{
+ // Allow only one IOProc at a time
+ if (!m_DeviceId || m_IoProc)
+ return false;
+
+ OSStatus ret = AudioDeviceCreateIOProcID(m_DeviceId, ioProc, pCallbackData, &m_IoProc);
+ if (ret)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::AddIOProc: "
+ "Unable to add IOProc. Error = {}",
+ GetError(ret));
+ m_IoProc = NULL;
+ return false;
+ }
+
+ Start();
+
+ return true;
+}
+
+bool CCoreAudioDevice::RemoveIOProc()
+{
+ if (!m_DeviceId || !m_IoProc)
+ return false;
+
+ Stop();
+
+ OSStatus ret = AudioDeviceDestroyIOProcID(m_DeviceId, m_IoProc);
+ if (ret)
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::RemoveIOProc: "
+ "Unable to remove IOProc. Error = {}",
+ GetError(ret));
+
+ m_IoProc = NULL; // Clear the reference no matter what
+
+ usleep(100000);
+
+ return true;
+}
+
+std::string CCoreAudioDevice::GetName() const
+{
+ if (!m_DeviceId)
+ return "";
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyDeviceNameCFString;
+
+ std::string name;
+ CFStringRef deviceName = NULL;
+ UInt32 propertySize = sizeof(deviceName);
+
+ OSStatus ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &propertySize, &deviceName);
+
+ if (ret != noErr)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::GetName: "
+ "Unable to get device name - id: {:#04x}. Error = {}",
+ (uint)m_DeviceId, GetError(ret));
+ }
+ else
+ {
+ CDarwinUtils::CFStringRefToUTF8String(deviceName, name);
+ CFRelease(deviceName);
+ }
+
+ return name;
+}
+
+bool CCoreAudioDevice::IsDigital() const
+{
+ bool isDigital = false;
+ UInt32 transportType = 0;
+ if (!m_DeviceId)
+ return false;
+
+ transportType = GetTransportType();
+ if (transportType == INT_MAX)
+ return false;
+
+ if (transportType == kIOAudioDeviceTransportTypeFireWire)
+ isDigital = true;
+ if (transportType == kIOAudioDeviceTransportTypeUSB)
+ isDigital = true;
+ if (transportType == kIOAudioDeviceTransportTypeHdmi)
+ isDigital = true;
+ if (transportType == kIOAudioDeviceTransportTypeDisplayPort)
+ isDigital = true;
+ if (transportType == kIOAudioDeviceTransportTypeThunderbolt)
+ isDigital = true;
+ if (transportType == kAudioStreamTerminalTypeDigitalAudioInterface)
+ isDigital = true;
+
+ return isDigital;
+}
+
+UInt32 CCoreAudioDevice::GetTransportType() const
+{
+ UInt32 transportType = 0;
+ if (!m_DeviceId)
+ return INT_MAX;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyTransportType;
+
+ UInt32 propertySize = sizeof(transportType);
+ OSStatus ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &propertySize, &transportType);
+ if (ret != noErr)
+ return INT_MAX;
+ return transportType;
+}
+
+UInt32 CCoreAudioDevice::GetTotalOutputChannels() const
+{
+ UInt32 channels = 0;
+
+ if (!m_DeviceId)
+ return channels;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyStreamConfiguration;
+
+ UInt32 size = 0;
+ OSStatus ret = AudioObjectGetPropertyDataSize(m_DeviceId, &propertyAddress, 0, NULL, &size);
+ if (ret != noErr)
+ return channels;
+
+ AudioBufferList* pList = (AudioBufferList*)malloc(size);
+ ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &size, pList);
+ if (ret == noErr)
+ {
+ for(UInt32 buffer = 0; buffer < pList->mNumberBuffers; ++buffer)
+ channels += pList->mBuffers[buffer].mNumberChannels;
+ }
+ else
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::GetTotalOutputChannels: "
+ "Unable to get total device output channels - id: {:#04x}. Error = {}",
+ (uint)m_DeviceId, GetError(ret));
+ }
+
+ free(pList);
+
+ return channels;
+}
+
+UInt32 CCoreAudioDevice::GetNumChannelsOfStream(UInt32 streamIdx) const
+{
+ UInt32 channels = 0;
+
+ if (!m_DeviceId)
+ return channels;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyStreamConfiguration;
+
+ UInt32 size = 0;
+ OSStatus ret = AudioObjectGetPropertyDataSize(m_DeviceId, &propertyAddress, 0, NULL, &size);
+ if (ret != noErr)
+ return channels;
+
+ AudioBufferList* pList = (AudioBufferList*)malloc(size);
+ ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &size, pList);
+ if (ret == noErr)
+ {
+ if (streamIdx < pList->mNumberBuffers)
+ channels = pList->mBuffers[streamIdx].mNumberChannels;
+ }
+ else
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::GetNumChannelsOfStream: "
+ "Unable to get number of stream output channels - id: {:#04x}. Error = {}",
+ (uint)m_DeviceId, GetError(ret));
+ }
+
+ free(pList);
+
+ return channels;
+}
+
+bool CCoreAudioDevice::GetStreams(AudioStreamIdList* pList)
+{
+ if (!pList || !m_DeviceId)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyStreams;
+
+ UInt32 propertySize = 0;
+ OSStatus ret = AudioObjectGetPropertyDataSize(m_DeviceId, &propertyAddress, 0, NULL, &propertySize);
+ if (ret != noErr)
+ return false;
+
+ UInt32 streamCount = propertySize / sizeof(AudioStreamID);
+ AudioStreamID* pStreamList = new AudioStreamID[streamCount];
+ ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &propertySize, pStreamList);
+ if (ret == noErr)
+ {
+ for (UInt32 stream = 0; stream < streamCount; stream++)
+ pList->push_back(pStreamList[stream]);
+ }
+ delete[] pStreamList;
+
+ return ret == noErr;
+}
+
+
+bool CCoreAudioDevice::IsRunning()
+{
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyDeviceIsRunning;
+
+ UInt32 isRunning = 0;
+ UInt32 propertySize = sizeof(isRunning);
+ OSStatus ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &propertySize, &isRunning);
+ if (ret != noErr)
+ return false;
+
+ return isRunning != 0;
+}
+
+bool CCoreAudioDevice::SetHogStatus(bool hog)
+{
+ // According to Jeff Moore (Core Audio, Apple), Setting kAudioDevicePropertyHogMode
+ // is a toggle and the only way to tell if you do get hog mode is to compare
+ // the returned pid against getpid, if the match, you have hog mode, if not you don't.
+ if (!m_DeviceId)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyHogMode;
+
+ if (hog)
+ {
+ // Not already set
+ if (m_HogPid == -1)
+ {
+ OSStatus ret = AudioObjectSetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, sizeof(m_HogPid), &m_HogPid);
+
+ // even if setting hogmode was successful our PID might not get written
+ // into m_HogPid (so it stays -1). Readback hogstatus for judging if we
+ // had success on getting hog status
+ // We do this only when AudioObjectSetPropertyData didn't set m_HogPid because
+ // it seems that in the other cases the GetHogStatus could return -1
+ // which would overwrite our valid m_HogPid again
+ // Man we should never touch this shit again ;)
+ if (m_HogPid == -1)
+ m_HogPid = GetHogStatus();
+
+ if (ret || m_HogPid != getpid())
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::SetHogStatus: "
+ "Unable to set 'hog' status. Error = {}",
+ GetError(ret));
+ return false;
+ }
+ }
+ }
+ else
+ {
+ // Currently Set
+ if (m_HogPid > -1)
+ {
+ pid_t hogPid = -1;
+ OSStatus ret = AudioObjectSetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, sizeof(hogPid), &hogPid);
+ if (ret || hogPid == getpid())
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::SetHogStatus: "
+ "Unable to release 'hog' status. Error = {}",
+ GetError(ret));
+ return false;
+ }
+ // Reset internal state
+ m_HogPid = hogPid;
+ }
+ }
+ return true;
+}
+
+pid_t CCoreAudioDevice::GetHogStatus()
+{
+ if (!m_DeviceId)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyHogMode;
+
+ pid_t hogPid = -1;
+ UInt32 size = sizeof(hogPid);
+ AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &size, &hogPid);
+
+ return hogPid;
+}
+
+bool CCoreAudioDevice::SetMixingSupport(UInt32 mix)
+{
+ if (!m_DeviceId)
+ return false;
+
+ if (!GetMixingSupport())
+ return false;
+
+ int restore = -1;
+ if (m_MixerRestore == -1)
+ {
+ // This is our first change to this setting. Store the original setting for restore
+ restore = (GetMixingSupport() ? 1 : 0);
+ }
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertySupportsMixing;
+
+ UInt32 mixEnable = mix ? 1 : 0;
+ OSStatus ret = AudioObjectSetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, sizeof(mixEnable), &mixEnable);
+ if (ret != noErr)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::SetMixingSupport: "
+ "Unable to set MixingSupport to {}. Error = {}",
+ mix ? "'On'" : "'Off'", GetError(ret));
+ return false;
+ }
+ if (m_MixerRestore == -1)
+ m_MixerRestore = restore;
+ return true;
+}
+
+bool CCoreAudioDevice::GetMixingSupport()
+{
+ if (!m_DeviceId)
+ return false;
+
+ UInt32 size;
+ UInt32 mix = 0;
+ Boolean writable = false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertySupportsMixing;
+
+ if( AudioObjectHasProperty( m_DeviceId, &propertyAddress ) )
+ {
+ OSStatus ret = AudioObjectIsPropertySettable(m_DeviceId, &propertyAddress, &writable);
+ if (ret)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::SupportsMixing: "
+ "Unable to get propertyinfo mixing support. Error = {}",
+ GetError(ret));
+ writable = false;
+ }
+
+ if (writable)
+ {
+ size = sizeof(mix);
+ ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &size, &mix);
+ if (ret != noErr)
+ mix = 0;
+ }
+ }
+ CLog::Log(LOGDEBUG,
+ "CCoreAudioDevice::SupportsMixing: "
+ "Device mixing support : {}.",
+ mix ? "'Yes'" : "'No'");
+
+ return (mix > 0);
+}
+
+bool CCoreAudioDevice::SetCurrentVolume(Float32 vol)
+{
+ if (!m_DeviceId)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kHALOutputParam_Volume;
+
+ OSStatus ret = AudioObjectSetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, sizeof(Float32), &vol);
+ if (ret != noErr)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::SetCurrentVolume: "
+ "Unable to set AudioUnit volume. Error = {}",
+ GetError(ret));
+ return false;
+ }
+ return true;
+}
+
+bool CCoreAudioDevice::GetPreferredChannelLayout(CCoreAudioChannelLayout& layout) const
+{
+ if (!m_DeviceId)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyPreferredChannelLayout;
+
+ UInt32 propertySize = 0;
+ OSStatus ret = AudioObjectGetPropertyDataSize(m_DeviceId, &propertyAddress, 0, NULL, &propertySize);
+ if (ret)
+ return false;
+
+ void* pBuf = malloc(propertySize);
+ ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &propertySize, pBuf);
+ if (ret != noErr)
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::GetPreferredChannelLayout: "
+ "Unable to retrieve preferred channel layout. Error = {}",
+ GetError(ret));
+ else
+ {
+ // Copy the result into the caller's instance
+ layout.CopyLayout(*((AudioChannelLayout*)pBuf));
+ }
+ free(pBuf);
+ return (ret == noErr);
+}
+
+bool CCoreAudioDevice::GetPreferredChannelLayoutForStereo(CCoreAudioChannelLayout &layout) const
+{
+ if (!m_DeviceId)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyPreferredChannelsForStereo;
+
+ UInt32 channels[2];// this will receive the channel labels
+ UInt32 propertySize = sizeof(channels);
+
+ OSStatus ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &propertySize, &channels);
+ if (ret != noErr)
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::GetPreferredChannelLayoutForStereo: "
+ "Unable to retrieve preferred channel layout. Error = {}",
+ GetError(ret));
+ else
+ {
+ // Copy/generate a layout into the result into the caller's instance
+ layout.CopyLayoutForStereo(channels);
+ }
+ return (ret == noErr);
+}
+
+std::string CCoreAudioDevice::GetCurrentDataSourceName() const
+{
+ UInt32 dataSourceId = 0;
+ std::string dataSourceName = "";
+ if(GetDataSource(dataSourceId))
+ {
+ dataSourceName = GetDataSourceName(dataSourceId);
+ }
+ return dataSourceName;
+}
+
+std::string CCoreAudioDevice::GetDataSourceName(UInt32 dataSourceId) const
+{
+ UInt32 propertySize = 0;
+ CFStringRef dataSourceNameCF;
+ std::string dataSourceName;
+ std::string ret = "";
+
+ if (!m_DeviceId)
+ return ret;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyDataSourceNameForIDCFString;
+
+ AudioValueTranslation translation;
+ translation.mInputData = &dataSourceId;
+ translation.mInputDataSize = sizeof(UInt32);
+ translation.mOutputData = &dataSourceNameCF;
+ translation.mOutputDataSize = sizeof ( CFStringRef );
+ propertySize = sizeof(AudioValueTranslation);
+ OSStatus status = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &propertySize, &translation);
+
+ if (( status == noErr ) && dataSourceNameCF )
+ {
+ if (CDarwinUtils::CFStringRefToUTF8String(dataSourceNameCF, dataSourceName))
+ {
+ ret = dataSourceName;
+ }
+ CFRelease ( dataSourceNameCF );
+ }
+
+ return ret;
+}
+
+bool CCoreAudioDevice::GetDataSource(UInt32 &dataSourceId) const
+{
+ bool ret = false;
+
+ if (!m_DeviceId)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyDataSource;
+
+ UInt32 size = sizeof(dataSourceId);
+ OSStatus status = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &size, &dataSourceId);
+ if(status == noErr)
+ ret = true;
+
+ return ret;
+}
+
+bool CCoreAudioDevice::SetDataSource(UInt32 &dataSourceId)
+{
+ bool ret = false;
+
+ if (!m_DeviceId)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyDataSource;
+
+ UInt32 size = sizeof(dataSourceId);
+ OSStatus status = AudioObjectSetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, size, &dataSourceId);
+ if(status == noErr)
+ ret = true;
+
+ return ret;
+}
+
+bool CCoreAudioDevice::GetDataSources(CoreAudioDataSourceList* pList) const
+{
+ if (!pList || !m_DeviceId)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyDataSources;
+
+ UInt32 propertySize = 0;
+ OSStatus ret = AudioObjectGetPropertyDataSize(m_DeviceId, &propertyAddress, 0, NULL, &propertySize);
+ if (ret != noErr)
+ return false;
+
+ UInt32 sources = propertySize / sizeof(UInt32);
+ UInt32* pSources = new UInt32[sources];
+ ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &propertySize, pSources);
+ if (ret == noErr)
+ {
+ for (UInt32 i = 0; i < sources; i++)
+ pList->push_back(pSources[i]);
+ }
+ delete[] pSources;
+ return (!ret);
+}
+
+Float64 CCoreAudioDevice::GetNominalSampleRate()
+{
+ if (!m_DeviceId)
+ return 0.0;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyNominalSampleRate;
+
+ Float64 sampleRate = 0.0;
+ UInt32 propertySize = sizeof(Float64);
+ OSStatus ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &propertySize, &sampleRate);
+ if (ret != noErr)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::GetNominalSampleRate: "
+ "Unable to retrieve current device sample rate. Error = {}",
+ GetError(ret));
+
+ return 0.0;
+ }
+ return sampleRate;
+}
+
+bool CCoreAudioDevice::SetNominalSampleRate(Float64 sampleRate)
+{
+ if (!m_DeviceId || sampleRate == 0.0)
+ return false;
+
+ Float64 currentRate = GetNominalSampleRate();
+ if (currentRate == sampleRate)
+ return true; //No need to change
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyNominalSampleRate;
+
+ OSStatus ret = AudioObjectSetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, sizeof(Float64), &sampleRate);
+ if (ret != noErr)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::SetNominalSampleRate: "
+ "Unable to set current device sample rate to {:0.0f}. Error = {}",
+ (float)sampleRate, GetError(ret));
+ return false;
+ }
+ if (m_SampleRateRestore == 0.0)
+ m_SampleRateRestore = currentRate;
+
+ return true;
+}
+
+UInt32 CCoreAudioDevice::GetNumLatencyFrames()
+{
+ UInt32 num_latency_frames = 0;
+ if (!m_DeviceId)
+ return 0;
+
+ // number of frames of latency in the AudioDevice
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyLatency;
+
+ UInt32 i_param = 0;
+ UInt32 i_param_size = sizeof(uint32_t);
+ OSStatus ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &i_param_size, &i_param);
+ if (ret == noErr)
+ num_latency_frames += i_param;
+
+ // number of frames in the IO buffers
+ propertyAddress.mSelector = kAudioDevicePropertyBufferFrameSize;
+ ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &i_param_size, &i_param);
+ if (ret == noErr)
+ num_latency_frames += i_param;
+
+ // number for frames in ahead the current hardware position that is safe to do IO
+ propertyAddress.mSelector = kAudioDevicePropertySafetyOffset;
+ ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &i_param_size, &i_param);
+ if (ret == noErr)
+ num_latency_frames += i_param;
+
+ return (num_latency_frames);
+}
+
+UInt32 CCoreAudioDevice::GetBufferSize()
+{
+ if (!m_DeviceId)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyBufferFrameSize;
+
+ UInt32 size = 0;
+ UInt32 propertySize = sizeof(size);
+ OSStatus ret = AudioObjectGetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, &propertySize, &size);
+ if (ret != noErr)
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::GetBufferSize: "
+ "Unable to retrieve buffer size. Error = {}",
+ GetError(ret));
+ return size;
+}
+
+bool CCoreAudioDevice::SetBufferSize(UInt32 size)
+{
+ if (!m_DeviceId)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioDevicePropertyScopeOutput;
+ propertyAddress.mElement = 0;
+ propertyAddress.mSelector = kAudioDevicePropertyBufferFrameSize;
+
+ UInt32 propertySize = sizeof(size);
+ OSStatus ret = AudioObjectSetPropertyData(m_DeviceId, &propertyAddress, 0, NULL, propertySize, &size);
+ if (ret != noErr)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioDevice::SetBufferSize: "
+ "Unable to set buffer size. Error = {}",
+ GetError(ret));
+ }
+
+ if (GetBufferSize() != size)
+ CLog::Log(LOGERROR, "CCoreAudioDevice::SetBufferSize: Buffer size change not applied.");
+
+ return (ret == noErr);
+}
+
+XbmcThreads::EndTime<> CCoreAudioDevice::m_callbackSuppressTimer;
+AudioObjectPropertyListenerProc CCoreAudioDevice::m_defaultOutputDeviceChangedCB = NULL;
+
+
+OSStatus CCoreAudioDevice::defaultOutputDeviceChanged(AudioObjectID inObjectID,
+ UInt32 inNumberAddresses,
+ const AudioObjectPropertyAddress inAddresses[],
+ void* inClientData)
+{
+ if (m_callbackSuppressTimer.IsTimePast() && m_defaultOutputDeviceChangedCB != NULL)
+ return m_defaultOutputDeviceChangedCB(inObjectID, inNumberAddresses, inAddresses, inClientData);
+ return 0;
+}
+
+void CCoreAudioDevice::RegisterDeviceChangedCB(bool bRegister, AudioObjectPropertyListenerProc callback, void *ref)
+{
+ OSStatus ret = noErr;
+ AudioObjectPropertyAddress inAdr =
+ {
+ kAudioHardwarePropertyDevices,
+ kAudioObjectPropertyScopeGlobal,
+ kAudioObjectPropertyElementMaster
+ };
+
+ if (bRegister)
+ ret = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &inAdr, callback, ref);
+ else
+ ret = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &inAdr, callback, ref);
+
+ if (ret != noErr)
+ CLog::Log(LOGERROR,
+ "CCoreAudioAE::Deinitialize - error {} a listener callback for device changes!",
+ bRegister ? "attaching" : "removing");
+}
+
+void CCoreAudioDevice::RegisterDefaultOutputDeviceChangedCB(bool bRegister, AudioObjectPropertyListenerProc callback, void *ref)
+{
+ OSStatus ret = noErr;
+ static int registered = -1;
+
+ //only allow registration once
+ if (bRegister == (registered == 1))
+ return;
+
+ AudioObjectPropertyAddress inAdr =
+ {
+ kAudioHardwarePropertyDefaultOutputDevice,
+ kAudioObjectPropertyScopeGlobal,
+ kAudioObjectPropertyElementMaster
+ };
+
+ if (bRegister)
+ {
+ ret = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &inAdr, defaultOutputDeviceChanged, ref);
+ m_defaultOutputDeviceChangedCB = callback;
+ }
+ else
+ {
+ ret = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &inAdr, defaultOutputDeviceChanged, ref);
+ m_defaultOutputDeviceChangedCB = NULL;
+ }
+
+ if (ret != noErr)
+ CLog::Log(LOGERROR,
+ "CCoreAudioAE::Deinitialize - error {} a listener callback for default output "
+ "device changes!",
+ bRegister ? "attaching" : "removing");
+ else
+ registered = bRegister ? 1 : 0;
+}
+
diff --git a/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioDevice.h b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioDevice.h
new file mode 100644
index 0000000..99d84f6
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioDevice.h
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2011-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/Sinks/osx/CoreAudioStream.h"
+#include "threads/SystemClock.h"
+
+#include <list>
+#include <string>
+#include <vector>
+
+#include <CoreAudio/CoreAudio.h>
+
+typedef std::vector<UInt32> CoreAudioDataSourceList;
+typedef std::list<AudioDeviceID> CoreAudioDeviceList;
+
+class CCoreAudioChannelLayout;
+
+class CCoreAudioDevice
+{
+public:
+ CCoreAudioDevice() = default;
+ explicit CCoreAudioDevice(AudioDeviceID deviceId);
+ virtual ~CCoreAudioDevice();
+
+ bool Open(AudioDeviceID deviceId);
+ void Close();
+
+ void Start();
+ void Stop();
+ void RemoveObjectListenerProc(AudioObjectPropertyListenerProc callback, void *pClientData);
+ bool SetObjectListenerProc(AudioObjectPropertyListenerProc callback, void *pClientData);
+
+ AudioDeviceID GetId() const {return m_DeviceId;}
+ std::string GetName() const;
+ bool IsDigital() const;
+ UInt32 GetTransportType() const;
+ UInt32 GetTotalOutputChannels() const;
+ UInt32 GetNumChannelsOfStream(UInt32 streamIdx) const;
+ bool GetStreams(AudioStreamIdList *pList);
+ bool IsRunning();
+ bool SetHogStatus(bool hog);
+ pid_t GetHogStatus();
+ bool SetMixingSupport(UInt32 mix);
+ bool GetMixingSupport();
+ bool SetCurrentVolume(Float32 vol);
+ bool GetPreferredChannelLayout(CCoreAudioChannelLayout &layout) const;
+ bool GetPreferredChannelLayoutForStereo(CCoreAudioChannelLayout &layout) const;
+ bool GetDataSources(CoreAudioDataSourceList *pList) const;
+ bool GetDataSource(UInt32 &dataSourceId) const;
+ bool SetDataSource(UInt32 &dataSourceId);
+ std::string GetDataSourceName(UInt32 dataSourceId) const;
+ std::string GetCurrentDataSourceName() const;
+ Float64 GetNominalSampleRate();
+ bool SetNominalSampleRate(Float64 sampleRate);
+ UInt32 GetNumLatencyFrames();
+ UInt32 GetBufferSize();
+ bool SetBufferSize(UInt32 size);
+
+ static void RegisterDeviceChangedCB(bool bRegister, AudioObjectPropertyListenerProc callback, void *ref);
+ static void RegisterDefaultOutputDeviceChangedCB(bool bRegister, AudioObjectPropertyListenerProc callback, void *ref);
+ // suppresses the default output device changed callback for given time in ms
+ static void SuppressDefaultOutputDeviceCB(unsigned int suppressTimeMs)
+ {
+ m_callbackSuppressTimer.Set(std::chrono::milliseconds(suppressTimeMs));
+ }
+
+ bool AddIOProc(AudioDeviceIOProc ioProc, void* pCallbackData);
+ bool RemoveIOProc();
+protected:
+ bool m_Started = false;
+ AudioDeviceID m_DeviceId = 0;
+ int m_MixerRestore = -1;
+ AudioDeviceIOProc m_IoProc = nullptr;
+ AudioObjectPropertyListenerProc m_ObjectListenerProc = nullptr;
+
+ Float64 m_SampleRateRestore = 0.0;
+ pid_t m_HogPid = -1;
+ unsigned int m_frameSize = 0;
+ unsigned int m_OutputBufferIndex = 0;
+ unsigned int m_BufferSizeRestore = 0;
+
+ static XbmcThreads::EndTime<> m_callbackSuppressTimer;
+ static AudioObjectPropertyListenerProc m_defaultOutputDeviceChangedCB;
+ static OSStatus defaultOutputDeviceChanged(AudioObjectID inObjectID,
+ UInt32 inNumberAddresses,
+ const AudioObjectPropertyAddress inAddresses[],
+ void* inClientData);
+
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioHardware.cpp b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioHardware.cpp
new file mode 100644
index 0000000..a59c8f3
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioHardware.cpp
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2011-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 "CoreAudioHardware.h"
+
+#include "cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h"
+#include "utils/log.h"
+
+#include "platform/darwin/DarwinUtils.h"
+
+bool CCoreAudioHardware::GetAutoHogMode()
+{
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioHardwarePropertyHogModeIsAllowed;
+
+ UInt32 val = 0;
+ UInt32 size = sizeof(val);
+ OSStatus ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &size, &val);
+ if (ret != noErr)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioHardware::GetAutoHogMode: "
+ "Unable to get auto 'hog' mode. Error = {}",
+ GetError(ret));
+ return false;
+ }
+ return (val == 1);
+}
+
+void CCoreAudioHardware::SetAutoHogMode(bool enable)
+{
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioHardwarePropertyHogModeIsAllowed;
+
+ UInt32 val = enable ? 1 : 0;
+ UInt32 size = sizeof(val);
+ OSStatus ret = AudioObjectSetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, size, &val);
+ if (ret != noErr)
+ CLog::Log(LOGERROR,
+ "CCoreAudioHardware::SetAutoHogMode: "
+ "Unable to set auto 'hog' mode. Error = {}",
+ GetError(ret));
+}
+
+void CCoreAudioHardware::ResetAudioDevices()
+{
+ CLog::Log(LOGDEBUG, "CCoreAudioHardware::ResetAudioDevices resetting our devices to LPCM");
+ CoreAudioDeviceList list;
+ if (GetOutputDevices(&list))
+ {
+ for (CoreAudioDeviceList::iterator it = list.begin(); it != list.end(); ++it)
+ {
+ CCoreAudioDevice device(*it);
+
+ AudioStreamIdList streams;
+ if (device.GetStreams(&streams))
+ {
+ CLog::Log(LOGDEBUG, "CCoreAudioHardware::ResetAudioDevices {} streams for device {}",
+ streams.size(), device.GetName());
+ for (AudioStreamIdList::iterator ait = streams.begin(); ait != streams.end(); ++ait)
+ ResetStream(*ait);
+ }
+ }
+ }
+}
+
+void CCoreAudioHardware::ResetStream(AudioStreamID streamId)
+{
+ CCoreAudioStream stream;
+ stream.Open(streamId);
+
+ AudioStreamBasicDescription desc;
+ if (stream.GetPhysicalFormat(&desc))
+ {
+ if (desc.mFormatID == 'IAC3' || desc.mFormatID == kAudioFormat60958AC3)
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "CCoreAudioHardware::ResetStream stream 0x{:x} is in encoded format.. setting to LPCM",
+ (unsigned int)streamId);
+
+ StreamFormatList availableFormats;
+ if (stream.GetAvailablePhysicalFormats(&availableFormats))
+ {
+ for (StreamFormatList::iterator fmtIt = availableFormats.begin(); fmtIt != availableFormats.end() ; ++fmtIt)
+ {
+ AudioStreamRangedDescription fmtDesc = *fmtIt;
+ if (fmtDesc.mFormat.mFormatID == kAudioFormatLinearPCM)
+ {
+ AudioStreamBasicDescription newFmt = fmtDesc.mFormat;
+
+ if (stream.SetPhysicalFormat(&newFmt))
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ stream.Close(false);
+}
+
+AudioDeviceID CCoreAudioHardware::FindAudioDevice(const std::string &searchName)
+{
+ AudioDeviceID deviceId = 0;
+
+ if (!searchName.length())
+ return deviceId;
+
+ std::string searchNameLowerCase = searchName;
+ std::transform(searchNameLowerCase.begin(), searchNameLowerCase.end(), searchNameLowerCase.begin(), ::tolower );
+ if (searchNameLowerCase.compare("default") == 0)
+ {
+ AudioDeviceID defaultDevice = GetDefaultOutputDevice();
+ CLog::Log(LOGDEBUG,
+ "CCoreAudioHardware::FindAudioDevice: "
+ "Returning default device [{:#04x}].",
+ (uint)defaultDevice);
+ return defaultDevice;
+ }
+ CLog::Log(LOGDEBUG,
+ "CCoreAudioHardware::FindAudioDevice: "
+ "Searching for device - {}.",
+ searchName);
+
+ // Obtain a list of all available audio devices
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioHardwarePropertyDevices;
+
+ UInt32 size = 0;
+ OSStatus ret = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &size);
+ if (ret != noErr)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioHardware::FindAudioDevice: "
+ "Unable to retrieve the size of the list of available devices. Error = {}",
+ GetError(ret));
+ return 0;
+ }
+
+ size_t deviceCount = size / sizeof(AudioDeviceID);
+ AudioDeviceID* pDevices = new AudioDeviceID[deviceCount];
+ ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &size, pDevices);
+ if (ret != noErr)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioHardware::FindAudioDevice: "
+ "Unable to retrieve the list of available devices. Error = {}",
+ GetError(ret));
+ delete[] pDevices;
+ return 0;
+ }
+
+ // Attempt to locate the requested device
+ std::string deviceName;
+ for (size_t dev = 0; dev < deviceCount; dev++)
+ {
+ CCoreAudioDevice device;
+ device.Open((pDevices[dev]));
+ deviceName = device.GetName();
+ std::transform( deviceName.begin(), deviceName.end(), deviceName.begin(), ::tolower );
+ if (searchNameLowerCase.compare(deviceName) == 0)
+ deviceId = pDevices[dev];
+ if (deviceId)
+ break;
+ }
+ delete[] pDevices;
+
+ return deviceId;
+}
+
+AudioDeviceID CCoreAudioHardware::GetDefaultOutputDevice()
+{
+ AudioDeviceID deviceId = 0;
+ static AudioDeviceID lastDeviceId = 0;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
+
+ UInt32 size = sizeof(AudioDeviceID);
+ OSStatus ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &size, &deviceId);
+
+ // outputDevice is set to 0 if there is no audio device available
+ // or if the default device is set to an encoded format
+ if (ret != noErr || !deviceId)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioHardware::GetDefaultOutputDevice:"
+ " Unable to identify default output device. Error = {}",
+ GetError(ret));
+ // if there was no error and no deviceId was returned
+ // return the last known default device
+ if (ret == noErr && !deviceId)
+ return lastDeviceId;
+ else
+ return 0;
+ }
+
+ lastDeviceId = deviceId;
+
+ return deviceId;
+}
+
+void CCoreAudioHardware::GetOutputDeviceName(std::string& name)
+{
+ name = "Default";
+ AudioDeviceID deviceId = GetDefaultOutputDevice();
+
+ if (deviceId)
+ {
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioObjectPropertyName;
+
+ CFStringRef theDeviceName = NULL;
+ UInt32 propertySize = sizeof(CFStringRef);
+ OSStatus ret = AudioObjectGetPropertyData(deviceId, &propertyAddress, 0, NULL, &propertySize, &theDeviceName);
+ if (ret != noErr)
+ return;
+
+ CDarwinUtils::CFStringRefToUTF8String(theDeviceName, name);
+
+ CFRelease(theDeviceName);
+ }
+}
+
+UInt32 CCoreAudioHardware::GetOutputDevices(CoreAudioDeviceList *pList)
+{
+ UInt32 found = 0;
+ if (!pList)
+ return found;
+
+ // Obtain a list of all available audio devices
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioHardwarePropertyDevices;
+
+ UInt32 size = 0;
+ OSStatus ret = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &size);
+ if (ret != noErr)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioHardware::GetOutputDevices:"
+ " Unable to retrieve the size of the list of available devices. Error = {}",
+ GetError(ret));
+ return found;
+ }
+
+ size_t deviceCount = size / sizeof(AudioDeviceID);
+ AudioDeviceID* pDevices = new AudioDeviceID[deviceCount];
+ ret = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &size, pDevices);
+ if (ret != noErr)
+ CLog::Log(LOGERROR,
+ "CCoreAudioHardware::GetOutputDevices:"
+ " Unable to retrieve the list of available devices. Error = {}",
+ GetError(ret));
+ else
+ {
+ for (size_t dev = 0; dev < deviceCount; dev++)
+ {
+ CCoreAudioDevice device(pDevices[dev]);
+ if (device.GetTotalOutputChannels() == 0)
+ continue;
+ found++;
+ pList->push_back(pDevices[dev]);
+ }
+ }
+ delete[] pDevices;
+
+ return found;
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioHardware.h b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioHardware.h
new file mode 100644
index 0000000..9c6cd45
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioHardware.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2011-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/Sinks/osx/CoreAudioDevice.h"
+
+// There is only one AudioSystemObject instance system-side.
+// Therefore, all CCoreAudioHardware methods are static
+class CCoreAudioHardware
+{
+public:
+ static bool GetAutoHogMode();
+ static void SetAutoHogMode(bool enable);
+ static AudioStreamBasicDescription* FormatsList(AudioStreamID stream);
+ static AudioStreamID* StreamsList(AudioDeviceID device);
+ static void ResetAudioDevices();
+ static void ResetStream(AudioStreamID streamId);
+ static AudioDeviceID FindAudioDevice(const std::string &deviceName);
+ static AudioDeviceID GetDefaultOutputDevice();
+ static void GetOutputDeviceName(std::string &name);
+ static UInt32 GetOutputDevices(CoreAudioDeviceList *pList);
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioStream.cpp b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioStream.cpp
new file mode 100644
index 0000000..7e94e3c
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioStream.cpp
@@ -0,0 +1,498 @@
+/*
+ * Copyright (C) 2011-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 "CoreAudioStream.h"
+
+#include "CoreAudioDevice.h"
+#include "cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h"
+#include "utils/log.h"
+
+using namespace std::chrono_literals;
+
+CCoreAudioStream::CCoreAudioStream()
+{
+ m_OriginalVirtualFormat.mFormatID = 0;
+ m_OriginalPhysicalFormat.mFormatID = 0;
+}
+
+CCoreAudioStream::~CCoreAudioStream()
+{
+ Close();
+}
+
+bool CCoreAudioStream::Open(AudioStreamID streamId)
+{
+ m_StreamId = streamId;
+ CLog::Log(LOGDEBUG, "CCoreAudioStream::Open: Opened stream {:#04x}.", (uint)m_StreamId);
+
+ // watch for physical property changes.
+ AudioObjectPropertyAddress propertyAOPA;
+ propertyAOPA.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAOPA.mElement = kAudioObjectPropertyElementMaster;
+ propertyAOPA.mSelector = kAudioStreamPropertyPhysicalFormat;
+ if (AudioObjectAddPropertyListener(m_StreamId, &propertyAOPA, HardwareStreamListener, this) != noErr)
+ CLog::Log(LOGERROR, "CCoreAudioStream::Open: couldn't set up a physical property listener.");
+
+ // watch for virtual property changes.
+ propertyAOPA.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAOPA.mElement = kAudioObjectPropertyElementMaster;
+ propertyAOPA.mSelector = kAudioStreamPropertyVirtualFormat;
+ if (AudioObjectAddPropertyListener(m_StreamId, &propertyAOPA, HardwareStreamListener, this) != noErr)
+ CLog::Log(LOGERROR, "CCoreAudioStream::Open: couldn't set up a virtual property listener.");
+
+ return true;
+}
+
+//! @todo Should it even be possible to change both the
+//! physical and virtual formats, since the devices do it themselves?
+void CCoreAudioStream::Close(bool restore)
+{
+ if (!m_StreamId)
+ return;
+
+ std::string formatString;
+
+ // remove the physical/virtual property listeners before we make changes
+ // that will trigger callbacks that we do not care about.
+ AudioObjectPropertyAddress propertyAOPA;
+ propertyAOPA.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAOPA.mElement = kAudioObjectPropertyElementMaster;
+ propertyAOPA.mSelector = kAudioStreamPropertyPhysicalFormat;
+ if (AudioObjectRemovePropertyListener(m_StreamId, &propertyAOPA, HardwareStreamListener, this) != noErr)
+ CLog::Log(LOGDEBUG, "CCoreAudioStream::Close: Couldn't remove property listener.");
+
+ propertyAOPA.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAOPA.mElement = kAudioObjectPropertyElementMaster;
+ propertyAOPA.mSelector = kAudioStreamPropertyVirtualFormat;
+ if (AudioObjectRemovePropertyListener(m_StreamId, &propertyAOPA, HardwareStreamListener, this) != noErr)
+ CLog::Log(LOGDEBUG, "CCoreAudioStream::Close: Couldn't remove property listener.");
+
+ // Revert any format changes we made
+ if (restore && m_OriginalVirtualFormat.mFormatID && m_StreamId)
+ {
+ CLog::Log(LOGDEBUG,
+ "CCoreAudioStream::Close: "
+ "Restoring original virtual format for stream {:#04x}. ({})",
+ (uint)m_StreamId, StreamDescriptionToString(m_OriginalVirtualFormat, formatString));
+ AudioStreamBasicDescription setFormat = m_OriginalVirtualFormat;
+ SetVirtualFormat(&setFormat);
+ }
+ if (restore && m_OriginalPhysicalFormat.mFormatID && m_StreamId)
+ {
+ CLog::Log(LOGDEBUG,
+ "CCoreAudioStream::Close: "
+ "Restoring original physical format for stream {:#04x}. ({})",
+ (uint)m_StreamId, StreamDescriptionToString(m_OriginalPhysicalFormat, formatString));
+ AudioStreamBasicDescription setFormat = m_OriginalPhysicalFormat;
+ SetPhysicalFormat(&setFormat);
+ }
+
+ m_OriginalVirtualFormat.mFormatID = 0;
+ m_OriginalPhysicalFormat.mFormatID = 0;
+ CLog::Log(LOGDEBUG, "CCoreAudioStream::Close: Closed stream {:#04x}.", (uint)m_StreamId);
+ m_StreamId = 0;
+}
+
+UInt32 CCoreAudioStream::GetDirection()
+{
+ if (!m_StreamId)
+ return 0;
+
+ UInt32 val = 0;
+ UInt32 size = sizeof(UInt32);
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioStreamPropertyDirection;
+
+ OSStatus ret = AudioObjectGetPropertyData(m_StreamId, &propertyAddress, 0, NULL, &size, &val);
+ if (ret)
+ return 0;
+
+ return val;
+}
+
+// WARNING - don't rely on this method - the return value of
+// GetTerminalType is driver specific - the checked return
+// values are only recommendations from apple
+bool CCoreAudioStream::IsDigitalOutput(AudioStreamID id)
+{
+ UInt32 type = GetTerminalType(id);
+ // yes apple is mixing types here...
+ return (type == kAudioStreamTerminalTypeDigitalAudioInterface ||
+ type == kIOAudioDeviceTransportTypeDisplayPort ||
+ type == kIOAudioDeviceTransportTypeHdmi ||
+ type == kIOAudioDeviceTransportTypeFireWire ||
+ type == kIOAudioDeviceTransportTypeThunderbolt ||
+ type == kIOAudioDeviceTransportTypeUSB);
+}
+
+bool CCoreAudioStream::GetStartingChannelInDevice(AudioStreamID id, UInt32 &startingChannel)
+{
+ if (!id)
+ return false;
+
+ UInt32 i_param_size = sizeof(UInt32);
+ UInt32 i_param;
+ startingChannel = 0;
+ bool ret = false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioStreamPropertyStartingChannel;
+
+ // number of frames of latency in the AudioStream
+ OSStatus status = AudioObjectGetPropertyData(id, &propertyAddress, 0, NULL, &i_param_size, &i_param);
+ if (status == noErr)
+ {
+ startingChannel = i_param;
+ ret = true;
+ }
+
+ return ret;
+}
+
+UInt32 CCoreAudioStream::GetTerminalType(AudioStreamID id)
+{
+ if (!id)
+ return 0;
+
+ UInt32 val = 0;
+ UInt32 size = sizeof(UInt32);
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioStreamPropertyTerminalType;
+
+ OSStatus ret = AudioObjectGetPropertyData(id, &propertyAddress, 0, NULL, &size, &val);
+ if (ret)
+ return 0;
+ return val;
+}
+
+UInt32 CCoreAudioStream::GetNumLatencyFrames()
+{
+ if (!m_StreamId)
+ return 0;
+
+ UInt32 i_param_size = sizeof(uint32_t);
+ UInt32 i_param, num_latency_frames = 0;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioStreamPropertyLatency;
+
+ // number of frames of latency in the AudioStream
+ OSStatus ret = AudioObjectGetPropertyData(m_StreamId, &propertyAddress, 0, NULL, &i_param_size, &i_param);
+ if (ret == noErr)
+ {
+ num_latency_frames += i_param;
+ }
+
+ return num_latency_frames;
+}
+
+bool CCoreAudioStream::GetVirtualFormat(AudioStreamBasicDescription* pDesc)
+{
+ if (!pDesc || !m_StreamId)
+ return false;
+
+ UInt32 size = sizeof(AudioStreamBasicDescription);
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioStreamPropertyVirtualFormat;
+ OSStatus ret = AudioObjectGetPropertyDataSize(m_StreamId, &propertyAddress, 0, NULL, &size);
+ if (ret)
+ return false;
+
+ ret = AudioObjectGetPropertyData(m_StreamId, &propertyAddress, 0, NULL, &size, pDesc);
+ if (ret)
+ return false;
+ return true;
+}
+
+bool CCoreAudioStream::SetVirtualFormat(AudioStreamBasicDescription* pDesc)
+{
+ if (!pDesc || !m_StreamId)
+ return false;
+
+ std::string formatString;
+
+ // suppress callbacks for the default output device change
+ // for the next 2 seconds because setting format
+ // might trigger a change (when setting/unsetting an encoded
+ // passthrough format)
+ CCoreAudioDevice::SuppressDefaultOutputDeviceCB(2000);
+
+
+ if (!m_OriginalVirtualFormat.mFormatID)
+ {
+ // Store the original format (as we found it) so that it can be restored later
+ if (!GetVirtualFormat(&m_OriginalVirtualFormat))
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioStream::SetVirtualFormat: "
+ "Unable to retrieve current virtual format for stream {:#04x}.",
+ (uint)m_StreamId);
+ return false;
+ }
+ }
+ m_virtual_format_event.Reset();
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioStreamPropertyVirtualFormat;
+
+ UInt32 propertySize = sizeof(AudioStreamBasicDescription);
+ OSStatus ret = AudioObjectSetPropertyData(m_StreamId, &propertyAddress, 0, NULL, propertySize, pDesc);
+ if (ret)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioStream::SetVirtualFormat: "
+ "Unable to set virtual format for stream {:#04x}. Error = {}",
+ (uint)m_StreamId, GetError(ret));
+ return false;
+ }
+
+ // The AudioStreamSetProperty is not only asynchronous,
+ // it is also not Atomic, in its behaviour.
+ // Therefore we check 5 times before we really give up.
+ // FIXME: failing isn't actually implemented yet.
+ for (int i = 0; i < 10; ++i)
+ {
+ AudioStreamBasicDescription checkVirtualFormat;
+ if (!GetVirtualFormat(&checkVirtualFormat))
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioStream::SetVirtualFormat: "
+ "Unable to retrieve current physical format for stream {:#04x}.",
+ (uint)m_StreamId);
+ return false;
+ }
+ if (checkVirtualFormat.mSampleRate == pDesc->mSampleRate &&
+ checkVirtualFormat.mFormatID == pDesc->mFormatID &&
+ checkVirtualFormat.mFramesPerPacket == pDesc->mFramesPerPacket)
+ {
+ // The right format is now active.
+ CLog::Log(LOGDEBUG,
+ "CCoreAudioStream::SetVirtualFormat: "
+ "Virtual format for stream {:#04x}. now active ({})",
+ (uint)m_StreamId, StreamDescriptionToString(checkVirtualFormat, formatString));
+ break;
+ }
+ m_virtual_format_event.Wait(100ms);
+ }
+ return true;
+}
+
+bool CCoreAudioStream::GetPhysicalFormat(AudioStreamBasicDescription* pDesc)
+{
+ if (!pDesc || !m_StreamId)
+ return false;
+
+ UInt32 size = sizeof(AudioStreamBasicDescription);
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioStreamPropertyPhysicalFormat;
+
+ OSStatus ret = AudioObjectGetPropertyData(m_StreamId, &propertyAddress, 0, NULL, &size, pDesc);
+ if (ret)
+ return false;
+ return true;
+}
+
+bool CCoreAudioStream::SetPhysicalFormat(AudioStreamBasicDescription* pDesc)
+{
+ if (!pDesc || !m_StreamId)
+ return false;
+
+ std::string formatString;
+
+ // suppress callbacks for the default output device change
+ // for the next 2 seconds because setting format
+ // might trigger a change (when setting/unsetting an encoded
+ // passthrough format)
+ CCoreAudioDevice::SuppressDefaultOutputDeviceCB(2000);
+
+ if (!m_OriginalPhysicalFormat.mFormatID)
+ {
+ // Store the original format (as we found it) so that it can be restored later
+ if (!GetPhysicalFormat(&m_OriginalPhysicalFormat))
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioStream::SetPhysicalFormat: "
+ "Unable to retrieve current physical format for stream {:#04x}.",
+ (uint)m_StreamId);
+ return false;
+ }
+ }
+ m_physical_format_event.Reset();
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioStreamPropertyPhysicalFormat;
+
+ UInt32 propertySize = sizeof(AudioStreamBasicDescription);
+ OSStatus ret = AudioObjectSetPropertyData(m_StreamId, &propertyAddress, 0, NULL, propertySize, pDesc);
+ if (ret)
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioStream::SetPhysicalFormat: "
+ "Unable to set physical format for stream {:#04x}. Error = {}",
+ (uint)m_StreamId, GetError(ret));
+ return false;
+ }
+
+ // The AudioStreamSetProperty is not only asynchronous,
+ // it is also not Atomic, in its behaviour.
+ // Therefore we check 5 times before we really give up.
+ // FIXME: failing isn't actually implemented yet.
+ for(int i = 0; i < 10; ++i)
+ {
+ AudioStreamBasicDescription checkPhysicalFormat;
+ if (!GetPhysicalFormat(&checkPhysicalFormat))
+ {
+ CLog::Log(LOGERROR,
+ "CCoreAudioStream::SetPhysicalFormat: "
+ "Unable to retrieve current physical format for stream {:#04x}.",
+ (uint)m_StreamId);
+ return false;
+ }
+ if (checkPhysicalFormat.mSampleRate == pDesc->mSampleRate &&
+ checkPhysicalFormat.mFormatID == pDesc->mFormatID &&
+ checkPhysicalFormat.mFramesPerPacket == pDesc->mFramesPerPacket &&
+ checkPhysicalFormat.mChannelsPerFrame == pDesc->mChannelsPerFrame)
+ {
+ // The right format is now active.
+ CLog::Log(LOGDEBUG,
+ "CCoreAudioStream::SetPhysicalFormat: "
+ "Physical format for stream {:#04x}. now active ({})",
+ (uint)m_StreamId, StreamDescriptionToString(checkPhysicalFormat, formatString));
+ break;
+ }
+ m_physical_format_event.Wait(100ms);
+ }
+
+ return true;
+}
+
+bool CCoreAudioStream::GetAvailableVirtualFormats(StreamFormatList* pList)
+{
+ return GetAvailableVirtualFormats(m_StreamId, pList);
+}
+
+bool CCoreAudioStream::GetAvailableVirtualFormats(AudioStreamID id, StreamFormatList* pList)
+{
+ if (!pList || !id)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioStreamPropertyAvailableVirtualFormats;
+
+ UInt32 propertySize = 0;
+ OSStatus ret = AudioObjectGetPropertyDataSize(id, &propertyAddress, 0, NULL, &propertySize);
+ if (ret)
+ return false;
+
+ UInt32 formatCount = propertySize / sizeof(AudioStreamRangedDescription);
+ AudioStreamRangedDescription *pFormatList = new AudioStreamRangedDescription[formatCount];
+ ret = AudioObjectGetPropertyData(id, &propertyAddress, 0, NULL, &propertySize, pFormatList);
+ if (!ret)
+ {
+ for (UInt32 format = 0; format < formatCount; format++)
+ pList->push_back(pFormatList[format]);
+ }
+ delete[] pFormatList;
+ return (ret == noErr);
+}
+
+bool CCoreAudioStream::GetAvailablePhysicalFormats(StreamFormatList* pList)
+{
+ return GetAvailablePhysicalFormats(m_StreamId, pList);
+}
+
+bool CCoreAudioStream::GetAvailablePhysicalFormats(AudioStreamID id, StreamFormatList* pList)
+{
+ if (!pList || !id)
+ return false;
+
+ AudioObjectPropertyAddress propertyAddress;
+ propertyAddress.mScope = kAudioObjectPropertyScopeGlobal;
+ propertyAddress.mElement = kAudioObjectPropertyElementMaster;
+ propertyAddress.mSelector = kAudioStreamPropertyAvailablePhysicalFormats;
+
+ UInt32 propertySize = 0;
+ OSStatus ret = AudioObjectGetPropertyDataSize(id, &propertyAddress, 0, NULL, &propertySize);
+ if (ret)
+ return false;
+
+ UInt32 formatCount = propertySize / sizeof(AudioStreamRangedDescription);
+ AudioStreamRangedDescription *pFormatList = new AudioStreamRangedDescription[formatCount];
+ ret = AudioObjectGetPropertyData(id, &propertyAddress, 0, NULL, &propertySize, pFormatList);
+ if (!ret)
+ {
+ for (UInt32 format = 0; format < formatCount; format++)
+ pList->push_back(pFormatList[format]);
+ }
+ delete[] pFormatList;
+ return (ret == noErr);
+}
+
+OSStatus CCoreAudioStream::HardwareStreamListener(AudioObjectID inObjectID,
+ UInt32 inNumberAddresses, const AudioObjectPropertyAddress inAddresses[], void *inClientData)
+{
+ CCoreAudioStream *ca_stream = (CCoreAudioStream*)inClientData;
+
+ for (UInt32 i = 0; i < inNumberAddresses; i++)
+ {
+ if (inAddresses[i].mSelector == kAudioStreamPropertyPhysicalFormat)
+ {
+ AudioStreamBasicDescription actualFormat;
+ UInt32 propertySize = sizeof(AudioStreamBasicDescription);
+ // hardware physical format has changed.
+ if (AudioObjectGetPropertyData(ca_stream->m_StreamId, &inAddresses[i], 0, NULL, &propertySize, &actualFormat) == noErr)
+ {
+ std::string formatString;
+ CLog::Log(LOGINFO,
+ "CCoreAudioStream::HardwareStreamListener: "
+ "Hardware physical format changed to {}",
+ StreamDescriptionToString(actualFormat, formatString));
+ ca_stream->m_physical_format_event.Set();
+ }
+ }
+ else if (inAddresses[i].mSelector == kAudioStreamPropertyVirtualFormat)
+ {
+ // hardware virtual format has changed.
+ AudioStreamBasicDescription actualFormat;
+ UInt32 propertySize = sizeof(AudioStreamBasicDescription);
+ if (AudioObjectGetPropertyData(ca_stream->m_StreamId, &inAddresses[i], 0, NULL, &propertySize, &actualFormat) == noErr)
+ {
+ std::string formatString;
+ CLog::Log(LOGINFO,
+ "CCoreAudioStream::HardwareStreamListener: "
+ "Hardware virtual format changed to {}",
+ StreamDescriptionToString(actualFormat, formatString));
+ ca_stream->m_virtual_format_event.Set();
+ }
+ }
+ }
+
+ return noErr;
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioStream.h b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioStream.h
new file mode 100644
index 0000000..a0c6778
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/osx/CoreAudioStream.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2011-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/Event.h"
+
+#include <list>
+#include <vector>
+
+#include <CoreAudio/CoreAudio.h>
+#include <IOKit/audio/IOAudioTypes.h>
+
+
+typedef std::vector<AudioStreamID> AudioStreamIdList;
+typedef std::vector<AudioStreamRangedDescription> StreamFormatList;
+
+class CCoreAudioStream
+{
+public:
+ CCoreAudioStream();
+ virtual ~CCoreAudioStream();
+
+ bool Open(AudioStreamID streamId);
+ void Close(bool restore = true);
+
+ AudioStreamID GetId() const {return m_StreamId;}
+ UInt32 GetDirection();
+ static UInt32 GetTerminalType(AudioStreamID id);
+ UInt32 GetNumLatencyFrames();
+ bool GetVirtualFormat(AudioStreamBasicDescription *pDesc);
+ bool GetPhysicalFormat(AudioStreamBasicDescription *pDesc);
+ bool SetVirtualFormat(AudioStreamBasicDescription *pDesc);
+ bool SetPhysicalFormat(AudioStreamBasicDescription *pDesc);
+ bool GetAvailableVirtualFormats(StreamFormatList *pList);
+ bool GetAvailablePhysicalFormats(StreamFormatList *pList);
+ static bool GetAvailableVirtualFormats(AudioStreamID id, StreamFormatList *pList);
+ static bool GetAvailablePhysicalFormats(AudioStreamID id, StreamFormatList *pList);
+ static bool IsDigitalOutput(AudioStreamID id);
+ static bool GetStartingChannelInDevice(AudioStreamID id, UInt32 &startingChannel);
+
+protected:
+ static OSStatus HardwareStreamListener(AudioObjectID inObjectID,
+ UInt32 inNumberAddresses, const AudioObjectPropertyAddress inAddresses[], void* inClientData);
+
+ CEvent m_virtual_format_event;
+ CEvent m_physical_format_event;
+
+ AudioStreamID m_StreamId = 0;
+ AudioStreamBasicDescription m_OriginalVirtualFormat;
+ AudioStreamBasicDescription m_OriginalPhysicalFormat;
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/AESinkPipewire.cpp b/xbmc/cores/AudioEngine/Sinks/pipewire/AESinkPipewire.cpp
new file mode 100644
index 0000000..487b9ca
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/AESinkPipewire.cpp
@@ -0,0 +1,492 @@
+/*
+ * Copyright (C) 2010-2021 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 "AESinkPipewire.h"
+
+#include "CompileInfo.h"
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Sinks/pipewire/Pipewire.h"
+#include "cores/AudioEngine/Sinks/pipewire/PipewireCore.h"
+#include "cores/AudioEngine/Sinks/pipewire/PipewireNode.h"
+#include "cores/AudioEngine/Sinks/pipewire/PipewireStream.h"
+#include "cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.h"
+#include "utils/Map.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <pipewire/keys.h>
+#include <spa/param/audio/raw.h>
+
+using namespace std::chrono_literals;
+
+namespace
+{
+
+// clang-format off
+constexpr std::array<uint32_t, 14> defaultSampleRates = {
+ 5512,
+ 8000,
+ 11025,
+ 16000,
+ 22050,
+ 32000,
+ 44100,
+ 48000,
+ 64000,
+ 88200,
+ 96000,
+ 176400,
+ 192000,
+ 384000};
+// clang-format on
+
+constexpr auto formatMap = make_map<spa_audio_format, AEDataFormat>(
+ {{SPA_AUDIO_FORMAT_U8, AEDataFormat::AE_FMT_U8},
+ {SPA_AUDIO_FORMAT_S16, AEDataFormat::AE_FMT_S16NE},
+ {SPA_AUDIO_FORMAT_S24_32, AEDataFormat::AE_FMT_S24NE4},
+ {SPA_AUDIO_FORMAT_S32, AEDataFormat::AE_FMT_S32NE},
+ {SPA_AUDIO_FORMAT_S24, AEDataFormat::AE_FMT_S24NE3},
+ {SPA_AUDIO_FORMAT_F32, AEDataFormat::AE_FMT_FLOAT}});
+
+constexpr uint8_t PWFormatToSampleSize(spa_audio_format format)
+{
+ switch (format)
+ {
+ case SPA_AUDIO_FORMAT_S8:
+ case SPA_AUDIO_FORMAT_U8:
+ return 1;
+ case SPA_AUDIO_FORMAT_S16:
+ return 2;
+ case SPA_AUDIO_FORMAT_S24:
+ return 3;
+ case SPA_AUDIO_FORMAT_S24_32:
+ case SPA_AUDIO_FORMAT_S32:
+ case SPA_AUDIO_FORMAT_F32:
+ return 4;
+ default:
+ return 0;
+ }
+}
+
+constexpr std::string_view PWFormatToString(spa_audio_format format)
+{
+ switch (format)
+ {
+ case SPA_AUDIO_FORMAT_U8:
+ return "u8";
+ case SPA_AUDIO_FORMAT_S8:
+ return "s8";
+ case SPA_AUDIO_FORMAT_S16:
+ return "s16";
+ case SPA_AUDIO_FORMAT_S24:
+ return "s24";
+ case SPA_AUDIO_FORMAT_S24_32:
+ return "s24_32";
+ case SPA_AUDIO_FORMAT_S32:
+ return "s32";
+ case SPA_AUDIO_FORMAT_F32:
+ return "f32";
+ default:
+ return "(invalid)";
+ }
+}
+
+spa_audio_format AEFormatToPWFormat(AEDataFormat& format)
+{
+ const auto it = std::find_if(formatMap.cbegin(), formatMap.cend(),
+ [&format](auto p) { return p.second == format; });
+ if (it != formatMap.cend())
+ return it->first;
+
+ // default if format not found in map
+ return SPA_AUDIO_FORMAT_F32;
+}
+
+constexpr AEDataFormat PWFormatToAEFormat(spa_audio_format& format)
+{
+ return formatMap.at(format);
+}
+
+// clang-format off
+constexpr auto channelMap = make_map<spa_audio_channel, AEChannel>({
+ {SPA_AUDIO_CHANNEL_FL, AEChannel::AE_CH_FL},
+ {SPA_AUDIO_CHANNEL_FR, AEChannel::AE_CH_FR},
+ {SPA_AUDIO_CHANNEL_FC, AEChannel::AE_CH_FC},
+ {SPA_AUDIO_CHANNEL_LFE, AEChannel::AE_CH_LFE},
+ {SPA_AUDIO_CHANNEL_SL, AEChannel::AE_CH_SL},
+ {SPA_AUDIO_CHANNEL_SR, AEChannel::AE_CH_SR},
+ {SPA_AUDIO_CHANNEL_FLC, AEChannel::AE_CH_FLOC},
+ {SPA_AUDIO_CHANNEL_FRC, AEChannel::AE_CH_FROC},
+ {SPA_AUDIO_CHANNEL_RC, AEChannel::AE_CH_BC},
+ {SPA_AUDIO_CHANNEL_RL, AEChannel::AE_CH_BL},
+ {SPA_AUDIO_CHANNEL_RR, AEChannel::AE_CH_BR},
+ {SPA_AUDIO_CHANNEL_TC, AEChannel::AE_CH_TC},
+ {SPA_AUDIO_CHANNEL_TFL, AEChannel::AE_CH_TFL},
+ {SPA_AUDIO_CHANNEL_TFC, AEChannel::AE_CH_TFC},
+ {SPA_AUDIO_CHANNEL_TFR, AEChannel::AE_CH_TFR},
+ {SPA_AUDIO_CHANNEL_TRL, AEChannel::AE_CH_TBL},
+ {SPA_AUDIO_CHANNEL_TRC, AEChannel::AE_CH_TBC},
+ {SPA_AUDIO_CHANNEL_TRR, AEChannel::AE_CH_TBR},
+ {SPA_AUDIO_CHANNEL_BC, AEChannel::AE_CH_BC},
+ {SPA_AUDIO_CHANNEL_BLC, AEChannel::AE_CH_BLOC},
+ {SPA_AUDIO_CHANNEL_BRC, AEChannel::AE_CH_BROC}});
+// clang-format on
+
+std::vector<spa_audio_channel> AEChannelMapToPWChannelMap(CAEChannelInfo& channelInfo)
+{
+ std::vector<spa_audio_channel> channels;
+ for (uint32_t count = 0; count < channelInfo.Count(); count++)
+ {
+ const auto it =
+ std::find_if(channelMap.cbegin(), channelMap.cend(),
+ [&channelInfo, &count](auto p) { return p.second == channelInfo[count]; });
+
+ if (it != channelMap.cend())
+ channels.emplace_back(it->first);
+ }
+
+ return channels;
+}
+
+CAEChannelInfo PWChannelMapToAEChannelMap(std::vector<spa_audio_channel>& channelInfo)
+{
+ CAEChannelInfo channels;
+ for (const auto& channel : channelInfo)
+ channels += channelMap.at(channel);
+
+ return channels;
+}
+
+} // namespace
+
+namespace AE
+{
+namespace SINK
+{
+
+std::unique_ptr<PIPEWIRE::CPipewire> pipewire;
+
+bool CAESinkPipewire::Register()
+{
+ pipewire = std::make_unique<PIPEWIRE::CPipewire>();
+
+ bool success{false};
+
+ try
+ {
+ success = pipewire->Start();
+ }
+ catch (std::exception& e)
+ {
+ success = false;
+ }
+
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "CAESinkPipewire::{} - failed to connect to server", __FUNCTION__);
+ pipewire.reset();
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CAESinkPipewire::{} - connected to server", __FUNCTION__);
+
+ AE::AESinkRegEntry entry;
+ entry.sinkName = "PIPEWIRE";
+ entry.createFunc = CAESinkPipewire::Create;
+ entry.enumerateFunc = CAESinkPipewire::EnumerateDevicesEx;
+ entry.cleanupFunc = CAESinkPipewire::Destroy;
+ AE::CAESinkFactory::RegisterSink(entry);
+
+ return true;
+}
+
+IAESink* CAESinkPipewire::Create(std::string& device, AEAudioFormat& desiredFormat)
+{
+ IAESink* sink = new CAESinkPipewire();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+
+void CAESinkPipewire::EnumerateDevicesEx(AEDeviceInfoList& list, bool force)
+{
+ auto loop = pipewire->GetThreadLoop();
+ loop->Lock();
+
+ CAEDeviceInfo defaultDevice;
+ defaultDevice.m_deviceType = AE_DEVTYPE_PCM;
+ defaultDevice.m_deviceName = "Default";
+ defaultDevice.m_displayName = "Default";
+ defaultDevice.m_displayNameExtra = "Default Output Device (PIPEWIRE)";
+ defaultDevice.m_wantsIECPassthrough = true;
+
+ std::for_each(formatMap.cbegin(), formatMap.cend(),
+ [&defaultDevice](const auto& pair)
+ { defaultDevice.m_dataFormats.emplace_back(pair.second); });
+
+ std::for_each(defaultSampleRates.cbegin(), defaultSampleRates.cend(),
+ [&defaultDevice](const auto& rate)
+ { defaultDevice.m_sampleRates.emplace_back(rate); });
+
+ defaultDevice.m_channels = CAEChannelInfo(AE_CH_LAYOUT_2_0);
+
+ list.emplace_back(defaultDevice);
+
+ for (const auto& global : pipewire->GetGlobals())
+ {
+ CAEDeviceInfo device;
+ device.m_deviceType = AE_DEVTYPE_PCM;
+ device.m_deviceName = global.second->name;
+ device.m_displayName = global.second->description;
+ device.m_displayNameExtra = global.second->description + " (PIPEWIRE)";
+ device.m_wantsIECPassthrough = true;
+
+ std::for_each(formatMap.cbegin(), formatMap.cend(),
+ [&device](const auto& pair) { device.m_dataFormats.emplace_back(pair.second); });
+
+ std::for_each(defaultSampleRates.cbegin(), defaultSampleRates.cend(),
+ [&device](const auto& rate) { device.m_sampleRates.emplace_back(rate); });
+
+ auto proxy = global.second->proxy.get();
+ auto node = static_cast<PIPEWIRE::CPipewireNode*>(proxy);
+
+ node->EnumerateFormats();
+
+ int ret = loop->Wait(5s);
+ if (ret == -ETIMEDOUT)
+ {
+ CLog::Log(LOGDEBUG,
+ "CAESinkPipewire::{} - timed out out waiting for formats to be enumerated",
+ __FUNCTION__);
+ continue;
+ }
+
+ auto& channels = node->GetChannels();
+ if (channels.size() < 1)
+ continue;
+
+ for (const auto& channel : channels)
+ {
+ const auto ch = channelMap.find(channel);
+ if (ch != channelMap.cend())
+ device.m_channels += ch->second;
+ }
+
+ list.emplace_back(device);
+ }
+
+ loop->Unlock();
+}
+
+void CAESinkPipewire::Destroy()
+{
+ pipewire.reset();
+}
+
+bool CAESinkPipewire::Initialize(AEAudioFormat& format, std::string& device)
+{
+ auto core = pipewire->GetCore();
+ auto loop = pipewire->GetThreadLoop();
+ auto& stream = pipewire->GetStream();
+
+ loop->Lock();
+
+ auto& globals = pipewire->GetGlobals();
+
+ uint32_t id;
+ if (device == "Default")
+ {
+ id = PW_ID_ANY;
+ }
+ else
+ {
+ auto target = std::find_if(globals.begin(), globals.end(),
+ [&device](const auto& p) { return device == p.second->name; });
+ if (target == globals.end())
+ {
+ loop->Unlock();
+ return false;
+ }
+
+ id = target->first;
+ }
+
+ stream = std::make_shared<PIPEWIRE::CPipewireStream>(core->Get());
+
+ stream->AddListener(pipewire.get());
+
+ m_latency = 20; // ms
+ uint32_t frames = std::nearbyint((m_latency * format.m_sampleRate) / 1000.0);
+ std::string fraction = StringUtils::Format("{}/{}", frames, format.m_sampleRate);
+
+ std::array<spa_dict_item, 5> items = {
+ SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_TYPE, "Audio"),
+ SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_CATEGORY, "Playback"),
+ SPA_DICT_ITEM_INIT(PW_KEY_APP_NAME, CCompileInfo::GetAppName()),
+ SPA_DICT_ITEM_INIT(PW_KEY_NODE_NAME, CCompileInfo::GetAppName()),
+ SPA_DICT_ITEM_INIT(PW_KEY_NODE_LATENCY, fraction.c_str())};
+
+ auto properties = SPA_DICT_INIT(items.data(), items.size());
+ stream->UpdateProperties(&properties);
+
+ auto pwFormat = AEFormatToPWFormat(format.m_dataFormat);
+ format.m_dataFormat = PWFormatToAEFormat(pwFormat);
+
+ auto pwChannels = AEChannelMapToPWChannelMap(format.m_channelLayout);
+ format.m_channelLayout = PWChannelMapToAEChannelMap(pwChannels);
+
+ CLog::Log(LOGDEBUG, "CAESinkPipewire::{} - rate: {}", __FUNCTION__, format.m_sampleRate);
+ CLog::Log(LOGDEBUG, "CAESinkPipewire::{} - channels: {}", __FUNCTION__, pwChannels.size());
+ CLog::Log(LOGDEBUG, "CAESinkPipewire::{} - format: {}", __FUNCTION__, PWFormatToString(pwFormat));
+ CLog::Log(LOGDEBUG, "CAESinkPipewire::{} - samplesize: {}", __FUNCTION__,
+ PWFormatToSampleSize(pwFormat));
+ CLog::Log(LOGDEBUG, "CAESinkPipewire::{} - framesize: {}", __FUNCTION__,
+ pwChannels.size() * PWFormatToSampleSize(pwFormat));
+ CLog::Log(LOGDEBUG, "CAESinkPipewire::{} - latency: {}/{} ({:.3f}s)", __FUNCTION__, frames,
+ format.m_sampleRate, static_cast<double>(frames) / format.m_sampleRate);
+
+ spa_audio_info_raw info{};
+ info.format = pwFormat;
+ info.flags = SPA_AUDIO_FLAG_NONE;
+ info.rate = format.m_sampleRate;
+ info.channels = static_cast<uint32_t>(pwChannels.size());
+
+ for (size_t index = 0; index < pwChannels.size(); index++)
+ info.position[index] = pwChannels[index];
+
+ if (!stream->Connect(id, info))
+ {
+ loop->Unlock();
+ return false;
+ }
+
+ pw_stream_state state;
+ do
+ {
+ state = stream->GetState();
+ if (state == PW_STREAM_STATE_PAUSED)
+ break;
+
+ CLog::Log(LOGDEBUG, "CAESinkPipewire::{} - waiting", __FUNCTION__);
+
+ int ret = loop->Wait(5s);
+ if (ret == -ETIMEDOUT)
+ {
+ CLog::Log(LOGDEBUG, "CAESinkPipewire::{} - timed out waiting for stream to be paused",
+ __FUNCTION__);
+ loop->Unlock();
+ return false;
+ }
+ } while (state != PW_STREAM_STATE_PAUSED);
+
+ CLog::Log(LOGDEBUG, "CAESinkPipewire::{} - initialized", __FUNCTION__);
+
+ format.m_frameSize = pwChannels.size() * PWFormatToSampleSize(pwFormat);
+ format.m_frames = frames;
+
+ m_format = format;
+
+ loop->Unlock();
+
+ return true;
+}
+
+void CAESinkPipewire::Deinitialize()
+{
+ auto loop = pipewire->GetThreadLoop();
+ auto& stream = pipewire->GetStream();
+
+ loop->Lock();
+
+ stream->Flush(false);
+
+ loop->Unlock();
+
+ stream.reset();
+}
+
+double CAESinkPipewire::GetCacheTotal()
+{
+ return m_latency / 1000.0;
+}
+
+unsigned int CAESinkPipewire::AddPackets(uint8_t** data, unsigned int frames, unsigned int offset)
+{
+ auto loop = pipewire->GetThreadLoop();
+ auto& stream = pipewire->GetStream();
+
+ loop->Lock();
+
+ if (stream->GetState() == PW_STREAM_STATE_PAUSED)
+ stream->SetActive(true);
+
+ pw_buffer* pwBuffer = nullptr;
+ while (!pwBuffer)
+ {
+ pwBuffer = stream->DequeueBuffer();
+ if (pwBuffer)
+ break;
+
+ int ret = loop->Wait(1s);
+ if (ret == -ETIMEDOUT)
+ {
+ loop->Unlock();
+ return 0;
+ }
+ }
+
+ spa_buffer* spaBuffer = pwBuffer->buffer;
+ spa_data* spaData = &spaBuffer->datas[0];
+
+ size_t length = frames * m_format.m_frameSize;
+
+ if (spaData->maxsize < length)
+ length = spaData->maxsize;
+
+ void* buffer = data[0] + offset * m_format.m_frameSize;
+
+ std::memcpy(spaData->data, buffer, length);
+
+ spaData->chunk->offset = 0;
+ spaData->chunk->stride = m_format.m_frameSize;
+ spaData->chunk->size = length;
+
+ stream->QueueBuffer(pwBuffer);
+
+ loop->Unlock();
+
+ return length / m_format.m_frameSize;
+}
+
+void CAESinkPipewire::GetDelay(AEDelayStatus& status)
+{
+ status.SetDelay(m_latency / 1000.0);
+}
+
+void CAESinkPipewire::Drain()
+{
+ auto loop = pipewire->GetThreadLoop();
+ auto& stream = pipewire->GetStream();
+
+ loop->Lock();
+
+ stream->Flush(true);
+
+ int ret = loop->Wait(1s);
+ if (ret == -ETIMEDOUT)
+ {
+ CLog::Log(LOGDEBUG, "CAESinkPipewire::{} - wait timed out, already drained?", __FUNCTION__);
+ }
+
+ loop->Unlock();
+}
+
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/AESinkPipewire.h b/xbmc/cores/AudioEngine/Sinks/pipewire/AESinkPipewire.h
new file mode 100644
index 0000000..b346e82
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/AESinkPipewire.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010-2021 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"
+
+namespace AE
+{
+namespace SINK
+{
+
+class CAESinkPipewire : public IAESink
+{
+public:
+ CAESinkPipewire() = default;
+ ~CAESinkPipewire() override = default;
+
+ static bool Register();
+ static IAESink* Create(std::string& device, AEAudioFormat& desiredFormat);
+ static void EnumerateDevicesEx(AEDeviceInfoList& list, bool force = false);
+ static void Destroy();
+
+ // overrides via IAESink
+ const char* GetName() override { return "PIPEWIRE"; }
+
+ bool Initialize(AEAudioFormat& format, std::string& device) override;
+ void Deinitialize() override;
+
+ double GetCacheTotal() override;
+ void GetDelay(AEDelayStatus& status) override;
+
+ unsigned int AddPackets(uint8_t** data, unsigned int frames, unsigned int offset) override;
+ void Drain() override;
+
+private:
+ AEAudioFormat m_format;
+ double m_latency;
+};
+
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/Pipewire.cpp b/xbmc/cores/AudioEngine/Sinks/pipewire/Pipewire.cpp
new file mode 100644
index 0000000..5943aed
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/Pipewire.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2010-2021 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 "Pipewire.h"
+
+#include "cores/AudioEngine/Sinks/pipewire/PipewireContext.h"
+#include "cores/AudioEngine/Sinks/pipewire/PipewireCore.h"
+#include "cores/AudioEngine/Sinks/pipewire/PipewireRegistry.h"
+#include "cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.h"
+#include "utils/log.h"
+
+#include <pipewire/pipewire.h>
+
+using namespace std::chrono_literals;
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+CPipewire::CPipewire()
+{
+ pw_init(nullptr, nullptr);
+}
+
+CPipewire::~CPipewire()
+{
+ if (m_loop)
+ {
+ m_loop->Unlock();
+ m_loop->Stop();
+ }
+
+ m_stream.reset();
+ m_registry.reset();
+ m_core.reset();
+ m_context.reset();
+ m_loop.reset();
+
+ pw_deinit();
+}
+
+bool CPipewire::Start()
+{
+ m_loop = std::make_unique<CPipewireThreadLoop>();
+
+ m_context = std::make_unique<CPipewireContext>(m_loop->Get());
+
+ m_loop->Lock();
+
+ if (!m_loop->Start())
+ {
+ CLog::Log(LOGERROR, "Pipewire: failed to start threaded mainloop: {}", strerror(errno));
+ return false;
+ }
+
+ m_core = std::make_unique<CPipewireCore>(m_context->Get());
+ m_core->AddListener(this);
+
+ m_registry = std::make_unique<CPipewireRegistry>(m_core->Get());
+ m_registry->AddListener(this);
+
+ m_core->Sync();
+
+ int ret = m_loop->Wait(5s);
+
+ m_loop->Unlock();
+
+ if (ret == -ETIMEDOUT)
+ {
+ CLog::Log(LOGDEBUG, "CAESinkPipewire::{} - timed out out waiting for synchronization",
+ __FUNCTION__);
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/Pipewire.h b/xbmc/cores/AudioEngine/Sinks/pipewire/Pipewire.h
new file mode 100644
index 0000000..c69655e
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/Pipewire.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2010-2021 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/Sinks/pipewire/PipewireProxy.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include <pipewire/properties.h>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+class CPipewireThreadLoop;
+class CPipewireContext;
+class CPipewireCore;
+class CPipewireRegistry;
+class CPipewireStream;
+
+class CPipewire
+{
+public:
+ CPipewire();
+ ~CPipewire();
+
+ bool Start();
+
+ CPipewireThreadLoop* GetThreadLoop() { return m_loop.get(); }
+ CPipewireContext* GetContext() { return m_context.get(); }
+ CPipewireCore* GetCore() { return m_core.get(); }
+ CPipewireRegistry* GetRegistry() { return m_registry.get(); }
+ std::shared_ptr<CPipewireStream>& GetStream() { return m_stream; }
+
+ struct PipewirePropertiesDeleter
+ {
+ void operator()(pw_properties* p) { pw_properties_free(p); }
+ };
+
+ struct global
+ {
+ std::string name;
+ std::string description;
+ uint32_t id;
+ uint32_t permissions;
+ std::string type;
+ uint32_t version;
+ std::unique_ptr<pw_properties, PipewirePropertiesDeleter> properties;
+ std::unique_ptr<CPipewireProxy> proxy;
+ };
+
+ std::map<uint32_t, std::unique_ptr<global>>& GetGlobals() { return m_globals; }
+
+private:
+ std::map<uint32_t, std::unique_ptr<global>> m_globals;
+
+ std::unique_ptr<CPipewireThreadLoop> m_loop;
+ std::unique_ptr<CPipewireContext> m_context;
+ std::unique_ptr<CPipewireCore> m_core;
+ std::unique_ptr<CPipewireRegistry> m_registry;
+ std::shared_ptr<CPipewireStream> m_stream;
+};
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireContext.cpp b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireContext.cpp
new file mode 100644
index 0000000..7683ede
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireContext.cpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2010-2021 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 "PipewireContext.h"
+
+#include "utils/log.h"
+
+#include <stdexcept>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+CPipewireContext::CPipewireContext(pw_loop* loop)
+{
+ m_context.reset(pw_context_new(loop, nullptr, 0));
+ if (!m_context)
+ {
+ CLog::Log(LOGERROR, "CPipewireContext: failed to create context: {}", strerror(errno));
+ throw std::runtime_error("CPipewireContext: failed to create context");
+ }
+}
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireContext.h b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireContext.h
new file mode 100644
index 0000000..eeb880c
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireContext.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010-2021 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 <memory>
+
+#include <pipewire/context.h>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+class CPipewireContext
+{
+public:
+ explicit CPipewireContext(pw_loop* loop);
+ CPipewireContext() = delete;
+ ~CPipewireContext() = default;
+
+ pw_context* Get() const { return m_context.get(); }
+
+private:
+ struct PipewireContextDeleter
+ {
+ void operator()(pw_context* p) { pw_context_destroy(p); }
+ };
+
+ std::unique_ptr<pw_context, PipewireContextDeleter> m_context;
+};
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireCore.cpp b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireCore.cpp
new file mode 100644
index 0000000..f5230b0
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireCore.cpp
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2010-2021 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 "PipewireCore.h"
+
+#include "cores/AudioEngine/Sinks/pipewire/Pipewire.h"
+#include "cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.h"
+#include "utils/log.h"
+
+#include <stdexcept>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+CPipewireCore::CPipewireCore(pw_context* context) : m_coreEvents(CreateCoreEvents())
+{
+ m_core.reset(pw_context_connect(context, nullptr, 0));
+ if (!m_core)
+ {
+ CLog::Log(LOGERROR, "CPipewireCore: failed to create core: {}", strerror(errno));
+ throw std::runtime_error("CPipewireCore: failed to create core");
+ }
+}
+
+CPipewireCore::~CPipewireCore()
+{
+ spa_hook_remove(&m_coreListener);
+}
+
+void CPipewireCore::AddListener(void* userdata)
+{
+ pw_core_add_listener(m_core.get(), &m_coreListener, &m_coreEvents, userdata);
+}
+
+void CPipewireCore::Sync()
+{
+ m_sync = pw_core_sync(m_core.get(), 0, m_sync);
+}
+
+void CPipewireCore::OnCoreDone(void* userdata, uint32_t id, int seq)
+{
+ auto pipewire = reinterpret_cast<CPipewire*>(userdata);
+ auto core = pipewire->GetCore();
+ auto loop = pipewire->GetThreadLoop();
+
+ if (core->GetSync() == seq)
+ loop->Signal(false);
+}
+
+pw_core_events CPipewireCore::CreateCoreEvents()
+{
+ pw_core_events coreEvents = {};
+ coreEvents.version = PW_VERSION_CORE_EVENTS;
+ coreEvents.done = OnCoreDone;
+
+ return coreEvents;
+}
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireCore.h b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireCore.h
new file mode 100644
index 0000000..5f78718
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireCore.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2010-2021 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 <memory>
+
+#include <pipewire/core.h>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+class CPipewireCore
+{
+public:
+ explicit CPipewireCore(pw_context* context);
+ CPipewireCore() = delete;
+ ~CPipewireCore();
+
+ pw_core* Get() const { return m_core.get(); }
+
+ void AddListener(void* userdata);
+ void Sync();
+ int GetSync() const { return m_sync; }
+
+private:
+ static void OnCoreDone(void* userdata, uint32_t id, int seq);
+
+ static pw_core_events CreateCoreEvents();
+
+ const pw_core_events m_coreEvents;
+
+ spa_hook m_coreListener;
+
+ struct PipewireCoreDeleter
+ {
+ void operator()(pw_core* p) { pw_core_disconnect(p); }
+ };
+
+ std::unique_ptr<pw_core, PipewireCoreDeleter> m_core;
+
+ int m_sync;
+};
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireNode.cpp b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireNode.cpp
new file mode 100644
index 0000000..66e3576
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireNode.cpp
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2010-2021 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 "PipewireNode.h"
+
+#include "cores/AudioEngine/Sinks/pipewire/Pipewire.h"
+#include "cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <spa/param/format.h>
+#include <spa/pod/iter.h>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+CPipewireNode::CPipewireNode(pw_registry* registry, uint32_t id, const char* type)
+ : CPipewireProxy(registry, id, type, PW_VERSION_NODE), m_nodeEvents(CreateNodeEvents())
+{
+}
+
+CPipewireNode::~CPipewireNode()
+{
+ spa_hook_remove(&m_objectListener);
+}
+
+void CPipewireNode::AddListener(void* userdata)
+{
+ m_pipewire = reinterpret_cast<CPipewire*>(userdata);
+
+ pw_proxy_add_object_listener(m_proxy.get(), &m_objectListener, &m_nodeEvents, this);
+
+ CPipewireProxy::AddListener(userdata);
+}
+
+void CPipewireNode::EnumerateFormats()
+{
+ if (!m_info)
+ return;
+
+ for (uint32_t param = 0; param < m_info->n_params; param++)
+ {
+ if (m_info->params[param].id == SPA_PARAM_EnumFormat)
+ pw_node_enum_params(m_proxy.get(), 0, m_info->params[param].id, 0, 0, NULL);
+ }
+}
+
+void CPipewireNode::Info(void* userdata, const struct pw_node_info* info)
+{
+ auto node = reinterpret_cast<CPipewireNode*>(userdata);
+
+ if (node->m_info)
+ {
+ CLog::Log(LOGDEBUG, "CPipewireNode::{} - node {} changed", __FUNCTION__, info->id);
+ pw_node_info* m_info = node->m_info.get();
+ m_info = pw_node_info_update(m_info, info);
+ }
+ else
+ {
+ node->m_info.reset(pw_node_info_update(node->m_info.get(), info));
+ }
+}
+
+template<typename T>
+static T Parse(uint32_t type, void* body, uint32_t size)
+{
+ switch (type)
+ {
+ case SPA_TYPE_Id:
+ case SPA_TYPE_Int:
+ return *reinterpret_cast<T*>(body);
+
+ default:
+ throw std::runtime_error(StringUtils::Format("unhandled type: {}", type));
+ }
+}
+
+template<typename T>
+static std::set<T> ParseArray(uint32_t type, void* body, uint32_t size)
+{
+ switch (type)
+ {
+ case SPA_TYPE_Id:
+ case SPA_TYPE_Int:
+ {
+ std::set<T> values;
+ values.emplace(Parse<T>(type, body, size));
+
+ return values;
+ }
+ case SPA_TYPE_Array:
+ {
+ auto array = reinterpret_cast<spa_pod_array_body*>(body);
+ void* p;
+ std::set<T> values;
+ SPA_POD_ARRAY_BODY_FOREACH(array, size, p)
+ values.emplace(Parse<T>(array->child.type, p, array->child.size));
+
+ return values;
+ }
+ case SPA_TYPE_Choice:
+ {
+ auto choice = reinterpret_cast<spa_pod_choice_body*>(body);
+ void* p;
+ std::set<T> values;
+ SPA_POD_CHOICE_BODY_FOREACH(choice, size, p)
+ values.emplace(Parse<T>(choice->child.type, p, choice->child.size));
+
+ return values;
+ }
+ default:
+ throw std::runtime_error(StringUtils::Format("unhandled array: {}", type));
+ }
+}
+
+void CPipewireNode::Parse(uint32_t type, void* body, uint32_t size)
+{
+ switch (type)
+ {
+ case SPA_TYPE_Object:
+ {
+ auto object = reinterpret_cast<spa_pod_object_body*>(body);
+
+ switch (object->type)
+ {
+ case SPA_TYPE_OBJECT_Format:
+ {
+ spa_pod_prop* prop;
+ SPA_POD_OBJECT_BODY_FOREACH(object, size, prop)
+ {
+ spa_format format = static_cast<spa_format>(prop->key);
+
+ switch (format)
+ {
+ case SPA_FORMAT_AUDIO_format:
+ {
+ m_formats = ParseArray<spa_audio_format>(
+ prop->value.type, SPA_POD_CONTENTS(spa_pod_prop, prop), prop->value.size);
+ break;
+ }
+ case SPA_FORMAT_AUDIO_rate:
+ {
+ m_rates = ParseArray<uint32_t>(
+ prop->value.type, SPA_POD_CONTENTS(spa_pod_prop, prop), prop->value.size);
+ break;
+ }
+ case SPA_FORMAT_AUDIO_position:
+ {
+ m_channels = ParseArray<spa_audio_channel>(
+ prop->value.type, SPA_POD_CONTENTS(spa_pod_prop, prop), prop->value.size);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ break;
+ }
+ default:
+ return;
+ }
+
+ break;
+ }
+ default:
+ return;
+ }
+}
+
+void CPipewireNode::Param(void* userdata,
+ int seq,
+ uint32_t id,
+ uint32_t index,
+ uint32_t next,
+ const struct spa_pod* param)
+{
+ auto node = reinterpret_cast<CPipewireNode*>(userdata);
+ auto pipewire = node->GetPipewire();
+ auto loop = pipewire->GetThreadLoop();
+
+ node->Parse(SPA_POD_TYPE(param), SPA_POD_BODY(param), SPA_POD_BODY_SIZE(param));
+
+ loop->Signal(false);
+}
+
+pw_node_events CPipewireNode::CreateNodeEvents()
+{
+ pw_node_events nodeEvents = {};
+ nodeEvents.version = PW_VERSION_NODE_EVENTS;
+ nodeEvents.info = Info;
+ nodeEvents.param = Param;
+
+ return nodeEvents;
+}
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireNode.h b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireNode.h
new file mode 100644
index 0000000..9da85fb
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireNode.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2010-2021 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/Sinks/pipewire/PipewireProxy.h"
+
+#include <memory>
+#include <set>
+
+#include <pipewire/node.h>
+#include <spa/param/audio/raw.h>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+class CPipewire;
+
+class CPipewireNode : public CPipewireProxy
+{
+public:
+ explicit CPipewireNode(pw_registry* registry, uint32_t id, const char* type);
+ CPipewireNode() = delete;
+ ~CPipewireNode() override;
+
+ void AddListener(void* userdata) override;
+
+ void EnumerateFormats();
+
+ pw_node_info* GetInfo() { return m_info.get(); }
+
+ std::set<spa_audio_format>& GetFormats() { return m_formats; }
+ std::set<spa_audio_channel>& GetChannels() { return m_channels; }
+ std::set<uint32_t>& GetRates() { return m_rates; }
+
+ CPipewire* GetPipewire() { return m_pipewire; }
+
+private:
+ void Parse(uint32_t type, void* body, uint32_t size);
+
+ static void Info(void* userdata, const struct pw_node_info* info);
+ static void Param(void* userdata,
+ int seq,
+ uint32_t id,
+ uint32_t index,
+ uint32_t next,
+ const struct spa_pod* param);
+
+ static pw_node_events CreateNodeEvents();
+
+ const pw_node_events m_nodeEvents;
+
+ spa_hook m_objectListener;
+
+ struct PipewireNodeInfoDeleter
+ {
+ void operator()(pw_node_info* p) { pw_node_info_free(p); }
+ };
+
+ std::unique_ptr<pw_node_info, PipewireNodeInfoDeleter> m_info;
+
+ std::set<spa_audio_format> m_formats;
+ std::set<spa_audio_channel> m_channels;
+ std::set<uint32_t> m_rates;
+
+ CPipewire* m_pipewire;
+};
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireProxy.cpp b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireProxy.cpp
new file mode 100644
index 0000000..4d21281
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireProxy.cpp
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010-2021 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 "PipewireProxy.h"
+
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "utils/log.h"
+
+#include <stdexcept>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+CPipewireProxy::CPipewireProxy(pw_registry* registry,
+ uint32_t id,
+ const char* type,
+ uint32_t version)
+ : m_proxyEvents(CreateProxyEvents())
+{
+ m_proxy.reset(reinterpret_cast<pw_proxy*>(pw_registry_bind(registry, id, type, version, 0)));
+ if (!m_proxy)
+ {
+ CLog::Log(LOGERROR, "CPipewireProxy: failed to create proxy: {}", strerror(errno));
+ throw std::runtime_error("CPipewireProxy: failed to create proxy");
+ }
+}
+
+CPipewireProxy::~CPipewireProxy()
+{
+ spa_hook_remove(&m_proxyListener);
+}
+
+void CPipewireProxy::AddListener(void* userdata)
+{
+ pw_proxy_add_listener(m_proxy.get(), &m_proxyListener, &m_proxyEvents, nullptr);
+}
+
+void CPipewireProxy::Bound(void* userdata, uint32_t id)
+{
+ CLog::Log(LOGDEBUG, "CPipewireProxy::{} - id={}", __FUNCTION__, id);
+
+ auto AE = CServiceBroker::GetActiveAE();
+ if (AE)
+ AE->DeviceCountChange("PIPEWIRE");
+}
+
+void CPipewireProxy::Removed(void* userdata)
+{
+ CLog::Log(LOGDEBUG, "CPipewireProxy::{}", __FUNCTION__);
+
+ auto AE = CServiceBroker::GetActiveAE();
+ if (AE)
+ AE->DeviceCountChange("PIPEWIRE");
+}
+
+pw_proxy_events CPipewireProxy::CreateProxyEvents()
+{
+ pw_proxy_events proxyEvents = {};
+ proxyEvents.version = PW_VERSION_PROXY_EVENTS;
+ proxyEvents.bound = Bound;
+ proxyEvents.removed = Removed;
+
+ return proxyEvents;
+}
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireProxy.h b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireProxy.h
new file mode 100644
index 0000000..e864c10
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireProxy.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010-2021 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 <memory>
+
+#include <pipewire/core.h>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+class CPipewireProxy
+{
+public:
+ CPipewireProxy() = delete;
+ virtual ~CPipewireProxy();
+
+ virtual void AddListener(void* userdata);
+
+protected:
+ explicit CPipewireProxy(pw_registry* registry, uint32_t id, const char* type, uint32_t version);
+
+ struct PipewireProxyDeleter
+ {
+ void operator()(pw_proxy* p) { pw_proxy_destroy(p); }
+ };
+
+ std::unique_ptr<pw_proxy, PipewireProxyDeleter> m_proxy;
+
+private:
+ static void Bound(void* userdata, uint32_t id);
+ static void Removed(void* userdata);
+
+ static pw_proxy_events CreateProxyEvents();
+
+ const pw_proxy_events m_proxyEvents;
+
+ spa_hook m_proxyListener;
+};
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireRegistry.cpp b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireRegistry.cpp
new file mode 100644
index 0000000..04e912d
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireRegistry.cpp
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2010-2021 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 "PipewireRegistry.h"
+
+#include "cores/AudioEngine/Sinks/pipewire/Pipewire.h"
+#include "cores/AudioEngine/Sinks/pipewire/PipewireNode.h"
+#include "utils/log.h"
+
+#include <stdexcept>
+
+#include <pipewire/keys.h>
+#include <pipewire/node.h>
+#include <pipewire/type.h>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+CPipewireRegistry::CPipewireRegistry(pw_core* core) : m_registryEvents(CreateRegistryEvents())
+{
+ m_registry.reset(pw_core_get_registry(core, PW_VERSION_REGISTRY, 0));
+ if (!m_registry)
+ {
+ CLog::Log(LOGERROR, "CPipewireRegistry: failed to create registry: {}", strerror(errno));
+ throw std::runtime_error("CPipewireRegistry: failed to create registry");
+ }
+}
+
+void CPipewireRegistry::AddListener(void* userdata)
+{
+ pw_registry_add_listener(m_registry.get(), &m_registryListener, &m_registryEvents, userdata);
+}
+
+void CPipewireRegistry::OnGlobalAdded(void* userdata,
+ uint32_t id,
+ uint32_t permissions,
+ const char* type,
+ uint32_t version,
+ const struct spa_dict* props)
+{
+ auto pipewire = reinterpret_cast<CPipewire*>(userdata);
+
+ if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0)
+ {
+ const char* mediaClass = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
+ if (!mediaClass)
+ return;
+
+ if (strcmp(mediaClass, "Audio/Sink") != 0)
+ return;
+
+ const char* name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
+ if (!name)
+ return;
+
+ const char* desc = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
+ if (!desc)
+ return;
+
+ auto& globals = pipewire->GetGlobals();
+
+ globals[id] = std::make_unique<CPipewire::global>();
+ globals[id]->name = std::string(name);
+ globals[id]->description = std::string(desc);
+ globals[id]->id = id;
+ globals[id]->permissions = permissions;
+ globals[id]->type = std::string(type);
+ globals[id]->version = version;
+ globals[id]->properties.reset(pw_properties_new_dict(props));
+ globals[id]->proxy = std::make_unique<CPipewireNode>(pipewire->GetRegistry()->Get(), id, type);
+ globals[id]->proxy->AddListener(userdata);
+ }
+}
+
+void CPipewireRegistry::OnGlobalRemoved(void* userdata, uint32_t id)
+{
+ auto pipewire = reinterpret_cast<CPipewire*>(userdata);
+ auto& globals = pipewire->GetGlobals();
+
+ auto global = globals.find(id);
+ if (global != globals.end())
+ {
+ CLog::Log(LOGDEBUG, "CPipewireRegistry::{} - id={} type={}", __FUNCTION__, id,
+ global->second->type);
+
+ globals.erase(global);
+ }
+}
+
+pw_registry_events CPipewireRegistry::CreateRegistryEvents()
+{
+ pw_registry_events registryEvents = {};
+ registryEvents.version = PW_VERSION_REGISTRY_EVENTS;
+ registryEvents.global = OnGlobalAdded;
+ registryEvents.global_remove = OnGlobalRemoved;
+
+ return registryEvents;
+}
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireRegistry.h b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireRegistry.h
new file mode 100644
index 0000000..6cbc7dd
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireRegistry.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010-2021 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 <memory>
+
+#include <pipewire/core.h>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+class CPipewireRegistry
+{
+public:
+ explicit CPipewireRegistry(pw_core* core);
+ CPipewireRegistry() = delete;
+ ~CPipewireRegistry() = default;
+
+ pw_registry* Get() const { return m_registry.get(); }
+
+ void AddListener(void* userdata);
+
+private:
+ static void OnGlobalAdded(void* userdata,
+ uint32_t id,
+ uint32_t permissions,
+ const char* type,
+ uint32_t version,
+ const struct spa_dict* props);
+ static void OnGlobalRemoved(void* userdata, uint32_t id);
+
+ static pw_registry_events CreateRegistryEvents();
+
+ const pw_registry_events m_registryEvents;
+
+ spa_hook m_registryListener;
+ struct PipewireRegistryDeleter
+ {
+ void operator()(pw_registry* p) { pw_proxy_destroy(reinterpret_cast<pw_proxy*>(p)); }
+ };
+
+ std::unique_ptr<pw_registry, PipewireRegistryDeleter> m_registry;
+};
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireStream.cpp b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireStream.cpp
new file mode 100644
index 0000000..10d298f
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireStream.cpp
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2010-2021 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 "PipewireStream.h"
+
+#include "cores/AudioEngine/Sinks/pipewire/Pipewire.h"
+#include "cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.h"
+#include "utils/log.h"
+
+#include <stdexcept>
+
+#include <spa/param/audio/format-utils.h>
+#include <spa/pod/builder.h>
+#include <spa/utils/result.h>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+CPipewireStream::CPipewireStream(pw_core* core) : m_streamEvents(CreateStreamEvents())
+{
+ m_stream.reset(pw_stream_new(core, nullptr, pw_properties_new(nullptr, nullptr)));
+ if (!m_stream)
+ {
+ CLog::Log(LOGERROR, "CPipewireStream: failed to create stream: {}", strerror(errno));
+ throw std::runtime_error("CPipewireStream: failed to create stream");
+ }
+}
+
+CPipewireStream::~CPipewireStream()
+{
+ spa_hook_remove(&m_streamListener);
+}
+
+void CPipewireStream::AddListener(void* userdata)
+{
+ pw_stream_add_listener(m_stream.get(), &m_streamListener, &m_streamEvents, userdata);
+}
+
+bool CPipewireStream::Connect(uint32_t id, spa_audio_info_raw& info)
+{
+ std::array<uint8_t, 1024> buffer;
+ auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
+ auto params = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &info);
+
+ int ret = pw_stream_connect(m_stream.get(), PW_DIRECTION_OUTPUT, id,
+ static_cast<pw_stream_flags>(PW_STREAM_FLAG_AUTOCONNECT |
+ PW_STREAM_FLAG_INACTIVE |
+ PW_STREAM_FLAG_MAP_BUFFERS),
+ const_cast<const spa_pod**>(&params), 1);
+
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CPipewireStream: failed to connect stream: {}", spa_strerror(errno));
+ return false;
+ }
+
+ return true;
+}
+
+pw_stream_state CPipewireStream::GetState()
+{
+ return pw_stream_get_state(m_stream.get(), nullptr);
+}
+
+void CPipewireStream::SetActive(bool active)
+{
+ pw_stream_set_active(m_stream.get(), active);
+}
+
+pw_buffer* CPipewireStream::DequeueBuffer()
+{
+ return pw_stream_dequeue_buffer(m_stream.get());
+}
+
+void CPipewireStream::QueueBuffer(pw_buffer* buffer)
+{
+ pw_stream_queue_buffer(m_stream.get(), buffer);
+}
+
+void CPipewireStream::Flush(bool drain)
+{
+ pw_stream_flush(m_stream.get(), drain);
+}
+
+uint32_t CPipewireStream::GetNodeId()
+{
+ return pw_stream_get_node_id(m_stream.get());
+}
+
+void CPipewireStream::UpdateProperties(spa_dict* dict)
+{
+ pw_stream_update_properties(m_stream.get(), dict);
+}
+
+
+void CPipewireStream::StateChanged(void* userdata,
+ enum pw_stream_state old,
+ enum pw_stream_state state,
+ const char* error)
+{
+ auto pipewire = reinterpret_cast<CPipewire*>(userdata);
+ auto loop = pipewire->GetThreadLoop();
+ auto stream = pipewire->GetStream();
+
+ CLog::Log(LOGDEBUG, "CPipewireStream::{} - stream state changed {} -> {}", __FUNCTION__,
+ pw_stream_state_as_string(old), pw_stream_state_as_string(state));
+
+ if (state == PW_STREAM_STATE_STREAMING)
+ CLog::Log(LOGDEBUG, "CPipewireStream::{} - stream node {}", __FUNCTION__, stream->GetNodeId());
+
+ if (state == PW_STREAM_STATE_ERROR)
+ CLog::Log(LOGDEBUG, "CPipewireStream::{} - stream node {} error: {}", __FUNCTION__,
+ stream->GetNodeId(), error);
+
+ loop->Signal(false);
+}
+
+void CPipewireStream::Process(void* userdata)
+{
+ auto pipewire = reinterpret_cast<CPipewire*>(userdata);
+ auto loop = pipewire->GetThreadLoop();
+
+ loop->Signal(false);
+}
+
+void CPipewireStream::Drained(void* userdata)
+{
+ auto pipewire = reinterpret_cast<CPipewire*>(userdata);
+ auto loop = pipewire->GetThreadLoop();
+ auto stream = pipewire->GetStream();
+
+ stream->SetActive(false);
+
+ CLog::Log(LOGDEBUG, "CPipewireStream::{}", __FUNCTION__);
+
+ loop->Signal(false);
+}
+
+pw_stream_events CPipewireStream::CreateStreamEvents()
+{
+ pw_stream_events streamEvents = {};
+ streamEvents.version = PW_VERSION_STREAM_EVENTS;
+ streamEvents.state_changed = StateChanged;
+ streamEvents.process = Process;
+ streamEvents.drained = Drained;
+
+ return streamEvents;
+}
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireStream.h b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireStream.h
new file mode 100644
index 0000000..695c576
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireStream.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010-2021 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 <memory>
+
+#include <pipewire/core.h>
+#include <pipewire/stream.h>
+#include <spa/param/audio/raw.h>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+class CPipewireStream
+{
+public:
+ explicit CPipewireStream(pw_core* core);
+ CPipewireStream() = delete;
+ ~CPipewireStream();
+
+ pw_stream* Get() const { return m_stream.get(); }
+
+ void AddListener(void* userdata);
+ bool Connect(uint32_t id, spa_audio_info_raw& info);
+
+ pw_stream_state GetState();
+ void SetActive(bool active);
+
+ pw_buffer* DequeueBuffer();
+ void QueueBuffer(pw_buffer* buffer);
+
+ void Flush(bool drain);
+
+ uint32_t GetNodeId();
+
+ void UpdateProperties(spa_dict* dict);
+
+private:
+ static void StateChanged(void* userdata,
+ enum pw_stream_state old,
+ enum pw_stream_state state,
+ const char* error);
+ static void Process(void* userdata);
+ static void Drained(void* userdata);
+
+ static pw_stream_events CreateStreamEvents();
+
+ const pw_stream_events m_streamEvents;
+
+ spa_hook m_streamListener;
+
+ struct PipewireStreamDeleter
+ {
+ void operator()(pw_stream* p) { pw_stream_destroy(p); }
+ };
+
+ std::unique_ptr<pw_stream, PipewireStreamDeleter> m_stream;
+};
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.cpp b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.cpp
new file mode 100644
index 0000000..8334097
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.cpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2010-2021 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 "PipewireThreadLoop.h"
+
+#include "utils/log.h"
+
+#include <stdexcept>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+CPipewireThreadLoop::CPipewireThreadLoop()
+{
+ m_mainloop.reset(pw_thread_loop_new("pipewire", nullptr));
+ if (!m_mainloop)
+ {
+ CLog::Log(LOGERROR, "CPipewireThreadLoop: failed to create main loop: {}", strerror(errno));
+ throw std::runtime_error("CPipewireThreadLoop: failed to create main loop");
+ }
+}
+
+bool CPipewireThreadLoop::Start()
+{
+ return pw_thread_loop_start(m_mainloop.get()) == 0;
+}
+
+void CPipewireThreadLoop::Stop()
+{
+ pw_thread_loop_stop(m_mainloop.get());
+}
+
+void CPipewireThreadLoop::Lock()
+{
+ pw_thread_loop_lock(m_mainloop.get());
+}
+
+void CPipewireThreadLoop::Unlock()
+{
+ pw_thread_loop_unlock(m_mainloop.get());
+}
+
+int CPipewireThreadLoop::Wait(std::chrono::nanoseconds timeout)
+{
+ timespec abstime;
+ pw_thread_loop_get_time(m_mainloop.get(), &abstime, timeout.count());
+
+ return pw_thread_loop_timed_wait_full(m_mainloop.get(), &abstime);
+}
+
+void CPipewireThreadLoop::Signal(bool accept)
+{
+ pw_thread_loop_signal(m_mainloop.get(), accept);
+}
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.h b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.h
new file mode 100644
index 0000000..32c219b
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/pipewire/PipewireThreadLoop.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2010-2021 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 <chrono>
+#include <memory>
+
+#include <pipewire/thread-loop.h>
+
+namespace AE
+{
+namespace SINK
+{
+namespace PIPEWIRE
+{
+
+class CPipewireThreadLoop
+{
+public:
+ CPipewireThreadLoop();
+ ~CPipewireThreadLoop() = default;
+
+ pw_loop* Get() const { return pw_thread_loop_get_loop(m_mainloop.get()); }
+
+ bool Start();
+ void Stop();
+
+ void Lock();
+ void Unlock();
+
+ int Wait(std::chrono::nanoseconds timeout);
+ void Signal(bool accept);
+
+private:
+ struct PipewireThreadLoopDeleter
+ {
+ void operator()(pw_thread_loop* p) { pw_thread_loop_destroy(p); }
+ };
+
+ std::unique_ptr<pw_thread_loop, PipewireThreadLoopDeleter> m_mainloop;
+};
+
+} // namespace PIPEWIRE
+} // namespace SINK
+} // namespace AE
diff --git a/xbmc/cores/AudioEngine/Sinks/test/CMakeLists.txt b/xbmc/cores/AudioEngine/Sinks/test/CMakeLists.txt
new file mode 100644
index 0000000..48788a2
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/test/CMakeLists.txt
@@ -0,0 +1,7 @@
+if(MACOSX)
+ list(APPEND SOURCES TestAESinkDARWINOSX.cpp)
+endif()
+
+if(SOURCES)
+ core_add_test_library(audioengine_sink_test)
+endif()
diff --git a/xbmc/cores/AudioEngine/Sinks/test/TestAESinkDARWINOSX.cpp b/xbmc/cores/AudioEngine/Sinks/test/TestAESinkDARWINOSX.cpp
new file mode 100644
index 0000000..d4b7adc
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/test/TestAESinkDARWINOSX.cpp
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2014-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 "cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h"
+#include "cores/AudioEngine/Sinks/osx/AEDeviceEnumerationOSX.h"
+#include "cores/AudioEngine/Sinks/osx/CoreAudioHardware.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+
+#include <vector>
+
+#include <gtest/gtest.h>
+
+std::vector<AudioStreamBasicDescription> stereoFormatsWithPassthrough;
+std::vector<AudioStreamBasicDescription> stereoFormatsWithoutPassthrough;
+std::vector<AudioStreamBasicDescription> allFormatsWithPassthrough;
+std::vector<AudioStreamBasicDescription> allFormatsWithoutPassthrough;
+
+void addLPCMFormats(std::vector<AudioStreamBasicDescription> &streamFormats)
+{
+ AudioStreamBasicDescription streamFormat;
+
+ FillOutASBDForLPCM(streamFormat, 96000, 8, 16, 16, false, false, false);
+ streamFormats.push_back(streamFormat); // + 0
+
+ FillOutASBDForLPCM(streamFormat, 48000, 8, 16, 16, false, false, false);
+ streamFormats.push_back(streamFormat); // + 1
+
+ FillOutASBDForLPCM(streamFormat, 44100, 8, 16, 16, false, false, false);
+ streamFormats.push_back(streamFormat); // + 2
+
+ FillOutASBDForLPCM(streamFormat, 96000, 8, 20, 20, false, false, false);
+ streamFormats.push_back(streamFormat); // + 3
+
+ FillOutASBDForLPCM(streamFormat, 48000, 8, 20, 20, false, false, false);
+ streamFormats.push_back(streamFormat); // + 4
+
+ FillOutASBDForLPCM(streamFormat, 44100, 8, 20, 20, false, false, false);
+ streamFormats.push_back(streamFormat); // + 5
+
+ FillOutASBDForLPCM(streamFormat, 96000, 8, 24, 24, false, false, false);
+ streamFormats.push_back(streamFormat); // + 6
+
+ FillOutASBDForLPCM(streamFormat, 48000, 8, 24, 24, false, false, false);
+ streamFormats.push_back(streamFormat); // + 7
+
+ FillOutASBDForLPCM(streamFormat, 44100, 8, 24, 24, false, false, false);
+ streamFormats.push_back(streamFormat); // + 8
+}
+
+void addPassthroughFormats(std::vector<AudioStreamBasicDescription> &streamFormats)
+{
+ AudioStreamBasicDescription streamFormat;
+
+ FillOutASBDForLPCM(streamFormat, 96000, 2, 16, 16, false, false, false);
+ streamFormat.mFormatID = kAudioFormat60958AC3;
+ streamFormats.push_back(streamFormat); // stereoFormatsWithoutPassthrough.size() + 0
+
+ FillOutASBDForLPCM(streamFormat, 48000, 2, 16, 16, false, false, false);
+ streamFormat.mFormatID = kAudioFormat60958AC3;
+ streamFormats.push_back(streamFormat); // stereoFormatsWithoutPassthrough.size() + 1
+
+ FillOutASBDForLPCM(streamFormat, 44100, 2, 16, 16, false, false, false);
+ streamFormat.mFormatID = kAudioFormat60958AC3;
+ streamFormats.push_back(streamFormat); // stereoFormatsWithoutPassthrough.size() + 2
+}
+
+void initStereoFormatsWithoutPassthrough()
+{
+ AudioStreamBasicDescription streamFormat;
+ FillOutASBDForLPCM(streamFormat, 96000, 2, 16, 16, false, false, false);
+ stereoFormatsWithoutPassthrough.push_back(streamFormat); // 0
+
+ FillOutASBDForLPCM(streamFormat, 48000, 2, 16, 16, false, false, false);
+ stereoFormatsWithoutPassthrough.push_back(streamFormat); // 1
+
+ FillOutASBDForLPCM(streamFormat, 44100, 2, 16, 16, false, false, false);
+ stereoFormatsWithoutPassthrough.push_back(streamFormat); // 2
+
+ FillOutASBDForLPCM(streamFormat, 96000, 2, 20, 20, false, false, false);
+ stereoFormatsWithoutPassthrough.push_back(streamFormat); // 3
+
+ FillOutASBDForLPCM(streamFormat, 48000, 2, 20, 20, false, false, false);
+ stereoFormatsWithoutPassthrough.push_back(streamFormat); // 4
+
+ FillOutASBDForLPCM(streamFormat, 44100, 2, 20, 20, false, false, false);
+ stereoFormatsWithoutPassthrough.push_back(streamFormat); // 5
+
+ FillOutASBDForLPCM(streamFormat, 96000, 2, 24, 24, false, false, false);
+ stereoFormatsWithoutPassthrough.push_back(streamFormat); // 6
+
+ FillOutASBDForLPCM(streamFormat, 48000, 2, 24, 24, false, false, false);
+ stereoFormatsWithoutPassthrough.push_back(streamFormat); // 7
+
+ FillOutASBDForLPCM(streamFormat, 44100, 2, 24, 24, false, false, false);
+ stereoFormatsWithoutPassthrough.push_back(streamFormat); // 8
+}
+
+void initStreamFormats()
+{
+ stereoFormatsWithPassthrough.clear();
+ stereoFormatsWithoutPassthrough.clear();
+ allFormatsWithPassthrough.clear();
+ allFormatsWithoutPassthrough.clear();
+
+ initStereoFormatsWithoutPassthrough();
+
+ stereoFormatsWithPassthrough = stereoFormatsWithoutPassthrough;
+ allFormatsWithoutPassthrough = stereoFormatsWithoutPassthrough;
+
+ addLPCMFormats( allFormatsWithoutPassthrough);
+
+ allFormatsWithPassthrough = allFormatsWithoutPassthrough;
+
+ addPassthroughFormats(stereoFormatsWithPassthrough);
+ addPassthroughFormats(allFormatsWithPassthrough);
+}
+
+
+AEAudioFormat getAC3AEFormat()
+{
+ AEAudioFormat srcFormat;
+ srcFormat.m_dataFormat = AE_FMT_RAW;
+ srcFormat.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_AC3;
+ srcFormat.m_sampleRate = 48000;
+ srcFormat.m_channelLayout = AE_CH_LAYOUT_2_0;
+ srcFormat.m_frames = 0;
+ srcFormat.m_frameSize = 0;
+ return srcFormat;
+}
+
+AEAudioFormat getStereo22050AEFormat()
+{
+ AEAudioFormat srcFormat;
+ srcFormat.m_dataFormat = AE_FMT_FLOAT;
+ srcFormat.m_sampleRate = 22050;
+ srcFormat.m_channelLayout = AE_CH_LAYOUT_2_0;
+ srcFormat.m_frames = 0;
+ srcFormat.m_frameSize = 0;
+ return srcFormat;
+}
+
+AEAudioFormat getStereo48000AEFormat()
+{
+ AEAudioFormat srcFormat;
+ srcFormat.m_dataFormat = AE_FMT_FLOAT;
+ srcFormat.m_sampleRate = 48000;
+ srcFormat.m_channelLayout = AE_CH_LAYOUT_2_0;
+ srcFormat.m_frames = 0;
+ srcFormat.m_frameSize = 0;
+ return srcFormat;
+}
+
+AEAudioFormat getLPCM96000AEFormat()
+{
+ AEAudioFormat srcFormat;
+ srcFormat.m_dataFormat = AE_FMT_FLOAT;
+ srcFormat.m_sampleRate = 96000;
+ srcFormat.m_channelLayout = AE_CH_LAYOUT_5_1;
+ srcFormat.m_frames = 0;
+ srcFormat.m_frameSize = 0;
+ return srcFormat;
+}
+
+unsigned int findMatchingFormat(const std::vector<AudioStreamBasicDescription> &formatList, const AEAudioFormat &srcFormat)
+{
+ unsigned int formatIdx = 0;
+ float highestScore = 0;
+ float currentScore = 0;
+ AEDeviceEnumerationOSX devEnum((AudioDeviceID)0);
+
+// fprintf(stderr, "%s: Matching streamFormat for source: %s with samplerate: %d\n", __FUNCTION__, CAEUtil::DataFormatToStr(srcFormat.m_dataFormat), srcFormat.m_sampleRate);
+ for (unsigned int i = 0; i < formatList.size(); i++)
+ {
+ AudioStreamBasicDescription desc = formatList[i];
+ std::string formatString;
+ currentScore = devEnum.ScoreFormat(desc, srcFormat);
+// fprintf(stderr, "%s: Physical Format: %s idx: %d rated %f\n", __FUNCTION__, StreamDescriptionToString(desc, formatString), i, currentScore);
+
+ if (currentScore > highestScore)
+ {
+ formatIdx = i;
+ highestScore = currentScore;
+ }
+ }
+
+ return formatIdx;
+}
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchAc3InStereoWithPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ //try to match the stream formats for ac3
+ AEAudioFormat srcFormat = getAC3AEFormat();
+ // mach ac3 in streamformats with dedicated passthrough format
+ formatIdx = findMatchingFormat(stereoFormatsWithPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, stereoFormatsWithoutPassthrough.size() + 1);
+}
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchAc3InStereoWithoutPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ //try to match the stream formats for ac3
+ AEAudioFormat srcFormat = getAC3AEFormat();
+ //match ac3 in streamformats without dedicated passthrough format (bitstream)
+ formatIdx = findMatchingFormat(stereoFormatsWithoutPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)1);
+}
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchAc3InAllWithPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ //try to match the stream formats for ac3
+ AEAudioFormat srcFormat = getAC3AEFormat();
+ // mach ac3 in streamformats with dedicated passthrough format
+ formatIdx = findMatchingFormat(allFormatsWithPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx,allFormatsWithoutPassthrough.size() + 1);
+}
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchAc3InAllWithoutPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ //try to match the stream formats for ac3
+ AEAudioFormat srcFormat = getAC3AEFormat();
+ //match ac3 in streamformats without dedicated passthrough format (bitstream)
+ formatIdx = findMatchingFormat(allFormatsWithoutPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)1);
+}
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloatStereo22050InStereoWithPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match stereo float 22050hz
+ AEAudioFormat srcFormat = getStereo22050AEFormat();
+ formatIdx = findMatchingFormat(stereoFormatsWithPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)8);
+}
+
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloatStereo22050InStereoWithoutPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match stereo float 22050hz
+ AEAudioFormat srcFormat = getStereo22050AEFormat();
+
+ formatIdx = findMatchingFormat(stereoFormatsWithoutPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)8);
+}
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloatStereo22050InAllWithPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match stereo float 22050hz
+ AEAudioFormat srcFormat = getStereo22050AEFormat();
+ formatIdx = findMatchingFormat(allFormatsWithPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)8);
+}
+
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloatStereo22050InAllWithoutPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match stereo float 22050hz
+ AEAudioFormat srcFormat = getStereo22050AEFormat();
+
+ formatIdx = findMatchingFormat(allFormatsWithoutPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)8);
+}
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloatStereo48000InStereoWithPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match stereo float 48000hz
+ AEAudioFormat srcFormat = getStereo48000AEFormat();
+ formatIdx = findMatchingFormat(stereoFormatsWithPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)7);
+}
+
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloatStereo48000InStereoWithoutPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match stereo float 48000hz
+ AEAudioFormat srcFormat = getStereo48000AEFormat();
+
+ formatIdx = findMatchingFormat(stereoFormatsWithoutPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)7);
+}
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloatStereo48000InAllWithPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match stereo float 48000hz
+ AEAudioFormat srcFormat = getStereo48000AEFormat();
+ formatIdx = findMatchingFormat(allFormatsWithPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)7);
+}
+
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloatStereo48000InAllWithoutPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match stereo float 48000hz
+ AEAudioFormat srcFormat = getStereo48000AEFormat();
+
+ formatIdx = findMatchingFormat(allFormatsWithoutPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)7);
+}
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloat5_1_96000InStereoWithPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match lpcm float 96000hz
+ AEAudioFormat srcFormat = getLPCM96000AEFormat();
+ formatIdx = findMatchingFormat(stereoFormatsWithPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)6);
+}
+
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloat5_1_96000InStereoWithoutPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match lpcm float 96000hz
+ AEAudioFormat srcFormat = getLPCM96000AEFormat();
+
+ formatIdx = findMatchingFormat(stereoFormatsWithoutPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)6);
+}
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloat5_1_96000InAllWithPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match lpcm float 96000hz
+ AEAudioFormat srcFormat = getLPCM96000AEFormat();
+ formatIdx = findMatchingFormat(allFormatsWithPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)15);
+}
+
+
+TEST(TestAESinkDARWINOSXScoreStream, MatchFloat5_1_96000InAllWithoutPassthroughFormats)
+{
+ unsigned int formatIdx = 0;
+ initStreamFormats();
+
+ // match lpcm float 96000hz
+ AEAudioFormat srcFormat = getLPCM96000AEFormat();
+
+ formatIdx = findMatchingFormat(allFormatsWithoutPassthrough, srcFormat);
+ EXPECT_EQ(formatIdx, (unsigned int)15);
+}
diff --git a/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin.cpp b/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin.cpp
new file mode 100644
index 0000000..7f6e4f9
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin.cpp
@@ -0,0 +1,174 @@
+/*
+ * 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 "AESinkFactoryWin.h"
+
+#include "utils/log.h"
+
+#define ERRTOSTR(err) case err: return #err
+
+const char *WASAPIErrToStr(HRESULT err)
+{
+ switch (err)
+ {
+ ERRTOSTR(AUDCLNT_E_NOT_INITIALIZED);
+ ERRTOSTR(AUDCLNT_E_ALREADY_INITIALIZED);
+ ERRTOSTR(AUDCLNT_E_WRONG_ENDPOINT_TYPE);
+ ERRTOSTR(AUDCLNT_E_DEVICE_INVALIDATED);
+ ERRTOSTR(AUDCLNT_E_NOT_STOPPED);
+ ERRTOSTR(AUDCLNT_E_BUFFER_TOO_LARGE);
+ ERRTOSTR(AUDCLNT_E_OUT_OF_ORDER);
+ ERRTOSTR(AUDCLNT_E_UNSUPPORTED_FORMAT);
+ ERRTOSTR(AUDCLNT_E_INVALID_SIZE);
+ ERRTOSTR(AUDCLNT_E_DEVICE_IN_USE);
+ ERRTOSTR(AUDCLNT_E_BUFFER_OPERATION_PENDING);
+ ERRTOSTR(AUDCLNT_E_THREAD_NOT_REGISTERED);
+ ERRTOSTR(AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED);
+ ERRTOSTR(AUDCLNT_E_ENDPOINT_CREATE_FAILED);
+ ERRTOSTR(AUDCLNT_E_SERVICE_NOT_RUNNING);
+ ERRTOSTR(AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED);
+ ERRTOSTR(AUDCLNT_E_EXCLUSIVE_MODE_ONLY);
+ ERRTOSTR(AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL);
+ ERRTOSTR(AUDCLNT_E_EVENTHANDLE_NOT_SET);
+ ERRTOSTR(AUDCLNT_E_INCORRECT_BUFFER_SIZE);
+ ERRTOSTR(AUDCLNT_E_BUFFER_SIZE_ERROR);
+ ERRTOSTR(AUDCLNT_E_CPUUSAGE_EXCEEDED);
+ ERRTOSTR(AUDCLNT_E_BUFFER_ERROR);
+ ERRTOSTR(AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED);
+ ERRTOSTR(AUDCLNT_E_INVALID_DEVICE_PERIOD);
+ ERRTOSTR(E_POINTER);
+ ERRTOSTR(E_INVALIDARG);
+ ERRTOSTR(E_OUTOFMEMORY);
+ default: break;
+ }
+ return "Undefined";
+}
+
+ void CAESinkFactoryWin::AEChannelsFromSpeakerMask(CAEChannelInfo& channelLayout, DWORD speakers)
+ {
+ channelLayout.Reset();
+
+ for (int i = 0; i < WASAPI_SPEAKER_COUNT; i++)
+ {
+ if (speakers & WASAPIChannelOrder[i])
+ channelLayout += AEChannelNames[i];
+ }
+
+ };
+
+ DWORD CAESinkFactoryWin::SpeakerMaskFromAEChannels(const CAEChannelInfo &channels)
+ {
+ DWORD mask = 0;
+
+ for (unsigned int i = 0; i < channels.Count(); i++)
+ {
+ for (unsigned int j = 0; j < WASAPI_SPEAKER_COUNT; j++)
+ if (channels[i] == AEChannelNames[j])
+ mask |= WASAPIChannelOrder[j];
+ }
+ return mask;
+ };
+
+ DWORD CAESinkFactoryWin::ChLayoutToChMask(const enum AEChannel * layout, unsigned int * numberOfChannels /*= NULL*/)
+ {
+ if (numberOfChannels)
+ *numberOfChannels = 0;
+ if (!layout)
+ return 0;
+
+ DWORD mask = 0;
+ unsigned int i;
+ for (i = 0; layout[i] != AE_CH_NULL; i++)
+ mask |= WASAPIChannelOrder[layout[i]];
+
+ if (numberOfChannels)
+ *numberOfChannels = i;
+
+ return mask;
+ };
+
+ void CAESinkFactoryWin::BuildWaveFormatExtensible(AEAudioFormat &format, WAVEFORMATEXTENSIBLE &wfxex)
+ {
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ wfxex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
+
+ if (format.m_dataFormat != AE_FMT_RAW) // PCM data
+ {
+ wfxex.dwChannelMask = CAESinkFactoryWin::SpeakerMaskFromAEChannels(format.m_channelLayout);
+ wfxex.Format.nChannels = (WORD)format.m_channelLayout.Count();
+ wfxex.Format.nSamplesPerSec = format.m_sampleRate;
+ wfxex.Format.wBitsPerSample = CAEUtil::DataFormatToBits((AEDataFormat)format.m_dataFormat);
+ wfxex.SubFormat = format.m_dataFormat < AE_FMT_FLOAT ? KSDATAFORMAT_SUBTYPE_PCM : KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+ }
+ else //Raw bitstream
+ {
+ wfxex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+ if (format.m_dataFormat == AE_FMT_RAW &&
+ ((format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_AC3) ||
+ (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_EAC3) ||
+ (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTSHD_CORE) ||
+ (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTS_2048) ||
+ (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTS_1024) ||
+ (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTS_512)))
+ {
+ if (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_EAC3)
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS;
+ else
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL;
+ wfxex.dwChannelMask = bool(format.m_channelLayout.Count() == 2) ? KSAUDIO_SPEAKER_STEREO : KSAUDIO_SPEAKER_5POINT1;
+ wfxex.Format.wBitsPerSample = 16;
+ wfxex.Samples.wValidBitsPerSample = 16;
+ wfxex.Format.nChannels = (WORD)format.m_channelLayout.Count();
+ wfxex.Format.nSamplesPerSec = format.m_sampleRate;
+ if (format.m_streamInfo.m_sampleRate == 0)
+ CLog::Log(LOGERROR, "Invalid sample rate supplied for RAW format");
+ }
+ else if (format.m_dataFormat == AE_FMT_RAW &&
+ ((format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTSHD_MA) ||
+ (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD) ||
+ (format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_DTSHD)))
+ {
+ // IEC 61937 transmissions over HDMI
+ wfxex.Format.nSamplesPerSec = 192000L;
+ wfxex.Format.wBitsPerSample = 16;
+ wfxex.Samples.wValidBitsPerSample = 16;
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
+
+ switch (format.m_streamInfo.m_type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP;
+ wfxex.Format.nChannels = 8; // Four IEC 60958 Lines.
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD;
+ wfxex.Format.nChannels = 8; // Four IEC 60958 Lines.
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ wfxex.SubFormat = KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD;
+ wfxex.Format.nChannels = 2; // One IEC 60958 Lines.
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_5POINT1;
+ break;
+ }
+
+ if (format.m_channelLayout.Count() == 8)
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_7POINT1_SURROUND;
+ else
+ wfxex.dwChannelMask = KSAUDIO_SPEAKER_5POINT1;
+ }
+ }
+
+ if (format.m_dataFormat == AE_FMT_S24NE4MSB)
+ wfxex.Samples.wValidBitsPerSample = 24;
+ else
+ wfxex.Samples.wValidBitsPerSample = wfxex.Format.wBitsPerSample;
+
+ wfxex.Format.nBlockAlign = wfxex.Format.nChannels * (wfxex.Format.wBitsPerSample >> 3);
+ wfxex.Format.nAvgBytesPerSec = wfxex.Format.nSamplesPerSec * wfxex.Format.nBlockAlign;
+ };
diff --git a/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin.h b/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin.h
new file mode 100644
index 0000000..dbe53f3
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin.h
@@ -0,0 +1,218 @@
+/*
+ * 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 "cores/AudioEngine/Utils/AEDeviceInfo.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "utils/log.h"
+
+#include <audioclient.h>
+
+#define WASAPI_SPEAKER_COUNT 21
+
+#ifdef TARGET_WINDOWS_STORE
+#ifndef _WAVEFORMATEXTENSIBLE_IEC61937_
+#define _WAVEFORMATEXTENSIBLE_IEC61937_
+typedef struct _WAVEFORMATEXTENSIBLE_IEC61937
+{
+ WAVEFORMATEXTENSIBLE FormatExt;
+ uint32_t dwEncodedSamplesPerSec;
+ uint32_t dwEncodedChannelCount;
+ uint32_t dwAverageBytesPerSec;
+} WAVEFORMATEXTENSIBLE_IEC61937, *PWAVEFORMATEXTENSIBLE_IEC61937;
+#endif /* _WAVEFORMATEXTENSIBLE_IEC61937_ */
+
+#define STATIC_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS \
+ 0x0000000aL, 0x0cea, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71
+DEFINE_GUIDSTRUCT("0000000a-0cea-0010-8000-00aa00389b71", KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS);
+#define KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS DEFINE_GUIDNAMED(KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS)
+
+#define STATIC_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL\
+ DEFINE_WAVEFORMATEX_GUID(WAVE_FORMAT_DOLBY_AC3_SPDIF)
+DEFINE_GUIDSTRUCT("00000092-0000-0010-8000-00aa00389b71", KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL);
+#define KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL DEFINE_GUIDNAMED(KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL)
+
+#define STATIC_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP \
+ 0x0000000cL, 0x0cea, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71
+DEFINE_GUIDSTRUCT("0000000c-0cea-0010-8000-00aa00389b71", KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP);
+#define KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP DEFINE_GUIDNAMED(KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP)
+
+#define STATIC_KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD \
+ 0x0000000bL, 0x0cea, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71
+DEFINE_GUIDSTRUCT("0000000b-0cea-0010-8000-00aa00389b71", KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD);
+#define KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD DEFINE_GUIDNAMED(KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD)
+
+#define STATIC_KSDATAFORMAT_SUBTYPE_IEC61937_DTS\
+ DEFINE_WAVEFORMATEX_GUID(WAVE_FORMAT_DTS)
+DEFINE_GUIDSTRUCT("00000008-0000-0010-8000-00aa00389b71", KSDATAFORMAT_SUBTYPE_IEC61937_DTS);
+#define KSDATAFORMAT_SUBTYPE_IEC61937_DTS DEFINE_GUIDNAMED(KSDATAFORMAT_SUBTYPE_IEC61937_DTS)
+
+
+#define KSAUDIO_SPEAKER_STEREO (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT)
+#define KSAUDIO_SPEAKER_7POINT1_SURROUND (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | \
+ SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | \
+ SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | \
+ SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT)
+#define KSAUDIO_SPEAKER_5POINT1 (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | \
+ SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | \
+ SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT)
+#endif
+
+static const unsigned int WASAPISampleRateCount = 10;
+static const unsigned int WASAPISampleRates[] = { 384000, 192000, 176400, 96000, 88200, 48000, 44100, 32000, 22050, 11025 };
+
+static const enum AEChannel layoutsList[][16] =
+{
+ /* Most common configurations */
+ {AE_CH_FC, AE_CH_NULL}, // Mono
+ {AE_CH_FL, AE_CH_FR, AE_CH_NULL}, // Stereo
+ {AE_CH_FL, AE_CH_FR, AE_CH_LFE, AE_CH_NULL}, // 2.1
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_NULL}, // 3 front speakers
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_NULL}, // 3 front speakers + LFE
+ {AE_CH_FL, AE_CH_FR, AE_CH_BL, AE_CH_BR, AE_CH_NULL}, // Quad
+ {AE_CH_FL, AE_CH_FR, AE_CH_BL, AE_CH_BR, AE_CH_LFE, AE_CH_NULL}, // Quad + LFE
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BC, AE_CH_NULL}, // Surround
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BC, AE_CH_LFE, AE_CH_NULL}, // Surround + LFE
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_NULL}, // Standard 5.1
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_BL, AE_CH_BR, AE_CH_NULL}, // 5.1 wide (obsolete)
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR, AE_CH_NULL}, // Standard 7.1
+ /* Less common configurations */
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_BL, AE_CH_BR, AE_CH_FLOC,AE_CH_FROC,AE_CH_NULL}, // 7.1 wide (obsolete)
+ /* Exotic configurations */
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_SL, AE_CH_SR, AE_CH_NULL}, // Standard 5.1 w/o LFE
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BL, AE_CH_BR, AE_CH_NULL}, // 5.1 wide w/o LFE
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_SL, AE_CH_SR, AE_CH_BC, AE_CH_NULL}, // Standard 5.1 w/o LFE + Back Center
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BL, AE_CH_BC, AE_CH_BR, AE_CH_NULL}, // 5.1 wide w/o LFE + Back Center
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_BL, AE_CH_BR, AE_CH_TC, AE_CH_NULL}, // DVD speakers
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BL, AE_CH_BR, AE_CH_BC, AE_CH_LFE, AE_CH_NULL}, // 5.1 wide + Back Center
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR, AE_CH_NULL}, // Standard 7.1 w/o LFE
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BL, AE_CH_BR, AE_CH_FLOC,AE_CH_FROC,AE_CH_NULL}, // 7.1 wide w/o LFE
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BC, AE_CH_BR, AE_CH_NULL}, // Standard 7.1 + Back Center
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR, AE_CH_FLOC,AE_CH_FROC,AE_CH_NULL}, // Standard 7.1 + front wide
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR, AE_CH_TFL, AE_CH_TFR, AE_CH_NULL}, // Standard 7.1 + 2 front top
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR, AE_CH_TFL, AE_CH_TFR, AE_CH_TFC, AE_CH_NULL}, // Standard 7.1 + 3 front top
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR, AE_CH_TFL, AE_CH_TFR, AE_CH_TBL, AE_CH_TBR, AE_CH_NULL}, // Standard 7.1 + 2 front top + 2 back top
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR, AE_CH_TFL, AE_CH_TFR, AE_CH_TFC, AE_CH_TBL, AE_CH_TBR, AE_CH_NULL}, // Standard 7.1 + 3 front top + 2 back top
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR, AE_CH_TFL, AE_CH_TFR, AE_CH_TFC, AE_CH_TBL, AE_CH_TBR, AE_CH_TBC, AE_CH_NULL}, // Standard 7.1 + 3 front top + 3 back top
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR, AE_CH_TFL, AE_CH_TFR, AE_CH_TFC, AE_CH_TBL, AE_CH_TBR, AE_CH_TBC, AE_CH_TC, AE_CH_NULL} // Standard 7.1 + 3 front top + 3 back top + Top Center
+};
+
+static enum AEChannel layoutsByChCount[9][9] = {
+ {AE_CH_NULL},
+ {AE_CH_FC, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_BL, AE_CH_BR, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BL, AE_CH_BR, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BL, AE_CH_BR, AE_CH_LFE, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BL, AE_CH_BR, AE_CH_BC, AE_CH_LFE, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BL, AE_CH_BR, AE_CH_SL, AE_CH_SR, AE_CH_LFE, AE_CH_NULL}};
+
+static const unsigned int WASAPIChannelOrder[] = {AE_CH_RAW,
+ SPEAKER_FRONT_LEFT, SPEAKER_FRONT_RIGHT, SPEAKER_FRONT_CENTER,
+ SPEAKER_LOW_FREQUENCY, SPEAKER_BACK_LEFT, SPEAKER_BACK_RIGHT,
+ SPEAKER_FRONT_LEFT_OF_CENTER, SPEAKER_FRONT_RIGHT_OF_CENTER,
+ SPEAKER_BACK_CENTER, SPEAKER_SIDE_LEFT, SPEAKER_SIDE_RIGHT,
+ SPEAKER_TOP_FRONT_LEFT, SPEAKER_TOP_FRONT_RIGHT, SPEAKER_TOP_FRONT_CENTER,
+ SPEAKER_TOP_CENTER, SPEAKER_TOP_BACK_LEFT, SPEAKER_TOP_BACK_RIGHT,
+ SPEAKER_TOP_BACK_CENTER, SPEAKER_RESERVED, SPEAKER_RESERVED};
+
+static const enum AEChannel AEChannelNames[] = {AE_CH_RAW,
+ AE_CH_FL, AE_CH_FR, AE_CH_FC,
+ AE_CH_LFE, AE_CH_BL, AE_CH_BR,
+ AE_CH_FLOC, AE_CH_FROC,
+ AE_CH_BC, AE_CH_SL, AE_CH_SR,
+ AE_CH_TFL, AE_CH_TFR, AE_CH_TFC ,
+ AE_CH_TC , AE_CH_TBL, AE_CH_TBR,
+ AE_CH_TBC, AE_CH_BLOC, AE_CH_BROC};
+
+struct winEndpointsToAEDeviceType
+{
+ std::string winEndpointType;
+ AEDeviceType aeDeviceType;
+};
+
+const winEndpointsToAEDeviceType winEndpoints[] =
+{
+ { "Network Device - ", AE_DEVTYPE_PCM },
+ { "Speakers - ", AE_DEVTYPE_PCM },
+ { "LineLevel - ", AE_DEVTYPE_PCM },
+ { "Headphones - ", AE_DEVTYPE_PCM },
+ { "Microphone - ", AE_DEVTYPE_PCM },
+ { "Headset - ", AE_DEVTYPE_PCM },
+ { "Handset - ", AE_DEVTYPE_PCM },
+ { "Digital Passthrough - ", AE_DEVTYPE_IEC958 },
+ { "SPDIF - ", AE_DEVTYPE_IEC958 },
+ { "HDMI - ", AE_DEVTYPE_HDMI },
+ { "Unknown - ", AE_DEVTYPE_PCM },
+};
+
+struct sampleFormat
+{
+ GUID subFormat;
+ unsigned int bitsPerSample;
+ unsigned int validBitsPerSample;
+ AEDataFormat subFormatType;
+};
+
+//! @todo
+//! Sample formats go from float -> 32 bit int -> 24 bit int (packed in 32) -> -> 24 bit int -> 16 bit int */
+//! versions of Kodi before 14.0 had a bug which made S24NE4MSB the first format selected
+//! this bug worked around some driver bug of some IEC958 devices which report S32 but can't handle it
+//! correctly. So far I have never seen and WASAPI device using S32 and don't think probing S24 before
+//! S32 has any negative impact.
+static const sampleFormat testFormats[] = { {KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 32, 32, AE_FMT_FLOAT},
+ {KSDATAFORMAT_SUBTYPE_PCM, 32, 24, AE_FMT_S24NE4MSB},
+ {KSDATAFORMAT_SUBTYPE_PCM, 32, 32, AE_FMT_S32NE},
+ {KSDATAFORMAT_SUBTYPE_PCM, 24, 24, AE_FMT_S24NE3},
+ {KSDATAFORMAT_SUBTYPE_PCM, 16, 16, AE_FMT_S16NE} };
+
+struct RendererDetail
+{
+ std::string strDeviceId;
+ std::string strDescription;
+ std::string strWinDevType;
+ AEDeviceType eDeviceType;
+ unsigned int nChannels;
+ unsigned int uiChannelMask;
+ bool bDefault;
+};
+
+struct IAEWASAPIDevice
+{
+ std::string deviceId;
+ virtual int Release() = 0;
+ virtual HRESULT Activate(IAudioClient** ppAudioClient) = 0;
+ virtual bool IsUSBDevice() = 0;
+};
+
+class CAESinkFactoryWin
+{
+public:
+ /*
+ Gets list of audio renderers available on platform
+ */
+ static std::vector<RendererDetail> GetRendererDetails();
+ /*
+ Gets default device id
+ */
+ static std::string GetDefaultDeviceId();
+ /*
+ Initialize IAEWASAPIDevice
+ */
+ static HRESULT ActivateWASAPIDevice(std::string &device, IAEWASAPIDevice** ppDevice);
+
+ static void AEChannelsFromSpeakerMask(CAEChannelInfo& channelLayout, DWORD speakers);
+
+ static DWORD SpeakerMaskFromAEChannels(const CAEChannelInfo &channels);
+
+ static DWORD ChLayoutToChMask(const enum AEChannel * layout, unsigned int * numberOfChannels = NULL);
+
+ static void BuildWaveFormatExtensible(AEAudioFormat &format, WAVEFORMATEXTENSIBLE &wfxex);
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin10.cpp b/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin10.cpp
new file mode 100644
index 0000000..3ef08b1
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin10.cpp
@@ -0,0 +1,245 @@
+/*
+ * 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 "AESinkFactoryWin.h"
+#include "utils/log.h"
+
+#include "platform/win10/AsyncHelpers.h"
+#include "platform/win32/CharsetConverter.h"
+
+#include <mmdeviceapi.h>
+#include <mmreg.h>
+#include <winrt/Windows.Devices.Enumeration.h>
+#include <winrt/Windows.Media.Devices.Core.h>
+#include <winrt/Windows.Media.Devices.h>
+
+using namespace winrt::Windows::Devices::Enumeration;
+using namespace winrt::Windows::Media::Devices;
+using namespace winrt::Windows::Media::Devices::Core;
+using namespace Microsoft::WRL;
+
+static winrt::hstring PKEY_Device_FriendlyName = L"System.ItemNameDisplay";
+static winrt::hstring PKEY_AudioEndpoint_FormFactor = L"{1da5d803-d492-4edd-8c23-e0c0ffee7f0e} 0";
+static winrt::hstring PKEY_AudioEndpoint_ControlPanelPageProvider = L"{1da5d803-d492-4edd-8c23-e0c0ffee7f0e} 1";
+static winrt::hstring PKEY_AudioEndpoint_Association = L"{1da5d803-d492-4edd-8c23-e0c0ffee7f0e} 2";
+static winrt::hstring PKEY_AudioEndpoint_PhysicalSpeakers = L"{1da5d803-d492-4edd-8c23-e0c0ffee7f0e} 3";
+static winrt::hstring PKEY_AudioEndpoint_GUID = L"{1da5d803-d492-4edd-8c23-e0c0ffee7f0e} 4";
+static winrt::hstring PKEY_AudioEndpoint_Disable_SysFx = L"{1da5d803-d492-4edd-8c23-e0c0ffee7f0e} 5";
+static winrt::hstring PKEY_AudioEndpoint_FullRangeSpeakers = L"{1da5d803-d492-4edd-8c23-e0c0ffee7f0e} 6";
+static winrt::hstring PKEY_AudioEndpoint_Supports_EventDriven_Mode = L"{1da5d803-d492-4edd-8c23-e0c0ffee7f0e} 7";
+static winrt::hstring PKEY_AudioEndpoint_JackSubType = L"{1da5d803-d492-4edd-8c23-e0c0ffee7f0e} 8";
+static winrt::hstring PKEY_AudioEndpoint_Default_VolumeInDb = L"{1da5d803-d492-4edd-8c23-e0c0ffee7f0e} 9";
+static winrt::hstring PKEY_AudioEngine_DeviceFormat = L"{f19f064d-082c-4e27-bc73-6882a1bb8e4c} 0";
+static winrt::hstring PKEY_Device_EnumeratorName = L"{a45c254e-df1c-4efd-8020-67d146a850e0} 24";
+
+std::vector<RendererDetail> CAESinkFactoryWin::GetRendererDetails()
+{
+ std::vector<RendererDetail> list;
+ try
+ {
+ // Get the string identifier of the audio renderer
+ auto defaultId = MediaDevice::GetDefaultAudioRenderId(AudioDeviceRole::Default);
+ auto audioSelector = MediaDevice::GetAudioRenderSelector();
+
+ // Add custom properties to the query
+ DeviceInformationCollection devInfocollection = Wait(DeviceInformation::FindAllAsync(audioSelector,
+ {
+ PKEY_AudioEndpoint_FormFactor,
+ PKEY_AudioEndpoint_GUID,
+ PKEY_AudioEndpoint_PhysicalSpeakers,
+ PKEY_AudioEngine_DeviceFormat,
+ PKEY_Device_EnumeratorName
+ }));
+ if (devInfocollection == nullptr || devInfocollection.Size() == 0)
+ goto failed;
+
+ for (unsigned int i = 0; i < devInfocollection.Size(); i++)
+ {
+ RendererDetail details;
+
+ DeviceInformation devInfo = devInfocollection.GetAt(i);
+ if (devInfo.Properties().Size() == 0)
+ goto failed;
+
+ winrt::IInspectable propObj = nullptr;
+
+ propObj = devInfo.Properties().Lookup(PKEY_AudioEndpoint_FormFactor);
+ if (!propObj)
+ goto failed;
+
+ details.strWinDevType = winEndpoints[propObj.as<winrt::IPropertyValue>().GetUInt32()].winEndpointType;
+ details.eDeviceType = winEndpoints[propObj.as<winrt::IPropertyValue>().GetUInt32()].aeDeviceType;
+
+ unsigned long ulChannelMask = 0;
+ unsigned int nChannels = 0;
+
+ propObj = devInfo.Properties().Lookup(PKEY_AudioEngine_DeviceFormat);
+ if (propObj)
+ {
+ winrt::com_array<uint8_t> com_arr;
+ propObj.as<winrt::IPropertyValue>().GetUInt8Array(com_arr);
+
+ WAVEFORMATEXTENSIBLE* smpwfxex = (WAVEFORMATEXTENSIBLE*)com_arr.data();
+ nChannels = std::max(std::min(smpwfxex->Format.nChannels, (WORD)8), (WORD)2);
+ ulChannelMask = smpwfxex->dwChannelMask;
+ }
+ else
+ {
+ // suppose stereo
+ nChannels = 2;
+ ulChannelMask = 3;
+ }
+
+ propObj = devInfo.Properties().Lookup(PKEY_AudioEndpoint_PhysicalSpeakers);
+ details.uiChannelMask = propObj ? propObj.as<winrt::IPropertyValue>().GetUInt32() : ulChannelMask;
+ details.nChannels = nChannels;
+
+ details.strDescription = KODI::PLATFORM::WINDOWS::FromW(devInfo.Name().c_str());
+ details.strDeviceId = KODI::PLATFORM::WINDOWS::FromW(devInfo.Id().c_str());
+
+ details.bDefault = (devInfo.Id() == defaultId);
+
+ list.push_back(details);
+ }
+ return list;
+ }
+ catch (...)
+ {
+ }
+
+failed:
+ CLog::Log(LOGERROR, __FUNCTION__": Failed to enumerate audio renderer devices.");
+ return list;
+}
+
+class CAudioInterfaceActivator : public winrt::implements<CAudioInterfaceActivator, IActivateAudioInterfaceCompletionHandler>
+{
+ Concurrency::task_completion_event<IAudioClient*> m_ActivateCompleted;
+
+ STDMETHODIMP ActivateCompleted(IActivateAudioInterfaceAsyncOperation *pAsyncOp)
+ {
+ HRESULT hr = S_OK, hr2 = S_OK;
+ ComPtr<IUnknown> clientUnk;
+ IAudioClient* audioClient2;
+
+ // Get the audio activation result as IUnknown pointer
+ hr2 = pAsyncOp->GetActivateResult(&hr, &clientUnk);
+
+ // Report activation failure
+ if (FAILED(hr))
+ {
+ m_ActivateCompleted.set_exception(winrt::hresult_error(hr));
+ goto exit;
+ }
+
+ // Report failure to get activate result
+ if (FAILED(hr2))
+ {
+ m_ActivateCompleted.set_exception(winrt::hresult_error(hr2));
+ goto exit;
+ }
+
+ // Query for the activated IAudioClient2 interface
+ hr = clientUnk->QueryInterface(IID_PPV_ARGS(&audioClient2));
+
+ if (FAILED(hr))
+ {
+ m_ActivateCompleted.set_exception(winrt::hresult_error(hr));
+ goto exit;
+ }
+
+ // Set the completed event and return success
+ m_ActivateCompleted.set(audioClient2);
+
+ exit:
+ return hr;
+
+ }
+public:
+ static Concurrency::task<IAudioClient*> ActivateAsync(LPCWCHAR pszDeviceId)
+ {
+ winrt::com_ptr<CAudioInterfaceActivator> activator = winrt::make_self<CAudioInterfaceActivator>();
+ ComPtr<IActivateAudioInterfaceAsyncOperation> asyncOp;
+
+ HRESULT hr = ActivateAudioInterfaceAsync(
+ pszDeviceId,
+ __uuidof(IAudioClient2),
+ nullptr,
+ activator.get(),
+ &asyncOp);
+
+ if (FAILED(hr))
+ throw winrt::hresult_error(hr);
+
+ // Wait for the activate completed event and return result
+ return Concurrency::create_task(activator->m_ActivateCompleted);
+
+ }
+};
+
+struct AEWASAPIDeviceWin10 : public IAEWASAPIDevice
+{
+ HRESULT AEWASAPIDeviceWin10::Activate(IAudioClient** ppAudioClient)
+ {
+ if (!ppAudioClient)
+ return E_POINTER;
+
+ HRESULT hr;
+ std::wstring deviceIdW = KODI::PLATFORM::WINDOWS::ToW(deviceId);
+ IAudioClient* pClient = CAudioInterfaceActivator::ActivateAsync(deviceIdW.c_str())
+ .then([&hr](Concurrency::task<IAudioClient*> task) -> IAudioClient*
+ {
+ try
+ {
+ return task.get();
+ }
+ catch (const winrt::hresult_error& ex)
+ {
+ hr = ex.code();
+ return nullptr;
+ }
+ }).get();
+
+ if (pClient)
+ {
+ *ppAudioClient = pClient;
+ return S_OK;
+ }
+ return E_UNEXPECTED;
+ };
+
+ int AEWASAPIDeviceWin10::Release() override
+ {
+ delete this;
+ return 0;
+ };
+
+ bool AEWASAPIDeviceWin10::IsUSBDevice() override
+ {
+ // TODO
+ return false;
+ }
+};
+
+std::string CAESinkFactoryWin::GetDefaultDeviceId()
+{
+ // Get the string identifier of the audio renderer
+ auto defaultId = MediaDevice::GetDefaultAudioRenderId(AudioDeviceRole::Default);
+ return KODI::PLATFORM::WINDOWS::FromW(defaultId.c_str());
+}
+
+HRESULT CAESinkFactoryWin::ActivateWASAPIDevice(std::string &device, IAEWASAPIDevice** ppDevice)
+{
+ if (!ppDevice)
+ return E_POINTER;
+
+ AEWASAPIDeviceWin10* pDevice = new AEWASAPIDeviceWin10;
+ pDevice->deviceId = device;
+
+ *ppDevice = pDevice;
+ return S_OK;
+} \ No newline at end of file
diff --git a/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin32.cpp b/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin32.cpp
new file mode 100644
index 0000000..aef296e
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/windows/AESinkFactoryWin32.cpp
@@ -0,0 +1,313 @@
+/*
+ * 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 "AESinkFactoryWin.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include "platform/win32/CharsetConverter.h"
+
+#include <algorithm>
+
+#include <mmdeviceapi.h>
+#include <wrl/client.h>
+
+const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
+const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
+const IID IID_IAudioClient = __uuidof(IAudioClient);
+
+DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14);
+DEFINE_PROPERTYKEY(PKEY_Device_EnumeratorName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 24);
+
+extern const char *WASAPIErrToStr(HRESULT err);
+#define EXIT_ON_FAILURE(hr, reason) \
+ if (FAILED(hr)) \
+ { \
+ CLog::LogF(LOGERROR, reason " - HRESULT = {} ErrorMessage = {}", hr, WASAPIErrToStr(hr)); \
+ goto failed; \
+ }
+
+using namespace Microsoft::WRL;
+
+std::vector<RendererDetail> CAESinkFactoryWin::GetRendererDetails()
+{
+ std::vector<RendererDetail> list;
+ ComPtr<IMMDeviceEnumerator> pEnumerator = nullptr;
+ ComPtr<IMMDeviceCollection> pEnumDevices = nullptr;
+ ComPtr<IMMDevice> pDefaultDevice = nullptr;
+ LPWSTR pwszID = nullptr;
+ std::wstring wstrDDID;
+ HRESULT hr;
+ UINT uiCount = 0;
+
+ hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, reinterpret_cast<void**>(pEnumerator.GetAddressOf()));
+ EXIT_ON_FAILURE(hr, "Could not allocate WASAPI device enumerator.")
+
+
+ // get the default audio endpoint
+ if (pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, pDefaultDevice.GetAddressOf()) == S_OK)
+ {
+ if (pDefaultDevice->GetId(&pwszID) == S_OK)
+ {
+ wstrDDID = pwszID;
+ CoTaskMemFree(pwszID);
+ }
+ pDefaultDevice.Reset();
+ }
+
+ // enumerate over all audio endpoints
+ hr = pEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, pEnumDevices.GetAddressOf());
+ EXIT_ON_FAILURE(hr, "Retrieval of audio endpoint enumeration failed.")
+
+ hr = pEnumDevices->GetCount(&uiCount);
+ EXIT_ON_FAILURE(hr, "Retrieval of audio endpoint count failed.")
+
+ for (UINT i = 0; i < uiCount; i++)
+ {
+ RendererDetail details{};
+ ComPtr<IMMDevice> pDevice = nullptr;
+ ComPtr<IPropertyStore> pProperty = nullptr;
+ PROPVARIANT varName;
+ PropVariantInit(&varName);
+
+ hr = pEnumDevices->Item(i, pDevice.GetAddressOf());
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGERROR, "Retrieval of WASAPI endpoint failed.");
+ goto failed;
+ }
+
+ hr = pDevice->OpenPropertyStore(STGM_READ, pProperty.ReleaseAndGetAddressOf());
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGERROR, "Retrieval of WASAPI endpoint properties failed.");
+ goto failed;
+ }
+
+ hr = pProperty->GetValue(PKEY_Device_FriendlyName, &varName);
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGERROR, "Retrieval of WASAPI endpoint device name failed.");
+ goto failed;
+ }
+
+ details.strDescription = KODI::PLATFORM::WINDOWS::FromW(varName.pwszVal);
+ PropVariantClear(&varName);
+
+ hr = pProperty->GetValue(PKEY_AudioEndpoint_GUID, &varName);
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGERROR, "Retrieval of WASAPI endpoint GUID failed.");
+ goto failed;
+ }
+
+ details.strDeviceId = KODI::PLATFORM::WINDOWS::FromW(varName.pwszVal);
+ PropVariantClear(&varName);
+
+ hr = pProperty->GetValue(PKEY_AudioEndpoint_FormFactor, &varName);
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGERROR, "Retrieval of WASAPI endpoint form factor failed.");
+ goto failed;
+ }
+ details.strWinDevType = winEndpoints[(EndpointFormFactor)varName.uiVal].winEndpointType;
+ details.eDeviceType = winEndpoints[(EndpointFormFactor)varName.uiVal].aeDeviceType;
+
+ PropVariantClear(&varName);
+
+ hr = pProperty->GetValue(PKEY_AudioEndpoint_PhysicalSpeakers, &varName);
+ if (FAILED(hr))
+ {
+ CLog::Log(LOGERROR, "Retrieval of WASAPI endpoint speaker layout failed.");
+ goto failed;
+ }
+
+ details.uiChannelMask = std::max(varName.uintVal, (unsigned int)KSAUDIO_SPEAKER_STEREO);
+ PropVariantClear(&varName);
+
+ if (pDevice->GetId(&pwszID) == S_OK)
+ {
+ if (wstrDDID.compare(pwszID) == 0)
+ details.bDefault = true;
+
+ CoTaskMemFree(pwszID);
+ }
+
+ list.push_back(details);
+ }
+
+ return list;
+
+failed:
+
+ CLog::Log(LOGERROR, "Failed to enumerate audio renderer devices.");
+ return list;
+}
+
+struct AEWASAPIDeviceWin32 : public IAEWASAPIDevice
+{
+ friend CAESinkFactoryWin;
+
+ HRESULT AEWASAPIDeviceWin32::Activate(IAudioClient** ppAudioClient)
+ {
+ HRESULT hr = S_FALSE;
+
+ if (!ppAudioClient)
+ return E_POINTER;
+
+ try
+ {
+ ComPtr<IAudioClient> pClient = nullptr;
+ hr = m_pDevice->Activate(IID_IAudioClient, CLSCTX_ALL, NULL, reinterpret_cast<void**>(pClient.GetAddressOf()));
+ if (SUCCEEDED(hr) && pClient)
+ {
+ *ppAudioClient = pClient.Detach();
+ return hr;
+ }
+ }
+ catch (...) {}
+ return hr;
+ };
+
+ int AEWASAPIDeviceWin32::Release() override
+ {
+ delete this;
+ return 0;
+ };
+
+ bool AEWASAPIDeviceWin32::IsUSBDevice() override
+ {
+ bool ret = false;
+ ComPtr<IPropertyStore> pProperty = nullptr;
+ PROPVARIANT varName;
+ PropVariantInit(&varName);
+
+ HRESULT hr = m_pDevice->OpenPropertyStore(STGM_READ, pProperty.GetAddressOf());
+ if (!SUCCEEDED(hr))
+ return ret;
+ hr = pProperty->GetValue(PKEY_Device_EnumeratorName, &varName);
+
+ std::string str = KODI::PLATFORM::WINDOWS::FromW(varName.pwszVal);
+ StringUtils::ToUpper(str);
+ ret = (str == "USB");
+ PropVariantClear(&varName);
+ return ret;
+ }
+
+protected:
+ AEWASAPIDeviceWin32(IMMDevice* pDevice)
+ : m_pDevice(pDevice)
+ {
+ }
+
+private:
+ ComPtr<IMMDevice> m_pDevice{ nullptr };
+};
+
+std::string CAESinkFactoryWin::GetDefaultDeviceId()
+{
+ std::string strDeviceId = "";
+ ComPtr<IMMDevice> pDevice = nullptr;
+ ComPtr<IMMDeviceEnumerator> pEnumerator = nullptr;
+ LPWSTR pwszID = NULL;
+ std::wstring wstrDDID;
+
+ HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, reinterpret_cast<void**>(pEnumerator.GetAddressOf()));
+ EXIT_ON_FAILURE(hr, "Could not allocate WASAPI device enumerator.")
+
+ // get the default audio endpoint
+ if (pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, pDevice.GetAddressOf()) == S_OK)
+ {
+ ComPtr<IPropertyStore> pProperty = nullptr;
+ PROPVARIANT varName;
+ PropVariantInit(&varName);
+
+ hr = pDevice->OpenPropertyStore(STGM_READ, pProperty.GetAddressOf());
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Retrieval of WASAPI endpoint properties failed.");
+ goto failed;
+ }
+
+ hr = pProperty->GetValue(PKEY_AudioEndpoint_GUID, &varName);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Retrieval of WASAPI endpoint GUID failed.");
+ goto failed;
+ }
+ strDeviceId = KODI::PLATFORM::WINDOWS::FromW(varName.pwszVal);
+ PropVariantClear(&varName);
+
+ }
+
+failed:
+ return strDeviceId;
+}
+
+HRESULT CAESinkFactoryWin::ActivateWASAPIDevice(std::string &device, IAEWASAPIDevice **ppDevice)
+{
+ ComPtr<IMMDevice> pDevice = nullptr;
+ ComPtr<IMMDeviceEnumerator> pEnumerator = nullptr;
+ ComPtr<IMMDeviceCollection> pEnumDevices = nullptr;
+ UINT uiCount = 0;
+
+ if (!ppDevice)
+ return E_POINTER;
+
+ HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, reinterpret_cast<void**>(pEnumerator.GetAddressOf()));
+ EXIT_ON_FAILURE(hr, "Could not allocate WASAPI device enumerator.")
+
+ /* Get our device. First try to find the named device. */
+
+ hr = pEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, pEnumDevices.GetAddressOf());
+ EXIT_ON_FAILURE(hr, "Retrieval of audio endpoint enumeration failed.")
+
+ hr = pEnumDevices->GetCount(&uiCount);
+ EXIT_ON_FAILURE(hr, "Retrieval of audio endpoint count failed.")
+
+ for (UINT i = 0; i < uiCount; i++)
+ {
+ ComPtr<IPropertyStore> pProperty = nullptr;
+ PROPVARIANT varName;
+
+ hr = pEnumDevices->Item(i, pDevice.GetAddressOf());
+ EXIT_ON_FAILURE(hr, "Retrieval of WASAPI endpoint failed.")
+
+ hr = pDevice->OpenPropertyStore(STGM_READ, pProperty.GetAddressOf());
+ EXIT_ON_FAILURE(hr, "Retrieval of WASAPI endpoint properties failed.")
+
+ hr = pProperty->GetValue(PKEY_AudioEndpoint_GUID, &varName);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "Retrieval of WASAPI endpoint GUID failed.");
+ goto failed;
+ }
+
+ std::string strDevName = KODI::PLATFORM::WINDOWS::FromW(varName.pwszVal);
+
+ if (device == strDevName)
+ i = uiCount;
+ else
+ pDevice.Reset();
+
+ PropVariantClear(&varName);
+ }
+
+ if (pDevice)
+ {
+ AEWASAPIDeviceWin32* pAEDevice = new AEWASAPIDeviceWin32(pDevice.Get());
+ pAEDevice->deviceId = device;
+ *ppDevice = pAEDevice;
+ return S_OK;
+ }
+
+ return E_FAIL;
+
+failed:
+ CLog::LogF(LOGERROR, "WASAPI initialization failed.");
+ return hr;
+}
diff --git a/xbmc/cores/AudioEngine/Utils/AEAudioFormat.h b/xbmc/cores/AudioEngine/Utils/AEAudioFormat.h
new file mode 100644
index 0000000..c0638c1
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEAudioFormat.h
@@ -0,0 +1,69 @@
+/*
+ * 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 "AEChannelInfo.h"
+#include "AEStreamInfo.h"
+
+#define AE_IS_PLANAR(x) ((x) >= AE_FMT_U8P && (x) <= AE_FMT_FLOATP)
+
+/**
+ * The audio format structure that fully defines a stream's audio information
+ */
+struct AEAudioFormat
+{
+ /**
+ * The stream's data format (eg, AE_FMT_S16LE)
+ */
+ enum AEDataFormat m_dataFormat;
+
+ /**
+ * The stream's sample rate (eg, 48000)
+ */
+ unsigned int m_sampleRate;
+
+ /**
+ * The stream's channel layout
+ */
+ CAEChannelInfo m_channelLayout;
+
+ /**
+ * The number of frames per period
+ */
+ unsigned int m_frames;
+
+ /**
+ * The size of one frame in bytes
+ */
+ unsigned int m_frameSize;
+
+ /**
+ * Stream info of raw passthrough
+ */
+ CAEStreamInfo m_streamInfo;
+
+ AEAudioFormat()
+ {
+ m_dataFormat = AE_FMT_INVALID;
+ m_sampleRate = 0;
+ m_frames = 0;
+ m_frameSize = 0;
+ }
+
+ bool operator==(const AEAudioFormat& fmt) const
+ {
+ return m_dataFormat == fmt.m_dataFormat &&
+ m_sampleRate == fmt.m_sampleRate &&
+ m_channelLayout == fmt.m_channelLayout &&
+ m_frames == fmt.m_frames &&
+ m_frameSize == fmt.m_frameSize &&
+ m_streamInfo == fmt.m_streamInfo;
+ }
+};
+
diff --git a/xbmc/cores/AudioEngine/Utils/AEBitstreamPacker.cpp b/xbmc/cores/AudioEngine/Utils/AEBitstreamPacker.cpp
new file mode 100644
index 0000000..cf1c515
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEBitstreamPacker.cpp
@@ -0,0 +1,439 @@
+/*
+ * 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 "AEBitstreamPacker.h"
+
+#include "AEPackIEC61937.h"
+#include "AEStreamInfo.h"
+#include "utils/log.h"
+
+#include <array>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+extern "C"
+{
+#include <libavutil/intreadwrite.h>
+}
+
+namespace
+{
+constexpr auto BURST_HEADER_SIZE = 8;
+constexpr auto EAC3_MAX_BURST_PAYLOAD_SIZE = 24576 - BURST_HEADER_SIZE;
+
+constexpr auto MAT_PKT_OFFSET = 61440;
+constexpr auto MAT_FRAME_SIZE = 61424;
+
+/* magic MAT format values, meaning is unknown at this point */
+constexpr std::array<uint8_t, 20> mat_start_code = {
+ 0x07, 0x9E, 0x00, 0x03, 0x84, 0x01, 0x01, 0x01, 0x80, 0x00,
+ 0x56, 0xA5, 0x3B, 0xF4, 0x81, 0x83, 0x49, 0x80, 0x77, 0xE0,
+};
+
+constexpr std::array<uint8_t, 12> mat_middle_code = {
+ 0xC3, 0xC1, 0x42, 0x49, 0x3B, 0xFA, 0x82, 0x83, 0x49, 0x80, 0x77, 0xE0,
+};
+
+constexpr std::array<uint8_t, 16> mat_end_code = {
+ 0xC3, 0xC2, 0xC0, 0xC4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x97, 0x11,
+};
+
+struct MatCode
+{
+ int pos;
+ const uint8_t* code;
+ unsigned int len;
+};
+
+std::array<MatCode, 3> MatCodes = {{
+ {0, mat_start_code.data(), mat_start_code.size()},
+ {30708, mat_middle_code.data(), mat_middle_code.size()},
+ {MAT_FRAME_SIZE - mat_end_code.size(), mat_end_code.data(), mat_end_code.size()},
+}};
+
+} // unnamed namespace
+
+CAEBitstreamPacker::CAEBitstreamPacker()
+{
+ Reset();
+}
+
+CAEBitstreamPacker::~CAEBitstreamPacker()
+{
+}
+
+void CAEBitstreamPacker::Pack(CAEStreamInfo &info, uint8_t* data, int size)
+{
+ m_pauseDuration = 0;
+ switch (info.m_type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ PackTrueHD(info, data, size);
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ PackDTSHD (info, data, size);
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ m_dataSize = CAEPackIEC61937::PackAC3(data, size, m_packedBuffer);
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ PackEAC3 (info, data, size);
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ m_dataSize = CAEPackIEC61937::PackDTS_512(data, size, m_packedBuffer, info.m_dataIsLE);
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ m_dataSize = CAEPackIEC61937::PackDTS_1024(data, size, m_packedBuffer, info.m_dataIsLE);
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ m_dataSize = CAEPackIEC61937::PackDTS_2048(data, size, m_packedBuffer, info.m_dataIsLE);
+ break;
+
+ default:
+ CLog::Log(LOGERROR, "CAEBitstreamPacker::Pack - no pack function");
+ }
+}
+
+bool CAEBitstreamPacker::PackPause(CAEStreamInfo &info, unsigned int millis, bool iecBursts)
+{
+ // re-use last buffer
+ if (m_pauseDuration == millis)
+ return false;
+
+ switch (info.m_type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ m_dataSize = CAEPackIEC61937::PackPause(m_packedBuffer, millis, GetOutputChannelMap(info).Count() * 2, GetOutputRate(info), 4, info.m_sampleRate);
+ m_pauseDuration = millis;
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ m_dataSize = CAEPackIEC61937::PackPause(m_packedBuffer, millis, GetOutputChannelMap(info).Count() * 2, GetOutputRate(info), 3, info.m_sampleRate);
+ m_pauseDuration = millis;
+ break;
+
+ default:
+ CLog::Log(LOGERROR, "CAEBitstreamPacker::Pack - no pack function");
+ }
+
+ if (!iecBursts)
+ {
+ memset(m_packedBuffer, 0, m_dataSize);
+ }
+
+ return true;
+}
+
+unsigned int CAEBitstreamPacker::GetSize() const
+{
+ return m_dataSize;
+}
+
+uint8_t* CAEBitstreamPacker::GetBuffer()
+{
+ return m_packedBuffer;
+}
+
+void CAEBitstreamPacker::Reset()
+{
+ m_dataSize = 0;
+ m_pauseDuration = 0;
+ m_packedBuffer[0] = 0;
+}
+
+/* we need to pack 24 TrueHD audio units into the unknown MAT format before packing into IEC61937 */
+void CAEBitstreamPacker::PackTrueHD(CAEStreamInfo &info, uint8_t* data, int size)
+{
+ /* create the buffer if it doesn't already exist */
+ if (m_trueHD[0].empty())
+ {
+ m_trueHD[0].resize(MAT_FRAME_SIZE);
+ m_trueHD[1].resize(MAT_FRAME_SIZE);
+ m_thd = {};
+ }
+
+ if (size < 10)
+ return;
+
+ uint8_t* pBuf = m_trueHD[m_thd.bufferIndex].data();
+ const uint8_t* pData = data;
+
+ int totalFrameSize = size;
+ int dataRem = size;
+ int paddingRem = 0;
+ int ratebits = 0;
+ int nextCodeIdx = 0;
+ uint16_t inputTiming = 0;
+ bool havePacket = false;
+
+ if (AV_RB24(data + 4) == 0xf8726f)
+ {
+ /* major sync unit, fetch sample rate */
+ if (data[7] == 0xba)
+ ratebits = data[8] >> 4;
+ else if (data[7] == 0xbb)
+ ratebits = data[9] >> 4;
+ else
+ return;
+
+ m_thd.samplesPerFrame = 40 << (ratebits & 3);
+ }
+
+ if (!m_thd.samplesPerFrame)
+ return;
+
+ inputTiming = AV_RB16(data + 2);
+
+ if (m_thd.prevFrameSize)
+ {
+ uint16_t deltaSamples = inputTiming - m_thd.prevFrameTime;
+ /*
+ * One multiple-of-48kHz frame is 1/1200 sec and the IEC 61937 rate
+ * is 768kHz = 768000*4 bytes/sec.
+ * The nominal space per frame is therefore
+ * (768000*4 bytes/sec) * (1/1200 sec) = 2560 bytes.
+ * For multiple-of-44.1kHz frames: 1/1102.5 sec, 705.6kHz, 2560 bytes.
+ *
+ * 2560 is divisible by samplesPerFrame.
+ */
+ int deltaBytes = deltaSamples * 2560 / m_thd.samplesPerFrame;
+
+ /* padding needed before this frame */
+ paddingRem = deltaBytes - m_thd.prevFrameSize;
+
+ // detects stream discontinuities
+ if (paddingRem < 0 || paddingRem >= MAT_FRAME_SIZE * 2)
+ {
+ m_thd = {}; // recovering after seek
+ return;
+ }
+ }
+
+ for (nextCodeIdx = 0; nextCodeIdx < static_cast<int>(MatCodes.size()); nextCodeIdx++)
+ if (m_thd.bufferFilled <= MatCodes[nextCodeIdx].pos)
+ break;
+
+ if (nextCodeIdx >= static_cast<int>(MatCodes.size()))
+ return;
+
+ while (paddingRem || dataRem || MatCodes[nextCodeIdx].pos == m_thd.bufferFilled)
+ {
+ if (MatCodes[nextCodeIdx].pos == m_thd.bufferFilled)
+ {
+ /* time to insert MAT code */
+ int codeLen = MatCodes[nextCodeIdx].len;
+ int codeLenRemaining = codeLen;
+ memcpy(pBuf + MatCodes[nextCodeIdx].pos, MatCodes[nextCodeIdx].code, codeLen);
+ m_thd.bufferFilled += codeLen;
+
+ nextCodeIdx++;
+ if (nextCodeIdx == static_cast<int>(MatCodes.size()))
+ {
+ nextCodeIdx = 0;
+
+ /* this was the last code, move to the next MAT frame */
+ havePacket = true;
+ m_thd.outputBuffer = pBuf;
+ m_thd.bufferIndex ^= 1;
+ pBuf = m_trueHD[m_thd.bufferIndex].data();
+ m_thd.bufferFilled = 0;
+
+ /* inter-frame gap has to be counted as well, add it */
+ codeLenRemaining += MAT_PKT_OFFSET - MAT_FRAME_SIZE;
+ }
+
+ if (paddingRem)
+ {
+ /* consider the MAT code as padding */
+ const int countedAsPadding = std::min(paddingRem, codeLenRemaining);
+ paddingRem -= countedAsPadding;
+ codeLenRemaining -= countedAsPadding;
+ }
+ /* count the remainder of the code as part of frame size */
+ if (codeLenRemaining)
+ totalFrameSize += codeLenRemaining;
+ }
+
+ if (paddingRem)
+ {
+ const int paddingLen = std::min(MatCodes[nextCodeIdx].pos - m_thd.bufferFilled, paddingRem);
+
+ memset(pBuf + m_thd.bufferFilled, 0, paddingLen);
+ m_thd.bufferFilled += paddingLen;
+ paddingRem -= paddingLen;
+
+ if (paddingRem)
+ continue; /* time to insert MAT code */
+ }
+
+ if (dataRem)
+ {
+ const int dataLen = std::min(MatCodes[nextCodeIdx].pos - m_thd.bufferFilled, dataRem);
+
+ memcpy(pBuf + m_thd.bufferFilled, pData, dataLen);
+ m_thd.bufferFilled += dataLen;
+ pData += dataLen;
+ dataRem -= dataLen;
+ }
+ }
+
+ m_thd.prevFrameSize = totalFrameSize;
+ m_thd.prevFrameTime = inputTiming;
+
+ if (!havePacket)
+ return;
+
+ m_dataSize = CAEPackIEC61937::PackTrueHD(m_thd.outputBuffer, MAT_FRAME_SIZE, m_packedBuffer);
+}
+
+void CAEBitstreamPacker::PackDTSHD(CAEStreamInfo &info, uint8_t* data, int size)
+{
+ static const uint8_t dtshd_start_code[10] = { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xfe };
+ unsigned int dataSize = sizeof(dtshd_start_code) + 2 + size;
+
+ if (dataSize > m_dtsHDSize)
+ {
+ m_dtsHDSize = dataSize;
+ m_dtsHD.resize(dataSize);
+ memcpy(m_dtsHD.data(), dtshd_start_code, sizeof(dtshd_start_code));
+ }
+
+ m_dtsHD[sizeof(dtshd_start_code) + 0] = ((uint16_t)size & 0xFF00) >> 8;
+ m_dtsHD[sizeof(dtshd_start_code) + 1] = ((uint16_t)size & 0x00FF);
+ memcpy(m_dtsHD.data() + sizeof(dtshd_start_code) + 2, data, size);
+
+ m_dataSize =
+ CAEPackIEC61937::PackDTSHD(m_dtsHD.data(), dataSize, m_packedBuffer, info.m_dtsPeriod);
+}
+
+void CAEBitstreamPacker::PackEAC3(CAEStreamInfo &info, uint8_t* data, int size)
+{
+ unsigned int framesPerBurst = info.m_repeat;
+
+ if (m_eac3FramesPerBurst != framesPerBurst)
+ {
+ /* switched streams, discard partial burst */
+ m_eac3Size = 0;
+ m_eac3FramesPerBurst = framesPerBurst;
+ }
+
+ if (m_eac3FramesPerBurst == 1)
+ {
+ /* simple case, just pass through */
+ m_dataSize = CAEPackIEC61937::PackEAC3(data, size, m_packedBuffer);
+ }
+ else
+ {
+ /* multiple frames needed to achieve 6 blocks as required by IEC 61937-3:2007 */
+
+ if (m_eac3.size() == 0)
+ m_eac3.resize(EAC3_MAX_BURST_PAYLOAD_SIZE);
+
+ unsigned int newsize = m_eac3Size + size;
+ bool overrun = newsize > EAC3_MAX_BURST_PAYLOAD_SIZE;
+
+ if (!overrun)
+ {
+ memcpy(m_eac3.data() + m_eac3Size, data, size);
+ m_eac3Size = newsize;
+ m_eac3FramesCount++;
+ }
+
+ if (m_eac3FramesCount >= m_eac3FramesPerBurst || overrun)
+ {
+ m_dataSize = CAEPackIEC61937::PackEAC3(m_eac3.data(), m_eac3Size, m_packedBuffer);
+ m_eac3Size = 0;
+ m_eac3FramesCount = 0;
+ }
+ }
+}
+
+unsigned int CAEBitstreamPacker::GetOutputRate(const CAEStreamInfo& info)
+{
+ unsigned int rate;
+ switch (info.m_type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ rate = info.m_sampleRate;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ rate = info.m_sampleRate * 4;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ if (info.m_sampleRate == 48000 ||
+ info.m_sampleRate == 96000 ||
+ info.m_sampleRate == 192000)
+ rate = 192000;
+ else
+ rate = 176400;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ rate = info.m_sampleRate;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ rate = 192000;
+ break;
+ default:
+ rate = 48000;
+ break;
+ }
+ return rate;
+}
+
+CAEChannelInfo CAEBitstreamPacker::GetOutputChannelMap(const CAEStreamInfo& info)
+{
+ int channels = 2;
+ switch (info.m_type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ channels = 2;
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ channels = 8;
+ break;
+
+ default:
+ break;
+ }
+
+ CAEChannelInfo channelMap;
+ for (int i=0; i<channels; i++)
+ {
+ channelMap += AE_CH_RAW;
+ }
+
+ return channelMap;
+}
diff --git a/xbmc/cores/AudioEngine/Utils/AEBitstreamPacker.h b/xbmc/cores/AudioEngine/Utils/AEBitstreamPacker.h
new file mode 100644
index 0000000..1497c03
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEBitstreamPacker.h
@@ -0,0 +1,66 @@
+/*
+ * 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 "AEChannelInfo.h"
+#include "AEPackIEC61937.h"
+
+#include <list>
+#include <stdint.h>
+#include <vector>
+
+class CAEStreamInfo;
+
+class CAEBitstreamPacker
+{
+public:
+ CAEBitstreamPacker();
+ ~CAEBitstreamPacker();
+
+ void Pack(CAEStreamInfo &info, uint8_t* data, int size);
+ bool PackPause(CAEStreamInfo &info, unsigned int millis, bool iecBursts);
+ void Reset();
+ uint8_t* GetBuffer();
+ unsigned int GetSize() const;
+ static unsigned int GetOutputRate(const CAEStreamInfo& info);
+ static CAEChannelInfo GetOutputChannelMap(const CAEStreamInfo& info);
+
+private:
+ void PackTrueHD(CAEStreamInfo &info, uint8_t* data, int size);
+ void PackDTSHD(CAEStreamInfo &info, uint8_t* data, int size);
+ void PackEAC3(CAEStreamInfo &info, uint8_t* data, int size);
+
+ /* we keep the trueHD and dtsHD buffers separate so that we can handle a fast stream switch */
+ std::vector<uint8_t> m_trueHD[2];
+
+ struct TrueHD
+ {
+ int prevFrameSize;
+ int samplesPerFrame;
+ int bufferFilled;
+ int bufferIndex;
+ uint16_t prevFrameTime;
+ uint8_t* outputBuffer;
+ };
+
+ TrueHD m_thd{};
+
+ std::vector<uint8_t> m_dtsHD;
+ unsigned int m_dtsHDSize = 0;
+
+ std::vector<uint8_t> m_eac3;
+ unsigned int m_eac3Size = 0;
+ unsigned int m_eac3FramesCount = 0;
+ unsigned int m_eac3FramesPerBurst = 0;
+
+ unsigned int m_dataSize = 0;
+ uint8_t m_packedBuffer[MAX_IEC61937_PACKET];
+ unsigned int m_pauseDuration = 0;
+};
+
diff --git a/xbmc/cores/AudioEngine/Utils/AEChannelData.h b/xbmc/cores/AudioEngine/Utils/AEChannelData.h
new file mode 100644
index 0000000..71010bb
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEChannelData.h
@@ -0,0 +1,111 @@
+/*
+ * 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
+
+/**
+ * The possible channels
+ */
+enum AEChannel
+{
+ AE_CH_NULL = -1,
+ AE_CH_RAW ,
+
+ AE_CH_FL , AE_CH_FR , AE_CH_FC , AE_CH_LFE, AE_CH_BL , AE_CH_BR , AE_CH_FLOC,
+ AE_CH_FROC, AE_CH_BC , AE_CH_SL , AE_CH_SR , AE_CH_TFL , AE_CH_TFR , AE_CH_TFC ,
+ AE_CH_TC , AE_CH_TBL, AE_CH_TBR, AE_CH_TBC, AE_CH_BLOC, AE_CH_BROC,
+
+ /* p16v devices */
+ AE_CH_UNKNOWN1 , AE_CH_UNKNOWN2 , AE_CH_UNKNOWN3 , AE_CH_UNKNOWN4 ,
+ AE_CH_UNKNOWN5 , AE_CH_UNKNOWN6 , AE_CH_UNKNOWN7 , AE_CH_UNKNOWN8 ,
+ AE_CH_UNKNOWN9 , AE_CH_UNKNOWN10, AE_CH_UNKNOWN11, AE_CH_UNKNOWN12,
+ AE_CH_UNKNOWN13, AE_CH_UNKNOWN14, AE_CH_UNKNOWN15, AE_CH_UNKNOWN16,
+ AE_CH_UNKNOWN17, AE_CH_UNKNOWN18, AE_CH_UNKNOWN19, AE_CH_UNKNOWN20,
+ AE_CH_UNKNOWN21, AE_CH_UNKNOWN22, AE_CH_UNKNOWN23, AE_CH_UNKNOWN24,
+ AE_CH_UNKNOWN25, AE_CH_UNKNOWN26, AE_CH_UNKNOWN27, AE_CH_UNKNOWN28,
+ AE_CH_UNKNOWN29, AE_CH_UNKNOWN30, AE_CH_UNKNOWN31, AE_CH_UNKNOWN32,
+ AE_CH_UNKNOWN33, AE_CH_UNKNOWN34, AE_CH_UNKNOWN35, AE_CH_UNKNOWN36,
+ AE_CH_UNKNOWN37, AE_CH_UNKNOWN38, AE_CH_UNKNOWN39, AE_CH_UNKNOWN40,
+ AE_CH_UNKNOWN41, AE_CH_UNKNOWN42, AE_CH_UNKNOWN43, AE_CH_UNKNOWN44,
+ AE_CH_UNKNOWN45, AE_CH_UNKNOWN46, AE_CH_UNKNOWN47, AE_CH_UNKNOWN48,
+ AE_CH_UNKNOWN49, AE_CH_UNKNOWN50, AE_CH_UNKNOWN51, AE_CH_UNKNOWN52,
+ AE_CH_UNKNOWN53, AE_CH_UNKNOWN54, AE_CH_UNKNOWN55, AE_CH_UNKNOWN56,
+ AE_CH_UNKNOWN57, AE_CH_UNKNOWN58, AE_CH_UNKNOWN59, AE_CH_UNKNOWN60,
+ AE_CH_UNKNOWN61, AE_CH_UNKNOWN62, AE_CH_UNKNOWN63, AE_CH_UNKNOWN64,
+
+ AE_CH_MAX
+};
+
+/**
+ * Standard channel layouts
+ */
+enum AEStdChLayout
+{
+ AE_CH_LAYOUT_INVALID = -1,
+
+ AE_CH_LAYOUT_1_0 = 0,
+ AE_CH_LAYOUT_2_0,
+ AE_CH_LAYOUT_2_1,
+ AE_CH_LAYOUT_3_0,
+ AE_CH_LAYOUT_3_1,
+ AE_CH_LAYOUT_4_0,
+ AE_CH_LAYOUT_4_1,
+ AE_CH_LAYOUT_5_0,
+ AE_CH_LAYOUT_5_1,
+ AE_CH_LAYOUT_7_0,
+ AE_CH_LAYOUT_7_1,
+
+ AE_CH_LAYOUT_MAX
+};
+
+/**
+ * The various data formats
+ * LE = Little Endian, BE = Big Endian, NE = Native Endian
+ * @note This is ordered from the worst to best preferred formats
+ */
+enum AEDataFormat
+{
+ AE_FMT_INVALID = -1,
+
+ AE_FMT_U8,
+
+ AE_FMT_S16BE,
+ AE_FMT_S16LE,
+ AE_FMT_S16NE,
+
+ AE_FMT_S32BE,
+ AE_FMT_S32LE,
+ AE_FMT_S32NE,
+
+ AE_FMT_S24BE4,
+ AE_FMT_S24LE4,
+ AE_FMT_S24NE4, // 24 bits in lower 3 bytes
+ AE_FMT_S24NE4MSB, // S32 with bits_per_sample < 32
+
+ AE_FMT_S24BE3,
+ AE_FMT_S24LE3,
+ AE_FMT_S24NE3, /* S24 in 3 bytes */
+
+ AE_FMT_DOUBLE,
+ AE_FMT_FLOAT,
+
+ // Bitstream
+ AE_FMT_RAW,
+
+ /* planar formats */
+ AE_FMT_U8P,
+ AE_FMT_S16NEP,
+ AE_FMT_S32NEP,
+ AE_FMT_S24NE4P,
+ AE_FMT_S24NE4MSBP,
+ AE_FMT_S24NE3P,
+ AE_FMT_DOUBLEP,
+ AE_FMT_FLOATP,
+
+ AE_FMT_MAX
+};
diff --git a/xbmc/cores/AudioEngine/Utils/AEChannelInfo.cpp b/xbmc/cores/AudioEngine/Utils/AEChannelInfo.cpp
new file mode 100644
index 0000000..e3d74ca
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEChannelInfo.cpp
@@ -0,0 +1,385 @@
+/*
+ * 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 "AEChannelInfo.h"
+
+#include <algorithm>
+#include <assert.h>
+#include <limits>
+#include <string.h>
+
+CAEChannelInfo::CAEChannelInfo()
+{
+ Reset();
+}
+
+CAEChannelInfo::CAEChannelInfo(const enum AEChannel* rhs)
+{
+ *this = rhs;
+}
+
+CAEChannelInfo::CAEChannelInfo(const AEStdChLayout rhs)
+{
+ *this = rhs;
+}
+
+void CAEChannelInfo::ResolveChannels(const CAEChannelInfo& rhs)
+{
+ /* mono gets upmixed to dual mono */
+ if (m_channelCount == 1 && m_channels[0] == AE_CH_FC)
+ {
+ Reset();
+ *this += AE_CH_FL;
+ *this += AE_CH_FR;
+ return;
+ }
+
+ bool srcHasSL = false;
+ bool srcHasSR = false;
+ bool srcHasRL = false;
+ bool srcHasRR = false;
+ bool srcHasBC = false;
+
+ bool dstHasSL = false;
+ bool dstHasSR = false;
+ bool dstHasRL = false;
+ bool dstHasRR = false;
+ bool dstHasBC = false;
+
+ for (unsigned int c = 0; c < rhs.m_channelCount; ++c)
+ switch(rhs.m_channels[c])
+ {
+ case AE_CH_SL: dstHasSL = true; break;
+ case AE_CH_SR: dstHasSR = true; break;
+ case AE_CH_BL: dstHasRL = true; break;
+ case AE_CH_BR: dstHasRR = true; break;
+ case AE_CH_BC: dstHasBC = true; break;
+ default:
+ break;
+ }
+
+ CAEChannelInfo newInfo;
+ for (unsigned int i = 0; i < m_channelCount; ++i)
+ {
+ switch (m_channels[i])
+ {
+ case AE_CH_SL: srcHasSL = true; break;
+ case AE_CH_SR: srcHasSR = true; break;
+ case AE_CH_BL: srcHasRL = true; break;
+ case AE_CH_BR: srcHasRR = true; break;
+ case AE_CH_BC: srcHasBC = true; break;
+ default:
+ break;
+ }
+
+ bool found = false;
+ for (unsigned int c = 0; c < rhs.m_channelCount; ++c)
+ if (m_channels[i] == rhs.m_channels[c])
+ {
+ found = true;
+ break;
+ }
+
+ if (found)
+ newInfo += m_channels[i];
+ }
+
+ /* we need to ensure we end up with rear or side channels for downmix to work */
+ if (srcHasSL && !dstHasSL && dstHasRL && !newInfo.HasChannel(AE_CH_BL))
+ newInfo += AE_CH_BL;
+ if (srcHasSR && !dstHasSR && dstHasRR && !newInfo.HasChannel(AE_CH_BR))
+ newInfo += AE_CH_BR;
+ if (srcHasRL && !dstHasRL && dstHasSL && !newInfo.HasChannel(AE_CH_SL))
+ newInfo += AE_CH_SL;
+ if (srcHasRR && !dstHasRR && dstHasSR && !newInfo.HasChannel(AE_CH_SR))
+ newInfo += AE_CH_SR;
+
+ // mix back center if not available in destination layout
+ // prefer mixing into backs if available
+ if (srcHasBC && !dstHasBC)
+ {
+ if (dstHasRL && !newInfo.HasChannel(AE_CH_BL))
+ newInfo += AE_CH_BL;
+ else if (dstHasSL && !newInfo.HasChannel(AE_CH_SL))
+ newInfo += AE_CH_SL;
+
+ if (dstHasRR && !newInfo.HasChannel(AE_CH_BR))
+ newInfo += AE_CH_BR;
+ else if (dstHasSR && !newInfo.HasChannel(AE_CH_SR))
+ newInfo += AE_CH_SR;
+ }
+
+ *this = newInfo;
+}
+
+void CAEChannelInfo::Reset()
+{
+ m_channelCount = 0;
+ for (AEChannel& channel : m_channels)
+ channel = AE_CH_NULL;
+}
+
+CAEChannelInfo& CAEChannelInfo::operator=(const CAEChannelInfo& rhs)
+{
+ if (this == &rhs)
+ return *this;
+
+ /* clone the information */
+ m_channelCount = rhs.m_channelCount;
+ memcpy(m_channels, rhs.m_channels, sizeof(enum AEChannel) * rhs.m_channelCount);
+
+ return *this;
+}
+
+CAEChannelInfo& CAEChannelInfo::operator=(const enum AEChannel* rhs)
+{
+ Reset();
+ if (rhs == NULL)
+ return *this;
+
+ while (m_channelCount < AE_CH_MAX && rhs[m_channelCount] != AE_CH_NULL)
+ {
+ m_channels[m_channelCount] = rhs[m_channelCount];
+ ++m_channelCount;
+ }
+
+ /* the last entry should be NULL, if not we were passed a non null terminated list */
+ assert(rhs[m_channelCount] == AE_CH_NULL);
+
+ return *this;
+}
+
+CAEChannelInfo& CAEChannelInfo::operator=(const enum AEStdChLayout rhs)
+{
+ assert(rhs > AE_CH_LAYOUT_INVALID && rhs < AE_CH_LAYOUT_MAX);
+
+ static constexpr enum AEChannel layouts[AE_CH_LAYOUT_MAX][9] = {
+ {AE_CH_FC, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_LFE, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_BL, AE_CH_BR, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_BL, AE_CH_BR, AE_CH_LFE, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BL, AE_CH_BR, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_BL, AE_CH_BR, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_BL, AE_CH_BR, AE_CH_SL, AE_CH_SR, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_BL, AE_CH_BR, AE_CH_SL, AE_CH_SR,
+ AE_CH_NULL}};
+
+ *this = layouts[rhs];
+ return *this;
+}
+
+bool CAEChannelInfo::operator==(const CAEChannelInfo& rhs) const
+{
+ /* if the channel count doesn't match, no need to check further */
+ if (m_channelCount != rhs.m_channelCount)
+ return false;
+
+ /* make sure the channel order is the same */
+ for (unsigned int i = 0; i < m_channelCount; ++i)
+ if (m_channels[i] != rhs.m_channels[i])
+ return false;
+
+ return true;
+}
+
+bool CAEChannelInfo::operator!=(const CAEChannelInfo& rhs) const
+{
+ return !(*this == rhs);
+}
+
+CAEChannelInfo& CAEChannelInfo::operator+=(const enum AEChannel& rhs)
+{
+ assert(m_channelCount < AE_CH_MAX);
+ assert(rhs > AE_CH_NULL && rhs < AE_CH_MAX);
+
+ m_channels[m_channelCount++] = rhs;
+ return *this;
+}
+
+CAEChannelInfo& CAEChannelInfo::operator-=(const enum AEChannel& rhs)
+{
+ assert(rhs > AE_CH_NULL && rhs < AE_CH_MAX);
+
+ unsigned int i = 0;
+ while(i < m_channelCount && m_channels[i] != rhs)
+ i++;
+ if (i >= m_channelCount)
+ return *this; // Channel not found
+
+ for(; i < m_channelCount-1; i++)
+ m_channels[i] = m_channels[i+1];
+
+ m_channels[i] = AE_CH_NULL;
+ m_channelCount--;
+ return *this;
+}
+
+enum AEChannel CAEChannelInfo::operator[](unsigned int i) const
+{
+ assert(i < m_channelCount);
+ return m_channels[i];
+}
+
+CAEChannelInfo::operator std::string() const
+{
+ if (m_channelCount == 0)
+ return "NULL";
+
+ std::string s;
+ for (unsigned int i = 0; i < m_channelCount - 1; ++i)
+ {
+ s.append(GetChName(m_channels[i]));
+ s.append(", ");
+ }
+ s.append(GetChName(m_channels[m_channelCount-1]));
+
+ return s;
+}
+
+bool CAEChannelInfo::IsChannelValid(const unsigned int pos)
+{
+ assert(pos < m_channelCount);
+ bool isValid = false;
+ if (m_channels[pos] > AE_CH_NULL && m_channels[pos] < AE_CH_UNKNOWN1)
+ isValid = true;
+
+ return isValid;
+}
+
+bool CAEChannelInfo::IsLayoutValid()
+{
+ if (m_channelCount == 0)
+ return false;
+
+ for (unsigned int i = 0; i < m_channelCount; ++i)
+ {
+ // we need at least one valid channel
+ if (IsChannelValid(i))
+ return true;
+ }
+ return false;
+}
+
+const char* CAEChannelInfo::GetChName(const enum AEChannel ch)
+{
+ assert(ch >= 0 && ch < AE_CH_MAX);
+
+ static const char* channels[AE_CH_MAX] =
+ {
+ "RAW" ,
+ "FL" , "FR" , "FC" , "LFE", "BL" , "BR" , "FLOC",
+ "FROC", "BC" , "SL" , "SR" , "TFL" , "TFR" , "TFC" ,
+ "TC" , "TBL", "TBR", "TBC", "BLOC", "BROC",
+
+ /* p16v devices */
+ "UNKNOWN1" , "UNKNOWN2" , "UNKNOWN3" , "UNKNOWN4" ,
+ "UNKNOWN5" , "UNKNOWN6" , "UNKNOWN7" , "UNKNOWN8" ,
+ "UNKNOWN9" , "UNKNOWN10", "UNKNOWN11", "UNKNOWN12",
+ "UNKNOWN13", "UNKNOWN14", "UNKNOWN15", "UNKNOWN16",
+ "UNKNOWN17", "UNKNOWN18", "UNKNOWN19", "UNKNOWN20",
+ "UNKNOWN21", "UNKNOWN22", "UNKNOWN23", "UNKNOWN24",
+ "UNKNOWN25", "UNKNOWN26", "UNKNOWN27", "UNKNOWN28",
+ "UNKNOWN29", "UNKNOWN30", "UNKNOWN31", "UNKNOWN32",
+ "UNKNOWN33", "UNKNOWN34", "UNKNOWN35", "UNKNOWN36",
+ "UNKNOWN37", "UNKNOWN38", "UNKNOWN39", "UNKNOWN40",
+ "UNKNOWN41", "UNKNOWN42", "UNKNOWN43", "UNKNOWN44",
+ "UNKNOWN45", "UNKNOWN46", "UNKNOWN47", "UNKNOWN48",
+ "UNKNOWN49", "UNKNOWN50", "UNKNOWN51", "UNKNOWN52",
+ "UNKNOWN53", "UNKNOWN54", "UNKNOWN55", "UNKNOWN56",
+ "UNKNOWN57", "UNKNOWN58", "UNKNOWN59", "UNKNOWN60",
+ "UNKNOWN61", "UNKNOWN62", "UNKNOWN63", "UNKNOWN64"
+ };
+
+ return channels[ch];
+}
+
+bool CAEChannelInfo::HasChannel(const enum AEChannel ch) const
+{
+ for (unsigned int i = 0; i < m_channelCount; ++i)
+ if (m_channels[i] == ch)
+ return true;
+ return false;
+}
+
+bool CAEChannelInfo::ContainsChannels(const CAEChannelInfo& rhs) const
+{
+ for (unsigned int i = 0; i < rhs.m_channelCount; ++i)
+ {
+ if (!HasChannel(rhs.m_channels[i]))
+ return false;
+ }
+ return true;
+}
+
+void CAEChannelInfo::ReplaceChannel(const enum AEChannel from, const enum AEChannel to)
+{
+ for (unsigned int i = 0; i < m_channelCount; ++i)
+ {
+ if (m_channels[i] == from)
+ {
+ m_channels[i] = to;
+ break;
+ }
+ }
+}
+
+int CAEChannelInfo::BestMatch(const std::vector<CAEChannelInfo>& dsts, int* score) const
+{
+ CAEChannelInfo availableDstChannels;
+ for (unsigned int i = 0; i < dsts.size(); i++)
+ availableDstChannels.AddMissingChannels(dsts[i]);
+
+ /* if we have channels not existing in any destination layout but that
+ * are remappable (e.g. RC => RL+RR), do those remaps */
+ CAEChannelInfo src(*this);
+ src.ResolveChannels(availableDstChannels);
+
+ bool remapped = (src != *this);
+ /* good enough approximation (does not account for added channels) */
+ int dropped = std::max((int)src.Count() - (int)Count(), 0);
+
+ int bestScore = std::numeric_limits<int>::min();
+ int bestMatch = -1;
+
+ for (unsigned int i = 0; i < dsts.size(); i++)
+ {
+ const CAEChannelInfo& dst = dsts[i];
+ int okChannels = 0;
+
+ for (unsigned int j = 0; j < src.Count(); j++)
+ okChannels += dst.HasChannel(src[j]);
+
+ int missingChannels = src.Count() - okChannels;
+ int extraChannels = dst.Count() - okChannels;
+
+ int curScore = 0 - (missingChannels + dropped) * 1000 - extraChannels * 10 - (remapped ? 1 : 0);
+
+ if (curScore > bestScore)
+ {
+ bestScore = curScore;
+ bestMatch = i;
+ if (curScore == 0)
+ break;
+ }
+ }
+
+ if (score)
+ *score = bestScore;
+
+ return bestMatch;
+}
+
+void CAEChannelInfo::AddMissingChannels(const CAEChannelInfo& rhs)
+{
+ for (unsigned int i = 0; i < rhs.Count(); i++)
+ if (!HasChannel(rhs[i]))
+ *this += rhs[i];
+}
diff --git a/xbmc/cores/AudioEngine/Utils/AEChannelInfo.h b/xbmc/cores/AudioEngine/Utils/AEChannelInfo.h
new file mode 100644
index 0000000..1b59e5f
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEChannelInfo.h
@@ -0,0 +1,55 @@
+/*
+ * 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 "AEChannelData.h"
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+class CHelper_libKODI_audioengine;
+
+class CAEChannelInfo {
+ friend class CHelper_libKODI_audioengine;
+
+public:
+ CAEChannelInfo();
+ explicit CAEChannelInfo(const enum AEChannel* rhs);
+ CAEChannelInfo(const enum AEStdChLayout rhs);
+ ~CAEChannelInfo() = default;
+ CAEChannelInfo(const CAEChannelInfo&) = default;
+ CAEChannelInfo& operator=(const CAEChannelInfo& rhs);
+ CAEChannelInfo& operator=(const enum AEChannel* rhs);
+ CAEChannelInfo& operator=(const enum AEStdChLayout rhs);
+ bool operator==(const CAEChannelInfo& rhs) const;
+ bool operator!=(const CAEChannelInfo& rhs) const;
+ CAEChannelInfo& operator+=(const enum AEChannel& rhs);
+ CAEChannelInfo& operator-=(const enum AEChannel& rhs);
+ enum AEChannel operator[](unsigned int i) const;
+ operator std::string() const;
+
+ /* remove any channels that dont exist in the provided info */
+ void ResolveChannels(const CAEChannelInfo& rhs);
+ void Reset();
+ inline unsigned int Count() const { return m_channelCount; }
+ static const char* GetChName(const enum AEChannel ch);
+ bool HasChannel(const enum AEChannel ch) const;
+ bool IsChannelValid(const unsigned int pos);
+ bool IsLayoutValid();
+ bool ContainsChannels(const CAEChannelInfo& rhs) const;
+ void ReplaceChannel(const enum AEChannel from, const enum AEChannel to);
+ int BestMatch(const std::vector<CAEChannelInfo>& dsts, int* score = NULL) const;
+ void AddMissingChannels(const CAEChannelInfo& rhs);
+
+private:
+ unsigned int m_channelCount;
+ enum AEChannel m_channels[AE_CH_MAX];
+};
+
diff --git a/xbmc/cores/AudioEngine/Utils/AEDeviceInfo.cpp b/xbmc/cores/AudioEngine/Utils/AEDeviceInfo.cpp
new file mode 100644
index 0000000..473a849
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEDeviceInfo.cpp
@@ -0,0 +1,66 @@
+/*
+ * 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 "AEDeviceInfo.h"
+
+#include "AEUtil.h"
+
+#include <sstream>
+
+CAEDeviceInfo::operator std::string()
+{
+ std::stringstream ss;
+ ss << "m_deviceName : " << m_deviceName << '\n';
+ ss << "m_displayName : " << m_displayName << '\n';
+ ss << "m_displayNameExtra: " << m_displayNameExtra << '\n';
+ ss << "m_deviceType : " << DeviceTypeToString(m_deviceType) + '\n';
+ ss << "m_channels : " << (std::string)m_channels << '\n';
+
+ ss << "m_sampleRates : ";
+ for (AESampleRateList::iterator itt = m_sampleRates.begin(); itt != m_sampleRates.end(); ++itt)
+ {
+ if (itt != m_sampleRates.begin())
+ ss << ',';
+ ss << *itt;
+ }
+ ss << '\n';
+
+ ss << "m_dataFormats : ";
+ for (AEDataFormatList::iterator itt = m_dataFormats.begin(); itt != m_dataFormats.end(); ++itt)
+ {
+ if (itt != m_dataFormats.begin())
+ ss << ',';
+ ss << CAEUtil::DataFormatToStr(*itt);
+ }
+ ss << '\n';
+
+ ss << "m_streamTypes : ";
+ for (AEDataTypeList::iterator itt = m_streamTypes.begin(); itt != m_streamTypes.end(); ++itt)
+ {
+ if (itt != m_streamTypes.begin())
+ ss << ',';
+ ss << CAEUtil::StreamTypeToStr(*itt);
+ }
+ if (m_streamTypes.empty())
+ ss << "No passthrough capabilities";
+ ss << '\n';
+
+ return ss.str();
+}
+
+std::string CAEDeviceInfo::DeviceTypeToString(enum AEDeviceType deviceType)
+{
+ switch (deviceType)
+ {
+ case AE_DEVTYPE_PCM : return "AE_DEVTYPE_PCM" ; break;
+ case AE_DEVTYPE_IEC958: return "AE_DEVTYPE_IEC958"; break;
+ case AE_DEVTYPE_HDMI : return "AE_DEVTYPE_HDMI" ; break;
+ case AE_DEVTYPE_DP : return "AE_DEVTYPE_DP" ; break;
+ }
+ return "INVALID";
+}
diff --git a/xbmc/cores/AudioEngine/Utils/AEDeviceInfo.h b/xbmc/cores/AudioEngine/Utils/AEDeviceInfo.h
new file mode 100644
index 0000000..387d617
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEDeviceInfo.h
@@ -0,0 +1,52 @@
+/*
+ * 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 "cores/AudioEngine/Utils/AEChannelInfo.h"
+#include "cores/AudioEngine/Utils/AEStreamInfo.h"
+
+#include <string>
+#include <vector>
+
+typedef std::vector<unsigned int> AESampleRateList;
+typedef std::vector<enum AEDataFormat> AEDataFormatList;
+typedef std::vector<CAEStreamInfo::DataType> AEDataTypeList;
+
+enum AEDeviceType {
+ AE_DEVTYPE_PCM,
+ AE_DEVTYPE_IEC958,
+ AE_DEVTYPE_HDMI,
+ AE_DEVTYPE_DP
+};
+
+/**
+ * This classt provides the details of what the audio output hardware is capable of
+ */
+class CAEDeviceInfo
+{
+public:
+ std::string m_deviceName; /* the driver device name */
+ std::string m_displayName; /* the friendly display name */
+ std::string m_displayNameExtra; /* additional display name info, ie, monitor name from ELD */
+ enum AEDeviceType m_deviceType; /* the device type, PCM, IEC958 or HDMI */
+ CAEChannelInfo m_channels; /* the channels the device is capable of rendering */
+ AESampleRateList m_sampleRates; /* the samplerates the device is capable of rendering */
+ AEDataFormatList m_dataFormats; /* the dataformats the device is capable of rendering */
+ AEDataTypeList m_streamTypes;
+
+ bool m_wantsIECPassthrough; /* if sink supports passthrough encapsulation is done when set to true */
+
+ bool m_onlyPassthrough{false}; // sink only only should be used for passthrough (audio PT device)
+ bool m_onlyPCM{false}; // sink only should be used for PCM (audio device)
+
+ operator std::string();
+ static std::string DeviceTypeToString(enum AEDeviceType deviceType);
+};
+
+typedef std::vector<CAEDeviceInfo> AEDeviceInfoList;
diff --git a/xbmc/cores/AudioEngine/Utils/AEELDParser.cpp b/xbmc/cores/AudioEngine/Utils/AEELDParser.cpp
new file mode 100644
index 0000000..a9a4cf1
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEELDParser.cpp
@@ -0,0 +1,198 @@
+/*
+ * 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 "AEELDParser.h"
+
+#include "AEDeviceInfo.h"
+#include "utils/EndianSwap.h"
+
+#include <algorithm>
+#include <functional>
+#include <stdio.h>
+#include <string.h>
+
+#define GRAB_BITS(buf, byte, lowbit, bits) ((buf[byte] >> (lowbit)) & ((1 << (bits)) - 1))
+
+typedef struct
+{
+ uint8_t eld_ver;
+ uint8_t baseline_eid_len;
+ uint8_t cea_edid_ver;
+ uint8_t monitor_name_length;
+ uint8_t sad_count;
+ uint8_t conn_type;
+ bool s_ai;
+ bool hdcp;
+ uint8_t audio_sync_delay;
+ bool rlrc; /* rear left and right of center */
+ bool flrc; /* front left and right of center */
+ bool rc; /* rear center */
+ bool rlr; /* rear left and right */
+ bool fc; /* front center */
+ bool lfe; /* LFE */
+ bool flr; /* front left and right */
+ uint64_t port_id;
+ char mfg_name[4];
+ uint16_t product_code;
+ std::string monitor_name;
+} ELDHeader;
+
+#define ELD_VER_CEA_816D 2
+#define ELD_VER_PARTIAL 31
+
+#define ELD_EDID_VER_NONE 0
+#define ELD_EDID_VER_CEA_861 1
+#define ELD_EDID_VER_CEA_861_A 2
+#define ELD_EDID_VER_CEA_861_BCD 3
+
+#define ELD_CONN_TYPE_HDMI 0
+#define ELD_CONN_TYPE_DP 1
+#define ELD_CONN_TYPE_RESERVED1 2
+#define ELD_CONN_TYPE_RESERVED2 3
+
+#define CEA_861_FORMAT_RESERVED1 0
+#define CEA_861_FORMAT_LPCM 1
+#define CEA_861_FORMAT_AC3 2
+#define CEA_861_FORMAT_MPEG1 3
+#define CEA_861_FORMAT_MP3 4
+#define CEA_861_FORMAT_MPEG2 5
+#define CEA_861_FORMAT_AAC 6
+#define CEA_861_FORMAT_DTS 7
+#define CEA_861_FORMAT_ATRAC 8
+#define CEA_861_FORMAT_SACD 9
+#define CEA_861_FORMAT_EAC3 10
+#define CEA_861_FORMAT_DTSHD 11
+#define CEA_861_FORMAT_MLP 12
+#define CEA_861_FORMAT_DST 13
+#define CEA_861_FORMAT_WMAPRO 14
+#define CEA_861_FORMAT_RESERVED2 15
+
+#define rtrim(s) s.erase(std::find_if(s.rbegin(), s.rend(), std::not1(std::ptr_fun<int, int>(std::isspace))).base(), s.end())
+
+void CAEELDParser::Parse(const uint8_t *data, size_t length, CAEDeviceInfo& info)
+{
+ ELDHeader header;
+ header.eld_ver = (data[0 ] & 0xF8) >> 3;
+ if (header.eld_ver != ELD_VER_CEA_816D && header.eld_ver != ELD_VER_PARTIAL)
+ return;
+
+ header.baseline_eid_len = data[2 ];
+ header.cea_edid_ver = (data[4 ] & 0xE0) >> 5;
+ header.monitor_name_length = data[4 ] & 0x1F;
+ header.sad_count = (data[5 ] & 0xF0) >> 4;
+ header.conn_type = (data[5 ] & 0x0C) >> 2;
+ header.s_ai = (data[5 ] & 0x02) == 0x02;
+ header.hdcp = (data[5 ] & 0x01) == 0x01;
+ header.audio_sync_delay = data[6 ];
+ header.rlrc = (data[7 ] & 0x40) == 0x40;
+ header.flrc = (data[7 ] & 0x20) == 0x20;
+ header.rc = (data[7 ] & 0x10) == 0x10;
+ header.rlr = (data[7 ] & 0x08) == 0x08;
+ header.fc = (data[7 ] & 0x04) == 0x04;
+ header.lfe = (data[7 ] & 0x02) == 0x02;
+ header.flr = (data[7 ] & 0x01) == 0x01;
+ header.port_id = Endian_SwapLE64(*((const uint64_t*)(data + 8)));
+ header.mfg_name[0] = 'A' + ((data[16] >> 2) & 0x1F) - 1;
+ header.mfg_name[1] = 'A' + (((data[16] << 3) | (data[17] >> 5)) & 0x1F) - 1;
+ header.mfg_name[2] = 'A' + (data[17] & 0x1F) - 1;
+ header.mfg_name[3] = '\0';
+ header.product_code = Endian_SwapLE16(*((const uint16_t*)(data + 18)));
+
+ switch (header.conn_type)
+ {
+ case ELD_CONN_TYPE_HDMI: info.m_deviceType = AE_DEVTYPE_HDMI; break;
+ case ELD_CONN_TYPE_DP : info.m_deviceType = AE_DEVTYPE_DP ; break;
+ }
+
+ info.m_displayNameExtra = header.mfg_name;
+ if (header.monitor_name_length <= 16)
+ {
+ header.monitor_name.assign((const char *)(data + 20), header.monitor_name_length);
+ rtrim(header.monitor_name);
+ if (header.monitor_name.length() > 0)
+ {
+ info.m_displayNameExtra.append(" ");
+ info.m_displayNameExtra.append(header.monitor_name);
+ if (header.conn_type == ELD_CONN_TYPE_HDMI)
+ info.m_displayNameExtra.append(" on HDMI" );
+ else
+ info.m_displayNameExtra.append(" on DisplayPort");
+ }
+ }
+
+ if (header.flr)
+ {
+ if (!info.m_channels.HasChannel(AE_CH_FL))
+ info.m_channels += AE_CH_FL;
+ if (!info.m_channels.HasChannel(AE_CH_FR))
+ info.m_channels += AE_CH_FR;
+ }
+
+ if (header.lfe)
+ if (!info.m_channels.HasChannel(AE_CH_LFE))
+ info.m_channels += AE_CH_LFE;
+
+ if (header.fc)
+ if (!info.m_channels.HasChannel(AE_CH_FC))
+ info.m_channels += AE_CH_FC;
+
+ if (header.rlr)
+ {
+ if (!info.m_channels.HasChannel(AE_CH_BL))
+ info.m_channels += AE_CH_BL;
+ if (!info.m_channels.HasChannel(AE_CH_BR))
+ info.m_channels += AE_CH_BR;
+ }
+
+ if (header.rc)
+ if (!info.m_channels.HasChannel(AE_CH_BC))
+ info.m_channels += AE_CH_BC;
+
+ if (header.flrc)
+ {
+ if (!info.m_channels.HasChannel(AE_CH_FLOC))
+ info.m_channels += AE_CH_FLOC;
+ if (!info.m_channels.HasChannel(AE_CH_FROC))
+ info.m_channels += AE_CH_FROC;
+ }
+
+ if (header.rlrc)
+ {
+ if (!info.m_channels.HasChannel(AE_CH_BLOC))
+ info.m_channels += AE_CH_BLOC;
+ if (!info.m_channels.HasChannel(AE_CH_BROC))
+ info.m_channels += AE_CH_BROC;
+ }
+
+ const uint8_t *sad = data + 20 + header.monitor_name_length;
+ for(uint8_t i = 0; i < header.sad_count; ++i)
+ {
+ uint8_t offset = i * 3;
+ uint8_t formatCode = (sad[offset + 0] >> 3) & 0xF;
+ //uint8_t channelCount = (sad[offset + 0] & 0x7) + 1;
+ //uint8_t sampleRates = sad[offset + 1];
+
+ AEDataFormat fmt = AE_FMT_INVALID;
+ switch (formatCode)
+ {
+ //! @todo implement
+ case CEA_861_FORMAT_AC3 : fmt = AE_FMT_RAW ; break;
+ case CEA_861_FORMAT_DTS : fmt = AE_FMT_RAW ; break;
+ case CEA_861_FORMAT_DTSHD: fmt = AE_FMT_RAW ; break;
+ case CEA_861_FORMAT_EAC3 : fmt = AE_FMT_RAW ; break;
+ case CEA_861_FORMAT_LPCM : fmt = AE_FMT_RAW ; break;
+ case CEA_861_FORMAT_MLP : fmt = AE_FMT_RAW; break;
+ }
+
+ if (fmt == AE_FMT_INVALID)
+ continue;
+
+ if (std::find(info.m_dataFormats.begin(), info.m_dataFormats.end(), fmt) == info.m_dataFormats.end())
+ info.m_dataFormats.push_back(fmt);
+ }
+}
diff --git a/xbmc/cores/AudioEngine/Utils/AEELDParser.h b/xbmc/cores/AudioEngine/Utils/AEELDParser.h
new file mode 100644
index 0000000..1e8bfb1
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEELDParser.h
@@ -0,0 +1,20 @@
+/*
+ * 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 <cstring>
+#include <stdint.h>
+
+class CAEDeviceInfo;
+
+class CAEELDParser {
+public:
+ static void Parse(const uint8_t *data, size_t length, CAEDeviceInfo& info);
+};
+
diff --git a/xbmc/cores/AudioEngine/Utils/AELimiter.cpp b/xbmc/cores/AudioEngine/Utils/AELimiter.cpp
new file mode 100644
index 0000000..393d1d5
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AELimiter.cpp
@@ -0,0 +1,77 @@
+/*
+ * 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 "AELimiter.h"
+
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/MathUtils.h"
+
+#include <algorithm>
+#include <math.h>
+
+CAELimiter::CAELimiter()
+{
+ m_amplify = 1.0f;
+ m_attenuation = 1.0f;
+ m_samplerate = 48000.0f;
+ m_holdcounter = 0;
+ m_increase = 0.0f;
+}
+
+float CAELimiter::Run(float* frame[AE_CH_MAX], int channels, int offset /*= 0*/, bool planar /*= false*/)
+{
+ float highest = 0.0f;
+ if (!planar)
+ {
+ for(int i=0; i<channels; i++)
+ {
+ highest = std::max(highest, fabsf(*(frame[0]+offset+i)));
+ }
+ }
+ else
+ {
+ for(int i=0; i<channels; i++)
+ {
+ highest = std::max(highest, fabsf(*(frame[i]+offset)));
+ }
+ }
+
+ float sample = highest * m_amplify;
+ if (sample * m_attenuation > 1.0f)
+ {
+ m_attenuation = 1.0f / sample;
+ m_holdcounter = MathUtils::round_int(static_cast<double>(
+ m_samplerate *
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_limiterHold));
+ m_increase = powf(std::min(sample, 10000.0f), 1.0f / (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_limiterRelease * m_samplerate));
+ }
+
+ float attenuation = m_attenuation;
+
+ if (m_holdcounter > 0)
+ {
+ m_holdcounter--;
+ }
+ else
+ {
+ if (m_increase > 0.0f)
+ {
+ m_attenuation *= m_increase;
+ if (m_attenuation > 1.0f)
+ {
+ m_increase = 0.0f;
+ m_attenuation = 1.0f;
+ }
+ }
+ }
+
+ return attenuation * m_amplify;
+}
+
diff --git a/xbmc/cores/AudioEngine/Utils/AELimiter.h b/xbmc/cores/AudioEngine/Utils/AELimiter.h
new file mode 100644
index 0000000..a79fa66
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AELimiter.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "AEAudioFormat.h"
+
+#include <algorithm>
+
+class CAELimiter
+{
+ private:
+ float m_amplify;
+ float m_attenuation;
+ float m_samplerate;
+ int m_holdcounter;
+ float m_increase;
+
+ public:
+ CAELimiter();
+
+ void SetAmplification(float amplify)
+ {
+ m_amplify = std::max(std::min(amplify, 1000.0f), 0.0f);
+ }
+
+ float GetAmplification() const
+ {
+ return m_amplify;
+ }
+
+ void SetSamplerate(int samplerate)
+ {
+ m_samplerate = (float)samplerate;
+ }
+
+ float Run(float* frame[AE_CH_MAX], int channels, int offset = 0, bool planar = false);
+};
diff --git a/xbmc/cores/AudioEngine/Utils/AEPackIEC61937.cpp b/xbmc/cores/AudioEngine/Utils/AEPackIEC61937.cpp
new file mode 100644
index 0000000..411d481
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEPackIEC61937.cpp
@@ -0,0 +1,235 @@
+/*
+ * 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 "AEPackIEC61937.h"
+
+#include <cassert>
+#include <string.h>
+
+#define IEC61937_PREAMBLE1 0xF872
+#define IEC61937_PREAMBLE2 0x4E1F
+
+inline void SwapEndian(uint16_t *dst, uint16_t *src, unsigned int size)
+{
+ for (unsigned int i = 0; i < size; ++i, ++dst, ++src)
+ *dst = ((*src & 0xFF00) >> 8) | ((*src & 0x00FF) << 8);
+}
+
+int CAEPackIEC61937::PackAC3(uint8_t *data, unsigned int size, uint8_t *dest)
+{
+ assert(size <= OUT_FRAMESTOBYTES(AC3_FRAME_SIZE));
+ struct IEC61937Packet *packet = (struct IEC61937Packet*)dest;
+
+ packet->m_preamble1 = IEC61937_PREAMBLE1;
+ packet->m_preamble2 = IEC61937_PREAMBLE2;
+ packet->m_length = size << 3;
+
+ if (data == NULL)
+ data = packet->m_data;
+#ifdef __BIG_ENDIAN__
+ else
+ memcpy(packet->m_data, data, size);
+#else
+
+ int bitstream_mode = data[5] & 0x7;
+ packet->m_type = IEC61937_TYPE_AC3 | (bitstream_mode << 8);
+
+ size += size & 0x1;
+ SwapEndian((uint16_t*)packet->m_data, (uint16_t*)data, size >> 1);
+#endif
+
+ memset(packet->m_data + size, 0, OUT_FRAMESTOBYTES(AC3_FRAME_SIZE) - IEC61937_DATA_OFFSET - size);
+ return OUT_FRAMESTOBYTES(AC3_FRAME_SIZE);
+}
+
+int CAEPackIEC61937::PackEAC3(uint8_t *data, unsigned int size, uint8_t *dest)
+{
+ assert(size <= OUT_FRAMESTOBYTES(EAC3_FRAME_SIZE));
+ struct IEC61937Packet *packet = (struct IEC61937Packet*)dest;
+
+ packet->m_preamble1 = IEC61937_PREAMBLE1;
+ packet->m_preamble2 = IEC61937_PREAMBLE2;
+ packet->m_type = IEC61937_TYPE_EAC3;
+ packet->m_length = size;
+
+ if (data == NULL)
+ data = packet->m_data;
+#ifdef __BIG_ENDIAN__
+ else
+ memcpy(packet->m_data, data, size);
+#else
+ size += size & 0x1;
+ SwapEndian((uint16_t*)packet->m_data, (uint16_t*)data, size >> 1);
+#endif
+
+ memset(packet->m_data + size, 0, OUT_FRAMESTOBYTES(EAC3_FRAME_SIZE) - IEC61937_DATA_OFFSET - size);
+ return OUT_FRAMESTOBYTES(EAC3_FRAME_SIZE);
+}
+
+int CAEPackIEC61937::PackDTS_512(uint8_t *data, unsigned int size, uint8_t *dest, bool littleEndian)
+{
+ return PackDTS(data, size, dest, littleEndian, OUT_FRAMESTOBYTES(DTS1_FRAME_SIZE), IEC61937_TYPE_DTS1);
+}
+
+int CAEPackIEC61937::PackDTS_1024(uint8_t *data, unsigned int size, uint8_t *dest, bool littleEndian)
+{
+ return PackDTS(data, size, dest, littleEndian, OUT_FRAMESTOBYTES(DTS2_FRAME_SIZE), IEC61937_TYPE_DTS2);
+}
+
+int CAEPackIEC61937::PackDTS_2048(uint8_t *data, unsigned int size, uint8_t *dest, bool littleEndian)
+{
+ return PackDTS(data, size, dest, littleEndian, OUT_FRAMESTOBYTES(DTS3_FRAME_SIZE), IEC61937_TYPE_DTS3);
+}
+
+int CAEPackIEC61937::PackTrueHD(uint8_t *data, unsigned int size, uint8_t *dest)
+{
+ if (size == 0)
+ return OUT_FRAMESTOBYTES(TRUEHD_FRAME_SIZE);
+
+ assert(size <= OUT_FRAMESTOBYTES(TRUEHD_FRAME_SIZE));
+ struct IEC61937Packet *packet = (struct IEC61937Packet*)dest;
+ packet->m_preamble1 = IEC61937_PREAMBLE1;
+ packet->m_preamble2 = IEC61937_PREAMBLE2;
+ packet->m_type = IEC61937_TYPE_TRUEHD;
+ packet->m_length = size;
+
+ if (data == NULL)
+ data = packet->m_data;
+#ifdef __BIG_ENDIAN__
+ else
+ memcpy(packet->m_data, data, size);
+#else
+ size += size & 0x1;
+ SwapEndian((uint16_t*)packet->m_data, (uint16_t*)data, size >> 1);
+#endif
+
+ memset(packet->m_data + size, 0, OUT_FRAMESTOBYTES(TRUEHD_FRAME_SIZE) - IEC61937_DATA_OFFSET - size);
+ return OUT_FRAMESTOBYTES(TRUEHD_FRAME_SIZE);
+}
+
+int CAEPackIEC61937::PackDTSHD(uint8_t *data, unsigned int size, uint8_t *dest, unsigned int period)
+{
+ unsigned int subtype;
+ switch (period)
+ {
+ case 512: subtype = 0; break;
+ case 1024: subtype = 1; break;
+ case 2048: subtype = 2; break;
+ case 4096: subtype = 3; break;
+ case 8192: subtype = 4; break;
+ case 16384: subtype = 5; break;
+
+ default:
+ return 0;
+ }
+
+ struct IEC61937Packet *packet = (struct IEC61937Packet*)dest;
+ packet->m_preamble1 = IEC61937_PREAMBLE1;
+ packet->m_preamble2 = IEC61937_PREAMBLE2;
+ packet->m_type = IEC61937_TYPE_DTSHD | (subtype << 8);
+
+ /* Align so that (length_code & 0xf) == 0x8. This is reportedly needed
+ * with some receivers, but the exact requirement is unconfirmed. */
+ packet->m_length = ((size + 0x17) &~ 0x0f) - 0x08;
+
+ if (data == NULL)
+ data = packet->m_data;
+#ifdef __BIG_ENDIAN__
+ else
+ memcpy(packet->m_data, data, size);
+#else
+ size += size & 0x1;
+ SwapEndian((uint16_t*)packet->m_data, (uint16_t*)data, size >> 1);
+#endif
+
+ unsigned int burstsize = period << 2;
+ memset(packet->m_data + size, 0, burstsize - IEC61937_DATA_OFFSET - size);
+ return burstsize;
+}
+
+int CAEPackIEC61937::PackDTS(uint8_t *data, unsigned int size, uint8_t *dest, bool littleEndian,
+ unsigned int frameSize, uint16_t type)
+{
+ assert(size <= frameSize);
+
+ /* BE is the standard endianness, byteswap needed if LE */
+ bool byteSwapNeeded = littleEndian;
+
+#ifndef __BIG_ENDIAN__
+ /* on LE systems we want LE output, byteswap needed */
+ byteSwapNeeded ^= true;
+#endif
+
+ struct IEC61937Packet *packet = (struct IEC61937Packet*)dest;
+ uint8_t *dataTo;
+
+ if (size == frameSize)
+ {
+ /* No packing possible or needed, DTS stream is suitable for direct output */
+ dataTo = dest;
+ }
+ else if (size <= frameSize - IEC61937_DATA_OFFSET)
+ {
+ /* Fits to IEC61937, perform packing */
+ packet->m_preamble1 = IEC61937_PREAMBLE1;
+ packet->m_preamble2 = IEC61937_PREAMBLE2;
+ packet->m_type = type;
+ packet->m_length = size << 3;
+
+ dataTo = packet->m_data;
+ }
+ else
+ {
+ /* Stream is unsuitable for both packing and direct output */
+ return 0;
+ }
+
+ if (data == NULL)
+ data = dataTo;
+ else if (!byteSwapNeeded)
+ memcpy(dataTo, data, size);
+
+ if (byteSwapNeeded)
+ {
+ size += size & 0x1;
+ SwapEndian((uint16_t*)dataTo, (uint16_t*)data, size >> 1);
+ }
+
+ if (size != frameSize)
+ memset(packet->m_data + size, 0, frameSize - IEC61937_DATA_OFFSET - size);
+
+ return frameSize;
+}
+
+int CAEPackIEC61937::PackPause(uint8_t *dest, unsigned int millis, unsigned int framesize, unsigned int samplerate, unsigned int rep_period, unsigned int encodedRate)
+{
+ int periodInBytes = rep_period * framesize;
+ double periodInTime = (double)rep_period / samplerate * 1000;
+ int periodsNeeded = millis / periodInTime;
+ int maxPeriods = MAX_IEC61937_PACKET / periodInBytes;
+ if (periodsNeeded > maxPeriods)
+ periodsNeeded = maxPeriods;
+ uint16_t gap = encodedRate * millis / 1000;
+
+ struct IEC61937Packet *packet = (struct IEC61937Packet*)dest;
+ packet->m_preamble1 = IEC61937_PREAMBLE1;
+ packet->m_preamble2 = IEC61937_PREAMBLE2;
+ packet->m_type = 3;
+ packet->m_length = 32;
+ memset(packet->m_data, 0, periodInBytes - 8);
+
+ for (int i=1; i<periodsNeeded; i++)
+ {
+ memcpy(dest+i*periodInBytes, dest, periodInBytes);
+ }
+
+ uint16_t *gapPtr = reinterpret_cast<uint16_t*>(packet->m_data);
+ *gapPtr = gap;
+
+ return periodsNeeded * periodInBytes;
+}
diff --git a/xbmc/cores/AudioEngine/Utils/AEPackIEC61937.h b/xbmc/cores/AudioEngine/Utils/AEPackIEC61937.h
new file mode 100644
index 0000000..5a83b65
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEPackIEC61937.h
@@ -0,0 +1,76 @@
+/*
+ * 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 <list>
+#include <stdint.h>
+
+#define MAX_IEC61937_PACKET 61440
+#define IEC61937_DATA_OFFSET 8
+
+#define DTS1_FRAME_SIZE 512
+#define DTS2_FRAME_SIZE 1024
+#define DTS3_FRAME_SIZE 2048
+#define AC3_FRAME_SIZE 1536
+#define EAC3_FRAME_SIZE 6144
+#define TRUEHD_FRAME_SIZE 15360
+
+#define OUT_SAMPLESIZE 16
+#define OUT_CHANNELS 2
+#define OUT_FRAMESTOBYTES(a) ((a) * OUT_CHANNELS * (OUT_SAMPLESIZE>>3))
+
+class CAEPackIEC61937
+{
+public:
+ CAEPackIEC61937() = default;
+ typedef int (*PackFunc)(uint8_t *data, unsigned int size, uint8_t *dest);
+
+ static int PackAC3 (uint8_t *data, unsigned int size, uint8_t *dest);
+ static int PackEAC3 (uint8_t *data, unsigned int size, uint8_t *dest);
+ static int PackDTS_512 (uint8_t *data, unsigned int size, uint8_t *dest, bool littleEndian);
+ static int PackDTS_1024(uint8_t *data, unsigned int size, uint8_t *dest, bool littleEndian);
+ static int PackDTS_2048(uint8_t *data, unsigned int size, uint8_t *dest, bool littleEndian);
+ static int PackTrueHD (uint8_t *data, unsigned int size, uint8_t *dest);
+ static int PackDTSHD (uint8_t *data, unsigned int size, uint8_t *dest, unsigned int period);
+ static int PackPause(uint8_t *dest, unsigned int millis, unsigned int framesize, unsigned int samplerate, unsigned int rep_period, unsigned int encodedRate);
+private:
+
+ static int PackDTS(uint8_t *data, unsigned int size, uint8_t *dest, bool littleEndian,
+ unsigned int frameSize, uint16_t type);
+
+ enum IEC61937DataType
+ {
+ IEC61937_TYPE_NULL = 0x00,
+ IEC61937_TYPE_AC3 = 0x01,
+ IEC61937_TYPE_DTS1 = 0x0B, /* 512 samples */
+ IEC61937_TYPE_DTS2 = 0x0C, /* 1024 samples */
+ IEC61937_TYPE_DTS3 = 0x0D, /* 2048 samples */
+ IEC61937_TYPE_DTSHD = 0x11,
+ IEC61937_TYPE_EAC3 = 0x15,
+ IEC61937_TYPE_TRUEHD = 0x16
+ };
+
+#ifdef __GNUC__
+ struct __attribute__((__packed__)) IEC61937Packet
+#else
+ __pragma(pack(push, 1))
+ struct IEC61937Packet
+#endif
+ {
+ uint16_t m_preamble1;
+ uint16_t m_preamble2;
+ uint16_t m_type;
+ uint16_t m_length;
+ uint8_t m_data[MAX_IEC61937_PACKET - IEC61937_DATA_OFFSET];
+ };
+#ifndef __GNUC__
+ __pragma(pack(pop))
+#endif
+};
+
diff --git a/xbmc/cores/AudioEngine/Utils/AERingBuffer.h b/xbmc/cores/AudioEngine/Utils/AERingBuffer.h
new file mode 100644
index 0000000..8a2a1aa
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AERingBuffer.h
@@ -0,0 +1,284 @@
+/*
+ * 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
+
+#define AE_RING_BUFFER_OK 0;
+#define AE_RING_BUFFER_EMPTY 1;
+#define AE_RING_BUFFER_FULL 2;
+#define AE_RING_BUFFER_NOTAVAILABLE 3;
+
+//#define AE_RING_BUFFER_DEBUG
+
+#include "utils/log.h"
+#include "utils/MemUtils.h"
+
+#include <string.h>
+
+/**
+ * This buffer can be used by one read and one write thread at any one time
+ * without the risk of data corruption.
+ * If you intend to call the Reset() method, please use Locks.
+ * All other operations are thread-safe.
+ */
+class AERingBuffer {
+
+public:
+ AERingBuffer() = default;
+
+ AERingBuffer(unsigned int size, unsigned int planes = 1) { Create(size, planes); }
+
+ ~AERingBuffer()
+ {
+#ifdef AE_RING_BUFFER_DEBUG
+ CLog::Log(LOGDEBUG, "AERingBuffer::~AERingBuffer: Deleting buffer.");
+#endif
+ for (unsigned int i = 0; i < m_planes; i++)
+ KODI::MEMORY::AlignedFree(m_Buffer[i]);
+ delete[] m_Buffer;
+ }
+
+ /**
+ * Allocates space for buffer, and sets it's contents to 0.
+ *
+ * @return true on success, false otherwise
+ */
+ bool Create(int size, unsigned int planes = 1)
+ {
+ m_Buffer = new unsigned char*[planes];
+ for (unsigned int i = 0; i < planes; i++)
+ {
+ m_Buffer[i] = static_cast<unsigned char*>(KODI::MEMORY::AlignedMalloc(size, 16));
+ if (!m_Buffer[i])
+ return false;
+ memset(m_Buffer[i], 0, size);
+ }
+ m_iSize = size;
+ m_planes = planes;
+ return true;
+ }
+
+ /**
+ * Fills the buffer with zeros and resets the pointers.
+ * This method is not thread-safe, so before using this method
+ * please acquire a Lock()
+ */
+ void Reset() {
+#ifdef AE_RING_BUFFER_DEBUG
+ CLog::Log(LOGDEBUG, "AERingBuffer::Reset: Buffer reset.");
+#endif
+ m_iWritten = 0;
+ m_iRead = 0;
+ m_iReadPos = 0;
+ m_iWritePos = 0;
+ }
+
+ /**
+ * Writes data to buffer.
+ * Attempt to write more bytes than available results in AE_RING_BUFFER_FULL.
+ *
+ * @return AE_RING_BUFFER_OK on success, otherwise an error code
+ */
+ int Write(unsigned char *src, unsigned int size, unsigned int plane = 0)
+ {
+ unsigned int space = GetWriteSize();
+
+ //do we have enough space for all the data?
+ if (size > space || plane >= m_planes)
+ {
+#ifdef AE_RING_BUFFER_DEBUG
+ CLog::Log(LOGDEBUG,
+ "AERingBuffer: Not enough space, ignoring data. Requested: {} Available: {}", size,
+ space);
+#endif
+ return AE_RING_BUFFER_FULL;
+ }
+
+ //no wrapping?
+ if ( m_iSize > size + m_iWritePos )
+ {
+#ifdef AE_RING_BUFFER_DEBUG
+ CLog::Log(LOGDEBUG, "AERingBuffer: Written to: {} size: {} space before: {}", m_iWritePos,
+ size, space);
+#endif
+ memcpy(m_Buffer[plane] + m_iWritePos, src, size);
+ }
+ //need to wrap
+ else
+ {
+ unsigned int first = m_iSize - m_iWritePos;
+ unsigned int second = size - first;
+#ifdef AE_RING_BUFFER_DEBUG
+ CLog::Log(LOGDEBUG,
+ "AERingBuffer: Written to (split) first: {} second: {} size: {} space before: {}",
+ first, second, size, space);
+#endif
+ memcpy(m_Buffer[plane] + m_iWritePos, src, first);
+ memcpy(m_Buffer[plane], src + first, second);
+ }
+ if (plane + 1 == m_planes)
+ WriteFinished(size);
+
+ return AE_RING_BUFFER_OK;
+ }
+
+ /**
+ * Reads data from buffer.
+ * Attempt to read more bytes than available results in RING_BUFFER_NOTAVAILABLE.
+ * Reading from empty buffer returns AE_RING_BUFFER_EMPTY
+ *
+ * @return AE_RING_BUFFER_OK on success, otherwise an error code
+ */
+ int Read(unsigned char *dest, unsigned int size, unsigned int plane = 0)
+ {
+ unsigned int space = GetReadSize();
+
+ //want to read more than we have written?
+ if( space == 0 )
+ {
+#ifdef AE_RING_BUFFER_DEBUG
+ CLog::Log(LOGDEBUG, "AERingBuffer: Can't read from empty buffer.");
+#endif
+ return AE_RING_BUFFER_EMPTY;
+ }
+
+ //want to read more than we have available
+ if( size > space || plane >= m_planes)
+ {
+#ifdef AE_RING_BUFFER_DEBUG
+ CLog::Log(LOGDEBUG, "AERingBuffer: Can't read {} bytes when we only have {}.", size, space);
+#endif
+ return AE_RING_BUFFER_NOTAVAILABLE;
+ }
+
+ //no wrapping?
+ if ( size + m_iReadPos < m_iSize )
+ {
+#ifdef AE_RING_BUFFER_DEBUG
+ CLog::Log(LOGDEBUG, "AERingBuffer: Reading from: {} size: {} space before: {}", m_iWritePos,
+ size, space);
+#endif
+ if (dest)
+ memcpy(dest, m_Buffer[plane] + m_iReadPos, size);
+ }
+ //need to wrap
+ else
+ {
+ unsigned int first = m_iSize - m_iReadPos;
+ unsigned int second = size - first;
+#ifdef AE_RING_BUFFER_DEBUG
+ CLog::Log(LOGDEBUG,
+ "AERingBuffer: Reading from (split) first: {} second: {} size: {} space before: {}",
+ first, second, size, space);
+#endif
+ if (dest)
+ {
+ memcpy(dest, m_Buffer[plane] + m_iReadPos, first);
+ memcpy(dest + first, m_Buffer[plane], second);
+ }
+ }
+ if (plane + 1 == m_planes)
+ ReadFinished(size);
+
+ return AE_RING_BUFFER_OK;
+ }
+
+ /**
+ * Dumps the buffer.
+ */
+ void Dump()
+ {
+ unsigned char *bufferContents = static_cast<unsigned char*>(KODI::MEMORY::AlignedMalloc(m_iSize * m_planes + 1, 16));
+ unsigned char *dest = bufferContents;
+ for (unsigned int j = 0; j < m_planes; j++)
+ {
+ for (unsigned int i=0; i<m_iSize; i++)
+ {
+ if (i >= m_iReadPos && i<m_iWritePos)
+ *dest++ = m_Buffer[j][i];
+ else
+ *dest++ = '_';
+ }
+ }
+ bufferContents[m_iSize*m_planes] = '\0';
+ CLog::LogF(LOGDEBUG, "Buffer Content: {}", reinterpret_cast<const char*>(bufferContents));
+ KODI::MEMORY::AlignedFree(bufferContents);
+ }
+
+ /**
+ * Returns available space for writing to buffer.
+ * Attempt to write more bytes than available results in AE_RING_BUFFER_FULL.
+ */
+ unsigned int GetWriteSize()
+ {
+ return m_iSize - ( m_iWritten - m_iRead );
+ }
+
+ /**
+ * Returns available space for reading from buffer.
+ * Attempt to read more bytes than available results in AE_RING_BUFFER_EMPTY.
+ */
+ unsigned int GetReadSize()
+ {
+ return m_iWritten - m_iRead;
+ }
+
+ /**
+ * Returns the buffer size.
+ */
+ unsigned int GetMaxSize()
+ {
+ return m_iSize;
+ }
+
+ /**
+ * Returns the number of planes
+ */
+ unsigned int NumPlanes() const
+ {
+ return m_planes;
+ }
+private:
+ /**
+ * Increments the write pointer.
+ * Called at the end of writing to all planes.
+ */
+ void WriteFinished(unsigned int size)
+ {
+ if ( m_iSize > size + m_iWritePos )
+ m_iWritePos += size;
+ else // wrapping
+ m_iWritePos = size - (m_iSize - m_iWritePos);
+
+ //we can increase the write count now
+ m_iWritten+=size;
+ }
+
+ /**
+ * Increments the read pointer.
+ * Called at the end of reading to all planes.
+ */
+ void ReadFinished(unsigned int size)
+ {
+ if ( size + m_iReadPos < m_iSize )
+ m_iReadPos += size;
+ else
+ m_iReadPos = size - (m_iSize - m_iReadPos);
+
+ //we can increase the read count now
+ m_iRead+=size;
+ }
+
+ unsigned int m_iReadPos = 0;
+ unsigned int m_iWritePos = 0;
+ unsigned int m_iRead = 0;
+ unsigned int m_iWritten = 0;
+ unsigned int m_iSize = 0;
+ unsigned int m_planes = 0;
+ unsigned char** m_Buffer = nullptr;
+};
diff --git a/xbmc/cores/AudioEngine/Utils/AEStreamData.h b/xbmc/cores/AudioEngine/Utils/AEStreamData.h
new file mode 100644
index 0000000..a0624f4
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEStreamData.h
@@ -0,0 +1,19 @@
+/*
+ * 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
+
+/**
+ * Bit options to pass to IAE::MakeStream
+ */
+enum AEStreamOptions
+{
+ AESTREAM_FORCE_RESAMPLE = 1 << 0, /* force resample even if rates match */
+ AESTREAM_PAUSED = 1 << 1, /* create the stream paused */
+ AESTREAM_AUTOSTART = 1 << 2, /* autostart the stream when enough data is buffered */
+};
diff --git a/xbmc/cores/AudioEngine/Utils/AEStreamInfo.cpp b/xbmc/cores/AudioEngine/Utils/AEStreamInfo.cpp
new file mode 100644
index 0000000..f12a025
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEStreamInfo.cpp
@@ -0,0 +1,865 @@
+/*
+ * 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 "AEStreamInfo.h"
+
+#include "utils/log.h"
+
+#include <algorithm>
+#include <string.h>
+
+#define DTS_PREAMBLE_14BE 0x1FFFE800
+#define DTS_PREAMBLE_14LE 0xFF1F00E8
+#define DTS_PREAMBLE_16BE 0x7FFE8001
+#define DTS_PREAMBLE_16LE 0xFE7F0180
+#define DTS_PREAMBLE_HD 0x64582025
+#define DTS_PREAMBLE_XCH 0x5a5a5a5a
+#define DTS_PREAMBLE_XXCH 0x47004a03
+#define DTS_PREAMBLE_X96K 0x1d95f262
+#define DTS_PREAMBLE_XBR 0x655e315e
+#define DTS_PREAMBLE_LBR 0x0a801921
+#define DTS_PREAMBLE_XLL 0x41a29547
+#define DTS_SFREQ_COUNT 16
+#define MAX_EAC3_BLOCKS 6
+#define UNKNOWN_DTS_EXTENSION 255
+
+static const uint16_t AC3Bitrates[] = {32, 40, 48, 56, 64, 80, 96, 112, 128, 160,
+ 192, 224, 256, 320, 384, 448, 512, 576, 640};
+static const uint16_t AC3FSCod[] = {48000, 44100, 32000, 0};
+static const uint8_t AC3BlkCod[] = {1, 2, 3, 6};
+static const uint8_t AC3Channels[] = {2, 1, 2, 3, 3, 4, 4, 5};
+static const uint8_t DTSChannels[] = {1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6, 7, 8, 8};
+static const uint8_t THDChanMap[] = {2, 1, 1, 2, 2, 2, 2, 1, 1, 2, 2, 1, 1};
+
+static const uint32_t DTSSampleRates[DTS_SFREQ_COUNT] = {0, 8000, 16000, 32000, 64000, 128000,
+ 11025, 22050, 44100, 88200, 176400, 12000,
+ 24000, 48000, 96000, 192000};
+
+CAEStreamParser::CAEStreamParser() : m_syncFunc(&CAEStreamParser::DetectType)
+{
+ av_crc_init(m_crcTrueHD, 0, 16, 0x2D, sizeof(m_crcTrueHD));
+}
+
+double CAEStreamInfo::GetDuration() const
+{
+ double duration = 0;
+ switch (m_type)
+ {
+ case STREAM_TYPE_AC3:
+ duration = 0.032;
+ break;
+ case STREAM_TYPE_EAC3:
+ duration = 6144.0 / m_sampleRate / 4;
+ break;
+ case STREAM_TYPE_TRUEHD:
+ int rate;
+ if (m_sampleRate == 48000 || m_sampleRate == 96000 || m_sampleRate == 192000)
+ rate = 192000;
+ else
+ rate = 176400;
+ duration = 3840.0 / rate;
+ break;
+ case STREAM_TYPE_DTS_512:
+ case STREAM_TYPE_DTSHD_CORE:
+ case STREAM_TYPE_DTSHD:
+ case STREAM_TYPE_DTSHD_MA:
+ duration = 512.0 / m_sampleRate;
+ break;
+ case STREAM_TYPE_DTS_1024:
+ duration = 1024.0 / m_sampleRate;
+ break;
+ case STREAM_TYPE_DTS_2048:
+ duration = 2048.0 / m_sampleRate;
+ break;
+ default:
+ CLog::Log(LOGERROR, "CAEStreamInfo::GetDuration - invalid stream type");
+ break;
+ }
+ return duration * 1000;
+}
+
+bool CAEStreamInfo::operator==(const CAEStreamInfo& info) const
+{
+ if (m_type != info.m_type)
+ return false;
+ if (m_dataIsLE != info.m_dataIsLE)
+ return false;
+ if (m_repeat != info.m_repeat)
+ return false;
+ return true;
+}
+
+void CAEStreamParser::Reset()
+{
+ m_skipBytes = 0;
+ m_bufferSize = 0;
+ m_needBytes = 0;
+ m_hasSync = false;
+}
+
+int CAEStreamParser::AddData(uint8_t* data,
+ unsigned int size,
+ uint8_t** buffer,
+ unsigned int* bufferSize)
+{
+ if (size == 0)
+ {
+ if (bufferSize)
+ *bufferSize = 0;
+ return 0;
+ }
+
+ if (m_skipBytes)
+ {
+ unsigned int canSkip = std::min(size, m_skipBytes);
+ unsigned int room = sizeof(m_buffer) - m_bufferSize;
+ unsigned int copy = std::min(room, canSkip);
+
+ memcpy(m_buffer + m_bufferSize, data, copy);
+ m_bufferSize += copy;
+ m_skipBytes -= copy;
+
+ if (m_skipBytes)
+ {
+ if (bufferSize)
+ *bufferSize = 0;
+ return copy;
+ }
+
+ GetPacket(buffer, bufferSize);
+ return copy;
+ }
+ else
+ {
+ unsigned int consumed = 0;
+ unsigned int offset = 0;
+ unsigned int room = sizeof(m_buffer) - m_bufferSize;
+ while (true)
+ {
+ if (!size)
+ {
+ if (bufferSize)
+ *bufferSize = 0;
+ return consumed;
+ }
+
+ unsigned int copy = std::min(room, size);
+ memcpy(m_buffer + m_bufferSize, data, copy);
+ m_bufferSize += copy;
+ consumed += copy;
+ data += copy;
+ size -= copy;
+ room -= copy;
+
+ if (m_needBytes > m_bufferSize)
+ continue;
+
+ m_needBytes = 0;
+ offset = (this->*m_syncFunc)(m_buffer, m_bufferSize);
+
+ if (m_hasSync)
+ break;
+ else
+ {
+ // lost sync
+ m_syncFunc = &CAEStreamParser::DetectType;
+ m_info.m_type = CAEStreamInfo::STREAM_TYPE_NULL;
+ m_info.m_repeat = 1;
+
+ // if the buffer is full, or the offset < the buffer size
+ if (m_bufferSize == sizeof(m_buffer) || offset < m_bufferSize)
+ {
+ m_bufferSize -= offset;
+ room += offset;
+ memmove(m_buffer, m_buffer + offset, m_bufferSize);
+ }
+ }
+ }
+
+ // if we got here, we acquired sync on the buffer
+
+ // align the buffer
+ if (offset)
+ {
+ m_bufferSize -= offset;
+ memmove(m_buffer, m_buffer + offset, m_bufferSize);
+ }
+
+ // bytes to skip until the next packet
+ m_skipBytes = std::max(0, (int)m_fsize - (int)m_bufferSize);
+ if (m_skipBytes)
+ {
+ if (bufferSize)
+ *bufferSize = 0;
+ return consumed;
+ }
+
+ if (!m_needBytes)
+ GetPacket(buffer, bufferSize);
+ else if (bufferSize)
+ *bufferSize = 0;
+
+ return consumed;
+ }
+}
+
+void CAEStreamParser::GetPacket(uint8_t** buffer, unsigned int* bufferSize)
+{
+ // if the caller wants the packet
+ if (buffer)
+ {
+ // if it is dtsHD and we only want the core, just fetch that
+ unsigned int size = m_fsize;
+ if (m_info.m_type == CAEStreamInfo::STREAM_TYPE_DTSHD_CORE)
+ size = m_coreSize;
+
+ // make sure the buffer is allocated and big enough
+ if (!*buffer || !bufferSize || *bufferSize < size)
+ {
+ delete[] * buffer;
+ *buffer = new uint8_t[size];
+ }
+
+ // copy the data into the buffer and update the size
+ memcpy(*buffer, m_buffer, size);
+ if (bufferSize)
+ *bufferSize = size;
+ }
+
+ // remove the parsed data from the buffer
+ m_bufferSize -= m_fsize;
+ memmove(m_buffer, m_buffer + m_fsize, m_bufferSize);
+ m_fsize = 0;
+ m_coreSize = 0;
+}
+
+// SYNC FUNCTIONS
+
+// This function looks for sync words across the types in parallel, and only does an exhaustive
+// test if it finds a syncword. Once sync has been established, the relevant sync function sets
+// m_syncFunc to itself. This function will only be called again if total sync is lost, which
+// allows is to switch stream types on the fly much like a real receiver does.
+unsigned int CAEStreamParser::DetectType(uint8_t* data, unsigned int size)
+{
+ unsigned int skipped = 0;
+ unsigned int possible = 0;
+
+ while (size > 8)
+ {
+ // if it could be DTS
+ unsigned int header = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3];
+ if (header == DTS_PREAMBLE_14LE || header == DTS_PREAMBLE_14BE || header == DTS_PREAMBLE_16LE ||
+ header == DTS_PREAMBLE_16BE)
+ {
+ unsigned int skip = SyncDTS(data, size);
+ if (m_hasSync || m_needBytes)
+ return skipped + skip;
+ else
+ possible = skipped;
+ }
+
+ // if it could be AC3
+ if (data[0] == 0x0b && data[1] == 0x77)
+ {
+ unsigned int skip = SyncAC3(data, size);
+ if (m_hasSync || m_needBytes)
+ return skipped + skip;
+ else
+ possible = skipped;
+ }
+
+ // if it could be TrueHD
+ if (data[4] == 0xf8 && data[5] == 0x72 && data[6] == 0x6f && data[7] == 0xba)
+ {
+ unsigned int skip = SyncTrueHD(data, size);
+ if (m_hasSync)
+ return skipped + skip;
+ else
+ possible = skipped;
+ }
+
+ // move along one byte
+ --size;
+ ++skipped;
+ ++data;
+ }
+
+ return possible ? possible : skipped;
+}
+
+bool CAEStreamParser::TrySyncAC3(uint8_t* data,
+ unsigned int size,
+ bool resyncing,
+ bool wantEAC3dependent)
+{
+ if (size < 8)
+ return false;
+
+ // look for an ac3 sync word
+ if (data[0] != 0x0b || data[1] != 0x77)
+ return false;
+
+ uint8_t bsid = data[5] >> 3;
+ uint8_t acmod = data[6] >> 5;
+ uint8_t lfeon;
+
+ int8_t pos = 4;
+ if ((acmod & 0x1) && (acmod != 0x1))
+ pos -= 2;
+ if (acmod & 0x4)
+ pos -= 2;
+ if (acmod == 0x2)
+ pos -= 2;
+ if (pos < 0)
+ lfeon = (data[7] & 0x64) ? 1 : 0;
+ else
+ lfeon = ((data[6] >> pos) & 0x1) ? 1 : 0;
+
+ if (bsid > 0x11 || acmod > 7)
+ return false;
+
+ if (bsid <= 10)
+ {
+ // Normal AC-3
+
+ if (wantEAC3dependent)
+ return false;
+
+ uint8_t fscod = data[4] >> 6;
+ uint8_t frmsizecod = data[4] & 0x3F;
+ if (fscod == 3 || frmsizecod > 37)
+ return false;
+
+ // get the details we need to check crc1 and framesize
+ unsigned int bitRate = AC3Bitrates[frmsizecod >> 1];
+ unsigned int framesize = 0;
+ switch (fscod)
+ {
+ case 0:
+ framesize = bitRate * 2;
+ break;
+ case 1:
+ framesize = (320 * bitRate / 147 + (frmsizecod & 1 ? 1 : 0));
+ break;
+ case 2:
+ framesize = bitRate * 4;
+ break;
+ }
+
+ m_fsize = framesize << 1;
+ m_info.m_sampleRate = AC3FSCod[fscod];
+
+ // dont do extensive testing if we have not lost sync
+ if (m_info.m_type == CAEStreamInfo::STREAM_TYPE_AC3 && !resyncing)
+ return true;
+
+ // this may be the main stream of EAC3
+ unsigned int fsizeMain = m_fsize;
+ unsigned int reqBytes = fsizeMain + 8;
+ if (size < reqBytes)
+ {
+ // not enough data to check for E-AC3 dependent frame, request more
+ m_needBytes = reqBytes;
+ m_fsize = 0;
+ // no need to resync => return true
+ return true;
+ }
+ m_info.m_ac3FrameSize = fsizeMain;
+ if (TrySyncAC3(data + fsizeMain, size - fsizeMain, resyncing, true))
+ {
+ // concatenate the main and dependent frames
+ m_fsize += fsizeMain;
+ return true;
+ }
+
+ unsigned int crc_size;
+ // if we have enough data, validate the entire packet, else try to validate crc2 (5/8 of the packet)
+ if (framesize <= size)
+ crc_size = framesize - 1;
+ else
+ crc_size = (framesize >> 1) + (framesize >> 3) - 1;
+
+ if (crc_size <= size)
+ if (av_crc(av_crc_get_table(AV_CRC_16_ANSI), 0, &data[2], crc_size * 2))
+ return false;
+
+ // if we get here, we can sync
+ m_hasSync = true;
+ m_info.m_channels = AC3Channels[acmod] + lfeon;
+ m_syncFunc = &CAEStreamParser::SyncAC3;
+ m_info.m_type = CAEStreamInfo::STREAM_TYPE_AC3;
+ m_info.m_ac3FrameSize += m_fsize;
+ m_info.m_repeat = 1;
+
+ CLog::Log(LOGINFO, "CAEStreamParser::TrySyncAC3 - AC3 stream detected ({} channels, {}Hz)",
+ m_info.m_channels, m_info.m_sampleRate);
+ return true;
+ }
+ else
+ {
+ // Enhanced AC-3
+ uint8_t strmtyp = data[2] >> 6;
+ if (strmtyp == 3)
+ return false;
+
+ if (strmtyp != 1 && wantEAC3dependent)
+ {
+ CLog::Log(LOGDEBUG,
+ "CAEStreamParser::TrySyncAC3 - Unexpected stream type: {} (wantEAC3dependent: {})",
+ strmtyp, wantEAC3dependent);
+ return false;
+ }
+
+ unsigned int framesize = (((data[2] & 0x7) << 8) | data[3]) + 1;
+ uint8_t fscod = (data[4] >> 6) & 0x3;
+ uint8_t cod = (data[4] >> 4) & 0x3;
+ uint8_t acmod = (data[4] >> 1) & 0x7;
+ uint8_t lfeon = data[4] & 0x1;
+ uint8_t blocks;
+
+ if (fscod == 0x3)
+ {
+ if (cod == 0x3)
+ return false;
+
+ blocks = 6;
+ m_info.m_sampleRate = AC3FSCod[cod] >> 1;
+ }
+ else
+ {
+ blocks = AC3BlkCod[cod];
+ m_info.m_sampleRate = AC3FSCod[fscod];
+ }
+
+ m_fsize = framesize << 1;
+ m_info.m_repeat = MAX_EAC3_BLOCKS / blocks;
+
+ // EAC3 can have a dependent stream too
+ if (!wantEAC3dependent)
+ {
+ unsigned int fsizeMain = m_fsize;
+ unsigned int reqBytes = fsizeMain + 8;
+ if (size < reqBytes)
+ {
+ // not enough data to check for E-AC3 dependent frame, request more
+ m_needBytes = reqBytes;
+ m_fsize = 0;
+ // no need to resync => return true
+ return true;
+ }
+ m_info.m_ac3FrameSize = fsizeMain;
+ if (TrySyncAC3(data + fsizeMain, size - fsizeMain, resyncing, true))
+ {
+ // concatenate the main and dependent frames
+ m_fsize += fsizeMain;
+ return true;
+ }
+ }
+
+ if (m_info.m_type == CAEStreamInfo::STREAM_TYPE_EAC3 && m_hasSync && !resyncing)
+ return true;
+
+ // if we get here, we can sync
+ m_hasSync = true;
+ m_info.m_channels = AC3Channels[acmod] + lfeon;
+ m_syncFunc = &CAEStreamParser::SyncAC3;
+ m_info.m_type = CAEStreamInfo::STREAM_TYPE_EAC3;
+ m_info.m_ac3FrameSize += m_fsize;
+
+ CLog::Log(LOGINFO, "CAEStreamParser::TrySyncAC3 - E-AC3 stream detected ({} channels, {}Hz)",
+ m_info.m_channels, m_info.m_sampleRate);
+ return true;
+ }
+}
+
+unsigned int CAEStreamParser::SyncAC3(uint8_t* data, unsigned int size)
+{
+ unsigned int skip = 0;
+
+ for (; size - skip > 7; ++skip, ++data)
+ {
+ bool resyncing = (skip != 0);
+ if (TrySyncAC3(data, size - skip, resyncing, false))
+ return skip;
+ }
+
+ // if we get here, the entire packet is invalid and we have lost sync
+ CLog::Log(LOGINFO, "CAEStreamParser::SyncAC3 - AC3 sync lost");
+ m_hasSync = false;
+ return skip;
+}
+
+unsigned int CAEStreamParser::SyncDTS(uint8_t* data, unsigned int size)
+{
+ if (size < 13)
+ {
+ if (m_needBytes < 13)
+ m_needBytes = 14;
+ return 0;
+ }
+
+ unsigned int skip = 0;
+ for (; size - skip > 13; ++skip, ++data)
+ {
+ unsigned int header = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3];
+ unsigned int hd_sync = 0;
+ unsigned int dtsBlocks;
+ unsigned int amode;
+ unsigned int sfreq;
+ unsigned int target_rate;
+ unsigned int extension = 0;
+ unsigned int ext_type = UNKNOWN_DTS_EXTENSION;
+ unsigned int lfe;
+ int bits;
+
+ switch (header)
+ {
+ // 14bit BE
+ case DTS_PREAMBLE_14BE:
+ if (data[4] != 0x07 || (data[5] & 0xf0) != 0xf0)
+ continue;
+ dtsBlocks = (((data[5] & 0x7) << 4) | ((data[6] & 0x3C) >> 2)) + 1;
+ m_fsize = (((((data[6] & 0x3) << 8) | data[7]) << 4) | ((data[8] & 0x3C) >> 2)) + 1;
+ amode = ((data[8] & 0x3) << 4) | ((data[9] & 0xF0) >> 4);
+ target_rate = ((data[10] & 0x3e) >> 1);
+ extension = ((data[11] & 0x1));
+ ext_type = ((data[11] & 0xe) >> 1);
+ sfreq = data[9] & 0xF;
+ lfe = (data[12] & 0x18) >> 3;
+ m_info.m_dataIsLE = false;
+ bits = 14;
+ break;
+
+ // 14bit LE
+ case DTS_PREAMBLE_14LE:
+ if (data[5] != 0x07 || (data[4] & 0xf0) != 0xf0)
+ continue;
+ dtsBlocks = (((data[4] & 0x7) << 4) | ((data[7] & 0x3C) >> 2)) + 1;
+ m_fsize = (((((data[7] & 0x3) << 8) | data[6]) << 4) | ((data[9] & 0x3C) >> 2)) + 1;
+ amode = ((data[9] & 0x3) << 4) | ((data[8] & 0xF0) >> 4);
+ target_rate = ((data[11] & 0x3e) >> 1);
+ extension = ((data[10] & 0x1));
+ ext_type = ((data[10] & 0xe) >> 1);
+ sfreq = data[8] & 0xF;
+ lfe = (data[13] & 0x18) >> 3;
+ m_info.m_dataIsLE = true;
+ bits = 14;
+ break;
+
+ // 16bit BE
+ case DTS_PREAMBLE_16BE:
+ dtsBlocks = (((data[4] & 0x1) << 7) | ((data[5] & 0xFC) >> 2)) + 1;
+ m_fsize = (((((data[5] & 0x3) << 8) | data[6]) << 4) | ((data[7] & 0xF0) >> 4)) + 1;
+ amode = ((data[7] & 0x0F) << 2) | ((data[8] & 0xC0) >> 6);
+ sfreq = (data[8] & 0x3C) >> 2;
+ target_rate = ((data[8] & 0x03) << 3) | ((data[9] & 0xe0) >> 5);
+ extension = (data[10] & 0x10) >> 4;
+ ext_type = (data[10] & 0xe0) >> 5;
+ lfe = (data[10] >> 1) & 0x3;
+ m_info.m_dataIsLE = false;
+ bits = 16;
+ break;
+
+ // 16bit LE
+ case DTS_PREAMBLE_16LE:
+ dtsBlocks = (((data[5] & 0x1) << 7) | ((data[4] & 0xFC) >> 2)) + 1;
+ m_fsize = (((((data[4] & 0x3) << 8) | data[7]) << 4) | ((data[6] & 0xF0) >> 4)) + 1;
+ amode = ((data[6] & 0x0F) << 2) | ((data[9] & 0xC0) >> 6);
+ sfreq = (data[9] & 0x3C) >> 2;
+ target_rate = ((data[9] & 0x03) << 3) | ((data[8] & 0xe0) >> 5);
+ extension = (data[11] & 0x10) >> 4;
+ ext_type = (data[11] & 0xe0) >> 5;
+ lfe = (data[11] >> 1) & 0x3;
+ m_info.m_dataIsLE = true;
+ bits = 16;
+ break;
+
+ default:
+ continue;
+ }
+
+ if (sfreq == 0 || sfreq >= DTS_SFREQ_COUNT)
+ continue;
+
+ // make sure the framesize is sane
+ if (m_fsize < 96 || m_fsize > 16384)
+ continue;
+
+ bool invalid = false;
+ CAEStreamInfo::DataType dataType;
+ switch (dtsBlocks << 5)
+ {
+ case 512:
+ dataType = CAEStreamInfo::STREAM_TYPE_DTS_512;
+ break;
+ case 1024:
+ dataType = CAEStreamInfo::STREAM_TYPE_DTS_1024;
+ break;
+ case 2048:
+ dataType = CAEStreamInfo::STREAM_TYPE_DTS_2048;
+ break;
+ default:
+ invalid = true;
+ break;
+ }
+
+ if (invalid)
+ continue;
+
+ // adjust the fsize for 14 bit streams
+ if (bits == 14)
+ m_fsize = m_fsize / 14 * 16;
+
+ // we need enough data to check for DTS-HD
+ if (size - skip < m_fsize + 10)
+ {
+ // we can assume DTS sync at this point
+ m_syncFunc = &CAEStreamParser::SyncDTS;
+ m_needBytes = m_fsize + 10;
+ m_fsize = 0;
+
+ return skip;
+ }
+
+ // look for DTS-HD
+ hd_sync = (data[m_fsize] << 24) | (data[m_fsize + 1] << 16) | (data[m_fsize + 2] << 8) |
+ data[m_fsize + 3];
+ if (hd_sync == DTS_PREAMBLE_HD)
+ {
+ int hd_size;
+ bool blownup = (data[m_fsize + 5] & 0x20) != 0;
+ if (blownup)
+ hd_size = (((data[m_fsize + 6] & 0x01) << 19) | (data[m_fsize + 7] << 11) |
+ (data[m_fsize + 8] << 3) | ((data[m_fsize + 9] & 0xe0) >> 5)) +
+ 1;
+ else
+ hd_size = (((data[m_fsize + 6] & 0x1f) << 11) | (data[m_fsize + 7] << 3) |
+ ((data[m_fsize + 8] & 0xe0) >> 5)) +
+ 1;
+
+ int header_size;
+ if (blownup)
+ header_size = (((data[m_fsize + 5] & 0x1f) << 7) | ((data[m_fsize + 6] & 0xfe) >> 1)) + 1;
+ else
+ header_size = (((data[m_fsize + 5] & 0x1f) << 3) | ((data[m_fsize + 6] & 0xe0) >> 5)) + 1;
+
+ hd_sync = data[m_fsize + header_size] << 24 | data[m_fsize + header_size + 1] << 16 |
+ data[m_fsize + header_size + 2] << 8 | data[m_fsize + header_size + 3];
+
+ // set the type according to core or not
+ if (m_coreOnly)
+ dataType = CAEStreamInfo::STREAM_TYPE_DTSHD_CORE;
+ else if (hd_sync == DTS_PREAMBLE_XLL)
+ dataType = CAEStreamInfo::STREAM_TYPE_DTSHD_MA;
+ else if (hd_sync == DTS_PREAMBLE_XCH || hd_sync == DTS_PREAMBLE_XXCH ||
+ hd_sync == DTS_PREAMBLE_X96K || hd_sync == DTS_PREAMBLE_XBR ||
+ hd_sync == DTS_PREAMBLE_LBR)
+ dataType = CAEStreamInfo::STREAM_TYPE_DTSHD;
+ else
+ dataType = m_info.m_type;
+
+ m_coreSize = m_fsize;
+ m_fsize += hd_size;
+ }
+
+ unsigned int sampleRate = DTSSampleRates[sfreq];
+ if (!m_hasSync || skip || dataType != m_info.m_type || sampleRate != m_info.m_sampleRate ||
+ dtsBlocks != m_dtsBlocks)
+ {
+ m_hasSync = true;
+ m_info.m_type = dataType;
+ m_info.m_sampleRate = sampleRate;
+ m_dtsBlocks = dtsBlocks;
+ m_info.m_channels = DTSChannels[amode] + (lfe ? 1 : 0);
+ m_syncFunc = &CAEStreamParser::SyncDTS;
+ m_info.m_repeat = 1;
+
+ if (dataType == CAEStreamInfo::STREAM_TYPE_DTSHD_MA)
+ {
+ m_info.m_channels += 2; // FIXME: this needs to be read out, not sure how to do that yet
+ m_info.m_dtsPeriod = (192000 * (8 >> 1)) * (m_dtsBlocks << 5) / m_info.m_sampleRate;
+ }
+ else if (dataType == CAEStreamInfo::STREAM_TYPE_DTSHD)
+ {
+ m_info.m_dtsPeriod = (192000 * (2 >> 1)) * (m_dtsBlocks << 5) / m_info.m_sampleRate;
+ }
+ else
+ {
+ m_info.m_dtsPeriod =
+ (m_info.m_sampleRate * (2 >> 1)) * (m_dtsBlocks << 5) / m_info.m_sampleRate;
+ }
+
+ std::string type;
+ switch (dataType)
+ {
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ type = "dtsHD";
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ type = "dtsHD MA";
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ type = "dtsHD (core)";
+ break;
+ default:
+ type = "dts";
+ break;
+ }
+
+ if (extension)
+ {
+ switch (ext_type)
+ {
+ case 0:
+ type += " XCH";
+ break;
+ case 2:
+ type += " X96";
+ break;
+ case 6:
+ type += " XXCH";
+ break;
+ default:
+ type += " ext unknown";
+ break;
+ }
+ }
+
+ CLog::Log(LOGINFO,
+ "CAEStreamParser::SyncDTS - {} stream detected ({} channels, {}Hz, {}bit {}, "
+ "period: {}, syncword: 0x{:x}, target rate: 0x{:x}, framesize {}))",
+ type, m_info.m_channels, m_info.m_sampleRate, bits, m_info.m_dataIsLE ? "LE" : "BE",
+ m_info.m_dtsPeriod, hd_sync, target_rate, m_fsize);
+ }
+
+ return skip;
+ }
+
+ // lost sync
+ CLog::Log(LOGINFO, "CAEStreamParser::SyncDTS - DTS sync lost");
+ m_hasSync = false;
+ return skip;
+}
+
+inline unsigned int CAEStreamParser::GetTrueHDChannels(const uint16_t chanmap)
+{
+ int channels = 0;
+ for (int i = 0; i < 13; ++i)
+ channels += THDChanMap[i] * ((chanmap >> i) & 1);
+ return channels;
+}
+
+unsigned int CAEStreamParser::SyncTrueHD(uint8_t* data, unsigned int size)
+{
+ unsigned int left = size;
+ unsigned int skip = 0;
+
+ // if MLP
+ for (; left; ++skip, ++data, --left)
+ {
+ // if we dont have sync and there is less the 8 bytes, then break out
+ if (!m_hasSync && left < 8)
+ return size;
+
+ // if its a major audio unit
+ uint16_t length = ((data[0] & 0x0F) << 8 | data[1]) << 1;
+ uint32_t syncword = ((((data[4] << 8 | data[5]) << 8) | data[6]) << 8) | data[7];
+ if (syncword == 0xf8726fba)
+ {
+ // we need 32 bytes to sync on a master audio unit
+ if (left < 32)
+ return skip;
+
+ // get the rate and ensure its valid
+ int rate = (data[8] & 0xf0) >> 4;
+ if (rate == 0xF)
+ continue;
+
+ unsigned int major_sync_size = 28;
+ if (data[29] & 1)
+ {
+ // extension(s) present, look up count
+ int extension_count = data[30] >> 4;
+ major_sync_size += 2 + extension_count * 2;
+ }
+
+ if (left < 4 + major_sync_size)
+ return skip;
+
+ // verify the crc of the audio unit
+ uint16_t crc = av_crc(m_crcTrueHD, 0, data + 4, major_sync_size - 4);
+ crc ^= (data[4 + major_sync_size - 3] << 8) | data[4 + major_sync_size - 4];
+ if (((data[4 + major_sync_size - 1] << 8) | data[4 + major_sync_size - 2]) != crc)
+ continue;
+
+ // get the sample rate and substreams, we have a valid master audio unit
+ m_info.m_sampleRate = (rate & 0x8 ? 44100 : 48000) << (rate & 0x7);
+ m_substreams = (data[20] & 0xF0) >> 4;
+
+ // get the number of encoded channels
+ uint16_t channel_map = ((data[10] & 0x1F) << 8) | data[11];
+ if (!channel_map)
+ channel_map = (data[9] << 1) | (data[10] >> 7);
+ m_info.m_channels = CAEStreamParser::GetTrueHDChannels(channel_map);
+
+ if (!m_hasSync)
+ CLog::Log(LOGINFO,
+ "CAEStreamParser::SyncTrueHD - TrueHD stream detected ({} channels, {}Hz)",
+ m_info.m_channels, m_info.m_sampleRate);
+
+ m_hasSync = true;
+ m_fsize = length;
+ m_info.m_type = CAEStreamInfo::STREAM_TYPE_TRUEHD;
+ m_syncFunc = &CAEStreamParser::SyncTrueHD;
+ m_info.m_repeat = 1;
+ return skip;
+ }
+ else
+ {
+ // we cant sink to a subframe until we have the information from a master audio unit
+ if (!m_hasSync)
+ continue;
+
+ // if there is not enough data left to verify the packet, just return the skip amount
+ if (left < (unsigned int)m_substreams * 4)
+ return skip;
+
+ // verify the parity
+ int p = 0;
+ uint8_t check = 0;
+ for (int i = -1; i < m_substreams; ++i)
+ {
+ check ^= data[p++];
+ check ^= data[p++];
+ if (i == -1 || data[p - 2] & 0x80)
+ {
+ check ^= data[p++];
+ check ^= data[p++];
+ }
+ }
+
+ // if the parity nibble does not match
+ if ((((check >> 4) ^ check) & 0xF) != 0xF)
+ {
+ // lost sync
+ m_hasSync = false;
+ CLog::Log(LOGINFO, "CAEStreamParser::SyncTrueHD - Sync Lost");
+ continue;
+ }
+ else
+ {
+ m_fsize = length;
+ return skip;
+ }
+ }
+ }
+
+ // lost sync
+ m_hasSync = false;
+ return skip;
+}
diff --git a/xbmc/cores/AudioEngine/Utils/AEStreamInfo.h b/xbmc/cores/AudioEngine/Utils/AEStreamInfo.h
new file mode 100644
index 0000000..dff809d
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEStreamInfo.h
@@ -0,0 +1,102 @@
+/*
+ * 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 "AEPackIEC61937.h"
+#include "AEChannelInfo.h"
+#include <stdint.h>
+
+/* ffmpeg re-defines this, so undef it to squash the warning */
+#undef restrict
+
+extern "C" {
+#include <libavutil/crc.h>
+}
+
+class CAEStreamInfo
+{
+public:
+ double GetDuration() const;
+ bool operator==(const CAEStreamInfo& info) const;
+
+ enum DataType
+ {
+ STREAM_TYPE_NULL,
+ STREAM_TYPE_AC3,
+ STREAM_TYPE_DTS_512,
+ STREAM_TYPE_DTS_1024,
+ STREAM_TYPE_DTS_2048,
+ STREAM_TYPE_DTSHD,
+ STREAM_TYPE_DTSHD_CORE,
+ STREAM_TYPE_EAC3,
+ STREAM_TYPE_MLP,
+ STREAM_TYPE_TRUEHD,
+ STREAM_TYPE_DTSHD_MA
+ };
+ DataType m_type = STREAM_TYPE_NULL;
+ unsigned int m_sampleRate;
+ unsigned int m_channels;
+ bool m_dataIsLE = true;
+ unsigned int m_dtsPeriod = 0;
+ unsigned int m_repeat = 0;
+ unsigned int m_ac3FrameSize = 0;
+};
+
+class CAEStreamParser
+{
+public:
+
+ CAEStreamParser();
+ ~CAEStreamParser() = default;
+
+ int AddData(uint8_t *data, unsigned int size, uint8_t **buffer = NULL, unsigned int *bufferSize = 0);
+
+ void SetCoreOnly(bool value) { m_coreOnly = value; }
+ unsigned int IsValid() const { return m_hasSync; }
+ unsigned int GetSampleRate() const { return m_info.m_sampleRate; }
+ unsigned int GetChannels() const { return m_info.m_channels; }
+ unsigned int GetFrameSize() const { return m_fsize; }
+ // unsigned int GetDTSBlocks() const { return m_dtsBlocks; }
+ unsigned int GetDTSPeriod() const { return m_info.m_dtsPeriod; }
+ unsigned int GetEAC3BlocksDiv() const { return m_info.m_repeat; }
+ enum CAEStreamInfo::DataType GetDataType() const { return m_info.m_type; }
+ bool IsLittleEndian() const { return m_info.m_dataIsLE; }
+ unsigned int GetBufferSize() const { return m_bufferSize; }
+ CAEStreamInfo& GetStreamInfo() { return m_info; }
+ void Reset();
+
+private:
+ uint8_t m_buffer[MAX_IEC61937_PACKET];
+ unsigned int m_bufferSize = 0;
+ unsigned int m_skipBytes = 0;
+
+ typedef unsigned int (CAEStreamParser::*ParseFunc)(uint8_t *data, unsigned int size);
+
+ CAEStreamInfo m_info;
+ bool m_coreOnly = false;
+ unsigned int m_needBytes = 0;
+ ParseFunc m_syncFunc;
+ bool m_hasSync = false;
+
+ unsigned int m_coreSize = 0; /* core size for dtsHD */
+ unsigned int m_dtsBlocks = 0;
+ unsigned int m_fsize = 0;
+ int m_substreams = 0; /* used for TrueHD */
+ AVCRC m_crcTrueHD[1024]; /* TrueHD crc table */
+
+ void GetPacket(uint8_t **buffer, unsigned int *bufferSize);
+ unsigned int DetectType(uint8_t *data, unsigned int size);
+ bool TrySyncAC3(uint8_t *data, unsigned int size, bool resyncing, bool wantEAC3dependent);
+ unsigned int SyncAC3(uint8_t *data, unsigned int size);
+ unsigned int SyncDTS(uint8_t *data, unsigned int size);
+ unsigned int SyncTrueHD(uint8_t *data, unsigned int size);
+
+ static unsigned int GetTrueHDChannels(const uint16_t chanmap);
+};
+
diff --git a/xbmc/cores/AudioEngine/Utils/AEUtil.cpp b/xbmc/cores/AudioEngine/Utils/AEUtil.cpp
new file mode 100644
index 0000000..bfa2cf9
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEUtil.cpp
@@ -0,0 +1,583 @@
+/*
+ * 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.
+ */
+#ifndef __STDC_LIMIT_MACROS
+ #define __STDC_LIMIT_MACROS
+#endif
+
+#include "AEUtil.h"
+#include "utils/log.h"
+#include "utils/TimeUtils.h"
+
+#include <cassert>
+
+#if defined(HAVE_SSE) && defined(__SSE__)
+#include <xmmintrin.h>
+#endif
+
+extern "C" {
+#include <libavutil/channel_layout.h>
+}
+
+void AEDelayStatus::SetDelay(double d)
+{
+ delay = d;
+ maxcorrection = d;
+ tick = CurrentHostCounter();
+}
+
+double AEDelayStatus::GetDelay() const
+{
+ double d = 0;
+ if (tick)
+ d = (double)(CurrentHostCounter() - tick) / CurrentHostFrequency();
+ if (d > maxcorrection)
+ d = maxcorrection;
+
+ return delay - d;
+}
+
+CAEChannelInfo CAEUtil::GuessChLayout(const unsigned int channels)
+{
+ CLog::Log(LOGWARNING, "CAEUtil::GuessChLayout - "
+ "This method should really never be used, please fix the code that called this");
+
+ CAEChannelInfo result;
+ if (channels < 1 || channels > 8)
+ return result;
+
+ switch (channels)
+ {
+ case 1: result = AE_CH_LAYOUT_1_0; break;
+ case 2: result = AE_CH_LAYOUT_2_0; break;
+ case 3: result = AE_CH_LAYOUT_3_0; break;
+ case 4: result = AE_CH_LAYOUT_4_0; break;
+ case 5: result = AE_CH_LAYOUT_5_0; break;
+ case 6: result = AE_CH_LAYOUT_5_1; break;
+ case 7: result = AE_CH_LAYOUT_7_0; break;
+ case 8: result = AE_CH_LAYOUT_7_1; break;
+ }
+
+ return result;
+}
+
+const char* CAEUtil::GetStdChLayoutName(const enum AEStdChLayout layout)
+{
+ if (layout < 0 || layout >= AE_CH_LAYOUT_MAX)
+ return "UNKNOWN";
+
+ static const char* layouts[AE_CH_LAYOUT_MAX] =
+ {
+ "1.0",
+ "2.0", "2.1", "3.0", "3.1", "4.0",
+ "4.1", "5.0", "5.1", "7.0", "7.1"
+ };
+
+ return layouts[layout];
+}
+
+unsigned int CAEUtil::DataFormatToBits(const enum AEDataFormat dataFormat)
+{
+ if (dataFormat < 0 || dataFormat >= AE_FMT_MAX)
+ return 0;
+
+ static const unsigned int formats[AE_FMT_MAX] =
+ {
+ 8, /* U8 */
+
+ 16, /* S16BE */
+ 16, /* S16LE */
+ 16, /* S16NE */
+
+ 32, /* S32BE */
+ 32, /* S32LE */
+ 32, /* S32NE */
+
+ 32, /* S24BE */
+ 32, /* S24LE */
+ 32, /* S24NE */
+ 32, /* S24NER */
+
+ 24, /* S24BE3 */
+ 24, /* S24LE3 */
+ 24, /* S24NE3 */
+
+ sizeof(double) << 3, /* DOUBLE */
+ sizeof(float ) << 3, /* FLOAT */
+
+ 8, /* RAW */
+
+ 8, /* U8P */
+ 16, /* S16NEP */
+ 32, /* S32NEP */
+ 32, /* S24NEP */
+ 32, /* S24NERP*/
+ 24, /* S24NE3P*/
+ sizeof(double) << 3, /* DOUBLEP */
+ sizeof(float ) << 3 /* FLOATP */
+ };
+
+ return formats[dataFormat];
+}
+
+unsigned int CAEUtil::DataFormatToUsedBits(const enum AEDataFormat dataFormat)
+{
+ if (dataFormat == AE_FMT_S24BE4 || dataFormat == AE_FMT_S24LE4 ||
+ dataFormat == AE_FMT_S24NE4 || dataFormat == AE_FMT_S24NE4MSB)
+ return 24;
+ else
+ return DataFormatToBits(dataFormat);
+}
+
+unsigned int CAEUtil::DataFormatToDitherBits(const enum AEDataFormat dataFormat)
+{
+ if (dataFormat == AE_FMT_S24NE4MSB)
+ return 8;
+ if (dataFormat == AE_FMT_S24NE3)
+ return -8;
+ else
+ return 0;
+}
+
+const char* CAEUtil::StreamTypeToStr(const enum CAEStreamInfo::DataType dataType)
+{
+ switch (dataType)
+ {
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ return "STREAM_TYPE_AC3";
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ return "STREAM_TYPE_DTSHD";
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ return "STREAM_TYPE_DTSHD_MA";
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ return "STREAM_TYPE_DTSHD_CORE";
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ return "STREAM_TYPE_DTS_1024";
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ return "STREAM_TYPE_DTS_2048";
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ return "STREAM_TYPE_DTS_512";
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ return "STREAM_TYPE_EAC3";
+ case CAEStreamInfo::STREAM_TYPE_MLP:
+ return "STREAM_TYPE_MLP";
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ return "STREAM_TYPE_TRUEHD";
+
+ default:
+ return "STREAM_TYPE_NULL";
+ }
+}
+
+const char* CAEUtil::DataFormatToStr(const enum AEDataFormat dataFormat)
+{
+ if (dataFormat < 0 || dataFormat >= AE_FMT_MAX)
+ return "UNKNOWN";
+
+ static const char *formats[AE_FMT_MAX] =
+ {
+ "AE_FMT_U8",
+
+ "AE_FMT_S16BE",
+ "AE_FMT_S16LE",
+ "AE_FMT_S16NE",
+
+ "AE_FMT_S32BE",
+ "AE_FMT_S32LE",
+ "AE_FMT_S32NE",
+
+ "AE_FMT_S24BE4",
+ "AE_FMT_S24LE4",
+ "AE_FMT_S24NE4", /* S24 in 4 bytes */
+ "AE_FMT_S24NE4MSB",
+
+ "AE_FMT_S24BE3",
+ "AE_FMT_S24LE3",
+ "AE_FMT_S24NE3", /* S24 in 3 bytes */
+
+ "AE_FMT_DOUBLE",
+ "AE_FMT_FLOAT",
+
+ "AE_FMT_RAW",
+
+ /* planar formats */
+ "AE_FMT_U8P",
+ "AE_FMT_S16NEP",
+ "AE_FMT_S32NEP",
+ "AE_FMT_S24NE4P",
+ "AE_FMT_S24NE4MSBP",
+ "AE_FMT_S24NE3P",
+ "AE_FMT_DOUBLEP",
+ "AE_FMT_FLOATP"
+ };
+
+ return formats[dataFormat];
+}
+
+#if defined(HAVE_SSE) && defined(__SSE__)
+void CAEUtil::SSEMulArray(float *data, const float mul, uint32_t count)
+{
+ const __m128 m = _mm_set_ps1(mul);
+
+ /* work around invalid alignment */
+ while (((uintptr_t)data & 0xF) && count > 0)
+ {
+ data[0] *= mul;
+ ++data;
+ --count;
+ }
+
+ uint32_t even = count & ~0x3;
+ for (uint32_t i = 0; i < even; i+=4, data+=4)
+ {
+ __m128 to = _mm_load_ps(data);
+ *(__m128*)data = _mm_mul_ps (to, m);
+ }
+
+ if (even != count)
+ {
+ uint32_t odd = count - even;
+ if (odd == 1)
+ data[0] *= mul;
+ else
+ {
+ __m128 to;
+ if (odd == 2)
+ {
+ to = _mm_setr_ps(data[0], data[1], 0, 0);
+ __m128 ou = _mm_mul_ps(to, m);
+ data[0] = ((float*)&ou)[0];
+ data[1] = ((float*)&ou)[1];
+ }
+ else
+ {
+ to = _mm_setr_ps(data[0], data[1], data[2], 0);
+ __m128 ou = _mm_mul_ps(to, m);
+ data[0] = ((float*)&ou)[0];
+ data[1] = ((float*)&ou)[1];
+ data[2] = ((float*)&ou)[2];
+ }
+ }
+ }
+}
+
+void CAEUtil::SSEMulAddArray(float *data, float *add, const float mul, uint32_t count)
+{
+ const __m128 m = _mm_set_ps1(mul);
+
+ /* work around invalid alignment */
+ while ((((uintptr_t)data & 0xF) || ((uintptr_t)add & 0xF)) && count > 0)
+ {
+ data[0] += add[0] * mul;
+ ++add;
+ ++data;
+ --count;
+ }
+
+ uint32_t even = count & ~0x3;
+ for (uint32_t i = 0; i < even; i+=4, data+=4, add+=4)
+ {
+ __m128 ad = _mm_load_ps(add );
+ __m128 to = _mm_load_ps(data);
+ *(__m128*)data = _mm_add_ps (to, _mm_mul_ps(ad, m));
+ }
+
+ if (even != count)
+ {
+ uint32_t odd = count - even;
+ if (odd == 1)
+ data[0] += add[0] * mul;
+ else
+ {
+ __m128 ad;
+ __m128 to;
+ if (odd == 2)
+ {
+ ad = _mm_setr_ps(add [0], add [1], 0, 0);
+ to = _mm_setr_ps(data[0], data[1], 0, 0);
+ __m128 ou = _mm_add_ps(to, _mm_mul_ps(ad, m));
+ data[0] = ((float*)&ou)[0];
+ data[1] = ((float*)&ou)[1];
+ }
+ else
+ {
+ ad = _mm_setr_ps(add [0], add [1], add [2], 0);
+ to = _mm_setr_ps(data[0], data[1], data[2], 0);
+ __m128 ou = _mm_add_ps(to, _mm_mul_ps(ad, m));
+ data[0] = ((float*)&ou)[0];
+ data[1] = ((float*)&ou)[1];
+ data[2] = ((float*)&ou)[2];
+ }
+ }
+ }
+}
+#endif
+
+inline float CAEUtil::SoftClamp(const float x)
+{
+#if 1
+ /*
+ This is a rational function to approximate a tanh-like soft clipper.
+ It is based on the pade-approximation of the tanh function with tweaked coefficients.
+ See: http://www.musicdsp.org/showone.php?id=238
+ */
+ if (x < -3.0f)
+ return -1.0f;
+ else if (x > 3.0f)
+ return 1.0f;
+ float y = x * x;
+ return x * (27.0f + y) / (27.0f + 9.0f * y);
+#else
+ /* slower method using tanh, but more accurate */
+
+ static const double k = 0.9f;
+ /* perform a soft clamp */
+ if (x > k)
+ x = (float) (tanh((x - k) / (1 - k)) * (1 - k) + k);
+ else if (x < -k)
+ x = (float) (tanh((x + k) / (1 - k)) * (1 - k) - k);
+
+ /* hard clamp anything still outside the bounds */
+ if (x > 1.0f)
+ return 1.0f;
+ if (x < -1.0f)
+ return -1.0f;
+
+ /* return the final sample */
+ return x;
+#endif
+}
+
+void CAEUtil::ClampArray(float *data, uint32_t count)
+{
+#if !defined(HAVE_SSE) || !defined(__SSE__)
+ for (uint32_t i = 0; i < count; ++i)
+ data[i] = SoftClamp(data[i]);
+
+#else
+ const __m128 c1 = _mm_set_ps1(27.0f);
+ const __m128 c2 = _mm_set_ps1(27.0f + 9.0f);
+
+ /* work around invalid alignment */
+ while (((uintptr_t)data & 0xF) && count > 0)
+ {
+ data[0] = SoftClamp(data[0]);
+ ++data;
+ --count;
+ }
+
+ uint32_t even = count & ~0x3;
+ for (uint32_t i = 0; i < even; i+=4, data+=4)
+ {
+ /* tanh approx clamp */
+ __m128 dt = _mm_load_ps(data);
+ __m128 tmp = _mm_mul_ps(dt, dt);
+ *(__m128*)data = _mm_div_ps(
+ _mm_mul_ps(
+ dt,
+ _mm_add_ps(c1, tmp)
+ ),
+ _mm_add_ps(c2, tmp)
+ );
+ }
+
+ if (even != count)
+ {
+ uint32_t odd = count - even;
+ if (odd == 1)
+ data[0] = SoftClamp(data[0]);
+ else
+ {
+ __m128 dt;
+ __m128 tmp;
+ __m128 out;
+ if (odd == 2)
+ {
+ /* tanh approx clamp */
+ dt = _mm_setr_ps(data[0], data[1], 0, 0);
+ tmp = _mm_mul_ps(dt, dt);
+ out = _mm_div_ps(
+ _mm_mul_ps(
+ dt,
+ _mm_add_ps(c1, tmp)
+ ),
+ _mm_add_ps(c2, tmp)
+ );
+
+ data[0] = ((float*)&out)[0];
+ data[1] = ((float*)&out)[1];
+ }
+ else
+ {
+ /* tanh approx clamp */
+ dt = _mm_setr_ps(data[0], data[1], data[2], 0);
+ tmp = _mm_mul_ps(dt, dt);
+ out = _mm_div_ps(
+ _mm_mul_ps(
+ dt,
+ _mm_add_ps(c1, tmp)
+ ),
+ _mm_add_ps(c2, tmp)
+ );
+
+ data[0] = ((float*)&out)[0];
+ data[1] = ((float*)&out)[1];
+ data[2] = ((float*)&out)[2];
+ }
+ }
+ }
+#endif
+}
+
+bool CAEUtil::S16NeedsByteSwap(AEDataFormat in, AEDataFormat out)
+{
+ const AEDataFormat nativeFormat =
+#ifdef WORDS_BIGENDIAN
+ AE_FMT_S16BE;
+#else
+ AE_FMT_S16LE;
+#endif
+
+ if (in == AE_FMT_S16NE || (in == AE_FMT_RAW))
+ in = nativeFormat;
+ if (out == AE_FMT_S16NE || (out == AE_FMT_RAW))
+ out = nativeFormat;
+
+ return in != out;
+}
+
+uint64_t CAEUtil::GetAVChannelLayout(const CAEChannelInfo &info)
+{
+ uint64_t channelLayout = 0;
+ if (info.HasChannel(AE_CH_FL)) channelLayout |= AV_CH_FRONT_LEFT;
+ if (info.HasChannel(AE_CH_FR)) channelLayout |= AV_CH_FRONT_RIGHT;
+ if (info.HasChannel(AE_CH_FC)) channelLayout |= AV_CH_FRONT_CENTER;
+ if (info.HasChannel(AE_CH_LFE)) channelLayout |= AV_CH_LOW_FREQUENCY;
+ if (info.HasChannel(AE_CH_BL)) channelLayout |= AV_CH_BACK_LEFT;
+ if (info.HasChannel(AE_CH_BR)) channelLayout |= AV_CH_BACK_RIGHT;
+ if (info.HasChannel(AE_CH_FLOC)) channelLayout |= AV_CH_FRONT_LEFT_OF_CENTER;
+ if (info.HasChannel(AE_CH_FROC)) channelLayout |= AV_CH_FRONT_RIGHT_OF_CENTER;
+ if (info.HasChannel(AE_CH_BC)) channelLayout |= AV_CH_BACK_CENTER;
+ if (info.HasChannel(AE_CH_SL)) channelLayout |= AV_CH_SIDE_LEFT;
+ if (info.HasChannel(AE_CH_SR)) channelLayout |= AV_CH_SIDE_RIGHT;
+ if (info.HasChannel(AE_CH_TC)) channelLayout |= AV_CH_TOP_CENTER;
+ if (info.HasChannel(AE_CH_TFL)) channelLayout |= AV_CH_TOP_FRONT_LEFT;
+ if (info.HasChannel(AE_CH_TFC)) channelLayout |= AV_CH_TOP_FRONT_CENTER;
+ if (info.HasChannel(AE_CH_TFR)) channelLayout |= AV_CH_TOP_FRONT_RIGHT;
+ if (info.HasChannel(AE_CH_TBL)) channelLayout |= AV_CH_TOP_BACK_LEFT;
+ if (info.HasChannel(AE_CH_TBC)) channelLayout |= AV_CH_TOP_BACK_CENTER;
+ if (info.HasChannel(AE_CH_TBR)) channelLayout |= AV_CH_TOP_BACK_RIGHT;
+
+ return channelLayout;
+}
+
+CAEChannelInfo CAEUtil::GetAEChannelLayout(uint64_t layout)
+{
+ CAEChannelInfo channelLayout;
+ channelLayout.Reset();
+
+ if (layout & AV_CH_FRONT_LEFT) channelLayout += AE_CH_FL;
+ if (layout & AV_CH_FRONT_RIGHT) channelLayout += AE_CH_FR;
+ if (layout & AV_CH_FRONT_CENTER) channelLayout += AE_CH_FC;
+ if (layout & AV_CH_LOW_FREQUENCY) channelLayout += AE_CH_LFE;
+ if (layout & AV_CH_BACK_LEFT) channelLayout += AE_CH_BL;
+ if (layout & AV_CH_BACK_RIGHT) channelLayout += AE_CH_BR;
+ if (layout & AV_CH_FRONT_LEFT_OF_CENTER) channelLayout += AE_CH_FLOC;
+ if (layout & AV_CH_FRONT_RIGHT_OF_CENTER) channelLayout += AE_CH_FROC;
+ if (layout & AV_CH_BACK_CENTER) channelLayout += AE_CH_BC;
+ if (layout & AV_CH_SIDE_LEFT) channelLayout += AE_CH_SL;
+ if (layout & AV_CH_SIDE_RIGHT) channelLayout += AE_CH_SR;
+ if (layout & AV_CH_TOP_CENTER) channelLayout += AE_CH_TC;
+ if (layout & AV_CH_TOP_FRONT_LEFT) channelLayout += AE_CH_TFL;
+ if (layout & AV_CH_TOP_FRONT_CENTER) channelLayout += AE_CH_TFC;
+ if (layout & AV_CH_TOP_FRONT_RIGHT) channelLayout += AE_CH_TFR;
+ if (layout & AV_CH_TOP_BACK_LEFT) channelLayout += AE_CH_BL;
+ if (layout & AV_CH_TOP_BACK_CENTER) channelLayout += AE_CH_BC;
+ if (layout & AV_CH_TOP_BACK_RIGHT) channelLayout += AE_CH_BR;
+
+ return channelLayout;
+}
+
+AVSampleFormat CAEUtil::GetAVSampleFormat(AEDataFormat format)
+{
+ switch (format)
+ {
+ case AEDataFormat::AE_FMT_U8:
+ return AV_SAMPLE_FMT_U8;
+ case AEDataFormat::AE_FMT_S16NE:
+ return AV_SAMPLE_FMT_S16;
+ case AEDataFormat::AE_FMT_S32NE:
+ return AV_SAMPLE_FMT_S32;
+ case AEDataFormat::AE_FMT_S24NE4:
+ return AV_SAMPLE_FMT_S32;
+ case AEDataFormat::AE_FMT_S24NE4MSB:
+ return AV_SAMPLE_FMT_S32;
+ case AEDataFormat::AE_FMT_S24NE3:
+ return AV_SAMPLE_FMT_S32;
+ case AEDataFormat::AE_FMT_FLOAT:
+ return AV_SAMPLE_FMT_FLT;
+ case AEDataFormat::AE_FMT_DOUBLE:
+ return AV_SAMPLE_FMT_DBL;
+ case AEDataFormat::AE_FMT_U8P:
+ return AV_SAMPLE_FMT_U8P;
+ case AEDataFormat::AE_FMT_S16NEP:
+ return AV_SAMPLE_FMT_S16P;
+ case AEDataFormat::AE_FMT_S32NEP:
+ return AV_SAMPLE_FMT_S32P;
+ case AEDataFormat::AE_FMT_S24NE4P:
+ return AV_SAMPLE_FMT_S32P;
+ case AEDataFormat::AE_FMT_S24NE4MSBP:
+ return AV_SAMPLE_FMT_S32P;
+ case AEDataFormat::AE_FMT_S24NE3P:
+ return AV_SAMPLE_FMT_S32P;
+ case AEDataFormat::AE_FMT_FLOATP:
+ return AV_SAMPLE_FMT_FLTP;
+ case AEDataFormat::AE_FMT_DOUBLEP:
+ return AV_SAMPLE_FMT_DBLP;
+ case AEDataFormat::AE_FMT_RAW:
+ return AV_SAMPLE_FMT_U8;
+ default:
+ {
+ if (AE_IS_PLANAR(format))
+ return AV_SAMPLE_FMT_FLTP;
+ else
+ return AV_SAMPLE_FMT_FLT;
+ }
+ }
+}
+
+uint64_t CAEUtil::GetAVChannel(enum AEChannel aechannel)
+{
+ switch (aechannel)
+ {
+ case AE_CH_FL: return AV_CH_FRONT_LEFT;
+ case AE_CH_FR: return AV_CH_FRONT_RIGHT;
+ case AE_CH_FC: return AV_CH_FRONT_CENTER;
+ case AE_CH_LFE: return AV_CH_LOW_FREQUENCY;
+ case AE_CH_BL: return AV_CH_BACK_LEFT;
+ case AE_CH_BR: return AV_CH_BACK_RIGHT;
+ case AE_CH_FLOC: return AV_CH_FRONT_LEFT_OF_CENTER;
+ case AE_CH_FROC: return AV_CH_FRONT_RIGHT_OF_CENTER;
+ case AE_CH_BC: return AV_CH_BACK_CENTER;
+ case AE_CH_SL: return AV_CH_SIDE_LEFT;
+ case AE_CH_SR: return AV_CH_SIDE_RIGHT;
+ case AE_CH_TC: return AV_CH_TOP_CENTER;
+ case AE_CH_TFL: return AV_CH_TOP_FRONT_LEFT;
+ case AE_CH_TFC: return AV_CH_TOP_FRONT_CENTER;
+ case AE_CH_TFR: return AV_CH_TOP_FRONT_RIGHT;
+ case AE_CH_TBL: return AV_CH_TOP_BACK_LEFT;
+ case AE_CH_TBC: return AV_CH_TOP_BACK_CENTER;
+ case AE_CH_TBR: return AV_CH_TOP_BACK_RIGHT;
+ default:
+ return 0;
+ }
+}
+
+int CAEUtil::GetAVChannelIndex(enum AEChannel aechannel, uint64_t layout)
+{
+ return av_get_channel_layout_channel_index(layout, GetAVChannel(aechannel));
+}
diff --git a/xbmc/cores/AudioEngine/Utils/AEUtil.h b/xbmc/cores/AudioEngine/Utils/AEUtil.h
new file mode 100644
index 0000000..034e115
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Utils/AEUtil.h
@@ -0,0 +1,176 @@
+/*
+ * 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 "AEAudioFormat.h"
+#include "PlatformDefs.h"
+#include <math.h>
+
+extern "C" {
+#include <libavutil/samplefmt.h>
+}
+
+// AV sync options
+enum AVSync
+{
+ SYNC_DISCON = 0,
+ SYNC_RESAMPLE
+};
+
+struct AEDelayStatus
+{
+ void SetDelay(double d);
+ double GetDelay() const;
+
+ double delay = 0.0; // delay in sink currently
+ double maxcorrection = 0.0; // time correction must not be greater than sink delay
+ int64_t tick = 0; // timestamp when delay was calculated
+};
+
+/**
+ * @brief lockless consistency guaranteeer
+ *
+ * Requires write to be a higher priority thread
+ *
+ * use in writer:
+ * m_locker.enter();
+ * update_stuff();
+ * m_locker.leave();
+ *
+ * use in reader:
+ * CAESpinLock lock(m_locker);
+ * do {
+ * get_stuff();
+ * } while(lock.retry());
+ */
+
+class CAESpinSection
+{
+public:
+ void enter() { m_enter++; }
+ void leave() { m_leave = m_enter; }
+
+protected:
+ friend class CAESpinLock;
+ volatile unsigned int m_enter = 0;
+ volatile unsigned int m_leave = 0;
+};
+
+class CAESpinLock
+{
+public:
+ explicit CAESpinLock(CAESpinSection& section)
+ : m_section(section)
+ , m_begin(section.m_enter)
+ {}
+
+ bool retry()
+ {
+ if(m_section.m_enter != m_begin
+ || m_section.m_enter != m_section.m_leave)
+ {
+ m_begin = m_section.m_enter;
+ return true;
+ }
+ else
+ return false;
+ }
+
+private:
+ CAESpinSection& m_section;
+ unsigned int m_begin;
+};
+
+class CAEUtil
+{
+private:
+
+ static float SoftClamp(const float x);
+
+public:
+ static CAEChannelInfo GuessChLayout (const unsigned int channels);
+ static const char* GetStdChLayoutName(const enum AEStdChLayout layout);
+ static unsigned int DataFormatToBits (const enum AEDataFormat dataFormat);
+ static unsigned int DataFormatToUsedBits (const enum AEDataFormat dataFormat);
+ static unsigned int DataFormatToDitherBits(const enum AEDataFormat dataFormat);
+ static const char* DataFormatToStr (const enum AEDataFormat dataFormat);
+ static const char* StreamTypeToStr(const enum CAEStreamInfo::DataType dataType);
+
+ /*! \brief convert a volume percentage (as a proportion) to a dB gain
+ We assume a dB range of 60dB, i.e. assume that 0% volume corresponds
+ to a reduction of 60dB.
+ \param value the volume from 0..1
+ \return the corresponding gain in dB from -60dB .. 0dB.
+ \sa GainToScale
+ */
+ static inline float PercentToGain(const float value)
+ {
+ static const float db_range = 60.0f;
+ return (value - 1)*db_range;
+ }
+
+ /*! \brief convert a dB gain to volume percentage (as a proportion)
+ We assume a dB range of 60dB, i.e. assume that 0% volume corresponds
+ to a reduction of 60dB.
+ \param the corresponding gain in dB from -60dB .. 0dB.
+ \return value the volume from 0..1
+ \sa ScaleToGain
+ */
+ static inline float GainToPercent(const float gain)
+ {
+ static const float db_range = 60.0f;
+ return 1+(gain/db_range);
+ }
+
+ /*! \brief convert a dB gain to a scale factor for audio manipulation
+ Inverts gain = 20 log_10(scale)
+ \param dB the gain in decibels.
+ \return the scale factor (equivalent to a voltage multiplier).
+ \sa PercentToGain
+ */
+ static inline float GainToScale(const float dB)
+ {
+ float val = 0.0f;
+ // we need to make sure that our lowest db returns plain zero
+ if (dB > -60.0f)
+ val = pow(10.0f, dB/20);
+
+ // in order to not introduce computing overhead for nearly zero
+ // values of dB e.g. -0.01 or -0.001 we clamp to top
+ if (val >= 0.99f)
+ val = 1.0f;
+
+ return val;
+ }
+
+ /*! \brief convert a scale factor to dB gain for audio manipulation
+ Inverts GainToScale result
+ \param the scale factor (equivalent to a voltage multiplier).
+ \return dB the gain in decibels.
+ \sa GainToScale
+ */
+ static inline float ScaleToGain(const float scale)
+ {
+ return 20*log10(scale);
+ }
+
+ #if defined(HAVE_SSE) && defined(__SSE__)
+ static void SSEMulArray (float *data, const float mul, uint32_t count);
+ static void SSEMulAddArray (float *data, float *add, const float mul, uint32_t count);
+ #endif
+ static void ClampArray(float *data, uint32_t count);
+
+ static bool S16NeedsByteSwap(AEDataFormat in, AEDataFormat out);
+
+ static uint64_t GetAVChannelLayout(const CAEChannelInfo &info);
+ static CAEChannelInfo GetAEChannelLayout(uint64_t layout);
+ static AVSampleFormat GetAVSampleFormat(AEDataFormat format);
+ static uint64_t GetAVChannel(enum AEChannel aechannel);
+ static int GetAVChannelIndex(enum AEChannel aechannel, uint64_t layout);
+};
diff --git a/xbmc/cores/CMakeLists.txt b/xbmc/cores/CMakeLists.txt
new file mode 100644
index 0000000..f0d6597
--- /dev/null
+++ b/xbmc/cores/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES DataCacheCore.cpp
+ FFmpeg.cpp
+ VideoSettings.cpp)
+
+set(HEADERS DataCacheCore.h
+ EdlEdit.h
+ FFmpeg.h
+ GameSettings.h
+ IPlayer.h
+ IPlayerCallback.h
+ MenuType.h
+ VideoSettings.h)
+
+core_add_library(cores)
diff --git a/xbmc/cores/DataCacheCore.cpp b/xbmc/cores/DataCacheCore.cpp
new file mode 100644
index 0000000..a8a0c4c
--- /dev/null
+++ b/xbmc/cores/DataCacheCore.cpp
@@ -0,0 +1,493 @@
+/*
+ * 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 "DataCacheCore.h"
+
+#include "ServiceBroker.h"
+#include "cores/EdlEdit.h"
+
+#include <mutex>
+#include <utility>
+
+CDataCacheCore::CDataCacheCore() :
+ m_playerVideoInfo {},
+ m_playerAudioInfo {},
+ m_contentInfo {},
+ m_renderInfo {},
+ m_stateInfo {}
+{
+}
+
+CDataCacheCore::~CDataCacheCore() = default;
+
+CDataCacheCore& CDataCacheCore::GetInstance()
+{
+ return CServiceBroker::GetDataCacheCore();
+}
+
+void CDataCacheCore::Reset()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ m_stateInfo.m_speed = 1.0;
+ m_stateInfo.m_tempo = 1.0;
+ m_stateInfo.m_stateSeeking = false;
+ m_stateInfo.m_renderGuiLayer = false;
+ m_stateInfo.m_renderVideoLayer = false;
+ m_playerStateChanged = false;
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_contentSection);
+
+ m_contentInfo.Reset();
+ }
+}
+
+bool CDataCacheCore::HasAVInfoChanges()
+{
+ bool ret = m_hasAVInfoChanges;
+ m_hasAVInfoChanges = false;
+ return ret;
+}
+
+void CDataCacheCore::SignalVideoInfoChange()
+{
+ m_hasAVInfoChanges = true;
+}
+
+void CDataCacheCore::SignalAudioInfoChange()
+{
+ m_hasAVInfoChanges = true;
+}
+
+void CDataCacheCore::SignalSubtitleInfoChange()
+{
+ m_hasAVInfoChanges = true;
+}
+
+void CDataCacheCore::SetVideoDecoderName(std::string name, bool isHw)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ m_playerVideoInfo.decoderName = std::move(name);
+ m_playerVideoInfo.isHwDecoder = isHw;
+}
+
+std::string CDataCacheCore::GetVideoDecoderName()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ return m_playerVideoInfo.decoderName;
+}
+
+bool CDataCacheCore::IsVideoHwDecoder()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ return m_playerVideoInfo.isHwDecoder;
+}
+
+
+void CDataCacheCore::SetVideoDeintMethod(std::string method)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ m_playerVideoInfo.deintMethod = std::move(method);
+}
+
+std::string CDataCacheCore::GetVideoDeintMethod()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ return m_playerVideoInfo.deintMethod;
+}
+
+void CDataCacheCore::SetVideoPixelFormat(std::string pixFormat)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ m_playerVideoInfo.pixFormat = std::move(pixFormat);
+}
+
+std::string CDataCacheCore::GetVideoPixelFormat()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ return m_playerVideoInfo.pixFormat;
+}
+
+void CDataCacheCore::SetVideoStereoMode(std::string mode)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ m_playerVideoInfo.stereoMode = std::move(mode);
+}
+
+std::string CDataCacheCore::GetVideoStereoMode()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ return m_playerVideoInfo.stereoMode;
+}
+
+void CDataCacheCore::SetVideoDimensions(int width, int height)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ m_playerVideoInfo.width = width;
+ m_playerVideoInfo.height = height;
+}
+
+int CDataCacheCore::GetVideoWidth()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ return m_playerVideoInfo.width;
+}
+
+int CDataCacheCore::GetVideoHeight()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ return m_playerVideoInfo.height;
+}
+
+void CDataCacheCore::SetVideoFps(float fps)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ m_playerVideoInfo.fps = fps;
+}
+
+float CDataCacheCore::GetVideoFps()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ return m_playerVideoInfo.fps;
+}
+
+void CDataCacheCore::SetVideoDAR(float dar)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ m_playerVideoInfo.dar = dar;
+}
+
+float CDataCacheCore::GetVideoDAR()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+
+ return m_playerVideoInfo.dar;
+}
+
+void CDataCacheCore::SetVideoInterlaced(bool isInterlaced)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+ m_playerVideoInfo.m_isInterlaced = isInterlaced;
+}
+
+bool CDataCacheCore::IsVideoInterlaced()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoPlayerSection);
+ return m_playerVideoInfo.m_isInterlaced;
+}
+
+// player audio info
+void CDataCacheCore::SetAudioDecoderName(std::string name)
+{
+ std::unique_lock<CCriticalSection> lock(m_audioPlayerSection);
+
+ m_playerAudioInfo.decoderName = std::move(name);
+}
+
+std::string CDataCacheCore::GetAudioDecoderName()
+{
+ std::unique_lock<CCriticalSection> lock(m_audioPlayerSection);
+
+ return m_playerAudioInfo.decoderName;
+}
+
+void CDataCacheCore::SetAudioChannels(std::string channels)
+{
+ std::unique_lock<CCriticalSection> lock(m_audioPlayerSection);
+
+ m_playerAudioInfo.channels = std::move(channels);
+}
+
+std::string CDataCacheCore::GetAudioChannels()
+{
+ std::unique_lock<CCriticalSection> lock(m_audioPlayerSection);
+
+ return m_playerAudioInfo.channels;
+}
+
+void CDataCacheCore::SetAudioSampleRate(int sampleRate)
+{
+ std::unique_lock<CCriticalSection> lock(m_audioPlayerSection);
+
+ m_playerAudioInfo.sampleRate = sampleRate;
+}
+
+int CDataCacheCore::GetAudioSampleRate()
+{
+ std::unique_lock<CCriticalSection> lock(m_audioPlayerSection);
+
+ return m_playerAudioInfo.sampleRate;
+}
+
+void CDataCacheCore::SetAudioBitsPerSample(int bitsPerSample)
+{
+ std::unique_lock<CCriticalSection> lock(m_audioPlayerSection);
+
+ m_playerAudioInfo.bitsPerSample = bitsPerSample;
+}
+
+int CDataCacheCore::GetAudioBitsPerSample()
+{
+ std::unique_lock<CCriticalSection> lock(m_audioPlayerSection);
+
+ return m_playerAudioInfo.bitsPerSample;
+}
+
+void CDataCacheCore::SetEditList(const std::vector<EDL::Edit>& editList)
+{
+ std::unique_lock<CCriticalSection> lock(m_contentSection);
+ m_contentInfo.SetEditList(editList);
+}
+
+const std::vector<EDL::Edit>& CDataCacheCore::GetEditList() const
+{
+ std::unique_lock<CCriticalSection> lock(m_contentSection);
+ return m_contentInfo.GetEditList();
+}
+
+void CDataCacheCore::SetCuts(const std::vector<int64_t>& cuts)
+{
+ std::unique_lock<CCriticalSection> lock(m_contentSection);
+ m_contentInfo.SetCuts(cuts);
+}
+
+const std::vector<int64_t>& CDataCacheCore::GetCuts() const
+{
+ std::unique_lock<CCriticalSection> lock(m_contentSection);
+ return m_contentInfo.GetCuts();
+}
+
+void CDataCacheCore::SetSceneMarkers(const std::vector<int64_t>& sceneMarkers)
+{
+ std::unique_lock<CCriticalSection> lock(m_contentSection);
+ m_contentInfo.SetSceneMarkers(sceneMarkers);
+}
+
+const std::vector<int64_t>& CDataCacheCore::GetSceneMarkers() const
+{
+ std::unique_lock<CCriticalSection> lock(m_contentSection);
+ return m_contentInfo.GetSceneMarkers();
+}
+
+void CDataCacheCore::SetChapters(const std::vector<std::pair<std::string, int64_t>>& chapters)
+{
+ std::unique_lock<CCriticalSection> lock(m_contentSection);
+ m_contentInfo.SetChapters(chapters);
+}
+
+const std::vector<std::pair<std::string, int64_t>>& CDataCacheCore::GetChapters() const
+{
+ std::unique_lock<CCriticalSection> lock(m_contentSection);
+ return m_contentInfo.GetChapters();
+}
+
+void CDataCacheCore::SetRenderClockSync(bool enable)
+{
+ std::unique_lock<CCriticalSection> lock(m_renderSection);
+
+ m_renderInfo.m_isClockSync = enable;
+}
+
+bool CDataCacheCore::IsRenderClockSync()
+{
+ std::unique_lock<CCriticalSection> lock(m_renderSection);
+
+ return m_renderInfo.m_isClockSync;
+}
+
+// player states
+void CDataCacheCore::SeekFinished(int64_t offset)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ m_stateInfo.m_lastSeekTime = std::chrono::system_clock::now();
+ m_stateInfo.m_lastSeekOffset = offset;
+}
+
+int64_t CDataCacheCore::GetSeekOffSet() const
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ return m_stateInfo.m_lastSeekOffset;
+}
+
+bool CDataCacheCore::HasPerformedSeek(int64_t lastSecondInterval) const
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ if (m_stateInfo.m_lastSeekTime == std::chrono::time_point<std::chrono::system_clock>{})
+ {
+ return false;
+ }
+ return (std::chrono::system_clock::now() - m_stateInfo.m_lastSeekTime) <
+ std::chrono::duration_cast<std::chrono::seconds>(
+ std::chrono::duration<int64_t>(lastSecondInterval));
+}
+
+void CDataCacheCore::SetStateSeeking(bool active)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ m_stateInfo.m_stateSeeking = active;
+ m_playerStateChanged = true;
+}
+
+bool CDataCacheCore::IsSeeking()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_stateInfo.m_stateSeeking;
+}
+
+void CDataCacheCore::SetSpeed(float tempo, float speed)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ m_stateInfo.m_tempo = tempo;
+ m_stateInfo.m_speed = speed;
+}
+
+float CDataCacheCore::GetSpeed()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_stateInfo.m_speed;
+}
+
+float CDataCacheCore::GetTempo()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_stateInfo.m_tempo;
+}
+
+void CDataCacheCore::SetFrameAdvance(bool fa)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ m_stateInfo.m_frameAdvance = fa;
+}
+
+bool CDataCacheCore::IsFrameAdvance()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_stateInfo.m_frameAdvance;
+}
+
+bool CDataCacheCore::IsPlayerStateChanged()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ bool ret(m_playerStateChanged);
+ m_playerStateChanged = false;
+
+ return ret;
+}
+
+void CDataCacheCore::SetGuiRender(bool gui)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ m_stateInfo.m_renderGuiLayer = gui;
+ m_playerStateChanged = true;
+}
+
+bool CDataCacheCore::GetGuiRender()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_stateInfo.m_renderGuiLayer;
+}
+
+void CDataCacheCore::SetVideoRender(bool video)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ m_stateInfo.m_renderVideoLayer = video;
+ m_playerStateChanged = true;
+}
+
+bool CDataCacheCore::GetVideoRender()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_stateInfo.m_renderVideoLayer;
+}
+
+void CDataCacheCore::SetPlayTimes(time_t start, int64_t current, int64_t min, int64_t max)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ m_timeInfo.m_startTime = start;
+ m_timeInfo.m_time = current;
+ m_timeInfo.m_timeMin = min;
+ m_timeInfo.m_timeMax = max;
+}
+
+void CDataCacheCore::GetPlayTimes(time_t &start, int64_t &current, int64_t &min, int64_t &max)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ start = m_timeInfo.m_startTime;
+ current = m_timeInfo.m_time;
+ min = m_timeInfo.m_timeMin;
+ max = m_timeInfo.m_timeMax;
+}
+
+time_t CDataCacheCore::GetStartTime()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ return m_timeInfo.m_startTime;
+}
+
+int64_t CDataCacheCore::GetPlayTime()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ return m_timeInfo.m_time;
+}
+
+int64_t CDataCacheCore::GetMinTime()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ return m_timeInfo.m_timeMin;
+}
+
+int64_t CDataCacheCore::GetMaxTime()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ return m_timeInfo.m_timeMax;
+}
+
+float CDataCacheCore::GetPlayPercentage()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ // Note: To calculate accurate percentage, all time data must be consistent,
+ // which is the case for data cache core. Calculation can not be done
+ // outside of data cache core or a possibility to lock the data cache
+ // core from outside would be needed.
+ int64_t iTotalTime = m_timeInfo.m_timeMax - m_timeInfo.m_timeMin;
+ if (iTotalTime <= 0)
+ return 0;
+
+ return m_timeInfo.m_time * 100 / static_cast<float>(iTotalTime);
+}
diff --git a/xbmc/cores/DataCacheCore.h b/xbmc/cores/DataCacheCore.h
new file mode 100644
index 0000000..95a734b
--- /dev/null
+++ b/xbmc/cores/DataCacheCore.h
@@ -0,0 +1,335 @@
+/*
+ * 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 "EdlEdit.h"
+#include "threads/CriticalSection.h"
+
+#include <atomic>
+#include <chrono>
+#include <string>
+#include <vector>
+
+class CDataCacheCore
+{
+public:
+ CDataCacheCore();
+ virtual ~CDataCacheCore();
+ static CDataCacheCore& GetInstance();
+ void Reset();
+ bool HasAVInfoChanges();
+ void SignalVideoInfoChange();
+ void SignalAudioInfoChange();
+ void SignalSubtitleInfoChange();
+
+ // player video info
+ void SetVideoDecoderName(std::string name, bool isHw);
+ std::string GetVideoDecoderName();
+ bool IsVideoHwDecoder();
+ void SetVideoDeintMethod(std::string method);
+ std::string GetVideoDeintMethod();
+ void SetVideoPixelFormat(std::string pixFormat);
+ std::string GetVideoPixelFormat();
+ void SetVideoStereoMode(std::string mode);
+ std::string GetVideoStereoMode();
+ void SetVideoDimensions(int width, int height);
+ int GetVideoWidth();
+ int GetVideoHeight();
+ void SetVideoFps(float fps);
+ float GetVideoFps();
+ void SetVideoDAR(float dar);
+ float GetVideoDAR();
+
+ /*!
+ * @brief Set if the video is interlaced in cache.
+ * @param isInterlaced Set true when the video is interlaced
+ */
+ void SetVideoInterlaced(bool isInterlaced);
+
+ /*!
+ * @brief Check if the video is interlaced from cache
+ * @return True if interlaced, otherwise false
+ */
+ bool IsVideoInterlaced();
+
+ // player audio info
+ void SetAudioDecoderName(std::string name);
+ std::string GetAudioDecoderName();
+ void SetAudioChannels(std::string channels);
+ std::string GetAudioChannels();
+ void SetAudioSampleRate(int sampleRate);
+ int GetAudioSampleRate();
+ void SetAudioBitsPerSample(int bitsPerSample);
+ int GetAudioBitsPerSample();
+
+ // content info
+
+ /*!
+ * @brief Set the EDL edit list to cache.
+ * @param editList The vector of edits to fill.
+ */
+ void SetEditList(const std::vector<EDL::Edit>& editList);
+
+ /*!
+ * @brief Get the EDL edit list in cache.
+ * @return The EDL edits or an empty vector if no edits exist.
+ */
+ const std::vector<EDL::Edit>& GetEditList() const;
+
+ /*!
+ * @brief Set the list of cut markers in cache.
+ * @return The list of cuts or an empty list if no cuts exist
+ */
+ void SetCuts(const std::vector<int64_t>& cuts);
+
+ /*!
+ * @brief Get the list of cut markers from cache.
+ * @return The list of cut markers or an empty vector if no cuts exist.
+ */
+ const std::vector<int64_t>& GetCuts() const;
+
+ /*!
+ * @brief Set the list of scene markers in cache.
+ * @return The list of scene markers or an empty list if no scene markers exist
+ */
+ void SetSceneMarkers(const std::vector<int64_t>& sceneMarkers);
+
+ /*!
+ * @brief Get the list of scene markers markers from cache.
+ * @return The list of scene markers or an empty vector if no scene exist.
+ */
+ const std::vector<int64_t>& GetSceneMarkers() const;
+
+ void SetChapters(const std::vector<std::pair<std::string, int64_t>>& chapters);
+
+ /*!
+ * @brief Get the chapter list in cache.
+ * @return The list of chapters or an empty vector if no chapters exist.
+ */
+ const std::vector<std::pair<std::string, int64_t>>& GetChapters() const;
+
+ // render info
+ void SetRenderClockSync(bool enabled);
+ bool IsRenderClockSync();
+
+ // player states
+ /*!
+ * @brief Notifies the cache core that a seek operation has finished
+ * @param offset - the seek offset
+ */
+ void SeekFinished(int64_t offset);
+
+ void SetStateSeeking(bool active);
+ bool IsSeeking();
+
+ /*!
+ * @brief Checks if a seek has been performed in the last provided seconds interval
+ * @param lastSecondInterval - the last elapsed second interval to check for a seek operation
+ * @return true if a seek was performed in the lastSecondInterval, false otherwise
+ */
+ bool HasPerformedSeek(int64_t lastSecondInterval) const;
+
+ /*!
+ * @brief Gets the last seek offset
+ * @return the last seek offset
+ */
+ int64_t GetSeekOffSet() const;
+
+ void SetSpeed(float tempo, float speed);
+ float GetSpeed();
+ float GetTempo();
+ void SetFrameAdvance(bool fa);
+ bool IsFrameAdvance();
+ bool IsPlayerStateChanged();
+ void SetGuiRender(bool gui);
+ bool GetGuiRender();
+ void SetVideoRender(bool video);
+ bool GetVideoRender();
+ void SetPlayTimes(time_t start, int64_t current, int64_t min, int64_t max);
+ void GetPlayTimes(time_t &start, int64_t &current, int64_t &min, int64_t &max);
+
+ /*!
+ * \brief Get the start time
+ *
+ * For a typical video this will be zero. For live TV, this is a reference time
+ * in units of time_t (UTC) from which time elapsed starts. Ideally this would
+ * be the start of the tv show but can be any other time as well.
+ */
+ time_t GetStartTime();
+
+ /*!
+ * \brief Get the current time of playback
+ *
+ * This is the time elapsed, in ms, since the start time.
+ */
+ int64_t GetPlayTime();
+
+ /*!
+ * \brief Get the current percentage of playback if a playback buffer is available.
+ *
+ * If there is no playback buffer, percentage will be 0.
+ */
+ float GetPlayPercentage();
+
+ /*!
+ * \brief Get the minimum time
+ *
+ * This will be zero for a typical video. With timeshift, this is the time,
+ * in ms, that the player can go back. This can be before the start time.
+ */
+ int64_t GetMinTime();
+
+ /*!
+ * \brief Get the maximum time
+ *
+ * This is the maximum time, in ms, that the player can skip forward. For a
+ * typical video, this will be the total length. For live TV without
+ * timeshift this is zero, and for live TV with timeshift this will be the
+ * buffer ahead.
+ */
+ int64_t GetMaxTime();
+
+protected:
+ std::atomic_bool m_hasAVInfoChanges = false;
+
+ CCriticalSection m_videoPlayerSection;
+ struct SPlayerVideoInfo
+ {
+ std::string decoderName;
+ bool isHwDecoder;
+ std::string deintMethod;
+ std::string pixFormat;
+ std::string stereoMode;
+ int width;
+ int height;
+ float fps;
+ float dar;
+ bool m_isInterlaced;
+ } m_playerVideoInfo;
+
+ CCriticalSection m_audioPlayerSection;
+ struct SPlayerAudioInfo
+ {
+ std::string decoderName;
+ std::string channels;
+ int sampleRate;
+ int bitsPerSample;
+ } m_playerAudioInfo;
+
+ mutable CCriticalSection m_contentSection;
+ struct SContentInfo
+ {
+ public:
+ /*!
+ * @brief Set the EDL edit list in cache.
+ * @param editList the list of edits to store in cache
+ */
+ void SetEditList(const std::vector<EDL::Edit>& editList) { m_editList = editList; }
+
+ /*!
+ * @brief Get the EDL edit list in cache.
+ * @return the list of edits in cache
+ */
+ const std::vector<EDL::Edit>& GetEditList() const { return m_editList; }
+
+ /*!
+ * @brief Save the list of cut markers in cache.
+ * @param cuts the list of cut markers to store in cache
+ */
+ void SetCuts(const std::vector<int64_t>& cuts) { m_cuts = cuts; }
+
+ /*!
+ * @brief Get the list of cut markers in cache.
+ * @return the list of cut markers in cache
+ */
+ const std::vector<int64_t>& GetCuts() const { return m_cuts; }
+
+ /*!
+ * @brief Save the list of scene markers in cache.
+ * @param sceneMarkers the list of scene markers to store in cache
+ */
+ void SetSceneMarkers(const std::vector<int64_t>& sceneMarkers)
+ {
+ m_sceneMarkers = sceneMarkers;
+ }
+
+ /*!
+ * @brief Get the list of scene markers in cache.
+ * @return the list of scene markers in cache
+ */
+ const std::vector<int64_t>& GetSceneMarkers() const { return m_sceneMarkers; }
+
+ /*!
+ * @brief Save the chapter list in cache.
+ * @param chapters the list of chapters to store in cache
+ */
+ void SetChapters(const std::vector<std::pair<std::string, int64_t>>& chapters)
+ {
+ m_chapters = chapters;
+ }
+
+ /*!
+ * @brief Get the list of chapters in cache.
+ * @return the list of chapters in cache
+ */
+ const std::vector<std::pair<std::string, int64_t>>& GetChapters() const { return m_chapters; }
+
+ /*!
+ * @brief Reset the content cache to the original values (all empty)
+ */
+ void Reset()
+ {
+ m_editList.clear();
+ m_chapters.clear();
+ m_cuts.clear();
+ m_sceneMarkers.clear();
+ }
+
+ private:
+ /*!< list of EDL edits */
+ std::vector<EDL::Edit> m_editList;
+ /*!< name and position for chapters */
+ std::vector<std::pair<std::string, int64_t>> m_chapters;
+ /*!< position for EDL cuts */
+ std::vector<int64_t> m_cuts;
+ /*!< position for EDL scene markers */
+ std::vector<int64_t> m_sceneMarkers;
+ } m_contentInfo;
+
+ CCriticalSection m_renderSection;
+ struct SRenderInfo
+ {
+ bool m_isClockSync;
+ } m_renderInfo;
+
+ mutable CCriticalSection m_stateSection;
+ bool m_playerStateChanged = false;
+ struct SStateInfo
+ {
+ bool m_stateSeeking;
+ bool m_renderGuiLayer;
+ bool m_renderVideoLayer;
+ float m_tempo;
+ float m_speed;
+ bool m_frameAdvance;
+ /*! Time point of the last seek operation */
+ std::chrono::time_point<std::chrono::system_clock> m_lastSeekTime{
+ std::chrono::time_point<std::chrono::system_clock>{}};
+ /*! Last seek offset */
+ int64_t m_lastSeekOffset{0};
+ } m_stateInfo;
+
+ struct STimeInfo
+ {
+ time_t m_startTime;
+ int64_t m_time;
+ int64_t m_timeMax;
+ int64_t m_timeMin;
+ } m_timeInfo = {};
+};
diff --git a/xbmc/cores/DllLoader/CMakeLists.txt b/xbmc/cores/DllLoader/CMakeLists.txt
new file mode 100644
index 0000000..313b8fe
--- /dev/null
+++ b/xbmc/cores/DllLoader/CMakeLists.txt
@@ -0,0 +1,38 @@
+set(SOURCES coff.cpp
+ dll.cpp
+ DllLoader.cpp
+ DllLoaderContainer.cpp
+ dll_tracker.cpp
+ dll_tracker_file.cpp
+ dll_tracker_library.cpp
+ dll_util.cpp
+ LibraryLoader.cpp)
+
+set(HEADERS coff.h
+ coffldr.h
+ dll.h
+ DllLoader.h
+ DllLoaderContainer.h
+ dll_tracker.h
+ dll_tracker_file.h
+ dll_tracker_library.h
+ dll_util.h
+ LibraryLoader.h)
+
+if(NOT CORE_SYSTEM_NAME STREQUAL windows AND NOT CORE_SYSTEM_NAME STREQUAL windowsstore)
+ list(APPEND SOURCES mmap_anon.c
+ SoLoader.cpp)
+ list(APPEND HEADERS mmap_anon.h
+ SoLoader.h)
+ if(NOT CORE_SYSTEM_NAME STREQUAL freebsd)
+ list(APPEND SOURCES ldt_keeper.c)
+ list(APPEND HEADERS ldt_keeper.h)
+ endif()
+else()
+ list(APPEND SOURCES Win32DllLoader.cpp)
+ list(APPEND HEADERS Win32DllLoader.h)
+endif()
+
+add_definitions(-DAPI_DEBUG)
+
+core_add_library(dllloader)
diff --git a/xbmc/cores/DllLoader/DllLoader-linux.cpp b/xbmc/cores/DllLoader/DllLoader-linux.cpp
new file mode 100644
index 0000000..4d56e30
--- /dev/null
+++ b/xbmc/cores/DllLoader/DllLoader-linux.cpp
@@ -0,0 +1,69 @@
+/*
+ * 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 "DllLoader.h"
+#include "DllLoaderContainer.h"
+
+CoffLoader::CoffLoader() :
+ hModule (NULL ),
+ CoffFileHeader (NULL ),
+ OptionHeader (NULL ),
+ WindowsHeader (NULL ),
+ Directory (NULL ),
+ SectionHeader (NULL ),
+ SymTable (NULL ),
+ StringTable (NULL ),
+ SectionData (NULL ),
+ EntryAddress (0 ),
+ NumberOfSymbols (0 ),
+ SizeOfStringTable (0 ),
+ NumOfDirectories (0 ),
+ NumOfSections (0 ),
+ FileHeaderOffset (0 )
+{
+}
+
+CoffLoader::~CoffLoader()
+{
+}
+
+DllLoaderContainer::DllLoaderContainer()
+{
+}
+
+DllLoader* DllLoaderContainer::LoadModule(const char* sName, const char* sCurrentDir, bool bLoadSymbols)
+{
+ return NULL;
+}
+
+bool DllLoader::Load()
+{
+ return false;
+}
+
+void DllLoader::Unload()
+{
+}
+
+void DllLoaderContainer::ReleaseModule(DllLoader*& pDll)
+{
+}
+
+DllLoader::DllLoader(const char *dll, bool track, bool bSystemDll, bool bLoadSymbols, Export* exp)
+{
+}
+
+DllLoader::~DllLoader()
+{
+}
+
+int DllLoader::ResolveExport(const char* x, void** y)
+{
+}
+
+DllLoaderContainer g_dlls;
diff --git a/xbmc/cores/DllLoader/DllLoader.cpp b/xbmc/cores/DllLoader/DllLoader.cpp
new file mode 100644
index 0000000..d7953de
--- /dev/null
+++ b/xbmc/cores/DllLoader/DllLoader.cpp
@@ -0,0 +1,808 @@
+/*
+ * 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 <stdlib.h>
+#include <algorithm>
+
+#include "DllLoader.h"
+#include "DllLoaderContainer.h"
+#include "filesystem/SpecialProtocol.h"
+#include "dll_tracker.h"
+#include "dll_util.h"
+#include <limits>
+#include "utils/log.h"
+
+#ifdef TARGET_WINDOWS
+extern "C" FILE *fopen_utf8(const char *_Filename, const char *_Mode);
+#else
+#define fopen_utf8 fopen
+#endif
+
+#include "commons/Exception.h"
+
+#define DLL_PROCESS_DETACH 0
+#define DLL_PROCESS_ATTACH 1
+#define DLL_THREAD_ATTACH 2
+#define DLL_THREAD_DETACH 3
+#define DLL_PROCESS_VERIFIER 4
+
+
+#ifndef APIENTRY
+#define APIENTRY __stdcall
+#endif
+
+// Entry point of a dll (DllMain)
+typedef int (APIENTRY *EntryFunc)(HINSTANCE hinstDLL, DWORD fdwReason, void* lpvReserved);
+
+
+#ifdef TARGET_POSIX
+/*
+ * This is a dirty hack.
+ * The win32 DLLs contain an alloca routine, that first probes the soon
+ * to be allocated new memory *below* the current stack pointer in 4KByte
+ * increments. After the mem probing below the current %esp, the stack
+ * pointer is finally decremented to make room for the "alloca"ed memory.
+ * Maybe the probing code is intended to extend the stack on a windows box.
+ * Anyway, the linux kernel does *not* extend the stack by simply accessing
+ * memory below %esp; it segfaults.
+ * The extend_stack_for_dll_alloca() routine just preallocates a big chunk
+ * of memory on the stack, for use by the DLLs alloca routine.
+ * Added the noinline attribute as e.g. gcc 3.2.2 inlines this function
+ * in a way that breaks it.
+ */
+static void __attribute__((noinline)) extend_stack_for_dll_alloca(void)
+{
+ volatile int* mem =(volatile int*)alloca(0x20000);
+ *mem=0x1234;
+}
+#endif
+
+
+DllLoader::DllLoader(const char *sDll, bool bTrack, bool bSystemDll, bool bLoadSymbols, Export* exps) : LibraryLoader(sDll)
+{
+ ImportDirTable = 0;
+ m_pExportHead = NULL;
+ m_pStaticExports = exps;
+ m_bTrack = bTrack;
+ m_bSystemDll = bSystemDll;
+ m_pDlls = NULL;
+
+
+ if(!bSystemDll)
+ {
+ // Initialize FS segment, important for quicktime dll's
+#if defined(USE_LDT_KEEPER)
+ m_ldt_fs = Setup_LDT_Keeper();
+#endif
+ }
+
+ DllLoaderContainer::RegisterDll(this);
+ if (m_bTrack) tracker_dll_add(this);
+ m_bLoadSymbols=bLoadSymbols;
+
+ m_bUnloadSymbols=false;
+
+ /* system dll's are never loaded in any way, so let's just use the pointer */
+ /* to this object as their base address */
+ if (m_bSystemDll)
+ hModule = (HMODULE)this;
+
+}
+
+DllLoader::~DllLoader()
+{
+ while (m_pExportHead)
+ {
+ ExportEntry* entry = m_pExportHead;
+ m_pExportHead = entry->next;
+
+ free(entry);
+ }
+
+ while (m_pDlls)
+ {
+ LoadedList* entry = m_pDlls;
+ m_pDlls = entry->pNext;
+ LibraryLoader* lib = entry->pDll;
+ if (entry->pDll) DllLoaderContainer::ReleaseModule(lib);
+ delete entry;
+ }
+
+ // can't unload a system dll, as this might be happening during xbmc destruction
+ if(!m_bSystemDll)
+ {
+ DllLoaderContainer::UnRegisterDll(this);
+
+#ifdef USE_LDT_KEEPER
+ Restore_LDT_Keeper(m_ldt_fs);
+#endif
+ }
+ if (m_bTrack) tracker_dll_free(this);
+
+ ImportDirTable = 0;
+
+ // hModule points to DllLoader in this case
+ if (m_bSystemDll)
+ hModule = NULL;
+}
+
+int DllLoader::Parse()
+{
+ int iResult = 0;
+
+ std::string strFileName= GetFileName();
+ FILE* fp = fopen_utf8(CSpecialProtocol::TranslatePath(strFileName).c_str(), "rb");
+
+ if (fp)
+ {
+ if (CoffLoader::ParseCoff(fp))
+ {
+ if(WindowsHeader)
+ tracker_dll_set_addr(this, (uintptr_t)hModule,
+ (uintptr_t)hModule + WindowsHeader->SizeOfImage - 1);
+ else
+ {
+ uintptr_t iMinAddr = std::numeric_limits<uintptr_t>::max();
+ uintptr_t iMaxAddr = 0;
+ // dll is loaded now, this means we also know the base address of it and its size
+ for (int i = 0; i < NumOfSections; ++i)
+ {
+ iMinAddr = std::min<uintptr_t>(iMinAddr,
+ (uintptr_t)SectionHeader[i].VirtualAddress);
+ iMaxAddr = std::max<uintptr_t>(iMaxAddr,
+ (uintptr_t)(SectionHeader[i].VirtualAddress +
+ SectionHeader[i].VirtualSize));
+ }
+ if(iMaxAddr > iMinAddr)
+ {
+ iMinAddr += (uintptr_t)hModule;
+ iMaxAddr += (uintptr_t)hModule;
+ tracker_dll_set_addr(this, iMinAddr, iMaxAddr - 1);
+ }
+ }
+ LoadExports();
+ iResult = 1;
+ }
+ fclose(fp);
+ }
+ if (iResult == 0)
+ {
+ m_bTrack = false;
+ }
+ return iResult;
+}
+
+void DllLoader::PrintImportLookupTable(unsigned long ImportLookupTable_RVA)
+{
+ unsigned long *Table = (unsigned long*)RVA2Data(ImportLookupTable_RVA);
+
+ while (*Table)
+ {
+ if (*Table & 0x80000000)
+ {
+ // Process Ordinal...
+ CLog::Log(LOGDEBUG, " Ordinal: {:01X}", *Table & 0x7fffffff);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, " Don't process Hint/Name Table yet...");
+ }
+ Table++;
+ }
+}
+
+void DllLoader::PrintImportTable(ImportDirTable_t *ImportDirTable)
+{
+ ImportDirTable_t *Imp = ImportDirTable;
+ int HavePrinted = 0;
+
+ CLog::Log(LOGDEBUG, "The Coff Image contains the following imports:");
+ while ( Imp->ImportLookupTable_RVA != 0 ||
+ Imp->TimeStamp != 0 ||
+ Imp->ForwarderChain != 0 ||
+ Imp->Name_RVA != 0 ||
+ Imp->ImportAddressTable_RVA != 0)
+ {
+ char *Name;
+ HavePrinted = 1;
+
+ Name = (char*)RVA2Data(Imp->Name_RVA);
+
+ CLog::Log(LOGDEBUG, " {}:", Name);
+ CLog::Log(LOGDEBUG, " ImportAddressTable: {:04X}", Imp->ImportAddressTable_RVA);
+ CLog::Log(LOGDEBUG, " ImportLookupTable: {:04X}", Imp->ImportLookupTable_RVA);
+ CLog::Log(LOGDEBUG, " TimeStamp: {:01X}", Imp->TimeStamp);
+ CLog::Log(LOGDEBUG, " Forwarder Chain: {:01X}", Imp->ForwarderChain);
+
+ PrintImportLookupTable(Imp->ImportLookupTable_RVA);
+ CLog::Log(LOGDEBUG, "");
+ Imp++;
+ }
+ if (!HavePrinted) CLog::Log(LOGDEBUG, "None.");
+}
+
+void DllLoader::PrintExportTable(ExportDirTable_t *ExportDirTable)
+{
+ char *Name = (char*)RVA2Data(ExportDirTable->Name_RVA);
+
+ unsigned long *ExportAddressTable = (unsigned long*)RVA2Data(ExportDirTable->ExportAddressTable_RVA);
+ unsigned long *NamePointerTable = (unsigned long*)RVA2Data(ExportDirTable->NamePointerTable_RVA);
+ unsigned short *OrdinalTable = (unsigned short*)RVA2Data(ExportDirTable->OrdinalTable_RVA);
+
+
+ CLog::Log(LOGDEBUG, "Export Table for {}:", Name);
+
+ CLog::Log(LOGDEBUG, "ExportFlags: {:04X}", ExportDirTable->ExportFlags);
+ CLog::Log(LOGDEBUG, "TimeStamp: {:04X}", ExportDirTable->TimeStamp);
+ CLog::Log(LOGDEBUG, "Major Ver: {:02X}", ExportDirTable->MajorVersion);
+ CLog::Log(LOGDEBUG, "Minor Ver: {:02X}", ExportDirTable->MinorVersion);
+ CLog::Log(LOGDEBUG, "Name RVA: {:04X}", ExportDirTable->Name_RVA);
+ CLog::Log(LOGDEBUG, "OrdinalBase {}", ExportDirTable->OrdinalBase);
+ CLog::Log(LOGDEBUG, "NumAddrTable {}", ExportDirTable->NumAddrTable);
+ CLog::Log(LOGDEBUG, "NumNamePtrs {}", ExportDirTable->NumNamePtrs);
+ CLog::Log(LOGDEBUG, "ExportAddressTable_RVA {:04X}", ExportDirTable->ExportAddressTable_RVA);
+ CLog::Log(LOGDEBUG, "NamePointerTable_RVA {:04X}", ExportDirTable->NamePointerTable_RVA);
+ CLog::Log(LOGDEBUG, "OrdinalTable_RVA {:04X}", ExportDirTable->OrdinalTable_RVA);
+
+ CLog::Log(LOGDEBUG, "Public Exports:");
+ CLog::Log(LOGDEBUG, " ordinal hint RVA name");
+ for (unsigned int i = 0; i < ExportDirTable->NumNamePtrs; i++)
+ {
+ char *Name = (char*)RVA2Data(NamePointerTable[i]);
+
+ CLog::Log(LOGDEBUG, " {}", OrdinalTable[i] + ExportDirTable->OrdinalBase);
+ CLog::Log(LOGDEBUG, " {}", OrdinalTable[i]);
+ CLog::Log(LOGDEBUG, " {:08X}", ExportAddressTable[OrdinalTable[i]]);
+ CLog::Log(LOGDEBUG, " {}", Name);
+ }
+}
+
+int DllLoader::ResolveImports(void)
+{
+ int bResult = 1;
+ if ( NumOfDirectories >= 2 && Directory[IMPORT_TABLE].Size > 0 )
+ {
+ ImportDirTable = (ImportDirTable_t*)RVA2Data(Directory[IMPORT_TABLE].RVA);
+
+#ifdef DUMPING_DATA
+ PrintImportTable(ImportDirTable);
+#endif
+
+ ImportDirTable_t *Imp = ImportDirTable;
+
+ while ( Imp->ImportLookupTable_RVA != 0 ||
+ Imp->TimeStamp != 0 ||
+ Imp->ForwarderChain != 0 ||
+ Imp->Name_RVA != 0 ||
+ Imp->ImportAddressTable_RVA != 0)
+ {
+ const char *Name = (const char*)RVA2Data(Imp->Name_RVA);
+
+ const char* FileName=ResolveReferencedDll(Name);
+ // If possible use the dll name WITH path to resolve exports. We could have loaded
+ // a dll with the same name as another dll but from a different directory
+ if (FileName) Name=FileName;
+
+ unsigned long *Table = (unsigned long*)RVA2Data(Imp->ImportLookupTable_RVA);
+ unsigned long *Addr = (unsigned long*)RVA2Data(Imp->ImportAddressTable_RVA);
+
+ while (*Table)
+ {
+ if (*Table & 0x80000000)
+ {
+ void *Fixup;
+ if ( !ResolveOrdinal(Name, *Table&0x7ffffff, &Fixup) )
+ {
+ bResult = 0;
+ char szBuf[128];
+ CLog::Log(LOGDEBUG, "Unable to resolve ordinal {} {}", Name, *Table & 0x7ffffff);
+ sprintf(szBuf, "%lu", *Table&0x7ffffff);
+ *Addr = create_dummy_function(Name, szBuf);
+ tracker_dll_data_track(this, *Addr);
+ }
+ else
+ {
+ *Addr = (unsigned long)Fixup; //woohoo!!
+ }
+ }
+ else
+ {
+ // We don't handle Hint/Name tables yet!!!
+ char *ImpName = (char*)RVA2Data(*Table + 2);
+
+ void *Fixup;
+ if ( !ResolveName(Name, ImpName, &Fixup) )
+ {
+ *Addr=get_win_function_address(Name, ImpName);
+ if(!*Addr)
+ {
+ CLog::Log(LOGDEBUG, "Unable to resolve {} {}", Name, ImpName);
+ *Addr = create_dummy_function(Name, ImpName);
+ tracker_dll_data_track(this, *Addr);
+ bResult = 0;
+ }
+ }
+ else
+ {
+ *Addr = (unsigned long)Fixup;
+ }
+ }
+ Table++;
+ Addr++;
+ }
+ Imp++;
+ }
+ }
+ return bResult;
+}
+
+const char* DllLoader::ResolveReferencedDll(const char* dll)
+{
+ DllLoader* pDll = static_cast<DllLoader*>(DllLoaderContainer::LoadModule(dll, GetPath(), m_bLoadSymbols));
+
+ if (!pDll)
+ {
+ CLog::Log(LOGDEBUG, "Unable to load referenced dll {} - Dll: {}", dll, GetFileName());
+ return NULL;
+ }
+ else if (!pDll->IsSystemDll())
+ {
+ LoadedList* entry=new LoadedList;
+ entry->pDll=pDll;
+ entry->pNext=m_pDlls;
+ m_pDlls=entry;
+ }
+
+ return pDll->GetFileName();
+}
+
+int DllLoader::LoadExports()
+{
+ if ( NumOfDirectories > EXPORT_TABLE && Directory[EXPORT_TABLE].Size > 0 )
+ {
+ ExportDirTable = (ExportDirTable_t*)RVA2Data(Directory[EXPORT_TABLE].RVA);
+
+#ifdef DUMPING_DATA
+ PrintExportTable(ExportDirTable);
+#endif
+
+ //! @todo Validate all pointers are valid. Is a zero RVA valid or not? I'd guess not as it would
+ //! point to the coff file header, thus not right.
+
+ unsigned long *ExportAddressTable = (unsigned long*)RVA2Data(ExportDirTable->ExportAddressTable_RVA);
+ unsigned long *NamePointerTable = (unsigned long*)RVA2Data(ExportDirTable->NamePointerTable_RVA);
+ unsigned short *OrdinalTable = (unsigned short*)RVA2Data(ExportDirTable->OrdinalTable_RVA);
+
+ for (unsigned int i = 0; i < ExportDirTable->NumNamePtrs; i++)
+ {
+ char *Name = (char*)RVA2Data(NamePointerTable[i]);
+ void* Addr = (void*)RVA2Data(ExportAddressTable[OrdinalTable[i]]);
+ AddExport(Name, OrdinalTable[i]+ExportDirTable->OrdinalBase, Addr);
+ }
+ }
+ return 0;
+}
+
+int DllLoader::ResolveExport(const char *sName, void **pAddr, bool logging)
+{
+ Export* pExport=GetExportByFunctionName(sName);
+
+ if (pExport)
+ {
+ if (m_bTrack && pExport->track_function)
+ *pAddr=(void*)pExport->track_function;
+ else
+ *pAddr=(void*)pExport->function;
+
+ return 1;
+ }
+
+ const char* sDllName = strrchr(GetFileName(), '\\');
+ if (sDllName) sDllName += 1;
+ else sDllName = GetFileName();
+
+ if (logging)
+ CLog::Log(LOGWARNING, "Unable to resolve: {} {}", sDllName, sName);
+ return 0;
+}
+
+int DllLoader::ResolveOrdinal(unsigned long ordinal, void **pAddr)
+{
+ Export* pExport=GetExportByOrdinal(ordinal);
+
+ if (pExport)
+ {
+ if (m_bTrack && pExport->track_function)
+ *pAddr=(void*)pExport->track_function;
+ else
+ *pAddr=(void*)pExport->function;
+
+ return 1;
+ }
+
+ const char* sDllName = strrchr(GetFileName(), '\\');
+ if (sDllName) sDllName += 1;
+ else sDllName = GetFileName();
+
+ CLog::Log(LOGWARNING, "Unable to resolve: {} {}", sDllName, ordinal);
+ return 0;
+}
+
+Export* DllLoader::GetExportByOrdinal(unsigned long ordinal)
+{
+ ExportEntry* entry = m_pExportHead;
+
+ while (entry)
+ {
+ if (ordinal == entry->exp.ordinal)
+ {
+ return &entry->exp;
+ }
+ entry = entry->next;
+ }
+
+ if( m_pStaticExports )
+ {
+ Export* exp = m_pStaticExports;
+ while(exp->function || exp->track_function || exp->name)
+ {
+ if (ordinal == exp->ordinal)
+ return exp;
+ exp++;
+ }
+ }
+
+ return NULL;
+}
+
+Export* DllLoader::GetExportByFunctionName(const char* sFunctionName)
+{
+ ExportEntry* entry = m_pExportHead;
+
+ while (entry)
+ {
+ if (entry->exp.name && strcmp(sFunctionName, entry->exp.name) == 0)
+ {
+ return &entry->exp;
+ }
+ entry = entry->next;
+ }
+
+ if( m_pStaticExports )
+ {
+ Export* exp = m_pStaticExports;
+ while(exp->function || exp->track_function || exp->name)
+ {
+ if (exp->name && strcmp(sFunctionName, exp->name) == 0)
+ return exp;
+ exp++;
+ }
+ }
+
+ return NULL;
+}
+
+int DllLoader::ResolveOrdinal(const char *sName, unsigned long ordinal, void **fixup)
+{
+ DllLoader* pDll = static_cast<DllLoader*>(DllLoaderContainer::GetModule(sName));
+
+ if (pDll)
+ {
+ Export* pExp = pDll->GetExportByOrdinal(ordinal);
+ if(pExp)
+ {
+ if (m_bTrack && pExp->track_function)
+ *fixup = (void*)(pExp->track_function);
+ else
+ *fixup = (void*)(pExp->function);
+
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+int DllLoader::ResolveName(const char *sName, char* sFunction, void **fixup)
+{
+ DllLoader* pDll = static_cast<DllLoader*>(DllLoaderContainer::GetModule(sName));
+
+ if (pDll)
+ {
+ Export* pExp = pDll->GetExportByFunctionName(sFunction);
+ if(pExp)
+ {
+ if (m_bTrack && pExp->track_function)
+ *fixup = (void*)(pExp->track_function);
+ else
+ *fixup = (void*)(pExp->function);
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+void DllLoader::AddExport(unsigned long ordinal, void* function, void* track_function)
+{
+ ExportEntry* entry = (ExportEntry*)malloc(sizeof(ExportEntry));
+ if (!entry)
+ return;
+ entry->exp.function = function;
+ entry->exp.ordinal = ordinal;
+ entry->exp.track_function = track_function;
+ entry->exp.name = NULL;
+
+ entry->next = m_pExportHead;
+ m_pExportHead = entry;
+}
+
+void DllLoader::AddExport(char* sFunctionName, unsigned long ordinal, void* function, void* track_function)
+{
+ int len = sizeof(ExportEntry);
+
+ ExportEntry* entry = (ExportEntry*)malloc(len + strlen(sFunctionName) + 1);
+ if (!entry)
+ return;
+ entry->exp.function = function;
+ entry->exp.ordinal = ordinal;
+ entry->exp.track_function = track_function;
+ entry->exp.name = ((char*)(entry)) + len;
+ strcpy(const_cast<char*>(entry->exp.name), sFunctionName);
+
+ entry->next = m_pExportHead;
+ m_pExportHead = entry;
+}
+
+void DllLoader::AddExport(char* sFunctionName, void* function, void* track_function)
+{
+ int len = sizeof(ExportEntry);
+
+ ExportEntry* entry = (ExportEntry*)malloc(len + strlen(sFunctionName) + 1);
+ if (!entry)
+ return;
+ entry->exp.function = (void*)function;
+ entry->exp.ordinal = -1;
+ entry->exp.track_function = track_function;
+ entry->exp.name = ((char*)(entry)) + len;
+ strcpy(const_cast<char*>(entry->exp.name), sFunctionName);
+
+ entry->next = m_pExportHead;
+ m_pExportHead = entry;
+}
+
+bool DllLoader::Load()
+{
+ if (!Parse())
+ {
+ CLog::Log(LOGERROR, "Unable to open dll {}", GetFileName());
+ return false;
+ }
+
+ ResolveImports();
+ LoadSymbols();
+
+ // only execute DllMain if no EntryPoint is found
+ if (!EntryAddress)
+ ResolveExport("DllMain", (void**)&EntryAddress);
+
+#ifdef LOGALL
+ CLog::Log(LOGDEBUG, "Executing EntryPoint with DLL_PROCESS_ATTACH at: 0x{:x} - Dll: {}",
+ pLoader->EntryAddress, sName);
+#endif
+
+ if(EntryAddress)
+ {
+ EntryFunc initdll = (EntryFunc)EntryAddress;
+ /* since we are handing execution over to unknown code, safeguard here */
+ try
+ {
+#ifdef TARGET_POSIX
+ extend_stack_for_dll_alloca();
+#endif
+ initdll((HINSTANCE)hModule, DLL_PROCESS_ATTACH , 0); //call "DllMain" with DLL_PROCESS_ATTACH
+
+#ifdef LOGALL
+ CLog::Log(LOGDEBUG, "EntryPoint with DLL_PROCESS_ATTACH called - Dll: {}", sName);
+#endif
+
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "{} - Unhandled exception during DLL_PROCESS_ATTACH", __FUNCTION__);
+
+ // vp7vfw.dll throws a CUserException due to a missing export
+ // but the export isn't really needed for normal operation
+ // and dll works anyway, so let's ignore it
+
+ if (StringUtils::CompareNoCase(GetName(), "vp7vfw.dll") != 0)
+ return false;
+
+
+ CLog::Log(LOGDEBUG, "{} - Ignoring exception during DLL_PROCESS_ATTACH", __FUNCTION__);
+ }
+
+ // init function may have fixed up the export table
+ // this is what I expect should happens on PECompact2
+ // dll's if export table is compressed.
+ if(!m_pExportHead)
+ LoadExports();
+ }
+
+ return true;
+}
+
+void DllLoader::Unload()
+{
+#ifdef LOGALL
+ CLog::Log(LOGDEBUG, "Executing EntryPoint with DLL_PROCESS_DETACH at: 0x{:x} - Dll: {}",
+ pDll->EntryAddress, pDll->GetFileName());
+#endif
+
+ //call "DllMain" with DLL_PROCESS_DETACH
+ if(EntryAddress)
+ {
+ EntryFunc initdll = (EntryFunc)EntryAddress;
+ initdll((HINSTANCE)hModule, DLL_PROCESS_DETACH , 0);
+ }
+
+#ifdef LOGALL
+ CLog::Log(LOGDEBUG, "EntryPoint with DLL_PROCESS_DETACH called - Dll: {}", pDll->GetFileName());
+#endif
+
+ if (m_bUnloadSymbols)
+ UnloadSymbols();
+}
+
+// This function is a hack to get symbols loaded for
+// dlls. The function FFinishImageLoad internally allocates
+// memory which is/can never be freed. And the dll can not be
+// unloaded.
+void DllLoader::LoadSymbols()
+{
+#ifdef ENABLE_SYMBOL_LOADING
+ if (!m_bLoadSymbols ) return;
+
+ // don't load debug symbols unless we have a debugger present
+ // seems these calls break on some bioses. i suppose it could
+ // be related to if the bios has debug capabilities.
+ if (!DmIsDebuggerPresent())
+ {
+ m_bLoadSymbols=false;
+ return;
+ }
+
+ LPVOID pBaseAddress=GetXbdmBaseAddress();
+
+ if (pBaseAddress)
+ {
+ CoffLoader dllxbdm;
+ if (dllxbdm.ParseHeaders(pBaseAddress))
+ {
+ int offset=GetFFinishImageLoadOffset(dllxbdm.WindowsHeader->CheckSum);
+
+ if (offset==0)
+ {
+ CLog::Log(LOGDEBUG,
+ "DllLoader: Unable to load symbols for {}. No offset for xbdm.dll with checksum "
+ "{:#08X} found",
+ GetName(), dllxbdm.WindowsHeader->CheckSum);
+ return;
+ }
+
+ // Get a function pointer to the unexported function FFinishImageLoad
+ fnFFinishImageLoad FFinishImageLoad=(fnFFinishImageLoad)((LPBYTE)pBaseAddress+offset);
+
+ // Prepare parameter for the function call
+ LDR_DATA_TABLE_ENTRY ldte;
+ LPLDR_DATA_TABLE_ENTRY pldteout;
+ ldte.DllBase=hModule; // Address where this dll is loaded into memory
+ char* szName=GetName(); // Name of this dll without path
+
+ try
+ {
+ // Call FFinishImageLoad to register this dll to the debugger and load its symbols.
+ FFinishImageLoad(&ldte, szName, &pldteout);
+ }
+ catch(...)
+ {
+ CLog::Log(LOGDEBUG, "DllLoader: Loading symbols for {} failed with an exception.",
+ GetName());
+ }
+ }
+ }
+ else
+ CLog::Log(LOGDEBUG, "DllLoader: Can't load symbols for {}. xbdm.dll is needed and not loaded",
+ GetName());
+
+#ifdef ENABLE_SYMBOL_UNLOADING
+ m_bUnloadSymbols=true; // Do this to allow unloading this dll from dllloadercontainer
+#endif
+
+#else
+ m_bLoadSymbols=false;
+#endif
+}
+
+// This function is even more a hack
+// It will remove the dll from the Debug manager
+// but vs.net does not unload the symbols (don't know why)
+// The dll can be loaded again after unloading.
+// This function leaks memory.
+void DllLoader::UnloadSymbols()
+{
+#ifdef ENABLE_SYMBOL_UNLOADING
+ ANSI_STRING name;
+ OBJECT_ATTRIBUTES attributes;
+ RtlInitAnsiString(&name, GetName());
+ InitializeObjectAttributes(&attributes, &name, OBJ_CASE_INSENSITIVE, NULL);
+
+ // Try to unload the symbols from vs.net debugger
+ DbgUnLoadImageSymbols(&name, (ULONG)hModule, 0xFFFFFFFF);
+
+ LPVOID pBaseAddress=GetXbdmBaseAddress();
+
+ if (pBaseAddress)
+ {
+ CoffLoader dllxbdm;
+ if (dllxbdm.ParseHeaders(pBaseAddress))
+ {
+ int offset=GetDmiOffset(dllxbdm.WindowsHeader->CheckSum);
+
+ if (offset==0)
+ {
+ CLog::Log(LOGDEBUG,
+ "DllLoader: Unable to unload symbols for {}. No offset for xbdm.dll with "
+ "checksum {:#08X} found",
+ GetName(), dllxbdm.WindowsHeader->CheckSum);
+ return;
+ }
+
+ try
+ {
+ std::wstring strNameW;
+ g_charsetConverter.utf8ToW(GetName(), strNameW);
+
+ // Get the address of the global struct g_dmi
+ // It is located inside the xbdm.dll and
+ // get the LoadedModuleList member (here the entry var)
+ // of the structure.
+ LPBYTE g_dmi=((LPBYTE)pBaseAddress)+offset;
+ LIST_ENTRY* entry=(LIST_ENTRY*)(g_dmi+4);
+
+ // Search for the dll we are unloading...
+ while (entry)
+ {
+ std::wstring baseName=(wchar_t*)((LDR_DATA_TABLE_ENTRY*)entry)->BaseDllName.Buffer;
+ if (baseName == strNameW)
+ {
+ // ...and remove it from the LoadedModuleList and free its memory.
+ LIST_ENTRY* back=entry->Blink;
+ LIST_ENTRY* front=entry->Flink;
+ back->Flink=front;
+ front->Blink=back;
+ DmFreePool(entry);
+ break;
+ }
+
+ entry=entry->Flink;
+ }
+ }
+ catch(...)
+ {
+ CLog::Log(LOGDEBUG, "DllLoader: Unloading symbols for {} failed with an exception.",
+ GetName());
+ }
+ }
+ }
+ else
+ CLog::Log(LOGDEBUG, "DllLoader: Can't unload symbols for {}. xbdm.dll is needed and not loaded",
+ GetName());
+#endif
+}
diff --git a/xbmc/cores/DllLoader/DllLoader.h b/xbmc/cores/DllLoader/DllLoader.h
new file mode 100644
index 0000000..79e019d
--- /dev/null
+++ b/xbmc/cores/DllLoader/DllLoader.h
@@ -0,0 +1,113 @@
+/*
+ * 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 "coffldr.h"
+#include "LibraryLoader.h"
+
+// clang-format off
+#if defined(__linux__) && \
+ !defined(__aarch64__) && \
+ !defined(__alpha__) && \
+ !defined(__arc__) && \
+ !defined(__arm__) && \
+ !defined(__loongarch__) && \
+ !defined(__mips__) && \
+ !defined(__powerpc__) && \
+ !defined(__or1k__) && \
+ !defined(__riscv) && \
+ !defined(__SH4__) && \
+ !defined(__s390x__) && \
+ !defined(__sparc__) && \
+ !defined(__xtensa__)
+#define USE_LDT_KEEPER
+#include "ldt_keeper.h"
+#endif
+// clang-format on
+
+#ifndef NULL
+#define NULL 0
+#endif
+
+class DllLoader;
+
+
+typedef struct Export
+{
+ const char* name;
+ unsigned long ordinal;
+ void* function;
+ void* track_function;
+} Export;
+
+typedef struct ExportEntry
+{
+ Export exp;
+ ExportEntry* next;
+} ExportEntry;
+
+typedef struct _LoadedList
+{
+ DllLoader* pDll;
+ _LoadedList* pNext;
+} LoadedList;
+
+class DllLoader : public CoffLoader, public LibraryLoader
+{
+public:
+ DllLoader(const char *dll, bool track = false, bool bSystemDll = false, bool bLoadSymbols = false, Export* exports = NULL);
+ ~DllLoader() override;
+
+ bool Load() override;
+ void Unload() override;
+
+ int ResolveExport(const char*, void** ptr, bool logging = true) override;
+ int ResolveOrdinal(unsigned long ordinal, void** ptr) override;
+ bool HasSymbols() override { return m_bLoadSymbols && !m_bUnloadSymbols; }
+ bool IsSystemDll() override { return m_bSystemDll; }
+ HMODULE GetHModule() override { return (HMODULE)hModule; }
+
+ Export* GetExportByFunctionName(const char* sFunctionName);
+ Export* GetExportByOrdinal(unsigned long ordinal);
+protected:
+ int Parse();
+ int ResolveImports();
+
+ void AddExport(unsigned long ordinal, void* function, void* track_function = NULL);
+ void AddExport(char* sFunctionName, unsigned long ordinal, void* function, void* track_function = NULL);
+ void AddExport(char* sFunctionName, void* function, void* track_function = NULL);
+ void SetExports(Export* exports) { m_pStaticExports = exports; }
+
+protected:
+ // Just pointers; don't delete...
+ ImportDirTable_t *ImportDirTable;
+ ExportDirTable_t *ExportDirTable;
+ bool m_bTrack;
+ bool m_bSystemDll; // true if this dll should not be removed
+ bool m_bLoadSymbols; // when true this dll should not be removed
+ bool m_bUnloadSymbols;
+ ExportEntry* m_pExportHead;
+ Export* m_pStaticExports;
+ LoadedList* m_pDlls;
+
+#ifdef USE_LDT_KEEPER
+ ldt_fs_t* m_ldt_fs;
+#endif
+
+ void PrintImportLookupTable(unsigned long ImportLookupTable_RVA);
+ void PrintImportTable(ImportDirTable_t *ImportDirTable);
+ void PrintExportTable(ExportDirTable_t *ExportDirTable);
+
+ int ResolveOrdinal(const char*, unsigned long, void**);
+ int ResolveName(const char*, char*, void **);
+ const char* ResolveReferencedDll(const char* dll);
+ int LoadExports();
+ void LoadSymbols();
+ static void UnloadSymbols();
+};
diff --git a/xbmc/cores/DllLoader/DllLoaderContainer.cpp b/xbmc/cores/DllLoader/DllLoaderContainer.cpp
new file mode 100644
index 0000000..e56b02f
--- /dev/null
+++ b/xbmc/cores/DllLoader/DllLoaderContainer.cpp
@@ -0,0 +1,344 @@
+/*
+ * 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 "DllLoaderContainer.h"
+#ifdef TARGET_POSIX
+#include "SoLoader.h"
+#endif
+#ifdef TARGET_WINDOWS
+#include "Win32DllLoader.h"
+#endif
+#include "DllLoader.h"
+#include "dll_tracker.h" // for python unload hack
+#include "filesystem/File.h"
+#include "utils/URIUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "URL.h"
+
+#if defined(TARGET_WINDOWS)
+#define ENV_PARTIAL_PATH \
+ "special://xbmcbin/;" \
+ "special://xbmcbin/system/;" \
+ "special://xbmcbin/system/python/;" \
+ "special://xbmc/;" \
+ "special://xbmc/system/;" \
+ "special://xbmc/system/python/"
+#else
+#define ENV_PARTIAL_PATH \
+ "special://xbmcbin/system/;" \
+ "special://xbmcbin/system/players/mplayer/;" \
+ "special://xbmcbin/system/players/VideoPlayer/;" \
+ "special://xbmcbin/system/players/paplayer/;" \
+ "special://xbmcbin/system/python/;" \
+ "special://xbmc/system/;" \
+ "special://xbmc/system/players/mplayer/;" \
+ "special://xbmc/system/players/VideoPlayer/;" \
+ "special://xbmc/system/players/paplayer/;" \
+ "special://xbmc/system/python/"
+#endif
+#if defined(TARGET_DARWIN)
+#define ENV_PATH ENV_PARTIAL_PATH \
+ ";special://frameworks/"
+#else
+#define ENV_PATH ENV_PARTIAL_PATH
+#endif
+
+//Define this to get logging on all calls to load/unload of dlls
+//#define LOGALL
+
+
+using namespace XFILE;
+
+LibraryLoader* DllLoaderContainer::m_dlls[64] = {};
+int DllLoaderContainer::m_iNrOfDlls = 0;
+bool DllLoaderContainer::m_bTrack = true;
+
+void DllLoaderContainer::Clear()
+{
+}
+
+HMODULE DllLoaderContainer::GetModuleAddress(const char* sName)
+{
+ return (HMODULE)GetModule(sName);
+}
+
+LibraryLoader* DllLoaderContainer::GetModule(const char* sName)
+{
+ for (int i = 0; i < m_iNrOfDlls && m_dlls[i] != NULL; i++)
+ {
+ if (StringUtils::CompareNoCase(m_dlls[i]->GetName(), sName) == 0)
+ return m_dlls[i];
+ if (!m_dlls[i]->IsSystemDll() &&
+ StringUtils::CompareNoCase(m_dlls[i]->GetFileName(), sName) == 0)
+ return m_dlls[i];
+ }
+
+ return NULL;
+}
+
+LibraryLoader* DllLoaderContainer::GetModule(const HMODULE hModule)
+{
+ for (int i = 0; i < m_iNrOfDlls && m_dlls[i] != NULL; i++)
+ {
+ if (m_dlls[i]->GetHModule() == hModule) return m_dlls[i];
+ }
+ return NULL;
+}
+
+LibraryLoader* DllLoaderContainer::LoadModule(const char* sName, const char* sCurrentDir/*=NULL*/, bool bLoadSymbols/*=false*/)
+{
+ LibraryLoader* pDll=NULL;
+
+ if (IsSystemDll(sName))
+ {
+ pDll = GetModule(sName);
+ }
+ else if (sCurrentDir)
+ {
+ std::string strPath=sCurrentDir;
+ strPath+=sName;
+ pDll = GetModule(strPath.c_str());
+ }
+
+ if (!pDll)
+ {
+ pDll = GetModule(sName);
+ }
+
+ if (!pDll)
+ {
+ pDll = FindModule(sName, sCurrentDir, bLoadSymbols);
+ }
+ else if (!pDll->IsSystemDll())
+ {
+ pDll->IncrRef();
+
+#ifdef LOGALL
+ CLog::Log(LOGDEBUG, "Already loaded Dll {} at 0x{:x}", pDll->GetFileName(), pDll);
+#endif
+
+ }
+
+ return pDll;
+}
+
+LibraryLoader* DllLoaderContainer::FindModule(const char* sName, const char* sCurrentDir, bool bLoadSymbols)
+{
+ if (URIUtils::IsInArchive(sName))
+ {
+ CURL url(sName);
+ std::string newName = "special://temp/";
+ newName += url.GetFileName();
+ CFile::Copy(sName, newName);
+ return FindModule(newName.c_str(), sCurrentDir, bLoadSymbols);
+ }
+
+ if (CURL::IsFullPath(sName))
+ { // Has a path, just try to load
+ return LoadDll(sName, bLoadSymbols);
+ }
+#ifdef TARGET_POSIX
+ else if (strcmp(sName, "xbmc.so") == 0)
+ return LoadDll(sName, bLoadSymbols);
+#endif
+ else if (sCurrentDir)
+ { // in the path of the parent dll?
+ std::string strPath=sCurrentDir;
+ strPath+=sName;
+
+ if (CFile::Exists(strPath))
+ return LoadDll(strPath.c_str(), bLoadSymbols);
+ }
+
+ // in environment variable?
+ std::vector<std::string> vecEnv;
+
+#if defined(TARGET_ANDROID)
+ std::string systemLibs = getenv("KODI_ANDROID_SYSTEM_LIBS");
+ vecEnv = StringUtils::Split(systemLibs, ':');
+ std::string localLibs = getenv("KODI_ANDROID_LIBS");
+ vecEnv.insert(vecEnv.begin(),localLibs);
+#else
+ vecEnv = StringUtils::Split(ENV_PATH, ';');
+#endif
+ LibraryLoader* pDll = NULL;
+
+ for (std::vector<std::string>::const_iterator i = vecEnv.begin(); i != vecEnv.end(); ++i)
+ {
+ std::string strPath = *i;
+ URIUtils::AddSlashAtEnd(strPath);
+
+#ifdef LOGALL
+ CLog::Log(LOGDEBUG, "Searching for the dll {} in directory {}", sName, strPath);
+#endif
+
+ strPath+=sName;
+
+ // Have we already loaded this dll
+ if ((pDll = GetModule(strPath.c_str())) != NULL)
+ return pDll;
+
+ if (CFile::Exists(strPath))
+ return LoadDll(strPath.c_str(), bLoadSymbols);
+ }
+
+ // can't find it in any of our paths - could be a system dll
+ if ((pDll = LoadDll(sName, bLoadSymbols)) != NULL)
+ return pDll;
+
+ CLog::Log(LOGDEBUG, "Dll {} was not found in path", sName);
+ return NULL;
+}
+
+void DllLoaderContainer::ReleaseModule(LibraryLoader*& pDll)
+{
+ if (!pDll)
+ return;
+ if (pDll->IsSystemDll())
+ {
+ CLog::Log(LOGFATAL, "{} is a system dll and should never be released", pDll->GetName());
+ return;
+ }
+
+ int iRefCount=pDll->DecrRef();
+ if (iRefCount==0)
+ {
+
+#ifdef LOGALL
+ CLog::Log(LOGDEBUG, "Releasing Dll {}", pDll->GetFileName());
+#endif
+
+ if (!pDll->HasSymbols())
+ {
+ pDll->Unload();
+ delete pDll;
+ pDll=NULL;
+ }
+ else
+ CLog::Log(LOGINFO, "{} has symbols loaded and can never be unloaded", pDll->GetName());
+ }
+#ifdef LOGALL
+ else
+ {
+ CLog::Log(LOGDEBUG, "Dll {} is still referenced with a count of {}", pDll->GetFileName(),
+ iRefCount);
+ }
+#endif
+}
+
+LibraryLoader* DllLoaderContainer::LoadDll(const char* sName, bool bLoadSymbols)
+{
+
+#ifdef LOGALL
+ CLog::Log(LOGDEBUG, "Loading dll {}", sName);
+#endif
+
+ LibraryLoader* pLoader;
+#ifdef TARGET_POSIX
+ pLoader = new SoLoader(sName, bLoadSymbols);
+#elif defined(TARGET_WINDOWS)
+ pLoader = new Win32DllLoader(sName, false);
+#else
+ pLoader = new DllLoader(sName, m_bTrack, false, bLoadSymbols);
+#endif
+
+ if (!pLoader)
+ {
+ CLog::Log(LOGERROR, "Unable to create dll {}", sName);
+ return NULL;
+ }
+
+ if (!pLoader->Load())
+ {
+ delete pLoader;
+ return NULL;
+ }
+
+ return pLoader;
+}
+
+bool DllLoaderContainer::IsSystemDll(const char* sName)
+{
+ for (int i = 0; i < m_iNrOfDlls && m_dlls[i] != NULL; i++)
+ {
+ if (m_dlls[i]->IsSystemDll() && StringUtils::CompareNoCase(m_dlls[i]->GetName(), sName) == 0)
+ return true;
+ }
+
+ return false;
+}
+
+int DllLoaderContainer::GetNrOfModules()
+{
+ return m_iNrOfDlls;
+}
+
+LibraryLoader* DllLoaderContainer::GetModule(int iPos)
+{
+ if (iPos < m_iNrOfDlls) return m_dlls[iPos];
+ return NULL;
+}
+
+void DllLoaderContainer::RegisterDll(LibraryLoader* pDll)
+{
+ for (LibraryLoader*& dll : m_dlls)
+ {
+ if (dll == NULL)
+ {
+ dll = pDll;
+ m_iNrOfDlls++;
+ break;
+ }
+ }
+}
+
+void DllLoaderContainer::UnRegisterDll(LibraryLoader* pDll)
+{
+ if (pDll)
+ {
+ if (pDll->IsSystemDll())
+ {
+ CLog::Log(LOGFATAL, "{} is a system dll and should never be removed", pDll->GetName());
+ }
+ else
+ {
+ // remove from the list
+ bool bRemoved = false;
+ for (int i = 0; i < m_iNrOfDlls && m_dlls[i]; i++)
+ {
+ if (m_dlls[i] == pDll) bRemoved = true;
+ if (bRemoved && i + 1 < m_iNrOfDlls)
+ {
+ m_dlls[i] = m_dlls[i + 1];
+ }
+ }
+ if (bRemoved)
+ {
+ m_iNrOfDlls--;
+ m_dlls[m_iNrOfDlls] = NULL;
+ }
+ }
+ }
+}
+
+void DllLoaderContainer::UnloadPythonDlls()
+{
+ // unload all dlls that python could have loaded
+ for (int i = 0; i < m_iNrOfDlls && m_dlls[i] != NULL; i++)
+ {
+ const char* name = m_dlls[i]->GetName();
+ if (strstr(name, ".pyd") != NULL)
+ {
+ LibraryLoader* pDll = m_dlls[i];
+ ReleaseModule(pDll);
+ i = 0;
+ }
+ }
+
+}
diff --git a/xbmc/cores/DllLoader/DllLoaderContainer.h b/xbmc/cores/DllLoader/DllLoaderContainer.h
new file mode 100644
index 0000000..3769ff8
--- /dev/null
+++ b/xbmc/cores/DllLoader/DllLoaderContainer.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "LibraryLoader.h"
+
+class DllLoaderContainer
+{
+public:
+ static void Clear();
+ static HMODULE GetModuleAddress(const char* sName);
+ static int GetNrOfModules();
+ static LibraryLoader* GetModule(int iPos);
+ static LibraryLoader* GetModule(const char* sName);
+ static LibraryLoader* GetModule(const HMODULE hModule);
+ static LibraryLoader* LoadModule(const char* sName, const char* sCurrentDir=NULL, bool bLoadSymbols=false);
+ static void ReleaseModule(LibraryLoader*& pDll);
+
+ static void RegisterDll(LibraryLoader* pDll);
+ static void UnRegisterDll(LibraryLoader* pDll);
+ static void UnloadPythonDlls();
+
+private:
+ static LibraryLoader* FindModule(const char* sName, const char* sCurrentDir, bool bLoadSymbols);
+ static LibraryLoader* LoadDll(const char* sName, bool bLoadSymbols);
+ static bool IsSystemDll(const char* sName);
+
+ static LibraryLoader* m_dlls[64];
+ static int m_iNrOfDlls;
+ static bool m_bTrack;
+};
diff --git a/xbmc/cores/DllLoader/LibraryLoader.cpp b/xbmc/cores/DllLoader/LibraryLoader.cpp
new file mode 100644
index 0000000..5689900
--- /dev/null
+++ b/xbmc/cores/DllLoader/LibraryLoader.cpp
@@ -0,0 +1,62 @@
+/*
+ * 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 "LibraryLoader.h"
+
+#include "utils/log.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+LibraryLoader::LibraryLoader(const std::string& libraryFile):
+ m_fileName(libraryFile)
+{
+ size_t pos = m_fileName.find_last_of("\\/");
+ if (pos != std::string::npos)
+ m_path = m_fileName.substr(0, pos);
+
+ m_iRefCount = 1;
+}
+
+LibraryLoader::~LibraryLoader() = default;
+
+const char *LibraryLoader::GetName() const
+{
+ size_t pos = m_fileName.find_last_of('/');
+ if (pos != std::string::npos)
+ return &m_fileName.at(pos + 1); // don't include /
+ return m_fileName.c_str();
+}
+
+const char *LibraryLoader::GetFileName() const
+{
+ return m_fileName.c_str();
+}
+
+const char *LibraryLoader::GetPath() const
+{
+ return m_path.c_str();
+}
+
+int LibraryLoader::IncrRef()
+{
+ m_iRefCount++;
+ return m_iRefCount;
+}
+
+int LibraryLoader::DecrRef()
+{
+ m_iRefCount--;
+ return m_iRefCount;
+}
+
+int LibraryLoader::ResolveOrdinal(unsigned long ordinal, void** ptr)
+{
+ CLog::Log(LOGWARNING, "{} - Unable to resolve {} in dll {}", __FUNCTION__, ordinal, GetName());
+ return 0;
+}
diff --git a/xbmc/cores/DllLoader/LibraryLoader.h b/xbmc/cores/DllLoader/LibraryLoader.h
new file mode 100644
index 0000000..a8db986
--- /dev/null
+++ b/xbmc/cores/DllLoader/LibraryLoader.h
@@ -0,0 +1,46 @@
+/*
+ * 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 <string>
+
+#ifdef TARGET_POSIX
+#include "PlatformDefs.h"
+#endif
+
+class LibraryLoader
+{
+public:
+ explicit LibraryLoader(const std::string& libraryFile);
+ virtual ~LibraryLoader();
+
+ virtual bool Load() = 0;
+ virtual void Unload() = 0;
+
+ virtual int ResolveExport(const char* symbol, void** ptr, bool logging = true) = 0;
+ virtual int ResolveOrdinal(unsigned long ordinal, void** ptr);
+ virtual bool IsSystemDll() = 0;
+ virtual HMODULE GetHModule() = 0;
+ virtual bool HasSymbols() = 0;
+
+ const char *GetName() const; // eg "mplayer.dll"
+ const char *GetFileName() const; // "special://xbmcbin/system/mplayer/players/mplayer.dll"
+ const char *GetPath() const; // "special://xbmcbin/system/mplayer/players/"
+
+ int IncrRef();
+ int DecrRef();
+ int GetRef();
+
+private:
+ LibraryLoader(const LibraryLoader&);
+ LibraryLoader& operator=(const LibraryLoader&);
+ std::string m_fileName;
+ std::string m_path;
+ int m_iRefCount;
+};
diff --git a/xbmc/cores/DllLoader/SoLoader.cpp b/xbmc/cores/DllLoader/SoLoader.cpp
new file mode 100644
index 0000000..2b5cbce
--- /dev/null
+++ b/xbmc/cores/DllLoader/SoLoader.cpp
@@ -0,0 +1,101 @@
+/*
+ * 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 "SoLoader.h"
+
+#include "filesystem/SpecialProtocol.h"
+#include "utils/log.h"
+
+#include <dlfcn.h>
+
+SoLoader::SoLoader(const std::string &so, bool bGlobal) : LibraryLoader(so)
+{
+ m_soHandle = NULL;
+ m_bGlobal = bGlobal;
+ m_bLoaded = false;
+}
+
+SoLoader::~SoLoader()
+{
+ if (m_bLoaded)
+ Unload();
+}
+
+bool SoLoader::Load()
+{
+ if (m_soHandle != NULL)
+ return true;
+
+ std::string strFileName= CSpecialProtocol::TranslatePath(GetFileName());
+ if (strFileName == "xbmc.so")
+ {
+ CLog::Log(LOGDEBUG, "Loading Internal Library");
+ m_soHandle = RTLD_DEFAULT;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Loading: {}", strFileName);
+ int flags = RTLD_LAZY;
+ m_soHandle = dlopen(strFileName.c_str(), flags);
+ if (!m_soHandle)
+ {
+ CLog::Log(LOGERROR, "Unable to load {}, reason: {}", strFileName, dlerror());
+ return false;
+ }
+ }
+ m_bLoaded = true;
+ return true;
+}
+
+void SoLoader::Unload()
+{
+
+ if (m_soHandle)
+ {
+ if (dlclose(m_soHandle) != 0)
+ CLog::Log(LOGERROR, "Unable to unload {}, reason: {}", GetName(), dlerror());
+ }
+ m_bLoaded = false;
+ m_soHandle = NULL;
+}
+
+int SoLoader::ResolveExport(const char* symbol, void** f, bool logging)
+{
+ if (!m_bLoaded && !Load())
+ {
+ if (logging)
+ CLog::Log(LOGWARNING, "Unable to resolve: {} {}, reason: so not loaded", GetName(), symbol);
+ return 0;
+ }
+
+ void* s = dlsym(m_soHandle, symbol);
+ if (!s)
+ {
+ if (logging)
+ CLog::Log(LOGWARNING, "Unable to resolve: {} {}, reason: {}", GetName(), symbol, dlerror());
+ return 0;
+ }
+
+ *f = s;
+ return 1;
+}
+
+bool SoLoader::IsSystemDll()
+{
+ return false;
+}
+
+HMODULE SoLoader::GetHModule()
+{
+ return m_soHandle;
+}
+
+bool SoLoader::HasSymbols()
+{
+ return false;
+}
diff --git a/xbmc/cores/DllLoader/SoLoader.h b/xbmc/cores/DllLoader/SoLoader.h
new file mode 100644
index 0000000..4adc7e9
--- /dev/null
+++ b/xbmc/cores/DllLoader/SoLoader.h
@@ -0,0 +1,35 @@
+/*
+ * 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 <stdio.h>
+#ifdef TARGET_POSIX
+#include "PlatformDefs.h"
+#endif
+#include "DllLoader.h"
+
+class SoLoader : public LibraryLoader
+{
+public:
+ SoLoader(const std::string &so, bool bGlobal = false);
+ ~SoLoader() override;
+
+ bool Load() override;
+ void Unload() override;
+
+ int ResolveExport(const char* symbol, void** ptr, bool logging = true) override;
+ bool IsSystemDll() override;
+ HMODULE GetHModule() override;
+ bool HasSymbols() override;
+
+private:
+ void* m_soHandle;
+ bool m_bGlobal;
+ bool m_bLoaded;
+};
diff --git a/xbmc/cores/DllLoader/Win32DllLoader.cpp b/xbmc/cores/DllLoader/Win32DllLoader.cpp
new file mode 100644
index 0000000..be0d4a6
--- /dev/null
+++ b/xbmc/cores/DllLoader/Win32DllLoader.cpp
@@ -0,0 +1,408 @@
+/*
+ * 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 "Win32DllLoader.h"
+
+#include "DllLoader.h"
+#include "DllLoaderContainer.h"
+#include "dll_tracker_file.h"
+#include "dll_tracker_library.h"
+#include "exports/emu_msvcrt.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include "platform/win32/CharsetConverter.h"
+
+#include <limits>
+
+extern "C" FARPROC WINAPI dllWin32GetProcAddress(HMODULE hModule, LPCSTR function);
+
+//dllLoadLibraryA, dllFreeLibrary, dllGetProcAddress are from dllLoader,
+//they are wrapper functions of COFF/PE32 loader.
+extern "C" HMODULE WINAPI dllLoadLibraryA(LPCSTR libname);
+extern "C" BOOL WINAPI dllFreeLibrary(HINSTANCE hLibModule);
+
+// our exports
+Export win32_exports[] =
+{
+ { "LoadLibraryA", -1, (void*)dllLoadLibraryA, (void*)track_LoadLibraryA },
+ { "FreeLibrary", -1, (void*)dllFreeLibrary, (void*)track_FreeLibrary },
+// msvcrt
+ { "_close", -1, (void*)dll_close, (void*)track_close},
+ { "_lseek", -1, (void*)dll_lseek, NULL },
+ { "_read", -1, (void*)dll_read, NULL },
+ { "_write", -1, (void*)dll_write, NULL },
+ { "_lseeki64", -1, (void*)dll_lseeki64, NULL },
+ { "_open", -1, (void*)dll_open, (void*)track_open },
+ { "fflush", -1, (void*)dll_fflush, NULL },
+ { "fprintf", -1, (void*)dll_fprintf, NULL },
+ { "fwrite", -1, (void*)dll_fwrite, NULL },
+ { "putchar", -1, (void*)dll_putchar, NULL },
+ { "_fstat", -1, (void*)dll_fstat, NULL },
+ { "_mkdir", -1, (void*)dll_mkdir, NULL },
+ { "_stat", -1, (void*)dll_stat, NULL },
+ { "_fstat32", -1, (void*)dll_fstat, NULL },
+ { "_stat32", -1, (void*)dll_stat, NULL },
+ { "_findclose", -1, (void*)dll_findclose, NULL },
+ { "_findfirst", -1, (void*)dll_findfirst, NULL },
+ { "_findnext", -1, (void*)dll_findnext, NULL },
+ { "_findfirst64i32", -1, (void*)dll_findfirst64i32, NULL },
+ { "_findnext64i32", -1, (void*)dll_findnext64i32, NULL },
+ { "fclose", -1, (void*)dll_fclose, (void*)track_fclose},
+ { "feof", -1, (void*)dll_feof, NULL },
+ { "fgets", -1, (void*)dll_fgets, NULL },
+ { "fopen", -1, (void*)dll_fopen, (void*)track_fopen},
+ { "fopen_s", -1, (void*)dll_fopen_s, NULL },
+ { "putc", -1, (void*)dll_putc, NULL },
+ { "fputc", -1, (void*)dll_fputc, NULL },
+ { "fputs", -1, (void*)dll_fputs, NULL },
+ { "fread", -1, (void*)dll_fread, NULL },
+ { "fseek", -1, (void*)dll_fseek, NULL },
+ { "ftell", -1, (void*)dll_ftell, NULL },
+ { "getc", -1, (void*)dll_getc, NULL },
+ { "fgetc", -1, (void*)dll_getc, NULL },
+ { "rewind", -1, (void*)dll_rewind, NULL },
+ { "vfprintf", -1, (void*)dll_vfprintf, NULL },
+ { "fgetpos", -1, (void*)dll_fgetpos, NULL },
+ { "fsetpos", -1, (void*)dll_fsetpos, NULL },
+ { "_stati64", -1, (void*)dll_stati64, NULL },
+ { "_stat64", -1, (void*)dll_stat64, NULL },
+ { "_stat64i32", -1, (void*)dll_stat64i32, NULL },
+ { "_fstati64", -1, (void*)dll_fstati64, NULL },
+ { "_fstat64", -1, (void*)dll_fstat64, NULL },
+ { "_fstat64i32", -1, (void*)dll_fstat64i32, NULL },
+ { "_telli64", -1, (void*)dll_telli64, NULL },
+ { "_tell", -1, (void*)dll_tell, NULL },
+ { "_fileno", -1, (void*)dll_fileno, NULL },
+ { "ferror", -1, (void*)dll_ferror, NULL },
+ { "freopen", -1, (void*)dll_freopen, (void*)track_freopen},
+ { "fscanf", -1, (void*)dll_fscanf, NULL },
+ { "ungetc", -1, (void*)dll_ungetc, NULL },
+ { "_fdopen", -1, (void*)dll_fdopen, NULL },
+ { "clearerr", -1, (void*)dll_clearerr, NULL },
+ // for debugging
+ { "printf", -1, (void*)dllprintf, NULL },
+ { "vprintf", -1, (void*)dllvprintf, NULL },
+ { "perror", -1, (void*)dllperror, NULL },
+ { "puts", -1, (void*)dllputs, NULL },
+ // workarounds for non-win32 signals
+ { "signal", -1, (void*)dll_signal, NULL },
+
+ // libdvdnav + python need this (due to us using dll_putenv() to put stuff only?)
+ { "getenv", -1, (void*)dll_getenv, NULL },
+ { "_environ", -1, (void*)&dll__environ, NULL },
+ { "_open_osfhandle", -1, (void*)dll_open_osfhandle, NULL },
+
+ { NULL, -1, NULL, NULL }
+};
+
+Win32DllLoader::Win32DllLoader(const std::string& dll, bool isSystemDll)
+ : LibraryLoader(dll)
+ , bIsSystemDll(isSystemDll)
+{
+ m_dllHandle = NULL;
+ DllLoaderContainer::RegisterDll(this);
+}
+
+Win32DllLoader::~Win32DllLoader()
+{
+ if (m_dllHandle)
+ Unload();
+ DllLoaderContainer::UnRegisterDll(this);
+}
+
+bool Win32DllLoader::Load()
+{
+ using namespace KODI::PLATFORM::WINDOWS;
+
+ if (m_dllHandle != NULL)
+ return true;
+
+ std::string strFileName = GetFileName();
+ auto strDllW = ToW(CSpecialProtocol::TranslatePath(strFileName));
+
+#ifdef TARGET_WINDOWS_STORE
+ // The path cannot be an absolute path or a relative path that contains ".." in the path.
+ auto appPath = winrt::Windows::ApplicationModel::Package::Current().InstalledLocation().Path();
+ size_t len = appPath.size();
+
+ if (!appPath.empty() && wcsnicmp(appPath.c_str(), strDllW.c_str(), len) == 0)
+ {
+ if (strDllW.at(len) == '\\' || strDllW.at(len) == '/')
+ len++;
+ std::wstring relative = strDllW.substr(len);
+ m_dllHandle = LoadPackagedLibrary(relative.c_str(), 0);
+ }
+ else
+ m_dllHandle = LoadPackagedLibrary(strDllW.c_str(), 0);
+#else
+ m_dllHandle = LoadLibraryExW(strDllW.c_str(), NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
+#endif
+
+ if (!m_dllHandle)
+ {
+ DWORD dw = GetLastError();
+ wchar_t* lpMsgBuf = NULL;
+ DWORD strLen = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, dw, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), (LPWSTR)&lpMsgBuf, 0, NULL);
+ if (strLen == 0)
+ strLen = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL), (LPWSTR)&lpMsgBuf, 0, NULL);
+
+ if (strLen != 0)
+ {
+ auto strMessage = FromW(lpMsgBuf, strLen);
+ CLog::Log(LOGERROR, "{}: Failed to load \"{}\" with error {}: \"{}\"", __FUNCTION__,
+ CSpecialProtocol::TranslatePath(strFileName), dw, strMessage);
+ }
+ else
+ CLog::Log(LOGERROR, "{}: Failed to load \"{}\" with error {}", __FUNCTION__,
+ CSpecialProtocol::TranslatePath(strFileName), dw);
+
+ LocalFree(lpMsgBuf);
+ return false;
+ }
+
+ // handle functions that the dll imports
+ if (NeedsHooking(strFileName.c_str()))
+ OverrideImports(strFileName);
+
+ return true;
+}
+
+void Win32DllLoader::Unload()
+{
+ // restore our imports
+ RestoreImports();
+
+ if (m_dllHandle)
+ {
+ if (!FreeLibrary(m_dllHandle))
+ CLog::Log(LOGERROR, "{} Unable to unload {}", __FUNCTION__, GetName());
+ }
+
+ m_dllHandle = NULL;
+}
+
+int Win32DllLoader::ResolveExport(const char* symbol, void** f, bool logging)
+{
+ if (!m_dllHandle && !Load())
+ {
+ if (logging)
+ CLog::Log(LOGWARNING, "{} - Unable to resolve: {} {}, reason: DLL not loaded", __FUNCTION__,
+ GetName(), symbol);
+ return 0;
+ }
+
+ void *s = GetProcAddress(m_dllHandle, symbol);
+
+ if (!s)
+ {
+ if (logging)
+ CLog::Log(LOGWARNING, "{} - Unable to resolve: {} {}", __FUNCTION__, GetName(), symbol);
+ return 0;
+ }
+
+ *f = s;
+ return 1;
+}
+
+bool Win32DllLoader::IsSystemDll()
+{
+ return bIsSystemDll;
+}
+
+HMODULE Win32DllLoader::GetHModule()
+{
+ return m_dllHandle;
+}
+
+bool Win32DllLoader::HasSymbols()
+{
+ return false;
+}
+
+void Win32DllLoader::OverrideImports(const std::string &dll)
+{
+ using KODI::PLATFORM::WINDOWS::ToW;
+ auto strdllW = ToW(CSpecialProtocol::TranslatePath(dll));
+ auto image_base = reinterpret_cast<BYTE*>(m_dllHandle);
+
+ if (!image_base)
+ {
+ CLog::Log(LOGERROR, "{} - unable to GetModuleHandle for dll {}", __FUNCTION__, dll);
+ return;
+ }
+
+ auto dos_header = reinterpret_cast<PIMAGE_DOS_HEADER>(image_base);
+ auto nt_header = reinterpret_cast<PIMAGE_NT_HEADERS>(image_base + dos_header->e_lfanew); // e_lfanew = value at 0x3c
+
+ auto imp_desc = reinterpret_cast<PIMAGE_IMPORT_DESCRIPTOR>(
+ image_base + nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
+
+ if (!imp_desc)
+ {
+ CLog::Log(LOGERROR, "{} - unable to get import directory for dll {}", __FUNCTION__, dll);
+ return;
+ }
+
+ // loop over all imported dlls
+ for (int i = 0; imp_desc[i].Characteristics != 0; i++)
+ {
+ auto dllName = reinterpret_cast<char*>(image_base + imp_desc[i].Name);
+
+ // check whether this is one of our dll's.
+ if (NeedsHooking(dllName))
+ {
+ // this will do a loadlibrary on it, which should effectively make sure that it's hooked
+ // Note that the library has obviously already been loaded by the OS (as it's implicitly linked)
+ // so all this will do is insert our hook and make sure our DllLoaderContainer knows about it
+ auto hModule = dllLoadLibraryA(dllName);
+ if (hModule)
+ m_referencedDlls.push_back(hModule);
+ }
+
+ PIMAGE_THUNK_DATA orig_first_thunk = reinterpret_cast<PIMAGE_THUNK_DATA>(image_base + imp_desc[i].OriginalFirstThunk);
+ PIMAGE_THUNK_DATA first_thunk = reinterpret_cast<PIMAGE_THUNK_DATA>(image_base + imp_desc[i].FirstThunk);
+
+ // and then loop over all imported functions
+ for (int j = 0; orig_first_thunk[j].u1.Function != 0; j++)
+ {
+ void *fixup = NULL;
+ if (orig_first_thunk[j].u1.Function & 0x80000000)
+ ResolveOrdinal(dllName, (orig_first_thunk[j].u1.Ordinal & 0x7fffffff), &fixup);
+ else
+ { // resolve by name
+ PIMAGE_IMPORT_BY_NAME orig_imports_by_name = (PIMAGE_IMPORT_BY_NAME)(
+ image_base + orig_first_thunk[j].u1.AddressOfData);
+
+ ResolveImport(dllName, (char*)orig_imports_by_name->Name, &fixup);
+ }/*
+ if (!fixup)
+ { // create a dummy function for tracking purposes
+ PIMAGE_IMPORT_BY_NAME orig_imports_by_name = (PIMAGE_IMPORT_BY_NAME)(
+ image_base + orig_first_thunk[j].u1.AddressOfData);
+ fixup = CreateDummyFunction(dllName, (char*)orig_imports_by_name->Name);
+ }*/
+ if (fixup)
+ {
+ // save the old function
+ Import import;
+ import.table = &first_thunk[j].u1.Function;
+ import.function = first_thunk[j].u1.Function;
+ m_overriddenImports.push_back(import);
+
+ DWORD old_prot = 0;
+
+ // change to protection settings so we can write to memory area
+ VirtualProtect((PVOID)&first_thunk[j].u1.Function, 4, PAGE_EXECUTE_READWRITE, &old_prot);
+
+ // patch the address of function to point to our overridden version
+ first_thunk[j].u1.Function = (uintptr_t)fixup;
+
+ // reset to old settings
+ VirtualProtect((PVOID)&first_thunk[j].u1.Function, 4, old_prot, &old_prot);
+ }
+ }
+ }
+}
+
+bool Win32DllLoader::NeedsHooking(const char *dllName)
+{
+ if ( !StringUtils::EndsWithNoCase(dllName, "libdvdcss-2.dll")
+ && !StringUtils::EndsWithNoCase(dllName, "libdvdnav.dll"))
+ return false;
+
+ LibraryLoader *loader = DllLoaderContainer::GetModule(dllName);
+ if (loader)
+ {
+ // may have hooked this already (we can have repeats in the import table)
+ for (unsigned int i = 0; i < m_referencedDlls.size(); i++)
+ {
+ if (loader->GetHModule() == m_referencedDlls[i])
+ return false;
+ }
+ }
+ return true;
+}
+
+void Win32DllLoader::RestoreImports()
+{
+ // first unhook any referenced dll's
+ for (auto& module : m_referencedDlls)
+ dllFreeLibrary(module);
+ m_referencedDlls.clear();
+
+ for (auto& import : m_overriddenImports)
+ {
+ // change to protection settings so we can write to memory area
+ DWORD old_prot = 0;
+ VirtualProtect(import.table, 4, PAGE_EXECUTE_READWRITE, &old_prot);
+
+ *static_cast<uintptr_t *>(import.table) = import.function;
+
+ // reset to old settings
+ VirtualProtect(import.table, 4, old_prot, &old_prot);
+ }
+}
+
+bool FunctionNeedsWrapping(Export *exports, const char *functionName, void **fixup)
+{
+ Export *exp = exports;
+ while (exp->name)
+ {
+ if (strcmp(exp->name, functionName) == 0)
+ { //! @todo Should we be tracking stuff?
+ if (0)
+ *fixup = exp->track_function;
+ else
+ *fixup = exp->function;
+ return true;
+ }
+ exp++;
+ }
+ return false;
+}
+
+bool Win32DllLoader::ResolveImport(const char *dllName, const char *functionName, void **fixup)
+{
+ return FunctionNeedsWrapping(win32_exports, functionName, fixup);
+}
+
+bool Win32DllLoader::ResolveOrdinal(const char *dllName, unsigned long ordinal, void **fixup)
+{
+ Export *exp = win32_exports;
+ while (exp->name)
+ {
+ if (exp->ordinal == ordinal)
+ { //! @todo Should we be tracking stuff?
+ if (0)
+ *fixup = exp->track_function;
+ else
+ *fixup = exp->function;
+ return true;
+ }
+ exp++;
+ }
+ return false;
+}
+
+extern "C" FARPROC __stdcall dllWin32GetProcAddress(HMODULE hModule, LPCSTR function)
+{
+ // if the high-order word is zero, then lpProcName is the function's ordinal value
+ if (reinterpret_cast<uintptr_t>(function) > std::numeric_limits<WORD>::max())
+ {
+ // first check whether this function is one of the ones we need to wrap
+ void *fixup = NULL;
+ if (FunctionNeedsWrapping(win32_exports, function, &fixup))
+ return (FARPROC)fixup;
+ }
+
+ // Nope
+ return GetProcAddress(hModule, function);
+}
+
diff --git a/xbmc/cores/DllLoader/Win32DllLoader.h b/xbmc/cores/DllLoader/Win32DllLoader.h
new file mode 100644
index 0000000..f11eb54
--- /dev/null
+++ b/xbmc/cores/DllLoader/Win32DllLoader.h
@@ -0,0 +1,49 @@
+/*
+ * 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 "LibraryLoader.h"
+
+#include <vector>
+
+class Win32DllLoader : public LibraryLoader
+{
+public:
+ class Import
+ {
+ public:
+ void *table;
+ uintptr_t function;
+ };
+
+ Win32DllLoader(const std::string& dll, bool isSystemDll);
+ ~Win32DllLoader();
+
+ virtual bool Load();
+ virtual void Unload();
+
+ virtual int ResolveExport(const char* symbol, void** ptr, bool logging = true);
+ virtual bool IsSystemDll();
+ virtual HMODULE GetHModule();
+ virtual bool HasSymbols();
+
+private:
+ void OverrideImports(const std::string &dll);
+ void RestoreImports();
+ static bool ResolveImport(const char *dllName, const char *functionName, void **fixup);
+ static bool ResolveOrdinal(const char *dllName, unsigned long ordinal, void **fixup);
+ bool NeedsHooking(const char *dllName);
+
+ HMODULE m_dllHandle;
+ bool bIsSystemDll;
+
+ std::vector<Import> m_overriddenImports;
+ std::vector<HMODULE> m_referencedDlls;
+};
+
diff --git a/xbmc/cores/DllLoader/coff.cpp b/xbmc/cores/DllLoader/coff.cpp
new file mode 100644
index 0000000..6e330c9
--- /dev/null
+++ b/xbmc/cores/DllLoader/coff.cpp
@@ -0,0 +1,997 @@
+/*
+ * 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 <stdlib.h>
+#include <string.h>
+#include "coff.h"
+#include "coffldr.h"
+
+//#define DUMPING_DATA 1
+
+#ifndef __GNUC__
+#pragma warning (disable:4806)
+#endif
+
+#include "utils/log.h"
+#define printf(format, ...) CLog::Log(LOGDEBUG, format , ##__VA_ARGS__)
+
+const char *DATA_DIR_NAME[16] =
+ {
+ "Export Table",
+ "Import Table",
+ "Resource Table",
+ "Exception Table",
+ "Certificate Table",
+ "Base Relocation Table",
+ "Debug",
+ "Architecture",
+ "Global Ptr",
+ "TLS Table",
+ "Load Config Table",
+ "Bound Import",
+ "IAT",
+ "Delay Import Descriptor",
+ "COM+ Runtime Header",
+ "Reserved"
+ };
+
+
+CoffLoader::CoffLoader()
+{
+ CoffFileHeader = 0;
+ OptionHeader = 0;
+ WindowsHeader = 0;
+ Directory = 0;
+ SectionHeader = 0;
+ SymTable = 0;
+ StringTable = 0;
+ SectionData = 0;
+
+ NumberOfSymbols = 0;
+ SizeOfStringTable = 0;
+ NumOfDirectories = 0;
+ NumOfSections = 0;
+ FileHeaderOffset = 0;
+ EntryAddress = 0;
+ hModule = NULL;
+}
+
+CoffLoader::~CoffLoader()
+{
+ if ( hModule )
+ {
+#ifdef TARGET_POSIX
+ free(hModule);
+#else
+ VirtualFree(hModule, 0, MEM_RELEASE);
+#endif
+ hModule = NULL;
+ }
+ if ( SymTable )
+ {
+ delete [] SymTable;
+ SymTable = 0;
+ }
+ if ( StringTable )
+ {
+ delete [] StringTable;
+ StringTable = 0;
+ }
+ if ( SectionData )
+ {
+ delete [] SectionData;
+ SectionData = 0;
+ }
+}
+
+// Has nothing to do with the coff loader itself
+// it can be used to parse the headers of a dll
+// already loaded into memory
+int CoffLoader::ParseHeaders(void* hModule)
+{
+ if (strncmp((char*)hModule, "MZ", 2) != 0)
+ return 0;
+
+ int* Offset = (int*)((char*)hModule+0x3c);
+ if (*Offset <= 0)
+ return 0;
+
+ if (strncmp((char*)hModule+*Offset, "PE\0\0", 4) != 0)
+ return 0;
+
+ FileHeaderOffset = *Offset + 4;
+
+ CoffFileHeader = (COFF_FileHeader_t *) ( (char*)hModule + FileHeaderOffset );
+ NumOfSections = CoffFileHeader->NumberOfSections;
+
+ OptionHeader = (OptionHeader_t *) ( (char*)CoffFileHeader + sizeof(COFF_FileHeader_t) );
+ WindowsHeader = (WindowsHeader_t *) ( (char*)OptionHeader + OPTHDR_SIZE );
+ EntryAddress = OptionHeader->Entry;
+ NumOfDirectories = WindowsHeader->NumDirectories;
+
+ Directory = (Image_Data_Directory_t *) ( (char*)WindowsHeader + WINHDR_SIZE);
+ SectionHeader = (SectionHeader_t *) ( (char*)Directory + sizeof(Image_Data_Directory_t) * NumOfDirectories);
+
+ if (CoffFileHeader->MachineType != IMAGE_FILE_MACHINE_I386)
+ return 0;
+
+#ifdef DUMPING_DATA
+ PrintFileHeader(CoffFileHeader);
+#endif
+
+ if ( CoffFileHeader->SizeOfOptionHeader == 0 ) //not an image file, object file maybe
+ return 0;
+
+ // process Option Header
+ if (OptionHeader->Magic == OPTMAGIC_PE32P)
+ {
+ printf("PE32+ not supported\n");
+ return 0;
+ }
+ else if (OptionHeader->Magic == OPTMAGIC_PE32)
+ {
+
+#ifdef DUMPING_DATA
+ PrintOptionHeader(OptionHeader);
+ PrintWindowsHeader(WindowsHeader);
+#endif
+
+ }
+ else
+ {
+ //add error message
+ return 0;
+ }
+
+#ifdef DUMPING_DATA
+ for (int DirCount = 0; DirCount < NumOfDirectories; DirCount++)
+ {
+ printf("Data Directory %02d: %s\n", DirCount + 1, DATA_DIR_NAME[DirCount]);
+ printf(" RVA: %08X\n", Directory[DirCount].RVA);
+ printf(" Size: %08X\n\n", Directory[DirCount].Size);
+ }
+#endif
+
+ return 1;
+
+}
+
+int CoffLoader::LoadCoffHModule(FILE *fp)
+{
+ //test file signatures
+ char Sig[4];
+ rewind(fp);
+ memset(Sig, 0, sizeof(Sig));
+ if (!fread(Sig, 1, 2, fp) || strncmp(Sig, "MZ", 2) != 0)
+ return 0;
+
+ if (fseek(fp, 0x3c, SEEK_SET) != 0)
+ return 0;
+
+ int Offset = 0;
+ if (!fread(&Offset, sizeof(int), 1, fp) || (Offset <= 0))
+ return 0;
+
+ if (fseek(fp, Offset, SEEK_SET) != 0)
+ return 0;
+
+ memset(Sig, 0, sizeof(Sig));
+ if (!fread(Sig, 1, 4, fp) || strncmp(Sig, "PE\0\0", 4) != 0)
+ return 0;
+
+ Offset += 4;
+ FileHeaderOffset = Offset;
+
+ // Load and process Header
+ if (fseek(fp, FileHeaderOffset + sizeof(COFF_FileHeader_t) + OPTHDR_SIZE, SEEK_SET)) //skip to winows headers
+ return 0;
+
+ WindowsHeader_t tempWindowsHeader;
+ size_t readcount = fread(&tempWindowsHeader, 1, WINHDR_SIZE, fp);
+ if (readcount != WINHDR_SIZE) //test file size error
+ return 0;
+
+ // alloc aligned memory
+#ifdef TARGET_POSIX
+ hModule = malloc(tempWindowsHeader.SizeOfImage);
+#elif defined TARGET_WINDOWS_STORE
+ hModule = VirtualAllocFromApp(GetCurrentProcess(), tempWindowsHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
+#else
+ hModule = VirtualAllocEx(GetCurrentProcess(), (PVOID)tempWindowsHeader.ImageBase, tempWindowsHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
+ if (hModule == NULL)
+ hModule = VirtualAlloc(GetCurrentProcess(), tempWindowsHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
+#endif
+ if (hModule == NULL)
+ return 0; //memory allocation fails
+
+ rewind(fp);
+ readcount = fread(hModule, 1, tempWindowsHeader.SizeOfHeaders, fp);
+ if (readcount != tempWindowsHeader.SizeOfHeaders) //file size error
+ return 0;
+
+ CoffFileHeader = (COFF_FileHeader_t *) ( (char*)hModule + FileHeaderOffset );
+ NumOfSections = CoffFileHeader->NumberOfSections;
+
+ OptionHeader = (OptionHeader_t *) ( (char*)CoffFileHeader + sizeof(COFF_FileHeader_t) );
+ WindowsHeader = (WindowsHeader_t *) ( (char*)OptionHeader + OPTHDR_SIZE );
+ EntryAddress = OptionHeader->Entry;
+ NumOfDirectories = WindowsHeader->NumDirectories;
+
+ Directory = (Image_Data_Directory_t *) ( (char*)WindowsHeader + WINHDR_SIZE);
+ SectionHeader = (SectionHeader_t *) ( (char*)Directory + sizeof(Image_Data_Directory_t) * NumOfDirectories);
+
+ if (CoffFileHeader->MachineType != IMAGE_FILE_MACHINE_I386)
+ return 0;
+
+#ifdef DUMPING_DATA
+ PrintFileHeader(CoffFileHeader);
+#endif
+
+ if ( CoffFileHeader->SizeOfOptionHeader == 0 ) //not an image file, object file maybe
+ return 0;
+
+ // process Option Header
+ if (OptionHeader->Magic == OPTMAGIC_PE32P)
+ {
+ printf("PE32+ not supported\n");
+ return 0;
+ }
+ else if (OptionHeader->Magic == OPTMAGIC_PE32)
+ {
+
+#ifdef DUMPING_DATA
+ PrintOptionHeader(OptionHeader);
+ PrintWindowsHeader(WindowsHeader);
+#endif
+
+ }
+ else
+ {
+ //add error message
+ return 0;
+ }
+
+#ifdef DUMPING_DATA
+ for (int DirCount = 0; DirCount < NumOfDirectories; DirCount++)
+ {
+ printf("Data Directory %02d: %s\n", DirCount + 1, DATA_DIR_NAME[DirCount]);
+ printf(" RVA: %08X\n", Directory[DirCount].RVA);
+ printf(" Size: %08X\n\n", Directory[DirCount].Size);
+ }
+#endif
+
+ return 1;
+
+}
+
+int CoffLoader::LoadSymTable(FILE *fp)
+{
+ int Offset = ftell(fp);
+ if (Offset < 0)
+ return 0;
+
+ if ( CoffFileHeader->PointerToSymbolTable == 0 )
+ return 1;
+
+ if (fseek(fp, CoffFileHeader->PointerToSymbolTable /* + CoffBeginOffset*/, SEEK_SET) != 0)
+ return 0;
+
+ SymbolTable_t *tmp = new SymbolTable_t[CoffFileHeader->NumberOfSymbols];
+ if (!tmp)
+ {
+ printf("Could not allocate memory for symbol table!\n");
+ return 0;
+ }
+ if (!fread((void *)tmp, CoffFileHeader->NumberOfSymbols, sizeof(SymbolTable_t), fp))
+ {
+ delete[] tmp;
+ return 0;
+ }
+ NumberOfSymbols = CoffFileHeader->NumberOfSymbols;
+ SymTable = tmp;
+ if (fseek(fp, Offset, SEEK_SET) != 0)
+ return 0;
+ return 1;
+}
+
+int CoffLoader::LoadStringTable(FILE *fp)
+{
+ int StringTableSize;
+ char *tmp = NULL;
+
+ int Offset = ftell(fp);
+ if (Offset < 0)
+ return 0;
+
+ if ( CoffFileHeader->PointerToSymbolTable == 0 )
+ return 1;
+
+ if (fseek(fp, CoffFileHeader->PointerToSymbolTable +
+ CoffFileHeader->NumberOfSymbols * sizeof(SymbolTable_t),
+ SEEK_SET) != 0)
+ return 0;
+
+ if (!fread(&StringTableSize, 1, sizeof(int), fp))
+ return 0;
+ StringTableSize -= 4;
+ if (StringTableSize != 0)
+ {
+ tmp = new char[StringTableSize];
+ if (tmp == NULL)
+ {
+ printf("Could not allocate memory for string table\n");
+ return 0;
+ }
+ if (!fread((void *)tmp, StringTableSize, sizeof(char), fp))
+ {
+ delete[] tmp;
+ return 0;
+ }
+ }
+ SizeOfStringTable = StringTableSize;
+ StringTable = tmp;
+ if (fseek(fp, Offset, SEEK_SET) != 0)
+ return 0;
+ return 1;
+}
+
+int CoffLoader::LoadSections(FILE *fp)
+{
+ NumOfSections = CoffFileHeader->NumberOfSections;
+
+ SectionData = new char * [NumOfSections];
+ if ( !SectionData )
+ return 0;
+
+ // Bobbin007: for debug dlls this check always fails
+
+ //////check VMA size!!!!!
+ //unsigned long vma_size = 0;
+ //for (int SctnCnt = 0; SctnCnt < NumOfSections; SctnCnt++)
+ //{
+ // SectionHeader_t *ScnHdr = (SectionHeader_t *)(SectionHeader + SctnCnt);
+ // vma_size = max(vma_size, ScnHdr->VirtualAddress + ScnHdr->SizeOfRawData);
+ // vma_size = max(vma_size, ScnHdr->VirtualAddress + ScnHdr->VirtualSize);
+ //}
+
+ //if (WindowsHeader->SizeOfImage < vma_size)
+ // return 0; //something wrong with file
+
+ for (int SctnCnt = 0; SctnCnt < NumOfSections; SctnCnt++)
+ {
+ SectionHeader_t *ScnHdr = SectionHeader + SctnCnt;
+ SectionData[SctnCnt] = ((char*)hModule + ScnHdr->VirtualAddress);
+
+ if (fseek(fp, ScnHdr->PtrToRawData, SEEK_SET) != 0)
+ return 0;
+
+ if (!fread(SectionData[SctnCnt], 1, ScnHdr->SizeOfRawData, fp))
+ return 0;
+
+#ifdef DUMPING_DATA
+ //debug blocks
+ char szBuf[128];
+ char namebuf[9];
+ for (int i = 0; i < 8; i++)
+ namebuf[i] = ScnHdr->Name[i];
+ namebuf[8] = '\0';
+ sprintf(szBuf, "Load code Sections %s Memory %p,Length %x\n", namebuf,
+ SectionData[SctnCnt], max(ScnHdr->VirtualSize, ScnHdr->SizeOfRawData));
+ OutputDebugString(szBuf);
+#endif
+
+ if ( ScnHdr->SizeOfRawData < ScnHdr->VirtualSize ) //initialize BSS data in the end of section
+ {
+ memset((char*)((long)(SectionData[SctnCnt]) + ScnHdr->SizeOfRawData), 0, ScnHdr->VirtualSize - ScnHdr->SizeOfRawData);
+ }
+
+ if (ScnHdr->Characteristics & IMAGE_SCN_CNT_BSS) //initialize whole .BSS section, pure .BSS is obsolete
+ {
+ memset(SectionData[SctnCnt], 0, ScnHdr->VirtualSize);
+ }
+
+#ifdef DUMPING_DATA
+ PrintSection(SectionHeader + SctnCnt, SectionData[SctnCnt]);
+#endif
+
+ }
+ return 1;
+}
+
+//FIXME: Add the Free Resources functions
+
+int CoffLoader::RVA2Section(unsigned long RVA)
+{
+ NumOfSections = CoffFileHeader->NumberOfSections;
+ for ( int i = 0; i < NumOfSections; i++)
+ {
+ if ( SectionHeader[i].VirtualAddress <= RVA )
+ {
+ if ( i + 1 != NumOfSections )
+ {
+ if ( RVA < SectionHeader[i + 1].VirtualAddress )
+ {
+ if ( SectionHeader[i].VirtualAddress + SectionHeader[i].VirtualSize <= RVA )
+ printf("Warning! Address outside of Section: %lx!\n", RVA);
+ // else
+ return i;
+ }
+ }
+ else
+ {
+ if ( SectionHeader[i].VirtualAddress + SectionHeader[i].VirtualSize <= RVA )
+ printf("Warning! Address outside of Section: %lx!\n", RVA);
+ // else
+ return i;
+ }
+ }
+ }
+ printf("RVA2Section lookup failure!\n");
+ return 0;
+}
+
+void* CoffLoader::RVA2Data(unsigned long RVA)
+{
+ int Sctn = RVA2Section(RVA);
+
+ if( RVA < SectionHeader[Sctn].VirtualAddress
+ || RVA >= SectionHeader[Sctn].VirtualAddress + SectionHeader[Sctn].VirtualSize)
+ {
+ // RVA2Section is lying, let's use base address of dll instead, only works if
+ // DLL has been loaded fully into memory, which we normally do
+ return (void*)(RVA + (unsigned long)hModule);
+ }
+ return SectionData[Sctn] + RVA - SectionHeader[Sctn].VirtualAddress;
+}
+
+unsigned long CoffLoader::Data2RVA(void* address)
+{
+ for ( int i = 0; i < CoffFileHeader->NumberOfSections; i++)
+ {
+ if(address >= SectionData[i] && address < SectionData[i] + SectionHeader[i].VirtualSize)
+ return (unsigned long)address - (unsigned long)SectionData[i] + SectionHeader[i].VirtualAddress;
+ }
+
+ // Section wasn't found, so use relative to main load of dll
+ return (unsigned long)address - (unsigned long)hModule;
+}
+
+char *CoffLoader::GetStringTblIndex(int index)
+{
+ char *table = StringTable;
+
+ while (index--)
+ table += strlen(table) + 1;
+ return table;
+}
+
+char *CoffLoader::GetStringTblOff(int Offset)
+{
+ return StringTable + Offset - 4;
+}
+
+char *CoffLoader::GetSymbolName(SymbolTable_t *sym)
+{
+ long long index = sym->Name.Offset;
+ int low = (int)(index & 0xFFFFFFFF);
+ int high = (int)((index >> 32) & 0xFFFFFFFF);
+
+ if (low == 0)
+ {
+ return GetStringTblOff(high);
+ }
+ else
+ {
+ static char shortname[9];
+ memset(shortname, 0, 9);
+ strncpy(shortname, (char *)sym->Name.ShortName, 8);
+ return shortname;
+ }
+}
+
+char *CoffLoader::GetSymbolName(int index)
+{
+ SymbolTable_t *sym = &(SymTable[index]);
+ return GetSymbolName(sym);
+}
+
+void CoffLoader::PrintStringTable(void)
+{
+ int size = SizeOfStringTable;
+ int index = 0;
+ char *table = StringTable;
+
+ printf("\nSTRING TABLE\n");
+ while (size)
+ {
+ printf("%2d: %s\n", index++, table);
+ size -= strlen(table) + 1;
+ table += strlen(table) + 1;
+ }
+ printf("\n");
+}
+
+
+void CoffLoader::PrintSymbolTable(void)
+{
+ int SymIndex;
+
+ printf("COFF SYMBOL TABLE\n");
+ for (SymIndex = 0; SymIndex < NumberOfSymbols; SymIndex++)
+ {
+ printf("%03X ", SymIndex);
+ printf("%08lX ", SymTable[SymIndex].Value);
+
+ if (SymTable[SymIndex].SectionNumber == IMAGE_SYM_ABSOLUTE)
+ printf("ABS ");
+ else if (SymTable[SymIndex].SectionNumber == IMAGE_SYM_DEBUG)
+ printf("DEBUG ");
+ else if (SymTable[SymIndex].SectionNumber == IMAGE_SYM_UNDEFINED)
+ printf("UNDEF ");
+ else
+ {
+ printf("SECT%d ", SymTable[SymIndex].SectionNumber);
+ if (SymTable[SymIndex].SectionNumber < 10)
+ printf(" ");
+ if (SymTable[SymIndex].SectionNumber < 100)
+ printf(" ");
+ }
+
+ if (SymTable[SymIndex].Type == 0)
+ printf("notype ");
+ else
+ {
+ printf("%X ", SymTable[SymIndex].Type);
+ if (SymTable[SymIndex].Type < 0x10)
+ printf(" ");
+ if (SymTable[SymIndex].Type < 0x100)
+ printf(" ");
+ if (SymTable[SymIndex].Type < 0x1000)
+ printf(" ");
+ }
+
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_END_OF_FUNCTION)
+ printf("End of Function ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_NULL)
+ printf("Null ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_AUTOMATIC)
+ printf("Automatic ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_EXTERNAL)
+ printf("External ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_STATIC)
+ printf("Static ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_REGISTER)
+ printf("Register ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_EXTERNAL_DEF)
+ printf("External Def ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_LABEL)
+ printf("Label ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_UNDEFINED_LABEL)
+ printf("Undefined Label ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_MEMBER_OF_STRUCT)
+ printf("Member Of Struct ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_ARGUMENT)
+ printf("Argument ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_STRUCT_TAG)
+ printf("Struct Tag ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_MEMBER_OF_UNION)
+ printf("Member Of Union ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_UNION_TAG)
+ printf("Union Tag ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_TYPE_DEFINITION)
+ printf("Type Definition ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_UNDEFINED_STATIC)
+ printf("Undefined Static ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_ENUM_TAG)
+ printf("Enum Tag ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_MEMBER_OF_ENUM)
+ printf("Member Of Enum ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_REGISTER_PARAM)
+ printf("Register Param ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_BIT_FIELD)
+ printf("Bit Field ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_BLOCK)
+ printf("Block ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_FUNCTION)
+ printf("Function ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_END_OF_STRUCT)
+ printf("End Of Struct ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_FILE)
+ printf("File ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_SECTION)
+ printf("Section ");
+ if (SymTable[SymIndex].StorageClass == IMAGE_SYM_CLASS_WEAK_EXTERNAL)
+ printf("Weak External ");
+
+ printf("| %s", GetSymbolName(SymIndex));
+
+ SymIndex += SymTable[SymIndex].NumberOfAuxSymbols;
+ printf("\n");
+ }
+ printf("\n");
+
+}
+
+void CoffLoader::PrintFileHeader(COFF_FileHeader_t *FileHeader)
+{
+ printf("COFF Header\n");
+ printf("------------------------------------------\n\n");
+
+ printf("MachineType: 0x%04X\n", FileHeader->MachineType);
+ printf("NumberOfSections: 0x%04X\n", FileHeader->NumberOfSections);
+ printf("TimeDateStamp: 0x%08lX\n",FileHeader->TimeDateStamp);
+ printf("PointerToSymbolTable: 0x%08lX\n",FileHeader->PointerToSymbolTable);
+ printf("NumberOfSymbols: 0x%08lX\n",FileHeader->NumberOfSymbols);
+ printf("SizeOfOptionHeader: 0x%04X\n", FileHeader->SizeOfOptionHeader);
+ printf("Characteristics: 0x%04X\n", FileHeader->Characteristics);
+
+ if (FileHeader->Characteristics & IMAGE_FILE_RELOCS_STRIPPED)
+ printf(" IMAGE_FILE_RELOCS_STRIPPED\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE)
+ printf(" IMAGE_FILE_EXECUTABLE_IMAGE\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_LINE_NUMS_STRIPPED)
+ printf(" IMAGE_FILE_LINE_NUMS_STRIPPED\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_LOCAL_SYMS_STRIPPED)
+ printf(" IMAGE_FILE_LOCAL_SYMS_STRIPPED\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_AGGRESSIVE_WS_TRIM)
+ printf(" IMAGE_FILE_AGGRESSIVE_WS_TRIM\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_LARGE_ADDRESS_AWARE)
+ printf(" IMAGE_FILE_LARGE_ADDRESS_AWARE\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_16BIT_MACHINE)
+ printf(" IMAGE_FILE_16BIT_MACHINE\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_BYTES_REVERSED_LO)
+ printf(" IMAGE_FILE_BYTES_REVERSED_LO\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_32BIT_MACHINE)
+ printf(" IMAGE_FILE_32BIT_MACHINE\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_DEBUG_STRIPPED)
+ printf(" IMAGE_FILE_DEBUG_STRIPPED\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP)
+ printf(" IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_SYSTEM)
+ printf(" IMAGE_FILE_SYSTEM\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_DLL)
+ printf(" IMAGE_FILE_DLL\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_UP_SYSTEM_ONLY)
+ printf(" IMAGE_FILE_UP_SYSTEM_ONLY\n");
+
+ if (FileHeader->Characteristics & IMAGE_FILE_BYTES_REVERSED_HI)
+ printf(" IMAGE_FILE_BYTES_REVERSED_HI\n");
+
+ printf("\n");
+}
+
+void CoffLoader::PrintOptionHeader(OptionHeader_t *OptHdr)
+{
+ printf("Option Header\n");
+ printf("------------------------------------------\n\n");
+
+ printf("Magic: 0x%04X\n", OptHdr->Magic);
+ printf("Linker Major Ver: 0x%02X\n", VERSION_MAJOR(OptHdr->LinkVersion));
+ printf("Linker Minor Ver: 0x%02X\n", VERSION_MINOR(OptHdr->LinkVersion));
+ printf("Code Size: 0x%08lX\n", OptHdr->CodeSize);
+ printf("Data Size: 0x%08lX\n", OptHdr->DataSize);
+ printf("BSS Size: 0x%08lX\n", OptHdr->BssSize);
+ printf("Entry: 0x%08lX\n", OptHdr->Entry);
+ printf("Code Base: 0x%08lX\n", OptHdr->CodeBase);
+ printf("Data Base: 0x%08lX\n", OptHdr->DataBase);
+ printf("\n");
+}
+
+void CoffLoader::PrintWindowsHeader(WindowsHeader_t *WinHdr)
+{
+ printf("Windows Specific Option Header\n");
+ printf("------------------------------------------\n\n");
+
+ printf("Image Base: 0x%08lX\n", WinHdr->ImageBase);
+ printf("Section Alignment: 0x%08lX\n", WinHdr->SectionAlignment);
+ printf("File Alignment: 0x%08lX\n", WinHdr->FileAlignment);
+ printf("OS Version: %d.%08d\n", BIGVERSION_MAJOR(WinHdr->OSVer), BIGVERSION_MINOR(WinHdr->OSVer));
+ printf("Image Version: %d.%08d\n", BIGVERSION_MAJOR(WinHdr->ImgVer), BIGVERSION_MINOR(WinHdr->ImgVer));
+ printf("SubSystem Version: %d.%08d\n", BIGVERSION_MAJOR(WinHdr->SubSysVer), BIGVERSION_MINOR(WinHdr->SubSysVer));
+ printf("Size of Image: 0x%08lX\n", WinHdr->SizeOfImage);
+ printf("Size of Headers: 0x%08lX\n", WinHdr->SizeOfHeaders);
+ printf("Checksum: 0x%08lX\n", WinHdr->CheckSum);
+ printf("Subsystem: 0x%04X\n", WinHdr->Subsystem);
+ printf("DLL Flags: 0x%04X\n", WinHdr->DLLFlags);
+ printf("Sizeof Stack Resv: 0x%08lX\n", WinHdr->SizeOfStackReserve);
+ printf("Sizeof Stack Comm: 0x%08lX\n", WinHdr->SizeOfStackCommit);
+ printf("Sizeof Heap Resv: 0x%08lX\n", WinHdr->SizeOfHeapReserve);
+ printf("Sizeof Heap Comm: 0x%08lX\n", WinHdr->SizeOfHeapCommit);
+ printf("Loader Flags: 0x%08lX\n", WinHdr->LoaderFlags);
+ printf("Num Directories: %ld\n", WinHdr->NumDirectories);
+ printf("\n");
+}
+
+void CoffLoader::PrintSection(SectionHeader_t* ScnHdr, const char* data)
+{
+ char SectionName[9];
+
+ strncpy(SectionName, (char *)ScnHdr->Name, 8);
+ SectionName[8] = 0;
+ printf("Section: %s\n", SectionName);
+ printf("------------------------------------------\n\n");
+
+ printf("Virtual Size: 0x%08lX\n", ScnHdr->VirtualSize);
+ printf("Virtual Address: 0x%08lX\n", ScnHdr->VirtualAddress);
+ printf("Sizeof Raw Data: 0x%08lX\n", ScnHdr->SizeOfRawData);
+ printf("Ptr To Raw Data: 0x%08lX\n", ScnHdr->PtrToRawData);
+ printf("Ptr To Relocations: 0x%08lX\n", ScnHdr->PtrToRelocations);
+ printf("Ptr To Line Nums: 0x%08lX\n", ScnHdr->PtrToLineNums);
+ printf("Num Relocations: 0x%04X\n", ScnHdr->NumRelocations);
+ printf("Num Line Numbers: 0x%04X\n", ScnHdr->NumLineNumbers);
+ printf("Characteristics: 0x%08lX\n", ScnHdr->Characteristics);
+ if (ScnHdr->Characteristics & IMAGE_SCN_CNT_CODE)
+ printf(" IMAGE_SCN_CNT_CODE\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_CNT_DATA)
+ printf(" IMAGE_SCN_CNT_DATA\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_CNT_BSS)
+ printf(" IMAGE_SCN_CNT_BSS\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_LNK_INFO)
+ printf(" IMAGE_SCN_LNK_INFO\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_LNK_REMOVE)
+ printf(" IMAGE_SCN_LNK_REMOVE\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_LNK_COMDAT)
+ printf(" IMAGE_SCN_LNK_COMDAT\n");
+
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_1BYTES)
+ printf(" IMAGE_SCN_ALIGN_1BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_2BYTES)
+ printf(" IMAGE_SCN_ALIGN_2BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_4BYTES)
+ printf(" IMAGE_SCN_ALIGN_4BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_8BYTES)
+ printf(" IMAGE_SCN_ALIGN_8BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_16BYTES)
+ printf(" IMAGE_SCN_ALIGN_16BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_32BYTES)
+ printf(" IMAGE_SCN_ALIGN_32BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_64BYTES)
+ printf(" IMAGE_SCN_ALIGN_64BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_128BYTES)
+ printf(" IMAGE_SCN_ALIGN_128BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_256BYTES)
+ printf(" IMAGE_SCN_ALIGN_256BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_512BYTES)
+ printf(" IMAGE_SCN_ALIGN_512BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_1024BYTES)
+ printf(" IMAGE_SCN_ALIGN_1024BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_2048BYTES)
+ printf(" IMAGE_SCN_ALIGN_2048BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_4096BYTES)
+ printf(" IMAGE_SCN_ALIGN_4096BYTES\n");
+ if ((ScnHdr->Characteristics & IMAGE_SCN_ALIGN_MASK) == IMAGE_SCN_ALIGN_8192BYTES)
+ printf(" IMAGE_SCN_ALIGN_8192BYTES\n");
+
+ if (ScnHdr->Characteristics & IMAGE_SCN_LNK_NRELOC_OVFL)
+ printf(" IMAGE_SCN_LNK_NRELOC_OVFL\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_MEM_DISCARDABLE)
+ printf(" IMAGE_SCN_MEM_DISCARDABLE\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_MEM_NOT_CACHED)
+ printf(" IMAGE_SCN_MEM_NOT_CACHED\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_MEM_NOT_PAGED)
+ printf(" IMAGE_SCN_MEM_NOT_PAGED\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_MEM_SHARED)
+ printf(" IMAGE_SCN_MEM_SHARED\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_MEM_EXECUTE)
+ printf(" IMAGE_SCN_MEM_EXECUTE\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_MEM_READ)
+ printf(" IMAGE_SCN_MEM_READ\n");
+ if (ScnHdr->Characteristics & IMAGE_SCN_MEM_WRITE)
+ printf(" IMAGE_SCN_MEM_WRITE\n");
+ printf("\n");
+
+ // Read the section Data, Relocations, & Line Nums
+ // Save the offset
+
+ if (ScnHdr->SizeOfRawData > 0)
+ {
+ unsigned int i;
+ // Print the Raw Data
+
+ printf("\nRAW DATA");
+ for (i = 0; i < ScnHdr->VirtualSize; i++)
+ {
+ if ((i % 16) == 0)
+ printf("\n %08X: ", i);
+ char ch = data[i];
+ printf("%02X ", (unsigned int)ch);
+ }
+ printf("\n\n");
+ }
+
+ /*
+ #if 0
+ if (ScnHdr->NumRelocations > 0)
+ {
+ // Print Section Relocations
+ ObjReloc_t ObjReloc;
+
+ fseek(fp, ScnHdr->PtrToRelocations/ * + CoffBeginOffset* /, SEEK_SET);
+ printf("RELOCATIONS\n");
+ printf(" Symbol Symbol\n");
+ printf(" Offset Type Index Name\n");
+ printf(" -------- -------- -------- ------\n");
+ for (int i = 0; i < ScnHdr->NumRelocations; i++)
+ {
+ fread(&ObjReloc, 1, sizeof(ObjReloc_t), fp);
+ printf(" %08X ", ObjReloc.VirtualAddress);
+
+ if (ObjReloc.Type == IMAGE_REL_I386_ABSOLUTE)
+ printf("ABSOLUTE ");
+ if (ObjReloc.Type == IMAGE_REL_I386_DIR16)
+ printf("DIR16 ");
+ if (ObjReloc.Type == IMAGE_REL_I386_REL16)
+ printf("REL16 ");
+ if (ObjReloc.Type == IMAGE_REL_I386_DIR32)
+ printf("DIR32 ");
+ if (ObjReloc.Type == IMAGE_REL_I386_DIR32NB)
+ printf("DIR32NB ");
+ if (ObjReloc.Type == IMAGE_REL_I386_SEG12)
+ printf("SEG12 ");
+ if (ObjReloc.Type == IMAGE_REL_I386_SECTION)
+ printf("SECTION ");
+ if (ObjReloc.Type == IMAGE_REL_I386_SECREL)
+ printf("SECREL ");
+ if (ObjReloc.Type == IMAGE_REL_I386_REL32)
+ printf("REL32 ");
+ printf("%8X ", ObjReloc.SymTableIndex);
+ printf("%s\n", GetSymbolName(ObjReloc.SymTableIndex));
+ }
+ printf("\n");
+ }
+
+ if (ScnHdr->NumLineNumbers > 0)
+ {
+ // Print The Line Number Info
+ LineNumbers_t LineNumber;
+ int LineCnt = 0;
+ int BaseLineNum = -1;
+
+ fseek(fp, ScnHdr->PtrToLineNums/ * + CoffBeginOffset* /, SEEK_SET);
+ printf("LINE NUMBERS");
+ for (int i = 0; i < ScnHdr->NumLineNumbers; i++)
+ {
+ int LNOffset = ftell(fp);
+
+ fread(&LineNumber, 1, sizeof(LineNumbers_t), fp);
+ if (LineNumber.LineNum == 0)
+ {
+ SymbolTable_t *Sym;
+ int SymIndex;
+
+ printf("\n");
+ SymIndex = LineNumber.Type.SymbolTableIndex;
+ Sym = &(SymTable[SymIndex]);
+ if (Sym->NumberOfAuxSymbols > 0)
+ {
+ Sym = &(SymTable[SymIndex+1]);
+ AuxFuncDef_t *FuncDef = (AuxFuncDef_t *)Sym;
+
+ if (FuncDef->PtrToLineNumber == LNOffset)
+ {
+ Sym = &(SymTable[FuncDef->TagIndex]);
+ if (Sym->NumberOfAuxSymbols > 0)
+ {
+ Sym = &(SymTable[FuncDef->TagIndex+1]);
+ AuxBfEf_t *Bf = (AuxBfEf_t *)Sym;
+ BaseLineNum = Bf->LineNumber;
+ }
+ }
+ }
+ printf(" Symbol Index: %8x ", SymIndex);
+ printf(" Base line number: %8d\n", BaseLineNum);
+ printf(" Symbol name = %s", GetSymbolName(SymIndex));
+ LineCnt = 0;
+ }
+ else
+ {
+ if ((LineCnt%4) == 0)
+ {
+ printf("\n ");
+ LineCnt = 0;
+ }
+ printf("%08X(%5d) ", LineNumber.Type.VirtualAddress,
+ LineNumber.LineNum + BaseLineNum);
+ LineCnt ++;
+ }
+ }
+ printf("\n");
+ }
+ #endif
+ */
+
+ printf("\n");
+}
+
+int CoffLoader::ParseCoff(FILE *fp)
+{
+ if ( !LoadCoffHModule(fp) )
+ {
+ printf("Failed to load/find COFF hModule header\n");
+ return 0;
+ }
+ if ( !LoadSymTable(fp) ||
+ !LoadStringTable(fp) ||
+ !LoadSections(fp) )
+ return 0;
+
+ PerformFixups();
+
+#ifdef DUMPING_DATA
+ PrintSymbolTable();
+ PrintStringTable();
+#endif
+ return 1;
+}
+
+void CoffLoader::PerformFixups(void)
+{
+ int FixupDataSize;
+ char *FixupData;
+ char *EndData;
+
+ EntryAddress = (unsigned long)RVA2Data(EntryAddress);
+
+ if( reinterpret_cast<void*>(WindowsHeader->ImageBase) == hModule )
+ return;
+
+ if ( !Directory )
+ return ;
+
+ if ( NumOfDirectories <= BASE_RELOCATION_TABLE )
+ return ;
+
+ if ( !Directory[BASE_RELOCATION_TABLE].Size )
+ return ;
+
+ FixupDataSize = Directory[BASE_RELOCATION_TABLE].Size;
+ FixupData = (char*)RVA2Data(Directory[BASE_RELOCATION_TABLE].RVA);
+ EndData = FixupData + FixupDataSize;
+
+ while (FixupData < EndData)
+ {
+ // Starting a new Fixup Block
+ unsigned long PageRVA = *((unsigned long*)FixupData);
+ FixupData += 4;
+ unsigned long BlockSize = *((unsigned long*)FixupData);
+ FixupData += 4;
+
+ BlockSize -= 8;
+ for (unsigned int i = 0; i < BlockSize / 2; i++)
+ {
+ unsigned short Fixup = *((unsigned short*)FixupData);
+ FixupData += 2;
+ int Type = (Fixup >> 12) & 0x0f;
+ Fixup &= 0xfff;
+ if (Type == IMAGE_REL_BASED_HIGHLOW)
+ {
+ unsigned long *Off = (unsigned long*)RVA2Data(Fixup + PageRVA);
+ *Off = (unsigned long)RVA2Data(*Off - WindowsHeader->ImageBase);
+ }
+ else if (Type == IMAGE_REL_BASED_ABSOLUTE)
+ {}
+ else
+ {
+ printf("Unsupported fixup type!!\n");
+ }
+ }
+ }
+}
diff --git a/xbmc/cores/DllLoader/coff.h b/xbmc/cores/DllLoader/coff.h
new file mode 100644
index 0000000..f421d43
--- /dev/null
+++ b/xbmc/cores/DllLoader/coff.h
@@ -0,0 +1,491 @@
+/*
+ * 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
+
+//#pragma message("including coff.h")
+//
+// COFF -- Common Object File Format
+// Used commonly by Un*x and is embedded in Windows PE
+// file format.
+//
+
+// These structures must be packed
+#pragma pack(1)
+
+
+/*
+ * Some general purpose MACROs
+ */
+
+#define VERSION_MAJOR(x) ((unsigned int)((x)& 0xff))
+#define VERSION_MINOR(x) ((unsigned int)(((x)>8) &0xff))
+
+#define BIGVERSION_MAJOR(x) ((unsigned int)((x)& 0xffff))
+#define BIGVERSION_MINOR(x) ((unsigned int)(((x)>16) &0xffff))
+
+/*
+ * COFF File Header (Object & Image)
+ * Spec section 3.3
+ */
+
+typedef struct
+{
+ unsigned short MachineType; /* magic type */
+ unsigned short NumberOfSections; /* number of sections */
+ unsigned long TimeDateStamp; /* time & date stamp */
+ unsigned long PointerToSymbolTable; /* file pointer to symtab */
+ unsigned long NumberOfSymbols; /* number of symtab entries */
+ unsigned short SizeOfOptionHeader; /* sizeof(optional hdr) */
+ unsigned short Characteristics; /* flags */
+}
+COFF_FileHeader_t;
+
+/*
+ * Machine Types
+ * Spec section 3.3.1
+ * (only i386 relevant for us)
+ */
+
+#if 1
+
+#ifndef IMAGE_FILE_MACHINE_I386
+#define IMAGE_FILE_MACHINE_I386 0x14c
+#endif
+
+
+
+#define IMAGE_FILE_RELOCS_STRIPPED 0x0001
+#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002
+#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004
+#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008
+#define IMAGE_FILE_AGGRESSIVE_WS_TRIM 0x0010
+#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020
+#define IMAGE_FILE_16BIT_MACHINE 0x0040
+#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080
+#define IMAGE_FILE_32BIT_MACHINE 0x0100
+#define IMAGE_FILE_DEBUG_STRIPPED 0x0200
+#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400
+#define IMAGE_FILE_SYSTEM 0x1000
+#define IMAGE_FILE_DLL 0x2000
+#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000
+#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000
+
+#endif
+
+
+
+#define OPTMAGIC_PE32 0x010b
+#define OPTMAGIC_PE32P 0x020b
+
+#define OPTHDR_SIZE 28
+#define OPTHDR_SIZEP 24
+#define WINHDR_SIZE 68
+#define WINHDR_SIZEP 88
+
+/*
+ * Optional Header Standard Fields (Image Only)
+ * Spec section 3.4.1
+ */
+
+typedef struct
+{
+ unsigned short Magic;
+ unsigned short LinkVersion;
+ unsigned long CodeSize;
+ unsigned long DataSize;
+ unsigned long BssSize;
+ unsigned long Entry;
+ unsigned long CodeBase;
+ unsigned long DataBase;
+}
+OptionHeader_t;
+
+typedef struct
+{
+ unsigned short Magic;
+ unsigned short LinkVersion;
+ unsigned long CodeSize;
+ unsigned long DataSize;
+ unsigned long BssSize;
+ unsigned long Entry;
+ unsigned long CodeBase;
+}
+OptionHeaderPlus_t;
+
+/*
+ * Optional Header Windows NT-Specific Fields (Image Only)
+ * Spec section 3.4.2
+ */
+
+typedef struct
+{
+ unsigned long ImageBase;
+ unsigned long SectionAlignment;
+ unsigned long FileAlignment;
+ unsigned long OSVer;
+ unsigned long ImgVer;
+ unsigned long SubSysVer;
+ unsigned long Reserved;
+ unsigned long SizeOfImage;
+ unsigned long SizeOfHeaders;
+ unsigned long CheckSum;
+ unsigned short Subsystem;
+ unsigned short DLLFlags;
+ unsigned long SizeOfStackReserve;
+ unsigned long SizeOfStackCommit;
+ unsigned long SizeOfHeapReserve;
+ unsigned long SizeOfHeapCommit;
+ unsigned long LoaderFlags;
+ unsigned long NumDirectories;
+}
+WindowsHeader_t;
+
+typedef struct
+{
+ unsigned long long ImageBase;
+ unsigned long SectionAlignment;
+ unsigned long FileAlignment;
+ unsigned long OSVer;
+ unsigned long ImgVer;
+ unsigned long SubSysVer;
+ unsigned long Reserved;
+ unsigned long SizeOfImage;
+ unsigned long SizeOfHeaders;
+ unsigned long CheckSum;
+ unsigned short Subsystem;
+ unsigned short DLLFlags;
+ unsigned long long SizeOfStackReserve;
+ unsigned long long SizeOfStackCommit;
+ unsigned long long SizeOfHeapReserve;
+ unsigned long long SizeOfHeapCommit;
+ unsigned long LoaderFlags;
+ unsigned long NumDirectories;
+}
+WindowsHeaderPlus_t;
+
+/*
+#define IMAGE_SUBSYSTEM_UNKNOWN 0
+#define IMAGE_SUBSYSTEM_NATIVE 1
+#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2
+#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3
+#define IMAGE_SUBSYSTEM_POSIX_CUI 7
+#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9
+#define IMAGE_SUBSYSTEM_EFI_APPLICATION 10
+#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER 11
+#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER 12
+
+#define IMAGE_DLLCHARACTERISTICS_NO_BIND 0x0800
+#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER 0x2000
+#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE 0X8000
+*/
+
+/*
+ * Optional Header Data Directories (Image Only)
+ * Spec section 3.4.3
+ */
+
+typedef struct
+{
+ unsigned long RVA;
+ unsigned long Size;
+}
+Image_Data_Directory_t;
+
+enum Directory_Items {
+ EXPORT_TABLE = 0,
+ IMPORT_TABLE,
+ RESOURCE_TABLE,
+ EXCEPTION_TABLE,
+ CERTIFICATE_TABLE,
+ BASE_RELOCATION_TABLE,
+ DEBUG_,
+ ARCHITECTURE,
+ GLOBAL_PTR,
+ TLS_TABLE,
+ LOAD_CONFIG_TABLE,
+ BOUND_IMPORT,
+ IAT,
+ DELAY_IMPORT_DESCRIPTOR,
+ COM_RUNTIME_HEADER,
+ RESERVED
+};
+
+/*
+ * Section Table (Section Headers)
+ * Spec section 4.
+ */
+
+
+typedef struct
+{
+ unsigned char Name[8];
+ unsigned long VirtualSize;
+ unsigned long VirtualAddress;
+ unsigned long SizeOfRawData;
+ unsigned long PtrToRawData;
+ unsigned long PtrToRelocations;
+ unsigned long PtrToLineNums;
+ unsigned short NumRelocations;
+ unsigned short NumLineNumbers;
+ unsigned long Characteristics;
+}
+SectionHeader_t;
+
+/*
+ * Section Flags (Characteristics)
+ * Spec section 4.1
+ */
+
+#define IMAGE_SCN_CNT_CODE 0x00000020
+#define IMAGE_SCN_CNT_DATA 0x00000040
+#define IMAGE_SCN_CNT_BSS 0x00000080
+#define IMAGE_SCN_LNK_INFO 0x00000200
+#define IMAGE_SCN_LNK_REMOVE 0x00000800
+#define IMAGE_SCN_LNK_COMDAT 0x00001000
+#define IMAGE_SCN_ALIGN_1BYTES 0x00100000
+#define IMAGE_SCN_ALIGN_2BYTES 0x00200000
+#define IMAGE_SCN_ALIGN_4BYTES 0x00300000
+#define IMAGE_SCN_ALIGN_8BYTES 0x00400000
+#define IMAGE_SCN_ALIGN_16BYTES 0x00500000
+#define IMAGE_SCN_ALIGN_32BYTES 0x00600000
+#define IMAGE_SCN_ALIGN_64BYTES 0x00700000
+#define IMAGE_SCN_ALIGN_128BYTES 0x00800000
+#define IMAGE_SCN_ALIGN_256BYTES 0x00900000
+#define IMAGE_SCN_ALIGN_512BYTES 0x00A00000
+#define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000
+#define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000
+#define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000
+#define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000
+#define IMAGE_SCN_ALIGN_MASK 0x00F00000
+#define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000
+#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000
+#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000
+#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000
+#define IMAGE_SCN_MEM_SHARED 0x10000000
+#define IMAGE_SCN_MEM_EXECUTE 0x20000000
+#define IMAGE_SCN_MEM_READ 0x40000000
+#define IMAGE_SCN_MEM_WRITE 0x80000000
+
+/*
+ * COFF Relocations (Object Only)
+ * Spec section 5.2
+ */
+
+typedef struct
+{
+ unsigned long VirtualAddress;
+ unsigned long SymTableIndex;
+ unsigned short Type;
+}
+ObjReloc_t;
+
+/*
+ * COFF Relocation Type Indicators
+ * Spec section 5.2.1
+ */
+
+#define IMAGE_REL_I386_ABSOLUTE 0x0000
+#define IMAGE_REL_I386_DIR16 0x0001
+#define IMAGE_REL_I386_REL16 0x0002
+#define IMAGE_REL_I386_DIR32 0x0006
+#define IMAGE_REL_I386_DIR32NB 0x0007
+#define IMAGE_REL_I386_SEG12 0x0009
+#define IMAGE_REL_I386_SECTION 0x000A
+#define IMAGE_REL_I386_SECREL 0x000B
+#define IMAGE_REL_I386_REL32 0x0014
+
+/*
+ * COFF Line Numbers
+ * Spec section 5.3
+ */
+
+typedef struct
+{
+ union {
+ unsigned long SymbolTableIndex;
+ unsigned long VirtualAddress;
+ } Type;
+ unsigned short LineNum;
+}
+LineNumbers_t;
+
+/*
+ * COFF Symbol Table
+ * Spec section 5.4
+ */
+
+typedef struct
+{
+ union {
+ unsigned char ShortName[8];
+ unsigned long long Offset;
+ } Name;
+ unsigned long Value;
+ unsigned short SectionNumber;
+ unsigned short Type;
+ unsigned char StorageClass;
+ unsigned char NumberOfAuxSymbols;
+}
+SymbolTable_t;
+
+#if !defined(TARGET_WINDOWS)
+
+#define IMAGE_SYM_UNDEFINED 0
+#define IMAGE_SYM_ABSOLUTE 0xFFFF
+#define IMAGE_SYM_DEBUG 0xFFFE
+
+
+#define IMAGE_SYM_TYPE_NULL 0
+#define IMAGE_SYM_TYPE_VOID 1
+#define IMAGE_SYM_TYPE_CHAR 2
+#define IMAGE_SYM_TYPE_SHORT 3
+#define IMAGE_SYM_TYPE_INT 4
+#define IMAGE_SYM_TYPE_LONG 5
+#define IMAGE_SYM_TYPE_FLOAT 6
+#define IMAGE_SYM_TYPE_DOUBLE 7
+#define IMAGE_SYM_TYPE_STRUCT 8
+#define IMAGE_SYM_TYPE_UNION 9
+#define IMAGE_SYM_TYPE_ENUM 10
+#define IMAGE_SYM_TYPE_MOE 11
+#define IMAGE_SYM_TYPE_BYTE 12
+#define IMAGE_SYM_TYPE_WORD 13
+#define IMAGE_SYM_TYPE_UINT 14
+#define IMAGE_SYM_TYPE_DWORD 15
+
+#define IMAGE_SYM_DWORD_NULL 0
+#define IMAGE_SYM_DWORD_POINTER 1
+#define IMAGE_SYM_DWORD_FUNCTION 2
+#define IMAGE_SYM_DWORD_ARRAY 3
+
+
+#define IMAGE_SYM_CLASS_END_OF_FUNCTION 0xFF
+#define IMAGE_SYM_CLASS_NULL 0
+#define IMAGE_SYM_CLASS_AUTOMATIC 1
+#define IMAGE_SYM_CLASS_EXTERNAL 2
+#define IMAGE_SYM_CLASS_STATIC 3
+#define IMAGE_SYM_CLASS_REGISTER 4
+#define IMAGE_SYM_CLASS_EXTERNAL_DEF 5
+#define IMAGE_SYM_CLASS_LABEL 6
+#define IMAGE_SYM_CLASS_UNDEFINED_LABEL 7
+#define IMAGE_SYM_CLASS_MEMBER_OF_STRUCT 8
+#define IMAGE_SYM_CLASS_ARGUMENT 9
+#define IMAGE_SYM_CLASS_STRUCT_TAG 10
+#define IMAGE_SYM_CLASS_MEMBER_OF_UNION 11
+#define IMAGE_SYM_CLASS_UNION_TAG 12
+#define IMAGE_SYM_CLASS_TYPE_DEFINITION 13
+#define IMAGE_SYM_CLASS_UNDEFINED_STATIC 14
+#define IMAGE_SYM_CLASS_ENUM_TAG 15
+#define IMAGE_SYM_CLASS_MEMBER_OF_ENUM 16
+#define IMAGE_SYM_CLASS_REGISTER_PARAM 17
+#define IMAGE_SYM_CLASS_BIT_FIELD 18
+#define IMAGE_SYM_CLASS_BLOCK 100
+#define IMAGE_SYM_CLASS_FUNCTION 101
+#define IMAGE_SYM_CLASS_END_OF_STRUCT 102
+#define IMAGE_SYM_CLASS_FILE 103
+#define IMAGE_SYM_CLASS_SECTION 104
+#define IMAGE_SYM_CLASS_WEAK_EXTERNAL 105
+#endif
+
+typedef struct
+{
+ unsigned long TagIndex;
+ unsigned long TotalSize;
+ unsigned long PtrToLineNumber;
+ unsigned long PtrToNextFunc;
+ unsigned short unused;
+}
+AuxFuncDef_t;
+
+/*
+ * Symbol Auxiliary Record: .bf and .ef
+ * Spec section 5.5.2
+ */
+
+typedef struct
+{
+ unsigned long unused;
+ unsigned short LineNumber;
+ unsigned long unused1;
+ unsigned short unused2;
+ unsigned long PtrToNextFunc;
+ unsigned char unused3;
+}
+AuxBfEf_t;
+
+/*
+ * Export Section (Directory)
+ * Spec section 6.3
+ */
+
+/*
+ * Export Directory Table
+ * Spec section 6.3.1
+ */
+
+typedef struct
+{
+ unsigned long ExportFlags;
+ unsigned long TimeStamp;
+ unsigned short MajorVersion;
+ unsigned short MinorVersion;
+ unsigned long Name_RVA;
+ unsigned long OrdinalBase;
+ unsigned long NumAddrTable;
+ unsigned long NumNamePtrs;
+ unsigned long ExportAddressTable_RVA;
+ unsigned long NamePointerTable_RVA;
+ unsigned long OrdinalTable_RVA;
+}
+ExportDirTable_t;
+
+
+/*
+ * Import Section (Directory)
+ * Spec section 6.4
+ */
+
+/*
+ * Import Directory Table
+ * Spec Section 6.4.1
+ */
+
+typedef struct
+{
+ unsigned long ImportLookupTable_RVA;
+ unsigned long TimeStamp;
+ unsigned long ForwarderChain;
+ unsigned long Name_RVA;
+ unsigned long ImportAddressTable_RVA;
+}
+ImportDirTable_t;
+
+/*
+ * .reloc Relocation types
+ * spec section 6.6
+ */
+
+#if 1
+#define IMAGE_REL_BASED_ABSOLUTE 0
+#define IMAGE_REL_BASED_HIGH 1
+#define IMAGE_REL_BASED_LOW 2
+#define IMAGE_REL_BASED_HIGHLOW 3
+#define IMAGE_REL_BASED_HIGHADJ 4
+#define IMAGE_REL_BASED_MIPS_JMPADDR 5
+#define IMAGE_REL_BASED_SECTION 6
+#define IMAGE_REL_BASED_REL32 7
+#define IMAGE_REL_BASED_MIPS_JMPADDR16 9
+#define IMAGE_REL_BASED_DIR64 10
+#define IMAGE_REL_BASED_HIGH3ADJ 11
+#endif
+
+
+
+
+#pragma pack()
+
diff --git a/xbmc/cores/DllLoader/coffldr.h b/xbmc/cores/DllLoader/coffldr.h
new file mode 100644
index 0000000..9da7801
--- /dev/null
+++ b/xbmc/cores/DllLoader/coffldr.h
@@ -0,0 +1,79 @@
+/*
+ * 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
+
+//#pragma message("including coffldr.h")
+#include "coff.h"
+
+#include <stdio.h>
+
+class CoffLoader
+{
+public:
+ CoffLoader();
+ virtual ~CoffLoader();
+
+ int ParseCoff(FILE *fp);
+ int ParseHeaders(void* hModule);
+
+ void *hModule; //standard windows HINSTANCE handle hold the whole image
+ //Pointers to somewhere in hModule, do not free these pointers
+ COFF_FileHeader_t *CoffFileHeader;
+ OptionHeader_t *OptionHeader;
+ WindowsHeader_t *WindowsHeader;
+ Image_Data_Directory_t *Directory;
+ SectionHeader_t *SectionHeader;
+
+protected:
+
+ // Allocated structures... hModule now hold the master Memory handle
+ SymbolTable_t *SymTable;
+ char *StringTable;
+ char **SectionData;
+
+ unsigned long EntryAddress; //Initialize entry point
+
+ // Unnecessary data
+ // This is data that is used only during linking and is not necessary
+ // while the program is running in general
+
+ int NumberOfSymbols;
+ int SizeOfStringTable;
+ int NumOfDirectories;
+ int NumOfSections;
+ int FileHeaderOffset;
+
+ // Members for printing the structures
+ static void PrintFileHeader(COFF_FileHeader_t *FileHeader);
+ static void PrintWindowsHeader(WindowsHeader_t *WinHdr);
+ static void PrintOptionHeader(OptionHeader_t *OptHdr);
+ static void PrintSection(SectionHeader_t* ScnHdr, const char* data);
+ void PrintStringTable(void);
+ void PrintSymbolTable(void);
+
+ // Members for Loading the Different structures
+ int LoadCoffHModule(FILE * fp);
+ int LoadSymTable(FILE *fp);
+ int LoadStringTable(FILE *fp);
+ int LoadSections(FILE *fp);
+
+ // Members for access some of the Data
+
+ int RVA2Section(unsigned long RVA);
+ void* RVA2Data(unsigned long RVA);
+ unsigned long Data2RVA(void* address);
+
+ char *GetStringTblIndex(int index);
+ char *GetStringTblOff(int Offset);
+ char *GetSymbolName(SymbolTable_t *sym);
+ char *GetSymbolName(int index);
+
+ void PerformFixups(void);
+};
+
diff --git a/xbmc/cores/DllLoader/dll.cpp b/xbmc/cores/DllLoader/dll.cpp
new file mode 100644
index 0000000..9690529
--- /dev/null
+++ b/xbmc/cores/DllLoader/dll.cpp
@@ -0,0 +1,252 @@
+/*
+ * 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 "dll.h"
+
+#include "DllLoader.h"
+#include "DllLoaderContainer.h"
+#include "dll_tracker.h"
+#include "dll_util.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <climits>
+
+#define DEFAULT_DLLPATH "special://xbmc/system/players/mplayer/codecs/"
+#define HIGH_WORD(a) ((uintptr_t)(a) >> 16)
+#define LOW_WORD(a) ((unsigned short)(((uintptr_t)(a)) & MAXWORD))
+
+//#define API_DEBUG
+
+char* getpath(char *buf, const char *full)
+{
+ const char* pos;
+ if ((pos = strrchr(full, PATH_SEPARATOR_CHAR)))
+ {
+ strncpy(buf, full, pos - full + 1 );
+ buf[pos - full + 1] = 0;
+ return buf;
+ }
+ else
+ {
+ buf[0] = 0;
+ return buf;
+ }
+}
+
+extern "C" HMODULE __stdcall dllLoadLibraryExtended(const char* lib_file, const char* sourcedll)
+{
+ char libname[MAX_PATH + 1] = {};
+ char libpath[MAX_PATH + 1] = {};
+ LibraryLoader* dll = NULL;
+
+ /* extract name */
+ const char* p = strrchr(lib_file, PATH_SEPARATOR_CHAR);
+ if (p)
+ strncpy(libname, p+1, sizeof(libname) - 1);
+ else
+ strncpy(libname, lib_file, sizeof(libname) - 1);
+ libname[sizeof(libname) - 1] = '\0';
+
+ if( libname[0] == '\0' )
+ return NULL;
+
+ /* extract path */
+ getpath(libpath, lib_file);
+
+ if (sourcedll)
+ {
+ /* also check for invalid paths which begin with a \ */
+ if( libpath[0] == '\0' || libpath[0] == PATH_SEPARATOR_CHAR )
+ {
+ /* use calling dll's path as base address for this call */
+ getpath(libpath, sourcedll);
+
+ /* mplayer has all it's dlls in a codecs subdirectory */
+ if (strstr(sourcedll, "mplayer.dll"))
+ strcat(libpath, "codecs\\");
+ }
+ }
+
+ /* if we still don't have a path, use default path */
+ if( libpath[0] == '\0' )
+ strcpy(libpath, DEFAULT_DLLPATH);
+
+ /* msdn docs state */
+ /* "If no file name extension is specified in the lpFileName parameter, the default library extension .dll is appended. */
+ /* However, the file name string can include a trailing point character (.) to indicate that the module name has no extension." */
+ if( strrchr(libname, '.') == NULL )
+ strcat(libname, ".dll");
+ else if( libname[strlen(libname)-1] == '.' )
+ libname[strlen(libname)-1] = '\0';
+
+ dll = DllLoaderContainer::LoadModule(libname, libpath);
+
+ if (dll)
+ return (HMODULE)dll->GetHModule();
+
+ CLog::Log(LOGERROR, "LoadLibrary('{}') failed", libname);
+ return NULL;
+}
+
+extern "C" HMODULE __stdcall dllLoadLibraryA(const char* file)
+{
+ return dllLoadLibraryExtended(file, NULL);
+}
+
+#define DONT_RESOLVE_DLL_REFERENCES 0x00000001
+#define LOAD_LIBRARY_AS_DATAFILE 0x00000002
+#define LOAD_WITH_ALTERED_SEARCH_PATH 0x00000008
+#define LOAD_IGNORE_CODE_AUTHZ_LEVEL 0x00000010
+
+extern "C" HMODULE __stdcall dllLoadLibraryExExtended(const char* lpLibFileName, HANDLE hFile, DWORD dwFlags, const char* sourcedll)
+{
+ char strFlags[512];
+ strFlags[0] = '\0';
+
+ if (dwFlags & DONT_RESOLVE_DLL_REFERENCES) strcat(strFlags, "\n - DONT_RESOLVE_DLL_REFERENCES");
+ if (dwFlags & LOAD_IGNORE_CODE_AUTHZ_LEVEL) strcat(strFlags, "\n - LOAD_IGNORE_CODE_AUTHZ_LEVEL");
+ if (dwFlags & LOAD_LIBRARY_AS_DATAFILE) strcat(strFlags, "\n - LOAD_LIBRARY_AS_DATAFILE");
+ if (dwFlags & LOAD_WITH_ALTERED_SEARCH_PATH) strcat(strFlags, "\n - LOAD_WITH_ALTERED_SEARCH_PATH");
+
+ CLog::Log(LOGDEBUG, "LoadLibraryExA called with flags: {}", strFlags);
+
+ return dllLoadLibraryExtended(lpLibFileName, sourcedll);
+}
+
+extern "C" HMODULE __stdcall dllLoadLibraryExA(const char* lpLibFileName, HANDLE hFile, DWORD dwFlags)
+{
+ return dllLoadLibraryExExtended(lpLibFileName, hFile, dwFlags, NULL);
+}
+
+extern "C" int __stdcall dllFreeLibrary(HINSTANCE hLibModule)
+{
+ LibraryLoader* dllhandle = DllLoaderContainer::GetModule(hLibModule);
+
+ if( !dllhandle )
+ {
+ CLog::Log(LOGERROR, "{} - Invalid hModule specified", __FUNCTION__);
+ return 1;
+ }
+
+ // to make sure systems dlls are never deleted
+ if (dllhandle->IsSystemDll()) return 1;
+
+ DllLoaderContainer::ReleaseModule(dllhandle);
+
+ return 1;
+}
+
+extern "C" intptr_t (*__stdcall dllGetProcAddress(HMODULE hModule, const char* function))(void)
+{
+ uintptr_t loc = (uintptr_t)_ReturnAddress();
+
+ void* address = NULL;
+ LibraryLoader* dll = DllLoaderContainer::GetModule(hModule);
+
+ if( !dll )
+ {
+ CLog::Log(LOGERROR, "{} - Invalid hModule specified", __FUNCTION__);
+ return NULL;
+ }
+
+ /* how can somebody get the stupid idea to create such a stupid function */
+ /* where you never know if the given pointer is a pointer or a value */
+ if( HIGH_WORD(function) == 0 && LOW_WORD(function) < 1000)
+ {
+ if( dll->ResolveOrdinal(LOW_WORD(function), &address) )
+ {
+ CLog::Log(LOGDEBUG, "{}({}({}), {}) => {}", __FUNCTION__, fmt::ptr(hModule), dll->GetName(),
+ LOW_WORD(function), fmt::ptr(address));
+ }
+ else if( dll->IsSystemDll() )
+ {
+ char ordinal[6] = {};
+ sprintf(ordinal, "%u", LOW_WORD(function));
+ address = (void*)create_dummy_function(dll->GetName(), ordinal);
+
+ /* add to tracklist if we are tracking this source dll */
+ DllTrackInfo* track = tracker_get_dlltrackinfo(loc);
+ if( track )
+ tracker_dll_data_track(track->pDll, (uintptr_t)address);
+
+ CLog::Log(LOGDEBUG, "{} - created dummy function {}!{}", __FUNCTION__, dll->GetName(),
+ ordinal);
+ }
+ else
+ {
+ address = NULL;
+ CLog::Log(LOGDEBUG, "{}({}({}), '{}') => {}", __FUNCTION__, fmt::ptr(hModule), dll->GetName(),
+ function, fmt::ptr(address));
+ }
+ }
+ else
+ {
+ if( dll->ResolveExport(function, &address) )
+ {
+ CLog::Log(LOGDEBUG, "{}({}({}), '{}') => {}", __FUNCTION__, fmt::ptr(hModule), dll->GetName(),
+ function, fmt::ptr(address));
+ }
+ else
+ {
+ DllTrackInfo* track = tracker_get_dlltrackinfo(loc);
+ /* some dll's require us to always return a function or it will fail, other's */
+ /* decide functionality depending on if the functions exist and may fail */
+ if (dll->IsSystemDll() && track &&
+ StringUtils::CompareNoCase(track->pDll->GetName(), "CoreAVCDecoder.ax") == 0)
+ {
+ address = (void*)create_dummy_function(dll->GetName(), function);
+ tracker_dll_data_track(track->pDll, (uintptr_t)address);
+ CLog::Log(LOGDEBUG, "{} - created dummy function {}!{}", __FUNCTION__, dll->GetName(),
+ function);
+ }
+ else
+ {
+ address = NULL;
+ CLog::Log(LOGDEBUG, "{}({}({}), '{}') => {}", __FUNCTION__, fmt::ptr(hModule),
+ dll->GetName(), function, fmt::ptr(address));
+ }
+ }
+ }
+
+ return (intptr_t(*)(void)) address;
+}
+
+extern "C" HMODULE WINAPI dllGetModuleHandleA(const char* lpModuleName)
+{
+ /*
+ If the file name extension is omitted, the default library extension .dll is appended.
+ The file name string can include a trailing point character (.) to indicate that the module name has no extension.
+ The string does not have to specify a path. When specifying a path, be sure to use backslashes (\), not forward slashes (/).
+ The name is compared (case independently)
+ If this parameter is NULL, GetModuleHandle returns a handle to the file used to create the calling process (.exe file).
+ */
+
+ if( lpModuleName == NULL )
+ return NULL;
+
+ char* strModuleName = new char[strlen(lpModuleName) + 5];
+ strcpy(strModuleName, lpModuleName);
+
+ if (strrchr(strModuleName, '.') == 0) strcat(strModuleName, ".dll");
+
+ //CLog::Log(LOGDEBUG, "GetModuleHandleA({}) .. looking up", lpModuleName);
+
+ LibraryLoader *p = DllLoaderContainer::GetModule(strModuleName);
+ delete []strModuleName;
+
+ if (p)
+ {
+ //CLog::Log(LOGDEBUG, "GetModuleHandleA('{}') => 0x{:x}", lpModuleName, h);
+ return (HMODULE)p->GetHModule();
+ }
+
+ CLog::Log(LOGDEBUG, "GetModuleHandleA('{}') failed", lpModuleName);
+ return NULL;
+}
diff --git a/xbmc/cores/DllLoader/dll.h b/xbmc/cores/DllLoader/dll.h
new file mode 100644
index 0000000..5a0b605
--- /dev/null
+++ b/xbmc/cores/DllLoader/dll.h
@@ -0,0 +1,20 @@
+/*
+ * 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 "PlatformDefs.h"
+
+extern "C" HMODULE __stdcall dllLoadLibraryExtended(const char* file, const char* sourcedll);
+extern "C" HMODULE __stdcall dllLoadLibraryA(const char* file);
+extern "C" HMODULE __stdcall dllLoadLibraryExExtended(const char* lpLibFileName, HANDLE hFile, DWORD dwFlags, const char* sourcedll);
+extern "C" HMODULE __stdcall dllLoadLibraryExA(const char* lpLibFileName, HANDLE hFile, DWORD dwFlags);
+extern "C" int __stdcall dllFreeLibrary(HINSTANCE hLibModule);
+extern "C" intptr_t (*__stdcall dllGetProcAddress(HMODULE hModule, const char* function))(void);
+extern "C" HMODULE WINAPI dllGetModuleHandleA(const char* lpModuleName);
+
diff --git a/xbmc/cores/DllLoader/dll_tracker.cpp b/xbmc/cores/DllLoader/dll_tracker.cpp
new file mode 100644
index 0000000..252232d
--- /dev/null
+++ b/xbmc/cores/DllLoader/dll_tracker.cpp
@@ -0,0 +1,145 @@
+/*
+ * 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 "dll_tracker.h"
+
+#include "DllLoader.h"
+#include "dll_tracker_file.h"
+#include "dll_tracker_library.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <stdlib.h>
+
+#ifdef _cplusplus
+extern "C"
+{
+#endif
+
+CCriticalSection g_trackerLock;
+TrackedDllList g_trackedDlls;
+
+void tracker_dll_add(DllLoader* pDll)
+{
+ DllTrackInfo* trackInfo = new DllTrackInfo;
+ trackInfo->pDll = pDll;
+ trackInfo->lMinAddr = 0;
+ trackInfo->lMaxAddr = 0;
+ std::unique_lock<CCriticalSection> locktd(g_trackerLock);
+ g_trackedDlls.push_back(trackInfo);
+}
+
+void tracker_dll_free(DllLoader* pDll)
+{
+ std::unique_lock<CCriticalSection> locktd(g_trackerLock);
+ for (TrackedDllsIter it = g_trackedDlls.begin(); it != g_trackedDlls.end();)
+ {
+ // NOTE: This code assumes that the same dll pointer can be in more than one
+ // slot of the vector g_trackedDlls. If it's not, then it can be
+ // simplified by returning after we've found the one we want, saving
+ // the iterator shuffling, and reducing potential bugs.
+ if ((*it)->pDll == pDll)
+ {
+ try
+ {
+ tracker_library_free_all(*it);
+ tracker_file_free_all(*it);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGFATAL, "Error freeing tracked dll resources");
+ }
+ // free all functions which where created at the time we loaded the dll
+ DummyListIter dit = (*it)->dummyList.begin();
+ while (dit != (*it)->dummyList.end()) { free((void*)*dit); ++dit; }
+ (*it)->dummyList.clear();
+
+ delete (*it);
+ it = g_trackedDlls.erase(it);
+ }
+ else
+ ++it;
+ }
+}
+
+void tracker_dll_set_addr(const DllLoader* pDll, uintptr_t min, uintptr_t max)
+{
+ std::unique_lock<CCriticalSection> locktd(g_trackerLock);
+ for (TrackedDllsIter it = g_trackedDlls.begin(); it != g_trackedDlls.end(); ++it)
+ {
+ if ((*it)->pDll == pDll)
+ {
+ (*it)->lMinAddr = min;
+ (*it)->lMaxAddr = max;
+ break;
+ }
+ }
+}
+
+const char* tracker_getdllname(uintptr_t caller)
+{
+ DllTrackInfo *track = tracker_get_dlltrackinfo(caller);
+ if(track)
+ return track->pDll->GetFileName();
+ return "";
+}
+
+DllTrackInfo* tracker_get_dlltrackinfo(uintptr_t caller)
+{
+ std::unique_lock<CCriticalSection> locktd(g_trackerLock);
+ for (TrackedDllsIter it = g_trackedDlls.begin(); it != g_trackedDlls.end(); ++it)
+ {
+ if (caller >= (*it)->lMinAddr && caller <= (*it)->lMaxAddr)
+ {
+ return *it;
+ }
+ }
+
+ // crap not in any base address, check if it may be in virtual spaces
+ for (TrackedDllsIter it = g_trackedDlls.begin(); it != g_trackedDlls.end(); ++it)
+ {
+ for(VAllocListIter it2 = (*it)->virtualList.begin(); it2 != (*it)->virtualList.end(); ++it2)
+ {
+ if( it2->first <= caller && caller < it2->first + it2->second.size )
+ return *it;
+
+ }
+ }
+
+ return NULL;
+}
+
+DllTrackInfo* tracker_get_dlltrackinfo_byobject(const DllLoader* pDll)
+{
+ std::unique_lock<CCriticalSection> locktd(g_trackerLock);
+ for (TrackedDllsIter it = g_trackedDlls.begin(); it != g_trackedDlls.end(); ++it)
+ {
+ if ((*it)->pDll == pDll)
+ {
+ return *it;
+ }
+ }
+ return NULL;
+}
+
+void tracker_dll_data_track(const DllLoader* pDll, uintptr_t addr)
+{
+ std::unique_lock<CCriticalSection> locktd(g_trackerLock);
+ for (TrackedDllsIter it = g_trackedDlls.begin(); it != g_trackedDlls.end(); ++it)
+ {
+ if (pDll == (*it)->pDll)
+ {
+ (*it)->dummyList.push_back((uintptr_t)addr);
+ break;
+ }
+ }
+}
+
+#ifdef _cplusplus
+}
+#endif
diff --git a/xbmc/cores/DllLoader/dll_tracker.h b/xbmc/cores/DllLoader/dll_tracker.h
new file mode 100644
index 0000000..68e1779
--- /dev/null
+++ b/xbmc/cores/DllLoader/dll_tracker.h
@@ -0,0 +1,132 @@
+/*
+ * 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 "threads/CriticalSection.h"
+#include "PlatformDefs.h"
+#ifdef TARGET_WINDOWS
+#if defined(TARGET_WINDOWS_STORE)
+#include <WinSock2.h>
+#endif
+#endif
+
+#include <list>
+#include <map>
+
+class DllLoader;
+
+struct AllocLenCaller
+{
+ size_t size;
+ uintptr_t calleraddr;
+};
+
+enum TrackedFileType
+{
+ FILE_XBMC_OPEN,
+ FILE_XBMC_FOPEN,
+ FILE_OPEN,
+ FILE_FOPEN
+};
+
+typedef struct _TrackedFile
+{
+ TrackedFileType type;
+ uintptr_t handle;
+ char* name;
+} TrackedFile;
+
+typedef std::map<uintptr_t, AllocLenCaller> DataList;
+typedef std::map<uintptr_t, AllocLenCaller>::iterator DataListIter;
+
+typedef std::list<TrackedFile*> FileList;
+typedef std::list<TrackedFile*>::iterator FileListIter;
+
+typedef std::list<HMODULE> DllList;
+typedef std::list<HMODULE>::iterator DllListIter;
+
+typedef std::list<uintptr_t> DummyList;
+typedef std::list<uintptr_t>::iterator DummyListIter;
+
+typedef std::list<SOCKET> SocketList;
+typedef std::list<SOCKET>::iterator SocketListIter;
+
+typedef std::list<HANDLE> HeapObjectList;
+typedef std::list<HANDLE>::iterator HeapObjectListIter;
+
+typedef std::map<uintptr_t, AllocLenCaller> VAllocList;
+typedef std::map<uintptr_t, AllocLenCaller>::iterator VAllocListIter;
+
+typedef struct _DllTrackInfo
+{
+ DllLoader* pDll;
+ uintptr_t lMinAddr;
+ uintptr_t lMaxAddr;
+
+ DataList dataList;
+
+ // list with dll's that are loaded by this dll
+ DllList dllList;
+
+ // for dummy functions that are created if no exported function could be found
+ DummyList dummyList;
+
+ FileList fileList;
+ SocketList socketList;
+
+ HeapObjectList heapobjectList;
+
+ VAllocList virtualList;
+} DllTrackInfo;
+
+class TrackedDllList : public std::list<DllTrackInfo*>, public CCriticalSection {};
+typedef std::list<DllTrackInfo*>::iterator TrackedDllsIter;
+
+#ifdef _cplusplus
+extern "C"
+{
+#endif
+
+extern CCriticalSection g_trackerLock;
+extern TrackedDllList g_trackedDlls;
+
+// add a dll for tracking
+void tracker_dll_add(DllLoader* pDll);
+
+// remove a dll, and free all its resources
+void tracker_dll_free(DllLoader* pDll);
+
+// sets the dll base address and size
+void tracker_dll_set_addr(const DllLoader* pDll, uintptr_t min, uintptr_t max);
+
+// returns the name from the dll that contains this address or "" if not found
+const char* tracker_getdllname(uintptr_t caller);
+
+// returns a function pointer if there is one available for it, or NULL if not ofund
+void* tracker_dll_get_function(DllLoader* pDll, char* sFunctionName);
+
+DllTrackInfo* tracker_get_dlltrackinfo_byobject(const DllLoader* pDll);
+
+DllTrackInfo* tracker_get_dlltrackinfo(uintptr_t caller);
+
+void tracker_dll_data_track(const DllLoader* pDll, uintptr_t addr);
+
+#ifdef TARGET_POSIX
+#define _ReturnAddress() __builtin_return_address(0)
+#endif
+
+#ifdef _cplusplus
+}
+#endif
+
+#ifndef TARGET_POSIX
+extern "C" void * _ReturnAddress(void);
+#pragma intrinsic(_ReturnAddress)
+#endif
+
diff --git a/xbmc/cores/DllLoader/dll_tracker_file.cpp b/xbmc/cores/DllLoader/dll_tracker_file.cpp
new file mode 100644
index 0000000..b5d1214
--- /dev/null
+++ b/xbmc/cores/DllLoader/dll_tracker_file.cpp
@@ -0,0 +1,134 @@
+/*
+ * 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 "dll_tracker_file.h"
+
+#include "DllLoader.h"
+#include "dll_tracker.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <stdlib.h>
+
+#ifdef TARGET_POSIX
+#define dll_open open
+#define dll_fopen fopen
+#define dll_close close
+#define dll_fclose fclose
+#define dll_freopen freopen
+#else
+#include "exports/emu_msvcrt.h"
+#include <io.h>
+#endif
+
+extern "C" void tracker_file_track(uintptr_t caller, uintptr_t handle, TrackedFileType type, const char* sFile)
+{
+ DllTrackInfo* pInfo = tracker_get_dlltrackinfo(caller);
+ if (pInfo)
+ {
+ std::unique_lock<CCriticalSection> lock(g_trackerLock);
+ TrackedFile* file = new TrackedFile;
+ file->handle = handle;
+ file->type = type;
+ file->name = strdup(sFile);
+ pInfo->fileList.push_back(file);
+ }
+}
+
+extern "C" void tracker_file_free(uintptr_t caller, uintptr_t handle, TrackedFileType type)
+{
+ DllTrackInfo* pInfo = tracker_get_dlltrackinfo(caller);
+ if (pInfo)
+ {
+ std::unique_lock<CCriticalSection> lock(g_trackerLock);
+ for (FileListIter it = pInfo->fileList.begin(); it != pInfo->fileList.end(); ++it)
+ {
+ TrackedFile* file = *it;
+ if (file->handle == handle && file->type == type)
+ {
+ free(file->name);
+ delete file;
+ pInfo->fileList.erase(it);
+ return;
+ }
+ }
+ }
+ CLog::Log(LOGWARNING, "unable to remove tracked file from tracker");
+}
+
+extern "C" void tracker_file_free_all(DllTrackInfo* pInfo)
+{
+ if (!pInfo->fileList.empty())
+ {
+ std::unique_lock<CCriticalSection> lock(g_trackerLock);
+ CLog::Log(LOGDEBUG, "{0}: Detected open files: {1}", pInfo->pDll->GetFileName(), pInfo->fileList.size());
+ for (FileListIter it = pInfo->fileList.begin(); it != pInfo->fileList.end(); ++it)
+ {
+ TrackedFile* file = *it;
+ CLog::Log(LOGDEBUG, "{}", file->name);
+ free(file->name);
+
+ if (file->type == FILE_XBMC_OPEN) dll_close(file->handle);
+ else if (file->type == FILE_XBMC_FOPEN) dll_fclose((FILE*)file->handle);
+ else if (file->type == FILE_OPEN) close(file->handle);
+ else if (file->type == FILE_FOPEN) fclose((FILE*)file->handle);
+
+ delete file;
+ }
+ }
+ pInfo->fileList.erase(pInfo->fileList.begin(), pInfo->fileList.end());
+}
+
+extern "C"
+{
+ int track_open(const char* sFileName, int iMode)
+ {
+ uintptr_t loc = (uintptr_t)_ReturnAddress();
+
+ int fd = dll_open(sFileName, iMode);
+ if (fd >= 0) tracker_file_track(loc, fd, FILE_XBMC_OPEN, sFileName);
+ return fd;
+ }
+
+ int track_close(int fd)
+ {
+ uintptr_t loc = (uintptr_t)_ReturnAddress();
+
+ tracker_file_free(loc, fd, FILE_XBMC_OPEN);
+ return dll_close(fd);
+ }
+
+ FILE* track_fopen(const char* sFileName, const char* mode)
+ {
+ uintptr_t loc = (uintptr_t)_ReturnAddress();
+
+ FILE* fd = dll_fopen(sFileName, mode);
+ if (fd) tracker_file_track(loc, (uintptr_t)fd, FILE_XBMC_FOPEN, sFileName);
+ return fd;
+ }
+
+ int track_fclose(FILE* stream)
+ {
+ uintptr_t loc = (uintptr_t)_ReturnAddress();
+
+ tracker_file_free(loc, (uintptr_t)stream, FILE_XBMC_FOPEN);
+ return dll_fclose(stream);
+ }
+
+ FILE* track_freopen(const char *path, const char *mode, FILE *stream)
+ {
+ uintptr_t loc = (uintptr_t)_ReturnAddress();
+
+ tracker_file_free(loc, (uintptr_t)stream, FILE_XBMC_FOPEN);
+ stream = dll_freopen(path, mode, stream);
+ if (stream)
+ tracker_file_track(loc, (uintptr_t)stream, FILE_XBMC_FOPEN, path);
+ return stream;
+ }
+
+}
diff --git a/xbmc/cores/DllLoader/dll_tracker_file.h b/xbmc/cores/DllLoader/dll_tracker_file.h
new file mode 100644
index 0000000..e6be371
--- /dev/null
+++ b/xbmc/cores/DllLoader/dll_tracker_file.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "dll_tracker.h"
+
+#include <stdio.h>
+
+extern "C" void tracker_file_track(uintptr_t caller, uintptr_t handle, TrackedFileType type, const char* sFile = "");
+extern "C" void tracker_file_free(uintptr_t caller, uintptr_t handle, TrackedFileType type);
+extern "C" void tracker_file_free_all(DllTrackInfo* pInfo);
+
+extern "C"
+{
+ int track_open(const char* sFileName, int iMode);
+ int track_close(int fd);
+ FILE* track_fopen(const char* sFileName, const char* mode);
+ int track_fclose(FILE* stream);
+ FILE* track_freopen(const char *path, const char *mode, FILE *stream);
+}
+
diff --git a/xbmc/cores/DllLoader/dll_tracker_library.cpp b/xbmc/cores/DllLoader/dll_tracker_library.cpp
new file mode 100644
index 0000000..8cfac13
--- /dev/null
+++ b/xbmc/cores/DllLoader/dll_tracker_library.cpp
@@ -0,0 +1,122 @@
+/*
+ * 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 "dll_tracker_library.h"
+
+#include "DllLoader.h"
+#include "DllLoaderContainer.h"
+#include "dll.h"
+#include "dll_tracker.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+extern "C" inline void tracker_library_track(uintptr_t caller, HMODULE hHandle)
+{
+ DllTrackInfo* pInfo = tracker_get_dlltrackinfo(caller);
+ if (pInfo && hHandle)
+ {
+ std::unique_lock<CCriticalSection> lock(g_trackerLock);
+ pInfo->dllList.push_back(hHandle);
+ }
+}
+
+extern "C" inline void tracker_library_free(uintptr_t caller, HMODULE hHandle)
+{
+ DllTrackInfo* pInfo = tracker_get_dlltrackinfo(caller);
+ if (pInfo && hHandle)
+ {
+ std::unique_lock<CCriticalSection> lock(g_trackerLock);
+ for (DllListIter it = pInfo->dllList.begin(); it != pInfo->dllList.end(); ++it)
+ {
+ if (*it == hHandle)
+ {
+ pInfo->dllList.erase(it);
+ break;
+ }
+ }
+ }
+}
+
+extern "C" void tracker_library_free_all(DllTrackInfo* pInfo)
+{
+ // unloading unloaded dll's
+ if (!pInfo->dllList.empty())
+ {
+ std::unique_lock<CCriticalSection> lock(g_trackerLock);
+ CLog::Log(LOGDEBUG,"{0}: Detected {1} unloaded dll's", pInfo->pDll->GetFileName(), pInfo->dllList.size());
+ for (DllListIter it = pInfo->dllList.begin(); it != pInfo->dllList.end(); ++it)
+ {
+ LibraryLoader* pDll = DllLoaderContainer::GetModule((HMODULE)*it);
+ if( !pDll)
+ {
+ CLog::Log(LOGERROR, "{} - Invalid module in tracker", __FUNCTION__);
+ return;
+ }
+
+ if (!pDll->IsSystemDll())
+ {
+ if (strlen(pDll->GetFileName()) > 0)
+ CLog::Log(LOGDEBUG, " : {}", pDll->GetFileName());
+ }
+ }
+
+ // now unload the dlls
+ for (DllListIter it = pInfo->dllList.begin(); it != pInfo->dllList.end(); ++it)
+ {
+ LibraryLoader* pDll = DllLoaderContainer::GetModule((HMODULE)*it);
+ if( !pDll)
+ {
+ CLog::Log(LOGERROR, "{} - Invalid module in tracker", __FUNCTION__);
+ return;
+ }
+
+ if (!pDll->IsSystemDll())
+ {
+ dllFreeLibrary((HMODULE)pDll->GetHModule());
+ }
+ }
+ }
+}
+
+extern "C" HMODULE __stdcall track_LoadLibraryA(const char* file)
+{
+ uintptr_t loc = (uintptr_t)_ReturnAddress();
+
+ DllTrackInfo* pInfo = tracker_get_dlltrackinfo(loc);
+ const char* path = NULL;
+ if (pInfo) path = pInfo->pDll->GetFileName();
+
+ HMODULE hHandle = dllLoadLibraryExtended(file, path);
+ tracker_library_track(loc, hHandle);
+
+ return hHandle;
+}
+
+extern "C" HMODULE __stdcall track_LoadLibraryExA(const char* lpLibFileName, HANDLE hFile, DWORD dwFlags)
+{
+ uintptr_t loc = (uintptr_t)_ReturnAddress();
+
+ DllTrackInfo* pInfo = tracker_get_dlltrackinfo(loc);
+ const char* path = NULL;
+ if (pInfo) path = pInfo->pDll->GetFileName();
+
+ HMODULE hHandle = dllLoadLibraryExExtended(lpLibFileName, hFile, dwFlags, path);
+ tracker_library_track(loc, hHandle);
+
+ return hHandle;
+}
+
+extern "C" int __stdcall track_FreeLibrary(HINSTANCE hLibModule)
+{
+ uintptr_t loc = (uintptr_t)_ReturnAddress();
+
+ tracker_library_free(loc, hLibModule);
+
+ return dllFreeLibrary(hLibModule);
+}
diff --git a/xbmc/cores/DllLoader/dll_tracker_library.h b/xbmc/cores/DllLoader/dll_tracker_library.h
new file mode 100644
index 0000000..6670d70
--- /dev/null
+++ b/xbmc/cores/DllLoader/dll_tracker_library.h
@@ -0,0 +1,17 @@
+/*
+ * 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 "dll_tracker.h"
+
+extern "C" void tracker_library_free_all(DllTrackInfo* pInfo);
+
+extern "C" HMODULE __stdcall track_LoadLibraryA(const char* file);
+extern "C" HMODULE __stdcall track_LoadLibraryExA(const char* lpLibFileName, HANDLE hFile, DWORD dwFlags);
+extern "C" int __stdcall track_FreeLibrary(HINSTANCE hLibModule);
diff --git a/xbmc/cores/DllLoader/dll_util.cpp b/xbmc/cores/DllLoader/dll_util.cpp
new file mode 100644
index 0000000..b33c02e
--- /dev/null
+++ b/xbmc/cores/DllLoader/dll_util.cpp
@@ -0,0 +1,119 @@
+/*
+ * 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 "utils/log.h"
+#include "dll_util.h"
+
+#ifdef TARGET_WINDOWS
+#include "platform/win32/CharsetConverter.h"
+#include <windows.h>
+#endif
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef _cplusplus
+extern "C"
+{
+#endif
+
+static int iDllDummyOutputCall = 0;
+void dll_dummy_output(char* dllname, char* funcname)
+{
+ CLog::Log(LOGERROR, "{}: Unresolved function called ({}), Count number {}", dllname, funcname,
+ ++iDllDummyOutputCall);
+}
+
+// this piece of asm code only calls dll_dummy_output(s, s) and will return NULL
+unsigned char dummy_func[] = {
+ 0x55, // push ebp
+ 0x8b, 0xec, // mov ebp,esp
+ 0xa1, 0, 0, 0, 0, // mov eax,dword ptr [0 0 0 0]
+ 0x50, // push eax
+ 0xa1, 0, 0, 0, 0, // mov eax,dword ptr [0 0 0 0]
+ 0x50, // push eax
+ 0xff, 0x15, 0, 0, 0, 0, // call dword ptr[dll_dummy_output]
+ 0x83, 0xc4, 0x08, // add esp,8
+ 0x33, 0xc0, // xor eax,eax // return NULL
+ 0x5d, // pop ebp
+ 0xc3 // ret
+ };
+
+/* Create a new callable function
+ * This allocates a few bytes with the next content
+ *
+ * 1 function in assembly code (dummy_func)
+ * 2 datapointer (pointer to dll string)
+ * 3 datapointer (pointer to function string)
+ * 4 datapointer (pointer to function string)
+ * 5 string (string of chars representing dll name)
+ * 6 string (string of chars representing function name)
+ */
+uintptr_t create_dummy_function(const char* strDllName, const char* strFunctionName)
+{
+ size_t iFunctionSize = sizeof(dummy_func);
+ size_t iDllNameSize = strlen(strDllName) + 1;
+ size_t iFunctionNameSize = strlen(strFunctionName) + 1;
+
+ // allocate memory for function + strings + 3 x 4 bytes for three datapointers
+ char* pData = (char*)malloc(iFunctionSize + 12 + iDllNameSize + iFunctionNameSize);
+ if (!pData)
+ return 0;
+
+ char* offDataPointer1 = pData + iFunctionSize;
+ char* offDataPointer2 = pData + iFunctionSize + 4;
+ char* offDataPointer3 = pData + iFunctionSize + 8;
+ char* offStringDll = pData + iFunctionSize + 12;
+ char* offStringFunc = pData + iFunctionSize + 12 + iDllNameSize;
+
+ // 1 copy assembly code
+ memcpy(pData, dummy_func, iFunctionSize);
+
+ // insert pointers to datapointers into assembly code (fills 0x00000000 in dummy_func)
+ *(int*)(pData + 4) = (intptr_t)offDataPointer1;
+ *(int*)(pData + 10) = (intptr_t)offDataPointer2;
+ *(int*)(pData + 17) = (intptr_t)offDataPointer3;
+
+ // 2 fill datapointer with pointer to 5 (string)
+ *(int*)offDataPointer1 = (intptr_t)offStringFunc;
+ // 3 fill datapointer with pointer to 6 (string)
+ *(int*)offDataPointer2 = (intptr_t)offStringDll;
+ // 4 fill datapointer with pointer to dll_dummy_output
+ *(int*)offDataPointer3 = (intptr_t)dll_dummy_output;
+
+ // copy arguments to 5 (string) and 6 (string)
+ memcpy(offStringDll, strDllName, iDllNameSize);
+ memcpy(offStringFunc, strFunctionName, iFunctionNameSize);
+
+ return (uintptr_t)pData;
+}
+
+uintptr_t get_win_function_address(const char* strDllName, const char* strFunctionName)
+{
+#ifdef TARGET_WINDOWS_DESKTOP
+ using KODI::PLATFORM::WINDOWS::ToW;
+ auto strDllNameW = ToW(strDllName);
+ HMODULE handle = GetModuleHandle(strDllNameW.c_str());
+ if(handle == nullptr)
+ {
+ handle = LoadLibrary(strDllNameW.c_str());
+ }
+ if(handle != nullptr)
+ {
+ auto pGNSI = reinterpret_cast<uintptr_t>(GetProcAddress(handle, strFunctionName));
+ if(pGNSI != NULL)
+ return pGNSI;
+
+ FreeLibrary(handle);
+ }
+#endif
+ return 0;
+}
+
+#ifdef _cplusplus
+}
+#endif
diff --git a/xbmc/cores/DllLoader/dll_util.h b/xbmc/cores/DllLoader/dll_util.h
new file mode 100644
index 0000000..3b0b6c4
--- /dev/null
+++ b/xbmc/cores/DllLoader/dll_util.h
@@ -0,0 +1,24 @@
+/*
+ * 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 <stdint.h>
+
+#ifdef _cplusplus
+extern "C"
+{
+#endif
+
+uintptr_t create_dummy_function(const char* strDllName, const char* strFunctionName);
+uintptr_t get_win_function_address(const char* strDllName, const char* strFunctionName);
+
+#ifdef _cplusplus
+}
+#endif
+
diff --git a/xbmc/cores/DllLoader/exports/CMakeLists.txt b/xbmc/cores/DllLoader/exports/CMakeLists.txt
new file mode 100644
index 0000000..4039669
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/CMakeLists.txt
@@ -0,0 +1,33 @@
+set(SOURCES emu_dummy.cpp
+ emu_msvcrt.cpp)
+
+set(HEADERS emu_dummy.h
+ emu_msvcrt.h)
+
+core_add_library(dllexports)
+
+if(APPLE)
+ add_library(wrapper OBJECT wrapper.c)
+ add_custom_target(wrapper.def ALL ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/wrapper_mach_alias wrapper.def)
+ set_target_properties(wrapper PROPERTIES FOLDER "Build Utilities")
+ set_target_properties(wrapper.def PROPERTIES FOLDER "Build Utilities")
+ add_dependencies(wrapper.def wrapper)
+elseif(NOT CORE_SYSTEM_NAME STREQUAL windows AND NOT CORE_SYSTEM_NAME STREQUAL windowsstore)
+ add_options(C ALL_BUILDS "-fPIC")
+ add_library(wrapper OBJECT wrapper.c)
+
+ if(USE_LTO)
+ add_custom_target(wrapper.def ALL ${CMAKE_NM} ${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/wrapper.dir/wrapper.c.o | grep __wrap | awk '{ printf(\"%s \", \$\$3) }' | sed \"s/^/${CMAKE_C_COMPILE_OPTIONS_IPO} /\" | sed \"s/___wrap_/__wrap_/g\" | sed \"s/__wrap_/-Wl,-wrap,/g\" > wrapper.def && test -s wrapper.def)
+ else()
+ add_custom_target(wrapper.def ALL ${CMAKE_NM} ${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/wrapper.dir/wrapper.c.o | grep __wrap | awk '{ printf(\"%s \", \$\$3) }' | sed \"s/___wrap_/__wrap_/g\" | sed \"s/__wrap_/-Wl,-wrap,/g\" > wrapper.def && test -s wrapper.def)
+ endif()
+
+ if(CORE_SYSTEM_NAME STREQUAL android)
+ add_custom_command(TARGET wrapper.def COMMAND echo \"-L${DEPENDS_PATH}/lib/dummy-lib${APP_NAME_LC} -l${APP_NAME_LC}\" >> wrapper.def)
+ endif()
+
+ set_target_properties(wrapper PROPERTIES FOLDER "Build Utilities")
+ set_target_properties(wrapper.def PROPERTIES FOLDER "Build Utilities")
+ add_dependencies(wrapper.def wrapper)
+endif()
+
diff --git a/xbmc/cores/DllLoader/exports/emu_dummy.cpp b/xbmc/cores/DllLoader/exports/emu_dummy.cpp
new file mode 100644
index 0000000..813c901
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/emu_dummy.cpp
@@ -0,0 +1,20 @@
+/*
+ * 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 "emu_dummy.h"
+
+#include "utils/log.h"
+
+extern "C" void not_implement( const char* debuginfo)
+{
+ if (debuginfo)
+ {
+ CLog::Log(LOGDEBUG, "{}", debuginfo);
+ }
+}
+
diff --git a/xbmc/cores/DllLoader/exports/emu_dummy.h b/xbmc/cores/DllLoader/exports/emu_dummy.h
new file mode 100644
index 0000000..f49e214
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/emu_dummy.h
@@ -0,0 +1,22 @@
+/*
+ * 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
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ void not_implement( const char* );
+
+#ifdef __cplusplus
+}
+#endif
+
+
diff --git a/xbmc/cores/DllLoader/exports/emu_msvcrt.cpp b/xbmc/cores/DllLoader/exports/emu_msvcrt.cpp
new file mode 100644
index 0000000..3f555db
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/emu_msvcrt.cpp
@@ -0,0 +1,2074 @@
+/*
+ * 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 <math.h>
+#include <mutex>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#ifndef TARGET_POSIX
+#include <io.h>
+#include <direct.h>
+#include <process.h>
+#include <errno.h>
+#else
+#if !defined(TARGET_DARWIN) && !defined(TARGET_FREEBSD)
+#include <mntent.h>
+#endif
+#endif
+#include <sys/stat.h>
+#include <sys/types.h>
+#if !defined(TARGET_FREEBSD) && (!defined(TARGET_ANDROID) && defined(__LP64__))
+#include <sys/timeb.h>
+#endif
+#ifdef HAS_DVD_DRIVE
+ #ifdef TARGET_POSIX
+ #include <sys/ioctl.h>
+ #if defined(TARGET_DARWIN)
+ #include <IOKit/storage/IODVDMediaBSDClient.h>
+ #elif !defined(TARGET_FREEBSD)
+ #include <linux/cdrom.h>
+ #endif
+ #endif
+#endif
+#include <fcntl.h>
+#include <time.h>
+#include <signal.h>
+#ifdef TARGET_POSIX
+#include "PlatformDefs.h" // for __stat64
+#endif
+#include "CompileInfo.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "emu_dummy.h"
+#include "emu_msvcrt.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "util/EmuFileWrapper.h"
+#include "utils/log.h"
+#ifndef TARGET_POSIX
+#include "utils/CharsetConverter.h"
+#include "utils/URIUtils.h"
+#endif
+#if !defined(TARGET_WINDOWS)
+#include <dlfcn.h>
+#endif
+#include "platform/Environment.h"
+#include "utils/StringUtils.h"
+#include "utils/XTimeUtils.h"
+
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/CharsetConverter.h"
+#endif
+
+using namespace XFILE;
+
+struct SDirData
+{
+ CFileItemList items;
+ int curr_index;
+ struct dirent *last_entry;
+ SDirData()
+ {
+ curr_index = -1;
+ last_entry = NULL;
+ }
+};
+
+#define MAX_OPEN_DIRS 10
+static SDirData vecDirsOpen[MAX_OPEN_DIRS];
+bool bVecDirsInited = false;
+extern void update_cache_dialog(const char* tmp);
+
+#define EMU_MAX_ENVIRONMENT_ITEMS 100
+static char *dll__environ_imp[EMU_MAX_ENVIRONMENT_ITEMS + 1];
+extern "C" char **dll__environ;
+char **dll__environ = dll__environ_imp;
+
+CCriticalSection dll_cs_environ;
+
+extern "C" void __stdcall init_emu_environ()
+{
+ memset(dll__environ, 0, EMU_MAX_ENVIRONMENT_ITEMS + 1);
+
+ // python
+#if defined(TARGET_WINDOWS_DESKTOP)
+ using KODI::PLATFORM::WINDOWS::FromW;
+ // fill our array with the windows system vars
+ LPTSTR lpszVariable;
+ LPTCH lpvEnv = NULL;
+ lpvEnv = GetEnvironmentStrings();
+ if (lpvEnv != NULL)
+ {
+ lpszVariable = (LPTSTR) lpvEnv;
+ while (*lpszVariable)
+ {
+ dll_putenv(FromW(lpszVariable).c_str());
+ lpszVariable += lstrlen(lpszVariable) + 1;
+ }
+ FreeEnvironmentStrings(lpvEnv);
+ }
+ dll_putenv("OS=win32");
+#elif defined(TARGET_WINDOWS_STORE)
+ dll_putenv("OS=win10");
+#elif defined(TARGET_DARWIN)
+ dll_putenv("OS=darwin");
+#elif defined(TARGET_POSIX)
+ dll_putenv("OS=linux");
+#else
+ dll_putenv("OS=unknown");
+#endif
+
+ // check if we are running as real xbmc.app or just binary
+ if (!CUtil::GetFrameworksPath(true).empty())
+ {
+ // using external python, it's build looking for xxx/lib/python(VERSIONMAJOR.MINOR)
+ // so point it to frameworks which is where python is located
+ dll_putenv(("PYTHONPATH=" +
+ CSpecialProtocol::TranslatePath("special://frameworks")).c_str());
+ dll_putenv(("PYTHONHOME=" +
+ CSpecialProtocol::TranslatePath("special://frameworks")).c_str());
+ dll_putenv(("PATH=.;" +
+ CSpecialProtocol::TranslatePath("special://xbmc") + ";" +
+ CSpecialProtocol::TranslatePath("special://frameworks")).c_str());
+ }
+ else
+ {
+ dll_putenv(("PYTHONPATH=" +
+ CSpecialProtocol::TranslatePath("special://xbmc/system/python/DLLs") + ";" +
+ CSpecialProtocol::TranslatePath("special://xbmc/system/python/Lib")).c_str());
+ dll_putenv(("PYTHONHOME=" +
+ CSpecialProtocol::TranslatePath("special://xbmc/system/python")).c_str());
+ dll_putenv(("PATH=.;" + CSpecialProtocol::TranslatePath("special://xbmc") + ";" +
+ CSpecialProtocol::TranslatePath("special://xbmc/system/python")).c_str());
+ }
+
+#if defined(TARGET_ANDROID)
+ std::string apkPath = getenv("KODI_ANDROID_APK");
+ apkPath += "/assets/python" + CCompileInfo::GetPythonVersion();
+ dll_putenv(("PYTHONHOME=" + apkPath).c_str());
+ dll_putenv("PYTHONOPTIMIZE=");
+ dll_putenv("PYTHONNOUSERSITE=1");
+ dll_putenv("PYTHONPATH=");
+#else
+ dll_putenv("PYTHONOPTIMIZE=1");
+#endif
+
+ //dll_putenv("PYTHONCASEOK=1");
+ //dll_putenv("PYTHONDEBUG=1");
+ //dll_putenv("PYTHONVERBOSE=2"); // "1" for normal verbose, "2" for more verbose ?
+ //dll_putenv("PYTHONDUMPREFS=1");
+ //dll_putenv("THREADDEBUG=1");
+ //dll_putenv("PYTHONMALLOCSTATS=1");
+ //dll_putenv("PYTHONY2K=1");
+ dll_putenv("TEMP=special://temp/temp"); // for python tempdir
+
+ // libdvdnav
+ dll_putenv("DVDREAD_NOKEYS=1");
+ //dll_putenv("DVDREAD_VERBOSE=1");
+ //dll_putenv("DVDREAD_USE_DIRECT=1");
+
+ // libdvdcss
+ dll_putenv("DVDCSS_METHOD=key");
+ dll_putenv("DVDCSS_VERBOSE=3");
+ dll_putenv("DVDCSS_CACHE=special://masterprofile/cache");
+}
+
+extern "C" void __stdcall update_emu_environ()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ // Use a proxy, if the GUI was configured as such
+ if (settings->GetBool(CSettings::SETTING_NETWORK_USEHTTPPROXY)
+ && !settings->GetString(CSettings::SETTING_NETWORK_HTTPPROXYSERVER).empty()
+ && settings->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYPORT) > 0
+ && settings->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYTYPE) == 0)
+ {
+ std::string strProxy;
+ if (!settings->GetString(CSettings::SETTING_NETWORK_HTTPPROXYUSERNAME).empty() &&
+ !settings->GetString(CSettings::SETTING_NETWORK_HTTPPROXYPASSWORD).empty())
+ {
+ strProxy = StringUtils::Format(
+ "{}:{}@", settings->GetString(CSettings::SETTING_NETWORK_HTTPPROXYUSERNAME),
+ settings->GetString(CSettings::SETTING_NETWORK_HTTPPROXYPASSWORD));
+ }
+
+ strProxy += settings->GetString(CSettings::SETTING_NETWORK_HTTPPROXYSERVER);
+ strProxy +=
+ StringUtils::Format(":{}", settings->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYPORT));
+
+ CEnvironment::setenv( "HTTP_PROXY", "http://" + strProxy, true );
+ CEnvironment::setenv( "HTTPS_PROXY", "http://" + strProxy, true );
+ }
+ else
+ {
+ // is there a better way to delete an environment variable?
+ // this works but leaves the variable
+ dll_putenv( "HTTP_PROXY=" );
+ dll_putenv( "HTTPS_PROXY=" );
+ }
+}
+
+extern "C" void __stdcall cleanup_emu_environ()
+{
+ for (int i = 0; i < EMU_MAX_ENVIRONMENT_ITEMS; i++)
+ {
+ free(dll__environ[i]);
+ dll__environ[i] = NULL;
+ }
+}
+
+static int convert_fmode(const char* mode)
+{
+ int iMode = O_BINARY;
+ if (strstr(mode, "r+"))
+ iMode |= O_RDWR;
+ else if (strchr(mode, 'r'))
+ iMode |= _O_RDONLY;
+ if (strstr(mode, "w+"))
+ iMode |= O_RDWR | _O_TRUNC;
+ else if (strchr(mode, 'w'))
+ iMode |= _O_WRONLY | O_CREAT;
+ return iMode;
+}
+
+#ifdef TARGET_WINDOWS
+static void to_finddata64i32(_wfinddata64i32_t *wdata, _finddata64i32_t *data)
+{
+ std::string strname;
+ g_charsetConverter.wToUTF8(wdata->name, strname);
+ size_t size = sizeof(data->name) / sizeof(char);
+ strncpy(data->name, strname.c_str(), size);
+ if (size)
+ data->name[size - 1] = '\0';
+ data->attrib = wdata->attrib;
+ data->time_create = wdata->time_create;
+ data->time_access = wdata->time_access;
+ data->time_write = wdata->time_write;
+ data->size = wdata->size;
+}
+
+static void to_wfinddata64i32(_finddata64i32_t *data, _wfinddata64i32_t *wdata)
+{
+ std::wstring strwname;
+ g_charsetConverter.utf8ToW(data->name, strwname, false);
+ size_t size = sizeof(wdata->name) / sizeof(wchar_t);
+ wcsncpy(wdata->name, strwname.c_str(), size);
+ if (size)
+ wdata->name[size - 1] = '\0';
+ wdata->attrib = data->attrib;
+ wdata->time_create = data->time_create;
+ wdata->time_access = data->time_access;
+ wdata->time_write = data->time_write;
+ wdata->size = data->size;
+}
+#endif
+
+extern "C"
+{
+ void dll_sleep(unsigned long imSec) { KODI::TIME::Sleep(std::chrono::milliseconds(imSec)); }
+
+ // FIXME, XXX, !!!!!!
+ void dllReleaseAll( )
+ {
+ // close all open dirs...
+ if (bVecDirsInited)
+ {
+ for (SDirData& dir : vecDirsOpen)
+ {
+ dir.items.Clear();
+ }
+ bVecDirsInited = false;
+ }
+ }
+
+ void* dllmalloc(size_t size)
+ {
+ void* pBlock = malloc(size);
+ if (!pBlock)
+ {
+ CLog::Log(LOGFATAL, "malloc {0} bytes failed, crash imminent", size);
+ }
+ return pBlock;
+ }
+
+ void dllfree( void* pPtr )
+ {
+ free(pPtr);
+ }
+
+ void* dllcalloc(size_t num, size_t size)
+ {
+ void* pBlock = calloc(num, size);
+ if (!pBlock)
+ {
+ CLog::Log(LOGFATAL, "calloc {0} bytes failed, crash imminent", size);
+ }
+ return pBlock;
+ }
+
+ void* dllrealloc( void *memblock, size_t size )
+ {
+ void* pBlock = realloc(memblock, size);
+ if (!pBlock)
+ {
+ CLog::Log(LOGFATAL, "realloc {0} bytes failed, crash imminent", size);
+ }
+ return pBlock;
+ }
+
+ void dllexit(int iCode)
+ {
+ not_implement("msvcrt.dll fake function exit() called\n"); //warning
+ }
+
+ void dllabort()
+ {
+ not_implement("msvcrt.dll fake function abort() called\n"); //warning
+ }
+
+ void* dll__dllonexit(PFV input, PFV ** start, PFV ** end)
+ {
+ //ported from WINE code
+ PFV *tmp;
+ int len;
+
+ if (!start || !*start || !end || !*end)
+ {
+ //FIXME("bad table\n");
+ return NULL;
+ }
+
+ len = (*end - *start);
+
+ if (++len <= 0)
+ return NULL;
+
+ tmp = (PFV*) realloc (*start, len * sizeof(tmp) );
+ if (!tmp)
+ return NULL;
+ *start = tmp;
+ *end = tmp + len;
+ tmp[len - 1] = input;
+ return (void *)input;
+
+ //wrong handling, this function is used for register functions
+ //that called before exit use _initterm functions.
+
+ //dllReleaseAll( );
+ //return TRUE;
+ }
+
+ _onexit_t dll_onexit(_onexit_t func)
+ {
+ not_implement("msvcrt.dll fake function dll_onexit() called\n");
+
+ // register to dll unload list
+ // return func if successfully added to the dll unload list
+ return NULL;
+ }
+
+ int dllputs(const char* szLine)
+ {
+ if (!szLine[0]) return EOF;
+ if (szLine[strlen(szLine) - 1] != '\n')
+ CLog::Log(LOGDEBUG, " msg: {}", szLine);
+ else
+ CLog::Log(LOGDEBUG, " msg: {}", szLine);
+
+ // return a non negative value
+ return 0;
+ }
+
+ int dllprintf(const char *format, ...)
+ {
+ va_list va;
+ static char tmp[2048];
+ va_start(va, format);
+ _vsnprintf(tmp, 2048, format, va);
+ va_end(va);
+ tmp[2048 - 1] = 0;
+ CLog::Log(LOGDEBUG, " msg: {}", tmp);
+
+ return strlen(tmp);
+ }
+
+ char *dll_fullpath(char *absPath, const char *relPath, size_t maxLength)
+ {
+ unsigned int len = strlen(relPath);
+ if (len > maxLength && absPath != NULL) return NULL;
+
+ // dll has to make sure it uses the correct path for now
+ if (len > 1 && relPath[1] == ':')
+ {
+ if (absPath == NULL) absPath = dll_strdup(relPath);
+ else
+ {
+ strncpy(absPath, relPath, maxLength);
+ if (maxLength != 0)
+ absPath[maxLength-1] = '\0';
+ }
+ return absPath;
+ }
+ if (!strncmp(relPath, "\\Device\\Cdrom0", 14))
+ {
+ // needed?
+ if (absPath == NULL) absPath = strdup(relPath);
+ else
+ {
+ strncpy(absPath, relPath, maxLength);
+ if (maxLength != 0)
+ absPath[maxLength-1] = '\0';
+ }
+ return absPath;
+ }
+
+ not_implement("msvcrt.dll incomplete function _fullpath(...) called\n"); //warning
+ return NULL;
+ }
+
+ FILE* dll_popen(const char *command, const char *mode)
+ {
+ not_implement("msvcrt.dll fake function _popen(...) called\n"); //warning
+ return NULL;
+ }
+
+ void *dll_dlopen(const char *filename, int flag)
+ {
+#if !defined(TARGET_WINDOWS)
+ return dlopen(filename, flag);
+#else
+ return NULL;
+#endif
+ }
+
+ int dll_pclose(FILE *stream)
+ {
+ not_implement("msvcrt.dll fake function _pclose(...) called\n"); //warning
+ return 0;
+ }
+
+ FILE* dll_fdopen(int fd, const char* mode)
+ {
+ EmuFileObject* o = g_emuFileWrapper.GetFileObjectByDescriptor(fd);
+ if (o)
+ {
+ if(!o->used)
+ return NULL;
+
+ int nmode = convert_fmode(mode);
+ if( (o->mode & nmode) != nmode)
+ CLog::Log(LOGWARNING, "dll_fdopen - mode 0x{:x} differs from fd mode 0x{:x}", nmode,
+ o->mode);
+ return reinterpret_cast<FILE*>(o);
+ }
+ else if (!IS_STD_DESCRIPTOR(fd))
+ {
+ // it might be something else than a file, or the file is not emulated
+ // let the operating system handle it
+ return _fdopen(fd, mode);
+ }
+
+ not_implement("msvcrt.dll incomplete function _fdopen(...) called\n");
+ return NULL;
+ }
+
+ int dll_open(const char* szFileName, int iMode)
+ {
+ char str[1024];
+ int size = sizeof(str);
+ // move to CFile classes
+ if (strncmp(szFileName, "\\Device\\Cdrom0", 14) == 0)
+ {
+ // replace "\\Device\\Cdrom0" with "D:"
+ strncpy(str, "D:", size);
+ if (size)
+ {
+ str[size-1] = '\0';
+ strncat(str, szFileName + 14, size - strlen(str));
+ }
+ }
+ else
+ {
+ strncpy(str, szFileName, size);
+ if (size)
+ str[size-1] = '\0';
+ }
+
+ CFile* pFile = new CFile();
+ bool bWrite = false;
+ if ((iMode & O_RDWR) || (iMode & O_WRONLY))
+ bWrite = true;
+ bool bOverwrite=false;
+ if ((iMode & _O_TRUNC) || (iMode & O_CREAT))
+ bOverwrite = true;
+ // currently always overwrites
+ bool bResult;
+
+ // We need to validate the path here as some calls from ie. libdvdnav
+ // or the python DLLs have malformed slashes on Win32
+ // (-> E:\test\VIDEO_TS/VIDEO_TS.BUP))
+ if (bWrite)
+ bResult = pFile->OpenForWrite(CUtil::ValidatePath(str), bOverwrite);
+ else
+ bResult = pFile->Open(CUtil::ValidatePath(str), READ_TRUNCATED);
+
+ if (bResult)
+ {
+ EmuFileObject* object = g_emuFileWrapper.RegisterFileObject(pFile);
+ if (object == NULL)
+ {
+ pFile->Close();
+ delete pFile;
+ return -1;
+ }
+ object->mode = iMode;
+ FILE* f = reinterpret_cast<FILE*>(object);
+ return g_emuFileWrapper.GetDescriptorByStream(f);
+ }
+ delete pFile;
+ return -1;
+ }
+
+ FILE* dll_freopen(const char *path, const char *mode, FILE *stream)
+ {
+ if (g_emuFileWrapper.StreamIsEmulatedFile(stream))
+ {
+ dll_fclose(stream);
+ return dll_fopen(path, mode);
+ }
+
+ // error
+ // close stream and return NULL
+ dll_fclose(stream);
+ return NULL;
+ }
+
+ int dll_read(int fd, void* buffer, unsigned int uiSize)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (pFile != NULL)
+ {
+ errno = 0;
+ const ssize_t ret = pFile->Read(buffer, uiSize);
+ if (ret < 0)
+ {
+ const int err = errno; // help compiler to optimize, "errno" can be macro
+ if (err == 0 ||
+ (err != EAGAIN && err != EINTR && err != EIO && err != EOVERFLOW && err != EWOULDBLOCK &&
+ err != ECONNRESET && err != ENOTCONN && err != ETIMEDOUT &&
+ err != ENOBUFS && err != ENOMEM && err != ENXIO))
+ errno = EIO; // exact errno is unknown or incorrect, use default error number
+
+ return -1;
+ }
+ return ret;
+ }
+ else if (!IS_STD_DESCRIPTOR(fd))
+ {
+ // it might be something else than a file, or the file is not emulated
+ // let the operating system handle it
+ return read(fd, buffer, uiSize);
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ errno = EBADF;
+ return -1;
+ }
+
+ int dll_write(int fd, const void* buffer, unsigned int uiSize)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (pFile != NULL)
+ {
+ errno = 0;
+ const ssize_t ret = pFile->Write(buffer, uiSize);
+ if (ret < 0)
+ {
+ const int err = errno; // help compiler to optimize, "errno" can be macro
+ if (err == 0 ||
+ (err != EAGAIN && err != EFBIG && err != EINTR && err != EIO && err != ENOSPC && err != EPIPE && err != EWOULDBLOCK &&
+ err != ECONNRESET &&
+ err != ENOBUFS && err != ENXIO &&
+ err != EACCES && err != ENETDOWN && err != ENETUNREACH))
+ errno = EIO; // exact errno is unknown or incorrect, use default error number
+
+ return -1;
+ }
+ return ret;
+ }
+ else if (!IS_STD_DESCRIPTOR(fd))
+ {
+ // it might be something else than a file, or the file is not emulated
+ // let the operating system handle it
+ return write(fd, buffer, uiSize);
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ errno = EBADF;
+ return -1;
+ }
+
+ int dll_fstat64(int fd, struct __stat64 *buf)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (pFile != NULL)
+ return pFile->Stat(buf);
+ else if (IS_STD_DESCRIPTOR(fd))
+#if defined(TARGET_WINDOWS)
+ return _fstat64(fd, buf);
+#else
+ return fstat64(fd, buf);
+#endif
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return -1;
+ }
+
+ int dll_close(int fd)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (pFile != NULL)
+ {
+ g_emuFileWrapper.UnRegisterFileObjectByDescriptor(fd);
+
+ pFile->Close();
+ delete pFile;
+ return 0;
+ }
+ else if (!IS_STD_DESCRIPTOR(fd) && fd >= 0)
+ {
+ // it might be something else than a file, or the file is not emulated
+ // let the operating system handle it
+ return close(fd);
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return -1;
+ }
+
+ __off64_t dll_lseeki64(int fd, __off64_t lPos, int iWhence)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (pFile != NULL)
+ {
+ lPos = pFile->Seek(lPos, iWhence);
+ return lPos;
+ }
+ else if (!IS_STD_DESCRIPTOR(fd))
+ {
+ // it might be something else than a file, or the file is not emulated
+ // let the operating system handle it
+ // not supported: return lseeki64(fd, lPos, iWhence);
+ CLog::Log(LOGWARNING, "msvcrt.dll: dll_lseeki64 called, TODO: add 'int64 -> long' type checking"); //warning
+ return static_cast<long long>(lseek(fd, (long)lPos, iWhence));
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return -1ll;
+ }
+
+ __off_t dll_lseek(int fd, __off_t lPos, int iWhence)
+ {
+ if (g_emuFileWrapper.DescriptorIsEmulatedFile(fd))
+ {
+ return (__off_t)dll_lseeki64(fd, lPos, iWhence);
+ }
+ else if (!IS_STD_DESCRIPTOR(fd))
+ {
+ // it might be something else than a file, or the file is not emulated
+ // let the operating system handle it
+ return lseek(fd, lPos, iWhence);
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return -1;
+ }
+
+ void dll_rewind(FILE* stream)
+ {
+ int fd = g_emuFileWrapper.GetDescriptorByStream(stream);
+ if (fd >= 0)
+ {
+ dll_lseeki64(fd, 0, SEEK_SET);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ }
+ }
+
+ //---------------------------------------------------------------------------------------------------------
+ void dll_flockfile(FILE *stream)
+ {
+ int fd = g_emuFileWrapper.GetDescriptorByStream(stream);
+ if (fd >= 0)
+ {
+ g_emuFileWrapper.LockFileObjectByDescriptor(fd);
+ return;
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ }
+
+ int dll_ftrylockfile(FILE *stream)
+ {
+ int fd = g_emuFileWrapper.GetDescriptorByStream(stream);
+ if (fd >= 0)
+ {
+ if (g_emuFileWrapper.TryLockFileObjectByDescriptor(fd))
+ return 0;
+ return -1;
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return -1;
+ }
+
+ void dll_funlockfile(FILE *stream)
+ {
+ int fd = g_emuFileWrapper.GetDescriptorByStream(stream);
+ if (fd >= 0)
+ {
+ g_emuFileWrapper.UnlockFileObjectByDescriptor(fd);
+ return;
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ }
+
+ int dll_fclose(FILE * stream)
+ {
+ int fd = g_emuFileWrapper.GetDescriptorByStream(stream);
+ if (fd >= 0)
+ {
+ return dll_close(fd) == 0 ? 0 : EOF;
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return EOF;
+ }
+
+#ifndef TARGET_POSIX
+ // should be moved to CFile classes
+ intptr_t dll_findfirst(const char *file, struct _finddata_t *data)
+ {
+ struct _finddata64i32_t data64i32;
+ intptr_t ret = dll_findfirst64i32(file, &data64i32);
+ if (ret != -1)
+ {
+ int size = sizeof(data->name);
+ strncpy(data->name, data64i32.name, size);
+ if (size)
+ data->name[size - 1] = '\0';
+ data->size = (_fsize_t)data64i32.size;
+ data->time_write = (time_t)data64i32.time_write;
+ data->time_access = (time_t)data64i32.time_access;
+ }
+ return ret;
+ }
+
+ intptr_t dll_findfirst64i32(const char *file, struct _finddata64i32_t *data)
+ {
+ char str[1024];
+ int size = sizeof(str);
+ CURL url(CSpecialProtocol::TranslatePath(file));
+ if (url.IsLocal())
+ {
+ // move to CFile classes
+ if (strncmp(file, "\\Device\\Cdrom0", 14) == 0)
+ {
+ // replace "\\Device\\Cdrom0" with "D:"
+ strncpy(str, "D:", size);
+ if (size)
+ {
+ str[size - 1] = '\0';
+ strncat(str, file + 14, size - strlen(str));
+ }
+ }
+ else
+ {
+ strncpy(str, file, size);
+ if (size)
+ str[size - 1] = '\0';
+ }
+
+ // Make sure the slashes are correct & translate the path
+ struct _wfinddata64i32_t wdata;
+ std::wstring strwfile;
+ g_charsetConverter.utf8ToW(CUtil::ValidatePath(CSpecialProtocol::TranslatePath(str)), strwfile, false);
+ intptr_t ret = _wfindfirst64i32(strwfile.c_str(), &wdata);
+ if (ret != -1)
+ to_finddata64i32(&wdata, data);
+ return ret;
+ }
+ // non-local files. handle through IDirectory-class - only supports '*.bah' or '*.*'
+ std::string strURL(file);
+ std::string strMask;
+ if (url.GetFileName().find("*.*") != std::string::npos)
+ {
+ std::string strReplaced = url.GetFileName();
+ StringUtils::Replace(strReplaced, "*.*","");
+ url.SetFileName(strReplaced);
+ }
+ else if (url.GetFileName().find("*.") != std::string::npos)
+ {
+ strMask = URIUtils::GetExtension(url.GetFileName());
+ url.SetFileName(url.GetFileName().substr(0, url.GetFileName().find("*.")));
+ }
+ else if (url.GetFileName().find("*") != std::string::npos)
+ {
+ std::string strReplaced = url.GetFileName();
+ StringUtils::Replace(strReplaced, "*","");
+ url.SetFileName(strReplaced);
+ }
+ int iDirSlot=0; // locate next free directory
+ while ((iDirSlot < MAX_OPEN_DIRS) && (vecDirsOpen[iDirSlot].curr_index != -1)) iDirSlot++;
+ if (iDirSlot >= MAX_OPEN_DIRS)
+ return -1; // no free slots
+ strURL = url.Get();
+ bVecDirsInited = true;
+ vecDirsOpen[iDirSlot].items.Clear();
+ XFILE::CDirectory::GetDirectory(strURL, vecDirsOpen[iDirSlot].items, strMask, DIR_FLAG_DEFAULTS);
+ if (vecDirsOpen[iDirSlot].items.Size())
+ {
+ int size = sizeof(data->name);
+ strncpy(data->name,vecDirsOpen[iDirSlot].items[0]->GetLabel().c_str(), size);
+ if (size)
+ data->name[size - 1] = '\0';
+ data->size = static_cast<_fsize_t>(vecDirsOpen[iDirSlot].items[0]->m_dwSize);
+ data->time_write = 0;
+ data->time_access = 0;
+ vecDirsOpen[iDirSlot].curr_index = 0;
+ return (intptr_t)&vecDirsOpen[iDirSlot];
+ }
+ vecDirsOpen[iDirSlot].curr_index = -1;
+ return -1; // whatever != NULL
+ }
+
+ // should be moved to CFile classes
+ int dll_findnext(intptr_t f, _finddata_t* data)
+ {
+ struct _finddata64i32_t data64i32;
+ int ret = dll_findnext64i32(f, &data64i32);
+ if (ret == 0)
+ {
+ int size = sizeof(data->name);
+ strncpy(data->name, data64i32.name, size);
+ if (size)
+ data->name[size - 1] = '\0';
+ data->size = (_fsize_t)data64i32.size;
+ data->time_write = (time_t)data64i32.time_write;
+ data->time_access = (time_t)data64i32.time_access;
+ }
+ return ret;
+ }
+
+ int dll_findnext64i32(intptr_t f, _finddata64i32_t* data)
+ {
+ int found = MAX_OPEN_DIRS;
+ for (int i = 0; i < MAX_OPEN_DIRS; i++)
+ {
+ if (f == (intptr_t)&vecDirsOpen[i] && vecDirsOpen[i].curr_index != -1)
+ {
+ found = i;
+ break;
+ }
+ }
+ if (found >= MAX_OPEN_DIRS)
+ {
+ struct _wfinddata64i32_t wdata;
+ to_wfinddata64i32(data, &wdata);
+ intptr_t ret = _wfindnext64i32(f, &wdata); // local dir
+ if (ret != -1)
+ to_finddata64i32(&wdata, data);
+ return ret;
+ }
+
+ // we have a valid data structure. get next item!
+ int iItem = vecDirsOpen[found].curr_index;
+ if (iItem+1 < vecDirsOpen[found].items.Size()) // we have a winner!
+ {
+ int size = sizeof(data->name);
+ strncpy(data->name,vecDirsOpen[found].items[iItem+1]->GetLabel().c_str(), size);
+ if (size)
+ data->name[size - 1] = '\0';
+ data->size = static_cast<_fsize_t>(vecDirsOpen[found].items[iItem+1]->m_dwSize);
+ vecDirsOpen[found].curr_index++;
+ return 0;
+ }
+
+ vecDirsOpen[found].items.Clear();
+ return -1;
+ }
+
+ int dll_findclose(intptr_t handle)
+ {
+ int found = MAX_OPEN_DIRS;
+ for (int i = 0; i < MAX_OPEN_DIRS; i++)
+ {
+ if (handle == (intptr_t)&vecDirsOpen[i] && vecDirsOpen[i].curr_index != -1)
+ {
+ found = i;
+ break;
+ }
+ }
+ if (found >= MAX_OPEN_DIRS)
+ return _findclose(handle);
+
+ vecDirsOpen[found].items.Clear();
+ vecDirsOpen[found].curr_index = -1;
+ return 0;
+ }
+
+ void dll__security_error_handler(int code, void *data)
+ {
+ //NOTE: __security_error_handler has been removed in VS2005 and up
+ CLog::Log(LOGERROR, "security_error, code {}", code);
+ }
+
+#endif
+
+ DIR *dll_opendir(const char *file)
+ {
+ CURL url(CSpecialProtocol::TranslatePath(file));
+ if (url.IsLocal())
+ { // Make sure the slashes are correct & translate the path
+ return opendir(CUtil::ValidatePath(url.Get().c_str()).c_str());
+ }
+
+ // locate next free directory
+ int iDirSlot=0;
+ while ((iDirSlot<MAX_OPEN_DIRS) && (vecDirsOpen[iDirSlot].curr_index != -1)) iDirSlot++;
+ if (iDirSlot >= MAX_OPEN_DIRS)
+ {
+ CLog::Log(LOGDEBUG, "Dll: Max open dirs reached");
+ return NULL; // no free slots
+ }
+
+ bVecDirsInited = true;
+ vecDirsOpen[iDirSlot].items.Clear();
+
+ if (XFILE::CDirectory::GetDirectory(url.Get(), vecDirsOpen[iDirSlot].items, "", DIR_FLAG_DEFAULTS))
+ {
+ vecDirsOpen[iDirSlot].curr_index = 0;
+ return (DIR *)&vecDirsOpen[iDirSlot];
+ }
+ else
+ return NULL;
+ }
+
+ struct dirent *dll_readdir(DIR *dirp)
+ {
+ if (!dirp)
+ return NULL;
+
+ bool emulated(false);
+ for (const SDirData& dir : vecDirsOpen)
+ {
+ if (dirp == (DIR*)&dir)
+ {
+ emulated = true;
+ break;
+ }
+ }
+ if (!emulated)
+ return readdir(dirp); // local dir
+
+ // dirp is actually a SDirData*
+ SDirData* dirData = reinterpret_cast<SDirData*>(dirp);
+ if (dirData->last_entry)
+ free(dirData->last_entry);
+ struct dirent *entry = NULL;
+ entry = (dirent*) malloc(sizeof(*entry));
+ if (dirData->curr_index < dirData->items.Size() + 2)
+ { // simulate the '.' and '..' dir entries
+ if (dirData->curr_index == 0)
+ strncpy(entry->d_name, ".\0", 2);
+ else if (dirData->curr_index == 1)
+ strncpy(entry->d_name, "..\0", 3);
+ else
+ {
+ strncpy(entry->d_name, dirData->items[dirData->curr_index - 2]->GetLabel().c_str(), sizeof(entry->d_name));
+ entry->d_name[sizeof(entry->d_name)-1] = '\0'; // null-terminate any truncated paths
+ }
+ dirData->last_entry = entry;
+ dirData->curr_index++;
+ return entry;
+ }
+ free(entry);
+ return NULL;
+ }
+
+ int dll_closedir(DIR *dirp)
+ {
+ bool emulated(false);
+ for (const SDirData& dir : vecDirsOpen)
+ {
+ if (dirp == (DIR*)&dir)
+ {
+ emulated = true;
+ break;
+ }
+ }
+ if (!emulated)
+ return closedir(dirp);
+
+ SDirData* dirData = reinterpret_cast<SDirData*>(dirp);
+ dirData->items.Clear();
+ if (dirData->last_entry)
+ {
+ dirData->last_entry = NULL;
+ }
+ dirData->curr_index = -1;
+ return 0;
+ }
+
+ void dll_rewinddir(DIR *dirp)
+ {
+ bool emulated(false);
+ for (const SDirData& dir : vecDirsOpen)
+ {
+ if (dirp == (DIR*)&dir)
+ {
+ emulated = true;
+ break;
+ }
+ }
+ if (!emulated)
+ {
+ rewinddir(dirp);
+ return;
+ }
+
+ SDirData* dirData = reinterpret_cast<SDirData*>(dirp);
+ if (dirData->last_entry)
+ {
+ dirData->last_entry = NULL;
+ }
+ dirData->curr_index = 0;
+ }
+
+ char* dll_fgets(char* pszString, int num ,FILE * stream)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByStream(stream);
+ if (pFile != NULL)
+ {
+ if (pFile->GetPosition() < pFile->GetLength())
+ {
+ bool bRead = pFile->ReadString(pszString, num);
+ if (bRead)
+ {
+ return pszString;
+ }
+ }
+ else return NULL; //eof
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return NULL;
+ }
+
+ int dll_feof(FILE * stream)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByStream(stream);
+ if (pFile != NULL)
+ {
+ if (pFile->GetPosition() < pFile->GetLength()) return 0;
+ else return 1;
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return 1; // eof by default
+ }
+
+ int dll_fread(void * buffer, size_t size, size_t count, FILE * stream)
+ {
+ if (size == 0 || count == 0)
+ return 0;
+
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByStream(stream);
+ if (pFile != NULL)
+ {
+ size_t read = 0;
+ const size_t bufSize = size * count;
+ do // fread() must read all data until buffer is filled or eof/error occurs
+ {
+ const ssize_t r = pFile->Read(((int8_t*)buffer) + read, bufSize - read);
+ if (r <= 0)
+ break;
+ read += r;
+ } while (bufSize > read);
+ return read / size;
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return 0;
+ }
+
+ int dll_fgetc(FILE* stream)
+ {
+ if (g_emuFileWrapper.StreamIsEmulatedFile(stream))
+ {
+ // it is a emulated file
+ unsigned char buf;
+
+ if (dll_fread(&buf, 1, 1, stream) <= 0)
+ return EOF;
+
+ return (int)buf;
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return EOF;
+ }
+
+ int dll_getc(FILE* stream)
+ {
+ if (g_emuFileWrapper.StreamIsEmulatedFile(stream))
+ {
+ // This routine is normally implemented as a macro with the same result as fgetc().
+ return dll_fgetc(stream);
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return EOF;
+ }
+
+ FILE* dll_fopen(const char* filename, const char* mode)
+ {
+ FILE* file = NULL;
+#if defined(TARGET_LINUX) && !defined(TARGET_ANDROID)
+ if (strcmp(filename, _PATH_MOUNTED) == 0
+ || strcmp(filename, _PATH_MNTTAB) == 0)
+ {
+ CLog::Log(LOGINFO,
+ "{} - something opened the mount file, let's hope it knows what it's doing",
+ __FUNCTION__);
+ return fopen(filename, mode);
+ }
+#endif
+ int fd = dll_open(filename, convert_fmode(mode));
+ if (fd >= 0)
+ {
+ file = g_emuFileWrapper.GetStreamByDescriptor(fd);
+ }
+
+ return file;
+ }
+
+ int dll_fopen_s(FILE** pFile, const char * filename, const char * mode)
+ {
+ if (pFile == NULL || filename == NULL || mode == NULL)
+ return EINVAL;
+
+ *pFile = dll_fopen(filename, mode);
+ if (*pFile == NULL)
+ return errno;
+
+ return 0;
+ }
+
+ int dll_putc(int c, FILE *stream)
+ {
+ if (g_emuFileWrapper.StreamIsEmulatedFile(stream) || IS_STD_STREAM(stream))
+ {
+ return dll_fputc(c, stream);
+ }
+ return EOF;
+ }
+
+ int dll_putchar(int c)
+ {
+ return dll_putc(c, stdout);
+ }
+
+ int dll_fputc(int character, FILE* stream)
+ {
+ if (IS_STDOUT_STREAM(stream) || IS_STDERR_STREAM(stream) || !IS_VALID_STREAM(stream))
+ {
+ unsigned char tmp[2] = { (unsigned char)character, 0 };
+ dllputs((char *)tmp);
+ return character;
+ }
+ else
+ {
+ if (g_emuFileWrapper.StreamIsEmulatedFile(stream))
+ {
+ int fd = g_emuFileWrapper.GetDescriptorByStream(stream);
+ if (fd >= 0)
+ {
+ unsigned char c = (unsigned char)character;
+ int iItemsWritten = dll_write(fd, &c, 1);
+ if (iItemsWritten == 1)
+ return character;
+ }
+ }
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return EOF;
+ }
+
+ int dll_fputs(const char * szLine, FILE* stream)
+ {
+ if (IS_STDOUT_STREAM(stream) || IS_STDERR_STREAM(stream) || !IS_VALID_STREAM(stream))
+ {
+ dllputs(szLine);
+ return 0;
+ }
+ else
+ {
+ if (g_emuFileWrapper.StreamIsEmulatedFile(stream))
+ {
+ size_t len = strlen(szLine);
+ return dll_fwrite(static_cast<const void*>(szLine), sizeof(char), len, stream);
+ }
+ }
+
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return EOF;
+ }
+
+ int dll_fseek64(FILE* stream, off64_t offset, int origin)
+ {
+ int fd = g_emuFileWrapper.GetDescriptorByStream(stream);
+ if (fd >= 0)
+ {
+ if (dll_lseeki64(fd, offset, origin) != -1)
+ {
+ return 0;
+ }
+ else return -1;
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return -1;
+ }
+
+ int dll_fseek(FILE *stream, long offset, int origin)
+ {
+ return dll_fseek64(stream, offset, origin);
+ }
+
+ int dll_ungetc(int c, FILE* stream)
+ {
+ if (g_emuFileWrapper.StreamIsEmulatedFile(stream))
+ {
+ // it is a emulated file
+ int d;
+ if (dll_fseek(stream, -1, SEEK_CUR)!=0)
+ return EOF;
+ d = dll_fgetc(stream);
+ if (d == EOF)
+ return EOF;
+
+ dll_fseek(stream, -1, SEEK_CUR);
+ if (c != d)
+ {
+ CLog::Log(LOGWARNING, "{}: c != d", __FUNCTION__);
+ d = fputc(c, stream);
+ if (d != c)
+ CLog::Log(LOGERROR, "{}: Write failed!", __FUNCTION__);
+ else
+ dll_fseek(stream, -1, SEEK_CUR);
+ }
+ return d;
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return EOF;
+ }
+
+ long dll_ftell(FILE *stream)
+ {
+ return (long)dll_ftell64(stream);
+ }
+
+ off64_t dll_ftell64(FILE *stream)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByStream(stream);
+ if (pFile != NULL)
+ {
+ return (off64_t)pFile->GetPosition();
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return -1;
+ }
+
+ long dll_tell(int fd)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (pFile != NULL)
+ {
+ return (long)pFile->GetPosition();
+ }
+ else if (!IS_STD_DESCRIPTOR(fd))
+ {
+ // it might be something else than a file, or the file is not emulated
+ // let the operating system handle it
+#ifndef TARGET_POSIX
+ return tell(fd);
+#else
+ return lseek(fd, 0, SEEK_CUR);
+#endif
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return -1;
+ }
+
+ long long dll_telli64(int fd)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (pFile != NULL)
+ {
+ return static_cast<long long>(pFile->GetPosition());
+ }
+ else if (!IS_STD_DESCRIPTOR(fd))
+ {
+ // it might be something else than a file, or the file is not emulated
+ // let the operating system handle it
+ // not supported return telli64(fd);
+ CLog::Log(LOGWARNING, "msvcrt.dll: dll_telli64 called, TODO: add 'int64 -> long' type checking"); //warning
+#ifndef TARGET_POSIX
+ return static_cast<long long>(tell(fd));
+#elif defined(TARGET_DARWIN) || defined(TARGET_FREEBSD) || defined(TARGET_ANDROID)
+ return lseek(fd, 0, SEEK_CUR);
+#else
+ return lseek64(fd, 0, SEEK_CUR);
+#endif
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return -1;
+ }
+
+ size_t dll_fwrite(const void * buffer, size_t size, size_t count, FILE* stream)
+ {
+ if (size == 0 || count == 0)
+ return 0;
+
+ if (IS_STDOUT_STREAM(stream) || IS_STDERR_STREAM(stream) || !IS_VALID_STREAM(stream))
+ {
+ char* buf = (char*)malloc(size * count + 1);
+ if (buf)
+ {
+ memcpy(buf, buffer, size * count);
+ buf[size * count] = 0; // string termination
+
+ CLog::Log(LOGDEBUG, "{}", buf);
+
+ free(buf);
+ return count;
+ }
+ }
+ else
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByStream(stream);
+ if (pFile != NULL)
+ {
+ size_t written = 0;
+ const size_t bufSize = size * count;
+ do // fwrite() must write all data until whole buffer is written or error occurs
+ {
+ const ssize_t w = pFile->Write(((const int8_t*)buffer) + written, bufSize - written);
+ if (w <= 0)
+ break;
+ written += w;
+ } while (bufSize > written);
+ return written / size;
+ }
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return 0;
+ }
+
+ int dll_fflush(FILE* stream)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByStream(stream);
+ if (pFile != NULL)
+ {
+ pFile->Flush();
+ return 0;
+ }
+
+ // std stream, no need to flush
+ return 0;
+ }
+
+ int dll_ferror(FILE* stream)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByStream(stream);
+ if (pFile != NULL)
+ {
+ // unimplemented
+ return 0;
+ }
+ else if (IS_STD_STREAM(stream))
+ return 0;
+ else
+ return ferror(stream);
+ }
+
+ int dllvprintf(const char *format, va_list va)
+ {
+ std::string buffer = StringUtils::FormatV(format, va);
+ CLog::Log(LOGDEBUG, " msg: {}", buffer);
+ return buffer.length();
+ }
+
+ int dll_vfprintf(FILE *stream, const char *format, va_list va)
+ {
+ static char tmp[2048];
+
+ if (_vsnprintf(tmp, 2048, format, va) == -1)
+ {
+ CLog::Log(LOGWARNING, "dll_vfprintf: Data lost due to undersized buffer");
+ }
+ tmp[2048 - 1] = 0;
+
+ if (IS_STDOUT_STREAM(stream) || IS_STDERR_STREAM(stream) || !IS_VALID_STREAM(stream))
+ {
+ CLog::Log(LOGINFO, " msg: {}", tmp);
+ return strlen(tmp);
+ }
+ else
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByStream(stream);
+ if (pFile != NULL)
+ {
+ int len = strlen(tmp);
+ // replace all '\n' occurrences with '\r\n'...
+ char tmp2[2048];
+ int j = 0;
+ for (int i = 0; i < len; i++)
+ {
+ if (j == 2047)
+ { // out of space
+ if (i != len-1)
+ CLog::Log(LOGWARNING, "dll_fprintf: Data lost due to undersized buffer");
+ break;
+ }
+ if (tmp[i] == '\n' && ((i > 0 && tmp[i-1] != '\r') || i == 0) && j < 2047 - 2)
+ { // need to add a \r
+ tmp2[j++] = '\r';
+ tmp2[j++] = '\n';
+ }
+ else
+ { // just add the character as-is
+ tmp2[j++] = tmp[i];
+ }
+ }
+ // terminate string
+ tmp2[j] = 0;
+ len = strlen(tmp2);
+ pFile->Write(tmp2, len);
+ return len;
+ }
+ }
+
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return strlen(tmp);
+ }
+
+ int dll_fscanf(FILE* stream, const char* format, ...)
+ {
+ CLog::Log(LOGERROR, "{} is not implemented", __FUNCTION__);
+ return -1;
+ }
+
+ int dll_fprintf(FILE* stream, const char* format, ...)
+ {
+ int res;
+ va_list va;
+ va_start(va, format);
+ res = dll_vfprintf(stream, format, va);
+ va_end(va);
+ return res;
+ }
+
+ int dll_fgetpos(FILE* stream, fpos_t* pos)
+ {
+ fpos64_t tmpPos = {};
+ int ret;
+
+ ret = dll_fgetpos64(stream, &tmpPos);
+#if !defined(TARGET_POSIX) || defined(TARGET_DARWIN) || defined(TARGET_FREEBSD) || defined(TARGET_ANDROID)
+ *pos = (fpos_t)tmpPos;
+#else
+ pos->__pos = (off_t)tmpPos.__pos;
+#endif
+ return ret;
+ }
+
+ int dll_fgetpos64(FILE *stream, fpos64_t *pos)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByStream(stream);
+ if (pFile != NULL)
+ {
+#if !defined(TARGET_POSIX) || defined(TARGET_DARWIN) || defined(TARGET_FREEBSD) || defined(TARGET_ANDROID)
+ *pos = pFile->GetPosition();
+#else
+ pos->__pos = pFile->GetPosition();
+#endif
+ return 0;
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return EINVAL;
+ }
+
+ int dll_fsetpos64(FILE* stream, const fpos64_t* pos)
+ {
+ int fd = g_emuFileWrapper.GetDescriptorByStream(stream);
+ if (fd >= 0)
+ {
+#if !defined(TARGET_POSIX) || defined(TARGET_DARWIN) || defined(TARGET_FREEBSD) || defined(TARGET_ANDROID)
+ if (dll_lseeki64(fd, *pos, SEEK_SET) >= 0)
+#else
+ if (dll_lseeki64(fd, (__off64_t)pos->__pos, SEEK_SET) >= 0)
+#endif
+ {
+ return 0;
+ }
+ else
+ {
+ return EINVAL;
+ }
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return EINVAL;
+ }
+
+ int dll_fsetpos(FILE* stream, const fpos_t* pos)
+ {
+ int fd = g_emuFileWrapper.GetDescriptorByStream(stream);
+ if (fd >= 0)
+ {
+ fpos64_t tmpPos;
+#if !defined(TARGET_POSIX) || defined(TARGET_DARWIN) || defined(TARGET_FREEBSD) || defined(TARGET_ANDROID)
+ tmpPos= *pos;
+#else
+ tmpPos.__pos = (off64_t)(pos->__pos);
+#endif
+ return dll_fsetpos64(stream, &tmpPos);
+ }
+ CLog::Log(LOGERROR, "{} emulated function failed", __FUNCTION__);
+ return EINVAL;
+ }
+
+ int dll_fileno(FILE* stream)
+ {
+ int fd = g_emuFileWrapper.GetDescriptorByStream(stream);
+ if (fd >= 0)
+ {
+ return fd;
+ }
+ else if (IS_STDIN_STREAM(stream))
+ {
+ return 0;
+ }
+ else if (IS_STDOUT_STREAM(stream))
+ {
+ return 1;
+ }
+ else if (IS_STDERR_STREAM(stream))
+ {
+ return 2;
+ }
+ else
+ {
+ return fileno(stream);
+ }
+
+ return -1;
+ }
+
+ void dll_clearerr(FILE* stream)
+ {
+ if (g_emuFileWrapper.StreamIsEmulatedFile(stream))
+ {
+ // not implemented
+ }
+ }
+
+ char* dll_strdup( const char* str)
+ {
+ char* pdup;
+ pdup = strdup(str);
+ return pdup;
+ }
+
+ //Critical Section has been fixed in EMUkernel32.cpp
+
+ int dll_initterm(PFV* start, const PFV* end) //pncrt.dll
+ {
+ PFV * temp;
+ for (temp = start; temp < end; temp ++)
+ if (*temp)
+ (*temp)(); //call initial function table.
+ return 0;
+ }
+
+ //SLOW CODE SHOULD BE REVISED
+ int dll_stat(const char *path, struct stat *buffer)
+ {
+ if (!StringUtils::CompareNoCase(path, "shout://", 8)) // don't stat shoutcast
+ return -1;
+ if (!StringUtils::CompareNoCase(path, "mms://", 6)) // don't stat mms
+ return -1;
+
+#ifdef TARGET_POSIX
+ if (!StringUtils::CompareNoCase(path, "D:") || !StringUtils::CompareNoCase(path, "D:\\"))
+ {
+ buffer->st_mode = S_IFDIR;
+ return 0;
+ }
+#endif
+ if (!StringUtils::CompareNoCase(path, "\\Device\\Cdrom0") ||
+ !StringUtils::CompareNoCase(path, "\\Device\\Cdrom0\\"))
+ {
+ buffer->st_mode = _S_IFDIR;
+ return 0;
+ }
+
+ struct __stat64 tStat;
+ if (CFile::Stat(path, &tStat) == 0)
+ {
+ CUtil::Stat64ToStat(buffer, &tStat);
+ return 0;
+ }
+ // errno is set by file.Stat(...)
+ return -1;
+ }
+
+ int dll_stati64(const char *path, struct _stati64 *buffer)
+ {
+ struct __stat64 a;
+ memset(&a, 0, sizeof(a));
+
+ if(dll_stat64(path, &a) == 0)
+ {
+ CUtil::Stat64ToStatI64(buffer, &a);
+ return 0;
+ }
+ return -1;
+ }
+
+ int dll_stat64(const char *path, struct __stat64 *buffer)
+ {
+ if (!StringUtils::CompareNoCase(path, "shout://", 8)) // don't stat shoutcast
+ return -1;
+ if (!StringUtils::CompareNoCase(path, "mms://", 6)) // don't stat mms
+ return -1;
+
+#ifdef TARGET_POSIX
+ if (!StringUtils::CompareNoCase(path, "D:") || !StringUtils::CompareNoCase(path, "D:\\"))
+ {
+ buffer->st_mode = _S_IFDIR;
+ return 0;
+ }
+#endif
+ if (!StringUtils::CompareNoCase(path, "\\Device\\Cdrom0") ||
+ !StringUtils::CompareNoCase(path, "\\Device\\Cdrom0\\"))
+ {
+ buffer->st_mode = _S_IFDIR;
+ return 0;
+ }
+
+ return CFile::Stat(path, buffer);
+ }
+
+#ifdef TARGET_WINDOWS
+ int dll_stat64i32(const char *path, struct _stat64i32 *buffer)
+ {
+ struct __stat64 a;
+ if(dll_stat64(path, &a) == 0)
+ {
+ CUtil::Stat64ToStat64i32(buffer, &a);
+ return 0;
+ }
+ return -1;
+ }
+#endif
+
+ int dll_fstat(int fd, struct stat* buffer)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (pFile != NULL)
+ {
+ struct __stat64 tStat;
+ if (pFile->Stat(&tStat) == 0)
+ {
+ CUtil::Stat64ToStat(buffer, &tStat);
+ return 0;
+ }
+ }
+ else if (!IS_STD_DESCRIPTOR(fd))
+ {
+ return fstat(fd, buffer);
+ }
+
+ // fstat on stdin, stdout or stderr should fail
+ // this is what python expects
+ return -1;
+ }
+
+ int dll_fstati64(int fd, struct _stati64 *buffer)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (pFile != NULL)
+ {
+ CLog::Log(LOGINFO, "Stating open file");
+
+ buffer->st_size = pFile->GetLength();
+ buffer->st_mode = _S_IFREG;
+ return 0;
+ }
+ else if (!IS_STD_DESCRIPTOR(fd))
+ {
+ CLog::Log(LOGWARNING, "msvcrt.dll: dll_fstati64 called, TODO: add 'int64 <-> long' type checking"); //warning
+ // need to use fstat and convert everything
+ struct stat temp;
+ int res = fstat(fd, &temp);
+ if (res == 0)
+ {
+ CUtil::StatToStatI64(buffer, &temp);
+ }
+ return res;
+ }
+
+ // fstat on stdin, stdout or stderr should fail
+ // this is what python expects
+ return -1;
+ }
+
+#ifdef TARGET_WINDOWS
+ int dll_fstat64i32(int fd, struct _stat64i32 *buffer)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (pFile != NULL)
+ {
+ struct __stat64 tStat = {};
+ if (pFile->Stat(&tStat) == 0)
+ {
+ CUtil::Stat64ToStat64i32(buffer, &tStat);
+ return 0;
+ }
+ return -1;
+ }
+ else if (!IS_STD_DESCRIPTOR(fd))
+ {
+ CLog::Log(LOGWARNING, "msvcrt.dll: dll_fstati64 called, TODO: add 'int64 <-> long' type checking"); //warning
+ // need to use fstat and convert everything
+ struct __stat64 temp;
+ int res = _fstat64(fd, &temp);
+ if (res == 0)
+ {
+ CUtil::Stat64ToStat64i32(buffer, &temp);
+ }
+ return res;
+ }
+
+ // fstat on stdin, stdout or stderr should fail
+ // this is what python expects
+ return -1;
+ }
+#endif
+
+ int dll_setmode ( int handle, int mode )
+ {
+ not_implement("msvcrt.dll fake function dll_setmode() called\n");
+ return -1;
+ }
+
+ void dllperror(const char* s)
+ {
+ if (s)
+ {
+ CLog::Log(LOGERROR, "perror: {}", s);
+ }
+ }
+
+ char* dllstrerror(int iErr)
+ {
+ static char szError[32];
+ sprintf(szError, "err:%i", iErr);
+ return (char*)szError;
+ }
+
+ int dll_mkdir(const char* dir)
+ {
+ if (!dir) return -1;
+
+ // Make sure the slashes are correct & translate the path
+ std::string strPath = CUtil::ValidatePath(CSpecialProtocol::TranslatePath(dir));
+#ifndef TARGET_POSIX
+ std::wstring strWPath;
+ g_charsetConverter.utf8ToW(strPath, strWPath, false);
+ return _wmkdir(strWPath.c_str());
+#else
+ return mkdir(strPath.c_str(), 0755);
+#endif
+ }
+
+ const char* dll_getcwd(char *buffer, int maxlen)
+ {
+ not_implement("msvcrt.dll fake function dll_getcwd() called\n");
+ return "special://xbmc/";
+ }
+
+ int dll_putenv(const char* envstring)
+ {
+ bool added = false;
+
+ if (envstring != NULL)
+ {
+ const char *value_start = strchr(envstring, '=');
+
+ if (value_start != NULL)
+ {
+ char var[64];
+ int size = strlen(envstring) + 1;
+ char *value = (char*)malloc(size);
+
+ if (!value)
+ return -1;
+ value[0] = 0;
+
+ memcpy(var, envstring, value_start - envstring);
+ var[value_start - envstring] = 0;
+ char* temp = var;
+ while (*temp)
+ {
+ *temp = (char)toupper(*temp);
+ temp++;
+ }
+
+ strncpy(value, value_start + 1, size);
+ if (size)
+ value[size - 1] = '\0';
+
+ {
+ std::unique_lock<CCriticalSection> lock(dll_cs_environ);
+
+ char** free_position = NULL;
+ for (int i = 0; i < EMU_MAX_ENVIRONMENT_ITEMS && free_position == NULL; i++)
+ {
+ if (dll__environ[i] != NULL)
+ {
+ // we only support overwriting the old values
+ if (StringUtils::CompareNoCase(dll__environ[i], var, strlen(var)) == 0)
+ {
+ // free it first
+ free(dll__environ[i]);
+ dll__environ[i] = NULL;
+ free_position = &dll__environ[i];
+ }
+ }
+ else
+ {
+ free_position = &dll__environ[i];
+ }
+ }
+
+ if (free_position != NULL)
+ {
+ // free position, copy value
+ size = strlen(var) + strlen(value) + 2;
+ *free_position = (char*)malloc(size); // for '=' and 0 termination
+ if ((*free_position))
+ {
+ strncpy(*free_position, var, size);
+ (*free_position)[size - 1] = '\0';
+ strncat(*free_position, "=", size - strlen(*free_position));
+ strncat(*free_position, value, size - strlen(*free_position));
+ added = true;
+ }
+ }
+
+ }
+
+ free(value);
+ }
+ }
+
+ return added ? 0 : -1;
+ }
+
+ char* dll_getenv(const char* szKey)
+ {
+ char* value = NULL;
+
+ {
+ std::unique_lock<CCriticalSection> lock(dll_cs_environ);
+
+ update_emu_environ();//apply any changes
+
+ for (int i = 0; i < EMU_MAX_ENVIRONMENT_ITEMS && value == NULL; i++)
+ {
+ if (dll__environ[i])
+ {
+ if (StringUtils::CompareNoCase(dll__environ[i], szKey, strlen(szKey)) == 0)
+ {
+ // found it
+ value = dll__environ[i] + strlen(szKey) + 1;
+ }
+ }
+ }
+ }
+
+ if (value != NULL)
+ {
+ return value;
+ }
+
+ return NULL;
+ }
+
+ int dll_ctype(int i)
+ {
+ not_implement("msvcrt.dll fake function dll_ctype() called\n");
+ return 0;
+ }
+
+ int dll_system(const char *command)
+ {
+ not_implement("msvcrt.dll fake function dll_system() called\n");
+ return 0; //system(command);
+ }
+
+ void (__cdecl * dll_signal(int sig, void (__cdecl *func)(int)))(int)
+ {
+#if defined(TARGET_WINDOWS)
+ //vs2008 asserts for known signals, return err for everything unknown to windows.
+ if (sig == 5 || sig == 7 || sig == 9 || sig == 10 || sig == 12 || sig == 14 || sig == 18 || sig == 19 || sig == 20)
+ return SIG_ERR;
+#endif
+ return signal(sig, func);
+ }
+
+ int dll_getpid()
+ {
+ return 1;
+ }
+
+ int dll__commit(int fd)
+ {
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (pFile != NULL)
+ {
+ pFile->Flush();
+ return 0;
+ }
+ else if (!IS_STD_DESCRIPTOR(fd))
+ {
+ // it might be something else than a file, or the file is not emulated
+ // let the operating system handle it
+#ifndef TARGET_POSIX
+ return _commit(fd);
+#else
+ return fsync(fd);
+#endif
+ }
+
+ // std stream, no need to flush
+ return 0;
+ }
+
+ char*** dll___p__environ()
+ {
+ static char*** t = &dll__environ;
+ return (char***)&t;
+ }
+
+#ifdef TARGET_POSIX
+#if defined(TARGET_ANDROID)
+ volatile int * __cdecl dll_errno(void)
+ {
+ return &errno;
+ }
+#else
+ int * __cdecl dll_errno(void)
+ {
+ return &errno;
+ }
+#endif
+
+ int __cdecl dll_ioctl(int fd, unsigned long int request, va_list va)
+ {
+ int ret;
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByDescriptor(fd);
+ if (!pFile)
+ return -1;
+
+#if defined(HAS_DVD_DRIVE) && !defined(TARGET_FREEBSD)
+#if !defined(TARGET_DARWIN)
+ if(request == DVD_READ_STRUCT || request == DVD_AUTH)
+#else
+ if(request == DKIOCDVDSENDKEY || request == DKIOCDVDREPORTKEY || request == DKIOCDVDREADSTRUCTURE)
+#endif
+ {
+ void *p1 = va_arg(va, void*);
+ SNativeIoControl d;
+ d.request = request;
+ d.param = p1;
+ ret = pFile->IoControl(IOCTRL_NATIVE, &d);
+ if(ret<0)
+ CLog::Log(LOGWARNING, "{} - {} request failed with error [{}] {}", __FUNCTION__, request,
+ errno, strerror(errno));
+ }
+ else
+#endif
+ {
+ CLog::Log(LOGWARNING, "{} - Unknown request type {}", __FUNCTION__, request);
+ ret = -1;
+ }
+ return ret;
+ }
+#endif
+
+ int dll_setvbuf(FILE *stream, char *buf, int type, size_t size)
+ {
+ CLog::Log(LOGWARNING, "{} - May not be implemented correctly", __FUNCTION__);
+ return 0;
+ }
+
+ struct mntent *dll_getmntent(FILE *fp)
+ {
+ if (!fp)
+ return nullptr;
+
+#if defined(TARGET_LINUX) && !defined(TARGET_ANDROID)
+ struct mntent* mountPoint = getmntent(fp);
+ if (mountPoint)
+ return mountPoint;
+
+ // warn if this is a kodi vfs file not associated with a mountpoint
+ CFile* pFile = g_emuFileWrapper.GetFileXbmcByStream(fp);
+ if (pFile)
+ {
+ CLog::LogF(LOGWARNING, "getmntent is not implemented for our virtual filesystem");
+ }
+ return nullptr;
+#else
+ CLog::LogF(LOGWARNING, "Unimplemented function called");
+ return nullptr;
+#endif
+ }
+
+ struct mntent* dll_getmntent_r(FILE* fp, struct mntent* result, char* buffer, int bufsize)
+ {
+ if (!fp || !result || !buffer)
+ return nullptr;
+
+#if defined(TARGET_LINUX) && !defined(TARGET_ANDROID)
+ struct mntent* mountPoint = getmntent_r(fp, result, buffer, bufsize);
+ if (mountPoint)
+ return mountPoint;
+#endif
+ return nullptr;
+ }
+
+ // this needs to be wrapped, since dll's have their own file
+ // descriptor list, but we always use app's list with our wrappers
+ int __cdecl dll_open_osfhandle(intptr_t _OSFileHandle, int _Flags)
+ {
+#ifdef TARGET_WINDOWS
+ return _open_osfhandle(_OSFileHandle, _Flags);
+#else
+ return -1;
+#endif
+ }
+
+}
diff --git a/xbmc/cores/DllLoader/exports/emu_msvcrt.h b/xbmc/cores/DllLoader/exports/emu_msvcrt.h
new file mode 100644
index 0000000..cee233b
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/emu_msvcrt.h
@@ -0,0 +1,167 @@
+/*
+ * 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 <stdint.h>
+#include <stdio.h>
+
+#include "PlatformDefs.h"
+
+#ifdef TARGET_POSIX
+#define _onexit_t void*
+#endif
+
+#if defined(TARGET_DARWIN) || defined(TARGET_FREEBSD) || defined(TARGET_ANDROID)
+typedef off_t __off_t;
+typedef int64_t off64_t;
+typedef off64_t __off64_t;
+typedef fpos_t fpos64_t;
+#endif
+
+#ifdef TARGET_WINDOWS
+#include "platform/win32/dirent.h"
+#else
+#include <dirent.h>
+#endif
+
+typedef void ( *PFV)(void);
+
+#define __IS_STDIN_STREAM(stream) (stream == stdin || fileno(stream) == fileno(stdin) || fileno(stream) == 0)
+#define __IS_STDOUT_STREAM(stream) (stream == stdout || fileno(stream) == fileno(stdout) || fileno(stream) == 1)
+#define __IS_STDERR_STREAM(stream) (stream == stderr || fileno(stream) == fileno(stderr) || fileno(stream) == 2)
+#define IS_STDIN_STREAM(stream) (stream != NULL && __IS_STDIN_STREAM(stream))
+#define IS_STDOUT_STREAM(stream) (stream != NULL && __IS_STDOUT_STREAM(stream))
+#define IS_STDERR_STREAM(stream) (stream != NULL && __IS_STDERR_STREAM(stream))
+#define IS_VALID_STREAM(stream) (stream != nullptr)
+
+
+#define IS_STD_STREAM(stream) (stream != NULL && (__IS_STDIN_STREAM(stream) || __IS_STDOUT_STREAM(stream) || __IS_STDERR_STREAM(stream)))
+
+#define IS_STDIN_DESCRIPTOR(fd) (fd == 0)
+#define IS_STDOUT_DESCRIPTOR(fd) (fd == 1)
+#define IS_STDERR_DESCRIPTOR(fd) (fd == 2)
+
+#define IS_STD_DESCRIPTOR(fd) (IS_STDIN_DESCRIPTOR(fd) || IS_STDOUT_DESCRIPTOR(fd) || IS_STDERR_DESCRIPTOR(fd))
+
+
+extern "C"
+{
+ char* dll_strdup( const char* str);
+ void dll_sleep(unsigned long imSec);
+ void InitFiles();
+ void dllReleaseAll( );
+ void* dllmalloc(size_t size);
+ void dllfree( void* pPtr );
+ void* dllcalloc( size_t num, size_t size );
+ void* dllrealloc( void *memblock, size_t size );
+ void dllexit(int iCode);
+ void dllabort();
+ void* dll__dllonexit(PFV input, PFV ** start, PFV ** end);
+ _onexit_t dll_onexit(_onexit_t func);
+ int dllputs(const char* szLine);
+ int dll_putchar(int c);
+ int dll_putc(int c, FILE *stream);
+ int dllprintf( const char *format, ... );
+ int dllvprintf(const char *format, va_list va);
+ char *dll_fullpath(char *absPath, const char *relPath, size_t maxLength);
+ FILE *dll_popen(const char *command, const char *mode);
+ int dll_pclose(FILE *stream);
+ FILE* dll_fdopen(int i, const char* file);
+ int dll_open(const char* szFileName, int iMode);
+ int dll_read(int fd, void* buffer, unsigned int uiSize);
+ int dll_write(int fd, const void* buffer, unsigned int uiSize);
+ int dll_close(int fd);
+ __off64_t dll_lseeki64(int fd, __off64_t lPos, int iWhence);
+ __off_t dll_lseek(int fd, __off_t lPos, int iWhence);
+ char* dll_getenv(const char* szKey);
+ int dll_fclose (FILE * stream);
+#ifndef TARGET_POSIX
+ intptr_t dll_findfirst(const char *file, struct _finddata_t *data);
+ int dll_findnext(intptr_t f, _finddata_t* data);
+ int dll_findclose(intptr_t handle);
+ intptr_t dll_findfirst64i32(const char *file, struct _finddata64i32_t *data);
+ int dll_findnext64i32(intptr_t f, _finddata64i32_t* data);
+ void dll__security_error_handler(int code, void *data);
+#endif
+ DIR *dll_opendir(const char *filename);
+ struct dirent *dll_readdir(DIR *dirp);
+ int dll_closedir(DIR *dirp);
+ void dll_rewinddir(DIR *dirp);
+ char * dll_fgets (char* pszString, int num , FILE * stream);
+ int dll_fgetc (FILE* stream);
+ int dll_feof (FILE * stream);
+ int dll_fread (void * buffer, size_t size, size_t count, FILE * stream);
+ int dll_getc (FILE * stream);
+ FILE * dll_fopen(const char * filename, const char * mode);
+ int dll_fopen_s(FILE** pFile, const char * filename, const char * mode);
+ int dll_fputc (int character, FILE * stream);
+ int dll_putcchar (int character);
+ int dll_fputs (const char * szLine , FILE* stream);
+ int dll_fseek ( FILE * stream , long offset , int origin );
+ int dll_fseek64(FILE *stream, off64_t offset, int origin);
+ int dll_ungetc (int c, FILE * stream);
+ long dll_ftell(FILE *stream);
+ off64_t dll_ftell64(FILE *stream);
+ long dll_tell ( int fd );
+ long long dll_telli64 ( int fd );
+ size_t dll_fwrite ( const void * buffer, size_t size, size_t count, FILE * stream );
+ int dll_fflush (FILE * stream);
+ int dll_ferror (FILE * stream);
+ int dll_vfprintf(FILE *stream, const char *format, va_list va);
+ int dll_fprintf(FILE* stream , const char * format, ...);
+ int dll_fgetpos(FILE* stream, fpos_t* pos);
+ int dll_fgetpos64(FILE *stream, fpos64_t *pos);
+ int dll_fsetpos(FILE* stream, const fpos_t* pos);
+ int dll_fsetpos64(FILE* stream, const fpos64_t* pos);
+ int dll_fileno(FILE* stream);
+ void dll_rewind(FILE* stream);
+ void dll_clearerr(FILE* stream);
+ int dll_initterm(PFV* start, const PFV* end);
+ uintptr_t dll_beginthread(void( *start_address )( void * ),unsigned stack_size,void *arglist);
+ int dll_stati64(const char *path, struct _stati64 *buffer);
+ int dll_stat64(const char *path, struct __stat64 *buffer);
+#ifdef TARGET_WINDOWS
+ int dll_stat64i32(const char *path, struct _stat64i32 *buffer);
+#endif
+ int dll_stat(const char *path, struct stat *buffer);
+ int dll_fstat(int fd, struct stat *buffer);
+ int dll_fstati64(int fd, struct _stati64 *buffer);
+ int dll_setmode(int handle, int mode );
+ void dllperror(const char* s);
+ char* dllstrerror(int iErr);
+ int dll_mkdir(const char* dir);
+ const char* dll_getcwd(char *buffer, int maxlen);
+ int dll_putenv(const char* envstring);
+ int dll_ctype(int i);
+ int dll_system(const char *command);
+ void (__cdecl * dll_signal(int sig, void (__cdecl *func)(int)))(int);
+ int dll_getpid();
+ int dll__commit(int fd);
+ char*** dll___p__environ();
+ FILE* dll_freopen(const char *path, const char *mode, FILE *stream);
+ int dll_fscanf(FILE *stream, const char *format , ...);
+ void dll_flockfile(FILE *file);
+ int dll_ftrylockfile(FILE *file);
+ void dll_funlockfile(FILE *file);
+ int dll_fstat64(int fd, struct __stat64 *buf);
+#ifdef TARGET_WINDOWS
+ int dll_fstat64i32(int fd, struct _stat64i32 *buffer);
+ int dll_open_osfhandle(intptr_t _OSFileHandle, int _Flags);
+#endif
+ int dll_setvbuf(FILE *stream, char *buf, int type, size_t size);
+
+#if defined(TARGET_ANDROID)
+ volatile int * __cdecl dll_errno(void);
+#elif defined(TARGET_POSIX)
+ int * __cdecl dll_errno(void);
+#endif
+
+ extern char **dll__environ;
+}
+
diff --git a/xbmc/cores/DllLoader/exports/kernel32.def b/xbmc/cores/DllLoader/exports/kernel32.def
new file mode 100755
index 0000000..b23a9be
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/kernel32.def
@@ -0,0 +1,25 @@
+LIBRARY kernel32.dll
+
+VERSION 1.0
+
+EXPORTS
+ AddAtomA
+ FindAtomA
+ GetAtomNameA
+ CreateThread
+ FindClose
+ FindFirstFileA
+ FindNextFileA
+ GetFileAttributesA
+ GetLastError
+ GetModuleFileNameA
+ GetNumberOfConsoleInputEvents
+ GetStdHandle
+ ReadConsoleInputA
+ SetUnhandledExceptionFilter
+ Sleep
+ TerminateThread
+ GetCurrentThread
+ QueryPerformanceCounter
+ QueryPerformanceFrequency
+ SetThreadPriority \ No newline at end of file
diff --git a/xbmc/cores/DllLoader/exports/msvcrt.def b/xbmc/cores/DllLoader/exports/msvcrt.def
new file mode 100755
index 0000000..73a63b4
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/msvcrt.def
@@ -0,0 +1,123 @@
+LIBRARY msvcrt.dll
+
+VERSION 1.0
+
+EXPORTS
+ _close
+ _lseek
+ _read
+ _write
+ __dllonexit
+ __mb_cur_max
+ _assert
+ _errno
+ _ftime
+ _iob
+ _isctype
+ _lseeki64
+ _open
+ _snprintf
+ _stricmp
+ _strnicmp
+ _vsnprintf
+ abort
+ atof
+ atoi
+ cos
+ cosh
+ exp
+ fflush
+ floor
+ fprintf
+ free
+ frexp
+ fwrite
+ gmtime
+ ldexp
+ localtime
+ log
+ log10
+ malloc
+ memcpy
+ memmove
+ memset
+ mktime
+ perror
+ printf
+ putchar
+ puts
+ qsort
+ realloc
+ sin
+ sinh
+ sprintf
+ sqrt
+ sscanf
+ strchr
+ strcmp
+ strcpy
+ strlen
+ strncpy
+ strrchr
+ strtod
+ strtok
+ strtol
+ strtoul
+ tan
+ tanh
+ time
+ toupper
+ _memccpy
+ _fstat
+ _memccpy
+ _mkdir
+ _pclose
+ _popen
+ _sleep
+ _stat
+ _strdup
+ _swab
+ _findclose
+ _findfirst
+ _findnext
+ _fullpath
+ _pctype
+ calloc
+ ceil
+ ctime
+ exit
+ fclose
+ feof
+ fgets
+ fopen
+ fputc
+ fputs
+ fread
+ fseek
+ ftell
+ getc
+ getenv
+ putc
+ rand
+ remove
+ rewind
+ setlocale
+ signal
+ srand
+ strcat
+ strcoll
+ strerror
+ strncat
+ strncmp
+ strpbrk
+ strstr
+ tolower
+ strspn
+ strcspn
+ fsetpos
+ fgetpos
+ _fstati64
+ _stati64
+ _tell
+ _telli64
+ _setmode \ No newline at end of file
diff --git a/xbmc/cores/DllLoader/exports/pncrt.def b/xbmc/cores/DllLoader/exports/pncrt.def
new file mode 100755
index 0000000..ba34b22
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/pncrt.def
@@ -0,0 +1,114 @@
+LIBRARY msvcrt.dll
+
+VERSION 1.0
+
+EXPORTS
+ _close
+ _lseek
+ _read
+ _write
+ __dllonexit
+ __mb_cur_max
+ _assert
+ _errno
+ _ftime
+ _iob
+ _isctype
+ _lseeki64
+ _open
+ _snprintf
+ _stricmp
+ _strnicmp
+ _vsnprintf
+ abort
+ atof
+ atoi
+ cos
+ cosh
+ exp
+ fflush
+ floor
+ fprintf
+ free
+ frexp
+ fwrite
+ gmtime
+ ldexp
+ localtime
+ log
+ log10
+ malloc
+ memcpy
+ memmove
+ memset
+ mktime
+ perror
+ printf
+ putchar
+ puts
+ qsort
+ realloc
+ sin
+ sinh
+ sprintf
+ sqrt
+ sscanf
+ strchr
+ strcmp
+ strcpy
+ strlen
+ strncpy
+ strrchr
+ strtod
+ strtok
+ strtol
+ strtoul
+ tan
+ tanh
+ time
+ toupper
+ _memccpy
+ _fstat
+ _memccpy
+ _mkdir
+ _pclose
+ _popen
+ _sleep
+ _stat
+ _strdup
+ _swab
+ _findclose
+ _findfirst
+ _findnext
+ _fullpath
+ _pctype
+ calloc
+ ceil
+ ctime
+ exit
+ fclose
+ feof
+ fgets
+ fopen
+ fputc
+ fputs
+ fread
+ fseek
+ ftell
+ getc
+ getenv
+ putc
+ rand
+ remove
+ rewind
+ setlocale
+ signal
+ srand
+ strcat
+ strcoll
+ strerror
+ strncat
+ strncmp
+ strpbrk
+ strstr
+ tolower \ No newline at end of file
diff --git a/xbmc/cores/DllLoader/exports/util/CMakeLists.txt b/xbmc/cores/DllLoader/exports/util/CMakeLists.txt
new file mode 100644
index 0000000..3fa6f31
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/util/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES EmuFileWrapper.cpp)
+
+set(HEADERS EmuFileWrapper.h)
+
+core_add_library(exports_utils)
diff --git a/xbmc/cores/DllLoader/exports/util/EmuFileWrapper.cpp b/xbmc/cores/DllLoader/exports/util/EmuFileWrapper.cpp
new file mode 100644
index 0000000..40d6b61
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/util/EmuFileWrapper.cpp
@@ -0,0 +1,232 @@
+/*
+ * 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 "EmuFileWrapper.h"
+
+#include "filesystem/File.h"
+
+#include <mutex>
+
+CEmuFileWrapper g_emuFileWrapper;
+
+namespace
+{
+
+constexpr bool isValidFilePtr(const FILE* f)
+{
+ return (f != nullptr);
+}
+
+}
+CEmuFileWrapper::CEmuFileWrapper()
+{
+ // since we always use dlls we might just initialize it directly
+ for (EmuFileObject& file : m_files)
+ {
+ memset(&file, 0, sizeof(EmuFileObject));
+ file.used = false;
+ file.fd = -1;
+ }
+}
+
+CEmuFileWrapper::~CEmuFileWrapper()
+{
+ CleanUp();
+}
+
+void CEmuFileWrapper::CleanUp()
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ for (EmuFileObject& file : m_files)
+ {
+ if (file.used)
+ {
+ file.file_xbmc->Close();
+ delete file.file_xbmc;
+
+ if (file.file_lock)
+ {
+ delete file.file_lock;
+ file.file_lock = nullptr;
+ }
+ file.used = false;
+ file.fd = -1;
+ }
+ }
+}
+
+EmuFileObject* CEmuFileWrapper::RegisterFileObject(XFILE::CFile* pFile)
+{
+ EmuFileObject* object = nullptr;
+
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+
+ for (int i = 0; i < MAX_EMULATED_FILES; i++)
+ {
+ if (!m_files[i].used)
+ {
+ // found a free location
+ object = &m_files[i];
+ object->used = true;
+ object->file_xbmc = pFile;
+ object->fd = (i + FILE_WRAPPER_OFFSET);
+ object->file_lock = new CCriticalSection();
+ break;
+ }
+ }
+
+ return object;
+}
+
+void CEmuFileWrapper::UnRegisterFileObjectByDescriptor(int fd)
+{
+ int i = fd - FILE_WRAPPER_OFFSET;
+ if (! (i >= 0 && i < MAX_EMULATED_FILES))
+ return;
+
+ if (!m_files[i].used)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+
+ // we assume the emulated function already deleted the CFile object
+ if (m_files[i].file_lock)
+ {
+ delete m_files[i].file_lock;
+ m_files[i].file_lock = nullptr;
+ }
+ m_files[i].used = false;
+ m_files[i].fd = -1;
+}
+
+void CEmuFileWrapper::UnRegisterFileObjectByStream(FILE* stream)
+{
+ if (isValidFilePtr(stream))
+ {
+ EmuFileObject* o = reinterpret_cast<EmuFileObject*>(stream);
+ return UnRegisterFileObjectByDescriptor(o->fd);
+ }
+}
+
+void CEmuFileWrapper::LockFileObjectByDescriptor(int fd)
+{
+ int i = fd - FILE_WRAPPER_OFFSET;
+ if (i >= 0 && i < MAX_EMULATED_FILES)
+ {
+ if (m_files[i].used)
+ {
+ m_files[i].file_lock->lock();
+ }
+ }
+}
+
+bool CEmuFileWrapper::TryLockFileObjectByDescriptor(int fd)
+{
+ int i = fd - FILE_WRAPPER_OFFSET;
+ if (i >= 0 && i < MAX_EMULATED_FILES)
+ {
+ if (m_files[i].used)
+ {
+ return m_files[i].file_lock->try_lock();
+ }
+ }
+ return false;
+}
+
+void CEmuFileWrapper::UnlockFileObjectByDescriptor(int fd)
+{
+ int i = fd - FILE_WRAPPER_OFFSET;
+ if (i >= 0 && i < MAX_EMULATED_FILES)
+ {
+ if (m_files[i].used)
+ {
+ m_files[i].file_lock->unlock();
+ }
+ }
+}
+
+EmuFileObject* CEmuFileWrapper::GetFileObjectByDescriptor(int fd)
+{
+ int i = fd - FILE_WRAPPER_OFFSET;
+ if (i >= 0 && i < MAX_EMULATED_FILES)
+ {
+ if (m_files[i].used)
+ {
+ return &m_files[i];
+ }
+ }
+ return nullptr;
+}
+
+EmuFileObject* CEmuFileWrapper::GetFileObjectByStream(FILE* stream)
+{
+ if (isValidFilePtr(stream))
+ {
+ EmuFileObject* o = reinterpret_cast<EmuFileObject*>(stream);
+ return GetFileObjectByDescriptor(o->fd);
+ }
+
+ return nullptr;
+}
+
+XFILE::CFile* CEmuFileWrapper::GetFileXbmcByDescriptor(int fd)
+{
+ auto object = GetFileObjectByDescriptor(fd);
+ if (object != nullptr && object->used)
+ {
+ return object->file_xbmc;
+ }
+ return nullptr;
+}
+
+XFILE::CFile* CEmuFileWrapper::GetFileXbmcByStream(FILE* stream)
+{
+ if (isValidFilePtr(stream))
+ {
+ EmuFileObject* object = reinterpret_cast<EmuFileObject*>(stream);
+ if (object != nullptr && object->used)
+ {
+ return object->file_xbmc;
+ }
+ }
+ return nullptr;
+}
+
+int CEmuFileWrapper::GetDescriptorByStream(FILE* stream)
+{
+ if (isValidFilePtr(stream))
+ {
+ EmuFileObject* obj = reinterpret_cast<EmuFileObject*>(stream);
+ int i = obj->fd - FILE_WRAPPER_OFFSET;
+ if (i >= 0 && i < MAX_EMULATED_FILES)
+ {
+ return i + FILE_WRAPPER_OFFSET;
+ }
+ }
+ return -1;
+}
+
+FILE* CEmuFileWrapper::GetStreamByDescriptor(int fd)
+{
+ auto object = GetFileObjectByDescriptor(fd);
+ if (object != nullptr && object->used)
+ {
+ return reinterpret_cast<FILE*>(object);
+ }
+ return nullptr;
+}
+
+bool CEmuFileWrapper::StreamIsEmulatedFile(FILE* stream)
+{
+ if (isValidFilePtr(stream))
+ {
+ EmuFileObject* obj = reinterpret_cast<EmuFileObject*>(stream);
+ return DescriptorIsEmulatedFile(obj->fd);
+ }
+ return false;
+}
diff --git a/xbmc/cores/DllLoader/exports/util/EmuFileWrapper.h b/xbmc/cores/DllLoader/exports/util/EmuFileWrapper.h
new file mode 100644
index 0000000..a8a70fa
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/util/EmuFileWrapper.h
@@ -0,0 +1,73 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+#include <stdio.h>
+
+#if defined(TARGET_POSIX) && !defined(TARGET_DARWIN) && !defined(TARGET_FREEBSD) && !defined(TARGET_ANDROID) && !defined(__UCLIBC__)
+#define _file _fileno
+#elif defined(__UCLIBC__)
+#define _file __filedes
+#endif
+
+#define MAX_EMULATED_FILES 50
+#define FILE_WRAPPER_OFFSET 0x00000200
+
+namespace XFILE
+{
+ class CFile;
+}
+
+typedef struct stEmuFileObject
+{
+ XFILE::CFile* file_xbmc;
+ CCriticalSection *file_lock;
+ int mode;
+ //Stick this last to avoid 3-7 bytes of padding
+ bool used;
+ int fd;
+} EmuFileObject;
+
+class CEmuFileWrapper
+{
+public:
+ CEmuFileWrapper();
+ ~CEmuFileWrapper();
+
+ /**
+ * Only to be called when shutting down xbmc
+ */
+ void CleanUp();
+
+ EmuFileObject* RegisterFileObject(XFILE::CFile* pFile);
+ void UnRegisterFileObjectByDescriptor(int fd);
+ void UnRegisterFileObjectByStream(FILE* stream);
+ void LockFileObjectByDescriptor(int fd);
+ bool TryLockFileObjectByDescriptor(int fd);
+ void UnlockFileObjectByDescriptor(int fd);
+ EmuFileObject* GetFileObjectByDescriptor(int fd);
+ EmuFileObject* GetFileObjectByStream(FILE* stream);
+ XFILE::CFile* GetFileXbmcByDescriptor(int fd);
+ XFILE::CFile* GetFileXbmcByStream(FILE* stream);
+ static int GetDescriptorByStream(FILE* stream);
+ FILE* GetStreamByDescriptor(int fd);
+ static constexpr bool DescriptorIsEmulatedFile(int fd)
+ {
+ return fd >= FILE_WRAPPER_OFFSET && fd < FILE_WRAPPER_OFFSET + MAX_EMULATED_FILES;
+ }
+ static bool StreamIsEmulatedFile(FILE* stream);
+private:
+ EmuFileObject m_files[MAX_EMULATED_FILES];
+ CCriticalSection m_criticalSection;
+};
+
+extern CEmuFileWrapper g_emuFileWrapper;
+
diff --git a/xbmc/cores/DllLoader/exports/winMM.def b/xbmc/cores/DllLoader/exports/winMM.def
new file mode 100755
index 0000000..c389b3d
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/winMM.def
@@ -0,0 +1,6 @@
+LIBRARY WINMM.DLL
+
+VERSION 1.0
+
+EXPORTS
+ timeGetTime \ No newline at end of file
diff --git a/xbmc/cores/DllLoader/exports/wrapper.c b/xbmc/cores/DllLoader/exports/wrapper.c
new file mode 100644
index 0000000..455ec0b
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/wrapper.c
@@ -0,0 +1,513 @@
+/*
+ * 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.
+ */
+
+//
+// To recreate wrapper.def:
+//
+// bash# (echo -n "-Wl"; grep __wrap wrapper.c | grep -v bash | sed "s/.*__wrap_//g" | sed "s/(.*//g" | awk '{printf(",-wrap,%s",$0);}') > wrapper.def
+//
+#include <sys/types.h>
+#include <sys/stat.h>
+#if !defined(TARGET_ANDROID)
+#include <sys/statvfs.h>
+#endif
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdarg.h>
+#include <dirent.h>
+#include <dlfcn.h>
+
+#if defined(TARGET_DARWIN) || defined(TARGET_FREEBSD) || defined(TARGET_ANDROID)
+typedef off_t __off_t;
+typedef int64_t off64_t;
+typedef off64_t __off64_t;
+typedef fpos_t fpos64_t;
+#define stat64 stat
+#endif
+
+#ifdef TARGET_POSIX
+#define _stat stat
+#endif
+
+struct mntent;
+
+void* dllmalloc(size_t );
+void* dllcalloc( size_t , size_t );
+void* dllrealloc(void*, size_t);
+void dllfree(void*);
+
+int dll_open(const char* szFileName, int iMode);
+int dll_write(int fd, const void* buffer, unsigned int uiSize);
+int dll_read(int fd, void* buffer, unsigned int uiSize);
+off_t dll_lseek(int fd, __off_t lPos, int iWhence);
+__off64_t dll_lseeki64(int fd, __off64_t lPos, int iWhence);
+int dll_close(int fd);
+
+FILE * dll_fopen (const char * filename, const char * mode);
+FILE* dll_freopen(const char *path, const char *mode, FILE *stream);
+FILE* dll_fdopen(int i, const char* file);
+int dll_fclose (FILE * stream);
+int dll_ferror (FILE * stream);
+int dll_feof (FILE * stream);
+int dll_fileno(FILE* stream);
+void dll_clearerr(FILE* stream);
+int dll_fread (void * buffer, size_t size, size_t count, FILE * stream);
+size_t dll_fwrite ( const void * buffer, size_t size, size_t count, FILE * stream );
+int dll_fflush (FILE * stream);
+int dll_fputc (int character, FILE * stream);
+int dll_fputs (const char * szLine , FILE* stream);
+int dll_putc(int c, FILE *stream);
+int dll_fseek ( FILE * stream , long offset , int origin );
+int dll_fseek64(FILE *stream, off64_t offset, int origin);
+long dll_ftell(FILE *stream);
+off64_t dll_ftell64(FILE *stream);
+void dll_rewind(FILE* stream);
+int dll_fgetpos(FILE* stream, fpos_t* pos);
+int dll_fgetpos64(FILE *stream, fpos64_t *pos);
+int dll_fsetpos(FILE* stream, const fpos_t* pos);
+int dll_fsetpos64(FILE* stream, const fpos64_t* pos);
+DIR* dll_opendir(const char* name);
+struct dirent* dll_readdir(DIR* dirp);
+int dll_closedir(DIR* dirp);
+void dll_rewinddir(DIR* dirp);
+int dll_fprintf(FILE* stream , const char * format, ...);
+int dllprintf(const char *format, ...);
+int dll_vfprintf(FILE *stream, const char *format, va_list va);
+int dll_fgetc (FILE* stream);
+char * dll_fgets (char* pszString, int num , FILE * stream);
+int dll_getc (FILE * stream);
+int dll_ungetc (int c, FILE * stream);
+int dll_ioctl(int d, unsigned long int request, va_list va);
+int dll_stat(const char *path, struct _stat *buffer);
+int dll_stat64(const char *path, struct stat64 *buffer);
+void dll_flockfile(FILE *file);
+int dll_ftrylockfile(FILE *file);
+void dll_funlockfile(FILE *file);
+int dll_fstat64(int fd, struct stat64 *buf);
+int dll_fstat(int fd, struct _stat *buf);
+FILE* dll_popen(const char *command, const char *mode);
+void* dll_dlopen(const char *filename, int flag);
+int dll_setvbuf(FILE *stream, char *buf, int type, size_t size);
+struct mntent *dll_getmntent(FILE *fp);
+struct mntent* dll_getmntent_r(FILE* fp, struct mntent* result, char* buffer, int bufsize);
+
+void *__wrap_dlopen(const char *filename, int flag)
+{
+ return dlopen(filename, flag);
+}
+
+FILE *__wrap_popen(const char *command, const char *mode)
+{
+ return dll_popen(command, mode);
+}
+
+void* __wrap_calloc( size_t num, size_t size )
+{
+ return dllcalloc(num, size);
+}
+
+void* __wrap_malloc(size_t size)
+{
+ return dllmalloc(size);
+}
+
+void* __wrap_realloc( void *memblock, size_t size )
+{
+ return dllrealloc(memblock, size);
+}
+
+void __wrap_free( void* pPtr )
+{
+ dllfree(pPtr);
+}
+
+int __wrap_open(const char *file, int oflag, ...)
+{
+ return dll_open(file, oflag);
+}
+
+int __wrap_open64(const char *file, int oflag, ...)
+{
+ return dll_open(file, oflag);
+}
+
+int __wrap_close(int fd)
+{
+ return dll_close(fd);
+}
+
+ssize_t __wrap_write(int fd, const void *buf, size_t count)
+{
+ return dll_write(fd, buf, count);
+}
+
+ssize_t __wrap_read(int fd, void *buf, size_t count)
+{
+ return dll_read(fd, buf, count);
+}
+
+__off_t __wrap_lseek(int filedes, __off_t offset, int whence)
+{
+ return dll_lseek(filedes, offset, whence);
+}
+
+__off64_t __wrap_lseek64(int filedes, __off64_t offset, int whence)
+{
+ __off64_t seekRes = dll_lseeki64(filedes, offset, whence);
+ return seekRes;
+}
+
+int __wrap_fclose(FILE *fp)
+{
+ return dll_fclose(fp);
+}
+
+int __wrap_ferror(FILE *stream)
+{
+ return dll_ferror(stream);
+}
+
+void __wrap_clearerr(FILE *stream)
+{
+ dll_clearerr(stream);
+}
+
+int __wrap_feof(FILE *stream)
+{
+ return dll_feof(stream);
+}
+
+int __wrap_fileno(FILE *stream)
+{
+ return dll_fileno(stream);
+}
+
+FILE *__wrap_fopen(const char *path, const char *mode)
+{
+ return dll_fopen(path, mode);
+}
+
+FILE *__wrap_fopen64(const char *path, const char *mode)
+{
+ return dll_fopen(path, mode);
+}
+
+FILE *__wrap_fdopen(int filedes, const char *mode)
+{
+ return dll_fdopen(filedes, mode);
+}
+
+FILE *__wrap_freopen(const char *path, const char *mode, FILE *stream)
+{
+ return dll_freopen(path, mode, stream);
+}
+
+size_t __wrap_fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
+{
+ return dll_fread(ptr, size, nmemb, stream);
+}
+
+size_t __wrap_fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
+{
+ return dll_fwrite(ptr, size, nmemb, stream);
+}
+
+int __wrap_fflush(FILE *stream)
+{
+ return dll_fflush(stream);
+}
+
+int __wrap_fputc(int c, FILE *stream)
+{
+ return dll_fputc(c, stream);
+}
+
+int __wrap_fputs(const char *s, FILE *stream)
+{
+ return dll_fputs(s, stream);
+}
+
+int __wrap__IO_putc(int c, FILE *stream)
+{
+ return dll_putc(c, stream);
+}
+
+int __wrap_fseek(FILE *stream, long offset, int whence)
+{
+ return dll_fseek(stream, offset, whence);
+}
+
+int __wrap_fseeko64(FILE *stream, off64_t offset, int whence)
+{
+ return dll_fseek64(stream, offset, whence);
+}
+
+long __wrap_ftell(FILE *stream)
+{
+ return dll_ftell(stream);
+}
+
+off64_t __wrap_ftello64(FILE *stream)
+{
+ return dll_ftell64(stream);
+}
+
+void __wrap_rewind(FILE *stream)
+{
+ dll_rewind(stream);
+}
+
+int __wrap_fgetpos(FILE *stream, fpos_t *pos)
+{
+ return dll_fgetpos(stream, pos);
+}
+
+int __wrap_fgetpos64(FILE *stream, fpos64_t *pos)
+{
+ return dll_fgetpos64(stream, pos);
+}
+
+int __wrap_fsetpos(FILE *stream, fpos_t *pos)
+{
+ return dll_fsetpos(stream, pos);
+}
+
+int __wrap_fsetpos64(FILE *stream, fpos64_t *pos)
+{
+ return dll_fsetpos64(stream, pos);
+}
+
+DIR * __wrap_opendir(const char *name)
+{
+ return dll_opendir(name);
+}
+
+struct dirent * __wrap_readdir(DIR* dirp)
+{
+ return dll_readdir(dirp);
+}
+
+struct dirent * __wrap_readdir64(DIR* dirp)
+{
+ return dll_readdir(dirp);
+}
+
+int __wrap_closedir(DIR* dirp)
+{
+ return dll_closedir(dirp);
+}
+
+void __wrap_rewinddir(DIR* dirp)
+{
+ dll_rewinddir(dirp);
+}
+
+int __wrap_fprintf(FILE *stream, const char *format, ...)
+{
+ int res;
+ va_list va;
+ va_start(va, format);
+ res = dll_vfprintf(stream, format, va);
+ va_end(va);
+ return res;
+}
+
+int __wrap_vfprintf(FILE *stream, const char *format, va_list ap)
+{
+ return dll_vfprintf(stream, format, ap);
+}
+
+int __wrap_printf(const char *format, ...)
+{
+ int res;
+ va_list va;
+ va_start(va, format);
+ res = dll_vfprintf(stdout, format, va);
+ va_end(va);
+ return res;
+}
+
+int __wrap_fgetc(FILE *stream)
+{
+ return dll_fgetc(stream);
+}
+
+char *__wrap_fgets(char *s, int size, FILE *stream)
+{
+ return dll_fgets(s, size, stream);
+}
+
+int __wrap__IO_getc(FILE *stream)
+{
+ return dll_getc(stream);
+}
+
+int __wrap__IO_getc_unlocked(FILE *stream)
+{
+ return dll_getc(stream);
+}
+
+int __wrap_getc_unlocked(FILE *stream)
+{
+ return dll_getc(stream);
+}
+
+int __wrap_ungetc(int c, FILE *stream)
+{
+ return dll_ungetc(c, stream);
+}
+
+int __wrap_getc(FILE *stream)
+{
+ return dll_getc(stream);
+}
+
+int __wrap_ioctl(int d, unsigned long int request, ...)
+{
+ int res;
+ va_list va;
+ va_start(va, request);
+ res = dll_ioctl(d, request, va);
+ va_end(va);
+ return res;
+}
+
+int __wrap__stat(const char *path, struct _stat *buffer)
+{
+ return dll_stat(path, buffer);
+}
+
+int __wrap_stat(const char *path, struct _stat *buffer)
+{
+ return dll_stat(path, buffer);
+}
+
+int __wrap_stat64(const char* path, struct stat64* buffer)
+{
+ return dll_stat64(path, buffer);
+}
+
+int __wrap___xstat(int __ver, const char *__filename, struct stat *__stat_buf)
+{
+ return dll_stat(__filename, __stat_buf);
+}
+
+int __wrap___xstat64(int __ver, const char *__filename, struct stat64 *__stat_buf)
+{
+ return dll_stat64(__filename, __stat_buf);
+}
+
+int __wrap___lxstat64(int __ver, const char *__filename, struct stat64 *__stat_buf)
+{
+ return dll_stat64(__filename, __stat_buf);
+}
+
+void __wrap_flockfile(FILE *file)
+{
+ dll_flockfile(file);
+}
+
+int __wrap_ftrylockfile(FILE *file)
+{
+ return dll_ftrylockfile(file);
+}
+
+void __wrap_funlockfile(FILE *file)
+{
+ dll_funlockfile(file);
+}
+
+int __wrap___fxstat64(int ver, int fd, struct stat64 *buf)
+{
+ return dll_fstat64(fd, buf);
+}
+
+int __wrap___fxstat(int ver, int fd, struct stat *buf)
+{
+ return dll_fstat(fd, buf);
+}
+
+int __wrap_fstat(int fd, struct _stat *buf)
+{
+ return dll_fstat(fd, buf);
+}
+
+int __wrap_fstat64(int fd, struct stat64* buf)
+{
+ return dll_fstat64(fd, buf);
+}
+
+int __wrap_setvbuf(FILE *stream, char *buf, int type, size_t size)
+{
+ return dll_setvbuf(stream, buf, type, size);
+}
+
+struct mntent *__wrap_getmntent(FILE *fp)
+{
+#ifdef TARGET_POSIX
+ return dll_getmntent(fp);
+#endif
+ return NULL;
+}
+
+struct mntent* __wrap_getmntent_r(FILE* fp, struct mntent* result, char* buffer, int bufsize)
+{
+#ifdef TARGET_POSIX
+ return dll_getmntent_r(fp, result, buffer, bufsize);
+#endif
+ return NULL;
+}
+
+// GCC 4.3 in Ubuntu 8.10 defines _FORTIFY_SOURCE=2 which means, that fread, read etc
+// are actually #defines which are inlined when compiled with -O. Those defines
+// actually call __*chk (for example, __fread_chk). We need to bypass this whole
+// thing to actually call our wrapped functions.
+#if _FORTIFY_SOURCE > 1
+
+size_t __wrap___fread_chk(void * ptr, size_t ptrlen, size_t size, size_t n, FILE * stream)
+{
+ return dll_fread(ptr, size, n, stream);
+}
+
+int __wrap___printf_chk(int flag, const char *format, ...)
+{
+ int res;
+ va_list va;
+ va_start(va, format);
+ res = dll_vfprintf(stdout, format, va);
+ va_end(va);
+ return res;
+}
+
+int __wrap___vfprintf_chk(FILE* stream, int flag, const char *format, va_list ap)
+{
+ return dll_vfprintf(stream, format, ap);
+}
+
+int __wrap___fprintf_chk(FILE * stream, int flag, const char *format, ...)
+{
+ int res;
+ va_list va;
+ va_start(va, format);
+ res = dll_vfprintf(stream, format, va);
+ va_end(va);
+ return res;
+}
+
+char *__wrap___fgets_chk(char *s, size_t size, int n, FILE *stream)
+{
+ return dll_fgets(s, n, stream);
+}
+
+size_t __wrap___read_chk(int fd, void *buf, size_t nbytes, size_t buflen)
+{
+ return dll_read(fd, buf, nbytes);
+}
+
+#endif
diff --git a/xbmc/cores/DllLoader/exports/wrapper_mach_alias b/xbmc/cores/DllLoader/exports/wrapper_mach_alias
new file mode 100644
index 0000000..88ff675
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/wrapper_mach_alias
@@ -0,0 +1,62 @@
+# List of wrapper aliases for Mach-O
+___wrap_clearerr _clearerr
+___wrap_close _close
+___wrap_fclose _fclose
+___wrap_fdopen _fdopen
+___wrap_feof _feof
+___wrap_ferror _ferror
+___wrap_fflush _fflush
+___wrap_fgetc _fgetc
+___wrap_fgetpos _fgetpos
+___wrap_fgets _fgets
+___wrap_fileno _fileno
+___wrap_flockfile _flockfile
+___wrap_fopen _fopen
+___wrap_fopen64 _fopen64
+___wrap_fprintf _fprintf
+___wrap_fputc _fputc
+___wrap_fputs _fputs
+___wrap_fread _fread
+___wrap_freopen _freopen
+___wrap_fseek _fseek
+___wrap_fsetpos _fsetpos
+___wrap_fstatvfs64 _fstatvfs64
+___wrap_ftell _ftell
+___wrap_ftrylockfile _ftrylockfile
+___wrap_funlockfile _funlockfile
+___wrap_fwrite _fwrite
+___wrap_ioctl _ioctl
+___wrap_lseek _lseek
+___wrap_lseek64 _lseek64
+___wrap_open _open
+___wrap_open64 _open64
+___wrap_popen _popen
+___wrap_printf _printf
+___wrap_read _read
+___wrap_opendir _opendir
+___wrap_readdir _readdir
+___wrap_closedir _closedir
+___wrap_rewinddir _rewinddir
+___wrap_rewind _rewind
+___wrap__stat _stat
+___wrap_fstat _fstat
+___wrap_ungetc _ungetc
+___wrap_vfprintf _vfprintf
+___wrap_write _write
+___wrap__IO_putc _putc
+___wrap__IO_getc _getc
+___wrap__IO_getc_unlocked _getc_unlocked
+___wrap_fwrite _fwrite$UNIX2003
+___wrap_close _close$UNIX2003
+#___wrap_fcntl _fcntl$UNIX2003
+___wrap_open _open$UNIX2003
+___wrap_fopen _fopen$UNIX2003
+___wrap_fputs _fputs$UNIX2003
+___wrap_read _read$UNIX2003
+___wrap_write _write$UNIX2003
+___wrap_fstat _fstat$INODE64
+___wrap__stat _stat$INODE64
+___wrap_opendir _opendir$INODE64$UNIX2003
+___wrap_closedir _closedir$UNIX2003
+___wrap_readdir _readdir$INODE64
+___wrap_opendir _opendir$INODE64
diff --git a/xbmc/cores/DllLoader/exports/ws2_32.def b/xbmc/cores/DllLoader/exports/ws2_32.def
new file mode 100755
index 0000000..ac7712e
--- /dev/null
+++ b/xbmc/cores/DllLoader/exports/ws2_32.def
@@ -0,0 +1,25 @@
+LIBRARY WS2_32.DLL
+
+VERSION 1.0
+
+EXPORTS
+ WSACleanup
+ WSAGetLastError
+ WSAStartup
+ bind
+ closesocket
+ connect
+ gethostbyname
+ getsockopt
+ htonl
+ htons
+ inet_addr
+ inet_ntoa
+ ioctlsocket
+ ntohl
+ recv
+ select
+ send
+ sendto
+ setsockopt
+ socket \ No newline at end of file
diff --git a/xbmc/cores/DllLoader/ldt_keeper.c b/xbmc/cores/DllLoader/ldt_keeper.c
new file mode 100644
index 0000000..325d50c
--- /dev/null
+++ b/xbmc/cores/DllLoader/ldt_keeper.c
@@ -0,0 +1,282 @@
+/**
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ * This file MUST be in main library because LDT must
+ * be modified before program creates first thread
+ * - avifile includes this file from C++ code
+ * and initializes it at the start of player!
+ * it might sound like a hack and it really is - but
+ * as aviplay is decoding video with more than just one
+ * thread currently it's necessary to do it this way
+ * this might change in the future
+ */
+
+/* applied some modification to make make our xine friend more happy */
+
+/*
+ * Modified for use with MPlayer, detailed changelog at
+ * http://svn.mplayerhq.hu/mplayer/trunk/
+ * $Id: ldt_keeper.c 22733 2007-03-18 22:18:11Z nicodvb $
+ */
+
+// clang-format off
+#if !defined(__aarch64__) && \
+ !defined(__alpha__) &&\
+ !defined(__arc__) &&\
+ !defined(__arm__) && \
+ !defined(__loongarch__) && \
+ !defined(__mips__) && \
+ !defined(__or1k__) && \
+ !defined(__powerpc__) && \
+ !defined(__ppc__) && \
+ !defined(__riscv) && \
+ !defined(__SH4__) && \
+ !defined(__s390x__) && \
+ !defined(__sparc__) && \
+ !defined(__xtensa__)
+// clang-format on
+
+#include "ldt_keeper.h"
+
+#include <string.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <stdio.h>
+#include <unistd.h>
+#include "mmap_anon.h"
+#if defined( __linux__ ) && !defined(__powerpc__)
+#include <asm/unistd.h>
+#include <asm/ldt.h>
+/* prototype it here, so we won't depend on kernel headers */
+#ifdef __cplusplus
+extern "C" {
+#endif
+#if defined(TARGET_ANDROID) && (defined(__i386__) || defined(__x86_64__)) && !defined(modify_ldt)
+#define modify_ldt(a,b,c) syscall( __NR_modify_ldt, a, b, c);
+#else
+int modify_ldt(int func, void *ptr, unsigned long bytecount);
+#endif
+#ifdef __cplusplus
+}
+#endif
+#else
+#if defined(__NetBSD__) || defined(TARGET_FREEBSD) || defined(__OpenBSD__) || defined(__DragonFly__)
+#include <machine/segments.h>
+#include <machine/sysarch.h>
+#endif
+#if defined(TARGET_DARWIN)
+#include <i386/user_ldt.h>
+#endif
+
+#ifdef __svr4__
+#include <sys/segment.h>
+#include <sys/sysi86.h>
+
+/* solaris x86: add missing prototype for sysi86(), but only when sysi86(int, void*) is known to be valid */
+#ifdef HAVE_SYSI86_iv
+#ifdef __cplusplus
+extern "C" {
+#endif
+int sysi86(int, void*);
+#ifdef __cplusplus
+}
+#endif
+#endif
+
+#ifndef NUMSYSLDTS /* SunOS 2.5.1 does not define NUMSYSLDTS */
+#define NUMSYSLDTS 6 /* Let's hope the SunOS 5.8 value is OK */
+#endif
+
+#define TEB_SEL_IDX NUMSYSLDTS
+#endif
+
+#define LDT_ENTRIES 8192
+#define LDT_ENTRY_SIZE 8
+#pragma pack(4)
+struct user_desc {
+ unsigned int entry_number;
+ unsigned long base_addr;
+ unsigned int limit;
+ unsigned int seg_32bit:1;
+ unsigned int contents:2;
+ unsigned int read_exec_only:1;
+ unsigned int limit_in_pages:1;
+ unsigned int seg_not_present:1;
+};
+
+#define MODIFY_LDT_CONTENTS_DATA 0
+#define MODIFY_LDT_CONTENTS_STACK 1
+#define MODIFY_LDT_CONTENTS_CODE 2
+#endif
+
+
+/* user level (privilege level: 3) ldt (1<<2) segment selector */
+#define LDT_SEL(idx) ((idx) << 3 | 1 << 2 | 3)
+
+/* i got this value from wine sources, it's the first free LDT entry */
+#if (defined(TARGET_DARWIN) || defined(TARGET_FREEBSD)) && defined(LDT_AUTO_ALLOC)
+#define TEB_SEL_IDX LDT_AUTO_ALLOC
+#define USE_LDT_AA
+#endif
+
+#ifndef TEB_SEL_IDX
+#define TEB_SEL_IDX 17
+#endif
+
+static unsigned int fs_ldt = TEB_SEL_IDX;
+
+
+/**
+ * here is a small logical problem with Restore for multithreaded programs -
+ * in C++ we use static class for this...
+ */
+
+#ifdef __cplusplus
+extern "C"
+#endif
+void Setup_FS_Segment(void)
+{
+ unsigned int ldt_desc = LDT_SEL(fs_ldt);
+
+ __asm__ __volatile__(
+ "movl %0,%%eax; movw %%ax, %%fs" : : "r" (ldt_desc)
+ :"eax"
+ );
+}
+
+#if defined(__NetBSD__) || defined(TARGET_FREEBSD) || defined(__OpenBSD__) || defined(__DragonFly__) || defined(TARGET_DARWIN)
+static void LDT_EntryToBytes( unsigned long *buffer, const struct user_desc *content )
+{
+ *buffer++ = ((content->base_addr & 0x0000ffff) << 16) |
+ (content->limit & 0x0ffff);
+ *buffer = (content->base_addr & 0xff000000) |
+ ((content->base_addr & 0x00ff0000)>>16) |
+ (content->limit & 0xf0000) |
+ (content->contents << 10) |
+ ((content->read_exec_only == 0) << 9) |
+ ((content->seg_32bit != 0) << 22) |
+ ((content->limit_in_pages != 0) << 23) |
+ 0xf000;
+}
+#endif
+
+void* fs_seg=0;
+
+ldt_fs_t* Setup_LDT_Keeper(void)
+{
+ struct user_desc array;
+ int ret;
+ int sret;
+ ldt_fs_t* ldt_fs = (ldt_fs_t*) malloc(sizeof(ldt_fs_t));
+
+ if (!ldt_fs)
+ return NULL;
+
+#if defined(TARGET_DARWIN)
+ if (getenv("DYLD_BIND_AT_LAUNCH") == NULL)
+ printf("DYLD_BIND_AT_LAUNCH");
+#endif // TARGET_DARWIN
+
+ sret = sysconf(_SC_PAGE_SIZE);
+ if (sret == -1)
+ {
+ perror("ERROR: Couldn't allocate memory for fs segment");
+ free(ldt_fs);
+ return NULL;
+ }
+
+ fs_seg = ldt_fs->fs_seg = mmap_anon(NULL, sret, PROT_READ | PROT_WRITE, MAP_PRIVATE, 0);
+ if (ldt_fs->fs_seg == (void*)-1)
+ {
+ perror("ERROR: Couldn't allocate memory for fs segment");
+ free(ldt_fs);
+ return NULL;
+ }
+ *(void**)((char*)ldt_fs->fs_seg + 0x18) = ldt_fs->fs_seg;
+ memset(&array, 0, sizeof(array));
+ array.base_addr = (long)ldt_fs->fs_seg;
+ array.entry_number = TEB_SEL_IDX;
+ array.limit = array.base_addr+sysconf(_SC_PAGE_SIZE) - 1;
+ array.seg_32bit = 1;
+ array.read_exec_only = 0;
+ array.seg_not_present = 0;
+ array.contents=MODIFY_LDT_CONTENTS_DATA;
+ array.limit_in_pages = 0;
+#ifdef __linux__
+ /* ret=LDT_Modify(0x1, &array, sizeof(struct user_desc)); */
+ ret = modify_ldt(0x1, &array, sizeof(struct user_desc));
+ if (ret < 0)
+ {
+ perror("install_fs");
+ printf("Couldn't install fs segment, expect segfault\n");
+ }
+#endif /*linux*/
+
+#if defined(__NetBSD__) || defined(TARGET_FREEBSD) || defined(__OpenBSD__) || defined(__DragonFly__) || defined(TARGET_DARWIN)
+ {
+ unsigned long d[2];
+
+ LDT_EntryToBytes( d, &array );
+#ifdef USE_LDT_AA
+ ret = i386_set_ldt(LDT_AUTO_ALLOC, (const union ldt_entry *)d, 1);
+ array.entry_number = ret;
+ fs_ldt = ret;
+#else
+ ret = i386_set_ldt(array.entry_number, (const union ldt_entry *)d, 1);
+#endif
+ if (ret < 0)
+ {
+ perror("install_fs");
+ printf("Couldn't install fs segment, expect segfault\n");
+ printf("Did you reconfigure the kernel with \"options USER_LDT\"?\n");
+#ifdef __OpenBSD__
+ printf("On newer OpenBSD systems did you set machdep.userldt to 1?\n");
+#endif
+ }
+ }
+#endif // __NetBSD__ || TARGET_FREEBSD || __OpenBSD__ || __DragonFly__ || TARGET_DARWIN
+
+#if defined(__svr4__)
+ {
+ struct ssd ssd;
+ ssd.sel = LDT_SEL(TEB_SEL_IDX);
+ ssd.bo = array.base_addr;
+ ssd.ls = array.limit - array.base_addr;
+ ssd.acc1 = ((array.read_exec_only == 0) << 1) |
+ (array.contents << 2) |
+ 0xf0; /* P(resent) | DPL3 | S */
+ ssd.acc2 = 0x4; /* byte limit, 32-bit segment */
+ if (sysi86(SI86DSCR, &ssd) < 0)
+ {
+ perror("sysi86(SI86DSCR)");
+ printf("Couldn't install fs segment, expect segfault\n");
+ }
+ }
+#endif
+
+ Setup_FS_Segment();
+
+ ldt_fs->prev_struct = malloc(8);
+ array.base_addr = (uintptr_t)(ldt_fs->prev_struct);
+
+ return ldt_fs;
+}
+
+void Restore_LDT_Keeper(ldt_fs_t* ldt_fs)
+{
+ if (ldt_fs == NULL || ldt_fs->fs_seg == 0)
+ return;
+ if (ldt_fs->prev_struct)
+ free(ldt_fs->prev_struct);
+
+ int sret = sysconf(_SC_PAGE_SIZE);
+ if (sret != -1)
+ munmap((char*)ldt_fs->fs_seg, sret);
+ ldt_fs->fs_seg = 0;
+ free(ldt_fs);
+}
+
+#endif
diff --git a/xbmc/cores/DllLoader/ldt_keeper.h b/xbmc/cores/DllLoader/ldt_keeper.h
new file mode 100644
index 0000000..f6a262f
--- /dev/null
+++ b/xbmc/cores/DllLoader/ldt_keeper.h
@@ -0,0 +1,38 @@
+/**
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ * This file MUST be in main library because LDT must
+ * be modified before program creates first thread
+ * - avifile includes this file from C++ code
+ * and initializes it at the start of player!
+ * it might sound like a hack and it really is - but
+ * as aviplay is decoding video with more than just one
+ * thread currently it's necessary to do it this way
+ * this might change in the future
+ */
+
+/* applied some modification to make make our xine friend more happy */
+
+/*
+ * Modified for use with MPlayer, detailed changelog at
+ * http://svn.mplayerhq.hu/mplayer/trunk/
+ * $Id: ldt_keeper.c 22733 2007-03-18 22:18:11Z nicodvb $
+ */
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+typedef struct {
+ void* fs_seg;
+ char* prev_struct;
+ int fd;
+} ldt_fs_t;
+
+void Setup_FS_Segment(void);
+ldt_fs_t* Setup_LDT_Keeper(void);
+void Restore_LDT_Keeper(ldt_fs_t* ldt_fs);
+#ifdef __cplusplus
+}
+#endif
+
diff --git a/xbmc/cores/DllLoader/mmap_anon.c b/xbmc/cores/DllLoader/mmap_anon.c
new file mode 100644
index 0000000..5bb13cf
--- /dev/null
+++ b/xbmc/cores/DllLoader/mmap_anon.c
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+/**
+ * \file mmap_anon.c
+ * \brief Provide a compatible anonymous space mapping function
+ */
+
+#include <stdio.h>
+
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#if defined(MAP_ANON) && !defined(MAP_ANONYMOUS)
+#define MAP_ANONYMOUS MAP_ANON
+#endif
+
+/*
+ * mmap() anonymous space, depending on the system's mmap() style. On systems
+ * that use the /dev/zero mapping idiom, zerofd will be set to the file descriptor
+ * of the opened /dev/zero.
+ */
+
+ /**
+ * \brief mmap() anonymous space, depending on the system's mmap() style. On systems
+ * that use the /dev/zero mapping idiom, zerofd will be set to the file descriptor
+ * of the opened /dev/zero.
+ *
+ * \param addr address to map at.
+ * \param len number of bytes from addr to be mapped.
+ * \param prot protections (region accessibility).
+ * \param flags specifies the type of the mapped object.
+ * \param offset start mapping at byte offset.
+ * \param zerofd
+ * \return a pointer to the mapped region upon successful completion, -1 otherwise.
+ */
+void *mmap_anon(void *addr, size_t len, int prot, int flags, off_t offset)
+{
+ void *result;
+
+#ifndef MAP_ANONYMOUS
+ int fd;
+#endif
+
+ /* From loader/ext.c:
+ * "Linux EINVAL's on us if we don't pass MAP_PRIVATE to an anon mmap"
+ * Therefore we preserve the same behavior on all platforms, ie. no
+ * shared mappings of anon space (if the concepts are supported). */
+#if defined(MAP_SHARED) && defined(MAP_PRIVATE)
+ flags = (flags & ~MAP_SHARED) | MAP_PRIVATE;
+#endif /* defined(MAP_SHARED) && defined(MAP_PRIVATE) */
+
+#ifdef MAP_ANONYMOUS
+ /* BSD-style anonymous mapping */
+ result = mmap(addr, len, prot, flags | MAP_ANONYMOUS, -1, offset);
+#else
+ /* SysV-style anonymous mapping */
+ fd = open("/dev/zero", O_RDWR);
+ if(fd < 0){
+ perror( "Cannot open /dev/zero for READ+WRITE. Check permissions! error: ");
+ return NULL;
+ }
+
+ result = mmap(addr, len, prot, flags, fd, offset);
+ close(fd);
+#endif /* MAP_ANONYMOUS */
+
+ return result;
+}
diff --git a/xbmc/cores/DllLoader/mmap_anon.h b/xbmc/cores/DllLoader/mmap_anon.h
new file mode 100644
index 0000000..291ccf8
--- /dev/null
+++ b/xbmc/cores/DllLoader/mmap_anon.h
@@ -0,0 +1,14 @@
+/*
+ * 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 <sys/types.h>
+
+void *mmap_anon(void *, size_t, int, int, off_t);
+
diff --git a/xbmc/cores/EdlEdit.h b/xbmc/cores/EdlEdit.h
new file mode 100644
index 0000000..a06ed15
--- /dev/null
+++ b/xbmc/cores/EdlEdit.h
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+namespace EDL
+{
+
+constexpr int EDL_ACTION_NONE = -1;
+
+enum class Action
+{
+ CUT = 0,
+ MUTE = 1,
+ SCENE = 2,
+ COMM_BREAK = 3
+};
+
+struct Edit
+{
+ int start = 0; // ms
+ int end = 0; // ms
+ Action action = Action::CUT;
+};
+
+} // namespace EDL
diff --git a/xbmc/cores/ExternalPlayer/CMakeLists.txt b/xbmc/cores/ExternalPlayer/CMakeLists.txt
new file mode 100644
index 0000000..69c71d9
--- /dev/null
+++ b/xbmc/cores/ExternalPlayer/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES ExternalPlayer.cpp)
+
+set(HEADERS ExternalPlayer.h)
+
+core_add_library(externalplayer)
diff --git a/xbmc/cores/ExternalPlayer/ExternalPlayer.cpp b/xbmc/cores/ExternalPlayer/ExternalPlayer.cpp
new file mode 100644
index 0000000..b027e21
--- /dev/null
+++ b/xbmc/cores/ExternalPlayer/ExternalPlayer.cpp
@@ -0,0 +1,689 @@
+/*
+ * 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 "ExternalPlayer.h"
+
+#include "CompileInfo.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "cores/DataCacheCore.h"
+#include "dialogs/GUIDialogOK.h"
+#include "filesystem/MusicDatabaseFile.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "threads/SystemClock.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+#include "video/Bookmark.h"
+#include "windowing/WinSystem.h"
+#if defined(TARGET_WINDOWS)
+ #include "utils/CharsetConverter.h"
+ #include <Windows.h>
+#endif
+#if defined(TARGET_ANDROID)
+ #include "platform/android/activity/XBMCApp.h"
+#endif
+
+// If the process ends in less than this time (ms), we assume it's a launcher
+// and wait for manual intervention before continuing
+#define LAUNCHER_PROCESS_TIME 2000
+// Time (ms) we give a process we sent a WM_QUIT to close before terminating
+#define PROCESS_GRACE_TIME 3000
+// Default time after which the item's playcount is incremented
+#define DEFAULT_PLAYCOUNT_MIN_TIME 10
+
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+extern HWND g_hWnd;
+#endif
+
+CExternalPlayer::CExternalPlayer(IPlayerCallback& callback)
+ : IPlayer(callback),
+ CThread("ExternalPlayer")
+{
+ m_bAbortRequest = false;
+ m_bIsPlaying = false;
+ m_playbackStartTime = {};
+ m_speed = 1;
+ m_time = 0;
+
+ m_hideconsole = false;
+ m_warpcursor = WARP_NONE;
+ m_hidexbmc = false;
+ m_islauncher = false;
+ m_playCountMinTime = DEFAULT_PLAYCOUNT_MIN_TIME;
+ m_playOneStackItem = false;
+
+ m_dialog = NULL;
+#if defined(TARGET_WINDOWS_DESKTOP)
+ m_xPos = 0;
+ m_yPos = 0;
+
+ memset(&m_processInfo, 0, sizeof(m_processInfo));
+#endif
+}
+
+CExternalPlayer::~CExternalPlayer()
+{
+ CloseFile();
+}
+
+bool CExternalPlayer::OpenFile(const CFileItem& file, const CPlayerOptions &options)
+{
+ try
+ {
+ m_file = file;
+ m_bIsPlaying = true;
+ m_time = 0;
+ m_playbackStartTime = std::chrono::steady_clock::now();
+ m_launchFilename = file.GetDynPath();
+ CLog::Log(LOGINFO, "{}: {}", __FUNCTION__, m_launchFilename);
+ Create();
+
+ return true;
+ }
+ catch(...)
+ {
+ m_bIsPlaying = false;
+ CLog::Log(LOGERROR, "{} - Exception thrown", __FUNCTION__);
+ return false;
+ }
+}
+
+bool CExternalPlayer::CloseFile(bool reopen)
+{
+ m_bAbortRequest = true;
+
+ if (m_dialog && m_dialog->IsActive()) m_dialog->Close();
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+ if (m_bIsPlaying && m_processInfo.hProcess)
+ {
+ TerminateProcess(m_processInfo.hProcess, 1);
+ }
+#endif
+
+ return true;
+}
+
+bool CExternalPlayer::IsPlaying() const
+{
+ return m_bIsPlaying;
+}
+
+void CExternalPlayer::Process()
+{
+ std::string mainFile = m_launchFilename;
+ std::string archiveContent;
+
+ if (m_args.find("{0}") == std::string::npos)
+ {
+ // Unwind archive names
+ CURL url(m_launchFilename);
+ if (url.IsProtocol("zip") || url.IsProtocol("rar") /* || url.IsProtocol("iso9660") ??*/ || url.IsProtocol("udf"))
+ {
+ mainFile = url.GetHostName();
+ archiveContent = url.GetFileName();
+ }
+ if (url.IsProtocol("musicdb"))
+ mainFile = CMusicDatabaseFile::TranslateUrl(url);
+ if (url.IsProtocol("bluray"))
+ {
+ CURL base(url.GetHostName());
+ if (base.IsProtocol("udf"))
+ {
+ mainFile = base.GetHostName(); /* image file */
+ archiveContent = base.GetFileName();
+ }
+ else
+ mainFile = URIUtils::AddFileToFolder(base.Get(), url.GetFileName());
+ }
+ }
+
+ if (!m_filenameReplacers.empty())
+ {
+ for (unsigned int i = 0; i < m_filenameReplacers.size(); i++)
+ {
+ std::vector<std::string> vecSplit = StringUtils::Split(m_filenameReplacers[i], " , ");
+
+ // something is wrong, go to next substitution
+ if (vecSplit.size() != 4)
+ continue;
+
+ std::string strMatch = vecSplit[0];
+ StringUtils::Replace(strMatch, ",,",",");
+ bool bCaseless = vecSplit[3].find('i') != std::string::npos;
+ CRegExp regExp(bCaseless, CRegExp::autoUtf8);
+
+ if (!regExp.RegComp(strMatch.c_str()))
+ { // invalid regexp - complain in logs
+ CLog::Log(LOGERROR, "{}: Invalid RegExp:'{}'", __FUNCTION__, strMatch);
+ continue;
+ }
+
+ if (regExp.RegFind(mainFile) > -1)
+ {
+ std::string strPat = vecSplit[1];
+ StringUtils::Replace(strPat, ",,",",");
+
+ if (!regExp.RegComp(strPat.c_str()))
+ { // invalid regexp - complain in logs
+ CLog::Log(LOGERROR, "{}: Invalid RegExp:'{}'", __FUNCTION__, strPat);
+ continue;
+ }
+
+ std::string strRep = vecSplit[2];
+ StringUtils::Replace(strRep, ",,",",");
+ bool bGlobal = vecSplit[3].find('g') != std::string::npos;
+ bool bStop = vecSplit[3].find('s') != std::string::npos;
+ int iStart = 0;
+ while ((iStart = regExp.RegFind(mainFile, iStart)) > -1)
+ {
+ int iLength = regExp.GetFindLen();
+ mainFile = mainFile.substr(0, iStart) + regExp.GetReplaceString(strRep) + mainFile.substr(iStart + iLength);
+ if (!bGlobal)
+ break;
+ }
+ CLog::Log(LOGINFO, "{}: File matched:'{}' (RE='{}',Rep='{}') new filename:'{}'.",
+ __FUNCTION__, strMatch, strPat, strRep, mainFile);
+ if (bStop) break;
+ }
+ }
+ }
+
+ CLog::Log(LOGINFO, "{}: Player : {}", __FUNCTION__, m_filename);
+ CLog::Log(LOGINFO, "{}: File : {}", __FUNCTION__, mainFile);
+ CLog::Log(LOGINFO, "{}: Content: {}", __FUNCTION__, archiveContent);
+ CLog::Log(LOGINFO, "{}: Args : {}", __FUNCTION__, m_args);
+ CLog::Log(LOGINFO, "{}: Start", __FUNCTION__);
+
+ // make sure we surround the arguments with quotes where necessary
+ std::string strFName;
+ std::string strFArgs;
+#if defined(TARGET_WINDOWS_DESKTOP)
+ // W32 batch-file handline
+ if (StringUtils::EndsWith(m_filename, ".bat") || StringUtils::EndsWith(m_filename, ".cmd"))
+ {
+ // MSDN says you just need to do this, but cmd's handing of spaces and
+ // quotes is soo broken it seems to work much better if you just omit
+ // lpApplicationName and enclose the module in lpCommandLine in quotes
+ //strFName = "cmd.exe";
+ //strFArgs = "/c ";
+ }
+ else
+#endif
+ strFName = m_filename;
+
+ strFArgs.append("\"");
+ strFArgs.append(m_filename);
+ strFArgs.append("\" ");
+ strFArgs.append(m_args);
+
+ int nReplaced = StringUtils::Replace(strFArgs, "{0}", mainFile);
+
+ if (!nReplaced)
+ nReplaced = StringUtils::Replace(strFArgs, "{1}", mainFile) + StringUtils::Replace(strFArgs, "{2}", archiveContent);
+
+ if (!nReplaced)
+ {
+ strFArgs.append(" \"");
+ strFArgs.append(mainFile);
+ strFArgs.append("\"");
+ }
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+ if (m_warpcursor)
+ {
+ GetCursorPos(&m_ptCursorpos);
+ int x = 0;
+ int y = 0;
+ switch (m_warpcursor)
+ {
+ case WARP_BOTTOM_RIGHT:
+ x = GetSystemMetrics(SM_CXSCREEN);
+ case WARP_BOTTOM_LEFT:
+ y = GetSystemMetrics(SM_CYSCREEN);
+ break;
+ case WARP_TOP_RIGHT:
+ x = GetSystemMetrics(SM_CXSCREEN);
+ break;
+ case WARP_CENTER:
+ x = GetSystemMetrics(SM_CXSCREEN) / 2;
+ y = GetSystemMetrics(SM_CYSCREEN) / 2;
+ break;
+ }
+ CLog::Log(LOGINFO, "{}: Warping cursor to ({},{})", __FUNCTION__, x, y);
+ SetCursorPos(x,y);
+ }
+
+ LONG currentStyle = GetWindowLong(g_hWnd, GWL_EXSTYLE);
+#endif
+
+ if (m_hidexbmc && !m_islauncher)
+ {
+ CLog::Log(LOGINFO, "{}: Hiding {} window", __FUNCTION__, CCompileInfo::GetAppName());
+ CServiceBroker::GetWinSystem()->Hide();
+ }
+#if defined(TARGET_WINDOWS_DESKTOP)
+ else if (currentStyle & WS_EX_TOPMOST)
+ {
+ CLog::Log(LOGINFO, "{}: Lowering {} window", __FUNCTION__, CCompileInfo::GetAppName());
+ SetWindowPos(g_hWnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOREDRAW | SWP_ASYNCWINDOWPOS);
+ }
+
+ CLog::Log(LOGDEBUG, "{}: Unlocking foreground window", __FUNCTION__);
+ LockSetForegroundWindow(LSFW_UNLOCK);
+#endif
+
+ m_playbackStartTime = std::chrono::steady_clock::now();
+
+ /* Suspend AE temporarily so exclusive or hog-mode sinks */
+ /* don't block external player's access to audio device */
+ CServiceBroker::GetActiveAE()->Suspend();
+ // wait for AE has completed suspended
+ XbmcThreads::EndTime<> timer(2000ms);
+ while (!timer.IsTimePast() && !CServiceBroker::GetActiveAE()->IsSuspended())
+ {
+ CThread::Sleep(50ms);
+ }
+ if (timer.IsTimePast())
+ {
+ CLog::Log(LOGERROR, "{}: AudioEngine did not suspend before launching external player",
+ __FUNCTION__);
+ }
+
+ m_callback.OnPlayBackStarted(m_file);
+ m_callback.OnAVStarted(m_file);
+
+ bool ret = true;
+#if defined(TARGET_WINDOWS_DESKTOP)
+ ret = ExecuteAppW32(strFName.c_str(),strFArgs.c_str());
+#elif defined(TARGET_ANDROID)
+ ret = ExecuteAppAndroid(m_filename.c_str(), mainFile.c_str());
+#elif defined(TARGET_POSIX) && !defined(TARGET_DARWIN_EMBEDDED)
+ ret = ExecuteAppLinux(strFArgs.c_str());
+#endif
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - m_playbackStartTime);
+
+ if (ret && (m_islauncher || duration.count() < LAUNCHER_PROCESS_TIME))
+ {
+ if (m_hidexbmc)
+ {
+ CLog::Log(LOGINFO, "{}: {} cannot stay hidden for a launcher process", __FUNCTION__,
+ CCompileInfo::GetAppName());
+ CServiceBroker::GetWinSystem()->Show(false);
+ }
+
+ {
+ m_dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogOK>(WINDOW_DIALOG_OK);
+ m_dialog->SetHeading(CVariant{23100});
+ m_dialog->SetLine(1, CVariant{23104});
+ m_dialog->SetLine(2, CVariant{23105});
+ m_dialog->SetLine(3, CVariant{23106});
+ }
+
+ if (!m_bAbortRequest)
+ m_dialog->Open();
+ }
+
+ m_bIsPlaying = false;
+ CLog::Log(LOGINFO, "{}: Stop", __FUNCTION__);
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+ CServiceBroker::GetWinSystem()->Restore();
+
+ if (currentStyle & WS_EX_TOPMOST)
+ {
+ CLog::Log(LOGINFO, "{}: Showing {} window TOPMOST", __FUNCTION__, CCompileInfo::GetAppName());
+ SetWindowPos(g_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_ASYNCWINDOWPOS);
+ SetForegroundWindow(g_hWnd);
+ }
+ else
+#endif
+ {
+ CLog::Log(LOGINFO, "{}: Showing {} window", __FUNCTION__, CCompileInfo::GetAppName());
+ CServiceBroker::GetWinSystem()->Show();
+ }
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+ if (m_warpcursor)
+ {
+ m_xPos = 0;
+ m_yPos = 0;
+ if (&m_ptCursorpos != 0)
+ {
+ m_xPos = (m_ptCursorpos.x);
+ m_yPos = (m_ptCursorpos.y);
+ }
+ CLog::Log(LOGINFO, "{}: Restoring cursor to ({},{})", __FUNCTION__, m_xPos, m_yPos);
+ SetCursorPos(m_xPos,m_yPos);
+ }
+#endif
+
+ CBookmark bookmark;
+ bookmark.totalTimeInSeconds = 1;
+ bookmark.timeInSeconds = (duration.count() / 1000 >= m_playCountMinTime) ? 1 : 0;
+ bookmark.player = m_name;
+ m_callback.OnPlayerCloseFile(m_file, bookmark);
+
+ /* Resume AE processing of XBMC native audio */
+ if (!CServiceBroker::GetActiveAE()->Resume())
+ {
+ CLog::Log(LOGFATAL, "{}: Failed to restart AudioEngine after return from external player",
+ __FUNCTION__);
+ }
+
+ // We don't want to come back to an active screensaver
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+ appPower->WakeUpScreenSaverAndDPMS();
+
+ if (!ret || (m_playOneStackItem && g_application.CurrentFileItem().IsStack()))
+ m_callback.OnPlayBackStopped();
+ else
+ m_callback.OnPlayBackEnded();
+}
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+bool CExternalPlayer::ExecuteAppW32(const char* strPath, const char* strSwitches)
+{
+ CLog::Log(LOGINFO, "{}: {} {}", __FUNCTION__, strPath, strSwitches);
+
+ STARTUPINFOW si = {};
+ si.cb = sizeof(si);
+ si.dwFlags = STARTF_USESHOWWINDOW;
+ si.wShowWindow = m_hideconsole ? SW_HIDE : SW_SHOW;
+
+ std::wstring WstrPath, WstrSwitches;
+ g_charsetConverter.utf8ToW(strPath, WstrPath, false);
+ g_charsetConverter.utf8ToW(strSwitches, WstrSwitches, false);
+
+ if (m_bAbortRequest) return false;
+
+ BOOL ret = CreateProcessW(WstrPath.empty() ? NULL : WstrPath.c_str(),
+ (LPWSTR) WstrSwitches.c_str(), NULL, NULL, FALSE, NULL,
+ NULL, NULL, &si, &m_processInfo);
+
+ if (ret == FALSE)
+ {
+ DWORD lastError = GetLastError();
+ CLog::Log(LOGINFO, "{} - Failure: {}", __FUNCTION__, lastError);
+ }
+ else
+ {
+ int res = WaitForSingleObject(m_processInfo.hProcess, INFINITE);
+
+ switch (res)
+ {
+ case WAIT_OBJECT_0:
+ CLog::Log(LOGINFO, "{}: WAIT_OBJECT_0", __FUNCTION__);
+ break;
+ case WAIT_ABANDONED:
+ CLog::Log(LOGINFO, "{}: WAIT_ABANDONED", __FUNCTION__);
+ break;
+ case WAIT_TIMEOUT:
+ CLog::Log(LOGINFO, "{}: WAIT_TIMEOUT", __FUNCTION__);
+ break;
+ case WAIT_FAILED:
+ CLog::Log(LOGINFO, "{}: WAIT_FAILED ({})", __FUNCTION__, GetLastError());
+ ret = FALSE;
+ break;
+ }
+
+ CloseHandle(m_processInfo.hThread);
+ m_processInfo.hThread = 0;
+ CloseHandle(m_processInfo.hProcess);
+ m_processInfo.hProcess = 0;
+ }
+ return (ret == TRUE);
+}
+#endif
+
+#if !defined(TARGET_ANDROID) && !defined(TARGET_DARWIN_EMBEDDED) && defined(TARGET_POSIX)
+bool CExternalPlayer::ExecuteAppLinux(const char* strSwitches)
+{
+ CLog::Log(LOGINFO, "{}: {}", __FUNCTION__, strSwitches);
+
+ int ret = system(strSwitches);
+ if (ret != 0)
+ {
+ CLog::Log(LOGINFO, "{}: Failure: {}", __FUNCTION__, ret);
+ }
+
+ return (ret == 0);
+}
+#endif
+
+#if defined(TARGET_ANDROID)
+bool CExternalPlayer::ExecuteAppAndroid(const char* strSwitches,const char* strPath)
+{
+ CLog::Log(LOGINFO, "{}: {}", __FUNCTION__, strSwitches);
+
+ bool ret = CXBMCApp::StartActivity(strSwitches, "android.intent.action.VIEW", "video/*", strPath);
+
+ if (!ret)
+ {
+ CLog::Log(LOGINFO, "{}: Failure", __FUNCTION__);
+ }
+
+ return (ret == 0);
+}
+#endif
+
+void CExternalPlayer::Pause()
+{
+}
+
+bool CExternalPlayer::HasVideo() const
+{
+ return true;
+}
+
+bool CExternalPlayer::HasAudio() const
+{
+ return false;
+}
+
+bool CExternalPlayer::CanSeek() const
+{
+ return false;
+}
+
+void CExternalPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride)
+{
+}
+
+void CExternalPlayer::SeekPercentage(float iPercent)
+{
+}
+
+void CExternalPlayer::SetAVDelay(float fValue)
+{
+}
+
+float CExternalPlayer::GetAVDelay()
+{
+ return 0.0f;
+}
+
+void CExternalPlayer::SetSubTitleDelay(float fValue)
+{
+}
+
+float CExternalPlayer::GetSubTitleDelay()
+{
+ return 0.0;
+}
+
+void CExternalPlayer::SeekTime(int64_t iTime)
+{
+}
+
+void CExternalPlayer::SetSpeed(float speed)
+{
+ m_speed = speed;
+ CDataCacheCore::GetInstance().SetSpeed(1.0, speed);
+}
+
+bool CExternalPlayer::SetPlayerState(const std::string& state)
+{
+ return true;
+}
+
+bool CExternalPlayer::Initialize(TiXmlElement* pConfig)
+{
+ XMLUtils::GetString(pConfig, "filename", m_filename);
+ if (m_filename.length() > 0)
+ {
+ CLog::Log(LOGINFO, "ExternalPlayer Filename: {}", m_filename);
+ }
+ else
+ {
+ std::string xml;
+ xml<<*pConfig;
+ CLog::Log(LOGERROR, "ExternalPlayer Error: filename element missing from: {}", xml);
+ return false;
+ }
+
+ XMLUtils::GetString(pConfig, "args", m_args);
+ XMLUtils::GetBoolean(pConfig, "playonestackitem", m_playOneStackItem);
+ XMLUtils::GetBoolean(pConfig, "islauncher", m_islauncher);
+ XMLUtils::GetBoolean(pConfig, "hidexbmc", m_hidexbmc);
+ if (!XMLUtils::GetBoolean(pConfig, "hideconsole", m_hideconsole))
+ {
+#ifdef TARGET_WINDOWS_DESKTOP
+ // Default depends on whether player is a batch file
+ m_hideconsole = StringUtils::EndsWith(m_filename, ".bat");
+#endif
+ }
+
+ bool bHideCursor;
+ if (XMLUtils::GetBoolean(pConfig, "hidecursor", bHideCursor) && bHideCursor)
+ m_warpcursor = WARP_BOTTOM_RIGHT;
+
+ std::string warpCursor;
+ if (XMLUtils::GetString(pConfig, "warpcursor", warpCursor) && !warpCursor.empty())
+ {
+ if (warpCursor == "bottomright") m_warpcursor = WARP_BOTTOM_RIGHT;
+ else if (warpCursor == "bottomleft") m_warpcursor = WARP_BOTTOM_LEFT;
+ else if (warpCursor == "topleft") m_warpcursor = WARP_TOP_LEFT;
+ else if (warpCursor == "topright") m_warpcursor = WARP_TOP_RIGHT;
+ else if (warpCursor == "center") m_warpcursor = WARP_CENTER;
+ else
+ {
+ warpCursor = "none";
+ CLog::Log(LOGWARNING, "ExternalPlayer: invalid value for warpcursor: {}", warpCursor);
+ }
+ }
+
+ XMLUtils::GetInt(pConfig, "playcountminimumtime", m_playCountMinTime, 1, INT_MAX);
+
+ CLog::Log(
+ LOGINFO,
+ "ExternalPlayer Tweaks: hideconsole ({}), hidexbmc ({}), islauncher ({}), warpcursor ({})",
+ m_hideconsole ? "true" : "false", m_hidexbmc ? "true" : "false",
+ m_islauncher ? "true" : "false", warpCursor);
+
+#ifdef TARGET_WINDOWS_DESKTOP
+ m_filenameReplacers.push_back("^smb:// , / , \\\\ , g");
+ m_filenameReplacers.push_back("^smb:\\\\\\\\ , smb:(\\\\\\\\[^\\\\]*\\\\) , \\1 , ");
+#endif
+
+ TiXmlElement* pReplacers = pConfig->FirstChildElement("replacers");
+ while (pReplacers)
+ {
+ GetCustomRegexpReplacers(pReplacers, m_filenameReplacers);
+ pReplacers = pReplacers->NextSiblingElement("replacers");
+ }
+
+ return true;
+}
+
+void CExternalPlayer::GetCustomRegexpReplacers(TiXmlElement *pRootElement,
+ std::vector<std::string>& settings)
+{
+ int iAction = 0; // overwrite
+ // for backward compatibility
+ const char* szAppend = pRootElement->Attribute("append");
+ if ((szAppend && StringUtils::CompareNoCase(szAppend, "yes") == 0))
+ iAction = 1;
+ // action takes precedence if both attributes exist
+ const char* szAction = pRootElement->Attribute("action");
+ if (szAction)
+ {
+ iAction = 0; // overwrite
+ if (StringUtils::CompareNoCase(szAction, "append") == 0)
+ iAction = 1; // append
+ else if (StringUtils::CompareNoCase(szAction, "prepend") == 0)
+ iAction = 2; // prepend
+ }
+ if (iAction == 0)
+ settings.clear();
+
+ TiXmlElement* pReplacer = pRootElement->FirstChildElement("replacer");
+ int i = 0;
+ while (pReplacer)
+ {
+ if (pReplacer->FirstChild())
+ {
+ const char* szGlobal = pReplacer->Attribute("global");
+ const char* szStop = pReplacer->Attribute("stop");
+ bool bGlobal = szGlobal && StringUtils::CompareNoCase(szGlobal, "true") == 0;
+ bool bStop = szStop && StringUtils::CompareNoCase(szStop, "true") == 0;
+
+ std::string strMatch;
+ std::string strPat;
+ std::string strRep;
+ XMLUtils::GetString(pReplacer,"match",strMatch);
+ XMLUtils::GetString(pReplacer,"pat",strPat);
+ XMLUtils::GetString(pReplacer,"rep",strRep);
+
+ if (!strPat.empty() && !strRep.empty())
+ {
+ CLog::Log(LOGDEBUG," Registering replacer:");
+ CLog::Log(LOGDEBUG, " Match:[{}] Pattern:[{}] Replacement:[{}]", strMatch, strPat,
+ strRep);
+ CLog::Log(LOGDEBUG, " Global:[{}] Stop:[{}]", bGlobal ? "true" : "false",
+ bStop ? "true" : "false");
+ // keep literal commas since we use comma as a separator
+ StringUtils::Replace(strMatch, ",",",,");
+ StringUtils::Replace(strPat, ",",",,");
+ StringUtils::Replace(strRep, ",",",,");
+
+ std::string strReplacer = strMatch + " , " + strPat + " , " + strRep + " , " + (bGlobal ? "g" : "") + (bStop ? "s" : "");
+ if (iAction == 2)
+ settings.insert(settings.begin() + i++, 1, strReplacer);
+ else
+ settings.push_back(strReplacer);
+ }
+ else
+ {
+ // error message about missing tag
+ if (strPat.empty())
+ CLog::Log(LOGERROR," Missing <Pat> tag");
+ else
+ CLog::Log(LOGERROR," Missing <Rep> tag");
+ }
+ }
+
+ pReplacer = pReplacer->NextSiblingElement("replacer");
+ }
+}
diff --git a/xbmc/cores/ExternalPlayer/ExternalPlayer.h b/xbmc/cores/ExternalPlayer/ExternalPlayer.h
new file mode 100644
index 0000000..c8f39d5
--- /dev/null
+++ b/xbmc/cores/ExternalPlayer/ExternalPlayer.h
@@ -0,0 +1,89 @@
+/*
+ * 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 "FileItem.h"
+#include "cores/IPlayer.h"
+#include "threads/Thread.h"
+
+#include <string>
+#include <vector>
+
+class CGUIDialogOK;
+
+class CExternalPlayer : public IPlayer, public CThread
+{
+public:
+ enum WARP_CURSOR { WARP_NONE = 0, WARP_TOP_LEFT, WARP_TOP_RIGHT, WARP_BOTTOM_RIGHT, WARP_BOTTOM_LEFT, WARP_CENTER };
+
+ explicit CExternalPlayer(IPlayerCallback& callback);
+ ~CExternalPlayer() override;
+ bool Initialize(TiXmlElement* pConfig) override;
+ bool OpenFile(const CFileItem& file, const CPlayerOptions &options) override;
+ bool CloseFile(bool reopen = false) override;
+ bool IsPlaying() const override;
+ void Pause() override;
+ bool HasVideo() const override;
+ bool HasAudio() const override;
+ bool CanSeek() const override;
+ void Seek(bool bPlus, bool bLargeStep, bool bChapterOverride) override;
+ void SeekPercentage(float iPercent) override;
+ void SetVolume(float volume) override {}
+ void SetDynamicRangeCompression(long drc) override {}
+ void SetAVDelay(float fValue = 0.0f) override;
+ float GetAVDelay() override;
+
+ void SetSubTitleDelay(float fValue = 0.0f) override;
+ float GetSubTitleDelay() override;
+
+ void SeekTime(int64_t iTime) override;
+ void SetSpeed(float speed) override;
+ void DoAudioWork() override {}
+
+ bool SetPlayerState(const std::string& state) override;
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+ bool ExecuteAppW32(const char* strPath, const char* strSwitches);
+ //static void CALLBACK AppFinished(void* closure, BOOLEAN TimerOrWaitFired);
+#elif defined(TARGET_ANDROID)
+ bool ExecuteAppAndroid(const char* strSwitches,const char* strPath);
+#elif defined(TARGET_POSIX)
+ bool ExecuteAppLinux(const char* strSwitches);
+#endif
+
+private:
+ void GetCustomRegexpReplacers(TiXmlElement *pRootElement, std::vector<std::string>& settings);
+ void Process() override;
+
+ bool m_bAbortRequest;
+ bool m_bIsPlaying;
+ std::chrono::time_point<std::chrono::steady_clock> m_playbackStartTime;
+ float m_speed;
+ int m_time;
+ std::string m_launchFilename;
+#if defined(TARGET_WINDOWS_DESKTOP)
+ POINT m_ptCursorpos;
+ PROCESS_INFORMATION m_processInfo;
+#endif
+ CGUIDialogOK* m_dialog;
+#if defined(TARGET_WINDOWS_DESKTOP)
+ int m_xPos;
+ int m_yPos;
+#endif
+ std::string m_filename;
+ std::string m_args;
+ bool m_hideconsole;
+ bool m_hidexbmc;
+ bool m_islauncher;
+ bool m_playOneStackItem;
+ WARP_CURSOR m_warpcursor;
+ int m_playCountMinTime;
+ std::vector<std::string> m_filenameReplacers;
+ CFileItem m_file;
+};
diff --git a/xbmc/cores/FFmpeg.cpp b/xbmc/cores/FFmpeg.cpp
new file mode 100644
index 0000000..03a29f7
--- /dev/null
+++ b/xbmc/cores/FFmpeg.cpp
@@ -0,0 +1,119 @@
+/*
+ * 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 "cores/FFmpeg.h"
+
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <map>
+#include <mutex>
+
+static thread_local CFFmpegLog* CFFmpegLogTls;
+
+void CFFmpegLog::SetLogLevel(int level)
+{
+ CFFmpegLog::ClearLogLevel();
+ CFFmpegLog *log = new CFFmpegLog();
+ log->level = level;
+ CFFmpegLogTls = log;
+}
+
+int CFFmpegLog::GetLogLevel()
+{
+ CFFmpegLog* log = CFFmpegLogTls;
+ if (!log)
+ return -1;
+ return log->level;
+}
+
+void CFFmpegLog::ClearLogLevel()
+{
+ CFFmpegLog* log = CFFmpegLogTls;
+ CFFmpegLogTls = nullptr;
+ if (log)
+ delete log;
+}
+
+static CCriticalSection m_logSection;
+std::map<const CThread*, std::string> g_logbuffer;
+
+void ff_flush_avutil_log_buffers(void)
+{
+ std::unique_lock<CCriticalSection> lock(m_logSection);
+ /* Loop through the logbuffer list and remove any blank buffers
+ If the thread using the buffer is still active, it will just
+ add a new buffer next time it writes to the log */
+ std::map<const CThread*, std::string>::iterator it;
+ for (it = g_logbuffer.begin(); it != g_logbuffer.end(); )
+ if ((*it).second.empty())
+ g_logbuffer.erase(it++);
+ else
+ ++it;
+}
+
+void ff_avutil_log(void* ptr, int level, const char* format, va_list va)
+{
+ std::unique_lock<CCriticalSection> lock(m_logSection);
+ const CThread* threadId = CThread::GetCurrentThread();
+ std::string &buffer = g_logbuffer[threadId];
+
+ AVClass* avc= ptr ? *(AVClass**)ptr : NULL;
+
+ int maxLevel = AV_LOG_WARNING;
+ if (CFFmpegLog::GetLogLevel() > 0)
+ maxLevel = AV_LOG_INFO;
+
+ if (level > maxLevel && !CServiceBroker::GetLogging().CanLogComponent(LOGFFMPEG))
+ return;
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel <= LOG_LEVEL_NORMAL)
+ return;
+
+ int type;
+ switch (level)
+ {
+ case AV_LOG_INFO:
+ type = LOGINFO;
+ break;
+
+ case AV_LOG_ERROR:
+ type = LOGERROR;
+ break;
+
+ case AV_LOG_DEBUG:
+ default:
+ type = LOGDEBUG;
+ break;
+ }
+
+ std::string message = StringUtils::FormatV(format, va);
+ std::string prefix = StringUtils::Format("ffmpeg[{}]: ", fmt::ptr(threadId));
+ if (avc)
+ {
+ if (avc->item_name)
+ prefix += std::string("[") + avc->item_name(ptr) + "] ";
+ else if (avc->class_name)
+ prefix += std::string("[") + avc->class_name + "] ";
+ }
+
+ buffer += message;
+ int pos, start = 0;
+ while ((pos = buffer.find_first_of('\n', start)) >= 0)
+ {
+ if (pos > start)
+ CLog::Log(type, "{}{}", prefix, buffer.substr(start, pos - start));
+ start = pos+1;
+ }
+ buffer.erase(0, start);
+}
+
diff --git a/xbmc/cores/FFmpeg.h b/xbmc/cores/FFmpeg.h
new file mode 100644
index 0000000..e1194d1
--- /dev/null
+++ b/xbmc/cores/FFmpeg.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "ServiceBroker.h"
+#include "utils/CPUInfo.h"
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libavutil/avutil.h>
+#include <libavutil/log.h>
+#include <libavutil/ffversion.h>
+#include <libavfilter/avfilter.h>
+#include <libpostproc/postprocess.h>
+}
+
+inline int PPCPUFlags()
+{
+ unsigned int cpuFeatures = CServiceBroker::GetCPUInfo()->GetCPUFeatures();
+ int flags = 0;
+
+ if (cpuFeatures & CPU_FEATURE_MMX)
+ flags |= PP_CPU_CAPS_MMX;
+ if (cpuFeatures & CPU_FEATURE_MMX2)
+ flags |= PP_CPU_CAPS_MMX2;
+ if (cpuFeatures & CPU_FEATURE_3DNOW)
+ flags |= PP_CPU_CAPS_3DNOW;
+ if (cpuFeatures & CPU_FEATURE_ALTIVEC)
+ flags |= PP_CPU_CAPS_ALTIVEC;
+
+ return flags;
+}
+
+// callback used for logging
+void ff_avutil_log(void* ptr, int level, const char* format, va_list va);
+void ff_flush_avutil_log_buffers(void);
+
+class CFFmpegLog
+{
+public:
+ static void SetLogLevel(int level);
+ static int GetLogLevel();
+ static void ClearLogLevel();
+ int level;
+};
+
diff --git a/xbmc/cores/GameSettings.h b/xbmc/cores/GameSettings.h
new file mode 100644
index 0000000..a6b34c6
--- /dev/null
+++ b/xbmc/cores/GameSettings.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017-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
+
+namespace KODI
+{
+namespace RETRO
+{
+
+// NOTE: Only append
+enum class SCALINGMETHOD
+{
+ AUTO = 0,
+ NEAREST = 1,
+ LINEAR = 2,
+ MAX = LINEAR
+};
+
+/*!
+ * \ingroup games
+ * \brief Methods for stretching the game to the viewing area
+ */
+enum class STRETCHMODE
+{
+ /*!
+ * \brief Show the game at its normal aspect ratio
+ */
+ Normal,
+
+ /*!
+ * \brief Stretch the game to maintain a 4:3 aspect ratio
+ */
+ Stretch4x3,
+
+ /*!
+ * \brief Stretch the game to fill the viewing area
+ */
+ Fullscreen,
+
+ /*!
+ * \brief Show the game at its original size (humorous for old consoles
+ * on 4K TVs)
+ */
+ Original,
+};
+
+constexpr const char* STRETCHMODE_NORMAL_ID = "normal";
+constexpr const char* STRETCHMODE_STRETCH_4_3_ID = "4:3";
+constexpr const char* STRETCHMODE_FULLSCREEN_ID = "fullscreen";
+constexpr const char* STRETCHMODE_ORIGINAL_ID = "original";
+
+enum class RENDERFEATURE
+{
+ ROTATION,
+ STRETCH,
+ ZOOM,
+ PIXEL_RATIO,
+};
+
+}
+}
diff --git a/xbmc/cores/IPlayer.h b/xbmc/cores/IPlayer.h
new file mode 100644
index 0000000..706f9c1
--- /dev/null
+++ b/xbmc/cores/IPlayer.h
@@ -0,0 +1,277 @@
+/*
+ * 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 "IPlayerCallback.h"
+#include "Interface/StreamInfo.h"
+#include "MenuType.h"
+#include "VideoSettings.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#define CURRENT_STREAM -1
+#define CAPTUREFLAG_CONTINUOUS 0x01 //after a render is done, render a new one immediately
+#define CAPTUREFLAG_IMMEDIATELY 0x02 //read out immediately after render, this can cause a busy wait
+#define CAPTUREFORMAT_BGRA 0x01
+
+struct TextCacheStruct_t;
+class TiXmlElement;
+class CStreamDetails;
+class CAction;
+class IPlayerCallback;
+
+class CPlayerOptions
+{
+public:
+ CPlayerOptions()
+ {
+ starttime = 0LL;
+ startpercent = 0LL;
+ fullscreen = false;
+ videoOnly = false;
+ preferStereo = false;
+ }
+ double starttime; /* start time in seconds */
+ double startpercent; /* start time in percent */
+ std::string state; /* potential playerstate to restore to */
+ bool fullscreen; /* player is allowed to switch to fullscreen */
+ bool videoOnly; /* player is not allowed to play audio streams, video streams only */
+ bool preferStereo; /* prefer stereo streams when selecting initial audio stream*/
+};
+
+class CFileItem;
+
+enum IPlayerAudioCapabilities
+{
+ IPC_AUD_ALL,
+ IPC_AUD_OFFSET,
+ IPC_AUD_AMP,
+ IPC_AUD_SELECT_STREAM,
+ IPC_AUD_OUTPUT_STEREO,
+ IPC_AUD_SELECT_OUTPUT
+};
+
+enum IPlayerSubtitleCapabilities
+{
+ IPC_SUBS_ALL,
+ IPC_SUBS_SELECT,
+ IPC_SUBS_EXTERNAL,
+ IPC_SUBS_OFFSET
+};
+
+enum ERENDERFEATURE
+{
+ RENDERFEATURE_GAMMA,
+ RENDERFEATURE_BRIGHTNESS,
+ RENDERFEATURE_CONTRAST,
+ RENDERFEATURE_NOISE,
+ RENDERFEATURE_SHARPNESS,
+ RENDERFEATURE_NONLINSTRETCH,
+ RENDERFEATURE_ROTATION,
+ RENDERFEATURE_STRETCH,
+ RENDERFEATURE_ZOOM,
+ RENDERFEATURE_VERTICAL_SHIFT,
+ RENDERFEATURE_PIXEL_RATIO,
+ RENDERFEATURE_POSTPROCESS,
+ RENDERFEATURE_TONEMAP
+};
+
+class IPlayer
+{
+public:
+ explicit IPlayer(IPlayerCallback& callback) : m_callback(callback) {}
+ virtual ~IPlayer() = default;
+ virtual bool Initialize(TiXmlElement* pConfig) { return true; }
+ virtual bool OpenFile(const CFileItem& file, const CPlayerOptions& options){ return false;}
+ virtual bool QueueNextFile(const CFileItem &file) { return false; }
+ virtual void OnNothingToQueueNotify() {}
+ virtual bool CloseFile(bool reopen = false) = 0;
+ virtual bool IsPlaying() const { return false;}
+ virtual bool CanPause() const { return true; }
+ virtual void Pause() = 0;
+ virtual bool HasVideo() const = 0;
+ virtual bool HasAudio() const = 0;
+ virtual bool HasGame() const { return false; }
+ virtual bool HasRDS() const { return false; }
+ virtual bool HasID3() const { return false; }
+ virtual bool IsPassthrough() const { return false;}
+ virtual bool CanSeek() const { return true; }
+ virtual void Seek(bool bPlus = true, bool bLargeStep = false, bool bChapterOverride = false) = 0;
+ virtual bool SeekScene(bool bPlus = true) {return false;}
+ virtual void SeekPercentage(float fPercent = 0){}
+ virtual float GetCachePercentage() const { return 0; }
+ virtual void SetMute(bool bOnOff){}
+ virtual void SetVolume(float volume){}
+ virtual void SetDynamicRangeCompression(long drc){}
+
+ virtual void SetAVDelay(float fValue = 0.0f) {}
+ virtual float GetAVDelay() { return 0.0f; }
+
+ virtual void SetSubTitleDelay(float fValue = 0.0f) {}
+ virtual float GetSubTitleDelay() { return 0.0f; }
+ virtual int GetSubtitleCount() const { return 0; }
+ virtual int GetSubtitle() { return -1; }
+ virtual void GetSubtitleStreamInfo(int index, SubtitleStreamInfo& info) const {}
+ virtual void SetSubtitle(int iStream) {}
+ virtual bool GetSubtitleVisible() const { return false; }
+ virtual void SetSubtitleVisible(bool bVisible) {}
+
+ /*!
+ * \brief Set the subtitle vertical position,
+ * it depends on current screen resolution
+ * \param value The subtitle position in pixels
+ * \param save If true, the value will be saved to resolution info
+ */
+ virtual void SetSubtitleVerticalPosition(int value, bool save) {}
+
+ /** \brief Adds the subtitle(s) provided by the given file to the available player streams
+ * and actives the first of the added stream(s). E.g., vob subs can contain multiple streams.
+ * \param[in] strSubPath The full path of the subtitle file.
+ */
+ virtual void AddSubtitle(const std::string& strSubPath) {}
+
+ virtual int GetAudioStreamCount() const { return 0; }
+ virtual int GetAudioStream() { return -1; }
+ virtual void SetAudioStream(int iStream) {}
+ virtual void GetAudioStreamInfo(int index, AudioStreamInfo& info) const {}
+
+ virtual int GetVideoStream() const { return -1; }
+ virtual int GetVideoStreamCount() const { return 0; }
+ virtual void GetVideoStreamInfo(int streamId, VideoStreamInfo& info) const {}
+ virtual void SetVideoStream(int iStream) {}
+
+ virtual int GetPrograms(std::vector<ProgramInfo>& programs) { return 0; }
+ virtual void SetProgram(int progId) {}
+ virtual int GetProgramsCount() const { return 0; }
+
+ virtual bool HasTeletextCache() const { return false; }
+ virtual std::shared_ptr<TextCacheStruct_t> GetTeletextCache() { return nullptr; }
+ virtual void LoadPage(int p, int sp, unsigned char* buffer) {}
+
+ virtual int GetChapterCount() const { return 0; }
+ virtual int GetChapter() const { return -1; }
+ virtual void GetChapterName(std::string& strChapterName, int chapterIdx = -1) const {}
+ virtual int64_t GetChapterPos(int chapterIdx = -1) const { return 0; }
+ virtual int SeekChapter(int iChapter) { return -1; }
+// virtual bool GetChapterInfo(int chapter, SChapterInfo &info) { return false; }
+
+ virtual void SeekTime(int64_t iTime = 0) {}
+ /*
+ \brief seek relative to current time, returns false if not implemented by player
+ \param iTime The time in milliseconds to seek. A positive value will seek forward, a negative backward.
+ \return True if the player supports relative seeking, otherwise false
+ */
+ virtual bool SeekTimeRelative(int64_t iTime) { return false; }
+
+ /*!
+ \brief Sets the current time. This
+ can be used for injecting the current time.
+ This is not to be confused with a seek. It just
+ can be used if endless streams contain multiple
+ tracks in reality (like with airtunes)
+ */
+ virtual void SetTime(int64_t time) { }
+
+ /*!
+ \brief Set the total time in milliseconds
+ this can be used for injecting the duration in case
+ its not available in the underlaying decoder (airtunes for example)
+ */
+ virtual void SetTotalTime(int64_t time) { }
+ virtual void SetSpeed(float speed) = 0;
+ virtual void SetTempo(float tempo) {}
+ virtual bool SupportsTempo() const { return false; }
+ virtual void FrameAdvance(int frames) {}
+
+ //Returns true if not playback (paused or stopped being filled)
+ virtual bool IsCaching() const { return false; }
+ //Cache filled in Percent
+ virtual int GetCacheLevel() const { return -1; }
+
+ virtual bool IsInMenu() const { return false; }
+
+ /*!
+ * \brief Get the supported menu type
+ * \return The supported menu type
+ */
+ virtual MenuType GetSupportedMenuType() const { return MenuType::NONE; }
+
+ virtual void DoAudioWork() {}
+ virtual bool OnAction(const CAction& action) { return false; }
+
+ //returns a state that is needed for resuming from a specific time
+ virtual std::string GetPlayerState() { return ""; }
+ virtual bool SetPlayerState(const std::string& state) { return false; }
+
+ virtual void GetAudioCapabilities(std::vector<int>& audioCaps) const
+ {
+ audioCaps.assign(1, IPC_AUD_ALL);
+ }
+ /*!
+ \brief define the subtitle capabilities of the player
+ */
+ virtual void GetSubtitleCapabilities(std::vector<int>& subCaps) const
+ {
+ subCaps.assign(1, IPC_SUBS_ALL);
+ }
+
+ /*!
+ \brief hook into render loop of render thread
+ */
+ virtual void Render(bool clear, uint32_t alpha = 255, bool gui = true) {}
+ virtual void FlushRenderer() {}
+ virtual void SetRenderViewMode(int mode, float zoom, float par, float shift, bool stretch) {}
+ virtual float GetRenderAspectRatio() const { return 1.0; }
+ virtual void TriggerUpdateResolution() {}
+ virtual bool IsRenderingVideo() const { return false; }
+
+ virtual bool Supports(EINTERLACEMETHOD method) const { return false; }
+ virtual EINTERLACEMETHOD GetDeinterlacingMethodDefault() const
+ {
+ return EINTERLACEMETHOD::VS_INTERLACEMETHOD_NONE;
+ }
+ virtual bool Supports(ESCALINGMETHOD method) const { return false; }
+ virtual bool Supports(ERENDERFEATURE feature) const { return false; }
+
+ virtual unsigned int RenderCaptureAlloc() { return 0; }
+ virtual void RenderCaptureRelease(unsigned int captureId) {}
+ virtual void RenderCapture(unsigned int captureId,
+ unsigned int width,
+ unsigned int height,
+ int flags)
+ {
+ }
+ virtual bool RenderCaptureGetPixels(unsigned int captureId,
+ unsigned int millis,
+ uint8_t* buffer,
+ unsigned int size)
+ {
+ return false;
+ }
+
+ // video and audio settings
+ virtual CVideoSettings GetVideoSettings() const { return CVideoSettings(); }
+ virtual void SetVideoSettings(CVideoSettings& settings) {}
+
+ /*!
+ * \brief Check if any players are playing a game
+ *
+ * \return True if at least one player has an input device attached to the
+ * game, false otherwise
+ */
+ virtual bool HasGameAgent() const { return false; }
+
+ std::string m_name;
+ std::string m_type;
+
+protected:
+ IPlayerCallback& m_callback;
+};
diff --git a/xbmc/cores/IPlayerCallback.h b/xbmc/cores/IPlayerCallback.h
new file mode 100644
index 0000000..3c1d99b
--- /dev/null
+++ b/xbmc/cores/IPlayerCallback.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "VideoSettings.h"
+
+#include <stdint.h>
+
+class CFileItem;
+class CBookmark;
+
+class IPlayerCallback
+{
+public:
+ virtual ~IPlayerCallback() = default;
+ virtual void OnPlayBackEnded() = 0;
+ virtual void OnPlayBackStarted(const CFileItem &file) = 0;
+ virtual void OnPlayerCloseFile(const CFileItem& file, const CBookmark& bookmark) {}
+ virtual void OnPlayBackPaused() {}
+ virtual void OnPlayBackResumed() {}
+ virtual void OnPlayBackStopped() = 0;
+ virtual void OnPlayBackError() = 0;
+ virtual void OnQueueNextItem() = 0;
+ virtual void OnPlayBackSeek(int64_t iTime, int64_t seekOffset) {}
+ virtual void OnPlayBackSeekChapter(int iChapter) {}
+ virtual void OnPlayBackSpeedChanged(int iSpeed) {}
+ virtual void OnAVChange() {}
+ virtual void OnAVStarted(const CFileItem& file) {}
+ virtual void RequestVideoSettings(const CFileItem& fileItem) {}
+ virtual void StoreVideoSettings(const CFileItem& fileItem, const CVideoSettings& vs) {}
+};
diff --git a/xbmc/cores/MenuType.h b/xbmc/cores/MenuType.h
new file mode 100644
index 0000000..221d59d
--- /dev/null
+++ b/xbmc/cores/MenuType.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 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
+
+/*!
+* \brief Represents a Menu type (e.g. dvd menus, bluray menus, etc)
+*/
+enum class MenuType
+{
+ /*! No supported menu */
+ NONE,
+ /*! Supports native menus (e.g. those provided natively by blurays or dvds) */
+ NATIVE,
+ /*! Application specific menu such as the simplified menu for blurays */
+ SIMPLIFIED
+};
diff --git a/xbmc/cores/RetroPlayer/CMakeLists.txt b/xbmc/cores/RetroPlayer/CMakeLists.txt
new file mode 100644
index 0000000..3e78c54
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES RetroPlayer.cpp
+ RetroPlayerAutoSave.cpp
+ RetroPlayerInput.cpp
+ RetroPlayerUtils.cpp
+)
+
+set(HEADERS RetroPlayer.h
+ RetroPlayerAutoSave.h
+ RetroPlayerInput.h
+ RetroPlayerTypes.h
+ RetroPlayerUtils.h
+)
+
+core_add_library(retroplayer)
diff --git a/xbmc/cores/RetroPlayer/RetroPlayer.cpp b/xbmc/cores/RetroPlayer/RetroPlayer.cpp
new file mode 100644
index 0000000..030b851
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/RetroPlayer.cpp
@@ -0,0 +1,698 @@
+/*
+ * 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 "RetroPlayer.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "RetroPlayerAutoSave.h"
+#include "RetroPlayerInput.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "cores/DataCacheCore.h"
+#include "cores/IPlayerCallback.h"
+#include "cores/RetroPlayer/cheevos/Cheevos.h"
+#include "cores/RetroPlayer/guibridge/GUIGameMessenger.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guiplayback/GUIPlaybackControl.h"
+#include "cores/RetroPlayer/playback/IPlayback.h"
+#include "cores/RetroPlayer/playback/RealtimePlayback.h"
+#include "cores/RetroPlayer/playback/ReversiblePlayback.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "cores/RetroPlayer/rendering/RPRenderManager.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
+#include "cores/RetroPlayer/streams/RPStreamManager.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "games/GameServices.h"
+#include "games/GameSettings.h"
+#include "games/GameUtils.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/tags/GameInfoTag.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace GAME;
+using namespace RETRO;
+
+CRetroPlayer::CRetroPlayer(IPlayerCallback& callback)
+ : IPlayer(callback), m_gameServices(CServiceBroker::GetGameServices())
+{
+ ResetPlayback();
+ CServiceBroker::GetWinSystem()->RegisterRenderLoop(this);
+}
+
+CRetroPlayer::~CRetroPlayer()
+{
+ CServiceBroker::GetWinSystem()->UnregisterRenderLoop(this);
+ CloseFile();
+}
+
+bool CRetroPlayer::OpenFile(const CFileItem& file, const CPlayerOptions& options)
+{
+ CFileItem fileCopy(file);
+
+ std::string savestatePath;
+
+ // When playing a game, set the game client that we'll use to open the game.
+ // This will prompt the user to select a savestate if there are any.
+ // If there are no savestates, or the user wants to create a new savestate
+ // it will prompt the user to select a game client
+ if (!GAME::CGameUtils::FillInGameClient(fileCopy, savestatePath))
+ {
+ CLog::Log(LOGINFO,
+ "RetroPlayer[PLAYER]: No compatible game client selected, aborting playback");
+ return false;
+ }
+
+ // Check if we should open in standalone mode
+ const bool bStandalone = fileCopy.GetPath().empty();
+
+ m_processInfo.reset(CRPProcessInfo::CreateInstance());
+ if (!m_processInfo)
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[PLAYER]: Failed to create - no process info registered");
+ return false;
+ }
+
+ m_processInfo->SetDataCache(&CServiceBroker::GetDataCacheCore());
+ m_processInfo->ResetInfo();
+
+ m_guiMessenger = std::make_unique<CGUIGameMessenger>(*m_processInfo);
+ m_renderManager.reset(new CRPRenderManager(*m_processInfo));
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (IsPlaying())
+ CloseFile();
+
+ PrintGameInfo(fileCopy);
+
+ bool bSuccess = false;
+
+ std::string gameClientId = fileCopy.GetGameInfoTag()->GetGameClient();
+
+ ADDON::AddonPtr addon;
+ if (gameClientId.empty())
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[PLAYER]: Can't play game, no game client was passed!");
+ }
+ else if (!CServiceBroker::GetAddonMgr().GetAddon(gameClientId, addon, ADDON::AddonType::GAMEDLL,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[PLAYER]: Can't find add-on {} for game file!", gameClientId);
+ }
+ else
+ {
+ m_gameClient = std::static_pointer_cast<CGameClient>(addon);
+ if (m_gameClient->Initialize())
+ {
+ m_streamManager.reset(new CRPStreamManager(*m_renderManager, *m_processInfo));
+
+ // Initialize input
+ m_input = std::make_unique<CRetroPlayerInput>(CServiceBroker::GetPeripherals(),
+ *m_processInfo, m_gameClient);
+ m_input->StartAgentManager();
+
+ if (!bStandalone)
+ {
+ std::string redactedPath = CURL::GetRedacted(fileCopy.GetPath());
+ CLog::Log(LOGINFO, "RetroPlayer[PLAYER]: Opening: {}", redactedPath);
+ bSuccess = m_gameClient->OpenFile(fileCopy, *m_streamManager, m_input.get());
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "RetroPlayer[PLAYER]: Opening standalone");
+ bSuccess = m_gameClient->OpenStandalone(*m_streamManager, m_input.get());
+ }
+
+ if (bSuccess)
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Using game client {}", gameClientId);
+ else
+ CLog::Log(LOGERROR, "RetroPlayer[PLAYER]: Failed to open file using {}", gameClientId);
+ }
+ else
+ CLog::Log(LOGERROR, "RetroPlayer[PLAYER]: Failed to initialize {}", gameClientId);
+ }
+
+ if (bSuccess && !bStandalone)
+ {
+ CSavestateDatabase savestateDb;
+
+ std::unique_ptr<ISavestate> save = CSavestateDatabase::AllocateSavestate();
+ if (savestateDb.GetSavestate(savestatePath, *save))
+ {
+ // Check if game client is the same
+ if (save->GameClientID() != m_gameClient->ID())
+ {
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(save->GameClientID(), addon,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ // Warn the user that continuing with a different game client will
+ // overwrite the save
+ bool dummy;
+ if (!CGUIDialogYesNo::ShowAndGetInput(
+ 438, StringUtils::Format(g_localizeStrings.Get(35217), addon->Name()), dummy, 222,
+ 35218, 0))
+ bSuccess = false;
+ }
+ }
+ }
+ }
+
+ if (bSuccess)
+ {
+ // Switch to fullscreen
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SWITCHTOFULLSCREEN);
+
+ m_cheevos = std::make_shared<CCheevos>(m_gameClient.get(),
+ m_gameServices.GameSettings().GetRAUsername(),
+ m_gameServices.GameSettings().GetRAToken());
+
+ m_cheevos->EnableRichPresence();
+
+ // Initialize gameplay
+ CreatePlayback(savestatePath);
+ RegisterWindowCallbacks();
+ m_playbackControl.reset(new CGUIPlaybackControl(*this));
+ m_callback.OnPlayBackStarted(fileCopy);
+ m_callback.OnAVStarted(fileCopy);
+ if (!bStandalone)
+ m_autoSave.reset(new CRetroPlayerAutoSave(*this, m_gameServices.GameSettings()));
+
+ // Set video framerate
+ m_processInfo->SetVideoFps(static_cast<float>(m_gameClient->GetFrameRate()));
+ }
+ else
+ {
+ m_input.reset();
+ m_streamManager.reset();
+ if (m_gameClient)
+ m_gameClient->Unload();
+ m_gameClient.reset();
+ }
+
+ return bSuccess;
+}
+
+bool CRetroPlayer::CloseFile(bool reopen /* = false */)
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Closing file");
+
+ m_autoSave.reset();
+
+ UnregisterWindowCallbacks();
+
+ m_playbackControl.reset();
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (m_gameClient && m_gameServices.GameSettings().AutosaveEnabled())
+ {
+ std::string savePath = m_playback->CreateSavestate(true);
+ if (!savePath.empty())
+ CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Saved state to {}", CURL::GetRedacted(savePath));
+ else
+ CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Failed to save state at close");
+ }
+
+ m_playback.reset();
+
+ if (m_input)
+ m_input->StopAgentManager();
+
+ m_cheevos.reset();
+
+ if (m_gameClient)
+ m_gameClient->CloseFile();
+
+ m_input.reset();
+
+ m_streamManager.reset();
+
+ if (m_gameClient)
+ m_gameClient->Unload();
+ m_gameClient.reset();
+
+ m_renderManager.reset();
+ m_processInfo.reset();
+
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Playback ended");
+ m_callback.OnPlayBackEnded();
+
+ return true;
+}
+
+bool CRetroPlayer::IsPlaying() const
+{
+ if (m_gameClient)
+ return m_gameClient->IsPlaying();
+ return false;
+}
+
+bool CRetroPlayer::CanPause() const
+{
+ return m_playback->CanPause();
+}
+
+void CRetroPlayer::Pause()
+{
+ if (!CanPause())
+ return;
+
+ float speed;
+
+ if (m_playback->GetSpeed() == 0.0)
+ speed = 1.0f;
+ else
+ speed = 0.0f;
+
+ SetSpeed(speed);
+}
+
+bool CRetroPlayer::CanSeek() const
+{
+ return m_playback->CanSeek();
+}
+
+void CRetroPlayer::Seek(bool bPlus /* = true */,
+ bool bLargeStep /* = false */,
+ bool bChapterOverride /* = false */)
+{
+ if (!CanSeek())
+ return;
+
+ if (m_gameClient)
+ {
+ //! @todo
+ /*
+ if (bPlus)
+ {
+ if (bLargeStep)
+ m_playback->BigSkipForward();
+ else
+ m_playback->SmallSkipForward();
+ }
+ else
+ {
+ if (bLargeStep)
+ m_playback->BigSkipBackward();
+ else
+ m_playback->SmallSkipBackward();
+ }
+ */
+ }
+}
+
+void CRetroPlayer::SeekPercentage(float fPercent /* = 0 */)
+{
+ if (!CanSeek())
+ return;
+
+ if (fPercent < 0.0f)
+ fPercent = 0.0f;
+ else if (fPercent > 100.0f)
+ fPercent = 100.0f;
+
+ uint64_t totalTime = GetTotalTime();
+ if (totalTime != 0)
+ SeekTime(static_cast<int64_t>(totalTime * fPercent / 100.0f));
+}
+
+float CRetroPlayer::GetCachePercentage() const
+{
+ const float cacheMs = static_cast<float>(m_playback->GetCacheTimeMs());
+ const float totalMs = static_cast<float>(m_playback->GetTotalTimeMs());
+
+ if (totalMs != 0.0f)
+ return cacheMs / totalMs * 100.0f;
+
+ return 0.0f;
+}
+
+void CRetroPlayer::SetMute(bool bOnOff)
+{
+ if (m_streamManager)
+ m_streamManager->EnableAudio(!bOnOff);
+}
+
+void CRetroPlayer::SeekTime(int64_t iTime /* = 0 */)
+{
+ if (!CanSeek())
+ return;
+
+ m_playback->SeekTimeMs(static_cast<unsigned int>(iTime));
+}
+
+bool CRetroPlayer::SeekTimeRelative(int64_t iTime)
+{
+ if (!CanSeek())
+ return false;
+
+ SeekTime(GetTime() + iTime);
+
+ return true;
+}
+
+uint64_t CRetroPlayer::GetTime()
+{
+ return m_playback->GetTimeMs();
+}
+
+uint64_t CRetroPlayer::GetTotalTime()
+{
+ return m_playback->GetTotalTimeMs();
+}
+
+void CRetroPlayer::SetSpeed(float speed)
+{
+ if (m_playback->GetSpeed() != static_cast<double>(speed))
+ {
+ if (speed == 1.0f)
+ m_callback.OnPlayBackResumed();
+ else if (speed == 0.0f)
+ m_callback.OnPlayBackPaused();
+
+ SetSpeedInternal(static_cast<double>(speed));
+
+ if (speed == 0.0f)
+ {
+ const int dialogId = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog();
+ if (dialogId == WINDOW_FULLSCREEN_GAME)
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Opening OSD via speed change ({:f})", speed);
+ OpenOSD();
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Closing OSD via speed change ({:f})", speed);
+ CloseOSD();
+ }
+ }
+}
+
+bool CRetroPlayer::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_PLAYER_RESET:
+ {
+ if (m_gameClient)
+ {
+ float speed = static_cast<float>(m_playback->GetSpeed());
+
+ m_playback->SetSpeed(0.0);
+
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Sending reset command via ACTION_PLAYER_RESET");
+ m_cheevos->ResetRuntime();
+ m_gameClient->Input().HardwareReset();
+
+ // If rewinding or paused, begin playback
+ if (speed <= 0.0f)
+ speed = 1.0f;
+
+ SetSpeed(speed);
+ }
+ return true;
+ }
+ case ACTION_SHOW_OSD:
+ {
+ if (m_gameClient)
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Closing OSD via ACTION_SHOW_OSD");
+ CloseOSD();
+ return true;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
+
+std::string CRetroPlayer::GetPlayerState()
+{
+ std::string savestatePath;
+
+ if (m_autoSave)
+ {
+ savestatePath = m_playback->CreateSavestate(true);
+ if (savestatePath.empty())
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Continuing without saving");
+ m_autoSave.reset();
+ }
+ }
+ return savestatePath;
+}
+
+bool CRetroPlayer::SetPlayerState(const std::string& state)
+{
+ return m_playback->LoadSavestate(state);
+}
+
+void CRetroPlayer::FrameMove()
+{
+ if (m_renderManager)
+ m_renderManager->FrameMove();
+
+ if (m_playbackControl)
+ m_playbackControl->FrameMove();
+
+ if (m_processInfo)
+ m_processInfo->SetPlayTimes(0, GetTime(), 0, GetTotalTime());
+}
+
+void CRetroPlayer::Render(bool clear, uint32_t alpha /* = 255 */, bool gui /* = true */)
+{
+ // Performed by callbacks
+}
+
+bool CRetroPlayer::IsRenderingVideo() const
+{
+ return true;
+}
+
+bool CRetroPlayer::HasGameAgent() const
+{
+ if (m_gameClient)
+ return m_gameClient->Input().HasAgent();
+
+ return false;
+}
+
+std::string CRetroPlayer::GameClientID() const
+{
+ if (m_gameClient)
+ return m_gameClient->ID();
+
+ return "";
+}
+
+std::string CRetroPlayer::GetPlayingGame() const
+{
+ if (m_gameClient)
+ return m_gameClient->GetGamePath();
+
+ return "";
+}
+
+std::string CRetroPlayer::CreateSavestate(bool autosave)
+{
+ if (m_playback)
+ return m_playback->CreateSavestate(autosave);
+
+ return "";
+}
+
+bool CRetroPlayer::UpdateSavestate(const std::string& savestatePath)
+{
+ if (m_playback)
+ return !m_playback->CreateSavestate(false, savestatePath).empty();
+
+ return false;
+}
+
+bool CRetroPlayer::LoadSavestate(const std::string& savestatePath)
+{
+ if (m_playback)
+ return m_playback->LoadSavestate(savestatePath);
+
+ return false;
+}
+
+void CRetroPlayer::FreeSavestateResources(const std::string& savestatePath)
+{
+ if (m_renderManager)
+ m_renderManager->ClearVideoFrame(savestatePath);
+}
+
+void CRetroPlayer::CloseOSDCallback()
+{
+ CloseOSD();
+}
+
+void CRetroPlayer::SetPlaybackSpeed(double speed)
+{
+ if (m_playback)
+ {
+ if (m_playback->GetSpeed() != speed)
+ {
+ if (speed == 1.0)
+ {
+ IPlayerCallback* callback = &m_callback;
+ CServiceBroker::GetJobManager()->Submit([callback]() { callback->OnPlayBackResumed(); },
+ CJob::PRIORITY_NORMAL);
+ }
+ else if (speed == 0.0)
+ {
+ IPlayerCallback* callback = &m_callback;
+ CServiceBroker::GetJobManager()->Submit([callback]() { callback->OnPlayBackPaused(); },
+ CJob::PRIORITY_NORMAL);
+ }
+ }
+ }
+
+ SetSpeedInternal(speed);
+}
+
+void CRetroPlayer::EnableInput(bool bEnable)
+{
+ if (m_input)
+ m_input->EnableInput(bEnable);
+}
+
+bool CRetroPlayer::IsAutoSaveEnabled() const
+{
+ return m_playback->GetSpeed() > 0.0;
+}
+
+std::string CRetroPlayer::CreateAutosave()
+{
+ return m_playback->CreateSavestate(true);
+}
+
+void CRetroPlayer::SetSpeedInternal(double speed)
+{
+ OnSpeedChange(speed);
+
+ if (speed == 0.0)
+ m_playback->PauseAsync();
+ else
+ m_playback->SetSpeed(speed);
+}
+
+void CRetroPlayer::OnSpeedChange(double newSpeed)
+{
+ m_streamManager->EnableAudio(newSpeed == 1.0);
+ m_input->SetSpeed(newSpeed);
+ m_renderManager->SetSpeed(newSpeed);
+ m_processInfo->SetSpeed(static_cast<float>(newSpeed));
+}
+
+void CRetroPlayer::CreatePlayback(const std::string& savestatePath)
+{
+ if (m_gameClient->RequiresGameLoop())
+ {
+ m_playback->Deinitialize();
+ m_playback = std::make_unique<CReversiblePlayback>(
+ m_gameClient.get(), *m_renderManager, m_cheevos.get(), *m_guiMessenger,
+ m_gameClient->GetFrameRate(), m_gameClient->GetSerializeSize());
+ }
+ else
+ ResetPlayback();
+
+ if (!savestatePath.empty())
+ {
+ const bool bStandalone = m_gameClient->GetGamePath().empty();
+ if (!bStandalone)
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Loading savestate");
+
+ if (!SetPlayerState(savestatePath))
+ CLog::Log(LOGERROR, "RetroPlayer[SAVE]: Failed to load savestate");
+ }
+ }
+
+ m_playback->Initialize();
+}
+
+void CRetroPlayer::ResetPlayback()
+{
+ // Called from the constructor, m_playback might not be initialized
+ if (m_playback)
+ m_playback->Deinitialize();
+
+ m_playback.reset(new CRealtimePlayback);
+}
+
+void CRetroPlayer::OpenOSD()
+{
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_DIALOG_GAME_OSD);
+}
+
+void CRetroPlayer::CloseOSD()
+{
+ CServiceBroker::GetGUI()->GetWindowManager().CloseDialogs(true);
+}
+
+void CRetroPlayer::RegisterWindowCallbacks()
+{
+ m_gameServices.GameRenderManager().RegisterPlayer(m_renderManager->GetGUIRenderTargetFactory(),
+ m_renderManager.get(), this);
+}
+
+void CRetroPlayer::UnregisterWindowCallbacks()
+{
+ m_gameServices.GameRenderManager().UnregisterPlayer();
+}
+
+void CRetroPlayer::PrintGameInfo(const CFileItem& file) const
+{
+ const CGameInfoTag* tag = file.GetGameInfoTag();
+ if (tag)
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: ---------------------------------------");
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Game tag loaded");
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: URL: {}", tag->GetURL());
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Title: {}", tag->GetTitle());
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Platform: {}", tag->GetPlatform());
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Genres: {}",
+ StringUtils::Join(tag->GetGenres(), ", "));
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Developer: {}", tag->GetDeveloper());
+ if (tag->GetYear() > 0)
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Year: {}", tag->GetYear());
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Game Code: {}", tag->GetID());
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Region: {}", tag->GetRegion());
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Publisher: {}", tag->GetPublisher());
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Format: {}", tag->GetFormat());
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Cartridge type: {}", tag->GetCartridgeType());
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: Game client: {}", tag->GetGameClient());
+ CLog::Log(LOGDEBUG, "RetroPlayer[PLAYER]: ---------------------------------------");
+ }
+}
diff --git a/xbmc/cores/RetroPlayer/RetroPlayer.h b/xbmc/cores/RetroPlayer/RetroPlayer.h
new file mode 100644
index 0000000..fc383c9
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/RetroPlayer.h
@@ -0,0 +1,145 @@
+/*
+ * 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 "RetroPlayerAutoSave.h"
+#include "cores/IPlayer.h"
+#include "cores/RetroPlayer/guibridge/IGameCallback.h"
+#include "cores/RetroPlayer/playback/IPlaybackControl.h"
+#include "games/GameTypes.h"
+#include "guilib/DispResource.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameServices;
+}
+
+namespace RETRO
+{
+class CCheevos;
+class CGUIGameMessenger;
+class CRetroPlayerInput;
+class CRPProcessInfo;
+class CRPRenderManager;
+class CRPStreamManager;
+class IPlayback;
+
+class CRetroPlayer : public IPlayer,
+ public IRenderLoop,
+ public IGameCallback,
+ public IPlaybackCallback,
+ public IAutoSaveCallback
+{
+public:
+ explicit CRetroPlayer(IPlayerCallback& callback);
+ ~CRetroPlayer() override;
+
+ // implementation of IPlayer
+ bool OpenFile(const CFileItem& file, const CPlayerOptions& options) override;
+ bool CloseFile(bool reopen = false) override;
+ bool IsPlaying() const override;
+ bool CanPause() const override;
+ void Pause() override;
+ bool HasVideo() const override { return true; }
+ bool HasAudio() const override { return true; }
+ bool HasGame() const override { return true; }
+ bool CanSeek() const override;
+ void Seek(bool bPlus = true, bool bLargeStep = false, bool bChapterOverride = false) override;
+ void SeekPercentage(float fPercent = 0) override;
+ float GetCachePercentage() const override;
+ void SetMute(bool bOnOff) override;
+ void SeekTime(int64_t iTime = 0) override;
+ bool SeekTimeRelative(int64_t iTime) override;
+ void SetSpeed(float speed) override;
+ bool OnAction(const CAction& action) override;
+ std::string GetPlayerState() override;
+ bool SetPlayerState(const std::string& state) override;
+ void FrameMove() override;
+ void Render(bool clear, uint32_t alpha = 255, bool gui = true) override;
+ bool IsRenderingVideo() const override;
+ bool HasGameAgent() const override;
+
+ // Implementation of IGameCallback
+ std::string GameClientID() const override;
+ std::string GetPlayingGame() const override;
+ std::string CreateSavestate(bool autosave) override;
+ bool UpdateSavestate(const std::string& savestatePath) override;
+ bool LoadSavestate(const std::string& savestatePath) override;
+ void FreeSavestateResources(const std::string& savestatePath) override;
+ void CloseOSDCallback() override;
+
+ // Implementation of IPlaybackCallback
+ void SetPlaybackSpeed(double speed) override;
+ void EnableInput(bool bEnable) override;
+
+ // Implementation of IAutoSaveCallback
+ bool IsAutoSaveEnabled() const override;
+ std::string CreateAutosave() override;
+
+private:
+ void SetSpeedInternal(double speed);
+
+ /*!
+ * \brief Called when the speed changes
+ * \param newSpeed The new speed, possibly equal to the previous speed
+ */
+ void OnSpeedChange(double newSpeed);
+
+ // Playback functions
+ void CreatePlayback(const std::string& savestatePath);
+ void ResetPlayback();
+
+ /*!
+ * \brief Opens the OSD
+ */
+ void OpenOSD();
+
+ /*!
+ * \brief Closes the OSD and shows the FullscreenGame window
+ */
+ void CloseOSD();
+
+ void RegisterWindowCallbacks();
+ void UnregisterWindowCallbacks();
+
+ /**
+ * \brief Dump game information (if any) to the debug log.
+ */
+ void PrintGameInfo(const CFileItem& file) const;
+
+ uint64_t GetTime();
+ uint64_t GetTotalTime();
+
+ // Construction parameters
+ GAME::CGameServices& m_gameServices;
+
+ // Subsystems
+ std::unique_ptr<CRPProcessInfo> m_processInfo;
+ std::unique_ptr<CGUIGameMessenger> m_guiMessenger;
+ std::unique_ptr<CRPRenderManager> m_renderManager;
+ std::unique_ptr<CRPStreamManager> m_streamManager;
+ std::unique_ptr<CRetroPlayerInput> m_input;
+ std::unique_ptr<IPlayback> m_playback;
+ std::unique_ptr<IPlaybackControl> m_playbackControl;
+ std::unique_ptr<CRetroPlayerAutoSave> m_autoSave;
+ std::shared_ptr<CCheevos> m_cheevos;
+
+ // Game parameters
+ GAME::GameClientPtr m_gameClient;
+
+ // Synchronization parameters
+ CCriticalSection m_mutex;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/RetroPlayerAutoSave.cpp b/xbmc/cores/RetroPlayer/RetroPlayerAutoSave.cpp
new file mode 100644
index 0000000..872bfa8
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/RetroPlayerAutoSave.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017-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 "RetroPlayerAutoSave.h"
+
+#include "URL.h"
+#include "games/GameSettings.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace RETRO;
+using namespace std::chrono_literals;
+
+namespace
+{
+constexpr auto AUTOSAVE_DURATION_SECS = 10s; // Auto-save every 10 seconds
+}
+
+CRetroPlayerAutoSave::CRetroPlayerAutoSave(IAutoSaveCallback& callback,
+ GAME::CGameSettings& settings)
+ : CThread("CRetroPlayerAutoSave"), m_callback(callback), m_settings(settings)
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Initializing autosave");
+
+ Create(false);
+}
+
+CRetroPlayerAutoSave::~CRetroPlayerAutoSave()
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Deinitializing autosave");
+
+ StopThread();
+}
+
+void CRetroPlayerAutoSave::Process()
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Autosave thread started");
+
+ while (!m_bStop)
+ {
+ CThread::Sleep(AUTOSAVE_DURATION_SECS);
+
+ if (m_bStop)
+ break;
+
+ if (!m_settings.AutosaveEnabled())
+ continue;
+
+ if (m_callback.IsAutoSaveEnabled())
+ {
+ std::string savePath = m_callback.CreateAutosave();
+ if (!savePath.empty())
+ CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Saved state to {}", CURL::GetRedacted(savePath));
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "RetroPlayer[SAVE]: Autosave thread ended");
+}
diff --git a/xbmc/cores/RetroPlayer/RetroPlayerAutoSave.h b/xbmc/cores/RetroPlayer/RetroPlayerAutoSave.h
new file mode 100644
index 0000000..f34ab09
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/RetroPlayerAutoSave.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017-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/Thread.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClient;
+class CGameSettings;
+} // namespace GAME
+
+namespace RETRO
+{
+class IAutoSaveCallback
+{
+public:
+ virtual ~IAutoSaveCallback() = default;
+
+ virtual bool IsAutoSaveEnabled() const = 0;
+ virtual std::string CreateAutosave() = 0;
+};
+
+class CRetroPlayerAutoSave : protected CThread
+{
+public:
+ explicit CRetroPlayerAutoSave(IAutoSaveCallback& callback, GAME::CGameSettings& settings);
+
+ ~CRetroPlayerAutoSave() override;
+
+protected:
+ // implementation of CThread
+ void Process() override;
+
+private:
+ // Construction parameters
+ IAutoSaveCallback& m_callback;
+ GAME::CGameSettings& m_settings;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/RetroPlayerDefines.h b/xbmc/cores/RetroPlayer/RetroPlayerDefines.h
new file mode 100644
index 0000000..4d84641
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/RetroPlayerDefines.h
@@ -0,0 +1,12 @@
+/*
+ * Copyright (C) 2016-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
+
+#define GAME_STREAM_VIDEO_ID 1
+#define GAME_STREAM_AUDIO_ID 2
diff --git a/xbmc/cores/RetroPlayer/RetroPlayerInput.cpp b/xbmc/cores/RetroPlayer/RetroPlayerInput.cpp
new file mode 100644
index 0000000..0e6f813
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/RetroPlayerInput.cpp
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017-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 "RetroPlayerInput.h"
+
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "peripherals/EventPollHandle.h"
+#include "peripherals/Peripherals.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRetroPlayerInput::CRetroPlayerInput(PERIPHERALS::CPeripherals& peripheralManager,
+ CRPProcessInfo& processInfo,
+ GAME::GameClientPtr gameClient)
+ : m_peripheralManager(peripheralManager),
+ m_processInfo(processInfo),
+ m_gameClient(std::move(gameClient))
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[INPUT]: Initializing input");
+
+ m_inputPollHandle = m_peripheralManager.RegisterEventPoller();
+}
+
+CRetroPlayerInput::~CRetroPlayerInput()
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[INPUT]: Deinitializing input");
+
+ m_inputPollHandle.reset();
+}
+
+void CRetroPlayerInput::StartAgentManager()
+{
+ if (!m_bAgentManagerStarted)
+ {
+ m_bAgentManagerStarted = true;
+ m_processInfo.GetRenderContext().StartAgentManager(m_gameClient);
+ }
+}
+
+void CRetroPlayerInput::StopAgentManager()
+{
+ if (m_bAgentManagerStarted)
+ {
+ m_bAgentManagerStarted = false;
+ m_processInfo.GetRenderContext().StopAgentManager();
+ }
+}
+
+void CRetroPlayerInput::SetSpeed(double speed)
+{
+ if (speed != 0)
+ m_inputPollHandle->Activate();
+ else
+ m_inputPollHandle->Deactivate();
+}
+
+void CRetroPlayerInput::EnableInput(bool bEnabled)
+{
+ m_bEnabled = bEnabled;
+}
+
+void CRetroPlayerInput::PollInput()
+{
+ m_inputPollHandle->HandleEvents(true);
+}
diff --git a/xbmc/cores/RetroPlayer/RetroPlayerInput.h b/xbmc/cores/RetroPlayer/RetroPlayerInput.h
new file mode 100644
index 0000000..eb243b5
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/RetroPlayerInput.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017-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 "games/GameTypes.h"
+#include "games/addons/GameClientCallbacks.h"
+#include "peripherals/PeripheralTypes.h"
+
+namespace PERIPHERALS
+{
+class CPeripherals;
+}
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfo;
+
+class CRetroPlayerInput : public GAME::IGameInputCallback
+{
+public:
+ CRetroPlayerInput(PERIPHERALS::CPeripherals& peripheralManager,
+ CRPProcessInfo& processInfo,
+ GAME::GameClientPtr gameClient);
+ ~CRetroPlayerInput() override;
+
+ // Lifecycle functions
+ void StartAgentManager();
+ void StopAgentManager();
+
+ // Input functions
+ void SetSpeed(double speed);
+ void EnableInput(bool bEnabled);
+
+ // implementation of IGameInputCallback
+ bool AcceptsInput() const override { return m_bEnabled; }
+ void PollInput() override;
+
+private:
+ // Construction parameters
+ PERIPHERALS::CPeripherals& m_peripheralManager;
+ CRPProcessInfo& m_processInfo;
+
+ // Input variables
+ PERIPHERALS::EventPollHandlePtr m_inputPollHandle;
+ bool m_bEnabled = false;
+
+ // Game parameters
+ const GAME::GameClientPtr m_gameClient;
+ bool m_bAgentManagerStarted{false};
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/RetroPlayerTypes.h b/xbmc/cores/RetroPlayer/RetroPlayerTypes.h
new file mode 100644
index 0000000..af14900
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/RetroPlayerTypes.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017-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 <memory>
+#include <vector>
+
+namespace KODI
+{
+namespace RETRO
+{
+class IRenderBufferPool;
+using RenderBufferPoolPtr = std::shared_ptr<IRenderBufferPool>;
+using RenderBufferPoolVector = std::vector<RenderBufferPoolPtr>;
+
+enum class DataAccess
+{
+ READ_ONLY,
+ WRITE_ONLY,
+ READ_WRITE
+};
+
+enum class DataAlignment
+{
+ DATA_UNALIGNED,
+ DATA_ALIGNED,
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/RetroPlayerUtils.cpp b/xbmc/cores/RetroPlayer/RetroPlayerUtils.cpp
new file mode 100644
index 0000000..42d6e47
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/RetroPlayerUtils.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017-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 "RetroPlayerUtils.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+const char* CRetroPlayerUtils::StretchModeToIdentifier(STRETCHMODE stretchMode)
+{
+ switch (stretchMode)
+ {
+ case STRETCHMODE::Normal:
+ return STRETCHMODE_NORMAL_ID;
+ case STRETCHMODE::Stretch4x3:
+ return STRETCHMODE_STRETCH_4_3_ID;
+ case STRETCHMODE::Fullscreen:
+ return STRETCHMODE_FULLSCREEN_ID;
+ case STRETCHMODE::Original:
+ return STRETCHMODE_ORIGINAL_ID;
+ default:
+ break;
+ }
+
+ return "";
+}
+
+STRETCHMODE CRetroPlayerUtils::IdentifierToStretchMode(const std::string& stretchMode)
+{
+ if (stretchMode == STRETCHMODE_NORMAL_ID)
+ return STRETCHMODE::Normal;
+ else if (stretchMode == STRETCHMODE_STRETCH_4_3_ID)
+ return STRETCHMODE::Stretch4x3;
+ else if (stretchMode == STRETCHMODE_FULLSCREEN_ID)
+ return STRETCHMODE::Fullscreen;
+ else if (stretchMode == STRETCHMODE_ORIGINAL_ID)
+ return STRETCHMODE::Original;
+
+ return STRETCHMODE::Normal;
+}
diff --git a/xbmc/cores/RetroPlayer/RetroPlayerUtils.h b/xbmc/cores/RetroPlayer/RetroPlayerUtils.h
new file mode 100644
index 0000000..27fee5a
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/RetroPlayerUtils.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRetroPlayerUtils
+{
+public:
+ /*!
+ * \brief Convert a stretch mode enum to a short string identifier
+ *
+ * \param stretchMode The stretch mode
+ *
+ * \return A short string identifier specified by GameSettings.h, or an
+ * empty string if the stretch mode is invalid
+ */
+ static const char* StretchModeToIdentifier(STRETCHMODE stretchMode);
+
+ /*!
+ * \brief Convert a stretch mode identifier to an enum
+ *
+ * \param stretchMode The short string identifier, from GameSettings.h,
+ * representing the stretch mode
+ *
+ * \return The stretch mode enum, or STRETCHMODE::Normal if the identifier
+ * is invalid
+ */
+ static STRETCHMODE IdentifierToStretchMode(const std::string& stretchMode);
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/audio/AudioTranslator.cpp b/xbmc/cores/RetroPlayer/audio/AudioTranslator.cpp
new file mode 100644
index 0000000..91f37ec
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/audio/AudioTranslator.cpp
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017-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 "AudioTranslator.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+AEDataFormat CAudioTranslator::TranslatePCMFormat(PCMFormat format)
+{
+ switch (format)
+ {
+ case PCMFormat::FMT_S16NE:
+ return AE_FMT_S16NE;
+ default:
+ break;
+ }
+ return AE_FMT_INVALID;
+}
+
+AEChannel CAudioTranslator::TranslateAudioChannel(AudioChannel channel)
+{
+ switch (channel)
+ {
+ case AudioChannel::CH_FL:
+ return AE_CH_FL;
+ case AudioChannel::CH_FR:
+ return AE_CH_FR;
+ case AudioChannel::CH_FC:
+ return AE_CH_FC;
+ case AudioChannel::CH_LFE:
+ return AE_CH_LFE;
+ case AudioChannel::CH_BL:
+ return AE_CH_BL;
+ case AudioChannel::CH_BR:
+ return AE_CH_BR;
+ case AudioChannel::CH_FLOC:
+ return AE_CH_FLOC;
+ case AudioChannel::CH_FROC:
+ return AE_CH_FROC;
+ case AudioChannel::CH_BC:
+ return AE_CH_BC;
+ case AudioChannel::CH_SL:
+ return AE_CH_SL;
+ case AudioChannel::CH_SR:
+ return AE_CH_SR;
+ case AudioChannel::CH_TFL:
+ return AE_CH_TFL;
+ case AudioChannel::CH_TFR:
+ return AE_CH_TFR;
+ case AudioChannel::CH_TFC:
+ return AE_CH_TFC;
+ case AudioChannel::CH_TC:
+ return AE_CH_TC;
+ case AudioChannel::CH_TBL:
+ return AE_CH_TBL;
+ case AudioChannel::CH_TBR:
+ return AE_CH_TBR;
+ case AudioChannel::CH_TBC:
+ return AE_CH_TBC;
+ case AudioChannel::CH_BLOC:
+ return AE_CH_BLOC;
+ case AudioChannel::CH_BROC:
+ return AE_CH_BROC;
+ default:
+ break;
+ }
+ return AE_CH_NULL;
+}
diff --git a/xbmc/cores/RetroPlayer/audio/AudioTranslator.h b/xbmc/cores/RetroPlayer/audio/AudioTranslator.h
new file mode 100644
index 0000000..f8935d8
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/audio/AudioTranslator.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017-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/Utils/AEChannelData.h"
+#include "cores/RetroPlayer/streams/RetroPlayerStreamTypes.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CAudioTranslator
+{
+public:
+ /*!
+ * \brief Translate audio PCM format (Game API to AudioEngine).
+ * \param format The audio PCM format to translate.
+ * \return Translated audio PCM format.
+ */
+ static AEDataFormat TranslatePCMFormat(PCMFormat format);
+
+ /*!
+ * \brief Translate audio channels (Game API to AudioEngine).
+ * \param format The audio channels to translate.
+ * \return Translated audio channels.
+ */
+ static AEChannel TranslateAudioChannel(AudioChannel channel);
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/audio/CMakeLists.txt b/xbmc/cores/RetroPlayer/audio/CMakeLists.txt
new file mode 100644
index 0000000..1cbb113
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/audio/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES AudioTranslator.cpp
+)
+
+set(HEADERS AudioTranslator.h
+)
+
+core_add_library(rp_audio)
diff --git a/xbmc/cores/RetroPlayer/buffers/BaseRenderBuffer.cpp b/xbmc/cores/RetroPlayer/buffers/BaseRenderBuffer.cpp
new file mode 100644
index 0000000..2aec349
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/BaseRenderBuffer.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017-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 "BaseRenderBuffer.h"
+
+#include "IRenderBufferPool.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CBaseRenderBuffer::CBaseRenderBuffer() : m_refCount(0)
+{
+}
+
+void CBaseRenderBuffer::Acquire()
+{
+ m_refCount++;
+}
+
+void CBaseRenderBuffer::Acquire(std::shared_ptr<IRenderBufferPool> pool)
+{
+ m_refCount++;
+ m_pool = pool;
+}
+
+void CBaseRenderBuffer::Release()
+{
+ if (--m_refCount <= 0 && m_pool)
+ {
+ std::shared_ptr<IRenderBufferPool> pool = m_pool->GetPtr();
+ m_pool.reset();
+ pool->Return(this);
+ }
+}
diff --git a/xbmc/cores/RetroPlayer/buffers/BaseRenderBuffer.h b/xbmc/cores/RetroPlayer/buffers/BaseRenderBuffer.h
new file mode 100644
index 0000000..a88cd0c
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/BaseRenderBuffer.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017-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 "IRenderBuffer.h"
+
+#include <atomic>
+#include <memory>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CBaseRenderBuffer : public IRenderBuffer
+{
+public:
+ CBaseRenderBuffer();
+ ~CBaseRenderBuffer() override = default;
+
+ // Partial implementation of IRenderBuffer
+ void Acquire() override;
+ void Acquire(std::shared_ptr<IRenderBufferPool> pool) override;
+ void Release() override;
+ IRenderBufferPool* GetPool() override { return m_pool.get(); }
+
+protected:
+ // Reference counting
+ std::atomic_int m_refCount;
+
+ // Pool callback
+ std::shared_ptr<IRenderBufferPool> m_pool;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/BaseRenderBufferPool.cpp b/xbmc/cores/RetroPlayer/buffers/BaseRenderBufferPool.cpp
new file mode 100644
index 0000000..5ea42b6
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/BaseRenderBufferPool.cpp
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2017-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 "BaseRenderBufferPool.h"
+
+#include "IRenderBuffer.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPBaseRenderer.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace RETRO;
+
+CBaseRenderBufferPool::~CBaseRenderBufferPool()
+{
+ Flush();
+}
+
+void CBaseRenderBufferPool::RegisterRenderer(CRPBaseRenderer* renderer)
+{
+ std::unique_lock<CCriticalSection> lock(m_rendererMutex);
+
+ m_renderers.push_back(renderer);
+}
+
+void CBaseRenderBufferPool::UnregisterRenderer(CRPBaseRenderer* renderer)
+{
+ std::unique_lock<CCriticalSection> lock(m_rendererMutex);
+
+ m_renderers.erase(std::remove(m_renderers.begin(), m_renderers.end(), renderer),
+ m_renderers.end());
+}
+
+bool CBaseRenderBufferPool::HasVisibleRenderer() const
+{
+ std::unique_lock<CCriticalSection> lock(m_rendererMutex);
+
+ for (auto renderer : m_renderers)
+ {
+ if (renderer->IsVisible())
+ return true;
+ }
+
+ return false;
+}
+
+bool CBaseRenderBufferPool::Configure(AVPixelFormat format)
+{
+ m_format = format;
+
+ if (ConfigureInternal())
+ m_bConfigured = true;
+
+ return m_bConfigured;
+}
+
+IRenderBuffer* CBaseRenderBufferPool::GetBuffer(unsigned int width, unsigned int height)
+{
+ if (!m_bConfigured)
+ return nullptr;
+
+ IRenderBuffer* renderBuffer = nullptr;
+
+ void* header = nullptr;
+
+ if (GetHeaderWithTimeout(header))
+ {
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ for (auto it = m_free.begin(); it != m_free.end(); ++it)
+ {
+ std::unique_ptr<IRenderBuffer>& buffer = *it;
+
+ // Only return buffers of the same dimensions
+ const unsigned int bufferWidth = buffer->GetWidth();
+ const unsigned int bufferHeight = buffer->GetHeight();
+
+ if (bufferWidth == width && bufferHeight == height)
+ {
+ renderBuffer = buffer.release();
+ renderBuffer->SetHeader(header);
+ m_free.erase(it);
+ break;
+ }
+ }
+
+ if (renderBuffer == nullptr)
+ {
+ CLog::Log(LOGDEBUG,
+ "RetroPlayer[RENDER]: Creating render buffer of size {}x{} for buffer pool", width,
+ height);
+
+ std::unique_ptr<IRenderBuffer> renderBufferPtr(CreateRenderBuffer(header));
+ if (renderBufferPtr->Allocate(m_format, width, height))
+ renderBuffer = renderBufferPtr.release();
+ else
+ CLog::Log(LOGERROR, "RetroPlayer[RENDER]: Failed to allocate render buffer");
+ }
+
+ if (renderBuffer != nullptr)
+ {
+ renderBuffer->Acquire(GetPtr());
+ renderBuffer->Update();
+ }
+ }
+
+ return renderBuffer;
+}
+
+void CBaseRenderBufferPool::Return(IRenderBuffer* buffer)
+{
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ buffer->SetLoaded(false);
+ buffer->SetRendered(false);
+
+ std::unique_ptr<IRenderBuffer> bufferPtr(buffer);
+ m_free.emplace_back(std::move(bufferPtr));
+}
+
+void CBaseRenderBufferPool::Prime(unsigned int width, unsigned int height)
+{
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ // Allocate two buffers for double buffering
+ unsigned int bufferCount = 2;
+
+ std::vector<IRenderBuffer*> buffers;
+
+ for (unsigned int i = 0; i < bufferCount; i++)
+ {
+ IRenderBuffer* buffer = GetBuffer(width, height);
+ if (buffer == nullptr)
+ break;
+
+ if (!SendBuffer(buffer))
+ buffers.emplace_back(buffer);
+ }
+
+ for (auto buffer : buffers)
+ buffer->Release();
+}
+
+void CBaseRenderBufferPool::Flush()
+{
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ m_free.clear();
+ m_bConfigured = false;
+}
diff --git a/xbmc/cores/RetroPlayer/buffers/BaseRenderBufferPool.h b/xbmc/cores/RetroPlayer/buffers/BaseRenderBufferPool.h
new file mode 100644
index 0000000..cda1c7c
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/BaseRenderBufferPool.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017-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 "IRenderBufferPool.h"
+#include "threads/CriticalSection.h"
+
+#include <deque>
+#include <memory>
+#include <vector>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CBaseRenderBufferPool : public IRenderBufferPool
+{
+public:
+ CBaseRenderBufferPool() = default;
+ ~CBaseRenderBufferPool() override;
+
+ // Partial implementation of IRenderBufferPool
+ void RegisterRenderer(CRPBaseRenderer* renderer) override;
+ void UnregisterRenderer(CRPBaseRenderer* renderer) override;
+ bool HasVisibleRenderer() const override;
+ bool Configure(AVPixelFormat format) override;
+ bool IsConfigured() const override { return m_bConfigured; }
+ IRenderBuffer* GetBuffer(unsigned int width, unsigned int height) override;
+ void Return(IRenderBuffer* buffer) override;
+ void Prime(unsigned int width, unsigned int height) override;
+ void Flush() override;
+
+ // Buffer properties
+ AVPixelFormat Format() const { return m_format; }
+
+protected:
+ virtual IRenderBuffer* CreateRenderBuffer(void* header = nullptr) = 0;
+ virtual bool ConfigureInternal() { return true; }
+ virtual void* GetHeader(unsigned int timeoutMs = 0) { return nullptr; }
+ virtual bool GetHeaderWithTimeout(void*& header)
+ {
+ header = nullptr;
+ return true;
+ }
+ virtual bool SendBuffer(IRenderBuffer* buffer) { return false; }
+
+ // Configuration parameters
+ bool m_bConfigured = false;
+ AVPixelFormat m_format = AV_PIX_FMT_NONE;
+
+private:
+ // Buffer properties
+ std::deque<std::unique_ptr<IRenderBuffer>> m_free;
+
+ std::vector<CRPBaseRenderer*> m_renderers;
+ mutable CCriticalSection m_rendererMutex;
+ CCriticalSection m_bufferMutex;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/CMakeLists.txt b/xbmc/cores/RetroPlayer/buffers/CMakeLists.txt
new file mode 100644
index 0000000..1dc954a
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/CMakeLists.txt
@@ -0,0 +1,34 @@
+set(SOURCES BaseRenderBuffer.cpp
+ BaseRenderBufferPool.cpp
+ RenderBufferManager.cpp
+)
+
+set(HEADERS BaseRenderBuffer.h
+ BaseRenderBufferPool.h
+ IRenderBuffer.h
+ IRenderBufferPool.h
+ RenderBufferManager.h
+)
+
+if(OPENGL_FOUND OR OPENGLES_FOUND)
+ list(APPEND SOURCES RenderBufferOpenGLES.cpp
+ RenderBufferPoolOpenGLES.cpp)
+ list(APPEND HEADERS RenderBufferOpenGLES.h
+ RenderBufferPoolOpenGLES.h)
+endif()
+
+if(OPENGL_FOUND)
+ list(APPEND SOURCES RenderBufferOpenGL.cpp
+ RenderBufferPoolOpenGL.cpp)
+ list(APPEND HEADERS RenderBufferOpenGL.cpp
+ RenderBufferPoolOpenGL.cpp)
+endif()
+
+if(("gbm" IN_LIST CORE_PLATFORM_NAME_LC OR "wayland" IN_LIST CORE_PLATFORM_NAME_LC) AND EGL_FOUND)
+ list(APPEND SOURCES RenderBufferDMA.cpp
+ RenderBufferPoolDMA.cpp)
+ list(APPEND HEADERS RenderBufferDMA.h
+ RenderBufferPoolDMA.h)
+endif()
+
+core_add_library(rp-buffers)
diff --git a/xbmc/cores/RetroPlayer/buffers/IRenderBuffer.h b/xbmc/cores/RetroPlayer/buffers/IRenderBuffer.h
new file mode 100644
index 0000000..579a3e8
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/IRenderBuffer.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017-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/RetroPlayer/RetroPlayerTypes.h"
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+#include <memory>
+#include <stdint.h>
+
+namespace KODI
+{
+namespace RETRO
+{
+class IRenderBufferPool;
+
+class IRenderBuffer
+{
+public:
+ virtual ~IRenderBuffer() = default;
+
+ // Pool functions
+ virtual void Acquire() = 0;
+ virtual void Acquire(std::shared_ptr<IRenderBufferPool> pool) = 0;
+ virtual void Release() = 0;
+ virtual IRenderBufferPool* GetPool() = 0;
+
+ // Buffer functions
+ virtual bool Allocate(AVPixelFormat format, unsigned int width, unsigned int height) = 0;
+ virtual void Update() {} //! @todo Remove me
+ virtual size_t GetFrameSize() const = 0;
+ virtual uint8_t* GetMemory() = 0;
+ virtual DataAccess GetMemoryAccess() const = 0;
+ virtual DataAlignment GetMemoryAlignment() const { return DataAlignment::DATA_UNALIGNED; }
+ virtual void ReleaseMemory() {}
+ virtual bool UploadTexture() = 0;
+ virtual void BindToUnit(unsigned int unit) {}
+ virtual void SetHeader(void* header) {}
+
+ // Buffer properties
+ AVPixelFormat GetFormat() const { return m_format; }
+ unsigned int GetWidth() const { return m_width; }
+ unsigned int GetHeight() const { return m_height; }
+ bool IsLoaded() const { return m_bLoaded; }
+ void SetLoaded(bool bLoaded) { m_bLoaded = bLoaded; }
+ bool IsRendered() const { return m_bRendered; }
+ void SetRendered(bool bRendered) { m_bRendered = bRendered; }
+ unsigned int GetRotation() const { return m_rotationDegCCW; }
+ void SetRotation(unsigned int rotationDegCCW) { m_rotationDegCCW = rotationDegCCW; }
+
+protected:
+ AVPixelFormat m_format = AV_PIX_FMT_NONE;
+ unsigned int m_width = 0;
+ unsigned int m_height = 0;
+ bool m_bLoaded = false;
+ bool m_bRendered = false;
+ unsigned int m_rotationDegCCW = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/IRenderBufferPool.h b/xbmc/cores/RetroPlayer/buffers/IRenderBufferPool.h
new file mode 100644
index 0000000..f02257d
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/IRenderBufferPool.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017-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
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+#include <memory>
+#include <stdint.h>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderBufferManager;
+class CRenderVideoSettings;
+class CRPBaseRenderer;
+class IRenderBuffer;
+
+class IRenderBufferPool : public std::enable_shared_from_this<IRenderBufferPool>
+{
+public:
+ virtual ~IRenderBufferPool() = default;
+
+ virtual void RegisterRenderer(CRPBaseRenderer* renderer) = 0;
+ virtual void UnregisterRenderer(CRPBaseRenderer* renderer) = 0;
+ virtual bool HasVisibleRenderer() const = 0;
+
+ virtual bool Configure(AVPixelFormat format) = 0;
+
+ virtual bool IsConfigured() const = 0;
+
+ virtual bool IsCompatible(const CRenderVideoSettings& renderSettings) const = 0;
+
+ /*!
+ * \brief Get a free buffer from the pool, sets ref count to 1
+ *
+ * \param width The horizontal pixel count of the buffer
+ * \param height The vertical pixel could of the buffer
+ *
+ * \return The allocated buffer, or nullptr on failure
+ */
+ virtual IRenderBuffer* GetBuffer(unsigned int width, unsigned int height) = 0;
+
+ /*!
+ * \brief Called by buffer when ref count goes to zero
+ *
+ * \param buffer A fully dereferenced buffer
+ */
+ virtual void Return(IRenderBuffer* buffer) = 0;
+
+ virtual void Prime(unsigned int width, unsigned int height) = 0;
+
+ virtual void Flush() = 0;
+
+ /*!
+ * \brief Call in GetBuffer() before returning buffer to caller
+ */
+ virtual std::shared_ptr<IRenderBufferPool> GetPtr() { return shared_from_this(); }
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferDMA.cpp b/xbmc/cores/RetroPlayer/buffers/RenderBufferDMA.cpp
new file mode 100644
index 0000000..6277d62
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferDMA.cpp
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017-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 "RenderBufferDMA.h"
+
+#include "ServiceBroker.h"
+#include "utils/BufferObject.h"
+#include "utils/EGLImage.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+#include "windowing/linux/WinSystemEGL.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRenderBufferDMA::CRenderBufferDMA(CRenderContext& context, int fourcc)
+ : m_context(context), m_fourcc(fourcc), m_bo(CBufferObject::GetBufferObject(false))
+{
+ auto winSystemEGL =
+ dynamic_cast<KODI::WINDOWING::LINUX::CWinSystemEGL*>(CServiceBroker::GetWinSystem());
+
+ if (winSystemEGL == nullptr)
+ throw std::runtime_error("dynamic_cast failed to cast to CWinSystemEGL. This is likely due to "
+ "a build misconfiguration as DMA can only be used with EGL and "
+ "specifically platforms that implement CWinSystemEGL");
+
+ m_egl = std::make_unique<CEGLImage>(winSystemEGL->GetEGLDisplay());
+
+ CLog::Log(LOGDEBUG, "CRenderBufferDMA: using BufferObject type: {}", m_bo->GetName());
+}
+
+CRenderBufferDMA::~CRenderBufferDMA()
+{
+ DeleteTexture();
+}
+
+bool CRenderBufferDMA::Allocate(AVPixelFormat format, unsigned int width, unsigned int height)
+{
+ // Initialize IRenderBuffer
+ m_format = format;
+ m_width = width;
+ m_height = height;
+
+ m_bo->CreateBufferObject(m_fourcc, m_width, m_height);
+
+ return true;
+}
+
+size_t CRenderBufferDMA::GetFrameSize() const
+{
+ return m_bo->GetStride() * m_height;
+}
+
+uint8_t* CRenderBufferDMA::GetMemory()
+{
+ m_bo->SyncStart();
+ return m_bo->GetMemory();
+}
+
+void CRenderBufferDMA::ReleaseMemory()
+{
+ m_bo->ReleaseMemory();
+ m_bo->SyncEnd();
+}
+
+void CRenderBufferDMA::CreateTexture()
+{
+ glGenTextures(1, &m_textureId);
+
+ glBindTexture(m_textureTarget, m_textureId);
+
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glBindTexture(m_textureTarget, 0);
+}
+
+bool CRenderBufferDMA::UploadTexture()
+{
+ if (m_bo->GetFd() < 0)
+ return false;
+
+ if (!glIsTexture(m_textureId))
+ CreateTexture();
+
+ glBindTexture(m_textureTarget, m_textureId);
+
+ std::array<CEGLImage::EglPlane, CEGLImage::MAX_NUM_PLANES> planes;
+
+ planes[0].fd = m_bo->GetFd();
+ planes[0].offset = 0;
+ planes[0].pitch = m_bo->GetStride();
+ planes[0].modifier = m_bo->GetModifier();
+
+ CEGLImage::EglAttrs attribs;
+
+ attribs.width = m_width;
+ attribs.height = m_height;
+ attribs.format = m_fourcc;
+ attribs.planes = planes;
+
+ if (m_egl->CreateImage(attribs))
+ m_egl->UploadImage(m_textureTarget);
+
+ m_egl->DestroyImage();
+
+ glBindTexture(m_textureTarget, 0);
+
+ return true;
+}
+
+void CRenderBufferDMA::DeleteTexture()
+{
+ if (glIsTexture(m_textureId))
+ glDeleteTextures(1, &m_textureId);
+
+ m_textureId = 0;
+}
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferDMA.h b/xbmc/cores/RetroPlayer/buffers/RenderBufferDMA.h
new file mode 100644
index 0000000..a007c17
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferDMA.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017-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/RetroPlayer/buffers/BaseRenderBuffer.h"
+
+#include <memory>
+
+#include "system_gl.h"
+
+class CEGLImage;
+class IBufferObject;
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderContext;
+
+/**
+ * @brief Special IRenderBuffer implementation for use with CBufferObject.
+ * This buffer type uses Direct Memory Access (DMA) sharing via file
+ * descriptors (fds). The file descriptor is then used to create an
+ * EGL image.
+ *
+ */
+class CRenderBufferDMA : public CBaseRenderBuffer
+{
+public:
+ CRenderBufferDMA(CRenderContext& context, int fourcc);
+ ~CRenderBufferDMA() override;
+
+ // implementation of IRenderBuffer via CRenderBufferSysMem
+ bool Allocate(AVPixelFormat format, unsigned int width, unsigned int height) override;
+ size_t GetFrameSize() const override;
+ uint8_t* GetMemory() override;
+ DataAccess GetMemoryAccess() const override { return DataAccess::READ_WRITE; }
+ void ReleaseMemory() override;
+
+ // implementation of IRenderBuffer
+ bool UploadTexture() override;
+
+ GLuint TextureID() const { return m_textureId; }
+
+protected:
+ // Construction parameters
+ CRenderContext& m_context;
+ const int m_fourcc = 0;
+
+ const GLenum m_textureTarget = GL_TEXTURE_2D;
+ GLuint m_textureId = 0;
+
+private:
+ void CreateTexture();
+ void DeleteTexture();
+
+ std::unique_ptr<CEGLImage> m_egl;
+ std::unique_ptr<IBufferObject> m_bo;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferManager.cpp b/xbmc/cores/RetroPlayer/buffers/RenderBufferManager.cpp
new file mode 100644
index 0000000..2fcc5bd
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferManager.cpp
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017-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 "RenderBufferManager.h"
+
+#include "IRenderBufferPool.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "cores/RetroPlayer/rendering/RenderVideoSettings.h"
+
+#include <algorithm>
+#include <mutex>
+
+using namespace KODI;
+using namespace RETRO;
+
+CRenderBufferManager::~CRenderBufferManager()
+{
+ FlushPools();
+}
+
+void CRenderBufferManager::RegisterPools(IRendererFactory* factory, RenderBufferPoolVector pools)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_pools.emplace_back(RenderBufferPools{factory, std::move(pools)});
+}
+
+RenderBufferPoolVector CRenderBufferManager::GetPools(IRendererFactory* factory)
+{
+ RenderBufferPoolVector bufferPools;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ auto it = std::find_if(m_pools.begin(), m_pools.end(), [factory](const RenderBufferPools& pools) {
+ return pools.factory == factory;
+ });
+
+ if (it != m_pools.end())
+ bufferPools = it->pools;
+
+ return bufferPools;
+}
+
+std::vector<IRenderBufferPool*> CRenderBufferManager::GetBufferPools()
+{
+ std::vector<IRenderBufferPool*> bufferPools;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& pools : m_pools)
+ {
+ for (const auto& pool : pools.pools)
+ bufferPools.emplace_back(pool.get());
+ }
+
+ return bufferPools;
+}
+
+void CRenderBufferManager::FlushPools()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& pools : m_pools)
+ {
+ for (const auto& pool : pools.pools)
+ pool->Flush();
+ }
+}
+
+std::string CRenderBufferManager::GetRenderSystemName(IRenderBufferPool* renderBufferPool) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& pools : m_pools)
+ {
+ for (const auto& pool : pools.pools)
+ {
+ if (pool.get() == renderBufferPool)
+ return pools.factory->RenderSystemName();
+ }
+ }
+
+ return "";
+}
+
+bool CRenderBufferManager::HasScalingMethod(SCALINGMETHOD scalingMethod) const
+{
+ CRenderVideoSettings videoSettings;
+ videoSettings.SetScalingMethod(scalingMethod);
+
+ for (const auto& pools : m_pools)
+ {
+ for (const auto& pool : pools.pools)
+ if (pool->IsCompatible(videoSettings))
+ return true;
+ }
+
+ return false;
+}
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferManager.h b/xbmc/cores/RetroPlayer/buffers/RenderBufferManager.h
new file mode 100644
index 0000000..de69cad
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferManager.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+#include "cores/RetroPlayer/RetroPlayerTypes.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <vector>
+
+namespace KODI
+{
+namespace RETRO
+{
+class IRendererFactory;
+class IRenderBufferPools;
+
+class CRenderBufferManager
+{
+public:
+ CRenderBufferManager() = default;
+ ~CRenderBufferManager();
+
+ void RegisterPools(IRendererFactory* factory, RenderBufferPoolVector pools);
+ RenderBufferPoolVector GetPools(IRendererFactory* factory);
+ std::vector<IRenderBufferPool*> GetBufferPools();
+ void FlushPools();
+
+ std::string GetRenderSystemName(IRenderBufferPool* renderBufferPool) const;
+
+ bool HasScalingMethod(SCALINGMETHOD scalingMethod) const;
+
+protected:
+ struct RenderBufferPools
+ {
+ IRendererFactory* factory;
+ RenderBufferPoolVector pools;
+ };
+
+ std::vector<RenderBufferPools> m_pools;
+ mutable CCriticalSection m_critSection;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGL.cpp b/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGL.cpp
new file mode 100644
index 0000000..d852523
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGL.cpp
@@ -0,0 +1,76 @@
+/*
+ * 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 "RenderBufferOpenGL.h"
+
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRenderBufferOpenGL::CRenderBufferOpenGL(GLuint pixeltype,
+ GLuint internalformat,
+ GLuint pixelformat,
+ GLuint bpp)
+ : m_pixeltype(pixeltype), m_internalformat(internalformat), m_pixelformat(pixelformat), m_bpp(bpp)
+{
+}
+
+CRenderBufferOpenGL::~CRenderBufferOpenGL()
+{
+ DeleteTexture();
+}
+
+void CRenderBufferOpenGL::CreateTexture()
+{
+ glGenTextures(1, &m_textureId);
+
+ glBindTexture(m_textureTarget, m_textureId);
+
+ glTexImage2D(m_textureTarget, 0, m_internalformat, m_width, m_height, 0, m_pixelformat,
+ m_pixeltype, NULL);
+
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glBindTexture(m_textureTarget, 0);
+}
+
+bool CRenderBufferOpenGL::UploadTexture()
+{
+ if (!glIsTexture(m_textureId))
+ CreateTexture();
+
+ glBindTexture(m_textureTarget, m_textureId);
+
+ const int stride = GetFrameSize() / m_height;
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT, m_bpp);
+
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, stride / m_bpp);
+
+ //! @todo This is subject to change:
+ //! We want to use PBO's instead of glTexSubImage2D!
+ //! This code has been borrowed from OpenGL ES in order
+ //! to remove GL dependencies on GLES.
+ glTexSubImage2D(m_textureTarget, 0, 0, 0, m_width, m_height, m_pixelformat, m_pixeltype,
+ m_data.data());
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+
+ return true;
+}
+
+void CRenderBufferOpenGL::DeleteTexture()
+{
+ if (glIsTexture(m_textureId))
+ glDeleteTextures(1, &m_textureId);
+
+ m_textureId = 0;
+}
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGL.h b/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGL.h
new file mode 100644
index 0000000..9fcb9e2
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGL.h
@@ -0,0 +1,44 @@
+/*
+ * 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/RetroPlayer/buffers/video/RenderBufferSysMem.h"
+
+#include "system_gl.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderContext;
+
+class CRenderBufferOpenGL : public CRenderBufferSysMem
+{
+public:
+ CRenderBufferOpenGL(GLuint pixeltype, GLuint internalformat, GLuint pixelformat, GLuint bpp);
+ ~CRenderBufferOpenGL() override;
+
+ bool UploadTexture() override;
+ GLuint TextureID() const { return m_textureId; }
+
+private:
+ // Construction parameters
+ const GLuint m_pixeltype;
+ const GLuint m_internalformat;
+ const GLuint m_pixelformat;
+ const GLuint m_bpp;
+
+ const GLenum m_textureTarget = GL_TEXTURE_2D; //! @todo
+ GLuint m_textureId = 0;
+
+ void CreateTexture();
+ void DeleteTexture();
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGLES.cpp b/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGLES.cpp
new file mode 100644
index 0000000..8ed366e
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGLES.cpp
@@ -0,0 +1,101 @@
+/*
+ * 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 "RenderBufferOpenGLES.h"
+
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRenderBufferOpenGLES::CRenderBufferOpenGLES(CRenderContext& context,
+ GLuint pixeltype,
+ GLuint internalformat,
+ GLuint pixelformat,
+ GLuint bpp)
+ : m_context(context),
+ m_pixeltype(pixeltype),
+ m_internalformat(internalformat),
+ m_pixelformat(pixelformat),
+ m_bpp(bpp)
+{
+}
+
+CRenderBufferOpenGLES::~CRenderBufferOpenGLES()
+{
+ DeleteTexture();
+}
+
+void CRenderBufferOpenGLES::CreateTexture()
+{
+ glGenTextures(1, &m_textureId);
+
+ glBindTexture(m_textureTarget, m_textureId);
+
+ glTexImage2D(m_textureTarget, 0, m_internalformat, m_width, m_height, 0, m_pixelformat,
+ m_pixeltype, NULL);
+
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glBindTexture(m_textureTarget, 0);
+}
+
+bool CRenderBufferOpenGLES::UploadTexture()
+{
+ if (!glIsTexture(m_textureId))
+ CreateTexture();
+
+ glBindTexture(m_textureTarget, m_textureId);
+
+ const int stride = GetFrameSize() / m_height;
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT, m_bpp);
+
+ if (m_bpp == 4 && m_pixelformat == GL_RGBA)
+ {
+ // XOR Swap RGBA -> BGRA
+ // GLES 2.0 doesn't support strided textures (unless GL_UNPACK_ROW_LENGTH_EXT is supported)
+ uint8_t* pixels = const_cast<uint8_t*>(m_data.data());
+ for (unsigned int y = 0; y < m_height; ++y, pixels += stride)
+ {
+ for (int x = 0; x < stride; x += 4)
+ std::swap(pixels[x], pixels[x + 2]);
+ glTexSubImage2D(m_textureTarget, 0, 0, y, m_width, 1, m_pixelformat, m_pixeltype, pixels);
+ }
+ }
+ else if (m_context.IsExtSupported("GL_EXT_unpack_subimage"))
+ {
+#ifdef GL_UNPACK_ROW_LENGTH_EXT
+ glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, stride / m_bpp);
+ glTexSubImage2D(m_textureTarget, 0, 0, 0, m_width, m_height, m_pixelformat, m_pixeltype,
+ m_data.data());
+ glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, 0);
+#endif
+ }
+ else
+ {
+ uint8_t* pixels = const_cast<uint8_t*>(m_data.data());
+ for (unsigned int y = 0; y < m_height; ++y, pixels += stride)
+ glTexSubImage2D(m_textureTarget, 0, 0, y, m_width, 1, m_pixelformat, m_pixeltype, pixels);
+ }
+
+ glBindTexture(m_textureTarget, 0);
+
+ return true;
+}
+
+void CRenderBufferOpenGLES::DeleteTexture()
+{
+ if (glIsTexture(m_textureId))
+ glDeleteTextures(1, &m_textureId);
+
+ m_textureId = 0;
+}
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGLES.h b/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGLES.h
new file mode 100644
index 0000000..58e5d47
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferOpenGLES.h
@@ -0,0 +1,51 @@
+/*
+ * 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/RetroPlayer/buffers/video/RenderBufferSysMem.h"
+
+#include "system_gl.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderContext;
+
+class CRenderBufferOpenGLES : public CRenderBufferSysMem
+{
+public:
+ CRenderBufferOpenGLES(CRenderContext& context,
+ GLuint pixeltype,
+ GLuint internalformat,
+ GLuint pixelformat,
+ GLuint bpp);
+ ~CRenderBufferOpenGLES() override;
+
+ // implementation of IRenderBuffer via CRenderBufferSysMem
+ bool UploadTexture() override;
+
+ GLuint TextureID() const { return m_textureId; }
+
+private:
+ // Construction parameters
+ CRenderContext& m_context;
+ const GLuint m_pixeltype;
+ const GLuint m_internalformat;
+ const GLuint m_pixelformat;
+ const GLuint m_bpp;
+
+ const GLenum m_textureTarget = GL_TEXTURE_2D; //! @todo
+ GLuint m_textureId = 0;
+
+ void CreateTexture();
+ void DeleteTexture();
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolDMA.cpp b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolDMA.cpp
new file mode 100644
index 0000000..42c47a3
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolDMA.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017-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 "RenderBufferPoolDMA.h"
+
+#include "RenderBufferDMA.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h"
+
+#include <drm_fourcc.h>
+
+using namespace KODI;
+using namespace RETRO;
+
+CRenderBufferPoolDMA::CRenderBufferPoolDMA(CRenderContext& context) : m_context(context)
+{
+}
+
+bool CRenderBufferPoolDMA::IsCompatible(const CRenderVideoSettings& renderSettings) const
+{
+ if (!CRPRendererDMA::SupportsScalingMethod(renderSettings.GetScalingMethod()))
+ return false;
+
+ return true;
+}
+
+IRenderBuffer* CRenderBufferPoolDMA::CreateRenderBuffer(void* header /* = nullptr */)
+{
+ return new CRenderBufferDMA(m_context, m_fourcc);
+}
+
+bool CRenderBufferPoolDMA::ConfigureInternal()
+{
+ switch (m_format)
+ {
+ case AV_PIX_FMT_0RGB32:
+ {
+ m_fourcc = DRM_FORMAT_ARGB8888;
+ return true;
+ }
+ case AV_PIX_FMT_RGB555:
+ {
+ m_fourcc = DRM_FORMAT_ARGB1555;
+ return true;
+ }
+ case AV_PIX_FMT_RGB565:
+ {
+ m_fourcc = DRM_FORMAT_RGB565;
+ return true;
+ }
+ default:
+ break; // we shouldn't even get this far if we are given an unsupported pixel format
+ }
+
+ return false;
+}
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolDMA.h b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolDMA.h
new file mode 100644
index 0000000..81645bb
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolDMA.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017-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/RetroPlayer/buffers/BaseRenderBufferPool.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderContext;
+
+/**
+ * @brief Special IRenderBufferPool implementation that converts
+ * AVPixelFormat to DRM_FORMAT_* for use with CRenderBufferDMA.
+ *
+ */
+class CRenderBufferPoolDMA : public CBaseRenderBufferPool
+{
+public:
+ CRenderBufferPoolDMA(CRenderContext& context);
+ ~CRenderBufferPoolDMA() override = default;
+
+ // implementation of IRenderBufferPool via CBaseRenderBufferPool
+ bool IsCompatible(const CRenderVideoSettings& renderSettings) const override;
+
+protected:
+ // implementation of CBaseRenderBufferPool
+ IRenderBuffer* CreateRenderBuffer(void* header = nullptr) override;
+ bool ConfigureInternal() override;
+
+ // Construction parameters
+ CRenderContext& m_context;
+
+ // Configuration parameters
+ int m_fourcc = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGL.cpp b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGL.cpp
new file mode 100644
index 0000000..6d30346
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGL.cpp
@@ -0,0 +1,64 @@
+/*
+ * 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 "RenderBufferPoolOpenGL.h"
+
+#include "RenderBufferOpenGL.h"
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "cores/RetroPlayer/rendering/RenderVideoSettings.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h"
+#include "utils/GLUtils.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+bool CRenderBufferPoolOpenGL::IsCompatible(const CRenderVideoSettings& renderSettings) const
+{
+ return CRPRendererOpenGL::SupportsScalingMethod(renderSettings.GetScalingMethod());
+}
+
+IRenderBuffer* CRenderBufferPoolOpenGL::CreateRenderBuffer(void* header /* = nullptr */)
+{
+ return new CRenderBufferOpenGL(m_pixeltype, m_internalformat, m_pixelformat, m_bpp);
+}
+
+bool CRenderBufferPoolOpenGL::ConfigureInternal()
+{
+ // Configure CRenderBufferPoolOpenGLES
+ switch (m_format)
+ {
+ case AV_PIX_FMT_0RGB32:
+ {
+ m_pixeltype = GL_UNSIGNED_BYTE;
+ m_internalformat = GL_RGBA;
+ m_pixelformat = GL_BGRA;
+ m_bpp = sizeof(uint32_t);
+ return true;
+ }
+ case AV_PIX_FMT_RGB555:
+ {
+ m_pixeltype = GL_UNSIGNED_SHORT_5_5_5_1;
+ m_internalformat = GL_RGB;
+ m_pixelformat = GL_RGB;
+ m_bpp = sizeof(uint16_t);
+ return true;
+ }
+ case AV_PIX_FMT_RGB565:
+ {
+ m_pixeltype = GL_UNSIGNED_SHORT_5_6_5;
+ m_internalformat = GL_RGB;
+ m_pixelformat = GL_RGB;
+ m_bpp = sizeof(uint16_t);
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGL.h b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGL.h
new file mode 100644
index 0000000..e8689b9
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGL.h
@@ -0,0 +1,45 @@
+/*
+ * 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 "BaseRenderBufferPool.h"
+#include "IRenderBuffer.h"
+
+#include "system_gl.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderContext;
+class CRenderVideoSettings;
+
+class CRenderBufferPoolOpenGL : public CBaseRenderBufferPool
+{
+public:
+ CRenderBufferPoolOpenGL() = default;
+ ~CRenderBufferPoolOpenGL() override = default;
+
+ // implementation of IRenderBufferPool via CBaseRenderBufferPool
+ bool IsCompatible(const CRenderVideoSettings& renderSettings) const override;
+
+protected:
+ // implementation of CBaseRenderBufferPool
+ IRenderBuffer* CreateRenderBuffer(void* header = nullptr) override;
+ bool ConfigureInternal() override;
+
+private:
+ // Configuration parameters
+ GLuint m_pixeltype = 0;
+ GLuint m_internalformat = 0;
+ GLuint m_pixelformat = 0;
+ GLuint m_bpp = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGLES.cpp b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGLES.cpp
new file mode 100644
index 0000000..c7aa8f6
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGLES.cpp
@@ -0,0 +1,76 @@
+/*
+ * 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 "RenderBufferPoolOpenGLES.h"
+
+#include "RenderBufferOpenGLES.h"
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "cores/RetroPlayer/rendering/RenderVideoSettings.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h"
+#include "utils/GLUtils.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRenderBufferPoolOpenGLES::CRenderBufferPoolOpenGLES(CRenderContext& context) : m_context(context)
+{
+}
+
+bool CRenderBufferPoolOpenGLES::IsCompatible(const CRenderVideoSettings& renderSettings) const
+{
+ return CRPRendererOpenGLES::SupportsScalingMethod(renderSettings.GetScalingMethod());
+}
+
+IRenderBuffer* CRenderBufferPoolOpenGLES::CreateRenderBuffer(void* header /* = nullptr */)
+{
+ return new CRenderBufferOpenGLES(m_context, m_pixeltype, m_internalformat, m_pixelformat, m_bpp);
+}
+
+bool CRenderBufferPoolOpenGLES::ConfigureInternal()
+{
+ switch (m_format)
+ {
+ case AV_PIX_FMT_0RGB32:
+ {
+ m_pixeltype = GL_UNSIGNED_BYTE;
+ if (m_context.IsExtSupported("GL_EXT_texture_format_BGRA8888") ||
+ m_context.IsExtSupported("GL_IMG_texture_format_BGRA8888"))
+ {
+ m_internalformat = GL_BGRA_EXT;
+ m_pixelformat = GL_BGRA_EXT;
+ }
+ else if (m_context.IsExtSupported("GL_APPLE_texture_format_BGRA8888"))
+ {
+ // Apple's implementation does not conform to spec. Instead, they require
+ // differing format/internalformat, more like GL.
+ m_internalformat = GL_RGBA;
+ m_pixelformat = GL_BGRA_EXT;
+ }
+ else
+ {
+ m_internalformat = GL_RGBA;
+ m_pixelformat = GL_RGBA;
+ }
+ m_bpp = sizeof(uint32_t);
+ return true;
+ }
+ case AV_PIX_FMT_RGB555:
+ case AV_PIX_FMT_RGB565:
+ {
+ m_pixeltype = GL_UNSIGNED_SHORT_5_6_5;
+ m_internalformat = GL_RGB;
+ m_pixelformat = GL_RGB;
+ m_bpp = sizeof(uint16_t);
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
diff --git a/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGLES.h b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGLES.h
new file mode 100644
index 0000000..b511cdb
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/RenderBufferPoolOpenGLES.h
@@ -0,0 +1,47 @@
+/*
+ * 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 "BaseRenderBufferPool.h"
+#include "IRenderBuffer.h"
+
+#include "system_gl.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderContext;
+class CRenderVideoSettings;
+
+class CRenderBufferPoolOpenGLES : public CBaseRenderBufferPool
+{
+public:
+ CRenderBufferPoolOpenGLES(CRenderContext& context);
+ ~CRenderBufferPoolOpenGLES() override = default;
+
+ // implementation of IRenderBufferPool via CBaseRenderBufferPool
+ bool IsCompatible(const CRenderVideoSettings& renderSettings) const override;
+
+private:
+ // implementation of CBaseRenderBufferPool
+ IRenderBuffer* CreateRenderBuffer(void* header = nullptr) override;
+ bool ConfigureInternal() override;
+
+ // Construction parameters
+ CRenderContext& m_context;
+
+ // Configuration parameters
+ GLuint m_pixeltype = 0;
+ GLuint m_internalformat = 0;
+ GLuint m_pixelformat = 0;
+ GLuint m_bpp = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/video/CMakeLists.txt b/xbmc/cores/RetroPlayer/buffers/video/CMakeLists.txt
new file mode 100644
index 0000000..797ab90
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/video/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES RenderBufferGuiTexture.cpp
+ RenderBufferSysMem.cpp
+)
+
+set(HEADERS RenderBufferGuiTexture.h
+ RenderBufferSysMem.h
+)
+
+core_add_library(rp-buffers-video)
diff --git a/xbmc/cores/RetroPlayer/buffers/video/RenderBufferGuiTexture.cpp b/xbmc/cores/RetroPlayer/buffers/video/RenderBufferGuiTexture.cpp
new file mode 100644
index 0000000..9f8e05b
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/video/RenderBufferGuiTexture.cpp
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017-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 "RenderBufferGuiTexture.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRenderBufferGuiTexture::CRenderBufferGuiTexture(SCALINGMETHOD scalingMethod)
+ : m_scalingMethod(scalingMethod)
+{
+ m_textureFormat = XB_FMT_A8R8G8B8;
+}
+
+bool CRenderBufferGuiTexture::Allocate(AVPixelFormat format,
+ unsigned int width,
+ unsigned int height)
+{
+ // Initialize IRenderBuffer
+ m_format = TranslateFormat(m_textureFormat);
+
+ if (m_format != AV_PIX_FMT_NONE)
+ {
+ m_texture = CTexture::CreateTexture(width, height, m_textureFormat);
+ m_texture->SetScalingMethod(TranslateScalingMethod(m_scalingMethod));
+ m_texture->SetCacheMemory(true);
+
+ // Update IRenderBuffer
+ m_width = m_texture->GetTextureWidth();
+ m_height = m_texture->GetTextureHeight();
+
+ return true;
+ }
+
+ return false;
+}
+
+size_t CRenderBufferGuiTexture::GetFrameSize() const
+{
+ if (m_texture)
+ return m_texture->GetPitch() * m_texture->GetRows();
+
+ return 0;
+}
+
+uint8_t* CRenderBufferGuiTexture::GetMemory()
+{
+ if (m_texture)
+ return m_texture->GetPixels();
+
+ return nullptr;
+}
+
+bool CRenderBufferGuiTexture::UploadTexture()
+{
+ bool bLoaded = false;
+
+ if (m_texture)
+ {
+ m_texture->LoadToGPU();
+ bLoaded = true;
+ }
+
+ return bLoaded;
+}
+
+void CRenderBufferGuiTexture::BindToUnit(unsigned int unit)
+{
+ if (m_texture)
+ m_texture->BindToUnit(unit);
+}
+
+AVPixelFormat CRenderBufferGuiTexture::TranslateFormat(unsigned int textureFormat)
+{
+ switch (textureFormat)
+ {
+ case XB_FMT_RGBA8:
+ case XB_FMT_A8R8G8B8:
+ return AV_PIX_FMT_BGRA;
+ default:
+ break;
+ }
+
+ return AV_PIX_FMT_NONE;
+}
+
+TEXTURE_SCALING CRenderBufferGuiTexture::TranslateScalingMethod(SCALINGMETHOD scalingMethod)
+{
+ switch (scalingMethod)
+ {
+ case SCALINGMETHOD::NEAREST:
+ return TEXTURE_SCALING::NEAREST;
+ case SCALINGMETHOD::LINEAR:
+ return TEXTURE_SCALING::LINEAR;
+ default:
+ break;
+ }
+
+ return TEXTURE_SCALING::NEAREST;
+}
diff --git a/xbmc/cores/RetroPlayer/buffers/video/RenderBufferGuiTexture.h b/xbmc/cores/RetroPlayer/buffers/video/RenderBufferGuiTexture.h
new file mode 100644
index 0000000..b78d157
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/video/RenderBufferGuiTexture.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+#include "cores/RetroPlayer/buffers/BaseRenderBuffer.h"
+#include "guilib/Texture.h"
+#include "guilib/TextureFormats.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderBufferGuiTexture : public CBaseRenderBuffer
+{
+public:
+ CRenderBufferGuiTexture(SCALINGMETHOD scalingMethod);
+ ~CRenderBufferGuiTexture() override = default;
+
+ // implementation of IRenderBuffer via CBaseRenderBuffer
+ bool Allocate(AVPixelFormat format, unsigned int width, unsigned int height) override;
+ size_t GetFrameSize() const override;
+ uint8_t* GetMemory() override;
+ DataAccess GetMemoryAccess() const override { return DataAccess::READ_WRITE; }
+ bool UploadTexture() override;
+ void BindToUnit(unsigned int unit) override;
+
+ // GUI texture interface
+ CTexture* GetTexture() { return m_texture.get(); }
+
+protected:
+ AVPixelFormat TranslateFormat(unsigned int textureFormat);
+ TEXTURE_SCALING TranslateScalingMethod(SCALINGMETHOD scalingMethod);
+
+ // Texture parameters
+ SCALINGMETHOD m_scalingMethod;
+ unsigned int m_textureFormat = XB_FMT_UNKNOWN;
+ std::unique_ptr<CTexture> m_texture;
+};
+
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/buffers/video/RenderBufferSysMem.cpp b/xbmc/cores/RetroPlayer/buffers/video/RenderBufferSysMem.cpp
new file mode 100644
index 0000000..8fb4260
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/video/RenderBufferSysMem.cpp
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017-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 "RenderBufferSysMem.h"
+
+#include "cores/RetroPlayer/rendering/RenderTranslator.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+bool CRenderBufferSysMem::Allocate(AVPixelFormat format, unsigned int width, unsigned int height)
+{
+ // Initialize IRenderBuffer
+ m_format = format;
+ m_width = width;
+ m_height = height;
+
+ const size_t size = GetBufferSize(format, width, height);
+
+ if (m_format != AV_PIX_FMT_NONE && size > 0)
+ {
+ // Allocate memory
+ m_data.resize(size);
+ return true;
+ }
+
+ return false;
+}
+
+size_t CRenderBufferSysMem::GetFrameSize() const
+{
+ return m_data.size();
+}
+
+uint8_t* CRenderBufferSysMem::GetMemory()
+{
+ return m_data.data();
+}
+
+size_t CRenderBufferSysMem::GetBufferSize(AVPixelFormat format,
+ unsigned int width,
+ unsigned int height)
+{
+ const size_t bufferStride = CRenderTranslator::TranslateWidthToBytes(width, format);
+ const size_t bufferSize = bufferStride * height;
+
+ return bufferSize;
+}
diff --git a/xbmc/cores/RetroPlayer/buffers/video/RenderBufferSysMem.h b/xbmc/cores/RetroPlayer/buffers/video/RenderBufferSysMem.h
new file mode 100644
index 0000000..c88ce94
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/buffers/video/RenderBufferSysMem.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017-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/RetroPlayer/buffers/BaseRenderBuffer.h"
+
+#include <stdint.h>
+#include <vector>
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderBufferSysMem : public CBaseRenderBuffer
+{
+public:
+ CRenderBufferSysMem() = default;
+ ~CRenderBufferSysMem() override = default;
+
+ // implementation of IRenderBuffer
+ bool Allocate(AVPixelFormat format, unsigned int width, unsigned int height) override;
+ size_t GetFrameSize() const override;
+ uint8_t* GetMemory() override;
+ DataAccess GetMemoryAccess() const override { return DataAccess::READ_WRITE; }
+
+ // Utility functions
+ static size_t GetBufferSize(AVPixelFormat format, unsigned int width, unsigned int height);
+
+protected:
+ std::vector<uint8_t> m_data;
+};
+
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/cheevos/CMakeLists.txt b/xbmc/cores/RetroPlayer/cheevos/CMakeLists.txt
new file mode 100644
index 0000000..0e1bf23
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/cheevos/CMakeLists.txt
@@ -0,0 +1,6 @@
+set(SOURCES Cheevos.cpp)
+
+set(HEADERS Cheevos.h
+ RConsoleIDs.h)
+
+core_add_library(retroplayer_cheevos)
diff --git a/xbmc/cores/RetroPlayer/cheevos/Cheevos.cpp b/xbmc/cores/RetroPlayer/cheevos/Cheevos.cpp
new file mode 100644
index 0000000..fe82245
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/cheevos/Cheevos.cpp
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2020-2021 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 "Cheevos.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/File.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/cheevos/GameClientCheevos.h"
+#include "games/tags/GameInfoTag.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "vector"
+
+using namespace KODI;
+using namespace RETRO;
+
+namespace
+{
+// API JSON Field names
+constexpr auto SUCCESS = "Success";
+constexpr auto GAME_ID = "GameID";
+constexpr auto PATCH_DATA = "PatchData";
+constexpr auto RICH_PRESENCE = "RichPresencePatch";
+constexpr auto GAME_TITLE = "Title";
+constexpr auto PUBLISHER = "Publisher";
+constexpr auto DEVELOPER = "Developer";
+constexpr auto GENRE = "Genre";
+constexpr auto CONSOLE_NAME = "ConsoleName";
+
+constexpr int RESPONSE_SIZE = 64;
+} // namespace
+
+CCheevos::CCheevos(GAME::CGameClient* gameClient,
+ const std::string& userName,
+ const std::string& loginToken)
+ : m_gameClient(gameClient), m_userName(userName), m_loginToken(loginToken)
+{
+}
+
+void CCheevos::ResetRuntime()
+{
+ m_gameClient->Cheevos().RCResetRuntime();
+}
+
+bool CCheevos::LoadData()
+{
+ if (m_userName.empty() || m_loginToken.empty())
+ return false;
+
+ if (m_romHash.empty())
+ {
+ m_consoleID = ConsoleID();
+ if (m_consoleID == RConsoleID::RC_INVALID_ID)
+ return false;
+
+ std::string hash;
+ if (!m_gameClient->Cheevos().RCGenerateHashFromFile(hash, m_consoleID,
+ m_gameClient->GetGamePath().c_str()))
+ {
+ return false;
+ }
+
+ m_romHash = hash;
+ }
+
+ std::string requestURL;
+
+ if (!m_gameClient->Cheevos().RCGetGameIDUrl(requestURL, m_romHash))
+ return false;
+
+ XFILE::CFile response;
+ response.CURLCreate(requestURL);
+ response.CURLOpen(0);
+
+ char responseStr[RESPONSE_SIZE];
+ response.ReadString(responseStr, RESPONSE_SIZE);
+
+ response.Close();
+
+ CVariant data(CVariant::VariantTypeObject);
+ CJSONVariantParser::Parse(responseStr, data);
+
+ if (!data[SUCCESS].asBoolean())
+ return false;
+
+ m_gameID = data[GAME_ID].asUnsignedInteger32();
+
+ // For some reason RetroAchievements returns Success = true when the hash isn't found
+ if (m_gameID == 0)
+ return false;
+
+ if (!m_gameClient->Cheevos().RCGetPatchFileUrl(requestURL, m_userName, m_loginToken, m_gameID))
+ return false;
+
+ CURL curl(requestURL);
+ std::vector<uint8_t> patchData;
+ response.LoadFile(curl, patchData);
+
+ std::string strResponse(patchData.begin(), patchData.end());
+ CJSONVariantParser::Parse(strResponse, data);
+
+ if (!data[SUCCESS].asBoolean())
+ return false;
+
+ m_richPresenceScript = data[PATCH_DATA][RICH_PRESENCE].asString();
+ m_richPresenceLoaded = true;
+
+ std::unique_ptr<CFileItem> file{std::make_unique<CFileItem>()};
+
+ GAME::CGameInfoTag& tag = *file->GetGameInfoTag();
+ tag.SetTitle(data[PATCH_DATA][GAME_TITLE].asString());
+ tag.SetPublisher(data[PATCH_DATA][PUBLISHER].asString());
+ tag.SetDeveloper(data[PATCH_DATA][DEVELOPER].asString());
+ tag.SetGenres({data[PATCH_DATA][GENRE].asString()});
+ tag.SetPlatform(data[PATCH_DATA][CONSOLE_NAME].asString());
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_PLAYER_ITEM, -1, -1,
+ static_cast<void*>(file.release()));
+
+ return true;
+}
+
+void CCheevos::EnableRichPresence()
+{
+ if (!m_richPresenceLoaded)
+ {
+ if (!LoadData())
+ {
+ CLog::Log(LOGERROR, "Cheevos: Couldn't load patch file");
+ return;
+ }
+ }
+
+ m_gameClient->Cheevos().RCEnableRichPresence(m_richPresenceScript);
+ m_richPresenceScript.clear();
+}
+
+std::string CCheevos::GetRichPresenceEvaluation()
+{
+ if (!m_richPresenceLoaded)
+ {
+ CLog::Log(LOGERROR, "Cheevos: Rich Presence script was not found");
+ return "";
+ }
+
+ std::string evaluation;
+ m_gameClient->Cheevos().RCGetRichPresenceEvaluation(evaluation, m_consoleID);
+
+ std::string url;
+ std::string postData;
+ if (m_gameClient->Cheevos().RCPostRichPresenceUrl(url, postData, m_userName, m_loginToken,
+ m_gameID, evaluation))
+ {
+ XFILE::CCurlFile curl;
+ std::string res;
+ curl.Post(url, postData, res);
+ }
+
+ return evaluation;
+}
+
+RConsoleID CCheevos::ConsoleID()
+{
+ const std::string extension = URIUtils::GetExtension(m_gameClient->GetGamePath());
+ auto it = m_extensionToConsole.find(extension);
+
+ if (it == m_extensionToConsole.end())
+ return RConsoleID::RC_INVALID_ID;
+
+ return it->second;
+}
diff --git a/xbmc/cores/RetroPlayer/cheevos/Cheevos.h b/xbmc/cores/RetroPlayer/cheevos/Cheevos.h
new file mode 100644
index 0000000..70ffe41
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/cheevos/Cheevos.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2020-2021 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 "RConsoleIDs.h"
+
+#include <cstdint>
+#include <map>
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClient;
+}
+
+namespace RETRO
+{
+class CCheevos
+{
+public:
+ CCheevos(GAME::CGameClient* gameClient,
+ const std::string& userName,
+ const std::string& loginToken);
+ void ResetRuntime();
+ void EnableRichPresence();
+ std::string GetRichPresenceEvaluation();
+
+private:
+ bool LoadData();
+ RConsoleID ConsoleID();
+
+ GAME::CGameClient* const m_gameClient;
+ std::string m_userName;
+ std::string m_loginToken;
+ std::string m_romHash;
+ std::string m_richPresenceScript;
+ uint32_t m_gameID{};
+ RConsoleID m_consoleID = RConsoleID::RC_INVALID_ID;
+ bool m_richPresenceLoaded{};
+
+ const std::map<std::string, RConsoleID> m_extensionToConsole = {
+ {".a26", RConsoleID::RC_CONSOLE_ATARI_2600},
+ {".a78", RConsoleID::RC_CONSOLE_ATARI_7800},
+ {".agb", RConsoleID::RC_CONSOLE_GAMEBOY_ADVANCE},
+ {".cdi", RConsoleID::RC_CONSOLE_DREAMCAST},
+ {".cdt", RConsoleID::RC_CONSOLE_AMSTRAD_PC},
+ {".cgb", RConsoleID::RC_CONSOLE_GAMEBOY_COLOR},
+ {".chd", RConsoleID::RC_CONSOLE_DREAMCAST},
+ {".cpr", RConsoleID::RC_CONSOLE_AMSTRAD_PC},
+ {".d64", RConsoleID::RC_CONSOLE_COMMODORE_64},
+ {".gb", RConsoleID::RC_CONSOLE_GAMEBOY},
+ {".gba", RConsoleID::RC_CONSOLE_GAMEBOY_ADVANCE},
+ {".gbc", RConsoleID::RC_CONSOLE_GAMEBOY_COLOR},
+ {".gdi", RConsoleID::RC_CONSOLE_DREAMCAST},
+ {".j64", RConsoleID::RC_CONSOLE_ATARI_JAGUAR},
+ {".jag", RConsoleID::RC_CONSOLE_ATARI_JAGUAR},
+ {".lnx", RConsoleID::RC_CONSOLE_ATARI_LYNX},
+ {".mds", RConsoleID::RC_CONSOLE_SATURN},
+ {".min", RConsoleID::RC_CONSOLE_POKEMON_MINI},
+ {".mx1", RConsoleID::RC_CONSOLE_MSX},
+ {".mx2", RConsoleID::RC_CONSOLE_MSX},
+ {".n64", RConsoleID::RC_CONSOLE_NINTENDO_64},
+ {".ndd", RConsoleID::RC_CONSOLE_NINTENDO_64},
+ {".nds", RConsoleID::RC_CONSOLE_NINTENDO_DS},
+ {".nes", RConsoleID::RC_CONSOLE_NINTENDO},
+ {".o", RConsoleID::RC_CONSOLE_ATARI_LYNX},
+ {".pce", RConsoleID::RC_CONSOLE_PC_ENGINE},
+ {".sfc", RConsoleID::RC_CONSOLE_SUPER_NINTENDO},
+ {".sgx", RConsoleID::RC_CONSOLE_PC_ENGINE},
+ {".smc", RConsoleID::RC_CONSOLE_SUPER_NINTENDO},
+ {".sna", RConsoleID::RC_CONSOLE_AMSTRAD_PC},
+ {".tap", RConsoleID::RC_CONSOLE_AMSTRAD_PC},
+ {".u1", RConsoleID::RC_CONSOLE_NINTENDO_64},
+ {".v64", RConsoleID::RC_CONSOLE_NINTENDO_64},
+ {".vb", RConsoleID::RC_CONSOLE_VIRTUAL_BOY},
+ {".vboy", RConsoleID::RC_CONSOLE_VIRTUAL_BOY},
+ {".vec", RConsoleID::RC_CONSOLE_VECTREX},
+ {".voc", RConsoleID::RC_CONSOLE_AMSTRAD_PC},
+ {".z64", RConsoleID::RC_CONSOLE_NINTENDO_64}};
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/cheevos/RConsoleIDs.h b/xbmc/cores/RetroPlayer/cheevos/RConsoleIDs.h
new file mode 100644
index 0000000..c6c9d5a
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/cheevos/RConsoleIDs.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020-2021 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
+
+namespace KODI
+{
+namespace RETRO
+{
+enum class RConsoleID
+{
+ RC_INVALID_ID = -1,
+ RC_CONSOLE_MEGA_DRIVE = 1,
+ RC_CONSOLE_NINTENDO_64 = 2,
+ RC_CONSOLE_SUPER_NINTENDO = 3,
+ RC_CONSOLE_GAMEBOY = 4,
+ RC_CONSOLE_GAMEBOY_ADVANCE = 5,
+ RC_CONSOLE_GAMEBOY_COLOR = 6,
+ RC_CONSOLE_NINTENDO = 7,
+ RC_CONSOLE_PC_ENGINE = 8,
+ RC_CONSOLE_SEGA_CD = 9,
+ RC_CONSOLE_SEGA_32X = 10,
+ RC_CONSOLE_MASTER_SYSTEM = 11,
+ RC_CONSOLE_PLAYSTATION = 12,
+ RC_CONSOLE_ATARI_LYNX = 13,
+ RC_CONSOLE_NEOGEO_POCKET = 14,
+ RC_CONSOLE_GAME_GEAR = 15,
+ RC_CONSOLE_GAMECUBE = 16,
+ RC_CONSOLE_ATARI_JAGUAR = 17,
+ RC_CONSOLE_NINTENDO_DS = 18,
+ RC_CONSOLE_WII = 19,
+ RC_CONSOLE_WII_U = 20,
+ RC_CONSOLE_PLAYSTATION_2 = 21,
+ RC_CONSOLE_XBOX = 22,
+ RC_CONSOLE_MAGNAVOX_ODYSSEY = 23,
+ RC_CONSOLE_POKEMON_MINI = 24,
+ RC_CONSOLE_ATARI_2600 = 25,
+ RC_CONSOLE_MS_DOS = 26,
+ RC_CONSOLE_ARCADE = 27,
+ RC_CONSOLE_VIRTUAL_BOY = 28,
+ RC_CONSOLE_MSX = 29,
+ RC_CONSOLE_COMMODORE_64 = 30,
+ RC_CONSOLE_ZX81 = 31,
+ RC_CONSOLE_ORIC = 32,
+ RC_CONSOLE_SG1000 = 33,
+ RC_CONSOLE_VIC20 = 34,
+ RC_CONSOLE_AMIGA = 35,
+ RC_CONSOLE_AMIGA_ST = 36,
+ RC_CONSOLE_AMSTRAD_PC = 37,
+ RC_CONSOLE_APPLE_II = 38,
+ RC_CONSOLE_SATURN = 39,
+ RC_CONSOLE_DREAMCAST = 40,
+ RC_CONSOLE_PSP = 41,
+ RC_CONSOLE_CDI = 42,
+ RC_CONSOLE_3DO = 43,
+ RC_CONSOLE_COLECOVISION = 44,
+ RC_CONSOLE_INTELLIVISION = 45,
+ RC_CONSOLE_VECTREX = 46,
+ RC_CONSOLE_PC8800 = 47,
+ RC_CONSOLE_PC9800 = 48,
+ RC_CONSOLE_PCFX = 49,
+ RC_CONSOLE_ATARI_5200 = 50,
+ RC_CONSOLE_ATARI_7800 = 51,
+ RC_CONSOLE_X68K = 52,
+ RC_CONSOLE_WONDERSWAN = 53,
+ RC_CONSOLE_CASSETTEVISION = 54,
+ RC_CONSOLE_SUPER_CASSETTEVISION = 55,
+ RC_CONSOLE_NEO_GEO_CD = 56,
+ RC_CONSOLE_FAIRCHILD_CHANNEL_F = 57,
+ RC_CONSOLE_FM_TOWNS = 58,
+ RC_CONSOLE_ZX_SPECTRUM = 59,
+ RC_CONSOLE_GAME_AND_WATCH = 60,
+ RC_CONSOLE_NOKIA_NGAGE = 61,
+ RC_CONSOLE_NINTENDO_3DS = 62,
+
+ RC_CONSOLE_HUBS = 100,
+ RC_CONSOLE_EVENTS = 101
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guibridge/CMakeLists.txt b/xbmc/cores/RetroPlayer/guibridge/CMakeLists.txt
new file mode 100644
index 0000000..5ba7579
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/CMakeLists.txt
@@ -0,0 +1,24 @@
+set(SOURCES GUIGameMessenger.cpp
+ GUIGameRenderManager.cpp
+ GUIGameSettings.cpp
+ GUIGameSettingsHandle.cpp
+ GUIGameVideoHandle.cpp
+ GUIRenderHandle.cpp
+ GUIRenderTarget.cpp
+ GUIRenderTargetFactory.cpp
+)
+
+set(HEADERS GUIGameMessenger.h
+ GUIGameRenderManager.h
+ GUIGameSettings.h
+ GUIGameSettingsHandle.h
+ GUIGameVideoHandle.h
+ GUIRenderHandle.h
+ GUIRenderTarget.h
+ GUIRenderTargetFactory.h
+ IGameCallback.h
+ IGUIRenderSettings.h
+ IRenderCallback.h
+)
+
+core_add_library(retroplayer_guibridge)
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIGameMessenger.cpp b/xbmc/cores/RetroPlayer/guibridge/GUIGameMessenger.cpp
new file mode 100644
index 0000000..ff7efa3
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIGameMessenger.cpp
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 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 "GUIGameMessenger.h"
+
+#include "FileItem.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CGUIGameMessenger::CGUIGameMessenger(CRPProcessInfo& processInfo)
+ : m_guiComponent(processInfo.GetRenderContext().GUI())
+{
+}
+
+void CGUIGameMessenger::RefreshSavestates(const std::string& savestatePath /* = "" */,
+ ISavestate* savestate /* = nullptr */)
+{
+ if (m_guiComponent != nullptr)
+ {
+ CGUIMessage message(GUI_MSG_REFRESH_THUMBS, 0, WINDOW_DIALOG_IN_GAME_SAVES);
+
+ // Add path, if given
+ if (!savestatePath.empty())
+ message.SetStringParam(savestatePath);
+
+ // Add savestate info, if given
+ if (savestate != nullptr)
+ {
+ CFileItemPtr item = std::make_shared<CFileItem>();
+ CSavestateDatabase::GetSavestateItem(*savestate, savestatePath, *item);
+ message.SetItem(std::static_pointer_cast<CGUIListItem>(item));
+ }
+
+ // Notify the in-game savestate dialog
+ m_guiComponent->GetWindowManager().SendThreadMessage(message, WINDOW_DIALOG_IN_GAME_SAVES);
+ }
+}
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIGameMessenger.h b/xbmc/cores/RetroPlayer/guibridge/GUIGameMessenger.h
new file mode 100644
index 0000000..7384f9a
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIGameMessenger.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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 <string>
+
+class CGUIComponent;
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfo;
+class ISavestate;
+
+/*!
+ * \brief Class to send messages to the GUI, if a GUI is present
+ */
+class CGUIGameMessenger
+{
+public:
+ CGUIGameMessenger(CRPProcessInfo& processInfo);
+
+ /*!
+ * \brief Refresh savestate GUI elements being displayed
+ *
+ * \param savestatePath The savestate to refresh, or empty to refresh all savestates
+ * \param savestate Optional savestate info to send with the message
+ */
+ void RefreshSavestates(const std::string& savestatePath = "", ISavestate* savestate = nullptr);
+
+private:
+ CGUIComponent* const m_guiComponent;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIGameRenderManager.cpp b/xbmc/cores/RetroPlayer/guibridge/GUIGameRenderManager.cpp
new file mode 100644
index 0000000..4071d10
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIGameRenderManager.cpp
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2017-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 "GUIGameRenderManager.h"
+
+#include "GUIGameSettingsHandle.h"
+#include "GUIGameVideoHandle.h"
+#include "GUIRenderHandle.h"
+#include "GUIRenderTarget.h"
+#include "GUIRenderTargetFactory.h"
+#include "IGameCallback.h"
+#include "IRenderCallback.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace RETRO;
+
+CGUIGameRenderManager::~CGUIGameRenderManager() = default;
+
+void CGUIGameRenderManager::RegisterPlayer(CGUIRenderTargetFactory* factory,
+ IRenderCallback* callback,
+ IGameCallback* gameCallback)
+{
+ // Set factory
+ {
+ std::unique_lock<CCriticalSection> lock(m_targetMutex);
+ m_factory = factory;
+ UpdateRenderTargets();
+ }
+
+ // Set callback
+ {
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+ m_callback = callback;
+ }
+
+ // Set game callback
+ {
+ std::unique_lock<CCriticalSection> lock(m_gameCallbackMutex);
+ m_gameCallback = gameCallback;
+ }
+}
+
+void CGUIGameRenderManager::UnregisterPlayer()
+{
+ // Reset game callback
+ {
+ std::unique_lock<CCriticalSection> lock(m_gameCallbackMutex);
+ m_gameCallback = nullptr;
+ }
+
+ // Reset callback
+ {
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+ m_callback = nullptr;
+ }
+
+ // Reset factory
+ {
+ std::unique_lock<CCriticalSection> lock(m_targetMutex);
+ m_factory = nullptr;
+ UpdateRenderTargets();
+ }
+}
+
+std::shared_ptr<CGUIRenderHandle> CGUIGameRenderManager::RegisterControl(CGUIGameControl& control)
+{
+ std::unique_lock<CCriticalSection> lock(m_targetMutex);
+
+ // Create handle for game control
+ std::shared_ptr<CGUIRenderHandle> renderHandle(new CGUIRenderControlHandle(*this, control));
+
+ std::shared_ptr<CGUIRenderTarget> renderTarget;
+ if (m_factory != nullptr)
+ renderTarget.reset(m_factory->CreateRenderControl(control));
+
+ m_renderTargets.insert(std::make_pair(renderHandle.get(), std::move(renderTarget)));
+
+ return renderHandle;
+}
+
+std::shared_ptr<CGUIRenderHandle> CGUIGameRenderManager::RegisterWindow(
+ CGameWindowFullScreen& window)
+{
+ std::unique_lock<CCriticalSection> lock(m_targetMutex);
+
+ // Create handle for game window
+ std::shared_ptr<CGUIRenderHandle> renderHandle(new CGUIRenderFullScreenHandle(*this, window));
+
+ std::shared_ptr<CGUIRenderTarget> renderTarget;
+ if (m_factory != nullptr)
+ renderTarget.reset(m_factory->CreateRenderFullScreen(window));
+
+ m_renderTargets.insert(std::make_pair(renderHandle.get(), std::move(renderTarget)));
+
+ return renderHandle;
+}
+
+std::shared_ptr<CGUIGameVideoHandle> CGUIGameRenderManager::RegisterDialog(
+ GAME::CDialogGameVideoSelect& dialog)
+{
+ return std::make_shared<CGUIGameVideoHandle>(*this);
+}
+
+std::shared_ptr<CGUIGameSettingsHandle> CGUIGameRenderManager::RegisterGameSettingsDialog()
+{
+ return std::make_shared<CGUIGameSettingsHandle>(*this);
+}
+
+void CGUIGameRenderManager::UnregisterHandle(CGUIRenderHandle* handle)
+{
+ std::unique_lock<CCriticalSection> lock(m_targetMutex);
+
+ m_renderTargets.erase(handle);
+}
+
+void CGUIGameRenderManager::Render(CGUIRenderHandle* handle)
+{
+ std::unique_lock<CCriticalSection> lock(m_targetMutex);
+
+ auto it = m_renderTargets.find(handle);
+ if (it != m_renderTargets.end())
+ {
+ const std::shared_ptr<CGUIRenderTarget>& renderTarget = it->second;
+ if (renderTarget)
+ renderTarget->Render();
+ }
+}
+
+void CGUIGameRenderManager::RenderEx(CGUIRenderHandle* handle)
+{
+ std::unique_lock<CCriticalSection> lock(m_targetMutex);
+
+ auto it = m_renderTargets.find(handle);
+ if (it != m_renderTargets.end())
+ {
+ const std::shared_ptr<CGUIRenderTarget>& renderTarget = it->second;
+ if (renderTarget)
+ renderTarget->RenderEx();
+ }
+}
+
+void CGUIGameRenderManager::ClearBackground(CGUIRenderHandle* handle)
+{
+ std::unique_lock<CCriticalSection> lock(m_targetMutex);
+
+ auto it = m_renderTargets.find(handle);
+ if (it != m_renderTargets.end())
+ {
+ const std::shared_ptr<CGUIRenderTarget>& renderTarget = it->second;
+ if (renderTarget)
+ renderTarget->ClearBackground();
+ }
+}
+
+bool CGUIGameRenderManager::IsDirty(CGUIRenderHandle* handle)
+{
+ std::unique_lock<CCriticalSection> lock(m_targetMutex);
+
+ auto it = m_renderTargets.find(handle);
+ if (it != m_renderTargets.end())
+ {
+ const std::shared_ptr<CGUIRenderTarget>& renderTarget = it->second;
+ if (renderTarget)
+ return renderTarget->IsDirty();
+ }
+
+ return false;
+}
+
+bool CGUIGameRenderManager::IsPlayingGame()
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ return m_callback != nullptr;
+}
+
+bool CGUIGameRenderManager::SupportsRenderFeature(RENDERFEATURE feature)
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ if (m_callback != nullptr)
+ return m_callback->SupportsRenderFeature(feature);
+
+ return false;
+}
+
+bool CGUIGameRenderManager::SupportsScalingMethod(SCALINGMETHOD method)
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ if (m_callback != nullptr)
+ return m_callback->SupportsScalingMethod(method);
+
+ return false;
+}
+
+std::string CGUIGameRenderManager::GameClientID()
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ if (m_gameCallback != nullptr)
+ return m_gameCallback->GameClientID();
+
+ return "";
+}
+
+std::string CGUIGameRenderManager::GetPlayingGame()
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ if (m_gameCallback != nullptr)
+ return m_gameCallback->GetPlayingGame();
+
+ return "";
+}
+
+std::string CGUIGameRenderManager::CreateSavestate(bool autosave)
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ if (m_gameCallback != nullptr)
+ return m_gameCallback->CreateSavestate(autosave);
+
+ return "";
+}
+
+bool CGUIGameRenderManager::UpdateSavestate(const std::string& savestatePath)
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ if (m_gameCallback != nullptr)
+ return m_gameCallback->UpdateSavestate(savestatePath);
+
+ return false;
+}
+
+bool CGUIGameRenderManager::LoadSavestate(const std::string& savestatePath)
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ if (m_gameCallback != nullptr)
+ return m_gameCallback->LoadSavestate(savestatePath);
+
+ return false;
+}
+
+void CGUIGameRenderManager::FreeSavestateResources(const std::string& savestatePath)
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ if (m_gameCallback != nullptr)
+ m_gameCallback->FreeSavestateResources(savestatePath);
+}
+
+void CGUIGameRenderManager::CloseOSD()
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ if (m_gameCallback != nullptr)
+ m_gameCallback->CloseOSDCallback();
+}
+
+void CGUIGameRenderManager::UpdateRenderTargets()
+{
+ if (m_factory != nullptr)
+ {
+ for (auto& it : m_renderTargets)
+ {
+ CGUIRenderHandle* handle = it.first;
+ std::shared_ptr<CGUIRenderTarget>& renderTarget = it.second;
+
+ if (!renderTarget)
+ renderTarget.reset(CreateRenderTarget(handle));
+ }
+ }
+ else
+ {
+ for (auto& it : m_renderTargets)
+ it.second.reset();
+ }
+}
+
+CGUIRenderTarget* CGUIGameRenderManager::CreateRenderTarget(CGUIRenderHandle* handle)
+{
+ switch (handle->Type())
+ {
+ case RENDER_HANDLE::CONTROL:
+ {
+ CGUIRenderControlHandle* controlHandle = static_cast<CGUIRenderControlHandle*>(handle);
+ return m_factory->CreateRenderControl(controlHandle->GetControl());
+ }
+ case RENDER_HANDLE::WINDOW:
+ {
+ CGUIRenderFullScreenHandle* fullScreenHandle =
+ static_cast<CGUIRenderFullScreenHandle*>(handle);
+ return m_factory->CreateRenderFullScreen(fullScreenHandle->GetWindow());
+ }
+ default:
+ break;
+ }
+
+ return nullptr;
+}
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIGameRenderManager.h b/xbmc/cores/RetroPlayer/guibridge/GUIGameRenderManager.h
new file mode 100644
index 0000000..0a974b7
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIGameRenderManager.h
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameAdvancedSettings;
+class CDialogGameVideoSelect;
+} // namespace GAME
+
+namespace RETRO
+{
+class CGameWindowFullScreen;
+class CGUIGameControl;
+class CGUIGameSettingsHandle;
+class CGUIGameVideoHandle;
+class CGUIRenderTargetFactory;
+class CGUIRenderHandle;
+class CGUIRenderTarget;
+class IGameCallback;
+class IRenderCallback;
+class IPlayback;
+
+/*!
+ * \brief Class to safely route commands between the GUI and RetroPlayer
+ *
+ * This class is brought up before the GUI and player core factory. It
+ * provides the GUI with safe access to a registered player.
+ *
+ * Access to the player is done through handles. When a handle is no
+ * longer needed, it should be destroyed.
+ *
+ * Three kinds of handles are provided:
+ *
+ * - CGUIRenderHandle
+ * Allows the holder to invoke render events
+ *
+ * - CGUIGameVideoHandle
+ * Allows the holder to query video properties, such as the filter
+ * or view mode.
+ *
+ * - CGUIGameSettingsHandle
+ * Allows the holder to query game properties, such as the ID of the
+ * game client or the game's filename.
+ *
+ * Each manager fulfills the following design requirements:
+ *
+ * 1. No assumption of player lifetimes
+ *
+ * 2. No assumption of GUI element lifetimes, as long as handles are
+ * destroyed before this class is destructed
+ *
+ * 3. No limit on the number of handles
+ */
+class CGUIGameRenderManager
+{
+ // Classes that call into the protected interface
+ friend class CGUIGameSettingsHandle;
+ friend class CGUIGameVideoHandle;
+ friend class CGUIRenderHandle;
+
+public:
+ CGUIGameRenderManager() = default;
+ ~CGUIGameRenderManager();
+
+ /*!
+ * \brief Register a RetroPlayer instance
+ *
+ * \param factory The interface for creating render targets exposed to the GUI
+ * \param callback The interface for querying video properties
+ * \param gameCallback The interface for querying game properties
+ */
+ void RegisterPlayer(CGUIRenderTargetFactory* factory,
+ IRenderCallback* callback,
+ IGameCallback* gameCallback);
+
+ /*!
+ * \brief Unregister a RetroPlayer instance
+ */
+ void UnregisterPlayer();
+
+ /*!
+ * \brief Register a GUI game control ("gamewindow" skin control)
+ *
+ * \param control The game control
+ *
+ * \return A handle to invoke render events
+ */
+ std::shared_ptr<CGUIRenderHandle> RegisterControl(CGUIGameControl& control);
+
+ /*!
+ * \brief Register a fullscreen game window ("FullscreenGame" window)
+ *
+ * \param window The game window
+ *
+ * \return A handle to invoke render events
+ */
+ std::shared_ptr<CGUIRenderHandle> RegisterWindow(CGameWindowFullScreen& window);
+
+ /*!
+ * \brief Register a video select dialog (for selecting video filters,
+ * view modes, etc.)
+ *
+ * \param dialog The video select dialog
+ *
+ * \return A handle to query game and video properties
+ */
+ std::shared_ptr<CGUIGameVideoHandle> RegisterDialog(GAME::CDialogGameVideoSelect& dialog);
+
+ /*!
+ * \brief Register a game settings dialog
+ *
+ * \return A handle to query game properties
+ */
+ std::shared_ptr<CGUIGameSettingsHandle> RegisterGameSettingsDialog();
+
+protected:
+ // Functions exposed to friend class CGUIRenderHandle
+ void UnregisterHandle(CGUIRenderHandle* handle);
+ void Render(CGUIRenderHandle* handle);
+ void RenderEx(CGUIRenderHandle* handle);
+ void ClearBackground(CGUIRenderHandle* handle);
+ bool IsDirty(CGUIRenderHandle* handle);
+
+ // Functions exposed to friend class CGUIGameVideoHandle
+ void UnregisterHandle(CGUIGameVideoHandle* handle) {}
+ bool IsPlayingGame();
+ bool SupportsRenderFeature(RENDERFEATURE feature);
+ bool SupportsScalingMethod(SCALINGMETHOD method);
+
+ // Functions exposed to CGUIGameSettingsHandle
+ void UnregisterHandle(CGUIGameSettingsHandle* handle) {}
+ std::string GameClientID();
+ std::string GetPlayingGame();
+ std::string CreateSavestate(bool autosave);
+ bool UpdateSavestate(const std::string& savestatePath);
+ bool LoadSavestate(const std::string& savestatePath);
+ void FreeSavestateResources(const std::string& savestatePath);
+ void CloseOSD();
+
+private:
+ /*!
+ * \brief Helper function to create or destroy render targets when a
+ * factory is registered/unregistered
+ */
+ void UpdateRenderTargets();
+
+ /*!
+ * \brief Helper function to create a render target
+ *
+ * \param handle The handle given to the registered GUI element
+ *
+ * \return A target to receive rendering commands
+ */
+ CGUIRenderTarget* CreateRenderTarget(CGUIRenderHandle* handle);
+
+ // Render events
+ CGUIRenderTargetFactory* m_factory = nullptr;
+ std::map<CGUIRenderHandle*, std::shared_ptr<CGUIRenderTarget>> m_renderTargets;
+ CCriticalSection m_targetMutex;
+
+ // Video properties
+ IRenderCallback* m_callback = nullptr;
+ CCriticalSection m_callbackMutex;
+
+ // Game properties
+ IGameCallback* m_gameCallback = nullptr;
+ CCriticalSection m_gameCallbackMutex;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIGameSettings.cpp b/xbmc/cores/RetroPlayer/guibridge/GUIGameSettings.cpp
new file mode 100644
index 0000000..4b030d8
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIGameSettings.cpp
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017-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 "GUIGameSettings.h"
+
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "settings/GameSettings.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace RETRO;
+
+CGUIGameSettings::CGUIGameSettings(CRPProcessInfo& processInfo)
+ : m_processInfo(processInfo), m_guiSettings(processInfo.GetRenderContext().GetGameSettings())
+{
+ // Reset game settings
+ m_guiSettings = m_processInfo.GetRenderContext().GetDefaultGameSettings();
+
+ UpdateSettings();
+
+ m_guiSettings.RegisterObserver(this);
+}
+
+CGUIGameSettings::~CGUIGameSettings()
+{
+ m_guiSettings.UnregisterObserver(this);
+}
+
+CRenderSettings CGUIGameSettings::GetSettings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ return m_renderSettings;
+}
+
+void CGUIGameSettings::Notify(const Observable& obs, const ObservableMessage msg)
+{
+ switch (msg)
+ {
+ case ObservableMessageSettingsChanged:
+ {
+ UpdateSettings();
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void CGUIGameSettings::UpdateSettings()
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ // Get settings from GUI
+ std::string videoFilter = m_guiSettings.VideoFilter();
+ STRETCHMODE stretchMode = m_guiSettings.StretchMode();
+ unsigned int rotationDegCCW = m_guiSettings.RotationDegCCW();
+
+ // Save settings for renderer
+ m_renderSettings.VideoSettings().SetVideoFilter(videoFilter);
+ m_renderSettings.VideoSettings().SetRenderStretchMode(stretchMode);
+ m_renderSettings.VideoSettings().SetRenderRotation(rotationDegCCW);
+}
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIGameSettings.h b/xbmc/cores/RetroPlayer/guibridge/GUIGameSettings.h
new file mode 100644
index 0000000..6400b8d
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIGameSettings.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017-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 "IGUIRenderSettings.h"
+#include "cores/RetroPlayer/rendering/RenderSettings.h"
+#include "threads/CriticalSection.h"
+#include "utils/Observer.h"
+
+#include <memory>
+
+class CGameSettings;
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfo;
+
+class CGUIGameSettings : public IGUIRenderSettings, public Observer
+{
+public:
+ CGUIGameSettings(CRPProcessInfo& processInfo);
+ ~CGUIGameSettings() override;
+
+ // implementation of IGUIRenderSettings
+ CRenderSettings GetSettings() const override;
+
+ // implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override;
+
+private:
+ void UpdateSettings();
+
+ // Construction parameters
+ CRPProcessInfo& m_processInfo;
+
+ // GUI parameters
+ CGameSettings& m_guiSettings;
+
+ // Render parameters
+ CRenderSettings m_renderSettings;
+
+ // Synchronization parameters
+ mutable CCriticalSection m_mutex;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIGameSettingsHandle.cpp b/xbmc/cores/RetroPlayer/guibridge/GUIGameSettingsHandle.cpp
new file mode 100644
index 0000000..9edba41
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIGameSettingsHandle.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 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 "GUIGameSettingsHandle.h"
+
+#include "GUIGameRenderManager.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CGUIGameSettingsHandle::CGUIGameSettingsHandle(CGUIGameRenderManager& renderManager)
+ : m_renderManager(renderManager)
+{
+}
+
+CGUIGameSettingsHandle::~CGUIGameSettingsHandle()
+{
+ m_renderManager.UnregisterHandle(this);
+}
+
+std::string CGUIGameSettingsHandle::GameClientID()
+{
+ return m_renderManager.GameClientID();
+}
+
+std::string CGUIGameSettingsHandle::GetPlayingGame()
+{
+ return m_renderManager.GetPlayingGame();
+}
+
+std::string CGUIGameSettingsHandle::CreateSavestate(bool autosave)
+{
+ return m_renderManager.CreateSavestate(autosave);
+}
+
+bool CGUIGameSettingsHandle::UpdateSavestate(const std::string& savestatePath)
+{
+ return m_renderManager.UpdateSavestate(savestatePath);
+}
+
+bool CGUIGameSettingsHandle::LoadSavestate(const std::string& savestatePath)
+{
+ return m_renderManager.LoadSavestate(savestatePath);
+}
+
+void CGUIGameSettingsHandle::FreeSavestateResources(const std::string& savestatePath)
+{
+ return m_renderManager.FreeSavestateResources(savestatePath);
+}
+
+void CGUIGameSettingsHandle::CloseOSD()
+{
+ m_renderManager.CloseOSD();
+}
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h b/xbmc/cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h
new file mode 100644
index 0000000..beddb90
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 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 <string>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGUIGameRenderManager;
+class IPlayback;
+
+class CGUIGameSettingsHandle
+{
+public:
+ CGUIGameSettingsHandle(CGUIGameRenderManager& renderManager);
+ virtual ~CGUIGameSettingsHandle();
+
+ /*!
+ * \brief Get the ID of the active game client
+ *
+ * \return The ID of the active game client, or empty string if a game is
+ * not playing
+ */
+ std::string GameClientID();
+
+ /*!
+ * \brief Get the full path of the game being played
+ *
+ * \return The game's path, or empty string if a game is not playing
+ */
+ std::string GetPlayingGame();
+
+ /*!
+ * \brief Create a savestate of the current game being played
+ *
+ * \param autosave True if the save was invoked automatically, or false if
+ * the save was invoked by a player
+ *
+ * \return The path to the created savestate file, or empty string on
+ * failure or if a game is not playing
+ */
+ std::string CreateSavestate(bool autosave);
+
+ /*!
+ * \brief Update a savestate for the current game being played
+ *
+ * \param savestatePath The path to the created savestate file returned by
+ * CreateSavestate()
+ *
+ * \return True if the savestate was updated successfully, false otherwise
+ */
+ bool UpdateSavestate(const std::string& savestatePath);
+
+ /*!
+ * \brief Load a savestate for the current game being played
+ *
+ * \param savestatePath The path to the created savestate file returned by
+ * CreateSavestate()
+ *
+ * \return True if the savestate was loaded successfully, false otherwise
+ */
+ bool LoadSavestate(const std::string& savestatePath);
+
+ /*!
+ * \brief Clear the video frame stored for the given statestate
+ *
+ * Useful to reclaim memory if a savestate has been deleted.
+ *
+ * \param savestatePath The path to the savestate file
+ */
+ void FreeSavestateResources(const std::string& savestatePath);
+
+ /*!
+ * \brief Close the in-game OSD
+ */
+ void CloseOSD();
+
+private:
+ // Construction parameters
+ CGUIGameRenderManager& m_renderManager;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIGameVideoHandle.cpp b/xbmc/cores/RetroPlayer/guibridge/GUIGameVideoHandle.cpp
new file mode 100644
index 0000000..521cf4b
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIGameVideoHandle.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017-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 "GUIGameVideoHandle.h"
+
+#include "GUIGameRenderManager.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CGUIGameVideoHandle::CGUIGameVideoHandle(CGUIGameRenderManager& renderManager)
+ : m_renderManager(renderManager)
+{
+}
+
+CGUIGameVideoHandle::~CGUIGameVideoHandle()
+{
+ m_renderManager.UnregisterHandle(this);
+}
+
+bool CGUIGameVideoHandle::IsPlayingGame()
+{
+ return m_renderManager.IsPlayingGame();
+}
+
+bool CGUIGameVideoHandle::SupportsRenderFeature(RENDERFEATURE feature)
+{
+ return m_renderManager.SupportsRenderFeature(feature);
+}
+
+bool CGUIGameVideoHandle::SupportsScalingMethod(SCALINGMETHOD method)
+{
+ return m_renderManager.SupportsScalingMethod(method);
+}
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIGameVideoHandle.h b/xbmc/cores/RetroPlayer/guibridge/GUIGameVideoHandle.h
new file mode 100644
index 0000000..4d26fe4
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIGameVideoHandle.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGUIGameRenderManager;
+
+class CGUIGameVideoHandle
+{
+public:
+ CGUIGameVideoHandle(CGUIGameRenderManager& renderManager);
+ virtual ~CGUIGameVideoHandle();
+
+ bool IsPlayingGame();
+ bool SupportsRenderFeature(RENDERFEATURE feature);
+ bool SupportsScalingMethod(SCALINGMETHOD method);
+
+private:
+ // Construction parameters
+ CGUIGameRenderManager& m_renderManager;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIRenderHandle.cpp b/xbmc/cores/RetroPlayer/guibridge/GUIRenderHandle.cpp
new file mode 100644
index 0000000..a4b6403
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIRenderHandle.cpp
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017-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 "GUIRenderHandle.h"
+
+#include "GUIGameRenderManager.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+// --- CGUIRenderHandle --------------------------------------------------------
+
+CGUIRenderHandle::CGUIRenderHandle(CGUIGameRenderManager& renderManager, RENDER_HANDLE type)
+ : m_renderManager(renderManager), m_type(type)
+{
+}
+
+CGUIRenderHandle::~CGUIRenderHandle()
+{
+ m_renderManager.UnregisterHandle(this);
+}
+
+void CGUIRenderHandle::Render()
+{
+ m_renderManager.Render(this);
+}
+
+void CGUIRenderHandle::RenderEx()
+{
+ m_renderManager.RenderEx(this);
+}
+
+bool CGUIRenderHandle::IsDirty()
+{
+ return m_renderManager.IsDirty(this);
+}
+
+void CGUIRenderHandle::ClearBackground()
+{
+ m_renderManager.ClearBackground(this);
+}
+
+// --- CGUIRenderControlHandle -------------------------------------------------
+
+CGUIRenderControlHandle::CGUIRenderControlHandle(CGUIGameRenderManager& renderManager,
+ CGUIGameControl& control)
+ : CGUIRenderHandle(renderManager, RENDER_HANDLE::CONTROL), m_control(control)
+{
+}
+
+// --- CGUIRenderFullScreenHandle ----------------------------------------------
+
+CGUIRenderFullScreenHandle::CGUIRenderFullScreenHandle(CGUIGameRenderManager& renderManager,
+ CGameWindowFullScreen& window)
+ : CGUIRenderHandle(renderManager, RENDER_HANDLE::WINDOW), m_window(window)
+{
+}
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIRenderHandle.h b/xbmc/cores/RetroPlayer/guibridge/GUIRenderHandle.h
new file mode 100644
index 0000000..f5b8d5b
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIRenderHandle.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017-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
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGameWindowFullScreen;
+class CGUIGameControl;
+class CGUIGameRenderManager;
+
+enum class RENDER_HANDLE
+{
+ CONTROL,
+ WINDOW,
+};
+
+// --- CGUIRenderHandle ------------------------------------------------------
+
+class CGUIRenderHandle
+{
+public:
+ CGUIRenderHandle(CGUIGameRenderManager& renderManager, RENDER_HANDLE type);
+ virtual ~CGUIRenderHandle();
+
+ RENDER_HANDLE Type() const { return m_type; }
+
+ void Render();
+ void RenderEx();
+ bool IsDirty();
+ void ClearBackground();
+
+private:
+ // Construction parameters
+ CGUIGameRenderManager& m_renderManager;
+ const RENDER_HANDLE m_type;
+};
+
+// --- CGUIRenderControlHandle -----------------------------------------------
+
+class CGUIRenderControlHandle : public CGUIRenderHandle
+{
+public:
+ CGUIRenderControlHandle(CGUIGameRenderManager& renderManager, CGUIGameControl& control);
+ ~CGUIRenderControlHandle() override = default;
+
+ CGUIGameControl& GetControl() { return m_control; }
+
+private:
+ // Construction parameters
+ CGUIGameControl& m_control;
+};
+
+// --- CGUIRenderFullScreenHandle --------------------------------------------
+
+class CGUIRenderFullScreenHandle : public CGUIRenderHandle
+{
+public:
+ CGUIRenderFullScreenHandle(CGUIGameRenderManager& renderManager, CGameWindowFullScreen& window);
+ ~CGUIRenderFullScreenHandle() override = default;
+
+ CGameWindowFullScreen& GetWindow() { return m_window; }
+
+private:
+ // Construction parameters
+ CGameWindowFullScreen& m_window;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIRenderTarget.cpp b/xbmc/cores/RetroPlayer/guibridge/GUIRenderTarget.cpp
new file mode 100644
index 0000000..15993fc
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIRenderTarget.cpp
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017-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 "GUIRenderTarget.h"
+
+#include "cores/RetroPlayer/guicontrols/GUIGameControl.h"
+#include "cores/RetroPlayer/guiwindows/GameWindowFullScreen.h"
+#include "cores/RetroPlayer/rendering/IRenderManager.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+// --- CGUIRenderTarget --------------------------------------------------------
+
+CGUIRenderTarget::CGUIRenderTarget(IRenderManager* renderManager) : m_renderManager(renderManager)
+{
+}
+
+// --- CGUIRenderControl -------------------------------------------------------
+
+CGUIRenderControl::CGUIRenderControl(IRenderManager* renderManager, CGUIGameControl& gameControl)
+ : CGUIRenderTarget(renderManager), m_gameControl(gameControl)
+{
+}
+
+void CGUIRenderControl::Render()
+{
+ m_renderManager->RenderControl(true, true, m_gameControl.GetRenderRegion(),
+ m_gameControl.GetRenderSettings());
+}
+
+void CGUIRenderControl::RenderEx()
+{
+ //! @todo
+ // m_renderManager->RenderControl(false, false, m_gameControl.GetRenderRegion(),
+ // m_gameControl.GetRenderSettings());
+}
+
+// --- CGUIRenderFullScreen ----------------------------------------------------
+
+CGUIRenderFullScreen::CGUIRenderFullScreen(IRenderManager* renderManager,
+ CGameWindowFullScreen& window)
+ : CGUIRenderTarget(renderManager), m_window(window)
+{
+}
+
+void CGUIRenderFullScreen::Render()
+{
+ m_renderManager->RenderWindow(true, m_window.GetCoordsRes());
+}
+
+void CGUIRenderFullScreen::RenderEx()
+{
+ //! @todo
+ // m_renderManager->RenderWindow(false, m_window.GetCoordsRes());
+}
+
+void CGUIRenderFullScreen::ClearBackground()
+{
+ m_renderManager->ClearBackground();
+}
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIRenderTarget.h b/xbmc/cores/RetroPlayer/guibridge/GUIRenderTarget.h
new file mode 100644
index 0000000..574a13f
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIRenderTarget.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017-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
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGameWindowFullScreen;
+class CGUIGameControl;
+class IRenderManager;
+
+// --- CGUIRenderTarget ------------------------------------------------------
+
+/*!
+ * \brief A target of rendering commands
+ *
+ * This class abstracts the destination of rendering commands. As a result,
+ * controls and windows are given a unified API.
+ */
+class CGUIRenderTarget
+{
+public:
+ CGUIRenderTarget(IRenderManager* renderManager);
+
+ virtual ~CGUIRenderTarget() = default;
+
+ /*!
+ * \brief Draw the frame to the rendering area
+ */
+ virtual void Render() = 0;
+
+ /*!
+ * \brief Draw the frame to the rendering area differently somehow
+ */
+ virtual void RenderEx() = 0;
+
+ /*!
+ * \brief Clear the background of the rendering area
+ */
+ virtual void ClearBackground() {} //! @todo
+
+ /*!
+ * \brief Check of the rendering area is dirty
+ */
+ virtual bool IsDirty() { return true; } //! @todo
+
+protected:
+ // Construction parameters
+ IRenderManager* const m_renderManager;
+};
+
+// --- CGUIRenderControl -----------------------------------------------------
+
+class CGUIRenderControl : public CGUIRenderTarget
+{
+public:
+ CGUIRenderControl(IRenderManager* renderManager, CGUIGameControl& gameControl);
+ ~CGUIRenderControl() override = default;
+
+ // implementation of CGUIRenderTarget
+ void Render() override;
+ void RenderEx() override;
+
+private:
+ // Construction parameters
+ CGUIGameControl& m_gameControl;
+};
+
+// --- CGUIRenderFullScreen --------------------------------------------------
+
+class CGUIRenderFullScreen : public CGUIRenderTarget
+{
+public:
+ CGUIRenderFullScreen(IRenderManager* renderManager, CGameWindowFullScreen& window);
+ ~CGUIRenderFullScreen() override = default;
+
+ // implementation of CGUIRenderTarget
+ void Render() override;
+ void RenderEx() override;
+ void ClearBackground() override;
+
+private:
+ // Construction parameters
+ CGameWindowFullScreen& m_window;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIRenderTargetFactory.cpp b/xbmc/cores/RetroPlayer/guibridge/GUIRenderTargetFactory.cpp
new file mode 100644
index 0000000..05675c3
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIRenderTargetFactory.cpp
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017-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 "GUIRenderTargetFactory.h"
+
+#include "GUIRenderTarget.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CGUIRenderTargetFactory::CGUIRenderTargetFactory(IRenderManager* renderManager)
+ : m_renderManager(renderManager)
+{
+}
+
+CGUIRenderTarget* CGUIRenderTargetFactory::CreateRenderFullScreen(CGameWindowFullScreen& window)
+{
+ return new CGUIRenderFullScreen(m_renderManager, window);
+}
+
+CGUIRenderTarget* CGUIRenderTargetFactory::CreateRenderControl(CGUIGameControl& gameControl)
+{
+ return new CGUIRenderControl(m_renderManager, gameControl);
+}
diff --git a/xbmc/cores/RetroPlayer/guibridge/GUIRenderTargetFactory.h b/xbmc/cores/RetroPlayer/guibridge/GUIRenderTargetFactory.h
new file mode 100644
index 0000000..8066854
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/GUIRenderTargetFactory.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017-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
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGameWindowFullScreen;
+class CGUIGameControl;
+class IRenderManager;
+class CGUIRenderTarget;
+
+class CGUIRenderTargetFactory
+{
+public:
+ CGUIRenderTargetFactory(IRenderManager* renderManager);
+
+ /*!
+ * \brief Create a render target for the fullscreen window
+ */
+ CGUIRenderTarget* CreateRenderFullScreen(CGameWindowFullScreen& window);
+
+ /*!
+ * \brief Create a render target for a game control
+ */
+ CGUIRenderTarget* CreateRenderControl(CGUIGameControl& gameControl);
+
+private:
+ // Construction parameters
+ IRenderManager* m_renderManager;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guibridge/IGUIRenderSettings.h b/xbmc/cores/RetroPlayer/guibridge/IGUIRenderSettings.h
new file mode 100644
index 0000000..8fb0fd1
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/IGUIRenderSettings.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2017-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/RetroPlayer/rendering/RenderSettings.h"
+#include "utils/Geometry.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+/*!
+ * \brief Interface to pass render settings from the GUI to the renderer
+ */
+class IGUIRenderSettings
+{
+public:
+ virtual ~IGUIRenderSettings() = default;
+
+ /*!
+ * \brief Returns true if this render target has a video filter set
+ */
+ virtual bool HasVideoFilter() const { return true; }
+
+ /*!
+ * \brief Returns true if this render target has a stretch mode set
+ */
+ virtual bool HasStretchMode() const { return true; }
+
+ /*!
+ * \brief Returns true if this render target has a video rotation set
+ */
+ virtual bool HasRotation() const { return true; }
+
+ /*!
+ * \brief Returns true if this render target has a path to a savestate for
+ * showing pixel data
+ */
+ virtual bool HasPixels() const { return true; }
+
+ /*!
+ * \brief Get the settings used to render this target
+ *
+ * \return The render settings
+ */
+ virtual CRenderSettings GetSettings() const = 0;
+
+ /*!
+ * \brief Get the dimensions of this target
+ *
+ * Dimensions are ignored for fullscreen windows.
+ *
+ * \return The destination dimensions, or unused for fullscreen window
+ */
+ virtual CRect GetDimensions() const { return CRect{}; }
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guibridge/IGameCallback.h b/xbmc/cores/RetroPlayer/guibridge/IGameCallback.h
new file mode 100644
index 0000000..057dd04
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/IGameCallback.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 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 <string>
+
+namespace KODI
+{
+namespace RETRO
+{
+class IPlayback;
+
+class IGameCallback
+{
+public:
+ virtual ~IGameCallback() = default;
+
+ /*!
+ * \brief Get the game client being used to play the game
+ *
+ * \return The game client's ID, or empty if no game is being played
+ */
+ virtual std::string GameClientID() const = 0;
+
+ /*!
+ * \brief Get the game that is being played
+ *
+ * \return The path to the game, or empty if no game is being played
+ */
+ virtual std::string GetPlayingGame() const = 0;
+
+ /*!
+ * \brief Creates a savestate
+ *
+ * \param autosave Whether the save type is auto
+ *
+ * \return The path to the created savestate, or empty on error
+ */
+ virtual std::string CreateSavestate(bool autosave) = 0;
+
+ /*!
+ * \brief Updates a savestate with the current game being played
+ *
+ * \param savestatePath The path to the savestate
+ *
+ * \return True if the savestate was updated, false on error
+ */
+ virtual bool UpdateSavestate(const std::string& savestatePath) = 0;
+
+ /*!
+ * \brief Loads a savestate
+ *
+ * \param savestatePath The path to the savestate
+ *
+ * \return True if the savestate was loaded, false on error
+ */
+ virtual bool LoadSavestate(const std::string& savestatePath) = 0;
+
+ /*!
+ * \brief Frees resources allocated to the savestate, such as its video thumbnail
+ *
+ * \param savestatePath The path to the savestate
+ */
+ virtual void FreeSavestateResources(const std::string& savestatePath) = 0;
+
+ /*!
+ * \brief Closes the OSD
+ */
+ virtual void CloseOSDCallback() = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guibridge/IRenderCallback.h b/xbmc/cores/RetroPlayer/guibridge/IRenderCallback.h
new file mode 100644
index 0000000..948c6cd
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guibridge/IRenderCallback.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class IRenderCallback
+{
+public:
+ virtual ~IRenderCallback() = default;
+
+ virtual bool SupportsRenderFeature(RENDERFEATURE feature) const = 0;
+ virtual bool SupportsScalingMethod(SCALINGMETHOD method) const = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guicontrols/CMakeLists.txt b/xbmc/cores/RetroPlayer/guicontrols/CMakeLists.txt
new file mode 100644
index 0000000..86aba95
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guicontrols/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES GUIGameControl.cpp
+ GUIRenderSettings.cpp
+)
+
+set(HEADERS GUIGameControl.h
+ GUIRenderSettings.h
+)
+
+core_add_library(retroplayer_guicontrols)
diff --git a/xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.cpp b/xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.cpp
new file mode 100644
index 0000000..bf2ceb9
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.cpp
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2017-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 "GUIGameControl.h"
+
+#include "GUIRenderSettings.h"
+#include "ServiceBroker.h"
+#include "cores/RetroPlayer/RetroPlayerUtils.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIRenderHandle.h"
+#include "settings/GameSettings.h"
+#include "settings/MediaSettings.h"
+#include "utils/Geometry.h"
+#include "utils/StringUtils.h"
+
+#include <sstream>
+
+using namespace KODI;
+using namespace RETRO;
+
+CGUIGameControl::CGUIGameControl(
+ int parentID, int controlID, float posX, float posY, float width, float height)
+ : CGUIControl(parentID, controlID, posX, posY, width, height),
+ m_renderSettings(new CGUIRenderSettings(*this))
+{
+ // Initialize CGUIControl
+ ControlType = GUICONTROL_GAME;
+
+ m_renderSettings->SetDimensions(CRect(CPoint(posX, posY), CSize(width, height)));
+
+ RegisterControl();
+}
+
+CGUIGameControl::CGUIGameControl(const CGUIGameControl& other)
+ : CGUIControl(other),
+ m_videoFilterInfo(other.m_videoFilterInfo),
+ m_stretchModeInfo(other.m_stretchModeInfo),
+ m_rotationInfo(other.m_rotationInfo),
+ m_pixelInfo(other.m_pixelInfo),
+ m_bHasVideoFilter(other.m_bHasVideoFilter),
+ m_bHasStretchMode(other.m_bHasStretchMode),
+ m_bHasRotation(other.m_bHasRotation),
+ m_bHasPixels(other.m_bHasPixels),
+ m_renderSettings(new CGUIRenderSettings(*this))
+{
+ m_renderSettings->SetSettings(other.m_renderSettings->GetSettings());
+ m_renderSettings->SetDimensions(CRect(CPoint(m_posX, m_posY), CSize(m_width, m_height)));
+
+ RegisterControl();
+}
+
+CGUIGameControl::~CGUIGameControl()
+{
+ UnregisterControl();
+}
+
+void CGUIGameControl::SetVideoFilter(const GUILIB::GUIINFO::CGUIInfoLabel& videoFilter)
+{
+ m_videoFilterInfo = videoFilter;
+}
+
+void CGUIGameControl::SetStretchMode(const GUILIB::GUIINFO::CGUIInfoLabel& stretchMode)
+{
+ m_stretchModeInfo = stretchMode;
+}
+
+void CGUIGameControl::SetRotation(const KODI::GUILIB::GUIINFO::CGUIInfoLabel& rotation)
+{
+ m_rotationInfo = rotation;
+}
+
+void CGUIGameControl::SetPixels(const KODI::GUILIB::GUIINFO::CGUIInfoLabel& pixels)
+{
+ m_pixelInfo = pixels;
+}
+
+IGUIRenderSettings* CGUIGameControl::GetRenderSettings() const
+{
+ return m_renderSettings.get();
+}
+
+void CGUIGameControl::Process(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ //! @todo Proper processing which marks when its actually changed
+ if (m_renderHandle->IsDirty())
+ MarkDirtyRegion();
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIGameControl::Render()
+{
+ m_renderHandle->Render();
+
+ CGUIControl::Render();
+}
+
+void CGUIGameControl::RenderEx()
+{
+ m_renderHandle->RenderEx();
+
+ CGUIControl::RenderEx();
+}
+
+bool CGUIGameControl::CanFocus() const
+{
+ // Unfocusable
+ return false;
+}
+
+void CGUIGameControl::SetPosition(float posX, float posY)
+{
+ CGUIControl::SetPosition(posX, posY);
+ m_renderSettings->SetDimensions(CRect(CPoint(posX, posY), CSize(m_width, m_height)));
+}
+
+void CGUIGameControl::SetWidth(float width)
+{
+ CGUIControl::SetWidth(width);
+ m_renderSettings->SetDimensions(CRect(CPoint(m_posX, m_posY), CSize(width, m_height)));
+}
+
+void CGUIGameControl::SetHeight(float height)
+{
+ CGUIControl::SetHeight(height);
+ m_renderSettings->SetDimensions(CRect(CPoint(m_posX, m_posY), CSize(m_width, height)));
+}
+
+void CGUIGameControl::UpdateInfo(const CGUIListItem* item /* = nullptr */)
+{
+ if (item)
+ {
+ Reset();
+
+ std::string strVideoFilter = m_videoFilterInfo.GetItemLabel(item);
+ if (!strVideoFilter.empty())
+ {
+ m_renderSettings->SetVideoFilter(strVideoFilter);
+ m_bHasVideoFilter = true;
+ }
+
+ std::string strStretchMode = m_stretchModeInfo.GetItemLabel(item);
+ if (!strStretchMode.empty())
+ {
+ STRETCHMODE stretchMode = CRetroPlayerUtils::IdentifierToStretchMode(strStretchMode);
+ m_renderSettings->SetStretchMode(stretchMode);
+ m_bHasStretchMode = true;
+ }
+
+ std::string strRotation = m_rotationInfo.GetItemLabel(item);
+ if (StringUtils::IsNaturalNumber(strRotation))
+ {
+ unsigned int rotation;
+ std::istringstream(strRotation) >> rotation;
+ m_renderSettings->SetRotationDegCCW(rotation);
+ m_bHasRotation = true;
+ }
+
+ std::string strPixels = m_pixelInfo.GetItemLabel(item);
+ if (!strPixels.empty())
+ {
+ m_renderSettings->SetPixels(strPixels);
+ m_bHasPixels = true;
+ }
+ }
+}
+
+void CGUIGameControl::Reset()
+{
+ m_bHasVideoFilter = false;
+ m_bHasStretchMode = false;
+ m_bHasRotation = false;
+ m_bHasPixels = false;
+ m_renderSettings->Reset();
+}
+
+void CGUIGameControl::RegisterControl()
+{
+ m_renderHandle = CServiceBroker::GetGameRenderManager().RegisterControl(*this);
+}
+
+void CGUIGameControl::UnregisterControl()
+{
+ m_renderHandle.reset();
+}
diff --git a/xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.dox b/xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.dox
new file mode 100644
index 0000000..6350110
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.dox
@@ -0,0 +1,45 @@
+/*!
+
+\page Game_Control Game Control
+\brief **Used to display the currently playing game, with optional effects,
+whilst in the GUI.**
+
+\tableofcontents
+
+The gamewindow control is used for displaying the currently playing game
+elsewhere in the Kodi GUI. You can choose the position, and size of the game
+displayed, as well as various effects. Note that the control is only rendered if
+game is being played.
+
+
+--------------------------------------------------------------------------------
+\section Game_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="gamewindow" id="2">
+ <description>My first game control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Game_Control_sect2 Available tags
+
+The [default control](http://kodi.wiki/view/Default_Control_Tags) tags are
+applicable to this control.
+
+
+--------------------------------------------------------------------------------
+\section Game_Control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.h b/xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.h
new file mode 100644
index 0000000..a386952
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guicontrols/GUIGameControl.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017-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/GUIControl.h"
+#include "guilib/guiinfo/GUIInfoLabel.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGUIRenderSettings;
+class CGUIRenderHandle;
+class IGUIRenderSettings;
+
+// Label to use when disabling rendering via the <pixels> property. A path
+// to pixel data is expected, but instead this constant can be provided to
+// skip a file existence check in the renderer.
+constexpr const char* NO_PIXEL_DATA = "-";
+
+class CGUIGameControl : public CGUIControl
+{
+public:
+ CGUIGameControl(int parentID, int controlID, float posX, float posY, float width, float height);
+ CGUIGameControl(const CGUIGameControl& other);
+ ~CGUIGameControl() override;
+
+ // GUI functions
+ void SetVideoFilter(const KODI::GUILIB::GUIINFO::CGUIInfoLabel& videoFilter);
+ void SetStretchMode(const KODI::GUILIB::GUIINFO::CGUIInfoLabel& stretchMode);
+ void SetRotation(const KODI::GUILIB::GUIINFO::CGUIInfoLabel& rotation);
+ void SetPixels(const KODI::GUILIB::GUIINFO::CGUIInfoLabel& pixels);
+
+ // Rendering functions
+ bool HasVideoFilter() const { return m_bHasVideoFilter; }
+ bool HasStretchMode() const { return m_bHasStretchMode; }
+ bool HasRotation() const { return m_bHasRotation; }
+ bool HasPixels() const { return m_bHasPixels; }
+ IGUIRenderSettings* GetRenderSettings() const;
+
+ // implementation of CGUIControl
+ CGUIGameControl* Clone() const override { return new CGUIGameControl(*this); }
+ void Process(unsigned int currentTime, CDirtyRegionList& dirtyregions) override;
+ void Render() override;
+ void RenderEx() override;
+ bool CanFocus() const override;
+ void SetPosition(float posX, float posY) override;
+ void SetWidth(float width) override;
+ void SetHeight(float height) override;
+ void UpdateInfo(const CGUIListItem* item = nullptr) override;
+
+private:
+ void Reset();
+
+ void RegisterControl();
+ void UnregisterControl();
+
+ // GUI properties
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_videoFilterInfo;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_stretchModeInfo;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_rotationInfo;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_pixelInfo;
+
+ // Rendering properties
+ bool m_bHasVideoFilter = false;
+ bool m_bHasStretchMode = false;
+ bool m_bHasRotation = false;
+ bool m_bHasPixels = false;
+ std::unique_ptr<CGUIRenderSettings> m_renderSettings;
+ std::shared_ptr<CGUIRenderHandle> m_renderHandle;
+};
+
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guicontrols/GUIRenderSettings.cpp b/xbmc/cores/RetroPlayer/guicontrols/GUIRenderSettings.cpp
new file mode 100644
index 0000000..66cb4ea
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guicontrols/GUIRenderSettings.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2017-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 "GUIRenderSettings.h"
+
+#include "GUIGameControl.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace RETRO;
+
+CGUIRenderSettings::CGUIRenderSettings(CGUIGameControl& guiControl) : m_guiControl(guiControl)
+{
+}
+
+bool CGUIRenderSettings::HasVideoFilter() const
+{
+ return m_guiControl.HasVideoFilter();
+}
+
+bool CGUIRenderSettings::HasStretchMode() const
+{
+ return m_guiControl.HasStretchMode();
+}
+
+bool CGUIRenderSettings::HasRotation() const
+{
+ return m_guiControl.HasRotation();
+}
+
+bool CGUIRenderSettings::HasPixels() const
+{
+ return m_guiControl.HasPixels();
+}
+
+CRenderSettings CGUIRenderSettings::GetSettings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ return m_renderSettings;
+}
+
+CRect CGUIRenderSettings::GetDimensions() const
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ return m_renderDimensions;
+}
+
+void CGUIRenderSettings::Reset()
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ return m_renderSettings.Reset();
+}
+
+void CGUIRenderSettings::SetSettings(CRenderSettings settings)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ m_renderSettings = settings;
+}
+
+void CGUIRenderSettings::SetDimensions(const CRect& dimensions)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ m_renderDimensions = dimensions;
+}
+
+void CGUIRenderSettings::SetVideoFilter(const std::string& videoFilter)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ m_renderSettings.VideoSettings().SetVideoFilter(videoFilter);
+}
+
+void CGUIRenderSettings::SetStretchMode(STRETCHMODE stretchMode)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ m_renderSettings.VideoSettings().SetRenderStretchMode(stretchMode);
+}
+
+void CGUIRenderSettings::SetRotationDegCCW(unsigned int rotationDegCCW)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ m_renderSettings.VideoSettings().SetRenderRotation(rotationDegCCW);
+}
+
+void CGUIRenderSettings::SetPixels(const std::string& pixelPath)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ m_renderSettings.VideoSettings().SetPixels(pixelPath);
+}
diff --git a/xbmc/cores/RetroPlayer/guicontrols/GUIRenderSettings.h b/xbmc/cores/RetroPlayer/guicontrols/GUIRenderSettings.h
new file mode 100644
index 0000000..80ba115
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guicontrols/GUIRenderSettings.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+#include "cores/RetroPlayer/guibridge/IGUIRenderSettings.h"
+#include "cores/RetroPlayer/rendering/RenderSettings.h"
+#include "threads/CriticalSection.h"
+#include "utils/Geometry.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGUIGameControl;
+
+class CGUIRenderSettings : public IGUIRenderSettings
+{
+public:
+ CGUIRenderSettings(CGUIGameControl& guiControl);
+ ~CGUIRenderSettings() override = default;
+
+ // implementation of IGUIRenderSettings
+ bool HasVideoFilter() const override;
+ bool HasStretchMode() const override;
+ bool HasRotation() const override;
+ bool HasPixels() const override;
+ CRenderSettings GetSettings() const override;
+ CRect GetDimensions() const override;
+
+ // Render functions
+ void Reset();
+ void SetSettings(CRenderSettings settings);
+ void SetDimensions(const CRect& dimensions);
+ void SetVideoFilter(const std::string& videoFilter);
+ void SetStretchMode(STRETCHMODE stretchMode);
+ void SetRotationDegCCW(unsigned int rotationDegCCW);
+ void SetPixels(const std::string& pixelPath);
+
+private:
+ // Construction parameters
+ CGUIGameControl& m_guiControl;
+
+ // Render parameters
+ CRenderSettings m_renderSettings;
+ CRect m_renderDimensions;
+
+ // Synchronization parameters
+ mutable CCriticalSection m_mutex;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guiplayback/CMakeLists.txt b/xbmc/cores/RetroPlayer/guiplayback/CMakeLists.txt
new file mode 100644
index 0000000..4750111
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guiplayback/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES GUIPlaybackControl.cpp
+)
+
+set(HEADERS GUIPlaybackControl.h
+)
+
+core_add_library(retroplayer_guiplayback)
diff --git a/xbmc/cores/RetroPlayer/guiplayback/GUIPlaybackControl.cpp b/xbmc/cores/RetroPlayer/guiplayback/GUIPlaybackControl.cpp
new file mode 100644
index 0000000..054454f
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guiplayback/GUIPlaybackControl.cpp
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 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 "GUIPlaybackControl.h"
+
+#include "ServiceBroker.h"
+#include "games/dialogs/osd/DialogGameOSD.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CGUIPlaybackControl::CGUIPlaybackControl(IPlaybackCallback& callback) : m_callback(callback)
+{
+}
+
+CGUIPlaybackControl::~CGUIPlaybackControl() = default;
+
+void CGUIPlaybackControl::FrameMove()
+{
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui == nullptr)
+ return;
+
+ const int windowId = gui->GetWindowManager().GetActiveWindow();
+ const int dialogId = gui->GetWindowManager().GetActiveWindowOrDialog();
+
+ // Check if game has entered fullscreen yet
+ const bool bFullscreen = (windowId == WINDOW_FULLSCREEN_GAME);
+
+ // Check if game is in the OSD dialog
+ const bool bInMenu = (dialogId != WINDOW_FULLSCREEN_GAME);
+
+ // Check if game should play in the background of dialog
+ const bool bInBackground = GAME::CDialogGameOSD::PlayInBackground(dialogId);
+
+ GuiState nextState = NextState(bFullscreen, bInMenu, bInBackground);
+ if (nextState != m_state)
+ {
+ m_state = nextState;
+
+ double targetSpeed = GetTargetSpeed(m_state);
+ if (m_previousSpeed != targetSpeed)
+ {
+ m_previousSpeed = targetSpeed;
+ m_callback.SetPlaybackSpeed(targetSpeed);
+ }
+
+ m_callback.EnableInput(AcceptsInput(m_state));
+ }
+}
+
+CGUIPlaybackControl::GuiState CGUIPlaybackControl::NextState(bool bFullscreen,
+ bool bInMenu,
+ bool bInBackground)
+{
+ GuiState newState = m_state;
+
+ switch (m_state)
+ {
+ case GuiState::UNKNOWN:
+ {
+ // Wait for game to enter fullscreen
+ if (bFullscreen)
+ newState = GuiState::FULLSCREEN;
+ break;
+ }
+ case GuiState::FULLSCREEN:
+ {
+ if (bInMenu)
+ {
+ if (bInBackground)
+ newState = GuiState::MENU_PLAYING;
+ else
+ newState = GuiState::MENU_PAUSED;
+ }
+ break;
+ }
+ case GuiState::MENU_PAUSED:
+ {
+ if (!bInMenu)
+ newState = GuiState::FULLSCREEN;
+ else if (bInBackground)
+ newState = GuiState::MENU_PLAYING;
+ break;
+ }
+ case GuiState::MENU_PLAYING:
+ {
+ if (!bInBackground)
+ {
+ if (!bInMenu)
+ newState = GuiState::FULLSCREEN;
+ else
+ newState = GuiState::MENU_PAUSED;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return newState;
+}
+
+double CGUIPlaybackControl::GetTargetSpeed(GuiState state)
+{
+ double targetSpeed = 0.0;
+
+ switch (state)
+ {
+ case GuiState::FULLSCREEN:
+ {
+ targetSpeed = 1.0;
+ break;
+ }
+ case GuiState::MENU_PAUSED:
+ {
+ targetSpeed = 0.0;
+ break;
+ }
+ case GuiState::MENU_PLAYING:
+ {
+ targetSpeed = 1.0;
+ break;
+ }
+ default:
+ break;
+ }
+
+ return targetSpeed;
+}
+
+bool CGUIPlaybackControl::AcceptsInput(GuiState state)
+{
+ bool bEnableInput = false;
+
+ switch (state)
+ {
+ case GuiState::FULLSCREEN:
+ {
+ bEnableInput = true;
+ break;
+ }
+ default:
+ break;
+ }
+
+ return bEnableInput;
+}
diff --git a/xbmc/cores/RetroPlayer/guiplayback/GUIPlaybackControl.h b/xbmc/cores/RetroPlayer/guiplayback/GUIPlaybackControl.h
new file mode 100644
index 0000000..48baced
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guiplayback/GUIPlaybackControl.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 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/RetroPlayer/playback/IPlaybackControl.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+/*!
+ * \brief Class to control playback by monitoring OSD status
+ */
+class CGUIPlaybackControl : public IPlaybackControl
+{
+public:
+ CGUIPlaybackControl(IPlaybackCallback& callback);
+
+ ~CGUIPlaybackControl() override;
+
+ // Implementation of IPlaybackControl
+ void FrameMove() override;
+
+private:
+ enum class GuiState
+ {
+ UNKNOWN,
+ FULLSCREEN,
+ MENU_PAUSED,
+ MENU_PLAYING,
+ };
+
+ // Helper functions
+ GuiState NextState(bool bFullscreen, bool bInMenu, bool bInBackground);
+ static double GetTargetSpeed(GuiState state);
+ static bool AcceptsInput(GuiState state);
+
+ // Construction parameters
+ IPlaybackCallback& m_callback;
+
+ // State parameters
+ GuiState m_state = GuiState::UNKNOWN;
+ double m_previousSpeed = 0.0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guiwindows/CMakeLists.txt b/xbmc/cores/RetroPlayer/guiwindows/CMakeLists.txt
new file mode 100644
index 0000000..aaa05a2
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guiwindows/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES GameWindowFullScreen.cpp
+ GameWindowFullScreenText.cpp
+)
+
+set(HEADERS GameWindowFullScreen.h
+ GameWindowFullScreenText.h
+)
+
+core_add_library(retroplayer_guiwindows)
diff --git a/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreen.cpp b/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreen.cpp
new file mode 100644
index 0000000..f50ed7d
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreen.cpp
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2017-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 "GameWindowFullScreen.h"
+
+#include "GUIInfoManager.h" //! @todo Remove me
+#include "GameWindowFullScreenText.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h" //! @todo Remove me
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIRenderHandle.h"
+#include "games/GameServices.h"
+#include "games/GameSettings.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIControl.h"
+#include "guilib/GUIDialog.h"
+#include "guilib/GUIWindowManager.h" //! @todo Remove me
+#include "guilib/WindowIDs.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "windowing/GraphicContext.h" //! @todo Remove me
+
+using namespace KODI;
+using namespace KODI::GUILIB;
+using namespace RETRO;
+
+CGameWindowFullScreen::CGameWindowFullScreen(void)
+ : CGUIWindow(WINDOW_FULLSCREEN_GAME, "VideoFullScreen.xml"),
+ m_fullscreenText(new CGameWindowFullScreenText(*this))
+{
+ // initialize CGUIControl
+ m_controlStats = new GUICONTROLSTATS;
+
+ // initialize CGUIWindow
+ m_loadType = KEEP_IN_MEMORY;
+
+ RegisterWindow();
+}
+
+CGameWindowFullScreen::~CGameWindowFullScreen()
+{
+ UnregisterWindow();
+
+ delete m_controlStats;
+}
+
+void CGameWindowFullScreen::Process(unsigned int currentTime, CDirtyRegionList& dirtyregion)
+{
+ if (m_renderHandle->IsDirty())
+ MarkDirtyRegion();
+
+ m_controlStats->Reset();
+
+ CGUIWindow::Process(currentTime, dirtyregion);
+
+ //! @todo This isn't quite optimal - ideally we'd only be dirtying up the actual video render rect
+ //! which is probably the job of the renderer as it can more easily track resizing etc.
+ m_renderRegion.SetRect(
+ 0, 0, static_cast<float>(CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth()),
+ static_cast<float>(CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight()));
+}
+
+void CGameWindowFullScreen::Render()
+{
+ m_renderHandle->Render();
+
+ CGUIWindow::Render();
+}
+
+void CGameWindowFullScreen::RenderEx()
+{
+ CGUIWindow::RenderEx();
+
+ m_renderHandle->RenderEx();
+}
+
+bool CGameWindowFullScreen::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_SHOW_OSD:
+ case ACTION_TRIGGER_OSD:
+ {
+ TriggerOSD();
+ return true;
+ }
+ case ACTION_MOUSE_MOVE:
+ {
+ if (action.GetAmount(2) || action.GetAmount(3))
+ {
+ TriggerOSD();
+ return true;
+ }
+ break;
+ }
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ TriggerOSD();
+ return true;
+ }
+ case ACTION_SHOW_GUI:
+ {
+ // Switch back to the menu
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return true;
+ }
+ case ACTION_ASPECT_RATIO:
+ {
+ // Toggle the aspect ratio mode (only if the info is onscreen)
+ // g_application.GetAppPlayer().SetRenderViewMode(CViewModeSettings::GetNextQuickCycleViewMode(CMediaSettings::GetInstance().GetCurrentVideoSettings().m_ViewMode));
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return CGUIWindow::OnAction(action);
+}
+
+bool CGameWindowFullScreen::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_SETFOCUS:
+ case GUI_MSG_LOSTFOCUS:
+ {
+ if (message.GetSenderId() != WINDOW_FULLSCREEN_GAME)
+ return true;
+ break;
+ }
+ default:
+ break;
+ }
+
+ return CGUIWindow::OnMessage(message);
+}
+
+void CGameWindowFullScreen::FrameMove()
+{
+ m_fullscreenText->FrameMove();
+
+ CGUIWindow::FrameMove();
+}
+
+void CGameWindowFullScreen::ClearBackground()
+{
+ m_renderHandle->ClearBackground();
+
+ CGUIWindow::ClearBackground();
+}
+
+bool CGameWindowFullScreen::HasVisibleControls()
+{
+ return m_controlStats->nCountVisible > 0;
+}
+
+void CGameWindowFullScreen::OnWindowLoaded()
+{
+ CGUIWindow::OnWindowLoaded();
+
+ // Override the clear colour - we must never clear fullscreen
+ m_clearBackground = 0;
+
+ m_fullscreenText->OnWindowLoaded();
+}
+
+void CGameWindowFullScreen::OnInitWindow()
+{
+ GUIINFO::CPlayerGUIInfo& guiInfo =
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider();
+ guiInfo.SetShowInfo(false);
+
+ // Switch resolution
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetFullScreenVideo(true); //! @todo
+
+ CGUIWindow::OnInitWindow();
+
+ // Show OSD help
+ GAME::CGameSettings& gameSettings = CServiceBroker::GetGameServices().GameSettings();
+ if (gameSettings.ShowOSDHelp())
+ TriggerOSD();
+ else
+ {
+ //! @todo We need to route this check through the GUI bridge. By adding the
+ // dependency to the application player here, we are prevented from
+ // having multiple players.
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->HasGameAgent())
+ {
+ gameSettings.SetShowOSDHelp(true);
+ TriggerOSD();
+ }
+ }
+}
+
+void CGameWindowFullScreen::OnDeinitWindow(int nextWindowID)
+{
+ // Close all active modal dialogs
+ CServiceBroker::GetGUI()->GetWindowManager().CloseInternalModalDialogs(true);
+
+ CGUIWindow::OnDeinitWindow(nextWindowID);
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetFullScreenVideo(false); //! @todo
+}
+
+void CGameWindowFullScreen::TriggerOSD()
+{
+ CGUIDialog* pOSD = GetOSD();
+ if (pOSD != nullptr)
+ {
+ if (!pOSD->IsDialogRunning())
+ pOSD->Open();
+ }
+}
+
+CGUIDialog* CGameWindowFullScreen::GetOSD()
+{
+ return CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_GAME_OSD);
+}
+
+void CGameWindowFullScreen::RegisterWindow()
+{
+ m_renderHandle = CServiceBroker::GetGameRenderManager().RegisterWindow(*this);
+}
+
+void CGameWindowFullScreen::UnregisterWindow()
+{
+ m_renderHandle.reset();
+}
diff --git a/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreen.h b/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreen.h
new file mode 100644
index 0000000..a925c8b
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreen.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017-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/GUIWindow.h"
+
+class CGUIDialog;
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGameWindowFullScreenText;
+class CGUIRenderHandle;
+
+class CGameWindowFullScreen : public CGUIWindow
+{
+public:
+ CGameWindowFullScreen();
+ ~CGameWindowFullScreen() override;
+
+ // implementation of CGUIControl via CGUIWindow
+ void Process(unsigned int currentTime, CDirtyRegionList& dirtyregion) override;
+ void Render() override;
+ void RenderEx() override;
+ bool OnAction(const CAction& action) override;
+ bool OnMessage(CGUIMessage& message) override;
+
+ // implementation of CGUIWindow
+ void FrameMove() override;
+ void ClearBackground() override;
+ bool HasVisibleControls() override;
+ void OnWindowLoaded() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+protected:
+ // implementation of CGUIWindow
+ void OnInitWindow() override;
+
+private:
+ void TriggerOSD();
+ CGUIDialog* GetOSD();
+
+ void RegisterWindow();
+ void UnregisterWindow();
+
+ // GUI parameters
+ std::unique_ptr<CGameWindowFullScreenText> m_fullscreenText;
+
+ // Rendering parameters
+ std::shared_ptr<CGUIRenderHandle> m_renderHandle;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreenText.cpp b/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreenText.cpp
new file mode 100644
index 0000000..e0f722a
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreenText.cpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2017-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 "GameWindowFullScreenText.h"
+
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindow.h"
+#include "video/windows/GUIWindowFullScreenDefines.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CGameWindowFullScreenText::CGameWindowFullScreenText(CGUIWindow& fullscreenWindow)
+ : m_fullscreenWindow(fullscreenWindow)
+{
+}
+
+void CGameWindowFullScreenText::OnWindowLoaded()
+{
+ m_bShowText = false;
+ m_bTextChanged = true;
+ m_bTextVisibilityChanged = true;
+ m_lines.clear();
+}
+
+void CGameWindowFullScreenText::FrameMove()
+{
+ if (m_bTextChanged)
+ {
+ m_bTextChanged = false;
+ UploadText();
+ }
+
+ if (m_bTextVisibilityChanged)
+ {
+ m_bTextVisibilityChanged = false;
+
+ if (m_bShowText)
+ Show();
+ else
+ Hide();
+ }
+}
+
+const std::string& CGameWindowFullScreenText::GetText(unsigned int lineIndex) const
+{
+ if (lineIndex < m_lines.size())
+ return m_lines[lineIndex];
+
+ static const std::string empty;
+ return empty;
+}
+
+void CGameWindowFullScreenText::SetText(unsigned int lineIndex, std::string line)
+{
+ if (lineIndex >= m_lines.size())
+ m_lines.resize(lineIndex + 1);
+
+ m_lines[lineIndex] = std::move(line);
+}
+
+const std::vector<std::string>& CGameWindowFullScreenText::GetText() const
+{
+ return m_lines;
+}
+
+void CGameWindowFullScreenText::SetText(std::vector<std::string> text)
+{
+ m_lines = std::move(text);
+}
+
+void CGameWindowFullScreenText::UploadText()
+{
+ for (unsigned int i = 0; i < m_lines.size(); i++)
+ {
+ int rowControl = GetControlID(i);
+ if (rowControl > 0)
+ SET_CONTROL_LABEL(rowControl, m_lines[i]);
+ }
+}
+
+void CGameWindowFullScreenText::Show()
+{
+ SET_CONTROL_VISIBLE(LABEL_ROW1);
+ SET_CONTROL_VISIBLE(LABEL_ROW2);
+ SET_CONTROL_VISIBLE(LABEL_ROW3);
+ SET_CONTROL_VISIBLE(BLUE_BAR);
+}
+
+void CGameWindowFullScreenText::Hide()
+{
+ SET_CONTROL_HIDDEN(LABEL_ROW1);
+ SET_CONTROL_HIDDEN(LABEL_ROW2);
+ SET_CONTROL_HIDDEN(LABEL_ROW3);
+ SET_CONTROL_HIDDEN(BLUE_BAR);
+}
+
+int CGameWindowFullScreenText::GetID() const
+{
+ return m_fullscreenWindow.GetID();
+}
+
+bool CGameWindowFullScreenText::OnMessage(CGUIMessage& message)
+{
+ return m_fullscreenWindow.OnMessage(message);
+}
+
+int CGameWindowFullScreenText::GetControlID(unsigned int lineIndex)
+{
+ switch (lineIndex)
+ {
+ case 0:
+ return LABEL_ROW1;
+ case 1:
+ return LABEL_ROW2;
+ case 2:
+ return LABEL_ROW3;
+ default:
+ break;
+ }
+
+ return -1;
+}
diff --git a/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreenText.h b/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreenText.h
new file mode 100644
index 0000000..a454a03
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/guiwindows/GameWindowFullScreenText.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017-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 <string>
+#include <vector>
+
+class CGUIDialog;
+class CGUIMessage;
+class CGUIWindow;
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGameWindowFullScreenText
+{
+public:
+ CGameWindowFullScreenText(CGUIWindow& fullscreenWindow);
+ ~CGameWindowFullScreenText() = default;
+
+ // Window functions
+ void OnWindowLoaded();
+ void FrameMove();
+
+ /*!
+ * \brief Get a line of text
+ */
+ const std::string& GetText(unsigned int lineIndex) const;
+
+ /*!
+ * \brief Set a line of text
+ */
+ void SetText(unsigned int lineIndex, std::string line);
+
+ /*!
+ * \brief Get entire text
+ */
+ const std::vector<std::string>& GetText() const;
+
+ /*!
+ * \brief Set entire text
+ */
+ void SetText(std::vector<std::string> text);
+
+private:
+ // Window functions
+ void UploadText();
+ void Show();
+ void Hide();
+
+ /*!
+ * \brief Translate line index to the control ID in the skin
+ *
+ * \param lineIndex The line in the string vector
+ *
+ * \return The ID of the line's label control in the skin
+ */
+ static int GetControlID(unsigned int lineIndex);
+
+ // Window functions required by GUIMessage macros
+ //! @todo Change macros into functions
+ int GetID() const;
+ bool OnMessage(CGUIMessage& message);
+
+ // Construction parameters
+ CGUIWindow& m_fullscreenWindow;
+
+ // Window state
+ bool m_bShowText = false;
+ bool m_bTextChanged = true;
+ bool m_bTextVisibilityChanged = true;
+
+ // Text
+ std::vector<std::string> m_lines;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/messages/CMakeLists.txt b/xbmc/cores/RetroPlayer/messages/CMakeLists.txt
new file mode 100644
index 0000000..e2e5fa1
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/messages/CMakeLists.txt
@@ -0,0 +1,25 @@
+set(MESSAGES savestate.fbs
+ video.fbs
+)
+
+foreach(_file ${MESSAGES})
+ get_filename_component(FLATC_OUTPUT ${_file} NAME_WE)
+ set(FLATC_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${FLATC_OUTPUT}_generated.h)
+ list(APPEND FLATC_OUTPUTS ${FLATC_OUTPUT})
+
+ add_custom_command(OUTPUT ${FLATC_OUTPUT}
+ COMMAND ${FLATBUFFERS_FLATC_EXECUTABLE}
+ ARGS -c -o "${FLATBUFFERS_MESSAGES_INCLUDE_DIR}/" ${_file}
+ DEPENDS ${_file}
+ COMMENT "Building C++ header for ${_file}"
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+endforeach()
+
+add_custom_target(retroplayer_messages DEPENDS ${FLATC_OUTPUTS})
+set_target_properties(retroplayer_messages PROPERTIES FOLDER "Generated Messages"
+ INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}
+ SOURCES "${FLATC_OUTPUTS}")
+
+if(TARGET flatbuffers::flatbuffers)
+ add_dependencies(retroplayer_messages flatbuffers::flatbuffers)
+endif()
diff --git a/xbmc/cores/RetroPlayer/messages/savestate.fbs b/xbmc/cores/RetroPlayer/messages/savestate.fbs
new file mode 100644
index 0000000..7f8d805
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/messages/savestate.fbs
@@ -0,0 +1,64 @@
+//
+// Copyright (C) 2018 Team Kodi
+// This file is part of Kodi - https://kodi.tv
+//
+// SPDX-License-Identifier: MIT
+// See LICENSES/README.md for more information.
+//
+
+include "video.fbs";
+
+namespace KODI.RETRO;
+
+// Savestate schema
+// Version 3
+
+file_identifier "SAV_";
+
+enum SaveType : uint8 {
+ Unknown,
+ Auto,
+ Manual
+}
+
+table Savestate {
+ // Schema version
+ version:uint8 (id: 0);
+
+ // Savestate properties
+ type:SaveType (id: 1);
+ slot:uint8 (id: 2);
+ label:string (id: 3);
+ caption:string (id: 11);
+ created:string (id: 4); // W3C date time [ISO 8601 : 1988 (E)] with timezone info
+
+ // Game properties
+ game_file_name:string (id: 5);
+
+ // Environment properties
+ timestamp_frames:uint64 (id: 6);
+ timestamp_wall_clock_ns:uint64 (id: 7);
+
+ // Emulator properties
+ emulator_addon_id:string (id: 8);
+ emulator_version:string (id: 9); // Semantic version
+
+ // Video stream properties
+ pixel_format:PixelFormat (id: 12);
+ nominal_width:uint16 (id: 13);
+ nominal_height:uint16 (id: 14);
+ max_width:uint16 (id: 15);
+ max_height:uint16 (id: 16);
+ pixel_aspect_ratio:float (id: 17);
+
+ // Video frame properties
+ video_data:[uint8] (id: 18);
+ video_width:uint16 (id: 19);
+ video_height:uint16 (id: 20);
+ rotation_ccw:VideoRotation (id: 21);
+
+ // Memory properties
+ memory_data:[uint8] (id: 10);
+}
+
+root_type Savestate;
diff --git a/xbmc/cores/RetroPlayer/messages/video.fbs b/xbmc/cores/RetroPlayer/messages/video.fbs
new file mode 100644
index 0000000..f83ba5f
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/messages/video.fbs
@@ -0,0 +1,43 @@
+//
+// Copyright (C) 2023 Team Kodi
+// This file is part of Kodi - https://kodi.tv
+//
+// SPDX-License-Identifier: MIT
+// See LICENSES/README.md for more information.
+//
+
+namespace KODI.RETRO;
+
+enum PixelFormat : uint8 {
+ /// @brief Value for unknown pixel formats
+ Unknown,
+
+ /// @brief packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
+ /// This is the preferred format for web compatibility
+ RGBA_8888,
+
+ /// @brief packed RGB 8:8:8, 32bpp, XRGBXRGB... X=unused/undefined
+ XRGB_8888,
+
+ /// @brief packed BGR 8:8:8, 32bpp, BGRXBGRX... X=unused/undefined
+ BGRX_8888,
+
+ /// @brief packed RGB 5:6:5, 16bpp, (msb), 5R 6G 5B(lsb), big-endian
+ RGB_565_BE,
+
+ /// @brief packed RGB 5:6:5, 16bpp, (msb), 5R 6G 5B(lsb), little-endian
+ RGB_565_LE,
+
+ /// @brief packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb), big-endian, X=unused/undefined
+ RGB_555_BE,
+
+ /// @brief packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb), little-endian, X=unused/undefined
+ RGB_555_LE,
+}
+
+enum VideoRotation : uint8 {
+ CCW_0,
+ CCW_90,
+ CCW_180,
+ CCW_270,
+}
diff --git a/xbmc/cores/RetroPlayer/playback/CMakeLists.txt b/xbmc/cores/RetroPlayer/playback/CMakeLists.txt
new file mode 100644
index 0000000..058522f
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/playback/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(SOURCES GameLoop.cpp
+ ReversiblePlayback.cpp)
+
+set(HEADERS GameLoop.h
+ IPlayback.h
+ IPlaybackControl.h
+ RealtimePlayback.h
+ ReversiblePlayback.h)
+
+core_add_library(retroplayer_playback)
diff --git a/xbmc/cores/RetroPlayer/playback/GameLoop.cpp b/xbmc/cores/RetroPlayer/playback/GameLoop.cpp
new file mode 100644
index 0000000..c8862d2
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/playback/GameLoop.cpp
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2016-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 "GameLoop.h"
+
+#include <chrono>
+#include <cmath>
+
+using namespace KODI;
+using namespace RETRO;
+using namespace std::chrono_literals;
+
+#define DEFAULT_FPS 60 // In case fps is 0 (shouldn't happen)
+#define FOREVER_MS (7 * 24 * 60 * 60 * 1000) // 1 week is large enough
+
+CGameLoop::CGameLoop(IGameLoopCallback* callback, double fps)
+ : CThread("GameLoop"),
+ m_callback(callback),
+ m_fps(fps ? fps : DEFAULT_FPS),
+ m_speedFactor(0.0),
+ m_lastFrameMs(0.0),
+ m_adjustTime(0.0)
+{
+}
+
+CGameLoop::~CGameLoop()
+{
+ Stop();
+}
+
+void CGameLoop::Start()
+{
+ Create();
+}
+
+void CGameLoop::Stop()
+{
+ StopThread(false);
+ m_sleepEvent.Set();
+ StopThread(true);
+}
+
+void CGameLoop::SetSpeed(double speedFactor)
+{
+ m_speedFactor = speedFactor;
+
+ m_sleepEvent.Set();
+}
+
+void CGameLoop::PauseAsync()
+{
+ SetSpeed(0.0);
+}
+
+void CGameLoop::Process(void)
+{
+ while (!m_bStop)
+ {
+ if (m_speedFactor == 0.0)
+ {
+ m_lastFrameMs = 0.0;
+ m_sleepEvent.Wait(5000ms);
+ }
+ else
+ {
+ if (m_speedFactor > 0.0)
+ m_callback->FrameEvent();
+ else if (m_speedFactor < 0.0)
+ m_callback->RewindEvent();
+
+ if (m_lastFrameMs > 0.0)
+ {
+ m_lastFrameMs += FrameTimeMs();
+ m_adjustTime = m_lastFrameMs - NowMs();
+ }
+ else
+ {
+ m_lastFrameMs = NowMs();
+ m_adjustTime = 0.0;
+ }
+
+ // Calculate sleep time
+ double sleepTimeMs = SleepTimeMs();
+
+ // Sleep at least 1 ms to avoid sleeping forever
+ while (sleepTimeMs > 1.0)
+ {
+ m_sleepEvent.Wait(std::chrono::milliseconds(static_cast<unsigned int>(sleepTimeMs)));
+
+ if (m_bStop)
+ break;
+
+ // Speed may have changed, update sleep time
+ sleepTimeMs = SleepTimeMs();
+ }
+ }
+ }
+}
+
+double CGameLoop::FrameTimeMs() const
+{
+ if (m_speedFactor != 0.0)
+ return 1000.0 / m_fps / std::abs(m_speedFactor);
+ else
+ return 1000.0 / m_fps / 1.0;
+}
+
+double CGameLoop::SleepTimeMs() const
+{
+ // Calculate next frame time
+ const double nextFrameMs = m_lastFrameMs + FrameTimeMs();
+
+ // Calculate sleep time
+ double sleepTimeMs = (nextFrameMs - NowMs()) + m_adjustTime;
+
+ // Reset adjust time
+ m_adjustTime = 0.0;
+
+ // Positive or zero
+ sleepTimeMs = (sleepTimeMs >= 0.0 ? sleepTimeMs : 0.0);
+
+ return sleepTimeMs;
+}
+
+double CGameLoop::NowMs() const
+{
+ return std::chrono::duration<double, std::milli>(
+ std::chrono::steady_clock::now().time_since_epoch())
+ .count();
+}
diff --git a/xbmc/cores/RetroPlayer/playback/GameLoop.h b/xbmc/cores/RetroPlayer/playback/GameLoop.h
new file mode 100644
index 0000000..e4fbcff
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/playback/GameLoop.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016-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/Event.h"
+#include "threads/Thread.h"
+
+#include <atomic>
+
+namespace KODI
+{
+namespace RETRO
+{
+class IGameLoopCallback
+{
+public:
+ virtual ~IGameLoopCallback() = default;
+
+ /*!
+ * \brief The next frame is being shown
+ */
+ virtual void FrameEvent() = 0;
+
+ /*!
+ * \brief The prior frame is being shown
+ */
+ virtual void RewindEvent() = 0;
+};
+
+class CGameLoop : protected CThread
+{
+public:
+ CGameLoop(IGameLoopCallback* callback, double fps);
+
+ ~CGameLoop() override;
+
+ void Start();
+ void Stop();
+
+ double FPS() const { return m_fps; }
+
+ double GetSpeed() const { return m_speedFactor; }
+ void SetSpeed(double speedFactor);
+ void PauseAsync();
+
+protected:
+ // implementation of CThread
+ void Process() override;
+
+private:
+ double FrameTimeMs() const;
+ double SleepTimeMs() const;
+ double NowMs() const;
+
+ IGameLoopCallback* const m_callback;
+ const double m_fps;
+ std::atomic<double> m_speedFactor;
+ double m_lastFrameMs;
+ mutable double m_adjustTime;
+ CEvent m_sleepEvent;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/playback/IPlayback.h b/xbmc/cores/RetroPlayer/playback/IPlayback.h
new file mode 100644
index 0000000..9871889
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/playback/IPlayback.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016-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 <string>
+
+namespace KODI
+{
+namespace RETRO
+{
+class IPlayback
+{
+public:
+ virtual ~IPlayback() = default;
+
+ // Lifetime management
+ virtual void Initialize() = 0;
+ virtual void Deinitialize() = 0;
+
+ // Playback capabilities
+ virtual bool CanPause() const = 0;
+ virtual bool CanSeek() const = 0;
+
+ // Control playback
+ virtual unsigned int GetTimeMs() const = 0;
+ virtual unsigned int GetTotalTimeMs() const = 0;
+ virtual unsigned int GetCacheTimeMs() const = 0;
+ virtual void SeekTimeMs(unsigned int timeMs) = 0;
+ virtual double GetSpeed() const = 0;
+ virtual void SetSpeed(double speedFactor) = 0;
+ virtual void PauseAsync() = 0; // Pauses after the following frame
+
+ // Savestates
+ virtual std::string CreateSavestate(
+ bool autosave,
+ const std::string& savestatePath = "") = 0; // Returns the path of savestate on success
+ virtual bool LoadSavestate(const std::string& savestatePath) = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/playback/IPlaybackControl.h b/xbmc/cores/RetroPlayer/playback/IPlaybackControl.h
new file mode 100644
index 0000000..6637cae
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/playback/IPlaybackControl.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 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
+
+namespace KODI
+{
+namespace RETRO
+{
+/*!
+ * \brief The playback client being controlled
+ */
+class IPlaybackCallback
+{
+public:
+ virtual ~IPlaybackCallback() = default;
+
+ /*!
+ * \brief Set the playback speed
+ *
+ * \param speed The new speed
+ */
+ virtual void SetPlaybackSpeed(double speed) = 0;
+
+ /*!
+ * \brief Enable/disable game input
+ *
+ * \param bEnable True to enable input, false to disable input
+ */
+ virtual void EnableInput(bool bEnable) = 0;
+};
+
+/*!
+ * \brief Class that can control playback and input
+ */
+class IPlaybackControl
+{
+public:
+ virtual ~IPlaybackControl() = default;
+
+ /*!
+ * \brief Called every frame
+ */
+ virtual void FrameMove() = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/playback/RealtimePlayback.h b/xbmc/cores/RetroPlayer/playback/RealtimePlayback.h
new file mode 100644
index 0000000..15e3bf6
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/playback/RealtimePlayback.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016-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 "IPlayback.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRealtimePlayback : public IPlayback
+{
+public:
+ ~CRealtimePlayback() override = default;
+
+ // implementation of IPlayback
+ void Initialize() override {}
+ void Deinitialize() override {}
+ bool CanPause() const override { return false; }
+ bool CanSeek() const override { return false; }
+ unsigned int GetTimeMs() const override { return 0; }
+ unsigned int GetTotalTimeMs() const override { return 0; }
+ unsigned int GetCacheTimeMs() const override { return 0; }
+ void SeekTimeMs(unsigned int timeMs) override {}
+ double GetSpeed() const override { return 1.0; }
+ void SetSpeed(double speedFactor) override {}
+ void PauseAsync() override {}
+ std::string CreateSavestate(bool autosave, const std::string& savestatePath = "") override
+ {
+ return "";
+ }
+ bool LoadSavestate(const std::string& savestatePath) override { return false; }
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp
new file mode 100644
index 0000000..653b209
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.cpp
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2016-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 "ReversiblePlayback.h"
+
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "addons/AddonVersion.h"
+#include "cores/RetroPlayer/cheevos/Cheevos.h"
+#include "cores/RetroPlayer/guibridge/GUIGameMessenger.h"
+#include "cores/RetroPlayer/rendering/RPRenderManager.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
+#include "cores/RetroPlayer/streams/memory/DeltaPairMemoryStream.h"
+#include "filesystem/File.h"
+#include "games/GameServices.h"
+#include "games/GameSettings.h"
+#include "games/addons/GameClient.h"
+#include "utils/MathUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+
+using namespace KODI;
+using namespace RETRO;
+
+#define REWIND_FACTOR 0.25 // Rewind at 25% of gameplay speed
+
+CReversiblePlayback::CReversiblePlayback(GAME::CGameClient* gameClient,
+ CRPRenderManager& renderManager,
+ CCheevos* cheevos,
+ CGUIGameMessenger& guiMessenger,
+ double fps,
+ size_t serializeSize)
+ : m_gameClient(gameClient),
+ m_renderManager(renderManager),
+ m_cheevos(cheevos),
+ m_guiMessenger(guiMessenger),
+ m_gameLoop(this, fps),
+ m_savestateDatabase(new CSavestateDatabase),
+ m_totalFrameCount(0),
+ m_pastFrameCount(0),
+ m_futureFrameCount(0),
+ m_playTimeMs(0),
+ m_totalTimeMs(0),
+ m_cacheTimeMs(0)
+{
+ UpdateMemoryStream();
+
+ GAME::CGameSettings& gameSettings = CServiceBroker::GetGameServices().GameSettings();
+ gameSettings.RegisterObserver(this);
+}
+
+CReversiblePlayback::~CReversiblePlayback()
+{
+ GAME::CGameSettings& gameSettings = CServiceBroker::GetGameServices().GameSettings();
+ gameSettings.UnregisterObserver(this);
+
+ Deinitialize();
+}
+
+void CReversiblePlayback::Initialize()
+{
+ m_gameLoop.Start();
+}
+
+void CReversiblePlayback::Deinitialize()
+{
+ // Wait for autosave tasks
+ for (std::future<void>& task : m_savestateThreads)
+ task.wait();
+ m_savestateThreads.clear();
+
+ m_gameLoop.Stop();
+}
+
+void CReversiblePlayback::SeekTimeMs(unsigned int timeMs)
+{
+ const int offsetTimeMs = timeMs - GetTimeMs();
+ const int offsetFrames = MathUtils::round_int(offsetTimeMs / 1000.0 * m_gameLoop.FPS());
+
+ if (offsetFrames > 0)
+ {
+ const uint64_t frames = std::min(static_cast<uint64_t>(offsetFrames), m_futureFrameCount);
+ if (frames > 0)
+ {
+ m_gameLoop.SetSpeed(0.0);
+ AdvanceFrames(frames);
+ m_gameLoop.SetSpeed(1.0);
+ }
+ }
+ else if (offsetFrames < 0)
+ {
+ const uint64_t frames = std::min(static_cast<uint64_t>(-offsetFrames), m_pastFrameCount);
+ if (frames > 0)
+ {
+ m_gameLoop.SetSpeed(0.0);
+ RewindFrames(frames);
+ m_gameLoop.SetSpeed(1.0);
+ }
+ }
+}
+
+double CReversiblePlayback::GetSpeed() const
+{
+ return m_gameLoop.GetSpeed();
+}
+
+void CReversiblePlayback::SetSpeed(double speedFactor)
+{
+ if (speedFactor >= 0.0)
+ m_gameLoop.SetSpeed(speedFactor);
+ else
+ m_gameLoop.SetSpeed(speedFactor * REWIND_FACTOR);
+}
+
+void CReversiblePlayback::PauseAsync()
+{
+ m_gameLoop.PauseAsync();
+}
+
+std::string CReversiblePlayback::CreateSavestate(bool autosave,
+ const std::string& savestatePath /* = "" */)
+{
+ const size_t memorySize = m_gameClient->SerializeSize();
+
+ // Game client must support serialization
+ if (memorySize == 0)
+ return "";
+
+ //! @todo Handle savestates for standalone game clients
+ if (m_gameClient->GetGamePath().empty())
+ {
+ return "";
+ }
+
+ // Take a timestamp of the system clock
+ const CDateTime nowUTC = CDateTime::GetUTCDateTime();
+
+ // Record the frame count
+ const uint64_t timestampFrames = m_totalFrameCount;
+
+ // Get the savestate path
+ std::string savePath(savestatePath);
+ {
+ std::unique_lock<CCriticalSection> lock(m_savestateMutex);
+
+ if (autosave && savePath.empty())
+ savePath = m_autosavePath;
+
+ // Clear autosave path so the next autosave is created in a new slot and
+ // does not overwrite the newly-created manual save
+ if (!autosave && savePath == m_autosavePath)
+ m_autosavePath.clear();
+
+ // If path is still unknown, calculate it now
+ if (savePath.empty())
+ savePath = CSavestateDatabase::MakeSavestatePath(m_gameClient->GetGamePath(), nowUTC);
+
+ // Update autosave path
+ if (autosave)
+ m_autosavePath = savePath;
+ }
+
+ // Capture the current video frame
+ m_renderManager.CacheVideoFrame(savePath);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_savestateMutex);
+
+ // Prune any finished autosave threads
+ m_savestateThreads.erase(std::remove_if(m_savestateThreads.begin(), m_savestateThreads.end(),
+ [](std::future<void>& task) {
+ return task.wait_for(std::chrono::seconds(0)) ==
+ std::future_status::ready;
+ }),
+ m_savestateThreads.end());
+
+ // Save async to not block game loop
+ std::future<void> task =
+ std::async(std::launch::async, [this, autosave, savePath, nowUTC, timestampFrames]() {
+ CommitSavestate(autosave, savePath, nowUTC, timestampFrames);
+ });
+
+ m_savestateThreads.emplace_back(std::move(task));
+ }
+
+ return savePath;
+}
+
+void CReversiblePlayback::CommitSavestate(bool autosave,
+ const std::string& savePath,
+ const CDateTime& nowUTC,
+ uint64_t timestampFrames)
+{
+ std::unique_ptr<ISavestate> savestate = CSavestateDatabase::AllocateSavestate();
+ std::unique_ptr<ISavestate> loadedSavestate;
+
+ const size_t memorySize = m_gameClient->SerializeSize();
+ uint8_t* const memoryData = savestate->GetMemoryBuffer(memorySize);
+
+ // Copy the savestate memory
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ if (m_memoryStream && m_memoryStream->CurrentFrame() != nullptr)
+ {
+ std::memcpy(memoryData, m_memoryStream->CurrentFrame(), memorySize);
+ }
+ else
+ {
+ lock.unlock();
+ if (!m_gameClient->Serialize(memoryData, memorySize))
+ return;
+ }
+ }
+
+ // Attempt to get existing properties
+ {
+ std::unique_lock<CCriticalSection> lock(m_savestateMutex);
+ if (!savePath.empty() && XFILE::CFile::Exists(savePath))
+ {
+ loadedSavestate = CSavestateDatabase::AllocateSavestate();
+ if (!m_savestateDatabase->GetSavestate(savePath, *loadedSavestate))
+ loadedSavestate.reset();
+ }
+ }
+
+ const std::string caption = m_cheevos->GetRichPresenceEvaluation();
+ const std::string gameFileName = URIUtils::GetFileName(m_gameClient->GetGamePath());
+ const double timestampWallClock =
+ (timestampFrames /
+ m_gameClient->GetFrameRate()); //! @todo Accumulate playtime instead of deriving it
+ const std::string gameClientId = m_gameClient->ID();
+ const std::string gameClientVersion = m_gameClient->Version().asString();
+
+ savestate->SetType(autosave ? SAVE_TYPE::AUTO : SAVE_TYPE::MANUAL);
+ savestate->SetLabel(loadedSavestate ? loadedSavestate->Label() : "");
+ savestate->SetCaption(caption);
+ savestate->SetCreated(nowUTC);
+ savestate->SetGameFileName(gameFileName);
+ savestate->SetTimestampFrames(timestampFrames);
+ savestate->SetTimestampWallClock(timestampWallClock);
+ savestate->SetGameClientID(gameClientId);
+ savestate->SetGameClientVersion(gameClientVersion);
+
+ m_renderManager.SaveVideoFrame(savePath, *savestate);
+
+ savestate->Finalize();
+
+ bool success;
+ {
+ std::unique_lock<CCriticalSection> lock(m_savestateMutex);
+ success = m_savestateDatabase->AddSavestate(savePath, m_gameClient->GetGamePath(), *savestate);
+ }
+
+ if (success)
+ {
+ std::string thumbnailPath = CSavestateDatabase::MakeThumbnailPath(savePath);
+ m_renderManager.SaveThumbnail(thumbnailPath);
+ }
+
+ // Notify the GUI that the metadata for this savestate should be refreshed
+ m_guiMessenger.RefreshSavestates(savePath, savestate.get());
+}
+
+bool CReversiblePlayback::LoadSavestate(const std::string& savestatePath)
+{
+ const size_t memorySize = m_gameClient->SerializeSize();
+
+ // Game client must support serialization
+ if (memorySize == 0)
+ return false;
+
+ bool bSuccess = false;
+
+ std::unique_ptr<ISavestate> savestate = CSavestateDatabase::AllocateSavestate();
+ if (m_savestateDatabase->GetSavestate(savestatePath, *savestate))
+ {
+ if (savestate->GetMemorySize() != memorySize)
+ {
+ CLog::Log(LOGERROR, "Invalid memory size, got {}, expected {}", memorySize,
+ savestate->GetMemorySize());
+ }
+ else
+ {
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ if (m_memoryStream)
+ {
+ m_memoryStream->SetFrameCounter(savestate->TimestampFrames());
+ std::memcpy(m_memoryStream->BeginFrame(), savestate->GetMemoryData(), memorySize);
+ m_memoryStream->SubmitFrame();
+ }
+ }
+
+ if (m_gameClient->Deserialize(savestate->GetMemoryData(), memorySize))
+ {
+ m_totalFrameCount = savestate->TimestampFrames();
+ bSuccess = true;
+ if (savestate->Type() == SAVE_TYPE::AUTO)
+ m_autosavePath = savestatePath;
+ }
+ }
+ }
+
+ m_cheevos->ResetRuntime();
+
+ return bSuccess;
+}
+
+void CReversiblePlayback::FrameEvent()
+{
+ m_gameClient->RunFrame();
+
+ AddFrame();
+}
+
+void CReversiblePlayback::RewindEvent()
+{
+ RewindFrames(1);
+
+ m_gameClient->RunFrame();
+}
+
+void CReversiblePlayback::AddFrame()
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (m_memoryStream)
+ {
+ if (m_gameClient->Serialize(m_memoryStream->BeginFrame(), m_memoryStream->FrameSize()))
+ {
+ m_memoryStream->SubmitFrame();
+ UpdatePlaybackStats();
+ }
+ }
+
+ m_totalFrameCount++;
+}
+
+void CReversiblePlayback::RewindFrames(uint64_t frames)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (m_memoryStream)
+ {
+ m_memoryStream->RewindFrames(frames);
+ m_gameClient->Deserialize(m_memoryStream->CurrentFrame(), m_memoryStream->FrameSize());
+ UpdatePlaybackStats();
+ }
+
+ m_totalFrameCount -= std::min(m_totalFrameCount, frames);
+}
+
+void CReversiblePlayback::AdvanceFrames(uint64_t frames)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (m_memoryStream)
+ {
+ m_memoryStream->AdvanceFrames(frames);
+ m_gameClient->Deserialize(m_memoryStream->CurrentFrame(), m_memoryStream->FrameSize());
+ UpdatePlaybackStats();
+ }
+
+ m_totalFrameCount += frames;
+}
+
+void CReversiblePlayback::UpdatePlaybackStats()
+{
+ m_pastFrameCount = m_memoryStream->PastFramesAvailable();
+ m_futureFrameCount = m_memoryStream->FutureFramesAvailable();
+
+ const uint64_t played = m_pastFrameCount + (m_memoryStream->CurrentFrame() ? 1 : 0);
+ const uint64_t total = m_memoryStream->MaxFrameCount();
+ const uint64_t cached = m_futureFrameCount;
+
+ m_playTimeMs = MathUtils::round_int(1000.0 * played / m_gameLoop.FPS());
+ m_totalTimeMs = MathUtils::round_int(1000.0 * total / m_gameLoop.FPS());
+ m_cacheTimeMs = MathUtils::round_int(1000.0 * cached / m_gameLoop.FPS());
+}
+
+void CReversiblePlayback::Notify(const Observable& obs, const ObservableMessage msg)
+{
+ switch (msg)
+ {
+ case ObservableMessageSettingsChanged:
+ UpdateMemoryStream();
+ break;
+ default:
+ break;
+ }
+}
+
+void CReversiblePlayback::UpdateMemoryStream()
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ bool bRewindEnabled = false;
+
+ GAME::CGameSettings& gameSettings = CServiceBroker::GetGameServices().GameSettings();
+
+ if (m_gameClient->SerializeSize() > 0)
+ bRewindEnabled = gameSettings.RewindEnabled();
+
+ if (bRewindEnabled)
+ {
+ unsigned int rewindBufferSec = gameSettings.MaxRewindTimeSec();
+ if (rewindBufferSec < 10)
+ rewindBufferSec = 10; // Sanity check
+
+ unsigned int frameCount = MathUtils::round_int(rewindBufferSec * m_gameLoop.FPS());
+
+ if (!m_memoryStream)
+ {
+ m_memoryStream.reset(new CDeltaPairMemoryStream);
+ m_memoryStream->Init(m_gameClient->SerializeSize(), frameCount);
+ }
+
+ if (m_memoryStream->MaxFrameCount() != frameCount)
+ {
+ m_memoryStream->SetMaxFrameCount(frameCount);
+ }
+ }
+ else
+ {
+ m_memoryStream.reset();
+
+ // Reset playback stats
+ m_pastFrameCount = 0;
+ m_futureFrameCount = 0;
+ m_playTimeMs = 0;
+ m_totalTimeMs = 0;
+ m_cacheTimeMs = 0;
+ }
+}
diff --git a/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h
new file mode 100644
index 0000000..bd2fc37
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/playback/ReversiblePlayback.h
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016-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 "GameLoop.h"
+#include "IPlayback.h"
+#include "threads/CriticalSection.h"
+#include "utils/Observer.h"
+
+#include <future>
+#include <memory>
+#include <stddef.h>
+#include <stdint.h>
+
+class CDateTime;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClient;
+}
+
+namespace RETRO
+{
+class CCheevos;
+class CGUIGameMessenger;
+class CRPRenderManager;
+class CSavestateDatabase;
+class IMemoryStream;
+
+class CReversiblePlayback : public IPlayback, public IGameLoopCallback, public Observer
+{
+public:
+ CReversiblePlayback(GAME::CGameClient* gameClient,
+ CRPRenderManager& renderManager,
+ CCheevos* cheevos,
+ CGUIGameMessenger& guiMessenger,
+ double fps,
+ size_t serializeSize);
+
+ ~CReversiblePlayback() override;
+
+ // implementation of IPlayback
+ void Initialize() override;
+ void Deinitialize() override;
+ bool CanPause() const override { return true; }
+ bool CanSeek() const override { return true; }
+ unsigned int GetTimeMs() const override { return m_playTimeMs; }
+ unsigned int GetTotalTimeMs() const override { return m_totalTimeMs; }
+ unsigned int GetCacheTimeMs() const override { return m_cacheTimeMs; }
+ void SeekTimeMs(unsigned int timeMs) override;
+ double GetSpeed() const override;
+ void SetSpeed(double speedFactor) override;
+ void PauseAsync() override;
+ std::string CreateSavestate(bool autosave, const std::string& savestatePath = "") override;
+ bool LoadSavestate(const std::string& savestatePath) override;
+
+ // implementation of IGameLoopCallback
+ void FrameEvent() override;
+ void RewindEvent() override;
+
+ // implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override;
+
+private:
+ void AddFrame();
+ void RewindFrames(uint64_t frames);
+ void AdvanceFrames(uint64_t frames);
+ void UpdatePlaybackStats();
+ void UpdateMemoryStream();
+ void CommitSavestate(bool autosave,
+ const std::string& savePath,
+ const CDateTime& nowUTC,
+ uint64_t timestampFrames);
+
+ // Construction parameter
+ GAME::CGameClient* const m_gameClient;
+ CRPRenderManager& m_renderManager;
+ CCheevos* const m_cheevos;
+ CGUIGameMessenger& m_guiMessenger;
+
+ // Gameplay functionality
+ CGameLoop m_gameLoop;
+ std::unique_ptr<IMemoryStream> m_memoryStream;
+ CCriticalSection m_mutex;
+
+ // Savestate functionality
+ std::unique_ptr<CSavestateDatabase> m_savestateDatabase;
+ std::string m_autosavePath{};
+ std::vector<std::future<void>> m_savestateThreads;
+ CCriticalSection m_savestateMutex;
+
+ // Playback stats
+ uint64_t m_totalFrameCount;
+ uint64_t m_pastFrameCount;
+ uint64_t m_futureFrameCount;
+ unsigned int m_playTimeMs;
+ unsigned int m_totalTimeMs;
+ unsigned int m_cacheTimeMs;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/process/CMakeLists.txt b/xbmc/cores/RetroPlayer/process/CMakeLists.txt
new file mode 100644
index 0000000..f8b7fc1
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES RPProcessInfo.cpp
+)
+
+set(HEADERS RPProcessInfo.h
+)
+
+core_add_library(rp-process)
diff --git a/xbmc/cores/RetroPlayer/process/RPProcessInfo.cpp b/xbmc/cores/RetroPlayer/process/RPProcessInfo.cpp
new file mode 100644
index 0000000..93fec60
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/RPProcessInfo.cpp
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2017-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 "RPProcessInfo.h"
+
+#include "ServiceBroker.h"
+#include "cores/DataCacheCore.h"
+#include "cores/RetroPlayer/buffers/RenderBufferManager.h"
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "settings/DisplaySettings.h"
+#include "settings/MediaSettings.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+
+extern "C"
+{
+#include <libavutil/pixdesc.h>
+}
+
+#include <utility>
+
+using namespace KODI;
+using namespace RETRO;
+
+CreateRPProcessControl CRPProcessInfo::m_processControl = nullptr;
+std::vector<std::unique_ptr<IRendererFactory>> CRPProcessInfo::m_rendererFactories;
+CCriticalSection CRPProcessInfo::m_createSection;
+
+CRPProcessInfo::CRPProcessInfo(std::string platformName)
+ : m_platformName(std::move(platformName)),
+ m_renderBufferManager(new CRenderBufferManager),
+ m_renderContext(new CRenderContext(CServiceBroker::GetRenderSystem(),
+ CServiceBroker::GetWinSystem(),
+ CServiceBroker::GetWinSystem()->GetGfxContext(),
+ CDisplaySettings::GetInstance(),
+ CMediaSettings::GetInstance(),
+ CServiceBroker::GetGameServices(),
+ CServiceBroker::GetGUI()))
+{
+ for (auto& rendererFactory : m_rendererFactories)
+ {
+ RenderBufferPoolVector bufferPools = rendererFactory->CreateBufferPools(*m_renderContext);
+ if (!bufferPools.empty())
+ m_renderBufferManager->RegisterPools(rendererFactory.get(), std::move(bufferPools));
+ }
+
+ // Initialize default scaling method
+ for (auto scalingMethod : GetScalingMethods())
+ {
+ if (HasScalingMethod(scalingMethod))
+ {
+ m_defaultScalingMethod = scalingMethod;
+ break;
+ }
+ }
+}
+
+CRPProcessInfo::~CRPProcessInfo() = default;
+
+CRPProcessInfo* CRPProcessInfo::CreateInstance()
+{
+ CRPProcessInfo* processInfo = nullptr;
+
+ std::unique_lock<CCriticalSection> lock(m_createSection);
+
+ if (m_processControl != nullptr)
+ {
+ processInfo = m_processControl();
+
+ if (processInfo != nullptr)
+ CLog::Log(LOGINFO, "RetroPlayer[PROCESS]: Created process info for {}",
+ processInfo->GetPlatformName());
+ else
+ CLog::Log(LOGERROR, "RetroPlayer[PROCESS]: Failed to create process info");
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[PROCESS]: No process control registered");
+ }
+
+ return processInfo;
+}
+
+void CRPProcessInfo::RegisterProcessControl(CreateRPProcessControl createFunc)
+{
+ m_processControl = createFunc;
+}
+
+void CRPProcessInfo::RegisterRendererFactory(IRendererFactory* factory)
+{
+ std::unique_lock<CCriticalSection> lock(m_createSection);
+
+ CLog::Log(LOGINFO, "RetroPlayer[RENDER]: Registering renderer factory for {}",
+ factory->RenderSystemName());
+
+ m_rendererFactories.emplace_back(factory);
+}
+
+std::string CRPProcessInfo::GetRenderSystemName(IRenderBufferPool* renderBufferPool) const
+{
+ return m_renderBufferManager->GetRenderSystemName(renderBufferPool);
+}
+
+CRPBaseRenderer* CRPProcessInfo::CreateRenderer(IRenderBufferPool* renderBufferPool,
+ const CRenderSettings& renderSettings)
+{
+ std::unique_lock<CCriticalSection> lock(m_createSection);
+
+ for (auto& rendererFactory : m_rendererFactories)
+ {
+ RenderBufferPoolVector bufferPools = m_renderBufferManager->GetPools(rendererFactory.get());
+ for (auto& bufferPool : bufferPools)
+ {
+ if (bufferPool.get() == renderBufferPool)
+ return rendererFactory->CreateRenderer(renderSettings, *m_renderContext,
+ std::move(bufferPool));
+ }
+ }
+
+ CLog::Log(LOGERROR, "RetroPlayer[RENDER]: Failed to find a suitable renderer factory");
+
+ return nullptr;
+}
+
+void CRPProcessInfo::SetDataCache(CDataCacheCore* cache)
+{
+ m_dataCache = cache;
+ ;
+}
+
+void CRPProcessInfo::ResetInfo()
+{
+ if (m_dataCache != nullptr)
+ {
+ m_dataCache->SetVideoDecoderName("", false);
+ m_dataCache->SetVideoDeintMethod("");
+ m_dataCache->SetVideoPixelFormat("");
+ m_dataCache->SetVideoDimensions(0, 0);
+ m_dataCache->SetVideoFps(0.0f);
+ m_dataCache->SetVideoDAR(1.0f);
+ m_dataCache->SetAudioDecoderName("");
+ m_dataCache->SetAudioChannels("");
+ m_dataCache->SetAudioSampleRate(0);
+ m_dataCache->SetAudioBitsPerSample(0);
+ m_dataCache->SetRenderClockSync(false);
+ m_dataCache->SetStateSeeking(false);
+ m_dataCache->SetSpeed(1.0f, 1.0f);
+ m_dataCache->SetGuiRender(true); //! @todo
+ m_dataCache->SetVideoRender(false); //! @todo
+ m_dataCache->SetPlayTimes(0, 0, 0, 0);
+ }
+}
+
+bool CRPProcessInfo::HasScalingMethod(SCALINGMETHOD scalingMethod) const
+{
+ return m_renderBufferManager->HasScalingMethod(scalingMethod);
+}
+
+std::vector<SCALINGMETHOD> CRPProcessInfo::GetScalingMethods()
+{
+ return {
+ SCALINGMETHOD::NEAREST,
+ SCALINGMETHOD::LINEAR,
+ };
+}
+
+//******************************************************************************
+// video codec
+//******************************************************************************
+void CRPProcessInfo::SetVideoPixelFormat(AVPixelFormat pixFormat)
+{
+ const char* videoPixelFormat = av_get_pix_fmt_name(pixFormat);
+
+ if (m_dataCache != nullptr)
+ m_dataCache->SetVideoPixelFormat(videoPixelFormat != nullptr ? videoPixelFormat : "");
+}
+
+void CRPProcessInfo::SetVideoDimensions(int width, int height)
+{
+ if (m_dataCache != nullptr)
+ m_dataCache->SetVideoDimensions(width, height);
+}
+
+void CRPProcessInfo::SetVideoFps(float fps)
+{
+ if (m_dataCache != nullptr)
+ m_dataCache->SetVideoFps(fps);
+}
+
+//******************************************************************************
+// player audio info
+//******************************************************************************
+void CRPProcessInfo::SetAudioChannels(const std::string& channels)
+{
+ if (m_dataCache != nullptr)
+ m_dataCache->SetAudioChannels(channels);
+}
+
+void CRPProcessInfo::SetAudioSampleRate(int sampleRate)
+{
+ if (m_dataCache != nullptr)
+ m_dataCache->SetAudioSampleRate(sampleRate);
+}
+
+void CRPProcessInfo::SetAudioBitsPerSample(int bitsPerSample)
+{
+ if (m_dataCache != nullptr)
+ m_dataCache->SetAudioBitsPerSample(bitsPerSample);
+}
+
+//******************************************************************************
+// player states
+//******************************************************************************
+void CRPProcessInfo::SetSpeed(float speed)
+{
+ if (m_dataCache != nullptr)
+ m_dataCache->SetSpeed(1.0f, speed);
+}
+
+void CRPProcessInfo::SetPlayTimes(time_t start, int64_t current, int64_t min, int64_t max)
+{
+ if (m_dataCache != nullptr)
+ m_dataCache->SetPlayTimes(start, current, min, max);
+}
diff --git a/xbmc/cores/RetroPlayer/process/RPProcessInfo.h b/xbmc/cores/RetroPlayer/process/RPProcessInfo.h
new file mode 100644
index 0000000..bc5a5db
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/RPProcessInfo.h
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+#include "cores/RetroPlayer/RetroPlayerTypes.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <libavutil/pixfmt.h>
+
+class CDataCacheCore;
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderBufferManager;
+class CRenderContext;
+class CRenderSettings;
+class CRPBaseRenderer;
+class CRPProcessInfo;
+class IRenderBufferPool;
+
+/*!
+ * \brief Process info factory
+ */
+using CreateRPProcessControl = CRPProcessInfo* (*)();
+
+/*!
+ * \brief Rendering factory
+ */
+class IRendererFactory
+{
+public:
+ virtual ~IRendererFactory() = default;
+
+ /*!
+ * \brief Get a description name of the rendering system
+ */
+ virtual std::string RenderSystemName() const = 0;
+
+ /*!
+ * \brief Create a renderer
+ *
+ * \param settings The renderer's initial settings
+ * \param context The rendering context
+ * \param bufferPool The buffer pool to which buffers are returned
+ */
+ virtual CRPBaseRenderer* CreateRenderer(const CRenderSettings& settings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool) = 0;
+
+ /*!
+ * \brief Create buffer pools to manager buffers
+ *
+ * \param context The rendering context shared with the buffer pools
+ *
+ * \return The buffer pools supported by the rendering system
+ */
+ virtual RenderBufferPoolVector CreateBufferPools(CRenderContext& context) = 0;
+};
+
+/*!
+ * \brief Player process info
+ */
+class CRPProcessInfo
+{
+public:
+ static CRPProcessInfo* CreateInstance();
+ static void RegisterProcessControl(CreateRPProcessControl createFunc);
+ static void RegisterRendererFactory(IRendererFactory* factory);
+
+ virtual ~CRPProcessInfo();
+
+ /*!
+ * \brief Get the descriptive name of the platform
+ *
+ * \return The name of the platform as set by windowing
+ */
+ const std::string& GetPlatformName() const { return m_platformName; }
+
+ /*!
+ * \brief Get the descriptive name of the rendering system
+ *
+ * \param renderBufferPool A pool belonging to the rendering system
+ *
+ * \return The name of the rendering system as set by windowing
+ */
+ std::string GetRenderSystemName(IRenderBufferPool* renderBufferPool) const;
+
+ /*!
+ * \brief Create a renderer
+ *
+ * \param renderBufferPool The buffer pool used to return render buffers
+ * \param renderSettings The settings for this renderer
+ *
+ * \return The renderer, or nullptr on failure
+ */
+ CRPBaseRenderer* CreateRenderer(IRenderBufferPool* renderBufferPool,
+ const CRenderSettings& renderSettings);
+
+ /*!
+ * \brief Set data cache
+ */
+ void SetDataCache(CDataCacheCore* cache);
+
+ /*!
+ * \brief Reset data cache info
+ */
+ void ResetInfo();
+
+ /// @name Rendering functions
+ ///{
+
+ /*!
+ * \brief Get the context shared by the rendering system
+ */
+ CRenderContext& GetRenderContext() { return *m_renderContext; }
+
+ /*!
+ * \brief Get the buffer manager that owns the buffer pools
+ */
+ CRenderBufferManager& GetBufferManager() { return *m_renderBufferManager; }
+
+ /*!
+ * \brief Check if a buffer pool supports the given scaling method
+ */
+ bool HasScalingMethod(SCALINGMETHOD scalingMethod) const;
+
+ /*!
+ * \brief Get the default scaling method for this rendering system
+ */
+ SCALINGMETHOD GetDefaultScalingMethod() const { return m_defaultScalingMethod; }
+
+ ///}
+
+ /// @name Player video info
+ ///{
+ void SetVideoPixelFormat(AVPixelFormat pixFormat);
+ void SetVideoDimensions(int width, int height);
+ void SetVideoFps(float fps);
+ ///}
+
+ /// @name Player audio info
+ ///{
+ void SetAudioChannels(const std::string& channels);
+ void SetAudioSampleRate(int sampleRate);
+ void SetAudioBitsPerSample(int bitsPerSample);
+ ///}
+
+ /// @name Player states
+ ///{
+ void SetSpeed(float speed);
+ void SetPlayTimes(time_t start, int64_t current, int64_t min, int64_t max);
+ ///}
+
+protected:
+ /*!
+ * \brief Constructor
+ *
+ * \param platformName A descriptive name of the platform
+ */
+ CRPProcessInfo(std::string platformName);
+
+ /*!
+ * \brief Get all scaling methods available to the rendering system
+ */
+ static std::vector<SCALINGMETHOD> GetScalingMethods();
+
+ // Static factories
+ static CreateRPProcessControl m_processControl;
+ static std::vector<std::unique_ptr<IRendererFactory>> m_rendererFactories;
+ static CCriticalSection m_createSection;
+
+ // Construction parameters
+ const std::string m_platformName;
+
+ // Info parameters
+ CDataCacheCore* m_dataCache = nullptr;
+
+ // Rendering parameters
+ std::unique_ptr<CRenderBufferManager> m_renderBufferManager;
+
+private:
+ // Rendering parameters
+ std::unique_ptr<CRenderContext> m_renderContext;
+ SCALINGMETHOD m_defaultScalingMethod = SCALINGMETHOD::AUTO;
+};
+
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/process/X11/CMakeLists.txt b/xbmc/cores/RetroPlayer/process/X11/CMakeLists.txt
new file mode 100644
index 0000000..93e2c0f
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/X11/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES RPProcessInfoX11.cpp)
+
+set(HEADERS RPProcessInfoX11.h)
+
+core_add_library(rp-process-x11)
diff --git a/xbmc/cores/RetroPlayer/process/X11/RPProcessInfoX11.cpp b/xbmc/cores/RetroPlayer/process/X11/RPProcessInfoX11.cpp
new file mode 100644
index 0000000..05cc819
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/X11/RPProcessInfoX11.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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 "RPProcessInfoX11.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRPProcessInfoX11::CRPProcessInfoX11() : CRPProcessInfo("X11")
+{
+}
+
+CRPProcessInfo* CRPProcessInfoX11::Create()
+{
+ return new CRPProcessInfoX11();
+}
+
+void CRPProcessInfoX11::Register()
+{
+ CRPProcessInfo::RegisterProcessControl(CRPProcessInfoX11::Create);
+}
diff --git a/xbmc/cores/RetroPlayer/process/X11/RPProcessInfoX11.h b/xbmc/cores/RetroPlayer/process/X11/RPProcessInfoX11.h
new file mode 100644
index 0000000..9236e6f
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/X11/RPProcessInfoX11.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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/RetroPlayer/process/RPProcessInfo.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfoX11 : public CRPProcessInfo
+{
+public:
+ CRPProcessInfoX11();
+
+ static CRPProcessInfo* Create();
+ static void Register();
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/process/android/CMakeLists.txt b/xbmc/cores/RetroPlayer/process/android/CMakeLists.txt
new file mode 100644
index 0000000..08ed293
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/android/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES RPProcessInfoAndroid.cpp)
+set(HEADERS RPProcessInfoAndroid.h)
+
+core_add_library(rp-process-android)
diff --git a/xbmc/cores/RetroPlayer/process/android/RPProcessInfoAndroid.cpp b/xbmc/cores/RetroPlayer/process/android/RPProcessInfoAndroid.cpp
new file mode 100644
index 0000000..52498de
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/android/RPProcessInfoAndroid.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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 "RPProcessInfoAndroid.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRPProcessInfoAndroid::CRPProcessInfoAndroid() : CRPProcessInfo("Android")
+{
+}
+
+CRPProcessInfo* CRPProcessInfoAndroid::Create()
+{
+ return new CRPProcessInfoAndroid();
+}
+
+void CRPProcessInfoAndroid::Register()
+{
+ CRPProcessInfo::RegisterProcessControl(CRPProcessInfoAndroid::Create);
+}
diff --git a/xbmc/cores/RetroPlayer/process/android/RPProcessInfoAndroid.h b/xbmc/cores/RetroPlayer/process/android/RPProcessInfoAndroid.h
new file mode 100644
index 0000000..ef00acf
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/android/RPProcessInfoAndroid.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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/RetroPlayer/process/RPProcessInfo.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfoAndroid : public CRPProcessInfo
+{
+public:
+ CRPProcessInfoAndroid();
+
+ static CRPProcessInfo* Create();
+ static void Register();
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/process/gbm/CMakeLists.txt b/xbmc/cores/RetroPlayer/process/gbm/CMakeLists.txt
new file mode 100644
index 0000000..c41253f
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/gbm/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES RPProcessInfoGbm.cpp)
+set(HEADERS RPProcessInfoGbm.h)
+
+core_add_library(rp-process-gbm)
diff --git a/xbmc/cores/RetroPlayer/process/gbm/RPProcessInfoGbm.cpp b/xbmc/cores/RetroPlayer/process/gbm/RPProcessInfoGbm.cpp
new file mode 100644
index 0000000..f17ad3e
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/gbm/RPProcessInfoGbm.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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 "RPProcessInfoGbm.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRPProcessInfoGbm::CRPProcessInfoGbm() : CRPProcessInfo("GBM")
+{
+}
+
+CRPProcessInfo* CRPProcessInfoGbm::Create()
+{
+ return new CRPProcessInfoGbm();
+}
+
+void CRPProcessInfoGbm::Register()
+{
+ CRPProcessInfo::RegisterProcessControl(CRPProcessInfoGbm::Create);
+}
diff --git a/xbmc/cores/RetroPlayer/process/gbm/RPProcessInfoGbm.h b/xbmc/cores/RetroPlayer/process/gbm/RPProcessInfoGbm.h
new file mode 100644
index 0000000..c1453d7
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/gbm/RPProcessInfoGbm.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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/RetroPlayer/process/RPProcessInfo.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfoGbm : public CRPProcessInfo
+{
+public:
+ CRPProcessInfoGbm();
+
+ static CRPProcessInfo* Create();
+ static void Register();
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/process/ios/CMakeLists.txt b/xbmc/cores/RetroPlayer/process/ios/CMakeLists.txt
new file mode 100644
index 0000000..1e20f65
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/ios/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES RPProcessInfoIOS.cpp)
+
+set(HEADERS RPProcessInfoIOS.h)
+
+core_add_library(rp-process-ios)
diff --git a/xbmc/cores/RetroPlayer/process/ios/RPProcessInfoIOS.cpp b/xbmc/cores/RetroPlayer/process/ios/RPProcessInfoIOS.cpp
new file mode 100644
index 0000000..c849e76
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/ios/RPProcessInfoIOS.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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 "RPProcessInfoIOS.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRPProcessInfoIOS::CRPProcessInfoIOS() : CRPProcessInfo("iOS")
+{
+}
+
+CRPProcessInfo* CRPProcessInfoIOS::Create()
+{
+ return new CRPProcessInfoIOS();
+}
+
+void CRPProcessInfoIOS::Register()
+{
+ CRPProcessInfo::RegisterProcessControl(CRPProcessInfoIOS::Create);
+}
diff --git a/xbmc/cores/RetroPlayer/process/ios/RPProcessInfoIOS.h b/xbmc/cores/RetroPlayer/process/ios/RPProcessInfoIOS.h
new file mode 100644
index 0000000..8caa5b1
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/ios/RPProcessInfoIOS.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017-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/RetroPlayer/process/RPProcessInfo.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfoIOS : public CRPProcessInfo
+{
+public:
+ CRPProcessInfoIOS();
+
+ static CRPProcessInfo* Create();
+ static void Register();
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/process/osx/CMakeLists.txt b/xbmc/cores/RetroPlayer/process/osx/CMakeLists.txt
new file mode 100644
index 0000000..b8f5a18
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/osx/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES RPProcessInfoOSX.cpp)
+
+set(HEADERS RPProcessInfoOSX.h)
+
+core_add_library(rp-process-osx)
diff --git a/xbmc/cores/RetroPlayer/process/osx/RPProcessInfoOSX.cpp b/xbmc/cores/RetroPlayer/process/osx/RPProcessInfoOSX.cpp
new file mode 100644
index 0000000..c9192b9
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/osx/RPProcessInfoOSX.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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 "RPProcessInfoOSX.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRPProcessInfoOSX::CRPProcessInfoOSX() : CRPProcessInfo("macOS")
+{
+}
+
+CRPProcessInfo* CRPProcessInfoOSX::Create()
+{
+ return new CRPProcessInfoOSX();
+}
+
+void CRPProcessInfoOSX::Register()
+{
+ CRPProcessInfo::RegisterProcessControl(CRPProcessInfoOSX::Create);
+}
diff --git a/xbmc/cores/RetroPlayer/process/osx/RPProcessInfoOSX.h b/xbmc/cores/RetroPlayer/process/osx/RPProcessInfoOSX.h
new file mode 100644
index 0000000..646f3fa
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/osx/RPProcessInfoOSX.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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/RetroPlayer/process/RPProcessInfo.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfoOSX : public CRPProcessInfo
+{
+public:
+ CRPProcessInfoOSX();
+
+ static CRPProcessInfo* Create();
+ static void Register();
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/process/wayland/CMakeLists.txt b/xbmc/cores/RetroPlayer/process/wayland/CMakeLists.txt
new file mode 100644
index 0000000..1cc13fa
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/wayland/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES RPProcessInfoWayland.cpp)
+
+set(HEADERS RPProcessInfoWayland.h)
+
+core_add_library(rp-process-wayland)
diff --git a/xbmc/cores/RetroPlayer/process/wayland/RPProcessInfoWayland.cpp b/xbmc/cores/RetroPlayer/process/wayland/RPProcessInfoWayland.cpp
new file mode 100644
index 0000000..2de0fa9
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/wayland/RPProcessInfoWayland.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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 "RPProcessInfoWayland.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRPProcessInfoWayland::CRPProcessInfoWayland() : CRPProcessInfo("Wayland")
+{
+}
+
+CRPProcessInfo* CRPProcessInfoWayland::Create()
+{
+ return new CRPProcessInfoWayland();
+}
+
+void CRPProcessInfoWayland::Register()
+{
+ CRPProcessInfo::RegisterProcessControl(CRPProcessInfoWayland::Create);
+}
diff --git a/xbmc/cores/RetroPlayer/process/wayland/RPProcessInfoWayland.h b/xbmc/cores/RetroPlayer/process/wayland/RPProcessInfoWayland.h
new file mode 100644
index 0000000..244ee20
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/wayland/RPProcessInfoWayland.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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/RetroPlayer/process/RPProcessInfo.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfoWayland : public CRPProcessInfo
+{
+public:
+ CRPProcessInfoWayland();
+
+ static CRPProcessInfo* Create();
+ static void Register();
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/process/windows/CMakeLists.txt b/xbmc/cores/RetroPlayer/process/windows/CMakeLists.txt
new file mode 100644
index 0000000..644ce8e
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/windows/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES RPProcessInfoWin.cpp)
+set(HEADERS RPProcessInfoWin.h)
+
+core_add_library(rp-processwin)
diff --git a/xbmc/cores/RetroPlayer/process/windows/RPProcessInfoWin.cpp b/xbmc/cores/RetroPlayer/process/windows/RPProcessInfoWin.cpp
new file mode 100644
index 0000000..3601551
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/windows/RPProcessInfoWin.cpp
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017-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 "RPProcessInfoWin.h"
+
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPWinRenderer.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRPProcessInfoWin::CRPProcessInfoWin() : CRPProcessInfo("Windows")
+{
+}
+
+CRPProcessInfo* CRPProcessInfoWin::Create()
+{
+ return new CRPProcessInfoWin();
+}
+
+void CRPProcessInfoWin::Register()
+{
+ CRPProcessInfo::RegisterProcessControl(CRPProcessInfoWin::Create);
+}
diff --git a/xbmc/cores/RetroPlayer/process/windows/RPProcessInfoWin.h b/xbmc/cores/RetroPlayer/process/windows/RPProcessInfoWin.h
new file mode 100644
index 0000000..8596446
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/process/windows/RPProcessInfoWin.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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 "../RPProcessInfo.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfoWin : public CRPProcessInfo
+{
+public:
+ CRPProcessInfoWin();
+
+ static CRPProcessInfo* Create();
+ static void Register();
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/CMakeLists.txt b/xbmc/cores/RetroPlayer/rendering/CMakeLists.txt
new file mode 100644
index 0000000..5e02a7a
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(SOURCES RenderContext.cpp
+ RenderSettings.cpp
+ RenderTranslator.cpp
+ RenderUtils.cpp
+ RenderVideoSettings.cpp
+ RPRenderManager.cpp
+)
+
+set(HEADERS IRenderManager.h
+ RenderContext.h
+ RenderSettings.h
+ RenderTranslator.h
+ RenderUtils.h
+ RenderVideoSettings.h
+ RPRenderManager.h
+)
+
+core_add_library(rp-rendering)
diff --git a/xbmc/cores/RetroPlayer/rendering/IRenderManager.h b/xbmc/cores/RetroPlayer/rendering/IRenderManager.h
new file mode 100644
index 0000000..32bb08c
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/IRenderManager.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017-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 "utils/Geometry.h"
+
+struct RESOLUTION_INFO;
+
+namespace KODI
+{
+namespace RETRO
+{
+class IGUIRenderSettings;
+
+/*!
+ * \brief Interface to expose rendering functions to GUI components
+ */
+class IRenderManager
+{
+public:
+ virtual ~IRenderManager() = default;
+
+ /*!
+ * \brief Render a fullscreen window
+ *
+ * \param bClear Whether the render area should be cleared
+ * \param coordsRes Resolution that the window coordinates are in
+ */
+ virtual void RenderWindow(bool bClear, const RESOLUTION_INFO& coordsRes) = 0;
+
+ /*!
+ * \brief Render a game control
+ *
+ * \param bClear Whether the render area should be cleared
+ * \param bUseAlpha Whether the graphics context's alpha should be used
+ * \param renderRegion The region of the control being rendered
+ * \param renderSettings The settings used to render the control
+ */
+ virtual void RenderControl(bool bClear,
+ bool bUseAlpha,
+ const CRect& renderRegion,
+ const IGUIRenderSettings* renderSettings) = 0;
+
+ /*!
+ * \brief Clear the background of a fullscreen window
+ */
+ virtual void ClearBackground() = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/RPRenderManager.cpp b/xbmc/cores/RetroPlayer/rendering/RPRenderManager.cpp
new file mode 100644
index 0000000..7d76d7e
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RPRenderManager.cpp
@@ -0,0 +1,1066 @@
+/*
+ * Copyright (C) 2017-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 "RPRenderManager.h"
+
+#include "RenderContext.h"
+#include "RenderSettings.h"
+#include "RenderTranslator.h"
+#include "URL.h"
+#include "cores/RetroPlayer/buffers/IRenderBuffer.h"
+#include "cores/RetroPlayer/buffers/IRenderBufferPool.h"
+#include "cores/RetroPlayer/buffers/RenderBufferManager.h"
+#include "cores/RetroPlayer/guibridge/GUIGameSettings.h"
+#include "cores/RetroPlayer/guibridge/GUIRenderTargetFactory.h"
+#include "cores/RetroPlayer/guibridge/IGUIRenderSettings.h"
+#include "cores/RetroPlayer/guicontrols/GUIGameControl.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPBaseRenderer.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
+#include "cores/RetroPlayer/streams/RetroPlayerVideo.h"
+#include "filesystem/File.h"
+#include "pictures/Picture.h"
+#include "threads/SingleLock.h"
+#include "utils/ColorUtils.h"
+#include "utils/TransformMatrix.h"
+#include "utils/log.h"
+
+#include <cstring>
+#include <mutex>
+
+extern "C"
+{
+#include <libswscale/swscale.h>
+}
+
+#include <algorithm>
+#include <cstring>
+
+using namespace KODI;
+using namespace RETRO;
+
+CRPRenderManager::CRPRenderManager(CRPProcessInfo& processInfo)
+ : m_processInfo(processInfo),
+ m_renderContext(processInfo.GetRenderContext()),
+ m_renderSettings(new CGUIGameSettings(processInfo)),
+ m_renderControlFactory(new CGUIRenderTargetFactory(this))
+{
+}
+
+void CRPRenderManager::Initialize()
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[RENDER]: Initializing render manager");
+}
+
+void CRPRenderManager::Deinitialize()
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[RENDER]: Deinitializing render manager");
+
+ // Wait for savestate tasks
+ for (std::future<void>& task : m_savestateThreads)
+ task.wait();
+ m_savestateThreads.clear();
+
+ for (auto& pixelScalerMap : m_scalers)
+ {
+ for (auto& pixelScaler : pixelScalerMap.second)
+ {
+ if (pixelScaler.second != nullptr)
+ sws_freeContext(pixelScaler.second);
+ }
+ }
+ m_scalers.clear();
+
+ for (auto renderBuffer : m_renderBuffers)
+ renderBuffer->Release();
+ m_renderBuffers.clear();
+
+ for (auto buffer : m_pendingBuffers)
+ buffer->Release();
+ m_pendingBuffers.clear();
+
+ for (auto& [savestatePath, renderBuffers] : m_savestateBuffers)
+ {
+ for (auto renderBuffer : renderBuffers)
+ renderBuffer->Release();
+ }
+ m_savestateBuffers.clear();
+
+ m_renderers.clear();
+
+ m_state = RENDER_STATE::UNCONFIGURED;
+}
+
+bool CRPRenderManager::Configure(AVPixelFormat format,
+ unsigned int nominalWidth,
+ unsigned int nominalHeight,
+ unsigned int maxWidth,
+ unsigned int maxHeight,
+ float pixelAspectRatio)
+{
+ CLog::Log(LOGINFO, "RetroPlayer[RENDER]: Configuring format {}, nominal {}x{}, max {}x{}",
+ CRenderTranslator::TranslatePixelFormat(format), nominalWidth, nominalHeight, maxWidth,
+ maxHeight);
+
+ // Immutable parameters
+ m_format = format;
+ m_nominalWidth = nominalWidth;
+ m_nominalHeight = nominalHeight;
+ m_maxWidth = maxWidth;
+ m_maxHeight = maxHeight;
+ m_pixelAspectRatio = pixelAspectRatio;
+
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+
+ m_state = RENDER_STATE::CONFIGURING;
+
+ return true;
+}
+
+bool CRPRenderManager::GetVideoBuffer(unsigned int width,
+ unsigned int height,
+ VideoStreamBuffer& buffer)
+{
+ // Clear any previous pending buffers
+ for (IRenderBuffer* buffer : m_pendingBuffers)
+ buffer->Release();
+ m_pendingBuffers.clear();
+
+ if (m_bFlush || m_state != RENDER_STATE::CONFIGURED)
+ return false;
+
+ // We should do our best to get a valid render buffer. If we return false,
+ // the game add-on will likely allocate its own memory.
+ IRenderBuffer* renderBuffer = nullptr;
+
+ auto bufferPools = m_processInfo.GetBufferManager().GetBufferPools();
+
+ std::sort(bufferPools.begin(), bufferPools.end(),
+ [](const IRenderBufferPool* lhs, const IRenderBufferPool* rhs) {
+ // Prefer buffer pools with a visible renderer
+ if (lhs->HasVisibleRenderer() && !rhs->HasVisibleRenderer())
+ return true;
+ if (!lhs->HasVisibleRenderer() && rhs->HasVisibleRenderer())
+ return false;
+
+ //! @todo De-prioritize buffer pools with write-only or unaligned memory
+
+ return false;
+ });
+
+ for (IRenderBufferPool* bufferPool : bufferPools)
+ {
+ renderBuffer = bufferPool->GetBuffer(width, height);
+ if (renderBuffer != nullptr)
+ break;
+ else
+ CLog::Log(LOGDEBUG, "RetroPlayer[RENDER]: Unable to get video buffer for frame");
+ }
+
+ if (renderBuffer == nullptr)
+ return false;
+
+ buffer = VideoStreamBuffer{renderBuffer->GetFormat(), renderBuffer->GetMemory(),
+ renderBuffer->GetFrameSize(), renderBuffer->GetMemoryAccess(),
+ renderBuffer->GetMemoryAlignment()};
+
+ m_pendingBuffers.emplace_back(std::move(renderBuffer));
+
+ return true;
+}
+
+void CRPRenderManager::AddFrame(const uint8_t* data,
+ size_t size,
+ unsigned int width,
+ unsigned int height,
+ unsigned int orientationDegCCW)
+{
+ if (m_bFlush || m_state != RENDER_STATE::CONFIGURED)
+ return;
+
+ // Validate parameters
+ if (data == nullptr || size == 0 || width == 0 || height == 0)
+ return;
+
+ // Get render buffers to copy the frame into
+ std::vector<IRenderBuffer*> renderBuffers;
+
+ // Check pending buffers
+ for (IRenderBuffer* buffer : m_pendingBuffers)
+ {
+ if (buffer->GetMemory() == data)
+ {
+ buffer->Acquire();
+ renderBuffers.emplace_back(buffer);
+ }
+ }
+
+ // If we aren't submitting a zero-copy frame, copy into render buffer now
+ if (renderBuffers.empty())
+ {
+ // Copy frame to buffers with visible renderers
+ for (IRenderBufferPool* bufferPool : m_processInfo.GetBufferManager().GetBufferPools())
+ {
+ if (!bufferPool->HasVisibleRenderer())
+ continue;
+
+ IRenderBuffer* renderBuffer = bufferPool->GetBuffer(width, height);
+ if (renderBuffer != nullptr)
+ {
+ CopyFrame(renderBuffer, m_format, data, size, width, height);
+ renderBuffers.emplace_back(renderBuffer);
+ }
+ else
+ CLog::Log(LOGDEBUG, "RetroPlayer[RENDER]: Unable to get render buffer for frame");
+ }
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ // Set render buffers
+ for (auto renderBuffer : m_renderBuffers)
+ renderBuffer->Release();
+ m_renderBuffers = std::move(renderBuffers);
+
+ // Apply rotation to render buffers
+ for (auto renderBuffer : m_renderBuffers)
+ renderBuffer->SetRotation(orientationDegCCW);
+
+ // Cache frame if it arrived after being paused
+ if (m_speed == 0.0)
+ {
+ std::vector<uint8_t> cachedFrame = std::move(m_cachedFrame);
+
+ if (!m_bHasCachedFrame)
+ {
+ // In this case, cachedFrame is definitely empty (see invariant for
+ // m_bHasCachedFrame). Otherwise, cachedFrame may be empty if the frame
+ // is being copied in the rendering thread. In that case, we would want
+ // to leave cached frame empty to avoid caching another frame.
+
+ cachedFrame.resize(size);
+ m_bHasCachedFrame = true;
+ }
+
+ if (!cachedFrame.empty())
+ {
+ {
+ CSingleExit exit(m_bufferMutex);
+ std::memcpy(cachedFrame.data(), data, size);
+ }
+ m_cachedFrame = std::move(cachedFrame);
+ m_cachedWidth = width;
+ m_cachedHeight = height;
+ m_cachedRotationCCW = orientationDegCCW;
+ }
+ }
+ }
+}
+
+void CRPRenderManager::SetSpeed(double speed)
+{
+ m_speed = speed;
+}
+
+void CRPRenderManager::FrameMove()
+{
+ CheckFlush();
+
+ bool bIsConfigured = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+
+ if (m_state == RENDER_STATE::CONFIGURING)
+ {
+ m_state = RENDER_STATE::CONFIGURED;
+
+ CLog::Log(LOGINFO, "RetroPlayer[RENDER]: Renderer configured on first frame");
+ }
+
+ if (m_state == RENDER_STATE::CONFIGURED)
+ bIsConfigured = true;
+ }
+
+ if (bIsConfigured)
+ {
+ for (auto& renderer : m_renderers)
+ renderer->FrameMove();
+ }
+}
+
+void CRPRenderManager::CheckFlush()
+{
+ if (m_bFlush)
+ {
+ {
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+ for (auto renderBuffer : m_renderBuffers)
+ renderBuffer->Release();
+ m_renderBuffers.clear();
+
+ m_cachedFrame.clear();
+ m_cachedWidth = 0;
+ m_cachedHeight = 0;
+
+ m_bHasCachedFrame = false;
+ }
+
+ for (const auto& renderer : m_renderers)
+ renderer->Flush();
+
+ m_processInfo.GetBufferManager().FlushPools();
+
+ m_bFlush = false;
+ }
+}
+
+void CRPRenderManager::Flush()
+{
+ m_bFlush = true;
+}
+
+void CRPRenderManager::RenderWindow(bool bClear, const RESOLUTION_INFO& coordsRes)
+{
+ // Get a renderer for the fullscreen window
+ std::shared_ptr<CRPBaseRenderer> renderer = GetRendererForSettings(nullptr);
+ if (!renderer)
+ return;
+
+ // Get a render buffer for the renderer
+ IRenderBuffer* renderBuffer = GetRenderBuffer(renderer->GetBufferPool());
+
+ m_renderContext.SetRenderingResolution(m_renderContext.GetVideoResolution(), false);
+
+ if (!m_bDisplayScaleSet && m_renderContext.DisplayHardwareScalingEnabled())
+ {
+ // If the renderer has a render buffer, get the dimensions
+ const unsigned int sourceWidth = (renderBuffer != nullptr ? renderBuffer->GetWidth() : 0);
+ const unsigned int sourceHeight = (renderBuffer != nullptr ? renderBuffer->GetHeight() : 0);
+
+ // Get render video settings for the fullscreen window
+ CRenderVideoSettings renderVideoSettings = GetEffectiveSettings(nullptr);
+
+ // Get the scaling mode of the render video settings
+ const SCALINGMETHOD scaleMode = renderVideoSettings.GetScalingMethod();
+ const STRETCHMODE stretchMode = renderVideoSettings.GetRenderStretchMode();
+
+ // Update display with video dimensions for integer scaling
+ if (scaleMode == SCALINGMETHOD::NEAREST && stretchMode == STRETCHMODE::Original &&
+ sourceWidth > 0 && sourceHeight > 0)
+ {
+ RESOLUTION_INFO gameRes = m_renderContext.GetResInfo();
+ gameRes.Overscan.left = 0;
+ gameRes.Overscan.top = 0;
+ gameRes.Overscan.right = sourceWidth;
+ gameRes.Overscan.bottom = sourceHeight;
+ gameRes.iWidth = sourceWidth;
+ gameRes.iHeight = sourceHeight;
+ gameRes.iScreenWidth = sourceWidth;
+ gameRes.iScreenHeight = sourceHeight;
+
+ m_renderContext.UpdateDisplayHardwareScaling(gameRes);
+ m_bDisplayScaleSet = true;
+ }
+ }
+
+ RenderInternal(renderer, renderBuffer, bClear, 255);
+
+ m_renderContext.SetRenderingResolution(coordsRes, false);
+}
+
+void CRPRenderManager::RenderControl(bool bClear,
+ bool bUseAlpha,
+ const CRect& renderRegion,
+ const IGUIRenderSettings* renderSettings)
+{
+ // Get a renderer for the control
+ std::shared_ptr<CRPBaseRenderer> renderer = GetRendererForSettings(renderSettings);
+ if (!renderer)
+ return;
+
+ IRenderBuffer* renderBuffer = nullptr;
+
+ // Get render buffer for external pixels, if requested
+ const std::string& pixelPath = renderer->GetRenderSettings().VideoSettings().GetPixels();
+ if (!pixelPath.empty())
+ {
+ renderBuffer = GetRenderBufferForSavestate(pixelPath, renderer->GetBufferPool());
+ }
+ else
+ {
+ // Get a render buffer for the renderer
+ renderBuffer = GetRenderBuffer(renderer->GetBufferPool());
+ }
+
+ if (renderBuffer == nullptr)
+ return;
+
+ // Set fullscreen
+ const bool bWasFullscreen = m_renderContext.IsFullScreenVideo();
+ if (bWasFullscreen)
+ m_renderContext.SetFullScreenVideo(false);
+
+ // Set coordinates
+ CRect coords = renderSettings->GetDimensions();
+ m_renderContext.SetViewWindow(coords.x1, coords.y1, coords.x2, coords.y2);
+ TransformMatrix mat;
+ m_renderContext.SetTransform(mat, 1.0, 1.0);
+
+ // Clear render area
+ if (bClear)
+ {
+ CRect old = m_renderContext.GetScissors();
+ CRect region = renderRegion;
+ region.Intersect(old);
+ m_renderContext.SetScissors(region);
+ m_renderContext.Clear(UTILS::COLOR::BLACK);
+ m_renderContext.SetScissors(old);
+ }
+
+ // Calculate alpha
+ UTILS::COLOR::Color alpha = 255;
+ if (bUseAlpha)
+ alpha = m_renderContext.MergeAlpha(UTILS::COLOR::BLACK) >> 24;
+
+ RenderInternal(renderer, renderBuffer, false, alpha);
+
+ // Restore coordinates
+ m_renderContext.RemoveTransform();
+
+ // Restore fullscreen
+ if (bWasFullscreen)
+ m_renderContext.SetFullScreenVideo(true);
+}
+
+void CRPRenderManager::ClearBackground()
+{
+ m_renderContext.Clear(0);
+}
+
+bool CRPRenderManager::SupportsRenderFeature(RENDERFEATURE feature) const
+{
+ //! @todo Move to ProcessInfo
+ for (const auto& renderer : m_renderers)
+ {
+ if (renderer->Supports(feature))
+ return true;
+ }
+
+ return false;
+}
+
+bool CRPRenderManager::SupportsScalingMethod(SCALINGMETHOD method) const
+{
+ //! @todo Move to ProcessInfo
+ for (IRenderBufferPool* bufferPool : m_processInfo.GetBufferManager().GetBufferPools())
+ {
+ CRenderVideoSettings renderSettings;
+ renderSettings.SetScalingMethod(method);
+ if (bufferPool->IsCompatible(renderSettings))
+ return true;
+ }
+
+ return false;
+}
+
+void CRPRenderManager::RenderInternal(const std::shared_ptr<CRPBaseRenderer>& renderer,
+ IRenderBuffer* renderBuffer,
+ bool bClear,
+ uint32_t alpha)
+{
+ renderer->PreRender(bClear);
+
+ CSingleExit exitLock(m_renderContext.GraphicsMutex());
+
+ if (renderBuffer != nullptr)
+ {
+ bool bUploaded = true;
+
+ if (!renderBuffer->IsLoaded())
+ {
+ bUploaded = renderBuffer->UploadTexture();
+ renderBuffer->SetLoaded(true);
+ }
+
+ if (bUploaded)
+ renderer->SetBuffer(renderBuffer);
+
+ renderBuffer->Release();
+ }
+
+ renderer->RenderFrame(bClear, alpha);
+}
+
+std::shared_ptr<CRPBaseRenderer> CRPRenderManager::GetRendererForSettings(
+ const IGUIRenderSettings* renderSettings)
+{
+ std::shared_ptr<CRPBaseRenderer> renderer;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+ if (m_state == RENDER_STATE::UNCONFIGURED)
+ return renderer;
+ }
+
+ CRenderSettings effectiveRenderSettings;
+ effectiveRenderSettings.VideoSettings() = GetEffectiveSettings(renderSettings);
+
+ // Check renderers in order of buffer pools
+ for (IRenderBufferPool* bufferPool : m_processInfo.GetBufferManager().GetBufferPools())
+ {
+ renderer = GetRendererForPool(bufferPool, effectiveRenderSettings);
+ if (renderer)
+ break;
+ }
+
+ if (renderer)
+ {
+ renderer->SetScalingMethod(effectiveRenderSettings.VideoSettings().GetScalingMethod());
+ renderer->SetStretchMode(effectiveRenderSettings.VideoSettings().GetRenderStretchMode());
+ renderer->SetRenderRotation(effectiveRenderSettings.VideoSettings().GetRenderRotation());
+ renderer->SetPixels(effectiveRenderSettings.VideoSettings().GetPixels());
+ }
+
+ return renderer;
+}
+
+std::shared_ptr<CRPBaseRenderer> CRPRenderManager::GetRendererForPool(
+ IRenderBufferPool* bufferPool, const CRenderSettings& renderSettings)
+{
+ std::shared_ptr<CRPBaseRenderer> renderer;
+
+ if (!bufferPool->IsCompatible(renderSettings.VideoSettings()))
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[RENDER]: buffer pool is not compatible with renderer");
+ return renderer;
+ }
+
+ // Get compatible renderer for this buffer pool
+ for (const auto& it : m_renderers)
+ {
+ if (it->GetBufferPool() != bufferPool)
+ continue;
+
+ if (!it->IsCompatible(renderSettings.VideoSettings()))
+ continue;
+
+ renderer = it;
+ break;
+ }
+
+ // If buffer pool has no compatible renderers, create one now
+ if (!renderer)
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[RENDER]: Creating renderer for {}",
+ m_processInfo.GetRenderSystemName(bufferPool));
+
+ renderer.reset(m_processInfo.CreateRenderer(bufferPool, renderSettings));
+ if (renderer && renderer->Configure(m_format))
+ {
+ // Ensure we have a render buffer for this renderer
+ CreateRenderBuffer(renderer->GetBufferPool());
+
+ m_renderers.insert(renderer);
+ }
+ else
+ renderer.reset();
+ }
+
+ return renderer;
+}
+
+bool CRPRenderManager::HasRenderBuffer(IRenderBufferPool* bufferPool)
+{
+ bool bHasRenderBuffer = false;
+
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ auto it = std::find_if(
+ m_renderBuffers.begin(), m_renderBuffers.end(),
+ [bufferPool](IRenderBuffer* renderBuffer) { return renderBuffer->GetPool() == bufferPool; });
+
+ if (it != m_renderBuffers.end())
+ bHasRenderBuffer = true;
+
+ return bHasRenderBuffer;
+}
+
+IRenderBuffer* CRPRenderManager::GetRenderBuffer(IRenderBufferPool* bufferPool)
+{
+ if (m_bFlush || m_state != RENDER_STATE::CONFIGURED)
+ return nullptr;
+
+ IRenderBuffer* renderBuffer = nullptr;
+
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ auto getRenderBuffer = [bufferPool](IRenderBuffer* renderBuffer) {
+ return renderBuffer->GetPool() == bufferPool;
+ };
+
+ auto it = std::find_if(m_renderBuffers.begin(), m_renderBuffers.end(), getRenderBuffer);
+
+ // If our renderer has no buffer, try to create one from paused frame now
+ if (it == m_renderBuffers.end())
+ {
+ CreateRenderBuffer(bufferPool);
+ it = std::find_if(m_renderBuffers.begin(), m_renderBuffers.end(), getRenderBuffer);
+ }
+
+ if (it != m_renderBuffers.end())
+ {
+ renderBuffer = *it;
+ renderBuffer->Acquire();
+ }
+
+ return renderBuffer;
+}
+
+IRenderBuffer* CRPRenderManager::GetRenderBufferForSavestate(const std::string& savestatePath,
+ const IRenderBufferPool* bufferPool)
+{
+ IRenderBuffer* renderBuffer = nullptr;
+
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ // Check to see if we have a buffers for the specified path
+ auto it = m_savestateBuffers.find(savestatePath);
+ if (it != m_savestateBuffers.end())
+ {
+ // Get a render buffer belonging to the specified pool
+ const std::vector<IRenderBuffer*>& renderBuffers = it->second;
+
+ auto it = std::find_if(
+ renderBuffers.begin(), renderBuffers.end(),
+ [bufferPool](IRenderBuffer* buffer) { return buffer->GetPool() == bufferPool; });
+
+ if (it != renderBuffers.end())
+ {
+ renderBuffer = *it;
+ renderBuffer->Acquire();
+ }
+ }
+ else
+ {
+ // The path isn't loaded, mark it as seen and load asynchronously
+ m_savestateBuffers[savestatePath] = {};
+ LoadVideoFrameAsync(savestatePath);
+ }
+
+ return renderBuffer;
+}
+
+void CRPRenderManager::CreateRenderBuffer(IRenderBufferPool* bufferPool)
+{
+ if (m_bFlush || m_state != RENDER_STATE::CONFIGURED)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ if (!HasRenderBuffer(bufferPool) && m_bHasCachedFrame)
+ {
+ IRenderBuffer* renderBuffer =
+ CreateFromCache(m_cachedFrame, m_cachedWidth, m_cachedHeight, bufferPool, m_bufferMutex);
+ if (renderBuffer != nullptr)
+ m_renderBuffers.emplace_back(renderBuffer);
+ }
+}
+
+IRenderBuffer* CRPRenderManager::CreateFromCache(std::vector<uint8_t>& cachedFrame,
+ unsigned int width,
+ unsigned int height,
+ IRenderBufferPool* bufferPool,
+ CCriticalSection& mutex)
+{
+ // Take ownership of cached frame
+ std::vector<uint8_t> ownedFrame = std::move(cachedFrame);
+
+ if (!ownedFrame.empty())
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[RENDER]: Creating render buffer for renderer from cache");
+
+ IRenderBuffer* renderBuffer = bufferPool->GetBuffer(width, height);
+ if (renderBuffer != nullptr)
+ {
+ CSingleExit exit(mutex);
+ CopyFrame(renderBuffer, m_format, ownedFrame.data(), ownedFrame.size(), width, height);
+ }
+
+ // Return ownership of cached frame
+ cachedFrame = std::move(ownedFrame);
+
+ return renderBuffer;
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[RENDER]: Failed to create render buffer, no cached frame");
+ }
+
+ return nullptr;
+}
+
+void CRPRenderManager::CopyFrame(IRenderBuffer* renderBuffer,
+ AVPixelFormat format,
+ const uint8_t* data,
+ size_t size,
+ unsigned int width,
+ unsigned int height)
+{
+ const uint8_t* source = data;
+ uint8_t* target = renderBuffer->GetMemory();
+
+ if (target != nullptr)
+ {
+ const unsigned int sourceStride = static_cast<unsigned int>(size / height);
+ const unsigned int targetStride =
+ static_cast<unsigned int>(renderBuffer->GetFrameSize() / renderBuffer->GetHeight());
+
+ if (format == renderBuffer->GetFormat())
+ {
+ if (sourceStride == targetStride)
+ std::memcpy(target, source, size);
+ else
+ {
+ const unsigned int widthBytes = CRenderTranslator::TranslateWidthToBytes(width, format);
+ if (widthBytes > 0)
+ {
+ for (unsigned int i = 0; i < height; i++)
+ std::memcpy(target + targetStride * i, source + sourceStride * i, widthBytes);
+ }
+ }
+ }
+ else
+ {
+ SwsContext*& scalerContext = m_scalers[format][renderBuffer->GetFormat()];
+ scalerContext = sws_getCachedContext(
+ scalerContext, width, height, format, renderBuffer->GetWidth(), renderBuffer->GetHeight(),
+ renderBuffer->GetFormat(), SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
+
+ if (scalerContext != nullptr)
+ {
+ uint8_t* src[] = {const_cast<uint8_t*>(source), nullptr, nullptr, nullptr};
+ int srcStride[] = {static_cast<int>(sourceStride), 0, 0, 0};
+ uint8_t* dst[] = {target, nullptr, nullptr, nullptr};
+ int dstStride[] = {static_cast<int>(targetStride), 0, 0, 0};
+
+ sws_scale(scalerContext, src, srcStride, 0, height, dst, dstStride);
+ }
+ }
+ }
+
+ renderBuffer->ReleaseMemory();
+}
+
+CRenderVideoSettings CRPRenderManager::GetEffectiveSettings(
+ const IGUIRenderSettings* settings) const
+{
+ CRenderVideoSettings effectiveSettings = m_renderSettings->GetSettings().VideoSettings();
+
+ if (settings != nullptr)
+ {
+ if (settings->HasVideoFilter())
+ effectiveSettings.SetVideoFilter(settings->GetSettings().VideoSettings().GetVideoFilter());
+ if (settings->HasStretchMode())
+ effectiveSettings.SetRenderStretchMode(
+ settings->GetSettings().VideoSettings().GetRenderStretchMode());
+ if (settings->HasRotation())
+ effectiveSettings.SetRenderRotation(
+ settings->GetSettings().VideoSettings().GetRenderRotation());
+ if (settings->HasPixels())
+ effectiveSettings.SetPixels(settings->GetSettings().VideoSettings().GetPixels());
+ }
+
+ // Sanitize settings
+ if (!m_processInfo.HasScalingMethod(effectiveSettings.GetScalingMethod()))
+ {
+ effectiveSettings.SetScalingMethod(m_processInfo.GetDefaultScalingMethod());
+ }
+
+ return effectiveSettings;
+}
+
+void CRPRenderManager::SaveThumbnail(const std::string& thumbnailPath)
+{
+ // Get a suitable render buffer for capturing the video data, or use the
+ // cached frame if a readable buffer can't be found
+ IRenderBuffer* renderBuffer = nullptr;
+ std::vector<uint8_t> cachedFrame;
+
+ GetVideoFrame(renderBuffer, cachedFrame);
+
+ // Video frame properties
+ AVPixelFormat sourceFormat = AV_PIX_FMT_NONE;
+ const uint8_t* sourceData = nullptr;
+ size_t sourceSize = 0;
+ unsigned int width = 0;
+ unsigned int height = 0;
+ unsigned int rotationCCW = 0;
+
+ if (renderBuffer != nullptr)
+ {
+ sourceFormat = renderBuffer->GetFormat();
+ sourceData = renderBuffer->GetMemory();
+ sourceSize = renderBuffer->GetFrameSize();
+ width = renderBuffer->GetWidth();
+ height = renderBuffer->GetHeight();
+ rotationCCW = renderBuffer->GetRotation();
+ }
+ else if (!cachedFrame.empty())
+ {
+ sourceFormat = m_format;
+ sourceData = m_cachedFrame.data();
+ sourceSize = m_cachedFrame.size();
+ width = m_cachedWidth;
+ height = m_cachedHeight;
+ rotationCCW = m_cachedRotationCCW;
+ }
+
+ if (sourceFormat == AV_PIX_FMT_NONE)
+ {
+ CLog::Log(LOGERROR, "Failed to get a video frame for savestate thumbnail");
+ return;
+ }
+
+ std::vector<uint8_t> copiedData(sourceData, sourceData + sourceSize);
+
+ const int stride = CRenderTranslator::TranslateWidthToBytes(width, sourceFormat);
+
+ unsigned int scaleWidth = 400;
+ unsigned int scaleHeight = 220;
+ CPicture::GetScale(width, height, scaleWidth, scaleHeight);
+
+ const int bytesPerPixel = 4;
+ std::vector<uint8_t> scaledImage(scaleWidth * scaleHeight * bytesPerPixel);
+
+ const AVPixelFormat outFormat = AV_PIX_FMT_BGR0;
+ const int scaleStride = CRenderTranslator::TranslateWidthToBytes(scaleWidth, outFormat);
+
+ if (CPicture::ScaleImage(copiedData.data(), width, height, stride, sourceFormat,
+ scaledImage.data(), scaleWidth, scaleHeight, scaleStride, outFormat))
+ {
+ //! @todo rotate image by rotationCCW
+ (void)rotationCCW;
+
+ CPicture::CreateThumbnailFromSurface(scaledImage.data(), scaleWidth, scaleHeight, scaleStride,
+ thumbnailPath);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Failed to scale image from size {}x{} to size {}x{}", width, height,
+ scaleWidth, scaleHeight);
+ }
+
+ FreeVideoFrame(renderBuffer, std::move(cachedFrame));
+}
+
+void CRPRenderManager::CacheVideoFrame(const std::string& savestatePath)
+{
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ // Get the render buffers for this savestate path
+ std::vector<IRenderBuffer*>& savestateBuffers = m_savestateBuffers[savestatePath];
+
+ // Release old buffers
+ for (IRenderBuffer* renderBuffer : savestateBuffers)
+ renderBuffer->Release();
+
+ // Save buffers
+ savestateBuffers = m_renderBuffers;
+
+ // Acquire new buffers
+ for (IRenderBuffer* renderBuffer : savestateBuffers)
+ renderBuffer->Acquire();
+}
+
+void CRPRenderManager::SaveVideoFrame(const std::string& savestatePath, ISavestate& savestate)
+{
+ // Get a suitable render buffer for capturing the video data, or use the
+ // cached frame if a readable buffer can't be found
+ IRenderBuffer* readableBuffer = nullptr;
+ std::vector<uint8_t> cachedFrame;
+
+ GetVideoFrame(readableBuffer, cachedFrame);
+
+ // Video frame properties
+ AVPixelFormat targetFormat = AV_PIX_FMT_NONE;
+ unsigned int width = 0;
+ unsigned int height = 0;
+ unsigned int rotationCCW = 0;
+ size_t sourceSize = 0;
+ const uint8_t* sourceData = nullptr;
+
+ if (readableBuffer != nullptr)
+ {
+ targetFormat = readableBuffer->GetFormat();
+ width = readableBuffer->GetWidth();
+ height = readableBuffer->GetHeight();
+ rotationCCW = readableBuffer->GetRotation();
+ sourceSize = readableBuffer->GetFrameSize();
+ sourceData = readableBuffer->GetMemory();
+ }
+ else if (!cachedFrame.empty())
+ {
+ targetFormat = m_format;
+ width = m_cachedWidth;
+ height = m_cachedHeight;
+ rotationCCW = m_cachedRotationCCW;
+ sourceSize = m_cachedFrame.size();
+ sourceData = m_cachedFrame.data();
+ }
+
+ if (targetFormat == AV_PIX_FMT_NONE)
+ {
+ CLog::Log(LOGERROR, "Failed to get a video frame for savestate video frame");
+ }
+ else
+ {
+ // Serialize video stream properties
+ savestate.SetPixelFormat(targetFormat);
+ savestate.SetNominalWidth(m_nominalWidth);
+ savestate.SetNominalHeight(m_nominalHeight);
+ savestate.SetMaxWidth(m_maxWidth);
+ savestate.SetMaxHeight(m_maxHeight);
+ savestate.SetPixelAspectRatio(m_pixelAspectRatio);
+
+ // Serialize video frame properties
+ savestate.SetVideoWidth(width);
+ savestate.SetVideoHeight(height);
+ savestate.SetRotationDegCCW(rotationCCW);
+ uint8_t* const targetData = savestate.GetVideoBuffer(sourceSize);
+ std::memcpy(targetData, sourceData, sourceSize);
+ }
+
+ FreeVideoFrame(readableBuffer, std::move(cachedFrame));
+}
+
+void CRPRenderManager::ClearVideoFrame(const std::string& savestatePath)
+{
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ auto it = m_savestateBuffers.find(savestatePath);
+ if (it != m_savestateBuffers.end())
+ {
+ for (auto renderBuffer : it->second)
+ renderBuffer->Release();
+ m_savestateBuffers.erase(it);
+ }
+}
+
+void CRPRenderManager::GetVideoFrame(IRenderBuffer*& readableBuffer,
+ std::vector<uint8_t>& cachedFrame)
+{
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ // Get a readable render buffer
+ auto it = std::find_if(m_renderBuffers.begin(), m_renderBuffers.end(),
+ [](const IRenderBuffer* renderBuffer) {
+ return renderBuffer->GetMemoryAccess() != DataAccess::WRITE_ONLY;
+ });
+
+ // Aquire buffer if one was found
+ if (it != m_renderBuffers.end())
+ {
+ readableBuffer = *it;
+ readableBuffer->Acquire();
+ }
+ else
+ {
+ // If no buffers were readable, check the cached frame
+ if (m_speed == 0.0 && m_bHasCachedFrame && !m_cachedFrame.empty())
+ cachedFrame = std::move(m_cachedFrame);
+ }
+}
+
+void CRPRenderManager::FreeVideoFrame(IRenderBuffer* readableBuffer,
+ std::vector<uint8_t> cachedFrame)
+{
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+
+ // Free resources
+ if (readableBuffer != nullptr)
+ {
+ readableBuffer->ReleaseMemory();
+ readableBuffer->Release();
+ }
+ if (!cachedFrame.empty())
+ {
+ m_cachedFrame = std::move(cachedFrame);
+ }
+}
+
+void CRPRenderManager::LoadVideoFrameAsync(const std::string& savestatePath)
+{
+ // Prune any finished loader threads
+ m_savestateThreads.erase(std::remove_if(m_savestateThreads.begin(), m_savestateThreads.end(),
+ [](std::future<void>& task) {
+ return task.wait_for(std::chrono::seconds(0)) ==
+ std::future_status::ready;
+ }),
+ m_savestateThreads.end());
+
+ // Load the video data from the savestate asynchronously
+ std::future<void> task = std::async(
+ std::launch::async, [this, savestatePath]() { LoadVideoFrameSync(savestatePath); });
+
+ m_savestateThreads.emplace_back(std::move(task));
+}
+
+void CRPRenderManager::LoadVideoFrameSync(const std::string& savestatePath)
+{
+ if (!XFILE::CFile::Exists(savestatePath))
+ {
+ CLog::Log(LOGERROR, "Failed to load savestate: doesn't exist at path {}",
+ CURL::GetRedacted(savestatePath));
+ return;
+ }
+
+ std::unique_ptr<ISavestate> savestate = CSavestateDatabase::AllocateSavestate();
+ CSavestateDatabase db;
+ if (!db.GetSavestate(savestatePath, *savestate))
+ return;
+
+ // Load video data
+ const AVPixelFormat format = savestate->GetPixelFormat();
+ const uint8_t* data = savestate->GetVideoData();
+ const size_t size = savestate->GetVideoSize();
+ const unsigned int width = savestate->GetVideoWidth();
+ const unsigned int height = savestate->GetVideoHeight();
+ const unsigned int rotationCCW = savestate->GetRotationDegCCW();
+
+ // Validate parameters
+ if (format == AV_PIX_FMT_NONE || data == nullptr || size == 0 || width == 0 || height == 0)
+ {
+ CLog::Log(LOGERROR, "Invalid video data: format {}, data {}, size {}, width {}, height {}",
+ static_cast<long int>(format), static_cast<const void*>(data), size, width, height);
+ return;
+ }
+
+ // Copy frame to render buffers
+ std::vector<IRenderBuffer*> renderBuffers;
+ for (auto* bufferPool : m_processInfo.GetBufferManager().GetBufferPools())
+ {
+ IRenderBuffer* newBuffer = bufferPool->GetBuffer(width, height);
+ if (newBuffer == nullptr)
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[RENDER]: Unable to get render buffer for savestate");
+ continue;
+ }
+
+ CopyFrame(newBuffer, format, data, size, width, height);
+ newBuffer->SetRotation(rotationCCW);
+
+ renderBuffers.emplace_back(newBuffer);
+ }
+
+ // Save render buffers
+ std::unique_lock<CCriticalSection> lock(m_bufferMutex);
+ m_savestateBuffers[savestatePath] = std::move(renderBuffers);
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/RPRenderManager.h b/xbmc/cores/RetroPlayer/rendering/RPRenderManager.h
new file mode 100644
index 0000000..bd1ebb5
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RPRenderManager.h
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2017-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 "IRenderManager.h"
+#include "RenderVideoSettings.h"
+#include "cores/RetroPlayer/guibridge/IRenderCallback.h"
+#include "threads/CriticalSection.h"
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+#include <atomic>
+#include <future>
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+struct SwsContext;
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGUIRenderTargetFactory;
+class CRenderContext;
+class CRenderSettings;
+class CRPBaseRenderer;
+class CRPProcessInfo;
+class IGUIRenderSettings;
+class IRenderBuffer;
+class IRenderBufferPool;
+class ISavestate;
+struct VideoStreamBuffer;
+
+/*!
+ * \brief Renders video frames provided by the game loop
+ *
+ * Generally, buffer pools are registered by the windowing subsystem. A buffer
+ * pool provides a software or hardware buffer to store the added frame. When
+ * RenderManager is created, it instantiates all registered buffer pools.
+ *
+ * When a frame arrives, it is copied into a buffer from each buffer pool with
+ * a visible renderer. For example, if a GLES and MMAL renderer are both
+ * visible in the GUI, then the frame will be copied into two buffers.
+ *
+ * When it is time to render the frame, the GUI control or window calls into
+ * this class through the IRenderManager interface. RenderManager selects an
+ * appropriate renderer to use to render the frame. The renderer is then
+ * given the buffer that came from its buffer pool.
+ *
+ * Special behavior is needed when the game is paused. As no new frames are
+ * delivered, a newly created renderer will stay black. For this scenario,
+ * when we detect a pause event, the frame is preemptively cached so that a
+ * newly created renderer will have something to display.
+ */
+class CRPRenderManager : public IRenderManager, public IRenderCallback
+{
+public:
+ CRPRenderManager(CRPProcessInfo& processInfo);
+ ~CRPRenderManager() override = default;
+
+ void Initialize();
+ void Deinitialize();
+
+ /*!
+ * \brief Access the factory for creating GUI render targets
+ */
+ CGUIRenderTargetFactory* GetGUIRenderTargetFactory() { return m_renderControlFactory.get(); }
+
+ // Stream properties, set upon configuration
+ AVPixelFormat GetPixelFormat() const { return m_format; }
+ unsigned int GetNominalWidth() const { return m_nominalWidth; }
+ unsigned int GetNominalHeight() const { return m_nominalHeight; }
+ unsigned int GetMaxWidth() const { return m_maxWidth; }
+ unsigned int GetMaxHeight() const { return m_maxHeight; }
+ float GetPixelAspectRatio() const { return m_pixelAspectRatio; }
+
+ // Functions called from game loop
+ bool Configure(AVPixelFormat format,
+ unsigned int nominalWidth,
+ unsigned int nominalHeight,
+ unsigned int maxWidth,
+ unsigned int maxHeight,
+ float pixelAspectRatio);
+ bool GetVideoBuffer(unsigned int width, unsigned int height, VideoStreamBuffer& buffer);
+ void AddFrame(const uint8_t* data,
+ size_t size,
+ unsigned int width,
+ unsigned int height,
+ unsigned int orientationDegCW);
+ void Flush();
+
+ // Functions called from the player
+ void SetSpeed(double speed);
+
+ // Functions called from render thread
+ void FrameMove();
+
+ // Implementation of IRenderManager
+ void RenderWindow(bool bClear, const RESOLUTION_INFO& coordsRes) override;
+ void RenderControl(bool bClear,
+ bool bUseAlpha,
+ const CRect& renderRegion,
+ const IGUIRenderSettings* renderSettings) override;
+ void ClearBackground() override;
+
+ // Implementation of IRenderCallback
+ bool SupportsRenderFeature(RENDERFEATURE feature) const override;
+ bool SupportsScalingMethod(SCALINGMETHOD method) const override;
+
+ // Savestate functions
+ void SaveThumbnail(const std::string& thumbnailPath);
+
+ // Savestate functions
+ void CacheVideoFrame(const std::string& savestatePath);
+ void SaveVideoFrame(const std::string& savestatePath, ISavestate& savestate);
+ void ClearVideoFrame(const std::string& savestatePath);
+
+private:
+ /*!
+ * \brief Get or create a renderer compatible with the given render settings
+ */
+ std::shared_ptr<CRPBaseRenderer> GetRendererForSettings(const IGUIRenderSettings* renderSettings);
+
+ /*!
+ * \brief Get or create a renderer for the given buffer pool and render settings
+ */
+ std::shared_ptr<CRPBaseRenderer> GetRendererForPool(IRenderBufferPool* bufferPool,
+ const CRenderSettings& renderSettings);
+
+ /*!
+ * \brief Render a frame using the given renderer
+ */
+ void RenderInternal(const std::shared_ptr<CRPBaseRenderer>& renderer,
+ IRenderBuffer* renderBuffer,
+ bool bClear,
+ uint32_t alpha);
+
+ /*!
+ * \brief Return true if we have a render buffer belonging to the specified pool
+ */
+ bool HasRenderBuffer(IRenderBufferPool* bufferPool);
+
+ /*!
+ * \brief Get a render buffer belonging to the specified pool
+ */
+ IRenderBuffer* GetRenderBuffer(IRenderBufferPool* bufferPool);
+
+ /*!
+ * \brief Get a render buffer containing pixels from the specified savestate
+ */
+ IRenderBuffer* GetRenderBufferForSavestate(const std::string& savestatePath,
+ const IRenderBufferPool* bufferPool);
+
+ /*!
+ * \brief Create a render buffer for the specified pool from a cached frame
+ */
+ void CreateRenderBuffer(IRenderBufferPool* bufferPool);
+
+ /*!
+ * \brief Create a render buffer and copy the cached data into it
+ *
+ * The cached frame is accessed by both the game and rendering threads,
+ * and therefore requires synchronization.
+ *
+ * However, assuming the memory copy is expensive, we must avoid holding
+ * the mutex during the copy.
+ *
+ * To allow for this, the function is permitted to invalidate its
+ * cachedFrame parameter, as long as it is restored upon exit. While the
+ * mutex is exited inside this function, cachedFrame is guaranteed to be
+ * empty.
+ *
+ * \param cachedFrame The cached frame
+ * \param width The width of the cached frame
+ * \param height The height of the cached frame
+ * \param bufferPool The buffer pool used to create the render buffer
+ * \param mutex The locked mutex, to be unlocked during memory copy
+ *
+ * \return The render buffer if one was created from the cached frame,
+ * otherwise nullptr
+ */
+ IRenderBuffer* CreateFromCache(std::vector<uint8_t>& cachedFrame,
+ unsigned int width,
+ unsigned int height,
+ IRenderBufferPool* bufferPool,
+ CCriticalSection& mutex);
+
+ /*!
+ * \brief Utility function to copy a frame and rescale pixels if necessary
+ */
+ void CopyFrame(IRenderBuffer* renderBuffer,
+ AVPixelFormat format,
+ const uint8_t* data,
+ size_t size,
+ unsigned int width,
+ unsigned int height);
+
+ CRenderVideoSettings GetEffectiveSettings(const IGUIRenderSettings* settings) const;
+
+ void CheckFlush();
+
+ void GetVideoFrame(IRenderBuffer*& readableBuffer, std::vector<uint8_t>& cachedFrame);
+ void FreeVideoFrame(IRenderBuffer* readableBuffer, std::vector<uint8_t> cachedFrame);
+ void LoadVideoFrameAsync(const std::string& savestatePath);
+ void LoadVideoFrameSync(const std::string& savestatePath);
+
+ // Construction parameters
+ CRPProcessInfo& m_processInfo;
+ CRenderContext& m_renderContext;
+
+ // Subsystems
+ std::shared_ptr<IGUIRenderSettings> m_renderSettings;
+ std::shared_ptr<CGUIRenderTargetFactory> m_renderControlFactory;
+
+ // Stream properties
+ AVPixelFormat m_format = AV_PIX_FMT_NONE;
+ unsigned int m_nominalWidth{0};
+ unsigned int m_nominalHeight{0};
+ unsigned int m_maxWidth = 0;
+ unsigned int m_maxHeight = 0;
+ float m_pixelAspectRatio{1.0f};
+
+ // Render resources
+ std::set<std::shared_ptr<CRPBaseRenderer>> m_renderers;
+ std::vector<IRenderBuffer*> m_pendingBuffers; // Only access from game thread
+ std::vector<IRenderBuffer*> m_renderBuffers;
+ std::map<AVPixelFormat, std::map<AVPixelFormat, SwsContext*>> m_scalers; // From -> to -> context
+ std::vector<uint8_t> m_cachedFrame;
+ unsigned int m_cachedWidth = 0;
+ unsigned int m_cachedHeight = 0;
+ unsigned int m_cachedRotationCCW{0};
+ std::map<std::string, std::vector<IRenderBuffer*>>
+ m_savestateBuffers; // Render buffers for savestates
+ std::vector<std::future<void>> m_savestateThreads;
+
+ // State parameters
+ enum class RENDER_STATE
+ {
+ UNCONFIGURED,
+ CONFIGURING,
+ CONFIGURED,
+ };
+ RENDER_STATE m_state = RENDER_STATE::UNCONFIGURED;
+ bool m_bHasCachedFrame = false; // Invariant: m_cachedFrame is empty if false
+ std::set<std::string> m_failedShaderPresets;
+ std::atomic<bool> m_bFlush = {false};
+
+ // Windowing state
+ bool m_bDisplayScaleSet = false;
+
+ // Playback parameters
+ std::atomic<double> m_speed = {1.0};
+
+ // Synchronization parameters
+ CCriticalSection m_stateMutex;
+ CCriticalSection m_bufferMutex;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/RenderContext.cpp b/xbmc/cores/RetroPlayer/rendering/RenderContext.cpp
new file mode 100644
index 0000000..9be9c74
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RenderContext.cpp
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2017-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 "RenderContext.h"
+
+#include "games/GameServices.h"
+#include "games/agents/GameAgentManager.h"
+#include "rendering/RenderSystem.h"
+#include "settings/DisplaySettings.h"
+#include "settings/MediaSettings.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include "system_gl.h"
+
+#if defined(HAS_GL)
+#include "rendering/gl/RenderSystemGL.h"
+#elif HAS_GLES >= 2
+#include "rendering/gles/RenderSystemGLES.h"
+#elif defined(TARGET_WINDOWS)
+#include "rendering/dx/RenderSystemDX.h"
+#endif
+
+using namespace KODI;
+using namespace RETRO;
+
+CRenderContext::CRenderContext(CRenderSystemBase* rendering,
+ CWinSystemBase* windowing,
+ CGraphicContext& graphicsContext,
+ CDisplaySettings& displaySettings,
+ CMediaSettings& mediaSettings,
+ GAME::CGameServices& gameServices,
+ CGUIComponent* guiComponent)
+ : m_rendering(rendering),
+ m_windowing(windowing),
+ m_graphicsContext(graphicsContext),
+ m_displaySettings(displaySettings),
+ m_mediaSettings(mediaSettings),
+ m_gameServices(gameServices),
+ m_guiComponent(guiComponent)
+{
+}
+
+void CRenderContext::SetViewPort(const CRect& viewPort)
+{
+ m_rendering->SetViewPort(viewPort);
+}
+
+void CRenderContext::GetViewPort(CRect& viewPort)
+{
+ m_rendering->GetViewPort(viewPort);
+}
+
+void CRenderContext::SetScissors(const CRect& rect)
+{
+ m_rendering->SetScissors(rect);
+}
+
+void CRenderContext::ApplyStateBlock()
+{
+ m_rendering->ApplyStateBlock();
+}
+
+bool CRenderContext::IsExtSupported(const char* extension)
+{
+ return m_rendering->IsExtSupported(extension);
+}
+
+#if defined(HAS_GL) || defined(HAS_GLES)
+namespace
+{
+
+#ifdef HAS_GL
+static ShaderMethodGL TranslateShaderMethodGL(GL_SHADER_METHOD method)
+{
+ switch (method)
+ {
+ case GL_SHADER_METHOD::DEFAULT:
+ return ShaderMethodGL::SM_DEFAULT;
+ case GL_SHADER_METHOD::TEXTURE:
+ return ShaderMethodGL::SM_TEXTURE;
+ default:
+ break;
+ }
+
+ return ShaderMethodGL::SM_DEFAULT;
+}
+#endif
+#ifdef HAS_GLES
+static ShaderMethodGLES TranslateShaderMethodGLES(GL_SHADER_METHOD method)
+{
+ switch (method)
+ {
+ case GL_SHADER_METHOD::DEFAULT:
+ return ShaderMethodGLES::SM_DEFAULT;
+ case GL_SHADER_METHOD::TEXTURE:
+ return ShaderMethodGLES::SM_TEXTURE;
+ case GL_SHADER_METHOD::TEXTURE_NOALPHA:
+ return ShaderMethodGLES::SM_TEXTURE_NOALPHA;
+ default:
+ break;
+ }
+
+ return ShaderMethodGLES::SM_DEFAULT;
+}
+#endif
+
+} // namespace
+#endif
+
+void CRenderContext::EnableGUIShader(GL_SHADER_METHOD method)
+{
+#if defined(HAS_GL)
+ CRenderSystemGL* rendering = dynamic_cast<CRenderSystemGL*>(m_rendering);
+ if (rendering != nullptr)
+ rendering->EnableShader(TranslateShaderMethodGL(method));
+#elif HAS_GLES >= 2
+ CRenderSystemGLES* renderingGLES = dynamic_cast<CRenderSystemGLES*>(m_rendering);
+ if (renderingGLES != nullptr)
+ renderingGLES->EnableGUIShader(TranslateShaderMethodGLES(method));
+#endif
+}
+
+void CRenderContext::DisableGUIShader()
+{
+#if defined(HAS_GL)
+ CRenderSystemGL* renderingGL = dynamic_cast<CRenderSystemGL*>(m_rendering);
+ if (renderingGL != nullptr)
+ renderingGL->DisableShader();
+#elif HAS_GLES >= 2
+ CRenderSystemGLES* renderingGLES = dynamic_cast<CRenderSystemGLES*>(m_rendering);
+ if (renderingGLES != nullptr)
+ renderingGLES->DisableGUIShader();
+#endif
+}
+
+int CRenderContext::GUIShaderGetPos()
+{
+#if defined(HAS_GL)
+ CRenderSystemGL* renderingGL = dynamic_cast<CRenderSystemGL*>(m_rendering);
+ if (renderingGL != nullptr)
+ return static_cast<int>(renderingGL->ShaderGetPos());
+#elif HAS_GLES >= 2
+ CRenderSystemGLES* renderingGLES = dynamic_cast<CRenderSystemGLES*>(m_rendering);
+ if (renderingGLES != nullptr)
+ return static_cast<int>(renderingGLES->GUIShaderGetPos());
+#endif
+
+ return -1;
+}
+
+int CRenderContext::GUIShaderGetCoord0()
+{
+#if defined(HAS_GL)
+ CRenderSystemGL* renderingGL = dynamic_cast<CRenderSystemGL*>(m_rendering);
+ if (renderingGL != nullptr)
+ return static_cast<int>(renderingGL->ShaderGetCoord0());
+#elif HAS_GLES >= 2
+ CRenderSystemGLES* renderingGLES = dynamic_cast<CRenderSystemGLES*>(m_rendering);
+ if (renderingGLES != nullptr)
+ return static_cast<int>(renderingGLES->GUIShaderGetCoord0());
+#endif
+
+ return -1;
+}
+
+int CRenderContext::GUIShaderGetUniCol()
+{
+#if defined(HAS_GL)
+ CRenderSystemGL* renderingGL = dynamic_cast<CRenderSystemGL*>(m_rendering);
+ if (renderingGL != nullptr)
+ return static_cast<int>(renderingGL->ShaderGetUniCol());
+#elif HAS_GLES >= 2
+ CRenderSystemGLES* renderingGLES = dynamic_cast<CRenderSystemGLES*>(m_rendering);
+ if (renderingGLES != nullptr)
+ return static_cast<int>(renderingGLES->GUIShaderGetUniCol());
+#endif
+
+ return -1;
+}
+
+CGUIShaderDX* CRenderContext::GetGUIShader()
+{
+#if defined(HAS_DX)
+ CRenderSystemDX* renderingDX = dynamic_cast<CRenderSystemDX*>(m_rendering);
+ if (renderingDX != nullptr)
+ return renderingDX->GetGUIShader();
+#endif
+
+ return nullptr;
+}
+
+bool CRenderContext::UseLimitedColor()
+{
+ return m_windowing->UseLimitedColor();
+}
+
+bool CRenderContext::DisplayHardwareScalingEnabled()
+{
+ return m_windowing->DisplayHardwareScalingEnabled();
+}
+
+void CRenderContext::UpdateDisplayHardwareScaling(const RESOLUTION_INFO& resInfo)
+{
+ return m_windowing->UpdateDisplayHardwareScaling(resInfo);
+}
+
+int CRenderContext::GetScreenWidth()
+{
+ return m_graphicsContext.GetWidth();
+}
+
+int CRenderContext::GetScreenHeight()
+{
+ return m_graphicsContext.GetHeight();
+}
+
+const CRect& CRenderContext::GetScissors()
+{
+ return m_graphicsContext.GetScissors();
+}
+
+CRect CRenderContext::GetViewWindow()
+{
+ return m_graphicsContext.GetViewWindow();
+}
+
+void CRenderContext::SetViewWindow(float left, float top, float right, float bottom)
+{
+ m_graphicsContext.SetViewWindow(left, top, right, bottom);
+}
+
+void CRenderContext::SetFullScreenVideo(bool bOnOff)
+{
+ m_graphicsContext.SetFullScreenVideo(bOnOff);
+}
+
+bool CRenderContext::IsFullScreenVideo()
+{
+ return m_graphicsContext.IsFullScreenVideo();
+}
+
+bool CRenderContext::IsCalibrating()
+{
+ return m_graphicsContext.IsCalibrating();
+}
+
+RESOLUTION CRenderContext::GetVideoResolution()
+{
+ return m_graphicsContext.GetVideoResolution();
+}
+
+void CRenderContext::Clear(UTILS::COLOR::Color color)
+{
+ m_graphicsContext.Clear(color);
+}
+
+RESOLUTION_INFO CRenderContext::GetResInfo()
+{
+ return m_graphicsContext.GetResInfo();
+}
+
+void CRenderContext::SetRenderingResolution(const RESOLUTION_INFO& res, bool needsScaling)
+{
+ m_graphicsContext.SetRenderingResolution(res, needsScaling);
+}
+
+UTILS::COLOR::Color CRenderContext::MergeAlpha(UTILS::COLOR::Color color)
+{
+ return m_graphicsContext.MergeAlpha(color);
+}
+
+void CRenderContext::SetTransform(const TransformMatrix& matrix, float scaleX, float scaleY)
+{
+ m_graphicsContext.SetTransform(matrix, scaleX, scaleY);
+}
+
+void CRenderContext::RemoveTransform()
+{
+ m_graphicsContext.RemoveTransform();
+}
+
+CRect CRenderContext::StereoCorrection(const CRect& rect)
+{
+ return m_graphicsContext.StereoCorrection(rect);
+}
+
+CCriticalSection& CRenderContext::GraphicsMutex()
+{
+ return m_graphicsContext;
+}
+
+RESOLUTION_INFO& CRenderContext::GetResolutionInfo(RESOLUTION resolution)
+{
+ return m_displaySettings.GetResolutionInfo(resolution);
+}
+
+::CGameSettings& CRenderContext::GetGameSettings()
+{
+ return m_mediaSettings.GetCurrentGameSettings();
+}
+
+::CGameSettings& CRenderContext::GetDefaultGameSettings()
+{
+ return m_mediaSettings.GetDefaultGameSettings();
+}
+
+void CRenderContext::StartAgentManager(GAME::GameClientPtr gameClient)
+{
+ m_gameServices.GameAgentManager().Start(std::move(gameClient));
+}
+
+void CRenderContext::StopAgentManager()
+{
+ m_gameServices.GameAgentManager().Stop();
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/RenderContext.h b/xbmc/cores/RetroPlayer/rendering/RenderContext.h
new file mode 100644
index 0000000..de663a4
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RenderContext.h
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017-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 "games/GameTypes.h"
+#include "utils/ColorUtils.h"
+#include "utils/Geometry.h"
+#include "windowing/Resolution.h"
+
+class CCriticalSection;
+class CDisplaySettings;
+class CGameSettings;
+class CGraphicContext;
+class CGUIComponent;
+class CGUIShaderDX;
+class CMediaSettings;
+class CRenderSystemBase;
+class CWinSystemBase;
+class TransformMatrix;
+
+enum class GL_SHADER_METHOD
+{
+ DEFAULT,
+ TEXTURE,
+ TEXTURE_NOALPHA,
+};
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameServices;
+}
+
+namespace RETRO
+{
+class CRenderContext
+{
+public:
+ CRenderContext(CRenderSystemBase* rendering,
+ CWinSystemBase* windowing,
+ CGraphicContext& graphicsContext,
+ CDisplaySettings& displaySettings,
+ CMediaSettings& mediaSettings,
+ GAME::CGameServices& gameServices,
+ CGUIComponent* guiComponent);
+
+ CRenderSystemBase* Rendering() { return m_rendering; }
+ CWinSystemBase* Windowing() { return m_windowing; }
+ CGraphicContext& GraphicsContext() { return m_graphicsContext; }
+ CGUIComponent* GUI() { return m_guiComponent; }
+
+ // Rendering functions
+ void SetViewPort(const CRect& viewPort);
+ void GetViewPort(CRect& viewPort);
+ void SetScissors(const CRect& rect);
+ void ApplyStateBlock();
+ bool IsExtSupported(const char* extension);
+
+ // OpenGL(ES) rendering functions
+ void EnableGUIShader(GL_SHADER_METHOD method);
+ void DisableGUIShader();
+ int GUIShaderGetPos();
+ int GUIShaderGetCoord0();
+ int GUIShaderGetUniCol();
+
+ // DirectX rendering functions
+ CGUIShaderDX* GetGUIShader();
+
+ // Windowing functions
+ bool UseLimitedColor();
+ bool DisplayHardwareScalingEnabled();
+ void UpdateDisplayHardwareScaling(const RESOLUTION_INFO& resInfo);
+
+ // Graphics functions
+ int GetScreenWidth();
+ int GetScreenHeight();
+ const CRect& GetScissors();
+ CRect GetViewWindow();
+ void SetViewWindow(float left, float top, float right, float bottom);
+ void SetFullScreenVideo(bool bOnOff);
+ bool IsFullScreenVideo();
+ bool IsCalibrating();
+ RESOLUTION GetVideoResolution();
+ void Clear(UTILS::COLOR::Color color);
+ RESOLUTION_INFO GetResInfo();
+ void SetRenderingResolution(const RESOLUTION_INFO& res, bool needsScaling);
+ UTILS::COLOR::Color MergeAlpha(UTILS::COLOR::Color color);
+ void SetTransform(const TransformMatrix& matrix, float scaleX, float scaleY);
+ void RemoveTransform();
+ CRect StereoCorrection(const CRect& rect);
+ CCriticalSection& GraphicsMutex();
+
+ // Display settings
+ RESOLUTION_INFO& GetResolutionInfo(RESOLUTION resolution);
+
+ // Media settings
+ ::CGameSettings& GetGameSettings();
+ ::CGameSettings& GetDefaultGameSettings();
+
+ // Agent functions
+ void StartAgentManager(GAME::GameClientPtr gameClient);
+ void StopAgentManager();
+
+private:
+ // Construction parameters
+ CRenderSystemBase* const m_rendering;
+ CWinSystemBase* const m_windowing;
+ CGraphicContext& m_graphicsContext;
+ CDisplaySettings& m_displaySettings;
+ CMediaSettings& m_mediaSettings;
+ GAME::CGameServices& m_gameServices;
+ CGUIComponent* const m_guiComponent;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/RenderSettings.cpp b/xbmc/cores/RetroPlayer/rendering/RenderSettings.cpp
new file mode 100644
index 0000000..3db43d2
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RenderSettings.cpp
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017-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 "RenderSettings.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+void CRenderSettings::Reset()
+{
+ m_videoSettings.Reset();
+}
+
+bool CRenderSettings::operator==(const CRenderSettings& rhs) const
+{
+ return m_videoSettings == rhs.m_videoSettings;
+}
+
+bool CRenderSettings::operator<(const CRenderSettings& rhs) const
+{
+ if (m_videoSettings < rhs.m_videoSettings)
+ return true;
+ if (m_videoSettings > rhs.m_videoSettings)
+ return false;
+
+ return false;
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/RenderSettings.h b/xbmc/cores/RetroPlayer/rendering/RenderSettings.h
new file mode 100644
index 0000000..d40cb42
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RenderSettings.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017-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 "RenderVideoSettings.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderSettings
+{
+public:
+ CRenderSettings() { Reset(); }
+
+ void Reset();
+
+ bool operator==(const CRenderSettings& rhs) const;
+ bool operator<(const CRenderSettings& rhs) const;
+
+ CRenderVideoSettings& VideoSettings() { return m_videoSettings; }
+ const CRenderVideoSettings& VideoSettings() const { return m_videoSettings; }
+
+private:
+ CRenderVideoSettings m_videoSettings;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/RenderTranslator.cpp b/xbmc/cores/RetroPlayer/rendering/RenderTranslator.cpp
new file mode 100644
index 0000000..1256af7
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RenderTranslator.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017-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 "RenderTranslator.h"
+
+#include <stdint.h>
+
+using namespace KODI;
+using namespace RETRO;
+
+const char* CRenderTranslator::TranslatePixelFormat(AVPixelFormat format)
+{
+ switch (format)
+ {
+ case AV_PIX_FMT_0RGB32:
+ return "0RGB32";
+ case AV_PIX_FMT_RGBA:
+ return "RGBA32";
+ case AV_PIX_FMT_RGB565:
+ return "RGB565";
+ case AV_PIX_FMT_RGB555:
+ return "RGB555";
+ default:
+ break;
+ }
+
+ return "unknown";
+}
+
+const char* CRenderTranslator::TranslateScalingMethod(SCALINGMETHOD scalingMethod)
+{
+ switch (scalingMethod)
+ {
+ case SCALINGMETHOD::NEAREST:
+ return "nearest";
+ case SCALINGMETHOD::LINEAR:
+ return "linear";
+ default:
+ break;
+ }
+
+ return "";
+}
+
+unsigned int CRenderTranslator::TranslateWidthToBytes(unsigned int width, AVPixelFormat format)
+{
+ unsigned int bpp = 0;
+
+ switch (format)
+ {
+ case AV_PIX_FMT_0RGB32:
+ case AV_PIX_FMT_RGBA:
+ {
+ bpp = sizeof(uint32_t);
+ break;
+ }
+ case AV_PIX_FMT_RGB555:
+ {
+ bpp = sizeof(uint16_t);
+ break;
+ }
+ case AV_PIX_FMT_RGB565:
+ {
+ bpp = sizeof(uint16_t);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return width * bpp;
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/RenderTranslator.h b/xbmc/cores/RetroPlayer/rendering/RenderTranslator.h
new file mode 100644
index 0000000..575ad81
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RenderTranslator.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+
+#include <libavutil/pixfmt.h>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderTranslator
+{
+public:
+ /*!
+ * \brief Translate a pixel format to a string suitable for logging
+ */
+ static const char* TranslatePixelFormat(AVPixelFormat format);
+
+ /*!
+ * \brief Translate a scaling method to a string suitable for logging
+ */
+ static const char* TranslateScalingMethod(SCALINGMETHOD scalingMethod);
+
+ /*!
+ * \brief Translate a width in pixels to a width in bytes
+ *
+ * \param width The width in pixels
+ * \param format The pixel format
+ *
+ * \return The width in bytes, or 0 if unknown
+ */
+ static unsigned int TranslateWidthToBytes(unsigned int width, AVPixelFormat format);
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/RenderUtils.cpp b/xbmc/cores/RetroPlayer/rendering/RenderUtils.cpp
new file mode 100644
index 0000000..f8769dd
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RenderUtils.cpp
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 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 "RenderUtils.h"
+
+#include "utils/MathUtils.h"
+
+#include <cmath>
+
+using namespace KODI;
+using namespace RETRO;
+
+void CRenderUtils::CalculateStretchMode(STRETCHMODE stretchMode,
+ unsigned int rotationDegCCW,
+ unsigned int sourceWidth,
+ unsigned int sourceHeight,
+ float screenWidth,
+ float screenHeight,
+ float& pixelRatio,
+ float& zoomAmount)
+{
+ const float sourceFrameRatio = static_cast<float>(sourceWidth) / static_cast<float>(sourceHeight);
+
+ switch (stretchMode)
+ {
+ case STRETCHMODE::Normal:
+ {
+ switch (rotationDegCCW)
+ {
+ case 90:
+ case 270:
+ {
+ pixelRatio = 1.0f / (sourceFrameRatio * sourceFrameRatio);
+ break;
+ }
+ default:
+ pixelRatio = 1.0f;
+ break;
+ }
+ zoomAmount = 1.0f;
+
+ break;
+ }
+ case STRETCHMODE::Stretch4x3:
+ {
+ // Stretch to 4:3 ratio
+ pixelRatio = (4.0f / 3.0f) / sourceFrameRatio;
+ zoomAmount = 1.0f;
+
+ break;
+ }
+ case STRETCHMODE::Fullscreen:
+ {
+ // Stretch to the limits of the screen
+ pixelRatio = (screenWidth / screenHeight) / sourceFrameRatio;
+ zoomAmount = 1.0f;
+
+ break;
+ }
+ case STRETCHMODE::Original:
+ {
+ switch (rotationDegCCW)
+ {
+ case 90:
+ case 270:
+ {
+ pixelRatio = 1.0f / (sourceFrameRatio * sourceFrameRatio);
+ break;
+ }
+ default:
+ pixelRatio = 1.0f;
+ break;
+ }
+
+ // Calculate the correct zoom amount
+ // First zoom to full width
+ float newHeight = screenWidth / pixelRatio;
+ if (newHeight > screenHeight)
+ {
+ // Zoom to full height
+ newHeight = screenHeight;
+ }
+
+ // Now work out the zoom amount so that no zoom is done
+ zoomAmount = sourceHeight / newHeight;
+
+ switch (rotationDegCCW)
+ {
+ case 90:
+ case 270:
+ {
+ zoomAmount *= sourceFrameRatio;
+ break;
+ }
+ default:
+ break;
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void CRenderUtils::CalcNormalRenderRect(const CRect& viewRect,
+ float outputFrameRatio,
+ float zoomAmount,
+ CRect& destRect)
+{
+ const float offsetX = viewRect.x1;
+ const float offsetY = viewRect.y1;
+ const float width = viewRect.Width();
+ const float height = viewRect.Height();
+
+ // If view window is empty, set empty destination
+ if (height == 0 || width == 0)
+ {
+ destRect.SetRect(0.0f, 0.0f, 0.0f, 0.0f);
+ return;
+ }
+
+ // Maximize the game width
+ float newWidth = width;
+ float newHeight = newWidth / outputFrameRatio;
+
+ if (newHeight > height)
+ {
+ newHeight = height;
+ newWidth = newHeight * outputFrameRatio;
+ }
+
+ // Scale the game up by set zoom amount
+ newWidth *= zoomAmount;
+ newHeight *= zoomAmount;
+
+ // If we are less than one pixel off use the complete screen instead
+ if (std::fabs(newWidth - width) < 1.0f)
+ newWidth = width;
+ if (std::fabs(newHeight - height) < 1.0f)
+ newHeight = height;
+
+ // Center the game
+ float posY = (height - newHeight) / 2;
+ float posX = (width - newWidth) / 2;
+
+ destRect.x1 = static_cast<float>(MathUtils::round_int(static_cast<double>(posX + offsetX)));
+ destRect.x2 = destRect.x1 + MathUtils::round_int(static_cast<double>(newWidth));
+ destRect.y1 = static_cast<float>(MathUtils::round_int(static_cast<double>(posY + offsetY)));
+ destRect.y2 = destRect.y1 + MathUtils::round_int(static_cast<double>(newHeight));
+}
+
+void CRenderUtils::ClipRect(const CRect& viewRect, CRect& sourceRect, CRect& destRect)
+{
+ const float offsetX = viewRect.x1;
+ const float offsetY = viewRect.y1;
+ const float width = viewRect.Width();
+ const float height = viewRect.Height();
+
+ CRect original(destRect);
+ destRect.Intersect(CRect(offsetX, offsetY, offsetX + width, offsetY + height));
+ if (destRect != original)
+ {
+ float scaleX = sourceRect.Width() / original.Width();
+ float scaleY = sourceRect.Height() / original.Height();
+ sourceRect.x1 += (destRect.x1 - original.x1) * scaleX;
+ sourceRect.y1 += (destRect.y1 - original.y1) * scaleY;
+ sourceRect.x2 += (destRect.x2 - original.x2) * scaleX;
+ sourceRect.y2 += (destRect.y2 - original.y2) * scaleY;
+ }
+}
+
+std::array<CPoint, 4> CRenderUtils::ReorderDrawPoints(const CRect& destRect,
+ unsigned int orientationDegCCW)
+{
+ std::array<CPoint, 4> rotatedDestCoords{};
+
+ switch (orientationDegCCW)
+ {
+ case 0:
+ {
+ rotatedDestCoords[0] = CPoint{destRect.x1, destRect.y1}; // Top left
+ rotatedDestCoords[1] = CPoint{destRect.x2, destRect.y1}; // Top right
+ rotatedDestCoords[2] = CPoint{destRect.x2, destRect.y2}; // Bottom right
+ rotatedDestCoords[3] = CPoint{destRect.x1, destRect.y2}; // Bottom left
+ break;
+ }
+ case 90:
+ {
+ rotatedDestCoords[0] = CPoint{destRect.x1, destRect.y2}; // Bottom left
+ rotatedDestCoords[1] = CPoint{destRect.x1, destRect.y1}; // Top left
+ rotatedDestCoords[2] = CPoint{destRect.x2, destRect.y1}; // Top right
+ rotatedDestCoords[3] = CPoint{destRect.x2, destRect.y2}; // Bottom right
+ break;
+ }
+ case 180:
+ {
+ rotatedDestCoords[0] = CPoint{destRect.x2, destRect.y2}; // Bottom right
+ rotatedDestCoords[1] = CPoint{destRect.x1, destRect.y2}; // Bottom left
+ rotatedDestCoords[2] = CPoint{destRect.x1, destRect.y1}; // Top left
+ rotatedDestCoords[3] = CPoint{destRect.x2, destRect.y1}; // Top right
+ break;
+ }
+ case 270:
+ {
+ rotatedDestCoords[0] = CPoint{destRect.x2, destRect.y1}; // Top right
+ rotatedDestCoords[1] = CPoint{destRect.x2, destRect.y2}; // Bottom right
+ rotatedDestCoords[2] = CPoint{destRect.x1, destRect.y2}; // Bottom left
+ rotatedDestCoords[3] = CPoint{destRect.x1, destRect.y1}; // Top left
+ break;
+ }
+ default:
+ break;
+ }
+
+ return rotatedDestCoords;
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/RenderUtils.h b/xbmc/cores/RetroPlayer/rendering/RenderUtils.h
new file mode 100644
index 0000000..0976c3e
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RenderUtils.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 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/GameSettings.h"
+#include "utils/Geometry.h"
+
+#include <array>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderUtils
+{
+public:
+ static void CalculateStretchMode(STRETCHMODE stretchMode,
+ unsigned int rotationDegCCW,
+ unsigned int sourceWidth,
+ unsigned int sourceHeight,
+ float screenWidth,
+ float screenHeight,
+ float& pixelRatio,
+ float& zoomAmount);
+
+ static void CalcNormalRenderRect(const CRect& viewRect,
+ float outputFrameRatio,
+ float zoomAmount,
+ CRect& destRect);
+
+ static void ClipRect(const CRect& viewRect, CRect& sourceRect, CRect& destRect);
+
+ static std::array<CPoint, 4> ReorderDrawPoints(const CRect& destRect,
+ unsigned int orientationDegCCW);
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/RenderVideoSettings.cpp b/xbmc/cores/RetroPlayer/rendering/RenderVideoSettings.cpp
new file mode 100644
index 0000000..53ee3f0
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RenderVideoSettings.cpp
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017-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 "RenderVideoSettings.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+#define VIDEO_FILTER_NEAREST "nearest"
+#define VIDEO_FILTER_LINEAR "linear"
+
+void CRenderVideoSettings::Reset()
+{
+ m_scalingMethod = SCALINGMETHOD::AUTO;
+ m_stretchMode = STRETCHMODE::Normal;
+ m_rotationDegCCW = 0;
+ m_pixelPath.clear();
+}
+
+bool CRenderVideoSettings::operator==(const CRenderVideoSettings& rhs) const
+{
+ return m_scalingMethod == rhs.m_scalingMethod && m_stretchMode == rhs.m_stretchMode &&
+ m_rotationDegCCW == rhs.m_rotationDegCCW && m_pixelPath == rhs.m_pixelPath;
+}
+
+bool CRenderVideoSettings::operator<(const CRenderVideoSettings& rhs) const
+{
+ if (m_scalingMethod < rhs.m_scalingMethod)
+ return true;
+ if (m_scalingMethod > rhs.m_scalingMethod)
+ return false;
+
+ if (m_stretchMode < rhs.m_stretchMode)
+ return true;
+ if (m_stretchMode > rhs.m_stretchMode)
+ return false;
+
+ if (m_rotationDegCCW < rhs.m_rotationDegCCW)
+ return true;
+ if (m_rotationDegCCW > rhs.m_rotationDegCCW)
+ return false;
+
+ if (m_pixelPath < rhs.m_pixelPath)
+ return true;
+ if (m_pixelPath > rhs.m_pixelPath)
+ return false;
+
+ return false;
+}
+
+std::string CRenderVideoSettings::GetVideoFilter() const
+{
+ switch (m_scalingMethod)
+ {
+ case SCALINGMETHOD::NEAREST:
+ return VIDEO_FILTER_NEAREST;
+ case SCALINGMETHOD::LINEAR:
+ return VIDEO_FILTER_LINEAR;
+ default:
+ break;
+ }
+
+ return "";
+}
+
+void CRenderVideoSettings::SetVideoFilter(const std::string& videoFilter)
+{
+ if (videoFilter == VIDEO_FILTER_NEAREST)
+ {
+ m_scalingMethod = SCALINGMETHOD::NEAREST;
+ }
+ else if (videoFilter == VIDEO_FILTER_LINEAR)
+ {
+ m_scalingMethod = SCALINGMETHOD::LINEAR;
+ }
+ else
+ {
+ m_scalingMethod = SCALINGMETHOD::AUTO;
+ }
+}
+void CRenderVideoSettings::ResetPixels()
+{
+ m_pixelPath.clear();
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/RenderVideoSettings.h b/xbmc/cores/RetroPlayer/rendering/RenderVideoSettings.h
new file mode 100644
index 0000000..1d14270
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/RenderVideoSettings.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace RETRO
+{
+/*!
+ * \brief Video settings provided by the rendering system
+ */
+class CRenderVideoSettings
+{
+public:
+ CRenderVideoSettings() { Reset(); }
+
+ void Reset();
+
+ bool operator==(const CRenderVideoSettings& rhs) const;
+ bool operator!=(const CRenderVideoSettings& rhs) const { return !(*this == rhs); }
+ bool operator<(const CRenderVideoSettings& rhs) const;
+ bool operator>(const CRenderVideoSettings& rhs) const { return !(*this == rhs || *this < rhs); }
+
+ /*!
+ * \brief Get a string representation of the video filter parameters
+ */
+ std::string GetVideoFilter() const;
+ void SetVideoFilter(const std::string& videoFilter);
+
+ SCALINGMETHOD GetScalingMethod() const { return m_scalingMethod; }
+ void SetScalingMethod(SCALINGMETHOD method) { m_scalingMethod = method; }
+
+ STRETCHMODE GetRenderStretchMode() const { return m_stretchMode; }
+ void SetRenderStretchMode(STRETCHMODE mode) { m_stretchMode = mode; }
+
+ unsigned int GetRenderRotation() const { return m_rotationDegCCW; }
+ void SetRenderRotation(unsigned int rotationDegCCW) { m_rotationDegCCW = rotationDegCCW; }
+
+ std::string GetPixels() const { return m_pixelPath; }
+ void SetPixels(const std::string& pixelPath) { m_pixelPath = pixelPath; }
+ void ResetPixels();
+
+private:
+ SCALINGMETHOD m_scalingMethod;
+ STRETCHMODE m_stretchMode;
+ unsigned int m_rotationDegCCW;
+ std::string m_pixelPath;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/CMakeLists.txt b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/CMakeLists.txt
new file mode 100644
index 0000000..6f9d908
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/CMakeLists.txt
@@ -0,0 +1,26 @@
+set(SOURCES RPBaseRenderer.cpp
+ RPRendererGuiTexture.cpp)
+set(HEADERS RPBaseRenderer.h
+ RPRendererGuiTexture.h)
+
+if(CORE_SYSTEM_NAME STREQUAL windows)
+ list(APPEND SOURCES RPWinRenderer.cpp)
+ list(APPEND HEADERS RPWinRenderer.h)
+endif()
+
+if(OPENGL_FOUND OR OPENGLES_FOUND)
+ list(APPEND SOURCES RPRendererOpenGLES.cpp)
+ list(APPEND HEADERS RPRendererOpenGLES.h)
+endif()
+
+if(OPENGL_FOUND)
+ list(APPEND SOURCES RPRendererOpenGL.cpp)
+ list(APPEND HEADERS RPRendererOpenGL.h)
+endif()
+
+if(("gbm" IN_LIST CORE_PLATFORM_NAME_LC OR "wayland" IN_LIST CORE_PLATFORM_NAME_LC) AND EGL_FOUND)
+ list(APPEND SOURCES RPRendererDMA.cpp)
+ list(APPEND HEADERS RPRendererDMA.h)
+endif()
+
+core_add_library(rp-videorenderers)
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPBaseRenderer.cpp b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPBaseRenderer.cpp
new file mode 100644
index 0000000..9823475
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPBaseRenderer.cpp
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2017-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 "RPBaseRenderer.h"
+
+#include "cores/RetroPlayer/buffers/IRenderBuffer.h"
+#include "cores/RetroPlayer/buffers/IRenderBufferPool.h"
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "cores/RetroPlayer/rendering/RenderUtils.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+// Consider renderer visible until this many frames have passed without rendering
+#define VISIBLE_DURATION_FRAME_COUNT 1
+
+CRPBaseRenderer::CRPBaseRenderer(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool)
+ : m_context(context), m_bufferPool(std::move(bufferPool)), m_renderSettings(renderSettings)
+{
+ m_bufferPool->RegisterRenderer(this);
+}
+
+CRPBaseRenderer::~CRPBaseRenderer()
+{
+ SetBuffer(nullptr);
+
+ m_bufferPool->UnregisterRenderer(this);
+}
+
+bool CRPBaseRenderer::IsCompatible(const CRenderVideoSettings& settings) const
+{
+ return m_bufferPool->IsCompatible(settings);
+}
+
+bool CRPBaseRenderer::Configure(AVPixelFormat format)
+{
+ m_format = format;
+
+ if (!m_bufferPool->IsConfigured())
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[RENDER]: Configuring buffer pool");
+
+ if (!m_bufferPool->Configure(format))
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[RENDER]: Failed to configure buffer pool");
+ return false;
+ }
+ }
+
+ if (ConfigureInternal())
+ m_bConfigured = true;
+
+ return m_bConfigured;
+}
+
+void CRPBaseRenderer::FrameMove()
+{
+ m_renderFrameCount++;
+}
+
+bool CRPBaseRenderer::IsVisible() const
+{
+ return m_renderFrameCount <= m_lastRender + VISIBLE_DURATION_FRAME_COUNT;
+}
+
+IRenderBuffer* CRPBaseRenderer::GetRenderBuffer() const
+{
+ if (m_renderBuffer != nullptr)
+ {
+ m_renderBuffer->Acquire();
+ return m_renderBuffer;
+ }
+
+ return nullptr;
+}
+
+void CRPBaseRenderer::SetBuffer(IRenderBuffer* buffer)
+{
+ if (m_renderBuffer != buffer)
+ {
+ if (m_renderBuffer != nullptr)
+ m_renderBuffer->Release();
+
+ m_renderBuffer = buffer;
+
+ if (m_renderBuffer != nullptr)
+ m_renderBuffer->Acquire();
+ }
+}
+
+void CRPBaseRenderer::RenderFrame(bool clear, uint8_t alpha)
+{
+ m_lastRender = m_renderFrameCount;
+
+ if (!m_bConfigured || m_renderBuffer == nullptr)
+ return;
+
+ ManageRenderArea(*m_renderBuffer);
+
+ RenderInternal(clear, alpha);
+ PostRender();
+
+ m_renderBuffer->SetRendered(true);
+}
+
+void CRPBaseRenderer::Flush()
+{
+ SetBuffer(nullptr);
+ FlushInternal();
+}
+
+void CRPBaseRenderer::SetScalingMethod(SCALINGMETHOD method)
+{
+ m_renderSettings.VideoSettings().SetScalingMethod(method);
+}
+
+void CRPBaseRenderer::SetStretchMode(STRETCHMODE stretchMode)
+{
+ m_renderSettings.VideoSettings().SetRenderStretchMode(stretchMode);
+}
+
+void CRPBaseRenderer::SetRenderRotation(unsigned int rotationDegCCW)
+{
+ m_renderSettings.VideoSettings().SetRenderRotation(rotationDegCCW);
+}
+
+void CRPBaseRenderer::SetPixels(const std::string& pixelPath)
+{
+ m_renderSettings.VideoSettings().SetPixels(pixelPath);
+}
+
+void CRPBaseRenderer::ManageRenderArea(const IRenderBuffer& renderBuffer)
+{
+ // Get texture parameters
+ const unsigned int sourceWidth = renderBuffer.GetWidth();
+ const unsigned int sourceHeight = renderBuffer.GetHeight();
+ const unsigned int sourceRotationDegCCW = renderBuffer.GetRotation();
+ const float sourceAspectRatio =
+ static_cast<float>(sourceWidth) / static_cast<float>(sourceHeight);
+
+ const SCALINGMETHOD scaleMode = m_renderSettings.VideoSettings().GetScalingMethod();
+ const STRETCHMODE stretchMode = m_renderSettings.VideoSettings().GetRenderStretchMode();
+ const unsigned int rotationDegCCW =
+ (sourceRotationDegCCW + m_renderSettings.VideoSettings().GetRenderRotation()) % 360;
+
+ // Get screen parameters
+ float screenWidth;
+ float screenHeight;
+ //! @Todo screenPixelRatio unused - Possibly due to display integer scaling according to Garbear
+ float screenPixelRatio;
+
+ if (scaleMode == SCALINGMETHOD::NEAREST && stretchMode == STRETCHMODE::Original &&
+ m_context.DisplayHardwareScalingEnabled())
+ {
+ screenWidth = sourceWidth;
+ screenHeight = sourceHeight;
+ screenPixelRatio = 1.0;
+ }
+ else
+ {
+ GetScreenDimensions(screenWidth, screenHeight, screenPixelRatio);
+ }
+
+ // Entire target rendering area for the video (including black bars)
+ const CRect viewRect = m_context.GetViewWindow();
+
+ // Calculate pixel ratio and zoom amount
+ float pixelRatio = 1.0f;
+ float zoomAmount = 1.0f;
+ CRenderUtils::CalculateStretchMode(stretchMode, rotationDegCCW, sourceWidth, sourceHeight,
+ screenWidth, screenHeight, pixelRatio, zoomAmount);
+
+ // Calculate destination dimensions
+ CRect destRect;
+ CRenderUtils::CalcNormalRenderRect(viewRect, sourceAspectRatio * pixelRatio, zoomAmount,
+ destRect);
+
+ m_sourceRect.x1 = 0.0f;
+ m_sourceRect.y1 = 0.0f;
+ m_sourceRect.x2 = static_cast<float>(sourceWidth);
+ m_sourceRect.y2 = static_cast<float>(sourceHeight);
+
+ // Clip as needed
+ if (!(m_context.IsFullScreenVideo() || m_context.IsCalibrating()))
+ CRenderUtils::ClipRect(viewRect, m_sourceRect, destRect);
+
+ // Adapt the drawing rect points if we have to rotate
+ m_rotatedDestCoords = CRenderUtils::ReorderDrawPoints(destRect, rotationDegCCW);
+}
+
+void CRPBaseRenderer::MarkDirty()
+{
+ // CServiceBroker::GetGUI()->GetWindowManager().MarkDirty(m_dimensions); //! @todo
+}
+
+void CRPBaseRenderer::PreRender(bool clear)
+{
+ if (!m_bConfigured)
+ return;
+
+ // Clear screen
+ if (clear)
+ m_context.Clear(m_context.UseLimitedColor() ? UTILS::COLOR::LIMITED_BLACK
+ : UTILS::COLOR::BLACK);
+}
+
+void CRPBaseRenderer::PostRender()
+{
+ m_context.ApplyStateBlock();
+}
+
+void CRPBaseRenderer::GetScreenDimensions(float& screenWidth,
+ float& screenHeight,
+ float& screenPixelRatio)
+{
+ // Get our calibrated full screen resolution
+ RESOLUTION_INFO info = m_context.GetResInfo();
+
+ screenWidth = static_cast<float>(info.Overscan.right - info.Overscan.left);
+ screenHeight = static_cast<float>(info.Overscan.bottom - info.Overscan.top);
+
+ // Splitres scaling factor
+ float xscale = static_cast<float>(info.iScreenWidth) / static_cast<float>(info.iWidth);
+ float yscale = static_cast<float>(info.iScreenHeight) / static_cast<float>(info.iHeight);
+
+ screenWidth *= xscale;
+ screenHeight *= yscale;
+
+ screenPixelRatio = info.fPixelRatio;
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPBaseRenderer.h b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPBaseRenderer.h
new file mode 100644
index 0000000..6e4d030
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPBaseRenderer.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+#include "cores/RetroPlayer/rendering/RenderSettings.h"
+#include "utils/Geometry.h"
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+#include <array>
+#include <memory>
+#include <stdint.h>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderContext;
+class IRenderBuffer;
+class IRenderBufferPool;
+
+class CRPBaseRenderer
+{
+public:
+ CRPBaseRenderer(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool);
+ virtual ~CRPBaseRenderer();
+
+ /*!
+ * \brief Get the buffer pool used by this renderer
+ */
+ IRenderBufferPool* GetBufferPool() { return m_bufferPool.get(); }
+
+ // Player functions
+ bool Configure(AVPixelFormat format);
+ void FrameMove();
+ /*!
+ * \brief Performs whatever necessary before rendering the frame
+ */
+ void PreRender(bool clear);
+ void SetBuffer(IRenderBuffer* buffer);
+ void RenderFrame(bool clear, uint8_t alpha);
+
+ // Feature support
+ virtual bool Supports(RENDERFEATURE feature) const = 0;
+ bool IsCompatible(const CRenderVideoSettings& settings) const;
+ virtual SCALINGMETHOD GetDefaultScalingMethod() const = 0;
+
+ // Public renderer interface
+ virtual void Flush();
+
+ // Get render settings
+ const CRenderSettings& GetRenderSettings() const { return m_renderSettings; }
+
+ // Set render settings
+ void SetScalingMethod(SCALINGMETHOD method);
+ void SetStretchMode(STRETCHMODE stretchMode);
+ void SetRenderRotation(unsigned int rotationDegCCW);
+ void SetPixels(const std::string& pixelPath);
+
+ // Rendering properties
+ bool IsVisible() const;
+ IRenderBuffer* GetRenderBuffer() const;
+
+protected:
+ // Protected renderer interface
+ virtual bool ConfigureInternal() { return true; }
+ virtual void RenderInternal(bool clear, uint8_t alpha) = 0;
+ virtual void FlushInternal() {}
+
+ // Construction parameters
+ CRenderContext& m_context;
+ std::shared_ptr<IRenderBufferPool> m_bufferPool;
+
+ // Stream properties
+ bool m_bConfigured = false;
+ AVPixelFormat m_format = AV_PIX_FMT_NONE;
+
+ // Rendering properties
+ CRenderSettings m_renderSettings;
+ IRenderBuffer* m_renderBuffer = nullptr;
+
+ // Geometry properties
+ CRect m_sourceRect;
+ std::array<CPoint, 4> m_rotatedDestCoords{};
+
+private:
+ /*!
+ * \brief Calculate driven dimensions
+ */
+ virtual void ManageRenderArea(const IRenderBuffer& renderBuffer);
+
+ /*!
+ * \brief Performs whatever necessary after a frame has been rendered
+ */
+ void PostRender();
+
+ void MarkDirty();
+
+ // Utility functions
+ void GetScreenDimensions(float& screenWidth, float& screenHeight, float& screenPixelRatio);
+
+ uint64_t m_renderFrameCount = 0;
+ uint64_t m_lastRender = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.cpp b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.cpp
new file mode 100644
index 0000000..97a9eaa
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.cpp
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017-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 "RPRendererDMA.h"
+
+#include "cores/RetroPlayer/buffers/RenderBufferDMA.h"
+#include "cores/RetroPlayer/buffers/RenderBufferPoolDMA.h"
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "cores/RetroPlayer/rendering/RenderVideoSettings.h"
+#include "utils/BufferObjectFactory.h"
+#include "utils/GLUtils.h"
+
+#include <cassert>
+#include <cstddef>
+
+using namespace KODI;
+using namespace RETRO;
+
+std::string CRendererFactoryDMA::RenderSystemName() const
+{
+ return "DMA";
+}
+
+CRPBaseRenderer* CRendererFactoryDMA::CreateRenderer(const CRenderSettings& settings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool)
+{
+ return new CRPRendererDMA(settings, context, std::move(bufferPool));
+}
+
+RenderBufferPoolVector CRendererFactoryDMA::CreateBufferPools(CRenderContext& context)
+{
+ if (!CBufferObjectFactory::CreateBufferObject(false))
+ return {};
+
+ return {std::make_shared<CRenderBufferPoolDMA>(context)};
+}
+
+CRPRendererDMA::CRPRendererDMA(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool)
+ : CRPRendererOpenGLES(renderSettings, context, std::move(bufferPool))
+{
+}
+
+void CRPRendererDMA::Render(uint8_t alpha)
+{
+ auto renderBuffer = static_cast<CRenderBufferDMA*>(m_renderBuffer);
+ assert(renderBuffer != nullptr);
+
+ CRect rect = m_sourceRect;
+
+ rect.x1 /= renderBuffer->GetWidth();
+ rect.x2 /= renderBuffer->GetWidth();
+ rect.y1 /= renderBuffer->GetHeight();
+ rect.y2 /= renderBuffer->GetHeight();
+
+ const uint32_t color = (alpha << 24) | 0xFFFFFF;
+
+ glBindTexture(m_textureTarget, renderBuffer->TextureID());
+
+ GLint filter = GL_NEAREST;
+ if (GetRenderSettings().VideoSettings().GetScalingMethod() == SCALINGMETHOD::LINEAR)
+ filter = GL_LINEAR;
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, filter);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, filter);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ m_context.EnableGUIShader(GL_SHADER_METHOD::TEXTURE);
+
+ GLubyte colour[4];
+ GLubyte idx[4] = {0, 1, 3, 2}; // Determines order of triangle strip
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ } vertex[4];
+
+ GLint vertLoc = m_context.GUIShaderGetPos();
+ GLint loc = m_context.GUIShaderGetCoord0();
+ GLint uniColLoc = m_context.GUIShaderGetUniCol();
+
+ // Setup color values
+ colour[0] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::R, color);
+ colour[1] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::G, color);
+ colour[2] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::B, color);
+ colour[3] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::A, color);
+
+ for (unsigned int i = 0; i < 4; i++)
+ {
+ // Setup vertex position values
+ vertex[i].x = m_rotatedDestCoords[i].x;
+ vertex[i].y = m_rotatedDestCoords[i].y;
+ vertex[i].z = 0.0f;
+ }
+
+ // Setup texture coordinates
+ vertex[0].u1 = vertex[3].u1 = rect.x1;
+ vertex[0].v1 = vertex[1].v1 = rect.y1;
+ vertex[1].u1 = vertex[2].u1 = rect.x2;
+ vertex[2].v1 = vertex[3].v1 = rect.y2;
+
+ glBindBuffer(GL_ARRAY_BUFFER, m_mainVertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex) * 4, &vertex[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(vertLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLuint*>(offsetof(PackedVertex, x)));
+ glVertexAttribPointer(loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLuint*>(offsetof(PackedVertex, u1)));
+
+ glEnableVertexAttribArray(vertLoc);
+ glEnableVertexAttribArray(loc);
+
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_mainIndexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte) * 4, idx, GL_STATIC_DRAW);
+
+ glUniform4f(uniColLoc, (colour[0] / 255.0f), (colour[1] / 255.0f), (colour[2] / 255.0f),
+ (colour[3] / 255.0f));
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+
+ glDisableVertexAttribArray(vertLoc);
+ glDisableVertexAttribArray(loc);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+
+ m_context.DisableGUIShader();
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h
new file mode 100644
index 0000000..38d7bae
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017-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 "RPRendererOpenGLES.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRendererFactoryDMA : public IRendererFactory
+{
+public:
+ ~CRendererFactoryDMA() override = default;
+
+ // implementation of IRendererFactory
+ std::string RenderSystemName() const override;
+ CRPBaseRenderer* CreateRenderer(const CRenderSettings& settings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool) override;
+ RenderBufferPoolVector CreateBufferPools(CRenderContext& context) override;
+};
+
+/**
+ * @brief Special CRPBaseRenderer implementation to handle Direct Memory
+ * Access (DMA) buffer types. For specific use with
+ * CRenderBufferPoolDMA and CRenderBufferDMA. A windowing system
+ * must register use of this renderer and register at least one
+ * CBufferObject types.
+ *
+ */
+class CRPRendererDMA : public CRPRendererOpenGLES
+{
+public:
+ CRPRendererDMA(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool);
+ ~CRPRendererDMA() override = default;
+
+protected:
+ // implementation of CRPRendererOpenGLES
+ void Render(uint8_t alpha) override;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererGuiTexture.cpp b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererGuiTexture.cpp
new file mode 100644
index 0000000..f38e240
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererGuiTexture.cpp
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2017-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 "RPRendererGuiTexture.h"
+
+#include "cores/RetroPlayer/buffers/video/RenderBufferGuiTexture.h"
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "cores/RetroPlayer/rendering/RenderVideoSettings.h"
+
+#if defined(HAS_DX)
+#include "guilib/GUIShaderDX.h"
+#include "guilib/TextureDX.h"
+
+#include <DirectXMath.h>
+using namespace DirectX;
+#endif
+
+#if defined(HAS_GL) || defined(HAS_GLES)
+#include "utils/GLUtils.h"
+
+#include "system_gl.h"
+#endif
+
+#include <cstddef>
+
+using namespace KODI;
+using namespace RETRO;
+
+// --- CRendererFactoryGuiTexture ----------------------------------------------
+
+std::string CRendererFactoryGuiTexture::RenderSystemName() const
+{
+ return "GUITexture";
+}
+
+CRPBaseRenderer* CRendererFactoryGuiTexture::CreateRenderer(
+ const CRenderSettings& settings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool)
+{
+ return new CRPRendererGuiTexture(settings, context, std::move(bufferPool));
+}
+
+RenderBufferPoolVector CRendererFactoryGuiTexture::CreateBufferPools(CRenderContext& context)
+{
+ return
+ {
+#if !defined(HAS_DX)
+ std::make_shared<CRenderBufferPoolGuiTexture>(SCALINGMETHOD::NEAREST),
+#endif
+ std::make_shared<CRenderBufferPoolGuiTexture>(SCALINGMETHOD::LINEAR),
+ };
+}
+
+// --- CRenderBufferPoolGuiTexture -----------------------------------------------
+
+CRenderBufferPoolGuiTexture::CRenderBufferPoolGuiTexture(SCALINGMETHOD scalingMethod)
+ : m_scalingMethod(scalingMethod)
+{
+}
+
+bool CRenderBufferPoolGuiTexture::IsCompatible(const CRenderVideoSettings& renderSettings) const
+{
+ return renderSettings.GetScalingMethod() == m_scalingMethod;
+}
+
+IRenderBuffer* CRenderBufferPoolGuiTexture::CreateRenderBuffer(void* header /* = nullptr */)
+{
+ return new CRenderBufferGuiTexture(m_scalingMethod);
+}
+
+// --- CRPRendererGuiTexture -----------------------------------------------------
+
+CRPRendererGuiTexture::CRPRendererGuiTexture(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool)
+ : CRPBaseRenderer(renderSettings, context, std::move(bufferPool))
+{
+}
+
+bool CRPRendererGuiTexture::Supports(RENDERFEATURE feature) const
+{
+ return feature == RENDERFEATURE::STRETCH || feature == RENDERFEATURE::ZOOM ||
+ feature == RENDERFEATURE::PIXEL_RATIO || feature == RENDERFEATURE::ROTATION;
+}
+
+void CRPRendererGuiTexture::RenderInternal(bool clear, uint8_t alpha)
+{
+ CRenderBufferGuiTexture* renderBuffer = static_cast<CRenderBufferGuiTexture*>(m_renderBuffer);
+
+ CRect rect = m_sourceRect;
+
+ rect.x1 /= renderBuffer->GetWidth();
+ rect.x2 /= renderBuffer->GetWidth();
+ rect.y1 /= renderBuffer->GetHeight();
+ rect.y2 /= renderBuffer->GetHeight();
+
+ float u1 = rect.x1;
+ float u2 = rect.x2;
+ float v1 = rect.y1;
+ float v2 = rect.y2;
+
+ const uint32_t color = (alpha << 24) | 0xFFFFFF;
+
+#if defined(HAS_DX)
+
+ Vertex vertex[5];
+ for (int i = 0; i < 4; i++)
+ {
+ vertex[i].pos = XMFLOAT3(m_rotatedDestCoords[i].x, m_rotatedDestCoords[i].y, 0);
+ CD3DHelper::XMStoreColor(&vertex[i].color, color);
+ vertex[i].texCoord = XMFLOAT2(0.0f, 0.0f);
+ vertex[i].texCoord2 = XMFLOAT2(0.0f, 0.0f);
+ }
+
+ (void)u1;
+ (void)v1;
+ vertex[1].texCoord.x = vertex[2].texCoord.x = u2;
+ vertex[2].texCoord.y = vertex[3].texCoord.y = v2;
+
+ vertex[4] = vertex[0]; // Not used when renderBuffer != nullptr
+
+ CGUIShaderDX* pGUIShader = m_context.GetGUIShader();
+ if (pGUIShader != nullptr)
+ {
+ pGUIShader->Begin(SHADER_METHOD_RENDER_TEXTURE_BLEND);
+
+ // Set state to render the image
+ auto dxTexture = static_cast<CDXTexture*>(renderBuffer->GetTexture());
+ ID3D11ShaderResourceView* shaderRes = dxTexture->GetShaderResource();
+ pGUIShader->SetShaderViews(1, &shaderRes);
+ pGUIShader->DrawQuad(vertex[0], vertex[1], vertex[2], vertex[3]);
+ }
+
+#elif defined(HAS_GL)
+
+ renderBuffer->BindToUnit(0);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glEnable(GL_BLEND);
+
+ m_context.EnableGUIShader(GL_SHADER_METHOD::TEXTURE);
+
+ GLubyte colour[4];
+ GLubyte idx[4] = {0, 1, 3, 2}; // Determines order of the vertices
+ GLuint vertexVBO;
+ GLuint indexVBO;
+
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ } vertex[4];
+
+ // Setup vertex position values
+ vertex[0].x = m_rotatedDestCoords[0].x;
+ vertex[0].y = m_rotatedDestCoords[0].y;
+ vertex[0].z = 0;
+ vertex[0].u1 = u1;
+ vertex[0].v1 = v1;
+
+ vertex[1].x = m_rotatedDestCoords[1].x;
+ vertex[1].y = m_rotatedDestCoords[1].y;
+ vertex[1].z = 0;
+ vertex[1].u1 = u2;
+ vertex[1].v1 = v1;
+
+ vertex[2].x = m_rotatedDestCoords[2].x;
+ vertex[2].y = m_rotatedDestCoords[2].y;
+ vertex[2].z = 0;
+ vertex[2].u1 = u2;
+ vertex[2].v1 = v2;
+
+ vertex[3].x = m_rotatedDestCoords[3].x;
+ vertex[3].y = m_rotatedDestCoords[3].y;
+ vertex[3].z = 0;
+ vertex[3].u1 = u1;
+ vertex[3].v1 = v2;
+
+ GLint posLoc = m_context.GUIShaderGetPos();
+ GLint tex0Loc = m_context.GUIShaderGetCoord0();
+ GLint uniColLoc = m_context.GUIShaderGetUniCol();
+
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex) * 4, &vertex[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+
+ glEnableVertexAttribArray(posLoc);
+ glEnableVertexAttribArray(tex0Loc);
+
+ // Setup Colour values
+ colour[0] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::R, color);
+ colour[1] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::G, color);
+ colour[2] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::B, color);
+ colour[3] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::A, color);
+
+ if (m_context.UseLimitedColor())
+ {
+ colour[0] = (235 - 16) * colour[0] / 255 + 16;
+ colour[1] = (235 - 16) * colour[1] / 255 + 16;
+ colour[2] = (235 - 16) * colour[2] / 255 + 16;
+ }
+
+ glUniform4f(uniColLoc, (colour[0] / 255.0f), (colour[1] / 255.0f), (colour[2] / 255.0f),
+ (colour[3] / 255.0f));
+
+ glGenBuffers(1, &indexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte) * 4, idx, GL_STATIC_DRAW);
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(tex0Loc);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &indexVBO);
+
+ m_context.DisableGUIShader();
+
+#elif defined(HAS_GLES)
+
+ renderBuffer->BindToUnit(0);
+
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glEnable(GL_BLEND); // Turn blending On
+
+ m_context.EnableGUIShader(GL_SHADER_METHOD::TEXTURE);
+
+ GLubyte col[4];
+ GLfloat ver[4][3];
+ GLfloat tex[4][2];
+ GLubyte idx[4] = {0, 1, 3, 2}; // Determines order of triangle strip
+
+ GLint posLoc = m_context.GUIShaderGetPos();
+ GLint tex0Loc = m_context.GUIShaderGetCoord0();
+ GLint uniColLoc = m_context.GUIShaderGetUniCol();
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, 0, 0, ver);
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, 0, tex);
+
+ glEnableVertexAttribArray(posLoc);
+ glEnableVertexAttribArray(tex0Loc);
+
+ // Setup color values
+ col[0] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::R, color);
+ col[1] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::G, color);
+ col[2] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::B, color);
+ col[3] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::A, color);
+
+ for (unsigned int i = 0; i < 4; i++)
+ {
+ // Setup vertex position values
+ ver[i][0] = m_rotatedDestCoords[i].x;
+ ver[i][1] = m_rotatedDestCoords[i].y;
+ ver[i][2] = 0.0f;
+ }
+
+ // Setup texture coordinates
+ tex[0][0] = tex[3][0] = u1;
+ tex[0][1] = tex[1][1] = v1;
+ tex[1][0] = tex[2][0] = u2;
+ tex[2][1] = tex[3][1] = v2;
+
+ glUniform4f(uniColLoc, (col[0] / 255.0f), (col[1] / 255.0f), (col[2] / 255.0f),
+ (col[3] / 255.0f));
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, idx);
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(tex0Loc);
+
+ m_context.DisableGUIShader();
+
+#endif
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererGuiTexture.h b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererGuiTexture.h
new file mode 100644
index 0000000..8441a67
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererGuiTexture.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017-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 "RPBaseRenderer.h"
+#include "cores/GameSettings.h"
+#include "cores/RetroPlayer/buffers/BaseRenderBufferPool.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRendererFactoryGuiTexture : public IRendererFactory
+{
+public:
+ ~CRendererFactoryGuiTexture() override = default;
+
+ // implementation of IRendererFactory
+ std::string RenderSystemName() const override;
+ CRPBaseRenderer* CreateRenderer(const CRenderSettings& settings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool) override;
+ RenderBufferPoolVector CreateBufferPools(CRenderContext& context) override;
+};
+
+class CRenderBufferPoolGuiTexture : public CBaseRenderBufferPool
+{
+public:
+ CRenderBufferPoolGuiTexture(SCALINGMETHOD scalingMethod);
+ ~CRenderBufferPoolGuiTexture() override = default;
+
+ // implementation of IRenderBufferPool via CBaseRenderBufferPool
+ bool IsCompatible(const CRenderVideoSettings& renderSettings) const override;
+
+ // implementation of CBaseRenderBufferPool
+ IRenderBuffer* CreateRenderBuffer(void* header = nullptr) override;
+
+private:
+ SCALINGMETHOD m_scalingMethod;
+};
+
+class CRPRendererGuiTexture : public CRPBaseRenderer
+{
+public:
+ CRPRendererGuiTexture(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool);
+ ~CRPRendererGuiTexture() override = default;
+
+ // public implementation of CRPBaseRenderer
+ bool Supports(RENDERFEATURE feature) const override;
+ SCALINGMETHOD GetDefaultScalingMethod() const override { return SCALINGMETHOD::NEAREST; }
+
+protected:
+ // protected implementation of CRPBaseRenderer
+ void RenderInternal(bool clear, uint8_t alpha) override;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.cpp b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.cpp
new file mode 100644
index 0000000..49ff7f8
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.cpp
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2017-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 "RPRendererOpenGL.h"
+
+#include "cores/RetroPlayer/buffers/RenderBufferOpenGL.h"
+#include "cores/RetroPlayer/buffers/RenderBufferPoolOpenGL.h"
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+
+#include <cstddef>
+
+using namespace KODI;
+using namespace RETRO;
+
+// --- CRendererFactoryOpenGL --------------------------------------------------
+
+std::string CRendererFactoryOpenGL::RenderSystemName() const
+{
+ return "OpenGL";
+}
+
+CRPBaseRenderer* CRendererFactoryOpenGL::CreateRenderer(
+ const CRenderSettings& settings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool)
+{
+ return new CRPRendererOpenGL(settings, context, std::move(bufferPool));
+}
+
+RenderBufferPoolVector CRendererFactoryOpenGL::CreateBufferPools(CRenderContext& context)
+{
+ return {std::make_shared<CRenderBufferPoolOpenGL>()};
+}
+
+// --- CRPRendererOpenGL -------------------------------------------------------
+
+CRPRendererOpenGL::CRPRendererOpenGL(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool)
+ : CRPBaseRenderer(renderSettings, context, std::move(bufferPool))
+{
+ // Initialize CRPRendererOpenGL
+ m_clearColour = m_context.UseLimitedColor() ? (16.0f / 0xff) : 0.0f;
+
+ // Set up main screen VAO/VBOs
+ glGenVertexArrays(1, &m_mainVAO);
+ glBindVertexArray(m_mainVAO);
+
+ glGenBuffers(1, &m_mainVertexVBO);
+ glGenBuffers(1, &m_mainIndexVBO);
+
+ m_context.EnableGUIShader(GL_SHADER_METHOD::TEXTURE);
+ GLint vertLoc = m_context.GUIShaderGetPos();
+ GLint loc = m_context.GUIShaderGetCoord0();
+ m_context.DisableGUIShader();
+
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_mainIndexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, m_mainVertexVBO);
+ glEnableVertexAttribArray(vertLoc);
+ glVertexAttribPointer(vertLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glEnableVertexAttribArray(loc);
+ glVertexAttribPointer(loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+
+ // Set up black bars VAO/VBO
+ glGenVertexArrays(1, &m_blackbarsVAO);
+ glBindVertexArray(m_blackbarsVAO);
+
+ glGenBuffers(1, &m_blackbarsVertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, m_blackbarsVertexVBO);
+
+ m_context.EnableGUIShader(GL_SHADER_METHOD::DEFAULT);
+ GLint posLoc = m_context.GUIShaderGetPos();
+ m_context.DisableGUIShader();
+
+ glEnableVertexAttribArray(posLoc);
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, GL_FALSE, sizeof(Svertex), 0);
+
+ // Unbind everything just to be safe
+ glBindVertexArray(0);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+}
+
+CRPRendererOpenGL::~CRPRendererOpenGL()
+{
+ glDeleteBuffers(1, &m_mainIndexVBO);
+ glDeleteBuffers(1, &m_mainVertexVBO);
+ glDeleteBuffers(1, &m_blackbarsVertexVBO);
+
+ glDeleteVertexArrays(1, &m_mainVAO);
+ glDeleteVertexArrays(1, &m_blackbarsVAO);
+}
+
+void CRPRendererOpenGL::RenderInternal(bool clear, uint8_t alpha)
+{
+ if (clear)
+ {
+ if (alpha == 255)
+ DrawBlackBars();
+ else
+ ClearBackBuffer();
+ }
+
+ if (alpha < 255)
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ }
+ else
+ {
+ glDisable(GL_BLEND);
+ }
+
+ Render(alpha);
+
+ glEnable(GL_BLEND);
+ glFlush();
+}
+
+void CRPRendererOpenGL::FlushInternal()
+{
+ if (!m_bConfigured)
+ return;
+
+ glFinish();
+}
+
+bool CRPRendererOpenGL::Supports(RENDERFEATURE feature) const
+{
+ return feature == RENDERFEATURE::STRETCH || feature == RENDERFEATURE::ZOOM ||
+ feature == RENDERFEATURE::PIXEL_RATIO || feature == RENDERFEATURE::ROTATION;
+}
+
+bool CRPRendererOpenGL::SupportsScalingMethod(SCALINGMETHOD method)
+{
+ return method == SCALINGMETHOD::NEAREST || method == SCALINGMETHOD::LINEAR;
+}
+
+void CRPRendererOpenGL::ClearBackBuffer()
+{
+ glClearColor(m_clearColour, m_clearColour, m_clearColour, 0.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+}
+
+void CRPRendererOpenGL::DrawBlackBars()
+{
+ glDisable(GL_BLEND);
+
+ Svertex vertices[24];
+ GLubyte count = 0;
+
+ m_context.EnableGUIShader(GL_SHADER_METHOD::DEFAULT);
+ GLint uniCol = m_context.GUIShaderGetUniCol();
+
+ glUniform4f(uniCol, m_clearColour / 255.0f, m_clearColour / 255.0f, m_clearColour / 255.0f, 1.0f);
+
+ // top quad
+ if (m_rotatedDestCoords[0].y > 0.0f)
+ {
+ GLubyte quad = count;
+ vertices[quad].x = 0.0;
+ vertices[quad].y = 0.0;
+ vertices[quad].z = 0;
+ vertices[quad + 1].x = m_context.GetScreenWidth();
+ vertices[quad + 1].y = 0;
+ vertices[quad + 1].z = 0;
+ vertices[quad + 2].x = m_context.GetScreenWidth();
+ vertices[quad + 2].y = m_rotatedDestCoords[0].y;
+ vertices[quad + 2].z = 0;
+ vertices[quad + 3] = vertices[quad + 2];
+ vertices[quad + 4].x = 0;
+ vertices[quad + 4].y = m_rotatedDestCoords[0].y;
+ vertices[quad + 4].z = 0;
+ vertices[quad + 5] = vertices[quad];
+ count += 6;
+ }
+
+ // bottom quad
+ if (m_rotatedDestCoords[2].y < m_context.GetScreenHeight())
+ {
+ GLubyte quad = count;
+ vertices[quad].x = 0.0;
+ vertices[quad].y = m_rotatedDestCoords[2].y;
+ vertices[quad].z = 0;
+ vertices[quad + 1].x = m_context.GetScreenWidth();
+ vertices[quad + 1].y = m_rotatedDestCoords[2].y;
+ vertices[quad + 1].z = 0;
+ vertices[quad + 2].x = m_context.GetScreenWidth();
+ vertices[quad + 2].y = m_context.GetScreenHeight();
+ vertices[quad + 2].z = 0;
+ vertices[quad + 3] = vertices[quad + 2];
+ vertices[quad + 4].x = 0;
+ vertices[quad + 4].y = m_context.GetScreenHeight();
+ vertices[quad + 4].z = 0;
+ vertices[quad + 5] = vertices[quad];
+ count += 6;
+ }
+
+ // left quad
+ if (m_rotatedDestCoords[0].x > 0.0f)
+ {
+ GLubyte quad = count;
+ vertices[quad].x = 0.0;
+ vertices[quad].y = m_rotatedDestCoords[0].y;
+ vertices[quad].z = 0;
+ vertices[quad + 1].x = m_rotatedDestCoords[0].x;
+ vertices[quad + 1].y = m_rotatedDestCoords[0].y;
+ vertices[quad + 1].z = 0;
+ vertices[quad + 2].x = m_rotatedDestCoords[3].x;
+ vertices[quad + 2].y = m_rotatedDestCoords[3].y;
+ vertices[quad + 2].z = 0;
+ vertices[quad + 3] = vertices[quad + 2];
+ vertices[quad + 4].x = 0;
+ vertices[quad + 4].y = m_rotatedDestCoords[3].y;
+ vertices[quad + 4].z = 0;
+ vertices[quad + 5] = vertices[quad];
+ count += 6;
+ }
+
+ // right quad
+ if (m_rotatedDestCoords[2].x < m_context.GetScreenWidth())
+ {
+ GLubyte quad = count;
+ vertices[quad].x = m_rotatedDestCoords[1].x;
+ vertices[quad].y = m_rotatedDestCoords[1].y;
+ vertices[quad].z = 0;
+ vertices[quad + 1].x = m_context.GetScreenWidth();
+ vertices[quad + 1].y = m_rotatedDestCoords[1].y;
+ vertices[quad + 1].z = 0;
+ vertices[quad + 2].x = m_context.GetScreenWidth();
+ vertices[quad + 2].y = m_rotatedDestCoords[2].y;
+ vertices[quad + 2].z = 0;
+ vertices[quad + 3] = vertices[quad + 2];
+ vertices[quad + 4].x = m_rotatedDestCoords[1].x;
+ vertices[quad + 4].y = m_rotatedDestCoords[2].y;
+ vertices[quad + 4].z = 0;
+ vertices[quad + 5] = vertices[quad];
+ count += 6;
+ }
+
+ glBindVertexArray(m_blackbarsVAO);
+
+ glBindBuffer(GL_ARRAY_BUFFER, m_blackbarsVertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(Svertex) * count, &vertices[0], GL_STATIC_DRAW);
+
+ glDrawArrays(GL_TRIANGLES, 0, count);
+
+ // Unbind VAO/VBO just to be safe
+ glBindVertexArray(0);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+
+ m_context.DisableGUIShader();
+}
+
+void CRPRendererOpenGL::Render(uint8_t alpha)
+{
+ CRenderBufferOpenGL* renderBuffer = static_cast<CRenderBufferOpenGL*>(m_renderBuffer);
+
+ if (renderBuffer == nullptr)
+ return;
+
+ CRect rect = m_sourceRect;
+
+ rect.x1 /= renderBuffer->GetWidth();
+ rect.x2 /= renderBuffer->GetWidth();
+ rect.y1 /= renderBuffer->GetHeight();
+ rect.y2 /= renderBuffer->GetHeight();
+
+ const uint32_t color = (alpha << 24) | 0xFFFFFF;
+
+ glBindTexture(m_textureTarget, renderBuffer->TextureID());
+
+ GLint filter = GL_NEAREST;
+ if (GetRenderSettings().VideoSettings().GetScalingMethod() == SCALINGMETHOD::LINEAR)
+ filter = GL_LINEAR;
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, filter);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, filter);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ m_context.EnableGUIShader(GL_SHADER_METHOD::TEXTURE);
+
+ GLubyte colour[4];
+ GLubyte idx[4] = {0, 1, 3, 2}; // Determines order of triangle strip
+ PackedVertex vertex[4];
+
+ GLint uniColLoc = m_context.GUIShaderGetUniCol();
+
+ // Setup color values
+ colour[0] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::R, color);
+ colour[1] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::G, color);
+ colour[2] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::B, color);
+ colour[3] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::A, color);
+
+ for (unsigned int i = 0; i < 4; i++)
+ {
+ // Setup vertex position values
+ vertex[i].x = m_rotatedDestCoords[i].x;
+ vertex[i].y = m_rotatedDestCoords[i].y;
+ vertex[i].z = 0.0f;
+ }
+
+ // Setup texture coordinates
+ vertex[0].u1 = vertex[3].u1 = rect.x1;
+ vertex[0].v1 = vertex[1].v1 = rect.y1;
+ vertex[1].u1 = vertex[2].u1 = rect.x2;
+ vertex[2].v1 = vertex[3].v1 = rect.y2;
+
+ glBindVertexArray(m_mainVAO);
+
+ glBindBuffer(GL_ARRAY_BUFFER, m_mainVertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex) * 4, &vertex[0], GL_STATIC_DRAW);
+
+ // No need to bind the index VBO, it's part of VAO state
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte) * 4, idx, GL_STATIC_DRAW);
+
+ glUniform4f(uniColLoc, (colour[0] / 255.0f), (colour[1] / 255.0f), (colour[2] / 255.0f),
+ (colour[3] / 255.0f));
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+
+ // Unbind VAO/VBO just to be safe
+ glBindVertexArray(0);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+
+ m_context.DisableGUIShader();
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h
new file mode 100644
index 0000000..be9e4f2
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017-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 "RPBaseRenderer.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+
+#include "system_gl.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderContext;
+
+class CRendererFactoryOpenGL : public IRendererFactory
+{
+public:
+ ~CRendererFactoryOpenGL() override = default;
+
+ // implementation of IRendererFactory
+ std::string RenderSystemName() const override;
+ CRPBaseRenderer* CreateRenderer(const CRenderSettings& settings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool) override;
+ RenderBufferPoolVector CreateBufferPools(CRenderContext& context) override;
+};
+
+class CRPRendererOpenGL : public CRPBaseRenderer
+{
+public:
+ CRPRendererOpenGL(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool);
+ ~CRPRendererOpenGL() override;
+
+ // implementation of CRPBaseRenderer
+ bool Supports(RENDERFEATURE feature) const override;
+ SCALINGMETHOD GetDefaultScalingMethod() const override { return SCALINGMETHOD::NEAREST; }
+
+ static bool SupportsScalingMethod(SCALINGMETHOD method);
+
+protected:
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ };
+ struct Svertex
+ {
+ float x;
+ float y;
+ float z;
+ };
+
+ // implementation of CRPBaseRenderer
+ void RenderInternal(bool clear, uint8_t alpha) override;
+ void FlushInternal() override;
+
+ /*!
+ * \brief Set the entire backbuffer to black
+ */
+ void ClearBackBuffer();
+
+ /*!
+ * \brief Draw black bars around the video quad
+ *
+ * This is more efficient than glClear() since it only sets pixels to
+ * black that aren't going to be overwritten by the game.
+ */
+ void DrawBlackBars();
+
+ virtual void Render(uint8_t alpha);
+
+ GLuint m_mainVAO;
+ GLuint m_mainVertexVBO;
+ GLuint m_mainIndexVBO;
+
+ GLuint m_blackbarsVAO;
+ GLuint m_blackbarsVertexVBO;
+
+ GLenum m_textureTarget = GL_TEXTURE_2D;
+ float m_clearColour = 0.0f;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.cpp b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.cpp
new file mode 100644
index 0000000..b8e4259
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.cpp
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2017-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 "RPRendererOpenGLES.h"
+
+#include "cores/RetroPlayer/buffers/RenderBufferOpenGLES.h"
+#include "cores/RetroPlayer/buffers/RenderBufferPoolOpenGLES.h"
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "cores/RetroPlayer/rendering/RenderVideoSettings.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+
+#include <cstring>
+#include <stddef.h>
+
+using namespace KODI;
+using namespace RETRO;
+
+// --- CRendererFactoryOpenGLES ------------------------------------------------
+
+std::string CRendererFactoryOpenGLES::RenderSystemName() const
+{
+ return "OpenGLES";
+}
+
+CRPBaseRenderer* CRendererFactoryOpenGLES::CreateRenderer(
+ const CRenderSettings& settings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool)
+{
+ return new CRPRendererOpenGLES(settings, context, std::move(bufferPool));
+}
+
+RenderBufferPoolVector CRendererFactoryOpenGLES::CreateBufferPools(CRenderContext& context)
+{
+ return {std::make_shared<CRenderBufferPoolOpenGLES>(context)};
+}
+
+// --- CRPRendererOpenGLES -----------------------------------------------------
+
+CRPRendererOpenGLES::CRPRendererOpenGLES(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool)
+ : CRPBaseRenderer(renderSettings, context, std::move(bufferPool))
+{
+ glGenBuffers(1, &m_mainIndexVBO);
+ glGenBuffers(1, &m_mainVertexVBO);
+ glGenBuffers(1, &m_blackbarsVertexVBO);
+}
+
+CRPRendererOpenGLES::~CRPRendererOpenGLES()
+{
+ glDeleteBuffers(1, &m_mainIndexVBO);
+ glDeleteBuffers(1, &m_mainVertexVBO);
+ glDeleteBuffers(1, &m_blackbarsVertexVBO);
+}
+
+void CRPRendererOpenGLES::RenderInternal(bool clear, uint8_t alpha)
+{
+ if (clear)
+ {
+ if (alpha == 255)
+ DrawBlackBars();
+ else
+ ClearBackBuffer();
+ }
+
+ if (alpha < 255)
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ }
+ else
+ {
+ glDisable(GL_BLEND);
+ }
+
+ Render(alpha);
+
+ glEnable(GL_BLEND);
+ glFlush();
+}
+
+void CRPRendererOpenGLES::FlushInternal()
+{
+ if (!m_bConfigured)
+ return;
+
+ glFinish();
+}
+
+bool CRPRendererOpenGLES::Supports(RENDERFEATURE feature) const
+{
+ return feature == RENDERFEATURE::STRETCH || feature == RENDERFEATURE::ZOOM ||
+ feature == RENDERFEATURE::PIXEL_RATIO || feature == RENDERFEATURE::ROTATION;
+}
+
+bool CRPRendererOpenGLES::SupportsScalingMethod(SCALINGMETHOD method)
+{
+ return method == SCALINGMETHOD::NEAREST || method == SCALINGMETHOD::LINEAR;
+}
+
+void CRPRendererOpenGLES::ClearBackBuffer()
+{
+ glClearColor(m_clearColour, m_clearColour, m_clearColour, 0.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+}
+
+void CRPRendererOpenGLES::DrawBlackBars()
+{
+ glDisable(GL_BLEND);
+
+ struct Svertex
+ {
+ float x;
+ float y;
+ float z;
+ };
+ Svertex vertices[24];
+ GLubyte count = 0;
+
+ m_context.EnableGUIShader(GL_SHADER_METHOD::DEFAULT);
+ GLint posLoc = m_context.GUIShaderGetPos();
+ GLint uniCol = m_context.GUIShaderGetUniCol();
+
+ glUniform4f(uniCol, m_clearColour / 255.0f, m_clearColour / 255.0f, m_clearColour / 255.0f, 1.0f);
+
+ // top quad
+ if (m_rotatedDestCoords[0].y > 0.0f)
+ {
+ GLubyte quad = count;
+ vertices[quad].x = 0.0;
+ vertices[quad].y = 0.0;
+ vertices[quad].z = 0;
+ vertices[quad + 1].x = m_context.GetScreenWidth();
+ vertices[quad + 1].y = 0;
+ vertices[quad + 1].z = 0;
+ vertices[quad + 2].x = m_context.GetScreenWidth();
+ vertices[quad + 2].y = m_rotatedDestCoords[0].y;
+ vertices[quad + 2].z = 0;
+ vertices[quad + 3] = vertices[quad + 2];
+ vertices[quad + 4].x = 0;
+ vertices[quad + 4].y = m_rotatedDestCoords[0].y;
+ vertices[quad + 4].z = 0;
+ vertices[quad + 5] = vertices[quad];
+ count += 6;
+ }
+
+ // bottom quad
+ if (m_rotatedDestCoords[2].y < m_context.GetScreenHeight())
+ {
+ GLubyte quad = count;
+ vertices[quad].x = 0.0;
+ vertices[quad].y = m_rotatedDestCoords[2].y;
+ vertices[quad].z = 0;
+ vertices[quad + 1].x = m_context.GetScreenWidth();
+ vertices[quad + 1].y = m_rotatedDestCoords[2].y;
+ vertices[quad + 1].z = 0;
+ vertices[quad + 2].x = m_context.GetScreenWidth();
+ vertices[quad + 2].y = m_context.GetScreenHeight();
+ vertices[quad + 2].z = 0;
+ vertices[quad + 3] = vertices[quad + 2];
+ vertices[quad + 4].x = 0;
+ vertices[quad + 4].y = m_context.GetScreenHeight();
+ vertices[quad + 4].z = 0;
+ vertices[quad + 5] = vertices[quad];
+ count += 6;
+ }
+
+ // left quad
+ if (m_rotatedDestCoords[0].x > 0.0f)
+ {
+ GLubyte quad = count;
+ vertices[quad].x = 0.0;
+ vertices[quad].y = m_rotatedDestCoords[0].y;
+ vertices[quad].z = 0;
+ vertices[quad + 1].x = m_rotatedDestCoords[0].x;
+ vertices[quad + 1].y = m_rotatedDestCoords[0].y;
+ vertices[quad + 1].z = 0;
+ vertices[quad + 2].x = m_rotatedDestCoords[3].x;
+ vertices[quad + 2].y = m_rotatedDestCoords[3].y;
+ vertices[quad + 2].z = 0;
+ vertices[quad + 3] = vertices[quad + 2];
+ vertices[quad + 4].x = 0;
+ vertices[quad + 4].y = m_rotatedDestCoords[3].y;
+ vertices[quad + 4].z = 0;
+ vertices[quad + 5] = vertices[quad];
+ count += 6;
+ }
+
+ // right quad
+ if (m_rotatedDestCoords[2].x < m_context.GetScreenWidth())
+ {
+ GLubyte quad = count;
+ vertices[quad].x = m_rotatedDestCoords[1].x;
+ vertices[quad].y = m_rotatedDestCoords[1].y;
+ vertices[quad].z = 0;
+ vertices[quad + 1].x = m_context.GetScreenWidth();
+ vertices[quad + 1].y = m_rotatedDestCoords[1].y;
+ vertices[quad + 1].z = 0;
+ vertices[quad + 2].x = m_context.GetScreenWidth();
+ vertices[quad + 2].y = m_rotatedDestCoords[2].y;
+ vertices[quad + 2].z = 0;
+ vertices[quad + 3] = vertices[quad + 2];
+ vertices[quad + 4].x = m_rotatedDestCoords[1].x;
+ vertices[quad + 4].y = m_rotatedDestCoords[2].y;
+ vertices[quad + 4].z = 0;
+ vertices[quad + 5] = vertices[quad];
+ count += 6;
+ }
+
+ glBindBuffer(GL_ARRAY_BUFFER, m_blackbarsVertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(Svertex) * count, &vertices[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, GL_FALSE, sizeof(Svertex), 0);
+ glEnableVertexAttribArray(posLoc);
+
+ glDrawArrays(GL_TRIANGLES, 0, count);
+
+ glDisableVertexAttribArray(posLoc);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+
+ m_context.DisableGUIShader();
+}
+
+void CRPRendererOpenGLES::Render(uint8_t alpha)
+{
+ CRenderBufferOpenGLES* renderBuffer = static_cast<CRenderBufferOpenGLES*>(m_renderBuffer);
+
+ if (renderBuffer == nullptr)
+ return;
+
+ CRect rect = m_sourceRect;
+
+ rect.x1 /= renderBuffer->GetWidth();
+ rect.x2 /= renderBuffer->GetWidth();
+ rect.y1 /= renderBuffer->GetHeight();
+ rect.y2 /= renderBuffer->GetHeight();
+
+ const uint32_t color = (alpha << 24) | 0xFFFFFF;
+
+ glBindTexture(m_textureTarget, renderBuffer->TextureID());
+
+ GLint filter = GL_NEAREST;
+ if (GetRenderSettings().VideoSettings().GetScalingMethod() == SCALINGMETHOD::LINEAR)
+ filter = GL_LINEAR;
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, filter);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, filter);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ m_context.EnableGUIShader(GL_SHADER_METHOD::TEXTURE_NOALPHA);
+
+ GLubyte colour[4];
+ GLubyte idx[4] = {0, 1, 3, 2}; // Determines order of triangle strip
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ } vertex[4];
+
+ GLint vertLoc = m_context.GUIShaderGetPos();
+ GLint loc = m_context.GUIShaderGetCoord0();
+ GLint uniColLoc = m_context.GUIShaderGetUniCol();
+
+ // Setup color values
+ colour[0] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::R, color);
+ colour[1] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::G, color);
+ colour[2] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::B, color);
+ colour[3] = UTILS::GL::GetChannelFromARGB(UTILS::GL::ColorChannel::A, color);
+
+ for (unsigned int i = 0; i < 4; i++)
+ {
+ // Setup vertex position values
+ vertex[i].x = m_rotatedDestCoords[i].x;
+ vertex[i].y = m_rotatedDestCoords[i].y;
+ vertex[i].z = 0.0f;
+ }
+
+ // Setup texture coordinates
+ vertex[0].u1 = vertex[3].u1 = rect.x1;
+ vertex[0].v1 = vertex[1].v1 = rect.y1;
+ vertex[1].u1 = vertex[2].u1 = rect.x2;
+ vertex[2].v1 = vertex[3].v1 = rect.y2;
+
+ glBindBuffer(GL_ARRAY_BUFFER, m_mainVertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex) * 4, &vertex[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(vertLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glVertexAttribPointer(loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+
+ glEnableVertexAttribArray(vertLoc);
+ glEnableVertexAttribArray(loc);
+
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_mainIndexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte) * 4, idx, GL_STATIC_DRAW);
+
+ glUniform4f(uniColLoc, (colour[0] / 255.0f), (colour[1] / 255.0f), (colour[2] / 255.0f),
+ (colour[3] / 255.0f));
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+
+ glDisableVertexAttribArray(vertLoc);
+ glDisableVertexAttribArray(loc);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+
+ m_context.DisableGUIShader();
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h
new file mode 100644
index 0000000..ca1f583
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017-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 "RPBaseRenderer.h"
+#include "cores/GameSettings.h"
+#include "cores/RetroPlayer/buffers/BaseRenderBufferPool.h"
+#include "cores/RetroPlayer/buffers/video/RenderBufferSysMem.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+
+#include <atomic>
+#include <stdint.h>
+#include <vector>
+
+#include "system_gl.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRendererFactoryOpenGLES : public IRendererFactory
+{
+public:
+ ~CRendererFactoryOpenGLES() override = default;
+
+ // implementation of IRendererFactory
+ std::string RenderSystemName() const override;
+ CRPBaseRenderer* CreateRenderer(const CRenderSettings& settings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool) override;
+ RenderBufferPoolVector CreateBufferPools(CRenderContext& context) override;
+};
+
+class CRPRendererOpenGLES : public CRPBaseRenderer
+{
+public:
+ CRPRendererOpenGLES(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool);
+ ~CRPRendererOpenGLES() override;
+
+ // implementation of CRPBaseRenderer
+ bool Supports(RENDERFEATURE feature) const override;
+ SCALINGMETHOD GetDefaultScalingMethod() const override { return SCALINGMETHOD::NEAREST; }
+
+ static bool SupportsScalingMethod(SCALINGMETHOD method);
+
+protected:
+ // implementation of CRPBaseRenderer
+ void RenderInternal(bool clear, uint8_t alpha) override;
+ void FlushInternal() override;
+
+ /*!
+ * \brief Set the entire backbuffer to black
+ */
+ void ClearBackBuffer();
+
+ /*!
+ * \brief Draw black bars around the video quad
+ *
+ * This is more efficient than glClear() since it only sets pixels to
+ * black that aren't going to be overwritten by the game.
+ */
+ void DrawBlackBars();
+
+ virtual void Render(uint8_t alpha);
+
+ GLuint m_mainIndexVBO;
+ GLuint m_mainVertexVBO;
+ GLuint m_blackbarsVertexVBO;
+ GLenum m_textureTarget = GL_TEXTURE_2D;
+ float m_clearColour = 0.0f;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPWinRenderer.cpp b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPWinRenderer.cpp
new file mode 100644
index 0000000..167f6a8
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPWinRenderer.cpp
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2017-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 "RPWinRenderer.h"
+
+#include "cores/RetroPlayer/rendering/RenderContext.h"
+#include "cores/RetroPlayer/rendering/RenderTranslator.h"
+#include "cores/RetroPlayer/rendering/RenderVideoSettings.h"
+#include "cores/RetroPlayer/rendering/VideoShaders/windows/RPWinOutputShader.h"
+#include "guilib/D3DResource.h"
+#include "rendering/dx/RenderSystemDX.h"
+#include "utils/log.h"
+
+extern "C"
+{
+#include <libswscale/swscale.h>
+}
+
+#include <cstring>
+
+using namespace KODI;
+using namespace RETRO;
+
+// --- CWinRendererFactory -----------------------------------------------------
+
+std::string CWinRendererFactory::RenderSystemName() const
+{
+ return "DirectX";
+}
+
+CRPBaseRenderer* CWinRendererFactory::CreateRenderer(const CRenderSettings& settings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool)
+{
+ return new CRPWinRenderer(settings, context, std::move(bufferPool));
+}
+
+RenderBufferPoolVector CWinRendererFactory::CreateBufferPools(CRenderContext& context)
+{
+ return {std::make_shared<CWinRenderBufferPool>()};
+}
+
+// --- CWinRenderBuffer --------------------------------------------------------
+
+CWinRenderBuffer::CWinRenderBuffer(AVPixelFormat pixFormat, DXGI_FORMAT dxFormat)
+ : m_pixFormat(pixFormat), m_targetDxFormat(dxFormat), m_targetPixFormat(GetPixFormat())
+{
+}
+
+CWinRenderBuffer::~CWinRenderBuffer()
+{
+ if (m_swsContext != nullptr)
+ sws_freeContext(m_swsContext);
+}
+
+bool CWinRenderBuffer::CreateTexture()
+{
+ if (!m_intermediateTarget->Create(m_width, m_height, 1, D3D11_USAGE_DYNAMIC, m_targetDxFormat))
+ {
+ CLog::Log(LOGERROR, "WinRenderer: Intermediate render target creation failed");
+ return false;
+ }
+
+ return true;
+}
+
+bool CWinRenderBuffer::GetTexture(uint8_t*& data, unsigned int& stride)
+{
+ // Scale and upload texture
+ D3D11_MAPPED_SUBRESOURCE destlr;
+ if (!m_intermediateTarget->LockRect(0, &destlr, D3D11_MAP_WRITE_DISCARD))
+ {
+ CLog::Log(LOGERROR, "WinRenderer: Failed to lock swtarget texture into memory");
+ return false;
+ }
+
+ data = static_cast<uint8_t*>(destlr.pData);
+ stride = destlr.RowPitch;
+
+ return true;
+}
+
+bool CWinRenderBuffer::ReleaseTexture()
+{
+ if (!m_intermediateTarget->UnlockRect(0))
+ {
+ CLog::Log(LOGERROR, "WinRenderer: Failed to unlock swtarget texture");
+ return false;
+ }
+
+ return true;
+}
+
+bool CWinRenderBuffer::UploadTexture()
+{
+ if (m_targetDxFormat == DXGI_FORMAT_UNKNOWN)
+ {
+ CLog::Log(LOGERROR, "WinRenderer: Invalid DX texture format");
+ return false;
+ }
+
+ if (!CreateScalingContext())
+ return false;
+
+ // Create intermediate texture
+ if (!m_intermediateTarget)
+ {
+ m_intermediateTarget.reset(new CD3DTexture);
+ if (!CreateTexture())
+ {
+ m_intermediateTarget.reset();
+ return false;
+ }
+ }
+
+ uint8_t* destData = nullptr;
+ unsigned int destStride = 0;
+ if (!GetTexture(destData, destStride))
+ return false;
+
+ const unsigned int sourceStride = static_cast<unsigned int>(m_data.size() / m_height);
+ ScalePixels(m_data.data(), sourceStride, destData, destStride);
+
+ if (!ReleaseTexture())
+ return false;
+
+ return true;
+}
+
+bool CWinRenderBuffer::CreateScalingContext()
+{
+ if (m_swsContext == nullptr)
+ {
+ m_swsContext = sws_getContext(m_width, m_height, m_pixFormat, m_width, m_height,
+ m_targetPixFormat, SWS_FAST_BILINEAR, NULL, NULL, NULL);
+
+ if (m_swsContext == nullptr)
+ {
+ CLog::Log(LOGERROR, "WinRenderer: Failed to create swscale context");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void CWinRenderBuffer::ScalePixels(const uint8_t* source,
+ unsigned int sourceStride,
+ uint8_t* target,
+ unsigned int targetStride)
+{
+ uint8_t* src[] = {const_cast<uint8_t*>(source), nullptr, nullptr, nullptr};
+ int srcStride[] = {static_cast<int>(sourceStride), 0, 0, 0};
+ uint8_t* dst[] = {target, nullptr, nullptr, nullptr};
+ int dstStride[] = {static_cast<int>(targetStride), 0, 0, 0};
+
+ sws_scale(m_swsContext, src, srcStride, 0, m_height, dst, dstStride);
+}
+
+AVPixelFormat CWinRenderBuffer::GetPixFormat()
+{
+ return AV_PIX_FMT_BGRA;
+}
+
+// --- CWinRenderBufferPool ----------------------------------------------------
+
+CWinRenderBufferPool::CWinRenderBufferPool()
+{
+ CompileOutputShaders();
+}
+
+bool CWinRenderBufferPool::IsCompatible(const CRenderVideoSettings& renderSettings) const
+{
+ return GetShader(renderSettings.GetScalingMethod()) != nullptr;
+}
+
+IRenderBuffer* CWinRenderBufferPool::CreateRenderBuffer(void* header /* = nullptr */)
+{
+ return new CWinRenderBuffer(m_format, m_targetDxFormat);
+}
+
+bool CWinRenderBufferPool::ConfigureDX()
+{
+ if (m_targetDxFormat != DXGI_FORMAT_UNKNOWN)
+ return false; // Already configured
+
+ // There are three pixel formats used by libretro: 0RGB32, RGB565 and
+ // RGB555. DirectX support for these varies, so always use BGRA32 as the
+ // intermediate format.
+ m_targetDxFormat = DXGI_FORMAT_B8G8R8A8_UNORM;
+
+ return true;
+}
+
+CRPWinOutputShader* CWinRenderBufferPool::GetShader(SCALINGMETHOD scalingMethod) const
+{
+ auto it = m_outputShaders.find(scalingMethod);
+
+ if (it != m_outputShaders.end())
+ return it->second.get();
+
+ return nullptr;
+}
+
+const std::vector<SCALINGMETHOD>& CWinRenderBufferPool::GetScalingMethods()
+{
+ static std::vector<SCALINGMETHOD> scalingMethods = {
+ SCALINGMETHOD::NEAREST,
+ SCALINGMETHOD::LINEAR,
+ };
+
+ return scalingMethods;
+}
+
+void CWinRenderBufferPool::CompileOutputShaders()
+{
+ for (auto scalingMethod : GetScalingMethods())
+ {
+ std::unique_ptr<CRPWinOutputShader> outputShader(new CRPWinOutputShader);
+ if (outputShader->Create(scalingMethod))
+ m_outputShaders[scalingMethod] = std::move(outputShader);
+ else
+ CLog::Log(LOGERROR, "RPWinRenderer: Unable to create output shader ({})",
+ CRenderTranslator::TranslateScalingMethod(scalingMethod));
+ }
+}
+
+// --- CRPWinRenderer ----------------------------------------------------------
+
+CRPWinRenderer::CRPWinRenderer(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool)
+ : CRPBaseRenderer(renderSettings, context, std::move(bufferPool))
+{
+}
+
+bool CRPWinRenderer::ConfigureInternal()
+{
+ CRenderSystemDX* renderingDx = static_cast<CRenderSystemDX*>(m_context.Rendering());
+
+ static_cast<CWinRenderBufferPool*>(m_bufferPool.get())->ConfigureDX();
+
+ return true;
+}
+
+void CRPWinRenderer::RenderInternal(bool clear, uint8_t alpha)
+{
+ CRenderSystemDX* renderingDx = static_cast<CRenderSystemDX*>(m_context.Rendering());
+
+ // Set alpha blend state
+ renderingDx->SetAlphaBlendEnable(alpha < 0xFF);
+
+ Render(renderingDx->GetBackBuffer());
+}
+
+bool CRPWinRenderer::Supports(RENDERFEATURE feature) const
+{
+ if (feature == RENDERFEATURE::STRETCH || feature == RENDERFEATURE::ZOOM ||
+ feature == RENDERFEATURE::PIXEL_RATIO || feature == RENDERFEATURE::ROTATION)
+ return true;
+
+ return false;
+}
+
+bool CRPWinRenderer::SupportsScalingMethod(SCALINGMETHOD method)
+{
+ if (method == SCALINGMETHOD::LINEAR || method == SCALINGMETHOD::NEAREST)
+ return true;
+
+ return false;
+}
+
+void CRPWinRenderer::Render(CD3DTexture& target)
+{
+ const CPoint destPoints[4] = {m_rotatedDestCoords[0], m_rotatedDestCoords[1],
+ m_rotatedDestCoords[2], m_rotatedDestCoords[3]};
+
+ if (m_renderBuffer != nullptr)
+ {
+ CD3DTexture* intermediateTarget = static_cast<CWinRenderBuffer*>(m_renderBuffer)->GetTarget();
+ if (intermediateTarget != nullptr)
+ {
+ CRect viewPort;
+ m_context.GetViewPort(viewPort);
+
+ // Pick appropriate output shader depending on the scaling method of the renderer
+ SCALINGMETHOD scalingMethod = m_renderSettings.VideoSettings().GetScalingMethod();
+
+ CWinRenderBufferPool* bufferPool = static_cast<CWinRenderBufferPool*>(m_bufferPool.get());
+ CRPWinOutputShader* outputShader = bufferPool->GetShader(scalingMethod);
+
+ // Use the picked output shader to render to the target
+ if (outputShader != nullptr)
+ {
+ outputShader->Render(*intermediateTarget, m_sourceRect, destPoints, viewPort, &target,
+ m_context.UseLimitedColor() ? 1 : 0);
+ }
+ }
+ }
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPWinRenderer.h b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPWinRenderer.h
new file mode 100644
index 0000000..20271f9
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoRenderers/RPWinRenderer.h
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2017-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 "RPBaseRenderer.h"
+#include "cores/RetroPlayer/buffers/BaseRenderBufferPool.h"
+#include "cores/RetroPlayer/buffers/video/RenderBufferSysMem.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+
+#include <memory>
+#include <stdint.h>
+#include <vector>
+
+#include <dxgi.h>
+
+class CD3DTexture;
+struct SwsContext;
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRenderContext;
+class CRPWinOutputShader;
+
+class CWinRendererFactory : public IRendererFactory
+{
+public:
+ virtual ~CWinRendererFactory() = default;
+
+ // implementation of IRendererFactory
+ std::string RenderSystemName() const override;
+ CRPBaseRenderer* CreateRenderer(const CRenderSettings& settings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool) override;
+ RenderBufferPoolVector CreateBufferPools(CRenderContext& context) override;
+};
+
+class CWinRenderBuffer : public CRenderBufferSysMem
+{
+public:
+ CWinRenderBuffer(AVPixelFormat pixFormat, DXGI_FORMAT dxFormat);
+ ~CWinRenderBuffer() override;
+
+ // implementation of IRenderBuffer via CRenderBufferSysMem
+ bool UploadTexture() override;
+
+ CD3DTexture* GetTarget() { return m_intermediateTarget.get(); }
+
+private:
+ bool CreateTexture();
+ bool GetTexture(uint8_t*& data, unsigned int& stride);
+ bool ReleaseTexture();
+
+ bool CreateScalingContext();
+ void ScalePixels(const uint8_t* source,
+ unsigned int sourceStride,
+ uint8_t* target,
+ unsigned int targetStride);
+
+ static AVPixelFormat GetPixFormat();
+
+ // Construction parameters
+ const AVPixelFormat m_pixFormat;
+ const DXGI_FORMAT m_targetDxFormat;
+
+ AVPixelFormat m_targetPixFormat;
+ std::unique_ptr<CD3DTexture> m_intermediateTarget;
+
+ SwsContext* m_swsContext = nullptr;
+};
+
+class CWinRenderBufferPool : public CBaseRenderBufferPool
+{
+public:
+ CWinRenderBufferPool();
+ ~CWinRenderBufferPool() override = default;
+
+ // implementation of IRenderBufferPool via CRenderBufferPoolSysMem
+ bool IsCompatible(const CRenderVideoSettings& renderSettings) const override;
+
+ // implementation of CBaseRenderBufferPool via CRenderBufferPoolSysMem
+ IRenderBuffer* CreateRenderBuffer(void* header = nullptr) override;
+
+ // DirectX interface
+ bool ConfigureDX();
+ CRPWinOutputShader* GetShader(SCALINGMETHOD scalingMethod) const;
+
+private:
+ static const std::vector<SCALINGMETHOD>& GetScalingMethods();
+
+ void CompileOutputShaders();
+
+ DXGI_FORMAT m_targetDxFormat = DXGI_FORMAT_UNKNOWN;
+ std::map<SCALINGMETHOD, std::unique_ptr<CRPWinOutputShader>> m_outputShaders;
+};
+
+class CRPWinRenderer : public CRPBaseRenderer
+{
+public:
+ CRPWinRenderer(const CRenderSettings& renderSettings,
+ CRenderContext& context,
+ std::shared_ptr<IRenderBufferPool> bufferPool);
+ ~CRPWinRenderer() override = default;
+
+ // implementation of CRPBaseRenderer
+ bool Supports(RENDERFEATURE feature) const override;
+ SCALINGMETHOD GetDefaultScalingMethod() const override { return DEFAULT_SCALING_METHOD; }
+
+ static bool SupportsScalingMethod(SCALINGMETHOD method);
+
+ /*!
+ * \brief The default scaling method of the renderer
+ */
+ static const SCALINGMETHOD DEFAULT_SCALING_METHOD = SCALINGMETHOD::NEAREST;
+
+protected:
+ // implementation of CRPBaseRenderer
+ bool ConfigureInternal() override;
+ void RenderInternal(bool clear, uint8_t alpha) override;
+
+private:
+ void Render(CD3DTexture& target);
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoShaders/CMakeLists.txt b/xbmc/cores/RetroPlayer/rendering/VideoShaders/CMakeLists.txt
new file mode 100644
index 0000000..d886000
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoShaders/CMakeLists.txt
@@ -0,0 +1,3 @@
+if(NOT ENABLE_STATIC_LIBS)
+ core_add_library(rp-videoshaders)
+endif()
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/CMakeLists.txt b/xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/CMakeLists.txt
new file mode 100644
index 0000000..51fda41
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES RPWinOutputShader.cpp)
+
+set(HEADERS RPWinOutputShader.h)
+
+core_add_library(rp-videoshaders-windows)
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/RPWinOutputShader.cpp b/xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/RPWinOutputShader.cpp
new file mode 100644
index 0000000..efc827b
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/RPWinOutputShader.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2017-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 "RPWinOutputShader.h"
+
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+bool CRPWinOutputShader::Create(SCALINGMETHOD scalingMethod)
+{
+ CWinShader::CreateVertexBuffer(4, sizeof(CUSTOMVERTEX));
+
+ DefinesMap defines;
+ switch (scalingMethod)
+ {
+ case SCALINGMETHOD::NEAREST:
+ defines["SAMP_NEAREST"] = "";
+ break;
+ case SCALINGMETHOD::LINEAR:
+ default:
+ break;
+ }
+
+ std::string effectPath("special://xbmc/system/shaders/rp_output_d3d.fx");
+
+ if (!LoadEffect(effectPath, &defines))
+ {
+ CLog::LogF(LOGERROR, "Failed to load shader {}.", effectPath);
+ return false;
+ }
+
+ // Create input layout
+ D3D11_INPUT_ELEMENT_DESC layout[] = {
+ {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
+ {"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0},
+ };
+ return CWinShader::CreateInputLayout(layout, ARRAYSIZE(layout));
+}
+
+void CRPWinOutputShader::Render(CD3DTexture& sourceTexture,
+ CRect sourceRect,
+ const CPoint points[4],
+ CRect& viewPort,
+ CD3DTexture* target,
+ unsigned range)
+{
+ PrepareParameters(sourceTexture.GetWidth(), sourceTexture.GetHeight(), sourceRect, points);
+ SetShaderParameters(sourceTexture, range, viewPort);
+ Execute({target}, 4);
+}
+
+void CRPWinOutputShader::PrepareParameters(unsigned sourceWidth,
+ unsigned sourceHeight,
+ CRect sourceRect,
+ const CPoint points[4])
+{
+ bool changed = false;
+ for (int i = 0; i < 4 && !changed; ++i)
+ changed = points[i] != m_destPoints[i];
+
+ if (m_sourceWidth != sourceWidth || m_sourceHeight != sourceHeight ||
+ m_sourceRect != sourceRect || changed)
+ {
+ m_sourceWidth = sourceWidth;
+ m_sourceHeight = sourceHeight;
+ m_sourceRect = sourceRect;
+
+ for (int i = 0; i < 4; ++i)
+ m_destPoints[i] = points[i];
+
+ CUSTOMVERTEX* v = nullptr;
+ CWinShader::LockVertexBuffer(static_cast<void**>(static_cast<void*>(&v)));
+
+ v[0].x = m_destPoints[0].x;
+ v[0].y = m_destPoints[0].y;
+ v[0].z = 0.0f;
+ v[0].tu = m_sourceRect.x1 / m_sourceWidth;
+ v[0].tv = m_sourceRect.y1 / m_sourceHeight;
+
+ v[1].x = m_destPoints[1].x;
+ v[1].y = m_destPoints[1].y;
+ v[1].z = 0.0f;
+ v[1].tu = m_sourceRect.x2 / m_sourceWidth;
+ v[1].tv = m_sourceRect.y1 / m_sourceHeight;
+
+ v[2].x = m_destPoints[2].x;
+ v[2].y = m_destPoints[2].y;
+ v[2].z = 0.0f;
+ v[2].tu = m_sourceRect.x2 / m_sourceWidth;
+ v[2].tv = m_sourceRect.y2 / m_sourceHeight;
+
+ v[3].x = m_destPoints[3].x;
+ v[3].y = m_destPoints[3].y;
+ v[3].z = 0.0f;
+ v[3].tu = m_sourceRect.x1 / m_sourceWidth;
+ v[3].tv = m_sourceRect.y2 / m_sourceHeight;
+
+ CWinShader::UnlockVertexBuffer();
+ }
+}
+
+void CRPWinOutputShader::SetShaderParameters(CD3DTexture& sourceTexture,
+ unsigned range,
+ CRect& viewPort)
+{
+ m_effect.SetTechnique("OUTPUT_T");
+ m_effect.SetResources("g_Texture", sourceTexture.GetAddressOfSRV(), 1);
+
+ float viewPortArray[2] = {viewPort.Width(), viewPort.Height()};
+ m_effect.SetFloatArray("g_viewPort", viewPortArray, 2);
+
+ float params[3] = {static_cast<float>(range)};
+ m_effect.SetFloatArray("m_params", params, 1);
+}
diff --git a/xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/RPWinOutputShader.h b/xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/RPWinOutputShader.h
new file mode 100644
index 0000000..ebeb164
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/rendering/VideoShaders/windows/RPWinOutputShader.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+#include "cores/VideoPlayer/VideoRenderers/VideoShaders/WinVideoFilter.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+
+class CRPWinOutputShader : public CWinShader
+{
+public:
+ ~CRPWinOutputShader() = default;
+
+ bool Create(SCALINGMETHOD scalingMethod);
+ void Render(CD3DTexture& sourceTexture,
+ CRect sourceRect,
+ const CPoint points[4],
+ CRect& viewPort,
+ CD3DTexture* target,
+ unsigned range = 0);
+
+private:
+ void PrepareParameters(unsigned sourceWidth,
+ unsigned sourceHeight,
+ CRect sourceRect,
+ const CPoint points[4]);
+ void SetShaderParameters(CD3DTexture& sourceTexture, unsigned range, CRect& viewPort);
+
+ unsigned m_sourceWidth{0};
+ unsigned m_sourceHeight{0};
+ CRect m_sourceRect{0.f, 0.f, 0.f, 0.f};
+ CPoint m_destPoints[4] = {
+ {0.f, 0.f},
+ {0.f, 0.f},
+ {0.f, 0.f},
+ {0.f, 0.f},
+ };
+
+ struct CUSTOMVERTEX
+ {
+ FLOAT x;
+ FLOAT y;
+ FLOAT z;
+
+ FLOAT tu;
+ FLOAT tv;
+ };
+};
+
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/savestates/CMakeLists.txt b/xbmc/cores/RetroPlayer/savestates/CMakeLists.txt
new file mode 100644
index 0000000..956776d
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/savestates/CMakeLists.txt
@@ -0,0 +1,19 @@
+set(SOURCES SavestateDatabase.cpp
+ SavestateFlatBuffer.cpp
+)
+
+set(HEADERS ISavestate.h
+ SavestateDatabase.h
+ SavestateFlatBuffer.h
+ SavestateTypes.h
+)
+
+core_add_library(retroplayer_savestates)
+
+set(DEPENDS retroplayer_messages)
+
+if(ENABLE_STATIC_LIBS)
+ add_dependencies(retroplayer_savestates ${DEPENDS})
+else()
+ add_dependencies(lib${APP_NAME_LC} ${DEPENDS})
+endif()
diff --git a/xbmc/cores/RetroPlayer/savestates/ISavestate.h b/xbmc/cores/RetroPlayer/savestates/ISavestate.h
new file mode 100644
index 0000000..c12d837
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/savestates/ISavestate.h
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 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 "SavestateTypes.h"
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+class CDateTime;
+
+namespace KODI
+{
+namespace RETRO
+{
+class ISavestate
+{
+public:
+ virtual ~ISavestate() = default;
+
+ /*!
+ * \brief Reset to the initial state
+ */
+ virtual void Reset() = 0;
+
+ /*!
+ * Access the data representation of this savestate
+ */
+ virtual bool Serialize(const uint8_t*& data, size_t& size) const = 0;
+
+ /// @name Savestate properties
+ ///{
+ /*!
+ * \brief The type of save action that created this savestate, either
+ * manual or automatic
+ */
+ virtual SAVE_TYPE Type() const = 0;
+
+ /*!
+ * \brief The slot this savestate was saved into, or 0 for no slot
+ *
+ * This allows for keyboard access of saved games using the number keys 1-9.
+ */
+ virtual uint8_t Slot() const = 0;
+
+ /*!
+ * \brief The label shown in the GUI for this savestate
+ */
+ virtual std::string Label() const = 0;
+
+ /*!
+ * \brief A caption that describes the state of the game for this savestate
+ */
+ virtual std::string Caption() const = 0;
+
+ /*!
+ * \brief The timestamp of this savestate's creation
+ */
+ virtual CDateTime Created() const = 0;
+ ///}
+
+ /// @name Game properties
+ ///{
+ /*!
+ * \brief The name of the file belonging to this savestate's game
+ */
+ virtual std::string GameFileName() const = 0;
+ ///}
+
+ /// @name Environment properties
+ ///{
+ /*!
+ * \brief The number of frames in the entire gameplay history
+ */
+ virtual uint64_t TimestampFrames() const = 0;
+
+ /*!
+ * \brief The duration of the entire gameplay history as seen by a wall clock
+ */
+ virtual double TimestampWallClock() const = 0;
+ ///}
+
+ /// @name Game client properties
+ ///{
+ /*!
+ * \brief The game client add-on ID that created this savestate
+ */
+ virtual std::string GameClientID() const = 0;
+
+ /*!
+ * \brief The semantic version of the game client
+ */
+ virtual std::string GameClientVersion() const = 0;
+ ///}
+
+ /// @name Video stream properties
+ ///{
+ /*!
+ * \brief The pixel format of the video stream
+ */
+ virtual AVPixelFormat GetPixelFormat() const = 0;
+
+ /*!
+ * \brief The nominal width of the video stream, a good guess for subsequent frames
+ */
+ virtual unsigned int GetNominalWidth() const = 0;
+
+ /*!
+ * \brief The nominal height of the video stream, a good guess for subsequent frames
+ */
+ virtual unsigned int GetNominalHeight() const = 0;
+
+ /*!
+ * \brief The maximum width of the video stream, in pixels
+ */
+ virtual unsigned int GetMaxWidth() const = 0;
+
+ /*!
+ * \brief The maximum height of the video stream, in pixels
+ */
+ virtual unsigned int GetMaxHeight() const = 0;
+
+ /*!
+ * \brief The pixel aspect ratio of the video stream
+ */
+ virtual float GetPixelAspectRatio() const = 0;
+ ///}
+
+ /// @name Video frame properties
+ ///{
+ /*!
+ * \brief A pointer to the frame's video data (pixels)
+ */
+ virtual const uint8_t* GetVideoData() const = 0;
+
+ /*!
+ * \brief The size of the frame's video data, in bytes
+ */
+ virtual size_t GetVideoSize() const = 0;
+
+ /*!
+ * \brief The width of the video frame, in pixels
+ */
+ virtual unsigned int GetVideoWidth() const = 0;
+
+ /*!
+ * \brief The height of the video frame, in pixels
+ */
+ virtual unsigned int GetVideoHeight() const = 0;
+
+ /*!
+ * \brief The rotation of the video frame, in degrees counter-clockwise
+ */
+ virtual unsigned int GetRotationDegCCW() const = 0;
+ ///}
+
+ /// @name Memory properties
+ ///{
+ /*!
+ * \brief A pointer to the internal memory (SRAM) of the frame
+ */
+ virtual const uint8_t* GetMemoryData() const = 0;
+
+ /*!
+ * \brief The size of the memory region returned by GetMemoryData()
+ */
+ virtual size_t GetMemorySize() const = 0;
+ ///}
+
+ /// @name Builders for setting individual fields
+ ///{
+ virtual void SetType(SAVE_TYPE type) = 0;
+ virtual void SetSlot(uint8_t slot) = 0;
+ virtual void SetLabel(const std::string& label) = 0;
+ virtual void SetCaption(const std::string& caption) = 0;
+ virtual void SetCreated(const CDateTime& createdUTC) = 0;
+ virtual void SetGameFileName(const std::string& gameFileName) = 0;
+ virtual void SetTimestampFrames(uint64_t timestampFrames) = 0;
+ virtual void SetTimestampWallClock(double timestampWallClock) = 0;
+ virtual void SetGameClientID(const std::string& gameClient) = 0;
+ virtual void SetGameClientVersion(const std::string& gameClient) = 0;
+ virtual void SetPixelFormat(AVPixelFormat pixelFormat) = 0;
+ virtual void SetNominalWidth(unsigned int nominalWidth) = 0;
+ virtual void SetNominalHeight(unsigned int nominalHeight) = 0;
+ virtual void SetMaxWidth(unsigned int maxWidth) = 0;
+ virtual void SetMaxHeight(unsigned int maxHeight) = 0;
+ virtual void SetPixelAspectRatio(float pixelAspectRatio) = 0;
+ virtual uint8_t* GetVideoBuffer(size_t size) = 0;
+ virtual void SetVideoWidth(unsigned int videoWidth) = 0;
+ virtual void SetVideoHeight(unsigned int videoHeight) = 0;
+ virtual void SetRotationDegCCW(unsigned int rotationCCW) = 0;
+ virtual uint8_t* GetMemoryBuffer(size_t size) = 0;
+ virtual void Finalize() = 0;
+ ///}
+
+ /*!
+ * \brief Take ownership and initialize the flatbuffer with the given vector
+ */
+ virtual bool Deserialize(std::vector<uint8_t> data) = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp
new file mode 100644
index 0000000..2611f25
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.cpp
@@ -0,0 +1,272 @@
+/*
+ * 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 "SavestateDatabase.h"
+
+#include "FileItem.h"
+#include "SavestateFlatBuffer.h"
+#include "URL.h"
+#include "XBDateTime.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/IFileTypes.h"
+#include "games/dialogs/DialogGameDefines.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+namespace
+{
+constexpr auto SAVESTATE_EXTENSION = ".sav";
+constexpr auto SAVESTATE_BASE_FOLDER = "special://home/saves/";
+} // namespace
+
+using namespace KODI;
+using namespace RETRO;
+
+CSavestateDatabase::CSavestateDatabase() = default;
+
+std::unique_ptr<ISavestate> CSavestateDatabase::AllocateSavestate()
+{
+ std::unique_ptr<ISavestate> savestate;
+
+ savestate.reset(new CSavestateFlatBuffer);
+
+ return savestate;
+}
+
+bool CSavestateDatabase::AddSavestate(const std::string& savestatePath,
+ const std::string& gamePath,
+ const ISavestate& save)
+{
+ bool bSuccess = false;
+ std::string path;
+
+ if (savestatePath.empty())
+ path = MakeSavestatePath(gamePath, save.Created());
+ else
+ path = savestatePath;
+
+ CLog::Log(LOGDEBUG, "Saving savestate to {}", CURL::GetRedacted(path));
+
+ const uint8_t* data = nullptr;
+ size_t size = 0;
+ if (save.Serialize(data, size))
+ {
+ XFILE::CFile file;
+ if (file.OpenForWrite(path, true))
+ {
+ const ssize_t written = file.Write(data, size);
+ if (written == static_cast<ssize_t>(size))
+ {
+ CLog::Log(LOGDEBUG, "Wrote savestate of {} bytes", size);
+ bSuccess = true;
+ }
+ }
+ else
+ CLog::Log(LOGERROR, "Failed to open savestate for writing");
+ }
+
+ return bSuccess;
+}
+
+bool CSavestateDatabase::GetSavestate(const std::string& savestatePath, ISavestate& save)
+{
+ bool bSuccess = false;
+
+ CLog::Log(LOGDEBUG, "Loading savestate from {}", CURL::GetRedacted(savestatePath));
+
+ std::vector<uint8_t> savestateData;
+
+ XFILE::CFile savestateFile;
+ if (savestateFile.Open(savestatePath, XFILE::READ_TRUNCATED))
+ {
+ int64_t size = savestateFile.GetLength();
+ if (size > 0)
+ {
+ savestateData.resize(static_cast<size_t>(size));
+
+ const ssize_t readLength = savestateFile.Read(savestateData.data(), savestateData.size());
+ if (readLength != static_cast<ssize_t>(savestateData.size()))
+ {
+ CLog::Log(LOGERROR, "Failed to read savestate {} of size {} bytes",
+ CURL::GetRedacted(savestatePath), size);
+ savestateData.clear();
+ }
+ }
+ else
+ CLog::Log(LOGERROR, "Failed to get savestate length: {}", CURL::GetRedacted(savestatePath));
+ }
+ else
+ CLog::Log(LOGERROR, "Failed to open savestate file {}", CURL::GetRedacted(savestatePath));
+
+ if (!savestateData.empty())
+ bSuccess = save.Deserialize(std::move(savestateData));
+
+ return bSuccess;
+}
+
+bool CSavestateDatabase::GetSavestatesNav(CFileItemList& items,
+ const std::string& gamePath,
+ const std::string& gameClient /* = "" */)
+{
+ const std::string savesFolder = MakePath(gamePath);
+
+ XFILE::CDirectory::CHints hints;
+ hints.mask = SAVESTATE_EXTENSION;
+
+ if (!XFILE::CDirectory::GetDirectory(savesFolder, items, hints))
+ return false;
+
+ if (!gameClient.empty())
+ {
+ for (int i = items.Size() - 1; i >= 0; i--)
+ {
+ std::unique_ptr<ISavestate> save = AllocateSavestate();
+ GetSavestate(items[i]->GetPath(), *save);
+ if (save->GameClientID() != gameClient)
+ items.Remove(i);
+ }
+ }
+
+ for (int i = 0; i < items.Size(); i++)
+ {
+ std::unique_ptr<ISavestate> savestate = AllocateSavestate();
+ GetSavestate(items[i]->GetPath(), *savestate);
+
+ GetSavestateItem(*savestate, items[i]->GetPath(), *items[i]);
+ }
+
+ return true;
+}
+
+void CSavestateDatabase::GetSavestateItem(const ISavestate& savestate,
+ const std::string& savestatePath,
+ CFileItem& item)
+{
+ CDateTime dateUTC = CDateTime::FromUTCDateTime(savestate.Created());
+
+ std::string label;
+ std::string label2;
+
+ // Date has the lowest priority of being shown
+ label = dateUTC.GetAsLocalizedDateTime(false, false);
+
+ // Label has the next priority
+ if (!savestate.Label().empty())
+ {
+ label2 = std::move(label);
+ label = savestate.Label();
+ }
+
+ // "Autosave" has the highest priority
+ if (savestate.Type() == SAVE_TYPE::AUTO)
+ {
+ label2 = std::move(label);
+ label = g_localizeStrings.Get(15316); // "Autosave"
+ }
+
+ item.SetLabel(label);
+ item.SetLabel2(label2);
+ item.SetPath(savestatePath);
+ item.SetArt("screenshot", MakeThumbnailPath(savestatePath));
+ item.SetProperty(SAVESTATE_LABEL, savestate.Label());
+ item.SetProperty(SAVESTATE_CAPTION, savestate.Caption());
+ item.SetProperty(SAVESTATE_GAME_CLIENT, savestate.GameClientID());
+ item.m_dateTime = dateUTC;
+}
+
+std::unique_ptr<ISavestate> CSavestateDatabase::RenameSavestate(const std::string& savestatePath,
+ const std::string& label)
+{
+ std::unique_ptr<ISavestate> savestate = AllocateSavestate();
+ if (!GetSavestate(savestatePath, *savestate))
+ return {};
+
+ std::unique_ptr<ISavestate> newSavestate = AllocateSavestate();
+
+ newSavestate->SetLabel(label);
+ newSavestate->SetCaption(savestate->Caption());
+ newSavestate->SetType(savestate->Type());
+ newSavestate->SetCreated(savestate->Created());
+ newSavestate->SetGameFileName(savestate->GameFileName());
+ newSavestate->SetTimestampFrames(savestate->TimestampFrames());
+ newSavestate->SetTimestampWallClock(savestate->TimestampWallClock());
+ newSavestate->SetGameClientID(savestate->GameClientID());
+ newSavestate->SetGameClientVersion(savestate->GameClientVersion());
+
+ size_t memorySize = savestate->GetMemorySize();
+ std::memcpy(newSavestate->GetMemoryBuffer(memorySize), savestate->GetMemoryData(), memorySize);
+
+ newSavestate->Finalize();
+
+ std::string path = savestatePath;
+ if (!AddSavestate(path, "", *newSavestate))
+ return {};
+
+ return newSavestate;
+}
+
+bool CSavestateDatabase::DeleteSavestate(const std::string& savestatePath)
+{
+ if (!XFILE::CFile::Delete(savestatePath))
+ {
+ CLog::Log(LOGERROR, "Failed to delete savestate file {}", CURL::GetRedacted(savestatePath));
+ return false;
+ }
+
+ XFILE::CFile::Delete(MakeThumbnailPath(savestatePath));
+ return true;
+}
+
+bool CSavestateDatabase::ClearSavestatesOfGame(const std::string& gamePath,
+ const std::string& gameClient /* = "" */)
+{
+ //! @todo
+ return false;
+}
+
+std::string CSavestateDatabase::MakeSavestatePath(const std::string& gamePath,
+ const CDateTime& creationTime)
+{
+ std::string path = MakePath(gamePath);
+ return URIUtils::AddFileToFolder(path, creationTime.GetAsSaveString() + SAVESTATE_EXTENSION);
+}
+
+std::string CSavestateDatabase::MakeThumbnailPath(const std::string& savestatePath)
+{
+ return URIUtils::ReplaceExtension(savestatePath, ".jpg");
+}
+
+std::string CSavestateDatabase::MakePath(const std::string& gamePath)
+{
+ if (!CreateFolderIfNotExists(SAVESTATE_BASE_FOLDER))
+ return "";
+
+ std::string gameName = URIUtils::GetFileName(gamePath);
+ std::string folderPath = URIUtils::AddFileToFolder(SAVESTATE_BASE_FOLDER, gameName);
+
+ if (!CreateFolderIfNotExists(folderPath))
+ return "";
+
+ return folderPath;
+}
+
+bool CSavestateDatabase::CreateFolderIfNotExists(const std::string& path)
+{
+ if (!XFILE::CDirectory::Exists(path))
+ {
+ if (!XFILE::CDirectory::Create(path))
+ {
+ CLog::Log(LOGERROR, "Failed to create folder: {}", path);
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h
new file mode 100644
index 0000000..b084771
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateDatabase.h
@@ -0,0 +1,61 @@
+/*
+ * 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 <memory>
+#include <string>
+
+class CDateTime;
+class CFileItem;
+class CFileItemList;
+
+namespace KODI
+{
+namespace RETRO
+{
+class ISavestate;
+
+class CSavestateDatabase
+{
+public:
+ CSavestateDatabase();
+ virtual ~CSavestateDatabase() = default;
+
+ static std::unique_ptr<ISavestate> AllocateSavestate();
+
+ bool AddSavestate(const std::string& savestatePath,
+ const std::string& gamePath,
+ const ISavestate& save);
+
+ bool GetSavestate(const std::string& savestatePath, ISavestate& save);
+
+ bool GetSavestatesNav(CFileItemList& items,
+ const std::string& gamePath,
+ const std::string& gameClient = "");
+
+ static void GetSavestateItem(const ISavestate& savestate,
+ const std::string& savestatePath,
+ CFileItem& item);
+
+ std::unique_ptr<ISavestate> RenameSavestate(const std::string& savestatePath,
+ const std::string& label);
+
+ bool DeleteSavestate(const std::string& savestatePath);
+
+ bool ClearSavestatesOfGame(const std::string& gamePath, const std::string& gameClient = "");
+
+ static std::string MakeSavestatePath(const std::string& gamePath, const CDateTime& creationTime);
+ static std::string MakeThumbnailPath(const std::string& savestatePath);
+
+private:
+ static std::string MakePath(const std::string& gamePath);
+ static bool CreateFolderIfNotExists(const std::string& path);
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.cpp b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.cpp
new file mode 100644
index 0000000..9b98bde
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.cpp
@@ -0,0 +1,637 @@
+/*
+ * Copyright (C) 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 "SavestateFlatBuffer.h"
+
+#include "XBDateTime.h"
+#include "savestate_generated.h"
+#include "utils/log.h"
+#include "video_generated.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+namespace
+{
+const uint8_t SCHEMA_VERSION = 3;
+const uint8_t SCHEMA_MIN_VERSION = 1;
+
+/*!
+ * \brief The initial size of the FlatBuffer's memory buffer
+ *
+ * 1024 is the default size in the FlatBuffers header. We might as well use
+ * this until our size requirements are more known.
+ */
+const size_t INITIAL_FLATBUFFER_SIZE = 1024;
+
+/*!
+ * \brief Translate the save type (RetroPlayer to FlatBuffers)
+ */
+SaveType TranslateType(SAVE_TYPE type)
+{
+ switch (type)
+ {
+ case SAVE_TYPE::AUTO:
+ return SaveType_Auto;
+ case SAVE_TYPE::MANUAL:
+ return SaveType_Manual;
+ default:
+ break;
+ }
+
+ return SaveType_Unknown;
+}
+
+/*!
+ * \brief Translate the save type (FlatBuffers to RetroPlayer)
+ */
+SAVE_TYPE TranslateType(SaveType type)
+{
+ switch (type)
+ {
+ case SaveType_Auto:
+ return SAVE_TYPE::AUTO;
+ case SaveType_Manual:
+ return SAVE_TYPE::MANUAL;
+ default:
+ break;
+ }
+
+ return SAVE_TYPE::UNKNOWN;
+}
+
+/*!
+ * \brief Translate the video pixel format (RetroPlayer to FlatBuffers)
+ */
+PixelFormat TranslatePixelFormat(AVPixelFormat pixelFormat)
+{
+ switch (pixelFormat)
+ {
+ case AV_PIX_FMT_RGBA:
+ return PixelFormat_RGBA_8888;
+
+ case AV_PIX_FMT_0RGB32:
+#if defined(__BIG_ENDIAN__)
+ return PixelFormat_XRGB_8888;
+#else
+ return PixelFormat_BGRX_8888;
+#endif
+
+ case AV_PIX_FMT_RGB565:
+#if defined(__BIG_ENDIAN__)
+ return PixelFormat_RGB_565_BE;
+#else
+ return PixelFormat_RGB_565_LE;
+#endif
+
+ case AV_PIX_FMT_RGB555:
+#if defined(__BIG_ENDIAN__)
+ return PixelFormat_RGB_555_BE;
+#else
+ return PixelFormat_RGB_555_LE;
+#endif
+
+ default:
+ break;
+ }
+
+ return PixelFormat_Unknown;
+}
+
+/*!
+ * \brief Translate the video pixel format (FlatBuffers to RetroPlayer)
+ */
+AVPixelFormat TranslatePixelFormat(PixelFormat pixelFormat)
+{
+ switch (pixelFormat)
+ {
+ case PixelFormat_RGBA_8888:
+ return AV_PIX_FMT_RGBA;
+
+ case PixelFormat_XRGB_8888:
+ return AV_PIX_FMT_0RGB;
+
+ case PixelFormat_BGRX_8888:
+ return AV_PIX_FMT_BGR0;
+
+ case PixelFormat_RGB_565_BE:
+ return AV_PIX_FMT_RGB565BE;
+
+ case PixelFormat_RGB_565_LE:
+ return AV_PIX_FMT_RGB565LE;
+
+ case PixelFormat_RGB_555_BE:
+ return AV_PIX_FMT_RGB555BE;
+
+ case PixelFormat_RGB_555_LE:
+ return AV_PIX_FMT_RGB555LE;
+
+ default:
+ break;
+ }
+
+ return AV_PIX_FMT_NONE;
+}
+
+/*!
+ * \brief Translate the video rotation (RetroPlayer to FlatBuffers)
+ */
+VideoRotation TranslateRotation(unsigned int rotationCCW)
+{
+ switch (rotationCCW)
+ {
+ case 0:
+ return VideoRotation_CCW_0;
+ case 90:
+ return VideoRotation_CCW_90;
+ case 180:
+ return VideoRotation_CCW_180;
+ case 270:
+ return VideoRotation_CCW_270;
+ default:
+ break;
+ }
+
+ return VideoRotation_CCW_0;
+}
+
+/*!
+ * \brief Translate the video rotation (RetroPlayer to FlatBuffers)
+ */
+unsigned int TranslateRotation(VideoRotation rotationCCW)
+{
+ switch (rotationCCW)
+ {
+ case VideoRotation_CCW_0:
+ return 0;
+ case VideoRotation_CCW_90:
+ return 90;
+ case VideoRotation_CCW_180:
+ return 180;
+ case VideoRotation_CCW_270:
+ return 270;
+ default:
+ break;
+ }
+
+ return 0;
+}
+} // namespace
+
+CSavestateFlatBuffer::CSavestateFlatBuffer()
+{
+ Reset();
+}
+
+CSavestateFlatBuffer::~CSavestateFlatBuffer() = default;
+
+void CSavestateFlatBuffer::Reset()
+{
+ m_builder.reset(new flatbuffers::FlatBufferBuilder(INITIAL_FLATBUFFER_SIZE));
+ m_data.clear();
+ m_savestate = nullptr;
+}
+
+bool CSavestateFlatBuffer::Serialize(const uint8_t*& data, size_t& size) const
+{
+ // Check if savestate was deserialized from vector or built with FlatBuffers
+ if (!m_data.empty())
+ {
+ data = m_data.data();
+ size = m_data.size();
+ }
+ else
+ {
+ data = m_builder->GetBufferPointer();
+ size = m_builder->GetSize();
+ }
+
+ return true;
+}
+
+SAVE_TYPE CSavestateFlatBuffer::Type() const
+{
+ if (m_savestate != nullptr)
+ return TranslateType(m_savestate->type());
+
+ return SAVE_TYPE::UNKNOWN;
+}
+
+void CSavestateFlatBuffer::SetType(SAVE_TYPE type)
+{
+ m_type = type;
+}
+
+uint8_t CSavestateFlatBuffer::Slot() const
+{
+ if (m_savestate != nullptr)
+ return m_savestate->slot();
+
+ return 0;
+}
+
+void CSavestateFlatBuffer::SetSlot(uint8_t slot)
+{
+ m_slot = slot;
+}
+
+std::string CSavestateFlatBuffer::Label() const
+{
+ std::string label;
+
+ if (m_savestate != nullptr && m_savestate->label())
+ label = m_savestate->label()->c_str();
+
+ return label;
+}
+
+void CSavestateFlatBuffer::SetLabel(const std::string& label)
+{
+ m_labelOffset.reset(new StringOffset{m_builder->CreateString(label)});
+}
+
+std::string CSavestateFlatBuffer::Caption() const
+{
+ std::string caption;
+
+ if (m_savestate != nullptr && m_savestate->caption())
+ caption = m_savestate->caption()->str();
+
+ return caption;
+}
+
+void CSavestateFlatBuffer::SetCaption(const std::string& caption)
+{
+ m_captionOffset = std::make_unique<StringOffset>(m_builder->CreateString(caption));
+}
+
+CDateTime CSavestateFlatBuffer::Created() const
+{
+ CDateTime createdUTC;
+
+ if (m_savestate != nullptr && m_savestate->created())
+ createdUTC.SetFromW3CDateTime(m_savestate->created()->c_str(), false);
+
+ return createdUTC;
+}
+
+void CSavestateFlatBuffer::SetCreated(const CDateTime& createdUTC)
+{
+ m_createdOffset =
+ std::make_unique<StringOffset>(m_builder->CreateString(createdUTC.GetAsW3CDateTime(true)));
+}
+
+std::string CSavestateFlatBuffer::GameFileName() const
+{
+ std::string gameFileName;
+
+ if (m_savestate != nullptr && m_savestate->game_file_name())
+ gameFileName = m_savestate->game_file_name()->c_str();
+
+ return gameFileName;
+}
+
+void CSavestateFlatBuffer::SetGameFileName(const std::string& gameFileName)
+{
+ m_gameFileNameOffset.reset(new StringOffset{m_builder->CreateString(gameFileName)});
+}
+
+uint64_t CSavestateFlatBuffer::TimestampFrames() const
+{
+ return m_savestate->timestamp_frames();
+}
+
+void CSavestateFlatBuffer::SetTimestampFrames(uint64_t timestampFrames)
+{
+ m_timestampFrames = timestampFrames;
+}
+
+double CSavestateFlatBuffer::TimestampWallClock() const
+{
+ if (m_savestate != nullptr)
+ return static_cast<double>(m_savestate->timestamp_wall_clock_ns()) / 1000.0 / 1000.0 / 1000.0;
+
+ return 0.0;
+}
+
+void CSavestateFlatBuffer::SetTimestampWallClock(double timestampWallClock)
+{
+ m_timestampWallClock = timestampWallClock;
+}
+
+std::string CSavestateFlatBuffer::GameClientID() const
+{
+ std::string gameClientId;
+
+ if (m_savestate != nullptr && m_savestate->emulator_addon_id())
+ gameClientId = m_savestate->emulator_addon_id()->c_str();
+
+ return gameClientId;
+}
+
+void CSavestateFlatBuffer::SetGameClientID(const std::string& gameClientId)
+{
+ m_emulatorAddonIdOffset.reset(new StringOffset{m_builder->CreateString(gameClientId)});
+}
+
+std::string CSavestateFlatBuffer::GameClientVersion() const
+{
+ std::string gameClientVersion;
+
+ if (m_savestate != nullptr && m_savestate->emulator_version())
+ gameClientVersion = m_savestate->emulator_version()->c_str();
+
+ return gameClientVersion;
+}
+
+void CSavestateFlatBuffer::SetGameClientVersion(const std::string& gameClientVersion)
+{
+ m_emulatorVersionOffset.reset(new StringOffset{m_builder->CreateString(gameClientVersion)});
+}
+
+AVPixelFormat CSavestateFlatBuffer::GetPixelFormat() const
+{
+ if (m_savestate != nullptr)
+ return TranslatePixelFormat(m_savestate->pixel_format());
+
+ return AV_PIX_FMT_NONE;
+}
+
+void CSavestateFlatBuffer::SetPixelFormat(AVPixelFormat pixelFormat)
+{
+ m_pixelFormat = pixelFormat;
+}
+
+unsigned int CSavestateFlatBuffer::GetNominalWidth() const
+{
+ if (m_savestate != nullptr)
+ return m_savestate->nominal_width();
+
+ return 0;
+}
+
+void CSavestateFlatBuffer::SetNominalWidth(unsigned int nominalWidth)
+{
+ m_nominalWidth = nominalWidth;
+}
+
+unsigned int CSavestateFlatBuffer::GetNominalHeight() const
+{
+ if (m_savestate != nullptr)
+ return m_savestate->nominal_height();
+
+ return 0;
+}
+
+void CSavestateFlatBuffer::SetNominalHeight(unsigned int nominalHeight)
+{
+ m_nominalHeight = nominalHeight;
+}
+
+unsigned int CSavestateFlatBuffer::GetMaxWidth() const
+{
+ if (m_savestate != nullptr)
+ return m_savestate->max_width();
+
+ return 0;
+}
+
+void CSavestateFlatBuffer::SetMaxWidth(unsigned int maxWidth)
+{
+ m_maxWidth = maxWidth;
+}
+
+unsigned int CSavestateFlatBuffer::GetMaxHeight() const
+{
+ if (m_savestate != nullptr)
+ return m_savestate->max_height();
+
+ return 0;
+}
+
+void CSavestateFlatBuffer::SetMaxHeight(unsigned int maxHeight)
+{
+ m_maxHeight = maxHeight;
+}
+
+float CSavestateFlatBuffer::GetPixelAspectRatio() const
+{
+ if (m_savestate != nullptr)
+ return m_savestate->pixel_aspect_ratio();
+
+ return 0.0f;
+}
+
+void CSavestateFlatBuffer::SetPixelAspectRatio(float pixelAspectRatio)
+{
+ m_pixelAspectRatio = pixelAspectRatio;
+}
+
+const uint8_t* CSavestateFlatBuffer::GetVideoData() const
+{
+ if (m_savestate != nullptr && m_savestate->video_data())
+ return m_savestate->video_data()->data();
+
+ return nullptr;
+}
+
+size_t CSavestateFlatBuffer::GetVideoSize() const
+{
+ if (m_savestate != nullptr && m_savestate->video_data())
+ return m_savestate->video_data()->size();
+
+ return 0;
+}
+
+uint8_t* CSavestateFlatBuffer::GetVideoBuffer(size_t size)
+{
+ uint8_t* videoBuffer = nullptr;
+
+ m_videoDataOffset =
+ std::make_unique<VectorOffset>(m_builder->CreateUninitializedVector(size, &videoBuffer));
+
+ return videoBuffer;
+}
+
+unsigned int CSavestateFlatBuffer::GetVideoWidth() const
+{
+ if (m_savestate != nullptr)
+ return m_savestate->video_width();
+
+ return 0;
+}
+
+void CSavestateFlatBuffer::SetVideoWidth(unsigned int videoWidth)
+{
+ m_videoWidth = videoWidth;
+}
+
+unsigned int CSavestateFlatBuffer::GetVideoHeight() const
+{
+ if (m_savestate != nullptr)
+ return m_savestate->video_height();
+
+ return 0;
+}
+
+void CSavestateFlatBuffer::SetVideoHeight(unsigned int videoHeight)
+{
+ m_videoHeight = videoHeight;
+}
+
+unsigned int CSavestateFlatBuffer::GetRotationDegCCW() const
+{
+ if (m_savestate != nullptr)
+ return TranslateRotation(m_savestate->rotation_ccw());
+
+ return 0;
+}
+
+void CSavestateFlatBuffer::SetRotationDegCCW(unsigned int rotationCCW)
+{
+ m_rotationCCW = rotationCCW;
+}
+
+const uint8_t* CSavestateFlatBuffer::GetMemoryData() const
+{
+ if (m_savestate != nullptr && m_savestate->memory_data())
+ return m_savestate->memory_data()->data();
+
+ return nullptr;
+}
+
+size_t CSavestateFlatBuffer::GetMemorySize() const
+{
+ if (m_savestate != nullptr && m_savestate->memory_data())
+ return m_savestate->memory_data()->size();
+
+ return 0;
+}
+
+uint8_t* CSavestateFlatBuffer::GetMemoryBuffer(size_t size)
+{
+ uint8_t* memoryBuffer = nullptr;
+
+ m_memoryDataOffset.reset(
+ new VectorOffset{m_builder->CreateUninitializedVector(size, &memoryBuffer)});
+
+ return memoryBuffer;
+}
+
+void CSavestateFlatBuffer::Finalize()
+{
+ // Helper class to build the nested Savestate table
+ SavestateBuilder savestateBuilder(*m_builder);
+
+ savestateBuilder.add_version(SCHEMA_VERSION);
+
+ savestateBuilder.add_type(TranslateType(m_type));
+
+ savestateBuilder.add_slot(m_slot);
+
+ if (m_labelOffset)
+ {
+ savestateBuilder.add_label(*m_labelOffset);
+ m_labelOffset.reset();
+ }
+
+ if (m_captionOffset)
+ {
+ savestateBuilder.add_caption(*m_captionOffset);
+ m_captionOffset.reset();
+ }
+
+ if (m_createdOffset)
+ {
+ savestateBuilder.add_created(*m_createdOffset);
+ m_createdOffset.reset();
+ }
+
+ if (m_gameFileNameOffset)
+ {
+ savestateBuilder.add_game_file_name(*m_gameFileNameOffset);
+ m_gameFileNameOffset.reset();
+ }
+
+ savestateBuilder.add_timestamp_frames(m_timestampFrames);
+
+ const uint64_t wallClockNs =
+ static_cast<uint64_t>(m_timestampWallClock * 1000.0 * 1000.0 * 1000.0);
+ savestateBuilder.add_timestamp_wall_clock_ns(wallClockNs);
+
+ if (m_emulatorAddonIdOffset)
+ {
+ savestateBuilder.add_emulator_addon_id(*m_emulatorAddonIdOffset);
+ m_emulatorAddonIdOffset.reset();
+ }
+
+ if (m_emulatorVersionOffset)
+ {
+ savestateBuilder.add_emulator_version(*m_emulatorVersionOffset);
+ m_emulatorVersionOffset.reset();
+ }
+
+ savestateBuilder.add_pixel_format(TranslatePixelFormat(m_pixelFormat));
+
+ savestateBuilder.add_nominal_width(m_nominalWidth);
+
+ savestateBuilder.add_nominal_height(m_nominalHeight);
+
+ savestateBuilder.add_max_width(m_maxWidth);
+
+ savestateBuilder.add_max_height(m_maxHeight);
+
+ savestateBuilder.add_pixel_aspect_ratio(m_pixelAspectRatio);
+
+ if (m_videoDataOffset)
+ {
+ savestateBuilder.add_video_data(*m_videoDataOffset);
+ m_videoDataOffset.reset();
+ }
+
+ savestateBuilder.add_video_width(m_videoWidth);
+
+ savestateBuilder.add_video_height(m_videoHeight);
+
+ savestateBuilder.add_rotation_ccw(TranslateRotation(m_rotationCCW));
+
+ if (m_memoryDataOffset)
+ {
+ savestateBuilder.add_memory_data(*m_memoryDataOffset);
+ m_memoryDataOffset.reset();
+ }
+
+ auto savestate = savestateBuilder.Finish();
+ FinishSavestateBuffer(*m_builder, savestate);
+
+ m_savestate = GetSavestate(m_builder->GetBufferPointer());
+}
+
+bool CSavestateFlatBuffer::Deserialize(std::vector<uint8_t> data)
+{
+ flatbuffers::Verifier verifier(data.data(), data.size());
+ if (VerifySavestateBuffer(verifier))
+ {
+ const Savestate* savestate = GetSavestate(data.data());
+
+ if (savestate->version() < SCHEMA_MIN_VERSION)
+ {
+ CLog::Log(LOGERROR,
+ "RetroPlayer[SAVE): Schema version {} not supported, must be at least version {}",
+ savestate->version(), SCHEMA_MIN_VERSION);
+ }
+ else
+ {
+ m_data = std::move(data);
+ m_savestate = GetSavestate(m_data.data());
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.h b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.h
new file mode 100644
index 0000000..fa42a9b
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateFlatBuffer.h
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 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 "ISavestate.h"
+
+#include <memory>
+
+#include <flatbuffers/flatbuffers.h>
+
+namespace KODI
+{
+namespace RETRO
+{
+struct Savestate;
+struct SavestateBuilder;
+
+class CSavestateFlatBuffer : public ISavestate
+{
+public:
+ CSavestateFlatBuffer();
+ ~CSavestateFlatBuffer() override;
+
+ // Implementation of ISavestate
+ void Reset() override;
+ bool Serialize(const uint8_t*& data, size_t& size) const override;
+ SAVE_TYPE Type() const override;
+ uint8_t Slot() const override;
+ std::string Label() const override;
+ std::string Caption() const override;
+ CDateTime Created() const override;
+ std::string GameFileName() const override;
+ uint64_t TimestampFrames() const override;
+ double TimestampWallClock() const override;
+ std::string GameClientID() const override;
+ std::string GameClientVersion() const override;
+ AVPixelFormat GetPixelFormat() const override;
+ unsigned int GetNominalWidth() const override;
+ unsigned int GetNominalHeight() const override;
+ unsigned int GetMaxWidth() const override;
+ unsigned int GetMaxHeight() const override;
+ float GetPixelAspectRatio() const override;
+ const uint8_t* GetVideoData() const override;
+ size_t GetVideoSize() const override;
+ unsigned int GetVideoWidth() const override;
+ unsigned int GetVideoHeight() const override;
+ unsigned int GetRotationDegCCW() const override;
+ const uint8_t* GetMemoryData() const override;
+ size_t GetMemorySize() const override;
+ void SetType(SAVE_TYPE type) override;
+ void SetSlot(uint8_t slot) override;
+ void SetLabel(const std::string& label) override;
+ void SetCaption(const std::string& caption) override;
+ void SetCreated(const CDateTime& createdUTC) override;
+ void SetGameFileName(const std::string& gameFileName) override;
+ void SetTimestampFrames(uint64_t timestampFrames) override;
+ void SetTimestampWallClock(double timestampWallClock) override;
+ void SetGameClientID(const std::string& gameClient) override;
+ void SetGameClientVersion(const std::string& gameClient) override;
+ void SetPixelFormat(AVPixelFormat pixelFormat) override;
+ void SetNominalWidth(unsigned int nominalWidth) override;
+ void SetNominalHeight(unsigned int nominalHeight) override;
+ void SetMaxWidth(unsigned int maxWidth) override;
+ void SetMaxHeight(unsigned int maxHeight) override;
+ void SetPixelAspectRatio(float pixelAspectRatio) override;
+ uint8_t* GetVideoBuffer(size_t size) override;
+ void SetVideoWidth(unsigned int videoWidth) override;
+ void SetVideoHeight(unsigned int videoHeight) override;
+ void SetRotationDegCCW(unsigned int rotationCCW) override;
+ uint8_t* GetMemoryBuffer(size_t size) override;
+ void Finalize() override;
+ bool Deserialize(std::vector<uint8_t> data) override;
+
+private:
+ /*!
+ * \brief Helper class to hold data needed in creation of a FlatBuffer
+ *
+ * The builder is used when deserializing from individual fields.
+ */
+ std::unique_ptr<flatbuffers::FlatBufferBuilder> m_builder;
+
+ /*!
+ * \brief System memory storage (for deserializing savestates)
+ *
+ * This memory is used when deserializing from a vector.
+ */
+ std::vector<uint8_t> m_data;
+
+ /*!
+ * \brief FlatBuffer struct used for accessing data
+ */
+ const Savestate* m_savestate = nullptr;
+
+ using StringOffset = flatbuffers::Offset<flatbuffers::String>;
+ using VectorOffset = flatbuffers::Offset<flatbuffers::Vector<uint8_t>>;
+
+ // Temporary deserialization variables
+ SAVE_TYPE m_type = SAVE_TYPE::UNKNOWN;
+ uint8_t m_slot = 0;
+ std::unique_ptr<StringOffset> m_labelOffset;
+ std::unique_ptr<StringOffset> m_captionOffset;
+ std::unique_ptr<StringOffset> m_createdOffset;
+ std::unique_ptr<StringOffset> m_gameFileNameOffset;
+ uint64_t m_timestampFrames = 0;
+ double m_timestampWallClock = 0.0;
+ std::unique_ptr<StringOffset> m_emulatorAddonIdOffset;
+ std::unique_ptr<StringOffset> m_emulatorVersionOffset;
+ AVPixelFormat m_pixelFormat{AV_PIX_FMT_NONE};
+ unsigned int m_nominalWidth{0};
+ unsigned int m_nominalHeight{0};
+ unsigned int m_maxWidth{0};
+ unsigned int m_maxHeight{0};
+ float m_pixelAspectRatio{0.0f};
+ std::unique_ptr<VectorOffset> m_videoDataOffset;
+ unsigned int m_videoWidth{0};
+ unsigned int m_videoHeight{0};
+ unsigned int m_rotationCCW{0};
+ std::unique_ptr<VectorOffset> m_memoryDataOffset;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/savestates/SavestateTypes.h b/xbmc/cores/RetroPlayer/savestates/SavestateTypes.h
new file mode 100644
index 0000000..e4bbc9b
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/savestates/SavestateTypes.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016-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
+
+namespace KODI
+{
+namespace RETRO
+{
+/*!
+ * \brief Type of save action, either:
+ *
+ * - automatic (saving was not prompted by the user)
+ * - manual (user manually prompted the save)
+ */
+enum class SAVE_TYPE
+{
+ UNKNOWN,
+ AUTO,
+ MANUAL,
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/streams/CMakeLists.txt b/xbmc/cores/RetroPlayer/streams/CMakeLists.txt
new file mode 100644
index 0000000..b5acbc9
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/CMakeLists.txt
@@ -0,0 +1,15 @@
+set(SOURCES RetroPlayerAudio.cpp
+ RetroPlayerStreamTypes.cpp
+ RetroPlayerVideo.cpp
+ RPStreamManager.cpp
+)
+
+set(HEADERS IRetroPlayerStream.h
+ IStreamManager.h
+ RetroPlayerAudio.h
+ RetroPlayerStreamTypes.h
+ RetroPlayerVideo.h
+ RPStreamManager.h
+)
+
+core_add_library(retroplayer_streams)
diff --git a/xbmc/cores/RetroPlayer/streams/IRetroPlayerStream.h b/xbmc/cores/RetroPlayer/streams/IRetroPlayerStream.h
new file mode 100644
index 0000000..dddf3ff
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/IRetroPlayerStream.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 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 "RetroPlayerStreamTypes.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+
+struct StreamProperties
+{
+};
+
+struct StreamBuffer
+{
+};
+
+struct StreamPacket
+{
+};
+
+class IRetroPlayerStream
+{
+public:
+ virtual ~IRetroPlayerStream() = default;
+
+ /*!
+ * \brief Open a stream
+ *
+ * \return True if the stream was opened, false otherwise
+ */
+ virtual bool OpenStream(const StreamProperties& properties) = 0;
+
+ /*!
+ * \brief Get a buffer for zero-copy stream data
+ *
+ * \param width The framebuffer width, or 0 for no width specified
+ * \param height The framebuffer height, or 0 for no height specified
+ * \param[out] buffer The buffer, or unmodified if false is returned
+ *
+ * \return True if a buffer was returned, false otherwise
+ */
+ virtual bool GetStreamBuffer(unsigned int width, unsigned int height, StreamBuffer& buffer) = 0;
+
+ /*!
+ * \brief Add a data packet to a stream
+ *
+ * \param packet The data packet
+ */
+ virtual void AddStreamData(const StreamPacket& packet) = 0;
+
+ /*!
+ * \brief Close the stream
+ */
+ virtual void CloseStream() = 0;
+};
+
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/streams/IStreamManager.h b/xbmc/cores/RetroPlayer/streams/IStreamManager.h
new file mode 100644
index 0000000..b490642
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/IStreamManager.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 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 "RetroPlayerStreamTypes.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+
+class IStreamManager
+{
+public:
+ virtual ~IStreamManager() = default;
+
+ /*!
+ * \brief Create a stream for gameplay data
+ *
+ * \param streamType The stream type
+ *
+ * \return A stream handle, or empty on failure
+ */
+ virtual StreamPtr CreateStream(StreamType streamType) = 0;
+
+ /*!
+ * \brief Free the specified stream
+ *
+ * \param stream The stream to close
+ */
+ virtual void CloseStream(StreamPtr stream) = 0;
+};
+
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/streams/RPStreamManager.cpp b/xbmc/cores/RetroPlayer/streams/RPStreamManager.cpp
new file mode 100644
index 0000000..2682d4d
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/RPStreamManager.cpp
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 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 "RPStreamManager.h"
+
+#include "IRetroPlayerStream.h"
+#include "RetroPlayerAudio.h"
+#include "RetroPlayerVideo.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRPStreamManager::CRPStreamManager(CRPRenderManager& renderManager, CRPProcessInfo& processInfo)
+ : m_renderManager(renderManager), m_processInfo(processInfo)
+{
+}
+
+void CRPStreamManager::EnableAudio(bool bEnable)
+{
+ if (m_audioStream != nullptr)
+ m_audioStream->Enable(bEnable);
+}
+
+StreamPtr CRPStreamManager::CreateStream(StreamType streamType)
+{
+ switch (streamType)
+ {
+ case StreamType::AUDIO:
+ {
+ // Save pointer to audio stream
+ m_audioStream = new CRetroPlayerAudio(m_processInfo);
+
+ return StreamPtr(m_audioStream);
+ }
+ case StreamType::VIDEO:
+ case StreamType::SW_BUFFER:
+ {
+ return StreamPtr(new CRetroPlayerVideo(m_renderManager, m_processInfo));
+ }
+ case StreamType::HW_BUFFER:
+ {
+ // return StreamPtr(new CRetroPlayerHardware(m_renderManager, m_processInfo)); //! @todo
+ }
+ default:
+ break;
+ }
+
+ return StreamPtr();
+}
+
+void CRPStreamManager::CloseStream(StreamPtr stream)
+{
+ if (stream)
+ {
+ if (stream.get() == m_audioStream)
+ m_audioStream = nullptr;
+
+ stream->CloseStream();
+ }
+}
diff --git a/xbmc/cores/RetroPlayer/streams/RPStreamManager.h b/xbmc/cores/RetroPlayer/streams/RPStreamManager.h
new file mode 100644
index 0000000..3615329
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/RPStreamManager.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 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 "IStreamManager.h"
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRetroPlayerAudio;
+class CRPProcessInfo;
+class CRPRenderManager;
+
+class CRPStreamManager : public IStreamManager
+{
+public:
+ CRPStreamManager(CRPRenderManager& renderManager, CRPProcessInfo& processInfo);
+ ~CRPStreamManager() override = default;
+
+ void EnableAudio(bool bEnable);
+
+ // Implementation of IStreamManager
+ StreamPtr CreateStream(StreamType streamType) override;
+ void CloseStream(StreamPtr stream) override;
+
+private:
+ // Construction parameters
+ CRPRenderManager& m_renderManager;
+ CRPProcessInfo& m_processInfo;
+
+ // Stream parameters
+ CRetroPlayerAudio* m_audioStream = nullptr;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/streams/RetroPlayerAudio.cpp b/xbmc/cores/RetroPlayer/streams/RetroPlayerAudio.cpp
new file mode 100644
index 0000000..d4e5a05
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/RetroPlayerAudio.cpp
@@ -0,0 +1,142 @@
+/*
+ * 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 "RetroPlayerAudio.h"
+
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "cores/AudioEngine/Interfaces/AEStream.h"
+#include "cores/AudioEngine/Utils/AEChannelInfo.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "cores/RetroPlayer/audio/AudioTranslator.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "utils/log.h"
+
+#include <cmath>
+
+using namespace KODI;
+using namespace RETRO;
+
+const double MAX_DELAY = 0.3; // seconds
+
+CRetroPlayerAudio::CRetroPlayerAudio(CRPProcessInfo& processInfo)
+ : m_processInfo(processInfo), m_pAudioStream(nullptr), m_bAudioEnabled(true)
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[AUDIO]: Initializing audio");
+}
+
+CRetroPlayerAudio::~CRetroPlayerAudio()
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[AUDIO]: Deinitializing audio");
+
+ CloseStream();
+}
+
+bool CRetroPlayerAudio::OpenStream(const StreamProperties& properties)
+{
+ const AudioStreamProperties& audioProperties =
+ static_cast<const AudioStreamProperties&>(properties);
+
+ const AEDataFormat pcmFormat = CAudioTranslator::TranslatePCMFormat(audioProperties.format);
+ if (pcmFormat == AE_FMT_INVALID)
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[AUDIO]: Unknown PCM format: {}",
+ static_cast<int>(audioProperties.format));
+ return false;
+ }
+
+ unsigned int iSampleRate = static_cast<unsigned int>(std::round(audioProperties.sampleRate));
+ if (iSampleRate == 0)
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[AUDIO]: Invalid samplerate: {:f}", audioProperties.sampleRate);
+ return false;
+ }
+
+ CAEChannelInfo channelLayout;
+ for (auto it = audioProperties.channelMap.begin(); it != audioProperties.channelMap.end(); ++it)
+ {
+ AEChannel channel = CAudioTranslator::TranslateAudioChannel(*it);
+ if (channel == AE_CH_NULL)
+ break;
+
+ channelLayout += channel;
+ }
+
+ if (!channelLayout.IsLayoutValid())
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[AUDIO]: Empty channel layout");
+ return false;
+ }
+
+ if (m_pAudioStream != nullptr)
+ CloseStream();
+
+ IAE* audioEngine = CServiceBroker::GetActiveAE();
+ if (audioEngine == nullptr)
+ return false;
+
+ CLog::Log(
+ LOGINFO,
+ "RetroPlayer[AUDIO]: Creating audio stream, format = {}, sample rate = {}, channels = {}",
+ CAEUtil::DataFormatToStr(pcmFormat), iSampleRate, channelLayout.Count());
+
+ AEAudioFormat audioFormat;
+ audioFormat.m_dataFormat = pcmFormat;
+ audioFormat.m_sampleRate = iSampleRate;
+ audioFormat.m_channelLayout = channelLayout;
+ m_pAudioStream = audioEngine->MakeStream(audioFormat);
+
+ if (m_pAudioStream == nullptr)
+ {
+ CLog::Log(LOGERROR, "RetroPlayer[AUDIO]: Failed to create audio stream");
+ return false;
+ }
+
+ m_processInfo.SetAudioChannels(audioFormat.m_channelLayout);
+ m_processInfo.SetAudioSampleRate(audioFormat.m_sampleRate);
+ m_processInfo.SetAudioBitsPerSample(CAEUtil::DataFormatToUsedBits(audioFormat.m_dataFormat));
+
+ return true;
+}
+
+void CRetroPlayerAudio::AddStreamData(const StreamPacket& packet)
+{
+ const AudioStreamPacket& audioPacket = static_cast<const AudioStreamPacket&>(packet);
+
+ if (m_bAudioEnabled)
+ {
+ if (m_pAudioStream)
+ {
+ const double delaySecs = m_pAudioStream->GetDelay();
+
+ const size_t frameSize = m_pAudioStream->GetChannelCount() *
+ (CAEUtil::DataFormatToBits(m_pAudioStream->GetDataFormat()) >> 3);
+
+ const unsigned int frameCount = static_cast<unsigned int>(audioPacket.size / frameSize);
+
+ if (delaySecs > MAX_DELAY)
+ {
+ m_pAudioStream->Flush();
+ CLog::Log(LOGDEBUG, "RetroPlayer[AUDIO]: Audio delay ({:0.2f} ms) is too high - flushing",
+ delaySecs * 1000);
+ }
+
+ m_pAudioStream->AddData(&audioPacket.data, 0, frameCount, nullptr);
+ }
+ }
+}
+
+void CRetroPlayerAudio::CloseStream()
+{
+ if (m_pAudioStream)
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[AUDIO]: Closing audio stream");
+
+ m_pAudioStream.reset();
+ }
+}
diff --git a/xbmc/cores/RetroPlayer/streams/RetroPlayerAudio.h b/xbmc/cores/RetroPlayer/streams/RetroPlayerAudio.h
new file mode 100644
index 0000000..184fd54
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/RetroPlayerAudio.h
@@ -0,0 +1,67 @@
+/*
+ * 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 "IRetroPlayerStream.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+
+#include <memory>
+
+class IAEStream;
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfo;
+
+struct AudioStreamProperties : public StreamProperties
+{
+ AudioStreamProperties(PCMFormat format, double sampleRate, AudioChannelMap channelMap)
+ : format(format), sampleRate(sampleRate), channelMap(channelMap)
+ {
+ }
+
+ PCMFormat format;
+ double sampleRate;
+ AudioChannelMap channelMap;
+};
+
+struct AudioStreamPacket : public StreamPacket
+{
+ AudioStreamPacket(const uint8_t* data, size_t size) : data(data), size(size) {}
+
+ const uint8_t* data;
+ size_t size;
+};
+
+class CRetroPlayerAudio : public IRetroPlayerStream
+{
+public:
+ explicit CRetroPlayerAudio(CRPProcessInfo& processInfo);
+ ~CRetroPlayerAudio() override;
+
+ void Enable(bool bEnabled) { m_bAudioEnabled = bEnabled; }
+
+ // implementation of IRetroPlayerStream
+ bool OpenStream(const StreamProperties& properties) override;
+ bool GetStreamBuffer(unsigned int width, unsigned int height, StreamBuffer& buffer) override
+ {
+ return false;
+ }
+ void AddStreamData(const StreamPacket& packet) override;
+ void CloseStream() override;
+
+private:
+ CRPProcessInfo& m_processInfo;
+ IAE::StreamPtr m_pAudioStream;
+ bool m_bAudioEnabled;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/streams/RetroPlayerStreamTypes.cpp b/xbmc/cores/RetroPlayer/streams/RetroPlayerStreamTypes.cpp
new file mode 100644
index 0000000..7f7d0de
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/RetroPlayerStreamTypes.cpp
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 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 "RetroPlayerStreamTypes.h"
+
+#include "IRetroPlayerStream.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+void DeleteStream::operator()(IRetroPlayerStream* stream)
+{
+ delete stream;
+}
diff --git a/xbmc/cores/RetroPlayer/streams/RetroPlayerStreamTypes.h b/xbmc/cores/RetroPlayer/streams/RetroPlayerStreamTypes.h
new file mode 100644
index 0000000..aa5686a
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/RetroPlayerStreamTypes.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 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 <array>
+#include <memory>
+
+namespace KODI
+{
+namespace RETRO
+{
+class IRetroPlayerStream;
+
+struct DeleteStream
+{
+ void operator()(IRetroPlayerStream* stream);
+};
+
+using StreamPtr = std::unique_ptr<IRetroPlayerStream, DeleteStream>;
+
+enum class StreamType
+{
+ AUDIO,
+ VIDEO,
+ SW_BUFFER,
+ HW_BUFFER,
+};
+
+enum class PCMFormat
+{
+ FMT_UNKNOWN,
+ FMT_S16NE,
+};
+
+enum class AudioChannel
+{
+ CH_NULL, // Channel list terminator
+ CH_FL,
+ CH_FR,
+ CH_FC,
+ CH_LFE,
+ CH_BL,
+ CH_BR,
+ CH_FLOC,
+ CH_FROC,
+ CH_BC,
+ CH_SL,
+ CH_SR,
+ CH_TFL,
+ CH_TFR,
+ CH_TFC,
+ CH_TC,
+ CH_TBL,
+ CH_TBR,
+ CH_TBC,
+ CH_BLOC,
+ CH_BROC,
+ CH_COUNT
+};
+
+using AudioChannelMap = std::array<AudioChannel, static_cast<unsigned int>(AudioChannel::CH_COUNT)>;
+
+enum class PixelFormat
+{
+ FMT_UNKNOWN,
+ FMT_0RGB8888,
+ FMT_RGB565,
+ FMT_0RGB1555,
+};
+
+enum class VideoRotation
+{
+ ROTATION_0,
+ ROTATION_90_CCW,
+ ROTATION_180_CCW,
+ ROTATION_270_CCW,
+};
+
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/streams/RetroPlayerVideo.cpp b/xbmc/cores/RetroPlayer/streams/RetroPlayerVideo.cpp
new file mode 100644
index 0000000..14c66a8
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/RetroPlayerVideo.cpp
@@ -0,0 +1,116 @@
+/*
+ * 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 "RetroPlayerVideo.h"
+
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "cores/RetroPlayer/rendering/RPRenderManager.h"
+#include "cores/RetroPlayer/rendering/RenderTranslator.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CRetroPlayerVideo::CRetroPlayerVideo(CRPRenderManager& renderManager, CRPProcessInfo& processInfo)
+ : m_renderManager(renderManager), m_processInfo(processInfo)
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[VIDEO]: Initializing video");
+
+ m_renderManager.Initialize();
+}
+
+CRetroPlayerVideo::~CRetroPlayerVideo()
+{
+ CLog::Log(LOGDEBUG, "RetroPlayer[VIDEO]: Deinitializing video");
+
+ CloseStream();
+ m_renderManager.Deinitialize();
+}
+
+bool CRetroPlayerVideo::OpenStream(const StreamProperties& properties)
+{
+ const VideoStreamProperties& videoProperties =
+ static_cast<const VideoStreamProperties&>(properties);
+
+ if (m_bOpen)
+ {
+ CloseStream();
+ m_bOpen = false;
+ }
+
+ const AVPixelFormat pixfmt = videoProperties.pixfmt;
+ const unsigned int nominalWidth = videoProperties.nominalWidth;
+ const unsigned int nominalHeight = videoProperties.nominalHeight;
+ const unsigned int maxWidth = videoProperties.maxWidth;
+ const unsigned int maxHeight = videoProperties.maxHeight;
+ const float pixelAspectRatio = videoProperties.pixelAspectRatio;
+
+ CLog::Log(LOGDEBUG,
+ "RetroPlayer[VIDEO]: Creating video stream - format {}, nominal {}x{}, max {}x{}",
+ CRenderTranslator::TranslatePixelFormat(pixfmt), nominalWidth, nominalHeight, maxWidth,
+ maxHeight);
+
+ m_processInfo.SetVideoPixelFormat(pixfmt);
+ m_processInfo.SetVideoDimensions(nominalWidth, nominalHeight); // Report nominal height for now
+
+ if (m_renderManager.Configure(pixfmt, nominalWidth, nominalHeight, maxWidth, maxHeight,
+ pixelAspectRatio))
+ m_bOpen = true;
+
+ return m_bOpen;
+}
+
+bool CRetroPlayerVideo::GetStreamBuffer(unsigned int width,
+ unsigned int height,
+ StreamBuffer& buffer)
+{
+ VideoStreamBuffer& videoBuffer = static_cast<VideoStreamBuffer&>(buffer);
+
+ if (m_bOpen)
+ return m_renderManager.GetVideoBuffer(width, height, videoBuffer);
+
+ return false;
+}
+
+void CRetroPlayerVideo::AddStreamData(const StreamPacket& packet)
+{
+ const VideoStreamPacket& videoPacket = static_cast<const VideoStreamPacket&>(packet);
+
+ if (m_bOpen)
+ {
+ unsigned int orientationDegCCW = 0;
+ switch (videoPacket.rotation)
+ {
+ case VideoRotation::ROTATION_90_CCW:
+ orientationDegCCW = 90;
+ break;
+ case VideoRotation::ROTATION_180_CCW:
+ orientationDegCCW = 180;
+ break;
+ case VideoRotation::ROTATION_270_CCW:
+ orientationDegCCW = 270;
+ break;
+ default:
+ break;
+ }
+
+ m_renderManager.AddFrame(videoPacket.data, videoPacket.size, videoPacket.width,
+ videoPacket.height, orientationDegCCW);
+ }
+}
+
+void CRetroPlayerVideo::CloseStream()
+{
+ if (m_bOpen)
+ {
+ CLog::Log(LOGDEBUG, "RetroPlayer[VIDEO]: Closing video stream");
+
+ m_renderManager.Flush();
+ m_bOpen = false;
+ }
+}
diff --git a/xbmc/cores/RetroPlayer/streams/RetroPlayerVideo.h b/xbmc/cores/RetroPlayer/streams/RetroPlayerVideo.h
new file mode 100644
index 0000000..8d153ab
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/RetroPlayerVideo.h
@@ -0,0 +1,112 @@
+/*
+ * 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 "IRetroPlayerStream.h"
+#include "cores/RetroPlayer/RetroPlayerTypes.h"
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+namespace KODI
+{
+namespace RETRO
+{
+class CRPProcessInfo;
+class CRPRenderManager;
+
+struct VideoStreamProperties : public StreamProperties
+{
+ VideoStreamProperties(AVPixelFormat pixfmt,
+ unsigned int nominalWidth,
+ unsigned int nominalHeight,
+ unsigned int maxWidth,
+ unsigned int maxHeight,
+ float pixelAspectRatio)
+ : pixfmt(pixfmt),
+ nominalWidth(nominalWidth),
+ nominalHeight(nominalHeight),
+ maxWidth(maxWidth),
+ maxHeight(maxHeight),
+ pixelAspectRatio(pixelAspectRatio)
+ {
+ }
+
+ AVPixelFormat pixfmt;
+ unsigned int nominalWidth;
+ unsigned int nominalHeight;
+ unsigned int maxWidth;
+ unsigned int maxHeight;
+ float pixelAspectRatio;
+};
+
+struct VideoStreamBuffer : public StreamBuffer
+{
+ VideoStreamBuffer() = default;
+
+ VideoStreamBuffer(
+ AVPixelFormat pixfmt, uint8_t* data, size_t size, DataAccess access, DataAlignment alignment)
+ : pixfmt(pixfmt), data(data), size(size), access(access), alignment(alignment)
+ {
+ }
+
+ AVPixelFormat pixfmt{AV_PIX_FMT_NONE};
+ uint8_t* data{nullptr};
+ size_t size{0};
+ DataAccess access{DataAccess::READ_WRITE};
+ DataAlignment alignment{DataAlignment::DATA_UNALIGNED};
+};
+
+struct VideoStreamPacket : public StreamPacket
+{
+ VideoStreamPacket(unsigned int width,
+ unsigned int height,
+ VideoRotation rotation,
+ const uint8_t* data,
+ size_t size)
+ : width(width), height(height), rotation(rotation), data(data), size(size)
+ {
+ }
+
+ unsigned int width;
+ unsigned int height;
+ VideoRotation rotation;
+ const uint8_t* data;
+ size_t size;
+};
+
+/*!
+ * \brief Renders video frames provided by the game loop
+ *
+ * \sa CRPRenderManager
+ */
+class CRetroPlayerVideo : public IRetroPlayerStream
+{
+public:
+ CRetroPlayerVideo(CRPRenderManager& m_renderManager, CRPProcessInfo& m_processInfo);
+ ~CRetroPlayerVideo() override;
+
+ // implementation of IRetroPlayerStream
+ bool OpenStream(const StreamProperties& properties) override;
+ bool GetStreamBuffer(unsigned int width, unsigned int height, StreamBuffer& buffer) override;
+ void AddStreamData(const StreamPacket& packet) override;
+ void CloseStream() override;
+
+private:
+ // Construction parameters
+ CRPRenderManager& m_renderManager;
+ CRPProcessInfo& m_processInfo;
+
+ // Stream properties
+ bool m_bOpen = false;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/streams/memory/BasicMemoryStream.cpp b/xbmc/cores/RetroPlayer/streams/memory/BasicMemoryStream.cpp
new file mode 100644
index 0000000..6d52d3f
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/memory/BasicMemoryStream.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016-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 "BasicMemoryStream.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+CBasicMemoryStream::CBasicMemoryStream()
+{
+ Reset();
+}
+
+void CBasicMemoryStream::Init(size_t frameSize, uint64_t maxFrameCount)
+{
+ Reset();
+
+ m_frameSize = frameSize;
+}
+
+void CBasicMemoryStream::Reset()
+{
+ m_frameSize = 0;
+ m_frameBuffer.reset();
+ m_bHasFrame = false;
+}
+
+uint8_t* CBasicMemoryStream::BeginFrame()
+{
+ if (m_frameSize == 0)
+ return nullptr;
+
+ if (!m_frameBuffer)
+ m_frameBuffer.reset(new uint8_t[m_frameSize]);
+
+ m_bHasFrame = false;
+
+ return m_frameBuffer.get();
+}
+
+void CBasicMemoryStream::SubmitFrame()
+{
+ if (m_frameBuffer)
+ m_bHasFrame = true;
+}
+
+const uint8_t* CBasicMemoryStream::CurrentFrame() const
+{
+ return m_bHasFrame ? m_frameBuffer.get() : nullptr;
+}
diff --git a/xbmc/cores/RetroPlayer/streams/memory/BasicMemoryStream.h b/xbmc/cores/RetroPlayer/streams/memory/BasicMemoryStream.h
new file mode 100644
index 0000000..c3cc716
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/memory/BasicMemoryStream.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016-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 "IMemoryStream.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CBasicMemoryStream : public IMemoryStream
+{
+public:
+ CBasicMemoryStream();
+
+ ~CBasicMemoryStream() override = default;
+
+ // implementation of IMemoryStream
+ void Init(size_t frameSize, uint64_t maxFrameCount) override;
+ void Reset() override;
+ size_t FrameSize() const override { return m_frameSize; }
+ uint64_t MaxFrameCount() const override { return 1; }
+ void SetMaxFrameCount(uint64_t maxFrameCount) override {}
+ uint8_t* BeginFrame() override;
+ void SubmitFrame() override;
+ const uint8_t* CurrentFrame() const override;
+ uint64_t FutureFramesAvailable() const override { return 0; }
+ uint64_t AdvanceFrames(uint64_t frameCount) override { return 0; }
+ uint64_t PastFramesAvailable() const override { return 0; }
+ uint64_t RewindFrames(uint64_t frameCount) override { return 0; }
+ uint64_t GetFrameCounter() const override { return 0; }
+ void SetFrameCounter(uint64_t frameCount) override{};
+
+private:
+ size_t m_frameSize;
+ std::unique_ptr<uint8_t[]> m_frameBuffer;
+ bool m_bHasFrame;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/streams/memory/CMakeLists.txt b/xbmc/cores/RetroPlayer/streams/memory/CMakeLists.txt
new file mode 100644
index 0000000..7297bc6
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/memory/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(SOURCES BasicMemoryStream.cpp
+ DeltaPairMemoryStream.cpp
+ LinearMemoryStream.cpp
+)
+
+set(HEADERS BasicMemoryStream.h
+ DeltaPairMemoryStream.h
+ IMemoryStream.h
+ LinearMemoryStream.h
+)
+
+core_add_library(retroplayer_memory)
diff --git a/xbmc/cores/RetroPlayer/streams/memory/DeltaPairMemoryStream.cpp b/xbmc/cores/RetroPlayer/streams/memory/DeltaPairMemoryStream.cpp
new file mode 100644
index 0000000..eae65cd
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/memory/DeltaPairMemoryStream.cpp
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016-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 "DeltaPairMemoryStream.h"
+
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+void CDeltaPairMemoryStream::Reset()
+{
+ CLinearMemoryStream::Reset();
+
+ m_rewindBuffer.clear();
+}
+
+void CDeltaPairMemoryStream::SubmitFrameInternal()
+{
+ m_rewindBuffer.emplace_back();
+ MemoryFrame& frame = m_rewindBuffer.back();
+
+ // Record frame history
+ frame.frameHistoryCount = m_currentFrameHistory++;
+
+ uint32_t* currentFrame = m_currentFrame.get();
+ uint32_t* nextFrame = m_nextFrame.get();
+
+ for (size_t i = 0; i < m_paddedFrameSize; i++)
+ {
+ uint32_t xor_val = currentFrame[i] ^ nextFrame[i];
+ if (xor_val)
+ {
+ DeltaPair pair = {i, xor_val};
+ frame.buffer.push_back(pair);
+ }
+ }
+
+ // Delta is generated, bring the new frame forward (m_nextFrame is now disposable)
+ std::swap(m_currentFrame, m_nextFrame);
+
+ m_bHasNextFrame = false;
+
+ if (PastFramesAvailable() + 1 > MaxFrameCount())
+ CullPastFrames(1);
+}
+
+uint64_t CDeltaPairMemoryStream::PastFramesAvailable() const
+{
+ return static_cast<uint64_t>(m_rewindBuffer.size());
+}
+
+uint64_t CDeltaPairMemoryStream::RewindFrames(uint64_t frameCount)
+{
+ uint64_t rewound;
+
+ for (rewound = 0; rewound < frameCount; rewound++)
+ {
+ if (m_rewindBuffer.empty())
+ break;
+
+ const MemoryFrame& frame = m_rewindBuffer.back();
+ const DeltaPair* buffer = frame.buffer.data();
+
+ size_t bufferSize = frame.buffer.size();
+
+ // buffer pointer redirection violates data-dependency requirements...
+ // no vectorization for us :(
+ for (size_t i = 0; i < bufferSize; i++)
+ m_currentFrame[buffer[i].pos] ^= buffer[i].delta;
+
+ // Restore frame history
+ m_currentFrameHistory = frame.frameHistoryCount;
+
+ m_rewindBuffer.pop_back();
+ }
+
+ return rewound;
+}
+
+void CDeltaPairMemoryStream::CullPastFrames(uint64_t frameCount)
+{
+ for (uint64_t removedCount = 0; removedCount < frameCount; removedCount++)
+ {
+ if (m_rewindBuffer.empty())
+ {
+ CLog::Log(LOGDEBUG,
+ "CDeltaPairMemoryStream: Tried to cull {} frames too many. Check your math!",
+ frameCount - removedCount);
+ break;
+ }
+ m_rewindBuffer.pop_front();
+ }
+}
diff --git a/xbmc/cores/RetroPlayer/streams/memory/DeltaPairMemoryStream.h b/xbmc/cores/RetroPlayer/streams/memory/DeltaPairMemoryStream.h
new file mode 100644
index 0000000..b2a7cc8
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/memory/DeltaPairMemoryStream.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016-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 "LinearMemoryStream.h"
+
+#include <deque>
+#include <vector>
+
+namespace KODI
+{
+namespace RETRO
+{
+/*!
+ * \brief Implementation of a linear memory stream using XOR deltas
+ */
+class CDeltaPairMemoryStream : public CLinearMemoryStream
+{
+public:
+ CDeltaPairMemoryStream() = default;
+
+ ~CDeltaPairMemoryStream() override = default;
+
+ // implementation of IMemoryStream via CLinearMemoryStream
+ void Reset() override;
+ uint64_t PastFramesAvailable() const override;
+ uint64_t RewindFrames(uint64_t frameCount) override;
+
+protected:
+ // implementation of CLinearMemoryStream
+ void SubmitFrameInternal() override;
+ void CullPastFrames(uint64_t frameCount) override;
+
+ /*!
+ * Rewinding is implemented by applying XOR deltas on the specific parts of
+ * the save state buffer which have changed. In practice, this is very fast
+ * and simple (linear scan) and allows deltas to be compressed down to 1-3%
+ * of original save state size depending on the system. The algorithm runs
+ * on 32 bits at a time for speed.
+ *
+ * Use std::deque here to achieve amortized O(1) on pop/push to front and
+ * back.
+ */
+ struct DeltaPair
+ {
+ size_t pos;
+ uint32_t delta;
+ };
+
+ using DeltaPairVector = std::vector<DeltaPair>;
+
+ struct MemoryFrame
+ {
+ DeltaPairVector buffer;
+ uint64_t frameHistoryCount;
+ };
+
+ std::deque<MemoryFrame> m_rewindBuffer;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/streams/memory/IMemoryStream.h b/xbmc/cores/RetroPlayer/streams/memory/IMemoryStream.h
new file mode 100644
index 0000000..379a6a4
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/memory/IMemoryStream.h
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2016-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 <stddef.h>
+#include <stdint.h>
+
+namespace KODI
+{
+namespace RETRO
+{
+/*!
+ * \brief Stream of serialized states from game clients
+ *
+ * A memory stream is composed of "frames" of memory representing serialized
+ * states of the game client. For each video frame run by the game loop, the
+ * game client's state is serialized into a buffer provided by this interface.
+ *
+ * Implementation of three types of memory streams are provided:
+ *
+ * - Basic memory stream: has only a current frame, and supports neither
+ * rewind nor forward seeking.
+ *
+ * \sa CBasicMemoryStream
+ *
+ * - Linear memory stream: can grow in one direction. It is possible to
+ * rewind, but not fast-forward.
+ *
+ * \sa CLinearMemoryStream
+ *
+ * - Nonlinear memory stream: can have frames both ahead of and behind
+ * the current frame. If a stream is rewound, it is possible to
+ * recover these frames by seeking forward again.
+ *
+ * \sa CNonlinearMemoryStream (TODO)
+ */
+class IMemoryStream
+{
+public:
+ virtual ~IMemoryStream() = default;
+
+ /*!
+ * \brief Initialize memory stream
+ *
+ * \param frameSize The size of the serialized memory state
+ * \param maxFrameCount The maximum number of frames this stream can hold
+ */
+ virtual void Init(size_t frameSize, uint64_t maxFrameCount) = 0;
+
+ /*!
+ * \brief Free any resources used by this stream
+ */
+ virtual void Reset() = 0;
+
+ /*!
+ * \brief Return the frame size passed to Init()
+ */
+ virtual size_t FrameSize() const = 0;
+
+ /*!
+ * \brief Return the current max frame count
+ */
+ virtual uint64_t MaxFrameCount() const = 0;
+
+ /*!
+ * \brief Update the max frame count
+ *
+ * Old frames may be deleted if the max frame count is reduced.
+ */
+ virtual void SetMaxFrameCount(uint64_t maxFrameCount) = 0;
+
+ /*!
+ * \ brief Get a pointer to which FrameSize() bytes can be written
+ *
+ * The buffer exposed by this function is passed to the game client, which
+ * fills it with a serialization of its current state.
+ */
+ virtual uint8_t* BeginFrame() = 0;
+
+ /*!
+ * \brief Indicate that a frame of size FrameSize() has been written to the
+ * location returned from BeginFrame()
+ */
+ virtual void SubmitFrame() = 0;
+
+ /*!
+ * \brief Get a pointer to the current frame
+ *
+ * This function must have no side effects. The pointer is valid until the
+ * stream is modified.
+ *
+ * \return A buffer of size FrameSize(), or nullptr if the stream is empty
+ */
+ virtual const uint8_t* CurrentFrame() const = 0;
+
+ /*!
+ * \brief Return the number of frames ahead of the current frame
+ *
+ * If the stream supports forward seeking, frames that are passed over
+ * during a "rewind" operation can be recovered again.
+ */
+ virtual uint64_t FutureFramesAvailable() const = 0;
+
+ /*!
+ * \brief Seek ahead the specified number of frames
+ *
+ * \return The number of frames advanced
+ */
+ virtual uint64_t AdvanceFrames(uint64_t frameCount) = 0;
+
+ /*!
+ * \brief Return the number of frames behind the current frame
+ */
+ virtual uint64_t PastFramesAvailable() const = 0;
+
+ /*!
+ * \brief Seek backwards the specified number of frames
+ *
+ * \return The number of frames rewound
+ */
+ virtual uint64_t RewindFrames(uint64_t frameCount) = 0;
+
+ /*!
+ * \brief Get the total number of frames played until the current frame
+ *
+ * \return The history of the current frame, or 0 for unknown
+ */
+ virtual uint64_t GetFrameCounter() const = 0;
+
+ /*!
+ * \brief Set the total number of frames played until the current frame
+ *
+ * \param frameCount The history of the current frame
+ */
+ virtual void SetFrameCounter(uint64_t frameCount) = 0;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/RetroPlayer/streams/memory/LinearMemoryStream.cpp b/xbmc/cores/RetroPlayer/streams/memory/LinearMemoryStream.cpp
new file mode 100644
index 0000000..65a7959
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/memory/LinearMemoryStream.cpp
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016-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 "LinearMemoryStream.h"
+
+using namespace KODI;
+using namespace RETRO;
+
+// Pad forward to nearest boundary of bytes
+#define PAD_TO_CEIL(x, bytes) ((((x) + (bytes)-1) / (bytes)) * (bytes))
+
+CLinearMemoryStream::CLinearMemoryStream()
+{
+ Reset();
+}
+
+void CLinearMemoryStream::Init(size_t frameSize, uint64_t maxFrameCount)
+{
+ Reset();
+
+ m_frameSize = frameSize;
+ m_paddedFrameSize = PAD_TO_CEIL(m_frameSize, sizeof(uint32_t));
+ m_maxFrames = maxFrameCount;
+}
+
+void CLinearMemoryStream::Reset()
+{
+ m_frameSize = 0;
+ m_paddedFrameSize = 0;
+ m_maxFrames = 0;
+ m_currentFrame.reset();
+ m_nextFrame.reset();
+ m_bHasCurrentFrame = false;
+ m_bHasNextFrame = false;
+ m_currentFrameHistory = 0;
+}
+
+void CLinearMemoryStream::SetMaxFrameCount(uint64_t maxFrameCount)
+{
+ if (maxFrameCount == 0)
+ {
+ Reset();
+ }
+ else
+ {
+ const uint64_t frameCount = BufferSize();
+ if (maxFrameCount < frameCount)
+ CullPastFrames(frameCount - maxFrameCount);
+ }
+
+ m_maxFrames = maxFrameCount;
+}
+
+uint8_t* CLinearMemoryStream::BeginFrame()
+{
+ if (m_paddedFrameSize == 0)
+ return nullptr;
+
+ if (!m_bHasCurrentFrame)
+ {
+ if (!m_currentFrame)
+ m_currentFrame.reset(new uint32_t[m_paddedFrameSize]);
+ return reinterpret_cast<uint8_t*>(m_currentFrame.get());
+ }
+
+ if (!m_nextFrame)
+ m_nextFrame.reset(new uint32_t[m_paddedFrameSize]);
+ return reinterpret_cast<uint8_t*>(m_nextFrame.get());
+}
+
+const uint8_t* CLinearMemoryStream::CurrentFrame() const
+{
+ if (m_bHasCurrentFrame)
+ return reinterpret_cast<const uint8_t*>(m_currentFrame.get());
+
+ return nullptr;
+}
+
+void CLinearMemoryStream::SubmitFrame()
+{
+ if (!m_bHasCurrentFrame)
+ {
+ m_bHasCurrentFrame = true;
+ }
+ else if (!m_bHasNextFrame)
+ {
+ m_bHasNextFrame = true;
+ }
+
+ if (m_bHasNextFrame)
+ {
+ SubmitFrameInternal();
+ }
+}
+
+uint64_t CLinearMemoryStream::BufferSize() const
+{
+ return PastFramesAvailable() + (m_bHasCurrentFrame ? 1 : 0);
+}
diff --git a/xbmc/cores/RetroPlayer/streams/memory/LinearMemoryStream.h b/xbmc/cores/RetroPlayer/streams/memory/LinearMemoryStream.h
new file mode 100644
index 0000000..f7b9465
--- /dev/null
+++ b/xbmc/cores/RetroPlayer/streams/memory/LinearMemoryStream.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016-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 "IMemoryStream.h"
+
+#include <memory>
+#include <stdint.h>
+
+namespace KODI
+{
+namespace RETRO
+{
+class CLinearMemoryStream : public IMemoryStream
+{
+public:
+ CLinearMemoryStream();
+
+ ~CLinearMemoryStream() override = default;
+
+ // partial implementation of IMemoryStream
+ void Init(size_t frameSize, uint64_t maxFrameCount) override;
+ void Reset() override;
+ size_t FrameSize() const override { return m_frameSize; }
+ uint64_t MaxFrameCount() const override { return m_maxFrames; }
+ void SetMaxFrameCount(uint64_t maxFrameCount) override;
+ uint8_t* BeginFrame() override;
+ void SubmitFrame() override;
+ const uint8_t* CurrentFrame() const override;
+ uint64_t FutureFramesAvailable() const override { return 0; }
+ uint64_t AdvanceFrames(uint64_t frameCount) override { return 0; }
+ uint64_t PastFramesAvailable() const override = 0;
+ uint64_t RewindFrames(uint64_t frameCount) override = 0;
+ uint64_t GetFrameCounter() const override { return m_currentFrameHistory; }
+ void SetFrameCounter(uint64_t frameCount) override { m_currentFrameHistory = frameCount; }
+
+protected:
+ virtual void SubmitFrameInternal() = 0;
+ virtual void CullPastFrames(uint64_t frameCount) = 0;
+
+ // Helper function
+ uint64_t BufferSize() const;
+
+ size_t m_paddedFrameSize;
+ uint64_t m_maxFrames;
+
+ /**
+ * Simple double-buffering. After XORing the two states, the next becomes
+ * the current, and the current becomes a buffer for the next call to
+ * CGameClient::Serialize().
+ */
+ std::unique_ptr<uint32_t[]> m_currentFrame;
+ std::unique_ptr<uint32_t[]> m_nextFrame;
+ bool m_bHasCurrentFrame;
+ bool m_bHasNextFrame;
+
+ uint64_t m_currentFrameHistory;
+
+private:
+ size_t m_frameSize;
+};
+} // namespace RETRO
+} // namespace KODI
diff --git a/xbmc/cores/VideoPlayer/AudioSinkAE.cpp b/xbmc/cores/VideoPlayer/AudioSinkAE.cpp
new file mode 100644
index 0000000..0af936f
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/AudioSinkAE.cpp
@@ -0,0 +1,387 @@
+/*
+ * 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 "AudioSinkAE.h"
+
+#include "DVDClock.h"
+#include "DVDCodecs/Audio/DVDAudioCodec.h"
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "cores/AudioEngine/Utils/AEAudioFormat.h"
+#include "cores/AudioEngine/Utils/AEStreamData.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+CAudioSinkAE::CAudioSinkAE(CDVDClock *clock) : m_pClock(clock)
+{
+ m_bPassthrough = false;
+ m_iBitsPerSample = 0;
+ m_sampleRate = 0;
+ m_bPaused = true;
+ m_playingPts = DVD_NOPTS_VALUE; //silence coverity uninitialized warning, is set elsewhere
+ m_timeOfPts = 0.0; //silence coverity uninitialized warning, is set elsewhere
+ m_syncError = 0.0;
+ m_syncErrorTime = 0;
+}
+
+CAudioSinkAE::~CAudioSinkAE()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+}
+
+bool CAudioSinkAE::Create(const DVDAudioFrame &audioframe, AVCodecID codec, bool needresampler)
+{
+ CLog::Log(LOGINFO, "Creating audio stream (codec id: {}, channels: {}, sample rate: {}, {})",
+ codec, audioframe.format.m_channelLayout.Count(), audioframe.format.m_sampleRate,
+ audioframe.passthrough ? "pass-through" : "no pass-through");
+
+ // if passthrough isset do something else
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ unsigned int options = needresampler && !audioframe.passthrough ? AESTREAM_FORCE_RESAMPLE : 0;
+ options |= AESTREAM_PAUSED;
+
+ AEAudioFormat format = audioframe.format;
+ m_pAudioStream = CServiceBroker::GetActiveAE()->MakeStream(
+ format,
+ options,
+ this
+ );
+ if (!m_pAudioStream)
+ return false;
+
+ m_dataFormat = audioframe.format.m_dataFormat;
+ m_sampleRate = audioframe.format.m_sampleRate;
+ m_iBitsPerSample = audioframe.bits_per_sample;
+ m_bPassthrough = audioframe.passthrough;
+ m_channelLayout = audioframe.format.m_channelLayout;
+ m_dataType = audioframe.format.m_streamInfo.m_type;
+
+ return true;
+}
+
+void CAudioSinkAE::Destroy(bool finish)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_pAudioStream)
+ {
+ m_pAudioStream.get_deleter().setFinish(finish);
+ m_pAudioStream.reset();
+ }
+
+ m_pAudioStream = NULL;
+ m_sampleRate = 0;
+ m_iBitsPerSample = 0;
+ m_bPassthrough = false;
+ m_bPaused = true;
+ m_playingPts = DVD_NOPTS_VALUE;
+}
+
+unsigned int CAudioSinkAE::AddPackets(const DVDAudioFrame &audioframe)
+{
+ m_bAbort = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_pAudioStream)
+ return 0;
+
+ CAESyncInfo info = m_pAudioStream->GetSyncInfo();
+ if (info.state == CAESyncInfo::SYNC_INSYNC)
+ {
+ unsigned int newTime = info.errortime;
+ if (newTime != m_syncErrorTime)
+ {
+ m_syncErrorTime = info.errortime;
+ m_syncError = info.error / 1000 * DVD_TIME_BASE;
+ m_resampleRatio = info.rr;
+ }
+ }
+ else
+ {
+ m_syncErrorTime = 0;
+ m_syncError = 0.0;
+ }
+
+ //Calculate a timeout when this definitely should be done
+ double timeout;
+ timeout = DVD_SEC_TO_TIME(m_pAudioStream->GetDelay()) + audioframe.duration;
+ timeout += DVD_SEC_TO_TIME(1.0);
+ timeout += m_pClock->GetAbsoluteClock();
+
+ unsigned int total = audioframe.nb_frames - audioframe.framesOut;
+ unsigned int frames = total;
+ unsigned int offset = audioframe.framesOut;
+ do
+ {
+ IAEStream::ExtData ext;
+ if (offset == 0)
+ {
+ ext.pts = audioframe.pts / DVD_TIME_BASE * 1000;
+ }
+ if (audioframe.hasDownmix)
+ {
+ ext.hasDownmix = true;
+ ext.centerMixLevel = audioframe.centerMixLevel;
+ }
+ unsigned int copied = m_pAudioStream->AddData(audioframe.data, offset, frames, &ext);
+ offset += copied;
+ frames -= copied;
+ if (frames <= 0)
+ break;
+
+ if (copied == 0 && timeout < m_pClock->GetAbsoluteClock())
+ {
+ CLog::Log(LOGERROR, "CDVDAudio::AddPacketsRenderer - timeout adding data to renderer");
+ break;
+ }
+
+ lock.unlock();
+ KODI::TIME::Sleep(1ms);
+ lock.lock();
+ } while (!m_bAbort);
+
+ m_playingPts = audioframe.pts + audioframe.duration - GetDelay();
+ m_timeOfPts = m_pClock->GetAbsoluteClock();
+
+ return total - frames;
+}
+
+void CAudioSinkAE::Drain()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_pAudioStream)
+ m_pAudioStream->Drain(true);
+}
+
+void CAudioSinkAE::SetVolume(float volume)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_pAudioStream)
+ m_pAudioStream->SetVolume(volume);
+}
+
+void CAudioSinkAE::SetDynamicRangeCompression(long drc)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_pAudioStream)
+ m_pAudioStream->SetAmplification(powf(10.0f, (float)drc / 2000.0f));
+}
+
+void CAudioSinkAE::Pause()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_pAudioStream)
+ m_pAudioStream->Pause();
+ CLog::Log(LOGDEBUG,"CDVDAudio::Pause - pausing audio stream");
+ m_playingPts = DVD_NOPTS_VALUE;
+}
+
+void CAudioSinkAE::Resume()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_pAudioStream)
+ m_pAudioStream->Resume();
+ CLog::Log(LOGDEBUG,"CDVDAudio::Resume - resume audio stream");
+}
+
+double CAudioSinkAE::GetDelay()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ double delay = 0.3;
+ if(m_pAudioStream)
+ delay = m_pAudioStream->GetDelay();
+
+ return delay * DVD_TIME_BASE;
+}
+
+void CAudioSinkAE::Flush()
+{
+ m_bAbort = true;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_pAudioStream)
+ {
+ m_pAudioStream->Flush();
+ CLog::Log(LOGDEBUG,"CDVDAudio::Flush - flush audio stream");
+ }
+ m_playingPts = DVD_NOPTS_VALUE;
+ m_syncError = 0.0;
+ m_syncErrorTime = 0;
+}
+
+void CAudioSinkAE::AbortAddPackets()
+{
+ m_bAbort = true;
+}
+
+bool CAudioSinkAE::IsValidFormat(const DVDAudioFrame &audioframe)
+{
+ if (!m_pAudioStream)
+ return false;
+
+ if (audioframe.passthrough != m_bPassthrough)
+ return false;
+
+ if (m_dataFormat != audioframe.format.m_dataFormat ||
+ m_sampleRate != audioframe.format.m_sampleRate ||
+ m_iBitsPerSample != audioframe.bits_per_sample ||
+ m_channelLayout != audioframe.format.m_channelLayout)
+ return false;
+
+ if (m_bPassthrough &&
+ m_dataType != audioframe.format.m_streamInfo.m_type)
+ return false;
+
+ return true;
+}
+
+double CAudioSinkAE::GetCacheTime()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_pAudioStream)
+ return 0.0;
+
+ return m_pAudioStream->GetCacheTime();
+}
+
+double CAudioSinkAE::GetCacheTotal()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_pAudioStream)
+ return 0.0;
+ return m_pAudioStream->GetCacheTotal();
+}
+
+double CAudioSinkAE::GetMaxDelay()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_pAudioStream)
+ return 0.0;
+ return m_pAudioStream->GetMaxDelay();
+}
+
+double CAudioSinkAE::GetPlayingPts()
+{
+ if (m_playingPts == DVD_NOPTS_VALUE)
+ return 0.0;
+
+ double now = m_pClock->GetAbsoluteClock();
+ double diff = now - m_timeOfPts;
+ double cache = GetCacheTime();
+ double played = 0.0;
+
+ if (diff < cache)
+ played = diff;
+ else
+ played = cache;
+
+ m_timeOfPts = now;
+ m_playingPts += played;
+ return m_playingPts;
+}
+
+double CAudioSinkAE::GetSyncError()
+{
+ return m_syncError;
+}
+
+void CAudioSinkAE::SetSyncErrorCorrection(double correction)
+{
+ m_syncError += correction;
+}
+
+double CAudioSinkAE::GetResampleRatio()
+{
+ return m_resampleRatio;
+}
+
+void CAudioSinkAE::SetResampleMode(int mode)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if(m_pAudioStream)
+ {
+ m_pAudioStream->SetResampleMode(mode);
+ }
+}
+
+double CAudioSinkAE::GetClock()
+{
+ if (m_pClock)
+ return (m_pClock->GetClock() + m_pClock->GetVsyncAdjust()) / DVD_TIME_BASE * 1000;
+ else
+ return 0.0;
+}
+
+double CAudioSinkAE::GetClockSpeed()
+{
+ if (m_pClock)
+ return m_pClock->GetClockSpeed();
+ else
+ return 1.0;
+}
+
+CAEStreamInfo::DataType CAudioSinkAE::GetPassthroughStreamType(AVCodecID codecId,
+ int samplerate,
+ int profile)
+{
+ AEAudioFormat format;
+ format.m_dataFormat = AE_FMT_RAW;
+ format.m_sampleRate = samplerate;
+ format.m_streamInfo.m_type = CAEStreamInfo::DataType::STREAM_TYPE_NULL;
+ switch (codecId)
+ {
+ case AV_CODEC_ID_AC3:
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_AC3;
+ format.m_streamInfo.m_sampleRate = samplerate;
+ break;
+
+ case AV_CODEC_ID_EAC3:
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_EAC3;
+ format.m_streamInfo.m_sampleRate = samplerate;
+ break;
+
+ case AV_CODEC_ID_DTS:
+ if (profile == FF_PROFILE_DTS_HD_HRA)
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD;
+ else if (profile == FF_PROFILE_DTS_HD_MA)
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD_MA;
+ else
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD_CORE;
+ format.m_streamInfo.m_sampleRate = samplerate;
+ break;
+
+ case AV_CODEC_ID_TRUEHD:
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_TRUEHD;
+ format.m_streamInfo.m_sampleRate = samplerate;
+ break;
+
+ default:
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_NULL;
+ }
+
+ bool supports = CServiceBroker::GetActiveAE()->SupportsRaw(format);
+
+ if (!supports && codecId == AV_CODEC_ID_DTS &&
+ format.m_streamInfo.m_type != CAEStreamInfo::STREAM_TYPE_DTSHD_CORE &&
+ CServiceBroker::GetActiveAE()->UsesDtsCoreFallback())
+ {
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD_CORE;
+ supports = CServiceBroker::GetActiveAE()->SupportsRaw(format);
+ }
+
+ if (supports)
+ return format.m_streamInfo.m_type;
+ else
+ return CAEStreamInfo::DataType::STREAM_TYPE_NULL;
+}
diff --git a/xbmc/cores/VideoPlayer/AudioSinkAE.h b/xbmc/cores/VideoPlayer/AudioSinkAE.h
new file mode 100644
index 0000000..e2aff52
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/AudioSinkAE.h
@@ -0,0 +1,85 @@
+/*
+ * 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/AE.h"
+#include "cores/AudioEngine/Interfaces/AEStream.h"
+#include "cores/AudioEngine/Utils/AEChannelInfo.h"
+#include "threads/CriticalSection.h"
+
+#include <atomic>
+#include <mutex>
+
+#include "PlatformDefs.h"
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+}
+
+typedef struct stDVDAudioFrame DVDAudioFrame;
+
+class CDVDClock;
+
+class CAudioSinkAE : IAEClockCallback
+{
+public:
+ explicit CAudioSinkAE(CDVDClock *clock);
+ ~CAudioSinkAE() override;
+
+ void SetVolume(float fVolume);
+ void SetDynamicRangeCompression(long drc);
+ void Pause();
+ void Resume();
+ bool Create(const DVDAudioFrame &audioframe, AVCodecID codec, bool needresampler);
+ bool IsValidFormat(const DVDAudioFrame &audioframe);
+ void Destroy(bool finish);
+ unsigned int AddPackets(const DVDAudioFrame &audioframe);
+ double GetPlayingPts();
+ double GetCacheTime();
+ double GetCacheTotal(); // returns total time a stream can buffer
+ double GetMaxDelay(); // returns total time of audio in AE for the stream
+ double GetDelay(); // returns the time it takes to play a packet if we add one at this time
+ double GetSyncError();
+ void SetSyncErrorCorrection(double correction);
+
+ /*!
+ * \brief Returns the resample ratio, or 0.0 if unknown/invalid
+ */
+ double GetResampleRatio();
+
+ void SetResampleMode(int mode);
+ void Flush();
+ void Drain();
+ void AbortAddPackets();
+
+ double GetClock() override;
+ double GetClockSpeed() override;
+
+ CAEStreamInfo::DataType GetPassthroughStreamType(AVCodecID codecId, int samplerate, int profile);
+
+protected:
+ IAE::StreamPtr m_pAudioStream;
+ double m_playingPts;
+ double m_timeOfPts;
+ double m_syncError;
+ unsigned int m_syncErrorTime;
+ double m_resampleRatio = 0.0; // invalid
+ CCriticalSection m_critSection;
+
+ AEDataFormat m_dataFormat;
+ unsigned int m_sampleRate;
+ int m_iBitsPerSample;
+ bool m_bPassthrough;
+ CAEChannelInfo m_channelLayout;
+ CAEStreamInfo::DataType m_dataType;
+ bool m_bPaused;
+
+ std::atomic_bool m_bAbort;
+ CDVDClock *m_pClock;
+};
diff --git a/xbmc/cores/VideoPlayer/Buffers/CMakeLists.txt b/xbmc/cores/VideoPlayer/Buffers/CMakeLists.txt
new file mode 100644
index 0000000..a876912
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Buffers/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES VideoBuffer.cpp)
+set(HEADERS VideoBuffer.h)
+
+if("gbm" IN_LIST CORE_PLATFORM_NAME_LC OR "wayland" IN_LIST CORE_PLATFORM_NAME_LC)
+ list(APPEND SOURCES VideoBufferDMA.cpp
+ VideoBufferDRMPRIME.cpp
+ VideoBufferPoolDMA.cpp)
+ list(APPEND HEADERS VideoBufferDMA.h
+ VideoBufferDRMPRIME.h
+ VideoBufferPoolDMA.h)
+endif()
+
+core_add_library(videoplayer-buffers)
diff --git a/xbmc/cores/VideoPlayer/Buffers/VideoBuffer.cpp b/xbmc/cores/VideoPlayer/Buffers/VideoBuffer.cpp
new file mode 100644
index 0000000..6586706
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Buffers/VideoBuffer.cpp
@@ -0,0 +1,466 @@
+/*
+ * 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 "VideoBuffer.h"
+
+#include <mutex>
+#include <string.h>
+#include <utility>
+
+//-----------------------------------------------------------------------------
+// CVideoBuffer
+//-----------------------------------------------------------------------------
+
+CVideoBuffer::CVideoBuffer(int id)
+{
+ m_id = id;
+ m_refCount = 0;
+}
+
+void CVideoBuffer::Acquire()
+{
+ m_refCount++;
+}
+
+void CVideoBuffer::Acquire(std::shared_ptr<IVideoBufferPool> pool)
+{
+ m_refCount++;
+ m_pool = std::move(pool);
+}
+
+void CVideoBuffer::Release()
+{
+ if (--m_refCount <= 0 && m_pool)
+ {
+ std::shared_ptr<IVideoBufferPool> pool = m_pool->GetPtr();
+ m_pool = nullptr;
+ pool->Return(m_id);
+ }
+}
+
+AVPixelFormat CVideoBuffer::GetFormat()
+{
+ return m_pixFormat;
+}
+
+bool CVideoBuffer::CopyPicture(YuvImage* pDst, YuvImage *pSrc)
+{
+ uint8_t *s = pSrc->plane[0];
+ uint8_t *d = pDst->plane[0];
+ int w = pDst->width * pDst->bpp;
+ int h = pDst->height;
+ if ((w == pSrc->stride[0]) && (pSrc->stride[0] == pDst->stride[0]))
+ {
+ memcpy(d, s, w*h);
+ }
+ else
+ {
+ for (int y = 0; y < h; y++)
+ {
+ memcpy(d, s, w);
+ s += pSrc->stride[0];
+ d += pDst->stride[0];
+ }
+ }
+ s = pSrc->plane[1];
+ d = pDst->plane[1];
+ w =(pDst->width >> pDst->cshift_x) * pDst->bpp;
+ h =(pDst->height >> pDst->cshift_y);
+ if ((w == pSrc->stride[1]) && (pSrc->stride[1] == pDst->stride[1]))
+ {
+ memcpy(d, s, w*h);
+ }
+ else
+ {
+ for (int y = 0; y < h; y++)
+ {
+ memcpy(d, s, w);
+ s += pSrc->stride[1];
+ d += pDst->stride[1];
+ }
+ }
+ s = pSrc->plane[2];
+ d = pDst->plane[2];
+ if ((w == pSrc->stride[2]) && (pSrc->stride[2] == pDst->stride[2]))
+ {
+ memcpy(d, s, w*h);
+ }
+ else
+ {
+ for (int y = 0; y < h; y++)
+ {
+ memcpy(d, s, w);
+ s += pSrc->stride[2];
+ d += pDst->stride[2];
+ }
+ }
+ return true;
+}
+
+
+bool CVideoBuffer::CopyNV12Picture(YuvImage* pDst, YuvImage *pSrc)
+{
+ uint8_t *s = pSrc->plane[0];
+ uint8_t *d = pDst->plane[0];
+ int w = pDst->width;
+ int h = pDst->height;
+ // Copy Y
+ if ((w == pSrc->stride[0]) && (pSrc->stride[0] == pDst->stride[0]))
+ {
+ memcpy(d, s, w*h);
+ }
+ else
+ {
+ for (int y = 0; y < h; y++)
+ {
+ memcpy(d, s, w);
+ s += pSrc->stride[0];
+ d += pDst->stride[0];
+ }
+ }
+
+ s = pSrc->plane[1];
+ d = pDst->plane[1];
+ w = pDst->width;
+ h = pDst->height >> 1;
+ // Copy packed UV (width is same as for Y as it's both U and V components)
+ if ((w == pSrc->stride[1]) && (pSrc->stride[1] == pDst->stride[1]))
+ {
+ memcpy(d, s, w*h);
+ }
+ else
+ {
+ for (int y = 0; y < h; y++)
+ {
+ memcpy(d, s, w);
+ s += pSrc->stride[1];
+ d += pDst->stride[1];
+ }
+ }
+
+ return true;
+}
+
+bool CVideoBuffer::CopyYUV422PackedPicture(YuvImage* pDst, YuvImage *pSrc)
+{
+ uint8_t *s = pSrc->plane[0];
+ uint8_t *d = pDst->plane[0];
+ int w = pDst->width;
+ int h = pDst->height;
+
+ // Copy YUYV
+ if ((w * 2 == pSrc->stride[0]) && (pSrc->stride[0] == pDst->stride[0]))
+ {
+ memcpy(d, s, w*h*2);
+ }
+ else
+ {
+ for (int y = 0; y < h; y++)
+ {
+ memcpy(d, s, w*2);
+ s += pSrc->stride[0];
+ d += pDst->stride[0];
+ }
+ }
+
+ return true;
+}
+
+CVideoBufferSysMem::CVideoBufferSysMem(IVideoBufferPool &pool, int id, AVPixelFormat format, int size)
+: CVideoBuffer(id)
+{
+ m_pixFormat = format;
+ m_size = size;
+ memset(&m_image, 0, sizeof(YuvImage));
+}
+
+CVideoBufferSysMem::~CVideoBufferSysMem()
+{
+ delete[] m_data;
+}
+
+uint8_t* CVideoBufferSysMem::GetMemPtr()
+{
+ return m_data;
+}
+
+void CVideoBufferSysMem::GetPlanes(uint8_t*(&planes)[YuvImage::MAX_PLANES])
+{
+ planes[0] = m_image.plane[0];
+ planes[1] = m_image.plane[1];
+ planes[2] = m_image.plane[2];
+}
+
+void CVideoBufferSysMem::GetStrides(int(&strides)[YuvImage::MAX_PLANES])
+{
+ strides[0] = m_image.stride[0];
+ strides[1] = m_image.stride[1];
+ strides[2] = m_image.stride[2];
+}
+
+void CVideoBufferSysMem::SetDimensions(int width, int height, const int (&strides)[YuvImage::MAX_PLANES])
+{
+ m_width = width;
+ m_height = height;
+
+ m_image.width = m_width;
+ m_image.height = m_height;
+ m_image.stride[0] = strides[0];
+ m_image.stride[1] = strides[1];
+ m_image.stride[2] = strides[2];
+ m_image.cshift_x = 1;
+ m_image.cshift_y = 1;
+ m_image.bpp = 1;
+
+ if (m_pixFormat == AV_PIX_FMT_YUV420P ||
+ m_pixFormat == AV_PIX_FMT_YUV420P16 ||
+ m_pixFormat == AV_PIX_FMT_YUV420P14 ||
+ m_pixFormat == AV_PIX_FMT_YUV420P12 ||
+ m_pixFormat == AV_PIX_FMT_YUV420P10 ||
+ m_pixFormat == AV_PIX_FMT_YUV420P9)
+ {
+ if (m_pixFormat != AV_PIX_FMT_YUV420P)
+ m_image.bpp = 2;
+
+ m_image.planesize[0] = m_image.stride[0] * m_image.height;
+ m_image.planesize[1] = m_image.stride[1] * (m_image.height >> m_image.cshift_y);
+ m_image.planesize[2] = m_image.stride[2] * (m_image.height >> m_image.cshift_y);
+ }
+ else if (m_pixFormat == AV_PIX_FMT_NV12)
+ {
+ // Y plane
+ m_image.planesize[0] = m_image.stride[0] * m_image.height;
+ // packed UV plane
+ m_image.planesize[1] = m_image.stride[1] * m_image.height / 2;
+ // third plane is not used
+ m_image.planesize[2] = 0;
+ }
+ else if (m_pixFormat == AV_PIX_FMT_YUYV422 ||
+ m_pixFormat == AV_PIX_FMT_UYVY422)
+ {
+ // packed YUYV plane
+ m_image.planesize[0] = m_image.stride[0] * m_image.height;
+ // second plane is not used
+ m_image.planesize[1] = 0;
+ // third plane is not used
+ m_image.planesize[2] = 0;
+ }
+
+ m_image.plane[0] = m_data;
+ m_image.plane[1] = m_data + m_image.planesize[0];
+ m_image.plane[2] = m_image.plane[1] + m_image.planesize[1];
+}
+
+void CVideoBufferSysMem::SetDimensions(int width, int height, const int (&strides)[YuvImage::MAX_PLANES], const int (&planeOffsets)[YuvImage::MAX_PLANES])
+{
+ SetDimensions(width, height, strides);
+
+ m_image.plane[0] = m_data + planeOffsets[0];
+ m_image.plane[1] = m_data + planeOffsets[1];
+ m_image.plane[2] = m_data + planeOffsets[2];
+}
+
+bool CVideoBufferSysMem::Alloc()
+{
+ m_data = new uint8_t[m_size];
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------
+// CVideoBufferPool
+//-----------------------------------------------------------------------------
+
+CVideoBufferPoolSysMem::~CVideoBufferPoolSysMem()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (auto buf : m_all)
+ {
+ delete buf;
+ }
+}
+
+CVideoBuffer* CVideoBufferPoolSysMem::Get()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CVideoBufferSysMem *buf = nullptr;
+ if (!m_free.empty())
+ {
+ int idx = m_free.front();
+ m_free.pop_front();
+ m_used.push_back(idx);
+ buf = m_all[idx];
+ }
+ else
+ {
+ int id = m_all.size();
+ buf = new CVideoBufferSysMem(*this, id, m_pixFormat, m_size);
+ buf->Alloc();
+ m_all.push_back(buf);
+ m_used.push_back(id);
+ }
+
+ buf->Acquire(GetPtr());
+ return buf;
+}
+
+void CVideoBufferPoolSysMem::Return(int id)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ auto it = m_used.begin();
+ while (it != m_used.end())
+ {
+ if (*it == id)
+ {
+ m_used.erase(it);
+ break;
+ }
+ else
+ ++it;
+ }
+ m_free.push_back(id);
+
+ if (m_bm && m_used.empty())
+ {
+ (m_bm->*m_cbDispose)(this);
+ }
+}
+
+void CVideoBufferPoolSysMem::Configure(AVPixelFormat format, int size)
+{
+ m_pixFormat = format;
+ m_size = size;
+ m_configured = true;
+}
+
+inline bool CVideoBufferPoolSysMem::IsConfigured()
+{
+ return m_configured;
+}
+
+bool CVideoBufferPoolSysMem::IsCompatible(AVPixelFormat format, int size)
+{
+ if (m_pixFormat == format &&
+ m_size == size)
+ return true;
+
+ return false;
+}
+
+void CVideoBufferPoolSysMem::Discard(CVideoBufferManager *bm, ReadyToDispose cb)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bm = bm;
+ m_cbDispose = cb;
+
+ if (m_used.empty())
+ (m_bm->*m_cbDispose)(this);
+}
+
+std::shared_ptr<IVideoBufferPool> CVideoBufferPoolSysMem::CreatePool()
+{
+ return std::make_shared<CVideoBufferPoolSysMem>();
+}
+
+//-----------------------------------------------------------------------------
+// CVideoBufferManager
+//-----------------------------------------------------------------------------
+
+CVideoBufferManager::CVideoBufferManager()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ RegisterPoolFactory("SysMem", &CVideoBufferPoolSysMem::CreatePool);
+}
+
+void CVideoBufferManager::RegisterPool(const std::shared_ptr<IVideoBufferPool>& pool)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ // preferred pools are to the front
+ m_pools.push_front(pool);
+}
+
+void CVideoBufferManager::RegisterPoolFactory(const std::string& id, CreatePoolFunc createFunc)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_poolFactories[id] = createFunc;
+}
+
+void CVideoBufferManager::ReleasePools()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::list<std::shared_ptr<IVideoBufferPool>> pools = m_pools;
+ m_pools.clear();
+
+ m_discardedPools = pools;
+
+ for (const auto& pool : pools)
+ {
+ pool->Discard(this, &CVideoBufferManager::ReadyForDisposal);
+ }
+}
+
+void CVideoBufferManager::ReleasePool(IVideoBufferPool *pool)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (auto it = m_pools.begin(); it != m_pools.end(); ++it)
+ {
+ if ((*it).get() == pool)
+ {
+ m_discardedPools.push_back(*it);
+ m_pools.erase(it);
+ pool->Discard(this, &CVideoBufferManager::ReadyForDisposal);
+ break;
+ }
+ }
+}
+
+void CVideoBufferManager::ReadyForDisposal(IVideoBufferPool *pool)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (auto it = m_discardedPools.begin(); it != m_discardedPools.end(); ++it)
+ {
+ if ((*it).get() == pool)
+ {
+ pool->Released(*this);
+ m_discardedPools.erase(it);
+ break;
+ }
+ }
+}
+
+CVideoBuffer* CVideoBufferManager::Get(AVPixelFormat format, int size, IVideoBufferPool **pPool)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& pool : m_pools)
+ {
+ if (!pool->IsConfigured())
+ {
+ pool->Configure(format, size);
+ }
+ if (pool->IsCompatible(format, size))
+ {
+ return pool->Get();
+ }
+ }
+
+ for (const auto& fact : m_poolFactories)
+ {
+ std::shared_ptr<IVideoBufferPool> pool = fact.second();
+ m_pools.push_front(pool);
+ pool->Configure(format, size);
+ if (pPool)
+ *pPool = pool.get();
+ return pool->Get();
+ }
+ return nullptr;
+}
diff --git a/xbmc/cores/VideoPlayer/Buffers/VideoBuffer.h b/xbmc/cores/VideoPlayer/Buffers/VideoBuffer.h
new file mode 100644
index 0000000..39c7d86
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Buffers/VideoBuffer.h
@@ -0,0 +1,197 @@
+/*
+ * 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 "threads/CriticalSection.h"
+#include <atomic>
+#include <deque>
+#include <list>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+extern "C" {
+#include <libavutil/pixfmt.h>
+}
+
+struct YuvImage
+{
+ static const int MAX_PLANES = 3;
+
+ uint8_t* plane[MAX_PLANES];
+ int planesize[MAX_PLANES];
+ int stride[MAX_PLANES];
+ unsigned int width;
+ unsigned int height;
+ unsigned int cshift_x; // this is the chroma shift used
+ unsigned int cshift_y;
+ unsigned int bpp; // bytes per pixel
+};
+
+//-----------------------------------------------------------------------------
+//
+//-----------------------------------------------------------------------------
+
+#define BUFFER_STATE_DECODER 0x01;
+#define BUFFER_STATE_RENDER 0x02;
+
+class CVideoBuffer;
+class IVideoBufferPool;
+class CVideoBufferManager;
+
+typedef void (CVideoBufferManager::*ReadyToDispose)(IVideoBufferPool *pool);
+
+class IVideoBufferPool : public std::enable_shared_from_this<IVideoBufferPool>
+{
+public:
+ virtual ~IVideoBufferPool() = default;
+
+ // get a free buffer from the pool, sets ref count to 1
+ virtual CVideoBuffer* Get() = 0;
+
+ // called by buffer when ref count goes to zero
+ virtual void Return(int id) = 0;
+
+ // required if pool is registered with BufferManager BM call configure
+ // as soon as it knows parameters: pixFmx, size
+ virtual void Configure(AVPixelFormat format, int size) {}
+
+ // required if pool is registered with BufferManager
+ virtual bool IsConfigured() { return false; }
+
+ // required if pool is registered with BufferManager
+ // called before Get() to check if buffer pool is suitable
+ virtual bool IsCompatible(AVPixelFormat format, int size) { return false; }
+
+ // callback when BM releases buffer pool. i.e. before a new codec is created
+ // clients can register a new pool on this callback
+ virtual void Released(CVideoBufferManager& videoBufferManager) {}
+
+ // called by BM when buffer is discarded
+ // pool calls back when all buffers are back home
+ virtual void Discard(CVideoBufferManager* bm, ReadyToDispose cb) { (bm->*cb)(this); }
+
+ // call on Get() before returning buffer to caller
+ std::shared_ptr<IVideoBufferPool> GetPtr() { return shared_from_this(); }
+};
+
+class CVideoBuffer
+{
+public:
+ CVideoBuffer() = delete;
+ virtual ~CVideoBuffer() = default;
+ void Acquire();
+ void Acquire(std::shared_ptr<IVideoBufferPool> pool);
+ void Release();
+ int GetId() const { return m_id; }
+
+ virtual AVPixelFormat GetFormat();
+ virtual uint8_t* GetMemPtr() { return nullptr; }
+ virtual void GetPlanes(uint8_t* (&planes)[YuvImage::MAX_PLANES]) {}
+ virtual void GetStrides(int (&strides)[YuvImage::MAX_PLANES]) {}
+ virtual void SetDimensions(int width, int height, const int (&strides)[YuvImage::MAX_PLANES]) {}
+ virtual void SetDimensions(int width,
+ int height,
+ const int (&strides)[YuvImage::MAX_PLANES],
+ const int (&planeOffsets)[YuvImage::MAX_PLANES])
+ {
+ }
+
+ static bool CopyPicture(YuvImage* pDst, YuvImage *pSrc);
+ static bool CopyNV12Picture(YuvImage* pDst, YuvImage *pSrc);
+ static bool CopyYUV422PackedPicture(YuvImage* pDst, YuvImage *pSrc);
+
+protected:
+ explicit CVideoBuffer(int id);
+ AVPixelFormat m_pixFormat = AV_PIX_FMT_NONE;
+ std::atomic_int m_refCount;
+ int m_id;
+ std::shared_ptr<IVideoBufferPool> m_pool;
+};
+
+class CVideoBufferSysMem : public CVideoBuffer
+{
+public:
+ CVideoBufferSysMem(IVideoBufferPool &pool, int id, AVPixelFormat format, int size);
+ ~CVideoBufferSysMem() override;
+ uint8_t* GetMemPtr() override;
+ void GetPlanes(uint8_t*(&planes)[YuvImage::MAX_PLANES]) override;
+ void GetStrides(int(&strides)[YuvImage::MAX_PLANES]) override;
+ void SetDimensions(int width, int height, const int (&strides)[YuvImage::MAX_PLANES]) override;
+ void SetDimensions(int width, int height, const int (&strides)[YuvImage::MAX_PLANES], const int (&planeOffsets)[YuvImage::MAX_PLANES]) override;
+ bool Alloc();
+
+protected:
+ int m_width = 0;
+ int m_height = 0;
+ int m_size = 0;
+ uint8_t *m_data = nullptr;
+ YuvImage m_image;
+};
+
+//-----------------------------------------------------------------------------
+//
+//-----------------------------------------------------------------------------
+
+class CVideoBufferPoolSysMem : public IVideoBufferPool
+{
+public:
+ ~CVideoBufferPoolSysMem() override;
+ CVideoBuffer* Get() override;
+ void Return(int id) override;
+ void Configure(AVPixelFormat format, int size) override;
+ bool IsConfigured() override;
+ bool IsCompatible(AVPixelFormat format, int size) override;
+ void Discard(CVideoBufferManager *bm, ReadyToDispose cb) override;
+
+ static std::shared_ptr<IVideoBufferPool> CreatePool();
+
+protected:
+ int m_width = 0;
+ int m_height = 0;
+ int m_size = 0;
+ AVPixelFormat m_pixFormat = AV_PIX_FMT_NONE;
+ bool m_configured = false;
+ CCriticalSection m_critSection;
+ CVideoBufferManager *m_bm = nullptr;
+ ReadyToDispose m_cbDispose;
+
+ std::vector<CVideoBufferSysMem*> m_all;
+ std::deque<int> m_used;
+ std::deque<int> m_free;
+};
+
+//-----------------------------------------------------------------------------
+//
+//-----------------------------------------------------------------------------
+
+typedef std::shared_ptr<IVideoBufferPool> (*CreatePoolFunc)();
+
+class CVideoBufferManager
+{
+public:
+ CVideoBufferManager();
+ void RegisterPool(const std::shared_ptr<IVideoBufferPool>& pool);
+ void RegisterPoolFactory(const std::string& id, CreatePoolFunc createFunc);
+ void ReleasePools();
+ void ReleasePool(IVideoBufferPool *pool);
+ CVideoBuffer* Get(AVPixelFormat format, int size, IVideoBufferPool **pPool);
+ void ReadyForDisposal(IVideoBufferPool *pool);
+
+protected:
+ CCriticalSection m_critSection;
+ std::list<std::shared_ptr<IVideoBufferPool>> m_pools;
+ std::list<std::shared_ptr<IVideoBufferPool>> m_discardedPools;
+ std::map<std::string, CreatePoolFunc> m_poolFactories;
+
+private:
+ CVideoBufferManager (const CVideoBufferManager&) = delete;
+ CVideoBufferManager& operator= (const CVideoBufferManager&) = delete;
+};
diff --git a/xbmc/cores/VideoPlayer/Buffers/VideoBufferDMA.cpp b/xbmc/cores/VideoPlayer/Buffers/VideoBufferDMA.cpp
new file mode 100644
index 0000000..2dd7c13
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Buffers/VideoBufferDMA.cpp
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2005-2020 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 "VideoBufferDMA.h"
+
+#include "ServiceBroker.h"
+#include "utils/BufferObject.h"
+#include "utils/DRMHelpers.h"
+#include "utils/log.h"
+
+extern "C"
+{
+#include <libavutil/imgutils.h>
+#include <libavutil/pixdesc.h>
+}
+
+CVideoBufferDMA::CVideoBufferDMA(IVideoBufferPool& pool, int id, uint32_t fourcc, uint64_t size)
+ : CVideoBufferDRMPRIMEFFmpeg(pool, id),
+ m_bo(CBufferObject::GetBufferObject(true)),
+ m_fourcc(fourcc),
+ m_size(size)
+{
+}
+
+CVideoBufferDMA::~CVideoBufferDMA()
+{
+ Destroy();
+}
+
+AVDRMFrameDescriptor* CVideoBufferDMA::GetDescriptor() const
+{
+ return const_cast<AVDRMFrameDescriptor*>(&m_descriptor);
+}
+
+uint8_t* CVideoBufferDMA::GetMemPtr()
+{
+ return m_addr;
+}
+
+void CVideoBufferDMA::GetPlanes(uint8_t* (&planes)[YuvImage::MAX_PLANES])
+{
+ for (uint32_t i = 0; i < YuvImage::MAX_PLANES; i++)
+ planes[i] = m_addr + m_offsets[i];
+}
+
+void CVideoBufferDMA::GetStrides(int (&strides)[YuvImage::MAX_PLANES])
+{
+ for (uint32_t i = 0; i < YuvImage::MAX_PLANES; i++)
+ strides[i] = m_strides[i];
+}
+
+void CVideoBufferDMA::SetDimensions(int width, int height)
+{
+ SetDimensions(width, height, m_strides, m_offsets);
+}
+
+void CVideoBufferDMA::SetDimensions(int width,
+ int height,
+ const int (&strides)[YuvImage::MAX_PLANES])
+{
+ SetDimensions(width, height, strides, m_offsets);
+}
+
+void CVideoBufferDMA::SetDimensions(int width,
+ int height,
+ const int (&strides)[YuvImage::MAX_PLANES],
+ const int (&planeOffsets)[YuvImage::MAX_PLANES])
+{
+ m_width = width;
+ m_height = height;
+
+ AVDRMFrameDescriptor* descriptor = &m_descriptor;
+ descriptor->nb_objects = 1;
+ descriptor->objects[0].fd = m_fd;
+ descriptor->nb_layers = 1;
+
+ AVDRMLayerDescriptor* layer = &descriptor->layers[0];
+ layer->format = m_fourcc;
+ layer->nb_planes = m_planes;
+
+ for (uint32_t i = 0; i < m_planes; i++)
+ {
+ layer->planes[i].offset = planeOffsets[i];
+ layer->planes[i].pitch = strides[i];
+ }
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGVIDEO))
+ {
+ std::string planeStr;
+ for (uint32_t plane = 0; plane < m_planes; plane++)
+ planeStr.append(fmt::format("\nplane[{}]: stride={}\toffset={}", plane, strides[plane],
+ planeOffsets[plane]));
+
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVideoBufferDMA::{} - frame layout id={} fourcc={}{}",
+ __FUNCTION__, m_id, DRMHELPERS::FourCCToString(m_fourcc), planeStr);
+ }
+}
+
+bool CVideoBufferDMA::Alloc()
+{
+ if (!m_bo->CreateBufferObject(m_size))
+ return false;
+
+ m_fd = m_bo->GetFd();
+ m_addr = m_bo->GetMemory();
+ m_planes = 3; // CAddonVideoCodec only requests AV_PIX_FMT_YUV420P for now
+
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVideoBufferDMA::{} - id={} fourcc={} fd={} size={} addr={}",
+ __FUNCTION__, m_id, DRMHELPERS::FourCCToString(m_fourcc), m_fd, m_size,
+ fmt::ptr(m_addr));
+
+ return true;
+}
+
+void CVideoBufferDMA::Export(AVFrame* frame, uint32_t width, uint32_t height)
+{
+ m_planes = av_pix_fmt_count_planes(static_cast<AVPixelFormat>(frame->format));
+
+ if (m_planes < 2)
+ throw std::runtime_error(
+ "non-planar formats not supported: " +
+ std::string(av_get_pix_fmt_name(static_cast<AVPixelFormat>(frame->format))));
+
+ for (uint32_t plane = 0; plane < m_planes; plane++)
+ {
+ m_strides[plane] =
+ av_image_get_linesize(static_cast<AVPixelFormat>(frame->format), width, plane);
+ m_offsets[plane] =
+ plane == 0 ? 0 : (m_offsets[plane - 1] + m_strides[plane - 1] * (height >> (plane - 1)));
+ }
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGVIDEO))
+ {
+ std::string planeStr;
+ for (uint32_t plane = 0; plane < m_planes; plane++)
+ planeStr.append(fmt::format("\nplane[{}]: stride={}\toffset={}", plane, m_strides[plane],
+ m_offsets[plane]));
+
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVideoBufferDMA::{} - frame layout id={} fourcc={}{}",
+ __FUNCTION__, m_id, DRMHELPERS::FourCCToString(m_fourcc), planeStr);
+ }
+
+ for (uint32_t i = 0; i < AV_NUM_DATA_POINTERS; i++)
+ {
+ frame->data[i] = i < m_planes ? m_addr + m_offsets[i] : nullptr;
+ frame->linesize[i] = i < m_planes ? m_strides[i] : 0;
+ frame->buf[i] = i == 0 ? frame->opaque_ref : nullptr;
+ }
+
+ frame->extended_data = frame->data;
+ frame->opaque_ref = nullptr;
+}
+
+void CVideoBufferDMA::SyncStart()
+{
+ m_bo->SyncStart();
+}
+
+void CVideoBufferDMA::SyncEnd()
+{
+ m_bo->SyncEnd();
+}
+
+void CVideoBufferDMA::Destroy()
+{
+ m_bo->ReleaseMemory();
+ m_bo->DestroyBufferObject();
+
+ for (auto& offset : m_offsets)
+ offset = 0;
+
+ for (auto& stride : m_strides)
+ stride = 0;
+
+ m_planes = 0;
+ m_width = 0;
+ m_height = 0;
+ m_fourcc = 0;
+ m_size = 0;
+ m_addr = nullptr;
+ m_fd = -1;
+}
diff --git a/xbmc/cores/VideoPlayer/Buffers/VideoBufferDMA.h b/xbmc/cores/VideoPlayer/Buffers/VideoBufferDMA.h
new file mode 100644
index 0000000..94eb1c8
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Buffers/VideoBufferDMA.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2005-2020 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/VideoPlayer/Buffers/VideoBufferDRMPRIME.h"
+
+#include <memory>
+
+class IBufferObject;
+
+class CVideoBufferDMA : public CVideoBufferDRMPRIMEFFmpeg
+{
+public:
+ CVideoBufferDMA(IVideoBufferPool& pool, int id, uint32_t fourcc, uint64_t size);
+ ~CVideoBufferDMA() override;
+
+ // implementation of CVideoBufferDRMPRIME via CVideoBufferDRMPRIMEFFmpeg
+ uint32_t GetWidth() const override { return m_width; }
+ uint32_t GetHeight() const override { return m_height; }
+ AVDRMFrameDescriptor* GetDescriptor() const override;
+
+ // implementation of CVideoBuffer via CVideoBufferDRMPRIMEFFmpeg
+ void GetPlanes(uint8_t* (&planes)[YuvImage::MAX_PLANES]) override;
+ void GetStrides(int (&strides)[YuvImage::MAX_PLANES]) override;
+ uint8_t* GetMemPtr() override;
+ void SetDimensions(int width, int height, const int (&strides)[YuvImage::MAX_PLANES]) override;
+ void SetDimensions(int width,
+ int height,
+ const int (&strides)[YuvImage::MAX_PLANES],
+ const int (&planeOffsets)[YuvImage::MAX_PLANES]) override;
+
+ void SetDimensions(int width, int height);
+ bool Alloc();
+ void Export(AVFrame* frame, uint32_t width, uint32_t height);
+
+ void SyncStart();
+ void SyncEnd();
+
+private:
+ void Destroy();
+
+ std::unique_ptr<IBufferObject> m_bo;
+
+ int m_offsets[YuvImage::MAX_PLANES]{0};
+ int m_strides[YuvImage::MAX_PLANES]{0};
+
+ AVDRMFrameDescriptor m_descriptor{};
+ uint32_t m_planes{0};
+ uint32_t m_width{0};
+ uint32_t m_height{0};
+ uint32_t m_fourcc{0};
+ uint64_t m_size{0};
+ uint8_t* m_addr{nullptr};
+ int m_fd{-1};
+};
diff --git a/xbmc/cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.cpp b/xbmc/cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.cpp
new file mode 100644
index 0000000..b1c23ff
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.cpp
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2017-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 "VideoBufferDRMPRIME.h"
+
+#include <mutex>
+
+
+extern "C"
+{
+#include <libavcodec/avcodec.h>
+#include <libavutil/pixdesc.h>
+}
+
+namespace DRMPRIME
+{
+
+std::string GetColorEncoding(const VideoPicture& picture)
+{
+ switch (picture.color_space)
+ {
+ case AVCOL_SPC_BT2020_CL:
+ case AVCOL_SPC_BT2020_NCL:
+ return "ITU-R BT.2020 YCbCr";
+ case AVCOL_SPC_SMPTE170M:
+ case AVCOL_SPC_BT470BG:
+ case AVCOL_SPC_FCC:
+ return "ITU-R BT.601 YCbCr";
+ case AVCOL_SPC_BT709:
+ return "ITU-R BT.709 YCbCr";
+ case AVCOL_SPC_RESERVED:
+ case AVCOL_SPC_UNSPECIFIED:
+ default:
+ if (picture.iWidth > 1024 || picture.iHeight >= 600)
+ return "ITU-R BT.709 YCbCr";
+ else
+ return "ITU-R BT.601 YCbCr";
+ }
+}
+
+std::string GetColorRange(const VideoPicture& picture)
+{
+ if (picture.color_range)
+ return "YCbCr full range";
+ return "YCbCr limited range";
+}
+
+uint8_t GetEOTF(const VideoPicture& picture)
+{
+ switch (picture.color_transfer)
+ {
+ case AVCOL_TRC_SMPTE2084:
+ return HDMI_EOTF_SMPTE_ST2084;
+ case AVCOL_TRC_ARIB_STD_B67:
+ case AVCOL_TRC_BT2020_10:
+ return HDMI_EOTF_BT_2100_HLG;
+ default:
+ return HDMI_EOTF_TRADITIONAL_GAMMA_SDR;
+ }
+}
+
+const AVMasteringDisplayMetadata* GetMasteringDisplayMetadata(const VideoPicture& picture)
+{
+ return picture.hasDisplayMetadata ? &picture.displayMetadata : nullptr;
+}
+
+const AVContentLightMetadata* GetContentLightMetadata(const VideoPicture& picture)
+{
+ return picture.hasLightMetadata ? &picture.lightMetadata : nullptr;
+}
+
+} // namespace DRMPRIME
+
+CVideoBufferDRMPRIME::CVideoBufferDRMPRIME(int id) : CVideoBuffer(id)
+{
+ m_pixFormat = AV_PIX_FMT_DRM_PRIME;
+}
+
+CVideoBufferDRMPRIMEFFmpeg::CVideoBufferDRMPRIMEFFmpeg(IVideoBufferPool& pool, int id)
+ : CVideoBufferDRMPRIME(id)
+{
+ m_pFrame = av_frame_alloc();
+}
+
+CVideoBufferDRMPRIMEFFmpeg::~CVideoBufferDRMPRIMEFFmpeg()
+{
+ Unref();
+ av_frame_free(&m_pFrame);
+}
+
+void CVideoBufferDRMPRIMEFFmpeg::SetRef(AVFrame* frame)
+{
+ av_frame_move_ref(m_pFrame, frame);
+}
+
+void CVideoBufferDRMPRIMEFFmpeg::Unref()
+{
+ av_frame_unref(m_pFrame);
+}
+
+bool CVideoBufferDRMPRIMEFFmpeg::IsValid() const
+{
+ AVDRMFrameDescriptor* descriptor = GetDescriptor();
+ return descriptor && descriptor->nb_layers;
+}
+
+CVideoBufferPoolDRMPRIMEFFmpeg::~CVideoBufferPoolDRMPRIMEFFmpeg()
+{
+ for (auto buf : m_all)
+ delete buf;
+}
+
+CVideoBuffer* CVideoBufferPoolDRMPRIMEFFmpeg::Get()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CVideoBufferDRMPRIMEFFmpeg* buf = nullptr;
+ if (!m_free.empty())
+ {
+ int idx = m_free.front();
+ m_free.pop_front();
+ m_used.push_back(idx);
+ buf = m_all[idx];
+ }
+ else
+ {
+ int id = m_all.size();
+ buf = new CVideoBufferDRMPRIMEFFmpeg(*this, id);
+ m_all.push_back(buf);
+ m_used.push_back(id);
+ }
+
+ buf->Acquire(GetPtr());
+ return buf;
+}
+
+void CVideoBufferPoolDRMPRIMEFFmpeg::Return(int id)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_all[id]->Unref();
+ auto it = m_used.begin();
+ while (it != m_used.end())
+ {
+ if (*it == id)
+ {
+ m_used.erase(it);
+ break;
+ }
+ else
+ ++it;
+ }
+ m_free.push_back(id);
+}
diff --git a/xbmc/cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.h b/xbmc/cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.h
new file mode 100644
index 0000000..e77f75b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.h
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2017-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/VideoPlayer/Buffers/VideoBuffer.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+
+extern "C"
+{
+#include <libavutil/frame.h>
+#include <libavutil/hwcontext_drm.h>
+#include <libavutil/mastering_display_metadata.h>
+}
+
+namespace DRMPRIME
+{
+
+// HDR enums is copied from linux include/linux/hdmi.h (strangely not part of uapi)
+enum hdmi_metadata_type
+{
+ HDMI_STATIC_METADATA_TYPE1 = 0,
+};
+enum hdmi_eotf
+{
+ HDMI_EOTF_TRADITIONAL_GAMMA_SDR,
+ HDMI_EOTF_TRADITIONAL_GAMMA_HDR,
+ HDMI_EOTF_SMPTE_ST2084,
+ HDMI_EOTF_BT_2100_HLG,
+};
+
+std::string GetColorEncoding(const VideoPicture& picture);
+std::string GetColorRange(const VideoPicture& picture);
+uint8_t GetEOTF(const VideoPicture& picture);
+const AVMasteringDisplayMetadata* GetMasteringDisplayMetadata(const VideoPicture& picture);
+const AVContentLightMetadata* GetContentLightMetadata(const VideoPicture& picture);
+
+} // namespace DRMPRIME
+
+class CVideoBufferDRMPRIME : public CVideoBuffer
+{
+public:
+ CVideoBufferDRMPRIME() = delete;
+ ~CVideoBufferDRMPRIME() override = default;
+
+ virtual void SetPictureParams(const VideoPicture& picture) { m_picture.SetParams(picture); }
+ virtual const VideoPicture& GetPicture() const { return m_picture; }
+ virtual uint32_t GetWidth() const { return GetPicture().iWidth; }
+ virtual uint32_t GetHeight() const { return GetPicture().iHeight; }
+
+ virtual AVDRMFrameDescriptor* GetDescriptor() const = 0;
+ virtual bool IsValid() const { return true; }
+ virtual bool AcquireDescriptor() { return true; }
+ virtual void ReleaseDescriptor() {}
+
+ uint32_t m_fb_id = 0;
+ uint32_t m_handles[AV_DRM_MAX_PLANES] = {};
+
+protected:
+ explicit CVideoBufferDRMPRIME(int id);
+
+ VideoPicture m_picture;
+};
+
+class CVideoBufferDRMPRIMEFFmpeg : public CVideoBufferDRMPRIME
+{
+public:
+ CVideoBufferDRMPRIMEFFmpeg(IVideoBufferPool& pool, int id);
+ ~CVideoBufferDRMPRIMEFFmpeg() override;
+ void SetRef(AVFrame* frame);
+ void Unref();
+
+ AVDRMFrameDescriptor* GetDescriptor() const override
+ {
+ return reinterpret_cast<AVDRMFrameDescriptor*>(m_pFrame->data[0]);
+ }
+ bool IsValid() const override;
+
+protected:
+ AVFrame* m_pFrame = nullptr;
+};
+
+class CVideoBufferPoolDRMPRIMEFFmpeg : public IVideoBufferPool
+{
+public:
+ ~CVideoBufferPoolDRMPRIMEFFmpeg() override;
+ void Return(int id) override;
+ CVideoBuffer* Get() override;
+
+protected:
+ CCriticalSection m_critSection;
+ std::vector<CVideoBufferDRMPRIMEFFmpeg*> m_all;
+ std::deque<int> m_used;
+ std::deque<int> m_free;
+};
diff --git a/xbmc/cores/VideoPlayer/Buffers/VideoBufferPoolDMA.cpp b/xbmc/cores/VideoPlayer/Buffers/VideoBufferPoolDMA.cpp
new file mode 100644
index 0000000..fb2dfc6
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Buffers/VideoBufferPoolDMA.cpp
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2005-2020 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 "VideoBufferPoolDMA.h"
+
+#include "cores/VideoPlayer/Buffers/VideoBufferDMA.h"
+#include "utils/BufferObjectFactory.h"
+
+#include <mutex>
+
+#include <drm_fourcc.h>
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+CVideoBufferPoolDMA::~CVideoBufferPoolDMA()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (auto buf : m_all)
+ delete buf;
+}
+
+CVideoBuffer* CVideoBufferPoolDMA::Get()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CVideoBufferDMA* buf = nullptr;
+ if (!m_free.empty())
+ {
+ int idx = m_free.front();
+ m_free.pop_front();
+ m_used.push_back(idx);
+ buf = m_all[idx];
+ }
+ else
+ {
+ int id = m_all.size();
+ buf = new CVideoBufferDMA(*this, id, m_fourcc, m_size);
+
+ if (!buf->Alloc())
+ {
+ delete buf;
+ return nullptr;
+ }
+
+ m_all.push_back(buf);
+ m_used.push_back(id);
+ }
+
+ buf->Acquire(GetPtr());
+ return buf;
+}
+
+void CVideoBufferPoolDMA::Return(int id)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_all[id]->Unref();
+ auto it = m_used.begin();
+ while (it != m_used.end())
+ {
+ if (*it == id)
+ {
+ m_used.erase(it);
+ break;
+ }
+ else
+ ++it;
+ }
+ m_free.push_back(id);
+}
+
+void CVideoBufferPoolDMA::Configure(AVPixelFormat format, int size)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_fourcc = TranslateFormat(format);
+ m_size = static_cast<uint64_t>(size);
+}
+
+bool CVideoBufferPoolDMA::IsConfigured()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ return (m_fourcc != 0 && m_size != 0);
+}
+
+bool CVideoBufferPoolDMA::IsCompatible(AVPixelFormat format, int size)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_fourcc != TranslateFormat(format) || m_size != static_cast<uint64_t>(size))
+ return false;
+
+ return true;
+}
+
+void CVideoBufferPoolDMA::Released(CVideoBufferManager& videoBufferManager)
+{
+ if (!CBufferObjectFactory::CreateBufferObject(true))
+ return;
+
+ videoBufferManager.RegisterPool(std::make_shared<CVideoBufferPoolDMA>());
+}
+
+std::shared_ptr<IVideoBufferPool> CVideoBufferPoolDMA::CreatePool()
+{
+ return std::make_shared<CVideoBufferPoolDMA>();
+}
+
+uint32_t CVideoBufferPoolDMA::TranslateFormat(AVPixelFormat format)
+{
+ switch (format)
+ {
+ case AV_PIX_FMT_YUV420P:
+ case AV_PIX_FMT_YUVJ420P:
+ return DRM_FORMAT_YUV420;
+ default:
+ return 0;
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/Buffers/VideoBufferPoolDMA.h b/xbmc/cores/VideoPlayer/Buffers/VideoBufferPoolDMA.h
new file mode 100644
index 0000000..a490fcb
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Buffers/VideoBufferPoolDMA.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2005-2020 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/VideoPlayer/Buffers/VideoBuffer.h"
+
+#include <memory>
+
+class CVideoBufferDMA;
+
+class CVideoBufferPoolDMA : public IVideoBufferPool
+{
+public:
+ CVideoBufferPoolDMA() = default;
+ ~CVideoBufferPoolDMA() override;
+
+ // implementation of IVideoBufferPool
+ CVideoBuffer* Get() override;
+ void Return(int id) override;
+ void Configure(AVPixelFormat format, int size) override;
+ bool IsConfigured() override;
+ bool IsCompatible(AVPixelFormat format, int size) override;
+ void Released(CVideoBufferManager& videoBufferManager) override;
+
+ static std::shared_ptr<IVideoBufferPool> CreatePool();
+
+protected:
+ CCriticalSection m_critSection;
+ std::vector<CVideoBufferDMA*> m_all;
+ std::deque<int> m_used;
+ std::deque<int> m_free;
+
+private:
+ uint32_t TranslateFormat(AVPixelFormat format);
+
+ uint32_t m_fourcc = 0;
+ uint64_t m_size = 0;
+};
diff --git a/xbmc/cores/VideoPlayer/CMakeLists.txt b/xbmc/cores/VideoPlayer/CMakeLists.txt
new file mode 100644
index 0000000..2c1042c
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/CMakeLists.txt
@@ -0,0 +1,46 @@
+set(SOURCES AudioSinkAE.cpp
+ DVDClock.cpp
+ DVDDemuxSPU.cpp
+ DVDFileInfo.cpp
+ DVDMessage.cpp
+ DVDMessageQueue.cpp
+ DVDOverlayContainer.cpp
+ DVDStreamInfo.cpp
+ PTSTracker.cpp
+ Edl.cpp
+ VideoPlayer.cpp
+ VideoPlayerAudio.cpp
+ VideoPlayerAudioID3.cpp
+ VideoPlayerRadioRDS.cpp
+ VideoPlayerSubtitle.cpp
+ VideoPlayerTeletext.cpp
+ VideoPlayerVideo.cpp
+ VideoReferenceClock.cpp)
+
+set(HEADERS AudioSinkAE.h
+ DVDClock.h
+ DVDDemuxSPU.h
+ DVDFileInfo.h
+ DVDMessage.h
+ DVDMessageQueue.h
+ DVDOverlayContainer.h
+ DVDResource.h
+ DVDStreamInfo.h
+ Edl.h
+ IVideoPlayer.h
+ PTSTracker.h
+ VideoPlayer.h
+ VideoPlayerAudio.h
+ VideoPlayerAudioID3.h
+ VideoPlayerRadioRDS.h
+ VideoPlayerSubtitle.h
+ VideoPlayerTeletext.h
+ VideoPlayerVideo.h
+ VideoReferenceClock.h
+ Interface/StreamInfo.h
+ Interface/DemuxPacket.h
+ Interface/DemuxCrypto.h
+ Interface/InputStreamConstants.h
+ Interface/TimingConstants.h)
+
+core_add_library(VideoPlayer)
diff --git a/xbmc/cores/VideoPlayer/DVDClock.cpp b/xbmc/cores/VideoPlayer/DVDClock.cpp
new file mode 100644
index 0000000..cd2d024
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDClock.cpp
@@ -0,0 +1,309 @@
+/*
+ * 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 "DVDClock.h"
+
+#include "VideoReferenceClock.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/MathUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+
+#include <inttypes.h>
+#include <math.h>
+#include <mutex>
+
+CDVDClock::CDVDClock()
+{
+ std::unique_lock<CCriticalSection> lock(m_systemsection);
+
+ m_pauseClock = 0;
+ m_bReset = true;
+ m_paused = false;
+ m_speedAfterPause = DVD_PLAYSPEED_PAUSE;
+ m_iDisc = 0;
+ m_maxspeedadjust = 5.0;
+ m_systemAdjust = 0;
+ m_speedAdjust = 0;
+ m_startClock = 0;
+ m_vSyncAdjust = 0;
+ m_frameTime = DVD_TIME_BASE / 60.0;
+
+ m_videoRefClock.reset(new CVideoReferenceClock());
+ m_lastSystemTime = m_videoRefClock->GetTime();
+ m_systemOffset = m_videoRefClock->GetTime();
+ m_systemFrequency = CurrentHostFrequency();
+ m_systemUsed = m_systemFrequency;
+}
+
+CDVDClock::~CDVDClock() = default;
+
+// Returns the current absolute clock in units of DVD_TIME_BASE (usually microseconds).
+double CDVDClock::GetAbsoluteClock(bool interpolated /*= true*/)
+{
+ std::unique_lock<CCriticalSection> lock(m_systemsection);
+
+ int64_t current;
+ current = m_videoRefClock->GetTime(interpolated);
+
+ return SystemToAbsolute(current);
+}
+
+double CDVDClock::GetClock(bool interpolated /*= true*/)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ int64_t current = m_videoRefClock->GetTime(interpolated);
+ m_systemAdjust += m_speedAdjust * (current - m_lastSystemTime);
+ m_lastSystemTime = current;
+
+ return SystemToPlaying(current);
+}
+
+double CDVDClock::GetClock(double& absolute, bool interpolated /*= true*/)
+{
+ int64_t current = m_videoRefClock->GetTime(interpolated);
+
+ std::unique_lock<CCriticalSection> lock(m_systemsection);
+ absolute = SystemToAbsolute(current);
+
+ m_systemAdjust += m_speedAdjust * (current - m_lastSystemTime);
+ m_lastSystemTime = current;
+
+ return SystemToPlaying(current);
+}
+
+void CDVDClock::SetVsyncAdjust(double adjustment)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_vSyncAdjust = adjustment;
+}
+
+double CDVDClock::GetVsyncAdjust()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_vSyncAdjust;
+}
+
+void CDVDClock::Pause(bool pause)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (pause && !m_paused)
+ {
+ if (!m_pauseClock)
+ m_speedAfterPause = m_systemFrequency * DVD_PLAYSPEED_NORMAL / m_systemUsed;
+ else
+ m_speedAfterPause = DVD_PLAYSPEED_PAUSE;
+
+ SetSpeed(DVD_PLAYSPEED_PAUSE);
+ m_paused = true;
+ }
+ else if (!pause && m_paused)
+ {
+ m_paused = false;
+ SetSpeed(m_speedAfterPause);
+ }
+}
+
+void CDVDClock::Advance(double time)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_pauseClock)
+ {
+ m_pauseClock += time / DVD_TIME_BASE * m_systemFrequency;
+ }
+}
+
+void CDVDClock::SetSpeed(int iSpeed)
+{
+ // this will sometimes be a little bit of due to rounding errors, ie clock might jump a bit when changing speed
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_paused)
+ {
+ m_speedAfterPause = iSpeed;
+ return;
+ }
+
+ if (iSpeed == DVD_PLAYSPEED_PAUSE)
+ {
+ if (!m_pauseClock)
+ m_pauseClock = m_videoRefClock->GetTime();
+ return;
+ }
+
+ int64_t current;
+ int64_t newfreq = m_systemFrequency * DVD_PLAYSPEED_NORMAL / iSpeed;
+
+ current = m_videoRefClock->GetTime();
+ if (m_pauseClock)
+ {
+ m_startClock += current - m_pauseClock;
+ m_pauseClock = 0;
+ }
+
+ m_startClock = current - (int64_t)((double)(current - m_startClock) * newfreq / m_systemUsed);
+ m_systemUsed = newfreq;
+}
+
+void CDVDClock::SetSpeedAdjust(double adjust)
+{
+ CLog::Log(LOGDEBUG, "CDVDClock::SetSpeedAdjust - adjusted:{:f}", adjust);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_speedAdjust = adjust;
+}
+
+double CDVDClock::GetSpeedAdjust()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_speedAdjust;
+}
+
+double CDVDClock::ErrorAdjust(double error, const char* log)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ double clock, absolute, adjustment;
+ clock = GetClock(absolute);
+
+ // skip minor updates while speed adjust is active
+ // -> adjusting buffer levels
+ if (m_speedAdjust != 0 && error < DVD_MSEC_TO_TIME(100))
+ {
+ return 0;
+ }
+
+ adjustment = error;
+
+ if (m_vSyncAdjust != 0)
+ {
+ // Audio ahead is more noticeable then audio behind video.
+ // Correct if aufio is more than 20ms ahead or more then
+ // 27ms behind. In a worst case scenario we switch from
+ // 20ms ahead to 21ms behind (for fps of 23.976)
+ if (error > 0.02 * DVD_TIME_BASE)
+ adjustment = m_frameTime;
+ else if (error < -0.027 * DVD_TIME_BASE)
+ adjustment = -m_frameTime;
+ else
+ adjustment = 0;
+ }
+
+ if (adjustment == 0)
+ return 0;
+
+ Discontinuity(clock+adjustment, absolute);
+
+ CLog::Log(LOGDEBUG, "CDVDClock::ErrorAdjust - {} - error:{:f}, adjusted:{:f}", log, error,
+ adjustment);
+ return adjustment;
+}
+
+void CDVDClock::Discontinuity(double clock, double absolute)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_startClock = AbsoluteToSystem(absolute);
+ if(m_pauseClock)
+ m_pauseClock = m_startClock;
+ m_iDisc = clock;
+ m_bReset = false;
+ m_systemAdjust = 0;
+ m_speedAdjust = 0;
+}
+
+void CDVDClock::SetMaxSpeedAdjust(double speed)
+{
+ std::unique_lock<CCriticalSection> lock(m_speedsection);
+
+ m_maxspeedadjust = speed;
+}
+
+//returns the refreshrate if the videoreferenceclock is running, -1 otherwise
+int CDVDClock::UpdateFramerate(double fps, double* interval /*= NULL*/)
+{
+ //sent with fps of 0 means we are not playing video
+ if(fps == 0.0)
+ return -1;
+
+ m_frameTime = 1/fps * DVD_TIME_BASE;
+
+ //check if the videoreferenceclock is running, will return -1 if not
+ double rate = m_videoRefClock->GetRefreshRate(interval);
+
+ if (rate <= 0)
+ return -1;
+
+ std::unique_lock<CCriticalSection> lock(m_speedsection);
+
+ double weight = (rate * 2) / fps;
+
+ //set the speed of the videoreferenceclock based on fps, refreshrate and maximum speed adjust set by user
+ if (m_maxspeedadjust > 0.05)
+ {
+ if (weight / MathUtils::round_int(weight) < 1.0 + m_maxspeedadjust / 100.0 &&
+ weight / MathUtils::round_int(weight) > 1.0 - m_maxspeedadjust / 100.0)
+ weight = MathUtils::round_int(weight);
+ }
+ double speed = (rate * 2.0 ) / (fps * weight);
+ lock.unlock();
+
+ m_videoRefClock->SetSpeed(speed);
+
+ return rate;
+}
+
+bool CDVDClock::GetClockInfo(int& MissedVblanks, double& ClockSpeed, double& RefreshRate) const
+{
+ return m_videoRefClock->GetClockInfo(MissedVblanks, ClockSpeed, RefreshRate);
+}
+
+double CDVDClock::SystemToAbsolute(int64_t system)
+{
+ return DVD_TIME_BASE * (double)(system - m_systemOffset) / m_systemFrequency;
+}
+
+int64_t CDVDClock::AbsoluteToSystem(double absolute)
+{
+ return (int64_t)(absolute / DVD_TIME_BASE * m_systemFrequency) + m_systemOffset;
+}
+
+double CDVDClock::SystemToPlaying(int64_t system)
+{
+ int64_t current;
+
+ if (m_bReset)
+ {
+ m_startClock = system;
+ m_systemUsed = m_systemFrequency;
+ if(m_pauseClock)
+ m_pauseClock = m_startClock;
+ m_iDisc = 0;
+ m_systemAdjust = 0;
+ m_speedAdjust = 0;
+ m_vSyncAdjust = 0;
+ m_bReset = false;
+ }
+
+ if (m_pauseClock)
+ current = m_pauseClock;
+ else
+ current = system;
+
+ return DVD_TIME_BASE * (double)(current - m_startClock + m_systemAdjust) / m_systemUsed + m_iDisc;
+}
+
+double CDVDClock::GetClockSpeed()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ double speed = (double)m_systemFrequency / m_systemUsed;
+ return m_videoRefClock->GetSpeed() * speed + m_speedAdjust;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDClock.h b/xbmc/cores/VideoPlayer/DVDClock.h
new file mode 100644
index 0000000..3c3f673
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDClock.h
@@ -0,0 +1,85 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+#include <memory>
+#include <stdint.h>
+
+class CVideoReferenceClock;
+
+class CDVDClock
+{
+public:
+
+ CDVDClock();
+ ~CDVDClock();
+
+ double GetClock(bool interpolated = true);
+ double GetClock(double& absolute, bool interpolated = true);
+
+ double ErrorAdjust(double error, const char* log);
+ void Discontinuity(double clock, double absolute);
+ void Discontinuity(double clock = 0LL)
+ {
+ Discontinuity(clock, GetAbsoluteClock());
+ }
+
+ void Reset() { m_bReset = true; }
+ void SetSpeed(int iSpeed);
+ void SetSpeedAdjust(double adjust);
+ double GetSpeedAdjust();
+
+ double GetClockSpeed(); /**< get the current speed of the clock relative normal system time */
+
+ /* tells clock at what framerate video is, to *
+ * allow it to adjust speed for a better match */
+ int UpdateFramerate(double fps, double* interval = NULL);
+
+ void SetMaxSpeedAdjust(double speed);
+
+ double GetAbsoluteClock(bool interpolated = true);
+ double GetFrequency() { return (double)m_systemFrequency ; }
+
+ bool GetClockInfo(int& MissedVblanks, double& ClockSpeed, double& RefreshRate) const;
+ void SetVsyncAdjust(double adjustment);
+ double GetVsyncAdjust();
+
+ void Pause(bool pause);
+ void Advance(double time);
+
+protected:
+ double SystemToAbsolute(int64_t system);
+ int64_t AbsoluteToSystem(double absolute);
+ double SystemToPlaying(int64_t system);
+
+ CCriticalSection m_critSection;
+ int64_t m_systemUsed;
+ int64_t m_startClock;
+ int64_t m_pauseClock;
+ double m_iDisc;
+ bool m_bReset;
+ bool m_paused;
+ int m_speedAfterPause;
+ std::unique_ptr<CVideoReferenceClock> m_videoRefClock;
+
+ int64_t m_systemFrequency;
+ int64_t m_systemOffset;
+ CCriticalSection m_systemsection;
+
+ int64_t m_systemAdjust;
+ int64_t m_lastSystemTime;
+ double m_speedAdjust;
+ double m_vSyncAdjust;
+ double m_frameTime;
+
+ double m_maxspeedadjust;
+ CCriticalSection m_speedsection;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Audio/CMakeLists.txt b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/CMakeLists.txt
new file mode 100644
index 0000000..91957ce
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES DVDAudioCodecFFmpeg.cpp
+ DVDAudioCodecPassthrough.cpp)
+
+set(HEADERS DVDAudioCodec.h
+ DVDAudioCodecFFmpeg.h
+ DVDAudioCodecPassthrough.h)
+
+if(CORE_SYSTEM_NAME STREQUAL android)
+ list(APPEND SOURCES DVDAudioCodecAndroidMediaCodec.cpp)
+ list(APPEND HEADERS DVDAudioCodecAndroidMediaCodec.h)
+endif()
+
+core_add_library(dvdaudiocodecs)
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodec.h b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodec.h
new file mode 100644
index 0000000..b92f41a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodec.h
@@ -0,0 +1,123 @@
+/*
+ * 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/Utils/AEAudioFormat.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+
+#include <vector>
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+}
+
+struct AVStream;
+
+class CDVDStreamInfo;
+class CDVDCodecOption;
+class CDVDCodecOptions;
+
+typedef struct stDVDAudioFrame
+{
+ uint8_t* data[16];
+ double pts;
+ bool hasTimestamp;
+ double duration;
+ unsigned int nb_frames;
+ unsigned int framesOut;
+ unsigned int framesize;
+ unsigned int planes;
+
+ AEAudioFormat format;
+ int bits_per_sample;
+ bool passthrough;
+ enum AVAudioServiceType audio_service_type;
+ enum AVMatrixEncoding matrix_encoding;
+ int profile;
+ bool hasDownmix;
+ double centerMixLevel;
+} DVDAudioFrame;
+
+class CDVDAudioCodec
+{
+public:
+
+ explicit CDVDAudioCodec(CProcessInfo &processInfo) : m_processInfo(processInfo) {}
+ virtual ~CDVDAudioCodec() = default;
+
+ /*
+ * Open the decoder, returns true on success
+ */
+ virtual bool Open(CDVDStreamInfo &hints, CDVDCodecOptions &options) = 0;
+
+ /*
+ * Dispose, Free all resources
+ */
+ virtual void Dispose() = 0;
+
+ /*
+ * returns false on error
+ *
+ */
+ virtual bool AddData(const DemuxPacket &packet) = 0;
+
+ /*
+ * the data is valid until the next call
+ */
+ virtual void GetData(DVDAudioFrame &frame) = 0;
+
+ /*
+ * resets the decoder
+ */
+ virtual void Reset() = 0;
+
+ /*
+ * returns the format for the audio stream
+ */
+ virtual AEAudioFormat GetFormat() = 0;
+
+ /*
+ * should return the average input bit rate
+ */
+ virtual int GetBitRate() { return 0; }
+
+ /*
+ * returns if the codec requests to use passthrough
+ */
+ virtual bool NeedPassthrough() { return false; }
+
+ /*
+ * should return codecs name
+ */
+ virtual std::string GetName() = 0;
+
+ /*
+ * should return amount of data decoded has buffered in preparation for next audio frame
+ */
+ virtual int GetBufferSize() { return 0; }
+
+ /*
+ * should return the ffmpeg matrix encoding type
+ */
+ virtual enum AVMatrixEncoding GetMatrixEncoding() { return AV_MATRIX_ENCODING_NONE; }
+
+ /*
+ * should return the ffmpeg audio service type
+ */
+ virtual enum AVAudioServiceType GetAudioServiceType() { return AV_AUDIO_SERVICE_TYPE_MAIN; }
+
+ /*
+ * should return the ffmpeg profile value
+ */
+ virtual int GetProfile() { return 0; }
+
+protected:
+ CProcessInfo &m_processInfo;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecAndroidMediaCodec.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecAndroidMediaCodec.cpp
new file mode 100644
index 0000000..9d38db2
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecAndroidMediaCodec.cpp
@@ -0,0 +1,807 @@
+/*
+ * Copyright (C) 2016 Christian Browet
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+// http://developer.android.com/reference/android/media/MediaCodec.html
+//
+// Android MediaCodec class can be used to access low-level media codec,
+// i.e. encoder/decoder components. (android.media.MediaCodec). Requires
+// SDK16+ which is 4.1 Jellybean and above.
+//
+
+#include "DVDAudioCodecAndroidMediaCodec.h"
+
+#include "DVDAudioCodecFFmpeg.h"
+#include "DVDAudioCodecPassthrough.h"
+#include "DVDCodecs/DVDCodecs.h"
+#include "DVDCodecs/DVDFactoryCodec.h"
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "cores/VideoPlayer/Interface/DemuxCrypto.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <cassert>
+#include <stdexcept>
+
+#include <androidjni/ByteBuffer.h>
+#include <androidjni/MediaCodec.h>
+#include <androidjni/MediaCodecCryptoInfo.h>
+#include <androidjni/MediaCodecInfo.h>
+#include <androidjni/MediaCodecList.h>
+#include <androidjni/MediaCrypto.h>
+#include <androidjni/MediaFormat.h>
+#include <androidjni/Surface.h>
+#include <androidjni/UUID.h>
+
+static const AEChannel KnownChannels[] = { AE_CH_FL, AE_CH_FR, AE_CH_FC, AE_CH_LFE, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR, AE_CH_BC, AE_CH_BLOC, AE_CH_BROC, AE_CH_NULL };
+
+static bool IsDownmixDecoder(const std::string &name)
+{
+ static const char *downmixDecoders[] = {
+ "OMX.dolby",
+ // End of list
+ NULL
+ };
+ for (const char **ptr = downmixDecoders; *ptr; ptr++)
+ {
+ if (!StringUtils::CompareNoCase(*ptr, name, strlen(*ptr)))
+ return true;
+ }
+ return false;
+}
+
+static bool IsDecoderWhitelisted(const std::string &name)
+{
+ static const char *whitelistDecoders[] = {
+ // End of list
+ NULL
+ };
+ for (const char **ptr = whitelistDecoders; *ptr; ptr++)
+ {
+ if (!StringUtils::CompareNoCase(*ptr, name, strlen(*ptr)))
+ return true;
+ }
+ return false;
+}
+
+/****************************/
+
+CDVDAudioCodecAndroidMediaCodec::CDVDAudioCodecAndroidMediaCodec(CProcessInfo &processInfo) :
+ CDVDAudioCodec(processInfo),
+ m_formatname("mediacodec"),
+ m_opened(false),
+ m_codecIsFed(false),
+ m_samplerate(0),
+ m_channels(0),
+ m_buffer(NULL),
+ m_bufferSize(0),
+ m_bufferUsed(0),
+ m_currentPts(DVD_NOPTS_VALUE),
+ m_crypto(nullptr)
+{
+}
+
+CDVDAudioCodecAndroidMediaCodec::~CDVDAudioCodecAndroidMediaCodec()
+{
+ Dispose();
+}
+
+std::unique_ptr<CDVDAudioCodec> CDVDAudioCodecAndroidMediaCodec::Create(CProcessInfo& processInfo)
+{
+ return std::make_unique<CDVDAudioCodecAndroidMediaCodec>(processInfo);
+}
+
+bool CDVDAudioCodecAndroidMediaCodec::Register()
+{
+ CDVDFactoryCodec::RegisterHWAudioCodec("mediacodec_dec", &CDVDAudioCodecAndroidMediaCodec::Create);
+ return true;
+}
+
+bool CDVDAudioCodecAndroidMediaCodec::Open(CDVDStreamInfo &hints, CDVDCodecOptions &options)
+{
+ m_hints = hints;
+
+ CLog::Log(LOGDEBUG,
+ "CDVDAudioCodecAndroidMediaCodec::Open codec({}), profile({}), tag({}), extrasize({})",
+ hints.codec, hints.profile, hints.codec_tag, hints.extrasize);
+
+ // First check if passthrough decoder is supported
+ CAEStreamInfo::DataType ptStreamType = CAEStreamInfo::STREAM_TYPE_NULL;
+ for (const auto &key : options.m_keys)
+ if (key.m_name == "ptstreamtype")
+ {
+ ptStreamType = static_cast<CAEStreamInfo::DataType>(atoi(key.m_value.c_str()));
+ break;
+ }
+
+ if (ptStreamType != CAEStreamInfo::STREAM_TYPE_NULL)
+ {
+ //Look if the PT decoder can be opened
+ m_decryptCodec = std::shared_ptr<CDVDAudioCodec>(new CDVDAudioCodecPassthrough(m_processInfo, ptStreamType));
+ if (m_decryptCodec->Open(hints, options))
+ goto PROCESSDECODER;
+ }
+
+ switch(m_hints.codec)
+ {
+ case AV_CODEC_ID_AAC:
+ case AV_CODEC_ID_AAC_LATM:
+ if (!m_hints.extrasize)
+ {
+ CLog::Log(LOGINFO, "CDVDAudioCodecAndroidMediaCodec: extradata required for aac decoder!");
+ return false;
+ }
+
+ m_mime = "audio/mp4a-latm";
+ m_formatname = "amc-aac";
+ break;
+
+ case AV_CODEC_ID_MP2:
+ m_mime = "audio/mpeg-L2";
+ m_formatname = "amc-mp2";
+ break;
+
+ case AV_CODEC_ID_MP3:
+ m_mime = "audio/mpeg";
+ m_formatname = "amc-mp3";
+ break;
+
+ case AV_CODEC_ID_VORBIS:
+ m_mime = "audio/vorbis";
+ m_formatname = "amc-ogg";
+
+ //TODO
+ return false;
+
+ break;
+
+ case AV_CODEC_ID_WMAPRO:
+ m_mime = "audio/wmapro";
+ m_formatname = "amc-wma";
+
+ //TODO
+ return false;
+
+ break;
+
+ case AV_CODEC_ID_WMAV1:
+ case AV_CODEC_ID_WMAV2:
+ m_mime = "audio/x-ms-wma";
+ m_formatname = "amc-wma";
+ //TODO
+ return false;
+
+ break;
+
+ case AV_CODEC_ID_AC3:
+ m_mime = "audio/ac3";
+ m_formatname = "amc-ac3";
+ break;
+
+ case AV_CODEC_ID_EAC3:
+ m_mime = "audio/eac3";
+ m_formatname = "amc-eac3";
+ break;
+
+ default:
+ CLog::Log(LOGINFO, "CDVDAudioCodecAndroidMediaCodec: Unknown hints.codec({})", hints.codec);
+ return false;
+ break;
+ }
+
+ {
+ //StereoDownmixAllowed is true if the user has selected 2.0 Audio channels in settings
+ bool stereoDownmixAllowed = CServiceBroker::GetActiveAE()->HasStereoAudioChannelCount();
+ const std::vector<CJNIMediaCodecInfo> codecInfos =
+ CJNIMediaCodecList(CJNIMediaCodecList::REGULAR_CODECS).getCodecInfos();
+ std::vector<std::string> mimeTypes;
+
+ for (const CJNIMediaCodecInfo& codec_info : codecInfos)
+ {
+ if (codec_info.isEncoder())
+ continue;
+
+ std::string codecName = codec_info.getName();
+
+ if (!IsDecoderWhitelisted(codecName))
+ continue;
+
+ if (m_hints.channels > 2 && !stereoDownmixAllowed && IsDownmixDecoder(codecName))
+ continue;
+
+ mimeTypes = codec_info.getSupportedTypes();
+ if (std::find(mimeTypes.begin(), mimeTypes.end(), m_mime) != mimeTypes.end())
+ {
+ m_codec = std::shared_ptr<CJNIMediaCodec>(new CJNIMediaCodec(CJNIMediaCodec::createByCodecName(codecName)));
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ m_codec = NULL;
+ continue;
+ }
+ CLog::Log(LOGINFO, "CDVDAudioCodecAndroidMediaCodec: Selected audio decoder: {}",
+ codecName);
+ break;
+ }
+ }
+ }
+
+PROCESSDECODER:
+
+ if (m_crypto)
+ delete m_crypto;
+
+ if (m_hints.cryptoSession)
+ {
+ CLog::Log(LOGDEBUG, "CDVDAudioCodecAndroidMediaCodec::Open Initializing MediaCrypto");
+
+ CJNIUUID uuid(jni::jhobject(NULL));
+ if (m_hints.cryptoSession->keySystem == CRYPTO_SESSION_SYSTEM_WIDEVINE)
+ uuid = CJNIUUID(0xEDEF8BA979D64ACEULL, 0xA3C827DCD51D21EDULL);
+ else if (m_hints.cryptoSession->keySystem == CRYPTO_SESSION_SYSTEM_PLAYREADY)
+ uuid = CJNIUUID(0x9A04F07998404286ULL, 0xAB92E65BE0885F95ULL);
+ else
+ {
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::Open Unsupported crypto-keysystem:{}",
+ m_hints.cryptoSession->keySystem);
+ return false;
+ }
+
+ m_crypto =
+ new CJNIMediaCrypto(uuid, std::vector<char>(m_hints.cryptoSession->sessionId.begin(),
+ m_hints.cryptoSession->sessionId.end()));
+
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "MediaCrypto::ExceptionCheck: <init>");
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ return false;
+ }
+ }
+ else
+ m_crypto = new CJNIMediaCrypto(jni::jhobject(NULL));
+
+ if (!m_codec)
+ {
+ if (m_hints.cryptoSession)
+ {
+ m_mime = "audio/raw";
+
+ // Workaround for old Android devices
+ // Prefer the Google raw decoder over the MediaTek one
+ const std::vector<CJNIMediaCodecInfo> codecInfos =
+ CJNIMediaCodecList(CJNIMediaCodecList::REGULAR_CODECS).getCodecInfos();
+
+ bool mtk_raw_decoder = false;
+ bool google_raw_decoder = false;
+
+ for (const CJNIMediaCodecInfo& codec_info : codecInfos)
+ {
+ if (codec_info.isEncoder())
+ continue;
+
+ if (codec_info.getName() == "OMX.MTK.AUDIO.DECODER.RAW")
+ mtk_raw_decoder = true;
+ if (codec_info.getName() == "OMX.google.raw.decoder")
+ google_raw_decoder = true;
+ }
+
+ if (CJNIBase::GetSDKVersion() <= 27 && mtk_raw_decoder && google_raw_decoder)
+ {
+ CLog::Log(LOGDEBUG, "CDVDAudioCodecAndroidMediaCodec::Open Prefer the Google raw decoder "
+ "over the MediaTek one");
+ m_codec = std::shared_ptr<CJNIMediaCodec>(
+ new CJNIMediaCodec(CJNIMediaCodec::createByCodecName("OMX.google.raw.decoder")));
+ }
+ else
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "CDVDAudioCodecAndroidMediaCodec::Open Use the raw decoder proposed by the platform");
+ m_codec = std::shared_ptr<CJNIMediaCodec>(
+ new CJNIMediaCodec(CJNIMediaCodec::createDecoderByType(m_mime)));
+ }
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::Open Failed creating raw decoder");
+ return false;
+ }
+ if (!m_decryptCodec)
+ {
+ CDVDStreamInfo ffhints = hints;
+ ffhints.cryptoSession = nullptr;
+
+ m_decryptCodec = std::shared_ptr<CDVDAudioCodec>(new CDVDAudioCodecFFmpeg(m_processInfo));
+ if (!m_decryptCodec->Open(ffhints, options))
+ {
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::Open() Failed opening FFmpeg decoder");
+ return false;
+ }
+ }
+ CLog::Log(LOGINFO, "CDVDAudioCodecAndroidMediaCodec Use raw decoder and decode using {}",
+ m_decryptCodec->GetName());
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "CDVDAudioCodecAndroidMediaCodec::Open() Use default handling for non encrypted stream");
+ return false;
+ }
+ }
+
+ if (!ConfigureMediaCodec())
+ {
+ m_codec.reset();
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CDVDAudioCodecAndroidMediaCodec Open Android MediaCodec {}", m_formatname);
+
+ m_opened = true;
+ m_codecIsFed = false;
+ m_currentPts = DVD_NOPTS_VALUE;
+ return m_opened;
+}
+
+std::string CDVDAudioCodecAndroidMediaCodec::GetName()
+{
+ if (m_decryptCodec)
+ return "amc-raw/" + m_decryptCodec->GetName();
+ return m_formatname;
+};
+
+void CDVDAudioCodecAndroidMediaCodec::Dispose()
+{
+ if (!m_opened)
+ return;
+
+ m_opened = false;
+
+ if (m_codec)
+ {
+ m_codec->stop();
+ m_codec->release();
+ m_codec.reset();
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ }
+ }
+
+ if (m_crypto)
+ {
+ delete m_crypto;
+ m_crypto = nullptr;
+ }
+ m_decryptCodec = nullptr;
+}
+
+bool CDVDAudioCodecAndroidMediaCodec::AddData(const DemuxPacket &packet)
+{
+ CLog::Log(LOGDEBUG, LOGAUDIO,
+ "CDVDAudioCodecAndroidMediaCodec::AddData dts:{:0.4f} pts:{:0.4f} size({})", packet.dts,
+ packet.pts, packet.iSize);
+
+ if (packet.pData)
+ {
+ // try to fetch an input buffer
+ int64_t timeout_us = 5000;
+ int index = m_codec->dequeueInputBuffer(timeout_us);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ std::string err = CJNIBase::ExceptionToString();
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::AddData ExceptionCheck \n {}", err);
+ }
+ else if (index >= 0)
+ {
+ CJNIByteBuffer buffer = m_codec->getInputBuffer(index);
+ int size = buffer.capacity();
+
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "CDVDMediaCodecInfo::AddData getInputBuffers ExceptionCheck");
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ }
+
+ if (packet.iSize > size)
+ {
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::AddData, iSize({}) > size({})",
+ packet.iSize, size);
+ return packet.iSize;
+ }
+ // fetch a pointer to the ByteBuffer backing store
+ uint8_t *dst_ptr = (uint8_t*)xbmc_jnienv()->GetDirectBufferAddress(buffer.get_raw());
+
+ if (dst_ptr)
+ {
+ // Codec specifics
+ switch(m_hints.codec)
+ {
+ default:
+ memcpy(dst_ptr, packet.pData, packet.iSize);
+ break;
+ }
+ }
+ else
+ return false;
+
+ CJNIMediaCodecCryptoInfo *cryptoInfo(0);
+ if (!!m_crypto->get_raw() && packet.cryptoInfo)
+ {
+ if (CJNIBase::GetSDKVersion() < 25 &&
+ packet.cryptoInfo->mode == CJNIMediaCodec::CRYPTO_MODE_AES_CBC)
+ {
+ CLog::LogF(LOGERROR, "Device API does not support CBCS decryption");
+ return false;
+ }
+ cryptoInfo = new CJNIMediaCodecCryptoInfo();
+ cryptoInfo->set(
+ packet.cryptoInfo->numSubSamples,
+ std::vector<int>(packet.cryptoInfo->clearBytes,
+ packet.cryptoInfo->clearBytes + packet.cryptoInfo->numSubSamples),
+ std::vector<int>(packet.cryptoInfo->cipherBytes,
+ packet.cryptoInfo->cipherBytes + packet.cryptoInfo->numSubSamples),
+ std::vector<char>(std::begin(packet.cryptoInfo->kid), std::end(packet.cryptoInfo->kid)),
+ std::vector<char>(std::begin(packet.cryptoInfo->iv), std::end(packet.cryptoInfo->iv)),
+ packet.cryptoInfo->mode == CJNIMediaCodec::CRYPTO_MODE_AES_CBC
+ ? CJNIMediaCodec::CRYPTO_MODE_AES_CBC
+ : CJNIMediaCodec::CRYPTO_MODE_AES_CTR);
+
+ CJNIMediaCodecCryptoInfoPattern cryptoInfoPattern(packet.cryptoInfo->cryptBlocks,
+ packet.cryptoInfo->skipBlocks);
+ cryptoInfo->setPattern(cryptoInfoPattern);
+ }
+
+ int flags = 0;
+ int offset = 0;
+ int64_t presentationTimeUs = static_cast<int64_t>(packet.pts);
+
+ if (!cryptoInfo)
+ m_codec->queueInputBuffer(index, offset, packet.iSize, presentationTimeUs, flags);
+ else
+ {
+ m_codec->queueSecureInputBuffer(index, offset, *cryptoInfo, presentationTimeUs, flags);
+ delete cryptoInfo;
+ }
+
+ // clear any jni exceptions, jni gets upset if we do not.
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::Decode ExceptionCheck");
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ }
+ m_codecIsFed = true;
+ }
+ }
+
+ if (m_decryptCodec)
+ {
+ DemuxPacket newPkt;
+ newPkt.iSize = GetData(&newPkt.pData);
+ newPkt.pts = m_currentPts;
+ newPkt.iStreamId = packet.iStreamId;
+ newPkt.demuxerId = packet.demuxerId;
+ newPkt.iGroupId = packet.iGroupId;
+ newPkt.pSideData = packet.pSideData;
+ newPkt.duration = packet.duration;
+ newPkt.dispTime = packet.dispTime;
+ newPkt.recoveryPoint = packet.recoveryPoint;
+ if (!packet.pData || newPkt.iSize)
+ m_decryptCodec->AddData(newPkt);
+ }
+ else
+ {
+ m_format.m_dataFormat = GetDataFormat();
+ m_format.m_channelLayout = GetChannelMap();
+ m_format.m_sampleRate = GetSampleRate();
+ m_format.m_frameSize = m_format.m_channelLayout.Count() * CAEUtil::DataFormatToBits(m_format.m_dataFormat) >> 3;
+ }
+ return true;
+}
+
+void CDVDAudioCodecAndroidMediaCodec::Reset()
+{
+ if (!m_opened)
+ return;
+
+ if (m_codec && m_codecIsFed)
+ {
+ // now we can flush the actual MediaCodec object
+ m_codec->flush();
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::Reset ExceptionCheck");
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ }
+ }
+ m_codecIsFed = false;
+
+ if (m_decryptCodec)
+ m_decryptCodec->Reset();
+
+ m_currentPts = DVD_NOPTS_VALUE;
+}
+
+AEAudioFormat CDVDAudioCodecAndroidMediaCodec::GetFormat()
+{
+ if (m_decryptCodec)
+ return m_decryptCodec->GetFormat();
+
+ return m_format;
+}
+
+CAEChannelInfo CDVDAudioCodecAndroidMediaCodec::GetChannelMap()
+{
+ CAEChannelInfo chaninfo;
+
+ for (int i=0; i<m_channels; ++i)
+ chaninfo += KnownChannels[i];
+
+ return chaninfo;
+}
+
+bool CDVDAudioCodecAndroidMediaCodec::ConfigureMediaCodec(void)
+{
+ // setup a MediaFormat to match the audio content,
+ // used by codec during configure
+ CJNIMediaFormat mediaformat(
+ CJNIMediaFormat::createAudioFormat(m_mime, m_hints.samplerate, m_hints.channels));
+
+ if (!m_decryptCodec)
+ {
+ // handle codec extradata
+ if (m_hints.extrasize)
+ {
+ size_t size = m_hints.extrasize;
+ void *src_ptr = m_hints.extradata;
+ // Allocate a byte buffer via allocateDirect in java instead of NewDirectByteBuffer,
+ // since the latter doesn't allocate storage of its own, and we don't know how long
+ // the codec uses the buffer.
+ CJNIByteBuffer bytebuffer = CJNIByteBuffer::allocateDirect(size);
+ void *dts_ptr = xbmc_jnienv()->GetDirectBufferAddress(bytebuffer.get_raw());
+ memcpy(dts_ptr, src_ptr, size);
+ // codec will automatically handle buffers as extradata
+ // using entries with keys "csd-0", "csd-1", etc.
+ mediaformat.setByteBuffer("csd-0", bytebuffer);
+ }
+ else if (m_hints.codec == AV_CODEC_ID_AAC || m_hints.codec == AV_CODEC_ID_AAC_LATM)
+ {
+ mediaformat.setInteger(CJNIMediaFormat::KEY_IS_ADTS, 1);
+ }
+ }
+
+ // configure and start the codec.
+ // use the MediaFormat that we have setup.
+ // use a null MediaCrypto, our content is not encrypted.
+ // use a null Surface
+ int flags = 0;
+ CJNISurface surface(jni::jhobject(NULL));
+ m_codec->configure(mediaformat, surface, *m_crypto, flags);
+
+ // always, check/clear jni exceptions.
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::ExceptionCheck: configure");
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ return false;
+ }
+
+ m_codec->start();
+ // always, check/clear jni exceptions.
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::ExceptionCheck: start");
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ return false;
+ }
+
+ // There is no guarantee we'll get an INFO_OUTPUT_FORMAT_CHANGED (up to Android 4.3)
+ // Configure the output with defaults
+ if (!m_decryptCodec)
+ ConfigureOutputFormat(&mediaformat);
+
+ return true;
+}
+
+void CDVDAudioCodecAndroidMediaCodec::GetData(DVDAudioFrame &frame)
+{
+ if (m_decryptCodec)
+ {
+ m_decryptCodec->GetData(frame);
+ return;
+ }
+
+ frame.passthrough = false;
+ frame.nb_frames = 0;
+ frame.framesOut = 0;
+ frame.format.m_dataFormat = m_format.m_dataFormat;
+ frame.format.m_channelLayout = m_format.m_channelLayout;
+ frame.framesize = (CAEUtil::DataFormatToBits(frame.format.m_dataFormat) >> 3) * frame.format.m_channelLayout.Count();
+
+ if (frame.framesize == 0)
+ return;
+
+ if (!m_codecIsFed)
+ return;
+
+ frame.nb_frames = GetData(frame.data)/frame.framesize;
+ frame.planes = AE_IS_PLANAR(frame.format.m_dataFormat) ? frame.format.m_channelLayout.Count() : 1;
+ frame.bits_per_sample = CAEUtil::DataFormatToBits(frame.format.m_dataFormat);
+ frame.format.m_sampleRate = m_format.m_sampleRate;
+ frame.pts = m_currentPts;
+ m_currentPts = DVD_NOPTS_VALUE;
+ frame.matrix_encoding = GetMatrixEncoding();
+ frame.audio_service_type = GetAudioServiceType();
+ frame.profile = GetProfile();
+ // compute duration.
+ if (frame.format.m_sampleRate)
+ frame.duration = ((double)frame.nb_frames * DVD_TIME_BASE) / frame.format.m_sampleRate;
+ else
+ frame.duration = 0.0;
+ if (frame.nb_frames > 0 && CServiceBroker::GetLogging().CanLogComponent(LOGAUDIO))
+ CLog::Log(LOGDEBUG, "MediaCodecAudio::GetData: frames:{} pts: {:0.4f}", frame.nb_frames,
+ frame.pts);
+}
+
+int CDVDAudioCodecAndroidMediaCodec::GetData(uint8_t** dst)
+{
+ m_bufferUsed = 0;
+
+ int64_t timeout_us = 10000;
+ CJNIMediaCodecBufferInfo bufferInfo;
+ int index = m_codec->dequeueOutputBuffer(bufferInfo, timeout_us);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ std::string err = CJNIBase::ExceptionToString();
+ CLog::Log(LOGERROR,
+ "CDVDAudioCodecAndroidMediaCodec::GetData ExceptionCheck; dequeueOutputBuffer \n {}",
+ err);
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ return 0;
+ }
+ if (index >= 0)
+ {
+ CJNIByteBuffer buffer = m_codec->getOutputBuffer(index);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR,
+ "CDVDAudioCodecAndroidMediaCodec::GetData ExceptionCheck: getOutputBuffer({})",
+ index);
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ return 0;
+ }
+
+ int flags = bufferInfo.flags();
+ if (flags & CJNIMediaCodec::BUFFER_FLAG_SYNC_FRAME)
+ CLog::Log(LOGDEBUG, "CDVDAudioCodecAndroidMediaCodec:: BUFFER_FLAG_SYNC_FRAME");
+
+ if (flags & CJNIMediaCodec::BUFFER_FLAG_CODEC_CONFIG)
+ CLog::Log(LOGDEBUG, "CDVDAudioCodecAndroidMediaCodec:: BUFFER_FLAG_CODEC_CONFIG");
+
+ if (flags & CJNIMediaCodec::BUFFER_FLAG_END_OF_STREAM)
+ {
+ CLog::Log(LOGDEBUG, "CDVDAudioCodecAndroidMediaCodec:: BUFFER_FLAG_END_OF_STREAM");
+ m_codec->releaseOutputBuffer(index, false);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::GetData ExceptionCheck: releaseOutputBuffer");
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ return 0;
+ }
+ return 0;
+ }
+
+ int size = bufferInfo.size();
+ int offset = bufferInfo.offset();
+
+ if (!buffer.isDirect())
+ CLog::Log(LOGWARNING, "CDVDAudioCodecAndroidMediaCodec:: buffer.isDirect == false");
+
+ if (size && buffer.capacity())
+ {
+ uint8_t *src_ptr = (uint8_t*)xbmc_jnienv()->GetDirectBufferAddress(buffer.get_raw());
+ src_ptr += offset;
+
+ if (size > m_bufferSize)
+ {
+ m_bufferSize = size;
+ m_buffer = (uint8_t*)realloc(m_buffer, m_bufferSize);
+ if (m_buffer == nullptr)
+ throw std::runtime_error("Failed to realloc memory, insufficient memory available");
+ }
+
+ memcpy(m_buffer, src_ptr, size);
+ m_bufferUsed = size;
+ }
+ else
+ return 0;
+
+ m_codec->releaseOutputBuffer(index, false);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::GetData ExceptionCheck: releaseOutputBuffer");
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ }
+
+ CLog::Log(LOGDEBUG, LOGAUDIO, "CDVDAudioCodecAndroidMediaCodec::GetData index({}), size({})",
+ index, m_bufferUsed);
+
+ m_currentPts = bufferInfo.presentationTimeUs() == (int64_t)DVD_NOPTS_VALUE ? DVD_NOPTS_VALUE : bufferInfo.presentationTimeUs();
+
+ // always, check/clear jni exceptions.
+ if (xbmc_jnienv()->ExceptionCheck())
+ xbmc_jnienv()->ExceptionClear();
+ }
+ else if (index == CJNIMediaCodec::INFO_OUTPUT_BUFFERS_CHANGED)
+ {
+ CLog::Log(LOGDEBUG, "CDVDAudioCodecAndroidMediaCodec:: GetData OUTPUT_BUFFERS_CHANGED");
+ }
+ else if (index == CJNIMediaCodec::INFO_OUTPUT_FORMAT_CHANGED)
+ {
+ CJNIMediaFormat mediaformat = m_codec->getOutputFormat();
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::GetData(INFO_OUTPUT_FORMAT_CHANGED) ExceptionCheck: getOutputBuffers");
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ }
+ ConfigureOutputFormat(&mediaformat);
+ }
+ else if (index == CJNIMediaCodec::INFO_TRY_AGAIN_LATER)
+ {
+ // normal dequeueOutputBuffer timeout, ignore it.
+ m_bufferUsed = 0;
+ }
+ else
+ {
+ // we should never get here
+ CLog::Log(LOGERROR, "CDVDAudioCodecAndroidMediaCodec::GetData unknown index({})", index);
+ }
+
+ *dst = m_buffer;
+ return m_bufferUsed;
+}
+
+void CDVDAudioCodecAndroidMediaCodec::ConfigureOutputFormat(CJNIMediaFormat* mediaformat)
+{
+ m_samplerate = 0;
+ m_channels = 0;
+
+ if (mediaformat->containsKey("sample-rate"))
+ m_samplerate = mediaformat->getInteger("sample-rate");
+ if (mediaformat->containsKey("channel-count"))
+ m_channels = mediaformat->getInteger("channel-count");
+
+#if 1 //defined(DEBUG_VERBOSE)
+ CLog::Log(LOGDEBUG,
+ "CDVDAudioCodecAndroidMediaCodec:: "
+ "sample_rate({}), channel_count({})",
+ m_samplerate, m_channels);
+#endif
+
+ // clear any jni exceptions
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecAndroidMediaCodec.h b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecAndroidMediaCodec.h
new file mode 100644
index 0000000..f084765
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecAndroidMediaCodec.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 Christian Browet
+ * 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 "DVDAudioCodec.h"
+#include "DVDStreamInfo.h"
+#include "cores/AudioEngine/Utils/AEAudioFormat.h"
+
+#include <memory>
+#include <queue>
+#include <vector>
+
+class CJNIMediaCodec;
+class CJNIMediaCrypto;
+class CJNIMediaFormat;
+class CJNIByteBuffer;
+class CProcessInfo;
+
+struct DemuxPacket;
+
+class CDVDAudioCodecAndroidMediaCodec : public CDVDAudioCodec
+{
+public:
+ CDVDAudioCodecAndroidMediaCodec(CProcessInfo &processInfo);
+ ~CDVDAudioCodecAndroidMediaCodec() override;
+
+ // registration
+ static std::unique_ptr<CDVDAudioCodec> Create(CProcessInfo& processInfo);
+ static bool Register();
+
+ // required overrides
+public:
+ bool Open(CDVDStreamInfo &hints, CDVDCodecOptions &options) override;
+ void Dispose() override;
+ bool AddData(const DemuxPacket &packet) override;
+ void GetData(DVDAudioFrame &frame) override;
+ void Reset() override;
+ AEAudioFormat GetFormat() override;
+ std::string GetName() override;
+
+protected:
+ int GetData(uint8_t** dst);
+ int GetChannels() { return m_channels; }
+ int GetEncodedChannels() { return m_channels; }
+ CAEChannelInfo GetChannelMap();
+ int GetSampleRate() { return m_samplerate; }
+ int GetEncodedSampleRate() { return m_samplerate; }
+ enum AEDataFormat GetDataFormat() { return AE_FMT_S16NE; }
+
+ bool ConfigureMediaCodec(void);
+ void ConfigureOutputFormat(CJNIMediaFormat* mediaformat);
+
+ CDVDStreamInfo m_hints;
+ std::string m_mime;
+ std::string m_codecname;
+ std::string m_formatname;
+ bool m_opened, m_codecIsFed;
+ int m_samplerate;
+ int m_channels;
+ uint8_t* m_buffer;
+ int m_bufferSize;
+ int m_bufferUsed;
+ AEAudioFormat m_format;
+ double m_currentPts;
+
+ std::shared_ptr<CJNIMediaCodec> m_codec;
+ CJNIMediaCrypto *m_crypto;
+ std::shared_ptr<CDVDAudioCodec> m_decryptCodec;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecFFmpeg.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecFFmpeg.cpp
new file mode 100644
index 0000000..25f3f08
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecFFmpeg.cpp
@@ -0,0 +1,402 @@
+/*
+ * 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 "DVDAudioCodecFFmpeg.h"
+#include "ServiceBroker.h"
+#include "../../DVDStreamInfo.h"
+#include "utils/log.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "DVDCodecs/DVDCodecs.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+
+extern "C" {
+#include <libavutil/opt.h>
+}
+
+CDVDAudioCodecFFmpeg::CDVDAudioCodecFFmpeg(CProcessInfo &processInfo) : CDVDAudioCodec(processInfo)
+{
+ m_pCodecContext = NULL;
+
+ m_channels = 0;
+ m_layout = 0;
+
+ m_pFrame = nullptr;
+ m_eof = false;
+}
+
+CDVDAudioCodecFFmpeg::~CDVDAudioCodecFFmpeg()
+{
+ Dispose();
+}
+
+bool CDVDAudioCodecFFmpeg::Open(CDVDStreamInfo &hints, CDVDCodecOptions &options)
+{
+ if (hints.cryptoSession)
+ {
+ CLog::Log(LOGERROR,"CDVDAudioCodecFFmpeg::Open() CryptoSessions unsupported!");
+ return false;
+ }
+
+ AVCodec* pCodec = NULL;
+ bool allowdtshddecode = true;
+
+ // set any special options
+ for(std::vector<CDVDCodecOption>::iterator it = options.m_keys.begin(); it != options.m_keys.end(); ++it)
+ if (it->m_name == "allowdtshddecode")
+ allowdtshddecode = atoi(it->m_value.c_str()) != 0;
+
+ if (hints.codec == AV_CODEC_ID_DTS && allowdtshddecode)
+ pCodec = avcodec_find_decoder_by_name("dcadec");
+
+ if (!pCodec)
+ pCodec = avcodec_find_decoder(hints.codec);
+
+ if (!pCodec)
+ {
+ CLog::Log(LOGDEBUG, "CDVDAudioCodecFFmpeg::Open() Unable to find codec {}", hints.codec);
+ return false;
+ }
+
+ m_pCodecContext = avcodec_alloc_context3(pCodec);
+ if (!m_pCodecContext)
+ return false;
+
+ m_pCodecContext->debug = 0;
+ m_pCodecContext->workaround_bugs = 1;
+
+ if (pCodec->capabilities & AV_CODEC_CAP_TRUNCATED)
+ m_pCodecContext->flags |= AV_CODEC_FLAG_TRUNCATED;
+
+ m_matrixEncoding = AV_MATRIX_ENCODING_NONE;
+ m_channels = 0;
+ m_pCodecContext->channels = hints.channels;
+ m_hint_layout = hints.channellayout;
+ m_pCodecContext->sample_rate = hints.samplerate;
+ m_pCodecContext->block_align = hints.blockalign;
+ m_pCodecContext->bit_rate = hints.bitrate;
+ m_pCodecContext->bits_per_coded_sample = hints.bitspersample;
+
+ if(m_pCodecContext->bits_per_coded_sample == 0)
+ m_pCodecContext->bits_per_coded_sample = 16;
+
+ if( hints.extradata && hints.extrasize > 0 )
+ {
+ m_pCodecContext->extradata = (uint8_t*)av_mallocz(hints.extrasize + AV_INPUT_BUFFER_PADDING_SIZE);
+ if(m_pCodecContext->extradata)
+ {
+ m_pCodecContext->extradata_size = hints.extrasize;
+ memcpy(m_pCodecContext->extradata, hints.extradata, hints.extrasize);
+ }
+ }
+
+ float applyDrc = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioApplyDrc;
+ if (applyDrc >= 0.0f)
+ av_opt_set_double(m_pCodecContext, "drc_scale", static_cast<double>(applyDrc),
+ AV_OPT_SEARCH_CHILDREN);
+
+ if (avcodec_open2(m_pCodecContext, pCodec, NULL) < 0)
+ {
+ CLog::Log(LOGDEBUG,"CDVDAudioCodecFFmpeg::Open() Unable to open codec");
+ Dispose();
+ return false;
+ }
+
+ m_pFrame = av_frame_alloc();
+ if (!m_pFrame)
+ {
+ Dispose();
+ return false;
+ }
+
+ m_iSampleFormat = AV_SAMPLE_FMT_NONE;
+ m_matrixEncoding = AV_MATRIX_ENCODING_NONE;
+ m_hasDownmix = false;
+
+ m_codecName = "ff-" + std::string(m_pCodecContext->codec->name);
+
+ CLog::Log(LOGINFO, "CDVDAudioCodecFFmpeg::Open() Successful opened audio decoder {}",
+ m_pCodecContext->codec->name);
+
+ return true;
+}
+
+void CDVDAudioCodecFFmpeg::Dispose()
+{
+ av_frame_free(&m_pFrame);
+ avcodec_free_context(&m_pCodecContext);
+}
+
+bool CDVDAudioCodecFFmpeg::AddData(const DemuxPacket &packet)
+{
+ if (!m_pCodecContext)
+ return false;
+
+ if (m_eof)
+ {
+ Reset();
+ }
+
+ AVPacket* avpkt = av_packet_alloc();
+ if (!avpkt)
+ {
+ CLog::Log(LOGERROR, "CDVDAudioCodecFFmpeg::{} - av_packet_alloc failed: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ avpkt->data = packet.pData;
+ avpkt->size = packet.iSize;
+ avpkt->dts = (packet.dts == DVD_NOPTS_VALUE)
+ ? AV_NOPTS_VALUE
+ : static_cast<int64_t>(packet.dts / DVD_TIME_BASE * AV_TIME_BASE);
+ avpkt->pts = (packet.pts == DVD_NOPTS_VALUE)
+ ? AV_NOPTS_VALUE
+ : static_cast<int64_t>(packet.pts / DVD_TIME_BASE * AV_TIME_BASE);
+ avpkt->side_data = static_cast<AVPacketSideData*>(packet.pSideData);
+ avpkt->side_data_elems = packet.iSideDataElems;
+
+ int ret = avcodec_send_packet(m_pCodecContext, avpkt);
+
+ //! @todo: properly handle avpkt side_data. this works around our improper use of the side_data
+ // as we pass pointers to ffmpeg allocated memory for the side_data. we should really be allocating
+ // and storing our own AVPacket. This will require some extensive changes.
+ av_buffer_unref(&avpkt->buf);
+ av_free(avpkt);
+
+ // try again
+ if (ret == AVERROR(EAGAIN))
+ {
+ return false;
+ }
+
+ return true;
+}
+
+void CDVDAudioCodecFFmpeg::GetData(DVDAudioFrame &frame)
+{
+ frame.nb_frames = 0;
+
+ uint8_t* data[16]{};
+ int bytes = GetData(data);
+ if (!bytes)
+ {
+ return;
+ }
+
+ frame.passthrough = false;
+ frame.format.m_dataFormat = m_format.m_dataFormat;
+ frame.format.m_channelLayout = m_format.m_channelLayout;
+ frame.framesize = (CAEUtil::DataFormatToBits(frame.format.m_dataFormat) >> 3) * frame.format.m_channelLayout.Count();
+ if(frame.framesize == 0)
+ return;
+
+ frame.nb_frames = bytes/frame.framesize;
+ frame.framesOut = 0;
+ frame.planes = AE_IS_PLANAR(frame.format.m_dataFormat) ? frame.format.m_channelLayout.Count() : 1;
+
+ for (unsigned int i=0; i<frame.planes; i++)
+ frame.data[i] = data[i];
+
+ frame.bits_per_sample = CAEUtil::DataFormatToBits(frame.format.m_dataFormat);
+ frame.format.m_sampleRate = m_format.m_sampleRate;
+ frame.matrix_encoding = GetMatrixEncoding();
+ frame.audio_service_type = GetAudioServiceType();
+ frame.profile = GetProfile();
+ // compute duration.
+ if (frame.format.m_sampleRate)
+ frame.duration = ((double)frame.nb_frames * DVD_TIME_BASE) / frame.format.m_sampleRate;
+ else
+ frame.duration = 0.0;
+
+ int64_t bpts = m_pFrame->best_effort_timestamp;
+ if(bpts != AV_NOPTS_VALUE)
+ frame.pts = (double)bpts * DVD_TIME_BASE / AV_TIME_BASE;
+ else
+ frame.pts = DVD_NOPTS_VALUE;
+
+ frame.hasDownmix = m_hasDownmix;
+ if (frame.hasDownmix)
+ {
+ frame.centerMixLevel = m_downmixInfo.center_mix_level;
+ }
+}
+
+int CDVDAudioCodecFFmpeg::GetData(uint8_t** dst)
+{
+ int ret = avcodec_receive_frame(m_pCodecContext, m_pFrame);
+ if (!ret)
+ {
+ if (m_pFrame->nb_side_data)
+ {
+ for (int i = 0; i < m_pFrame->nb_side_data; i++)
+ {
+ AVFrameSideData *sd = m_pFrame->side_data[i];
+ if (sd->data)
+ {
+ if (sd->type == AV_FRAME_DATA_MATRIXENCODING)
+ {
+ m_matrixEncoding = *(enum AVMatrixEncoding*)sd->data;
+ }
+ else if (sd->type == AV_FRAME_DATA_DOWNMIX_INFO)
+ {
+ m_downmixInfo = *(AVDownmixInfo*)sd->data;
+ m_hasDownmix = true;
+ }
+ }
+ }
+ }
+
+ m_format.m_dataFormat = GetDataFormat();
+ m_format.m_channelLayout = GetChannelMap();
+ m_format.m_sampleRate = GetSampleRate();
+ m_format.m_frameSize = m_format.m_channelLayout.Count() *
+ CAEUtil::DataFormatToBits(m_format.m_dataFormat) >> 3;
+
+ int planes = av_sample_fmt_is_planar(m_pCodecContext->sample_fmt) ? m_pFrame->channels : 1;
+ for (int i=0; i<planes; i++)
+ dst[i] = m_pFrame->extended_data[i];
+
+ return m_pFrame->nb_samples * m_pFrame->channels *
+ av_get_bytes_per_sample(m_pCodecContext->sample_fmt);
+ }
+
+ return 0;
+}
+
+void CDVDAudioCodecFFmpeg::Reset()
+{
+ if (m_pCodecContext) avcodec_flush_buffers(m_pCodecContext);
+ m_eof = false;
+}
+
+int CDVDAudioCodecFFmpeg::GetChannels()
+{
+ return m_pCodecContext->channels;
+}
+
+int CDVDAudioCodecFFmpeg::GetSampleRate()
+{
+ if (m_pCodecContext)
+ return m_pCodecContext->sample_rate;
+ return 0;
+}
+
+enum AEDataFormat CDVDAudioCodecFFmpeg::GetDataFormat()
+{
+ switch(m_pCodecContext->sample_fmt)
+ {
+ case AV_SAMPLE_FMT_U8 : return AE_FMT_U8;
+ case AV_SAMPLE_FMT_U8P : return AE_FMT_U8P;
+ case AV_SAMPLE_FMT_S16: return AE_FMT_S16NE;
+ case AV_SAMPLE_FMT_S16P: return AE_FMT_S16NEP;
+ case AV_SAMPLE_FMT_S32: return AE_FMT_S32NE;
+ case AV_SAMPLE_FMT_S32P: return AE_FMT_S32NEP;
+ case AV_SAMPLE_FMT_FLT: return AE_FMT_FLOAT;
+ case AV_SAMPLE_FMT_FLTP: return AE_FMT_FLOATP;
+ case AV_SAMPLE_FMT_DBL: return AE_FMT_DOUBLE;
+ case AV_SAMPLE_FMT_DBLP: return AE_FMT_DOUBLEP;
+ case AV_SAMPLE_FMT_NONE:
+ default:
+ CLog::Log(LOGERROR, "CDVDAudioCodecFFmpeg::GetDataFormat - invalid data format");
+ return AE_FMT_INVALID;
+ }
+}
+
+int CDVDAudioCodecFFmpeg::GetBitRate()
+{
+ if (m_pCodecContext)
+ return static_cast<int>(m_pCodecContext->bit_rate);
+ return 0;
+}
+
+enum AVMatrixEncoding CDVDAudioCodecFFmpeg::GetMatrixEncoding()
+{
+ return m_matrixEncoding;
+}
+
+enum AVAudioServiceType CDVDAudioCodecFFmpeg::GetAudioServiceType()
+{
+ if (m_pCodecContext)
+ return m_pCodecContext->audio_service_type;
+ return AV_AUDIO_SERVICE_TYPE_MAIN;
+}
+
+int CDVDAudioCodecFFmpeg::GetProfile()
+{
+ if (m_pCodecContext)
+ return m_pCodecContext->profile;
+ return 0;
+}
+
+static unsigned count_bits(int64_t value)
+{
+ unsigned bits = 0;
+ for(;value;++bits)
+ value &= value - 1;
+ return bits;
+}
+
+void CDVDAudioCodecFFmpeg::BuildChannelMap()
+{
+ if (m_channels == m_pCodecContext->channels && m_layout == m_pCodecContext->channel_layout)
+ return; //nothing to do here
+
+ m_channels = m_pCodecContext->channels;
+ m_layout = m_pCodecContext->channel_layout;
+
+ int64_t layout;
+
+ int bits = count_bits(m_pCodecContext->channel_layout);
+ if (bits == m_pCodecContext->channels)
+ layout = m_pCodecContext->channel_layout;
+ else
+ {
+ CLog::Log(LOGINFO,
+ "CDVDAudioCodecFFmpeg::GetChannelMap - FFmpeg reported {} channels, but the layout "
+ "contains {} - trying hints",
+ m_pCodecContext->channels, bits);
+ if (static_cast<int>(count_bits(m_hint_layout)) == m_pCodecContext->channels)
+ layout = m_hint_layout;
+ else
+ {
+ layout = av_get_default_channel_layout(m_pCodecContext->channels);
+ CLog::Log(LOGINFO, "Using default layout...");
+ }
+ }
+
+ m_channelLayout.Reset();
+
+ if (layout & AV_CH_FRONT_LEFT ) m_channelLayout += AE_CH_FL ;
+ if (layout & AV_CH_FRONT_RIGHT ) m_channelLayout += AE_CH_FR ;
+ if (layout & AV_CH_FRONT_CENTER ) m_channelLayout += AE_CH_FC ;
+ if (layout & AV_CH_LOW_FREQUENCY ) m_channelLayout += AE_CH_LFE ;
+ if (layout & AV_CH_BACK_LEFT ) m_channelLayout += AE_CH_BL ;
+ if (layout & AV_CH_BACK_RIGHT ) m_channelLayout += AE_CH_BR ;
+ if (layout & AV_CH_FRONT_LEFT_OF_CENTER ) m_channelLayout += AE_CH_FLOC;
+ if (layout & AV_CH_FRONT_RIGHT_OF_CENTER) m_channelLayout += AE_CH_FROC;
+ if (layout & AV_CH_BACK_CENTER ) m_channelLayout += AE_CH_BC ;
+ if (layout & AV_CH_SIDE_LEFT ) m_channelLayout += AE_CH_SL ;
+ if (layout & AV_CH_SIDE_RIGHT ) m_channelLayout += AE_CH_SR ;
+ if (layout & AV_CH_TOP_CENTER ) m_channelLayout += AE_CH_TC ;
+ if (layout & AV_CH_TOP_FRONT_LEFT ) m_channelLayout += AE_CH_TFL ;
+ if (layout & AV_CH_TOP_FRONT_CENTER ) m_channelLayout += AE_CH_TFC ;
+ if (layout & AV_CH_TOP_FRONT_RIGHT ) m_channelLayout += AE_CH_TFR ;
+ if (layout & AV_CH_TOP_BACK_LEFT ) m_channelLayout += AE_CH_BL ;
+ if (layout & AV_CH_TOP_BACK_CENTER ) m_channelLayout += AE_CH_BC ;
+ if (layout & AV_CH_TOP_BACK_RIGHT ) m_channelLayout += AE_CH_BR ;
+
+ m_channels = m_pCodecContext->channels;
+}
+
+CAEChannelInfo CDVDAudioCodecFFmpeg::GetChannelMap()
+{
+ BuildChannelMap();
+ return m_channelLayout;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecFFmpeg.h b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecFFmpeg.h
new file mode 100644
index 0000000..3441d8f
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecFFmpeg.h
@@ -0,0 +1,64 @@
+/*
+ * 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 "DVDAudioCodec.h"
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libavutil/avutil.h>
+#include <libavutil/channel_layout.h>
+#include <libswresample/swresample.h>
+#include <libavutil/downmix_info.h>
+}
+
+class CProcessInfo;
+
+class CDVDAudioCodecFFmpeg : public CDVDAudioCodec
+{
+public:
+ explicit CDVDAudioCodecFFmpeg(CProcessInfo &processInfo);
+ ~CDVDAudioCodecFFmpeg() override;
+ bool Open(CDVDStreamInfo &hints,
+ CDVDCodecOptions &options) override;
+ void Dispose() override;
+ bool AddData(const DemuxPacket &packet) override;
+ void GetData(DVDAudioFrame &frame) override;
+ void Reset() override;
+ AEAudioFormat GetFormat() override { return m_format; }
+ std::string GetName() override { return m_codecName; }
+ enum AVMatrixEncoding GetMatrixEncoding() override;
+ enum AVAudioServiceType GetAudioServiceType() override;
+ int GetProfile() override;
+
+protected:
+ int GetData(uint8_t** dst);
+ enum AEDataFormat GetDataFormat();
+ int GetSampleRate();
+ int GetChannels();
+ CAEChannelInfo GetChannelMap();
+ int GetBitRate() override;
+ void BuildChannelMap();
+
+ AEAudioFormat m_format;
+ AVCodecContext* m_pCodecContext;
+ enum AVSampleFormat m_iSampleFormat = AV_SAMPLE_FMT_NONE;
+ CAEChannelInfo m_channelLayout;
+ enum AVMatrixEncoding m_matrixEncoding = AV_MATRIX_ENCODING_NONE;
+ AVFrame* m_pFrame;
+ AVDownmixInfo m_downmixInfo;
+ bool m_hasDownmix = false;
+ bool m_eof;
+ int m_channels;
+ uint64_t m_layout;
+ std::string m_codecName;
+ uint64_t m_hint_layout;
+};
+
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecPassthrough.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecPassthrough.cpp
new file mode 100644
index 0000000..3fa6c58
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecPassthrough.cpp
@@ -0,0 +1,261 @@
+/*
+ * 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 "DVDAudioCodecPassthrough.h"
+
+#include "DVDCodecs/DVDCodecs.h"
+#include "DVDStreamInfo.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+extern "C"
+{
+#include <libavcodec/avcodec.h>
+}
+
+namespace
+{
+constexpr auto TRUEHD_BUF_SIZE = 61440;
+}
+
+CDVDAudioCodecPassthrough::CDVDAudioCodecPassthrough(CProcessInfo &processInfo, CAEStreamInfo::DataType streamType) :
+ CDVDAudioCodec(processInfo)
+{
+ m_format.m_streamInfo.m_type = streamType;
+ m_deviceIsRAW = processInfo.WantsRawPassthrough();
+}
+
+CDVDAudioCodecPassthrough::~CDVDAudioCodecPassthrough(void)
+{
+ Dispose();
+}
+
+bool CDVDAudioCodecPassthrough::Open(CDVDStreamInfo &hints, CDVDCodecOptions &options)
+{
+ m_parser.SetCoreOnly(false);
+ switch (m_format.m_streamInfo.m_type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ m_codecName = "pt-ac3";
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ m_codecName = "pt-eac3";
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_MA:
+ m_codecName = "pt-dtshd";
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_DTSHD:
+ m_codecName = "pt-dtshd";
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ m_codecName = "pt-dts";
+ m_parser.SetCoreOnly(true);
+ break;
+
+ case CAEStreamInfo::STREAM_TYPE_TRUEHD:
+ m_codecName = "pt-truehd";
+
+ CLog::Log(LOGDEBUG, "CDVDAudioCodecPassthrough::{} - passthrough output device is {}",
+ __func__, m_deviceIsRAW ? "RAW" : "IEC");
+ break;
+
+ default:
+ return false;
+ }
+
+ m_dataSize = 0;
+ m_bufferSize = 0;
+ m_backlogSize = 0;
+ m_currentPts = DVD_NOPTS_VALUE;
+ m_nextPts = DVD_NOPTS_VALUE;
+ return true;
+}
+
+void CDVDAudioCodecPassthrough::Dispose()
+{
+ if (m_buffer)
+ {
+ delete[] m_buffer;
+ m_buffer = NULL;
+ }
+
+ free(m_backlogBuffer);
+ m_backlogBuffer = nullptr;
+ m_backlogBufferSize = 0;
+
+ m_bufferSize = 0;
+}
+
+bool CDVDAudioCodecPassthrough::AddData(const DemuxPacket &packet)
+{
+ if (m_backlogSize)
+ {
+ m_dataSize = m_bufferSize;
+ unsigned int consumed = m_parser.AddData(m_backlogBuffer, m_backlogSize, &m_buffer, &m_dataSize);
+ m_bufferSize = std::max(m_bufferSize, m_dataSize);
+ if (consumed != m_backlogSize)
+ {
+ memmove(m_backlogBuffer, m_backlogBuffer+consumed, m_backlogSize-consumed);
+ }
+ m_backlogSize -= consumed;
+ }
+
+ unsigned char *pData(const_cast<uint8_t*>(packet.pData));
+ int iSize(packet.iSize);
+
+ if (pData)
+ {
+ if (m_currentPts == DVD_NOPTS_VALUE)
+ {
+ if (m_nextPts != DVD_NOPTS_VALUE)
+ {
+ m_currentPts = m_nextPts;
+ m_nextPts = packet.pts;
+ }
+ else if (packet.pts != DVD_NOPTS_VALUE)
+ {
+ m_currentPts = packet.pts;
+ }
+ }
+ else
+ {
+ m_nextPts = packet.pts;
+ }
+ }
+
+ if (pData && !m_backlogSize)
+ {
+ if (iSize <= 0)
+ return true;
+
+ m_dataSize = m_bufferSize;
+ int used = m_parser.AddData(pData, iSize, &m_buffer, &m_dataSize);
+ m_bufferSize = std::max(m_bufferSize, m_dataSize);
+
+ if (used != iSize)
+ {
+ if (m_backlogBufferSize < static_cast<unsigned int>(iSize - used))
+ {
+ m_backlogBufferSize = std::max(TRUEHD_BUF_SIZE, iSize - used);
+ m_backlogBuffer = static_cast<uint8_t*>(realloc(m_backlogBuffer, m_backlogBufferSize));
+ }
+ m_backlogSize = iSize - used;
+ memcpy(m_backlogBuffer, pData + used, m_backlogSize);
+ }
+ }
+ else if (pData)
+ {
+ if (m_backlogBufferSize < (m_backlogSize + iSize))
+ {
+ m_backlogBufferSize = std::max(TRUEHD_BUF_SIZE, static_cast<int>(m_backlogSize + iSize));
+ m_backlogBuffer = static_cast<uint8_t*>(realloc(m_backlogBuffer, m_backlogBufferSize));
+ }
+ memcpy(m_backlogBuffer + m_backlogSize, pData, iSize);
+ m_backlogSize += iSize;
+ }
+
+ if (!m_dataSize)
+ return true;
+
+ m_format.m_dataFormat = AE_FMT_RAW;
+ m_format.m_streamInfo = m_parser.GetStreamInfo();
+ m_format.m_sampleRate = m_parser.GetSampleRate();
+ m_format.m_frameSize = 1;
+ CAEChannelInfo layout;
+ for (unsigned int i = 0; i < m_parser.GetChannels(); i++)
+ {
+ layout += AE_CH_RAW;
+ }
+ m_format.m_channelLayout = layout;
+
+ if (m_format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD)
+ {
+ if (m_trueHDBuffer.empty())
+ {
+ m_trueHDBuffer.resize(TRUEHD_BUF_SIZE);
+ m_trueHDoffset = 0;
+ }
+
+ if (m_trueHDoffset == 0)
+ m_trueHDframes = 0;
+
+ memcpy(m_trueHDBuffer.data() + m_trueHDoffset, m_buffer, m_dataSize);
+ m_trueHDoffset += m_dataSize;
+
+ m_trueHDframes++;
+
+ // Only 12 audio units are packed in the buffer to avoid overflows and reduce latency.
+ // Compensates for the small increased latency in CAEBitstreamPacker::PackTrueHD (IEC)
+ // Android IEC packer (RAW) needs 24 audio units.
+ const unsigned int nFrames = m_deviceIsRAW ? 24 : 12;
+
+ if (m_trueHDframes == nFrames)
+ {
+ m_dataSize = TRUEHD_BUF_SIZE;
+ m_trueHDoffset = 0;
+ m_trueHDframes = 0;
+ }
+ else
+ m_dataSize = 0;
+ }
+
+ return true;
+}
+
+void CDVDAudioCodecPassthrough::GetData(DVDAudioFrame &frame)
+{
+ frame.nb_frames = GetData(frame.data);
+ frame.framesOut = 0;
+
+ if (frame.nb_frames == 0)
+ return;
+
+ frame.passthrough = true;
+ frame.format = m_format;
+ frame.planes = 1;
+ frame.bits_per_sample = 8;
+ frame.duration = DVD_MSEC_TO_TIME(frame.format.m_streamInfo.GetDuration());
+ frame.pts = m_currentPts;
+ m_currentPts = DVD_NOPTS_VALUE;
+}
+
+int CDVDAudioCodecPassthrough::GetData(uint8_t** dst)
+{
+ if (!m_dataSize)
+ AddData(DemuxPacket());
+
+ if (m_format.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD)
+ *dst = m_trueHDBuffer.data();
+ else
+ *dst = m_buffer;
+
+ int bytes = m_dataSize;
+ m_dataSize = 0;
+ return bytes;
+}
+
+void CDVDAudioCodecPassthrough::Reset()
+{
+ m_trueHDoffset = 0;
+ m_dataSize = 0;
+ m_bufferSize = 0;
+ m_backlogSize = 0;
+ m_currentPts = DVD_NOPTS_VALUE;
+ m_nextPts = DVD_NOPTS_VALUE;
+ m_parser.Reset();
+}
+
+int CDVDAudioCodecPassthrough::GetBufferSize()
+{
+ return (int)m_parser.GetBufferSize();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecPassthrough.h b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecPassthrough.h
new file mode 100644
index 0000000..09d5b12
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecPassthrough.h
@@ -0,0 +1,56 @@
+/*
+ * 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 "DVDAudioCodec.h"
+#include "cores/AudioEngine/Utils/AEAudioFormat.h"
+#include "cores/AudioEngine/Utils/AEBitstreamPacker.h"
+#include "cores/AudioEngine/Utils/AEStreamInfo.h"
+
+#include <list>
+#include <vector>
+
+class CProcessInfo;
+
+class CDVDAudioCodecPassthrough : public CDVDAudioCodec
+{
+public:
+ CDVDAudioCodecPassthrough(CProcessInfo &processInfo, CAEStreamInfo::DataType streamType);
+ ~CDVDAudioCodecPassthrough() override;
+
+ bool Open(CDVDStreamInfo &hints, CDVDCodecOptions &options) override;
+ void Dispose() override;
+ bool AddData(const DemuxPacket &packet) override;
+ void GetData(DVDAudioFrame &frame) override;
+ void Reset() override;
+ AEAudioFormat GetFormat() override { return m_format; }
+ bool NeedPassthrough() override { return true; }
+ std::string GetName() override { return m_codecName; }
+ int GetBufferSize() override;
+
+private:
+ int GetData(uint8_t** dst);
+ CAEStreamParser m_parser;
+ uint8_t* m_buffer = nullptr;
+ unsigned int m_bufferSize = 0;
+ unsigned int m_dataSize = 0;
+ AEAudioFormat m_format;
+ uint8_t *m_backlogBuffer = nullptr;
+ unsigned int m_backlogBufferSize = 0;
+ unsigned int m_backlogSize = 0;
+ double m_currentPts = DVD_NOPTS_VALUE;
+ double m_nextPts = DVD_NOPTS_VALUE;
+ std::string m_codecName;
+
+ // TrueHD specifics
+ std::vector<uint8_t> m_trueHDBuffer;
+ unsigned int m_trueHDoffset = 0;
+ unsigned int m_trueHDframes = 0;
+ bool m_deviceIsRAW{false};
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/CMakeLists.txt b/xbmc/cores/VideoPlayer/DVDCodecs/CMakeLists.txt
new file mode 100644
index 0000000..16f0141
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/CMakeLists.txt
@@ -0,0 +1,8 @@
+set(SOURCES DVDCodecUtils.cpp
+ DVDFactoryCodec.cpp)
+
+set(HEADERS DVDCodecUtils.h
+ DVDCodecs.h
+ DVDFactoryCodec.h)
+
+core_add_library(dvdcodecs)
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecUtils.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecUtils.cpp
new file mode 100644
index 0000000..5b7f34d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecUtils.cpp
@@ -0,0 +1,77 @@
+/*
+ * 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 "DVDCodecUtils.h"
+
+#include "cores/FFmpeg.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+
+#include <array>
+
+extern "C" {
+#include <libswscale/swscale.h>
+}
+
+bool CDVDCodecUtils::IsVP3CompatibleWidth(int width)
+{
+ // known hardware limitation of purevideo 3 (VP3). (the Nvidia 9400 is a purevideo 3 chip)
+ // from nvidia's linux vdpau README: All current third generation PureVideo hardware
+ // (G98, MCP77, MCP78, MCP79, MCP7A) cannot decode H.264 for the following horizontal resolutions:
+ // 769-784, 849-864, 929-944, 1009–1024, 1793–1808, 1873–1888, 1953–1968 and 2033-2048 pixel.
+ // This relates to the following macroblock sizes.
+ int unsupported[] = {49, 54, 59, 64, 113, 118, 123, 128};
+ for (int u : unsupported)
+ {
+ if (u == (width + 15) / 16)
+ return false;
+ }
+ return true;
+}
+
+double CDVDCodecUtils::NormalizeFrameduration(double frameduration, bool *match)
+{
+ //if the duration is within 20 microseconds of a common duration, use that
+ // clang-format off
+ constexpr std::array<double, 8> durations = {
+ DVD_TIME_BASE * 1.001 / 24.0,
+ DVD_TIME_BASE / 24.0,
+ DVD_TIME_BASE / 25.0,
+ DVD_TIME_BASE * 1.001 / 30.0,
+ DVD_TIME_BASE / 30.0,
+ DVD_TIME_BASE / 50.0,
+ DVD_TIME_BASE * 1.001 / 60.0,
+ DVD_TIME_BASE / 60.0
+ };
+ // clang-format on
+
+ double lowestdiff = DVD_TIME_BASE;
+ int selected = -1;
+ for (size_t i = 0; i < durations.size(); i++)
+ {
+ double diff = fabs(frameduration - durations[i]);
+ if (diff < DVD_MSEC_TO_TIME(0.02) && diff < lowestdiff)
+ {
+ selected = i;
+ lowestdiff = diff;
+ }
+ }
+
+ if (selected != -1)
+ {
+ if (match)
+ *match = true;
+ return durations[selected];
+ }
+ else
+ {
+ if (match)
+ *match = false;
+ return frameduration;
+ }
+}
+
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecUtils.h b/xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecUtils.h
new file mode 100644
index 0000000..37872d1
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecUtils.h
@@ -0,0 +1,18 @@
+/*
+ * 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
+
+
+class CDVDCodecUtils
+{
+public:
+ static bool IsVP3CompatibleWidth(int width);
+ static double NormalizeFrameduration(double frameduration, bool *match = nullptr);
+};
+
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecs.h b/xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecs.h
new file mode 100644
index 0000000..6b68823
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/DVDCodecs.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <string>
+#include <vector>
+
+// special options that can be passed to a codec
+class CDVDCodecOption
+{
+public:
+ CDVDCodecOption(const std::string& name, const std::string& value) : m_name(name), m_value(value) {}
+ std::string m_name;
+ std::string m_value;
+};
+
+class CDVDCodecOptions
+{
+public:
+ std::vector<CDVDCodecOption> m_keys;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.cpp
new file mode 100644
index 0000000..6f9769b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.cpp
@@ -0,0 +1,289 @@
+/*
+ * 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 "DVDFactoryCodec.h"
+
+#include "Audio/DVDAudioCodec.h"
+#include "Audio/DVDAudioCodecFFmpeg.h"
+#include "Audio/DVDAudioCodecPassthrough.h"
+#include "DVDStreamInfo.h"
+#include "Overlay/DVDOverlayCodec.h"
+#include "Overlay/DVDOverlayCodecCCText.h"
+#include "Overlay/DVDOverlayCodecFFmpeg.h"
+#include "Overlay/DVDOverlayCodecSSA.h"
+#include "Overlay/DVDOverlayCodecTX3G.h"
+#include "Overlay/DVDOverlayCodecText.h"
+#include "Overlay/OverlayCodecWebVTT.h"
+#include "Video/AddonVideoCodec.h"
+#include "Video/DVDVideoCodec.h"
+#include "Video/DVDVideoCodecFFmpeg.h"
+#include "addons/AddonProvider.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDCodecs.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <utility>
+
+//------------------------------------------------------------------------------
+// Video
+//------------------------------------------------------------------------------
+
+std::map<std::string, CreateHWVideoCodec> CDVDFactoryCodec::m_hwVideoCodecs;
+std::map<std::string, CreateHWAudioCodec> CDVDFactoryCodec::m_hwAudioCodecs;
+
+std::map<std::string, CreateHWAccel> CDVDFactoryCodec::m_hwAccels;
+
+CCriticalSection videoCodecSection, audioCodecSection;
+
+std::unique_ptr<CDVDVideoCodec> CDVDFactoryCodec::CreateVideoCodec(CDVDStreamInfo& hint,
+ CProcessInfo& processInfo)
+{
+ std::unique_lock<CCriticalSection> lock(videoCodecSection);
+
+ std::unique_ptr<CDVDVideoCodec> pCodec;
+ CDVDCodecOptions options;
+
+ // addon handler for this stream ?
+
+ if (hint.externalInterfaces)
+ {
+ ADDON::AddonInfoPtr addonInfo;
+ KODI_HANDLE parentInstance;
+ hint.externalInterfaces->GetAddonInstance(ADDON::IAddonProvider::INSTANCE_VIDEOCODEC, addonInfo, parentInstance);
+ if (addonInfo && parentInstance)
+ {
+ pCodec = std::make_unique<CAddonVideoCodec>(processInfo, addonInfo, parentInstance);
+ if (pCodec->Open(hint, options))
+ {
+ return pCodec;
+ }
+ }
+ return nullptr;
+ }
+
+ // platform specifig video decoders
+ if (!(hint.codecOptions & CODEC_FORCE_SOFTWARE))
+ {
+ for (auto &codec : m_hwVideoCodecs)
+ {
+ pCodec = CreateVideoCodecHW(codec.first, processInfo);
+ if (pCodec && pCodec->Open(hint, options))
+ {
+ return pCodec;
+ }
+ }
+ if (!(hint.codecOptions & CODEC_ALLOW_FALLBACK))
+ return nullptr;
+ }
+
+ pCodec = std::make_unique<CDVDVideoCodecFFmpeg>(processInfo);
+ if (pCodec->Open(hint, options))
+ {
+ return pCodec;
+ }
+
+ return nullptr;
+}
+
+std::unique_ptr<CDVDVideoCodec> CDVDFactoryCodec::CreateVideoCodecHW(const std::string& id,
+ CProcessInfo& processInfo)
+{
+ std::unique_lock<CCriticalSection> lock(videoCodecSection);
+
+ auto it = m_hwVideoCodecs.find(id);
+ if (it != m_hwVideoCodecs.end())
+ {
+ return it->second(processInfo);
+ }
+
+ return nullptr;
+}
+
+IHardwareDecoder* CDVDFactoryCodec::CreateVideoCodecHWAccel(const std::string& id,
+ CDVDStreamInfo& hint,
+ CProcessInfo& processInfo,
+ AVPixelFormat fmt)
+{
+ std::unique_lock<CCriticalSection> lock(videoCodecSection);
+
+ auto it = m_hwAccels.find(id);
+ if (it != m_hwAccels.end())
+ {
+ return it->second(hint, processInfo, fmt);
+ }
+
+ return nullptr;
+}
+
+
+void CDVDFactoryCodec::RegisterHWVideoCodec(const std::string& id, CreateHWVideoCodec createFunc)
+{
+ std::unique_lock<CCriticalSection> lock(videoCodecSection);
+
+ m_hwVideoCodecs[id] = std::move(createFunc);
+}
+
+void CDVDFactoryCodec::ClearHWVideoCodecs()
+{
+ std::unique_lock<CCriticalSection> lock(videoCodecSection);
+
+ m_hwVideoCodecs.clear();
+}
+
+std::vector<std::string> CDVDFactoryCodec::GetHWAccels()
+{
+ std::unique_lock<CCriticalSection> lock(videoCodecSection);
+
+ std::vector<std::string> ret;
+ ret.reserve(m_hwAccels.size());
+ for (auto &hwaccel : m_hwAccels)
+ {
+ ret.push_back(hwaccel.first);
+ }
+ return ret;
+}
+
+void CDVDFactoryCodec::RegisterHWAccel(const std::string& id, CreateHWAccel createFunc)
+{
+ std::unique_lock<CCriticalSection> lock(videoCodecSection);
+
+ m_hwAccels[id] = std::move(createFunc);
+}
+
+void CDVDFactoryCodec::ClearHWAccels()
+{
+ std::unique_lock<CCriticalSection> lock(videoCodecSection);
+
+ m_hwAccels.clear();
+}
+
+//------------------------------------------------------------------------------
+// Audio
+//------------------------------------------------------------------------------
+
+std::unique_ptr<CDVDAudioCodec> CDVDFactoryCodec::CreateAudioCodec(
+ CDVDStreamInfo& hint,
+ CProcessInfo& processInfo,
+ bool allowpassthrough,
+ bool allowdtshddecode,
+ CAEStreamInfo::DataType ptStreamType)
+{
+ std::unique_ptr<CDVDAudioCodec> pCodec;
+ CDVDCodecOptions options;
+
+ if (allowpassthrough && ptStreamType != CAEStreamInfo::STREAM_TYPE_NULL)
+ options.m_keys.emplace_back("ptstreamtype", StringUtils::SizeToString(ptStreamType));
+
+ if (!allowdtshddecode)
+ options.m_keys.emplace_back("allowdtshddecode", "0");
+
+ // platform specifig audio decoders
+ for (auto &codec : m_hwAudioCodecs)
+ {
+ pCodec = CreateAudioCodecHW(codec.first, processInfo);
+ if (pCodec && pCodec->Open(hint, options))
+ {
+ return pCodec;
+ }
+ }
+
+ // we don't use passthrough if "sync playback to display" is enabled
+ if (allowpassthrough && ptStreamType != CAEStreamInfo::STREAM_TYPE_NULL)
+ {
+ pCodec = std::make_unique<CDVDAudioCodecPassthrough>(processInfo, ptStreamType);
+ if (pCodec->Open(hint, options))
+ {
+ return pCodec;
+ }
+ }
+
+ pCodec = std::make_unique<CDVDAudioCodecFFmpeg>(processInfo);
+ if (pCodec->Open(hint, options))
+ {
+ return pCodec;
+ }
+
+ return nullptr;
+}
+
+void CDVDFactoryCodec::RegisterHWAudioCodec(const std::string& id, CreateHWAudioCodec createFunc)
+{
+ std::unique_lock<CCriticalSection> lock(audioCodecSection);
+
+ m_hwAudioCodecs[id] = std::move(createFunc);
+}
+
+void CDVDFactoryCodec::ClearHWAudioCodecs()
+{
+ std::unique_lock<CCriticalSection> lock(audioCodecSection);
+
+ m_hwAudioCodecs.clear();
+}
+
+std::unique_ptr<CDVDAudioCodec> CDVDFactoryCodec::CreateAudioCodecHW(const std::string& id,
+ CProcessInfo& processInfo)
+{
+ std::unique_lock<CCriticalSection> lock(audioCodecSection);
+
+ auto it = m_hwAudioCodecs.find(id);
+ if (it != m_hwAudioCodecs.end())
+ {
+ return it->second(processInfo);
+ }
+
+ return nullptr;
+}
+
+//------------------------------------------------------------------------------
+// Overlay
+//------------------------------------------------------------------------------
+
+std::unique_ptr<CDVDOverlayCodec> CDVDFactoryCodec::CreateOverlayCodec(CDVDStreamInfo& hint)
+{
+ std::unique_ptr<CDVDOverlayCodec> pCodec;
+ CDVDCodecOptions options;
+
+ switch (hint.codec)
+ {
+ case AV_CODEC_ID_TEXT:
+ {
+ if (hint.source == STREAM_SOURCE_VIDEOMUX)
+ pCodec = std::make_unique<CDVDOverlayCodecCCText>();
+ else
+ pCodec = std::make_unique<CDVDOverlayCodecText>();
+ break;
+ }
+ case AV_CODEC_ID_SUBRIP:
+ pCodec = std::make_unique<CDVDOverlayCodecText>();
+ break;
+
+ case AV_CODEC_ID_SSA:
+ case AV_CODEC_ID_ASS:
+ pCodec = std::make_unique<CDVDOverlayCodecSSA>();
+ break;
+
+ case AV_CODEC_ID_MOV_TEXT:
+ pCodec = std::make_unique<CDVDOverlayCodecTX3G>();
+ break;
+
+ case AV_CODEC_ID_WEBVTT:
+ pCodec = std::make_unique<COverlayCodecWebVTT>();
+ break;
+
+ default:
+ pCodec = std::make_unique<CDVDOverlayCodecFFmpeg>();
+ break;
+ }
+
+ if (pCodec->Open(hint, options))
+ return pCodec;
+
+ return nullptr;
+}
+
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h b/xbmc/cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h
new file mode 100644
index 0000000..5c32add
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h
@@ -0,0 +1,80 @@
+/*
+ * 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/Utils/AEStreamInfo.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+
+#include <functional>
+#include <map>
+#include <string>
+#include <vector>
+
+extern "C" {
+#include <libavutil/pixfmt.h>
+}
+
+class CDVDVideoCodec;
+class CDVDAudioCodec;
+class CDVDOverlayCodec;
+class IHardwareDecoder;
+
+class CDemuxStreamVideo;
+class CDVDStreamInfo;
+class CDVDCodecOption;
+class CDVDCodecOptions;
+
+using CreateHWVideoCodec =
+ std::function<std::unique_ptr<CDVDVideoCodec>(CProcessInfo& processInfo)>;
+using CreateHWAccel = std::function<IHardwareDecoder*(
+ CDVDStreamInfo& hint, CProcessInfo& processInfo, AVPixelFormat fmt)>;
+using CreateHWAudioCodec =
+ std::function<std::unique_ptr<CDVDAudioCodec>(CProcessInfo& processInfo)>;
+
+class CDVDFactoryCodec
+{
+public:
+ static std::unique_ptr<CDVDVideoCodec> CreateVideoCodec(CDVDStreamInfo& hint,
+ CProcessInfo& processInfo);
+
+ static IHardwareDecoder* CreateVideoCodecHWAccel(const std::string& id,
+ CDVDStreamInfo& hint,
+ CProcessInfo& processInfo,
+ AVPixelFormat fmt);
+
+ static std::unique_ptr<CDVDAudioCodec> CreateAudioCodec(CDVDStreamInfo& hint,
+ CProcessInfo& processInfo,
+ bool allowpassthrough,
+ bool allowdtshddecode,
+ CAEStreamInfo::DataType ptStreamType);
+
+ static std::unique_ptr<CDVDOverlayCodec> CreateOverlayCodec(CDVDStreamInfo& hint);
+
+ static void RegisterHWVideoCodec(const std::string& id, CreateHWVideoCodec createFunc);
+ static void ClearHWVideoCodecs();
+
+ static void RegisterHWAccel(const std::string& id, CreateHWAccel createFunc);
+ static std::vector<std::string> GetHWAccels();
+ static void ClearHWAccels();
+
+ static void RegisterHWAudioCodec(const std::string& id, CreateHWAudioCodec createFunc);
+ static void ClearHWAudioCodecs();
+
+
+protected:
+ static std::unique_ptr<CDVDVideoCodec> CreateVideoCodecHW(const std::string& id,
+ CProcessInfo& processInfo);
+ static std::unique_ptr<CDVDAudioCodec> CreateAudioCodecHW(const std::string& id,
+ CProcessInfo& processInfo);
+
+ static std::map<std::string, CreateHWVideoCodec> m_hwVideoCodecs;
+ static std::map<std::string, CreateHWAccel> m_hwAccels;
+ static std::map<std::string, CreateHWAudioCodec> m_hwAudioCodecs;
+};
+
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/CMakeLists.txt b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/CMakeLists.txt
new file mode 100644
index 0000000..666b3bc
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/CMakeLists.txt
@@ -0,0 +1,27 @@
+set(SOURCES DVDOverlayCodec.cpp
+ DVDOverlayCodecFFmpeg.cpp
+ DVDOverlayCodecSSA.cpp
+ DVDOverlayCodecText.cpp
+ DVDOverlayCodecCCText.cpp
+ DVDOverlayCodecTX3G.cpp
+ OverlayCodecWebVTT.cpp
+ contrib/cc_decoder.c
+ contrib/cc_decoder708.cpp)
+
+set(HEADERS DVDOverlay.h
+ DVDOverlayCodec.h
+ DVDOverlayCodecFFmpeg.h
+ DVDOverlayCodecSSA.h
+ DVDOverlayCodecTX3G.h
+ DVDOverlayCodecText.h
+ DVDOverlayCodecCCText.h
+ DVDOverlayImage.h
+ DVDOverlayLibass.h
+ DVDOverlaySSA.h
+ DVDOverlaySpu.h
+ DVDOverlayText.h
+ OverlayCodecWebVTT.h
+ contrib/cc_decoder.h
+ contrib/cc_decoder708.h)
+
+core_add_library(dvdoverlaycodecs)
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlay.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlay.h
new file mode 100644
index 0000000..bf8ff7a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlay.h
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2006-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 <assert.h>
+#include <atomic>
+#include <stdexcept>
+#include <vector>
+
+enum DVDOverlayType
+{
+ DVDOVERLAY_TYPE_NONE = -1,
+ DVDOVERLAY_TYPE_SPU = 1,
+ DVDOVERLAY_TYPE_TEXT = 2,
+ DVDOVERLAY_TYPE_IMAGE = 3,
+ DVDOVERLAY_TYPE_SSA = 4,
+ DVDOVERLAY_TYPE_GROUP = 5,
+};
+
+class CDVDOverlay
+{
+public:
+ explicit CDVDOverlay(DVDOverlayType type)
+ {
+ m_type = type;
+
+ iPTSStartTime = 0LL;
+ iPTSStopTime = 0LL;
+ bForced = false;
+ replace = false;
+ m_references = 1;
+ m_textureid = 0;
+ m_enableTextAlign = false;
+ m_overlayContainerFlushable = true;
+ m_setForcedMargins = false;
+ }
+
+ CDVDOverlay(const CDVDOverlay& src)
+ {
+ m_type = src.m_type;
+ iPTSStartTime = src.iPTSStartTime;
+ iPTSStopTime = src.iPTSStopTime;
+ bForced = src.bForced;
+ replace = src.replace;
+ m_references = 1;
+ m_textureid = 0;
+ m_enableTextAlign = src.m_enableTextAlign;
+ m_overlayContainerFlushable = src.m_overlayContainerFlushable;
+ m_setForcedMargins = src.m_setForcedMargins;
+ }
+
+ virtual ~CDVDOverlay()
+ {
+ assert(m_references == 0);
+ }
+
+ /**
+ * increase the reference counter by one.
+ */
+ CDVDOverlay* Acquire()
+ {
+ m_references++;
+ return this;
+ }
+
+ /**
+ * decrease the reference counter by one.
+ */
+ int Release()
+ {
+ m_references--;
+ int ret = m_references;
+ if (m_references == 0)
+ delete this;
+ return ret;
+ }
+
+ /**
+ * static release function for use with shared ptr for example
+ */
+ static void Release(CDVDOverlay* ov)
+ {
+ ov->Release();
+ }
+
+ bool IsOverlayType(DVDOverlayType type) { return (m_type == type); }
+
+ /**
+ * return a copy to VideoPlayerSubtitle in order to have hw resources cleared
+ * after rendering
+ */
+ virtual CDVDOverlay* Clone() { return Acquire(); }
+
+ /*
+ * \brief Enable the use of text alignment (left/center/right).
+ */
+ virtual void SetTextAlignEnabled(bool enable)
+ {
+ throw std::logic_error("EnableTextAlign method not implemented.");
+ }
+
+ /*
+ * \brief Return true if the text alignment (left/center/right) is enabled otherwise false.
+ */
+ bool IsTextAlignEnabled() { return m_enableTextAlign; }
+
+ /*
+ * \brief Allow/Disallow the overlay container to flush the overlay.
+ */
+ void SetOverlayContainerFlushable(bool isFlushable) { m_overlayContainerFlushable = isFlushable; }
+
+ /*
+ * \brief Return true when the overlay container can flush the overlay on flush events.
+ */
+ bool IsOverlayContainerFlushable() { return m_overlayContainerFlushable; }
+
+ /*
+ * \brief Specify if the margins are handled by the subtitle codec/parser.
+ */
+ void SetForcedMargins(bool setForcedMargins) { m_setForcedMargins = setForcedMargins; }
+
+ /*
+ * \brief Return true if the margins are handled by the subtitle codec/parser.
+ */
+ bool IsForcedMargins() const { return m_setForcedMargins; }
+
+ double iPTSStartTime;
+ double iPTSStopTime;
+ bool bForced; // display, no matter what
+ bool replace; // replace by next nomatter what stoptime it has
+ unsigned long m_textureid;
+
+protected:
+ DVDOverlayType m_type;
+ bool m_enableTextAlign;
+ bool m_overlayContainerFlushable;
+ bool m_setForcedMargins;
+
+private:
+ std::atomic_int m_references;
+};
+
+typedef std::vector<CDVDOverlay*> VecOverlays;
+typedef std::vector<CDVDOverlay*>::iterator VecOverlaysIter;
+
+
+class CDVDOverlayGroup : public CDVDOverlay
+{
+
+public:
+ ~CDVDOverlayGroup() override
+ {
+ for(VecOverlaysIter it = m_overlays.begin(); it != m_overlays.end(); ++it)
+ (*it)->Release();
+ m_overlays.clear();
+ }
+
+ CDVDOverlayGroup()
+ : CDVDOverlay(DVDOVERLAY_TYPE_GROUP)
+ {
+ }
+
+ CDVDOverlayGroup(CDVDOverlayGroup& src)
+ : CDVDOverlay(src)
+ {
+ for(VecOverlaysIter it = src.m_overlays.begin(); it != src.m_overlays.end(); ++it)
+ m_overlays.push_back((*it)->Clone());
+ }
+ VecOverlays m_overlays;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodec.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodec.cpp
new file mode 100644
index 0000000..27d33c7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodec.cpp
@@ -0,0 +1,40 @@
+/*
+ * 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 "DVDOverlayCodec.h"
+
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+
+
+void CDVDOverlayCodec::GetAbsoluteTimes(double& starttime, double& stoptime, DemuxPacket* pkt)
+{
+ if (!pkt)
+ return;
+
+ double duration = 0.0;
+ double pts = starttime;
+
+ // we assume pts from packet is better than what
+ // decoder gives us, only take duration
+ // from decoder if available
+ if (stoptime > starttime)
+ duration = stoptime - starttime;
+ else if (pkt->duration != DVD_NOPTS_VALUE)
+ duration = pkt->duration;
+
+ if (pkt->pts != DVD_NOPTS_VALUE)
+ pts = pkt->pts;
+ else if (pkt->dts != DVD_NOPTS_VALUE)
+ pts = pkt->dts;
+
+ starttime = pts;
+ if (duration > 0)
+ stoptime = pts + duration;
+ else
+ stoptime = 0;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodec.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodec.h
new file mode 100644
index 0000000..05c336d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodec.h
@@ -0,0 +1,99 @@
+/*
+ * 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/VideoPlayer/DVDDemuxers/DVDDemux.h"
+
+#include <string>
+
+#include "PlatformDefs.h"
+
+// VC_ messages, messages can be combined
+enum class OverlayMessage
+{
+ // an error occurred, no other messages will be returned
+ OC_ERROR = 0x00000001,
+
+ // the decoder needs more data
+ OC_BUFFER = 0x00000002,
+
+ // the decoder decoded an overlay, call Decode(NULL, 0) again to parse the rest of the data
+ OC_OVERLAY = 0x00000004,
+
+ // the decoder has decoded the packet, no overlay will be provided because the previous one is still valid
+ OC_DONE = 0x00000008,
+};
+
+class CDVDOverlay;
+class CDVDStreamInfo;
+class CDVDCodecOption;
+class CDVDCodecOptions;
+
+class CDVDOverlayCodec
+{
+public:
+
+ explicit CDVDOverlayCodec(const char* name) : m_codecName(name) {}
+
+ virtual ~CDVDOverlayCodec() = default;
+
+ /*
+ * Open the decoder, returns true on success
+ */
+ virtual bool Open(CDVDStreamInfo &hints, CDVDCodecOptions &options) = 0;
+
+ /*
+ * Dispose, Free all resources
+ */
+ virtual void Dispose() = 0;
+
+ /*
+ * returns one or a combination of VC_ messages
+ * pData and iSize can be NULL, this means we should flush the rest of the data.
+ */
+ virtual OverlayMessage Decode(DemuxPacket* pPacket) = 0;
+
+ /*
+ * Reset the decoder.
+ * Should be the same as calling Dispose and Open after each other
+ */
+ virtual void Reset() = 0;
+
+ /*
+ * Flush the current working packet
+ * This may leave the internal state intact
+ */
+ virtual void Flush() = 0;
+
+ /*
+ * returns a valid overlay or NULL
+ * the data is valid until the next Decode call
+ */
+ virtual CDVDOverlay* GetOverlay() = 0;
+
+ /*
+ * return codecs name
+ */
+ const std::string& GetName() const { return m_codecName; }
+
+protected:
+ /*
+ * \brief Adapts startTime, stopTIme from the subtitle stream (which is relative to stream pts)
+ * so that it returns the absolute start and stop timestamps.
+ */
+ static void GetAbsoluteTimes(double& starttime, double& stoptime, DemuxPacket* pkt);
+
+ struct SubtitlePacketExtraData
+ {
+ double m_chapterStartTime;
+ };
+
+private:
+ std::string m_codecName;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecCCText.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecCCText.cpp
new file mode 100644
index 0000000..347c8b6
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecCCText.cpp
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2005-2021 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 "DVDOverlayCodecCCText.h"
+
+#include "DVDCodecs/DVDCodecs.h"
+#include "DVDOverlayText.h"
+#include "DVDStreamInfo.h"
+#include "cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+namespace
+{
+// Muxed Closed Caption subtitles don't have the stop PTS value,
+// the stop value can be taken from: the start PTS of the next subtitle,
+// or, the start PTS of the next subtitle without text -if any-,
+// otherwise we fallback to 20 secs duration (long duration so as not
+// to cause side effects on texts that expand like karaoke, but slowly)
+constexpr double DEFAULT_DURATION = 20.0 * (double)DVD_TIME_BASE;
+} // namespace
+
+CDVDOverlayCodecCCText::CDVDOverlayCodecCCText() : CDVDOverlayCodec("CC Text Subtitle Decoder")
+{
+ m_pOverlay = nullptr;
+ m_prevSubId = NO_SUBTITLE_ID;
+ m_prevPTSStart = 0.0;
+ m_prevText.clear();
+ m_changePrevStopTime = false;
+}
+
+CDVDOverlayCodecCCText::~CDVDOverlayCodecCCText()
+{
+ Dispose();
+}
+
+bool CDVDOverlayCodecCCText::Open(CDVDStreamInfo& hints, CDVDCodecOptions& options)
+{
+ Dispose();
+
+ return Initialize();
+}
+
+void CDVDOverlayCodecCCText::Dispose()
+{
+ if (m_pOverlay)
+ {
+ m_pOverlay->Release();
+ m_pOverlay = nullptr;
+ }
+}
+
+OverlayMessage CDVDOverlayCodecCCText::Decode(DemuxPacket* pPacket)
+{
+ if (!pPacket)
+ return OverlayMessage::OC_ERROR;
+
+ uint8_t* data = pPacket->pData;
+ std::string text((char*)data, (char*)(data + pPacket->iSize));
+
+ // We delete some old Events to avoid allocating too much,
+ // this can easily happen with karaoke text or live videos
+ int lastId = DeleteSubtitles(50, 200);
+ if (m_prevSubId != NO_SUBTITLE_ID)
+ {
+ if (lastId == NO_SUBTITLE_ID)
+ {
+ m_prevPTSStart = 0.0;
+ m_prevText.clear();
+ m_changePrevStopTime = false;
+ }
+ m_prevSubId = lastId;
+ }
+
+ double PTSStartTime = 0;
+ double PTSStopTime = 0;
+
+ CDVDOverlayCodec::GetAbsoluteTimes(PTSStartTime, PTSStopTime, pPacket);
+ CDVDSubtitleTagSami TagConv;
+
+ PTSStopTime = PTSStartTime + DEFAULT_DURATION;
+
+ if (TagConv.Init())
+ {
+ TagConv.ConvertLine(text);
+ TagConv.CloseTag(text);
+
+ if (!text.empty())
+ {
+ if (m_prevText == text)
+ {
+ // Extend the duration of previously added event
+ ChangeSubtitleStopTime(m_prevSubId, PTSStartTime + DEFAULT_DURATION);
+ }
+ else
+ {
+ // Set the stop time of previously added event based on current start PTS
+ if (m_changePrevStopTime)
+ ChangeSubtitleStopTime(m_prevSubId, PTSStartTime);
+
+ m_prevSubId = AddSubtitle(text, PTSStartTime, PTSStopTime);
+ m_changePrevStopTime = true;
+ }
+ }
+ else if (m_prevText != text)
+ {
+ // Set the stop time of previously added event based on current start PTS
+ ChangeSubtitleStopTime(m_prevSubId, PTSStartTime);
+ m_changePrevStopTime = false;
+ }
+
+ m_prevText = text;
+ m_prevPTSStart = PTSStartTime;
+ }
+ else
+ CLog::Log(LOGERROR, "{} - Failed to initialize tag converter", __FUNCTION__);
+
+ return m_pOverlay ? OverlayMessage::OC_DONE : OverlayMessage::OC_OVERLAY;
+}
+
+void CDVDOverlayCodecCCText::PostProcess(std::string& text)
+{
+ // The data that come from InputStream could contains \r chars
+ // we have to remove them all because it causes to display empty box "tofu"
+ //! @todo This must be removed after the rework of the CC decoders
+ StringUtils::Replace(text, "\r", "");
+ CSubtitlesAdapter::PostProcess(text);
+}
+
+void CDVDOverlayCodecCCText::Reset()
+{
+ Dispose();
+ Flush();
+}
+
+void CDVDOverlayCodecCCText::Flush()
+{
+ if (m_pOverlay)
+ {
+ m_pOverlay->Release();
+ m_pOverlay = nullptr;
+ }
+
+ m_prevSubId = NO_SUBTITLE_ID;
+ m_prevPTSStart = 0.0;
+ m_prevText.clear();
+ m_changePrevStopTime = false;
+
+ FlushSubtitles();
+}
+
+CDVDOverlay* CDVDOverlayCodecCCText::GetOverlay()
+{
+ if (m_pOverlay)
+ return nullptr;
+ m_pOverlay = CreateOverlay();
+ m_pOverlay->SetTextAlignEnabled(true);
+ return m_pOverlay->Acquire();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecCCText.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecCCText.h
new file mode 100644
index 0000000..808b45e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecCCText.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2005-2021 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 "DVDOverlayCodec.h"
+#include "DVDStreamInfo.h"
+#include "DVDSubtitles/SubtitlesAdapter.h"
+
+class CDVDOverlay;
+
+class CDVDOverlayCodecCCText : public CDVDOverlayCodec, private CSubtitlesAdapter
+{
+public:
+ CDVDOverlayCodecCCText();
+ ~CDVDOverlayCodecCCText() override;
+ bool Open(CDVDStreamInfo& hints, CDVDCodecOptions& options) override;
+ OverlayMessage Decode(DemuxPacket* pPacket) override;
+ void Reset() override;
+ void Flush() override;
+ CDVDOverlay* GetOverlay() override;
+
+ // Specialization of CSubtitlesAdapter
+ void PostProcess(std::string& text) override;
+
+private:
+ void Dispose() override;
+ CDVDOverlay* m_pOverlay;
+ int m_prevSubId;
+ double m_prevPTSStart;
+ std::string m_prevText;
+ bool m_changePrevStopTime;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecFFmpeg.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecFFmpeg.cpp
new file mode 100644
index 0000000..8c26cad
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecFFmpeg.cpp
@@ -0,0 +1,300 @@
+/*
+ * 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 "DVDOverlayCodecFFmpeg.h"
+
+#include "DVDOverlayImage.h"
+#include "DVDStreamInfo.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/EndianSwap.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+CDVDOverlayCodecFFmpeg::CDVDOverlayCodecFFmpeg() : CDVDOverlayCodec("FFmpeg Subtitle Decoder")
+{
+ m_pCodecContext = NULL;
+ m_SubtitleIndex = -1;
+ m_width = 0;
+ m_height = 0;
+ m_StartTime = 0.0;
+ m_StopTime = 0.0;
+ memset(&m_Subtitle, 0, sizeof(m_Subtitle));
+}
+
+CDVDOverlayCodecFFmpeg::~CDVDOverlayCodecFFmpeg()
+{
+ Dispose();
+}
+
+bool CDVDOverlayCodecFFmpeg::Open(CDVDStreamInfo &hints, CDVDCodecOptions &options)
+{
+
+ // decoding of this kind of subs does not work reliable
+ if (hints.codec == AV_CODEC_ID_EIA_608)
+ return false;
+
+ AVCodec* pCodec = avcodec_find_decoder(hints.codec);
+ if (!pCodec)
+ {
+ CLog::Log(LOGDEBUG, "{} - Unable to find codec {}", __FUNCTION__, hints.codec);
+ return false;
+ }
+
+ m_pCodecContext = avcodec_alloc_context3(pCodec);
+ if (!m_pCodecContext)
+ return false;
+
+ m_pCodecContext->debug = 0;
+ m_pCodecContext->workaround_bugs = FF_BUG_AUTODETECT;
+ m_pCodecContext->codec_tag = hints.codec_tag;
+ m_pCodecContext->time_base.num = 1;
+ m_pCodecContext->time_base.den = DVD_TIME_BASE;
+ m_pCodecContext->pkt_timebase.num = 1;
+ m_pCodecContext->pkt_timebase.den = DVD_TIME_BASE;
+
+ if( hints.extradata && hints.extrasize > 0 )
+ {
+ m_pCodecContext->extradata_size = hints.extrasize;
+ m_pCodecContext->extradata = (uint8_t*)av_mallocz(hints.extrasize + AV_INPUT_BUFFER_PADDING_SIZE);
+ memcpy(m_pCodecContext->extradata, hints.extradata, hints.extrasize);
+
+ // start parsing of extra data - create a copy to be safe and make it zero-terminating to avoid access violations!
+ unsigned int parse_extrasize = hints.extrasize;
+ char* parse_extra = new char[parse_extrasize + 1];
+ memcpy(parse_extra, hints.extradata, parse_extrasize);
+ parse_extra[parse_extrasize] = '\0';
+
+ // assume that the extra data is formatted as a concatenation of lines ('\n' terminated)
+ char *ptr = parse_extra;
+ do // read line by line
+ {
+ if (!strncmp(ptr, "size:", 5))
+ {
+ int width = 0, height = 0;
+ sscanf(ptr, "size: %dx%d", &width, &height);
+ if (width > 0 && height > 0)
+ {
+ m_pCodecContext->width = width;
+ m_pCodecContext->height = height;
+ CLog::Log(LOGDEBUG, "{} - parsed extradata: size: {} x {}", __FUNCTION__, width, height);
+ }
+ }
+ /*
+ // leaving commented code: these items don't work yet... but they may be meaningful
+ if (!strncmp(ptr, "palette:", 8))
+ if (sscanf(ptr, "palette: %x, %x, %x, %x, %x, %x, %x, %x,"
+ " %x, %x, %x, %x, %x, %x, %x, %x", ...
+ if (!StringUtils::CompareNoCase(ptr, "forced subs: on", 15))
+ forced_subs_only = 1;
+ */
+ // if tried all possibilities, then read newline char and move to next line
+ ptr = strchr(ptr, '\n');
+ if (ptr != NULL) ptr++;
+ }
+ while (ptr != NULL && ptr <= parse_extra + parse_extrasize);
+
+ delete[] parse_extra;
+ }
+
+ if (avcodec_open2(m_pCodecContext, pCodec, NULL) < 0)
+ {
+ CLog::Log(LOGDEBUG,"CDVDVideoCodecFFmpeg::Open() Unable to open codec");
+ avcodec_free_context(&m_pCodecContext);
+ return false;
+ }
+
+ return true;
+}
+
+void CDVDOverlayCodecFFmpeg::Dispose()
+{
+ avsubtitle_free(&m_Subtitle);
+ avcodec_free_context(&m_pCodecContext);
+}
+
+OverlayMessage CDVDOverlayCodecFFmpeg::Decode(DemuxPacket* pPacket)
+{
+ if (!m_pCodecContext || !pPacket)
+ return OverlayMessage::OC_ERROR;
+
+ int gotsub = 0, len = 0;
+
+ avsubtitle_free(&m_Subtitle);
+
+ AVPacket* avpkt = av_packet_alloc();
+ if (!avpkt)
+ {
+ CLog::Log(LOGERROR, "CDVDOverlayCodecFFmpeg::{} - av_packet_alloc failed: {}", __FUNCTION__,
+ strerror(errno));
+ return OverlayMessage::OC_ERROR;
+ }
+
+ avpkt->data = pPacket->pData;
+ avpkt->size = pPacket->iSize;
+ avpkt->pts = pPacket->pts == DVD_NOPTS_VALUE ? AV_NOPTS_VALUE : (int64_t)pPacket->pts;
+ avpkt->dts = pPacket->dts == DVD_NOPTS_VALUE ? AV_NOPTS_VALUE : (int64_t)pPacket->dts;
+
+ len = avcodec_decode_subtitle2(m_pCodecContext, &m_Subtitle, &gotsub, avpkt);
+
+ int size = avpkt->size;
+
+ av_packet_free(&avpkt);
+
+ if (len < 0)
+ {
+ CLog::Log(LOGERROR, "{} - avcodec_decode_subtitle returned failure", __FUNCTION__);
+ Flush();
+ return OverlayMessage::OC_ERROR;
+ }
+
+ if (len != size)
+ CLog::Log(LOGWARNING, "{} - avcodec_decode_subtitle didn't consume the full packet",
+ __FUNCTION__);
+
+ if (!gotsub)
+ return OverlayMessage::OC_BUFFER;
+
+ double pts_offset = 0.0;
+
+ if (m_pCodecContext->codec_id == AV_CODEC_ID_HDMV_PGS_SUBTITLE && m_Subtitle.format == 0)
+ {
+ // for pgs subtitles the packet pts of the end_segments are wrong
+ // instead use the subtitle pts to calc the offset here
+ // see http://git.videolan.org/?p=ffmpeg.git;a=commit;h=2939e258f9d1fff89b3b68536beb931b54611585
+
+ if (m_Subtitle.pts != AV_NOPTS_VALUE && pPacket->pts != DVD_NOPTS_VALUE)
+ {
+ pts_offset = m_Subtitle.pts - pPacket->pts ;
+ }
+ }
+
+ m_StartTime = DVD_MSEC_TO_TIME(m_Subtitle.start_display_time);
+ m_StopTime = DVD_MSEC_TO_TIME(m_Subtitle.end_display_time);
+
+ //adapt start and stop time to our packet pts
+ CDVDOverlayCodec::GetAbsoluteTimes(m_StartTime, m_StopTime, pPacket);
+
+ m_StartTime += pts_offset;
+ if (m_StopTime > 0)
+ m_StopTime += pts_offset;
+
+ m_SubtitleIndex = 0;
+
+ return OverlayMessage::OC_OVERLAY;
+}
+
+void CDVDOverlayCodecFFmpeg::Reset()
+{
+ Flush();
+}
+
+void CDVDOverlayCodecFFmpeg::Flush()
+{
+ avsubtitle_free(&m_Subtitle);
+ m_SubtitleIndex = -1;
+
+ avcodec_flush_buffers(m_pCodecContext);
+}
+
+CDVDOverlay* CDVDOverlayCodecFFmpeg::GetOverlay()
+{
+ if(m_SubtitleIndex<0)
+ return nullptr;
+
+ if(m_Subtitle.num_rects == 0 && m_SubtitleIndex == 0)
+ {
+ // we must add an empty overlay to replace the previous one
+ CDVDOverlay* o = new CDVDOverlay(DVDOVERLAY_TYPE_NONE);
+ o->iPTSStartTime = m_StartTime;
+ o->iPTSStopTime = 0;
+ o->replace = true;
+ m_SubtitleIndex++;
+ return o;
+ }
+
+ if(m_Subtitle.format == 0)
+ {
+ if(m_SubtitleIndex >= (int)m_Subtitle.num_rects)
+ return nullptr;
+
+ if(m_Subtitle.rects[m_SubtitleIndex] == NULL)
+ return nullptr;
+
+ AVSubtitleRect rect = *m_Subtitle.rects[m_SubtitleIndex];
+ if (rect.data[0] == NULL)
+ return nullptr;
+
+ m_height = m_pCodecContext->height;
+ m_width = m_pCodecContext->width;
+
+ if (m_pCodecContext->codec_id == AV_CODEC_ID_DVB_SUBTITLE)
+ {
+ // ETSI EN 300 743 V1.3.1
+ // 5.3.1
+ // Absence of a DDS in a stream implies that the stream is coded in accordance with EN 300 743 (V1.2.1) [5] and that a
+ // display width of 720 pixels and a display height of 576 lines may be assumed.
+ if (!m_height && !m_width)
+ {
+ m_width = 720;
+ m_height = 576;
+ }
+ }
+
+ RENDER_STEREO_MODE render_stereo_mode = CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode();
+ if (render_stereo_mode != RENDER_STEREO_MODE_OFF &&
+ render_stereo_mode != RENDER_STEREO_MODE_HARDWAREBASED)
+ {
+ if (rect.h > m_height / 2)
+ {
+ m_height /= 2;
+ rect.h /= 2;
+ }
+ else if (rect.w > m_width / 2)
+ {
+ m_width /= 2;
+ rect.w /= 2;
+ }
+ }
+
+ CDVDOverlayImage* overlay = new CDVDOverlayImage();
+
+ overlay->iPTSStartTime = m_StartTime;
+ overlay->iPTSStopTime = m_StopTime;
+ overlay->replace = true;
+ overlay->linesize = rect.w;
+ overlay->pixels.resize(rect.w * rect.h);
+ overlay->palette.resize(rect.nb_colors);
+ overlay->x = rect.x;
+ overlay->y = rect.y;
+ overlay->width = rect.w;
+ overlay->height = rect.h;
+ overlay->bForced = rect.flags != 0;
+ overlay->source_width = m_width;
+ overlay->source_height = m_height;
+
+ uint8_t* s = rect.data[0];
+ uint8_t* t = overlay->pixels.data();
+
+ for (int i = 0; i < rect.h; i++)
+ {
+ memcpy(t, s, rect.w);
+ s += rect.linesize[0];
+ t += overlay->linesize;
+ }
+
+ for (int i = 0; i < rect.nb_colors; i++)
+ overlay->palette[i] = Endian_SwapLE32(((uint32_t *)rect.data[1])[i]);
+
+ m_SubtitleIndex++;
+
+ return overlay;
+ }
+
+ return nullptr;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecFFmpeg.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecFFmpeg.h
new file mode 100644
index 0000000..a95ce15
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecFFmpeg.h
@@ -0,0 +1,42 @@
+/*
+ * 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 "DVDOverlayCodec.h"
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavutil/avutil.h>
+}
+
+class CDVDOverlaySpu;
+class CDVDOverlayText;
+
+class CDVDOverlayCodecFFmpeg : public CDVDOverlayCodec
+{
+public:
+ CDVDOverlayCodecFFmpeg();
+ ~CDVDOverlayCodecFFmpeg() override;
+ bool Open(CDVDStreamInfo &hints, CDVDCodecOptions &options) override;
+ OverlayMessage Decode(DemuxPacket* pPacket) override;
+ void Reset() override;
+ void Flush() override;
+ CDVDOverlay* GetOverlay() override;
+
+private:
+ void Dispose() override;
+ AVCodecContext* m_pCodecContext;
+ AVSubtitle m_Subtitle;
+ int m_SubtitleIndex;
+ double m_StartTime;
+ double m_StopTime;
+
+ int m_width;
+ int m_height;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecSSA.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecSSA.cpp
new file mode 100644
index 0000000..7b6d0be
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecSSA.cpp
@@ -0,0 +1,141 @@
+/*
+ * 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 "DVDOverlayCodecSSA.h"
+
+#include "DVDCodecs/DVDCodecs.h"
+#include "DVDOverlaySSA.h"
+#include "DVDStreamInfo.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SubtitlesSettings.h"
+#include "utils/StringUtils.h"
+
+#include <memory>
+
+using namespace KODI;
+
+CDVDOverlayCodecSSA::CDVDOverlayCodecSSA()
+ : CDVDOverlayCodec("SSA Subtitle Decoder"), m_libass(std::make_shared<CDVDSubtitlesLibass>())
+{
+ m_pOverlay = nullptr;
+ m_order = 0;
+ m_libass->Configure();
+}
+
+CDVDOverlayCodecSSA::~CDVDOverlayCodecSSA()
+{
+ Dispose();
+}
+
+bool CDVDOverlayCodecSSA::Open(CDVDStreamInfo& hints, CDVDCodecOptions& options)
+{
+ if (hints.codec != AV_CODEC_ID_SSA && hints.codec != AV_CODEC_ID_ASS)
+ return false;
+
+ Dispose();
+
+ return m_libass->DecodeHeader(static_cast<char*>(hints.extradata), hints.extrasize);
+}
+
+void CDVDOverlayCodecSSA::Dispose()
+{
+ if (m_pOverlay)
+ {
+ m_pOverlay->Release();
+ m_pOverlay = nullptr;
+ }
+}
+
+OverlayMessage CDVDOverlayCodecSSA::Decode(DemuxPacket* pPacket)
+{
+ if (!pPacket)
+ return OverlayMessage::OC_ERROR;
+
+ double pts = pPacket->dts != DVD_NOPTS_VALUE ? pPacket->dts : pPacket->pts;
+ // libass only has a precision of msec
+ pts = round(pts / 1000) * 1000;
+
+ uint8_t* data = pPacket->pData;
+ int size = pPacket->iSize;
+ double duration = pPacket->duration;
+ if (duration == DVD_NOPTS_VALUE)
+ duration = 0.0;
+
+ if (strncmp((const char*)data, "Dialogue:", 9) == 0)
+ {
+ int sh, sm, ss, sc, eh, em, es, ec;
+ double beg, end;
+ size_t pos;
+ std::string line, line2;
+ std::vector<std::string> lines;
+ StringUtils::Tokenize((const char*)data, lines, "\r\n");
+ for (size_t i = 0; i < lines.size(); i++)
+ {
+ line = lines[i];
+ StringUtils::Trim(line);
+ std::unique_ptr<char[]> layer(new char[line.length() + 1]);
+
+ if (sscanf(line.c_str(), "%*[^:]:%[^,],%d:%d:%d%*c%d,%d:%d:%d%*c%d", layer.get(), &sh, &sm,
+ &ss, &sc, &eh, &em, &es, &ec) != 9)
+ continue;
+
+ end = 10000 * ((eh * 360000.0) + (em * 6000.0) + (es * 100.0) + ec);
+ beg = 10000 * ((sh * 360000.0) + (sm * 6000.0) + (ss * 100.0) + sc);
+
+ pos = line.find_first_of(',', 0);
+ pos = line.find_first_of(',', pos + 1);
+ pos = line.find_first_of(',', pos + 1);
+ if (pos == std::string::npos)
+ continue;
+
+ line2 = StringUtils::Format("{},{},{}", m_order++, layer.get(), line.substr(pos + 1));
+
+ m_libass->DecodeDemuxPkt(line2.c_str(), static_cast<int>(line2.length()), beg, end - beg);
+ }
+ }
+ else
+ {
+ m_libass->DecodeDemuxPkt((char*)data, size, pts, duration);
+ }
+
+ return m_pOverlay ? OverlayMessage::OC_DONE : OverlayMessage::OC_OVERLAY;
+}
+
+void CDVDOverlayCodecSSA::Reset()
+{
+ Dispose();
+ Flush();
+}
+
+void CDVDOverlayCodecSSA::Flush()
+{
+ if (m_pOverlay)
+ {
+ m_pOverlay->Release();
+ m_pOverlay = nullptr;
+ }
+
+ m_order = 0;
+}
+
+CDVDOverlay* CDVDOverlayCodecSSA::GetOverlay()
+{
+ if (m_pOverlay)
+ return nullptr;
+ m_pOverlay = new CDVDOverlaySSA(m_libass);
+ m_pOverlay->iPTSStartTime = 0;
+ m_pOverlay->iPTSStopTime = DVD_NOPTS_VALUE;
+ auto overrideStyles{
+ CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()->GetOverrideStyles()};
+ m_pOverlay->SetForcedMargins(overrideStyles != SUBTITLES::OverrideStyles::STYLES_POSITIONS &&
+ overrideStyles != SUBTITLES::OverrideStyles::POSITIONS);
+ return m_pOverlay->Acquire();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecSSA.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecSSA.h
new file mode 100644
index 0000000..a31882f
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecSSA.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "DVDOverlayCodec.h"
+#include "DVDStreamInfo.h"
+#include "DVDSubtitles/DVDSubtitlesLibass.h"
+
+class CDVDOverlaySSA;
+
+class CDVDOverlayCodecSSA : public CDVDOverlayCodec
+{
+public:
+ CDVDOverlayCodecSSA();
+ ~CDVDOverlayCodecSSA() override;
+ bool Open(CDVDStreamInfo& hints, CDVDCodecOptions& options) override;
+ OverlayMessage Decode(DemuxPacket* pPacket) override;
+ void Reset() override;
+ void Flush() override;
+ CDVDOverlay* GetOverlay() override;
+
+private:
+ void Dispose() override;
+ std::shared_ptr<CDVDSubtitlesLibass> m_libass;
+ CDVDOverlaySSA* m_pOverlay;
+ int m_order;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecTX3G.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecTX3G.cpp
new file mode 100644
index 0000000..bdc92e5
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecTX3G.cpp
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2011-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 "DVDOverlayCodecTX3G.h"
+
+#include "DVDCodecs/DVDCodecs.h"
+#include "DVDOverlayText.h"
+#include "DVDStreamInfo.h"
+#include "DVDSubtitles/SubtitlesStyle.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "utils/CharArrayParser.h"
+#include "utils/ColorUtils.h"
+#include "utils/StreamUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <vector>
+
+// 3GPP/TX3G (aka MPEG-4 Timed Text) Subtitle support
+// 3GPP -> 3rd Generation Partnership Program
+// adapted from https://github.com/HandBrake/HandBrake/blob/master/libhb/dectx3gsub.c;
+
+namespace
+{
+enum FaceStyleFlag
+{
+ BOLD = 0x1,
+ ITALIC = 0x2,
+ UNDERLINE = 0x4
+};
+
+struct StyleRecord
+{
+ uint16_t startChar; // index in terms of character (not byte) position
+ uint16_t endChar; // index in terms of character (not byte) position
+ uint16_t fontID;
+ uint8_t faceStyleFlags; // FaceStyleFlag
+ uint8_t fontSize;
+ UTILS::COLOR::Color textColorARGB;
+ unsigned int textColorAlphaCh;
+};
+
+constexpr uint32_t BOX_TYPE_UUID = StreamUtils::MakeFourCC('u', 'u', 'i', 'd');
+constexpr uint32_t BOX_TYPE_STYL = StreamUtils::MakeFourCC('s', 't', 'y', 'l'); // TextStyleBox
+
+void ConvertStyleToTags(std::string& strUTF8, const StyleRecord& style, bool closingTags)
+{
+ if (style.faceStyleFlags & BOLD)
+ strUTF8.append(closingTags ? "{\\b0}" : "{\\b1}");
+ if (style.faceStyleFlags & ITALIC)
+ strUTF8.append(closingTags ? "{\\i0}" : "{\\i1}");
+ if (style.faceStyleFlags & UNDERLINE)
+ strUTF8.append(closingTags ? "{\\u0}" : "{\\u1}");
+ if (style.textColorARGB != UTILS::COLOR::WHITE)
+ {
+ if (closingTags)
+ strUTF8 += "{\\c}";
+ else
+ {
+ UTILS::COLOR::Color color = UTILS::COLOR::ConvertToBGR(style.textColorARGB);
+ strUTF8 += StringUtils::Format("{{\\c&H{:06x}&}}", color);
+ }
+ }
+ if (style.textColorAlphaCh != 255)
+ {
+ // Libass use inverted alpha channel 0==opaque
+ unsigned int alpha = 0;
+ if (!closingTags)
+ alpha = 255 - style.textColorAlphaCh;
+ strUTF8 += StringUtils::Format("{{\\1a&H{:02x}&}}", alpha);
+ }
+}
+} // unnamed namespace
+
+CDVDOverlayCodecTX3G::CDVDOverlayCodecTX3G() : CDVDOverlayCodec("TX3G Subtitle Decoder")
+{
+}
+
+CDVDOverlayCodecTX3G::~CDVDOverlayCodecTX3G()
+{
+ Dispose();
+}
+
+bool CDVDOverlayCodecTX3G::Open(CDVDStreamInfo& hints, CDVDCodecOptions& options)
+{
+ if (hints.codec != AV_CODEC_ID_MOV_TEXT)
+ return false;
+
+ Dispose();
+
+ return Initialize();
+}
+
+void CDVDOverlayCodecTX3G::Dispose()
+{
+ if (m_pOverlay)
+ {
+ m_pOverlay->Release();
+ m_pOverlay = nullptr;
+ }
+}
+
+OverlayMessage CDVDOverlayCodecTX3G::Decode(DemuxPacket* pPacket)
+{
+ double PTSStartTime = 0;
+ double PTSStopTime = 0;
+
+ CDVDOverlayCodec::GetAbsoluteTimes(PTSStartTime, PTSStopTime, pPacket);
+
+ char* data = reinterpret_cast<char*>(pPacket->pData);
+
+ // Parse the packet as a TX3G TextSample.
+ CCharArrayParser sampleData;
+ sampleData.Reset(data, pPacket->iSize);
+
+ uint16_t textLength = 0;
+ char* text = nullptr;
+ if (sampleData.CharsLeft() >= 2)
+ textLength = sampleData.ReadNextUnsignedShort();
+ if (sampleData.CharsLeft() >= textLength)
+ {
+ text = data + sampleData.GetPosition();
+ sampleData.SkipChars(textLength);
+ }
+ if (!text)
+ return OverlayMessage::OC_ERROR;
+
+ std::vector<StyleRecord> styleRecords;
+
+ // Read all TextSampleModifierBox types
+ while (sampleData.CharsLeft() > 0)
+ {
+ if (sampleData.CharsLeft() < MP4_BOX_HEADER_SIZE)
+ {
+ CLog::Log(LOGWARNING, "{} - Incomplete box header found", __FUNCTION__);
+ break;
+ }
+
+ uint32_t boxSize = sampleData.ReadNextUnsignedInt();
+ uint32_t boxType = sampleData.ReadNextUnsignedInt();
+
+ if (boxType == BOX_TYPE_UUID)
+ {
+ CLog::Log(LOGDEBUG, "{} - Sample data has unsupported extended type 'uuid'", __FUNCTION__);
+ }
+ else if (boxType == BOX_TYPE_STYL)
+ {
+ // Parse the contained StyleRecords
+ if (styleRecords.size() != 0)
+ {
+ CLog::Log(LOGDEBUG, "{} - Found additional TextStyleBox, skipping", __FUNCTION__);
+ sampleData.SkipChars(boxSize - MP4_BOX_HEADER_SIZE);
+ continue;
+ }
+
+ if (sampleData.CharsLeft() < 2)
+ {
+ CLog::Log(LOGWARNING, "{} - Incomplete TextStyleBox header found", __FUNCTION__);
+ return OverlayMessage::OC_ERROR;
+ }
+ uint16_t styleCount = sampleData.ReadNextUnsignedShort();
+
+ // Get the data of each style record
+ // Each style is ordered by starting character offset, and the starting
+ // offset of one style record shall be greater than or equal to the
+ // ending character offset of the preceding record,
+ // styles records shall not overlap their character ranges.
+ for (int i = 0; i < styleCount; i++)
+ {
+ if (sampleData.CharsLeft() < 12)
+ {
+ CLog::Log(LOGWARNING, "{} - Incomplete StyleRecord found, skipping", __FUNCTION__);
+ sampleData.SkipChars(sampleData.CharsLeft());
+ continue;
+ }
+
+ StyleRecord styleRec;
+ styleRec.startChar = sampleData.ReadNextUnsignedShort();
+ styleRec.endChar = sampleData.ReadNextUnsignedShort();
+ styleRec.fontID = sampleData.ReadNextUnsignedShort();
+ styleRec.faceStyleFlags = sampleData.ReadNextUnsignedChar();
+ styleRec.fontSize = sampleData.ReadNextUnsignedChar();
+ styleRec.textColorARGB = UTILS::COLOR::ConvertToARGB(sampleData.ReadNextUnsignedInt());
+ styleRec.textColorAlphaCh = (styleRec.textColorARGB & 0xFF000000) >> 24;
+ // clamp bgnChar/bgnChar to textLength, we alloc enough space above and
+ // this fixes broken encoders that do not handle endChar correctly.
+ if (styleRec.startChar > textLength)
+ styleRec.startChar = textLength;
+ if (styleRec.endChar > textLength)
+ styleRec.endChar = textLength;
+
+ // Skip zero-length style
+ if (styleRec.startChar == styleRec.endChar)
+ continue;
+
+ styleRecords.emplace_back(styleRec);
+ }
+ }
+ else
+ {
+ // Other types of TextSampleModifierBox are not supported
+ sampleData.SkipChars(boxSize - MP4_BOX_HEADER_SIZE);
+ }
+ }
+
+ uint16_t charIndex = 0;
+ size_t styleIndex = 0;
+ std::string strUTF8;
+ bool skipChars = false;
+ // Parse the text to add the converted styles records,
+ // index over textLength chars to include broken encoders,
+ // so we pickup closing styles on broken encoders
+ for (char* curPos = text; curPos <= text + textLength; curPos++)
+ {
+ if ((*curPos & 0xC0) == 0x80)
+ {
+ // Is a non-first byte of a multi-byte UTF-8 character
+ strUTF8.append(static_cast<const char*>(curPos), 1);
+ continue; // ...without incrementing 'charIndex'
+ }
+
+ // Go through styles, a style can end where another one begins
+ while (styleIndex < styleRecords.size())
+ {
+ if (styleRecords[styleIndex].startChar == charIndex)
+ {
+ ConvertStyleToTags(strUTF8, styleRecords[styleIndex], false);
+ break;
+ }
+ else if (styleRecords[styleIndex].endChar == charIndex)
+ {
+ ConvertStyleToTags(strUTF8, styleRecords[styleIndex], true);
+ styleIndex++;
+ }
+ else
+ break;
+ }
+
+ if (*curPos == '{') // erase unsupported tags
+ skipChars = true;
+
+ // Skip all \r because it causes the line to display empty box "tofu"
+ if (!skipChars && *curPos != '\0' && *curPos != '\r')
+ strUTF8.append(static_cast<const char*>(curPos), 1);
+
+ if (*curPos == '}')
+ skipChars = false;
+
+ charIndex++;
+ }
+
+ if (strUTF8.empty())
+ return OverlayMessage::OC_BUFFER;
+
+ AddSubtitle(strUTF8, PTSStartTime, PTSStopTime);
+
+ return m_pOverlay ? OverlayMessage::OC_DONE : OverlayMessage::OC_OVERLAY;
+}
+
+void CDVDOverlayCodecTX3G::PostProcess(std::string& text)
+{
+ if (text[text.size() - 1] == '\n')
+ text.erase(text.size() - 1);
+ CSubtitlesAdapter::PostProcess(text);
+}
+
+void CDVDOverlayCodecTX3G::Reset()
+{
+ Dispose();
+ Flush();
+}
+
+void CDVDOverlayCodecTX3G::Flush()
+{
+ if (m_pOverlay)
+ {
+ m_pOverlay->Release();
+ m_pOverlay = nullptr;
+ }
+
+ FlushSubtitles();
+}
+
+CDVDOverlay* CDVDOverlayCodecTX3G::GetOverlay()
+{
+ if (m_pOverlay)
+ return nullptr;
+ m_pOverlay = CreateOverlay();
+ return m_pOverlay->Acquire();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecTX3G.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecTX3G.h
new file mode 100644
index 0000000..0c4983d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecTX3G.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "DVDOverlayCodec.h"
+#include "DVDSubtitles/SubtitlesAdapter.h"
+
+class CDVDOverlay;
+
+class CDVDOverlayCodecTX3G : public CDVDOverlayCodec, private CSubtitlesAdapter
+{
+public:
+ CDVDOverlayCodecTX3G();
+ ~CDVDOverlayCodecTX3G() override;
+ bool Open(CDVDStreamInfo& hints, CDVDCodecOptions& options) override;
+ OverlayMessage Decode(DemuxPacket* pPacket) override;
+ void Reset() override;
+ void Flush() override;
+ CDVDOverlay* GetOverlay() override;
+
+ // Specialization of CSubtitlesAdapter
+ void PostProcess(std::string& text) override;
+
+private:
+ void Dispose() override;
+ CDVDOverlay* m_pOverlay{nullptr};
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecText.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecText.cpp
new file mode 100644
index 0000000..0c4d817
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecText.cpp
@@ -0,0 +1,150 @@
+/*
+ * 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 "DVDOverlayCodecText.h"
+
+#include "DVDCodecs/DVDCodecs.h"
+#include "DVDOverlayText.h"
+#include "DVDStreamInfo.h"
+#include "cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+
+namespace
+{
+// Subtitle packets reaching the decoder may not have the stop PTS value,
+// the stop value can be taken from the start PTS of the next subtitle.
+// Otherwise we fallback to a default of 20 secs duration. This is only
+// applied if the subtitle packets have 0 duration (stop > start).
+constexpr double DEFAULT_DURATION = 20.0 * static_cast<double>(DVD_TIME_BASE);
+} // namespace
+
+CDVDOverlayCodecText::CDVDOverlayCodecText() : CDVDOverlayCodec("Text Subtitle Decoder")
+{
+ m_pOverlay = nullptr;
+}
+
+CDVDOverlayCodecText::~CDVDOverlayCodecText()
+{
+ Dispose();
+}
+
+bool CDVDOverlayCodecText::Open(CDVDStreamInfo& hints, CDVDCodecOptions& options)
+{
+ if (hints.codec != AV_CODEC_ID_TEXT && hints.codec != AV_CODEC_ID_SSA &&
+ hints.codec != AV_CODEC_ID_SUBRIP)
+ return false;
+
+ m_codecId = hints.codec;
+
+ Dispose();
+
+ return Initialize();
+}
+
+void CDVDOverlayCodecText::Dispose()
+{
+ if (m_pOverlay)
+ {
+ m_pOverlay->Release();
+ m_pOverlay = nullptr;
+ }
+}
+
+OverlayMessage CDVDOverlayCodecText::Decode(DemuxPacket* pPacket)
+{
+ if (!pPacket)
+ return OverlayMessage::OC_ERROR;
+
+ uint8_t* data = pPacket->pData;
+ char* start = (char*)data;
+ char* end = (char*)(data + pPacket->iSize);
+
+ if (m_codecId == AV_CODEC_ID_SSA)
+ {
+ // currently just skip the prefixed ssa fields (8 fields)
+ int nFieldCount = 8;
+ while (nFieldCount > 0 && start < end)
+ {
+ if (*start == ',')
+ nFieldCount--;
+
+ start++;
+ }
+ }
+
+ std::string text(start, end);
+ double PTSStartTime = 0;
+ double PTSStopTime = 0;
+
+ CDVDOverlayCodec::GetAbsoluteTimes(PTSStartTime, PTSStopTime, pPacket);
+ CDVDSubtitleTagSami TagConv;
+
+ if (TagConv.Init())
+ {
+ TagConv.ConvertLine(text);
+ TagConv.CloseTag(text);
+
+ // similar to CC text, if the subtitle duration is invalid (stop > start)
+ // we might want to assume the stop time will be set by the next received
+ // packet
+ if (PTSStopTime < PTSStartTime)
+ {
+ if (m_changePrevStopTime)
+ {
+ ChangeSubtitleStopTime(m_prevSubId, PTSStartTime);
+ m_changePrevStopTime = false;
+ }
+
+ PTSStopTime = PTSStartTime + DEFAULT_DURATION;
+ m_changePrevStopTime = true;
+ }
+
+ m_prevSubId = AddSubtitle(text, PTSStartTime, PTSStopTime);
+ }
+ else
+ CLog::Log(LOGERROR, "{} - Failed to initialize tag converter", __FUNCTION__);
+
+ return m_pOverlay ? OverlayMessage::OC_DONE : OverlayMessage::OC_OVERLAY;
+}
+
+void CDVDOverlayCodecText::PostProcess(std::string& text)
+{
+ // The data that come from InputStream could contains \r chars
+ // we have to remove them all because it causes to display empty box "tofu"
+ StringUtils::Replace(text, "\r", "");
+ CSubtitlesAdapter::PostProcess(text);
+}
+
+void CDVDOverlayCodecText::Reset()
+{
+ Dispose();
+ Flush();
+}
+
+void CDVDOverlayCodecText::Flush()
+{
+ if (m_pOverlay)
+ {
+ m_pOverlay->Release();
+ m_pOverlay = nullptr;
+ }
+
+ FlushSubtitles();
+}
+
+CDVDOverlay* CDVDOverlayCodecText::GetOverlay()
+{
+ if (m_pOverlay)
+ return nullptr;
+ m_pOverlay = CreateOverlay();
+ return m_pOverlay->Acquire();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecText.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecText.h
new file mode 100644
index 0000000..5a5c3bf
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayCodecText.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "DVDOverlayCodec.h"
+#include "DVDStreamInfo.h"
+#include "DVDSubtitles/SubtitlesAdapter.h"
+
+extern "C"
+{
+#include <libavcodec/avcodec.h>
+}
+
+class CDVDOverlay;
+
+class CDVDOverlayCodecText : public CDVDOverlayCodec, private CSubtitlesAdapter
+{
+public:
+ CDVDOverlayCodecText();
+ ~CDVDOverlayCodecText() override;
+ bool Open(CDVDStreamInfo& hints, CDVDCodecOptions& options) override;
+ OverlayMessage Decode(DemuxPacket* pPacket) override;
+ void Reset() override;
+ void Flush() override;
+ CDVDOverlay* GetOverlay() override;
+
+ // Specialization of CSubtitlesAdapter
+ void PostProcess(std::string& text) override;
+
+private:
+ void Dispose() override;
+ CDVDOverlay* m_pOverlay;
+ CDVDStreamInfo m_hints;
+ int m_prevSubId{-1};
+ bool m_changePrevStopTime{false};
+ AVCodecID m_codecId{AV_CODEC_ID_NONE};
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayImage.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayImage.h
new file mode 100644
index 0000000..3af66d8
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayImage.h
@@ -0,0 +1,86 @@
+/*
+ * 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 "DVDOverlay.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "PlatformDefs.h"
+
+class CDVDOverlayImage : public CDVDOverlay
+{
+public:
+ CDVDOverlayImage() : CDVDOverlay(DVDOVERLAY_TYPE_IMAGE)
+ {
+ }
+
+ CDVDOverlayImage(const CDVDOverlayImage& src, int sub_x, int sub_y, int sub_w, int sub_h)
+ : CDVDOverlay(src)
+ {
+ int bpp;
+ if (!src.palette.empty())
+ {
+ bpp = 1;
+ palette = src.palette;
+ }
+ else
+ {
+ bpp = 4;
+ palette.clear();
+ }
+
+ linesize = sub_w * bpp;
+ x = sub_x;
+ y = sub_y;
+ width = sub_w;
+ height = sub_h;
+ source_width = src.source_width;
+ source_height = src.source_height;
+
+ pixels.resize(sub_h * linesize);
+
+ uint8_t* s = src.data_at(sub_x, sub_y);
+ uint8_t* t = pixels.data();
+
+ for (int row = 0; row < sub_h; ++row)
+ {
+ memcpy(t, s, linesize);
+ s += src.linesize;
+ t += linesize;
+ }
+
+ m_textureid = 0;
+ }
+
+ ~CDVDOverlayImage() override = default;
+
+ CDVDOverlayImage* Clone() override
+ {
+ return new CDVDOverlayImage(*this);
+ }
+
+ uint8_t* data_at(int sub_x, int sub_y) const
+ {
+ const int bpp = palette.empty() ? 4 : 1;
+ return const_cast<uint8_t*>(pixels.data() + ((sub_y - y) * linesize + (sub_x - x) * bpp));
+ }
+
+ std::vector<uint8_t> pixels;
+ std::vector<uint32_t> palette;
+
+ int linesize{0};
+ int x{0};
+ int y{0};
+ int width{0};
+ int height{0};
+ int source_width{0};
+ int source_height{0};
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayLibass.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayLibass.h
new file mode 100644
index 0000000..db80da2
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayLibass.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2006-2021 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 "DVDOverlay.h"
+#include "cores/VideoPlayer/DVDSubtitles/DVDSubtitlesLibass.h"
+
+#include <memory>
+
+class CDVDOverlayLibass : public CDVDOverlay
+{
+public:
+ explicit CDVDOverlayLibass(const std::shared_ptr<CDVDSubtitlesLibass>& libass,
+ DVDOverlayType type)
+ : CDVDOverlay(type), m_libass(libass)
+ {
+ }
+
+ CDVDOverlayLibass(const CDVDOverlayLibass& src) : CDVDOverlay(src), m_libass(src.m_libass) {}
+
+ /*!
+ \brief Getter for Libass handler
+ \return The Libass handler.
+ */
+ std::shared_ptr<CDVDSubtitlesLibass> GetLibassHandler() const { return m_libass; }
+
+private:
+ std::shared_ptr<CDVDSubtitlesLibass> m_libass;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySSA.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySSA.h
new file mode 100644
index 0000000..c7b899a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySSA.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "DVDOverlayLibass.h"
+#include "cores/VideoPlayer/DVDSubtitles/DVDSubtitlesLibass.h"
+
+#include <memory>
+
+class CDVDOverlaySSA : public CDVDOverlayLibass
+{
+public:
+ explicit CDVDOverlaySSA(const std::shared_ptr<CDVDSubtitlesLibass>& libass)
+ : CDVDOverlayLibass(libass, DVDOVERLAY_TYPE_SSA)
+ {
+ replace = true;
+ }
+
+ ~CDVDOverlaySSA() override = default;
+
+ CDVDOverlaySSA* Clone() override { return new CDVDOverlaySSA(*this); }
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySpu.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySpu.h
new file mode 100644
index 0000000..10a45e8
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySpu.h
@@ -0,0 +1,95 @@
+/*
+ * 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 "DVDOverlay.h"
+
+#include <stdint.h>
+#include <string.h>
+
+class CDVDOverlaySpu : public CDVDOverlay
+{
+public:
+ CDVDOverlaySpu() : CDVDOverlay(DVDOVERLAY_TYPE_SPU)
+ {
+ pTFData = 0;
+ pBFData = 0;
+ x = 0;
+ y = 0;
+ width = 0;
+ height = 0;
+
+ crop_i_x_end = 0;
+ crop_i_y_end = 0;
+ crop_i_x_start = 0;
+ crop_i_y_start = 0;
+
+ bHasColor = false;
+ bHasAlpha = false;
+
+ memset(result, 0, sizeof(result));
+ memset(alpha, 0, sizeof(alpha));
+ memset(color, 0, sizeof(color));
+ memset(highlight_alpha, 0, sizeof(highlight_alpha));
+ memset(highlight_color, 0, sizeof(highlight_color));
+ }
+
+ CDVDOverlaySpu(const CDVDOverlaySpu& src)
+ : CDVDOverlay(src)
+ {
+ pTFData = src.pTFData;
+ pBFData = src.pBFData;
+ x = src.x;
+ y = src.y;
+ width = src.width;
+ height = src.height;
+
+ crop_i_x_end = src.crop_i_x_end;
+ crop_i_y_end = src.crop_i_y_end;
+ crop_i_x_start = src.crop_i_x_start;
+ crop_i_y_start = src.crop_i_y_start;
+
+ bHasColor = src.bHasColor;
+ bHasAlpha = src.bHasAlpha;
+
+ memcpy(result , src.result , sizeof(result));
+ memcpy(alpha , src.alpha , sizeof(alpha));
+ memcpy(color , src.color , sizeof(color));
+ memcpy(highlight_alpha, src.highlight_alpha, sizeof(highlight_alpha));
+ memcpy(highlight_color, src.highlight_color, sizeof(highlight_color));
+ }
+
+ uint8_t result[2*65536 + 20]; // rle data
+ int pTFData; // pointer to top field picture data (needs rle parsing)
+ int pBFData; // pointer to bottom field picture data (needs rle parsing)
+ int x;
+ int y;
+ int width;
+ int height;
+
+ // the four contrasts, [0] = background
+ int alpha[4];
+ bool bHasAlpha;
+
+ // the four yuv colors, containing [][0] = Y, [][1] = Cr, [][2] = Cb
+ // [0][] = background, [1][] = pattern, [2][] = emphasis1, [3][] = emphasis2
+ int color[4][3];
+ bool bHasColor;
+
+ // used for cropping overlays
+ int crop_i_x_end;
+ int crop_i_y_end;
+ int crop_i_x_start;
+ int crop_i_y_start;
+
+ // provided by the navigator engine
+ // should be used on the highlighted areas
+ int highlight_color[4][3];
+ int highlight_alpha[4];
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayText.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayText.h
new file mode 100644
index 0000000..72cf8f1
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayText.h
@@ -0,0 +1,30 @@
+/*
+ * 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 "DVDOverlayLibass.h"
+#include "cores/VideoPlayer/DVDSubtitles/DVDSubtitlesLibass.h"
+
+#include <memory>
+
+class CDVDOverlayText : public CDVDOverlayLibass
+{
+public:
+ explicit CDVDOverlayText(const std::shared_ptr<CDVDSubtitlesLibass>& libass)
+ : CDVDOverlayLibass(libass, DVDOVERLAY_TYPE_TEXT)
+ {
+ replace = true;
+ }
+
+ ~CDVDOverlayText() override = default;
+
+ CDVDOverlayText* Clone() override { return new CDVDOverlayText(*this); }
+
+ void SetTextAlignEnabled(bool enable) override { m_enableTextAlign = enable; }
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/OverlayCodecWebVTT.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/OverlayCodecWebVTT.cpp
new file mode 100644
index 0000000..a2fd14a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/OverlayCodecWebVTT.cpp
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2005-2021 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 "OverlayCodecWebVTT.h"
+
+#include "DVDCodecs/DVDCodecs.h"
+#include "DVDOverlayText.h"
+#include "DVDStreamInfo.h"
+#include "cores/VideoPlayer/DVDSubtitles/SubtitlesStyle.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "utils/CharArrayParser.h"
+
+#include <cstring>
+#include <memory>
+#include <string>
+
+using namespace KODI;
+
+COverlayCodecWebVTT::COverlayCodecWebVTT() : CDVDOverlayCodec("WebVTT Subtitle Decoder")
+{
+ m_pOverlay = nullptr;
+}
+
+COverlayCodecWebVTT::~COverlayCodecWebVTT()
+{
+ Dispose();
+}
+
+bool COverlayCodecWebVTT::Open(CDVDStreamInfo& hints, CDVDCodecOptions& options)
+{
+ Dispose();
+
+ if (!Initialize())
+ return false;
+
+ if (!m_webvttHandler.Initialize())
+ return false;
+
+ // Extradata can be provided by Inputstream addons (e.g. inputstream.adaptive)
+ if (hints.extradata)
+ {
+ std::string extradata{static_cast<char*>(hints.extradata), hints.extrasize};
+ if (extradata == "file")
+ {
+ // WebVTT data like single file are sent one time only,
+ // then we have to prevent the flush performed by video seek
+ m_allowFlush = false;
+ }
+ else if (extradata == "fmp4")
+ {
+ // WebVTT in MP4 encapsulated subtitles (ISO/IEC 14496-30:2014)
+ m_isISOFormat = true;
+ }
+ }
+
+ return true;
+}
+
+void COverlayCodecWebVTT::Dispose()
+{
+ if (m_pOverlay)
+ {
+ m_pOverlay->Release();
+ m_pOverlay = nullptr;
+ }
+}
+
+OverlayMessage COverlayCodecWebVTT::Decode(DemuxPacket* pPacket)
+{
+ if (!pPacket)
+ return OverlayMessage::OC_ERROR;
+
+ const char* data = reinterpret_cast<const char*>(pPacket->pData);
+ std::vector<subtitleData> subtitleList;
+
+ m_webvttHandler.Reset();
+
+ // WebVTT subtitles has no relation with packet PTS then if
+ // a period/chapter change happens (e.g. HLS streaming) VP can detect a discontinuity
+ // and adjust the packet PTS by substracting the pts offset correction value,
+ // so here we have to adjust WebVTT subtitles PTS by substracting it at same way
+ m_webvttHandler.SetPeriodStart(pPacket->m_ptsOffsetCorrection * -1);
+
+ if (m_isISOFormat)
+ {
+ double prevSubStopTime = 0.0;
+
+ m_webvttHandler.DecodeStream(data, pPacket->iSize, pPacket->dts, &subtitleList,
+ prevSubStopTime);
+
+ // Set stop time to all previously added subtitles
+ if (prevSubStopTime > 0)
+ {
+ for (auto& subId : m_previousSubIds)
+ {
+ ChangeSubtitleStopTime(subId, prevSubStopTime);
+ }
+ m_previousSubIds.clear();
+ }
+ }
+ else
+ {
+ if (!m_webvttHandler.CheckSignature(data))
+ return OverlayMessage::OC_ERROR;
+
+ CCharArrayParser charArrayParser;
+ charArrayParser.Reset(data, pPacket->iSize);
+ std::string line;
+
+ while (charArrayParser.ReadNextLine(line))
+ {
+ m_webvttHandler.DecodeLine(line, &subtitleList);
+ }
+
+ // We send an empty line to mark the end of the last Cue
+ m_webvttHandler.DecodeLine("", &subtitleList);
+ }
+
+ for (auto& subData : subtitleList)
+ {
+ SUBTITLES::STYLE::subtitleOpts opts;
+ opts.useMargins = subData.useMargins;
+ opts.marginLeft = subData.marginLeft;
+ opts.marginRight = subData.marginRight;
+ opts.marginVertical = subData.marginVertical;
+
+ int subId = AddSubtitle(subData.text, subData.startTime, subData.stopTime, &opts);
+
+ if (m_isISOFormat)
+ m_previousSubIds.emplace_back(subId);
+ }
+
+ return m_pOverlay ? OverlayMessage::OC_DONE : OverlayMessage::OC_OVERLAY;
+}
+
+void COverlayCodecWebVTT::Reset()
+{
+ Flush();
+}
+
+void COverlayCodecWebVTT::Flush()
+{
+ if (m_allowFlush)
+ {
+ if (m_pOverlay)
+ {
+ m_pOverlay->Release();
+ m_pOverlay = nullptr;
+ }
+ m_previousSubIds.clear();
+ FlushSubtitles();
+ }
+}
+
+CDVDOverlay* COverlayCodecWebVTT::GetOverlay()
+{
+ if (m_pOverlay)
+ return nullptr;
+ m_pOverlay = CreateOverlay();
+ m_pOverlay->SetOverlayContainerFlushable(m_allowFlush);
+ m_pOverlay->SetForcedMargins(m_webvttHandler.IsForcedMargins());
+ return m_pOverlay->Acquire();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/OverlayCodecWebVTT.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/OverlayCodecWebVTT.h
new file mode 100644
index 0000000..46c46c8
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/OverlayCodecWebVTT.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2005-2021 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 "DVDOverlayCodec.h"
+#include "DVDStreamInfo.h"
+#include "DVDSubtitles/SubtitlesAdapter.h"
+#include "cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTISOHandler.h"
+
+#include <vector>
+
+class CDVDOverlay;
+
+class COverlayCodecWebVTT : public CDVDOverlayCodec, private CSubtitlesAdapter
+{
+public:
+ COverlayCodecWebVTT();
+ ~COverlayCodecWebVTT() override;
+ bool Open(CDVDStreamInfo& hints, CDVDCodecOptions& options) override;
+ OverlayMessage Decode(DemuxPacket* pPacket) override;
+ void Reset() override;
+ void Flush() override;
+ CDVDOverlay* GetOverlay() override;
+
+private:
+ void Dispose() override;
+ CDVDOverlay* m_pOverlay;
+ CWebVTTISOHandler m_webvttHandler;
+ bool m_isISOFormat{false};
+ bool m_allowFlush{true};
+ std::vector<int> m_previousSubIds;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder.c b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder.c
new file mode 100644
index 0000000..2d2d7b6
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder.c
@@ -0,0 +1,874 @@
+/*
+ * Copyright (C) 2008-2020 Team Kodi
+ *
+ * Copyright (C) 2000-2008 the xine project
+ *
+ * Copyright (C) Christian Vogler
+ * cvogler@gradient.cis.upenn.edu - December 2001
+ *
+ * This file is part of xine, a free video player.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ *
+ * stuff needed to provide closed captioning decoding and display
+ *
+ * Some small bits and pieces of the EIA-608 captioning decoder were
+ * adapted from CCDecoder 0.9.1 by Mike Baker. The latest version is
+ * available at http://sourceforge.net/projects/ccdecoder/.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "cc_decoder.h"
+
+/* colors specified by the EIA 608 standard */
+enum { WHITE, GREEN, BLUE, CYAN, RED, YELLOW, MAGENTA, BLACK };
+
+/* --------------------- misc. EIA 608 definitions -------------------*/
+
+/* mapping from PAC row code to actual CC row */
+static int rowdata[] = {10, -1, 0, 1, 2, 3, 11, 12, 13, 14, 4, 5, 6, 7, 8, 9};
+
+/* CC codes use odd parity for error detection, since they originally were */
+/* transmitted via noisy video signals */
+static int parity_table[256];
+
+static cc_buffer_t* active_ccbuffer(cc_decoder_t* dec);
+
+/* --------------------- EIA 608 charsets -----------------------------*/
+// for all charsets, CC codes are essentially identical to ASCII apart for a few exceptions
+// see https://en.wikipedia.org/wiki/EIA-608#Characters for reference
+
+enum cc_charset
+{
+ CCSET_BASIC_AMERICAN = 0,
+ CCSET_SPECIAL_AMERICAN,
+ CCSET_EXTENDED_SPANISH_FRENCH_MISC,
+ CCSET_EXTENDED_PORTUGUESE_GERMAN_DANISH
+};
+
+const char* get_char_override(uint8_t charset, uint8_t c)
+{
+ if (charset == CCSET_BASIC_AMERICAN)
+ {
+ switch (c)
+ {
+ case 0x27:
+ return u8"\u2019";
+ case 0x2a:
+ return u8"\u00e1";
+ case 0x5c:
+ return u8"\u00e9";
+ case 0x5e:
+ return u8"\u00ed";
+ case 0x5f:
+ return u8"\u00f3";
+ case 0x60:
+ return u8"\u00fa";
+ case 0x7b:
+ return u8"\u00e7";
+ case 0x7c:
+ return u8"\u00f7";
+ case 0x7d:
+ return u8"\u00d1";
+ case 0x7e:
+ return u8"\u00f1";
+ case 0x7f:
+ return u8"\u2588";
+ default:
+ break;
+ }
+ }
+ else if (charset == CCSET_SPECIAL_AMERICAN)
+ {
+ switch (c)
+ {
+ case 0x30:
+ return u8"\u00ae";
+ case 0x31:
+ return u8"\u00b0";
+ case 0x32:
+ return u8"\u00bd";
+ case 0x33:
+ return u8"\u00bf";
+ case 0x34:
+ return u8"\u2122";
+ case 0x35:
+ return u8"\u00a2";
+ case 0x36:
+ return u8"\u00a3";
+ case 0x37:
+ return u8"\u266a";
+ case 0x38:
+ return u8"\u00e0";
+ case 0x39:
+ return u8"\u00A0";
+ case 0x3a:
+ return u8"\u00e8";
+ case 0x3b:
+ return u8"\u00e2";
+ case 0x3c:
+ return u8"\u00ea";
+ case 0x3d:
+ return u8"\u00ee";
+ case 0x3e:
+ return u8"\u00f4";
+ case 0x3f:
+ return u8"\u00fb";
+ default:
+ break;
+ }
+ }
+ else if (charset == CCSET_EXTENDED_SPANISH_FRENCH_MISC)
+ {
+ switch (c)
+ {
+ case 0x20:
+ return u8"\u00c1";
+ case 0x21:
+ return u8"\u00c9";
+ case 0x22:
+ return u8"\u00d3";
+ case 0x23:
+ return u8"\u00da";
+ case 0x24:
+ return u8"\u00dc";
+ case 0x25:
+ return u8"\u00fc";
+ case 0x26:
+ return u8"\u00b4";
+ case 0x27:
+ return u8"\u00a1";
+ case 0x28:
+ return u8"*";
+ case 0x29:
+ return u8"\u2018";
+ case 0x2a:
+ return u8"-";
+ case 0x2b:
+ return u8"\u00a9";
+ case 0x2c:
+ return u8"\u2120";
+ case 0x2d:
+ return u8"\u00b7";
+ case 0x2e:
+ return u8"\u201c";
+ case 0x2f:
+ return u8"\u201d";
+ case 0x30:
+ return u8"\u00c0";
+ case 0x31:
+ return u8"\u00c2";
+ case 0x32:
+ return u8"\u00c7";
+ case 0x33:
+ return u8"\u00c8";
+ case 0x34:
+ return u8"\u00ca";
+ case 0x35:
+ return u8"\u00cb";
+ case 0x36:
+ return u8"\u00eb";
+ case 0x37:
+ return u8"\u00ce";
+ case 0x38:
+ return u8"\u00cf";
+ case 0x39:
+ return u8"\u00ef";
+ case 0x3a:
+ return u8"\u00d4";
+ case 0x3b:
+ return u8"\u00d9";
+ case 0x3c:
+ return u8"\u00f9";
+ case 0x3d:
+ return u8"\u00db";
+ case 0x3e:
+ return u8"\u00ab";
+ case 0x3f:
+ return u8"\u00bb";
+ default:
+ break;
+ }
+ }
+ else if (charset == CCSET_EXTENDED_PORTUGUESE_GERMAN_DANISH)
+ {
+ switch (c)
+ {
+ case 0x20:
+ return u8"\u00c3";
+ case 0x21:
+ return u8"\u00e3";
+ case 0x22:
+ return u8"\u00cd";
+ case 0x23:
+ return u8"\u00cc";
+ case 0x24:
+ return u8"\u00ec";
+ case 0x25:
+ return u8"\u00d2";
+ case 0x26:
+ return u8"\u00f2";
+ case 0x27:
+ return u8"\u00d5";
+ case 0x28:
+ return u8"\u00f5";
+ case 0x29:
+ return u8"{";
+ case 0x2a:
+ return u8"}";
+ case 0x2b:
+ return u8"\\";
+ case 0x2c:
+ return u8"^";
+ case 0x2d:
+ return u8"_";
+ case 0x2e:
+ return u8"|";
+ case 0x2f:
+ return u8"~";
+ case 0x30:
+ return u8"\u00c4";
+ case 0x31:
+ return u8"\u00e4";
+ case 0x32:
+ return u8"\u00d6";
+ case 0x33:
+ return u8"\u00f6";
+ case 0x34:
+ return u8"\u00df";
+ case 0x35:
+ return u8"\u00a5";
+ case 0x36:
+ return u8"\u00a4";
+ case 0x37:
+ return u8"\u00a6";
+ case 0x38:
+ return u8"\u00c5";
+ case 0x39:
+ return u8"\u00e5";
+ case 0x3a:
+ return u8"\u00d8";
+ case 0x3b:
+ return u8"\u00f8";
+ case 0x3c:
+ return u8"\u250c";
+ case 0x3d:
+ return u8"\u2510";
+ case 0x3e:
+ return u8"\u2514";
+ case 0x3f:
+ return u8"\u2518";
+ default:
+ break;
+ }
+ }
+ // regular ascii char
+ return NULL;
+}
+
+/*---------------- general utility functions ---------------------*/
+
+static int parity(uint8_t byte)
+{
+ int i;
+ int ones = 0;
+
+ for (i = 0; i < 7; i++) {
+ if (byte & (1 << i))
+ ones++;
+ }
+
+ return ones & 1;
+}
+
+static void build_parity_table(void)
+{
+ uint8_t byte;
+ for (byte = 0; byte <= 127; byte++) {
+ int parity_v = parity(byte);
+ /* CC uses odd parity (i.e., # of 1's in byte is odd.) */
+ parity_table[byte] = parity_v;
+ parity_table[byte | 0x80] = !parity_v;
+ }
+}
+
+static int good_parity(uint16_t data)
+{
+ int ret = parity_table[data & 0xff] && parity_table[(data & 0xff00) >> 8];
+ if (! ret)
+ printf("Bad parity in EIA-608 data (%x)\n", data);
+ return ret;
+}
+
+static void ccbuf_add_char(cc_buffer_t* buf, uint8_t c, uint8_t charset)
+{
+ cc_row_t *rowbuf = &buf->rows[buf->rowpos];
+ int pos = rowbuf->pos;
+ int left_displayable = (pos > 0) && (pos <= rowbuf->num_chars);
+
+ if (pos >= CC_COLUMNS)
+ {
+ return;
+ }
+
+ /* midrow PAC attributes are applied only if there is no displayable */
+ /* character to the immediate left. This makes the implementation rather */
+ /* complicated, but this is what the EIA-608 standard specifies. :-( */
+ if (rowbuf->pac_attr_chg && !rowbuf->attr_chg && !left_displayable)
+ {
+ rowbuf->attr_chg = 1;
+ rowbuf->cells[pos].attributes = rowbuf->pac_attr;
+ }
+
+ rowbuf->cells[pos].c = c;
+ rowbuf->cells[pos].charset = charset;
+ rowbuf->cells[pos].midrow_attr = rowbuf->attr_chg;
+ rowbuf->pos++;
+
+ if (rowbuf->num_chars < rowbuf->pos)
+ rowbuf->num_chars = rowbuf->pos;
+
+ rowbuf->attr_chg = 0;
+ rowbuf->pac_attr_chg = 0;
+}
+
+
+static void ccbuf_set_cursor(cc_buffer_t *buf, int row, int column,
+ int underline, int italics, int color)
+{
+ cc_row_t *rowbuf = &buf->rows[row];
+ cc_attribute_t attr;
+
+ attr.italic = italics;
+ attr.underline = underline;
+ attr.foreground = color;
+ attr.background = BLACK;
+
+ rowbuf->pac_attr = attr;
+ rowbuf->pac_attr_chg = 1;
+
+ buf->rowpos = row;
+ rowbuf->pos = column;
+ rowbuf->attr_chg = 0;
+}
+
+
+static void ccbuf_apply_attribute(cc_buffer_t *buf, cc_attribute_t *attr)
+{
+ cc_row_t *rowbuf = &buf->rows[buf->rowpos];
+ int pos = rowbuf->pos;
+
+ rowbuf->attr_chg = 1;
+ rowbuf->cells[pos].attributes = *attr;
+ /* A midrow attribute always counts as a space */
+ ccbuf_add_char(buf, (unsigned int)' ', CCSET_BASIC_AMERICAN);
+}
+
+
+static void ccbuf_tab(cc_buffer_t *buf, int tabsize)
+{
+ cc_row_t *rowbuf = &buf->rows[buf->rowpos];
+ rowbuf->pos += tabsize;
+ if (rowbuf->pos > CC_COLUMNS)
+ {
+ rowbuf->pos = CC_COLUMNS;
+ return;
+ }
+ /* tabs have no effect on pending PAC attribute changes */
+}
+
+/*----------------- cc_memory_t methods --------------------------------*/
+
+static void ccrow_der(cc_row_t *row, int pos)
+{
+ int i;
+ for (i = pos; i < CC_COLUMNS; i++)
+ {
+ row->cells[i].c = ' ';
+ }
+}
+
+static void ccmem_clear(cc_memory_t *buf)
+{
+ int i;
+ memset(buf, 0, sizeof (cc_memory_t));
+ for (i = 0; i < CC_ROWS; i++)
+ {
+ ccrow_der(&buf->channel[0].rows[i], 0);
+ ccrow_der(&buf->channel[1].rows[i], 0);
+ }
+}
+
+static void ccmem_init(cc_memory_t *buf)
+{
+ ccmem_clear(buf);
+}
+
+static void ccmem_exit(cc_memory_t *buf)
+{
+/*FIXME: anything to deallocate?*/
+}
+
+void ccmem_tobuf(cc_decoder_t *dec)
+{
+ cc_buffer_t *buf = &dec->on_buf->channel[dec->on_buf->channel_no];
+ int empty = 1;
+ dec->textlen = 0;
+ int i,j;
+ for (i = 0; i < CC_ROWS; i++)
+ {
+ for (j = 0; j<CC_COLUMNS; j++)
+ if (buf->rows[i].cells[j].c != ' ')
+ {
+ empty = 0;
+ break;
+ }
+ if (!empty)
+ break;
+ }
+ if (empty)
+ return; // Nothing to write
+
+ for (i = 0; i<CC_ROWS; i++)
+ {
+ int empty = 1;
+ for (j = 0; j<CC_COLUMNS; j++)
+ if (buf->rows[i].cells[j].c != ' ')
+ empty = 0;
+ if (!empty)
+ {
+ int f, l; // First,last used char
+ for (f = 0; f<CC_COLUMNS; f++)
+ if (buf->rows[i].cells[f].c != ' ')
+ break;
+ for (l = CC_COLUMNS-1; l>0; l--)
+ if (buf->rows[i].cells[l].c != ' ')
+ break;
+ for (j = f; j <= l; j++)
+ {
+ const char* chbytes =
+ get_char_override(buf->rows[i].cells[j].charset, buf->rows[i].cells[j].c);
+ if (chbytes != NULL)
+ {
+ for (; *chbytes != '\0'; chbytes++)
+ {
+ dec->text[dec->textlen++] = *chbytes;
+ }
+ }
+ else
+ {
+ // ascii char
+ dec->text[dec->textlen++] = (unsigned char)buf->rows[i].cells[j].c;
+ }
+ }
+ dec->text[dec->textlen++] = '\n';
+ }
+ }
+
+ // FIXME: the end-of-string char is often wrong cause unexpected behaviours
+ if (dec->textlen > 0)
+ {
+ if (dec->text[dec->textlen - 1] == '\n' && dec->text[dec->textlen] != '\0')
+ {
+ dec->text[dec->textlen] = '\0';
+ }
+ else if (dec->text[dec->textlen] != '\0')
+ {
+ dec->text[dec->textlen++] = '\n';
+ dec->text[dec->textlen++] = '\0';
+ }
+ }
+
+ dec->callback(0, dec->userdata);
+}
+
+/*----------------- cc_decoder_t methods --------------------------------*/
+
+static void cc_set_channel(cc_decoder_t *dec, int channel)
+{
+ (*dec->active)->channel_no = channel;
+}
+
+static cc_buffer_t *active_ccbuffer(cc_decoder_t *dec)
+{
+ cc_memory_t *mem = *dec->active;
+ return &mem->channel[mem->channel_no];
+}
+
+static void cc_swap_buffers(cc_decoder_t *dec)
+{
+ cc_memory_t *temp;
+
+ /* hide caption in displayed memory */
+ /* cc_hide_displayed(dec); */
+
+ temp = dec->on_buf;
+ dec->on_buf = dec->off_buf;
+ dec->off_buf = temp;
+
+ /* show new displayed memory */
+ /* cc_show_displayed(dec); */
+}
+
+static void cc_roll_up(cc_decoder_t *dec)
+{
+ cc_buffer_t *buf = active_ccbuffer(dec);
+ int i, j;
+ for (i = buf->rowpos - dec->rollup_rows + 1; i < buf->rowpos; i++)
+ {
+ if (i < 0)
+ continue;
+
+ for (j = 0; j < CC_COLUMNS; j++)
+ {
+ buf->rows[i].cells[j] = buf->rows[i + 1].cells[j];
+ }
+ }
+ for (j = 0; j < CC_COLUMNS; j++)
+ {
+ buf->rows[buf->rowpos].cells[j].c = ' ';
+ }
+ buf->rows[buf->rowpos].pos = 0;
+}
+
+static void cc_decode_standard_char(cc_decoder_t *dec, uint8_t c1, uint8_t c2)
+{
+ cc_buffer_t *buf = active_ccbuffer(dec);
+ /* c1 always is a valid character */
+ ccbuf_add_char(buf, c1, CCSET_BASIC_AMERICAN);
+ /* c2 might not be a printable character, even if c1 was */
+ if (c2 & 0x60)
+ ccbuf_add_char(buf, c2, CCSET_BASIC_AMERICAN);
+}
+
+
+static void cc_decode_PAC(cc_decoder_t *dec, int channel,
+ uint8_t c1, uint8_t c2)
+{
+ cc_buffer_t *buf;
+ int row, column = 0;
+ int underline, italics = 0, color;
+
+ /* There is one invalid PAC code combination. Ignore it. */
+ if (c1 == 0x10 && c2 > 0x5f)
+ return;
+
+ cc_set_channel(dec, channel);
+ buf = active_ccbuffer(dec);
+
+ row = rowdata[((c1 & 0x07) << 1) | ((c2 & 0x20) >> 5)];
+ if (c2 & 0x10)
+ {
+ column = ((c2 & 0x0e) >> 1) * 4; /* preamble indentation */
+ color = WHITE; /* indented lines have white color */
+ }
+ else if ((c2 & 0x0e) == 0x0e)
+ {
+ italics = 1; /* italics, they are always white */
+ color = WHITE;
+ }
+ else
+ color = (c2 & 0x0e) >> 1;
+ underline = c2 & 0x01;
+
+ ccbuf_set_cursor(buf, row, column, underline, italics, color);
+}
+
+static void cc_decode_ext_attribute(cc_decoder_t* dec, int channel)
+{
+ cc_set_channel(dec, channel);
+}
+
+static void cc_decode_special_char(cc_decoder_t* dec, int channel, uint8_t charset, uint8_t c2)
+{
+ cc_buffer_t *buf;
+
+ cc_set_channel(dec, channel);
+ buf = active_ccbuffer(dec);
+ ccbuf_add_char(buf, c2, charset);
+}
+
+static void cc_decode_midrow_attr(cc_decoder_t* dec, int channel, uint8_t c2)
+{
+ cc_buffer_t *buf;
+ cc_attribute_t attr;
+
+ cc_set_channel(dec, channel);
+ buf = active_ccbuffer(dec);
+ if (c2 < 0x2e)
+ {
+ attr.italic = 0;
+ attr.foreground = (c2 & 0xe) >> 1;
+ }
+ else
+ {
+ attr.italic = 1;
+ attr.foreground = WHITE;
+ }
+ attr.underline = c2 & 0x1;
+ attr.background = BLACK;
+
+ ccbuf_apply_attribute(buf, &attr);
+}
+
+
+static void cc_decode_misc_control_code(cc_decoder_t* dec, int channel, uint8_t c2)
+{
+ cc_set_channel(dec, channel);
+ cc_buffer_t *buf;
+
+ switch (c2)
+ { /* 0x20 <= c2 <= 0x2f */
+ case 0x20: /* RCL */
+ dec->style = CC_POPON;
+ dec->active = &dec->off_buf;
+ break;
+
+ case 0x21: /* backspace */
+ break;
+
+ case 0x24: /* DER */
+ buf = active_ccbuffer(dec);
+ ccrow_der(&buf->rows[buf->rowpos], buf->rows[buf->rowpos].pos);
+ break;
+
+ case 0x25: /* RU2 */
+ dec->rollup_rows = 2;
+ dec->style = CC_ROLLUP;
+ dec->active = &dec->on_buf;
+ break;
+
+ case 0x26: /* RU3 */
+ dec->rollup_rows = 3;
+ dec->style = CC_ROLLUP;
+ dec->active = &dec->on_buf;
+ break;
+
+ case 0x27: /* RU4 */
+ dec->rollup_rows = 4;
+ dec->style = CC_ROLLUP;
+ dec->active = &dec->on_buf;
+ break;
+
+ case 0x28: /* FON */
+ break;
+
+ case 0x29: /* RDC */
+ dec->style = CC_PAINTON;
+ dec->active = &dec->on_buf;
+ break;
+
+ case 0x2a: /* TR */
+ break;
+
+ case 0x2b: /* RTD */
+ break;
+
+ case 0x2c: /* EDM - erase displayed memory */
+ /* cc_hide_displayed(dec); */
+ ccmem_clear(dec->on_buf);
+ break;
+
+ case 0x2d: /* carriage return */
+ if (dec->style == CC_ROLLUP)
+ {
+ cc_roll_up(dec);
+ }
+ break;
+
+ case 0x2e: /* ENM - erase non-displayed memory */
+ ccmem_clear(dec->off_buf);
+ break;
+
+ case 0x2f: /* EOC - swap displayed and non displayed memory */
+ cc_swap_buffers(dec);
+ dec->style = CC_POPON;
+ dec->active = &dec->off_buf;
+ ccmem_tobuf(dec);
+ break;
+
+ default:
+ break;
+ }
+}
+
+static void cc_decode_tab(cc_decoder_t* dec, int channel, uint8_t c2)
+{
+ cc_buffer_t *buf;
+
+ cc_set_channel(dec, channel);
+ buf = active_ccbuffer(dec);
+ ccbuf_tab(buf, c2 & 0x3);
+}
+
+static void cc_decode_EIA608(cc_decoder_t *dec, uint16_t data)
+{
+ uint8_t c1 = data & 0x7f;
+ uint8_t c2 = (data >> 8) & 0x7f;
+
+ if (c1 & 0x60)
+ { /* normal character, 0x20 <= c1 <= 0x7f */
+ if (dec->style == CC_NOTSET)
+ return;
+
+ cc_decode_standard_char(dec, c1, c2);
+ if (dec->style == CC_ROLLUP)
+ {
+ ccmem_tobuf(dec);
+ }
+ }
+ else if (c1 & 0x10)
+ {
+ /* control code or special character */
+ /* 0x10 <= c1 <= 0x1f */
+ int channel = (c1 & 0x08) >> 3;
+ c1 &= ~0x08;
+
+ /* control sequences are often repeated. In this case, we should */
+ /* evaluate it only once. */
+ if (data != dec->lastcode)
+ {
+ if (c2 & 0x40)
+ { /* preamble address code: 0x40 <= c2 <= 0x7f */
+ cc_decode_PAC(dec, channel, c1, c2);
+ }
+ else
+ {
+ switch (c1)
+ {
+ case 0x10: /* extended background attribute code */
+ cc_decode_ext_attribute(dec, channel);
+ break;
+
+ case 0x11: /* attribute or special character */
+ if (dec->style == CC_NOTSET)
+ return;
+
+ if ((c2 & 0x30) == 0x30)
+ {
+ /* special char: 0x30 <= c2 <= 0x3f */
+ /* CCSET_SPECIAL_AMERICAN */
+ cc_decode_special_char(dec, channel, CCSET_SPECIAL_AMERICAN, c2);
+ if (dec->style == CC_ROLLUP)
+ {
+ ccmem_tobuf(dec);
+ }
+ }
+ else if (c2 & 0x20)
+ {
+ /* midrow attribute: 0x20 <= c2 <= 0x2f */
+ cc_decode_midrow_attr(dec, channel, c2);
+ }
+ break;
+
+ case 0x12: /* CCSET_EXTENDED_SPANISH_FRENCH_MISC */
+ cc_decode_special_char(dec, channel, CCSET_EXTENDED_SPANISH_FRENCH_MISC, c2);
+ if (dec->style == CC_ROLLUP)
+ {
+ ccmem_tobuf(dec);
+ }
+ break;
+
+ case 0x13: /* CCSET_EXTENDED_PORTUGUESE_GERMAN_DANISH */
+ cc_decode_special_char(dec, channel, CCSET_EXTENDED_PORTUGUESE_GERMAN_DANISH, c2);
+ if (dec->style == CC_ROLLUP)
+ {
+ ccmem_tobuf(dec);
+ }
+ break;
+
+ case 0x14: /* possibly miscellaneous control code */
+ cc_decode_misc_control_code(dec, channel, c2);
+ break;
+
+ case 0x17: /* possibly misc. control code TAB offset */
+ /* 0x21 <= c2 <= 0x23 */
+ if (c2 >= 0x21 && c2 <= 0x23)
+ {
+ cc_decode_tab(dec, channel, c2);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ dec->lastcode = data;
+}
+
+
+void decode_cc(cc_decoder_t *dec, const uint8_t *buffer, uint32_t buf_len)
+{
+ uint32_t i;
+ for (i = 0; i<buf_len; i += 3)
+ {
+ unsigned char cc_type = buffer[i] & 0x03;
+
+ uint8_t data1 = buffer[i + 1];
+ uint8_t data2 = buffer[i + 2];
+
+ switch (cc_type)
+ {
+ case 0:
+ if (good_parity(data1 | (data2 << 8)))
+ {
+ cc_decode_EIA608(dec, data1 | (data2 << 8));
+ }
+ break;
+
+ case 1:
+ break;
+
+ default:
+ break;
+ }
+ }
+}
+
+cc_decoder_t *cc_decoder_open()
+{
+ cc_decoder_t *dec = (cc_decoder_t *) calloc(1, sizeof (cc_decoder_t));
+ if (!dec)
+ return NULL;
+
+ ccmem_init(&dec->buffer[0]);
+ ccmem_init(&dec->buffer[1]);
+ dec->on_buf = &dec->buffer[0];
+ dec->off_buf = &dec->buffer[1];
+ dec->active = &dec->off_buf;
+
+ dec->lastcode = 0;
+
+ return dec;
+}
+
+void cc_decoder_close(cc_decoder_t *dec)
+{
+ ccmem_exit(&dec->buffer[0]);
+ ccmem_exit(&dec->buffer[1]);
+
+ free(dec);
+}
+
+
+/*--------------- initialization methods --------------------------*/
+
+void cc_decoder_init(void)
+{
+ build_parity_table();
+}
+
+#ifdef __cplusplus
+}
+#endif
+
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder.h
new file mode 100644
index 0000000..d9fc266
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder.h
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2000-2008 the xine project
+ *
+ * Copyright (C) Christian Vogler
+ * cvogler@gradient.cis.upenn.edu - December 2001
+ *
+ * This file is part of xine, a free video player.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ *
+ * stuff needed to provide closed captioning decoding and display
+ *
+ * Some small bits and pieces of the EIA-608 captioning decoder were
+ * adapted from CCDecoder 0.9.1 by Mike Baker. The latest version is
+ * available at http://sourceforge.net/projects/ccdecoder/.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#define CC_ROWS 15
+#define CC_COLUMNS 32
+#define CC_CHANNELS 2
+
+typedef struct cc_attribute_s {
+ uint8_t italic;
+ uint8_t underline;
+ uint8_t foreground;
+ uint8_t background;
+} cc_attribute_t;
+
+/* CC character cell */
+typedef struct cc_char_cell_s
+{
+ uint8_t c; /* character code, not the same as ASCII */
+ cc_attribute_t attributes; /* attributes of this character, if changed */
+ /* here */
+ uint8_t charset; /* charset type */
+ int midrow_attr; /* true if this cell changes an attribute */
+} cc_char_cell_t;
+
+/* a single row in the closed captioning memory */
+typedef struct cc_row_s
+{
+ cc_char_cell_t cells[CC_COLUMNS];
+ int pos; /* position of the cursor */
+ int num_chars; /* how many characters in the row are data */
+ int attr_chg; /* true if midrow attr. change at cursor pos */
+ int pac_attr_chg; /* true if attribute has changed via PAC */
+ cc_attribute_t pac_attr; /* PAC attr. that hasn't been applied yet */
+} cc_row_t;
+
+/* closed captioning memory for a single channel */
+typedef struct cc_buffer_s
+{
+ cc_row_t rows[CC_ROWS];
+ int rowpos; /* row cursor position */
+} cc_buffer_t;
+
+/* captioning memory for all channels */
+typedef struct cc_memory_s
+{
+ cc_buffer_t channel[CC_CHANNELS];
+ int channel_no; /* currently active channel */
+} cc_memory_t;
+
+enum cc_style
+{
+ CC_NOTSET = 0,
+ CC_ROLLUP,
+ CC_PAINTON,
+ CC_POPON
+};
+
+/* The closed captioning decoder data structure */
+struct cc_decoder_s
+{
+ /* CC decoder buffer - one onscreen, one offscreen */
+ cc_memory_t buffer[2];
+ /* onscreen, offscreen buffer ptrs */
+ cc_memory_t *on_buf;
+ cc_memory_t *off_buf;
+ /* which buffer is active for receiving data */
+ cc_memory_t **active;
+
+ /* the last captioning code seen (control codes are often sent twice
+ in a row, but should be processed only once) */
+ uint32_t lastcode;
+
+ uint16_t rollup_rows;
+ enum cc_style style;
+
+ void *userdata;
+ void(*callback)(int service, void *userdata);
+ char text[CC_ROWS*CC_COLUMNS + 1];
+ int textlen;
+};
+
+typedef struct cc_decoder_s cc_decoder_t;
+
+cc_decoder_t *cc_decoder_open();
+void cc_decoder_close(cc_decoder_t *this_obj);
+void cc_decoder_init(void);
+
+void decode_cc(cc_decoder_t *dec, const uint8_t *buffer, uint32_t buf_len);
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder708.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder708.cpp
new file mode 100644
index 0000000..910b724
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder708.cpp
@@ -0,0 +1,1192 @@
+/*
+ * this is mostly borrowed from ccextractor http://ccextractor.sourceforge.net/
+ */
+
+#include "cc_decoder708.h"
+
+#include "utils/log.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/********************************************************
+256 BYTES IS ENOUGH FOR ALL THE SUPPORTED CHARACTERS IN
+EIA-708, SO INTERNALLY WE USE THIS TABLE (FOR CONVENIENCE)
+
+00-1F -> Characters that are in the G2 group in 20-3F,
+ except for 06, which is used for the closed captions
+ sign "CC" which is defined in group G3 as 00. (this
+ is by the article 33).
+20-7F -> Group G0 as is - corresponds to the ASCII code
+80-9F -> Characters that are in the G2 group in 60-7F
+ (there are several blank characters here, that's OK)
+A0-FF -> Group G1 as is - non-English characters and symbols
+*/
+
+unsigned char get_internal_from_G0 (unsigned char g0_char)
+{
+ return g0_char;
+}
+
+unsigned char get_internal_from_G1 (unsigned char g1_char)
+{
+ return g1_char;
+}
+
+//! @todo Probably not right
+// G2: Extended Control Code Set 1
+unsigned char get_internal_from_G2 (unsigned char g2_char)
+{
+ // according to the comment a few lines above those lines are indeed wrong
+ /*
+ if (g2_char>=0x20 && g2_char<=0x3F)
+ return g2_char-0x20;
+ if (g2_char>=0x60 && g2_char<=0x7F)
+ return g2_char+0x20;
+ */
+ // Rest unmapped, so we return a blank space
+ return 0x20;
+}
+
+//! @todo Probably not right
+// G3: Future Characters and Icon Expansion
+unsigned char get_internal_from_G3 (unsigned char g3_char)
+{
+ if (g3_char==0xa0) // The "CC" (closed captions) sign
+ return 0x06;
+ // Rest unmapped, so we return a blank space
+ return 0x20;
+}
+
+void clearTV (cc708_service_decoder *decoder);
+
+const char *COMMANDS_C0[32]=
+{
+ "NUL", // 0 = NUL
+ NULL, // 1 = Reserved
+ NULL, // 2 = Reserved
+ "ETX", // 3 = ETX
+ NULL, // 4 = Reserved
+ NULL, // 5 = Reserved
+ NULL, // 6 = Reserved
+ NULL, // 7 = Reserved
+ "BS", // 8 = Backspace
+ NULL, // 9 = Reserved
+ NULL, // A = Reserved
+ NULL, // B = Reserved
+ "FF", // C = FF
+ "CR", // D = CR
+ "HCR", // E = HCR
+ NULL, // F = Reserved
+ "EXT1",// 0x10 = EXT1,
+ NULL, // 0x11 = Reserved
+ NULL, // 0x12 = Reserved
+ NULL, // 0x13 = Reserved
+ NULL, // 0x14 = Reserved
+ NULL, // 0x15 = Reserved
+ NULL, // 0x16 = Reserved
+ NULL, // 0x17 = Reserved
+ "P16", // 0x18 = P16
+ NULL, // 0x19 = Reserved
+ NULL, // 0x1A = Reserved
+ NULL, // 0x1B = Reserved
+ NULL, // 0x1C = Reserved
+ NULL, // 0x1D = Reserved
+ NULL, // 0x1E = Reserved
+ NULL, // 0x1F = Reserved
+};
+
+struct S_COMMANDS_C1 COMMANDS_C1[32]=
+{
+ {CW0,"CW0","SetCurrentWindow0", 1},
+ {CW1,"CW1","SetCurrentWindow1", 1},
+ {CW2,"CW2","SetCurrentWindow2", 1},
+ {CW3,"CW3","SetCurrentWindow3", 1},
+ {CW4,"CW4","SetCurrentWindow4", 1},
+ {CW5,"CW5","SetCurrentWindow5", 1},
+ {CW6,"CW6","SetCurrentWindow6", 1},
+ {CW7,"CW7","SetCurrentWindow7", 1},
+ {CLW,"CLW","ClearWindows", 2},
+ {DSW,"DSW","DisplayWindows", 2},
+ {HDW,"HDW","HideWindows", 2},
+ {TGW,"TGW","ToggleWindows", 2},
+ {DLW,"DLW","DeleteWindows", 2},
+ {DLY,"DLY","Delay", 2},
+ {DLC,"DLC","DelayCancel", 1},
+ {RST,"RST","Reset", 1},
+ {SPA,"SPA","SetPenAttributes", 3},
+ {SPC,"SPC","SetPenColor", 4},
+ {SPL,"SPL","SetPenLocation", 3},
+ {RSV93,"RSV93","Reserved", 1},
+ {RSV94,"RSV94","Reserved", 1},
+ {RSV95,"RSV95","Reserved", 1},
+ {RSV96,"RSV96","Reserved", 1},
+ {SWA,"SWA","SetWindowAttributes", 5},
+ {DF0,"DF0","DefineWindow0", 7},
+ {DF1,"DF0","DefineWindow1", 7},
+ {DF2,"DF0","DefineWindow2", 7},
+ {DF3,"DF0","DefineWindow3", 7},
+ {DF4,"DF0","DefineWindow4", 7},
+ {DF5,"DF0","DefineWindow5", 7},
+ {DF6,"DF0","DefineWindow6", 7},
+ {DF7,"DF0","DefineWindow7", 7}
+};
+
+void clear_packet(cc708_service_decoder *decoder)
+{
+ decoder->parent->m_current_packet_length = 0;
+}
+
+void cc708_service_reset(cc708_service_decoder *decoder)
+{
+ // There's lots of other stuff that we need to do, such as canceling delays
+ for (e708Window& window : decoder->windows)
+ {
+ window.is_defined=0;
+ window.visible=0;
+ window.memory_reserved=0;
+ window.is_empty=1;
+ memset (window.commands, 0,
+ sizeof (window.commands));
+ }
+ decoder->current_window=-1;
+ clearTV(decoder);
+ decoder->inited=1;
+}
+
+void cc708_reset(cc708_service_decoder *decoders)
+{
+ for (int i = 0; i<CCX_DECODERS_708_MAX_SERVICES; i++)
+ {
+ cc708_service_reset (&decoders[i]);
+ }
+ // Empty packet buffer
+ clear_packet(&decoders[0]);
+ decoders[0].parent->m_last_seq = -1;
+}
+
+int compWindowsPriorities (const void *a, const void *b)
+{
+ const e708Window *w1=*(e708Window * const*)a;
+ const e708Window *w2=*(e708Window * const*)b;
+ return w1->priority-w2->priority;
+}
+
+void clearTV (cc708_service_decoder *decoder)
+{
+ for (unsigned char (&row)[I708_SCREENGRID_COLUMNS + 1] : decoder->tv.chars)
+ {
+ memset (row, ' ', I708_SCREENGRID_COLUMNS);
+ row[I708_SCREENGRID_COLUMNS]=0;
+ }
+};
+
+void printTVtoBuf (cc708_service_decoder *decoder)
+{
+ int empty=1;
+ decoder->textlen = 0;
+ for (unsigned char(&row)[I708_SCREENGRID_COLUMNS + 1] : decoder->tv.chars)
+ {
+ for (int j=0;j<210;j++)
+ if (row[j] != ' ')
+ {
+ empty=0;
+ break;
+ }
+ if (!empty)
+ break;
+ }
+ if (empty)
+ return; // Nothing to write
+
+ for (unsigned char(&row)[I708_SCREENGRID_COLUMNS + 1] : decoder->tv.chars)
+ {
+ int empty=1;
+ for (int j=0;j<210;j++)
+ if (row[j] != ' ')
+ empty=0;
+ if (!empty)
+ {
+ int f,l; // First,last used char
+ for (f=0;f<210;f++)
+ if (row[f] != ' ')
+ break;
+ for (l=209;l>0;l--)
+ if (row[l]!=' ')
+ break;
+ for (int j=f;j<=l;j++)
+ decoder->text[decoder->textlen++] = row[j];
+ decoder->text[decoder->textlen++] = '\r';
+ decoder->text[decoder->textlen++] = '\n';
+ }
+ }
+
+ // FIXME: the end-of-string char is often wrong cause unexpected behaviours
+ if (decoder->textlen >= 2)
+ {
+ if (decoder->text[decoder->textlen - 2] == '\r' &&
+ decoder->text[decoder->textlen - 1] == '\n' && decoder->text[decoder->textlen] != '\0')
+ {
+ decoder->text[decoder->textlen] = '\0';
+ }
+ else if (decoder->text[decoder->textlen] != '\0')
+ {
+ decoder->text[decoder->textlen++] = '\r';
+ decoder->text[decoder->textlen++] = '\n';
+ decoder->text[decoder->textlen++] = '\0';
+ }
+ }
+}
+
+void updateScreen (cc708_service_decoder *decoder)
+{
+ clearTV (decoder);
+
+ // THIS FUNCTION WILL DO THE MAGIC OF ACTUALLY EXPORTING THE DECODER STATUS
+ // TO SEVERAL FILES
+ e708Window *wnd[I708_MAX_WINDOWS]; // We'll store here the visible windows that contain anything
+ int visible=0;
+ for (e708Window& window : decoder->windows)
+ {
+ if (window.is_defined && window.visible && !window.is_empty)
+ wnd[visible++]=&window;
+ }
+ qsort (wnd,visible,sizeof (e708Window *),compWindowsPriorities);
+
+ for (int i=0;i<visible;i++)
+ {
+ int top,left;
+ // For each window we calculate the top,left position depending on the
+ // anchor
+ switch (wnd[i]->anchor_point)
+ {
+ case anchorpoint_top_left:
+ top=wnd[i]->anchor_vertical;
+ left=wnd[i]->anchor_horizontal;
+ break;
+ case anchorpoint_top_center:
+ top=wnd[i]->anchor_vertical;
+ left=wnd[i]->anchor_horizontal - wnd[i]->col_count/2;
+ break;
+ case anchorpoint_top_right:
+ top=wnd[i]->anchor_vertical;
+ left=wnd[i]->anchor_horizontal - wnd[i]->col_count;
+ break;
+ case anchorpoint_middle_left:
+ top=wnd[i]->anchor_vertical - wnd[i]->row_count/2;
+ left=wnd[i]->anchor_horizontal;
+ break;
+ case anchorpoint_middle_center:
+ top=wnd[i]->anchor_vertical - wnd[i]->row_count/2;
+ left=wnd[i]->anchor_horizontal - wnd[i]->col_count/2;
+ break;
+ case anchorpoint_middle_right:
+ top=wnd[i]->anchor_vertical - wnd[i]->row_count/2;
+ left=wnd[i]->anchor_horizontal - wnd[i]->col_count;
+ break;
+ case anchorpoint_bottom_left:
+ top=wnd[i]->anchor_vertical - wnd[i]->row_count;
+ left=wnd[i]->anchor_horizontal;
+ break;
+ case anchorpoint_bottom_center:
+ top=wnd[i]->anchor_vertical - wnd[i]->row_count;
+ left=wnd[i]->anchor_horizontal - wnd[i]->col_count/2;
+ break;
+ case anchorpoint_bottom_right:
+ top=wnd[i]->anchor_vertical - wnd[i]->row_count;
+ left=wnd[i]->anchor_horizontal - wnd[i]->col_count;
+ break;
+ default: // Shouldn't happen, but skip the window just in case
+ continue;
+ }
+ if (top<0)
+ top=0;
+ if (left<0)
+ left=0;
+ int copyrows=top + wnd[i]->row_count >= I708_SCREENGRID_ROWS ?
+ I708_SCREENGRID_ROWS - top : wnd[i]->row_count;
+ int copycols=left + wnd[i]->col_count >= I708_SCREENGRID_COLUMNS ?
+ I708_SCREENGRID_COLUMNS - left : wnd[i]->col_count;
+ for (int j=0;j<copyrows;j++)
+ {
+ memcpy (decoder->tv.chars[top+j],wnd[i]->rows[j],copycols);
+ }
+ }
+ printTVtoBuf(decoder);
+ decoder->callback(decoder->service, decoder->userdata);
+}
+
+void rollupWindow(cc708_service_decoder *decoder, int window)
+{
+ for (int row=0; row<decoder->windows[window].row_count - 1; row++)
+ {
+ memcpy(decoder->windows[window].rows[row], decoder->windows[window].rows[row+1], decoder->windows[window].col_count);
+ }
+ memset(decoder->windows[window].rows[decoder->windows[window].row_count-1], ' ', decoder->windows[window].col_count);
+}
+
+/* This function handles future codes. While by definition we can't do any work on them, we must return
+how many bytes would be consumed if these codes were supported, as defined in the specs.
+Note: EXT1 not included */
+// C2: Extended Miscellaneous Control Codes
+//! @todo This code is completely untested due to lack of samples. Just following specs!
+int handle_708_C2 (cc708_service_decoder *decoder, unsigned char *data, int data_length)
+{
+ if (data[0]<=0x07) // 00-07...
+ return 1; // ... Single-byte control bytes (0 additional bytes)
+ else if (data[0]<=0x0f) // 08-0F ...
+ return 2; // ..two-byte control codes (1 additional byte)
+ else if (data[0]<=0x17) // 10-17 ...
+ return 3; // ..three-byte control codes (2 additional bytes)
+ return 4; // 18-1F => four-byte control codes (3 additional bytes)
+}
+
+int handle_708_C3 (cc708_service_decoder *decoder, unsigned char *data, int data_length)
+{
+ if (data[0] < 0x80 || data[0] > 0x9F)
+ CLog::Log(LOGERROR, "{} - Entry in handle_708_C3 with an out of range value", __FUNCTION__);
+ if (data[0]<=0x87) // 80-87...
+ return 5; // ... Five-byte control bytes (4 additional bytes)
+ else if (data[0]<=0x8F) // 88-8F ...
+ return 6; // ..Six-byte control codes (5 additional byte)
+ // If here, then 90-9F ...
+
+ // These are variable length commands, that can even span several segments
+ // (they allow even downloading fonts or graphics).
+ //! @todo Implement if a sample ever appears
+ return 0; // Unreachable, but otherwise there's compilers warnings
+}
+
+// This function handles extended codes (EXT1 + code), from the extended sets
+// G2 (20-7F) => Mostly unmapped, except for a few characters.
+// G3 (A0-FF) => A0 is the CC symbol, everything else reserved for future expansion in EIA708-B
+// C2 (00-1F) => Reserved for future extended misc. control and captions command codes
+//! @todo This code is completely untested due to lack of samples. Just following specs!
+// Returns number of used bytes, usually 1 (since EXT1 is not counted).
+int handle_708_extended_char (cc708_service_decoder *decoder, unsigned char *data, int data_length)
+{
+ int used;
+ unsigned char c=0x20; // Default to space
+ unsigned char code=data[0];
+ if (/* data[i]>=0x00 && */ code<=0x1F) // Comment to silence warning
+ {
+ used=handle_708_C2 (decoder, data, data_length);
+ }
+ // Group G2 - Extended Miscellaneous Characters
+ else if (code>=0x20 && code<=0x7F)
+ {
+ c=get_internal_from_G2 (code);
+ used=1;
+ process_character (decoder, c);
+ }
+ // Group C3
+ else if (code>=0x80 && code<=0x9F)
+ {
+ used=handle_708_C3 (decoder, data, data_length);
+ //! @todo Something
+ }
+ // Group G3
+ else
+ {
+ c=get_internal_from_G3 (code);
+ used=1;
+ process_character (decoder, c);
+ }
+ return used;
+}
+
+void process_cr (cc708_service_decoder *decoder)
+{
+ switch (decoder->windows[decoder->current_window].attribs.print_dir)
+ {
+ case pd_left_to_right:
+ decoder->windows[decoder->current_window].pen_column=0;
+ if (decoder->windows[decoder->current_window].pen_row+1 < decoder->windows[decoder->current_window].row_count)
+ decoder->windows[decoder->current_window].pen_row++;
+ break;
+ case pd_right_to_left:
+ decoder->windows[decoder->current_window].pen_column=decoder->windows[decoder->current_window].col_count;
+ if (decoder->windows[decoder->current_window].pen_row+1 < decoder->windows[decoder->current_window].row_count)
+ decoder->windows[decoder->current_window].pen_row++;
+ break;
+ case pd_top_to_bottom:
+ decoder->windows[decoder->current_window].pen_row=0;
+ if (decoder->windows[decoder->current_window].pen_column+1 < decoder->windows[decoder->current_window].col_count)
+ decoder->windows[decoder->current_window].pen_column++;
+ break;
+ case pd_bottom_to_top:
+ decoder->windows[decoder->current_window].pen_row=decoder->windows[decoder->current_window].row_count;
+ if (decoder->windows[decoder->current_window].pen_column+1 < decoder->windows[decoder->current_window].col_count)
+ decoder->windows[decoder->current_window].pen_column++;
+ break;
+ }
+
+ if (decoder->windows[decoder->current_window].anchor_point == anchorpoint_bottom_left ||
+ decoder->windows[decoder->current_window].anchor_point == anchorpoint_bottom_center)
+ {
+ rollupWindow(decoder, decoder->current_window);
+ updateScreen(decoder);
+ }
+}
+
+int handle_708_C0 (cc708_service_decoder *decoder, unsigned char *data, int data_length)
+{
+ const char *name=COMMANDS_C0[data[0]];
+ if (name==NULL)
+ name="Reserved";
+ int len=-1;
+ // These commands have a known length even if they are reserved.
+ if (/* data[0]>=0x00 && */ data[0]<=0xF) // Comment to silence warning
+ {
+ switch (data[0])
+ {
+ case 0x0d: //CR
+ process_cr (decoder);
+ break;
+ case 0x0e: // HCR (Horizontal Carriage Return)
+ //! @todo Process HDR
+ break;
+ case 0x0c: // FF (Form Feed)
+ //! @todo Process FF
+ break;
+ }
+ len=1;
+ }
+ else if (data[0]>=0x10 && data[0]<=0x17)
+ {
+ // Note that 0x10 is actually EXT1 and is dealt with somewhere else. Rest is undefined as per
+ // CEA-708-D
+ len=2;
+ }
+ else if (data[0]>=0x18 && data[0]<=0x1F)
+ {
+ // Only PE16 is defined.
+ if (data[0]==0x18) // PE16
+ {
+ ; //! @todo Handle PE16
+ }
+ len=3;
+ }
+ if (len==-1)
+ {
+ return -1;
+ }
+ if (len>data_length)
+ {
+ return -1;
+ }
+ //! @todo Do something useful eventually
+ return len;
+}
+
+
+void process_character(cc708_service_decoder* decoder, unsigned char internal_char)
+{
+ if (decoder->current_window == -1 ||
+ !decoder->windows[decoder->current_window]
+ .is_defined) // Writing to a non existing window, skipping
+ return;
+ decoder->windows[decoder->current_window].is_empty = 0;
+ decoder->windows[decoder->current_window]
+ .rows[decoder->windows[decoder->current_window].pen_row]
+ [decoder->windows[decoder->current_window].pen_column] = internal_char;
+ /* Not positive this interpretation is correct. Word wrapping is optional, so
+ let's assume we don't need to autoscroll */
+ switch (decoder->windows[decoder->current_window].attribs.print_dir)
+ {
+ case pd_left_to_right:
+ if (decoder->windows[decoder->current_window].pen_column + 1 <
+ decoder->windows[decoder->current_window].col_count)
+ decoder->windows[decoder->current_window].pen_column++;
+ break;
+ case pd_right_to_left:
+ if (decoder->windows->pen_column > 0)
+ decoder->windows[decoder->current_window].pen_column--;
+ break;
+ case pd_top_to_bottom:
+ if (decoder->windows[decoder->current_window].pen_row + 1 <
+ decoder->windows[decoder->current_window].row_count)
+ decoder->windows[decoder->current_window].pen_row++;
+ break;
+ case pd_bottom_to_top:
+ if (decoder->windows[decoder->current_window].pen_row > 0)
+ decoder->windows[decoder->current_window].pen_row--;
+ break;
+ }
+}
+
+// G0 - Code Set - ASCII printable characters
+int handle_708_G0 (cc708_service_decoder *decoder, unsigned char *data, int data_length)
+{
+ //! @todo Substitution of the music note character for the ASCII DEL character
+ unsigned char c=get_internal_from_G0 (data[0]);
+ process_character (decoder, c);
+ return 1;
+}
+
+// G1 Code Set - ISO 8859-1 LATIN-1 Character Set
+int handle_708_G1 (cc708_service_decoder *decoder, unsigned char *data, int data_length)
+{
+ unsigned char c=get_internal_from_G1 (data[0]);
+ process_character (decoder, c);
+ return 1;
+}
+
+/*-------------------------------------------------------
+ WINDOW COMMANDS
+ ------------------------------------------------------- */
+void handle_708_CWx_SetCurrentWindow (cc708_service_decoder *decoder, int new_window)
+{
+ if (decoder->windows[new_window].is_defined)
+ decoder->current_window=new_window;
+}
+
+void clearWindowText(e708Window *window)
+{
+ for (int i = 0; i<I708_MAX_ROWS; i++)
+ {
+ memset(window->rows[i], ' ', I708_MAX_COLUMNS);
+ window->rows[i][I708_MAX_COLUMNS] = 0;
+ }
+ memset(window->rows[I708_MAX_ROWS], 0, I708_MAX_COLUMNS + 1);
+ window->is_empty = 1;
+
+}
+
+void clearWindow (cc708_service_decoder *decoder, int window)
+{
+ if (decoder->windows[window].is_defined)
+ clearWindowText(&decoder->windows[window]);
+}
+
+void handle_708_CLW_ClearWindows (cc708_service_decoder *decoder, int windows_bitmap)
+{
+ if (windows_bitmap==0)
+ ;//ccx_common_logging.debug_ftn(CCX_DMT_708, "None\n");
+ else
+ {
+ for (int i=0; i<8; i++)
+ {
+ if (windows_bitmap & 1)
+ {
+ clearWindow (decoder, i);
+ }
+ windows_bitmap>>=1;
+ }
+ }
+}
+
+void handle_708_DSW_DisplayWindows (cc708_service_decoder *decoder, int windows_bitmap)
+{
+ if (windows_bitmap==0)
+ ;//ccx_common_logging.debug_ftn(CCX_DMT_708, "None\n");
+ else
+ {
+ int changes=0;
+ for (e708Window& window : decoder->windows)
+ {
+ if (windows_bitmap & 1)
+ {
+ if (!window.visible)
+ {
+ changes=1;
+ window.visible=1;
+ }
+ }
+ windows_bitmap>>=1;
+ }
+ if (changes)
+ updateScreen (decoder);
+ }
+}
+
+void handle_708_HDW_HideWindows (cc708_service_decoder *decoder, int windows_bitmap)
+{
+ if (windows_bitmap==0)
+ ;//ccx_common_logging.debug_ftn(CCX_DMT_708, "None\n");
+ else
+ {
+ int changes=0;
+ for (e708Window& window : decoder->windows)
+ {
+ if (windows_bitmap & 1)
+ {
+ if (window.is_defined && window.visible && !window.is_empty)
+ {
+ changes=1;
+ window.visible=0;
+ }
+ //! @todo Actually Hide Window
+ }
+ windows_bitmap>>=1;
+ }
+ if (changes)
+ updateScreen (decoder);
+ }
+}
+
+void handle_708_TGW_ToggleWindows (cc708_service_decoder *decoder, int windows_bitmap)
+{
+ if (windows_bitmap==0)
+ ;//ccx_common_logging.debug_ftn(CCX_DMT_708, "None\n");
+ else
+ {
+ for (e708Window& window : decoder->windows)
+ {
+ if (windows_bitmap & 1)
+ {
+ window.visible=!window.visible;
+ }
+ windows_bitmap>>=1;
+ }
+ updateScreen(decoder);
+ }
+}
+
+void handle_708_DFx_DefineWindow (cc708_service_decoder *decoder, int window, unsigned char *data)
+{
+ if (decoder->windows[window].is_defined &&
+ memcmp (decoder->windows[window].commands, data+1, 6)==0)
+ {
+ return;
+ }
+ decoder->windows[window].number=window;
+ int priority = (data[1] ) & 0x7;
+ int col_lock = (data[1]>>3) & 0x1;
+ int row_lock = (data[1]>>4) & 0x1;
+ int visible = (data[1]>>5) & 0x1;
+ int anchor_vertical = data[2] & 0x7f;
+ int relative_pos = (data[2]>>7);
+ int anchor_horizontal = data[3];
+ int row_count = data[4] & 0xf;
+ int anchor_point = data[4]>>4;
+ int col_count = data[5] & 0x3f;
+ int pen_style = data[6] & 0x7;
+ int win_style = (data[6]>>3) & 0x7;
+ col_count++; // These increments seems to be needed but no documentation
+ row_count++; // backs it up
+
+ if (anchor_vertical > I708_SCREENGRID_ROWS)
+ anchor_vertical = I708_SCREENGRID_ROWS;
+
+ decoder->windows[window].priority=priority;
+ decoder->windows[window].col_lock=col_lock;
+ decoder->windows[window].row_lock=row_lock;
+ decoder->windows[window].visible=visible;
+ decoder->windows[window].anchor_vertical=anchor_vertical;
+ decoder->windows[window].relative_pos=relative_pos;
+ decoder->windows[window].anchor_horizontal=anchor_horizontal;
+ decoder->windows[window].row_count=row_count;
+ decoder->windows[window].anchor_point=anchor_point;
+ decoder->windows[window].col_count=col_count;
+ decoder->windows[window].pen_style=pen_style;
+ decoder->windows[window].win_style=win_style;
+ if (!decoder->windows[window].is_defined)
+ {
+ // If the window is being created, all character positions in the window
+ // are set to the fill color...
+ //! @todo COLORS
+ // ...and the pen location is set to (0,0)
+ decoder->windows[window].pen_column=0;
+ decoder->windows[window].pen_row=0;
+ if (!decoder->windows[window].memory_reserved)
+ {
+ for (int i=0;i<=I708_MAX_ROWS;i++)
+ {
+ decoder->windows[window].rows[i]=(unsigned char *) malloc (I708_MAX_COLUMNS+1);
+ if (decoder->windows[window].rows[i]==NULL) // Great
+ {
+ decoder->windows[window].is_defined=0;
+ decoder->current_window=-1;
+ for (int j=0;j<i;j++)
+ free (decoder->windows[window].rows[j]);
+ return; //! @todo Warn somehow
+ }
+ }
+ decoder->windows[window].memory_reserved=1;
+ }
+ decoder->windows[window].is_defined=1;
+ memset(&decoder->windows[window].attribs, 0, sizeof(e708Window_attribs));
+ clearWindowText (&decoder->windows[window]);
+ }
+
+ // ...also makes the defined windows the current window (setCurrentWindow)
+ handle_708_CWx_SetCurrentWindow (decoder, window);
+ memcpy (decoder->windows[window].commands, data+1, 6);
+}
+
+void handle_708_SWA_SetWindowAttributes (cc708_service_decoder *decoder, unsigned char *data)
+{
+ int fill_color = (data[1] ) & 0x3f;
+ int fill_opacity = (data[1]>>6) & 0x03;
+ int border_color = (data[2] ) & 0x3f;
+ int border_type01 = (data[2]>>6) & 0x03;
+ int justify = (data[3] ) & 0x03;
+ int scroll_dir = (data[3]>>2) & 0x03;
+ int print_dir = (data[3]>>4) & 0x03;
+ int word_wrap = (data[3]>>6) & 0x01;
+ int border_type = (data[3]>>5) | border_type01;
+ int display_eff = (data[4] ) & 0x03;
+ int effect_dir = (data[4]>>2) & 0x03;
+ int effect_speed = (data[4]>>4) & 0x0f;
+ if (decoder->current_window==-1)
+ {
+ // Can't do anything yet - we need a window to be defined first.
+ return;
+ }
+ decoder->windows[decoder->current_window].attribs.fill_color=fill_color;
+ decoder->windows[decoder->current_window].attribs.fill_opacity=fill_opacity;
+ decoder->windows[decoder->current_window].attribs.border_color=border_color;
+ decoder->windows[decoder->current_window].attribs.border_type01=border_type01;
+ decoder->windows[decoder->current_window].attribs.justify=justify;
+ decoder->windows[decoder->current_window].attribs.scroll_dir=scroll_dir;
+ decoder->windows[decoder->current_window].attribs.print_dir=print_dir;
+ decoder->windows[decoder->current_window].attribs.word_wrap=word_wrap;
+ decoder->windows[decoder->current_window].attribs.border_type=border_type;
+ decoder->windows[decoder->current_window].attribs.display_eff=display_eff;
+ decoder->windows[decoder->current_window].attribs.effect_dir=effect_dir;
+ decoder->windows[decoder->current_window].attribs.effect_speed=effect_speed;
+
+}
+
+void deleteWindow (cc708_service_decoder *decoder, int window)
+{
+ if (window==decoder->current_window)
+ {
+ // If the current window is deleted, then the decoder's current window ID
+ // is unknown and must be reinitialized with either the SetCurrentWindow
+ // or DefineWindow command.
+ decoder->current_window=-1;
+ }
+ //! @todo Do the actual deletion (remove from display if needed, etc), mark as
+ // not defined, etc
+ if (decoder->windows[window].is_defined)
+ {
+ clearWindowText(&decoder->windows[window]);
+ }
+ decoder->windows[window].is_defined=0;
+}
+
+void handle_708_DLW_DeleteWindows (cc708_service_decoder *decoder, int windows_bitmap)
+{
+ int changes=0;
+ if (windows_bitmap==0)
+ ; //ccx_common_logging.debug_ftn(CCX_DMT_708, "None\n");
+ else
+ {
+ for (int i=0; i<8; i++)
+ {
+ if (windows_bitmap & 1)
+ {
+ if (decoder->windows[i].is_defined && decoder->windows[i].visible && !decoder->windows[i].is_empty)
+ changes=1;
+ deleteWindow (decoder, i);
+ }
+ windows_bitmap>>=1;
+ }
+ }
+ if (changes)
+ updateScreen (decoder);
+
+}
+
+/*-------------------------------------------------------
+ WINDOW COMMANDS
+ ------------------------------------------------------- */
+void handle_708_SPA_SetPenAttributes (cc708_service_decoder *decoder, unsigned char *data)
+{
+ int pen_size = (data[1] ) & 0x3;
+ int offset = (data[1]>>2) & 0x3;
+ int text_tag = (data[1]>>4) & 0xf;
+ int font_tag = (data[2] ) & 0x7;
+ int edge_type = (data[2]>>3) & 0x7;
+ int underline = (data[2]>>4) & 0x1;
+ int italic = (data[2]>>5) & 0x1;
+ if (decoder->current_window==-1)
+ {
+ // Can't do anything yet - we need a window to be defined first.
+ return;
+ }
+ decoder->windows[decoder->current_window].pen.pen_size=pen_size;
+ decoder->windows[decoder->current_window].pen.offset=offset;
+ decoder->windows[decoder->current_window].pen.text_tag=text_tag;
+ decoder->windows[decoder->current_window].pen.font_tag=font_tag;
+ decoder->windows[decoder->current_window].pen.edge_type=edge_type;
+ decoder->windows[decoder->current_window].pen.underline=underline;
+ decoder->windows[decoder->current_window].pen.italic=italic;
+}
+
+void handle_708_SPC_SetPenColor (cc708_service_decoder *decoder, unsigned char *data)
+{
+ int fg_color = (data[1] ) & 0x3f;
+ int fg_opacity = (data[1]>>6) & 0x03;
+ int bg_color = (data[2] ) & 0x3f;
+ int bg_opacity = (data[2]>>6) & 0x03;
+ int edge_color = (data[3]>>6) & 0x3f;
+ if (decoder->current_window==-1)
+ {
+ // Can't do anything yet - we need a window to be defined first.
+ return;
+ }
+
+ decoder->windows[decoder->current_window].pen_color.fg_color=fg_color;
+ decoder->windows[decoder->current_window].pen_color.fg_opacity=fg_opacity;
+ decoder->windows[decoder->current_window].pen_color.bg_color=bg_color;
+ decoder->windows[decoder->current_window].pen_color.bg_opacity=bg_opacity;
+ decoder->windows[decoder->current_window].pen_color.edge_color=edge_color;
+}
+
+
+void handle_708_SPL_SetPenLocation (cc708_service_decoder *decoder, unsigned char *data)
+{
+ int row = data[1] & 0x0f;
+ int col = data[2] & 0x3f;
+ if (decoder->current_window==-1)
+ {
+ // Can't do anything yet - we need a window to be defined first.
+ return;
+ }
+ decoder->windows[decoder->current_window].pen_row=row;
+ decoder->windows[decoder->current_window].pen_column=col;
+}
+
+
+/*-------------------------------------------------------
+ SYNCHRONIZATION COMMANDS
+ ------------------------------------------------------- */
+void handle_708_DLY_Delay (cc708_service_decoder *decoder, int tenths_of_sec)
+{
+ //! @todo Probably ask for the current FTS and wait for this time before resuming -
+ // not sure it's worth it though
+}
+
+void handle_708_DLC_DelayCancel (cc708_service_decoder *decoder)
+{
+ //! @todo See above
+}
+
+// C1 Code Set - Captioning Commands Control Codes
+int handle_708_C1 (cc708_service_decoder *decoder, unsigned char *data, int data_length)
+{
+ struct S_COMMANDS_C1 com=COMMANDS_C1[data[0]-0x80];
+ if (com.length>data_length)
+ {
+ return -1;
+ }
+ switch (com.code)
+ {
+ case CW0: /* SetCurrentWindow */
+ case CW1:
+ case CW2:
+ case CW3:
+ case CW4:
+ case CW5:
+ case CW6:
+ case CW7:
+ handle_708_CWx_SetCurrentWindow (decoder, com.code-CW0); /* Window 0 to 7 */
+ break;
+ case CLW:
+ handle_708_CLW_ClearWindows (decoder, data[1]);
+ break;
+ case DSW:
+ handle_708_DSW_DisplayWindows (decoder, data[1]);
+ break;
+ case HDW:
+ handle_708_HDW_HideWindows (decoder, data[1]);
+ break;
+ case TGW:
+ handle_708_TGW_ToggleWindows (decoder, data[1]);
+ break;
+ case DLW:
+ handle_708_DLW_DeleteWindows (decoder, data[1]);
+ break;
+ case DLY:
+ handle_708_DLY_Delay (decoder, data[1]);
+ break;
+ case DLC:
+ handle_708_DLC_DelayCancel (decoder);
+ break;
+ case RST:
+ cc708_service_reset(decoder);
+ break;
+ case SPA:
+ handle_708_SPA_SetPenAttributes (decoder, data);
+ break;
+ case SPC:
+ handle_708_SPC_SetPenColor (decoder, data);
+ break;
+ case SPL:
+ handle_708_SPL_SetPenLocation (decoder, data);
+ break;
+ case RSV93:
+ case RSV94:
+ case RSV95:
+ case RSV96:
+ break;
+ case SWA:
+ handle_708_SWA_SetWindowAttributes (decoder, data);
+ break;
+ case DF0:
+ case DF1:
+ case DF2:
+ case DF3:
+ case DF4:
+ case DF5:
+ case DF6:
+ case DF7:
+ handle_708_DFx_DefineWindow (decoder, com.code-DF0, data); /* Window 0 to 7 */
+ break;
+ default:
+ break;
+ }
+
+ return com.length;
+}
+
+
+void process_service_block (cc708_service_decoder *decoder, unsigned char *data, int data_length)
+{
+ int i=0;
+ while (i<data_length)
+ {
+ int used=-1;
+ if (data[i]!=EXT1)
+ {
+ // Group C0
+ if (/* data[i]>=0x00 && */ data[i]<=0x1F) // Comment to silence warning
+ {
+ used=handle_708_C0 (decoder,data+i,data_length-i);
+ }
+ // Group G0
+ else if (data[i]>=0x20 && data[i]<=0x7F)
+ {
+ used=handle_708_G0 (decoder,data+i,data_length-i);
+ }
+ // Group C1
+ else if (data[i]>=0x80 && data[i]<=0x9F)
+ {
+ used=handle_708_C1 (decoder,data+i,data_length-i);
+ }
+ // Group C2
+ else
+ used=handle_708_G1 (decoder,data+i,data_length-i);
+ if (used==-1)
+ {
+ //! @todo Not sure if a local reset is going to be helpful here.
+ cc708_service_reset (decoder);
+ return;
+ }
+ }
+ else // Use extended set
+ {
+ used=handle_708_extended_char (decoder, data+i+1,data_length-1);
+ used++; // Since we had EXT1
+ }
+ i+=used;
+ }
+
+ // update rollup windows
+ int update = 0;
+ for (e708Window& window : decoder->windows)
+ {
+ if (window.is_defined && window.visible &&
+ (window.anchor_point == anchorpoint_bottom_left ||
+ window.anchor_point == anchorpoint_bottom_center))
+ {
+ update++;
+ break;
+ }
+ }
+ if (update)
+ {
+ updateScreen(decoder);
+ }
+}
+
+bool check_current_packet_complete (cc708_service_decoder *decoders)
+{
+ int len = decoders[0].parent->m_current_packet[0] & 0x3F; // 6 least significants bits
+ if (decoders[0].parent->m_current_packet_length == 0)
+ return false;
+
+ if (len==0) // This is well defined in EIA-708; no magic.
+ len=128;
+ else
+ len=len*2;
+
+ // Note that len here is the length including the header
+ if (decoders[0].parent->m_current_packet_length == len) // Is this possible?
+ {
+ return true;
+ }
+ return false;
+}
+
+void process_current_packet (cc708_service_decoder *decoders)
+{
+ int seq = (decoders[0].parent->m_current_packet[0] & 0xC0) >> 6; // Two most significants bits
+ int len = decoders[0].parent->m_current_packet[0] & 0x3F; // 6 least significants bits
+ if (decoders[0].parent->m_current_packet_length == 0)
+ return;
+
+ if (len==0) // This is well defined in EIA-708; no magic.
+ len=128;
+ else
+ len=len*2;
+ // Note that len here is the length including the header
+ if (decoders[0].parent->m_current_packet_length != len) // Is this possible?
+ {
+ cc708_reset(decoders);
+ return;
+ }
+ int last_seq = decoders[0].parent->m_last_seq;
+ if ((last_seq != -1) && ((last_seq+1)%4 != seq))
+ {
+ cc708_reset(decoders);
+ return;
+ }
+ decoders[0].parent->m_last_seq = seq;
+
+ unsigned char *pos = decoders[0].parent->m_current_packet + 1;
+
+ while (pos < decoders[0].parent->m_current_packet + len)
+ {
+ int service_number=(pos[0] & 0xE0)>>5; // 3 more significant bits
+ int block_length = (pos[0] & 0x1F); // 5 less significant bits
+
+ if (service_number==7) // There is an extended header
+ {
+ pos++;
+ service_number=(pos[0] & 0x3F); // 6 more significant bits
+ if (service_number<7)
+ {
+ }
+ pos = decoders[0].parent->m_current_packet + len;
+ break;
+ }
+
+ pos++; // Move to service data
+ if (service_number==0 && block_length!=0) // Illegal, but specs say what to do...
+ {
+ pos = decoders[0].parent->m_current_packet + len; // Move to end
+ break;
+ }
+
+ if (service_number>0 && decoders[service_number].inited)
+ process_service_block (&decoders[service_number], pos, block_length);
+
+ pos+=block_length; // Skip data
+ }
+
+ clear_packet(&decoders[0]);
+
+ if (pos != decoders[0].parent->m_current_packet + len) // For some reason we didn't parse the whole packet
+ {
+ cc708_reset(decoders);
+ }
+
+ if (len<128 && *pos) // Null header is mandatory if there is room
+ {
+ ;//ccx_common_logging.debug_ftn(CCX_DMT_708, "Warning: Null header expected but not found.\n");
+ }
+}
+
+void decode_708 (const unsigned char *data, int datalength, cc708_service_decoder* decoders)
+{
+ /* Note: The data has this format:
+ 1 byte for cc_valid and cc_type
+ 2 bytes for the actual data */
+ for (int i=0; i<datalength; i+=3)
+ {
+ unsigned char cc_valid=data[i] & 0x04;
+ unsigned char cc_type=data[i] & 0x03;
+
+ switch (cc_type)
+ {
+ case 0:
+ // only use 608 as fallback
+ if (!decoders[0].parent->m_seen708)
+ decode_cc(decoders[0].parent->m_cc608decoder, (const uint8_t*)data+i, 3);
+ break;
+ case 2:
+ if (cc_valid==0) // This ends the previous packet if complete
+ {
+ if (check_current_packet_complete(decoders))
+ {
+ process_current_packet(decoders);
+ }
+ }
+ else
+ {
+ if (decoders[0].parent->m_current_packet_length < 254)
+ {
+ decoders[0].parent->m_current_packet[decoders[0].parent->m_current_packet_length++]=data[i+1];
+ decoders[0].parent->m_current_packet[decoders[0].parent->m_current_packet_length++]=data[i+2];
+ }
+ }
+ break;
+ case 3:
+ process_current_packet(decoders);
+ if (cc_valid)
+ {
+ if (decoders[0].parent->m_current_packet_length < 128)
+ {
+ decoders[0].parent->m_current_packet[decoders[0].parent->m_current_packet_length++]=data[i+1];
+ decoders[0].parent->m_current_packet[decoders[0].parent->m_current_packet_length++]=data[i+2];
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+void ccx_decoders_708_init(cc708_service_decoder *decoders, void (*handler)(int service, void *userdata), void *userdata, CDecoderCC708 *parent)
+{
+ for (int i = 0; i<CCX_DECODERS_708_MAX_SERVICES; i++)
+ {
+ cc708_service_reset (&decoders[i]);
+ decoders[i].srt_counter=0;
+ decoders[i].service = i;
+ decoders[i].callback = handler;
+ decoders[i].userdata = userdata;
+ decoders[i].parent = parent;
+ }
+ decoders[0].parent->m_cc608decoder->callback = handler;
+ decoders[0].parent->m_cc608decoder->userdata = userdata;
+
+ decoders[0].parent->m_current_packet_length = 0;
+ decoders[0].parent->m_last_seq = -1;
+ decoders[0].parent->m_seen708 = false;
+ decoders[0].parent->m_seen608 = false;
+}
+
+//-----------------------------------------------------------------------------
+//
+//-----------------------------------------------------------------------------
+
+CDecoderCC708::CDecoderCC708()
+{
+ m_inited = false;
+ cc_decoder_init();
+}
+
+CDecoderCC708::~CDecoderCC708()
+{
+ delete [] m_cc708decoders;
+ cc_decoder_close(m_cc608decoder);
+}
+
+void CDecoderCC708::Init(void (*handler)(int service, void *userdata), void *userdata)
+{
+ m_cc608decoder = cc_decoder_open();
+ m_cc708decoders = new cc708_service_decoder[8];
+ ccx_decoders_708_init(m_cc708decoders, handler, userdata, this);
+}
+
+void CDecoderCC708::Decode(const unsigned char *data, int datalength)
+{
+ decode_708(data, datalength, m_cc708decoders);
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder708.h b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder708.h
new file mode 100644
index 0000000..a891012
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder708.h
@@ -0,0 +1,304 @@
+#pragma once
+
+#include <sys/stat.h>
+
+extern "C"{
+#include "cc_decoder.h"
+}
+
+#define MAX_708_PACKET_LENGTH 128
+#define CCX_DECODERS_708_MAX_SERVICES 8
+#define I708_MAX_ROWS 15
+#define I708_MAX_COLUMNS 42
+#define I708_SCREENGRID_ROWS 75
+#define I708_SCREENGRID_COLUMNS 210
+#define I708_MAX_WINDOWS 8
+
+enum COMMANDS_C0_CODES
+{
+ NUL=0,
+ ETX=3,
+ BS=8,
+ FF=0xC,
+ CR=0xD,
+ HCR=0xE,
+ EXT1=0x10,
+ P16=0x18
+};
+
+enum COMMANDS_C1_CODES
+{
+ CW0=0x80,
+ CW1=0x81,
+ CW2=0x82,
+ CW3=0x83,
+ CW4=0x84,
+ CW5=0x85,
+ CW6=0x86,
+ CW7=0x87,
+ CLW=0x88,
+ DSW=0x89,
+ HDW=0x8A,
+ TGW=0x8B,
+ DLW=0x8C,
+ DLY=0x8D,
+ DLC=0x8E,
+ RST=0x8F,
+ SPA=0x90,
+ SPC=0x91,
+ SPL=0x92,
+ RSV93=0x93,
+ RSV94=0x94,
+ RSV95=0x95,
+ RSV96=0x96,
+ SWA=0x97,
+ DF0=0x98,
+ DF1=0x99,
+ DF2=0x9A,
+ DF3=0x9B,
+ DF4=0x9C,
+ DF5=0x9D,
+ DF6=0x9E,
+ DF7=0x9F
+};
+
+struct S_COMMANDS_C1
+{
+ int code;
+ const char *name;
+ const char *description;
+ int length;
+};
+
+
+enum eWindowsAttribJustify
+{
+ left=0,
+ right=1,
+ center=2,
+ full=3
+};
+
+enum eWindowsAttribPrintDirection
+{
+ pd_left_to_right=0,
+ pd_right_to_left=1,
+ pd_top_to_bottom=2,
+ pd_bottom_to_top=3
+};
+
+enum eWindowsAttribScrollDirection
+{
+ sd_left_to_right=0,
+ sd_right_to_left=1,
+ sd_top_to_bottom=2,
+ sd_bottom_to_top=3
+};
+
+enum eWindowsAttribScrollDisplayEffect
+{
+ snap=0,
+ fade=1,
+ wipe=2
+};
+
+enum eWindowsAttribEffectDirection
+{
+ left_to_right=0,
+ right_to_left=1,
+ top_to_bottom=2,
+ bottom_to_top=3
+};
+
+enum eWindowsAttribFillOpacity
+{
+ solid=0,
+ flash=1,
+ translucent=2,
+ transparent=3
+};
+
+enum eWindowsAttribBorderType
+{
+ none=0,
+ raised=1,
+ depressed=2,
+ uniform=3,
+ shadow_left=4,
+ shadow_right=5
+};
+
+enum ePenAttribSize
+{
+ pensize_small=0,
+ pensize_standard=1,
+ pensize_large=2
+};
+
+enum ePenAttribFontStyle
+{
+ fontstyle_default_or_undefined=0,
+ monospaced_with_serifs=1,
+ proportionally_spaced_with_serifs=2,
+ monospaced_without_serifs=3,
+ proportionally_spaced_without_serifs=4,
+ casual_font_type=5,
+ cursive_font_type=6,
+ small_capitals=7
+};
+
+enum ePanAttribTextTag
+{
+ texttag_dialog=0,
+ texttag_source_or_speaker_id=1,
+ texttag_electronic_voice=2,
+ texttag_foreign_language=3,
+ texttag_voiceover=4,
+ texttag_audible_translation=5,
+ texttag_subtitle_translation=6,
+ texttag_voice_quality_description=7,
+ texttag_song_lyrics=8,
+ texttag_sound_effect_description=9,
+ texttag_musical_score_description=10,
+ texttag_expletive=11,
+ texttag_undefined_12=12,
+ texttag_undefined_13=13,
+ texttag_undefined_14=14,
+ texttag_not_to_be_displayed=15
+};
+
+enum ePanAttribOffset
+{
+ offset_subscript=0,
+ offset_normal=1,
+ offset_superscript=2
+};
+
+enum ePanAttribEdgeType
+{
+ edgetype_none=0,
+ edgetype_raised=1,
+ edgetype_depressed=2,
+ edgetype_uniform=3,
+ edgetype_left_drop_shadow=4,
+ edgetype_right_drop_shadow=5
+};
+
+enum eAnchorPoints
+{
+ anchorpoint_top_left = 0,
+ anchorpoint_top_center = 1,
+ anchorpoint_top_right =2,
+ anchorpoint_middle_left = 3,
+ anchorpoint_middle_center = 4,
+ anchorpoint_middle_right = 5,
+ anchorpoint_bottom_left = 6,
+ anchorpoint_bottom_center = 7,
+ anchorpoint_bottom_right = 8
+};
+
+typedef struct e708Pen_color
+{
+ int fg_color;
+ int fg_opacity;
+ int bg_color;
+ int bg_opacity;
+ int edge_color;
+} e708Pen_color;
+
+typedef struct e708Pen_attribs
+{
+ int pen_size;
+ int offset;
+ int text_tag;
+ int font_tag;
+ int edge_type;
+ int underline;
+ int italic;
+} e708Pen_attribs;
+
+typedef struct e708Window_attribs
+{
+ int fill_color;
+ int fill_opacity;
+ int border_color;
+ int border_type01;
+ int justify;
+ int scroll_dir;
+ int print_dir;
+ int word_wrap;
+ int border_type;
+ int display_eff;
+ int effect_dir;
+ int effect_speed;
+} e708Window_attribs;
+
+typedef struct e708Window
+{
+ int is_defined;
+ int number; // Handy, in case we only have a pointer to the window
+ int priority;
+ int col_lock;
+ int row_lock;
+ int visible;
+ int anchor_vertical;
+ int relative_pos;
+ int anchor_horizontal;
+ int row_count;
+ int anchor_point;
+ int col_count;
+ int pen_style;
+ int win_style;
+ unsigned char commands[6]; // Commands used to create this window
+ e708Window_attribs attribs;
+ e708Pen_attribs pen;
+ e708Pen_color pen_color;
+ int pen_row;
+ int pen_column;
+ unsigned char *rows[I708_MAX_ROWS+1]; // Max is 15, but we define an extra one for convenience
+ int memory_reserved;
+ int is_empty;
+} e708Window;
+
+typedef struct tvscreen
+{
+ unsigned char chars[I708_SCREENGRID_ROWS][I708_SCREENGRID_COLUMNS+1];
+}
+tvscreen;
+
+class CDecoderCC708;
+typedef struct cc708_service_decoder
+{
+ e708Window windows[I708_MAX_WINDOWS];
+ int current_window;
+ int inited;
+ int service;
+ tvscreen tv;
+ int is_empty_tv;
+ int srt_counter;
+ void *userdata;
+ void (*callback)(int service, void *userdata);
+ char text[I708_SCREENGRID_ROWS*I708_SCREENGRID_COLUMNS+1];
+ int textlen;
+ CDecoderCC708 *parent;
+}
+cc708_service_decoder;
+
+void process_character (cc708_service_decoder *decoder, unsigned char internal_char);
+
+class CDecoderCC708
+{
+public:
+ CDecoderCC708();
+ virtual ~CDecoderCC708();
+ void Init(void (*handler)(int service, void *userdata), void *userdata);
+ void Decode(const unsigned char *data, int datalength);
+ bool m_inited;
+ cc708_service_decoder* m_cc708decoders;
+ cc_decoder_t *m_cc608decoder;
+ unsigned char m_current_packet[MAX_708_PACKET_LENGTH]; // Length according to EIA-708B, part 5
+ int m_current_packet_length;
+ int m_last_seq;
+ bool m_seen708;
+ bool m_seen608;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/AddonVideoCodec.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Video/AddonVideoCodec.cpp
new file mode 100644
index 0000000..8a11885
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/AddonVideoCodec.cpp
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2017-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 "AddonVideoCodec.h"
+
+#include "addons/addoninfo/AddonInfo.h"
+#include "cores/VideoPlayer/Buffers/VideoBuffer.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDCodecs.h"
+#include "cores/VideoPlayer/DVDStreamInfo.h"
+#include "cores/VideoPlayer/Interface/DemuxCrypto.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/log.h"
+
+CAddonVideoCodec::CAddonVideoCodec(CProcessInfo& processInfo,
+ ADDON::AddonInfoPtr& addonInfo,
+ KODI_HANDLE parentInstance)
+ : CDVDVideoCodec(processInfo),
+ IAddonInstanceHandler(
+ ADDON_INSTANCE_VIDEOCODEC, addonInfo, ADDON::ADDON_INSTANCE_ID_UNUSED, parentInstance),
+ m_codecFlags(0),
+ m_displayAspect(0.0f)
+{
+ m_ifc.videocodec = new AddonInstance_VideoCodec;
+ m_ifc.videocodec->props = new AddonProps_VideoCodec();
+ m_ifc.videocodec->toAddon = new KodiToAddonFuncTable_VideoCodec();
+ m_ifc.videocodec->toKodi = new AddonToKodiFuncTable_VideoCodec();
+
+ m_ifc.videocodec->toKodi->kodiInstance = this;
+ m_ifc.videocodec->toKodi->get_frame_buffer = get_frame_buffer;
+ m_ifc.videocodec->toKodi->release_frame_buffer = release_frame_buffer;
+ if (CreateInstance() != ADDON_STATUS_OK || !m_ifc.videocodec->toAddon->open)
+ {
+ CLog::Log(LOGERROR, "CInputStreamAddon: Failed to create add-on instance for '{}'",
+ addonInfo->ID());
+ return;
+ }
+}
+
+CAddonVideoCodec::~CAddonVideoCodec()
+{
+ //free remaining buffers
+ Reset();
+
+ DestroyInstance();
+
+ // Delete "C" interface structures
+ delete m_ifc.videocodec->toAddon;
+ delete m_ifc.videocodec->toKodi;
+ delete m_ifc.videocodec->props;
+ delete m_ifc.videocodec;
+}
+
+bool CAddonVideoCodec::CopyToInitData(VIDEOCODEC_INITDATA &initData, CDVDStreamInfo &hints)
+{
+ initData.codecProfile = STREAMCODEC_PROFILE::CodecProfileNotNeeded;
+ switch (hints.codec)
+ {
+ case AV_CODEC_ID_H264:
+ initData.codec = VIDEOCODEC_H264;
+ switch (hints.profile)
+ {
+ case 0:
+ case FF_PROFILE_UNKNOWN:
+ initData.codecProfile = STREAMCODEC_PROFILE::CodecProfileUnknown;
+ break;
+ case FF_PROFILE_H264_BASELINE:
+ initData.codecProfile = STREAMCODEC_PROFILE::H264CodecProfileBaseline;
+ break;
+ case FF_PROFILE_H264_MAIN:
+ initData.codecProfile = STREAMCODEC_PROFILE::H264CodecProfileMain;
+ break;
+ case FF_PROFILE_H264_EXTENDED:
+ initData.codecProfile = STREAMCODEC_PROFILE::H264CodecProfileExtended;
+ break;
+ case FF_PROFILE_H264_HIGH:
+ initData.codecProfile = STREAMCODEC_PROFILE::H264CodecProfileHigh;
+ break;
+ case FF_PROFILE_H264_HIGH_10:
+ initData.codecProfile = STREAMCODEC_PROFILE::H264CodecProfileHigh10;
+ break;
+ case FF_PROFILE_H264_HIGH_422:
+ initData.codecProfile = STREAMCODEC_PROFILE::H264CodecProfileHigh422;
+ break;
+ case FF_PROFILE_H264_HIGH_444_PREDICTIVE:
+ initData.codecProfile = STREAMCODEC_PROFILE::H264CodecProfileHigh444Predictive;
+ break;
+ default:
+ return false;
+ }
+ break;
+ case AV_CODEC_ID_VP8:
+ initData.codec = VIDEOCODEC_VP8;
+ break;
+ case AV_CODEC_ID_VP9:
+ initData.codec = VIDEOCODEC_VP9;
+ switch (hints.profile)
+ {
+ case FF_PROFILE_UNKNOWN:
+ initData.codecProfile = STREAMCODEC_PROFILE::CodecProfileUnknown;
+ break;
+ case FF_PROFILE_VP9_0:
+ initData.codecProfile = STREAMCODEC_PROFILE::VP9CodecProfile0;
+ break;
+ case FF_PROFILE_VP9_1:
+ initData.codecProfile = STREAMCODEC_PROFILE::VP9CodecProfile1;
+ break;
+ case FF_PROFILE_VP9_2:
+ initData.codecProfile = STREAMCODEC_PROFILE::VP9CodecProfile2;
+ break;
+ case FF_PROFILE_VP9_3:
+ initData.codecProfile = STREAMCODEC_PROFILE::VP9CodecProfile3;
+ break;
+ default:
+ return false;
+ }
+ break;
+ case AV_CODEC_ID_AV1:
+ initData.codec = VIDEOCODEC_AV1;
+ switch (hints.profile)
+ {
+ case FF_PROFILE_UNKNOWN:
+ initData.codecProfile = STREAMCODEC_PROFILE::CodecProfileUnknown;
+ break;
+ case FF_PROFILE_AV1_MAIN:
+ initData.codecProfile = STREAMCODEC_PROFILE::AV1CodecProfileMain;
+ break;
+ case FF_PROFILE_AV1_HIGH:
+ initData.codecProfile = STREAMCODEC_PROFILE::AV1CodecProfileHigh;
+ break;
+ case FF_PROFILE_AV1_PROFESSIONAL:
+ initData.codecProfile = STREAMCODEC_PROFILE::AV1CodecProfileProfessional;
+ break;
+ default:
+ return false;
+ }
+ break;
+ default:
+ return false;
+ }
+ if (hints.cryptoSession)
+ {
+ switch (hints.cryptoSession->keySystem)
+ {
+ case CRYPTO_SESSION_SYSTEM_NONE:
+ initData.cryptoSession.keySystem = STREAM_CRYPTO_KEY_SYSTEM_NONE;
+ break;
+ case CRYPTO_SESSION_SYSTEM_WIDEVINE:
+ initData.cryptoSession.keySystem = STREAM_CRYPTO_KEY_SYSTEM_WIDEVINE;
+ break;
+ case CRYPTO_SESSION_SYSTEM_PLAYREADY:
+ initData.cryptoSession.keySystem = STREAM_CRYPTO_KEY_SYSTEM_PLAYREADY;
+ break;
+ case CRYPTO_SESSION_SYSTEM_WISEPLAY:
+ initData.cryptoSession.keySystem = STREAM_CRYPTO_KEY_SYSTEM_WISEPLAY;
+ break;
+ default:
+ return false;
+ }
+
+ strncpy(initData.cryptoSession.sessionId, hints.cryptoSession->sessionId.c_str(),
+ sizeof(initData.cryptoSession.sessionId) - 1);
+ }
+
+ initData.extraData = reinterpret_cast<const uint8_t*>(hints.extradata);
+ initData.extraDataSize = hints.extrasize;
+ initData.width = hints.width;
+ initData.height = hints.height;
+ initData.videoFormats = m_formats;
+
+ m_displayAspect = (hints.aspect > 0.0 && !hints.forced_aspect) ? static_cast<float>(hints.aspect) : 0.0f;
+ m_width = hints.width;
+ m_height = hints.height;
+
+ m_processInfo.SetVideoDimensions(hints.width, hints.height);
+ m_processInfo.SetVideoDAR(m_displayAspect);
+ if (hints.fpsscale)
+ m_processInfo.SetVideoFps(static_cast<float>(hints.fpsrate) / hints.fpsscale);
+
+ return true;
+}
+
+bool CAddonVideoCodec::Open(CDVDStreamInfo &hints, CDVDCodecOptions &options)
+{
+ if (!m_ifc.videocodec->toAddon->open)
+ return false;
+
+ unsigned int nformats(0);
+ m_formats[nformats++] = VIDEOCODEC_FORMAT_YV12;
+ m_formats[nformats] = VIDEOCODEC_FORMAT_UNKNOWN;
+
+ VIDEOCODEC_INITDATA initData;
+ if (!CopyToInitData(initData, hints))
+ return false;
+
+ bool ret = m_ifc.videocodec->toAddon->open(m_ifc.videocodec, &initData);
+ m_processInfo.SetVideoDecoderName(GetName(), false);
+
+ return ret;
+}
+
+bool CAddonVideoCodec::Reconfigure(CDVDStreamInfo &hints)
+{
+ if (!m_ifc.videocodec->toAddon->reconfigure)
+ return false;
+
+ VIDEOCODEC_INITDATA initData;
+ if (!CopyToInitData(initData, hints))
+ return false;
+
+ return m_ifc.videocodec->toAddon->reconfigure(m_ifc.videocodec, &initData);
+}
+
+bool CAddonVideoCodec::AddData(const DemuxPacket &packet)
+{
+ if (!m_ifc.videocodec->toAddon->add_data)
+ return false;
+
+ return m_ifc.videocodec->toAddon->add_data(m_ifc.videocodec, &packet);
+}
+
+CDVDVideoCodec::VCReturn CAddonVideoCodec::GetPicture(VideoPicture* pVideoPicture)
+{
+ if (!m_ifc.videocodec->toAddon->get_picture)
+ return CDVDVideoCodec::VC_ERROR;
+
+ VIDEOCODEC_PICTURE picture;
+ picture.flags = (m_codecFlags & DVD_CODEC_CTRL_DRAIN) ? VIDEOCODEC_PICTURE_FLAG_DRAIN
+ : VIDEOCODEC_PICTURE_FLAG_DROP;
+
+ switch (m_ifc.videocodec->toAddon->get_picture(m_ifc.videocodec, &picture))
+ {
+ case VIDEOCODEC_RETVAL::VC_NONE:
+ return CDVDVideoCodec::VC_NONE;
+ case VIDEOCODEC_RETVAL::VC_ERROR:
+ return CDVDVideoCodec::VC_ERROR;
+ case VIDEOCODEC_RETVAL::VC_BUFFER:
+ return CDVDVideoCodec::VC_BUFFER;
+ case VIDEOCODEC_RETVAL::VC_PICTURE:
+ pVideoPicture->iWidth = picture.width;
+ pVideoPicture->iHeight = picture.height;
+ pVideoPicture->pts = static_cast<double>(picture.pts);
+ pVideoPicture->dts = DVD_NOPTS_VALUE;
+ pVideoPicture->iFlags = 0;
+ pVideoPicture->chroma_position = 0;
+ pVideoPicture->colorBits = 8;
+ pVideoPicture->color_primaries = AVColorPrimaries::AVCOL_PRI_UNSPECIFIED;
+ pVideoPicture->color_range = 0;
+ pVideoPicture->color_space = AVCOL_SPC_UNSPECIFIED;
+ pVideoPicture->color_transfer = 0;
+ pVideoPicture->hasDisplayMetadata = false;
+ pVideoPicture->hasLightMetadata = false;
+ pVideoPicture->iDuration = 0;
+ pVideoPicture->iFrameType = 0;
+ pVideoPicture->iRepeatPicture = 0;
+ pVideoPicture->pict_type = 0;
+ pVideoPicture->qp_table = nullptr;
+ pVideoPicture->qscale_type = 0;
+ pVideoPicture->qstride = 0;
+ pVideoPicture->stereoMode.clear();
+
+ if (m_codecFlags & DVD_CODEC_CTRL_DROP)
+ pVideoPicture->iFlags |= DVP_FLAG_DROPPED;
+
+ if (pVideoPicture->videoBuffer)
+ pVideoPicture->videoBuffer->Release();
+
+ pVideoPicture->videoBuffer = static_cast<CVideoBuffer*>(picture.videoBufferHandle);
+
+ int strides[YuvImage::MAX_PLANES], planeOffsets[YuvImage::MAX_PLANES];
+ for (int i = 0; i<YuvImage::MAX_PLANES; ++i)
+ strides[i] = picture.stride[i];
+ for (int i = 0; i<YuvImage::MAX_PLANES; ++i)
+ planeOffsets[i] = picture.planeOffsets[i];
+
+ pVideoPicture->videoBuffer->SetDimensions(picture.width, picture.height, strides, planeOffsets);
+
+ pVideoPicture->iDisplayWidth = pVideoPicture->iWidth;
+ pVideoPicture->iDisplayHeight = pVideoPicture->iHeight;
+ if (m_displayAspect > 0.0f)
+ {
+ pVideoPicture->iDisplayWidth = ((int)lrint(pVideoPicture->iHeight * m_displayAspect)) & ~3;
+ if (pVideoPicture->iDisplayWidth > pVideoPicture->iWidth)
+ {
+ pVideoPicture->iDisplayWidth = pVideoPicture->iWidth;
+ pVideoPicture->iDisplayHeight = ((int)lrint(pVideoPicture->iWidth / m_displayAspect)) & ~3;
+ }
+ }
+
+ CLog::Log(LOGDEBUG, LOGVIDEO,
+ "CAddonVideoCodec: GetPicture::VC_PICTURE with pts {} {}x{} ({}x{}) {} {}:{} "
+ "offset:{},{},{}, stride:{},{},{}",
+ picture.pts, pVideoPicture->iWidth, pVideoPicture->iHeight,
+ pVideoPicture->iDisplayWidth, pVideoPicture->iDisplayHeight, m_displayAspect,
+ fmt::ptr(picture.decodedData), picture.decodedDataSize, picture.planeOffsets[0],
+ picture.planeOffsets[1], picture.planeOffsets[2], picture.stride[0],
+ picture.stride[1], picture.stride[2]);
+
+ if (picture.width != m_width || picture.height != m_height)
+ {
+ m_width = picture.width;
+ m_height = picture.height;
+ m_processInfo.SetVideoDimensions(m_width, m_height);
+ }
+
+ return CDVDVideoCodec::VC_PICTURE;
+ case VIDEOCODEC_RETVAL::VC_EOF:
+ CLog::Log(LOGINFO, "CAddonVideoCodec: GetPicture: EOF");
+ return CDVDVideoCodec::VC_EOF;
+ default:
+ return CDVDVideoCodec::VC_ERROR;
+ }
+}
+
+const char* CAddonVideoCodec::GetName()
+{
+ if (m_ifc.videocodec->toAddon->get_name)
+ return m_ifc.videocodec->toAddon->get_name(m_ifc.videocodec);
+ return "";
+}
+
+void CAddonVideoCodec::Reset()
+{
+ CVideoBuffer *videoBuffer;
+
+ CLog::Log(LOGDEBUG, "CAddonVideoCodec: Reset");
+
+ // Get the remaining pictures out of the external decoder
+ VIDEOCODEC_PICTURE picture;
+ picture.flags = VIDEOCODEC_PICTURE_FLAG_DRAIN;
+
+ VIDEOCODEC_RETVAL ret;
+ while ((ret = m_ifc.videocodec->toAddon->get_picture(m_ifc.videocodec, &picture)) !=
+ VIDEOCODEC_RETVAL::VC_EOF)
+ {
+ if (ret == VIDEOCODEC_RETVAL::VC_PICTURE)
+ {
+ videoBuffer = static_cast<CVideoBuffer*>(picture.videoBufferHandle);
+ if (videoBuffer)
+ videoBuffer->Release();
+ }
+ }
+ if (m_ifc.videocodec->toAddon->reset)
+ m_ifc.videocodec->toAddon->reset(m_ifc.videocodec);
+}
+
+bool CAddonVideoCodec::GetFrameBuffer(VIDEOCODEC_PICTURE &picture)
+{
+ CVideoBuffer *videoBuffer = m_processInfo.GetVideoBufferManager().Get(AV_PIX_FMT_YUV420P, picture.decodedDataSize, nullptr);
+ if (!videoBuffer)
+ {
+ CLog::Log(LOGERROR,"CAddonVideoCodec::GetFrameBuffer Failed to allocate buffer");
+ return false;
+ }
+ picture.decodedData = videoBuffer->GetMemPtr();
+ picture.videoBufferHandle = videoBuffer;
+
+ return true;
+}
+
+void CAddonVideoCodec::ReleaseFrameBuffer(KODI_HANDLE videoBufferHandle)
+{
+ if (videoBufferHandle)
+ static_cast<CVideoBuffer*>(videoBufferHandle)->Release();
+}
+
+/********************* ADDON-TO-KODI **********************/
+
+bool CAddonVideoCodec::get_frame_buffer(void* kodiInstance, VIDEOCODEC_PICTURE *picture)
+{
+ if (!kodiInstance)
+ return false;
+
+ return static_cast<CAddonVideoCodec*>(kodiInstance)->GetFrameBuffer(*picture);
+}
+
+void CAddonVideoCodec::release_frame_buffer(void* kodiInstance, KODI_HANDLE videoBufferHandle)
+{
+ if (!kodiInstance)
+ return;
+
+ static_cast<CAddonVideoCodec*>(kodiInstance)->ReleaseFrameBuffer(videoBufferHandle);
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/AddonVideoCodec.h b/xbmc/cores/VideoPlayer/DVDCodecs/Video/AddonVideoCodec.h
new file mode 100644
index 0000000..8e3015c
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/AddonVideoCodec.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017-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 "DVDVideoCodec.h"
+#include "addons/AddonProvider.h"
+#include "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/VideoCodec.h"
+
+class BufferPool;
+
+class CAddonVideoCodec
+ : public CDVDVideoCodec
+ , public ADDON::IAddonInstanceHandler
+{
+public:
+ CAddonVideoCodec(CProcessInfo& processInfo,
+ ADDON::AddonInfoPtr& addonInfo,
+ KODI_HANDLE parentInstance);
+ ~CAddonVideoCodec() override;
+
+ bool Open(CDVDStreamInfo &hints, CDVDCodecOptions &options) override;
+ bool Reconfigure(CDVDStreamInfo &hints) override;
+ bool AddData(const DemuxPacket &packet) override;
+ void Reset() override;
+ VCReturn GetPicture(VideoPicture* pVideoPicture) override;
+ const char* GetName() override;
+ void SetCodecControl(int flags) override { m_codecFlags = flags; }
+
+private:
+ bool CopyToInitData(VIDEOCODEC_INITDATA &initData, CDVDStreamInfo &hints);
+
+ /*!
+ * @brief All picture members can be expected to be set correctly except decodedData and pts.
+ * GetFrameBuffer has to set decodedData to a valid memory address and return true.
+ * In case buffer allocation fails, return false.
+ */
+ bool GetFrameBuffer(VIDEOCODEC_PICTURE &picture);
+ void ReleaseFrameBuffer(KODI_HANDLE videoBufferHandle);
+
+ static bool get_frame_buffer(void* kodiInstance, VIDEOCODEC_PICTURE *picture);
+ static void release_frame_buffer(void* kodiInstance, KODI_HANDLE videoBufferHandle);
+
+ int m_codecFlags;
+ VIDEOCODEC_FORMAT m_formats[VIDEOCODEC_FORMAT_MAXFORMATS + 1];
+ float m_displayAspect;
+ unsigned int m_width, m_height;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/CMakeLists.txt b/xbmc/cores/VideoPlayer/DVDCodecs/Video/CMakeLists.txt
new file mode 100644
index 0000000..517acc3
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/CMakeLists.txt
@@ -0,0 +1,44 @@
+set(SOURCES AddonVideoCodec.cpp
+ DVDVideoCodec.cpp
+ DVDVideoCodecFFmpeg.cpp)
+
+set(HEADERS AddonVideoCodec.h
+ DVDVideoCodec.h
+ DVDVideoCodecFFmpeg.h)
+
+if(NOT ENABLE_EXTERNAL_LIBAV)
+ list(APPEND SOURCES DVDVideoPPFFmpeg.cpp)
+ list(APPEND HEADERS DVDVideoPPFFmpeg.h)
+endif()
+
+if(CORE_SYSTEM_NAME STREQUAL windows OR CORE_SYSTEM_NAME STREQUAL windowsstore)
+ list(APPEND SOURCES DXVA.cpp)
+ list(APPEND HEADERS DXVA.h)
+endif()
+
+if(VDPAU_FOUND)
+ list(APPEND SOURCES VDPAU.cpp)
+ list(APPEND HEADERS VDPAU.h)
+endif()
+
+if(VAAPI_FOUND)
+ list(APPEND SOURCES VAAPI.cpp)
+ list(APPEND HEADERS VAAPI.h)
+endif()
+
+if(APPLE)
+ list(APPEND SOURCES VTB.cpp)
+ list(APPEND HEADERS VTB.h)
+endif()
+
+if(CORE_SYSTEM_NAME STREQUAL android)
+ list(APPEND SOURCES DVDVideoCodecAndroidMediaCodec.cpp)
+ list(APPEND HEADERS DVDVideoCodecAndroidMediaCodec.h)
+endif()
+
+if("gbm" IN_LIST CORE_PLATFORM_NAME_LC OR "wayland" IN_LIST CORE_PLATFORM_NAME_LC)
+ list(APPEND SOURCES DVDVideoCodecDRMPRIME.cpp)
+ list(APPEND HEADERS DVDVideoCodecDRMPRIME.h)
+endif()
+
+core_add_library(dvdvideocodecs)
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.cpp
new file mode 100644
index 0000000..5e02594
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.cpp
@@ -0,0 +1,83 @@
+/*
+ * 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 "DVDVideoCodec.h"
+
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+
+#include <string>
+#include <vector>
+
+//******************************************************************************
+// VideoPicture
+//******************************************************************************
+
+VideoPicture::VideoPicture() = default;
+
+VideoPicture::~VideoPicture()
+{
+ if (videoBuffer)
+ {
+ videoBuffer->Release();
+ }
+}
+
+void VideoPicture::Reset()
+{
+ if (videoBuffer)
+ videoBuffer->Release();
+ videoBuffer = nullptr;
+ pts = DVD_NOPTS_VALUE;
+ dts = DVD_NOPTS_VALUE;
+ iFlags = 0;
+ iRepeatPicture = 0;
+ iDuration = 0;
+ iFrameType = 0;
+ color_space = AVCOL_SPC_UNSPECIFIED;
+ color_range = 0;
+ chroma_position = 0;
+ color_primaries = AVColorPrimaries::AVCOL_PRI_UNSPECIFIED;
+ color_transfer = 0;
+ colorBits = 8;
+ stereoMode.clear();
+
+ qp_table = nullptr;
+ qstride = 0;
+ qscale_type = 0;
+ pict_type = 0;
+
+ hasDisplayMetadata = false;
+ hasLightMetadata = false;
+
+ iWidth = 0;
+ iHeight = 0;
+ iDisplayWidth = 0;
+ iDisplayHeight = 0;
+}
+
+VideoPicture& VideoPicture::CopyRef(const VideoPicture &pic)
+{
+ if (videoBuffer)
+ videoBuffer->Release();
+ *this = pic;
+ if (videoBuffer)
+ videoBuffer->Acquire();
+ return *this;
+}
+
+VideoPicture& VideoPicture::SetParams(const VideoPicture &pic)
+{
+ if (videoBuffer)
+ videoBuffer->Release();
+ *this = pic;
+ videoBuffer = nullptr;
+ return *this;
+}
+
+VideoPicture::VideoPicture(VideoPicture const&) = default;
+VideoPicture& VideoPicture::operator=(VideoPicture const&) = default;
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h
new file mode 100644
index 0000000..b23fd48
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h
@@ -0,0 +1,267 @@
+/*
+ * 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 "DVDResource.h"
+#include "cores/VideoPlayer/Buffers/VideoBuffer.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavutil/mastering_display_metadata.h>
+}
+
+#include <vector>
+#include <string>
+#include <map>
+
+class CSetting;
+
+// when modifying these structures, make sure you update all codecs accordingly
+#define FRAME_TYPE_UNDEF 0
+#define FRAME_TYPE_I 1
+#define FRAME_TYPE_P 2
+#define FRAME_TYPE_B 3
+#define FRAME_TYPE_D 4
+
+
+// should be entirely filled by all codecs
+struct VideoPicture
+{
+public:
+ VideoPicture();
+ ~VideoPicture();
+ VideoPicture& CopyRef(const VideoPicture &pic);
+ VideoPicture& SetParams(const VideoPicture &pic);
+ void Reset(); // reinitialize members, videoBuffer will be released if set!
+
+ CVideoBuffer *videoBuffer = nullptr;
+
+ double pts; // timestamp in seconds, used in the CVideoPlayer class to keep track of pts
+ double dts;
+ unsigned int iFlags;
+ double iRepeatPicture;
+ double iDuration;
+ unsigned int iFrameType : 4; //< see defines above // 1->I, 2->P, 3->B, 0->Undef
+ unsigned int color_space;
+ unsigned int color_range : 1; //< 1 indicate if we have a full range of color
+ unsigned int chroma_position;
+ unsigned int color_primaries;
+ unsigned int color_transfer;
+ unsigned int colorBits = 8;
+ std::string stereoMode;
+
+ int8_t* qp_table; //< Quantization parameters, primarily used by filters
+ int qstride;
+ int qscale_type;
+ int pict_type;
+
+ bool hasDisplayMetadata = false;
+ AVMasteringDisplayMetadata displayMetadata;
+ bool hasLightMetadata = false;
+ AVContentLightMetadata lightMetadata;
+
+ AVPixelFormat pixelFormat; //< source pixel format
+
+ unsigned int iWidth;
+ unsigned int iHeight;
+ unsigned int iDisplayWidth; //< width of the picture without black bars
+ unsigned int iDisplayHeight; //< height of the picture without black bars
+
+private:
+ VideoPicture(VideoPicture const&);
+ VideoPicture& operator=(VideoPicture const&);
+};
+
+#define DVP_FLAG_TOP_FIELD_FIRST 0x00000001
+#define DVP_FLAG_REPEAT_TOP_FIELD 0x00000002 //< Set to indicate that the top field should be repeated
+#define DVP_FLAG_INTERLACED 0x00000008 //< Set to indicate that this frame is interlaced
+#define DVP_FLAG_DROPPED 0x00000010 //< indicate that this picture has been dropped in decoder stage, will have no data
+
+#define DVD_CODEC_CTRL_SKIPDEINT 0x01000000 //< request to skip a deinterlacing cycle, if possible
+#define DVD_CODEC_CTRL_NO_POSTPROC 0x02000000 //< see GetCodecStats
+#define DVD_CODEC_CTRL_HURRY 0x04000000 //< see GetCodecStats
+#define DVD_CODEC_CTRL_DROP 0x08000000 //< drop in decoder or set DVP_FLAG_DROPPED, no render of frame
+#define DVD_CODEC_CTRL_DROP_ANY 0x10000000 //< drop some non-reference frame
+#define DVD_CODEC_CTRL_DRAIN 0x20000000 //< squeeze out pictured without feeding new packets
+#define DVD_CODEC_CTRL_ROTATE 0x40000000 //< rotate if renderer does not support it
+
+// DVP_FLAG 0x00000100 - 0x00000f00 is in use by libmpeg2!
+
+#define DVP_QSCALE_UNKNOWN 0
+#define DVP_QSCALE_MPEG1 1
+#define DVP_QSCALE_MPEG2 2
+#define DVP_QSCALE_H264 3
+
+class CDVDStreamInfo;
+class CDVDCodecOption;
+class CDVDCodecOptions;
+
+class CDVDVideoCodec
+{
+public:
+
+ enum VCReturn
+ {
+ VC_NONE = 0,
+ VC_ERROR, //< an error occurred, no other messages will be returned
+ VC_FATAL, //< non recoverable error
+ VC_BUFFER, //< the decoder needs more data
+ VC_PICTURE, //< the decoder got a picture, call Decode(NULL, 0) again to parse the rest of the data
+ VC_FLUSHED, //< the decoder lost it's state, we need to restart decoding again
+ VC_NOBUFFER, //< last FFmpeg GetBuffer failed
+ VC_REOPEN, //< decoder request to re-open
+ VC_EOF //< EOF
+ };
+
+ explicit CDVDVideoCodec(CProcessInfo &processInfo) : m_processInfo(processInfo) {}
+ virtual ~CDVDVideoCodec() = default;
+
+ /**
+ * Open the decoder, returns true on success
+ * Decoders not capable of running multiple instances should return false in case
+ * there is already a instance open
+ */
+ virtual bool Open(CDVDStreamInfo &hints, CDVDCodecOptions &options) = 0;
+
+ /**
+ * Reconfigure the decoder, returns true on success
+ * Decoders not capable of running multiple instances may be capable of reconfiguring
+ * the running instance. If Reconfigure returns false, player will close / open
+ * the decoder
+ */
+ virtual bool Reconfigure(CDVDStreamInfo &hints)
+ {
+ return false;
+ }
+
+ /**
+ * add data, decoder has to consume the entire packet
+ * returns true if the packet was consumed or if resubmitting it is useless
+ */
+ virtual bool AddData(const DemuxPacket &packet) = 0;
+
+ /**
+ * Reset the decoder.
+ * Should be the same as calling Dispose and Open after each other
+ */
+ virtual void Reset() = 0;
+
+ /**
+ * GetPicture controls decoding. Player calls it on every cycle
+ * it can signal a picture, request a buffer, or return none, if nothing applies
+ * the data is valid until the next GetPicture return VC_PICTURE
+ */
+ virtual VCReturn GetPicture(VideoPicture* pVideoPicture) = 0;
+
+ /**
+ * will be called by video player indicating the playback speed. see DVD_PLAYSPEED_NORMAL,
+ * DVD_PLAYSPEED_PAUSE and friends.
+ */
+ virtual void SetSpeed(int iSpeed) {}
+
+ /**
+ * should return codecs name
+ */
+ virtual const char* GetName() = 0;
+
+ /**
+ * How many packets should player remember, so codec can recover should
+ * something cause it to flush outside of players control
+ */
+ virtual unsigned GetConvergeCount()
+ {
+ return 0;
+ }
+
+ /**
+ * Number of references to old pictures that are allowed to be retained when
+ * calling decode on the next demux packet
+ */
+ virtual unsigned GetAllowedReferences() { return 0; }
+
+ /**
+ * For calculation of dropping requirements player asks for some information.
+ * - pts : right after decoder, used to detect gaps (dropped frames in decoder)
+ * - droppedFrames : indicates if decoder has dropped a frame
+ * -1 means that decoder has no info on this.
+ * - skippedPics : indicates if postproc has skipped a already decoded picture
+ * -1 means that decoder has no info on this.
+ *
+ * If codec does not implement this method, pts of decoded frame at input
+ * video player is used. In case decoder does post-proc and de-interlacing there
+ * may be quite some frames queued up between exit decoder and entry player.
+ */
+ virtual bool GetCodecStats(double &pts, int &droppedFrames, int &skippedPics)
+ {
+ droppedFrames = -1;
+ skippedPics = -1;
+ return false;
+ }
+
+ /**
+ * Codec can be informed by player with the following flags:
+ *
+ * DVD_CODEC_CTRL_NO_POSTPROC :
+ * if speed is not normal the codec can switch off
+ * postprocessing and de-interlacing
+ *
+ * DVD_CODEC_CTRL_HURRY :
+ * codecs may do postprocessing and de-interlacing.
+ * If video buffers in RenderManager are about to run dry,
+ * this is signaled to codec. Codec can wait for post-proc
+ * to be finished instead of returning empty and getting another
+ * packet.
+ *
+ * DVD_CODEC_CTRL_DRAIN :
+ * instruct decoder to deliver last pictures without requesting
+ * new packets
+ *
+ * DVD_CODEC_CTRL_DROP :
+ * this packet is going to be dropped. decoder is free to use it
+ * for decoding
+ *
+ */
+ virtual void SetCodecControl(int flags) {}
+
+ /**
+ * Re-open the decoder.
+ * Decoder request to re-open
+ */
+ virtual void Reopen() {}
+
+protected:
+ CProcessInfo &m_processInfo;
+};
+
+// callback interface for ffmpeg hw accelerators
+class IHardwareDecoder : public IDVDResourceCounted<IHardwareDecoder>
+{
+public:
+ IHardwareDecoder() = default;
+ ~IHardwareDecoder() override = default;
+ virtual bool Open(AVCodecContext* avctx, AVCodecContext* mainctx, const enum AVPixelFormat) = 0;
+ virtual CDVDVideoCodec::VCReturn Decode(AVCodecContext* avctx, AVFrame* frame) = 0;
+ virtual bool GetPicture(AVCodecContext* avctx, VideoPicture* picture) = 0;
+ virtual CDVDVideoCodec::VCReturn Check(AVCodecContext* avctx) = 0;
+ virtual void Reset() {}
+ virtual unsigned GetAllowedReferences() { return 0; }
+ virtual bool CanSkipDeint() {return false; }
+ virtual const std::string Name() = 0;
+ virtual void SetCodecControl(int flags) {}
+};
+
+class ICallbackHWAccel
+{
+public:
+ virtual ~ICallbackHWAccel() = default;
+ virtual IHardwareDecoder* GetHWAccel() = 0;
+ virtual bool GetPictureCommon(VideoPicture* pVideoPicture) = 0;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.cpp
new file mode 100644
index 0000000..48050cf
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.cpp
@@ -0,0 +1,1745 @@
+/*
+ * Copyright (C) 2013-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.
+ */
+
+// http://developer.android.com/reference/android/media/MediaCodec.html
+//
+// Android MediaCodec class can be used to access low-level media codec,
+// i.e. encoder/decoder components. (android.media.MediaCodec). Requires SDK21+
+//
+
+#include "DVDVideoCodecAndroidMediaCodec.h"
+
+#include "DVDCodecs/DVDFactoryCodec.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/Buffers/VideoBuffer.h"
+#include "cores/VideoPlayer/Interface/DemuxCrypto.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFlags.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderManager.h"
+#include "media/decoderfilter/DecoderFilterManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/BitstreamConverter.h"
+#include "utils/BitstreamWriter.h"
+#include "utils/CPUInfo.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/android/AndroidUtils.h"
+
+#include "platform/android/activity/JNIXBMCSurfaceTextureOnFrameAvailableListener.h"
+#include "platform/android/activity/XBMCApp.h"
+
+#include <cassert>
+#include <memory>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+#include <androidjni/ByteBuffer.h>
+#include <androidjni/MediaCodec.h>
+#include <androidjni/MediaCodecBufferInfo.h>
+#include <androidjni/MediaCodecCryptoInfo.h>
+#include <androidjni/MediaCodecInfo.h>
+#include <androidjni/MediaCodecList.h>
+#include <androidjni/MediaCrypto.h>
+#include <androidjni/Surface.h>
+#include <androidjni/SurfaceTexture.h>
+#include <androidjni/UUID.h>
+
+using namespace KODI::MESSAGING;
+
+enum MEDIACODEC_STATES
+{
+ MEDIACODEC_STATE_UNINITIALIZED,
+ MEDIACODEC_STATE_CONFIGURED,
+ MEDIACODEC_STATE_FLUSHED,
+ MEDIACODEC_STATE_RUNNING,
+ MEDIACODEC_STATE_WAIT_ENDOFSTREAM,
+ MEDIACODEC_STATE_ENDOFSTREAM,
+ MEDIACODEC_STATE_ERROR,
+ MEDIACODEC_STATE_STOPPED
+};
+
+static bool IsSupportedColorFormat(int color_format)
+{
+ static const int supported_colorformats[] = {
+ CJNIMediaCodecInfoCodecCapabilities::COLOR_FormatYUV420Planar,
+ CJNIMediaCodecInfoCodecCapabilities::COLOR_TI_FormatYUV420PackedSemiPlanar,
+ CJNIMediaCodecInfoCodecCapabilities::COLOR_FormatYUV420SemiPlanar,
+ CJNIMediaCodecInfoCodecCapabilities::COLOR_QCOM_FormatYUV420SemiPlanar,
+ CJNIMediaCodecInfoCodecCapabilities::OMX_QCOM_COLOR_FormatYVU420SemiPlanarInterlace,
+ -1
+ };
+ for (const int *ptr = supported_colorformats; *ptr != -1; ptr++)
+ {
+ if (color_format == *ptr)
+ return true;
+ }
+ return false;
+}
+
+/*****************************************************************************/
+/*****************************************************************************/
+class CDVDMediaCodecOnFrameAvailable : public CEvent, public CJNIXBMCSurfaceTextureOnFrameAvailableListener
+{
+public:
+ CDVDMediaCodecOnFrameAvailable(std::shared_ptr<CJNISurfaceTexture> &surfaceTexture)
+ : CJNIXBMCSurfaceTextureOnFrameAvailableListener()
+ , m_surfaceTexture(surfaceTexture)
+ {
+ m_surfaceTexture->setOnFrameAvailableListener(*this);
+ }
+
+ ~CDVDMediaCodecOnFrameAvailable() override
+ {
+ // unhook the callback
+ CJNIXBMCSurfaceTextureOnFrameAvailableListener nullListener(jni::jhobject(NULL));
+ m_surfaceTexture->setOnFrameAvailableListener(nullListener);
+ }
+
+protected:
+ void onFrameAvailable(CJNISurfaceTexture) override { Set(); }
+
+private:
+ std::shared_ptr<CJNISurfaceTexture> m_surfaceTexture;
+};
+
+/*****************************************************************************/
+/*****************************************************************************/
+void CMediaCodecVideoBuffer::Set(int bufferId, int textureId,
+ std::shared_ptr<CJNISurfaceTexture> surfacetexture,
+ std::shared_ptr<CDVDMediaCodecOnFrameAvailable> frameready,
+ std::shared_ptr<CJNIXBMCVideoView> videoview)
+{
+ m_bufferId = bufferId;
+ m_textureId = textureId;
+ m_surfacetexture = std::move(surfacetexture);
+ m_frameready = std::move(frameready);
+ m_videoview = std::move(videoview);
+}
+
+bool CMediaCodecVideoBuffer::WaitForFrame(int millis)
+{
+ return m_frameready->Wait(std::chrono::milliseconds(millis));
+}
+
+void CMediaCodecVideoBuffer::ReleaseOutputBuffer(bool render, int64_t displayTime, CMediaCodecVideoBufferPool* pool)
+{
+ std::shared_ptr<CJNIMediaCodec> codec(
+ static_cast<CMediaCodecVideoBufferPool*>(pool ? pool : m_pool.get())->GetMediaCodec());
+
+ if (m_bufferId < 0 || !codec)
+ return;
+
+ // release OutputBuffer and render if indicated
+ // then wait for rendered frame to become available.
+
+ if (render)
+ if (m_frameready)
+ m_frameready->Reset();
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGVIDEO))
+ {
+ int64_t diff = displayTime ? displayTime - CurrentHostCounter() : 0;
+ CLog::Log(
+ LOGDEBUG,
+ "CMediaCodecVideoBuffer::ReleaseOutputBuffer index({}), render({}), time:{}, offset:{}",
+ m_bufferId, render, displayTime, diff);
+ }
+
+ if (!render || displayTime == 0)
+ codec->releaseOutputBuffer(m_bufferId, render);
+ else
+ codec->releaseOutputBufferAtTime(m_bufferId, displayTime);
+ m_bufferId = -1; //mark released
+
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "CMediaCodecVideoBuffer::ReleaseOutputBuffer error in render({})", render);
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ }
+}
+
+int CMediaCodecVideoBuffer::GetBufferId() const
+{
+ // since m_texture never changes,
+ // we do not need a m_section lock here.
+ return m_bufferId;
+}
+
+int CMediaCodecVideoBuffer::GetTextureId() const
+{
+ // since m_texture never changes,
+ // we do not need a m_section lock here.
+ return m_textureId;
+}
+
+void CMediaCodecVideoBuffer::GetTransformMatrix(float *textureMatrix)
+{
+ m_surfacetexture->getTransformMatrix(textureMatrix);
+}
+
+void CMediaCodecVideoBuffer::UpdateTexImage()
+{
+ // updateTexImage will check and spew any prior gl errors,
+ // clear them before we call updateTexImage.
+ glGetError();
+
+ // this is key, after calling releaseOutputBuffer, we must
+ // wait a little for MediaCodec to render to the surface.
+ // Then we can updateTexImage without delay. If we do not
+ // wait, then video playback gets jerky. To optimize this,
+ // we hook the SurfaceTexture OnFrameAvailable callback
+ // using CJNISurfaceTextureOnFrameAvailableListener and wait
+ // on a CEvent to fire. 50ms seems to be a good max fallback.
+ WaitForFrame(50);
+
+ m_surfacetexture->updateTexImage();
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "CMediaCodecVideoBuffer::UpdateTexImage updateTexImage:ExceptionCheck");
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ }
+
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ CLog::Log(LOGERROR, "CMediaCodecVideoBuffer::UpdateTexImage getTimestamp:ExceptionCheck");
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ }
+}
+
+void CMediaCodecVideoBuffer::RenderUpdate(const CRect &DestRect, int64_t displayTime)
+{
+ CRect surfRect = m_videoview->getSurfaceRect();
+ if (DestRect != surfRect)
+ {
+ CRect adjRect = CXBMCApp::Get().MapRenderToDroid(DestRect);
+ if (adjRect != surfRect)
+ {
+ m_videoview->setSurfaceRect(adjRect);
+ CLog::Log(LOGDEBUG, LOGVIDEO,
+ "CMediaCodecVideoBuffer::RenderUpdate: Dest - {:f}+{:f}-{:f}x{:f}", DestRect.x1,
+ DestRect.y1, DestRect.Width(), DestRect.Height());
+ CLog::Log(LOGDEBUG, LOGVIDEO,
+ "CMediaCodecVideoBuffer::RenderUpdate: Adj - {:f}+{:f}-{:f}x{:f}", adjRect.x1,
+ adjRect.y1, adjRect.Width(), adjRect.Height());
+
+ // setVideoViewSurfaceRect is async, so skip rendering this frame
+ ReleaseOutputBuffer(false, 0);
+ }
+ else
+ ReleaseOutputBuffer(true, displayTime);
+ }
+ else
+ ReleaseOutputBuffer(true, displayTime);
+}
+
+/*****************************************************************************/
+/*****************************************************************************/
+CMediaCodecVideoBufferPool::~CMediaCodecVideoBufferPool()
+{
+ CLog::Log(LOGDEBUG,
+ "CMediaCodecVideoBufferPool::~CMediaCodecVideoBufferPool Releasing {} buffers",
+ static_cast<unsigned int>(m_videoBuffers.size()));
+ for (auto buffer : m_videoBuffers)
+ delete buffer;
+}
+
+CVideoBuffer* CMediaCodecVideoBufferPool::Get()
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+
+ if (m_freeBuffers.empty())
+ {
+ m_freeBuffers.push_back(m_videoBuffers.size());
+ m_videoBuffers.push_back(new CMediaCodecVideoBuffer(static_cast<int>(m_videoBuffers.size())));
+ }
+ int bufferIdx(m_freeBuffers.back());
+ m_freeBuffers.pop_back();
+
+ m_videoBuffers[bufferIdx]->Acquire(shared_from_this());
+
+ return m_videoBuffers[bufferIdx];
+}
+
+void CMediaCodecVideoBufferPool::Return(int id)
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ m_videoBuffers[id]->ReleaseOutputBuffer(false, 0, this);
+ m_freeBuffers.push_back(id);
+}
+
+std::shared_ptr<CJNIMediaCodec> CMediaCodecVideoBufferPool::GetMediaCodec()
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ return m_codec;
+}
+
+void CMediaCodecVideoBufferPool::ResetMediaCodec()
+{
+ ReleaseMediaCodecBuffers();
+
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ m_codec = nullptr;
+}
+
+void CMediaCodecVideoBufferPool::ReleaseMediaCodecBuffers()
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ for (auto buffer : m_videoBuffers)
+ buffer->ReleaseOutputBuffer(false, 0, this);
+}
+
+
+/*****************************************************************************/
+/*****************************************************************************/
+CDVDVideoCodecAndroidMediaCodec::CDVDVideoCodecAndroidMediaCodec(CProcessInfo &processInfo, bool surface_render)
+: CDVDVideoCodec(processInfo)
+, m_formatname("mediacodec")
+, m_opened(false)
+, m_jnivideoview(nullptr)
+, m_textureId(0)
+, m_OutputDuration(0)
+, m_fpsDuration(0)
+, m_lastPTS(-1)
+, m_dtsShift(DVD_NOPTS_VALUE)
+, m_bitstream(nullptr)
+, m_render_surface(surface_render)
+, m_mpeg2_sequence(nullptr)
+, m_useDTSforPTS(false)
+{
+ m_videobuffer.Reset();
+}
+
+CDVDVideoCodecAndroidMediaCodec::~CDVDVideoCodecAndroidMediaCodec()
+{
+ Dispose();
+
+ if (m_crypto)
+ {
+ delete m_crypto;
+ m_crypto = nullptr;
+ }
+ if (m_mpeg2_sequence)
+ {
+ delete (m_mpeg2_sequence);
+ m_mpeg2_sequence = nullptr;
+ }
+}
+
+std::unique_ptr<CDVDVideoCodec> CDVDVideoCodecAndroidMediaCodec::Create(CProcessInfo& processInfo)
+{
+ return std::make_unique<CDVDVideoCodecAndroidMediaCodec>(processInfo);
+}
+
+bool CDVDVideoCodecAndroidMediaCodec::Register()
+{
+ CDVDFactoryCodec::RegisterHWVideoCodec("mediacodec_dec", &CDVDVideoCodecAndroidMediaCodec::Create);
+ return true;
+}
+
+std::atomic<bool> CDVDVideoCodecAndroidMediaCodec::m_InstanceGuard(false);
+
+bool CDVDVideoCodecAndroidMediaCodec::Open(CDVDStreamInfo &hints, CDVDCodecOptions &options)
+{
+ std::vector<CJNIMediaCodecInfo> codecInfos;
+ int profile(0);
+ CJNIUUID uuid(0, 0);
+
+ m_opened = false;
+ m_needSecureDecoder = false;
+ // allow only 1 instance here
+ if (m_InstanceGuard.exchange(true))
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::Open - InstanceGuard locked");
+ return false;
+ }
+
+ // mediacodec crashes with null size. Trap this...
+ if (!hints.width || !hints.height)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::Open - {}", "null size, cannot handle");
+ goto FAIL;
+ }
+ else if (hints.orientation && m_render_surface && CJNIBase::GetSDKVersion() < 23)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::Open - {}",
+ "Surface does not support orientation before API 23");
+ goto FAIL;
+ }
+ else if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEMEDIACODEC) &&
+ !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEMEDIACODECSURFACE))
+ goto FAIL;
+
+ CLog::Log(
+ LOGDEBUG,
+ "CDVDVideoCodecAndroidMediaCodec::Open hints: Width {} x Height {}, Fpsrate {} / Fpsscale "
+ "{}, CodecID {}, Level {}, Profile {}, PTS_invalid {}, Tag {}, Extradata-Size: {}",
+ hints.width, hints.height, hints.fpsrate, hints.fpsscale, hints.codec, hints.level,
+ hints.profile, hints.ptsinvalid, hints.codec_tag, hints.extrasize);
+
+ m_render_surface = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEMEDIACODECSURFACE);
+ m_state = MEDIACODEC_STATE_UNINITIALIZED;
+ m_codecControlFlags = 0;
+ m_hints = hints;
+ m_indexInputBuffer = -1;
+ m_dtsShift = DVD_NOPTS_VALUE;
+ m_useDTSforPTS = false;
+
+ switch(m_hints.codec)
+ {
+ case AV_CODEC_ID_MPEG2VIDEO:
+ m_mime = "video/mpeg2";
+ m_mpeg2_sequence = new mpeg2_sequence;
+ m_mpeg2_sequence->width = m_hints.width;
+ m_mpeg2_sequence->height = m_hints.height;
+ m_mpeg2_sequence->ratio = m_hints.aspect;
+ m_mpeg2_sequence->fps_scale = m_hints.fpsscale;
+ m_mpeg2_sequence->fps_rate = m_hints.fpsrate;
+ m_useDTSforPTS = true;
+ m_formatname = "amc-mpeg2";
+ break;
+ case AV_CODEC_ID_MPEG4:
+ m_mime = "video/mp4v-es";
+ m_formatname = "amc-mpeg4";
+ m_useDTSforPTS = true;
+ break;
+ case AV_CODEC_ID_H263:
+ m_mime = "video/3gpp";
+ m_formatname = "amc-h263";
+ break;
+ case AV_CODEC_ID_VP6:
+ case AV_CODEC_ID_VP6F:
+ m_mime = "video/x-vnd.on2.vp6";
+ m_formatname = "amc-vp6";
+ break;
+ case AV_CODEC_ID_VP8:
+ m_mime = "video/x-vnd.on2.vp8";
+ m_formatname = "amc-vp8";
+ break;
+ case AV_CODEC_ID_VP9:
+ switch(hints.profile)
+ {
+ case FF_PROFILE_VP9_0:
+ profile = CJNIMediaCodecInfoCodecProfileLevel::VP9Profile0;
+ break;
+ case FF_PROFILE_VP9_1:
+ profile = CJNIMediaCodecInfoCodecProfileLevel::VP9Profile1;
+ break;
+ case FF_PROFILE_VP9_2:
+ profile = CJNIMediaCodecInfoCodecProfileLevel::VP9Profile2;
+ break;
+ case FF_PROFILE_VP9_3:
+ profile = CJNIMediaCodecInfoCodecProfileLevel::VP9Profile3;
+ break;
+ default:;
+ }
+ m_mime = "video/x-vnd.on2.vp9";
+ m_formatname = "amc-vp9";
+ free(m_hints.extradata);
+ m_hints.extradata = nullptr;
+ m_hints.extrasize = 0;
+ break;
+ case AV_CODEC_ID_AVS:
+ case AV_CODEC_ID_CAVS:
+ case AV_CODEC_ID_H264:
+ switch(hints.profile)
+ {
+ case FF_PROFILE_H264_HIGH_10:
+ profile = CJNIMediaCodecInfoCodecProfileLevel::AVCProfileHigh10;
+ break;
+ case FF_PROFILE_H264_HIGH_422:
+ profile = CJNIMediaCodecInfoCodecProfileLevel::AVCProfileHigh422;
+ break;
+ case FF_PROFILE_H264_HIGH_444:
+ profile = CJNIMediaCodecInfoCodecProfileLevel::AVCProfileHigh444;
+ break;
+ // All currently not supported formats
+ case FF_PROFILE_H264_HIGH_10_INTRA:
+ case FF_PROFILE_H264_HIGH_422_INTRA:
+ case FF_PROFILE_H264_HIGH_444_PREDICTIVE:
+ case FF_PROFILE_H264_HIGH_444_INTRA:
+ case FF_PROFILE_H264_CAVLC_444:
+ goto FAIL;
+ default:
+ break;
+ }
+ m_mime = "video/avc";
+ m_formatname = "amc-h264";
+ // check for h264-avcC and convert to h264-annex-b
+ if (m_hints.extradata && !m_hints.cryptoSession)
+ {
+ m_bitstream = std::make_unique<CBitstreamConverter>();
+ if (!m_bitstream->Open(m_hints.codec, (uint8_t*)m_hints.extradata, m_hints.extrasize, true))
+ {
+ m_bitstream.reset();
+ }
+ }
+ break;
+ case AV_CODEC_ID_HEVC:
+ {
+ if (m_hints.profile == FF_PROFILE_HEVC_REXT)
+ {
+ // No known h/w decoder supporting Hi10P
+ goto FAIL;
+ }
+
+ m_mime = "video/hevc";
+ m_formatname = "amc-hevc";
+
+ bool isDvhe = (m_hints.codec_tag == MKTAG('d', 'v', 'h', 'e'));
+ bool isDvh1 = (m_hints.codec_tag == MKTAG('d', 'v', 'h', '1'));
+
+ if (isDvhe || isDvh1)
+ {
+ bool displaySupportsDovi = CAndroidUtils::GetDisplayHDRCapabilities().SupportsDolbyVision();
+ bool mediaCodecSupportsDovi =
+ CAndroidUtils::SupportsMediaCodecMimeType("video/dolby-vision");
+
+ CLog::Log(LOGDEBUG,
+ "CDVDVideoCodecAndroidMediaCodec::Open Dolby Vision playback support: "
+ "Display: {}, MediaCodec: {}",
+ displaySupportsDovi, mediaCodecSupportsDovi);
+
+ if (displaySupportsDovi && mediaCodecSupportsDovi)
+ {
+ m_mime = "video/dolby-vision";
+ m_formatname = isDvhe ? "amc-dvhe" : "amc-dvh1";
+ }
+ }
+
+ // check for hevc-hvcC and convert to h265-annex-b
+ if (m_hints.extradata && !m_hints.cryptoSession)
+ {
+ m_bitstream = std::make_unique<CBitstreamConverter>();
+ if (!m_bitstream->Open(m_hints.codec, (uint8_t*)m_hints.extradata, m_hints.extrasize, true))
+ {
+ m_bitstream.reset();
+ }
+ }
+ break;
+ }
+ case AV_CODEC_ID_WMV3:
+ if (m_hints.extrasize == 4 || m_hints.extrasize == 5)
+ {
+ // Convert to SMPTE 421M-2006 Annex-L
+ static uint8_t annexL_hdr1[] = {0x8e, 0x01, 0x00, 0xc5, 0x04, 0x00, 0x00, 0x00};
+ static uint8_t annexL_hdr2[] = {0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+ free(m_hints.extradata);
+ m_hints.extrasize = 36;
+ m_hints.extradata = malloc(m_hints.extrasize);
+
+ unsigned int offset = 0;
+ char buf[4];
+ memcpy(m_hints.extradata, annexL_hdr1, sizeof(annexL_hdr1));
+ offset += sizeof(annexL_hdr1);
+ memcpy(&((char *)(m_hints.extradata))[offset], hints.extradata, 4);
+ offset += 4;
+ BS_WL32(buf, hints.height);
+ memcpy(&((char *)(m_hints.extradata))[offset], buf, 4);
+ offset += 4;
+ BS_WL32(buf, hints.width);
+ memcpy(&((char *)(m_hints.extradata))[offset], buf, 4);
+ offset += 4;
+ memcpy(&((char *)(m_hints.extradata))[offset], annexL_hdr2, sizeof(annexL_hdr2));
+ }
+
+ m_mime = "video/x-ms-wmv";
+ m_formatname = "amc-wmv";
+ break;
+ case AV_CODEC_ID_VC1:
+ {
+ if (m_hints.extrasize < 16)
+ goto FAIL;
+
+ // Reduce extradata to first SEQ header
+ unsigned int seq_offset = 0;
+ for (; seq_offset <= m_hints.extrasize-4; ++seq_offset)
+ {
+ char *ptr = &((char*)m_hints.extradata)[seq_offset];
+ if (ptr[0] == 0x00 && ptr[1] == 0x00 && ptr[2] == 0x01 && ptr[3] == 0x0f)
+ break;
+ }
+ if (seq_offset > m_hints.extrasize-4)
+ goto FAIL;
+
+ if (seq_offset)
+ {
+ free(m_hints.extradata);
+ m_hints.extrasize -= seq_offset;
+ m_hints.extradata = malloc(m_hints.extrasize);
+ memcpy(m_hints.extradata, &((char *)(hints.extradata))[seq_offset], m_hints.extrasize);
+ }
+
+ m_mime = "video/wvc1";
+ m_formatname = "amc-vc1";
+ break;
+ }
+ case AV_CODEC_ID_AV1:
+ {
+ switch (hints.profile)
+ {
+ case FF_PROFILE_AV1_MAIN:
+ profile = CJNIMediaCodecInfoCodecProfileLevel::AV1ProfileMain8;
+ break;
+ case FF_PROFILE_AV1_HIGH:
+ case FF_PROFILE_AV1_PROFESSIONAL:
+ goto FAIL;
+ break;
+ default:
+ break;
+ }
+ m_mime = "video/av01";
+ m_formatname = "amc-av1";
+ free(m_hints.extradata);
+ m_hints.extradata = nullptr;
+ m_hints.extrasize = 0;
+ break;
+ }
+ default:
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::Open Unknown hints.codec({})",
+ hints.codec);
+ goto FAIL;
+ break;
+ }
+
+ if (m_crypto)
+ {
+ delete m_crypto;
+ m_crypto = nullptr;
+ }
+
+ if (m_hints.cryptoSession)
+ {
+ if (m_hints.cryptoSession->keySystem == CRYPTO_SESSION_SYSTEM_WIDEVINE)
+ uuid = CJNIUUID(0xEDEF8BA979D64ACELL, 0xA3C827DCD51D21EDLL);
+ else if (m_hints.cryptoSession->keySystem == CRYPTO_SESSION_SYSTEM_PLAYREADY)
+ uuid = CJNIUUID(0x9A04F07998404286LL, 0xAB92E65BE0885F95LL);
+ else if (m_hints.cryptoSession->keySystem == CRYPTO_SESSION_SYSTEM_WISEPLAY)
+ uuid = CJNIUUID(0X3D5E6D359B9A41E8LL, 0XB843DD3C6E72C42CLL);
+ else
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::Open Unsupported crypto-keysystem {}",
+ m_hints.cryptoSession->keySystem);
+ goto FAIL;
+ }
+ CJNIMediaCrypto crypto(uuid, std::vector<char>(m_hints.cryptoSession->sessionId.begin(),
+ m_hints.cryptoSession->sessionId.end()));
+ m_needSecureDecoder =
+ crypto.requiresSecureDecoderComponent(m_mime) &&
+ (m_hints.cryptoSession->flags & DemuxCryptoSession::FLAG_SECURE_DECODER) != 0;
+
+ CLog::Log(
+ LOGINFO,
+ "CDVDVideoCodecAndroidMediaCodec::Open: Secure decoder requested: {} (stream flags: {})",
+ m_needSecureDecoder ? "true" : "false", m_hints.cryptoSession->flags);
+ }
+
+ m_codec = nullptr;
+ m_colorFormat = -1;
+ codecInfos = CJNIMediaCodecList(CJNIMediaCodecList::REGULAR_CODECS).getCodecInfos();
+
+ for (const CJNIMediaCodecInfo& codec_info : codecInfos)
+ {
+ if (codec_info.isEncoder())
+ continue;
+
+ m_codecname = codec_info.getName();
+ if (!CServiceBroker::GetDecoderFilterManager()->isValid(m_codecname, m_hints))
+ continue;
+
+ CLog::Log(LOGINFO, "CDVDVideoCodecAndroidMediaCodec::Open Testing codec:{}", m_codecname);
+
+ CJNIMediaCodecInfoCodecCapabilities codec_caps = codec_info.getCapabilitiesForType(m_mime);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ // Unsupported type?
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ continue;
+ }
+
+ bool codecIsSecure(
+ m_codecname.find(".secure") != std::string::npos ||
+ codec_caps.isFeatureSupported(CJNIMediaCodecInfoCodecCapabilities::FEATURE_SecurePlayback));
+ if (m_needSecureDecoder)
+ {
+ if (!codecIsSecure)
+ m_codecname += ".secure";
+ }
+ else if (codecIsSecure)
+ {
+ CLog::Log(LOGINFO, "CDVDVideoCodecAndroidMediaCodec::Open: skipping insecure decoder while "
+ "secure decoding is required");
+ continue;
+ }
+
+ std::vector<int> color_formats = codec_caps.colorFormats();
+
+ if (profile)
+ {
+ std::vector<CJNIMediaCodecInfoCodecProfileLevel> profileLevels = codec_caps.profileLevels();
+ if (std::find_if(profileLevels.cbegin(), profileLevels.cend(),
+ [&](const CJNIMediaCodecInfoCodecProfileLevel& profileLevel) {
+ return profileLevel.profile() == profile;
+ }) == profileLevels.cend())
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::Open: profile not supported: {}",
+ profile);
+ continue;
+ }
+ }
+
+ std::vector<std::string> types = codec_info.getSupportedTypes();
+ // return the 1st one we find, that one is typically 'the best'
+ for (size_t j = 0; j < types.size(); ++j)
+ {
+ if (types[j] == m_mime)
+ {
+ m_codec = std::shared_ptr<CJNIMediaCodec>(
+ new CJNIMediaCodec(CJNIMediaCodec::createByCodecName(m_codecname)));
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ m_codec = nullptr;
+ }
+ if (!m_codec)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::Open cannot create codec");
+ continue;
+ }
+
+ for (size_t k = 0; k < color_formats.size(); ++k)
+ {
+ CLog::Log(LOGDEBUG,
+ "CDVDVideoCodecAndroidMediaCodec::Open "
+ "m_codecname({}), colorFormat({})",
+ m_codecname, color_formats[k]);
+ if (IsSupportedColorFormat(color_formats[k]))
+ m_colorFormat = color_formats[k]; // Save color format for initial output configuration
+ }
+ break;
+ }
+ }
+ if (m_codec)
+ break;
+ }
+ if (!m_codec)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec:: Failed to create Android MediaCodec");
+ goto FAIL;
+ }
+
+ if (m_hints.cryptoSession)
+ {
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::Open Initializing MediaCrypto");
+
+ m_crypto =
+ new CJNIMediaCrypto(uuid, std::vector<char>(m_hints.cryptoSession->sessionId.begin(),
+ m_hints.cryptoSession->sessionId.end()));
+
+ if (!m_crypto)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::Open: MediaCrypto creation failed");
+ goto FAIL;
+ }
+ }
+
+ if (m_render_surface)
+ {
+ m_jnivideoview.reset(CJNIXBMCVideoView::createVideoView(this));
+ if (!m_jnivideoview || !m_jnivideoview->waitForSurface(2000))
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec: VideoView creation failed!!");
+ goto FAIL;
+ }
+ }
+
+ // setup a YUV420P VideoPicture buffer.
+ // first make sure all properties are reset.
+ m_videobuffer.Reset();
+
+ m_videobuffer.iWidth = m_hints.width;
+ m_videobuffer.iHeight = m_hints.height;
+ // these will get reset to crop values later
+ m_videobuffer.iDisplayWidth = m_hints.width;
+ m_videobuffer.iDisplayHeight = m_hints.height;
+
+ if (!ConfigureMediaCodec())
+ goto FAIL;
+
+ if (m_codecname.find("OMX.Nvidia", 0, 10) == 0)
+ m_invalidPTSValue = AV_NOPTS_VALUE;
+ else if (m_codecname.find("OMX.MTK", 0, 7) == 0)
+ m_invalidPTSValue = -1; //Use DTS
+ else
+ m_invalidPTSValue = 0;
+
+ CLog::Log(LOGINFO,
+ "CDVDVideoCodecAndroidMediaCodec:: "
+ "Open Android MediaCodec {}",
+ m_codecname);
+
+ m_opened = true;
+
+ m_processInfo.SetVideoDecoderName(m_formatname, true );
+ m_processInfo.SetVideoPixelFormat(m_render_surface ? "Surface" : "EGL");
+ m_processInfo.SetVideoDimensions(m_hints.width, m_hints.height);
+ m_processInfo.SetVideoDeintMethod("hardware");
+ m_processInfo.SetVideoDAR(m_hints.aspect);
+
+ m_videoBufferPool = std::shared_ptr<CMediaCodecVideoBufferPool>(new CMediaCodecVideoBufferPool(m_codec));
+
+ UpdateFpsDuration();
+
+ return true;
+
+FAIL:
+ m_InstanceGuard.exchange(false);
+ if (m_crypto)
+ {
+ delete m_crypto;
+ m_crypto = nullptr;
+ }
+
+ if (m_jnivideoview)
+ {
+ m_jnivideoview->release();
+ m_jnivideoview.reset();
+ }
+
+ m_codec = nullptr;
+
+ m_bitstream.reset();
+
+ return false;
+}
+
+void CDVDVideoCodecAndroidMediaCodec::Dispose()
+{
+ if (!m_opened)
+ return;
+
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::{}", __func__);
+
+ // invalidate any inflight outputbuffers
+ FlushInternal();
+
+ if (m_videoBufferPool)
+ {
+ m_videoBufferPool->ResetMediaCodec();
+ m_videoBufferPool = nullptr;
+ }
+
+ m_videobuffer.iFlags = 0;
+
+ if (m_codec)
+ {
+ m_codec->stop();
+ xbmc_jnienv()->ExceptionClear();
+ m_codec->release();
+ xbmc_jnienv()->ExceptionClear();
+ m_codec = nullptr;
+ m_state = MEDIACODEC_STATE_STOPPED;
+ }
+ ReleaseSurfaceTexture();
+
+ m_InstanceGuard.exchange(false);
+ if (m_render_surface)
+ {
+ m_jnivideoview->release();
+ m_jnivideoview.reset();
+ }
+
+ m_bitstream.reset();
+
+ m_opened = false;
+}
+
+bool CDVDVideoCodecAndroidMediaCodec::AddData(const DemuxPacket &packet)
+{
+ if (!m_opened || m_state == MEDIACODEC_STATE_STOPPED)
+ return false;
+
+ double pts(packet.pts), dts(packet.dts);
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGVIDEO))
+ CLog::Log(LOGDEBUG,
+ "CDVDVideoCodecAndroidMediaCodec::AddData dts:{:0.2f} pts:{:0.2f} sz:{} "
+ "indexBuffer:{} current state ({})",
+ dts, pts, packet.iSize, m_indexInputBuffer, m_state);
+ else if (m_state != MEDIACODEC_STATE_RUNNING)
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::AddData current state ({})", m_state);
+
+ if (m_hints.ptsinvalid)
+ pts = DVD_NOPTS_VALUE;
+
+ uint8_t *pData(packet.pData);
+ size_t iSize(packet.iSize);
+
+ if (m_state == MEDIACODEC_STATE_ENDOFSTREAM || m_state == MEDIACODEC_STATE_ERROR)
+ {
+ // We received a packet but already reached EOS or Error. Reset...
+ Reset();
+ }
+
+ if (pData && iSize)
+ {
+ if (m_indexInputBuffer >= 0)
+ {
+ if (!(m_state == MEDIACODEC_STATE_FLUSHED || m_state == MEDIACODEC_STATE_RUNNING))
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::AddData: Wrong state ({})", m_state);
+ return false;
+ }
+
+ if (m_mpeg2_sequence && CBitstreamConverter::mpeg2_sequence_header(pData, iSize, m_mpeg2_sequence))
+ {
+ m_hints.fpsrate = m_mpeg2_sequence->fps_rate;
+ m_hints.fpsscale = m_mpeg2_sequence->fps_scale;
+ m_hints.width = m_mpeg2_sequence->width;
+ m_hints.height = m_mpeg2_sequence->height;
+ m_hints.aspect = static_cast<double>(m_mpeg2_sequence->ratio);
+
+ m_processInfo.SetVideoDAR(m_hints.aspect);
+ UpdateFpsDuration();
+ }
+
+ // we have an input buffer, fill it.
+ if (pData && m_bitstream)
+ {
+ m_bitstream->Convert(pData, iSize);
+
+ if (m_state == MEDIACODEC_STATE_FLUSHED && !m_bitstream->CanStartDecode())
+ {
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::AddData: waiting for keyframe (bitstream)");
+ return true;
+ }
+
+ iSize = m_bitstream->GetConvertSize();
+ pData = m_bitstream->GetConvertBuffer();
+ }
+
+ if (m_state == MEDIACODEC_STATE_FLUSHED)
+ m_state = MEDIACODEC_STATE_RUNNING;
+
+ CJNIByteBuffer buffer = m_codec->getInputBuffer(m_indexInputBuffer);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::AddData: getInputBuffer failed");
+ return false;
+ }
+
+ size_t out_size = buffer.capacity();
+ if ((size_t)iSize > out_size)
+ {
+ CLog::Log(LOGINFO, "CDVDVideoCodecAndroidMediaCodec::AddData: iSize({}) > size({})", iSize,
+ out_size);
+ iSize = out_size;
+ }
+ uint8_t* dst_ptr = (uint8_t*)xbmc_jnienv()->GetDirectBufferAddress(buffer.get_raw());
+
+ CJNIMediaCodecCryptoInfo* cryptoInfo(nullptr);
+ if (m_crypto && packet.cryptoInfo)
+ {
+ std::vector<int> clearBytes(packet.cryptoInfo->clearBytes,
+ packet.cryptoInfo->clearBytes +
+ packet.cryptoInfo->numSubSamples);
+ std::vector<int> cipherBytes(packet.cryptoInfo->cipherBytes,
+ packet.cryptoInfo->cipherBytes +
+ packet.cryptoInfo->numSubSamples);
+
+ cryptoInfo = new CJNIMediaCodecCryptoInfo();
+ if (CJNIBase::GetSDKVersion() < 25 &&
+ packet.cryptoInfo->mode == CJNIMediaCodec::CRYPTO_MODE_AES_CBC)
+ {
+ CLog::LogF(LOGERROR, "Device API does not support CBCS decryption");
+ return false;
+ }
+
+ cryptoInfo->set(
+ packet.cryptoInfo->numSubSamples, clearBytes, cipherBytes,
+ std::vector<char>(std::begin(packet.cryptoInfo->kid), std::end(packet.cryptoInfo->kid)),
+ std::vector<char>(std::begin(packet.cryptoInfo->iv), std::end(packet.cryptoInfo->iv)),
+ packet.cryptoInfo->mode == CJNIMediaCodec::CRYPTO_MODE_AES_CBC
+ ? CJNIMediaCodec::CRYPTO_MODE_AES_CBC
+ : CJNIMediaCodec::CRYPTO_MODE_AES_CTR);
+
+ CJNIMediaCodecCryptoInfoPattern cryptoInfoPattern(packet.cryptoInfo->cryptBlocks,
+ packet.cryptoInfo->skipBlocks);
+ cryptoInfo->setPattern(cryptoInfoPattern);
+ }
+ if (dst_ptr)
+ {
+ // Codec specifics
+ switch(m_hints.codec)
+ {
+ case AV_CODEC_ID_VC1:
+ {
+ if (iSize >= 4 && pData[0] == 0x00 && pData[1] == 0x00 && pData[2] == 0x01 && (pData[3] == 0x0d || pData[3] == 0x0f))
+ memcpy(dst_ptr, pData, iSize);
+ else
+ {
+ dst_ptr[0] = 0x00;
+ dst_ptr[1] = 0x00;
+ dst_ptr[2] = 0x01;
+ dst_ptr[3] = 0x0d;
+ memcpy(dst_ptr+4, pData, iSize);
+ iSize += 4;
+ }
+
+ break;
+ }
+
+ default:
+ memcpy(dst_ptr, pData, iSize);
+ break;
+ }
+ }
+
+
+ // Translate from VideoPlayer dts/pts to MediaCodec pts,
+ // pts WILL get re-ordered by MediaCodec if needed.
+ // Do not try to pass pts as a unioned double/int64_t,
+ // some android devices will diddle with presentationTimeUs
+ // and you will get NaN back and VideoPlayerVideo will barf.
+ if (m_dtsShift == DVD_NOPTS_VALUE)
+ m_dtsShift = (dts == DVD_NOPTS_VALUE) ? 0 : dts;
+
+ int64_t presentationTimeUs = m_invalidPTSValue;
+ if (pts != DVD_NOPTS_VALUE)
+ {
+ presentationTimeUs = (pts - m_dtsShift);
+ m_useDTSforPTS = false;
+ }
+ else if ((presentationTimeUs < 0 || m_useDTSforPTS) && dts != DVD_NOPTS_VALUE)
+ presentationTimeUs = (dts - m_dtsShift);
+ else
+ presentationTimeUs = 0;
+
+ int flags = 0;
+ int offset = 0;
+
+ if (!cryptoInfo)
+ m_codec->queueInputBuffer(m_indexInputBuffer, offset, iSize, presentationTimeUs, flags);
+ else
+ {
+ m_codec->queueSecureInputBuffer(m_indexInputBuffer, offset, *cryptoInfo, presentationTimeUs,
+ flags);
+ delete cryptoInfo, cryptoInfo = nullptr;
+ }
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::AddData error");
+ }
+ m_indexInputBuffer = -1;
+ }
+ else
+ return false;
+ }
+ return true;
+}
+
+void CDVDVideoCodecAndroidMediaCodec::Reset()
+{
+ if (!m_opened)
+ return;
+
+ if (m_codec)
+ {
+ // now we can flush the actual MediaCodec object
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::Reset Current state ({})", m_state);
+ m_codec->flush();
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::Reset: flush failed");
+ }
+
+ if (m_state == MEDIACODEC_STATE_ERROR)
+ {
+ m_codec->stop();
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::Reset: stop failed");
+ }
+ ConfigureMediaCodec();
+ }
+ else
+ {
+ CJNIMediaFormat mediaFormat = m_codec->getOutputFormat();
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::Reset: getOutputFormat failed");
+ }
+ else
+ InjectExtraData(mediaFormat);
+ }
+
+ m_state = MEDIACODEC_STATE_FLUSHED;
+
+ // Invalidate our local VideoPicture bits
+ m_videobuffer.pts = DVD_NOPTS_VALUE;
+
+ m_dtsShift = DVD_NOPTS_VALUE;
+ m_indexInputBuffer = -1;
+
+ if (m_bitstream)
+ m_bitstream->ResetStartDecode();
+ }
+}
+
+bool CDVDVideoCodecAndroidMediaCodec::Reconfigure(CDVDStreamInfo &hints)
+{
+ if (m_hints.Equal(hints, CDVDStreamInfo::COMPARE_ALL &
+ ~(CDVDStreamInfo::COMPARE_ID | CDVDStreamInfo::COMPARE_EXTRADATA)))
+ {
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::Reconfigure: true");
+ m_hints = hints;
+ return true;
+ }
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::Reconfigure: false");
+ return false;
+}
+
+CDVDVideoCodec::VCReturn CDVDVideoCodecAndroidMediaCodec::GetPicture(VideoPicture* pVideoPicture)
+{
+ if (!m_opened)
+ return VC_NONE;
+
+ if (m_state == MEDIACODEC_STATE_ERROR || m_state == MEDIACODEC_STATE_ENDOFSTREAM)
+ return VC_EOF;
+
+ if (m_OutputDuration < m_fpsDuration || (m_codecControlFlags & DVD_CODEC_CTRL_DRAIN) != 0)
+ {
+ m_videobuffer.videoBuffer = pVideoPicture->videoBuffer;
+
+ int retgp = GetOutputPicture();
+
+ if (retgp > 0)
+ {
+ pVideoPicture->videoBuffer = nullptr;
+ pVideoPicture->SetParams(m_videobuffer);
+ pVideoPicture->videoBuffer = m_videobuffer.videoBuffer;
+
+ CLog::Log(LOGDEBUG, LOGVIDEO,
+ "CDVDVideoCodecAndroidMediaCodec::GetPicture index: {}, pts:{:0.4f}",
+ static_cast<CMediaCodecVideoBuffer*>(m_videobuffer.videoBuffer)->GetBufferId(),
+ pVideoPicture->pts);
+
+ m_videobuffer.videoBuffer = nullptr;
+
+ return VC_PICTURE;
+ }
+ else
+ {
+ m_videobuffer.videoBuffer = nullptr;
+ if (retgp == -1 || m_state == MEDIACODEC_STATE_WAIT_ENDOFSTREAM) // EOS
+ {
+ m_state = (retgp == -2) ? MEDIACODEC_STATE_ERROR : MEDIACODEC_STATE_ENDOFSTREAM;
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CDVDVideoCodecAndroidMediaCodec::GetPicture VC_EOF");
+ return VC_EOF;
+ }
+ }
+ }
+ else
+ m_OutputDuration = 0;
+
+ if ((m_codecControlFlags & DVD_CODEC_CTRL_DRAIN) == 0)
+ {
+ // try to fetch an input buffer
+ if (m_indexInputBuffer < 0)
+ {
+ m_indexInputBuffer = m_codec->dequeueInputBuffer(5000 /*timeout*/);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR,
+ "CDVDVideoCodecAndroidMediaCodec::GetPicture dequeueInputBuffer failed");
+ m_indexInputBuffer = -1;
+ }
+ }
+
+ if (m_indexInputBuffer >= 0)
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CDVDVideoCodecAndroidMediaCodec::GetPicture VC_BUFFER");
+ return VC_BUFFER;
+ }
+ }
+ else if (m_state == MEDIACODEC_STATE_RUNNING)
+ {
+ SignalEndOfStream();
+ m_state = MEDIACODEC_STATE_WAIT_ENDOFSTREAM;
+ }
+ else if (m_state == MEDIACODEC_STATE_FLUSHED)
+ return VC_EOF;
+
+ return VC_NONE;
+}
+
+void CDVDVideoCodecAndroidMediaCodec::SetCodecControl(int flags)
+{
+ if (m_codecControlFlags != flags)
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CDVDVideoCodecAndroidMediaCodec::{} {:x}->{:x}", __func__,
+ m_codecControlFlags, flags);
+ m_codecControlFlags = flags;
+ }
+}
+
+unsigned CDVDVideoCodecAndroidMediaCodec::GetAllowedReferences()
+{
+ return 4;
+}
+
+void CDVDVideoCodecAndroidMediaCodec::FlushInternal()
+{
+ SignalEndOfStream();
+
+ m_OutputDuration = 0;
+ m_lastPTS = -1;
+ m_dtsShift = DVD_NOPTS_VALUE;
+}
+
+void CDVDVideoCodecAndroidMediaCodec::SignalEndOfStream()
+{
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::{}: state: {}", __func__, m_state);
+ if (m_codec && (m_state == MEDIACODEC_STATE_RUNNING))
+ {
+ // Release all mediaodec output buffers to allow drain if we don't get inputbuffer early
+ if (m_videoBufferPool)
+ {
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::{}: ReleaseMediaCodecBuffers",
+ __func__);
+ m_videoBufferPool->ReleaseMediaCodecBuffers();
+ }
+
+ if (m_indexInputBuffer < 0)
+ {
+ m_indexInputBuffer = m_codec->dequeueInputBuffer(100000);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR,
+ "CDVDVideoCodecAndroidMediaCodec::SignalEndOfStream: dequeueInputBuffer failed");
+ }
+ }
+
+ xbmc_jnienv()->ExceptionClear();
+
+ if (m_indexInputBuffer >= 0)
+ {
+ m_codec->queueInputBuffer(m_indexInputBuffer, 0, 0, 0,
+ CJNIMediaCodec::BUFFER_FLAG_END_OF_STREAM);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::{}: queueInputBuffer failed");
+ }
+ else
+ {
+ m_indexInputBuffer = -1;
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::{}: BUFFER_FLAG_END_OF_STREAM send",
+ __func__);
+ }
+ }
+ else
+ CLog::Log(LOGWARNING, "CDVDVideoCodecAndroidMediaCodec::{}: invalid index: {}", __func__,
+ m_indexInputBuffer);
+ }
+}
+
+void CDVDVideoCodecAndroidMediaCodec::InjectExtraData(CJNIMediaFormat& mediaformat)
+{
+ if (!m_hints.extrasize)
+ return;
+
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec::{}", __func__);
+ size_t size = m_hints.extrasize;
+ void* src_ptr = m_hints.extradata;
+ if (m_bitstream)
+ {
+ size = m_bitstream->GetExtraSize();
+ src_ptr = m_bitstream->GetExtraData();
+ }
+ // Allocate a byte buffer via allocateDirect in java instead of NewDirectByteBuffer,
+ // since the latter doesn't allocate storage of its own, and we don't know how long
+ // the codec uses the buffer.
+ CJNIByteBuffer bytebuffer = CJNIByteBuffer::allocateDirect(size);
+ void* dts_ptr = xbmc_jnienv()->GetDirectBufferAddress(bytebuffer.get_raw());
+ memcpy(dts_ptr, src_ptr, size);
+ // codec will automatically handle buffers as extradata
+ // using entries with keys "csd-0", "csd-1", etc.
+ mediaformat.setByteBuffer("csd-0", bytebuffer);
+}
+
+std::vector<uint8_t> CDVDVideoCodecAndroidMediaCodec::GetHDRStaticMetadata()
+{
+ std::vector<uint8_t> metadata;
+ if (m_hints.masteringMetadata)
+ {
+ // for more information, see CTA+861.3-A standard document
+ static const double MAX_CHROMATICITY = 50000;
+ static const double MAX_LUMINANCE = 10000;
+ metadata.resize(25);
+ metadata[0] = 0;
+ unsigned short* data = reinterpret_cast<unsigned short*>(&metadata[1]);
+ data[0] = static_cast<unsigned short>(
+ av_q2d(m_hints.masteringMetadata->display_primaries[0][0]) * MAX_CHROMATICITY + 0.5);
+ data[1] = static_cast<unsigned short>(
+ av_q2d(m_hints.masteringMetadata->display_primaries[0][1]) * MAX_CHROMATICITY + 0.5);
+ data[2] = static_cast<unsigned short>(
+ av_q2d(m_hints.masteringMetadata->display_primaries[1][0]) * MAX_CHROMATICITY + 0.5);
+ data[3] = static_cast<unsigned short>(
+ av_q2d(m_hints.masteringMetadata->display_primaries[1][1]) * MAX_CHROMATICITY + 0.5);
+ data[4] = static_cast<unsigned short>(
+ av_q2d(m_hints.masteringMetadata->display_primaries[2][0]) * MAX_CHROMATICITY + 0.5);
+ data[5] = static_cast<unsigned short>(
+ av_q2d(m_hints.masteringMetadata->display_primaries[2][1]) * MAX_CHROMATICITY + 0.5);
+ data[6] = static_cast<unsigned short>(
+ av_q2d(m_hints.masteringMetadata->white_point[0]) * MAX_CHROMATICITY + 0.5);
+ data[7] = static_cast<unsigned short>(
+ av_q2d(m_hints.masteringMetadata->white_point[1]) * MAX_CHROMATICITY + 0.5);
+ data[8] = static_cast<unsigned short>(av_q2d(m_hints.masteringMetadata->max_luminance) + 0.5);
+ data[9] = static_cast<unsigned short>(
+ av_q2d(m_hints.masteringMetadata->min_luminance) * MAX_LUMINANCE + 0.5);
+ // we can have HDR content that does not provide content light level metadata
+ if (m_hints.contentLightMetadata)
+ {
+ data[10] = static_cast<unsigned short>(m_hints.contentLightMetadata->MaxCLL);
+ data[11] = static_cast<unsigned short>(m_hints.contentLightMetadata->MaxFALL);
+ }
+ }
+ return metadata;
+}
+
+bool CDVDVideoCodecAndroidMediaCodec::ConfigureMediaCodec(void)
+{
+ // setup a MediaFormat to match the video content,
+ // used by codec during configure
+ CJNIMediaFormat mediaformat =
+ CJNIMediaFormat::createVideoFormat(m_mime, m_hints.width, m_hints.height);
+ mediaformat.setInteger(CJNIMediaFormat::KEY_MAX_INPUT_SIZE, 0);
+
+ if (CJNIBase::GetSDKVersion() >= 23 && m_render_surface)
+ {
+ // Handle rotation
+ mediaformat.setInteger(CJNIMediaFormat::KEY_ROTATION, m_hints.orientation);
+ mediaformat.setFeatureEnabled(CJNIMediaCodecInfoCodecCapabilities::FEATURE_TunneledPlayback,
+ false);
+ if (m_needSecureDecoder)
+ mediaformat.setFeatureEnabled(CJNIMediaCodecInfoCodecCapabilities::FEATURE_SecurePlayback,
+ true);
+ }
+
+ if (CJNIBase::GetSDKVersion() >= 24)
+ {
+ if (m_hints.colorRange != AVCOL_RANGE_UNSPECIFIED)
+ mediaformat.setInteger(CJNIMediaFormat::KEY_COLOR_RANGE, m_hints.colorRange);
+
+ if (m_hints.colorPrimaries != AVCOL_PRI_UNSPECIFIED)
+ {
+ switch (m_hints.colorPrimaries)
+ {
+ case AVCOL_PRI_BT709:
+ mediaformat.setInteger(CJNIMediaFormat::KEY_COLOR_STANDARD,
+ CJNIMediaFormat::COLOR_STANDARD_BT709);
+ break;
+ case AVCOL_PRI_BT2020:
+ mediaformat.setInteger(CJNIMediaFormat::KEY_COLOR_STANDARD,
+ CJNIMediaFormat::COLOR_STANDARD_BT2020);
+ break;
+ default:; // do nothing
+ }
+ }
+
+ if (m_hints.colorTransferCharacteristic != AVCOL_TRC_UNSPECIFIED)
+ {
+ switch (m_hints.colorTransferCharacteristic)
+ {
+ case AVCOL_TRC_LINEAR:
+ mediaformat.setInteger(CJNIMediaFormat::KEY_COLOR_TRANSFER,
+ CJNIMediaFormat::COLOR_TRANSFER_LINEAR);
+ break;
+ case AVCOL_TRC_SMPTE170M:
+ mediaformat.setInteger(CJNIMediaFormat::KEY_COLOR_TRANSFER,
+ CJNIMediaFormat::COLOR_TRANSFER_SDR_VIDEO);
+ break;
+ case AVCOL_TRC_SMPTE2084:
+ mediaformat.setInteger(CJNIMediaFormat::KEY_COLOR_TRANSFER,
+ CJNIMediaFormat::COLOR_TRANSFER_ST2084);
+ break;
+ case AVCOL_TRC_ARIB_STD_B67:
+ mediaformat.setInteger(CJNIMediaFormat::KEY_COLOR_TRANSFER,
+ CJNIMediaFormat::COLOR_TRANSFER_HLG);
+ break;
+ default:; // do nothing
+ }
+ }
+ std::vector<uint8_t> hdr_static_data = GetHDRStaticMetadata();
+ if (!hdr_static_data.empty())
+ {
+ CJNIByteBuffer bytebuffer = CJNIByteBuffer::allocateDirect(hdr_static_data.size());
+ void* dts_ptr = xbmc_jnienv()->GetDirectBufferAddress(bytebuffer.get_raw());
+ memcpy(dts_ptr, hdr_static_data.data(), hdr_static_data.size());
+ mediaformat.setByteBuffer(CJNIMediaFormat::KEY_HDR_STATIC_INFO, bytebuffer);
+ }
+ }
+
+ // handle codec extradata
+ InjectExtraData(mediaformat);
+
+ if (m_render_surface)
+ {
+ m_jnivideosurface = m_jnivideoview->getSurface();
+ if (!m_jnivideosurface)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec: VideoView getSurface failed!!");
+ m_jnivideoview->release();
+ m_jnivideoview.reset();
+ return false;
+ }
+ m_formatname += "(S)";
+ }
+ else
+ InitSurfaceTexture();
+
+ // configure and start the codec.
+ // use the MediaFormat that we have setup.
+ // use a null MediaCrypto, our content is not encrypted.
+ m_codec->configure(mediaformat, m_jnivideosurface,
+ m_crypto ? *m_crypto : CJNIMediaCrypto(jni::jhobject(NULL)), 0);
+
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::ConfigureMediaCodec: configure failed");
+ return false;
+ }
+ m_state = MEDIACODEC_STATE_CONFIGURED;
+
+ m_codec->start();
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ Dispose();
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec:ConfigureMediaCodec: start failed");
+ return false;
+ }
+ m_state = MEDIACODEC_STATE_FLUSHED;
+
+ // There is no guarantee we'll get an INFO_OUTPUT_FORMAT_CHANGED (up to Android 4.3)
+ // Configure the output with defaults
+ ConfigureOutputFormat(mediaformat);
+
+ return true;
+}
+
+int CDVDVideoCodecAndroidMediaCodec::GetOutputPicture(void)
+{
+ int rtn = 0;
+ int64_t timeout_us = (m_state == MEDIACODEC_STATE_WAIT_ENDOFSTREAM) ? 100000 : 10000;
+ CJNIMediaCodecBufferInfo bufferInfo;
+
+ ssize_t index = m_codec->dequeueOutputBuffer(bufferInfo, timeout_us);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR,
+ "CDVDVideoCodecAndroidMediaCodec:GetOutputPicture dequeueOutputBuffer failed");
+ return -2;
+ }
+
+ if (index >= 0)
+ {
+ int64_t pts = bufferInfo.presentationTimeUs();
+ m_videobuffer.dts = DVD_NOPTS_VALUE;
+ m_videobuffer.pts = DVD_NOPTS_VALUE;
+ if (pts != AV_NOPTS_VALUE)
+ {
+ m_videobuffer.pts = pts;
+ m_videobuffer.pts += m_dtsShift;
+ if (m_lastPTS >= 0 && pts > m_lastPTS)
+ m_OutputDuration += pts - m_lastPTS;
+ m_lastPTS = pts;
+ }
+
+ if (m_codecControlFlags & DVD_CODEC_CTRL_DROP)
+ {
+ m_codec->releaseOutputBuffer(index, false);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(
+ LOGERROR,
+ "CDVDVideoCodecAndroidMediaCodec::GetOutputPicture: releaseOutputBuffer (drop) failed");
+ }
+ return -2;
+ }
+
+ int flags = bufferInfo.flags();
+ if (flags & CJNIMediaCodec::BUFFER_FLAG_END_OF_STREAM)
+ {
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec:: BUFFER_FLAG_END_OF_STREAM");
+ m_codec->releaseOutputBuffer(index, false);
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(
+ LOGERROR,
+ "CDVDVideoCodecAndroidMediaCodec::GetOutputPicture: releaseOutputBuffer (eof) failed");
+ }
+ return -1;
+ }
+
+ if (m_videobuffer.videoBuffer)
+ m_videobuffer.videoBuffer->Release();
+
+ m_videobuffer.videoBuffer = m_videoBufferPool->Get();
+ static_cast<CMediaCodecVideoBuffer*>(m_videobuffer.videoBuffer)->Set(index, m_textureId, m_surfaceTexture, m_frameAvailable, m_jnivideoview);
+
+ rtn = 1;
+ }
+ else if (index == CJNIMediaCodec::INFO_OUTPUT_FORMAT_CHANGED)
+ {
+ CJNIMediaFormat mediaformat = m_codec->getOutputFormat();
+ if (xbmc_jnienv()->ExceptionCheck())
+ {
+ xbmc_jnienv()->ExceptionDescribe();
+ xbmc_jnienv()->ExceptionClear();
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::GetOutputPicture(INFO_OUTPUT_FORMAT_CHANGED) ExceptionCheck: getOutputBuffers");
+ }
+ else
+ ConfigureOutputFormat(mediaformat);
+ }
+ else if (index == CJNIMediaCodec::INFO_TRY_AGAIN_LATER ||
+ index == CJNIMediaCodec::INFO_OUTPUT_BUFFERS_CHANGED)
+ {
+ // ignore
+ rtn = 0;
+ }
+ else
+ {
+ // we should never get here
+ CLog::Log(LOGERROR, "CDVDVideoCodecAndroidMediaCodec::GetOutputPicture unknown index({})",
+ index);
+ rtn = -2;
+ }
+
+ return rtn;
+}
+
+void CDVDVideoCodecAndroidMediaCodec::ConfigureOutputFormat(CJNIMediaFormat& mediaformat)
+{
+ int width = 0;
+ int height = 0;
+ int stride = 0;
+ int slice_height= 0;
+ int color_format= 0;
+ int crop_left = 0;
+ int crop_top = 0;
+ int crop_right = 0;
+ int crop_bottom = 0;
+
+ if (mediaformat.containsKey(CJNIMediaFormat::KEY_WIDTH))
+ width = mediaformat.getInteger(CJNIMediaFormat::KEY_WIDTH);
+ if (mediaformat.containsKey(CJNIMediaFormat::KEY_HEIGHT))
+ height = mediaformat.getInteger(CJNIMediaFormat::KEY_HEIGHT);
+ if (mediaformat.containsKey(CJNIMediaFormat::KEY_COLOR_FORMAT))
+ color_format = mediaformat.getInteger(CJNIMediaFormat::KEY_COLOR_FORMAT);
+
+ if (CJNIBase::GetSDKVersion() >= 23)
+ {
+ if (mediaformat.containsKey(CJNIMediaFormat::KEY_STRIDE))
+ stride = mediaformat.getInteger(CJNIMediaFormat::KEY_STRIDE);
+ if (mediaformat.containsKey(CJNIMediaFormat::KEY_SLICE_HEIGHT))
+ slice_height = mediaformat.getInteger(CJNIMediaFormat::KEY_SLICE_HEIGHT);
+ }
+
+ if (CJNIBase::GetSDKVersion() >= 33)
+ {
+ if (mediaformat.containsKey(CJNIMediaFormat::KEY_CROP_LEFT))
+ crop_left = mediaformat.getInteger(CJNIMediaFormat::KEY_CROP_LEFT);
+ if (mediaformat.containsKey(CJNIMediaFormat::KEY_CROP_TOP))
+ crop_top = mediaformat.getInteger(CJNIMediaFormat::KEY_CROP_TOP);
+ if (mediaformat.containsKey(CJNIMediaFormat::KEY_CROP_RIGHT))
+ crop_right = mediaformat.getInteger(CJNIMediaFormat::KEY_CROP_RIGHT);
+ if (mediaformat.containsKey(CJNIMediaFormat::KEY_CROP_BOTTOM))
+ crop_bottom = mediaformat.getInteger(CJNIMediaFormat::KEY_CROP_BOTTOM);
+ }
+
+ if (!crop_right)
+ crop_right = width-1;
+ if (!crop_bottom)
+ crop_bottom = height-1;
+
+ // clear any jni exceptions
+ if (xbmc_jnienv()->ExceptionCheck())
+ xbmc_jnienv()->ExceptionClear();
+
+ CLog::Log(LOGDEBUG,
+ "CDVDVideoCodecAndroidMediaCodec:: "
+ "width({}), height({}), stride({}), slice-height({}), color-format({})",
+ width, height, stride, slice_height, color_format);
+ CLog::Log(LOGDEBUG,
+ "CDVDVideoCodecAndroidMediaCodec:: "
+ "crop-left({}), crop-top({}), crop-right({}), crop-bottom({})",
+ crop_left, crop_top, crop_right, crop_bottom);
+
+ if (m_render_surface)
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec:: Multi-Surface Rendering");
+ else
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecAndroidMediaCodec:: Direct Surface Rendering");
+
+ if (crop_right)
+ width = crop_right + 1 - crop_left;
+ if (crop_bottom)
+ height = crop_bottom + 1 - crop_top;
+
+ m_videobuffer.iDisplayWidth = m_videobuffer.iWidth = width;
+ m_videobuffer.iDisplayHeight = m_videobuffer.iHeight = height;
+
+ if (m_hints.aspect > 1.0 && !m_hints.forced_aspect)
+ {
+ m_videobuffer.iDisplayWidth = ((int)lrint(m_videobuffer.iHeight * m_hints.aspect)) & ~3;
+ if (m_videobuffer.iDisplayWidth > m_videobuffer.iWidth)
+ {
+ m_videobuffer.iDisplayWidth = m_videobuffer.iWidth;
+ m_videobuffer.iDisplayHeight = ((int)lrint(m_videobuffer.iWidth / m_hints.aspect)) & ~3;
+ }
+ }
+}
+
+void CDVDVideoCodecAndroidMediaCodec::CallbackInitSurfaceTexture(void *userdata)
+{
+ CDVDVideoCodecAndroidMediaCodec *ctx = static_cast<CDVDVideoCodecAndroidMediaCodec*>(userdata);
+ ctx->InitSurfaceTexture();
+}
+
+void CDVDVideoCodecAndroidMediaCodec::InitSurfaceTexture(void)
+{
+ if (m_render_surface)
+ return;
+
+ // We MUST create the GLES texture on the main thread
+ // to match where the valid GLES context is located.
+ // It would be nice to move this out of here, we would need
+ // to create/fetch/create from g_RenderManager. But g_RenderManager
+ // does not know we are using MediaCodec until Configure and we
+ // we need m_surfaceTexture valid before then. Chicken, meet Egg.
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ // localize GLuint so we do not spew gles includes in our header
+ GLuint texture_id;
+
+ glGenTextures(1, &texture_id);
+ glBindTexture( GL_TEXTURE_EXTERNAL_OES, texture_id);
+ glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glBindTexture( GL_TEXTURE_EXTERNAL_OES, 0);
+ m_textureId = texture_id;
+
+ m_surfaceTexture = std::shared_ptr<CJNISurfaceTexture>(new CJNISurfaceTexture(m_textureId));
+ // hook the surfaceTexture OnFrameAvailable callback
+ m_frameAvailable = std::shared_ptr<CDVDMediaCodecOnFrameAvailable>(new CDVDMediaCodecOnFrameAvailable(m_surfaceTexture));
+ m_jnivideosurface = CJNISurface(*m_surfaceTexture);
+ }
+ else
+ {
+ ThreadMessageCallback callbackData;
+ callbackData.callback = &CallbackInitSurfaceTexture;
+ callbackData.userptr = (void*)this;
+
+ // wait for it.
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_CALLBACK, -1, -1,
+ static_cast<void*>(&callbackData));
+ }
+}
+
+void CDVDVideoCodecAndroidMediaCodec::ReleaseSurfaceTexture(void)
+{
+ if (m_render_surface)
+ return;
+
+ // it is safe to delete here even though these items
+ // were created in the main thread instance
+ m_jnivideosurface = CJNISurface(jni::jhobject(NULL));
+ m_frameAvailable.reset();
+ m_surfaceTexture.reset();
+
+ if (m_textureId > 0)
+ {
+ GLuint texture_id = m_textureId;
+ glDeleteTextures(1, &texture_id);
+ m_textureId = 0;
+ }
+}
+
+void CDVDVideoCodecAndroidMediaCodec::UpdateFpsDuration()
+{
+ if (m_hints.fpsrate > 0 && m_hints.fpsscale > 0)
+ m_fpsDuration = static_cast<uint32_t>(static_cast<uint64_t>(DVD_TIME_BASE) * m_hints.fpsscale / m_hints.fpsrate);
+ else
+ m_fpsDuration = 1;
+
+ m_processInfo.SetVideoFps(static_cast<float>(m_hints.fpsrate) / m_hints.fpsscale);
+
+ CLog::Log(LOGDEBUG,
+ "CDVDVideoCodecAndroidMediaCodec::UpdateFpsDuration fpsRate:{} fpsscale:{}, fpsDur:{}",
+ m_hints.fpsrate, m_hints.fpsscale, m_fpsDuration);
+}
+
+void CDVDVideoCodecAndroidMediaCodec::surfaceChanged(CJNISurfaceHolder holder, int format, int width, int height)
+{
+}
+
+void CDVDVideoCodecAndroidMediaCodec::surfaceCreated(CJNISurfaceHolder holder)
+{
+ if (m_state == MEDIACODEC_STATE_STOPPED)
+ {
+ ConfigureMediaCodec();
+ }
+}
+
+void CDVDVideoCodecAndroidMediaCodec::surfaceDestroyed(CJNISurfaceHolder holder)
+{
+ if (m_state != MEDIACODEC_STATE_STOPPED && m_state != MEDIACODEC_STATE_UNINITIALIZED)
+ {
+ m_state = MEDIACODEC_STATE_STOPPED;
+ if (m_jnivideosurface)
+ m_jnivideosurface.release();
+ m_codec->stop();
+ xbmc_jnienv()->ExceptionClear();
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.h b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.h
new file mode 100644
index 0000000..3deba5d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.h
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2013-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 "DVDStreamInfo.h"
+#include "DVDVideoCodec.h"
+#include "cores/VideoPlayer/Buffers/VideoBuffer.h"
+#include "threads/SingleLock.h"
+#include "threads/Thread.h"
+#include "utils/Geometry.h"
+
+#include "platform/android/activity/JNIXBMCVideoView.h"
+
+#include <atomic>
+#include <deque>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include <android/native_window.h>
+#include <android/native_window_jni.h>
+#include <androidjni/Surface.h>
+
+class CJNISurface;
+class CJNISurfaceTexture;
+class CJNIMediaCodec;
+class CJNIMediaCrypto;
+class CJNIMediaFormat;
+class CJNIMediaCodecBufferInfo;
+class CDVDMediaCodecOnFrameAvailable;
+class CJNIByteBuffer;
+class CBitstreamConverter;
+
+struct DemuxCryptoInfo;
+struct mpeg2_sequence;
+
+
+typedef struct amc_demux
+{
+ uint8_t* pData;
+ int iSize;
+ double dts;
+ double pts;
+} amc_demux;
+
+class CMediaCodecVideoBufferPool;
+
+class CMediaCodecVideoBuffer : public CVideoBuffer
+{
+public:
+ CMediaCodecVideoBuffer(int id) : CVideoBuffer(id) {}
+ ~CMediaCodecVideoBuffer() override = default;
+
+ void Set(int internalId,
+ int textureId,
+ std::shared_ptr<CJNISurfaceTexture> surfaceTexture,
+ std::shared_ptr<CDVDMediaCodecOnFrameAvailable> frameAvailable,
+ std::shared_ptr<CJNIXBMCVideoView> videoView);
+
+ // meat and potatoes
+ bool WaitForFrame(int millis);
+ // MediaCodec related
+ void ReleaseOutputBuffer(bool render,
+ int64_t displayTime,
+ CMediaCodecVideoBufferPool* pool = nullptr);
+ // SurfaceTexture released
+ int GetBufferId() const;
+ int GetTextureId() const;
+ void GetTransformMatrix(float* textureMatrix);
+ void UpdateTexImage();
+ void RenderUpdate(const CRect& DestRect, int64_t displayTime);
+ bool HasSurfaceTexture() const { return m_surfacetexture.operator bool(); }
+
+private:
+ int m_bufferId = -1;
+ unsigned int m_textureId = 0;
+ // shared_ptr bits, shared between
+ // CDVDVideoCodecAndroidMediaCodec and LinuxRenderGLES.
+ std::shared_ptr<CJNISurfaceTexture> m_surfacetexture;
+ std::shared_ptr<CDVDMediaCodecOnFrameAvailable> m_frameready;
+ std::shared_ptr<CJNIXBMCVideoView> m_videoview;
+};
+
+class CMediaCodecVideoBufferPool : public IVideoBufferPool
+{
+public:
+ CMediaCodecVideoBufferPool(std::shared_ptr<CJNIMediaCodec> mediaCodec)
+ : m_codec(std::move(mediaCodec))
+ {
+ }
+
+ ~CMediaCodecVideoBufferPool() override;
+
+ CVideoBuffer* Get() override;
+ void Return(int id) override;
+
+ std::shared_ptr<CJNIMediaCodec> GetMediaCodec();
+ void ResetMediaCodec();
+ void ReleaseMediaCodecBuffers();
+
+private:
+ CCriticalSection m_criticalSection;
+ std::shared_ptr<CJNIMediaCodec> m_codec;
+
+ std::vector<CMediaCodecVideoBuffer*> m_videoBuffers;
+ std::vector<int> m_freeBuffers;
+};
+
+class CDVDVideoCodecAndroidMediaCodec : public CDVDVideoCodec, public CJNISurfaceHolderCallback
+{
+public:
+ CDVDVideoCodecAndroidMediaCodec(CProcessInfo& processInfo, bool surface_render = false);
+ ~CDVDVideoCodecAndroidMediaCodec() override;
+
+ // registration
+ static std::unique_ptr<CDVDVideoCodec> Create(CProcessInfo& processInfo);
+ static bool Register();
+
+ // required overrides
+ bool Open(CDVDStreamInfo& hints, CDVDCodecOptions& options) override;
+ bool AddData(const DemuxPacket& packet) override;
+ void Reset() override;
+ bool Reconfigure(CDVDStreamInfo& hints) override;
+ VCReturn GetPicture(VideoPicture* pVideoPicture) override;
+ const char* GetName() override { return m_formatname.c_str(); }
+ void SetCodecControl(int flags) override;
+ unsigned GetAllowedReferences() override;
+
+protected:
+ void Dispose();
+ void FlushInternal(void);
+ void SignalEndOfStream();
+ void InjectExtraData(CJNIMediaFormat& mediaformat);
+ std::vector<uint8_t> GetHDRStaticMetadata();
+ bool ConfigureMediaCodec(void);
+ int GetOutputPicture(void);
+ void ConfigureOutputFormat(CJNIMediaFormat& mediaformat);
+ void UpdateFpsDuration();
+
+ // surface handling functions
+ static void CallbackInitSurfaceTexture(void*);
+ void InitSurfaceTexture(void);
+ void ReleaseSurfaceTexture(void);
+
+ CDVDStreamInfo m_hints;
+ std::string m_mime;
+ std::string m_codecname;
+ int m_colorFormat;
+ std::string m_formatname;
+ bool m_opened;
+ bool m_needSecureDecoder = false;
+ int m_codecControlFlags;
+ int m_state;
+
+ std::shared_ptr<CJNIXBMCVideoView> m_jnivideoview;
+ CJNISurface m_jnivideosurface;
+ unsigned int m_textureId;
+ std::shared_ptr<CJNIMediaCodec> m_codec;
+ CJNIMediaCrypto* m_crypto = nullptr;
+ std::shared_ptr<CJNISurfaceTexture> m_surfaceTexture;
+ std::shared_ptr<CDVDMediaCodecOnFrameAvailable> m_frameAvailable;
+
+ amc_demux m_demux_pkt;
+ std::shared_ptr<CMediaCodecVideoBufferPool> m_videoBufferPool;
+
+ uint32_t m_OutputDuration, m_fpsDuration;
+ int64_t m_lastPTS;
+ int64_t m_invalidPTSValue = 0;
+ double m_dtsShift;
+
+ static std::atomic<bool> m_InstanceGuard;
+
+ std::unique_ptr<CBitstreamConverter> m_bitstream;
+ VideoPicture m_videobuffer;
+
+ int m_indexInputBuffer;
+ bool m_render_surface;
+ mpeg2_sequence* m_mpeg2_sequence;
+ int m_src_offset[4];
+ int m_src_stride[4];
+ bool m_useDTSforPTS;
+
+ // CJNISurfaceHolderCallback interface
+public:
+ void surfaceChanged(CJNISurfaceHolder holder, int format, int width, int height) override;
+ void surfaceCreated(CJNISurfaceHolder holder) override;
+ void surfaceDestroyed(CJNISurfaceHolder holder) override;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.cpp
new file mode 100644
index 0000000..80ca602
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.cpp
@@ -0,0 +1,647 @@
+/*
+ * Copyright (C) 2017-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 "DVDVideoCodecDRMPRIME.h"
+
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/Buffers/VideoBufferDMA.h"
+#include "cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDCodecs.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "threads/SingleLock.h"
+#include "utils/CPUInfo.h"
+#include "utils/log.h"
+
+#if defined(HAVE_GBM)
+#include "windowing/gbm/WinSystemGbm.h"
+#endif
+
+extern "C"
+{
+#include <libavcodec/avcodec.h>
+#include <libavutil/error.h>
+#include <libavutil/imgutils.h>
+#include <libavutil/opt.h>
+#include <libavutil/pixdesc.h>
+}
+
+namespace
+{
+
+constexpr const char* SETTING_VIDEOPLAYER_USEPRIMEDECODERFORHW{"videoplayer.useprimedecoderforhw"};
+
+static void ReleaseBuffer(void* opaque, uint8_t* data)
+{
+ CVideoBufferDMA* buffer = static_cast<CVideoBufferDMA*>(opaque);
+ buffer->Release();
+}
+
+static void AlignedSize(AVCodecContext* avctx, int& width, int& height)
+{
+ int w = width;
+ int h = height;
+ AVFrame picture;
+ int unaligned;
+ int stride_align[AV_NUM_DATA_POINTERS];
+
+ avcodec_align_dimensions2(avctx, &w, &h, stride_align);
+
+ do
+ {
+ // NOTE: do not align linesizes individually, this breaks e.g. assumptions
+ // that linesize[0] == 2*linesize[1] in the MPEG-encoder for 4:2:2
+ av_image_fill_linesizes(picture.linesize, avctx->pix_fmt, w);
+ // increase alignment of w for next try (rhs gives the lowest bit set in w)
+ w += w & ~(w - 1);
+
+ unaligned = 0;
+ for (int i = 0; i < 4; i++)
+ unaligned |= picture.linesize[i] % stride_align[i];
+ } while (unaligned);
+
+ width = w;
+ height = h;
+}
+
+} // namespace
+
+CDVDVideoCodecDRMPRIME::CDVDVideoCodecDRMPRIME(CProcessInfo& processInfo)
+ : CDVDVideoCodec(processInfo)
+{
+ m_pFrame = av_frame_alloc();
+ m_videoBufferPool = std::make_shared<CVideoBufferPoolDRMPRIMEFFmpeg>();
+}
+
+CDVDVideoCodecDRMPRIME::~CDVDVideoCodecDRMPRIME()
+{
+ av_frame_free(&m_pFrame);
+ avcodec_free_context(&m_pCodecContext);
+}
+
+std::unique_ptr<CDVDVideoCodec> CDVDVideoCodecDRMPRIME::Create(CProcessInfo& processInfo)
+{
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_VIDEOPLAYER_USEPRIMEDECODER))
+ return std::make_unique<CDVDVideoCodecDRMPRIME>(processInfo);
+
+ return nullptr;
+}
+
+void CDVDVideoCodecDRMPRIME::Register()
+{
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return;
+
+ auto setting = settings->GetSetting(CSettings::SETTING_VIDEOPLAYER_USEPRIMEDECODER);
+ if (!setting)
+ {
+ CLog::Log(LOGERROR, "Failed to load setting for: {}",
+ CSettings::SETTING_VIDEOPLAYER_USEPRIMEDECODER);
+ return;
+ }
+
+ setting->SetVisible(true);
+
+ setting = settings->GetSetting(SETTING_VIDEOPLAYER_USEPRIMEDECODERFORHW);
+ if (!setting)
+ {
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", SETTING_VIDEOPLAYER_USEPRIMEDECODERFORHW);
+ return;
+ }
+
+ setting->SetVisible(true);
+
+ CDVDFactoryCodec::RegisterHWVideoCodec("drm_prime", CDVDVideoCodecDRMPRIME::Create);
+}
+
+static bool IsSupportedHwFormat(const enum AVPixelFormat fmt)
+{
+ bool hw = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ SETTING_VIDEOPLAYER_USEPRIMEDECODERFORHW);
+
+ return fmt == AV_PIX_FMT_DRM_PRIME && hw;
+}
+
+static bool IsSupportedSwFormat(const enum AVPixelFormat fmt)
+{
+ return fmt == AV_PIX_FMT_YUV420P || fmt == AV_PIX_FMT_YUVJ420P;
+}
+
+static const AVCodecHWConfig* FindHWConfig(const AVCodec* codec)
+{
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ SETTING_VIDEOPLAYER_USEPRIMEDECODERFORHW))
+ return nullptr;
+
+ const AVCodecHWConfig* config = nullptr;
+ for (int n = 0; (config = avcodec_get_hw_config(codec, n)); n++)
+ {
+ if (!IsSupportedHwFormat(config->pix_fmt))
+ continue;
+
+ if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) &&
+ config->device_type == AV_HWDEVICE_TYPE_DRM)
+ return config;
+
+ if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_INTERNAL))
+ return config;
+ }
+
+ return nullptr;
+}
+
+static const AVCodec* FindDecoder(CDVDStreamInfo& hints)
+{
+ const AVCodec* codec = nullptr;
+ void* i = 0;
+
+ if (!(hints.codecOptions & CODEC_FORCE_SOFTWARE))
+ while ((codec = av_codec_iterate(&i)))
+ {
+ if (!av_codec_is_decoder(codec))
+ continue;
+ if (codec->id != hints.codec)
+ continue;
+
+ const AVCodecHWConfig* config = FindHWConfig(codec);
+ if (config)
+ return codec;
+ }
+
+ codec = avcodec_find_decoder(hints.codec);
+ if (codec && (codec->capabilities & AV_CODEC_CAP_DR1) == AV_CODEC_CAP_DR1)
+ return codec;
+
+ return nullptr;
+}
+
+enum AVPixelFormat CDVDVideoCodecDRMPRIME::GetFormat(struct AVCodecContext* avctx,
+ const enum AVPixelFormat* fmt)
+{
+ for (int n = 0; fmt[n] != AV_PIX_FMT_NONE; n++)
+ {
+ if (IsSupportedHwFormat(fmt[n]) || IsSupportedSwFormat(fmt[n]))
+ {
+ CDVDVideoCodecDRMPRIME* ctx = static_cast<CDVDVideoCodecDRMPRIME*>(avctx->opaque);
+ ctx->UpdateProcessInfo(avctx, fmt[n]);
+ return fmt[n];
+ }
+ }
+
+ CLog::Log(LOGERROR, "CDVDVideoCodecDRMPRIME::{} - unsupported pixel format", __FUNCTION__);
+ return AV_PIX_FMT_NONE;
+}
+
+int CDVDVideoCodecDRMPRIME::GetBuffer(struct AVCodecContext* avctx, AVFrame* frame, int flags)
+{
+ if (IsSupportedSwFormat(static_cast<AVPixelFormat>(frame->format)))
+ {
+ int width = frame->width;
+ int height = frame->height;
+
+ AlignedSize(avctx, width, height);
+
+ int size;
+ switch (avctx->pix_fmt)
+ {
+ case AV_PIX_FMT_YUV420P:
+ case AV_PIX_FMT_YUVJ420P:
+ size = width * height * 3 / 2;
+ break;
+ default:
+ return -1;
+ }
+
+ CDVDVideoCodecDRMPRIME* ctx = static_cast<CDVDVideoCodecDRMPRIME*>(avctx->opaque);
+ auto buffer = dynamic_cast<CVideoBufferDMA*>(
+ ctx->m_processInfo.GetVideoBufferManager().Get(avctx->pix_fmt, size, nullptr));
+ if (!buffer)
+ return -1;
+
+ frame->opaque = static_cast<void*>(buffer);
+ frame->opaque_ref =
+ av_buffer_create(nullptr, 0, ReleaseBuffer, frame->opaque, AV_BUFFER_FLAG_READONLY);
+
+ buffer->Export(frame, width, height);
+ buffer->SyncStart();
+
+ return 0;
+ }
+
+ return avcodec_default_get_buffer2(avctx, frame, flags);
+}
+
+bool CDVDVideoCodecDRMPRIME::Open(CDVDStreamInfo& hints, CDVDCodecOptions& options)
+{
+ const AVCodec* pCodec = FindDecoder(hints);
+ if (!pCodec)
+ {
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecDRMPRIME::{} - unable to find decoder for codec {}",
+ __FUNCTION__, hints.codec);
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CDVDVideoCodecDRMPRIME::{} - using decoder {}", __FUNCTION__,
+ pCodec->long_name ? pCodec->long_name : pCodec->name);
+
+ m_pCodecContext = avcodec_alloc_context3(pCodec);
+ if (!m_pCodecContext)
+ return false;
+
+ m_hints = hints;
+
+ const AVCodecHWConfig* pConfig = FindHWConfig(pCodec);
+ if (pConfig && (pConfig->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) &&
+ pConfig->device_type == AV_HWDEVICE_TYPE_DRM)
+ {
+ const char* device = nullptr;
+
+ if (getenv("KODI_RENDER_NODE"))
+ device = getenv("KODI_RENDER_NODE");
+
+#if defined(HAVE_GBM)
+ auto winSystem = dynamic_cast<KODI::WINDOWING::GBM::CWinSystemGbm*>(CServiceBroker::GetWinSystem());
+
+ if (winSystem)
+ {
+ auto drm = winSystem->GetDrm();
+
+ if (!drm)
+ return false;
+
+ if (!device)
+ device = drm->GetRenderDevicePath();
+ }
+#endif
+
+ //! @todo: fix with proper device when dma-hints wayland protocol works
+ if (!device)
+ device = "/dev/dri/renderD128";
+
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecDRMPRIME::{} - using drm device for av_hwdevice_ctx: {}", __FUNCTION__, device);
+
+ if (av_hwdevice_ctx_create(&m_pCodecContext->hw_device_ctx, pConfig->device_type,
+ device, nullptr, 0) < 0)
+ {
+ CLog::Log(LOGERROR,
+ "CDVDVideoCodecDRMPRIME::{} - unable to create hwdevice context using device: {}",
+ __FUNCTION__, device);
+ avcodec_free_context(&m_pCodecContext);
+ return false;
+ }
+ }
+
+ m_pCodecContext->pix_fmt = AV_PIX_FMT_DRM_PRIME;
+ m_pCodecContext->opaque = static_cast<void*>(this);
+ m_pCodecContext->get_format = GetFormat;
+ m_pCodecContext->get_buffer2 = GetBuffer;
+ m_pCodecContext->codec_tag = hints.codec_tag;
+ m_pCodecContext->coded_width = hints.width;
+ m_pCodecContext->coded_height = hints.height;
+ m_pCodecContext->bits_per_coded_sample = hints.bitsperpixel;
+ m_pCodecContext->time_base.num = 1;
+ m_pCodecContext->time_base.den = DVD_TIME_BASE;
+ m_pCodecContext->thread_count = CServiceBroker::GetCPUInfo()->GetCPUCount();
+
+ if (hints.extradata && hints.extrasize > 0)
+ {
+ m_pCodecContext->extradata_size = hints.extrasize;
+ m_pCodecContext->extradata =
+ static_cast<uint8_t*>(av_mallocz(hints.extrasize + AV_INPUT_BUFFER_PADDING_SIZE));
+ memcpy(m_pCodecContext->extradata, hints.extradata, hints.extrasize);
+ }
+
+ for (auto&& option : options.m_keys)
+ av_opt_set(m_pCodecContext, option.m_name.c_str(), option.m_value.c_str(), 0);
+
+ if (avcodec_open2(m_pCodecContext, pCodec, nullptr) < 0)
+ {
+ CLog::Log(LOGINFO, "CDVDVideoCodecDRMPRIME::{} - unable to open codec", __FUNCTION__);
+ avcodec_free_context(&m_pCodecContext);
+ if (hints.codecOptions & CODEC_FORCE_SOFTWARE)
+ return false;
+
+ hints.codecOptions |= CODEC_FORCE_SOFTWARE;
+ return Open(hints, options);
+ }
+
+ UpdateProcessInfo(m_pCodecContext, m_pCodecContext->pix_fmt);
+ m_processInfo.SetVideoDeintMethod("none");
+ m_processInfo.SetVideoDAR(hints.aspect);
+
+ return true;
+}
+
+void CDVDVideoCodecDRMPRIME::UpdateProcessInfo(struct AVCodecContext* avctx,
+ const enum AVPixelFormat pix_fmt)
+{
+ const char* pixFmtName = av_get_pix_fmt_name(pix_fmt);
+ m_processInfo.SetVideoPixelFormat(pixFmtName ? pixFmtName : "");
+ m_processInfo.SetVideoDimensions(avctx->coded_width, avctx->coded_height);
+
+ if (avctx->codec && avctx->codec->name)
+ m_name = std::string("ff-") + avctx->codec->name;
+ else
+ m_name = "ffmpeg";
+
+ m_processInfo.SetVideoDecoderName(m_name + "-drm_prime", IsSupportedHwFormat(pix_fmt));
+}
+
+bool CDVDVideoCodecDRMPRIME::AddData(const DemuxPacket& packet)
+{
+ if (!m_pCodecContext)
+ return true;
+
+ if (!packet.pData)
+ return true;
+
+ AVPacket* avpkt = av_packet_alloc();
+ if (!avpkt)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecDRMPRIME::{} - av_packet_alloc failed: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ avpkt->data = packet.pData;
+ avpkt->size = packet.iSize;
+ avpkt->dts = (packet.dts == DVD_NOPTS_VALUE)
+ ? AV_NOPTS_VALUE
+ : static_cast<int64_t>(packet.dts / DVD_TIME_BASE * AV_TIME_BASE);
+ avpkt->pts = (packet.pts == DVD_NOPTS_VALUE)
+ ? AV_NOPTS_VALUE
+ : static_cast<int64_t>(packet.pts / DVD_TIME_BASE * AV_TIME_BASE);
+ avpkt->side_data = static_cast<AVPacketSideData*>(packet.pSideData);
+ avpkt->side_data_elems = packet.iSideDataElems;
+
+ int ret = avcodec_send_packet(m_pCodecContext, avpkt);
+
+ //! @todo: properly handle avpkt side_data. this works around our improper use of the side_data
+ // as we pass pointers to ffmpeg allocated memory for the side_data. we should really be allocating
+ // and storing our own AVPacket. This will require some extensive changes.
+ av_buffer_unref(&avpkt->buf);
+ av_free(avpkt);
+
+ if (ret == AVERROR(EAGAIN))
+ return false;
+ else if (ret)
+ {
+ char err[AV_ERROR_MAX_STRING_SIZE] = {};
+ av_strerror(ret, err, AV_ERROR_MAX_STRING_SIZE);
+ CLog::Log(LOGERROR, "CDVDVideoCodecDRMPRIME::{} - send packet failed: {} ({})", __FUNCTION__,
+ err, ret);
+ if (ret != AVERROR_EOF && ret != AVERROR_INVALIDDATA)
+ return false;
+ }
+
+ return true;
+}
+
+void CDVDVideoCodecDRMPRIME::Reset()
+{
+ if (!m_pCodecContext)
+ return;
+
+ Drain();
+
+ do
+ {
+ int ret = avcodec_receive_frame(m_pCodecContext, m_pFrame);
+ if (ret == AVERROR_EOF)
+ break;
+ else if (ret)
+ {
+ char err[AV_ERROR_MAX_STRING_SIZE] = {};
+ av_strerror(ret, err, AV_ERROR_MAX_STRING_SIZE);
+ CLog::Log(LOGERROR, "CDVDVideoCodecDRMPRIME::{} - receive frame failed: {} ({})",
+ __FUNCTION__, err, ret);
+ break;
+ }
+ else
+ av_frame_unref(m_pFrame);
+ } while (true);
+
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecDRMPRIME::{} - flush buffers", __FUNCTION__);
+ avcodec_flush_buffers(m_pCodecContext);
+}
+
+void CDVDVideoCodecDRMPRIME::Drain()
+{
+ AVPacket* avpkt = av_packet_alloc();
+ if (!avpkt)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecDRMPRIME::{} - av_packet_alloc failed: {}", __FUNCTION__,
+ strerror(errno));
+ return;
+ }
+
+ avpkt->data = nullptr;
+ avpkt->size = 0;
+
+ int ret = avcodec_send_packet(m_pCodecContext, avpkt);
+ if (ret && ret != AVERROR_EOF)
+ {
+ char err[AV_ERROR_MAX_STRING_SIZE] = {};
+ av_strerror(ret, err, AV_ERROR_MAX_STRING_SIZE);
+ CLog::Log(LOGERROR, "CDVDVideoCodecDRMPRIME::{} - send packet failed: {} ({})", __FUNCTION__,
+ err, ret);
+ }
+
+ av_packet_free(&avpkt);
+}
+
+void CDVDVideoCodecDRMPRIME::SetPictureParams(VideoPicture* pVideoPicture)
+{
+ pVideoPicture->iWidth = m_pFrame->width;
+ pVideoPicture->iHeight = m_pFrame->height;
+
+ double aspect_ratio = 0;
+ AVRational pixel_aspect = m_pFrame->sample_aspect_ratio;
+ if (pixel_aspect.num)
+ aspect_ratio = av_q2d(pixel_aspect) * pVideoPicture->iWidth / pVideoPicture->iHeight;
+
+ if (aspect_ratio <= 0.0)
+ aspect_ratio =
+ static_cast<double>(pVideoPicture->iWidth) / static_cast<double>(pVideoPicture->iHeight);
+
+ if (m_DAR != aspect_ratio)
+ {
+ m_DAR = aspect_ratio;
+ m_processInfo.SetVideoDAR(static_cast<float>(m_DAR));
+ }
+
+ pVideoPicture->iDisplayWidth =
+ (static_cast<int>(lrint(pVideoPicture->iHeight * aspect_ratio))) & -3;
+ pVideoPicture->iDisplayHeight = pVideoPicture->iHeight;
+ if (pVideoPicture->iDisplayWidth > pVideoPicture->iWidth)
+ {
+ pVideoPicture->iDisplayWidth = pVideoPicture->iWidth;
+ pVideoPicture->iDisplayHeight =
+ (static_cast<int>(lrint(pVideoPicture->iWidth / aspect_ratio))) & -3;
+ }
+
+ pVideoPicture->color_range = m_pFrame->color_range == AVCOL_RANGE_JPEG ||
+ m_pFrame->format == AV_PIX_FMT_YUVJ420P ||
+ m_hints.colorRange == AVCOL_RANGE_JPEG;
+ pVideoPicture->color_primaries = m_pFrame->color_primaries == AVCOL_PRI_UNSPECIFIED
+ ? m_hints.colorPrimaries
+ : m_pFrame->color_primaries;
+ pVideoPicture->color_transfer = m_pFrame->color_trc == AVCOL_TRC_UNSPECIFIED
+ ? m_hints.colorTransferCharacteristic
+ : m_pFrame->color_trc;
+ pVideoPicture->color_space =
+ m_pFrame->colorspace == AVCOL_SPC_UNSPECIFIED ? m_hints.colorSpace : m_pFrame->colorspace;
+ pVideoPicture->chroma_position = m_pFrame->chroma_location;
+
+ pVideoPicture->colorBits = 8;
+ if (m_pCodecContext->codec_id == AV_CODEC_ID_HEVC &&
+ m_pCodecContext->profile == FF_PROFILE_HEVC_MAIN_10)
+ pVideoPicture->colorBits = 10;
+ else if (m_pCodecContext->codec_id == AV_CODEC_ID_H264 &&
+ (m_pCodecContext->profile == FF_PROFILE_H264_HIGH_10 ||
+ m_pCodecContext->profile == FF_PROFILE_H264_HIGH_10_INTRA))
+ pVideoPicture->colorBits = 10;
+
+ pVideoPicture->hasDisplayMetadata = false;
+ AVFrameSideData* sd = av_frame_get_side_data(m_pFrame, AV_FRAME_DATA_MASTERING_DISPLAY_METADATA);
+ if (sd)
+ {
+ pVideoPicture->displayMetadata = *reinterpret_cast<AVMasteringDisplayMetadata*>(sd->data);
+ pVideoPicture->hasDisplayMetadata = true;
+ }
+ else if (m_hints.masteringMetadata)
+ {
+ pVideoPicture->displayMetadata = *m_hints.masteringMetadata.get();
+ pVideoPicture->hasDisplayMetadata = true;
+ }
+
+ pVideoPicture->hasLightMetadata = false;
+ sd = av_frame_get_side_data(m_pFrame, AV_FRAME_DATA_CONTENT_LIGHT_LEVEL);
+ if (sd)
+ {
+ pVideoPicture->lightMetadata = *reinterpret_cast<AVContentLightMetadata*>(sd->data);
+ pVideoPicture->hasLightMetadata = true;
+ }
+ else if (m_hints.contentLightMetadata)
+ {
+ pVideoPicture->lightMetadata = *m_hints.contentLightMetadata.get();
+ pVideoPicture->hasLightMetadata = true;
+ }
+
+ pVideoPicture->iRepeatPicture = 0;
+ pVideoPicture->iFlags = 0;
+ pVideoPicture->iFlags |= m_pFrame->interlaced_frame ? DVP_FLAG_INTERLACED : 0;
+ pVideoPicture->iFlags |= m_pFrame->top_field_first ? DVP_FLAG_TOP_FIELD_FIRST : 0;
+ pVideoPicture->iFlags |= m_pFrame->data[0] ? 0 : DVP_FLAG_DROPPED;
+
+ if (m_codecControlFlags & DVD_CODEC_CTRL_DROP)
+ {
+ pVideoPicture->iFlags |= DVP_FLAG_DROPPED;
+ }
+
+ int64_t pts = m_pFrame->best_effort_timestamp;
+ pVideoPicture->pts = (pts == AV_NOPTS_VALUE)
+ ? DVD_NOPTS_VALUE
+ : static_cast<double>(pts) * DVD_TIME_BASE / AV_TIME_BASE;
+ pVideoPicture->dts = DVD_NOPTS_VALUE;
+}
+
+CDVDVideoCodec::VCReturn CDVDVideoCodecDRMPRIME::GetPicture(VideoPicture* pVideoPicture)
+{
+ if (m_codecControlFlags & DVD_CODEC_CTRL_DRAIN)
+ Drain();
+
+ int ret = avcodec_receive_frame(m_pCodecContext, m_pFrame);
+ if (ret == AVERROR(EAGAIN))
+ return VC_BUFFER;
+ else if (ret == AVERROR_EOF)
+ {
+ if (m_codecControlFlags & DVD_CODEC_CTRL_DRAIN)
+ {
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecDRMPRIME::{} - flush buffers", __FUNCTION__);
+ avcodec_flush_buffers(m_pCodecContext);
+ SetCodecControl(m_codecControlFlags & ~DVD_CODEC_CTRL_DRAIN);
+ }
+ return VC_EOF;
+ }
+ else if (ret)
+ {
+ char err[AV_ERROR_MAX_STRING_SIZE] = {};
+ av_strerror(ret, err, AV_ERROR_MAX_STRING_SIZE);
+ CLog::Log(LOGERROR, "CDVDVideoCodecDRMPRIME::{} - receive frame failed: {} ({})", __FUNCTION__,
+ err, ret);
+ return VC_ERROR;
+ }
+
+ SetPictureParams(pVideoPicture);
+
+ if (pVideoPicture->videoBuffer)
+ {
+ pVideoPicture->videoBuffer->Release();
+ pVideoPicture->videoBuffer = nullptr;
+ }
+
+ if (IsSupportedHwFormat(static_cast<AVPixelFormat>(m_pFrame->format)))
+ {
+ CVideoBufferDRMPRIMEFFmpeg* buffer =
+ dynamic_cast<CVideoBufferDRMPRIMEFFmpeg*>(m_videoBufferPool->Get());
+ buffer->SetPictureParams(*pVideoPicture);
+ buffer->SetRef(m_pFrame);
+ pVideoPicture->videoBuffer = buffer;
+ }
+ else if (m_pFrame->opaque)
+ {
+ CVideoBufferDMA* buffer = static_cast<CVideoBufferDMA*>(m_pFrame->opaque);
+ buffer->SetPictureParams(*pVideoPicture);
+ buffer->Acquire();
+ buffer->SyncEnd();
+ buffer->SetDimensions(m_pFrame->width, m_pFrame->height);
+
+ pVideoPicture->videoBuffer = buffer;
+ av_frame_unref(m_pFrame);
+ }
+
+ if (!pVideoPicture->videoBuffer)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecDRMPRIME::{} - videoBuffer:nullptr format:{}", __FUNCTION__,
+ av_get_pix_fmt_name(static_cast<AVPixelFormat>(m_pFrame->format)));
+ av_frame_unref(m_pFrame);
+ return VC_ERROR;
+ }
+
+ return VC_PICTURE;
+}
+
+void CDVDVideoCodecDRMPRIME::SetCodecControl(int flags)
+{
+ m_codecControlFlags = flags;
+
+ if (m_pCodecContext)
+ {
+ if ((flags & DVD_CODEC_CTRL_DROP_ANY) != 0)
+ {
+ m_pCodecContext->skip_frame = AVDISCARD_NONREF;
+ m_pCodecContext->skip_idct = AVDISCARD_NONREF;
+ m_pCodecContext->skip_loop_filter = AVDISCARD_NONREF;
+ }
+ else
+ {
+ m_pCodecContext->skip_frame = AVDISCARD_DEFAULT;
+ m_pCodecContext->skip_idct = AVDISCARD_DEFAULT;
+ m_pCodecContext->skip_loop_filter = AVDISCARD_DEFAULT;
+ }
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.h b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.h
new file mode 100644
index 0000000..db49d16
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017-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/VideoPlayer/Buffers/VideoBuffer.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+#include "cores/VideoPlayer/DVDStreamInfo.h"
+
+#include <memory>
+
+class CDVDVideoCodecDRMPRIME : public CDVDVideoCodec
+{
+public:
+ explicit CDVDVideoCodecDRMPRIME(CProcessInfo& processInfo);
+ ~CDVDVideoCodecDRMPRIME() override;
+
+ static std::unique_ptr<CDVDVideoCodec> Create(CProcessInfo& processInfo);
+ static void Register();
+
+ bool Open(CDVDStreamInfo& hints, CDVDCodecOptions& options) override;
+ bool AddData(const DemuxPacket& packet) override;
+ void Reset() override;
+ CDVDVideoCodec::VCReturn GetPicture(VideoPicture* pVideoPicture) override;
+ const char* GetName() override { return m_name.c_str(); }
+ unsigned GetAllowedReferences() override { return 5; }
+ void SetCodecControl(int flags) override;
+
+protected:
+ void Drain();
+ void SetPictureParams(VideoPicture* pVideoPicture);
+ void UpdateProcessInfo(struct AVCodecContext* avctx, const enum AVPixelFormat fmt);
+ static enum AVPixelFormat GetFormat(struct AVCodecContext* avctx, const enum AVPixelFormat* fmt);
+ static int GetBuffer(struct AVCodecContext* avctx, AVFrame* frame, int flags);
+
+ std::string m_name;
+ int m_codecControlFlags = 0;
+ CDVDStreamInfo m_hints;
+ double m_DAR = 1.0;
+ AVCodecContext* m_pCodecContext = nullptr;
+ AVFrame* m_pFrame = nullptr;
+ std::shared_ptr<IVideoBufferPool> m_videoBufferPool;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecFFmpeg.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecFFmpeg.cpp
new file mode 100644
index 0000000..881c02e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecFFmpeg.cpp
@@ -0,0 +1,1380 @@
+/*
+ * 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 "DVDVideoCodecFFmpeg.h"
+
+#include "DVDCodecs/DVDCodecs.h"
+#include "DVDCodecs/DVDFactoryCodec.h"
+#include "DVDStreamInfo.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderManager.h"
+#include "cores/VideoSettings.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CPUInfo.h"
+#include "utils/StringUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+
+extern "C" {
+#include <libavutil/opt.h>
+#include <libavutil/mastering_display_metadata.h>
+#include <libavfilter/avfilter.h>
+#include <libavfilter/buffersink.h>
+#include <libavfilter/buffersrc.h>
+#include <libavutil/pixdesc.h>
+}
+
+#ifndef TARGET_POSIX
+#define RINT(x) ((x) >= 0 ? ((int)((x) + 0.5)) : ((int)((x) - 0.5)))
+#else
+#include <math.h>
+#define RINT lrint
+#endif
+
+enum DecoderState
+{
+ STATE_NONE,
+ STATE_SW_SINGLE,
+ STATE_HW_SINGLE,
+ STATE_HW_FAILED,
+ STATE_SW_MULTI
+};
+
+enum EFilterFlags {
+ FILTER_NONE = 0x0,
+ FILTER_DEINTERLACE_YADIF = 0x1, //< use first deinterlace mode
+ FILTER_DEINTERLACE_ANY = 0xf, //< use any deinterlace mode
+ FILTER_DEINTERLACE_FLAGGED = 0x10, //< only deinterlace flagged frames
+ FILTER_DEINTERLACE_HALFED = 0x20, //< do half rate deinterlacing
+ FILTER_ROTATE = 0x40, //< rotate image according to the codec hints
+};
+
+//------------------------------------------------------------------------------
+// Video Buffers
+//------------------------------------------------------------------------------
+
+class CVideoBufferFFmpeg : public CVideoBuffer
+{
+public:
+ CVideoBufferFFmpeg(IVideoBufferPool &pool, int id);
+ ~CVideoBufferFFmpeg() override;
+ void GetPlanes(uint8_t*(&planes)[YuvImage::MAX_PLANES]) override;
+ void GetStrides(int(&strides)[YuvImage::MAX_PLANES]) override;
+
+ void SetRef(AVFrame *frame);
+ void Unref();
+
+protected:
+ AVFrame* m_pFrame;
+};
+
+CVideoBufferFFmpeg::CVideoBufferFFmpeg(IVideoBufferPool &pool, int id)
+: CVideoBuffer(id)
+{
+ m_pFrame = av_frame_alloc();
+}
+
+CVideoBufferFFmpeg::~CVideoBufferFFmpeg()
+{
+ av_frame_free(&m_pFrame);
+}
+
+void CVideoBufferFFmpeg::GetPlanes(uint8_t*(&planes)[YuvImage::MAX_PLANES])
+{
+ planes[0] = m_pFrame->data[0];
+ planes[1] = m_pFrame->data[1];
+ planes[2] = m_pFrame->data[2];
+}
+
+void CVideoBufferFFmpeg::GetStrides(int(&strides)[YuvImage::MAX_PLANES])
+{
+ strides[0] = m_pFrame->linesize[0];
+ strides[1] = m_pFrame->linesize[1];
+ strides[2] = m_pFrame->linesize[2];
+}
+
+void CVideoBufferFFmpeg::SetRef(AVFrame *frame)
+{
+ av_frame_unref(m_pFrame);
+ av_frame_move_ref(m_pFrame, frame);
+ m_pixFormat = (AVPixelFormat)m_pFrame->format;
+}
+
+void CVideoBufferFFmpeg::Unref()
+{
+ av_frame_unref(m_pFrame);
+}
+
+//------------------------------------------------------------------------------
+
+class CVideoBufferPoolFFmpeg : public IVideoBufferPool
+{
+public:
+ ~CVideoBufferPoolFFmpeg() override;
+ void Return(int id) override;
+ CVideoBuffer* Get() override;
+
+protected:
+ CCriticalSection m_critSection;
+ std::vector<CVideoBufferFFmpeg*> m_all;
+ std::deque<int> m_used;
+ std::deque<int> m_free;
+};
+
+CVideoBufferPoolFFmpeg::~CVideoBufferPoolFFmpeg()
+{
+ for (auto buf : m_all)
+ {
+ delete buf;
+ }
+}
+
+CVideoBuffer* CVideoBufferPoolFFmpeg::Get()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CVideoBufferFFmpeg *buf = nullptr;
+ if (!m_free.empty())
+ {
+ int idx = m_free.front();
+ m_free.pop_front();
+ m_used.push_back(idx);
+ buf = m_all[idx];
+ }
+ else
+ {
+ int id = m_all.size();
+ buf = new CVideoBufferFFmpeg(*this, id);
+ m_all.push_back(buf);
+ m_used.push_back(id);
+ }
+
+ buf->Acquire(GetPtr());
+ return buf;
+}
+
+void CVideoBufferPoolFFmpeg::Return(int id)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_all[id]->Unref();
+ auto it = m_used.begin();
+ while (it != m_used.end())
+ {
+ if (*it == id)
+ {
+ m_used.erase(it);
+ break;
+ }
+ else
+ ++it;
+ }
+ m_free.push_back(id);
+}
+
+//------------------------------------------------------------------------------
+// main class
+//------------------------------------------------------------------------------
+
+CDVDVideoCodecFFmpeg::CDropControl::CDropControl()
+{
+ Reset(true);
+}
+
+void CDVDVideoCodecFFmpeg::CDropControl::Reset(bool init)
+{
+ m_lastPTS = AV_NOPTS_VALUE;
+
+ if (init || m_state != VALID)
+ {
+ m_count = 0;
+ m_diffPTS = 0;
+ m_state = INIT;
+ }
+}
+
+void CDVDVideoCodecFFmpeg::CDropControl::Process(int64_t pts, bool drop)
+{
+ if (m_state == INIT)
+ {
+ if (pts != AV_NOPTS_VALUE && m_lastPTS != AV_NOPTS_VALUE)
+ {
+ m_diffPTS += pts - m_lastPTS;
+ m_count++;
+ }
+ if (m_count > 10)
+ {
+ m_diffPTS = m_diffPTS / m_count;
+ if (m_diffPTS > 0)
+ {
+ CLog::Log(LOGINFO, "CDVDVideoCodecFFmpeg::CDropControl: calculated diff time: {}",
+ m_diffPTS);
+ m_state = CDropControl::VALID;
+ m_count = 0;
+ }
+ }
+ }
+ else if (m_state == VALID && !drop)
+ {
+ if (std::abs(pts - m_lastPTS - m_diffPTS) > m_diffPTS * 0.2)
+ {
+ m_count++;
+ if (m_count > 5)
+ {
+ CLog::Log(LOGINFO, "CDVDVideoCodecFFmpeg::CDropControl: lost diff");
+ Reset(true);
+ }
+ }
+ else
+ m_count = 0;
+ }
+ m_lastPTS = pts;
+}
+
+enum AVPixelFormat CDVDVideoCodecFFmpeg::GetFormat(struct AVCodecContext * avctx, const AVPixelFormat * fmt)
+{
+ ICallbackHWAccel *cb = static_cast<ICallbackHWAccel*>(avctx->opaque);
+ CDVDVideoCodecFFmpeg* ctx = dynamic_cast<CDVDVideoCodecFFmpeg*>(cb);
+
+ const char* pixFmtName = av_get_pix_fmt_name(*fmt);
+
+ ctx->m_processInfo.SetVideoDimensions(avctx->coded_width, avctx->coded_height);
+
+ // if frame threading is enabled hw accel is not allowed
+ // 2nd condition:
+ // fix an ffmpeg issue here, it calls us with an invalid profile
+ // then a 2nd call with a valid one
+ if(ctx->m_decoderState != STATE_HW_SINGLE ||
+ (avctx->codec_id == AV_CODEC_ID_VC1 && avctx->profile == FF_PROFILE_UNKNOWN))
+ {
+ AVPixelFormat defaultFmt = avcodec_default_get_format(avctx, fmt);
+ pixFmtName = av_get_pix_fmt_name(defaultFmt);
+ ctx->m_processInfo.SetVideoPixelFormat(pixFmtName ? pixFmtName : "");
+ ctx->m_processInfo.SetSwDeinterlacingMethods();
+ return defaultFmt;
+ }
+
+ // hardware decoder de-selected, restore standard ffmpeg
+ if (ctx->HasHardware())
+ {
+ ctx->SetHardware(nullptr);
+ avctx->get_buffer2 = avcodec_default_get_buffer2;
+ avctx->slice_flags = 0;
+ av_buffer_unref(&avctx->hw_frames_ctx);
+ }
+
+ const AVPixelFormat * cur = fmt;
+ while (*cur != AV_PIX_FMT_NONE)
+ {
+ pixFmtName = av_get_pix_fmt_name(*cur);
+
+ auto hwaccels = CDVDFactoryCodec::GetHWAccels();
+ for (auto &hwaccel : hwaccels)
+ {
+ IHardwareDecoder *pDecoder(CDVDFactoryCodec::CreateVideoCodecHWAccel(hwaccel, ctx->m_hints,
+ ctx->m_processInfo, *cur));
+ if (pDecoder)
+ {
+ if (pDecoder->Open(avctx, ctx->m_pCodecContext, *cur))
+ {
+ ctx->m_processInfo.SetVideoPixelFormat(pixFmtName ? pixFmtName : "");
+ ctx->SetHardware(pDecoder);
+ return *cur;
+ }
+ pDecoder->Release();
+ }
+ }
+ cur++;
+ }
+
+ ctx->m_processInfo.SetVideoPixelFormat(pixFmtName ? pixFmtName : "");
+ ctx->m_decoderState = STATE_HW_FAILED;
+ return avcodec_default_get_format(avctx, fmt);
+}
+
+CDVDVideoCodecFFmpeg::CDVDVideoCodecFFmpeg(CProcessInfo &processInfo)
+: CDVDVideoCodec(processInfo), m_postProc(processInfo)
+{
+ m_videoBufferPool = std::make_shared<CVideoBufferPoolFFmpeg>();
+
+ m_decoderState = STATE_NONE;
+}
+
+CDVDVideoCodecFFmpeg::~CDVDVideoCodecFFmpeg()
+{
+ Dispose();
+}
+
+bool CDVDVideoCodecFFmpeg::Open(CDVDStreamInfo &hints, CDVDCodecOptions &options)
+{
+ if (hints.cryptoSession)
+ {
+ CLog::Log(LOGERROR,"CDVDVideoCodecFFmpeg::Open() CryptoSessions unsupported!");
+ return false;
+ }
+
+ m_hints = hints;
+ m_options = options;
+
+ AVCodec* pCodec = nullptr;
+
+ m_iOrientation = hints.orientation;
+
+ m_formats.clear();
+ m_formats = m_processInfo.GetPixFormats();
+ m_formats.push_back(AV_PIX_FMT_NONE); /* always add none to get a terminated list in ffmpeg world */
+ m_processInfo.SetSwDeinterlacingMethods();
+ m_processInfo.SetVideoInterlaced(false);
+
+ // libdav1d av1 sw decoding is implemented as a separate decoder
+ // in ffmpeg which is always found first when calling `avcodec_find_decoder`.
+ // To get hwaccels we look for decoders registered for `av1` (unless sw decoding is enforced).
+ // The decoder state check is needed to succesfully fallback to sw decoding if
+ // necessary (on retry).
+ if (hints.codec == AV_CODEC_ID_AV1 && m_decoderState != STATE_HW_FAILED &&
+ !(hints.codecOptions & CODEC_FORCE_SOFTWARE))
+ pCodec = avcodec_find_decoder_by_name("av1");
+
+ if (!pCodec)
+ pCodec = avcodec_find_decoder(hints.codec);
+
+ if(pCodec == NULL)
+ {
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecFFmpeg::Open() Unable to find codec {}", hints.codec);
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CDVDVideoCodecFFmpeg::Open() Using codec: {}",
+ pCodec->long_name ? pCodec->long_name : pCodec->name);
+
+ m_pCodecContext = avcodec_alloc_context3(pCodec);
+ if (!m_pCodecContext)
+ return false;
+
+ m_pCodecContext->opaque = static_cast<ICallbackHWAccel*>(this);
+ m_pCodecContext->debug = 0;
+ m_pCodecContext->workaround_bugs = FF_BUG_AUTODETECT;
+ m_pCodecContext->get_format = GetFormat;
+ m_pCodecContext->codec_tag = hints.codec_tag;
+
+ // setup threading model
+ if (!(hints.codecOptions & CODEC_FORCE_SOFTWARE))
+ {
+ if (m_decoderState == STATE_NONE)
+ {
+ m_decoderState = STATE_HW_SINGLE;
+ }
+ else
+ {
+ int num_threads = CServiceBroker::GetCPUInfo()->GetCPUCount() * 3 / 2;
+ num_threads = std::max(1, std::min(num_threads, 16));
+ m_pCodecContext->thread_count = num_threads;
+ m_decoderState = STATE_SW_MULTI;
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecFFmpeg - open frame threaded with {} threads",
+ num_threads);
+ }
+ }
+ else
+ m_decoderState = STATE_SW_SINGLE;
+
+ // if we don't do this, then some codecs seem to fail.
+ m_pCodecContext->coded_height = hints.height;
+ m_pCodecContext->coded_width = hints.width;
+ m_pCodecContext->bits_per_coded_sample = hints.bitsperpixel;
+ m_pCodecContext->bits_per_raw_sample = hints.bitdepth;
+
+ if( hints.extradata && hints.extrasize > 0 )
+ {
+ m_pCodecContext->extradata_size = hints.extrasize;
+ m_pCodecContext->extradata = (uint8_t*)av_mallocz(hints.extrasize + AV_INPUT_BUFFER_PADDING_SIZE);
+ memcpy(m_pCodecContext->extradata, hints.extradata, hints.extrasize);
+ }
+
+ // advanced setting override for skip loop filter (see avcodec.h for valid options)
+ //! @todo allow per video setting?
+ int iSkipLoopFilter = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iSkipLoopFilter;
+ if (iSkipLoopFilter != 0)
+ {
+ m_pCodecContext->skip_loop_filter = static_cast<AVDiscard>(iSkipLoopFilter);
+ }
+
+ // set any special options
+ for(std::vector<CDVDCodecOption>::iterator it = options.m_keys.begin(); it != options.m_keys.end(); ++it)
+ {
+ av_opt_set(m_pCodecContext, it->m_name.c_str(), it->m_value.c_str(), 0);
+ }
+
+ if (avcodec_open2(m_pCodecContext, pCodec, nullptr) < 0)
+ {
+ CLog::Log(LOGDEBUG,"CDVDVideoCodecFFmpeg::Open() Unable to open codec");
+ avcodec_free_context(&m_pCodecContext);
+ return false;
+ }
+
+ m_pFrame = av_frame_alloc();
+ if (!m_pFrame)
+ {
+ avcodec_free_context(&m_pCodecContext);
+ return false;
+ }
+
+ m_pDecodedFrame = av_frame_alloc();
+ if (!m_pDecodedFrame)
+ {
+ av_frame_free(&m_pFrame);
+ avcodec_free_context(&m_pCodecContext);
+ return false;
+ }
+
+ m_pFilterFrame = av_frame_alloc();
+ if (!m_pFilterFrame)
+ {
+ av_frame_free(&m_pFrame);
+ av_frame_free(&m_pDecodedFrame);
+ avcodec_free_context(&m_pCodecContext);
+ return false;
+ }
+
+ UpdateName();
+ const char* pixFmtName = av_get_pix_fmt_name(m_pCodecContext->pix_fmt);
+ m_processInfo.SetVideoDimensions(m_pCodecContext->coded_width, m_pCodecContext->coded_height);
+ m_processInfo.SetVideoPixelFormat(pixFmtName ? pixFmtName : "");
+
+ m_dropCtrl.Reset(true);
+ m_eof = false;
+ return true;
+}
+
+void CDVDVideoCodecFFmpeg::Dispose()
+{
+ av_frame_free(&m_pFrame);
+ av_frame_free(&m_pDecodedFrame);
+ av_frame_free(&m_pFilterFrame);
+ avcodec_free_context(&m_pCodecContext);
+
+ if (m_pHardware)
+ {
+ m_pHardware->Release();
+ m_pHardware = nullptr;
+ }
+
+ FilterClose();
+}
+
+void CDVDVideoCodecFFmpeg::SetFilters()
+{
+ // ask codec to do deinterlacing if possible
+ EINTERLACEMETHOD mInt = m_processInfo.GetVideoSettings().m_InterlaceMethod;
+
+ if (!m_processInfo.Supports(mInt))
+ mInt = m_processInfo.GetFallbackDeintMethod();
+
+ unsigned int filters = 0;
+
+ if (mInt != VS_INTERLACEMETHOD_NONE && m_interlaced)
+ {
+ if (mInt == VS_INTERLACEMETHOD_DEINTERLACE)
+ filters = FILTER_DEINTERLACE_ANY;
+ else if (mInt == VS_INTERLACEMETHOD_DEINTERLACE_HALF)
+ filters = FILTER_DEINTERLACE_ANY | FILTER_DEINTERLACE_HALFED;
+
+ if (filters)
+ filters |= FILTER_DEINTERLACE_FLAGGED;
+ }
+
+ if (m_codecControlFlags & DVD_CODEC_CTRL_ROTATE)
+ filters |= FILTER_ROTATE;
+
+ m_filters_next.clear();
+
+ if (filters & FILTER_ROTATE)
+ {
+ switch(m_iOrientation)
+ {
+ case 90:
+ m_filters_next += "transpose=1";
+ break;
+ case 180:
+ m_filters_next += "vflip,hflip";
+ break;
+ case 270:
+ m_filters_next += "transpose=2";
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (filters & FILTER_DEINTERLACE_YADIF)
+ {
+ if (filters & FILTER_DEINTERLACE_HALFED)
+ m_filters_next = "yadif=0:-1";
+ else
+ m_filters_next = "yadif=1:-1";
+
+ if (filters & FILTER_DEINTERLACE_FLAGGED)
+ m_filters_next += ":1";
+ }
+}
+
+void CDVDVideoCodecFFmpeg::UpdateName()
+{
+ if(m_pCodecContext->codec->name)
+ m_name = std::string("ff-") + m_pCodecContext->codec->name;
+ else
+ m_name = "ffmpeg";
+
+ if(m_pHardware)
+ m_name += "-" + m_pHardware->Name();
+
+ m_processInfo.SetVideoDecoderName(m_name, m_pHardware ? true : false);
+
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecFFmpeg - Updated codec: {}", m_name);
+}
+
+union pts_union
+{
+ double pts_d;
+ int64_t pts_i;
+};
+
+static int64_t pts_dtoi(double pts)
+{
+ pts_union u;
+ u.pts_d = pts;
+ return u.pts_i;
+}
+
+bool CDVDVideoCodecFFmpeg::AddData(const DemuxPacket &packet)
+{
+ if (!m_pCodecContext)
+ return true;
+
+ if (!packet.pData)
+ return true;
+
+ if (m_eof)
+ {
+ Reset();
+ }
+
+ if (packet.recoveryPoint)
+ m_started = true;
+
+ m_dts = packet.dts;
+ m_pCodecContext->reordered_opaque = pts_dtoi(packet.pts);
+
+ AVPacket* avpkt = av_packet_alloc();
+ if (!avpkt)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecFFmpeg::{} - av_packet_alloc failed: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ avpkt->data = packet.pData;
+ avpkt->size = packet.iSize;
+ avpkt->dts = (packet.dts == DVD_NOPTS_VALUE)
+ ? AV_NOPTS_VALUE
+ : static_cast<int64_t>(packet.dts / DVD_TIME_BASE * AV_TIME_BASE);
+ avpkt->pts = (packet.pts == DVD_NOPTS_VALUE)
+ ? AV_NOPTS_VALUE
+ : static_cast<int64_t>(packet.pts / DVD_TIME_BASE * AV_TIME_BASE);
+ avpkt->side_data = static_cast<AVPacketSideData*>(packet.pSideData);
+ avpkt->side_data_elems = packet.iSideDataElems;
+
+ int ret = avcodec_send_packet(m_pCodecContext, avpkt);
+
+ //! @todo: properly handle avpkt side_data. this works around our improper use of the side_data
+ // as we pass pointers to ffmpeg allocated memory for the side_data. we should really be allocating
+ // and storing our own AVPacket. This will require some extensive changes.
+ av_buffer_unref(&avpkt->buf);
+ av_free(avpkt);
+
+ // try again
+ if (ret == AVERROR(EAGAIN))
+ {
+ return false;
+ }
+ // error
+ else if (ret)
+ {
+ // handle VC_NOBUFFER error for hw accel
+ if (m_pHardware)
+ {
+ int result = m_pHardware->Check(m_pCodecContext);
+ if (result == VC_NOBUFFER)
+ {
+ return false;
+ }
+ }
+ }
+
+ m_iLastKeyframe++;
+ // put a limit on convergence count to avoid huge mem usage on streams without keyframes
+ if (m_iLastKeyframe > 300)
+ m_iLastKeyframe = 300;
+
+ m_startedInput = true;
+
+ return true;
+}
+
+CDVDVideoCodec::VCReturn CDVDVideoCodecFFmpeg::GetPicture(VideoPicture* pVideoPicture)
+{
+ if (!m_startedInput)
+ {
+ return VC_BUFFER;
+ }
+ else if (m_eof)
+ {
+ return VC_EOF;
+ }
+
+ // handle hw accelerators first, they may have frames ready
+ if (m_pHardware)
+ {
+ int flags = m_codecControlFlags;
+ flags &= ~DVD_CODEC_CTRL_DRAIN;
+ m_pHardware->SetCodecControl(flags);
+ CDVDVideoCodec::VCReturn ret = m_pHardware->Decode(m_pCodecContext, nullptr);
+ if (ret == VC_PICTURE)
+ {
+ if (m_pHardware->GetPicture(m_pCodecContext, pVideoPicture))
+ return VC_PICTURE;
+ else
+ return VC_ERROR;
+ }
+ else if (ret == VC_BUFFER)
+ ;
+ else
+ return ret;
+ }
+ else if (m_pFilterGraph && !m_filterEof)
+ {
+ CDVDVideoCodec::VCReturn ret = FilterProcess(nullptr);
+ if (ret == VC_PICTURE)
+ {
+ if (!SetPictureParams(pVideoPicture))
+ return VC_ERROR;
+ return VC_PICTURE;
+ }
+ else if (ret == VC_BUFFER)
+ ;
+ else
+ return ret;
+ }
+
+ // process ffmpeg
+ if (m_codecControlFlags & DVD_CODEC_CTRL_DRAIN)
+ {
+ AVPacket* avpkt = av_packet_alloc();
+ if (!avpkt)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecFFmpeg::{} - av_packet_alloc failed: {}", __FUNCTION__,
+ strerror(errno));
+ return VC_ERROR;
+ }
+ avpkt->data = nullptr;
+ avpkt->size = 0;
+ avpkt->dts = AV_NOPTS_VALUE;
+ avpkt->pts = AV_NOPTS_VALUE;
+ avcodec_send_packet(m_pCodecContext, avpkt);
+
+ av_packet_free(&avpkt);
+ }
+
+ int ret = avcodec_receive_frame(m_pCodecContext, m_pDecodedFrame);
+
+ if (m_decoderState == STATE_HW_FAILED && !m_pHardware)
+ return VC_REOPEN;
+
+ if(m_iLastKeyframe < m_pCodecContext->has_b_frames + 2)
+ m_iLastKeyframe = m_pCodecContext->has_b_frames + 2;
+
+ if (ret == AVERROR_EOF)
+ {
+ // next drain hw accel or filter
+ if (m_pHardware)
+ {
+ int flags = m_codecControlFlags;
+ flags |= DVD_CODEC_CTRL_DRAIN;
+ m_pHardware->SetCodecControl(flags);
+ int ret = m_pHardware->Decode(m_pCodecContext, nullptr);
+ if (ret == VC_PICTURE)
+ {
+ if (m_pHardware->GetPicture(m_pCodecContext, pVideoPicture))
+ return VC_PICTURE;
+ else
+ return VC_ERROR;
+ }
+ else
+ {
+ m_eof = true;
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecFFmpeg::GetPicture - eof hw accel");
+ return VC_EOF;
+ }
+ }
+ else if (m_pFilterGraph && !m_filterEof)
+ {
+ int ret = FilterProcess(nullptr);
+ if (ret == VC_PICTURE)
+ {
+ if (!SetPictureParams(pVideoPicture))
+ return VC_ERROR;
+ else
+ return VC_PICTURE;
+ }
+ else
+ {
+ m_eof = true;
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecFFmpeg::GetPicture - eof filter");
+ return VC_EOF;
+ }
+ }
+ else
+ {
+ m_eof = true;
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecFFmpeg::GetPicture - eof");
+ return VC_EOF;
+ }
+ }
+ else if (ret == AVERROR(EAGAIN))
+ {
+ return VC_BUFFER;
+ }
+ else if (ret)
+ {
+ CLog::Log(LOGERROR, "{} - avcodec_receive_frame returned failure", __FUNCTION__);
+ return VC_ERROR;
+ }
+
+ // here we got a frame
+ int64_t framePTS = m_pDecodedFrame->best_effort_timestamp;
+
+ if (m_pCodecContext->skip_frame > AVDISCARD_DEFAULT)
+ {
+ if (m_dropCtrl.m_state == CDropControl::VALID &&
+ m_dropCtrl.m_lastPTS != AV_NOPTS_VALUE &&
+ framePTS != AV_NOPTS_VALUE &&
+ framePTS > (m_dropCtrl.m_lastPTS + m_dropCtrl.m_diffPTS * 1.5))
+ {
+ m_droppedFrames++;
+ if (m_interlaced)
+ m_droppedFrames++;
+ }
+ }
+ m_dropCtrl.Process(framePTS, m_pCodecContext->skip_frame > AVDISCARD_DEFAULT);
+
+ if (m_pDecodedFrame->key_frame)
+ {
+ m_started = true;
+ m_iLastKeyframe = m_pCodecContext->has_b_frames + 2;
+ }
+ if (m_pDecodedFrame->interlaced_frame)
+ m_interlaced = true;
+ else
+ m_interlaced = false;
+
+ if (!m_processInfo.GetVideoInterlaced() && m_interlaced)
+ m_processInfo.SetVideoInterlaced(m_interlaced);
+
+ if (!m_started)
+ {
+ int frames = 300;
+ if (m_dropCtrl.m_state == CDropControl::VALID)
+ frames = static_cast<int>(6000000 / m_dropCtrl.m_diffPTS);
+ if (m_iLastKeyframe >= frames && m_pDecodedFrame->pict_type == AV_PICTURE_TYPE_I)
+ {
+ m_started = true;
+ }
+ else
+ {
+ av_frame_unref(m_pDecodedFrame);
+ return VC_BUFFER;
+ }
+ }
+
+ // push the frame to hw decoder for further processing
+ if (m_pHardware)
+ {
+ av_frame_unref(m_pFrame);
+ av_frame_move_ref(m_pFrame, m_pDecodedFrame);
+ CDVDVideoCodec::VCReturn ret = m_pHardware->Decode(m_pCodecContext, m_pFrame);
+ if (ret == VC_FLUSHED)
+ {
+ Reset();
+ return ret;
+ }
+ else if (ret == VC_FATAL)
+ {
+ m_decoderState = STATE_HW_FAILED;
+ return VC_REOPEN;
+ }
+ else if (ret == VC_PICTURE)
+ {
+ if (m_pHardware->GetPicture(m_pCodecContext, pVideoPicture))
+ return VC_PICTURE;
+ else
+ return VC_ERROR;
+ }
+
+ return ret;
+ }
+ // process filters for sw decoding
+ else
+ {
+ SetFilters();
+
+ bool need_scale = std::find(m_formats.begin(),
+ m_formats.end(),
+ m_pCodecContext->pix_fmt) == m_formats.end();
+
+ bool need_reopen = false;
+ if (m_filters != m_filters_next)
+ need_reopen = true;
+
+ if (!m_filters_next.empty() && m_filterEof)
+ need_reopen = true;
+
+ if (m_pFilterIn)
+ {
+ if (m_pFilterIn->outputs[0]->format != m_pCodecContext->pix_fmt ||
+ m_pFilterIn->outputs[0]->w != m_pCodecContext->width ||
+ m_pFilterIn->outputs[0]->h != m_pCodecContext->height)
+ need_reopen = true;
+ }
+
+ // try to setup new filters
+ if (need_reopen || (need_scale && m_pFilterGraph == nullptr))
+ {
+ m_filters = m_filters_next;
+
+ if (FilterOpen(m_filters, need_scale) < 0)
+ FilterClose();
+ }
+
+ if (m_pFilterGraph && !m_filterEof)
+ {
+ CDVDVideoCodec::VCReturn ret = FilterProcess(m_pDecodedFrame);
+ if (ret != VC_PICTURE)
+ return VC_NONE;
+ }
+ else
+ {
+ av_frame_unref(m_pFrame);
+ av_frame_move_ref(m_pFrame, m_pDecodedFrame);
+ }
+
+ if (!SetPictureParams(pVideoPicture))
+ return VC_ERROR;
+ else
+ return VC_PICTURE;
+ }
+
+ return VC_NONE;
+}
+
+bool CDVDVideoCodecFFmpeg::SetPictureParams(VideoPicture* pVideoPicture)
+{
+ if (!GetPictureCommon(pVideoPicture))
+ return false;
+
+ pVideoPicture->iFlags |= m_pFrame->data[0] ? 0 : DVP_FLAG_DROPPED;
+
+ if (pVideoPicture->videoBuffer)
+ pVideoPicture->videoBuffer->Release();
+ pVideoPicture->videoBuffer = nullptr;
+
+ CVideoBufferFFmpeg *buffer = dynamic_cast<CVideoBufferFFmpeg*>(m_videoBufferPool->Get());
+ buffer->SetRef(m_pFrame);
+ pVideoPicture->videoBuffer = buffer;
+
+ if (m_processInfo.GetVideoSettings().m_PostProcess)
+ {
+ m_postProc.SetType(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoPPFFmpegPostProc, false);
+ m_postProc.Process(pVideoPicture);
+ }
+
+ return true;
+}
+
+void CDVDVideoCodecFFmpeg::Reset()
+{
+ m_started = false;
+ m_startedInput = false;
+ m_interlaced = false;
+ m_decoderPts = DVD_NOPTS_VALUE;
+ m_skippedDeint = 0;
+ m_droppedFrames = 0;
+ m_eof = false;
+ m_iLastKeyframe = m_pCodecContext->has_b_frames;
+ avcodec_flush_buffers(m_pCodecContext);
+ av_frame_unref(m_pFrame);
+
+ if (m_pHardware)
+ m_pHardware->Reset();
+
+ m_filters = "";
+ FilterClose();
+ m_dropCtrl.Reset(false);
+}
+
+void CDVDVideoCodecFFmpeg::Reopen()
+{
+ Dispose();
+ if (!Open(m_hints, m_options))
+ {
+ Dispose();
+ }
+}
+
+bool CDVDVideoCodecFFmpeg::GetPictureCommon(VideoPicture* pVideoPicture)
+{
+ if (!m_pFrame)
+ return false;
+
+ pVideoPicture->iWidth = m_pFrame->width;
+ pVideoPicture->iHeight = m_pFrame->height;
+
+ /* crop of 10 pixels if demuxer asked it */
+ if(m_pCodecContext->coded_width && m_pCodecContext->coded_width < (int)pVideoPicture->iWidth
+ && m_pCodecContext->coded_width > (int)pVideoPicture->iWidth - 10)
+ pVideoPicture->iWidth = m_pCodecContext->coded_width;
+
+ if(m_pCodecContext->coded_height && m_pCodecContext->coded_height < (int)pVideoPicture->iHeight
+ && m_pCodecContext->coded_height > (int)pVideoPicture->iHeight - 10)
+ pVideoPicture->iHeight = m_pCodecContext->coded_height;
+
+ double aspect_ratio;
+
+ /* use variable in the frame */
+ AVRational pixel_aspect = m_pFrame->sample_aspect_ratio;
+
+ if (pixel_aspect.num == 0)
+ aspect_ratio = 0;
+ else
+ aspect_ratio = av_q2d(pixel_aspect) * pVideoPicture->iWidth / pVideoPicture->iHeight;
+
+ if (aspect_ratio <= 0.0)
+ aspect_ratio = static_cast<double>(pVideoPicture->iWidth) / pVideoPicture->iHeight;
+
+ if (m_DAR != aspect_ratio)
+ {
+ m_DAR = aspect_ratio;
+ m_processInfo.SetVideoDAR(static_cast<float>(m_DAR));
+ }
+
+ /* XXX: we suppose the screen has a 1.0 pixel ratio */ // CDVDVideo will compensate it.
+ pVideoPicture->iDisplayHeight = pVideoPicture->iHeight;
+ pVideoPicture->iDisplayWidth = ((int)RINT(pVideoPicture->iHeight * aspect_ratio)) & -3;
+ if (pVideoPicture->iDisplayWidth > pVideoPicture->iWidth)
+ {
+ pVideoPicture->iDisplayWidth = pVideoPicture->iWidth;
+ pVideoPicture->iDisplayHeight = ((int)RINT(pVideoPicture->iWidth / aspect_ratio)) & -3;
+ }
+
+
+ pVideoPicture->pts = DVD_NOPTS_VALUE;
+
+ AVDictionaryEntry * entry = av_dict_get(m_pFrame->metadata, "stereo_mode", NULL, 0);
+ if(entry && entry->value)
+ {
+ pVideoPicture->stereoMode = (const char*)entry->value;
+ }
+ else
+ pVideoPicture->stereoMode.clear();
+
+ pVideoPicture->iRepeatPicture = 0.5 * m_pFrame->repeat_pict;
+ pVideoPicture->iFlags = 0;
+ pVideoPicture->iFlags |= m_pFrame->interlaced_frame ? DVP_FLAG_INTERLACED : 0;
+ pVideoPicture->iFlags |= m_pFrame->top_field_first ? DVP_FLAG_TOP_FIELD_FIRST: 0;
+
+ if (m_codecControlFlags & DVD_CODEC_CTRL_DROP)
+ {
+ pVideoPicture->iFlags |= DVP_FLAG_DROPPED;
+ }
+
+ pVideoPicture->pixelFormat = m_pCodecContext->sw_pix_fmt;
+
+ pVideoPicture->chroma_position = m_pCodecContext->chroma_sample_location;
+ pVideoPicture->color_primaries = m_pCodecContext->color_primaries == AVCOL_PRI_UNSPECIFIED ? m_hints.colorPrimaries : m_pCodecContext->color_primaries;
+ pVideoPicture->color_transfer = m_pCodecContext->color_trc == AVCOL_TRC_UNSPECIFIED ? m_hints.colorTransferCharacteristic : m_pCodecContext->color_trc;
+ pVideoPicture->color_space = m_pCodecContext->colorspace == AVCOL_SPC_UNSPECIFIED ? m_hints.colorSpace : m_pCodecContext->colorspace;
+ pVideoPicture->colorBits = 8;
+
+ // determine how number of bits of encoded video
+ if (m_pCodecContext->pix_fmt == AV_PIX_FMT_YUV420P12)
+ pVideoPicture->colorBits = 12;
+ else if (m_pCodecContext->pix_fmt == AV_PIX_FMT_YUV420P10)
+ pVideoPicture->colorBits = 10;
+ else if (m_pCodecContext->codec_id == AV_CODEC_ID_HEVC &&
+ m_pCodecContext->profile == FF_PROFILE_HEVC_MAIN_10)
+ pVideoPicture->colorBits = 10;
+ else if (m_pCodecContext->codec_id == AV_CODEC_ID_H264 &&
+ (m_pCodecContext->profile == FF_PROFILE_H264_HIGH_10||
+ m_pCodecContext->profile == FF_PROFILE_H264_HIGH_10_INTRA))
+ pVideoPicture->colorBits = 10;
+ else if (m_pCodecContext->codec_id == AV_CODEC_ID_VP9 &&
+ (m_pCodecContext->profile == FF_PROFILE_VP9_2 ||
+ m_pCodecContext->profile == FF_PROFILE_VP9_3))
+ pVideoPicture->colorBits = 10;
+
+ if (m_pCodecContext->color_range == AVCOL_RANGE_JPEG ||
+ m_pCodecContext->pix_fmt == AV_PIX_FMT_YUVJ420P)
+ pVideoPicture->color_range = 1;
+ else
+ pVideoPicture->color_range = m_hints.colorRange == AVCOL_RANGE_JPEG ? 1 : 0;
+
+ //! @todo: ffmpeg doesn't seem like they know how they want to handle this.
+ // av_frame_get_qp_table is deprecated but there doesn't seem to be a valid
+ // replacement. the following is basically what av_frame_get_qp_table does
+ // internally so we can avoid the deprecation warning however it may still
+ // break in the future because some definitions are guarded and may be removed.
+
+ pVideoPicture->qp_table = nullptr;
+ pVideoPicture->qstride = 0;
+ pVideoPicture->qscale_type = 0;
+
+ AVFrameSideData* sd;
+ sd = av_frame_get_side_data(m_pFrame, AV_FRAME_DATA_QP_TABLE_PROPERTIES);
+ if (sd)
+ {
+ struct qp_properties
+ {
+ int stride;
+ int type;
+ };
+
+ auto qp = reinterpret_cast<qp_properties*>(sd->data);
+
+ sd = av_frame_get_side_data(m_pFrame, AV_FRAME_DATA_QP_TABLE_DATA);
+ if (sd && sd->buf && qp)
+ {
+ // this seems wrong but it's what ffmpeg does internally
+ pVideoPicture->qp_table = reinterpret_cast<int8_t*>(sd->buf->data);
+ pVideoPicture->qstride = qp->stride;
+ pVideoPicture->qscale_type = qp->type;
+ }
+ }
+
+ pVideoPicture->pict_type = m_pFrame->pict_type;
+
+ // metadata
+ pVideoPicture->hasDisplayMetadata = false;
+ pVideoPicture->hasLightMetadata = false;
+ sd = av_frame_get_side_data(m_pFrame, AV_FRAME_DATA_MASTERING_DISPLAY_METADATA);
+ if (sd)
+ {
+ pVideoPicture->displayMetadata = *(AVMasteringDisplayMetadata *)sd->data;
+ pVideoPicture->hasDisplayMetadata = true;
+ }
+ else if (m_hints.masteringMetadata)
+ {
+ pVideoPicture->displayMetadata = *m_hints.masteringMetadata.get();
+ pVideoPicture->hasDisplayMetadata = true;
+ }
+ sd = av_frame_get_side_data(m_pFrame, AV_FRAME_DATA_CONTENT_LIGHT_LEVEL);
+ if (sd)
+ {
+ pVideoPicture->lightMetadata = *(AVContentLightMetadata *)sd->data;
+ pVideoPicture->hasLightMetadata = true;
+ }
+ else if (m_hints.contentLightMetadata)
+ {
+ pVideoPicture->lightMetadata = *m_hints.contentLightMetadata.get();
+ pVideoPicture->hasLightMetadata = true;
+ }
+
+ if (pVideoPicture->iRepeatPicture)
+ pVideoPicture->dts = DVD_NOPTS_VALUE;
+ else
+ pVideoPicture->dts = m_dts;
+
+ m_dts = DVD_NOPTS_VALUE;
+
+ int64_t bpts = m_pFrame->best_effort_timestamp;
+ if (bpts != AV_NOPTS_VALUE)
+ {
+ pVideoPicture->pts = (double)bpts * DVD_TIME_BASE / AV_TIME_BASE;
+ if (pVideoPicture->pts == m_decoderPts)
+ {
+ pVideoPicture->iRepeatPicture = -0.5;
+ pVideoPicture->pts = DVD_NOPTS_VALUE;
+ pVideoPicture->dts = DVD_NOPTS_VALUE;
+ }
+ }
+ else
+ pVideoPicture->pts = DVD_NOPTS_VALUE;
+
+ if (pVideoPicture->pts != DVD_NOPTS_VALUE)
+ m_decoderPts = pVideoPicture->pts;
+
+ if (m_requestSkipDeint)
+ {
+ pVideoPicture->iFlags |= DVD_CODEC_CTRL_SKIPDEINT;
+ m_skippedDeint++;
+ }
+
+ m_requestSkipDeint = false;
+ pVideoPicture->iFlags |= m_codecControlFlags;
+
+ return true;
+}
+
+int CDVDVideoCodecFFmpeg::FilterOpen(const std::string& filters, bool scale)
+{
+ int result;
+
+ if (m_pFilterGraph)
+ FilterClose();
+
+ if (filters.empty() && !scale)
+ return 0;
+
+ if (m_pHardware)
+ {
+ CLog::Log(LOGWARNING, "CDVDVideoCodecFFmpeg::FilterOpen - skipped opening filters on hardware decode");
+ return 0;
+ }
+
+ if (!(m_pFilterGraph = avfilter_graph_alloc()))
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecFFmpeg::FilterOpen - unable to alloc filter graph");
+ return -1;
+ }
+
+ const AVFilter* srcFilter = avfilter_get_by_name("buffer");
+ const AVFilter* outFilter = avfilter_get_by_name("buffersink"); // should be last filter in the graph for now
+
+ std::string args = StringUtils::Format(
+ "{}:{}:{}:{}:{}:{}:{}", m_pCodecContext->width, m_pCodecContext->height,
+ m_pCodecContext->pix_fmt, m_pCodecContext->time_base.num ? m_pCodecContext->time_base.num : 1,
+ m_pCodecContext->time_base.num ? m_pCodecContext->time_base.den : 1,
+ m_pCodecContext->sample_aspect_ratio.num != 0 ? m_pCodecContext->sample_aspect_ratio.num : 1,
+ m_pCodecContext->sample_aspect_ratio.num != 0 ? m_pCodecContext->sample_aspect_ratio.den : 1);
+
+ if ((result = avfilter_graph_create_filter(&m_pFilterIn, srcFilter, "src", args.c_str(), NULL, m_pFilterGraph)) < 0)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecFFmpeg::FilterOpen - avfilter_graph_create_filter: src");
+ return result;
+ }
+
+ if ((result = avfilter_graph_create_filter(&m_pFilterOut, outFilter, "out", NULL, NULL, m_pFilterGraph)) < 0)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecFFmpeg::FilterOpen - avfilter_graph_create_filter: out");
+ return result;
+ }
+ if ((result = av_opt_set_int_list(m_pFilterOut, "pix_fmts", &m_formats[0], AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN)) < 0)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecFFmpeg::FilterOpen - failed settings pix formats");
+ return result;
+ }
+
+ if (!filters.empty())
+ {
+ AVFilterInOut* outputs = avfilter_inout_alloc();
+ AVFilterInOut* inputs = avfilter_inout_alloc();
+
+ outputs->name = av_strdup("in");
+ outputs->filter_ctx = m_pFilterIn;
+ outputs->pad_idx = 0;
+ outputs->next = nullptr;
+
+ inputs->name = av_strdup("out");
+ inputs->filter_ctx = m_pFilterOut;
+ inputs->pad_idx = 0;
+ inputs->next = nullptr;
+
+ result = avfilter_graph_parse_ptr(m_pFilterGraph, m_filters.c_str(), &inputs, &outputs, NULL);
+ avfilter_inout_free(&outputs);
+ avfilter_inout_free(&inputs);
+
+ if (result < 0)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecFFmpeg::FilterOpen - avfilter_graph_parse");
+ return result;
+ }
+
+ if (filters.compare(0,5,"yadif") == 0)
+ {
+ m_processInfo.SetVideoDeintMethod(filters);
+ }
+ else
+ {
+ m_processInfo.SetVideoDeintMethod("none");
+ }
+ }
+ else
+ {
+ if ((result = avfilter_link(m_pFilterIn, 0, m_pFilterOut, 0)) < 0)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecFFmpeg::FilterOpen - avfilter_link");
+ return result;
+ }
+
+ m_processInfo.SetVideoDeintMethod("none");
+ }
+
+ if ((result = avfilter_graph_config(m_pFilterGraph, nullptr)) < 0)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecFFmpeg::FilterOpen - avfilter_graph_config");
+ return result;
+ }
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGVIDEO))
+ {
+ char* graphDump = avfilter_graph_dump(m_pFilterGraph, nullptr);
+ if (graphDump)
+ {
+ CLog::Log(LOGDEBUG, "CDVDVideoCodecFFmpeg::FilterOpen - Final filter graph:\n{}", graphDump);
+ av_freep(&graphDump);
+ }
+ }
+
+ m_filterEof = false;
+ return result;
+}
+
+void CDVDVideoCodecFFmpeg::FilterClose()
+{
+ if (m_pFilterGraph)
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CDVDVideoCodecFFmpeg::FilterClose - Freeing filter graph");
+ avfilter_graph_free(&m_pFilterGraph);
+
+ // Disposed by above code
+ m_pFilterIn = nullptr;
+ m_pFilterOut = nullptr;
+ }
+}
+
+CDVDVideoCodec::VCReturn CDVDVideoCodecFFmpeg::FilterProcess(AVFrame* frame)
+{
+ int result;
+
+ if (frame || (m_codecControlFlags & DVD_CODEC_CTRL_DRAIN))
+ {
+ result = av_buffersrc_add_frame(m_pFilterIn, frame);
+ if (result < 0)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecFFmpeg::FilterProcess - av_buffersrc_add_frame");
+ return VC_ERROR;
+ }
+ }
+
+ result = av_buffersink_get_frame(m_pFilterOut, m_pFilterFrame);
+
+ if (result == AVERROR(EAGAIN))
+ return VC_BUFFER;
+ else if (result == AVERROR_EOF)
+ {
+ result = av_buffersink_get_frame(m_pFilterOut, m_pFilterFrame);
+ m_filterEof = true;
+ if (result < 0)
+ return VC_BUFFER;
+ }
+ else if (result < 0)
+ {
+ CLog::Log(LOGERROR, "CDVDVideoCodecFFmpeg::FilterProcess - av_buffersink_get_frame");
+ return VC_ERROR;
+ }
+
+ av_frame_unref(m_pFrame);
+ av_frame_move_ref(m_pFrame, m_pFilterFrame);
+
+ return VC_PICTURE;
+}
+
+unsigned CDVDVideoCodecFFmpeg::GetConvergeCount()
+{
+ return m_iLastKeyframe;
+}
+
+unsigned CDVDVideoCodecFFmpeg::GetAllowedReferences()
+{
+ if(m_pHardware)
+ return m_pHardware->GetAllowedReferences();
+ else
+ return 0;
+}
+
+bool CDVDVideoCodecFFmpeg::GetCodecStats(double &pts, int &droppedFrames, int &skippedPics)
+{
+ if (m_decoderPts != DVD_NOPTS_VALUE)
+ pts = m_decoderPts;
+ else
+ pts = m_dts;
+
+ if (m_droppedFrames)
+ droppedFrames = m_droppedFrames;
+ else
+ droppedFrames = -1;
+ m_droppedFrames = 0;
+
+ if (m_skippedDeint)
+ skippedPics = m_skippedDeint;
+ else
+ skippedPics = -1;
+ m_skippedDeint = 0;
+
+ return true;
+}
+
+void CDVDVideoCodecFFmpeg::SetCodecControl(int flags)
+{
+ m_codecControlFlags = flags;
+
+ if (m_pCodecContext)
+ {
+ bool bDrop = (flags & DVD_CODEC_CTRL_DROP_ANY) != 0;
+ if (bDrop && m_pHardware && m_pHardware->CanSkipDeint())
+ {
+ m_requestSkipDeint = true;
+ bDrop = false;
+ }
+ else
+ m_requestSkipDeint = false;
+
+ if (bDrop)
+ {
+ m_pCodecContext->skip_frame = AVDISCARD_NONREF;
+ m_pCodecContext->skip_idct = AVDISCARD_NONREF;
+ m_pCodecContext->skip_loop_filter = AVDISCARD_NONREF;
+ }
+ else
+ {
+ m_pCodecContext->skip_frame = AVDISCARD_DEFAULT;
+ m_pCodecContext->skip_idct = AVDISCARD_DEFAULT;
+ m_pCodecContext->skip_loop_filter = AVDISCARD_DEFAULT;
+ }
+ }
+
+ if (m_pHardware)
+ m_pHardware->SetCodecControl(flags);
+}
+
+void CDVDVideoCodecFFmpeg::SetHardware(IHardwareDecoder* hardware)
+{
+ if (m_pHardware)
+ m_pHardware->Release();
+ m_pHardware = hardware;
+ UpdateName();
+}
+
+IHardwareDecoder* CDVDVideoCodecFFmpeg::GetHWAccel()
+{
+ return m_pHardware;
+}
+
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecFFmpeg.h b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecFFmpeg.h
new file mode 100644
index 0000000..86c83cd
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecFFmpeg.h
@@ -0,0 +1,117 @@
+/*
+ * 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/VideoPlayer/DVDCodecs/DVDCodecs.h"
+#include "cores/VideoPlayer/DVDStreamInfo.h"
+#include "DVDVideoCodec.h"
+#include "DVDVideoPPFFmpeg.h"
+#include <string>
+#include <vector>
+
+extern "C" {
+#include <libavfilter/avfilter.h>
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libavutil/avutil.h>
+#include <libswscale/swscale.h>
+#include <libpostproc/postprocess.h>
+}
+
+class CVideoBufferPoolFFmpeg;
+
+class CDVDVideoCodecFFmpeg : public CDVDVideoCodec, public ICallbackHWAccel
+{
+public:
+ explicit CDVDVideoCodecFFmpeg(CProcessInfo &processInfo);
+ ~CDVDVideoCodecFFmpeg() override;
+ bool Open(CDVDStreamInfo &hints, CDVDCodecOptions &options) override;
+ bool AddData(const DemuxPacket &packet) override;
+ void Reset() override;
+ void Reopen() override;
+ CDVDVideoCodec::VCReturn GetPicture(VideoPicture* pVideoPicture) override;
+ const char* GetName() override { return m_name.c_str(); }; // m_name is never changed after open
+ unsigned GetConvergeCount() override;
+ unsigned GetAllowedReferences() override;
+ bool GetCodecStats(double &pts, int &droppedFrames, int &skippedPics) override;
+ void SetCodecControl(int flags) override;
+
+ IHardwareDecoder* GetHWAccel() override;
+ bool GetPictureCommon(VideoPicture* pVideoPicture) override;
+
+protected:
+ void Dispose();
+ static enum AVPixelFormat GetFormat(struct AVCodecContext * avctx, const AVPixelFormat * fmt);
+
+ int FilterOpen(const std::string& filters, bool scale);
+ void FilterClose();
+ CDVDVideoCodec::VCReturn FilterProcess(AVFrame* frame);
+ void SetFilters();
+ void UpdateName();
+ bool SetPictureParams(VideoPicture* pVideoPicture);
+
+ bool HasHardware() { return m_pHardware != nullptr; }
+ void SetHardware(IHardwareDecoder *hardware);
+
+ AVFrame* m_pFrame = nullptr;;
+ AVFrame* m_pDecodedFrame = nullptr;;
+ AVCodecContext* m_pCodecContext = nullptr;;
+ std::shared_ptr<CVideoBufferPoolFFmpeg> m_videoBufferPool;
+
+ std::string m_filters;
+ std::string m_filters_next;
+ AVFilterGraph* m_pFilterGraph = nullptr;
+ AVFilterContext* m_pFilterIn = nullptr;
+ AVFilterContext* m_pFilterOut = nullptr;;
+ AVFrame* m_pFilterFrame = nullptr;;
+ bool m_filterEof = false;
+ bool m_eof = false;
+
+ CDVDVideoPPFFmpeg m_postProc;
+
+ int m_iPictureWidth = 0;
+ int m_iPictureHeight = 0;
+ int m_iScreenWidth = 0;
+ int m_iScreenHeight = 0;
+ int m_iOrientation = 0;// orientation of the video in degrees counter clockwise
+
+ std::string m_name;
+ int m_decoderState;
+ IHardwareDecoder *m_pHardware = nullptr;
+ int m_iLastKeyframe = 0;
+ double m_dts = DVD_NOPTS_VALUE;
+ bool m_started = false;
+ bool m_startedInput = false;
+ std::vector<AVPixelFormat> m_formats;
+ double m_decoderPts = DVD_NOPTS_VALUE;
+ int m_skippedDeint = 0;
+ int m_droppedFrames = 0;
+ bool m_requestSkipDeint = false;
+ int m_codecControlFlags = 0;
+ bool m_interlaced = false;
+ double m_DAR = 1.0;
+ CDVDStreamInfo m_hints;
+ CDVDCodecOptions m_options;
+
+ struct CDropControl
+ {
+ CDropControl();
+ void Reset(bool init);
+ void Process(int64_t pts, bool drop);
+
+ int64_t m_lastPTS;
+ int64_t m_diffPTS;
+ int m_count;
+ enum
+ {
+ INIT,
+ VALID
+ } m_state;
+ } m_dropCtrl;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoPPFFmpeg.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoPPFFmpeg.cpp
new file mode 100644
index 0000000..a98fbb1
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoPPFFmpeg.cpp
@@ -0,0 +1,130 @@
+/*
+ * 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 "DVDVideoPPFFmpeg.h"
+#include "utils/log.h"
+#include "cores/FFmpeg.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+
+extern "C" {
+#include <libavutil/mem.h>
+}
+
+CDVDVideoPPFFmpeg::CDVDVideoPPFFmpeg(CProcessInfo &processInfo):
+ m_sType(""), m_processInfo(processInfo)
+{
+ m_pMode = m_pContext = NULL;
+ m_iInitWidth = m_iInitHeight = 0;
+ m_deinterlace = false;
+}
+
+CDVDVideoPPFFmpeg::~CDVDVideoPPFFmpeg()
+{
+ Dispose();
+}
+
+void CDVDVideoPPFFmpeg::Dispose()
+{
+ if (m_pMode)
+ {
+ pp_free_mode(m_pMode);
+ m_pMode = NULL;
+ }
+ if(m_pContext)
+ {
+ pp_free_context(m_pContext);
+ m_pContext = NULL;
+ }
+
+ m_iInitWidth = 0;
+ m_iInitHeight = 0;
+}
+
+bool CDVDVideoPPFFmpeg::CheckInit(int iWidth, int iHeight)
+{
+ if (m_iInitWidth != iWidth || m_iInitHeight != iHeight)
+ {
+ if (m_pContext || m_pMode)
+ {
+ Dispose();
+ }
+
+ m_pContext = pp_get_context(iWidth, iHeight, PPCPUFlags() | PP_FORMAT_420);
+
+ m_iInitWidth = iWidth;
+ m_iInitHeight = iHeight;
+
+ m_pMode = pp_get_mode_by_name_and_quality(m_sType.c_str(), PP_QUALITY_MAX);
+ }
+
+ if (m_pMode)
+ return true;
+ else
+ return false;
+}
+
+void CDVDVideoPPFFmpeg::SetType(const std::string& mType, bool deinterlace)
+{
+ m_deinterlace = deinterlace;
+
+ if (mType == m_sType)
+ return;
+
+ m_sType = mType;
+
+ if(m_pContext || m_pMode)
+ Dispose();
+}
+
+void CDVDVideoPPFFmpeg::Process(VideoPicture* pPicture)
+{
+ VideoPicture* pSource = pPicture;
+ CVideoBuffer *videoBuffer;
+
+ if (pSource->videoBuffer->GetFormat() != AV_PIX_FMT_YUV420P)
+ return;
+
+ if (!CheckInit(pSource->iWidth, pSource->iHeight))
+ {
+ CLog::Log(LOGERROR, "Initialization of ffmpeg postprocessing failed");
+ return;
+ }
+
+ uint8_t* srcPlanes[YuvImage::MAX_PLANES], *dstPlanes[YuvImage::MAX_PLANES];
+ int srcStrides[YuvImage::MAX_PLANES]{};
+ pSource->videoBuffer->GetPlanes(srcPlanes);
+ pSource->videoBuffer->GetStrides(srcStrides);
+
+ videoBuffer = m_processInfo.GetVideoBufferManager().Get(AV_PIX_FMT_YUV420P,
+ srcStrides[0] * pPicture->iHeight +
+ srcStrides[1] * pPicture->iHeight, nullptr);
+ if (!videoBuffer)
+ {
+ return;
+ }
+
+ videoBuffer->SetDimensions(pPicture->iWidth, pPicture->iHeight, srcStrides);
+ videoBuffer->GetPlanes(dstPlanes);
+ //! @bug libpostproc isn't const correct
+ pp_postprocess(const_cast<const uint8_t **>(srcPlanes), srcStrides,
+ dstPlanes, srcStrides,
+ pSource->iWidth, pSource->iHeight,
+ pSource->qp_table, pSource->qstride,
+ m_pMode, m_pContext,
+ pSource->pict_type | pSource->qscale_type ? PP_PICT_TYPE_QP2 : 0);
+
+
+ pPicture->SetParams(*pSource);
+ if (pPicture->videoBuffer)
+ pPicture->videoBuffer->Release();
+ pPicture->videoBuffer = videoBuffer;
+
+ if (m_deinterlace)
+ pPicture->iFlags &= ~DVP_FLAG_INTERLACED;
+}
+
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoPPFFmpeg.h b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoPPFFmpeg.h
new file mode 100644
index 0000000..386a4c7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DVDVideoPPFFmpeg.h
@@ -0,0 +1,42 @@
+/*
+ * 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 "DVDVideoCodec.h"
+
+#include <string>
+
+class CProcessInfo;
+
+class CDVDVideoPPFFmpeg
+{
+public:
+
+ explicit CDVDVideoPPFFmpeg(CProcessInfo &processInfo);
+ ~CDVDVideoPPFFmpeg();
+
+ void SetType(const std::string& mType, bool deinterlace);
+ void Process(VideoPicture *pPicture);
+
+protected:
+ std::string m_sType;
+ CProcessInfo &m_processInfo;
+
+ void *m_pContext;
+ void *m_pMode;
+ bool m_deinterlace;
+
+ void Dispose();
+
+ int m_iInitWidth, m_iInitHeight;
+ bool CheckInit(int iWidth, int iHeight);
+ bool CheckFrameBuffer(const VideoPicture* pSource);
+};
+
+
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DXVA.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DXVA.cpp
new file mode 100644
index 0000000..74a1eb3
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DXVA.cpp
@@ -0,0 +1,1573 @@
+/*
+ * 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.
+ */
+
+// setting that here because otherwise SampleFormat is defined to AVSampleFormat
+// which we don't use here
+#define FF_API_OLD_SAMPLE_FMT 0
+
+#define LIMIT_VIDEO_MEMORY_4K 2960ull
+
+#include "DXVA.h"
+
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDCodecUtils.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderManager.h"
+#include "rendering/dx/DeviceResources.h"
+#include "rendering/dx/RenderContext.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CPUInfo.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#include <Windows.h>
+#include <d3d11_4.h>
+#include <dxva.h>
+#include <initguid.h>
+
+using namespace DXVA;
+using namespace Microsoft::WRL;
+using namespace std::chrono_literals;
+
+DEFINE_GUID(DXVADDI_Intel_ModeH264_A, 0x604F8E64,0x4951,0x4c54,0x88,0xFE,0xAB,0xD2,0x5C,0x15,0xB3,0xD6);
+DEFINE_GUID(DXVADDI_Intel_ModeH264_C, 0x604F8E66,0x4951,0x4c54,0x88,0xFE,0xAB,0xD2,0x5C,0x15,0xB3,0xD6);
+DEFINE_GUID(DXVADDI_Intel_ModeH264_E, 0x604F8E68,0x4951,0x4c54,0x88,0xFE,0xAB,0xD2,0x5C,0x15,0xB3,0xD6);
+DEFINE_GUID(DXVADDI_Intel_ModeVC1_E, 0xBCC5DB6D,0xA2B6,0x4AF0,0xAC,0xE4,0xAD,0xB1,0xF7,0x87,0xBC,0x89);
+DEFINE_GUID(DXVA_ModeH264_VLD_NoFGT_Flash, 0x4245F676,0x2BBC,0x4166,0xa0,0xBB,0x54,0xE7,0xB8,0x49,0xC3,0x80);
+DEFINE_GUID(DXVA_Intel_VC1_ClearVideo_2, 0xE07EC519,0xE651,0x4CD6,0xAC,0x84,0x13,0x70,0xCC,0xEE,0xC8,0x51);
+
+// redefine DXVA_NoEncrypt with other macro, solves unresolved external symbol linker error
+#ifndef DXVA_NoEncrypt
+DEFINE_GUID(DXVA_NoEncrypt, 0x1b81beD0, 0xa0c7, 0x11d3, 0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5);
+#endif
+
+static const int PROFILES_MPEG2_SIMPLE[] = { FF_PROFILE_MPEG2_SIMPLE,
+ FF_PROFILE_UNKNOWN };
+static const int PROFILES_MPEG2_MAIN[] = { FF_PROFILE_MPEG2_SIMPLE,
+ FF_PROFILE_MPEG2_MAIN,
+ FF_PROFILE_UNKNOWN };
+static const int PROFILES_H264_HIGH[] = { FF_PROFILE_H264_BASELINE,
+ FF_PROFILE_H264_CONSTRAINED_BASELINE,
+ FF_PROFILE_H264_MAIN,
+ FF_PROFILE_H264_HIGH,
+ FF_PROFILE_UNKNOWN };
+static const int PROFILES_HEVC_MAIN[] = { FF_PROFILE_HEVC_MAIN,
+ FF_PROFILE_UNKNOWN };
+static const int PROFILES_HEVC_MAIN10[] = { FF_PROFILE_HEVC_MAIN,
+ FF_PROFILE_HEVC_MAIN_10,
+ FF_PROFILE_UNKNOWN };
+static const int PROFILES_VP9_0[] = { FF_PROFILE_VP9_0,
+ FF_PROFILE_UNKNOWN };
+static const int PROFILES_VP9_10_2[] = { FF_PROFILE_VP9_2,
+ FF_PROFILE_UNKNOWN };
+
+typedef struct
+{
+ const char* name;
+ const GUID* guid;
+ int codec;
+ const int* profiles;
+} dxva2_mode_t;
+
+/* XXX Preferred modes must come first */
+static const std::vector<dxva2_mode_t> dxva2_modes = {
+ { "MPEG2 variable-length decoder", &D3D11_DECODER_PROFILE_MPEG2_VLD, AV_CODEC_ID_MPEG2VIDEO, PROFILES_MPEG2_MAIN },
+ { "MPEG1/2 variable-length decoder", &D3D11_DECODER_PROFILE_MPEG2and1_VLD, AV_CODEC_ID_MPEG2VIDEO, PROFILES_MPEG2_MAIN },
+ { "MPEG2 motion compensation", &D3D11_DECODER_PROFILE_MPEG2_MOCOMP, 0, nullptr },
+ { "MPEG2 inverse discrete cosine transform", &D3D11_DECODER_PROFILE_MPEG2_IDCT, 0, nullptr},
+
+ { "MPEG-1 variable-length decoder", &D3D11_DECODER_PROFILE_MPEG1_VLD, 0, nullptr },
+
+ { "H.264 variable-length decoder, film grain technology", &D3D11_DECODER_PROFILE_H264_VLD_FGT, AV_CODEC_ID_H264, PROFILES_H264_HIGH },
+ { "H.264 variable-length decoder, no film grain technology (Intel ClearVideo)", &DXVADDI_Intel_ModeH264_E, AV_CODEC_ID_H264, PROFILES_H264_HIGH },
+ { "H.264 variable-length decoder, no film grain technology", &D3D11_DECODER_PROFILE_H264_VLD_NOFGT, AV_CODEC_ID_H264, PROFILES_H264_HIGH },
+ { "H.264 variable-length decoder, no film grain technology, FMO/ASO", &D3D11_DECODER_PROFILE_H264_VLD_WITHFMOASO_NOFGT, AV_CODEC_ID_H264, PROFILES_H264_HIGH },
+ { "H.264 variable-length decoder, no film grain technology, Flash", &DXVA_ModeH264_VLD_NoFGT_Flash, AV_CODEC_ID_H264, PROFILES_H264_HIGH },
+
+ { "H.264 inverse discrete cosine transform, film grain technology", &D3D11_DECODER_PROFILE_H264_IDCT_FGT, 0, nullptr },
+ { "H.264 inverse discrete cosine transform, no film grain technology", &D3D11_DECODER_PROFILE_H264_IDCT_NOFGT, 0, nullptr },
+ { "H.264 inverse discrete cosine transform, no film grain technology (Intel)", &DXVADDI_Intel_ModeH264_C, 0, nullptr },
+
+ { "H.264 motion compensation, film grain technology", &D3D11_DECODER_PROFILE_H264_MOCOMP_FGT, 0, nullptr },
+ { "H.264 motion compensation, no film grain technology", &D3D11_DECODER_PROFILE_H264_MOCOMP_NOFGT, 0, nullptr },
+ { "H.264 motion compensation, no film grain technology (Intel)", &DXVADDI_Intel_ModeH264_A, 0, nullptr },
+
+ { "H.264 stereo high profile, mbs flag set", &D3D11_DECODER_PROFILE_H264_VLD_STEREO_PROGRESSIVE_NOFGT, 0, nullptr },
+ { "H.264 stereo high profile", &D3D11_DECODER_PROFILE_H264_VLD_STEREO_NOFGT, 0, nullptr },
+ { "H.264 multi-view high profile", &D3D11_DECODER_PROFILE_H264_VLD_MULTIVIEW_NOFGT, 0, nullptr },
+
+ { "Windows Media Video 8 motion compensation", &D3D11_DECODER_PROFILE_WMV8_MOCOMP, 0, nullptr },
+ { "Windows Media Video 8 post processing", &D3D11_DECODER_PROFILE_WMV8_POSTPROC, 0, nullptr },
+
+ { "Windows Media Video 9 inverse discrete cosine transform", &D3D11_DECODER_PROFILE_WMV9_IDCT, 0, nullptr },
+ { "Windows Media Video 9 motion compensation", &D3D11_DECODER_PROFILE_WMV9_MOCOMP, 0, nullptr },
+ { "Windows Media Video 9 post processing", &D3D11_DECODER_PROFILE_WMV9_POSTPROC, 0, nullptr },
+
+ { "VC-1 variable-length decoder", &D3D11_DECODER_PROFILE_VC1_VLD, AV_CODEC_ID_VC1, nullptr },
+ { "VC-1 variable-length decoder", &D3D11_DECODER_PROFILE_VC1_VLD, AV_CODEC_ID_WMV3, nullptr },
+ { "VC-1 variable-length decoder 2010", &D3D11_DECODER_PROFILE_VC1_D2010, AV_CODEC_ID_VC1, nullptr },
+ { "VC-1 variable-length decoder 2010", &D3D11_DECODER_PROFILE_VC1_D2010, AV_CODEC_ID_WMV3, nullptr },
+ { "VC-1 variable-length decoder 2 (Intel)", &DXVA_Intel_VC1_ClearVideo_2, 0, nullptr },
+ { "VC-1 variable-length decoder (Intel)", &DXVADDI_Intel_ModeVC1_E, 0, nullptr },
+
+ { "VC-1 inverse discrete cosine transform", &D3D11_DECODER_PROFILE_VC1_IDCT, 0, nullptr },
+ { "VC-1 motion compensation", &D3D11_DECODER_PROFILE_VC1_MOCOMP, 0, nullptr },
+ { "VC-1 post processing", &D3D11_DECODER_PROFILE_VC1_POSTPROC, 0, nullptr },
+
+ { "HEVC variable-length decoder, main", &D3D11_DECODER_PROFILE_HEVC_VLD_MAIN, AV_CODEC_ID_HEVC, PROFILES_HEVC_MAIN },
+ { "HEVC variable-length decoder, main10", &D3D11_DECODER_PROFILE_HEVC_VLD_MAIN10, AV_CODEC_ID_HEVC, PROFILES_HEVC_MAIN10 },
+
+ { "VP9 variable-length decoder, Profile 0", &D3D11_DECODER_PROFILE_VP9_VLD_PROFILE0, AV_CODEC_ID_VP9, PROFILES_VP9_0 },
+ { "VP9 variable-length decoder, 10bit, profile 2", &D3D11_DECODER_PROFILE_VP9_VLD_10BIT_PROFILE2, AV_CODEC_ID_VP9, PROFILES_VP9_10_2 },
+};
+
+// Preferred targets must be first
+static const DXGI_FORMAT render_targets_dxgi[] = {
+ DXGI_FORMAT_NV12,
+ DXGI_FORMAT_P010,
+ DXGI_FORMAT_P016,
+ DXGI_FORMAT_UNKNOWN
+};
+
+// List of PCI Device ID of ATI cards with UVD or UVD+ decoding block.
+static DWORD UVDDeviceID [] = {
+ 0x95C0, // ATI Radeon HD 3400 Series (and others)
+ 0x95C5, // ATI Radeon HD 3400 Series (and others)
+ 0x95C4, // ATI Radeon HD 3400 Series (and others)
+ 0x94C3, // ATI Radeon HD 3410
+ 0x9589, // ATI Radeon HD 3600 Series (and others)
+ 0x9598, // ATI Radeon HD 3600 Series (and others)
+ 0x9591, // ATI Radeon HD 3600 Series (and others)
+ 0x9501, // ATI Radeon HD 3800 Series (and others)
+ 0x9505, // ATI Radeon HD 3800 Series (and others)
+ 0x9507, // ATI Radeon HD 3830
+ 0x9513, // ATI Radeon HD 3850 X2
+ 0x950F, // ATI Radeon HD 3850 X2
+ 0x0000
+};
+
+// List of PCI Device ID of nVidia cards with the macroblock width issue. More or less the VP3 block.
+// Per NVIDIA Accelerated Linux Graphics Driver, Appendix A Supported NVIDIA GPU Products, cards with note 1.
+static DWORD VP3DeviceID [] = {
+ 0x06E0, // GeForce 9300 GE
+ 0x06E1, // GeForce 9300 GS
+ 0x06E2, // GeForce 8400
+ 0x06E4, // GeForce 8400 GS
+ 0x06E5, // GeForce 9300M GS
+ 0x06E6, // GeForce G100
+ 0x06E8, // GeForce 9200M GS
+ 0x06E9, // GeForce 9300M GS
+ 0x06EC, // GeForce G 105M
+ 0x06EF, // GeForce G 103M
+ 0x06F1, // GeForce G105M
+ 0x0844, // GeForce 9100M G
+ 0x0845, // GeForce 8200M G
+ 0x0846, // GeForce 9200
+ 0x0847, // GeForce 9100
+ 0x0848, // GeForce 8300
+ 0x0849, // GeForce 8200
+ 0x084A, // nForce 730a
+ 0x084B, // GeForce 9200
+ 0x084C, // nForce 980a/780a SLI
+ 0x084D, // nForce 750a SLI
+ 0x0860, // GeForce 9400
+ 0x0861, // GeForce 9400
+ 0x0862, // GeForce 9400M G
+ 0x0863, // GeForce 9400M
+ 0x0864, // GeForce 9300
+ 0x0865, // ION
+ 0x0866, // GeForce 9400M G
+ 0x0867, // GeForce 9400
+ 0x0868, // nForce 760i SLI
+ 0x086A, // GeForce 9400
+ 0x086C, // GeForce 9300 / nForce 730i
+ 0x086D, // GeForce 9200
+ 0x086E, // GeForce 9100M G
+ 0x086F, // GeForce 8200M G
+ 0x0870, // GeForce 9400M
+ 0x0871, // GeForce 9200
+ 0x0872, // GeForce G102M
+ 0x0873, // GeForce G102M
+ 0x0874, // ION
+ 0x0876, // ION
+ 0x087A, // GeForce 9400
+ 0x087D, // ION
+ 0x087E, // ION LE
+ 0x087F, // ION LE
+ 0x0000
+};
+
+static std::string GUIDToString(const GUID& guid)
+{
+ std::string buffer = StringUtils::Format(
+ "{:08X}-{:04x}-{:04x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", guid.Data1,
+ guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3],
+ guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
+ return buffer;
+}
+
+static const dxva2_mode_t* dxva2_find_mode(const GUID* guid)
+{
+ for (const dxva2_mode_t& mode : dxva2_modes)
+ {
+ if (IsEqualGUID(*mode.guid, *guid))
+ return &mode;
+ }
+ return nullptr;
+}
+
+//-----------------------------------------------------------------------------
+// DXVA Context
+//-----------------------------------------------------------------------------
+
+CContext::weak_ptr CContext::m_context;
+CCriticalSection CContext::m_section;
+
+CContext::~CContext()
+{
+ Close();
+}
+
+void CContext::Release(CDecoder* decoder)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ const auto it = std::find(m_decoders.begin(), m_decoders.end(), decoder);
+ if (it != m_decoders.end())
+ m_decoders.erase(it);
+}
+
+void CContext::Close()
+{
+ CLog::Log(LOGINFO, "DXVA: closing decoder context.");
+ DestroyContext();
+}
+
+CContext::shared_ptr CContext::EnsureContext(CDecoder* decoder)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ auto context = m_context.lock();
+ if (context)
+ {
+ if (!context->IsValidDecoder(decoder))
+ context->m_decoders.push_back(decoder);
+ return context;
+ }
+
+ context.reset(new CContext());
+ {
+ if (!context->CreateContext())
+ return shared_ptr();
+
+ m_context = context;
+ }
+
+ return EnsureContext(decoder);
+}
+
+bool CContext::CreateContext()
+{
+ HRESULT hr = E_FAIL;
+ ComPtr<ID3D11Device> pD3DDevice;
+ ComPtr<ID3D11DeviceContext> pD3DDeviceContext;
+
+ m_sharingAllowed = DX::DeviceResources::Get()->IsNV12SharedTexturesSupported();
+
+ if (m_sharingAllowed)
+ {
+ CLog::LogF(LOGINFO, "creating discrete d3d11va device for decoding.");
+
+ std::vector<D3D_FEATURE_LEVEL> featureLevels;
+ if (CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin10))
+ {
+ featureLevels.push_back(D3D_FEATURE_LEVEL_12_1);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_12_0);
+ }
+ if (CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin8))
+ featureLevels.push_back(D3D_FEATURE_LEVEL_11_1);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_11_0);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_10_1);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_10_0);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_9_3);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_9_2);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_9_1);
+
+ hr = D3D11CreateDevice(DX::DeviceResources::Get()->GetAdapter(), D3D_DRIVER_TYPE_UNKNOWN,
+ nullptr, D3D11_CREATE_DEVICE_VIDEO_SUPPORT, featureLevels.data(),
+ featureLevels.size(), D3D11_SDK_VERSION, &pD3DDevice, nullptr,
+ &pD3DDeviceContext);
+
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGWARNING, "unable to create device for decoding, fallback to using app device.");
+ m_sharingAllowed = false;
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGWARNING, "using app d3d11 device for decoding due extended NV12 shared "
+ "textures it's not supported.");
+ }
+
+ if (FAILED(hr))
+ {
+ pD3DDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ pD3DDeviceContext = DX::DeviceResources::Get()->GetImmediateContext();
+ }
+
+ if (FAILED(pD3DDevice.As(&m_pD3D11Device)) || FAILED(pD3DDeviceContext.As(&m_pD3D11Context)))
+ {
+ CLog::LogF(LOGWARNING, "failed to get Video Device and Context.");
+ return false;
+ }
+
+ if (FAILED(hr) || !m_sharingAllowed)
+ {
+ // enable multi-threaded protection only if is used same d3d11 device for rendering and decoding
+ ComPtr<ID3D11Multithread> multithread;
+ hr = pD3DDevice.As(&multithread);
+ if (SUCCEEDED(hr))
+ multithread->SetMultithreadProtected(1);
+ }
+
+ QueryCaps();
+
+ // Some older Ati devices can only open a single decoder at a given time
+ std::string renderer = DX::Windowing()->GetRenderRenderer();
+ if (renderer.find("Radeon HD 2") != std::string::npos ||
+ renderer.find("Radeon HD 3") != std::string::npos ||
+ renderer.find("Radeon HD 4") != std::string::npos ||
+ renderer.find("Radeon HD 5") != std::string::npos)
+ {
+ m_atiWorkaround = true;
+ }
+
+ // Sets high priority process for smooth playback in all circumstances
+ SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
+
+ return true;
+}
+
+void CContext::DestroyContext()
+{
+ delete[] m_input_list;
+ m_pD3D11Device = nullptr;
+ m_pD3D11Context = nullptr;
+
+ // Restores normal priority process
+ SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS);
+}
+
+void CContext::QueryCaps()
+{
+ m_input_count = m_pD3D11Device->GetVideoDecoderProfileCount();
+
+ m_input_list = new GUID[m_input_count];
+ for (unsigned i = 0; i < m_input_count; i++)
+ {
+ if (FAILED(m_pD3D11Device->GetVideoDecoderProfile(i, &m_input_list[i])))
+ {
+ CLog::Log(LOGINFO, "DXVA: failed getting video decoder profile");
+ return;
+ }
+ const dxva2_mode_t* mode = dxva2_find_mode(&m_input_list[i]);
+ if (mode)
+ CLog::Log(LOGDEBUG, "DXVA: supports '{}'", mode->name);
+ else
+ CLog::Log(LOGDEBUG, "DXVA: supports {}", GUIDToString(m_input_list[i]));
+ }
+}
+
+bool CContext::GetFormatAndConfig(AVCodecContext* avctx, D3D11_VIDEO_DECODER_DESC &format, D3D11_VIDEO_DECODER_CONFIG &config) const
+{
+ format.OutputFormat = DXGI_FORMAT_UNKNOWN;
+
+ // iterate through our predefined dxva modes and find the first matching for desired codec
+ // once we found a mode, get a target we support in render_targets_dxgi DXGI_FORMAT_UNKNOWN
+ for (const dxva2_mode_t& mode : dxva2_modes)
+ {
+ if (mode.codec != avctx->codec_id)
+ continue;
+
+ bool supported = false;
+ for (unsigned i = 0; i < m_input_count && !supported; i++)
+ {
+ supported = IsEqualGUID(m_input_list[i], *mode.guid) != 0;
+ }
+ if (supported)
+ {
+ // check profiles
+ supported = false;
+ if (mode.profiles == nullptr)
+ supported = true;
+ else if (avctx->profile == FF_PROFILE_UNKNOWN)
+ supported = true;
+ else
+ for (const int* pProfile = &mode.profiles[0]; *pProfile != FF_PROFILE_UNKNOWN; ++pProfile)
+ {
+ if (*pProfile == avctx->profile)
+ {
+ supported = true;
+ break;
+ }
+ }
+ if (!supported)
+ CLog::Log(LOGDEBUG, "DXVA: Unsupported profile {} for {}.", avctx->profile, mode.name);
+ }
+ if (!supported)
+ continue;
+
+ CLog::Log(LOGDEBUG, "DXVA: trying '{}'.", mode.name);
+ for (unsigned j = 0; render_targets_dxgi[j]; ++j)
+ {
+ bool bHighBits = (avctx->codec_id == AV_CODEC_ID_HEVC && (avctx->sw_pix_fmt == AV_PIX_FMT_YUV420P10 || avctx->profile == FF_PROFILE_HEVC_MAIN_10))
+ || (avctx->codec_id == AV_CODEC_ID_VP9 && (avctx->sw_pix_fmt == AV_PIX_FMT_YUV420P10 || avctx->profile == FF_PROFILE_VP9_2));
+ if (bHighBits && render_targets_dxgi[j] < DXGI_FORMAT_P010)
+ continue;
+
+ BOOL format_supported = FALSE;
+ HRESULT res = m_pD3D11Device->CheckVideoDecoderFormat(mode.guid, render_targets_dxgi[j], &format_supported);
+ if (FAILED(res) || !format_supported)
+ {
+ CLog::Log(LOGINFO, "DXVA: Output format {} is not supported by '{}'",
+ render_targets_dxgi[j], mode.name);
+ continue;
+ }
+
+ // check decoder config
+ D3D11_VIDEO_DECODER_DESC checkFormat = {*mode.guid, static_cast<UINT>(avctx->coded_width),
+ static_cast<UINT>(avctx->coded_height),
+ render_targets_dxgi[j]};
+ if (!GetConfig(checkFormat, config))
+ continue;
+
+ // config is found, update decoder description
+ format.Guid = *mode.guid;
+ format.OutputFormat = render_targets_dxgi[j];
+ format.SampleWidth = avctx->coded_width;
+ format.SampleHeight = avctx->coded_height;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CContext::GetConfig(const D3D11_VIDEO_DECODER_DESC &format, D3D11_VIDEO_DECODER_CONFIG &config) const
+{
+ // find what decode configs are available
+ UINT cfg_count = 0;
+ if (FAILED(m_pD3D11Device->GetVideoDecoderConfigCount(&format, &cfg_count)))
+ {
+ CLog::LogF(LOGINFO, "failed getting decoder configuration count.");
+ return false;
+ }
+ if (!cfg_count)
+ {
+ CLog::LogF(LOGINFO, "no decoder configuration possible for {}x{} ({}).", format.SampleWidth,
+ format.SampleHeight, format.OutputFormat);
+ return false;
+ }
+
+ config = {};
+ const unsigned bitstream = 2; // ConfigBitstreamRaw = 2 is required for Poulsbo and handles skipping better with nVidia
+ for (unsigned i = 0; i< cfg_count; i++)
+ {
+ D3D11_VIDEO_DECODER_CONFIG pConfig = {};
+ if (FAILED(m_pD3D11Device->GetVideoDecoderConfig(&format, i, &pConfig)))
+ {
+ CLog::LogF(LOGINFO, "failed getting decoder configuration.");
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "DXVA: config {}: bitstream type {}{}.", i, pConfig.ConfigBitstreamRaw,
+ IsEqualGUID(pConfig.guidConfigBitstreamEncryption, DXVA_NoEncrypt) ? ""
+ : ", encrypted");
+
+ // select first available
+ if (config.ConfigBitstreamRaw == 0 && pConfig.ConfigBitstreamRaw != 0)
+ config = pConfig;
+ // override with preferred if found
+ if (config.ConfigBitstreamRaw != bitstream && pConfig.ConfigBitstreamRaw == bitstream)
+ config = pConfig;
+ }
+
+ if (!config.ConfigBitstreamRaw)
+ {
+ CLog::Log(LOGDEBUG, "DXVA: failed to find a raw input bitstream.");
+ return false;
+ }
+
+ return true;
+}
+
+bool CContext::CreateSurfaces(const D3D11_VIDEO_DECODER_DESC& format, uint32_t count,
+ uint32_t alignment, ID3D11VideoDecoderOutputView** surfaces,
+ HANDLE* pHandle, bool trueShared) const
+{
+ HRESULT hr = S_OK;
+ ComPtr<ID3D11Device> pD3DDevice;
+ ComPtr<ID3D11DeviceContext> pD3DDeviceContext;
+ ComPtr<ID3D11DeviceContext1> pD3DDeviceContext1;
+
+ m_pD3D11Context->GetDevice(&pD3DDevice);
+ pD3DDevice->GetImmediateContext(&pD3DDeviceContext);
+ pD3DDeviceContext.As(&pD3DDeviceContext1);
+
+ CD3D11_TEXTURE2D_DESC texDesc(format.OutputFormat,
+ FFALIGN(format.SampleWidth, alignment),
+ FFALIGN(format.SampleHeight, alignment),
+ count, 1, D3D11_BIND_DECODER);
+ UINT supported;
+ if (SUCCEEDED(pD3DDevice->CheckFormatSupport(format.OutputFormat, &supported)) &&
+ (supported & D3D11_FORMAT_SUPPORT_SHADER_SAMPLE))
+ {
+ texDesc.BindFlags |= D3D11_BIND_SHADER_RESOURCE;
+ }
+ if (trueShared)
+ {
+ texDesc.MiscFlags |= D3D11_RESOURCE_MISC_SHARED;
+ }
+
+ CLog::Log(LOGDEBUG, "DXVA: allocating {} surfaces with format {}.", count, format.OutputFormat);
+
+ ComPtr<ID3D11Texture2D> texture;
+ if (FAILED(pD3DDevice->CreateTexture2D(&texDesc, NULL, texture.GetAddressOf())))
+ {
+ CLog::LogF(LOGERROR, "failed creating decoder texture array.");
+ return false;
+ }
+
+ // acquire shared handle once
+ if (trueShared && pHandle)
+ {
+ ComPtr<IDXGIResource> dxgiResource;
+ if (FAILED(texture.As(&dxgiResource)) || FAILED(dxgiResource->GetSharedHandle(pHandle)))
+ {
+ CLog::LogF(LOGERROR, "unable to get shared handle for texture");
+ *pHandle = INVALID_HANDLE_VALUE;
+ }
+ }
+
+ D3D11_VIDEO_DECODER_OUTPUT_VIEW_DESC vdovDesc = {
+ format.Guid,
+ D3D11_VDOV_DIMENSION_TEXTURE2D,
+ { 0 }
+ };
+ // For video views with YUV or YCbBr formats, ClearView doesn't
+ // convert color values but assumes UINT texture format
+ float clearColor[] = {0.f, 127.f, 127.f, 255.f}; // black color in YUV
+
+ size_t i;
+ for (i = 0; i < count; ++i)
+ {
+ vdovDesc.Texture2D.ArraySlice = D3D11CalcSubresource(0, i, texDesc.MipLevels);
+ hr = m_pD3D11Device->CreateVideoDecoderOutputView(texture.Get(), &vdovDesc, &surfaces[i]);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "failed creating surfaces.");
+ break;
+ }
+ if (pD3DDeviceContext1)
+ pD3DDeviceContext1->ClearView(surfaces[i], clearColor, nullptr, 0);
+ }
+
+ if (FAILED(hr))
+ {
+ for (size_t j = 0; j < i && surfaces[j]; ++j)
+ {
+ surfaces[j]->Release();
+ surfaces[j] = nullptr;
+ };
+ }
+
+ return SUCCEEDED(hr);
+}
+
+bool CContext::CreateDecoder(const D3D11_VIDEO_DECODER_DESC &format, const D3D11_VIDEO_DECODER_CONFIG &config
+ , ID3D11VideoDecoder **decoder, ID3D11VideoContext **context)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ int retry = 0;
+ while (retry < 2)
+ {
+ if (!m_atiWorkaround || retry > 0)
+ {
+ ComPtr<ID3D11VideoDecoder> pDecoder;
+ HRESULT res = m_pD3D11Device->CreateVideoDecoder(&format, &config, pDecoder.GetAddressOf());
+ if (!FAILED(res))
+ {
+ *decoder = pDecoder.Detach();
+ return SUCCEEDED(m_pD3D11Context.CopyTo(context));
+ }
+ }
+
+ if (retry == 0)
+ {
+ CLog::LogF(LOGINFO, "hw may not support multiple decoders, releasing existing ones.");
+ for (auto& m_decoder : m_decoders)
+ m_decoder->CloseDXVADecoder();
+ }
+ retry++;
+ }
+
+ CLog::LogF(LOGERROR, "failed creating decoder.");
+ return false;
+}
+
+bool CContext::IsValidDecoder(CDecoder* decoder)
+{
+ return std::find(m_decoders.begin(), m_decoders.end(), decoder) != m_decoders.end();
+}
+
+bool CContext::Check() const
+{
+ if (!m_sharingAllowed)
+ return true;
+
+ ComPtr<ID3D11Device> pDevice;
+ m_pD3D11Context->GetDevice(&pDevice);
+
+ return SUCCEEDED(pDevice->GetDeviceRemovedReason());
+}
+
+bool CContext::Reset()
+{
+ if (Check())
+ {
+ DXGI_ADAPTER_DESC appDesc = {};
+ DX::DeviceResources::Get()->GetAdapterDesc(&appDesc);
+
+ ComPtr<IDXGIDevice> ctxDevice;
+ ComPtr<IDXGIAdapter> ctxAdapter;
+ if (SUCCEEDED(m_pD3D11Device.As(&ctxDevice)) && SUCCEEDED(ctxDevice->GetAdapter(&ctxAdapter)))
+ {
+ DXGI_ADAPTER_DESC ctxDesc = {};
+ ctxAdapter->GetDesc(&ctxDesc);
+
+ if (appDesc.AdapterLuid.HighPart == ctxDesc.AdapterLuid.HighPart &&
+ appDesc.AdapterLuid.LowPart == ctxDesc.AdapterLuid.LowPart)
+ {
+ // 1. we have valid device
+ // 2. we are on the same adapter
+ // 3. don't reset context.
+ return true;
+ }
+ }
+ }
+ DestroyContext();
+ return CreateContext();
+}
+
+//-----------------------------------------------------------------------------
+// DXVA::CVideoBuffer
+//-----------------------------------------------------------------------------
+
+DXVA::CVideoBuffer::CVideoBuffer(int id)
+ : ::CVideoBuffer(id)
+{
+ m_pixFormat = AV_PIX_FMT_D3D11VA_VLD;
+ m_pFrame = av_frame_alloc();
+}
+
+DXVA::CVideoBuffer::~CVideoBuffer()
+{
+ av_frame_free(&m_pFrame);
+}
+
+void DXVA::CVideoBuffer::Initialize(CDecoder* decoder)
+{
+ width = FFALIGN(decoder->m_format.SampleWidth, decoder->m_surface_alignment);
+ height = FFALIGN(decoder->m_format.SampleHeight, decoder->m_surface_alignment);
+ format = decoder->m_format.OutputFormat;
+}
+
+HRESULT DXVA::CVideoBuffer::GetResource(ID3D11Resource** ppResource)
+{
+ if (!view)
+ return E_NOT_SET;
+
+ view->GetResource(ppResource);
+ return S_OK;
+}
+
+unsigned DXVA::CVideoBuffer::GetIdx()
+{
+ D3D11_VIDEO_DECODER_OUTPUT_VIEW_DESC vpivd = {};
+ ComPtr<ID3D11VideoDecoderOutputView> pView = reinterpret_cast<ID3D11VideoDecoderOutputView*>(view);
+ pView->GetDesc(&vpivd);
+
+ return vpivd.Texture2D.ArraySlice;
+}
+
+void DXVA::CVideoBuffer::SetRef(AVFrame* frame)
+{
+ av_frame_unref(m_pFrame);
+ av_frame_ref(m_pFrame, frame);
+ view = reinterpret_cast<ID3D11View*>(frame->data[3]);
+}
+
+void DXVA::CVideoBuffer::Unref()
+{
+ view = nullptr;
+ av_frame_unref(m_pFrame);
+}
+
+HRESULT CVideoBufferShared::GetResource(ID3D11Resource** ppResource)
+{
+ HRESULT hr = S_OK;
+ if (handle == INVALID_HANDLE_VALUE)
+ return E_HANDLE;
+
+ if (!m_sharedRes)
+ {
+ // open resource on app device
+ ComPtr<ID3D11Device> pD3DDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ hr = pD3DDevice->OpenSharedResource(handle, __uuidof(ID3D11Resource), &m_sharedRes);
+ }
+
+ if (SUCCEEDED(hr))
+ hr = m_sharedRes.CopyTo(ppResource);
+
+ return hr;
+}
+
+void CVideoBufferShared::Initialize(CDecoder* decoder)
+{
+ CVideoBuffer::Initialize(decoder);
+
+ if (handle == INVALID_HANDLE_VALUE)
+ handle = decoder->m_sharedHandle;
+}
+
+void CVideoBufferCopy::Initialize(CDecoder* decoder)
+{
+ CVideoBuffer::Initialize(decoder);
+
+ if (!m_copyRes)
+ {
+ ComPtr<ID3D11Device> pDevice;
+ ComPtr<ID3D11DeviceContext> pDeviceContext;
+ ComPtr<ID3D11Texture2D> pDecoderTexture;
+ ComPtr<ID3D11Texture2D> pCopyTexture;
+ ComPtr<IDXGIResource> pDXGIResource;
+ ComPtr<ID3D11Resource> pResource;
+
+ decoder->m_pD3D11Context->GetDevice(&pDevice);
+ pDevice->GetImmediateContext(&pDeviceContext);
+
+ if (FAILED(CVideoBuffer::GetResource(&pResource)))
+ {
+ CLog::LogF(LOGDEBUG, "unable to get decoder resource");
+ return;
+ }
+
+ if (FAILED(pResource.As(&pDecoderTexture)))
+ {
+ CLog::LogF(LOGDEBUG, "unable to get decoder texture");
+ return;
+ }
+
+ D3D11_TEXTURE2D_DESC desc;
+ pDecoderTexture->GetDesc(&desc);
+ desc.ArraySize = 1;
+ desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
+ desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
+
+ if (FAILED(pDevice->CreateTexture2D(&desc, nullptr, &pCopyTexture)))
+ {
+ CLog::LogF(LOGDEBUG, "unable to create copy texture");
+ return;
+ }
+ if (FAILED(pCopyTexture.As(&pDXGIResource)))
+ {
+ CLog::LogF(LOGDEBUG, "unable to get DXGI resource for copy texture");
+ return;
+ }
+
+ HANDLE shared_handle;
+ if (FAILED(pDXGIResource->GetSharedHandle(&shared_handle)))
+ {
+ CLog::LogF(LOGDEBUG, "unable to get shared handle");
+ return;
+ }
+
+ handle = shared_handle;
+ pCopyTexture.As(&m_copyRes);
+ pResource.As(&m_pResource);
+ pDeviceContext.As(&m_pDeviceContext);
+ }
+
+ if (m_copyRes)
+ {
+ // sends commands to GPU (ensures that the last decoded image is ready)
+ m_pDeviceContext->Flush();
+
+ // copy decoder surface on decoder device
+ m_pDeviceContext->CopySubresourceRegion(m_copyRes.Get(), 0, 0, 0, 0, m_pResource.Get(),
+ CVideoBuffer::GetIdx(), nullptr);
+
+ if (decoder->m_DVDWorkaround) // DVDs menus/stills need extra Flush()
+ m_pDeviceContext->Flush();
+ }
+}
+
+//-----------------------------------------------------------------------------
+// DXVA::CVideoBufferPool
+//-----------------------------------------------------------------------------
+
+CVideoBufferPool::CVideoBufferPool() = default;
+
+CVideoBufferPool::~CVideoBufferPool()
+{
+ CLog::LogF(LOGDEBUG, "destructing buffer pool.");
+ Reset();
+}
+
+::CVideoBuffer* CVideoBufferPool::Get()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ CVideoBuffer* retPic;
+ if (!m_freeOut.empty())
+ {
+ const size_t idx = m_freeOut.front();
+ m_freeOut.pop_front();
+ retPic = m_out[idx];
+ }
+ else
+ {
+ const size_t idx = m_out.size();
+ retPic = CreateBuffer(idx);
+ m_out.push_back(retPic);
+ }
+
+ retPic->Acquire(GetPtr());
+ return retPic;
+}
+
+void CVideoBufferPool::Return(int id)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ auto buf = m_out[id];
+ buf->Unref();
+
+ m_freeOut.push_back(id);
+}
+
+void CVideoBufferPool::AddView(ID3D11View* view)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ const size_t idx = m_views.size();
+ m_views.push_back(view);
+ m_freeViews.push_back(idx);
+}
+
+bool CVideoBufferPool::IsValid(ID3D11View* view)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ return std::find(m_views.begin(), m_views.end(), view) != m_views.end();
+}
+
+bool CVideoBufferPool::ReturnView(ID3D11View* view)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ const auto it = std::find(m_views.begin(), m_views.end(), view);
+ if (it == m_views.end())
+ return false;
+
+ const size_t idx = it - m_views.begin();
+ m_freeViews.push_back(idx);
+ return true;
+}
+
+ID3D11View* CVideoBufferPool::GetView()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (!m_freeViews.empty())
+ {
+ const size_t idx = m_freeViews.front();
+ m_freeViews.pop_front();
+
+ return m_views[idx];
+ }
+ return nullptr;
+}
+
+void CVideoBufferPool::Reset()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ for (auto v : m_views)
+ if (v)
+ v->Release();
+
+ for (auto buf : m_out)
+ delete buf;
+
+ m_views.clear();
+ m_freeViews.clear();
+ m_out.clear();
+ m_freeOut.clear();
+}
+
+size_t CVideoBufferPool::Size()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ return m_views.size();
+}
+
+bool CVideoBufferPool::HasFree()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ return !m_freeViews.empty();
+}
+
+bool CVideoBufferPool::HasRefs()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ // out buffers hold views
+ const size_t buffRefs = m_out.size() - m_freeOut.size();
+ // ffmpeg refs = total - free - out refs
+ return m_freeViews.size() != m_views.size() - buffRefs;
+}
+
+//-----------------------------------------------------------------------------
+// DXVA::CDecoder
+//-----------------------------------------------------------------------------
+
+IHardwareDecoder* CDecoder::Create(CDVDStreamInfo &hint, CProcessInfo &processInfo, AVPixelFormat fmt)
+{
+ if (Supports(fmt) && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_VIDEOPLAYER_USEDXVA2))
+ return new CDecoder(processInfo);
+
+ return nullptr;
+}
+
+bool CDecoder::Register()
+{
+ CDVDFactoryCodec::RegisterHWAccel("dxva", Create);
+ return true;
+}
+
+CDecoder::CDecoder(CProcessInfo& processInfo)
+ : m_processInfo(processInfo)
+{
+ m_event.Set();
+ m_avD3D11Context = av_d3d11va_alloc_context();
+ m_avD3D11Context->cfg = reinterpret_cast<D3D11_VIDEO_DECODER_CONFIG*>(av_mallocz(sizeof(D3D11_VIDEO_DECODER_CONFIG)));
+ m_avD3D11Context->surface = reinterpret_cast<ID3D11VideoDecoderOutputView**>(av_mallocz_array(32, sizeof(ID3D11VideoDecoderOutputView*)));
+ m_bufferPool.reset();
+
+ DX::Windowing()->Register(this);
+}
+
+CDecoder::~CDecoder()
+{
+ CLog::LogF(LOGDEBUG, "destructing decoder, {}.", fmt::ptr(this));
+ DX::Windowing()->Unregister(this);
+
+ Close();
+ av_freep(&m_avD3D11Context->surface);
+ av_freep(&m_avD3D11Context->cfg);
+ av_freep(&m_avD3D11Context);
+}
+
+long CDecoder::Release()
+{
+ // if ffmpeg holds any references, flush buffers
+ if (m_bufferPool && m_bufferPool->HasRefs())
+ avcodec_flush_buffers(m_avCtx);
+
+ return IHardwareDecoder::Release();
+}
+
+void CDecoder::Close()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_pD3D11Decoder = nullptr;
+ m_pD3D11Context = nullptr;
+
+ if (m_videoBuffer)
+ {
+ m_videoBuffer->Release();
+ m_videoBuffer = nullptr;
+ }
+ m_format = {};
+ m_sharedHandle = INVALID_HANDLE_VALUE;
+
+ if (m_dxvaContext)
+ {
+ auto dxva_context = m_dxvaContext;
+ CLog::LogF(LOGINFO, "closing decoder.");
+ m_dxvaContext = nullptr;
+ dxva_context->Release(this);
+ }
+}
+
+static bool CheckH264L41(AVCodecContext* avctx)
+{
+ unsigned widthmbs = (avctx->coded_width + 15) / 16; // width in macroblocks
+ unsigned heightmbs = (avctx->coded_height + 15) / 16; // height in macroblocks
+ unsigned maxdpbmbs = 32768; // Decoded Picture Buffer (DPB) capacity in macroblocks for L4.1
+
+ return avctx->refs * widthmbs * heightmbs <= maxdpbmbs;
+}
+
+static bool IsL41LimitedATI()
+{
+ DXGI_ADAPTER_DESC AIdentifier = {};
+ DX::DeviceResources::Get()->GetAdapterDesc(&AIdentifier);
+
+ if (AIdentifier.VendorId == PCIV_AMD)
+ {
+ for (unsigned idx = 0; UVDDeviceID[idx] != 0; idx++)
+ {
+ if (UVDDeviceID[idx] == AIdentifier.DeviceId)
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool HasVP3WidthBug(AVCodecContext* avctx)
+{
+ // Some nVidia VP3 hardware cannot do certain macroblock widths
+ DXGI_ADAPTER_DESC AIdentifier = {};
+ DX::DeviceResources::Get()->GetAdapterDesc(&AIdentifier);
+
+ if (AIdentifier.VendorId == PCIV_NVIDIA &&
+ !CDVDCodecUtils::IsVP3CompatibleWidth(avctx->coded_width))
+ {
+ // Find the card in a known list of problematic VP3 hardware
+ for (unsigned idx = 0; VP3DeviceID[idx] != 0; idx++)
+ if (VP3DeviceID[idx] == AIdentifier.DeviceId)
+ return true;
+ }
+ return false;
+}
+
+static bool HasATIMP2Bug(AVCodecContext* avctx)
+{
+ DXGI_ADAPTER_DESC AIdentifier = {};
+ DX::DeviceResources::Get()->GetAdapterDesc(&AIdentifier);
+ if (AIdentifier.VendorId != PCIV_AMD)
+ return false;
+
+ // AMD/ATI card doesn't like some SD MPEG2 content
+ // here are params of these videos
+ return avctx->height <= 576
+ && avctx->colorspace == AVCOL_SPC_BT470BG
+ && avctx->color_primaries == AVCOL_PRI_BT470BG
+ && avctx->color_trc == AVCOL_TRC_GAMMA28;
+}
+
+static bool HasAMDH264SDiBug(AVCodecContext* avctx)
+{
+ DXGI_ADAPTER_DESC AIdentifier = {};
+ DX::DeviceResources::Get()->GetAdapterDesc(&AIdentifier);
+
+ if (AIdentifier.VendorId != PCIV_AMD)
+ return false;
+
+ // AMD card has issues with SD H264 interlaced content
+ return (avctx->width <= 720 && avctx->height <= 576 && avctx->codec_id == AV_CODEC_ID_H264 &&
+ avctx->field_order != AV_FIELD_PROGRESSIVE);
+}
+
+static bool CheckCompatibility(AVCodecContext* avctx)
+{
+ if (avctx->codec_id == AV_CODEC_ID_MPEG2VIDEO && HasATIMP2Bug(avctx))
+ return false;
+
+ // The incompatibilities are all for H264
+ if (avctx->codec_id != AV_CODEC_ID_H264)
+ return true;
+
+ // Macroblock width incompatibility
+ if (HasVP3WidthBug(avctx))
+ {
+ CLog::Log(LOGWARNING,
+ "DXVA: width {} is not supported with nVidia VP3 hardware. DXVA will not be used.",
+ avctx->coded_width);
+ return false;
+ }
+
+ // AMD H264 SD interlaced incompatibility
+ if (HasAMDH264SDiBug(avctx))
+ {
+ CLog::Log(
+ LOGWARNING,
+ "DXVA: H264 SD interlaced has issues on AMD graphics hardware. DXVA will not be used.");
+ return false;
+ }
+
+ // Check for hardware limited to H264 L4.1 (ie Bluray).
+
+ // No advanced settings: autodetect.
+ // The advanced setting lets the user override the autodetection (in case of false positive or negative)
+
+ bool checkcompat;
+ if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_DXVACheckCompatibilityPresent)
+ checkcompat = IsL41LimitedATI(); // ATI UVD and UVD+ cards can only do L4.1 - corresponds roughly to series 3xxx
+ else
+ checkcompat = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_DXVACheckCompatibility;
+
+ if (checkcompat && !CheckH264L41(avctx))
+ {
+ CLog::Log(LOGWARNING, "DXVA: compatibility check: video exceeds L4.1. DXVA will not be used.");
+ return false;
+ }
+
+ return true;
+}
+
+bool CDecoder::Open(AVCodecContext* avctx, AVCodecContext* mainctx, enum AVPixelFormat fmt)
+{
+ if (!CheckCompatibility(avctx))
+ return false;
+
+ // DVDs menus/stills need extra Flush() after copy texture
+ if (avctx->codec_id == AV_CODEC_ID_MPEG2VIDEO && avctx->height <= 576)
+ m_DVDWorkaround = true;
+
+ std::unique_lock<CCriticalSection> lock(m_section);
+ Close();
+
+ if (m_state == DXVA_LOST)
+ {
+ CLog::Log(LOGDEBUG, "DXVA: device is in lost state, we can't start.");
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "DXVA: open decoder.");
+ m_dxvaContext = CContext::EnsureContext(this);
+ if (!m_dxvaContext)
+ return false;
+
+ if (!m_dxvaContext->GetFormatAndConfig(avctx, m_format, *m_avD3D11Context->cfg))
+ {
+ CLog::Log(LOGDEBUG, "DXVA: unable to find an input/output format combination.");
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "DXVA: selected output format: {}.", m_format.OutputFormat);
+ CLog::Log(LOGDEBUG, "DXVA: source requires {} references.", avctx->refs);
+ if (m_format.Guid == DXVADDI_Intel_ModeH264_E && avctx->refs > 11)
+ {
+ const dxva2_mode_t* mode = dxva2_find_mode(&m_format.Guid);
+ CLog::Log(LOGWARNING, "DXVA: too many references {} for selected decoder '{}'.", avctx->refs,
+ mode->name);
+ return false;
+ }
+
+ if (6 > m_shared)
+ m_shared = 6;
+
+ m_refs = 2 + m_shared; // 1 decode + 1 safety + display
+ m_surface_alignment = 16;
+
+ DXGI_ADAPTER_DESC ad = {};
+ DX::DeviceResources::Get()->GetAdapterDesc(&ad);
+
+ size_t videoMem = ad.SharedSystemMemory + ad.DedicatedVideoMemory + ad.DedicatedSystemMemory;
+ CLog::LogF(LOGINFO, "Total video memory available is {} MB (dedicated = {} MB, shared = {} MB)",
+ videoMem / MB, (ad.DedicatedVideoMemory + ad.DedicatedSystemMemory) / MB,
+ ad.SharedSystemMemory / MB);
+
+ switch (avctx->codec_id)
+ {
+ case AV_CODEC_ID_MPEG2VIDEO:
+ /* decoding MPEG-2 requires additional alignment on some Intel GPUs,
+ but it causes issues for H.264 on certain AMD GPUs..... */
+ m_surface_alignment = 32;
+ m_refs += 2;
+ break;
+ case AV_CODEC_ID_HEVC:
+ /* the HEVC DXVA2 spec asks for 128 pixel aligned surfaces to ensure
+ all coding features have enough room to work with */
+ m_surface_alignment = 128;
+ // a driver may use multi-thread decoding internally
+ // on Xbox only add refs for <= Full HD due memory constraints (max 16 refs for 4K)
+ if (CSysInfo::GetWindowsDeviceFamily() != CSysInfo::Xbox)
+ {
+ m_refs += CServiceBroker::GetCPUInfo()->GetCPUCount();
+ }
+ else
+ {
+ if (avctx->width <= 1920)
+ m_refs += CServiceBroker::GetCPUInfo()->GetCPUCount() / 2;
+ }
+ // by specification hevc decoder can hold up to 8 unique refs
+ // ffmpeg may report only 1 refs frame when is unknown or not present in headers
+ m_refs += (avctx->refs > 1) ? avctx->refs : 8;
+ break;
+ case AV_CODEC_ID_H264:
+ // by specification h264 decoder can hold up to 16 unique refs
+ m_refs += avctx->refs ? avctx->refs : 16;
+ break;
+ case AV_CODEC_ID_VP9:
+ m_refs += 8;
+ break;
+ default:
+ m_refs += 2;
+ }
+
+ if (avctx->active_thread_type & FF_THREAD_FRAME)
+ m_refs += avctx->thread_count;
+
+ // Limit decoder surfaces to 32 maximum in any case. Since with some 16 cores / 32 threads
+ // new CPU's (Ryzen 5950x) this number may be higher than what the graphics card can handle.
+ if (m_refs > 32)
+ {
+ CLog::LogF(LOGWARNING, "The number of decoder surfaces has been limited from {} to 32.", m_refs);
+ m_refs = 32;
+ }
+
+ // Check if available video memory is sufficient for 4K decoding (is need ~3000 MB)
+ if (avctx->width >= 3840 && m_refs > 16 && videoMem < (LIMIT_VIDEO_MEMORY_4K * MB))
+ {
+ CLog::LogF(LOGWARNING,
+ "Current available video memory ({} MB) is insufficient 4K video decoding (DXVA2) "
+ "using {} surfaces. Decoder surfaces has been limited to 16.", videoMem / MB, m_refs);
+ m_refs = 16;
+ }
+
+ if (!OpenDecoder())
+ {
+ m_bufferPool.reset();
+ return false;
+ }
+
+ avctx->get_buffer2 = FFGetBuffer;
+ avctx->hwaccel_context = m_avD3D11Context;
+ avctx->slice_flags = SLICE_FLAG_ALLOW_FIELD | SLICE_FLAG_CODED_ORDER;
+
+ mainctx->get_buffer2 = FFGetBuffer;
+ mainctx->hwaccel_context = m_avD3D11Context;
+ mainctx->slice_flags = SLICE_FLAG_ALLOW_FIELD | SLICE_FLAG_CODED_ORDER;
+
+ m_avCtx = mainctx;
+
+ if (m_format.Guid == DXVADDI_Intel_ModeH264_E)
+ {
+#ifdef FF_DXVA2_WORKAROUND_INTEL_CLEARVIDEO
+ m_avD3D11Context->workaround |= FF_DXVA2_WORKAROUND_INTEL_CLEARVIDEO;
+#else
+ CLog::Log(
+ LOGWARNING,
+ "DXVA: used Intel ClearVideo decoder, but no support workaround for it in libavcodec.");
+#endif
+ }
+ else if (ad.VendorId == PCIV_AMD && IsL41LimitedATI())
+ {
+#ifdef FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG
+ m_avD3D11Context->workaround |= FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG;
+#else
+ CLog::Log(LOGWARNING, "DXVA: video card with different scaling list zigzag order detected, but "
+ "no support in libavcodec.");
+#endif
+ }
+
+ std::list<EINTERLACEMETHOD> deintMethods;
+ deintMethods.push_back(VS_INTERLACEMETHOD_NONE);
+ m_processInfo.UpdateDeinterlacingMethods(deintMethods);
+ m_processInfo.SetDeinterlacingMethodDefault(VS_INTERLACEMETHOD_DXVA_AUTO);
+
+ m_state = DXVA_OPEN;
+ return true;
+}
+
+CDVDVideoCodec::VCReturn CDecoder::Decode(AVCodecContext* avctx, AVFrame* frame)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ const CDVDVideoCodec::VCReturn result = Check(avctx);
+ if (result != CDVDVideoCodec::VC_NONE)
+ return result;
+
+ if (frame)
+ {
+ if (m_bufferPool->IsValid(reinterpret_cast<ID3D11View*>(frame->data[3])))
+ {
+ if (m_videoBuffer)
+ m_videoBuffer->Release();
+ m_videoBuffer = reinterpret_cast<CVideoBuffer*>(m_bufferPool->Get());
+ if (!m_videoBuffer)
+ {
+ CLog::Log(LOGERROR, "DXVA: ran out of buffers.");
+ return CDVDVideoCodec::VC_ERROR;
+ }
+ m_videoBuffer->SetRef(frame);
+ m_videoBuffer->Initialize(this);
+ return CDVDVideoCodec::VC_PICTURE;
+ }
+ CLog::Log(LOGWARNING, "DXVA: ignoring invalid surface.");
+ return CDVDVideoCodec::VC_BUFFER;
+ }
+
+ return CDVDVideoCodec::VC_BUFFER;
+}
+
+bool CDecoder::GetPicture(AVCodecContext* avctx, VideoPicture* picture)
+{
+ static_cast<ICallbackHWAccel*>(avctx->opaque)->GetPictureCommon(picture);
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (picture->videoBuffer)
+ picture->videoBuffer->Release();
+ picture->videoBuffer = m_videoBuffer;
+ m_videoBuffer = nullptr;
+
+ if (!m_dxvaContext->IsContextShared())
+ {
+ int queued, discard, free;
+ m_processInfo.GetRenderBuffers(queued, discard, free);
+ if (free > 1)
+ {
+ DX::Windowing()->RequestDecodingTime();
+ }
+ else
+ {
+ DX::Windowing()->ReleaseDecodingTime();
+ }
+ }
+ return true;
+}
+
+void CDecoder::Reset()
+{
+ if (m_videoBuffer)
+ {
+ m_videoBuffer->Release();
+ m_videoBuffer = nullptr;
+ }
+}
+
+CDVDVideoCodec::VCReturn CDecoder::Check(AVCodecContext* avctx)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ // we may not have a hw decoder on systems (AMD HD2xxx, HD3xxx) which are only capable
+ // of opening a single decoder and VideoPlayer opened a new stream without having flushed
+ // current one.
+ if (!m_pD3D11Decoder)
+ return CDVDVideoCodec::VC_BUFFER;
+
+ // reset decoder if context detects an error on its device
+ if (!m_dxvaContext->Check())
+ m_state = DXVA_RESET;
+
+ // app device is lost
+ if (m_state == DXVA_LOST)
+ {
+ lock.unlock();
+ // wait app device restoration
+ m_event.Wait(2000ms);
+ lock.lock();
+
+ // still in lost state after 2sec
+ if (m_state == DXVA_LOST)
+ {
+ Close();
+ CLog::LogF(LOGERROR, "device didn't reset in reasonable time.");
+ return CDVDVideoCodec::VC_ERROR;
+ }
+ }
+
+ if (m_state != DXVA_OPEN)
+ {
+ // reset context in case of app device reset or context device error
+ if (!m_dxvaContext->Reset())
+ {
+ CLog::LogF(LOGERROR, "context didn't reset.");
+ return CDVDVideoCodec::VC_ERROR;
+ }
+
+ if (!Open(avctx, avctx, avctx->pix_fmt))
+ {
+ CLog::LogF(LOGERROR, "decoder was not able to reset.");
+ Close();
+ return CDVDVideoCodec::VC_ERROR;
+ }
+ // decoder re-opened
+ m_state = DXVA_OPEN;
+ return CDVDVideoCodec::VC_FLUSHED;
+ }
+
+ if (avctx->refs > m_refs)
+ {
+ CLog::LogF(LOGWARNING, "number of required reference frames increased, recreating decoder.");
+ Close();
+ return CDVDVideoCodec::VC_FLUSHED;
+ }
+
+ // Status reports are available only for the DXVA2_ModeH264 and DXVA2_ModeVC1 modes
+ if (avctx->codec_id != AV_CODEC_ID_H264 && avctx->codec_id != AV_CODEC_ID_VC1 &&
+ avctx->codec_id != AV_CODEC_ID_WMV3)
+ return CDVDVideoCodec::VC_NONE;
+
+#ifdef TARGET_WINDOWS_DESKTOP
+ D3D11_VIDEO_DECODER_EXTENSION data = {};
+ union {
+ DXVA_Status_H264 h264;
+ DXVA_Status_VC1 vc1;
+ } status = {};
+
+ /* I'm not sure, but MSDN says nothing about extensions functions in D3D11, try to using with same way as in DX9 */
+ data.Function = DXVA_STATUS_REPORTING_FUNCTION;
+ data.pPrivateOutputData = &status;
+ data.PrivateOutputDataSize = avctx->codec_id == AV_CODEC_ID_H264 ? sizeof(DXVA_Status_H264) : sizeof(DXVA_Status_VC1);
+ HRESULT hr;
+ if (FAILED(hr = m_pD3D11Context->DecoderExtension(m_pD3D11Decoder.Get(), &data)))
+ {
+ CLog::Log(LOGWARNING, "DXVA: failed to get decoder status - {:#08X}.", hr);
+ return CDVDVideoCodec::VC_ERROR;
+ }
+
+ if (avctx->codec_id == AV_CODEC_ID_H264)
+ {
+ if (status.h264.bStatus)
+ CLog::Log(LOGWARNING, "DXVA: decoder problem of status {} with {}.", status.h264.bStatus,
+ status.h264.bBufType);
+ }
+ else
+ {
+ if (status.vc1.bStatus)
+ CLog::Log(LOGWARNING, "DXVA: decoder problem of status {} with {}.", status.vc1.bStatus,
+ status.vc1.bBufType);
+ }
+#endif
+ return CDVDVideoCodec::VC_NONE;
+}
+
+bool CDecoder::OpenDecoder()
+{
+ m_pD3D11Decoder = nullptr;
+ m_pD3D11Context = nullptr;
+ m_avD3D11Context->decoder = nullptr;
+ m_avD3D11Context->video_context = nullptr;
+ m_avD3D11Context->surface_count = m_refs;
+
+ // use true shared buffers always on Intel or Nvidia/AMD with recent drivers
+ const bool trueShared = DX::DeviceResources::Get()->IsDXVA2SharedDecoderSurfaces();
+
+ if (!m_dxvaContext->CreateSurfaces(m_format, m_avD3D11Context->surface_count, m_surface_alignment,
+ m_avD3D11Context->surface, &m_sharedHandle, trueShared))
+ return false;
+
+ if (!m_dxvaContext->CreateDecoder(m_format, *m_avD3D11Context->cfg, m_pD3D11Decoder.GetAddressOf(),
+ m_pD3D11Context.GetAddressOf()))
+ return false;
+
+ if (m_dxvaContext->IsContextShared())
+ {
+ if (trueShared)
+ m_bufferPool = std::make_shared<CVideoBufferPoolTyped<CVideoBufferShared>>();
+ else
+ m_bufferPool = std::make_shared<CVideoBufferPoolTyped<CVideoBufferCopy>>();
+ }
+ else
+ m_bufferPool = std::make_shared<CVideoBufferPoolTyped<CVideoBuffer>>();
+
+ for (unsigned i = 0; i < m_avD3D11Context->surface_count; i++)
+ m_bufferPool->AddView(m_avD3D11Context->surface[i]);
+
+ m_avD3D11Context->decoder = m_pD3D11Decoder.Get();
+ m_avD3D11Context->video_context = m_pD3D11Context.Get();
+
+ return true;
+}
+
+bool CDecoder::Supports(enum AVPixelFormat fmt)
+{
+ return fmt == AV_PIX_FMT_D3D11VA_VLD;
+}
+
+void CDecoder::FFReleaseBuffer(void* opaque, uint8_t* data)
+{
+ auto decoder = static_cast<CDecoder*>(opaque);
+ decoder->ReleaseBuffer(data);
+}
+
+void CDecoder::ReleaseBuffer(uint8_t* data)
+{
+ const auto view = reinterpret_cast<ID3D11VideoDecoderOutputView*>(data);
+ if (!m_bufferPool->ReturnView(view))
+ {
+ CLog::LogF(LOGWARNING, "return of invalid surface.");
+ }
+
+ IHardwareDecoder::Release();
+}
+
+int CDecoder::FFGetBuffer(AVCodecContext* avctx, AVFrame* pic, int flags)
+{
+ auto* cb = static_cast<ICallbackHWAccel*>(avctx->opaque);
+ auto decoder = dynamic_cast<CDecoder*>(cb->GetHWAccel());
+
+ return decoder->GetBuffer(avctx, pic);
+}
+
+int CDecoder::GetBuffer(AVCodecContext* avctx, AVFrame* pic)
+{
+ if (!m_pD3D11Decoder)
+ return -1;
+
+ ID3D11View* view = m_bufferPool->GetView();
+ if (view == nullptr)
+ {
+ CLog::LogF(LOGERROR, "no surface available.");
+ m_state = DXVA_RESET;
+ return -1;
+ }
+
+ pic->reordered_opaque = avctx->reordered_opaque;
+
+ for (unsigned i = 0; i < 4; i++)
+ {
+ pic->data[i] = nullptr;
+ pic->linesize[i] = 0;
+ }
+
+ pic->data[0] = reinterpret_cast<uint8_t*>(view);
+ pic->data[3] = reinterpret_cast<uint8_t*>(view);
+ AVBufferRef* buffer = av_buffer_create(pic->data[3], 0, CDecoder::FFReleaseBuffer, this, 0);
+ if (!buffer)
+ {
+ CLog::LogF(LOGERROR, "error creating buffer.");
+ return -1;
+ }
+ pic->buf[0] = buffer;
+
+ Acquire();
+
+ return 0;
+}
+
+unsigned CDecoder::GetAllowedReferences()
+{
+ return m_shared;
+}
+
+void CDecoder::CloseDXVADecoder()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_pD3D11Decoder = nullptr;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/DXVA.h b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DXVA.h
new file mode 100644
index 0000000..1883bb8
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/DXVA.h
@@ -0,0 +1,262 @@
+/*
+ * 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/VideoPlayer/Buffers/VideoBuffer.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+#include "guilib/D3DResource.h"
+#include "threads/Event.h"
+
+#include <mutex>
+#include <vector>
+
+#include <wrl/client.h>
+extern "C"
+{
+#include <libavcodec/avcodec.h>
+#include <libavcodec/d3d11va.h>
+}
+
+namespace DXVA
+{
+class CDecoder;
+
+class CVideoBuffer : public ::CVideoBuffer
+{
+ template<typename TBuffer>
+ friend class CVideoBufferPoolTyped;
+
+public:
+ virtual ~CVideoBuffer();
+
+ void SetRef(AVFrame* frame);
+ void Unref();
+
+ virtual void Initialize(CDecoder* decoder);
+ virtual HRESULT GetResource(ID3D11Resource** ppResource);
+ virtual unsigned GetIdx();
+
+ ID3D11View* view = nullptr;
+ DXGI_FORMAT format = DXGI_FORMAT_UNKNOWN;
+ unsigned width = 0;
+ unsigned height = 0;
+
+protected:
+ explicit CVideoBuffer(int id);
+
+private:
+ AVFrame* m_pFrame{nullptr};
+};
+
+class CVideoBufferShared : public CVideoBuffer
+{
+ template<typename TBuffer>
+ friend class CVideoBufferPoolTyped;
+
+public:
+ HRESULT GetResource(ID3D11Resource** ppResource) override;
+ void Initialize(CDecoder* decoder) override;
+
+protected:
+ explicit CVideoBufferShared(int id)
+ : CVideoBuffer(id) {}
+
+ HANDLE handle = INVALID_HANDLE_VALUE;
+ Microsoft::WRL::ComPtr<ID3D11Resource> m_sharedRes;
+};
+
+class CVideoBufferCopy : public CVideoBufferShared
+{
+ template<typename TBuffer>
+ friend class CVideoBufferPoolTyped;
+
+public:
+ void Initialize(CDecoder* decoder) override;
+ unsigned GetIdx() override { return 0; }
+
+protected:
+ explicit CVideoBufferCopy(int id)
+ : CVideoBufferShared(id) {}
+
+ Microsoft::WRL::ComPtr<ID3D11Resource> m_copyRes;
+ Microsoft::WRL::ComPtr<ID3D11Resource> m_pResource;
+ Microsoft::WRL::ComPtr<ID3D11DeviceContext> m_pDeviceContext;
+};
+
+class CContext
+{
+public:
+ typedef std::shared_ptr<CContext> shared_ptr;
+ typedef std::weak_ptr<CContext> weak_ptr;
+
+ ~CContext();
+
+ static shared_ptr EnsureContext(CDecoder* decoder);
+ bool GetFormatAndConfig(AVCodecContext* avctx, D3D11_VIDEO_DECODER_DESC& format, D3D11_VIDEO_DECODER_CONFIG& config) const;
+ bool CreateSurfaces(const D3D11_VIDEO_DECODER_DESC& format, uint32_t count, uint32_t alignment,
+ ID3D11VideoDecoderOutputView** surfaces, HANDLE* pHandle, bool trueShared) const;
+ bool CreateDecoder(const D3D11_VIDEO_DECODER_DESC& format, const D3D11_VIDEO_DECODER_CONFIG& config,
+ ID3D11VideoDecoder** decoder, ID3D11VideoContext** context);
+ void Release(CDecoder* decoder);
+
+ bool Check() const;
+ bool Reset();
+ bool IsContextShared() const
+ {
+ return m_sharingAllowed;
+ }
+ bool HasAMDWorkaround() const
+ {
+ return m_atiWorkaround;
+ }
+
+private:
+ explicit CContext() = default;
+
+ void Close();
+ bool CreateContext();
+ void DestroyContext();
+ void QueryCaps();
+ bool IsValidDecoder(CDecoder* decoder);
+ bool GetConfig(const D3D11_VIDEO_DECODER_DESC& format, D3D11_VIDEO_DECODER_CONFIG& config) const;
+
+ static weak_ptr m_context;
+ static CCriticalSection m_section;
+
+ UINT m_input_count = 0;
+ GUID* m_input_list = nullptr;
+ bool m_atiWorkaround = false;
+ bool m_sharingAllowed = false;
+ Microsoft::WRL::ComPtr<ID3D11VideoContext> m_pD3D11Context;
+ Microsoft::WRL::ComPtr<ID3D11VideoDevice> m_pD3D11Device;
+ std::vector<CDecoder*> m_decoders;
+};
+
+class CVideoBufferPool : public IVideoBufferPool
+{
+public:
+ typedef std::shared_ptr<CVideoBufferPool> shared_ptr;
+
+ CVideoBufferPool();
+ virtual ~CVideoBufferPool();
+
+ // IVideoBufferPool overrides
+ ::CVideoBuffer* Get() override;
+ void Return(int id) override;
+
+ // views pool
+ void AddView(ID3D11View* view);
+ bool ReturnView(ID3D11View* view);
+ ID3D11View* GetView();
+ bool IsValid(ID3D11View* view);
+ size_t Size();
+ bool HasFree();
+ bool HasRefs();
+
+protected:
+ void Reset();
+ virtual CVideoBuffer* CreateBuffer(int idx) = 0;
+
+ CCriticalSection m_section;
+
+ std::vector<ID3D11View*> m_views;
+ std::deque<size_t> m_freeViews;
+ std::vector<CVideoBuffer*> m_out;
+ std::deque<size_t> m_freeOut;
+};
+
+template<typename TBuffer>
+class CVideoBufferPoolTyped : public CVideoBufferPool
+{
+protected:
+ CVideoBuffer* CreateBuffer(int idx) override
+ {
+ return new TBuffer(idx);
+ }
+};
+
+class CDecoder : public IHardwareDecoder, public ID3DResource
+{
+public:
+ ~CDecoder() override;
+
+ static IHardwareDecoder* Create(CDVDStreamInfo& hint, CProcessInfo& processInfo, AVPixelFormat fmt);
+ static bool Register();
+
+ // IHardwareDecoder overrides
+ bool Open(AVCodecContext* avctx, AVCodecContext* mainctx, const enum AVPixelFormat) override;
+ CDVDVideoCodec::VCReturn Decode(AVCodecContext* avctx, AVFrame* frame) override;
+ bool GetPicture(AVCodecContext* avctx, VideoPicture* picture) override;
+ CDVDVideoCodec::VCReturn Check(AVCodecContext* avctx) override;
+ const std::string Name() override { return "d3d11va"; }
+ unsigned GetAllowedReferences() override;
+ void Reset() override;
+
+ // IDVDResourceCounted overrides
+ long Release() override;
+
+ bool OpenDecoder();
+ int GetBuffer(AVCodecContext* avctx, AVFrame* pic);
+ void ReleaseBuffer(uint8_t* data);
+ void Close();
+ void CloseDXVADecoder();
+
+ //static members
+ static bool Supports(enum AVPixelFormat fmt);
+ static int FFGetBuffer(AVCodecContext* avctx, AVFrame* pic, int flags);
+ static void FFReleaseBuffer(void* opaque, uint8_t* data);
+
+protected:
+ friend CVideoBuffer;
+ friend CVideoBufferShared;
+ friend CVideoBufferCopy;
+
+ explicit CDecoder(CProcessInfo& processInfo);
+
+ enum EDeviceState
+ {
+ DXVA_OPEN,
+ DXVA_RESET,
+ DXVA_LOST
+ } m_state = DXVA_OPEN;
+
+
+ // ID3DResource overrides
+ void OnCreateDevice() override
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_state = DXVA_RESET;
+ m_event.Set();
+ }
+ void OnDestroyDevice(bool fatal) override
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_state = DXVA_LOST;
+ m_event.Reset();
+ }
+
+ CEvent m_event;
+ CCriticalSection m_section;
+ CProcessInfo& m_processInfo;
+ Microsoft::WRL::ComPtr<ID3D11VideoDecoder> m_pD3D11Decoder;
+ Microsoft::WRL::ComPtr<ID3D11VideoContext> m_pD3D11Context;
+ CVideoBufferPool::shared_ptr m_bufferPool;
+ CContext::shared_ptr m_dxvaContext;
+ CVideoBuffer* m_videoBuffer = nullptr;
+ struct AVD3D11VAContext* m_avD3D11Context = nullptr;
+ struct AVCodecContext* m_avCtx = nullptr;
+ int m_refs = 0;
+ unsigned int m_shared = 0;
+ unsigned int m_surface_alignment = 0;
+ HANDLE m_sharedHandle = INVALID_HANDLE_VALUE;
+ D3D11_VIDEO_DECODER_DESC m_format = {};
+ bool m_DVDWorkaround = false;
+};
+
+} // namespace DXVA
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/VAAPI.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VAAPI.cpp
new file mode 100644
index 0000000..0487b32
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VAAPI.cpp
@@ -0,0 +1,3242 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "VAAPI.h"
+
+#include "DVDVideoCodec.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDCodecUtils.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/MemUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <array>
+#include <mutex>
+
+#include <drm_fourcc.h>
+#include <va/va_drm.h>
+#include <va/va_drmcommon.h>
+
+extern "C" {
+#include <libavutil/avutil.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_vaapi.h>
+#include <libavutil/opt.h>
+#include <libavfilter/buffersink.h>
+#include <libavfilter/buffersrc.h>
+}
+
+#include "system_egl.h"
+
+#include <EGL/eglext.h>
+#include <va/va_vpp.h>
+#include <xf86drm.h>
+
+#if VA_CHECK_VERSION(1, 0, 0)
+# include <va/va_str.h>
+#endif
+
+using namespace VAAPI;
+using namespace std::chrono_literals;
+
+#define NUM_RENDER_PICS 7
+
+constexpr auto SETTING_VIDEOPLAYER_USEVAAPI = "videoplayer.usevaapi";
+constexpr auto SETTING_VIDEOPLAYER_USEVAAPIAV1 = "videoplayer.usevaapiav1";
+constexpr auto SETTING_VIDEOPLAYER_USEVAAPIHEVC = "videoplayer.usevaapihevc";
+constexpr auto SETTING_VIDEOPLAYER_USEVAAPIMPEG2 = "videoplayer.usevaapimpeg2";
+constexpr auto SETTING_VIDEOPLAYER_USEVAAPIMPEG4 = "videoplayer.usevaapimpeg4";
+constexpr auto SETTING_VIDEOPLAYER_USEVAAPIVC1 = "videoplayer.usevaapivc1";
+constexpr auto SETTING_VIDEOPLAYER_USEVAAPIVP8 = "videoplayer.usevaapivp8";
+constexpr auto SETTING_VIDEOPLAYER_USEVAAPIVP9 = "videoplayer.usevaapivp9";
+constexpr auto SETTING_VIDEOPLAYER_PREFERVAAPIRENDER = "videoplayer.prefervaapirender";
+
+void VAAPI::VaErrorCallback(void *user_context, const char *message)
+{
+ std::string str{message};
+ CLog::Log(LOGERROR, "libva error: {}", StringUtils::TrimRight(str));
+}
+
+void VAAPI::VaInfoCallback(void *user_context, const char *message)
+{
+ std::string str{message};
+ CLog::Log(LOGDEBUG, "libva info: {}", StringUtils::TrimRight(str));
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+CVAAPIContext *CVAAPIContext::m_context = 0;
+CCriticalSection CVAAPIContext::m_section;
+
+CVAAPIContext::CVAAPIContext()
+{
+ m_context = 0;
+ m_refCount = 0;
+ m_profiles = NULL;
+}
+
+void CVAAPIContext::Release(CDecoder *decoder)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ auto it = find(m_decoders.begin(), m_decoders.end(), decoder);
+ if (it != m_decoders.end())
+ m_decoders.erase(it);
+
+ m_refCount--;
+ if (m_refCount <= 0)
+ {
+ Close();
+ delete this;
+ m_context = 0;
+ }
+}
+
+void CVAAPIContext::Close()
+{
+ CLog::Log(LOGINFO, "VAAPI::Close - closing decoder context");
+ if (m_renderNodeFD >= 0)
+ {
+ close(m_renderNodeFD);
+ }
+
+ DestroyContext();
+}
+
+bool CVAAPIContext::EnsureContext(CVAAPIContext **ctx, CDecoder *decoder)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (m_context)
+ {
+ m_context->m_refCount++;
+ *ctx = m_context;
+ if (!m_context->IsValidDecoder(decoder))
+ m_context->m_decoders.push_back(decoder);
+ return true;
+ }
+
+ m_context = new CVAAPIContext();
+ *ctx = m_context;
+ {
+ std::unique_lock<CCriticalSection> gLock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ if (!m_context->CreateContext())
+ {
+ delete m_context;
+ m_context = 0;
+ *ctx = NULL;
+ return false;
+ }
+ }
+
+ m_context->m_refCount++;
+
+ if (!m_context->IsValidDecoder(decoder))
+ m_context->m_decoders.push_back(decoder);
+ *ctx = m_context;
+ return true;
+}
+
+void CVAAPIContext::SetValidDRMVaDisplayFromRenderNode()
+{
+ int const buf_size{128};
+ char name[buf_size];
+ int fd{-1};
+
+ // 128 is the start of the NUM in renderD<NUM>
+ for (int i = 128; i < (128 + 16); i++)
+ {
+ snprintf(name, buf_size, "/dev/dri/renderD%d", i);
+
+ fd = open(name, O_RDWR);
+
+ if (fd < 0)
+ {
+ continue;
+ }
+
+ auto display = vaGetDisplayDRM(fd);
+
+ if (display != nullptr)
+ {
+ m_renderNodeFD = fd;
+ m_display = display;
+ return;
+ }
+ close(fd);
+ }
+
+ CLog::Log(LOGERROR, "Failed to find any open render nodes in /dev/dri/renderD<num>");
+}
+
+void CVAAPIContext::SetVaDisplayForSystem()
+{
+ m_display = CDecoder::m_pWinSystem->GetVADisplay();
+
+ // Fallback to DRM
+ if (!m_display)
+ {
+ // Render nodes depends on kernel >= 3.15
+ SetValidDRMVaDisplayFromRenderNode();
+ }
+}
+
+bool CVAAPIContext::CreateContext()
+{
+ SetVaDisplayForSystem();
+
+ if (m_display == nullptr)
+ {
+ CLog::Log(LOGERROR, "Failed to find any VaDisplays for this system");
+ return false;
+ }
+
+#if VA_CHECK_VERSION(1, 0, 0)
+ vaSetErrorCallback(m_display, VaErrorCallback, nullptr);
+ vaSetInfoCallback(m_display, VaInfoCallback, nullptr);
+#endif
+
+ int major_version, minor_version;
+ if (!CheckSuccess(vaInitialize(m_display, &major_version, &minor_version), "vaInitialize"))
+ {
+ vaTerminate(m_display);
+ m_display = NULL;
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI - initialize version {}.{}", major_version, minor_version);
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI - driver in use: {}", vaQueryVendorString(m_display));
+
+ QueryCaps();
+ if (!m_profileCount)
+ return false;
+
+ return true;
+}
+
+void CVAAPIContext::DestroyContext()
+{
+ delete[] m_profiles;
+ if (m_display)
+ {
+ if (CheckSuccess(vaTerminate(m_display), "vaTerminate"))
+ {
+ m_display = NULL;
+ }
+ else
+ {
+#if VA_CHECK_VERSION(1, 0, 0)
+ vaSetErrorCallback(m_display, nullptr, nullptr);
+ vaSetInfoCallback(m_display, nullptr, nullptr);
+#endif
+ }
+ }
+}
+
+void CVAAPIContext::QueryCaps()
+{
+ m_profileCount = 0;
+
+ int max_profiles = vaMaxNumProfiles(m_display);
+ m_profiles = new VAProfile[max_profiles];
+
+ if (!CheckSuccess(vaQueryConfigProfiles(m_display, m_profiles, &m_profileCount), "vaQueryConfigProfiles"))
+ return;
+
+ for(int i = 0; i < m_profileCount; i++)
+ {
+#if VA_CHECK_VERSION(1, 0, 0)
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI - profile {}", vaProfileStr(m_profiles[i]));
+#else
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI - profile {}", m_profiles[i]);
+#endif
+ }
+}
+
+VAConfigAttrib CVAAPIContext::GetAttrib(VAProfile profile)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ VAConfigAttrib attrib;
+ attrib.type = VAConfigAttribRTFormat;
+ CheckSuccess(vaGetConfigAttributes(m_display, profile, VAEntrypointVLD, &attrib, 1), "vaGetConfigAttributes");
+
+ return attrib;
+}
+
+bool CVAAPIContext::SupportsProfile(VAProfile profile)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ for (int i=0; i<m_profileCount; i++)
+ {
+ if (m_profiles[i] == profile)
+ return true;
+ }
+ return false;
+}
+
+VAConfigID CVAAPIContext::CreateConfig(VAProfile profile, VAConfigAttrib attrib)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ VAConfigID config = VA_INVALID_ID;
+ CheckSuccess(vaCreateConfig(m_display, profile, VAEntrypointVLD, &attrib, 1, &config), "vaCreateConfig");
+
+ return config;
+}
+
+bool CVAAPIContext::CheckSuccess(VAStatus status, const std::string& function)
+{
+ if (status != VA_STATUS_SUCCESS)
+ {
+ CLog::Log(LOGERROR, "VAAPI/context {} error: {} ({})", function, vaErrorStr(status), status);
+ return false;
+ }
+ return true;
+}
+
+VADisplay CVAAPIContext::GetDisplay()
+{
+ return m_display;
+}
+
+bool CVAAPIContext::IsValidDecoder(CDecoder *decoder)
+{
+ auto it = find(m_decoders.begin(), m_decoders.end(), decoder);
+ if (it != m_decoders.end())
+ return true;
+
+ return false;
+}
+
+void CVAAPIContext::FFReleaseBuffer(void *opaque, uint8_t *data)
+{
+ CDecoder *va = static_cast<CDecoder*>(opaque);
+ if (m_context && m_context->IsValidDecoder(va))
+ {
+ va->FFReleaseBuffer(data);
+ }
+}
+
+//-----------------------------------------------------------------------------
+// VAAPI Video Surface states
+//-----------------------------------------------------------------------------
+
+#define SURFACE_USED_FOR_REFERENCE 0x01
+#define SURFACE_USED_FOR_RENDER 0x02
+
+void CVideoSurfaces::AddSurface(VASurfaceID surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_state[surf] = 0;
+ m_freeSurfaces.push_back(surf);
+}
+
+void CVideoSurfaces::ClearReference(VASurfaceID surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_state.find(surf) == m_state.end())
+ {
+ CLog::Log(LOGWARNING, "CVideoSurfaces::ClearReference - surface invalid");
+ return;
+ }
+ m_state[surf] &= ~SURFACE_USED_FOR_REFERENCE;
+ if (m_state[surf] == 0)
+ {
+ m_freeSurfaces.push_back(surf);
+ }
+}
+
+bool CVideoSurfaces::MarkRender(VASurfaceID surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_state.find(surf) == m_state.end())
+ {
+ CLog::Log(LOGWARNING, "CVideoSurfaces::MarkRender - surface invalid");
+ return false;
+ }
+ auto it = std::find(m_freeSurfaces.begin(), m_freeSurfaces.end(), surf);
+ if (it != m_freeSurfaces.end())
+ {
+ m_freeSurfaces.erase(it);
+ }
+ m_state[surf] |= SURFACE_USED_FOR_RENDER;
+ return true;
+}
+
+void CVideoSurfaces::ClearRender(VASurfaceID surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_state.find(surf) == m_state.end())
+ {
+ CLog::Log(LOGWARNING, "CVideoSurfaces::ClearRender - surface invalid");
+ return;
+ }
+ m_state[surf] &= ~SURFACE_USED_FOR_RENDER;
+ if (m_state[surf] == 0)
+ {
+ m_freeSurfaces.push_back(surf);
+ }
+}
+
+bool CVideoSurfaces::IsValid(VASurfaceID surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_state.find(surf) != m_state.end())
+ return true;
+ else
+ return false;
+}
+
+VASurfaceID CVideoSurfaces::GetFree(VASurfaceID surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_state.find(surf) != m_state.end())
+ {
+ auto it = std::find(m_freeSurfaces.begin(), m_freeSurfaces.end(), surf);
+ if (it == m_freeSurfaces.end())
+ {
+ CLog::Log(LOGWARNING, "CVideoSurfaces::GetFree - surface not free");
+ }
+ else
+ {
+ m_freeSurfaces.erase(it);
+ m_state[surf] = SURFACE_USED_FOR_REFERENCE;
+ return surf;
+ }
+ }
+
+ if (!m_freeSurfaces.empty())
+ {
+ VASurfaceID freeSurf = m_freeSurfaces.front();
+ m_freeSurfaces.pop_front();
+ m_state[freeSurf] = SURFACE_USED_FOR_REFERENCE;
+ return freeSurf;
+ }
+
+ return VA_INVALID_SURFACE;
+}
+
+VASurfaceID CVideoSurfaces::GetAtIndex(int idx)
+{
+ if ((size_t) idx >= m_state.size())
+ return VA_INVALID_SURFACE;
+
+ auto it = m_state.begin();
+ for(int i = 0; i < idx; i++)
+ ++it;
+ return it->first;
+}
+
+VASurfaceID CVideoSurfaces::RemoveNext(bool skiprender)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ VASurfaceID surf;
+ for(auto it = m_state.begin(); it != m_state.end(); ++it)
+ {
+ if (skiprender && it->second & SURFACE_USED_FOR_RENDER)
+ continue;
+ surf = it->first;
+ m_state.erase(surf);
+
+ auto it2 = std::find(m_freeSurfaces.begin(), m_freeSurfaces.end(), surf);
+ if (it2 != m_freeSurfaces.end())
+ m_freeSurfaces.erase(it2);
+ return surf;
+ }
+ return VA_INVALID_SURFACE;
+}
+
+void CVideoSurfaces::Reset()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_freeSurfaces.clear();
+ m_state.clear();
+}
+
+int CVideoSurfaces::Size()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ return m_state.size();
+}
+
+bool CVideoSurfaces::HasFree()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ return !m_freeSurfaces.empty();
+}
+
+int CVideoSurfaces::NumFree()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ return m_freeSurfaces.size();
+}
+
+bool CVideoSurfaces::HasRefs()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for (const auto &i : m_state)
+ {
+ if (i.second & SURFACE_USED_FOR_REFERENCE)
+ return true;
+ }
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// VAAPI
+//-----------------------------------------------------------------------------
+
+bool CDecoder::m_capGeneral = false;
+bool CDecoder::m_capDeepColor = false;
+IVaapiWinSystem* CDecoder::m_pWinSystem = nullptr;
+
+CDecoder::CDecoder(CProcessInfo& processInfo) :
+ m_vaapiOutput(*this, &m_inMsgEvent),
+ m_processInfo(processInfo)
+{
+ m_vaapiConfig.videoSurfaces = &m_videoSurfaces;
+
+ m_vaapiConfigured = false;
+ m_DisplayState = VAAPI_OPEN;
+ m_vaapiConfig.context = 0;
+ m_vaapiConfig.configId = VA_INVALID_ID;
+ m_vaapiConfig.processInfo = &m_processInfo;
+ m_avctx = nullptr;
+ m_getBufferError = 0;
+}
+
+CDecoder::~CDecoder()
+{
+ Close();
+}
+
+bool CDecoder::Open(AVCodecContext* avctx, AVCodecContext* mainctx, const enum AVPixelFormat fmt)
+{
+ if (!m_capGeneral)
+ return false;
+
+ // check if user wants to decode this format with VAAPI
+ std::map<AVCodecID, std::string> settings_map = {
+ {AV_CODEC_ID_H263, SETTING_VIDEOPLAYER_USEVAAPIMPEG4},
+ {AV_CODEC_ID_MPEG4, SETTING_VIDEOPLAYER_USEVAAPIMPEG4},
+ {AV_CODEC_ID_WMV3, SETTING_VIDEOPLAYER_USEVAAPIVC1},
+ {AV_CODEC_ID_VC1, SETTING_VIDEOPLAYER_USEVAAPIVC1},
+ {AV_CODEC_ID_MPEG2VIDEO, SETTING_VIDEOPLAYER_USEVAAPIMPEG2},
+ {AV_CODEC_ID_VP8, SETTING_VIDEOPLAYER_USEVAAPIVP8},
+ {AV_CODEC_ID_VP9, SETTING_VIDEOPLAYER_USEVAAPIVP9},
+ {AV_CODEC_ID_HEVC, SETTING_VIDEOPLAYER_USEVAAPIHEVC},
+ {AV_CODEC_ID_AV1, SETTING_VIDEOPLAYER_USEVAAPIAV1},
+ };
+
+ auto entry = settings_map.find(avctx->codec_id);
+ if (entry != settings_map.end())
+ {
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return false;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return false;
+
+ auto setting = settings->GetSetting(entry->second);
+ if (!setting)
+ {
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", entry->second);
+ return false;
+ }
+
+ bool enabled = settings->GetBool(entry->second) && setting->IsVisible();
+ if (!enabled)
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI - open decoder");
+
+ if (!CVAAPIContext::EnsureContext(&m_vaapiConfig.context, this))
+ return false;
+
+ if(avctx->coded_width == 0 ||
+ avctx->coded_height == 0)
+ {
+ CLog::Log(LOGWARNING,"VAAPI::Open: no width/height available, can't init");
+ return false;
+ }
+
+ m_vaapiConfig.driverIsMesa = StringUtils::StartsWith(vaQueryVendorString(m_vaapiConfig.context->GetDisplay()), "Mesa");
+ m_vaapiConfig.vidWidth = avctx->width;
+ m_vaapiConfig.vidHeight = avctx->height;
+ m_vaapiConfig.outWidth = avctx->width;
+ m_vaapiConfig.outHeight = avctx->height;
+ m_vaapiConfig.surfaceWidth = avctx->coded_width;
+ m_vaapiConfig.surfaceHeight = avctx->coded_height;
+ m_vaapiConfig.aspect = avctx->sample_aspect_ratio;
+ m_vaapiConfig.bitDepth = avctx->bits_per_raw_sample;
+ m_DisplayState = VAAPI_OPEN;
+ m_vaapiConfigured = false;
+ m_presentPicture = nullptr;
+ m_getBufferError = 0;
+
+ VAProfile profile;
+ switch (avctx->codec_id)
+ {
+ case AV_CODEC_ID_MPEG2VIDEO:
+ profile = VAProfileMPEG2Main;
+ if (!m_vaapiConfig.context->SupportsProfile(profile))
+ return false;
+ break;
+ case AV_CODEC_ID_MPEG4:
+ case AV_CODEC_ID_H263:
+ profile = VAProfileMPEG4AdvancedSimple;
+ if (!m_vaapiConfig.context->SupportsProfile(profile))
+ return false;
+ break;
+ case AV_CODEC_ID_H264:
+ {
+ if (avctx->profile == FF_PROFILE_H264_CONSTRAINED_BASELINE)
+ {
+ profile = VAProfileH264ConstrainedBaseline;
+ if (!m_vaapiConfig.context->SupportsProfile(profile))
+ return false;
+ }
+ else
+ {
+ if(avctx->profile == FF_PROFILE_H264_MAIN)
+ {
+ profile = VAProfileH264Main;
+ if (m_vaapiConfig.context->SupportsProfile(profile))
+ break;
+ }
+ profile = VAProfileH264High;
+ if (!m_vaapiConfig.context->SupportsProfile(profile))
+ return false;
+ }
+ break;
+ }
+ case AV_CODEC_ID_HEVC:
+ {
+ if (avctx->profile == FF_PROFILE_HEVC_MAIN_10)
+ {
+ if (!m_capDeepColor)
+ return false;
+
+ profile = VAProfileHEVCMain10;
+ }
+ else if (avctx->profile == FF_PROFILE_HEVC_MAIN)
+ profile = VAProfileHEVCMain;
+ else
+ profile = VAProfileNone;
+ if (!m_vaapiConfig.context->SupportsProfile(profile))
+ return false;
+ break;
+ }
+ case AV_CODEC_ID_VP8:
+ {
+ profile = VAProfileVP8Version0_3;
+ if (!m_vaapiConfig.context->SupportsProfile(profile))
+ return false;
+ break;
+ }
+ case AV_CODEC_ID_VP9:
+ {
+ if (avctx->profile == FF_PROFILE_VP9_0)
+ profile = VAProfileVP9Profile0;
+ else if (avctx->profile == FF_PROFILE_VP9_2)
+ profile = VAProfileVP9Profile2;
+ else
+ profile = VAProfileNone;
+ if (!m_vaapiConfig.context->SupportsProfile(profile))
+ return false;
+ break;
+ }
+ case AV_CODEC_ID_WMV3:
+ profile = VAProfileVC1Main;
+ if (!m_vaapiConfig.context->SupportsProfile(profile))
+ return false;
+ break;
+ case AV_CODEC_ID_VC1:
+ profile = VAProfileVC1Advanced;
+ if (!m_vaapiConfig.context->SupportsProfile(profile))
+ return false;
+ break;
+#if VA_CHECK_VERSION(1, 8, 0)
+ case AV_CODEC_ID_AV1:
+ {
+ if (avctx->profile == FF_PROFILE_AV1_MAIN)
+ profile = VAProfileAV1Profile0;
+ else if (avctx->profile == FF_PROFILE_AV1_HIGH)
+ profile = VAProfileAV1Profile1;
+ else
+ profile = VAProfileNone;
+ if (!m_vaapiConfig.context->SupportsProfile(profile))
+ return false;
+ break;
+ }
+#endif
+ default:
+ return false;
+ }
+
+ m_vaapiConfig.profile = profile;
+ m_vaapiConfig.attrib = m_vaapiConfig.context->GetAttrib(profile);
+ if ((m_vaapiConfig.attrib.value & (VA_RT_FORMAT_YUV420 | VA_RT_FORMAT_YUV420_10BPP)) == 0)
+ {
+ CLog::Log(LOGERROR, "VAAPI - invalid yuv format {:x}", m_vaapiConfig.attrib.value);
+ return false;
+ }
+
+ if (avctx->codec_id == AV_CODEC_ID_H264)
+ {
+ m_vaapiConfig.maxReferences = avctx->refs;
+ if (m_vaapiConfig.maxReferences > 16)
+ m_vaapiConfig.maxReferences = 16;
+ if (m_vaapiConfig.maxReferences < 5)
+ m_vaapiConfig.maxReferences = 5;
+ }
+ else if (avctx->codec_id == AV_CODEC_ID_HEVC)
+ m_vaapiConfig.maxReferences = 16;
+ else if (avctx->codec_id == AV_CODEC_ID_VP9)
+ m_vaapiConfig.maxReferences = 8;
+ else if (avctx->codec_id == AV_CODEC_ID_AV1)
+ m_vaapiConfig.maxReferences = 18;
+ else
+ m_vaapiConfig.maxReferences = 2;
+
+ // add an extra surface for safety, some faulty material
+ // make ffmpeg require more buffers
+ m_vaapiConfig.maxReferences += 6;
+
+ if (!ConfigVAAPI())
+ {
+ return false;
+ }
+
+ m_deviceRef = std::unique_ptr<AVBufferRef, AVBufferRefDeleter>(
+ av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI), AVBufferRefDeleter());
+
+ AVHWDeviceContext* deviceCtx = (AVHWDeviceContext*)m_deviceRef->data;
+ AVVAAPIDeviceContext *vaapiDeviceCtx = (AVVAAPIDeviceContext*)deviceCtx->hwctx;
+ AVBufferRef* framesRef = av_hwframe_ctx_alloc(m_deviceRef.get());
+ AVHWFramesContext *framesCtx = (AVHWFramesContext*)framesRef->data;
+ AVVAAPIFramesContext *vaapiFramesCtx = (AVVAAPIFramesContext*)framesCtx->hwctx;
+
+ vaapiDeviceCtx->display = m_vaapiConfig.dpy;
+ vaapiDeviceCtx->driver_quirks = AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS;
+ vaapiFramesCtx->nb_attributes = 0;
+ vaapiFramesCtx->nb_surfaces = m_videoSurfaces.Size();
+ VASurfaceID *surfaceIds = (VASurfaceID*)av_malloc(vaapiFramesCtx->nb_surfaces * sizeof(VASurfaceID));
+ for (int i=0; i<vaapiFramesCtx->nb_surfaces; ++i)
+ surfaceIds[i] = m_videoSurfaces.GetAtIndex(i);
+ vaapiFramesCtx->surface_ids = surfaceIds;
+ framesCtx->format = AV_PIX_FMT_VAAPI;
+ framesCtx->width = avctx->coded_width;
+ framesCtx->height = avctx->coded_height;
+
+ avctx->hw_frames_ctx = framesRef;
+ avctx->get_buffer2 = CDecoder::FFGetBuffer;
+ avctx->slice_flags = SLICE_FLAG_CODED_ORDER|SLICE_FLAG_ALLOW_FIELD;
+
+ m_avctx = mainctx;
+ return true;
+}
+
+void CDecoder::Close()
+{
+ CLog::Log(LOGINFO, "VAAPI::{}", __FUNCTION__);
+
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+
+ FiniVAAPIOutput();
+
+ m_deviceRef.reset();
+
+ if (m_vaapiConfig.context)
+ m_vaapiConfig.context->Release(this);
+ m_vaapiConfig.context = 0;
+}
+
+long CDecoder::Release()
+{
+ // if ffmpeg holds any references, flush buffers
+ if (m_avctx && m_videoSurfaces.HasRefs())
+ {
+ avcodec_flush_buffers(m_avctx);
+ }
+
+ if (m_presentPicture)
+ {
+ m_presentPicture->Release();
+ m_presentPicture = nullptr;
+ }
+ // check if we should do some pre-cleanup here
+ // a second decoder might need resources
+ if (m_vaapiConfigured == true)
+ {
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI::Release pre-cleanup");
+
+ std::unique_lock<CCriticalSection> lock1(CServiceBroker::GetWinSystem()->GetGfxContext());
+ Message *reply;
+ if (m_vaapiOutput.m_controlPort.SendOutMessageSync(COutputControlProtocol::PRECLEANUP,
+ &reply,
+ 2000))
+ {
+ bool success = reply->signal == COutputControlProtocol::ACC ? true : false;
+ reply->Release();
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "VAAPI::{} - pre-cleanup returned error", __FUNCTION__);
+ m_DisplayState = VAAPI_ERROR;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "VAAPI::{} - pre-cleanup timed out", __FUNCTION__);
+ m_DisplayState = VAAPI_ERROR;
+ }
+
+ VASurfaceID surf;
+ while((surf = m_videoSurfaces.RemoveNext(true)) != VA_INVALID_SURFACE)
+ {
+ CheckSuccess(vaDestroySurfaces(m_vaapiConfig.dpy, &surf, 1), "vaDestroySurfaces");
+ }
+ }
+ return IHardwareDecoder::Release();
+}
+
+long CDecoder::ReleasePicReference()
+{
+ return IHardwareDecoder::Release();
+}
+
+int CDecoder::FFGetBuffer(AVCodecContext *avctx, AVFrame *pic, int flags)
+{
+ ICallbackHWAccel* cb = static_cast<ICallbackHWAccel*>(avctx->opaque);
+ CDecoder* va = static_cast<CDecoder*>(cb->GetHWAccel());
+
+ // while we are waiting to recover we can't do anything
+ std::unique_lock<CCriticalSection> lock(va->m_DecoderSection);
+
+ if(va->m_DisplayState != VAAPI_OPEN)
+ {
+ CLog::Log(LOGWARNING, "VAAPI::FFGetBuffer - returning due to awaiting recovery");
+ return -1;
+ }
+
+ VASurfaceID surf = (VASurfaceID)(uintptr_t)pic->data[3];
+ surf = va->m_videoSurfaces.GetFree(surf != 0 ? surf : VA_INVALID_SURFACE);
+
+ if (surf == VA_INVALID_SURFACE)
+ {
+ uint16_t decoded, processed, render;
+ bool vpp;
+ va->m_bufferStats.Get(decoded, processed, render, vpp);
+ CLog::Log(LOGWARNING, "VAAPI::FFGetBuffer - no surface available - dec: {}, render: {}",
+ decoded, render);
+ va->m_getBufferError++;
+ return -1;
+ }
+
+ va->m_getBufferError = 0;
+
+ pic->data[1] = pic->data[2] = NULL;
+ pic->data[0] = (uint8_t*)(uintptr_t)surf;
+ pic->data[3] = (uint8_t*)(uintptr_t)surf;
+ pic->linesize[0] = pic->linesize[1] = pic->linesize[2] = 0;
+ AVBufferRef *buffer = av_buffer_create(pic->data[3], 0, CVAAPIContext::FFReleaseBuffer, va, 0);
+ if (!buffer)
+ {
+ CLog::Log(LOGERROR, "VAAPI::{} - error creating buffer", __FUNCTION__);
+ return -1;
+ }
+ pic->buf[0] = buffer;
+
+ pic->reordered_opaque = avctx->reordered_opaque;
+ va->Acquire();
+ return 0;
+}
+
+void CDecoder::FFReleaseBuffer(uint8_t *data)
+{
+ {
+ VASurfaceID surf;
+
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+
+ surf = (VASurfaceID)(uintptr_t)data;
+ m_videoSurfaces.ClearReference(surf);
+ }
+
+ IHardwareDecoder::Release();
+}
+
+void CDecoder::SetCodecControl(int flags)
+{
+ m_codecControl = flags & (DVD_CODEC_CTRL_DRAIN | DVD_CODEC_CTRL_HURRY);
+}
+
+CDVDVideoCodec::VCReturn CDecoder::Decode(AVCodecContext* avctx, AVFrame* pFrame)
+{
+ CDVDVideoCodec::VCReturn result = Check(avctx);
+ if (result != CDVDVideoCodec::VC_NOBUFFER && result != CDVDVideoCodec::VC_NONE)
+ return result;
+
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+
+ if (!m_vaapiConfigured)
+ return CDVDVideoCodec::VC_ERROR;
+
+ if (pFrame)
+ { // we have a new frame from decoder
+
+ VASurfaceID surf = (VASurfaceID)(uintptr_t)pFrame->data[3];
+ // ffmpeg vc-1 decoder does not flush, make sure the data buffer is still valid
+ if (!m_videoSurfaces.IsValid(surf))
+ {
+ CLog::Log(LOGWARNING, "VAAPI::Decode - ignoring invalid buffer");
+ return CDVDVideoCodec::VC_BUFFER;
+ }
+ m_videoSurfaces.MarkRender(surf);
+
+ // send frame to output for processing
+ CVaapiDecodedPicture *pic = new CVaapiDecodedPicture();
+ static_cast<ICallbackHWAccel*>(avctx->opaque)->GetPictureCommon(&(pic->DVDPic));
+ m_codecControl = pic->DVDPic.iFlags & (DVD_CODEC_CTRL_HURRY | DVD_CODEC_CTRL_NO_POSTPROC);
+ pic->videoSurface = surf;
+ m_bufferStats.IncDecoded();
+ CPayloadWrap<CVaapiDecodedPicture> *payload = new CPayloadWrap<CVaapiDecodedPicture>(pic);
+ m_vaapiOutput.m_dataPort.SendOutMessage(COutputDataProtocol::NEWFRAME, payload);
+ }
+
+ uint16_t decoded, processed, render;
+ bool vpp;
+ Message *msg;
+ while (m_vaapiOutput.m_controlPort.ReceiveInMessage(&msg))
+ {
+ if (msg->signal == COutputControlProtocol::ERROR)
+ {
+ m_DisplayState = VAAPI_ERROR;
+ msg->Release();
+ return CDVDVideoCodec::VC_ERROR;
+ }
+ msg->Release();
+ }
+
+ bool drain = (m_codecControl & DVD_CODEC_CTRL_DRAIN);
+
+ m_bufferStats.Get(decoded, processed, render, vpp);
+ // if all pics are drained, break the loop by setting VC_EOF
+ if (drain && decoded <= 0 && processed <= 0 && render <= 0)
+ return CDVDVideoCodec::VC_EOF;
+
+ while (true)
+ {
+ // first fill the buffers to keep vaapi busy
+ if (!drain && decoded < 2 && processed < 3 && m_videoSurfaces.HasFree())
+ {
+ return CDVDVideoCodec::VC_BUFFER;
+ }
+ else if (m_vaapiOutput.m_dataPort.ReceiveInMessage(&msg))
+ {
+ if (msg->signal == COutputDataProtocol::PICTURE)
+ {
+ if (m_presentPicture)
+ {
+ m_presentPicture->Release();
+ m_presentPicture = nullptr;
+ }
+
+ m_presentPicture = *(CVaapiRenderPicture**)msg->data;
+ m_bufferStats.DecRender();
+ m_bufferStats.SetParams(0, m_codecControl);
+ msg->Release();
+ return CDVDVideoCodec::VC_PICTURE;
+ }
+ msg->Release();
+ }
+ else if (m_vaapiOutput.m_controlPort.ReceiveInMessage(&msg))
+ {
+ if (msg->signal == COutputControlProtocol::STATS)
+ {
+ msg->Release();
+ m_bufferStats.Get(decoded, processed, render, vpp);
+ if (!drain && decoded < 2 && processed < 3)
+ {
+ return CDVDVideoCodec::VC_BUFFER;
+ }
+ }
+ else
+ {
+ msg->Release();
+ m_DisplayState = VAAPI_ERROR;
+ return CDVDVideoCodec::VC_ERROR;
+ }
+ }
+
+ if (!m_inMsgEvent.Wait(2000ms))
+ break;
+ }
+
+ CLog::Log(LOGERROR,
+ "VAAPI::{} - timed out waiting for output message - decoded: {}, proc: {}, has free "
+ "surface: {}",
+ __FUNCTION__, decoded, processed, m_videoSurfaces.HasFree() ? "yes" : "no");
+ m_DisplayState = VAAPI_ERROR;
+
+ return CDVDVideoCodec::VC_ERROR;
+}
+
+CDVDVideoCodec::VCReturn CDecoder::Check(AVCodecContext* avctx)
+{
+ EDisplayState state;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+ state = m_DisplayState;
+ }
+
+ if (state == VAAPI_LOST)
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI::Check waiting for display reset event");
+ if (!m_DisplayEvent.Wait(4000ms))
+ {
+ CLog::Log(LOGERROR, "VAAPI::Check - device didn't reset in reasonable time");
+ state = VAAPI_RESET;
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+ state = m_DisplayState;
+ }
+ }
+ if (state == VAAPI_RESET || state == VAAPI_ERROR)
+ {
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+
+ avcodec_flush_buffers(avctx);
+ FiniVAAPIOutput();
+ if (m_vaapiConfig.context)
+ m_vaapiConfig.context->Release(this);
+ m_vaapiConfig.context = 0;
+
+ if (CVAAPIContext::EnsureContext(&m_vaapiConfig.context, this) && ConfigVAAPI())
+ {
+ m_DisplayState = VAAPI_OPEN;
+ }
+
+ if (state == VAAPI_RESET)
+ return CDVDVideoCodec::VC_FLUSHED;
+ else
+ return CDVDVideoCodec::VC_ERROR;
+ }
+
+ if (m_getBufferError > 0 && m_getBufferError < 5)
+ {
+ // if there is no other error, sleep for a short while
+ // in order not to drain player's message queue
+ KODI::TIME::Sleep(10ms);
+
+ return CDVDVideoCodec::VC_NOBUFFER;
+ }
+
+ return CDVDVideoCodec::VC_NONE;
+}
+
+bool CDecoder::GetPicture(AVCodecContext* avctx, VideoPicture* picture)
+{
+ if (picture->videoBuffer)
+ {
+ picture->videoBuffer->Release();
+ picture->videoBuffer = nullptr;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+
+ if (m_DisplayState != VAAPI_OPEN)
+ return false;
+
+ picture->SetParams(m_presentPicture->DVDPic);
+ picture->videoBuffer = m_presentPicture;
+ m_presentPicture = nullptr;
+
+ return true;
+}
+
+void CDecoder::Reset()
+{
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+
+ if (m_presentPicture)
+ {
+ m_presentPicture->Release();
+ m_presentPicture = nullptr;
+ }
+
+ if (!m_vaapiConfigured)
+ return;
+
+ Message *reply;
+ if (m_vaapiOutput.m_controlPort.SendOutMessageSync(COutputControlProtocol::FLUSH,
+ &reply,
+ 2000))
+ {
+ bool success = reply->signal == COutputControlProtocol::ACC ? true : false;
+ reply->Release();
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "VAAPI::{} - flush returned error", __FUNCTION__);
+ m_DisplayState = VAAPI_ERROR;
+ }
+ else
+ {
+ m_bufferStats.Reset();
+ m_getBufferError = 0;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "VAAPI::{} - flush timed out", __FUNCTION__);
+ m_DisplayState = VAAPI_ERROR;
+ }
+}
+
+bool CDecoder::CanSkipDeint()
+{
+ return m_bufferStats.CanSkipDeint();
+}
+
+bool CDecoder::CheckSuccess(VAStatus status, const std::string& function)
+{
+ if (status != VA_STATUS_SUCCESS)
+ {
+ CLog::Log(LOGERROR, "VAAPI/decoder {} error: {} ({})", function, vaErrorStr(status), status);
+ m_ErrorCount++;
+
+ if(m_DisplayState == VAAPI_OPEN)
+ {
+ if (m_ErrorCount > 2)
+ m_DisplayState = VAAPI_ERROR;
+ }
+ return false;
+ }
+ m_ErrorCount = 0;
+ return true;
+}
+
+bool CDecoder::ConfigVAAPI()
+{
+ m_vaapiConfig.dpy = m_vaapiConfig.context->GetDisplay();
+ m_vaapiConfig.attrib = m_vaapiConfig.context->GetAttrib(m_vaapiConfig.profile);
+ if ((m_vaapiConfig.attrib.value & (VA_RT_FORMAT_YUV420 | VA_RT_FORMAT_YUV420_10BPP)) == 0)
+ {
+ CLog::Log(LOGERROR, "VAAPI - invalid yuv format {:x}", m_vaapiConfig.attrib.value);
+ return false;
+ }
+
+ m_vaapiConfig.configId = m_vaapiConfig.context->CreateConfig(m_vaapiConfig.profile,
+ m_vaapiConfig.attrib);
+ if (m_vaapiConfig.configId == VA_INVALID_ID)
+ return false;
+
+ // create surfaces
+ unsigned int format = VA_RT_FORMAT_YUV420;
+ std::int32_t pixelFormat = VA_FOURCC_NV12;
+
+ if ((m_vaapiConfig.profile == VAProfileHEVCMain10 || m_vaapiConfig.profile == VAProfileVP9Profile2
+#if VA_CHECK_VERSION(1, 8, 0)
+ || m_vaapiConfig.profile == VAProfileAV1Profile0
+#endif
+ ) &&
+ m_vaapiConfig.bitDepth == 10)
+ {
+ format = VA_RT_FORMAT_YUV420_10BPP;
+ pixelFormat = VA_FOURCC_P010;
+ }
+
+ VASurfaceAttrib attribs[1], *attrib;
+ attrib = attribs;
+ attrib->flags = VA_SURFACE_ATTRIB_SETTABLE;
+ attrib->type = VASurfaceAttribPixelFormat;
+ attrib->value.type = VAGenericValueTypeInteger;
+ attrib->value.value.i = pixelFormat;
+
+ VASurfaceID surfaces[32];
+ int nb_surfaces = m_vaapiConfig.maxReferences;
+ if (!CheckSuccess(
+ vaCreateSurfaces(m_vaapiConfig.dpy, format, m_vaapiConfig.surfaceWidth,
+ m_vaapiConfig.surfaceHeight, surfaces,
+ nb_surfaces, attribs, 1), "vaCreateSurfaces"))
+ {
+ return false;
+ }
+ for (int i=0; i<nb_surfaces; i++)
+ {
+ m_videoSurfaces.AddSurface(surfaces[i]);
+ }
+
+ // initialize output
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ m_vaapiConfig.stats = &m_bufferStats;
+ m_bufferStats.Reset();
+ m_vaapiOutput.Start();
+ Message *reply;
+ if (m_vaapiOutput.m_controlPort.SendOutMessageSync(COutputControlProtocol::INIT,
+ &reply,
+ 2000,
+ &m_vaapiConfig,
+ sizeof(m_vaapiConfig)))
+ {
+ bool success = reply->signal == COutputControlProtocol::ACC ? true : false;
+ if (!success)
+ {
+ reply->Release();
+ CLog::Log(LOGERROR, "VAAPI::{} - vaapi output returned error", __FUNCTION__);
+ m_vaapiOutput.Dispose();
+ return false;
+ }
+ reply->Release();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "VAAPI::{} - failed to init output", __FUNCTION__);
+ m_vaapiOutput.Dispose();
+ return false;
+ }
+
+ m_inMsgEvent.Reset();
+ m_vaapiConfigured = true;
+ m_ErrorCount = 0;
+
+ return true;
+}
+
+void CDecoder::FiniVAAPIOutput()
+{
+ if (!m_vaapiConfigured)
+ return;
+
+ // uninit output
+ m_vaapiOutput.Dispose();
+ m_vaapiConfigured = false;
+
+ // destroy surfaces
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI::FiniVAAPIOutput destroying {} video surfaces",
+ m_videoSurfaces.Size());
+ VASurfaceID surf;
+ while((surf = m_videoSurfaces.RemoveNext()) != VA_INVALID_SURFACE)
+ {
+ CheckSuccess(vaDestroySurfaces(m_vaapiConfig.dpy, &surf, 1), "vaDestroySurfaces");
+ }
+ m_videoSurfaces.Reset();
+
+ // destroy vaapi config
+ if (m_vaapiConfig.configId != VA_INVALID_ID)
+ CheckSuccess(vaDestroyConfig(m_vaapiConfig.dpy, m_vaapiConfig.configId), "vaDestroyConfig");
+ m_vaapiConfig.configId = VA_INVALID_ID;
+}
+
+void CDecoder::ReturnRenderPicture(CVaapiRenderPicture *renderPic)
+{
+ m_vaapiOutput.m_dataPort.SendOutMessage(COutputDataProtocol::RETURNPIC, &renderPic, sizeof(renderPic));
+}
+
+IHardwareDecoder* CDecoder::Create(CDVDStreamInfo &hint, CProcessInfo &processInfo, AVPixelFormat fmt)
+{
+ if (fmt == AV_PIX_FMT_VAAPI_VLD && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(SETTING_VIDEOPLAYER_USEVAAPI))
+ return new VAAPI::CDecoder(processInfo);
+
+ return nullptr;
+}
+
+void CDecoder::Register(IVaapiWinSystem *winSystem, bool deepColor)
+{
+ m_pWinSystem = winSystem;
+
+ CVaapiConfig config;
+ if (!CVAAPIContext::EnsureContext(&config.context, nullptr))
+ return;
+
+ m_capGeneral = true;
+ m_capDeepColor = deepColor;
+ CDVDFactoryCodec::RegisterHWAccel("vaapi", CDecoder::Create);
+ config.context->Release(nullptr);
+
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return;
+
+ constexpr std::array<const char*, 9> vaapiSettings = {
+ SETTING_VIDEOPLAYER_USEVAAPI, SETTING_VIDEOPLAYER_USEVAAPIMPEG4,
+ SETTING_VIDEOPLAYER_USEVAAPIVC1, SETTING_VIDEOPLAYER_USEVAAPIMPEG2,
+ SETTING_VIDEOPLAYER_USEVAAPIVP8, SETTING_VIDEOPLAYER_USEVAAPIVP9,
+ SETTING_VIDEOPLAYER_USEVAAPIHEVC, SETTING_VIDEOPLAYER_PREFERVAAPIRENDER,
+ SETTING_VIDEOPLAYER_USEVAAPIAV1};
+
+ for (const auto vaapiSetting : vaapiSettings)
+ {
+ auto setting = settings->GetSetting(vaapiSetting);
+ if (!setting)
+ {
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", vaapiSetting);
+ continue;
+ }
+
+ setting->SetVisible(true);
+ }
+}
+
+void CDecoder::AVBufferRefDeleter::operator()(AVBufferRef* p) const
+{
+ av_buffer_unref(&p);
+}
+
+//-----------------------------------------------------------------------------
+// Buffer Pool
+//-----------------------------------------------------------------------------
+
+/**
+ * Buffer pool holds allocated vaapi and gl resources
+ * Embedded in COutput
+ */
+class VAAPI::CVaapiBufferPool : public IVideoBufferPool
+{
+public:
+ explicit CVaapiBufferPool(CDecoder &decoder);
+ ~CVaapiBufferPool() override;
+ CVideoBuffer* Get() override;
+ void Return(int id) override;
+ CVaapiRenderPicture* GetVaapi();
+ bool HasFree();
+ void QueueReturnPicture(CVaapiRenderPicture *pic);
+ CVaapiRenderPicture* ProcessSyncPicture();
+ void Init();
+ void DeleteTextures(bool precleanup);
+
+ std::deque<CVaapiProcessedPicture> processedPics;
+ std::deque<CVaapiProcessedPicture> processedPicsAway;
+ std::deque<CVaapiDecodedPicture> decodedPics;
+ int procPicId;
+
+protected:
+ std::vector<CVaapiRenderPicture*> allRenderPics;
+ std::deque<int> usedRenderPics;
+ std::deque<int> freeRenderPics;
+ std::deque<int> syncRenderPics;
+
+ CDecoder &m_vaapi;
+};
+
+CVaapiBufferPool::CVaapiBufferPool(CDecoder &decoder)
+ : m_vaapi(decoder)
+{
+ CVaapiRenderPicture *pic;
+ for (unsigned int i = 0; i < NUM_RENDER_PICS; i++)
+ {
+ pic = new CVaapiRenderPicture(i);
+ allRenderPics.push_back(pic);
+ freeRenderPics.push_back(i);
+ }
+}
+
+CVaapiBufferPool::~CVaapiBufferPool()
+{
+ CVaapiRenderPicture *pic;
+ for (unsigned int i = 0; i < NUM_RENDER_PICS; i++)
+ {
+ pic = allRenderPics[i];
+ delete pic;
+ }
+ allRenderPics.clear();
+}
+
+CVideoBuffer* CVaapiBufferPool::Get()
+{
+ if (freeRenderPics.empty())
+ return nullptr;
+
+ int idx = freeRenderPics.front();
+ freeRenderPics.pop_front();
+ usedRenderPics.push_back(idx);
+
+ CVideoBuffer *retPic = allRenderPics[idx];
+ retPic->Acquire(GetPtr());
+
+ m_vaapi.Acquire();
+
+ return retPic;
+}
+
+void CVaapiBufferPool::Return(int id)
+{
+ CVaapiRenderPicture *pic = allRenderPics[id];
+
+ m_vaapi.ReturnRenderPicture(pic);
+ m_vaapi.ReleasePicReference();
+}
+
+CVaapiRenderPicture* CVaapiBufferPool::GetVaapi()
+{
+ return dynamic_cast<CVaapiRenderPicture*>(Get());
+}
+
+bool CVaapiBufferPool::HasFree()
+{
+ return !freeRenderPics.empty();
+}
+
+void CVaapiBufferPool::QueueReturnPicture(CVaapiRenderPicture *pic)
+{
+ std::deque<int>::iterator it;
+ for (it = usedRenderPics.begin(); it != usedRenderPics.end(); ++it)
+ {
+ if (allRenderPics[*it] == pic)
+ {
+ break;
+ }
+ }
+
+ if (it == usedRenderPics.end())
+ {
+ CLog::Log(LOGWARNING, "CVaapiRenderPicture::QueueReturnPicture - pic not found");
+ return;
+ }
+
+ // check if already queued
+ auto it2 = find(syncRenderPics.begin(), syncRenderPics.end(), *it);
+ if (it2 == syncRenderPics.end())
+ {
+ syncRenderPics.push_back(*it);
+ }
+}
+
+CVaapiRenderPicture* CVaapiBufferPool::ProcessSyncPicture()
+{
+ CVaapiRenderPicture *retPic = nullptr;
+
+ for (auto it = syncRenderPics.begin(); it != syncRenderPics.end(); ++it)
+ {
+ retPic = allRenderPics[*it];
+
+ freeRenderPics.push_back(*it);
+
+ auto it2 = find(usedRenderPics.begin(), usedRenderPics.end(),*it);
+ if (it2 == usedRenderPics.end())
+ {
+ CLog::Log(LOGERROR, "CVaapiRenderPicture::ProcessSyncPicture - pic not found in queue");
+ }
+ else
+ {
+ usedRenderPics.erase(it2);
+ }
+ it = syncRenderPics.erase(it);
+
+ if (!retPic->valid)
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVaapiRenderPicture::{} - return of invalid render pic",
+ __FUNCTION__);
+ retPic = nullptr;
+ }
+ break;
+ }
+ return retPic;
+}
+
+void CVaapiBufferPool::Init()
+{
+ for (auto &pic : allRenderPics)
+ {
+ pic->avFrame = av_frame_alloc();
+ pic->valid = false;
+ }
+ procPicId = 0;
+}
+
+void CVaapiBufferPool::DeleteTextures(bool precleanup)
+{
+ for (auto &pic : allRenderPics)
+ {
+ if (precleanup && pic->valid)
+ continue;
+
+ av_frame_free(&pic->avFrame);
+ pic->valid = false;
+ }
+}
+
+void CVaapiRenderPicture::GetPlanes(uint8_t*(&planes)[YuvImage::MAX_PLANES])
+{
+ planes[0] = avFrame->data[0];
+ planes[1] = avFrame->data[1];
+ planes[2] = avFrame->data[2];
+}
+
+void CVaapiRenderPicture::GetStrides(int(&strides)[YuvImage::MAX_PLANES])
+{
+ strides[0] = avFrame->linesize[0];
+ strides[1] = avFrame->linesize[1];
+ strides[2] = avFrame->linesize[2];
+}
+
+//-----------------------------------------------------------------------------
+// Output
+//-----------------------------------------------------------------------------
+COutput::COutput(CDecoder &decoder, CEvent *inMsgEvent) :
+ CThread("Vaapi-Output"),
+ m_controlPort("OutputControlPort", inMsgEvent, &m_outMsgEvent),
+ m_dataPort("OutputDataPort", inMsgEvent, &m_outMsgEvent),
+ m_vaapi(decoder)
+{
+ m_inMsgEvent = inMsgEvent;
+ m_bufferPool = std::make_shared<CVaapiBufferPool>(decoder);
+}
+
+void COutput::Start()
+{
+ Create();
+}
+
+COutput::~COutput()
+{
+ Dispose();
+}
+
+void COutput::Dispose()
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ m_bStop = true;
+ m_outMsgEvent.Set();
+ StopThread();
+ m_controlPort.Purge();
+ m_dataPort.Purge();
+}
+
+void COutput::OnStartup()
+{
+ CLog::Log(LOGINFO, "COutput::OnStartup: Output Thread created");
+}
+
+void COutput::OnExit()
+{
+ CLog::Log(LOGINFO, "COutput::OnExit: Output Thread terminated");
+}
+
+enum OUTPUT_STATES
+{
+ O_TOP = 0, // 0
+ O_TOP_ERROR, // 1
+ O_TOP_UNCONFIGURED, // 2
+ O_TOP_CONFIGURED, // 3
+ O_TOP_CONFIGURED_IDLE, // 4
+ O_TOP_CONFIGURED_WORK, // 5
+ O_TOP_CONFIGURED_STEP1, // 6
+ O_TOP_CONFIGURED_STEP2, // 7
+ O_TOP_CONFIGURED_OUTPUT, // 8
+};
+
+int VAAPI_OUTPUT_parentStates[] = {
+ -1,
+ 0, //TOP_ERROR
+ 0, //TOP_UNCONFIGURED
+ 0, //TOP_CONFIGURED
+ 3, //TOP_CONFIGURED_IDLE
+ 3, //TOP_CONFIGURED_WORK
+ 3, //TOP_CONFIGURED_STEP1
+ 3, //TOP_CONFIGURED_STEP2
+ 3, //TOP_CONFIGURED_OUTPUT
+};
+
+void COutput::StateMachine(int signal, Protocol *port, Message *msg)
+{
+ for (int state = m_state; ; state = VAAPI_OUTPUT_parentStates[state])
+ {
+ switch (state)
+ {
+ case O_TOP: // TOP
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::FLUSH:
+ msg->Reply(COutputControlProtocol::ACC);
+ return;
+ case COutputControlProtocol::PRECLEANUP:
+ msg->Reply(COutputControlProtocol::ACC);
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case COutputDataProtocol::RETURNPIC:
+ CVaapiRenderPicture *pic;
+ pic = *((CVaapiRenderPicture**)msg->data);
+ QueueReturnPicture(pic);
+ return;
+ case COutputDataProtocol::RETURNPROCPIC:
+ int id;
+ id = *((int*)msg->data);
+ ProcessReturnProcPicture(id);
+ return;
+ default:
+ break;
+ }
+ }
+ {
+ std::string portName = port == NULL ? "timer" : port->portName;
+ CLog::Log(LOGWARNING, "COutput::{} - signal: {} form port: {} not handled for state: {}",
+ __FUNCTION__, signal, portName, m_state);
+ }
+ return;
+
+ case O_TOP_ERROR:
+ break;
+
+ case O_TOP_UNCONFIGURED:
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::INIT:
+ CVaapiConfig *data;
+ data = reinterpret_cast<CVaapiConfig*>(msg->data);
+ if (data)
+ {
+ m_config = *data;
+ }
+ Init();
+
+ // set initial number of
+ EnsureBufferPool();
+ m_state = O_TOP_CONFIGURED_IDLE;
+ msg->Reply(COutputControlProtocol::ACC);
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case O_TOP_CONFIGURED:
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::FLUSH:
+ Flush();
+ msg->Reply(COutputControlProtocol::ACC);
+ m_state = O_TOP_CONFIGURED_IDLE;
+ return;
+ case COutputControlProtocol::PRECLEANUP:
+ Flush();
+ ReleaseBufferPool(true);
+ msg->Reply(COutputControlProtocol::ACC);
+ m_state = O_TOP_UNCONFIGURED;
+ m_extTimeout = 10000;
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case COutputDataProtocol::NEWFRAME:
+ CPayloadWrap<CVaapiDecodedPicture> *payload;
+ payload = dynamic_cast<CPayloadWrap<CVaapiDecodedPicture>*>(msg->payloadObj.get());
+ if (payload)
+ {
+ m_bufferPool->decodedPics.push_back(*(payload->GetPlayload()));
+ m_extTimeout = 0;
+ }
+ return;
+ case COutputDataProtocol::RETURNPIC:
+ CVaapiRenderPicture *pic;
+ pic = *((CVaapiRenderPicture**)msg->data);
+ QueueReturnPicture(pic);
+ m_controlPort.SendInMessage(COutputControlProtocol::STATS);
+ m_extTimeout = 0;
+ return;
+ case COutputDataProtocol::RETURNPROCPIC:
+ int id;
+ id = *((int*)msg->data);
+ ProcessReturnProcPicture(id);
+ m_extTimeout = 0;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case O_TOP_CONFIGURED_IDLE:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::TIMEOUT:
+ ProcessSyncPicture();
+ m_extTimeout = 100;
+ if (HasWork())
+ {
+ m_state = O_TOP_CONFIGURED_WORK;
+ m_extTimeout = 0;
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case O_TOP_CONFIGURED_WORK:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::TIMEOUT:
+ if (PreferPP())
+ {
+ m_currentPicture = m_bufferPool->decodedPics.front();
+ m_bufferPool->decodedPics.pop_front();
+ InitCycle();
+ m_state = O_TOP_CONFIGURED_STEP1;
+ m_extTimeout = 0;
+ return;
+ }
+ else if (m_bufferPool->HasFree() &&
+ !m_bufferPool->processedPics.empty())
+ {
+ m_state = O_TOP_CONFIGURED_OUTPUT;
+ m_extTimeout = 0;
+ return;
+ }
+ else
+ m_state = O_TOP_CONFIGURED_IDLE;
+ m_extTimeout = 100;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case O_TOP_CONFIGURED_STEP1:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::TIMEOUT:
+ {
+ if (!m_pp->AddPicture(m_currentPicture))
+ {
+ m_state = O_TOP_ERROR;
+ return;
+ }
+ CVaapiProcessedPicture outPic;
+ if (m_pp->Filter(outPic))
+ {
+ m_config.stats->IncProcessed();
+ m_bufferPool->processedPics.push_back(outPic);
+ m_state = O_TOP_CONFIGURED_STEP2;
+ }
+ else
+ {
+ m_state = O_TOP_CONFIGURED_IDLE;
+ }
+ m_config.stats->DecDecoded();
+ m_controlPort.SendInMessage(COutputControlProtocol::STATS);
+ m_extTimeout = 0;
+ return;
+ }
+ default:
+ break;
+ }
+ }
+ break;
+
+ case O_TOP_CONFIGURED_STEP2:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::TIMEOUT:
+ {
+ CVaapiProcessedPicture outPic;
+ if (m_pp->Filter(outPic))
+ {
+ m_bufferPool->processedPics.push_back(outPic);
+ m_config.stats->IncProcessed();
+ m_extTimeout = 0;
+ return;
+ }
+ m_state = O_TOP_CONFIGURED_IDLE;
+ m_extTimeout = 0;
+ return;
+ }
+ default:
+ break;
+ }
+ }
+ break;
+
+ case O_TOP_CONFIGURED_OUTPUT:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::TIMEOUT:
+ if (!m_bufferPool->processedPics.empty())
+ {
+ CVaapiRenderPicture *outPic;
+ CVaapiProcessedPicture procPic;
+ procPic = m_bufferPool->processedPics.front();
+ m_bufferPool->processedPics.pop_front();
+ outPic = ProcessPicture(procPic);
+ if (outPic)
+ {
+ m_config.stats->IncRender();
+ m_dataPort.SendInMessage(COutputDataProtocol::PICTURE, &outPic, sizeof(outPic));
+ }
+ m_config.stats->DecProcessed();
+ }
+ m_state = O_TOP_CONFIGURED_IDLE;
+ m_extTimeout = 0;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ default: // we are in no state, should not happen
+ CLog::Log(LOGERROR, "COutput::{} - no valid state: {}", __FUNCTION__, m_state);
+ return;
+ }
+ } // for
+}
+
+void COutput::Process()
+{
+ Message *msg = NULL;
+ Protocol *port = NULL;
+ bool gotMsg;
+
+ m_state = O_TOP_UNCONFIGURED;
+ m_extTimeout = 1000;
+ m_bStateMachineSelfTrigger = false;
+
+ while (!m_bStop)
+ {
+ gotMsg = false;
+
+ if (m_bStateMachineSelfTrigger)
+ {
+ m_bStateMachineSelfTrigger = false;
+ // self trigger state machine
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ continue;
+ }
+ // check control port
+ else if (m_controlPort.ReceiveOutMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_controlPort;
+ }
+ // check data port
+ else if (m_dataPort.ReceiveOutMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_dataPort;
+ }
+ if (gotMsg)
+ {
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ continue;
+ }
+
+ // wait for message
+ else if (m_outMsgEvent.Wait(std::chrono::milliseconds(m_extTimeout)))
+ {
+ continue;
+ }
+ // time out
+ else
+ {
+ msg = m_controlPort.GetMessage();
+ msg->signal = COutputControlProtocol::TIMEOUT;
+ port = 0;
+ // signal timeout to state machine
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ }
+ }
+ Flush();
+ Uninit();
+}
+
+bool COutput::Init()
+{
+ m_diMethods.numDiMethods = 0;
+
+ m_pp = new CFFmpegPostproc();
+ m_pp->PreInit(m_config, &m_diMethods);
+ delete m_pp;
+
+ m_pp = new CVppPostproc();
+ m_pp->PreInit(m_config, &m_diMethods);
+ delete m_pp;
+
+ m_pp = nullptr;
+
+ std::list<EINTERLACEMETHOD> deintMethods;
+ deintMethods.assign(m_diMethods.diMethods, m_diMethods.diMethods + m_diMethods.numDiMethods);
+ m_config.processInfo->UpdateDeinterlacingMethods(deintMethods);
+ m_config.processInfo->SetDeinterlacingMethodDefault(EINTERLACEMETHOD::VS_INTERLACEMETHOD_VAAPI_BOB);
+
+ m_seenInterlaced = false;
+
+ return true;
+}
+
+bool COutput::Uninit()
+{
+ ProcessSyncPicture();
+ ReleaseBufferPool();
+ if (m_pp)
+ {
+ std::shared_ptr<CPostproc> pp(m_pp);
+ m_discardedPostprocs.push_back(pp);
+ m_pp->Discard(this, &COutput::ReadyForDisposal);
+ m_pp = nullptr;
+ }
+
+ if (!m_discardedPostprocs.empty())
+ {
+ CLog::Log(LOGERROR, "VAAPI::COutput::Uninit - not all CPostprcs released");
+ }
+ return true;
+}
+
+void COutput::Flush()
+{
+ Message *msg;
+ while (m_dataPort.ReceiveOutMessage(&msg))
+ {
+ if (msg->signal == COutputDataProtocol::NEWFRAME)
+ {
+ CPayloadWrap<CVaapiDecodedPicture> *payload;
+ payload = dynamic_cast<CPayloadWrap<CVaapiDecodedPicture>*>(msg->payloadObj.get());
+ if (payload)
+ {
+ CVaapiDecodedPicture pic = *(payload->GetPlayload());
+ m_config.videoSurfaces->ClearRender(pic.videoSurface);
+ }
+ }
+ else if (msg->signal == COutputDataProtocol::RETURNPIC)
+ {
+ CVaapiRenderPicture *pic;
+ pic = *((CVaapiRenderPicture**)msg->data);
+ QueueReturnPicture(pic);
+ }
+ msg->Release();
+ }
+
+ while (m_dataPort.ReceiveInMessage(&msg))
+ {
+ if (msg->signal == COutputDataProtocol::PICTURE)
+ {
+ CVaapiRenderPicture *pic;
+ pic = *((CVaapiRenderPicture**)msg->data);
+ pic->Release();
+ }
+ msg->Release();
+ }
+
+ for (unsigned int i = 0; i < m_bufferPool->decodedPics.size(); i++)
+ {
+ m_config.videoSurfaces->ClearRender(m_bufferPool->decodedPics[i].videoSurface);
+ }
+ m_bufferPool->decodedPics.clear();
+
+ for (unsigned int i = 0; i < m_bufferPool->processedPics.size(); i++)
+ {
+ ReleaseProcessedPicture(m_bufferPool->processedPics[i]);
+ }
+ m_bufferPool->processedPics.clear();
+
+ if (m_pp)
+ m_pp->Flush();
+}
+
+bool COutput::HasWork()
+{
+ // send a pic to renderer
+ if (m_bufferPool->HasFree() && !m_bufferPool->processedPics.empty())
+ return true;
+
+ bool ppWantsPic = true;
+ if (m_pp)
+ ppWantsPic = m_pp->WantsPic();
+
+ if (!m_bufferPool->decodedPics.empty() && m_bufferPool->processedPics.size() < 4 && ppWantsPic)
+ return true;
+
+ return false;
+}
+
+bool COutput::PreferPP()
+{
+ if (!m_bufferPool->decodedPics.empty())
+ {
+ if (!m_pp)
+ return true;
+
+ if (!m_pp->WantsPic())
+ return false;
+
+ if (!m_pp->DoesSync() && m_bufferPool->processedPics.size() < 4)
+ return true;
+
+ if (!m_bufferPool->HasFree() || m_bufferPool->processedPics.empty())
+ return true;
+ }
+
+ return false;
+}
+
+void COutput::InitCycle()
+{
+ uint64_t latency;
+ int flags;
+ m_config.stats->GetParams(latency, flags);
+
+ m_config.stats->SetCanSkipDeint(false);
+
+ EINTERLACEMETHOD method = m_config.processInfo->GetVideoSettings().m_InterlaceMethod;
+ bool interlaced = m_currentPicture.DVDPic.iFlags & DVP_FLAG_INTERLACED;
+ // Remember whether any interlaced frames were encountered already.
+ // If this is the case, the deinterlace method will never automatically be switched to NONE again in
+ // order to not change deint methods every few frames in PAFF streams.
+ m_seenInterlaced = m_seenInterlaced || interlaced;
+
+ if (!(flags & DVD_CODEC_CTRL_NO_POSTPROC) &&
+ m_seenInterlaced &&
+ method != VS_INTERLACEMETHOD_NONE)
+ {
+ if (!m_config.processInfo->Supports(method))
+ method = VS_INTERLACEMETHOD_VAAPI_BOB;
+
+ if (m_pp && !m_pp->UpdateDeintMethod(method))
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI output: Current postproc does not want new deinterlace mode, removing");
+ std::shared_ptr<CPostproc> pp(m_pp);
+ m_discardedPostprocs.push_back(pp);
+ m_pp->Discard(this, &COutput::ReadyForDisposal);
+ m_pp = nullptr;
+ m_config.processInfo->SetVideoDeintMethod("unknown");
+ }
+ if (!m_pp)
+ {
+ if (method == VS_INTERLACEMETHOD_DEINTERLACE ||
+ method == VS_INTERLACEMETHOD_RENDER_BOB)
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI output: Initializing ffmpeg postproc");
+ m_pp = new CFFmpegPostproc();
+ m_config.stats->SetVpp(false);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI output: Initializing vaapi postproc");
+ m_pp = new CVppPostproc();
+ m_config.stats->SetVpp(true);
+ }
+ if (m_pp->PreInit(m_config))
+ {
+ m_pp->Init(method);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "VAAPI output: Postproc preinit failed");
+ delete m_pp;
+ m_pp = nullptr;
+ }
+ }
+ }
+ // progressive
+ else
+ {
+ method = VS_INTERLACEMETHOD_NONE;
+
+ if (m_pp && !m_pp->UpdateDeintMethod(method))
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI output: Current postproc does not want new deinterlace mode, removing");
+ std::shared_ptr<CPostproc> pp(m_pp);
+ m_discardedPostprocs.push_back(pp);
+ m_pp->Discard(this, &COutput::ReadyForDisposal);
+ m_pp = nullptr;
+ m_config.processInfo->SetVideoDeintMethod("unknown");
+ }
+ if (!m_pp)
+ {
+ const bool preferVaapiRender = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(SETTING_VIDEOPLAYER_PREFERVAAPIRENDER);
+ // For 1080p/i or below, always use CVppPostproc even when not deinterlacing
+ // Reason is: mesa cannot dynamically switch surfaces between use for VAAPI post-processing
+ // and use for direct export, so we run into trouble if we or the user want to switch
+ // deinterlacing on/off mid-stream.
+ // See also: https://bugs.freedesktop.org/show_bug.cgi?id=105145
+ const bool alwaysInsertVpp = m_config.driverIsMesa &&
+ ((m_config.vidWidth * m_config.vidHeight) <= (1920 * 1080)) &&
+ interlaced;
+
+ m_config.stats->SetVpp(false);
+ if (!preferVaapiRender)
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI output: Initializing ffmpeg postproc");
+ m_pp = new CFFmpegPostproc();
+ }
+ else if (alwaysInsertVpp)
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI output: Initializing vaapi postproc");
+ m_pp = new CVppPostproc();
+ m_config.stats->SetVpp(true);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI output: Initializing skip postproc");
+ m_pp = new CSkipPostproc();
+ }
+ if (m_pp->PreInit(m_config))
+ {
+ m_pp->Init(method);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "VAAPI output: Postproc preinit failed");
+ delete m_pp;
+ m_pp = nullptr;
+ }
+ }
+ }
+ if (!m_pp) // fallback
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI output: Initializing skip postproc as fallback");
+ m_pp = new CSkipPostproc();
+ m_config.stats->SetVpp(false);
+ if (m_pp->PreInit(m_config))
+ m_pp->Init(method);
+ }
+}
+
+CVaapiRenderPicture* COutput::ProcessPicture(CVaapiProcessedPicture &pic)
+{
+ CVaapiRenderPicture *retPic;
+ retPic = m_bufferPool->GetVaapi();
+ retPic->DVDPic.SetParams(pic.DVDPic);
+
+ if (!pic.source)
+ {
+ CLog::Log(LOGERROR, "VAAPI::ProcessPicture - pic has no source");
+ retPic->Release();
+ return nullptr;
+ }
+
+ if (pic.source->UseVideoSurface())
+ {
+ vaSyncSurface(m_config.dpy, pic.videoSurface);
+ pic.id = m_bufferPool->procPicId++;
+ m_bufferPool->processedPicsAway.push_back(pic);
+ retPic->procPic = pic;
+ retPic->vadsp = m_config.dpy;
+ }
+ else
+ {
+ av_frame_move_ref(retPic->avFrame, pic.frame);
+ pic.source->ClearRef(pic);
+ retPic->procPic.videoSurface = VA_INVALID_ID;
+ }
+
+ retPic->DVDPic.dts = DVD_NOPTS_VALUE;
+ retPic->DVDPic.iWidth = m_config.vidWidth;
+ retPic->DVDPic.iHeight = m_config.vidHeight;
+
+ retPic->valid = true;
+
+ return retPic;
+}
+
+void COutput::ReleaseProcessedPicture(CVaapiProcessedPicture &pic)
+{
+ if (!pic.source)
+ {
+ return;
+ }
+ pic.source->ClearRef(pic);
+ pic.source = nullptr;
+}
+
+void COutput::QueueReturnPicture(CVaapiRenderPicture *pic)
+{
+ m_bufferPool->QueueReturnPicture(pic);
+ ProcessSyncPicture();
+}
+
+void COutput::ProcessSyncPicture()
+{
+ CVaapiRenderPicture *pic;
+
+ pic = m_bufferPool->ProcessSyncPicture();
+
+ if (pic)
+ {
+ ProcessReturnPicture(pic);
+ }
+}
+
+void COutput::ProcessReturnPicture(CVaapiRenderPicture *pic)
+{
+ if (pic->avFrame)
+ av_frame_unref(pic->avFrame);
+
+ ProcessReturnProcPicture(pic->procPic.id);
+ pic->valid = false;
+}
+
+void COutput::ProcessReturnProcPicture(int id)
+{
+ for (auto it=m_bufferPool->processedPicsAway.begin(); it!=m_bufferPool->processedPicsAway.end(); ++it)
+ {
+ if (it->id == id)
+ {
+ ReleaseProcessedPicture(*it);
+ m_bufferPool->processedPicsAway.erase(it);
+ break;
+ }
+ }
+}
+
+void COutput::EnsureBufferPool()
+{
+ m_bufferPool->Init();
+}
+
+void COutput::ReleaseBufferPool(bool precleanup)
+{
+ ProcessSyncPicture();
+
+ m_bufferPool->DeleteTextures(precleanup);
+
+ for (unsigned int i = 0; i < m_bufferPool->decodedPics.size(); i++)
+ {
+ m_config.videoSurfaces->ClearRender(m_bufferPool->decodedPics[i].videoSurface);
+ }
+ m_bufferPool->decodedPics.clear();
+
+ for (unsigned int i = 0; i < m_bufferPool->processedPics.size(); i++)
+ {
+ ReleaseProcessedPicture(m_bufferPool->processedPics[i]);
+ }
+ m_bufferPool->processedPics.clear();
+}
+
+void COutput::ReadyForDisposal(CPostproc *pp)
+{
+ for (auto it = m_discardedPostprocs.begin(); it != m_discardedPostprocs.end(); ++it)
+ {
+ if ((*it).get() == pp)
+ {
+ m_discardedPostprocs.erase(it);
+ break;
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Postprocessing
+//-----------------------------------------------------------------------------
+
+bool CSkipPostproc::PreInit(CVaapiConfig &config, SDiMethods *methods)
+{
+ m_config = config;
+ return true;
+}
+
+bool CSkipPostproc::Init(EINTERLACEMETHOD method)
+{
+ m_config.processInfo->SetVideoDeintMethod("none");
+ return true;
+}
+
+bool CSkipPostproc::AddPicture(CVaapiDecodedPicture &inPic)
+{
+ m_pic = inPic;
+ m_step = 0;
+ return true;
+}
+
+bool CSkipPostproc::Filter(CVaapiProcessedPicture &outPic)
+{
+ if (m_step > 0)
+ return false;
+ outPic.DVDPic.SetParams(m_pic.DVDPic);
+ outPic.videoSurface = m_pic.videoSurface;
+ m_refsToSurfaces++;
+ outPic.source = this;
+ outPic.DVDPic.iFlags &= ~(DVP_FLAG_TOP_FIELD_FIRST |
+ DVP_FLAG_REPEAT_TOP_FIELD |
+ DVP_FLAG_INTERLACED);
+ m_step++;
+ return true;
+}
+
+void CSkipPostproc::ClearRef(CVaapiProcessedPicture &pic)
+{
+ m_config.videoSurfaces->ClearRender(pic.videoSurface);
+ m_refsToSurfaces--;
+
+ if (m_pOut && m_refsToSurfaces <= 0)
+ (m_pOut->*m_cbDispose)(this);
+}
+
+void CSkipPostproc::Flush()
+{
+
+}
+
+bool CSkipPostproc::UpdateDeintMethod(EINTERLACEMETHOD method)
+{
+ if (method == VS_INTERLACEMETHOD_NONE)
+ return true;
+
+ return false;
+}
+
+bool CSkipPostproc::DoesSync()
+{
+ return false;
+}
+
+bool CSkipPostproc::UseVideoSurface()
+{
+ return true;
+}
+
+void CSkipPostproc::Discard(COutput *output, ReadyToDispose cb)
+{
+ m_pOut = output;
+ m_cbDispose = cb;
+ if (m_refsToSurfaces <= 0)
+ (m_pOut->*m_cbDispose)(this);
+}
+
+//-----------------------------------------------------------------------------
+// VPP Postprocessing
+//-----------------------------------------------------------------------------
+
+CVppPostproc::CVppPostproc()
+{
+}
+
+CVppPostproc::~CVppPostproc()
+{
+ Dispose();
+}
+
+bool CVppPostproc::PreInit(CVaapiConfig &config, SDiMethods *methods)
+{
+ m_config = config;
+
+ // create config
+ if (!CheckSuccess(
+ vaCreateConfig(m_config.dpy, VAProfileNone, VAEntrypointVideoProc, NULL, 0, &m_configId),
+ "vaCreateConfig"))
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVppPostproc::PreInit - VPP init failed in vaCreateConfig");
+
+ return false;
+ }
+
+ VASurfaceAttrib attribs[1], *attrib;
+ attrib = attribs;
+ attrib->flags = VA_SURFACE_ATTRIB_SETTABLE;
+ attrib->type = VASurfaceAttribPixelFormat;
+ attrib->value.type = VAGenericValueTypeInteger;
+ attrib->value.value.i = VA_FOURCC_NV12;
+
+ // create surfaces
+ VASurfaceID surfaces[32];
+ unsigned int format = VA_RT_FORMAT_YUV420;
+ if (m_config.profile == VAProfileHEVCMain10)
+ {
+ format = VA_RT_FORMAT_YUV420_10BPP;
+ attrib->value.value.i = VA_FOURCC_P010;
+ }
+ int nb_surfaces = NUM_RENDER_PICS;
+ if (!CheckSuccess(
+ vaCreateSurfaces(m_config.dpy, format, m_config.surfaceWidth, m_config.surfaceHeight,
+ surfaces, nb_surfaces,
+ attribs, 1), "vaCreateSurfaces"))
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVppPostproc::PreInit - VPP init failed in vaCreateSurfaces");
+
+ return false;
+ }
+ for (int i=0; i<nb_surfaces; i++)
+ {
+ m_videoSurfaces.AddSurface(surfaces[i]);
+ }
+
+ // create vaapi decoder context
+ if (!CheckSuccess(
+ vaCreateContext(m_config.dpy, m_configId, m_config.surfaceWidth, m_config.surfaceHeight, 0,
+ surfaces,
+ nb_surfaces, &m_contextId), "vaCreateContext"))
+ {
+ m_contextId = VA_INVALID_ID;
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVppPostproc::PreInit - VPP init failed in vaCreateContext");
+
+ return false;
+ }
+
+ VAProcFilterType filters[VAProcFilterCount];
+ unsigned int numFilters = VAProcFilterCount;
+ VAProcFilterCapDeinterlacing deinterlacingCaps[VAProcDeinterlacingCount];
+ unsigned int numDeinterlacingCaps = VAProcDeinterlacingCount;
+
+ if (!CheckSuccess(vaQueryVideoProcFilters(m_config.dpy, m_contextId, filters, &numFilters),
+ "vaQueryVideoProcFilters"))
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVppPostproc::PreInit - VPP init failed in vaQueryVideoProcFilters");
+
+ return false;
+ }
+
+ if (!CheckSuccess(vaQueryVideoProcFilterCaps(m_config.dpy, m_contextId, VAProcFilterDeinterlacing,
+ deinterlacingCaps,
+ &numDeinterlacingCaps), "vaQueryVideoProcFilterCaps"))
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVppPostproc::PreInit - VPP init failed in vaQueryVideoProcFilterCaps");
+
+ return false;
+ }
+
+ if (methods)
+ {
+ for (unsigned int i = 0; i < numFilters; i++)
+ {
+ if (filters[i] == VAProcFilterDeinterlacing)
+ {
+ for (unsigned int j = 0; j < numDeinterlacingCaps; j++)
+ {
+ if (deinterlacingCaps[j].type == VAProcDeinterlacingBob)
+ {
+ methods->diMethods[methods->numDiMethods++] = VS_INTERLACEMETHOD_VAAPI_BOB;
+ }
+ else if (deinterlacingCaps[j].type == VAProcDeinterlacingMotionAdaptive)
+ {
+ methods->diMethods[methods->numDiMethods++] = VS_INTERLACEMETHOD_VAAPI_MADI;
+ }
+ else if (deinterlacingCaps[j].type == VAProcDeinterlacingMotionCompensated)
+ {
+ methods->diMethods[methods->numDiMethods++] = VS_INTERLACEMETHOD_VAAPI_MACI;
+ }
+ }
+ }
+ }
+ }
+ return true;
+}
+
+bool CVppPostproc::Init(EINTERLACEMETHOD method)
+{
+ m_forwardRefs = 0;
+ m_backwardRefs = 0;
+ m_currentIdx = 0;
+ m_frameCount = 0;
+ m_vppMethod = VS_INTERLACEMETHOD_AUTO;
+
+ return UpdateDeintMethod(method);
+}
+
+
+bool CVppPostproc::UpdateDeintMethod(EINTERLACEMETHOD method)
+{
+ if (method == m_vppMethod)
+ {
+ return true;
+ }
+
+ m_vppMethod = method;
+ m_forwardRefs = 0;
+ m_backwardRefs = 0;
+
+ if (m_filter != VA_INVALID_ID)
+ {
+ CheckSuccess(vaDestroyBuffer(m_config.dpy, m_filter), "vaDestroyBuffer");
+ m_filter = VA_INVALID_ID;
+ }
+
+ VAProcDeinterlacingType vppMethod;
+ switch (method)
+ {
+ case VS_INTERLACEMETHOD_VAAPI_BOB:
+ vppMethod = VAProcDeinterlacingBob;
+ m_config.processInfo->SetVideoDeintMethod("vaapi-bob");
+ break;
+ case VS_INTERLACEMETHOD_VAAPI_MADI:
+ vppMethod = VAProcDeinterlacingMotionAdaptive;
+ m_config.processInfo->SetVideoDeintMethod("vaapi-madi");
+ break;
+ case VS_INTERLACEMETHOD_VAAPI_MACI:
+ vppMethod = VAProcDeinterlacingMotionCompensated;
+ m_config.processInfo->SetVideoDeintMethod("vaapi-mcdi");
+ break;
+ case VS_INTERLACEMETHOD_NONE:
+ // Early exit, filter parameter buffer not needed then
+ m_config.processInfo->SetVideoDeintMethod("vaapi-none");
+ return true;
+ default:
+ m_config.processInfo->SetVideoDeintMethod("unknown");
+ return false;
+ }
+
+ VAProcFilterParameterBufferDeinterlacing filterparams;
+ filterparams.type = VAProcFilterDeinterlacing;
+ filterparams.algorithm = vppMethod;
+ filterparams.flags = 0;
+
+ if (!CheckSuccess(vaCreateBuffer(m_config.dpy, m_contextId, VAProcFilterParameterBufferType,
+ sizeof(filterparams), 1,
+ &filterparams, &m_filter), "vaCreateBuffer"))
+ {
+ m_filter = VA_INVALID_ID;
+ return false;
+ }
+
+ VAProcPipelineCaps pplCaps;
+ if (!CheckSuccess(vaQueryVideoProcPipelineCaps(m_config.dpy, m_contextId, &m_filter, 1, &pplCaps),
+ "vaQueryVideoProcPipelineCaps"))
+ {
+ return false;
+ }
+
+ m_forwardRefs = pplCaps.num_forward_references;
+ m_backwardRefs = pplCaps.num_backward_references;
+
+ return true;
+}
+
+void CVppPostproc::Dispose()
+{
+ // make sure surfaces are idle
+ for (int i=0; i<m_videoSurfaces.Size(); i++)
+ {
+ CheckSuccess(vaSyncSurface(m_config.dpy, m_videoSurfaces.GetAtIndex(i)), "vaSyncSurface");
+ }
+
+ if (m_filter != VA_INVALID_ID)
+ {
+ CheckSuccess(vaDestroyBuffer(m_config.dpy, m_filter), "vaDestroyBuffer");
+ m_filter = VA_INVALID_ID;
+ }
+ if (m_contextId != VA_INVALID_ID)
+ {
+ CheckSuccess(vaDestroyContext(m_config.dpy, m_contextId), "vaDestroyContext");
+ m_contextId = VA_INVALID_ID;
+ }
+ VASurfaceID surf;
+ while((surf = m_videoSurfaces.RemoveNext()) != VA_INVALID_SURFACE)
+ {
+ CheckSuccess(vaDestroySurfaces(m_config.dpy, &surf, 1), "vaDestroySurface");
+ }
+ m_videoSurfaces.Reset();
+
+ if (m_configId != VA_INVALID_ID)
+ {
+ CheckSuccess(vaDestroyConfig(m_config.dpy, m_configId), "vaDestroyConfig");
+ m_configId = VA_INVALID_ID;
+ }
+
+ // release all decoded pictures
+ Flush();
+}
+
+bool CVppPostproc::AddPicture(CVaapiDecodedPicture &pic)
+{
+ pic.index = m_frameCount;
+ m_decodedPics.push_front(pic);
+ m_frameCount++;
+ m_step = 0;
+ m_config.stats->SetCanSkipDeint(true);
+ return true;
+}
+
+bool CVppPostproc::Filter(CVaapiProcessedPicture &outPic)
+{
+ if (m_step>1)
+ {
+ Advance();
+ return false;
+ }
+
+ // we need a free render target
+ VASurfaceID surf = m_videoSurfaces.GetFree(VA_INVALID_SURFACE);
+ if (surf == VA_INVALID_SURFACE)
+ {
+ CLog::Log(LOGERROR, "VAAPI - VPP - no free render target");
+ return false;
+ }
+ // clear reference in case we return false
+ m_videoSurfaces.ClearReference(surf);
+
+ // move window of frames we are looking at to account for backward (=future) refs
+ const auto currentIdx = m_currentIdx - m_backwardRefs;
+
+ // make sure we have all needed forward refs
+ if ((currentIdx - m_forwardRefs) < m_decodedPics.back().index)
+ {
+ Advance();
+ return false;
+ }
+
+ auto it = std::find_if(m_decodedPics.begin(), m_decodedPics.end(),
+ [currentIdx](const CVaapiDecodedPicture &picture){
+ return picture.index == currentIdx;
+ });
+ if (it==m_decodedPics.end())
+ {
+ return false;
+ }
+ outPic.DVDPic.SetParams(it->DVDPic);
+
+ // skip deinterlacing cycle if requested
+ if ((m_step == 1) &&
+ ((outPic.DVDPic.iFlags & DVD_CODEC_CTRL_SKIPDEINT) || !(outPic.DVDPic.iFlags & DVP_FLAG_INTERLACED) || (m_vppMethod == VS_INTERLACEMETHOD_NONE)))
+ {
+ Advance();
+ return false;
+ }
+
+ // vpp deinterlacing
+ VAProcFilterParameterBufferDeinterlacing *filterParams;
+ VABufferID pipelineBuf;
+ VAProcPipelineParameterBuffer *pipelineParams;
+ VARectangle inputRegion;
+ VARectangle outputRegion;
+
+ if (!CheckSuccess(vaBeginPicture(m_config.dpy, m_contextId, surf), "vaBeginPicture"))
+ {
+ return false;
+ }
+
+ if (!CheckSuccess(vaCreateBuffer(m_config.dpy, m_contextId, VAProcPipelineParameterBufferType,
+ sizeof(VAProcPipelineParameterBuffer), 1, NULL, &pipelineBuf), "vaCreateBuffer"))
+ {
+ return false;
+ }
+ if (!CheckSuccess(vaMapBuffer(m_config.dpy, pipelineBuf, (void**) &pipelineParams),
+ "vaMapBuffer"))
+ {
+ return false;
+ }
+ memset(pipelineParams, 0, sizeof(VAProcPipelineParameterBuffer));
+
+ inputRegion.x = outputRegion.x = 0;
+ inputRegion.y = outputRegion.y = 0;
+ inputRegion.width = outputRegion.width = m_config.surfaceWidth;
+ inputRegion.height = outputRegion.height = m_config.surfaceHeight;
+
+ pipelineParams->output_region = &outputRegion;
+ pipelineParams->surface_region = &inputRegion;
+ pipelineParams->output_background_color = 0xff000000;
+ pipelineParams->filter_flags = 0;
+
+ VASurfaceID forwardRefs[32];
+ VASurfaceID backwardRefs[32];
+ pipelineParams->forward_references = forwardRefs;
+ pipelineParams->backward_references = backwardRefs;
+ pipelineParams->num_forward_references = 0;
+ pipelineParams->num_backward_references = 0;
+
+ int maxPic = currentIdx + m_backwardRefs;
+ int minPic = currentIdx - m_forwardRefs;
+ int curPic = currentIdx;
+
+ // deinterlace flag
+ if (m_vppMethod != VS_INTERLACEMETHOD_NONE)
+ {
+ unsigned int flags = 0;
+
+ if (it->DVDPic.iFlags & DVP_FLAG_INTERLACED)
+ {
+ if (it->DVDPic.iFlags & DVP_FLAG_TOP_FIELD_FIRST)
+ flags = 0;
+ else
+ flags = VA_DEINTERLACING_BOTTOM_FIELD_FIRST | VA_DEINTERLACING_BOTTOM_FIELD;
+
+ if (m_step)
+ {
+ if (flags & VA_DEINTERLACING_BOTTOM_FIELD)
+ flags &= ~VA_DEINTERLACING_BOTTOM_FIELD;
+ else
+ flags |= VA_DEINTERLACING_BOTTOM_FIELD;
+ }
+ }
+ if (!CheckSuccess(vaMapBuffer(m_config.dpy, m_filter, (void**) &filterParams), "vaMapBuffer"))
+ {
+ return false;
+ }
+ filterParams->flags = flags;
+ if (!CheckSuccess(vaUnmapBuffer(m_config.dpy, m_filter), "vaUnmapBuffer"))
+ {
+ return false;
+ }
+
+ pipelineParams->filters = &m_filter;
+ pipelineParams->num_filters = 1;
+ }
+ else
+ {
+ pipelineParams->num_filters = 0;
+ }
+
+ // references
+ double ptsLast = DVD_NOPTS_VALUE;
+ double pts = DVD_NOPTS_VALUE;
+
+ pipelineParams->surface = VA_INVALID_SURFACE;
+ for (const auto &picture : m_decodedPics)
+ {
+ if (picture.index >= minPic && picture.index <= maxPic)
+ {
+ if (picture.index > curPic)
+ {
+ backwardRefs[(picture.index - curPic) - 1] = picture.videoSurface;
+ pipelineParams->num_backward_references++;
+ }
+ else if (picture.index == curPic)
+ {
+ pipelineParams->surface = picture.videoSurface;
+ pts = picture.DVDPic.pts;
+ }
+ if (picture.index < curPic)
+ {
+ forwardRefs[(curPic - picture.index) - 1] = picture.videoSurface;
+ pipelineParams->num_forward_references++;
+ if (picture.index == curPic - 1)
+ ptsLast = picture.DVDPic.pts;
+ }
+ }
+ }
+
+ // set pts for 2nd frame
+ if (m_step && pts != DVD_NOPTS_VALUE && ptsLast != DVD_NOPTS_VALUE)
+ outPic.DVDPic.pts += (pts-ptsLast)/2;
+
+ if (pipelineParams->surface == VA_INVALID_SURFACE)
+ return false;
+
+ if (!CheckSuccess(vaUnmapBuffer(m_config.dpy, pipelineBuf), "vaUnmmapBuffer"))
+ {
+ return false;
+ }
+
+ if (!CheckSuccess(vaRenderPicture(m_config.dpy, m_contextId, &pipelineBuf, 1), "vaRenderPicture"))
+ {
+ return false;
+ }
+
+ if (!CheckSuccess(vaEndPicture(m_config.dpy, m_contextId), "vaEndPicture"))
+ {
+ return false;
+ }
+
+ if (!CheckSuccess(vaDestroyBuffer(m_config.dpy, pipelineBuf), "vaDestroyBuffer"))
+ {
+ return false;
+ }
+
+ m_step++;
+ outPic.videoSurface = m_videoSurfaces.GetFree(surf);
+ outPic.source = this;
+ outPic.DVDPic.iFlags &= ~(DVP_FLAG_TOP_FIELD_FIRST |
+ DVP_FLAG_REPEAT_TOP_FIELD |
+ DVP_FLAG_INTERLACED);
+
+ return true;
+}
+
+void CVppPostproc::Advance()
+{
+ m_currentIdx++;
+
+ // release all unneeded refs
+ auto it = m_decodedPics.begin();
+ while (it != m_decodedPics.end())
+ {
+ if (it->index < m_currentIdx - m_forwardRefs - m_backwardRefs)
+ {
+ m_config.videoSurfaces->ClearRender(it->videoSurface);
+ it = m_decodedPics.erase(it);
+ }
+ else
+ ++it;
+ }
+}
+
+void CVppPostproc::ClearRef(CVaapiProcessedPicture &pic)
+{
+ m_videoSurfaces.ClearReference(pic.videoSurface);
+
+ if (m_pOut && !m_videoSurfaces.HasRefs())
+ (m_pOut->*m_cbDispose)(this);
+}
+
+void CVppPostproc::Flush()
+{
+ // release all decoded pictures
+ auto it = m_decodedPics.begin();
+ while (it != m_decodedPics.end())
+ {
+ m_config.videoSurfaces->ClearRender(it->videoSurface);
+ it = m_decodedPics.erase(it);
+ }
+}
+
+bool CVppPostproc::DoesSync()
+{
+ return false;
+}
+
+bool CVppPostproc::WantsPic()
+{
+ // need at least 2 for deinterlacing
+ if (m_videoSurfaces.NumFree() > 1)
+ return true;
+
+ return false;
+}
+
+bool CVppPostproc::UseVideoSurface()
+{
+ return true;
+}
+
+void CVppPostproc::Discard(COutput *output, ReadyToDispose cb)
+{
+ m_pOut = output;
+ m_cbDispose = cb;
+ if (!m_videoSurfaces.HasRefs())
+ (m_pOut->*m_cbDispose)(this);
+}
+
+bool CVppPostproc::CheckSuccess(VAStatus status, const std::string& function)
+{
+ if (status != VA_STATUS_SUCCESS)
+ {
+ CLog::Log(LOGERROR, "VAAPI/vpp {} error: {} ({})", function, vaErrorStr(status), status);
+ return false;
+ }
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// FFmpeg Postprocessing
+//-----------------------------------------------------------------------------
+
+#define CACHED_BUFFER_SIZE 4096
+
+CFFmpegPostproc::CFFmpegPostproc()
+{
+ m_cache = NULL;
+ m_pFilterFrameIn = NULL;
+ m_pFilterFrameOut = NULL;
+ m_pFilterGraph = NULL;
+ m_DVDPic.pts = DVD_NOPTS_VALUE;
+ m_frametime = 0;
+ m_lastOutPts = DVD_NOPTS_VALUE;
+}
+
+CFFmpegPostproc::~CFFmpegPostproc()
+{
+ Close();
+ KODI::MEMORY::AlignedFree(m_cache);
+ m_dllSSE4.Unload();
+ av_frame_free(&m_pFilterFrameIn);
+ av_frame_free(&m_pFilterFrameOut);
+}
+
+bool CFFmpegPostproc::PreInit(CVaapiConfig &config, SDiMethods *methods)
+{
+ m_config = config;
+ bool use_filter = true;
+
+ // copying large surfaces via sse4 is a bit slow
+ // we just return false here as the primary use case the
+ // sse4 copy method is deinterlacing of max 1080i content
+ if (m_config.vidWidth > 1920 || m_config.vidHeight > 1088)
+ return false;
+
+ VAImage image;
+ image.image_id = VA_INVALID_ID;
+ VASurfaceID surface = config.videoSurfaces->GetAtIndex(0);
+ VAStatus status = vaDeriveImage(config.dpy, surface, &image);
+ if (status != VA_STATUS_SUCCESS)
+ {
+ CLog::Log(LOGINFO, "VAAPI::SupportsFilter vaDeriveImage not supported by driver - ffmpeg postprocessing and CPU-copy rendering will not be available");
+ use_filter = false;
+ }
+ if (use_filter && (image.format.fourcc != VA_FOURCC_NV12))
+ {
+ CLog::Log(LOGWARNING,"VAAPI::SupportsFilter image format not NV12");
+ use_filter = false;
+ }
+ if (use_filter && ((image.pitches[0] % 64) || (image.pitches[1] % 64)))
+ {
+ CLog::Log(LOGWARNING,"VAAPI::SupportsFilter patches no multiple of 64");
+ use_filter = false;
+ }
+ if (image.image_id != VA_INVALID_ID)
+ CheckSuccess(vaDestroyImage(config.dpy, image.image_id), "vaDestroyImage");
+
+ if (use_filter && !m_dllSSE4.Load())
+ {
+ CLog::Log(LOGERROR,"VAAPI::SupportsFilter failed loading sse4 lib");
+ use_filter = false;
+ }
+
+ if (use_filter)
+ {
+ m_cache = static_cast<uint8_t*>(KODI::MEMORY::AlignedMalloc(CACHED_BUFFER_SIZE, 64));
+ if (methods)
+ {
+ methods->diMethods[methods->numDiMethods++] = VS_INTERLACEMETHOD_DEINTERLACE;
+ methods->diMethods[methods->numDiMethods++] = VS_INTERLACEMETHOD_RENDER_BOB;
+ }
+ }
+ return use_filter;
+}
+
+bool CFFmpegPostproc::Init(EINTERLACEMETHOD method)
+{
+ if (!(m_pFilterGraph = avfilter_graph_alloc()))
+ {
+ CLog::Log(LOGERROR, "VAAPI::CFFmpegPostproc::Init - unable to alloc filter graph");
+ return false;
+ }
+
+ const AVFilter* srcFilter = avfilter_get_by_name("buffer");
+ const AVFilter* outFilter = avfilter_get_by_name("buffersink");
+
+ std::string args = StringUtils::Format("{}:{}:{}:{}:{}:{}:{}", m_config.vidWidth,
+ m_config.vidHeight, AV_PIX_FMT_NV12, 1, 1,
+ (m_config.aspect.num != 0) ? m_config.aspect.num : 1,
+ (m_config.aspect.num != 0) ? m_config.aspect.den : 1);
+
+ if (avfilter_graph_create_filter(&m_pFilterIn, srcFilter, "src", args.c_str(), NULL, m_pFilterGraph) < 0)
+ {
+ CLog::Log(LOGERROR, "VAAPI::CFFmpegPostproc::Init - avfilter_graph_create_filter: src");
+ return false;
+ }
+
+ if (avfilter_graph_create_filter(&m_pFilterOut, outFilter, "out", NULL, NULL, m_pFilterGraph) < 0)
+ {
+ CLog::Log(LOGERROR, "CFFmpegPostproc::Init - avfilter_graph_create_filter: out");
+ return false;
+ }
+
+ enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_NV12, AV_PIX_FMT_NONE };
+ if (av_opt_set_int_list(m_pFilterOut, "pix_fmts", pix_fmts, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN) < 0)
+ {
+ CLog::Log(LOGERROR, "VAAPI::CFFmpegPostproc::Init - failed settings pix formats");
+ return false;
+ }
+
+ AVFilterInOut* outputs = avfilter_inout_alloc();
+ AVFilterInOut* inputs = avfilter_inout_alloc();
+
+ outputs->name = av_strdup("in");
+ outputs->filter_ctx = m_pFilterIn;
+ outputs->pad_idx = 0;
+ outputs->next = NULL;
+
+ inputs->name = av_strdup("out");
+ inputs->filter_ctx = m_pFilterOut;
+ inputs->pad_idx = 0;
+ inputs->next = NULL;
+
+ if (method == VS_INTERLACEMETHOD_DEINTERLACE)
+ {
+ std::string filter;
+
+ filter = "yadif=1:-1";
+
+ if (avfilter_graph_parse_ptr(m_pFilterGraph, filter.c_str(), &inputs, &outputs, NULL) < 0)
+ {
+ CLog::Log(LOGERROR, "VAAPI::CFFmpegPostproc::Init - avfilter_graph_parse");
+ avfilter_inout_free(&outputs);
+ avfilter_inout_free(&inputs);
+ return false;
+ }
+
+ avfilter_inout_free(&outputs);
+ avfilter_inout_free(&inputs);
+
+ if (avfilter_graph_config(m_pFilterGraph, NULL) < 0)
+ {
+ CLog::Log(LOGERROR, "VAAPI::CFFmpegPostproc::Init - avfilter_graph_config");
+ return false;
+ }
+
+ m_config.processInfo->SetVideoDeintMethod("yadif");
+ }
+ else if (method == VS_INTERLACEMETHOD_RENDER_BOB ||
+ method == VS_INTERLACEMETHOD_NONE)
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VAAPI::CFFmpegPostproc::Init - skip deinterlacing");
+ avfilter_inout_free(&outputs);
+ avfilter_inout_free(&inputs);
+ m_config.processInfo->SetVideoDeintMethod("none");
+ }
+ else
+ {
+ avfilter_inout_free(&outputs);
+ avfilter_inout_free(&inputs);
+ m_config.processInfo->SetVideoDeintMethod("unknown");
+ return false;
+ }
+ m_diMethod = method;
+
+ m_pFilterFrameIn = av_frame_alloc();
+ m_pFilterFrameOut = av_frame_alloc();
+ return true;
+}
+
+bool CFFmpegPostproc::AddPicture(CVaapiDecodedPicture &inPic)
+{
+ VASurfaceID surf = inPic.videoSurface;
+ VAImage image;
+ uint8_t *buf;
+ if (m_DVDPic.pts != DVD_NOPTS_VALUE && inPic.DVDPic.pts != DVD_NOPTS_VALUE)
+ {
+ m_frametime = inPic.DVDPic.pts - m_DVDPic.pts;
+ }
+ m_DVDPic.SetParams(inPic.DVDPic);
+ bool result = false;
+
+ if (!CheckSuccess(vaSyncSurface(m_config.dpy, surf), "vaSyncSurface"))
+ goto error;
+
+ if (!CheckSuccess(vaDeriveImage(m_config.dpy, surf, &image), "vaDeriveImage"))
+ goto error;
+
+ if (!CheckSuccess(vaMapBuffer(m_config.dpy, image.buf, (void**) &buf), "vaMapBuffer"))
+ goto error;
+
+ m_pFilterFrameIn->format = AV_PIX_FMT_NV12;
+ m_pFilterFrameIn->width = m_config.vidWidth;
+ m_pFilterFrameIn->height = m_config.vidHeight;
+ m_pFilterFrameIn->linesize[0] = image.pitches[0];
+ m_pFilterFrameIn->linesize[1] = image.pitches[1];
+ m_pFilterFrameIn->interlaced_frame = (inPic.DVDPic.iFlags & DVP_FLAG_INTERLACED) ? 1 : 0;
+ m_pFilterFrameIn->top_field_first = (inPic.DVDPic.iFlags & DVP_FLAG_TOP_FIELD_FIRST) ? 1 : 0;
+
+ if (inPic.DVDPic.pts == DVD_NOPTS_VALUE)
+ m_pFilterFrameIn->pts = AV_NOPTS_VALUE;
+ else
+ m_pFilterFrameIn->pts = (inPic.DVDPic.pts / DVD_TIME_BASE) * AV_TIME_BASE;
+
+ m_pFilterFrameIn->pkt_dts = m_pFilterFrameIn->pts;
+ m_pFilterFrameIn->best_effort_timestamp = m_pFilterFrameIn->pts;
+
+ av_frame_get_buffer(m_pFilterFrameIn, 64);
+
+ uint8_t *src, *dst;
+ src = buf + image.offsets[0];
+ dst = m_pFilterFrameIn->data[0];
+ m_dllSSE4.copy_frame(src, dst, m_cache, m_config.vidWidth, m_config.vidHeight, image.pitches[0]);
+
+ src = buf + image.offsets[1];
+ dst = m_pFilterFrameIn->data[1];
+ m_dllSSE4.copy_frame(src, dst, m_cache, image.width, image.height/2, image.pitches[1]);
+
+ m_pFilterFrameIn->linesize[0] = image.pitches[0];
+ m_pFilterFrameIn->linesize[1] = image.pitches[1];
+ m_pFilterFrameIn->data[2] = NULL;
+ m_pFilterFrameIn->data[3] = NULL;
+ m_pFilterFrameIn->pkt_size = image.data_size;
+
+ CheckSuccess(vaUnmapBuffer(m_config.dpy, image.buf), "vaUnmapBuffer");
+ CheckSuccess(vaDestroyImage(m_config.dpy, image.image_id), "vaDestroyImage");
+
+ if (m_diMethod == VS_INTERLACEMETHOD_DEINTERLACE)
+ {
+ if (av_buffersrc_add_frame(m_pFilterIn, m_pFilterFrameIn) < 0)
+ {
+ CLog::Log(LOGERROR, "CFFmpegPostproc::AddPicture - av_buffersrc_add_frame");
+ goto error;
+ }
+ }
+ else if (m_diMethod == VS_INTERLACEMETHOD_RENDER_BOB ||
+ m_diMethod == VS_INTERLACEMETHOD_NONE)
+ {
+ av_frame_move_ref(m_pFilterFrameOut, m_pFilterFrameIn);
+ m_step = 0;
+ }
+ av_frame_unref(m_pFilterFrameIn);
+
+ result = true;
+
+error:
+ m_config.videoSurfaces->ClearRender(surf);
+ return result;
+}
+
+bool CFFmpegPostproc::Filter(CVaapiProcessedPicture &outPic)
+{
+ outPic.DVDPic.SetParams(m_DVDPic);
+ if (m_diMethod == VS_INTERLACEMETHOD_DEINTERLACE)
+ {
+ int result;
+ result = av_buffersink_get_frame(m_pFilterOut, m_pFilterFrameOut);
+
+ if(result == AVERROR(EAGAIN) || result == AVERROR_EOF)
+ return false;
+ else if(result < 0)
+ {
+ CLog::Log(LOGERROR, "CFFmpegPostproc::Filter - av_buffersink_get_frame");
+ return false;
+ }
+ outPic.DVDPic.iFlags &= ~(DVP_FLAG_TOP_FIELD_FIRST |
+ DVP_FLAG_REPEAT_TOP_FIELD |
+ DVP_FLAG_INTERLACED);
+ }
+ else if (m_diMethod == VS_INTERLACEMETHOD_RENDER_BOB ||
+ m_diMethod == VS_INTERLACEMETHOD_NONE)
+ {
+ if (m_step > 0)
+ return false;
+ }
+
+ m_step++;
+ outPic.frame = av_frame_clone(m_pFilterFrameOut);
+ av_frame_unref(m_pFilterFrameOut);
+
+ outPic.source = this;
+ m_refsToPics++;
+
+ int64_t bpts = outPic.frame->best_effort_timestamp;
+ if(bpts != AV_NOPTS_VALUE)
+ {
+ outPic.DVDPic.pts = (double)bpts * DVD_TIME_BASE / AV_TIME_BASE;
+ }
+ else
+ outPic.DVDPic.pts = DVD_NOPTS_VALUE;
+
+ double pts = outPic.DVDPic.pts;
+ if (m_lastOutPts != DVD_NOPTS_VALUE && m_lastOutPts == pts)
+ {
+ outPic.DVDPic.pts += m_frametime/2;
+ }
+ m_lastOutPts = pts;
+
+ return true;
+}
+
+void CFFmpegPostproc::ClearRef(CVaapiProcessedPicture &pic)
+{
+ av_frame_free(&pic.frame);
+ m_refsToPics--;
+
+ if (m_pOut && m_refsToPics <= 0 && m_cbDispose)
+ (m_pOut->*m_cbDispose)(this);
+}
+
+void CFFmpegPostproc::Close()
+{
+ if (m_pFilterGraph)
+ {
+ avfilter_graph_free(&m_pFilterGraph);
+ }
+}
+
+void CFFmpegPostproc::Flush()
+{
+ Close();
+ Init(m_diMethod);
+ m_DVDPic.pts = DVD_NOPTS_VALUE;
+ m_frametime = 0;
+ m_lastOutPts = DVD_NOPTS_VALUE;
+}
+
+bool CFFmpegPostproc::UpdateDeintMethod(EINTERLACEMETHOD method)
+{
+ /// \todo switching between certain methods could be done without deinit/init
+ return (m_diMethod == method);
+}
+
+bool CFFmpegPostproc::DoesSync()
+{
+ return true;
+}
+
+bool CFFmpegPostproc::UseVideoSurface()
+{
+ return false;
+}
+
+void CFFmpegPostproc::Discard(COutput *output, ReadyToDispose cb)
+{
+ m_pOut = output;
+ m_cbDispose = cb;
+ if (m_refsToPics <= 0)
+ (m_pOut->*m_cbDispose)(this);
+}
+
+bool CFFmpegPostproc::CheckSuccess(VAStatus status, const std::string& function)
+{
+ if (status != VA_STATUS_SUCCESS)
+ {
+ CLog::Log(LOGERROR, "VAAPI/ffpp error: {} ({})", function, vaErrorStr(status), status);
+ return false;
+ }
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/VAAPI.h b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VAAPI.h
new file mode 100644
index 0000000..1202199
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VAAPI.h
@@ -0,0 +1,648 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "DVDVideoCodec.h"
+#include "cores/VideoPlayer/Buffers/VideoBuffer.h"
+#include "cores/VideoSettings.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/SharedSection.h"
+#include "threads/Thread.h"
+#include "utils/ActorProtocol.h"
+#include "utils/Geometry.h"
+
+#include "platform/linux/sse4/DllLibSSE4.h"
+
+#include <list>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+#include <va/va.h>
+
+extern "C" {
+#include <libavutil/avutil.h>
+#include <libavfilter/avfilter.h>
+}
+
+using namespace Actor;
+
+class CProcessInfo;
+
+#define FULLHD_WIDTH 1920
+
+namespace VAAPI
+{
+
+void VaErrorCallback(void *user_context, const char *message);
+void VaInfoCallback(void *user_context, const char *message);
+
+//-----------------------------------------------------------------------------
+// VAAPI data structs
+//-----------------------------------------------------------------------------
+
+class CDecoder;
+
+/**
+ * Buffer statistics used to control number of frames in queue
+ */
+
+class CVaapiBufferStats
+{
+public:
+ uint16_t decodedPics;
+ uint16_t processedPics;
+ uint16_t renderPics;
+ uint64_t latency; // time decoder has waited for a frame, ideally there is no latency
+ int codecFlags;
+ bool canSkipDeint;
+ int processCmd;
+ bool isVpp;
+
+ void IncDecoded()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ decodedPics++;
+ }
+ void DecDecoded()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ decodedPics--;
+ }
+ void IncProcessed()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ processedPics++;
+ }
+ void DecProcessed()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ processedPics--;
+ }
+ void IncRender()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ renderPics++;
+ }
+ void DecRender()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ renderPics--;
+ }
+ void Reset()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ decodedPics = 0;
+ processedPics = 0;
+ renderPics = 0;
+ latency = 0;
+ isVpp = false;
+ }
+ void Get(uint16_t& decoded, uint16_t& processed, uint16_t& render, bool& vpp)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ decoded = decodedPics, processed = processedPics, render = renderPics;
+ vpp = isVpp;
+ }
+ void SetParams(uint64_t time, int flags)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ latency = time;
+ codecFlags = flags;
+ }
+ void GetParams(uint64_t& lat, int& flags)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ lat = latency;
+ flags = codecFlags;
+ }
+ void SetCmd(int cmd)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ processCmd = cmd;
+ }
+ void GetCmd(int& cmd)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ cmd = processCmd;
+ processCmd = 0;
+ }
+ void SetCanSkipDeint(bool canSkip)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ canSkipDeint = canSkip;
+ }
+ bool CanSkipDeint()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ if (canSkipDeint)
+ return true;
+ else
+ return false;
+ }
+ void SetVpp(bool vpp)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ isVpp = vpp;
+ }
+
+private:
+ CCriticalSection m_sec;
+};
+
+/**
+ * CVaapiConfig holds all configuration parameters needed by vaapi
+ * The structure is sent to the internal classes CMixer and COutput
+ * for init.
+ */
+
+class CVideoSurfaces;
+class CVAAPIContext;
+
+struct CVaapiConfig
+{
+ int surfaceWidth;
+ int surfaceHeight;
+ int vidWidth;
+ int vidHeight;
+ int outWidth;
+ int outHeight;
+ AVRational aspect;
+ VAConfigID configId;
+ CVaapiBufferStats *stats;
+ int upscale;
+ CVideoSurfaces *videoSurfaces;
+ uint32_t maxReferences;
+ CVAAPIContext *context;
+ VADisplay dpy;
+ VAProfile profile;
+ VAConfigAttrib attrib;
+ CProcessInfo *processInfo;
+ bool driverIsMesa;
+ int bitDepth;
+};
+
+/**
+ * Holds a decoded frame
+ * Input to COutput for further processing
+ */
+struct CVaapiDecodedPicture
+{
+ CVaapiDecodedPicture() = default;
+ CVaapiDecodedPicture(const CVaapiDecodedPicture &rhs)
+ {
+ *this = rhs;
+ }
+ CVaapiDecodedPicture& operator=(const CVaapiDecodedPicture& rhs)
+ {
+ DVDPic.SetParams(rhs.DVDPic);
+ videoSurface = rhs.videoSurface;
+ index = rhs.index;
+ return *this;
+ };
+ VideoPicture DVDPic;
+ VASurfaceID videoSurface;
+ int index;
+};
+
+/**
+ * Frame after having been processed by vpp
+ */
+class CPostproc;
+struct CVaapiProcessedPicture
+{
+ CVaapiProcessedPicture() = default;
+ CVaapiProcessedPicture(const CVaapiProcessedPicture &rhs)
+ {
+ *this = rhs;
+ }
+ CVaapiProcessedPicture& operator=(const CVaapiProcessedPicture& rhs)
+ {
+ DVDPic.SetParams(rhs.DVDPic);
+ videoSurface = rhs.videoSurface;
+ frame = rhs.frame;
+ id = rhs.id;
+ source = rhs.source;
+ crop = rhs.crop;
+ return *this;
+ };
+
+ VideoPicture DVDPic;
+ VASurfaceID videoSurface;
+ AVFrame *frame;
+ int id;
+ CPostproc *source = nullptr;
+ bool crop;
+};
+
+class CVaapiRenderPicture : public CVideoBuffer
+{
+public:
+ explicit CVaapiRenderPicture(int id) : CVideoBuffer(id) { }
+ void GetPlanes(uint8_t*(&planes)[YuvImage::MAX_PLANES]) override;
+ void GetStrides(int(&strides)[YuvImage::MAX_PLANES]) override;
+ VideoPicture DVDPic;
+ CVaapiProcessedPicture procPic;
+ AVFrame *avFrame = nullptr;
+
+ bool valid = false;
+ VADisplay vadsp;
+};
+
+//-----------------------------------------------------------------------------
+// Output
+//-----------------------------------------------------------------------------
+
+class COutputControlProtocol : public Protocol
+{
+public:
+ COutputControlProtocol(std::string name, CEvent* inEvent, CEvent* outEvent)
+ : Protocol(std::move(name), inEvent, outEvent)
+ {
+ }
+ enum OutSignal
+ {
+ INIT,
+ FLUSH,
+ PRECLEANUP,
+ TIMEOUT,
+ };
+ enum InSignal
+ {
+ ACC,
+ ERROR,
+ STATS,
+ };
+};
+
+class COutputDataProtocol : public Protocol
+{
+public:
+ COutputDataProtocol(std::string name, CEvent* inEvent, CEvent* outEvent)
+ : Protocol(std::move(name), inEvent, outEvent)
+ {
+ }
+ enum OutSignal
+ {
+ NEWFRAME = 0,
+ RETURNPIC,
+ RETURNPROCPIC,
+ };
+ enum InSignal
+ {
+ PICTURE,
+ };
+};
+
+struct SDiMethods
+{
+ EINTERLACEMETHOD diMethods[8];
+ int numDiMethods;
+};
+
+/**
+ * COutput is embedded in CDecoder and embeds vpp
+ * The class has its own OpenGl context which is shared with render thread
+ * COutput generated ready to render textures and passes them back to
+ * CDecoder
+ */
+
+class CDecoder;
+class CPostproc;
+class CVaapiBufferPool;
+
+class COutput : private CThread
+{
+public:
+ COutput(CDecoder &decoder, CEvent *inMsgEvent);
+ ~COutput() override;
+ void Start();
+ void Dispose();
+ COutputControlProtocol m_controlPort;
+ COutputDataProtocol m_dataPort;
+protected:
+ void OnStartup() override;
+ void OnExit() override;
+ void Process() override;
+ void StateMachine(int signal, Protocol *port, Message *msg);
+ bool HasWork();
+ bool PreferPP();
+ void InitCycle();
+ CVaapiRenderPicture* ProcessPicture(CVaapiProcessedPicture &pic);
+ void QueueReturnPicture(CVaapiRenderPicture *pic);
+ void ProcessReturnPicture(CVaapiRenderPicture *pic);
+ void ProcessReturnProcPicture(int id);
+ void ProcessSyncPicture();
+ void ReleaseProcessedPicture(CVaapiProcessedPicture &pic);
+ bool Init();
+ bool Uninit();
+ void Flush();
+ void EnsureBufferPool();
+ void ReleaseBufferPool(bool precleanup = false);
+ void ReadyForDisposal(CPostproc *pp);
+ CEvent m_outMsgEvent;
+ CEvent *m_inMsgEvent;
+ int m_state;
+ bool m_bStateMachineSelfTrigger;
+ CDecoder &m_vaapi;
+
+ // extended state variables for state machine
+ int m_extTimeout;
+ /// \brief Whether at least one interlaced frame was encountered in the video stream (indicating that more interlaced frames could potentially follow)
+ bool m_seenInterlaced;
+ CVaapiConfig m_config;
+ std::shared_ptr<CVaapiBufferPool> m_bufferPool;
+ CVaapiDecodedPicture m_currentPicture;
+ CPostproc *m_pp;
+ std::list<std::shared_ptr<CPostproc>> m_discardedPostprocs;
+ SDiMethods m_diMethods;
+};
+
+//-----------------------------------------------------------------------------
+// VAAPI Video Surface states
+//-----------------------------------------------------------------------------
+
+class CVideoSurfaces
+{
+public:
+ void AddSurface(VASurfaceID surf);
+ void ClearReference(VASurfaceID surf);
+ bool MarkRender(VASurfaceID surf);
+ void ClearRender(VASurfaceID surf);
+ bool IsValid(VASurfaceID surf);
+ VASurfaceID GetFree(VASurfaceID surf);
+ VASurfaceID GetAtIndex(int idx);
+ VASurfaceID RemoveNext(bool skiprender = false);
+ void Reset();
+ int Size();
+ bool HasFree();
+ bool HasRefs();
+ int NumFree();
+protected:
+ std::map<VASurfaceID, int> m_state;
+ std::list<VASurfaceID> m_freeSurfaces;
+ CCriticalSection m_section;
+};
+
+//-----------------------------------------------------------------------------
+// VAAPI decoder
+//-----------------------------------------------------------------------------
+
+class CVAAPIContext
+{
+public:
+ static bool EnsureContext(CVAAPIContext **ctx, CDecoder *decoder);
+ void Release(CDecoder *decoder);
+ VADisplay GetDisplay();
+ bool SupportsProfile(VAProfile profile);
+ VAConfigAttrib GetAttrib(VAProfile profile);
+ VAConfigID CreateConfig(VAProfile profile, VAConfigAttrib attrib);
+ static void FFReleaseBuffer(void *opaque, uint8_t *data);
+private:
+ CVAAPIContext();
+ void Close();
+ void SetVaDisplayForSystem();
+ bool CreateContext();
+ void DestroyContext();
+ void QueryCaps();
+ bool CheckSuccess(VAStatus status, const std::string& function);
+ bool IsValidDecoder(CDecoder *decoder);
+ void SetValidDRMVaDisplayFromRenderNode();
+ static CVAAPIContext *m_context;
+ static CCriticalSection m_section;
+ VADisplay m_display = NULL;
+ int m_refCount;
+ int m_profileCount;
+ VAProfile *m_profiles;
+ std::vector<CDecoder*> m_decoders;
+ int m_renderNodeFD{-1};
+};
+
+//-----------------------------------------------------------------------------
+// Interface into windowing
+//-----------------------------------------------------------------------------
+
+class IVaapiWinSystem
+{
+public:
+ virtual ~IVaapiWinSystem() = default;
+
+ virtual VADisplay GetVADisplay() = 0;
+ virtual void* GetEGLDisplay() { return nullptr; }
+};
+
+//-----------------------------------------------------------------------------
+// VAAPI main class
+//-----------------------------------------------------------------------------
+
+class CDecoder
+ : public IHardwareDecoder
+{
+ friend class CVaapiBufferPool;
+
+public:
+
+ explicit CDecoder(CProcessInfo& processInfo);
+ ~CDecoder() override;
+
+ bool Open (AVCodecContext* avctx, AVCodecContext* mainctx, const enum AVPixelFormat) override;
+ CDVDVideoCodec::VCReturn Decode (AVCodecContext* avctx, AVFrame* frame) override;
+ bool GetPicture(AVCodecContext* avctx, VideoPicture* picture) override;
+ void Reset() override;
+ virtual void Close();
+ long Release() override;
+ bool CanSkipDeint() override;
+ unsigned GetAllowedReferences() override { return 4; }
+
+ CDVDVideoCodec::VCReturn Check(AVCodecContext* avctx) override;
+ const std::string Name() override { return "vaapi"; }
+ void SetCodecControl(int flags) override;
+
+ void FFReleaseBuffer(uint8_t *data);
+ static int FFGetBuffer(AVCodecContext *avctx, AVFrame *pic, int flags);
+
+ static IHardwareDecoder* Create(CDVDStreamInfo &hint, CProcessInfo &processInfo, AVPixelFormat fmt);
+ static void Register(IVaapiWinSystem *winSystem, bool deepColor);
+
+ static IVaapiWinSystem* m_pWinSystem;
+
+protected:
+ void SetWidthHeight(int width, int height);
+ bool ConfigVAAPI();
+ bool CheckStatus(VAStatus vdp_st, int line);
+ void FiniVAAPIOutput();
+ void ReturnRenderPicture(CVaapiRenderPicture *renderPic);
+ long ReleasePicReference();
+ bool CheckSuccess(VAStatus status, const std::string& function);
+
+ enum EDisplayState
+ { VAAPI_OPEN
+ , VAAPI_RESET
+ , VAAPI_LOST
+ , VAAPI_ERROR
+ } m_DisplayState;
+ CCriticalSection m_DecoderSection;
+ CEvent m_DisplayEvent;
+ int m_ErrorCount;
+
+ bool m_vaapiConfigured;
+ CVaapiConfig m_vaapiConfig;
+ CVideoSurfaces m_videoSurfaces;
+ AVCodecContext* m_avctx;
+ int m_getBufferError;
+
+ COutput m_vaapiOutput;
+ CVaapiBufferStats m_bufferStats;
+ CEvent m_inMsgEvent;
+ CVaapiRenderPicture *m_presentPicture = nullptr;
+
+ int m_codecControl;
+ CProcessInfo& m_processInfo;
+
+ static bool m_capGeneral;
+ static bool m_capDeepColor;
+
+private:
+ struct AVBufferRefDeleter
+ {
+ void operator()(AVBufferRef* p) const;
+ };
+
+ std::unique_ptr<AVBufferRef, AVBufferRefDeleter> m_deviceRef;
+};
+
+//-----------------------------------------------------------------------------
+// Postprocessing
+//-----------------------------------------------------------------------------
+
+/**
+ * Base class
+ */
+typedef void (COutput::*ReadyToDispose)(CPostproc *pool);
+class CPostproc
+{
+public:
+ virtual ~CPostproc() = default;
+ virtual bool PreInit(CVaapiConfig &config, SDiMethods *methods = NULL) = 0;
+ virtual bool Init(EINTERLACEMETHOD method) = 0;
+ virtual bool AddPicture(CVaapiDecodedPicture &inPic) = 0;
+ virtual bool Filter(CVaapiProcessedPicture &outPic) = 0;
+ virtual void ClearRef(CVaapiProcessedPicture &pic) = 0;
+ virtual void Flush() = 0;
+ virtual bool UpdateDeintMethod(EINTERLACEMETHOD method) = 0;
+ virtual bool DoesSync() = 0;
+ virtual bool WantsPic() {return true;}
+ virtual bool UseVideoSurface() = 0;
+ virtual void Discard(COutput* output, ReadyToDispose cb) { (output->*cb)(this); }
+
+protected:
+ CVaapiConfig m_config;
+ int m_step;
+};
+
+/**
+ * skip post processing
+ */
+class CSkipPostproc : public CPostproc
+{
+public:
+ bool PreInit(CVaapiConfig &config, SDiMethods *methods = NULL) override;
+ bool Init(EINTERLACEMETHOD method) override;
+ bool AddPicture(CVaapiDecodedPicture &inPic) override;
+ bool Filter(CVaapiProcessedPicture &outPic) override;
+ void ClearRef(CVaapiProcessedPicture &pic) override;
+ void Flush() override;
+ bool UpdateDeintMethod(EINTERLACEMETHOD method) override;
+ bool DoesSync() override;
+ bool UseVideoSurface() override;
+ void Discard(COutput *output, ReadyToDispose cb) override;
+protected:
+ CVaapiDecodedPicture m_pic;
+ ReadyToDispose m_cbDispose = nullptr;
+ COutput *m_pOut;
+ int m_refsToSurfaces = 0;
+};
+
+/**
+ * VAAPI post processing
+ */
+class CVppPostproc : public CPostproc
+{
+public:
+ CVppPostproc();
+ ~CVppPostproc() override;
+ bool PreInit(CVaapiConfig &config, SDiMethods *methods = NULL) override;
+ bool Init(EINTERLACEMETHOD method) override;
+ bool AddPicture(CVaapiDecodedPicture &inPic) override;
+ bool Filter(CVaapiProcessedPicture &outPic) override;
+ void ClearRef(CVaapiProcessedPicture &pic) override;
+ void Flush() override;
+ bool UpdateDeintMethod(EINTERLACEMETHOD method) override;
+ bool DoesSync() override;
+ bool WantsPic() override;
+ bool UseVideoSurface() override;
+ void Discard(COutput *output, ReadyToDispose cb) override;
+protected:
+ bool CheckSuccess(VAStatus status, const std::string& function);
+ void Dispose();
+ void Advance();
+ VAConfigID m_configId = VA_INVALID_ID;
+ VAContextID m_contextId = VA_INVALID_ID;
+ CVideoSurfaces m_videoSurfaces;
+ std::deque<CVaapiDecodedPicture> m_decodedPics;
+ VABufferID m_filter = VA_INVALID_ID;
+ int m_forwardRefs, m_backwardRefs;
+ int m_currentIdx;
+ int m_frameCount;
+ EINTERLACEMETHOD m_vppMethod;
+ ReadyToDispose m_cbDispose = nullptr;
+ COutput *m_pOut = nullptr;
+};
+
+/**
+ * ffmpeg filter
+ */
+class CFFmpegPostproc : public CPostproc
+{
+public:
+ CFFmpegPostproc();
+ ~CFFmpegPostproc() override;
+ bool PreInit(CVaapiConfig &config, SDiMethods *methods = NULL) override;
+ bool Init(EINTERLACEMETHOD method) override;
+ bool AddPicture(CVaapiDecodedPicture &inPic) override;
+ bool Filter(CVaapiProcessedPicture &outPic) override;
+ void ClearRef(CVaapiProcessedPicture &pic) override;
+ void Flush() override;
+ bool UpdateDeintMethod(EINTERLACEMETHOD method) override;
+ bool DoesSync() override;
+ bool UseVideoSurface() override;
+ void Discard(COutput *output, ReadyToDispose cb) override;
+protected:
+ bool CheckSuccess(VAStatus status, const std::string& function);
+ void Close();
+ DllLibSSE4 m_dllSSE4;
+ uint8_t *m_cache;
+ AVFilterGraph* m_pFilterGraph;
+ AVFilterContext* m_pFilterIn;
+ AVFilterContext* m_pFilterOut;
+ AVFrame *m_pFilterFrameIn;
+ AVFrame *m_pFilterFrameOut;
+ EINTERLACEMETHOD m_diMethod;
+ VideoPicture m_DVDPic;
+ double m_frametime;
+ double m_lastOutPts;
+ ReadyToDispose m_cbDispose = nullptr;
+ COutput *m_pOut;
+ int m_refsToPics = 0;
+};
+
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/VDPAU.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VDPAU.cpp
new file mode 100644
index 0000000..fc7f6b9
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VDPAU.cpp
@@ -0,0 +1,3464 @@
+/*
+ * 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 "VDPAU.h"
+
+#include "DVDCodecs/DVDCodecUtils.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFlags.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderManager.h"
+#include "guilib/TextureManager.h"
+#include "rendering/RenderSystem.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/MathUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/X11/WinSystemX11.h"
+
+#include <array>
+#include <mutex>
+
+#include <dlfcn.h>
+
+using namespace Actor;
+using namespace VDPAU;
+using namespace std::chrono_literals;
+
+#define NUM_RENDER_PICS 7
+#define NUM_CROP_PIX 3
+
+#define ARSIZE(x) (sizeof(x) / sizeof((x)[0]))
+
+CDecoder::Desc decoder_profiles[] = {
+ {"MPEG1", VDP_DECODER_PROFILE_MPEG1, 0},
+ {"MPEG2_SIMPLE", VDP_DECODER_PROFILE_MPEG2_SIMPLE, 0},
+ {"MPEG2_MAIN", VDP_DECODER_PROFILE_MPEG2_MAIN, 0},
+ {"H264_BASELINE", VDP_DECODER_PROFILE_H264_BASELINE, 0},
+ {"H264_MAIN", VDP_DECODER_PROFILE_H264_MAIN, 0},
+ {"H264_HIGH", VDP_DECODER_PROFILE_H264_HIGH, 0},
+ {"VC1_SIMPLE", VDP_DECODER_PROFILE_VC1_SIMPLE, 0},
+ {"VC1_MAIN", VDP_DECODER_PROFILE_VC1_MAIN, 0},
+ {"VC1_ADVANCED", VDP_DECODER_PROFILE_VC1_ADVANCED, 0},
+ {"MPEG4_PART2_ASP", VDP_DECODER_PROFILE_MPEG4_PART2_ASP, 0},
+#ifdef VDP_DECODER_PROFILE_HEVC_MAIN
+ {"HEVC_MAIN", VDP_DECODER_PROFILE_HEVC_MAIN, 0},
+#endif
+#ifdef VDP_DECODER_PROFILE_VP9_PROFILE_0
+ {"VP9_PROFILE_0", VDP_DECODER_PROFILE_VP9_PROFILE_0, 0},
+#endif
+};
+
+static struct SInterlaceMapping
+{
+ const EINTERLACEMETHOD method;
+ const VdpVideoMixerFeature feature;
+} g_interlace_mapping[] =
+{ {VS_INTERLACEMETHOD_VDPAU_TEMPORAL , VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL}
+, {VS_INTERLACEMETHOD_VDPAU_TEMPORAL_HALF , VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL}
+, {VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL , VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL_SPATIAL}
+, {VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL_HALF, VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL_SPATIAL}
+, {VS_INTERLACEMETHOD_VDPAU_INVERSE_TELECINE , VDP_VIDEO_MIXER_FEATURE_INVERSE_TELECINE}
+, {VS_INTERLACEMETHOD_NONE , (VdpVideoMixerFeature)-1}
+};
+
+static float studioCSCKCoeffs601[3] = {0.299, 0.587, 0.114}; //BT601 {Kr, Kg, Kb}
+static float studioCSCKCoeffs709[3] = {0.2126, 0.7152, 0.0722}; //BT709 {Kr, Kg, Kb}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+CVDPAUContext *CVDPAUContext::m_context = 0;
+CCriticalSection CVDPAUContext::m_section;
+Display *CVDPAUContext::m_display = 0;
+void *CVDPAUContext::m_dlHandle = 0;
+
+CVDPAUContext::CVDPAUContext()
+{
+ m_context = 0;
+ m_refCount = 0;
+}
+
+void CVDPAUContext::Release()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ m_refCount--;
+ if (m_refCount <= 0)
+ {
+ Close();
+ delete this;
+ m_context = 0;
+ }
+}
+
+void CVDPAUContext::Close()
+{
+ CLog::Log(LOGINFO, "VDPAU::Close - closing decoder context");
+ DestroyContext();
+}
+
+bool CVDPAUContext::EnsureContext(CVDPAUContext **ctx)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (m_context)
+ {
+ m_context->m_refCount++;
+ *ctx = m_context;
+ return true;
+ }
+
+ m_context = new CVDPAUContext();
+ *ctx = m_context;
+ {
+ std::unique_lock<CCriticalSection> gLock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ if (!m_context->LoadSymbols() || !m_context->CreateContext())
+ {
+ delete m_context;
+ m_context = 0;
+ *ctx = NULL;
+ return false;
+ }
+ }
+
+ m_context->m_refCount++;
+
+ *ctx = m_context;
+ return true;
+}
+
+VDPAU_procs& CVDPAUContext::GetProcs()
+{
+ return m_vdpProcs;
+}
+
+VdpVideoMixerFeature* CVDPAUContext::GetFeatures()
+{
+ return m_vdpFeatures;
+}
+
+int CVDPAUContext::GetFeatureCount()
+{
+ return m_featureCount;
+}
+
+bool CVDPAUContext::LoadSymbols()
+{
+ if (!m_dlHandle)
+ {
+ m_dlHandle = dlopen("libvdpau.so.1", RTLD_LAZY);
+ if (!m_dlHandle)
+ {
+ const char* error = dlerror();
+ if (!error)
+ error = "dlerror() returned NULL";
+
+ CLog::Log(LOGERROR, "VDPAU::LoadSymbols: Unable to get handle to lib: {}", error);
+ return false;
+ }
+ }
+
+ char* error;
+ (void)dlerror();
+ dl_vdp_device_create_x11 = (VdpStatus (*)(Display*, int, VdpDevice*, VdpStatus (**)(VdpDevice, VdpFuncId, void**)))dlsym(m_dlHandle, (const char*)"vdp_device_create_x11");
+ error = dlerror();
+ if (error)
+ {
+ CLog::Log(LOGERROR, "(VDPAU) - {} in {}", error, __FUNCTION__);
+ m_vdpDevice = VDP_INVALID_HANDLE;
+ return false;
+ }
+ return true;
+}
+
+bool CVDPAUContext::CreateContext()
+{
+ CLog::Log(LOGINFO, "VDPAU::CreateContext - creating decoder context");
+
+ int screen;
+ {
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ if (!m_display)
+ m_display = XOpenDisplay(NULL);
+
+ if (!m_display)
+ return false;
+
+ screen = static_cast<KODI::WINDOWING::X11::CWinSystemX11*>(CServiceBroker::GetWinSystem())->GetScreen();
+ }
+
+ VdpStatus vdp_st;
+ // Create Device
+ vdp_st = dl_vdp_device_create_x11(m_display,
+ screen,
+ &m_vdpDevice,
+ &m_vdpProcs.vdp_get_proc_address);
+
+ CLog::Log(LOGINFO, "vdp_device = {:#08x} vdp_st = {:#08x}", m_vdpDevice, vdp_st);
+ if (vdp_st != VDP_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, "(VDPAU) unable to init VDPAU - vdp_st = 0x{:x}. Falling back.", vdp_st);
+ m_vdpDevice = VDP_INVALID_HANDLE;
+ return false;
+ }
+
+ QueryProcs();
+ SpewHardwareAvailable();
+ return true;
+}
+
+void CVDPAUContext::QueryProcs()
+{
+ VdpStatus vdp_st;
+
+#define VDP_PROC(id, proc) \
+ do { \
+ vdp_st = m_vdpProcs.vdp_get_proc_address(m_vdpDevice, id, (void**)&proc); \
+ if (vdp_st != VDP_STATUS_OK) \
+ { \
+ CLog::Log(LOGERROR, "CVDPAUContext::GetProcs - failed to get proc id"); \
+ } \
+ } while(0);
+
+ VDP_PROC(VDP_FUNC_ID_GET_ERROR_STRING , m_vdpProcs.vdp_get_error_string);
+ VDP_PROC(VDP_FUNC_ID_DEVICE_DESTROY , m_vdpProcs.vdp_device_destroy);
+ VDP_PROC(VDP_FUNC_ID_GENERATE_CSC_MATRIX , m_vdpProcs.vdp_generate_csc_matrix);
+ VDP_PROC(VDP_FUNC_ID_VIDEO_SURFACE_CREATE , m_vdpProcs.vdp_video_surface_create);
+ VDP_PROC(VDP_FUNC_ID_VIDEO_SURFACE_DESTROY , m_vdpProcs.vdp_video_surface_destroy);
+ VDP_PROC(VDP_FUNC_ID_VIDEO_SURFACE_PUT_BITS_Y_CB_CR , m_vdpProcs.vdp_video_surface_put_bits_y_cb_cr);
+ VDP_PROC(VDP_FUNC_ID_VIDEO_SURFACE_GET_BITS_Y_CB_CR , m_vdpProcs.vdp_video_surface_get_bits_y_cb_cr);
+ VDP_PROC(VDP_FUNC_ID_OUTPUT_SURFACE_PUT_BITS_Y_CB_CR , m_vdpProcs.vdp_output_surface_put_bits_y_cb_cr);
+ VDP_PROC(VDP_FUNC_ID_OUTPUT_SURFACE_PUT_BITS_NATIVE , m_vdpProcs.vdp_output_surface_put_bits_native);
+ VDP_PROC(VDP_FUNC_ID_OUTPUT_SURFACE_CREATE , m_vdpProcs.vdp_output_surface_create);
+ VDP_PROC(VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY , m_vdpProcs.vdp_output_surface_destroy);
+ VDP_PROC(VDP_FUNC_ID_OUTPUT_SURFACE_GET_BITS_NATIVE , m_vdpProcs.vdp_output_surface_get_bits_native);
+ VDP_PROC(VDP_FUNC_ID_OUTPUT_SURFACE_RENDER_OUTPUT_SURFACE, m_vdpProcs.vdp_output_surface_render_output_surface);
+ VDP_PROC(VDP_FUNC_ID_OUTPUT_SURFACE_PUT_BITS_INDEXED , m_vdpProcs.vdp_output_surface_put_bits_indexed);
+ VDP_PROC(VDP_FUNC_ID_VIDEO_MIXER_CREATE , m_vdpProcs.vdp_video_mixer_create);
+ VDP_PROC(VDP_FUNC_ID_VIDEO_MIXER_SET_FEATURE_ENABLES , m_vdpProcs.vdp_video_mixer_set_feature_enables);
+ VDP_PROC(VDP_FUNC_ID_VIDEO_MIXER_DESTROY , m_vdpProcs.vdp_video_mixer_destroy);
+ VDP_PROC(VDP_FUNC_ID_VIDEO_MIXER_RENDER , m_vdpProcs.vdp_video_mixer_render);
+ VDP_PROC(VDP_FUNC_ID_VIDEO_MIXER_SET_ATTRIBUTE_VALUES , m_vdpProcs.vdp_video_mixer_set_attribute_values);
+ VDP_PROC(VDP_FUNC_ID_VIDEO_MIXER_QUERY_PARAMETER_SUPPORT , m_vdpProcs.vdp_video_mixer_query_parameter_support);
+ VDP_PROC(VDP_FUNC_ID_VIDEO_MIXER_QUERY_FEATURE_SUPPORT , m_vdpProcs.vdp_video_mixer_query_feature_support);
+ VDP_PROC(VDP_FUNC_ID_DECODER_CREATE , m_vdpProcs.vdp_decoder_create);
+ VDP_PROC(VDP_FUNC_ID_DECODER_DESTROY , m_vdpProcs.vdp_decoder_destroy);
+ VDP_PROC(VDP_FUNC_ID_DECODER_RENDER , m_vdpProcs.vdp_decoder_render);
+ VDP_PROC(VDP_FUNC_ID_DECODER_QUERY_CAPABILITIES , m_vdpProcs.vdp_decoder_query_caps);
+#undef VDP_PROC
+}
+
+VdpDevice CVDPAUContext::GetDevice()
+{
+ return m_vdpDevice;
+}
+
+void CVDPAUContext::DestroyContext()
+{
+ if (!m_vdpProcs.vdp_device_destroy)
+ return;
+
+ m_vdpProcs.vdp_device_destroy(m_vdpDevice);
+ m_vdpDevice = VDP_INVALID_HANDLE;
+}
+
+void CVDPAUContext::SpewHardwareAvailable() //Copyright (c) 2008 Wladimir J. van der Laan -- VDPInfo
+{
+ VdpStatus rv;
+ CLog::Log(LOGINFO, "VDPAU Decoder capabilities:");
+ CLog::Log(LOGINFO, "name level macbs width height");
+ CLog::Log(LOGINFO, "------------------------------------");
+ for(const CDecoder::Desc& decoder_profile : decoder_profiles)
+ {
+ VdpBool is_supported = false;
+ uint32_t max_level, max_macroblocks, max_width, max_height;
+ rv = m_vdpProcs.vdp_decoder_query_caps(m_vdpDevice, decoder_profile.id,
+ &is_supported, &max_level, &max_macroblocks, &max_width, &max_height);
+ if(rv == VDP_STATUS_OK && is_supported)
+ {
+ CLog::Log(LOGINFO, "{:<16} {:2} {:5} {:5} {:5}", decoder_profile.name, max_level,
+ max_macroblocks, max_width, max_height);
+ }
+ }
+ CLog::Log(LOGINFO, "------------------------------------");
+ m_featureCount = 0;
+#define CHECK_SUPPORT(feature) \
+ do \
+ { \
+ VdpBool supported; \
+ if (m_vdpProcs.vdp_video_mixer_query_feature_support(m_vdpDevice, feature, &supported) == \
+ VDP_STATUS_OK && \
+ supported) \
+ { \
+ CLog::Log(LOGINFO, "Mixer feature: " #feature); \
+ m_vdpFeatures[m_featureCount++] = feature; \
+ } \
+ } while (false)
+
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_NOISE_REDUCTION);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_SHARPNESS);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL_SPATIAL);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_INVERSE_TELECINE);
+#ifdef VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L1
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L1);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L2);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L3);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L4);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L5);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L6);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L7);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L8);
+ CHECK_SUPPORT(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L9);
+#endif
+#undef CHECK_SUPPORT
+}
+
+bool CVDPAUContext::Supports(VdpVideoMixerFeature feature)
+{
+ for(int i = 0; i < m_featureCount; i++)
+ {
+ if(m_vdpFeatures[i] == feature)
+ return true;
+ }
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// VDPAU Video Surface states
+//-----------------------------------------------------------------------------
+
+#define SURFACE_USED_FOR_REFERENCE 0x01
+#define SURFACE_USED_FOR_RENDER 0x02
+
+void CVideoSurfaces::AddSurface(VdpVideoSurface surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_state[surf] = SURFACE_USED_FOR_REFERENCE;
+}
+
+void CVideoSurfaces::ClearReference(VdpVideoSurface surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_state.find(surf) == m_state.end())
+ {
+ CLog::Log(LOGWARNING, "CVideoSurfaces::ClearReference - surface invalid");
+ return;
+ }
+ m_state[surf] &= ~SURFACE_USED_FOR_REFERENCE;
+ if (m_state[surf] == 0)
+ {
+ m_freeSurfaces.push_back(surf);
+ }
+}
+
+bool CVideoSurfaces::MarkRender(VdpVideoSurface surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_state.find(surf) == m_state.end())
+ {
+ CLog::Log(LOGWARNING, "CVideoSurfaces::MarkRender - surface invalid");
+ return false;
+ }
+ std::list<VdpVideoSurface>::iterator it;
+ it = std::find(m_freeSurfaces.begin(), m_freeSurfaces.end(), surf);
+ if (it != m_freeSurfaces.end())
+ {
+ m_freeSurfaces.erase(it);
+ }
+ m_state[surf] |= SURFACE_USED_FOR_RENDER;
+ return true;
+}
+
+void CVideoSurfaces::ClearRender(VdpVideoSurface surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_state.find(surf) == m_state.end())
+ {
+ CLog::Log(LOGWARNING, "CVideoSurfaces::ClearRender - surface invalid");
+ return;
+ }
+ m_state[surf] &= ~SURFACE_USED_FOR_RENDER;
+ if (m_state[surf] == 0)
+ {
+ m_freeSurfaces.push_back(surf);
+ }
+}
+
+bool CVideoSurfaces::IsValid(VdpVideoSurface surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_state.find(surf) != m_state.end())
+ return true;
+ else
+ return false;
+}
+
+VdpVideoSurface CVideoSurfaces::GetFree(VdpVideoSurface surf)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_state.find(surf) != m_state.end())
+ {
+ std::list<VdpVideoSurface>::iterator it;
+ it = std::find(m_freeSurfaces.begin(), m_freeSurfaces.end(), surf);
+ if (it == m_freeSurfaces.end())
+ {
+ CLog::Log(LOGWARNING, "CVideoSurfaces::GetFree - surface not free");
+ }
+ else
+ {
+ m_freeSurfaces.erase(it);
+ m_state[surf] = SURFACE_USED_FOR_REFERENCE;
+ return surf;
+ }
+ }
+
+ if (!m_freeSurfaces.empty())
+ {
+ VdpVideoSurface freeSurf = m_freeSurfaces.front();
+ m_freeSurfaces.pop_front();
+ m_state[freeSurf] = SURFACE_USED_FOR_REFERENCE;
+ return freeSurf;
+ }
+
+ return VDP_INVALID_HANDLE;
+}
+
+VdpVideoSurface CVideoSurfaces::RemoveNext(bool skiprender)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ VdpVideoSurface surf;
+ std::map<VdpVideoSurface, int>::iterator it;
+ for(it = m_state.begin(); it != m_state.end(); ++it)
+ {
+ if (skiprender && it->second & SURFACE_USED_FOR_RENDER)
+ continue;
+ surf = it->first;
+ m_state.erase(surf);
+
+ std::list<VdpVideoSurface>::iterator it2;
+ it2 = std::find(m_freeSurfaces.begin(), m_freeSurfaces.end(), surf);
+ if (it2 != m_freeSurfaces.end())
+ m_freeSurfaces.erase(it2);
+ return surf;
+ }
+ return VDP_INVALID_HANDLE;
+}
+
+void CVideoSurfaces::Reset()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_freeSurfaces.clear();
+ m_state.clear();
+}
+
+int CVideoSurfaces::Size()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ return m_state.size();
+}
+
+bool CVideoSurfaces::HasRefs()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for (const auto &i : m_state)
+ {
+ if (i.second & SURFACE_USED_FOR_REFERENCE)
+ return true;
+ }
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// CVDPAU
+//-----------------------------------------------------------------------------
+
+bool CDecoder::m_capGeneral = false;
+
+CDecoder::CDecoder(CProcessInfo& processInfo) :
+ m_vdpauOutput(*this, &m_inMsgEvent), m_processInfo(processInfo)
+{
+ m_vdpauConfig.videoSurfaces = &m_videoSurfaces;
+
+ m_vdpauConfigured = false;
+ m_DisplayState = VDPAU_OPEN;
+ m_vdpauConfig.context = 0;
+ m_vdpauConfig.processInfo = &m_processInfo;
+ m_vdpauConfig.resetCounter = 0;
+}
+
+bool CDecoder::Open(AVCodecContext* avctx, AVCodecContext* mainctx, const enum AVPixelFormat fmt)
+{
+ // this could be done better by querying actual hw capabilities
+ // but since vdpau will be dropped anyway in v19, this should do
+ if (avctx->sw_pix_fmt != AV_PIX_FMT_YUV420P &&
+ avctx->sw_pix_fmt != AV_PIX_FMT_YUVJ420P)
+ return false;
+
+ // check if user wants to decode this format with VDPAU
+ std::string gpuvendor = CServiceBroker::GetRenderSystem()->GetRenderVendor();
+ std::transform(gpuvendor.begin(), gpuvendor.end(), gpuvendor.begin(), ::tolower);
+ // nvidia is whitelisted despite for mpeg-4 we need to query user settings
+ if ((gpuvendor.compare(0, 6, "nvidia") != 0) || (avctx->codec_id == AV_CODEC_ID_MPEG4) || (avctx->codec_id == AV_CODEC_ID_H263))
+ {
+ std::map<AVCodecID, std::string> settings_map = {
+ { AV_CODEC_ID_H263, CSettings::SETTING_VIDEOPLAYER_USEVDPAUMPEG4 },
+ { AV_CODEC_ID_MPEG4, CSettings::SETTING_VIDEOPLAYER_USEVDPAUMPEG4 },
+ { AV_CODEC_ID_WMV3, CSettings::SETTING_VIDEOPLAYER_USEVDPAUVC1 },
+ { AV_CODEC_ID_VC1, CSettings::SETTING_VIDEOPLAYER_USEVDPAUVC1 },
+ { AV_CODEC_ID_MPEG2VIDEO, CSettings::SETTING_VIDEOPLAYER_USEVDPAUMPEG2 },
+ };
+
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return false;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return false;
+
+ auto entry = settings_map.find(avctx->codec_id);
+ if (entry != settings_map.end())
+ {
+ auto setting = settings->GetSetting(entry->second);
+ if (!setting)
+ {
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", entry->second);
+ return false;
+ }
+
+ bool enabled = settings->GetBool(entry->second) && setting->IsVisible();
+ if (!enabled)
+ return false;
+ }
+ }
+
+ if (!CServiceBroker::GetRenderSystem()->IsExtSupported("GL_NV_vdpau_interop"))
+ {
+ CLog::Log(LOGINFO, "VDPAU::Open: required extension GL_NV_vdpau_interop not found");
+ return false;
+ }
+
+ if (avctx->coded_width == 0 ||
+ avctx->coded_height == 0)
+ {
+ CLog::Log(LOGWARNING,"VDPAU::Open: no width/height available, can't init");
+ return false;
+ }
+ m_vdpauConfig.numRenderBuffers = 5;
+ m_vdpauConfig.timeOpened = CurrentHostCounter();
+
+ if (!CVDPAUContext::EnsureContext(&m_vdpauConfig.context))
+ return false;
+
+ m_DisplayState = VDPAU_OPEN;
+ m_vdpauConfigured = false;
+
+ m_presentPicture = 0;
+
+ {
+ VdpDecoderProfile profile = 0;
+
+ // convert FFMPEG codec ID to VDPAU profile.
+ ReadFormatOf(avctx->codec_id, profile, m_vdpauConfig.vdpChromaType);
+ if(profile)
+ {
+ VdpStatus vdp_st;
+ VdpBool is_supported = false;
+ uint32_t max_level, max_macroblocks, max_width, max_height;
+
+ // query device capabilities to ensure that VDPAU can handle the requested codec
+ vdp_st = m_vdpauConfig.context->GetProcs().vdp_decoder_query_caps(m_vdpauConfig.context->GetDevice(),
+ profile, &is_supported, &max_level, &max_macroblocks, &max_width, &max_height);
+
+ // test to make sure there is a possibility the codec will work
+ if (CheckStatus(vdp_st, __LINE__))
+ {
+ CLog::Log(LOGERROR, "VDPAU::Open: error {}({}) checking for decoder support",
+ m_vdpauConfig.context->GetProcs().vdp_get_error_string(vdp_st), vdp_st);
+ return false;
+ }
+
+ if (max_width < (uint32_t) avctx->coded_width || max_height < (uint32_t) avctx->coded_height)
+ {
+ CLog::Log(LOGWARNING,
+ "VDPAU::Open: requested picture dimensions ({}, {}) exceed hardware capabilities "
+ "( {}, {}).",
+ avctx->coded_width, avctx->coded_height, max_width, max_height);
+ return false;
+ }
+
+ if (!CDVDCodecUtils::IsVP3CompatibleWidth(avctx->coded_width))
+ CLog::Log(LOGWARNING, "VDPAU::Open width {} might not be supported because of hardware bug",
+ avctx->width);
+
+ // attempt to create a decoder with this width/height, some sizes are not supported by hw
+ vdp_st = m_vdpauConfig.context->GetProcs().vdp_decoder_create(m_vdpauConfig.context->GetDevice(), profile, avctx->coded_width, avctx->coded_height, 5, &m_vdpauConfig.vdpDecoder);
+
+ if (CheckStatus(vdp_st, __LINE__))
+ {
+ CLog::Log(LOGERROR, "VDPAU::Open: error: {}({}) checking for decoder support",
+ m_vdpauConfig.context->GetProcs().vdp_get_error_string(vdp_st), vdp_st);
+ return false;
+ }
+
+ m_vdpauConfig.context->GetProcs().vdp_decoder_destroy(m_vdpauConfig.vdpDecoder);
+ CheckStatus(vdp_st, __LINE__);
+
+ // finally setup ffmpeg
+ memset(&m_hwContext, 0, sizeof(AVVDPAUContext));
+ m_hwContext.render2 = CDecoder::Render;
+ avctx->get_buffer2 = CDecoder::FFGetBuffer;
+ avctx->slice_flags = SLICE_FLAG_CODED_ORDER|SLICE_FLAG_ALLOW_FIELD;
+ avctx->hwaccel_context = &m_hwContext;
+
+ CServiceBroker::GetWinSystem()->Register(this);
+ m_avctx = mainctx;
+ return true;
+ }
+ }
+ return false;
+}
+
+CDecoder::~CDecoder()
+{
+ Close();
+}
+
+void CDecoder::Close()
+{
+ CLog::Log(LOGINFO, " (VDPAU) {}", __FUNCTION__);
+
+ CServiceBroker::GetWinSystem()->Unregister(this);
+
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+
+ FiniVDPAUOutput();
+ m_vdpauOutput.Dispose();
+
+ if (m_vdpauConfig.context)
+ m_vdpauConfig.context->Release();
+ m_vdpauConfig.context = 0;
+}
+
+long CDecoder::Release()
+{
+ // if ffmpeg holds any references, flush buffers
+ if (m_avctx && m_videoSurfaces.HasRefs())
+ {
+ avcodec_flush_buffers(m_avctx);
+ }
+
+ // check if we should do some pre-cleanup here
+ // a second decoder might need resources
+ if (m_vdpauConfigured == true)
+ {
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+ CLog::Log(LOGINFO, "CVDPAU::Release pre-cleanup");
+
+ Message *reply;
+ if (m_vdpauOutput.m_controlPort.SendOutMessageSync(COutputControlProtocol::PRECLEANUP,
+ &reply,
+ 2000))
+ {
+ bool success = reply->signal == COutputControlProtocol::ACC ? true : false;
+ reply->Release();
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "VDPAU::{} - pre-cleanup returned error", __FUNCTION__);
+ m_DisplayState = VDPAU_ERROR;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "VDPAU::{} - pre-cleanup timed out", __FUNCTION__);
+ m_DisplayState = VDPAU_ERROR;
+ }
+
+ VdpVideoSurface surf;
+ while((surf = m_videoSurfaces.RemoveNext(true)) != VDP_INVALID_HANDLE)
+ {
+ m_vdpauConfig.context->GetProcs().vdp_video_surface_destroy(surf);
+ }
+ }
+ return IHardwareDecoder::Release();
+}
+
+long CDecoder::ReleasePicReference()
+{
+ return IHardwareDecoder::Release();
+}
+
+void CDecoder::SetWidthHeight(int width, int height)
+{
+ m_vdpauConfig.upscale = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoVDPAUScaling;
+
+ //pick the smallest dimensions, so we downscale with vdpau and upscale with opengl when appropriate
+ //this requires the least amount of gpu memory bandwidth
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth() < width || CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight() < height || m_vdpauConfig.upscale >= 0)
+ {
+ //scale width to desktop size if the aspect ratio is the same or bigger than the desktop
+ if ((double)height * CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth() / width <= (double)CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight())
+ {
+ m_vdpauConfig.outWidth = CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth();
+ m_vdpauConfig.outHeight = MathUtils::round_int((double)height * CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth() / width);
+ }
+ else //scale height to the desktop size if the aspect ratio is smaller than the desktop
+ {
+ m_vdpauConfig.outHeight = CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight();
+ m_vdpauConfig.outWidth = MathUtils::round_int((double)width * CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight() / height);
+ }
+ }
+ else
+ { //let opengl scale
+ m_vdpauConfig.outWidth = width;
+ m_vdpauConfig.outHeight = height;
+ }
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVDPAU::SetWidthHeight Setting OutWidth: {} OutHeight: {}",
+ m_vdpauConfig.outWidth, m_vdpauConfig.outHeight);
+}
+
+void CDecoder::OnLostDisplay()
+{
+ CLog::Log(LOGINFO, "CVDPAU::OnLostDevice event");
+
+ int count = CServiceBroker::GetWinSystem()->GetGfxContext().exit();
+
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+ FiniVDPAUOutput();
+ if (m_vdpauConfig.context)
+ m_vdpauConfig.context->Release();
+ m_vdpauConfig.context = 0;
+
+ m_DisplayState = VDPAU_LOST;
+ lock.unlock();
+ m_DisplayEvent.Reset();
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().restore(count);
+}
+
+void CDecoder::OnResetDisplay()
+{
+ CLog::Log(LOGINFO, "CVDPAU::OnResetDevice event");
+
+ int count = CServiceBroker::GetWinSystem()->GetGfxContext().exit();
+
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+ if (m_DisplayState == VDPAU_LOST)
+ {
+ m_DisplayState = VDPAU_RESET;
+ lock.unlock();
+ m_DisplayEvent.Set();
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().restore(count);
+}
+
+CDVDVideoCodec::VCReturn CDecoder::Check(AVCodecContext* avctx)
+{
+ EDisplayState state;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+ state = m_DisplayState;
+ }
+
+ if (state == VDPAU_LOST)
+ {
+ CLog::Log(LOGINFO, "CVDPAU::Check waiting for display reset event");
+ if (!m_DisplayEvent.Wait(4000ms))
+ {
+ CLog::Log(LOGERROR, "CVDPAU::Check - device didn't reset in reasonable time");
+ state = VDPAU_RESET;
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+ state = m_DisplayState;
+ }
+ }
+ if (state == VDPAU_RESET || state == VDPAU_ERROR)
+ {
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+
+ avcodec_flush_buffers(avctx);
+ FiniVDPAUOutput();
+ if (m_vdpauConfig.context)
+ m_vdpauConfig.context->Release();
+ m_vdpauConfig.context = 0;
+
+ if (CVDPAUContext::EnsureContext(&m_vdpauConfig.context))
+ {
+ m_DisplayState = VDPAU_OPEN;
+ m_vdpauConfigured = false;
+ }
+
+ if (state == VDPAU_RESET)
+ return CDVDVideoCodec::VC_FLUSHED;
+ else
+ return CDVDVideoCodec::VC_ERROR;
+ }
+ return CDVDVideoCodec::VC_NONE;
+}
+
+bool CDecoder::IsVDPAUFormat(AVPixelFormat format)
+{
+ if (format == AV_PIX_FMT_VDPAU)
+ return true;
+ else
+ return false;
+}
+
+bool CDecoder::Supports(VdpVideoMixerFeature feature)
+{
+ return m_vdpauConfig.context->Supports(feature);
+}
+
+void CDecoder::FiniVDPAUOutput()
+{
+ if (!m_vdpauConfigured)
+ return;
+
+ CLog::Log(LOGINFO, " (VDPAU) {}", __FUNCTION__);
+
+ // uninit output
+ m_vdpauOutput.Dispose();
+ m_vdpauConfigured = false;
+
+ VdpStatus vdp_st;
+
+ vdp_st = m_vdpauConfig.context->GetProcs().vdp_decoder_destroy(m_vdpauConfig.vdpDecoder);
+ if (CheckStatus(vdp_st, __LINE__))
+ return;
+ m_vdpauConfig.vdpDecoder = VDP_INVALID_HANDLE;
+
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVDPAU::FiniVDPAUOutput destroying {} video surfaces",
+ m_videoSurfaces.Size());
+
+ VdpVideoSurface surf;
+ while((surf = m_videoSurfaces.RemoveNext()) != VDP_INVALID_HANDLE)
+ {
+ m_vdpauConfig.context->GetProcs().vdp_video_surface_destroy(surf);
+ if (CheckStatus(vdp_st, __LINE__))
+ return;
+ }
+ m_videoSurfaces.Reset();
+}
+
+void CDecoder::ReadFormatOf( AVCodecID codec
+ , VdpDecoderProfile &vdp_decoder_profile
+ , VdpChromaType &vdp_chroma_type)
+{
+ switch (codec)
+ {
+ case AV_CODEC_ID_MPEG1VIDEO:
+ vdp_decoder_profile = VDP_DECODER_PROFILE_MPEG1;
+ vdp_chroma_type = VDP_CHROMA_TYPE_420;
+ break;
+ case AV_CODEC_ID_MPEG2VIDEO:
+ vdp_decoder_profile = VDP_DECODER_PROFILE_MPEG2_MAIN;
+ vdp_chroma_type = VDP_CHROMA_TYPE_420;
+ break;
+ case AV_CODEC_ID_H264:
+ vdp_decoder_profile = VDP_DECODER_PROFILE_H264_HIGH;
+ vdp_chroma_type = VDP_CHROMA_TYPE_420;
+ break;
+#ifdef VDP_DECODER_PROFILE_HEVC_MAIN
+ case AV_CODEC_ID_HEVC:
+ vdp_decoder_profile = VDP_DECODER_PROFILE_HEVC_MAIN;
+ vdp_chroma_type = VDP_CHROMA_TYPE_420;
+ break;
+#endif
+#ifdef VDP_DECODER_PROFILE_VP9_PROFILE_0
+ case AV_CODEC_ID_VP9:
+ vdp_decoder_profile = VDP_DECODER_PROFILE_VP9_PROFILE_0;
+ vdp_chroma_type = VDP_CHROMA_TYPE_420;
+ break;
+#endif
+ case AV_CODEC_ID_WMV3:
+ vdp_decoder_profile = VDP_DECODER_PROFILE_VC1_MAIN;
+ vdp_chroma_type = VDP_CHROMA_TYPE_420;
+ break;
+ case AV_CODEC_ID_VC1:
+ vdp_decoder_profile = VDP_DECODER_PROFILE_VC1_ADVANCED;
+ vdp_chroma_type = VDP_CHROMA_TYPE_420;
+ break;
+ case AV_CODEC_ID_MPEG4:
+ vdp_decoder_profile = VDP_DECODER_PROFILE_MPEG4_PART2_ASP;
+ vdp_chroma_type = VDP_CHROMA_TYPE_420;
+ break;
+ default:
+ vdp_decoder_profile = 0;
+ vdp_chroma_type = 0;
+ break;
+ }
+}
+
+bool CDecoder::ConfigVDPAU(AVCodecContext* avctx, int ref_frames)
+{
+ FiniVDPAUOutput();
+
+ VdpStatus vdp_st;
+ VdpDecoderProfile vdp_decoder_profile;
+
+ m_vdpauConfig.vidWidth = avctx->width;
+ m_vdpauConfig.vidHeight = avctx->height;
+ m_vdpauConfig.surfaceWidth = avctx->coded_width;
+ m_vdpauConfig.surfaceHeight = avctx->coded_height;
+
+ SetWidthHeight(avctx->width,avctx->height);
+
+ CLog::Log(LOGINFO, " (VDPAU) screenWidth:{} vidWidth:{} surfaceWidth:{}", m_vdpauConfig.outWidth,
+ m_vdpauConfig.vidWidth, m_vdpauConfig.surfaceWidth);
+ CLog::Log(LOGINFO, " (VDPAU) screenHeight:{} vidHeight:{} surfaceHeight:{}",
+ m_vdpauConfig.outHeight, m_vdpauConfig.vidHeight, m_vdpauConfig.surfaceHeight);
+
+ ReadFormatOf(avctx->codec_id, vdp_decoder_profile, m_vdpauConfig.vdpChromaType);
+
+ if (avctx->codec_id == AV_CODEC_ID_H264)
+ {
+ m_vdpauConfig.maxReferences = ref_frames;
+ if (m_vdpauConfig.maxReferences > 16) m_vdpauConfig.maxReferences = 16;
+ if (m_vdpauConfig.maxReferences < 5) m_vdpauConfig.maxReferences = 5;
+ }
+ else if (avctx->codec_id == AV_CODEC_ID_HEVC)
+ {
+ // The DPB works quite differently in hevc and there isn't a per-file max
+ // reference number, so we force the maximum number (source: upstream ffmpeg)
+ m_vdpauConfig.maxReferences = 16;
+ }
+ else if (avctx->codec_id == AV_CODEC_ID_VP9)
+ {
+ if (avctx->profile != FF_PROFILE_VP9_0)
+ return false;
+
+ m_vdpauConfig.maxReferences = 8;
+ }
+ else
+ m_vdpauConfig.maxReferences = 2;
+
+ vdp_st = m_vdpauConfig.context->GetProcs().vdp_decoder_create(m_vdpauConfig.context->GetDevice(),
+ vdp_decoder_profile,
+ m_vdpauConfig.surfaceWidth,
+ m_vdpauConfig.surfaceHeight,
+ m_vdpauConfig.maxReferences,
+ &m_vdpauConfig.vdpDecoder);
+ if (CheckStatus(vdp_st, __LINE__))
+ return false;
+
+ // initialize output
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ m_vdpauConfig.stats = &m_bufferStats;
+ m_vdpauConfig.vdpau = this;
+ m_bufferStats.Reset();
+ m_vdpauOutput.Start();
+ Message *reply;
+ if (m_vdpauOutput.m_controlPort.SendOutMessageSync(COutputControlProtocol::INIT,
+ &reply,
+ 2000,
+ &m_vdpauConfig,
+ sizeof(m_vdpauConfig)))
+ {
+ bool success = reply->signal == COutputControlProtocol::ACC ? true : false;
+ reply->Release();
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "VDPAU::{} - vdpau output returned error", __FUNCTION__);
+ m_vdpauOutput.Dispose();
+ return false;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "VDPAU::{} - failed to init output", __FUNCTION__);
+ m_vdpauOutput.Dispose();
+ return false;
+ }
+
+ m_inMsgEvent.Reset();
+ m_vdpauConfigured = true;
+ m_ErrorCount = 0;
+ m_vdpauConfig.resetCounter++;
+ return true;
+}
+
+int CDecoder::FFGetBuffer(AVCodecContext *avctx, AVFrame *pic, int flags)
+{
+ ICallbackHWAccel* cb = static_cast<ICallbackHWAccel*>(avctx->opaque);
+ CDecoder* vdp = static_cast<CDecoder*>(cb->GetHWAccel());
+
+ // while we are waiting to recover we can't do anything
+ std::unique_lock<CCriticalSection> lock(vdp->m_DecoderSection);
+
+ if(vdp->m_DisplayState != VDPAU_OPEN)
+ {
+ CLog::Log(LOGWARNING, "CVDPAU::FFGetBuffer - returning due to awaiting recovery");
+ return -1;
+ }
+
+ VdpVideoSurface surf = (VdpVideoSurface)(uintptr_t)pic->data[3];
+ surf = vdp->m_videoSurfaces.GetFree(surf != 0 ? surf : VDP_INVALID_HANDLE);
+
+ VdpStatus vdp_st = VDP_STATUS_ERROR;
+ if (surf == VDP_INVALID_HANDLE)
+ {
+ // create a new surface
+ VdpDecoderProfile profile;
+ ReadFormatOf(avctx->codec_id, profile, vdp->m_vdpauConfig.vdpChromaType);
+
+ vdp_st = vdp->m_vdpauConfig.context->GetProcs().vdp_video_surface_create(vdp->m_vdpauConfig.context->GetDevice(),
+ vdp->m_vdpauConfig.vdpChromaType,
+ avctx->coded_width,
+ avctx->coded_height,
+ &surf);
+ vdp->CheckStatus(vdp_st, __LINE__);
+ if (vdp_st != VDP_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, "CVDPAU::FFGetBuffer - No Video surface available could be created");
+ return -1;
+ }
+ vdp->m_videoSurfaces.AddSurface(surf);
+ }
+
+ pic->data[1] = pic->data[2] = NULL;
+ pic->data[0] = (uint8_t*)(uintptr_t)surf;
+ pic->data[3] = (uint8_t*)(uintptr_t)surf;
+ pic->linesize[0] = pic->linesize[1] = pic->linesize[2] = 0;
+ AVBufferRef *buffer = av_buffer_create(pic->data[3], 0, FFReleaseBuffer, vdp, 0);
+ if (!buffer)
+ {
+ CLog::Log(LOGERROR, "CVDPAU::{} - error creating buffer", __FUNCTION__);
+ return -1;
+ }
+ pic->buf[0] = buffer;
+
+ pic->reordered_opaque= avctx->reordered_opaque;
+ return 0;
+}
+
+void CDecoder::FFReleaseBuffer(void *opaque, uint8_t *data)
+{
+ CDecoder *vdp = static_cast<CDecoder*>(opaque);
+
+ VdpVideoSurface surf;
+
+ std::unique_lock<CCriticalSection> lock(vdp->m_DecoderSection);
+
+ surf = (VdpVideoSurface)(uintptr_t)data;
+
+ vdp->m_videoSurfaces.ClearReference(surf);
+}
+
+int CDecoder::Render(struct AVCodecContext *s, struct AVFrame *src,
+ const VdpPictureInfo *info, uint32_t buffers_used,
+ const VdpBitstreamBuffer *buffers)
+{
+ ICallbackHWAccel* ctx = static_cast<ICallbackHWAccel*>(s->opaque);
+ CDecoder* vdp = static_cast<CDecoder*>(ctx->GetHWAccel());
+
+ // while we are waiting to recover we can't do anything
+ std::unique_lock<CCriticalSection> lock(vdp->m_DecoderSection);
+
+ if(vdp->m_DisplayState != VDPAU_OPEN)
+ return -1;
+
+ if(src->linesize[0] || src->linesize[1] || src->linesize[2])
+ {
+ CLog::Log(LOGERROR, "CVDPAU::FFDrawSlice - invalid linesizes or offsets provided");
+ return -1;
+ }
+
+ VdpStatus vdp_st;
+ VdpVideoSurface surf = (VdpVideoSurface)(uintptr_t)src->data[3];
+
+ // ffmpeg vc-1 decoder does not flush, make sure the data buffer is still valid
+ if (!vdp->m_videoSurfaces.IsValid(surf))
+ {
+ CLog::Log(LOGWARNING, "CVDPAU::FFDrawSlice - ignoring invalid buffer");
+ return -1;
+ }
+
+ uint32_t max_refs = 0;
+ if(s->codec_id == AV_CODEC_ID_H264)
+ max_refs = s->refs;
+
+ if(vdp->m_vdpauConfig.vdpDecoder == VDP_INVALID_HANDLE
+ || vdp->m_vdpauConfigured == false
+ || vdp->m_vdpauConfig.maxReferences < max_refs)
+ {
+ if(!vdp->ConfigVDPAU(s, max_refs))
+ return -1;
+ }
+
+ uint64_t startTime = CurrentHostCounter();
+ uint16_t decoded, processed, rend;
+ vdp->m_bufferStats.Get(decoded, processed, rend);
+ vdp_st = vdp->m_vdpauConfig.context->GetProcs().vdp_decoder_render(vdp->m_vdpauConfig.vdpDecoder,
+ surf, info, buffers_used, buffers);
+ if (vdp->CheckStatus(vdp_st, __LINE__))
+ return -1;
+
+ uint64_t diff = CurrentHostCounter() - startTime;
+ if (diff*1000/CurrentHostFrequency() > 30)
+ CLog::Log(
+ LOGDEBUG, LOGVIDEO,
+ "CVDPAU::DrawSlice - VdpDecoderRender long decoding: {} ms, dec: {}, proc: {}, rend: {}",
+ (int)((diff * 1000) / CurrentHostFrequency()), decoded, processed, rend);
+
+ return 0;
+}
+
+void CDecoder::SetCodecControl(int flags)
+{
+ m_codecControl = flags & (DVD_CODEC_CTRL_DRAIN | DVD_CODEC_CTRL_HURRY);
+ if (m_codecControl & DVD_CODEC_CTRL_DRAIN)
+ m_bufferStats.SetDraining(true);
+ else
+ m_bufferStats.SetDraining(false);
+}
+
+CDVDVideoCodec::VCReturn CDecoder::Decode(AVCodecContext *avctx, AVFrame *pFrame)
+{
+ CDVDVideoCodec::VCReturn result = Check(avctx);
+ if (result != CDVDVideoCodec::VC_NONE)
+ return result;
+
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+
+ if (!m_vdpauConfigured)
+ return CDVDVideoCodec::VC_ERROR;
+
+ if(pFrame)
+ { // we have a new frame from decoder
+
+ VdpVideoSurface surf = (VdpVideoSurface)(uintptr_t)pFrame->data[3];
+ // ffmpeg vc-1 decoder does not flush, make sure the data buffer is still valid
+ if (!m_videoSurfaces.IsValid(surf))
+ {
+ CLog::Log(LOGWARNING, "CVDPAU::Decode - ignoring invalid buffer");
+ return CDVDVideoCodec::VC_BUFFER;
+ }
+ m_videoSurfaces.MarkRender(surf);
+
+ // send frame to output for processing
+ CVdpauDecodedPicture *pic = new CVdpauDecodedPicture();
+ static_cast<ICallbackHWAccel*>(avctx->opaque)->GetPictureCommon(&(pic->DVDPic));
+ m_codecControl = pic->DVDPic.iFlags & (DVD_CODEC_CTRL_HURRY | DVD_CODEC_CTRL_NO_POSTPROC);
+ pic->videoSurface = surf;
+ pic->DVDPic.color_space = avctx->colorspace;
+ m_bufferStats.IncDecoded();
+ CPayloadWrap<CVdpauDecodedPicture> *payload = new CPayloadWrap<CVdpauDecodedPicture>(pic);
+ m_vdpauOutput.m_dataPort.SendOutMessage(COutputDataProtocol::NEWFRAME, payload);
+ }
+
+ uint16_t decoded, processed, render;
+ Message *msg;
+ while (m_vdpauOutput.m_controlPort.ReceiveInMessage(&msg))
+ {
+ if (msg->signal == COutputControlProtocol::ERROR)
+ {
+ m_DisplayState = VDPAU_ERROR;
+ msg->Release();
+ return CDVDVideoCodec::VC_BUFFER;
+ }
+ msg->Release();
+ }
+
+ bool drain = (m_codecControl & DVD_CODEC_CTRL_DRAIN);
+
+ m_bufferStats.Get(decoded, processed, render);
+ // if all pics are drained, break the loop by setting VC_EOF
+ if (drain && decoded <= 0 && processed <= 0 && render <= 0)
+ return CDVDVideoCodec::VC_EOF;
+
+ uint64_t startTime = CurrentHostCounter();
+ while (true)
+ {
+ // first fill the buffers to keep vdpau busy
+ // mixer will run with decoded >= 2. output is limited by number of output surfaces
+ // In case mixer is bypassed we limit by looking at processed
+ if (!drain && decoded < 3 && processed < 3)
+ {
+ return CDVDVideoCodec::VC_BUFFER;
+ }
+ else if (m_vdpauOutput.m_dataPort.ReceiveInMessage(&msg))
+ {
+ if (msg->signal == COutputDataProtocol::PICTURE)
+ {
+ if (m_presentPicture)
+ {
+ m_presentPicture->Release();
+ m_presentPicture = nullptr;
+ }
+
+ m_presentPicture = *(CVdpauRenderPicture**)msg->data;
+ m_bufferStats.DecRender();
+ msg->Release();
+ uint64_t diff = CurrentHostCounter() - startTime;
+ m_bufferStats.SetParams(diff, m_codecControl);
+ return CDVDVideoCodec::VC_PICTURE;
+ }
+ msg->Release();
+ }
+ else if (m_vdpauOutput.m_controlPort.ReceiveInMessage(&msg))
+ {
+ if (msg->signal == COutputControlProtocol::STATS)
+ {
+ msg->Release();
+ m_bufferStats.Get(decoded, processed, render);
+ if (!drain && decoded < 3 && processed < 3)
+ {
+ return CDVDVideoCodec::VC_BUFFER;
+ }
+ }
+ else
+ {
+ m_DisplayState = VDPAU_ERROR;
+ msg->Release();
+ return CDVDVideoCodec::VC_ERROR;
+ }
+ }
+
+ if (!m_inMsgEvent.Wait(2000ms))
+ break;
+ }
+
+ CLog::Log(LOGERROR, "VDPAU::{} - timed out waiting for output message", __FUNCTION__);
+ m_DisplayState = VDPAU_ERROR;
+
+ return CDVDVideoCodec::VC_ERROR;
+}
+
+bool CDecoder::GetPicture(AVCodecContext* avctx, VideoPicture* picture)
+{
+ if (picture->videoBuffer)
+ {
+ picture->videoBuffer->Release();
+ picture->videoBuffer = nullptr;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+
+ if (m_DisplayState != VDPAU_OPEN)
+ return false;
+
+ picture->SetParams(m_presentPicture->DVDPic);
+ picture->videoBuffer = m_presentPicture;
+ m_presentPicture = nullptr;
+
+ return true;
+}
+
+void CDecoder::Reset()
+{
+ std::unique_lock<CCriticalSection> lock(m_DecoderSection);
+
+ if (m_presentPicture)
+ {
+ m_presentPicture->Release();
+ m_presentPicture = nullptr;
+ }
+
+ if (!m_vdpauConfigured)
+ return;
+
+ Message *reply;
+ if (m_vdpauOutput.m_controlPort.SendOutMessageSync(COutputControlProtocol::FLUSH,
+ &reply,
+ 2000))
+ {
+ bool success = reply->signal == COutputControlProtocol::ACC ? true : false;
+ reply->Release();
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "VDPAU::{} - flush returned error", __FUNCTION__);
+ m_DisplayState = VDPAU_ERROR;
+ }
+ else
+ m_bufferStats.Reset();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "VDPAU::{} - flush timed out", __FUNCTION__);
+ m_DisplayState = VDPAU_ERROR;
+ }
+}
+
+bool CDecoder::CanSkipDeint()
+{
+ return m_bufferStats.CanSkipDeint();
+}
+
+void CDecoder::ReturnRenderPicture(CVdpauRenderPicture *renderPic)
+{
+ m_vdpauOutput.m_dataPort.SendOutMessage(COutputDataProtocol::RETURNPIC, &renderPic, sizeof(renderPic));
+}
+
+bool CDecoder::CheckStatus(VdpStatus vdp_st, int line)
+{
+ if (vdp_st != VDP_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, " (VDPAU) Error: {}({}) at {}:{}",
+ m_vdpauConfig.context->GetProcs().vdp_get_error_string(vdp_st), vdp_st, __FILE__,
+ line);
+
+ m_ErrorCount++;
+
+ if(m_DisplayState == VDPAU_OPEN)
+ {
+ if (vdp_st == VDP_STATUS_DISPLAY_PREEMPTED)
+ {
+ m_DisplayEvent.Reset();
+ m_DisplayState = VDPAU_LOST;
+ }
+ else if (m_ErrorCount > 2)
+ m_DisplayState = VDPAU_ERROR;
+ }
+
+ return true;
+ }
+ m_ErrorCount = 0;
+ return false;
+}
+
+IHardwareDecoder* CDecoder::Create(CDVDStreamInfo &hint, CProcessInfo &processInfo, AVPixelFormat fmt)
+ {
+ if (CDecoder::IsVDPAUFormat(fmt) && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEVDPAU))
+ return new VDPAU::CDecoder(processInfo);
+
+ return nullptr;
+ }
+
+void CDecoder::Register()
+{
+ CVDPAUContext *context;
+ if (!CVDPAUContext::EnsureContext(&context))
+ return;
+
+ context->Release();
+
+ m_capGeneral = true;
+
+ CDVDFactoryCodec::RegisterHWAccel("vdpau", CDecoder::Create);
+
+ std::string gpuvendor = CServiceBroker::GetRenderSystem()->GetRenderVendor();
+ std::transform(gpuvendor.begin(), gpuvendor.end(), gpuvendor.begin(), ::tolower);
+ bool isNvidia = (gpuvendor.compare(0, 6, "nvidia") == 0);
+
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return;
+
+ auto setting = settings->GetSetting(CSettings::SETTING_VIDEOPLAYER_USEVDPAU);
+ if (!setting)
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", CSettings::SETTING_VIDEOPLAYER_USEVDPAU);
+ else
+ setting->SetVisible(true);
+
+ if (!isNvidia)
+ {
+ constexpr std::array<const char*, 4> vdpauSettings = {
+ CSettings::SETTING_VIDEOPLAYER_USEVDPAUMPEG4, CSettings::SETTING_VIDEOPLAYER_USEVDPAUVC1,
+ CSettings::SETTING_VIDEOPLAYER_USEVDPAUMPEG2, CSettings::SETTING_VIDEOPLAYER_USEVDPAUMIXER};
+
+ for (const auto& vdpauSetting : vdpauSettings)
+ {
+ setting = settings->GetSetting(vdpauSetting);
+ if (!setting)
+ {
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", vdpauSetting);
+ continue;
+ }
+
+ setting->SetVisible(true);
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// BufferPool
+//-----------------------------------------------------------------------------
+
+class VDPAU::CVdpauBufferPool : public IVideoBufferPool
+{
+public:
+ explicit CVdpauBufferPool(CDecoder &decoder);
+ ~CVdpauBufferPool() override;
+ CVideoBuffer* Get() override;
+ void Return(int id) override;
+ CVdpauRenderPicture* GetVdpau();
+ bool HasFree();
+ void QueueReturnPicture(CVdpauRenderPicture *pic);
+ CVdpauRenderPicture* ProcessSyncPicture();
+ void InvalidateUsed();
+
+ unsigned short numOutputSurfaces;
+ std::vector<VdpOutputSurface> outputSurfaces;
+ std::queue<CVdpauProcessedPicture> processedPics;
+ std::deque<CVdpauProcessedPicture> processedPicsAway;
+
+ int procPicId = 0;
+
+protected:
+ std::vector<CVdpauRenderPicture*> allRenderPics;
+ std::deque<int> usedRenderPics;
+ std::deque<int> freeRenderPics;
+ std::deque<int> syncRenderPics;
+
+ CDecoder &m_vdpau;
+};
+
+CVdpauBufferPool::CVdpauBufferPool(CDecoder &decoder)
+ : m_vdpau(decoder)
+{
+ CVdpauRenderPicture *pic;
+ for (unsigned int i = 0; i < NUM_RENDER_PICS; i++)
+ {
+ pic = new CVdpauRenderPicture(i);
+ allRenderPics.push_back(pic);
+ freeRenderPics.push_back(i);
+ }
+}
+
+CVdpauBufferPool::~CVdpauBufferPool()
+{
+ CVdpauRenderPicture *pic;
+ for (unsigned int i = 0; i < NUM_RENDER_PICS; i++)
+ {
+ pic = allRenderPics[i];
+ delete pic;
+ }
+ allRenderPics.clear();
+}
+
+CVideoBuffer* CVdpauBufferPool::Get()
+{
+ if (freeRenderPics.empty())
+ return nullptr;
+
+ int idx = freeRenderPics.front();
+ freeRenderPics.pop_front();
+ usedRenderPics.push_back(idx);
+
+ CVideoBuffer *retPic = allRenderPics[idx];
+ retPic->Acquire(GetPtr());
+
+ m_vdpau.Acquire();
+
+ return retPic;
+}
+
+void CVdpauBufferPool::Return(int id)
+{
+ CVdpauRenderPicture *pic = allRenderPics[id];
+
+ m_vdpau.ReturnRenderPicture(pic);
+ m_vdpau.ReleasePicReference();
+}
+
+CVdpauRenderPicture* CVdpauBufferPool::GetVdpau()
+{
+ return dynamic_cast<CVdpauRenderPicture*>(Get());
+}
+
+bool CVdpauBufferPool::HasFree()
+{
+ return !freeRenderPics.empty();
+}
+
+void CVdpauBufferPool::QueueReturnPicture(CVdpauRenderPicture *pic)
+{
+ std::deque<int>::iterator it;
+ for (it = usedRenderPics.begin(); it != usedRenderPics.end(); ++it)
+ {
+ if (allRenderPics[*it] == pic)
+ {
+ break;
+ }
+ }
+
+ if (it == usedRenderPics.end())
+ {
+ CLog::Log(LOGWARNING, "COutput::QueueReturnPicture - pic not found");
+ return;
+ }
+
+ // check if already queued
+ std::deque<int>::iterator it2 = find(syncRenderPics.begin(),
+ syncRenderPics.end(),
+ *it);
+ if (it2 == syncRenderPics.end())
+ {
+ syncRenderPics.push_back(*it);
+ }
+}
+
+CVdpauRenderPicture* CVdpauBufferPool::ProcessSyncPicture()
+{
+ CVdpauRenderPicture *retPic = nullptr;
+
+ std::deque<int>::iterator it;
+ for (it = syncRenderPics.begin(); it != syncRenderPics.end(); )
+ {
+ retPic = allRenderPics[*it];
+
+ freeRenderPics.push_back(*it);
+
+ std::deque<int>::iterator it2 = find(usedRenderPics.begin(),
+ usedRenderPics.end(),
+ *it);
+ if (it2 == usedRenderPics.end())
+ {
+ CLog::Log(LOGERROR, "COutput::ProcessSyncPicture - pic not found in queue");
+ }
+ else
+ {
+ usedRenderPics.erase(it2);
+ }
+ it = syncRenderPics.erase(it);
+
+ break;
+ }
+ return retPic;
+}
+
+void CVdpauBufferPool::InvalidateUsed()
+{
+ std::deque<int>::iterator it;
+ for (it = usedRenderPics.begin(); it != usedRenderPics.end(); ++it)
+ {
+ allRenderPics[*it]->procPic.outputSurface = VDP_INVALID_HANDLE;
+ allRenderPics[*it]->procPic.videoSurface = VDP_INVALID_HANDLE;
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Mixer
+//-----------------------------------------------------------------------------
+CMixer::CMixer(CEvent *inMsgEvent) :
+ CThread("Vdpau Mixer"),
+ m_controlPort("ControlPort", inMsgEvent, &m_outMsgEvent),
+ m_dataPort("DataPort", inMsgEvent, &m_outMsgEvent)
+{
+ m_inMsgEvent = inMsgEvent;
+}
+
+CMixer::~CMixer()
+{
+ Dispose();
+}
+
+void CMixer::Start()
+{
+ Create();
+}
+
+void CMixer::Dispose()
+{
+ m_bStop = true;
+ m_outMsgEvent.Set();
+ StopThread();
+
+ m_controlPort.Purge();
+ m_dataPort.Purge();
+}
+
+bool CMixer::IsActive()
+{
+ return IsRunning();
+}
+
+void CMixer::OnStartup()
+{
+ CLog::Log(LOGINFO, "CMixer::OnStartup: Output Thread created");
+}
+
+void CMixer::OnExit()
+{
+ CLog::Log(LOGINFO, "CMixer::OnExit: Output Thread terminated");
+}
+
+enum MIXER_STATES
+{
+ M_TOP = 0, // 0
+ M_TOP_ERROR, // 1
+ M_TOP_UNCONFIGURED, // 2
+ M_TOP_CONFIGURED, // 3
+ M_TOP_CONFIGURED_WAIT1, // 4
+ M_TOP_CONFIGURED_STEP1, // 5
+ M_TOP_CONFIGURED_WAIT2, // 6
+ M_TOP_CONFIGURED_STEP2, // 7
+};
+
+int MIXER_parentStates[] = {
+ -1,
+ 0, //TOP_ERROR
+ 0, //TOP_UNCONFIGURED
+ 0, //TOP_CONFIGURED
+ 3, //TOP_CONFIGURED_WAIT1
+ 3, //TOP_CONFIGURED_STEP1
+ 3, //TOP_CONFIGURED_WAIT2
+ 3, //TOP_CONFIGURED_STEP2
+};
+
+void CMixer::StateMachine(int signal, Protocol *port, Message *msg)
+{
+ for (int state = m_state; ; state = MIXER_parentStates[state])
+ {
+ switch (state)
+ {
+ case M_TOP: // TOP
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case CMixerControlProtocol::FLUSH:
+ Flush();
+ msg->Reply(CMixerControlProtocol::ACC);
+ return;
+ default:
+ break;
+ }
+ }
+ {
+ std::string portName = port == NULL ? "timer" : port->portName;
+ CLog::Log(LOGWARNING, "CMixer::{} - signal: {} form port: {} not handled for state: {}",
+ __FUNCTION__, signal, portName, m_state);
+ }
+ return;
+
+ case M_TOP_ERROR: // TOP
+ break;
+
+ case M_TOP_UNCONFIGURED:
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case CMixerControlProtocol::INIT:
+ CVdpauConfig *data;
+ data = (CVdpauConfig*)msg->data;
+ if (data)
+ {
+ m_config = *data;
+ }
+ Init();
+ if (!m_vdpError)
+ {
+ m_state = M_TOP_CONFIGURED_WAIT1;
+ msg->Reply(CMixerControlProtocol::ACC);
+ }
+ else
+ {
+ msg->Reply(CMixerControlProtocol::ERROR);
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case M_TOP_CONFIGURED:
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case CMixerControlProtocol::FLUSH:
+ Flush();
+ msg->Reply(CMixerControlProtocol::ACC);
+ m_state = M_TOP_CONFIGURED_WAIT1;
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case CMixerDataProtocol::FRAME:
+ CPayloadWrap<CVdpauDecodedPicture> *payload;
+ payload = dynamic_cast<CPayloadWrap<CVdpauDecodedPicture>*>(msg->payloadObj.get());
+ if (payload)
+ {
+ m_decodedPics.push(*(payload->GetPlayload()));
+ }
+ m_extTimeout = 0;
+ return;
+ case CMixerDataProtocol::BUFFER:
+ VdpOutputSurface *surf;
+ surf = (VdpOutputSurface*)msg->data;
+ if (surf)
+ {
+ m_outputSurfaces.push(*surf);
+ }
+ m_extTimeout = 0;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case M_TOP_CONFIGURED_WAIT1:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case CMixerControlProtocol::TIMEOUT:
+ if (!m_decodedPics.empty() && !m_outputSurfaces.empty())
+ {
+ m_state = M_TOP_CONFIGURED_STEP1;
+ m_bStateMachineSelfTrigger = true;
+ }
+ else if (!m_outputSurfaces.empty() &&
+ m_config.stats->IsDraining() &&
+ m_mixerInput.size() >= 1)
+ {
+ CVdpauDecodedPicture pic;
+ pic.DVDPic.SetParams(m_mixerInput[0].DVDPic);
+ pic.videoSurface = VDP_INVALID_HANDLE;
+ m_decodedPics.push(pic);
+ m_state = M_TOP_CONFIGURED_STEP1;
+ m_bStateMachineSelfTrigger = true;
+ }
+ else
+ {
+ m_extTimeout = 100;
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case M_TOP_CONFIGURED_STEP1:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case CMixerControlProtocol::TIMEOUT:
+ m_mixerInput.push_front(m_decodedPics.front());
+ m_decodedPics.pop();
+ if (m_mixerInput.size() < 2)
+ {
+ m_state = M_TOP_CONFIGURED_WAIT1;
+ m_extTimeout = 0;
+ return;
+ }
+ InitCycle();
+ ProcessPicture();
+ if (m_vdpError)
+ {
+ m_state = M_TOP_CONFIGURED_WAIT1;
+ m_extTimeout = 1000;
+ return;
+ }
+ if (!m_processPicture.isYuv)
+ m_outputSurfaces.pop();
+ m_config.stats->IncProcessed();
+ m_config.stats->DecDecoded();
+ m_dataPort.SendInMessage(CMixerDataProtocol::PICTURE,&m_processPicture,sizeof(m_processPicture));
+ if (m_mixersteps > 1)
+ {
+ m_state = M_TOP_CONFIGURED_WAIT2;
+ m_extTimeout = 0;
+ }
+ else
+ {
+ FiniCycle();
+ m_state = M_TOP_CONFIGURED_WAIT1;
+ m_extTimeout = 0;
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case M_TOP_CONFIGURED_WAIT2:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case CMixerControlProtocol::TIMEOUT:
+ if (!m_outputSurfaces.empty())
+ {
+ m_state = M_TOP_CONFIGURED_STEP2;
+ m_bStateMachineSelfTrigger = true;
+ }
+ else
+ {
+ m_extTimeout = 100;
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case M_TOP_CONFIGURED_STEP2:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case CMixerControlProtocol::TIMEOUT:
+ m_processPicture.outputSurface = m_outputSurfaces.front();
+ m_mixerstep = 1;
+ ProcessPicture();
+ if (m_vdpError)
+ {
+ m_state = M_TOP_CONFIGURED_WAIT1;
+ m_extTimeout = 1000;
+ return;
+ }
+ if (!m_processPicture.isYuv)
+ m_outputSurfaces.pop();
+ m_config.stats->IncProcessed();
+ m_dataPort.SendInMessage(CMixerDataProtocol::PICTURE,&m_processPicture,sizeof(m_processPicture));
+ FiniCycle();
+ m_state = M_TOP_CONFIGURED_WAIT1;
+ m_extTimeout = 0;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ default: // we are in no state, should not happen
+ CLog::Log(LOGERROR, "CMixer::{} - no valid state: {}", __FUNCTION__, m_state);
+ return;
+ }
+ } // for
+}
+
+void CMixer::Process()
+{
+ Message *msg = NULL;
+ Protocol *port = NULL;
+ bool gotMsg;
+
+ m_state = M_TOP_UNCONFIGURED;
+ m_extTimeout = 1000;
+ m_bStateMachineSelfTrigger = false;
+
+ while (!m_bStop)
+ {
+ gotMsg = false;
+
+ if (m_bStateMachineSelfTrigger)
+ {
+ m_bStateMachineSelfTrigger = false;
+ // self trigger state machine
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ continue;
+ }
+ // check control port
+ else if (m_controlPort.ReceiveOutMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_controlPort;
+ }
+ // check data port
+ else if (m_dataPort.ReceiveOutMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_dataPort;
+ }
+
+ if (gotMsg)
+ {
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ continue;
+ }
+
+ // wait for message
+ else if (m_outMsgEvent.Wait(std::chrono::milliseconds(m_extTimeout)))
+ {
+ continue;
+ }
+ // time out
+ else
+ {
+ msg = m_controlPort.GetMessage();
+ msg->signal = CMixerControlProtocol::TIMEOUT;
+ port = 0;
+ // signal timeout to state machine
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ }
+ }
+ Uninit();
+}
+
+void CMixer::CreateVdpauMixer()
+{
+ CLog::Log(LOGINFO, " (VDPAU) Creating the video mixer");
+
+ InitCSCMatrix(m_config.vidWidth);
+
+ VdpVideoMixerParameter parameters[] = {
+ VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH,
+ VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT,
+ VDP_VIDEO_MIXER_PARAMETER_CHROMA_TYPE};
+
+ void const * parameter_values[] = {
+ &m_config.surfaceWidth,
+ &m_config.surfaceHeight,
+ &m_config.vdpChromaType};
+
+ VdpStatus vdp_st = m_config.context->GetProcs().vdp_video_mixer_create(m_config.context->GetDevice(),
+ m_config.context->GetFeatureCount(),
+ m_config.context->GetFeatures(),
+ ARSIZE(parameters),
+ parameters,
+ parameter_values,
+ &m_videoMixer);
+ CheckStatus(vdp_st, __LINE__);
+
+}
+
+void CMixer::InitCSCMatrix(int Width)
+{
+ m_Procamp.struct_version = VDP_PROCAMP_VERSION;
+ m_Procamp.brightness = 0.0;
+ m_Procamp.contrast = 1.0;
+ m_Procamp.saturation = 1.0;
+ m_Procamp.hue = 0;
+}
+
+void CMixer::CheckFeatures()
+{
+ if (m_Upscale != m_config.upscale)
+ {
+ SetHWUpscaling();
+ m_Upscale = m_config.upscale;
+ }
+ if (m_Brightness != m_config.processInfo->GetVideoSettings().m_Brightness ||
+ m_Contrast != m_config.processInfo->GetVideoSettings().m_Contrast ||
+ m_ColorMatrix != m_mixerInput[1].DVDPic.color_space)
+ {
+ SetColor();
+ m_Brightness = m_config.processInfo->GetVideoSettings().m_Brightness;
+ m_Contrast = m_config.processInfo->GetVideoSettings().m_Contrast;
+ m_ColorMatrix = m_mixerInput[1].DVDPic.color_space;
+ }
+ if (m_NoiseReduction != m_config.processInfo->GetVideoSettings().m_NoiseReduction)
+ {
+ m_NoiseReduction = m_config.processInfo->GetVideoSettings().m_NoiseReduction;
+ SetNoiseReduction();
+ }
+ if (m_Sharpness != m_config.processInfo->GetVideoSettings().m_Sharpness)
+ {
+ m_Sharpness = m_config.processInfo->GetVideoSettings().m_Sharpness;
+ SetSharpness();
+ }
+ if (m_Deint != m_config.processInfo->GetVideoSettings().m_InterlaceMethod)
+ {
+ m_Deint = m_config.processInfo->GetVideoSettings().m_InterlaceMethod;
+ SetDeinterlacing();
+ }
+}
+
+void CMixer::SetPostProcFeatures(bool postProcEnabled)
+{
+ if (m_PostProc != postProcEnabled)
+ {
+ if (postProcEnabled)
+ {
+ SetNoiseReduction();
+ SetSharpness();
+ SetDeinterlacing();
+ SetHWUpscaling();
+ }
+ else
+ PostProcOff();
+ m_PostProc = postProcEnabled;
+ }
+}
+
+void CMixer::PostProcOff()
+{
+ VdpStatus vdp_st;
+
+ if (m_videoMixer == VDP_INVALID_HANDLE)
+ return;
+
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL,
+ VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL_SPATIAL,
+ VDP_VIDEO_MIXER_FEATURE_INVERSE_TELECINE};
+
+ VdpBool enabled[]={0,0,0};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+
+ if(m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_NOISE_REDUCTION))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_NOISE_REDUCTION};
+
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ }
+
+ if(m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_SHARPNESS))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_SHARPNESS};
+
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ }
+
+ DisableHQScaling();
+}
+
+bool CMixer::GenerateStudioCSCMatrix(VdpColorStandard colorStandard, VdpCSCMatrix &studioCSCMatrix)
+{
+ // instead use studioCSCKCoeffs601[3], studioCSCKCoeffs709[3] to generate float[3][4] matrix (float studioCSC[3][4])
+ // m00 = mRY = red: luma factor (contrast factor) (1.0)
+ // m10 = mGY = green: luma factor (contrast factor) (1.0)
+ // m20 = mBY = blue: luma factor (contrast factor) (1.0)
+ //
+ // m01 = mRB = red: blue color diff coeff (0.0)
+ // m11 = mGB = green: blue color diff coeff (-2Kb(1-Kb)/(Kg))
+ // m21 = mBB = blue: blue color diff coeff ((1-Kb)/0.5)
+ //
+ // m02 = mRR = red: red color diff coeff ((1-Kr)/0.5)
+ // m12 = mGR = green: red color diff coeff (-2Kr(1-Kr)/(Kg))
+ // m22 = mBR = blue: red color diff coeff (0.0)
+ //
+ // m03 = mRC = red: colour zero offset (brightness factor) (-(1-Kr)/0.5 * (128/255))
+ // m13 = mGC = green: colour zero offset (brightness factor) ((256/255) * (Kb(1-Kb) + Kr(1-Kr)) / Kg)
+ // m23 = mBC = blue: colour zero offset (brightness factor) (-(1-Kb)/0.5 * (128/255))
+
+ // columns
+ int Y = 0;
+ int Cb = 1;
+ int Cr = 2;
+ int C = 3;
+ // rows
+ int R = 0;
+ int G = 1;
+ int B = 2;
+ // colour standard coefficients for red, geen, blue
+ float Kr, Kg, Kb;
+ // colour diff zero position (use standard 8-bit coding precision)
+ float CDZ = 128; //256*0.5
+ // range excursion (use standard 8-bit coding precision)
+ float EXC = 255; //256-1
+
+ if (colorStandard == VDP_COLOR_STANDARD_ITUR_BT_601)
+ {
+ Kr = studioCSCKCoeffs601[0];
+ Kg = studioCSCKCoeffs601[1];
+ Kb = studioCSCKCoeffs601[2];
+ }
+ else // assume VDP_COLOR_STANDARD_ITUR_BT_709
+ {
+ Kr = studioCSCKCoeffs709[0];
+ Kg = studioCSCKCoeffs709[1];
+ Kb = studioCSCKCoeffs709[2];
+ }
+ // we keep luma unscaled to retain the levels present in source so that 16-235 luma is converted to RGB 16-235
+ studioCSCMatrix[R][Y] = 1.0f;
+ studioCSCMatrix[G][Y] = 1.0f;
+ studioCSCMatrix[B][Y] = 1.0f;
+
+ studioCSCMatrix[R][Cb] = 0.0f;
+ studioCSCMatrix[G][Cb] = -2 * Kb * (1 - Kb) / Kg;
+ studioCSCMatrix[B][Cb] = (1 - Kb) / 0.5f;
+
+ studioCSCMatrix[R][Cr] = (1 - Kr) / 0.5f;
+ studioCSCMatrix[G][Cr] = -2 * Kr * (1 - Kr) / Kg;
+ studioCSCMatrix[B][Cr] = 0.0f;
+
+ studioCSCMatrix[R][C] = -1 * studioCSCMatrix[R][Cr] * CDZ / EXC;
+ studioCSCMatrix[G][C] = -1 * (studioCSCMatrix[G][Cb] + studioCSCMatrix[G][Cr]) * CDZ / EXC;
+ studioCSCMatrix[B][C] = -1 * studioCSCMatrix[B][Cb] * CDZ / EXC;
+
+ return true;
+}
+
+void CMixer::SetColor()
+{
+ VdpStatus vdp_st;
+
+ if (m_Brightness != m_config.processInfo->GetVideoSettings().m_Brightness)
+ m_Procamp.brightness = (float)((m_config.processInfo->GetVideoSettings().m_Brightness)-50) / 100;
+ if (m_Contrast != m_config.processInfo->GetVideoSettings().m_Contrast)
+ m_Procamp.contrast = (float)((m_config.processInfo->GetVideoSettings().m_Contrast)+50) / 100;
+
+ VdpColorStandard colorStandard;
+ switch(m_mixerInput[1].DVDPic.color_space)
+ {
+ case AVCOL_SPC_BT709:
+ colorStandard = VDP_COLOR_STANDARD_ITUR_BT_709;
+ break;
+ case AVCOL_SPC_BT470BG:
+ case AVCOL_SPC_SMPTE170M:
+ colorStandard = VDP_COLOR_STANDARD_ITUR_BT_601;
+ break;
+ case AVCOL_SPC_SMPTE240M:
+ colorStandard = VDP_COLOR_STANDARD_SMPTE_240M;
+ break;
+ case AVCOL_SPC_FCC:
+ case AVCOL_SPC_UNSPECIFIED:
+ case AVCOL_SPC_RGB:
+ default:
+ if(m_config.surfaceWidth > 1000)
+ colorStandard = VDP_COLOR_STANDARD_ITUR_BT_709;
+ else
+ colorStandard = VDP_COLOR_STANDARD_ITUR_BT_601;
+ }
+
+ VdpVideoMixerAttribute attributes[] = { VDP_VIDEO_MIXER_ATTRIBUTE_CSC_MATRIX };
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE))
+ {
+ float studioCSC[3][4];
+ GenerateStudioCSCMatrix(colorStandard, studioCSC);
+ void const * pm_CSCMatrix[] = { &studioCSC };
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_attribute_values(m_videoMixer, ARSIZE(attributes), attributes, pm_CSCMatrix);
+ }
+ else
+ {
+ vdp_st = m_config.context->GetProcs().vdp_generate_csc_matrix(&m_Procamp, colorStandard, &m_CSCMatrix);
+ if(vdp_st != VDP_STATUS_ERROR)
+ {
+ void const * pm_CSCMatrix[] = { &m_CSCMatrix };
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_attribute_values(m_videoMixer, ARSIZE(attributes), attributes, pm_CSCMatrix);
+ }
+ }
+
+ CheckStatus(vdp_st, __LINE__);
+}
+
+void CMixer::SetNoiseReduction()
+{
+ if(!m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_NOISE_REDUCTION))
+ return;
+
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_NOISE_REDUCTION };
+ VdpVideoMixerAttribute attributes[] = { VDP_VIDEO_MIXER_ATTRIBUTE_NOISE_REDUCTION_LEVEL };
+ VdpStatus vdp_st;
+
+ if (!m_config.processInfo->GetVideoSettings().m_NoiseReduction)
+ {
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ return;
+ }
+ VdpBool enabled[]={1};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ float noiseReduction = m_config.processInfo->GetVideoSettings().m_NoiseReduction;
+ void* nr[] = { &noiseReduction };
+ CLog::Log(LOGINFO, "Setting Noise Reduction to {:f}",
+ m_config.processInfo->GetVideoSettings().m_NoiseReduction);
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_attribute_values(m_videoMixer, ARSIZE(attributes), attributes, nr);
+ CheckStatus(vdp_st, __LINE__);
+}
+
+void CMixer::SetSharpness()
+{
+ if(!m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_SHARPNESS))
+ return;
+
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_SHARPNESS };
+ VdpVideoMixerAttribute attributes[] = { VDP_VIDEO_MIXER_ATTRIBUTE_SHARPNESS_LEVEL };
+ VdpStatus vdp_st;
+
+ if (!m_config.processInfo->GetVideoSettings().m_Sharpness)
+ {
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ return;
+ }
+ VdpBool enabled[]={1};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ float sharpness = m_config.processInfo->GetVideoSettings().m_Sharpness;
+ void* sh[] = { &sharpness };
+ CLog::Log(LOGINFO, "Setting Sharpness to {:f}",
+ m_config.processInfo->GetVideoSettings().m_Sharpness);
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_attribute_values(m_videoMixer, ARSIZE(attributes), attributes, sh);
+ CheckStatus(vdp_st, __LINE__);
+}
+
+void CMixer::SetDeinterlacing()
+{
+ VdpStatus vdp_st;
+
+ if (m_videoMixer == VDP_INVALID_HANDLE)
+ return;
+
+ EINTERLACEMETHOD method = m_config.processInfo->GetVideoSettings().m_InterlaceMethod;
+
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL,
+ VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL_SPATIAL,
+ VDP_VIDEO_MIXER_FEATURE_INVERSE_TELECINE };
+
+ if (method == VS_INTERLACEMETHOD_NONE)
+ {
+ VdpBool enabled[] = {0,0,0};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ }
+ else
+ {
+ // fall back path if called with non supported method
+ if (!m_config.processInfo->Supports(method))
+ {
+ method = VS_INTERLACEMETHOD_VDPAU_TEMPORAL;
+ m_Deint = VS_INTERLACEMETHOD_VDPAU_TEMPORAL;
+ }
+
+ if (method == VS_INTERLACEMETHOD_VDPAU_TEMPORAL
+ || method == VS_INTERLACEMETHOD_VDPAU_TEMPORAL_HALF)
+ {
+ VdpBool enabled[] = {1,0,0};
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoVDPAUtelecine)
+ enabled[2] = 1;
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ }
+ else if (method == VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL
+ || method == VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL_HALF)
+ {
+ VdpBool enabled[] = {1,1,0};
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoVDPAUtelecine)
+ enabled[2] = 1;
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ }
+ else
+ {
+ VdpBool enabled[]={0,0,0};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ }
+ }
+ CheckStatus(vdp_st, __LINE__);
+
+ SetDeintSkipChroma();
+
+ m_config.useInteropYuv = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEVDPAUMIXER);
+
+ std::string deintStr = GetDeintStrFromInterlaceMethod(method);
+ // update deinterlacing method used in processInfo (none if progressive)
+ m_config.processInfo->SetVideoDeintMethod(deintStr);
+}
+
+void CMixer::SetDeintSkipChroma()
+{
+ VdpVideoMixerAttribute attribute[] = { VDP_VIDEO_MIXER_ATTRIBUTE_SKIP_CHROMA_DEINTERLACE};
+ VdpStatus vdp_st;
+
+ uint8_t val;
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoVDPAUdeintSkipChromaHD && m_config.outHeight >= 720)
+ val = 1;
+ else
+ val = 0;
+
+ void const *values[]={&val};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_attribute_values(m_videoMixer, ARSIZE(attribute), attribute, values);
+
+ CheckStatus(vdp_st, __LINE__);
+}
+
+void CMixer::SetHWUpscaling()
+{
+#ifdef VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L1
+
+ VdpStatus vdp_st;
+ VdpBool enabled[]={1};
+ switch (m_config.upscale)
+ {
+ case 9:
+ if (m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L9))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L9 };
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ break;
+ }
+ [[fallthrough]];
+ case 8:
+ if (m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L8))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L8 };
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ break;
+ }
+ [[fallthrough]];
+ case 7:
+ if (m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L7))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L7 };
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ break;
+ }
+ [[fallthrough]];
+ case 6:
+ if (m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L6))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L6 };
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ break;
+ }
+ [[fallthrough]];
+ case 5:
+ if (m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L5))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L5 };
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ break;
+ }
+ [[fallthrough]];
+ case 4:
+ if (m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L4))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L4 };
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ break;
+ }
+ [[fallthrough]];
+ case 3:
+ if (m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L3))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L3 };
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ break;
+ }
+ [[fallthrough]];
+ case 2:
+ if (m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L2))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L2 };
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ break;
+ }
+ [[fallthrough]];
+ case 1:
+ if (m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L1))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L1 };
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ break;
+ }
+ [[fallthrough]];
+ default:
+ DisableHQScaling();
+ return;
+ }
+ CheckStatus(vdp_st, __LINE__);
+#endif
+}
+
+void CMixer::DisableHQScaling()
+{
+ VdpStatus vdp_st;
+
+ if (m_videoMixer == VDP_INVALID_HANDLE)
+ return;
+
+ if(m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L1))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L1 };
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ }
+
+ if(m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L2))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L2 };
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ }
+
+ if(m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L3))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L3 };
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ }
+
+ if(m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L4))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L4 };
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ }
+
+ if(m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L5))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L5 };
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ }
+
+ if(m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L6))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L6 };
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ }
+
+ if(m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L7))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L7 };
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ }
+
+ if(m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L8))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L8 };
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ }
+
+ if(m_config.vdpau->Supports(VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L9))
+ {
+ VdpVideoMixerFeature feature[] = { VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L9 };
+ VdpBool enabled[] = {};
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_set_feature_enables(m_videoMixer, ARSIZE(feature), feature, enabled);
+ CheckStatus(vdp_st, __LINE__);
+ }
+}
+
+void CMixer::Init()
+{
+ m_Brightness = 0.0;
+ m_Contrast = 0.0;
+ m_NoiseReduction = 0.0;
+ m_Sharpness = 0.0;
+ m_Deint = 0;
+ m_Upscale = 0;
+ m_SeenInterlaceFlag = false;
+ m_ColorMatrix = 0;
+ m_PostProc = false;
+ m_vdpError = false;
+
+ m_config.upscale = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoVDPAUScaling;
+ m_config.useInteropYuv = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEVDPAUMIXER);
+
+ CreateVdpauMixer();
+
+ // update deinterlacing methods in processInfo
+ std::list<EINTERLACEMETHOD> deintMethods;
+ deintMethods.push_back(VS_INTERLACEMETHOD_NONE);
+ for(SInterlaceMapping* p = g_interlace_mapping; p->method != VS_INTERLACEMETHOD_NONE; p++)
+ {
+ if (p->method == VS_INTERLACEMETHOD_VDPAU_INVERSE_TELECINE)
+ continue;
+
+ if (m_config.vdpau->Supports(p->feature))
+ deintMethods.push_back(p->method);
+ }
+ deintMethods.push_back(VS_INTERLACEMETHOD_VDPAU_BOB);
+ deintMethods.push_back(VS_INTERLACEMETHOD_RENDER_BOB);
+ m_config.processInfo->UpdateDeinterlacingMethods(deintMethods);
+ m_config.processInfo->SetDeinterlacingMethodDefault(EINTERLACEMETHOD::VS_INTERLACEMETHOD_VDPAU_TEMPORAL);
+}
+
+void CMixer::Uninit()
+{
+ Flush();
+ while (!m_outputSurfaces.empty())
+ {
+ m_outputSurfaces.pop();
+ }
+ m_config.context->GetProcs().vdp_video_mixer_destroy(m_videoMixer);
+}
+
+void CMixer::Flush()
+{
+ while (!m_mixerInput.empty())
+ {
+ CVdpauDecodedPicture pic = m_mixerInput.back();
+ m_mixerInput.pop_back();
+ m_config.videoSurfaces->ClearRender(pic.videoSurface);
+ }
+ while (!m_decodedPics.empty())
+ {
+ CVdpauDecodedPicture pic = m_decodedPics.front();
+ m_decodedPics.pop();
+ m_config.videoSurfaces->ClearRender(pic.videoSurface);
+ }
+ Message *msg;
+ while (m_dataPort.ReceiveOutMessage(&msg))
+ {
+ if (msg->signal == CMixerDataProtocol::FRAME)
+ {
+ CPayloadWrap<CVdpauDecodedPicture> *payload;
+ payload = dynamic_cast<CPayloadWrap<CVdpauDecodedPicture>*>(msg->payloadObj.get());
+ if (payload)
+ {
+ CVdpauDecodedPicture pic = *(payload->GetPlayload());
+ m_config.videoSurfaces->ClearRender(pic.videoSurface);
+ }
+ }
+ else if (msg->signal == CMixerDataProtocol::BUFFER)
+ {
+ VdpOutputSurface *surf;
+ surf = (VdpOutputSurface*)msg->data;
+ m_outputSurfaces.push(*surf);
+ }
+ msg->Release();
+ }
+}
+
+std::string CMixer::GetDeintStrFromInterlaceMethod(EINTERLACEMETHOD method)
+{
+ switch (method)
+ {
+ case VS_INTERLACEMETHOD_NONE:
+ return "none";
+ case VS_INTERLACEMETHOD_VDPAU_BOB:
+ return "vdpau-bob";
+ case VS_INTERLACEMETHOD_VDPAU_TEMPORAL:
+ return "vdpau-temp";
+ case VS_INTERLACEMETHOD_VDPAU_TEMPORAL_HALF:
+ return "vdpau-temp-half";
+ case VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL:
+ return "vdpau-temp-spat";
+ case VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL_HALF:
+ return "vdpau-temp-spat-half";
+ case VS_INTERLACEMETHOD_RENDER_BOB:
+ return "bob";
+ default:
+ return "unknown";
+ }
+}
+
+void CMixer::InitCycle()
+{
+ CheckFeatures();
+ int flags;
+ uint64_t latency;
+ m_config.stats->GetParams(latency, flags);
+ if (flags & DVD_CODEC_CTRL_NO_POSTPROC)
+ SetPostProcFeatures(false);
+ else
+ SetPostProcFeatures(true);
+
+ m_config.stats->SetCanSkipDeint(false);
+
+ EINTERLACEMETHOD method = m_config.processInfo->GetVideoSettings().m_InterlaceMethod;
+ bool interlaced = m_mixerInput[1].DVDPic.iFlags & DVP_FLAG_INTERLACED;
+ m_SeenInterlaceFlag |= interlaced;
+
+ if (!(flags & DVD_CODEC_CTRL_NO_POSTPROC) &&
+ interlaced &&
+ method != VS_INTERLACEMETHOD_NONE)
+ {
+ if (!m_config.processInfo->Supports(method))
+ method = VS_INTERLACEMETHOD_VDPAU_TEMPORAL;
+
+ if (method == VS_INTERLACEMETHOD_VDPAU_BOB ||
+ method == VS_INTERLACEMETHOD_VDPAU_TEMPORAL ||
+ method == VS_INTERLACEMETHOD_VDPAU_TEMPORAL_HALF ||
+ method == VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL ||
+ method == VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL_HALF)
+ {
+ if(method == VS_INTERLACEMETHOD_VDPAU_TEMPORAL_HALF ||
+ method == VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL_HALF ||
+ !CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenVideo())
+ m_mixersteps = 1;
+ else
+ {
+ m_mixersteps = 2;
+ m_config.stats->SetCanSkipDeint(true);
+ }
+
+ if (m_mixerInput[1].DVDPic.iFlags & DVD_CODEC_CTRL_SKIPDEINT)
+ {
+ m_mixersteps = 1;
+ }
+
+ if(m_mixerInput[1].DVDPic.iFlags & DVP_FLAG_TOP_FIELD_FIRST)
+ m_mixerfield = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_TOP_FIELD;
+ else
+ m_mixerfield = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_BOTTOM_FIELD;
+
+ m_mixerInput[1].DVDPic.iFlags &= ~(DVP_FLAG_TOP_FIELD_FIRST |
+ DVP_FLAG_REPEAT_TOP_FIELD |
+ DVP_FLAG_INTERLACED);
+ m_mixerInput[1].isYuv = false;
+ m_config.useInteropYuv = false;
+ }
+ else if (method == VS_INTERLACEMETHOD_RENDER_BOB)
+ {
+ m_mixersteps = 1;
+ m_mixerfield = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME;
+ m_mixerInput[1].isYuv = true;
+ m_config.useInteropYuv = true;
+ }
+ }
+ else
+ {
+ m_mixersteps = 1;
+ m_mixerfield = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME;
+
+ if (m_config.useInteropYuv)
+ m_mixerInput[1].isYuv = true;
+ else
+ {
+ m_mixerInput[1].DVDPic.iFlags &= ~(DVP_FLAG_TOP_FIELD_FIRST |
+ DVP_FLAG_REPEAT_TOP_FIELD |
+ DVP_FLAG_INTERLACED);
+ m_mixerInput[1].isYuv = false;
+ }
+ }
+ m_mixerstep = 0;
+ //m_mixerInput[1].DVDPic.format = RENDER_FMT_VDPAU;
+
+ m_processPicture.crop = false;
+ if (!m_mixerInput[1].isYuv)
+ {
+ m_processPicture.outputSurface = m_outputSurfaces.front();
+ m_mixerInput[1].DVDPic.iWidth = m_config.outWidth;
+ m_mixerInput[1].DVDPic.iHeight = m_config.outHeight;
+ if (m_SeenInterlaceFlag)
+ {
+ double ratio = (double)m_mixerInput[1].DVDPic.iDisplayHeight / m_mixerInput[1].DVDPic.iHeight;
+ m_mixerInput[1].DVDPic.iDisplayHeight = lrint(ratio*(m_mixerInput[1].DVDPic.iHeight-NUM_CROP_PIX*2));
+ m_processPicture.crop = true;
+ }
+ }
+ else
+ {
+ m_mixerInput[1].DVDPic.iWidth = m_config.vidWidth;
+ m_mixerInput[1].DVDPic.iHeight = m_config.vidHeight;
+ }
+
+ m_processPicture.isYuv = m_mixerInput[1].isYuv;
+ m_processPicture.DVDPic.SetParams(m_mixerInput[1].DVDPic);
+ m_processPicture.videoSurface = m_mixerInput[1].videoSurface;
+}
+
+void CMixer::FiniCycle()
+{
+ // Keep video surfaces for one 2 cycles longer than used
+ // by mixer. This avoids blocking in decoder.
+ // NVidia recommends num_ref + 5
+ size_t surfToKeep = 5;
+
+ if (m_mixerInput.size() > 0 &&
+ (m_mixerInput[0].videoSurface == VDP_INVALID_HANDLE))
+ surfToKeep = 1;
+
+ while (m_mixerInput.size() > surfToKeep)
+ {
+ CVdpauDecodedPicture &tmp = m_mixerInput.back();
+ if (!m_processPicture.isYuv)
+ {
+ m_config.videoSurfaces->ClearRender(tmp.videoSurface);
+ }
+ m_mixerInput.pop_back();
+ }
+
+ if (surfToKeep == 1)
+ m_mixerInput.clear();
+}
+
+void CMixer::ProcessPicture()
+{
+ if (m_processPicture.isYuv)
+ return;
+
+ VdpStatus vdp_st;
+
+ if (m_mixerstep == 1)
+ {
+ if(m_mixerfield == VDP_VIDEO_MIXER_PICTURE_STRUCTURE_TOP_FIELD)
+ m_mixerfield = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_BOTTOM_FIELD;
+ else
+ m_mixerfield = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_TOP_FIELD;
+ }
+
+ VdpVideoSurface past_surfaces[4] = { VDP_INVALID_HANDLE, VDP_INVALID_HANDLE, VDP_INVALID_HANDLE, VDP_INVALID_HANDLE };
+ VdpVideoSurface futu_surfaces[2] = { VDP_INVALID_HANDLE, VDP_INVALID_HANDLE };
+ uint32_t pastCount = 4;
+ uint32_t futuCount = 2;
+
+ if(m_mixerfield == VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME)
+ {
+ // use only 2 past 1 future for progressive/weave
+ // (only used for postproc anyway eg noise reduction)
+ if (m_mixerInput.size() > 3)
+ past_surfaces[1] = m_mixerInput[3].videoSurface;
+ if (m_mixerInput.size() > 2)
+ past_surfaces[0] = m_mixerInput[2].videoSurface;
+ if (m_mixerInput.size() > 1)
+ futu_surfaces[0] = m_mixerInput[0].videoSurface;
+ pastCount = 2;
+ futuCount = 1;
+ }
+ else
+ {
+ if(m_mixerstep == 0)
+ { // first field
+ if (m_mixerInput.size() > 3)
+ {
+ past_surfaces[3] = m_mixerInput[3].videoSurface;
+ past_surfaces[2] = m_mixerInput[3].videoSurface;
+ }
+ if (m_mixerInput.size() > 2)
+ {
+ past_surfaces[1] = m_mixerInput[2].videoSurface;
+ past_surfaces[0] = m_mixerInput[2].videoSurface;
+ }
+ futu_surfaces[0] = m_mixerInput[1].videoSurface;
+ futu_surfaces[1] = m_mixerInput[0].videoSurface;
+ }
+ else
+ { // second field
+ if (m_mixerInput.size() > 3)
+ {
+ past_surfaces[3] = m_mixerInput[3].videoSurface;
+ }
+ if (m_mixerInput.size() > 2)
+ {
+ past_surfaces[2] = m_mixerInput[2].videoSurface;
+ past_surfaces[1] = m_mixerInput[2].videoSurface;
+ }
+ past_surfaces[0] = m_mixerInput[1].videoSurface;
+ futu_surfaces[0] = m_mixerInput[0].videoSurface;
+ futu_surfaces[1] = m_mixerInput[0].videoSurface;
+
+ if (m_mixerInput[0].DVDPic.pts != DVD_NOPTS_VALUE &&
+ m_mixerInput[1].DVDPic.pts != DVD_NOPTS_VALUE)
+ {
+ m_processPicture.DVDPic.pts = m_mixerInput[1].DVDPic.pts +
+ (m_mixerInput[0].DVDPic.pts -
+ m_mixerInput[1].DVDPic.pts) / 2;
+ }
+ else
+ m_processPicture.DVDPic.pts = DVD_NOPTS_VALUE;
+ m_processPicture.DVDPic.dts = DVD_NOPTS_VALUE;
+ }
+ m_processPicture.DVDPic.iRepeatPicture = 0.0;
+ } // interlaced
+
+ VdpRect sourceRect;
+ sourceRect.x0 = 0;
+ sourceRect.y0 = 0;
+ sourceRect.x1 = m_config.vidWidth;
+ sourceRect.y1 = m_config.vidHeight;
+
+ VdpRect destRect;
+ destRect.x0 = 0;
+ destRect.y0 = 0;
+ destRect.x1 = m_config.outWidth;
+ destRect.y1 = m_config.outHeight;
+
+ // start vdpau video mixer
+ vdp_st = m_config.context->GetProcs().vdp_video_mixer_render(m_videoMixer,
+ VDP_INVALID_HANDLE,
+ 0,
+ m_mixerfield,
+ pastCount,
+ past_surfaces,
+ m_mixerInput[1].videoSurface,
+ futuCount,
+ futu_surfaces,
+ &sourceRect,
+ m_processPicture.outputSurface,
+ &destRect,
+ &destRect,
+ 0,
+ NULL);
+ CheckStatus(vdp_st, __LINE__);
+}
+
+
+bool CMixer::CheckStatus(VdpStatus vdp_st, int line)
+{
+ if (vdp_st != VDP_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, " (VDPAU) Error: {}({}) at {}:{}",
+ m_config.context->GetProcs().vdp_get_error_string(vdp_st), vdp_st, __FILE__, line);
+ m_vdpError = true;
+ return true;
+ }
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// Output
+//-----------------------------------------------------------------------------
+COutput::COutput(CDecoder &decoder, CEvent *inMsgEvent) :
+ CThread("Vdpau Output"),
+ m_controlPort("OutputControlPort", inMsgEvent, &m_outMsgEvent),
+ m_dataPort("OutputDataPort", inMsgEvent, &m_outMsgEvent),
+ m_vdpau(decoder),
+ m_mixer(&m_outMsgEvent)
+{
+ m_inMsgEvent = inMsgEvent;
+ m_bufferPool = std::make_shared<CVdpauBufferPool>(decoder);
+}
+
+void COutput::Start()
+{
+ Create();
+}
+
+COutput::~COutput()
+{
+ Dispose();
+}
+
+void COutput::Dispose()
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ m_bStop = true;
+ m_outMsgEvent.Set();
+ StopThread();
+ m_controlPort.Purge();
+ m_dataPort.Purge();
+}
+
+void COutput::OnStartup()
+{
+ CLog::Log(LOGINFO, "COutput::OnStartup: Output Thread created");
+}
+
+void COutput::OnExit()
+{
+ CLog::Log(LOGINFO, "COutput::OnExit: Output Thread terminated");
+}
+
+enum OUTPUT_STATES
+{
+ O_TOP = 0, // 0
+ O_TOP_ERROR, // 1
+ O_TOP_UNCONFIGURED, // 2
+ O_TOP_CONFIGURED, // 3
+ O_TOP_CONFIGURED_IDLE, // 4
+ O_TOP_CONFIGURED_WORK, // 5
+};
+
+int VDPAU_OUTPUT_parentStates[] = {
+ -1,
+ 0, //TOP_ERROR
+ 0, //TOP_UNCONFIGURED
+ 0, //TOP_CONFIGURED
+ 3, //TOP_CONFIGURED_IDLE
+ 3, //TOP_CONFIGURED_WORK
+};
+
+void COutput::StateMachine(int signal, Protocol *port, Message *msg)
+{
+ for (int state = m_state; ; state = VDPAU_OUTPUT_parentStates[state])
+ {
+ switch (state)
+ {
+ case O_TOP: // TOP
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::FLUSH:
+ msg->Reply(COutputControlProtocol::ACC);
+ return;
+ case COutputControlProtocol::PRECLEANUP:
+ msg->Reply(COutputControlProtocol::ACC);
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case COutputDataProtocol::RETURNPIC:
+ CVdpauRenderPicture *pic;
+ pic = *((CVdpauRenderPicture**)msg->data);
+ QueueReturnPicture(pic);
+ return;
+ default:
+ break;
+ }
+ }
+ {
+ std::string portName = port == NULL ? "timer" : port->portName;
+ CLog::Log(LOGWARNING, "COutput::{} - signal: {} form port: {} not handled for state: {}",
+ __FUNCTION__, signal, portName, m_state);
+ }
+ return;
+
+ case O_TOP_ERROR:
+ break;
+
+ case O_TOP_UNCONFIGURED:
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::INIT:
+ CVdpauConfig *data;
+ data = (CVdpauConfig*)msg->data;
+ if (data)
+ {
+ m_config = *data;
+ }
+ Init();
+ Message *reply;
+ if (m_mixer.m_controlPort.SendOutMessageSync(CMixerControlProtocol::INIT,
+ &reply, 1000, &m_config, sizeof(m_config)))
+ {
+ if (reply->signal != CMixerControlProtocol::ACC)
+ m_vdpError = true;
+ reply->Release();
+ }
+
+ // set initial number of
+ m_bufferPool->numOutputSurfaces = 4;
+ EnsureBufferPool();
+ if (!m_vdpError)
+ {
+ m_state = O_TOP_CONFIGURED_IDLE;
+ msg->Reply(COutputControlProtocol::ACC, &m_config, sizeof(m_config));
+ }
+ else
+ {
+ m_state = O_TOP_ERROR;
+ msg->Reply(COutputControlProtocol::ERROR);
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case O_TOP_CONFIGURED:
+ if (port == &m_controlPort)
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::FLUSH:
+ Flush();
+ msg->Reply(COutputControlProtocol::ACC);
+ return;
+ case COutputControlProtocol::PRECLEANUP:
+ Flush();
+ PreCleanup();
+ msg->Reply(COutputControlProtocol::ACC);
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_dataPort)
+ {
+ switch (signal)
+ {
+ case COutputDataProtocol::NEWFRAME:
+ CPayloadWrap<CVdpauDecodedPicture> *payload;
+ payload = dynamic_cast<CPayloadWrap<CVdpauDecodedPicture>*>(msg->payloadObj.release());
+ if (payload)
+ {
+ m_mixer.m_dataPort.SendOutMessage(CMixerDataProtocol::FRAME, payload);
+ }
+ return;
+ case COutputDataProtocol::RETURNPIC:
+ CVdpauRenderPicture *pic;
+ pic = *((CVdpauRenderPicture**)msg->data);
+ QueueReturnPicture(pic);
+ m_controlPort.SendInMessage(COutputControlProtocol::STATS);
+ m_state = O_TOP_CONFIGURED_WORK;
+ m_extTimeout = 0;
+ return;
+ default:
+ break;
+ }
+ }
+ else if (port == &m_mixer.m_dataPort)
+ {
+ switch (signal)
+ {
+ case CMixerDataProtocol::PICTURE:
+ CVdpauProcessedPicture *pic;
+ pic = (CVdpauProcessedPicture*)msg->data;
+ m_bufferPool->processedPics.push(*pic);
+ m_state = O_TOP_CONFIGURED_WORK;
+ m_extTimeout = 0;
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case O_TOP_CONFIGURED_IDLE:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::TIMEOUT:
+ m_extTimeout = 100;
+ if (HasWork())
+ {
+ m_state = O_TOP_CONFIGURED_WORK;
+ m_extTimeout = 0;
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ case O_TOP_CONFIGURED_WORK:
+ if (port == NULL) // timeout
+ {
+ switch (signal)
+ {
+ case COutputControlProtocol::TIMEOUT:
+ if (HasWork())
+ {
+ CVdpauRenderPicture *pic;
+ pic = ProcessMixerPicture();
+ if (pic)
+ {
+ m_config.stats->DecProcessed();
+ m_config.stats->IncRender();
+ m_dataPort.SendInMessage(COutputDataProtocol::PICTURE, &pic, sizeof(pic));
+ }
+ m_extTimeout = 1;
+ }
+ else
+ {
+ m_state = O_TOP_CONFIGURED_IDLE;
+ m_extTimeout = 0;
+ }
+ return;
+ default:
+ break;
+ }
+ }
+ break;
+
+ default: // we are in no state, should not happen
+ CLog::Log(LOGERROR, "COutput::{} - no valid state: {}", __FUNCTION__, m_state);
+ return;
+ }
+ } // for
+}
+
+void COutput::Process()
+{
+ Message *msg = NULL;
+ Protocol *port = NULL;
+ bool gotMsg;
+
+ m_state = O_TOP_UNCONFIGURED;
+ m_extTimeout = 1000;
+ m_bStateMachineSelfTrigger = false;
+
+ while (!m_bStop)
+ {
+ gotMsg = false;
+
+ if (m_bStateMachineSelfTrigger)
+ {
+ m_bStateMachineSelfTrigger = false;
+ // self trigger state machine
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ continue;
+ }
+ // check control port
+ else if (m_controlPort.ReceiveOutMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_controlPort;
+ }
+ // check data port
+ else if (m_dataPort.ReceiveOutMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_dataPort;
+ }
+ // check mixer data port
+ else if (m_mixer.m_dataPort.ReceiveInMessage(&msg))
+ {
+ gotMsg = true;
+ port = &m_mixer.m_dataPort;
+ }
+ if (gotMsg)
+ {
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ continue;
+ }
+
+ // wait for message
+ else if (m_outMsgEvent.Wait(std::chrono::milliseconds(m_extTimeout)))
+ {
+ continue;
+ }
+ // time out
+ else
+ {
+ msg = m_controlPort.GetMessage();
+ msg->signal = COutputControlProtocol::TIMEOUT;
+ port = 0;
+ // signal timeout to state machine
+ StateMachine(msg->signal, port, msg);
+ if (!m_bStateMachineSelfTrigger)
+ {
+ msg->Release();
+ msg = NULL;
+ }
+ }
+ }
+ Flush();
+ Uninit();
+}
+
+bool COutput::Init()
+{
+ m_mixer.Start();
+ m_vdpError = false;
+
+ return true;
+}
+
+bool COutput::Uninit()
+{
+ m_mixer.Dispose();
+ ProcessSyncPicture();
+ ReleaseBufferPool();
+ return true;
+}
+
+void COutput::Flush()
+{
+ if (m_mixer.IsActive())
+ {
+ Message *reply;
+ if (m_mixer.m_controlPort.SendOutMessageSync(CMixerControlProtocol::FLUSH,
+ &reply,
+ 2000))
+ {
+ reply->Release();
+ }
+ else
+ CLog::Log(LOGERROR, "Coutput::{} - failed to flush mixer", __FUNCTION__);
+ }
+
+ Message *msg;
+
+ while (m_dataPort.ReceiveInMessage(&msg))
+ {
+ if (msg->signal == COutputDataProtocol::PICTURE)
+ {
+ CVdpauRenderPicture *pic;
+ pic = *((CVdpauRenderPicture**)msg->data);
+ pic->Release();
+ }
+ msg->Release();
+ }
+
+ while (m_dataPort.ReceiveOutMessage(&msg))
+ {
+ if (msg->signal == COutputDataProtocol::NEWFRAME)
+ {
+ CPayloadWrap<CVdpauDecodedPicture> *payload;
+ payload = dynamic_cast<CPayloadWrap<CVdpauDecodedPicture>*>(msg->payloadObj.get());
+ if (payload)
+ {
+ CVdpauDecodedPicture pic = *(payload->GetPlayload());
+ m_config.videoSurfaces->ClearRender(pic.videoSurface);
+ }
+ }
+ else if (msg->signal == COutputDataProtocol::RETURNPIC)
+ {
+ CVdpauRenderPicture *pic;
+ pic = *((CVdpauRenderPicture**)msg->data);
+ QueueReturnPicture(pic);
+ }
+ msg->Release();
+ }
+
+ while (m_mixer.m_dataPort.ReceiveInMessage(&msg))
+ {
+ if (msg->signal == CMixerDataProtocol::PICTURE)
+ {
+ CVdpauProcessedPicture pic = *reinterpret_cast<CVdpauProcessedPicture*>(msg->data);
+ m_bufferPool->processedPics.push(pic);
+ }
+ msg->Release();
+ }
+
+ // reset used render flag which was cleared on mixer flush
+ for (auto &awayPic : m_bufferPool->processedPicsAway)
+ {
+ if (awayPic.isYuv)
+ {
+ m_config.videoSurfaces->MarkRender(awayPic.videoSurface);
+ }
+ }
+
+ // clear processed pics
+ while(!m_bufferPool->processedPics.empty())
+ {
+ CVdpauProcessedPicture procPic = m_bufferPool->processedPics.front();
+ if (!procPic.isYuv)
+ {
+ m_mixer.m_dataPort.SendOutMessage(CMixerDataProtocol::BUFFER, &procPic.outputSurface, sizeof(procPic.outputSurface));
+ }
+ else
+ {
+ m_config.videoSurfaces->ClearRender(procPic.videoSurface);
+ }
+ m_bufferPool->processedPics.pop();
+ }
+}
+
+bool COutput::HasWork()
+{
+ if (!m_bufferPool->processedPics.empty() && m_bufferPool->HasFree())
+ return true;
+ return false;
+}
+
+CVdpauRenderPicture* COutput::ProcessMixerPicture()
+{
+ CVdpauRenderPicture *retPic = NULL;
+
+ if (!m_bufferPool->processedPics.empty() && m_bufferPool->HasFree())
+ {
+ retPic = m_bufferPool->GetVdpau();
+ CVdpauProcessedPicture procPic = m_bufferPool->processedPics.front();
+ procPic.id = m_bufferPool->procPicId++;
+ m_bufferPool->processedPics.pop();
+ m_bufferPool->processedPicsAway.push_back(procPic);
+ retPic->procPic = procPic;
+ retPic->device = reinterpret_cast<void*>(m_config.context->GetDevice());
+ retPic->procFunc = reinterpret_cast<void*>(m_config.context->GetProcs().vdp_get_proc_address);
+ retPic->ident = m_config.timeOpened + m_config.resetCounter;
+
+ retPic->DVDPic.SetParams(procPic.DVDPic);
+ if (!procPic.isYuv)
+ {
+ m_config.useInteropYuv = false;
+ m_bufferPool->numOutputSurfaces = NUM_RENDER_PICS;
+ EnsureBufferPool();
+ retPic->width = m_config.outWidth;
+ retPic->height = m_config.outHeight;
+ retPic->crop.x1 = 0;
+ retPic->crop.y1 = procPic.crop ? NUM_CROP_PIX : 0;
+ retPic->crop.x2 = m_config.outWidth;
+ retPic->crop.y2 = m_config.outHeight - retPic->crop.y1;
+ }
+ else
+ {
+ m_config.useInteropYuv = true;
+ retPic->width = m_config.surfaceWidth;
+ retPic->height = m_config.surfaceHeight;
+ retPic->crop.x1 = 0;
+ retPic->crop.y1 = 0;
+ retPic->crop.x2 = m_config.surfaceWidth - m_config.vidWidth;
+ retPic->crop.y2 = m_config.surfaceHeight - m_config.vidHeight;
+ }
+ }
+ return retPic;
+}
+
+void COutput::QueueReturnPicture(CVdpauRenderPicture *pic)
+{
+ m_bufferPool->QueueReturnPicture(pic);
+ ProcessSyncPicture();
+}
+
+void COutput::ProcessSyncPicture()
+{
+ CVdpauRenderPicture *pic;
+
+ pic = m_bufferPool->ProcessSyncPicture();
+
+ while (pic != nullptr)
+ {
+ ProcessReturnPicture(pic);
+ pic = m_bufferPool->ProcessSyncPicture();
+ }
+}
+
+void COutput::ProcessReturnPicture(CVdpauRenderPicture *pic)
+{
+ for (auto it=m_bufferPool->processedPicsAway.begin(); it!=m_bufferPool->processedPicsAway.end(); ++it)
+ {
+ if (it->id == pic->procPic.id)
+ {
+ if (pic->procPic.isYuv)
+ {
+ VdpVideoSurface surf = pic->procPic.videoSurface;
+ if (surf != VDP_INVALID_HANDLE)
+ m_config.videoSurfaces->ClearRender(surf);
+ }
+ else
+ {
+ VdpOutputSurface outSurf = pic->procPic.outputSurface;
+ if (outSurf != VDP_INVALID_HANDLE)
+ m_mixer.m_dataPort.SendOutMessage(CMixerDataProtocol::BUFFER, &outSurf, sizeof(outSurf));
+ }
+ m_bufferPool->processedPicsAway.erase(it);
+ break;
+ }
+ }
+}
+
+bool COutput::EnsureBufferPool()
+{
+ VdpStatus vdp_st;
+
+ // Creation of outputSurfaces
+ VdpOutputSurface outputSurface;
+ for (int i = m_bufferPool->outputSurfaces.size(); i < m_bufferPool->numOutputSurfaces; i++)
+ {
+ vdp_st = m_config.context->GetProcs().vdp_output_surface_create(m_config.context->GetDevice(),
+ VDP_RGBA_FORMAT_B8G8R8A8,
+ m_config.outWidth,
+ m_config.outHeight,
+ &outputSurface);
+ if (CheckStatus(vdp_st, __LINE__))
+ return false;
+ m_bufferPool->outputSurfaces.push_back(outputSurface);
+
+ m_mixer.m_dataPort.SendOutMessage(CMixerDataProtocol::BUFFER,
+ &outputSurface,
+ sizeof(VdpOutputSurface));
+ CLog::Log(LOGINFO, "VDPAU::COutput::InitBufferPool - Output Surface created");
+ }
+ return true;
+}
+
+void COutput::ReleaseBufferPool()
+{
+ VdpStatus vdp_st;
+
+ // release all output surfaces
+ m_bufferPool->InvalidateUsed();
+ for (unsigned int i = 0; i < m_bufferPool->outputSurfaces.size(); ++i)
+ {
+ if (m_bufferPool->outputSurfaces[i] == VDP_INVALID_HANDLE)
+ continue;
+ vdp_st = m_config.context->GetProcs().vdp_output_surface_destroy(m_bufferPool->outputSurfaces[i]);
+ CheckStatus(vdp_st, __LINE__);
+ }
+ m_bufferPool->outputSurfaces.clear();
+
+ ProcessSyncPicture();
+}
+
+void COutput::PreCleanup()
+{
+
+ VdpStatus vdp_st;
+
+ m_mixer.Dispose();
+ ProcessSyncPicture();
+
+ for (unsigned int i = 0; i < m_bufferPool->outputSurfaces.size(); ++i)
+ {
+ if (m_bufferPool->outputSurfaces[i] == VDP_INVALID_HANDLE)
+ continue;
+
+ // check if output surface is in use
+ bool used = false;
+ for (auto &picAway : m_bufferPool->processedPicsAway)
+ {
+ if (picAway.outputSurface == m_bufferPool->outputSurfaces[i])
+ {
+ used = true;
+ break;
+ }
+ }
+ if (used)
+ continue;
+
+ vdp_st = m_config.context->GetProcs().vdp_output_surface_destroy(m_bufferPool->outputSurfaces[i]);
+ CheckStatus(vdp_st, __LINE__);
+
+ m_bufferPool->outputSurfaces[i] = VDP_INVALID_HANDLE;
+ CLog::Log(LOGDEBUG, LOGVIDEO, "VDPAU::PreCleanup - released output surface");
+ }
+
+}
+
+void COutput::InitMixer()
+{
+ for (unsigned int i = 0; i < m_bufferPool->outputSurfaces.size(); ++i)
+ {
+ m_mixer.m_dataPort.SendOutMessage(CMixerDataProtocol::BUFFER,
+ &m_bufferPool->outputSurfaces[i],
+ sizeof(VdpOutputSurface));
+ }
+}
+
+bool COutput::CheckStatus(VdpStatus vdp_st, int line)
+{
+ if (vdp_st != VDP_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, " (VDPAU) Error: {}({}) at {}:{}",
+ m_config.context->GetProcs().vdp_get_error_string(vdp_st), vdp_st, __FILE__, line);
+ m_vdpError = true;
+ return true;
+ }
+ return false;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/VDPAU.h b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VDPAU.h
new file mode 100644
index 0000000..d92416e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VDPAU.h
@@ -0,0 +1,661 @@
+/*
+ * 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
+
+/**
+ * design goals:
+ * - improve performance
+ * max out hw resources: e.g. make 1080p60 play on ION2
+ * allow advanced de-interlacing on ION
+ *
+ * - add vdpau/opengl interop
+ *
+ * - remove tight dependency to render thread
+ * prior design needed to hijack render thread in order to do
+ * gl interop functions. In particular this was a problem for
+ * init and clear down. Introduction of GL_NV_vdpau_interop
+ * increased the need to be independent from render thread
+ *
+ * - move to an actor based design in order to reduce the number
+ * of locks needed.
+ */
+
+#include "cores/VideoPlayer/Buffers/VideoBuffer.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+#include "cores/VideoSettings.h"
+#include "guilib/DispResource.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/SharedSection.h"
+#include "threads/Thread.h"
+#include "utils/ActorProtocol.h"
+#include "utils/Geometry.h"
+
+#include <deque>
+#include <list>
+#include <map>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+extern "C" {
+#include <libavutil/avutil.h>
+#include <libavcodec/vdpau.h>
+}
+
+class CProcessInfo;
+
+namespace VDPAU
+{
+
+/**
+ * VDPAU interface to driver
+ */
+
+struct VDPAU_procs
+{
+ VdpGetProcAddress*vdp_get_proc_address;
+ VdpDeviceDestroy* vdp_device_destroy;
+
+ VdpVideoSurfaceCreate* vdp_video_surface_create;
+ VdpVideoSurfaceDestroy* vdp_video_surface_destroy;
+ VdpVideoSurfacePutBitsYCbCr* vdp_video_surface_put_bits_y_cb_cr;
+ VdpVideoSurfaceGetBitsYCbCr* vdp_video_surface_get_bits_y_cb_cr;
+
+ VdpOutputSurfacePutBitsYCbCr* vdp_output_surface_put_bits_y_cb_cr;
+ VdpOutputSurfacePutBitsNative* vdp_output_surface_put_bits_native;
+ VdpOutputSurfaceCreate* vdp_output_surface_create;
+ VdpOutputSurfaceDestroy* vdp_output_surface_destroy;
+ VdpOutputSurfaceGetBitsNative* vdp_output_surface_get_bits_native;
+ VdpOutputSurfaceRenderOutputSurface* vdp_output_surface_render_output_surface;
+ VdpOutputSurfacePutBitsIndexed* vdp_output_surface_put_bits_indexed;
+
+ VdpVideoMixerCreate* vdp_video_mixer_create;
+ VdpVideoMixerSetFeatureEnables* vdp_video_mixer_set_feature_enables;
+ VdpVideoMixerQueryParameterSupport* vdp_video_mixer_query_parameter_support;
+ VdpVideoMixerQueryFeatureSupport* vdp_video_mixer_query_feature_support;
+ VdpVideoMixerDestroy* vdp_video_mixer_destroy;
+ VdpVideoMixerRender* vdp_video_mixer_render;
+ VdpVideoMixerSetAttributeValues* vdp_video_mixer_set_attribute_values;
+
+ VdpGenerateCSCMatrix* vdp_generate_csc_matrix;
+
+ VdpGetErrorString* vdp_get_error_string;
+
+ VdpDecoderCreate* vdp_decoder_create;
+ VdpDecoderDestroy* vdp_decoder_destroy;
+ VdpDecoderRender* vdp_decoder_render;
+ VdpDecoderQueryCapabilities* vdp_decoder_query_caps;
+
+ VdpPreemptionCallbackRegister* vdp_preemption_callback_register;
+};
+
+//-----------------------------------------------------------------------------
+// VDPAU data structs
+//-----------------------------------------------------------------------------
+
+class CDecoder;
+
+/**
+ * Buffer statistics used to control number of frames in queue
+ */
+
+class CVdpauBufferStats
+{
+public:
+ uint16_t decodedPics;
+ uint16_t processedPics;
+ uint16_t renderPics;
+ uint64_t latency; // time decoder has waited for a frame, ideally there is no latency
+ int codecFlags;
+ bool canSkipDeint;
+ bool draining;
+
+ void IncDecoded()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ decodedPics++;
+ }
+ void DecDecoded()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ decodedPics--;
+ }
+ void IncProcessed()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ processedPics++;
+ }
+ void DecProcessed()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ processedPics--;
+ }
+ void IncRender()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ renderPics++;
+ }
+ void DecRender()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ renderPics--;
+ }
+ void Reset()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ decodedPics = 0;
+ processedPics = 0;
+ renderPics = 0;
+ latency = 0;
+ }
+ void Get(uint16_t& decoded, uint16_t& processed, uint16_t& render)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ decoded = decodedPics, processed = processedPics, render = renderPics;
+ }
+ void SetParams(uint64_t time, int flags)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ latency = time;
+ codecFlags = flags;
+ }
+ void GetParams(uint64_t& lat, int& flags)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ lat = latency;
+ flags = codecFlags;
+ }
+ void SetCanSkipDeint(bool canSkip)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ canSkipDeint = canSkip;
+ }
+ bool CanSkipDeint()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ if (canSkipDeint)
+ return true;
+ else
+ return false;
+ }
+ void SetDraining(bool drain)
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ draining = drain;
+ }
+ bool IsDraining()
+ {
+ std::unique_lock<CCriticalSection> l(m_sec);
+ if (draining)
+ return true;
+ else
+ return false;
+ }
+
+private:
+ CCriticalSection m_sec;
+};
+
+/**
+ * CVdpauConfig holds all configuration parameters needed by vdpau
+ * The structure is sent to the internal classes CMixer and COutput
+ * for init.
+ */
+
+class CVideoSurfaces;
+class CVDPAUContext;
+
+struct CVdpauConfig
+{
+ int surfaceWidth;
+ int surfaceHeight;
+ int vidWidth;
+ int vidHeight;
+ int outWidth;
+ int outHeight;
+ VdpDecoder vdpDecoder;
+ VdpChromaType vdpChromaType;
+ CVdpauBufferStats *stats;
+ CDecoder *vdpau;
+ int upscale;
+ CVideoSurfaces *videoSurfaces;
+ int numRenderBuffers;
+ uint32_t maxReferences;
+ bool useInteropYuv;
+ CVDPAUContext *context;
+ CProcessInfo *processInfo;
+ int resetCounter;
+ uint64_t timeOpened;
+};
+
+/**
+ * Holds a decoded frame
+ * Input to COutput for further processing
+ */
+struct CVdpauDecodedPicture
+{
+ CVdpauDecodedPicture() = default;
+ CVdpauDecodedPicture(const CVdpauDecodedPicture &rhs)
+ {
+ *this = rhs;
+ }
+ CVdpauDecodedPicture& operator=(const CVdpauDecodedPicture& rhs)
+ {
+ DVDPic.SetParams(rhs.DVDPic);
+ videoSurface = rhs.videoSurface;
+ isYuv = rhs.isYuv;
+ return *this;
+ };
+ VideoPicture DVDPic;
+ VdpVideoSurface videoSurface;
+ bool isYuv;
+};
+
+/**
+ * Frame after having been processed by vdpau mixer
+ */
+struct CVdpauProcessedPicture
+{
+ CVdpauProcessedPicture() = default;
+ CVdpauProcessedPicture(const CVdpauProcessedPicture& rhs)
+ {
+ *this = rhs;
+ }
+ CVdpauProcessedPicture& operator=(const CVdpauProcessedPicture& rhs)
+ {
+ DVDPic.SetParams(rhs.DVDPic);
+ videoSurface = rhs.videoSurface;
+ outputSurface = rhs.outputSurface;
+ crop = rhs.crop;
+ isYuv = rhs.isYuv;
+ id = rhs.id;
+ return *this;
+ };
+
+ VideoPicture DVDPic;
+ VdpVideoSurface videoSurface = VDP_INVALID_HANDLE;
+ VdpOutputSurface outputSurface = VDP_INVALID_HANDLE;
+ bool crop;
+ bool isYuv;
+ int id = 0;
+};
+
+class CVdpauRenderPicture : public CVideoBuffer
+{
+public:
+ explicit CVdpauRenderPicture(int id) : CVideoBuffer(id) { }
+ VideoPicture DVDPic;
+ CVdpauProcessedPicture procPic;
+ int width;
+ int height;
+ CRect crop;
+ void *device;
+ void *procFunc;
+ int64_t ident;
+};
+
+//-----------------------------------------------------------------------------
+// Mixer
+//-----------------------------------------------------------------------------
+
+class CMixerControlProtocol : public Actor::Protocol
+{
+public:
+ CMixerControlProtocol(std::string name, CEvent* inEvent, CEvent* outEvent)
+ : Protocol(std::move(name), inEvent, outEvent)
+ {
+ }
+ enum OutSignal
+ {
+ INIT = 0,
+ FLUSH,
+ TIMEOUT,
+ };
+ enum InSignal
+ {
+ ACC,
+ ERROR,
+ };
+};
+
+class CMixerDataProtocol : public Actor::Protocol
+{
+public:
+ CMixerDataProtocol(std::string name, CEvent* inEvent, CEvent* outEvent)
+ : Protocol(std::move(name), inEvent, outEvent)
+ {
+ }
+ enum OutSignal
+ {
+ FRAME,
+ BUFFER,
+ };
+ enum InSignal
+ {
+ PICTURE,
+ };
+};
+
+/**
+ * Embeds the vdpau video mixer
+ * Embedded by COutput class, gets decoded frames from COutput, processes
+ * them in mixer ands sends processed frames back to COutput
+ */
+class CMixer : private CThread
+{
+public:
+ explicit CMixer(CEvent *inMsgEvent);
+ ~CMixer() override;
+ void Start();
+ void Dispose();
+ bool IsActive();
+ CMixerControlProtocol m_controlPort;
+ CMixerDataProtocol m_dataPort;
+protected:
+ void OnStartup() override;
+ void OnExit() override;
+ void Process() override;
+ void StateMachine(int signal, Actor::Protocol *port, Actor::Message *msg);
+ void Init();
+ void Uninit();
+ void Flush();
+ void CreateVdpauMixer();
+ void ProcessPicture();
+ void InitCycle();
+ void FiniCycle();
+ void CheckFeatures();
+ void SetPostProcFeatures(bool postProcEnabled);
+ void PostProcOff();
+ void InitCSCMatrix(int Width);
+ bool GenerateStudioCSCMatrix(VdpColorStandard colorStandard, VdpCSCMatrix &studioCSCMatrix);
+ void SetColor();
+ void SetNoiseReduction();
+ void SetSharpness();
+ void SetDeintSkipChroma();
+ void SetDeinterlacing();
+ void SetHWUpscaling();
+ void DisableHQScaling();
+ std::string GetDeintStrFromInterlaceMethod(EINTERLACEMETHOD method);
+ bool CheckStatus(VdpStatus vdp_st, int line);
+ CEvent m_outMsgEvent;
+ CEvent *m_inMsgEvent;
+ int m_state;
+ bool m_bStateMachineSelfTrigger;
+
+ // extended state variables for state machine
+ int m_extTimeout;
+ bool m_vdpError;
+ CVdpauConfig m_config;
+ VdpVideoMixer m_videoMixer;
+ VdpProcamp m_Procamp;
+ VdpCSCMatrix m_CSCMatrix;
+ bool m_PostProc;
+ float m_Brightness;
+ float m_Contrast;
+ float m_NoiseReduction;
+ float m_Sharpness;
+ int m_Deint;
+ int m_Upscale;
+ bool m_SeenInterlaceFlag;
+ unsigned int m_ColorMatrix : 4;
+ VdpVideoMixerPictureStructure m_mixerfield;
+ int m_mixerstep;
+ int m_mixersteps;
+ CVdpauProcessedPicture m_processPicture;
+ std::queue<VdpOutputSurface> m_outputSurfaces;
+ std::queue<CVdpauDecodedPicture> m_decodedPics;
+ std::deque<CVdpauDecodedPicture> m_mixerInput;
+};
+
+//-----------------------------------------------------------------------------
+// Output
+//-----------------------------------------------------------------------------
+
+class COutputControlProtocol : public Actor::Protocol
+{
+public:
+ COutputControlProtocol(std::string name, CEvent* inEvent, CEvent* outEvent)
+ : Actor::Protocol(std::move(name), inEvent, outEvent)
+ {
+ }
+ enum OutSignal
+ {
+ INIT,
+ FLUSH,
+ PRECLEANUP,
+ TIMEOUT,
+ };
+ enum InSignal
+ {
+ ACC,
+ ERROR,
+ STATS,
+ };
+};
+
+class COutputDataProtocol : public Actor::Protocol
+{
+public:
+ COutputDataProtocol(std::string name, CEvent* inEvent, CEvent* outEvent)
+ : Actor::Protocol(std::move(name), inEvent, outEvent)
+ {
+ }
+ enum OutSignal
+ {
+ NEWFRAME = 0,
+ RETURNPIC,
+ };
+ enum InSignal
+ {
+ PICTURE,
+ };
+};
+
+/**
+ * COutput is embedded in CDecoder and embeds CMixer
+ * The class has its own OpenGl context which is shared with render thread
+ * COutput generated ready to render textures and passes them back to
+ * CDecoder
+ */
+class CVdpauBufferPool;
+
+class COutput : private CThread
+{
+public:
+ COutput(CDecoder &decoder, CEvent *inMsgEvent);
+ ~COutput() override;
+ void Start();
+ void Dispose();
+ COutputControlProtocol m_controlPort;
+ COutputDataProtocol m_dataPort;
+protected:
+ void OnStartup() override;
+ void OnExit() override;
+ void Process() override;
+ void StateMachine(int signal, Actor::Protocol *port, Actor::Message *msg);
+ bool HasWork();
+ CVdpauRenderPicture *ProcessMixerPicture();
+ void QueueReturnPicture(CVdpauRenderPicture *pic);
+ void ProcessReturnPicture(CVdpauRenderPicture *pic);
+ void ProcessSyncPicture();
+ bool Init();
+ bool Uninit();
+ void Flush();
+ bool EnsureBufferPool();
+ void ReleaseBufferPool();
+ void PreCleanup();
+ void InitMixer();
+ bool CheckStatus(VdpStatus vdp_st, int line);
+ CEvent m_outMsgEvent;
+ CEvent *m_inMsgEvent;
+ int m_state;
+ bool m_bStateMachineSelfTrigger;
+ CDecoder &m_vdpau;
+
+ // extended state variables for state machine
+ int m_extTimeout;
+ bool m_vdpError;
+ CVdpauConfig m_config;
+ std::shared_ptr<CVdpauBufferPool> m_bufferPool;
+ CMixer m_mixer;
+};
+
+//-----------------------------------------------------------------------------
+// VDPAU Video Surface states
+//-----------------------------------------------------------------------------
+
+class CVideoSurfaces
+{
+public:
+ void AddSurface(VdpVideoSurface surf);
+ void ClearReference(VdpVideoSurface surf);
+ bool MarkRender(VdpVideoSurface surf);
+ void ClearRender(VdpVideoSurface surf);
+ bool IsValid(VdpVideoSurface surf);
+ VdpVideoSurface GetFree(VdpVideoSurface surf);
+ VdpVideoSurface RemoveNext(bool skiprender = false);
+ void Reset();
+ int Size();
+ bool HasRefs();
+protected:
+ std::map<VdpVideoSurface, int> m_state;
+ std::list<VdpVideoSurface> m_freeSurfaces;
+ CCriticalSection m_section;
+};
+
+//-----------------------------------------------------------------------------
+// VDPAU decoder
+//-----------------------------------------------------------------------------
+
+class CVDPAUContext
+{
+public:
+ static bool EnsureContext(CVDPAUContext **ctx);
+ void Release();
+ VDPAU_procs& GetProcs();
+ VdpDevice GetDevice();
+ bool Supports(VdpVideoMixerFeature feature);
+ VdpVideoMixerFeature* GetFeatures();
+ int GetFeatureCount();
+private:
+ CVDPAUContext();
+ void Close();
+ bool LoadSymbols();
+ bool CreateContext();
+ void DestroyContext();
+ void QueryProcs();
+ void SpewHardwareAvailable();
+ static CVDPAUContext *m_context;
+ static CCriticalSection m_section;
+ static Display *m_display;
+ int m_refCount;
+ VdpVideoMixerFeature m_vdpFeatures[14];
+ int m_featureCount;
+ static void *m_dlHandle;
+ VdpDevice m_vdpDevice;
+ VDPAU_procs m_vdpProcs;
+ VdpStatus (*dl_vdp_device_create_x11)(Display* display, int screen, VdpDevice* device, VdpGetProcAddress **get_proc_address);
+};
+
+/**
+ * VDPAU main class
+ */
+class CDecoder
+ : public IHardwareDecoder
+ , public IDispResource
+{
+ friend class CVdpauBufferPool;
+
+public:
+
+ struct Desc
+ {
+ const char *name;
+ uint32_t id;
+ uint32_t aux; /* optional extra parameter... */
+ };
+
+ explicit CDecoder(CProcessInfo& processInfo);
+ ~CDecoder() override;
+
+ bool Open (AVCodecContext* avctx, AVCodecContext* mainctx, const enum AVPixelFormat) override;
+ CDVDVideoCodec::VCReturn Decode (AVCodecContext* avctx, AVFrame* frame) override;
+ bool GetPicture(AVCodecContext* avctx, VideoPicture* picture) override;
+ void Reset() override;
+ virtual void Close();
+ long Release() override;
+ bool CanSkipDeint() override;
+ unsigned GetAllowedReferences() override { return 5; }
+
+ CDVDVideoCodec::VCReturn Check(AVCodecContext* avctx) override;
+ const std::string Name() override { return "vdpau"; }
+ void SetCodecControl(int flags) override;
+
+ bool Supports(VdpVideoMixerFeature feature);
+ static bool IsVDPAUFormat(AVPixelFormat fmt);
+
+ static void FFReleaseBuffer(void *opaque, uint8_t *data);
+ static int FFGetBuffer(AVCodecContext *avctx, AVFrame *pic, int flags);
+ static int Render(struct AVCodecContext *s, struct AVFrame *src,
+ const VdpPictureInfo *info, uint32_t buffers_used,
+ const VdpBitstreamBuffer *buffers);
+
+ void OnLostDisplay() override;
+ void OnResetDisplay() override;
+
+ static IHardwareDecoder* Create(CDVDStreamInfo &hint, CProcessInfo &processInfo, AVPixelFormat fmt);
+ static void Register();
+
+protected:
+ void SetWidthHeight(int width, int height);
+ bool ConfigVDPAU(AVCodecContext *avctx, int ref_frames);
+ bool CheckStatus(VdpStatus vdp_st, int line);
+ void FiniVDPAUOutput();
+ void ReturnRenderPicture(CVdpauRenderPicture *renderPic);
+ long ReleasePicReference();
+
+ static void ReadFormatOf( AVCodecID codec
+ , VdpDecoderProfile &decoder_profile
+ , VdpChromaType &chroma_type);
+
+ // OnLostDevice triggers transition from all states to LOST
+ // internal errors trigger transition from OPEN to RESET
+ // OnResetDevice triggers transition from LOST to RESET
+ enum EDisplayState
+ { VDPAU_OPEN
+ , VDPAU_RESET
+ , VDPAU_LOST
+ , VDPAU_ERROR
+ } m_DisplayState;
+ CCriticalSection m_DecoderSection;
+ CEvent m_DisplayEvent;
+ int m_ErrorCount;
+
+ bool m_vdpauConfigured;
+ CVdpauConfig m_vdpauConfig;
+ CVideoSurfaces m_videoSurfaces;
+ AVVDPAUContext m_hwContext;
+ AVCodecContext* m_avctx = nullptr;
+
+ COutput m_vdpauOutput;
+ CVdpauBufferStats m_bufferStats;
+ CEvent m_inMsgEvent;
+ CVdpauRenderPicture *m_presentPicture = nullptr;
+
+ int m_codecControl;
+ CProcessInfo& m_processInfo;
+
+ static bool m_capGeneral;
+};
+
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/VTB.cpp b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VTB.cpp
new file mode 100644
index 0000000..00ad20c
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VTB.cpp
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "VTB.h"
+
+#include "DVDCodecs/DVDCodecUtils.h"
+#include "DVDCodecs/DVDFactoryCodec.h"
+#include "DVDVideoCodec.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+#include <mutex>
+
+extern "C" {
+#include <libavcodec/videotoolbox.h>
+}
+
+using namespace VTB;
+
+//------------------------------------------------------------------------------
+// Video Buffers
+//------------------------------------------------------------------------------
+
+CVideoBufferVTB::CVideoBufferVTB(IVideoBufferPool &pool, int id)
+: CVideoBuffer(id)
+{
+ m_pFrame = av_frame_alloc();
+}
+
+CVideoBufferVTB::~CVideoBufferVTB()
+{
+ av_frame_free(&m_pFrame);
+}
+
+void CVideoBufferVTB::SetRef(AVFrame *frame)
+{
+ av_frame_unref(m_pFrame);
+ av_frame_ref(m_pFrame, frame);
+ m_pbRef = (CVPixelBufferRef)m_pFrame->data[3];
+}
+
+void CVideoBufferVTB::Unref()
+{
+ av_frame_unref(m_pFrame);
+}
+
+CVPixelBufferRef CVideoBufferVTB::GetPB()
+{
+ return m_pbRef;
+}
+
+//------------------------------------------------------------------------------
+
+class VTB::CVideoBufferPoolVTB : public IVideoBufferPool
+{
+public:
+ ~CVideoBufferPoolVTB() override;
+ void Return(int id) override;
+ CVideoBuffer* Get() override;
+
+protected:
+ CCriticalSection m_critSection;
+ std::vector<CVideoBufferVTB*> m_all;
+ std::deque<int> m_used;
+ std::deque<int> m_free;
+};
+
+CVideoBufferPoolVTB::~CVideoBufferPoolVTB()
+{
+ for (auto buf : m_all)
+ {
+ delete buf;
+ }
+}
+
+CVideoBuffer* CVideoBufferPoolVTB::Get()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CVideoBufferVTB *buf = nullptr;
+ if (!m_free.empty())
+ {
+ int idx = m_free.front();
+ m_free.pop_front();
+ m_used.push_back(idx);
+ buf = m_all[idx];
+ }
+ else
+ {
+ int id = m_all.size();
+ buf = new CVideoBufferVTB(*this, id);
+ m_all.push_back(buf);
+ m_used.push_back(id);
+ }
+
+ buf->Acquire(GetPtr());
+ return buf;
+}
+
+void CVideoBufferPoolVTB::Return(int id)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_all[id]->Unref();
+ auto it = m_used.begin();
+ while (it != m_used.end())
+ {
+ if (*it == id)
+ {
+ m_used.erase(it);
+ break;
+ }
+ else
+ ++it;
+ }
+ m_free.push_back(id);
+}
+
+//------------------------------------------------------------------------------
+// main class
+//------------------------------------------------------------------------------
+
+IHardwareDecoder* CDecoder::Create(CDVDStreamInfo &hint, CProcessInfo &processInfo, AVPixelFormat fmt)
+{
+#if defined(TARGET_DARWIN_EMBEDDED)
+ // force disable HW acceleration for live streams
+ // to avoid absent image issue on interlaced videos
+ if (processInfo.IsRealtimeStream())
+ return nullptr;
+#endif
+
+ if (fmt == AV_PIX_FMT_VIDEOTOOLBOX && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEVTB))
+ return new VTB::CDecoder(processInfo);
+
+ return nullptr;
+}
+
+bool CDecoder::Register()
+{
+ CDVDFactoryCodec::RegisterHWAccel("vtb", CDecoder::Create);
+ return true;
+}
+
+CDecoder::CDecoder(CProcessInfo& processInfo) : m_processInfo(processInfo)
+{
+ m_avctx = nullptr;
+ m_videoBufferPool = std::make_shared<CVideoBufferPoolVTB>();
+}
+
+CDecoder::~CDecoder()
+{
+ if (m_renderBuffer)
+ m_renderBuffer->Release();
+ Close();
+}
+
+void CDecoder::Close()
+{
+
+}
+
+bool CDecoder::Open(AVCodecContext *avctx, AVCodecContext* mainctx, enum AVPixelFormat fmt)
+{
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEVTB))
+ return false;
+
+ AVBufferRef *deviceRef = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VIDEOTOOLBOX);
+ AVBufferRef *framesRef = av_hwframe_ctx_alloc(deviceRef);
+ AVHWFramesContext *framesCtx = (AVHWFramesContext*)framesRef->data;
+ framesCtx->format = AV_PIX_FMT_VIDEOTOOLBOX;
+ framesCtx->sw_format = AV_PIX_FMT_NV12;
+ avctx->hw_frames_ctx = framesRef;
+ m_avctx = avctx;
+
+ m_processInfo.SetVideoDeintMethod("none");
+
+ std::list<EINTERLACEMETHOD> deintMethods;
+ deintMethods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_NONE);
+ m_processInfo.UpdateDeinterlacingMethods(deintMethods);
+
+ return true;
+}
+
+CDVDVideoCodec::VCReturn CDecoder::Decode(AVCodecContext* avctx, AVFrame* frame)
+{
+ CDVDVideoCodec::VCReturn status = Check(avctx);
+ if(status)
+ return status;
+
+ if(frame)
+ {
+ if (frame->interlaced_frame)
+ return CDVDVideoCodec::VC_FATAL;
+
+ if (m_renderBuffer)
+ m_renderBuffer->Release();
+ m_renderBuffer = dynamic_cast<CVideoBufferVTB*>(m_videoBufferPool->Get());
+ m_renderBuffer->SetRef(frame);
+ return CDVDVideoCodec::VC_PICTURE;
+ }
+ else
+ return CDVDVideoCodec::VC_BUFFER;
+}
+
+bool CDecoder::GetPicture(AVCodecContext* avctx, VideoPicture* picture)
+{
+ ((ICallbackHWAccel*)avctx->opaque)->GetPictureCommon(picture);
+
+ if (picture->videoBuffer)
+ picture->videoBuffer->Release();
+
+ picture->videoBuffer = m_renderBuffer;
+ picture->videoBuffer->Acquire();
+ return true;
+}
+
+CDVDVideoCodec::VCReturn CDecoder::Check(AVCodecContext* avctx)
+{
+ return CDVDVideoCodec::VC_NONE;
+}
+
+unsigned CDecoder::GetAllowedReferences()
+{
+ return 5;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDCodecs/Video/VTB.h b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VTB.h
new file mode 100644
index 0000000..726d680
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDCodecs/Video/VTB.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#ifdef HAS_GL
+#include <OpenGL/gl.h>
+#else
+#include <OpenGLES/ES2/gl.h>
+#endif
+
+#include "DVDVideoCodecFFmpeg.h"
+#include "cores/VideoPlayer/Buffers/VideoBuffer.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+
+#include <CoreVideo/CVPixelBuffer.h>
+
+class CProcessInfo;
+
+namespace VTB
+{
+class CVideoBufferVTB;
+class CVideoBufferPoolVTB;
+
+class CVideoBufferVTB: public CVideoBuffer
+{
+public:
+ CVideoBufferVTB(IVideoBufferPool &pool, int id);
+ ~CVideoBufferVTB() override;
+ void SetRef(AVFrame *frame);
+ void Unref();
+ CVPixelBufferRef GetPB();
+
+ GLuint m_fence = 0;
+protected:
+ CVPixelBufferRef m_pbRef = nullptr;
+ AVFrame *m_pFrame;
+};
+
+class CDecoder: public IHardwareDecoder
+{
+public:
+ CDecoder(CProcessInfo& processInfo);
+ ~CDecoder() override;
+ static IHardwareDecoder* Create(CDVDStreamInfo &hint, CProcessInfo &processInfo, AVPixelFormat fmt);
+ static bool Register();
+ bool Open(AVCodecContext* avctx, AVCodecContext* mainctx, const enum AVPixelFormat) override;
+ CDVDVideoCodec::VCReturn Decode(AVCodecContext* avctx, AVFrame* frame) override;
+ bool GetPicture(AVCodecContext* avctx, VideoPicture* picture) override;
+ CDVDVideoCodec::VCReturn Check(AVCodecContext* avctx) override;
+ const std::string Name() override { return "vtb"; }
+ unsigned GetAllowedReferences() override;
+
+ void Close();
+
+protected:
+ unsigned m_renderbuffers_count;
+ AVCodecContext *m_avctx;
+ CProcessInfo& m_processInfo;
+ CVideoBufferVTB *m_renderBuffer = nullptr;
+ std::shared_ptr<CVideoBufferPoolVTB> m_videoBufferPool;
+};
+
+}
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxSPU.cpp b/xbmc/cores/VideoPlayer/DVDDemuxSPU.cpp
new file mode 100644
index 0000000..082d858
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxSPU.cpp
@@ -0,0 +1,660 @@
+/*
+ * 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 "DVDDemuxSPU.h"
+
+#include "DVDCodecs/Overlay/DVDOverlaySpu.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/log.h"
+
+#include <locale.h>
+#include <stdlib.h>
+
+#undef ALIGN
+#define ALIGN(value, alignment) (((value)+((alignment)-1))&~((alignment)-1))
+
+// #define SPU_DEBUG
+
+void DebugLog(const char *format, ...)
+{
+#ifdef SPU_DEBUG
+ static char temp_spubuffer[1024];
+ va_list va;
+
+ va_start(va, format);
+ _vsnprintf(temp_spubuffer, 1024, format, va);
+ va_end(va);
+
+ CLog::Log(LOGDEBUG,temp_spubuffer);
+#endif
+}
+
+CDVDDemuxSPU::CDVDDemuxSPU()
+{
+ memset(&m_spuData, 0, sizeof(m_spuData));
+ memset(m_clut, 0, sizeof(m_clut));
+ m_bHasClut = false;
+}
+
+CDVDDemuxSPU::~CDVDDemuxSPU()
+{
+ free(m_spuData.data);
+}
+
+void CDVDDemuxSPU::Reset()
+{
+ FlushCurrentPacket();
+
+ // We can't reset this during playback, cause we don't always
+ // get a new clut from libdvdnav leading to invalid colors
+ // so let's just never reset it. It will only be reset
+ // when VideoPlayer is destructed and constructed
+ // m_bHasClut = false;
+ // memset(m_clut, 0, sizeof(m_clut));
+}
+
+void CDVDDemuxSPU::FlushCurrentPacket()
+{
+ free(m_spuData.data);
+ memset(&m_spuData, 0, sizeof(m_spuData));
+}
+
+CDVDOverlaySpu* CDVDDemuxSPU::AddData(uint8_t* data, int iSize, double pts)
+{
+ SPUData* pSPUData = &m_spuData;
+
+ if (pSPUData->iNeededSize > 0 &&
+ (pSPUData->iSize != pSPUData->iNeededSize) &&
+ ((pSPUData->iSize + iSize) > pSPUData->iNeededSize))
+ {
+ DebugLog("corrupt spu data: packet does not fit");
+ m_spuData.iNeededSize = 0;
+ m_spuData.iSize = 0;
+ return NULL;
+ }
+
+ // check if we are about to start a new packet
+ if (pSPUData->iSize == pSPUData->iNeededSize)
+ {
+ // for now we don't delete the memory associated with m_spuData.data
+ pSPUData->iSize = 0;
+
+ // check spu data length, only needed / possible in the first spu packet
+ uint16_t length = data[0] << 8 | data[1];
+ if (length == 0)
+ {
+ DebugLog("corrupt spu data: zero packet");
+ m_spuData.iNeededSize = 0;
+ m_spuData.iSize = 0;
+ return NULL;
+ }
+ if (length > iSize) pSPUData->iNeededSize = length;
+ else pSPUData->iNeededSize = iSize;
+
+ // set presentation time stamp
+ pSPUData->pts = pts;
+ }
+
+ // allocate data if not already done ( done in blocks off 16384 bytes )
+ // or allocate some more if 16384 bytes is not enough
+ if ((pSPUData->iSize + iSize) > pSPUData->iAllocatedSize)
+ {
+ uint8_t* tmpptr = (uint8_t*)realloc(pSPUData->data, ALIGN(pSPUData->iSize + iSize, 0x4000));
+ if (!tmpptr)
+ {
+ free(pSPUData->data);
+ return NULL;
+ }
+ pSPUData->data = tmpptr;
+ }
+
+ if(!pSPUData->data)
+ return NULL; // crap realloc failed, this will have leaked some memory due to odd realloc
+
+ // add new data
+ memcpy(pSPUData->data + pSPUData->iSize, data, iSize);
+ pSPUData->iSize += iSize;
+
+ if (pSPUData->iNeededSize - pSPUData->iSize == 1) // to make it even
+ {
+ DebugLog("missing 1 byte to complete packet, adding 0xff");
+
+ pSPUData->data[pSPUData->iSize] = 0xff;
+ pSPUData->iSize++;
+ }
+
+ if (pSPUData->iSize == pSPUData->iNeededSize)
+ {
+ DebugLog("got complete spu packet\n length: %i bytes\n stream: %i\n", pSPUData->iSize);
+
+ return ParsePacket(pSPUData);
+ }
+
+ return NULL;
+}
+
+#define CMD_END 0xFF
+#define FSTA_DSP 0x00
+#define STA_DSP 0x01
+#define STP_DSP 0x02
+#define SET_COLOR 0x03
+#define SET_CONTR 0x04
+#define SET_DAREA 0x05
+#define SET_DSPXA 0x06
+#define CHG_COLCON 0x07
+
+CDVDOverlaySpu* CDVDDemuxSPU::ParsePacket(SPUData* pSPUData)
+{
+ unsigned int alpha[4];
+ uint8_t* pUnparsedData = NULL;
+
+ if (pSPUData->iNeededSize != pSPUData->iSize)
+ {
+ DebugLog("GetPacket, packet is incomplete, missing: %i bytes", (pSPUData->iNeededSize - pSPUData->iSize));
+ }
+
+ if (pSPUData->data[pSPUData->iSize - 1] != 0xff)
+ {
+ DebugLog("GetPacket, missing end of data 0xff");
+ }
+
+ CDVDOverlaySpu* pSPUInfo = new CDVDOverlaySpu();
+ uint8_t* p = pSPUData->data; // pointer to walk through all data
+
+ // get data length
+ uint16_t datalength = p[2] << 8 | p[3]; // datalength + 4 control bytes
+
+ pUnparsedData = pSPUData->data + 4;
+
+ // if it is set to 0 it means it's a menu overlay by default
+ // this is not what we want too, cause you get strange results on a parse error
+ pSPUInfo->iPTSStartTime = -1;
+
+ //skip data packet and goto control sequence
+ p += datalength;
+
+ bool bHasNewDCSQ = true;
+ while (bHasNewDCSQ)
+ {
+ DebugLog(" starting new SP_DCSQT");
+ // p is beginning of first SP_DCSQT now
+ uint16_t delay = p[0] << 8 | p[1];
+ uint16_t next_DCSQ = p[2] << 8 | p[3];
+
+ //offset within the Sub-Picture Unit to the next SP_DCSQ. If this is the last SP_DCSQ, it points to itself.
+ bHasNewDCSQ = ((pSPUData->data + next_DCSQ) != p);
+ // skip 4 bytes
+ p += 4;
+
+ while (*p != CMD_END && (unsigned int)(p - pSPUData->data) <= pSPUData->iSize)
+ {
+ switch (*p)
+ {
+ case FSTA_DSP:
+ p++;
+ DebugLog(" GetPacket, FSTA_DSP: Forced Start Display, no arguments");
+ pSPUInfo->iPTSStartTime = pSPUData->pts;
+ pSPUInfo->iPTSStopTime = 0x9000000000000LL;
+ pSPUInfo->bForced = true;
+ // delay is always 0, the VideoPlayer should decide when to display the packet (menu highlight)
+ break;
+ case STA_DSP:
+ {
+ p++;
+ pSPUInfo->iPTSStartTime = pSPUData->pts;
+ pSPUInfo->iPTSStartTime += (double)delay * 1024 * DVD_TIME_BASE / 90000;
+ DebugLog(" GetPacket, STA_DSP: Start Display, delay: %i", ((delay * 1024) / 90000));
+ }
+ break;
+ case STP_DSP:
+ {
+ p++;
+ pSPUInfo->iPTSStopTime = pSPUData->pts;
+ pSPUInfo->iPTSStopTime += (double)delay * 1024 * DVD_TIME_BASE / 90000;
+ DebugLog(" GetPacket, STP_DSP: Stop Display, delay: %i", ((delay * 1024) / 90000));
+ }
+ break;
+ case SET_COLOR:
+ {
+ p++;
+
+ if (m_bHasClut)
+ {
+ pSPUInfo->bHasColor = true;
+
+ unsigned int idx[4];
+ // 0, 1, 2, 3
+ idx[0] = (p[0] >> 4) & 0x0f;
+ idx[1] = (p[0]) & 0x0f;
+ idx[2] = (p[1] >> 4) & 0x0f;
+ idx[3] = (p[1]) & 0x0f;
+
+ for (int i = 0; i < 4 ; i++) // emphasis 1, emphasis 2, pattern, back ground
+ {
+ uint8_t* iColor = m_clut[idx[i]];
+
+ pSPUInfo->color[3 - i][0] = iColor[0]; // Y
+ pSPUInfo->color[3 - i][1] = iColor[1]; // Cr
+ pSPUInfo->color[3 - i][2] = iColor[2]; // Cb
+ }
+ }
+
+ DebugLog(" GetPacket, SET_COLOR:");
+ p += 2;
+ }
+ break;
+ case SET_CONTR: // alpha
+ {
+ p++;
+ // 3, 2, 1, 0
+ alpha[0] = (p[0] >> 4) & 0x0f;
+ alpha[1] = (p[0]) & 0x0f;
+ alpha[2] = (p[1] >> 4) & 0x0f;
+ alpha[3] = (p[1]) & 0x0f;
+
+ // Ignore blank alpha palette.
+ if (alpha[0] | alpha[1] | alpha[2] | alpha[3])
+ {
+ pSPUInfo->bHasAlpha = true;
+
+ // 0, 1, 2, 3
+ pSPUInfo->alpha[0] = alpha[3]; //0 // background, should be hidden
+ pSPUInfo->alpha[1] = alpha[2]; //1
+ pSPUInfo->alpha[2] = alpha[1]; //2 // wm button overlay
+ pSPUInfo->alpha[3] = alpha[0]; //3
+ }
+
+ DebugLog(" GetPacket, SET_CONTR:");
+ p += 2;
+ }
+ break;
+ case SET_DAREA:
+ {
+ p++;
+ pSPUInfo->x = (p[0] << 4) | (p[1] >> 4);
+ pSPUInfo->y = (p[3] << 4) | (p[4] >> 4);
+ pSPUInfo->width = (((p[1] & 0x0f) << 8) | p[2]) - pSPUInfo->x + 1;
+ pSPUInfo->height = (((p[4] & 0x0f) << 8) | p[5]) - pSPUInfo->y + 1;
+ DebugLog(" GetPacket, SET_DAREA: x,y:%i,%i width,height:%i,%i",
+ pSPUInfo->x, pSPUInfo->y, pSPUInfo->width, pSPUInfo->height);
+ p += 6;
+ }
+ break;
+ case SET_DSPXA:
+ {
+ p++;
+ uint16_t tfaddr = (p[0] << 8 | p[1]); // offset in packet
+ uint16_t bfaddr = (p[2] << 8 | p[3]); // offset in packet
+ pSPUInfo->pTFData = (tfaddr - 4); //pSPUInfo->pData + (tfaddr - 4); // pSPUData->data = packet startaddr - 4
+ pSPUInfo->pBFData = (bfaddr - 4); //pSPUInfo->pData + (bfaddr - 4); // pSPUData->data = packet startaddr - 4
+ p += 4;
+ DebugLog(" GetPacket, SET_DSPXA: tf: %i bf: %i ", tfaddr, bfaddr);
+ }
+ break;
+ case CHG_COLCON:
+ {
+ p++;
+ uint16_t paramlength = p[0] << 8 | p[1];
+ DebugLog("GetPacket, CHG_COLCON, skippin %i bytes", paramlength);
+ p += paramlength;
+ }
+ break;
+
+ default:
+ DebugLog("GetPacket, error parsing control sequence");
+ delete pSPUInfo;
+ return NULL;
+ break;
+ }
+ }
+ DebugLog(" end off SP_DCSQT");
+ if (*p == CMD_END) p++;
+ else
+ {
+ DebugLog("GetPacket, end off SP_DCSQT, but did not found 0xff (CMD_END)");
+ }
+ }
+
+ // parse the rle.
+ // this should be changed so it get's converted to a yuv overlay
+ return ParseRLE(pSPUInfo, pUnparsedData);
+}
+
+/*****************************************************************************
+ * AddNibble: read a nibble from a source packet and add it to our integer.
+ *****************************************************************************/
+inline unsigned int AddNibble(unsigned int i_code, const uint8_t* p_src, unsigned int* pi_index)
+{
+ if ( *pi_index & 0x1 )
+ {
+ return ( i_code << 4 | ( p_src[(*pi_index)++ >> 1] & 0xf ) );
+ }
+ else
+ {
+ return ( i_code << 4 | p_src[(*pi_index)++ >> 1] >> 4 );
+ }
+}
+
+/*****************************************************************************
+ * ParseRLE: parse the RLE part of the subtitle
+ *****************************************************************************
+ * This part parses the subtitle graphical data and stores it in a more
+ * convenient structure for later decoding. For more information on the
+ * subtitles format, see http://sam.zoy.org/doc/dvd/subtitles/index.html
+ *****************************************************************************/
+CDVDOverlaySpu* CDVDDemuxSPU::ParseRLE(CDVDOverlaySpu* pSPU, uint8_t* pUnparsedData)
+{
+ uint8_t* p_src = pUnparsedData;
+
+ unsigned int i_code = 0;
+
+ unsigned int i_width = pSPU->width;
+ unsigned int i_height = pSPU->height;
+ unsigned int i_x, i_y;
+
+ // allocate a buffer for the result
+ uint16_t* p_dest = (uint16_t*)pSPU->result;
+
+ /* The subtitles are interlaced, we need two offsets */
+ unsigned int i_id = 0; /* Start on the even SPU layer */
+ unsigned int pi_table[2];
+
+ /* Colormap statistics */
+ int i_border = -1;
+ int stats[4]; stats[0] = stats[1] = stats[2] = stats[3] = 0;
+
+ pi_table[ 0 ] = pSPU->pTFData << 1;
+ pi_table[ 1 ] = pSPU->pBFData << 1;
+
+ for ( i_y = 0 ; i_y < i_height ; i_y++ )
+ {
+ unsigned int *pi_offset = pi_table + i_id;
+
+ for ( i_x = 0 ; i_x < i_width ; i_x += i_code >> 2 )
+ {
+ i_code = AddNibble( 0, p_src, pi_offset );
+
+ if ( i_code < 0x04 )
+ {
+ i_code = AddNibble( i_code, p_src, pi_offset );
+
+ if ( i_code < 0x10 )
+ {
+ i_code = AddNibble( i_code, p_src, pi_offset );
+
+ if ( i_code < 0x040 )
+ {
+ i_code = AddNibble( i_code, p_src, pi_offset );
+
+ if ( i_code < 0x0100 )
+ {
+ /* If the 14 first bits are set to 0, then it's a
+ * new line. We emulate it. */
+ if ( i_code < 0x0004 )
+ {
+ i_code |= ( i_width - i_x ) << 2;
+ }
+ else
+ {
+ /* We have a boo boo ! */
+ CLog::Log(LOGERROR, "ParseRLE: unknown RLE code {:#4x}", i_code);
+ pSPU->Release();
+ return NULL;
+ }
+ }
+ }
+ }
+ }
+
+ if ( ( (i_code >> 2) + i_x + i_y * i_width ) > i_height * i_width )
+ {
+ CLog::Log(LOGERROR, "ParseRLE: out of bounds, {} at ({},{}) is out of {}x{}", i_code >> 2,
+ i_x, i_y, i_width, i_height);
+ pSPU->Release();
+ return NULL;
+ }
+
+ // keep trace of all occurring pixels, even keeping the background in mind
+ stats[i_code & 0x3] += i_code >> 2;
+
+ // count the number of pixels for every occurring parts, without background
+ if (pSPU->alpha[i_code & 0x3] != 0x00)
+ {
+ // the last non background pixel is probably the border color
+ i_border = i_code & 0x3;
+ stats[i_border] += i_code >> 2;
+ }
+
+ /* Check we aren't overwriting our data range
+ This occurs on "The Triplets of BelleVille" region 4 disk (NTSC)"
+ where we use around 96k rather than 64k + 20bytes */
+ if ((uint8_t *)p_dest >= pSPU->result + sizeof(pSPU->result))
+ {
+ CLog::Log(LOGERROR, "ParseRLE: Overrunning our data range. Need {} bytes",
+ (long)((uint8_t*)p_dest - pSPU->result));
+ pSPU->Release();
+ return NULL;
+ }
+ *p_dest++ = i_code;
+ }
+
+ /* Check that we didn't go too far */
+ if ( i_x > i_width )
+ {
+ CLog::Log(LOGERROR, "ParseRLE: i_x overflowed, {} > {}", i_x, i_width);
+ pSPU->Release();
+ return NULL;
+ }
+
+ /* Byte-align the stream */
+ if ( *pi_offset & 0x1 )
+ {
+ (*pi_offset)++;
+ }
+
+ /* Swap fields */
+ i_id = ~i_id & 0x1;
+ }
+
+ /* We shouldn't get any padding bytes */
+ if ( i_y < i_height )
+ {
+ DebugLog("ParseRLE: padding bytes found in RLE sequence" );
+ DebugLog("ParseRLE: send mail to <sam@zoy.org> if you want to help debugging this" );
+
+ /* Skip them just in case */
+ while ( i_y < i_height )
+ {
+ /* Check we aren't overwriting our data range
+ This occurs on "The Triplets of BelleVille" region 4 disk (NTSC)"
+ where we use around 96k rather than 64k + 20bytes */
+ if ((uint8_t *)p_dest >= pSPU->result + sizeof(pSPU->result))
+ {
+ CLog::Log(LOGERROR, "ParseRLE: Overrunning our data range. Need {} bytes",
+ (long)((uint8_t*)p_dest - pSPU->result));
+ pSPU->Release();
+ return NULL;
+ }
+ *p_dest++ = i_width << 2;
+ i_y++;
+ }
+
+ pSPU->Release();
+ return NULL;
+ }
+
+ DebugLog("ParseRLE: valid subtitle, size: %ix%i, position: %i,%i",
+ pSPU->width, pSPU->height, pSPU->x, pSPU->y );
+
+ // forced spu's (menu overlays) retrieve their alpha/color information from InputStreamNavigator::GetCurrentButtonInfo
+ // also they may contain completely covering data which is supposed to be hidden normally
+ // since whole spu is drawn, if this is done for forced, that may be displayed
+ // so we must trust what is given
+ if( !pSPU->bForced )
+ {
+ // Handle color if no palette was found.
+ // we only set it if there is a valid i_border color
+ if (!pSPU->bHasColor)
+ {
+ CLog::Log(LOGINFO, "{} - no color palette found, using default", __FUNCTION__);
+ FindSubtitleColor(i_border, stats, pSPU);
+ }
+
+ // check alpha values, for non forced spu's we use a default value
+ if (pSPU->bHasAlpha)
+ {
+ // check alpha values
+ // the array stats represents the nr of pixels for each color channel
+ // thus if there are no pixels to display, we assume the alphas are incorrect.
+ if (!CanDisplayWithAlphas(pSPU->alpha, stats))
+ {
+ CLog::Log(LOGINFO, "{} - no matching color and alpha found, resetting alpha",
+ __FUNCTION__);
+
+ pSPU->alpha[0] = 0x00; // back ground
+ pSPU->alpha[1] = 0x0f;
+ pSPU->alpha[2] = 0x0f;
+ pSPU->alpha[3] = 0x0f;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "{} - ignoring blank alpha palette, using default", __FUNCTION__);
+
+ pSPU->alpha[0] = 0x00; // back ground
+ pSPU->alpha[1] = 0x0f;
+ pSPU->alpha[2] = 0x0f;
+ pSPU->alpha[3] = 0x0f;
+ }
+
+ }
+
+ return pSPU;
+}
+
+void CDVDDemuxSPU::FindSubtitleColor(int last_color, int stats[4], CDVDOverlaySpu* pSPU)
+{
+ const int COLOR_INNER = 0;
+ const int COLOR_SHADE = 1;
+ const int COLOR_BORDER = 2;
+
+ //uint8_t custom_subtitle_color[4][3] = { // blue, yellow and something else (xine)
+ // { 0x80, 0x90, 0x80 }, // inner color
+ // { 0x00, 0x90, 0x00 }, // shade color
+ // { 0x00, 0x90, 0xff } // border color
+ //};
+
+ uint8_t custom_subtitle_color[4][3] = { // inner color white, gray shading and a black border
+ { 0xff, 0x80, 0x80 }, // inner color, white
+ { 0x80, 0x80, 0x80 }, // shade color, gray
+ { 0x00, 0x80, 0x80 } // border color, black
+ };
+
+ //uint8_t custom_subtitle_color[4][3] = { // completely white and a black border
+ // { 0xff, 0x80, 0x80 }, // inner color, white
+ // { 0xff, 0x80, 0x80 }, // shade color, white
+ // { 0x00, 0x80, 0x80 } // border color, black
+ //};
+
+
+ int nrOfUsedColors = 0;
+ for (int alpha : pSPU->alpha)
+ {
+ if (alpha > 0) nrOfUsedColors++;
+ }
+
+ if (nrOfUsedColors == 0)
+ {
+ // nothing todo
+ DebugLog("FindSubtitleColor: all 4 alpha channels are 0, nothing todo");
+ }
+ else if (nrOfUsedColors == 1)
+ {
+ // only one color is used, probably the inner color
+ for (int i = 0; i < 4; i++) // find the position that is used
+ {
+ if (pSPU->alpha[i] > 0)
+ {
+ pSPU->color[i][0] = custom_subtitle_color[COLOR_INNER][0]; // Y
+ pSPU->color[i][1] = custom_subtitle_color[COLOR_INNER][1]; // Cr ?
+ pSPU->color[i][2] = custom_subtitle_color[COLOR_INNER][2]; // Cb ?
+ return;
+ }
+ }
+
+ }
+ else
+ {
+ // old code
+
+ if (last_color >= 0 && last_color < 4)
+ {
+ int i, i_inner = -1, i_shade = -1;
+ // Set the border color, the last color is probably the border color
+ pSPU->color[last_color][0] = custom_subtitle_color[COLOR_BORDER][0];
+ pSPU->color[last_color][1] = custom_subtitle_color[COLOR_BORDER][1];
+ pSPU->color[last_color][2] = custom_subtitle_color[COLOR_BORDER][2];
+ stats[last_color] = 0;
+
+ // find the inner colors
+ for ( i = 0 ; i < 4 && i_inner == -1 ; i++ )
+ {
+ if ( stats[i] )
+ {
+ i_inner = i;
+ }
+ }
+
+ // try to find the shade color
+ for ( ; i < 4 && i_shade == -1 ; i++)
+ {
+ if ( stats[i] )
+ {
+ if ( stats[i] > stats[i_inner] )
+ {
+ i_shade = i_inner;
+ i_inner = i;
+ }
+ else
+ {
+ i_shade = i;
+ }
+ }
+ }
+
+ /* Set the inner color */
+ if ( i_inner != -1 )
+ {
+ // white color
+ pSPU->color[i_inner][0] = custom_subtitle_color[COLOR_INNER][0]; // Y
+ pSPU->color[i_inner][1] = custom_subtitle_color[COLOR_INNER][1]; // Cr ?
+ pSPU->color[i_inner][2] = custom_subtitle_color[COLOR_INNER][2]; // Cb ?
+ }
+
+ /* Set the anti-aliasing color */
+ if ( i_shade != -1 )
+ {
+ // gray
+ pSPU->color[i_shade][0] = custom_subtitle_color[COLOR_SHADE][0];
+ pSPU->color[i_shade][1] = custom_subtitle_color[COLOR_SHADE][1];
+ pSPU->color[i_shade][2] = custom_subtitle_color[COLOR_SHADE][2];
+ }
+
+ DebugLog("ParseRLE: using custom palette (border %i, inner %i, shade %i)", last_color, i_inner, i_shade);
+ }
+ }
+}
+
+bool CDVDDemuxSPU::CanDisplayWithAlphas(const int a[4], const int stats[4])
+{
+ return(
+ a[0] * stats[0] > 0 ||
+ a[1] * stats[1] > 0 ||
+ a[2] * stats[2] > 0 ||
+ a[3] * stats[3] > 0);
+}
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxSPU.h b/xbmc/cores/VideoPlayer/DVDDemuxSPU.h
new file mode 100644
index 0000000..8f1bcc1
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxSPU.h
@@ -0,0 +1,54 @@
+/*
+ * 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 <stdint.h>
+
+struct AVFrame;
+class CDVDOverlaySpu;
+
+typedef struct SPUData
+{
+ uint8_t* data;
+ unsigned int iSize; // current data size
+ unsigned int iNeededSize; // wanted packet size
+ unsigned int iAllocatedSize;
+ double pts;
+}
+SPUData;
+
+// upto 32 streams can exist
+#define DVD_MAX_SPUSTREAMS 32
+
+class CDVDDemuxSPU final
+{
+public:
+ CDVDDemuxSPU();
+ ~CDVDDemuxSPU();
+
+ CDVDOverlaySpu* AddData(uint8_t* data, int iSize, double pts); // returns a packet from ParsePacket if possible
+
+ CDVDOverlaySpu* ParseRLE(CDVDOverlaySpu* pSPU, uint8_t* pUnparsedData);
+ static void FindSubtitleColor(int last_color, int stats[4], CDVDOverlaySpu* pSPU);
+ static bool CanDisplayWithAlphas(const int a[4], const int stats[4]);
+
+ void Reset();
+ void FlushCurrentPacket(); // flushes current unparsed data
+
+ // m_clut set by libdvdnav once in a time
+ // color lookup table is representing 16 different yuv colors
+ // [][0] = Y, [][1] = Cr, [][2] = Cb
+ uint8_t m_clut[16][3];
+ bool m_bHasClut;
+
+protected:
+ CDVDOverlaySpu* ParsePacket(SPUData* pSPUData);
+
+ SPUData m_spuData;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/CMakeLists.txt b/xbmc/cores/VideoPlayer/DVDDemuxers/CMakeLists.txt
new file mode 100644
index 0000000..48710f8
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/CMakeLists.txt
@@ -0,0 +1,23 @@
+set(SOURCES DemuxMultiSource.cpp
+ DVDDemux.cpp
+ DVDDemuxBXA.cpp
+ DVDDemuxCC.cpp
+ DVDDemuxCDDA.cpp
+ DVDDemuxClient.cpp
+ DVDDemuxFFmpeg.cpp
+ DVDDemuxUtils.cpp
+ DVDDemuxVobsub.cpp
+ DVDFactoryDemuxer.cpp)
+
+set(HEADERS DemuxMultiSource.h
+ DVDDemux.h
+ DVDDemuxBXA.h
+ DVDDemuxCC.h
+ DVDDemuxCDDA.h
+ DVDDemuxClient.h
+ DVDDemuxFFmpeg.h
+ DVDDemuxUtils.h
+ DVDDemuxVobsub.h
+ DVDFactoryDemuxer.h)
+
+core_add_library(dvddemuxers)
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemux.cpp b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemux.cpp
new file mode 100644
index 0000000..c2ac33f
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemux.cpp
@@ -0,0 +1,96 @@
+/*
+ * 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 "DVDDemux.h"
+
+std::string CDemuxStreamAudio::GetStreamType()
+{
+ std::string strInfo;
+ switch (codec)
+ {
+ case AV_CODEC_ID_AC3:
+ strInfo = "AC3 ";
+ break;
+ case AV_CODEC_ID_EAC3:
+ strInfo = "DD+ ";
+ break;
+ case AV_CODEC_ID_DTS:
+ {
+ switch (profile)
+ {
+ case FF_PROFILE_DTS_HD_MA:
+ strInfo = "DTS-HD MA ";
+ break;
+ case FF_PROFILE_DTS_HD_HRA:
+ strInfo = "DTS-HD HRA ";
+ break;
+ default:
+ strInfo = "DTS ";
+ break;
+ }
+ break;
+ }
+ case AV_CODEC_ID_MP2:
+ strInfo = "MP2 ";
+ break;
+ case AV_CODEC_ID_MP3:
+ strInfo = "MP3 ";
+ break;
+ case AV_CODEC_ID_TRUEHD:
+ strInfo = "TrueHD ";
+ break;
+ case AV_CODEC_ID_AAC:
+ strInfo = "AAC ";
+ break;
+ case AV_CODEC_ID_ALAC:
+ strInfo = "ALAC ";
+ break;
+ case AV_CODEC_ID_FLAC:
+ strInfo = "FLAC ";
+ break;
+ case AV_CODEC_ID_OPUS:
+ strInfo = "Opus ";
+ break;
+ case AV_CODEC_ID_VORBIS:
+ strInfo = "Vorbis ";
+ break;
+ case AV_CODEC_ID_PCM_BLURAY:
+ case AV_CODEC_ID_PCM_DVD:
+ strInfo = "PCM ";
+ break;
+ default:
+ strInfo = "";
+ break;
+ }
+
+ strInfo += m_channelLayoutName;
+
+ return strInfo;
+}
+
+int CDVDDemux::GetNrOfStreams(StreamType streamType)
+{
+ int iCounter = 0;
+
+ for (auto pStream : GetStreams())
+ {
+ if (pStream && pStream->type == streamType) iCounter++;
+ }
+
+ return iCounter;
+}
+
+int CDVDDemux::GetNrOfSubtitleStreams()
+{
+ return GetNrOfStreams(STREAM_SUBTITLE);
+}
+
+std::string CDemuxStream::GetStreamName()
+{
+ return name;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemux.h b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemux.h
new file mode 100644
index 0000000..f4f7161
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemux.h
@@ -0,0 +1,381 @@
+/*
+ * 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 "Interface/StreamInfo.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+struct DemuxPacket;
+struct DemuxCryptoSession;
+
+class CDVDInputStream;
+
+namespace ADDON
+{
+class IAddonProvider;
+}
+
+#ifndef __GNUC__
+#pragma warning(push)
+#pragma warning(disable : 4244)
+#endif
+
+extern "C"
+{
+#include <libavcodec/avcodec.h>
+#include <libavutil/mastering_display_metadata.h>
+}
+
+#ifndef __GNUC__
+#pragma warning(pop)
+#endif
+
+enum StreamType
+{
+ STREAM_NONE = 0, // if unknown
+ STREAM_AUDIO, // audio stream
+ STREAM_VIDEO, // video stream
+ STREAM_DATA, // data stream
+ STREAM_SUBTITLE, // subtitle stream
+ STREAM_TELETEXT, // Teletext data stream
+ STREAM_RADIO_RDS, // Radio RDS data stream
+ STREAM_AUDIO_ID3 // Audio ID3 data stream
+};
+
+enum StreamSource
+{
+ STREAM_SOURCE_NONE = 0x000,
+ STREAM_SOURCE_DEMUX = 0x100,
+ STREAM_SOURCE_NAV = 0x200,
+ STREAM_SOURCE_DEMUX_SUB = 0x300,
+ STREAM_SOURCE_TEXT = 0x400,
+ STREAM_SOURCE_VIDEOMUX = 0x500
+};
+
+#define STREAM_SOURCE_MASK(a) ((a)&0xf00)
+
+/*
+ * CDemuxStream
+ * Base class for all demuxer streams
+ */
+class CDemuxStream
+{
+public:
+ CDemuxStream()
+ {
+ uniqueId = 0;
+ dvdNavId = 0;
+ demuxerId = -1;
+ codec_fourcc = 0;
+ profile = FF_PROFILE_UNKNOWN;
+ level = FF_LEVEL_UNKNOWN;
+ type = STREAM_NONE;
+ source = STREAM_SOURCE_NONE;
+ iDuration = 0;
+ pPrivate = NULL;
+ ExtraSize = 0;
+ disabled = false;
+ changes = 0;
+ flags = StreamFlags::FLAG_NONE;
+ }
+
+ virtual ~CDemuxStream() = default;
+ CDemuxStream(CDemuxStream&&) = default;
+
+ virtual std::string GetStreamName();
+
+ int uniqueId; // unique stream id
+ int dvdNavId;
+ int64_t demuxerId; // id of the associated demuxer
+ AVCodecID codec = AV_CODEC_ID_NONE;
+ unsigned int codec_fourcc; // if available
+ int profile; // encoder profile of the stream reported by the decoder. used to qualify hw decoders.
+ int level; // encoder level of the stream reported by the decoder. used to qualify hw decoders.
+ StreamType type;
+ int source;
+
+ int iDuration; // in mseconds
+ void* pPrivate; // private pointer for the demuxer
+ std::unique_ptr<uint8_t[]> ExtraData; // extra data for codec to use
+ unsigned int ExtraSize; // size of extra data
+
+ StreamFlags flags;
+ std::string language; // RFC 5646 language code (empty string if undefined)
+ bool disabled; // set when stream is disabled. (when no decoder exists)
+
+ std::string name;
+ std::string codecName;
+
+ int changes; // increment on change which player may need to know about
+
+ std::shared_ptr<DemuxCryptoSession> cryptoSession;
+ std::shared_ptr<ADDON::IAddonProvider> externalInterfaces;
+};
+
+class CDemuxStreamVideo : public CDemuxStream
+{
+public:
+ CDemuxStreamVideo() { type = STREAM_VIDEO; }
+
+ ~CDemuxStreamVideo() override = default;
+ int iFpsScale = 0; // scale of 1000 and a rate of 29970 will result in 29.97 fps
+ int iFpsRate = 0;
+ int iHeight = 0; // height of the stream reported by the demuxer
+ int iWidth = 0; // width of the stream reported by the demuxer
+ double fAspect = 0; // display aspect of stream
+ bool bVFR = false; // variable framerate
+ bool bPTSInvalid = false; // pts cannot be trusted (avi's).
+ bool bForcedAspect = false; // aspect is forced from container
+ int iOrientation = 0; // orientation of the video in degrees counter clockwise
+ int iBitsPerPixel = 0;
+ int iBitRate = 0;
+ int bitDepth = 0;
+
+ AVColorSpace colorSpace = AVCOL_SPC_UNSPECIFIED;
+ AVColorRange colorRange = AVCOL_RANGE_UNSPECIFIED;
+ AVColorPrimaries colorPrimaries = AVCOL_PRI_UNSPECIFIED;
+ AVColorTransferCharacteristic colorTransferCharacteristic = AVCOL_TRC_UNSPECIFIED;
+
+ std::shared_ptr<AVMasteringDisplayMetadata> masteringMetaData;
+ std::shared_ptr<AVContentLightMetadata> contentLightMetaData;
+
+ std::string stereo_mode; // expected stereo mode
+ StreamHdrType hdr_type = StreamHdrType::HDR_TYPE_NONE; // type of HDR for this stream (hdr10, etc)
+};
+
+class CDemuxStreamAudio : public CDemuxStream
+{
+public:
+ CDemuxStreamAudio()
+ : CDemuxStream()
+ {
+ iChannels = 0;
+ iSampleRate = 0;
+ iBlockAlign = 0;
+ iBitRate = 0;
+ iBitsPerSample = 0;
+ iChannelLayout = 0;
+ type = STREAM_AUDIO;
+ }
+
+ ~CDemuxStreamAudio() override = default;
+
+ std::string GetStreamType();
+
+ int iChannels;
+ int iSampleRate;
+ int iBlockAlign;
+ int iBitRate;
+ int iBitsPerSample;
+ uint64_t iChannelLayout;
+ std::string m_channelLayoutName;
+};
+
+class CDemuxStreamSubtitle : public CDemuxStream
+{
+public:
+ CDemuxStreamSubtitle()
+ : CDemuxStream()
+ {
+ type = STREAM_SUBTITLE;
+ }
+};
+
+class CDemuxStreamTeletext : public CDemuxStream
+{
+public:
+ CDemuxStreamTeletext()
+ : CDemuxStream()
+ {
+ type = STREAM_TELETEXT;
+ }
+};
+
+class CDemuxStreamAudioID3 : public CDemuxStream
+{
+public:
+ CDemuxStreamAudioID3() : CDemuxStream() { type = STREAM_AUDIO_ID3; }
+};
+
+class CDemuxStreamRadioRDS : public CDemuxStream
+{
+public:
+ CDemuxStreamRadioRDS()
+ : CDemuxStream()
+ {
+ type = STREAM_RADIO_RDS;
+ }
+};
+
+class CDVDDemux
+{
+public:
+ CDVDDemux()
+ : m_demuxerId(NewGuid())
+ {
+ }
+ virtual ~CDVDDemux() = default;
+
+
+ /*
+ * Reset the entire demuxer (same result as closing and opening it)
+ */
+ virtual bool Reset() = 0;
+
+ /*
+ * Aborts any internal reading that might be stalling main thread
+ * NOTICE - this can be called from another thread
+ */
+ virtual void Abort() {}
+
+ /*
+ * Flush the demuxer, if any data is kept in buffers, this should be freed now
+ */
+ virtual void Flush() = 0;
+
+ /*
+ * Read a packet, returns NULL on error
+ *
+ */
+ virtual DemuxPacket* Read() = 0;
+
+ /*
+ * Seek, time in msec calculated from stream start
+ */
+ virtual bool SeekTime(double time, bool backwards = false, double* startpts = NULL) = 0;
+
+ /*
+ * Seek to a specified chapter.
+ * startpts can be updated to the point where display should start
+ */
+ virtual bool SeekChapter(int chapter, double* startpts = NULL) { return false; }
+
+ /*
+ * Get the number of chapters available
+ */
+ virtual int GetChapterCount() { return 0; }
+
+ /*
+ * Get current chapter
+ */
+ virtual int GetChapter() { return 0; }
+
+ /*
+ * Get the name of a chapter
+ * \param strChapterName[out] Name of chapter
+ * \param chapterIdx -1 for current chapter, else a chapter index
+ */
+ virtual void GetChapterName(std::string& strChapterName, int chapterIdx = -1) {}
+
+ /*
+ * Get the position of a chapter
+ * \param chapterIdx -1 for current chapter, else a chapter index
+ */
+ virtual int64_t GetChapterPos(int chapterIdx = -1) { return 0; }
+
+ /*
+ * Set the playspeed, if demuxer can handle different
+ * speeds of playback
+ */
+ virtual void SetSpeed(int iSpeed) {}
+
+ /*
+ * Let demuxer know if we want to fill demux queue
+ */
+ virtual void FillBuffer(bool mode) {}
+
+ /*
+ * returns the total time in msec
+ */
+ virtual int GetStreamLength() { return 0; }
+
+ /*
+ * returns the stream or NULL on error
+ */
+ virtual CDemuxStream* GetStream(int64_t demuxerId, int iStreamId) const
+ {
+ return GetStream(iStreamId);
+ };
+
+ virtual std::vector<CDemuxStream*> GetStreams() const = 0;
+
+ /*
+ * return nr of streams, 0 if none
+ */
+ virtual int GetNrOfStreams() const = 0;
+
+ /*
+ * get a list of available programs
+ */
+ virtual int GetPrograms(std::vector<ProgramInfo>& programs) { return 0; }
+
+ /*
+ * select programs
+ */
+ virtual void SetProgram(int progId) {}
+
+ /*
+ * returns opened filename
+ */
+ virtual std::string GetFileName() { return ""; }
+
+ /*
+ * return nr of subtitle streams, 0 if none
+ */
+ int GetNrOfSubtitleStreams();
+
+ /*
+ * return a user-presentable codec name of the given stream
+ */
+ virtual std::string GetStreamCodecName(int64_t demuxerId, int iStreamId)
+ {
+ return GetStreamCodecName(iStreamId);
+ };
+
+ /*
+ * enable / disable demux stream
+ */
+ virtual void EnableStream(int64_t demuxerId, int id, bool enable) { EnableStream(id, enable); }
+
+ /*
+ * implicitly enable and open a demux stream for playback
+ */
+ virtual void OpenStream(int64_t demuxerId, int id) { OpenStream(id); }
+
+ /*
+ * sets desired width / height for video stream
+ * adaptive demuxers like DASH can use this to choose best fitting video stream
+ */
+ virtual void SetVideoResolution(unsigned int width, unsigned int height) {}
+
+ /*
+ * return the id of the demuxer
+ */
+ int64_t GetDemuxerId() { return m_demuxerId; }
+
+protected:
+ virtual void EnableStream(int id, bool enable) {}
+ virtual void OpenStream(int id) {}
+ virtual CDemuxStream* GetStream(int iStreamId) const = 0;
+ virtual std::string GetStreamCodecName(int iStreamId) { return ""; }
+
+ int GetNrOfStreams(StreamType streamType);
+
+ int64_t m_demuxerId;
+
+private:
+ int64_t NewGuid()
+ {
+ static int64_t guid = 0;
+ return guid++;
+ }
+};
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxBXA.cpp b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxBXA.cpp
new file mode 100644
index 0000000..d37f07b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxBXA.cpp
@@ -0,0 +1,186 @@
+/*
+ * 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 "DVDDemuxBXA.h"
+
+#include "DVDDemuxUtils.h"
+#include "DVDInputStreams/DVDInputStream.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/StringUtils.h"
+
+// AirTunes audio Demuxer.
+
+class CDemuxStreamAudioBXA
+ : public CDemuxStreamAudio
+{
+ std::string m_codec;
+public:
+ CDemuxStreamAudioBXA(CDVDDemuxBXA *parent, const std::string& codec)
+ : m_codec(codec)
+
+ {}
+};
+
+CDVDDemuxBXA::CDVDDemuxBXA() : CDVDDemux()
+{
+ m_stream = NULL;
+ m_bytes = 0;
+ memset(&m_header, 0x0, sizeof(Demux_BXA_FmtHeader));
+}
+
+CDVDDemuxBXA::~CDVDDemuxBXA()
+{
+ Dispose();
+}
+
+bool CDVDDemuxBXA::Open(const std::shared_ptr<CDVDInputStream>& pInput)
+{
+ Abort();
+
+ Dispose();
+
+ if(!pInput || !pInput->IsStreamType(DVDSTREAM_TYPE_FILE))
+ return false;
+
+ if(pInput->Read((uint8_t *)&m_header, sizeof(Demux_BXA_FmtHeader)) < 1)
+ return false;
+
+ // file valid?
+ if (strncmp(m_header.fourcc, "BXA ", 4) != 0 || m_header.type != BXA_PACKET_TYPE_FMT_DEMUX)
+ {
+ pInput->Seek(0, SEEK_SET);
+ return false;
+ }
+
+ m_pInput = pInput;
+
+ m_stream = new CDemuxStreamAudioBXA(this, "BXA");
+
+ if(!m_stream)
+ return false;
+
+ m_stream->iSampleRate = m_header.sampleRate;
+ m_stream->iBitsPerSample = m_header.bitsPerSample;
+ m_stream->iBitRate = m_header.sampleRate * m_header.channels * m_header.bitsPerSample;
+ m_stream->iChannels = m_header.channels;
+ m_stream->type = STREAM_AUDIO;
+ m_stream->codec = AV_CODEC_ID_PCM_S16LE;
+
+ return true;
+}
+
+void CDVDDemuxBXA::Dispose()
+{
+ delete m_stream;
+ m_stream = NULL;
+
+ m_pInput = NULL;
+ m_bytes = 0;
+
+ memset(&m_header, 0x0, sizeof(Demux_BXA_FmtHeader));
+}
+
+bool CDVDDemuxBXA::Reset()
+{
+ std::shared_ptr<CDVDInputStream> pInputStream = m_pInput;
+ Dispose();
+ return Open(pInputStream);
+}
+
+void CDVDDemuxBXA::Abort()
+{
+ if(m_pInput)
+ return m_pInput->Abort();
+}
+
+void CDVDDemuxBXA::Flush()
+{
+}
+
+#define BXA_READ_SIZE 4096
+DemuxPacket* CDVDDemuxBXA::Read()
+{
+ if(!m_pInput)
+ return NULL;
+
+ DemuxPacket* pPacket = CDVDDemuxUtils::AllocateDemuxPacket(BXA_READ_SIZE);
+
+ if (!pPacket)
+ {
+ if (m_pInput)
+ m_pInput->Close();
+ return NULL;
+ }
+
+ pPacket->iSize = m_pInput->Read(pPacket->pData, BXA_READ_SIZE);
+ pPacket->iStreamId = 0;
+
+ if(pPacket->iSize < 1)
+ {
+ delete pPacket;
+ pPacket = NULL;
+ }
+ else
+ {
+ int n = (m_header.channels * m_header.bitsPerSample * m_header.sampleRate)>>3;
+ if (n > 0)
+ {
+ m_bytes += pPacket->iSize;
+ pPacket->dts = (double)m_bytes * DVD_TIME_BASE / n;
+ pPacket->pts = pPacket->dts;
+ }
+ else
+ {
+ pPacket->dts = DVD_NOPTS_VALUE;
+ pPacket->pts = DVD_NOPTS_VALUE;
+ }
+ }
+
+ return pPacket;
+}
+
+CDemuxStream* CDVDDemuxBXA::GetStream(int iStreamId) const
+{
+ if(iStreamId != 0)
+ return NULL;
+
+ return m_stream;
+}
+
+std::vector<CDemuxStream*> CDVDDemuxBXA::GetStreams() const
+{
+ std::vector<CDemuxStream*> streams;
+
+ if (m_stream != nullptr)
+ {
+ streams.push_back(m_stream);
+ }
+
+ return streams;
+}
+
+int CDVDDemuxBXA::GetNrOfStreams() const
+{
+ return (m_stream == NULL ? 0 : 1);
+}
+
+std::string CDVDDemuxBXA::GetFileName()
+{
+ if(m_pInput)
+ return m_pInput->GetFileName();
+ else
+ return "";
+}
+
+std::string CDVDDemuxBXA::GetStreamCodecName(int iStreamId)
+{
+ if (m_stream && iStreamId == 0)
+ return "BXA";
+ else
+ return "";
+}
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxBXA.h b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxBXA.h
new file mode 100644
index 0000000..94fa8c9
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxBXA.h
@@ -0,0 +1,69 @@
+/*
+ * 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 "DVDDemux.h"
+
+#ifdef TARGET_WINDOWS
+#define __attribute__(dummy_val)
+#pragma pack(push)
+#pragma pack(1)
+#endif
+
+typedef struct
+{
+ char fourcc[4];
+ uint32_t type;
+ uint32_t channels;
+ uint32_t sampleRate;
+ uint32_t bitsPerSample;
+ uint64_t durationMs;
+} __attribute__((__packed__)) Demux_BXA_FmtHeader;
+
+#ifdef TARGET_WINDOWS
+#pragma pack(pop)
+#endif
+
+#include <vector>
+
+#define BXA_PACKET_TYPE_FMT_DEMUX 1
+
+class CDemuxStreamAudioBXA;
+
+class CDVDDemuxBXA : public CDVDDemux
+{
+public:
+
+ CDVDDemuxBXA();
+ ~CDVDDemuxBXA() override;
+
+ bool Open(const std::shared_ptr<CDVDInputStream>& pInput);
+ void Dispose();
+ bool Reset() override;
+ void Abort() override;
+ void Flush() override;
+ DemuxPacket* Read() override;
+ bool SeekTime(double time, bool backwards = false, double* startpts = NULL) override { return false; }
+ int GetStreamLength() override { return (int)m_header.durationMs; }
+ CDemuxStream* GetStream(int iStreamId) const override;
+ std::vector<CDemuxStream*> GetStreams() const override;
+ int GetNrOfStreams() const override;
+ std::string GetFileName() override;
+ std::string GetStreamCodecName(int iStreamId) override;
+
+protected:
+ friend class CDemuxStreamAudioBXA;
+ std::shared_ptr<CDVDInputStream> m_pInput;
+ int64_t m_bytes;
+
+ CDemuxStreamAudioBXA *m_stream;
+
+ Demux_BXA_FmtHeader m_header;
+};
+
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCC.cpp b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCC.cpp
new file mode 100644
index 0000000..b91e8e6
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCC.cpp
@@ -0,0 +1,414 @@
+/*
+ * 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 "DVDDemuxCC.h"
+
+#include "DVDDemuxUtils.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/contrib/cc_decoder708.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+
+#include <algorithm>
+
+class CBitstream
+{
+public:
+ CBitstream(uint8_t *data, int bits)
+ {
+ m_data = data;
+ m_offset = 0;
+ m_len = bits;
+ m_error = false;
+ }
+ unsigned int readBits(int num)
+ {
+ int r = 0;
+ while (num > 0)
+ {
+ if (m_offset >= m_len)
+ {
+ m_error = true;
+ return 0;
+ }
+ num--;
+ if (m_data[m_offset / 8] & (1 << (7 - (m_offset & 7))))
+ r |= 1 << num;
+ m_offset++;
+ }
+ return r;
+ }
+ unsigned int readGolombUE(int maxbits = 32)
+ {
+ int lzb = -1;
+ int bits = 0;
+ for (int b = 0; !b; lzb++, bits++)
+ {
+ if (bits > maxbits)
+ return 0;
+ b = readBits(1);
+ }
+ return (1 << lzb) - 1 + readBits(lzb);
+ }
+
+private:
+ uint8_t *m_data;
+ int m_offset;
+ int m_len;
+ bool m_error;
+};
+
+class CCaptionBlock
+{
+ CCaptionBlock(const CCaptionBlock&) = delete;
+ CCaptionBlock& operator=(const CCaptionBlock&) = delete;
+public:
+ explicit CCaptionBlock(int size)
+ {
+ m_data = (uint8_t*)malloc(size);
+ m_size = size;
+ m_pts = 0.0; //silence coverity uninitialized warning, is set elsewhere
+ }
+ virtual ~CCaptionBlock()
+ {
+ free(m_data);
+ }
+ double m_pts;
+ uint8_t *m_data;
+ int m_size;
+};
+
+bool reorder_sort (CCaptionBlock *lhs, CCaptionBlock *rhs)
+{
+ return (lhs->m_pts > rhs->m_pts);
+}
+
+CDVDDemuxCC::CDVDDemuxCC(AVCodecID codec) : m_codec(codec)
+{
+ m_hasData = false;
+ m_curPts = 0.0;
+ m_ccDecoder = NULL;
+}
+
+CDVDDemuxCC::~CDVDDemuxCC()
+{
+ Dispose();
+}
+
+CDemuxStream* CDVDDemuxCC::GetStream(int iStreamId) const
+{
+ for (int i=0; i<GetNrOfStreams(); i++)
+ {
+ if (m_streams[i].uniqueId == iStreamId)
+ return const_cast<CDemuxStreamSubtitle*>(&m_streams[i]);
+ }
+ return nullptr;
+}
+
+std::vector<CDemuxStream*> CDVDDemuxCC::GetStreams() const
+{
+ std::vector<CDemuxStream*> streams;
+
+ int num = GetNrOfStreams();
+ streams.reserve(num);
+ for (int i = 0; i < num; ++i)
+ {
+ streams.push_back(const_cast<CDemuxStreamSubtitle*>(&m_streams[i]));
+ }
+
+ return streams;
+}
+
+int CDVDDemuxCC::GetNrOfStreams() const
+{
+ return m_streams.size();
+}
+
+DemuxPacket* CDVDDemuxCC::Read(DemuxPacket *pSrcPacket)
+{
+ DemuxPacket *pPacket = NULL;
+ uint32_t startcode = 0xffffffff;
+ int picType = 0;
+ int p = 0;
+ int len;
+
+ if (!pSrcPacket)
+ {
+ pPacket = Decode();
+ return pPacket;
+ }
+ if (pSrcPacket->pts == DVD_NOPTS_VALUE)
+ {
+ return pPacket;
+ }
+
+ while (!m_ccTempBuffer.empty())
+ {
+ m_ccReorderBuffer.push_back(m_ccTempBuffer.back());
+ m_ccTempBuffer.pop_back();
+ }
+
+ while ((len = pSrcPacket->iSize - p) > 3)
+ {
+ if ((startcode & 0xffffff00) == 0x00000100)
+ {
+ if (m_codec == AV_CODEC_ID_MPEG2VIDEO)
+ {
+ int scode = startcode & 0xFF;
+ if (scode == 0x00)
+ {
+ if (len > 4)
+ {
+ uint8_t *buf = pSrcPacket->pData + p;
+ picType = (buf[1] & 0x38) >> 3;
+ }
+ }
+ else if (scode == 0xb2) // user data
+ {
+ uint8_t *buf = pSrcPacket->pData + p;
+ if (len >= 6 &&
+ buf[0] == 'G' && buf[1] == 'A' && buf[2] == '9' && buf[3] == '4' &&
+ buf[4] == 3 && (buf[5] & 0x40))
+ {
+ int cc_count = buf[5] & 0x1f;
+ if (cc_count > 0 && len >= 7 + cc_count * 3)
+ {
+ CCaptionBlock *cc = new CCaptionBlock(cc_count * 3);
+ memcpy(cc->m_data, buf + 7, cc_count * 3);
+ cc->m_pts = pSrcPacket->pts;
+ if (picType == 1 || picType == 2)
+ m_ccTempBuffer.push_back(cc);
+ else
+ m_ccReorderBuffer.push_back(cc);
+ }
+ }
+ else if (len >= 6 &&
+ buf[0] == 'C' && buf[1] == 'C' && buf[2] == 1)
+ {
+ int oddidx = (buf[4] & 0x80) ? 0 : 1;
+ int cc_count = (buf[4] & 0x3e) >> 1;
+ int extrafield = buf[4] & 0x01;
+ if (extrafield)
+ cc_count++;
+
+ if (cc_count > 0 && len >= 5 + cc_count * 3 * 2)
+ {
+ CCaptionBlock *cc = new CCaptionBlock(cc_count * 3);
+ uint8_t *src = buf + 5;
+ uint8_t *dst = cc->m_data;
+
+ for (int i = 0; i < cc_count; i++)
+ {
+ for (int j = 0; j < 2; j++)
+ {
+ if (i == cc_count - 1 && extrafield && j == 1)
+ break;
+
+ if ((oddidx == j) && (src[0] == 0xFF))
+ {
+ dst[0] = 0x04;
+ dst[1] = src[1];
+ dst[2] = src[2];
+ dst += 3;
+ }
+ src += 3;
+ }
+ }
+ cc->m_pts = pSrcPacket->pts;
+ m_ccReorderBuffer.push_back(cc);
+ picType = 1;
+ }
+ }
+ }
+ }
+ else if (m_codec == AV_CODEC_ID_H264)
+ {
+ int scode = startcode & 0x9F;
+ // slice data comes after SEI
+ if (scode >= 1 && scode <= 5)
+ {
+ uint8_t *buf = pSrcPacket->pData + p;
+ CBitstream bs(buf, len * 8);
+ bs.readGolombUE();
+ int sliceType = bs.readGolombUE();
+ if (sliceType == 2 || sliceType == 7) // I slice
+ picType = 1;
+ else if (sliceType == 0 || sliceType == 5) // P slice
+ picType = 2;
+ if (picType == 0)
+ {
+ while (!m_ccTempBuffer.empty())
+ {
+ m_ccReorderBuffer.push_back(m_ccTempBuffer.back());
+ m_ccTempBuffer.pop_back();
+ }
+ }
+ }
+ if (scode == 0x06) // SEI
+ {
+ uint8_t *buf = pSrcPacket->pData + p;
+ if (len >= 12 &&
+ buf[3] == 0 && buf[4] == 49 &&
+ buf[5] == 'G' && buf[6] == 'A' && buf[7] == '9' && buf[8] == '4' && buf[9] == 3)
+ {
+ uint8_t *userdata = buf + 10;
+ int cc_count = userdata[0] & 0x1f;
+ if (len >= cc_count * 3 + 10)
+ {
+ CCaptionBlock *cc = new CCaptionBlock(cc_count * 3);
+ memcpy(cc->m_data, userdata + 2, cc_count * 3);
+ cc->m_pts = pSrcPacket->pts;
+ m_ccTempBuffer.push_back(cc);
+ }
+ }
+ }
+ }
+ }
+ startcode = startcode << 8 | pSrcPacket->pData[p++];
+ }
+
+ if ((picType == 1 || picType == 2) && !m_ccReorderBuffer.empty())
+ {
+ if (!m_ccDecoder)
+ {
+ if (!OpenDecoder())
+ return NULL;
+ }
+ std::sort(m_ccReorderBuffer.begin(), m_ccReorderBuffer.end(), reorder_sort);
+ pPacket = Decode();
+ }
+ return pPacket;
+}
+
+void CDVDDemuxCC::Handler(int service, void *userdata)
+{
+ CDVDDemuxCC *ctx = static_cast<CDVDDemuxCC*>(userdata);
+
+ unsigned int idx;
+
+ // switch back from 608 fallback if we got 708
+ if (ctx->m_ccDecoder->m_seen608 && ctx->m_ccDecoder->m_seen708)
+ {
+ for (idx = 0; idx < ctx->m_streamdata.size(); idx++)
+ {
+ if (ctx->m_streamdata[idx].service == 0)
+ break;
+ }
+ if (idx < ctx->m_streamdata.size())
+ {
+ ctx->m_streamdata.erase(ctx->m_streamdata.begin() + idx);
+ ctx->m_ccDecoder->m_seen608 = false;
+ }
+ if (service == 0)
+ return;
+ }
+
+ for (idx = 0; idx < ctx->m_streamdata.size(); idx++)
+ {
+ if (ctx->m_streamdata[idx].service == service)
+ break;
+ }
+ if (idx >= ctx->m_streamdata.size())
+ {
+ CDemuxStreamSubtitle stream;
+ stream.source = STREAM_SOURCE_VIDEOMUX;
+ stream.language = "cc";
+ stream.flags = FLAG_HEARING_IMPAIRED;
+ stream.codec = AV_CODEC_ID_TEXT;
+ stream.uniqueId = service;
+ ctx->m_streams.push_back(std::move(stream));
+
+ streamdata data;
+ data.streamIdx = idx;
+ data.service = service;
+ ctx->m_streamdata.push_back(data);
+
+ if (service == 0)
+ ctx->m_ccDecoder->m_seen608 = true;
+ else
+ ctx->m_ccDecoder->m_seen708 = true;
+ }
+
+ ctx->m_streamdata[idx].pts = ctx->m_curPts;
+ ctx->m_streamdata[idx].hasData = true;
+ ctx->m_hasData = true;
+}
+
+bool CDVDDemuxCC::OpenDecoder()
+{
+ m_ccDecoder = new CDecoderCC708();
+ m_ccDecoder->Init(Handler, this);
+ return true;
+}
+
+void CDVDDemuxCC::Dispose()
+{
+ m_streams.clear();
+ m_streamdata.clear();
+ delete m_ccDecoder;
+ m_ccDecoder = NULL;
+
+ while (!m_ccReorderBuffer.empty())
+ {
+ delete m_ccReorderBuffer.back();
+ m_ccReorderBuffer.pop_back();
+ }
+ while (!m_ccTempBuffer.empty())
+ {
+ delete m_ccTempBuffer.back();
+ m_ccTempBuffer.pop_back();
+ }
+}
+
+DemuxPacket* CDVDDemuxCC::Decode()
+{
+ DemuxPacket *pPacket = NULL;
+
+ while(!m_hasData && !m_ccReorderBuffer.empty())
+ {
+ CCaptionBlock *cc = m_ccReorderBuffer.back();
+ m_ccReorderBuffer.pop_back();
+ m_curPts = cc->m_pts;
+ m_ccDecoder->Decode(cc->m_data, cc->m_size);
+ delete cc;
+ }
+
+ if (m_hasData)
+ {
+ for (unsigned int i=0; i<m_streamdata.size(); i++)
+ {
+ if (m_streamdata[i].hasData)
+ {
+ int service = m_streamdata[i].service;
+
+ char *data;
+ int len;
+ if (service == 0)
+ {
+ data = m_ccDecoder->m_cc608decoder->text;
+ len = m_ccDecoder->m_cc608decoder->textlen;
+ }
+ else
+ {
+ data = m_ccDecoder->m_cc708decoders[service].text;
+ len = m_ccDecoder->m_cc708decoders[service].textlen;
+ }
+
+ pPacket = CDVDDemuxUtils::AllocateDemuxPacket(len);
+ pPacket->iSize = len;
+ memcpy(pPacket->pData, data, pPacket->iSize);
+
+ pPacket->iStreamId = service;
+ pPacket->pts = m_streamdata[i].pts;
+ pPacket->duration = 0;
+ m_streamdata[i].hasData = false;
+ break;
+ }
+ m_hasData = false;
+ }
+ }
+ return pPacket;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCC.h b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCC.h
new file mode 100644
index 0000000..3662709
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCC.h
@@ -0,0 +1,58 @@
+/*
+ * 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 "DVDDemux.h"
+
+#include <vector>
+
+class CCaptionBlock;
+class CDecoderCC708;
+
+class CDVDDemuxCC : public CDVDDemux
+{
+public:
+ explicit CDVDDemuxCC(AVCodecID codec);
+ ~CDVDDemuxCC() override;
+
+ bool Reset() override { return true; }
+ void Flush() override {};
+ DemuxPacket* Read() override { return NULL; }
+ bool SeekTime(double time, bool backwards = false, double* startpts = NULL) override
+ {
+ return true;
+ }
+ CDemuxStream* GetStream(int iStreamId) const override;
+ std::vector<CDemuxStream*> GetStreams() const override;
+ int GetNrOfStreams() const override;
+
+ DemuxPacket* Read(DemuxPacket *packet);
+ static void Handler(int service, void *userdata);
+
+protected:
+ bool OpenDecoder();
+ void Dispose();
+ DemuxPacket* Decode();
+
+ struct streamdata
+ {
+ int streamIdx;
+ int service;
+ bool hasData ;
+ double pts;
+ };
+ std::vector<streamdata> m_streamdata;
+ std::vector<CDemuxStreamSubtitle> m_streams;
+ bool m_hasData;
+ double m_curPts;
+ std::vector<CCaptionBlock*> m_ccReorderBuffer;
+ std::vector<CCaptionBlock*> m_ccTempBuffer;
+ CDecoderCC708 *m_ccDecoder;
+ AVCodecID m_codec;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCDDA.cpp b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCDDA.cpp
new file mode 100644
index 0000000..8d86082
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCDDA.cpp
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2013-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 "DVDDemuxCDDA.h"
+
+#include "DVDDemuxUtils.h"
+#include "DVDInputStreams/DVDInputStream.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+
+// CDDA audio demuxer based on AirTunes audio Demuxer.
+
+class CDemuxStreamAudioCDDA
+ : public CDemuxStreamAudio
+{
+};
+
+CDVDDemuxCDDA::CDVDDemuxCDDA() : CDVDDemux()
+{
+ m_stream = NULL;
+ m_bytes = 0;
+}
+
+CDVDDemuxCDDA::~CDVDDemuxCDDA()
+{
+ Dispose();
+}
+
+bool CDVDDemuxCDDA::Open(const std::shared_ptr<CDVDInputStream>& pInput)
+{
+ Abort();
+
+ Dispose();
+
+ if(!pInput || !pInput->IsStreamType(DVDSTREAM_TYPE_FILE))
+ return false;
+
+ m_pInput = pInput;
+
+ m_stream = new CDemuxStreamAudioCDDA();
+
+ if(!m_stream)
+ return false;
+
+ m_stream->iSampleRate = 44100;
+ m_stream->iBitsPerSample = 16;
+ m_stream->iBitRate = 44100 * 2 * 16;
+ m_stream->iChannels = 2;
+ m_stream->type = STREAM_AUDIO;
+ m_stream->codec = AV_CODEC_ID_PCM_S16LE;
+
+ return true;
+}
+
+void CDVDDemuxCDDA::Dispose()
+{
+ delete m_stream;
+ m_stream = NULL;
+
+ m_pInput = NULL;
+ m_bytes = 0;
+}
+
+bool CDVDDemuxCDDA::Reset()
+{
+ std::shared_ptr<CDVDInputStream> pInputStream = m_pInput;
+ Dispose();
+ return Open(pInputStream);
+}
+
+void CDVDDemuxCDDA::Abort()
+{
+ if(m_pInput)
+ return m_pInput->Abort();
+}
+
+void CDVDDemuxCDDA::Flush()
+{
+}
+
+#define CDDA_READ_SIZE 4096
+DemuxPacket* CDVDDemuxCDDA::Read()
+{
+ if(!m_pInput)
+ return NULL;
+
+ DemuxPacket* pPacket = CDVDDemuxUtils::AllocateDemuxPacket(CDDA_READ_SIZE);
+
+ if (!pPacket)
+ {
+ if (m_pInput)
+ m_pInput->Close();
+ return NULL;
+ }
+
+ pPacket->iSize = m_pInput->Read(pPacket->pData, CDDA_READ_SIZE);
+ pPacket->iStreamId = 0;
+
+ if(pPacket->iSize < 1)
+ {
+ delete pPacket;
+ pPacket = NULL;
+ }
+ else
+ {
+ int n = m_stream->iBitRate>>3;
+ if (n > 0)
+ {
+ m_bytes += pPacket->iSize;
+ pPacket->dts = (double)m_bytes * DVD_TIME_BASE / n;
+ pPacket->pts = pPacket->dts;
+ }
+ else
+ {
+ pPacket->dts = DVD_NOPTS_VALUE;
+ pPacket->pts = DVD_NOPTS_VALUE;
+ }
+ }
+
+ return pPacket;
+}
+
+bool CDVDDemuxCDDA::SeekTime(double time, bool backwards, double* startpts)
+{
+ int bytes_per_second = m_stream->iBitRate>>3;
+ // clamp seeks to bytes per full sample
+ int clamp_bytes = (m_stream->iBitsPerSample>>3) * m_stream->iChannels;
+
+ // time is in milliseconds
+ int64_t seekPos = m_pInput->Seek((((int64_t)time * bytes_per_second / 1000) / clamp_bytes ) * clamp_bytes, SEEK_SET) > 0;
+ if (seekPos > 0)
+ m_bytes = seekPos;
+
+ if (startpts)
+ *startpts = (double)m_bytes * DVD_TIME_BASE / bytes_per_second;
+
+ return seekPos > 0;
+};
+
+int CDVDDemuxCDDA::GetStreamLength()
+{
+ int64_t num_track_bytes = m_pInput->GetLength();
+ int bytes_per_second = (m_stream->iBitRate>>3);
+ int64_t track_mseconds = num_track_bytes*1000 / bytes_per_second;
+ return (int)track_mseconds;
+}
+
+CDemuxStream* CDVDDemuxCDDA::GetStream(int iStreamId) const
+{
+ if(iStreamId != 0)
+ return NULL;
+
+ return m_stream;
+}
+
+std::vector<CDemuxStream*> CDVDDemuxCDDA::GetStreams() const
+{
+ std::vector<CDemuxStream*> streams;
+
+ if (m_stream != nullptr)
+ {
+ streams.push_back(m_stream);
+ }
+
+ return streams;
+}
+
+int CDVDDemuxCDDA::GetNrOfStreams() const
+{
+ return (m_stream == NULL ? 0 : 1);
+}
+
+std::string CDVDDemuxCDDA::GetFileName()
+{
+ if(m_pInput)
+ return m_pInput->GetFileName();
+ else
+ return "";
+}
+
+std::string CDVDDemuxCDDA::GetStreamCodecName(int iStreamId)
+{
+ if (m_stream && iStreamId == 0)
+ return "pcm";
+ else
+ return "";
+}
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCDDA.h b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCDDA.h
new file mode 100644
index 0000000..01b61f1
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxCDDA.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013-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 "DVDDemux.h"
+
+#include <vector>
+
+#ifdef TARGET_WINDOWS
+#define __attribute__(dummy_val)
+#endif
+
+class CDemuxStreamAudioCDDA;
+
+class CDVDDemuxCDDA : public CDVDDemux
+{
+public:
+
+ CDVDDemuxCDDA();
+ ~CDVDDemuxCDDA() override;
+
+ bool Open(const std::shared_ptr<CDVDInputStream>& pInput);
+ void Dispose();
+ bool Reset() override;
+ void Abort() override;
+ void Flush() override;
+ DemuxPacket* Read() override;
+ bool SeekTime(double time, bool backwards = false, double* startpts = NULL) override;
+ int GetStreamLength() override;
+ CDemuxStream* GetStream(int iStreamId) const override;
+ std::vector<CDemuxStream*> GetStreams() const override;
+ int GetNrOfStreams() const override;
+ std::string GetFileName() override;
+ std::string GetStreamCodecName(int iStreamId) override;
+
+protected:
+ friend class CDemuxStreamAudioCDDA;
+ std::shared_ptr<CDVDInputStream> m_pInput;
+ int64_t m_bytes;
+
+ CDemuxStreamAudioCDDA *m_stream;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxClient.cpp b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxClient.cpp
new file mode 100644
index 0000000..2bdc3ea
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxClient.cpp
@@ -0,0 +1,764 @@
+/*
+ * 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 "DVDDemuxClient.h"
+
+#include "DVDDemuxUtils.h"
+#include "DVDInputStreams/DVDInputStream.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/log.h"
+
+#include <utility>
+
+#define FF_MAX_EXTRADATA_SIZE ((1 << 28) - AV_INPUT_BUFFER_PADDING_SIZE)
+
+class CDemuxStreamClientInternal
+{
+public:
+ ~CDemuxStreamClientInternal()
+ {
+ DisposeParser();
+ }
+
+ void DisposeParser()
+ {
+ if (m_parser)
+ {
+ av_parser_close(m_parser);
+ m_parser = nullptr;
+ }
+ if (m_context)
+ {
+ avcodec_free_context(&m_context);
+ m_context = nullptr;
+ }
+ }
+
+ AVCodecParserContext *m_parser = nullptr;
+ AVCodecContext *m_context = nullptr;
+ bool m_parser_split = false;
+};
+
+template <class T>
+class CDemuxStreamClientInternalTpl : public CDemuxStreamClientInternal, public T
+{
+};
+
+CDVDDemuxClient::CDVDDemuxClient() : CDVDDemux()
+{
+ m_streams.clear();
+}
+
+CDVDDemuxClient::~CDVDDemuxClient()
+{
+ Dispose();
+}
+
+bool CDVDDemuxClient::Open(std::shared_ptr<CDVDInputStream> pInput)
+{
+ Abort();
+
+ m_pInput = std::move(pInput);
+ m_IDemux = std::dynamic_pointer_cast<CDVDInputStream::IDemux>(m_pInput);
+ if (!m_IDemux)
+ return false;
+
+ if (!m_IDemux->OpenDemux())
+ return false;
+
+ RequestStreams();
+
+ m_displayTime = 0;
+ m_dtsAtDisplayTime = DVD_NOPTS_VALUE;
+ return true;
+}
+
+void CDVDDemuxClient::Dispose()
+{
+ DisposeStreams();
+
+ m_pInput = nullptr;
+ m_IDemux = nullptr;
+}
+
+void CDVDDemuxClient::DisposeStreams()
+{
+ m_streams.clear();
+ m_videoStreamPlaying = -1;
+}
+
+bool CDVDDemuxClient::Reset()
+{
+ std::shared_ptr<CDVDInputStream> pInputStream = m_pInput;
+ Dispose();
+ return Open(pInputStream);
+}
+
+void CDVDDemuxClient::Abort()
+{
+ if (m_IDemux)
+ m_IDemux->AbortDemux();
+}
+
+void CDVDDemuxClient::Flush()
+{
+ if (m_IDemux)
+ m_IDemux->FlushDemux();
+
+ m_displayTime = 0;
+ m_dtsAtDisplayTime = DVD_NOPTS_VALUE;
+}
+
+bool CDVDDemuxClient::ParsePacket(DemuxPacket* pkt)
+{
+ bool change = false;
+
+ CDemuxStream* st = GetStream(pkt->iStreamId);
+ if (st == nullptr || st->changes < 0 || st->ExtraSize || !CodecHasExtraData(st->codec))
+ return change;
+
+ CDemuxStreamClientInternal* stream = dynamic_cast<CDemuxStreamClientInternal*>(st);
+
+ if (stream == nullptr ||
+ stream->m_parser == nullptr)
+ return change;
+
+ if (stream->m_context == nullptr)
+ {
+ AVCodec *codec = avcodec_find_decoder(st->codec);
+ if (codec == nullptr)
+ {
+ CLog::Log(LOGERROR, "{} - can't find decoder", __FUNCTION__);
+ stream->DisposeParser();
+ return change;
+ }
+
+ stream->m_context = avcodec_alloc_context3(codec);
+ if (stream->m_context == nullptr)
+ {
+ CLog::Log(LOGERROR, "{} - can't allocate context", __FUNCTION__);
+ stream->DisposeParser();
+ return change;
+ }
+ stream->m_context->time_base.num = 1;
+ stream->m_context->time_base.den = DVD_TIME_BASE;
+ }
+
+ if (stream->m_parser_split && stream->m_parser->parser->split)
+ {
+ int len = stream->m_parser->parser->split(stream->m_context, pkt->pData, pkt->iSize);
+ if (len > 0 && len < FF_MAX_EXTRADATA_SIZE)
+ {
+ st->changes++;
+ st->disabled = false;
+ st->ExtraSize = len;
+ st->ExtraData = std::make_unique<uint8_t[]>(len + AV_INPUT_BUFFER_PADDING_SIZE);
+ memcpy(st->ExtraData.get(), pkt->pData, len);
+ memset(st->ExtraData.get() + len, 0, AV_INPUT_BUFFER_PADDING_SIZE);
+ stream->m_parser_split = false;
+ change = true;
+ CLog::Log(LOGDEBUG, "CDVDDemuxClient::ParsePacket - split extradata");
+
+ // Allow ffmpeg to transport codec information to stream->m_context
+ if (!avcodec_open2(stream->m_context, stream->m_context->codec, nullptr))
+ {
+ AVPacket* avpkt = av_packet_alloc();
+ if (!avpkt)
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxClient::{} - av_packet_alloc failed: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+ avpkt->data = pkt->pData;
+ avpkt->size = pkt->iSize;
+ avpkt->dts = avpkt->pts = AV_NOPTS_VALUE;
+ avcodec_send_packet(stream->m_context, avpkt);
+ avcodec_close(stream->m_context);
+ av_packet_free(&avpkt);
+ }
+ }
+ }
+
+ uint8_t *outbuf = nullptr;
+ int outbuf_size = 0;
+ int len = av_parser_parse2(stream->m_parser,
+ stream->m_context, &outbuf, &outbuf_size,
+ pkt->pData, pkt->iSize,
+ (int64_t)(pkt->pts * DVD_TIME_BASE),
+ (int64_t)(pkt->dts * DVD_TIME_BASE),
+ 0);
+
+ // our parse is setup to parse complete frames, so we don't care about outbufs
+ if (len >= 0)
+ {
+ if (stream->m_context->profile != st->profile &&
+ stream->m_context->profile != FF_PROFILE_UNKNOWN)
+ {
+ CLog::Log(LOGDEBUG, "CDVDDemuxClient::ParsePacket - ({}) profile changed from {} to {}", st->uniqueId, st->profile, stream->m_context->profile);
+ st->profile = stream->m_context->profile;
+ st->changes++;
+ st->disabled = false;
+ }
+
+ if (stream->m_context->level != st->level &&
+ stream->m_context->level != FF_LEVEL_UNKNOWN)
+ {
+ CLog::Log(LOGDEBUG, "CDVDDemuxClient::ParsePacket - ({}) level changed from {} to {}", st->uniqueId, st->level, stream->m_context->level);
+ st->level = stream->m_context->level;
+ st->changes++;
+ st->disabled = false;
+ }
+
+ switch (st->type)
+ {
+ case STREAM_AUDIO:
+ {
+ CDemuxStreamClientInternalTpl<CDemuxStreamAudio>* sta = static_cast<CDemuxStreamClientInternalTpl<CDemuxStreamAudio>*>(st);
+ if (stream->m_context->channels != sta->iChannels && stream->m_context->channels != 0)
+ {
+ CLog::Log(LOGDEBUG, "CDVDDemuxClient::ParsePacket - ({}) channels changed from {} to {}", st->uniqueId, sta->iChannels, stream->m_context->channels);
+ sta->iChannels = stream->m_context->channels;
+ sta->changes++;
+ sta->disabled = false;
+ }
+ if (stream->m_context->sample_rate != sta->iSampleRate &&
+ stream->m_context->sample_rate != 0)
+ {
+ CLog::Log(LOGDEBUG, "CDVDDemuxClient::ParsePacket - ({}) samplerate changed from {} to {}", st->uniqueId, sta->iSampleRate, stream->m_context->sample_rate);
+ sta->iSampleRate = stream->m_context->sample_rate;
+ sta->changes++;
+ sta->disabled = false;
+ }
+ if (stream->m_context->channels)
+ st->changes = -1; // stop parsing
+ break;
+ }
+ case STREAM_VIDEO:
+ {
+ CDemuxStreamClientInternalTpl<CDemuxStreamVideo>* stv = static_cast<CDemuxStreamClientInternalTpl<CDemuxStreamVideo>*>(st);
+ if (stream->m_parser->width != stv->iWidth && stream->m_parser->width != 0)
+ {
+ CLog::Log(LOGDEBUG, "CDVDDemuxClient::ParsePacket - ({}) width changed from {} to {}",
+ st->uniqueId, stv->iWidth, stream->m_parser->width);
+ stv->iWidth = stream->m_parser->width;
+ stv->changes++;
+ stv->disabled = false;
+ }
+ if (stream->m_parser->height != stv->iHeight && stream->m_parser->height != 0)
+ {
+ CLog::Log(LOGDEBUG, "CDVDDemuxClient::ParsePacket - ({}) height changed from {} to {}",
+ st->uniqueId, stv->iHeight, stream->m_parser->height);
+ stv->iHeight = stream->m_parser->height;
+ stv->changes++;
+ stv->disabled = false;
+ }
+ if (stream->m_context->sample_aspect_ratio.num && stream->m_context->height)
+ {
+ double fAspect =
+ (av_q2d(stream->m_context->sample_aspect_ratio) * stream->m_context->width) /
+ stream->m_context->height;
+ if (abs(fAspect - stv->fAspect) > 0.001 && fAspect >= 0.001)
+ {
+ CLog::Log(LOGDEBUG, "CDVDDemuxClient::ParsePacket - ({}) aspect changed from {} to {}",
+ st->uniqueId, stv->fAspect, fAspect);
+ stv->fAspect = fAspect;
+ stv->changes++;
+ stv->disabled = false;
+ }
+ }
+ if (stream->m_context->framerate.num &&
+ (stv->iFpsRate != stream->m_context->framerate.num ||
+ stv->iFpsScale != stream->m_context->framerate.den))
+ {
+ CLog::Log(LOGDEBUG, "CDVDDemuxClient::ParsePacket - ({}) fps changed from {}/{} to {}/{}",
+ st->uniqueId, stv->iFpsRate, stv->iFpsScale, stream->m_context->framerate.num,
+ stream->m_context->framerate.den);
+ stv->iFpsRate = stream->m_context->framerate.num;
+ stv->iFpsScale = stream->m_context->framerate.den;
+ stv->changes++;
+ stv->disabled = false;
+ }
+
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+ else
+ CLog::Log(LOGDEBUG, "{} - parser returned error {}", __FUNCTION__, len);
+
+ return change;
+}
+
+DemuxPacket* CDVDDemuxClient::Read()
+{
+ if (!m_IDemux)
+ return nullptr;
+
+ if (m_packet)
+ return m_packet.release();
+
+ m_packet.reset(m_IDemux->ReadDemux());
+ if (!m_packet)
+ {
+ return nullptr;
+ }
+
+ if (m_packet->iStreamId == DMX_SPECIALID_STREAMINFO)
+ {
+ RequestStreams();
+ CDVDDemuxUtils::FreeDemuxPacket(m_packet.release());
+ return CDVDDemuxUtils::AllocateDemuxPacket(0);
+ }
+ else if (m_packet->iStreamId == DMX_SPECIALID_STREAMCHANGE)
+ {
+ RequestStreams();
+ }
+ else if (m_packet->iStreamId >= 0 && m_streams.count(m_packet->iStreamId) > 0)
+ {
+ if (ParsePacket(m_packet.get()))
+ {
+ RequestStreams();
+ DemuxPacket *pPacket = CDVDDemuxUtils::AllocateDemuxPacket(0);
+ pPacket->iStreamId = DMX_SPECIALID_STREAMCHANGE;
+ pPacket->demuxerId = m_demuxerId;
+ return pPacket;
+ }
+ }
+
+ if (!IsVideoReady())
+ {
+ CDVDDemuxUtils::FreeDemuxPacket(m_packet.release());
+ DemuxPacket *pPacket = CDVDDemuxUtils::AllocateDemuxPacket(0);
+ pPacket->demuxerId = m_demuxerId;
+ return pPacket;
+ }
+
+ //! @todo drop this block
+ CDVDInputStream::IDisplayTime *inputStream = m_pInput->GetIDisplayTime();
+ if (inputStream)
+ {
+ int dispTime = inputStream->GetTime();
+ if (m_displayTime != dispTime)
+ {
+ m_displayTime = dispTime;
+ if (m_packet->dts != DVD_NOPTS_VALUE)
+ {
+ m_dtsAtDisplayTime = m_packet->dts;
+ }
+ }
+ if (m_dtsAtDisplayTime != DVD_NOPTS_VALUE && m_packet->dts != DVD_NOPTS_VALUE)
+ {
+ m_packet->dispTime = m_displayTime;
+ m_packet->dispTime += DVD_TIME_TO_MSEC(m_packet->dts - m_dtsAtDisplayTime);
+ }
+ }
+
+ return m_packet.release();
+}
+
+CDemuxStream* CDVDDemuxClient::GetStream(int iStreamId) const
+{
+ auto stream = m_streams.find(iStreamId);
+ if (stream == m_streams.end())
+ return nullptr;
+
+ return stream->second.get();
+}
+
+std::vector<CDemuxStream*> CDVDDemuxClient::GetStreams() const
+{
+ std::vector<CDemuxStream*> streams;
+
+ streams.reserve(m_streams.size());
+ for (auto &st : m_streams)
+ streams.push_back(st.second.get());
+
+ return streams;
+}
+
+void CDVDDemuxClient::RequestStreams()
+{
+ std::map<int, std::shared_ptr<CDemuxStream>> newStreamMap;
+ for (auto stream : m_IDemux->GetStreams())
+ SetStreamProps(stream, newStreamMap, false);
+ m_streams = newStreamMap;
+}
+
+void CDVDDemuxClient::SetStreamProps(CDemuxStream *stream, std::map<int, std::shared_ptr<CDemuxStream>> &map, bool forceInit)
+{
+ if (!stream)
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxClient::RequestStream - invalid stream");
+ DisposeStreams();
+ return;
+ }
+
+ std::shared_ptr<CDemuxStream> currentStream(GetStreamInternal(stream->uniqueId));
+ std::shared_ptr<CDemuxStream> toStream;
+
+ if (stream->type == STREAM_AUDIO)
+ {
+ CDemuxStreamAudio *source = dynamic_cast<CDemuxStreamAudio*>(stream);
+ if (!source)
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxClient::RequestStream - invalid audio stream with id {}",
+ stream->uniqueId);
+ DisposeStreams();
+ return;
+ }
+
+ std::shared_ptr<CDemuxStreamClientInternalTpl<CDemuxStreamAudio>> streamAudio;
+ if (currentStream)
+ streamAudio = std::dynamic_pointer_cast<CDemuxStreamClientInternalTpl<CDemuxStreamAudio>>(currentStream);
+ if (forceInit || !streamAudio || streamAudio->codec != source->codec)
+ {
+ streamAudio.reset(new CDemuxStreamClientInternalTpl<CDemuxStreamAudio>());
+ streamAudio->m_parser = av_parser_init(source->codec);
+ if (streamAudio->m_parser)
+ streamAudio->m_parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
+ streamAudio->iSampleRate = source->iSampleRate;
+ streamAudio->iChannels = source->iChannels;
+ }
+
+ streamAudio->iBlockAlign = source->iBlockAlign;
+ streamAudio->iBitRate = source->iBitRate;
+ streamAudio->iBitsPerSample = source->iBitsPerSample;
+ if (source->ExtraSize > 0 && source->ExtraData)
+ {
+ streamAudio->ExtraData = std::make_unique<uint8_t[]>(source->ExtraSize);
+ streamAudio->ExtraSize = source->ExtraSize;
+ for (unsigned int j=0; j<source->ExtraSize; j++)
+ streamAudio->ExtraData[j] = source->ExtraData[j];
+ }
+ streamAudio->m_parser_split = true;
+ streamAudio->changes++;
+ map[stream->uniqueId] = streamAudio;
+ toStream = streamAudio;
+ }
+ else if (stream->type == STREAM_VIDEO)
+ {
+ CDemuxStreamVideo *source = dynamic_cast<CDemuxStreamVideo*>(stream);
+
+ if (!source)
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxClient::RequestStream - invalid video stream with id {}",
+ stream->uniqueId);
+ DisposeStreams();
+ return;
+ }
+
+ std::shared_ptr<CDemuxStreamClientInternalTpl<CDemuxStreamVideo>> streamVideo;
+ if (currentStream)
+ streamVideo = std::dynamic_pointer_cast<CDemuxStreamClientInternalTpl<CDemuxStreamVideo>>(currentStream);
+ if (forceInit || !streamVideo || streamVideo->codec != source->codec)
+ {
+ streamVideo.reset(new CDemuxStreamClientInternalTpl<CDemuxStreamVideo>());
+ streamVideo->m_parser = av_parser_init(source->codec);
+ if (streamVideo->m_parser)
+ streamVideo->m_parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
+ streamVideo->iHeight = source->iHeight;
+ streamVideo->iWidth = source->iWidth;
+ streamVideo->fAspect = source->fAspect;
+ streamVideo->iFpsScale = source->iFpsScale;
+ streamVideo->iFpsRate = source->iFpsRate;
+ }
+ streamVideo->iBitRate = source->iBitRate;
+ if (source->ExtraSize > 0 && source->ExtraData)
+ {
+ streamVideo->ExtraData = std::make_unique<uint8_t[]>(source->ExtraSize);
+ streamVideo->ExtraSize = source->ExtraSize;
+ for (unsigned int j=0; j<source->ExtraSize; j++)
+ streamVideo->ExtraData[j] = source->ExtraData[j];
+ }
+ streamVideo->colorPrimaries = source->colorPrimaries;
+ streamVideo->colorRange = source->colorRange;
+ streamVideo->colorSpace = source->colorSpace;
+ streamVideo->colorTransferCharacteristic = source->colorTransferCharacteristic;
+ streamVideo->masteringMetaData = source->masteringMetaData;
+ streamVideo->contentLightMetaData = source->contentLightMetaData;
+
+ streamVideo->m_parser_split = true;
+ streamVideo->changes++;
+ map[stream->uniqueId] = streamVideo;
+ toStream = streamVideo;
+ }
+ else if (stream->type == STREAM_SUBTITLE)
+ {
+ CDemuxStreamSubtitle *source = dynamic_cast<CDemuxStreamSubtitle*>(stream);
+
+ if (!source)
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxClient::RequestStream - invalid subtitle stream with id {}",
+ stream->uniqueId);
+ DisposeStreams();
+ return;
+ }
+
+ std::shared_ptr<CDemuxStreamClientInternalTpl<CDemuxStreamSubtitle>> streamSubtitle;
+ if (currentStream)
+ streamSubtitle = std::dynamic_pointer_cast<CDemuxStreamClientInternalTpl<CDemuxStreamSubtitle>>(currentStream);
+ if (!streamSubtitle || streamSubtitle->codec != source->codec)
+ {
+ streamSubtitle.reset(new CDemuxStreamClientInternalTpl<CDemuxStreamSubtitle>());
+ streamSubtitle->m_parser = av_parser_init(source->codec);
+ if (streamSubtitle->m_parser)
+ streamSubtitle->m_parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
+ }
+
+ if (source->ExtraSize == 4)
+ {
+ streamSubtitle->ExtraData = std::make_unique<uint8_t[]>(4);
+ streamSubtitle->ExtraSize = 4;
+ for (int j=0; j<4; j++)
+ streamSubtitle->ExtraData[j] = source->ExtraData[j];
+ }
+ map[stream->uniqueId] = streamSubtitle;
+ toStream = streamSubtitle;
+ }
+ else if (stream->type == STREAM_TELETEXT)
+ {
+ CDemuxStreamTeletext *source = dynamic_cast<CDemuxStreamTeletext*>(stream);
+
+ if (!source)
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxClient::RequestStream - invalid teletext stream with id {}",
+ stream->uniqueId);
+ DisposeStreams();
+ return;
+ }
+
+ std::shared_ptr<CDemuxStreamClientInternalTpl<CDemuxStreamTeletext>> streamTeletext;
+ if (currentStream)
+ streamTeletext = std::dynamic_pointer_cast<CDemuxStreamClientInternalTpl<CDemuxStreamTeletext>>(currentStream);
+ if (!streamTeletext || streamTeletext->codec != source->codec)
+ {
+ streamTeletext.reset(new CDemuxStreamClientInternalTpl<CDemuxStreamTeletext>());
+ }
+
+ map[stream->uniqueId] = streamTeletext;
+ toStream = streamTeletext;
+ }
+ else if (stream->type == STREAM_RADIO_RDS)
+ {
+ CDemuxStreamRadioRDS *source = dynamic_cast<CDemuxStreamRadioRDS*>(stream);
+
+ if (!source)
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxClient::RequestStream - invalid radio-rds stream with id {}",
+ stream->uniqueId);
+ DisposeStreams();
+ return;
+ }
+
+ std::shared_ptr<CDemuxStreamClientInternalTpl<CDemuxStreamRadioRDS>> streamRDS;
+ if (currentStream)
+ streamRDS = std::dynamic_pointer_cast<CDemuxStreamClientInternalTpl<CDemuxStreamRadioRDS>>(currentStream);
+ if (!streamRDS || streamRDS->codec != source->codec)
+ {
+ streamRDS.reset(new CDemuxStreamClientInternalTpl<CDemuxStreamRadioRDS>());
+ }
+
+ map[stream->uniqueId] = streamRDS;
+ toStream = streamRDS;
+ }
+ else if (stream->type == STREAM_AUDIO_ID3)
+ {
+ CDemuxStreamAudioID3* source = dynamic_cast<CDemuxStreamAudioID3*>(stream);
+
+ if (!source)
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxClient::RequestStream - invalid audio ID3 stream with id {}",
+ stream->uniqueId);
+ DisposeStreams();
+ return;
+ }
+
+ std::shared_ptr<CDemuxStreamClientInternalTpl<CDemuxStreamAudioID3>> streamID3;
+ if (currentStream)
+ streamID3 = std::dynamic_pointer_cast<CDemuxStreamClientInternalTpl<CDemuxStreamAudioID3>>(
+ currentStream);
+ if (!streamID3 || streamID3->codec != source->codec)
+ {
+ streamID3 = std::make_shared<CDemuxStreamClientInternalTpl<CDemuxStreamAudioID3>>();
+ }
+
+ map[stream->uniqueId] = streamID3;
+ toStream = streamID3;
+ }
+ else
+ {
+ std::shared_ptr<CDemuxStreamClientInternalTpl<CDemuxStream>> streamGen;
+ streamGen = std::make_shared<CDemuxStreamClientInternalTpl<CDemuxStream>>();
+ map[stream->uniqueId] = streamGen;
+ toStream = streamGen;
+ }
+
+ // only update profile / level if we create a new stream
+ // existing streams may be corrected by ParsePacket
+ if (!currentStream || !CodecHasExtraData(stream->codec))
+ {
+ toStream->profile = stream->profile;
+ toStream->level = stream->level;
+ }
+
+ toStream->uniqueId = stream->uniqueId;
+ toStream->codec = stream->codec;
+ toStream->codecName = stream->codecName;
+ toStream->codec_fourcc = stream->codec_fourcc;
+ toStream->flags = stream->flags;
+ toStream->cryptoSession = stream->cryptoSession;
+ toStream->externalInterfaces = stream->externalInterfaces;
+ toStream->language = stream->language;
+ toStream->name = stream->name;
+
+ CLog::Log(LOGDEBUG, "CDVDDemuxClient::RequestStream(): added/updated stream {} with codec_id {}",
+ toStream->uniqueId, toStream->codec);
+}
+
+std::shared_ptr<CDemuxStream> CDVDDemuxClient::GetStreamInternal(int iStreamId)
+{
+ auto stream = m_streams.find(iStreamId);
+ if (stream != m_streams.end())
+ {
+ return stream->second;
+ }
+ else
+ return nullptr;
+}
+
+int CDVDDemuxClient::GetNrOfStreams() const
+{
+ return m_streams.size();
+}
+
+bool CDVDDemuxClient::IsVideoReady()
+{
+ for (const auto& stream : m_streams)
+ {
+ if (stream.first == m_videoStreamPlaying &&
+ stream.second->type == STREAM_VIDEO &&
+ CodecHasExtraData(stream.second->codec) &&
+ stream.second->ExtraData == nullptr)
+ return false;
+ }
+ return true;
+}
+
+std::string CDVDDemuxClient::GetFileName()
+{
+ if (m_pInput)
+ return m_pInput->GetFileName();
+ else
+ return "";
+}
+
+std::string CDVDDemuxClient::GetStreamCodecName(int iStreamId)
+{
+ CDemuxStream *stream = GetStream(iStreamId);
+ std::string strName;
+ if (stream)
+ {
+ if (stream->codec == AV_CODEC_ID_AC3)
+ strName = "ac3";
+ else if (stream->codec == AV_CODEC_ID_MP2)
+ strName = "mp2";
+ else if (stream->codec == AV_CODEC_ID_AAC)
+ strName = "aac";
+ else if (stream->codec == AV_CODEC_ID_DTS)
+ strName = "dca";
+ else if (stream->codec == AV_CODEC_ID_MPEG2VIDEO)
+ strName = "mpeg2video";
+ else if (stream->codec == AV_CODEC_ID_H264)
+ strName = "h264";
+ else if (stream->codec == AV_CODEC_ID_EAC3)
+ strName = "eac3";
+ else if (stream->codec == AV_CODEC_ID_VP8)
+ strName = "vp8";
+ else if (stream->codec == AV_CODEC_ID_VP9)
+ strName = "vp9";
+ else if (stream->codec == AV_CODEC_ID_HEVC)
+ strName = "hevc";
+ else if (stream->codec == AV_CODEC_ID_AV1)
+ strName = "av1";
+ }
+ return strName;
+}
+
+bool CDVDDemuxClient::SeekTime(double timems, bool backwards, double *startpts)
+{
+ if (m_IDemux)
+ {
+ m_displayTime = 0;
+ m_dtsAtDisplayTime = DVD_NOPTS_VALUE;
+ return m_IDemux->SeekTime(timems, backwards, startpts);
+ }
+ return false;
+}
+
+void CDVDDemuxClient::SetSpeed (int speed)
+{
+ if (m_IDemux)
+ {
+ m_IDemux->SetSpeed(speed);
+ }
+}
+
+void CDVDDemuxClient::FillBuffer(bool mode)
+{
+ if (m_IDemux)
+ {
+ m_IDemux->FillBuffer(mode);
+ }
+}
+
+void CDVDDemuxClient::EnableStream(int id, bool enable)
+{
+ if (m_IDemux)
+ {
+ m_IDemux->EnableStream(id, enable);
+ }
+}
+
+void CDVDDemuxClient::OpenStream(int id)
+{
+ // OpenStream may change some parameters
+ // in this case we need to reset our stream properties
+ if (m_IDemux)
+ {
+ bool bOpenStream = m_IDemux->OpenStream(id);
+
+ CDemuxStream *stream(m_IDemux->GetStream(id));
+ if (stream && stream->type == STREAM_VIDEO)
+ m_videoStreamPlaying = id;
+
+ if (bOpenStream)
+ SetStreamProps(stream, m_streams, true);
+ }
+}
+
+void CDVDDemuxClient::SetVideoResolution(unsigned int width, unsigned int height)
+{
+ if (m_IDemux)
+ {
+ m_IDemux->SetVideoResolution(width, height, width, height);
+ }
+}
+
+bool CDVDDemuxClient::CodecHasExtraData(AVCodecID id)
+{
+ switch (id)
+ {
+ case AV_CODEC_ID_VP9:
+ return false;
+ default:
+ return true;
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxClient.h b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxClient.h
new file mode 100644
index 0000000..9b5ac42
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxClient.h
@@ -0,0 +1,66 @@
+/*
+ * 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 "DVDDemux.h"
+#include "DVDInputStreams/DVDInputStream.h"
+
+#include <map>
+#include <vector>
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+}
+
+class CDVDDemuxClient : public CDVDDemux
+{
+public:
+
+ CDVDDemuxClient();
+ ~CDVDDemuxClient() override;
+
+ bool Open(std::shared_ptr<CDVDInputStream> pInput);
+ void Dispose();
+ bool Reset() override;
+ void Abort() override;
+ void Flush() override;
+ DemuxPacket* Read() override;
+ bool SeekTime(double time, bool backwards = false, double* startpts = NULL) override;
+ void SetSpeed(int iSpeed) override;
+ void FillBuffer(bool mode) override;
+ CDemuxStream* GetStream(int iStreamId) const override;
+ std::vector<CDemuxStream*> GetStreams() const override;
+ int GetNrOfStreams() const override;
+ std::string GetFileName() override;
+ std::string GetStreamCodecName(int iStreamId) override;
+ void EnableStream(int id, bool enable) override;
+ void OpenStream(int id) override;
+ void SetVideoResolution(unsigned int width, unsigned int height) override;
+
+protected:
+ void RequestStreams();
+ void SetStreamProps(CDemuxStream *stream, std::map<int, std::shared_ptr<CDemuxStream>> &map, bool forceInit);
+ bool ParsePacket(DemuxPacket* pPacket);
+ void DisposeStreams();
+ std::shared_ptr<CDemuxStream> GetStreamInternal(int iStreamId);
+ bool IsVideoReady();
+
+ std::shared_ptr<CDVDInputStream> m_pInput;
+ std::shared_ptr<CDVDInputStream::IDemux> m_IDemux;
+ std::map<int, std::shared_ptr<CDemuxStream>> m_streams;
+ int m_displayTime;
+ double m_dtsAtDisplayTime;
+ std::unique_ptr<DemuxPacket> m_packet;
+ int m_videoStreamPlaying = -1;
+
+private:
+ static inline bool CodecHasExtraData(AVCodecID id);
+};
+
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxFFmpeg.cpp b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxFFmpeg.cpp
new file mode 100644
index 0000000..739bf51
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxFFmpeg.cpp
@@ -0,0 +1,2549 @@
+/*
+ * 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 "DVDDemuxFFmpeg.h"
+
+#include "DVDDemuxUtils.h"
+#include "DVDInputStreams/DVDInputStream.h"
+#include "DVDInputStreams/DVDInputStreamFFmpeg.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "commons/Exception.h"
+#include "cores/FFmpeg.h"
+#include "cores/MenuType.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h" // for DVD_TIME_BASE
+#include "filesystem/CurlFile.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SystemClock.h"
+#include "utils/FontUtils.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <sstream>
+#include <utility>
+
+extern "C"
+{
+#include "libavutil/pixdesc.h"
+}
+
+#ifdef HAVE_LIBBLURAY
+#include "DVDInputStreams/DVDInputStreamBluray.h"
+#endif
+#ifndef __STDC_CONSTANT_MACROS
+#define __STDC_CONSTANT_MACROS
+#endif
+#ifndef __STDC_LIMIT_MACROS
+#define __STDC_LIMIT_MACROS
+#endif
+#ifdef TARGET_POSIX
+#include <stdint.h>
+#endif
+
+extern "C" {
+#include <libavutil/dict.h>
+#include <libavutil/opt.h>
+}
+
+using namespace std::chrono_literals;
+
+struct StereoModeConversionMap
+{
+ const char* name;
+ const char* mode;
+};
+
+// we internally use the matroska string representation of stereoscopic modes.
+// This struct is a conversion map to convert stereoscopic mode values
+// from asf/wmv to the internally used matroska ones
+static const struct StereoModeConversionMap WmvToInternalStereoModeMap[] =
+{
+ { "SideBySideRF", "right_left" },
+ { "SideBySideLF", "left_right" },
+ { "OverUnderRT", "bottom_top" },
+ { "OverUnderLT", "top_bottom" },
+ {}
+};
+
+namespace
+{
+const std::vector<std::string> font_mimetypes = {"application/x-truetype-font",
+ "application/vnd.ms-opentype",
+ "application/x-font-ttf",
+ "application/x-font", // probably incorrect
+ "application/font-sfnt",
+ "font/collection",
+ "font/otf",
+ "font/sfnt",
+ "font/ttf"};
+
+bool AttachmentIsFont(const AVDictionaryEntry* dict)
+{
+ if (dict)
+ {
+ const std::string mimeType = dict->value;
+ return std::find_if(font_mimetypes.begin(), font_mimetypes.end(),
+ [&mimeType](const std::string& str) { return str == mimeType; }) !=
+ font_mimetypes.end();
+ }
+ return false;
+}
+} // namespace
+
+#define FF_MAX_EXTRADATA_SIZE ((1 << 28) - AV_INPUT_BUFFER_PADDING_SIZE)
+
+std::string CDemuxStreamAudioFFmpeg::GetStreamName()
+{
+ if (!m_stream)
+ return "";
+ if (!m_description.empty())
+ return m_description;
+ else
+ return CDemuxStream::GetStreamName();
+}
+
+std::string CDemuxStreamSubtitleFFmpeg::GetStreamName()
+{
+ if (!m_stream)
+ return "";
+ if (!m_description.empty())
+ return m_description;
+ else
+ return CDemuxStream::GetStreamName();
+}
+
+std::string CDemuxStreamVideoFFmpeg::GetStreamName()
+{
+ if (!m_stream)
+ return "";
+ if (!m_description.empty())
+ return m_description;
+ else
+ return CDemuxStream::GetStreamName();
+}
+
+CDemuxParserFFmpeg::~CDemuxParserFFmpeg()
+{
+ if (m_codecCtx)
+ avcodec_free_context(&m_codecCtx);
+ if (m_parserCtx)
+ {
+ av_parser_close(m_parserCtx);
+ m_parserCtx = nullptr;
+ }
+}
+
+static int interrupt_cb(void* ctx)
+{
+ CDVDDemuxFFmpeg* demuxer = static_cast<CDVDDemuxFFmpeg*>(ctx);
+ if (demuxer && demuxer->Aborted())
+ return 1;
+ return 0;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////
+/*
+static int dvd_file_open(URLContext* h, const char* filename, int flags)
+{
+ return -1;
+}
+*/
+
+static int dvd_file_read(void* h, uint8_t* buf, int size)
+{
+ if (interrupt_cb(h))
+ return AVERROR_EXIT;
+
+ std::shared_ptr<CDVDInputStream> pInputStream = static_cast<CDVDDemuxFFmpeg*>(h)->m_pInput;
+ int len = pInputStream->Read(buf, size);
+ if (len == 0)
+ return AVERROR_EOF;
+ else
+ return len;
+}
+/*
+static int dvd_file_write(URLContext* h, uint8_t* buf, int size)
+{
+ return -1;
+}
+*/
+static int64_t dvd_file_seek(void* h, int64_t pos, int whence)
+{
+ if (interrupt_cb(h))
+ return AVERROR_EXIT;
+
+ std::shared_ptr<CDVDInputStream> pInputStream = static_cast<CDVDDemuxFFmpeg*>(h)->m_pInput;
+ if (whence == AVSEEK_SIZE)
+ return pInputStream->GetLength();
+ else
+ return pInputStream->Seek(pos, whence & ~AVSEEK_FORCE);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////////
+
+CDVDDemuxFFmpeg::CDVDDemuxFFmpeg() : CDVDDemux()
+{
+ m_pFormatContext = NULL;
+ m_ioContext = NULL;
+ m_currentPts = DVD_NOPTS_VALUE;
+ m_bMatroska = false;
+ m_bAVI = false;
+ m_bSup = false;
+ m_speed = DVD_PLAYSPEED_NORMAL;
+ m_program = UINT_MAX;
+ m_pkt.result = -1;
+ memset(&m_pkt.pkt, 0, sizeof(AVPacket));
+ m_streaminfo = true; /* set to true if we want to look for streams before playback */
+ m_checkTransportStream = false;
+ m_dtsAtDisplayTime = DVD_NOPTS_VALUE;
+}
+
+CDVDDemuxFFmpeg::~CDVDDemuxFFmpeg()
+{
+ Dispose();
+ ff_flush_avutil_log_buffers();
+}
+
+bool CDVDDemuxFFmpeg::Aborted()
+{
+ if (m_timeout.IsTimePast())
+ return true;
+
+ std::shared_ptr<CDVDInputStreamFFmpeg> input = std::dynamic_pointer_cast<CDVDInputStreamFFmpeg>(m_pInput);
+ if (input && input->Aborted())
+ return true;
+
+ return false;
+}
+
+bool CDVDDemuxFFmpeg::Open(const std::shared_ptr<CDVDInputStream>& pInput, bool fileinfo)
+{
+ AVInputFormat* iformat = NULL;
+ std::string strFile;
+ m_streaminfo = !pInput->IsRealtime() && !m_reopen;
+ m_reopen = false;
+ m_currentPts = DVD_NOPTS_VALUE;
+ m_speed = DVD_PLAYSPEED_NORMAL;
+ m_program = UINT_MAX;
+ m_seekToKeyFrame = false;
+
+ const AVIOInterruptCB int_cb = { interrupt_cb, this };
+
+ if (!pInput)
+ return false;
+
+ m_pInput = pInput;
+ strFile = m_pInput->GetFileName();
+
+ if (m_pInput->GetContent().length() > 0)
+ {
+ std::string content = m_pInput->GetContent();
+ StringUtils::ToLower(content);
+
+ /* check if we can get a hint from content */
+ if (content.compare("video/x-vobsub") == 0)
+ iformat = av_find_input_format("mpeg");
+ else if (content.compare("video/x-dvd-mpeg") == 0)
+ iformat = av_find_input_format("mpeg");
+ else if (content.compare("video/mp2t") == 0)
+ iformat = av_find_input_format("mpegts");
+ else if (content.compare("multipart/x-mixed-replace") == 0)
+ iformat = av_find_input_format("mjpeg");
+ }
+
+ // open the demuxer
+ m_pFormatContext = avformat_alloc_context();
+ m_pFormatContext->interrupt_callback = int_cb;
+
+ // try to abort after 30 seconds
+ m_timeout.Set(30s);
+
+ if (m_pInput->IsStreamType(DVDSTREAM_TYPE_FFMPEG))
+ {
+ // special stream type that makes avformat handle file opening
+ // allows internal ffmpeg protocols to be used
+ AVDictionary* options = GetFFMpegOptionsFromInput();
+
+ CURL url = m_pInput->GetURL();
+
+ int result = -1;
+ if (url.IsProtocol("mms"))
+ {
+ // try mmsh, then mmst
+ url.SetProtocol("mmsh");
+ url.SetProtocolOptions("");
+ result = avformat_open_input(&m_pFormatContext, url.Get().c_str(), iformat, &options);
+ if (result < 0)
+ {
+ url.SetProtocol("mmst");
+ strFile = url.Get();
+ }
+ }
+ else if (url.IsProtocol("udp") || url.IsProtocol("rtp"))
+ {
+ std::string strURL = url.Get();
+ CLog::Log(LOGDEBUG, "CDVDDemuxFFmpeg::Open() UDP/RTP Original URL '{}'", strURL);
+ size_t found = strURL.find("://");
+ if (found != std::string::npos)
+ {
+ size_t start = found + 3;
+ found = strURL.find('@');
+
+ if (found != std::string::npos && found > start)
+ {
+ // sourceip found
+ std::string strSourceIp = strURL.substr(start, found - start);
+
+ strFile = strURL.substr(0, start);
+ strFile += strURL.substr(found);
+ if (strFile.back() == '/')
+ strFile.pop_back();
+ strFile += "?sources=";
+ strFile += strSourceIp;
+ CLog::Log(LOGDEBUG, "CDVDDemuxFFmpeg::Open() UDP/RTP URL '{}'", strFile);
+ }
+ }
+ }
+ if (result < 0)
+ {
+ m_pFormatContext->flags |= AVFMT_FLAG_PRIV_OPT;
+ if (avformat_open_input(&m_pFormatContext, strFile.c_str(), iformat, &options) < 0)
+ {
+ CLog::Log(LOGDEBUG, "Error, could not open file {}", CURL::GetRedacted(strFile));
+ Dispose();
+ av_dict_free(&options);
+ return false;
+ }
+ av_dict_free(&options);
+ avformat_close_input(&m_pFormatContext);
+ m_pFormatContext = avformat_alloc_context();
+ m_pFormatContext->interrupt_callback = int_cb;
+ m_pFormatContext->flags &= ~AVFMT_FLAG_PRIV_OPT;
+ AVDictionary* options = GetFFMpegOptionsFromInput();
+ av_dict_set_int(&options, "load_all_variants", 0, AV_OPT_SEARCH_CHILDREN);
+ if (avformat_open_input(&m_pFormatContext, strFile.c_str(), iformat, &options) < 0)
+ {
+ CLog::Log(LOGDEBUG, "Error, could not open file (2) {}", CURL::GetRedacted(strFile));
+ Dispose();
+ av_dict_free(&options);
+ return false;
+ }
+ }
+ av_dict_free(&options);
+ }
+ else
+ {
+ bool seekable = true;
+ if (m_pInput->Seek(0, SEEK_POSSIBLE) == 0)
+ {
+ seekable = false;
+ }
+ int bufferSize = 4096;
+ int blockSize = m_pInput->GetBlockSize();
+
+ if (blockSize > 1 && seekable) // non seakable input streams are not supposed to set block size
+ bufferSize = blockSize;
+
+ unsigned char* buffer = (unsigned char*)av_malloc(bufferSize);
+ m_ioContext = avio_alloc_context(buffer, bufferSize, 0, this, dvd_file_read, NULL, dvd_file_seek);
+
+ if (blockSize > 1 && seekable)
+ m_ioContext->max_packet_size = bufferSize;
+
+ if (!seekable)
+ m_ioContext->seekable = 0;
+
+ std::string content = m_pInput->GetContent();
+ StringUtils::ToLower(content);
+ if (StringUtils::StartsWith(content, "audio/l16"))
+ iformat = av_find_input_format("s16be");
+
+ if (iformat == nullptr)
+ {
+ // let ffmpeg decide which demuxer we have to open
+ bool trySPDIFonly = (m_pInput->GetContent() == "audio/x-spdif-compressed");
+
+ if (!trySPDIFonly)
+ av_probe_input_buffer(m_ioContext, &iformat, strFile.c_str(), NULL, 0, 0);
+
+ // Use the more low-level code in case we have been built against an old
+ // FFmpeg without the above av_probe_input_buffer(), or in case we only
+ // want to probe for spdif (DTS or IEC 61937) compressed audio
+ // specifically, or in case the file is a wav which may contain DTS or
+ // IEC 61937 (e.g. ac3-in-wav) and we want to check for those formats.
+ if (trySPDIFonly || (iformat && strcmp(iformat->name, "wav") == 0))
+ {
+ AVProbeData pd;
+ int probeBufferSize = 32768;
+ std::unique_ptr<uint8_t[]> probe_buffer (new uint8_t[probeBufferSize + AVPROBE_PADDING_SIZE]);
+
+ // init probe data
+ pd.buf = probe_buffer.get();
+ pd.filename = strFile.c_str();
+
+ // read data using avformat's buffers
+ pd.buf_size = avio_read(m_ioContext, pd.buf, probeBufferSize);
+ if (pd.buf_size <= 0)
+ {
+ CLog::Log(LOGERROR, "{} - error reading from input stream, {}", __FUNCTION__,
+ CURL::GetRedacted(strFile));
+ return false;
+ }
+ memset(pd.buf + pd.buf_size, 0, AVPROBE_PADDING_SIZE);
+
+ // restore position again
+ avio_seek(m_ioContext , 0, SEEK_SET);
+
+ // the advancedsetting is for allowing the user to force outputting the
+ // 44.1 kHz DTS wav file as PCM, so that an A/V receiver can decode
+ // it (this is temporary until we handle 44.1 kHz passthrough properly)
+ if (trySPDIFonly || (iformat && strcmp(iformat->name, "wav") == 0 && !CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_VideoPlayerIgnoreDTSinWAV))
+ {
+ // check for spdif and dts
+ // This is used with wav files and audio CDs that may contain
+ // a DTS or AC3 track padded for S/PDIF playback. If neither of those
+ // is present, we assume it is PCM audio.
+ // AC3 is always wrapped in iec61937 (ffmpeg "spdif"), while DTS
+ // may be just padded.
+ AVInputFormat* iformat2;
+ iformat2 = av_find_input_format("spdif");
+
+ if (iformat2 && iformat2->read_probe(&pd) > AVPROBE_SCORE_MAX / 4)
+ {
+ iformat = iformat2;
+ }
+ else
+ {
+ // not spdif or no spdif demuxer, try dts
+ iformat2 = av_find_input_format("dts");
+
+ if (iformat2 && iformat2->read_probe(&pd) > AVPROBE_SCORE_MAX / 4)
+ {
+ iformat = iformat2;
+ }
+ else if (trySPDIFonly)
+ {
+ // not dts either, return false in case we were explicitly
+ // requested to only check for S/PDIF padded compressed audio
+ CLog::Log(LOGDEBUG, "{} - not spdif or dts file, falling back", __FUNCTION__);
+ return false;
+ }
+ }
+ }
+ }
+
+ if (!iformat)
+ {
+ std::string content = m_pInput->GetContent();
+
+ /* check if we can get a hint from content */
+ if (content.compare("audio/aacp") == 0)
+ iformat = av_find_input_format("aac");
+ else if (content.compare("audio/aac") == 0)
+ iformat = av_find_input_format("aac");
+ else if (content.compare("video/flv") == 0)
+ iformat = av_find_input_format("flv");
+ else if (content.compare("video/x-flv") == 0)
+ iformat = av_find_input_format("flv");
+ }
+
+ if (!iformat)
+ {
+ CLog::Log(LOGERROR, "{} - error probing input format, {}", __FUNCTION__,
+ CURL::GetRedacted(strFile));
+ return false;
+ }
+ else
+ {
+ if (iformat->name)
+ CLog::Log(LOGDEBUG, "{} - probing detected format [{}]", __FUNCTION__, iformat->name);
+ else
+ CLog::Log(LOGDEBUG, "{} - probing detected unnamed format", __FUNCTION__);
+ }
+ }
+
+
+ m_pFormatContext->pb = m_ioContext;
+
+ AVDictionary* options = NULL;
+ if (iformat->name && (strcmp(iformat->name, "mp3") == 0 || strcmp(iformat->name, "mp2") == 0))
+ {
+ CLog::Log(LOGDEBUG, "{} - setting usetoc to 0 for accurate VBR MP3 seek", __FUNCTION__);
+ av_dict_set(&options, "usetoc", "0", 0);
+ }
+
+ if (StringUtils::StartsWith(content, "audio/l16"))
+ {
+ int channels = 2;
+ int samplerate = 44100;
+ GetL16Parameters(channels, samplerate);
+ av_dict_set_int(&options, "channels", channels, 0);
+ av_dict_set_int(&options, "sample_rate", samplerate, 0);
+ }
+
+ if (avformat_open_input(&m_pFormatContext, strFile.c_str(), iformat, &options) < 0)
+ {
+ CLog::Log(LOGERROR, "{} - Error, could not open file {}", __FUNCTION__,
+ CURL::GetRedacted(strFile));
+ Dispose();
+ av_dict_free(&options);
+ return false;
+ }
+ av_dict_free(&options);
+ }
+
+ // Avoid detecting framerate if advancedsettings.xml says so
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoFpsDetect == 0)
+ m_pFormatContext->fps_probe_size = 0;
+
+ // analyse very short to speed up mjpeg playback start
+ if (iformat && (strcmp(iformat->name, "mjpeg") == 0) && m_ioContext->seekable == 0)
+ av_opt_set_int(m_pFormatContext, "analyzeduration", 500000, 0);
+
+ bool skipCreateStreams = false;
+ bool isBluray = pInput->IsStreamType(DVDSTREAM_TYPE_BLURAY);
+
+ // this should never happen. Log it to inform about the error.
+ if (m_pFormatContext->nb_streams > 0 && m_pFormatContext->streams == nullptr)
+ {
+ CLog::LogF(LOGERROR, "Detected number of streams is greater than zero but AVStream array is "
+ "empty. Please report this bug.");
+ }
+
+ // don't re-open mpegts streams with hevc encoding as the params are not correctly detected again
+ if (iformat && (strcmp(iformat->name, "mpegts") == 0) && !fileinfo && !isBluray &&
+ m_pFormatContext->nb_streams > 0 && m_pFormatContext->streams != nullptr &&
+ m_pFormatContext->streams[0]->codecpar->codec_id != AV_CODEC_ID_HEVC)
+ {
+ av_opt_set_int(m_pFormatContext, "analyzeduration", 500000, 0);
+ m_checkTransportStream = true;
+ skipCreateStreams = true;
+ }
+ else if (!iformat || ((strcmp(iformat->name, "mpegts") != 0) ||
+ ((strcmp(iformat->name, "mpegts") == 0) &&
+ m_pFormatContext->nb_streams > 0 && m_pFormatContext->streams != nullptr &&
+ m_pFormatContext->streams[0]->codecpar->codec_id == AV_CODEC_ID_HEVC)))
+ {
+ m_streaminfo = true;
+ }
+
+ if (iformat && (strcmp(iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0))
+ {
+ if (URIUtils::IsRemote(strFile))
+ m_pFormatContext->iformat->flags |= AVFMT_NOGENSEARCH;
+ }
+
+ // we need to know if this is matroska, avi or sup later
+ m_bMatroska = strncmp(m_pFormatContext->iformat->name, "matroska", 8) == 0; // for "matroska.webm"
+ m_bAVI = strcmp(m_pFormatContext->iformat->name, "avi") == 0;
+ m_bSup = strcmp(m_pFormatContext->iformat->name, "sup") == 0;
+
+ if (m_streaminfo)
+ {
+ /* to speed up dvd switches, only analyse very short */
+ if (m_pInput->IsStreamType(DVDSTREAM_TYPE_DVD))
+ av_opt_set_int(m_pFormatContext, "analyzeduration", 500000, 0);
+
+ CLog::Log(LOGDEBUG, "{} - avformat_find_stream_info starting", __FUNCTION__);
+ int iErr = avformat_find_stream_info(m_pFormatContext, NULL);
+ if (iErr < 0)
+ {
+ CLog::Log(LOGWARNING, "could not find codec parameters for {}", CURL::GetRedacted(strFile));
+ if (m_pInput->IsStreamType(DVDSTREAM_TYPE_DVD) ||
+ m_pInput->IsStreamType(DVDSTREAM_TYPE_BLURAY) ||
+ (m_pFormatContext->nb_streams == 1 &&
+ m_pFormatContext->streams[0]->codecpar->codec_id == AV_CODEC_ID_AC3) ||
+ m_checkTransportStream)
+ {
+ // special case, our codecs can still handle it.
+ }
+ else
+ {
+ Dispose();
+ return false;
+ }
+ }
+ CLog::Log(LOGDEBUG, "{} - av_find_stream_info finished", __FUNCTION__);
+
+ // print some extra information
+ av_dump_format(m_pFormatContext, 0, CURL::GetRedacted(strFile).c_str(), 0);
+
+ if (m_checkTransportStream)
+ {
+ // make sure we start video with an i-frame
+ ResetVideoStreams();
+ }
+ }
+ else
+ {
+ m_program = 0;
+ m_checkTransportStream = true;
+ skipCreateStreams = true;
+ }
+
+ // reset any timeout
+ m_timeout.SetInfinite();
+
+ // if format can be nonblocking, let's use that
+ m_pFormatContext->flags |= AVFMT_FLAG_NONBLOCK;
+
+ // deprecated, will be always set in future versions
+ m_pFormatContext->flags |= AVFMT_FLAG_KEEP_SIDE_DATA;
+
+ UpdateCurrentPTS();
+
+ // select the correct program if requested
+ m_initialProgramNumber = UINT_MAX;
+ CVariant programProp(pInput->GetProperty("program"));
+ if (!programProp.isNull())
+ m_initialProgramNumber = static_cast<int>(programProp.asInteger());
+
+ // in case of mpegts and we have not seen pat/pmt, defer creation of streams
+ if (!skipCreateStreams || m_pFormatContext->nb_programs > 0)
+ {
+ unsigned int nProgram = UINT_MAX;
+ if (m_pFormatContext->nb_programs > 0)
+ {
+ // select the correct program if requested
+ if (m_initialProgramNumber != UINT_MAX)
+ {
+ for (unsigned int i = 0; i < m_pFormatContext->nb_programs; ++i)
+ {
+ if (m_pFormatContext->programs[i]->program_num == static_cast<int>(m_initialProgramNumber))
+ {
+ nProgram = i;
+ m_initialProgramNumber = UINT_MAX;
+ break;
+ }
+ }
+ }
+ else if (m_pFormatContext->iformat && strcmp(m_pFormatContext->iformat->name, "hls") == 0)
+ {
+ nProgram = HLSSelectProgram();
+ }
+ else
+ {
+ // skip programs without or empty audio/video streams
+ for (unsigned int i = 0; nProgram == UINT_MAX && i < m_pFormatContext->nb_programs; i++)
+ {
+ for (unsigned int j = 0; j < m_pFormatContext->programs[i]->nb_stream_indexes; j++)
+ {
+ int idx = m_pFormatContext->programs[i]->stream_index[j];
+ AVStream* st = m_pFormatContext->streams[idx];
+ if ((st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && st->codec_info_nb_frames > 0) ||
+ (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && st->codecpar->sample_rate > 0))
+ {
+ nProgram = i;
+ break;
+ }
+ }
+ }
+ }
+ }
+ CreateStreams(nProgram);
+ }
+
+ m_newProgram = m_program;
+
+ // allow IsProgramChange to return true
+ if (skipCreateStreams && GetNrOfStreams() == 0)
+ m_program = 0;
+
+ m_displayTime = 0;
+ m_dtsAtDisplayTime = DVD_NOPTS_VALUE;
+ m_startTime = 0;
+ m_seekStream = -1;
+
+ if (m_checkTransportStream && m_streaminfo)
+ {
+ int64_t duration = m_pFormatContext->duration;
+ std::shared_ptr<CDVDInputStream> pInputStream = m_pInput;
+ Dispose();
+ m_reopen = true;
+ if (!Open(pInputStream, false))
+ return false;
+ m_pFormatContext->duration = duration;
+ }
+
+ return true;
+}
+
+void CDVDDemuxFFmpeg::Dispose()
+{
+ m_pkt.result = -1;
+ av_packet_unref(&m_pkt.pkt);
+
+ if (m_pFormatContext)
+ {
+ if (m_ioContext && m_pFormatContext->pb && m_pFormatContext->pb != m_ioContext)
+ {
+ CLog::Log(LOGWARNING, "CDVDDemuxFFmpeg::Dispose - demuxer changed our byte context behind our back, possible memleak");
+ m_ioContext = m_pFormatContext->pb;
+ }
+ avformat_close_input(&m_pFormatContext);
+ }
+
+ if (m_ioContext)
+ {
+ av_free(m_ioContext->buffer);
+ av_free(m_ioContext);
+ }
+
+ m_ioContext = NULL;
+ m_pFormatContext = NULL;
+ m_speed = DVD_PLAYSPEED_NORMAL;
+
+ DisposeStreams();
+
+ m_pInput = NULL;
+}
+
+bool CDVDDemuxFFmpeg::Reset()
+{
+ std::shared_ptr<CDVDInputStream> pInputStream = m_pInput;
+ Dispose();
+ return Open(pInputStream, false);
+}
+
+void CDVDDemuxFFmpeg::Flush()
+{
+ if (m_pFormatContext)
+ {
+ if (m_pFormatContext->pb)
+ avio_flush(m_pFormatContext->pb);
+ avformat_flush(m_pFormatContext);
+ }
+
+ m_currentPts = DVD_NOPTS_VALUE;
+
+ m_pkt.result = -1;
+ av_packet_unref(&m_pkt.pkt);
+
+ m_displayTime = 0;
+ m_dtsAtDisplayTime = DVD_NOPTS_VALUE;
+ m_seekToKeyFrame = false;
+}
+
+void CDVDDemuxFFmpeg::Abort()
+{
+ m_timeout.SetExpired();
+}
+
+void CDVDDemuxFFmpeg::SetSpeed(int iSpeed)
+{
+ if (!m_pFormatContext)
+ return;
+
+ if (m_speed == iSpeed)
+ return;
+
+ if (m_speed != DVD_PLAYSPEED_PAUSE && iSpeed == DVD_PLAYSPEED_PAUSE)
+ av_read_pause(m_pFormatContext);
+ else if (m_speed == DVD_PLAYSPEED_PAUSE && iSpeed != DVD_PLAYSPEED_PAUSE)
+ av_read_play(m_pFormatContext);
+ m_speed = iSpeed;
+
+ AVDiscard discard = AVDISCARD_NONE;
+ if (m_speed > 4 * DVD_PLAYSPEED_NORMAL)
+ discard = AVDISCARD_NONKEY;
+ else if (m_speed > 2 * DVD_PLAYSPEED_NORMAL)
+ discard = AVDISCARD_BIDIR;
+ else if (m_speed < DVD_PLAYSPEED_PAUSE)
+ discard = AVDISCARD_NONKEY;
+
+
+ for(unsigned int i = 0; i < m_pFormatContext->nb_streams; i++)
+ {
+ if (m_pFormatContext->streams[i])
+ {
+ if (m_pFormatContext->streams[i]->discard != AVDISCARD_ALL)
+ m_pFormatContext->streams[i]->discard = discard;
+ }
+ }
+}
+
+AVDictionary* CDVDDemuxFFmpeg::GetFFMpegOptionsFromInput()
+{
+ const std::shared_ptr<CDVDInputStreamFFmpeg> input =
+ std::dynamic_pointer_cast<CDVDInputStreamFFmpeg>(m_pInput);
+
+ CURL url = m_pInput->GetURL();
+ AVDictionary* options = nullptr;
+
+ // For a local file we need the following protocol whitelist
+ if (url.GetProtocol().empty() || url.IsProtocol("file"))
+ av_dict_set(&options, "protocol_whitelist", "file,http,https,tcp,tls,crypto", 0);
+
+ if (url.IsProtocol("http") || url.IsProtocol("https"))
+ {
+ std::map<std::string, std::string> protocolOptions;
+ url.GetProtocolOptions(protocolOptions);
+ std::string headers;
+ bool hasUserAgent = false;
+ bool hasCookies = false;
+ for(std::map<std::string, std::string>::const_iterator it = protocolOptions.begin(); it != protocolOptions.end(); ++it)
+ {
+ std::string name = it->first;
+ StringUtils::ToLower(name);
+ const std::string &value = it->second;
+
+ // set any of these ffmpeg options
+ if (name == "seekable" || name == "reconnect" || name == "reconnect_at_eof" ||
+ name == "reconnect_streamed" || name == "reconnect_delay_max" ||
+ name == "icy" || name == "icy_metadata_headers" || name == "icy_metadata_packet")
+ {
+ CLog::Log(LOGDEBUG,
+ "CDVDDemuxFFmpeg::GetFFMpegOptionsFromInput() adding ffmpeg option '{}: {}'",
+ it->first, value);
+ av_dict_set(&options, name.c_str(), value.c_str(), 0);
+ }
+ // map some standard http headers to the ffmpeg related options
+ else if (name == "user-agent")
+ {
+ av_dict_set(&options, "user_agent", value.c_str(), 0);
+ CLog::Log(
+ LOGDEBUG,
+ "CDVDDemuxFFmpeg::GetFFMpegOptionsFromInput() adding ffmpeg option 'user_agent: {}'",
+ value);
+ hasUserAgent = true;
+ }
+ else if (name == "cookies")
+ {
+ // in the plural option expect multiple Set-Cookie values. They are passed \n delimited to FFMPEG
+ av_dict_set(&options, "cookies", value.c_str(), 0);
+ CLog::Log(LOGDEBUG,
+ "CDVDDemuxFFmpeg::GetFFMpegOptionsFromInput() adding ffmpeg option 'cookies: {}'",
+ value);
+ hasCookies = true;
+ }
+ else if (name == "cookie")
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "CDVDDemuxFFmpeg::GetFFMpegOptionsFromInput() adding ffmpeg header value 'cookie: {}'",
+ value);
+ headers.append(it->first).append(": ").append(value).append("\r\n");
+ hasCookies = true;
+ }
+ // other standard headers (see https://en.wikipedia.org/wiki/List_of_HTTP_header_fields) are appended as actual headers
+ else if (name == "accept" || name == "accept-language" || name == "accept-datetime" ||
+ name == "authorization" || name == "cache-control" || name == "connection" || name == "content-md5" ||
+ name == "date" || name == "expect" || name == "forwarded" || name == "from" || name == "if-match" ||
+ name == "if-modified-since" || name == "if-none-match" || name == "if-range" || name == "if-unmodified-since" ||
+ name == "max-forwards" || name == "origin" || name == "pragma" || name == "range" || name == "referer" ||
+ name == "te" || name == "upgrade" || name == "via" || name == "warning" || name == "x-requested-with" ||
+ name == "dnt" || name == "x-forwarded-for" || name == "x-forwarded-host" || name == "x-forwarded-proto" ||
+ name == "front-end-https" || name == "x-http-method-override" || name == "x-att-deviceid" ||
+ name == "x-wap-profile" || name == "x-uidh" || name == "x-csrf-token" || name == "x-request-id" ||
+ name == "x-correlation-id")
+ {
+ if (name == "authorization")
+ {
+ CLog::Log(LOGDEBUG,
+ "CDVDDemuxFFmpeg::GetFFMpegOptionsFromInput() adding custom header option '{}: "
+ "***********'",
+ it->first);
+ }
+ else
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "CDVDDemuxFFmpeg::GetFFMpegOptionsFromInput() adding custom header option '{}: {}'",
+ it->first, value);
+ }
+ headers.append(it->first).append(": ").append(value).append("\r\n");
+ }
+ // Any other headers that need to be sent would be user defined and should be prefixed
+ // by a `!`. We mask these values so we don't log anything we shouldn't
+ else if (name.length() > 0 && name[0] == '!')
+ {
+ CLog::Log(LOGDEBUG,
+ "CDVDDemuxFFmpeg::GetFFMpegOptionsFromInput() adding user custom header option "
+ "'{}: ***********'",
+ it->first);
+ headers.append(it->first.substr(1)).append(": ").append(value).append("\r\n");
+ }
+ // for everything else we ignore the headers options if not specified above
+ else
+ {
+ CLog::Log(LOGDEBUG,
+ "CDVDDemuxFFmpeg::GetFFMpegOptionsFromInput() ignoring header option '{}'",
+ it->first);
+ }
+ }
+ if (!hasUserAgent)
+ {
+ // set default xbmc user-agent.
+ av_dict_set(&options, "user_agent", CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_userAgent.c_str(), 0);
+ }
+
+ if (!headers.empty())
+ av_dict_set(&options, "headers", headers.c_str(), 0);
+
+ if (!hasCookies)
+ {
+ std::string cookies;
+ if (XFILE::CCurlFile::GetCookies(url, cookies))
+ av_dict_set(&options, "cookies", cookies.c_str(), 0);
+ }
+ }
+
+ if (input)
+ {
+ const std::string host = input->GetProxyHost();
+ if (!host.empty() && input->GetProxyType() == "http")
+ {
+ std::ostringstream urlStream;
+
+ const uint16_t port = input->GetProxyPort();
+ const std::string user = input->GetProxyUser();
+ const std::string password = input->GetProxyPassword();
+
+ urlStream << "http://";
+
+ if (!user.empty()) {
+ urlStream << user;
+ if (!password.empty())
+ urlStream << ":" << password;
+ urlStream << "@";
+ }
+
+ urlStream << host << ':' << port;
+
+ av_dict_set(&options, "http_proxy", urlStream.str().c_str(), 0);
+ }
+
+ // rtsp options
+ if (url.IsProtocol("rtsp"))
+ {
+ CVariant transportProp{m_pInput->GetProperty("rtsp_transport")};
+ if (!transportProp.isNull() &&
+ (transportProp == "tcp" || transportProp == "udp" || transportProp == "udp_multicast"))
+ {
+ CLog::LogF(LOGDEBUG, "GetFFMpegOptionsFromInput() Forcing rtsp transport protocol to '{}'",
+ transportProp.asString());
+ av_dict_set(&options, "rtsp_transport", transportProp.asString().c_str(), 0);
+ }
+ }
+
+ // rtmp options
+ if (url.IsProtocol("rtmp") || url.IsProtocol("rtmpt") ||
+ url.IsProtocol("rtmpe") || url.IsProtocol("rtmpte") ||
+ url.IsProtocol("rtmps"))
+ {
+ static const std::map<std::string,std::string> optionmap =
+ {{{"SWFPlayer", "rtmp_swfurl"},
+ {"swfplayer", "rtmp_swfurl"},
+ {"PageURL", "rtmp_pageurl"},
+ {"pageurl", "rtmp_pageurl"},
+ {"PlayPath", "rtmp_playpath"},
+ {"playpath", "rtmp_playpath"},
+ {"TcUrl", "rtmp_tcurl"},
+ {"tcurl", "rtmp_tcurl"},
+ {"IsLive", "rtmp_live"},
+ {"islive", "rtmp_live"},
+ {"swfurl", "rtmp_swfurl"},
+ {"swfvfy", "rtmp_swfverify"},
+ }};
+
+ for (const auto& it : optionmap)
+ {
+ if (input->GetItem().HasProperty(it.first))
+ {
+ av_dict_set(&options, it.second.c_str(),
+ input->GetItem().GetProperty(it.first).asString().c_str(),0);
+ }
+ }
+
+ CURL tmpUrl = url;
+ std::vector<std::string> opts = StringUtils::Split(tmpUrl.Get(), " ");
+ if (opts.size() > 1) // inline rtmp options
+ {
+ std::string swfurl;
+ bool swfvfy=false;
+ for (size_t i = 1; i < opts.size(); ++i)
+ {
+ std::vector<std::string> value = StringUtils::Split(opts[i], "=", 2);
+ StringUtils::ToLower(value[0]);
+ auto it = optionmap.find(value[0]);
+ if (it != optionmap.end())
+ {
+ if (value[0] == "swfurl" || value[0] == "SWFPlayer")
+ swfurl = value[1];
+ if (value[0] == "swfvfy" && (value[1] == "true" || value[1] == "1"))
+ swfvfy = true;
+ else
+ av_dict_set(&options, it->second.c_str(), value[1].c_str(), 0);
+ }
+ if (swfvfy)
+ av_dict_set(&options, "rtmp_swfverify", swfurl.c_str(), 0);
+ }
+ tmpUrl = CURL(opts.front());
+ }
+ }
+ }
+
+ return options;
+}
+
+double CDVDDemuxFFmpeg::ConvertTimestamp(int64_t pts, int den, int num)
+{
+ if (pts == (int64_t)AV_NOPTS_VALUE)
+ return DVD_NOPTS_VALUE;
+
+ // do calculations in floats as they can easily overflow otherwise
+ // we don't care for having a completely exact timestamp anyway
+ double timestamp = (double)pts * num / den;
+ double starttime = 0.0;
+
+ const std::shared_ptr<CDVDInputStream::IMenus> menuInterface =
+ std::dynamic_pointer_cast<CDVDInputStream::IMenus>(m_pInput);
+ if ((!menuInterface || menuInterface->GetSupportedMenuType() != MenuType::NATIVE) &&
+ m_pFormatContext->start_time != static_cast<int64_t>(AV_NOPTS_VALUE))
+ {
+ starttime = static_cast<double>(m_pFormatContext->start_time) / AV_TIME_BASE;
+ }
+
+ if (m_checkTransportStream)
+ starttime = m_startTime;
+
+ if (!m_bSup)
+ {
+ if (timestamp > starttime || m_checkTransportStream)
+ timestamp -= starttime;
+ // allow for largest possible difference in pts and dts for a single packet
+ else if (timestamp + 0.5 > starttime)
+ timestamp = 0;
+ }
+
+ return timestamp * DVD_TIME_BASE;
+}
+
+DemuxPacket* CDVDDemuxFFmpeg::Read()
+{
+ DemuxPacket* pPacket = NULL;
+ // on some cases where the received packet is invalid we will need to return an empty packet (0 length) otherwise the main loop (in CVideoPlayer)
+ // would consider this the end of stream and stop.
+ bool bReturnEmpty = false;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection); // open lock scope
+ if (m_pFormatContext)
+ {
+ // assume we are not eof
+ if (m_pFormatContext->pb)
+ m_pFormatContext->pb->eof_reached = 0;
+
+ // check for saved packet after a program change
+ if (m_pkt.result < 0)
+ {
+ // keep track if ffmpeg doesn't always set these
+ m_pkt.pkt.size = 0;
+ m_pkt.pkt.data = NULL;
+
+ // timeout reads after 100ms
+ m_timeout.Set(20s);
+ m_pkt.result = av_read_frame(m_pFormatContext, &m_pkt.pkt);
+ m_timeout.SetInfinite();
+ }
+
+ if (m_pkt.result == AVERROR(EINTR) || m_pkt.result == AVERROR(EAGAIN))
+ {
+ // timeout, probably no real error, return empty packet
+ bReturnEmpty = true;
+ }
+ else if (m_pkt.result == AVERROR_EOF)
+ {
+ }
+ else if (m_pkt.result < 0)
+ {
+ Flush();
+ }
+ // check size and stream index for being in a valid range
+ else if (m_pkt.pkt.size < 0 || m_pkt.pkt.stream_index < 0 ||
+ m_pkt.pkt.stream_index >= (int)m_pFormatContext->nb_streams)
+ {
+ // XXX, in some cases ffmpeg returns a negative packet size
+ if (m_pFormatContext->pb && !m_pFormatContext->pb->eof_reached)
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxFFmpeg::Read() no valid packet");
+ bReturnEmpty = true;
+ Flush();
+ }
+ else
+ CLog::Log(LOGERROR, "CDVDDemuxFFmpeg::Read() returned invalid packet and eof reached");
+
+ m_pkt.result = -1;
+ av_packet_unref(&m_pkt.pkt);
+ }
+ else
+ {
+ ParsePacket(&m_pkt.pkt);
+
+ if (IsProgramChange())
+ {
+ CLog::Log(LOGINFO, "CDVDDemuxFFmpeg::Read() stream change");
+ av_dump_format(m_pFormatContext, 0, CURL::GetRedacted(m_pInput->GetFileName()).c_str(),
+ 0);
+
+ // update streams
+ CreateStreams(m_program);
+
+ pPacket = CDVDDemuxUtils::AllocateDemuxPacket(0);
+ pPacket->iStreamId = DMX_SPECIALID_STREAMCHANGE;
+ pPacket->demuxerId = m_demuxerId;
+
+ return pPacket;
+ }
+
+ AVStream* stream = m_pFormatContext->streams[m_pkt.pkt.stream_index];
+
+ if (IsTransportStreamReady())
+ {
+ if (m_program != UINT_MAX)
+ {
+ /* check so packet belongs to selected program */
+ for (unsigned int i = 0; i < m_pFormatContext->programs[m_program]->nb_stream_indexes;
+ i++)
+ {
+ if (m_pkt.pkt.stream_index ==
+ (int)m_pFormatContext->programs[m_program]->stream_index[i])
+ {
+ pPacket = CDVDDemuxUtils::AllocateDemuxPacket(m_pkt.pkt.size);
+ break;
+ }
+ }
+
+ if (!pPacket)
+ bReturnEmpty = true;
+ }
+ else
+ pPacket = CDVDDemuxUtils::AllocateDemuxPacket(m_pkt.pkt.size);
+ }
+ else
+ bReturnEmpty = true;
+
+ if (pPacket)
+ {
+ if (m_bAVI && stream->codecpar && stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
+ {
+ // AVI's always have borked pts, specially if m_pFormatContext->flags includes
+ // AVFMT_FLAG_GENPTS so always use dts
+ m_pkt.pkt.pts = AV_NOPTS_VALUE;
+ }
+
+ // copy contents into our own packet
+ pPacket->iSize = m_pkt.pkt.size;
+
+ // maybe we can avoid a memcpy here by detecting where pkt.destruct is pointing too?
+ if (m_pkt.pkt.data)
+ memcpy(pPacket->pData, m_pkt.pkt.data, pPacket->iSize);
+
+ pPacket->pts =
+ ConvertTimestamp(m_pkt.pkt.pts, stream->time_base.den, stream->time_base.num);
+ pPacket->dts =
+ ConvertTimestamp(m_pkt.pkt.dts, stream->time_base.den, stream->time_base.num);
+ pPacket->duration = DVD_SEC_TO_TIME((double)m_pkt.pkt.duration * stream->time_base.num /
+ stream->time_base.den);
+
+ CDVDDemuxUtils::StoreSideData(pPacket, &m_pkt.pkt);
+
+ CDVDInputStream::IDisplayTime* inputStream = m_pInput->GetIDisplayTime();
+ if (inputStream)
+ {
+ int dispTime = inputStream->GetTime();
+ if (m_displayTime != dispTime)
+ {
+ m_displayTime = dispTime;
+ if (pPacket->dts != DVD_NOPTS_VALUE)
+ {
+ m_dtsAtDisplayTime = pPacket->dts;
+ }
+ }
+ if (m_dtsAtDisplayTime != DVD_NOPTS_VALUE && pPacket->dts != DVD_NOPTS_VALUE)
+ {
+ pPacket->dispTime = m_displayTime;
+ pPacket->dispTime += DVD_TIME_TO_MSEC(pPacket->dts - m_dtsAtDisplayTime);
+ }
+ }
+
+ // used to guess streamlength
+ if (pPacket->dts != DVD_NOPTS_VALUE &&
+ (pPacket->dts > m_currentPts || m_currentPts == DVD_NOPTS_VALUE))
+ m_currentPts = pPacket->dts;
+
+ // store internal id until we know the continuous id presented to player
+ // the stream might not have been created yet
+ pPacket->iStreamId = m_pkt.pkt.stream_index;
+ }
+ m_pkt.result = -1;
+ av_packet_unref(&m_pkt.pkt);
+ }
+ }
+ } // end of lock scope
+ if (bReturnEmpty && !pPacket)
+ pPacket = CDVDDemuxUtils::AllocateDemuxPacket(0);
+
+ if (!pPacket)
+ return nullptr;
+
+ // check streams, can we make this a bit more simple?
+ if (pPacket->iStreamId >= 0)
+ {
+ CDemuxStream* stream = GetStream(pPacket->iStreamId);
+ if (!stream ||
+ stream->pPrivate != m_pFormatContext->streams[pPacket->iStreamId] ||
+ stream->codec != m_pFormatContext->streams[pPacket->iStreamId]->codecpar->codec_id)
+ {
+ // content has changed, or stream did not yet exist
+ stream = AddStream(pPacket->iStreamId);
+ }
+ // we already check for a valid m_streams[pPacket->iStreamId] above
+ else if (stream->type == STREAM_AUDIO)
+ {
+ CDemuxStreamAudioFFmpeg* audiostream = dynamic_cast<CDemuxStreamAudioFFmpeg*>(stream);
+ if (audiostream && (audiostream->iChannels != m_pFormatContext->streams[pPacket->iStreamId]->codecpar->channels ||
+ audiostream->iSampleRate != m_pFormatContext->streams[pPacket->iStreamId]->codecpar->sample_rate))
+ {
+ // content has changed
+ stream = AddStream(pPacket->iStreamId);
+ }
+ }
+ else if (stream->type == STREAM_VIDEO)
+ {
+ if (static_cast<CDemuxStreamVideo*>(stream)->iWidth != m_pFormatContext->streams[pPacket->iStreamId]->codecpar->width ||
+ static_cast<CDemuxStreamVideo*>(stream)->iHeight != m_pFormatContext->streams[pPacket->iStreamId]->codecpar->height)
+ {
+ // content has changed
+ stream = AddStream(pPacket->iStreamId);
+ }
+ if (stream && stream->codec == AV_CODEC_ID_H264)
+ pPacket->recoveryPoint = m_seekToKeyFrame;
+ m_seekToKeyFrame = false;
+ }
+ if (!stream)
+ {
+ CDVDDemuxUtils::FreeDemuxPacket(pPacket);
+ pPacket = CDVDDemuxUtils::AllocateDemuxPacket(0);
+ return pPacket;
+ }
+
+ pPacket->iStreamId = stream->uniqueId;
+ pPacket->demuxerId = m_demuxerId;
+ }
+ return pPacket;
+}
+
+bool CDVDDemuxFFmpeg::SeekTime(double time, bool backwards, double* startpts)
+{
+ bool hitEnd = false;
+
+ if (!m_pInput)
+ return false;
+
+ if (time < 0)
+ {
+ time = 0;
+ hitEnd = true;
+ }
+
+ m_pkt.result = -1;
+ av_packet_unref(&m_pkt.pkt);
+
+ CDVDInputStream::IPosTime* ist = m_pInput->GetIPosTime();
+ if (ist)
+ {
+ if (!ist->PosTime(static_cast<int>(time)))
+ return false;
+
+ if (startpts)
+ *startpts = DVD_NOPTS_VALUE;
+
+ Flush();
+
+ return true;
+ }
+
+ if (!m_pInput->Seek(0, SEEK_POSSIBLE) &&
+ !m_pInput->IsStreamType(DVDSTREAM_TYPE_FFMPEG))
+ {
+ CLog::Log(LOGDEBUG, "{} - input stream reports it is not seekable", __FUNCTION__);
+ return false;
+ }
+
+ int64_t seek_pts = (int64_t)time * (AV_TIME_BASE / 1000);
+ bool ismp3 = m_pFormatContext->iformat && (strcmp(m_pFormatContext->iformat->name, "mp3") == 0);
+
+ if (m_checkTransportStream)
+ {
+ XbmcThreads::EndTime<> timer(1000ms);
+
+ while (!IsTransportStreamReady())
+ {
+ DemuxPacket* pkt = Read();
+ if (pkt)
+ CDVDDemuxUtils::FreeDemuxPacket(pkt);
+ else
+ KODI::TIME::Sleep(10ms);
+ m_pkt.result = -1;
+ av_packet_unref(&m_pkt.pkt);
+
+ if (timer.IsTimePast())
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxFFmpeg::{} - Timed out waiting for video to be ready",
+ __FUNCTION__);
+ return false;
+ }
+ }
+
+ AVStream* st = m_pFormatContext->streams[m_seekStream];
+ seek_pts = av_rescale(static_cast<int64_t>(m_startTime + time / 1000), st->time_base.den,
+ st->time_base.num);
+ }
+ else if (m_pFormatContext->start_time != (int64_t)AV_NOPTS_VALUE && !ismp3 && !m_bSup)
+ seek_pts += m_pFormatContext->start_time;
+
+ int ret;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ ret = av_seek_frame(m_pFormatContext, m_seekStream, seek_pts, backwards ? AVSEEK_FLAG_BACKWARD : 0);
+
+ if (ret < 0)
+ {
+ int64_t starttime = m_pFormatContext->start_time;
+ if (m_checkTransportStream)
+ {
+ AVStream* st = m_pFormatContext->streams[m_seekStream];
+ starttime =
+ av_rescale(static_cast<int64_t>(m_startTime), st->time_base.num, st->time_base.den);
+ }
+
+ // demuxer can return failure, if seeking behind eof
+ if (m_pFormatContext->duration &&
+ seek_pts >= (m_pFormatContext->duration + starttime))
+ {
+ // force eof
+ // files of realtime streams may grow
+ if (!m_pInput->IsRealtime())
+ m_pInput->Close();
+ else
+ ret = 0;
+ }
+ else if (m_pInput->IsEOF())
+ ret = 0;
+ }
+
+ if (ret >= 0)
+ {
+ if (m_pFormatContext->iformat->read_seek)
+ m_seekToKeyFrame = true;
+
+ UpdateCurrentPTS();
+ }
+ }
+
+ if (m_currentPts == DVD_NOPTS_VALUE)
+ CLog::Log(LOGDEBUG, "{} - unknown position after seek", __FUNCTION__);
+ else
+ CLog::Log(LOGDEBUG, "{} - seek ended up on time {}", __FUNCTION__,
+ (int)(m_currentPts / DVD_TIME_BASE * 1000));
+
+ // in this case the start time is requested time
+ if (startpts)
+ *startpts = DVD_MSEC_TO_TIME(time);
+
+ if (ret >= 0)
+ {
+ if (!hitEnd)
+ return true;
+ else
+ return false;
+ }
+ else
+ return false;
+}
+
+bool CDVDDemuxFFmpeg::SeekByte(int64_t pos)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ int ret = av_seek_frame(m_pFormatContext, -1, pos, AVSEEK_FLAG_BYTE);
+
+ if (ret >= 0)
+ UpdateCurrentPTS();
+
+ m_pkt.result = -1;
+ av_packet_unref(&m_pkt.pkt);
+
+ return (ret >= 0);
+}
+
+void CDVDDemuxFFmpeg::UpdateCurrentPTS()
+{
+ m_currentPts = DVD_NOPTS_VALUE;
+
+ int idx = av_find_default_stream_index(m_pFormatContext);
+ if (idx >= 0)
+ {
+ AVStream* stream = m_pFormatContext->streams[idx];
+ if (stream && stream->cur_dts != (int64_t)AV_NOPTS_VALUE)
+ {
+ double ts = ConvertTimestamp(stream->cur_dts, stream->time_base.den, stream->time_base.num);
+ m_currentPts = ts;
+ }
+ }
+}
+
+int CDVDDemuxFFmpeg::GetStreamLength()
+{
+ if (!m_pFormatContext)
+ return 0;
+
+ if (m_pFormatContext->duration < 0 ||
+ m_pFormatContext->duration == AV_NOPTS_VALUE)
+ return 0;
+
+ return (int)(m_pFormatContext->duration / (AV_TIME_BASE / 1000));
+}
+
+/**
+ * @brief Finds stream based on unique id
+ */
+CDemuxStream* CDVDDemuxFFmpeg::GetStream(int iStreamId) const
+{
+ auto it = m_streams.find(iStreamId);
+ if (it != m_streams.end())
+ return it->second;
+
+ return nullptr;
+}
+
+std::vector<CDemuxStream*> CDVDDemuxFFmpeg::GetStreams() const
+{
+ std::vector<CDemuxStream*> streams;
+
+ streams.reserve(m_streams.size());
+ for (auto& iter : m_streams)
+ streams.push_back(iter.second);
+
+ return streams;
+}
+
+int CDVDDemuxFFmpeg::GetNrOfStreams() const
+{
+ return static_cast<int>(m_streams.size());
+}
+
+int CDVDDemuxFFmpeg::GetPrograms(std::vector<ProgramInfo>& programs)
+{
+ programs.clear();
+ if (!m_pFormatContext || m_pFormatContext->nb_programs <= 1)
+ return 0;
+
+ for (unsigned int i = 0; i < m_pFormatContext->nb_programs; i++)
+ {
+ std::ostringstream os;
+ ProgramInfo prog;
+ prog.id = i;
+ os << i;
+ prog.name = os.str();
+ if (i == m_program)
+ prog.playing = true;
+
+ if (!m_pFormatContext->programs[i]->metadata)
+ continue;
+
+ AVDictionaryEntry* tag = av_dict_get(m_pFormatContext->programs[i]->metadata, "", nullptr, AV_DICT_IGNORE_SUFFIX);
+ while (tag)
+ {
+ os << " - " << tag->key << ": " << tag->value;
+ tag = av_dict_get(m_pFormatContext->programs[i]->metadata, "", tag, AV_DICT_IGNORE_SUFFIX);
+ }
+ prog.name = os.str();
+ programs.push_back(prog);
+ }
+ return static_cast<int>(programs.size());
+}
+
+void CDVDDemuxFFmpeg::SetProgram(int progId)
+{
+ m_newProgram = progId;
+}
+
+double CDVDDemuxFFmpeg::SelectAspect(AVStream* st, bool& forced)
+{
+ // trust matroska container
+ if (m_bMatroska && st->sample_aspect_ratio.num != 0)
+ {
+ forced = true;
+ double dar = av_q2d(st->sample_aspect_ratio);
+ // for stereo modes, use codec aspect ratio
+ AVDictionaryEntry* entry = av_dict_get(st->metadata, "stereo_mode", NULL, 0);
+ if (entry)
+ {
+ if (strcmp(entry->value, "left_right") == 0 || strcmp(entry->value, "right_left") == 0)
+ dar /= 2;
+ else if (strcmp(entry->value, "top_bottom") == 0 || strcmp(entry->value, "bottom_top") == 0)
+ dar *= 2;
+ }
+ return dar;
+ }
+
+ /* if stream aspect is 1:1 or 0:0 use codec aspect */
+ if ((st->sample_aspect_ratio.den == 1 || st->sample_aspect_ratio.den == 0) &&
+ (st->sample_aspect_ratio.num == 1 || st->sample_aspect_ratio.num == 0) &&
+ st->codecpar->sample_aspect_ratio.num != 0)
+ {
+ forced = false;
+ return av_q2d(st->codecpar->sample_aspect_ratio);
+ }
+
+ if (st->sample_aspect_ratio.num != 0)
+ {
+ forced = true;
+ return av_q2d(st->sample_aspect_ratio);
+ }
+
+ forced = false;
+ return 0.0;
+}
+
+void CDVDDemuxFFmpeg::CreateStreams(unsigned int program)
+{
+ DisposeStreams();
+
+ // add the ffmpeg streams to our own stream map
+ if (m_pFormatContext->nb_programs)
+ {
+ // check if desired program is available
+ if (program < m_pFormatContext->nb_programs)
+ {
+ m_program = program;
+ m_streamsInProgram = m_pFormatContext->programs[program]->nb_stream_indexes;
+ m_pFormatContext->programs[program]->discard = AVDISCARD_NONE;
+ }
+ else
+ m_program = UINT_MAX;
+
+ // look for first non empty stream and discard nonselected programs
+ for (unsigned int i = 0; i < m_pFormatContext->nb_programs; i++)
+ {
+ if (m_program == UINT_MAX && m_pFormatContext->programs[i]->nb_stream_indexes > 0)
+ {
+ m_program = i;
+ }
+
+ if (i != m_program)
+ m_pFormatContext->programs[i]->discard = AVDISCARD_ALL;
+ }
+ if (m_program != UINT_MAX)
+ {
+ m_pFormatContext->programs[m_program]->discard = AVDISCARD_NONE;
+
+ // add streams from selected program
+ for (unsigned int i = 0; i < m_pFormatContext->programs[m_program]->nb_stream_indexes; i++)
+ {
+ int streamIdx = m_pFormatContext->programs[m_program]->stream_index[i];
+ m_pFormatContext->streams[streamIdx]->discard = AVDISCARD_NONE;
+ AddStream(streamIdx);
+ }
+
+ // discard all unneeded streams
+ for (unsigned int i = 0; i < m_pFormatContext->nb_streams; i++)
+ {
+ m_pFormatContext->streams[i]->discard = AVDISCARD_NONE;
+ if (GetStream(i) == nullptr)
+ m_pFormatContext->streams[i]->discard = AVDISCARD_ALL;
+ }
+ }
+ }
+ else
+ m_program = UINT_MAX;
+
+ // if there were no programs or they were all empty, add all streams
+ if (m_program == UINT_MAX)
+ {
+ for (unsigned int i = 0; i < m_pFormatContext->nb_streams; i++)
+ AddStream(i);
+ }
+}
+
+void CDVDDemuxFFmpeg::DisposeStreams()
+{
+ std::map<int, CDemuxStream*>::iterator it;
+ for(it = m_streams.begin(); it != m_streams.end(); ++it)
+ delete it->second;
+ m_streams.clear();
+ m_parsers.clear();
+}
+
+CDemuxStream* CDVDDemuxFFmpeg::AddStream(int streamIdx)
+{
+ AVStream* pStream = m_pFormatContext->streams[streamIdx];
+ if (pStream && pStream->discard != AVDISCARD_ALL)
+ {
+ // Video (mp4) from GoPro cameras can have a 'meta' track used for a file repair containing
+ // 'fdsc' data, this is also called the SOS track.
+ if (pStream->codecpar->codec_tag == MKTAG('f','d','s','c'))
+ {
+ CLog::Log(LOGDEBUG, "CDVDDemuxFFmpeg::AddStream - discarding fdsc stream");
+ pStream->discard = AVDISCARD_ALL;
+ return nullptr;
+ }
+
+ CDemuxStream* stream = nullptr;
+
+ switch (pStream->codecpar->codec_type)
+ {
+ case AVMEDIA_TYPE_AUDIO:
+ {
+ CDemuxStreamAudioFFmpeg* st = new CDemuxStreamAudioFFmpeg(pStream);
+ stream = st;
+ st->iChannels = pStream->codecpar->channels;
+ st->iSampleRate = pStream->codecpar->sample_rate;
+ st->iBlockAlign = pStream->codecpar->block_align;
+ st->iBitRate = static_cast<int>(pStream->codecpar->bit_rate);
+ st->iBitsPerSample = pStream->codecpar->bits_per_raw_sample;
+ st->iChannelLayout = pStream->codecpar->channel_layout;
+ char buf[32] = {};
+ av_get_channel_layout_string(buf, 31, st->iChannels, st->iChannelLayout);
+ st->m_channelLayoutName = buf;
+ if (st->iBitsPerSample == 0)
+ st->iBitsPerSample = pStream->codecpar->bits_per_coded_sample;
+
+ if (av_dict_get(pStream->metadata, "title", NULL, 0))
+ st->m_description = av_dict_get(pStream->metadata, "title", NULL, 0)->value;
+
+ break;
+ }
+ case AVMEDIA_TYPE_VIDEO:
+ {
+ CDemuxStreamVideoFFmpeg* st = new CDemuxStreamVideoFFmpeg(pStream);
+ stream = st;
+ if (strcmp(m_pFormatContext->iformat->name, "flv") == 0)
+ st->bVFR = true;
+ else
+ st->bVFR = false;
+
+ // never trust pts in avi files with h264.
+ if (m_bAVI && pStream->codecpar->codec_id == AV_CODEC_ID_H264)
+ st->bPTSInvalid = true;
+
+ AVRational r_frame_rate = pStream->r_frame_rate;
+
+ //average fps is more accurate for mkv files
+ if (m_bMatroska && pStream->avg_frame_rate.den && pStream->avg_frame_rate.num)
+ {
+ st->iFpsRate = pStream->avg_frame_rate.num;
+ st->iFpsScale = pStream->avg_frame_rate.den;
+ }
+ else if (r_frame_rate.den && r_frame_rate.num)
+ {
+ st->iFpsRate = r_frame_rate.num;
+ st->iFpsScale = r_frame_rate.den;
+ }
+ else
+ {
+ st->iFpsRate = 0;
+ st->iFpsScale = 0;
+ }
+
+ if (pStream->codec_info_nb_frames > 0 &&
+ pStream->codec_info_nb_frames <= 2 &&
+ m_pInput->IsStreamType(DVDSTREAM_TYPE_DVD))
+ {
+ CLog::Log(LOGDEBUG, "{} - fps may be unreliable since ffmpeg decoded only {} frame(s)",
+ __FUNCTION__, pStream->codec_info_nb_frames);
+ st->iFpsRate = 0;
+ st->iFpsScale = 0;
+ }
+
+ st->iWidth = pStream->codecpar->width;
+ st->iHeight = pStream->codecpar->height;
+ st->fAspect = SelectAspect(pStream, st->bForcedAspect);
+ if (pStream->codecpar->height)
+ st->fAspect *= (double)pStream->codecpar->width / pStream->codecpar->height;
+ st->iOrientation = 0;
+ st->iBitsPerPixel = pStream->codecpar->bits_per_coded_sample;
+ st->iBitRate = static_cast<int>(pStream->codecpar->bit_rate);
+ st->bitDepth = 8;
+ const AVPixFmtDescriptor* desc =
+ av_pix_fmt_desc_get(static_cast<AVPixelFormat>(pStream->codecpar->format));
+ if (desc != nullptr)
+ st->bitDepth = desc->comp[0].depth;
+
+ st->colorPrimaries = pStream->codecpar->color_primaries;
+ st->colorSpace = pStream->codecpar->color_space;
+ st->colorTransferCharacteristic = pStream->codecpar->color_trc;
+ st->colorRange = pStream->codecpar->color_range;
+ st->hdr_type = DetermineHdrType(pStream);
+
+ int size = 0;
+ uint8_t* side_data = nullptr;
+
+ side_data = av_stream_get_side_data(pStream, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, &size);
+ if (side_data && size)
+ {
+ st->masteringMetaData = std::make_shared<AVMasteringDisplayMetadata>(
+ *reinterpret_cast<AVMasteringDisplayMetadata*>(side_data));
+ }
+
+ side_data = av_stream_get_side_data(pStream, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, &size);
+ if (side_data && size)
+ {
+ st->contentLightMetaData = std::make_shared<AVContentLightMetadata>(
+ *reinterpret_cast<AVContentLightMetadata*>(side_data));
+ }
+
+ AVDictionaryEntry* rtag = av_dict_get(pStream->metadata, "rotate", NULL, 0);
+ if (rtag)
+ st->iOrientation = atoi(rtag->value);
+
+ // detect stereoscopic mode
+ std::string stereoMode = GetStereoModeFromMetadata(pStream->metadata);
+ // check for metadata in file if detection in stream failed
+ if (stereoMode.empty())
+ stereoMode = GetStereoModeFromMetadata(m_pFormatContext->metadata);
+ if (!stereoMode.empty())
+ st->stereo_mode = stereoMode;
+
+
+ if (m_pInput->IsStreamType(DVDSTREAM_TYPE_DVD))
+ {
+ if (pStream->codecpar->codec_id == AV_CODEC_ID_PROBE)
+ {
+ // fix MPEG-1/MPEG-2 video stream probe returning AV_CODEC_ID_PROBE for still frames.
+ // ffmpeg issue 1871, regression from ffmpeg r22831.
+ if ((pStream->id & 0xF0) == 0xE0)
+ {
+ pStream->codecpar->codec_id = AV_CODEC_ID_MPEG2VIDEO;
+ pStream->codecpar->codec_tag = MKTAG('M','P','2','V');
+ CLog::Log(LOGERROR, "{} - AV_CODEC_ID_PROBE detected, forcing AV_CODEC_ID_MPEG2VIDEO",
+ __FUNCTION__);
+ }
+ }
+ }
+ if (av_dict_get(pStream->metadata, "title", NULL, 0))
+ st->m_description = av_dict_get(pStream->metadata, "title", NULL, 0)->value;
+
+ break;
+ }
+ case AVMEDIA_TYPE_DATA:
+ {
+ stream = new CDemuxStream();
+ stream->type = STREAM_DATA;
+ break;
+ }
+ case AVMEDIA_TYPE_SUBTITLE:
+ {
+ if (pStream->codecpar->codec_id == AV_CODEC_ID_DVB_TELETEXT && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_TELETEXTENABLED))
+ {
+ CDemuxStreamTeletext* st = new CDemuxStreamTeletext();
+ stream = st;
+ stream->type = STREAM_TELETEXT;
+ break;
+ }
+ else
+ {
+ CDemuxStreamSubtitleFFmpeg* st = new CDemuxStreamSubtitleFFmpeg(pStream);
+ stream = st;
+
+ if (av_dict_get(pStream->metadata, "title", NULL, 0))
+ st->m_description = av_dict_get(pStream->metadata, "title", NULL, 0)->value;
+
+ break;
+ }
+ }
+ case AVMEDIA_TYPE_ATTACHMENT:
+ {
+ // MKV attachments. Only bothering with fonts for now.
+ AVDictionaryEntry* attachmentMimetype =
+ av_dict_get(pStream->metadata, "mimetype", nullptr, 0);
+
+ if (pStream->codecpar->codec_id == AV_CODEC_ID_TTF ||
+ pStream->codecpar->codec_id == AV_CODEC_ID_OTF || AttachmentIsFont(attachmentMimetype))
+ {
+ // Temporary fonts are extracted to the temporary fonts path
+ //! @todo: temporary font file management should be completely
+ //! removed, by sending font data to the subtitle renderer and
+ //! using libass ass_add_font to add the fonts directly in memory.
+ std::string filePath{UTILS::FONT::FONTPATH::TEMP};
+ XFILE::CDirectory::Create(filePath);
+
+ AVDictionaryEntry* nameTag = av_dict_get(pStream->metadata, "filename", NULL, 0);
+ if (nameTag)
+ {
+ filePath += CUtil::MakeLegalFileName(nameTag->value, LEGAL_WIN32_COMPAT);
+ XFILE::CFile file;
+ if (pStream->codecpar->extradata && file.OpenForWrite(filePath))
+ {
+ if (file.Write(pStream->codecpar->extradata, pStream->codecpar->extradata_size) !=
+ pStream->codecpar->extradata_size)
+ {
+ file.Close();
+ XFILE::CFile::Delete(filePath);
+ CLog::LogF(LOGDEBUG, "Error saving font file \"{}\"", filePath);
+ }
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Attached font has no name");
+ }
+ }
+ stream = new CDemuxStream();
+ stream->type = STREAM_NONE;
+ break;
+ }
+ default:
+ {
+ // if analyzing streams is skipped, unknown streams may become valid later
+ if (m_streaminfo && IsTransportStreamReady())
+ {
+ CLog::Log(LOGDEBUG, "CDVDDemuxFFmpeg::AddStream - discarding unknown stream with id: {}",
+ pStream->index);
+ pStream->discard = AVDISCARD_ALL;
+ return nullptr;
+ }
+ stream = new CDemuxStream();
+ stream->type = STREAM_NONE;
+ }
+ }
+
+ // generic stuff
+ if (pStream->duration != (int64_t)AV_NOPTS_VALUE)
+ stream->iDuration = (int)((pStream->duration / AV_TIME_BASE) & 0xFFFFFFFF);
+
+ stream->codec = pStream->codecpar->codec_id;
+ stream->codec_fourcc = pStream->codecpar->codec_tag;
+ stream->profile = pStream->codecpar->profile;
+ stream->level = pStream->codecpar->level;
+
+ stream->source = STREAM_SOURCE_DEMUX;
+ stream->pPrivate = pStream;
+ stream->flags = (StreamFlags)pStream->disposition;
+
+ AVDictionaryEntry* langTag = av_dict_get(pStream->metadata, "language", NULL, 0);
+ if (!langTag)
+ {
+ // only for avi audio streams
+ if ((strcmp(m_pFormatContext->iformat->name, "avi") == 0) && (pStream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO))
+ {
+ // only defined for streams 1 to 9
+ if ((streamIdx > 0) && (streamIdx < 10))
+ {
+ // search for language information in RIFF-Header ("IAS1": first language - "IAS9": ninth language)
+ char riff_tag_string[5] = {'I', 'A', 'S', (char)(streamIdx + '0'), '\0'};
+ langTag = av_dict_get(m_pFormatContext->metadata, riff_tag_string, NULL, 0);
+ if (!langTag && (streamIdx == 1))
+ {
+ // search for language information in RIFF-Header ("ILNG": language)
+ langTag = av_dict_get(m_pFormatContext->metadata, "language", NULL, 0);
+ }
+ }
+ }
+ }
+ if (langTag)
+ {
+ stream->language = std::string(langTag->value, 3);
+ //! @FIXME: Matroska v4 support BCP-47 language code with LanguageIETF element
+ //! that have the priority over the Language element, but this is not currently
+ //! implemented in to ffmpeg library. Since ffmpeg read only the Language element
+ //! all tracks will be identified with same language (of Language element).
+ //! As workaround to allow set the right language code we provide the possibility
+ //! to set the language code in the title field, this allow to kodi to recognize
+ //! the right language and select the right track to be played at playback starts.
+ AVDictionaryEntry* title = av_dict_get(pStream->metadata, "title", NULL, 0);
+ if (title && title->value)
+ {
+ const std::string langCode = g_LangCodeExpander.FindLanguageCodeWithSubtag(title->value);
+ if (!langCode.empty())
+ stream->language = langCode;
+ }
+ }
+
+ if (stream->type != STREAM_NONE && pStream->codecpar->extradata && pStream->codecpar->extradata_size > 0)
+ {
+ stream->ExtraSize = pStream->codecpar->extradata_size;
+ stream->ExtraData = std::make_unique<uint8_t[]>(pStream->codecpar->extradata_size);
+ memcpy(stream->ExtraData.get(), pStream->codecpar->extradata,
+ pStream->codecpar->extradata_size);
+ }
+
+#ifdef HAVE_LIBBLURAY
+ if (m_pInput->IsStreamType(DVDSTREAM_TYPE_BLURAY))
+ {
+ // UHD BD have a secondary video stream called by Dolby as enhancement layer.
+ // This is not used by streaming services and devices (ATV, Nvidia Shield, XONE).
+ if (pStream->id == 0x1015)
+ {
+ CLog::Log(LOGDEBUG, "CDVDDemuxFFmpeg::AddStream - discarding Dolby Vision stream");
+ pStream->discard = AVDISCARD_ALL;
+ delete stream;
+ return nullptr;
+ }
+ stream->dvdNavId = pStream->id;
+
+ auto it = std::find_if(m_streams.begin(), m_streams.end(),
+ [&stream](const std::pair<int, CDemuxStream*>& v)
+ {return (v.second->dvdNavId == stream->dvdNavId) && (v.second->type == stream->type); });
+
+ if (it != m_streams.end())
+ {
+ if (stream->codec == AV_CODEC_ID_AC3 && it->second->codec == AV_CODEC_ID_TRUEHD)
+ CLog::Log(LOGDEBUG, "CDVDDemuxFFmpeg::AddStream - discarding duplicated bluray stream (truehd ac3 core)");
+ else
+ CLog::Log(LOGDEBUG, "CDVDDemuxFFmpeg::AddStream - discarding duplicate bluray stream {}",
+ stream->codecName);
+
+ pStream->discard = AVDISCARD_ALL;
+ delete stream;
+ return nullptr;
+ }
+ std::static_pointer_cast<CDVDInputStreamBluray>(m_pInput)->GetStreamInfo(pStream->id, stream->language);
+ }
+#endif
+ if (m_pInput->IsStreamType(DVDSTREAM_TYPE_DVD))
+ {
+ // this stuff is really only valid for dvd's.
+ // this is so that the physicalid matches the
+ // id's reported from libdvdnav
+ switch (stream->codec)
+ {
+ case AV_CODEC_ID_AC3:
+ stream->dvdNavId = pStream->id - 128;
+ break;
+ case AV_CODEC_ID_DTS:
+ stream->dvdNavId = pStream->id - 136;
+ break;
+ case AV_CODEC_ID_MP2:
+ stream->dvdNavId = pStream->id - 448;
+ break;
+ case AV_CODEC_ID_PCM_S16BE:
+ stream->dvdNavId = pStream->id - 160;
+ break;
+ case AV_CODEC_ID_DVD_SUBTITLE:
+ stream->dvdNavId = pStream->id - 0x20;
+ break;
+ default:
+ stream->dvdNavId = pStream->id & 0x1f;
+ break;
+ }
+ }
+
+ stream->uniqueId = pStream->index;
+ stream->demuxerId = m_demuxerId;
+
+ AddStream(stream->uniqueId, stream);
+ return stream;
+ }
+ else
+ return nullptr;
+}
+
+/**
+ * @brief Adds or updates a demux stream based in ffmpeg id
+ */
+void CDVDDemuxFFmpeg::AddStream(int streamIdx, CDemuxStream* stream)
+{
+ std::pair<std::map<int, CDemuxStream*>::iterator, bool> res;
+
+ res = m_streams.insert(std::make_pair(streamIdx, stream));
+ if (res.second)
+ {
+ /* was new stream */
+ stream->uniqueId = streamIdx;
+ }
+ else
+ {
+ delete res.first->second;
+ res.first->second = stream;
+ }
+ CLog::Log(LOGDEBUG, "CDVDDemuxFFmpeg::AddStream ID: {}", streamIdx);
+}
+
+
+std::string CDVDDemuxFFmpeg::GetFileName()
+{
+ if (m_pInput)
+ return m_pInput->GetFileName();
+ else
+ return "";
+}
+
+int CDVDDemuxFFmpeg::GetChapterCount()
+{
+ std::shared_ptr<CDVDInputStream::IChapter> ich = std::dynamic_pointer_cast<CDVDInputStream::IChapter>(m_pInput);
+ if (ich)
+ return ich->GetChapterCount();
+
+ if (m_pFormatContext == NULL)
+ return 0;
+
+ return m_pFormatContext->nb_chapters;
+}
+
+int CDVDDemuxFFmpeg::GetChapter()
+{
+ std::shared_ptr<CDVDInputStream::IChapter> ich = std::dynamic_pointer_cast<CDVDInputStream::IChapter>(m_pInput);
+ if (ich)
+ return ich->GetChapter();
+
+ if (m_pFormatContext == NULL
+ || m_currentPts == DVD_NOPTS_VALUE)
+ return 0;
+
+ for(unsigned i = 0; i < m_pFormatContext->nb_chapters; i++)
+ {
+ AVChapter* chapter = m_pFormatContext->chapters[i];
+ if (m_currentPts >= ConvertTimestamp(chapter->start, chapter->time_base.den, chapter->time_base.num)
+ && m_currentPts < ConvertTimestamp(chapter->end, chapter->time_base.den, chapter->time_base.num))
+ return i + 1;
+ }
+
+ return 0;
+}
+
+void CDVDDemuxFFmpeg::GetChapterName(std::string& strChapterName, int chapterIdx)
+{
+ if (chapterIdx <= 0 || chapterIdx > GetChapterCount())
+ chapterIdx = GetChapter();
+ std::shared_ptr<CDVDInputStream::IChapter> ich = std::dynamic_pointer_cast<CDVDInputStream::IChapter>(m_pInput);
+ if (ich)
+ ich->GetChapterName(strChapterName, chapterIdx);
+ else
+ {
+ if (chapterIdx <= 0)
+ return;
+
+ AVDictionaryEntry* titleTag = av_dict_get(m_pFormatContext->chapters[chapterIdx - 1]->metadata,
+ "title", NULL, 0);
+ if (titleTag)
+ strChapterName = titleTag->value;
+ }
+}
+
+int64_t CDVDDemuxFFmpeg::GetChapterPos(int chapterIdx)
+{
+ if (chapterIdx <= 0 || chapterIdx > GetChapterCount())
+ chapterIdx = GetChapter();
+ if (chapterIdx <= 0)
+ return 0;
+
+ std::shared_ptr<CDVDInputStream::IChapter> ich = std::dynamic_pointer_cast<CDVDInputStream::IChapter>(m_pInput);
+ if (ich)
+ return ich->GetChapterPos(chapterIdx);
+
+ return static_cast<int64_t>(m_pFormatContext->chapters[chapterIdx - 1]->start * av_q2d(m_pFormatContext->chapters[chapterIdx - 1]->time_base));
+}
+
+bool CDVDDemuxFFmpeg::SeekChapter(int chapter, double* startpts)
+{
+ if (chapter < 1)
+ chapter = 1;
+
+ std::shared_ptr<CDVDInputStream::IChapter> ich = std::dynamic_pointer_cast<CDVDInputStream::IChapter>(m_pInput);
+ if (ich)
+ {
+ CLog::Log(LOGDEBUG, "{} - chapter seeking using input stream", __FUNCTION__);
+ if (!ich->SeekChapter(chapter))
+ return false;
+
+ if (startpts)
+ {
+ *startpts = DVD_SEC_TO_TIME(static_cast<double>(ich->GetChapterPos(chapter)));
+ }
+
+ Flush();
+ return true;
+ }
+
+ if (m_pFormatContext == NULL)
+ return false;
+
+ if (chapter < 1 || chapter > (int)m_pFormatContext->nb_chapters)
+ return false;
+
+ AVChapter* ch = m_pFormatContext->chapters[chapter - 1];
+ double dts = ConvertTimestamp(ch->start, ch->time_base.den, ch->time_base.num);
+ return SeekTime(DVD_TIME_TO_MSEC(dts), true, startpts);
+}
+
+std::string CDVDDemuxFFmpeg::GetStreamCodecName(int iStreamId)
+{
+ CDemuxStream* stream = GetStream(iStreamId);
+ std::string strName;
+ if (stream)
+ {
+ /* use profile to determine the DTS type */
+ if (stream->codec == AV_CODEC_ID_DTS)
+ {
+ if (stream->profile == FF_PROFILE_DTS_HD_MA)
+ strName = "dtshd_ma";
+ else if (stream->profile == FF_PROFILE_DTS_HD_HRA)
+ strName = "dtshd_hra";
+ else
+ strName = "dca";
+
+ return strName;
+ }
+
+ AVCodec* codec = avcodec_find_decoder(stream->codec);
+ if (codec)
+ strName = avcodec_get_name(codec->id);
+ }
+ return strName;
+}
+
+bool CDVDDemuxFFmpeg::IsProgramChange()
+{
+ if (m_program == UINT_MAX)
+ return false;
+
+ if (m_program == 0 && !m_pFormatContext->nb_programs)
+ return false;
+
+ if (m_initialProgramNumber != UINT_MAX)
+ {
+ for (unsigned int i = 0; i < m_pFormatContext->nb_programs; ++i)
+ {
+ if (m_pFormatContext->programs[i]->program_num == static_cast<int>(m_initialProgramNumber))
+ {
+ m_newProgram = i;
+ m_initialProgramNumber = UINT_MAX;
+ break;
+ }
+ }
+ if (m_initialProgramNumber != UINT_MAX)
+ return false;
+ }
+
+ if (m_program != m_newProgram)
+ {
+ m_program = m_newProgram;
+ return true;
+ }
+
+ if (m_pFormatContext->programs[m_program]->nb_stream_indexes != m_streamsInProgram)
+ return true;
+
+ if (m_program >= m_pFormatContext->nb_programs)
+ return true;
+
+ for (unsigned int i = 0; i < m_pFormatContext->programs[m_program]->nb_stream_indexes; i++)
+ {
+ int idx = m_pFormatContext->programs[m_program]->stream_index[i];
+ if (m_pFormatContext->streams[idx]->discard >= AVDISCARD_ALL)
+ continue;
+ CDemuxStream* stream = GetStream(idx);
+ if (!stream)
+ return true;
+ if (m_pFormatContext->streams[idx]->codecpar->codec_id != stream->codec)
+ return true;
+ if (m_pFormatContext->streams[idx]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
+ {
+ CDemuxStreamAudioFFmpeg* audiostream = dynamic_cast<CDemuxStreamAudioFFmpeg*>(stream);
+ if (audiostream &&
+ m_pFormatContext->streams[idx]->codecpar->channels != audiostream->iChannels)
+ {
+ return true;
+ }
+ }
+ if (m_pFormatContext->streams[idx]->codecpar->extradata_size != static_cast<int>(stream->ExtraSize))
+ return true;
+ }
+ return false;
+}
+
+unsigned int CDVDDemuxFFmpeg::HLSSelectProgram()
+{
+ unsigned int prog = UINT_MAX;
+
+ int bandwidth = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_NETWORK_BANDWIDTH) * 1000;
+ if (bandwidth <= 0)
+ bandwidth = INT_MAX;
+
+ int selectedBitrate = 0;
+ int selectedRes = 0;
+ for (unsigned int i = 0; i < m_pFormatContext->nb_programs; ++i)
+ {
+ int strBitrate = 0;
+ AVDictionaryEntry* tag = av_dict_get(m_pFormatContext->programs[i]->metadata, "variant_bitrate", NULL, 0);
+ if (tag)
+ strBitrate = atoi(tag->value);
+ else
+ continue;
+
+ int strRes = 0;
+ for (unsigned int j = 0; j < m_pFormatContext->programs[i]->nb_stream_indexes; j++)
+ {
+ int idx = m_pFormatContext->programs[i]->stream_index[j];
+ AVStream* pStream = m_pFormatContext->streams[idx];
+ if (pStream && pStream->codecpar &&
+ pStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
+ {
+ strRes = pStream->codecpar->width * pStream->codecpar->height;
+ }
+ }
+
+ if ((strRes && strRes < selectedRes) && selectedBitrate < bandwidth)
+ continue;
+
+ bool want = false;
+
+ if (strBitrate <= bandwidth)
+ {
+ if (strBitrate > selectedBitrate || strRes > selectedRes)
+ want = true;
+ }
+ else
+ {
+ if (strBitrate < selectedBitrate)
+ want = true;
+ }
+
+ if (want)
+ {
+ selectedRes = strRes;
+ selectedBitrate = strBitrate;
+ prog = i;
+ }
+ }
+ return prog;
+}
+
+std::string CDVDDemuxFFmpeg::GetStereoModeFromMetadata(AVDictionary* pMetadata)
+{
+ std::string stereoMode;
+ AVDictionaryEntry* tag = NULL;
+
+ // matroska
+ tag = av_dict_get(pMetadata, "stereo_mode", NULL, 0);
+ if (tag && tag->value)
+ stereoMode = tag->value;
+
+ // asf / wmv
+ if (stereoMode.empty())
+ {
+ tag = av_dict_get(pMetadata, "Stereoscopic", NULL, 0);
+ if (tag && tag->value)
+ {
+ tag = av_dict_get(pMetadata, "StereoscopicLayout", NULL, 0);
+ if (tag && tag->value)
+ stereoMode = ConvertCodecToInternalStereoMode(tag->value, WmvToInternalStereoModeMap);
+ }
+ }
+
+ return stereoMode;
+}
+
+std::string CDVDDemuxFFmpeg::ConvertCodecToInternalStereoMode(const std::string &mode, const StereoModeConversionMap* conversionMap)
+{
+ size_t i = 0;
+ while (conversionMap[i].name)
+ {
+ if (mode == conversionMap[i].name)
+ return conversionMap[i].mode;
+ i++;
+ }
+ return "";
+}
+
+void CDVDDemuxFFmpeg::ParsePacket(AVPacket* pkt)
+{
+ AVStream* st = m_pFormatContext->streams[pkt->stream_index];
+
+ if (st && st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
+ {
+ auto parser = m_parsers.find(st->index);
+ if (parser == m_parsers.end())
+ {
+ m_parsers.insert(std::make_pair(st->index,
+ std::unique_ptr<CDemuxParserFFmpeg>(new CDemuxParserFFmpeg())));
+ parser = m_parsers.find(st->index);
+
+ parser->second->m_parserCtx = av_parser_init(st->codecpar->codec_id);
+
+ AVCodec* codec = avcodec_find_decoder(st->codecpar->codec_id);
+ if (codec == nullptr)
+ {
+ CLog::Log(LOGERROR, "{} - can't find decoder", __FUNCTION__);
+ m_parsers.erase(parser);
+ return;
+ }
+ parser->second->m_codecCtx = avcodec_alloc_context3(codec);
+ }
+
+ CDemuxStream* stream = GetStream(st->index);
+ if (!stream)
+ return;
+
+ if (parser->second->m_parserCtx &&
+ parser->second->m_parserCtx->parser &&
+ parser->second->m_parserCtx->parser->split &&
+ !st->codecpar->extradata)
+ {
+ int i = parser->second->m_parserCtx->parser->split(parser->second->m_codecCtx, pkt->data, pkt->size);
+ if (i > 0 && i < FF_MAX_EXTRADATA_SIZE)
+ {
+ st->codecpar->extradata = (uint8_t*)av_malloc(i + AV_INPUT_BUFFER_PADDING_SIZE);
+ if (st->codecpar->extradata)
+ {
+ CLog::Log(LOGDEBUG,
+ "CDVDDemuxFFmpeg::ParsePacket() fetching extradata, extradata_size({})", i);
+ st->codecpar->extradata_size = i;
+ memcpy(st->codecpar->extradata, pkt->data, i);
+ memset(st->codecpar->extradata + i, 0, AV_INPUT_BUFFER_PADDING_SIZE);
+
+ if (parser->second->m_parserCtx->parser->parser_parse)
+ {
+ parser->second->m_codecCtx->extradata = st->codecpar->extradata;
+ parser->second->m_codecCtx->extradata_size = st->codecpar->extradata_size;
+ const uint8_t* outbufptr;
+ int bufSize;
+ parser->second->m_parserCtx->flags |= PARSER_FLAG_COMPLETE_FRAMES;
+ parser->second->m_parserCtx->parser->parser_parse(parser->second->m_parserCtx,
+ parser->second->m_codecCtx,
+ &outbufptr, &bufSize,
+ pkt->data, pkt->size);
+ parser->second->m_codecCtx->extradata = nullptr;
+ parser->second->m_codecCtx->extradata_size = 0;
+
+ if (parser->second->m_parserCtx->width != 0)
+ {
+ st->codecpar->width = parser->second->m_parserCtx->width;
+ st->codecpar->height = parser->second->m_parserCtx->height;
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxFFmpeg::ParsePacket() invalid width/height");
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+TRANSPORT_STREAM_STATE CDVDDemuxFFmpeg::TransportStreamAudioState()
+{
+ AVStream* st = nullptr;
+ bool hasAudio = false;
+
+ if (m_program != UINT_MAX)
+ {
+ for (unsigned int i = 0; i < m_pFormatContext->programs[m_program]->nb_stream_indexes; i++)
+ {
+ int idx = m_pFormatContext->programs[m_program]->stream_index[i];
+ st = m_pFormatContext->streams[idx];
+ if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
+ {
+ if (st->start_time != AV_NOPTS_VALUE)
+ {
+ if (!m_startTime)
+ {
+ m_startTime = av_rescale(st->cur_dts, st->time_base.num, st->time_base.den) - 0.000001;
+ m_seekStream = idx;
+ }
+ return TRANSPORT_STREAM_STATE::READY;
+ }
+ hasAudio = true;
+ }
+ }
+ }
+ else
+ {
+ for (unsigned int i = 0; i < m_pFormatContext->nb_streams; i++)
+ {
+ st = m_pFormatContext->streams[i];
+ if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
+ {
+ if (st->start_time != AV_NOPTS_VALUE)
+ {
+ if (!m_startTime)
+ {
+ m_startTime = av_rescale(st->cur_dts, st->time_base.num, st->time_base.den) - 0.000001;
+ m_seekStream = i;
+ }
+ return TRANSPORT_STREAM_STATE::READY;
+ }
+ hasAudio = true;
+ }
+ }
+ }
+
+ return (hasAudio) ? TRANSPORT_STREAM_STATE::NOTREADY : TRANSPORT_STREAM_STATE::NONE;
+}
+
+TRANSPORT_STREAM_STATE CDVDDemuxFFmpeg::TransportStreamVideoState()
+{
+ AVStream* st = nullptr;
+ bool hasVideo = false;
+
+ if (m_program == 0 && !m_pFormatContext->nb_programs)
+ return TRANSPORT_STREAM_STATE::NONE;
+
+ if (m_program != UINT_MAX)
+ {
+ for (unsigned int i = 0; i < m_pFormatContext->programs[m_program]->nb_stream_indexes; i++)
+ {
+ int idx = m_pFormatContext->programs[m_program]->stream_index[i];
+ st = m_pFormatContext->streams[idx];
+ if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
+ {
+ if (st->codecpar->extradata)
+ {
+ if (!m_startTime)
+ {
+ m_startTime = av_rescale(st->cur_dts, st->time_base.num, st->time_base.den) - 0.000001;
+ m_seekStream = idx;
+ }
+ return TRANSPORT_STREAM_STATE::READY;
+ }
+ hasVideo = true;
+ }
+ }
+ }
+ else
+ {
+ for (unsigned int i = 0; i < m_pFormatContext->nb_streams; i++)
+ {
+ st = m_pFormatContext->streams[i];
+ if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
+ {
+ if (st->codecpar->extradata)
+ {
+ if (!m_startTime)
+ {
+ m_startTime = av_rescale(st->cur_dts, st->time_base.num, st->time_base.den) - 0.000001;
+ m_seekStream = i;
+ }
+ return TRANSPORT_STREAM_STATE::READY;
+ }
+ hasVideo = true;
+ }
+ }
+ }
+
+ return (hasVideo) ? TRANSPORT_STREAM_STATE::NOTREADY : TRANSPORT_STREAM_STATE::NONE;
+}
+
+bool CDVDDemuxFFmpeg::IsTransportStreamReady()
+{
+ if (!m_checkTransportStream)
+ return true;
+
+ if (m_program == 0 && !m_pFormatContext->nb_programs)
+ return false;
+
+ TRANSPORT_STREAM_STATE state = TransportStreamVideoState();
+ if (state == TRANSPORT_STREAM_STATE::NONE)
+ state = TransportStreamAudioState();
+
+ return state == TRANSPORT_STREAM_STATE::READY;
+}
+
+void CDVDDemuxFFmpeg::ResetVideoStreams()
+{
+ AVStream* st;
+ for (unsigned int i = 0; i < m_pFormatContext->nb_streams; i++)
+ {
+ st = m_pFormatContext->streams[i];
+ if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
+ {
+ av_freep(&st->codecpar->extradata);
+ st->codecpar->extradata_size = 0;
+ }
+ }
+}
+
+void CDVDDemuxFFmpeg::GetL16Parameters(int &channels, int &samplerate)
+{
+ std::string content;
+ if (XFILE::CCurlFile::GetContentType(m_pInput->GetURL(), content))
+ {
+ StringUtils::ToLower(content);
+ const size_t len = content.length();
+ size_t pos = content.find(';');
+ while (pos < len)
+ {
+ // move to the next non-whitespace character
+ pos = content.find_first_not_of(" \t", pos + 1);
+
+ if (pos != std::string::npos)
+ {
+ if (content.compare(pos, 9, "channels=", 9) == 0)
+ {
+ pos += 9; // move position to char after 'channels='
+ size_t len = content.find(';', pos);
+ if (len != std::string::npos)
+ len -= pos;
+ std::string no_channels(content, pos, len);
+ // as we don't support any charset with ';' in name
+ StringUtils::Trim(no_channels, " \t");
+ if (!no_channels.empty())
+ {
+ int val = strtol(no_channels.c_str(), NULL, 0);
+ if (val > 0)
+ channels = val;
+ else
+ CLog::Log(LOGDEBUG, "CDVDDemuxFFmpeg::{} - no parameter for channels", __FUNCTION__);
+ }
+ }
+ else if (content.compare(pos, 5, "rate=", 5) == 0)
+ {
+ pos += 5; // move position to char after 'rate='
+ size_t len = content.find(';', pos);
+ if (len != std::string::npos)
+ len -= pos;
+ std::string rate(content, pos, len);
+ // as we don't support any charset with ';' in name
+ StringUtils::Trim(rate, " \t");
+ if (!rate.empty())
+ {
+ int val = strtol(rate.c_str(), NULL, 0);
+ if (val > 0)
+ samplerate = val;
+ else
+ CLog::Log(LOGDEBUG, "CDVDDemuxFFmpeg::{} - no parameter for samplerate",
+ __FUNCTION__);
+ }
+ }
+ pos = content.find(';', pos); // find next parameter
+ }
+ }
+ }
+}
+
+StreamHdrType CDVDDemuxFFmpeg::DetermineHdrType(AVStream* pStream)
+{
+ StreamHdrType hdrType = StreamHdrType::HDR_TYPE_NONE;
+
+ if (av_stream_get_side_data(pStream, AV_PKT_DATA_DOVI_CONF, nullptr)) // DoVi
+ hdrType = StreamHdrType::HDR_TYPE_DOLBYVISION;
+ else if (pStream->codecpar->color_trc == AVCOL_TRC_SMPTE2084) // HDR10
+ hdrType = StreamHdrType::HDR_TYPE_HDR10;
+ else if (pStream->codecpar->color_trc == AVCOL_TRC_ARIB_STD_B67) // HLG
+ hdrType = StreamHdrType::HDR_TYPE_HLG;
+ // file could be SMPTE2086 which FFmpeg currently returns as unknown
+ // so use the presence of static metadata to detect it
+ else if (av_stream_get_side_data(pStream, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, nullptr))
+ hdrType = StreamHdrType::HDR_TYPE_HDR10;
+
+ return hdrType;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxFFmpeg.h b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxFFmpeg.h
new file mode 100644
index 0000000..966817d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxFFmpeg.h
@@ -0,0 +1,181 @@
+/*
+ * 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 "DVDDemux.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include <map>
+#include <memory>
+#include <vector>
+
+extern "C" {
+#include <libavformat/avformat.h>
+}
+
+class CDVDDemuxFFmpeg;
+class CURL;
+
+enum class TRANSPORT_STREAM_STATE
+{
+ NONE,
+ READY,
+ NOTREADY,
+};
+
+class CDemuxStreamVideoFFmpeg : public CDemuxStreamVideo
+{
+public:
+ explicit CDemuxStreamVideoFFmpeg(AVStream* stream) : m_stream(stream) {}
+ std::string GetStreamName() override;
+
+ std::string m_description;
+protected:
+ AVStream* m_stream = nullptr;
+};
+
+class CDemuxStreamAudioFFmpeg : public CDemuxStreamAudio
+{
+public:
+ explicit CDemuxStreamAudioFFmpeg(AVStream* stream) : m_stream(stream) {}
+ std::string GetStreamName() override;
+
+ std::string m_description;
+protected:
+ CDVDDemuxFFmpeg* m_parent;
+ AVStream* m_stream = nullptr;
+};
+
+class CDemuxStreamSubtitleFFmpeg
+ : public CDemuxStreamSubtitle
+{
+public:
+ explicit CDemuxStreamSubtitleFFmpeg(AVStream* stream) : m_stream(stream) {}
+ std::string GetStreamName() override;
+
+ std::string m_description;
+protected:
+ CDVDDemuxFFmpeg* m_parent;
+ AVStream* m_stream = nullptr;
+};
+
+class CDemuxParserFFmpeg
+{
+public:
+ ~CDemuxParserFFmpeg();
+ AVCodecParserContext* m_parserCtx = nullptr;
+ AVCodecContext* m_codecCtx = nullptr;
+};
+
+#define FFMPEG_DVDNAV_BUFFER_SIZE 2048 // for dvd's
+
+struct StereoModeConversionMap;
+
+class CDVDDemuxFFmpeg : public CDVDDemux
+{
+public:
+ CDVDDemuxFFmpeg();
+ ~CDVDDemuxFFmpeg() override;
+
+ bool Open(const std::shared_ptr<CDVDInputStream>& pInput, bool fileinfo);
+ void Dispose();
+ bool Reset() override ;
+ void Flush() override;
+ void Abort() override;
+ void SetSpeed(int iSpeed) override;
+ std::string GetFileName() override;
+
+ DemuxPacket* Read() override;
+
+ bool SeekTime(double time, bool backwards = false, double* startpts = NULL) override;
+ bool SeekByte(int64_t pos);
+ int GetStreamLength() override;
+ CDemuxStream* GetStream(int iStreamId) const override;
+ std::vector<CDemuxStream*> GetStreams() const override;
+ int GetNrOfStreams() const override;
+ int GetPrograms(std::vector<ProgramInfo>& programs) override;
+ void SetProgram(int progId) override;
+
+ bool SeekChapter(int chapter, double* startpts = NULL) override;
+ int GetChapterCount() override;
+ int GetChapter() override;
+ void GetChapterName(std::string& strChapterName, int chapterIdx=-1) override;
+ int64_t GetChapterPos(int chapterIdx = -1) override;
+ std::string GetStreamCodecName(int iStreamId) override;
+
+ bool Aborted();
+
+ AVFormatContext* m_pFormatContext;
+ std::shared_ptr<CDVDInputStream> m_pInput;
+
+protected:
+ friend class CDemuxStreamAudioFFmpeg;
+ friend class CDemuxStreamVideoFFmpeg;
+ friend class CDemuxStreamSubtitleFFmpeg;
+
+ CDemuxStream* AddStream(int streamIdx);
+ void AddStream(int streamIdx, CDemuxStream* stream);
+ void CreateStreams(unsigned int program = UINT_MAX);
+ void DisposeStreams();
+ void ParsePacket(AVPacket* pkt);
+ TRANSPORT_STREAM_STATE TransportStreamAudioState();
+ TRANSPORT_STREAM_STATE TransportStreamVideoState();
+ bool IsTransportStreamReady();
+ void ResetVideoStreams();
+ AVDictionary* GetFFMpegOptionsFromInput();
+ double ConvertTimestamp(int64_t pts, int den, int num);
+ void UpdateCurrentPTS();
+ bool IsProgramChange();
+ unsigned int HLSSelectProgram();
+
+ std::string GetStereoModeFromMetadata(AVDictionary* pMetadata);
+ std::string ConvertCodecToInternalStereoMode(const std::string& mode, const StereoModeConversionMap* conversionMap);
+
+ void GetL16Parameters(int& channels, int& samplerate);
+ double SelectAspect(AVStream* st, bool& forced);
+
+ StreamHdrType DetermineHdrType(AVStream* pStream);
+
+ CCriticalSection m_critSection;
+ std::map<int, CDemuxStream*> m_streams;
+ std::map<int, std::unique_ptr<CDemuxParserFFmpeg>> m_parsers;
+
+ AVIOContext* m_ioContext;
+
+ double m_currentPts; // used for stream length estimation
+ bool m_bMatroska;
+ bool m_bAVI;
+ bool m_bSup;
+ int m_speed;
+ unsigned int m_program;
+ unsigned int m_streamsInProgram;
+ unsigned int m_newProgram;
+ unsigned int m_initialProgramNumber;
+ int m_seekStream;
+
+ XbmcThreads::EndTime<> m_timeout;
+
+ // Due to limitations of ffmpeg, we only can detect a program change
+ // with a packet. This struct saves the packet for the next read and
+ // signals STREAMCHANGE to player
+ struct
+ {
+ AVPacket pkt; // packet ffmpeg returned
+ int result; // result from av_read_packet
+ }m_pkt;
+
+ bool m_streaminfo;
+ bool m_reopen = false;
+ bool m_checkTransportStream;
+ int m_displayTime = 0;
+ double m_dtsAtDisplayTime;
+ bool m_seekToKeyFrame = false;
+ double m_startTime = 0;
+};
+
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.cpp b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.cpp
new file mode 100644
index 0000000..66127c2
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.cpp
@@ -0,0 +1,111 @@
+/*
+ * 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 "DVDDemuxUtils.h"
+
+#include "cores/VideoPlayer/Interface/DemuxCrypto.h"
+#include "utils/MemUtils.h"
+#include "utils/log.h"
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+}
+
+void CDVDDemuxUtils::FreeDemuxPacket(DemuxPacket* pPacket)
+{
+ if (pPacket)
+ {
+ if (pPacket->pData)
+ KODI::MEMORY::AlignedFree(pPacket->pData);
+ if (pPacket->iSideDataElems)
+ {
+ AVPacket* avPkt = av_packet_alloc();
+ if (!avPkt)
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxUtils::{} - av_packet_alloc failed: {}", __FUNCTION__,
+ strerror(errno));
+ }
+ else
+ {
+ avPkt->side_data = static_cast<AVPacketSideData*>(pPacket->pSideData);
+ avPkt->side_data_elems = pPacket->iSideDataElems;
+
+ //! @todo: properly handle avpkt side_data. this works around our improper use of the side_data
+ // as we pass pointers to ffmpeg allocated memory for the side_data. we should really be allocating
+ // and storing our own AVPacket. This will require some extensive changes.
+
+ // here we make use of ffmpeg to free the side_data, we shouldn't have to allocate an intermediate AVPacket though
+ av_packet_free(&avPkt);
+ }
+ }
+ if (pPacket->cryptoInfo)
+ delete pPacket->cryptoInfo;
+ delete pPacket;
+ }
+}
+
+DemuxPacket* CDVDDemuxUtils::AllocateDemuxPacket(int iDataSize)
+{
+ DemuxPacket* pPacket = new DemuxPacket();
+
+ if (iDataSize > 0)
+ {
+ // need to allocate a few bytes more.
+ // From avcodec.h (ffmpeg)
+ /**
+ * Required number of additionally allocated bytes at the end of the input bitstream for decoding.
+ * this is mainly needed because some optimized bitstream readers read
+ * 32 or 64 bit at once and could read over the end<br>
+ * Note, if the first 23 bits of the additional bytes are not 0 then damaged
+ * MPEG bitstreams could cause overread and segfault
+ */
+ pPacket->pData = static_cast<uint8_t*>(KODI::MEMORY::AlignedMalloc(iDataSize + AV_INPUT_BUFFER_PADDING_SIZE, 16));
+ if (!pPacket->pData)
+ {
+ FreeDemuxPacket(pPacket);
+ return NULL;
+ }
+
+ // reset the last 8 bytes to 0;
+ memset(pPacket->pData + iDataSize, 0, AV_INPUT_BUFFER_PADDING_SIZE);
+ }
+
+ return pPacket;
+}
+
+DemuxPacket* CDVDDemuxUtils::AllocateDemuxPacket(unsigned int iDataSize, unsigned int encryptedSubsampleCount)
+{
+ DemuxPacket *ret(AllocateDemuxPacket(iDataSize));
+ if (ret && encryptedSubsampleCount > 0)
+ ret->cryptoInfo = new DemuxCryptoInfo(encryptedSubsampleCount);
+ return ret;
+}
+
+void CDVDDemuxUtils::StoreSideData(DemuxPacket *pkt, AVPacket *src)
+{
+ AVPacket* avPkt = av_packet_alloc();
+ if (!avPkt)
+ {
+ CLog::Log(LOGERROR, "CDVDDemuxUtils::{} - av_packet_alloc failed: {}", __FUNCTION__,
+ strerror(errno));
+ return;
+ }
+
+ // here we make allocate an intermediate AVPacket to allow ffmpeg to allocate the side_data
+ // via the copy below. we then reference this allocated memory in the DemuxPacket. this behaviour
+ // is bad and will require a larger rework.
+ av_packet_copy_props(avPkt, src);
+ pkt->pSideData = avPkt->side_data;
+ pkt->iSideDataElems = avPkt->side_data_elems;
+
+ //! @todo: properly handle avpkt side_data. this works around our improper use of the side_data
+ // as we pass pointers to ffmpeg allocated memory for the side_data. we should really be allocating
+ // and storing our own AVPacket. This will require some extensive changes.
+ av_buffer_unref(&avPkt->buf);
+ av_free(avPkt);
+}
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.h b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.h
new file mode 100644
index 0000000..dcb44bc
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.h
@@ -0,0 +1,24 @@
+/*
+ * 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/VideoPlayer/Interface/DemuxPacket.h"
+extern "C" {
+#include <libavcodec/avcodec.h>
+}
+
+class CDVDDemuxUtils
+{
+public:
+ static void FreeDemuxPacket(DemuxPacket* pPacket);
+ static DemuxPacket* AllocateDemuxPacket(int iDataSize = 0);
+ static DemuxPacket* AllocateDemuxPacket(unsigned int iDataSize, unsigned int encryptedSubsampleCount);
+ static void StoreSideData(DemuxPacket *pkt, AVPacket *src);
+};
+
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxVobsub.cpp b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxVobsub.cpp
new file mode 100644
index 0000000..5c7ef1b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxVobsub.cpp
@@ -0,0 +1,269 @@
+/*
+ * 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 "DVDDemuxVobsub.h"
+
+#include "DVDCodecs/DVDCodecs.h"
+#include "DVDDemuxFFmpeg.h"
+#include "DVDInputStreams/DVDFactoryInputStream.h"
+#include "DVDInputStreams/DVDInputStream.h"
+#include "DVDStreamInfo.h"
+#include "DVDSubtitles/DVDSubtitleStream.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/StringUtils.h"
+
+CDVDDemuxVobsub::CDVDDemuxVobsub() = default;
+
+CDVDDemuxVobsub::~CDVDDemuxVobsub()
+{
+ for(unsigned i=0;i<m_Streams.size();i++)
+ {
+ delete m_Streams[i];
+ }
+ m_Streams.clear();
+}
+
+std::vector<CDemuxStream*> CDVDDemuxVobsub::GetStreams() const
+{
+ std::vector<CDemuxStream*> streams;
+
+ streams.reserve(m_Streams.size());
+ for (auto iter : m_Streams)
+ streams.push_back(iter);
+
+ return streams;
+}
+
+bool CDVDDemuxVobsub::Open(const std::string& filename, int source, const std::string& subfilename)
+{
+ m_Filename = filename;
+ m_source = source;
+
+ std::unique_ptr<CDVDSubtitleStream> pStream(new CDVDSubtitleStream());
+ if(!pStream->Open(filename))
+ return false;
+
+ std::string vobsub = subfilename;
+ if ( vobsub == "")
+ {
+ vobsub = filename;
+ vobsub.erase(vobsub.rfind('.'), vobsub.size());
+ vobsub += ".sub";
+ }
+
+ CFileItem item(vobsub, false);
+ item.SetMimeType("video/x-vobsub");
+ item.SetContentLookup(false);
+ m_Input = CDVDFactoryInputStream::CreateInputStream(NULL, item);
+ if (!m_Input || !m_Input->Open())
+ return false;
+
+ m_Demuxer.reset(new CDVDDemuxFFmpeg());
+ if (!m_Demuxer->Open(m_Input, false))
+ return false;
+
+ CDVDStreamInfo hints;
+ CDVDCodecOptions options;
+ hints.codec = AV_CODEC_ID_DVD_SUBTITLE;
+
+ std::string line;
+
+ SState state;
+ state.delay = 0;
+ state.id = -1;
+
+ while (pStream->ReadLine(line))
+ {
+ if (line[0] == '#')
+ continue;
+
+ size_t pos = line.find_first_of(':');
+ if (pos != std::string::npos)
+ {
+ pos += 1;
+ std::string param = line.substr(0, pos);
+ std::string data = line.substr(pos, line.size() - pos);
+
+ if (param == "langidx:")
+ ParseLangIdx(state, data);
+ else if (param == "delay:")
+ ParseDelay(state, data);
+ else if (param == "id:")
+ ParseId(state, data);
+ else if (param == "timestamp:")
+ ParseTimestamp(state, data);
+ else if (param == "palette:" || param == "size:" || param == "org:" ||
+ param == "custom colors:" || param == "scale:" || param == "alpha:" ||
+ param == "fadein/out:" || param == "forced subs:")
+ ParseExtra(state, line);
+ else
+ continue;
+ }
+ }
+
+ struct sorter s;
+ sort(m_Timestamps.begin(), m_Timestamps.end(), s);
+ m_Timestamp = m_Timestamps.begin();
+
+ for(unsigned i=0;i<m_Streams.size();i++)
+ {
+ m_Streams[i]->ExtraSize = state.extra.length()+1;
+ m_Streams[i]->ExtraData = std::make_unique<uint8_t[]>(m_Streams[i]->ExtraSize);
+ strcpy((char*)m_Streams[i]->ExtraData.get(), state.extra.c_str());
+ }
+
+ return true;
+}
+
+bool CDVDDemuxVobsub::Reset()
+{
+ Flush();
+ return true;
+}
+
+void CDVDDemuxVobsub::Flush()
+{
+ m_Demuxer->Flush();
+}
+
+bool CDVDDemuxVobsub::SeekTime(double time, bool backwards, double* startpts)
+{
+ double pts = DVD_MSEC_TO_TIME(time);
+ m_Timestamp = m_Timestamps.begin();
+ for (;m_Timestamp != m_Timestamps.end();++m_Timestamp)
+ {
+ if(m_Timestamp->pts > pts)
+ break;
+ }
+ for (unsigned i=0;i<m_Streams.size() && m_Timestamps.begin() != m_Timestamp;i++)
+ {
+ --m_Timestamp;
+ }
+ return true;
+}
+
+DemuxPacket* CDVDDemuxVobsub::Read()
+{
+ std::vector<STimestamp>::iterator current;
+ do {
+ if(m_Timestamp == m_Timestamps.end())
+ return NULL;
+
+ current = m_Timestamp++;
+ } while(m_Streams[current->id]->m_discard == true);
+
+ if(!m_Demuxer->SeekByte(current->pos))
+ return NULL;
+
+ DemuxPacket *packet = m_Demuxer->Read();
+ if(!packet)
+ return NULL;
+
+ packet->iStreamId = current->id;
+ packet->pts = current->pts;
+ packet->dts = current->pts;
+
+ return packet;
+}
+
+bool CDVDDemuxVobsub::ParseLangIdx(SState& state, std::string& line)
+{
+ return true;
+}
+
+bool CDVDDemuxVobsub::ParseDelay(SState& state, std::string& line)
+{
+ int h,m,s,ms;
+ bool negative = false;
+
+ StringUtils::Trim(line);
+
+ if (line[0] == '-')
+ {
+ negative = true;
+ line.erase(0, 1);
+ }
+
+ if (sscanf(line.c_str(), "%d:%d:%d:%d", &h, &m, &s, &ms) != 4)
+ return false;
+ state.delay = h*3600.0 + m*60.0 + s + ms*0.001;
+ if(negative)
+ state.delay *= -1;
+ return true;
+}
+
+bool CDVDDemuxVobsub::ParseId(SState& state, std::string& line)
+{
+ std::unique_ptr<CStream> stream(new CStream(this));
+
+ StringUtils::Trim(line);
+ stream->language = line.substr(0, 2);
+
+ size_t pos = line.find_first_of(',');
+ if (pos != std::string::npos)
+ {
+ pos += 1;
+ line.erase(0, pos);
+ }
+ StringUtils::TrimLeft(line);
+ pos = line.find_first_of(':');
+ if (pos != std::string::npos && line.substr(0, pos + 1) == "index:")
+ {
+ pos += 1;
+ stream->uniqueId = std::atoi(line.substr(pos, line.size() - pos).c_str());
+ }
+ else
+ stream->uniqueId = -1;
+
+ stream->codec = AV_CODEC_ID_DVD_SUBTITLE;
+ stream->uniqueId = m_Streams.size();
+ stream->source = m_source;
+ stream->demuxerId = m_demuxerId;
+
+ state.id = stream->uniqueId;
+ m_Streams.push_back(stream.release());
+ return true;
+}
+
+bool CDVDDemuxVobsub::ParseExtra(SState& state, const std::string& line)
+{
+ state.extra += line;
+ state.extra += '\n';
+ return true;
+}
+
+bool CDVDDemuxVobsub::ParseTimestamp(SState& state, std::string& line)
+{
+ if(state.id < 0)
+ return false;
+
+ int h,m,s,ms;
+ STimestamp timestamp;
+
+ StringUtils::Trim(line);
+ if (sscanf(line.c_str(), "%d:%d:%d:%d, filepos:%" PRIx64, &h, &m, &s, &ms, &timestamp.pos) != 5)
+ return false;
+
+ timestamp.id = state.id;
+ timestamp.pts = DVD_SEC_TO_TIME(state.delay + h*3600.0 + m*60.0 + s + ms*0.001);
+ m_Timestamps.push_back(timestamp);
+ return true;
+}
+
+void CDVDDemuxVobsub::EnableStream(int id, bool enable)
+{
+ for (auto &stream : m_Streams)
+ {
+ if (stream->uniqueId == id)
+ {
+ stream->m_discard = !enable;
+ break;
+ }
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxVobsub.h b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxVobsub.h
new file mode 100644
index 0000000..c82b6a1
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDDemuxVobsub.h
@@ -0,0 +1,88 @@
+/*
+ * 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 "DVDDemux.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CDVDOverlayCodecFFmpeg;
+class CDVDInputStream;
+class CDVDDemuxFFmpeg;
+
+class CDVDDemuxVobsub : public CDVDDemux
+{
+public:
+ CDVDDemuxVobsub();
+ ~CDVDDemuxVobsub() override;
+
+ bool Open(const std::string& filename, int source, const std::string& subfilename);
+
+ // implementation of CDVDDemux
+ bool Reset() override;
+ void Flush() override;
+ DemuxPacket* Read() override;
+ bool SeekTime(double time, bool backwards, double* startpts = NULL) override;
+ CDemuxStream* GetStream(int index) const override { return m_Streams[index]; }
+ std::vector<CDemuxStream*> GetStreams() const override;
+ int GetNrOfStreams() const override { return m_Streams.size(); }
+ std::string GetFileName() override { return m_Filename; }
+ void EnableStream(int id, bool enable) override;
+
+private:
+ class CStream
+ : public CDemuxStreamSubtitle
+ {
+ public:
+ explicit CStream(CDVDDemuxVobsub* parent)
+ : m_discard(false), m_parent(parent)
+ {}
+
+ bool m_discard;
+ CDVDDemuxVobsub* m_parent;
+ };
+
+ typedef struct STimestamp
+ {
+ int64_t pos;
+ double pts;
+ int id;
+ } STimestamp;
+
+ std::string m_Filename;
+ std::shared_ptr<CDVDInputStream> m_Input;
+ std::unique_ptr<CDVDDemuxFFmpeg> m_Demuxer;
+ std::vector<STimestamp> m_Timestamps;
+ std::vector<STimestamp>::iterator m_Timestamp;
+ std::vector<CStream*> m_Streams;
+ int m_source = -1;
+
+ typedef struct SState
+ {
+ int id;
+ double delay;
+ std::string extra;
+ } SState;
+
+ struct sorter
+ {
+ bool operator()(const STimestamp &p1, const STimestamp &p2)
+ {
+ return p1.pts < p2.pts || (p1.pts == p2.pts && p1.id < p2.id);
+ }
+ };
+
+ bool ParseLangIdx(SState& state, std::string& line);
+ bool ParseDelay(SState& state, std::string& line);
+ bool ParseId(SState& state, std::string& line);
+ bool ParseExtra(SState& state, const std::string& line);
+ bool ParseTimestamp(SState& state, std::string& line);
+};
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDFactoryDemuxer.cpp b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDFactoryDemuxer.cpp
new file mode 100644
index 0000000..35ba376
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDFactoryDemuxer.cpp
@@ -0,0 +1,80 @@
+/*
+ * 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 "DVDFactoryDemuxer.h"
+
+#include "DVDDemuxBXA.h"
+#include "DVDDemuxCDDA.h"
+#include "DVDDemuxClient.h"
+#include "DVDDemuxFFmpeg.h"
+#include "DVDInputStreams/DVDInputStream.h"
+#include "DemuxMultiSource.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+CDVDDemux* CDVDFactoryDemuxer::CreateDemuxer(const std::shared_ptr<CDVDInputStream>& pInputStream,
+ bool fileinfo)
+{
+ if (!pInputStream)
+ return NULL;
+
+ // Try to open the AirTunes demuxer
+ if (pInputStream->IsStreamType(DVDSTREAM_TYPE_FILE) && pInputStream->GetContent().compare("audio/x-xbmc-pcm") == 0 )
+ {
+ // audio/x-xbmc-pcm this is the used codec for AirTunes
+ // (apples audio only streaming)
+ std::unique_ptr<CDVDDemuxBXA> demuxer(new CDVDDemuxBXA());
+ if(demuxer->Open(pInputStream))
+ return demuxer.release();
+ else
+ return NULL;
+ }
+
+ // Try to open CDDA demuxer
+ if (pInputStream->IsStreamType(DVDSTREAM_TYPE_FILE) && pInputStream->GetContent().compare("application/octet-stream") == 0)
+ {
+ std::string filename = pInputStream->GetFileName();
+ if (filename.substr(0, 7) == "cdda://")
+ {
+ CLog::Log(LOGDEBUG, "DVDFactoryDemuxer: Stream is probably CD audio. Creating CDDA demuxer.");
+
+ std::unique_ptr<CDVDDemuxCDDA> demuxer(new CDVDDemuxCDDA());
+ if (demuxer->Open(pInputStream))
+ {
+ return demuxer.release();
+ }
+ }
+ }
+
+ // Input stream handles demuxing
+ if (pInputStream->GetIDemux())
+ {
+ std::unique_ptr<CDVDDemuxClient> demuxer(new CDVDDemuxClient());
+ if(demuxer->Open(pInputStream))
+ return demuxer.release();
+ else
+ return nullptr;
+ }
+
+ // Try to open the MultiFiles demuxer
+ if (pInputStream->IsStreamType(DVDSTREAM_TYPE_MULTIFILES))
+ {
+ std::unique_ptr<CDemuxMultiSource> demuxer(new CDemuxMultiSource());
+ if (demuxer->Open(pInputStream))
+ return demuxer.release();
+ else
+ return NULL;
+ }
+
+ std::unique_ptr<CDVDDemuxFFmpeg> demuxer(new CDVDDemuxFFmpeg());
+ if (demuxer->Open(pInputStream, fileinfo))
+ return demuxer.release();
+ else
+ return NULL;
+}
+
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DVDFactoryDemuxer.h b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDFactoryDemuxer.h
new file mode 100644
index 0000000..1d2bddf
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DVDFactoryDemuxer.h
@@ -0,0 +1,21 @@
+/*
+ * 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 <memory>
+
+class CDVDDemux;
+class CDVDInputStream;
+
+class CDVDFactoryDemuxer
+{
+public:
+ static CDVDDemux* CreateDemuxer(const std::shared_ptr<CDVDInputStream>& pInputStream,
+ bool fileinfo = false);
+};
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DemuxMultiSource.cpp b/xbmc/cores/VideoPlayer/DVDDemuxers/DemuxMultiSource.cpp
new file mode 100644
index 0000000..9f49630
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DemuxMultiSource.cpp
@@ -0,0 +1,236 @@
+/*
+ * 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 "DemuxMultiSource.h"
+
+#include "DVDDemuxUtils.h"
+#include "DVDFactoryDemuxer.h"
+#include "DVDInputStreams/DVDInputStream.h"
+#include "Util.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/log.h"
+
+
+CDemuxMultiSource::CDemuxMultiSource() = default;
+
+CDemuxMultiSource::~CDemuxMultiSource()
+{
+ Dispose();
+}
+
+void CDemuxMultiSource::Abort()
+{
+ for (auto& iter : m_demuxerMap)
+ iter.second->Abort();
+}
+
+void CDemuxMultiSource::Dispose()
+{
+ while (!m_demuxerQueue.empty())
+ {
+ m_demuxerQueue.pop();
+ }
+
+ m_demuxerMap.clear();
+ m_DemuxerToInputStreamMap.clear();
+ m_pInput = NULL;
+
+}
+
+void CDemuxMultiSource::EnableStream(int64_t demuxerId, int id, bool enable)
+{
+ auto iter = m_demuxerMap.find(demuxerId);
+ if (iter != m_demuxerMap.end())
+ {
+ DemuxPtr demuxer = iter->second;
+ demuxer->EnableStream(demuxerId, id, enable);
+ }
+}
+
+void CDemuxMultiSource::Flush()
+{
+ for (auto& iter : m_demuxerMap)
+ iter.second->Flush();
+}
+
+int CDemuxMultiSource::GetNrOfStreams() const
+{
+ int streamsCount = 0;
+ for (auto& iter : m_demuxerMap)
+ streamsCount += iter.second->GetNrOfStreams();
+
+ return streamsCount;
+}
+
+CDemuxStream* CDemuxMultiSource::GetStream(int64_t demuxerId, int iStreamId) const
+{
+ auto iter = m_demuxerMap.find(demuxerId);
+ if (iter != m_demuxerMap.end())
+ {
+ return iter->second->GetStream(demuxerId, iStreamId);
+ }
+ else
+ return NULL;
+}
+
+std::vector<CDemuxStream*> CDemuxMultiSource::GetStreams() const
+{
+ std::vector<CDemuxStream*> streams;
+
+ for (auto& iter : m_demuxerMap)
+ {
+ for (auto& stream : iter.second->GetStreams())
+ {
+ streams.push_back(stream);
+ }
+ }
+ return streams;
+}
+
+std::string CDemuxMultiSource::GetStreamCodecName(int64_t demuxerId, int iStreamId)
+{
+ auto iter = m_demuxerMap.find(demuxerId);
+ if (iter != m_demuxerMap.end())
+ {
+ return iter->second->GetStreamCodecName(demuxerId, iStreamId);
+ }
+ else
+ return "";
+};
+
+int CDemuxMultiSource::GetStreamLength()
+{
+ int length = 0;
+ for (auto& iter : m_demuxerMap)
+ {
+ length = std::max(length, iter.second->GetStreamLength());
+ }
+
+ return length;
+}
+
+bool CDemuxMultiSource::Open(const std::shared_ptr<CDVDInputStream>& pInput)
+{
+ if (!pInput)
+ return false;
+
+ m_pInput = std::dynamic_pointer_cast<InputStreamMultiStreams>(pInput);
+
+ if (!m_pInput)
+ return false;
+
+ auto iter = m_pInput->m_InputStreams.begin();
+ while (iter != m_pInput->m_InputStreams.end())
+ {
+ DemuxPtr demuxer = DemuxPtr(CDVDFactoryDemuxer::CreateDemuxer((*iter)));
+ if (!demuxer)
+ {
+ iter = m_pInput->m_InputStreams.erase(iter);
+ }
+ else
+ {
+ SetMissingStreamDetails(demuxer);
+
+ m_demuxerMap[demuxer->GetDemuxerId()] = demuxer;
+ m_DemuxerToInputStreamMap[demuxer] = *iter;
+ m_demuxerQueue.push(std::make_pair(-1.0, demuxer));
+ ++iter;
+ }
+ }
+ return !m_demuxerMap.empty();
+}
+
+bool CDemuxMultiSource::Reset()
+{
+ bool ret = true;
+ for (auto& iter : m_demuxerMap)
+ {
+ if (!iter.second->Reset())
+ ret = false;
+ }
+ return ret;
+}
+
+DemuxPacket* CDemuxMultiSource::Read()
+{
+ if (m_demuxerQueue.empty())
+ return NULL;
+
+ DemuxPtr currentDemuxer = m_demuxerQueue.top().second;
+ m_demuxerQueue.pop();
+
+ if (!currentDemuxer)
+ return NULL;
+
+ DemuxPacket* packet = currentDemuxer->Read();
+ if (packet)
+ {
+ double readTime = 0;
+ if (packet->dts != DVD_NOPTS_VALUE)
+ readTime = packet->dts;
+ else
+ readTime = packet->pts;
+ m_demuxerQueue.push(std::make_pair(readTime, currentDemuxer));
+ }
+ else
+ {
+ auto input = m_DemuxerToInputStreamMap.find(currentDemuxer);
+ if (input != m_DemuxerToInputStreamMap.end())
+ {
+ if (input->second->IsEOF())
+ {
+ CLog::Log(LOGDEBUG, "{} - Demuxer for file {} is at eof, removed it from the queue",
+ __FUNCTION__, CURL::GetRedacted(currentDemuxer->GetFileName()));
+ }
+ else //maybe add an error counter?
+ m_demuxerQueue.push(std::make_pair(-1.0, currentDemuxer));
+ }
+ }
+
+ return packet;
+}
+
+bool CDemuxMultiSource::SeekTime(double time, bool backwards, double* startpts)
+{
+ DemuxQueue demuxerQueue = DemuxQueue();
+ bool ret = false;
+ for (auto& iter : m_demuxerMap)
+ {
+ if (iter.second->SeekTime(time, false, startpts))
+ {
+ demuxerQueue.push(std::make_pair(*startpts, iter.second));
+ CLog::Log(LOGDEBUG, "{} - starting demuxer from: {:f}", __FUNCTION__, time);
+ ret = true;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - failed to start demuxing from: {:f}", __FUNCTION__, time);
+ }
+ }
+ m_demuxerQueue = demuxerQueue;
+ return ret;
+}
+
+void CDemuxMultiSource::SetMissingStreamDetails(const DemuxPtr& demuxer)
+{
+ std::string baseFileName = m_pInput->GetFileName();
+ std::string fileName = demuxer->GetFileName();
+ for (auto& stream : demuxer->GetStreams())
+ {
+ ExternalStreamInfo info = CUtil::GetExternalStreamDetailsFromFilename(baseFileName, fileName);
+
+ if (stream->flags == StreamFlags::FLAG_NONE)
+ {
+ stream->flags = static_cast<StreamFlags>(info.flag);
+ }
+ if (stream->language.empty())
+ {
+ stream->language = info.language;
+ }
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/DVDDemuxers/DemuxMultiSource.h b/xbmc/cores/VideoPlayer/DVDDemuxers/DemuxMultiSource.h
new file mode 100644
index 0000000..4406fd9
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDDemuxers/DemuxMultiSource.h
@@ -0,0 +1,64 @@
+/*
+ * 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 "DVDDemux.h"
+#include "DVDInputStreams/InputStreamMultiSource.h"
+
+#include <map>
+#include <queue>
+#include <string>
+#include <utility>
+#include <vector>
+
+typedef std::shared_ptr<CDVDDemux> DemuxPtr;
+
+struct comparator{
+ bool operator()(const std::pair<double, DemuxPtr>& x, const std::pair<double, DemuxPtr>& y) const
+ {
+ return x.first > y.first;
+ }
+};
+
+typedef std::priority_queue<std::pair<double, DemuxPtr>, std::vector<std::pair<double, DemuxPtr>>, comparator> DemuxQueue;
+
+class CDemuxMultiSource : public CDVDDemux
+{
+
+public:
+ CDemuxMultiSource();
+ ~CDemuxMultiSource() override;
+
+ bool Open(const std::shared_ptr<CDVDInputStream>& pInput);
+
+ // implementation of CDVDDemux
+ void Abort() override;
+ void EnableStream(int64_t demuxerId, int id, bool enable) override;
+ void Flush() override;
+ int GetNrOfStreams() const override;
+ CDemuxStream* GetStream(int64_t demuxerId, int iStreamId) const override;
+ std::vector<CDemuxStream*> GetStreams() const override;
+ std::string GetStreamCodecName(int64_t demuxerId, int iStreamId) override;
+ int GetStreamLength() override;
+ DemuxPacket* Read() override;
+ bool Reset() override;
+ bool SeekTime(double time, bool backwards = false, double* startpts = NULL) override;
+
+protected:
+ CDemuxStream* GetStream(int iStreamId) const override { return nullptr; }
+
+private:
+ void Dispose();
+ void SetMissingStreamDetails(const DemuxPtr& demuxer);
+
+ std::shared_ptr<InputStreamMultiStreams> m_pInput = NULL;
+ std::map<DemuxPtr, InputStreamPtr> m_DemuxerToInputStreamMap;
+ DemuxQueue m_demuxerQueue;
+ std::map<int64_t, DemuxPtr> m_demuxerMap;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDFileInfo.cpp b/xbmc/cores/VideoPlayer/DVDFileInfo.cpp
new file mode 100644
index 0000000..0860b40
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDFileInfo.cpp
@@ -0,0 +1,497 @@
+/*
+ * 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 "DVDFileInfo.h"
+#include "ServiceBroker.h"
+#include "FileItem.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "pictures/Picture.h"
+#include "video/VideoInfoTag.h"
+#include "filesystem/StackDirectory.h"
+#include "utils/log.h"
+#include "utils/URIUtils.h"
+
+#include "DVDStreamInfo.h"
+#include "DVDInputStreams/DVDInputStream.h"
+#ifdef HAVE_LIBBLURAY
+#include "DVDInputStreams/DVDInputStreamBluray.h"
+#endif
+#include "DVDInputStreams/DVDFactoryInputStream.h"
+#include "DVDDemuxers/DVDDemux.h"
+#include "DVDDemuxers/DVDDemuxUtils.h"
+#include "DVDDemuxers/DVDFactoryDemuxer.h"
+#include "DVDCodecs/DVDFactoryCodec.h"
+#include "DVDCodecs/Video/DVDVideoCodec.h"
+#include "DVDCodecs/Video/DVDVideoCodecFFmpeg.h"
+#include "DVDDemuxers/DVDDemuxVobsub.h"
+#include "Process/ProcessInfo.h"
+
+#include <libavcodec/avcodec.h>
+#include <libswscale/swscale.h>
+#include "filesystem/File.h"
+#include "cores/FFmpeg.h"
+#include "TextureCache.h"
+#include "Util.h"
+#include "utils/LangCodeExpander.h"
+
+#include <cstdlib>
+#include <memory>
+
+extern "C" {
+#include <libavformat/avformat.h>
+}
+
+bool CDVDFileInfo::GetFileDuration(const std::string &path, int& duration)
+{
+ std::unique_ptr<CDVDDemux> demux;
+
+ CFileItem item(path, false);
+ auto input = CDVDFactoryInputStream::CreateInputStream(NULL, item);
+ if (!input)
+ return false;
+
+ if (!input->Open())
+ return false;
+
+ demux.reset(CDVDFactoryDemuxer::CreateDemuxer(input, true));
+ if (!demux)
+ return false;
+
+ duration = demux->GetStreamLength();
+ if (duration > 0)
+ return true;
+ else
+ return false;
+}
+
+int DegreeToOrientation(int degrees)
+{
+ switch(degrees)
+ {
+ case 90:
+ return 5;
+ case 180:
+ return 2;
+ case 270:
+ return 7;
+ default:
+ return 0;
+ }
+}
+
+bool CDVDFileInfo::ExtractThumb(const CFileItem& fileItem,
+ CTextureDetails &details,
+ CStreamDetails *pStreamDetails,
+ int64_t pos)
+{
+ const std::string redactPath = CURL::GetRedacted(fileItem.GetPath());
+ auto start = std::chrono::steady_clock::now();
+
+ CFileItem item(fileItem);
+ item.SetMimeTypeForInternetFile();
+ auto pInputStream = CDVDFactoryInputStream::CreateInputStream(NULL, item);
+ if (!pInputStream)
+ {
+ CLog::Log(LOGERROR, "InputStream: Error creating stream for {}", redactPath);
+ return false;
+ }
+
+ if (!pInputStream->Open())
+ {
+ CLog::Log(LOGERROR, "InputStream: Error opening, {}", redactPath);
+ return false;
+ }
+
+ CDVDDemux *pDemuxer = NULL;
+
+ try
+ {
+ pDemuxer = CDVDFactoryDemuxer::CreateDemuxer(pInputStream, true);
+ if(!pDemuxer)
+ {
+ CLog::Log(LOGERROR, "{} - Error creating demuxer", __FUNCTION__);
+ return false;
+ }
+ }
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "{} - Exception thrown when opening demuxer", __FUNCTION__);
+ if (pDemuxer)
+ delete pDemuxer;
+
+ return false;
+ }
+
+ if (pStreamDetails)
+ {
+
+ const std::string& strPath = item.GetPath();
+ DemuxerToStreamDetails(pInputStream, pDemuxer, *pStreamDetails, strPath);
+
+ //extern subtitles
+ std::vector<std::string> filenames;
+ std::string video_path;
+ if (strPath.empty())
+ video_path = pInputStream->GetFileName();
+ else
+ video_path = strPath;
+
+ CUtil::ScanForExternalSubtitles(video_path, filenames);
+
+ for(unsigned int i=0;i<filenames.size();i++)
+ {
+ // if vobsub subtitle:
+ if (URIUtils::GetExtension(filenames[i]) == ".idx")
+ {
+ std::string strSubFile;
+ if ( CUtil::FindVobSubPair(filenames, filenames[i], strSubFile) )
+ AddExternalSubtitleToDetails(video_path, *pStreamDetails, filenames[i], strSubFile);
+ }
+ else
+ {
+ if ( !CUtil::IsVobSub(filenames, filenames[i]) )
+ {
+ AddExternalSubtitleToDetails(video_path, *pStreamDetails, filenames[i]);
+ }
+ }
+ }
+ }
+
+ int nVideoStream = -1;
+ int64_t demuxerId = -1;
+ for (CDemuxStream* pStream : pDemuxer->GetStreams())
+ {
+ if (pStream)
+ {
+ // ignore if it's a picture attachment (e.g. jpeg artwork)
+ if (pStream->type == STREAM_VIDEO && !(pStream->flags & AV_DISPOSITION_ATTACHED_PIC))
+ {
+ nVideoStream = pStream->uniqueId;
+ demuxerId = pStream->demuxerId;
+ }
+ else
+ pDemuxer->EnableStream(pStream->demuxerId, pStream->uniqueId, false);
+ }
+ }
+
+ bool bOk = false;
+ int packetsTried = 0;
+
+ if (nVideoStream != -1)
+ {
+ std::unique_ptr<CProcessInfo> pProcessInfo(CProcessInfo::CreateInstance());
+ std::vector<AVPixelFormat> pixFmts;
+ pixFmts.push_back(AV_PIX_FMT_YUV420P);
+ pProcessInfo->SetPixFormats(pixFmts);
+
+ CDVDStreamInfo hint(*pDemuxer->GetStream(demuxerId, nVideoStream), true);
+ hint.codecOptions = CODEC_FORCE_SOFTWARE;
+
+ std::unique_ptr<CDVDVideoCodec> pVideoCodec =
+ CDVDFactoryCodec::CreateVideoCodec(hint, *pProcessInfo);
+
+ if (pVideoCodec)
+ {
+ int nTotalLen = pDemuxer->GetStreamLength();
+ int64_t nSeekTo = (pos == -1) ? nTotalLen / 3 : pos;
+
+ CLog::Log(LOGDEBUG, "{} - seeking to pos {}ms (total: {}ms) in {}", __FUNCTION__, nSeekTo,
+ nTotalLen, redactPath);
+
+ if (pDemuxer->SeekTime(static_cast<double>(nSeekTo), true))
+ {
+ CDVDVideoCodec::VCReturn iDecoderState = CDVDVideoCodec::VC_NONE;
+ VideoPicture picture = {};
+
+ // num streams * 160 frames, should get a valid frame, if not abort.
+ int abort_index = pDemuxer->GetNrOfStreams() * 160;
+ do
+ {
+ DemuxPacket* pPacket = pDemuxer->Read();
+ packetsTried++;
+
+ if (!pPacket)
+ break;
+
+ if (pPacket->iStreamId != nVideoStream)
+ {
+ CDVDDemuxUtils::FreeDemuxPacket(pPacket);
+ continue;
+ }
+
+ pVideoCodec->AddData(*pPacket);
+ CDVDDemuxUtils::FreeDemuxPacket(pPacket);
+
+ iDecoderState = CDVDVideoCodec::VC_NONE;
+ while (iDecoderState == CDVDVideoCodec::VC_NONE)
+ {
+ iDecoderState = pVideoCodec->GetPicture(&picture);
+ }
+
+ if (iDecoderState == CDVDVideoCodec::VC_PICTURE)
+ {
+ if(!(picture.iFlags & DVP_FLAG_DROPPED))
+ break;
+ }
+
+ } while (abort_index--);
+
+ if (iDecoderState == CDVDVideoCodec::VC_PICTURE && !(picture.iFlags & DVP_FLAG_DROPPED))
+ {
+ {
+ unsigned int nWidth = std::min(picture.iDisplayWidth, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes);
+ double aspect = (double)picture.iDisplayWidth / (double)picture.iDisplayHeight;
+ if(hint.forced_aspect && hint.aspect != 0)
+ aspect = hint.aspect;
+ unsigned int nHeight = (unsigned int)((double)nWidth / aspect);
+
+ // We pass the buffers to sws_scale uses 16 aligned widths when using intrinsics
+ int sizeNeeded = FFALIGN(nWidth, 16) * nHeight * 4;
+ uint8_t *pOutBuf = static_cast<uint8_t*>(av_malloc(sizeNeeded));
+ struct SwsContext *context = sws_getContext(picture.iWidth, picture.iHeight,
+ AV_PIX_FMT_YUV420P, nWidth, nHeight, AV_PIX_FMT_BGRA, SWS_FAST_BILINEAR, NULL, NULL, NULL);
+
+ if (context)
+ {
+ uint8_t *planes[YuvImage::MAX_PLANES];
+ int stride[YuvImage::MAX_PLANES];
+ picture.videoBuffer->GetPlanes(planes);
+ picture.videoBuffer->GetStrides(stride);
+ uint8_t *src[4]= { planes[0], planes[1], planes[2], 0 };
+ int srcStride[] = { stride[0], stride[1], stride[2], 0 };
+ uint8_t *dst[] = { pOutBuf, 0, 0, 0 };
+ int dstStride[] = { (int)nWidth*4, 0, 0, 0 };
+ int orientation = DegreeToOrientation(hint.orientation);
+ sws_scale(context, src, srcStride, 0, picture.iHeight, dst, dstStride);
+ sws_freeContext(context);
+
+ details.width = nWidth;
+ details.height = nHeight;
+ CPicture::CacheTexture(pOutBuf, nWidth, nHeight, nWidth * 4, orientation, nWidth, nHeight, CTextureCache::GetCachedPath(details.file));
+ bOk = true;
+ }
+ av_free(pOutBuf);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - decode failed in {} after {} packets.", __FUNCTION__,
+ redactPath, packetsTried);
+ }
+ }
+ }
+ }
+
+ if (pDemuxer)
+ delete pDemuxer;
+
+ if(!bOk)
+ {
+ XFILE::CFile file;
+ if(file.OpenForWrite(CTextureCache::GetCachedPath(details.file)))
+ file.Close();
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "{} - measured {} ms to extract thumb from file <{}> in {} packets. ",
+ __FUNCTION__, duration.count(), redactPath, packetsTried);
+
+ return bOk;
+}
+
+/**
+ * \brief Open the item pointed to by pItem and extract streamdetails
+ * \return true if the stream details have changed
+ */
+bool CDVDFileInfo::GetFileStreamDetails(CFileItem *pItem)
+{
+ if (!pItem)
+ return false;
+
+ std::string strFileNameAndPath;
+ if (pItem->HasVideoInfoTag())
+ strFileNameAndPath = pItem->GetVideoInfoTag()->m_strFileNameAndPath;
+
+ if (strFileNameAndPath.empty())
+ strFileNameAndPath = pItem->GetDynPath();
+
+ std::string playablePath = strFileNameAndPath;
+ if (URIUtils::IsStack(playablePath))
+ playablePath = XFILE::CStackDirectory::GetFirstStackedFile(playablePath);
+
+ CFileItem item(playablePath, false);
+ item.SetMimeTypeForInternetFile();
+ auto pInputStream = CDVDFactoryInputStream::CreateInputStream(NULL, item);
+ if (!pInputStream)
+ return false;
+
+ if (pInputStream->IsStreamType(DVDSTREAM_TYPE_PVRMANAGER))
+ {
+ return false;
+ }
+
+ if (pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD) || !pInputStream->Open())
+ {
+ return false;
+ }
+
+ CDVDDemux *pDemuxer = CDVDFactoryDemuxer::CreateDemuxer(pInputStream, true);
+ if (pDemuxer)
+ {
+ bool retVal = DemuxerToStreamDetails(pInputStream, pDemuxer, pItem->GetVideoInfoTag()->m_streamDetails, strFileNameAndPath);
+ delete pDemuxer;
+ return retVal;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+bool CDVDFileInfo::DemuxerToStreamDetails(const std::shared_ptr<CDVDInputStream>& pInputStream,
+ CDVDDemux* pDemuxer,
+ const std::vector<CStreamDetailSubtitle>& subs,
+ CStreamDetails& details)
+{
+ bool result = DemuxerToStreamDetails(pInputStream, pDemuxer, details);
+ for (unsigned int i = 0; i < subs.size(); i++)
+ {
+ CStreamDetailSubtitle* sub = new CStreamDetailSubtitle();
+ sub->m_strLanguage = subs[i].m_strLanguage;
+ details.AddStream(sub);
+ result = true;
+ }
+ return result;
+}
+
+/* returns true if details have been added */
+bool CDVDFileInfo::DemuxerToStreamDetails(const std::shared_ptr<CDVDInputStream>& pInputStream,
+ CDVDDemux* pDemux,
+ CStreamDetails& details,
+ const std::string& path)
+{
+ bool retVal = false;
+ details.Reset();
+
+ const CURL pathToUrl(path);
+ for (CDemuxStream* stream : pDemux->GetStreams())
+ {
+ if (stream->type == STREAM_VIDEO && !(stream->flags & AV_DISPOSITION_ATTACHED_PIC))
+ {
+ CStreamDetailVideo *p = new CStreamDetailVideo();
+ CDemuxStreamVideo* vstream = static_cast<CDemuxStreamVideo*>(stream);
+ p->m_iWidth = vstream->iWidth;
+ p->m_iHeight = vstream->iHeight;
+ p->m_fAspect = static_cast<float>(vstream->fAspect);
+ if (p->m_fAspect == 0.0f && p->m_iHeight > 0)
+ p->m_fAspect = (float)p->m_iWidth / p->m_iHeight;
+ p->m_strCodec = pDemux->GetStreamCodecName(stream->demuxerId, stream->uniqueId);
+ p->m_iDuration = pDemux->GetStreamLength();
+ p->m_strStereoMode = vstream->stereo_mode;
+ p->m_strLanguage = vstream->language;
+ p->m_strHdrType = CStreamDetails::HdrTypeToString(vstream->hdr_type);
+
+ // stack handling
+ if (URIUtils::IsStack(path))
+ {
+ CFileItemList files;
+ XFILE::CStackDirectory stack;
+ stack.GetDirectory(pathToUrl, files);
+
+ // skip first path as we already know the duration
+ for (int i = 1; i < files.Size(); i++)
+ {
+ int duration = 0;
+ if (CDVDFileInfo::GetFileDuration(files[i]->GetDynPath(), duration))
+ p->m_iDuration = p->m_iDuration + duration;
+ }
+ }
+
+ // finally, calculate seconds
+ if (p->m_iDuration > 0)
+ p->m_iDuration = p->m_iDuration / 1000;
+
+ details.AddStream(p);
+ retVal = true;
+ }
+
+ else if (stream->type == STREAM_AUDIO)
+ {
+ CStreamDetailAudio *p = new CStreamDetailAudio();
+ p->m_iChannels = static_cast<CDemuxStreamAudio*>(stream)->iChannels;
+ p->m_strLanguage = stream->language;
+ p->m_strCodec = pDemux->GetStreamCodecName(stream->demuxerId, stream->uniqueId);
+ details.AddStream(p);
+ retVal = true;
+ }
+
+ else if (stream->type == STREAM_SUBTITLE)
+ {
+ CStreamDetailSubtitle *p = new CStreamDetailSubtitle();
+ p->m_strLanguage = stream->language;
+ details.AddStream(p);
+ retVal = true;
+ }
+ } /* for iStream */
+
+ details.DetermineBestStreams();
+#ifdef HAVE_LIBBLURAY
+ // correct bluray runtime. we need the duration from the input stream, not the demuxer.
+ if (pInputStream->IsStreamType(DVDSTREAM_TYPE_BLURAY))
+ {
+ if (std::static_pointer_cast<CDVDInputStreamBluray>(pInputStream)->GetTotalTime() > 0)
+ {
+ const CStreamDetailVideo* dVideo = static_cast<const CStreamDetailVideo*>(details.GetNthStream(CStreamDetail::VIDEO, 0));
+ CStreamDetailVideo* detailVideo = const_cast<CStreamDetailVideo*>(dVideo);
+ if (detailVideo)
+ detailVideo->m_iDuration = std::static_pointer_cast<CDVDInputStreamBluray>(pInputStream)->GetTotalTime() / 1000;
+ }
+ }
+#endif
+ return retVal;
+}
+
+bool CDVDFileInfo::AddExternalSubtitleToDetails(const std::string &path, CStreamDetails &details, const std::string& filename, const std::string& subfilename)
+{
+ std::string ext = URIUtils::GetExtension(filename);
+ std::string vobsubfile = subfilename;
+ if(ext == ".idx")
+ {
+ if (vobsubfile.empty())
+ vobsubfile = URIUtils::ReplaceExtension(filename, ".sub");
+
+ CDVDDemuxVobsub v;
+ if (!v.Open(filename, STREAM_SOURCE_NONE, vobsubfile))
+ return false;
+
+ for(CDemuxStream* stream : v.GetStreams())
+ {
+ CStreamDetailSubtitle *dsub = new CStreamDetailSubtitle();
+ std::string lang = stream->language;
+ dsub->m_strLanguage = g_LangCodeExpander.ConvertToISO6392B(lang);
+ details.AddStream(dsub);
+ }
+ return true;
+ }
+ if(ext == ".sub")
+ {
+ std::string strReplace(URIUtils::ReplaceExtension(filename,".idx"));
+ if (XFILE::CFile::Exists(strReplace))
+ return false;
+ }
+
+ CStreamDetailSubtitle *dsub = new CStreamDetailSubtitle();
+ ExternalStreamInfo info = CUtil::GetExternalStreamDetailsFromFilename(path, filename);
+ dsub->m_strLanguage = g_LangCodeExpander.ConvertToISO6392B(info.language);
+ details.AddStream(dsub);
+
+ return true;
+}
+
diff --git a/xbmc/cores/VideoPlayer/DVDFileInfo.h b/xbmc/cores/VideoPlayer/DVDFileInfo.h
new file mode 100644
index 0000000..bb0a7ab
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDFileInfo.h
@@ -0,0 +1,52 @@
+/*
+ * 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 <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CDVDDemux;
+class CStreamDetails;
+class CStreamDetailSubtitle;
+class CDVDInputStream;
+class CTextureDetails;
+
+class CDVDFileInfo
+{
+public:
+ // Extract a thumbnail image from the media referenced by fileItem, optionally populating a streamdetails class with the data
+ static bool ExtractThumb(const CFileItem& fileItem,
+ CTextureDetails &details,
+ CStreamDetails *pStreamDetails,
+ int64_t pos);
+
+ // Probe the files streams and store the info in the VideoInfoTag
+ static bool GetFileStreamDetails(CFileItem *pItem);
+ static bool DemuxerToStreamDetails(const std::shared_ptr<CDVDInputStream>& pInputStream,
+ CDVDDemux* pDemux,
+ CStreamDetails& details,
+ const std::string& path = "");
+
+ /** \brief Probe the file's internal and external streams and store the info in the StreamDetails parameter.
+ * \param[out] details The file's StreamDetails consisting of internal streams and external subtitle streams.
+ */
+ static bool DemuxerToStreamDetails(const std::shared_ptr<CDVDInputStream>& pInputStream,
+ CDVDDemux* pDemuxer,
+ const std::vector<CStreamDetailSubtitle>& subs,
+ CStreamDetails& details);
+
+ static bool GetFileDuration(const std::string &path, int &duration);
+
+ /** \brief Probe the streams of an external subtitle file and store the info in the StreamDetails parameter.
+ * \param[out] details The external subtitle file's StreamDetails.
+ */
+ static bool AddExternalSubtitleToDetails(const std::string &path, CStreamDetails &details, const std::string& filename, const std::string& subfilename = "");
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/BlurayStateSerializer.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/BlurayStateSerializer.cpp
new file mode 100644
index 0000000..0a810c7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/BlurayStateSerializer.cpp
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 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 "BlurayStateSerializer.h"
+
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <charconv>
+#include <cstring>
+#include <sstream>
+
+namespace
+{
+// Serializer version - used to avoid processing deprecated/legacy schemas
+constexpr int BLURAYSTATESERIALIZER_VERSION = 1;
+} // namespace
+
+bool CBlurayStateSerializer::BlurayStateToXML(std::string& xmlstate, const BlurayState& state)
+{
+ CXBMCTinyXML xmlDoc{"libbluraystate"};
+
+ TiXmlElement eRoot{"libbluraystate"};
+ eRoot.SetAttribute("version", BLURAYSTATESERIALIZER_VERSION);
+
+ TiXmlElement xmlElement{"playlistId"};
+ TiXmlText xmlElementValue = std::to_string(state.playlistId);
+ xmlElement.InsertEndChild(xmlElementValue);
+ eRoot.InsertEndChild(xmlElement);
+ xmlDoc.InsertEndChild(eRoot);
+
+ std::stringstream stream;
+ stream << xmlDoc;
+ xmlstate = stream.str();
+ return true;
+}
+
+bool CBlurayStateSerializer::XMLToBlurayState(BlurayState& state, const std::string& xmlstate)
+{
+ CXBMCTinyXML xmlDoc;
+
+ xmlDoc.Parse(xmlstate);
+ if (xmlDoc.Error())
+ return false;
+
+ TiXmlHandle hRoot(xmlDoc.RootElement());
+ if (!hRoot.Element() || !StringUtils::EqualsNoCase(hRoot.Element()->Value(), "libbluraystate"))
+ {
+ CLog::LogF(LOGERROR, "Failed to deserialize bluray state - failed to detect root element.");
+ return false;
+ }
+
+ auto version = hRoot.Element()->Attribute("version");
+ if (!version ||
+ !StringUtils::EqualsNoCase(version, std::to_string(BLURAYSTATESERIALIZER_VERSION)))
+ {
+ CLog::LogF(LOGERROR, "Failed to deserialize bluray state - incompatible serializer version.");
+ return false;
+ }
+
+ const TiXmlElement* childElement = hRoot.Element()->FirstChildElement();
+ while (childElement)
+ {
+ const std::string property = childElement->Value();
+ if (property == "playlistId")
+ {
+ std::from_chars(childElement->GetText(),
+ childElement->GetText() + std::strlen(childElement->GetText()),
+ state.playlistId);
+ }
+ else
+ {
+ CLog::LogF(LOGWARNING, "Unmapped bluray state property {}, ignored.", childElement->Value());
+ }
+ childElement = childElement->NextSiblingElement();
+ }
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/BlurayStateSerializer.h b/xbmc/cores/VideoPlayer/DVDInputStreams/BlurayStateSerializer.h
new file mode 100644
index 0000000..ca808c0
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/BlurayStateSerializer.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 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 <string>
+
+class TiXmlElement;
+
+/*! \brief Pod structure which represents the current Bluray state */
+struct BlurayState
+{
+ /*! The current playlist id */
+ int32_t playlistId = -1;
+};
+
+/*! \brief Auxiliar class to serialize/deserialize the Bluray state (into/from XML)
+*/
+class CBlurayStateSerializer
+{
+public:
+ /*! \brief Default constructor */
+ CBlurayStateSerializer() = default;
+
+ /*! \brief Default destructor */
+ ~CBlurayStateSerializer() = default;
+
+ /*! \brief Provided the state in xml format, fills a BlurayState struct representing the Bluray state and returns the
+ * success status of the operation
+ * \param[in,out] state the Bluray state struct to be filled
+ * \param xmlstate a string describing the Bluray state (XML)
+ * \return true if it was possible to fill the state struct based on the XML content, false otherwise
+ */
+ bool XMLToBlurayState(BlurayState& state, const std::string& xmlstate);
+
+ /*! \brief Provided the BlurayState struct of the current playing dvd, serializes the struct to XML
+ * \param[in,out] xmlstate a string describing the Bluray state (XML)
+ * \param state the Bluray state struct
+ * \return true if it was possible to serialize the struct into XML, false otherwise
+ */
+ bool BlurayStateToXML(std::string& xmlstate, const BlurayState& state);
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/CMakeLists.txt b/xbmc/cores/VideoPlayer/DVDInputStreams/CMakeLists.txt
new file mode 100644
index 0000000..576ddda
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/CMakeLists.txt
@@ -0,0 +1,38 @@
+set(SOURCES BlurayStateSerializer.cpp
+ DVDFactoryInputStream.cpp
+ DVDInputStream.cpp
+ DVDInputStreamFFmpeg.cpp
+ DVDInputStreamFile.cpp
+ DVDInputStreamMemory.cpp
+ DVDInputStreamNavigator.cpp
+ DVDInputStreamStack.cpp
+ DVDStateSerializer.cpp
+ InputStreamAddon.cpp
+ InputStreamMultiSource.cpp
+ InputStreamPVRBase.cpp
+ InputStreamPVRChannel.cpp
+ InputStreamPVRRecording.cpp)
+
+set(HEADERS BlurayStateSerializer.h
+ DVDFactoryInputStream.h
+ DVDInputStream.h
+ DVDInputStreamFFmpeg.h
+ DVDInputStreamFile.h
+ DVDInputStreamMemory.h
+ DVDInputStreamNavigator.h
+ DVDInputStreamStack.h
+ DVDStateSerializer.h
+ DllDvdNav.h
+ InputStreamAddon.h
+ InputStreamMultiStreams.h
+ InputStreamMultiSource.h
+ InputStreamPVRBase.h
+ InputStreamPVRChannel.h
+ InputStreamPVRRecording.h)
+
+if(BLURAY_FOUND)
+ list(APPEND SOURCES DVDInputStreamBluray.cpp)
+ list(APPEND HEADERS DVDInputStreamBluray.h)
+endif()
+
+core_add_library(dvdinputstreams)
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.cpp
new file mode 100644
index 0000000..97c0896
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.cpp
@@ -0,0 +1,189 @@
+/*
+ * 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 "DVDFactoryInputStream.h"
+
+#include "DVDInputStream.h"
+#ifdef HAVE_LIBBLURAY
+#include "DVDInputStreamBluray.h"
+#endif
+#include "DVDInputStreamFFmpeg.h"
+#include "DVDInputStreamFile.h"
+#include "DVDInputStreamNavigator.h"
+#include "DVDInputStreamStack.h"
+#include "FileItem.h"
+#include "InputStreamAddon.h"
+#include "InputStreamMultiSource.h"
+#include "InputStreamPVRChannel.h"
+#include "InputStreamPVRRecording.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "cores/VideoPlayer/Interface/InputStreamConstants.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/IFileTypes.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+
+
+std::shared_ptr<CDVDInputStream> CDVDFactoryInputStream::CreateInputStream(IVideoPlayer* pPlayer, const CFileItem &fileitem, bool scanforextaudio)
+{
+ using namespace ADDON;
+
+ const std::string& file = fileitem.GetDynPath();
+ if (scanforextaudio)
+ {
+ // find any available external audio tracks
+ std::vector<std::string> filenames;
+ filenames.push_back(file);
+ CUtil::ScanForExternalAudio(file, filenames);
+ if (filenames.size() >= 2)
+ {
+ return CreateInputStream(pPlayer, fileitem, filenames);
+ }
+ }
+
+ std::vector<AddonInfoPtr> addonInfos;
+ CServiceBroker::GetAddonMgr().GetAddonInfos(addonInfos, true /*enabled only*/,
+ AddonType::INPUTSTREAM);
+ for (const auto& addonInfo : addonInfos)
+ {
+ if (CInputStreamAddon::Supports(addonInfo, fileitem))
+ {
+ // Used to inform input stream about special identifier;
+ const std::string instanceId =
+ fileitem.GetProperty(STREAM_PROPERTY_INPUTSTREAM_INSTANCE_ID).asString();
+
+ return std::make_shared<CInputStreamAddon>(addonInfo, pPlayer, fileitem, instanceId);
+ }
+ }
+
+ if (fileitem.GetProperty(STREAM_PROPERTY_INPUTSTREAM).asString() ==
+ STREAM_PROPERTY_VALUE_INPUTSTREAMFFMPEG)
+ return std::shared_ptr<CDVDInputStreamFFmpeg>(new CDVDInputStreamFFmpeg(fileitem));
+
+ if (fileitem.IsDiscImage())
+ {
+#ifdef HAVE_LIBBLURAY
+ CURL url("udf://");
+ url.SetHostName(file);
+ url.SetFileName("BDMV/index.bdmv");
+ if (CFileUtils::Exists(url.Get()))
+ return std::shared_ptr<CDVDInputStreamBluray>(new CDVDInputStreamBluray(pPlayer, fileitem));
+ url.SetHostName(file);
+ url.SetFileName("BDMV/INDEX.BDM");
+ if (CFileUtils::Exists(url.Get()))
+ return std::shared_ptr<CDVDInputStreamBluray>(new CDVDInputStreamBluray(pPlayer, fileitem));
+#endif
+
+ return std::shared_ptr<CDVDInputStreamNavigator>(new CDVDInputStreamNavigator(pPlayer, fileitem));
+ }
+
+#ifdef HAS_DVD_DRIVE
+ if (file.compare(CServiceBroker::GetMediaManager().TranslateDevicePath("")) == 0)
+ {
+#ifdef HAVE_LIBBLURAY
+ if (CFileUtils::Exists(URIUtils::AddFileToFolder(file, "BDMV", "index.bdmv")) ||
+ CFileUtils::Exists(URIUtils::AddFileToFolder(file, "BDMV", "INDEX.BDM")))
+ return std::shared_ptr<CDVDInputStreamBluray>(new CDVDInputStreamBluray(pPlayer, fileitem));
+#endif
+
+ return std::shared_ptr<CDVDInputStreamNavigator>(new CDVDInputStreamNavigator(pPlayer, fileitem));
+ }
+#endif
+
+ if (fileitem.IsDVDFile(false, true))
+ return std::shared_ptr<CDVDInputStreamNavigator>(new CDVDInputStreamNavigator(pPlayer, fileitem));
+ else if (URIUtils::IsPVRChannel(file))
+ return std::shared_ptr<CInputStreamPVRChannel>(new CInputStreamPVRChannel(pPlayer, fileitem));
+ else if (URIUtils::IsPVRRecording(file))
+ return std::shared_ptr<CInputStreamPVRRecording>(new CInputStreamPVRRecording(pPlayer, fileitem));
+#ifdef HAVE_LIBBLURAY
+ else if (fileitem.IsType(".bdmv") || fileitem.IsType(".mpls")
+ || fileitem.IsType(".bdm") || fileitem.IsType(".mpl")
+ || StringUtils::StartsWithNoCase(file, "bluray:"))
+ return std::shared_ptr<CDVDInputStreamBluray>(new CDVDInputStreamBluray(pPlayer, fileitem));
+#endif
+ else if (StringUtils::StartsWithNoCase(file, "rtp://") ||
+ StringUtils::StartsWithNoCase(file, "rtsp://") ||
+ StringUtils::StartsWithNoCase(file, "rtsps://") ||
+ StringUtils::StartsWithNoCase(file, "satip://") ||
+ StringUtils::StartsWithNoCase(file, "sdp://") ||
+ StringUtils::StartsWithNoCase(file, "udp://") ||
+ StringUtils::StartsWithNoCase(file, "tcp://") ||
+ StringUtils::StartsWithNoCase(file, "mms://") ||
+ StringUtils::StartsWithNoCase(file, "mmst://") ||
+ StringUtils::StartsWithNoCase(file, "mmsh://") ||
+ StringUtils::StartsWithNoCase(file, "rtmp://") ||
+ StringUtils::StartsWithNoCase(file, "rtmpt://") ||
+ StringUtils::StartsWithNoCase(file, "rtmpe://") ||
+ StringUtils::StartsWithNoCase(file, "rtmpte://") ||
+ StringUtils::StartsWithNoCase(file, "rtmps://"))
+ {
+ return std::shared_ptr<CDVDInputStreamFFmpeg>(new CDVDInputStreamFFmpeg(fileitem));
+ }
+ else if(StringUtils::StartsWithNoCase(file, "stack://"))
+ return std::shared_ptr<CDVDInputStreamStack>(new CDVDInputStreamStack(fileitem));
+
+ CFileItem finalFileitem(fileitem);
+
+ if (finalFileitem.IsInternetStream())
+ {
+ if (finalFileitem.ContentLookup())
+ {
+ CURL origUrl(finalFileitem.GetDynURL());
+ XFILE::CCurlFile curlFile;
+ // try opening the url to resolve all redirects if any
+ try
+ {
+ if (curlFile.Open(finalFileitem.GetDynURL()))
+ {
+ CURL finalUrl(curlFile.GetURL());
+ finalUrl.SetProtocolOptions(origUrl.GetProtocolOptions());
+ finalUrl.SetUserName(origUrl.GetUserName());
+ finalUrl.SetPassword(origUrl.GetPassWord());
+ finalFileitem.SetDynPath(finalUrl.Get());
+ }
+ curlFile.Close();
+ }
+ catch (XFILE::CRedirectException *pRedirectEx)
+ {
+ if (pRedirectEx)
+ {
+ delete pRedirectEx->m_pNewFileImp;
+ delete pRedirectEx;
+ }
+ }
+ }
+
+ if (finalFileitem.IsType(".m3u8"))
+ return std::shared_ptr<CDVDInputStreamFFmpeg>(new CDVDInputStreamFFmpeg(finalFileitem));
+
+ // mime type for m3u8/hls streams
+ if (finalFileitem.GetMimeType() == "application/vnd.apple.mpegurl" ||
+ finalFileitem.GetMimeType() == "application/x-mpegURL")
+ return std::shared_ptr<CDVDInputStreamFFmpeg>(new CDVDInputStreamFFmpeg(finalFileitem));
+
+ if (URIUtils::IsProtocol(finalFileitem.GetPath(), "udp"))
+ return std::shared_ptr<CDVDInputStreamFFmpeg>(new CDVDInputStreamFFmpeg(finalFileitem));
+ }
+
+ // our file interface handles all these types of streams
+ return std::shared_ptr<CDVDInputStreamFile>(new CDVDInputStreamFile(finalFileitem,
+ XFILE::READ_TRUNCATED |
+ XFILE::READ_BITRATE |
+ XFILE::READ_CHUNKED));
+}
+
+std::shared_ptr<CDVDInputStream> CDVDFactoryInputStream::CreateInputStream(IVideoPlayer* pPlayer, const CFileItem &fileitem, const std::vector<std::string>& filenames)
+{
+ return std::shared_ptr<CInputStreamMultiSource>(new CInputStreamMultiSource(pPlayer, fileitem, filenames));
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.h
new file mode 100644
index 0000000..c2feb71
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.h
@@ -0,0 +1,24 @@
+/*
+ * 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 <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CDVDInputStream;
+class IVideoPlayer;
+
+class CDVDFactoryInputStream
+{
+public:
+ static std::shared_ptr<CDVDInputStream> CreateInputStream(IVideoPlayer* pPlayer, const CFileItem &fileitem, bool scanforextaudio = false);
+ static std::shared_ptr<CDVDInputStream> CreateInputStream(IVideoPlayer* pPlayer, const CFileItem &fileitem, const std::vector<std::string>& filenames);
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.cpp
new file mode 100644
index 0000000..fee72ac
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.cpp
@@ -0,0 +1,47 @@
+/*
+ * 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 "DVDInputStream.h"
+
+#include "URL.h"
+#include "cores/VideoPlayer/Interface/InputStreamConstants.h"
+
+CDVDInputStream::CDVDInputStream(DVDStreamType streamType, const CFileItem& fileitem)
+{
+ m_streamType = streamType;
+ m_contentLookup = true;
+ m_realtime = fileitem.GetProperty(STREAM_PROPERTY_ISREALTIMESTREAM).asBoolean(false);
+ m_item = fileitem;
+}
+
+CDVDInputStream::~CDVDInputStream() = default;
+
+bool CDVDInputStream::Open()
+{
+ m_content = m_item.GetMimeType();
+ m_contentLookup = m_item.ContentLookup();
+ return true;
+}
+
+void CDVDInputStream::Close()
+{
+
+}
+
+std::string CDVDInputStream::GetFileName()
+{
+ CURL url(m_item.GetDynPath());
+
+ url.SetProtocolOptions("");
+ return url.Get();
+}
+
+CURL CDVDInputStream::GetURL()
+{
+ return m_item.GetDynURL();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.h
new file mode 100644
index 0000000..4633757
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStream.h
@@ -0,0 +1,215 @@
+/*
+ * 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 "FileItem.h"
+#include "URL.h"
+#include "cores/MenuType.h"
+#include "filesystem/IFileTypes.h"
+#include "utils/BitstreamStats.h"
+#include "utils/Geometry.h"
+
+#include <string>
+#include <vector>
+
+enum DVDStreamType
+{
+ DVDSTREAM_TYPE_NONE = -1,
+ DVDSTREAM_TYPE_FILE = 1,
+ DVDSTREAM_TYPE_DVD = 2,
+ DVDSTREAM_TYPE_HTTP = 3,
+ DVDSTREAM_TYPE_MEMORY = 4,
+ DVDSTREAM_TYPE_FFMPEG = 5,
+ DVDSTREAM_TYPE_TV = 6,
+ DVDSTREAM_TYPE_MPLS = 10,
+ DVDSTREAM_TYPE_BLURAY = 11,
+ DVDSTREAM_TYPE_PVRMANAGER = 12,
+ DVDSTREAM_TYPE_MULTIFILES = 13,
+ DVDSTREAM_TYPE_ADDON = 14
+};
+
+#define SEEK_POSSIBLE 0x10 // flag used to check if protocol allows seeks
+
+#define DVDSTREAM_BLOCK_SIZE_FILE (2048 * 16)
+#define DVDSTREAM_BLOCK_SIZE_DVD 2048
+
+namespace XFILE
+{
+ class CFile;
+}
+
+struct DemuxPacket;
+class CDemuxStream;
+
+class CDVDInputStream
+{
+public:
+
+ class IDisplayTime
+ {
+ public:
+ virtual ~IDisplayTime() = default;
+ virtual int GetTotalTime() = 0;
+ virtual int GetTime() = 0;
+ };
+
+ class ITimes
+ {
+ public:
+ struct Times
+ {
+ time_t startTime;
+ double ptsStart;
+ double ptsBegin;
+ double ptsEnd;
+ };
+ virtual ~ITimes() = default;
+ virtual bool GetTimes(Times &times) = 0;
+ };
+
+ class IPosTime
+ {
+ public:
+ virtual ~IPosTime() = default;
+ virtual bool PosTime(int ms) = 0;
+ };
+
+ class IChapter
+ {
+ public:
+ virtual ~IChapter() = default;
+ virtual int GetChapter() = 0;
+ virtual int GetChapterCount() = 0;
+ virtual void GetChapterName(std::string& name, int ch=-1) = 0;
+ virtual int64_t GetChapterPos(int ch=-1) = 0;
+ virtual bool SeekChapter(int ch) = 0;
+ };
+
+ class IMenus
+ {
+ public:
+ virtual ~IMenus() = default;
+ virtual void ActivateButton() = 0;
+ virtual void SelectButton(int iButton) = 0;
+ virtual int GetCurrentButton() = 0;
+ virtual int GetTotalButtons() = 0;
+ virtual void OnUp() = 0;
+ virtual void OnDown() = 0;
+ virtual void OnLeft() = 0;
+ virtual void OnRight() = 0;
+
+ /*! \brief Open the Menu
+ * \return true if the menu is successfully opened, false otherwise
+ */
+ virtual bool OnMenu() = 0;
+ virtual void OnBack() = 0;
+ virtual void OnNext() = 0;
+ virtual void OnPrevious() = 0;
+ virtual bool OnMouseMove(const CPoint &point) = 0;
+ virtual bool OnMouseClick(const CPoint &point) = 0;
+
+ /*!
+ * \brief Get the supported menu type
+ * \return The supported menu type
+ */
+ virtual MenuType GetSupportedMenuType() = 0;
+
+ virtual bool IsInMenu() = 0;
+ virtual void SkipStill() = 0;
+ virtual double GetTimeStampCorrection() { return 0.0; }
+ virtual bool GetState(std::string &xmlstate) = 0;
+ virtual bool SetState(const std::string &xmlstate) = 0;
+ virtual bool CanSeek() { return !IsInMenu(); }
+ };
+
+ class IDemux
+ {
+ public:
+ virtual ~IDemux() = default;
+ virtual bool OpenDemux() = 0;
+ virtual DemuxPacket* ReadDemux() = 0;
+ virtual CDemuxStream* GetStream(int iStreamId) const = 0;
+ virtual std::vector<CDemuxStream*> GetStreams() const = 0;
+ virtual void EnableStream(int iStreamId, bool enable) {}
+ virtual bool OpenStream(int iStreamId) { return false; }
+ virtual int GetNrOfStreams() const = 0;
+ virtual void SetSpeed(int iSpeed) = 0;
+ virtual void FillBuffer(bool mode) {}
+ virtual bool SeekTime(double time, bool backward = false, double* startpts = NULL) = 0;
+ virtual void AbortDemux() = 0;
+ virtual void FlushDemux() = 0;
+ virtual void SetVideoResolution(unsigned int width,
+ unsigned int height,
+ unsigned int maxWidth,
+ unsigned int maxHeight)
+ {
+ }
+ };
+
+ enum ENextStream
+ {
+ NEXTSTREAM_NONE,
+ NEXTSTREAM_OPEN,
+ NEXTSTREAM_RETRY,
+ };
+
+ CDVDInputStream(DVDStreamType m_streamType, const CFileItem& fileitem);
+ virtual ~CDVDInputStream();
+ virtual bool Open();
+ virtual void Close();
+ virtual int Read(uint8_t* buf, int buf_size) = 0;
+ virtual int64_t Seek(int64_t offset, int whence) = 0;
+ virtual int64_t GetLength() = 0;
+ virtual std::string& GetContent() { return m_content; }
+ virtual std::string GetFileName();
+ virtual CURL GetURL();
+ virtual ENextStream NextStream() { return NEXTSTREAM_NONE; }
+ virtual void Abort() {}
+ virtual int GetBlockSize() { return 0; }
+ virtual bool CanSeek() { return true; } //! @todo drop this
+ virtual bool CanPause() { return false; }
+
+ /*! \brief Indicate expected read rate in bytes per second.
+ * This could be used to throttle caching rate. Should
+ * be seen as only a hint
+ */
+ virtual void SetReadRate(uint32_t rate) {}
+
+ /*! \brief Get the cache status
+ \return true when cache status was successfully obtained
+ */
+ virtual bool GetCacheStatus(XFILE::SCacheStatus *status) { return false; }
+
+ bool IsStreamType(DVDStreamType type) const { return m_streamType == type; }
+ virtual bool IsEOF() = 0;
+ virtual BitstreamStats GetBitstreamStats() const { return m_stats; }
+
+ bool ContentLookup() { return m_contentLookup; }
+
+ virtual bool IsRealtime() { return m_realtime; }
+
+ void SetRealtime(bool realtime) { m_realtime = realtime; }
+
+ // interfaces
+ virtual IDemux* GetIDemux() { return nullptr; }
+ virtual IPosTime* GetIPosTime() { return nullptr; }
+ virtual IDisplayTime* GetIDisplayTime() { return nullptr; }
+ virtual ITimes* GetITimes() { return nullptr; }
+ virtual IChapter* GetIChapter() { return nullptr; }
+
+ const CVariant& GetProperty(const std::string& key) { return m_item.GetProperty(key); }
+
+protected:
+ DVDStreamType m_streamType;
+ BitstreamStats m_stats;
+ std::string m_content;
+ CFileItem m_item;
+ bool m_contentLookup;
+ bool m_realtime;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.cpp
new file mode 100644
index 0000000..8a1f6bc
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.cpp
@@ -0,0 +1,1307 @@
+/*
+ * 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 "DVDInputStreamBluray.h"
+
+#include "DVDCodecs/Overlay/DVDOverlay.h"
+#include "DVDCodecs/Overlay/DVDOverlayImage.h"
+#include "IVideoPlayer.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "filesystem/BlurayCallback.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/DiscSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Geometry.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <functional>
+#include <limits>
+
+#include <libbluray/bluray.h>
+#include <libbluray/log_control.h>
+
+#define LIBBLURAY_BYTESEEK 0
+
+using namespace XFILE;
+
+using namespace std::chrono_literals;
+
+static int read_blocks(void* handle, void* buf, int lba, int num_blocks)
+{
+ int result = -1;
+ CDVDInputStreamFile* lpstream = reinterpret_cast<CDVDInputStreamFile*>(handle);
+ int64_t offset = static_cast<int64_t>(lba) * 2048;
+ if (lpstream->Seek(offset, SEEK_SET) >= 0)
+ {
+ int64_t size = static_cast<int64_t>(num_blocks) * 2048;
+ if (size <= std::numeric_limits<int>::max())
+ result = lpstream->Read(reinterpret_cast<uint8_t*>(buf), static_cast<int>(size)) / 2048;
+ }
+
+ return result;
+}
+
+static void bluray_overlay_cb(void *this_gen, const BD_OVERLAY * ov)
+{
+ static_cast<CDVDInputStreamBluray*>(this_gen)->OverlayCallback(ov);
+}
+
+#ifdef HAVE_LIBBLURAY_BDJ
+void bluray_overlay_argb_cb(void *this_gen, const struct bd_argb_overlay_s * const ov)
+{
+ static_cast<CDVDInputStreamBluray*>(this_gen)->OverlayCallbackARGB(ov);
+}
+#endif
+
+CDVDInputStreamBluray::CDVDInputStreamBluray(IVideoPlayer* player, const CFileItem& fileitem) :
+ CDVDInputStream(DVDSTREAM_TYPE_BLURAY, fileitem), m_player(player)
+{
+ m_content = "video/x-mpegts";
+ memset(&m_event, 0, sizeof(m_event));
+#ifdef HAVE_LIBBLURAY_BDJ
+ memset(&m_argb, 0, sizeof(m_argb));
+#endif
+}
+
+CDVDInputStreamBluray::~CDVDInputStreamBluray()
+{
+ Close();
+}
+
+void CDVDInputStreamBluray::Abort()
+{
+ m_hold = HOLD_EXIT;
+}
+
+bool CDVDInputStreamBluray::IsEOF()
+{
+ return false;
+}
+
+BLURAY_TITLE_INFO* CDVDInputStreamBluray::GetTitleLongest()
+{
+ int titles = bd_get_titles(m_bd, TITLES_RELEVANT, 0);
+
+ BLURAY_TITLE_INFO *s = nullptr;
+ for(int i=0; i < titles; i++)
+ {
+ BLURAY_TITLE_INFO *t = bd_get_title_info(m_bd, i, 0);
+ if(!t)
+ {
+ CLog::Log(LOGDEBUG, "get_main_title - unable to get title {}", i);
+ continue;
+ }
+ if(!s || s->duration < t->duration)
+ std::swap(s, t);
+
+ if(t)
+ bd_free_title_info(t);
+ }
+ return s;
+}
+
+BLURAY_TITLE_INFO* CDVDInputStreamBluray::GetTitleFile(const std::string& filename)
+{
+ unsigned int playlist;
+ if(sscanf(filename.c_str(), "%05u.mpls", &playlist) != 1)
+ {
+ CLog::Log(LOGERROR, "get_playlist_title - unsupported playlist file selected {}",
+ CURL::GetRedacted(filename));
+ return nullptr;
+ }
+
+ return bd_get_playlist_info(m_bd, playlist, 0);
+}
+
+
+bool CDVDInputStreamBluray::Open()
+{
+ if(m_player == nullptr)
+ return false;
+
+ std::string strPath(m_item.GetDynPath());
+ std::string filename;
+ std::string root;
+
+ bool openStream = false;
+ bool openDisc = false;
+ bool resumable = true;
+
+ // The item was selected via the simple menu
+ if (URIUtils::IsProtocol(strPath, "bluray"))
+ {
+ CURL url(strPath);
+ root = url.GetHostName();
+ filename = URIUtils::GetFileName(url.GetFileName());
+
+ // Check whether disc is AACS protected
+ CURL url3(root);
+ CFileItem base(url3, false);
+ openDisc = base.IsProtectedBlurayDisc();
+
+ // check for a menu call for an image file
+ if (StringUtils::EqualsNoCase(filename, "menu"))
+ {
+ //get rid of the udf:// protocol
+ CURL url2(root);
+ const std::string& root2 = url2.GetHostName();
+ CURL url(root2);
+ CFileItem item(url, false);
+ resumable = false;
+
+ // Check whether disc is AACS protected
+ if (!openDisc)
+ openDisc = item.IsProtectedBlurayDisc();
+
+ if (item.IsDiscImage())
+ {
+ if (!OpenStream(item))
+ return false;
+
+ openStream = true;
+ }
+ }
+ }
+ else if (m_item.IsDiscImage())
+ {
+ if (!OpenStream(m_item))
+ return false;
+
+ openStream = true;
+ }
+ else if (m_item.IsProtectedBlurayDisc())
+ {
+ openDisc = true;
+ }
+ else
+ {
+ strPath = URIUtils::GetDirectory(strPath);
+ URIUtils::RemoveSlashAtEnd(strPath);
+
+ if(URIUtils::GetFileName(strPath) == "PLAYLIST")
+ {
+ strPath = URIUtils::GetDirectory(strPath);
+ URIUtils::RemoveSlashAtEnd(strPath);
+ }
+
+ if(URIUtils::GetFileName(strPath) == "BDMV")
+ {
+ strPath = URIUtils::GetDirectory(strPath);
+ URIUtils::RemoveSlashAtEnd(strPath);
+ }
+ root = strPath;
+ filename = URIUtils::GetFileName(m_item.GetPath());
+ }
+
+ // root should not have trailing slash
+ URIUtils::RemoveSlashAtEnd(root);
+
+ bd_set_debug_handler(CBlurayCallback::bluray_logger);
+ bd_set_debug_mask(DBG_CRIT | DBG_BLURAY | DBG_NAV);
+
+ m_bd = bd_init();
+
+ if (!m_bd)
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - failed to initialize libbluray");
+ return false;
+ }
+
+ SetupPlayerSettings();
+
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - opening {}", CURL::GetRedacted(root));
+
+ if (openStream)
+ {
+ if (!bd_open_stream(m_bd, m_pstream.get(), read_blocks))
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - failed to open {} in stream mode",
+ CURL::GetRedacted(root));
+ return false;
+ }
+ }
+ else if (openDisc)
+ {
+ // This special case is required for opening original AACS protected Blu-ray discs. Otherwise
+ // things like Bus Encryption might not be handled properly and playback will fail.
+ m_rootPath = root;
+ if (!bd_open_disc(m_bd, root.c_str(), nullptr))
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - failed to open {} in disc mode",
+ CURL::GetRedacted(root));
+ return false;
+ }
+ }
+ else
+ {
+ m_rootPath = root;
+ if (!bd_open_files(m_bd, &m_rootPath, CBlurayCallback::dir_open, CBlurayCallback::file_open))
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - failed to open {} in files mode",
+ CURL::GetRedacted(root));
+ return false;
+ }
+ }
+
+ bd_get_event(m_bd, nullptr);
+
+ const BLURAY_DISC_INFO *disc_info = bd_get_disc_info(m_bd);
+
+ if (!disc_info)
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - bd_get_disc_info() failed");
+ return false;
+ }
+
+ if (disc_info->bluray_detected)
+ {
+#if (BLURAY_VERSION > BLURAY_VERSION_CODE(1,0,0))
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - Disc name : {}",
+ disc_info->disc_name ? disc_info->disc_name : "");
+#endif
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - First Play supported: {}",
+ disc_info->first_play_supported);
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - Top menu supported : {}",
+ disc_info->top_menu_supported);
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - HDMV titles : {}",
+ disc_info->num_hdmv_titles);
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - BD-J titles : {}",
+ disc_info->num_bdj_titles);
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - BD-J handled : {}",
+ disc_info->bdj_handled);
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - UNSUPPORTED titles : {}",
+ disc_info->num_unsupported_titles);
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - AACS detected : {}",
+ disc_info->aacs_detected);
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - libaacs detected : {}",
+ disc_info->libaacs_detected);
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - AACS handled : {}",
+ disc_info->aacs_handled);
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - BD+ detected : {}",
+ disc_info->bdplus_detected);
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - libbdplus detected : {}",
+ disc_info->libbdplus_detected);
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - BD+ handled : {}",
+ disc_info->bdplus_handled);
+#if (BLURAY_VERSION >= BLURAY_VERSION_CODE(1,0,0))
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::Open - no menus (libmmbd, or profile 6 bdj) : {}",
+ disc_info->no_menu_support);
+#endif
+ }
+ else
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - BluRay not detected");
+
+ if (disc_info->aacs_detected && !disc_info->aacs_handled)
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - Media stream scrambled/encrypted with AACS");
+ m_player->OnDiscNavResult(nullptr, BD_EVENT_ENC_ERROR);
+ return false;
+ }
+
+ if (disc_info->bdplus_detected && !disc_info->bdplus_handled)
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - Media stream scrambled/encrypted with BD+");
+ m_player->OnDiscNavResult(nullptr, BD_EVENT_ENC_ERROR);
+ return false;
+ }
+
+ int mode = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_DISC_PLAYBACK);
+
+ if (URIUtils::HasExtension(filename, ".mpls"))
+ {
+ m_navmode = false;
+ m_titleInfo = GetTitleFile(filename);
+ }
+ else if (mode == BD_PLAYBACK_MAIN_TITLE)
+ {
+ m_navmode = false;
+ m_titleInfo = GetTitleLongest();
+ }
+ else if (resumable && m_item.GetStartOffset() == STARTOFFSET_RESUME)
+ {
+ // resuming a bluray for which we have a saved state - the playlist will be open later on SetState
+ m_navmode = false;
+ return true;
+ }
+ else
+ {
+ m_navmode = true;
+ if (!disc_info->first_play_supported)
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - Can't play disc in HDMV navigation mode - First Play title not supported");
+ m_navmode = false;
+ }
+
+ if (m_navmode && disc_info->num_unsupported_titles > 0) {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - Unsupported titles found - Some titles can't be played in navigation mode");
+ }
+
+ if(!m_navmode)
+ m_titleInfo = GetTitleLongest();
+ }
+
+ if (m_navmode)
+ {
+
+ bd_register_overlay_proc (m_bd, this, bluray_overlay_cb);
+#ifdef HAVE_LIBBLURAY_BDJ
+ bd_register_argb_overlay_proc (m_bd, this, bluray_overlay_argb_cb, nullptr);
+#endif
+
+ if(bd_play(m_bd) <= 0)
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - failed play disk {}",
+ CURL::GetRedacted(strPath));
+ return false;
+ }
+ m_hold = HOLD_DATA;
+ }
+ else
+ {
+ if(!m_titleInfo)
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - failed to get title info");
+ return false;
+ }
+
+ if(!bd_select_playlist(m_bd, m_titleInfo->playlist))
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Open - failed to select playlist {}",
+ m_titleInfo->idx);
+ return false;
+ }
+ m_clip = nullptr;
+ }
+
+ // Process any events that occurred during opening
+ while (bd_get_event(m_bd, &m_event))
+ ProcessEvent();
+
+ return true;
+}
+
+// close file and reset everything
+void CDVDInputStreamBluray::Close()
+{
+ FreeTitleInfo();
+
+ if(m_bd)
+ {
+ bd_register_overlay_proc(m_bd, nullptr, nullptr);
+ bd_close(m_bd);
+ }
+
+ m_bd = nullptr;
+ m_pstream.reset();
+ m_rootPath.clear();
+}
+
+void CDVDInputStreamBluray::FreeTitleInfo()
+{
+ if (m_titleInfo)
+ bd_free_title_info(m_titleInfo);
+
+ m_titleInfo = nullptr;
+ m_clip = nullptr;
+}
+
+void CDVDInputStreamBluray::ProcessEvent() {
+
+ int pid = -1;
+ switch (m_event.event) {
+
+ /* errors */
+
+ case BD_EVENT_ERROR:
+ switch (m_event.param)
+ {
+ case BD_ERROR_HDMV:
+ case BD_ERROR_BDJ:
+ m_player->OnDiscNavResult(nullptr, BD_EVENT_MENU_ERROR);
+ break;
+ default:
+ break;
+ }
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray - BD_EVENT_ERROR: Fatal error. Playback can't be continued.");
+ m_hold = HOLD_ERROR;
+ break;
+
+ case BD_EVENT_READ_ERROR:
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray - BD_EVENT_READ_ERROR");
+ break;
+
+ case BD_EVENT_ENCRYPTED:
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray - BD_EVENT_ENCRYPTED");
+ switch (m_event.param)
+ {
+ case BD_ERROR_AACS:
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray - BD_ERROR_AACS");
+ break;
+ case BD_ERROR_BDPLUS:
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray - BD_ERROR_BDPLUS");
+ break;
+ default:
+ break;
+ }
+ m_hold = HOLD_ERROR;
+ m_player->OnDiscNavResult(nullptr, BD_EVENT_ENC_ERROR);
+ break;
+
+ /* playback control */
+
+ case BD_EVENT_SEEK:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_SEEK");
+ //m_player->OnDVDNavResult(nullptr, 1);
+ //bd_read_skip_still(m_bd);
+ //m_hold = HOLD_HELD;
+ break;
+
+ case BD_EVENT_STILL_TIME:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_STILL_TIME {}", m_event.param);
+ pid = m_event.param;
+ m_player->OnDiscNavResult(static_cast<void*>(&pid), BD_EVENT_STILL_TIME);
+ m_hold = HOLD_STILL;
+ break;
+
+ case BD_EVENT_STILL:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_STILL {}", m_event.param);
+
+ pid = m_event.param;
+
+ if (pid == 0)
+ m_player->OnDiscNavResult(static_cast<void*>(&pid), BD_EVENT_STILL);
+ break;
+
+ case BD_EVENT_DISCONTINUITY:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_DISCONTINUITY");
+ m_hold = HOLD_STILL;
+ break;
+
+ /* playback position */
+
+ case BD_EVENT_ANGLE:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_ANGLE {}", m_event.param);
+ m_angle = m_event.param;
+
+ if (m_playlist <= MAX_PLAYLIST_ID)
+ {
+ FreeTitleInfo();
+ m_titleInfo = bd_get_playlist_info(m_bd, m_playlist, m_angle);
+ }
+ break;
+
+ case BD_EVENT_END_OF_TITLE:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_END_OF_TITLE {}", m_event.param);
+ /* when a title ends, playlist WILL eventually change */
+ FreeTitleInfo();
+ break;
+
+ case BD_EVENT_TITLE:
+ {
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_TITLE {}", m_event.param);
+ const BLURAY_DISC_INFO* disc_info = bd_get_disc_info(m_bd);
+
+ m_menu = false;
+ m_isInMainMenu = false;
+
+ if (m_event.param == BLURAY_TITLE_TOP_MENU)
+ {
+ m_title = disc_info->top_menu;
+ m_menu = true;
+ m_isInMainMenu = true;
+ }
+ else if (m_event.param == BLURAY_TITLE_FIRST_PLAY)
+ m_title = disc_info->first_play;
+ else if (m_event.param <= disc_info->num_titles)
+ m_title = disc_info->titles[m_event.param];
+ else
+ m_title = nullptr;
+
+ break;
+ }
+ case BD_EVENT_PLAYLIST:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_PLAYLIST {}", m_event.param);
+ m_playlist = m_event.param;
+ FreeTitleInfo();
+ m_titleInfo = bd_get_playlist_info(m_bd, m_playlist, m_angle);
+ break;
+
+ case BD_EVENT_PLAYITEM:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_PLAYITEM {}", m_event.param);
+ if (m_titleInfo && m_event.param < m_titleInfo->clip_count)
+ m_clip = &m_titleInfo->clips[m_event.param];
+ break;
+
+ case BD_EVENT_CHAPTER:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_CHAPTER {}", m_event.param);
+ break;
+
+ /* stream selection */
+
+ case BD_EVENT_AUDIO_STREAM:
+ pid = -1;
+ if (m_titleInfo && m_clip && static_cast<uint32_t>(m_clip->audio_stream_count) > (m_event.param - 1))
+ pid = m_clip->audio_streams[m_event.param - 1].pid;
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_AUDIO_STREAM {} {}", m_event.param, pid);
+ m_player->OnDiscNavResult(static_cast<void*>(&pid), BD_EVENT_AUDIO_STREAM);
+ break;
+
+ case BD_EVENT_PG_TEXTST:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_PG_TEXTST {}", m_event.param);
+ pid = m_event.param;
+ m_player->OnDiscNavResult(static_cast<void*>(&pid), BD_EVENT_PG_TEXTST);
+ break;
+
+ case BD_EVENT_PG_TEXTST_STREAM:
+ pid = -1;
+ if (m_titleInfo && m_clip && static_cast<uint32_t>(m_clip->pg_stream_count) > (m_event.param - 1))
+ pid = m_clip->pg_streams[m_event.param - 1].pid;
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_PG_TEXTST_STREAM {}, {}", m_event.param,
+ pid);
+ m_player->OnDiscNavResult(static_cast<void*>(&pid), BD_EVENT_PG_TEXTST_STREAM);
+ break;
+
+ case BD_EVENT_MENU:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_MENU {}", m_event.param);
+ m_menu = (m_event.param != 0);
+ if (!m_menu)
+ m_isInMainMenu = false;
+ break;
+
+ case BD_EVENT_IDLE:
+ KODI::TIME::Sleep(100ms);
+ break;
+
+ case BD_EVENT_SOUND_EFFECT:
+ {
+ BLURAY_SOUND_EFFECT effect;
+ if (bd_get_sound_effect(m_bd, m_event.param, &effect) <= 0)
+ {
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_SOUND_EFFECT {} not valid",
+ m_event.param);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_SOUND_EFFECT {}", m_event.param);
+ }
+ }
+
+ case BD_EVENT_IG_STREAM:
+ case BD_EVENT_SECONDARY_AUDIO:
+ case BD_EVENT_SECONDARY_AUDIO_STREAM:
+ case BD_EVENT_SECONDARY_VIDEO:
+ case BD_EVENT_SECONDARY_VIDEO_SIZE:
+ case BD_EVENT_SECONDARY_VIDEO_STREAM:
+ case BD_EVENT_PLAYMARK:
+ case BD_EVENT_KEY_INTEREST_TABLE:
+ case BD_EVENT_UO_MASK_CHANGED:
+ break;
+
+ case BD_EVENT_PLAYLIST_STOP:
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray - BD_EVENT_PLAYLIST_STOP: flush buffers");
+ m_player->OnDiscNavResult(nullptr, BD_EVENT_PLAYLIST_STOP);
+ break;
+ case BD_EVENT_NONE:
+ break;
+
+ default:
+ CLog::Log(LOGWARNING, "CDVDInputStreamBluray - unhandled libbluray event {} [param {}]",
+ m_event.event, m_event.param);
+ break;
+ }
+
+ /* event has been consumed */
+ m_event.event = BD_EVENT_NONE;
+}
+
+int CDVDInputStreamBluray::Read(uint8_t* buf, int buf_size)
+{
+ int result = 0;
+ m_dispTimeBeforeRead = static_cast<int>((bd_tell_time(m_bd) / 90));
+ if(m_navmode)
+ {
+ do {
+
+ if (m_hold == HOLD_HELD)
+ return 0;
+
+ if( m_hold == HOLD_ERROR
+ || m_hold == HOLD_EXIT)
+ return -1;
+
+ result = bd_read_ext (m_bd, buf, buf_size, &m_event);
+
+ if(result < 0)
+ {
+ m_hold = HOLD_ERROR;
+ return result;
+ }
+
+ /* Check for holding events */
+ switch(m_event.event) {
+ case BD_EVENT_SEEK:
+ case BD_EVENT_TITLE:
+ case BD_EVENT_ANGLE:
+ case BD_EVENT_PLAYLIST:
+ case BD_EVENT_PLAYITEM:
+ if(m_hold != HOLD_DATA)
+ {
+ m_hold = HOLD_HELD;
+ return result;
+ }
+ break;
+
+ case BD_EVENT_STILL_TIME:
+ if(m_hold == HOLD_STILL)
+ m_event.event = 0; /* Consume duplicate still event */
+ else
+ m_hold = HOLD_HELD;
+ return result;
+
+ default:
+ break;
+ }
+
+ if(result > 0)
+ m_hold = HOLD_NONE;
+
+ ProcessEvent();
+
+ } while(result == 0);
+
+ }
+ else
+ {
+ result = bd_read(m_bd, buf, buf_size);
+ while (bd_get_event(m_bd, &m_event))
+ ProcessEvent();
+ }
+ return result;
+}
+
+static uint8_t clamp(double v)
+{
+ return (v) > 255.0 ? 255 : ((v) < 0.0 ? 0 : static_cast<uint32_t>((v + 0.5)));
+}
+
+static uint32_t build_rgba(const BD_PG_PALETTE_ENTRY &e)
+{
+ double r = 1.164 * (e.Y - 16) + 1.596 * (e.Cr - 128);
+ double g = 1.164 * (e.Y - 16) - 0.391 * (e.Cb - 128) - 0.813 * (e.Cr - 128);
+ double b = 1.164 * (e.Y - 16) + 2.018 * (e.Cb - 128);
+ return static_cast<uint32_t>(e.T) << PIXEL_ASHIFT
+ | static_cast<uint32_t>(clamp(r)) << PIXEL_RSHIFT
+ | static_cast<uint32_t>(clamp(g)) << PIXEL_GSHIFT
+ | static_cast<uint32_t>(clamp(b)) << PIXEL_BSHIFT;
+}
+
+void CDVDInputStreamBluray::OverlayClose()
+{
+#if(BD_OVERLAY_INTERFACE_VERSION >= 2)
+ for(SPlane& plane : m_planes)
+ plane.o.clear();
+ CDVDOverlayGroup* group = new CDVDOverlayGroup();
+ group->bForced = true;
+ m_player->OnDiscNavResult(static_cast<void*>(group), BD_EVENT_MENU_OVERLAY);
+ group->Release();
+ m_hasOverlay = false;
+#endif
+}
+
+void CDVDInputStreamBluray::OverlayInit(SPlane& plane, int w, int h)
+{
+#if(BD_OVERLAY_INTERFACE_VERSION >= 2)
+ plane.o.clear();
+ plane.w = w;
+ plane.h = h;
+#endif
+}
+
+void CDVDInputStreamBluray::OverlayClear(SPlane& plane, int x, int y, int w, int h)
+{
+#if(BD_OVERLAY_INTERFACE_VERSION >= 2)
+ CRectInt ovr(x
+ , y
+ , x + w
+ , y + h);
+
+ /* fixup existing overlays */
+ for(SOverlays::iterator it = plane.o.begin(); it != plane.o.end();)
+ {
+ CRectInt old((*it)->x
+ , (*it)->y
+ , (*it)->x + (*it)->width
+ , (*it)->y + (*it)->height);
+
+ std::vector<CRectInt> rem = old.SubtractRect(ovr);
+
+ /* if no overlap we are done */
+ if(rem.size() == 1 && !(rem[0] != old))
+ {
+ ++it;
+ continue;
+ }
+
+ SOverlays add;
+ for(std::vector<CRectInt>::iterator itr = rem.begin(); itr != rem.end(); ++itr)
+ {
+ SOverlay overlay(new CDVDOverlayImage(*(*it)
+ , itr->x1
+ , itr->y1
+ , itr->Width()
+ , itr->Height())
+ , [](CDVDOverlay* ov) { ov->Release(); });
+ add.push_back(overlay);
+ }
+
+ it = plane.o.erase(it);
+ plane.o.insert(it, add.begin(), add.end());
+ }
+#endif
+}
+
+void CDVDInputStreamBluray::OverlayFlush(int64_t pts)
+{
+#if(BD_OVERLAY_INTERFACE_VERSION >= 2)
+ CDVDOverlayGroup* group = new CDVDOverlayGroup();
+ group->bForced = true;
+ group->iPTSStartTime = static_cast<double>(pts);
+ group->iPTSStopTime = 0;
+
+ for(SPlane& plane : m_planes)
+ {
+ for(SOverlays::iterator it = plane.o.begin(); it != plane.o.end(); ++it)
+ group->m_overlays.push_back((*it)->Acquire());
+ }
+
+ m_player->OnDiscNavResult(static_cast<void*>(group), BD_EVENT_MENU_OVERLAY);
+ group->Release();
+ m_hasOverlay = true;
+#endif
+}
+
+void CDVDInputStreamBluray::OverlayCallback(const BD_OVERLAY * const ov)
+{
+#if(BD_OVERLAY_INTERFACE_VERSION >= 2)
+ if(ov == nullptr || ov->cmd == BD_OVERLAY_CLOSE)
+ {
+ OverlayClose();
+ return;
+ }
+
+ if (ov->plane > 1)
+ {
+ CLog::Log(LOGWARNING, "CDVDInputStreamBluray - Ignoring overlay with multiple planes");
+ return;
+ }
+
+ SPlane& plane(m_planes[ov->plane]);
+
+ if (ov->cmd == BD_OVERLAY_CLEAR)
+ {
+ plane.o.clear();
+ return;
+ }
+
+ if (ov->cmd == BD_OVERLAY_INIT)
+ {
+ OverlayInit(plane, ov->w, ov->h);
+ return;
+ }
+
+ if (ov->cmd == BD_OVERLAY_DRAW || ov->cmd == BD_OVERLAY_WIPE)
+ OverlayClear(plane, ov->x, ov->y, ov->w, ov->h);
+
+ /* uncompress and draw bitmap */
+ if (ov->img && ov->cmd == BD_OVERLAY_DRAW)
+ {
+ SOverlay overlay(new CDVDOverlayImage(), [](CDVDOverlay* ov) { ov->Release(); });
+
+ if (ov->palette)
+ {
+ overlay->palette.resize(256);
+
+ for(unsigned i = 0; i < 256; i++)
+ overlay->palette[i] = build_rgba(ov->palette[i]);
+ }
+ else
+ overlay->palette.clear();
+
+ const BD_PG_RLE_ELEM *rlep = ov->img;
+ size_t bytes = ov->w * ov->h;
+ overlay->pixels.resize(bytes);
+
+ for (size_t i = 0; i < bytes; i += rlep->len, rlep++)
+ memset(overlay->pixels.data() + i, rlep->color, rlep->len);
+
+ overlay->linesize = ov->w;
+ overlay->x = ov->x;
+ overlay->y = ov->y;
+ overlay->height = ov->h;
+ overlay->width = ov->w;
+ overlay->source_height = plane.h;
+ overlay->source_width = plane.w;
+ plane.o.push_back(overlay);
+ }
+
+ if (ov->cmd == BD_OVERLAY_FLUSH)
+ OverlayFlush(ov->pts);
+#endif
+}
+
+#ifdef HAVE_LIBBLURAY_BDJ
+void CDVDInputStreamBluray::OverlayCallbackARGB(const struct bd_argb_overlay_s * const ov)
+{
+ if(ov == nullptr || ov->cmd == BD_ARGB_OVERLAY_CLOSE)
+ {
+ OverlayClose();
+ return;
+ }
+
+ if (ov->plane > 1)
+ {
+ CLog::Log(LOGWARNING, "CDVDInputStreamBluray - Ignoring overlay with multiple planes");
+ return;
+ }
+
+ SPlane& plane(m_planes[ov->plane]);
+
+ if (ov->cmd == BD_ARGB_OVERLAY_INIT)
+ {
+ OverlayInit(plane, ov->w, ov->h);
+ return;
+ }
+
+ if (ov->cmd == BD_ARGB_OVERLAY_DRAW)
+ OverlayClear(plane, ov->x, ov->y, ov->w, ov->h);
+
+ /* uncompress and draw bitmap */
+ if (ov->argb && ov->cmd == BD_ARGB_OVERLAY_DRAW)
+ {
+ SOverlay overlay(new CDVDOverlayImage(), [](CDVDOverlay* ov) { CDVDOverlay::Release(ov); });
+
+ overlay->palette.clear();
+ size_t bytes = static_cast<size_t>(ov->stride * ov->h * 4);
+ overlay->pixels.resize(bytes);
+ memcpy(overlay->pixels.data(), ov->argb, bytes);
+
+ overlay->linesize = ov->stride * 4;
+ overlay->x = ov->x;
+ overlay->y = ov->y;
+ overlay->height = ov->h;
+ overlay->width = ov->w;
+ overlay->source_height = plane.h;
+ overlay->source_width = plane.w;
+ plane.o.push_back(overlay);
+ }
+
+ if(ov->cmd == BD_ARGB_OVERLAY_FLUSH)
+ OverlayFlush(ov->pts);
+}
+#endif
+
+
+int CDVDInputStreamBluray::GetTotalTime()
+{
+ if(m_titleInfo)
+ return static_cast<int>(m_titleInfo->duration / 90);
+ else
+ return 0;
+}
+
+int CDVDInputStreamBluray::GetTime()
+{
+ return m_dispTimeBeforeRead;
+}
+
+bool CDVDInputStreamBluray::PosTime(int ms)
+{
+ if(bd_seek_time(m_bd, ms * 90) < 0)
+ return false;
+
+ while (bd_get_event(m_bd, &m_event))
+ ProcessEvent();
+
+ return true;
+}
+
+int CDVDInputStreamBluray::GetChapterCount()
+{
+ if(m_titleInfo)
+ return static_cast<int>(m_titleInfo->chapter_count);
+ else
+ return 0;
+}
+
+int CDVDInputStreamBluray::GetChapter()
+{
+ if(m_titleInfo)
+ return static_cast<int>(bd_get_current_chapter(m_bd) + 1);
+ else
+ return 0;
+}
+
+bool CDVDInputStreamBluray::SeekChapter(int ch)
+{
+ if(m_titleInfo && bd_seek_chapter(m_bd, ch-1) < 0)
+ return false;
+
+ while (bd_get_event(m_bd, &m_event))
+ ProcessEvent();
+
+ return true;
+}
+
+int64_t CDVDInputStreamBluray::GetChapterPos(int ch)
+{
+ if (ch == -1 || ch > GetChapterCount())
+ ch = GetChapter();
+
+ if (m_titleInfo && m_titleInfo->chapters)
+ return m_titleInfo->chapters[ch - 1].start / 90000;
+ else
+ return 0;
+}
+
+int64_t CDVDInputStreamBluray::Seek(int64_t offset, int whence)
+{
+#if LIBBLURAY_BYTESEEK
+ if(whence == SEEK_POSSIBLE)
+ return 1;
+ else if(whence == SEEK_CUR)
+ {
+ if(offset == 0)
+ return bd_tell(m_bd);
+ else
+ offset += bd_tell(m_bd);
+ }
+ else if(whence == SEEK_END)
+ offset += bd_get_title_size(m_bd);
+ else if(whence != SEEK_SET)
+ return -1;
+
+ int64_t pos = bd_seek(m_bd, offset);
+ if(pos < 0)
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamBluray::Seek - seek to {}, failed with {}", offset, pos);
+ return -1;
+ }
+
+ if(pos != offset)
+ CLog::Log(LOGWARNING, "CDVDInputStreamBluray::Seek - seek to {}, ended at {}", offset, pos);
+
+ return offset;
+#else
+ if(whence == SEEK_POSSIBLE)
+ return 0;
+ return -1;
+#endif
+}
+
+int64_t CDVDInputStreamBluray::GetLength()
+{
+ return static_cast<int64_t>(bd_get_title_size(m_bd));
+}
+
+static bool find_stream(int pid, BLURAY_STREAM_INFO *info, int count, std::string &language)
+{
+ int i=0;
+ for(;i<count;i++,info++)
+ {
+ if(info->pid == static_cast<uint16_t>(pid))
+ break;
+ }
+ if(i==count)
+ return false;
+ language = reinterpret_cast<char*>(info->lang);
+ return true;
+}
+
+void CDVDInputStreamBluray::GetStreamInfo(int pid, std::string &language)
+{
+ if(!m_titleInfo || !m_clip)
+ return;
+
+ if (pid == HDMV_PID_VIDEO)
+ find_stream(pid, m_clip->video_streams, m_clip->video_stream_count, language);
+ else if (HDMV_PID_AUDIO_FIRST <= pid && pid <= HDMV_PID_AUDIO_LAST)
+ find_stream(pid, m_clip->audio_streams, m_clip->audio_stream_count, language);
+ else if (HDMV_PID_PG_FIRST <= pid && pid <= HDMV_PID_PG_LAST)
+ find_stream(pid, m_clip->pg_streams, m_clip->pg_stream_count, language);
+ else if (HDMV_PID_PG_HDR_FIRST <= pid && pid <= HDMV_PID_PG_HDR_LAST)
+ find_stream(pid, m_clip->pg_streams, m_clip->pg_stream_count, language);
+ else if (HDMV_PID_IG_FIRST <= pid && pid <= HDMV_PID_IG_LAST)
+ find_stream(pid, m_clip->ig_streams, m_clip->ig_stream_count, language);
+ else
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::GetStreamInfo - unhandled pid {}", pid);
+}
+
+CDVDInputStream::ENextStream CDVDInputStreamBluray::NextStream()
+{
+ if(!m_navmode || m_hold == HOLD_EXIT || m_hold == HOLD_ERROR)
+ return NEXTSTREAM_NONE;
+
+ /* process any current event */
+ ProcessEvent();
+
+ /* process all queued up events */
+ while(bd_get_event(m_bd, &m_event))
+ ProcessEvent();
+
+ if(m_hold == HOLD_STILL)
+ return NEXTSTREAM_RETRY;
+
+ m_hold = HOLD_DATA;
+ return NEXTSTREAM_OPEN;
+}
+
+void CDVDInputStreamBluray::UserInput(bd_vk_key_e vk)
+{
+ if(m_bd == nullptr || !m_navmode)
+ return;
+
+ int ret = bd_user_input(m_bd, -1, vk);
+ if (ret < 0)
+ {
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::UserInput - user input failed");
+ }
+ else
+ {
+ /* process all queued up events */
+ while (bd_get_event(m_bd, &m_event))
+ ProcessEvent();
+ }
+}
+
+bool CDVDInputStreamBluray::MouseMove(const CPoint &point)
+{
+ if (m_bd == nullptr || !m_navmode)
+ return false;
+
+ // Disable mouse selection for BD-J menus, since it's not implemented in libbluray as of version 1.0.2
+ if (m_title && m_title->bdj == 1)
+ return false;
+
+ if (bd_mouse_select(m_bd, -1, static_cast<uint16_t>(point.x), static_cast<uint16_t>(point.y)) < 0)
+ {
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::MouseMove - mouse select failed");
+ return false;
+ }
+
+ return true;
+}
+
+bool CDVDInputStreamBluray::MouseClick(const CPoint &point)
+{
+ if (m_bd == nullptr || !m_navmode)
+ return false;
+
+ // Disable mouse selection for BD-J menus, since it's not implemented in libbluray as of version 1.0.2
+ if (m_title && m_title->bdj == 1)
+ return false;
+
+ if (bd_mouse_select(m_bd, -1, static_cast<uint16_t>(point.x), static_cast<uint16_t>(point.y)) < 0)
+ {
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::MouseClick - mouse select failed");
+ return false;
+ }
+
+ if (bd_user_input(m_bd, -1, BD_VK_MOUSE_ACTIVATE) >= 0)
+ return true;
+
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::MouseClick - mouse click (user input) failed");
+ return false;
+}
+
+bool CDVDInputStreamBluray::OnMenu()
+{
+ if(m_bd == nullptr || !m_navmode)
+ {
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::OnMenu - navigation mode not enabled");
+ return false;
+ }
+
+ // we can not use this event to track a possible popup menu state since bd-j blu-rays can
+ // toggle the popup menu on their own without firing this event, and if they do this, our
+ // internal tracking state would be wrong. So just process and return.
+ if(bd_user_input(m_bd, -1, BD_VK_POPUP) >= 0)
+ {
+ return true;
+ }
+
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::OnMenu - popup failed, trying root");
+
+ if (bd_user_input(m_bd, -1, BD_VK_ROOT_MENU) >= 0)
+ {
+ return true;
+ }
+
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::OnMenu - root failed, trying explicit");
+ if (bd_menu_call(m_bd, -1) <= 0)
+ {
+ CLog::Log(LOGDEBUG, "CDVDInputStreamBluray::OnMenu - root failed");
+ return false;
+ }
+ return true;
+}
+
+bool CDVDInputStreamBluray::IsInMenu()
+{
+ if(m_bd == nullptr || !m_navmode)
+ return false;
+
+ // since there is no way to tell in a BD-J blu-ray when a popup menu actually is visible,
+ // we have to assume that the blu-ray is in menu/navigation mode when there is an overlay
+ // on screen, even if it might be invisible (which is impossible to detect)
+ if(m_menu || m_hasOverlay)
+ return true;
+ return false;
+}
+
+void CDVDInputStreamBluray::SkipStill()
+{
+ if(m_bd == nullptr || !m_navmode)
+ return;
+
+ if ( m_hold == HOLD_STILL)
+ {
+ m_hold = HOLD_HELD;
+ bd_read_skip_still(m_bd);
+
+ /* process all queued up events */
+ while (bd_get_event(m_bd, &m_event))
+ ProcessEvent();
+ }
+}
+
+bool CDVDInputStreamBluray::CanSeek()
+{
+ return !IsInMenu() || !m_isInMainMenu;
+}
+
+MenuType CDVDInputStreamBluray::GetSupportedMenuType()
+{
+ if (m_navmode)
+ {
+ return MenuType::NATIVE;
+ }
+ return MenuType::NONE;
+}
+
+void CDVDInputStreamBluray::SetupPlayerSettings()
+{
+ int region = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_BLURAY_PLAYERREGION);
+ if ( region != BLURAY_REGION_A
+ && region != BLURAY_REGION_B
+ && region != BLURAY_REGION_C)
+ {
+ CLog::Log(LOGWARNING, "CDVDInputStreamBluray::Open - Blu-ray region must be set in setting, assuming region A");
+ region = BLURAY_REGION_A;
+ }
+ bd_set_player_setting(m_bd, BLURAY_PLAYER_SETTING_REGION_CODE, static_cast<uint32_t>(region));
+ bd_set_player_setting(m_bd, BLURAY_PLAYER_SETTING_PARENTAL, 99);
+ bd_set_player_setting(m_bd, BLURAY_PLAYER_SETTING_3D_CAP, 0xffffffff);
+#if (BLURAY_VERSION >= BLURAY_VERSION_CODE(1, 0, 2))
+ bd_set_player_setting(m_bd, BLURAY_PLAYER_SETTING_PLAYER_PROFILE, BLURAY_PLAYER_PROFILE_6_v3_1);
+ bd_set_player_setting(m_bd, BLURAY_PLAYER_SETTING_UHD_CAP, 0xffffffff);
+ bd_set_player_setting(m_bd, BLURAY_PLAYER_SETTING_UHD_DISPLAY_CAP, 0xffffffff);
+ bd_set_player_setting(m_bd, BLURAY_PLAYER_SETTING_HDR_PREFERENCE, 0xffffffff);
+#else
+ bd_set_player_setting(m_bd, BLURAY_PLAYER_SETTING_PLAYER_PROFILE, BLURAY_PLAYER_PROFILE_5_v2_4);
+#endif
+
+ std::string langCode;
+ g_LangCodeExpander.ConvertToISO6392T(g_langInfo.GetDVDAudioLanguage(), langCode);
+ bd_set_player_setting_str(m_bd, BLURAY_PLAYER_SETTING_AUDIO_LANG, langCode.c_str());
+
+ g_LangCodeExpander.ConvertToISO6392T(g_langInfo.GetDVDSubtitleLanguage(), langCode);
+ bd_set_player_setting_str(m_bd, BLURAY_PLAYER_SETTING_PG_LANG, langCode.c_str());
+
+ g_LangCodeExpander.ConvertToISO6392T(g_langInfo.GetDVDMenuLanguage(), langCode);
+ bd_set_player_setting_str(m_bd, BLURAY_PLAYER_SETTING_MENU_LANG, langCode.c_str());
+
+ g_LangCodeExpander.ConvertToISO6391(g_langInfo.GetRegionLocale(), langCode);
+ bd_set_player_setting_str(m_bd, BLURAY_PLAYER_SETTING_COUNTRY_CODE, langCode.c_str());
+
+#ifdef HAVE_LIBBLURAY_BDJ
+ std::string cacheDir = CSpecialProtocol::TranslatePath("special://userdata/cache/bluray/cache");
+ std::string persistentDir = CSpecialProtocol::TranslatePath("special://userdata/cache/bluray/persistent");
+ bd_set_player_setting_str(m_bd, BLURAY_PLAYER_PERSISTENT_ROOT, persistentDir.c_str());
+ bd_set_player_setting_str(m_bd, BLURAY_PLAYER_CACHE_ROOT, cacheDir.c_str());
+#endif
+}
+
+bool CDVDInputStreamBluray::OpenStream(CFileItem &item)
+{
+ m_pstream.reset(new CDVDInputStreamFile(item, 0));
+
+ if (!m_pstream->Open())
+ {
+ CLog::Log(LOGERROR, "Error opening image file {}", CURL::GetRedacted(item.GetPath()));
+ Close();
+ return false;
+ }
+
+ return true;
+}
+
+bool CDVDInputStreamBluray::GetState(std::string& xmlstate)
+{
+ if (!m_bd || !m_titleInfo)
+ {
+ return false;
+ }
+
+ BlurayState blurayState;
+ blurayState.playlistId = m_titleInfo->playlist;
+
+ if (!m_blurayStateSerializer.BlurayStateToXML(xmlstate, blurayState))
+ {
+ CLog::LogF(LOGWARNING, "Failed to serialize Bluray state");
+ return false;
+ }
+
+ return true;
+}
+
+bool CDVDInputStreamBluray::SetState(const std::string& xmlstate)
+{
+ if (!m_bd)
+ return false;
+
+ BlurayState blurayState;
+ if (!m_blurayStateSerializer.XMLToBlurayState(blurayState, xmlstate))
+ {
+ CLog::LogF(LOGWARNING, "Failed to deserialize Bluray state");
+ return false;
+ }
+
+ m_titleInfo = bd_get_playlist_info(m_bd, blurayState.playlistId, 0);
+ if (!m_titleInfo)
+ {
+ CLog::LogF(LOGERROR, "Open - failed to get title info");
+ return false;
+ }
+
+ if (!bd_select_playlist(m_bd, m_titleInfo->playlist))
+ {
+ CLog::LogF(LOGERROR, "Open - failed to select playlist {}", m_titleInfo->idx);
+ return false;
+ }
+
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.h
new file mode 100644
index 0000000..05af77e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamBluray.h
@@ -0,0 +1,192 @@
+/*
+ * 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 "BlurayStateSerializer.h"
+#include "DVDInputStream.h"
+
+#include <list>
+#include <memory>
+
+extern "C"
+{
+#include <libbluray/bluray.h>
+#include <libbluray/bluray-version.h>
+#include <libbluray/keys.h>
+#include <libbluray/overlay.h>
+#include <libbluray/player_settings.h>
+#include "DVDInputStreamFile.h"
+}
+
+#define MAX_PLAYLIST_ID 99999
+#define MAX_CLIP_ID 99999
+#define BD_EVENT_MENU_OVERLAY -1
+#define BD_EVENT_MENU_ERROR -2
+#define BD_EVENT_ENC_ERROR -3
+
+#define HDMV_PID_VIDEO 0x1011
+#define HDMV_PID_AUDIO_FIRST 0x1100
+#define HDMV_PID_AUDIO_LAST 0x111f
+#define HDMV_PID_PG_FIRST 0x1200
+#define HDMV_PID_PG_LAST 0x121f
+#define HDMV_PID_PG_HDR_FIRST 0x12a0
+#define HDMV_PID_PG_HDR_LAST 0x12bf
+#define HDMV_PID_IG_FIRST 0x1400
+#define HDMV_PID_IG_LAST 0x141f
+
+class CDVDOverlayImage;
+class IVideoPlayer;
+
+class CDVDInputStreamBluray
+ : public CDVDInputStream
+ , public CDVDInputStream::IDisplayTime
+ , public CDVDInputStream::IChapter
+ , public CDVDInputStream::IPosTime
+ , public CDVDInputStream::IMenus
+{
+public:
+ CDVDInputStreamBluray() = delete;
+ CDVDInputStreamBluray(IVideoPlayer* player, const CFileItem& fileitem);
+ ~CDVDInputStreamBluray() override;
+ bool Open() override;
+ void Close() override;
+ int Read(uint8_t* buf, int buf_size) override;
+ int64_t Seek(int64_t offset, int whence) override;
+ void Abort() override;
+ bool IsEOF() override;
+ int64_t GetLength() override;
+ int GetBlockSize() override { return 6144; }
+ ENextStream NextStream() override;
+
+
+ /* IMenus */
+ void ActivateButton() override { UserInput(BD_VK_ENTER); }
+ void SelectButton(int iButton) override
+ {
+ if(iButton < 10)
+ UserInput((bd_vk_key_e)(BD_VK_0 + iButton));
+ }
+ int GetCurrentButton() override { return 0; }
+ int GetTotalButtons() override { return 0; }
+ void OnUp() override { UserInput(BD_VK_UP); }
+ void OnDown() override { UserInput(BD_VK_DOWN); }
+ void OnLeft() override { UserInput(BD_VK_LEFT); }
+ void OnRight() override { UserInput(BD_VK_RIGHT); }
+
+ /*! \brief Open the Menu
+ * \return true if the menu is successfully opened, false otherwise
+ */
+ bool OnMenu() override;
+ void OnBack() override
+ {
+ if(IsInMenu())
+ OnMenu();
+ }
+ void OnNext() override {}
+ void OnPrevious() override {}
+
+ /*!
+ * \brief Get the supported menu type
+ * \return The supported menu type
+ */
+ MenuType GetSupportedMenuType() override;
+
+ bool IsInMenu() override;
+ bool OnMouseMove(const CPoint &point) override { return MouseMove(point); }
+ bool OnMouseClick(const CPoint &point) override { return MouseClick(point); }
+ void SkipStill() override;
+ bool GetState(std::string& xmlstate) override;
+ bool SetState(const std::string& xmlstate) override;
+ bool CanSeek() override;
+
+
+ void UserInput(bd_vk_key_e vk);
+ bool MouseMove(const CPoint &point);
+ bool MouseClick(const CPoint &point);
+
+ int GetChapter() override;
+ int GetChapterCount() override;
+ void GetChapterName(std::string& name, int ch=-1) override {};
+ int64_t GetChapterPos(int ch) override;
+ bool SeekChapter(int ch) override;
+
+ CDVDInputStream::IDisplayTime* GetIDisplayTime() override { return this; }
+ int GetTotalTime() override;
+ int GetTime() override;
+
+ CDVDInputStream::IPosTime* GetIPosTime() override { return this; }
+ bool PosTime(int ms) override;
+
+ void GetStreamInfo(int pid, std::string &language);
+
+ void OverlayCallback(const BD_OVERLAY * const);
+#ifdef HAVE_LIBBLURAY_BDJ
+ void OverlayCallbackARGB(const struct bd_argb_overlay_s * const);
+#endif
+
+ BLURAY_TITLE_INFO* GetTitleLongest();
+ BLURAY_TITLE_INFO* GetTitleFile(const std::string& name);
+
+ void ProcessEvent();
+
+protected:
+ struct SPlane;
+
+ void OverlayFlush(int64_t pts);
+ void OverlayClose();
+ static void OverlayClear(SPlane& plane, int x, int y, int w, int h);
+ static void OverlayInit (SPlane& plane, int w, int h);
+
+ IVideoPlayer* m_player = nullptr;
+ BLURAY* m_bd = nullptr;
+ const BLURAY_TITLE* m_title = nullptr;
+ BLURAY_TITLE_INFO* m_titleInfo = nullptr;
+ uint32_t m_playlist = MAX_PLAYLIST_ID + 1;
+ BLURAY_CLIP_INFO* m_clip = nullptr;
+ uint32_t m_angle = 0;
+ bool m_menu = false;
+ bool m_isInMainMenu = false;
+ bool m_hasOverlay = false;
+ bool m_navmode = false;
+ int m_dispTimeBeforeRead = 0;
+
+ typedef std::shared_ptr<CDVDOverlayImage> SOverlay;
+ typedef std::list<SOverlay> SOverlays;
+
+ struct SPlane
+ {
+ SOverlays o;
+ int w = 0;
+ int h = 0;
+ };
+
+ SPlane m_planes[2];
+ enum EHoldState {
+ HOLD_NONE = 0,
+ HOLD_HELD,
+ HOLD_DATA,
+ HOLD_STILL,
+ HOLD_ERROR,
+ HOLD_EXIT
+ } m_hold = HOLD_NONE;
+ BD_EVENT m_event;
+#ifdef HAVE_LIBBLURAY_BDJ
+ struct bd_argb_buffer_s m_argb;
+#endif
+
+ private:
+ bool OpenStream(CFileItem &item);
+ void SetupPlayerSettings();
+ void FreeTitleInfo();
+ std::unique_ptr<CDVDInputStreamFile> m_pstream = nullptr;
+ std::string m_rootPath;
+
+ /*! Bluray state serializer handler */
+ CBlurayStateSerializer m_blurayStateSerializer;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFFmpeg.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFFmpeg.cpp
new file mode 100644
index 0000000..c8baf1b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFFmpeg.cpp
@@ -0,0 +1,127 @@
+/*
+ * 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 "DVDInputStreamFFmpeg.h"
+
+#include "playlists/PlayListM3U.h"
+#include "utils/StringUtils.h"
+
+
+using namespace XFILE;
+
+CDVDInputStreamFFmpeg::CDVDInputStreamFFmpeg(const CFileItem& fileitem)
+ : CDVDInputStream(DVDSTREAM_TYPE_FFMPEG, fileitem)
+{
+
+}
+
+CDVDInputStreamFFmpeg::~CDVDInputStreamFFmpeg()
+{
+ Close();
+}
+
+bool CDVDInputStreamFFmpeg::IsEOF()
+{
+ if(m_aborted)
+ return true;
+ else
+ return false;
+}
+
+bool CDVDInputStreamFFmpeg::Open()
+{
+ if (!CDVDInputStream::Open())
+ return false;
+
+ m_aborted = false;
+
+ if (StringUtils::CompareNoCase(m_item.GetDynPath(), "udp://", 6) == 0 ||
+ StringUtils::CompareNoCase(m_item.GetDynPath(), "rtp://", 6) == 0)
+ {
+ m_realtime = true;
+ }
+
+ return true;
+}
+
+// close file and reset everything
+void CDVDInputStreamFFmpeg::Close()
+{
+ CDVDInputStream::Close();
+}
+
+int CDVDInputStreamFFmpeg::Read(uint8_t* buf, int buf_size)
+{
+ return -1;
+}
+
+int64_t CDVDInputStreamFFmpeg::GetLength()
+{
+ return 0;
+}
+
+int64_t CDVDInputStreamFFmpeg::Seek(int64_t offset, int whence)
+{
+ return -1;
+}
+
+std::string CDVDInputStreamFFmpeg::GetProxyType() const
+{
+ return m_item.HasProperty("proxy.type") ?
+ m_item.GetProperty("proxy.type").asString() : std::string();
+}
+
+std::string CDVDInputStreamFFmpeg::GetProxyHost() const
+{
+ return m_item.HasProperty("proxy.host") ?
+ m_item.GetProperty("proxy.host").asString() : std::string();
+}
+
+uint16_t CDVDInputStreamFFmpeg::GetProxyPort() const
+{
+ if (m_item.HasProperty("proxy.port"))
+ return static_cast<uint16_t>(m_item.GetProperty("proxy.port").asInteger());
+
+ // Select the standard port
+ const std::string value = GetProxyType();
+ if (value == "socks4" || value == "socks4a" ||
+ value == "socks5" || value == "socks5-remote")
+ return 1080;
+ else
+ return 3128;
+}
+
+std::string CDVDInputStreamFFmpeg::GetProxyUser() const
+{
+ return m_item.HasProperty("proxy.user") ?
+ m_item.GetProperty("proxy.user").asString() : std::string();
+}
+
+std::string CDVDInputStreamFFmpeg::GetProxyPassword() const
+{
+ return m_item.HasProperty("proxy.password") ?
+ m_item.GetProperty("proxy.password").asString() : std::string();
+}
+
+std::string CDVDInputStreamFFmpeg::GetFileName()
+{
+ CURL url = GetURL();
+ // rtmp options
+ if (url.IsProtocol("rtmp") || url.IsProtocol("rtmpt") ||
+ url.IsProtocol("rtmpe") || url.IsProtocol("rtmpte") ||
+ url.IsProtocol("rtmps"))
+ {
+ std::vector<std::string> opts = StringUtils::Split(url.Get(), " ");
+ if (opts.size() > 0)
+ {
+ return opts.front();
+ }
+ return url.Get();
+ }
+ return CDVDInputStream::GetFileName();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFFmpeg.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFFmpeg.h
new file mode 100644
index 0000000..a3ea4d7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFFmpeg.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "DVDInputStream.h"
+
+class CDVDInputStreamFFmpeg
+ : public CDVDInputStream
+{
+public:
+ explicit CDVDInputStreamFFmpeg(const CFileItem& fileitem);
+ ~CDVDInputStreamFFmpeg() override;
+ bool Open() override;
+ void Close() override;
+ int Read(uint8_t* buf, int buf_size) override;
+ int64_t Seek(int64_t offset, int whence) override;
+ bool IsEOF() override;
+ int64_t GetLength() override;
+ std::string GetFileName() override;
+
+ void Abort() override { m_aborted = true; }
+ bool Aborted() { return m_aborted; }
+
+ const CFileItem& GetItem() const { return m_item; }
+
+ std::string GetProxyType() const;
+ std::string GetProxyHost() const;
+ uint16_t GetProxyPort() const;
+ std::string GetProxyUser() const;
+ std::string GetProxyPassword() const;
+
+protected:
+ bool m_aborted = false;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFile.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFile.cpp
new file mode 100644
index 0000000..bebc683
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFile.cpp
@@ -0,0 +1,162 @@
+/*
+ * 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 "DVDInputStreamFile.h"
+
+#include "filesystem/File.h"
+#include "filesystem/IFile.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+CDVDInputStreamFile::CDVDInputStreamFile(const CFileItem& fileitem, unsigned int flags)
+ : CDVDInputStream(DVDSTREAM_TYPE_FILE, fileitem), m_flags(flags)
+{
+ m_pFile = NULL;
+ m_eof = true;
+}
+
+CDVDInputStreamFile::~CDVDInputStreamFile()
+{
+ Close();
+}
+
+bool CDVDInputStreamFile::IsEOF()
+{
+ return !m_pFile || m_eof;
+}
+
+bool CDVDInputStreamFile::Open()
+{
+ if (!CDVDInputStream::Open())
+ return false;
+
+ m_pFile = new CFile();
+ if (!m_pFile)
+ return false;
+
+ unsigned int flags = m_flags;
+
+ // If this file is audio and/or video (= not a subtitle) flag to caller
+ if (!m_item.IsSubtitle())
+ flags |= READ_AUDIO_VIDEO;
+
+ std::string content = m_item.GetMimeType();
+
+ if (content == "video/mp4" ||
+ content == "video/x-msvideo" ||
+ content == "video/avi" ||
+ content == "video/x-matroska" ||
+ content == "video/x-matroska-3d")
+ flags |= READ_MULTI_STREAM;
+
+ // open file in binary mode
+ if (!m_pFile->Open(m_item.GetDynPath(), flags))
+ {
+ delete m_pFile;
+ m_pFile = NULL;
+ return false;
+ }
+
+ if (m_pFile->GetImplementation() && (content.empty() || content == "application/octet-stream"))
+ m_content = m_pFile->GetImplementation()->GetProperty(XFILE::FILE_PROPERTY_CONTENT_TYPE);
+
+ m_eof = false;
+ return true;
+}
+
+// close file and reset everything
+void CDVDInputStreamFile::Close()
+{
+ if (m_pFile)
+ {
+ m_pFile->Close();
+ delete m_pFile;
+ }
+
+ CDVDInputStream::Close();
+ m_pFile = NULL;
+ m_eof = true;
+}
+
+int CDVDInputStreamFile::Read(uint8_t* buf, int buf_size)
+{
+ if(!m_pFile) return -1;
+
+ ssize_t ret = m_pFile->Read(buf, buf_size);
+
+ if (ret < 0)
+ return -1; // player will retry read in case of error until playback is stopped
+
+ /* we currently don't support non completing reads */
+ if (ret == 0)
+ m_eof = true;
+
+ return (int)ret;
+}
+
+int64_t CDVDInputStreamFile::Seek(int64_t offset, int whence)
+{
+ if(!m_pFile) return -1;
+
+ if(whence == SEEK_POSSIBLE)
+ return m_pFile->IoControl(IOCTRL_SEEK_POSSIBLE, NULL);
+
+ int64_t ret = m_pFile->Seek(offset, whence);
+
+ /* if we succeed, we are not eof anymore */
+ if( ret >= 0 ) m_eof = false;
+
+ return ret;
+}
+
+int64_t CDVDInputStreamFile::GetLength()
+{
+ if (m_pFile)
+ return m_pFile->GetLength();
+ return 0;
+}
+
+bool CDVDInputStreamFile::GetCacheStatus(XFILE::SCacheStatus *status)
+{
+ if(m_pFile && m_pFile->IoControl(IOCTRL_CACHE_STATUS, status) >= 0)
+ return true;
+ else
+ return false;
+}
+
+BitstreamStats CDVDInputStreamFile::GetBitstreamStats() const
+{
+ if (!m_pFile)
+ return m_stats; // dummy return. defined in CDVDInputStream
+
+ if(m_pFile->GetBitstreamStats())
+ return *m_pFile->GetBitstreamStats();
+ else
+ return m_stats;
+}
+
+int CDVDInputStreamFile::GetBlockSize()
+{
+ if(m_pFile)
+ return m_pFile->GetChunkSize();
+ else
+ return 0;
+}
+
+void CDVDInputStreamFile::SetReadRate(uint32_t rate)
+{
+ // Increase requested rate by 10%:
+ uint32_t maxrate = static_cast<uint32_t>(1.1 * rate);
+
+ if(m_pFile->IoControl(IOCTRL_CACHE_SETRATE, &maxrate) >= 0)
+ CLog::Log(LOGDEBUG,
+ "CDVDInputStreamFile::SetReadRate - set cache throttle rate to {} bytes per second",
+ maxrate);
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFile.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFile.h
new file mode 100644
index 0000000..fc6d3ab
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamFile.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "DVDInputStream.h"
+
+class CDVDInputStreamFile : public CDVDInputStream
+{
+public:
+ explicit CDVDInputStreamFile(const CFileItem& fileitem, unsigned int flags);
+ ~CDVDInputStreamFile() override;
+ bool Open() override;
+ void Close() override;
+ int Read(uint8_t* buf, int buf_size) override;
+ int64_t Seek(int64_t offset, int whence) override;
+ bool IsEOF() override;
+ int64_t GetLength() override;
+ BitstreamStats GetBitstreamStats() const override ;
+ int GetBlockSize() override;
+ void SetReadRate(uint32_t rate) override;
+ bool GetCacheStatus(XFILE::SCacheStatus *status) override;
+
+protected:
+ XFILE::CFile* m_pFile = nullptr;
+ bool m_eof = false;
+ unsigned int m_flags = 0;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamMemory.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamMemory.cpp
new file mode 100644
index 0000000..2d5a910
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamMemory.cpp
@@ -0,0 +1,96 @@
+/*
+ * 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 "DVDInputStreamMemory.h"
+
+CDVDInputStreamMemory::CDVDInputStreamMemory(CFileItem& fileitem) : CDVDInputStream(DVDSTREAM_TYPE_MEMORY, fileitem)
+{
+ m_pData = NULL;
+ m_iDataSize = 0;
+ m_iDataPos = 0;
+}
+
+CDVDInputStreamMemory::~CDVDInputStreamMemory()
+{
+ Close();
+}
+
+bool CDVDInputStreamMemory::IsEOF()
+{
+ if(m_iDataPos >= m_iDataSize)
+ return true;
+
+ return false;
+}
+
+bool CDVDInputStreamMemory::Open()
+{
+ if (!CDVDInputStream::Open())
+ return false;
+
+ return true;
+}
+
+// close file and reset everything
+void CDVDInputStreamMemory::Close()
+{
+ if (m_pData) delete[] m_pData;
+ m_pData = NULL;
+ m_iDataSize = 0;
+ m_iDataPos = 0;
+
+ CDVDInputStream::Close();
+}
+
+int CDVDInputStreamMemory::Read(uint8_t* buf, int buf_size)
+{
+ int iBytesToCopy = buf_size;
+ int iBytesLeft = m_iDataSize - m_iDataPos;
+ if (iBytesToCopy > iBytesLeft) iBytesToCopy = iBytesLeft;
+
+ if (iBytesToCopy > 0)
+ {
+ memcpy(buf, m_pData + m_iDataPos, iBytesToCopy);
+ m_iDataPos += iBytesToCopy;
+ }
+
+ return iBytesToCopy;
+}
+
+int64_t CDVDInputStreamMemory::Seek(int64_t offset, int whence)
+{
+ switch (whence)
+ {
+ case SEEK_CUR:
+ {
+ if ((m_iDataPos + offset) > m_iDataSize) return -1;
+ else m_iDataPos += (int)offset;
+ break;
+ }
+ case SEEK_END:
+ {
+ m_iDataPos = m_iDataSize;
+ break;
+ }
+ case SEEK_SET:
+ {
+ if (offset > m_iDataSize || offset < 0) return -1;
+ else m_iDataPos = (int)offset;
+ break;
+ }
+ default:
+ return -1;
+ }
+ return m_iDataPos;
+}
+
+int64_t CDVDInputStreamMemory::GetLength()
+{
+ return m_iDataSize;
+}
+
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamMemory.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamMemory.h
new file mode 100644
index 0000000..a7d494c
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamMemory.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DVDInputStream.h"
+
+class CDVDInputStreamMemory : public CDVDInputStream
+{
+public:
+ explicit CDVDInputStreamMemory(CFileItem &fileitem);
+ ~CDVDInputStreamMemory() override;
+ bool Open() override;
+ void Close() override;
+ int Read(uint8_t* buf, int buf_size) override;
+ int64_t Seek(int64_t offset, int whence) override;
+ bool IsEOF() override;
+ int64_t GetLength() override;
+
+protected:
+ uint8_t* m_pData;
+ int m_iDataSize;
+ int m_iDataPos;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.cpp
new file mode 100644
index 0000000..e7b54e3
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.cpp
@@ -0,0 +1,1577 @@
+/*
+ * Copyright (C) 2005-2022 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 "DVDInputStreamNavigator.h"
+#include "filesystem/IFileTypes.h"
+#include "utils/LangCodeExpander.h"
+#include "../DVDDemuxSPU.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "utils/Geometry.h"
+#include "utils/log.h"
+#include "utils/URIUtils.h"
+#include "utils/StringUtils.h"
+#include "guilib/LocalizeStrings.h"
+#if defined(TARGET_DARWIN_OSX)
+#include "platform/darwin/osx/CocoaInterface.h"
+#endif
+#if defined(TARGET_WINDOWS_STORE)
+#include "filesystem/SpecialProtocol.h"
+#include "platform/Environment.h"
+#endif
+
+namespace
+{
+constexpr int HOLDMODE_NONE = 0;
+/* set internally when we wish to flush demuxer */
+constexpr int HOLDMODE_HELD = 1;
+/* set by inputstream user, when they wish to skip the held mode */
+constexpr int HOLDMODE_SKIP = 2;
+/* set after hold mode has been exited, and action that inited it has been executed */
+constexpr int HOLDMODE_DATA = 3;
+
+// DVD Subpicture types
+constexpr int DVD_SUBPICTURE_TYPE_NOTSPECIFIED = 0;
+constexpr int DVD_SUBPICTURE_TYPE_LANGUAGE = 1;
+
+// DVD Subpicture language extensions
+constexpr int DVD_SUBPICTURE_LANG_EXT_NOTSPECIFIED = 0;
+constexpr int DVD_SUBPICTURE_LANG_EXT_NORMALCAPTIONS = 1;
+constexpr int DVD_SUBPICTURE_LANG_EXT_BIGCAPTIONS = 2;
+constexpr int DVD_SUBPICTURE_LANG_EXT_CHILDRENSCAPTIONS = 3;
+constexpr int DVD_SUBPICTURE_LANG_EXT_NORMALCC = 5;
+constexpr int DVD_SUBPICTURE_LANG_EXT_BIGCC = 6;
+constexpr int DVD_SUBPICTURE_LANG_EXT_CHILDRENSCC = 7;
+constexpr int DVD_SUBPICTURE_LANG_EXT_FORCED = 9;
+constexpr int DVD_SUBPICTURE_LANG_EXT_NORMALDIRECTORSCOMMENTS = 13;
+constexpr int DVD_SUBPICTURE_LANG_EXT_BIGDIRECTORSCOMMENTS = 14;
+constexpr int DVD_SUBPICTURE_LANG_EXT_CHILDRENDIRECTORSCOMMENTS = 15;
+
+// DVD Audio language extensions
+constexpr int DVD_AUDIO_LANG_EXT_NOTSPECIFIED = 0;
+constexpr int DVD_AUDIO_LANG_EXT_NORMALCAPTIONS = 1;
+constexpr int DVD_AUDIO_LANG_EXT_VISUALLYIMPAIRED = 2;
+constexpr int DVD_AUDIO_LANG_EXT_DIRECTORSCOMMENTS1 = 3;
+constexpr int DVD_AUDIO_LANG_EXT_DIRECTORSCOMMENTS2 = 4;
+} // namespace
+
+static int dvd_inputstreamnavigator_cb_seek(void * p_stream, uint64_t i_pos);
+static int dvd_inputstreamnavigator_cb_read(void * p_stream, void * buffer, int i_read);
+static int dvd_inputstreamnavigator_cb_readv(void * p_stream, void * p_iovec, int i_blocks);
+static void dvd_logger(void* priv, dvdnav_logger_level_t level, const char* fmt, va_list va);
+
+CDVDInputStreamNavigator::CDVDInputStreamNavigator(IVideoPlayer* player, const CFileItem& fileitem)
+ : CDVDInputStream(DVDSTREAM_TYPE_DVD, fileitem), m_pstream(nullptr)
+{
+ m_dvdnav = 0;
+ m_pVideoPlayer = player;
+ m_bCheckButtons = false;
+ m_iCellStart = 0;
+ m_iVobUnitStart = 0LL;
+ m_iVobUnitStop = 0LL;
+ m_iVobUnitCorrection = 0LL;
+ m_bInMenu = false;
+ m_holdmode = HOLDMODE_NONE;
+ m_iTitle = m_iTitleCount = 0;
+ m_iPart = m_iPartCount = 0;
+ m_iTime = m_iTotalTime = 0;
+ m_bEOF = false;
+ m_lastevent = DVDNAV_NOP;
+ m_dvdnav_stream_cb.pf_read = dvd_inputstreamnavigator_cb_read;
+ m_dvdnav_stream_cb.pf_readv = dvd_inputstreamnavigator_cb_readv;
+ m_dvdnav_stream_cb.pf_seek = dvd_inputstreamnavigator_cb_seek;
+
+ memset(m_lastblock, 0, sizeof(m_lastblock));
+}
+
+CDVDInputStreamNavigator::~CDVDInputStreamNavigator()
+{
+ Close();
+}
+
+bool CDVDInputStreamNavigator::Open()
+{
+ m_item.SetMimeType("video/x-dvd-mpeg");
+ if (!CDVDInputStream::Open())
+ return false;
+
+#if defined(TARGET_WINDOWS_STORE)
+ // libdvdcss
+ CEnvironment::putenv("DVDCSS_METHOD=key");
+ CEnvironment::putenv("DVDCSS_VERBOSE=3");
+ CEnvironment::putenv("DVDCSS_CACHE=" + CSpecialProtocol::TranslatePath("special://masterprofile/cache"));
+#endif
+
+ // load libdvdnav.dll
+ if (!m_dll.Load())
+ return false;
+
+ // load the dvd language codes
+ // g_LangCodeExpander.LoadStandardCodes();
+
+ // libdvdcss fails if the file path contains VIDEO_TS.IFO or VIDEO_TS/VIDEO_TS.IFO
+ // libdvdnav is still able to play without, so strip them.
+
+ std::string path = m_item.GetDynPath();
+ if(URIUtils::GetFileName(path) == "VIDEO_TS.IFO")
+ path = URIUtils::GetParentPath(path);
+ URIUtils::RemoveSlashAtEnd(path);
+ if(URIUtils::GetFileName(path) == "VIDEO_TS")
+ path = URIUtils::GetParentPath(path);
+ URIUtils::RemoveSlashAtEnd(path);
+
+#if defined(TARGET_DARWIN_OSX)
+ // if physical DVDs, libdvdnav wants "/dev/rdiskN" device name for OSX,
+ // strDVDFile will get realloc'ed and replaced IF this is a physical DVD.
+ char* strDVDFile = Cocoa_MountPoint2DeviceName(strdup(path.c_str()));
+ path = strDVDFile;
+ free(strDVDFile);
+#endif
+
+#if DVDNAV_VERSION >= 60100
+ dvdnav_logger_cb loggerCallback;
+ loggerCallback.pf_log = dvd_logger;
+#endif
+
+ // open up the DVD device
+ if (m_item.IsDiscImage())
+ {
+ // if dvd image file (ISO or alike) open using libdvdnav stream callback functions
+ m_pstream.reset(new CDVDInputStreamFile(m_item, XFILE::READ_TRUNCATED | XFILE::READ_BITRATE | XFILE::READ_CHUNKED));
+#if DVDNAV_VERSION >= 60100
+ if (!m_pstream->Open() || m_dll.dvdnav_open_stream2(&m_dvdnav, m_pstream.get(), &loggerCallback,
+ &m_dvdnav_stream_cb) != DVDNAV_STATUS_OK)
+#else
+ if (!m_pstream->Open() || m_dll.dvdnav_open_stream(&m_dvdnav, m_pstream.get(), &m_dvdnav_stream_cb) != DVDNAV_STATUS_OK)
+#endif
+ {
+ CLog::Log(LOGERROR, "Error opening image file or Error on dvdnav_open_stream");
+ Close();
+ return false;
+ }
+ }
+#if DVDNAV_VERSION >= 60100
+ else if (m_dll.dvdnav_open2(&m_dvdnav, nullptr, &loggerCallback, path.c_str()) !=
+ DVDNAV_STATUS_OK)
+#else
+ else if (m_dll.dvdnav_open(&m_dvdnav, path.c_str()) != DVDNAV_STATUS_OK)
+#endif
+ {
+ CLog::Log(LOGERROR, "Error on dvdnav_open");
+ Close();
+ return false;
+ }
+
+ int region = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_DVDS_PLAYERREGION);
+ int mask = 0;
+ if(region > 0)
+ mask = 1 << (region-1);
+ else
+ {
+ // find out what region dvd reports itself to be from, and use that as mask if available
+ if (m_dll.dvdnav_get_disk_region_mask(m_dvdnav, &mask) == DVDNAV_STATUS_ERR)
+ {
+ CLog::LogF(LOGERROR, "Error getting DVD region code: {}",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ mask = 0xff;
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "{} - Setting region mask {:02x}", __FUNCTION__, mask);
+ m_dll.dvdnav_set_region_mask(m_dvdnav, mask);
+
+ // get default language settings
+ char language_menu[3];
+ strncpy(language_menu, g_langInfo.GetDVDMenuLanguage().c_str(), sizeof(language_menu)-1);
+ language_menu[2] = '\0';
+
+ char language_audio[3];
+ strncpy(language_audio, g_langInfo.GetDVDAudioLanguage().c_str(), sizeof(language_audio)-1);
+ language_audio[2] = '\0';
+
+ char language_subtitle[3];
+ strncpy(language_subtitle, g_langInfo.GetDVDSubtitleLanguage().c_str(), sizeof(language_subtitle)-1);
+ language_subtitle[2] = '\0';
+
+ // set language settings in case they are not set in xbmc's configuration
+ if (language_menu[0] == '\0') strcpy(language_menu, "en");
+ if (language_audio[0] == '\0') strcpy(language_audio, "en");
+ if (language_subtitle[0] == '\0') strcpy(language_subtitle, "en");
+
+ // set default language settings
+ if (m_dll.dvdnav_menu_language_select(m_dvdnav, (char*)language_menu) != DVDNAV_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, "Error on setting default menu language: {}",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ CLog::Log(LOGERROR, "Defaulting to \"en\"");
+ //! @bug libdvdnav isn't const correct
+ m_dll.dvdnav_menu_language_select(m_dvdnav, const_cast<char*>("en"));
+ }
+
+ if (m_dll.dvdnav_audio_language_select(m_dvdnav, (char*)language_audio) != DVDNAV_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, "Error on setting default audio language: {}",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ CLog::Log(LOGERROR, "Defaulting to \"en\"");
+ //! @bug libdvdnav isn't const correct
+ m_dll.dvdnav_audio_language_select(m_dvdnav, const_cast<char*>("en"));
+ }
+
+ if (m_dll.dvdnav_spu_language_select(m_dvdnav, (char*)language_subtitle) != DVDNAV_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, "Error on setting default subtitle language: {}",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ CLog::Log(LOGERROR, "Defaulting to \"en\"");
+ //! @bug libdvdnav isn't const correct
+ m_dll.dvdnav_spu_language_select(m_dvdnav, const_cast<char*>("en"));
+ }
+
+ // set read ahead cache usage
+ if (m_dll.dvdnav_set_readahead_flag(m_dvdnav, 1) != DVDNAV_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, "Error on dvdnav_set_readahead_flag: {}",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ Close();
+ return false;
+ }
+
+ // set the PGC positioning flag to have position information relatively to the
+ // whole feature instead of just relatively to the current chapter
+ if (m_dll.dvdnav_set_PGC_positioning_flag(m_dvdnav, 1) != DVDNAV_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, "Error on dvdnav_set_PGC_positioning_flag: {}",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ Close();
+ return false;
+ }
+
+ // jump directly to title menu
+ if(CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_DVDS_AUTOMENU))
+ {
+ int len, event;
+ uint8_t buf[2048];
+ uint8_t* buf_ptr = buf;
+
+ // must startup vm and pgc
+ m_dll.dvdnav_get_next_cache_block(m_dvdnav,&buf_ptr,&event,&len);
+ m_dll.dvdnav_sector_search(m_dvdnav, 0, SEEK_SET);
+
+ // first try title menu
+ if(m_dll.dvdnav_menu_call(m_dvdnav, DVD_MENU_Title) != DVDNAV_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, "Error on dvdnav_menu_call(Title): {}",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ // next try root menu
+ if(m_dll.dvdnav_menu_call(m_dvdnav, DVD_MENU_Root) != DVDNAV_STATUS_OK )
+ CLog::Log(LOGERROR, "Error on dvdnav_menu_call(Root): {}",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ }
+ }
+
+ m_bEOF = false;
+ m_bCheckButtons = false;
+ m_iCellStart = 0;
+ m_iVobUnitStart = 0LL;
+ m_iVobUnitStop = 0LL;
+ m_iVobUnitCorrection = 0LL;
+ m_bInMenu = false;
+ m_holdmode = HOLDMODE_NONE;
+ m_iTitle = m_iTitleCount = 0;
+ m_iPart = m_iPartCount = 0;
+ m_iTime = m_iTotalTime = 0;
+
+ return true;
+}
+
+void CDVDInputStreamNavigator::Close()
+{
+ if (!m_dvdnav) return;
+
+ // finish off by closing the dvdnav device
+ if (m_dll.dvdnav_close(m_dvdnav) != DVDNAV_STATUS_OK)
+ {
+ CLog::Log(LOGERROR, "Error on dvdnav_close: {}", m_dll.dvdnav_err_to_string(m_dvdnav));
+ return ;
+ }
+
+ CDVDInputStream::Close();
+ m_dvdnav = NULL;
+ m_bEOF = true;
+
+ if (m_pstream != nullptr)
+ {
+ m_pstream->Close();
+ m_pstream.reset();
+ }
+}
+
+int CDVDInputStreamNavigator::Read(uint8_t* buf, int buf_size)
+{
+ if (!m_dvdnav || m_bEOF) return 0;
+ if (buf_size < DVD_VIDEO_BLOCKSIZE)
+ {
+ CLog::Log(LOGERROR,
+ "CDVDInputStreamNavigator: buffer size is to small, {} bytes, should be 2048 bytes",
+ buf_size);
+ return -1;
+ }
+
+ int iBytesRead = 0;
+
+ int NOPcount = 0;
+ while (true)
+ {
+ int navresult = ProcessBlock(buf, &iBytesRead);
+
+ if (navresult == NAVRESULT_HOLD)
+ return 0; // return 0 bytes read;
+ else if (navresult == NAVRESULT_ERROR)
+ return -1;
+ else if (navresult == NAVRESULT_DATA)
+ return iBytesRead;
+ else if (navresult == NAVRESULT_NOP)
+ {
+ NOPcount++;
+ if (NOPcount == 1000)
+ {
+ m_bEOF = true;
+ CLog::Log(LOGERROR,"CDVDInputStreamNavigator: Stopping playback due to infinite loop, caused by badly authored DVD navigation structure. Try enabling 'Attempt to skip introduction before DVD menu'.");
+ m_pVideoPlayer->OnDiscNavResult(nullptr, DVDNAV_ERROR);
+ return -1; // fail and stop playback.
+ }
+ }
+ }
+
+ return iBytesRead;
+}
+
+// not working yet, but it is the recommended way for seeking
+int64_t CDVDInputStreamNavigator::Seek(int64_t offset, int whence)
+{
+ if(whence == SEEK_POSSIBLE)
+ return 0;
+ else
+ return -1;
+}
+
+int CDVDInputStreamNavigator::ProcessBlock(uint8_t* dest_buffer, int* read)
+{
+ if (!m_dvdnav)
+ return -1;
+
+ int result;
+ int len = 2048;
+
+ // m_tempbuffer will be used for anything that isn't a normal data block
+ uint8_t* buf = m_lastblock;
+ int iNavresult = -1;
+
+ if (m_holdmode == HOLDMODE_HELD)
+ return NAVRESULT_HOLD;
+
+ // the main reading function
+ if(m_holdmode == HOLDMODE_SKIP)
+ { /* we where holding data, return the data held */
+ m_holdmode = HOLDMODE_DATA;
+ result = DVDNAV_STATUS_OK;
+ }
+ else
+ result = m_dll.dvdnav_get_next_cache_block(m_dvdnav, &buf, &m_lastevent, &len);
+
+ if (result == DVDNAV_STATUS_ERR)
+ {
+ CLog::Log(LOGERROR, "Error getting next block: {}", m_dll.dvdnav_err_to_string(m_dvdnav));
+ m_bEOF = true;
+ return NAVRESULT_ERROR;
+ }
+
+ switch (m_lastevent)
+ {
+ case DVDNAV_BLOCK_OK:
+ {
+ // We have received a regular block of the currently playing MPEG stream.
+ // buf contains the data and len its length (obviously!) (which is always 2048 bytes btw)
+ m_holdmode = HOLDMODE_NONE;
+ memcpy(dest_buffer, buf, len);
+ *read = len;
+ iNavresult = NAVRESULT_DATA;
+ }
+ break;
+
+ case DVDNAV_NOP:
+ // Nothing to do here.
+ break;
+
+ case DVDNAV_STILL_FRAME:
+ {
+ // We have reached a still frame. A real player application would wait
+ // the amount of time specified by the still's length while still handling
+ // user input to make menus and other interactive stills work.
+ // A length of 0xff means an indefinite still which has to be skipped
+ // indirectly by some user interaction.
+ m_holdmode = HOLDMODE_NONE;
+ iNavresult = m_pVideoPlayer->OnDiscNavResult(buf, DVDNAV_STILL_FRAME);
+ }
+ break;
+
+ case DVDNAV_WAIT:
+ {
+ // We have reached a point in DVD playback, where timing is critical.
+ // Player application with internal fifos can introduce state
+ // inconsistencies, because libdvdnav is always the fifo's length
+ // ahead in the stream compared to what the application sees.
+ // Such applications should wait until their fifos are empty
+ // when they receive this type of event.
+ if(m_holdmode == HOLDMODE_NONE)
+ {
+ CLog::Log(LOGDEBUG, " - DVDNAV_WAIT (HOLDING)");
+ m_holdmode = HOLDMODE_HELD;
+ iNavresult = NAVRESULT_HOLD;
+ }
+ else
+ iNavresult = m_pVideoPlayer->OnDiscNavResult(buf, DVDNAV_WAIT);
+
+ /* if user didn't care for action, just skip it */
+ if(iNavresult == NAVRESULT_NOP)
+ SkipWait();
+ }
+ break;
+
+ case DVDNAV_SPU_CLUT_CHANGE:
+ // Player applications should pass the new colour lookup table to their
+ // SPU decoder. The CLUT is given as 16 uint32_t's in the buffer.
+ {
+ iNavresult = m_pVideoPlayer->OnDiscNavResult(buf, DVDNAV_SPU_CLUT_CHANGE);
+ }
+ break;
+
+ case DVDNAV_SPU_STREAM_CHANGE:
+ // Player applications should inform their SPU decoder to switch channels
+ {
+ m_bCheckButtons = true;
+ iNavresult = m_pVideoPlayer->OnDiscNavResult(buf, DVDNAV_SPU_STREAM_CHANGE);
+ }
+ break;
+
+ case DVDNAV_AUDIO_STREAM_CHANGE:
+ // Player applications should inform their audio decoder to switch channels
+ {
+ iNavresult = m_pVideoPlayer->OnDiscNavResult(buf, DVDNAV_AUDIO_STREAM_CHANGE);
+ }
+
+ break;
+
+ case DVDNAV_HIGHLIGHT:
+ {
+ iNavresult = m_pVideoPlayer->OnDiscNavResult(buf, DVDNAV_HIGHLIGHT);
+ }
+ break;
+
+ case DVDNAV_VTS_CHANGE:
+ // Some status information like video aspect and video scale permissions do
+ // not change inside a VTS. Therefore this event can be used to query such
+ // information only when necessary and update the decoding/displaying
+ // accordingly.
+ {
+ if(m_holdmode == HOLDMODE_NONE)
+ {
+ CLog::Log(LOGDEBUG, " - DVDNAV_VTS_CHANGE (HOLDING)");
+ m_holdmode = HOLDMODE_HELD;
+ iNavresult = NAVRESULT_HOLD;
+ }
+ else
+ {
+ bool menu = (0 == m_dll.dvdnav_is_domain_vts(m_dvdnav));
+ if (menu != m_bInMenu)
+ {
+ m_bInMenu = menu;
+ }
+ iNavresult = m_pVideoPlayer->OnDiscNavResult(buf, DVDNAV_VTS_CHANGE);
+ }
+ }
+ break;
+
+ case DVDNAV_CELL_CHANGE:
+ {
+ // Some status information like the current Title and Part numbers do not
+ // change inside a cell. Therefore this event can be used to query such
+ // information only when necessary and update the decoding/displaying
+ // accordingly.
+
+ uint32_t pos = 0;
+ uint32_t len = 0;
+
+ m_dll.dvdnav_current_title_info(m_dvdnav, &m_iTitle, &m_iPart);
+ m_dll.dvdnav_get_number_of_titles(m_dvdnav, &m_iTitleCount);
+ if(m_iTitle > 0)
+ m_dll.dvdnav_get_number_of_parts(m_dvdnav, m_iTitle, &m_iPartCount);
+ else
+ m_iPartCount = 0;
+ m_dll.dvdnav_get_position(m_dvdnav, &pos, &len);
+
+ // get chapters' timestamps if we have not cached them yet
+ if (m_mapTitleChapters.find(m_iTitle) == m_mapTitleChapters.end())
+ {
+ uint64_t* times = NULL;
+ uint64_t duration;
+ //dvdnav_describe_title_chapters returns 0 on failure and NULL for times
+ int entries = m_dll.dvdnav_describe_title_chapters(m_dvdnav, m_iTitle, &times, &duration);
+
+ if (entries != m_iPartCount)
+ CLog::Log(LOGDEBUG,
+ "{} - Number of chapters/positions differ: Chapters {}, positions {}",
+ __FUNCTION__, m_iPartCount, entries);
+
+ if (times)
+ {
+ // the times array stores the end timestamps of the chapters, e.g., times[0] stores the position/beginning of chapter 2
+ m_mapTitleChapters[m_iTitle][1] = 0;
+ for (int i = 0; i < entries - 1; ++i)
+ {
+ m_mapTitleChapters[m_iTitle][i + 2] = times[i] / 90000;
+ }
+ free(times);
+ }
+ }
+ CLog::Log(LOGDEBUG, "{} - Cell change: Title {}, Chapter {}", __FUNCTION__, m_iTitle,
+ m_iPart);
+ CLog::Log(LOGDEBUG, "{} - At position {:.0f}% inside the feature", __FUNCTION__,
+ 100 * (double)pos / (double)len);
+ //Get total segment time
+
+ dvdnav_cell_change_event_t* cell_change_event = reinterpret_cast<dvdnav_cell_change_event_t*>(buf);
+ m_iCellStart = cell_change_event->cell_start; // store cell time as we need that for time later
+ m_iTime = (int) (m_iCellStart / 90);
+ m_iTotalTime = (int) (cell_change_event->pgc_length / 90);
+
+ iNavresult = m_pVideoPlayer->OnDiscNavResult(buf, DVDNAV_CELL_CHANGE);
+ }
+ break;
+
+ case DVDNAV_NAV_PACKET:
+ {
+ // A NAV packet provides PTS discontinuity information, angle linking information and
+ // button definitions for DVD menus. Angles are handled completely inside libdvdnav.
+ // For the menus to work, the NAV packet information has to be passed to the overlay
+ // engine of the player so that it knows the dimensions of the button areas.
+
+ // Applications with fifos should not use these functions to retrieve NAV packets,
+ // they should implement their own NAV handling, because the packet you get from these
+ // functions will already be ahead in the stream which can cause state inconsistencies.
+ // Applications with fifos should therefore pass the NAV packet through the fifo
+ // and decoding pipeline just like any other data.
+
+ // Calculate current time
+ //unsigned int pos, len;
+ //m_dll.dvdnav_get_position(m_dvdnav, &pos, &len);
+ //m_iTime = (int)(((int64_t)m_iTotalTime * pos) / len);
+
+ pci_t* pci = m_dll.dvdnav_get_current_nav_pci(m_dvdnav);
+ m_dll.dvdnav_get_current_nav_dsi(m_dvdnav);
+
+ if(!pci)
+ {
+ iNavresult = NAVRESULT_NOP;
+ break;
+ }
+
+ /* if we have any buttons or are not in vts domain we assume we are in menu */
+ bool menu = pci->hli.hl_gi.hli_ss || (0 == m_dll.dvdnav_is_domain_vts(m_dvdnav));
+ if (menu != m_bInMenu)
+ {
+ m_bInMenu = menu;
+ }
+
+ /* check for any gap in the stream, this is likely a discontinuity */
+ int64_t gap = (int64_t)pci->pci_gi.vobu_s_ptm - m_iVobUnitStop;
+ if(gap)
+ {
+ /* make sure demuxer is flushed before we change any correction */
+ if(m_holdmode == HOLDMODE_NONE)
+ {
+ CLog::Log(LOGDEBUG, "DVDNAV_NAV_PACKET (HOLDING)");
+ m_holdmode = HOLDMODE_HELD;
+ iNavresult = NAVRESULT_HOLD;
+ break;
+ }
+ m_iVobUnitCorrection += gap;
+
+ CLog::Log(LOGDEBUG, "DVDNAV_NAV_PACKET - DISCONTINUITY FROM:{} TO:{} DIFF:{}",
+ (m_iVobUnitStop * 1000) / 90, ((int64_t)pci->pci_gi.vobu_s_ptm * 1000) / 90,
+ (gap * 1000) / 90);
+ }
+
+ m_iVobUnitStart = pci->pci_gi.vobu_s_ptm;
+ m_iVobUnitStop = pci->pci_gi.vobu_e_ptm;
+
+ m_iTime = (int) ( m_dll.dvdnav_get_current_time(m_dvdnav) / 90 );
+
+ iNavresult = m_pVideoPlayer->OnDiscNavResult((void*)pci, DVDNAV_NAV_PACKET);
+ }
+ break;
+
+ case DVDNAV_HOP_CHANNEL:
+ // This event is issued whenever a non-seamless operation has been executed.
+ // Applications with fifos should drop the fifos content to speed up responsiveness.
+ {
+ iNavresult = m_pVideoPlayer->OnDiscNavResult(NULL, DVDNAV_HOP_CHANNEL);
+ }
+ break;
+
+ case DVDNAV_STOP:
+ {
+ // Playback should end here.
+
+ // don't read any further, it could be libdvdnav had some problems reading
+ // the disc. reading further results in a crash
+ m_bEOF = true;
+
+ m_pVideoPlayer->OnDiscNavResult(NULL, DVDNAV_STOP);
+ iNavresult = NAVRESULT_ERROR;
+ }
+ break;
+
+ default:
+ {
+ CLog::Log(LOGDEBUG, "CDVDInputStreamNavigator: Unknown event ({})", m_lastevent);
+ }
+ break;
+
+ }
+
+ // check if libdvdnav gave us some other buffer to work with
+ // probably not needed since function will check if buf
+ // is part of the internal cache, but do it for good measure
+ if( buf != m_lastblock )
+ m_dll.dvdnav_free_cache_block(m_dvdnav, buf);
+
+ return iNavresult;
+}
+
+bool CDVDInputStreamNavigator::SetActiveAudioStream(int iId)
+{
+ CLog::Log(LOGDEBUG, "Setting active audio stream id: {}", iId);
+
+ if (!m_dvdnav)
+ return false;
+
+ dvdnav_status_t ret = m_dll.dvdnav_set_active_stream(m_dvdnav, iId, DVD_AUDIO_STREAM);
+ if (ret == DVDNAV_STATUS_ERR)
+ {
+ CLog::LogF(LOGERROR, "dvdnav_set_active_stream (audio) failed: {}",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ }
+
+ return ret == DVDNAV_STATUS_OK;
+}
+
+bool CDVDInputStreamNavigator::SetActiveSubtitleStream(int iId)
+{
+ CLog::LogF(LOGDEBUG, "Setting active subtitle stream id: {}", iId);
+
+ if (!m_dvdnav)
+ return false;
+
+ dvdnav_status_t ret = m_dll.dvdnav_set_active_stream(m_dvdnav, iId, DVD_SUBTITLE_STREAM);
+ if (ret == DVDNAV_STATUS_ERR)
+ {
+ CLog::LogF(LOGERROR, "dvdnav_set_active_stream (subtitles) failed: {}",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ }
+
+ return ret == DVDNAV_STATUS_OK;
+}
+
+void CDVDInputStreamNavigator::ActivateButton()
+{
+ if (m_dvdnav)
+ {
+ m_dll.dvdnav_button_activate(m_dvdnav, m_dll.dvdnav_get_current_nav_pci(m_dvdnav));
+ }
+}
+
+void CDVDInputStreamNavigator::SelectButton(int iButton)
+{
+ if (!m_dvdnav) return;
+ m_dll.dvdnav_button_select(m_dvdnav, m_dll.dvdnav_get_current_nav_pci(m_dvdnav), iButton);
+}
+
+int CDVDInputStreamNavigator::GetCurrentButton()
+{
+ if (!m_dvdnav)
+ {
+ return -1;
+ }
+
+ int button = 0;
+ if (m_dll.dvdnav_get_current_highlight(m_dvdnav, &button) == DVDNAV_STATUS_ERR)
+ {
+ CLog::LogF(LOGERROR, "dvdnav_get_current_highlight failed: {}",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ return -1;
+ }
+ return button;
+}
+
+void CDVDInputStreamNavigator::CheckButtons()
+{
+ if (m_dvdnav && m_bCheckButtons)
+ {
+ m_bCheckButtons = false;
+ pci_t* pci = m_dll.dvdnav_get_current_nav_pci(m_dvdnav);
+ int iCurrentButton = GetCurrentButton();
+
+ if( iCurrentButton > 0 && iCurrentButton < 37 )
+ {
+ btni_t* button = &(pci->hli.btnit[iCurrentButton-1]);
+
+ // menu buttons are always cropped overlays, so if there is no such information
+ // we assume the button is invalid
+ if ((button->x_start || button->x_end || button->y_start || button->y_end))
+ {
+ // button has info, it's valid
+ return;
+ }
+ }
+
+ // select first valid button.
+ for (int i = 0; i < 36; i++)
+ {
+ if (pci->hli.btnit[i].x_start ||
+ pci->hli.btnit[i].x_end ||
+ pci->hli.btnit[i].y_start ||
+ pci->hli.btnit[i].y_end)
+ {
+ CLog::Log(LOGWARNING, "CDVDInputStreamNavigator: found invalid button({})", iCurrentButton);
+ CLog::Log(LOGWARNING, "CDVDInputStreamNavigator: switching to button({}) instead", i + 1);
+ m_dll.dvdnav_button_select(m_dvdnav, pci, i + 1);
+ break;
+ }
+ }
+ }
+}
+
+int CDVDInputStreamNavigator::GetTotalButtons()
+{
+ if (!m_dvdnav) return 0;
+
+ pci_t* pci = m_dll.dvdnav_get_current_nav_pci(m_dvdnav);
+
+ int counter = 0;
+ for (const btni_t& buttonInfo : pci->hli.btnit)
+ {
+ if (buttonInfo.x_start ||
+ buttonInfo.x_end ||
+ buttonInfo.y_start ||
+ buttonInfo.y_end)
+ {
+ counter++;
+ }
+ }
+ return counter;
+}
+
+void CDVDInputStreamNavigator::OnUp()
+{
+ if (m_dvdnav) m_dll.dvdnav_upper_button_select(m_dvdnav, m_dll.dvdnav_get_current_nav_pci(m_dvdnav));
+}
+
+void CDVDInputStreamNavigator::OnDown()
+{
+ if (m_dvdnav) m_dll.dvdnav_lower_button_select(m_dvdnav, m_dll.dvdnav_get_current_nav_pci(m_dvdnav));
+}
+
+void CDVDInputStreamNavigator::OnLeft()
+{
+ if (m_dvdnav) m_dll.dvdnav_left_button_select(m_dvdnav, m_dll.dvdnav_get_current_nav_pci(m_dvdnav));
+}
+
+void CDVDInputStreamNavigator::OnRight()
+{
+ if (m_dvdnav) m_dll.dvdnav_right_button_select(m_dvdnav, m_dll.dvdnav_get_current_nav_pci(m_dvdnav));
+}
+
+bool CDVDInputStreamNavigator::OnMouseMove(const CPoint &point)
+{
+ if (m_dvdnav)
+ {
+ pci_t* pci = m_dll.dvdnav_get_current_nav_pci(m_dvdnav);
+ return (DVDNAV_STATUS_OK == m_dll.dvdnav_mouse_select(m_dvdnav, pci, (int32_t)point.x, (int32_t)point.y));
+ }
+ return false;
+}
+
+bool CDVDInputStreamNavigator::OnMouseClick(const CPoint &point)
+{
+ if (m_dvdnav)
+ {
+ pci_t* pci = m_dll.dvdnav_get_current_nav_pci(m_dvdnav);
+ return (DVDNAV_STATUS_OK == m_dll.dvdnav_mouse_activate(m_dvdnav, pci, (int32_t)point.x, (int32_t)point.y));
+ }
+ return false;
+}
+
+bool CDVDInputStreamNavigator::OnMenu()
+{
+ if (!m_dvdnav)
+ {
+ return false;
+ }
+
+ return m_dll.dvdnav_menu_call(m_dvdnav, DVD_MENU_Escape) == DVDNAV_STATUS_OK;
+}
+
+void CDVDInputStreamNavigator::OnBack()
+{
+ if (m_dvdnav) m_dll.dvdnav_go_up(m_dvdnav);
+}
+
+// we don't allow skipping in menu's cause it will remove menu overlays
+void CDVDInputStreamNavigator::OnNext()
+{
+ if (m_dvdnav && !(IsInMenu() && GetTotalButtons() > 0))
+ {
+ m_dll.dvdnav_next_pg_search(m_dvdnav);
+ }
+}
+
+// we don't allow skipping in menu's cause it will remove menu overlays
+void CDVDInputStreamNavigator::OnPrevious()
+{
+ if (m_dvdnav && !(IsInMenu() && GetTotalButtons() > 0))
+ {
+ m_dll.dvdnav_prev_pg_search(m_dvdnav);
+ }
+}
+
+void CDVDInputStreamNavigator::SkipStill()
+{
+ if (!m_dvdnav)
+ return ;
+ m_dll.dvdnav_still_skip(m_dvdnav);
+}
+
+void CDVDInputStreamNavigator::SkipWait()
+{
+ if (!m_dvdnav) return ;
+ m_dll.dvdnav_wait_skip(m_dvdnav);
+}
+
+CDVDInputStream::ENextStream CDVDInputStreamNavigator::NextStream()
+{
+ if(m_holdmode == HOLDMODE_HELD)
+ m_holdmode = HOLDMODE_SKIP;
+
+ if(m_bEOF)
+ return NEXTSTREAM_NONE;
+ else if(m_lastevent == DVDNAV_VTS_CHANGE)
+ return NEXTSTREAM_OPEN;
+ else
+ return NEXTSTREAM_RETRY;
+}
+
+int CDVDInputStreamNavigator::GetActiveSubtitleStream()
+{
+ int activeStream = 0;
+
+ if (!m_dvdnav)
+ {
+ return activeStream;
+ }
+
+ const int8_t logicalSubStreamId = m_dll.dvdnav_get_active_spu_stream(m_dvdnav);
+ if (logicalSubStreamId < 0)
+ {
+ return activeStream;
+ }
+
+ int subStreamCount = GetSubTitleStreamCount();
+ for (int subpN = 0; subpN < subStreamCount; subpN++)
+ {
+ if (m_dll.dvdnav_get_spu_logical_stream(m_dvdnav, subpN) == logicalSubStreamId)
+ {
+ activeStream = subpN;
+ break;
+ }
+ }
+
+ return activeStream;
+}
+
+SubtitleStreamInfo CDVDInputStreamNavigator::GetSubtitleStreamInfo(const int iId)
+{
+ SubtitleStreamInfo info;
+ if (!m_dvdnav)
+ return info;
+
+ subp_attr_t subp_attributes;
+
+ if (m_dll.dvdnav_get_spu_attr(m_dvdnav, iId, &subp_attributes) == DVDNAV_STATUS_OK)
+ {
+ SetSubtitleStreamName(info, subp_attributes);
+
+ char lang[3];
+ lang[2] = 0;
+ lang[1] = (subp_attributes.lang_code & 255);
+ lang[0] = (subp_attributes.lang_code >> 8) & 255;
+
+ info.language = g_LangCodeExpander.ConvertToISO6392B(lang);
+ }
+
+ return info;
+}
+
+void CDVDInputStreamNavigator::SetSubtitleStreamName(SubtitleStreamInfo &info, const subp_attr_t &subp_attributes)
+{
+ if (subp_attributes.type == DVD_SUBPICTURE_TYPE_LANGUAGE ||
+ subp_attributes.type == DVD_SUBPICTURE_TYPE_NOTSPECIFIED)
+ {
+ switch (subp_attributes.code_extension)
+ {
+ case DVD_SUBPICTURE_LANG_EXT_NOTSPECIFIED:
+ case DVD_SUBPICTURE_LANG_EXT_CHILDRENSCAPTIONS:
+ break;
+
+ case DVD_SUBPICTURE_LANG_EXT_NORMALCAPTIONS:
+ case DVD_SUBPICTURE_LANG_EXT_NORMALCC:
+ case DVD_SUBPICTURE_LANG_EXT_BIGCAPTIONS:
+ case DVD_SUBPICTURE_LANG_EXT_BIGCC:
+ case DVD_SUBPICTURE_LANG_EXT_CHILDRENSCC:
+ info.flags = StreamFlags::FLAG_HEARING_IMPAIRED;
+ break;
+ case DVD_SUBPICTURE_LANG_EXT_FORCED:
+ info.flags = StreamFlags::FLAG_FORCED;
+ break;
+ case DVD_SUBPICTURE_LANG_EXT_NORMALDIRECTORSCOMMENTS:
+ case DVD_SUBPICTURE_LANG_EXT_BIGDIRECTORSCOMMENTS:
+ case DVD_SUBPICTURE_LANG_EXT_CHILDRENDIRECTORSCOMMENTS:
+ info.name = g_localizeStrings.Get(37001);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+int CDVDInputStreamNavigator::GetSubTitleStreamCount()
+{
+ if (!m_dvdnav)
+ {
+ return 0;
+ }
+ return m_dll.dvdnav_get_number_of_streams(m_dvdnav, DVD_SUBTITLE_STREAM);
+}
+
+int CDVDInputStreamNavigator::GetActiveAudioStream()
+{
+ if (!m_dvdnav)
+ {
+ return -1;
+ }
+
+ const int8_t logicalAudioStreamId = m_dll.dvdnav_get_active_audio_stream(m_dvdnav);
+ if (logicalAudioStreamId < 0)
+ {
+ return -1;
+ }
+
+ int activeStream = -1;
+ int audioStreamCount = GetAudioStreamCount();
+ for (int audioN = 0; audioN < audioStreamCount; audioN++)
+ {
+ if (m_dll.dvdnav_get_audio_logical_stream(m_dvdnav, audioN) == logicalAudioStreamId)
+ {
+ activeStream = audioN;
+ break;
+ }
+ }
+
+ return activeStream;
+}
+
+void CDVDInputStreamNavigator::SetAudioStreamName(AudioStreamInfo &info, const audio_attr_t &audio_attributes)
+{
+ switch( audio_attributes.code_extension )
+ {
+ case DVD_AUDIO_LANG_EXT_VISUALLYIMPAIRED:
+ info.name = g_localizeStrings.Get(37000);
+ info.flags = StreamFlags::FLAG_VISUAL_IMPAIRED;
+ break;
+ case DVD_AUDIO_LANG_EXT_DIRECTORSCOMMENTS1:
+ info.name = g_localizeStrings.Get(37001);
+ break;
+ case DVD_AUDIO_LANG_EXT_DIRECTORSCOMMENTS2:
+ info.name = g_localizeStrings.Get(37002);
+ break;
+ case DVD_AUDIO_LANG_EXT_NOTSPECIFIED:
+ case DVD_AUDIO_LANG_EXT_NORMALCAPTIONS:
+ default:
+ break;
+ }
+
+ switch(audio_attributes.audio_format)
+ {
+ case DVD_AUDIO_FORMAT_AC3:
+ info.name += " AC3";
+ info.codecName = "ac3";
+ break;
+ case DVD_AUDIO_FORMAT_UNKNOWN_1:
+ info.name += " UNKNOWN #1";
+ break;
+ case DVD_AUDIO_FORMAT_MPEG:
+ info.name += " MPEG AUDIO";
+ info.codecName = "mp1";
+ break;
+ case DVD_AUDIO_FORMAT_MPEG2_EXT:
+ info.name += " MP2 Ext.";
+ info.codecName = "mp2";
+ break;
+ case DVD_AUDIO_FORMAT_LPCM:
+ info.name += " LPCM";
+ info.codecName = "pcm";
+ break;
+ case DVD_AUDIO_FORMAT_UNKNOWN_5:
+ info.name += " UNKNOWN #5";
+ break;
+ case DVD_AUDIO_FORMAT_DTS:
+ info.name += " DTS";
+ info.codecName = "dts";
+ break;
+ case DVD_AUDIO_FORMAT_SDDS:
+ info.name += " SDDS";
+ break;
+ default:
+ info.name += " Other";
+ break;
+ }
+
+ switch(audio_attributes.channels + 1)
+ {
+ case 1:
+ info.name += " Mono";
+ break;
+ case 2:
+ info.name += " Stereo";
+ break;
+ case 6:
+ info.name += " 5.1";
+ break;
+ case 7:
+ info.name += " 6.1";
+ break;
+ default:
+ char temp[32];
+ sprintf(temp, " %d-chs", audio_attributes.channels + 1);
+ info.name += temp;
+ }
+
+ StringUtils::TrimLeft(info.name);
+}
+
+AudioStreamInfo CDVDInputStreamNavigator::GetAudioStreamInfo(const int iId)
+{
+ AudioStreamInfo info;
+ if (!m_dvdnav)
+ return info;
+
+ audio_attr_t audio_attributes;
+
+ if (m_dll.dvdnav_get_audio_attr(m_dvdnav, iId, &audio_attributes) == DVDNAV_STATUS_OK)
+ {
+ SetAudioStreamName(info, audio_attributes);
+
+ char lang[3];
+ lang[2] = 0;
+ lang[1] = (audio_attributes.lang_code & 255);
+ lang[0] = (audio_attributes.lang_code >> 8) & 255;
+
+ info.language = g_LangCodeExpander.ConvertToISO6392B(lang);
+ info.channels = audio_attributes.channels + 1;
+ }
+
+ return info;
+}
+
+int CDVDInputStreamNavigator::GetAudioStreamCount()
+{
+ if (!m_dvdnav)
+ {
+ return 0;
+ }
+ return m_dll.dvdnav_get_number_of_streams(m_dvdnav, DVD_AUDIO_STREAM);
+}
+
+int CDVDInputStreamNavigator::GetAngleCount()
+{
+ if (!m_dvdnav)
+ return -1;
+
+ int number_of_angles;
+ int current_angle;
+ dvdnav_status_t status = m_dll.dvdnav_get_angle_info(m_dvdnav, &current_angle, &number_of_angles);
+
+ if (status == DVDNAV_STATUS_OK)
+ return number_of_angles;
+ else
+ return -1;
+}
+
+int CDVDInputStreamNavigator::GetActiveAngle()
+{
+ if (!m_dvdnav)
+ return -1;
+
+ int number_of_angles;
+ int current_angle;
+ if (m_dll.dvdnav_get_angle_info(m_dvdnav, &current_angle, &number_of_angles) == DVDNAV_STATUS_ERR)
+ {
+ CLog::LogF(LOGERROR, "Failed to get current angle: {}", m_dll.dvdnav_err_to_string(m_dvdnav));
+ return -1;
+ }
+ return current_angle;
+}
+
+bool CDVDInputStreamNavigator::SetAngle(int angle)
+{
+ if (!m_dvdnav)
+ return false;
+
+ dvdnav_status_t status = m_dll.dvdnav_angle_change(m_dvdnav, angle);
+
+ return (status == DVDNAV_STATUS_OK);
+}
+
+bool CDVDInputStreamNavigator::GetCurrentButtonInfo(CDVDOverlaySpu* pOverlayPicture, CDVDDemuxSPU* pSPU, int iButtonType)
+{
+ int colorAndAlpha[4][4];
+ dvdnav_highlight_area_t hl;
+
+ if (!m_dvdnav)
+ {
+ return false;
+ }
+
+ int button = GetCurrentButton();
+ if (button < 0)
+ {
+ return false;
+ }
+
+ if (DVDNAV_STATUS_OK == m_dll.dvdnav_get_highlight_area(
+ m_dll.dvdnav_get_current_nav_pci(m_dvdnav), button, iButtonType, &hl))
+ {
+ // button cropping information
+ pOverlayPicture->crop_i_x_start = hl.sx;
+ pOverlayPicture->crop_i_x_end = hl.ex;
+ pOverlayPicture->crop_i_y_start = hl.sy;
+ pOverlayPicture->crop_i_y_end = hl.ey;
+ }
+
+ if (pSPU->m_bHasClut)
+ {
+ // get color stored in the highlight area palete using the previously stored clut
+ for (unsigned i = 0; i < 4; i++)
+ {
+ uint8_t* yuvColor = pSPU->m_clut[(hl.palette >> (16 + i * 4)) & 0x0f];
+ uint8_t alpha = (((hl.palette >> (i * 4)) & 0x0f));
+
+ colorAndAlpha[i][0] = yuvColor[0];
+ colorAndAlpha[i][1] = yuvColor[1];
+ colorAndAlpha[i][2] = yuvColor[2];
+ colorAndAlpha[i][3] = alpha;
+ }
+
+ for (int i = 0; i < 4; i++)
+ {
+ pOverlayPicture->highlight_alpha[i] = colorAndAlpha[i][3];
+ for (int j = 0; j < 3; j++)
+ pOverlayPicture->highlight_color[i][j] = colorAndAlpha[i][j];
+ }
+ }
+
+ return true;
+}
+
+int CDVDInputStreamNavigator::GetTotalTime()
+{
+ //We use buffers of this as they can get called from multiple threads, and could block if we are currently reading data
+ return m_iTotalTime;
+}
+
+int CDVDInputStreamNavigator::GetTime()
+{
+ //We use buffers of this as they can get called from multiple threads, and could block if we are currently reading data
+ return m_iTime;
+}
+
+bool CDVDInputStreamNavigator::PosTime(int iTimeInMsec)
+{
+ if( m_dll.dvdnav_jump_to_sector_by_time(m_dvdnav, iTimeInMsec * 90, 0) == DVDNAV_STATUS_ERR )
+ {
+ CLog::Log(LOGDEBUG, "dvdnav: dvdnav_jump_to_sector_by_time failed( {} )",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ return false;
+ }
+ m_iTime = iTimeInMsec;
+ return true;
+}
+
+bool CDVDInputStreamNavigator::SeekChapter(int iChapter)
+{
+ if (!m_dvdnav)
+ return false;
+
+ // cannot allow to return true in case of buttons (overlays) because otherwise back in VideoPlayer FlushBuffers will remove menu overlays
+ // therefore we just skip the request in case there are buttons and return false
+ if (IsInMenu() && GetTotalButtons() > 0)
+ {
+ CLog::Log(LOGDEBUG, "{} - Seeking chapter is not allowed in menu set with buttons",
+ __FUNCTION__);
+ return false;
+ }
+
+ bool enabled = IsSubtitleStreamEnabled();
+ int audio = GetActiveAudioStream();
+ int subtitle = GetActiveSubtitleStream();
+
+ if (iChapter == (m_iPart + 1))
+ {
+ if (m_dll.dvdnav_next_pg_search(m_dvdnav) == DVDNAV_STATUS_ERR)
+ {
+ CLog::Log(LOGERROR, "dvdnav: dvdnav_next_pg_search( {} )",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ return false;
+ }
+ }
+ else if (iChapter == (m_iPart - 1))
+ {
+ if (m_dll.dvdnav_prev_pg_search(m_dvdnav) == DVDNAV_STATUS_ERR)
+ {
+ CLog::Log(LOGERROR, "dvdnav: dvdnav_prev_pg_search( {} )",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ return false;
+ }
+ }
+ else if (m_dll.dvdnav_part_play(m_dvdnav, m_iTitle, iChapter) == DVDNAV_STATUS_ERR)
+ {
+ CLog::Log(LOGERROR, "dvdnav: dvdnav_part_play failed( {} )",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ return false;
+ }
+
+ SetActiveSubtitleStream(subtitle);
+ SetActiveAudioStream(audio);
+ EnableSubtitleStream(enabled);
+ return true;
+}
+
+float CDVDInputStreamNavigator::GetVideoAspectRatio()
+{
+ int iAspect = m_dll.dvdnav_get_video_aspect(m_dvdnav);
+ int iPerm = m_dll.dvdnav_get_video_scale_permission(m_dvdnav);
+
+ //The video scale permissions should give if the source is letterboxed
+ //and such. should be able to give us info that we can zoom in automatically
+ //not sure what to do with it currently
+
+ CLog::Log(LOGINFO, "{} - Aspect wanted: {}, Scale permissions: {}", __FUNCTION__, iAspect, iPerm);
+ switch(iAspect)
+ {
+ case 0: //4:3
+ return 4.0f / 3.0f;
+ case 3: //16:9
+ return 16.0f / 9.0f;
+ default: //Unknown, use libmpeg2
+ return 0.0f;
+ }
+}
+
+void CDVDInputStreamNavigator::EnableSubtitleStream(bool bEnable)
+{
+ if (!m_dvdnav)
+ return;
+
+ m_dll.dvdnav_toggle_spu_stream(m_dvdnav, static_cast<uint8_t>(bEnable));
+}
+
+bool CDVDInputStreamNavigator::IsSubtitleStreamEnabled()
+{
+ if (!m_dvdnav)
+ return false;
+
+ return m_dll.dvdnav_get_active_spu_stream(m_dvdnav) >= 0;
+}
+
+bool CDVDInputStreamNavigator::FillDVDState(DVDState& dvdState)
+{
+ if (!m_dvdnav)
+ {
+ return false;
+ }
+
+ if (m_dll.dvdnav_current_title_program(m_dvdnav, &dvdState.title, &dvdState.pgcn,
+ &dvdState.pgn) == DVDNAV_STATUS_ERR)
+ {
+ CLog::LogF(LOGERROR, "Failed to get current title info ({})",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ return false;
+ }
+
+ int current_angle = GetActiveAngle();
+ if (current_angle == -1)
+ {
+ CLog::LogF(LOGERROR, "Could not detect current angle, ignoring saved state");
+ return false;
+ }
+ dvdState.current_angle = current_angle;
+ dvdState.audio_num = GetActiveAudioStream();
+ dvdState.subp_num = GetActiveSubtitleStream();
+ dvdState.sub_enabled = IsSubtitleStreamEnabled();
+
+ return true;
+}
+
+bool CDVDInputStreamNavigator::GetState(std::string& xmlstate)
+{
+ if( !m_dvdnav )
+ {
+ return false;
+ }
+
+ // do not save state if we are not playing a title stream (e.g. if we are in menus)
+ if (!m_dll.dvdnav_is_domain_vts(m_dvdnav))
+ {
+ return false;
+ }
+
+ DVDState dvdState;
+ if (!FillDVDState(dvdState))
+ {
+ CLog::LogF(LOGWARNING, "Failed to obtain current dvdnav state");
+ return false;
+ }
+
+ if (!m_dvdStateSerializer.DVDStateToXML(xmlstate, dvdState))
+ {
+ CLog::Log(LOGWARNING,
+ "CDVDInputStreamNavigator::SetNavigatorState - Failed to serialize state");
+ return false;
+ }
+
+ return true;
+}
+
+bool CDVDInputStreamNavigator::SetState(const std::string& xmlstate)
+{
+ if (!m_dvdnav)
+ return false;
+
+ DVDState dvdState;
+ if (!m_dvdStateSerializer.XMLToDVDState(dvdState, xmlstate))
+ {
+ CLog::LogF(LOGWARNING, "Failed to deserialize state");
+ return false;
+ }
+
+ m_dll.dvdnav_program_play(m_dvdnav, dvdState.title, dvdState.pgcn, dvdState.pgn);
+ m_dll.dvdnav_angle_change(m_dvdnav, dvdState.current_angle);
+ SetActiveSubtitleStream(dvdState.subp_num);
+ SetActiveAudioStream(dvdState.audio_num);
+ EnableSubtitleStream(dvdState.sub_enabled);
+ return true;
+}
+
+std::string CDVDInputStreamNavigator::GetDVDTitleString()
+{
+ if (!m_dvdnav)
+ return "";
+
+ const char* str = NULL;
+ if (m_dll.dvdnav_get_title_string(m_dvdnav, &str) == DVDNAV_STATUS_OK)
+ return str;
+ else
+ return "";
+}
+
+std::string CDVDInputStreamNavigator::GetDVDSerialString()
+{
+ if (!m_dvdnav)
+ return "";
+
+ const char* str = NULL;
+ if (m_dll.dvdnav_get_serial_string(m_dvdnav, &str) == DVDNAV_STATUS_OK)
+ return str;
+ else
+ return "";
+}
+
+std::string CDVDInputStreamNavigator::GetDVDVolIdString()
+{
+ if (!m_dvdnav)
+ return "";
+
+ const char* volIdTmp = m_dll.dvdnav_get_volid_string(m_dvdnav);
+ if (volIdTmp)
+ {
+ std::string volId{volIdTmp};
+ free(const_cast<char*>(volIdTmp));
+ return volId;
+ }
+ return "";
+}
+
+int64_t CDVDInputStreamNavigator::GetChapterPos(int ch)
+{
+ if (ch == -1 || ch > GetChapterCount())
+ ch = GetChapter();
+
+ std::map<int, std::map<int, int64_t>>::iterator title = m_mapTitleChapters.find(m_iTitle);
+ if (title != m_mapTitleChapters.end())
+ {
+ std::map<int, int64_t>::iterator chapter = title->second.find(ch);
+ if (chapter != title->second.end())
+ return chapter->second;
+ }
+ return 0;
+}
+
+void CDVDInputStreamNavigator::GetVideoResolution(uint32_t* width, uint32_t* height)
+{
+ if (!m_dvdnav) return;
+
+ // for version <= 5.0.3 this functions returns 0 instead of DVDNAV_STATUS_OK and -1 instead of DVDNAV_STATUS_ERR
+ int status = m_dll.dvdnav_get_video_resolution(m_dvdnav, width, height);
+ if (status == -1)
+ {
+ CLog::Log(LOGWARNING,
+ "CDVDInputStreamNavigator::GetVideoResolution - Failed to get resolution ({})",
+ m_dll.dvdnav_err_to_string(m_dvdnav));
+ *width = 0;
+ *height = 0;
+ }
+}
+
+VideoStreamInfo CDVDInputStreamNavigator::GetVideoStreamInfo()
+{
+ VideoStreamInfo info;
+ if (!m_dvdnav)
+ return info;
+
+ info.angles = GetAngleCount();
+ info.videoAspectRatio = GetVideoAspectRatio();
+ uint32_t width = 0;
+ uint32_t height = 0;
+ GetVideoResolution(&width, &height);
+
+ info.width = static_cast<int>(width);
+ info.height = static_cast<int>(height);
+
+ // Until we add get_video_attr or get_video_codec we can't distinguish MPEG-1 (h261)
+ // from MPEG-2 (h262). The latter is far more common, so use this.
+ info.codecName = "mpeg2";
+
+ return info;
+}
+
+int dvd_inputstreamnavigator_cb_seek(void * p_stream, uint64_t i_pos)
+{
+ CDVDInputStreamFile *lpstream = reinterpret_cast<CDVDInputStreamFile*>(p_stream);
+ if (lpstream->Seek(i_pos, SEEK_SET) >= 0)
+ return 0;
+ else
+ return -1;
+}
+
+int dvd_inputstreamnavigator_cb_read(void * p_stream, void * buffer, int i_read)
+{
+ CDVDInputStreamFile *lpstream = reinterpret_cast<CDVDInputStreamFile*>(p_stream);
+
+ int i_ret = 0;
+ while (i_ret < i_read)
+ {
+ int i_r;
+ i_r = lpstream->Read(reinterpret_cast<uint8_t *>(buffer) + i_ret, i_read - i_ret);
+ if (i_r < 0)
+ {
+ CLog::Log(LOGERROR,"read error dvd_inputstreamnavigator_cb_read");
+ return i_r;
+ }
+ if (i_r == 0)
+ break;
+
+ i_ret += i_r;
+ }
+
+ return i_ret;
+}
+
+void dvd_logger(void* priv, dvdnav_logger_level_t level, const char* fmt, va_list va)
+{
+ const std::string message = StringUtils::FormatV(fmt, va);
+ auto logLevel = LOGDEBUG;
+ switch (level)
+ {
+ case DVDNAV_LOGGER_LEVEL_INFO:
+ logLevel = LOGINFO;
+ break;
+ case DVDNAV_LOGGER_LEVEL_ERROR:
+ logLevel = LOGERROR;
+ break;
+ case DVDNAV_LOGGER_LEVEL_WARN:
+ logLevel = LOGWARNING;
+ break;
+ case DVDNAV_LOGGER_LEVEL_DEBUG:
+ logLevel = LOGDEBUG;
+ break;
+ default:
+ break;
+ };
+ CLog::Log(logLevel, "Libdvd: {}", message);
+}
+
+int dvd_inputstreamnavigator_cb_readv(void * p_stream, void * p_iovec, int i_blocks)
+{
+ // NOTE/TODO: this vectored read callback somehow doesn't seem to be called by libdvdnav.
+ // Therefore, the code below isn't really tested, but inspired from the libc_readv code for Win32 in libdvdcss (device.c:713).
+ CDVDInputStreamFile *lpstream = reinterpret_cast<CDVDInputStreamFile*>(p_stream);
+ const struct iovec* lpiovec = reinterpret_cast<const struct iovec*>(p_iovec);
+
+ int i_index, i_len, i_total = 0;
+ unsigned char *p_base;
+ int i_bytes;
+
+ for (i_index = i_blocks; i_index; i_index--, lpiovec++)
+ {
+ i_len = lpiovec->iov_len;
+ p_base = reinterpret_cast<unsigned char*>(lpiovec->iov_base);
+
+ if (i_len <= 0)
+ continue;
+
+ i_bytes = lpstream->Read(p_base, i_len);
+ if (i_bytes < 0)
+ return -1;
+ else
+ i_total += i_bytes;
+
+ if (i_bytes != i_len)
+ {
+ /* We reached the end of the file or a signal interrupted
+ * the read. Return a partial read. */
+ int i_seek = lpstream->Seek(i_total,0);
+ if (i_seek < 0)
+ return i_seek;
+
+ /* We have to return now so that i_pos isn't clobbered */
+ return i_total;
+ }
+ }
+ return i_total;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.h
new file mode 100644
index 0000000..d33364b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.h
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2005-2022 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 "../DVDCodecs/Overlay/DVDOverlaySpu.h"
+#include "../IVideoPlayer.h"
+#include "DVDDemuxers/DVDDemux.h"
+#include "DVDInputStream.h"
+#include "DVDInputStreamFile.h"
+#include "DVDStateSerializer.h"
+#include "DllDvdNav.h"
+#include "cores/MenuType.h"
+#include "utils/Geometry.h"
+
+#include <string>
+
+#define DVD_VIDEO_BLOCKSIZE DVD_VIDEO_LB_LEN // 2048 bytes
+
+#define NAVRESULT_NOP 0x00000001 // keep processing messages
+#define NAVRESULT_DATA 0x00000002 // return data to demuxer
+#define NAVRESULT_ERROR 0x00000003 // return read error to demuxer
+#define NAVRESULT_HOLD 0x00000004 // return eof to demuxer
+
+#define LIBDVDNAV_BUTTON_NORMAL 0
+#define LIBDVDNAV_BUTTON_CLICKED 1
+
+#define DVDNAV_ERROR -1
+
+class CDVDDemuxSPU;
+class CSPUInfo;
+class CDVDOverlayPicture;
+
+struct dvdnav_s;
+
+class CDVDInputStreamNavigator
+ : public CDVDInputStream
+ , public CDVDInputStream::IDisplayTime
+ , public CDVDInputStream::IChapter
+ , public CDVDInputStream::IPosTime
+ , public CDVDInputStream::IMenus
+{
+public:
+ CDVDInputStreamNavigator(IVideoPlayer* player, const CFileItem& fileitem);
+ ~CDVDInputStreamNavigator() override;
+
+ bool Open() override;
+ void Close() override;
+ int Read(uint8_t* buf, int buf_size) override;
+ int64_t Seek(int64_t offset, int whence) override;
+ int GetBlockSize() override { return DVDSTREAM_BLOCK_SIZE_DVD; }
+ bool IsEOF() override { return m_bEOF; }
+ int64_t GetLength() override { return 0; }
+ ENextStream NextStream() override ;
+
+ void ActivateButton() override;
+ void SelectButton(int iButton) override;
+ void SkipStill() override;
+ void SkipWait();
+ void OnUp() override;
+ void OnDown() override;
+ void OnLeft() override;
+ void OnRight() override;
+
+ /*! \brief Open the Menu
+ * \return true if the menu is successfully opened, false otherwise
+ */
+ bool OnMenu() override;
+
+ void OnBack() override;
+ void OnNext() override;
+ void OnPrevious() override;
+ bool OnMouseMove(const CPoint &point) override;
+ bool OnMouseClick(const CPoint &point) override;
+
+ int GetCurrentButton() override;
+ int GetTotalButtons() override;
+ bool GetCurrentButtonInfo(CDVDOverlaySpu* pOverlayPicture, CDVDDemuxSPU* pSPU, int iButtonType /* 0 = selection, 1 = action (clicked)*/);
+
+ /*!
+ * \brief Get the supported menu type
+ * \return The supported menu type
+ */
+ MenuType GetSupportedMenuType() override { return MenuType::NATIVE; }
+
+ bool IsInMenu() override { return m_bInMenu; }
+ double GetTimeStampCorrection() override { return (double)(m_iVobUnitCorrection * 1000) / 90; }
+
+ int GetActiveSubtitleStream();
+ int GetSubTitleStreamCount();
+ SubtitleStreamInfo GetSubtitleStreamInfo(const int iId);
+
+ bool SetActiveSubtitleStream(int iId);
+ void EnableSubtitleStream(bool bEnable);
+ bool IsSubtitleStreamEnabled();
+
+ int GetActiveAudioStream();
+ int GetAudioStreamCount();
+ int GetActiveAngle();
+ bool SetAngle(int angle);
+ bool SetActiveAudioStream(int iId);
+ AudioStreamInfo GetAudioStreamInfo(const int iId);
+
+ bool GetState(std::string &xmlstate) override;
+ bool SetState(const std::string &xmlstate) override;
+
+ int GetChapter() override { return m_iPart; } // the current part in the current title
+ int GetChapterCount() override { return m_iPartCount; } // the number of parts in the current title
+ void GetChapterName(std::string& name, int idx=-1) override {};
+ int64_t GetChapterPos(int ch=-1) override;
+ bool SeekChapter(int iChapter) override;
+
+ CDVDInputStream::IDisplayTime* GetIDisplayTime() override { return this; }
+ int GetTotalTime() override; // the total time in milli seconds
+ int GetTime() override; // the current position in milli seconds
+
+ float GetVideoAspectRatio();
+
+ CDVDInputStream::IPosTime* GetIPosTime() override { return this; }
+ bool PosTime(int iTimeInMsec) override; //seek within current pg(c)
+
+ std::string GetDVDTitleString();
+
+ /*!
+ * \brief Get the DVD volume ID string. Alternative to the dvd title (since some DVD authors
+ even forget to set it).
+ * \return The DVD volume id
+ */
+ std::string GetDVDVolIdString();
+
+ std::string GetDVDSerialString();
+
+ void CheckButtons();
+
+ VideoStreamInfo GetVideoStreamInfo();
+
+protected:
+
+ int ProcessBlock(uint8_t* buffer, int* read);
+
+ static void SetAudioStreamName(AudioStreamInfo &info, const audio_attr_t &audio_attributes);
+ static void SetSubtitleStreamName(SubtitleStreamInfo &info, const subp_attr_t &subp_attributes);
+
+ int GetAngleCount();
+ void GetVideoResolution(uint32_t * width, uint32_t * height);
+
+ /*! \brief Provided a pod DVDState struct, fill it with the current dvdnav state
+ * \param[in,out] dvdstate the DVD state struct to be filled
+ * \return true if it was possible to fill the state struct based on the current dvdnav state, false otherwise
+ */
+ bool FillDVDState(DVDState& dvdstate);
+
+ DllDvdNav m_dll;
+ bool m_bCheckButtons;
+ bool m_bEOF;
+
+ int m_holdmode;
+
+ int m_iTotalTime;
+ int m_iTime;
+ int64_t m_iCellStart; // start time of current cell in pts units (90khz clock)
+
+ bool m_bInMenu;
+
+ int64_t m_iVobUnitStart;
+ int64_t m_iVobUnitStop;
+ int64_t m_iVobUnitCorrection;
+
+ int m_iTitleCount;
+ int m_iTitle;
+
+ int m_iPartCount;
+ int m_iPart;
+
+ struct dvdnav_s* m_dvdnav;
+ dvdnav_stream_cb m_dvdnav_stream_cb;
+ std::unique_ptr<CDVDInputStreamFile> m_pstream;
+
+ IVideoPlayer* m_pVideoPlayer;
+
+ uint8_t m_lastblock[DVD_VIDEO_BLOCKSIZE];
+ int m_lastevent;
+
+ std::map<int, std::map<int, int64_t>> m_mapTitleChapters;
+
+ /*! DVD state serializer handler */
+ CDVDStateSerializer m_dvdStateSerializer;
+};
+
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.cpp
new file mode 100644
index 0000000..1f01861
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.cpp
@@ -0,0 +1,169 @@
+/*
+ * 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 "DVDInputStreamStack.h"
+
+#include "FileItem.h"
+#include "filesystem/File.h"
+#include "filesystem/StackDirectory.h"
+#include "utils/log.h"
+
+#include <limits.h>
+
+using namespace XFILE;
+
+CDVDInputStreamStack::CDVDInputStreamStack(const CFileItem& fileitem) : CDVDInputStream(DVDSTREAM_TYPE_FILE, fileitem)
+{
+ m_eof = true;
+ m_pos = 0;
+ m_length = 0;
+}
+
+CDVDInputStreamStack::~CDVDInputStreamStack()
+{
+ Close();
+}
+
+bool CDVDInputStreamStack::IsEOF()
+{
+ return m_eof;
+}
+
+bool CDVDInputStreamStack::Open()
+{
+ if (!CDVDInputStream::Open())
+ return false;
+
+ CStackDirectory dir;
+ CFileItemList items;
+
+ const CURL pathToUrl(m_item.GetDynPath());
+ if(!dir.GetDirectory(pathToUrl, items))
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamStack::Open - failed to get list of stacked items");
+ return false;
+ }
+
+ m_length = 0;
+ m_eof = false;
+
+ for(int index = 0; index < items.Size(); index++)
+ {
+ TFile file(new CFile());
+
+ if (!file->Open(items[index]->GetDynPath(), READ_TRUNCATED))
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamStack::Open - failed to open stack part '{}' - skipping",
+ items[index]->GetDynPath());
+ continue;
+ }
+ TSeg segment;
+ segment.file = file;
+ segment.length = file->GetLength();
+
+ if(segment.length <= 0)
+ {
+ CLog::Log(LOGERROR,
+ "CDVDInputStreamStack::Open - failed to get file length for '{}' - skipping",
+ items[index]->GetDynPath());
+ continue;
+ }
+
+ m_length += segment.length;
+
+ m_files.push_back(segment);
+ }
+
+ if(m_files.empty())
+ return false;
+
+ m_file = m_files[0].file;
+ m_eof = false;
+
+ return true;
+}
+
+// close file and reset everything
+void CDVDInputStreamStack::Close()
+{
+ CDVDInputStream::Close();
+ m_files.clear();
+ m_file.reset();
+ m_eof = true;
+}
+
+int CDVDInputStreamStack::Read(uint8_t* buf, int buf_size)
+{
+ if(m_file == NULL || m_eof)
+ return 0;
+
+ unsigned int ret = m_file->Read(buf, buf_size);
+
+ if(ret > INT_MAX)
+ return -1;
+
+ if(ret == 0)
+ {
+ m_eof = true;
+ if(Seek(m_pos, SEEK_SET) < 0)
+ {
+ CLog::Log(LOGERROR, "CDVDInputStreamStack::Read - failed to seek into next file");
+ m_eof = true;
+ m_file.reset();
+ return -1;
+ }
+ }
+
+ m_pos += ret;
+
+ return (int)ret;
+}
+
+int64_t CDVDInputStreamStack::Seek(int64_t offset, int whence)
+{
+ int64_t pos, len;
+
+ if (whence == SEEK_SET)
+ pos = offset;
+ else if(whence == SEEK_CUR)
+ pos = offset + m_pos;
+ else if(whence == SEEK_END)
+ pos = offset + m_length;
+ else
+ return -1;
+
+ len = 0;
+ for(TSegVec::iterator it = m_files.begin(); it != m_files.end(); ++it)
+ {
+ if(len + it->length > pos)
+ {
+ TFile file = it->file;
+ int64_t file_pos = pos - len;
+ if(file->GetPosition() != file_pos)
+ {
+ if(file->Seek(file_pos, SEEK_SET) < 0)
+ return false;
+ }
+
+ m_file = file;
+ m_pos = pos;
+ m_eof = false;
+ return pos;
+ }
+ len += it->length;
+ }
+
+ return -1;
+}
+
+int64_t CDVDInputStreamStack::GetLength()
+{
+ return m_length;
+}
+
+
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.h
new file mode 100644
index 0000000..02befe8
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDInputStreamStack.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "DVDInputStream.h"
+
+#include <memory>
+#include <vector>
+
+class CDVDInputStreamStack : public CDVDInputStream
+{
+public:
+ explicit CDVDInputStreamStack(const CFileItem& fileitem);
+ ~CDVDInputStreamStack() override;
+
+ bool Open() override;
+ void Close() override;
+ int Read(uint8_t* buf, int buf_size) override;
+ int64_t Seek(int64_t offset, int whence) override;
+ bool IsEOF() override;
+ int64_t GetLength() override;
+
+protected:
+
+ typedef std::shared_ptr<XFILE::CFile> TFile;
+
+ struct TSeg
+ {
+ TFile file;
+ int64_t length;
+ };
+
+ typedef std::vector<TSeg> TSegVec;
+
+ TSegVec m_files; ///< collection of open ptr's to all files in stack
+ TFile m_file; ///< currently active file
+ bool m_eof;
+ int64_t m_pos;
+ int64_t m_length;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDStateSerializer.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDStateSerializer.cpp
new file mode 100644
index 0000000..c09779b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDStateSerializer.cpp
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2005-2022 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 "DVDStateSerializer.h"
+
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <charconv>
+#include <cstring>
+#include <sstream>
+
+namespace
+{
+// Serializer version - used to avoid processing deprecated/legacy schemas
+constexpr int DVDSTATESERIALIZER_VERSION = 2;
+}
+
+bool CDVDStateSerializer::DVDStateToXML(std::string& xmlstate, const DVDState& state)
+{
+ CXBMCTinyXML xmlDoc{"navstate"};
+
+ TiXmlElement eRoot{"navstate"};
+ eRoot.SetAttribute("version", DVDSTATESERIALIZER_VERSION);
+
+ AddXMLElement(eRoot, "title", std::to_string(state.title));
+ AddXMLElement(eRoot, "pgn", std::to_string(state.pgn));
+ AddXMLElement(eRoot, "pgcn", std::to_string(state.pgcn));
+ AddXMLElement(eRoot, "current_angle", std::to_string(state.current_angle));
+ AddXMLElement(eRoot, "audio_num", std::to_string(state.audio_num));
+ AddXMLElement(eRoot, "subp_num", std::to_string(state.subp_num));
+ AddXMLElement(eRoot, "sub_enabled", state.sub_enabled ? "true" : "false");
+ xmlDoc.InsertEndChild(eRoot);
+
+ std::stringstream stream;
+ stream << xmlDoc;
+ xmlstate = stream.str();
+ return true;
+}
+
+bool CDVDStateSerializer::XMLToDVDState(DVDState& state, const std::string& xmlstate)
+{
+ CXBMCTinyXML xmlDoc;
+
+ xmlDoc.Parse(xmlstate);
+ if (xmlDoc.Error())
+ return false;
+
+ TiXmlHandle hRoot(xmlDoc.RootElement());
+ if (!hRoot.Element() || !StringUtils::EqualsNoCase(hRoot.Element()->Value(), "navstate"))
+ {
+ CLog::LogF(LOGERROR, "Failed to deserialize dvd state - failed to detect root element.");
+ return false;
+ }
+
+ auto version = hRoot.Element()->Attribute("version");
+ if (!version || !StringUtils::EqualsNoCase(version, std::to_string(DVDSTATESERIALIZER_VERSION)))
+ {
+ CLog::LogF(LOGERROR, "Failed to deserialize dvd state - incompatible serializer version.");
+ return false;
+ }
+
+ const TiXmlElement* childElement = hRoot.Element()->FirstChildElement();
+ while (childElement)
+ {
+ const std::string property = childElement->Value();
+ if (property == "title")
+ {
+ std::from_chars(childElement->GetText(),
+ childElement->GetText() + std::strlen(childElement->GetText()), state.title);
+ }
+ else if (property == "pgn")
+ {
+ std::from_chars(childElement->GetText(),
+ childElement->GetText() + std::strlen(childElement->GetText()), state.pgn);
+ }
+ else if (property == "pgcn")
+ {
+ std::from_chars(childElement->GetText(),
+ childElement->GetText() + std::strlen(childElement->GetText()), state.pgcn);
+ }
+ else if (property == "current_angle")
+ {
+ std::from_chars(childElement->GetText(),
+ childElement->GetText() + std::strlen(childElement->GetText()),
+ state.current_angle);
+ }
+ else if (property == "subp_num")
+ {
+ std::from_chars(childElement->GetText(),
+ childElement->GetText() + std::strlen(childElement->GetText()),
+ state.subp_num);
+ }
+ else if (property == "audio_num")
+ {
+ std::from_chars(childElement->GetText(),
+ childElement->GetText() + std::strlen(childElement->GetText()),
+ state.audio_num);
+ }
+ else if (property == "sub_enabled")
+ {
+ state.sub_enabled = StringUtils::EqualsNoCase(childElement->GetText(), "true");
+ }
+ else
+ {
+ CLog::LogF(LOGWARNING, "Unmapped dvd state property {}, ignored.", childElement->Value());
+ }
+ childElement = childElement->NextSiblingElement();
+ }
+ return true;
+}
+
+void CDVDStateSerializer::AddXMLElement(TiXmlElement& root,
+ const std::string& name,
+ const std::string& value)
+{
+ TiXmlElement xmlElement{name};
+ TiXmlText xmlElementValue = value;
+ xmlElement.InsertEndChild(xmlElementValue);
+ root.InsertEndChild(xmlElement);
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DVDStateSerializer.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDStateSerializer.h
new file mode 100644
index 0000000..debba10
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DVDStateSerializer.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2005-2022 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 <string>
+
+class TiXmlElement;
+
+/*! \brief Pod structure which represents the current dvd state with respect to dvdnav properties */
+struct DVDState
+{
+ /*! The current title being played */
+ int32_t title = -1;
+ /*! Program number */
+ int32_t pgn = -1;
+ /*! Program cell number */
+ int32_t pgcn = -1;
+ /*! Current playing angle */
+ int32_t current_angle = -1;
+ /*! Physical subtitle id set in dvdnav */
+ int8_t subp_num = -1;
+ /*! Physical audio stream id set in dvdnav */
+ int8_t audio_num = -1;
+ /*! If subtitles are enabled or disabled */
+ bool sub_enabled = false;
+};
+
+/*! \brief Auxiliar class to serialize/deserialize the dvd state (into/from XML)
+*/
+class CDVDStateSerializer
+{
+public:
+ /*! \brief Default constructor */
+ CDVDStateSerializer() = default;
+
+ /*! \brief Default destructor */
+ ~CDVDStateSerializer() = default;
+
+ /*! \brief Provided the state in xml format, fills a DVDState struct representing the DVD state and returns the
+ * success status of the operation
+ * \param[in,out] state the DVD state struct to be filled
+ * \param xmlstate a string describing the dvd state (XML)
+ * \return true if it was possible to fill the state struct based on the XML content, false otherwise
+ */
+ bool XMLToDVDState(DVDState& state, const std::string& xmlstate);
+
+ /*! \brief Provided the DVDState struct of the current playing dvd, serializes the struct to XML
+ * \param[in,out] xmlstate a string describing the dvd state (XML)
+ * \param state the DVD state struct
+ * \return true if it was possible to serialize the struct into XML, false otherwise
+ */
+ bool DVDStateToXML(std::string& xmlstate, const DVDState& state);
+
+private:
+ /*! \brief Appends a new element with the given name and value to a provided root XML element
+ * \param[in,out] root the root xml element to append the new element
+ * \param name the new element name
+ * \param value the new element value
+ */
+ void AddXMLElement(TiXmlElement& root, const std::string& name, const std::string& value);
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/DllDvdNav.h b/xbmc/cores/VideoPlayer/DVDInputStreams/DllDvdNav.h
new file mode 100644
index 0000000..7a0c149
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/DllDvdNav.h
@@ -0,0 +1,275 @@
+/*
+ * 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
+
+extern "C" {
+#define DVDNAV_COMPILE
+ #include <stdint.h>
+
+ #include "dvdnav/dvdnav.h"
+
+ #ifndef WIN32
+ #define WIN32
+ #endif // WIN32
+
+ #ifndef HAVE_CONFIG_H
+ #define HAVE_CONFIG_H
+ #endif
+
+ #include "dvdnav/dvd_types.h"
+
+ #ifdef WIN32 // WIN32INCLUDES
+ #undef HAVE_CONFIG_H
+ #endif
+}
+#include "DynamicDll.h"
+
+class DllDvdNavInterface
+{
+public:
+ virtual ~DllDvdNavInterface() = default;
+ virtual dvdnav_status_t dvdnav_open(dvdnav_t **dest, const char *path)=0;
+ virtual dvdnav_status_t dvdnav_open2(dvdnav_t** dest,
+ void*,
+ const dvdnav_logger_cb*,
+ const char* path) = 0;
+ virtual dvdnav_status_t dvdnav_open_stream(dvdnav_t **dest, void *stream, dvdnav_stream_cb *stream_cb) = 0;
+ virtual dvdnav_status_t dvdnav_open_stream2(dvdnav_t** dest,
+ void* stream,
+ const dvdnav_logger_cb*,
+ dvdnav_stream_cb* stream_cb) = 0;
+ virtual dvdnav_status_t dvdnav_close(dvdnav_t *self)=0;
+ virtual dvdnav_status_t dvdnav_reset(dvdnav_t *self)=0;
+ virtual const char* dvdnav_err_to_string(dvdnav_t *self)=0;
+ virtual dvdnav_status_t dvdnav_set_readahead_flag(dvdnav_t *self, int32_t read_ahead_flag)=0;
+ virtual dvdnav_status_t dvdnav_set_PGC_positioning_flag(dvdnav_t *self, int32_t pgc_based_flag)=0;
+ virtual dvdnav_status_t dvdnav_get_next_cache_block(dvdnav_t *self, uint8_t **buf, int32_t *event, int32_t *len)=0;
+ virtual dvdnav_status_t dvdnav_free_cache_block(dvdnav_t *self, unsigned char *buf)=0;
+ virtual dvdnav_status_t dvdnav_still_skip(dvdnav_t *self)=0;
+ virtual dvdnav_status_t dvdnav_wait_skip(dvdnav_t *self)=0;
+ virtual dvdnav_status_t dvdnav_stop(dvdnav_t *self)=0;
+ virtual dvdnav_status_t dvdnav_button_select(dvdnav_t *self, pci_t *pci, int32_t button)=0;
+ virtual dvdnav_status_t dvdnav_button_activate(dvdnav_t *self, pci_t *pci)=0;
+ virtual dvdnav_status_t dvdnav_upper_button_select(dvdnav_t *self, pci_t *pci)=0;
+ virtual dvdnav_status_t dvdnav_lower_button_select(dvdnav_t *self, pci_t *pci)=0;
+ virtual dvdnav_status_t dvdnav_right_button_select(dvdnav_t *self, pci_t *pci)=0;
+ virtual dvdnav_status_t dvdnav_left_button_select(dvdnav_t *self, pci_t *pci)=0;
+ virtual dvdnav_status_t dvdnav_sector_search(dvdnav_t *self, uint64_t offset, int32_t origin)=0;
+ virtual pci_t* dvdnav_get_current_nav_pci(dvdnav_t *self)=0;
+ virtual dsi_t* dvdnav_get_current_nav_dsi(dvdnav_t *self)=0;
+ virtual dvdnav_status_t dvdnav_get_position(dvdnav_t *self, uint32_t *pos, uint32_t *len)=0;
+ virtual dvdnav_status_t dvdnav_current_title_info(dvdnav_t *self, int32_t *title, int32_t *part)=0;
+ virtual dvdnav_status_t dvdnav_spu_language_select(dvdnav_t *self, char *code)=0;
+ virtual dvdnav_status_t dvdnav_audio_language_select(dvdnav_t *self, char *code)=0;
+ virtual dvdnav_status_t dvdnav_menu_language_select(dvdnav_t *self, char *code)=0;
+ virtual int8_t dvdnav_is_domain_vts(dvdnav_t *self)=0;
+ virtual int8_t dvdnav_get_active_spu_stream(dvdnav_t *self)=0;
+ virtual int8_t dvdnav_get_spu_logical_stream(dvdnav_t *self, uint8_t subp_num)=0;
+ virtual uint16_t dvdnav_spu_stream_to_lang(dvdnav_t *self, uint8_t stream)=0;
+ virtual dvdnav_status_t dvdnav_get_current_highlight(dvdnav_t *self, int32_t *button)=0;
+ virtual dvdnav_status_t dvdnav_menu_call(dvdnav_t *self, DVDMenuID_t menu)=0;
+ virtual dvdnav_status_t dvdnav_prev_pg_search(dvdnav_t *self)=0;
+ virtual dvdnav_status_t dvdnav_next_pg_search(dvdnav_t *self)=0;
+ virtual dvdnav_status_t dvdnav_get_highlight_area(pci_t *nav_pci , int32_t button, int32_t mode, dvdnav_highlight_area_t *highlight)=0;
+ virtual dvdnav_status_t dvdnav_go_up(dvdnav_t *self)=0;
+ virtual int8_t dvdnav_get_active_audio_stream(dvdnav_t *self)=0;
+ virtual uint16_t dvdnav_audio_stream_to_lang(dvdnav_t *self, uint8_t stream)=0;
+ virtual int8_t dvdnav_get_audio_logical_stream(dvdnav_t *self, uint8_t audio_num)=0;
+ virtual dvdnav_status_t dvdnav_set_region_mask(dvdnav_t *self, int32_t region_mask)=0;
+ virtual uint8_t dvdnav_get_video_aspect(dvdnav_t *self)=0;
+ virtual uint8_t dvdnav_get_video_scale_permission(dvdnav_t *self)=0;
+ virtual dvdnav_status_t dvdnav_get_number_of_titles(dvdnav_t *self, int32_t *titles)=0;
+ virtual dvdnav_status_t dvdnav_get_number_of_parts(dvdnav_t *self, int32_t title, int32_t *parts)=0;
+ virtual dvdnav_status_t dvdnav_title_play(dvdnav_t *self, int32_t title)=0;
+ virtual dvdnav_status_t dvdnav_part_play(dvdnav_t *self, int32_t title, int32_t part)=0;
+ virtual dvdnav_status_t dvdnav_get_audio_attr(dvdnav_t * self, int32_t streamid, audio_attr_t* audio_attributes)=0;
+ virtual dvdnav_status_t dvdnav_get_spu_attr(dvdnav_t * self, int32_t streamid, subp_attr_t* stitle_attributes)=0;
+ virtual dvdnav_status_t dvdnav_jump_to_sector_by_time(dvdnav_t* self,
+ uint64_t offset,
+ int32_t origin) = 0;
+ virtual int64_t dvdnav_convert_time(dvd_time_t *time)=0;
+ virtual dvdnav_status_t dvdnav_get_angle_info(dvdnav_t *self, int32_t *current_angle,int32_t *number_of_angles)=0;
+ virtual dvdnav_status_t dvdnav_angle_change(dvdnav_t *self, int32_t angle) = 0;
+ virtual dvdnav_status_t dvdnav_mouse_activate(dvdnav_t *self, pci_t *pci, int32_t x, int32_t y)=0;
+ virtual dvdnav_status_t dvdnav_mouse_select(dvdnav_t *self, pci_t *pci, int32_t x, int32_t y)=0;
+ virtual dvdnav_status_t dvdnav_get_title_string(dvdnav_t *self, const char **title_str)=0;
+ virtual dvdnav_status_t dvdnav_get_serial_string(dvdnav_t *self, const char **serial_str)=0;
+ virtual const char* dvdnav_get_volid_string(dvdnav_t* self) = 0;
+ virtual dvdnav_status_t dvdnav_get_disk_region_mask(dvdnav_t* self, int32_t* region_mask) = 0;
+ virtual uint32_t dvdnav_describe_title_chapters(dvdnav_t* self, uint32_t title, uint64_t** times, uint64_t* duration)=0;
+ virtual int64_t dvdnav_get_current_time(dvdnav_t* self) = 0;
+ virtual int dvdnav_get_video_resolution(dvdnav_t* self, uint32_t* width, uint32_t* height)=0;
+ virtual int8_t dvdnav_get_number_of_streams(dvdnav_t* self, dvdnav_stream_type_t stream_type) = 0;
+ virtual dvdnav_status_t dvdnav_toggle_spu_stream(dvdnav_t* self, uint8_t visibility) = 0;
+ virtual dvdnav_status_t dvdnav_set_active_stream(dvdnav_t* self,
+ uint8_t stream_num,
+ dvdnav_stream_type_t stream_type) = 0;
+ virtual dvdnav_status_t dvdnav_program_play(dvdnav_t* self,
+ int32_t title,
+ int32_t pgcn,
+ int32_t pgn) = 0;
+ virtual dvdnav_status_t dvdnav_current_title_program(dvdnav_t* self,
+ int32_t* title,
+ int32_t* pgcn,
+ int32_t* pgn) = 0;
+};
+
+class DllDvdNav : public DllDynamic, DllDvdNavInterface
+{
+ DECLARE_DLL_WRAPPER(DllDvdNav, DLL_PATH_LIBDVDNAV)
+
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_open, (dvdnav_t **p1, const char *p2))
+ DEFINE_METHOD4(dvdnav_status_t,
+ dvdnav_open2,
+ (dvdnav_t * *p1, void* p2, const dvdnav_logger_cb* p3, const char* p4))
+ DEFINE_METHOD3(dvdnav_status_t, dvdnav_open_stream, (dvdnav_t **p1, void *p2, dvdnav_stream_cb *p3))
+ DEFINE_METHOD4(dvdnav_status_t,
+ dvdnav_open_stream2,
+ (dvdnav_t * *p1, void* p2, const dvdnav_logger_cb* p3, dvdnav_stream_cb* p4))
+ DEFINE_METHOD1(dvdnav_status_t, dvdnav_close, (dvdnav_t *p1))
+ DEFINE_METHOD1(dvdnav_status_t, dvdnav_reset, (dvdnav_t *p1))
+ DEFINE_METHOD1(const char*, dvdnav_err_to_string, (dvdnav_t *p1))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_set_readahead_flag, (dvdnav_t *p1, int32_t p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_set_PGC_positioning_flag, (dvdnav_t *p1, int32_t p2))
+ DEFINE_METHOD4(dvdnav_status_t, dvdnav_get_next_cache_block, (dvdnav_t *p1, uint8_t **p2, int32_t *p3, int32_t *p4))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_free_cache_block, (dvdnav_t *p1, unsigned char *p2))
+ DEFINE_METHOD1(dvdnav_status_t, dvdnav_still_skip, (dvdnav_t *p1))
+ DEFINE_METHOD1(dvdnav_status_t, dvdnav_wait_skip, (dvdnav_t *p1))
+ DEFINE_METHOD1(dvdnav_status_t, dvdnav_stop, (dvdnav_t *p1))
+ DEFINE_METHOD3(dvdnav_status_t, dvdnav_button_select, (dvdnav_t *p1, pci_t *p2, int32_t p3))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_button_activate,(dvdnav_t *p1, pci_t *p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_upper_button_select, (dvdnav_t *p1, pci_t *p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_lower_button_select, (dvdnav_t *p1, pci_t *p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_right_button_select, (dvdnav_t *p1, pci_t *p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_left_button_select, (dvdnav_t *p1, pci_t *p2))
+ DEFINE_METHOD3(dvdnav_status_t, dvdnav_sector_search, (dvdnav_t *p1, uint64_t p2, int32_t p3))
+ DEFINE_METHOD1(pci_t*, dvdnav_get_current_nav_pci, (dvdnav_t *p1))
+ DEFINE_METHOD1(dsi_t*, dvdnav_get_current_nav_dsi, (dvdnav_t *p1))
+ DEFINE_METHOD3(dvdnav_status_t, dvdnav_get_position, (dvdnav_t *p1, uint32_t *p2, uint32_t *p3))
+ DEFINE_METHOD3(dvdnav_status_t, dvdnav_current_title_info, (dvdnav_t *p1, int32_t *p2, int32_t *p3))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_spu_language_select, (dvdnav_t *p1, char *p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_audio_language_select, (dvdnav_t *p1, char *p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_menu_language_select, (dvdnav_t *p1, char *p2))
+ DEFINE_METHOD1(int8_t, dvdnav_is_domain_vts, (dvdnav_t *p1))
+ DEFINE_METHOD1(int8_t, dvdnav_get_active_spu_stream, (dvdnav_t *p1))
+ DEFINE_METHOD2(int8_t, dvdnav_get_spu_logical_stream, (dvdnav_t *p1, uint8_t p2))
+ DEFINE_METHOD2(uint16_t, dvdnav_spu_stream_to_lang, (dvdnav_t *p1, uint8_t p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_get_current_highlight, (dvdnav_t *p1, int32_t *p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_menu_call, (dvdnav_t *p1, DVDMenuID_t p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_get_disk_region_mask, (dvdnav_t * p1, int32_t* p2))
+ DEFINE_METHOD1(dvdnav_status_t, dvdnav_prev_pg_search, (dvdnav_t *p1))
+ DEFINE_METHOD1(dvdnav_status_t, dvdnav_next_pg_search, (dvdnav_t *p1))
+ DEFINE_METHOD4(dvdnav_status_t, dvdnav_get_highlight_area, (pci_t *p1, int32_t p2, int32_t p3, dvdnav_highlight_area_t *p4))
+ DEFINE_METHOD1(dvdnav_status_t, dvdnav_go_up, (dvdnav_t *p1))
+ DEFINE_METHOD1(int8_t, dvdnav_get_active_audio_stream, (dvdnav_t *p1))
+ DEFINE_METHOD2(uint16_t, dvdnav_audio_stream_to_lang, (dvdnav_t *p1, uint8_t p2))
+ DEFINE_METHOD2(int8_t, dvdnav_get_audio_logical_stream, (dvdnav_t *p1, uint8_t p2))
+ DEFINE_METHOD2(int8_t, dvdnav_get_number_of_streams, (dvdnav_t * p1, dvdnav_stream_type_t p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_set_region_mask, (dvdnav_t *p1, int32_t p2))
+ DEFINE_METHOD1(uint8_t, dvdnav_get_video_aspect, (dvdnav_t *p1))
+ DEFINE_METHOD1(uint8_t, dvdnav_get_video_scale_permission, (dvdnav_t *p1))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_get_number_of_titles, (dvdnav_t *p1, int32_t *p2))
+ DEFINE_METHOD3(dvdnav_status_t, dvdnav_get_number_of_parts, (dvdnav_t *p1, int32_t p2, int32_t *p3))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_title_play, (dvdnav_t *p1, int32_t p2))
+ DEFINE_METHOD3(dvdnav_status_t, dvdnav_part_play, (dvdnav_t *p1, int32_t p2, int32_t p3))
+ DEFINE_METHOD3(dvdnav_status_t, dvdnav_get_audio_attr, (dvdnav_t * p1, int32_t p2, audio_attr_t* p3))
+ DEFINE_METHOD3(dvdnav_status_t, dvdnav_get_spu_attr, (dvdnav_t * p1, int32_t p2, subp_attr_t* p3))
+ DEFINE_METHOD3(dvdnav_status_t, dvdnav_jump_to_sector_by_time, (dvdnav_t * p1, uint64_t p2, int32_t p3))
+ DEFINE_METHOD1(int64_t, dvdnav_convert_time, (dvd_time_t *p1))
+ DEFINE_METHOD3(dvdnav_status_t, dvdnav_get_angle_info, (dvdnav_t *p1, int32_t *p2,int32_t *p3))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_angle_change, (dvdnav_t *p1, int32_t p2))
+ DEFINE_METHOD4(dvdnav_status_t, dvdnav_mouse_activate, (dvdnav_t *p1, pci_t *p2, int32_t p3, int32_t p4))
+ DEFINE_METHOD4(dvdnav_status_t, dvdnav_mouse_select, (dvdnav_t *p1, pci_t *p2, int32_t p3, int32_t p4))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_get_title_string, (dvdnav_t *p1, const char **p2))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_get_serial_string, (dvdnav_t *p1, const char **p2))
+ DEFINE_METHOD1(const char*, dvdnav_get_volid_string, (dvdnav_t * p1))
+ DEFINE_METHOD4(uint32_t, dvdnav_describe_title_chapters, (dvdnav_t* p1, uint32_t p2, uint64_t** p3, uint64_t* p4))
+ DEFINE_METHOD1(int64_t, dvdnav_get_current_time, (dvdnav_t * p1))
+ DEFINE_METHOD3(int, dvdnav_get_video_resolution, (dvdnav_t * p1, uint32_t* p2, uint32_t* p3))
+ DEFINE_METHOD2(dvdnav_status_t, dvdnav_toggle_spu_stream, (dvdnav_t * p1, uint8_t p2))
+ DEFINE_METHOD3(dvdnav_status_t,
+ dvdnav_set_active_stream,
+ (dvdnav_t * p1, uint8_t p2, dvdnav_stream_type_t p3))
+ DEFINE_METHOD4(dvdnav_status_t,
+ dvdnav_program_play,
+ (dvdnav_t * p1, int32_t p2, int32_t p3, int32_t p4))
+ DEFINE_METHOD4(dvdnav_status_t,
+ dvdnav_current_title_program,
+ (dvdnav_t * p1, int32_t* p2, int32_t* p3, int32_t* p4))
+ BEGIN_METHOD_RESOLVE()
+ RESOLVE_METHOD(dvdnav_open)
+ RESOLVE_METHOD(dvdnav_open2)
+ RESOLVE_METHOD(dvdnav_open_stream)
+ RESOLVE_METHOD(dvdnav_open_stream2)
+ RESOLVE_METHOD(dvdnav_close)
+ RESOLVE_METHOD(dvdnav_reset)
+ RESOLVE_METHOD(dvdnav_err_to_string)
+ RESOLVE_METHOD(dvdnav_set_readahead_flag)
+ RESOLVE_METHOD(dvdnav_set_PGC_positioning_flag)
+ RESOLVE_METHOD(dvdnav_get_next_cache_block)
+ RESOLVE_METHOD(dvdnav_free_cache_block)
+ RESOLVE_METHOD(dvdnav_still_skip)
+ RESOLVE_METHOD(dvdnav_wait_skip)
+ RESOLVE_METHOD(dvdnav_stop)
+ RESOLVE_METHOD(dvdnav_get_number_of_streams)
+ RESOLVE_METHOD(dvdnav_get_disk_region_mask)
+ RESOLVE_METHOD(dvdnav_button_select)
+ RESOLVE_METHOD(dvdnav_button_activate)
+ RESOLVE_METHOD(dvdnav_upper_button_select)
+ RESOLVE_METHOD(dvdnav_lower_button_select)
+ RESOLVE_METHOD(dvdnav_right_button_select)
+ RESOLVE_METHOD(dvdnav_left_button_select)
+ RESOLVE_METHOD(dvdnav_sector_search)
+ RESOLVE_METHOD(dvdnav_get_current_nav_pci)
+ RESOLVE_METHOD(dvdnav_get_current_nav_dsi)
+ RESOLVE_METHOD(dvdnav_get_position)
+ RESOLVE_METHOD(dvdnav_current_title_info)
+ RESOLVE_METHOD(dvdnav_spu_language_select)
+ RESOLVE_METHOD(dvdnav_audio_language_select)
+ RESOLVE_METHOD(dvdnav_menu_language_select)
+ RESOLVE_METHOD(dvdnav_is_domain_vts)
+ RESOLVE_METHOD(dvdnav_get_active_spu_stream)
+ RESOLVE_METHOD(dvdnav_get_spu_logical_stream)
+ RESOLVE_METHOD(dvdnav_spu_stream_to_lang)
+ RESOLVE_METHOD(dvdnav_get_current_highlight)
+ RESOLVE_METHOD(dvdnav_menu_call)
+ RESOLVE_METHOD(dvdnav_prev_pg_search)
+ RESOLVE_METHOD(dvdnav_next_pg_search)
+ RESOLVE_METHOD(dvdnav_get_highlight_area)
+ RESOLVE_METHOD(dvdnav_go_up)
+ RESOLVE_METHOD(dvdnav_get_active_audio_stream)
+ RESOLVE_METHOD(dvdnav_audio_stream_to_lang)
+ RESOLVE_METHOD(dvdnav_get_audio_logical_stream)
+ RESOLVE_METHOD(dvdnav_set_region_mask)
+ RESOLVE_METHOD(dvdnav_get_video_aspect)
+ RESOLVE_METHOD(dvdnav_get_video_scale_permission)
+ RESOLVE_METHOD(dvdnav_get_number_of_titles)
+ RESOLVE_METHOD(dvdnav_get_number_of_parts)
+ RESOLVE_METHOD(dvdnav_title_play)
+ RESOLVE_METHOD(dvdnav_part_play)
+ RESOLVE_METHOD(dvdnav_get_audio_attr)
+ RESOLVE_METHOD(dvdnav_get_spu_attr)
+ RESOLVE_METHOD(dvdnav_jump_to_sector_by_time)
+ RESOLVE_METHOD(dvdnav_convert_time)
+ RESOLVE_METHOD(dvdnav_get_angle_info)
+ RESOLVE_METHOD(dvdnav_angle_change)
+ RESOLVE_METHOD(dvdnav_mouse_activate)
+ RESOLVE_METHOD(dvdnav_mouse_select)
+ RESOLVE_METHOD(dvdnav_get_title_string)
+ RESOLVE_METHOD(dvdnav_get_serial_string)
+ RESOLVE_METHOD(dvdnav_get_volid_string)
+ RESOLVE_METHOD(dvdnav_describe_title_chapters)
+ RESOLVE_METHOD(dvdnav_get_current_time)
+ RESOLVE_METHOD(dvdnav_get_video_resolution)
+ RESOLVE_METHOD(dvdnav_toggle_spu_stream)
+ RESOLVE_METHOD(dvdnav_set_active_stream)
+ RESOLVE_METHOD(dvdnav_program_play)
+ RESOLVE_METHOD(dvdnav_current_title_program)
+ END_METHOD_RESOLVE()
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamAddon.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamAddon.cpp
new file mode 100644
index 0000000..8ef7975
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamAddon.cpp
@@ -0,0 +1,777 @@
+/*
+ * 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 "InputStreamAddon.h"
+
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/VideoCodec.h"
+#include "cores/VideoPlayer/DVDDemuxers/DVDDemux.h"
+#include "cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.h"
+#include "cores/VideoPlayer/Interface/DemuxCrypto.h"
+#include "cores/VideoPlayer/Interface/InputStreamConstants.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "windowing/Resolution.h"
+
+CInputStreamProvider::CInputStreamProvider(const ADDON::AddonInfoPtr& addonInfo,
+ KODI_HANDLE parentInstance)
+ : m_addonInfo(addonInfo), m_parentInstance(parentInstance)
+{
+}
+
+void CInputStreamProvider::GetAddonInstance(INSTANCE_TYPE instance_type,
+ ADDON::AddonInfoPtr& addonInfo,
+ KODI_HANDLE& parentInstance)
+{
+ if (instance_type == ADDON::IAddonProvider::INSTANCE_VIDEOCODEC)
+ {
+ addonInfo = m_addonInfo;
+ parentInstance = m_parentInstance;
+ }
+}
+
+/*****************************************************************************************************************/
+
+using namespace ADDON;
+
+CInputStreamAddon::CInputStreamAddon(const AddonInfoPtr& addonInfo,
+ IVideoPlayer* player,
+ const CFileItem& fileitem,
+ const std::string& instanceId)
+ : IAddonInstanceHandler(
+ ADDON_INSTANCE_INPUTSTREAM, addonInfo, ADDON_INSTANCE_ID_UNUSED, nullptr, instanceId),
+ CDVDInputStream(DVDSTREAM_TYPE_ADDON, fileitem),
+ m_player(player)
+{
+ std::string listitemprops =
+ addonInfo->Type(AddonType::INPUTSTREAM)->GetValue("@listitemprops").asString();
+ std::string name(addonInfo->ID());
+
+ m_fileItemProps = StringUtils::Tokenize(listitemprops, "|");
+ for (auto &key : m_fileItemProps)
+ {
+ StringUtils::Trim(key);
+ key = name + "." + key;
+ }
+ m_caps = {};
+}
+
+CInputStreamAddon::~CInputStreamAddon()
+{
+ Close();
+}
+
+bool CInputStreamAddon::Supports(const AddonInfoPtr& addonInfo, const CFileItem& fileitem)
+{
+ /// @todo Error for users to show deprecation, can be removed in Kodi 20
+ CVariant oldAddonProp = fileitem.GetProperty("inputstreamaddon");
+ if (!oldAddonProp.isNull())
+ {
+ CLog::Log(LOGERROR,
+ "CInputStreamAddon::{} - 'inputstreamaddon' has been deprecated, "
+ "please use `#KODIPROP:inputstream={}` instead",
+ __func__, oldAddonProp.asString());
+ }
+
+ // check if a specific inputstream addon is requested
+ CVariant addon = fileitem.GetProperty(STREAM_PROPERTY_INPUTSTREAM);
+ if (!addon.isNull())
+ return (addon.asString() == addonInfo->ID());
+
+ // check protocols
+ std::string protocol = CURL(fileitem.GetDynPath()).GetProtocol();
+ if (!protocol.empty())
+ {
+ std::string protocols =
+ addonInfo->Type(AddonType::INPUTSTREAM)->GetValue("@protocols").asString();
+ if (!protocols.empty())
+ {
+ std::vector<std::string> protocolsList = StringUtils::Tokenize(protocols, "|");
+ for (auto& value : protocolsList)
+ {
+ StringUtils::Trim(value);
+ if (value == protocol)
+ return true;
+ }
+ }
+ }
+
+ std::string filetype = fileitem.GetURL().GetFileType();
+ if (!filetype.empty())
+ {
+ std::string extensions =
+ addonInfo->Type(AddonType::INPUTSTREAM)->GetValue("@extension").asString();
+ if (!extensions.empty())
+ {
+ std::vector<std::string> extensionsList = StringUtils::Tokenize(extensions, "|");
+ for (auto& value : extensionsList)
+ {
+ StringUtils::Trim(value);
+ if (value == filetype)
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool CInputStreamAddon::Open()
+{
+ // Create "C" interface structures, used as own parts to prevent API problems on update
+ m_ifc.inputstream = new AddonInstance_InputStream;
+ m_ifc.inputstream->props = new AddonProps_InputStream();
+ m_ifc.inputstream->toAddon = new KodiToAddonFuncTable_InputStream();
+ m_ifc.inputstream->toKodi = new AddonToKodiFuncTable_InputStream();
+
+ m_ifc.inputstream->toKodi->kodiInstance = this;
+ m_ifc.inputstream->toKodi->free_demux_packet = cb_free_demux_packet;
+ m_ifc.inputstream->toKodi->allocate_demux_packet = cb_allocate_demux_packet;
+ m_ifc.inputstream->toKodi->allocate_encrypted_demux_packet = cb_allocate_encrypted_demux_packet;
+ /*
+ // Way to include part on new API version
+ if (Addon()->GetTypeVersionDll(ADDON_TYPE::ADDON_INSTANCE_INPUTSTREAM) >= AddonVersion("3.0.0")) // Set the version to your new
+ {
+
+ }
+ */
+ if (CreateInstance() != ADDON_STATUS_OK || !m_ifc.inputstream->toAddon->open)
+ return false;
+
+ INPUTSTREAM_PROPERTY props = {};
+ std::map<std::string, std::string> propsMap;
+ for (auto &key : m_fileItemProps)
+ {
+ if (m_item.GetProperty(key).isNull())
+ continue;
+ propsMap[key] = m_item.GetProperty(key).asString();
+ }
+
+ props.m_nCountInfoValues = 0;
+ for (auto &pair : propsMap)
+ {
+ props.m_ListItemProperties[props.m_nCountInfoValues].m_strKey = pair.first.c_str();
+ props.m_ListItemProperties[props.m_nCountInfoValues].m_strValue = pair.second.c_str();
+ props.m_nCountInfoValues++;
+
+ if (props.m_nCountInfoValues >= STREAM_MAX_PROPERTY_COUNT)
+ {
+ CLog::Log(LOGERROR,
+ "CInputStreamAddon::{} - Hit max count of stream properties, "
+ "have {}, actual count: {}",
+ __func__, STREAM_MAX_PROPERTY_COUNT, propsMap.size());
+ break;
+ }
+ }
+
+ props.m_strURL = m_item.GetDynPath().c_str();
+ props.m_mimeType = m_item.GetMimeType().c_str();
+
+ std::string libFolder = URIUtils::GetDirectory(Addon()->Path());
+ std::string profileFolder = CSpecialProtocol::TranslatePath(Addon()->Profile());
+ props.m_libFolder = libFolder.c_str();
+ props.m_profileFolder = profileFolder.c_str();
+
+ DetectScreenResolution();
+
+ bool ret = m_ifc.inputstream->toAddon->open(m_ifc.inputstream, &props);
+ if (ret)
+ {
+ m_caps = {};
+ m_ifc.inputstream->toAddon->get_capabilities(m_ifc.inputstream, &m_caps);
+
+ m_subAddonProvider = std::shared_ptr<CInputStreamProvider>(
+ new CInputStreamProvider(GetAddonInfo(), m_ifc.inputstream->toAddon->addonInstance));
+ }
+ return ret;
+}
+
+void CInputStreamAddon::Close()
+{
+ if (m_ifc.inputstream->toAddon->close)
+ m_ifc.inputstream->toAddon->close(m_ifc.inputstream);
+ DestroyInstance();
+
+ // Delete "C" interface structures
+ delete m_ifc.inputstream->toAddon;
+ delete m_ifc.inputstream->toKodi;
+ delete m_ifc.inputstream->props;
+ delete m_ifc.inputstream;
+ m_ifc.inputstream = nullptr;
+}
+
+bool CInputStreamAddon::IsEOF()
+{
+ return false;
+}
+
+int CInputStreamAddon::Read(uint8_t* buf, int buf_size)
+{
+ if (!m_ifc.inputstream->toAddon->read_stream)
+ return -1;
+
+ return m_ifc.inputstream->toAddon->read_stream(m_ifc.inputstream, buf, buf_size);
+}
+
+int64_t CInputStreamAddon::Seek(int64_t offset, int whence)
+{
+ if (!m_ifc.inputstream->toAddon->seek_stream)
+ return -1;
+
+ return m_ifc.inputstream->toAddon->seek_stream(m_ifc.inputstream, offset, whence);
+}
+
+int64_t CInputStreamAddon::GetLength()
+{
+ if (!m_ifc.inputstream->toAddon->length_stream)
+ return -1;
+
+ return m_ifc.inputstream->toAddon->length_stream(m_ifc.inputstream);
+}
+
+int CInputStreamAddon::GetBlockSize()
+{
+ if (!m_ifc.inputstream->toAddon->block_size_stream)
+ return 0;
+
+ return m_ifc.inputstream->toAddon->block_size_stream(m_ifc.inputstream);
+}
+
+bool CInputStreamAddon::CanSeek()
+{
+ return (m_caps.m_mask & INPUTSTREAM_SUPPORTS_SEEK) != 0;
+}
+
+bool CInputStreamAddon::CanPause()
+{
+ return (m_caps.m_mask & INPUTSTREAM_SUPPORTS_PAUSE) != 0;
+}
+
+// IDisplayTime
+CDVDInputStream::IDisplayTime* CInputStreamAddon::GetIDisplayTime()
+{
+ if ((m_caps.m_mask & INPUTSTREAM_SUPPORTS_IDISPLAYTIME) == 0)
+ return nullptr;
+
+ return this;
+}
+
+int CInputStreamAddon::GetTotalTime()
+{
+ if (!m_ifc.inputstream->toAddon->get_total_time)
+ return 0;
+
+ return m_ifc.inputstream->toAddon->get_total_time(m_ifc.inputstream);
+}
+
+int CInputStreamAddon::GetTime()
+{
+ if (!m_ifc.inputstream->toAddon->get_time)
+ return 0;
+
+ return m_ifc.inputstream->toAddon->get_time(m_ifc.inputstream);
+}
+
+// ITime
+CDVDInputStream::ITimes* CInputStreamAddon::GetITimes()
+{
+ // Check if screen resolution is changed during playback
+ // e.g. window resized and callback to add-on
+ DetectScreenResolution();
+
+ if ((m_caps.m_mask & INPUTSTREAM_SUPPORTS_ITIME) == 0)
+ return nullptr;
+
+ return this;
+}
+
+bool CInputStreamAddon::GetTimes(Times &times)
+{
+ if (!m_ifc.inputstream->toAddon->get_times)
+ return false;
+
+ INPUTSTREAM_TIMES i_times;
+
+ if (m_ifc.inputstream->toAddon->get_times(m_ifc.inputstream, &i_times))
+ {
+ times.ptsBegin = i_times.ptsBegin;
+ times.ptsEnd = i_times.ptsEnd;
+ times.ptsStart = i_times.ptsStart;
+ times.startTime = i_times.startTime;
+ return true;
+ }
+ return false;
+}
+
+// IPosTime
+CDVDInputStream::IPosTime* CInputStreamAddon::GetIPosTime()
+{
+ if ((m_caps.m_mask & INPUTSTREAM_SUPPORTS_IPOSTIME) == 0)
+ return nullptr;
+
+ return this;
+}
+
+bool CInputStreamAddon::PosTime(int ms)
+{
+ if (!m_ifc.inputstream->toAddon->pos_time)
+ return false;
+
+ return m_ifc.inputstream->toAddon->pos_time(m_ifc.inputstream, ms);
+}
+
+// IDemux
+CDVDInputStream::IDemux* CInputStreamAddon::GetIDemux()
+{
+ if ((m_caps.m_mask & INPUTSTREAM_SUPPORTS_IDEMUX) == 0)
+ return nullptr;
+
+ return this;
+}
+
+bool CInputStreamAddon::OpenDemux()
+{
+ if ((m_caps.m_mask & INPUTSTREAM_SUPPORTS_IDEMUX) != 0)
+ return true;
+ else
+ return false;
+}
+
+DemuxPacket* CInputStreamAddon::ReadDemux()
+{
+ if (!m_ifc.inputstream->toAddon->demux_read)
+ return nullptr;
+
+ return reinterpret_cast<DemuxPacket*>(m_ifc.inputstream->toAddon->demux_read(m_ifc.inputstream));
+}
+
+std::vector<CDemuxStream*> CInputStreamAddon::GetStreams() const
+{
+ std::vector<CDemuxStream*> streams;
+
+ INPUTSTREAM_IDS streamIDs = {};
+ bool ret = m_ifc.inputstream->toAddon->get_stream_ids(m_ifc.inputstream, &streamIDs);
+ if (!ret || streamIDs.m_streamCount > INPUTSTREAM_MAX_STREAM_COUNT)
+ return streams;
+
+ for (unsigned int i = 0; i < streamIDs.m_streamCount; ++i)
+ if (CDemuxStream* stream = GetStream(streamIDs.m_streamIds[i]))
+ streams.push_back(stream);
+
+ return streams;
+}
+
+CDemuxStream* CInputStreamAddon::GetStream(int streamId) const
+{
+ INPUTSTREAM_INFO stream{};
+ KODI_HANDLE demuxStream = nullptr;
+ bool ret = m_ifc.inputstream->toAddon->get_stream(m_ifc.inputstream, streamId, &stream,
+ &demuxStream, cb_get_stream_transfer);
+ if (!ret || stream.m_streamType == INPUTSTREAM_TYPE_NONE)
+ return nullptr;
+
+ return static_cast<CDemuxStream*>(demuxStream);
+}
+
+KODI_HANDLE CInputStreamAddon::cb_get_stream_transfer(KODI_HANDLE handle,
+ int streamId,
+ INPUTSTREAM_INFO* stream)
+{
+ CInputStreamAddon* thisClass = static_cast<CInputStreamAddon*>(handle);
+ if (!thisClass || !stream)
+ return nullptr;
+
+ std::string codecName(stream->m_codecName);
+ AVCodec* codec = nullptr;
+
+ if (stream->m_streamType != INPUTSTREAM_TYPE_TELETEXT &&
+ stream->m_streamType != INPUTSTREAM_TYPE_RDS && stream->m_streamType != INPUTSTREAM_TYPE_ID3)
+ {
+ StringUtils::ToLower(codecName);
+ codec = avcodec_find_decoder_by_name(codecName.c_str());
+ if (!codec)
+ return nullptr;
+ }
+
+ CDemuxStream* demuxStream;
+
+ if (stream->m_streamType == INPUTSTREAM_TYPE_AUDIO)
+ {
+ CDemuxStreamAudio *audioStream = new CDemuxStreamAudio();
+
+ audioStream->iChannels = stream->m_Channels;
+ audioStream->iSampleRate = stream->m_SampleRate;
+ audioStream->iBlockAlign = stream->m_BlockAlign;
+ audioStream->iBitRate = stream->m_BitRate;
+ audioStream->iBitsPerSample = stream->m_BitsPerSample;
+ demuxStream = audioStream;
+ }
+ else if (stream->m_streamType == INPUTSTREAM_TYPE_VIDEO)
+ {
+ CDemuxStreamVideo *videoStream = new CDemuxStreamVideo();
+
+ videoStream->iFpsScale = stream->m_FpsScale;
+ videoStream->iFpsRate = stream->m_FpsRate;
+ videoStream->iWidth = stream->m_Width;
+ videoStream->iHeight = stream->m_Height;
+ videoStream->fAspect = static_cast<double>(stream->m_Aspect);
+ videoStream->iBitRate = stream->m_BitRate;
+ videoStream->profile = ConvertVideoCodecProfile(stream->m_codecProfile);
+
+ /*! Added on API version 2.0.8 */
+ //@{
+ videoStream->colorSpace = static_cast<AVColorSpace>(stream->m_colorSpace);
+ videoStream->colorRange = static_cast<AVColorRange>(stream->m_colorRange);
+ //@}
+
+ /*! Added on API version 2.0.9 */
+ //@{
+ videoStream->colorPrimaries = static_cast<AVColorPrimaries>(stream->m_colorPrimaries);
+ videoStream->colorTransferCharacteristic =
+ static_cast<AVColorTransferCharacteristic>(stream->m_colorTransferCharacteristic);
+
+ if (stream->m_masteringMetadata)
+ {
+ videoStream->masteringMetaData =
+ std::shared_ptr<AVMasteringDisplayMetadata>(new AVMasteringDisplayMetadata);
+ videoStream->masteringMetaData->display_primaries[0][0] =
+ av_d2q(stream->m_masteringMetadata->primary_r_chromaticity_x, INT_MAX);
+ videoStream->masteringMetaData->display_primaries[0][1] =
+ av_d2q(stream->m_masteringMetadata->primary_r_chromaticity_y, INT_MAX);
+ videoStream->masteringMetaData->display_primaries[1][0] =
+ av_d2q(stream->m_masteringMetadata->primary_g_chromaticity_x, INT_MAX);
+ videoStream->masteringMetaData->display_primaries[1][1] =
+ av_d2q(stream->m_masteringMetadata->primary_g_chromaticity_y, INT_MAX);
+ videoStream->masteringMetaData->display_primaries[2][0] =
+ av_d2q(stream->m_masteringMetadata->primary_b_chromaticity_x, INT_MAX);
+ videoStream->masteringMetaData->display_primaries[2][1] =
+ av_d2q(stream->m_masteringMetadata->primary_b_chromaticity_y, INT_MAX);
+ videoStream->masteringMetaData->white_point[0] =
+ av_d2q(stream->m_masteringMetadata->white_point_chromaticity_x, INT_MAX);
+ videoStream->masteringMetaData->white_point[1] =
+ av_d2q(stream->m_masteringMetadata->white_point_chromaticity_y, INT_MAX);
+ videoStream->masteringMetaData->min_luminance =
+ av_d2q(stream->m_masteringMetadata->luminance_min, INT_MAX);
+ videoStream->masteringMetaData->max_luminance =
+ av_d2q(stream->m_masteringMetadata->luminance_max, INT_MAX);
+ videoStream->masteringMetaData->has_luminance =
+ videoStream->masteringMetaData->has_primaries = 1;
+ }
+
+ if (stream->m_contentLightMetadata)
+ {
+ videoStream->contentLightMetaData =
+ std::shared_ptr<AVContentLightMetadata>(new AVContentLightMetadata);
+ videoStream->contentLightMetaData->MaxCLL =
+ static_cast<unsigned>(stream->m_contentLightMetadata->max_cll);
+ videoStream->contentLightMetaData->MaxFALL =
+ static_cast<unsigned>(stream->m_contentLightMetadata->max_fall);
+ }
+ //@}
+
+ /*
+ // Way to include part on new API version
+ if (Addon()->GetTypeVersionDll(ADDON_TYPE::ADDON_INSTANCE_INPUTSTREAM) >= AddonVersion("3.0.0")) // Set the version to your new
+ {
+
+ }
+ */
+
+ demuxStream = videoStream;
+ }
+ else if (stream->m_streamType == INPUTSTREAM_TYPE_SUBTITLE)
+ {
+ CDemuxStreamSubtitle *subtitleStream = new CDemuxStreamSubtitle();
+ demuxStream = subtitleStream;
+ }
+ else if (stream->m_streamType == INPUTSTREAM_TYPE_TELETEXT)
+ {
+ CDemuxStreamTeletext* teletextStream = new CDemuxStreamTeletext();
+ demuxStream = teletextStream;
+ }
+ else if (stream->m_streamType == INPUTSTREAM_TYPE_RDS)
+ {
+ CDemuxStreamRadioRDS* rdsStream = new CDemuxStreamRadioRDS();
+ demuxStream = rdsStream;
+ }
+ else if (stream->m_streamType == INPUTSTREAM_TYPE_ID3)
+ {
+ CDemuxStreamAudioID3* id3Stream = new CDemuxStreamAudioID3();
+ demuxStream = id3Stream;
+ }
+ else
+ return nullptr;
+
+ demuxStream->name = stream->m_name;
+ if (codec)
+ demuxStream->codec = codec->id;
+ else
+ demuxStream->codec = AV_CODEC_ID_DVB_TELETEXT;
+ demuxStream->codecName = stream->m_codecInternalName;
+ demuxStream->uniqueId = streamId;
+ demuxStream->flags = static_cast<StreamFlags>(stream->m_flags);
+ demuxStream->language = stream->m_language;
+
+ if (thisClass->GetAddonInfo()->DependencyVersion(ADDON_INSTANCE_VERSION_INPUTSTREAM_XML_ID) >=
+ CAddonVersion("2.0.8"))
+ {
+ demuxStream->codec_fourcc = stream->m_codecFourCC;
+ }
+
+ if (stream->m_ExtraData && stream->m_ExtraSize)
+ {
+ demuxStream->ExtraData = std::make_unique<uint8_t[]>(stream->m_ExtraSize);
+ demuxStream->ExtraSize = stream->m_ExtraSize;
+ for (unsigned int j = 0; j < stream->m_ExtraSize; ++j)
+ demuxStream->ExtraData[j] = stream->m_ExtraData[j];
+ }
+
+ if (stream->m_cryptoSession.keySystem != STREAM_CRYPTO_KEY_SYSTEM_NONE &&
+ stream->m_cryptoSession.keySystem < STREAM_CRYPTO_KEY_SYSTEM_COUNT)
+ {
+ static const CryptoSessionSystem map[] = {
+ CRYPTO_SESSION_SYSTEM_NONE,
+ CRYPTO_SESSION_SYSTEM_WIDEVINE,
+ CRYPTO_SESSION_SYSTEM_PLAYREADY,
+ CRYPTO_SESSION_SYSTEM_WISEPLAY,
+ };
+ demuxStream->cryptoSession = std::shared_ptr<DemuxCryptoSession>(
+ new DemuxCryptoSession(map[stream->m_cryptoSession.keySystem],
+ stream->m_cryptoSession.sessionId, stream->m_cryptoSession.flags));
+
+ if ((stream->m_features & INPUTSTREAM_FEATURE_DECODE) != 0)
+ demuxStream->externalInterfaces = thisClass->m_subAddonProvider;
+ }
+ return demuxStream;
+}
+
+void CInputStreamAddon::EnableStream(int streamId, bool enable)
+{
+ if (!m_ifc.inputstream->toAddon->enable_stream)
+ return;
+
+ m_ifc.inputstream->toAddon->enable_stream(m_ifc.inputstream, streamId, enable);
+}
+
+bool CInputStreamAddon::OpenStream(int streamId)
+{
+ if (!m_ifc.inputstream->toAddon->open_stream)
+ return false;
+
+ return m_ifc.inputstream->toAddon->open_stream(m_ifc.inputstream, streamId);
+}
+
+int CInputStreamAddon::GetNrOfStreams() const
+{
+ return m_streamCount;
+}
+
+void CInputStreamAddon::SetSpeed(int speed)
+{
+ if (!m_ifc.inputstream->toAddon->demux_set_speed)
+ return;
+
+ m_ifc.inputstream->toAddon->demux_set_speed(m_ifc.inputstream, speed);
+}
+
+bool CInputStreamAddon::SeekTime(double time, bool backward, double* startpts)
+{
+ if (!m_ifc.inputstream->toAddon->demux_seek_time)
+ return false;
+
+ if ((m_caps.m_mask & INPUTSTREAM_SUPPORTS_IPOSTIME) != 0)
+ {
+ if (!PosTime(static_cast<int>(time)))
+ return false;
+
+ FlushDemux();
+
+ if(startpts)
+ *startpts = DVD_NOPTS_VALUE;
+ return true;
+ }
+
+ return m_ifc.inputstream->toAddon->demux_seek_time(m_ifc.inputstream, time, backward, startpts);
+}
+
+void CInputStreamAddon::AbortDemux()
+{
+ if (m_ifc.inputstream->toAddon->demux_abort)
+ m_ifc.inputstream->toAddon->demux_abort(m_ifc.inputstream);
+}
+
+void CInputStreamAddon::FlushDemux()
+{
+ if (m_ifc.inputstream->toAddon->demux_flush)
+ m_ifc.inputstream->toAddon->demux_flush(m_ifc.inputstream);
+}
+
+void CInputStreamAddon::SetVideoResolution(unsigned int width,
+ unsigned int height,
+ unsigned int maxWidth,
+ unsigned int maxHeight)
+{
+ if (m_ifc.inputstream->toAddon->set_video_resolution)
+ m_ifc.inputstream->toAddon->set_video_resolution(m_ifc.inputstream, width, height, maxWidth,
+ maxHeight);
+}
+
+bool CInputStreamAddon::IsRealtime()
+{
+ if (m_ifc.inputstream->toAddon->is_real_time_stream)
+ return m_ifc.inputstream->toAddon->is_real_time_stream(m_ifc.inputstream);
+ return false;
+}
+
+
+// IChapter
+CDVDInputStream::IChapter* CInputStreamAddon::GetIChapter()
+{
+ if ((m_caps.m_mask & INPUTSTREAM_SUPPORTS_ICHAPTER) == 0)
+ return nullptr;
+
+ return this;
+}
+
+int CInputStreamAddon::GetChapter()
+{
+ if (m_ifc.inputstream->toAddon->get_chapter)
+ return m_ifc.inputstream->toAddon->get_chapter(m_ifc.inputstream);
+
+ return -1;
+}
+
+int CInputStreamAddon::GetChapterCount()
+{
+ if (m_ifc.inputstream->toAddon->get_chapter_count)
+ return m_ifc.inputstream->toAddon->get_chapter_count(m_ifc.inputstream);
+
+ return 0;
+}
+
+void CInputStreamAddon::GetChapterName(std::string& name, int ch)
+{
+ name.clear();
+ if (m_ifc.inputstream->toAddon->get_chapter_name)
+ {
+ const char* res = m_ifc.inputstream->toAddon->get_chapter_name(m_ifc.inputstream, ch);
+ if (res)
+ name = res;
+ }
+}
+
+int64_t CInputStreamAddon::GetChapterPos(int ch)
+{
+ if (m_ifc.inputstream->toAddon->get_chapter_pos)
+ return m_ifc.inputstream->toAddon->get_chapter_pos(m_ifc.inputstream, ch);
+
+ return 0;
+}
+
+bool CInputStreamAddon::SeekChapter(int ch)
+{
+ if (m_ifc.inputstream->toAddon->seek_chapter)
+ return m_ifc.inputstream->toAddon->seek_chapter(m_ifc.inputstream, ch);
+
+ return false;
+}
+
+int CInputStreamAddon::ConvertVideoCodecProfile(STREAMCODEC_PROFILE profile)
+{
+ switch (profile)
+ {
+ case H264CodecProfileBaseline:
+ return FF_PROFILE_H264_BASELINE;
+ case H264CodecProfileMain:
+ return FF_PROFILE_H264_MAIN;
+ case H264CodecProfileExtended:
+ return FF_PROFILE_H264_EXTENDED;
+ case H264CodecProfileHigh:
+ return FF_PROFILE_H264_HIGH;
+ case H264CodecProfileHigh10:
+ return FF_PROFILE_H264_HIGH_10;
+ case H264CodecProfileHigh422:
+ return FF_PROFILE_H264_HIGH_422;
+ case H264CodecProfileHigh444Predictive:
+ return FF_PROFILE_H264_HIGH_444_PREDICTIVE;
+ case VP9CodecProfile0:
+ return FF_PROFILE_VP9_0;
+ case VP9CodecProfile1:
+ return FF_PROFILE_VP9_1;
+ case VP9CodecProfile2:
+ return FF_PROFILE_VP9_2;
+ case VP9CodecProfile3:
+ return FF_PROFILE_VP9_3;
+ case AV1CodecProfileMain:
+ return FF_PROFILE_AV1_MAIN;
+ case AV1CodecProfileHigh:
+ return FF_PROFILE_AV1_HIGH;
+ case AV1CodecProfileProfessional:
+ return FF_PROFILE_AV1_PROFESSIONAL;
+ default:
+ return FF_PROFILE_UNKNOWN;
+ }
+}
+
+void CInputStreamAddon::DetectScreenResolution()
+{
+ unsigned int videoWidth{1280};
+ unsigned int videoHeight{720};
+ if (m_player)
+ {
+ m_player->GetVideoResolution(videoWidth, videoHeight);
+ }
+ if (m_currentVideoWidth != videoWidth || m_currentVideoHeight != videoHeight)
+ {
+ unsigned int maxWidth{videoWidth};
+ unsigned int maxHeight{videoHeight};
+ // For Adaptive stream technology is needed to know the screen resolution
+ // one parameter used to fit stream resolution to screen resolution.
+ // Currently we provide current GUI resolution, but if Adjust refresh rate
+ // is enabled the GUI resolution is no longer relevant, Adjust refresh rate
+ // will change screen resolution based on whitelist (if any) and only after
+ // that have the video stream in the demuxer, therefore will fail because
+ // the addon has as reference the GUI resolution.
+ // So we have to provide the max resolution info before the playback take place
+ // in order to allow addon to provide in the demuxer the best stream resolution
+ // that can fit the supported screen resolution (changed when playback start).
+ CResolutionUtils::GetMaxAllowedResolution(maxWidth, maxHeight);
+
+ SetVideoResolution(videoWidth, videoHeight, maxWidth, maxHeight);
+
+ m_currentVideoWidth = videoWidth;
+ m_currentVideoHeight = videoHeight;
+ }
+}
+
+/*!
+ * Callbacks from add-on to kodi
+ */
+//@{
+DEMUX_PACKET* CInputStreamAddon::cb_allocate_demux_packet(void* kodiInstance, int data_size)
+{
+ return CDVDDemuxUtils::AllocateDemuxPacket(data_size);
+}
+
+DEMUX_PACKET* CInputStreamAddon::cb_allocate_encrypted_demux_packet(
+ void* kodiInstance, unsigned int dataSize, unsigned int encryptedSubsampleCount)
+{
+ return CDVDDemuxUtils::AllocateDemuxPacket(dataSize, encryptedSubsampleCount);
+}
+
+void CInputStreamAddon::cb_free_demux_packet(void* kodiInstance, DEMUX_PACKET* packet)
+{
+ CDVDDemuxUtils::FreeDemuxPacket(static_cast<DemuxPacket*>(packet));
+}
+
+//@}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamAddon.h b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamAddon.h
new file mode 100644
index 0000000..8471404
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamAddon.h
@@ -0,0 +1,169 @@
+/*
+ * 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 "DVDInputStream.h"
+#include "IVideoPlayer.h"
+#include "addons/AddonProvider.h"
+#include "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Inputstream.h"
+
+#include <memory>
+#include <vector>
+
+class CInputStreamProvider
+ : public ADDON::IAddonProvider
+{
+public:
+ CInputStreamProvider(const ADDON::AddonInfoPtr& addonInfo, KODI_HANDLE parentInstance);
+
+ void GetAddonInstance(INSTANCE_TYPE instance_type,
+ ADDON::AddonInfoPtr& addonInfo,
+ KODI_HANDLE& parentInstance) override;
+
+private:
+ ADDON::AddonInfoPtr m_addonInfo;
+ KODI_HANDLE m_parentInstance;
+};
+
+//! \brief Input stream class
+class CInputStreamAddon
+ : public ADDON::IAddonInstanceHandler
+ , public CDVDInputStream
+ , public CDVDInputStream::IDisplayTime
+ , public CDVDInputStream::ITimes
+ , public CDVDInputStream::IPosTime
+ , public CDVDInputStream::IDemux
+ , public CDVDInputStream::IChapter
+{
+public:
+ CInputStreamAddon(const ADDON::AddonInfoPtr& addonInfo,
+ IVideoPlayer* player,
+ const CFileItem& fileitem,
+ const std::string& instanceId);
+ ~CInputStreamAddon() override;
+
+ static bool Supports(const ADDON::AddonInfoPtr& addonInfo, const CFileItem& fileitem);
+
+ // CDVDInputStream
+ bool Open() override;
+ void Close() override;
+ int Read(uint8_t* buf, int buf_size) override;
+ int64_t Seek(int64_t offset, int whence) override;
+ int64_t GetLength() override;
+ int GetBlockSize() override;
+ bool IsEOF() override;
+ bool CanSeek() override; //! @todo drop this
+ bool CanPause() override;
+
+ // IDisplayTime
+ CDVDInputStream::IDisplayTime* GetIDisplayTime() override;
+ int GetTotalTime() override;
+ int GetTime() override;
+
+ // ITime
+ CDVDInputStream::ITimes* GetITimes() override;
+ bool GetTimes(Times &times) override;
+
+ // IPosTime
+ CDVDInputStream::IPosTime* GetIPosTime() override;
+ bool PosTime(int ms) override;
+
+ // IDemux
+ CDVDInputStream::IDemux* GetIDemux() override;
+ bool OpenDemux() override;
+ DemuxPacket* ReadDemux() override;
+ CDemuxStream* GetStream(int streamId) const override;
+ std::vector<CDemuxStream*> GetStreams() const override;
+ void EnableStream(int streamId, bool enable) override;
+ bool OpenStream(int streamid) override;
+
+ int GetNrOfStreams() const override;
+ void SetSpeed(int speed) override;
+ bool SeekTime(double time, bool backward = false, double* startpts = nullptr) override;
+ void AbortDemux() override;
+ void FlushDemux() override;
+ void SetVideoResolution(unsigned int width,
+ unsigned int height,
+ unsigned int maxWidth,
+ unsigned int maxHeight) override;
+ bool IsRealtime() override;
+
+ // IChapter
+ CDVDInputStream::IChapter* GetIChapter() override;
+ int GetChapter() override;
+ int GetChapterCount() override;
+ void GetChapterName(std::string& name, int ch = -1) override;
+ int64_t GetChapterPos(int ch = -1) override;
+ bool SeekChapter(int ch) override;
+
+protected:
+ static int ConvertVideoCodecProfile(STREAMCODEC_PROFILE profile);
+
+ IVideoPlayer* m_player;
+
+private:
+ void DetectScreenResolution();
+
+ unsigned int m_currentVideoWidth{0};
+ unsigned int m_currentVideoHeight{0};
+
+ std::vector<std::string> m_fileItemProps;
+ INPUTSTREAM_CAPABILITIES m_caps;
+
+ int m_streamCount = 0;
+
+ std::shared_ptr<CInputStreamProvider> m_subAddonProvider;
+
+ /*!
+ * Callbacks from add-on to kodi
+ */
+ //@{
+ /*!
+ * @brief Allocate a demux packet. Free with FreeDemuxPacket
+ * @param kodiInstance A pointer to the add-on.
+ * @param iDataSize The size of the data that will go into the packet
+ * @return The allocated packet.
+ */
+ static DEMUX_PACKET* cb_allocate_demux_packet(void* kodiInstance, int iDataSize = 0);
+
+ /*!
+ * @brief Allocate an encrypted demux packet. Free with FreeDemuxPacket
+ * @param kodiInstance A pointer to the add-on.
+ * @param dataSize The size of the data that will go into the packet
+ * @param encryptedSubsampleCount The number of subsample description blocks to allocate
+ * @return The allocated packet.
+ */
+ static DEMUX_PACKET* cb_allocate_encrypted_demux_packet(void* kodiInstance,
+ unsigned int dataSize,
+ unsigned int encryptedSubsampleCount);
+
+ /*!
+ * @brief Free a packet that was allocated with AllocateDemuxPacket
+ * @param kodiInstance A pointer to the add-on.
+ * @param pPacket The packet to free.
+ */
+ static void cb_free_demux_packet(void* kodiInstance, DEMUX_PACKET* pPacket);
+
+ /*!
+ * @brief Callback used by @ref GetStream to get the data
+ *
+ * Used as callback to prevent memleaks as the temporary stack memory on addon
+ * can be used to give data to Kodi.
+ *
+ * @param[in] handle Pointer to identify this class
+ * @param[in] streamId The related stream Identifier
+ * @param[in] stream "C" structure with stream information
+ * @return The created demux stream packet
+ */
+ static KODI_HANDLE cb_get_stream_transfer(KODI_HANDLE handle,
+ int streamId,
+ INPUTSTREAM_INFO* stream);
+ //@}
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiSource.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiSource.cpp
new file mode 100644
index 0000000..b44ec5a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiSource.cpp
@@ -0,0 +1,141 @@
+/*
+ * 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 "InputStreamMultiSource.h"
+
+#include "DVDFactoryInputStream.h"
+#include "utils/log.h"
+
+#include <map>
+
+using namespace XFILE;
+
+CInputStreamMultiSource::CInputStreamMultiSource(IVideoPlayer* pPlayer, const CFileItem& fileitem, const std::vector<std::string>& filenames) : InputStreamMultiStreams(DVDSTREAM_TYPE_MULTIFILES, fileitem),
+ m_pPlayer(pPlayer),
+ m_filenames(filenames)
+{
+}
+
+CInputStreamMultiSource::~CInputStreamMultiSource()
+{
+ Close();
+}
+
+void CInputStreamMultiSource::Abort()
+{
+ for (const auto& iter : m_InputStreams)
+ iter->Abort();
+}
+
+void CInputStreamMultiSource::Close()
+{
+ m_InputStreams.clear();
+ CDVDInputStream::Close();
+}
+
+BitstreamStats CInputStreamMultiSource::GetBitstreamStats() const
+{
+ return m_stats;
+}
+
+int CInputStreamMultiSource::GetBlockSize()
+{
+ return 0;
+}
+
+bool CInputStreamMultiSource::GetCacheStatus(XFILE::SCacheStatus *status)
+{
+ return false;
+}
+
+int64_t CInputStreamMultiSource::GetLength()
+{
+ int64_t length = 0;
+ for (const auto& iter : m_InputStreams)
+ {
+ length = std::max(length, iter->GetLength());
+ }
+
+ return length;
+}
+
+bool CInputStreamMultiSource::IsEOF()
+{
+ if (m_InputStreams.empty())
+ return true;
+
+ for (const auto& iter : m_InputStreams)
+ {
+ if (!(iter->IsEOF()))
+ return false;
+ }
+
+ return true;
+}
+
+CDVDInputStream::ENextStream CInputStreamMultiSource::NextStream()
+{
+ bool eOF = IsEOF();
+ if (m_InputStreams.empty() || eOF)
+ return NEXTSTREAM_NONE;
+
+
+ CDVDInputStream::ENextStream next;
+ for (const auto& iter : m_InputStreams)
+ {
+ next = iter->NextStream();
+ if (next != NEXTSTREAM_NONE)
+ return next;
+ }
+
+ return NEXTSTREAM_RETRY;
+}
+
+bool CInputStreamMultiSource::Open()
+{
+ if (!m_pPlayer || m_filenames.empty())
+ return false;
+
+ for (unsigned int i = 0; i < m_filenames.size(); i++)
+ {
+ CFileItem fileitem = CFileItem(m_filenames[i], false);
+ fileitem.SetMimeTypeForInternetFile();
+ InputStreamPtr inputstream(CDVDFactoryInputStream::CreateInputStream(m_pPlayer, fileitem));
+ if (!inputstream)
+ {
+ CLog::Log(LOGERROR,
+ "CDVDPlayer::OpenInputStream - unable to create input stream for file [{}]",
+ m_filenames[i]);
+ continue;
+ }
+
+ if (!inputstream->Open())
+ {
+ CLog::Log(LOGERROR, "CDVDPlayer::OpenInputStream - error opening file [{}]", m_filenames[i]);
+ continue;
+ }
+ m_InputStreams.push_back(inputstream);
+ }
+ return !m_InputStreams.empty();
+}
+
+int CInputStreamMultiSource::Read(uint8_t* buf, int buf_size)
+{
+ return -1;
+}
+
+int64_t CInputStreamMultiSource::Seek(int64_t offset, int whence)
+{
+ return -1;
+}
+
+void CInputStreamMultiSource::SetReadRate(uint32_t rate)
+{
+ for (const auto& iter : m_InputStreams)
+ iter->SetReadRate(rate);
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiSource.h b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiSource.h
new file mode 100644
index 0000000..5242f6d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiSource.h
@@ -0,0 +1,42 @@
+/*
+ * 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 "DVDInputStream.h"
+#include "InputStreamMultiStreams.h"
+
+#include <string>
+#include <vector>
+
+class IVideoPlayer;
+
+class CInputStreamMultiSource : public InputStreamMultiStreams
+{
+
+public:
+ CInputStreamMultiSource(IVideoPlayer* pPlayer, const CFileItem& fileitem, const std::vector<std::string>& filenames);
+ ~CInputStreamMultiSource() override;
+
+ void Abort() override;
+ void Close() override;
+ BitstreamStats GetBitstreamStats() const override ;
+ int GetBlockSize() override;
+ bool GetCacheStatus(XFILE::SCacheStatus *status) override;
+ int64_t GetLength() override;
+ bool IsEOF() override;
+ CDVDInputStream::ENextStream NextStream() override;
+ bool Open() override;
+ int Read(uint8_t* buf, int buf_size) override;
+ int64_t Seek(int64_t offset, int whence) override;
+ void SetReadRate(uint32_t rate) override;
+
+protected:
+ IVideoPlayer* m_pPlayer;
+ std::vector<std::string> m_filenames;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiStreams.h b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiStreams.h
new file mode 100644
index 0000000..6ee1bdb
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamMultiStreams.h
@@ -0,0 +1,31 @@
+/*
+ * 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 "DVDInputStream.h"
+
+#include <string>
+#include <vector>
+
+typedef std::shared_ptr<CDVDInputStream> InputStreamPtr;
+class IDVDPlayer;
+
+class InputStreamMultiStreams : public CDVDInputStream
+{
+ friend class CDemuxMultiSource;
+
+public:
+ InputStreamMultiStreams(DVDStreamType type, const CFileItem& fileitem)
+ : CDVDInputStream(type, fileitem) {}
+
+ ~InputStreamMultiStreams() override = default;
+
+protected:
+ std::vector<InputStreamPtr> m_InputStreams; // input streams for current playing file
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRBase.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRBase.cpp
new file mode 100644
index 0000000..03c4bf4
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRBase.cpp
@@ -0,0 +1,369 @@
+/*
+ * 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 "InputStreamPVRBase.h"
+
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/DVDDemuxers/DVDDemux.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+
+CInputStreamPVRBase::CInputStreamPVRBase(IVideoPlayer* pPlayer, const CFileItem& fileitem)
+ : CDVDInputStream(DVDSTREAM_TYPE_PVRMANAGER, fileitem),
+ m_eof(true),
+ m_StreamProps(new PVR_STREAM_PROPERTIES()),
+ m_client(CServiceBroker::GetPVRManager().GetClient(fileitem))
+{
+ if (!m_client)
+ CLog::Log(LOGERROR,
+ "CInputStreamPVRBase - {} - unable to obtain pvr addon instance for item '{}'",
+ __FUNCTION__, fileitem.GetPath());
+}
+
+CInputStreamPVRBase::~CInputStreamPVRBase()
+{
+ m_streamMap.clear();
+}
+
+bool CInputStreamPVRBase::IsEOF()
+{
+ return m_eof;
+}
+
+bool CInputStreamPVRBase::Open()
+{
+ if (CDVDInputStream::Open() && OpenPVRStream())
+ {
+ m_eof = false;
+ m_StreamProps->iStreamCount = 0;
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+void CInputStreamPVRBase::Close()
+{
+ ClosePVRStream();
+ CDVDInputStream::Close();
+ m_eof = true;
+}
+
+int CInputStreamPVRBase::Read(uint8_t* buf, int buf_size)
+{
+ int ret = ReadPVRStream(buf, buf_size);
+
+ // we currently don't support non completing reads
+ if (ret == 0)
+ m_eof = true;
+ else if (ret < -1)
+ ret = -1;
+
+ return ret;
+}
+
+int64_t CInputStreamPVRBase::Seek(int64_t offset, int whence)
+{
+ if (whence == SEEK_POSSIBLE)
+ return CanSeek() ? 1 : 0;
+
+ int64_t ret = SeekPVRStream(offset, whence);
+
+ // if we succeed, we are not eof anymore
+ if (ret >= 0)
+ m_eof = false;
+
+ return ret;
+}
+
+int64_t CInputStreamPVRBase::GetLength()
+{
+ return GetPVRStreamLength();
+}
+
+int CInputStreamPVRBase::GetBlockSize()
+{
+ int ret = -1;
+
+ if (m_client)
+ m_client->GetStreamReadChunkSize(ret);
+
+ return ret;
+}
+
+bool CInputStreamPVRBase::GetTimes(Times &times)
+{
+ PVR_STREAM_TIMES streamTimes = {};
+ if (m_client && m_client->GetStreamTimes(&streamTimes) == PVR_ERROR_NO_ERROR)
+ {
+ times.startTime = streamTimes.startTime;
+ times.ptsStart = streamTimes.ptsStart;
+ times.ptsBegin = streamTimes.ptsBegin;
+ times.ptsEnd = streamTimes.ptsEnd;
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+CDVDInputStream::ENextStream CInputStreamPVRBase::NextStream()
+{
+ return NextPVRStream();
+}
+
+bool CInputStreamPVRBase::CanPause()
+{
+ return CanPausePVRStream();
+}
+
+bool CInputStreamPVRBase::CanSeek()
+{
+ return CanSeekPVRStream();
+}
+
+void CInputStreamPVRBase::Pause(bool bPaused)
+{
+ if (m_client)
+ m_client->PauseStream(bPaused);
+}
+
+bool CInputStreamPVRBase::IsRealtime()
+{
+ bool ret = false;
+
+ if (m_client)
+ m_client->IsRealTimeStream(ret);
+
+ return ret;
+}
+
+bool CInputStreamPVRBase::OpenDemux()
+{
+ if (m_client)
+ {
+ m_client->GetStreamProperties(m_StreamProps.get());
+ UpdateStreamMap();
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+DemuxPacket* CInputStreamPVRBase::ReadDemux()
+{
+ if (!m_client)
+ return nullptr;
+
+ DemuxPacket* pPacket = nullptr;
+ m_client->DemuxRead(pPacket);
+ if (!pPacket)
+ {
+ return nullptr;
+ }
+ else if (pPacket->iStreamId == DMX_SPECIALID_STREAMINFO)
+ {
+ m_client->GetStreamProperties(m_StreamProps.get());
+ return pPacket;
+ }
+ else if (pPacket->iStreamId == DMX_SPECIALID_STREAMCHANGE)
+ {
+ m_client->GetStreamProperties(m_StreamProps.get());
+ UpdateStreamMap();
+ }
+
+ return pPacket;
+}
+
+CDemuxStream* CInputStreamPVRBase::GetStream(int iStreamId) const
+{
+ const auto stream = m_streamMap.find(iStreamId);
+ if (stream != m_streamMap.end())
+ return stream->second.get();
+ else
+ return nullptr;
+}
+
+std::vector<CDemuxStream*> CInputStreamPVRBase::GetStreams() const
+{
+ std::vector<CDemuxStream*> streams;
+
+ streams.reserve(m_streamMap.size());
+ for (const auto& st : m_streamMap)
+ streams.emplace_back(st.second.get());
+
+ return streams;
+}
+
+int CInputStreamPVRBase::GetNrOfStreams() const
+{
+ return m_StreamProps->iStreamCount;
+}
+
+void CInputStreamPVRBase::SetSpeed(int Speed)
+{
+ if (m_client)
+ m_client->SetSpeed(Speed);
+}
+
+void CInputStreamPVRBase::FillBuffer(bool mode)
+{
+ if (m_client)
+ m_client->FillBuffer(mode);
+}
+
+bool CInputStreamPVRBase::SeekTime(double timems, bool backwards, double *startpts)
+{
+ if (m_client)
+ return m_client->SeekTime(timems, backwards, startpts) == PVR_ERROR_NO_ERROR;
+ else
+ return false;
+}
+
+void CInputStreamPVRBase::AbortDemux()
+{
+ if (m_client)
+ m_client->DemuxAbort();
+}
+
+void CInputStreamPVRBase::FlushDemux()
+{
+ if (m_client)
+ m_client->DemuxFlush();
+}
+
+std::shared_ptr<CDemuxStream> CInputStreamPVRBase::GetStreamInternal(int iStreamId)
+{
+ const auto stream = m_streamMap.find(iStreamId);
+ if (stream != m_streamMap.end())
+ return stream->second;
+ else
+ return nullptr;
+}
+
+void CInputStreamPVRBase::UpdateStreamMap()
+{
+ std::map<int, std::shared_ptr<CDemuxStream>> newStreamMap;
+
+ int num = GetNrOfStreams();
+ for (int i = 0; i < num; ++i)
+ {
+ PVR_STREAM_PROPERTIES::PVR_STREAM stream = m_StreamProps->stream[i];
+
+ std::shared_ptr<CDemuxStream> dStream = GetStreamInternal(stream.iPID);
+
+ if (stream.iCodecType == PVR_CODEC_TYPE_AUDIO)
+ {
+ std::shared_ptr<CDemuxStreamAudio> streamAudio;
+
+ if (dStream)
+ streamAudio = std::dynamic_pointer_cast<CDemuxStreamAudio>(dStream);
+ if (!streamAudio)
+ streamAudio = std::make_shared<CDemuxStreamAudio>();
+
+ streamAudio->iChannels = stream.iChannels;
+ streamAudio->iSampleRate = stream.iSampleRate;
+ streamAudio->iBlockAlign = stream.iBlockAlign;
+ streamAudio->iBitRate = stream.iBitRate;
+ streamAudio->iBitsPerSample = stream.iBitsPerSample;
+
+ dStream = streamAudio;
+ }
+ else if (stream.iCodecType == PVR_CODEC_TYPE_VIDEO)
+ {
+ std::shared_ptr<CDemuxStreamVideo> streamVideo;
+
+ if (dStream)
+ streamVideo = std::dynamic_pointer_cast<CDemuxStreamVideo>(dStream);
+ if (!streamVideo)
+ streamVideo = std::make_shared<CDemuxStreamVideo>();
+
+ streamVideo->iFpsScale = stream.iFPSScale;
+ streamVideo->iFpsRate = stream.iFPSRate;
+ streamVideo->iHeight = stream.iHeight;
+ streamVideo->iWidth = stream.iWidth;
+ streamVideo->fAspect = static_cast<double>(stream.fAspect);
+
+ dStream = streamVideo;
+ }
+ else if (stream.iCodecId == AV_CODEC_ID_DVB_TELETEXT)
+ {
+ std::shared_ptr<CDemuxStreamTeletext> streamTeletext;
+
+ if (dStream)
+ streamTeletext = std::dynamic_pointer_cast<CDemuxStreamTeletext>(dStream);
+ if (!streamTeletext)
+ streamTeletext = std::make_shared<CDemuxStreamTeletext>();
+
+ dStream = streamTeletext;
+ }
+ else if (stream.iCodecType == PVR_CODEC_TYPE_SUBTITLE)
+ {
+ std::shared_ptr<CDemuxStreamSubtitle> streamSubtitle;
+
+ if (dStream)
+ streamSubtitle = std::dynamic_pointer_cast<CDemuxStreamSubtitle>(dStream);
+ if (!streamSubtitle)
+ streamSubtitle = std::make_shared<CDemuxStreamSubtitle>();
+
+ if (stream.iSubtitleInfo)
+ {
+ streamSubtitle->ExtraData = std::make_unique<uint8_t[]>(4);
+ streamSubtitle->ExtraSize = 4;
+ streamSubtitle->ExtraData[0] = (stream.iSubtitleInfo >> 8) & 0xff;
+ streamSubtitle->ExtraData[1] = (stream.iSubtitleInfo >> 0) & 0xff;
+ streamSubtitle->ExtraData[2] = (stream.iSubtitleInfo >> 24) & 0xff;
+ streamSubtitle->ExtraData[3] = (stream.iSubtitleInfo >> 16) & 0xff;
+ }
+ dStream = streamSubtitle;
+ }
+ else if (stream.iCodecType == PVR_CODEC_TYPE_RDS &&
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ "pvrplayback.enableradiords"))
+ {
+ std::shared_ptr<CDemuxStreamRadioRDS> streamRadioRDS;
+
+ if (dStream)
+ streamRadioRDS = std::dynamic_pointer_cast<CDemuxStreamRadioRDS>(dStream);
+ if (!streamRadioRDS)
+ streamRadioRDS = std::make_shared<CDemuxStreamRadioRDS>();
+
+ dStream = streamRadioRDS;
+ }
+ else if (stream.iCodecType == PVR_CODEC_TYPE_ID3)
+ {
+ std::shared_ptr<CDemuxStreamAudioID3> streamAudioID3;
+
+ if (dStream)
+ streamAudioID3 = std::dynamic_pointer_cast<CDemuxStreamAudioID3>(dStream);
+ if (!streamAudioID3)
+ streamAudioID3 = std::make_shared<CDemuxStreamAudioID3>();
+
+ dStream = std::move(streamAudioID3);
+ }
+ else
+ dStream = std::make_shared<CDemuxStream>();
+
+ dStream->codec = (AVCodecID)stream.iCodecId;
+ dStream->uniqueId = stream.iPID;
+ dStream->language = stream.strLanguage;
+
+ newStreamMap[stream.iPID] = dStream;
+ }
+
+ m_streamMap = newStreamMap;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRBase.h b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRBase.h
new file mode 100644
index 0000000..b42f742
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRBase.h
@@ -0,0 +1,83 @@
+/*
+ * 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 "DVDInputStream.h"
+
+#include <map>
+#include <memory>
+#include <vector>
+
+class CFileItem;
+class IDemux;
+class IVideoPlayer;
+struct PVR_STREAM_PROPERTIES;
+
+namespace PVR
+{
+ class CPVRClient;
+}
+
+class CInputStreamPVRBase
+ : public CDVDInputStream
+ , public CDVDInputStream::ITimes
+ , public CDVDInputStream::IDemux
+{
+public:
+ CInputStreamPVRBase(IVideoPlayer* pPlayer, const CFileItem& fileitem);
+ ~CInputStreamPVRBase() override;
+ bool Open() override;
+ void Close() override;
+ int Read(uint8_t* buf, int buf_size) override;
+ int64_t Seek(int64_t offset, int whence) override;
+ bool IsEOF() override;
+ int64_t GetLength() override;
+ int GetBlockSize() override;
+
+ ENextStream NextStream() override;
+ bool IsRealtime() override;
+
+ CDVDInputStream::ITimes* GetITimes() override { return this; }
+ bool GetTimes(Times &times) override;
+
+ bool CanSeek() override; //! @todo drop this
+ bool CanPause() override;
+ void Pause(bool bPaused);
+
+ // Demux interface
+ CDVDInputStream::IDemux* GetIDemux() override { return nullptr; }
+ bool OpenDemux() override;
+ DemuxPacket* ReadDemux() override;
+ CDemuxStream* GetStream(int iStreamId) const override;
+ std::vector<CDemuxStream*> GetStreams() const override;
+ int GetNrOfStreams() const override;
+ void SetSpeed(int iSpeed) override;
+ void FillBuffer(bool mode) override;
+ bool SeekTime(double time, bool backward = false, double* startpts = NULL) override;
+ void AbortDemux() override;
+ void FlushDemux() override;
+
+protected:
+ void UpdateStreamMap();
+ std::shared_ptr<CDemuxStream> GetStreamInternal(int iStreamId);
+
+ virtual bool OpenPVRStream() = 0;
+ virtual void ClosePVRStream() = 0;
+ virtual int ReadPVRStream(uint8_t* buf, int buf_size) = 0;
+ virtual int64_t SeekPVRStream(int64_t offset, int whence) = 0;
+ virtual int64_t GetPVRStreamLength() = 0;
+ virtual ENextStream NextPVRStream() = 0;
+ virtual bool CanPausePVRStream() = 0;
+ virtual bool CanSeekPVRStream() = 0;
+
+ bool m_eof;
+ std::shared_ptr<PVR_STREAM_PROPERTIES> m_StreamProps;
+ std::map<int, std::shared_ptr<CDemuxStream>> m_streamMap;
+ std::shared_ptr<PVR::CPVRClient> m_client;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRChannel.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRChannel.cpp
new file mode 100644
index 0000000..b2cde61
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRChannel.cpp
@@ -0,0 +1,125 @@
+/*
+ * 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 "InputStreamPVRChannel.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "utils/log.h"
+
+using namespace PVR;
+
+CInputStreamPVRChannel::CInputStreamPVRChannel(IVideoPlayer* pPlayer, const CFileItem& fileitem)
+ : CInputStreamPVRBase(pPlayer, fileitem),
+ m_bDemuxActive(false)
+{
+}
+
+CInputStreamPVRChannel::~CInputStreamPVRChannel()
+{
+ Close();
+}
+
+CDVDInputStream::IDemux* CInputStreamPVRChannel::GetIDemux()
+{
+ if (m_bDemuxActive)
+ return this;
+
+ return CInputStreamPVRBase::GetIDemux();
+}
+
+bool CInputStreamPVRChannel::OpenPVRStream()
+{
+ std::shared_ptr<CPVRChannel> channel = m_item.GetPVRChannelInfoTag();
+ if (!channel)
+ channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetByPath(m_item.GetPath());
+
+ if (!channel)
+ CLog::Log(LOGERROR,
+ "CInputStreamPVRChannel - {} - unable to obtain channel instance for channel {}",
+ __FUNCTION__, m_item.GetPath());
+
+ if (channel && m_client && (m_client->OpenLiveStream(channel) == PVR_ERROR_NO_ERROR))
+ {
+ m_bDemuxActive = m_client->GetClientCapabilities().HandlesDemuxing();
+ CLog::Log(LOGDEBUG, "CInputStreamPVRChannel - {} - opened channel stream {}", __FUNCTION__,
+ m_item.GetPath());
+ return true;
+ }
+ return false;
+}
+
+void CInputStreamPVRChannel::ClosePVRStream()
+{
+ if (m_client && (m_client->CloseLiveStream() == PVR_ERROR_NO_ERROR))
+ {
+ m_bDemuxActive = false;
+ CLog::Log(LOGDEBUG, "CInputStreamPVRChannel - {} - closed channel stream {}", __FUNCTION__,
+ m_item.GetPath());
+ }
+}
+
+int CInputStreamPVRChannel::ReadPVRStream(uint8_t* buf, int buf_size)
+{
+ int ret = -1;
+
+ if (m_client)
+ m_client->ReadLiveStream(buf, buf_size, ret);
+
+ return ret;
+}
+
+int64_t CInputStreamPVRChannel::SeekPVRStream(int64_t offset, int whence)
+{
+ int64_t ret = -1;
+
+ if (m_client)
+ m_client->SeekLiveStream(offset, whence, ret);
+
+ return ret;
+}
+
+int64_t CInputStreamPVRChannel::GetPVRStreamLength()
+{
+ int64_t ret = -1;
+
+ if (m_client)
+ m_client->GetLiveStreamLength(ret);
+
+ return ret;
+}
+
+CDVDInputStream::ENextStream CInputStreamPVRChannel::NextPVRStream()
+{
+ if (m_eof)
+ return NEXTSTREAM_OPEN;
+
+ return NEXTSTREAM_RETRY;
+}
+
+bool CInputStreamPVRChannel::CanPausePVRStream()
+{
+ bool ret = false;
+
+ if (m_client)
+ m_client->CanPauseStream(ret);
+
+ return ret;
+}
+
+bool CInputStreamPVRChannel::CanSeekPVRStream()
+{
+ bool ret = false;
+
+ if (m_client)
+ m_client->CanSeekStream(ret);
+
+ return ret;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRChannel.h b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRChannel.h
new file mode 100644
index 0000000..d8d8beb
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRChannel.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "InputStreamPVRBase.h"
+
+class CInputStreamPVRChannel : public CInputStreamPVRBase
+{
+public:
+ CInputStreamPVRChannel(IVideoPlayer* pPlayer, const CFileItem& fileitem);
+ ~CInputStreamPVRChannel() override;
+
+ CDVDInputStream::IDemux* GetIDemux() override;
+
+protected:
+ bool OpenPVRStream() override;
+ void ClosePVRStream() override;
+ int ReadPVRStream(uint8_t* buf, int buf_size) override;
+ int64_t SeekPVRStream(int64_t offset, int whence) override;
+ int64_t GetPVRStreamLength() override;
+ ENextStream NextPVRStream() override;
+ bool CanPausePVRStream() override;
+ bool CanSeekPVRStream() override;
+
+private:
+ bool m_bDemuxActive;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRRecording.cpp b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRRecording.cpp
new file mode 100644
index 0000000..6c41ffb
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRRecording.cpp
@@ -0,0 +1,102 @@
+/*
+ * 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 "InputStreamPVRRecording.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "utils/log.h"
+
+using namespace PVR;
+
+CInputStreamPVRRecording::CInputStreamPVRRecording(IVideoPlayer* pPlayer, const CFileItem& fileitem)
+ : CInputStreamPVRBase(pPlayer, fileitem)
+{
+}
+
+CInputStreamPVRRecording::~CInputStreamPVRRecording()
+{
+ Close();
+}
+
+bool CInputStreamPVRRecording::OpenPVRStream()
+{
+ std::shared_ptr<CPVRRecording> recording = m_item.GetPVRRecordingInfoTag();
+ if (!recording)
+ recording = CServiceBroker::GetPVRManager().Recordings()->GetByPath(m_item.GetPath());
+
+ if (!recording)
+ CLog::Log(
+ LOGERROR,
+ "CInputStreamPVRRecording - {} - unable to obtain recording instance for recording {}",
+ __FUNCTION__, m_item.GetPath());
+
+ if (recording && m_client && (m_client->OpenRecordedStream(recording) == PVR_ERROR_NO_ERROR))
+ {
+ CLog::Log(LOGDEBUG, "CInputStreamPVRRecording - {} - opened recording stream {}", __FUNCTION__,
+ m_item.GetPath());
+ return true;
+ }
+ return false;
+}
+
+void CInputStreamPVRRecording::ClosePVRStream()
+{
+ if (m_client && (m_client->CloseRecordedStream() == PVR_ERROR_NO_ERROR))
+ {
+ CLog::Log(LOGDEBUG, "CInputStreamPVRRecording - {} - closed recording stream {}", __FUNCTION__,
+ m_item.GetPath());
+ }
+}
+
+int CInputStreamPVRRecording::ReadPVRStream(uint8_t* buf, int buf_size)
+{
+ int iRead = -1;
+
+ if (m_client)
+ m_client->ReadRecordedStream(buf, buf_size, iRead);
+
+ return iRead;
+}
+
+int64_t CInputStreamPVRRecording::SeekPVRStream(int64_t offset, int whence)
+{
+ int64_t ret = -1;
+
+ if (m_client)
+ m_client->SeekRecordedStream(offset, whence, ret);
+
+ return ret;
+}
+
+int64_t CInputStreamPVRRecording::GetPVRStreamLength()
+{
+ int64_t ret = -1;
+
+ if (m_client)
+ m_client->GetRecordedStreamLength(ret);
+
+ return ret;
+}
+
+CDVDInputStream::ENextStream CInputStreamPVRRecording::NextPVRStream()
+{
+ return NEXTSTREAM_NONE;
+}
+
+bool CInputStreamPVRRecording::CanPausePVRStream()
+{
+ return true;
+}
+
+bool CInputStreamPVRRecording::CanSeekPVRStream()
+{
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRRecording.h b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRRecording.h
new file mode 100644
index 0000000..2a873c9
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/InputStreamPVRRecording.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "InputStreamPVRBase.h"
+
+class CInputStreamPVRRecording : public CInputStreamPVRBase
+{
+public:
+ CInputStreamPVRRecording(IVideoPlayer* pPlayer, const CFileItem& fileitem);
+ ~CInputStreamPVRRecording() override;
+
+protected:
+ bool OpenPVRStream() override;
+ void ClosePVRStream() override;
+ int ReadPVRStream(uint8_t* buf, int buf_size) override;
+ int64_t SeekPVRStream(int64_t offset, int whence) override;
+ int64_t GetPVRStreamLength() override;
+ ENextStream NextPVRStream() override;
+ bool CanPausePVRStream() override;
+ bool CanSeekPVRStream() override;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/config.h b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/config.h
new file mode 100644
index 0000000..0a310ea
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/config.h
@@ -0,0 +1,76 @@
+/*
+ * 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
+
+/* config.h. Generated by hand. */
+#if defined(TARGET_POSIX)
+#include "PlatformDefs.h"
+#else
+#include <windows.h>
+#endif
+#include <stdio.h>
+
+//#define HAVE_DLFCN_H 1
+#define HAVE_DVDCSS_DVDCSS_H 1
+/* #undef HAVE_DVDCSS_DVDCSS_H*/
+/* #undef HAVE_INTTYPES_H */
+#define HAVE_MEMORY_H 1
+#define HAVE_STDINT_H 1
+#define HAVE_STDLIB_H 1
+#define HAVE_STRINGS_H 1
+#define HAVE_STRING_H 1
+#define HAVE_SYS_STAT_H 1
+#define HAVE_SYS_TYPES_H 1
+/* #undef HAVE_UNISTD_H */
+#ifndef PACKAGE
+#define PACKAGE "libdvdread"
+#endif
+#ifndef PACKAGE_BUGREPORT
+#define PACKAGE_BUGREPORT ""
+#endif
+#ifndef PACKAGE_NAME
+#define PACKAGE_NAME ""
+#endif
+#ifndef PACKAGE_STRING
+#define PACKAGE_STRING ""
+#endif
+#ifndef PACKAGE_TARNAME
+#define PACKAGE_TARNAME ""
+#endif
+#ifndef PACKAGE_VERSION
+#define PACKAGE_VERSION ""
+#endif
+#define STDC_HEADERS 1
+#ifndef VERSION
+#define VERSION "1.2.6"
+#endif
+/* #undef WORDS_BIGENDIAN */
+/* #undef __DARWIN__ */
+/* #undef const */
+#define inline __inline
+/* #undef size_t */
+
+#define ssize_t int
+
+#ifndef PATH_MAX
+#define PATH_MAX MAX_PATH
+#endif
+
+#ifndef S_ISDIR
+#define S_ISDIR(m) ((m) & _S_IFDIR)
+#endif
+#ifndef S_ISREG
+#define S_ISREG(m) ((m) & _S_IFREG)
+#endif
+#ifndef S_ISBLK
+#define S_ISBLK(m) 0
+#endif
+#ifndef S_ISCHR
+#define S_ISCHR(m) 0
+#endif
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvd_reader.h b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvd_reader.h
new file mode 100644
index 0000000..ba80262
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvd_reader.h
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2001, 2002 Billy Biggs <vektor@dumbterm.net>,
+ * Håkan Hjort <d95hjort@dtek.chalmers.se>,
+ * Björn Englund <d4bjorn@dtek.chalmers.se>
+ *
+ * This file is part of libdvdread.
+ *
+ * libdvdread 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 of the License, or
+ * (at your option) any later version.
+ *
+ * libdvdread 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 libdvdread; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#ifdef _MSC_VER
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#endif
+
+#include <sys/types.h>
+//#include <inttypes.h>
+#include <stdarg.h>
+
+/*****************************************************************************
+* iovec structure: vectored data entry
+*****************************************************************************/
+#ifdef TARGET_WINDOWS
+struct iovec
+{
+ void *iov_base; /* Pointer to data. */
+ size_t iov_len; /* Length of data. */
+};
+#else
+# include <sys/uio.h> /* struct iovec */
+#endif
+
+/**
+ * The DVD access interface.
+ *
+ * This file contains the functions that form the interface for
+ * reading files located on a DVD.
+ */
+
+/**
+ * The length of one Logical Block of a DVD.
+ */
+#define DVD_VIDEO_LB_LEN 2048
+
+/**
+ * Maximum length of filenames allowed in UDF.
+ */
+#define MAX_UDF_FILE_NAME_LEN 2048
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Opaque type that is used as a handle for one instance of an opened DVD.
+ */
+typedef struct dvd_reader_s dvd_reader_t;
+typedef struct dvd_reader_device_s dvd_reader_device_t;
+
+/**
+ * Opaque type for a file read handle, much like a normal fd or FILE *.
+ */
+typedef struct dvd_file_s dvd_file_t;
+
+struct dvd_reader_stream_cb
+{
+ int (*pf_seek)(void* p_stream, uint64_t i_pos);
+ int (*pf_read)(void* p_stream, void* buffer, int i_read);
+ int (*pf_readv)(void* p_stream, void* p_iovec, int i_blocks);
+};
+typedef struct dvd_reader_stream_cb dvd_reader_stream_cb;
+
+/**
+ * Custom logger callback for DVDOpen[Stream]2
+ * @param private Handle as provided in Open functions
+ * @param level Log level
+ * @param fmt Format string
+ * @param args Arguments list
+ * pf_log(priv, level, fmt, args);
+ */
+typedef enum
+{
+ DVD_LOGGER_LEVEL_INFO,
+ DVD_LOGGER_LEVEL_ERROR,
+ DVD_LOGGER_LEVEL_WARN,
+ DVD_LOGGER_LEVEL_DEBUG,
+} dvd_logger_level_t;
+
+typedef struct
+{
+ void (*pf_log)(void*, dvd_logger_level_t, const char*, va_list);
+} dvd_logger_cb;
+
+/**
+ * Public type that is used to provide statistics on a handle.
+ */
+typedef struct {
+ off_t size; /**< Total size of file in bytes */
+ int nr_parts; /**< Number of file parts */
+ off_t parts_size[9]; /**< Size of each part in bytes */
+} dvd_stat_t;
+
+/**
+ * Opens a block device of a DVD-ROM file, or an image file, or a directory
+ * name for a mounted DVD or HD copy of a DVD.
+ * The second form of Open function (DVDOpenStream) can be used to
+ * provide custom stream_cb functions to access the DVD (see libdvdcss).
+ *
+ * If the given file is a block device, or is the mountpoint for a block
+ * device, then that device is used for CSS authentication using libdvdcss.
+ * If no device is available, then no CSS authentication is performed,
+ * and we hope that the image is decrypted.
+ *
+ * If the path given is a directory, then the files in that directory may be
+ * in any one of these formats:
+ *
+ * path/VIDEO_TS/VTS_01_1.VOB
+ * path/video_ts/vts_01_1.vob
+ * path/VTS_01_1.VOB
+ * path/vts_01_1.vob
+ *
+ * @param path Specifies the the device, file or directory to be used.
+ * @param stream is a private handle used by stream_cb
+ * @param stream_cb is a struct containing seek and read functions
+ * @return If successful a a read handle is returned. Otherwise 0 is returned.
+ *
+ * dvd = DVDOpen(path);
+ * dvd = DVDOpenStream(stream, &stream_cb);
+ */
+dvd_reader_t *DVDOpen( const char * );
+dvd_reader_t* DVDOpenStream(void*, dvd_reader_stream_cb*);
+
+/**
+ * Same as DVDOpen, but with private handle to be passed back on callbacks
+ *
+ * @param path Specifies the the device, file or directory to be used.
+ * @param priv is a private handle
+ * @param logcb is a custom logger callback struct, or NULL if none needed
+ * @param stream_cb is a struct containing seek and read functions
+ * @return If successful a a read handle is returned. Otherwise 0 is returned.
+ *
+ * dvd = DVDOpen2(priv, logcb, path);
+ * dvd = DVDOpenStream2(priv, logcb, &stream_cb);
+ */
+dvd_reader_t* DVDOpen2(void*, const dvd_logger_cb*, const char*);
+dvd_reader_t* DVDOpenStream2(void*, const dvd_logger_cb*, dvd_reader_stream_cb*);
+
+/**
+ * Closes and cleans up the DVD reader object.
+ *
+ * You must close all open files before calling this function.
+ *
+ * @param dvd A read handle that should be closed.
+ *
+ * DVDClose(dvd);
+ */
+void DVDClose( dvd_reader_t * );
+
+/**
+ *
+ */
+typedef enum
+{
+ DVD_READ_INFO_FILE, /**< VIDEO_TS.IFO or VTS_XX_0.IFO (title) */
+ DVD_READ_INFO_BACKUP_FILE, /**< VIDEO_TS.BUP or VTS_XX_0.BUP (title) */
+ DVD_READ_MENU_VOBS, /**< VIDEO_TS.VOB or VTS_XX_0.VOB (title) */
+ DVD_READ_TITLE_VOBS /**< VTS_XX_[1-9].VOB (title). All files in
+ the title set are opened and read as a
+ single file. */
+} dvd_read_domain_t;
+
+/**
+ * Stats a file on the DVD given the title number and domain.
+ * The information about the file is stored in a dvd_stat_t
+ * which contains information about the size of the file and
+ * the number of parts in case of a multipart file and the respective
+ * sizes of the parts.
+ * A multipart file is for instance VTS_02_1.VOB, VTS_02_2.VOB, VTS_02_3.VOB
+ * The size of VTS_02_1.VOB will be stored in stat->parts_size[0],
+ * VTS_02_2.VOB in stat->parts_size[1], ...
+ * The total size (sum of all parts) is stored in stat->size and
+ * stat->nr_parts will hold the number of parts.
+ * Only DVD_READ_TITLE_VOBS (VTS_??_[1-9].VOB) can be multipart files.
+ *
+ * This function is only of use if you want to get the size of each file
+ * in the filesystem. These sizes are not needed to use any other
+ * functions in libdvdread.
+ *
+ * @param dvd A dvd read handle.
+ * @param titlenum Which Video Title Set should be used, VIDEO_TS is 0.
+ * @param domain Which domain.
+ * @param stat Pointer to where the result is stored.
+ * @return If successful 0, otherwise -1.
+ *
+ * int DVDFileStat(dvd, titlenum, domain, stat);
+ */
+int DVDFileStat(dvd_reader_t *, int, dvd_read_domain_t, dvd_stat_t *);
+
+/**
+ * Opens a file on the DVD given the title number and domain.
+ *
+ * If the title number is 0, the video manager information is opened
+ * (VIDEO_TS.[IFO,BUP,VOB]). Returns a file structure which may be
+ * used for reads, or 0 if the file was not found.
+ *
+ * @param dvd A dvd read handle.
+ * @param titlenum Which Video Title Set should be used, VIDEO_TS is 0.
+ * @param domain Which domain.
+ * @return If successful a a file read handle is returned, otherwise 0.
+ *
+ * dvd_file = DVDOpenFile(dvd, titlenum, domain); */
+dvd_file_t *DVDOpenFile( dvd_reader_t *, int, dvd_read_domain_t );
+
+/**
+ * Closes a file and frees the associated structure.
+ *
+ * @param dvd_file The file read handle to be closed.
+ *
+ * DVDCloseFile(dvd_file);
+ */
+void DVDCloseFile( dvd_file_t * );
+
+/**
+ * Reads block_count number of blocks from the file at the given block offset.
+ * Returns number of blocks read on success, -1 on error. This call is only
+ * for reading VOB data, and should not be used when reading the IFO files.
+ * When reading from an encrypted drive, blocks are decrypted using libdvdcss
+ * where required.
+ *
+ * @param dvd_file A file read handle.
+ * @param offset Block offset from the start of the file to start reading at.
+ * @param block_count Number of block to read.
+ * @param data Pointer to a buffer to write the data into.
+ * @return Returns number of blocks read on success, -1 on error.
+ *
+ * blocks_read = DVDReadBlocks(dvd_file, offset, block_count, data);
+ */
+ssize_t DVDReadBlocks( dvd_file_t *, int, size_t, unsigned char * );
+
+/**
+ * Seek to the given position in the file. Returns the resulting position in
+ * bytes from the beginning of the file. The seek position is only used for
+ * byte reads from the file, the block read call always reads from the given
+ * offset.
+ *
+ * @param dvd_file A file read handle.
+ * @param seek_offset Byte offset from the start of the file to seek to.
+ * @return The resulting position in bytes from the beginning of the file.
+ *
+ * offset_set = DVDFileSeek(dvd_file, seek_offset);
+ */
+int32_t DVDFileSeek( dvd_file_t *, int32_t );
+
+/**
+ * Reads the given number of bytes from the file. This call can only be used
+ * on the information files, and may not be used for reading from a VOB. This
+ * reads from and increments the current seek position for the file.
+ *
+ * @param dvd_file A file read handle.
+ * @param data Pointer to a buffer to write the data into.
+ * @param bytes Number of bytes to read.
+ * @return Returns number of bytes read on success, -1 on error.
+ *
+ * bytes_read = DVDReadBytes(dvd_file, data, bytes);
+ */
+ssize_t DVDReadBytes( dvd_file_t *, void *, size_t );
+
+/**
+ * Returns the file size in blocks.
+ *
+ * @param dvd_file A file read handle.
+ * @return The size of the file in blocks, -1 on error.
+ *
+ * blocks = DVDFileSize(dvd_file);
+ */
+ssize_t DVDFileSize( dvd_file_t * );
+
+/**
+ * Get a unique 128 bit disc ID.
+ * This is the MD5 sum of VIDEO_TS.IFO and the VTS_0?_0.IFO files
+ * in title order (those that exist).
+ * If you need a 'text' representation of the id, print it as a
+ * hexadecimal number, using lowercase letters, discid[0] first.
+ * I.e. the same format as the command-line 'md5sum' program uses.
+ *
+ * @param dvd A read handle to get the disc ID from
+ * @param discid The buffer to put the disc ID into. The buffer must
+ * have room for 128 bits (16 chars).
+ * @return 0 on success, -1 on error.
+ */
+int DVDDiscID( dvd_reader_t *, unsigned char * );
+
+/**
+ * Get the UDF VolumeIdentifier and VolumeSetIdentifier
+ * from the PrimaryVolumeDescriptor.
+ *
+ * @param dvd A read handle to get the disc ID from
+ * @param volid The buffer to put the VolumeIdentifier into.
+ * The VolumeIdentifier is latin-1 encoded (8bit unicode)
+ * null terminated and max 32 bytes (including '\0')
+ * @param volid_size No more than volid_size bytes will be copied to volid.
+ * If the VolumeIdentifier is truncated because of this
+ * it will still be null terminated.
+ * @param volsetid The buffer to put the VolumeSetIdentifier into.
+ * The VolumeIdentifier is 128 bytes as
+ * stored in the UDF PrimaryVolumeDescriptor.
+ * Note that this is not a null terminated string.
+ * @param volsetid_size At most volsetid_size bytes will be copied to volsetid.
+ * @return 0 on success, -1 on error.
+ */
+int DVDUDFVolumeInfo(dvd_reader_t*, char*, unsigned int, unsigned char*, unsigned int);
+
+int DVDFileSeekForce( dvd_file_t *, int offset, int force_size);
+
+/**
+ * Get the ISO9660 VolumeIdentifier and VolumeSetIdentifier
+ *
+ * * Only use this function as fallback if DVDUDFVolumeInfo returns -1 *
+ * * this will happen on a disc mastered only with a iso9660 filesystem *
+ * * All video DVD discs have UDF filesystem *
+ *
+ * @param dvd A read handle to get the disc ID from
+ * @param volid The buffer to put the VolumeIdentifier into.
+ * The VolumeIdentifier is coded with '0-9','A-Z','_'
+ * null terminated and max 33 bytes (including '\0')
+ * @param volid_size No more than volid_size bytes will be copied to volid.
+ * If the VolumeIdentifier is truncated because of this
+ * it will still be null terminated.
+ * @param volsetid The buffer to put the VolumeSetIdentifier into.
+ * The VolumeIdentifier is 128 bytes as
+ * stored in the ISO9660 PrimaryVolumeDescriptor.
+ * Note that this is not a null terminated string.
+ * @param volsetid_size At most volsetid_size bytes will be copied to volsetid.
+ * @return 0 on success, -1 on error.
+ */
+int DVDISOVolumeInfo(dvd_reader_t*, char*, unsigned int, unsigned char*, unsigned int);
+
+/**
+ * Sets the level of caching that is done when reading from a device
+ *
+ * @param dvd A read handle to get the disc ID from
+ * @param level The level of caching wanted.
+ * -1 - returns the current setting.
+ * 0 - UDF Cache turned off.
+ * 1 - (default level) Pointers to IFO files and some data from
+ * PrimaryVolumeDescriptor are cached.
+ *
+ * @return The level of caching.
+ */
+int DVDUDFCacheLevel( dvd_reader_t *, int );
+
+#ifdef __cplusplus
+};
+#endif
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvd_types.h b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvd_types.h
new file mode 100644
index 0000000..5b2e802
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvd_types.h
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2000, 2001 Björn Englund, Håkan Hjort
+ *
+ * This file is part of libdvdnav, a DVD navigation library. It is a modified
+ * file originally part of the Ogle DVD player project.
+ *
+ * libdvdnav 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 of the License, or
+ * (at your option) any later version.
+ *
+ * libdvdnav 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 libdvdnav; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/*
+ * Various useful structs and enums for DVDs.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+/*
+ * DVD Menu ID
+ * (see dvdnav_menu_call())
+ */
+typedef enum {
+ /* When used in VTS domain, DVD_MENU_Escape behaves like DVD_MENU_Root,
+ * but from within a menu domain, DVD_MENU_Escape resumes playback. */
+ DVD_MENU_Escape = 0,
+ DVD_MENU_Title = 2,
+ DVD_MENU_Root = 3,
+ DVD_MENU_Subpicture = 4,
+ DVD_MENU_Audio = 5,
+ DVD_MENU_Angle = 6,
+ DVD_MENU_Part = 7
+} DVDMenuID_t;
+
+/*
+ * Stream Types
+ * (see dvdnav_get_number_of_streams())
+ */
+typedef enum
+{
+ DVD_SUBTITLE_STREAM = 0,
+ DVD_AUDIO_STREAM = 1
+} dvdnav_stream_type_t;
+
+/* Domain */
+typedef enum
+{
+ DVD_DOMAIN_FirstPlay = 1, /* First Play Domain */
+ DVD_DOMAIN_VTSTitle = 2, /* Video Title Set Domain */
+ DVD_DOMAIN_VMGM = 4, /* Video Manager Domain */
+ DVD_DOMAIN_VTSMenu = 8 /* Video Title Set Menu Domain */
+} DVDDomain_t;
+
+/*
+ * Structure containing info on highlight areas
+ * (see dvdnav_get_highlight_area())
+ */
+typedef struct {
+ uint32_t palette; /* The CLUT entries for the highlight palette
+ (4-bits per entry -> 4 entries) */
+ uint16_t sx,sy,ex,ey; /* The start/end x,y positions */
+ uint32_t pts; /* Highlight PTS to match with SPU */
+
+ /* button number for the SPU decoder/overlaying engine */
+ uint32_t buttonN;
+} dvdnav_highlight_area_t;
+
+/* The audio format */
+typedef enum
+{
+ DVD_AUDIO_FORMAT_AC3 = 0,
+ DVD_AUDIO_FORMAT_UNKNOWN_1 = 1,
+ DVD_AUDIO_FORMAT_MPEG = 2,
+ DVD_AUDIO_FORMAT_MPEG2_EXT = 3,
+ DVD_AUDIO_FORMAT_LPCM = 4,
+ DVD_AUDIO_FORMAT_UNKNOWN_5 = 5,
+ DVD_AUDIO_FORMAT_DTS = 6,
+ DVD_AUDIO_FORMAT_SDDS = 7
+} DVDAudioFormat_t;
+
+/* the following types are currently unused */
+
+#if 0
+
+/* User operation permissions */
+typedef enum {
+ UOP_FLAG_TitleOrTimePlay = 0x00000001,
+ UOP_FLAG_ChapterSearchOrPlay = 0x00000002,
+ UOP_FLAG_TitlePlay = 0x00000004,
+ UOP_FLAG_Stop = 0x00000008,
+ UOP_FLAG_GoUp = 0x00000010,
+ UOP_FLAG_TimeOrChapterSearch = 0x00000020,
+ UOP_FLAG_PrevOrTopPGSearch = 0x00000040,
+ UOP_FLAG_NextPGSearch = 0x00000080,
+ UOP_FLAG_ForwardScan = 0x00000100,
+ UOP_FLAG_BackwardScan = 0x00000200,
+ UOP_FLAG_TitleMenuCall = 0x00000400,
+ UOP_FLAG_RootMenuCall = 0x00000800,
+ UOP_FLAG_SubPicMenuCall = 0x00001000,
+ UOP_FLAG_AudioMenuCall = 0x00002000,
+ UOP_FLAG_AngleMenuCall = 0x00004000,
+ UOP_FLAG_ChapterMenuCall = 0x00008000,
+ UOP_FLAG_Resume = 0x00010000,
+ UOP_FLAG_ButtonSelectOrActivate = 0x00020000,
+ UOP_FLAG_StillOff = 0x00040000,
+ UOP_FLAG_PauseOn = 0x00080000,
+ UOP_FLAG_AudioStreamChange = 0x00100000,
+ UOP_FLAG_SubPicStreamChange = 0x00200000,
+ UOP_FLAG_AngleChange = 0x00400000,
+ UOP_FLAG_KaraokeAudioPresModeChange = 0x00800000,
+ UOP_FLAG_VideoPresModeChange = 0x01000000
+} DVDUOP_t;
+
+/* Parental Level */
+typedef enum {
+ DVD_PARENTAL_LEVEL_1 = 1,
+ DVD_PARENTAL_LEVEL_2 = 2,
+ DVD_PARENTAL_LEVEL_3 = 3,
+ DVD_PARENTAL_LEVEL_4 = 4,
+ DVD_PARENTAL_LEVEL_5 = 5,
+ DVD_PARENTAL_LEVEL_6 = 6,
+ DVD_PARENTAL_LEVEL_7 = 7,
+ DVD_PARENTAL_LEVEL_8 = 8,
+ DVD_PARENTAL_LEVEL_None = 15
+} DVDParentalLevel_t;
+
+/* Language ID (ISO-639 language code) */
+typedef uint16_t DVDLangID_t;
+
+/* Country ID (ISO-3166 country code) */
+typedef uint16_t DVDCountryID_t;
+
+/* Register */
+typedef uint16_t DVDRegister_t;
+typedef enum {
+ DVDFalse = 0,
+ DVDTrue = 1
+} DVDBool_t;
+typedef DVDRegister_t DVDGPRMArray_t[16];
+typedef DVDRegister_t DVDSPRMArray_t[24];
+
+/* Navigation */
+typedef int DVDStream_t;
+typedef int DVDPTT_t;
+typedef int DVDTitle_t;
+
+/* Angle number (1-9 or default?) */
+typedef int DVDAngle_t;
+
+/* Timecode */
+typedef struct {
+ uint8_t Hours;
+ uint8_t Minutes;
+ uint8_t Seconds;
+ uint8_t Frames;
+} DVDTimecode_t;
+
+/* Subpicture stream number (0-31,62,63) */
+typedef int DVDSubpictureStream_t;
+
+/* Audio stream number (0-7, 15(none)) */
+typedef int DVDAudioStream_t;
+
+/* The audio application mode */
+typedef enum {
+ DVD_AUDIO_APP_MODE_None = 0,
+ DVD_AUDIO_APP_MODE_Karaoke = 1,
+ DVD_AUDIO_APP_MODE_Surround = 2,
+ DVD_AUDIO_APP_MODE_Other = 3
+} DVDAudioAppMode_t;
+
+/* Audio language extension */
+typedef enum {
+ DVD_AUDIO_LANG_EXT_NotSpecified = 0,
+ DVD_AUDIO_LANG_EXT_NormalCaptions = 1,
+ DVD_AUDIO_LANG_EXT_VisuallyImpaired = 2,
+ DVD_AUDIO_LANG_EXT_DirectorsComments1 = 3,
+ DVD_AUDIO_LANG_EXT_DirectorsComments2 = 4
+} DVDAudioLangExt_t;
+
+/* Subpicture language extension */
+typedef enum {
+ DVD_SUBPICTURE_LANG_EXT_NotSpecified = 0,
+ DVD_SUBPICTURE_LANG_EXT_NormalCaptions = 1,
+ DVD_SUBPICTURE_LANG_EXT_BigCaptions = 2,
+ DVD_SUBPICTURE_LANG_EXT_ChildrensCaptions = 3,
+ DVD_SUBPICTURE_LANG_EXT_NormalCC = 5,
+ DVD_SUBPICTURE_LANG_EXT_BigCC = 6,
+ DVD_SUBPICTURE_LANG_EXT_ChildrensCC = 7,
+ DVD_SUBPICTURE_LANG_EXT_Forced = 9,
+ DVD_SUBPICTURE_LANG_EXT_NormalDirectorsComments = 13,
+ DVD_SUBPICTURE_LANG_EXT_BigDirectorsComments = 14,
+ DVD_SUBPICTURE_LANG_EXT_ChildrensDirectorsComments = 15,
+} DVDSubpictureLangExt_t;
+
+/* Karaoke Downmix mode */
+typedef enum {
+ DVD_KARAOKE_DOWNMIX_0to0 = 0x0001,
+ DVD_KARAOKE_DOWNMIX_1to0 = 0x0002,
+ DVD_KARAOKE_DOWNMIX_2to0 = 0x0004,
+ DVD_KARAOKE_DOWNMIX_3to0 = 0x0008,
+ DVD_KARAOKE_DOWNMIX_4to0 = 0x0010,
+ DVD_KARAOKE_DOWNMIX_Lto0 = 0x0020,
+ DVD_KARAOKE_DOWNMIX_Rto0 = 0x0040,
+ DVD_KARAOKE_DOWNMIX_0to1 = 0x0100,
+ DVD_KARAOKE_DOWNMIX_1to1 = 0x0200,
+ DVD_KARAOKE_DOWNMIX_2to1 = 0x0400,
+ DVD_KARAOKE_DOWNMIX_3to1 = 0x0800,
+ DVD_KARAOKE_DOWNMIX_4to1 = 0x1000,
+ DVD_KARAOKE_DOWNMIX_Lto1 = 0x2000,
+ DVD_KARAOKE_DOWNMIX_Rto1 = 0x4000
+} DVDKaraokeDownmix_t;
+typedef int DVDKaraokeDownmixMask_t;
+
+/* Display mode */
+typedef enum {
+ DVD_DISPLAY_MODE_ContentDefault = 0,
+ DVD_DISPLAY_MODE_16x9 = 1,
+ DVD_DISPLAY_MODE_4x3PanScan = 2,
+ DVD_DISPLAY_MODE_4x3Letterboxed = 3
+} DVDDisplayMode_t;
+
+/* Audio attributes */
+typedef struct {
+ DVDAudioAppMode_t AppMode;
+ DVDAudioFormat_t AudioFormat;
+ DVDLangID_t Language;
+ DVDAudioLangExt_t LanguageExtension;
+ DVDBool_t HasMultichannelInfo;
+ DVDAudioSampleFreq_t SampleFrequency;
+ DVDAudioSampleQuant_t SampleQuantization;
+ DVDChannelNumber_t NumberOfChannels;
+} DVDAudioAttributes_t;
+typedef int DVDAudioSampleFreq_t;
+typedef int DVDAudioSampleQuant_t;
+typedef int DVDChannelNumber_t;
+
+/* Subpicture attributes */
+typedef enum {
+ DVD_SUBPICTURE_TYPE_NotSpecified = 0,
+ DVD_SUBPICTURE_TYPE_Language = 1,
+ DVD_SUBPICTURE_TYPE_Other = 2
+} DVDSubpictureType_t;
+typedef enum {
+ DVD_SUBPICTURE_CODING_RunLength = 0,
+ DVD_SUBPICTURE_CODING_Extended = 1,
+ DVD_SUBPICTURE_CODING_Other = 2
+} DVDSubpictureCoding_t;
+typedef struct {
+ DVDSubpictureType_t Type;
+ DVDSubpictureCoding_t CodingMode;
+ DVDLangID_t Language;
+ DVDSubpictureLangExt_t LanguageExtension;
+} DVDSubpictureAttributes_t;
+
+/* Video attributes */
+typedef struct {
+ DVDBool_t PanscanPermitted;
+ DVDBool_t LetterboxPermitted;
+ int AspectX;
+ int AspectY;
+ int FrameRate;
+ int FrameHeight;
+ DVDVideoCompression_t Compression;
+ DVDBool_t Line21Field1InGop;
+ DVDBool_t Line21Field2InGop;
+ int more_to_come;
+} DVDVideoAttributes_t;
+typedef int DVDVideoCompression_t;
+
+#endif
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvdnav.h b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvdnav.h
new file mode 100644
index 0000000..63d501b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvdnav.h
@@ -0,0 +1,789 @@
+/*
+ * Copyright (C) 2001 Rich Wareham <richwareham@users.sourceforge.net>
+ *
+ * This file is part of libdvdnav, a DVD navigation library.
+ *
+ * libdvdnav 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 of the License, or
+ * (at your option) any later version.
+ *
+ * libdvdnav 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 libdvdnav; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/*
+ * This is the main header file applications should include if they want
+ * to access dvdnav functionality.
+ */
+
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "dvd_reader.h"
+#include "dvd_types.h"
+#include "dvdnav_events.h"
+#include "nav_types.h"
+#include "version.h"
+
+#include <stdarg.h>
+
+ /*********************************************************************
+ * dvdnav data types *
+ *********************************************************************/
+
+ /*
+ * Opaque data-type can be viewed as a 'DVD handle'. You should get
+ * a pointer to a dvdnav_t from the dvdnav_open() function.
+ * Never call free() on the pointer, you have to give it back with
+ * dvdnav_close().
+ */
+ typedef struct dvdnav_s dvdnav_t;
+
+ /* Status as reported by most of libdvdnav's functions */
+ typedef int32_t dvdnav_status_t;
+
+ typedef dvd_reader_stream_cb dvdnav_stream_cb;
+
+/*
+ * Unless otherwise stated, all functions return DVDNAV_STATUS_OK if
+ * they succeeded, otherwise DVDNAV_STATUS_ERR is returned and the error may
+ * be obtained by calling dvdnav_err_to_string().
+ */
+#define DVDNAV_STATUS_ERR 0
+#define DVDNAV_STATUS_OK 1
+
+/*********************************************************************
+ * initialisation & housekeeping functions *
+ *********************************************************************/
+
+/*
+ * Logger callback definition
+ */
+typedef enum
+{
+ DVDNAV_LOGGER_LEVEL_INFO,
+ DVDNAV_LOGGER_LEVEL_ERROR,
+ DVDNAV_LOGGER_LEVEL_WARN,
+ DVDNAV_LOGGER_LEVEL_DEBUG,
+} dvdnav_logger_level_t;
+
+typedef struct
+{
+ void (*pf_log)(void*, dvdnav_logger_level_t, const char*, va_list);
+} dvdnav_logger_cb;
+
+/*
+ * These functions allow you to open a DVD device and associate it
+ * with a dvdnav_t.
+ */
+
+/*
+ * Attempts to open the DVD drive at the specified path or using external
+ * seek/read functions (dvdnav_open_stream) and pre-cache the CSS-keys.
+ * libdvdread is used to access the DVD, so any source supported by libdvdread
+ * can be given with "path" or "stream_cb". Currently, using dvdnav_open,
+ * libdvdread can access : DVD drives, DVD image files, DVD file-by-file
+ * copies. Using dvdnav_open_stream, libdvdread can access any kind of DVD
+ * storage via custom implementation of seek/read functions.
+ *
+ * The resulting dvdnav_t handle will be written to *dest.
+ */
+dvdnav_status_t dvdnav_open(dvdnav_t **dest, const char *path);
+dvdnav_status_t dvdnav_open_stream(dvdnav_t** dest, void* priv, dvdnav_stream_cb* stream_cb);
+
+dvdnav_status_t dvdnav_open2(dvdnav_t** dest, void*, const dvdnav_logger_cb*, const char* path);
+dvdnav_status_t dvdnav_open_stream2(dvdnav_t** dest,
+ void* priv,
+ const dvdnav_logger_cb*,
+ dvdnav_stream_cb* stream_cb);
+
+dvdnav_status_t dvdnav_dup(dvdnav_t** dest, dvdnav_t* src);
+dvdnav_status_t dvdnav_free_dup(dvdnav_t* _this);
+
+/*
+ * Closes a dvdnav_t previously opened with dvdnav_open(), freeing any
+ * memory associated with it.
+ */
+dvdnav_status_t dvdnav_close(dvdnav_t *self);
+
+/*
+ * Resets the DVD virtual machine and cache buffers.
+ */
+dvdnav_status_t dvdnav_reset(dvdnav_t *self);
+
+/*
+ * Fills a pointer with a value pointing to a string describing
+ * the path associated with an open dvdnav_t. It assigns *path to NULL
+ * on error.
+ */
+dvdnav_status_t dvdnav_path(dvdnav_t *self, const char **path);
+
+/*
+ * Returns a human-readable string describing the last error.
+ */
+const char* dvdnav_err_to_string(dvdnav_t *self);
+
+const char* dvdnav_version(void);
+
+/*********************************************************************
+ * changing and reading DVD player characteristics *
+ *********************************************************************/
+
+/*
+ * These functions allow you to manipulate the various global characteristics
+ * of the DVD playback engine.
+ */
+
+/*
+ * Sets the region mask (bit 0 set implies region 1, bit 1 set implies
+ * region 2, etc) of the virtual machine. Generally you will only need to set
+ * this if you are playing RCE discs which query the virtual machine as to its
+ * region setting.
+ *
+ * This has _nothing_ to do with the region setting of the DVD drive.
+ */
+dvdnav_status_t dvdnav_set_region_mask(dvdnav_t *self, int32_t region_mask);
+
+/*
+ * Returns the region mask (bit 0 set implies region 1, bit 1 set implies
+ * region 2, etc) of the virtual machine.
+ *
+ * This has _nothing_ to do with the region setting of the DVD drive.
+ */
+dvdnav_status_t dvdnav_get_region_mask(dvdnav_t *self, int32_t *region_mask);
+
+/*
+ * Specify whether read-ahead caching should be used. You may not want this if your
+ * decoding engine does its own buffering.
+ *
+ * The default read-ahead cache does not use an additional thread for the reading
+ * (see read_cache.c for a threaded cache, but note that this code is currently
+ * unmaintained). It prebuffers on VOBU level by reading ahead several buffers
+ * on every read request. The speed of this prebuffering has been optimized to
+ * also work on slow DVD drives.
+ *
+ * If in addition you want to prevent memcpy's to improve performance, have a look
+ * at dvdnav_get_next_cache_block().
+ */
+dvdnav_status_t dvdnav_set_readahead_flag(dvdnav_t *self, int32_t read_ahead_flag);
+
+/*
+ * Query whether read-ahead caching/buffering will be used.
+ */
+dvdnav_status_t dvdnav_get_readahead_flag(dvdnav_t *self, int32_t *read_ahead_flag);
+
+/*
+ * Specify whether the positioning works PGC or PG based.
+ * Programs (PGs) on DVDs are similar to Chapters and a program chain (PGC)
+ * usually covers a whole feature. This affects the behaviour of the
+ * functions dvdnav_get_position() and dvdnav_sector_search(). See there.
+ * Default is PG based positioning.
+ */
+dvdnav_status_t dvdnav_set_PGC_positioning_flag(dvdnav_t *self, int32_t pgc_based_flag);
+
+/*
+ * Query whether positioning is PG or PGC based.
+ */
+dvdnav_status_t dvdnav_get_PGC_positioning_flag(dvdnav_t *self, int32_t *pgc_based_flag);
+
+
+/*********************************************************************
+ * reading data *
+ *********************************************************************/
+
+/*
+ * These functions are used to poll the playback engine and actually get data
+ * off the DVD.
+ */
+
+/*
+ * Attempts to get the next block off the DVD and copies it into the buffer 'buf'.
+ * If there is any special actions that may need to be performed, the value
+ * pointed to by 'event' gets set accordingly.
+ *
+ * If 'event' is DVDNAV_BLOCK_OK then 'buf' is filled with the next block
+ * (note that means it has to be at /least/ 2048 bytes big). 'len' is
+ * then set to 2048.
+ *
+ * Otherwise, buf is filled with an appropriate event structure and
+ * len is set to the length of that structure.
+ *
+ * See the dvdnav_events.h header for information on the various events.
+ */
+dvdnav_status_t dvdnav_get_next_block(dvdnav_t* self, uint8_t* buf, int32_t* event, int32_t* len);
+
+/*
+ * This basically does the same as dvdnav_get_next_block. The only difference is
+ * that it avoids a memcopy, when the requested block was found in the cache.
+ * In such a case (cache hit) this function will return a different pointer than
+ * the one handed in, pointing directly into the relevant block in the cache.
+ * Those pointers must _never_ be freed but instead returned to the library via
+ * dvdnav_free_cache_block().
+ */
+dvdnav_status_t dvdnav_get_next_cache_block(dvdnav_t* self,
+ uint8_t** buf,
+ int32_t* event,
+ int32_t* len);
+
+/*
+ * All buffers which came from the internal cache (when dvdnav_get_next_cache_block()
+ * returned a buffer different from the one handed in) have to be freed with this
+ * function. Although handing in other buffers not from the cache doesn't cause any harm.
+ */
+dvdnav_status_t dvdnav_free_cache_block(dvdnav_t *self, unsigned char *buf);
+
+/*
+ * If we are currently in a still-frame this function skips it.
+ *
+ * See also the DVDNAV_STILL_FRAME event.
+ */
+dvdnav_status_t dvdnav_still_skip(dvdnav_t *self);
+
+/*
+ * If we are currently in WAIT state, that is: the application is required to
+ * wait for its fifos to become empty, calling this signals libdvdnav that this
+ * is achieved and that it can continue.
+ *
+ * See also the DVDNAV_WAIT event.
+ */
+dvdnav_status_t dvdnav_wait_skip(dvdnav_t *self);
+
+/*
+ * Returns the still time from the currently playing cell.
+ * The still time is given in seconds with 0xff meaning an indefinite still.
+ *
+ * This function can be used to detect still frames before they are reached.
+ * Some players might need this to prepare for a frame to be shown for a
+ * longer time than usual.
+ */
+uint32_t dvdnav_get_next_still_flag(dvdnav_t *self);
+
+/*
+ * Stops playback. The next event obtained with one of the get_next_block
+ * functions will be a DVDNAV_STOP event.
+ *
+ * It is not required to call this before dvdnav_close().
+ */
+dvdnav_status_t dvdnav_stop(dvdnav_t *self);
+
+/*
+ * Returns the region mask (bit 0 set implies region 1, bit 1 set implies
+ * region 2, etc) reported by the dvd disc being played.
+ *
+ * Note this has no relation with the region setting of the DVD drive.
+ * Old DVD drives (RPC-I) used to delegate most of the RCE handling to the CPU and
+ * will actually call the virtual machine (VM) for its region setting. In those cases,
+ * changing the VM region mask via dvdnav_set_region_mask() will circunvent
+ * the region protection scheme. This is no longer the case with more recent (RPC-II) drives
+ * as RCE is handled internally by the drive firmware.
+ *
+ */
+dvdnav_status_t dvdnav_get_disk_region_mask(dvdnav_t* self, int32_t* region_mask);
+
+/*********************************************************************
+ * title/part navigation *
+ *********************************************************************/
+
+/*
+ * Returns the number of titles on the disk.
+ */
+dvdnav_status_t dvdnav_get_number_of_titles(dvdnav_t *self, int32_t *titles);
+
+/*
+ * Returns the number of parts within the given title.
+ */
+dvdnav_status_t dvdnav_get_number_of_parts(dvdnav_t *self, int32_t title, int32_t *parts);
+
+/*
+ * Returns the number of angles for the given title.
+ */
+dvdnav_status_t dvdnav_get_number_of_angles(dvdnav_t* self, int32_t title, int32_t* angles);
+
+/*
+ * Plays the specified title of the DVD from its beginning (that is: part 1).
+ */
+dvdnav_status_t dvdnav_title_play(dvdnav_t *self, int32_t title);
+
+/*
+ * Plays the specified title, starting from the specified part.
+ */
+dvdnav_status_t dvdnav_part_play(dvdnav_t *self, int32_t title, int32_t part);
+
+/*
+ * Plays the specified title, starting from the specified program
+ */
+dvdnav_status_t dvdnav_program_play(dvdnav_t *self, int32_t title, int32_t pgcn, int32_t pgn);
+
+/*
+ * Stores in *times an array (that the application *must* free) of
+ * dvdtimes corresponding to the chapter times for the chosen title.
+ * *duration will have the duration of the title
+ * The number of entries in *times is the result of the function.
+ * On error *times is NULL and the output is 0
+ */
+uint32_t dvdnav_describe_title_chapters(dvdnav_t *self, int32_t title, uint64_t **times, uint64_t *duration);
+
+/*
+ * Play the specified amount of parts of the specified title of
+ * the DVD then STOP.
+ *
+ * Currently unimplemented!
+ */
+dvdnav_status_t dvdnav_part_play_auto_stop(dvdnav_t* self,
+ int32_t title,
+ int32_t part,
+ int32_t parts_to_play);
+
+/*
+ * Play the specified title starting from the specified time.
+ *
+ * Currently unimplemented!
+ */
+dvdnav_status_t dvdnav_time_play(dvdnav_t* self, int32_t title, uint64_t time);
+
+/*
+ * Stop playing the current position and jump to the specified menu.
+ *
+ * See also DVDMenuID_t from libdvdread
+ */
+dvdnav_status_t dvdnav_menu_call(dvdnav_t *self, DVDMenuID_t menu);
+
+/*
+ * Return the title number and part currently being played.
+ * A title of 0 indicates we are in a menu. In this case, part
+ * is set to the current menu's ID.
+ */
+dvdnav_status_t dvdnav_current_title_info(dvdnav_t* self, int32_t* title, int32_t* part);
+
+/*
+ * Return the title number, pgcn and pgn currently being played.
+ * A title of 0 indicates, we are in a menu.
+ */
+dvdnav_status_t dvdnav_current_title_program(dvdnav_t* self,
+ int32_t* title,
+ int32_t* pgcn,
+ int32_t* pgn);
+
+/*
+ * Return the current position (in blocks) within the current
+ * title and the length (in blocks) of said title.
+ *
+ * Current implementation is wrong and likely to behave unpredictably!
+ * Use is discouraged!
+ */
+dvdnav_status_t dvdnav_get_position_in_title(dvdnav_t* self, uint32_t* pos, uint32_t* len);
+
+/*
+ * This function is only available for compatibility reasons.
+ *
+ * Stop playing the current position and start playback of the current title
+ * from the specified part.
+ */
+dvdnav_status_t dvdnav_part_search(dvdnav_t *self, int32_t part);
+
+
+/*********************************************************************
+ * program chain/program navigation *
+ *********************************************************************/
+
+/*
+ * Stop playing the current position and start playback from the last
+ * VOBU boundary before the given sector. The sector number is not
+ * meant to be an absolute physical DVD sector, but a relative sector
+ * in the current program. This function cannot leave the current
+ * program and will fail if asked to do so.
+ *
+ * If program chain based positioning is enabled
+ * (see dvdnav_set_PGC_positioning_flag()), this will seek to the relative
+ * sector inside the current program chain.
+ *
+ * 'origin' can be one of SEEK_SET, SEEK_CUR, SEEK_END as defined in
+ * fcntl.h.
+ */
+dvdnav_status_t dvdnav_sector_search(dvdnav_t* self, int64_t offset, int32_t origin);
+
+/*
+ returns the current stream time in PTS ticks as reported by the IFO structures
+ divide it by 90000 to get the current play time in seconds
+ */
+int64_t dvdnav_get_current_time(dvdnav_t *self);
+
+/*
+ * Stop playing the current position and start playback of the title
+ * from the specified timecode.
+ *
+ * Currently implemented using interpolation. That interpolation is slightly
+ * inaccurate.
+ */
+dvdnav_status_t dvdnav_time_search(dvdnav_t* self, uint64_t time);
+
+/*
+ * Find the nearest vobu and jump to it
+ *
+ * Alternative to dvdnav_time_search (see full documentation on searching.jump_to_time.readme)
+ * Jumps to the provided PTS (which is defined as time_in_ms * 90). mode means the navigation mode,
+ * currently only the Default (0) is implemented:
+ * 0: Default. Jump to a time which may be either <> time_in_pts_ticks
+ * 1: After. Always jump to a time that is > time_in_pts_ticks
+ * -1: Before. Always jump to a time that is < time_in_pts_ticks
+ */
+dvdnav_status_t dvdnav_jump_to_sector_by_time(dvdnav_t* self,
+ uint64_t time_in_pts_ticks,
+ int32_t mode);
+
+/*
+ * Stop playing current position and play the "GoUp"-program chain.
+ * (which generally leads to the title menu or a higher-level menu).
+ */
+dvdnav_status_t dvdnav_go_up(dvdnav_t *self);
+
+/*
+ * Stop playing the current position and start playback at the
+ * previous program (if it exists).
+ */
+dvdnav_status_t dvdnav_prev_pg_search(dvdnav_t *self);
+
+/*
+ * Stop playing the current position and start playback at the
+ * first program.
+ */
+dvdnav_status_t dvdnav_top_pg_search(dvdnav_t *self);
+
+/*
+ * Stop playing the current position and start playback at the
+ * next program (if it exists).
+ */
+dvdnav_status_t dvdnav_next_pg_search(dvdnav_t *self);
+
+/*
+ * Return the current position (in blocks) within the current
+ * program and the length (in blocks) of current program.
+ *
+ * If program chain based positioning is enabled
+ * (see dvdnav_set_PGC_positioning_flag()), this will return the
+ * relative position in and the length of the current program chain.
+ */
+dvdnav_status_t dvdnav_get_position(dvdnav_t* self, uint32_t* pos, uint32_t* len);
+
+/*********************************************************************
+ * menu highlights *
+ *********************************************************************/
+
+/*
+ * Most functions related to highlights take a NAV PCI packet as a parameter.
+ * While you can get such a packet from libdvdnav, this will result in
+ * errors for players with internal FIFOs because due to the FIFO length,
+ * libdvdnav will be ahead in the stream compared to what the user is
+ * seeing on screen. Therefore, player applications who have a NAV
+ * packet available, which is better in sync with the actual playback,
+ * should always pass this one to these functions.
+ */
+
+/*
+ * Get the currently highlighted button
+ * number (1..36) or 0 if no button is highlighted.
+ */
+dvdnav_status_t dvdnav_get_current_highlight(dvdnav_t *self, int32_t *button);
+
+/*
+ * Returns the Presentation Control Information (PCI) structure associated
+ * with the current position.
+ *
+ * Read the general notes above.
+ * See also libdvdreads nav_types.h for definition of pci_t.
+ */
+pci_t* dvdnav_get_current_nav_pci(dvdnav_t *self);
+
+/*
+ * Returns the DSI (data search information) structure associated
+ * with the current position.
+ *
+ * Read the general notes above.
+ * See also libdvdreads nav_types.h for definition of dsi_t.
+ */
+dsi_t* dvdnav_get_current_nav_dsi(dvdnav_t *self);
+
+/*
+ * Get the area associated with a certain button.
+ */
+dvdnav_status_t dvdnav_get_highlight_area(pci_t* nav_pci,
+ int32_t button,
+ int32_t mode,
+ dvdnav_highlight_area_t* highlight);
+
+/*
+ * Move button highlight around as suggested by function name (e.g. with arrow keys).
+ */
+dvdnav_status_t dvdnav_upper_button_select(dvdnav_t *self, pci_t *pci);
+dvdnav_status_t dvdnav_lower_button_select(dvdnav_t *self, pci_t *pci);
+dvdnav_status_t dvdnav_right_button_select(dvdnav_t *self, pci_t *pci);
+dvdnav_status_t dvdnav_left_button_select(dvdnav_t *self, pci_t *pci);
+
+/*
+ * Activate ("press") the currently highlighted button.
+ */
+dvdnav_status_t dvdnav_button_activate(dvdnav_t *self, pci_t *pci);
+
+/*
+ * Highlight a specific button.
+ */
+dvdnav_status_t dvdnav_button_select(dvdnav_t *self, pci_t *pci, int32_t button);
+
+/*
+ * Activate ("press") specified button.
+ */
+dvdnav_status_t dvdnav_button_select_and_activate(dvdnav_t *self, pci_t *pci, int32_t button);
+
+/*
+ * Activate ("press") a button and execute specified command.
+ */
+dvdnav_status_t dvdnav_button_activate_cmd(dvdnav_t *self, int32_t button, vm_cmd_t *cmd);
+
+/*
+ * Select button at specified video frame coordinates.
+ */
+dvdnav_status_t dvdnav_mouse_select(dvdnav_t *self, pci_t *pci, int32_t x, int32_t y);
+
+/*
+ * Activate ("press") button at specified video frame coordinates.
+ */
+dvdnav_status_t dvdnav_mouse_activate(dvdnav_t *self, pci_t *pci, int32_t x, int32_t y);
+
+
+/*********************************************************************
+ * languages *
+ *********************************************************************/
+
+/*
+ * The language codes expected by these functions are two character
+ * codes as defined in ISO639.
+ */
+
+/*
+ * Set which menu language we should use per default.
+ */
+dvdnav_status_t dvdnav_menu_language_select(dvdnav_t* self, char* code);
+
+/*
+ * Set which audio language we should use per default.
+ */
+dvdnav_status_t dvdnav_audio_language_select(dvdnav_t* self, char* code);
+
+/*
+ * Set which spu language we should use per default.
+ */
+dvdnav_status_t dvdnav_spu_language_select(dvdnav_t* self, char* code);
+
+/*********************************************************************
+ * obtaining stream attributes *
+ *********************************************************************/
+
+/*
+ * Return a string describing the title of the DVD.
+ * This is an ID string encoded on the disc by the author. In many cases
+ * this is a descriptive string such as `THE_MATRIX' but sometimes is singularly
+ * uninformative such as `PDVD-011421'. Some DVD authors even forget to set this,
+ * so you may also read the default of the authoring software they used, like
+ * `DVDVolume'.
+ */
+dvdnav_status_t dvdnav_get_title_string(dvdnav_t *self, const char **title_str);
+
+/*
+ * Returns a string containing the serial number of the DVD.
+ * This has a max of 15 characters and should be more unique than the
+ * title string.
+ */
+dvdnav_status_t dvdnav_get_serial_string(dvdnav_t *self, const char **serial_str);
+
+/*
+ * Returns the VolumeIdentifier of the disc or NULL if it could
+ * not be obtained. The VolumeIdentifier might be latin-1 encoded
+ * (8bit unicode) null terminated and max 32 bytes (including '\0');
+ * or coded with '0-9','A-Z','_' null terminated and max 33 bytes
+ * (including '\0').
+ * See also dvdnav_get_title_string
+ *
+ * Note: The string is malloc'd so caller has to free() the returned
+ * string when done with it.
+ */
+const char* dvdnav_get_volid_string(dvdnav_t* self);
+
+/*
+ * Get video aspect code.
+ * The aspect code does only change on VTS boundaries.
+ * See the DVDNAV_VTS_CHANGE event.
+ *
+ * 0 -- 4:3, 2 -- 16:9
+ */
+uint8_t dvdnav_get_video_aspect(dvdnav_t *self);
+
+/*
+ * Get video resolution.
+ */
+dvdnav_status_t dvdnav_get_video_resolution(dvdnav_t* self, uint32_t* width, uint32_t* height);
+
+/*
+ * Get video scaling permissions.
+ * The scaling permission does only change on VTS boundaries.
+ * See the DVDNAV_VTS_CHANGE event.
+ *
+ * bit0 set = deny letterboxing, bit1 set = deny pan&scan
+ */
+uint8_t dvdnav_get_video_scale_permission(dvdnav_t *self);
+
+/*
+ * Converts a *logical* audio stream id into language code
+ * (returns 0xffff if no such stream).
+ */
+uint16_t dvdnav_audio_stream_to_lang(dvdnav_t *self, uint8_t stream);
+
+/*
+ * Returns the format of *logical* audio stream 'stream'
+ * (returns 0xffff if no such stream).
+ */
+uint16_t dvdnav_audio_stream_format(dvdnav_t *self, uint8_t stream);
+
+/*
+ * Returns number of channels in *logical* audio stream 'stream'
+ * (returns 0xffff if no such stream).
+ */
+uint16_t dvdnav_audio_stream_channels(dvdnav_t *self, uint8_t stream);
+
+/*
+ * Converts a *logical* subpicture stream id into country code
+ * (returns 0xffff if no such stream).
+ */
+uint16_t dvdnav_spu_stream_to_lang(dvdnav_t *self, uint8_t stream);
+
+/*
+ * Converts a *physical* (MPEG) audio stream id into a logical stream number.
+ */
+int8_t dvdnav_get_audio_logical_stream(dvdnav_t *self, uint8_t audio_num);
+
+#define HAVE_GET_AUDIO_ATTR
+/*
+ * Get audio attr
+ */
+dvdnav_status_t dvdnav_get_audio_attr(dvdnav_t *self, uint8_t audio_mum, audio_attr_t *audio_attr);
+
+/*
+ * Converts a *physical* (MPEG) subpicture stream id into a logical stream number.
+ */
+int8_t dvdnav_get_spu_logical_stream(dvdnav_t *self, uint8_t subp_num);
+
+#define HAVE_GET_SPU_ATTR
+/*
+ * Get spu attr
+ */
+dvdnav_status_t dvdnav_get_spu_attr(dvdnav_t *self, uint8_t audio_mum, subp_attr_t *subp_attr);
+
+/*
+ * Get active audio stream.
+ */
+int8_t dvdnav_get_active_audio_stream(dvdnav_t *self);
+
+/*
+ * Get active spu stream.
+ */
+int8_t dvdnav_get_active_spu_stream(dvdnav_t *self);
+
+/*
+ * Get the set of user operations that are currently prohibited.
+ * There are potentially new restrictions right after
+ * DVDNAV_CHANNEL_HOP and DVDNAV_NAV_PACKET.
+ */
+user_ops_t dvdnav_get_restrictions(dvdnav_t *self);
+
+/*
+ * Returns the number of streams provided its type (e.g. subtitles, audio, etc)
+ */
+int8_t dvdnav_get_number_of_streams(dvdnav_t* self, dvdnav_stream_type_t stream_type);
+
+/*********************************************************************
+ * setting stream attributes *
+ *********************************************************************/
+
+/*
+ * Set the visible (enable) status of the current spu stream
+ * (to enable/disable subtitles)
+ * visibility defines if the spu stream should be enabled/visible (1) or disabled (0)
+ */
+dvdnav_status_t dvdnav_toggle_spu_stream(dvdnav_t* self, uint8_t visibility);
+
+/*
+ * Set the given stream id and stream type as active
+ * stream_num - the physical index of the stream
+ * stream_type - the stream type (audio or subtitles)
+ */
+dvdnav_status_t dvdnav_set_active_stream(dvdnav_t* self,
+ uint8_t stream_num,
+ dvdnav_stream_type_t stream_type);
+
+/*********************************************************************
+ * multiple angles *
+ *********************************************************************/
+
+/*
+ * The libdvdnav library abstracts away the difference between seamless and
+ * non-seamless angles. From the point of view of the programmer you just set the
+ * angle number and all is well in the world. You will always see only the
+ * selected angle coming from the get_next_block functions.
+ *
+ * Note:
+ * It is quite possible that some tremendously strange DVD feature might change the
+ * angle number from under you. Generally you should always view the results from
+ * dvdnav_get_angle_info() as definitive only up to the next time you call
+ * dvdnav_get_next_block().
+ */
+
+/*
+ * Sets the current angle. If you try to follow a non existent angle
+ * the call fails.
+ */
+dvdnav_status_t dvdnav_angle_change(dvdnav_t *self, int32_t angle);
+
+/*
+ * Returns the current angle and number of angles present.
+ */
+dvdnav_status_t dvdnav_get_angle_info(dvdnav_t* self,
+ int32_t* current_angle,
+ int32_t* number_of_angles);
+
+/*********************************************************************
+ * domain queries *
+ *********************************************************************/
+
+/*
+ * Are we in the First Play domain?
+ */
+int8_t dvdnav_is_domain_fp(dvdnav_t *self);
+
+/*
+ * Are we in the Video management Menu domain?
+ */
+int8_t dvdnav_is_domain_vmgm(dvdnav_t *self);
+
+/*
+ * Are we in the Video Title Menu domain?
+ */
+int8_t dvdnav_is_domain_vtsm(dvdnav_t *self);
+
+/*
+ * Are we in the Video Title Set domain?
+ */
+int8_t dvdnav_is_domain_vts(dvdnav_t *self);
+
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvdnav_events.h b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvdnav_events.h
new file mode 100644
index 0000000..970fa6a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/dvdnav_events.h
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2001 Rich Wareham <richwareham@users.sourceforge.net>
+ *
+ * This file is part of libdvdnav, a DVD navigation library.
+ *
+ * libdvdnav 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 of the License, or
+ * (at your option) any later version.
+ *
+ * libdvdnav 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 libdvdnav; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/*
+ * This header defines events and event types
+ */
+
+#pragma once
+
+/*
+ * DVDNAV_BLOCK_OK
+ *
+ * A regular data block from the DVD has been returned.
+ * This one should be demuxed and decoded for playback.
+ */
+#define DVDNAV_BLOCK_OK 0
+
+/*
+ * DVDNAV_NOP
+ *
+ * Just ignore this.
+ */
+#define DVDNAV_NOP 1
+
+/*
+ * DVDNAV_STILL_FRAME
+ *
+ * We have reached a still frame. The player application should wait
+ * the amount of time specified by the still's length while still handling
+ * user input to make menus and other interactive stills work.
+ * The last delivered frame should be kept showing.
+ * Once the still has timed out, call dvdnav_skip_still().
+ * A length of 0xff means an infinite still which has to be skipped
+ * indirectly by some user interaction.
+ */
+#define DVDNAV_STILL_FRAME 2
+
+typedef struct {
+ /* The length (in seconds) the still frame should be displayed for,
+ * or 0xff if infinite. */
+ int length;
+} dvdnav_still_event_t;
+
+
+/*
+ * DVDNAV_SPU_STREAM_CHANGE
+ *
+ * Inform the SPU decoding/overlaying engine to switch SPU channels.
+ */
+#define DVDNAV_SPU_STREAM_CHANGE 3
+
+typedef struct {
+ /* The physical (MPEG) stream number for widescreen SPU display.
+ * Use this, if you blend the SPU on an anamorphic image before
+ * unsqueezing it. */
+ int physical_wide;
+
+ /* The physical (MPEG) stream number for letterboxed display.
+ * Use this, if you blend the SPU on an anamorphic image after
+ * unsqueezing it. */
+ int physical_letterbox;
+
+ /* The physical (MPEG) stream number for pan&scan display.
+ * Use this, if you blend the SPU on an anamorphic image after
+ * unsqueezing it the pan&scan way. */
+ int physical_pan_scan;
+
+ /* The logical (DVD) stream number. */
+ int logical;
+} dvdnav_spu_stream_change_event_t;
+
+
+/*
+ * DVDNAV_AUDIO_STREAM_CHANGE
+ *
+ * Inform the audio decoder to switch channels.
+ */
+#define DVDNAV_AUDIO_STREAM_CHANGE 4
+
+typedef struct {
+ /* The physical (MPEG) stream number. */
+ int physical;
+
+ /* The logical (DVD) stream number. */
+ int logical;
+} dvdnav_audio_stream_change_event_t;
+
+
+/*
+ * DVDNAV_VTS_CHANGE
+ *
+ * Some status information like video aspect and video scale permissions do
+ * not change inside a VTS. Therefore this event can be used to query such
+ * information only when necessary and update the decoding/displaying
+ * accordingly.
+ */
+#define DVDNAV_VTS_CHANGE 5
+
+typedef struct {
+ int old_vtsN; /* the old VTS number */
+ DVDDomain_t old_domain; /* the old domain */
+ int new_vtsN; /* the new VTS number */
+ DVDDomain_t new_domain; /* the new domain */
+} dvdnav_vts_change_event_t;
+
+
+/*
+ * DVDNAV_CELL_CHANGE
+ *
+ * Some status information like the current Title and Part numbers do not
+ * change inside a cell. Therefore this event can be used to query such
+ * information only when necessary and update the decoding/displaying
+ * accordingly.
+ * Some useful information for accurate time display is also reported
+ * together with this event.
+ */
+#define DVDNAV_CELL_CHANGE 6
+
+typedef struct {
+ int cellN; /* the new cell number */
+ int pgN; /* the current program number */
+ int64_t cell_length; /* the length of the current cell in sectors */
+ int64_t pg_length; /* the length of the current program in sectors */
+ int64_t pgc_length; /* the length of the current program chain in PTS ticks */
+ int64_t cell_start; /* the start offset of the current cell relatively to the PGC in sectors */
+ int64_t pg_start; /* the start offset of the current PG relatively to the PGC in sectors */
+} dvdnav_cell_change_event_t;
+
+
+/*
+ * DVDNAV_NAV_PACKET
+ *
+ * NAV packets are useful for various purposes. They define the button
+ * highlight areas and VM commands of DVD menus, so they should in any
+ * case be sent to the SPU decoder/overlaying engine for the menus to work.
+ * NAV packets also provide a way to detect PTS discontinuities, because
+ * they carry the start and end PTS values for the current VOBU.
+ * (pci.vobu_s_ptm and pci.vobu_e_ptm) Whenever the start PTS of the
+ * current NAV does not match the end PTS of the previous NAV, a PTS
+ * discontinuity has occurred.
+ * NAV packets can also be used for time display, because they are
+ * timestamped relatively to the current Cell.
+ */
+#define DVDNAV_NAV_PACKET 7
+
+/*
+ * DVDNAV_STOP
+ *
+ * Applications should end playback here. A subsequent dvdnav_get_next_block()
+ * call will restart the VM from the beginning of the DVD.
+ */
+#define DVDNAV_STOP 8
+
+/*
+ * DVDNAV_HIGHLIGHT
+ *
+ * The current button highlight changed. Inform the overlaying engine to
+ * highlight a different button. Please note, that at the moment only mode 1
+ * highlights are reported this way. That means, when the button highlight
+ * has been moved around by some function call, you will receive an event
+ * telling you the new button. But when a button gets activated, you have
+ * to handle the mode 2 highlighting (that is some different colour the
+ * button turns to on activation) in your application.
+ */
+#define DVDNAV_HIGHLIGHT 9
+
+typedef struct {
+ /* highlight mode: 0 - hide, 1 - show, 2 - activate, currently always 1 */
+ int display;
+
+ /* FIXME: these fields are currently not set */
+ uint32_t palette; /* The CLUT entries for the highlight palette
+ (4-bits per entry -> 4 entries) */
+ uint16_t sx,sy,ex,ey; /* The start/end x,y positions */
+ uint32_t pts; /* Highlight PTS to match with SPU */
+
+ /* button number for the SPU decoder/overlaying engine */
+ uint32_t buttonN;
+} dvdnav_highlight_event_t;
+
+
+/*
+ * DVDNAV_SPU_CLUT_CHANGE
+ *
+ * Inform the SPU decoder/overlaying engine to update its colour lookup table.
+ * The CLUT is given as 16 uint32_t's in the buffer.
+ */
+#define DVDNAV_SPU_CLUT_CHANGE 10
+
+/*
+ * DVDNAV_HOP_CHANNEL
+ *
+ * A non-seamless operation has been performed. Applications can drop all
+ * their internal fifo's content, which will speed up the response.
+ */
+#define DVDNAV_HOP_CHANNEL 12
+
+/*
+ * DVDNAV_WAIT
+ *
+ * We have reached a point in DVD playback, where timing is critical.
+ * Player application with internal fifos can introduce state
+ * inconsistencies, because libdvdnav is always the fifo's length
+ * ahead in the stream compared to what the application sees.
+ * Such applications should wait until their fifos are empty
+ * when they receive this type of event.
+ * Once this is achieved, call dvdnav_skip_wait().
+ */
+#define DVDNAV_WAIT 13
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/ifo_types.h b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/ifo_types.h
new file mode 100644
index 0000000..4191b67
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/ifo_types.h
@@ -0,0 +1,754 @@
+/*
+ * Copyright (C) 2000, 2001 Björn Englund <d4bjorn@dtek.chalmers.se>,
+ * Håkan Hjort <d95hjort@dtek.chalmers.se>
+ *
+ * This file is part of libdvdread.
+ *
+ * libdvdread 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 of the License, or
+ * (at your option) any later version.
+ *
+ * libdvdread 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 libdvdread; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+//#include <inttypes.h>
+#include "dvd_reader.h"
+
+
+#undef ATTRIBUTE_PACKED
+
+#if defined(__GNUC__)
+#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 95)
+#if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 4)) && !defined(__clang__)
+#define ATTRIBUTE_PACKED __attribute__((packed, gcc_struct))
+#else
+#define ATTRIBUTE_PACKED __attribute__((packed))
+#endif
+#define PRAGMA_PACK 0
+#endif
+#endif
+
+#if !defined(ATTRIBUTE_PACKED)
+#define ATTRIBUTE_PACKED
+#define PRAGMA_PACK 1
+#endif
+
+#if PRAGMA_PACK
+#pragma pack(1)
+#endif
+
+
+/**
+ * Common
+ *
+ * The following structures are used in both the VMGI and VTSI.
+ */
+
+
+/**
+ * DVD Time Information.
+ */
+typedef struct {
+ uint8_t hour;
+ uint8_t minute;
+ uint8_t second;
+ uint8_t frame_u; /* The two high bits are the frame rate. */
+} ATTRIBUTE_PACKED dvd_time_t;
+
+/**
+ * Type to store per-command data.
+ */
+typedef struct {
+ uint8_t bytes[8];
+} ATTRIBUTE_PACKED vm_cmd_t;
+#define COMMAND_DATA_SIZE 8U
+
+
+/**
+ * Video Attributes.
+ */
+typedef struct {
+ unsigned char mpeg_version : 2;
+ unsigned char video_format : 2;
+ unsigned char display_aspect_ratio : 2;
+ unsigned char permitted_df : 2;
+
+ unsigned char line21_cc_1 : 1;
+ unsigned char line21_cc_2 : 1;
+ unsigned char unknown1 : 1;
+ unsigned char bit_rate : 1;
+
+ unsigned char picture_size : 2;
+ unsigned char letterboxed : 1;
+ unsigned char film_mode : 1;
+} ATTRIBUTE_PACKED video_attr_t;
+
+/**
+ * Audio Attributes.
+ */
+typedef struct {
+ unsigned char audio_format : 3;
+ unsigned char multichannel_extension : 1;
+ unsigned char lang_type : 2;
+ unsigned char application_mode : 2;
+
+ unsigned char quantization : 2;
+ unsigned char sample_frequency : 2;
+ unsigned char unknown1 : 1;
+ unsigned char channels : 3;
+ uint16_t lang_code;
+ uint8_t lang_extension;
+ uint8_t code_extension;
+ uint8_t unknown3;
+ union {
+ struct ATTRIBUTE_PACKED {
+ unsigned char unknown4 : 1;
+ unsigned char channel_assignment : 3;
+ unsigned char version : 2;
+ unsigned char mc_intro : 1; /* probably 0: true, 1:false */
+ unsigned char mode : 1; /* Karaoke mode 0: solo 1: duet */
+ } karaoke;
+ struct ATTRIBUTE_PACKED {
+ unsigned char unknown5 : 4;
+ unsigned char dolby_encoded : 1; /* suitable for surround decoding */
+ unsigned char unknown6 : 3;
+ } surround;
+ } ATTRIBUTE_PACKED app_info;
+} ATTRIBUTE_PACKED audio_attr_t;
+
+
+/**
+ * MultiChannel Extension
+ */
+typedef struct {
+ unsigned char zero1 : 7;
+ unsigned char ach0_gme : 1;
+
+ unsigned char zero2 : 7;
+ unsigned char ach1_gme : 1;
+
+ unsigned char zero3 : 4;
+ unsigned char ach2_gv1e : 1;
+ unsigned char ach2_gv2e : 1;
+ unsigned char ach2_gm1e : 1;
+ unsigned char ach2_gm2e : 1;
+
+ unsigned char zero4 : 4;
+ unsigned char ach3_gv1e : 1;
+ unsigned char ach3_gv2e : 1;
+ unsigned char ach3_gmAe : 1;
+ unsigned char ach3_se2e : 1;
+
+ unsigned char zero5 : 4;
+ unsigned char ach4_gv1e : 1;
+ unsigned char ach4_gv2e : 1;
+ unsigned char ach4_gmBe : 1;
+ unsigned char ach4_seBe : 1;
+ uint8_t zero6[19];
+} ATTRIBUTE_PACKED multichannel_ext_t;
+
+
+/**
+ * Subpicture Attributes.
+ */
+typedef struct {
+ /*
+ * type: 0 not specified
+ * 1 language
+ * 2 other
+ * coding mode: 0 run length
+ * 1 extended
+ * 2 other
+ * language: indicates language if type == 1
+ * lang extension: if type == 1 contains the lang extension
+ */
+ unsigned char code_mode : 3;
+ unsigned char zero1 : 3;
+ unsigned char type : 2;
+ uint8_t zero2;
+ uint16_t lang_code;
+ uint8_t lang_extension;
+ uint8_t code_extension;
+} ATTRIBUTE_PACKED subp_attr_t;
+
+
+
+/**
+ * PGC Command Table.
+ */
+typedef struct {
+ uint16_t nr_of_pre;
+ uint16_t nr_of_post;
+ uint16_t nr_of_cell;
+ uint16_t last_byte;
+ vm_cmd_t *pre_cmds;
+ vm_cmd_t *post_cmds;
+ vm_cmd_t *cell_cmds;
+} ATTRIBUTE_PACKED pgc_command_tbl_t;
+#define PGC_COMMAND_TBL_SIZE 8U
+
+/**
+ * PGC Program Map
+ */
+typedef uint8_t pgc_program_map_t;
+
+/**
+ * Cell Playback Information.
+ */
+typedef struct {
+ unsigned char block_mode : 2;
+ unsigned char block_type : 2;
+ unsigned char seamless_play : 1;
+ unsigned char interleaved : 1;
+ unsigned char stc_discontinuity : 1;
+ unsigned char seamless_angle : 1;
+ unsigned char zero_1 : 1;
+ unsigned char playback_mode : 1; /**< When set, enter StillMode after each VOBU */
+ unsigned char restricted : 1; /**< ?? drop out of fastforward? */
+ unsigned char cell_type : 5; /** for karaoke, reserved otherwise */
+ uint8_t still_time;
+ uint8_t cell_cmd_nr;
+ dvd_time_t playback_time;
+ uint32_t first_sector;
+ uint32_t first_ilvu_end_sector;
+ uint32_t last_vobu_start_sector;
+ uint32_t last_sector;
+} ATTRIBUTE_PACKED cell_playback_t;
+
+#define BLOCK_TYPE_NONE 0x0
+#define BLOCK_TYPE_ANGLE_BLOCK 0x1
+
+#define BLOCK_MODE_NOT_IN_BLOCK 0x0
+#define BLOCK_MODE_FIRST_CELL 0x1
+#define BLOCK_MODE_IN_BLOCK 0x2
+#define BLOCK_MODE_LAST_CELL 0x3
+
+/**
+ * Cell Position Information.
+ */
+typedef struct {
+ uint16_t vob_id_nr;
+ uint8_t zero_1;
+ uint8_t cell_nr;
+} ATTRIBUTE_PACKED cell_position_t;
+
+/**
+ * User Operations.
+ */
+typedef struct {
+ unsigned char zero : 7; /* 25-31 */
+ unsigned char video_pres_mode_change : 1; /* 24 */
+
+ unsigned char karaoke_audio_pres_mode_change : 1; /* 23 */
+ unsigned char angle_change : 1;
+ unsigned char subpic_stream_change : 1;
+ unsigned char audio_stream_change : 1;
+ unsigned char pause_on : 1;
+ unsigned char still_off : 1;
+ unsigned char button_select_or_activate : 1;
+ unsigned char resume : 1; /* 16 */
+
+ unsigned char chapter_menu_call : 1; /* 15 */
+ unsigned char angle_menu_call : 1;
+ unsigned char audio_menu_call : 1;
+ unsigned char subpic_menu_call : 1;
+ unsigned char root_menu_call : 1;
+ unsigned char title_menu_call : 1;
+ unsigned char backward_scan : 1;
+ unsigned char forward_scan : 1; /* 8 */
+
+ unsigned char next_pg_search : 1; /* 7 */
+ unsigned char prev_or_top_pg_search : 1;
+ unsigned char time_or_chapter_search : 1;
+ unsigned char go_up : 1;
+ unsigned char stop : 1;
+ unsigned char title_play : 1;
+ unsigned char chapter_search_or_play : 1;
+ unsigned char title_or_time_play : 1; /* 0 */
+} ATTRIBUTE_PACKED user_ops_t;
+
+/**
+ * Program Chain Information.
+ */
+typedef struct {
+ uint16_t zero_1;
+ uint8_t nr_of_programs;
+ uint8_t nr_of_cells;
+ dvd_time_t playback_time;
+ user_ops_t prohibited_ops;
+ uint16_t audio_control[8]; /* New type? */
+ uint32_t subp_control[32]; /* New type? */
+ uint16_t next_pgc_nr;
+ uint16_t prev_pgc_nr;
+ uint16_t goup_pgc_nr;
+ uint8_t pg_playback_mode;
+ uint8_t still_time;
+ uint32_t palette[16]; /* New type struct {zero_1, Y, Cr, Cb} ? */
+ uint16_t command_tbl_offset;
+ uint16_t program_map_offset;
+ uint16_t cell_playback_offset;
+ uint16_t cell_position_offset;
+ pgc_command_tbl_t *command_tbl;
+ pgc_program_map_t *program_map;
+ cell_playback_t *cell_playback;
+ cell_position_t *cell_position;
+ int ref_count;
+} ATTRIBUTE_PACKED pgc_t;
+#define PGC_SIZE 236U
+
+/**
+ * Program Chain Information Search Pointer.
+ */
+typedef struct {
+ uint8_t entry_id;
+ unsigned char block_mode : 2;
+ unsigned char block_type : 2;
+ unsigned char zero_1 : 4;
+ uint16_t ptl_id_mask;
+ uint32_t pgc_start_byte;
+ pgc_t *pgc;
+} ATTRIBUTE_PACKED pgci_srp_t;
+#define PGCI_SRP_SIZE 8U
+
+/**
+ * Program Chain Information Table.
+ */
+typedef struct {
+ uint16_t nr_of_pgci_srp;
+ uint16_t zero_1;
+ uint32_t last_byte;
+ pgci_srp_t *pgci_srp;
+ int ref_count;
+} ATTRIBUTE_PACKED pgcit_t;
+#define PGCIT_SIZE 8U
+
+/**
+ * Menu PGCI Language Unit.
+ */
+typedef struct {
+ uint16_t lang_code;
+ uint8_t lang_extension;
+ uint8_t exists;
+ uint32_t lang_start_byte;
+ pgcit_t *pgcit;
+} ATTRIBUTE_PACKED pgci_lu_t;
+#define PGCI_LU_SIZE 8U
+
+/**
+ * Menu PGCI Unit Table.
+ */
+typedef struct {
+ uint16_t nr_of_lus;
+ uint16_t zero_1;
+ uint32_t last_byte;
+ pgci_lu_t *lu;
+} ATTRIBUTE_PACKED pgci_ut_t;
+#define PGCI_UT_SIZE 8U
+
+/**
+ * Cell Address Information.
+ */
+typedef struct {
+ uint16_t vob_id;
+ uint8_t cell_id;
+ uint8_t zero_1;
+ uint32_t start_sector;
+ uint32_t last_sector;
+} ATTRIBUTE_PACKED cell_adr_t;
+
+/**
+ * Cell Address Table.
+ */
+typedef struct {
+ uint16_t nr_of_vobs; /* VOBs */
+ uint16_t zero_1;
+ uint32_t last_byte;
+ cell_adr_t *cell_adr_table; /* No explicit size given. */
+} ATTRIBUTE_PACKED c_adt_t;
+#define C_ADT_SIZE 8U
+
+/**
+ * VOBU Address Map.
+ */
+typedef struct {
+ uint32_t last_byte;
+ uint32_t *vobu_start_sectors;
+} ATTRIBUTE_PACKED vobu_admap_t;
+#define VOBU_ADMAP_SIZE 4U
+
+
+
+
+/**
+ * VMGI
+ *
+ * The following structures relate to the Video Manager.
+ */
+
+/**
+ * Video Manager Information Management Table.
+ */
+typedef struct {
+ char vmg_identifier[12];
+ uint32_t vmg_last_sector;
+ uint8_t zero_1[12];
+ uint32_t vmgi_last_sector;
+ uint8_t zero_2;
+ uint8_t specification_version;
+ uint32_t vmg_category;
+ uint16_t vmg_nr_of_volumes;
+ uint16_t vmg_this_volume_nr;
+ uint8_t disc_side;
+ uint8_t zero_3[19];
+ uint16_t vmg_nr_of_title_sets; /* Number of VTSs. */
+ char provider_identifier[32];
+ uint64_t vmg_pos_code;
+ uint8_t zero_4[24];
+ uint32_t vmgi_last_byte;
+ uint32_t first_play_pgc;
+ uint8_t zero_5[56];
+ uint32_t vmgm_vobs; /* sector */
+ uint32_t tt_srpt; /* sector */
+ uint32_t vmgm_pgci_ut; /* sector */
+ uint32_t ptl_mait; /* sector */
+ uint32_t vts_atrt; /* sector */
+ uint32_t txtdt_mgi; /* sector */
+ uint32_t vmgm_c_adt; /* sector */
+ uint32_t vmgm_vobu_admap; /* sector */
+ uint8_t zero_6[32];
+
+ video_attr_t vmgm_video_attr;
+ uint8_t zero_7;
+ uint8_t nr_of_vmgm_audio_streams; /* should be 0 or 1 */
+ audio_attr_t vmgm_audio_attr;
+ audio_attr_t zero_8[7];
+ uint8_t zero_9[17];
+ uint8_t nr_of_vmgm_subp_streams; /* should be 0 or 1 */
+ subp_attr_t vmgm_subp_attr;
+ subp_attr_t zero_10[27]; /* XXX: how much 'padding' here? */
+} ATTRIBUTE_PACKED vmgi_mat_t;
+
+typedef struct {
+ unsigned char zero_1 : 1;
+ unsigned char multi_or_random_pgc_title : 1; /* 0: one sequential pgc title */
+ unsigned char jlc_exists_in_cell_cmd : 1;
+ unsigned char jlc_exists_in_prepost_cmd : 1;
+ unsigned char jlc_exists_in_button_cmd : 1;
+ unsigned char jlc_exists_in_tt_dom : 1;
+ unsigned char chapter_search_or_play : 1; /* UOP 1 */
+ unsigned char title_or_time_play : 1; /* UOP 0 */
+} ATTRIBUTE_PACKED playback_type_t;
+
+/**
+ * Title Information.
+ */
+typedef struct {
+ playback_type_t pb_ty;
+ uint8_t nr_of_angles;
+ uint16_t nr_of_ptts;
+ uint16_t parental_id;
+ uint8_t title_set_nr;
+ uint8_t vts_ttn;
+ uint32_t title_set_sector;
+} ATTRIBUTE_PACKED title_info_t;
+
+/**
+ * PartOfTitle Search Pointer Table.
+ */
+typedef struct {
+ uint16_t nr_of_srpts;
+ uint16_t zero_1;
+ uint32_t last_byte;
+ title_info_t *title;
+} ATTRIBUTE_PACKED tt_srpt_t;
+#define TT_SRPT_SIZE 8U
+
+
+/**
+ * Parental Management Information Unit Table.
+ * Level 1 (US: G), ..., 7 (US: NC-17), 8
+ */
+#define PTL_MAIT_NUM_LEVEL 8
+typedef uint16_t pf_level_t[PTL_MAIT_NUM_LEVEL];
+
+/**
+ * Parental Management Information Unit Table.
+ */
+typedef struct {
+ uint16_t country_code;
+ uint16_t zero_1;
+ uint16_t pf_ptl_mai_start_byte;
+ uint16_t zero_2;
+ pf_level_t *pf_ptl_mai; /* table of (nr_of_vtss + 1), video_ts is first */
+} ATTRIBUTE_PACKED ptl_mait_country_t;
+#define PTL_MAIT_COUNTRY_SIZE 8U
+
+/**
+ * Parental Management Information Table.
+ */
+typedef struct {
+ uint16_t nr_of_countries;
+ uint16_t nr_of_vtss;
+ uint32_t last_byte;
+ ptl_mait_country_t *countries;
+} ATTRIBUTE_PACKED ptl_mait_t;
+#define PTL_MAIT_SIZE 8U
+
+/**
+ * Video Title Set Attributes.
+ */
+typedef struct {
+ uint32_t last_byte;
+ uint32_t vts_cat;
+
+ video_attr_t vtsm_vobs_attr;
+ uint8_t zero_1;
+ uint8_t nr_of_vtsm_audio_streams; /* should be 0 or 1 */
+ audio_attr_t vtsm_audio_attr;
+ audio_attr_t zero_2[7];
+ uint8_t zero_3[16];
+ uint8_t zero_4;
+ uint8_t nr_of_vtsm_subp_streams; /* should be 0 or 1 */
+ subp_attr_t vtsm_subp_attr;
+ subp_attr_t zero_5[27];
+
+ uint8_t zero_6[2];
+
+ video_attr_t vtstt_vobs_video_attr;
+ uint8_t zero_7;
+ uint8_t nr_of_vtstt_audio_streams;
+ audio_attr_t vtstt_audio_attr[8];
+ uint8_t zero_8[16];
+ uint8_t zero_9;
+ uint8_t nr_of_vtstt_subp_streams;
+ subp_attr_t vtstt_subp_attr[32];
+} ATTRIBUTE_PACKED vts_attributes_t;
+#define VTS_ATTRIBUTES_SIZE 542U
+#define VTS_ATTRIBUTES_MIN_SIZE 356U
+
+/**
+ * Video Title Set Attribute Table.
+ */
+typedef struct {
+ uint16_t nr_of_vtss;
+ uint16_t zero_1;
+ uint32_t last_byte;
+ vts_attributes_t *vts;
+ uint32_t *vts_atrt_offsets; /* offsets table for each vts_attributes */
+} ATTRIBUTE_PACKED vts_atrt_t;
+#define VTS_ATRT_SIZE 8U
+
+/**
+ * Text Data. (Incomplete)
+ */
+typedef struct {
+ uint32_t last_byte; /* offsets are relative here */
+ uint16_t offsets[100]; /* == nr_of_srpts + 1 (first is disc title) */
+#if 0
+ uint16_t unknown; /* 0x48 ?? 0x48 words (16bit) info following */
+ uint16_t zero_1;
+
+ uint8_t type_of_info; /* ?? 01 == disc, 02 == Title, 04 == Title part */
+ uint8_t unknown1;
+ uint8_t unknown2;
+ uint8_t unknown3;
+ uint8_t unknown4; /* ?? always 0x30 language?, text format? */
+ uint8_t unknown5;
+ uint16_t offset; /* from first */
+
+ char text[12]; /* ended by 0x09 */
+#endif
+} ATTRIBUTE_PACKED txtdt_t;
+
+/**
+ * Text Data Language Unit. (Incomplete)
+ */
+typedef struct {
+ uint16_t lang_code;
+ uint8_t zero_1;
+ uint8_t
+ char_set; /* 0x00 reserved Unicode, 0x01 ISO 646, 0x10 JIS Roman & JIS Kanji, 0x11 ISO 8859-1, 0x12 Shift JIS Kanji */
+ uint32_t txtdt_start_byte; /* prt, rel start of vmg_txtdt_mgi */
+ txtdt_t *txtdt;
+} ATTRIBUTE_PACKED txtdt_lu_t;
+#define TXTDT_LU_SIZE 8U
+
+/**
+ * Text Data Manager Information. (Incomplete)
+ */
+typedef struct {
+ char disc_name[12];
+ uint16_t unknown1;
+ uint16_t nr_of_language_units;
+ uint32_t last_byte;
+ txtdt_lu_t *lu;
+} ATTRIBUTE_PACKED txtdt_mgi_t;
+#define TXTDT_MGI_SIZE 20U
+
+
+/**
+ * VTS
+ *
+ * Structures relating to the Video Title Set (VTS).
+ */
+
+/**
+ * Video Title Set Information Management Table.
+ */
+typedef struct {
+ char vts_identifier[12];
+ uint32_t vts_last_sector;
+ uint8_t zero_1[12];
+ uint32_t vtsi_last_sector;
+ uint8_t zero_2;
+ uint8_t specification_version;
+ uint32_t vts_category;
+ uint16_t zero_3;
+ uint16_t zero_4;
+ uint8_t zero_5;
+ uint8_t zero_6[19];
+ uint16_t zero_7;
+ uint8_t zero_8[32];
+ uint64_t zero_9;
+ uint8_t zero_10[24];
+ uint32_t vtsi_last_byte;
+ uint32_t zero_11;
+ uint8_t zero_12[56];
+ uint32_t vtsm_vobs; /* sector */
+ uint32_t vtstt_vobs; /* sector */
+ uint32_t vts_ptt_srpt; /* sector */
+ uint32_t vts_pgcit; /* sector */
+ uint32_t vtsm_pgci_ut; /* sector */
+ uint32_t vts_tmapt; /* sector */
+ uint32_t vtsm_c_adt; /* sector */
+ uint32_t vtsm_vobu_admap; /* sector */
+ uint32_t vts_c_adt; /* sector */
+ uint32_t vts_vobu_admap; /* sector */
+ uint8_t zero_13[24];
+
+ video_attr_t vtsm_video_attr;
+ uint8_t zero_14;
+ uint8_t nr_of_vtsm_audio_streams; /* should be 0 or 1 */
+ audio_attr_t vtsm_audio_attr;
+ audio_attr_t zero_15[7];
+ uint8_t zero_16[17];
+ uint8_t nr_of_vtsm_subp_streams; /* should be 0 or 1 */
+ subp_attr_t vtsm_subp_attr;
+ subp_attr_t zero_17[27];
+ uint8_t zero_18[2];
+
+ video_attr_t vts_video_attr;
+ uint8_t zero_19;
+ uint8_t nr_of_vts_audio_streams;
+ audio_attr_t vts_audio_attr[8];
+ uint8_t zero_20[17];
+ uint8_t nr_of_vts_subp_streams;
+ subp_attr_t vts_subp_attr[32];
+ uint16_t zero_21;
+ multichannel_ext_t vts_mu_audio_attr[8];
+ /* XXX: how much 'padding' here, if any? */
+} ATTRIBUTE_PACKED vtsi_mat_t;
+
+/**
+ * PartOfTitle Unit Information.
+ */
+typedef struct {
+ uint16_t pgcn;
+ uint16_t pgn;
+} ATTRIBUTE_PACKED ptt_info_t;
+
+/**
+ * PartOfTitle Information.
+ */
+typedef struct {
+ uint16_t nr_of_ptts;
+ ptt_info_t *ptt;
+} ATTRIBUTE_PACKED ttu_t;
+
+/**
+ * PartOfTitle Search Pointer Table.
+ */
+typedef struct {
+ uint16_t nr_of_srpts;
+ uint16_t zero_1;
+ uint32_t last_byte;
+ ttu_t *title;
+ uint32_t *ttu_offset; /* offset table for each ttu */
+} ATTRIBUTE_PACKED vts_ptt_srpt_t;
+#define VTS_PTT_SRPT_SIZE 8U
+
+
+/**
+ * Time Map Entry.
+ */
+/* Should this be bit field at all or just the uint32_t? */
+typedef uint32_t map_ent_t;
+
+/**
+ * Time Map.
+ */
+typedef struct {
+ uint8_t tmu; /* Time unit, in seconds */
+ uint8_t zero_1;
+ uint16_t nr_of_entries;
+ map_ent_t *map_ent;
+} ATTRIBUTE_PACKED vts_tmap_t;
+#define VTS_TMAP_SIZE 4U
+
+/**
+ * Time Map Table.
+ */
+typedef struct {
+ uint16_t nr_of_tmaps;
+ uint16_t zero_1;
+ uint32_t last_byte;
+ vts_tmap_t *tmap;
+ uint32_t *tmap_offset; /* offset table for each tmap */
+} ATTRIBUTE_PACKED vts_tmapt_t;
+#define VTS_TMAPT_SIZE 8U
+
+
+#if PRAGMA_PACK
+#pragma pack()
+#endif
+
+
+/**
+ * The following structure defines an IFO file. The structure is divided into
+ * two parts, the VMGI, or Video Manager Information, which is read from the
+ * VIDEO_TS.[IFO,BUP] file, and the VTSI, or Video Title Set Information, which
+ * is read in from the VTS_XX_0.[IFO,BUP] files.
+ */
+typedef struct {
+ /* VMGI */
+ vmgi_mat_t *vmgi_mat;
+ tt_srpt_t *tt_srpt;
+ pgc_t *first_play_pgc;
+ ptl_mait_t *ptl_mait;
+ vts_atrt_t *vts_atrt;
+ txtdt_mgi_t *txtdt_mgi;
+
+ /* Common */
+ pgci_ut_t *pgci_ut;
+ c_adt_t *menu_c_adt;
+ vobu_admap_t *menu_vobu_admap;
+
+ /* VTSI */
+ vtsi_mat_t *vtsi_mat;
+ vts_ptt_srpt_t *vts_ptt_srpt;
+ pgcit_t *vts_pgcit;
+ vts_tmapt_t *vts_tmapt;
+ c_adt_t *vts_c_adt;
+ vobu_admap_t *vts_vobu_admap;
+} ifo_handle_t;
+
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/nav_types.h b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/nav_types.h
new file mode 100644
index 0000000..aa33f23
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/nav_types.h
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2000, 2001, 2002 Håkan Hjort <d95hjort@dtek.chalmers.se>
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ * See LICENSES/README.md for more information.
+ *
+ * The data structures in this file should represent the layout of the
+ * pci and dsi packets as they are stored in the stream. Information
+ * found by reading the source to VOBDUMP is the base for the structure
+ * and names of these data types.
+ *
+ * VOBDUMP: a program for examining DVD .VOB files.
+ * Copyright 1998, 1999 Eric Smith <eric@brouhaha.com>
+ */
+
+#pragma once
+
+//#include <inttypes.h>
+#include "ifo_types.h" /* only dvd_time_t, vm_cmd_t and user_ops_t */
+
+
+#undef ATTRIBUTE_PACKED
+#undef PRAGMA_PACK_BEGIN
+#undef PRAGMA_PACK_END
+
+#if defined(__GNUC__)
+#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 95)
+#define ATTRIBUTE_PACKED __attribute__ ((packed))
+#define PRAGMA_PACK 0
+#endif
+#endif
+
+#if !defined(ATTRIBUTE_PACKED)
+#define ATTRIBUTE_PACKED
+#define PRAGMA_PACK 1
+#endif
+
+
+/* The length including the substream id byte. */
+#define PCI_BYTES 0x3d4
+#define DSI_BYTES 0x3fa
+
+#define PS2_PCI_SUBSTREAM_ID 0x00
+#define PS2_DSI_SUBSTREAM_ID 0x01
+
+/* Remove this */
+#define DSI_START_BYTE 1031
+
+
+#if PRAGMA_PACK
+#pragma pack(1)
+#endif
+
+
+/**
+ * PCI General Information
+ */
+typedef struct {
+ uint32_t nv_pck_lbn; /**< sector address of this nav pack */
+ uint16_t vobu_cat; /**< 'category' of vobu */
+ uint16_t zero1; /**< reserved */
+ user_ops_t vobu_uop_ctl; /**< UOP of vobu */
+ uint32_t vobu_s_ptm; /**< start presentation time of vobu */
+ uint32_t vobu_e_ptm; /**< end presentation time of vobu */
+ uint32_t vobu_se_e_ptm; /**< end ptm of sequence end in vobu */
+ dvd_time_t e_eltm; /**< Cell elapsed time */
+ char vobu_isrc[32];
+} ATTRIBUTE_PACKED pci_gi_t;
+
+/**
+ * Non Seamless Angle Information
+ */
+typedef struct {
+ uint32_t nsml_agl_dsta[9]; /**< address of destination vobu in AGL_C#n */
+} ATTRIBUTE_PACKED nsml_agli_t;
+
+/**
+ * Highlight General Information
+ *
+ * For btngrX_dsp_ty the bits have the following meaning:
+ * 000b: normal 4/3 only buttons
+ * XX1b: wide (16/9) buttons
+ * X1Xb: letterbox buttons
+ * 1XXb: pan&scan buttons
+ */
+typedef struct {
+ uint16_t hli_ss; /**< status, only low 2 bits 0: no buttons, 1: different 2: equal 3: equal except for button cmds */
+ uint32_t hli_s_ptm; /**< start ptm of hli */
+ uint32_t hli_e_ptm; /**< end ptm of hli */
+ uint32_t btn_se_e_ptm; /**< end ptm of button select */
+ unsigned int zero1 : 2; /**< reserved */
+ unsigned int btngr_ns : 2; /**< number of button groups 1, 2 or 3 with 36/18/12 buttons */
+ unsigned int zero2 : 1; /**< reserved */
+ unsigned int btngr1_dsp_ty : 3; /**< display type of subpic stream for button group 1 */
+ unsigned int zero3 : 1; /**< reserved */
+ unsigned int btngr2_dsp_ty : 3; /**< display type of subpic stream for button group 2 */
+ unsigned int zero4 : 1; /**< reserved */
+ unsigned int btngr3_dsp_ty : 3; /**< display type of subpic stream for button group 3 */
+ uint8_t btn_ofn; /**< button offset number range 0-255 */
+ uint8_t btn_ns; /**< number of valid buttons <= 36/18/12 (low 6 bits) */
+ uint8_t nsl_btn_ns; /**< number of buttons selectable by U_BTNNi (low 6 bits) nsl_btn_ns <= btn_ns */
+ uint8_t zero5; /**< reserved */
+ uint8_t fosl_btnn; /**< forcedly selected button (low 6 bits) */
+ uint8_t foac_btnn; /**< forcedly activated button (low 6 bits) */
+} ATTRIBUTE_PACKED hl_gi_t;
+
+
+/**
+ * Button Color Information Table
+ * Each entry being a 32bit word that contains the color indexes and alpha
+ * values to use. They are all represented by 4 bit number and stored
+ * like this [Ci3, Ci2, Ci1, Ci0, A3, A2, A1, A0]. The actual palette
+ * that the indexes reference is in the PGC.
+ * @TODO split the uint32_t into a struct
+ */
+typedef struct {
+ uint32_t btn_coli[3][2]; /**< [button color number-1][select:0/action:1] */
+} ATTRIBUTE_PACKED btn_colit_t;
+
+/**
+ * Button Information
+ *
+ * NOTE: I've had to change the structure from the disk layout to get
+ * the packing to work with Sun's Forte C compiler.
+ * The 4 and 7 bytes are 'rotated' was: ABC DEF GHIJ is: ABCG DEFH IJ
+ */
+typedef struct {
+ unsigned int btn_coln : 2; /**< button color number */
+ unsigned int x_start : 10; /**< x start offset within the overlay */
+ unsigned int zero1 : 2; /**< reserved */
+ unsigned int x_end : 10; /**< x end offset within the overlay */
+
+ unsigned int auto_action_mode : 2; /**< 0: no, 1: activated if selected */
+ unsigned int y_start : 10; /**< y start offset within the overlay */
+ unsigned int zero2 : 2; /**< reserved */
+ unsigned int y_end : 10; /**< y end offset within the overlay */
+
+ unsigned int zero3 : 2; /**< reserved */
+ unsigned int up : 6; /**< button index when pressing up */
+ unsigned int zero4 : 2; /**< reserved */
+ unsigned int down : 6; /**< button index when pressing down */
+ unsigned int zero5 : 2; /**< reserved */
+ unsigned int left : 6; /**< button index when pressing left */
+ unsigned int zero6 : 2; /**< reserved */
+ unsigned int right : 6; /**< button index when pressing right */
+ vm_cmd_t cmd;
+} ATTRIBUTE_PACKED btni_t;
+
+/**
+ * Highlight Information
+ */
+typedef struct {
+ hl_gi_t hl_gi;
+ btn_colit_t btn_colit;
+ btni_t btnit[36];
+} ATTRIBUTE_PACKED hli_t;
+
+/**
+ * PCI packet
+ */
+typedef struct {
+ pci_gi_t pci_gi;
+ nsml_agli_t nsml_agli;
+ hli_t hli;
+ uint8_t zero1[189];
+} ATTRIBUTE_PACKED pci_t;
+
+
+
+
+/**
+ * DSI General Information
+ */
+typedef struct {
+ uint32_t nv_pck_scr;
+ uint32_t nv_pck_lbn; /**< sector address of this nav pack */
+ uint32_t vobu_ea; /**< end address of this VOBU */
+ uint32_t vobu_1stref_ea; /**< end address of the 1st reference image */
+ uint32_t vobu_2ndref_ea; /**< end address of the 2nd reference image */
+ uint32_t vobu_3rdref_ea; /**< end address of the 3rd reference image */
+ uint16_t vobu_vob_idn; /**< VOB Id number that this VOBU is part of */
+ uint8_t zero1; /**< reserved */
+ uint8_t vobu_c_idn; /**< Cell Id number that this VOBU is part of */
+ dvd_time_t c_eltm; /**< Cell elapsed time */
+} ATTRIBUTE_PACKED dsi_gi_t;
+
+/**
+ * Seamless Playback Information
+ */
+typedef struct {
+ uint16_t category; /**< 'category' of seamless VOBU */
+ uint32_t ilvu_ea; /**< end address of interleaved Unit */
+ uint32_t ilvu_sa; /**< start address of next interleaved unit */
+ uint16_t size; /**< size of next interleaved unit */
+ uint32_t vob_v_s_s_ptm; /**< video start ptm in vob */
+ uint32_t vob_v_e_e_ptm; /**< video end ptm in vob */
+ struct {
+ uint32_t stp_ptm1;
+ uint32_t stp_ptm2;
+ uint32_t gap_len1;
+ uint32_t gap_len2;
+ } vob_a[8];
+} ATTRIBUTE_PACKED sml_pbi_t;
+
+/**
+ * Seamless Angle Information for one angle
+ */
+typedef struct {
+ uint32_t address; /**< offset to next ILVU, high bit is before/after */
+ uint16_t size; /**< byte size of the ILVU pointed to by address */
+} ATTRIBUTE_PACKED sml_agl_data_t;
+
+/**
+ * Seamless Angle Information
+ */
+typedef struct {
+ sml_agl_data_t data[9];
+} ATTRIBUTE_PACKED sml_agli_t;
+
+/**
+ * VOBU Search Information
+ */
+typedef struct {
+ uint32_t next_video; /**< Next vobu that contains video */
+ uint32_t fwda[19]; /**< Forwards, time */
+ uint32_t next_vobu;
+ uint32_t prev_vobu;
+ uint32_t bwda[19]; /**< Backwards, time */
+ uint32_t prev_video;
+} ATTRIBUTE_PACKED vobu_sri_t;
+
+#define SRI_END_OF_CELL 0x3fffffff
+
+/**
+ * Synchronous Information
+ */
+typedef struct {
+ uint16_t a_synca[8]; /**< offset to first audio packet for this VOBU */
+ uint32_t sp_synca[32]; /**< offset to first subpicture packet */
+} ATTRIBUTE_PACKED synci_t;
+
+/**
+ * DSI packet
+ */
+typedef struct {
+ dsi_gi_t dsi_gi;
+ sml_pbi_t sml_pbi;
+ sml_agli_t sml_agli;
+ vobu_sri_t vobu_sri;
+ synci_t synci;
+ uint8_t zero1[471];
+} ATTRIBUTE_PACKED dsi_t;
+
+
+#if PRAGMA_PACK
+#pragma pack()
+#endif
+
diff --git a/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/version.h b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/version.h
new file mode 100644
index 0000000..dced5e7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDInputStreams/dvdnav/version.h
@@ -0,0 +1,29 @@
+/*
+* This file is part of libdvdnav, a DVD navigation library.
+*
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+*/
+#pragma once
+
+#define DVDNAV_VERSION_CODE(major, minor, micro) (((major)*10000) + ((minor)*100) + ((micro)*1))
+
+#define DVDNAV_VERSION_MAJOR 6
+#define DVDNAV_VERSION_MINOR 1
+#define DVDNAV_VERSION_MICRO 1
+
+#define DVDNAV_VERSION_STRING "6.1.1"
+
+#define DVDNAV_VERSION \
+ DVDNAV_VERSION_CODE(DVDNAV_VERSION_MAJOR, DVDNAV_VERSION_MINOR, DVDNAV_VERSION_MICRO)
diff --git a/xbmc/cores/VideoPlayer/DVDMessage.cpp b/xbmc/cores/VideoPlayer/DVDMessage.cpp
new file mode 100644
index 0000000..34e8076
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDMessage.cpp
@@ -0,0 +1,110 @@
+/*
+ * 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 "DVDMessage.h"
+
+#include "DVDDemuxers/DVDDemuxUtils.h"
+#include "threads/Condition.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "utils/MathUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+class CDVDMsgGeneralSynchronizePriv
+{
+public:
+ CDVDMsgGeneralSynchronizePriv(std::chrono::milliseconds timeout, unsigned int sources)
+ : sources(sources), reached(0), m_timer(timeout)
+ {}
+ unsigned int sources;
+ unsigned int reached;
+ CCriticalSection section;
+ XbmcThreads::ConditionVariable condition;
+ XbmcThreads::EndTime<> m_timer;
+};
+
+/**
+ * CDVDMsgGeneralSynchronize --- GENERAL_SYNCRONIZR
+ */
+CDVDMsgGeneralSynchronize::CDVDMsgGeneralSynchronize(std::chrono::milliseconds timeout,
+ unsigned int sources)
+ : CDVDMsg(GENERAL_SYNCHRONIZE), m_p(new CDVDMsgGeneralSynchronizePriv(timeout, sources))
+{
+}
+
+CDVDMsgGeneralSynchronize::~CDVDMsgGeneralSynchronize()
+{
+ m_p->condition.notifyAll();
+
+ delete m_p;
+}
+
+bool CDVDMsgGeneralSynchronize::Wait(std::chrono::milliseconds timeout, unsigned int source)
+{
+ std::unique_lock<CCriticalSection> lock(m_p->section);
+
+ XbmcThreads::EndTime<> timer{timeout};
+
+ m_p->reached |= (source & m_p->sources);
+ if ((m_p->sources & SYNCSOURCE_ANY) && source)
+ m_p->reached |= SYNCSOURCE_ANY;
+
+ m_p->condition.notifyAll();
+
+ while (m_p->reached != m_p->sources)
+ {
+ timeout = std::min(m_p->m_timer.GetTimeLeft(), timer.GetTimeLeft());
+ if (m_p->condition.wait(lock, timeout))
+ continue;
+
+ if (m_p->m_timer.IsTimePast())
+ {
+ CLog::Log(LOGDEBUG, "CDVDMsgGeneralSynchronize - global timeout");
+ return true; // global timeout, we are done
+ }
+ if (timer.IsTimePast())
+ {
+ return false; /* request timeout, should be retried */
+ }
+ }
+ return true;
+}
+
+void CDVDMsgGeneralSynchronize::Wait(std::atomic<bool>& abort, unsigned int source)
+{
+ while (!Wait(100ms, source) && !abort)
+ ;
+}
+
+/**
+ * CDVDMsgDemuxerPacket --- DEMUXER_PACKET
+ */
+CDVDMsgDemuxerPacket::CDVDMsgDemuxerPacket(DemuxPacket* packet, bool drop) : CDVDMsg(DEMUXER_PACKET)
+{
+ m_packet = packet;
+ m_drop = drop;
+}
+
+CDVDMsgDemuxerPacket::~CDVDMsgDemuxerPacket()
+{
+ if (m_packet)
+ CDVDDemuxUtils::FreeDemuxPacket(m_packet);
+}
+
+unsigned int CDVDMsgDemuxerPacket::GetPacketSize()
+{
+ if (m_packet)
+ return m_packet->iSize;
+ else
+ return 0;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDMessage.h b/xbmc/cores/VideoPlayer/DVDMessage.h
new file mode 100644
index 0000000..5b1dcf7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDMessage.h
@@ -0,0 +1,333 @@
+/*
+ * 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 "FileItem.h"
+#include "cores/IPlayer.h"
+
+#include <atomic>
+#include <string.h>
+#include <string>
+
+struct DemuxPacket;
+
+class CDVDMsg
+{
+public:
+ // clang-format off
+ enum Message
+ {
+ NONE = 1000,
+
+ // messages used in the whole system
+ GENERAL_RESYNC, //
+ GENERAL_FLUSH, // flush all buffers
+ GENERAL_RESET, // reset codecs for new data
+ GENERAL_PAUSE,
+ GENERAL_STREAMCHANGE, //
+ GENERAL_SYNCHRONIZE, //
+ GENERAL_GUI_ACTION, // gui action of some sort
+ GENERAL_EOF, // eof of stream
+
+ // player core related messages (cVideoPlayer.cpp)
+ PLAYER_SET_AUDIOSTREAM, //
+ PLAYER_SET_VIDEOSTREAM, //
+ PLAYER_SET_SUBTITLESTREAM, //
+ PLAYER_SET_SUBTITLESTREAM_VISIBLE, //
+ PLAYER_SET_STATE, // restore the VideoPlayer to a certain state
+ PLAYER_SET_PROGRAM,
+ PLAYER_SET_UPDATE_STREAM_DETAILS, // player should update file item stream details with its current streams
+ PLAYER_SEEK, //
+ PLAYER_SEEK_CHAPTER, //
+ PLAYER_SETSPEED, // set the playback speed
+ PLAYER_REQUEST_STATE,
+ PLAYER_OPENFILE,
+ PLAYER_STARTED, // sent whenever a sub player has finished it's first frame after open
+ PLAYER_AVCHANGE, // signal a change in audio, video or subtitle parameters
+ PLAYER_ABORT,
+ PLAYER_REPORT_STATE,
+ PLAYER_FRAME_ADVANCE,
+ PLAYER_DISPLAY_RESET, // report display reset event
+
+ // demuxer related messages
+ DEMUXER_PACKET, // data packet
+ DEMUXER_RESET, // reset the demuxer
+
+ // video related messages
+ VIDEO_SET_ASPECT, // set aspectratio of video
+ VIDEO_DRAIN, // wait for decoder to output last frame
+
+ // subtitle related messages
+ SUBTITLE_CLUTCHANGE,
+ SUBTITLE_ADDFILE
+ };
+ // clang-format on
+
+ explicit CDVDMsg(Message msg)
+ {
+ m_message = msg;
+ }
+
+ virtual ~CDVDMsg() = default;
+
+ /**
+ * checks for message type
+ */
+ inline bool IsType(Message msg)
+ {
+ return (m_message == msg);
+ }
+
+ inline Message GetMessageType()
+ {
+ return m_message;
+ }
+
+private:
+ Message m_message;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//////
+////// GENERAL_ Messages
+//////
+////////////////////////////////////////////////////////////////////////////////
+
+#define SYNCSOURCE_AUDIO 0x01
+#define SYNCSOURCE_VIDEO 0x02
+#define SYNCSOURCE_PLAYER 0x04
+#define SYNCSOURCE_ANY 0x08
+
+class CDVDMsgGeneralSynchronizePriv;
+class CDVDMsgGeneralSynchronize : public CDVDMsg
+{
+public:
+ CDVDMsgGeneralSynchronize(std::chrono::milliseconds timeout, unsigned int sources);
+ ~CDVDMsgGeneralSynchronize() override;
+
+ // waits until all threads waiting, released the object
+ // if abort is set somehow
+ bool Wait(std::chrono::milliseconds ms, unsigned int source);
+ void Wait(std::atomic<bool>& abort, unsigned int source);
+
+private:
+ class CDVDMsgGeneralSynchronizePriv* m_p;
+};
+
+template <typename T>
+class CDVDMsgType : public CDVDMsg
+{
+public:
+ CDVDMsgType(Message type, const T &value)
+ : CDVDMsg(type)
+ , m_value(value)
+ {}
+
+ ~CDVDMsgType() override = default;
+
+ operator T() { return m_value; }
+ T m_value;
+};
+
+typedef CDVDMsgType<bool> CDVDMsgBool;
+typedef CDVDMsgType<int> CDVDMsgInt;
+typedef CDVDMsgType<double> CDVDMsgDouble;
+
+////////////////////////////////////////////////////////////////////////////////
+//////
+////// PLAYER_ Messages
+//////
+////////////////////////////////////////////////////////////////////////////////
+
+class CDVDMsgPlayerSetAudioStream : public CDVDMsg
+{
+public:
+ explicit CDVDMsgPlayerSetAudioStream(int streamId) : CDVDMsg(PLAYER_SET_AUDIOSTREAM) { m_streamId = streamId; }
+ ~CDVDMsgPlayerSetAudioStream() override = default;
+
+ int GetStreamId() { return m_streamId; }
+private:
+ int m_streamId;
+};
+
+class CDVDMsgPlayerSetVideoStream : public CDVDMsg
+{
+public:
+ explicit CDVDMsgPlayerSetVideoStream(int streamId) : CDVDMsg(PLAYER_SET_VIDEOSTREAM) { m_streamId = streamId; }
+ ~CDVDMsgPlayerSetVideoStream() override = default;
+
+ int GetStreamId() const { return m_streamId; }
+private:
+ int m_streamId;
+};
+
+class CDVDMsgPlayerSetSubtitleStream : public CDVDMsg
+{
+public:
+ explicit CDVDMsgPlayerSetSubtitleStream(int streamId) : CDVDMsg(PLAYER_SET_SUBTITLESTREAM) { m_streamId = streamId; }
+ ~CDVDMsgPlayerSetSubtitleStream() override = default;
+
+ int GetStreamId() { return m_streamId; }
+private:
+ int m_streamId;
+};
+
+class CDVDMsgPlayerSetState : public CDVDMsg
+{
+public:
+ explicit CDVDMsgPlayerSetState(const std::string& state) : CDVDMsg(PLAYER_SET_STATE), m_state(state) {}
+ ~CDVDMsgPlayerSetState() override = default;
+
+ std::string GetState() { return m_state; }
+private:
+ std::string m_state;
+};
+
+class CDVDMsgPlayerSeek : public CDVDMsg
+{
+public:
+ struct CMode
+ {
+ double time = 0;
+ bool relative = false;
+ bool backward = false;
+ bool accurate = true;
+ bool sync = true;
+ bool restore = true;
+ bool trickplay = false;
+ };
+
+ explicit CDVDMsgPlayerSeek(CDVDMsgPlayerSeek::CMode mode) : CDVDMsg(PLAYER_SEEK),
+ m_mode(mode)
+ {}
+ ~CDVDMsgPlayerSeek() override = default;
+
+ double GetTime() { return m_mode.time; }
+ bool GetRelative() { return m_mode.relative; }
+ bool GetBackward() { return m_mode.backward; }
+ bool GetAccurate() { return m_mode.accurate; }
+ bool GetRestore() { return m_mode.restore; }
+ bool GetTrickPlay() { return m_mode.trickplay; }
+ bool GetSync() { return m_mode.sync; }
+
+private:
+ CMode m_mode;
+};
+
+class CDVDMsgPlayerSeekChapter : public CDVDMsg
+{
+ public:
+ explicit CDVDMsgPlayerSeekChapter(int iChapter)
+ : CDVDMsg(PLAYER_SEEK_CHAPTER)
+ , m_iChapter(iChapter)
+ {}
+ ~CDVDMsgPlayerSeekChapter() override = default;
+
+ int GetChapter() const { return m_iChapter; }
+
+ private:
+
+ int m_iChapter;
+};
+
+class CDVDMsgPlayerSetSpeed : public CDVDMsg
+{
+public:
+ struct SpeedParams
+ {
+ int m_speed;
+ bool m_isTempo;
+ };
+
+ explicit CDVDMsgPlayerSetSpeed(SpeedParams params)
+ : CDVDMsg(PLAYER_SETSPEED)
+ , m_params(params)
+ {}
+ ~CDVDMsgPlayerSetSpeed() override = default;
+
+ int GetSpeed() const { return m_params.m_speed; }
+ bool IsTempo() const { return m_params.m_isTempo; }
+
+private:
+
+ SpeedParams m_params;
+
+};
+
+class CDVDMsgOpenFile : public CDVDMsg
+{
+public:
+ struct FileParams
+ {
+ CFileItem m_item;
+ CPlayerOptions m_options;
+ };
+
+ explicit CDVDMsgOpenFile(const FileParams &params)
+ : CDVDMsg(PLAYER_OPENFILE)
+ , m_params(params)
+ {}
+ ~CDVDMsgOpenFile() override = default;
+
+ CFileItem& GetItem() { return m_params.m_item; }
+ CPlayerOptions& GetOptions() { return m_params.m_options; }
+
+private:
+
+ FileParams m_params;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//////
+////// DEMUXER_ Messages
+//////
+////////////////////////////////////////////////////////////////////////////////
+
+class CDVDMsgDemuxerPacket : public CDVDMsg
+{
+public:
+ CDVDMsgDemuxerPacket(DemuxPacket* packet, bool drop = false);
+ ~CDVDMsgDemuxerPacket() override;
+ DemuxPacket* GetPacket() { return m_packet; }
+ unsigned int GetPacketSize();
+ bool GetPacketDrop() { return m_drop; }
+ DemuxPacket* m_packet;
+ bool m_drop;
+};
+
+class CDVDMsgDemuxerReset : public CDVDMsg
+{
+public:
+ CDVDMsgDemuxerReset() : CDVDMsg(DEMUXER_RESET) {}
+ ~CDVDMsgDemuxerReset() override = default;
+};
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+//////
+////// VIDEO_ Messages
+//////
+////////////////////////////////////////////////////////////////////////////////
+
+
+////////////////////////////////////////////////////////////////////////////////
+//////
+////// SUBTITLE_ Messages
+//////
+////////////////////////////////////////////////////////////////////////////////
+
+class CDVDMsgSubtitleClutChange : public CDVDMsg
+{
+public:
+ explicit CDVDMsgSubtitleClutChange(uint8_t* data) : CDVDMsg(SUBTITLE_CLUTCHANGE) { memcpy(m_data, data, 16*4); }
+ ~CDVDMsgSubtitleClutChange() override = default;
+
+ uint8_t m_data[16][4];
+};
diff --git a/xbmc/cores/VideoPlayer/DVDMessageQueue.cpp b/xbmc/cores/VideoPlayer/DVDMessageQueue.cpp
new file mode 100644
index 0000000..12ea7a9
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDMessageQueue.cpp
@@ -0,0 +1,351 @@
+/*
+ * 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 "DVDMessageQueue.h"
+
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/log.h"
+
+#include <math.h>
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+CDVDMessageQueue::CDVDMessageQueue(const std::string &owner) : m_hEvent(true), m_owner(owner)
+{
+ m_iDataSize = 0;
+ m_bInitialized = false;
+
+ m_TimeBack = DVD_NOPTS_VALUE;
+ m_TimeFront = DVD_NOPTS_VALUE;
+ m_TimeSize = 1.0 / 4.0; /* 4 seconds */
+ m_iMaxDataSize = 0;
+}
+
+CDVDMessageQueue::~CDVDMessageQueue()
+{
+ // remove all remaining messages
+ Flush(CDVDMsg::NONE);
+}
+
+void CDVDMessageQueue::Init()
+{
+ m_iDataSize = 0;
+ m_bAbortRequest = false;
+ m_bInitialized = true;
+ m_TimeBack = DVD_NOPTS_VALUE;
+ m_TimeFront = DVD_NOPTS_VALUE;
+ m_drain = false;
+}
+
+void CDVDMessageQueue::Flush(CDVDMsg::Message type)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ m_messages.remove_if([type](const DVDMessageListItem &item){
+ return type == CDVDMsg::NONE || item.message->IsType(type);
+ });
+
+ m_prioMessages.remove_if([type](const DVDMessageListItem &item){
+ return type == CDVDMsg::NONE || item.message->IsType(type);
+ });
+
+ if (type == CDVDMsg::DEMUXER_PACKET || type == CDVDMsg::NONE)
+ {
+ m_iDataSize = 0;
+ m_TimeBack = DVD_NOPTS_VALUE;
+ m_TimeFront = DVD_NOPTS_VALUE;
+ }
+}
+
+void CDVDMessageQueue::Abort()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ m_bAbortRequest = true;
+
+ // inform waiter for abort action
+ m_hEvent.Set();
+}
+
+void CDVDMessageQueue::End()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ Flush(CDVDMsg::NONE);
+
+ m_bInitialized = false;
+ m_iDataSize = 0;
+ m_bAbortRequest = false;
+}
+
+MsgQueueReturnCode CDVDMessageQueue::Put(const std::shared_ptr<CDVDMsg>& pMsg, int priority)
+{
+ return Put(pMsg, priority, true);
+}
+
+MsgQueueReturnCode CDVDMessageQueue::PutBack(const std::shared_ptr<CDVDMsg>& pMsg, int priority)
+{
+ return Put(pMsg, priority, false);
+}
+
+MsgQueueReturnCode CDVDMessageQueue::Put(const std::shared_ptr<CDVDMsg>& pMsg,
+ int priority,
+ bool front)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (!m_bInitialized)
+ {
+ CLog::Log(LOGWARNING, "CDVDMessageQueue({})::Put MSGQ_NOT_INITIALIZED", m_owner);
+ return MSGQ_NOT_INITIALIZED;
+ }
+ if (!pMsg)
+ {
+ CLog::Log(LOGFATAL, "CDVDMessageQueue({})::Put MSGQ_INVALID_MSG", m_owner);
+ return MSGQ_INVALID_MSG;
+ }
+
+ if (priority > 0)
+ {
+ int prio = priority;
+ if (!front)
+ prio++;
+
+ auto it = std::find_if(m_prioMessages.begin(), m_prioMessages.end(),
+ [prio](const DVDMessageListItem &item){
+ return prio <= item.priority;
+ });
+ m_prioMessages.emplace(it, pMsg, priority);
+ }
+ else
+ {
+ if (m_messages.empty())
+ {
+ m_iDataSize = 0;
+ m_TimeBack = DVD_NOPTS_VALUE;
+ m_TimeFront = DVD_NOPTS_VALUE;
+ }
+
+ if (front)
+ m_messages.emplace_front(pMsg, priority);
+ else
+ m_messages.emplace_back(pMsg, priority);
+ }
+
+ if (pMsg->IsType(CDVDMsg::DEMUXER_PACKET) && priority == 0)
+ {
+ DemuxPacket* packet = static_cast<CDVDMsgDemuxerPacket*>(pMsg.get())->GetPacket();
+ if (packet)
+ {
+ m_iDataSize += packet->iSize;
+ if (front)
+ UpdateTimeFront();
+ else
+ UpdateTimeBack();
+ }
+ }
+
+ // inform waiter for new packet
+ m_hEvent.Set();
+
+ return MSGQ_OK;
+}
+
+MsgQueueReturnCode CDVDMessageQueue::Get(std::shared_ptr<CDVDMsg>& pMsg,
+ unsigned int iTimeoutInMilliSeconds,
+ int& priority)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ int ret = 0;
+
+ if (!m_bInitialized)
+ {
+ CLog::Log(LOGFATAL, "CDVDMessageQueue({})::Get MSGQ_NOT_INITIALIZED", m_owner);
+ return MSGQ_NOT_INITIALIZED;
+ }
+
+ while (!m_bAbortRequest)
+ {
+ std::list<DVDMessageListItem> &msgs = (priority > 0 || !m_prioMessages.empty()) ? m_prioMessages : m_messages;
+
+ if (!msgs.empty() && (msgs.back().priority >= priority || m_drain))
+ {
+ DVDMessageListItem& item(msgs.back());
+ priority = item.priority;
+
+ if (item.message->IsType(CDVDMsg::DEMUXER_PACKET) && item.priority == 0)
+ {
+ DemuxPacket* packet =
+ std::static_pointer_cast<CDVDMsgDemuxerPacket>(item.message)->GetPacket();
+ if (packet)
+ {
+ m_iDataSize -= packet->iSize;
+ }
+ }
+
+ pMsg = std::move(item.message);
+ msgs.pop_back();
+ UpdateTimeBack();
+ ret = MSGQ_OK;
+ break;
+ }
+ else if (!iTimeoutInMilliSeconds)
+ {
+ ret = MSGQ_TIMEOUT;
+ break;
+ }
+ else
+ {
+ m_hEvent.Reset();
+ lock.unlock();
+
+ // wait for a new message
+ if (!m_hEvent.Wait(std::chrono::milliseconds(iTimeoutInMilliSeconds)))
+ return MSGQ_TIMEOUT;
+
+ lock.lock();
+ }
+ }
+
+ if (m_bAbortRequest)
+ return MSGQ_ABORT;
+
+ return (MsgQueueReturnCode)ret;
+}
+
+void CDVDMessageQueue::UpdateTimeFront()
+{
+ if (!m_messages.empty())
+ {
+ auto &item = m_messages.front();
+ if (item.message->IsType(CDVDMsg::DEMUXER_PACKET))
+ {
+ DemuxPacket* packet =
+ std::static_pointer_cast<CDVDMsgDemuxerPacket>(item.message)->GetPacket();
+ if (packet)
+ {
+ if (packet->dts != DVD_NOPTS_VALUE)
+ m_TimeFront = packet->dts;
+ else if (packet->pts != DVD_NOPTS_VALUE)
+ m_TimeFront = packet->pts;
+
+ if (m_TimeBack == DVD_NOPTS_VALUE)
+ m_TimeBack = m_TimeFront;
+ }
+ }
+ }
+}
+
+void CDVDMessageQueue::UpdateTimeBack()
+{
+ if (!m_messages.empty())
+ {
+ auto &item = m_messages.back();
+ if (item.message->IsType(CDVDMsg::DEMUXER_PACKET))
+ {
+ DemuxPacket* packet =
+ std::static_pointer_cast<CDVDMsgDemuxerPacket>(item.message)->GetPacket();
+ if (packet)
+ {
+ if (packet->dts != DVD_NOPTS_VALUE)
+ m_TimeBack = packet->dts;
+ else if (packet->pts != DVD_NOPTS_VALUE)
+ m_TimeBack = packet->pts;
+
+ if (m_TimeFront == DVD_NOPTS_VALUE)
+ m_TimeFront = m_TimeBack;
+ }
+ }
+ }
+}
+
+unsigned CDVDMessageQueue::GetPacketCount(CDVDMsg::Message type)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (!m_bInitialized)
+ return 0;
+
+ unsigned count = 0;
+ for (const auto &item : m_messages)
+ {
+ if(item.message->IsType(type))
+ count++;
+ }
+ for (const auto &item : m_prioMessages)
+ {
+ if(item.message->IsType(type))
+ count++;
+ }
+
+ return count;
+}
+
+void CDVDMessageQueue::WaitUntilEmpty()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_drain = true;
+ }
+
+ CLog::Log(LOGINFO, "CDVDMessageQueue({})::WaitUntilEmpty", m_owner);
+ auto msg = std::make_shared<CDVDMsgGeneralSynchronize>(40s, SYNCSOURCE_ANY);
+ Put(msg);
+ msg->Wait(m_bAbortRequest, 0);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_drain = false;
+ }
+}
+
+int CDVDMessageQueue::GetLevel() const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (m_iDataSize > m_iMaxDataSize)
+ return 100;
+ if (m_iDataSize == 0)
+ return 0;
+
+ if (IsDataBased())
+ {
+ return std::min(100, 100 * m_iDataSize / m_iMaxDataSize);
+ }
+
+ int level = std::min(100.0, ceil(100.0 * m_TimeSize * (m_TimeFront - m_TimeBack) / DVD_TIME_BASE ));
+
+ // if we added lots of packets with NOPTS, make sure that the queue is not signalled empty
+ if (level == 0 && m_iDataSize != 0)
+ {
+ CLog::Log(LOGDEBUG, "CDVDMessageQueue::GetLevel() - can't determine level");
+ return 1;
+ }
+
+ return level;
+}
+
+int CDVDMessageQueue::GetTimeSize() const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (IsDataBased())
+ return 0;
+ else
+ return (int)((m_TimeFront - m_TimeBack) / DVD_TIME_BASE);
+}
+
+bool CDVDMessageQueue::IsDataBased() const
+{
+ return (m_TimeBack == DVD_NOPTS_VALUE ||
+ m_TimeFront == DVD_NOPTS_VALUE ||
+ m_TimeFront <= m_TimeBack);
+}
diff --git a/xbmc/cores/VideoPlayer/DVDMessageQueue.h b/xbmc/cores/VideoPlayer/DVDMessageQueue.h
new file mode 100644
index 0000000..81ffd0e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDMessageQueue.h
@@ -0,0 +1,116 @@
+/*
+ * 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 "DVDMessage.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+
+#include <algorithm>
+#include <atomic>
+#include <list>
+#include <string>
+
+struct DVDMessageListItem
+{
+ DVDMessageListItem(std::shared_ptr<CDVDMsg> msg, int prio) : message(std::move(msg))
+ {
+ priority = prio;
+ }
+ DVDMessageListItem() { priority = 0; }
+ DVDMessageListItem(const DVDMessageListItem&) = delete;
+ ~DVDMessageListItem() = default;
+
+ DVDMessageListItem& operator=(const DVDMessageListItem&) = delete;
+
+ std::shared_ptr<CDVDMsg> message;
+ int priority;
+};
+
+enum MsgQueueReturnCode
+{
+ MSGQ_OK = 1,
+ MSGQ_TIMEOUT = 0,
+ MSGQ_ABORT = -1, // negative for legacy, not an error actually
+ MSGQ_NOT_INITIALIZED = -2,
+ MSGQ_INVALID_MSG = -3,
+ MSGQ_OUT_OF_MEMORY = -4
+};
+
+#define MSGQ_IS_ERROR(c) (c < 0)
+
+class CDVDMessageQueue
+{
+public:
+ explicit CDVDMessageQueue(const std::string &owner);
+ virtual ~CDVDMessageQueue();
+
+ void Init();
+ void Flush(CDVDMsg::Message message = CDVDMsg::DEMUXER_PACKET);
+ void Abort();
+ void End();
+
+ MsgQueueReturnCode Put(const std::shared_ptr<CDVDMsg>& pMsg, int priority = 0);
+ MsgQueueReturnCode PutBack(const std::shared_ptr<CDVDMsg>& pMsg, int priority = 0);
+
+ /**
+ * msg, message type from DVDMessage.h
+ * timeout, timeout in msec
+ * priority, minimum priority to get, outputs returned packets priority
+ */
+ MsgQueueReturnCode Get(std::shared_ptr<CDVDMsg>& pMsg,
+ unsigned int iTimeoutInMilliSeconds,
+ int& priority);
+ MsgQueueReturnCode Get(std::shared_ptr<CDVDMsg>& pMsg, unsigned int iTimeoutInMilliSeconds)
+ {
+ int priority = 0;
+ return Get(pMsg, iTimeoutInMilliSeconds, priority);
+ }
+
+ int GetDataSize() const { return m_iDataSize; }
+ int GetTimeSize() const;
+ unsigned GetPacketCount(CDVDMsg::Message type);
+ bool ReceivedAbortRequest() { return m_bAbortRequest; }
+ void WaitUntilEmpty();
+
+ // non messagequeue related functions
+ bool IsFull() const { return GetLevel() == 100; }
+ int GetLevel() const;
+
+ void SetMaxDataSize(int iMaxDataSize) { m_iMaxDataSize = iMaxDataSize; }
+ void SetMaxTimeSize(double sec) { m_TimeSize = 1.0 / std::max(1.0, sec); }
+ int GetMaxDataSize() const { return m_iMaxDataSize; }
+ double GetMaxTimeSize() const { return m_TimeSize; }
+ bool IsInited() const { return m_bInitialized; }
+ bool IsDataBased() const;
+
+private:
+ MsgQueueReturnCode Put(const std::shared_ptr<CDVDMsg>& pMsg, int priority, bool front);
+ void UpdateTimeFront();
+ void UpdateTimeBack();
+
+ CEvent m_hEvent;
+ mutable CCriticalSection m_section;
+
+ std::atomic<bool> m_bAbortRequest = false;
+ bool m_bInitialized;
+ bool m_drain = false;
+
+ int m_iDataSize;
+ double m_TimeFront;
+ double m_TimeBack;
+ double m_TimeSize;
+
+ int m_iMaxDataSize;
+ std::string m_owner;
+
+ std::list<DVDMessageListItem> m_messages;
+ std::list<DVDMessageListItem> m_prioMessages;
+};
+
diff --git a/xbmc/cores/VideoPlayer/DVDOverlayContainer.cpp b/xbmc/cores/VideoPlayer/DVDOverlayContainer.cpp
new file mode 100644
index 0000000..ec66b2d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDOverlayContainer.cpp
@@ -0,0 +1,195 @@
+/*
+ * 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 "DVDOverlayContainer.h"
+
+#include "DVDInputStreams/DVDInputStreamNavigator.h"
+
+#include <mutex>
+
+CDVDOverlayContainer::CDVDOverlayContainer() = default;
+
+CDVDOverlayContainer::~CDVDOverlayContainer()
+{
+ Clear();
+}
+
+void CDVDOverlayContainer::ProcessAndAddOverlayIfValid(CDVDOverlay* pOverlay)
+{
+ pOverlay->Acquire();
+
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ // markup any non ending overlays, to finish
+ // when this new one starts, there can be
+ // multiple overlays queued at same start
+ // point so only stop them when we get a
+ // new startpoint
+ for(int i = m_overlays.size();i>0;)
+ {
+ i--;
+ if(m_overlays[i]->iPTSStopTime)
+ {
+ if(!m_overlays[i]->replace)
+ break;
+ if(m_overlays[i]->iPTSStopTime <= pOverlay->iPTSStartTime)
+ break;
+ }
+
+ if (m_overlays[i]->iPTSStartTime != pOverlay->iPTSStartTime)
+ m_overlays[i]->iPTSStopTime = pOverlay->iPTSStartTime;
+ }
+
+ m_overlays.emplace_back(pOverlay);
+}
+
+VecOverlays* CDVDOverlayContainer::GetOverlays()
+{
+ return &m_overlays;
+}
+
+VecOverlaysIter CDVDOverlayContainer::Remove(VecOverlaysIter itOverlay)
+{
+ VecOverlaysIter itNext;
+ CDVDOverlay* pOverlay = *itOverlay;
+
+ {
+ std::unique_lock<CCriticalSection> lock(*this);
+ itNext = m_overlays.erase(itOverlay);
+ }
+
+ pOverlay->Release();
+
+ return itNext;
+}
+
+void CDVDOverlayContainer::CleanUp(double pts)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ VecOverlaysIter it = m_overlays.begin();
+ while (it != m_overlays.end())
+ {
+ CDVDOverlay* pOverlay = *it;
+
+ // never delete forced overlays, they are used in menu's
+ // clear takes care of removing them
+ // also if stoptime = 0, it means the next subtitles will use its starttime as the stoptime
+ // which means we cannot delete overlays with stoptime 0
+ if (!pOverlay->bForced && pOverlay->iPTSStopTime <= pts && pOverlay->iPTSStopTime != 0)
+ {
+ //CLog::Log(LOGDEBUG,"CDVDOverlay::CleanUp, removing {}", (int)(pts / 1000));
+ //CLog::Log(LOGDEBUG,"CDVDOverlay::CleanUp, remove, start : {}, stop : {}", (int)(pOverlay->iPTSStartTime / 1000), (int)(pOverlay->iPTSStopTime / 1000));
+ it = Remove(it);
+ continue;
+ }
+ else if (pOverlay->bForced)
+ {
+ //Check for newer replacements
+ VecOverlaysIter it2 = it;
+ bool bNewer = false;
+ while (!bNewer && ++it2 != m_overlays.end())
+ {
+ CDVDOverlay* pOverlay2 = *it2;
+ if (pOverlay2->bForced && pOverlay2->iPTSStartTime <= pts) bNewer = true;
+ }
+
+ if (bNewer)
+ {
+ it = Remove(it);
+ continue;
+ }
+ }
+ ++it;
+ }
+
+}
+
+void CDVDOverlayContainer::Flush()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ // Flush only the overlays marked as flushable
+ m_overlays.erase(std::remove_if(m_overlays.begin(), m_overlays.end(),
+ [](CDVDOverlay* ov) {
+ bool isFlushable = ov->IsOverlayContainerFlushable();
+ if (isFlushable)
+ ov->Release();
+ return isFlushable;
+ }),
+ m_overlays.end());
+}
+
+void CDVDOverlayContainer::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ for (auto &overlay : m_overlays)
+ {
+ overlay->Release();
+ }
+ m_overlays.clear();
+}
+
+size_t CDVDOverlayContainer::GetSize()
+{
+ return m_overlays.size();
+}
+
+bool CDVDOverlayContainer::ContainsOverlayType(DVDOverlayType type)
+{
+ bool result = false;
+
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ VecOverlaysIter it = m_overlays.begin();
+ while (!result && it != m_overlays.end())
+ {
+ if ((*it)->IsOverlayType(type)) result = true;
+ ++it;
+ }
+
+ return result;
+}
+
+/*
+ * iAction should be LIBDVDNAV_BUTTON_NORMAL or LIBDVDNAV_BUTTON_CLICKED
+ */
+void CDVDOverlayContainer::UpdateOverlayInfo(
+ const std::shared_ptr<CDVDInputStreamNavigator>& pStream, CDVDDemuxSPU* pSpu, int iAction)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ pStream->CheckButtons();
+
+ //Update any forced overlays.
+ for(VecOverlays::iterator it = m_overlays.begin(); it != m_overlays.end(); ++it )
+ {
+ if ((*it)->IsOverlayType(DVDOVERLAY_TYPE_SPU))
+ {
+ CDVDOverlaySpu* pOverlaySpu = (CDVDOverlaySpu*)(*it);
+
+ // make sure its a forced (menu) overlay
+ // set menu spu color and alpha data if there is a valid menu overlay
+ if (pOverlaySpu->bForced)
+ {
+ if (pOverlaySpu->Acquire()->Release() > 1)
+ {
+ pOverlaySpu = new CDVDOverlaySpu(*pOverlaySpu);
+ (*it)->Release();
+ (*it) = pOverlaySpu;
+ }
+
+ if(pStream->GetCurrentButtonInfo(pOverlaySpu, pSpu, iAction))
+ {
+ pOverlaySpu->m_textureid = 0;
+ }
+
+ }
+ }
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/DVDOverlayContainer.h b/xbmc/cores/VideoPlayer/DVDOverlayContainer.h
new file mode 100644
index 0000000..98f3373
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDOverlayContainer.h
@@ -0,0 +1,60 @@
+/*
+ * 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 "DVDCodecs/Overlay/DVDOverlay.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+
+class CDVDInputStreamNavigator;
+class CDVDDemuxSPU;
+
+class CDVDOverlayContainer : public CCriticalSection
+{
+public:
+ CDVDOverlayContainer();
+ virtual ~CDVDOverlayContainer();
+
+ /*!
+ * \brief Adds an overlay into the container by processing the existing overlay collection first
+ *
+ * \details Processes the overlay collection whenever a new overlay is added. Useful to change
+ * the overlay's PTS values of previously added overlays if the collection itself is sequential. This
+ * is, for example, the case of ASS subtitles in which a single call to ass_render_frame generates all
+ * the subtitle images on a single call even if two subtitles exist at the same time frame. Other cases
+ * might exist where an overlay shouldn't be added to the collection if completely contained in another
+ * overlay.
+ *
+ * \param pPicture pointer to the overlay to be evaluated and possibly added to the collection
+ */
+ void ProcessAndAddOverlayIfValid(CDVDOverlay* pPicture);
+
+ VecOverlays* GetOverlays(); // get the first overlay in this fifo
+ bool ContainsOverlayType(DVDOverlayType type);
+
+ void Clear(); // clear the fifo and delete all overlays
+
+ /*
+ * \brief Flush the overlays.
+ */
+ void Flush();
+
+ void CleanUp(double pts); // validates all overlays against current pts
+ size_t GetSize();
+
+ void UpdateOverlayInfo(const std::shared_ptr<CDVDInputStreamNavigator>& pStream,
+ CDVDDemuxSPU* pSpu,
+ int iAction);
+
+private:
+ VecOverlaysIter Remove(VecOverlaysIter itOverlay); // removes a specific overlay
+
+ VecOverlays m_overlays;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDResource.h b/xbmc/cores/VideoPlayer/DVDResource.h
new file mode 100644
index 0000000..56a1e75
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDResource.h
@@ -0,0 +1,37 @@
+/*
+ * 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 <assert.h>
+#include <atomic>
+
+template<typename T> struct IDVDResourceCounted
+{
+ IDVDResourceCounted() : m_refs(1) {}
+ virtual ~IDVDResourceCounted() = default;
+
+ IDVDResourceCounted(const IDVDResourceCounted &) = delete;
+ IDVDResourceCounted &operator=(const IDVDResourceCounted &) = delete;
+
+ virtual T* Acquire()
+ {
+ ++m_refs;
+ return static_cast<T*>(this);
+ }
+
+ virtual long Release()
+ {
+ long count = --m_refs;
+ assert(count >= 0);
+ if (count == 0)
+ delete static_cast<T*>(this);
+ return count;
+ }
+ std::atomic<long> m_refs;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDStreamInfo.cpp b/xbmc/cores/VideoPlayer/DVDStreamInfo.cpp
new file mode 100644
index 0000000..f8f7c71
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDStreamInfo.cpp
@@ -0,0 +1,310 @@
+/*
+ * 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 "DVDStreamInfo.h"
+
+#include "DVDDemuxers/DVDDemux.h"
+#include "cores/VideoPlayer/Interface/DemuxCrypto.h"
+
+CDVDStreamInfo::CDVDStreamInfo() { extradata = NULL; Clear(); }
+CDVDStreamInfo::CDVDStreamInfo(const CDVDStreamInfo &right, bool withextradata ) { extradata = NULL; Clear(); Assign(right, withextradata); }
+CDVDStreamInfo::CDVDStreamInfo(const CDemuxStream &right, bool withextradata ) { extradata = NULL; Clear(); Assign(right, withextradata); }
+
+CDVDStreamInfo::~CDVDStreamInfo()
+{
+ if( extradata && extrasize ) free(extradata);
+
+ extradata = NULL;
+ extrasize = 0;
+}
+
+
+void CDVDStreamInfo::Clear()
+{
+ codec = AV_CODEC_ID_NONE;
+ type = STREAM_NONE;
+ uniqueId = -1;
+ source = STREAM_SOURCE_NONE;
+ codecOptions = 0;
+ codec_tag = 0;
+ flags = 0;
+ filename.clear();
+ dvd = false;
+
+ if( extradata && extrasize ) free(extradata);
+
+ extradata = NULL;
+ extrasize = 0;
+
+ cryptoSession = nullptr;
+ externalInterfaces = nullptr;
+
+ fpsscale = 0;
+ fpsrate = 0;
+ height = 0;
+ width = 0;
+ aspect = 0.0;
+ vfr = false;
+ stills = false;
+ level = 0;
+ profile = 0;
+ ptsinvalid = false;
+ forced_aspect = false;
+ bitsperpixel = 0;
+ colorSpace = AVCOL_SPC_UNSPECIFIED;
+ colorRange = AVCOL_RANGE_UNSPECIFIED;
+ colorPrimaries = AVCOL_PRI_UNSPECIFIED;
+ colorTransferCharacteristic = AVCOL_TRC_UNSPECIFIED;
+ masteringMetadata = nullptr;
+ contentLightMetadata = nullptr;
+ stereo_mode.clear();
+
+ channels = 0;
+ samplerate = 0;
+ blockalign = 0;
+ bitrate = 0;
+ bitspersample = 0;
+ channellayout = 0;
+
+ orientation = 0;
+ bitdepth = 0;
+}
+
+bool CDVDStreamInfo::Equal(const CDVDStreamInfo& right, int compare)
+{
+ if (codec != right.codec || type != right.type ||
+ ((compare & COMPARE_ID) && uniqueId != right.uniqueId) ||
+ ((compare & COMPARE_ID) && demuxerId != right.demuxerId) || codec_tag != right.codec_tag ||
+ flags != right.flags)
+ return false;
+
+ if (compare & COMPARE_EXTRADATA)
+ {
+ if( extrasize != right.extrasize ) return false;
+ if( extrasize )
+ {
+ if( memcmp(extradata, right.extradata, extrasize) != 0 ) return false;
+ }
+ }
+
+ // VIDEO
+ // clang-format off
+ if (fpsscale != right.fpsscale
+ || fpsrate != right.fpsrate
+ || height != right.height
+ || width != right.width
+ || stills != right.stills
+ || level != right.level
+ || profile != right.profile
+ || ptsinvalid != right.ptsinvalid
+ || forced_aspect != right.forced_aspect
+ || bitsperpixel != right.bitsperpixel
+ || bitdepth != right.bitdepth
+ || vfr != right.vfr
+ || colorSpace != right.colorSpace
+ || colorRange != right.colorRange
+ || colorPrimaries != right.colorPrimaries
+ || colorTransferCharacteristic != right.colorTransferCharacteristic
+ || stereo_mode != right.stereo_mode)
+ return false;
+ // clang-format on
+
+ if (masteringMetadata && right.masteringMetadata)
+ {
+ if (masteringMetadata->has_luminance != right.masteringMetadata->has_luminance
+ || masteringMetadata->has_primaries != right.masteringMetadata->has_primaries)
+ return false;
+
+ if (masteringMetadata->has_primaries)
+ {
+ for (unsigned int i(0); i < 3; ++i)
+ for (unsigned int j(0); j < 2; ++j)
+ if (av_cmp_q(masteringMetadata->display_primaries[i][j], right.masteringMetadata->display_primaries[i][j]))
+ return false;
+ for (unsigned int i(0); i < 2; ++i)
+ if (av_cmp_q(masteringMetadata->white_point[i], right.masteringMetadata->white_point[i]))
+ return false;
+ }
+
+ if (masteringMetadata->has_luminance)
+ {
+ if (av_cmp_q(masteringMetadata->min_luminance, right.masteringMetadata->min_luminance)
+ || av_cmp_q(masteringMetadata->max_luminance, right.masteringMetadata->max_luminance))
+ return false;
+ }
+ }
+ else if (masteringMetadata || right.masteringMetadata)
+ return false;
+
+ if (contentLightMetadata && right.contentLightMetadata)
+ {
+ if (contentLightMetadata->MaxCLL != right.contentLightMetadata->MaxCLL
+ || contentLightMetadata->MaxFALL != right.contentLightMetadata->MaxFALL)
+ return false;
+ }
+ else if (contentLightMetadata || right.contentLightMetadata)
+ return false;
+
+ // AUDIO
+ if( channels != right.channels
+ || samplerate != right.samplerate
+ || blockalign != right.blockalign
+ || bitrate != right.bitrate
+ || bitspersample != right.bitspersample
+ || channellayout != right.channellayout)
+ return false;
+
+ // SUBTITLE
+
+ // Crypto
+ if ((cryptoSession == nullptr) != (right.cryptoSession == nullptr))
+ return false;
+
+ if (cryptoSession && !(*cryptoSession == *right.cryptoSession))
+ return false;
+
+ return true;
+}
+
+bool CDVDStreamInfo::Equal(const CDemuxStream& right, bool withextradata)
+{
+ CDVDStreamInfo info;
+ info.Assign(right, withextradata);
+ return Equal(info, withextradata ? COMPARE_ALL : COMPARE_ALL & ~COMPARE_EXTRADATA);
+}
+
+
+// ASSIGNMENT
+void CDVDStreamInfo::Assign(const CDVDStreamInfo& right, bool withextradata)
+{
+ codec = right.codec;
+ type = right.type;
+ uniqueId = right.uniqueId;
+ demuxerId = right.demuxerId;
+ source = right.source;
+ codec_tag = right.codec_tag;
+ flags = right.flags;
+ filename = right.filename;
+ dvd = right.dvd;
+
+ if( extradata && extrasize ) free(extradata);
+
+ if( withextradata && right.extrasize )
+ {
+ extrasize = right.extrasize;
+ extradata = malloc(extrasize);
+ if (!extradata)
+ return;
+ memcpy(extradata, right.extradata, extrasize);
+ }
+ else
+ {
+ extrasize = 0;
+ extradata = 0;
+ }
+
+ cryptoSession = right.cryptoSession;
+ externalInterfaces = right.externalInterfaces;
+
+ // VIDEO
+ fpsscale = right.fpsscale;
+ fpsrate = right.fpsrate;
+ height = right.height;
+ width = right.width;
+ aspect = right.aspect;
+ stills = right.stills;
+ level = right.level;
+ profile = right.profile;
+ ptsinvalid = right.ptsinvalid;
+ forced_aspect = right.forced_aspect;
+ orientation = right.orientation;
+ bitsperpixel = right.bitsperpixel;
+ bitdepth = right.bitdepth;
+ vfr = right.vfr;
+ codecOptions = right.codecOptions;
+ colorSpace = right.colorSpace;
+ colorRange = right.colorRange;
+ colorPrimaries = right.colorPrimaries;
+ colorTransferCharacteristic = right.colorTransferCharacteristic;
+ masteringMetadata = right.masteringMetadata;
+ contentLightMetadata = right.contentLightMetadata;
+ stereo_mode = right.stereo_mode;
+
+ // AUDIO
+ channels = right.channels;
+ samplerate = right.samplerate;
+ blockalign = right.blockalign;
+ bitrate = right.bitrate;
+ bitspersample = right.bitspersample;
+ channellayout = right.channellayout;
+
+ // SUBTITLE
+}
+
+void CDVDStreamInfo::Assign(const CDemuxStream& right, bool withextradata)
+{
+ Clear();
+
+ codec = right.codec;
+ type = right.type;
+ uniqueId = right.uniqueId;
+ demuxerId = right.demuxerId;
+ source = right.source;
+ codec_tag = right.codec_fourcc;
+ profile = right.profile;
+ level = right.level;
+ flags = right.flags;
+
+ if (withextradata && right.ExtraSize)
+ {
+ extrasize = right.ExtraSize;
+ extradata = malloc(extrasize);
+ if (!extradata)
+ return;
+ memcpy(extradata, right.ExtraData.get(), extrasize);
+ }
+
+ cryptoSession = right.cryptoSession;
+ externalInterfaces = right.externalInterfaces;
+
+ if (right.type == STREAM_AUDIO)
+ {
+ const CDemuxStreamAudio *stream = static_cast<const CDemuxStreamAudio*>(&right);
+ channels = stream->iChannels;
+ samplerate = stream->iSampleRate;
+ blockalign = stream->iBlockAlign;
+ bitrate = stream->iBitRate;
+ bitspersample = stream->iBitsPerSample;
+ channellayout = stream->iChannelLayout;
+ }
+ else if (right.type == STREAM_VIDEO)
+ {
+ const CDemuxStreamVideo *stream = static_cast<const CDemuxStreamVideo*>(&right);
+ fpsscale = stream->iFpsScale;
+ fpsrate = stream->iFpsRate;
+ height = stream->iHeight;
+ width = stream->iWidth;
+ aspect = stream->fAspect;
+ vfr = stream->bVFR;
+ ptsinvalid = stream->bPTSInvalid;
+ forced_aspect = stream->bForcedAspect;
+ orientation = stream->iOrientation;
+ bitsperpixel = stream->iBitsPerPixel;
+ bitdepth = stream->bitDepth;
+ colorSpace = stream->colorSpace;
+ colorRange = stream->colorRange;
+ colorPrimaries = stream->colorPrimaries;
+ colorTransferCharacteristic = stream->colorTransferCharacteristic;
+ masteringMetadata = stream->masteringMetaData;
+ contentLightMetadata = stream->contentLightMetaData;
+ stereo_mode = stream->stereo_mode;
+ }
+ else if (right.type == STREAM_SUBTITLE)
+ {
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/DVDStreamInfo.h b/xbmc/cores/VideoPlayer/DVDStreamInfo.h
new file mode 100644
index 0000000..8a3da27
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDStreamInfo.h
@@ -0,0 +1,123 @@
+/*
+ * 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 "DVDDemuxers/DVDDemux.h"
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+}
+
+#define CODEC_FORCE_SOFTWARE 0x01
+#define CODEC_ALLOW_FALLBACK 0x02
+
+class CDemuxStream;
+struct DemuxCryptoSession;
+
+class CDVDStreamInfo
+{
+public:
+ CDVDStreamInfo();
+ CDVDStreamInfo(const CDVDStreamInfo &right, bool withextradata = true);
+ CDVDStreamInfo(const CDemuxStream &right, bool withextradata = true);
+
+ ~CDVDStreamInfo();
+
+ void Clear(); // clears current information
+ bool Equal(const CDVDStreamInfo& right, int compare);
+ bool Equal(const CDemuxStream &right, bool withextradata);
+
+ void Assign(const CDVDStreamInfo &right, bool withextradata);
+ void Assign(const CDemuxStream &right, bool withextradata);
+
+ enum
+ {
+ COMPARE_EXTRADATA = 1,
+ COMPARE_ID = 2,
+ COMPARE_ALL = 3,
+ };
+
+ AVCodecID codec;
+ StreamType type;
+ int uniqueId;
+ int demuxerId = -1;
+ int source{STREAM_SOURCE_NONE};
+ int flags;
+ std::string filename;
+ bool dvd;
+ int codecOptions;
+
+ // VIDEO
+ int fpsscale; // scale of 1001 and a rate of 60000 will result in 59.94 fps
+ int fpsrate;
+ int height; // height of the stream reported by the demuxer
+ int width; // width of the stream reported by the demuxer
+ double aspect; // display aspect as reported by demuxer
+ bool vfr; // variable framerate
+ bool stills; // there may be odd still frames in video
+ int level; // encoder level of the stream reported by the decoder. used to qualify hw decoders.
+ int profile; // encoder profile of the stream reported by the decoder. used to qualify hw decoders.
+ bool ptsinvalid; // pts cannot be trusted (avi's).
+ bool forced_aspect; // aspect is forced from container
+ int orientation; // orientation of the video in degrees counter clockwise
+ int bitsperpixel;
+ int bitdepth;
+ AVColorSpace colorSpace;
+ AVColorRange colorRange;
+ AVColorPrimaries colorPrimaries;
+ AVColorTransferCharacteristic colorTransferCharacteristic;
+ std::shared_ptr<AVMasteringDisplayMetadata> masteringMetadata;
+ std::shared_ptr<AVContentLightMetadata> contentLightMetadata;
+ std::string stereo_mode; // stereoscopic 3d mode
+
+ // AUDIO
+ int channels;
+ int samplerate;
+ int bitrate;
+ int blockalign;
+ int bitspersample;
+ uint64_t channellayout;
+
+ // SUBTITLE
+
+ // CODEC EXTRADATA
+ void* extradata; // extra data for codec to use
+ unsigned int extrasize; // size of extra data
+ unsigned int codec_tag; // extra identifier hints for decoding
+
+ // Crypto initialization Data
+ std::shared_ptr<DemuxCryptoSession> cryptoSession;
+ std::shared_ptr<ADDON::IAddonProvider> externalInterfaces;
+
+ bool operator==(const CDVDStreamInfo& right) { return Equal(right, COMPARE_ALL); }
+ bool operator!=(const CDVDStreamInfo& right) { return !Equal(right, COMPARE_ALL); }
+
+ CDVDStreamInfo& operator=(const CDVDStreamInfo& right)
+ {
+ if (this != &right)
+ Assign(right, true);
+
+ return *this;
+ }
+
+ bool operator==(const CDemuxStream& right)
+ {
+ return Equal(CDVDStreamInfo(right, true), COMPARE_ALL);
+ }
+ bool operator!=(const CDemuxStream& right)
+ {
+ return !Equal(CDVDStreamInfo(right, true), COMPARE_ALL);
+ }
+
+ CDVDStreamInfo& operator=(const CDemuxStream& right)
+ {
+ Assign(right, true);
+ return *this;
+ }
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/CMakeLists.txt b/xbmc/cores/VideoPlayer/DVDSubtitles/CMakeLists.txt
new file mode 100644
index 0000000..16d685a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/CMakeLists.txt
@@ -0,0 +1,33 @@
+set(SOURCES DVDFactorySubtitle.cpp
+ DVDSubtitleLineCollection.cpp
+ DVDSubtitleParserMicroDVD.cpp
+ DVDSubtitleParserMPL2.cpp
+ DVDSubtitleParserSami.cpp
+ DVDSubtitleParserSubrip.cpp
+ DVDSubtitleParserVplayer.cpp
+ DVDSubtitleStream.cpp
+ DVDSubtitlesLibass.cpp
+ DVDSubtitleParserSSA.cpp
+ DVDSubtitleTagMicroDVD.cpp
+ DVDSubtitleTagSami.cpp
+ SubtitleParserWebVTT.cpp
+ SubtitlesAdapter.cpp)
+
+set(HEADERS DVDFactorySubtitle.h
+ DVDSubtitleLineCollection.h
+ DVDSubtitleParser.h
+ DVDSubtitleParserMPL2.h
+ DVDSubtitleParserMicroDVD.h
+ DVDSubtitleParserSSA.h
+ DVDSubtitleParserSami.h
+ DVDSubtitleParserSubrip.h
+ DVDSubtitleParserVplayer.h
+ DVDSubtitleStream.h
+ DVDSubtitleTagMicroDVD.h
+ DVDSubtitleTagSami.h
+ DVDSubtitlesLibass.h
+ SubtitleParserWebVTT.h
+ SubtitlesAdapter.h
+ SubtitlesStyle.h)
+
+core_add_library(dvdsubtitles)
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDFactorySubtitle.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDFactorySubtitle.cpp
new file mode 100644
index 0000000..5127033
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDFactorySubtitle.cpp
@@ -0,0 +1,82 @@
+/*
+ * 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 "DVDFactorySubtitle.h"
+
+#include "DVDSubtitleParserMPL2.h"
+#include "DVDSubtitleParserMicroDVD.h"
+#include "DVDSubtitleParserSSA.h"
+#include "DVDSubtitleParserSami.h"
+#include "DVDSubtitleParserSubrip.h"
+#include "DVDSubtitleParserVplayer.h"
+#include "DVDSubtitleStream.h"
+#include "SubtitleParserWebVTT.h"
+#include "utils/StringUtils.h"
+
+#include <cstring>
+#include <memory>
+
+CDVDSubtitleParser* CDVDFactorySubtitle::CreateParser(std::string& strFile)
+{
+ std::string line;
+ int i;
+
+ std::unique_ptr<CDVDSubtitleStream> pStream(new CDVDSubtitleStream());
+ if(!pStream->Open(strFile))
+ {
+ return nullptr;
+ }
+
+ for (int t = 0; t < 256; t++)
+ {
+ if (pStream->ReadLine(line))
+ {
+ if ((sscanf(line.c_str(), "{%d}{}", &i) == 1) ||
+ (sscanf(line.c_str(), "{%d}{%d}", &i, &i) == 2))
+ {
+ return new CDVDSubtitleParserMicroDVD(std::move(pStream), strFile);
+ }
+ else if (sscanf(line.c_str(), "[%d][%d]", &i, &i) == 2)
+ {
+ return new CDVDSubtitleParserMPL2(std::move(pStream), strFile);
+ }
+ else if (sscanf(line.c_str(), "%d:%d:%d%*c%d --> %d:%d:%d%*c%d", &i, &i, &i, &i, &i, &i, &i,
+ &i) == 8)
+ {
+ return new CDVDSubtitleParserSubrip(std::move(pStream), strFile);
+ }
+ else if (sscanf(line.c_str(), "%d:%d:%d:", &i, &i, &i) == 3)
+ {
+ return new CDVDSubtitleParserVplayer(std::move(pStream), strFile);
+ }
+ else if (!StringUtils::CompareNoCase(line, "!: This is a Sub Station Alpha v", 32) ||
+ !StringUtils::CompareNoCase(line, "ScriptType: v4.00", 17) ||
+ !StringUtils::CompareNoCase(line, "Dialogue: Marked", 16) ||
+ !StringUtils::CompareNoCase(line, "Dialogue: ", 10) ||
+ !StringUtils::CompareNoCase(line, "[Events]", 8))
+ {
+ return new CDVDSubtitleParserSSA(std::move(pStream), strFile);
+ }
+ else if (line == "<SAMI>")
+ {
+ return new CDVDSubtitleParserSami(std::move(pStream), strFile);
+ }
+ else if (!StringUtils::CompareNoCase(line, "WEBVTT", 6))
+ {
+ return new CSubtitleParserWebVTT(std::move(pStream), strFile);
+ }
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ return nullptr;
+}
+
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDFactorySubtitle.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDFactorySubtitle.h
new file mode 100644
index 0000000..724c558
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDFactorySubtitle.h
@@ -0,0 +1,25 @@
+/*
+ * 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 <string>
+#include <vector>
+
+class CDVDSubtitleParser;
+class CDVDSubtitleStream;
+
+typedef std::vector<std::string> VecSubtitleFiles;
+typedef std::vector<std::string>::iterator VecSubtitleFilesIter;
+
+class CDVDFactorySubtitle
+{
+public:
+ static CDVDSubtitleParser* CreateParser(std::string& strFile);
+};
+
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleLineCollection.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleLineCollection.cpp
new file mode 100644
index 0000000..3ef2703
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleLineCollection.cpp
@@ -0,0 +1,111 @@
+/*
+ * 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 "DVDSubtitleLineCollection.h"
+
+#include <stddef.h>
+
+
+CDVDSubtitleLineCollection::CDVDSubtitleLineCollection()
+{
+ m_pHead = NULL;
+ m_pCurrent = NULL;
+ m_pTail = NULL;
+
+ m_iSize = 0;
+}
+
+CDVDSubtitleLineCollection::~CDVDSubtitleLineCollection()
+{
+ Clear();
+}
+
+void CDVDSubtitleLineCollection::Add(CDVDOverlay* pOverlay)
+{
+ ListElement* pElement = new ListElement;
+ pElement->pOverlay = pOverlay;
+ pElement->pNext = NULL;
+
+ if (!m_pHead)
+ {
+ m_pHead = m_pTail = pElement;
+ m_pCurrent = m_pHead;
+ }
+ else
+ {
+ m_pTail->pNext = pElement;
+ m_pTail = pElement;
+ }
+
+ m_iSize++;
+}
+
+void CDVDSubtitleLineCollection::Sort()
+{
+ if (!m_pHead || !m_pHead->pNext)
+ return;
+
+ for (ListElement* p1 = m_pHead; p1->pNext != NULL; p1 = p1->pNext)
+ {
+ for (ListElement* p2 = p1->pNext; p2 != NULL; p2 = p2->pNext)
+ {
+ if (p1->pOverlay->iPTSStartTime > p2->pOverlay->iPTSStartTime)
+ {
+ CDVDOverlay* temp = p1->pOverlay;
+ p1->pOverlay = p2->pOverlay;
+ p2->pOverlay = temp;
+ }
+ }
+ }
+}
+
+CDVDOverlay* CDVDSubtitleLineCollection::Get(double iPts)
+{
+ CDVDOverlay* pOverlay = NULL;
+
+ if (m_pCurrent)
+ {
+ while (m_pCurrent && m_pCurrent->pOverlay->iPTSStopTime < iPts)
+ {
+ m_pCurrent = m_pCurrent->pNext;
+ }
+
+ if (m_pCurrent)
+ {
+ pOverlay = m_pCurrent->pOverlay;
+
+ // advance to the next overlay
+ m_pCurrent = m_pCurrent->pNext;
+ }
+ }
+ return pOverlay;
+}
+
+void CDVDSubtitleLineCollection::Reset()
+{
+ m_pCurrent = m_pHead;
+}
+
+void CDVDSubtitleLineCollection::Clear()
+{
+ ListElement* pElement = NULL;
+
+ while (m_pHead)
+ {
+ pElement = m_pHead;
+ m_pHead = pElement->pNext;
+
+ pElement->pOverlay->Release();
+ delete pElement;
+ }
+
+ m_pTail = NULL;
+ m_pHead = NULL;
+ m_pCurrent = NULL;
+ m_iSize = 0;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleLineCollection.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleLineCollection.h
new file mode 100644
index 0000000..177fe96
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleLineCollection.h
@@ -0,0 +1,48 @@
+/*
+ * 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 "../DVDCodecs/Overlay/DVDOverlay.h"
+
+typedef struct stListElement
+{
+ CDVDOverlay* pOverlay;
+ struct stListElement* pNext;
+
+} ListElement;
+
+class CDVDSubtitleLineCollection
+{
+public:
+ CDVDSubtitleLineCollection();
+ virtual ~CDVDSubtitleLineCollection();
+
+ //void Lock() { EnterCriticalSection(&m_critSection); }
+ //void Unlock() { LeaveCriticalSection(&m_critSection); }
+
+ void Add(CDVDOverlay* pSubtitle);
+ void Sort();
+
+ CDVDOverlay* Get(double iPts = 0LL); // get the first overlay in this fifo
+
+ void Reset();
+
+ void Remove();
+ void Clear();
+ int GetSize() { return m_iSize; }
+
+private:
+ ListElement* m_pHead;
+ ListElement* m_pCurrent;
+ ListElement* m_pTail;
+
+ int m_iSize;
+ //CRITICAL_SECTION m_critSection;
+};
+
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParser.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParser.h
new file mode 100644
index 0000000..5a31926
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParser.h
@@ -0,0 +1,88 @@
+/*
+ * 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 "../DVDCodecs/Overlay/DVDOverlay.h"
+#include "DVDSubtitleLineCollection.h"
+#include "DVDSubtitleStream.h"
+
+#include <memory>
+#include <stdio.h>
+#include <string>
+
+class CDVDStreamInfo;
+
+class CDVDSubtitleParser
+{
+public:
+ virtual ~CDVDSubtitleParser() = default;
+ virtual bool Open(CDVDStreamInfo &hints) = 0;
+ virtual void Dispose() = 0;
+ virtual void Reset() = 0;
+ virtual CDVDOverlay* Parse(double iPts) = 0;
+ virtual const std::string& GetName() const = 0;
+};
+
+class CDVDSubtitleParserCollection
+ : public CDVDSubtitleParser
+{
+public:
+ explicit CDVDSubtitleParserCollection(const std::string& strFile) : m_filename(strFile) {}
+ ~CDVDSubtitleParserCollection() override = default;
+ CDVDOverlay* Parse(double iPts) override
+ {
+ CDVDOverlay* o = m_collection.Get(iPts);
+ if(o == NULL)
+ return o;
+ return o->Clone();
+ }
+ void Reset() override { m_collection.Reset(); }
+ void Dispose() override { m_collection.Clear(); }
+
+protected:
+ CDVDSubtitleLineCollection m_collection;
+ std::string m_filename;
+};
+
+class CDVDSubtitleParserText
+ : public CDVDSubtitleParserCollection
+{
+public:
+ CDVDSubtitleParserText(std::unique_ptr<CDVDSubtitleStream>&& stream,
+ const std::string& filename,
+ const char* name)
+ : CDVDSubtitleParserCollection(filename), m_pStream(std::move(stream)), m_parserName(name)
+ {
+ }
+
+ ~CDVDSubtitleParserText() override = default;
+
+ /*
+ * \brief Returns parser name
+ */
+ const std::string& GetName() const override { return m_parserName; }
+
+protected:
+ using CDVDSubtitleParserCollection::Open;
+ bool Open()
+ {
+ if(m_pStream)
+ {
+ if (m_pStream->Seek(0))
+ return true;
+ }
+ else
+ m_pStream.reset(new CDVDSubtitleStream());
+
+ return m_pStream->Open(m_filename);
+ }
+
+ std::unique_ptr<CDVDSubtitleStream> m_pStream;
+ std::string m_parserName;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMPL2.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMPL2.cpp
new file mode 100644
index 0000000..1844535
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMPL2.cpp
@@ -0,0 +1,72 @@
+/*
+ * 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 "DVDSubtitleParserMPL2.h"
+
+#include "DVDStreamInfo.h"
+#include "DVDSubtitleTagMicroDVD.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/RegExp.h"
+
+#include <cstdlib>
+
+CDVDSubtitleParserMPL2::CDVDSubtitleParserMPL2(std::unique_ptr<CDVDSubtitleStream>&& stream,
+ const std::string& filename)
+ : CDVDSubtitleParserText(std::move(stream), filename, "MPL2 Subtitle Parser"),
+ m_framerate(DVD_TIME_BASE / 10.0)
+{
+}
+
+CDVDSubtitleParserMPL2::~CDVDSubtitleParserMPL2()
+{
+ Dispose();
+}
+
+bool CDVDSubtitleParserMPL2::Open(CDVDStreamInfo& hints)
+{
+ if (!CDVDSubtitleParserText::Open())
+ return false;
+
+ if (!Initialize())
+ return false;
+
+ // MPL2 is time-based, with 0.1s accuracy
+ m_framerate = DVD_TIME_BASE / 10.0;
+
+ CRegExp reg;
+ if (!reg.RegComp("\\[([0-9]+)\\]\\[([0-9]+)\\](.+)"))
+ return false;
+ CDVDSubtitleTagMicroDVD TagConv;
+ std::string line;
+
+ while (m_pStream->ReadLine(line))
+ {
+ int pos = reg.RegFind(line);
+ if (pos > -1)
+ {
+ std::string startFrame(reg.GetMatch(1));
+ std::string endFrame(reg.GetMatch(2));
+ std::string text(reg.GetMatch(3));
+
+ double iPTSStartTime = m_framerate * std::atoi(startFrame.c_str());
+ double iPTSStopTime = m_framerate * std::atoi(endFrame.c_str());
+
+ TagConv.ConvertLine(text);
+ AddSubtitle(text, iPTSStartTime, iPTSStopTime);
+ }
+ }
+
+ m_collection.Add(CreateOverlay());
+
+ return true;
+}
+
+void CDVDSubtitleParserMPL2::Dispose()
+{
+ CDVDSubtitleParserCollection::Dispose();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMPL2.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMPL2.h
new file mode 100644
index 0000000..4887196
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMPL2.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DVDSubtitleParser.h"
+#include "SubtitlesAdapter.h"
+
+#include <memory>
+
+class CDVDSubtitleParserMPL2 : public CDVDSubtitleParserText, private CSubtitlesAdapter
+{
+public:
+ CDVDSubtitleParserMPL2(std::unique_ptr<CDVDSubtitleStream>&& stream, const std::string& strFile);
+ ~CDVDSubtitleParserMPL2() override;
+
+ bool Open(CDVDStreamInfo& hints) override;
+ void Dispose() override;
+
+private:
+ double m_framerate;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMicroDVD.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMicroDVD.cpp
new file mode 100644
index 0000000..1965f8f
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMicroDVD.cpp
@@ -0,0 +1,79 @@
+/*
+ * 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 "DVDSubtitleParserMicroDVD.h"
+
+#include "DVDStreamInfo.h"
+#include "DVDSubtitleTagMicroDVD.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/RegExp.h"
+#include "utils/log.h"
+
+#include <cstdlib>
+
+CDVDSubtitleParserMicroDVD::CDVDSubtitleParserMicroDVD(std::unique_ptr<CDVDSubtitleStream>&& stream,
+ const std::string& filename)
+ : CDVDSubtitleParserText(std::move(stream), filename, "MicroDVD Subtitle Parser"),
+ m_framerate(DVD_TIME_BASE / 25.0)
+{
+}
+
+CDVDSubtitleParserMicroDVD::~CDVDSubtitleParserMicroDVD()
+{
+ Dispose();
+}
+
+bool CDVDSubtitleParserMicroDVD::Open(CDVDStreamInfo& hints)
+{
+ if (!CDVDSubtitleParserText::Open())
+ return false;
+
+ if (!Initialize())
+ return false;
+
+ CLog::Log(LOGDEBUG, "{} - framerate {}:{}", __FUNCTION__, hints.fpsrate, hints.fpsscale);
+ if (hints.fpsscale > 0 && hints.fpsrate > 0)
+ {
+ m_framerate = (double)hints.fpsscale / (double)hints.fpsrate;
+ m_framerate *= DVD_TIME_BASE;
+ }
+ else
+ m_framerate = DVD_TIME_BASE / 25.0;
+
+ CRegExp reg;
+ if (!reg.RegComp("\\{([0-9]+)\\}\\{([0-9]+)\\}(.+)"))
+ return false;
+ CDVDSubtitleTagMicroDVD TagConv;
+ std::string line;
+
+ while (m_pStream->ReadLine(line))
+ {
+ int pos = reg.RegFind(line);
+ if (pos > -1)
+ {
+ std::string startFrame(reg.GetMatch(1));
+ std::string endFrame(reg.GetMatch(2));
+ std::string text(reg.GetMatch(3));
+
+ double iPTSStartTime = m_framerate * std::atoi(startFrame.c_str());
+ double iPTSStopTime = m_framerate * std::atoi(endFrame.c_str());
+
+ TagConv.ConvertLine(text);
+ AddSubtitle(text, iPTSStartTime, iPTSStopTime);
+ }
+ }
+
+ m_collection.Add(CreateOverlay());
+
+ return true;
+}
+
+void CDVDSubtitleParserMicroDVD::Dispose()
+{
+ CDVDSubtitleParserCollection::Dispose();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMicroDVD.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMicroDVD.h
new file mode 100644
index 0000000..3986891
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserMicroDVD.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "DVDSubtitleParser.h"
+#include "SubtitlesAdapter.h"
+
+#include <memory>
+
+class CDVDSubtitleParserMicroDVD : public CDVDSubtitleParserText, private CSubtitlesAdapter
+{
+public:
+ CDVDSubtitleParserMicroDVD(std::unique_ptr<CDVDSubtitleStream>&& stream,
+ const std::string& strFile);
+ ~CDVDSubtitleParserMicroDVD() override;
+
+ bool Open(CDVDStreamInfo& hints) override;
+ void Dispose() override;
+
+private:
+ double m_framerate;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSSA.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSSA.cpp
new file mode 100644
index 0000000..1332a80
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSSA.cpp
@@ -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.
+ */
+
+#include "DVDSubtitleParserSSA.h"
+
+#include "DVDCodecs/Overlay/DVDOverlaySSA.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SubtitlesSettings.h"
+
+using namespace KODI;
+
+CDVDSubtitleParserSSA::CDVDSubtitleParserSSA(std::unique_ptr<CDVDSubtitleStream>&& pStream,
+ const std::string& strFile)
+ : CDVDSubtitleParserText(std::move(pStream), strFile, "SSA Subtitle Parser"),
+ m_libass(std::make_shared<CDVDSubtitlesLibass>())
+{
+ m_libass->Configure();
+}
+
+CDVDSubtitleParserSSA::~CDVDSubtitleParserSSA()
+{
+ Dispose();
+}
+
+bool CDVDSubtitleParserSSA::Open(CDVDStreamInfo& hints)
+{
+
+ if (!CDVDSubtitleParserText::Open())
+ return false;
+
+ const std::string& data = m_pStream->GetData();
+ if (!m_libass->CreateTrack(const_cast<char*>(data.c_str()), data.length()))
+ return false;
+
+ CDVDOverlaySSA* overlay = new CDVDOverlaySSA(m_libass);
+ overlay->iPTSStartTime = 0.0;
+ overlay->iPTSStopTime = DVD_NOPTS_VALUE;
+ auto overrideStyles{
+ CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()->GetOverrideStyles()};
+ overlay->SetForcedMargins(overrideStyles != SUBTITLES::OverrideStyles::STYLES_POSITIONS &&
+ overrideStyles != SUBTITLES::OverrideStyles::POSITIONS);
+ m_collection.Add(overlay);
+
+ return true;
+}
+
+void CDVDSubtitleParserSSA::Dispose()
+{
+ CDVDSubtitleParserCollection::Dispose();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSSA.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSSA.h
new file mode 100644
index 0000000..2ccb577
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSSA.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DVDSubtitleParser.h"
+#include "DVDSubtitlesLibass.h"
+
+#include <memory>
+
+class CDVDSubtitleParserSSA : public CDVDSubtitleParserText
+{
+public:
+ CDVDSubtitleParserSSA(std::unique_ptr<CDVDSubtitleStream>&& pStream, const std::string& strFile);
+ ~CDVDSubtitleParserSSA() override;
+
+ bool Open(CDVDStreamInfo& hints) override;
+ void Dispose() override;
+
+private:
+ std::shared_ptr<CDVDSubtitlesLibass> m_libass;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSami.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSami.cpp
new file mode 100644
index 0000000..2108e55
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSami.cpp
@@ -0,0 +1,157 @@
+/*
+ * 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 "DVDSubtitleParserSami.h"
+
+#include "DVDStreamInfo.h"
+#include "DVDSubtitleTagSami.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+CDVDSubtitleParserSami::CDVDSubtitleParserSami(std::unique_ptr<CDVDSubtitleStream>&& pStream,
+ const std::string& filename)
+ : CDVDSubtitleParserText(std::move(pStream), filename, "SAMI Subtitle Parser")
+{
+}
+
+CDVDSubtitleParserSami::~CDVDSubtitleParserSami()
+{
+ Dispose();
+}
+
+bool CDVDSubtitleParserSami::Open(CDVDStreamInfo& hints)
+{
+ if (!CDVDSubtitleParserText::Open())
+ return false;
+
+ if (!Initialize())
+ return false;
+
+ CRegExp regLine(true);
+ if (!regLine.RegComp("<SYNC START=\"?([0-9]+)\"?>(.+)?"))
+ return false;
+ CRegExp regClassID(true);
+ if (!regClassID.RegComp("<P Class=\"?([\\w\\d]+)\"?>"))
+ return false;
+
+ std::string strFileName;
+ std::string strClassID;
+ strFileName = StringUtils::ToLower(URIUtils::GetFileName(m_filename));
+
+ CDVDSubtitleTagSami TagConv;
+ if (!TagConv.Init())
+ return false;
+
+ TagConv.LoadHead(m_pStream.get());
+
+ // If there are more languages contained in a file,
+ // try getting the language class ID that matches the language name
+ // specified in the filename
+ if (TagConv.m_Langclass.size() >= 2)
+ {
+ for (unsigned int i = 0; i < TagConv.m_Langclass.size(); i++)
+ {
+ std::string langName = TagConv.m_Langclass[i].Name;
+ StringUtils::ToLower(langName);
+ if (strFileName.find(langName) != std::string::npos)
+ {
+ strClassID = TagConv.m_Langclass[i].ID;
+ break;
+ }
+ }
+ // No language specified or found, try to select the first class ID
+ if (strClassID.empty() && !(TagConv.m_Langclass.empty()))
+ {
+ strClassID = TagConv.m_Langclass[0].ID;
+ }
+ }
+
+ const char* langClassID{nullptr};
+ if (!strClassID.empty())
+ {
+ StringUtils::ToLower(strClassID);
+ langClassID = strClassID.c_str();
+ }
+
+ int prevSubId = NO_SUBTITLE_ID;
+ double lastPTSStartTime = 0;
+ std::string lastLangClassID;
+ // SAMI synchronization provides only the start time value,
+ // for the stop time it takes in consideration the start time of the next line,
+ // that, could, be an empty string with a "&nbsp;" tag.
+ // Last line could not have the stop time then we set as default 4 secs.
+ int defaultDuration = 4 * DVD_TIME_BASE;
+ std::string line;
+
+ while (m_pStream->ReadLine(line))
+ {
+ // Find the language Class ID in current line (if exist)
+ if (regClassID.RegFind(line) > -1)
+ {
+ lastLangClassID = regClassID.GetMatch(1);
+ StringUtils::ToLower(lastLangClassID);
+ }
+
+ int pos = regLine.RegFind(line);
+ if (pos > -1) // Sync tag found
+ {
+ double currStartTime = static_cast<double>(std::atoi(regLine.GetMatch(1).c_str()));
+ double currPTSStartTime = currStartTime * DVD_TIME_BASE / 1000;
+
+ // We set the duration for the previous line (Event) by using the current start time
+ ChangeSubtitleStopTime(prevSubId, currPTSStartTime);
+
+ // We try to get text after Sync tag (if exists)
+ std::string text = regLine.GetMatch(2);
+ if (text.empty())
+ {
+ prevSubId = NO_SUBTITLE_ID;
+ }
+ else
+ {
+ TagConv.ConvertLine(text, langClassID);
+ TagConv.CloseTag(text);
+ prevSubId = AddSubtitle(text, currPTSStartTime, currPTSStartTime + defaultDuration);
+ }
+
+ lastPTSStartTime = currPTSStartTime;
+ }
+ else
+ {
+ // Lines without Sync tag e.g. for multiple styles or lines,
+ // need to be appended to last line added with sync tag
+ // but they have to match the current language Class ID (if set)
+ if (!strClassID.empty() && strClassID != lastLangClassID)
+ continue;
+
+ std::string text(line);
+ TagConv.ConvertLine(text, langClassID);
+ TagConv.CloseTag(text);
+ if (prevSubId != NO_SUBTITLE_ID)
+ {
+ text.insert(0, "\n");
+ AppendToSubtitle(prevSubId, text.c_str());
+ }
+ else
+ {
+ prevSubId = AddSubtitle(text, lastPTSStartTime, lastPTSStartTime + defaultDuration);
+ }
+ }
+ }
+
+ m_collection.Add(CreateOverlay());
+
+ return true;
+}
+
+void CDVDSubtitleParserSami::Dispose()
+{
+ CDVDSubtitleParserCollection::Dispose();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSami.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSami.h
new file mode 100644
index 0000000..a7fe497
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSami.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "DVDSubtitleParser.h"
+#include "SubtitlesAdapter.h"
+
+#include <memory>
+
+class CRegExp;
+
+class CDVDSubtitleParserSami : public CDVDSubtitleParserText, private CSubtitlesAdapter
+{
+public:
+ CDVDSubtitleParserSami(std::unique_ptr<CDVDSubtitleStream>&& pStream, const std::string& strFile);
+ ~CDVDSubtitleParserSami() override;
+
+ bool Open(CDVDStreamInfo& hints) override;
+ void Dispose() override;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSubrip.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSubrip.cpp
new file mode 100644
index 0000000..245c782
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSubrip.cpp
@@ -0,0 +1,94 @@
+/*
+ * 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 "DVDSubtitleParserSubrip.h"
+
+#include "DVDSubtitleTagSami.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/StringUtils.h"
+
+CDVDSubtitleParserSubrip::CDVDSubtitleParserSubrip(std::unique_ptr<CDVDSubtitleStream>&& pStream,
+ const std::string& strFile)
+ : CDVDSubtitleParserText(std::move(pStream), strFile, "SubRip Subtitle Parser")
+{
+}
+
+CDVDSubtitleParserSubrip::~CDVDSubtitleParserSubrip()
+{
+ Dispose();
+}
+
+bool CDVDSubtitleParserSubrip::Open(CDVDStreamInfo& hints)
+{
+ if (!CDVDSubtitleParserText::Open())
+ return false;
+
+ if (!Initialize())
+ return false;
+
+ CDVDSubtitleTagSami TagConv;
+ if (!TagConv.Init())
+ return false;
+
+ std::string line;
+
+ while (m_pStream->ReadLine(line))
+ {
+ StringUtils::Trim(line);
+
+ if (line.length() > 0)
+ {
+ char sep;
+ int hh1, mm1, ss1, ms1, hh2, mm2, ss2, ms2;
+ int c = sscanf(line.c_str(), "%d%c%d%c%d%c%d --> %d%c%d%c%d%c%d\n", &hh1, &sep, &mm1, &sep,
+ &ss1, &sep, &ms1, &hh2, &sep, &mm2, &sep, &ss2, &sep, &ms2);
+
+ if (c == 1)
+ {
+ // numbering, skip it
+ }
+ else if (c == 14) // time info
+ {
+ double iPTSStartTime =
+ ((double)(((hh1 * 60 + mm1) * 60) + ss1) * 1000 + ms1) * (DVD_TIME_BASE / 1000);
+ double iPTSStopTime =
+ ((double)(((hh2 * 60 + mm2) * 60) + ss2) * 1000 + ms2) * (DVD_TIME_BASE / 1000);
+
+ std::string convText;
+ while (m_pStream->ReadLine(line))
+ {
+ StringUtils::Trim(line);
+
+ // empty line, next subtitle is about to start
+ if (line.empty())
+ break;
+
+ if (convText.size() > 0)
+ convText += "\n";
+ TagConv.ConvertLine(line);
+ convText += line;
+ }
+
+ if (!convText.empty())
+ {
+ TagConv.CloseTag(convText);
+ AddSubtitle(convText, iPTSStartTime, iPTSStopTime);
+ }
+ }
+ }
+ }
+
+ m_collection.Add(CreateOverlay());
+
+ return true;
+}
+
+void CDVDSubtitleParserSubrip::Dispose()
+{
+ CDVDSubtitleParserCollection::Dispose();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSubrip.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSubrip.h
new file mode 100644
index 0000000..19099f2
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserSubrip.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "DVDSubtitleParser.h"
+#include "SubtitlesAdapter.h"
+
+#include <memory>
+
+class CDVDSubtitleParserSubrip : public CDVDSubtitleParserText, private CSubtitlesAdapter
+{
+public:
+ CDVDSubtitleParserSubrip(std::unique_ptr<CDVDSubtitleStream>&& pStream,
+ const std::string& strFile);
+ ~CDVDSubtitleParserSubrip() override;
+
+ bool Open(CDVDStreamInfo& hints) override;
+ void Dispose() override;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserVplayer.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserVplayer.cpp
new file mode 100644
index 0000000..d8ca31a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserVplayer.cpp
@@ -0,0 +1,89 @@
+/*
+ * 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 "DVDSubtitleParserVplayer.h"
+
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+
+#include <cstdlib>
+
+CDVDSubtitleParserVplayer::CDVDSubtitleParserVplayer(std::unique_ptr<CDVDSubtitleStream>&& pStream,
+ const std::string& strFile)
+ : CDVDSubtitleParserText(std::move(pStream), strFile, "VPlayer Subtitle Parser"),
+ m_framerate(DVD_TIME_BASE)
+{
+}
+
+CDVDSubtitleParserVplayer::~CDVDSubtitleParserVplayer()
+{
+ Dispose();
+}
+
+bool CDVDSubtitleParserVplayer::Open(CDVDStreamInfo& hints)
+{
+ if (!CDVDSubtitleParserText::Open())
+ return false;
+
+ if (!Initialize())
+ return false;
+
+ // Vplayer subtitles have 1-second resolution
+ m_framerate = DVD_TIME_BASE;
+
+ // Vplayer subtitles don't have StopTime, so we use following subtitle's StartTime
+ // for that, unless gap was more than 4 seconds. Then we set subtitle duration
+ // for 4 seconds, to not have text hanging around in silent scenes...
+ int defaultDuration = 4 * (int)m_framerate;
+
+ CRegExp reg;
+ if (!reg.RegComp("([0-9]+):([0-9]+):([0-9]+):(.+)$"))
+ return false;
+
+ int prevSubId = NO_SUBTITLE_ID;
+ double prevPTSStartTime = 0.;
+ std::string line;
+
+ while (m_pStream->ReadLine(line))
+ {
+ if (reg.RegFind(line) > -1)
+ {
+ int hour = std::atoi(reg.GetMatch(1).c_str());
+ int min = std::atoi(reg.GetMatch(2).c_str());
+ int sec = std::atoi(reg.GetMatch(3).c_str());
+ std::string currText = reg.GetMatch(4);
+
+ double currPTSStartTime = m_framerate * (3600 * hour + 60 * min + sec);
+
+ // We have to set the stop time for the previous line (Event)
+ // by using the start time of the current line,
+ // but if the duration is too long we keep the default 4 secs
+ double PTSDuration = currPTSStartTime - prevPTSStartTime;
+
+ if (PTSDuration < defaultDuration)
+ ChangeSubtitleStopTime(prevSubId, currPTSStartTime);
+
+ // A single line can contain multiple lines split by |
+ StringUtils::Replace(currText, "|", "\n");
+
+ prevSubId = AddSubtitle(currText, currPTSStartTime, currPTSStartTime + defaultDuration);
+
+ prevPTSStartTime = currPTSStartTime;
+ }
+ }
+
+ m_collection.Add(CreateOverlay());
+
+ return true;
+}
+
+void CDVDSubtitleParserVplayer::Dispose()
+{
+ CDVDSubtitleParserCollection::Dispose();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserVplayer.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserVplayer.h
new file mode 100644
index 0000000..d49caf3
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleParserVplayer.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "DVDSubtitleParser.h"
+#include "SubtitlesAdapter.h"
+
+#include <memory>
+
+class CDVDSubtitleParserVplayer : public CDVDSubtitleParserText, private CSubtitlesAdapter
+{
+public:
+ CDVDSubtitleParserVplayer(std::unique_ptr<CDVDSubtitleStream>&& pStream,
+ const std::string& strFile);
+ ~CDVDSubtitleParserVplayer() override;
+
+ bool Open(CDVDStreamInfo& hints) override;
+ void Dispose() override;
+
+private:
+ double m_framerate;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleStream.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleStream.cpp
new file mode 100644
index 0000000..3fbe53f
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleStream.cpp
@@ -0,0 +1,138 @@
+/*
+ * 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 "DVDSubtitleStream.h"
+
+#include "DVDInputStreams/DVDFactoryInputStream.h"
+#include "DVDInputStreams/DVDInputStream.h"
+#include "utils/CharsetConverter.h"
+#include "utils/CharsetDetection.h"
+#include "utils/URIUtils.h"
+#include "utils/Utf8Utils.h"
+#include "utils/log.h"
+
+#include <cstdio>
+#include <cstring>
+#include <memory>
+
+CDVDSubtitleStream::CDVDSubtitleStream() = default;
+
+CDVDSubtitleStream::~CDVDSubtitleStream() = default;
+
+bool CDVDSubtitleStream::Open(const std::string& strFile)
+{
+ CFileItem item(strFile, false);
+ item.SetContentLookup(false);
+ std::shared_ptr<CDVDInputStream> pInputStream(CDVDFactoryInputStream::CreateInputStream(NULL, item));
+ if (pInputStream && pInputStream->Open())
+ {
+ // prepare buffer
+ size_t totalread = 0;
+ std::vector<uint8_t> buf(1024);
+
+ if (URIUtils::HasExtension(strFile, ".sub") && IsIncompatible(pInputStream.get(), buf, &totalread))
+ {
+ CLog::Log(LOGDEBUG,
+ "{}: file {} seems to be a vob sub"
+ "file without an idx file, skipping it",
+ __FUNCTION__, CURL::GetRedacted(pInputStream->GetFileName()));
+ buf.clear();
+ return false;
+ }
+
+ static const size_t chunksize = 64 * 1024;
+
+ int read;
+ do
+ {
+ if (totalread == buf.size())
+ buf.resize(buf.size() + chunksize);
+
+ read = pInputStream->Read(buf.data() + totalread, static_cast<int>(buf.size() - totalread));
+ if (read > 0)
+ totalread += read;
+ } while (read > 0);
+
+ if (!totalread)
+ return false;
+
+ std::string tmpStr(reinterpret_cast<char*>(buf.data()), totalread);
+ buf.clear();
+
+ std::string enc(CCharsetDetection::GetBomEncoding(tmpStr));
+ if (enc == "UTF-8" || (enc.empty() && CUtf8Utils::isValidUtf8(tmpStr)))
+ m_subtitleData = tmpStr;
+ else if (!enc.empty())
+ {
+ std::string converted;
+ g_charsetConverter.ToUtf8(enc, tmpStr, converted);
+ if (converted.empty())
+ return false;
+
+ m_subtitleData = converted;
+ }
+ else
+ {
+ std::string converted;
+ g_charsetConverter.subtitleCharsetToUtf8(tmpStr, converted);
+ if (converted.empty())
+ return false;
+
+ m_subtitleData = converted;
+ }
+
+ m_arrayParser.Reset(m_subtitleData.c_str(), m_subtitleData.size());
+ return true;
+ }
+
+ return false;
+}
+
+bool CDVDSubtitleStream::IsIncompatible(CDVDInputStream* pInputStream,
+ std::vector<uint8_t>& buf,
+ size_t* bytesRead)
+{
+ if (!pInputStream)
+ return true;
+
+ static const uint8_t vobsub[] = { 0x00, 0x00, 0x01, 0xBA };
+
+ int read = pInputStream->Read(buf.data(), static_cast<int>(buf.size()));
+
+ if (read < 0)
+ {
+ return true;
+ }
+ else
+ {
+ *bytesRead = (size_t)read;
+ }
+
+ if (read >= 4)
+ {
+ if (!std::memcmp(buf.data(), vobsub, 4))
+ return true;
+ }
+
+ return false;
+}
+
+std::string CDVDSubtitleStream::Read(int length)
+{
+ return m_arrayParser.ReadNextString(length);
+}
+
+bool CDVDSubtitleStream::Seek(int offset)
+{
+ return m_arrayParser.SetPosition(offset);
+}
+
+bool CDVDSubtitleStream::ReadLine(std::string& line)
+{
+ return m_arrayParser.ReadNextLine(line);
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleStream.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleStream.h
new file mode 100644
index 0000000..78ff9ae
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleStream.h
@@ -0,0 +1,65 @@
+/*
+ * 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 "utils/CharArrayParser.h"
+
+#include <sstream>
+#include <string>
+#include <vector>
+
+class CDVDInputStream;
+
+// buffered class for subtitle reading
+
+class CDVDSubtitleStream
+{
+public:
+ CDVDSubtitleStream();
+ virtual ~CDVDSubtitleStream();
+
+ bool Open(const std::string& strFile);
+
+ /** \brief Checks if the subtitle associated with the pInputStream
+ * is known to be incompatible, e.g., vob sub files.
+ * \param[in] pInputStream The input stream for the subtitle to check.
+ */
+ bool IsIncompatible(CDVDInputStream* pInputStream, std::vector<uint8_t>& buf, size_t* bytesRead);
+
+ /*!
+ * \brief Read some data of specified length, from the current position
+ * \param length The length of data to be read
+ * \return The string read
+ */
+ std::string Read(int length);
+
+ /*!
+ * \brief Change the current data position to the specified offset
+ * \param offset The new position
+ * \return True if success, otherwise false
+ */
+ bool Seek(int offset);
+
+ /*!
+ * \brief Read a line of data
+ * \param[OUT] line The data read
+ * \return True if read, otherwise false if EOF
+ */
+ bool ReadLine(std::string& line);
+
+ /*!
+ * \brief Get the full data
+ * \return The data
+ */
+ const std::string& GetData() { return m_subtitleData; }
+
+private:
+ std::string m_subtitleData;
+ CCharArrayParser m_arrayParser;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagMicroDVD.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagMicroDVD.cpp
new file mode 100644
index 0000000..a63345a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagMicroDVD.cpp
@@ -0,0 +1,165 @@
+/*
+ * 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 "DVDSubtitleTagMicroDVD.h"
+
+#include "utils/StringUtils.h"
+
+void CDVDSubtitleTagMicroDVD::ConvertLine(std::string& strUTF8)
+{
+ m_flag[FLAG_BOLD] = 0;
+ m_flag[FLAG_ITALIC] = 0;
+ m_flag[FLAG_UNDERLINE] = 0;
+ m_flag[FLAG_STRIKETHROUGH] = 0;
+ m_flag[FLAG_COLOR] = 0;
+
+ int machine_status = 1;
+ size_t pos = 0;
+
+ while (machine_status > 0)
+ {
+ if (machine_status == 1)
+ {
+ if (strUTF8[pos] == '{')
+ {
+ size_t pos2 = strUTF8.find(':', pos);
+ size_t pos3 = strUTF8.find('}', pos2);
+
+ if ((pos2 != std::string::npos) && (pos3 != std::string::npos))
+ {
+ std::string tagName = strUTF8.substr(pos + 1, pos2 - pos - 1);
+ std::string tagValue = strUTF8.substr(pos2 + 1, pos3 - pos2 - 1);
+ StringUtils::ToLower(tagValue);
+ strUTF8.erase(pos, pos3 - pos + 1);
+ if ((tagName == "Y") || (tagName == "y"))
+ {
+ if ((tagValue == "b") && (m_flag[FLAG_BOLD] == 0))
+ {
+ m_flag[FLAG_BOLD] = (tagName == "Y") ? TAG_ALL_LINE : TAG_ONE_LINE;
+ strUTF8.insert(pos, "{\\b1}");
+ pos += 5;
+ }
+ else if ((tagValue == "i") && (m_flag[FLAG_ITALIC] == 0))
+ {
+ m_flag[FLAG_ITALIC] = (tagName == "Y") ? TAG_ALL_LINE : TAG_ONE_LINE;
+ strUTF8.insert(pos, "{\\i1}");
+ pos += 5;
+ }
+ else if ((tagValue == "u") && (m_flag[FLAG_UNDERLINE] == 0))
+ {
+ m_flag[FLAG_UNDERLINE] = (tagName == "U") ? TAG_ALL_LINE : TAG_ONE_LINE;
+ strUTF8.insert(pos, "{\\u1}");
+ pos += 5;
+ }
+ else if ((tagValue == "s") && (m_flag[FLAG_STRIKETHROUGH] == 0))
+ {
+ m_flag[FLAG_STRIKETHROUGH] = (tagName == "S") ? TAG_ALL_LINE : TAG_ONE_LINE;
+ strUTF8.insert(pos, "{\\s1}");
+ pos += 5;
+ }
+ }
+ else if ((tagName == "C") || (tagName == "c"))
+ {
+ if ((tagValue[0] == '$') && (tagValue.size() == 7))
+ {
+ bool bHex = true;
+ for (int i = 1; i < 7; i++)
+ {
+ char temp = tagValue[i];
+ if (!(('0' <= temp && temp <= '9') || ('a' <= temp && temp <= 'f') ||
+ ('A' <= temp && temp <= 'F')))
+ {
+ bHex = false;
+ break;
+ }
+ }
+
+ if (bHex && (m_flag[FLAG_COLOR] == 0))
+ {
+ m_flag[FLAG_COLOR] = (tagName == "C") ? TAG_ALL_LINE : TAG_ONE_LINE;
+ std::string tempColorTag = "{\\c&H" + tagValue.substr(1, 6) + "&}";
+ strUTF8.insert(pos, tempColorTag);
+ pos += tempColorTag.length();
+ }
+ }
+ }
+ }
+ else
+ machine_status = 2;
+ }
+ else if (strUTF8[pos] == '/')
+ {
+ if (m_flag[FLAG_ITALIC] == 0)
+ {
+ m_flag[FLAG_ITALIC] = TAG_ONE_LINE;
+ strUTF8.insert(pos, "{\\i1}");
+ pos += 5;
+ }
+ else
+ strUTF8.erase(pos, 1);
+ }
+ else
+ machine_status = 2;
+ }
+ else if (machine_status == 2)
+ {
+ size_t pos4;
+ if ((pos4 = strUTF8.find('|', pos)) != std::string::npos)
+ {
+ pos = pos4;
+ if (m_flag[FLAG_BOLD] == TAG_ONE_LINE)
+ {
+ m_flag[FLAG_BOLD] = 0;
+ strUTF8.insert(pos, "{\\b0}");
+ pos += 5;
+ }
+ if (m_flag[FLAG_ITALIC] == TAG_ONE_LINE)
+ {
+ m_flag[FLAG_ITALIC] = 0;
+ strUTF8.insert(pos, "{\\i0}");
+ pos += 5;
+ }
+ if (m_flag[FLAG_UNDERLINE] == TAG_ONE_LINE)
+ {
+ m_flag[FLAG_UNDERLINE] = 0;
+ strUTF8.insert(pos, "{\\u0}");
+ pos += 5;
+ }
+ if (m_flag[FLAG_STRIKETHROUGH] == TAG_ONE_LINE)
+ {
+ m_flag[FLAG_STRIKETHROUGH] = 0;
+ strUTF8.insert(pos, "{\\s0}");
+ pos += 5;
+ }
+ if (m_flag[FLAG_COLOR] == TAG_ONE_LINE)
+ {
+ m_flag[FLAG_COLOR] = 0;
+ strUTF8.insert(pos, "{\\c}");
+ pos += 4;
+ }
+ strUTF8.replace(pos, 1, "\n");
+ pos += 1;
+ machine_status = 1;
+ }
+ else
+ {
+ if (m_flag[FLAG_BOLD] != 0)
+ strUTF8.append("{\\b0}");
+ if (m_flag[FLAG_ITALIC] != 0)
+ strUTF8.append("{\\i0}");
+ if (m_flag[FLAG_UNDERLINE] != 0)
+ strUTF8.append("{\\u0}");
+ if (m_flag[FLAG_STRIKETHROUGH] != 0)
+ strUTF8.append("{\\s0}");
+ if (m_flag[FLAG_COLOR] != 0)
+ strUTF8.append("{\\c}");
+ machine_status = 0;
+ }
+ }
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagMicroDVD.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagMicroDVD.h
new file mode 100644
index 0000000..224f789
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagMicroDVD.h
@@ -0,0 +1,33 @@
+/*
+ * 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 <stdio.h>
+#include <string.h>
+#include <string>
+
+#define FLAG_BOLD 0
+#define FLAG_ITALIC 1
+#define FLAG_UNDERLINE 2
+#define FLAG_STRIKETHROUGH 3
+#define FLAG_COLOR 4
+
+#define TAG_ONE_LINE 1
+#define TAG_ALL_LINE 2
+
+
+class CDVDSubtitleTagMicroDVD
+{
+public:
+ CDVDSubtitleTagMicroDVD() { memset(&m_flag, 0, sizeof(m_flag)); }
+ void ConvertLine(std::string& strUTF8);
+
+private:
+ int m_flag[5];
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.cpp
new file mode 100644
index 0000000..654b9ee
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.cpp
@@ -0,0 +1,308 @@
+/*
+ * 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 "DVDSubtitleTagSami.h"
+
+#include "DVDSubtitleStream.h"
+#include "utils/CharsetConverter.h"
+#include "utils/ColorUtils.h"
+#include "utils/HTMLUtil.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+
+namespace
+{
+
+std::string TranslateColorValue(std::string value)
+{
+ // Get hex color limited to first 6 chars only (e.g. #000000)
+ if (value[0] == '#' && value.size() >= 7)
+ return value.substr(1, 6);
+
+ // Find hex by color name
+ //! @todo: is needed to implement a common way to get color resources
+ //! in order to find the color name on CSS colors list
+ StringUtils::ToLower(value);
+ const auto itHtmlColor = UTILS::COLOR::HTML_BASIC_COLORS.find(value);
+ if (itHtmlColor != UTILS::COLOR::HTML_BASIC_COLORS.cend())
+ return UTILS::COLOR::ConvertoToHexRGB(itHtmlColor->second);
+
+ // Try validate hex color value
+ if (value.size() == 6)
+ {
+ bool isHex = true;
+ for (size_t i = 0; i < 6; i++)
+ {
+ const char currChar = value[i];
+ if (!(('0' <= currChar && currChar <= '9') || ('a' <= currChar && currChar <= 'f') ||
+ ('A' <= currChar && currChar <= 'F')))
+ {
+ isHex = false;
+ break;
+ }
+ }
+ if (isHex)
+ return value;
+ }
+
+ // Fallback to white
+ return "FFFFFF";
+}
+
+} // unnamed namespace
+
+CDVDSubtitleTagSami::~CDVDSubtitleTagSami()
+{
+ delete m_tags;
+ delete m_tagOptions;
+}
+
+bool CDVDSubtitleTagSami::Init()
+{
+ delete m_tags;
+ delete m_tagOptions;
+ m_tags = new CRegExp(true);
+ if (!m_tags->RegComp("(<[^>]*>|\\[nh])"))
+ return false;
+
+ m_tagOptions = new CRegExp(true);
+ if (!m_tagOptions->RegComp("([a-z]+)[ \t]*=[ \t]*(?:[\"'])?([^\"'> ]+)(?:[\"'])?(?:>)?"))
+ return false;
+
+ return true;
+}
+
+void CDVDSubtitleTagSami::ConvertLine(std::string& strUTF8, const char* langClassID)
+{
+ StringUtils::Trim(strUTF8);
+
+ int pos = 0;
+ int del_start = 0;
+ while ((pos = m_tags->RegFind(strUTF8.c_str(), pos)) >= 0)
+ {
+ // Parser for SubRip/SAMI Tags
+ std::string fullTag = m_tags->GetMatch(0);
+ StringUtils::ToLower(fullTag);
+ strUTF8.erase(pos, fullTag.length());
+ if (fullTag == "<b>")
+ {
+ m_flag[FLAG_BOLD] = true;
+ strUTF8.insert(pos, "{\\b1}");
+ pos += 5;
+ }
+ else if ((fullTag == "</b>") && m_flag[FLAG_BOLD])
+ {
+ m_flag[FLAG_BOLD] = false;
+ strUTF8.insert(pos, "{\\b0}");
+ pos += 5;
+ }
+ else if (fullTag == "<i>")
+ {
+ m_flag[FLAG_ITALIC] = true;
+ strUTF8.insert(pos, "{\\i1}");
+ pos += 5;
+ }
+ else if ((fullTag == "</i>") && m_flag[FLAG_ITALIC])
+ {
+ m_flag[FLAG_ITALIC] = false;
+ strUTF8.insert(pos, "{\\i0}");
+ pos += 5;
+ }
+ else if (fullTag == "<u>")
+ {
+ m_flag[FLAG_UNDERLINE] = true;
+ strUTF8.insert(pos, "{\\u1}");
+ pos += 5;
+ }
+ else if ((fullTag == "</u>") && m_flag[FLAG_UNDERLINE])
+ {
+ m_flag[FLAG_UNDERLINE] = false;
+ strUTF8.insert(pos, "{\\u0}");
+ pos += 5;
+ }
+ else if (fullTag == "<s>")
+ {
+ m_flag[FLAG_STRIKETHROUGH] = true;
+ strUTF8.insert(pos, "{\\s1}");
+ pos += 5;
+ }
+ else if ((fullTag == "</s>") && m_flag[FLAG_STRIKETHROUGH])
+ {
+ m_flag[FLAG_STRIKETHROUGH] = false;
+ strUTF8.insert(pos, "{\\s0}");
+ pos += 5;
+ }
+ else if ((fullTag == "</font>") && m_flag[FLAG_COLOR])
+ {
+ m_flag[FLAG_COLOR] = false;
+ strUTF8.insert(pos, "{\\c}");
+ pos += 4;
+ }
+ else if (StringUtils::StartsWith(fullTag, "<font"))
+ {
+ int pos2 = 5;
+ while ((pos2 = m_tagOptions->RegFind(fullTag.c_str(), pos2)) >= 0)
+ {
+ std::string tagOptionName = m_tagOptions->GetMatch(1);
+ std::string tagOptionValue = m_tagOptions->GetMatch(2);
+ pos2 += static_cast<int>(tagOptionName.length() + tagOptionValue.length());
+ if (tagOptionName == "color")
+ {
+ m_flag[FLAG_COLOR] = true;
+
+ std::string colorHex = TranslateColorValue(tagOptionValue);
+ // Convert RGB to BGR
+ std::swap(colorHex[0], colorHex[4]);
+ std::swap(colorHex[1], colorHex[5]);
+
+ std::string colorTag = "{\\c&H" + colorHex + "&}";
+ strUTF8.insert(pos, colorTag);
+ pos += static_cast<int>(colorTag.length());
+ }
+ }
+ }
+ // Parse specific SAMI Tags (all below)
+ else if (langClassID && (StringUtils::StartsWith(fullTag, "<p ")))
+ {
+ int pos2 = 3;
+ while ((pos2 = m_tagOptions->RegFind(fullTag.c_str(), pos2)) >= 0)
+ {
+ std::string tagOptionName = m_tagOptions->GetMatch(1);
+ std::string tagOptionValue = StringUtils::ToLower(m_tagOptions->GetMatch(2));
+ pos2 += static_cast<int>(tagOptionName.length() + tagOptionValue.length());
+ if (tagOptionName == "class")
+ {
+ if (m_flag[FLAG_LANGUAGE])
+ {
+ strUTF8.erase(del_start, pos - del_start);
+ pos = del_start;
+ }
+ if (!tagOptionValue.compare(langClassID))
+ {
+ m_flag[FLAG_LANGUAGE] = false;
+ }
+ else
+ {
+ m_flag[FLAG_LANGUAGE] = true;
+ del_start = pos;
+ }
+ break;
+ }
+ }
+ }
+ else if ((fullTag == "</p>") && m_flag[FLAG_LANGUAGE])
+ {
+ strUTF8.erase(del_start, pos - del_start);
+ pos = del_start;
+ m_flag[FLAG_LANGUAGE] = false;
+ }
+ else if ((fullTag == "\\n") || (StringUtils::StartsWith(fullTag, "<br") && !strUTF8.empty()))
+ {
+ strUTF8.insert(pos, "\n");
+ pos += 1;
+ }
+ // SubRip (.srt) hard space
+ else if (fullTag == "\\h")
+ {
+ // Unicode no-break space
+ strUTF8.insert(pos, "\xC2\xA0");
+ pos += 2;
+ }
+ }
+
+ if (m_flag[FLAG_LANGUAGE])
+ strUTF8.erase(del_start);
+
+ if (strUTF8.empty())
+ return;
+ if (strUTF8 == "&nbsp;") // SAMI specific blank paragraph parameter
+ {
+ strUTF8.clear();
+ return;
+ }
+
+ std::wstring wStrHtml, wStr;
+ g_charsetConverter.utf8ToW(strUTF8, wStrHtml, false);
+ HTML::CHTMLUtil::ConvertHTMLToW(wStrHtml, wStr);
+ g_charsetConverter.wToUTF8(wStr, strUTF8);
+}
+
+void CDVDSubtitleTagSami::CloseTag(std::string& text)
+{
+ if (m_flag[FLAG_BOLD])
+ {
+ m_flag[FLAG_BOLD] = false;
+ text += "{\\b0}";
+ }
+ if (m_flag[FLAG_ITALIC])
+ {
+ m_flag[FLAG_ITALIC] = false;
+ text += "{\\i0}";
+ }
+ if (m_flag[FLAG_UNDERLINE])
+ {
+ m_flag[FLAG_UNDERLINE] = false;
+ text += "{\\u0}";
+ }
+ if (m_flag[FLAG_STRIKETHROUGH])
+ {
+ m_flag[FLAG_STRIKETHROUGH] = false;
+ text += "{\\s0}";
+ }
+ if (m_flag[FLAG_COLOR])
+ {
+ m_flag[FLAG_COLOR] = false;
+ text += "{\\c}";
+ }
+ m_flag[FLAG_LANGUAGE] = false;
+}
+
+void CDVDSubtitleTagSami::LoadHead(CDVDSubtitleStream* samiStream)
+{
+ bool inSTYLE = false;
+ CRegExp reg(true);
+ if (!reg.RegComp("\\.([a-z]+)[ \t]*\\{[ \t]*name:([^;]*?);[ \t]*lang:([^;]*?);[ "
+ "\t]*SAMIType:([^;]*?);[ \t]*\\}"))
+ return;
+
+ std::string line;
+ while (samiStream->ReadLine(line))
+ {
+ StringUtils::Trim(line);
+
+ if (StringUtils::EqualsNoCase(line, "<BODY>"))
+ break;
+ if (inSTYLE)
+ {
+ if (StringUtils::EqualsNoCase(line, "</STYLE>"))
+ break;
+ else
+ {
+ if (reg.RegFind(line.c_str()) > -1)
+ {
+ SLangclass lc;
+ lc.ID = reg.GetMatch(1);
+ lc.Name = reg.GetMatch(2);
+ lc.Lang = reg.GetMatch(3);
+ lc.SAMIType = reg.GetMatch(4);
+ StringUtils::Trim(lc.Name);
+ StringUtils::Trim(lc.Lang);
+ StringUtils::Trim(lc.SAMIType);
+ m_Langclass.push_back(lc);
+ }
+ }
+ }
+ else
+ {
+ if (StringUtils::EqualsNoCase(line, "<STYLE TYPE=\"text/css\">"))
+ inSTYLE = true;
+ }
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.h
new file mode 100644
index 0000000..433fd63
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.h
@@ -0,0 +1,65 @@
+/*
+ * 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 <stdio.h>
+#include <string>
+#include <vector>
+
+#define FLAG_BOLD 0
+#define FLAG_ITALIC 1
+#define FLAG_UNDERLINE 2
+#define FLAG_STRIKETHROUGH 3
+#define FLAG_COLOR 4
+#define FLAG_LANGUAGE 5
+
+class CDVDSubtitleStream;
+class CRegExp;
+
+class CDVDSubtitleTagSami
+{
+public:
+ CDVDSubtitleTagSami()
+ {
+ m_tags = NULL;
+ m_tagOptions = NULL;
+ m_flag[FLAG_BOLD] = false;
+ m_flag[FLAG_ITALIC] = false;
+ m_flag[FLAG_UNDERLINE] = false;
+ m_flag[FLAG_STRIKETHROUGH] = false;
+ m_flag[FLAG_COLOR] = false;
+ m_flag[FLAG_LANGUAGE] = false; //set to true when classID != lang
+ }
+ virtual ~CDVDSubtitleTagSami();
+ bool Init();
+ /*!
+ \brief Convert a subtitle text line.
+ \param line The text line
+ \param len The line length
+ \param langClassID The SAMI Class ID language (keep the language lines with this ID and ignore all others)
+ */
+ void ConvertLine(std::string& strUTF8, const char* langClassID = NULL);
+ void CloseTag(std::string& text);
+ void LoadHead(CDVDSubtitleStream* samiStream);
+
+ typedef struct
+ {
+ std::string ID;
+ std::string Name;
+ std::string Lang;
+ std::string SAMIType;
+ } SLangclass;
+
+ std::vector<SLangclass> m_Langclass;
+
+private:
+ CRegExp* m_tags;
+ CRegExp* m_tagOptions;
+ bool m_flag[6];
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitlesLibass.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitlesLibass.cpp
new file mode 100644
index 0000000..98d9cdf
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitlesLibass.cpp
@@ -0,0 +1,742 @@
+/*
+ * 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 "DVDSubtitlesLibass.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SubtitlesSettings.h"
+#include "utils/FontUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cstring>
+#include <mutex>
+
+using namespace KODI::SUBTITLES::STYLE;
+using namespace UTILS;
+
+namespace
+{
+constexpr int ASS_BORDER_STYLE_OUTLINE = 1; // Outline + drop shadow
+constexpr int ASS_BORDER_STYLE_BOX = 3; // Box + drop shadow
+constexpr int ASS_BORDER_STYLE_SQUARE_BOX = 4; // Square box + outline
+
+// Convert RGB/ARGB to RGBA by also applying the opacity value
+COLOR::Color ConvColor(COLOR::Color argbColor, int opacity = 100)
+{
+ return COLOR::ConvertToRGBA(COLOR::ChangeOpacity(argbColor, (100.0f - opacity) / 100.0f));
+}
+
+} // namespace
+
+static void libass_log(int level, const char* fmt, va_list args, void* data)
+{
+ if (level >= 5)
+ return;
+ std::string log = StringUtils::FormatV(fmt, args);
+ CLog::Log(LOGDEBUG, "CDVDSubtitlesLibass: [ass] {}", log);
+}
+
+CDVDSubtitlesLibass::CDVDSubtitlesLibass()
+{
+ CLog::Log(LOGINFO, "CDVDSubtitlesLibass: Using libass version {0:x}", ass_library_version());
+ CLog::Log(LOGINFO, "CDVDSubtitlesLibass: Creating ASS library structure");
+ m_library = ass_library_init();
+ if (!m_library)
+ return;
+
+ ass_set_message_cb(m_library, libass_log, this);
+
+ CLog::Log(LOGINFO, "CDVDSubtitlesLibass: Initializing ASS Renderer");
+
+ m_renderer = ass_renderer_init(m_library);
+
+ if (!m_renderer)
+ throw std::runtime_error("Libass render failed to initialize");
+}
+
+CDVDSubtitlesLibass::~CDVDSubtitlesLibass()
+{
+ if (m_track)
+ ass_free_track(m_track);
+ ass_renderer_done(m_renderer);
+ ass_library_done(m_library);
+}
+
+void CDVDSubtitlesLibass::Configure()
+{
+ CLog::Log(LOGINFO, "CDVDSubtitlesLibass: Initializing ASS library font settings");
+
+ if (!m_renderer)
+ {
+ CLog::Log(LOGERROR, "CDVDSubtitlesLibass: Failed to initialize ASS font settings. ASS renderer "
+ "not initialized.");
+ return;
+ }
+
+ ass_set_margins(m_renderer, 0, 0, 0, 0);
+ ass_set_use_margins(m_renderer, 0);
+
+ // Libass uses system font provider (like fontconfig) by default in some
+ // platforms (e.g. linux/windows), on some other systems like android the
+ // font provider is currenlty not supported, then an user can add his
+ // additionals fonts only by using the user fonts folder.
+ ass_set_fonts_dir(m_library,
+ CSpecialProtocol::TranslatePath(UTILS::FONT::FONTPATH::USER).c_str());
+
+ // Load additional fonts into Libass memory
+ CFileItemList items;
+ // Get fonts from system directory
+ if (XFILE::CDirectory::Exists(UTILS::FONT::FONTPATH::SYSTEM))
+ {
+ XFILE::CDirectory::GetDirectory(UTILS::FONT::FONTPATH::SYSTEM, items,
+ UTILS::FONT::SUPPORTED_EXTENSIONS_MASK,
+ XFILE::DIR_FLAG_NO_FILE_DIRS | XFILE::DIR_FLAG_NO_FILE_INFO);
+ }
+ // Get temporary fonts
+ if (XFILE::CDirectory::Exists(UTILS::FONT::FONTPATH::TEMP, false))
+ {
+ XFILE::CDirectory::GetDirectory(
+ UTILS::FONT::FONTPATH::TEMP, items, UTILS::FONT::SUPPORTED_EXTENSIONS_MASK,
+ XFILE::DIR_FLAG_BYPASS_CACHE | XFILE::DIR_FLAG_NO_FILE_DIRS | XFILE::DIR_FLAG_NO_FILE_INFO);
+ }
+ for (const auto& item : items)
+ {
+ if (item->m_bIsFolder)
+ continue;
+ const std::string filepath = item->GetPath();
+ const std::string fileName = item->GetLabel();
+ std::vector<uint8_t> buffer;
+ if (XFILE::CFile().LoadFile(filepath, buffer) <= 0)
+ {
+ CLog::LogF(LOGERROR, "Failed to load file {}", filepath);
+ continue;
+ }
+#if LIBASS_VERSION >= 0x01501000
+ ass_add_font(m_library, fileName.c_str(), reinterpret_cast<const char*>(buffer.data()),
+ static_cast<int>(buffer.size()));
+#else
+ ass_add_font(m_library, const_cast<char*>(fileName.c_str()),
+ reinterpret_cast<char*>(buffer.data()), static_cast<int>(buffer.size()));
+#endif
+ if (StringUtils::CompareNoCase(fileName, FONT::FONT_DEFAULT_FILENAME) == 0)
+ {
+ m_defaultFontFamilyName = FONT::GetFontFamily(buffer);
+ }
+ }
+ if (m_defaultFontFamilyName.empty())
+ {
+ CLog::LogF(LOGERROR,
+ "The application font {} is missing. The default subtitle font cannot be set.",
+ FONT::FONT_DEFAULT_FILENAME);
+ }
+
+ ass_set_fonts(m_renderer,
+ UTILS::FONT::FONTPATH::GetSystemFontPath(FONT::FONT_DEFAULT_FILENAME).c_str(),
+ m_defaultFontFamilyName.c_str(), ASS_FONTPROVIDER_AUTODETECT, nullptr, 1);
+
+ // Extract font must be set before loading ASS/SSA data,
+ // after that cannot be changed
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ bool overrideFont = settings->GetBool(CSettings::SETTING_SUBTITLES_OVERRIDEFONTS);
+ ass_set_extract_fonts(m_library, overrideFont ? 0 : 1);
+}
+
+bool CDVDSubtitlesLibass::DecodeHeader(char* data, int size)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!m_library || !data)
+ return false;
+
+ CLog::Log(LOGINFO, "CDVDSubtitlesLibass: Creating new ASS track");
+ m_track = ass_new_track(m_library);
+
+ ass_process_codec_private(m_track, data, size);
+ return true;
+}
+
+bool CDVDSubtitlesLibass::DecodeDemuxPkt(const char* data, int size, double start, double duration)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!m_track)
+ {
+ CLog::Log(LOGERROR, "{} - No SSA header found.", __FUNCTION__);
+ return false;
+ }
+
+ //! @bug libass isn't const correct
+ ass_process_chunk(m_track, const_cast<char*>(data), size, DVD_TIME_TO_MSEC(start),
+ DVD_TIME_TO_MSEC(duration));
+ return true;
+}
+
+bool CDVDSubtitlesLibass::CreateTrack()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!m_library)
+ {
+ CLog::Log(LOGERROR, "{} - Failed to create ASS track, library not initialized.", __FUNCTION__);
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CDVDSubtitlesLibass: Creating new ASS track");
+ m_track = ass_new_track(m_library);
+ if (m_track == NULL)
+ {
+ CLog::Log(LOGERROR, "{} - Failed to allocate ASS track.", __FUNCTION__);
+ return false;
+ }
+
+ m_track->track_type = m_track->TRACK_TYPE_ASS;
+ m_track->Timer = 100.;
+ // Set fixed values to PlayRes to allow the use of style override code for positioning
+ m_track->PlayResX = static_cast<int>(VIEWPORT_WIDTH);
+ m_track->PlayResY = static_cast<int>(VIEWPORT_HEIGHT);
+ m_track->Kerning = true; // Font kerning improves the letterspacing
+ m_track->WrapStyle = 1; // The line feed \n doesn't break but wraps (instead \N breaks)
+
+ return true;
+}
+
+bool CDVDSubtitlesLibass::CreateStyle()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!m_library)
+ {
+ CLog::Log(LOGERROR, "{} - Failed to create ASS style, library not initialized.", __FUNCTION__);
+ return false;
+ }
+
+ if (!m_track)
+ {
+ CLog::Log(LOGERROR, "{} - Failed to create ASS style, track not initialized.", __FUNCTION__);
+ return false;
+ }
+
+ m_defaultKodiStyleId = ass_alloc_style(m_track);
+ return true;
+}
+
+bool CDVDSubtitlesLibass::CreateTrack(char* buf, size_t size)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!m_library)
+ {
+ CLog::Log(LOGERROR, "{} - No ASS library struct (m_library)", __FUNCTION__);
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CDVDSubtitlesLibass: Creating m_track from SSA buffer");
+
+ m_track = ass_read_memory(m_library, buf, size, 0);
+ if (m_track == NULL)
+ return false;
+
+ return true;
+}
+
+ASS_Image* CDVDSubtitlesLibass::RenderImage(double pts,
+ renderOpts opts,
+ bool updateStyle,
+ const std::shared_ptr<struct style>& subStyle,
+ int* changes)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!m_renderer || !m_track)
+ {
+ CLog::Log(LOGERROR, "{} - ASS renderer/ASS track not initialized.", __FUNCTION__);
+ return nullptr;
+ }
+
+ if (!subStyle)
+ {
+ CLog::Log(LOGERROR, "{} - The subtitle overlay style is not set.", __FUNCTION__);
+ return nullptr;
+ }
+
+ if (updateStyle || m_currentDefaultStyleId == ASS_NO_ID)
+ {
+ ApplyStyle(subStyle, opts);
+ }
+
+ // Reversed par value
+ // from: >1 tighter pixels, <1 wider pixels
+ // to: <1 tighter pixels, >1 wider pixels
+ float par = (opts.m_par - 2.0f) * -1;
+ ass_set_pixel_aspect(m_renderer, static_cast<double>(par));
+
+ ass_set_frame_size(m_renderer, static_cast<int>(opts.frameWidth),
+ static_cast<int>(opts.frameHeight));
+
+ bool useFrameMargins;
+
+ if (m_subtitleType == NATIVE)
+ {
+ ass_set_storage_size(m_renderer, static_cast<int>(opts.sourceWidth),
+ static_cast<int>(opts.sourceHeight));
+ useFrameMargins =
+ opts.marginsMode == MarginsMode::DISABLED || opts.marginsMode == MarginsMode::INSIDE_VIDEO;
+ }
+ else
+ {
+ // Keep storage to default to keep consistent subtitles effects
+ // (like borders) when video resolution change while in playback
+ ass_set_storage_size(m_renderer, 0, 0);
+ useFrameMargins = opts.marginsMode == MarginsMode::INSIDE_VIDEO;
+ }
+
+ int marginTop{0};
+ int marginLeft{0};
+ if (useFrameMargins)
+ {
+ marginTop =
+ static_cast<int>((opts.frameHeight - std::min(opts.videoHeight, opts.frameHeight)) / 2);
+ marginLeft =
+ static_cast<int>((opts.frameWidth - std::min(opts.videoWidth, opts.frameWidth)) / 2);
+ }
+
+ ass_set_margins(m_renderer, marginTop, marginTop, marginLeft, marginLeft);
+ ass_set_use_margins(m_renderer, 0);
+
+ float fontScale{1.0f};
+ if (opts.marginsMode == MarginsMode::INSIDE_VIDEO)
+ {
+ // Make font size relative to window size instead of video,
+ // to show same font size even if the video do not cover in full the
+ // window (e.g. cropped videos, zoom effect) and player add black bars.
+ fontScale *= std::max(opts.frameHeight / opts.videoHeight, 1.0f);
+ }
+
+ ass_set_font_scale(m_renderer, static_cast<double>(fontScale));
+
+ ass_set_line_position(m_renderer, opts.position);
+
+ // For posterity ass_render_frame have an inconsistent rendering for overlapped subtitles cases,
+ // if the playback occurs in sequence (without seeks) the overlapped subtitles lines will be rendered in right order
+ // if you seek forward/backward the video, the overlapped subtitles lines could be rendered in the wrong order
+ // this is a known side effect from libass devs and not a bug from our part
+ return ass_render_frame(m_renderer, m_track, DVD_TIME_TO_MSEC(pts), changes);
+}
+
+void CDVDSubtitlesLibass::ApplyStyle(const std::shared_ptr<struct style>& subStyle, renderOpts opts)
+{
+ CLog::Log(LOGDEBUG, "{} - Start setting up the LibAss style", __FUNCTION__);
+
+ if (!subStyle)
+ {
+ CLog::Log(LOGERROR, "{} - The subtitle overlay style is not set.", __FUNCTION__);
+ return;
+ }
+
+ // ASS_Style is a POD struct need to be initialized with {}
+ ASS_Style defaultStyle{};
+ ASS_Style* style = nullptr;
+
+ if (m_subtitleType == ADAPTED ||
+ (m_subtitleType == NATIVE &&
+ (subStyle->assOverrideStyles != OverrideStyles::DISABLED || subStyle->assOverrideFont)))
+ {
+ m_currentDefaultStyleId = m_defaultKodiStyleId;
+
+ if (m_subtitleType == NATIVE)
+ {
+ style = &defaultStyle;
+ }
+ else
+ {
+ style = &m_track->styles[m_currentDefaultStyleId];
+ }
+
+ free(style->Name);
+ style->Name = strdup("KodiDefault");
+
+ // Calculate the scale
+ // Font size, borders, etc... are specified in pixel unit in scaled
+ // for a window height of 720, so we need to rescale to our PlayResY
+ double playResY{static_cast<double>(m_track->PlayResY)};
+ double scaleDefault{playResY / 720};
+ double scale{scaleDefault};
+ if (m_subtitleType == NATIVE &&
+ (subStyle->assOverrideStyles == OverrideStyles::STYLES ||
+ subStyle->assOverrideStyles == OverrideStyles::STYLES_POSITIONS ||
+ subStyle->assOverrideFont))
+ {
+ // With styles overridden the PlayResY will be changed to 288
+ scale = 288.0 / 720;
+ }
+
+ // It is mandatory set the FontName, the text is case sensitive
+ free(style->FontName);
+ if (subStyle->fontName == KODI::SUBTITLES::FONT_DEFAULT_FAMILYNAME)
+ style->FontName = strdup(m_defaultFontFamilyName.c_str());
+ else
+ style->FontName = strdup(subStyle->fontName.c_str());
+
+ // Configure the font properties
+ style->FontSize = subStyle->fontSize * scale;
+
+ // Modifies the width/height of the font (1 = 100%)
+ style->ScaleX = 1.0;
+ style->ScaleY = 1.0;
+ // Extra space between characters causes the underlined
+ // text line to become more discontinuous (test on LibAss 15.1)
+ style->Spacing = 0;
+
+ bool isFontBold =
+ (subStyle->fontStyle == FontStyle::BOLD || subStyle->fontStyle == FontStyle::BOLD_ITALIC);
+ bool isFontItalic =
+ (subStyle->fontStyle == FontStyle::ITALIC || subStyle->fontStyle == FontStyle::BOLD_ITALIC);
+ style->Bold = isFontBold * -1;
+ style->Italic = isFontItalic * -1;
+
+ //! @todo Libass has a problem with color transparencies when set to:
+ //! PrimaryColour/SecondaryColour/OutlineColour by causing a gap between border
+ //! and text color. As workaround the SecondaryColour must have no transparency
+ //! this will fix just use cases without transparencies, for a full fix will be
+ //! needed in future update libass library having the gap fix.
+
+ // Set default subtitles color
+ style->PrimaryColour = ConvColor(subStyle->fontColor, subStyle->fontOpacity);
+ // Set secondary colour for karaoke
+ // left part is filled with PrimaryColour, right one with SecondaryColour
+ //! @bug in libass - force secondary color without transparency otherwise
+ //! cause a visible color gap, we also avoid reusing the same primary
+ //! color otherwise the karaoke effect will not be visible.
+ style->SecondaryColour = ConvColor(COLOR::BLACK);
+
+ // Configure the effects
+ double lineSpacing = 0.0;
+ if (subStyle->borderStyle == BorderType::OUTLINE ||
+ subStyle->borderStyle == BorderType::OUTLINE_NO_SHADOW)
+ {
+ style->BorderStyle = ASS_BORDER_STYLE_OUTLINE;
+ style->Outline = (10.00 / 100 * subStyle->fontBorderSize) * scale;
+ style->OutlineColour = ConvColor(subStyle->fontBorderColor, subStyle->fontOpacity);
+ if (subStyle->borderStyle == BorderType::OUTLINE_NO_SHADOW)
+ {
+ style->BackColour = ConvColor(COLOR::NONE, 0); // Set the shadow color
+ style->Shadow = 0; // Set the shadow size
+ }
+ else
+ {
+ style->BackColour =
+ ConvColor(subStyle->shadowColor, subStyle->shadowOpacity); // Set the shadow color
+ style->Shadow = (10.00 / 100 * subStyle->shadowSize) * scale; // Set the shadow size
+ }
+ }
+ else if (subStyle->borderStyle == BorderType::BOX)
+ {
+ // This BorderStyle not support outline color/size
+ style->BorderStyle = ASS_BORDER_STYLE_BOX;
+ style->Outline = 4 * scale; // Space between the text and the box edges
+ style->OutlineColour =
+ ConvColor(subStyle->backgroundColor,
+ subStyle->backgroundOpacity); // Set the background border color
+ style->BackColour =
+ ConvColor(subStyle->shadowColor, subStyle->shadowOpacity); // Set the box shadow color
+ style->Shadow = (10.00 / 100 * subStyle->shadowSize) * scale; // Set the box shadow size
+ // By default a box overlaps the other, then we increase a bit the line spacing
+ lineSpacing = 8.0 * scaleDefault;
+ }
+ else if (subStyle->borderStyle == BorderType::SQUARE_BOX)
+ {
+ // This BorderStyle not support shadow color/size
+ style->BorderStyle = ASS_BORDER_STYLE_SQUARE_BOX;
+ style->Outline = (10.00 / 100 * subStyle->fontBorderSize) * scale;
+ style->OutlineColour = ConvColor(subStyle->fontBorderColor, subStyle->fontOpacity);
+ style->BackColour = ConvColor(subStyle->backgroundColor, subStyle->backgroundOpacity);
+ style->Shadow = 4 * scale; // Space between the text and the box edges
+ }
+
+ // ass_set_line_spacing do not scale, so we have to scale to frame size
+ ass_set_line_spacing(m_renderer,
+ lineSpacing / playResY * static_cast<double>(opts.frameHeight));
+
+ style->Blur = (10.00 / 100 * subStyle->blur);
+
+ // Set the margins (in pixel)
+ if (opts.marginsMode == MarginsMode::DISABLED)
+ {
+ style->MarginL = 0;
+ style->MarginR = 0;
+ style->MarginV = 0;
+ }
+ else
+ {
+ double marginLR = 20;
+ if (opts.horizontalAlignment != HorizontalAlign::DISABLED)
+ {
+ // If the subtitle text is aligned on the left or right
+ // of the screen, we set an extra left/right margin
+ marginLR += static_cast<double>(opts.frameWidth) / 10;
+ }
+ style->MarginL = static_cast<int>(marginLR * scaleDefault);
+ style->MarginR = static_cast<int>(marginLR * scaleDefault);
+ style->MarginV = static_cast<int>(subStyle->marginVertical * scaleDefault);
+ }
+
+ // Set the vertical alignment
+ if (subStyle->alignment == FontAlign::TOP_LEFT ||
+ subStyle->alignment == FontAlign::TOP_CENTER || subStyle->alignment == FontAlign::TOP_RIGHT)
+ style->Alignment = VALIGN_TOP;
+ else if (subStyle->alignment == FontAlign::MIDDLE_LEFT ||
+ subStyle->alignment == FontAlign::MIDDLE_CENTER ||
+ subStyle->alignment == FontAlign::MIDDLE_RIGHT)
+ style->Alignment = VALIGN_CENTER;
+ else if (subStyle->alignment == FontAlign::SUB_LEFT ||
+ subStyle->alignment == FontAlign::SUB_CENTER ||
+ subStyle->alignment == FontAlign::SUB_RIGHT)
+ style->Alignment = VALIGN_SUB;
+
+ // Set the horizontal alignment, giving priority to horizontalFontAlign property when set
+ if (opts.horizontalAlignment == HorizontalAlign::LEFT)
+ style->Alignment |= HALIGN_LEFT;
+ else if (opts.horizontalAlignment == HorizontalAlign::CENTER)
+ style->Alignment |= HALIGN_CENTER;
+ else if (opts.horizontalAlignment == HorizontalAlign::RIGHT)
+ style->Alignment |= HALIGN_RIGHT;
+ else if (subStyle->alignment == FontAlign::TOP_LEFT ||
+ subStyle->alignment == FontAlign::MIDDLE_LEFT ||
+ subStyle->alignment == FontAlign::SUB_LEFT)
+ style->Alignment |= HALIGN_LEFT;
+ else if (subStyle->alignment == FontAlign::TOP_CENTER ||
+ subStyle->alignment == FontAlign::MIDDLE_CENTER ||
+ subStyle->alignment == FontAlign::SUB_CENTER)
+ style->Alignment |= HALIGN_CENTER;
+ else if (subStyle->alignment == FontAlign::TOP_RIGHT ||
+ subStyle->alignment == FontAlign::MIDDLE_RIGHT ||
+ subStyle->alignment == FontAlign::SUB_RIGHT)
+ style->Alignment |= HALIGN_RIGHT;
+ }
+
+ if (m_subtitleType == NATIVE)
+ {
+ ConfigureAssOverride(subStyle, style);
+ m_currentDefaultStyleId = m_track->default_style;
+ }
+}
+
+int CDVDSubtitlesLibass::GetPlayResY()
+{
+ if (!m_track)
+ {
+ CLog::Log(LOGERROR, "{} - ASS renderer/ASS track not initialized.", __FUNCTION__);
+ return VIEWPORT_HEIGHT;
+ }
+ return m_track->PlayResY;
+}
+
+void CDVDSubtitlesLibass::ConfigureAssOverride(const std::shared_ptr<struct style>& subStyle,
+ ASS_Style* style)
+{
+ if (!subStyle)
+ {
+ CLog::Log(LOGERROR, "{} - The subtitle overlay style is not set.", __FUNCTION__);
+ return;
+ }
+
+ // Default behaviour, disable ASS embedded styles override (if has been changed)
+ int stylesFlags{ASS_OVERRIDE_DEFAULT};
+ if (style)
+ {
+ // Manage override cases with ASS embedded styles
+ if (subStyle->assOverrideStyles == OverrideStyles::STYLES)
+ {
+ stylesFlags = ASS_OVERRIDE_BIT_COLORS | ASS_OVERRIDE_BIT_ATTRIBUTES |
+ ASS_OVERRIDE_BIT_BORDER | ASS_OVERRIDE_BIT_MARGINS;
+ }
+ else if (subStyle->assOverrideStyles == OverrideStyles::STYLES_POSITIONS)
+ {
+ stylesFlags = ASS_OVERRIDE_BIT_COLORS | ASS_OVERRIDE_BIT_ATTRIBUTES |
+ ASS_OVERRIDE_BIT_BORDER | ASS_OVERRIDE_BIT_MARGINS | ASS_OVERRIDE_BIT_ALIGNMENT;
+ }
+ else if (subStyle->assOverrideStyles == OverrideStyles::POSITIONS)
+ {
+ stylesFlags = ASS_OVERRIDE_BIT_ALIGNMENT | ASS_OVERRIDE_BIT_MARGINS;
+ }
+ if (subStyle->assOverrideFont)
+ {
+ stylesFlags |= ASS_OVERRIDE_BIT_FONT_SIZE_FIELDS | ASS_OVERRIDE_BIT_FONT_NAME;
+ }
+ ass_set_selective_style_override(m_renderer, style);
+ }
+
+ ass_set_selective_style_override_enabled(m_renderer, stylesFlags);
+}
+
+ASS_Event* CDVDSubtitlesLibass::GetEvents()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!m_track)
+ {
+ CLog::Log(LOGERROR, "{} - Missing ASS structs (m_track)", __FUNCTION__);
+ return NULL;
+ }
+ return m_track->events;
+}
+
+int CDVDSubtitlesLibass::GetNrOfEvents() const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!m_track)
+ return 0;
+ return m_track->n_events;
+}
+
+int CDVDSubtitlesLibass::AddEvent(const char* text, double startTime, double stopTime)
+{
+ return AddEvent(text, startTime, stopTime, nullptr);
+}
+
+int CDVDSubtitlesLibass::AddEvent(const char* text,
+ double startTime,
+ double stopTime,
+ subtitleOpts* opts)
+{
+ if (text == NULL || text[0] == '\0')
+ {
+ CLog::Log(LOGDEBUG,
+ "{} - Add event skipped due to empty text (with start time: {}, stop time {})",
+ __FUNCTION__, startTime, stopTime);
+ return ASS_NO_ID;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!m_library || !m_track)
+ {
+ CLog::Log(LOGERROR, "{} - Missing ASS structs (m_library or m_track)", __FUNCTION__);
+ return ASS_NO_ID;
+ }
+
+ int eventId = ass_alloc_event(m_track);
+ if (eventId >= 0)
+ {
+ ASS_Event* event = m_track->events + eventId;
+ event->Start = DVD_TIME_TO_MSEC(startTime);
+ event->Duration = DVD_TIME_TO_MSEC(stopTime - startTime);
+ event->Style = m_defaultKodiStyleId;
+ event->ReadOrder = eventId;
+ event->Text = strdup(text);
+ if (opts && opts->useMargins)
+ {
+ event->MarginL = opts->marginLeft;
+ event->MarginR = opts->marginRight;
+ event->MarginV = opts->marginVertical;
+ }
+ return eventId;
+ }
+ else
+ CLog::Log(LOGERROR, "{} - Cannot allocate a new event", __FUNCTION__);
+ return ASS_NO_ID;
+}
+
+void CDVDSubtitlesLibass::AppendTextToEvent(int eventId, const char* text)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (eventId == ASS_NO_ID || text == NULL || text[0] == '\0')
+ return;
+ if (!m_track)
+ {
+ CLog::Log(LOGERROR, "{} - Missing ASS structs (m_track)", __FUNCTION__);
+ return;
+ }
+
+ ASS_Event* assEvents = m_track->events;
+ if (!assEvents)
+ {
+ CLog::Log(LOGERROR, "{} - Failed append text to Event ID {}, there are no Events",
+ __FUNCTION__, eventId);
+ return;
+ }
+
+ ASS_Event* assEvent = (assEvents + eventId);
+ if (assEvent)
+ {
+ size_t buffSize = strlen(assEvent->Text) + strlen(text) + 1;
+ char* appendedText = new char[buffSize];
+ strcpy(appendedText, assEvent->Text);
+ strcat(appendedText, text);
+ free(assEvent->Text);
+ assEvent->Text = strdup(appendedText);
+ delete[] appendedText;
+ }
+}
+
+void CDVDSubtitlesLibass::ChangeEventStopTime(int eventId, double stopTime)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (eventId == ASS_NO_ID)
+ return;
+ if (!m_track)
+ {
+ CLog::Log(LOGERROR, "{} - Missing ASS structs (m_track)", __FUNCTION__);
+ return;
+ }
+
+ ASS_Event* assEvents = m_track->events;
+ if (!assEvents)
+ {
+ CLog::Log(LOGERROR, "{} - Failed change stop time to Event ID {}, there are no Events",
+ __FUNCTION__, eventId);
+ return;
+ }
+
+ ASS_Event* assEvent = (assEvents + eventId);
+ if (assEvent)
+ assEvent->Duration = (DVD_TIME_TO_MSEC(stopTime) - assEvent->Start);
+}
+
+void CDVDSubtitlesLibass::FlushEvents()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!m_library || !m_track)
+ {
+ CLog::Log(LOGERROR, "{} - Missing ASS structs (m_library or m_track)", __FUNCTION__);
+ return;
+ }
+
+ ass_flush_events(m_track);
+}
+
+int CDVDSubtitlesLibass::DeleteEvents(int nEvents, int threshold)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!m_library || !m_track)
+ {
+ CLog::Log(LOGERROR, "{} - Missing ASS structs (m_library or m_track)", __FUNCTION__);
+ return ASS_NO_ID;
+ }
+
+ if (m_track->n_events == 0)
+ return ASS_NO_ID;
+ if (m_track->n_events < (threshold - nEvents))
+ return m_track->n_events - 1;
+
+ // Currently LibAss do not have delete event method we have to free the events
+ // and reassign all events starting with the first empty position
+ int n = 0;
+ for (; n < nEvents; n++)
+ {
+ ass_free_event(m_track, n);
+ m_track->n_events--;
+ }
+ for (int i = 0; n > 0 && i < threshold; i++)
+ {
+ m_track->events[i] = m_track->events[i + n];
+ }
+ return m_track->n_events - 1;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitlesLibass.h b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitlesLibass.h
new file mode 100644
index 0000000..67ae044
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/DVDSubtitlesLibass.h
@@ -0,0 +1,169 @@
+/*
+ * 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 "SubtitlesStyle.h"
+#include "threads/CriticalSection.h"
+#include "utils/ColorUtils.h"
+
+#include <memory>
+
+#include <ass/ass.h>
+#include <ass/ass_types.h>
+
+/** Wrapper for Libass **/
+
+static constexpr int ASS_NO_ID = -1;
+
+enum ASSSubType
+{
+ NATIVE = 0,
+ ADAPTED
+};
+
+class CDVDSubtitlesLibass
+{
+public:
+ CDVDSubtitlesLibass();
+ ~CDVDSubtitlesLibass();
+
+ /*!
+ * \brief Configure libass. This method groups any configurations
+ * that might change throughout the lifecycle of libass (e.g. fonts)
+ */
+ void Configure();
+
+ ASS_Image* RenderImage(double pts,
+ KODI::SUBTITLES::STYLE::renderOpts opts,
+ bool updateStyle,
+ const std::shared_ptr<struct KODI::SUBTITLES::STYLE::style>& subStyle,
+ int* changes = NULL);
+
+ ASS_Event* GetEvents();
+
+ /*!
+ * \brief Get the number of events (subtitle entries) in the ASS track
+ * \return The number of events in the ASS track
+ */
+ int GetNrOfEvents() const;
+
+ /*!
+ * \brief Decode Header of ASS/SSA, needed to properly decode
+ * demux packets with DecodeDemuxPkt
+ * \return True if success, false if error
+ */
+ bool DecodeHeader(char* data, int size);
+
+ /*!
+ * \brief Decode ASS/SSA demux packet (depends from DecodeHeader)
+ * \return True if success, false if error
+ */
+ bool DecodeDemuxPkt(const char* data, int size, double start, double duration);
+
+ /*!
+ * \brief Create a new ASS track based on an SSA buffer
+ * \return True if success, false if error
+ */
+ bool CreateTrack(char* buf, size_t size);
+
+ /*!
+ * \brief Flush buffered events
+ */
+ void FlushEvents();
+
+ /*!
+ * \brief Get PlayResY value
+ * \return The PlayResY value of current track
+ */
+ int GetPlayResY();
+
+protected:
+ /*!
+ * \brief Create a new empty ASS track
+ * \return True if success, false if error
+ */
+ bool CreateTrack();
+
+ /*!
+ * \brief Create a new empty ASS style
+ * \return True if success, false if error
+ */
+ bool CreateStyle();
+
+ /*!
+ * \brief Specify whether the subtitles are
+ * native (loaded from ASS/SSA file or stream)
+ * or adapted (converted from other types e.g. SubRip)
+ */
+ void SetSubtitleType(ASSSubType type) { m_subtitleType = type; }
+
+ /*!
+ * \brief Add an ASS event to show a subtitle on a specified time
+ * \param text The subtitle text
+ * \param startTime The PTS start time of the Event
+ * \param stopTime The PTS stop time of the Event
+ * \return Return the Event ID, otherwise ASS_NO_ID if fails
+ */
+ int AddEvent(const char* text, double startTime, double stopTime);
+
+ /*!
+ * \brief Add an ASS event to show a subtitle on a specified time
+ * \param text The subtitle text
+ * \param startTime The PTS start time of the Event
+ * \param stopTime The PTS stop time of the Event
+ * \param opts Subtitle options
+ * \return Return the Event ID, otherwise ASS_NO_ID if fails
+ */
+ int AddEvent(const char* text,
+ double startTime,
+ double stopTime,
+ KODI::SUBTITLES::STYLE::subtitleOpts* opts);
+
+ /*!
+ * \brief Append text to the specified event
+ */
+ void AppendTextToEvent(int eventId, const char* text);
+
+ /*!
+ * \brief Delete old events only if the total number of events reaches the threshold
+ * \param nEvents The number of events to delete
+ * \param threshold Start deleting only when the number of events is reached
+ * \return The updated ID of the last Event, otherwise ASS_NO_ID if error or no events
+ */
+ int DeleteEvents(int nEvents, int threshold);
+
+ /*!
+ * \brief Change the stop time of an Event with the specified time
+ * \param eventId The ASS Event ID
+ * \param stopTime The PTS stop time
+ */
+ void ChangeEventStopTime(int eventId, double stopTime);
+
+ friend class CSubtitlesAdapter;
+
+
+private:
+ void ConfigureAssOverride(const std::shared_ptr<struct KODI::SUBTITLES::STYLE::style>& subStyle,
+ ASS_Style* style);
+ void ApplyStyle(const std::shared_ptr<struct KODI::SUBTITLES::STYLE::style>& subStyle,
+ KODI::SUBTITLES::STYLE::renderOpts opts);
+
+ ASS_Library* m_library = nullptr;
+ ASS_Track* m_track = nullptr;
+ ASS_Renderer* m_renderer = nullptr;
+ mutable CCriticalSection m_section;
+ ASSSubType m_subtitleType{NATIVE};
+
+ // current default style ID of the ASS track
+ int m_currentDefaultStyleId{ASS_NO_ID};
+
+ // default allocated style ID for the kodi user configured subtitle style
+ int m_defaultKodiStyleId{ASS_NO_ID};
+ std::string m_defaultFontFamilyName;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitleParserWebVTT.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitleParserWebVTT.cpp
new file mode 100644
index 0000000..8481240
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitleParserWebVTT.cpp
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2005-2021 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 "SubtitleParserWebVTT.h"
+
+#include "DVDCodecs/Overlay/DVDOverlay.h"
+#include "SubtitlesStyle.h"
+#include "cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTHandler.h"
+#include "utils/CharArrayParser.h"
+#include "utils/StringUtils.h"
+
+#include <vector>
+
+using namespace KODI;
+
+CSubtitleParserWebVTT::CSubtitleParserWebVTT(std::unique_ptr<CDVDSubtitleStream>&& pStream,
+ const std::string& strFile)
+ : CDVDSubtitleParserText(std::move(pStream), strFile, "WebVTT Subtitle Parser")
+{
+}
+
+CSubtitleParserWebVTT::~CSubtitleParserWebVTT()
+{
+ Dispose();
+}
+
+bool CSubtitleParserWebVTT::Open(CDVDStreamInfo& hints)
+{
+ if (!CDVDSubtitleParserText::Open())
+ return false;
+
+ if (!Initialize())
+ return false;
+
+ CWebVTTHandler m_webvttHandler;
+
+ if (!m_webvttHandler.Initialize())
+ return false;
+
+ // Get the first chars to check WebVTT signature
+ if (!m_webvttHandler.CheckSignature(m_pStream->Read(10)))
+ return false;
+ m_pStream->Seek(0);
+
+ // Start decoding all lines
+ std::vector<subtitleData> subtitleList;
+ std::string line;
+
+ while (m_pStream->ReadLine(line))
+ {
+ m_webvttHandler.DecodeLine(line, &subtitleList);
+ }
+
+ // We send an empty line to mark the end of the last Cue
+ m_webvttHandler.DecodeLine("", &subtitleList);
+
+ // Send decoded lines to the renderer
+ for (auto& subData : subtitleList)
+ {
+ SUBTITLES::STYLE::subtitleOpts opts;
+ opts.useMargins = subData.useMargins;
+ opts.marginLeft = subData.marginLeft;
+ opts.marginRight = subData.marginRight;
+ opts.marginVertical = subData.marginVertical;
+
+ AddSubtitle(subData.text, subData.startTime, subData.stopTime, &opts);
+ }
+
+ CDVDOverlay* overlay = CreateOverlay();
+ overlay->SetForcedMargins(m_webvttHandler.IsForcedMargins());
+ m_collection.Add(overlay);
+
+ return true;
+}
+
+void CSubtitleParserWebVTT::Dispose()
+{
+ CDVDSubtitleParserCollection::Dispose();
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitleParserWebVTT.h b/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitleParserWebVTT.h
new file mode 100644
index 0000000..8bc52cf
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitleParserWebVTT.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2005-2021 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 "DVDSubtitleParser.h"
+#include "SubtitlesAdapter.h"
+
+#include <memory>
+
+class CSubtitleParserWebVTT : public CDVDSubtitleParserText, public CSubtitlesAdapter
+{
+public:
+ CSubtitleParserWebVTT(std::unique_ptr<CDVDSubtitleStream>&& pStream, const std::string& strFile);
+ ~CSubtitleParserWebVTT() override;
+
+ bool Open(CDVDStreamInfo& hints) override;
+ void Dispose() override;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesAdapter.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesAdapter.cpp
new file mode 100644
index 0000000..6ceee88
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesAdapter.cpp
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2005-2021 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 "SubtitlesAdapter.h"
+
+#include "DVDCodecs/Overlay/DVDOverlay.h"
+#include "DVDCodecs/Overlay/DVDOverlayText.h"
+#include "DVDSubtitlesLibass.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+
+#include <memory>
+
+CSubtitlesAdapter::CSubtitlesAdapter() : m_libass(std::make_shared<CDVDSubtitlesLibass>())
+{
+ m_libass->Configure();
+}
+
+CSubtitlesAdapter::~CSubtitlesAdapter()
+{
+}
+
+bool CSubtitlesAdapter::Initialize()
+{
+ m_libass->SetSubtitleType(ADAPTED);
+ return m_libass->CreateTrack() && m_libass->CreateStyle();
+}
+
+int CSubtitlesAdapter::AddSubtitle(std::string& text, double startTime, double stopTime)
+{
+ return AddSubtitle(text, startTime, stopTime, nullptr);
+}
+
+int CSubtitlesAdapter::AddSubtitle(std::string& text,
+ double startTime,
+ double stopTime,
+ KODI::SUBTITLES::STYLE::subtitleOpts* opts)
+{
+ PostProcess(text);
+ int ret = m_libass->AddEvent(text.c_str(), startTime, stopTime, opts);
+ if (ret == ASS_NO_ID)
+ return NO_SUBTITLE_ID;
+ return ret;
+}
+
+void CSubtitlesAdapter::AppendToSubtitle(int subtitleId, const char* text)
+{
+ if (subtitleId == NO_SUBTITLE_ID)
+ subtitleId = ASS_NO_ID;
+ return m_libass->AppendTextToEvent(subtitleId, text);
+}
+
+int CSubtitlesAdapter::DeleteSubtitles(int nSubtitles, int threshold)
+{
+ int ret = m_libass->DeleteEvents(nSubtitles, threshold);
+ if (ret == ASS_NO_ID)
+ return NO_SUBTITLE_ID;
+ return ret;
+}
+
+void CSubtitlesAdapter::ChangeSubtitleStopTime(int subtitleId, double stopTime)
+{
+ if (subtitleId == NO_SUBTITLE_ID)
+ subtitleId = ASS_NO_ID;
+ return m_libass->ChangeEventStopTime(subtitleId, stopTime);
+}
+
+void CSubtitlesAdapter::FlushSubtitles()
+{
+ // Flush events to avoid display duplicates events e.g. on video seek
+ m_libass->FlushEvents();
+}
+
+CDVDOverlay* CSubtitlesAdapter::CreateOverlay()
+{
+ // Warning with Libass the overlay does not contain image or text then is not tied to Libass Events
+ // any variation of the overlay Start/Stop PTS time will cause problems with the rendering.
+ // The better thing is create a single overlay without PTS stop time to avoid side effects,
+ // maybe this situation could be improved in the future.
+
+ // Side effects that happens when you create each overlay based on each Libass Event:
+ // - A small delay when switching on/off the overlay renderer, cause subtitles
+ // flickering when Events have close timing. A possible cause of the
+ // delay could be the async implementation of overlay management chain.
+ // - When an overlay disable the renderer, Libass has no possibility to
+ // complete the rendering of text animations (not sure if related to previous problem)
+ // with the result of displaying broken animations on the screen.
+ CDVDOverlayText* overlay = new CDVDOverlayText(m_libass);
+ overlay->iPTSStartTime = 0.0;
+ overlay->iPTSStopTime = DVD_NOPTS_VALUE;
+ return overlay;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesAdapter.h b/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesAdapter.h
new file mode 100644
index 0000000..14b2b89
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesAdapter.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2005-2021 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 "SubtitlesStyle.h"
+
+#include <memory>
+#include <string>
+
+class CDVDOverlay;
+class CDVDSubtitlesLibass;
+
+static constexpr int NO_SUBTITLE_ID = -1;
+
+class CSubtitlesAdapter
+{
+public:
+ CSubtitlesAdapter();
+ virtual ~CSubtitlesAdapter();
+
+ /*!
+ * \brief Initialize the subtitles adapter
+ * \return True if success, false if error
+ */
+ bool Initialize();
+
+ /*!
+ * \brief Add a subtitle
+ * \param text The subtitle text
+ * \param startTime The PTS start time of the subtitle
+ * \param stopTime The PTS stop time of the subtitle
+ * \return Return the subtitle ID, otherwise NO_SUBTITLE_ID if fails
+ */
+ int AddSubtitle(std::string& text, double startTime, double stopTime);
+
+ /*!
+ * \brief Add a subtitle with supplementary options
+ * \param text The subtitle text
+ * \param startTime The PTS start time of the subtitle
+ * \param stopTime The PTS stop time of the subtitle
+ * \param opts Subtitle options
+ * \return Return the subtitle ID, otherwise NO_SUBTITLE_ID if fails
+ */
+ int AddSubtitle(std::string& text,
+ double startTime,
+ double stopTime,
+ KODI::SUBTITLES::STYLE::subtitleOpts* opts);
+
+ /*!
+ * \brief Append text to the specified subtitle ID
+ * \param subtitleId The subtitle ID
+ * \param text The text to append
+ */
+ void AppendToSubtitle(int subtitleId, const char* text);
+
+ /*!
+ * \brief Delete old subtitles only if the total number of subtitles added reaches the threshold
+ * \param nSubtitles The number of subtitles to delete
+ * \param threshold Start deleting only when the number of subtitles is reached
+ * \return The updated ID of the last subtitle, otherwise NO_SUBTITLE_ID if error or no subtitles
+ */
+ int DeleteSubtitles(int nSubtitles, int threshold);
+
+ /*!
+ * \brief Change the stop time of a subtitle ID with the specified time
+ * \param subtitleId The subtitle ID
+ * \param stopTime The PTS stop time
+ */
+ void ChangeSubtitleStopTime(int subtitleId, double stopTime);
+
+ void FlushSubtitles();
+
+ CDVDOverlay* CreateOverlay();
+
+protected:
+ /*!
+ * \brief Post processing of subtitle, will be called before processing
+ * AddSubtitle method
+ * \param text The subtitle text
+ */
+ virtual void PostProcess(std::string& text){};
+
+private:
+ std::shared_ptr<CDVDSubtitlesLibass> m_libass;
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesStyle.h b/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesStyle.h
new file mode 100644
index 0000000..f276012
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/SubtitlesStyle.h
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2005-2021 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 "utils/ColorUtils.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace SUBTITLES
+{
+namespace STYLE
+{
+
+constexpr double VIEWPORT_HEIGHT = 1080.0;
+constexpr double VIEWPORT_WIDTH = 1920.0;
+constexpr int MARGIN_VERTICAL = 75;
+
+enum class HorizontalAlign
+{
+ DISABLED = 0,
+ LEFT,
+ CENTER,
+ RIGHT
+};
+
+enum class FontAlign
+{
+ TOP_LEFT = 0,
+ TOP_CENTER,
+ TOP_RIGHT,
+ MIDDLE_LEFT,
+ MIDDLE_CENTER,
+ MIDDLE_RIGHT,
+ SUB_LEFT,
+ SUB_CENTER,
+ SUB_RIGHT
+};
+
+enum class FontStyle
+{
+ NORMAL = 0,
+ BOLD,
+ ITALIC,
+ BOLD_ITALIC
+};
+
+enum class BorderType
+{
+ OUTLINE_NO_SHADOW,
+ OUTLINE,
+ BOX,
+ SQUARE_BOX
+};
+
+enum class OverrideStyles
+{
+ DISABLED = 0,
+ POSITIONS,
+ STYLES,
+ STYLES_POSITIONS
+};
+
+enum class MarginsMode
+{
+ // Use style margins only
+ DEFAULT,
+ // Apply margins to position text within the video area (cropped videos)
+ INSIDE_VIDEO,
+ // Disable any kind of margin
+ DISABLED
+};
+
+struct style
+{
+ std::string fontName; // Font family name
+ double fontSize; // Font size in pixel
+ FontStyle fontStyle = FontStyle::NORMAL;
+ UTILS::COLOR::Color fontColor = UTILS::COLOR::WHITE;
+ int fontBorderSize = 15; // In %
+ UTILS::COLOR::Color fontBorderColor = UTILS::COLOR::BLACK;
+ int fontOpacity = 100; // In %
+ BorderType borderStyle = BorderType::OUTLINE;
+ UTILS::COLOR::Color backgroundColor = UTILS::COLOR::BLACK;
+ int backgroundOpacity = 0; // In %
+ int shadowSize = 0; // In %
+ UTILS::COLOR::Color shadowColor = UTILS::COLOR::BLACK;
+ int shadowOpacity = 100; // In %
+ FontAlign alignment = FontAlign::TOP_LEFT;
+ // Override styles to native ASS/SSA format type only
+ OverrideStyles assOverrideStyles = OverrideStyles::DISABLED;
+ // Override fonts to native ASS/SSA format type only
+ bool assOverrideFont = false;
+ // Vertical margin value in pixels scaled for VIEWPORT_HEIGHT
+ int marginVertical = MARGIN_VERTICAL;
+ int blur = 0; // In %
+};
+
+struct subtitleOpts
+{
+ bool useMargins = false;
+ int marginLeft;
+ int marginRight;
+ int marginVertical;
+};
+
+struct renderOpts
+{
+ float frameWidth;
+ float frameHeight;
+ // Video size width, may be influenced by video settings (e.g. zoom)
+ float videoWidth;
+ // Video size height, may be influenced by video settings (e.g. zoom)
+ float videoHeight;
+ float sourceWidth;
+ float sourceHeight;
+ float m_par; // Set the pixel aspect ratio
+ MarginsMode marginsMode = MarginsMode::DEFAULT;
+ // Vertical line position of subtitles in percentage
+ // only for bottom alignment, 0 = bottom (no change), 100 = on top
+ double position = 0;
+ HorizontalAlign horizontalAlignment = HorizontalAlign::DISABLED;
+};
+
+} // namespace STYLE
+} // namespace SUBTITLES
+} // namespace KODI
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/CMakeLists.txt b/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/CMakeLists.txt
new file mode 100644
index 0000000..0d91326
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES WebVTTHandler.cpp
+ WebVTTISOHandler.cpp)
+
+set(HEADERS WebVTTHandler.h
+ WebVTTISOHandler.h)
+
+core_add_library(subtitles_webvtt)
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTHandler.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTHandler.cpp
new file mode 100644
index 0000000..5591550
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTHandler.cpp
@@ -0,0 +1,1166 @@
+/*
+ * Copyright (C) 2005-2021 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 "WebVTTHandler.h"
+
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/DVDSubtitles/SubtitlesStyle.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "filesystem/SpecialProtocol.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SubtitlesSettings.h"
+#include "utils/CSSUtils.h"
+#include "utils/CharsetConverter.h"
+#include "utils/HTMLUtil.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cstring>
+
+// This code follow W3C standard https://www.w3.org/TR/webvtt1/
+// Due to some Libass rendering limits some feature are not fully implemented
+// all these have been documented in the code (like CSS parsing)
+// Other special cases are not supported:
+// - Cue region (text will be displayed at the bottom)
+// - Cue "line" setting as number value
+// - Vertical text (used for some specific asian languages only)
+
+using namespace KODI::SUBTITLES::STYLE;
+
+namespace
+{
+// WebVTT signature
+constexpr const char* signatureCharsBOM = "\xEF\xBB\xBF\x57\x45\x42\x56\x54\x54";
+constexpr const char* signatureChars = "\x57\x45\x42\x56\x54\x54";
+constexpr char signatureLastChars[] = {'\x0A', '\x0D', '\x20', '\x09'};
+
+constexpr char tagPattern[] = "<(\\/)?([^a-zA-Z >]+)?([^\\d:. >]+)?(\\.[^ >]+)?(?> ([^>]+))?>";
+
+constexpr char cueTimePattern[] = "^(?>(\\d{2,}):)?(\\d{2}):(\\d{2}\\.\\d{3})"
+ "[ \\t]*-->[ \\t]*"
+ "(?>(\\d{2,}):)?(\\d{2}):(\\d{2}\\.\\d{3})";
+
+constexpr char timePattern[] = "<(?>(\\d{2,}):)?(\\d{2}):(\\d{2}\\.\\d{3})>";
+
+// Regex patterns for cue properties
+const std::map<std::string, std::string> cuePropsPatterns = {
+ {"position", "position\\:(\\d+|\\d+\\.\\d+|auto)%"},
+ {"positionAlign", "position\\:\\d+\\.\\d+%,([a-z]+)"},
+ {"size", "size\\:((\\d+\\.)?\\d+%?)"},
+ {"line", "line\\:(\\d+%|\\d+\\.\\d+%|-?\\d+|auto)(,|\\s|$)"},
+ {"align", "align\\:([a-z]+)"},
+ {"vertical", "vertical\\:(rl|lr)"},
+ {"snapToLines", "snapToLines\\:(true|false)"}};
+
+constexpr char cueCssTagPattern[] = "::cue\\(([^\\(]+)\\)|(?>(::cue)\\(?\\)?) *{";
+
+const std::map<std::string, std::string> cueCssPatterns = {
+ {"colorName", "color:\\s*([a-zA-Z]+)($|;|\\s|})"},
+ {"colorRGB", "color:\\s?rgba?\\((\\d{1,3},\\d{1,3},\\d{1,3})(,\\d{1,3})?\\)"},
+ {"fontStyle", "font-style:\\s*(italic)($|;|\\s|})"},
+ {"fontWeight", "font-weight:\\s*(bold)($|;|\\s|})"},
+ {"textDecoration", "text-decoration:\\s*(underline)($|;|\\s|})"}};
+
+const std::map<std::string, webvttCssStyle> cueCssDefaultColorClasses = {
+ {".white", {WebvttSelector::CLASS, ".white", "FFFFFF"}},
+ {".lime", {WebvttSelector::CLASS, ".lime", "00FF00"}},
+ {".cyan", {WebvttSelector::CLASS, ".cyan", "00FFFF"}},
+ {".red", {WebvttSelector::CLASS, ".red", "FF0000"}},
+ {".yellow", {WebvttSelector::CLASS, ".yellow", "FFFF00"}},
+ {".magenta", {WebvttSelector::CLASS, ".magenta", "FF00FF"}},
+ {".blue", {WebvttSelector::CLASS, ".blue", "0000FF"}},
+ {".black", {WebvttSelector::CLASS, ".black", "000000"}}};
+
+enum FlagTags
+{
+ FLAG_TAG_BOLD = 0,
+ FLAG_TAG_ITALIC,
+ FLAG_TAG_UNDERLINE,
+ FLAG_TAG_CLASS,
+ FLAG_TAG_COLOR,
+ FLAG_TAG_COUNT
+};
+
+bool ValidateSignature(const std::string& data, const char* signature)
+{
+ const size_t signatureLen = std::strlen(signature);
+ if (data.size() > signatureLen)
+ {
+ if (data.compare(0, signatureLen, signature) == 0)
+ {
+ // Check if last char is valid
+ if (std::strchr(signatureLastChars, data[signatureLen]) != nullptr)
+ return true;
+ }
+ }
+ return false;
+}
+
+void InsertTextPos(std::string& text, const std::string& insert, int& pos)
+{
+ text.insert(pos, insert);
+ pos += static_cast<int>(insert.length());
+}
+
+std::string ConvertStyleToOpenTags(int flagTags[], webvttCssStyle& style)
+{
+ std::string tags;
+ if (style.m_isFontBold)
+ {
+ if (flagTags[FLAG_TAG_BOLD] == 0)
+ tags += "{\\b1}";
+ flagTags[FLAG_TAG_BOLD] += 1;
+ }
+ if (style.m_isFontItalic)
+ {
+ if (flagTags[FLAG_TAG_ITALIC] == 0)
+ tags += "{\\i1}";
+ flagTags[FLAG_TAG_ITALIC] += 1;
+ }
+ if (style.m_isFontUnderline)
+ {
+ if (flagTags[FLAG_TAG_UNDERLINE] == 0)
+ tags += "{\\u1}";
+ flagTags[FLAG_TAG_UNDERLINE] += 1;
+ }
+ if (!style.m_color.empty())
+ {
+ if (flagTags[FLAG_TAG_COLOR] > 0)
+ tags += "{\\c}";
+ flagTags[FLAG_TAG_COLOR] += 1;
+ tags += "{\\c&H" + style.m_color + "&}";
+ }
+ return tags;
+}
+
+std::string ConvertStyleToCloseTags(int flagTags[],
+ webvttCssStyle* style,
+ webvttCssStyle* baseStyle)
+{
+ std::string tags;
+ if (style->m_isFontBold)
+ {
+ flagTags[FLAG_TAG_BOLD] = flagTags[FLAG_TAG_BOLD] > 0 ? (flagTags[FLAG_TAG_BOLD] - 1) : 0;
+ if (flagTags[FLAG_TAG_BOLD] == 0)
+ tags += "{\\b0}";
+ }
+ if (style->m_isFontItalic)
+ {
+ flagTags[FLAG_TAG_ITALIC] = flagTags[FLAG_TAG_ITALIC] > 0 ? (flagTags[FLAG_TAG_ITALIC] - 1) : 0;
+ if (flagTags[FLAG_TAG_ITALIC] == 0)
+ tags += "{\\i0}";
+ }
+ if (style->m_isFontUnderline)
+ {
+ flagTags[FLAG_TAG_UNDERLINE] =
+ flagTags[FLAG_TAG_UNDERLINE] > 0 ? (flagTags[FLAG_TAG_UNDERLINE] - 1) : 0;
+ if (flagTags[FLAG_TAG_UNDERLINE] == 0)
+ tags += "{\\u0}";
+ }
+ if (!style->m_color.empty())
+ {
+ flagTags[FLAG_TAG_COLOR] = flagTags[FLAG_TAG_COLOR] > 0 ? (flagTags[FLAG_TAG_COLOR] - 1) : 0;
+ tags += "{\\c}";
+ if (flagTags[FLAG_TAG_COLOR] > 0 && !baseStyle->m_color.empty())
+ tags += "{\\c&H" + baseStyle->m_color + "&}";
+ }
+ return tags;
+}
+
+void TranslateEscapeChars(std::string& text)
+{
+ if (text.find('&') != std::string::npos)
+ {
+ // The specs says to use unicode
+ // U+200E for "&lrm;" and U+200F for "&rlm;"
+ // but libass rendering assume the text as left-to-right,
+ // to display text in the right order we have to use embedded codes
+ StringUtils::Replace(text, "&lrm;", u8"\u202a");
+ StringUtils::Replace(text, "&rlm;", u8"\u202b");
+ StringUtils::Replace(text, "&#x2068;", u8"\u2068");
+ StringUtils::Replace(text, "&#x2069;", u8"\u2069");
+ StringUtils::Replace(text, "&amp;", "&");
+ StringUtils::Replace(text, "&lt;", "<");
+ StringUtils::Replace(text, "&gt;", ">");
+ StringUtils::Replace(text, "&nbsp;", " ");
+ }
+}
+
+constexpr int MICROS_PER_SECOND = 1000000;
+constexpr int MPEG_TIMESCALE = 90000;
+
+} // unnamed namespace
+
+bool CWebVTTHandler::Initialize()
+{
+ m_currentSection = WebvttSection::UNDEFINED;
+
+ // Load default CSS Style values
+ AddDefaultCssClasses();
+
+ // Compile regex patterns
+ if (!m_tagsRegex.RegComp(tagPattern))
+ return false;
+ if (!m_cueTimeRegex.RegComp(cueTimePattern))
+ return false;
+ if (!m_timeRegex.RegComp(timePattern))
+ return false;
+ if (!m_cueCssTagRegex.RegComp(cueCssTagPattern))
+ return false;
+ for (auto const& item : cuePropsPatterns)
+ {
+ CRegExp reg = CRegExp();
+ if (!reg.RegComp(item.second))
+ return false;
+ m_cuePropsMapRegex.insert({item.first, reg});
+ }
+ for (auto const& item : cueCssPatterns)
+ {
+ CRegExp reg = CRegExp();
+ if (!reg.RegComp(item.second))
+ return false;
+ m_cueCssStyleMapRegex.insert({item.first, reg});
+ }
+
+ auto overrideStyles{
+ CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()->GetOverrideStyles()};
+ m_overridePositions = (overrideStyles == KODI::SUBTITLES::OverrideStyles::STYLES_POSITIONS ||
+ overrideStyles == KODI::SUBTITLES::OverrideStyles::POSITIONS);
+ m_overrideStyle = (overrideStyles == KODI::SUBTITLES::OverrideStyles::STYLES_POSITIONS ||
+ overrideStyles == KODI::SUBTITLES::OverrideStyles::STYLES);
+
+ return true;
+}
+
+void CWebVTTHandler::Reset()
+{
+ m_previousLines[0].clear();
+ m_previousLines[1].clear();
+ m_previousLines[2].clear();
+ m_currentSection = WebvttSection::UNDEFINED;
+ m_offset = 0;
+}
+
+bool CWebVTTHandler::CheckSignature(const std::string& data)
+{
+ // Check the sequence of chars to identify WebVTT signature
+ if (ValidateSignature(data, signatureCharsBOM) || ValidateSignature(data, signatureChars))
+ return true;
+
+ CLog::LogF(LOGERROR, "WebVTT signature not valid");
+ return false;
+}
+
+void CWebVTTHandler::DecodeLine(std::string line, std::vector<subtitleData>* subList)
+{
+ // Keep lines values history, needed to identify the cue ID
+ m_previousLines[0] = m_previousLines[1];
+ m_previousLines[1] = m_previousLines[2];
+ m_previousLines[2] = line;
+
+ if (m_currentSection == WebvttSection::UNDEFINED)
+ {
+ if (line == "STYLE" || line == "Style:") // "Style:" Youtube spec
+ {
+ m_currentSection = WebvttSection::STYLE;
+ return;
+ }
+ else if (line == "REGION")
+ {
+ m_currentSection = WebvttSection::REGION;
+ return;
+ }
+ else if (line == "NOTE")
+ {
+ m_currentSection = WebvttSection::NOTE;
+ }
+ else if (StringUtils::StartsWith(line, "X-TIMESTAMP-MAP")) // HLS streaming spec
+ {
+ // Get the HLS timestamp values to sync the subtitles with video
+ CRegExp regLocal;
+ CRegExp regMpegTs;
+
+ if (regLocal.RegComp("LOCAL:((?:(\\d{1,}):)?(\\d{2}):(\\d{2}\\.\\d{3}))") &&
+ regMpegTs.RegComp("MPEGTS:(\\d+)"))
+ {
+ double tsLocalUs{0.0};
+ double tsMpegUs{0.0};
+
+ if ((regLocal.RegFind(line) >= 0 && regLocal.GetSubCount() == 4) &&
+ regMpegTs.RegFind(line) >= 0)
+ {
+ tsLocalUs = GetTimeFromRegexTS(regLocal, 2);
+ // Converts a 90 kHz clock timestamp to a timestamp in microseconds
+ tsMpegUs = std::stod(regMpegTs.GetMatch(1)) * MICROS_PER_SECOND / MPEG_TIMESCALE;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR,
+ "Failed to get X-TIMESTAMP-MAP values, subtitles could be out of sync");
+ }
+
+ // offset = periodStart + tsMpegUs - tsLocalUs
+ m_offset += tsMpegUs - tsLocalUs;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR,
+ "Failed to compile X-TIMESTAMP-MAP regexes, subtitles could be out of sync");
+ }
+ }
+ else if (IsCueLine(line))
+ {
+ // From here we start the cue conversions,
+ // other sections should not be allowed with exception of "NOTE"
+ m_currentSection = WebvttSection::CUE;
+ }
+ }
+
+ if (m_currentSection == WebvttSection::CUE || m_currentSection == WebvttSection::CUE_TEXT)
+ {
+ if (IsCueLine(line))
+ {
+ if (m_currentSection == WebvttSection::CUE_TEXT && !m_subtitleData.text.empty())
+ {
+ CLog::LogF(LOGWARNING,
+ "Malformed cue, is missing the empty line for the end of cue section");
+
+ // Recover the current cue, add the subtitle to the list
+ ConvertAddSubtitle(subList);
+
+ // Change to a new cue section
+ m_currentSection = WebvttSection::CUE;
+ }
+ else if (m_currentSection == WebvttSection::CUE_TEXT)
+ {
+ CLog::LogF(LOGWARNING, "Malformed cue, the cue is within the text area");
+ return; // Continue to try again for possible subtitle text after this line
+ }
+
+ // Create new subtitle data
+ if (m_currentSection == WebvttSection::CUE)
+ m_subtitleData = subtitleData();
+
+ // Get and store the cue data
+ GetCueData(line);
+
+ // The cue ID is optional, can be a number or a text,
+ // used to identify chapters or to apply a specific CSS Style
+ if (m_previousLines[0].empty() && !m_previousLines[1].empty())
+ m_subtitleData.cueSettings.id = m_previousLines[1];
+
+ // From the next line we should have the text area
+ // NOTE: The cue properties will be computed at the first subtitle text line
+ m_currentSection = WebvttSection::CUE_TEXT;
+ return; // Start read the cue text area from the next line
+ }
+
+ if (m_currentSection == WebvttSection::CUE_TEXT)
+ {
+ if (line.empty()) // An empty line means the end of the current cue
+ {
+ if (m_subtitleData.text.empty())
+ {
+ CLog::LogF(LOGWARNING, "Malformed cue, is missing the subtitle text");
+ m_currentSection = WebvttSection::UNDEFINED;
+ return; // This cue will be skipped
+ }
+
+ ConvertAddSubtitle(subList);
+
+ m_currentSection = WebvttSection::CUE;
+ }
+ else
+ {
+ if (line == "{")
+ {
+ // Starting point for JSON metadata
+ m_currentSection = WebvttSection::CUE_METADATA;
+ return;
+ }
+ // Collect and convert all subtitle text lines
+ if (!m_subtitleData.text.empty())
+ m_subtitleData.text += "\n";
+
+ TranslateEscapeChars(line);
+
+ // We need to calculate the text position right here,
+ // when we have the first subtitle text line
+ if (m_subtitleData.text.empty())
+ CalculateTextPosition(line);
+
+ m_subtitleData.text += line;
+ }
+ }
+ }
+ else if (m_currentSection == WebvttSection::CUE_METADATA)
+ {
+ // Cue metadata is not supported (is for scripted apps)
+ if (line.empty()) // End of section
+ m_currentSection = WebvttSection::UNDEFINED;
+ }
+ else if (m_currentSection == WebvttSection::STYLE ||
+ m_currentSection == WebvttSection::STYLE_CONTENT)
+ {
+ // Non-implemented CSS selector features:
+ // - Attribute selector [lang="xx-yy"] for applicable language
+ // - Pseudo-classes
+ // - Cue region
+ // - Cascading classes for color-background (takes only first color)
+
+ if (line.empty()) // End of section
+ {
+ m_feedCssSelectorNames.clear();
+ m_currentSection = WebvttSection::UNDEFINED;
+ }
+
+ if (m_currentSection == WebvttSection::STYLE)
+ {
+ if (m_feedCssSelectorNames.empty())
+ m_feedCssStyle = webvttCssStyle();
+
+ // Collect cue selectors (also handle multiple inline selectors)
+ for (std::string& cueSelector : StringUtils::Split(line, ','))
+ {
+ if (m_cueCssTagRegex.RegFind(cueSelector) >= 0)
+ {
+ std::string selectorName = m_cueCssTagRegex.GetMatch(m_cueCssTagRegex.GetSubCount());
+ UTILS::CSS::Escape(selectorName);
+ m_feedCssSelectorNames.emplace_back(selectorName);
+ }
+ }
+
+ if (line.find('{') != std::string::npos && !m_feedCssSelectorNames.empty())
+ {
+ // Detect the selector type, from the first selector name
+ std::string_view selectorName = m_feedCssSelectorNames[0];
+
+ if (selectorName == "::cue")
+ m_feedCssStyle.m_selectorType = WebvttSelector::ANY;
+ else if (selectorName[0] == '#')
+ m_feedCssStyle.m_selectorType = WebvttSelector::ID;
+ else if (selectorName.find('.') != std::string::npos)
+ m_feedCssStyle.m_selectorType = WebvttSelector::CLASS;
+ else if (selectorName.compare(0, 9, "lang[lang") == 0 ||
+ selectorName.compare(0, 7, "v[voice") == 0)
+ m_feedCssStyle.m_selectorType = WebvttSelector::ATTRIBUTE;
+ else if (selectorName[0] == ':') // Pseudo-classes not implemented
+ m_feedCssStyle.m_selectorType = WebvttSelector::UNSUPPORTED;
+ else
+ m_feedCssStyle.m_selectorType = WebvttSelector::TYPE;
+
+ if (m_feedCssStyle.m_selectorType == WebvttSelector::UNSUPPORTED)
+ {
+ m_feedCssSelectorNames.clear();
+ m_currentSection = WebvttSection::UNDEFINED;
+ return;
+ }
+
+ // Go through to recover possible data inline with the selector
+ m_currentSection = WebvttSection::STYLE_CONTENT;
+ }
+ }
+
+ if (m_currentSection == WebvttSection::STYLE_CONTENT)
+ {
+ // Get and store the CSS Style properties
+ // Font color
+ const std::string colorName = GetCueCssValue("colorName", line);
+ if (!colorName.empty()) // From CSS Color name
+ {
+ if (!m_CSSColorsLoaded) // Load colors in lazy way
+ LoadColors();
+
+ auto colorInfo =
+ std::find_if(m_CSSColors.begin(), m_CSSColors.end(),
+ [&](const std::pair<std::string, UTILS::COLOR::ColorInfo>& item) {
+ return StringUtils::CompareNoCase(item.first, colorName) == 0;
+ });
+ if (colorInfo != m_CSSColors.end())
+ {
+ const uint32_t color = UTILS::COLOR::ConvertToBGR(colorInfo->second.colorARGB);
+ m_feedCssStyle.m_color = StringUtils::Format("{:6x}", color);
+ }
+ }
+ std::string colorRGB = GetCueCssValue("colorRGB", line);
+ if (!colorRGB.empty()) // From CSS Color numeric R,G,B values
+ {
+ const auto intValues = StringUtils::Split(colorRGB, ",");
+ uint32_t color = UTILS::COLOR::ConvertIntToRGB(
+ std::stoi(intValues[2]), std::stoi(intValues[1]), std::stoi(intValues[0]));
+ m_feedCssStyle.m_color = StringUtils::Format("{:6x}", color);
+ }
+ // Font bold
+ if (!GetCueCssValue("fontWeight", line).empty())
+ m_feedCssStyle.m_isFontBold = true;
+ // Font italic
+ if (!GetCueCssValue("fontStyle", line).empty())
+ m_feedCssStyle.m_isFontItalic = true;
+ // Font underline
+ if (!GetCueCssValue("textDecoration", line).empty())
+ m_feedCssStyle.m_isFontUnderline = true;
+
+ if (line.find('}') != std::string::npos || line.empty()) // End of current style
+ {
+ // Store the style
+ // Overwrite existing selectors to allow authors to change default classes
+ auto& selectorTypeMap = m_cssSelectors[m_feedCssStyle.m_selectorType];
+ // For multiple selectors, copy the style for each one
+ for (std::string_view selectorName : m_feedCssSelectorNames)
+ {
+ webvttCssStyle cssStyleCopy = m_feedCssStyle;
+ // Convert CSS syntax to WebVTT syntax selector
+ std::string selectorNameConv{selectorName};
+ if (m_feedCssStyle.m_selectorType == WebvttSelector::ATTRIBUTE)
+ {
+ selectorNameConv = selectorName.substr(0, selectorName.find('['));
+ selectorNameConv += " ";
+ const size_t attribPosStart = selectorName.find('"');
+ const size_t attribPosEnd = selectorName.find_last_of('"');
+ if (attribPosEnd > attribPosStart)
+ {
+ selectorNameConv +=
+ selectorName.substr(attribPosStart + 1, attribPosEnd - 1 - attribPosStart);
+ }
+ }
+ else if (m_feedCssStyle.m_selectorType == WebvttSelector::ID)
+ selectorNameConv.erase(0, 1); // Remove # char
+
+ cssStyleCopy.m_selectorName = selectorNameConv;
+ selectorTypeMap[selectorNameConv] = cssStyleCopy;
+ }
+ m_feedCssSelectorNames.clear();
+
+ // Let's go back to "STYLE" to parse multiple "::cue" on the same section
+ m_currentSection = WebvttSection::STYLE;
+ }
+ }
+ }
+ else if (m_currentSection == WebvttSection::REGION)
+ {
+ // Regions are not supported
+ if (line.empty()) // End of section
+ m_currentSection = WebvttSection::UNDEFINED;
+ }
+ else if (m_currentSection == WebvttSection::NOTE)
+ {
+ if (line.empty()) // End of section
+ m_currentSection = WebvttSection::UNDEFINED;
+ }
+}
+
+bool CWebVTTHandler::IsCueLine(std::string& line)
+{
+ return m_cueTimeRegex.RegFind(line) >= 0;
+}
+
+void CWebVTTHandler::GetCueData(std::string& cueText)
+{
+ std::string cueSettings;
+ // Valid time formats: (included with or without spaces near -->)
+ // 00:00.000 --> 00:00.000
+ // 00:00:00.000 --> 00:00:00.000
+ if (m_cueTimeRegex.GetSubCount() == 6)
+ {
+ m_subtitleData.startTime = GetTimeFromRegexTS(m_cueTimeRegex) + m_offset;
+ m_subtitleData.stopTime = GetTimeFromRegexTS(m_cueTimeRegex, 4) + m_offset;
+ cueSettings =
+ cueText.substr(m_cueTimeRegex.GetFindLen(), cueText.length() - m_cueTimeRegex.GetFindLen());
+ StringUtils::Trim(cueSettings);
+ }
+ else // This should never happen
+ {
+ CLog::LogF(LOGERROR, "Cue timing not found");
+ }
+
+ // Parse the cue settings
+ GetCueSettings(cueSettings);
+}
+
+void CWebVTTHandler::GetCueSettings(std::string& cueSettings)
+{
+ webvttCueSettings settings;
+ // settings.regionId = ""; // "region" is not supported
+
+ const std::string cueVertical =
+ GetCueSettingValue("vertical", cueSettings, ""); // Ref. Writing direction
+ if (cueVertical == "lr")
+ settings.verticalAlign = WebvttVAlign::VERTICAL_LR;
+ else if (cueVertical == "rl")
+ settings.verticalAlign = WebvttVAlign::VERTICAL_RL;
+ else
+ settings.verticalAlign = WebvttVAlign::HORIZONTAL;
+
+ const std::string cuePos = GetCueSettingValue("position", cueSettings, "auto");
+ if (cuePos == "auto")
+ settings.position.isAuto = true;
+ else
+ {
+ settings.position.isAuto = false;
+ settings.position.value = std::stod(cuePos.c_str());
+ if (settings.position.value > 100) // Not valid
+ settings.position.value = 50;
+ }
+
+ const std::string cuePosAlign = GetCueSettingValue("positionAlign", cueSettings, "auto");
+ if (cuePosAlign == "line-left")
+ settings.positionAlign = WebvttAlign::LEFT;
+ else if (cuePosAlign == "line-right")
+ settings.positionAlign = WebvttAlign::RIGHT;
+ else if (cuePosAlign == "center" || cuePosAlign == "middle") // "middle" undocumented
+ settings.positionAlign = WebvttAlign::CENTER;
+ else if (cuePosAlign == "start") // Undocumented
+ settings.positionAlign = WebvttAlign::START;
+ else if (cuePosAlign == "end") // Undocumented
+ settings.positionAlign = WebvttAlign::END;
+ else
+ settings.positionAlign = WebvttAlign::AUTO;
+
+ const std::string cueSize = GetCueSettingValue("size", cueSettings, "100.00");
+ settings.size = std::stod(cueSize.c_str());
+ if (settings.size > 100.0) // Not valid
+ settings.size = 100.0;
+
+ const std::string cueSnapToLines = GetCueSettingValue("snapToLines", cueSettings, "true");
+ settings.snapToLines = cueSnapToLines == "true";
+
+ std::string cueLine = GetCueSettingValue("line", cueSettings, "auto");
+ settings.line.isAuto = false;
+ auto cueLinePercPos = cueLine.find('%');
+ if (cueLine == "auto")
+ {
+ settings.line.isAuto = true;
+ cueLine = "0";
+ }
+ else if (cueLinePercPos != std::string::npos || !settings.snapToLines)
+ {
+ settings.snapToLines = false;
+ if (cueLinePercPos != std::string::npos)
+ cueLine.pop_back(); // Remove % at the end
+ }
+ settings.line.value = std::stod(cueLine.c_str());
+
+ // The optional "alignment" property of "line" setting is not supported.
+
+ const std::string cueAlign = GetCueSettingValue("align", cueSettings, "");
+ if (cueAlign == "left")
+ settings.align = WebvttAlign::LEFT;
+ else if (cueAlign == "right")
+ settings.align = WebvttAlign::RIGHT;
+ else if (cueAlign == "center" || cueAlign == "middle") // Middle undocumented
+ settings.align = WebvttAlign::CENTER;
+ else if (cueAlign == "start")
+ settings.align = WebvttAlign::START;
+ else if (cueAlign == "end")
+ settings.align = WebvttAlign::END;
+ else
+ settings.align = WebvttAlign::CENTER;
+
+ m_subtitleData.cueSettings = settings;
+}
+
+void CWebVTTHandler::CalculateTextPosition(std::string& subtitleText)
+{
+ // Here we cannot handle a kind of cue box, to simulate it we use the margins
+ // to position the text on the screen, the subtitle text (horizontal)
+ // alignment will be also computed.
+
+ // Sidenote: Limits to bidi direction checks, the webvtt doc specifies that
+ // each line of text should be aligned according to the direction of text,
+ // but here we limit this by checking the text direction of the first line
+ // only, and will be used for all lines of the Cue text.
+
+ int marginLeft = 0;
+ int marginRight = 0;
+ int marginVertical = 0;
+ TextAlignment textAlign;
+
+ webvttCueSettings* settings = &m_subtitleData.cueSettings;
+
+ // Compute cue box "align" value
+ if (settings->align == WebvttAlign::START)
+ {
+ // Clean text from tags or bidi check doesn't work
+ std::string textNoTags = subtitleText;
+ HTML::CHTMLUtil::RemoveTags(textNoTags);
+ if (CCharsetConverter::utf8IsRTLBidiDirection(textNoTags))
+ settings->align = WebvttAlign::RIGHT;
+ else
+ settings->align = WebvttAlign::LEFT;
+ }
+ else if (settings->align == WebvttAlign::END)
+ {
+ // Clean text from tags or bidi check doesn't work
+ std::string textNoTags = subtitleText;
+ HTML::CHTMLUtil::RemoveTags(textNoTags);
+ if (CCharsetConverter::utf8IsRTLBidiDirection(textNoTags))
+ settings->align = WebvttAlign::LEFT;
+ else
+ settings->align = WebvttAlign::RIGHT;
+ }
+
+ // Compute cue box "position" value
+ if (settings->position.isAuto)
+ {
+ // Position of cue box depends from text alignment
+ if (settings->align == WebvttAlign::LEFT)
+ settings->position.value = 0;
+ else if (settings->align == WebvttAlign::RIGHT)
+ settings->position.value = 100;
+ else
+ settings->position.value = 50;
+ }
+ int cuePosPixel = static_cast<int>((VIEWPORT_WIDTH / 100) * settings->position.value);
+
+ // Compute cue box "position alignment" value
+ if (settings->positionAlign == WebvttAlign::AUTO)
+ {
+ if (settings->align == WebvttAlign::LEFT)
+ settings->positionAlign = WebvttAlign::LEFT; // line-left
+ else if (settings->align == WebvttAlign::RIGHT)
+ settings->positionAlign = WebvttAlign::RIGHT; // line-right
+ else
+ settings->positionAlign = WebvttAlign::CENTER;
+ }
+ else if (settings->positionAlign == WebvttAlign::START) // Undocumented
+ {
+ // Is not clear if bidi check here is needed
+ // Clean text from tags or bidi check doesn't work
+ std::string textNoTags = subtitleText;
+ HTML::CHTMLUtil::RemoveTags(textNoTags);
+ if (CCharsetConverter::utf8IsRTLBidiDirection(textNoTags))
+ settings->positionAlign = WebvttAlign::RIGHT; // line-right
+ else
+ settings->positionAlign = WebvttAlign::LEFT; // line-left
+ }
+ else if (settings->positionAlign == WebvttAlign::END) // Undocumented
+ {
+ // Is not clear if bidi check here is needed
+ // Clean text from tags or bidi check doesn't work
+ std::string textNoTags = subtitleText;
+ HTML::CHTMLUtil::RemoveTags(textNoTags);
+ if (CCharsetConverter::utf8IsRTLBidiDirection(textNoTags))
+ settings->positionAlign = WebvttAlign::LEFT; // line-left
+ else
+ settings->positionAlign = WebvttAlign::RIGHT; // line-right
+ }
+
+ // Compute cue box "size" value
+ int cueSizePixel = static_cast<int>((VIEWPORT_WIDTH / 100) * settings->size);
+
+ // Calculate Left/Right margins,
+ // by taking into account cue box "position alignment" and cue box "size"
+ if (settings->positionAlign == WebvttAlign::LEFT) // line-left
+ {
+ marginLeft = cuePosPixel;
+ marginRight = static_cast<int>(VIEWPORT_WIDTH - (cuePosPixel + cueSizePixel));
+ }
+ else if (settings->positionAlign == WebvttAlign::RIGHT) // line-right
+ {
+ marginLeft = static_cast<int>(cuePosPixel - cueSizePixel);
+ marginRight = static_cast<int>(VIEWPORT_WIDTH - cuePosPixel);
+ }
+ else if (settings->positionAlign == WebvttAlign::CENTER)
+ {
+ int cueHalfSize = static_cast<int>(static_cast<double>(cueSizePixel) / 2);
+ marginLeft = cuePosPixel - cueHalfSize;
+ marginRight = static_cast<int>(VIEWPORT_WIDTH - (cuePosPixel + cueHalfSize));
+ }
+
+ // Compute cue box "line"
+ double cueLinePerc = 100.00;
+ if (settings->snapToLines) // Numeric line value
+ {
+ // "line" as numeric value is not supported.
+ // From docs is specified to calculate the line position
+ // by the height of the first line of text, but is not clear:
+ // 1) how we can calculate the position when the text size is not fixed
+ // 2) what is the max value and if can change between webvtt files
+ // ref. https://www.w3.org/TR/webvtt1/#webvtt-line-cue-setting
+ }
+ else // Percentage line value
+ {
+ if (settings->line.value < 0.00)
+ cueLinePerc = 0;
+ else if (settings->line.value > 100.00)
+ cueLinePerc = 100;
+ else
+ cueLinePerc = settings->line.value;
+ }
+
+ // The vertical margin should always referred from the top to simulate
+ // the current cue box position without a cue box.
+ // But if the vertical margin is too high and the text size is very large,
+ // in some cases the text could go off-screen.
+ // To try ensure that the text does not go off-screen,
+ // above a certain threshold of vertical margin we align from bottom.
+ bool useAlignBottom = false;
+ if (cueLinePerc >= 90)
+ {
+ useAlignBottom = true;
+ marginVertical = MARGIN_VERTICAL;
+ }
+ else
+ marginVertical = static_cast<int>((VIEWPORT_HEIGHT / 100) * cueLinePerc);
+
+ if (settings->align == WebvttAlign::LEFT)
+ textAlign = useAlignBottom ? TextAlignment::SUB_LEFT : TextAlignment::TOP_LEFT;
+ else if (settings->align == WebvttAlign::RIGHT)
+ textAlign = useAlignBottom ? TextAlignment::SUB_RIGHT : TextAlignment::TOP_RIGHT;
+ else
+ textAlign = useAlignBottom ? TextAlignment::SUB_CENTER : TextAlignment::TOP_CENTER;
+
+ m_subtitleData.textAlign = textAlign;
+ m_subtitleData.useMargins = !m_overridePositions;
+ m_subtitleData.marginLeft = std::max(marginLeft, 0);
+ m_subtitleData.marginRight = std::max(marginRight, 0);
+ m_subtitleData.marginVertical = marginVertical;
+}
+
+std::string CWebVTTHandler::GetCueSettingValue(const std::string& propName,
+ std::string& text,
+ std::string defaultValue)
+{
+ if (m_cuePropsMapRegex[propName].RegFind(text) >= 0)
+ return m_cuePropsMapRegex[propName].GetMatch(1);
+
+ return defaultValue;
+}
+
+std::string CWebVTTHandler::GetCueCssValue(const std::string& cssPropName, std::string& line)
+{
+ if (m_cueCssStyleMapRegex[cssPropName].RegFind(line) >= 0)
+ return m_cueCssStyleMapRegex[cssPropName].GetMatch(1);
+
+ return "";
+}
+
+void CWebVTTHandler::AddDefaultCssClasses()
+{
+ m_cssSelectors[WebvttSelector::CLASS] = cueCssDefaultColorClasses;
+}
+
+bool CWebVTTHandler::GetBaseStyle(webvttCssStyle& style)
+{
+ // Get the style applied to all cue's
+ bool isBaseStyleFound = false;
+ if (!m_overrideStyle)
+ {
+ auto& selectorAnyMap = m_cssSelectors[WebvttSelector::ANY];
+ auto itBaseStyle = selectorAnyMap.find("::cue");
+ if (itBaseStyle != selectorAnyMap.end())
+ {
+ style = itBaseStyle->second;
+ isBaseStyleFound = true;
+ }
+ }
+
+ // Try find the CSS Style by cue ID
+ // and merge it to the base style
+ if (!m_subtitleData.cueSettings.id.empty())
+ {
+ auto& selectorIdMap = m_cssSelectors[WebvttSelector::ID];
+ auto itCssStyle = selectorIdMap.find(m_subtitleData.cueSettings.id);
+ if (itCssStyle != selectorIdMap.end())
+ {
+ webvttCssStyle& idStyle = itCssStyle->second;
+ style.m_isFontBold = style.m_isFontBold || idStyle.m_isFontBold;
+ style.m_isFontItalic = style.m_isFontItalic || idStyle.m_isFontItalic;
+ style.m_isFontUnderline = style.m_isFontUnderline || idStyle.m_isFontUnderline;
+ style.m_color = idStyle.m_color.empty() ? style.m_color : idStyle.m_color;
+ return true;
+ }
+ }
+ return isBaseStyleFound;
+}
+
+void CWebVTTHandler::ConvertSubtitle(std::string& text)
+{
+ int pos = 0;
+ int flagTags[FLAG_TAG_COUNT] = {0};
+
+ std::string textRaw;
+ int lastPos{0};
+ webvttCssStyle baseStyle;
+ bool isBaseStyleSet = GetBaseStyle(baseStyle);
+
+ if (isBaseStyleSet)
+ {
+ const std::string baseStyleTag = ConvertStyleToOpenTags(flagTags, baseStyle);
+ text.insert(0, baseStyleTag);
+ lastPos = baseStyleTag.length();
+ }
+
+ // Map to store opened CSS tags [tag name]+[style selector]
+ std::deque<std::pair<std::string, webvttCssStyle*>> cssTagsOpened;
+ // Scan all tags
+ while ((pos = m_tagsRegex.RegFind(text, pos)) >= 0)
+ {
+ tagToken tag;
+ tag.m_token = StringUtils::ToLower(m_tagsRegex.GetMatch(0));
+ tag.m_isClosing = m_tagsRegex.GetMatch(1) == "/";
+ if (!m_tagsRegex.GetMatch(2).empty())
+ tag.m_timestampTag = tag.m_token;
+ tag.m_tag = StringUtils::ToLower(m_tagsRegex.GetMatch(3));
+ tag.m_classes = StringUtils::Split(m_tagsRegex.GetMatch(4).erase(0, 1), ".");
+ tag.m_annotation = m_tagsRegex.GetMatch(5);
+
+ text.erase(pos, tag.m_token.length());
+ // Keep a copy of the text without tags
+ textRaw += text.substr(lastPos, pos - lastPos);
+
+ if (tag.m_isClosing)
+ InsertCssStyleCloseTag(tag, text, pos, flagTags, cssTagsOpened, baseStyle);
+
+ if (tag.m_tag == "b")
+ {
+ if (!tag.m_isClosing)
+ {
+ if (flagTags[FLAG_TAG_BOLD] == 0)
+ InsertTextPos(text, "{\\b1}", pos);
+ flagTags[FLAG_TAG_BOLD] += 1;
+ }
+ else if (flagTags[FLAG_TAG_BOLD] > 0)
+ { // Closing tag (if previously opened)
+ flagTags[FLAG_TAG_BOLD] = flagTags[FLAG_TAG_BOLD] > 0 ? (flagTags[FLAG_TAG_BOLD] - 1) : 0;
+ if (flagTags[FLAG_TAG_BOLD] == 0)
+ InsertTextPos(text, "{\\b0}", pos);
+ }
+ }
+ else if (tag.m_tag == "i")
+ {
+ if (!tag.m_isClosing)
+ {
+ if (flagTags[FLAG_TAG_ITALIC] == 0)
+ InsertTextPos(text, "{\\i1}", pos);
+ flagTags[FLAG_TAG_ITALIC] += 1;
+ }
+ else if (flagTags[FLAG_TAG_ITALIC] > 0)
+ { // Closing tag (if previously opened)
+ flagTags[FLAG_TAG_ITALIC] =
+ flagTags[FLAG_TAG_ITALIC] > 0 ? (flagTags[FLAG_TAG_ITALIC] - 1) : 0;
+ if (flagTags[FLAG_TAG_ITALIC] == 0)
+ InsertTextPos(text, "{\\i0}", pos);
+ }
+ }
+ else if (tag.m_tag == "u")
+ {
+ if (!tag.m_isClosing)
+ {
+ if (flagTags[FLAG_TAG_UNDERLINE] == 0)
+ InsertTextPos(text, "{\\u1}", pos);
+ flagTags[FLAG_TAG_UNDERLINE] += 1;
+ }
+ else if (flagTags[FLAG_TAG_ITALIC] > 0)
+ { // Closing tag (if previously opened)
+ flagTags[FLAG_TAG_UNDERLINE] =
+ flagTags[FLAG_TAG_UNDERLINE] > 0 ? (flagTags[FLAG_TAG_UNDERLINE] - 1) : 0;
+ if (flagTags[FLAG_TAG_UNDERLINE] == 0)
+ InsertTextPos(text, "{\\u0}", pos);
+ }
+ }
+
+ if (!tag.m_isClosing)
+ InsertCssStyleStartTag(tag, text, pos, flagTags, cssTagsOpened);
+
+ lastPos = pos;
+ }
+ // Keep a copy of the text without tags
+ textRaw += text.substr(lastPos);
+
+ m_subtitleData.textRaw = textRaw;
+
+ if (isBaseStyleSet)
+ {
+ webvttCssStyle emptyStyle{};
+ text += ConvertStyleToCloseTags(flagTags, &baseStyle, &emptyStyle);
+ }
+
+ // Check for malformed tags still opened
+ if (!cssTagsOpened.empty() || flagTags[FLAG_TAG_BOLD] > 0 || flagTags[FLAG_TAG_ITALIC] > 0 ||
+ flagTags[FLAG_TAG_UNDERLINE] > 0)
+ {
+ text += "{\\r}"; // Cancel all opened tags
+ }
+
+ // Add text alignment (based on cue settings)
+ if (!m_overridePositions)
+ {
+ if (m_subtitleData.textAlign == TextAlignment::TOP_LEFT)
+ text.insert(0, "{\\an7}");
+ else if (m_subtitleData.textAlign == TextAlignment::TOP_RIGHT)
+ text.insert(0, "{\\an9}");
+ else if (m_subtitleData.textAlign == TextAlignment::TOP_CENTER)
+ text.insert(0, "{\\an8}");
+ else if (m_subtitleData.textAlign == TextAlignment::SUB_LEFT)
+ text.insert(0, "{\\an1}");
+ else if (m_subtitleData.textAlign == TextAlignment::SUB_RIGHT)
+ text.insert(0, "{\\an3}");
+ else if (m_subtitleData.textAlign == TextAlignment::SUB_CENTER)
+ text.insert(0, "{\\an2}");
+ }
+}
+
+void CWebVTTHandler::InsertCssStyleStartTag(
+ const tagToken& tag,
+ std::string& text,
+ int& pos,
+ int flagTags[],
+ std::deque<std::pair<std::string, webvttCssStyle*>>& cssTagsOpened)
+{
+ if (!tag.m_timestampTag.empty())
+ {
+ // Timestamp tag will be interpreded as karaoke effect
+ if (m_timeRegex.RegFind(tag.m_timestampTag) >= 0)
+ {
+ const double timeStart = GetTimeFromRegexTS(m_timeRegex) + m_offset;
+ // Libass works with duration instead of timestamp
+ // so we need to find the next timestamp
+ double timeEnd = m_subtitleData.stopTime;
+ if (m_timeRegex.RegFind(text) >= 0)
+ timeEnd = GetTimeFromRegexTS(m_timeRegex) + m_offset;
+
+ if (timeStart <= timeEnd)
+ {
+ int duration = static_cast<int>(timeEnd - timeStart) / 10000;
+ std::string assTag = "{\\k" + std::to_string(duration) + "}";
+ text.insert(pos, assTag);
+ pos += static_cast<int>(assTag.length());
+ }
+ else
+ CLog::LogF(LOGERROR, "Unable to get duration from timestamp: {}", tag.m_timestampTag);
+ }
+ else
+ CLog::LogF(LOGERROR, "Error parsing timestamp tag: {}", tag.m_timestampTag);
+
+ return;
+ }
+
+ bool hasAttribute = !tag.m_annotation.empty() && (tag.m_tag == "lang" || tag.m_tag == "v");
+
+ webvttCssStyle* cssStyle{nullptr};
+ if (hasAttribute)
+ {
+ auto& selectorMap = m_cssSelectors[WebvttSelector::ATTRIBUTE];
+ auto itCssStyle = selectorMap.find(tag.m_tag + " " + tag.m_annotation);
+ if (itCssStyle != selectorMap.end())
+ cssStyle = &itCssStyle->second;
+ }
+ else if (!tag.m_classes.empty())
+ {
+ auto& selectorMap = m_cssSelectors[WebvttSelector::CLASS];
+ // Cascading classes not implemented
+ const std::string className = "." + tag.m_classes[0];
+ // Class selector that target a specific element have the priority
+ auto itCssStyle = selectorMap.find(tag.m_tag + className);
+ if (itCssStyle != selectorMap.end())
+ cssStyle = &itCssStyle->second;
+
+ if (!cssStyle)
+ {
+ auto itCssStyle = selectorMap.find(className);
+ if (itCssStyle != selectorMap.end())
+ cssStyle = &itCssStyle->second;
+ }
+ }
+ else
+ {
+ auto& selectorMap = m_cssSelectors[WebvttSelector::TYPE];
+ auto itCssStyle = selectorMap.find(tag.m_tag);
+ if (itCssStyle != selectorMap.end())
+ cssStyle = &itCssStyle->second;
+ }
+
+ if (cssStyle)
+ {
+ // Insert the CSS Style converted as tags
+ const std::string tags = ConvertStyleToOpenTags(flagTags, *cssStyle);
+ text.insert(pos, tags);
+ pos += static_cast<int>(tags.length());
+ // Keep track of the opened tags
+ cssTagsOpened.emplace_front(tag.m_tag, cssStyle);
+ }
+}
+
+void CWebVTTHandler::InsertCssStyleCloseTag(
+ const tagToken& tag,
+ std::string& text,
+ int& pos,
+ int flagTags[],
+ std::deque<std::pair<std::string, webvttCssStyle*>>& cssTagsOpened,
+ webvttCssStyle& baseStyle)
+{
+ if (cssTagsOpened.empty())
+ return;
+
+ std::pair<std::string, webvttCssStyle*> stylePair = cssTagsOpened.front();
+ if (stylePair.first == tag.m_tag)
+ {
+ cssTagsOpened.pop_front();
+ webvttCssStyle* style = &baseStyle;
+ if (!cssTagsOpened.empty())
+ style = cssTagsOpened.front().second;
+ const std::string tags = ConvertStyleToCloseTags(flagTags, stylePair.second, style);
+ text.insert(pos, tags);
+ pos += static_cast<int>(tags.length());
+ }
+}
+
+void CWebVTTHandler::ConvertAddSubtitle(std::vector<subtitleData>* subList)
+{
+ // Convert tags and apply the CSS Styles converted
+ ConvertSubtitle(m_subtitleData.text);
+
+ if (m_lastSubtitleData)
+ {
+ // Check for same subtitle data
+ if (m_lastSubtitleData->startTime == m_subtitleData.startTime &&
+ m_lastSubtitleData->stopTime == m_subtitleData.stopTime &&
+ m_lastSubtitleData->textRaw == m_subtitleData.textRaw &&
+ m_lastSubtitleData->cueSettings == m_subtitleData.cueSettings)
+ {
+ if (subList->empty())
+ {
+ // On segmented WebVTT, it can happen that the last subtitle entry is sent
+ // on consecutive demux packet. Hence we avoid showing overlapping subs.
+ return;
+ }
+ else
+ {
+ // Youtube WebVTT can have multiple cues with same time, text and position
+ // sometimes with different css color but only last cue will be visible
+ // this cause unexpected results on screen, so we keep only the current one
+ // and delete the previous one.
+ subList->pop_back();
+ }
+ }
+ }
+
+ subList->emplace_back(m_subtitleData);
+ m_lastSubtitleData = std::make_unique<subtitleData>(m_subtitleData);
+}
+
+void CWebVTTHandler::LoadColors()
+{
+ CGUIColorManager colorManager;
+ colorManager.LoadColorsListFromXML(
+ CSpecialProtocol::TranslatePathConvertCase("special://xbmc/system/colors.xml"), m_CSSColors,
+ false);
+ m_CSSColorsLoaded = true;
+}
+
+double CWebVTTHandler::GetTimeFromRegexTS(CRegExp& regex, int indexStart /* = 1 */)
+{
+ int sHours = 0;
+ if (!regex.GetMatch(indexStart).empty())
+ sHours = std::stoi(regex.GetMatch(indexStart));
+ int sMinutes = std::stoi(regex.GetMatch(indexStart + 1));
+ double sSeconds = std::stod(regex.GetMatch(indexStart + 2));
+ return (static_cast<double>(sHours * 3600 + sMinutes * 60) + sSeconds) * DVD_TIME_BASE;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTHandler.h b/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTHandler.h
new file mode 100644
index 0000000..1934a74
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTHandler.h
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2005-2021 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/GUIColorManager.h"
+#include "utils/ColorUtils.h"
+#include "utils/RegExp.h"
+
+#include <deque>
+#include <map>
+#include <memory>
+#include <stdio.h>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+enum class WebvttSection
+{
+ UNDEFINED = 0,
+ STYLE,
+ STYLE_CONTENT,
+ REGION,
+ CUE,
+ CUE_TEXT,
+ CUE_METADATA,
+ NOTE
+};
+
+enum class WebvttAlign
+{
+ AUTO = 0,
+ LEFT,
+ CENTER,
+ RIGHT,
+ START,
+ END
+};
+
+enum class WebvttVAlign
+{
+ HORIZONTAL = 0,
+ VERTICAL_RL,
+ VERTICAL_LR,
+};
+
+enum class TextAlignment
+{
+ TOP_LEFT = 0,
+ TOP_CENTER,
+ TOP_RIGHT,
+ SUB_LEFT,
+ SUB_CENTER,
+ SUB_RIGHT
+};
+
+struct webvttAutoValue
+{
+ double value = 0;
+ bool isAuto = true;
+};
+
+struct webvttCueSettings
+{
+ std::string id;
+ std::string regionId;
+ WebvttVAlign verticalAlign;
+ bool snapToLines;
+ webvttAutoValue line;
+ webvttAutoValue position;
+ WebvttAlign positionAlign;
+ double size;
+ WebvttAlign align;
+
+ bool operator==(webvttCueSettings const& other) const
+ {
+ return this->verticalAlign == other.verticalAlign && this->snapToLines == other.snapToLines &&
+ this->line.isAuto == other.line.isAuto && this->line.value == other.line.value &&
+ this->position.isAuto == other.position.isAuto &&
+ this->position.value == other.position.value &&
+ this->positionAlign == other.positionAlign && this->size == other.size &&
+ this->align == other.align;
+ }
+};
+
+struct subtitleData
+{
+ std::string text;
+ std::string textRaw; // Text without tags
+ webvttCueSettings cueSettings;
+ double startTime;
+ double stopTime;
+ bool useMargins;
+ int marginLeft;
+ int marginRight;
+ int marginVertical;
+ TextAlignment textAlign;
+};
+
+enum class WebvttSelector
+{
+ ANY = 0,
+ ID,
+ TYPE,
+ CLASS,
+ ATTRIBUTE,
+ UNSUPPORTED
+};
+
+struct webvttCssStyle
+{
+ webvttCssStyle() {}
+ webvttCssStyle(WebvttSelector selectorType,
+ const std::string& selectorName,
+ const std::string& colorHexRGB)
+ : m_selectorType{selectorType}, m_selectorName{selectorName}
+ {
+ // Color hex values need to be in BGR format
+ m_color = colorHexRGB.substr(4, 2) + colorHexRGB.substr(2, 2) + colorHexRGB.substr(0, 2);
+ }
+
+ WebvttSelector m_selectorType = WebvttSelector::ANY;
+ std::string m_selectorName;
+ std::string m_color;
+ bool m_isFontBold = false;
+ bool m_isFontItalic = false;
+ bool m_isFontUnderline = false;
+};
+
+struct tagToken
+{
+ std::string m_token; // Entire tag
+ std::string m_tag;
+ std::string m_timestampTag;
+ std::string m_annotation;
+ std::vector<std::string> m_classes;
+ bool m_isClosing;
+};
+
+class CWebVTTHandler
+{
+public:
+ CWebVTTHandler(){};
+ ~CWebVTTHandler(){};
+
+ /*!
+ * \brief Prepare the handler to the decoding
+ */
+ bool Initialize();
+
+ /*
+ * \brief Reset handler data
+ */
+ void Reset();
+
+ /*!
+ * \brief Verify the validity of the WebVTT signature
+ */
+ bool CheckSignature(const std::string& data);
+
+ /*!
+ * \brief Decode a line of the WebVTT text data
+ * \param line The line to decode
+ * \param subList The list to be filled with decoded subtitles
+ */
+ void DecodeLine(std::string line, std::vector<subtitleData>* subList);
+
+ /*
+ * \brief Return true if the margins are handled by the parser.
+ */
+ bool IsForcedMargins() const { return !m_overridePositions; }
+
+ /*
+ * \brief Set the period start pts to sync subtitles
+ */
+ void SetPeriodStart(double pts) { m_offset = pts; }
+
+protected:
+ void CalculateTextPosition(std::string& subtitleText);
+ void ConvertSubtitle(std::string& text);
+ void GetCueSettings(std::string& cueSettings);
+ subtitleData m_subtitleData;
+
+private:
+ bool IsCueLine(std::string& line);
+ void GetCueData(std::string& cueText);
+ std::string GetCueSettingValue(const std::string& propName,
+ std::string& text,
+ std::string defaultValue);
+ std::string GetCueCssValue(const std::string& cssPropName, std::string& line);
+ void AddDefaultCssClasses();
+ void InsertCssStyleStartTag(const tagToken& tag,
+ std::string& text,
+ int& pos,
+ int flagTags[],
+ std::deque<std::pair<std::string, webvttCssStyle*>>& cssTagsOpened);
+ void InsertCssStyleCloseTag(const tagToken& tag,
+ std::string& text,
+ int& pos,
+ int flagTags[],
+ std::deque<std::pair<std::string, webvttCssStyle*>>& cssTagsOpened,
+ webvttCssStyle& baseStyle);
+ bool GetBaseStyle(webvttCssStyle& style);
+ void ConvertAddSubtitle(std::vector<subtitleData>* subList);
+ void LoadColors();
+ double GetTimeFromRegexTS(CRegExp& regex, int indexStart = 1);
+
+ // Last subtitle data added, must persist and be updated between all demuxer packages
+ std::unique_ptr<subtitleData> m_lastSubtitleData;
+
+ std::string m_previousLines[3];
+ bool m_overrideStyle{false};
+ bool m_overridePositions{false};
+ WebvttSection m_currentSection{WebvttSection::UNDEFINED};
+ CRegExp m_cueTimeRegex;
+ CRegExp m_timeRegex;
+ std::map<std::string, CRegExp> m_cuePropsMapRegex;
+ CGUIColorManager m_colorManager;
+ CRegExp m_tagsRegex;
+ CRegExp m_cueCssTagRegex;
+ std::map<std::string, CRegExp> m_cueCssStyleMapRegex;
+ std::vector<std::string> m_feedCssSelectorNames;
+ webvttCssStyle m_feedCssStyle;
+ std::map<WebvttSelector, std::map<std::string, webvttCssStyle>> m_cssSelectors;
+
+ bool m_CSSColorsLoaded{false};
+ std::vector<std::pair<std::string, UTILS::COLOR::ColorInfo>> m_CSSColors;
+ double m_offset{0.0};
+};
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTISOHandler.cpp b/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTISOHandler.cpp
new file mode 100644
index 0000000..c19ca35
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTISOHandler.cpp
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2005-2021 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 "WebVTTISOHandler.h"
+
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/CharArrayParser.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StreamUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <cstring>
+#include <stdint.h>
+
+// WebVTT in MP4 encapsulated subtitles (ISO/IEC 14496-30:2014)
+// This format type that differ from the text WebVTT, it makes use
+// of ISO BMFF byte stream where the data are enclosed in boxes (first 4 byte
+// specify the size and next 4 byte specify the type).
+// Start/Stop times info are not included, the start time is the current pts
+// provided from the decoder, the stop time is defined from the pts of the next
+// packages (depends from the box type).
+
+namespace
+{
+constexpr int defaultSubDuration = 20 * static_cast<int>(DVD_TIME_BASE);
+// VTTEmptyCueBox
+constexpr uint32_t ISO_BOX_TYPE_VTTE = StreamUtils::MakeFourCC('v', 't', 't', 'e');
+// VTTCueBox
+constexpr uint32_t ISO_BOX_TYPE_VTTC = StreamUtils::MakeFourCC('v', 't', 't', 'c');
+// VTTContinuationCueBox
+constexpr uint32_t ISO_BOX_TYPE_VTTX = StreamUtils::MakeFourCC('v', 't', 't', 'x');
+// CueIDBox
+constexpr uint32_t ISO_BOX_TYPE_IDEN = StreamUtils::MakeFourCC('i', 'd', 'e', 'n');
+// CueSettingsBox
+constexpr uint32_t ISO_BOX_TYPE_STTG = StreamUtils::MakeFourCC('s', 't', 't', 'g');
+// CuePayloadBox
+constexpr uint32_t ISO_BOX_TYPE_PAYL = StreamUtils::MakeFourCC('p', 'a', 'y', 'l');
+} // unnamed namespace
+
+
+void CWebVTTISOHandler::DecodeStream(const char* buffer,
+ int bufferSize,
+ double pts,
+ std::vector<subtitleData>* subList,
+ double& prevSubStopTime)
+{
+ CCharArrayParser sampleData;
+ sampleData.Reset(buffer, bufferSize);
+
+ // A sample data package can contain:
+ // - One VTTE
+ // or
+ // - One or more VTTC and/or VTTX (where all share the same start/stop times)
+
+ while (sampleData.CharsLeft() > 0)
+ {
+ if (sampleData.CharsLeft() < MP4_BOX_HEADER_SIZE)
+ {
+ CLog::Log(LOGWARNING, "{} - Incomplete box header found", __FUNCTION__);
+ break;
+ }
+
+ uint32_t boxSize = sampleData.ReadNextUnsignedInt();
+ uint32_t boxType = sampleData.ReadNextUnsignedInt();
+ if (boxType == ISO_BOX_TYPE_VTTE)
+ {
+ // VTTE is used to set the stop time value to previously subtitles
+ prevSubStopTime = pts;
+ sampleData.SkipChars(boxSize - MP4_BOX_HEADER_SIZE);
+ }
+ else if (boxType == ISO_BOX_TYPE_VTTC)
+ {
+ // VTTC can be used to set the stop time value to previously subtitles
+ prevSubStopTime = pts;
+ m_subtitleData = subtitleData();
+ m_subtitleData.startTime = pts;
+ m_subtitleData.stopTime = pts + defaultSubDuration;
+ if (ParseVTTCueBox(sampleData, boxSize - MP4_BOX_HEADER_SIZE, subList))
+ subList->emplace_back(m_subtitleData);
+ }
+ else if (boxType == ISO_BOX_TYPE_VTTX)
+ {
+ // VTTX could be used to set the stop time value to previously subtitles
+ prevSubStopTime = pts;
+ m_subtitleData = subtitleData();
+ m_subtitleData.startTime = pts;
+ m_subtitleData.stopTime = pts + defaultSubDuration;
+ if (ParseVTTCueBox(sampleData, boxSize - MP4_BOX_HEADER_SIZE, subList))
+ subList->emplace_back(m_subtitleData);
+ }
+ else
+ {
+ // Skip unsupported box types
+ sampleData.SkipChars(boxSize - MP4_BOX_HEADER_SIZE);
+ }
+ }
+}
+
+bool CWebVTTISOHandler::ParseVTTCueBox(CCharArrayParser& sampleData,
+ int remainingCueBoxChars,
+ std::vector<subtitleData>* subList)
+{
+ std::string cueId;
+ std::string cueSettings;
+ std::string subtitleText;
+
+ // No order is imposed between box types,
+ // so we have to process the data after retrieving them.
+ while (remainingCueBoxChars > 0)
+ {
+ if (remainingCueBoxChars < MP4_BOX_HEADER_SIZE)
+ {
+ CLog::Log(LOGWARNING, "{} - Incomplete VTT Cue box header found", __FUNCTION__);
+ return false;
+ }
+ uint32_t boxSize = sampleData.ReadNextUnsignedInt();
+ uint32_t boxType = sampleData.ReadNextUnsignedInt();
+ int payloadLength = boxSize - MP4_BOX_HEADER_SIZE;
+ remainingCueBoxChars -= MP4_BOX_HEADER_SIZE;
+ remainingCueBoxChars -= payloadLength;
+ std::string payload = sampleData.ReadNextString(payloadLength);
+
+ if (boxType == ISO_BOX_TYPE_IDEN) // Optional
+ {
+ cueId = payload;
+ }
+ else if (boxType == ISO_BOX_TYPE_STTG) // Optional
+ {
+ cueSettings = payload;
+ }
+ else if (boxType == ISO_BOX_TYPE_PAYL)
+ {
+ subtitleText = payload;
+ }
+ }
+
+ m_subtitleData.cueSettings.id = cueId;
+ GetCueSettings(cueSettings);
+ CalculateTextPosition(subtitleText);
+ ConvertSubtitle(subtitleText);
+ m_subtitleData.text = subtitleText;
+
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTISOHandler.h b/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTISOHandler.h
new file mode 100644
index 0000000..b2eb073
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/DVDSubtitles/webvtt/WebVTTISOHandler.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2005-2021 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 "WebVTTHandler.h"
+
+class CCharArrayParser;
+
+
+class CWebVTTISOHandler : public CWebVTTHandler
+{
+public:
+ CWebVTTISOHandler(){};
+ ~CWebVTTISOHandler(){};
+
+ /*!
+ * \brief Decode a stream package of the WebVTT in MP4 encapsulated subtitles
+ * (ISO/IEC 14496-30:2014)
+ * \param buffer The data buffer
+ * \param bufferSize The buffer size
+ * \param subList The list to be filled with decoded subtitles
+ * \param[out] prevSubStopTime Provide the stop time value (depends on box type)
+ */
+ void DecodeStream(const char* buffer,
+ int bufferSize,
+ double pts,
+ std::vector<subtitleData>* subList,
+ double& prevSubStopTime);
+
+private:
+ bool ParseVTTCueBox(CCharArrayParser& sampleData,
+ int remainingCueBoxChars,
+ std::vector<subtitleData>* subList);
+};
diff --git a/xbmc/cores/VideoPlayer/Edl.cpp b/xbmc/cores/VideoPlayer/Edl.cpp
new file mode 100644
index 0000000..51f7ef7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Edl.cpp
@@ -0,0 +1,1040 @@
+/*
+ * 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 "Edl.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "cores/EdlEdit.h"
+#include "filesystem/File.h"
+#include "pvr/PVREdl.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include "PlatformDefs.h"
+
+#define COMSKIP_HEADER "FILE PROCESSING COMPLETE"
+#define VIDEOREDO_HEADER "<Version>2"
+#define VIDEOREDO_TAG_CUT "<Cut>"
+#define VIDEOREDO_TAG_SCENE "<SceneMarker "
+
+using namespace EDL;
+using namespace XFILE;
+
+CEdl::CEdl()
+{
+ Clear();
+}
+
+void CEdl::Clear()
+{
+ m_vecEdits.clear();
+ m_vecSceneMarkers.clear();
+ m_totalCutTime = 0;
+ m_lastEditTime = -1;
+}
+
+bool CEdl::ReadEditDecisionLists(const CFileItem& fileItem, const float fFramesPerSecond)
+{
+ bool bFound = false;
+
+ /*
+ * Only check for edit decision lists if the movie is on the local hard drive, or accessed over a
+ * network share.
+ */
+ const std::string& strMovie = fileItem.GetDynPath();
+ if ((URIUtils::IsHD(strMovie) || URIUtils::IsOnLAN(strMovie)) &&
+ !URIUtils::IsInternetStream(strMovie))
+ {
+ CLog::Log(LOGDEBUG,
+ "{} - Checking for edit decision lists (EDL) on local drive or remote share for: {}",
+ __FUNCTION__, CURL::GetRedacted(strMovie));
+
+ /*
+ * Read any available file format until a valid EDL related file is found.
+ */
+ if (!bFound)
+ bFound = ReadVideoReDo(strMovie);
+
+ if (!bFound)
+ bFound = ReadEdl(strMovie, fFramesPerSecond);
+
+ if (!bFound)
+ bFound = ReadComskip(strMovie, fFramesPerSecond);
+
+ if (!bFound)
+ bFound = ReadBeyondTV(strMovie);
+ }
+ else
+ {
+ bFound = ReadPvr(fileItem);
+ }
+
+ if (bFound)
+ {
+ MergeShortCommBreaks();
+ AddSceneMarkersAtStartAndEndOfEdits();
+ }
+
+ return bFound;
+}
+
+bool CEdl::ReadEdl(const std::string& strMovie, const float fFramesPerSecond)
+{
+ Clear();
+
+ std::string edlFilename(URIUtils::ReplaceExtension(strMovie, ".edl"));
+ if (!CFile::Exists(edlFilename))
+ return false;
+
+ CFile edlFile;
+ if (!edlFile.Open(edlFilename))
+ {
+ CLog::Log(LOGERROR, "{} - Could not open EDL file: {}", __FUNCTION__,
+ CURL::GetRedacted(edlFilename));
+ return false;
+ }
+
+ bool bError = false;
+ int iLine = 0;
+ std::string strBuffer;
+ strBuffer.resize(1024);
+ while (edlFile.ReadString(&strBuffer[0], 1024))
+ {
+ // Log any errors from previous run in the loop
+ if (bError)
+ CLog::Log(LOGWARNING, "{} - Error on line {} in EDL file: {}", __FUNCTION__, iLine,
+ CURL::GetRedacted(edlFilename));
+
+ bError = false;
+
+ iLine++;
+
+ char buffer1[513];
+ char buffer2[513];
+ int iAction;
+ int iFieldsRead = sscanf(strBuffer.c_str(), "%512s %512s %i", buffer1,
+ buffer2, &iAction);
+ if (iFieldsRead != 2 && iFieldsRead != 3) // Make sure we read the right number of fields
+ {
+ bError = true;
+ continue;
+ }
+
+ std::vector<std::string> strFields(2);
+ strFields[0] = buffer1;
+ strFields[1] = buffer2;
+
+ if (iFieldsRead == 2) // If only 2 fields read, then assume it's a scene marker.
+ {
+ iAction = atoi(strFields[1].c_str());
+ strFields[1] = strFields[0];
+ }
+
+ if (StringUtils::StartsWith(strFields[0], "##"))
+ {
+ CLog::Log(LOGDEBUG, "Skipping comment line {} in EDL file: {}", iLine,
+ CURL::GetRedacted(edlFilename));
+ continue;
+ }
+ /*
+ * For each of the first two fields read, parse based on whether it is a time string
+ * (HH:MM:SS.sss), frame marker (#12345), or normal seconds string (123.45).
+ */
+ int64_t editStartEnd[2];
+ for (int i = 0; i < 2; i++)
+ {
+ if (strFields[i].find(':') != std::string::npos) // HH:MM:SS.sss format
+ {
+ std::vector<std::string> fieldParts = StringUtils::Split(strFields[i], '.');
+ if (fieldParts.size() == 1) // No ms
+ {
+ editStartEnd[i] = StringUtils::TimeStringToSeconds(fieldParts[0]) *
+ static_cast<int64_t>(1000); // seconds to ms
+ }
+ else if (fieldParts.size() == 2) // Has ms. Everything after the dot (.) is ms
+ {
+ /*
+ * Have to pad or truncate the ms portion to 3 characters before converting to ms.
+ */
+ if (fieldParts[1].length() == 1)
+ {
+ fieldParts[1] = fieldParts[1] + "00";
+ }
+ else if (fieldParts[1].length() == 2)
+ {
+ fieldParts[1] = fieldParts[1] + "0";
+ }
+ else if (fieldParts[1].length() > 3)
+ {
+ fieldParts[1] = fieldParts[1].substr(0, 3);
+ }
+ editStartEnd[i] =
+ static_cast<int64_t>(StringUtils::TimeStringToSeconds(fieldParts[0])) * 1000 +
+ std::atoi(fieldParts[1].c_str()); // seconds to ms
+ }
+ else
+ {
+ bError = true;
+ continue;
+ }
+ }
+ else if (strFields[i][0] == '#') // #12345 format for frame number
+ {
+ if (fFramesPerSecond > 0.0f)
+ {
+ editStartEnd[i] = static_cast<int64_t>(std::atol(strFields[i].substr(1).c_str()) /
+ fFramesPerSecond * 1000); // frame number to ms
+ }
+ else
+ {
+ CLog::Log(LOGERROR,
+ "Edl::ReadEdl - Frame number not supported in EDL files when frame rate is "
+ "unavailable (ts) - supplied frame number: {}",
+ strFields[i].substr(1));
+ return false;
+ }
+ }
+ else // Plain old seconds in float format, e.g. 123.45
+ {
+ editStartEnd[i] = std::lround(std::atof(strFields[i].c_str()) * 1000); // seconds to ms
+ }
+ }
+
+ if (bError) // If there was an error in the for loop, ignore and continue with the next line
+ continue;
+
+ Edit edit;
+ edit.start = editStartEnd[0];
+ edit.end = editStartEnd[1];
+
+ switch (iAction)
+ {
+ case 0:
+ edit.action = Action::CUT;
+ if (!AddEdit(edit))
+ {
+ CLog::Log(LOGWARNING, "{} - Error adding cut from line {} in EDL file: {}", __FUNCTION__,
+ iLine, CURL::GetRedacted(edlFilename));
+ continue;
+ }
+ break;
+ case 1:
+ edit.action = Action::MUTE;
+ if (!AddEdit(edit))
+ {
+ CLog::Log(LOGWARNING, "{} - Error adding mute from line {} in EDL file: {}", __FUNCTION__,
+ iLine, CURL::GetRedacted(edlFilename));
+ continue;
+ }
+ break;
+ case 2:
+ if (!AddSceneMarker(edit.end))
+ {
+ CLog::Log(LOGWARNING, "{} - Error adding scene marker from line {} in EDL file: {}",
+ __FUNCTION__, iLine, CURL::GetRedacted(edlFilename));
+ continue;
+ }
+ break;
+ case 3:
+ edit.action = Action::COMM_BREAK;
+ if (!AddEdit(edit))
+ {
+ CLog::Log(LOGWARNING, "{} - Error adding commercial break from line {} in EDL file: {}",
+ __FUNCTION__, iLine, CURL::GetRedacted(edlFilename));
+ continue;
+ }
+ break;
+ default:
+ CLog::Log(LOGWARNING, "{} - Invalid action on line {} in EDL file: {}", __FUNCTION__, iLine,
+ CURL::GetRedacted(edlFilename));
+ continue;
+ }
+ }
+
+ if (bError) // Log last line warning, if there was one, since while loop will have terminated.
+ CLog::Log(LOGWARNING, "{} - Error on line {} in EDL file: {}", __FUNCTION__, iLine,
+ CURL::GetRedacted(edlFilename));
+
+ edlFile.Close();
+
+ if (HasEdits() || HasSceneMarker())
+ {
+ CLog::Log(LOGDEBUG, "{0} - Read {1} edits and {2} scene markers in EDL file: {3}", __FUNCTION__,
+ m_vecEdits.size(), m_vecSceneMarkers.size(), CURL::GetRedacted(edlFilename));
+ return true;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - No edits or scene markers found in EDL file: {}", __FUNCTION__,
+ CURL::GetRedacted(edlFilename));
+ return false;
+ }
+}
+
+bool CEdl::ReadComskip(const std::string& strMovie, const float fFramesPerSecond)
+{
+ Clear();
+
+ std::string comskipFilename(URIUtils::ReplaceExtension(strMovie, ".txt"));
+ if (!CFile::Exists(comskipFilename))
+ return false;
+
+ CFile comskipFile;
+ if (!comskipFile.Open(comskipFilename))
+ {
+ CLog::Log(LOGERROR, "{} - Could not open Comskip file: {}", __FUNCTION__,
+ CURL::GetRedacted(comskipFilename));
+ return false;
+ }
+
+ char szBuffer[1024];
+ if (comskipFile.ReadString(szBuffer, 1023)
+ && strncmp(szBuffer, COMSKIP_HEADER, strlen(COMSKIP_HEADER)) != 0) // Line 1.
+ {
+ CLog::Log(LOGERROR,
+ "{} - Invalid Comskip file: {}. Error reading line 1 - expected '{}' at start.",
+ __FUNCTION__, CURL::GetRedacted(comskipFilename), COMSKIP_HEADER);
+ comskipFile.Close();
+ return false;
+ }
+
+ int iFrames;
+ float fFrameRate;
+ if (sscanf(szBuffer, "FILE PROCESSING COMPLETE %i FRAMES AT %f", &iFrames, &fFrameRate) != 2)
+ {
+ /*
+ * Not all generated Comskip files have the frame rate information.
+ */
+ if (fFramesPerSecond > 0.0f)
+ {
+ fFrameRate = fFramesPerSecond;
+ CLog::Log(LOGWARNING,
+ "Edl::ReadComskip - Frame rate not in Comskip file. Using detected frames per "
+ "second: {:.3f}",
+ fFrameRate);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Edl::ReadComskip - Frame rate is unavailable and also not in Comskip file (ts).");
+ return false;
+ }
+ }
+ else
+ fFrameRate /= 100; // Reduce by factor of 100 to get fps.
+
+ (void)comskipFile.ReadString(szBuffer, 1023); // Line 2. Ignore "-------------"
+
+ bool bValid = true;
+ int iLine = 2;
+ while (bValid && comskipFile.ReadString(szBuffer, 1023)) // Line 3 and onwards.
+ {
+ iLine++;
+ double dStartFrame, dEndFrame;
+ if (sscanf(szBuffer, "%lf %lf", &dStartFrame, &dEndFrame) == 2)
+ {
+ Edit edit;
+ edit.start = std::lround(dStartFrame / static_cast<double>(fFrameRate) * 1000.0);
+ edit.end = std::lround(dEndFrame / static_cast<double>(fFrameRate) * 1000.0);
+ edit.action = Action::COMM_BREAK;
+ bValid = AddEdit(edit);
+ }
+ else
+ bValid = false;
+ }
+ comskipFile.Close();
+
+ if (!bValid)
+ {
+ CLog::Log(LOGERROR,
+ "{} - Invalid Comskip file: {}. Error on line {}. Clearing any valid commercial "
+ "breaks found.",
+ __FUNCTION__, CURL::GetRedacted(comskipFilename), iLine);
+ Clear();
+ return false;
+ }
+ else if (HasEdits())
+ {
+ CLog::Log(LOGDEBUG, "{0} - Read {1} commercial breaks from Comskip file: {2}", __FUNCTION__,
+ m_vecEdits.size(), CURL::GetRedacted(comskipFilename));
+ return true;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - No commercial breaks found in Comskip file: {}", __FUNCTION__,
+ CURL::GetRedacted(comskipFilename));
+ return false;
+ }
+}
+
+bool CEdl::ReadVideoReDo(const std::string& strMovie)
+{
+ /*
+ * VideoReDo file is strange. Tags are XML like, but it isn't an XML file.
+ *
+ * http://www.videoredo.com/
+ */
+
+ Clear();
+ std::string videoReDoFilename(URIUtils::ReplaceExtension(strMovie, ".Vprj"));
+ if (!CFile::Exists(videoReDoFilename))
+ return false;
+
+ CFile videoReDoFile;
+ if (!videoReDoFile.Open(videoReDoFilename))
+ {
+ CLog::Log(LOGERROR, "{} - Could not open VideoReDo file: {}", __FUNCTION__,
+ CURL::GetRedacted(videoReDoFilename));
+ return false;
+ }
+
+ char szBuffer[1024];
+ if (videoReDoFile.ReadString(szBuffer, 1023)
+ && strncmp(szBuffer, VIDEOREDO_HEADER, strlen(VIDEOREDO_HEADER)) != 0)
+ {
+ CLog::Log(LOGERROR,
+ "{} - Invalid VideoReDo file: {}. Error reading line 1 - expected {}. Only version 2 "
+ "files are supported.",
+ __FUNCTION__, CURL::GetRedacted(videoReDoFilename), VIDEOREDO_HEADER);
+ videoReDoFile.Close();
+ return false;
+ }
+
+ int iLine = 1;
+ bool bValid = true;
+ while (bValid && videoReDoFile.ReadString(szBuffer, 1023))
+ {
+ iLine++;
+ if (strncmp(szBuffer, VIDEOREDO_TAG_CUT, strlen(VIDEOREDO_TAG_CUT)) == 0) // Found the <Cut> tag
+ {
+ /*
+ * double is used as 32 bit float would overflow.
+ */
+ double dStart, dEnd;
+ if (sscanf(szBuffer + strlen(VIDEOREDO_TAG_CUT), "%lf:%lf", &dStart, &dEnd) == 2)
+ {
+ /*
+ * Times need adjusting by 1/10,000 to get ms.
+ */
+ Edit edit;
+ edit.start = std::lround(dStart / 10000);
+ edit.end = std::lround(dEnd / 10000);
+ edit.action = Action::CUT;
+ bValid = AddEdit(edit);
+ }
+ else
+ bValid = false;
+ }
+ else if (strncmp(szBuffer, VIDEOREDO_TAG_SCENE, strlen(VIDEOREDO_TAG_SCENE)) == 0) // Found the <SceneMarker > tag
+ {
+ int iScene;
+ double dSceneMarker;
+ if (sscanf(szBuffer + strlen(VIDEOREDO_TAG_SCENE), " %i>%lf", &iScene, &dSceneMarker) == 2)
+ bValid = AddSceneMarker(
+ std::lround(dSceneMarker / 10000)); // Times need adjusting by 1/10,000 to get ms.
+ else
+ bValid = false;
+ }
+ /*
+ * Ignore any other tags.
+ */
+ }
+ videoReDoFile.Close();
+
+ if (!bValid)
+ {
+ CLog::Log(LOGERROR,
+ "{} - Invalid VideoReDo file: {}. Error in line {}. Clearing any valid edits or "
+ "scenes found.",
+ __FUNCTION__, CURL::GetRedacted(videoReDoFilename), iLine);
+ Clear();
+ return false;
+ }
+ else if (HasEdits() || HasSceneMarker())
+ {
+ CLog::Log(LOGDEBUG, "{0} - Read {1} edits and {2} scene markers in VideoReDo file: {3}",
+ __FUNCTION__, m_vecEdits.size(), m_vecSceneMarkers.size(),
+ CURL::GetRedacted(videoReDoFilename));
+ return true;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - No edits or scene markers found in VideoReDo file: {}", __FUNCTION__,
+ CURL::GetRedacted(videoReDoFilename));
+ return false;
+ }
+}
+
+bool CEdl::ReadBeyondTV(const std::string& strMovie)
+{
+ Clear();
+
+ std::string beyondTVFilename(URIUtils::ReplaceExtension(strMovie, URIUtils::GetExtension(strMovie) + ".chapters.xml"));
+ if (!CFile::Exists(beyondTVFilename))
+ return false;
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(beyondTVFilename))
+ {
+ CLog::Log(LOGERROR, "{} - Could not load Beyond TV file: {}. {}", __FUNCTION__,
+ CURL::GetRedacted(beyondTVFilename), xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ if (xmlDoc.Error())
+ {
+ CLog::Log(LOGERROR, "{} - Could not parse Beyond TV file: {}. {}", __FUNCTION__,
+ CURL::GetRedacted(beyondTVFilename), xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement *pRoot = xmlDoc.RootElement();
+ if (!pRoot || strcmp(pRoot->Value(), "cutlist"))
+ {
+ CLog::Log(LOGERROR, "{} - Invalid Beyond TV file: {}. Expected root node to be <cutlist>",
+ __FUNCTION__, CURL::GetRedacted(beyondTVFilename));
+ return false;
+ }
+
+ bool bValid = true;
+ TiXmlElement *pRegion = pRoot->FirstChildElement("Region");
+ while (bValid && pRegion)
+ {
+ TiXmlElement *pStart = pRegion->FirstChildElement("start");
+ TiXmlElement *pEnd = pRegion->FirstChildElement("end");
+ if (pStart && pEnd && pStart->FirstChild() && pEnd->FirstChild())
+ {
+ /*
+ * Need to divide the start and end times by a factor of 10,000 to get msec.
+ * E.g. <start comment="00:02:44.9980867">1649980867</start>
+ *
+ * Use atof so doesn't overflow 32 bit float or integer / long.
+ * E.g. <end comment="0:26:49.0000009">16090090000</end>
+ *
+ * Don't use atoll even though it is more correct as it isn't natively supported by
+ * Visual Studio.
+ *
+ * atof() returns 0 if there were any problems and will subsequently be rejected in AddEdit().
+ */
+ Edit edit;
+ edit.start = std::lround((std::atof(pStart->FirstChild()->Value()) / 10000));
+ edit.end = std::lround((std::atof(pEnd->FirstChild()->Value()) / 10000));
+ edit.action = Action::COMM_BREAK;
+ bValid = AddEdit(edit);
+ }
+ else
+ bValid = false;
+
+ pRegion = pRegion->NextSiblingElement("Region");
+ }
+ if (!bValid)
+ {
+ CLog::Log(LOGERROR,
+ "{} - Invalid Beyond TV file: {}. Clearing any valid commercial breaks found.",
+ __FUNCTION__, CURL::GetRedacted(beyondTVFilename));
+ Clear();
+ return false;
+ }
+ else if (HasEdits())
+ {
+ CLog::Log(LOGDEBUG, "{0} - Read {1} commercial breaks from Beyond TV file: {2}", __FUNCTION__,
+ m_vecEdits.size(), CURL::GetRedacted(beyondTVFilename));
+ return true;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - No commercial breaks found in Beyond TV file: {}", __FUNCTION__,
+ CURL::GetRedacted(beyondTVFilename));
+ return false;
+ }
+}
+
+bool CEdl::ReadPvr(const CFileItem &fileItem)
+{
+ const std::vector<Edit> editlist = PVR::CPVREdl::GetEdits(fileItem);
+ for (const auto& edit : editlist)
+ {
+ switch (edit.action)
+ {
+ case Action::CUT:
+ case Action::MUTE:
+ case Action::COMM_BREAK:
+ if (AddEdit(edit))
+ {
+ CLog::Log(LOGDEBUG, "{} - Added break [{} - {}] found in PVR item for: {}.", __FUNCTION__,
+ MillisecondsToTimeString(edit.start), MillisecondsToTimeString(edit.end),
+ CURL::GetRedacted(fileItem.GetDynPath()));
+ }
+ else
+ {
+ CLog::Log(LOGERROR,
+ "{} - Invalid break [{} - {}] found in PVR item for: {}. Continuing anyway.",
+ __FUNCTION__, MillisecondsToTimeString(edit.start),
+ MillisecondsToTimeString(edit.end), CURL::GetRedacted(fileItem.GetDynPath()));
+ }
+ break;
+
+ case Action::SCENE:
+ if (!AddSceneMarker(edit.end))
+ {
+ CLog::Log(LOGWARNING, "{} - Error adding scene marker for PVR item", __FUNCTION__);
+ }
+ break;
+
+ default:
+ CLog::Log(LOGINFO, "{} - Ignoring entry of unknown edit action: {}", __FUNCTION__,
+ static_cast<int>(edit.action));
+ break;
+ }
+ }
+
+ return !editlist.empty();
+}
+
+bool CEdl::AddEdit(const Edit& newEdit)
+{
+ Edit edit = newEdit;
+
+ if (edit.action != Action::CUT && edit.action != Action::MUTE &&
+ edit.action != Action::COMM_BREAK)
+ {
+ CLog::Log(LOGERROR,
+ "{} - Not an Action::CUT, Action::MUTE, or Action::COMM_BREAK! [{} - {}], {}",
+ __FUNCTION__, MillisecondsToTimeString(edit.start),
+ MillisecondsToTimeString(edit.end), static_cast<int>(edit.action));
+ return false;
+ }
+
+ if (edit.start < 0)
+ {
+ CLog::Log(LOGERROR, "{} - Before start! [{} - {}], {}", __FUNCTION__,
+ MillisecondsToTimeString(edit.start), MillisecondsToTimeString(edit.end),
+ static_cast<int>(edit.action));
+ return false;
+ }
+
+ if (edit.start >= edit.end)
+ {
+ CLog::Log(LOGERROR, "{} - Times are around the wrong way or the same! [{} - {}], {}",
+ __FUNCTION__, MillisecondsToTimeString(edit.start),
+ MillisecondsToTimeString(edit.end), static_cast<int>(edit.action));
+ return false;
+ }
+
+ if (InEdit(edit.start) || InEdit(edit.end))
+ {
+ CLog::Log(LOGERROR, "{} - Start or end is in an existing edit! [{} - {}], {}", __FUNCTION__,
+ MillisecondsToTimeString(edit.start), MillisecondsToTimeString(edit.end),
+ static_cast<int>(edit.action));
+ return false;
+ }
+
+ for (size_t i = 0; i < m_vecEdits.size(); ++i)
+ {
+ if (edit.start < m_vecEdits[i].start && edit.end > m_vecEdits[i].end)
+ {
+ CLog::Log(LOGERROR, "{} - Edit surrounds an existing edit! [{} - {}], {}", __FUNCTION__,
+ MillisecondsToTimeString(edit.start), MillisecondsToTimeString(edit.end),
+ static_cast<int>(edit.action));
+ return false;
+ }
+ }
+
+ if (edit.action == Action::COMM_BREAK)
+ {
+ /*
+ * Detection isn't perfect near the edges of commercial breaks so automatically wait for a bit at
+ * the start (autowait) and automatically rewind by a bit (autowind) at the end of the commercial
+ * break.
+ */
+ int autowait = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iEdlCommBreakAutowait * 1000; // seconds -> ms
+ int autowind = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iEdlCommBreakAutowind * 1000; // seconds -> ms
+
+ if (edit.start > 0) // Only autowait if not at the start.
+ {
+ /* get the edit length so we don't start skipping after the end */
+ int editLength = edit.end - edit.start;
+ /* add the lesser of the edit length or the autowait to the start */
+ edit.start += autowait > editLength ? editLength : autowait;
+ }
+ if (edit.end > edit.start) // Only autowind if there is any edit time remaining.
+ {
+ /* get the remaining edit length so we don't rewind to before the start */
+ int editLength = edit.end - edit.start;
+ /* subtract the lesser of the edit length or the autowind from the end */
+ edit.end -= autowind > editLength ? editLength : autowind;
+ }
+ }
+
+ /*
+ * Insert edit in the list in the right position (ALL algorithms assume edits are in ascending order)
+ */
+ if (m_vecEdits.empty() || edit.start > m_vecEdits.back().start)
+ {
+ CLog::Log(LOGDEBUG, "{} - Pushing new edit to back [{} - {}], {}", __FUNCTION__,
+ MillisecondsToTimeString(edit.start), MillisecondsToTimeString(edit.end),
+ static_cast<int>(edit.action));
+ m_vecEdits.emplace_back(edit);
+ }
+ else
+ {
+ std::vector<Edit>::iterator pCurrentEdit;
+ for (pCurrentEdit = m_vecEdits.begin(); pCurrentEdit != m_vecEdits.end(); ++pCurrentEdit)
+ {
+ if (edit.start < pCurrentEdit->start)
+ {
+ CLog::Log(LOGDEBUG, "{} - Inserting new edit [{} - {}], {}", __FUNCTION__,
+ MillisecondsToTimeString(edit.start), MillisecondsToTimeString(edit.end),
+ static_cast<int>(edit.action));
+ m_vecEdits.insert(pCurrentEdit, edit);
+ break;
+ }
+ }
+ }
+
+ if (edit.action == Action::CUT)
+ m_totalCutTime += edit.end - edit.start;
+
+ return true;
+}
+
+bool CEdl::AddSceneMarker(const int iSceneMarker)
+{
+ Edit edit;
+
+ if (InEdit(iSceneMarker, &edit) && edit.action == Action::CUT) // Only works for current cuts.
+ return false;
+
+ CLog::Log(LOGDEBUG, "{} - Inserting new scene marker: {}", __FUNCTION__,
+ MillisecondsToTimeString(iSceneMarker));
+ m_vecSceneMarkers.push_back(iSceneMarker); // Unsorted
+
+ return true;
+}
+
+bool CEdl::HasEdits() const
+{
+ return !m_vecEdits.empty();
+}
+
+bool CEdl::HasCuts() const
+{
+ return m_totalCutTime > 0;
+}
+
+int CEdl::GetTotalCutTime() const
+{
+ return m_totalCutTime; // ms
+}
+
+const std::vector<EDL::Edit> CEdl::GetEditList() const
+{
+ // the sum of cut durations while we iterate over them
+ // note: edits are ordered by start time
+ int surpassedSumOfCutDurations{0};
+ std::vector<EDL::Edit> editList;
+
+ // @note we should not modify the original edits since
+ // they are used during playback. However we need to correct
+ // the start and end times to present on the GUI by removing
+ // the already surpassed cut time. The copy here is intentional
+ // \sa Player_Editlist
+ for (EDL::Edit edit : m_vecEdits)
+ {
+ if (edit.action == Action::CUT)
+ {
+ surpassedSumOfCutDurations += edit.end - edit.start;
+ continue;
+ }
+
+ // substract the duration of already surpassed cuts
+ edit.start -= surpassedSumOfCutDurations;
+ edit.end -= surpassedSumOfCutDurations;
+ editList.emplace_back(edit);
+ }
+
+ return editList;
+}
+
+const std::vector<int64_t> CEdl::GetCutMarkers() const
+{
+ int surpassedSumOfCutDurations{0};
+ std::vector<int64_t> cutList;
+ for (const EDL::Edit& edit : m_vecEdits)
+ {
+ if (edit.action != Action::CUT)
+ continue;
+
+ cutList.emplace_back(edit.start - surpassedSumOfCutDurations);
+ surpassedSumOfCutDurations += edit.end - edit.start;
+ }
+ return cutList;
+}
+
+const std::vector<int64_t> CEdl::GetSceneMarkers() const
+{
+ std::vector<int64_t> sceneMarkers;
+ sceneMarkers.reserve(m_vecSceneMarkers.size());
+ for (const int& scene : m_vecSceneMarkers)
+ {
+ sceneMarkers.emplace_back(GetTimeWithoutCuts(scene));
+ }
+ return sceneMarkers;
+}
+
+int CEdl::GetTimeWithoutCuts(int seek) const
+{
+ if (!HasCuts())
+ return seek;
+
+ int cutTime = 0;
+ for (const EDL::Edit& edit : m_vecEdits)
+ {
+ if (edit.action != Action::CUT)
+ continue;
+
+ // inside cut
+ if (seek >= edit.start && seek <= edit.end)
+ {
+ // decrease cut lenght by 1 ms to jump over the end boundary.
+ cutTime += seek - edit.start - 1;
+ }
+ // cut has already been passed over
+ else if (seek >= edit.start)
+ {
+ cutTime += edit.end - edit.start;
+ }
+ }
+ return seek - cutTime;
+}
+
+double CEdl::GetTimeAfterRestoringCuts(double seek) const
+{
+ if (!HasCuts())
+ return seek;
+
+ for (const EDL::Edit& edit : m_vecEdits)
+ {
+ double cutDuration = static_cast<double>(edit.end - edit.start);
+ // add 1 ms to jump over the start boundary
+ if (edit.action == Action::CUT && seek > edit.start + 1)
+ {
+ seek += cutDuration;
+ }
+ }
+ return seek;
+}
+
+bool CEdl::HasSceneMarker() const
+{
+ return !m_vecSceneMarkers.empty();
+}
+
+bool CEdl::InEdit(const int iSeek, Edit* pEdit)
+{
+ for (size_t i = 0; i < m_vecEdits.size(); ++i)
+ {
+ if (iSeek < m_vecEdits[i].start) // Early exit if not even up to the edit start time.
+ return false;
+
+ if (iSeek >= m_vecEdits[i].start && iSeek <= m_vecEdits[i].end) // Inside edit.
+ {
+ if (pEdit)
+ *pEdit = m_vecEdits[i];
+ return true;
+ }
+ }
+
+ return false;
+}
+
+int CEdl::GetLastEditTime() const
+{
+ return m_lastEditTime;
+}
+
+void CEdl::SetLastEditTime(int editTime)
+{
+ m_lastEditTime = editTime;
+}
+
+void CEdl::ResetLastEditTime()
+{
+ m_lastEditTime = -1;
+}
+
+void CEdl::SetLastEditActionType(EDL::Action action)
+{
+ m_lastEditActionType = action;
+}
+
+EDL::Action CEdl::GetLastEditActionType() const
+{
+ return m_lastEditActionType;
+}
+
+bool CEdl::GetNextSceneMarker(bool bPlus, const int iClock, int *iSceneMarker)
+{
+ if (!HasSceneMarker())
+ return false;
+
+ int iSeek = GetTimeAfterRestoringCuts(iClock);
+
+ int iDiff = 10 * 60 * 60 * 1000; // 10 hours to ms.
+ bool bFound = false;
+
+ if (bPlus) // Find closest scene forwards
+ {
+ for (int i = 0; i < (int)m_vecSceneMarkers.size(); i++)
+ {
+ if ((m_vecSceneMarkers[i] > iSeek) && ((m_vecSceneMarkers[i] - iSeek) < iDiff))
+ {
+ iDiff = m_vecSceneMarkers[i] - iSeek;
+ *iSceneMarker = m_vecSceneMarkers[i];
+ bFound = true;
+ }
+ }
+ }
+ else // Find closest scene backwards
+ {
+ for (int i = 0; i < (int)m_vecSceneMarkers.size(); i++)
+ {
+ if ((m_vecSceneMarkers[i] < iSeek) && ((iSeek - m_vecSceneMarkers[i]) < iDiff))
+ {
+ iDiff = iSeek - m_vecSceneMarkers[i];
+ *iSceneMarker = m_vecSceneMarkers[i];
+ bFound = true;
+ }
+ }
+ }
+
+ /*
+ * If the scene marker is in a cut then return the end of the cut. Can't guarantee that this is
+ * picked up when scene markers are added.
+ */
+ Edit edit;
+ if (bFound && InEdit(*iSceneMarker, &edit) && edit.action == Action::CUT)
+ *iSceneMarker = edit.end;
+
+ return bFound;
+}
+
+std::string CEdl::MillisecondsToTimeString(const int iMilliseconds)
+{
+ std::string strTimeString = StringUtils::SecondsToTimeString((long)(iMilliseconds / 1000), TIME_FORMAT_HH_MM_SS); // milliseconds to seconds
+ strTimeString += StringUtils::Format(".{:03}", iMilliseconds % 1000);
+ return strTimeString;
+}
+
+void CEdl::MergeShortCommBreaks()
+{
+ /*
+ * mythcommflag routinely seems to put a 20-40ms commercial break at the start of the recording.
+ *
+ * Remove any spurious short commercial breaks at the very start so they don't interfere with
+ * the algorithms below.
+ */
+ if (!m_vecEdits.empty() && m_vecEdits[0].action == Action::COMM_BREAK &&
+ (m_vecEdits[0].end - m_vecEdits[0].start) < 5 * 1000) // 5 seconds
+ {
+ CLog::Log(LOGDEBUG, "{} - Removing short commercial break at start [{} - {}]. <5 seconds",
+ __FUNCTION__, MillisecondsToTimeString(m_vecEdits[0].start),
+ MillisecondsToTimeString(m_vecEdits[0].end));
+ m_vecEdits.erase(m_vecEdits.begin());
+ }
+
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+ if (advancedSettings->m_bEdlMergeShortCommBreaks)
+ {
+ for (size_t i = 0; i < m_vecEdits.size() - 1; ++i)
+ {
+ if ((m_vecEdits[i].action == Action::COMM_BREAK &&
+ m_vecEdits[i + 1].action == Action::COMM_BREAK) &&
+ (m_vecEdits[i + 1].end - m_vecEdits[i].start <
+ advancedSettings->m_iEdlMaxCommBreakLength * 1000) // s to ms
+ && (m_vecEdits[i + 1].start - m_vecEdits[i].end <
+ advancedSettings->m_iEdlMaxCommBreakGap * 1000)) // s to ms
+ {
+ Edit commBreak;
+ commBreak.action = Action::COMM_BREAK;
+ commBreak.start = m_vecEdits[i].start;
+ commBreak.end = m_vecEdits[i + 1].end;
+
+ CLog::Log(
+ LOGDEBUG, "{} - Consolidating commercial break [{} - {}] and [{} - {}] to: [{} - {}]",
+ __FUNCTION__, MillisecondsToTimeString(m_vecEdits[i].start),
+ MillisecondsToTimeString(m_vecEdits[i].end),
+ MillisecondsToTimeString(m_vecEdits[i + 1].start),
+ MillisecondsToTimeString(m_vecEdits[i + 1].end),
+ MillisecondsToTimeString(commBreak.start), MillisecondsToTimeString(commBreak.end));
+
+ /*
+ * Erase old edits and insert the new merged one.
+ */
+ m_vecEdits.erase(m_vecEdits.begin() + i, m_vecEdits.begin() + i + 2);
+ m_vecEdits.insert(m_vecEdits.begin() + i, commBreak);
+
+ i--; // Reduce i to see if the next break is also within the max commercial break length.
+ }
+ }
+
+ /*
+ * To cater for recordings that are started early and then have a commercial break identified
+ * before the TV show starts, expand the first commercial break to the very beginning if it
+ * starts within the maximum start gap. This is done outside of the consolidation to prevent
+ * the maximum commercial break length being triggered.
+ */
+ if (!m_vecEdits.empty() && m_vecEdits[0].action == Action::COMM_BREAK &&
+ m_vecEdits[0].start < advancedSettings->m_iEdlMaxStartGap * 1000)
+ {
+ CLog::Log(LOGDEBUG, "{} - Expanding first commercial break back to start [{} - {}].",
+ __FUNCTION__, MillisecondsToTimeString(m_vecEdits[0].start),
+ MillisecondsToTimeString(m_vecEdits[0].end));
+ m_vecEdits[0].start = 0;
+ }
+
+ /*
+ * Remove any commercial breaks shorter than the minimum (unless at the start)
+ */
+ for (size_t i = 0; i < m_vecEdits.size(); ++i)
+ {
+ if (m_vecEdits[i].action == Action::COMM_BREAK && m_vecEdits[i].start > 0 &&
+ (m_vecEdits[i].end - m_vecEdits[i].start) <
+ advancedSettings->m_iEdlMinCommBreakLength * 1000)
+ {
+ CLog::Log(LOGDEBUG,
+ "{} - Removing short commercial break [{} - {}]. Minimum length: {} seconds",
+ __FUNCTION__, MillisecondsToTimeString(m_vecEdits[i].start),
+ MillisecondsToTimeString(m_vecEdits[i].end),
+ advancedSettings->m_iEdlMinCommBreakLength);
+ m_vecEdits.erase(m_vecEdits.begin() + i);
+
+ i--;
+ }
+ }
+ }
+}
+
+void CEdl::AddSceneMarkersAtStartAndEndOfEdits()
+{
+ for (const EDL::Edit& edit : m_vecEdits)
+ {
+ // Add scene markers at the start and end of commercial breaks
+ if (edit.action == Action::COMM_BREAK)
+ {
+ // Don't add a scene marker at the start.
+ if (edit.start > 0)
+ AddSceneMarker(edit.start);
+ AddSceneMarker(edit.end);
+ }
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/Edl.h b/xbmc/cores/VideoPlayer/Edl.h
new file mode 100644
index 0000000..faf3e73
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Edl.h
@@ -0,0 +1,210 @@
+/*
+ * 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/EdlEdit.h"
+
+#include <string>
+#include <vector>
+
+class CFileItem;
+
+class CEdl
+{
+public:
+ CEdl();
+
+ // FIXME: remove const modifier for fFramesPerSecond as it makes no sense as it means nothing
+ // for the reader of the interface, but limits the implementation
+ // to not modify the parameter on stack
+ bool ReadEditDecisionLists(const CFileItem& fileItem, const float fFramesPerSecond);
+ void Clear();
+
+ /*!
+ * @brief Check if there are any parsed edits in EDL for the current item
+ * @return true if EDL has edits, false otherwise
+ */
+ bool HasEdits() const;
+
+ /*!
+ * @brief Check if the edit list has EDL cuts (edits with action CUT)
+ * @return true if EDL has cuts, false otherwise
+ */
+ bool HasCuts() const;
+
+ bool HasSceneMarker() const;
+
+ /*!
+ * @brief Get the total cut time removed from the original item
+ * because of EDL cuts
+ * @return the total cut time
+ */
+ int GetTotalCutTime() const;
+
+ /*!
+ * @brief Providing a given seek time, return the actual time without
+ * considering cut ranges removed from the file
+ * @note VideoPlayer always displays/returns the playback time considering
+ * cut blocks are not part of the playable file
+ * @param seek the desired seek time
+ * @return the seek time without considering EDL cut blocks
+ */
+ int GetTimeWithoutCuts(int seek) const;
+
+ /*!
+ * @brief Provided a given seek time, return the time after correction with
+ * the addition of the already surpassed EDL cut ranges
+ * @note VideoPlayer uses it to restore the correct time after seek since cut blocks
+ * are not part of the playable file
+ * @param seek the desired seek time
+ * @return the seek time after applying the cut blocks already surpassed by the
+ * provided seek time
+ */
+ double GetTimeAfterRestoringCuts(double seek) const;
+
+ /*!
+ * @brief Get the raw EDL edit list.
+ * @return The EDL edits or an empty vector if no edits exist. Edits are
+ * provided with respect to the original media item timeline.
+ */
+ const std::vector<EDL::Edit>& GetRawEditList() const { return m_vecEdits; }
+
+ /*!
+ * @brief Get the EDL edit list.
+ * @return The EDL edits or an empty vector if no edits exist. Edits are
+ * provided with respect to the actual timeline, i.e. considering EDL cuts
+ * are not part of the media item.
+ */
+ const std::vector<EDL::Edit> GetEditList() const;
+
+ /*!
+ * @brief Get the list of EDL cut markers.
+ * @return The list of EDL cut markers or an empty vector if no EDL cuts exist.
+ * The returned values are accurate with respect to cut durations. I.e. if the file
+ * has multiple cuts, the positions of subsquent cuts are automatically corrected by
+ * substracting the previous cut durations.
+ */
+ const std::vector<int64_t> GetCutMarkers() const;
+
+ /*!
+ * @brief Get the list of EDL scene markers.
+ * @return The list of EDL scene markers or an empty vector if no EDL scene exist.
+ * The returned values are accurate with respect to cut durations. I.e. if the file
+ * has multiple cuts, the positions of scene markers are automatically corrected by
+ * substracting the surpassed cut durations until the scene marker point.
+ */
+ const std::vector<int64_t> GetSceneMarkers() const;
+
+ /*!
+ * @brief Check if for the provided seek time is contained within an EDL
+ * edit and fill pEdit with the respective edit struct.
+ * @note seek time refers to the time in the original file timeline (i.e. without
+ * considering cut blocks)
+ * @param iSeek The seek time (on the original timeline)
+ * @param[in,out] pEdit The edit pointer (or nullptr if iSeek not within an edit)
+ * @return true if iSeek is within an edit, false otherwise
+ */
+ bool InEdit(int iSeek, EDL::Edit* pEdit = nullptr);
+
+ /*!
+ * @brief Get the last processed edit time (set during playback when a given
+ * edit is surpassed)
+ * @return The last processed edit time (ms) or -1 if not any
+ */
+ int GetLastEditTime() const;
+
+ /*!
+ * @brief Set the last processed edit time (set during playback when a given
+ * edit is surpassed)
+ * @param editTime The last processed EDL edit time (ms)
+ */
+ void SetLastEditTime(int editTime);
+
+ /*!
+ * @brief Reset the last recorded edit time (-1)
+ */
+ void ResetLastEditTime();
+
+ /*!
+ * @brief Set the last processed edit action type
+ * @param action The action type (e.g. COMM_BREAK)
+ */
+ void SetLastEditActionType(EDL::Action action);
+
+ /*!
+ * @brief Get the last processed edit action type (set during playback when a given
+ * edit is surpassed)
+ * @return The last processed edit action type or -1 if not any
+ */
+ EDL::Action GetLastEditActionType() const;
+
+ // FIXME: remove const modifier for iClock as it makes no sense as it means nothing
+ // for the reader of the interface, but limits the implementation
+ // to not modify the parameter on stack
+ bool GetNextSceneMarker(bool bPlus, const int iClock, int *iSceneMarker);
+
+ // FIXME: remove const modifier as it makes no sense as it means nothing
+ // for the reader of the interface, but limits the implementation
+ // to not modify the parameter on stack
+ static std::string MillisecondsToTimeString(const int iMilliseconds);
+
+private:
+ // total cut time (edl cuts) in ms
+ int m_totalCutTime;
+ std::vector<EDL::Edit> m_vecEdits;
+ std::vector<int> m_vecSceneMarkers;
+
+ /*!
+ * @brief Last processed EDL edit time (ms)
+ */
+ int m_lastEditTime;
+
+ /*!
+ * @brief Last processed EDL edit action type
+ */
+ EDL::Action m_lastEditActionType{EDL::EDL_ACTION_NONE};
+
+ // FIXME: remove const modifier for fFramesPerSecond as it makes no sense as it means nothing
+ // for the reader of the interface, but limits the implementation
+ // to not modify the parameter on stack
+ bool ReadEdl(const std::string& strMovie, const float fFramesPerSecond);
+ // FIXME: remove const modifier for fFramesPerSecond as it makes no sense as it means nothing
+ // for the reader of the interface, but limits the implementation
+ // to not modify the parameter on stack
+ bool ReadComskip(const std::string& strMovie, const float fFramesPerSecond);
+ // FIXME: remove const modifier for strMovie as it makes no sense as it means nothing
+ // for the reader of the interface, but limits the implementation
+ // to not modify the parameter on stack
+ bool ReadVideoReDo(const std::string& strMovie);
+ // FIXME: remove const modifier for strMovie as it makes no sense as it means nothing
+ // for the reader of the interface, but limits the implementation
+ // to not modify the parameter on stack
+ bool ReadBeyondTV(const std::string& strMovie);
+ bool ReadPvr(const CFileItem& fileItem);
+
+ /*!
+ * @brief Adds an edit to the list of EDL edits
+ * @param newEdit the edit to add
+ * @return true if the operation succeeds, false otherwise
+ */
+ bool AddEdit(const EDL::Edit& newEdit);
+
+ // FIXME: remove const modifier for strMovie as it makes no sense as it means nothing
+ // for the reader of the interface, but limits the implementation
+ // to not modify the parameter on stack
+ bool AddSceneMarker(const int sceneMarker);
+
+ void MergeShortCommBreaks();
+
+ /*!
+ * @brief Adds scene markers at the start and end of some edits
+ * (currently only for commercial breaks)
+ */
+ void AddSceneMarkersAtStartAndEndOfEdits();
+};
diff --git a/xbmc/cores/VideoPlayer/IVideoPlayer.h b/xbmc/cores/VideoPlayer/IVideoPlayer.h
new file mode 100644
index 0000000..cf30266
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/IVideoPlayer.h
@@ -0,0 +1,126 @@
+/*
+ * 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 "DVDClock.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#define VideoPlayer_AUDIO 1
+#define VideoPlayer_VIDEO 2
+#define VideoPlayer_SUBTITLE 3
+#define VideoPlayer_TELETEXT 4
+#define VideoPlayer_RDS 5
+#define VideoPlayer_ID3 6
+
+class CDVDMsg;
+class CDVDStreamInfo;
+class CProcessInfo;
+
+class IVideoPlayer
+{
+public:
+ virtual int OnDiscNavResult(void* pData, int iMessage) = 0;
+ virtual void GetVideoResolution(unsigned int &width, unsigned int &height) = 0;
+ virtual ~IVideoPlayer() = default;
+};
+
+class IDVDStreamPlayer
+{
+public:
+ explicit IDVDStreamPlayer(CProcessInfo& processInfo) : m_processInfo(processInfo) {}
+ virtual ~IDVDStreamPlayer() = default;
+ virtual bool OpenStream(CDVDStreamInfo hint) = 0;
+ virtual void CloseStream(bool bWaitForBuffers) = 0;
+ virtual void SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority = 0) = 0;
+ virtual void FlushMessages() = 0;
+ virtual bool IsInited() const = 0;
+ virtual bool AcceptsData() const = 0;
+ virtual bool IsStalled() const = 0;
+
+ enum ESyncState
+ {
+ SYNC_STARTING,
+ SYNC_WAITSYNC,
+ SYNC_INSYNC
+ };
+protected:
+ CProcessInfo &m_processInfo;
+};
+
+struct SStartMsg
+{
+ double timestamp;
+ int player;
+ double cachetime;
+ double cachetotal;
+};
+
+struct SStateMsg
+{
+ IDVDStreamPlayer::ESyncState syncState;
+ int player;
+};
+
+class CDVDVideoCodec;
+
+class IDVDStreamPlayerVideo : public IDVDStreamPlayer
+{
+public:
+ explicit IDVDStreamPlayerVideo(CProcessInfo& processInfo) : IDVDStreamPlayer(processInfo) {}
+ ~IDVDStreamPlayerVideo() override = default;
+ bool OpenStream(CDVDStreamInfo hint) override = 0;
+ void CloseStream(bool bWaitForBuffers) override = 0;
+ virtual void Flush(bool sync) = 0;
+ bool AcceptsData() const override = 0;
+ virtual bool HasData() const = 0;
+ bool IsInited() const override = 0;
+ void SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority = 0) override = 0;
+ virtual void EnableSubtitle(bool bEnable) = 0;
+ virtual bool IsSubtitleEnabled() = 0;
+ virtual double GetSubtitleDelay() = 0;
+ virtual void SetSubtitleDelay(double delay) = 0;
+ bool IsStalled() const override = 0;
+ virtual bool IsRewindStalled() const { return false; }
+ virtual double GetCurrentPts() = 0;
+ virtual double GetOutputDelay() = 0;
+ virtual std::string GetPlayerInfo() = 0;
+ virtual int GetVideoBitrate() = 0;
+ virtual void SetSpeed(int iSpeed) = 0;
+ virtual bool IsEOS() { return false; }
+};
+
+class CDVDAudioCodec;
+class IDVDStreamPlayerAudio : public IDVDStreamPlayer
+{
+public:
+ explicit IDVDStreamPlayerAudio(CProcessInfo& processInfo) : IDVDStreamPlayer(processInfo) {}
+ ~IDVDStreamPlayerAudio() override = default;
+ bool OpenStream(CDVDStreamInfo hints) override = 0;
+ void CloseStream(bool bWaitForBuffers) override = 0;
+ virtual void SetSpeed(int speed) = 0;
+ virtual void Flush(bool sync) = 0;
+ bool AcceptsData() const override = 0;
+ virtual bool HasData() const = 0;
+ virtual int GetLevel() const = 0;
+ bool IsInited() const override = 0;
+ void SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority = 0) override = 0;
+ virtual void SetVolume(float fVolume) {}
+ virtual void SetMute(bool bOnOff) {}
+ virtual void SetDynamicRangeCompression(long drc) = 0;
+ virtual std::string GetPlayerInfo() = 0;
+ virtual int GetAudioChannels() = 0;
+ virtual double GetCurrentPts() = 0;
+ bool IsStalled() const override = 0;
+ virtual bool IsPassthrough() const = 0;
+ virtual float GetDynamicRangeAmplification() const = 0;
+ virtual bool IsEOS() { return false; }
+};
diff --git a/xbmc/cores/VideoPlayer/Interface/DemuxCrypto.h b/xbmc/cores/VideoPlayer/Interface/DemuxCrypto.h
new file mode 100644
index 0000000..4d2a859
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Interface/DemuxCrypto.h
@@ -0,0 +1,69 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_crypto.h"
+
+#include <string>
+
+//CryptoSession is usually obtained once per stream, but could change if an key expires
+
+enum CryptoSessionSystem : uint8_t
+{
+ CRYPTO_SESSION_SYSTEM_NONE,
+ CRYPTO_SESSION_SYSTEM_WIDEVINE,
+ CRYPTO_SESSION_SYSTEM_PLAYREADY,
+ CRYPTO_SESSION_SYSTEM_WISEPLAY,
+};
+
+struct DemuxCryptoSession
+{
+ DemuxCryptoSession(const CryptoSessionSystem sys, const char* sData, const uint8_t flags)
+ : sessionId(sData), keySystem(sys), flags(flags)
+ {
+ }
+
+ bool operator == (const DemuxCryptoSession &other) const
+ {
+ return keySystem == other.keySystem && sessionId == other.sessionId;
+ };
+
+ // encryped stream infos
+ std::string sessionId;
+ CryptoSessionSystem keySystem;
+
+ static const uint8_t FLAG_SECURE_DECODER = 1;
+ uint8_t flags;
+private:
+ DemuxCryptoSession(const DemuxCryptoSession&) = delete;
+ DemuxCryptoSession& operator=(const DemuxCryptoSession&) = delete;
+};
+
+//CryptoInfo stores the information to decrypt a sample
+
+struct DemuxCryptoInfo : DEMUX_CRYPTO_INFO
+{
+ explicit DemuxCryptoInfo(const unsigned int numSubs)
+ {
+ numSubSamples = numSubs;
+ flags = 0;
+ clearBytes = new uint16_t[numSubs];
+ cipherBytes = new uint32_t[numSubs];
+ };
+
+ ~DemuxCryptoInfo()
+ {
+ delete[] clearBytes;
+ delete[] cipherBytes;
+ }
+
+private:
+ DemuxCryptoInfo(const DemuxCryptoInfo&) = delete;
+ DemuxCryptoInfo& operator=(const DemuxCryptoInfo&) = delete;
+};
diff --git a/xbmc/cores/VideoPlayer/Interface/DemuxPacket.h b/xbmc/cores/VideoPlayer/Interface/DemuxPacket.h
new file mode 100644
index 0000000..a8ffb98
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Interface/DemuxPacket.h
@@ -0,0 +1,50 @@
+/*
+ * 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 "TimingConstants.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/demux_packet.h"
+
+#define DMX_SPECIALID_STREAMINFO DEMUX_SPECIALID_STREAMINFO
+#define DMX_SPECIALID_STREAMCHANGE DEMUX_SPECIALID_STREAMCHANGE
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif /* __cplusplus */
+
+ struct DemuxPacket : DEMUX_PACKET
+ {
+ DemuxPacket()
+ {
+ pData = nullptr;
+ iSize = 0;
+ iStreamId = -1;
+ demuxerId = -1;
+ iGroupId = -1;
+
+ pSideData = nullptr;
+ iSideDataElems = 0;
+
+ pts = DVD_NOPTS_VALUE;
+ dts = DVD_NOPTS_VALUE;
+ duration = 0;
+ dispTime = 0;
+ recoveryPoint = false;
+
+ cryptoInfo = nullptr;
+ }
+
+ //! @brief PTS offset correction applied to the PTS and DTS.
+ double m_ptsOffsetCorrection{0};
+ };
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif /* __cplusplus */
diff --git a/xbmc/cores/VideoPlayer/Interface/InputStreamConstants.h b/xbmc/cores/VideoPlayer/Interface/InputStreamConstants.h
new file mode 100644
index 0000000..93eabc5
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Interface/InputStreamConstants.h
@@ -0,0 +1,11 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/inputstream/stream_constants.h"
diff --git a/xbmc/cores/VideoPlayer/Interface/StreamInfo.h b/xbmc/cores/VideoPlayer/Interface/StreamInfo.h
new file mode 100644
index 0000000..df67662
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Interface/StreamInfo.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016-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 "utils/Geometry.h"
+
+#include <string>
+
+template <typename T> class CRectGen;
+typedef CRectGen<float> CRect;
+
+enum StreamFlags
+{
+ FLAG_NONE = 0x0000,
+ FLAG_DEFAULT = 0x0001,
+ FLAG_DUB = 0x0002,
+ FLAG_ORIGINAL = 0x0004,
+ FLAG_COMMENT = 0x0008,
+ FLAG_LYRICS = 0x0010,
+ FLAG_KARAOKE = 0x0020,
+ FLAG_FORCED = 0x0040,
+ FLAG_HEARING_IMPAIRED = 0x0080,
+ FLAG_VISUAL_IMPAIRED = 0x0100,
+ FLAG_STILL_IMAGES = 0x100000
+};
+
+enum class StreamHdrType
+{
+ HDR_TYPE_NONE, ///< <b>None</b>, returns an empty string when used in infolabels
+ HDR_TYPE_HDR10, ///< <b>HDR10</b>, returns `hdr10` when used in infolabels
+ HDR_TYPE_DOLBYVISION, ///< <b>Dolby Vision</b>, returns `dolbyvision` when used in infolabels
+ HDR_TYPE_HLG ///< <b>HLG</b>, returns `hlg` when used in infolabels
+};
+
+struct StreamInfo
+{
+ bool valid = false;
+ int bitrate = 0;
+ std::string language;
+ std::string name;
+ std::string codecName;
+ StreamFlags flags = StreamFlags::FLAG_NONE;
+
+protected:
+ StreamInfo() = default;
+ virtual ~StreamInfo() = default;
+};
+
+struct AudioStreamInfo : StreamInfo
+{
+ int channels = 0;
+ int samplerate = 0;
+ int bitspersample = 0;
+};
+
+struct SubtitleStreamInfo : StreamInfo
+{};
+
+struct VideoStreamInfo : StreamInfo
+{
+ float videoAspectRatio = 0.0f;
+ int height = 0;
+ int width = 0;
+ CRect SrcRect;
+ CRect DestRect;
+ CRect VideoRect;
+ std::string stereoMode;
+ int angles = 0;
+ StreamHdrType hdrType = StreamHdrType::HDR_TYPE_NONE;
+};
+
+struct ProgramInfo
+{
+ int id = -1;
+ bool playing = false;
+ std::string name;
+};
diff --git a/xbmc/cores/VideoPlayer/Interface/TimingConstants.h b/xbmc/cores/VideoPlayer/Interface/TimingConstants.h
new file mode 100644
index 0000000..3f526cc
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Interface/TimingConstants.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2017-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 <cstdint>
+
+#define DVD_TIME_BASE 1000000
+#define DVD_NOPTS_VALUE 0xFFF0000000000000
+
+constexpr int64_t DVD_TIME_TO_MSEC(double x)
+{
+ return static_cast<int64_t>(x * 1000 / DVD_TIME_BASE);
+}
+constexpr double DVD_SEC_TO_TIME(double x) { return x * DVD_TIME_BASE; }
+constexpr double DVD_MSEC_TO_TIME(double x) { return x * DVD_TIME_BASE / 1000; }
+
+#define DVD_PLAYSPEED_PAUSE 0 // frame stepping
+#define DVD_PLAYSPEED_NORMAL 1000
diff --git a/xbmc/cores/VideoPlayer/PTSTracker.cpp b/xbmc/cores/VideoPlayer/PTSTracker.cpp
new file mode 100644
index 0000000..c5fdbf1
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/PTSTracker.cpp
@@ -0,0 +1,323 @@
+/*
+ * 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 "PTSTracker.h"
+
+#include "DVDCodecs/DVDCodecUtils.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cmath>
+
+#define MAXERR DVD_MSEC_TO_TIME(2.5)
+
+CPtsTracker::CPtsTracker()
+{
+ ResetVFRDetection();
+ Flush();
+}
+
+void CPtsTracker::ResetVFRDetection(void)
+{
+ m_minframeduration = DVD_NOPTS_VALUE;
+ m_maxframeduration = DVD_NOPTS_VALUE;
+ m_VFRCounter = 0;
+ m_patternCounter = 0;
+ m_lastPattern.clear();
+}
+
+void CPtsTracker::Flush()
+{
+ m_pattern.clear();
+ m_ringpos = 0;
+ m_prevpts = DVD_NOPTS_VALUE;
+ m_ringfill = 0;
+ m_haspattern = false;
+ m_patternlength = 0;
+ m_frameduration = DVD_NOPTS_VALUE;
+ memset(m_diffring, 0, sizeof(m_diffring));
+}
+
+void CPtsTracker::Add(double pts)
+{
+ //can't get a diff with just one pts
+ if (m_prevpts == DVD_NOPTS_VALUE)
+ {
+ m_prevpts = pts;
+ return;
+ }
+
+ //increase the ringbuffer position
+ m_ringpos = (m_ringpos + 1) % DIFFRINGSIZE;
+ //add the current diff to the ringbuffer
+ m_diffring[m_ringpos] = pts - m_prevpts;
+ //save the pts
+ m_prevpts = pts;
+
+ if (m_ringfill < DIFFRINGSIZE)
+ m_ringfill++;
+
+ //only search for patterns if we have full ringbuffer
+ if (m_ringfill < DIFFRINGSIZE)
+ return;
+
+ //get the current pattern in the ringbuffer
+ std::vector<double> pattern;
+ GetPattern(pattern);
+
+ //check if the pattern is the same as the saved pattern
+ //and if it is actually a pattern
+ if (!CheckPattern(pattern))
+ {
+ if (m_haspattern)
+ {
+ m_VFRCounter++;
+ m_lastPattern = m_pattern;
+ CLog::Log(LOGDEBUG, "CPtsTracker: pattern lost on diff {:f}, number of losses {}", GetDiff(0),
+ m_VFRCounter);
+ Flush();
+ }
+
+ //no pattern detected or current pattern broke/changed
+ //save detected pattern so we can check it with the next iteration
+ m_pattern = pattern;
+
+ return;
+ }
+ else
+ {
+ if (!m_haspattern)
+ {
+ m_haspattern = true;
+ m_patternlength = m_pattern.size();
+
+ if (!m_lastPattern.empty() && !CheckPattern(m_lastPattern))
+ {
+ m_patternCounter++;
+ }
+
+ double frameduration = CalcFrameDuration();
+ CLog::Log(LOGDEBUG, "CPtsTracker: detected pattern of length {}: {}, frameduration: {:f}",
+ (int)pattern.size(), GetPatternStr(), frameduration);
+ }
+ }
+
+ m_frameduration = CalcFrameDuration();
+}
+
+//gets a diff diffnr into the past
+inline double CPtsTracker::GetDiff(int diffnr)
+{
+ //m_ringpos is the last added diff, so if we want to go in the past we have to move back in the ringbuffer
+ int pos = m_ringpos - diffnr;
+ if (pos < 0)
+ pos += DIFFRINGSIZE;
+
+ return m_diffring[pos];
+}
+
+//calculate the current pattern in the ringbuffer
+void CPtsTracker::GetPattern(std::vector<double>& pattern)
+{
+ int difftypesbuff[DIFFRINGSIZE]; //difftypes of the diffs, difftypesbuff[0] is the last added diff,
+ //difftypesbuff[1] the one added before that etc
+
+ //get the difftypes
+ std::vector<double> difftypes;
+ for (int i = 0; i < m_ringfill; i++)
+ {
+ bool hasmatch = false;
+ for (unsigned int j = 0; j < difftypes.size(); j++)
+ {
+ if (MatchDiff(GetDiff(i), difftypes[j]))
+ {
+ hasmatch = true;
+ break;
+ }
+ }
+
+ //if we don't have a match with a saved difftype, we add it as a new one
+ if (!hasmatch)
+ difftypes.push_back(GetDiff(i));
+ }
+
+ //mark each diff with what difftype it is
+ for (int i = 0; i < m_ringfill; i++)
+ {
+ for (unsigned int j = 0; j < difftypes.size(); j++)
+ {
+ if (MatchDiff(GetDiff(i), difftypes[j]))
+ {
+ difftypesbuff[i] = j;
+ break;
+ }
+ }
+ }
+
+ bool checkexisting = !m_pattern.empty();
+
+ //we check for patterns to the length of DIFFRINGSIZE / 2
+ for (int i = 1; i <= m_ringfill / 2; i++)
+ {
+ //check the existing pattern length first
+ int length = checkexisting ? m_pattern.size() : i;
+
+ bool hasmatch = true;
+ for (int j = 1; j <= m_ringfill / length; j++)
+ {
+ int nrdiffs = length;
+ //we want to check the full buffer to see if the pattern repeats
+ //but we can't go beyond the buffer
+ if (j * length + length > m_ringfill)
+ nrdiffs = m_ringfill - j * length;
+
+ if (nrdiffs < 1) //if the buffersize can be cleanly divided by i we're done here
+ break;
+
+ if (!MatchDifftype(difftypesbuff, difftypesbuff + j * length, nrdiffs))
+ {
+ hasmatch = false;
+ break;
+ }
+ }
+
+ if (checkexisting)
+ {
+ checkexisting = false;
+ i--;
+ }
+
+ if (hasmatch)
+ {
+ for (int i = 0; i < length; i++)
+ {
+ double avgdiff = 0.0;
+ for (int j = 0; j < m_ringfill / length; j++)
+ avgdiff += GetDiff(j * length + i);
+
+ avgdiff /= m_ringfill / length;
+ pattern.push_back(avgdiff);
+ }
+ break;
+ }
+ }
+ std::sort(pattern.begin(), pattern.end());
+}
+
+inline bool CPtsTracker::MatchDiff(double diff1, double diff2)
+{
+ return fabs(diff1 - diff2) < MAXERR;
+}
+
+//check if diffs1 is the same as diffs2
+inline bool CPtsTracker::MatchDifftype(int diffs1[], int diffs2[], int nrdiffs)
+{
+ for (int i = 0; i < nrdiffs; i++)
+ {
+ if (diffs1[i] != diffs2[i])
+ return false;
+ }
+ return true;
+}
+
+//check if our current detected pattern is the same as the one we saved
+bool CPtsTracker::CheckPattern(std::vector<double>& pattern)
+{
+ //if no pattern was detected or if the size of the patterns differ we don't have a match
+ if (pattern.empty() || pattern.size() != m_pattern.size())
+ return false;
+
+ if (pattern.size() == 1)
+ {
+ if (pattern[0] < MAXERR)
+ return false; //all diffs are too close to 0, can't use this
+ }
+
+ //check if the current pattern matches the saved pattern, with an offset of 1
+ for (unsigned int i = 0; i < m_pattern.size(); i++)
+ {
+ double diff = pattern[i];
+
+ if (!MatchDiff(diff, m_pattern[i]))
+ return false;
+ }
+
+ return true;
+}
+
+//calculate how long each frame should last from the saved pattern
+//also retrieve information of max and min frame rate duration, for VFR files case
+double CPtsTracker::CalcFrameDuration()
+{
+ if (!m_pattern.empty())
+ {
+ //take the average of all diffs in the pattern
+ double frameduration;
+ double current, currentmin, currentmax;
+
+ currentmin = m_pattern[0];
+ currentmax = currentmin;
+ frameduration = currentmin;
+ for (unsigned int i = 1; i < m_pattern.size(); i++)
+ {
+ current = m_pattern[i];
+ if (current>currentmax)
+ currentmax = current;
+ if (current<currentmin)
+ currentmin = current;
+ frameduration += current;
+ }
+ frameduration /= m_pattern.size();
+
+ // Update min and max frame duration, only if data is valid
+ bool standard = false;
+ double tempduration = CDVDCodecUtils::NormalizeFrameduration(currentmin, &standard);
+ if (m_minframeduration == DVD_NOPTS_VALUE)
+ {
+ if (standard)
+ m_minframeduration = tempduration;
+ }
+ else
+ {
+ if (standard && (tempduration < m_minframeduration))
+ m_minframeduration = tempduration;
+ }
+
+ tempduration = CDVDCodecUtils::NormalizeFrameduration(currentmax, &standard);
+ if (m_maxframeduration == DVD_NOPTS_VALUE)
+ {
+ if (standard)
+ m_maxframeduration = tempduration;
+ }
+ else
+ {
+ if (standard && (tempduration > m_maxframeduration))
+ m_maxframeduration = tempduration;
+ }
+
+ //frameduration is not completely correct, use a common one if it's close
+ return CDVDCodecUtils::NormalizeFrameduration(frameduration);
+ }
+
+ return DVD_NOPTS_VALUE;
+}
+
+//looks pretty in the log
+std::string CPtsTracker::GetPatternStr()
+{
+ std::string patternstr;
+
+ for (unsigned int i = 0; i < m_pattern.size(); i++)
+ patternstr += StringUtils::Format("{:.2f} ", m_pattern[i]);
+
+ StringUtils::Trim(patternstr);
+
+ return patternstr;
+}
diff --git a/xbmc/cores/VideoPlayer/PTSTracker.h b/xbmc/cores/VideoPlayer/PTSTracker.h
new file mode 100644
index 0000000..d7b98b6
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/PTSTracker.h
@@ -0,0 +1,59 @@
+/*
+ * 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 <string>
+#include <vector>
+
+#define DIFFRINGSIZE 120
+#define VFR_DETECTION_THRESHOLD 3
+#define VFR_PATTERN_THRESHOLD 2
+
+class CPtsTracker
+{
+ public:
+ CPtsTracker();
+ void Add(double pts);
+ void Flush(); //flush the saved pattern and the ringbuffer
+ void ResetVFRDetection(void);
+
+ int GetPatternLength() { return m_patternlength; }
+ double GetFrameDuration() { return m_frameduration; }
+ double GetMaxFrameDuration(void) { return m_maxframeduration; }
+ double GetMinFrameDuration(void) { return m_minframeduration; }
+ bool HasFullBuffer() { return m_ringfill == DIFFRINGSIZE; }
+ bool VFRDetection(void) { return ((m_VFRCounter >= VFR_DETECTION_THRESHOLD) && (m_patternCounter >= VFR_PATTERN_THRESHOLD)); }
+
+ private:
+ double m_prevpts; //last pts added
+ double m_diffring[DIFFRINGSIZE]; //ringbuffer of differences between pts'
+ int m_ringpos; //position of last diff added to ringbuffer
+ int m_ringfill; //how many diffs we have in the ringbuffer
+ double GetDiff(int diffnr); //gets diffs from now to the past
+
+ void GetPattern(std::vector<double>& pattern); //gets the current pattern
+
+ static bool MatchDiff(double diff1, double diff2); //checks if two diffs match by MAXERR
+ static bool MatchDifftype(int diffs1[], int diffs2[], int nrdiffs); //checks if the difftypes match
+
+ //checks if the current pattern matches with the saved m_pattern with offset m_patternpos
+ bool CheckPattern(std::vector<double>& pattern);
+
+ double CalcFrameDuration(); //calculates the frame duration from m_pattern
+
+ std::vector<double> m_pattern, m_lastPattern; //the last saved pattern
+ double m_frameduration; //frameduration exposed to VideoPlayer, used for calculating the fps
+ double m_maxframeduration; //Max value detected for frame duration (for VFR files case)
+ double m_minframeduration; //Min value detected for frame duration (for VFR files case)
+ bool m_haspattern; //for the log and detecting VFR files case
+ int m_patternlength; //for the codec info
+ int m_VFRCounter; //retry counter for VFR detection
+ int m_patternCounter;
+ std::string GetPatternStr(); //also for the log
+};
diff --git a/xbmc/cores/VideoPlayer/Process/CMakeLists.txt b/xbmc/cores/VideoPlayer/Process/CMakeLists.txt
new file mode 100644
index 0000000..3197852
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES ProcessInfo.cpp)
+set(HEADERS ProcessInfo.h)
+
+core_add_library(process)
diff --git a/xbmc/cores/VideoPlayer/Process/ProcessInfo.cpp b/xbmc/cores/VideoPlayer/Process/ProcessInfo.cpp
new file mode 100644
index 0000000..00c58cb
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/ProcessInfo.cpp
@@ -0,0 +1,696 @@
+/*
+ * 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 "ProcessInfo.h"
+
+#include "ServiceBroker.h"
+#include "cores/DataCacheCore.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+
+#include <mutex>
+
+CCriticalSection createSection;
+std::map<std::string, CreateProcessControl> CProcessInfo::m_processControls;
+
+void CProcessInfo::RegisterProcessControl(const std::string& id, CreateProcessControl createFunc)
+{
+ std::unique_lock<CCriticalSection> lock(createSection);
+
+ m_processControls.clear();
+ m_processControls[id] = createFunc;
+}
+
+CProcessInfo* CProcessInfo::CreateInstance()
+{
+ std::unique_lock<CCriticalSection> lock(createSection);
+
+ CProcessInfo *ret = nullptr;
+ for (auto &info : m_processControls)
+ {
+ ret = info.second();
+ if (ret)
+ return ret;
+ }
+ return new CProcessInfo();
+}
+
+CProcessInfo::CProcessInfo()
+{
+ m_videoSettingsLocked.reset(new CVideoSettingsLocked(m_videoSettings, m_settingsSection));
+}
+
+void CProcessInfo::SetDataCache(CDataCacheCore *cache)
+{
+ m_dataCache = cache;;
+
+ ResetVideoCodecInfo();
+ m_renderGuiLayer = false;
+ m_renderVideoLayer = false;
+ m_dataCache->SetGuiRender(m_renderGuiLayer);
+ m_dataCache->SetVideoRender(m_renderVideoLayer);
+}
+
+//******************************************************************************
+// video codec
+//******************************************************************************
+void CProcessInfo::ResetVideoCodecInfo()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_videoIsHWDecoder = false;
+ m_videoDecoderName = "unknown";
+ m_videoDeintMethod = "unknown";
+ m_videoPixelFormat = "unknown";
+ m_videoStereoMode.clear();
+ m_videoWidth = 0;
+ m_videoHeight = 0;
+ m_videoFPS = 0.0;
+ m_videoDAR = 0.0;
+ m_videoIsInterlaced = false;
+ m_deintMethods.clear();
+ m_deintMethods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_NONE);
+ m_deintMethodDefault = EINTERLACEMETHOD::VS_INTERLACEMETHOD_NONE;
+ m_stateSeeking = false;
+
+ if (m_dataCache)
+ {
+ m_dataCache->SetVideoDecoderName(m_videoDecoderName, m_videoIsHWDecoder);
+ m_dataCache->SetVideoDeintMethod(m_videoDeintMethod);
+ m_dataCache->SetVideoPixelFormat(m_videoPixelFormat);
+ m_dataCache->SetVideoDimensions(m_videoWidth, m_videoHeight);
+ m_dataCache->SetVideoFps(m_videoFPS);
+ m_dataCache->SetVideoDAR(m_videoDAR);
+ m_dataCache->SetStateSeeking(m_stateSeeking);
+ m_dataCache->SetVideoStereoMode(m_videoStereoMode);
+ }
+}
+
+void CProcessInfo::SetVideoDecoderName(const std::string &name, bool isHw)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_videoIsHWDecoder = isHw;
+ m_videoDecoderName = name;
+
+ if (m_dataCache)
+ m_dataCache->SetVideoDecoderName(m_videoDecoderName, m_videoIsHWDecoder);
+}
+
+std::string CProcessInfo::GetVideoDecoderName()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ return m_videoDecoderName;
+}
+
+bool CProcessInfo::IsVideoHwDecoder()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ return m_videoIsHWDecoder;
+}
+
+void CProcessInfo::SetVideoDeintMethod(const std::string &method)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_videoDeintMethod = method;
+
+ if (m_dataCache)
+ m_dataCache->SetVideoDeintMethod(m_videoDeintMethod);
+}
+
+std::string CProcessInfo::GetVideoDeintMethod()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ return m_videoDeintMethod;
+}
+
+void CProcessInfo::SetVideoPixelFormat(const std::string &pixFormat)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_videoPixelFormat = pixFormat;
+
+ if (m_dataCache)
+ m_dataCache->SetVideoPixelFormat(m_videoPixelFormat);
+}
+
+std::string CProcessInfo::GetVideoPixelFormat()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ return m_videoPixelFormat;
+}
+
+void CProcessInfo::SetVideoStereoMode(const std::string &mode)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_videoStereoMode = mode;
+
+ if (m_dataCache)
+ m_dataCache->SetVideoStereoMode(m_videoStereoMode);
+}
+
+std::string CProcessInfo::GetVideoStereoMode()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ return m_videoStereoMode;
+}
+
+void CProcessInfo::SetVideoDimensions(int width, int height)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_videoWidth = width;
+ m_videoHeight = height;
+
+ if (m_dataCache)
+ m_dataCache->SetVideoDimensions(m_videoWidth, m_videoHeight);
+}
+
+void CProcessInfo::GetVideoDimensions(int &width, int &height)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ width = m_videoWidth;
+ height = m_videoHeight;
+}
+
+void CProcessInfo::SetVideoFps(float fps)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_videoFPS = fps;
+
+ if (m_dataCache)
+ m_dataCache->SetVideoFps(m_videoFPS);
+}
+
+float CProcessInfo::GetVideoFps()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ return m_videoFPS;
+}
+
+void CProcessInfo::SetVideoDAR(float dar)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_videoDAR = dar;
+
+ if (m_dataCache)
+ m_dataCache->SetVideoDAR(m_videoDAR);
+}
+
+float CProcessInfo::GetVideoDAR()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ return m_videoDAR;
+}
+
+void CProcessInfo::SetVideoInterlaced(bool interlaced)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_videoIsInterlaced = interlaced;
+
+ if (m_dataCache)
+ m_dataCache->SetVideoInterlaced(interlaced);
+}
+
+bool CProcessInfo::GetVideoInterlaced()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ return m_videoIsInterlaced;
+}
+
+EINTERLACEMETHOD CProcessInfo::GetFallbackDeintMethod()
+{
+ return VS_INTERLACEMETHOD_DEINTERLACE;
+}
+
+void CProcessInfo::SetSwDeinterlacingMethods()
+{
+ std::list<EINTERLACEMETHOD> methods;
+ methods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_NONE);
+ methods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_DEINTERLACE);
+ methods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_DEINTERLACE_HALF);
+
+ UpdateDeinterlacingMethods(methods);
+ SetDeinterlacingMethodDefault(EINTERLACEMETHOD::VS_INTERLACEMETHOD_DEINTERLACE);
+}
+
+void CProcessInfo::UpdateDeinterlacingMethods(std::list<EINTERLACEMETHOD> &methods)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_deintMethods = methods;
+
+ for (auto &deint : m_renderInfo.m_deintMethods)
+ {
+ if (!Supports(deint))
+ m_deintMethods.push_back(deint);
+ }
+
+ if (!Supports(EINTERLACEMETHOD::VS_INTERLACEMETHOD_NONE))
+ m_deintMethods.push_front(EINTERLACEMETHOD::VS_INTERLACEMETHOD_NONE);
+}
+
+bool CProcessInfo::Supports(EINTERLACEMETHOD method) const
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ auto it = std::find(m_deintMethods.begin(), m_deintMethods.end(), method);
+ if (it != m_deintMethods.end())
+ return true;
+
+ return false;
+}
+
+void CProcessInfo::SetDeinterlacingMethodDefault(EINTERLACEMETHOD method)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_deintMethodDefault = method;
+}
+
+EINTERLACEMETHOD CProcessInfo::GetDeinterlacingMethodDefault() const
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ return m_deintMethodDefault;
+}
+
+CVideoBufferManager& CProcessInfo::GetVideoBufferManager()
+{
+ return m_videoBufferManager;
+}
+
+std::vector<AVPixelFormat> CProcessInfo::GetPixFormats()
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ if (m_pixFormats.empty())
+ {
+ return GetRenderFormats();
+ }
+ return m_pixFormats;
+}
+
+void CProcessInfo::SetPixFormats(std::vector<AVPixelFormat> &formats)
+{
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+
+ m_pixFormats = formats;
+}
+
+//******************************************************************************
+// player audio info
+//******************************************************************************
+void CProcessInfo::ResetAudioCodecInfo()
+{
+ std::unique_lock<CCriticalSection> lock(m_audioCodecSection);
+
+ m_audioDecoderName = "unknown";
+ m_audioChannels = "unknown";
+ m_audioSampleRate = 0;;
+ m_audioBitsPerSample = 0;
+
+ if (m_dataCache)
+ {
+ m_dataCache->SetAudioDecoderName(m_audioDecoderName);
+ m_dataCache->SetAudioChannels(m_audioChannels);
+ m_dataCache->SetAudioSampleRate(m_audioSampleRate);
+ m_dataCache->SetAudioBitsPerSample(m_audioBitsPerSample);
+ }
+}
+
+void CProcessInfo::SetAudioDecoderName(const std::string &name)
+{
+ std::unique_lock<CCriticalSection> lock(m_audioCodecSection);
+
+ m_audioDecoderName = name;
+
+ if (m_dataCache)
+ m_dataCache->SetAudioDecoderName(m_audioDecoderName);
+}
+
+std::string CProcessInfo::GetAudioDecoderName()
+{
+ std::unique_lock<CCriticalSection> lock(m_audioCodecSection);
+
+ return m_audioDecoderName;
+}
+
+void CProcessInfo::SetAudioChannels(const std::string &channels)
+{
+ std::unique_lock<CCriticalSection> lock(m_audioCodecSection);
+
+ m_audioChannels = channels;
+
+ if (m_dataCache)
+ m_dataCache->SetAudioChannels(m_audioChannels);
+}
+
+std::string CProcessInfo::GetAudioChannels()
+{
+ std::unique_lock<CCriticalSection> lock(m_audioCodecSection);
+
+ return m_audioChannels;
+}
+
+void CProcessInfo::SetAudioSampleRate(int sampleRate)
+{
+ std::unique_lock<CCriticalSection> lock(m_audioCodecSection);
+
+ m_audioSampleRate = sampleRate;
+
+ if (m_dataCache)
+ m_dataCache->SetAudioSampleRate(m_audioSampleRate);
+}
+
+int CProcessInfo::GetAudioSampleRate()
+{
+ std::unique_lock<CCriticalSection> lock(m_audioCodecSection);
+
+ return m_audioSampleRate;
+}
+
+void CProcessInfo::SetAudioBitsPerSample(int bitsPerSample)
+{
+ std::unique_lock<CCriticalSection> lock(m_audioCodecSection);
+
+ m_audioBitsPerSample = bitsPerSample;
+
+ if (m_dataCache)
+ m_dataCache->SetAudioBitsPerSample(m_audioBitsPerSample);
+}
+
+int CProcessInfo::GetAudioBitsPerSample()
+{
+ std::unique_lock<CCriticalSection> lock(m_audioCodecSection);
+
+ return m_audioBitsPerSample;
+}
+
+bool CProcessInfo::AllowDTSHDDecode()
+{
+ return true;
+}
+
+void CProcessInfo::SetRenderClockSync(bool enabled)
+{
+ std::unique_lock<CCriticalSection> lock(m_renderSection);
+
+ m_isClockSync = enabled;
+
+ if (m_dataCache)
+ m_dataCache->SetRenderClockSync(enabled);
+}
+
+bool CProcessInfo::IsRenderClockSync()
+{
+ std::unique_lock<CCriticalSection> lock(m_renderSection);
+
+ return m_isClockSync;
+}
+
+void CProcessInfo::UpdateRenderInfo(CRenderInfo &info)
+{
+ std::unique_lock<CCriticalSection> lock(m_renderSection);
+
+ m_renderInfo = info;
+
+ for (auto &deint : m_renderInfo.m_deintMethods)
+ {
+ if (!Supports(deint))
+ m_deintMethods.push_back(deint);
+ }
+}
+
+void CProcessInfo::UpdateRenderBuffers(int queued, int discard, int free)
+{
+ std::unique_lock<CCriticalSection> lock(m_renderSection);
+ m_renderBufQueued = queued;
+ m_renderBufDiscard = discard;
+ m_renderBufFree = free;
+}
+
+void CProcessInfo::GetRenderBuffers(int &queued, int &discard, int &free)
+{
+ std::unique_lock<CCriticalSection> lock(m_renderSection);
+ queued = m_renderBufQueued;
+ discard = m_renderBufDiscard;
+ free = m_renderBufFree;
+}
+
+std::vector<AVPixelFormat> CProcessInfo::GetRenderFormats()
+{
+ std::vector<AVPixelFormat> formats;
+ formats.push_back(AV_PIX_FMT_YUV420P);
+ return formats;
+}
+
+//******************************************************************************
+// player states
+//******************************************************************************
+void CProcessInfo::SeekFinished(int64_t offset)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ if (m_dataCache)
+ m_dataCache->SeekFinished(offset);
+}
+
+void CProcessInfo::SetStateSeeking(bool active)
+{
+ std::unique_lock<CCriticalSection> lock(m_renderSection);
+
+ m_stateSeeking = active;
+
+ if (m_dataCache)
+ m_dataCache->SetStateSeeking(active);
+}
+
+bool CProcessInfo::IsSeeking()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_stateSeeking;
+}
+
+void CProcessInfo::SetStateRealtime(bool state)
+{
+ std::unique_lock<CCriticalSection> lock(m_renderSection);
+
+ m_realTimeStream = state;
+}
+
+bool CProcessInfo::IsRealtimeStream()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_realTimeStream;
+}
+
+void CProcessInfo::SetSpeed(float speed)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ m_speed = speed;
+ m_newSpeed = speed;
+
+ if (m_dataCache)
+ m_dataCache->SetSpeed(m_newTempo, speed);
+}
+
+void CProcessInfo::SetNewSpeed(float speed)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ m_newSpeed = speed;
+
+ if (m_dataCache)
+ m_dataCache->SetSpeed(m_tempo, speed);
+}
+
+float CProcessInfo::GetNewSpeed()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_newSpeed;
+}
+
+void CProcessInfo::SetFrameAdvance(bool fa)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ m_frameAdvance = fa;
+
+ if (m_dataCache)
+ m_dataCache->SetFrameAdvance(fa);
+}
+
+bool CProcessInfo::IsFrameAdvance()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_frameAdvance;
+}
+
+void CProcessInfo::SetTempo(float tempo)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ m_tempo = tempo;
+ m_newTempo = tempo;
+
+ if (m_dataCache)
+ m_dataCache->SetSpeed(tempo, m_newSpeed);
+}
+
+void CProcessInfo::SetNewTempo(float tempo)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ m_newTempo = tempo;
+
+ if (m_dataCache)
+ m_dataCache->SetSpeed(tempo, m_speed);
+}
+
+float CProcessInfo::GetNewTempo()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_newTempo;
+}
+
+float CProcessInfo::MinTempoPlatform()
+{
+ return 0.75f;
+}
+
+float CProcessInfo::MaxTempoPlatform()
+{
+ return 1.55f;
+}
+
+bool CProcessInfo::IsTempoAllowed(float tempo)
+{
+ if (tempo > MinTempoPlatform() &&
+ (tempo < MaxTempoPlatform() || tempo < CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_maxTempo))
+ return true;
+
+ return false;
+}
+
+unsigned int CProcessInfo::GetMaxPassthroughOffSyncDuration() const
+{
+ return CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_maxPassthroughOffSyncDuration;
+}
+
+void CProcessInfo::SetLevelVQ(int level)
+{
+ m_levelVQ = level;
+}
+
+int CProcessInfo::GetLevelVQ()
+{
+ return m_levelVQ;
+}
+
+void CProcessInfo::SetGuiRender(bool gui)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ bool change = (m_renderGuiLayer != gui);
+ m_renderGuiLayer = gui;
+ if (change)
+ {
+ if (m_dataCache)
+ m_dataCache->SetGuiRender(gui);
+ }
+}
+
+bool CProcessInfo::GetGuiRender()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_renderGuiLayer;
+}
+
+void CProcessInfo::SetVideoRender(bool video)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ bool change = (m_renderVideoLayer != video);
+ m_renderVideoLayer = video;
+ if (change)
+ {
+ if (m_dataCache)
+ m_dataCache->SetVideoRender(video);
+ }
+}
+
+bool CProcessInfo::GetVideoRender()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+
+ return m_renderVideoLayer;
+}
+
+void CProcessInfo::SetPlayTimes(time_t start, int64_t current, int64_t min, int64_t max)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ m_startTime = start;
+ m_time = current;
+ m_timeMin = min;
+ m_timeMax = max;
+
+ if (m_dataCache)
+ {
+ m_dataCache->SetPlayTimes(start, current, min, max);
+ }
+}
+
+int64_t CProcessInfo::GetMaxTime()
+{
+ std::unique_lock<CCriticalSection> lock(m_stateSection);
+ return m_timeMax;
+}
+
+//******************************************************************************
+// settings
+//******************************************************************************
+CVideoSettings CProcessInfo::GetVideoSettings()
+{
+ std::unique_lock<CCriticalSection> lock(m_settingsSection);
+ return m_videoSettings;
+}
+
+CVideoSettingsLocked& CProcessInfo::GetVideoSettingsLocked()
+{
+ std::unique_lock<CCriticalSection> lock(m_settingsSection);
+ return *m_videoSettingsLocked;
+}
+
+void CProcessInfo::SetVideoSettings(CVideoSettings &settings)
+{
+ std::unique_lock<CCriticalSection> lock(m_settingsSection);
+ m_videoSettings = settings;
+}
diff --git a/xbmc/cores/VideoPlayer/Process/ProcessInfo.h b/xbmc/cores/VideoPlayer/Process/ProcessInfo.h
new file mode 100644
index 0000000..adac47b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/ProcessInfo.h
@@ -0,0 +1,180 @@
+/*
+ * 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/VideoPlayer/Buffers/VideoBuffer.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderInfo.h"
+#include "cores/VideoSettings.h"
+#include "threads/CriticalSection.h"
+
+#include <atomic>
+#include <list>
+#include <map>
+#include <string>
+
+class CProcessInfo;
+class CDataCacheCore;
+
+using CreateProcessControl = CProcessInfo* (*)();
+
+class CProcessInfo
+{
+public:
+ static CProcessInfo* CreateInstance();
+ static void RegisterProcessControl(const std::string& id, CreateProcessControl createFunc);
+ virtual ~CProcessInfo() = default;
+ void SetDataCache(CDataCacheCore *cache);
+
+ // player video
+ void ResetVideoCodecInfo();
+ void SetVideoDecoderName(const std::string &name, bool isHw);
+ std::string GetVideoDecoderName();
+ bool IsVideoHwDecoder();
+ void SetVideoDeintMethod(const std::string &method);
+ std::string GetVideoDeintMethod();
+ void SetVideoPixelFormat(const std::string &pixFormat);
+ std::string GetVideoPixelFormat();
+ void SetVideoStereoMode(const std::string &mode);
+ std::string GetVideoStereoMode();
+ void SetVideoDimensions(int width, int height);
+ void GetVideoDimensions(int &width, int &height);
+ void SetVideoFps(float fps);
+ float GetVideoFps();
+ void SetVideoDAR(float dar);
+ float GetVideoDAR();
+ void SetVideoInterlaced(bool interlaced);
+ bool GetVideoInterlaced();
+ virtual EINTERLACEMETHOD GetFallbackDeintMethod();
+ virtual void SetSwDeinterlacingMethods();
+ void UpdateDeinterlacingMethods(std::list<EINTERLACEMETHOD> &methods);
+ bool Supports(EINTERLACEMETHOD method) const;
+ void SetDeinterlacingMethodDefault(EINTERLACEMETHOD method);
+ EINTERLACEMETHOD GetDeinterlacingMethodDefault() const;
+ CVideoBufferManager& GetVideoBufferManager();
+ std::vector<AVPixelFormat> GetPixFormats();
+ void SetPixFormats(std::vector<AVPixelFormat> &formats);
+
+ // player audio info
+ void ResetAudioCodecInfo();
+ void SetAudioDecoderName(const std::string &name);
+ std::string GetAudioDecoderName();
+ void SetAudioChannels(const std::string &channels);
+ std::string GetAudioChannels();
+ void SetAudioSampleRate(int sampleRate);
+ int GetAudioSampleRate();
+ void SetAudioBitsPerSample(int bitsPerSample);
+ int GetAudioBitsPerSample();
+ virtual bool AllowDTSHDDecode();
+ virtual bool WantsRawPassthrough() { return false; }
+
+ // render info
+ void SetRenderClockSync(bool enabled);
+ bool IsRenderClockSync();
+ void UpdateRenderInfo(CRenderInfo &info);
+ void UpdateRenderBuffers(int queued, int discard, int free);
+ void GetRenderBuffers(int &queued, int &discard, int &free);
+ virtual std::vector<AVPixelFormat> GetRenderFormats();
+
+ // player states
+ /*!
+ * @brief Notifies that a seek operation has finished
+ * @param offset - the seek offset
+ */
+ void SeekFinished(int64_t offset);
+
+ void SetStateSeeking(bool active);
+ bool IsSeeking();
+ void SetStateRealtime(bool state);
+ bool IsRealtimeStream();
+ void SetSpeed(float speed);
+ void SetNewSpeed(float speed);
+ float GetNewSpeed();
+ void SetFrameAdvance(bool fa);
+ bool IsFrameAdvance();
+ void SetTempo(float tempo);
+ void SetNewTempo(float tempo);
+ float GetNewTempo();
+ bool IsTempoAllowed(float tempo);
+ virtual float MinTempoPlatform();
+ virtual float MaxTempoPlatform();
+ void SetLevelVQ(int level);
+ int GetLevelVQ();
+ void SetGuiRender(bool gui);
+ bool GetGuiRender();
+ void SetVideoRender(bool video);
+ bool GetVideoRender();
+ unsigned int GetMaxPassthroughOffSyncDuration() const;
+
+ void SetPlayTimes(time_t start, int64_t current, int64_t min, int64_t max);
+ int64_t GetMaxTime();
+
+ // settings
+ CVideoSettings GetVideoSettings();
+ void SetVideoSettings(CVideoSettings &settings);
+ CVideoSettingsLocked& GetVideoSettingsLocked();
+
+protected:
+ CProcessInfo();
+ static std::map<std::string, CreateProcessControl> m_processControls;
+ CDataCacheCore *m_dataCache = nullptr;
+
+ // player video info
+ bool m_videoIsHWDecoder;
+ std::string m_videoDecoderName;
+ std::string m_videoDeintMethod;
+ std::string m_videoPixelFormat;
+ std::string m_videoStereoMode;
+ int m_videoWidth;
+ int m_videoHeight;
+ float m_videoFPS;
+ float m_videoDAR;
+ bool m_videoIsInterlaced;
+ std::list<EINTERLACEMETHOD> m_deintMethods;
+ EINTERLACEMETHOD m_deintMethodDefault;
+ mutable CCriticalSection m_videoCodecSection;
+ CVideoBufferManager m_videoBufferManager;
+ std::vector<AVPixelFormat> m_pixFormats;
+
+ // player audio info
+ std::string m_audioDecoderName;
+ std::string m_audioChannels;
+ int m_audioSampleRate;
+ int m_audioBitsPerSample;
+ CCriticalSection m_audioCodecSection;
+
+ // render info
+ CCriticalSection m_renderSection;
+ bool m_isClockSync;
+ CRenderInfo m_renderInfo;
+ int m_renderBufQueued = 0;
+ int m_renderBufFree = 0;
+ int m_renderBufDiscard = 0;
+
+ // player states
+ CCriticalSection m_stateSection;
+ bool m_stateSeeking;
+ std::atomic_int m_levelVQ;
+ std::atomic_bool m_renderGuiLayer;
+ std::atomic_bool m_renderVideoLayer;
+ float m_tempo;
+ float m_newTempo;
+ float m_speed;
+ float m_newSpeed;
+ bool m_frameAdvance;
+ time_t m_startTime;
+ int64_t m_time;
+ int64_t m_timeMax;
+ int64_t m_timeMin;
+ bool m_realTimeStream;
+
+ // settings
+ CCriticalSection m_settingsSection;
+ CVideoSettings m_videoSettings;
+ std::unique_ptr<CVideoSettingsLocked> m_videoSettingsLocked;
+};
diff --git a/xbmc/cores/VideoPlayer/Process/X11/CMakeLists.txt b/xbmc/cores/VideoPlayer/Process/X11/CMakeLists.txt
new file mode 100644
index 0000000..d30d213
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/X11/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES ProcessInfoX11.cpp)
+
+set(HEADERS ProcessInfoX11.h)
+
+core_add_library(processX11)
diff --git a/xbmc/cores/VideoPlayer/Process/X11/ProcessInfoX11.cpp b/xbmc/cores/VideoPlayer/Process/X11/ProcessInfoX11.cpp
new file mode 100644
index 0000000..3fd91f4
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/X11/ProcessInfoX11.cpp
@@ -0,0 +1,59 @@
+/*
+ * 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 "ProcessInfoX11.h"
+
+#include <mutex>
+
+
+using namespace VIDEOPLAYER;
+
+CProcessInfo* CProcessInfoX11::Create()
+{
+ return new CProcessInfoX11();
+}
+
+void CProcessInfoX11::Register()
+{
+ CProcessInfo::RegisterProcessControl("X11", CProcessInfoX11::Create);
+}
+
+void CProcessInfoX11::SetSwDeinterlacingMethods()
+{
+ // first populate with the defaults from base implementation
+ CProcessInfo::SetSwDeinterlacingMethods();
+
+ std::list<EINTERLACEMETHOD> methods;
+ {
+ // get the current methods
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+ methods = m_deintMethods;
+ }
+ // add bob and blend deinterlacer for osx
+ methods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_RENDER_BOB);
+ methods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_RENDER_BLEND);
+
+ // update with the new methods list
+ UpdateDeinterlacingMethods(methods);
+}
+
+std::vector<AVPixelFormat> CProcessInfoX11::GetRenderFormats()
+{
+ return
+ {
+ AV_PIX_FMT_YUV420P,
+ AV_PIX_FMT_YUV420P9,
+ AV_PIX_FMT_YUV420P10,
+ AV_PIX_FMT_YUV420P12,
+ AV_PIX_FMT_YUV420P14,
+ AV_PIX_FMT_YUV420P16,
+ AV_PIX_FMT_NV12,
+ AV_PIX_FMT_YUYV422,
+ AV_PIX_FMT_UYVY422
+ };
+}
diff --git a/xbmc/cores/VideoPlayer/Process/X11/ProcessInfoX11.h b/xbmc/cores/VideoPlayer/Process/X11/ProcessInfoX11.h
new file mode 100644
index 0000000..72beefa
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/X11/ProcessInfoX11.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "../ProcessInfo.h"
+
+namespace VIDEOPLAYER
+{
+
+class CProcessInfoX11 : public CProcessInfo
+{
+public:
+ static CProcessInfo* Create();
+ static void Register();
+
+ void SetSwDeinterlacingMethods() override;
+ std::vector<AVPixelFormat> GetRenderFormats() override;
+};
+
+}
diff --git a/xbmc/cores/VideoPlayer/Process/android/CMakeLists.txt b/xbmc/cores/VideoPlayer/Process/android/CMakeLists.txt
new file mode 100644
index 0000000..c1505b1
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/android/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES ProcessInfoAndroid.cpp)
+
+set(HEADERS ProcessInfoAndroid.h)
+
+core_add_library(processAndroid)
diff --git a/xbmc/cores/VideoPlayer/Process/android/ProcessInfoAndroid.cpp b/xbmc/cores/VideoPlayer/Process/android/ProcessInfoAndroid.cpp
new file mode 100644
index 0000000..88e9a1e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/android/ProcessInfoAndroid.cpp
@@ -0,0 +1,41 @@
+/*
+ * 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 "ProcessInfoAndroid.h"
+
+#include "ServiceBroker.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+using namespace VIDEOPLAYER;
+
+CProcessInfo* CProcessInfoAndroid::Create()
+{
+ return new CProcessInfoAndroid();
+}
+
+void CProcessInfoAndroid::Register()
+{
+ CProcessInfo::RegisterProcessControl("android", CProcessInfoAndroid::Create);
+}
+
+EINTERLACEMETHOD CProcessInfoAndroid::GetFallbackDeintMethod()
+{
+ return EINTERLACEMETHOD::VS_INTERLACEMETHOD_DEINTERLACE_HALF;
+}
+
+bool CProcessInfoAndroid::WantsRawPassthrough()
+{
+ const std::string device = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGHDEVICE);
+
+ if (std::string::npos != device.find("(RAW)"))
+ return true;
+
+ return false;
+}
diff --git a/xbmc/cores/VideoPlayer/Process/android/ProcessInfoAndroid.h b/xbmc/cores/VideoPlayer/Process/android/ProcessInfoAndroid.h
new file mode 100644
index 0000000..4499c8d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/android/ProcessInfoAndroid.h
@@ -0,0 +1,27 @@
+/*
+ * 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/IPlayer.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+
+namespace VIDEOPLAYER
+{
+
+class CProcessInfoAndroid : public CProcessInfo
+{
+public:
+ CProcessInfoAndroid() = default;
+ static CProcessInfo* Create();
+ static void Register();
+ EINTERLACEMETHOD GetFallbackDeintMethod() override;
+ bool WantsRawPassthrough() override;
+};
+
+} // namespace VIDEOPLAYER
diff --git a/xbmc/cores/VideoPlayer/Process/gbm/CMakeLists.txt b/xbmc/cores/VideoPlayer/Process/gbm/CMakeLists.txt
new file mode 100644
index 0000000..852fc96
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/gbm/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES ProcessInfoGBM.cpp)
+set(HEADERS ProcessInfoGBM.h)
+
+core_add_library(processGBM)
diff --git a/xbmc/cores/VideoPlayer/Process/gbm/ProcessInfoGBM.cpp b/xbmc/cores/VideoPlayer/Process/gbm/ProcessInfoGBM.cpp
new file mode 100644
index 0000000..be97902
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/gbm/ProcessInfoGBM.cpp
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+#include "ProcessInfoGBM.h"
+
+#include "cores/VideoPlayer/Buffers/VideoBufferPoolDMA.h"
+
+using namespace VIDEOPLAYER;
+
+CProcessInfo* CProcessInfoGBM::Create()
+{
+ return new CProcessInfoGBM();
+}
+
+void CProcessInfoGBM::Register()
+{
+ CProcessInfo::RegisterProcessControl("gbm", CProcessInfoGBM::Create);
+}
+
+CProcessInfoGBM::CProcessInfoGBM()
+{
+ m_videoBufferManager.RegisterPool(std::make_shared<CVideoBufferPoolDMA>());
+}
+
+EINTERLACEMETHOD CProcessInfoGBM::GetFallbackDeintMethod()
+{
+#if defined(__arm__)
+ return EINTERLACEMETHOD::VS_INTERLACEMETHOD_DEINTERLACE_HALF;
+#else
+ return CProcessInfo::GetFallbackDeintMethod();
+#endif
+}
diff --git a/xbmc/cores/VideoPlayer/Process/gbm/ProcessInfoGBM.h b/xbmc/cores/VideoPlayer/Process/gbm/ProcessInfoGBM.h
new file mode 100644
index 0000000..0524d6e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/gbm/ProcessInfoGBM.h
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "cores/IPlayer.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+
+namespace VIDEOPLAYER
+{
+
+class CProcessInfoGBM : public CProcessInfo
+{
+public:
+ static CProcessInfo* Create();
+ static void Register();
+
+ CProcessInfoGBM();
+ EINTERLACEMETHOD GetFallbackDeintMethod() override;
+};
+
+} // namespace VIDEOPLAYER
diff --git a/xbmc/cores/VideoPlayer/Process/ios/CMakeLists.txt b/xbmc/cores/VideoPlayer/Process/ios/CMakeLists.txt
new file mode 100644
index 0000000..999b2da
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/ios/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES ProcessInfoIOS.cpp)
+
+set(HEADERS ProcessInfoIOS.h)
+
+core_add_library(processios)
diff --git a/xbmc/cores/VideoPlayer/Process/ios/ProcessInfoIOS.h b/xbmc/cores/VideoPlayer/Process/ios/ProcessInfoIOS.h
new file mode 100644
index 0000000..2b1824d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/ios/ProcessInfoIOS.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "../ProcessInfo.h"
+
+namespace VIDEOPLAYER
+{
+
+class CProcessInfoIOS : public CProcessInfo
+{
+public:
+ static CProcessInfo* Create();
+ static void Register();
+
+ void SetSwDeinterlacingMethods() override;
+};
+
+}
diff --git a/xbmc/cores/VideoPlayer/Process/ios/ProcessInfoIos.cpp b/xbmc/cores/VideoPlayer/Process/ios/ProcessInfoIos.cpp
new file mode 100644
index 0000000..ee3bc3e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/ios/ProcessInfoIos.cpp
@@ -0,0 +1,43 @@
+/*
+ * 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 "ProcessInfoIOS.h"
+
+#include <mutex>
+
+
+using namespace VIDEOPLAYER;
+
+CProcessInfo* CProcessInfoIOS::Create()
+{
+ return new CProcessInfoIOS();
+}
+
+void CProcessInfoIOS::Register()
+{
+ CProcessInfo::RegisterProcessControl("ios", CProcessInfoIOS::Create);
+}
+
+void CProcessInfoIOS::SetSwDeinterlacingMethods()
+{
+ // first populate with the defaults from base implementation
+ CProcessInfo::SetSwDeinterlacingMethods();
+
+ std::list<EINTERLACEMETHOD> methods;
+ {
+ // get the current methods
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+ methods = m_deintMethods;
+ }
+ // add bob deinterlacer for ios
+ methods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_RENDER_BOB);
+
+ // update with the new methods list
+ UpdateDeinterlacingMethods(methods);
+}
+
diff --git a/xbmc/cores/VideoPlayer/Process/osx/CMakeLists.txt b/xbmc/cores/VideoPlayer/Process/osx/CMakeLists.txt
new file mode 100644
index 0000000..b4c0da5
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/osx/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES ProcessInfoOSX.cpp)
+
+set(HEADERS ProcessInfoOSX.h)
+
+core_add_library(processosx)
diff --git a/xbmc/cores/VideoPlayer/Process/osx/ProcessInfoOSX.cpp b/xbmc/cores/VideoPlayer/Process/osx/ProcessInfoOSX.cpp
new file mode 100644
index 0000000..b301961
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/osx/ProcessInfoOSX.cpp
@@ -0,0 +1,58 @@
+/*
+ * 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 "ProcessInfoOSX.h"
+
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+
+#include <mutex>
+
+using namespace VIDEOPLAYER;
+
+CProcessInfo* CProcessInfoOSX::Create()
+{
+ return new CProcessInfoOSX();
+}
+
+void CProcessInfoOSX::Register()
+{
+ CProcessInfo::RegisterProcessControl("osx", CProcessInfoOSX::Create);
+}
+
+void CProcessInfoOSX::SetSwDeinterlacingMethods()
+{
+ // first populate with the defaults from base implementation
+ CProcessInfo::SetSwDeinterlacingMethods();
+
+ std::list<EINTERLACEMETHOD> methods;
+ {
+ // get the current methods
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+ methods = m_deintMethods;
+ }
+ // add bob and blend deinterlacer for osx
+ methods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_RENDER_BOB);
+ methods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_RENDER_BLEND);
+
+ // update with the new methods list
+ UpdateDeinterlacingMethods(methods);
+}
+
+std::vector<AVPixelFormat> CProcessInfoOSX::GetRenderFormats()
+{
+ std::vector<AVPixelFormat> formats;
+ formats.push_back(AV_PIX_FMT_YUV420P);
+ formats.push_back(AV_PIX_FMT_YUV420P10);
+ formats.push_back(AV_PIX_FMT_YUV420P16);
+ formats.push_back(AV_PIX_FMT_NV12);
+ formats.push_back(AV_PIX_FMT_YUYV422);
+ formats.push_back(AV_PIX_FMT_UYVY422);
+
+ return formats;
+}
+
diff --git a/xbmc/cores/VideoPlayer/Process/osx/ProcessInfoOSX.h b/xbmc/cores/VideoPlayer/Process/osx/ProcessInfoOSX.h
new file mode 100644
index 0000000..2013d6d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/osx/ProcessInfoOSX.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "../ProcessInfo.h"
+#include "cores/IPlayer.h"
+
+namespace VIDEOPLAYER
+{
+
+class CProcessInfoOSX : public CProcessInfo
+{
+public:
+ static CProcessInfo* Create();
+ static void Register();
+
+ void SetSwDeinterlacingMethods() override;
+ std::vector<AVPixelFormat> GetRenderFormats() override;
+};
+
+}
diff --git a/xbmc/cores/VideoPlayer/Process/wayland/CMakeLists.txt b/xbmc/cores/VideoPlayer/Process/wayland/CMakeLists.txt
new file mode 100644
index 0000000..bbc6cbd
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/wayland/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES ProcessInfoWayland.cpp)
+
+set(HEADERS ProcessInfoWayland.h)
+
+core_add_library(processWayland)
diff --git a/xbmc/cores/VideoPlayer/Process/wayland/ProcessInfoWayland.cpp b/xbmc/cores/VideoPlayer/Process/wayland/ProcessInfoWayland.cpp
new file mode 100644
index 0000000..b47a93a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/wayland/ProcessInfoWayland.cpp
@@ -0,0 +1,71 @@
+/*
+ * 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 "ProcessInfoWayland.h"
+
+#include "cores/VideoPlayer/Buffers/VideoBufferPoolDMA.h"
+
+#include <mutex>
+
+using namespace VIDEOPLAYER;
+
+CProcessInfo* CProcessInfoWayland::Create()
+{
+ return new CProcessInfoWayland();
+}
+
+void CProcessInfoWayland::Register()
+{
+ CProcessInfo::RegisterProcessControl("Wayland", CProcessInfoWayland::Create);
+}
+
+CProcessInfoWayland::CProcessInfoWayland()
+{
+ m_videoBufferManager.RegisterPool(std::make_shared<CVideoBufferPoolDMA>());
+}
+
+void CProcessInfoWayland::SetSwDeinterlacingMethods()
+{
+ // first populate with the defaults from base implementation
+ CProcessInfo::SetSwDeinterlacingMethods();
+
+ std::list<EINTERLACEMETHOD> methods;
+ {
+ // get the current methods
+ std::unique_lock<CCriticalSection> lock(m_videoCodecSection);
+ methods = m_deintMethods;
+ }
+ // add bob and blend deinterlacer
+ methods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_RENDER_BOB);
+ methods.push_back(EINTERLACEMETHOD::VS_INTERLACEMETHOD_RENDER_BLEND);
+
+ // update with the new methods list
+ UpdateDeinterlacingMethods(methods);
+}
+
+std::vector<AVPixelFormat> CProcessInfoWayland::GetRenderFormats()
+{
+ return
+ {
+ // GL & GLES
+ AV_PIX_FMT_YUV420P,
+ AV_PIX_FMT_NV12,
+
+#if defined(HAS_GL)
+ // Full GL only at the moment
+ // TODO YUV420Pxx need runtime-checking for GL_ALPHA16/GL_LUMINANCE16 support
+ AV_PIX_FMT_YUV420P9,
+ AV_PIX_FMT_YUV420P10,
+ AV_PIX_FMT_YUV420P12,
+ AV_PIX_FMT_YUV420P14,
+ AV_PIX_FMT_YUV420P16,
+ AV_PIX_FMT_YUYV422,
+ AV_PIX_FMT_UYVY422
+#endif
+ };
+}
diff --git a/xbmc/cores/VideoPlayer/Process/wayland/ProcessInfoWayland.h b/xbmc/cores/VideoPlayer/Process/wayland/ProcessInfoWayland.h
new file mode 100644
index 0000000..91df3f7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/wayland/ProcessInfoWayland.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "../ProcessInfo.h"
+
+namespace VIDEOPLAYER
+{
+
+class CProcessInfoWayland : public CProcessInfo
+{
+public:
+ static CProcessInfo* Create();
+ static void Register();
+
+ CProcessInfoWayland();
+ void SetSwDeinterlacingMethods() override;
+ std::vector<AVPixelFormat> GetRenderFormats() override;
+};
+
+}
diff --git a/xbmc/cores/VideoPlayer/Process/windows/CMakeLists.txt b/xbmc/cores/VideoPlayer/Process/windows/CMakeLists.txt
new file mode 100644
index 0000000..bb5047f
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/windows/CMakeLists.txt
@@ -0,0 +1,9 @@
+if(CORE_SYSTEM_NAME STREQUAL windows)
+ set(SOURCES ProcessInfoWin.cpp)
+ set(HEADERS ProcessInfoWin.h)
+elseif(CORE_SYSTEM_NAME STREQUAL windowsstore)
+ set(SOURCES ProcessInfoWin10.cpp)
+ set(HEADERS ProcessInfoWin10.h)
+endif()
+
+core_add_library(processwin)
diff --git a/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin.cpp b/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin.cpp
new file mode 100644
index 0000000..eaa6d77
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin.cpp
@@ -0,0 +1,41 @@
+/*
+ * 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 "ProcessInfoWin.h"
+
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+
+using namespace VIDEOPLAYER;
+
+CProcessInfo* CProcessInfoWin::Create()
+{
+ return new CProcessInfoWin();
+}
+
+void CProcessInfoWin::Register()
+{
+ RegisterProcessControl("win", Create);
+}
+
+EINTERLACEMETHOD CProcessInfoWin::GetFallbackDeintMethod()
+{
+ return VS_INTERLACEMETHOD_AUTO;
+}
+
+std::vector<AVPixelFormat> CProcessInfoWin::GetRenderFormats()
+{
+ return {
+ AV_PIX_FMT_D3D11VA_VLD,
+ AV_PIX_FMT_NV12,
+ AV_PIX_FMT_YUV420P,
+ AV_PIX_FMT_P010,
+ AV_PIX_FMT_YUV420P10,
+ AV_PIX_FMT_P016,
+ AV_PIX_FMT_YUV420P16
+ };
+}
diff --git a/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin.h b/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin.h
new file mode 100644
index 0000000..4393a35
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "../ProcessInfo.h"
+#include "cores/IPlayer.h"
+
+namespace VIDEOPLAYER
+{
+
+class CProcessInfoWin : public CProcessInfo
+{
+public:
+ static CProcessInfo* Create();
+ static void Register();
+
+ EINTERLACEMETHOD GetFallbackDeintMethod() override;
+ std::vector<AVPixelFormat> GetRenderFormats() override;
+};
+
+}
diff --git a/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin10.cpp b/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin10.cpp
new file mode 100644
index 0000000..00b6790
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin10.cpp
@@ -0,0 +1,43 @@
+/*
+ * 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 "ProcessInfoWin10.h"
+
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+
+#include <set>
+
+using namespace VIDEOPLAYER;
+
+CProcessInfo* CProcessInfoWin10::Create()
+{
+ return new CProcessInfoWin10();
+}
+
+void CProcessInfoWin10::Register()
+{
+ CProcessInfo::RegisterProcessControl("win10", CProcessInfoWin10::Create);
+}
+
+EINTERLACEMETHOD CProcessInfoWin10::GetFallbackDeintMethod()
+{
+ return EINTERLACEMETHOD::VS_INTERLACEMETHOD_AUTO;
+}
+
+std::vector<AVPixelFormat> CProcessInfoWin10::GetRenderFormats()
+{
+ return {
+ AV_PIX_FMT_D3D11VA_VLD,
+ AV_PIX_FMT_NV12,
+ AV_PIX_FMT_YUV420P,
+ AV_PIX_FMT_P010,
+ AV_PIX_FMT_YUV420P10,
+ AV_PIX_FMT_P016,
+ AV_PIX_FMT_YUV420P16
+ };
+}
diff --git a/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin10.h b/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin10.h
new file mode 100644
index 0000000..acb06e7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/Process/windows/ProcessInfoWin10.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "../ProcessInfo.h"
+#include "cores/IPlayer.h"
+
+namespace VIDEOPLAYER
+{
+
+class CProcessInfoWin10 : public CProcessInfo
+{
+public:
+ static CProcessInfo* Create();
+ static void Register();
+
+ EINTERLACEMETHOD GetFallbackDeintMethod() override;
+ std::vector<AVPixelFormat> GetRenderFormats() override;
+};
+
+}
diff --git a/xbmc/cores/VideoPlayer/VideoPlayer.cpp b/xbmc/cores/VideoPlayer/VideoPlayer.cpp
new file mode 100644
index 0000000..fcba11c
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayer.cpp
@@ -0,0 +1,5364 @@
+/*
+ * 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 "VideoPlayer.h"
+
+#include "DVDCodecs/DVDCodecUtils.h"
+#include "DVDDemuxers/DVDDemux.h"
+#include "DVDDemuxers/DVDDemuxCC.h"
+#include "DVDDemuxers/DVDDemuxFFmpeg.h"
+#include "DVDDemuxers/DVDDemuxUtils.h"
+#include "DVDDemuxers/DVDDemuxVobsub.h"
+#include "DVDDemuxers/DVDFactoryDemuxer.h"
+#include "DVDInputStreams/DVDFactoryInputStream.h"
+#include "DVDInputStreams/DVDInputStream.h"
+#include "DVDMessage.h"
+#include "VideoPlayerVideo.h"
+#include "application/Application.h"
+
+#include <mutex>
+#if defined(HAVE_LIBBLURAY)
+#include "DVDInputStreams/DVDInputStreamBluray.h"
+#endif
+#include "DVDInputStreams/DVDInputStreamNavigator.h"
+#include "DVDInputStreams/InputStreamPVRBase.h"
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "VideoPlayerAudio.h"
+#include "VideoPlayerRadioRDS.h"
+#include "cores/DataCacheCore.h"
+#include "cores/EdlEdit.h"
+#include "cores/FFmpeg.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderManager.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/StereoscopicsManager.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "threads/SingleLock.h"
+#include "utils/FontUtils.h"
+#include "utils/JobManager.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StreamDetails.h"
+#include "utils/StreamUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/Bookmark.h"
+#include "video/VideoInfoTag.h"
+#include "windowing/WinSystem.h"
+
+#include <iterator>
+#include <utility>
+
+using namespace std::chrono_literals;
+
+//------------------------------------------------------------------------------
+// selection streams
+//------------------------------------------------------------------------------
+
+#define PREDICATE_RETURN(lh, rh) \
+ do { \
+ if((lh) != (rh)) \
+ return (lh) > (rh); \
+ } while(0)
+
+class PredicateSubtitleFilter
+{
+private:
+ std::string audiolang;
+ bool original;
+ bool nosub;
+ bool onlyforced;
+ int currentSubStream;
+public:
+ /** \brief The class' operator() decides if the given (subtitle) SelectionStream is relevant wrt.
+ * preferred subtitle language and audio language. If the subtitle is relevant <B>false</B> false is returned.
+ *
+ * A subtitle is relevant if
+ * - it was previously selected, or
+ * - it's an external sub, or
+ * - it's a forced sub and "original stream's language" was selected and audio stream language matches, or
+ * - it's a default and a forced sub (could lead to users seeing forced subs in a foreign language!), or
+ * - its language matches the preferred subtitle's language (unequal to "original stream's language")
+ */
+ explicit PredicateSubtitleFilter(const std::string& lang, int subStream)
+ : audiolang(lang),
+ currentSubStream(subStream)
+ {
+ const std::string subtitleLang = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_SUBTITLELANGUAGE);
+ original = StringUtils::EqualsNoCase(subtitleLang, "original");
+ nosub = StringUtils::EqualsNoCase(subtitleLang, "none");
+ onlyforced = StringUtils::EqualsNoCase(subtitleLang, "forced_only");
+ };
+
+ bool operator()(const SelectionStream& ss) const
+ {
+ if (ss.type_index == currentSubStream)
+ return false;
+
+ if (nosub)
+ return true;
+
+ if (onlyforced)
+ {
+ if ((ss.flags & StreamFlags::FLAG_FORCED) && g_LangCodeExpander.CompareISO639Codes(ss.language, audiolang))
+ return false;
+ else
+ return true;
+ }
+
+ if(STREAM_SOURCE_MASK(ss.source) == STREAM_SOURCE_DEMUX_SUB || STREAM_SOURCE_MASK(ss.source) == STREAM_SOURCE_TEXT)
+ return false;
+
+ if ((ss.flags & StreamFlags::FLAG_FORCED) && g_LangCodeExpander.CompareISO639Codes(ss.language, audiolang))
+ return false;
+
+ if ((ss.flags & StreamFlags::FLAG_FORCED) && (ss.flags & StreamFlags::FLAG_DEFAULT))
+ return false;
+
+ if (ss.language == "cc" && ss.flags & StreamFlags::FLAG_HEARING_IMPAIRED)
+ return false;
+
+ if(!original)
+ {
+ std::string subtitle_language = g_langInfo.GetSubtitleLanguage();
+ if (g_LangCodeExpander.CompareISO639Codes(subtitle_language, ss.language))
+ return false;
+ }
+ else if (ss.flags & StreamFlags::FLAG_DEFAULT)
+ return false;
+
+ return true;
+ }
+};
+
+class PredicateAudioFilter
+{
+private:
+ int currentAudioStream;
+ bool preferStereo;
+public:
+ explicit PredicateAudioFilter(int audioStream, bool preferStereo)
+ : currentAudioStream(audioStream)
+ , preferStereo(preferStereo)
+ {
+ };
+ bool operator()(const SelectionStream& lh, const SelectionStream& rh)
+ {
+ PREDICATE_RETURN(lh.type_index == currentAudioStream
+ , rh.type_index == currentAudioStream);
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ if (!StringUtils::EqualsNoCase(settings->GetString(CSettings::SETTING_LOCALE_AUDIOLANGUAGE), "mediadefault"))
+ {
+ if (!StringUtils::EqualsNoCase(settings->GetString(CSettings::SETTING_LOCALE_AUDIOLANGUAGE), "original"))
+ {
+ std::string audio_language = g_langInfo.GetAudioLanguage();
+ PREDICATE_RETURN(g_LangCodeExpander.CompareISO639Codes(audio_language, lh.language)
+ , g_LangCodeExpander.CompareISO639Codes(audio_language, rh.language));
+ }
+ else
+ {
+ PREDICATE_RETURN(lh.flags & StreamFlags::FLAG_ORIGINAL,
+ rh.flags & StreamFlags::FLAG_ORIGINAL);
+ }
+
+ bool hearingimp = settings->GetBool(CSettings::SETTING_ACCESSIBILITY_AUDIOHEARING);
+ PREDICATE_RETURN(!hearingimp ? !(lh.flags & StreamFlags::FLAG_HEARING_IMPAIRED) : lh.flags & StreamFlags::FLAG_HEARING_IMPAIRED
+ , !hearingimp ? !(rh.flags & StreamFlags::FLAG_HEARING_IMPAIRED) : rh.flags & StreamFlags::FLAG_HEARING_IMPAIRED);
+
+ bool visualimp = settings->GetBool(CSettings::SETTING_ACCESSIBILITY_AUDIOVISUAL);
+ PREDICATE_RETURN(!visualimp ? !(lh.flags & StreamFlags::FLAG_VISUAL_IMPAIRED) : lh.flags & StreamFlags::FLAG_VISUAL_IMPAIRED
+ , !visualimp ? !(rh.flags & StreamFlags::FLAG_VISUAL_IMPAIRED) : rh.flags & StreamFlags::FLAG_VISUAL_IMPAIRED);
+ }
+
+ if (settings->GetBool(CSettings::SETTING_VIDEOPLAYER_PREFERDEFAULTFLAG))
+ {
+ PREDICATE_RETURN(lh.flags & StreamFlags::FLAG_DEFAULT,
+ rh.flags & StreamFlags::FLAG_DEFAULT);
+ }
+
+ if (preferStereo)
+ PREDICATE_RETURN(lh.channels == 2,
+ rh.channels == 2);
+ else
+ PREDICATE_RETURN(lh.channels,
+ rh.channels);
+
+ PREDICATE_RETURN(StreamUtils::GetCodecPriority(lh.codec),
+ StreamUtils::GetCodecPriority(rh.codec));
+
+ PREDICATE_RETURN(lh.flags & StreamFlags::FLAG_DEFAULT,
+ rh.flags & StreamFlags::FLAG_DEFAULT);
+ return false;
+ };
+};
+
+/** \brief The class' operator() decides if the given (subtitle) SelectionStream lh is 'better than' the given (subtitle) SelectionStream rh.
+* If lh is 'better than' rh the return value is true, false otherwise.
+*
+* A subtitle lh is 'better than' a subtitle rh (in evaluation order) if
+* - lh was previously selected, or
+* - lh is an external sub and rh not, or
+* - lh is a forced sub and ("original stream's language" was selected or subtitles are off) and audio stream language matches sub language and rh not, or
+* - lh is a default sub and ("original stream's language" was selected or subtitles are off) and audio stream language matches sub language and rh not, or
+* - lh is a sub where audio stream language matches sub language and (original stream's language" was selected or subtitles are off) and rh not, or
+* - lh is a forced sub and a default sub ("original stream's language" was selected or subtitles are off)
+* - lh is an external sub and its language matches the preferred subtitle's language (unequal to "original stream's language") and rh not, or
+* - lh is language matches the preferred subtitle's language (unequal to "original stream's language") and rh not, or
+* - lh is a default sub and rh not
+*/
+class PredicateSubtitlePriority
+{
+private:
+ std::string audiolang;
+ bool original;
+ bool subson;
+ PredicateSubtitleFilter filter;
+ int subStream;
+public:
+ explicit PredicateSubtitlePriority(const std::string& lang, int stream, bool ison)
+ : audiolang(lang),
+ original(StringUtils::EqualsNoCase(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_SUBTITLELANGUAGE), "original")),
+ subson(ison),
+ filter(lang, stream),
+ subStream(stream)
+ {
+ };
+
+ bool relevant(const SelectionStream& ss) const
+ {
+ return !filter(ss);
+ }
+
+ bool operator()(const SelectionStream& lh, const SelectionStream& rh) const
+ {
+ PREDICATE_RETURN(relevant(lh)
+ , relevant(rh));
+
+ PREDICATE_RETURN(lh.type_index == subStream
+ , rh.type_index == subStream);
+
+ // prefer external subs
+ PREDICATE_RETURN(STREAM_SOURCE_MASK(lh.source) == STREAM_SOURCE_DEMUX_SUB || STREAM_SOURCE_MASK(lh.source) == STREAM_SOURCE_TEXT
+ , STREAM_SOURCE_MASK(rh.source) == STREAM_SOURCE_DEMUX_SUB || STREAM_SOURCE_MASK(rh.source) == STREAM_SOURCE_TEXT);
+
+ if (!subson || original)
+ {
+ PREDICATE_RETURN(lh.flags & StreamFlags::FLAG_FORCED && g_LangCodeExpander.CompareISO639Codes(lh.language, audiolang)
+ , rh.flags & StreamFlags::FLAG_FORCED && g_LangCodeExpander.CompareISO639Codes(rh.language, audiolang));
+
+ PREDICATE_RETURN(lh.flags & StreamFlags::FLAG_DEFAULT && g_LangCodeExpander.CompareISO639Codes(lh.language, audiolang)
+ , rh.flags & StreamFlags::FLAG_DEFAULT && g_LangCodeExpander.CompareISO639Codes(rh.language, audiolang));
+
+ PREDICATE_RETURN(g_LangCodeExpander.CompareISO639Codes(lh.language, audiolang)
+ , g_LangCodeExpander.CompareISO639Codes(rh.language, audiolang));
+
+ PREDICATE_RETURN((lh.flags & (StreamFlags::FLAG_FORCED | StreamFlags::FLAG_DEFAULT)) == (StreamFlags::FLAG_FORCED | StreamFlags::FLAG_DEFAULT)
+ , (rh.flags & (StreamFlags::FLAG_FORCED | StreamFlags::FLAG_DEFAULT)) == (StreamFlags::FLAG_FORCED | StreamFlags::FLAG_DEFAULT));
+
+ }
+
+ std::string subtitle_language = g_langInfo.GetSubtitleLanguage();
+ if (!original)
+ {
+ PREDICATE_RETURN((STREAM_SOURCE_MASK(lh.source) == STREAM_SOURCE_DEMUX_SUB || STREAM_SOURCE_MASK(lh.source) == STREAM_SOURCE_TEXT) && g_LangCodeExpander.CompareISO639Codes(subtitle_language, lh.language)
+ , (STREAM_SOURCE_MASK(rh.source) == STREAM_SOURCE_DEMUX_SUB || STREAM_SOURCE_MASK(rh.source) == STREAM_SOURCE_TEXT) && g_LangCodeExpander.CompareISO639Codes(subtitle_language, rh.language));
+ }
+
+ if (!original)
+ {
+ PREDICATE_RETURN(g_LangCodeExpander.CompareISO639Codes(subtitle_language, lh.language)
+ , g_LangCodeExpander.CompareISO639Codes(subtitle_language, rh.language));
+
+ bool hearingimp = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_ACCESSIBILITY_SUBHEARING);
+ PREDICATE_RETURN(!hearingimp ? !(lh.flags & StreamFlags::FLAG_HEARING_IMPAIRED) : lh.flags & StreamFlags::FLAG_HEARING_IMPAIRED
+ , !hearingimp ? !(rh.flags & StreamFlags::FLAG_HEARING_IMPAIRED) : rh.flags & StreamFlags::FLAG_HEARING_IMPAIRED);
+ }
+
+ PREDICATE_RETURN(lh.flags & StreamFlags::FLAG_DEFAULT
+ , rh.flags & StreamFlags::FLAG_DEFAULT);
+
+ return false;
+ }
+};
+
+class PredicateVideoFilter
+{
+private:
+ int currentVideoStream;
+public:
+ explicit PredicateVideoFilter(int videoStream) : currentVideoStream(videoStream)
+ {
+ };
+ bool operator()(const SelectionStream& lh, const SelectionStream& rh)
+ {
+ PREDICATE_RETURN(lh.type_index == currentVideoStream,
+ rh.type_index == currentVideoStream);
+
+ PREDICATE_RETURN(lh.flags & StreamFlags::FLAG_DEFAULT,
+ rh.flags & StreamFlags::FLAG_DEFAULT);
+ return false;
+ }
+};
+
+void CSelectionStreams::Clear(StreamType type, StreamSource source)
+{
+ auto new_end = std::remove_if(m_Streams.begin(), m_Streams.end(),
+ [type, source](const SelectionStream &stream)
+ {
+ return (type == STREAM_NONE || stream.type == type) &&
+ (source == 0 || stream.source == source);
+ });
+ m_Streams.erase(new_end, m_Streams.end());
+}
+
+SelectionStream& CSelectionStreams::Get(StreamType type, int index)
+{
+ return const_cast<SelectionStream&>(std::as_const(*this).Get(type, index));
+}
+
+const SelectionStream& CSelectionStreams::Get(StreamType type, int index) const
+{
+ int count = -1;
+ for (size_t i = 0; i < m_Streams.size(); ++i)
+ {
+ if (m_Streams[i].type != type)
+ continue;
+ count++;
+ if (count == index)
+ return m_Streams[i];
+ }
+ return m_invalid;
+}
+
+std::vector<SelectionStream> CSelectionStreams::Get(StreamType type)
+{
+ std::vector<SelectionStream> streams;
+ std::copy_if(m_Streams.begin(), m_Streams.end(), std::back_inserter(streams),
+ [type](const SelectionStream &stream)
+ {
+ return stream.type == type;
+ });
+ return streams;
+}
+
+bool CSelectionStreams::Get(StreamType type, StreamFlags flag, SelectionStream& out)
+{
+ for(size_t i=0;i<m_Streams.size();i++)
+ {
+ if(m_Streams[i].type != type)
+ continue;
+ if((m_Streams[i].flags & flag) != flag)
+ continue;
+ out = m_Streams[i];
+ return true;
+ }
+ return false;
+}
+
+int CSelectionStreams::TypeIndexOf(StreamType type, int source, int64_t demuxerId, int id) const
+{
+ if (id < 0)
+ return -1;
+
+ auto it = std::find_if(m_Streams.begin(), m_Streams.end(),
+ [&](const SelectionStream& stream) {return stream.type == type
+ && stream.source == source && stream.id == id
+ && stream.demuxerId == demuxerId;});
+
+ if (it != m_Streams.end())
+ return it->type_index;
+ else
+ return -1;
+}
+
+int CSelectionStreams::Source(StreamSource source, const std::string& filename)
+{
+ int index = source - 1;
+ for (size_t i=0; i<m_Streams.size(); i++)
+ {
+ SelectionStream &s = m_Streams[i];
+ if (STREAM_SOURCE_MASK(s.source) != source)
+ continue;
+ // if it already exists, return same
+ if (s.filename == filename)
+ return s.source;
+ if (index < s.source)
+ index = s.source;
+ }
+ // return next index
+ return index + 1;
+}
+
+void CSelectionStreams::Update(SelectionStream& s)
+{
+ int index = TypeIndexOf(s.type, s.source, s.demuxerId, s.id);
+ if(index >= 0)
+ {
+ SelectionStream& o = Get(s.type, index);
+ s.type_index = o.type_index;
+ o = s;
+ }
+ else
+ {
+ s.type_index = CountType(s.type);
+ m_Streams.push_back(s);
+ }
+}
+
+void CSelectionStreams::Update(const std::shared_ptr<CDVDInputStream>& input,
+ CDVDDemux* demuxer,
+ const std::string& filename2)
+{
+ if(input && input->IsStreamType(DVDSTREAM_TYPE_DVD))
+ {
+ std::shared_ptr<CDVDInputStreamNavigator> nav = std::static_pointer_cast<CDVDInputStreamNavigator>(input);
+ std::string filename = nav->GetFileName();
+ int source = Source(STREAM_SOURCE_NAV, filename);
+
+ int count;
+ count = nav->GetAudioStreamCount();
+ for(int i=0;i<count;i++)
+ {
+ SelectionStream s;
+ s.source = source;
+ s.type = STREAM_AUDIO;
+ s.id = i;
+ s.flags = StreamFlags::FLAG_NONE;
+ s.filename = filename;
+
+ AudioStreamInfo info = nav->GetAudioStreamInfo(i);
+ s.name = info.name;
+ s.codec = info.codecName;
+ s.language = g_LangCodeExpander.ConvertToISO6392B(info.language);
+ s.channels = info.channels;
+ s.flags = info.flags;
+ Update(s);
+ }
+
+ count = nav->GetSubTitleStreamCount();
+ for(int i=0;i<count;i++)
+ {
+ SelectionStream s;
+ s.source = source;
+ s.type = STREAM_SUBTITLE;
+ s.id = i;
+ s.filename = filename;
+ s.channels = 0;
+
+ SubtitleStreamInfo info = nav->GetSubtitleStreamInfo(i);
+ s.name = info.name;
+ s.flags = info.flags;
+ s.language = g_LangCodeExpander.ConvertToISO6392B(info.language);
+ Update(s);
+ }
+
+ VideoStreamInfo info = nav->GetVideoStreamInfo();
+ for (int i = 1; i <= info.angles; i++)
+ {
+ SelectionStream s;
+ s.source = source;
+ s.type = STREAM_VIDEO;
+ s.id = i;
+ s.flags = StreamFlags::FLAG_NONE;
+ s.filename = filename;
+ s.channels = 0;
+ s.aspect_ratio = info.videoAspectRatio;
+ s.width = info.width;
+ s.height = info.height;
+ s.codec = info.codecName;
+ s.name = StringUtils::Format("{} {}", g_localizeStrings.Get(38032), i);
+ Update(s);
+ }
+ }
+ else if(demuxer)
+ {
+ std::string filename = demuxer->GetFileName();
+ int source;
+ if(input) /* hack to know this is sub decoder */
+ source = Source(STREAM_SOURCE_DEMUX, filename);
+ else if (!filename2.empty())
+ source = Source(STREAM_SOURCE_DEMUX_SUB, filename);
+ else
+ source = Source(STREAM_SOURCE_VIDEOMUX, filename);
+
+ for (auto stream : demuxer->GetStreams())
+ {
+ /* skip streams with no type */
+ if (stream->type == STREAM_NONE)
+ continue;
+ /* make sure stream is marked with right source */
+ stream->source = source;
+
+ SelectionStream s;
+ s.source = source;
+ s.type = stream->type;
+ s.id = stream->uniqueId;
+ s.demuxerId = stream->demuxerId;
+ s.language = g_LangCodeExpander.ConvertToISO6392B(stream->language);
+ s.flags = stream->flags;
+ s.filename = demuxer->GetFileName();
+ s.filename2 = filename2;
+ s.name = stream->GetStreamName();
+ s.codec = demuxer->GetStreamCodecName(stream->demuxerId, stream->uniqueId);
+ s.channels = 0; // Default to 0. Overwrite if STREAM_AUDIO below.
+ if(stream->type == STREAM_VIDEO)
+ {
+ CDemuxStreamVideo* vstream = static_cast<CDemuxStreamVideo*>(stream);
+ s.width = vstream->iWidth;
+ s.height = vstream->iHeight;
+ s.aspect_ratio = vstream->fAspect;
+ s.stereo_mode = vstream->stereo_mode;
+ s.bitrate = vstream->iBitRate;
+ s.hdrType = vstream->hdr_type;
+ }
+ if(stream->type == STREAM_AUDIO)
+ {
+ std::string type;
+ type = static_cast<CDemuxStreamAudio*>(stream)->GetStreamType();
+ if(type.length() > 0)
+ {
+ if(s.name.length() > 0)
+ s.name += " - ";
+ s.name += type;
+ }
+ s.channels = static_cast<CDemuxStreamAudio*>(stream)->iChannels;
+ s.bitrate = static_cast<CDemuxStreamAudio*>(stream)->iBitRate;
+ }
+ Update(s);
+ }
+ }
+ CServiceBroker::GetDataCacheCore().SignalAudioInfoChange();
+ CServiceBroker::GetDataCacheCore().SignalVideoInfoChange();
+ CServiceBroker::GetDataCacheCore().SignalSubtitleInfoChange();
+}
+
+void CSelectionStreams::Update(const std::shared_ptr<CDVDInputStream>& input, CDVDDemux* demuxer)
+{
+ Update(input, demuxer, "");
+}
+
+int CSelectionStreams::CountTypeOfSource(StreamType type, StreamSource source) const
+{
+ return std::count_if(m_Streams.begin(), m_Streams.end(),
+ [&](const SelectionStream& stream) {return (stream.type == type) && (stream.source == source);});
+}
+
+int CSelectionStreams::CountType(StreamType type) const
+{
+ return std::count_if(m_Streams.begin(), m_Streams.end(),
+ [&](const SelectionStream& stream) { return stream.type == type; });
+}
+
+//------------------------------------------------------------------------------
+// main class
+//------------------------------------------------------------------------------
+
+void CVideoPlayer::CreatePlayers()
+{
+ if (m_players_created)
+ return;
+
+ m_VideoPlayerVideo = new CVideoPlayerVideo(&m_clock, &m_overlayContainer, m_messenger, m_renderManager, *m_processInfo);
+ m_VideoPlayerAudio = new CVideoPlayerAudio(&m_clock, m_messenger, *m_processInfo);
+ m_VideoPlayerSubtitle = new CVideoPlayerSubtitle(&m_overlayContainer, *m_processInfo);
+ m_VideoPlayerTeletext = new CDVDTeletextData(*m_processInfo);
+ m_VideoPlayerRadioRDS = new CDVDRadioRDSData(*m_processInfo);
+ m_VideoPlayerAudioID3 = std::make_unique<CVideoPlayerAudioID3>(*m_processInfo);
+ m_players_created = true;
+}
+
+void CVideoPlayer::DestroyPlayers()
+{
+ if (!m_players_created)
+ return;
+
+ delete m_VideoPlayerVideo;
+ delete m_VideoPlayerAudio;
+ delete m_VideoPlayerSubtitle;
+ delete m_VideoPlayerTeletext;
+ delete m_VideoPlayerRadioRDS;
+ m_VideoPlayerAudioID3.reset();
+
+ m_players_created = false;
+}
+
+CVideoPlayer::CVideoPlayer(IPlayerCallback& callback)
+ : IPlayer(callback),
+ CThread("VideoPlayer"),
+ m_CurrentAudio(STREAM_AUDIO, VideoPlayer_AUDIO),
+ m_CurrentVideo(STREAM_VIDEO, VideoPlayer_VIDEO),
+ m_CurrentSubtitle(STREAM_SUBTITLE, VideoPlayer_SUBTITLE),
+ m_CurrentTeletext(STREAM_TELETEXT, VideoPlayer_TELETEXT),
+ m_CurrentRadioRDS(STREAM_RADIO_RDS, VideoPlayer_RDS),
+ m_CurrentAudioID3(STREAM_AUDIO_ID3, VideoPlayer_ID3),
+ m_messenger("player"),
+ m_renderManager(m_clock, this)
+{
+ m_outboundEvents.reset(new CJobQueue(false, 1, CJob::PRIORITY_NORMAL));
+ m_players_created = false;
+ m_pDemuxer = nullptr;
+ m_pSubtitleDemuxer = nullptr;
+ m_pCCDemuxer = nullptr;
+ m_pInputStream = nullptr;
+
+ m_dvd.Clear();
+ m_State.Clear();
+
+ m_bAbortRequest = false;
+ m_offset_pts = 0.0;
+ m_playSpeed = DVD_PLAYSPEED_NORMAL;
+ m_streamPlayerSpeed = DVD_PLAYSPEED_NORMAL;
+ m_caching = CACHESTATE_DONE;
+ m_HasVideo = false;
+ m_HasAudio = false;
+ m_UpdateStreamDetails = false;
+
+ memset(&m_SpeedState, 0, sizeof(m_SpeedState));
+
+ m_SkipCommercials = true;
+
+ m_processInfo.reset(CProcessInfo::CreateInstance());
+ // if we have a gui, register the cache
+ m_processInfo->SetDataCache(&CServiceBroker::GetDataCacheCore());
+ m_processInfo->SetSpeed(1.0);
+ m_processInfo->SetTempo(1.0);
+ m_processInfo->SetFrameAdvance(false);
+
+ CreatePlayers();
+
+ m_displayLost = false;
+ m_error = false;
+ m_bCloseRequest = false;
+ CServiceBroker::GetWinSystem()->Register(this);
+}
+
+CVideoPlayer::~CVideoPlayer()
+{
+ CServiceBroker::GetWinSystem()->Unregister(this);
+
+ CloseFile();
+ DestroyPlayers();
+
+ while (m_outboundEvents->IsProcessing())
+ {
+ CThread::Sleep(10ms);
+ }
+}
+
+bool CVideoPlayer::OpenFile(const CFileItem& file, const CPlayerOptions &options)
+{
+ CLog::Log(LOGINFO, "VideoPlayer::OpenFile: {}", CURL::GetRedacted(file.GetPath()));
+
+ if (IsRunning())
+ {
+ CDVDMsgOpenFile::FileParams params;
+ params.m_item = file;
+ params.m_options = options;
+ params.m_item.SetMimeTypeForInternetFile();
+ m_messenger.Put(std::make_shared<CDVDMsgOpenFile>(params), 1);
+
+ return true;
+ }
+
+ m_item = file;
+ m_playerOptions = options;
+
+ m_processInfo->SetPlayTimes(0,0,0,0);
+ m_bAbortRequest = false;
+ m_error = false;
+ m_bCloseRequest = false;
+ m_renderManager.PreInit();
+
+ Create();
+ m_messenger.Init();
+
+ m_callback.OnPlayBackStarted(m_item);
+
+ return true;
+}
+
+bool CVideoPlayer::CloseFile(bool reopen)
+{
+ CLog::Log(LOGINFO, "CVideoPlayer::CloseFile()");
+
+ // set the abort request so that other threads can finish up
+ m_bAbortRequest = true;
+ m_bCloseRequest = true;
+
+ // tell demuxer to abort
+ if(m_pDemuxer)
+ m_pDemuxer->Abort();
+
+ if(m_pSubtitleDemuxer)
+ m_pSubtitleDemuxer->Abort();
+
+ if(m_pInputStream)
+ m_pInputStream->Abort();
+
+ m_renderManager.UnInit();
+
+ CLog::Log(LOGINFO, "VideoPlayer: waiting for threads to exit");
+
+ // wait for the main thread to finish up
+ // since this main thread cleans up all other resources and threads
+ // we are done after the StopThread call
+ {
+ CSingleExit exitlock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ StopThread();
+ }
+
+ m_Edl.Clear();
+ CServiceBroker::GetDataCacheCore().SetEditList(m_Edl.GetEditList());
+ CServiceBroker::GetDataCacheCore().SetCuts(m_Edl.GetCutMarkers());
+ CServiceBroker::GetDataCacheCore().SetSceneMarkers(m_Edl.GetSceneMarkers());
+
+ m_HasVideo = false;
+ m_HasAudio = false;
+
+ CLog::Log(LOGINFO, "VideoPlayer: finished waiting");
+ return true;
+}
+
+bool CVideoPlayer::IsPlaying() const
+{
+ return !m_bStop;
+}
+
+void CVideoPlayer::OnStartup()
+{
+ m_CurrentVideo.Clear();
+ m_CurrentAudio.Clear();
+ m_CurrentSubtitle.Clear();
+ m_CurrentTeletext.Clear();
+ m_CurrentRadioRDS.Clear();
+ m_CurrentAudioID3.Clear();
+
+ UTILS::FONT::ClearTemporaryFonts();
+}
+
+bool CVideoPlayer::OpenInputStream()
+{
+ if (m_pInputStream.use_count() > 1)
+ throw std::runtime_error("m_pInputStream reference count is greater than 1");
+ m_pInputStream.reset();
+
+ CLog::Log(LOGINFO, "Creating InputStream");
+
+ // correct the filename if needed
+ const CURL url{m_item.GetPath()};
+ if (url.GetProtocol() == "dvd")
+ {
+ // FIXME: we should deprecate this when more than one device drive is supported
+ m_item.SetPath(CServiceBroker::GetMediaManager().TranslateDevicePath(""));
+ }
+ else if (url.GetProtocol() == "iso9660" && !url.GetHostName().empty() &&
+ url.GetFileName() == "VIDEO_TS/video_ts.ifo")
+ {
+ m_item.SetPath(url.GetHostName());
+ }
+
+ m_pInputStream = CDVDFactoryInputStream::CreateInputStream(this, m_item, true);
+ if (m_pInputStream == nullptr)
+ {
+ CLog::Log(LOGERROR, "CVideoPlayer::OpenInputStream - unable to create input stream for [{}]",
+ CURL::GetRedacted(m_item.GetPath()));
+ return false;
+ }
+
+ if (!m_pInputStream->Open())
+ {
+ CLog::Log(LOGERROR, "CVideoPlayer::OpenInputStream - error opening [{}]",
+ CURL::GetRedacted(m_item.GetPath()));
+ return false;
+ }
+
+ // find any available external subtitles for non dvd files
+ if (!m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD) &&
+ !m_pInputStream->IsStreamType(DVDSTREAM_TYPE_PVRMANAGER))
+ {
+ // find any available external subtitles
+ std::vector<std::string> filenames;
+ CUtil::ScanForExternalSubtitles(m_item.GetDynPath(), filenames);
+
+ // load any subtitles from file item
+ std::string key("subtitle:1");
+ for (unsigned s = 1; m_item.HasProperty(key); key = StringUtils::Format("subtitle:{}", ++s))
+ filenames.push_back(m_item.GetProperty(key).asString());
+
+ for (unsigned int i=0;i<filenames.size();i++)
+ {
+ // if vobsub subtitle:
+ if (URIUtils::HasExtension(filenames[i], ".idx"))
+ {
+ std::string strSubFile;
+ if (CUtil::FindVobSubPair( filenames, filenames[i], strSubFile))
+ AddSubtitleFile(filenames[i], strSubFile);
+ }
+ else
+ {
+ if (!CUtil::IsVobSub(filenames, filenames[i] ))
+ {
+ AddSubtitleFile(filenames[i]);
+ }
+ }
+ } // end loop over all subtitle files
+ }
+
+ m_clock.Reset();
+ m_dvd.Clear();
+
+ return true;
+}
+
+bool CVideoPlayer::OpenDemuxStream()
+{
+ CloseDemuxer();
+
+ CLog::Log(LOGINFO, "Creating Demuxer");
+
+ int attempts = 10;
+ while (!m_bStop && attempts-- > 0)
+ {
+ m_pDemuxer.reset(CDVDFactoryDemuxer::CreateDemuxer(m_pInputStream));
+ if(!m_pDemuxer && m_pInputStream->IsStreamType(DVDSTREAM_TYPE_PVRMANAGER))
+ {
+ continue;
+ }
+ else if(!m_pDemuxer && m_pInputStream->NextStream() != CDVDInputStream::NEXTSTREAM_NONE)
+ {
+ CLog::Log(LOGDEBUG, "{} - New stream available from input, retry open", __FUNCTION__);
+ continue;
+ }
+ break;
+ }
+
+ if (!m_pDemuxer)
+ {
+ CLog::Log(LOGERROR, "{} - Error creating demuxer", __FUNCTION__);
+ return false;
+ }
+
+ m_SelectionStreams.Clear(STREAM_NONE, STREAM_SOURCE_DEMUX);
+ m_SelectionStreams.Clear(STREAM_NONE, STREAM_SOURCE_NAV);
+ m_SelectionStreams.Update(m_pInputStream, m_pDemuxer.get());
+ m_pDemuxer->GetPrograms(m_programs);
+ UpdateContent();
+ m_demuxerSpeed = DVD_PLAYSPEED_NORMAL;
+ m_processInfo->SetStateRealtime(false);
+
+ int64_t len = m_pInputStream->GetLength();
+ int64_t tim = m_pDemuxer->GetStreamLength();
+ if (len > 0 && tim > 0)
+ m_pInputStream->SetReadRate(static_cast<uint32_t>(len * 1000 / tim));
+
+ m_offset_pts = 0;
+
+ return true;
+}
+
+void CVideoPlayer::CloseDemuxer()
+{
+ m_pDemuxer.reset();
+ m_SelectionStreams.Clear(STREAM_NONE, STREAM_SOURCE_DEMUX);
+
+ CServiceBroker::GetDataCacheCore().SignalAudioInfoChange();
+ CServiceBroker::GetDataCacheCore().SignalVideoInfoChange();
+ CServiceBroker::GetDataCacheCore().SignalSubtitleInfoChange();
+}
+
+void CVideoPlayer::OpenDefaultStreams(bool reset)
+{
+ // if input stream dictate, we will open later
+ if (m_dvd.iSelectedAudioStream >= 0 ||
+ m_dvd.iSelectedSPUStream >= 0)
+ return;
+
+ bool valid;
+
+ // open video stream
+ valid = false;
+
+ PredicateVideoFilter vf(m_processInfo->GetVideoSettings().m_VideoStream);
+ for (const auto &stream : m_SelectionStreams.Get(STREAM_VIDEO, vf))
+ {
+ if (OpenStream(m_CurrentVideo, stream.demuxerId, stream.id, stream.source, reset))
+ {
+ valid = true;
+ break;
+ }
+ }
+ if (!valid)
+ {
+ CloseStream(m_CurrentVideo, true);
+ m_processInfo->ResetVideoCodecInfo();
+ }
+
+ // open audio stream
+ valid = false;
+ if (!m_playerOptions.videoOnly)
+ {
+ PredicateAudioFilter af(m_processInfo->GetVideoSettings().m_AudioStream, m_playerOptions.preferStereo);
+ for (const auto &stream : m_SelectionStreams.Get(STREAM_AUDIO, af))
+ {
+ if(OpenStream(m_CurrentAudio, stream.demuxerId, stream.id, stream.source, reset))
+ {
+ valid = true;
+ break;
+ }
+ }
+ }
+
+ if(!valid)
+ {
+ CloseStream(m_CurrentAudio, true);
+ m_processInfo->ResetAudioCodecInfo();
+ }
+
+ // enable or disable subtitles
+ bool visible = m_processInfo->GetVideoSettings().m_SubtitleOn;
+
+ // open subtitle stream
+ SelectionStream as = m_SelectionStreams.Get(STREAM_AUDIO, GetAudioStream());
+ PredicateSubtitlePriority psp(as.language,
+ m_processInfo->GetVideoSettings().m_SubtitleStream,
+ m_processInfo->GetVideoSettings().m_SubtitleOn);
+ valid = false;
+ // We need to close CC subtitles to avoid conflicts with external sub stream
+ if (m_CurrentSubtitle.source == STREAM_SOURCE_VIDEOMUX)
+ CloseStream(m_CurrentSubtitle, false);
+
+ for (const auto &stream : m_SelectionStreams.Get(STREAM_SUBTITLE, psp))
+ {
+ if (OpenStream(m_CurrentSubtitle, stream.demuxerId, stream.id, stream.source))
+ {
+ valid = true;
+ if(!psp.relevant(stream))
+ visible = false;
+ else if(stream.flags & StreamFlags::FLAG_FORCED)
+ visible = true;
+ break;
+ }
+ }
+ if(!valid)
+ CloseStream(m_CurrentSubtitle, false);
+
+ if (!std::dynamic_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream) || m_playerOptions.state.empty())
+ SetSubtitleVisibleInternal(visible); // only set subtitle visibility if state not stored by dvd navigator, because navigator will restore it (if visible)
+
+ // open teletext stream
+ valid = false;
+ for (const auto &stream : m_SelectionStreams.Get(STREAM_TELETEXT))
+ {
+ if (OpenStream(m_CurrentTeletext, stream.demuxerId, stream.id, stream.source))
+ {
+ valid = true;
+ break;
+ }
+ }
+ if(!valid)
+ CloseStream(m_CurrentTeletext, false);
+
+ // open RDS stream
+ valid = false;
+ for (const auto &stream : m_SelectionStreams.Get(STREAM_RADIO_RDS))
+ {
+ if (OpenStream(m_CurrentRadioRDS, stream.demuxerId, stream.id, stream.source))
+ {
+ valid = true;
+ break;
+ }
+ }
+ if(!valid)
+ CloseStream(m_CurrentRadioRDS, false);
+
+ // open ID3 stream
+ valid = false;
+ for (const auto& stream : m_SelectionStreams.Get(STREAM_AUDIO_ID3))
+ {
+ if (OpenStream(m_CurrentAudioID3, stream.demuxerId, stream.id, stream.source))
+ {
+ valid = true;
+ break;
+ }
+ }
+ if (!valid)
+ CloseStream(m_CurrentAudioID3, false);
+
+ // disable demux streams
+ if (m_item.IsRemote() && m_pDemuxer)
+ {
+ for (auto &stream : m_SelectionStreams.m_Streams)
+ {
+ if (STREAM_SOURCE_MASK(stream.source) == STREAM_SOURCE_DEMUX)
+ {
+ if (stream.id != m_CurrentVideo.id && stream.id != m_CurrentAudio.id &&
+ stream.id != m_CurrentSubtitle.id && stream.id != m_CurrentTeletext.id &&
+ stream.id != m_CurrentRadioRDS.id && stream.id != m_CurrentAudioID3.id)
+ {
+ m_pDemuxer->EnableStream(stream.demuxerId, stream.id, false);
+ }
+ }
+ }
+ }
+}
+
+bool CVideoPlayer::ReadPacket(DemuxPacket*& packet, CDemuxStream*& stream)
+{
+
+ // check if we should read from subtitle demuxer
+ if (m_pSubtitleDemuxer && m_VideoPlayerSubtitle->AcceptsData())
+ {
+ packet = m_pSubtitleDemuxer->Read();
+
+ if(packet)
+ {
+ UpdateCorrection(packet, m_offset_pts);
+ if(packet->iStreamId < 0)
+ return true;
+
+ stream = m_pSubtitleDemuxer->GetStream(packet->demuxerId, packet->iStreamId);
+ if (!stream)
+ {
+ CLog::Log(LOGERROR, "{} - Error demux packet doesn't belong to a valid stream",
+ __FUNCTION__);
+ return false;
+ }
+ if (stream->source == STREAM_SOURCE_NONE)
+ {
+ m_SelectionStreams.Clear(STREAM_NONE, STREAM_SOURCE_DEMUX_SUB);
+ m_SelectionStreams.Update(NULL, m_pSubtitleDemuxer.get());
+ UpdateContent();
+ }
+ return true;
+ }
+ }
+
+ // read a data frame from stream.
+ if (m_pDemuxer)
+ packet = m_pDemuxer->Read();
+
+ if (packet)
+ {
+ // stream changed, update and open defaults
+ if (packet->iStreamId == DMX_SPECIALID_STREAMCHANGE)
+ {
+ m_SelectionStreams.Clear(STREAM_NONE, STREAM_SOURCE_DEMUX);
+ m_SelectionStreams.Update(m_pInputStream, m_pDemuxer.get());
+ m_pDemuxer->GetPrograms(m_programs);
+ UpdateContent();
+ OpenDefaultStreams(false);
+
+ // reevaluate HasVideo/Audio, we may have switched from/to a radio channel
+ if(m_CurrentVideo.id < 0)
+ m_HasVideo = false;
+ if(m_CurrentAudio.id < 0)
+ m_HasAudio = false;
+
+ return true;
+ }
+
+ UpdateCorrection(packet, m_offset_pts);
+
+ if(packet->iStreamId < 0)
+ return true;
+
+ if(m_pDemuxer)
+ {
+ stream = m_pDemuxer->GetStream(packet->demuxerId, packet->iStreamId);
+ if (!stream)
+ {
+ CLog::Log(LOGERROR, "{} - Error demux packet doesn't belong to a valid stream",
+ __FUNCTION__);
+ return false;
+ }
+ if(stream->source == STREAM_SOURCE_NONE)
+ {
+ m_SelectionStreams.Clear(STREAM_NONE, STREAM_SOURCE_DEMUX);
+ m_SelectionStreams.Update(m_pInputStream, m_pDemuxer.get());
+ UpdateContent();
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+bool CVideoPlayer::IsValidStream(const CCurrentStream& stream)
+{
+ if(stream.id<0)
+ return true; // we consider non selected as valid
+
+ int source = STREAM_SOURCE_MASK(stream.source);
+ if(source == STREAM_SOURCE_TEXT)
+ return true;
+ if (source == STREAM_SOURCE_DEMUX_SUB)
+ {
+ CDemuxStream* st = m_pSubtitleDemuxer->GetStream(stream.demuxerId, stream.id);
+ if(st == NULL || st->disabled)
+ return false;
+ if(st->type != stream.type)
+ return false;
+ return true;
+ }
+ if (source == STREAM_SOURCE_DEMUX)
+ {
+ CDemuxStream* st = m_pDemuxer->GetStream(stream.demuxerId, stream.id);
+ if(st == NULL || st->disabled)
+ return false;
+ if(st->type != stream.type)
+ return false;
+
+ if (m_pInputStream && m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ {
+ if (stream.type == STREAM_AUDIO && st->dvdNavId != m_dvd.iSelectedAudioStream)
+ return false;
+ if(stream.type == STREAM_SUBTITLE && st->dvdNavId != m_dvd.iSelectedSPUStream)
+ return false;
+ }
+
+ return true;
+ }
+ if (source == STREAM_SOURCE_VIDEOMUX)
+ {
+ CDemuxStream* st = m_pCCDemuxer->GetStream(stream.id);
+ if (st == NULL || st->disabled)
+ return false;
+ if (st->type != stream.type)
+ return false;
+ return true;
+ }
+
+ return false;
+}
+
+bool CVideoPlayer::IsBetterStream(const CCurrentStream& current, CDemuxStream* stream)
+{
+ // Do not reopen non-video streams if we're in video-only mode
+ if (m_playerOptions.videoOnly && current.type != STREAM_VIDEO)
+ return false;
+
+ if(stream->disabled)
+ return false;
+
+ if (m_pInputStream && (m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD) ||
+ m_pInputStream->IsStreamType(DVDSTREAM_TYPE_BLURAY)))
+ {
+ int source_type;
+
+ source_type = STREAM_SOURCE_MASK(current.source);
+ if (source_type != STREAM_SOURCE_DEMUX &&
+ source_type != STREAM_SOURCE_NONE)
+ return false;
+
+ source_type = STREAM_SOURCE_MASK(stream->source);
+ if(source_type != STREAM_SOURCE_DEMUX ||
+ stream->type != current.type ||
+ stream->uniqueId == current.id)
+ return false;
+
+ if(current.type == STREAM_AUDIO && stream->dvdNavId == m_dvd.iSelectedAudioStream)
+ return true;
+ if(current.type == STREAM_SUBTITLE && stream->dvdNavId == m_dvd.iSelectedSPUStream)
+ return true;
+ if(current.type == STREAM_VIDEO && current.id < 0)
+ return true;
+ }
+ else
+ {
+ if(stream->source == current.source &&
+ stream->uniqueId == current.id &&
+ stream->demuxerId == current.demuxerId)
+ return false;
+
+ if(stream->type != current.type)
+ return false;
+
+ if(current.type == STREAM_SUBTITLE)
+ return false;
+
+ if(current.id < 0)
+ return true;
+ }
+ return false;
+}
+
+void CVideoPlayer::CheckBetterStream(CCurrentStream& current, CDemuxStream* stream)
+{
+ IDVDStreamPlayer* player = GetStreamPlayer(current.player);
+ if (!IsValidStream(current) && (player == NULL || player->IsStalled()))
+ CloseStream(current, true);
+
+ if (IsBetterStream(current, stream))
+ OpenStream(current, stream->demuxerId, stream->uniqueId, stream->source);
+}
+
+void CVideoPlayer::Prepare()
+{
+ CFFmpegLog::SetLogLevel(1);
+ SetPlaySpeed(DVD_PLAYSPEED_NORMAL);
+ m_processInfo->SetSpeed(1.0);
+ m_processInfo->SetTempo(1.0);
+ m_processInfo->SetFrameAdvance(false);
+ m_State.Clear();
+ m_CurrentVideo.hint.Clear();
+ m_CurrentAudio.hint.Clear();
+ m_CurrentSubtitle.hint.Clear();
+ m_CurrentTeletext.hint.Clear();
+ m_CurrentRadioRDS.hint.Clear();
+ m_CurrentAudioID3.hint.Clear();
+ memset(&m_SpeedState, 0, sizeof(m_SpeedState));
+ m_offset_pts = 0;
+ m_CurrentAudio.lastdts = DVD_NOPTS_VALUE;
+ m_CurrentVideo.lastdts = DVD_NOPTS_VALUE;
+
+ IPlayerCallback *cb = &m_callback;
+ CFileItem fileItem = m_item;
+ m_outboundEvents->Submit([=]() {
+ cb->RequestVideoSettings(fileItem);
+ });
+
+ if (!OpenInputStream())
+ {
+ m_bAbortRequest = true;
+ m_error = true;
+ return;
+ }
+
+ bool discStateRestored = false;
+ if (std::shared_ptr<CDVDInputStream::IMenus> ptr = std::dynamic_pointer_cast<CDVDInputStream::IMenus>(m_pInputStream))
+ {
+ CLog::Log(LOGINFO, "VideoPlayer: playing a file with menu's");
+
+ if (!m_playerOptions.state.empty())
+ {
+ discStateRestored = ptr->SetState(m_playerOptions.state);
+ }
+ else if(std::shared_ptr<CDVDInputStreamNavigator> nav = std::dynamic_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream))
+ {
+ nav->EnableSubtitleStream(m_processInfo->GetVideoSettings().m_SubtitleOn);
+ }
+ }
+
+ if (!OpenDemuxStream())
+ {
+ m_bAbortRequest = true;
+ m_error = true;
+ return;
+ }
+ // give players a chance to reconsider now codecs are known
+ CreatePlayers();
+
+ if (!discStateRestored)
+ OpenDefaultStreams();
+
+ /*
+ * Check to see if the demuxer should start at something other than time 0. This will be the case
+ * if there was a start time specified as part of the "Start from where last stopped" (aka
+ * auto-resume) feature or if there is an EDL cut or commercial break that starts at time 0.
+ */
+ EDL::Edit edit;
+ int starttime = 0;
+ if (m_playerOptions.starttime > 0 || m_playerOptions.startpercent > 0)
+ {
+ if (m_playerOptions.startpercent > 0 && m_pDemuxer)
+ {
+ int playerStartTime = static_cast<int>((static_cast<double>(
+ m_pDemuxer->GetStreamLength() * (m_playerOptions.startpercent / 100.0))));
+ starttime = m_Edl.GetTimeAfterRestoringCuts(playerStartTime);
+ }
+ else
+ {
+ starttime = m_Edl.GetTimeAfterRestoringCuts(
+ static_cast<int>(m_playerOptions.starttime * 1000)); // s to ms
+ }
+ CLog::Log(LOGDEBUG, "{} - Start position set to last stopped position: {}", __FUNCTION__,
+ starttime);
+ }
+ else if (m_Edl.InEdit(starttime, &edit))
+ {
+ // save last edit times
+ m_Edl.SetLastEditTime(edit.start);
+ m_Edl.SetLastEditActionType(edit.action);
+
+ if (edit.action == EDL::Action::CUT)
+ {
+ starttime = edit.end;
+ CLog::Log(LOGDEBUG, "{} - Start position set to end of first cut: {}", __FUNCTION__,
+ starttime);
+ }
+ else if (edit.action == EDL::Action::COMM_BREAK)
+ {
+ if (m_SkipCommercials)
+ {
+ starttime = edit.end;
+ CLog::Log(LOGDEBUG, "{} - Start position set to end of first commercial break: {}",
+ __FUNCTION__, starttime);
+ }
+
+ const std::shared_ptr<CAdvancedSettings> advancedSettings =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+ if (advancedSettings && advancedSettings->m_EdlDisplayCommbreakNotifications)
+ {
+ const std::string timeString =
+ StringUtils::SecondsToTimeString(edit.end / 1000, TIME_FORMAT_MM_SS);
+ CGUIDialogKaiToast::QueueNotification(g_localizeStrings.Get(25011), timeString);
+ }
+ }
+ }
+
+ if (starttime > 0)
+ {
+ double startpts = DVD_NOPTS_VALUE;
+ if (m_pDemuxer)
+ {
+ if (m_pDemuxer->SeekTime(starttime, true, &startpts))
+ {
+ FlushBuffers(starttime / 1000 * AV_TIME_BASE, true, true);
+ CLog::Log(LOGDEBUG, "{} - starting demuxer from: {}", __FUNCTION__, starttime);
+ }
+ else
+ CLog::Log(LOGDEBUG, "{} - failed to start demuxing from: {}", __FUNCTION__, starttime);
+ }
+
+ if (m_pSubtitleDemuxer)
+ {
+ if(m_pSubtitleDemuxer->SeekTime(starttime, true, &startpts))
+ CLog::Log(LOGDEBUG, "{} - starting subtitle demuxer from: {}", __FUNCTION__, starttime);
+ else
+ CLog::Log(LOGDEBUG, "{} - failed to start subtitle demuxing from: {}", __FUNCTION__,
+ starttime);
+ }
+
+ m_clock.Discontinuity(DVD_MSEC_TO_TIME(starttime));
+ }
+
+ UpdatePlayState(0);
+
+ SetCaching(CACHESTATE_FLUSH);
+}
+
+void CVideoPlayer::Process()
+{
+ // Try to resolve the correct mime type. This can take some time, for example if a requested
+ // item is located at a slow/not reachable remote source. So, do mime type detection in vp worker
+ // thread, not directly when initalizing the player to keep GUI responsible.
+ m_item.SetMimeTypeForInternetFile();
+
+ CServiceBroker::GetWinSystem()->RegisterRenderLoop(this);
+
+ Prepare();
+
+ while (!m_bAbortRequest)
+ {
+ // check display lost
+ if (m_displayLost)
+ {
+ CThread::Sleep(50ms);
+ continue;
+ }
+
+ // check if in an edit (cut or commercial break) that should be automatically skipped
+ CheckAutoSceneSkip();
+
+ // handle messages send to this thread, like seek or demuxer reset requests
+ HandleMessages();
+
+ if (m_bAbortRequest)
+ break;
+
+ // should we open a new input stream?
+ if (!m_pInputStream)
+ {
+ if (OpenInputStream() == false)
+ {
+ m_bAbortRequest = true;
+ break;
+ }
+ }
+
+ // should we open a new demuxer?
+ if (!m_pDemuxer)
+ {
+ if (m_pInputStream->NextStream() == CDVDInputStream::NEXTSTREAM_NONE)
+ break;
+
+ if (m_pInputStream->IsEOF())
+ break;
+
+ if (OpenDemuxStream() == false)
+ {
+ m_bAbortRequest = true;
+ break;
+ }
+
+ // on channel switch we don't want to close stream players at this
+ // time. we'll get the stream change event later
+ if (!m_pInputStream->IsStreamType(DVDSTREAM_TYPE_PVRMANAGER) ||
+ !m_SelectionStreams.m_Streams.empty())
+ OpenDefaultStreams();
+
+ UpdatePlayState(0);
+ }
+
+ // handle eventual seeks due to playspeed
+ HandlePlaySpeed();
+
+ // update player state
+ UpdatePlayState(200);
+
+ // make sure we run subtitle process here
+ m_VideoPlayerSubtitle->Process(m_clock.GetClock() + m_State.time_offset - m_VideoPlayerVideo->GetSubtitleDelay(), m_State.time_offset);
+
+ // tell demuxer if we want to fill buffers
+ if (m_demuxerSpeed != DVD_PLAYSPEED_PAUSE)
+ {
+ int audioLevel = 90;
+ int videoLevel = 90;
+ bool fillBuffer = false;
+ if (m_CurrentAudio.id >= 0)
+ audioLevel = m_VideoPlayerAudio->GetLevel();
+ if (m_CurrentVideo.id >= 0)
+ videoLevel = m_processInfo->GetLevelVQ();
+ if (videoLevel < 85 && audioLevel < 85)
+ {
+ fillBuffer = true;
+ }
+ if (m_pDemuxer)
+ m_pDemuxer->FillBuffer(fillBuffer);
+ }
+
+ // if the queues are full, no need to read more
+ if ((!m_VideoPlayerAudio->AcceptsData() && m_CurrentAudio.id >= 0) ||
+ (!m_VideoPlayerVideo->AcceptsData() && m_CurrentVideo.id >= 0))
+ {
+ if (m_playSpeed == DVD_PLAYSPEED_PAUSE &&
+ m_demuxerSpeed != DVD_PLAYSPEED_PAUSE)
+ {
+ if (m_pDemuxer)
+ m_pDemuxer->SetSpeed(DVD_PLAYSPEED_PAUSE);
+ m_demuxerSpeed = DVD_PLAYSPEED_PAUSE;
+ }
+ CThread::Sleep(10ms);
+ continue;
+ }
+
+ // adjust demuxer speed; some rtsp servers wants to know for i.e. ff
+ // delay pause until queue is full
+ if (m_playSpeed != DVD_PLAYSPEED_PAUSE &&
+ m_demuxerSpeed != m_playSpeed)
+ {
+ if (m_pDemuxer)
+ m_pDemuxer->SetSpeed(m_playSpeed);
+ m_demuxerSpeed = m_playSpeed;
+ }
+
+ DemuxPacket* pPacket = NULL;
+ CDemuxStream *pStream = NULL;
+ ReadPacket(pPacket, pStream);
+ if (pPacket && !pStream)
+ {
+ /* probably a empty packet, just free it and move on */
+ CDVDDemuxUtils::FreeDemuxPacket(pPacket);
+ continue;
+ }
+
+ if (!pPacket)
+ {
+ // when paused, demuxer could be be returning empty
+ if (m_playSpeed == DVD_PLAYSPEED_PAUSE)
+ continue;
+
+ // check for a still frame state
+ if (std::shared_ptr<CDVDInputStream::IMenus> pStream = std::dynamic_pointer_cast<CDVDInputStream::IMenus>(m_pInputStream))
+ {
+ // stills will be skipped
+ if(m_dvd.state == DVDSTATE_STILL)
+ {
+ if (m_dvd.iDVDStillTime > 0ms)
+ {
+ const auto now = std::chrono::steady_clock::now();
+ const auto duration = now - m_dvd.iDVDStillStartTime;
+
+ if (duration >= m_dvd.iDVDStillTime)
+ {
+ m_dvd.iDVDStillTime = 0ms;
+ m_dvd.iDVDStillStartTime = {};
+ m_dvd.state = DVDSTATE_NORMAL;
+ pStream->SkipStill();
+ continue;
+ }
+ }
+ }
+ }
+
+ // if there is another stream available, reopen demuxer
+ CDVDInputStream::ENextStream next = m_pInputStream->NextStream();
+ if(next == CDVDInputStream::NEXTSTREAM_OPEN)
+ {
+ CloseDemuxer();
+
+ SetCaching(CACHESTATE_DONE);
+ CLog::Log(LOGINFO, "VideoPlayer: next stream, wait for old streams to be finished");
+ CloseStream(m_CurrentAudio, true);
+ CloseStream(m_CurrentVideo, true);
+
+ m_CurrentAudio.Clear();
+ m_CurrentVideo.Clear();
+ m_CurrentSubtitle.Clear();
+ continue;
+ }
+
+ // input stream asked us to just retry
+ if(next == CDVDInputStream::NEXTSTREAM_RETRY)
+ {
+ CThread::Sleep(100ms);
+ continue;
+ }
+
+ if (m_CurrentVideo.inited)
+ {
+ m_VideoPlayerVideo->SendMessage(std::make_shared<CDVDMsg>(CDVDMsg::VIDEO_DRAIN));
+ }
+
+ m_CurrentAudio.inited = false;
+ m_CurrentVideo.inited = false;
+ m_CurrentSubtitle.inited = false;
+ m_CurrentTeletext.inited = false;
+ m_CurrentRadioRDS.inited = false;
+ m_CurrentAudioID3.inited = false;
+
+ // if we are caching, start playing it again
+ SetCaching(CACHESTATE_DONE);
+
+ // while players are still playing, keep going to allow seekbacks
+ if (m_VideoPlayerAudio->HasData() ||
+ m_VideoPlayerVideo->HasData())
+ {
+ CThread::Sleep(100ms);
+ continue;
+ }
+
+ if (!m_pInputStream->IsEOF())
+ CLog::Log(LOGINFO, "{} - eof reading from demuxer", __FUNCTION__);
+
+ break;
+ }
+
+ // see if we can find something better to play
+ CheckBetterStream(m_CurrentAudio, pStream);
+ CheckBetterStream(m_CurrentVideo, pStream);
+ CheckBetterStream(m_CurrentSubtitle, pStream);
+ CheckBetterStream(m_CurrentTeletext, pStream);
+ CheckBetterStream(m_CurrentRadioRDS, pStream);
+ CheckBetterStream(m_CurrentAudioID3, pStream);
+
+ // demux video stream
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SUBTITLES_PARSECAPTIONS) && CheckIsCurrent(m_CurrentVideo, pStream, pPacket))
+ {
+ if (m_pCCDemuxer)
+ {
+ bool first = true;
+ while (!m_bAbortRequest)
+ {
+ DemuxPacket *pkt = m_pCCDemuxer->Read(first ? pPacket : NULL);
+ if (!pkt)
+ break;
+
+ first = false;
+ if (m_pCCDemuxer->GetNrOfStreams() != m_SelectionStreams.CountTypeOfSource(STREAM_SUBTITLE, STREAM_SOURCE_VIDEOMUX))
+ {
+ m_SelectionStreams.Clear(STREAM_SUBTITLE, STREAM_SOURCE_VIDEOMUX);
+ m_SelectionStreams.Update(NULL, m_pCCDemuxer.get(), "");
+ UpdateContent();
+ OpenDefaultStreams(false);
+ }
+ CDemuxStream *pSubStream = m_pCCDemuxer->GetStream(pkt->iStreamId);
+ if (pSubStream && m_CurrentSubtitle.id == pkt->iStreamId && m_CurrentSubtitle.source == STREAM_SOURCE_VIDEOMUX)
+ ProcessSubData(pSubStream, pkt);
+ else
+ CDVDDemuxUtils::FreeDemuxPacket(pkt);
+ }
+ }
+ }
+
+ if (IsInMenuInternal())
+ {
+ if (std::shared_ptr<CDVDInputStream::IMenus> menu = std::dynamic_pointer_cast<CDVDInputStream::IMenus>(m_pInputStream))
+ {
+ double correction = menu->GetTimeStampCorrection();
+ if (pPacket->dts != DVD_NOPTS_VALUE && pPacket->dts > correction)
+ pPacket->dts -= correction;
+ if (pPacket->pts != DVD_NOPTS_VALUE && pPacket->pts > correction)
+ pPacket->pts -= correction;
+ }
+ if (m_dvd.syncClock)
+ {
+ m_clock.Discontinuity(pPacket->dts);
+ m_dvd.syncClock = false;
+ }
+ }
+
+ // process the packet
+ ProcessPacket(pStream, pPacket);
+ }
+}
+
+bool CVideoPlayer::CheckIsCurrent(const CCurrentStream& current,
+ CDemuxStream* stream,
+ DemuxPacket* pkg)
+{
+ if(current.id == pkg->iStreamId &&
+ current.demuxerId == stream->demuxerId &&
+ current.source == stream->source &&
+ current.type == stream->type)
+ return true;
+ else
+ return false;
+}
+
+void CVideoPlayer::ProcessPacket(CDemuxStream* pStream, DemuxPacket* pPacket)
+{
+ // process packet if it belongs to selected stream.
+ // for dvd's don't allow automatic opening of streams*/
+
+ if (CheckIsCurrent(m_CurrentAudio, pStream, pPacket))
+ ProcessAudioData(pStream, pPacket);
+ else if (CheckIsCurrent(m_CurrentVideo, pStream, pPacket))
+ ProcessVideoData(pStream, pPacket);
+ else if (CheckIsCurrent(m_CurrentSubtitle, pStream, pPacket))
+ ProcessSubData(pStream, pPacket);
+ else if (CheckIsCurrent(m_CurrentTeletext, pStream, pPacket))
+ ProcessTeletextData(pStream, pPacket);
+ else if (CheckIsCurrent(m_CurrentRadioRDS, pStream, pPacket))
+ ProcessRadioRDSData(pStream, pPacket);
+ else if (CheckIsCurrent(m_CurrentAudioID3, pStream, pPacket))
+ ProcessAudioID3Data(pStream, pPacket);
+ else
+ {
+ CDVDDemuxUtils::FreeDemuxPacket(pPacket); // free it since we won't do anything with it
+ }
+}
+
+void CVideoPlayer::CheckStreamChanges(CCurrentStream& current, CDemuxStream* stream)
+{
+ if (current.stream != (void*)stream
+ || current.changes != stream->changes)
+ {
+ /* check so that dmuxer hints or extra data hasn't changed */
+ /* if they have, reopen stream */
+
+ if (current.hint != CDVDStreamInfo(*stream, true))
+ {
+ m_SelectionStreams.Clear(STREAM_NONE, STREAM_SOURCE_DEMUX);
+ m_SelectionStreams.Update(m_pInputStream, m_pDemuxer.get());
+ UpdateContent();
+ OpenDefaultStreams(false);
+ }
+
+ current.stream = (void*)stream;
+ current.changes = stream->changes;
+ }
+}
+
+void CVideoPlayer::ProcessAudioData(CDemuxStream* pStream, DemuxPacket* pPacket)
+{
+ CheckStreamChanges(m_CurrentAudio, pStream);
+
+ bool checkcont = CheckContinuity(m_CurrentAudio, pPacket);
+ UpdateTimestamps(m_CurrentAudio, pPacket);
+
+ if (checkcont && (m_CurrentAudio.avsync == CCurrentStream::AV_SYNC_CHECK))
+ m_CurrentAudio.avsync = CCurrentStream::AV_SYNC_NONE;
+
+ bool drop = false;
+ if (CheckPlayerInit(m_CurrentAudio))
+ drop = true;
+
+ /*
+ * If CheckSceneSkip() returns true then demux point is inside an EDL cut and the packets are dropped.
+ */
+ EDL::Edit edit;
+ if (CheckSceneSkip(m_CurrentAudio))
+ drop = true;
+ else if (m_Edl.InEdit(DVD_TIME_TO_MSEC(m_CurrentAudio.dts + m_offset_pts), &edit) &&
+ edit.action == EDL::Action::MUTE)
+ {
+ drop = true;
+ }
+
+ m_VideoPlayerAudio->SendMessage(std::make_shared<CDVDMsgDemuxerPacket>(pPacket, drop));
+
+ if (!drop)
+ m_CurrentAudio.packets++;
+}
+
+void CVideoPlayer::ProcessVideoData(CDemuxStream* pStream, DemuxPacket* pPacket)
+{
+ CheckStreamChanges(m_CurrentVideo, pStream);
+ bool checkcont = false;
+
+ if( pPacket->iSize != 4) //don't check the EOF_SEQUENCE of stillframes
+ {
+ checkcont = CheckContinuity(m_CurrentVideo, pPacket);
+ UpdateTimestamps(m_CurrentVideo, pPacket);
+ }
+ if (checkcont && (m_CurrentVideo.avsync == CCurrentStream::AV_SYNC_CHECK))
+ m_CurrentVideo.avsync = CCurrentStream::AV_SYNC_NONE;
+
+ bool drop = false;
+ if (CheckPlayerInit(m_CurrentVideo))
+ drop = true;
+
+ if (CheckSceneSkip(m_CurrentVideo))
+ drop = true;
+
+ m_VideoPlayerVideo->SendMessage(std::make_shared<CDVDMsgDemuxerPacket>(pPacket, drop));
+
+ if (!drop)
+ m_CurrentVideo.packets++;
+}
+
+void CVideoPlayer::ProcessSubData(CDemuxStream* pStream, DemuxPacket* pPacket)
+{
+ CheckStreamChanges(m_CurrentSubtitle, pStream);
+
+ UpdateTimestamps(m_CurrentSubtitle, pPacket);
+
+ bool drop = false;
+ if (CheckPlayerInit(m_CurrentSubtitle))
+ drop = true;
+
+ if (CheckSceneSkip(m_CurrentSubtitle))
+ drop = true;
+
+ m_VideoPlayerSubtitle->SendMessage(std::make_shared<CDVDMsgDemuxerPacket>(pPacket, drop));
+
+ if(m_pInputStream && m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ m_VideoPlayerSubtitle->UpdateOverlayInfo(std::static_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream), LIBDVDNAV_BUTTON_NORMAL);
+}
+
+void CVideoPlayer::ProcessTeletextData(CDemuxStream* pStream, DemuxPacket* pPacket)
+{
+ CheckStreamChanges(m_CurrentTeletext, pStream);
+
+ UpdateTimestamps(m_CurrentTeletext, pPacket);
+
+ bool drop = false;
+ if (CheckPlayerInit(m_CurrentTeletext))
+ drop = true;
+
+ if (CheckSceneSkip(m_CurrentTeletext))
+ drop = true;
+
+ m_VideoPlayerTeletext->SendMessage(std::make_shared<CDVDMsgDemuxerPacket>(pPacket, drop));
+}
+
+void CVideoPlayer::ProcessRadioRDSData(CDemuxStream* pStream, DemuxPacket* pPacket)
+{
+ CheckStreamChanges(m_CurrentRadioRDS, pStream);
+
+ UpdateTimestamps(m_CurrentRadioRDS, pPacket);
+
+ bool drop = false;
+ if (CheckPlayerInit(m_CurrentRadioRDS))
+ drop = true;
+
+ if (CheckSceneSkip(m_CurrentRadioRDS))
+ drop = true;
+
+ m_VideoPlayerRadioRDS->SendMessage(std::make_shared<CDVDMsgDemuxerPacket>(pPacket, drop));
+}
+
+void CVideoPlayer::ProcessAudioID3Data(CDemuxStream* pStream, DemuxPacket* pPacket)
+{
+ CheckStreamChanges(m_CurrentAudioID3, pStream);
+
+ UpdateTimestamps(m_CurrentAudioID3, pPacket);
+
+ bool drop = false;
+ if (CheckPlayerInit(m_CurrentAudioID3))
+ drop = true;
+
+ if (CheckSceneSkip(m_CurrentAudioID3))
+ drop = true;
+
+ m_VideoPlayerAudioID3->SendMessage(std::make_shared<CDVDMsgDemuxerPacket>(pPacket, drop));
+}
+
+bool CVideoPlayer::GetCachingTimes(double& level, double& delay, double& offset)
+{
+ if (!m_pInputStream || !m_pDemuxer)
+ return false;
+
+ XFILE::SCacheStatus status;
+ if (!m_pInputStream->GetCacheStatus(&status))
+ return false;
+
+ const uint64_t& cached = status.forward;
+ const uint32_t& currate = status.currate;
+ const uint32_t& maxrate = status.maxrate;
+ const uint32_t& lowrate = status.lowrate;
+
+ int64_t length = m_pInputStream->GetLength();
+ int64_t remain = length - m_pInputStream->Seek(0, SEEK_CUR);
+
+ if (length <= 0 || remain < 0)
+ return false;
+
+ double play_sbp = DVD_MSEC_TO_TIME(m_pDemuxer->GetStreamLength()) / length;
+ double queued = 1000.0 * GetQueueTime() / play_sbp;
+
+ delay = 0.0;
+ level = 0.0;
+ offset = (cached + queued) / length;
+
+ if (currate == 0)
+ return true;
+
+ double cache_sbp = 1.1 * (double)DVD_TIME_BASE / currate; /* underestimate by 10 % */
+ double play_left = play_sbp * (remain + queued); /* time to play out all remaining bytes */
+ double cache_left = cache_sbp * (remain - cached); /* time to cache the remaining bytes */
+ double cache_need = std::max(0.0, remain - play_left / cache_sbp); /* bytes needed until play_left == cache_left */
+
+ delay = cache_left - play_left;
+
+ if (lowrate > 0)
+ {
+ CLog::Log(LOGDEBUG, "Readrate {} was too low with {} required", lowrate, maxrate);
+ level = -1.0; /* buffer is full & our read rate is too low */
+ }
+ else
+ level = (cached + queued) / (cache_need + queued);
+
+ return true;
+}
+
+void CVideoPlayer::HandlePlaySpeed()
+{
+ const bool isInMenu = IsInMenuInternal();
+ const bool tolerateStall =
+ isInMenu || (m_CurrentVideo.hint.flags & StreamFlags::FLAG_STILL_IMAGES);
+
+ if (tolerateStall && m_caching != CACHESTATE_DONE)
+ SetCaching(CACHESTATE_DONE);
+
+ if (m_caching == CACHESTATE_FULL)
+ {
+ double level, delay, offset;
+ if (GetCachingTimes(level, delay, offset))
+ {
+ if (level < 0.0)
+ {
+ CGUIDialogKaiToast::QueueNotification(g_localizeStrings.Get(21454), g_localizeStrings.Get(21455));
+ SetCaching(CACHESTATE_INIT);
+ }
+ if (level >= 1.0)
+ SetCaching(CACHESTATE_INIT);
+ }
+ else
+ {
+ if ((!m_VideoPlayerAudio->AcceptsData() && m_CurrentAudio.id >= 0) ||
+ (!m_VideoPlayerVideo->AcceptsData() && m_CurrentVideo.id >= 0))
+ SetCaching(CACHESTATE_INIT);
+ }
+
+ // if audio stream stalled, wait until demux queue filled 10%
+ if (m_pInputStream->IsRealtime() &&
+ (m_CurrentAudio.id < 0 || m_VideoPlayerAudio->GetLevel() > 10))
+ {
+ SetCaching(CACHESTATE_INIT);
+ }
+ }
+
+ if (m_caching == CACHESTATE_INIT)
+ {
+ // if all enabled streams have been inited we are done
+ if ((m_CurrentVideo.id >= 0 || m_CurrentAudio.id >= 0) &&
+ (m_CurrentVideo.id < 0 || m_CurrentVideo.syncState != IDVDStreamPlayer::SYNC_STARTING) &&
+ (m_CurrentAudio.id < 0 || m_CurrentAudio.syncState != IDVDStreamPlayer::SYNC_STARTING))
+ SetCaching(CACHESTATE_PLAY);
+
+ // handle exceptions
+ if (m_CurrentAudio.id >= 0 && m_CurrentVideo.id >= 0)
+ {
+ if ((!m_VideoPlayerAudio->AcceptsData() || !m_VideoPlayerVideo->AcceptsData()) &&
+ m_cachingTimer.IsTimePast())
+ {
+ SetCaching(CACHESTATE_DONE);
+ }
+ }
+ }
+
+ if (m_caching == CACHESTATE_PLAY)
+ {
+ // if all enabled streams have started playing we are done
+ if ((m_CurrentVideo.id < 0 || !m_VideoPlayerVideo->IsStalled()) &&
+ (m_CurrentAudio.id < 0 || !m_VideoPlayerAudio->IsStalled()))
+ SetCaching(CACHESTATE_DONE);
+ }
+
+ if (m_caching == CACHESTATE_DONE)
+ {
+ if (m_playSpeed == DVD_PLAYSPEED_NORMAL && !tolerateStall)
+ {
+ // take action if audio or video stream is stalled
+ if (((m_VideoPlayerAudio->IsStalled() && m_CurrentAudio.inited) ||
+ (m_VideoPlayerVideo->IsStalled() && m_CurrentVideo.inited)) &&
+ m_syncTimer.IsTimePast())
+ {
+ if (m_pInputStream->IsRealtime())
+ {
+ if ((m_CurrentAudio.id >= 0 && m_CurrentAudio.syncState == IDVDStreamPlayer::SYNC_INSYNC &&
+ m_VideoPlayerAudio->IsStalled()) ||
+ (m_CurrentVideo.id >= 0 && m_CurrentVideo.syncState == IDVDStreamPlayer::SYNC_INSYNC &&
+ m_processInfo->GetLevelVQ() == 0))
+ {
+ CLog::Log(LOGDEBUG, "Stream stalled, start buffering. Audio: {} - Video: {}",
+ m_VideoPlayerAudio->GetLevel(), m_processInfo->GetLevelVQ());
+
+ if (m_VideoPlayerAudio->AcceptsData() && m_VideoPlayerVideo->AcceptsData())
+ SetCaching(CACHESTATE_FULL);
+ else
+ FlushBuffers(DVD_NOPTS_VALUE, false, true);
+ }
+ }
+ else
+ {
+ // start caching if audio and video have run dry
+ if (m_VideoPlayerAudio->GetLevel() <= 50 &&
+ m_processInfo->GetLevelVQ() <= 50)
+ {
+ SetCaching(CACHESTATE_FULL);
+ }
+ else if (m_CurrentAudio.id >= 0 && m_CurrentAudio.inited &&
+ m_CurrentAudio.syncState == IDVDStreamPlayer::SYNC_INSYNC &&
+ m_VideoPlayerAudio->GetLevel() == 0)
+ {
+ CLog::Log(LOGDEBUG,"CVideoPlayer::HandlePlaySpeed - audio stream stalled, triggering re-sync");
+ FlushBuffers(DVD_NOPTS_VALUE, true, true);
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = (int)GetUpdatedTime();
+ mode.backward = false;
+ mode.accurate = true;
+ mode.sync = true;
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ }
+ }
+ }
+ // care for live streams
+ else if (m_pInputStream->IsRealtime())
+ {
+ if (m_CurrentAudio.id >= 0)
+ {
+ double adjust = -1.0; // a unique value
+ if (m_clock.GetSpeedAdjust() >= 0 && m_VideoPlayerAudio->GetLevel() < 5)
+ adjust = -0.05;
+
+ if (m_clock.GetSpeedAdjust() < 0 && m_VideoPlayerAudio->GetLevel() > 10)
+ adjust = 0.0;
+
+ if (adjust != -1.0)
+ {
+ m_clock.SetSpeedAdjust(adjust);
+ }
+ }
+ }
+ }
+ }
+
+ // sync streams to clock
+ if ((m_CurrentVideo.syncState == IDVDStreamPlayer::SYNC_WAITSYNC) ||
+ (m_CurrentAudio.syncState == IDVDStreamPlayer::SYNC_WAITSYNC))
+ {
+ unsigned int threshold = 20;
+ if (m_pInputStream->IsRealtime())
+ threshold = 40;
+
+ bool video = m_CurrentVideo.id < 0 || (m_CurrentVideo.syncState == IDVDStreamPlayer::SYNC_WAITSYNC) ||
+ (m_CurrentVideo.packets == 0 && m_CurrentAudio.packets > threshold) ||
+ (!m_VideoPlayerAudio->AcceptsData() && m_processInfo->GetLevelVQ() < 10);
+ bool audio = m_CurrentAudio.id < 0 || (m_CurrentAudio.syncState == IDVDStreamPlayer::SYNC_WAITSYNC) ||
+ (m_CurrentAudio.packets == 0 && m_CurrentVideo.packets > threshold) ||
+ (!m_VideoPlayerVideo->AcceptsData() && m_VideoPlayerAudio->GetLevel() < 10);
+
+ if (m_CurrentAudio.syncState == IDVDStreamPlayer::SYNC_WAITSYNC &&
+ (m_CurrentAudio.avsync == CCurrentStream::AV_SYNC_CONT ||
+ m_CurrentVideo.syncState == IDVDStreamPlayer::SYNC_INSYNC))
+ {
+ m_CurrentAudio.syncState = IDVDStreamPlayer::SYNC_INSYNC;
+ m_CurrentAudio.avsync = CCurrentStream::AV_SYNC_NONE;
+ m_VideoPlayerAudio->SendMessage(
+ std::make_shared<CDVDMsgDouble>(CDVDMsg::GENERAL_RESYNC, m_clock.GetClock()), 1);
+ }
+ else if (m_CurrentVideo.syncState == IDVDStreamPlayer::SYNC_WAITSYNC &&
+ (m_CurrentVideo.avsync == CCurrentStream::AV_SYNC_CONT ||
+ m_CurrentAudio.syncState == IDVDStreamPlayer::SYNC_INSYNC))
+ {
+ m_CurrentVideo.syncState = IDVDStreamPlayer::SYNC_INSYNC;
+ m_CurrentVideo.avsync = CCurrentStream::AV_SYNC_NONE;
+ m_VideoPlayerVideo->SendMessage(
+ std::make_shared<CDVDMsgDouble>(CDVDMsg::GENERAL_RESYNC, m_clock.GetClock()), 1);
+ }
+ else if (video && audio)
+ {
+ double clock = 0;
+ if (m_CurrentAudio.syncState == IDVDStreamPlayer::SYNC_WAITSYNC)
+ CLog::Log(LOGDEBUG, "VideoPlayer::Sync - Audio - pts: {:f}, cache: {:f}, totalcache: {:f}",
+ m_CurrentAudio.starttime, m_CurrentAudio.cachetime, m_CurrentAudio.cachetotal);
+ if (m_CurrentVideo.syncState == IDVDStreamPlayer::SYNC_WAITSYNC)
+ CLog::Log(LOGDEBUG, "VideoPlayer::Sync - Video - pts: {:f}, cache: {:f}, totalcache: {:f}",
+ m_CurrentVideo.starttime, m_CurrentVideo.cachetime, m_CurrentVideo.cachetotal);
+
+ if (m_CurrentVideo.starttime != DVD_NOPTS_VALUE && m_CurrentVideo.packets > 0 &&
+ m_playSpeed == DVD_PLAYSPEED_PAUSE)
+ {
+ clock = m_CurrentVideo.starttime;
+ }
+ else if (m_CurrentAudio.starttime != DVD_NOPTS_VALUE && m_CurrentAudio.packets > 0)
+ {
+ if (m_pInputStream->IsRealtime())
+ clock = m_CurrentAudio.starttime - m_CurrentAudio.cachetotal - DVD_MSEC_TO_TIME(400);
+ else
+ clock = m_CurrentAudio.starttime - m_CurrentAudio.cachetime;
+
+ if (m_CurrentVideo.starttime != DVD_NOPTS_VALUE && (m_CurrentVideo.packets > 0))
+ {
+ if (m_CurrentVideo.starttime - m_CurrentVideo.cachetotal < clock)
+ {
+ clock = m_CurrentVideo.starttime - m_CurrentVideo.cachetotal;
+ }
+ else if (m_CurrentVideo.starttime > m_CurrentAudio.starttime &&
+ !m_pInputStream->IsRealtime())
+ {
+ int audioLevel = m_VideoPlayerAudio->GetLevel();
+ //@todo hardcoded 8 seconds in message queue
+ double maxAudioTime = clock + DVD_MSEC_TO_TIME(80 * audioLevel);
+ if ((m_CurrentVideo.starttime - m_CurrentVideo.cachetotal) > maxAudioTime)
+ clock = maxAudioTime;
+ else
+ clock = m_CurrentVideo.starttime - m_CurrentVideo.cachetotal;
+ }
+ }
+ }
+ else if (m_CurrentVideo.starttime != DVD_NOPTS_VALUE && m_CurrentVideo.packets > 0)
+ {
+ clock = m_CurrentVideo.starttime - m_CurrentVideo.cachetotal;
+ }
+
+ m_clock.Discontinuity(clock);
+ m_CurrentAudio.syncState = IDVDStreamPlayer::SYNC_INSYNC;
+ m_CurrentAudio.avsync = CCurrentStream::AV_SYNC_NONE;
+ m_CurrentVideo.syncState = IDVDStreamPlayer::SYNC_INSYNC;
+ m_CurrentVideo.avsync = CCurrentStream::AV_SYNC_NONE;
+ m_VideoPlayerAudio->SendMessage(
+ std::make_shared<CDVDMsgDouble>(CDVDMsg::GENERAL_RESYNC, clock), 1);
+ m_VideoPlayerVideo->SendMessage(
+ std::make_shared<CDVDMsgDouble>(CDVDMsg::GENERAL_RESYNC, clock), 1);
+ SetCaching(CACHESTATE_DONE);
+ UpdatePlayState(0);
+
+ m_syncTimer.Set(3000ms);
+
+ if (!m_State.streamsReady)
+ {
+ if (m_playerOptions.fullscreen)
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SWITCHTOFULLSCREEN);
+ }
+
+ IPlayerCallback *cb = &m_callback;
+ CFileItem fileItem = m_item;
+ m_outboundEvents->Submit([=]() {
+ cb->OnAVStarted(fileItem);
+ });
+ m_State.streamsReady = true;
+ }
+ }
+ else
+ {
+ // exceptions for which stream players won't start properly
+ // 1. videoplayer has not detected a keyframe within length of demux buffers
+ if (m_CurrentAudio.id >= 0 && m_CurrentVideo.id >= 0 &&
+ !m_VideoPlayerAudio->AcceptsData() &&
+ m_CurrentVideo.syncState == IDVDStreamPlayer::SYNC_STARTING &&
+ m_VideoPlayerVideo->IsStalled() &&
+ m_CurrentVideo.packets > 10)
+ {
+ m_VideoPlayerAudio->AcceptsData();
+ CLog::Log(LOGWARNING, "VideoPlayer::Sync - stream player video does not start, flushing buffers");
+ FlushBuffers(DVD_NOPTS_VALUE, true, true);
+ }
+ }
+ }
+
+ // handle ff/rw
+ if (m_playSpeed != DVD_PLAYSPEED_NORMAL && m_playSpeed != DVD_PLAYSPEED_PAUSE)
+ {
+ if (isInMenu)
+ {
+ // this can't be done in menu
+ SetPlaySpeed(DVD_PLAYSPEED_NORMAL);
+
+ }
+ else
+ {
+ bool check = true;
+
+ // only check if we have video
+ if (m_CurrentVideo.id < 0 || m_CurrentVideo.syncState != IDVDStreamPlayer::SYNC_INSYNC)
+ check = false;
+ // video message queue either initiated or already seen eof
+ else if (m_CurrentVideo.inited == false && m_playSpeed >= 0)
+ check = false;
+ // don't check if time has not advanced since last check
+ else if (m_SpeedState.lasttime == GetTime())
+ check = false;
+ // skip if frame at screen has no valid timestamp
+ else if (m_VideoPlayerVideo->GetCurrentPts() == DVD_NOPTS_VALUE)
+ check = false;
+ // skip if frame on screen has not changed
+ else if (m_SpeedState.lastpts == m_VideoPlayerVideo->GetCurrentPts() &&
+ (m_SpeedState.lastpts > m_State.dts || m_playSpeed > 0))
+ check = false;
+
+ if (check)
+ {
+ m_SpeedState.lastpts = m_VideoPlayerVideo->GetCurrentPts();
+ m_SpeedState.lasttime = GetTime();
+ m_SpeedState.lastabstime = m_clock.GetAbsoluteClock();
+ // check how much off clock video is when ff/rw:ing
+ // a problem here is that seeking isn't very accurate
+ // and since the clock will be resynced after seek
+ // we might actually not really be playing at the wanted
+ // speed. we'd need to have some way to not resync the clock
+ // after a seek to remember timing. still need to handle
+ // discontinuities somehow
+
+ double error;
+ error = m_clock.GetClock() - m_SpeedState.lastpts;
+ error *= m_playSpeed / abs(m_playSpeed);
+
+ // allow a bigger error when going ff, the faster we go
+ // the the bigger is the error we allow
+ if (m_playSpeed > DVD_PLAYSPEED_NORMAL)
+ {
+ int errorwin = m_playSpeed / DVD_PLAYSPEED_NORMAL;
+ if (errorwin > 8)
+ errorwin = 8;
+ error /= errorwin;
+ }
+
+ if (error > DVD_MSEC_TO_TIME(1000))
+ {
+ error = (m_clock.GetClock() - m_SpeedState.lastseekpts) / 1000;
+
+ if (std::abs(error) > 1000 || (m_VideoPlayerVideo->IsRewindStalled() && std::abs(error) > 100))
+ {
+ CLog::Log(LOGDEBUG, "CVideoPlayer::Process - Seeking to catch up, error was: {:f}",
+ error);
+ m_SpeedState.lastseekpts = m_clock.GetClock();
+ int direction = (m_playSpeed > 0) ? 1 : -1;
+ double iTime = (m_clock.GetClock() + m_State.time_offset + 1000000.0 * direction) / 1000;
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = iTime;
+ mode.backward = (m_playSpeed < 0);
+ mode.accurate = false;
+ mode.restore = false;
+ mode.trickplay = true;
+ mode.sync = false;
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ }
+ }
+ }
+ }
+ }
+
+ // reset tempo
+ if (!m_State.cantempo)
+ {
+ float currentTempo = m_processInfo->GetNewTempo();
+ if (currentTempo != 1.0f)
+ {
+ SetTempo(1.0f);
+ }
+ }
+}
+
+bool CVideoPlayer::CheckPlayerInit(CCurrentStream& current)
+{
+ if (current.inited)
+ return false;
+
+ if (current.startpts != DVD_NOPTS_VALUE)
+ {
+ if(current.dts == DVD_NOPTS_VALUE)
+ {
+ CLog::Log(LOGDEBUG, "{} - dropping packet type:{} dts:{:f} to get to start point at {:f}",
+ __FUNCTION__, current.player, current.dts, current.startpts);
+ return true;
+ }
+
+ if ((current.startpts - current.dts) > DVD_SEC_TO_TIME(20))
+ {
+ CLog::Log(LOGDEBUG, "{} - too far to decode before finishing seek", __FUNCTION__);
+ if(m_CurrentAudio.startpts != DVD_NOPTS_VALUE)
+ m_CurrentAudio.startpts = current.dts;
+ if(m_CurrentVideo.startpts != DVD_NOPTS_VALUE)
+ m_CurrentVideo.startpts = current.dts;
+ if(m_CurrentSubtitle.startpts != DVD_NOPTS_VALUE)
+ m_CurrentSubtitle.startpts = current.dts;
+ if(m_CurrentTeletext.startpts != DVD_NOPTS_VALUE)
+ m_CurrentTeletext.startpts = current.dts;
+ if(m_CurrentRadioRDS.startpts != DVD_NOPTS_VALUE)
+ m_CurrentRadioRDS.startpts = current.dts;
+ if (m_CurrentAudioID3.startpts != DVD_NOPTS_VALUE)
+ m_CurrentAudioID3.startpts = current.dts;
+ }
+
+ if(current.dts < current.startpts)
+ {
+ CLog::Log(LOGDEBUG, "{} - dropping packet type:{} dts:{:f} to get to start point at {:f}",
+ __FUNCTION__, current.player, current.dts, current.startpts);
+ return true;
+ }
+ }
+
+ if (current.dts != DVD_NOPTS_VALUE)
+ {
+ current.inited = true;
+ current.startpts = current.dts;
+ }
+ return false;
+}
+
+void CVideoPlayer::UpdateCorrection(DemuxPacket* pkt, double correction)
+{
+ pkt->m_ptsOffsetCorrection = correction;
+
+ if(pkt->dts != DVD_NOPTS_VALUE)
+ pkt->dts -= correction;
+ if(pkt->pts != DVD_NOPTS_VALUE)
+ pkt->pts -= correction;
+}
+
+void CVideoPlayer::UpdateTimestamps(CCurrentStream& current, DemuxPacket* pPacket)
+{
+ double dts = current.dts;
+ /* update stored values */
+ if(pPacket->dts != DVD_NOPTS_VALUE)
+ dts = pPacket->dts;
+ else if(pPacket->pts != DVD_NOPTS_VALUE)
+ dts = pPacket->pts;
+
+ /* calculate some average duration */
+ if(pPacket->duration != DVD_NOPTS_VALUE)
+ current.dur = pPacket->duration;
+ else if(dts != DVD_NOPTS_VALUE && current.dts != DVD_NOPTS_VALUE)
+ current.dur = 0.1 * (current.dur * 9 + (dts - current.dts));
+
+ current.dts = dts;
+
+ current.dispTime = pPacket->dispTime;
+}
+
+static void UpdateLimits(double& minimum, double& maximum, double dts)
+{
+ if(dts == DVD_NOPTS_VALUE)
+ return;
+ if(minimum == DVD_NOPTS_VALUE || minimum > dts) minimum = dts;
+ if(maximum == DVD_NOPTS_VALUE || maximum < dts) maximum = dts;
+}
+
+bool CVideoPlayer::CheckContinuity(CCurrentStream& current, DemuxPacket* pPacket)
+{
+ if (m_playSpeed < DVD_PLAYSPEED_PAUSE)
+ return false;
+
+ if( pPacket->dts == DVD_NOPTS_VALUE || current.dts == DVD_NOPTS_VALUE)
+ return false;
+
+ double mindts = DVD_NOPTS_VALUE, maxdts = DVD_NOPTS_VALUE;
+ UpdateLimits(mindts, maxdts, m_CurrentAudio.dts);
+ UpdateLimits(mindts, maxdts, m_CurrentVideo.dts);
+ UpdateLimits(mindts, maxdts, m_CurrentAudio.dts_end());
+ UpdateLimits(mindts, maxdts, m_CurrentVideo.dts_end());
+
+ /* if we don't have max and min, we can't do anything more */
+ if( mindts == DVD_NOPTS_VALUE || maxdts == DVD_NOPTS_VALUE )
+ return false;
+
+ double correction = 0.0;
+ if( pPacket->dts > maxdts + DVD_MSEC_TO_TIME(1000))
+ {
+ CLog::Log(LOGDEBUG,
+ "CVideoPlayer::CheckContinuity - resync forward :{}, prev:{:f}, curr:{:f}, diff:{:f}",
+ current.type, current.dts, pPacket->dts, pPacket->dts - maxdts);
+ correction = pPacket->dts - maxdts;
+ }
+
+ /* if it's large scale jump, correct for it after having confirmed the jump */
+ if(pPacket->dts + DVD_MSEC_TO_TIME(500) < current.dts_end())
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "CVideoPlayer::CheckContinuity - resync backward :{}, prev:{:f}, curr:{:f}, diff:{:f}",
+ current.type, current.dts, pPacket->dts, pPacket->dts - current.dts);
+ correction = pPacket->dts - current.dts_end();
+ }
+ else if(pPacket->dts < current.dts)
+ {
+ CLog::Log(LOGDEBUG,
+ "CVideoPlayer::CheckContinuity - wrapback :{}, prev:{:f}, curr:{:f}, diff:{:f}",
+ current.type, current.dts, pPacket->dts, pPacket->dts - current.dts);
+ }
+
+ double lastdts = pPacket->dts;
+ if(correction != 0.0)
+ {
+ // we want the dts values of two streams to close, or for one to be invalid (e.g. from a missing audio stream)
+ double this_dts = pPacket->dts;
+ double that_dts = current.type == STREAM_AUDIO ? m_CurrentVideo.lastdts : m_CurrentAudio.lastdts;
+
+ if (m_CurrentAudio.id == -1 || m_CurrentVideo.id == -1 ||
+ current.lastdts == DVD_NOPTS_VALUE ||
+ fabs(this_dts - that_dts) < DVD_MSEC_TO_TIME(1000))
+ {
+ m_offset_pts += correction;
+ UpdateCorrection(pPacket, correction);
+ lastdts = pPacket->dts;
+ CLog::Log(LOGDEBUG, "CVideoPlayer::CheckContinuity - update correction: {:f}", correction);
+ if (current.avsync == CCurrentStream::AV_SYNC_CHECK)
+ current.avsync = CCurrentStream::AV_SYNC_CONT;
+ }
+ else
+ {
+ // not sure yet - flags the packets as unknown until we get confirmation on another audio/video packet
+ pPacket->dts = DVD_NOPTS_VALUE;
+ pPacket->pts = DVD_NOPTS_VALUE;
+ }
+ }
+ else
+ {
+ if (current.avsync == CCurrentStream::AV_SYNC_CHECK)
+ current.avsync = CCurrentStream::AV_SYNC_CONT;
+ }
+ current.lastdts = lastdts;
+ return true;
+}
+
+bool CVideoPlayer::CheckSceneSkip(const CCurrentStream& current)
+{
+ if (!m_Edl.HasEdits())
+ return false;
+
+ if(current.dts == DVD_NOPTS_VALUE)
+ return false;
+
+ if(current.inited == false)
+ return false;
+
+ EDL::Edit edit;
+ return m_Edl.InEdit(DVD_TIME_TO_MSEC(current.dts + m_offset_pts), &edit) &&
+ edit.action == EDL::Action::CUT;
+}
+
+void CVideoPlayer::CheckAutoSceneSkip()
+{
+ if (!m_Edl.HasEdits())
+ return;
+
+ // Check that there is an audio and video stream.
+ if((m_CurrentAudio.id < 0 || m_CurrentAudio.syncState != IDVDStreamPlayer::SYNC_INSYNC) ||
+ (m_CurrentVideo.id < 0 || m_CurrentVideo.syncState != IDVDStreamPlayer::SYNC_INSYNC))
+ return;
+
+ // If there is a startpts defined for either the audio or video stream then VideoPlayer is still
+ // still decoding frames to get to the previously requested seek point.
+ if (m_CurrentAudio.inited == false ||
+ m_CurrentVideo.inited == false)
+ return;
+
+ const int64_t clock = GetTime();
+
+ const double correctClock = m_Edl.GetTimeAfterRestoringCuts(clock);
+ EDL::Edit edit;
+ if (!m_Edl.InEdit(correctClock, &edit))
+ {
+ // @note: Users are allowed to jump back into EDL commercial breaks
+ // do not reset the last edit time if the last surpassed edit is a commercial break
+ if (m_Edl.GetLastEditActionType() != EDL::Action::COMM_BREAK)
+ {
+ m_Edl.ResetLastEditTime();
+ }
+ return;
+ }
+
+ if (edit.action == EDL::Action::CUT)
+ {
+ if ((m_playSpeed > 0 && correctClock < (edit.start + 1000)) ||
+ (m_playSpeed < 0 && correctClock < (edit.end - 1000)))
+ {
+ CLog::Log(LOGDEBUG, "{} - Clock in EDL cut [{} - {}]: {}. Automatically skipping over.",
+ __FUNCTION__, CEdl::MillisecondsToTimeString(edit.start),
+ CEdl::MillisecondsToTimeString(edit.end), CEdl::MillisecondsToTimeString(clock));
+
+ // Seeking either goes to the start or the end of the cut depending on the play direction.
+ int seek = m_playSpeed >= 0 ? edit.end : edit.start;
+ if (m_Edl.GetLastEditTime() != seek)
+ {
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = seek;
+ mode.backward = true;
+ mode.accurate = true;
+ mode.restore = false;
+ mode.trickplay = false;
+ mode.sync = true;
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+
+ m_Edl.SetLastEditTime(seek);
+ m_Edl.SetLastEditActionType(edit.action);
+ }
+ }
+ }
+ else if (edit.action == EDL::Action::COMM_BREAK)
+ {
+ // marker for commbreak may be inaccurate. allow user to skip into break from the back
+ if (m_playSpeed >= 0 && m_Edl.GetLastEditTime() != edit.start && clock < edit.end - 1000)
+ {
+ const std::shared_ptr<CAdvancedSettings> advancedSettings =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+ if (advancedSettings && advancedSettings->m_EdlDisplayCommbreakNotifications)
+ {
+ const std::string timeString =
+ StringUtils::SecondsToTimeString((edit.end - edit.start) / 1000, TIME_FORMAT_MM_SS);
+ CGUIDialogKaiToast::QueueNotification(g_localizeStrings.Get(25011), timeString);
+ }
+
+ m_Edl.SetLastEditTime(edit.start);
+ m_Edl.SetLastEditActionType(edit.action);
+
+ if (m_SkipCommercials)
+ {
+ CLog::Log(LOGDEBUG,
+ "{} - Clock in commercial break [{} - {}]: {}. Automatically skipping to end of "
+ "commercial break",
+ __FUNCTION__, CEdl::MillisecondsToTimeString(edit.start),
+ CEdl::MillisecondsToTimeString(edit.end), CEdl::MillisecondsToTimeString(clock));
+
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = edit.end;
+ mode.backward = true;
+ mode.accurate = true;
+ mode.restore = false;
+ mode.trickplay = false;
+ mode.sync = true;
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ }
+ }
+ }
+}
+
+
+void CVideoPlayer::SynchronizeDemuxer()
+{
+ if(IsCurrentThread())
+ return;
+ if(!m_messenger.IsInited())
+ return;
+
+ auto message = std::make_shared<CDVDMsgGeneralSynchronize>(500ms, SYNCSOURCE_PLAYER);
+ m_messenger.Put(message);
+ message->Wait(m_bStop, 0);
+}
+
+IDVDStreamPlayer* CVideoPlayer::GetStreamPlayer(unsigned int target)
+{
+ if(target == VideoPlayer_AUDIO)
+ return m_VideoPlayerAudio;
+ if(target == VideoPlayer_VIDEO)
+ return m_VideoPlayerVideo;
+ if(target == VideoPlayer_SUBTITLE)
+ return m_VideoPlayerSubtitle;
+ if(target == VideoPlayer_TELETEXT)
+ return m_VideoPlayerTeletext;
+ if(target == VideoPlayer_RDS)
+ return m_VideoPlayerRadioRDS;
+ if (target == VideoPlayer_ID3)
+ return m_VideoPlayerAudioID3.get();
+ return NULL;
+}
+
+void CVideoPlayer::SendPlayerMessage(std::shared_ptr<CDVDMsg> pMsg, unsigned int target)
+{
+ IDVDStreamPlayer* player = GetStreamPlayer(target);
+ if(player)
+ player->SendMessage(std::move(pMsg), 0);
+}
+
+void CVideoPlayer::OnExit()
+{
+ CLog::Log(LOGINFO, "CVideoPlayer::OnExit()");
+
+ // set event to inform openfile something went wrong in case openfile is still waiting for this event
+ SetCaching(CACHESTATE_DONE);
+
+ // close each stream
+ if (!m_bAbortRequest)
+ CLog::Log(LOGINFO, "VideoPlayer: eof, waiting for queues to empty");
+
+ CFileItem fileItem(m_item);
+ UpdateFileItemStreamDetails(fileItem);
+
+ CloseStream(m_CurrentAudio, !m_bAbortRequest);
+ CloseStream(m_CurrentVideo, !m_bAbortRequest);
+ CloseStream(m_CurrentTeletext,!m_bAbortRequest);
+ CloseStream(m_CurrentRadioRDS, !m_bAbortRequest);
+ CloseStream(m_CurrentAudioID3, !m_bAbortRequest);
+ // the generalization principle was abused for subtitle player. actually it is not a stream player like
+ // video and audio. subtitle player does not run on its own thread, hence waitForBuffers makes
+ // no sense here. waitForBuffers is abused to clear overlay container (false clears container)
+ // subtitles are added from video player. after video player has finished, overlays have to be cleared.
+ CloseStream(m_CurrentSubtitle, false); // clear overlay container
+
+ CServiceBroker::GetWinSystem()->UnregisterRenderLoop(this);
+
+ IPlayerCallback *cb = &m_callback;
+ CVideoSettings vs = m_processInfo->GetVideoSettings();
+ m_outboundEvents->Submit([=]() {
+ cb->StoreVideoSettings(fileItem, vs);
+ });
+
+ CBookmark bookmark;
+ bookmark.totalTimeInSeconds = 0;
+ bookmark.timeInSeconds = 0;
+ if (m_State.startTime == 0)
+ {
+ bookmark.totalTimeInSeconds = m_State.timeMax / 1000;
+ bookmark.timeInSeconds = m_State.time / 1000;
+ }
+ bookmark.player = m_name;
+ bookmark.playerState = GetPlayerState();
+ m_outboundEvents->Submit([=]() {
+ cb->OnPlayerCloseFile(fileItem, bookmark);
+ });
+
+ // destroy objects
+ m_renderManager.Flush(false, false);
+ m_pDemuxer.reset();
+ m_pSubtitleDemuxer.reset();
+ m_subtitleDemuxerMap.clear();
+ m_pCCDemuxer.reset();
+ if (m_pInputStream.use_count() > 1)
+ throw std::runtime_error("m_pInputStream reference count is greater than 1");
+ m_pInputStream.reset();
+
+ // clean up all selection streams
+ m_SelectionStreams.Clear(STREAM_NONE, STREAM_SOURCE_NONE);
+
+ m_messenger.End();
+
+ CFFmpegLog::ClearLogLevel();
+ m_bStop = true;
+
+ bool error = m_error;
+ bool close = m_bCloseRequest;
+ m_outboundEvents->Submit([=]() {
+ if (close)
+ cb->OnPlayBackStopped();
+ else if (error)
+ cb->OnPlayBackError();
+ else
+ cb->OnPlayBackEnded();
+ });
+}
+
+void CVideoPlayer::HandleMessages()
+{
+ std::shared_ptr<CDVDMsg> pMsg = nullptr;
+
+ while (m_messenger.Get(pMsg, 0) == MSGQ_OK)
+ {
+ if (pMsg->IsType(CDVDMsg::PLAYER_OPENFILE) &&
+ m_messenger.GetPacketCount(CDVDMsg::PLAYER_OPENFILE) == 0)
+ {
+ CDVDMsgOpenFile& msg(*std::static_pointer_cast<CDVDMsgOpenFile>(pMsg));
+
+ IPlayerCallback *cb = &m_callback;
+ CFileItem fileItem(m_item);
+ UpdateFileItemStreamDetails(fileItem);
+ CVideoSettings vs = m_processInfo->GetVideoSettings();
+ m_outboundEvents->Submit([=]() {
+ cb->StoreVideoSettings(fileItem, vs);
+ });
+
+ CBookmark bookmark;
+ bookmark.totalTimeInSeconds = 0;
+ bookmark.timeInSeconds = 0;
+ if (m_State.startTime == 0)
+ {
+ bookmark.totalTimeInSeconds = m_State.timeMax / 1000;
+ bookmark.timeInSeconds = m_State.time / 1000;
+ }
+ bookmark.player = m_name;
+ bookmark.playerState = GetPlayerState();
+ m_outboundEvents->Submit([=]() {
+ cb->OnPlayerCloseFile(fileItem, bookmark);
+ });
+
+ m_item = msg.GetItem();
+ m_playerOptions = msg.GetOptions();
+
+ m_processInfo->SetPlayTimes(0,0,0,0);
+
+ m_outboundEvents->Submit([this]() {
+ m_callback.OnPlayBackStarted(m_item);
+ });
+
+ FlushBuffers(DVD_NOPTS_VALUE, true, true);
+ m_renderManager.Flush(false, false);
+ m_pDemuxer.reset();
+ m_pSubtitleDemuxer.reset();
+ m_subtitleDemuxerMap.clear();
+ m_pCCDemuxer.reset();
+ if (m_pInputStream.use_count() > 1)
+ throw std::runtime_error("m_pInputStream reference count is greater than 1");
+ m_pInputStream.reset();
+
+ m_SelectionStreams.Clear(STREAM_NONE, STREAM_SOURCE_NONE);
+
+ Prepare();
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SEEK) &&
+ m_messenger.GetPacketCount(CDVDMsg::PLAYER_SEEK) == 0 &&
+ m_messenger.GetPacketCount(CDVDMsg::PLAYER_SEEK_CHAPTER) == 0)
+ {
+ CDVDMsgPlayerSeek& msg(*std::static_pointer_cast<CDVDMsgPlayerSeek>(pMsg));
+
+ if (!m_State.canseek)
+ {
+ m_processInfo->SetStateSeeking(false);
+ continue;
+ }
+
+ // skip seeks if player has not finished the last seek
+ if (m_CurrentVideo.id >= 0 &&
+ m_CurrentVideo.syncState != IDVDStreamPlayer::SYNC_INSYNC)
+ {
+ double now = m_clock.GetAbsoluteClock();
+ if (m_playSpeed == DVD_PLAYSPEED_NORMAL &&
+ (now - m_State.lastSeek)/1000 < 2000 &&
+ !msg.GetAccurate())
+ {
+ m_processInfo->SetStateSeeking(false);
+ continue;
+ }
+ }
+
+ if (!msg.GetTrickPlay())
+ {
+ m_processInfo->SeekFinished(0);
+ SetCaching(CACHESTATE_FLUSH);
+ }
+
+ double start = DVD_NOPTS_VALUE;
+
+ double time = msg.GetTime();
+ if (msg.GetRelative())
+ time = (m_clock.GetClock() + m_State.time_offset) / 1000l + time;
+
+ time = msg.GetRestore() ? m_Edl.GetTimeAfterRestoringCuts(time) : time;
+
+ // if input stream doesn't support ISeekTime, convert back to pts
+ //! @todo
+ //! After demuxer we add an offset to input pts so that displayed time and clock are
+ //! increasing steadily. For seeking we need to determine the boundaries and offset
+ //! of the desired segment. With the current approach calculated time may point
+ //! to nirvana
+ if (m_pInputStream->GetIPosTime() == nullptr)
+ time -= m_State.time_offset/1000l;
+
+ CLog::Log(LOGDEBUG, "demuxer seek to: {:f}", time);
+ if (m_pDemuxer && m_pDemuxer->SeekTime(time, msg.GetBackward(), &start))
+ {
+ CLog::Log(LOGDEBUG, "demuxer seek to: {:f}, success", time);
+ if(m_pSubtitleDemuxer)
+ {
+ if(!m_pSubtitleDemuxer->SeekTime(time, msg.GetBackward()))
+ CLog::Log(LOGDEBUG, "failed to seek subtitle demuxer: {:f}, success", time);
+ }
+ // dts after successful seek
+ if (start == DVD_NOPTS_VALUE)
+ start = DVD_MSEC_TO_TIME(time) - m_State.time_offset;
+
+ m_State.dts = start;
+ m_State.lastSeek = m_clock.GetAbsoluteClock();
+
+ FlushBuffers(start, msg.GetAccurate(), msg.GetSync());
+ }
+ else if (m_pDemuxer)
+ {
+ CLog::Log(LOGDEBUG, "VideoPlayer: seek failed or hit end of stream");
+ // dts after successful seek
+ if (start == DVD_NOPTS_VALUE)
+ start = DVD_MSEC_TO_TIME(time) - m_State.time_offset;
+
+ m_State.dts = start;
+
+ FlushBuffers(start, false, true);
+ if (m_playSpeed != DVD_PLAYSPEED_PAUSE)
+ {
+ SetPlaySpeed(DVD_PLAYSPEED_NORMAL);
+ }
+ }
+
+ // set flag to indicate we have finished a seeking request
+ if(!msg.GetTrickPlay())
+ {
+ m_processInfo->SeekFinished(0);
+ }
+
+ // dvd's will issue a HOP_CHANNEL that we need to skip
+ if(m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ m_dvd.state = DVDSTATE_SEEK;
+
+ m_processInfo->SetStateSeeking(false);
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SEEK_CHAPTER) &&
+ m_messenger.GetPacketCount(CDVDMsg::PLAYER_SEEK) == 0 &&
+ m_messenger.GetPacketCount(CDVDMsg::PLAYER_SEEK_CHAPTER) == 0)
+ {
+ m_processInfo->SeekFinished(0);
+ SetCaching(CACHESTATE_FLUSH);
+
+ CDVDMsgPlayerSeekChapter& msg(*std::static_pointer_cast<CDVDMsgPlayerSeekChapter>(pMsg));
+ double start = DVD_NOPTS_VALUE;
+ int offset = 0;
+
+ // This should always be the case.
+ if(m_pDemuxer && m_pDemuxer->SeekChapter(msg.GetChapter(), &start))
+ {
+ FlushBuffers(start, true, true);
+ int64_t beforeSeek = GetTime();
+ offset = DVD_TIME_TO_MSEC(start) - static_cast<int>(beforeSeek);
+ m_callback.OnPlayBackSeekChapter(msg.GetChapter());
+ }
+ else if (m_pInputStream)
+ {
+ CDVDInputStream::IChapter* pChapter = m_pInputStream->GetIChapter();
+ if (pChapter && pChapter->SeekChapter(msg.GetChapter()))
+ {
+ FlushBuffers(start, true, true);
+ int64_t beforeSeek = GetTime();
+ offset = DVD_TIME_TO_MSEC(start) - static_cast<int>(beforeSeek);
+ m_callback.OnPlayBackSeekChapter(msg.GetChapter());
+ }
+ }
+ m_processInfo->SeekFinished(offset);
+ }
+ else if (pMsg->IsType(CDVDMsg::DEMUXER_RESET))
+ {
+ m_CurrentAudio.stream = NULL;
+ m_CurrentVideo.stream = NULL;
+ m_CurrentSubtitle.stream = NULL;
+
+ // we need to reset the demuxer, probably because the streams have changed
+ if(m_pDemuxer)
+ m_pDemuxer->Reset();
+ if(m_pSubtitleDemuxer)
+ m_pSubtitleDemuxer->Reset();
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SET_AUDIOSTREAM))
+ {
+ auto pMsg2 = std::static_pointer_cast<CDVDMsgPlayerSetAudioStream>(pMsg);
+
+ SelectionStream& st = m_SelectionStreams.Get(STREAM_AUDIO, pMsg2->GetStreamId());
+ if(st.source != STREAM_SOURCE_NONE)
+ {
+ if(st.source == STREAM_SOURCE_NAV && m_pInputStream && m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ {
+ std::shared_ptr<CDVDInputStreamNavigator> pStream = std::static_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream);
+ if(pStream->SetActiveAudioStream(st.id))
+ {
+ m_dvd.iSelectedAudioStream = -1;
+ CloseStream(m_CurrentAudio, false);
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = (int)GetUpdatedTime();
+ mode.backward = true;
+ mode.accurate = true;
+ mode.trickplay = true;
+ mode.sync = true;
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ }
+ }
+ else
+ {
+ CloseStream(m_CurrentAudio, false);
+ OpenStream(m_CurrentAudio, st.demuxerId, st.id, st.source);
+ AdaptForcedSubtitles();
+
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = (int)GetUpdatedTime();
+ mode.backward = true;
+ mode.accurate = true;
+ mode.trickplay = true;
+ mode.sync = true;
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ }
+ }
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SET_VIDEOSTREAM))
+ {
+ auto pMsg2 = std::static_pointer_cast<CDVDMsgPlayerSetVideoStream>(pMsg);
+
+ SelectionStream& st = m_SelectionStreams.Get(STREAM_VIDEO, pMsg2->GetStreamId());
+ if (st.source != STREAM_SOURCE_NONE)
+ {
+ if (st.source == STREAM_SOURCE_NAV && m_pInputStream && m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ {
+ std::shared_ptr<CDVDInputStreamNavigator> pStream = std::static_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream);
+ if (pStream->SetAngle(st.id))
+ {
+ m_dvd.iSelectedVideoStream = st.id;
+
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = (int)GetUpdatedTime();
+ mode.backward = true;
+ mode.accurate = true;
+ mode.trickplay = true;
+ mode.sync = true;
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ }
+ }
+ else
+ {
+ CloseStream(m_CurrentVideo, false);
+ OpenStream(m_CurrentVideo, st.demuxerId, st.id, st.source);
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = (int)GetUpdatedTime();
+ mode.backward = true;
+ mode.accurate = true;
+ mode.trickplay = true;
+ mode.sync = true;
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ }
+ }
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SET_SUBTITLESTREAM))
+ {
+ auto pMsg2 = std::static_pointer_cast<CDVDMsgPlayerSetSubtitleStream>(pMsg);
+
+ SelectionStream& st = m_SelectionStreams.Get(STREAM_SUBTITLE, pMsg2->GetStreamId());
+ if(st.source != STREAM_SOURCE_NONE)
+ {
+ if(st.source == STREAM_SOURCE_NAV && m_pInputStream && m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ {
+ std::shared_ptr<CDVDInputStreamNavigator> pStream = std::static_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream);
+ if(pStream->SetActiveSubtitleStream(st.id))
+ {
+ m_dvd.iSelectedSPUStream = -1;
+ CloseStream(m_CurrentSubtitle, false);
+ }
+ }
+ else
+ {
+ CloseStream(m_CurrentSubtitle, false);
+ OpenStream(m_CurrentSubtitle, st.demuxerId, st.id, st.source);
+ }
+ }
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SET_SUBTITLESTREAM_VISIBLE))
+ {
+ auto pValue = std::static_pointer_cast<CDVDMsgBool>(pMsg);
+ SetSubtitleVisibleInternal(pValue->m_value);
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SET_PROGRAM))
+ {
+ auto msg = std::static_pointer_cast<CDVDMsgInt>(pMsg);
+ if (m_pDemuxer)
+ {
+ m_pDemuxer->SetProgram(msg->m_value);
+ FlushBuffers(DVD_NOPTS_VALUE, false, true);
+ }
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SET_STATE))
+ {
+ SetCaching(CACHESTATE_FLUSH);
+
+ auto pMsgPlayerSetState = std::static_pointer_cast<CDVDMsgPlayerSetState>(pMsg);
+
+ if (std::shared_ptr<CDVDInputStream::IMenus> ptr = std::dynamic_pointer_cast<CDVDInputStream::IMenus>(m_pInputStream))
+ {
+ if(ptr->SetState(pMsgPlayerSetState->GetState()))
+ {
+ m_dvd.state = DVDSTATE_NORMAL;
+ m_dvd.iDVDStillStartTime = {};
+ m_dvd.iDVDStillTime = 0ms;
+ }
+ }
+
+ m_processInfo->SeekFinished(0);
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_FLUSH))
+ {
+ FlushBuffers(DVD_NOPTS_VALUE, true, true);
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SETSPEED))
+ {
+ int speed = std::static_pointer_cast<CDVDMsgPlayerSetSpeed>(pMsg)->GetSpeed();
+
+ // correct our current clock, as it would start going wrong otherwise
+ if (m_State.timestamp > 0)
+ {
+ double offset;
+ offset = m_clock.GetAbsoluteClock() - m_State.timestamp;
+ offset *= m_playSpeed / DVD_PLAYSPEED_NORMAL;
+ offset = DVD_TIME_TO_MSEC(offset);
+ if (offset > 1000)
+ offset = 1000;
+ if (offset < -1000)
+ offset = -1000;
+ m_State.time += offset;
+ m_State.timestamp = m_clock.GetAbsoluteClock();
+ }
+
+ if (speed != DVD_PLAYSPEED_PAUSE && m_playSpeed != DVD_PLAYSPEED_PAUSE && speed != m_playSpeed)
+ {
+ m_callback.OnPlayBackSpeedChanged(speed / DVD_PLAYSPEED_NORMAL);
+ m_processInfo->SeekFinished(0);
+ }
+
+ if (m_pInputStream->IsStreamType(DVDSTREAM_TYPE_PVRMANAGER) && speed != m_playSpeed)
+ {
+ std::shared_ptr<CInputStreamPVRBase> pvrinputstream = std::static_pointer_cast<CInputStreamPVRBase>(m_pInputStream);
+ pvrinputstream->Pause(speed == 0);
+ }
+
+ // do a seek after rewind, clock is not in sync with current pts
+ if ((speed == DVD_PLAYSPEED_NORMAL) &&
+ (m_playSpeed != DVD_PLAYSPEED_NORMAL) &&
+ (m_playSpeed != DVD_PLAYSPEED_PAUSE))
+ {
+ double iTime = m_VideoPlayerVideo->GetCurrentPts();
+ if (iTime == DVD_NOPTS_VALUE)
+ iTime = m_clock.GetClock();
+ iTime = (iTime + m_State.time_offset) / 1000;
+
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = iTime;
+ mode.backward = m_playSpeed < 0;
+ mode.accurate = true;
+ mode.trickplay = true;
+ mode.sync = true;
+ mode.restore = false;
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ }
+
+ if (std::static_pointer_cast<CDVDMsgPlayerSetSpeed>(pMsg)->IsTempo())
+ m_processInfo->SetTempo(static_cast<float>(speed) / DVD_PLAYSPEED_NORMAL);
+ else
+ m_processInfo->SetSpeed(static_cast<float>(speed) / DVD_PLAYSPEED_NORMAL);
+
+ m_processInfo->SetFrameAdvance(false);
+
+ m_playSpeed = speed;
+
+ m_caching = CACHESTATE_DONE;
+ m_clock.SetSpeed(speed);
+ m_VideoPlayerAudio->SetSpeed(speed);
+ m_VideoPlayerVideo->SetSpeed(speed);
+ m_streamPlayerSpeed = speed;
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_FRAME_ADVANCE))
+ {
+ if (m_playSpeed == DVD_PLAYSPEED_PAUSE)
+ {
+ int frames = std::static_pointer_cast<CDVDMsgInt>(pMsg)->m_value;
+ double time = DVD_TIME_BASE / static_cast<double>(m_processInfo->GetVideoFps()) * frames;
+ m_processInfo->SetFrameAdvance(true);
+ m_clock.Advance(time);
+ }
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_GUI_ACTION))
+ OnAction(std::static_pointer_cast<CDVDMsgType<CAction>>(pMsg)->m_value);
+ else if (pMsg->IsType(CDVDMsg::PLAYER_STARTED))
+ {
+ SStartMsg& msg = std::static_pointer_cast<CDVDMsgType<SStartMsg>>(pMsg)->m_value;
+ if (msg.player == VideoPlayer_AUDIO)
+ {
+ m_CurrentAudio.syncState = IDVDStreamPlayer::SYNC_WAITSYNC;
+ m_CurrentAudio.cachetime = msg.cachetime;
+ m_CurrentAudio.cachetotal = msg.cachetotal;
+ m_CurrentAudio.starttime = msg.timestamp;
+ }
+ if (msg.player == VideoPlayer_VIDEO)
+ {
+ m_CurrentVideo.syncState = IDVDStreamPlayer::SYNC_WAITSYNC;
+ m_CurrentVideo.cachetime = msg.cachetime;
+ m_CurrentVideo.cachetotal = msg.cachetotal;
+ m_CurrentVideo.starttime = msg.timestamp;
+ }
+ CLog::Log(LOGDEBUG, "CVideoPlayer::HandleMessages - player started {}", msg.player);
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_REPORT_STATE))
+ {
+ SStateMsg& msg = std::static_pointer_cast<CDVDMsgType<SStateMsg>>(pMsg)->m_value;
+ if (msg.player == VideoPlayer_AUDIO)
+ {
+ m_CurrentAudio.syncState = msg.syncState;
+ }
+ if (msg.player == VideoPlayer_VIDEO)
+ {
+ m_CurrentVideo.syncState = msg.syncState;
+ }
+ CLog::Log(LOGDEBUG, "CVideoPlayer::HandleMessages - player {} reported state: {}", msg.player,
+ msg.syncState);
+ }
+ else if (pMsg->IsType(CDVDMsg::SUBTITLE_ADDFILE))
+ {
+ int id = AddSubtitleFile(std::static_pointer_cast<CDVDMsgType<std::string>>(pMsg)->m_value);
+ if (id >= 0)
+ {
+ SetSubtitle(id);
+ SetSubtitleVisibleInternal(true);
+ }
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_SYNCHRONIZE))
+ {
+ if (std::static_pointer_cast<CDVDMsgGeneralSynchronize>(pMsg)->Wait(100ms, SYNCSOURCE_PLAYER))
+ CLog::Log(LOGDEBUG, "CVideoPlayer - CDVDMsg::GENERAL_SYNCHRONIZE");
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_AVCHANGE))
+ {
+ CServiceBroker::GetDataCacheCore().SignalAudioInfoChange();
+ CServiceBroker::GetDataCacheCore().SignalVideoInfoChange();
+ CServiceBroker::GetDataCacheCore().SignalSubtitleInfoChange();
+ IPlayerCallback *cb = &m_callback;
+ m_outboundEvents->Submit([=]() {
+ cb->OnAVChange();
+ });
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_ABORT))
+ {
+ CLog::Log(LOGDEBUG, "CVideoPlayer - CDVDMsg::PLAYER_ABORT");
+ m_bAbortRequest = true;
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SET_UPDATE_STREAM_DETAILS))
+ m_UpdateStreamDetails = true;
+ }
+}
+
+void CVideoPlayer::SetCaching(ECacheState state)
+{
+ if(state == CACHESTATE_FLUSH)
+ {
+ double level, delay, offset;
+ if(GetCachingTimes(level, delay, offset))
+ state = CACHESTATE_FULL;
+ else
+ state = CACHESTATE_INIT;
+ }
+
+ if(m_caching == state)
+ return;
+
+ CLog::Log(LOGDEBUG, "CVideoPlayer::SetCaching - caching state {}", state);
+ if (state == CACHESTATE_FULL ||
+ state == CACHESTATE_INIT)
+ {
+ m_clock.SetSpeed(DVD_PLAYSPEED_PAUSE);
+
+ m_VideoPlayerAudio->SetSpeed(DVD_PLAYSPEED_PAUSE);
+ m_VideoPlayerVideo->SetSpeed(DVD_PLAYSPEED_PAUSE);
+ m_streamPlayerSpeed = DVD_PLAYSPEED_PAUSE;
+
+ m_cachingTimer.Set(5000ms);
+ }
+
+ if (state == CACHESTATE_PLAY ||
+ (state == CACHESTATE_DONE && m_caching != CACHESTATE_PLAY))
+ {
+ m_clock.SetSpeed(m_playSpeed);
+ m_VideoPlayerAudio->SetSpeed(m_playSpeed);
+ m_VideoPlayerVideo->SetSpeed(m_playSpeed);
+ m_streamPlayerSpeed = m_playSpeed;
+ }
+ m_caching = state;
+
+ m_clock.SetSpeedAdjust(0);
+}
+
+void CVideoPlayer::SetPlaySpeed(int speed)
+{
+ if (IsPlaying())
+ {
+ CDVDMsgPlayerSetSpeed::SpeedParams params = { speed, false };
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSetSpeed>(params));
+ }
+ else
+ {
+ m_playSpeed = speed;
+ m_streamPlayerSpeed = speed;
+ }
+}
+
+bool CVideoPlayer::CanPause() const
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ return m_State.canpause;
+}
+
+void CVideoPlayer::Pause()
+{
+ // toggle between pause and normal speed
+ if (m_processInfo->GetNewSpeed() == 0)
+ {
+ SetSpeed(1);
+ }
+ else
+ {
+ SetSpeed(0);
+ }
+}
+
+bool CVideoPlayer::HasVideo() const
+{
+ return m_HasVideo;
+}
+
+bool CVideoPlayer::HasAudio() const
+{
+ return m_HasAudio;
+}
+
+bool CVideoPlayer::HasRDS() const
+{
+ return m_CurrentRadioRDS.id >= 0;
+}
+
+bool CVideoPlayer::HasID3() const
+{
+ return m_CurrentAudioID3.id >= 0;
+}
+
+bool CVideoPlayer::IsPassthrough() const
+{
+ return m_VideoPlayerAudio->IsPassthrough();
+}
+
+bool CVideoPlayer::CanSeek() const
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ return m_State.canseek;
+}
+
+void CVideoPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride)
+{
+ if (!m_State.canseek)
+ return;
+
+ if (bLargeStep && bChapterOverride && GetChapter() > 0 && GetChapterCount() > 1)
+ {
+ if (!bPlus)
+ {
+ SeekChapter(GetChapter() - 1);
+ return;
+ }
+ else if (GetChapter() < GetChapterCount())
+ {
+ SeekChapter(GetChapter() + 1);
+ return;
+ }
+ }
+
+ int64_t seekTarget;
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+ if (advancedSettings->m_videoUseTimeSeeking && m_processInfo->GetMaxTime() > 2000 * advancedSettings->m_videoTimeSeekForwardBig)
+ {
+ if (bLargeStep)
+ seekTarget = bPlus ? advancedSettings->m_videoTimeSeekForwardBig :
+ advancedSettings->m_videoTimeSeekBackwardBig;
+ else
+ seekTarget = bPlus ? advancedSettings->m_videoTimeSeekForward :
+ advancedSettings->m_videoTimeSeekBackward;
+ seekTarget *= 1000;
+ seekTarget += GetTime();
+ }
+ else
+ {
+ int percent;
+ if (bLargeStep)
+ percent = bPlus ? advancedSettings->m_videoPercentSeekForwardBig : advancedSettings->m_videoPercentSeekBackwardBig;
+ else
+ percent = bPlus ? advancedSettings->m_videoPercentSeekForward : advancedSettings->m_videoPercentSeekBackward;
+ seekTarget = static_cast<int64_t>(m_processInfo->GetMaxTime() * (GetPercentage() + percent) / 100);
+ }
+
+ bool restore = true;
+
+ int64_t time = GetTime();
+ if(g_application.CurrentFileItem().IsStack() &&
+ (seekTarget > m_processInfo->GetMaxTime() || seekTarget < 0))
+ {
+ g_application.SeekTime((seekTarget - time) * 0.001 + g_application.GetTime());
+ // warning, don't access any VideoPlayer variables here as
+ // the VideoPlayer object may have been destroyed
+ return;
+ }
+
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = (int)seekTarget;
+ mode.backward = !bPlus;
+ mode.accurate = false;
+ mode.restore = restore;
+ mode.trickplay = false;
+ mode.sync = true;
+
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ SynchronizeDemuxer();
+ if (seekTarget < 0)
+ seekTarget = 0;
+ m_callback.OnPlayBackSeek(seekTarget, seekTarget - time);
+}
+
+bool CVideoPlayer::SeekScene(bool bPlus)
+{
+ if (!m_Edl.HasSceneMarker())
+ return false;
+
+ /*
+ * There is a 5 second grace period applied when seeking for scenes backwards. If there is no
+ * grace period applied it is impossible to go backwards past a scene marker.
+ */
+ int64_t clock = GetTime();
+ if (!bPlus && clock > 5 * 1000) // 5 seconds
+ clock -= 5 * 1000;
+
+ int iScenemarker;
+ if (m_Edl.GetNextSceneMarker(bPlus, clock, &iScenemarker))
+ {
+ /*
+ * Seeking is flushed and inaccurate, just like Seek()
+ */
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = iScenemarker;
+ mode.backward = !bPlus;
+ mode.accurate = false;
+ mode.restore = false;
+ mode.trickplay = false;
+ mode.sync = true;
+
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ SynchronizeDemuxer();
+ return true;
+ }
+ return false;
+}
+
+void CVideoPlayer::GetGeneralInfo(std::string& strGeneralInfo)
+{
+ if (!m_bStop)
+ {
+ double apts = m_VideoPlayerAudio->GetCurrentPts();
+ double vpts = m_VideoPlayerVideo->GetCurrentPts();
+ double dDiff = 0;
+
+ if (apts != DVD_NOPTS_VALUE && vpts != DVD_NOPTS_VALUE)
+ dDiff = (apts - vpts) / DVD_TIME_BASE;
+
+ std::string strBuf;
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ if (m_State.cache_bytes >= 0)
+ {
+ strBuf += StringUtils::Format(" forward:{} {:2.0f}%",
+ StringUtils::SizeToString(m_State.cache_bytes),
+ m_State.cache_level * 100);
+ if (m_playSpeed == 0 || m_caching == CACHESTATE_FULL)
+ strBuf += StringUtils::Format(" {} msec", DVD_TIME_TO_MSEC(m_State.cache_delay));
+ }
+
+ strGeneralInfo = StringUtils::Format("Player: a/v:{: 6.3f}, {}", dDiff, strBuf);
+ }
+}
+
+void CVideoPlayer::SeekPercentage(float iPercent)
+{
+ int64_t iTotalTime = m_processInfo->GetMaxTime();
+
+ if (!iTotalTime)
+ return;
+
+ SeekTime((int64_t)(iTotalTime * iPercent / 100));
+}
+
+float CVideoPlayer::GetPercentage()
+{
+ int64_t iTotalTime = m_processInfo->GetMaxTime();
+
+ if (!iTotalTime)
+ return 0.0f;
+
+ return GetTime() * 100 / (float)iTotalTime;
+}
+
+float CVideoPlayer::GetCachePercentage() const
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ return (float) (m_State.cache_offset * 100); // NOTE: Percentage returned is relative
+}
+
+void CVideoPlayer::SetAVDelay(float fValue)
+{
+ m_processInfo->GetVideoSettingsLocked().SetAudioDelay(fValue);
+ m_renderManager.SetDelay(static_cast<int>(fValue * 1000.0f));
+}
+
+float CVideoPlayer::GetAVDelay()
+{
+ return static_cast<float>(m_renderManager.GetDelay()) / 1000.0f;
+}
+
+void CVideoPlayer::SetSubTitleDelay(float fValue)
+{
+ m_processInfo->GetVideoSettingsLocked().SetSubtitleDelay(fValue);
+ m_VideoPlayerVideo->SetSubtitleDelay(static_cast<double>(-fValue) * DVD_TIME_BASE);
+}
+
+float CVideoPlayer::GetSubTitleDelay()
+{
+ return (float) -m_VideoPlayerVideo->GetSubtitleDelay() / DVD_TIME_BASE;
+}
+
+bool CVideoPlayer::GetSubtitleVisible() const
+{
+ if (m_pInputStream && m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ {
+ std::shared_ptr<CDVDInputStreamNavigator> pStream = std::static_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream);
+ return pStream->IsSubtitleStreamEnabled();
+ }
+
+ return m_VideoPlayerVideo->IsSubtitleEnabled();
+}
+
+void CVideoPlayer::SetSubtitleVisible(bool bVisible)
+{
+ m_messenger.Put(
+ std::make_shared<CDVDMsgBool>(CDVDMsg::PLAYER_SET_SUBTITLESTREAM_VISIBLE, bVisible));
+ m_processInfo->GetVideoSettingsLocked().SetSubtitleVisible(bVisible);
+}
+
+void CVideoPlayer::SetSubtitleVisibleInternal(bool bVisible)
+{
+ m_VideoPlayerVideo->EnableSubtitle(bVisible);
+
+ if (m_pInputStream && m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ std::static_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream)->EnableSubtitleStream(bVisible);
+
+ CServiceBroker::GetDataCacheCore().SignalSubtitleInfoChange();
+}
+
+void CVideoPlayer::SetSubtitleVerticalPosition(int value, bool save)
+{
+ m_processInfo->GetVideoSettingsLocked().SetSubtitleVerticalPosition(value, save);
+ m_renderManager.SetSubtitleVerticalPosition(value, save);
+}
+
+std::shared_ptr<TextCacheStruct_t> CVideoPlayer::GetTeletextCache()
+{
+ if (m_CurrentTeletext.id < 0)
+ return nullptr;
+
+ return m_VideoPlayerTeletext->GetTeletextCache();
+}
+
+bool CVideoPlayer::HasTeletextCache() const
+{
+ return m_CurrentTeletext.id >= 0;
+}
+
+void CVideoPlayer::LoadPage(int p, int sp, unsigned char* buffer)
+{
+ if (m_CurrentTeletext.id < 0)
+ return;
+
+ return m_VideoPlayerTeletext->LoadPage(p, sp, buffer);
+}
+
+void CVideoPlayer::SeekTime(int64_t iTime)
+{
+ int64_t seekOffset = iTime - GetTime();
+
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = static_cast<double>(iTime);
+ mode.backward = true;
+ mode.accurate = true;
+ mode.trickplay = false;
+ mode.sync = true;
+
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ SynchronizeDemuxer();
+ m_callback.OnPlayBackSeek(iTime, seekOffset);
+ m_processInfo->SeekFinished(seekOffset);
+}
+
+bool CVideoPlayer::SeekTimeRelative(int64_t iTime)
+{
+ int64_t abstime = GetTime() + iTime;
+
+ // if the file has EDL cuts we can't rely on m_clock for relative seeks
+ // EDL cuts remove time from the original file, hence we might seek to
+ // positions too far from the current m_clock position. Seek to absolute
+ // time instead
+ if (m_Edl.HasCuts())
+ {
+ SeekTime(abstime);
+ return true;
+ }
+
+ CDVDMsgPlayerSeek::CMode mode;
+ mode.time = (int)iTime;
+ mode.relative = true;
+ mode.backward = (iTime < 0) ? true : false;
+ mode.accurate = false;
+ mode.trickplay = false;
+ mode.sync = true;
+
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeek>(mode));
+ m_processInfo->SetStateSeeking(true);
+
+ m_callback.OnPlayBackSeek(abstime, iTime);
+ m_processInfo->SeekFinished(iTime);
+ return true;
+}
+
+// return the time in milliseconds
+int64_t CVideoPlayer::GetTime()
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ return llrint(m_State.time);
+}
+
+void CVideoPlayer::SetSpeed(float speed)
+{
+ // can't rewind in menu as seeking isn't possible
+ // forward is fine
+ if (speed < 0 && IsInMenu())
+ return;
+
+ if (!CanSeek() && !CanPause())
+ return;
+
+ int iSpeed = static_cast<int>(speed * DVD_PLAYSPEED_NORMAL);
+
+ if (!CanSeek())
+ {
+ if ((iSpeed != DVD_PLAYSPEED_NORMAL) && (iSpeed != DVD_PLAYSPEED_PAUSE))
+ return;
+ }
+
+ float currentSpeed = m_processInfo->GetNewSpeed();
+ m_processInfo->SetNewSpeed(speed);
+ if (iSpeed != currentSpeed)
+ {
+ if (iSpeed == DVD_PLAYSPEED_NORMAL)
+ m_callback.OnPlayBackResumed();
+ else if (iSpeed == DVD_PLAYSPEED_PAUSE)
+ m_callback.OnPlayBackPaused();
+
+ if (iSpeed == DVD_PLAYSPEED_NORMAL)
+ {
+ float currentTempo = m_processInfo->GetNewTempo();
+ if (currentTempo != 1.0f)
+ {
+ SetTempo(currentTempo);
+ return;
+ }
+ }
+ SetPlaySpeed(iSpeed);
+ }
+}
+
+void CVideoPlayer::SetTempo(float tempo)
+{
+ tempo = floor(tempo * 100.0f + 0.5f) / 100.0f;
+ if (m_processInfo->IsTempoAllowed(tempo))
+ {
+ int speed = tempo * DVD_PLAYSPEED_NORMAL;
+ CDVDMsgPlayerSetSpeed::SpeedParams params = { speed, true };
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSetSpeed>(params));
+
+ m_processInfo->SetNewTempo(tempo);
+ }
+}
+
+void CVideoPlayer::FrameAdvance(int frames)
+{
+ float currentSpeed = m_processInfo->GetNewSpeed();
+ if (currentSpeed != DVD_PLAYSPEED_PAUSE)
+ return;
+
+ m_messenger.Put(std::make_shared<CDVDMsgInt>(CDVDMsg::PLAYER_FRAME_ADVANCE, frames));
+}
+
+bool CVideoPlayer::SupportsTempo() const
+{
+ return m_State.cantempo;
+}
+
+bool CVideoPlayer::OpenStream(CCurrentStream& current, int64_t demuxerId, int iStream, int source, bool reset /*= true*/)
+{
+ CDemuxStream* stream = NULL;
+ CDVDStreamInfo hint;
+
+ CLog::Log(LOGINFO, "Opening stream: {} source: {}", iStream, source);
+
+ if(STREAM_SOURCE_MASK(source) == STREAM_SOURCE_DEMUX_SUB)
+ {
+ int index = m_SelectionStreams.TypeIndexOf(current.type, source, demuxerId, iStream);
+ if (index < 0)
+ return false;
+ const SelectionStream& st = m_SelectionStreams.Get(current.type, index);
+
+ CLog::Log(LOGINFO, "Opening Subtitle file: {}", CURL::GetRedacted(st.filename));
+ m_pSubtitleDemuxer.reset();
+ const auto demux = m_subtitleDemuxerMap.find(demuxerId);
+ if (demux == m_subtitleDemuxerMap.end())
+ {
+ CLog::Log(LOGINFO, "No demuxer found for file {}", CURL::GetRedacted(st.filename));
+ return false;
+ }
+
+ m_pSubtitleDemuxer = demux->second;
+
+ double pts = m_VideoPlayerVideo->GetCurrentPts();
+ if(pts == DVD_NOPTS_VALUE)
+ pts = m_CurrentVideo.dts;
+ if(pts == DVD_NOPTS_VALUE)
+ pts = 0;
+ pts += m_offset_pts;
+ if (!m_pSubtitleDemuxer->SeekTime((int)(1000.0 * pts / (double)DVD_TIME_BASE)))
+ CLog::Log(LOGDEBUG, "{} - failed to start subtitle demuxing from: {:f}", __FUNCTION__, pts);
+ stream = m_pSubtitleDemuxer->GetStream(demuxerId, iStream);
+ if(!stream || stream->disabled)
+ return false;
+
+ m_pSubtitleDemuxer->EnableStream(demuxerId, iStream, true);
+
+ hint.Assign(*stream, true);
+ }
+ else if(STREAM_SOURCE_MASK(source) == STREAM_SOURCE_TEXT)
+ {
+ int index = m_SelectionStreams.TypeIndexOf(current.type, source, demuxerId, iStream);
+ if(index < 0)
+ return false;
+
+ hint.Clear();
+ hint.filename = m_SelectionStreams.Get(current.type, index).filename;
+ hint.fpsscale = m_CurrentVideo.hint.fpsscale;
+ hint.fpsrate = m_CurrentVideo.hint.fpsrate;
+ }
+ else if(STREAM_SOURCE_MASK(source) == STREAM_SOURCE_DEMUX)
+ {
+ if(!m_pDemuxer)
+ return false;
+
+ m_pDemuxer->OpenStream(demuxerId, iStream);
+
+ stream = m_pDemuxer->GetStream(demuxerId, iStream);
+ if (!stream || stream->disabled)
+ return false;
+
+ hint.Assign(*stream, true);
+
+ if(m_pInputStream && m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ hint.filename = "dvd";
+ }
+ else if(STREAM_SOURCE_MASK(source) == STREAM_SOURCE_VIDEOMUX)
+ {
+ if(!m_pCCDemuxer)
+ return false;
+
+ stream = m_pCCDemuxer->GetStream(iStream);
+ if(!stream || stream->disabled)
+ return false;
+
+ hint.Assign(*stream, false);
+ }
+
+ bool res;
+ switch(current.type)
+ {
+ case STREAM_AUDIO:
+ res = OpenAudioStream(hint, reset);
+ break;
+ case STREAM_VIDEO:
+ res = OpenVideoStream(hint, reset);
+ break;
+ case STREAM_SUBTITLE:
+ res = OpenSubtitleStream(hint);
+ break;
+ case STREAM_TELETEXT:
+ res = OpenTeletextStream(hint);
+ break;
+ case STREAM_RADIO_RDS:
+ res = OpenRadioRDSStream(hint);
+ break;
+ case STREAM_AUDIO_ID3:
+ res = OpenAudioID3Stream(hint);
+ break;
+ default:
+ res = false;
+ break;
+ }
+
+ if (res)
+ {
+ int oldId = current.id;
+ current.id = iStream;
+ current.demuxerId = demuxerId;
+ current.source = source;
+ current.hint = hint;
+ current.stream = (void*)stream;
+ current.lastdts = DVD_NOPTS_VALUE;
+ if (oldId >= 0 && current.avsync != CCurrentStream::AV_SYNC_FORCE)
+ current.avsync = CCurrentStream::AV_SYNC_CHECK;
+ if(stream)
+ current.changes = stream->changes;
+ }
+ else
+ {
+ if(stream)
+ {
+ /* mark stream as disabled, to disallow further attempts*/
+ CLog::Log(LOGWARNING, "{} - Unsupported stream {}. Stream disabled.", __FUNCTION__,
+ stream->uniqueId);
+ stream->disabled = true;
+ }
+ }
+
+ UpdateContentState();
+ CServiceBroker::GetDataCacheCore().SignalAudioInfoChange();
+ CServiceBroker::GetDataCacheCore().SignalVideoInfoChange();
+ CServiceBroker::GetDataCacheCore().SignalSubtitleInfoChange();
+
+ return res;
+}
+
+bool CVideoPlayer::OpenAudioStream(CDVDStreamInfo& hint, bool reset)
+{
+ IDVDStreamPlayer* player = GetStreamPlayer(m_CurrentAudio.player);
+ if(player == nullptr)
+ return false;
+
+ if(m_CurrentAudio.id < 0 ||
+ m_CurrentAudio.hint != hint)
+ {
+ if (!player->OpenStream(hint))
+ return false;
+
+ player->SendMessage(std::make_shared<CDVDMsgBool>(CDVDMsg::GENERAL_PAUSE, m_displayLost), 1);
+
+ static_cast<IDVDStreamPlayerAudio*>(player)->SetSpeed(m_streamPlayerSpeed);
+ m_CurrentAudio.syncState = IDVDStreamPlayer::SYNC_STARTING;
+ m_CurrentAudio.packets = 0;
+ }
+ else if (reset)
+ player->SendMessage(std::make_shared<CDVDMsg>(CDVDMsg::GENERAL_RESET), 0);
+
+ m_HasAudio = true;
+
+ static_cast<IDVDStreamPlayerAudio*>(player)->SendMessage(
+ std::make_shared<CDVDMsg>(CDVDMsg::PLAYER_REQUEST_STATE), 1);
+
+ return true;
+}
+
+bool CVideoPlayer::OpenVideoStream(CDVDStreamInfo& hint, bool reset)
+{
+ if (m_pInputStream && m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ {
+ /* set aspect ratio as requested by navigator for dvd's */
+ float aspect = std::static_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream)->GetVideoAspectRatio();
+ if (aspect != 0.0f)
+ {
+ hint.aspect = static_cast<double>(aspect);
+ hint.forced_aspect = true;
+ }
+ hint.dvd = true;
+ }
+ else if (m_pInputStream && m_pInputStream->IsStreamType(DVDSTREAM_TYPE_PVRMANAGER))
+ {
+ // set framerate if not set by demuxer
+ if (hint.fpsrate == 0 || hint.fpsscale == 0)
+ {
+ int fpsidx = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_PVRPLAYBACK_FPS);
+ if (fpsidx == 1)
+ {
+ hint.fpsscale = 1000;
+ hint.fpsrate = 50000;
+ }
+ else if (fpsidx == 2)
+ {
+ hint.fpsscale = 1001;
+ hint.fpsrate = 60000;
+ }
+ }
+ }
+
+ std::shared_ptr<CDVDInputStream::IMenus> pMenus = std::dynamic_pointer_cast<CDVDInputStream::IMenus>(m_pInputStream);
+ if(pMenus && pMenus->IsInMenu())
+ hint.stills = true;
+
+ if (hint.stereo_mode.empty())
+ {
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui != nullptr)
+ {
+ const CStereoscopicsManager &stereoscopicsManager = gui->GetStereoscopicsManager();
+ hint.stereo_mode = stereoscopicsManager.DetectStereoModeByString(m_item.GetPath());
+ }
+ }
+
+ if (hint.flags & AV_DISPOSITION_ATTACHED_PIC)
+ return false;
+
+ // set desired refresh rate
+ if (m_CurrentVideo.id < 0 && m_playerOptions.fullscreen &&
+ CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot() && hint.fpsrate != 0 &&
+ hint.fpsscale != 0)
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE) != ADJUST_REFRESHRATE_OFF)
+ {
+ double framerate = DVD_TIME_BASE / CDVDCodecUtils::NormalizeFrameduration((double)DVD_TIME_BASE * hint.fpsscale / hint.fpsrate);
+ RESOLUTION res = CResolutionUtils::ChooseBestResolution(static_cast<float>(framerate), hint.width, hint.height, !hint.stereo_mode.empty());
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(res, false);
+ m_renderManager.TriggerUpdateResolution(framerate, hint.width, hint.height, hint.stereo_mode);
+ }
+ }
+
+ IDVDStreamPlayer* player = GetStreamPlayer(m_CurrentVideo.player);
+ if(player == nullptr)
+ return false;
+
+ if(m_CurrentVideo.id < 0 ||
+ m_CurrentVideo.hint != hint)
+ {
+ if (hint.codec == AV_CODEC_ID_MPEG2VIDEO || hint.codec == AV_CODEC_ID_H264)
+ m_pCCDemuxer.reset();
+
+ if (!player->OpenStream(hint))
+ return false;
+
+ player->SendMessage(std::make_shared<CDVDMsgBool>(CDVDMsg::GENERAL_PAUSE, m_displayLost), 1);
+
+ // look for any EDL files
+ m_Edl.Clear();
+ float fFramesPerSecond = 0.0f;
+ if (m_CurrentVideo.hint.fpsscale > 0.0f)
+ fFramesPerSecond = static_cast<float>(m_CurrentVideo.hint.fpsrate) / static_cast<float>(m_CurrentVideo.hint.fpsscale);
+ m_Edl.ReadEditDecisionLists(m_item, fFramesPerSecond);
+ CServiceBroker::GetDataCacheCore().SetEditList(m_Edl.GetEditList());
+ CServiceBroker::GetDataCacheCore().SetCuts(m_Edl.GetCutMarkers());
+ CServiceBroker::GetDataCacheCore().SetSceneMarkers(m_Edl.GetSceneMarkers());
+
+ static_cast<IDVDStreamPlayerVideo*>(player)->SetSpeed(m_streamPlayerSpeed);
+ m_CurrentVideo.syncState = IDVDStreamPlayer::SYNC_STARTING;
+ m_CurrentVideo.packets = 0;
+ }
+ else if (reset)
+ player->SendMessage(std::make_shared<CDVDMsg>(CDVDMsg::GENERAL_RESET), 0);
+
+ m_HasVideo = true;
+
+ static_cast<IDVDStreamPlayerVideo*>(player)->SendMessage(
+ std::make_shared<CDVDMsg>(CDVDMsg::PLAYER_REQUEST_STATE), 1);
+
+ // open CC demuxer if video is mpeg2
+ if ((hint.codec == AV_CODEC_ID_MPEG2VIDEO || hint.codec == AV_CODEC_ID_H264) && !m_pCCDemuxer)
+ {
+ m_pCCDemuxer = std::make_unique<CDVDDemuxCC>(hint.codec);
+ m_SelectionStreams.Clear(STREAM_NONE, STREAM_SOURCE_VIDEOMUX);
+ }
+
+ return true;
+}
+
+bool CVideoPlayer::OpenSubtitleStream(const CDVDStreamInfo& hint)
+{
+ IDVDStreamPlayer* player = GetStreamPlayer(m_CurrentSubtitle.player);
+ if(player == nullptr)
+ return false;
+
+ if(m_CurrentSubtitle.id < 0 ||
+ m_CurrentSubtitle.hint != hint)
+ {
+ if (!player->OpenStream(hint))
+ return false;
+ }
+
+ return true;
+}
+
+void CVideoPlayer::AdaptForcedSubtitles()
+{
+ SelectionStream ss = m_SelectionStreams.Get(STREAM_SUBTITLE, GetSubtitle());
+ if (ss.flags & StreamFlags::FLAG_FORCED)
+ {
+ SelectionStream as = m_SelectionStreams.Get(STREAM_AUDIO, GetAudioStream());
+ bool found = false;
+ for (const auto &stream : m_SelectionStreams.Get(STREAM_SUBTITLE))
+ {
+ if (stream.flags & StreamFlags::FLAG_FORCED && g_LangCodeExpander.CompareISO639Codes(stream.language, as.language))
+ {
+ if (OpenStream(m_CurrentSubtitle, stream.demuxerId, stream.id, stream.source))
+ {
+ found = true;
+ SetSubtitleVisibleInternal(true);
+ break;
+ }
+ }
+ }
+ if (!found)
+ {
+ SetSubtitleVisibleInternal(false);
+ }
+ }
+}
+
+bool CVideoPlayer::OpenTeletextStream(CDVDStreamInfo& hint)
+{
+ if (!m_VideoPlayerTeletext->CheckStream(hint))
+ return false;
+
+ IDVDStreamPlayer* player = GetStreamPlayer(m_CurrentTeletext.player);
+ if(player == nullptr)
+ return false;
+
+ if(m_CurrentTeletext.id < 0 ||
+ m_CurrentTeletext.hint != hint)
+ {
+ if (!player->OpenStream(hint))
+ return false;
+ }
+
+ return true;
+}
+
+bool CVideoPlayer::OpenRadioRDSStream(CDVDStreamInfo& hint)
+{
+ if (!m_VideoPlayerRadioRDS->CheckStream(hint))
+ return false;
+
+ IDVDStreamPlayer* player = GetStreamPlayer(m_CurrentRadioRDS.player);
+ if(player == nullptr)
+ return false;
+
+ if(m_CurrentRadioRDS.id < 0 ||
+ m_CurrentRadioRDS.hint != hint)
+ {
+ if (!player->OpenStream(hint))
+ return false;
+ }
+
+ return true;
+}
+
+bool CVideoPlayer::OpenAudioID3Stream(CDVDStreamInfo& hint)
+{
+ if (!m_VideoPlayerAudioID3->CheckStream(hint))
+ return false;
+
+ IDVDStreamPlayer* player = GetStreamPlayer(m_CurrentAudioID3.player);
+ if (player == nullptr)
+ return false;
+
+ if (m_CurrentAudioID3.id < 0 || m_CurrentAudioID3.hint != hint)
+ {
+ if (!player->OpenStream(hint))
+ return false;
+ }
+
+ return true;
+}
+
+bool CVideoPlayer::CloseStream(CCurrentStream& current, bool bWaitForBuffers)
+{
+ if (current.id < 0)
+ return false;
+
+ CLog::Log(LOGINFO, "Closing stream player {}", current.player);
+
+ if(bWaitForBuffers)
+ SetCaching(CACHESTATE_DONE);
+
+ if (m_pDemuxer && STREAM_SOURCE_MASK(current.source) == STREAM_SOURCE_DEMUX)
+ m_pDemuxer->EnableStream(current.demuxerId, current.id, false);
+
+ IDVDStreamPlayer* player = GetStreamPlayer(current.player);
+ if (player)
+ {
+ if ((current.type == STREAM_AUDIO && current.syncState != IDVDStreamPlayer::SYNC_INSYNC) ||
+ (current.type == STREAM_VIDEO && current.syncState != IDVDStreamPlayer::SYNC_INSYNC) ||
+ m_bAbortRequest)
+ bWaitForBuffers = false;
+ player->CloseStream(bWaitForBuffers);
+ }
+
+ current.Clear();
+ return true;
+}
+
+void CVideoPlayer::FlushBuffers(double pts, bool accurate, bool sync)
+{
+ CLog::Log(LOGDEBUG, "CVideoPlayer::FlushBuffers - flushing buffers");
+
+ double startpts;
+ if (accurate)
+ startpts = pts;
+ else
+ startpts = DVD_NOPTS_VALUE;
+
+ if (sync)
+ {
+ m_CurrentAudio.inited = false;
+ m_CurrentAudio.avsync = CCurrentStream::AV_SYNC_FORCE;
+ m_CurrentVideo.inited = false;
+ m_CurrentVideo.avsync = CCurrentStream::AV_SYNC_FORCE;
+ m_CurrentSubtitle.inited = false;
+ m_CurrentTeletext.inited = false;
+ m_CurrentRadioRDS.inited = false;
+ }
+
+ m_CurrentAudio.dts = DVD_NOPTS_VALUE;
+ m_CurrentAudio.startpts = startpts;
+ m_CurrentAudio.packets = 0;
+
+ m_CurrentVideo.dts = DVD_NOPTS_VALUE;
+ m_CurrentVideo.startpts = startpts;
+ m_CurrentVideo.packets = 0;
+
+ m_CurrentSubtitle.dts = DVD_NOPTS_VALUE;
+ m_CurrentSubtitle.startpts = startpts;
+ m_CurrentSubtitle.packets = 0;
+
+ m_CurrentTeletext.dts = DVD_NOPTS_VALUE;
+ m_CurrentTeletext.startpts = startpts;
+ m_CurrentTeletext.packets = 0;
+
+ m_CurrentRadioRDS.dts = DVD_NOPTS_VALUE;
+ m_CurrentRadioRDS.startpts = startpts;
+ m_CurrentRadioRDS.packets = 0;
+
+ m_CurrentAudioID3.dts = DVD_NOPTS_VALUE;
+ m_CurrentAudioID3.startpts = startpts;
+ m_CurrentAudioID3.packets = 0;
+
+ m_VideoPlayerAudio->Flush(sync);
+ m_VideoPlayerVideo->Flush(sync);
+ m_VideoPlayerSubtitle->Flush();
+ m_VideoPlayerTeletext->Flush();
+ m_VideoPlayerRadioRDS->Flush();
+ m_VideoPlayerAudioID3->Flush();
+
+ if (m_playSpeed == DVD_PLAYSPEED_NORMAL ||
+ m_playSpeed == DVD_PLAYSPEED_PAUSE)
+ {
+ // make sure players are properly flushed, should put them in stalled state
+ auto msg = std::make_shared<CDVDMsgGeneralSynchronize>(1s, SYNCSOURCE_AUDIO | SYNCSOURCE_VIDEO);
+ m_VideoPlayerAudio->SendMessage(msg, 1);
+ m_VideoPlayerVideo->SendMessage(msg, 1);
+ msg->Wait(m_bStop, 0);
+
+ // purge any pending PLAYER_STARTED messages
+ m_messenger.Flush(CDVDMsg::PLAYER_STARTED);
+
+ // we should now wait for init cache
+ SetCaching(CACHESTATE_FLUSH);
+ if (sync)
+ {
+ m_CurrentAudio.syncState = IDVDStreamPlayer::SYNC_STARTING;
+ m_CurrentVideo.syncState = IDVDStreamPlayer::SYNC_STARTING;
+ }
+ }
+
+ if(pts != DVD_NOPTS_VALUE && sync)
+ m_clock.Discontinuity(pts);
+ UpdatePlayState(0);
+
+ m_demuxerSpeed = DVD_PLAYSPEED_NORMAL;
+ if (m_pDemuxer)
+ m_pDemuxer->SetSpeed(DVD_PLAYSPEED_NORMAL);
+}
+
+// since we call ffmpeg functions to decode, this is being called in the same thread as ::Process() is
+int CVideoPlayer::OnDiscNavResult(void* pData, int iMessage)
+{
+ if (!m_pInputStream)
+ return 0;
+
+#if defined(HAVE_LIBBLURAY)
+ if (m_pInputStream->IsStreamType(DVDSTREAM_TYPE_BLURAY))
+ {
+ switch (iMessage)
+ {
+ case BD_EVENT_MENU_OVERLAY:
+ m_overlayContainer.ProcessAndAddOverlayIfValid(static_cast<CDVDOverlay*>(pData));
+ break;
+ case BD_EVENT_PLAYLIST_STOP:
+ m_dvd.state = DVDSTATE_NORMAL;
+ m_dvd.iDVDStillTime = 0ms;
+ m_messenger.Put(std::make_shared<CDVDMsg>(CDVDMsg::GENERAL_FLUSH));
+ break;
+ case BD_EVENT_AUDIO_STREAM:
+ m_dvd.iSelectedAudioStream = *static_cast<int*>(pData);
+ break;
+
+ case BD_EVENT_PG_TEXTST_STREAM:
+ m_dvd.iSelectedSPUStream = *static_cast<int*>(pData);
+ break;
+ case BD_EVENT_PG_TEXTST:
+ {
+ bool enable = (*static_cast<int*>(pData) != 0);
+ m_VideoPlayerVideo->EnableSubtitle(enable);
+ }
+ break;
+ case BD_EVENT_STILL_TIME:
+ {
+ if (m_dvd.state != DVDSTATE_STILL)
+ {
+ // else notify the player we have received a still frame
+
+ m_dvd.iDVDStillTime = std::chrono::milliseconds(*static_cast<int*>(pData));
+ m_dvd.iDVDStillStartTime = std::chrono::steady_clock::now();
+
+ if (m_dvd.iDVDStillTime > 0ms)
+ m_dvd.iDVDStillTime *= 1000;
+
+ /* adjust for the output delay in the video queue */
+ std::chrono::milliseconds time = 0ms;
+ if (m_CurrentVideo.stream && m_dvd.iDVDStillTime > 0ms)
+ {
+ time = std::chrono::milliseconds(
+ static_cast<int>(m_VideoPlayerVideo->GetOutputDelay() / (DVD_TIME_BASE / 1000)));
+ if (time < 10000ms && time > 0ms)
+ m_dvd.iDVDStillTime += time;
+ }
+ m_dvd.state = DVDSTATE_STILL;
+ CLog::Log(LOGDEBUG, "BD_EVENT_STILL_TIME - waiting {} msec, with delay of {} msec",
+ m_dvd.iDVDStillTime.count(), time.count());
+ }
+ }
+ break;
+ case BD_EVENT_STILL:
+ {
+ bool on = static_cast<bool>(*static_cast<int*>(pData));
+ if (on && m_dvd.state != DVDSTATE_STILL)
+ {
+ m_dvd.state = DVDSTATE_STILL;
+ m_dvd.iDVDStillStartTime = std::chrono::steady_clock::now();
+ m_dvd.iDVDStillTime = 0ms;
+ CLog::Log(LOGDEBUG, "CDVDPlayer::OnDVDNavResult - libbluray DVDSTATE_STILL start");
+ }
+ else if (!on && m_dvd.state == DVDSTATE_STILL)
+ {
+ m_dvd.state = DVDSTATE_NORMAL;
+ m_dvd.iDVDStillStartTime = {};
+ m_dvd.iDVDStillTime = 0ms;
+ CLog::Log(LOGDEBUG, "CDVDPlayer::OnDVDNavResult - libbluray DVDSTATE_STILL end");
+ }
+ }
+ break;
+ case BD_EVENT_MENU_ERROR:
+ {
+ m_dvd.state = DVDSTATE_NORMAL;
+ CLog::Log(LOGDEBUG, "CVideoPlayer::OnDiscNavResult - libbluray menu not supported (DVDSTATE_NORMAL)");
+ CGUIDialogKaiToast::QueueNotification(g_localizeStrings.Get(25008), g_localizeStrings.Get(25009));
+ }
+ break;
+ case BD_EVENT_ENC_ERROR:
+ {
+ m_dvd.state = DVDSTATE_NORMAL;
+ CLog::Log(LOGDEBUG, "CVideoPlayer::OnDiscNavResult - libbluray the disc/file is encrypted and can't be played (DVDSTATE_NORMAL)");
+ CGUIDialogKaiToast::QueueNotification(g_localizeStrings.Get(16026), g_localizeStrings.Get(29805));
+ }
+ break;
+ default:
+ break;
+ }
+
+ return 0;
+ }
+#endif
+
+ if (m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ {
+ std::shared_ptr<CDVDInputStreamNavigator> pStream = std::static_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream);
+
+ switch (iMessage)
+ {
+ case DVDNAV_STILL_FRAME:
+ {
+ //CLog::Log(LOGDEBUG, "DVDNAV_STILL_FRAME");
+
+ dvdnav_still_event_t *still_event = static_cast<dvdnav_still_event_t*>(pData);
+ // should wait the specified time here while we let the player running
+ // after that call dvdnav_still_skip(m_dvdnav);
+
+ if (m_dvd.state != DVDSTATE_STILL)
+ {
+ // else notify the player we have received a still frame
+
+ if(still_event->length < 0xff)
+ m_dvd.iDVDStillTime = std::chrono::seconds(still_event->length);
+ else
+ m_dvd.iDVDStillTime = 0ms;
+
+ m_dvd.iDVDStillStartTime = std::chrono::steady_clock::now();
+
+ /* adjust for the output delay in the video queue */
+ std::chrono::milliseconds time = 0ms;
+ if (m_CurrentVideo.stream && m_dvd.iDVDStillTime > 0ms)
+ {
+ time = std::chrono::milliseconds(
+ static_cast<int>(m_VideoPlayerVideo->GetOutputDelay() / (DVD_TIME_BASE / 1000)));
+ if (time < 10000ms && time > 0ms)
+ m_dvd.iDVDStillTime += time;
+ }
+ m_dvd.state = DVDSTATE_STILL;
+ CLog::Log(LOGDEBUG, "DVDNAV_STILL_FRAME - waiting {} sec, with delay of {} msec",
+ still_event->length, time.count());
+ }
+ return NAVRESULT_HOLD;
+ }
+ break;
+ case DVDNAV_SPU_CLUT_CHANGE:
+ {
+ m_VideoPlayerSubtitle->SendMessage(
+ std::make_shared<CDVDMsgSubtitleClutChange>((uint8_t*)pData));
+ }
+ break;
+ case DVDNAV_SPU_STREAM_CHANGE:
+ {
+ dvdnav_spu_stream_change_event_t* event = static_cast<dvdnav_spu_stream_change_event_t*>(pData);
+
+ int iStream = event->physical_wide;
+ bool visible = !(iStream & 0x80);
+
+ SetSubtitleVisibleInternal(visible);
+
+ if (iStream >= 0)
+ m_dvd.iSelectedSPUStream = (iStream & ~0x80);
+ else
+ m_dvd.iSelectedSPUStream = -1;
+
+ m_CurrentSubtitle.stream = NULL;
+ }
+ break;
+ case DVDNAV_AUDIO_STREAM_CHANGE:
+ {
+ dvdnav_audio_stream_change_event_t* event = static_cast<dvdnav_audio_stream_change_event_t*>(pData);
+ // Tell system what audiostream should be opened by default
+ m_dvd.iSelectedAudioStream = event->physical;
+ m_CurrentAudio.stream = NULL;
+ }
+ break;
+ case DVDNAV_HIGHLIGHT:
+ {
+ //dvdnav_highlight_event_t* pInfo = (dvdnav_highlight_event_t*)pData;
+ int iButton = pStream->GetCurrentButton();
+ CLog::Log(LOGDEBUG, "DVDNAV_HIGHLIGHT: Highlight button {}", iButton);
+ m_VideoPlayerSubtitle->UpdateOverlayInfo(std::static_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream), LIBDVDNAV_BUTTON_NORMAL);
+ }
+ break;
+ case DVDNAV_VTS_CHANGE:
+ {
+ //dvdnav_vts_change_event_t* vts_change_event = (dvdnav_vts_change_event_t*)pData;
+ CLog::Log(LOGDEBUG, "DVDNAV_VTS_CHANGE");
+
+ //Make sure we clear all the old overlays here, or else old forced items are left.
+ m_overlayContainer.Clear();
+
+ //Force an aspect ratio that is set in the dvdheaders if available
+ m_CurrentVideo.hint.aspect = static_cast<double>(pStream->GetVideoAspectRatio());
+ if( m_VideoPlayerVideo->IsInited() )
+ m_VideoPlayerVideo->SendMessage(std::make_shared<CDVDMsgDouble>(
+ CDVDMsg::VIDEO_SET_ASPECT, m_CurrentVideo.hint.aspect));
+
+ m_SelectionStreams.Clear(STREAM_NONE, STREAM_SOURCE_NAV);
+ m_SelectionStreams.Update(m_pInputStream, m_pDemuxer.get());
+ UpdateContent();
+
+ return NAVRESULT_HOLD;
+ }
+ break;
+ case DVDNAV_CELL_CHANGE:
+ {
+ //dvdnav_cell_change_event_t* cell_change_event = (dvdnav_cell_change_event_t*)pData;
+ CLog::Log(LOGDEBUG, "DVDNAV_CELL_CHANGE");
+
+ if (m_dvd.state != DVDSTATE_STILL)
+ m_dvd.state = DVDSTATE_NORMAL;
+ }
+ break;
+ case DVDNAV_NAV_PACKET:
+ {
+ //pci_t* pci = (pci_t*)pData;
+
+ // this should be possible to use to make sure we get
+ // seamless transitions over these boundaries
+ // if we remember the old vobunits boundaries
+ // when a packet comes out of demuxer that has
+ // pts values outside that boundary, it belongs
+ // to the new vobunit, which has new timestamps
+ UpdatePlayState(0);
+ }
+ break;
+ case DVDNAV_HOP_CHANNEL:
+ {
+ // This event is issued whenever a non-seamless operation has been executed.
+ // Applications with fifos should drop the fifos content to speed up responsiveness.
+ CLog::Log(LOGDEBUG, "DVDNAV_HOP_CHANNEL");
+ if(m_dvd.state == DVDSTATE_SEEK)
+ m_dvd.state = DVDSTATE_NORMAL;
+ else
+ {
+ bool sync = !IsInMenuInternal();
+ FlushBuffers(DVD_NOPTS_VALUE, false, sync);
+ m_dvd.syncClock = true;
+ m_dvd.state = DVDSTATE_NORMAL;
+ if (m_pDemuxer)
+ m_pDemuxer->Flush();
+ }
+
+ return NAVRESULT_ERROR;
+ }
+ break;
+ case DVDNAV_STOP:
+ {
+ CLog::Log(LOGDEBUG, "DVDNAV_STOP");
+ m_dvd.state = DVDSTATE_NORMAL;
+ }
+ break;
+ case DVDNAV_ERROR:
+ {
+ CLog::Log(LOGDEBUG, "DVDNAV_ERROR");
+ m_dvd.state = DVDSTATE_NORMAL;
+ CGUIDialogKaiToast::QueueNotification(g_localizeStrings.Get(16026),
+ g_localizeStrings.Get(16029));
+ }
+ break;
+ default:
+ {}
+ break;
+ }
+ }
+ return NAVRESULT_NOP;
+}
+
+void CVideoPlayer::GetVideoResolution(unsigned int &width, unsigned int &height)
+{
+ RESOLUTION_INFO res = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ width = res.iWidth;
+ height = res.iHeight;
+}
+
+bool CVideoPlayer::OnAction(const CAction &action)
+{
+#define THREAD_ACTION(action) \
+ do \
+ { \
+ if (!IsCurrentThread()) \
+ { \
+ m_messenger.Put( \
+ std::make_shared<CDVDMsgType<CAction>>(CDVDMsg::GENERAL_GUI_ACTION, action)); \
+ return true; \
+ } \
+ } while (false)
+
+ std::shared_ptr<CDVDInputStream::IMenus> pMenus = std::dynamic_pointer_cast<CDVDInputStream::IMenus>(m_pInputStream);
+ if (pMenus)
+ {
+ if (m_dvd.state == DVDSTATE_STILL && m_dvd.iDVDStillTime != 0ms &&
+ pMenus->GetTotalButtons() == 0)
+ {
+ switch(action.GetID())
+ {
+ case ACTION_NEXT_ITEM:
+ case ACTION_MOVE_RIGHT:
+ case ACTION_MOVE_UP:
+ case ACTION_SELECT_ITEM:
+ {
+ THREAD_ACTION(action);
+ /* this will force us out of the stillframe */
+ CLog::Log(LOGDEBUG, "{} - User asked to exit stillframe", __FUNCTION__);
+ m_dvd.iDVDStillStartTime = {};
+ m_dvd.iDVDStillTime = 1ms;
+ }
+ return true;
+ }
+ }
+
+
+ switch (action.GetID())
+ {
+/* this code is disabled to allow switching playlist items (dvdimage "stacks") */
+#if 0
+ case ACTION_PREV_ITEM: // SKIP-:
+ {
+ THREAD_ACTION(action);
+ CLog::Log(LOGDEBUG, " - pushed prev");
+ pMenus->OnPrevious();
+ m_processInfo->SeekFinished(0);
+ return true;
+ }
+ break;
+ case ACTION_NEXT_ITEM: // SKIP+:
+ {
+ THREAD_ACTION(action);
+ CLog::Log(LOGDEBUG, " - pushed next");
+ pMenus->OnNext();
+ m_processInfo->SeekFinished(0);
+ return true;
+ }
+ break;
+#endif
+ case ACTION_SHOW_VIDEOMENU: // start button
+ {
+ THREAD_ACTION(action);
+ CLog::LogF(LOGDEBUG, "Trying to go to the menu");
+ if (pMenus->OnMenu())
+ {
+ if (m_playSpeed == DVD_PLAYSPEED_PAUSE)
+ {
+ SetPlaySpeed(DVD_PLAYSPEED_NORMAL);
+ m_callback.OnPlayBackResumed();
+ }
+
+ // send a message to everyone that we've gone to the menu
+ CGUIMessage msg(GUI_MSG_VIDEO_MENU_STARTED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+ return true;
+ }
+ break;
+ }
+
+ if (pMenus->IsInMenu())
+ {
+ switch (action.GetID())
+ {
+ case ACTION_NEXT_ITEM:
+ THREAD_ACTION(action);
+ CLog::Log(LOGDEBUG, " - pushed next in menu, stream will decide");
+ if (pMenus->CanSeek() && GetChapterCount() > 0 && GetChapter() < GetChapterCount())
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeekChapter>(GetChapter() + 1));
+ else
+ pMenus->OnNext();
+
+ m_processInfo->SeekFinished(0);
+ return true;
+ case ACTION_PREV_ITEM:
+ THREAD_ACTION(action);
+ CLog::Log(LOGDEBUG, " - pushed prev in menu, stream will decide");
+ if (pMenus->CanSeek() && GetChapterCount() > 0 && GetChapter() > 0)
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeekChapter>(GetChapter() - 1));
+ else
+ pMenus->OnPrevious();
+
+ m_processInfo->SeekFinished(0);
+ return true;
+ case ACTION_PREVIOUS_MENU:
+ case ACTION_NAV_BACK:
+ {
+ THREAD_ACTION(action);
+ CLog::Log(LOGDEBUG, " - menu back");
+ pMenus->OnBack();
+ }
+ break;
+ case ACTION_MOVE_LEFT:
+ {
+ THREAD_ACTION(action);
+ CLog::Log(LOGDEBUG, " - move left");
+ pMenus->OnLeft();
+ }
+ break;
+ case ACTION_MOVE_RIGHT:
+ {
+ THREAD_ACTION(action);
+ CLog::Log(LOGDEBUG, " - move right");
+ pMenus->OnRight();
+ }
+ break;
+ case ACTION_MOVE_UP:
+ {
+ THREAD_ACTION(action);
+ CLog::Log(LOGDEBUG, " - move up");
+ pMenus->OnUp();
+ }
+ break;
+ case ACTION_MOVE_DOWN:
+ {
+ THREAD_ACTION(action);
+ CLog::Log(LOGDEBUG, " - move down");
+ pMenus->OnDown();
+ }
+ break;
+
+ case ACTION_MOUSE_MOVE:
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ CRect rs, rd, rv;
+ m_renderManager.GetVideoRect(rs, rd, rv);
+ CPoint pt(action.GetAmount(), action.GetAmount(1));
+ if (!rd.PtInRect(pt))
+ return false; // out of bounds
+ THREAD_ACTION(action);
+ // convert to video coords...
+ pt -= CPoint(rd.x1, rd.y1);
+ pt.x *= rs.Width() / rd.Width();
+ pt.y *= rs.Height() / rd.Height();
+ pt += CPoint(rs.x1, rs.y1);
+ if (action.GetID() == ACTION_MOUSE_LEFT_CLICK)
+ {
+ if (pMenus->OnMouseClick(pt))
+ return true;
+ else
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(ACTION_TRIGGER_OSD)));
+ return false;
+ }
+ }
+ return pMenus->OnMouseMove(pt);
+ }
+ break;
+ case ACTION_SELECT_ITEM:
+ {
+ THREAD_ACTION(action);
+ CLog::Log(LOGDEBUG, " - button select");
+ // show button pushed overlay
+ if(m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD))
+ m_VideoPlayerSubtitle->UpdateOverlayInfo(std::static_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream), LIBDVDNAV_BUTTON_CLICKED);
+
+ pMenus->ActivateButton();
+ }
+ break;
+ case REMOTE_0:
+ case REMOTE_1:
+ case REMOTE_2:
+ case REMOTE_3:
+ case REMOTE_4:
+ case REMOTE_5:
+ case REMOTE_6:
+ case REMOTE_7:
+ case REMOTE_8:
+ case REMOTE_9:
+ {
+ THREAD_ACTION(action);
+ // Offset from key codes back to button number
+ int button = action.GetID() - REMOTE_0;
+ CLog::Log(LOGDEBUG, " - button pressed {}", button);
+ pMenus->SelectButton(button);
+ }
+ break;
+ default:
+ return false;
+ break;
+ }
+ return true; // message is handled
+ }
+ }
+
+ pMenus.reset();
+
+ switch (action.GetID())
+ {
+ case ACTION_NEXT_ITEM:
+ if (GetChapter() > 0 && GetChapter() < GetChapterCount())
+ {
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeekChapter>(GetChapter() + 1));
+ m_processInfo->SeekFinished(0);
+ return true;
+ }
+ else if (SeekScene(true))
+ return true;
+ else
+ break;
+ case ACTION_PREV_ITEM:
+ if (GetChapter() > 0)
+ {
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeekChapter>(GetChapter() - 1));
+ m_processInfo->SeekFinished(0);
+ return true;
+ }
+ else if (SeekScene(false))
+ return true;
+ else
+ break;
+ case ACTION_TOGGLE_COMMSKIP:
+ m_SkipCommercials = !m_SkipCommercials;
+ CGUIDialogKaiToast::QueueNotification(g_localizeStrings.Get(25011),
+ g_localizeStrings.Get(m_SkipCommercials ? 25013 : 25012));
+ break;
+ case ACTION_PLAYER_DEBUG:
+ m_renderManager.ToggleDebug();
+ break;
+ case ACTION_PLAYER_DEBUG_VIDEO:
+ m_renderManager.ToggleDebugVideo();
+ break;
+
+ case ACTION_PLAYER_PROCESS_INFO:
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_DIALOG_PLAYER_PROCESS_INFO)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_DIALOG_PLAYER_PROCESS_INFO);
+ return true;
+ }
+ break;
+ }
+
+ // return false to inform the caller we didn't handle the message
+ return false;
+}
+
+bool CVideoPlayer::IsInMenuInternal() const
+{
+ std::shared_ptr<CDVDInputStream::IMenus> pStream = std::dynamic_pointer_cast<CDVDInputStream::IMenus>(m_pInputStream);
+ if (pStream)
+ {
+ if (m_dvd.state == DVDSTATE_STILL)
+ return true;
+ else
+ return pStream->IsInMenu();
+ }
+ return false;
+}
+
+
+bool CVideoPlayer::IsInMenu() const
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ return m_State.isInMenu;
+}
+
+MenuType CVideoPlayer::GetSupportedMenuType() const
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ return m_State.menuType;
+}
+
+std::string CVideoPlayer::GetPlayerState()
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ return m_State.player_state;
+}
+
+bool CVideoPlayer::SetPlayerState(const std::string& state)
+{
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSetState>(state));
+ return true;
+}
+
+int CVideoPlayer::GetChapterCount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ return m_State.chapters.size();
+}
+
+int CVideoPlayer::GetChapter() const
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ return m_State.chapter;
+}
+
+void CVideoPlayer::GetChapterName(std::string& strChapterName, int chapterIdx) const
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ if (chapterIdx == -1 && m_State.chapter > 0 && m_State.chapter <= (int) m_State.chapters.size())
+ strChapterName = m_State.chapters[m_State.chapter - 1].first;
+ else if (chapterIdx > 0 && chapterIdx <= (int) m_State.chapters.size())
+ strChapterName = m_State.chapters[chapterIdx - 1].first;
+}
+
+int CVideoPlayer::SeekChapter(int iChapter)
+{
+ if (GetChapter() > 0)
+ {
+ if (iChapter < 0)
+ iChapter = 0;
+ if (iChapter > GetChapterCount())
+ return 0;
+
+ // Seek to the chapter.
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSeekChapter>(iChapter));
+ SynchronizeDemuxer();
+ }
+
+ return 0;
+}
+
+int64_t CVideoPlayer::GetChapterPos(int chapterIdx) const
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ if (chapterIdx > 0 && chapterIdx <= (int) m_State.chapters.size())
+ return m_State.chapters[chapterIdx - 1].second;
+
+ return -1;
+}
+
+void CVideoPlayer::AddSubtitle(const std::string& strSubPath)
+{
+ m_messenger.Put(
+ std::make_shared<CDVDMsgType<std::string>>(CDVDMsg::SUBTITLE_ADDFILE, strSubPath));
+}
+
+bool CVideoPlayer::IsCaching() const
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ return !m_State.isInMenu && m_State.caching;
+}
+
+int CVideoPlayer::GetCacheLevel() const
+{
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ return (int)(m_State.cache_level * 100);
+}
+
+double CVideoPlayer::GetQueueTime()
+{
+ int a = m_VideoPlayerAudio->GetLevel();
+ int v = m_processInfo->GetLevelVQ();
+ return std::max(a, v) * 8000.0 / 100;
+}
+
+int CVideoPlayer::AddSubtitleFile(const std::string& filename, const std::string& subfilename)
+{
+ std::string ext = URIUtils::GetExtension(filename);
+ std::string vobsubfile = subfilename;
+ if (ext == ".idx" || ext == ".sup")
+ {
+ std::shared_ptr<CDVDDemux> pDemux;
+ if (ext == ".idx")
+ {
+ if (vobsubfile.empty())
+ {
+ // find corresponding .sub (e.g. in case of manually selected .idx sub)
+ vobsubfile = CUtil::GetVobSubSubFromIdx(filename);
+ if (vobsubfile.empty())
+ return -1;
+ }
+
+ auto pDemuxVobsub = std::make_shared<CDVDDemuxVobsub>();
+ if (!pDemuxVobsub->Open(filename, STREAM_SOURCE_NONE, vobsubfile))
+ return -1;
+
+ m_SelectionStreams.Update(nullptr, pDemuxVobsub.get(), vobsubfile);
+ pDemux = pDemuxVobsub;
+ }
+ else // .sup file
+ {
+ CFileItem item(filename, false);
+ std::shared_ptr<CDVDInputStream> pInput;
+ pInput = CDVDFactoryInputStream::CreateInputStream(nullptr, item);
+ if (!pInput || !pInput->Open())
+ return -1;
+
+ auto pDemuxFFmpeg = std::make_shared<CDVDDemuxFFmpeg>();
+ if (!pDemuxFFmpeg->Open(pInput, false))
+ return -1;
+
+ m_SelectionStreams.Update(nullptr, pDemuxFFmpeg.get(), filename);
+ pDemux = pDemuxFFmpeg;
+ }
+
+ ExternalStreamInfo info =
+ CUtil::GetExternalStreamDetailsFromFilename(m_item.GetDynPath(), filename);
+
+ for (auto sub : pDemux->GetStreams())
+ {
+ if (sub->type != STREAM_SUBTITLE)
+ continue;
+
+ int index = m_SelectionStreams.TypeIndexOf(STREAM_SUBTITLE,
+ m_SelectionStreams.Source(STREAM_SOURCE_DEMUX_SUB, filename),
+ sub->demuxerId, sub->uniqueId);
+ SelectionStream& stream = m_SelectionStreams.Get(STREAM_SUBTITLE, index);
+
+ if (stream.name.empty())
+ stream.name = info.name;
+
+ if (stream.language.empty())
+ stream.language = info.language;
+
+ if (static_cast<StreamFlags>(info.flag) != StreamFlags::FLAG_NONE)
+ stream.flags = static_cast<StreamFlags>(info.flag);
+ }
+
+ UpdateContent();
+ // the demuxer id is unique
+ m_subtitleDemuxerMap[pDemux->GetDemuxerId()] = pDemux;
+ return m_SelectionStreams.TypeIndexOf(
+ STREAM_SUBTITLE, m_SelectionStreams.Source(STREAM_SOURCE_DEMUX_SUB, filename),
+ pDemux->GetDemuxerId(), 0);
+ }
+
+ if(ext == ".sub")
+ {
+ // if this looks like vobsub file (i.e. .idx found), add it as such
+ std::string vobsubidx = CUtil::GetVobSubIdxFromSub(filename);
+ if (!vobsubidx.empty())
+ return AddSubtitleFile(vobsubidx, filename);
+ }
+
+ SelectionStream s;
+ s.source = m_SelectionStreams.Source(STREAM_SOURCE_TEXT, filename);
+ s.type = STREAM_SUBTITLE;
+ s.id = 0;
+ s.filename = filename;
+ ExternalStreamInfo info = CUtil::GetExternalStreamDetailsFromFilename(m_item.GetDynPath(), filename);
+ s.name = info.name;
+ s.language = info.language;
+ if (static_cast<StreamFlags>(info.flag) != StreamFlags::FLAG_NONE)
+ s.flags = static_cast<StreamFlags>(info.flag);
+
+ m_SelectionStreams.Update(s);
+ UpdateContent();
+ return m_SelectionStreams.TypeIndexOf(STREAM_SUBTITLE, s.source, s.demuxerId, s.id);
+}
+
+void CVideoPlayer::UpdatePlayState(double timeout)
+{
+ if (m_State.timestamp != 0 &&
+ m_State.timestamp + DVD_MSEC_TO_TIME(timeout) > m_clock.GetAbsoluteClock())
+ return;
+
+ SPlayerState state(m_State);
+
+ state.dts = DVD_NOPTS_VALUE;
+ if (m_CurrentVideo.dts != DVD_NOPTS_VALUE)
+ state.dts = m_CurrentVideo.dts;
+ else if (m_CurrentAudio.dts != DVD_NOPTS_VALUE)
+ state.dts = m_CurrentAudio.dts;
+ else if (m_CurrentVideo.startpts != DVD_NOPTS_VALUE)
+ state.dts = m_CurrentVideo.startpts;
+ else if (m_CurrentAudio.startpts != DVD_NOPTS_VALUE)
+ state.dts = m_CurrentAudio.startpts;
+
+ state.startTime = 0;
+ state.timeMin = 0;
+
+ std::shared_ptr<CDVDInputStream::IMenus> pMenu = std::dynamic_pointer_cast<CDVDInputStream::IMenus>(m_pInputStream);
+
+ if (m_pDemuxer)
+ {
+ if (IsInMenuInternal() && pMenu && !pMenu->CanSeek())
+ state.chapter = 0;
+ else
+ state.chapter = m_pDemuxer->GetChapter();
+
+ state.chapters.clear();
+ if (m_pDemuxer->GetChapterCount() > 0)
+ {
+ for (int i = 0, ie = m_pDemuxer->GetChapterCount(); i < ie; ++i)
+ {
+ std::string name;
+ m_pDemuxer->GetChapterName(name, i + 1);
+ state.chapters.emplace_back(name, m_pDemuxer->GetChapterPos(i + 1));
+ }
+ }
+ CServiceBroker::GetDataCacheCore().SetChapters(state.chapters);
+
+ state.time = m_clock.GetClock(false) * 1000 / DVD_TIME_BASE;
+ state.timeMax = m_pDemuxer->GetStreamLength();
+ }
+
+ state.canpause = false;
+ state.canseek = false;
+ state.cantempo = false;
+ state.isInMenu = false;
+ state.menuType = MenuType::NONE;
+
+ if (m_pInputStream)
+ {
+ CDVDInputStream::IChapter* pChapter = m_pInputStream->GetIChapter();
+ if (pChapter)
+ {
+ if (IsInMenuInternal() && pMenu && !pMenu->CanSeek())
+ state.chapter = 0;
+ else
+ state.chapter = pChapter->GetChapter();
+
+ state.chapters.clear();
+ if (pChapter->GetChapterCount() > 0)
+ {
+ for (int i = 0, ie = pChapter->GetChapterCount(); i < ie; ++i)
+ {
+ std::string name;
+ pChapter->GetChapterName(name, i + 1);
+ state.chapters.push_back(make_pair(name, pChapter->GetChapterPos(i + 1)));
+ }
+ }
+ CServiceBroker::GetDataCacheCore().SetChapters(state.chapters);
+ }
+
+ CDVDInputStream::ITimes* pTimes = m_pInputStream->GetITimes();
+ CDVDInputStream::IDisplayTime* pDisplayTime = m_pInputStream->GetIDisplayTime();
+
+ CDVDInputStream::ITimes::Times times;
+ if (pTimes && pTimes->GetTimes(times))
+ {
+ state.startTime = times.startTime;
+ state.time = (m_clock.GetClock(false) - times.ptsStart) * 1000 / DVD_TIME_BASE;
+ state.timeMax = (times.ptsEnd - times.ptsStart) * 1000 / DVD_TIME_BASE;
+ state.timeMin = (times.ptsBegin - times.ptsStart) * 1000 / DVD_TIME_BASE;
+ state.time_offset = -times.ptsStart;
+ }
+ else if (pDisplayTime && pDisplayTime->GetTotalTime() > 0)
+ {
+ if (state.dts != DVD_NOPTS_VALUE)
+ {
+ int dispTime = 0;
+ if (m_CurrentVideo.id >= 0 && m_CurrentVideo.dispTime)
+ dispTime = m_CurrentVideo.dispTime;
+ else if (m_CurrentAudio.dispTime)
+ dispTime = m_CurrentAudio.dispTime;
+
+ state.time_offset = DVD_MSEC_TO_TIME(dispTime) - state.dts;
+ }
+ state.time += state.time_offset * 1000 / DVD_TIME_BASE;
+ state.timeMax = pDisplayTime->GetTotalTime();
+ }
+ else
+ {
+ state.time_offset = 0;
+ }
+
+ if (pMenu)
+ {
+ if (!pMenu->GetState(state.player_state))
+ state.player_state = "";
+
+ if (m_dvd.state == DVDSTATE_STILL)
+ {
+ const auto now = std::chrono::steady_clock::now();
+ const auto duration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - m_dvd.iDVDStillStartTime);
+ state.time = duration.count();
+ state.timeMax = m_dvd.iDVDStillTime.count();
+ state.isInMenu = true;
+ }
+ else if (IsInMenuInternal())
+ {
+ state.time = pDisplayTime->GetTime();
+ state.isInMenu = true;
+ if (!pMenu->CanSeek())
+ state.time_offset = 0;
+ }
+ state.menuType = pMenu->GetSupportedMenuType();
+ }
+
+ state.canpause = m_pInputStream->CanPause();
+
+ bool realtime = m_pInputStream->IsRealtime();
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEDISPLAYASCLOCK) &&
+ !realtime)
+ {
+ state.cantempo = true;
+ }
+ else
+ {
+ state.cantempo = false;
+ }
+
+ m_processInfo->SetStateRealtime(realtime);
+ }
+
+ if (m_Edl.HasCuts())
+ {
+ state.time = static_cast<double>(m_Edl.GetTimeWithoutCuts(state.time));
+ state.timeMax = state.timeMax - static_cast<double>(m_Edl.GetTotalCutTime());
+ }
+
+ if (m_caching > CACHESTATE_DONE && m_caching < CACHESTATE_PLAY)
+ state.caching = true;
+ else
+ state.caching = false;
+
+ double level, delay, offset;
+ if (GetCachingTimes(level, delay, offset))
+ {
+ state.cache_delay = std::max(0.0, delay);
+ state.cache_level = std::max(0.0, std::min(1.0, level));
+ state.cache_offset = offset;
+ }
+ else
+ {
+ state.cache_delay = 0.0;
+ state.cache_level = std::min(1.0, GetQueueTime() / 8000.0);
+ state.cache_offset = GetQueueTime() / state.timeMax;
+ }
+
+ XFILE::SCacheStatus status;
+ if (m_pInputStream && m_pInputStream->GetCacheStatus(&status))
+ {
+ state.cache_bytes = status.forward;
+ if(state.timeMax)
+ state.cache_bytes += m_pInputStream->GetLength() * (int64_t) (GetQueueTime() / state.timeMax);
+ }
+ else
+ state.cache_bytes = 0;
+
+ state.timestamp = m_clock.GetAbsoluteClock();
+
+ if (state.timeMax <= 0)
+ {
+ state.timeMax = state.time;
+ state.timeMin = state.time;
+ }
+ if (state.timeMin == state.timeMax)
+ {
+ state.canseek = false;
+ state.cantempo = false;
+ }
+ else
+ {
+ state.canseek = true;
+ state.canpause = true;
+ }
+
+ m_processInfo->SetPlayTimes(state.startTime, state.time, state.timeMin, state.timeMax);
+
+ std::unique_lock<CCriticalSection> lock(m_StateSection);
+ m_State = state;
+}
+
+int64_t CVideoPlayer::GetUpdatedTime()
+{
+ UpdatePlayState(0);
+ return llrint(m_State.time);
+}
+
+void CVideoPlayer::SetDynamicRangeCompression(long drc)
+{
+ m_processInfo->GetVideoSettingsLocked().SetVolumeAmplification(static_cast<float>(drc) / 100);
+ m_VideoPlayerAudio->SetDynamicRangeCompression(drc);
+}
+
+CVideoSettings CVideoPlayer::GetVideoSettings() const
+{
+ return m_processInfo->GetVideoSettings();
+}
+
+void CVideoPlayer::SetVideoSettings(CVideoSettings& settings)
+{
+ m_processInfo->SetVideoSettings(settings);
+ m_renderManager.SetVideoSettings(settings);
+ m_renderManager.SetDelay(static_cast<int>(settings.m_AudioDelay * 1000.0f));
+ m_renderManager.SetSubtitleVerticalPosition(settings.m_subtitleVerticalPosition,
+ settings.m_subtitleVerticalPositionSave);
+ m_VideoPlayerVideo->EnableSubtitle(settings.m_SubtitleOn);
+ m_VideoPlayerVideo->SetSubtitleDelay(static_cast<int>(-settings.m_SubtitleDelay * DVD_TIME_BASE));
+}
+
+void CVideoPlayer::FrameMove()
+{
+ m_renderManager.FrameMove();
+}
+
+void CVideoPlayer::Render(bool clear, uint32_t alpha, bool gui)
+{
+ m_renderManager.Render(clear, 0, alpha, gui);
+}
+
+void CVideoPlayer::FlushRenderer()
+{
+ m_renderManager.Flush(true, true);
+}
+
+void CVideoPlayer::SetRenderViewMode(int mode, float zoom, float par, float shift, bool stretch)
+{
+ m_processInfo->GetVideoSettingsLocked().SetViewMode(mode, zoom, par, shift, stretch);
+ m_renderManager.SetVideoSettings(m_processInfo->GetVideoSettings());
+ m_renderManager.SetViewMode(mode);
+}
+
+float CVideoPlayer::GetRenderAspectRatio() const
+{
+ return m_renderManager.GetAspectRatio();
+}
+
+void CVideoPlayer::TriggerUpdateResolution()
+{
+ std::string stereomode;
+ m_renderManager.TriggerUpdateResolution(0, 0, 0, stereomode);
+}
+
+bool CVideoPlayer::IsRenderingVideo() const
+{
+ return m_renderManager.IsConfigured();
+}
+
+bool CVideoPlayer::Supports(EINTERLACEMETHOD method) const
+{
+ if (!m_processInfo)
+ return false;
+ return m_processInfo->Supports(method);
+}
+
+EINTERLACEMETHOD CVideoPlayer::GetDeinterlacingMethodDefault() const
+{
+ if (!m_processInfo)
+ return EINTERLACEMETHOD::VS_INTERLACEMETHOD_NONE;
+ return m_processInfo->GetDeinterlacingMethodDefault();
+}
+
+bool CVideoPlayer::Supports(ESCALINGMETHOD method) const
+{
+ return m_renderManager.Supports(method);
+}
+
+bool CVideoPlayer::Supports(ERENDERFEATURE feature) const
+{
+ return m_renderManager.Supports(feature);
+}
+
+unsigned int CVideoPlayer::RenderCaptureAlloc()
+{
+ return m_renderManager.AllocRenderCapture();
+}
+
+void CVideoPlayer::RenderCapture(unsigned int captureId, unsigned int width, unsigned int height, int flags)
+{
+ m_renderManager.StartRenderCapture(captureId, width, height, flags);
+}
+
+void CVideoPlayer::RenderCaptureRelease(unsigned int captureId)
+{
+ m_renderManager.ReleaseRenderCapture(captureId);
+}
+
+bool CVideoPlayer::RenderCaptureGetPixels(unsigned int captureId, unsigned int millis, uint8_t *buffer, unsigned int size)
+{
+ return m_renderManager.RenderCaptureGetPixels(captureId, millis, buffer, size);
+}
+
+void CVideoPlayer::VideoParamsChange()
+{
+ m_messenger.Put(std::make_shared<CDVDMsg>(CDVDMsg::PLAYER_AVCHANGE));
+}
+
+void CVideoPlayer::GetDebugInfo(std::string &audio, std::string &video, std::string &general)
+{
+ audio = m_VideoPlayerAudio->GetPlayerInfo();
+ video = m_VideoPlayerVideo->GetPlayerInfo();
+ GetGeneralInfo(general);
+}
+
+void CVideoPlayer::UpdateClockSync(bool enabled)
+{
+ m_processInfo->SetRenderClockSync(enabled);
+}
+
+void CVideoPlayer::UpdateRenderInfo(CRenderInfo &info)
+{
+ m_processInfo->UpdateRenderInfo(info);
+}
+
+void CVideoPlayer::UpdateRenderBuffers(int queued, int discard, int free)
+{
+ m_processInfo->UpdateRenderBuffers(queued, discard, free);
+}
+
+void CVideoPlayer::UpdateGuiRender(bool gui)
+{
+ m_processInfo->SetGuiRender(gui);
+}
+
+void CVideoPlayer::UpdateVideoRender(bool video)
+{
+ m_processInfo->SetVideoRender(video);
+}
+
+// IDispResource interface
+void CVideoPlayer::OnLostDisplay()
+{
+ CLog::Log(LOGINFO, "VideoPlayer: OnLostDisplay received");
+ m_VideoPlayerAudio->SendMessage(std::make_shared<CDVDMsgBool>(CDVDMsg::GENERAL_PAUSE, true), 1);
+ m_VideoPlayerVideo->SendMessage(std::make_shared<CDVDMsgBool>(CDVDMsg::GENERAL_PAUSE, true), 1);
+ m_clock.Pause(true);
+ m_displayLost = true;
+ FlushRenderer();
+}
+
+void CVideoPlayer::OnResetDisplay()
+{
+ if (!m_displayLost)
+ return;
+
+ CLog::Log(LOGINFO, "VideoPlayer: OnResetDisplay received");
+ m_VideoPlayerAudio->SendMessage(std::make_shared<CDVDMsgBool>(CDVDMsg::GENERAL_PAUSE, false), 1);
+ m_VideoPlayerVideo->SendMessage(std::make_shared<CDVDMsgBool>(CDVDMsg::GENERAL_PAUSE, false), 1);
+ m_clock.Pause(false);
+ m_displayLost = false;
+ m_VideoPlayerAudio->SendMessage(std::make_shared<CDVDMsg>(CDVDMsg::PLAYER_DISPLAY_RESET), 1);
+}
+
+void CVideoPlayer::UpdateFileItemStreamDetails(CFileItem& item)
+{
+ if (!m_UpdateStreamDetails)
+ return;
+ m_UpdateStreamDetails = false;
+
+ CLog::Log(LOGDEBUG, "CVideoPlayer: updating file item stream details with available streams");
+
+ VideoStreamInfo videoInfo;
+ AudioStreamInfo audioInfo;
+ SubtitleStreamInfo subtitleInfo;
+ CVideoInfoTag* info = item.GetVideoInfoTag();
+ GetVideoStreamInfo(CURRENT_STREAM, videoInfo);
+ info->m_streamDetails.SetStreams(videoInfo, m_processInfo->GetMaxTime() / 1000, audioInfo,
+ subtitleInfo);
+
+ //grab all the audio and subtitle info and save it
+
+ for (int i = 0; i < GetAudioStreamCount(); i++)
+ {
+ GetAudioStreamInfo(i, audioInfo);
+ info->m_streamDetails.AddStream(new CStreamDetailAudio(audioInfo));
+ }
+
+ for (int i = 0; i < GetSubtitleCount(); i++)
+ {
+ GetSubtitleStreamInfo(i, subtitleInfo);
+ info->m_streamDetails.AddStream(new CStreamDetailSubtitle(subtitleInfo));
+ }
+}
+
+//------------------------------------------------------------------------------
+// content related methods
+//------------------------------------------------------------------------------
+
+void CVideoPlayer::UpdateContent()
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+ m_content.m_selectionStreams = m_SelectionStreams;
+ m_content.m_programs = m_programs;
+}
+
+void CVideoPlayer::UpdateContentState()
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+
+ m_content.m_videoIndex = m_SelectionStreams.TypeIndexOf(STREAM_VIDEO, m_CurrentVideo.source,
+ m_CurrentVideo.demuxerId, m_CurrentVideo.id);
+ m_content.m_audioIndex = m_SelectionStreams.TypeIndexOf(STREAM_AUDIO, m_CurrentAudio.source,
+ m_CurrentAudio.demuxerId, m_CurrentAudio.id);
+ m_content.m_subtitleIndex = m_SelectionStreams.TypeIndexOf(STREAM_SUBTITLE, m_CurrentSubtitle.source,
+ m_CurrentSubtitle.demuxerId, m_CurrentSubtitle.id);
+
+ if (m_pInputStream->IsStreamType(DVDSTREAM_TYPE_DVD) && m_content.m_videoIndex == -1 &&
+ m_content.m_audioIndex == -1)
+ {
+ std::shared_ptr<CDVDInputStreamNavigator> nav =
+ std::static_pointer_cast<CDVDInputStreamNavigator>(m_pInputStream);
+
+ m_content.m_videoIndex = m_SelectionStreams.TypeIndexOf(STREAM_VIDEO, STREAM_SOURCE_NAV, -1,
+ nav->GetActiveAngle());
+ m_content.m_audioIndex = m_SelectionStreams.TypeIndexOf(STREAM_AUDIO, STREAM_SOURCE_NAV, -1,
+ nav->GetActiveAudioStream());
+
+ // only update the subtitle index in libdvdnav if the subtitle is provided by the dvd itself,
+ // i.e. for external subtitles the index is always greater than the subtitlecount in dvdnav
+ if (m_content.m_subtitleIndex < nav->GetSubTitleStreamCount())
+ {
+ m_content.m_subtitleIndex = m_SelectionStreams.TypeIndexOf(
+ STREAM_SUBTITLE, STREAM_SOURCE_NAV, -1, nav->GetActiveSubtitleStream());
+ }
+ }
+}
+
+void CVideoPlayer::GetVideoStreamInfo(int streamId, VideoStreamInfo& info) const
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+
+ if (streamId == CURRENT_STREAM)
+ streamId = m_content.m_videoIndex;
+
+ if (streamId < 0 || streamId > GetVideoStreamCount() - 1)
+ {
+ info.valid = false;
+ return;
+ }
+
+ const SelectionStream& s = m_content.m_selectionStreams.Get(STREAM_VIDEO, streamId);
+ if (s.language.length() > 0)
+ info.language = s.language;
+
+ if (s.name.length() > 0)
+ info.name = s.name;
+
+ m_renderManager.GetVideoRect(info.SrcRect, info.DestRect, info.VideoRect);
+
+ info.valid = true;
+ info.bitrate = s.bitrate;
+ info.width = s.width;
+ info.height = s.height;
+ info.codecName = s.codec;
+ info.videoAspectRatio = s.aspect_ratio;
+ info.stereoMode = s.stereo_mode;
+ info.flags = s.flags;
+ info.hdrType = s.hdrType;
+}
+
+int CVideoPlayer::GetVideoStreamCount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+ return m_content.m_selectionStreams.CountType(STREAM_VIDEO);
+}
+
+int CVideoPlayer::GetVideoStream() const
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+ return m_content.m_videoIndex;
+}
+
+void CVideoPlayer::SetVideoStream(int iStream)
+{
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSetVideoStream>(iStream));
+ m_processInfo->GetVideoSettingsLocked().SetVideoStream(iStream);
+ SynchronizeDemuxer();
+}
+
+void CVideoPlayer::GetAudioStreamInfo(int index, AudioStreamInfo& info) const
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+
+ if (index == CURRENT_STREAM)
+ index = m_content.m_audioIndex;
+
+ if (index < 0 || index > GetAudioStreamCount() - 1)
+ {
+ info.valid = false;
+ return;
+ }
+
+ const SelectionStream& s = m_content.m_selectionStreams.Get(STREAM_AUDIO, index);
+ info.language = s.language;
+ info.name = s.name;
+
+ if (s.type == STREAM_NONE)
+ info.name += " (Invalid)";
+
+ info.valid = true;
+ info.bitrate = s.bitrate;
+ info.channels = s.channels;
+ info.codecName = s.codec;
+ info.flags = s.flags;
+}
+
+int CVideoPlayer::GetAudioStreamCount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+ return m_content.m_selectionStreams.CountType(STREAM_AUDIO);
+}
+
+int CVideoPlayer::GetAudioStream()
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+ return m_content.m_audioIndex;
+}
+
+void CVideoPlayer::SetAudioStream(int iStream)
+{
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSetAudioStream>(iStream));
+ m_processInfo->GetVideoSettingsLocked().SetAudioStream(iStream);
+ SynchronizeDemuxer();
+}
+
+void CVideoPlayer::GetSubtitleStreamInfo(int index, SubtitleStreamInfo& info) const
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+
+ if (index == CURRENT_STREAM)
+ index = m_content.m_subtitleIndex;
+
+ if (index < 0 || index > GetSubtitleCount() - 1)
+ {
+ info.valid = false;
+ info.language.clear();
+ info.flags = StreamFlags::FLAG_NONE;
+ return;
+ }
+
+ const SelectionStream& s = m_content.m_selectionStreams.Get(STREAM_SUBTITLE, index);
+ info.name = s.name;
+
+ if (s.type == STREAM_NONE)
+ info.name += "(Invalid)";
+
+ info.language = s.language;
+ info.flags = s.flags;
+}
+
+void CVideoPlayer::SetSubtitle(int iStream)
+{
+ m_messenger.Put(std::make_shared<CDVDMsgPlayerSetSubtitleStream>(iStream));
+ m_processInfo->GetVideoSettingsLocked().SetSubtitleStream(iStream);
+}
+
+int CVideoPlayer::GetSubtitleCount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+ return m_content.m_selectionStreams.CountType(STREAM_SUBTITLE);
+}
+
+int CVideoPlayer::GetSubtitle()
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+ return m_content.m_subtitleIndex;
+}
+
+int CVideoPlayer::GetPrograms(std::vector<ProgramInfo>& programs)
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+ programs = m_programs;
+ return programs.size();
+}
+
+void CVideoPlayer::SetProgram(int progId)
+{
+ m_messenger.Put(std::make_shared<CDVDMsgInt>(CDVDMsg::PLAYER_SET_PROGRAM, progId));
+}
+
+int CVideoPlayer::GetProgramsCount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_content.m_section);
+ return m_programs.size();
+}
+
+void CVideoPlayer::SetUpdateStreamDetails()
+{
+ m_messenger.Put(std::make_shared<CDVDMsg>(CDVDMsg::PLAYER_SET_UPDATE_STREAM_DETAILS));
+}
diff --git a/xbmc/cores/VideoPlayer/VideoPlayer.h b/xbmc/cores/VideoPlayer/VideoPlayer.h
new file mode 100644
index 0000000..e40fb7a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayer.h
@@ -0,0 +1,567 @@
+/*
+ * 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 "DVDClock.h"
+#include "DVDMessageQueue.h"
+#include "Edl.h"
+#include "FileItem.h"
+#include "IVideoPlayer.h"
+#include "VideoPlayerAudioID3.h"
+#include "VideoPlayerRadioRDS.h"
+#include "VideoPlayerSubtitle.h"
+#include "VideoPlayerTeletext.h"
+#include "cores/IPlayer.h"
+#include "cores/MenuType.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderManager.h"
+#include "guilib/DispResource.h"
+#include "threads/SystemClock.h"
+#include "threads/Thread.h"
+
+#include <atomic>
+#include <chrono>
+#include <memory>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+struct SPlayerState
+{
+ SPlayerState() { Clear(); }
+ void Clear()
+ {
+ timestamp = 0;
+ time = 0;
+ startTime = 0;
+ timeMin = 0;
+ timeMax = 0;
+ time_offset = 0;
+ dts = DVD_NOPTS_VALUE;
+ player_state = "";
+ isInMenu = false;
+ menuType = MenuType::NONE;
+ chapter = 0;
+ chapters.clear();
+ canpause = false;
+ canseek = false;
+ cantempo = false;
+ caching = false;
+ cache_bytes = 0;
+ cache_level = 0.0;
+ cache_delay = 0.0;
+ cache_offset = 0.0;
+ lastSeek = 0;
+ streamsReady = false;
+ }
+
+ double timestamp; // last time of update
+ double lastSeek; // time of last seek
+ double time_offset; // difference between time and pts
+
+ double time; // current playback time
+ double timeMax;
+ double timeMin;
+ time_t startTime;
+ double dts; // last known dts
+
+ std::string player_state; // full player state
+ bool isInMenu;
+ MenuType menuType;
+ bool streamsReady;
+
+ int chapter; // current chapter
+ std::vector<std::pair<std::string, int64_t>> chapters; // name and position for chapters
+
+ bool canpause; // pvr: can pause the current playing item
+ bool canseek; // pvr: can seek in the current playing item
+ bool cantempo;
+ bool caching;
+
+ int64_t cache_bytes; // number of bytes current's cached
+ double cache_level; // current estimated required cache level
+ double cache_delay; // time until cache is expected to reach estimated level
+ double cache_offset; // percentage of file ahead of current position
+};
+
+class CDVDInputStream;
+
+class CDVDDemux;
+class CDemuxStreamVideo;
+class CDemuxStreamAudio;
+class CStreamInfo;
+class CDVDDemuxCC;
+class CVideoPlayer;
+
+#define DVDSTATE_NORMAL 0x00000001 // normal dvd state
+#define DVDSTATE_STILL 0x00000002 // currently displaying a still frame
+#define DVDSTATE_WAIT 0x00000003 // waiting for demuxer read error
+#define DVDSTATE_SEEK 0x00000004 // we are finishing a seek request
+
+class CCurrentStream
+{
+public:
+ int64_t demuxerId; // demuxer's id of current playing stream
+ int id; // id of current playing stream
+ int source;
+ double dts; // last dts from demuxer, used to find discontinuities
+ double dur; // last frame expected duration
+ int dispTime; // display time from input stream
+ CDVDStreamInfo hint; // stream hints, used to notice stream changes
+ void* stream; // pointer or integer, identifying stream playing. if it changes stream changed
+ int changes; // remembered counter from stream to track codec changes
+ bool inited;
+ unsigned int packets;
+ IDVDStreamPlayer::ESyncState syncState;
+ double starttime;
+ double cachetime;
+ double cachetotal;
+ const StreamType type;
+ const int player;
+ // stuff to handle starting after seek
+ double startpts;
+ double lastdts;
+
+ enum
+ {
+ AV_SYNC_NONE,
+ AV_SYNC_CHECK,
+ AV_SYNC_CONT,
+ AV_SYNC_FORCE
+ } avsync;
+
+ CCurrentStream(StreamType t, int i)
+ : type(t)
+ , player(i)
+ {
+ Clear();
+ }
+
+ void Clear()
+ {
+ id = -1;
+ demuxerId = -1;
+ source = STREAM_SOURCE_NONE;
+ dts = DVD_NOPTS_VALUE;
+ dur = DVD_NOPTS_VALUE;
+ hint.Clear();
+ stream = NULL;
+ changes = 0;
+ inited = false;
+ packets = 0;
+ syncState = IDVDStreamPlayer::SYNC_STARTING;
+ starttime = DVD_NOPTS_VALUE;
+ startpts = DVD_NOPTS_VALUE;
+ lastdts = DVD_NOPTS_VALUE;
+ avsync = AV_SYNC_FORCE;
+ }
+
+ double dts_end()
+ {
+ if(dts == DVD_NOPTS_VALUE)
+ return DVD_NOPTS_VALUE;
+ if(dur == DVD_NOPTS_VALUE)
+ return dts;
+ return dts + dur;
+ }
+};
+
+//------------------------------------------------------------------------------
+// selection streams
+//------------------------------------------------------------------------------
+struct SelectionStream
+{
+ StreamType type = STREAM_NONE;
+ int type_index = 0;
+ std::string filename;
+ std::string filename2; // for vobsub subtitles, 2 files are necessary (idx/sub)
+ std::string language;
+ std::string name;
+ StreamFlags flags = StreamFlags::FLAG_NONE;
+ int source = 0;
+ int id = 0;
+ int64_t demuxerId = -1;
+ std::string codec;
+ int channels = 0;
+ int bitrate = 0;
+ int width = 0;
+ int height = 0;
+ CRect SrcRect;
+ CRect DestRect;
+ CRect VideoRect;
+ std::string stereo_mode;
+ float aspect_ratio = 0.0f;
+ StreamHdrType hdrType = StreamHdrType::HDR_TYPE_NONE;
+};
+
+class CSelectionStreams
+{
+public:
+ CSelectionStreams() = default;
+
+ int TypeIndexOf(StreamType type, int source, int64_t demuxerId, int id) const;
+ int CountTypeOfSource(StreamType type, StreamSource source) const;
+ int CountType(StreamType type) const;
+ SelectionStream& Get(StreamType type, int index);
+ const SelectionStream& Get(StreamType type, int index) const;
+ bool Get(StreamType type, StreamFlags flag, SelectionStream& out);
+ void Clear(StreamType type, StreamSource source);
+ int Source(StreamSource source, const std::string& filename);
+ void Update(SelectionStream& s);
+ void Update(const std::shared_ptr<CDVDInputStream>& input, CDVDDemux* demuxer);
+ void Update(const std::shared_ptr<CDVDInputStream>& input,
+ CDVDDemux* demuxer,
+ const std::string& filename2);
+
+ std::vector<SelectionStream> Get(StreamType type);
+ template<typename Compare> std::vector<SelectionStream> Get(StreamType type, Compare compare)
+ {
+ std::vector<SelectionStream> streams = Get(type);
+ std::stable_sort(streams.begin(), streams.end(), compare);
+ return streams;
+ }
+
+ std::vector<SelectionStream> m_Streams;
+
+protected:
+ SelectionStream m_invalid;
+};
+
+//------------------------------------------------------------------------------
+// main class
+//------------------------------------------------------------------------------
+
+class CProcessInfo;
+class CJobQueue;
+
+class CVideoPlayer : public IPlayer, public CThread, public IVideoPlayer,
+ public IDispResource, public IRenderLoop, public IRenderMsg
+{
+public:
+ explicit CVideoPlayer(IPlayerCallback& callback);
+ ~CVideoPlayer() override;
+ bool OpenFile(const CFileItem& file, const CPlayerOptions &options) override;
+ bool CloseFile(bool reopen = false) override;
+ bool IsPlaying() const override;
+ void Pause() override;
+ bool HasVideo() const override;
+ bool HasAudio() const override;
+ bool HasRDS() const override;
+ bool HasID3() const override;
+ bool IsPassthrough() const override;
+ bool CanSeek() const override;
+ void Seek(bool bPlus, bool bLargeStep, bool bChapterOverride) override;
+ bool SeekScene(bool bPlus = true) override;
+ void SeekPercentage(float iPercent) override;
+ float GetCachePercentage() const override;
+
+ void SetDynamicRangeCompression(long drc) override;
+ bool CanPause() const override;
+ void SetAVDelay(float fValue = 0.0f) override;
+ float GetAVDelay() override;
+ bool IsInMenu() const override;
+
+ /*!
+ * \brief Get the supported menu type
+ * \return The supported menu type
+ */
+ MenuType GetSupportedMenuType() const override;
+
+ void SetSubTitleDelay(float fValue = 0.0f) override;
+ float GetSubTitleDelay() override;
+ int GetSubtitleCount() const override;
+ int GetSubtitle() override;
+ void GetSubtitleStreamInfo(int index, SubtitleStreamInfo& info) const override;
+ void SetSubtitle(int iStream) override;
+ bool GetSubtitleVisible() const override;
+ void SetSubtitleVisible(bool bVisible) override;
+
+ /*!
+ * \brief Set the subtitle vertical position,
+ * it depends on current screen resolution
+ * \param value The subtitle position in pixels
+ * \param save If true, the value will be saved to resolution info
+ */
+ void SetSubtitleVerticalPosition(const int value, bool save) override;
+
+ void AddSubtitle(const std::string& strSubPath) override;
+
+ int GetAudioStreamCount() const override;
+ int GetAudioStream() override;
+ void SetAudioStream(int iStream) override;
+
+ int GetVideoStream() const override;
+ int GetVideoStreamCount() const override;
+ void GetVideoStreamInfo(int streamId, VideoStreamInfo& info) const override;
+ void SetVideoStream(int iStream) override;
+
+ int GetPrograms(std::vector<ProgramInfo>& programs) override;
+ void SetProgram(int progId) override;
+ int GetProgramsCount() const override;
+
+ std::shared_ptr<TextCacheStruct_t> GetTeletextCache() override;
+ bool HasTeletextCache() const override;
+ void LoadPage(int p, int sp, unsigned char* buffer) override;
+
+ int GetChapterCount() const override;
+ int GetChapter() const override;
+ void GetChapterName(std::string& strChapterName, int chapterIdx = -1) const override;
+ int64_t GetChapterPos(int chapterIdx = -1) const override;
+ int SeekChapter(int iChapter) override;
+
+ void SeekTime(int64_t iTime) override;
+ bool SeekTimeRelative(int64_t iTime) override;
+ void SetSpeed(float speed) override;
+ void SetTempo(float tempo) override;
+ bool SupportsTempo() const override;
+ void FrameAdvance(int frames) override;
+ bool OnAction(const CAction &action) override;
+
+ void GetAudioStreamInfo(int index, AudioStreamInfo& info) const override;
+
+ std::string GetPlayerState() override;
+ bool SetPlayerState(const std::string& state) override;
+
+ void FrameMove() override;
+ void Render(bool clear, uint32_t alpha = 255, bool gui = true) override;
+ void FlushRenderer() override;
+ void SetRenderViewMode(int mode, float zoom, float par, float shift, bool stretch) override;
+ float GetRenderAspectRatio() const override;
+ void TriggerUpdateResolution() override;
+ bool IsRenderingVideo() const override;
+ bool Supports(EINTERLACEMETHOD method) const override;
+ EINTERLACEMETHOD GetDeinterlacingMethodDefault() const override;
+ bool Supports(ESCALINGMETHOD method) const override;
+ bool Supports(ERENDERFEATURE feature) const override;
+
+ unsigned int RenderCaptureAlloc() override;
+ void RenderCapture(unsigned int captureId, unsigned int width, unsigned int height, int flags) override;
+ void RenderCaptureRelease(unsigned int captureId) override;
+ bool RenderCaptureGetPixels(unsigned int captureId, unsigned int millis, uint8_t *buffer, unsigned int size) override;
+
+ // IDispResource interface
+ void OnLostDisplay() override;
+ void OnResetDisplay() override;
+
+ bool IsCaching() const override;
+ int GetCacheLevel() const override;
+
+ int OnDiscNavResult(void* pData, int iMessage) override;
+ void GetVideoResolution(unsigned int &width, unsigned int &height) override;
+
+ CVideoSettings GetVideoSettings() const override;
+ void SetVideoSettings(CVideoSettings& settings) override;
+
+ void SetUpdateStreamDetails();
+
+protected:
+ friend class CSelectionStreams;
+
+ void OnStartup() override;
+ void OnExit() override;
+ void Process() override;
+ void VideoParamsChange() override;
+ void GetDebugInfo(std::string &audio, std::string &video, std::string &general) override;
+ void UpdateClockSync(bool enabled) override;
+ void UpdateRenderInfo(CRenderInfo &info) override;
+ void UpdateRenderBuffers(int queued, int discard, int free) override;
+ void UpdateGuiRender(bool gui) override;
+ void UpdateVideoRender(bool video) override;
+
+ void CreatePlayers();
+ void DestroyPlayers();
+
+ void Prepare();
+ bool OpenStream(CCurrentStream& current, int64_t demuxerId, int iStream, int source, bool reset = true);
+ bool OpenAudioStream(CDVDStreamInfo& hint, bool reset = true);
+ bool OpenVideoStream(CDVDStreamInfo& hint, bool reset = true);
+ bool OpenSubtitleStream(const CDVDStreamInfo& hint);
+ bool OpenTeletextStream(CDVDStreamInfo& hint);
+ bool OpenRadioRDSStream(CDVDStreamInfo& hint);
+ bool OpenAudioID3Stream(CDVDStreamInfo& hint);
+
+ /** \brief Switches forced subtitles to forced subtitles matching the language of the current audio track.
+ * If these are not available, subtitles are disabled.
+ */
+ void AdaptForcedSubtitles();
+ bool CloseStream(CCurrentStream& current, bool bWaitForBuffers);
+
+ bool CheckIsCurrent(const CCurrentStream& current, CDemuxStream* stream, DemuxPacket* pkg);
+ void ProcessPacket(CDemuxStream* pStream, DemuxPacket* pPacket);
+ void ProcessAudioData(CDemuxStream* pStream, DemuxPacket* pPacket);
+ void ProcessVideoData(CDemuxStream* pStream, DemuxPacket* pPacket);
+ void ProcessSubData(CDemuxStream* pStream, DemuxPacket* pPacket);
+ void ProcessTeletextData(CDemuxStream* pStream, DemuxPacket* pPacket);
+ void ProcessRadioRDSData(CDemuxStream* pStream, DemuxPacket* pPacket);
+ void ProcessAudioID3Data(CDemuxStream* pStream, DemuxPacket* pPacket);
+
+ int AddSubtitleFile(const std::string& filename, const std::string& subfilename = "");
+ void SetSubtitleVisibleInternal(bool bVisible);
+
+ /**
+ * one of the DVD_PLAYSPEED defines
+ */
+ void SetPlaySpeed(int iSpeed);
+
+ enum ECacheState
+ {
+ CACHESTATE_DONE = 0,
+ CACHESTATE_FULL, // player is filling up the demux queue
+ CACHESTATE_INIT, // player is waiting for first packet of each stream
+ CACHESTATE_PLAY, // player is waiting for players to not be stalled
+ CACHESTATE_FLUSH, // temporary state player will choose startup between init or full
+ };
+
+ void SetCaching(ECacheState state);
+
+ double GetQueueTime();
+ bool GetCachingTimes(double& play_left, double& cache_left, double& file_offset);
+
+ void FlushBuffers(double pts, bool accurate, bool sync);
+
+ void HandleMessages();
+ void HandlePlaySpeed();
+ bool IsInMenuInternal() const;
+ void SynchronizeDemuxer();
+ void CheckAutoSceneSkip();
+ bool CheckContinuity(CCurrentStream& current, DemuxPacket* pPacket);
+ bool CheckSceneSkip(const CCurrentStream& current);
+ bool CheckPlayerInit(CCurrentStream& current);
+ void UpdateCorrection(DemuxPacket* pkt, double correction);
+ void UpdateTimestamps(CCurrentStream& current, DemuxPacket* pPacket);
+ IDVDStreamPlayer* GetStreamPlayer(unsigned int player);
+ void SendPlayerMessage(std::shared_ptr<CDVDMsg> pMsg, unsigned int target);
+
+ bool ReadPacket(DemuxPacket*& packet, CDemuxStream*& stream);
+ bool IsValidStream(const CCurrentStream& stream);
+ bool IsBetterStream(const CCurrentStream& current, CDemuxStream* stream);
+ void CheckBetterStream(CCurrentStream& current, CDemuxStream* stream);
+ void CheckStreamChanges(CCurrentStream& current, CDemuxStream* stream);
+
+ bool OpenInputStream();
+ bool OpenDemuxStream();
+ void CloseDemuxer();
+ void OpenDefaultStreams(bool reset = true);
+
+ void UpdatePlayState(double timeout);
+ void GetGeneralInfo(std::string& strVideoInfo);
+ int64_t GetUpdatedTime();
+ int64_t GetTime();
+ float GetPercentage();
+
+ void UpdateContent();
+ void UpdateContentState();
+
+ void UpdateFileItemStreamDetails(CFileItem& item);
+
+ bool m_players_created;
+
+ CFileItem m_item;
+ CPlayerOptions m_playerOptions;
+ bool m_bAbortRequest;
+ bool m_error;
+ bool m_bCloseRequest;
+
+ ECacheState m_caching;
+ XbmcThreads::EndTime<> m_cachingTimer;
+
+ std::unique_ptr<CProcessInfo> m_processInfo;
+
+ CCurrentStream m_CurrentAudio;
+ CCurrentStream m_CurrentVideo;
+ CCurrentStream m_CurrentSubtitle;
+ CCurrentStream m_CurrentTeletext;
+ CCurrentStream m_CurrentRadioRDS;
+ CCurrentStream m_CurrentAudioID3;
+
+ CSelectionStreams m_SelectionStreams;
+ std::vector<ProgramInfo> m_programs;
+
+ struct SContent
+ {
+ mutable CCriticalSection m_section;
+ CSelectionStreams m_selectionStreams;
+ std::vector<ProgramInfo> m_programs;
+ int m_videoIndex{-1};
+ int m_audioIndex{-1};
+ int m_subtitleIndex{-1};
+ } m_content;
+
+ int m_playSpeed;
+ int m_streamPlayerSpeed;
+ int m_demuxerSpeed = DVD_PLAYSPEED_NORMAL;
+ struct SSpeedState
+ {
+ double lastpts; // holds last display pts during ff/rw operations
+ int64_t lasttime;
+ double lastseekpts;
+ double lastabstime;
+ } m_SpeedState;
+
+ double m_offset_pts;
+
+ CDVDMessageQueue m_messenger;
+ std::unique_ptr<CJobQueue> m_outboundEvents;
+
+ IDVDStreamPlayerVideo *m_VideoPlayerVideo;
+ IDVDStreamPlayerAudio *m_VideoPlayerAudio;
+ CVideoPlayerSubtitle *m_VideoPlayerSubtitle;
+ CDVDTeletextData *m_VideoPlayerTeletext;
+ CDVDRadioRDSData *m_VideoPlayerRadioRDS;
+ std::unique_ptr<CVideoPlayerAudioID3> m_VideoPlayerAudioID3;
+
+ CDVDClock m_clock;
+ CDVDOverlayContainer m_overlayContainer;
+
+ std::shared_ptr<CDVDInputStream> m_pInputStream;
+ std::unique_ptr<CDVDDemux> m_pDemuxer;
+ std::shared_ptr<CDVDDemux> m_pSubtitleDemuxer;
+ std::unordered_map<int64_t, std::shared_ptr<CDVDDemux>> m_subtitleDemuxerMap;
+ std::unique_ptr<CDVDDemuxCC> m_pCCDemuxer;
+
+ CRenderManager m_renderManager;
+
+ struct SDVDInfo
+ {
+ void Clear()
+ {
+ state = DVDSTATE_NORMAL;
+ iSelectedSPUStream = -1;
+ iSelectedAudioStream = -1;
+ iSelectedVideoStream = -1;
+ iDVDStillTime = std::chrono::milliseconds::zero();
+ iDVDStillStartTime = {};
+ syncClock = false;
+ }
+
+ int state; // current dvdstate
+ bool syncClock;
+ std::chrono::milliseconds
+ iDVDStillTime; // total time in ticks we should display the still before continuing
+ std::chrono::time_point<std::chrono::steady_clock>
+ iDVDStillStartTime; // time in ticks when we started the still
+ int iSelectedSPUStream; // mpeg stream id, or -1 if disabled
+ int iSelectedAudioStream; // mpeg stream id, or -1 if disabled
+ int iSelectedVideoStream; // mpeg stream id or angle, -1 if disabled
+ } m_dvd;
+
+ SPlayerState m_State;
+ mutable CCriticalSection m_StateSection;
+ XbmcThreads::EndTime<> m_syncTimer;
+
+ CEdl m_Edl;
+ bool m_SkipCommercials;
+
+ bool m_HasVideo;
+ bool m_HasAudio;
+
+ bool m_UpdateStreamDetails;
+
+ std::atomic<bool> m_displayLost;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerAudio.cpp b/xbmc/cores/VideoPlayer/VideoPlayerAudio.cpp
new file mode 100644
index 0000000..6ab5b5d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerAudio.cpp
@@ -0,0 +1,713 @@
+/*
+ * 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 "VideoPlayerAudio.h"
+
+#include "DVDCodecs/Audio/DVDAudioCodec.h"
+#include "DVDCodecs/DVDFactoryCodec.h"
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/MathUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#ifdef TARGET_RASPBERRY_PI
+#include "platform/linux/RBP.h"
+#endif
+
+#include <sstream>
+#include <iomanip>
+#include <math.h>
+
+using namespace std::chrono_literals;
+
+class CDVDMsgAudioCodecChange : public CDVDMsg
+{
+public:
+ CDVDMsgAudioCodecChange(const CDVDStreamInfo& hints, std::unique_ptr<CDVDAudioCodec> codec)
+ : CDVDMsg(GENERAL_STREAMCHANGE), m_codec(std::move(codec)), m_hints(hints)
+ {}
+ ~CDVDMsgAudioCodecChange() override = default;
+
+ std::unique_ptr<CDVDAudioCodec> m_codec;
+ CDVDStreamInfo m_hints;
+};
+
+
+CVideoPlayerAudio::CVideoPlayerAudio(CDVDClock* pClock, CDVDMessageQueue& parent, CProcessInfo &processInfo)
+: CThread("VideoPlayerAudio"), IDVDStreamPlayerAudio(processInfo)
+, m_messageQueue("audio")
+, m_messageParent(parent)
+, m_audioSink(pClock)
+{
+ m_pClock = pClock;
+ m_audioClock = 0;
+ m_speed = DVD_PLAYSPEED_NORMAL;
+ m_stalled = true;
+ m_paused = false;
+ m_syncState = IDVDStreamPlayer::SYNC_STARTING;
+ m_synctype = SYNC_DISCON;
+ m_prevsynctype = -1;
+ m_prevskipped = false;
+ m_maxspeedadjust = 0.0;
+
+ m_messageQueue.SetMaxDataSize(6 * 1024 * 1024);
+ m_messageQueue.SetMaxTimeSize(8.0);
+ m_disconAdjustTimeMs = processInfo.GetMaxPassthroughOffSyncDuration();
+}
+
+CVideoPlayerAudio::~CVideoPlayerAudio()
+{
+ StopThread();
+
+ // close the stream, and don't wait for the audio to be finished
+ // CloseStream(true);
+}
+
+bool CVideoPlayerAudio::OpenStream(CDVDStreamInfo hints)
+{
+ CLog::Log(LOGINFO, "Finding audio codec for: {}", hints.codec);
+ bool allowpassthrough = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEDISPLAYASCLOCK);
+ if (m_processInfo.IsRealtimeStream())
+ allowpassthrough = false;
+
+ CAEStreamInfo::DataType streamType =
+ m_audioSink.GetPassthroughStreamType(hints.codec, hints.samplerate, hints.profile);
+ std::unique_ptr<CDVDAudioCodec> codec = CDVDFactoryCodec::CreateAudioCodec(
+ hints, m_processInfo, allowpassthrough, m_processInfo.AllowDTSHDDecode(), streamType);
+ if(!codec)
+ {
+ CLog::Log(LOGERROR, "Unsupported audio codec");
+ return false;
+ }
+
+ if(m_messageQueue.IsInited())
+ m_messageQueue.Put(std::make_shared<CDVDMsgAudioCodecChange>(hints, std::move(codec)), 0);
+ else
+ {
+ OpenStream(hints, std::move(codec));
+ m_messageQueue.Init();
+ CLog::Log(LOGINFO, "Creating audio thread");
+ Create();
+ }
+ return true;
+}
+
+void CVideoPlayerAudio::OpenStream(CDVDStreamInfo& hints, std::unique_ptr<CDVDAudioCodec> codec)
+{
+ m_pAudioCodec = std::move(codec);
+
+ m_processInfo.ResetAudioCodecInfo();
+
+ /* store our stream hints */
+ m_streaminfo = hints;
+
+ /* update codec information from what codec gave out, if any */
+ int channelsFromCodec = m_pAudioCodec->GetFormat().m_channelLayout.Count();
+ int samplerateFromCodec = m_pAudioCodec->GetFormat().m_sampleRate;
+
+ if (channelsFromCodec > 0)
+ m_streaminfo.channels = channelsFromCodec;
+ if (samplerateFromCodec > 0)
+ m_streaminfo.samplerate = samplerateFromCodec;
+
+ /* check if we only just got sample rate, in which case the previous call
+ * to CreateAudioCodec() couldn't have started passthrough */
+ if (hints.samplerate != m_streaminfo.samplerate)
+ SwitchCodecIfNeeded();
+
+ m_audioClock = 0;
+ m_stalled = m_messageQueue.GetPacketCount(CDVDMsg::DEMUXER_PACKET) == 0;
+
+ m_prevsynctype = -1;
+ m_synctype = SYNC_DISCON;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEDISPLAYASCLOCK))
+ m_synctype = SYNC_RESAMPLE;
+ else if (m_processInfo.IsRealtimeStream())
+ m_synctype = SYNC_RESAMPLE;
+
+ if (m_synctype == SYNC_DISCON)
+ CLog::LogF(LOGINFO, "Allowing max Out-Of-Sync Value of {} ms", m_disconAdjustTimeMs);
+
+ m_prevskipped = false;
+
+ m_maxspeedadjust = 5.0;
+
+ m_messageParent.Put(std::make_shared<CDVDMsg>(CDVDMsg::PLAYER_AVCHANGE));
+ m_syncState = IDVDStreamPlayer::SYNC_STARTING;
+}
+
+void CVideoPlayerAudio::CloseStream(bool bWaitForBuffers)
+{
+ bool bWait = bWaitForBuffers && m_speed > 0 && !CServiceBroker::GetActiveAE()->IsSuspended();
+
+ // wait until buffers are empty
+ if (bWait)
+ m_messageQueue.WaitUntilEmpty();
+
+ // send abort message to the audio queue
+ m_messageQueue.Abort();
+
+ CLog::Log(LOGINFO, "Waiting for audio thread to exit");
+
+ // shut down the adio_decode thread and wait for it
+ StopThread(); // will set this->m_bStop to true
+
+ // destroy audio device
+ CLog::Log(LOGINFO, "Closing audio device");
+ if (bWait)
+ {
+ m_bStop = false;
+ m_audioSink.Drain();
+ m_bStop = true;
+ }
+ else
+ {
+ m_audioSink.Flush();
+ }
+
+ m_audioSink.Destroy(true);
+
+ // uninit queue
+ m_messageQueue.End();
+
+ CLog::Log(LOGINFO, "Deleting audio codec");
+ if (m_pAudioCodec)
+ {
+ m_pAudioCodec->Dispose();
+ m_pAudioCodec.reset();
+ }
+}
+
+void CVideoPlayerAudio::OnStartup()
+{
+}
+
+void CVideoPlayerAudio::UpdatePlayerInfo()
+{
+ std::ostringstream s;
+ s << "aq:" << std::setw(2) << std::min(99,m_messageQueue.GetLevel()) << "%";
+ s << ", Kb/s:" << std::fixed << std::setprecision(2) << m_audioStats.GetBitrate() / 1024.0;
+
+ // print a/v discontinuity adjustments counter when audio is not resampled (passthrough mode)
+ if (m_synctype == SYNC_DISCON)
+ s << ", a/v corrections (" << m_disconAdjustTimeMs << "ms): " << m_disconAdjustCounter;
+
+ //print the inverse of the resample ratio, since that makes more sense
+ //if the resample ratio is 0.5, then we're playing twice as fast
+ else if (m_synctype == SYNC_RESAMPLE)
+ s << ", rr:" << std::fixed << std::setprecision(5) << 1.0 / m_audioSink.GetResampleRatio();
+
+ SInfo info;
+ info.info = s.str();
+ info.pts = m_audioSink.GetPlayingPts();
+ info.passthrough = m_pAudioCodec && m_pAudioCodec->NeedPassthrough();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_info_section);
+ m_info = info;
+ }
+}
+
+void CVideoPlayerAudio::Process()
+{
+ CLog::Log(LOGINFO, "running thread: CVideoPlayerAudio::Process()");
+
+ DVDAudioFrame audioframe;
+ audioframe.nb_frames = 0;
+ audioframe.framesOut = 0;
+ m_audioStats.Start();
+ m_disconAdjustCounter = 0;
+
+ // Only enable "learning" if advancedsettings m_maxPassthroughOffSyncDuration
+ // not exists or has it's default 10 ms value, otherwise use advancedsettings value
+ if (m_disconAdjustTimeMs == 10)
+ {
+ m_disconTimer.Set(30s);
+ m_disconLearning = true;
+ }
+ else
+ {
+ m_disconLearning = false;
+ }
+
+ bool onlyPrioMsgs = false;
+
+ while (!m_bStop)
+ {
+ std::shared_ptr<CDVDMsg> pMsg;
+ int timeout = (int)(1000 * m_audioSink.GetCacheTime());
+
+ // read next packet and return -1 on error
+ int priority = 1;
+ //Do we want a new audio frame?
+ if (m_syncState == IDVDStreamPlayer::SYNC_STARTING || /* when not started */
+ m_processInfo.IsTempoAllowed(static_cast<float>(m_speed)/DVD_PLAYSPEED_NORMAL) ||
+ m_speed < DVD_PLAYSPEED_PAUSE || /* when rewinding */
+ (m_speed > DVD_PLAYSPEED_NORMAL && m_audioClock < m_pClock->GetClock())) /* when behind clock in ff */
+ priority = 0;
+
+ if (m_syncState == IDVDStreamPlayer::SYNC_WAITSYNC)
+ priority = 1;
+
+ if (m_paused)
+ priority = 1;
+
+ if (onlyPrioMsgs)
+ {
+ priority = 1;
+ timeout = 0;
+ }
+
+ MsgQueueReturnCode ret = m_messageQueue.Get(pMsg, timeout, priority);
+
+ onlyPrioMsgs = false;
+
+ if (MSGQ_IS_ERROR(ret))
+ {
+ if (!m_messageQueue.ReceivedAbortRequest())
+ CLog::Log(LOGERROR, "MSGQ_IS_ERROR returned true ({})", ret);
+
+ break;
+ }
+ else if (ret == MSGQ_TIMEOUT)
+ {
+ if (ProcessDecoderOutput(audioframe))
+ {
+ onlyPrioMsgs = true;
+ continue;
+ }
+
+ // if we only wanted priority messages, this isn't a stall
+ if (priority)
+ continue;
+
+ if (m_processInfo.IsTempoAllowed(static_cast<float>(m_speed)/DVD_PLAYSPEED_NORMAL) &&
+ !m_stalled && m_syncState == IDVDStreamPlayer::SYNC_INSYNC)
+ {
+ // while AE sync is active, we still have time to fill buffers
+ if (m_syncTimer.IsTimePast())
+ {
+ CLog::Log(LOGINFO, "CVideoPlayerAudio::Process - stream stalled");
+ m_stalled = true;
+ }
+ }
+ if (timeout == 0)
+ CThread::Sleep(10ms);
+
+ continue;
+ }
+
+ // handle messages
+ if (pMsg->IsType(CDVDMsg::GENERAL_SYNCHRONIZE))
+ {
+ if (std::static_pointer_cast<CDVDMsgGeneralSynchronize>(pMsg)->Wait(100ms, SYNCSOURCE_AUDIO))
+ CLog::Log(LOGDEBUG, "CVideoPlayerAudio - CDVDMsg::GENERAL_SYNCHRONIZE");
+ else
+ m_messageQueue.Put(pMsg, 1); // push back as prio message, to process other prio messages
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_RESYNC))
+ { //player asked us to set internal clock
+ double pts = std::static_pointer_cast<CDVDMsgDouble>(pMsg)->m_value;
+ CLog::Log(LOGDEBUG,
+ "CVideoPlayerAudio - CDVDMsg::GENERAL_RESYNC({:f}), level: {}, cache: {:f}", pts,
+ m_messageQueue.GetLevel(), m_audioSink.GetDelay());
+
+ double delay = m_audioSink.GetDelay();
+ if (pts > m_audioClock - delay + 0.5 * DVD_TIME_BASE)
+ {
+ m_audioSink.Flush();
+ }
+ m_audioClock = pts + delay;
+ if (m_speed != DVD_PLAYSPEED_PAUSE)
+ m_audioSink.Resume();
+ m_syncState = IDVDStreamPlayer::SYNC_INSYNC;
+ m_syncTimer.Set(3000ms);
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_RESET))
+ {
+ if (m_pAudioCodec)
+ m_pAudioCodec->Reset();
+ m_audioSink.Flush();
+ m_stalled = true;
+ m_audioClock = 0;
+ audioframe.nb_frames = 0;
+ m_syncState = IDVDStreamPlayer::SYNC_STARTING;
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_FLUSH))
+ {
+ bool sync = std::static_pointer_cast<CDVDMsgBool>(pMsg)->m_value;
+ m_audioSink.Flush();
+ m_stalled = true;
+ m_audioClock = 0;
+ audioframe.nb_frames = 0;
+
+ if (sync)
+ {
+ m_syncState = IDVDStreamPlayer::SYNC_STARTING;
+ m_audioSink.Pause();
+ }
+
+ if (m_pAudioCodec)
+ m_pAudioCodec->Reset();
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_EOF))
+ {
+ CLog::Log(LOGDEBUG, "CVideoPlayerAudio - CDVDMsg::GENERAL_EOF");
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SETSPEED))
+ {
+ double speed = std::static_pointer_cast<CDVDMsgInt>(pMsg)->m_value;
+
+ if (m_processInfo.IsTempoAllowed(static_cast<float>(speed)/DVD_PLAYSPEED_NORMAL))
+ {
+ if (speed != m_speed)
+ {
+ if (m_syncState == IDVDStreamPlayer::SYNC_INSYNC)
+ {
+ m_audioSink.Resume();
+ m_stalled = false;
+ }
+ }
+ }
+ else
+ {
+ m_audioSink.Pause();
+ }
+ m_speed = (int)speed;
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_STREAMCHANGE))
+ {
+ auto msg = std::static_pointer_cast<CDVDMsgAudioCodecChange>(pMsg);
+ OpenStream(msg->m_hints, std::move(msg->m_codec));
+ msg->m_codec = NULL;
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_PAUSE))
+ {
+ m_paused = std::static_pointer_cast<CDVDMsgBool>(pMsg)->m_value;
+ CLog::Log(LOGDEBUG, "CVideoPlayerAudio - CDVDMsg::GENERAL_PAUSE: {}", m_paused);
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_REQUEST_STATE))
+ {
+ SStateMsg msg;
+ msg.player = VideoPlayer_AUDIO;
+ msg.syncState = m_syncState;
+ m_messageParent.Put(
+ std::make_shared<CDVDMsgType<SStateMsg>>(CDVDMsg::PLAYER_REPORT_STATE, msg));
+ }
+ else if (pMsg->IsType(CDVDMsg::DEMUXER_PACKET))
+ {
+ DemuxPacket* pPacket = std::static_pointer_cast<CDVDMsgDemuxerPacket>(pMsg)->GetPacket();
+ bool bPacketDrop = std::static_pointer_cast<CDVDMsgDemuxerPacket>(pMsg)->GetPacketDrop();
+
+ if (bPacketDrop)
+ {
+ if (m_syncState != IDVDStreamPlayer::SYNC_STARTING)
+ {
+ m_audioSink.Drain();
+ m_audioSink.Flush();
+ audioframe.nb_frames = 0;
+ }
+ m_syncState = IDVDStreamPlayer::SYNC_STARTING;
+ continue;
+ }
+
+ if (!m_processInfo.IsTempoAllowed(static_cast<float>(m_speed) / DVD_PLAYSPEED_NORMAL) &&
+ m_syncState == IDVDStreamPlayer::SYNC_INSYNC)
+ {
+ continue;
+ }
+
+ if (!m_pAudioCodec->AddData(*pPacket))
+ {
+ m_messageQueue.PutBack(pMsg);
+ onlyPrioMsgs = true;
+ continue;
+ }
+
+ m_audioStats.AddSampleBytes(pPacket->iSize);
+ UpdatePlayerInfo();
+
+ if (ProcessDecoderOutput(audioframe))
+ {
+ onlyPrioMsgs = true;
+ }
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_DISPLAY_RESET))
+ {
+ m_displayReset = true;
+ }
+ }
+}
+
+bool CVideoPlayerAudio::ProcessDecoderOutput(DVDAudioFrame &audioframe)
+{
+ if (audioframe.nb_frames <= audioframe.framesOut)
+ {
+ audioframe.hasDownmix = false;
+
+ m_pAudioCodec->GetData(audioframe);
+
+ if (audioframe.nb_frames == 0)
+ {
+ return false;
+ }
+
+ audioframe.hasTimestamp = true;
+ if (audioframe.pts == DVD_NOPTS_VALUE)
+ {
+ audioframe.pts = m_audioClock;
+ audioframe.hasTimestamp = false;
+ }
+ else
+ {
+ m_audioClock = audioframe.pts;
+ }
+
+ if (audioframe.format.m_sampleRate && m_streaminfo.samplerate != (int) audioframe.format.m_sampleRate)
+ {
+ // The sample rate has changed or we just got it for the first time
+ // for this stream. See if we should enable/disable passthrough due
+ // to it.
+ m_streaminfo.samplerate = audioframe.format.m_sampleRate;
+ if (SwitchCodecIfNeeded())
+ {
+ audioframe.nb_frames = 0;
+ return false;
+ }
+ }
+
+ // if stream switches to realtime, disable pass through
+ // or switch to resample
+ if (m_processInfo.IsRealtimeStream() && m_synctype != SYNC_RESAMPLE)
+ {
+ m_synctype = SYNC_RESAMPLE;
+ if (SwitchCodecIfNeeded())
+ {
+ audioframe.nb_frames = 0;
+ return false;
+ }
+ }
+
+ // Display reset event has occurred
+ // See if we should enable passthrough
+ if (m_displayReset)
+ {
+ if (SwitchCodecIfNeeded())
+ {
+ audioframe.nb_frames = 0;
+ return false;
+ }
+ }
+
+ // demuxer reads metatags that influence channel layout
+ if (m_streaminfo.codec == AV_CODEC_ID_FLAC && m_streaminfo.channellayout)
+ audioframe.format.m_channelLayout = CAEUtil::GetAEChannelLayout(m_streaminfo.channellayout);
+
+ // we have successfully decoded an audio frame, setup renderer to match
+ if (!m_audioSink.IsValidFormat(audioframe))
+ {
+ if (m_speed)
+ m_audioSink.Drain();
+
+ m_audioSink.Destroy(false);
+
+ if (!m_audioSink.Create(audioframe, m_streaminfo.codec, m_synctype == SYNC_RESAMPLE))
+ CLog::Log(LOGERROR, "{} - failed to create audio renderer", __FUNCTION__);
+
+ m_prevsynctype = -1;
+
+ if (m_syncState == IDVDStreamPlayer::SYNC_INSYNC)
+ m_audioSink.Resume();
+ }
+
+ m_audioSink.SetDynamicRangeCompression(
+ static_cast<long>(m_processInfo.GetVideoSettings().m_VolumeAmplification * 100));
+
+ SetSyncType(audioframe.passthrough);
+
+ // downmix
+ double clev = audioframe.hasDownmix ? audioframe.centerMixLevel : M_SQRT1_2;
+ double curDB = 20 * log10(clev);
+ audioframe.centerMixLevel = pow(10, (curDB + m_processInfo.GetVideoSettings().m_CenterMixLevel) / 20);
+ audioframe.hasDownmix = true;
+ }
+
+ if (m_synctype == SYNC_DISCON)
+ {
+ double syncerror = m_audioSink.GetSyncError();
+
+ if (m_disconLearning)
+ {
+ const double syncErr = std::abs(syncerror);
+ if (syncErr > DVD_MSEC_TO_TIME(m_disconAdjustTimeMs))
+ m_disconAdjustTimeMs = DVD_TIME_TO_MSEC(syncErr);
+ if (m_disconTimer.IsTimePast())
+ {
+ m_disconLearning = false;
+ m_disconAdjustTimeMs = (static_cast<double>(m_disconAdjustTimeMs) * 1.15) + 5.0;
+ if (m_disconAdjustTimeMs > 100) // sanity check
+ m_disconAdjustTimeMs = 100;
+
+ CLog::LogF(LOGINFO, "Changed max allowed Out-Of-Sync value to {} ms due self-learning",
+ m_disconAdjustTimeMs);
+ }
+ }
+ else if (std::abs(syncerror) > DVD_MSEC_TO_TIME(m_disconAdjustTimeMs))
+ {
+ double correction = m_pClock->ErrorAdjust(syncerror, "CVideoPlayerAudio::OutputPacket");
+ if (correction != 0)
+ {
+ m_audioSink.SetSyncErrorCorrection(-correction);
+ m_disconAdjustCounter++;
+ }
+ }
+ }
+
+ int framesOutput = m_audioSink.AddPackets(audioframe);
+
+ // guess next pts
+ m_audioClock += audioframe.duration * ((double)framesOutput / audioframe.nb_frames);
+
+ audioframe.framesOut += framesOutput;
+
+ // signal to our parent that we have initialized
+ if (m_syncState == IDVDStreamPlayer::SYNC_STARTING)
+ {
+ double cachetotal = m_audioSink.GetCacheTotal();
+ double cachetime = m_audioSink.GetCacheTime();
+ if (cachetime >= cachetotal * 0.75)
+ {
+ m_syncState = IDVDStreamPlayer::SYNC_WAITSYNC;
+ m_stalled = false;
+ SStartMsg msg;
+ msg.player = VideoPlayer_AUDIO;
+ msg.cachetotal = m_audioSink.GetMaxDelay() * DVD_TIME_BASE;
+ msg.cachetime = m_audioSink.GetDelay();
+ msg.timestamp = audioframe.hasTimestamp ? audioframe.pts : DVD_NOPTS_VALUE;
+ m_messageParent.Put(std::make_shared<CDVDMsgType<SStartMsg>>(CDVDMsg::PLAYER_STARTED, msg));
+
+ m_streaminfo.channels = audioframe.format.m_channelLayout.Count();
+ m_processInfo.SetAudioChannels(audioframe.format.m_channelLayout);
+ m_processInfo.SetAudioSampleRate(audioframe.format.m_sampleRate);
+ m_processInfo.SetAudioBitsPerSample(audioframe.bits_per_sample);
+ m_processInfo.SetAudioDecoderName(m_pAudioCodec->GetName());
+ m_messageParent.Put(std::make_shared<CDVDMsg>(CDVDMsg::PLAYER_AVCHANGE));
+ }
+ }
+
+ return true;
+}
+
+void CVideoPlayerAudio::SetSyncType(bool passthrough)
+{
+ if (passthrough && m_synctype == SYNC_RESAMPLE)
+ m_synctype = SYNC_DISCON;
+
+ //if SetMaxSpeedAdjust returns false, it means no video is played and we need to use clock feedback
+ double maxspeedadjust = 0.0;
+ if (m_synctype == SYNC_RESAMPLE)
+ maxspeedadjust = m_maxspeedadjust;
+
+ m_pClock->SetMaxSpeedAdjust(maxspeedadjust);
+
+ if (m_synctype != m_prevsynctype)
+ {
+ const char *synctypes[] = {"clock feedback", "resample", "invalid"};
+ int synctype = (m_synctype >= 0 && m_synctype <= 1) ? m_synctype : 2;
+ CLog::Log(LOGDEBUG, "CVideoPlayerAudio:: synctype set to {}: {}", m_synctype,
+ synctypes[synctype]);
+ m_prevsynctype = m_synctype;
+ if (m_synctype == SYNC_RESAMPLE)
+ m_audioSink.SetResampleMode(1);
+ else
+ m_audioSink.SetResampleMode(0);
+ }
+}
+
+void CVideoPlayerAudio::OnExit()
+{
+#ifdef TARGET_WINDOWS
+ CoUninitialize();
+#endif
+
+ CLog::Log(LOGINFO, "thread end: CVideoPlayerAudio::OnExit()");
+}
+
+void CVideoPlayerAudio::SetSpeed(int speed)
+{
+ if(m_messageQueue.IsInited())
+ m_messageQueue.Put(std::make_shared<CDVDMsgInt>(CDVDMsg::PLAYER_SETSPEED, speed), 1);
+ else
+ m_speed = speed;
+}
+
+void CVideoPlayerAudio::Flush(bool sync)
+{
+ m_messageQueue.Flush();
+ m_messageQueue.Put(std::make_shared<CDVDMsgBool>(CDVDMsg::GENERAL_FLUSH, sync), 1);
+
+ m_audioSink.AbortAddPackets();
+}
+
+bool CVideoPlayerAudio::AcceptsData() const
+{
+ bool full = m_messageQueue.IsFull();
+ return !full;
+}
+
+bool CVideoPlayerAudio::SwitchCodecIfNeeded()
+{
+ if (m_displayReset)
+ CLog::Log(LOGINFO, "CVideoPlayerAudio: display reset occurred, checking for passthrough");
+ else
+ CLog::Log(LOGDEBUG, "CVideoPlayerAudio: stream props changed, checking for passthrough");
+
+ m_displayReset = false;
+
+ bool allowpassthrough = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEDISPLAYASCLOCK);
+ if (m_processInfo.IsRealtimeStream() || m_synctype == SYNC_RESAMPLE)
+ allowpassthrough = false;
+
+ CAEStreamInfo::DataType streamType = m_audioSink.GetPassthroughStreamType(
+ m_streaminfo.codec, m_streaminfo.samplerate, m_streaminfo.profile);
+ std::unique_ptr<CDVDAudioCodec> codec = CDVDFactoryCodec::CreateAudioCodec(
+ m_streaminfo, m_processInfo, allowpassthrough, m_processInfo.AllowDTSHDDecode(), streamType);
+
+ if (!codec || codec->NeedPassthrough() == m_pAudioCodec->NeedPassthrough())
+ {
+ // passthrough state has not changed
+ return false;
+ }
+
+ m_pAudioCodec = std::move(codec);
+
+ return true;
+}
+
+std::string CVideoPlayerAudio::GetPlayerInfo()
+{
+ std::unique_lock<CCriticalSection> lock(m_info_section);
+ return m_info.info;
+}
+
+int CVideoPlayerAudio::GetAudioChannels()
+{
+ return m_streaminfo.channels;
+}
+
+bool CVideoPlayerAudio::IsPassthrough() const
+{
+ std::unique_lock<CCriticalSection> lock(m_info_section);
+ return m_info.passthrough;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerAudio.h b/xbmc/cores/VideoPlayer/VideoPlayerAudio.h
new file mode 100644
index 0000000..4ed12c5
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerAudio.h
@@ -0,0 +1,123 @@
+/*
+ * 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 "AudioSinkAE.h"
+#include "DVDClock.h"
+#include "DVDMessageQueue.h"
+#include "DVDStreamInfo.h"
+#include "IVideoPlayer.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "threads/SystemClock.h"
+#include "threads/Thread.h"
+#include "utils/BitstreamStats.h"
+
+#include <list>
+#include <mutex>
+#include <utility>
+
+
+class CVideoPlayer;
+class CDVDAudioCodec;
+class CDVDAudioCodec;
+
+class CVideoPlayerAudio : public CThread, public IDVDStreamPlayerAudio
+{
+public:
+ CVideoPlayerAudio(CDVDClock* pClock, CDVDMessageQueue& parent, CProcessInfo &processInfo);
+ ~CVideoPlayerAudio() override;
+
+ bool OpenStream(CDVDStreamInfo hints) override;
+ void CloseStream(bool bWaitForBuffers) override;
+
+ void SetSpeed(int speed) override;
+ void Flush(bool sync) override;
+
+ // waits until all available data has been rendered
+ bool AcceptsData() const override;
+ bool HasData() const override { return m_messageQueue.GetDataSize() > 0; }
+ int GetLevel() const override { return m_messageQueue.GetLevel(); }
+ bool IsInited() const override { return m_messageQueue.IsInited(); }
+ void SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority = 0) override
+ {
+ m_messageQueue.Put(pMsg, priority);
+ }
+ void FlushMessages() override { m_messageQueue.Flush(); }
+
+ void SetDynamicRangeCompression(long drc) override { m_audioSink.SetDynamicRangeCompression(drc); }
+ float GetDynamicRangeAmplification() const override { return 0.0f; }
+
+ std::string GetPlayerInfo() override;
+ int GetAudioChannels() override;
+
+ double GetCurrentPts() override
+ {
+ std::unique_lock<CCriticalSection> lock(m_info_section);
+ return m_info.pts;
+ }
+
+ bool IsStalled() const override { return m_stalled; }
+ bool IsPassthrough() const override;
+
+protected:
+
+ void OnStartup() override;
+ void OnExit() override;
+ void Process() override;
+
+ bool ProcessDecoderOutput(DVDAudioFrame &audioframe);
+ void UpdatePlayerInfo();
+ void OpenStream(CDVDStreamInfo& hints, std::unique_ptr<CDVDAudioCodec> codec);
+ //! Switch codec if needed. Called when the sample rate gotten from the
+ //! codec changes, in which case we may want to switch passthrough on/off.
+ bool SwitchCodecIfNeeded();
+ void SetSyncType(bool passthrough);
+
+ CDVDMessageQueue m_messageQueue;
+ CDVDMessageQueue& m_messageParent;
+
+ // holds stream information for current playing stream
+ CDVDStreamInfo m_streaminfo;
+
+ double m_audioClock;
+
+ CAudioSinkAE m_audioSink; // audio output device
+ CDVDClock* m_pClock; // dvd master clock
+ std::unique_ptr<CDVDAudioCodec> m_pAudioCodec; // audio codec
+ BitstreamStats m_audioStats;
+
+ int m_speed;
+ bool m_stalled;
+ bool m_paused;
+ IDVDStreamPlayer::ESyncState m_syncState;
+ XbmcThreads::EndTime<> m_syncTimer;
+
+ int m_synctype;
+ int m_prevsynctype;
+
+ bool m_prevskipped;
+ double m_maxspeedadjust;
+
+ struct SInfo
+ {
+ std::string info;
+ double pts = DVD_NOPTS_VALUE;
+ bool passthrough = false;
+ };
+
+ mutable CCriticalSection m_info_section;
+ SInfo m_info;
+
+ bool m_displayReset = false;
+ unsigned int m_disconAdjustTimeMs = 10; // maximum sync-off before adjusting
+ int m_disconAdjustCounter = 0;
+ XbmcThreads::EndTime<> m_disconTimer;
+ bool m_disconLearning = false;
+};
+
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerAudioID3.cpp b/xbmc/cores/VideoPlayer/VideoPlayerAudioID3.cpp
new file mode 100644
index 0000000..b4db5bf
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerAudioID3.cpp
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2005-2022 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 "VideoPlayerAudioID3.h"
+
+#include "DVDStreamInfo.h"
+#include "GUIInfoManager.h"
+#include "Interface/DemuxPacket.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "guilib/GUIComponent.h"
+#include "music/tags/MusicInfoTag.h"
+#include "utils/log.h"
+
+#include <taglib/attachedpictureframe.h>
+#include <taglib/commentsframe.h>
+#include <taglib/id3v1genres.h>
+#include <taglib/id3v2framefactory.h>
+#include <taglib/mpegfile.h>
+#include <taglib/tbytevectorstream.h>
+#include <taglib/textidentificationframe.h>
+
+using namespace TagLib;
+
+CVideoPlayerAudioID3::CVideoPlayerAudioID3(CProcessInfo& processInfo)
+ : IDVDStreamPlayer(processInfo), CThread("VideoPlayerAudioID3"), m_messageQueue("id3")
+{
+ CLog::Log(LOGDEBUG, "Audio ID3 tag processor - new {}", __FUNCTION__);
+}
+
+CVideoPlayerAudioID3::~CVideoPlayerAudioID3()
+{
+ CLog::Log(LOGDEBUG, "Audio ID3 tag processor - delete {}", __FUNCTION__);
+ StopThread();
+}
+
+bool CVideoPlayerAudioID3::CheckStream(const CDVDStreamInfo& hints)
+{
+ return hints.type == STREAM_AUDIO_ID3;
+}
+
+void CVideoPlayerAudioID3::Flush()
+{
+ if (m_messageQueue.IsInited())
+ {
+ m_messageQueue.Flush();
+ m_messageQueue.Put(std::make_shared<CDVDMsg>(CDVDMsg::GENERAL_FLUSH));
+ }
+}
+
+void CVideoPlayerAudioID3::WaitForBuffers()
+{
+ m_messageQueue.WaitUntilEmpty();
+}
+
+bool CVideoPlayerAudioID3::OpenStream(CDVDStreamInfo hints)
+{
+ CloseStream(true);
+ m_messageQueue.Init();
+
+ if (hints.type == STREAM_AUDIO_ID3)
+ {
+ Flush();
+ CLog::Log(LOGINFO, "Creating Audio ID3 tag processor data thread");
+ Create();
+ return true;
+ }
+
+ return false;
+}
+
+void CVideoPlayerAudioID3::CloseStream(bool bWaitForBuffers)
+{
+ m_messageQueue.Abort();
+
+ CLog::Log(LOGINFO, "Audio ID3 tag processor - waiting for data thread to exit");
+ StopThread();
+
+ m_messageQueue.End();
+}
+
+void CVideoPlayerAudioID3::SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority)
+{
+ if (m_messageQueue.IsInited())
+ m_messageQueue.Put(pMsg, priority);
+}
+
+void CVideoPlayerAudioID3::FlushMessages()
+{
+ m_messageQueue.Flush();
+}
+
+bool CVideoPlayerAudioID3::IsInited() const
+{
+ return true;
+}
+
+bool CVideoPlayerAudioID3::AcceptsData() const
+{
+ return !m_messageQueue.IsFull();
+}
+
+bool CVideoPlayerAudioID3::IsStalled() const
+{
+ return true;
+}
+
+void CVideoPlayerAudioID3::OnExit()
+{
+ CLog::Log(LOGINFO, "Audio ID3 tag processor - thread end");
+}
+
+void CVideoPlayerAudioID3::Process()
+{
+ CLog::Log(LOGINFO, "Audio ID3 tag processor - running thread");
+
+ while (!m_bStop)
+ {
+ std::shared_ptr<CDVDMsg> pMsg;
+ int iPriority = (m_speed == DVD_PLAYSPEED_PAUSE) ? 1 : 0;
+ MsgQueueReturnCode ret = m_messageQueue.Get(pMsg, 2000, iPriority);
+
+ // Timeout for ID3 tag data is not a bad thing, so we continue without error
+ if (ret == MSGQ_TIMEOUT)
+ continue;
+
+ if (MSGQ_IS_ERROR(ret))
+ {
+ if (!m_messageQueue.ReceivedAbortRequest())
+ CLog::Log(LOGERROR, "MSGQ_IS_ERROR returned true ({})", ret);
+
+ break;
+ }
+
+ if (pMsg->IsType(CDVDMsg::DEMUXER_PACKET))
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ DemuxPacket* pPacket = std::static_pointer_cast<CDVDMsgDemuxerPacket>(pMsg)->GetPacket();
+ if (pPacket)
+ ProcessID3(pPacket->pData, pPacket->iSize);
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SETSPEED))
+ {
+ m_speed = std::static_pointer_cast<CDVDMsgInt>(pMsg)->m_value;
+ }
+ }
+}
+
+void CVideoPlayerAudioID3::ProcessID3(const unsigned char* data, unsigned int length) const
+{
+ if (data && length > 0)
+ {
+ ByteVectorStream tagStream(ByteVector(reinterpret_cast<const char*>(data), length));
+ if (tagStream.isOpen())
+ {
+ MPEG::File tagFile = MPEG::File(&tagStream, ID3v2::FrameFactory::instance());
+ if (tagFile.isOpen())
+ {
+ if (tagFile.hasID3v1Tag())
+ ProcessID3v1(tagFile.ID3v1Tag(false));
+
+ else if (tagFile.hasID3v2Tag())
+ ProcessID3v2(tagFile.ID3v2Tag(false));
+ }
+ }
+ }
+}
+
+void CVideoPlayerAudioID3::ProcessID3v1(const ID3v1::Tag* tag) const
+{
+ if (tag != nullptr && !tag->isEmpty())
+ {
+ MUSIC_INFO::CMusicInfoTag* currentMusic = g_application.CurrentFileItem().GetMusicInfoTag();
+ if (currentMusic)
+ {
+ bool changed = false;
+
+ const String title = tag->title();
+ if (!title.isEmpty())
+ {
+ currentMusic->SetTitle(title.to8Bit(true));
+ changed = true;
+ }
+
+ const String artist = tag->artist();
+ if (!artist.isEmpty())
+ {
+ currentMusic->SetArtist(artist.to8Bit(true));
+ changed = true;
+ }
+
+ const String album = tag->album();
+ if (!album.isEmpty())
+ {
+ currentMusic->SetAlbum(album.to8Bit(true));
+ changed = true;
+ }
+
+ const String comment = tag->comment();
+ if (!comment.isEmpty())
+ {
+ currentMusic->SetComment(comment.to8Bit(true));
+ changed = true;
+ }
+
+ const String genre = tag->genre();
+ if (!genre.isEmpty())
+ {
+ currentMusic->SetGenre(genre.to8Bit(true));
+ changed = true;
+ }
+
+ const unsigned int year = tag->year();
+ if (year != 0)
+ {
+ currentMusic->SetYear(year);
+ changed = true;
+ }
+
+ const unsigned int track = tag->track();
+ if (track != 0)
+ {
+ currentMusic->SetTrackNumber(track);
+ changed = true;
+ }
+
+ if (changed)
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(g_application.CurrentFileItem());
+ }
+ }
+}
+
+void CVideoPlayerAudioID3::ProcessID3v2(const ID3v2::Tag* tag) const
+{
+ if (tag != nullptr && !tag->isEmpty())
+ {
+ MUSIC_INFO::CMusicInfoTag* currentMusic = g_application.CurrentFileItem().GetMusicInfoTag();
+ if (currentMusic)
+ {
+ bool changed = false;
+
+ const ID3v2::FrameListMap& frameListMap = tag->frameListMap();
+ for (const auto& it : frameListMap)
+ {
+ if (!it.second.isEmpty())
+ {
+ if (it.first == "TIT2")
+ {
+ currentMusic->SetTitle(it.second.front()->toString().to8Bit(true));
+ changed = true;
+ }
+
+ else if (it.first == "TPE1")
+ {
+ currentMusic->SetArtist(GetID3v2StringList(it.second));
+ changed = true;
+ }
+
+ else if (it.first == "TALB")
+ {
+ currentMusic->SetAlbum(it.second.front()->toString().to8Bit(true));
+ changed = true;
+ }
+
+ else if (it.first == "COMM")
+ {
+ // Loop through and look for the main (no description) comment
+ for (const auto& ct : it.second)
+ {
+ auto commentsFrame = dynamic_cast<const ID3v2::CommentsFrame*>(ct);
+ if (commentsFrame && commentsFrame->description().isEmpty())
+ {
+ currentMusic->SetComment(commentsFrame->text().to8Bit(true));
+ changed = true;
+ break;
+ }
+ }
+ }
+
+ else if (it.first == "TCON")
+ {
+ currentMusic->SetGenre(GetID3v2StringList(it.second));
+ changed = true;
+ }
+
+ else if (it.first == "TYER")
+ {
+ currentMusic->SetYear(static_cast<int>(
+ strtol(it.second.front()->toString().toCString(true), nullptr, 10)));
+ changed = true;
+ }
+
+ else if (it.first == "TRCK")
+ {
+ currentMusic->SetTrackNumber(static_cast<int>(
+ strtol(it.second.front()->toString().toCString(true), nullptr, 10)));
+ changed = true;
+ }
+
+ // Support for setting the cover art image via CMusicInfoTag does not currently exist,
+ // the code sample below would check for an ID3v2 "APIC" tag of the proper type and
+ // convert the information into an EmbeddedArt object instance
+ //
+ // else if (it.first == "APIC")
+ // {
+ // // Loop through and look for the FrontCover picture frame
+ // for (const auto& pi : it.second)
+ // {
+ // auto pictureFrame = dynamic_cast<ID3v2::AttachedPictureFrame*>(pi);
+ // if (pictureFrame && pictureFrame->type() == ID3v2::AttachedPictureFrame::FrontCover)
+ // {
+ // EmbeddedArt coverArt(
+ // reinterpret_cast<const uint8_t*>(pictureFrame->picture().data()),
+ // pictureFrame->size(), pictureFrame->mimeType().to8Bit(true));
+
+ // // Assumes "void CMusicInfoTag::SetCoverArt(const EmbeddedArt& art)" exists
+ // currentMusic->SetCoverArt(coverArt);
+ // changed = true;
+ // }
+ // }
+ // }
+ }
+ }
+
+ if (changed)
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(g_application.CurrentFileItem());
+ }
+ }
+}
+
+std::vector<std::string> CVideoPlayerAudioID3::GetID3v2StringList(const ID3v2::FrameList& frameList)
+{
+ auto frame = dynamic_cast<const ID3v2::TextIdentificationFrame*>(frameList.front());
+ if (frame)
+ return StringListToVectorString(frame->fieldList());
+ return {};
+}
+
+std::vector<std::string> CVideoPlayerAudioID3::StringListToVectorString(
+ const StringList& stringList)
+{
+ std::vector<std::string> values;
+ for (const auto& value : stringList)
+ values.emplace_back(value.to8Bit(true));
+ return values;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerAudioID3.h b/xbmc/cores/VideoPlayer/VideoPlayerAudioID3.h
new file mode 100644
index 0000000..fe97a5e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerAudioID3.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2005-2022 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 "DVDMessageQueue.h"
+#include "IVideoPlayer.h"
+#include "Interface/TimingConstants.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <taglib/id3v1tag.h>
+#include <taglib/id3v2tag.h>
+#include <taglib/tstringlist.h>
+
+class CVideoPlayerAudioID3 : public IDVDStreamPlayer, private CThread
+{
+public:
+ explicit CVideoPlayerAudioID3(CProcessInfo& processInfo);
+ ~CVideoPlayerAudioID3() override;
+
+ bool CheckStream(const CDVDStreamInfo& hints);
+ void Flush();
+ void WaitForBuffers();
+
+ bool OpenStream(CDVDStreamInfo hints) override;
+ void CloseStream(bool bWaitForBuffers) override;
+ void SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority = 0) override;
+ void FlushMessages() override;
+ bool IsInited() const override;
+ bool AcceptsData() const override;
+ bool IsStalled() const override;
+
+protected:
+ void OnExit() override;
+ void Process() override;
+
+private:
+ void ProcessID3(const unsigned char* data, unsigned int length) const;
+ void ProcessID3v1(const TagLib::ID3v1::Tag* tag) const;
+ void ProcessID3v2(const TagLib::ID3v2::Tag* tag) const;
+
+ static std::vector<std::string> GetID3v2StringList(const TagLib::ID3v2::FrameList& frameList);
+ static std::vector<std::string> StringListToVectorString(const TagLib::StringList& stringList);
+
+ int m_speed = DVD_PLAYSPEED_NORMAL;
+ CCriticalSection m_critSection;
+ CDVDMessageQueue m_messageQueue;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerRadioRDS.cpp b/xbmc/cores/VideoPlayer/VideoPlayerRadioRDS.cpp
new file mode 100644
index 0000000..08963e7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerRadioRDS.cpp
@@ -0,0 +1,1678 @@
+/*
+ * 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.
+ */
+
+//#define RDS_IMPROVE_CHECK 1
+
+/*
+ * The RDS decoder bases partly on the source of the VDR radio plugin.
+ * http://www.egal-vdr.de/plugins/
+ * and reworked a bit with references from SPB 490, IEC62106
+ * and several other documents.
+ *
+ * A lot more information is sendet which is currently unused and partly
+ * not required.
+ */
+
+#include "VideoPlayerRadioRDS.h"
+
+#include "DVDStreamInfo.h"
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "Interface/DemuxPacket.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/AnnouncementManager.h"
+#include "music/tags/MusicInfoTag.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRRadioRDSInfoTag.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace XFILE;
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+/**
+ * Universal Encoder Communication Protocol (UECP)
+ * List of defined commands
+ * iaw.: SPB 490
+ */
+
+/// UECP Message element pointers (different on several commands)
+#define UECP_ME_MEC 0 // Message Element Code
+#define UECP_ME_DSN 1 // Data Set Number
+#define UECP_ME_PSN 2 // Program Service Number
+#define UECP_ME_MEL 3 // Message Element data Length
+#define UECP_ME_DATA 4 //
+
+/// RDS message commands
+#define UECP_RDS_PI 0x01 // Program Identification
+#define UECP_RDS_PS 0x02 // Program Service name
+#define UECP_RDS_PIN 0x06 // Program Item Number
+#define UECP_RDS_DI 0x04 // Decoder Identification and dynamic PTY indicator
+#define UECP_RDS_TA_TP 0x03 // Traffic Announcement identification / Traffic Program identification
+#define UECP_RDS_MS 0x05 // Music/Speech switch
+#define UECP_RDS_PTY 0x07 // Program TYpe
+#define UECP_RDS_PTYN 0x3A // Program TYpe Name
+#define UECP_RDS_RT 0x0A // RadioText
+#define UECP_RDS_AF 0x13 // Alternative Frequencies list
+#define UECP_RDS_EON_AF 0x14 // Enhanced Other Networks information
+#define UECP_SLOW_LABEL_CODES 0x1A // Slow Labeling codes
+#define UECP_LINKAGE_INFO 0x2E // Linkage information
+
+/// Open Data Application commands
+#define UECP_ODA_CONF_SHORT_MSG_CMD 0x40 // ODA configuration and short message command
+#define UECP_ODA_IDENT_GROUP_USAGE_SEQ 0x41 // ODA identification group usage sequence
+#define UECP_ODA_FREE_FORMAT_GROUP 0x42 // ODA free-format group
+#define UECP_ODA_REL_PRIOR_GROUP_SEQ 0x43 // ODA relative priority group sequence
+#define UECP_ODA_BURST_MODE_CONTROL 0x44 // ODA “Burst mode” control
+#define UECP_ODA_SPINN_WHEEL_TIMING_CTL 0x45 // ODA “Spinning Wheel” timing control
+#define UECP_ODA_DATA 0x46 // ODA Data
+#define UECP_ODA_DATA_CMD_ACCESS_RIGHT 0x47 // ODA data command access right
+
+/// DAB
+#define UECP_DAB_DYN_LABEL_CMD 0x48 // DAB: Dynamic Label command
+#define UECP_DAB_DYN_LABEL_MSG 0xAA // DAB: Dynamic Label message (DL)
+
+/// Transparent data commands
+#define UECP_TDC_TDC 0x26 // TDC
+#define UECP_TDC_EWS 0x2B // EWS
+#define UECP_TDC_IH 0x25 // IH
+#define UECP_TDC_TMC 0x30 // TMC
+#define UECP_TDC_FREE_FMT_GROUP 0x24 // Free-format group
+
+/// Paging commands
+#define UECP_PAGING_CALL_WITHOUT_MESSAGE 0x0C
+#define UECP_PAGING_CALL_NUMERIC_MESSAGE_10DIGITS 0x08
+#define UECP_PAGING_CALL_NUMERIC_MESSAGE_18DIGITS 0x20
+#define UECP_PAGING_CALL_ALPHANUMERIC_MESSAGE_80CHARACTERS 0x1B
+#define UECP_INTERNATIONAL_PAGING_NUMERIC_MESSAGE_15DIGITS 0x11
+#define UECP_INTERNATIONAL_PAGING_FUNCTIONS_MESSAGE 0x10
+#define UECP_TRANSMITTER_NETWORK_GROUP_DESIGNATION 0x12
+#define UECP_EPP_TM_INFO 0x31
+#define UECP_EPP_CALL_WITHOUT_ADDITIONAL_MESSAGE 0x32
+#define UECP_EPP_NATIONAL_INTERNATIONAL_CALL_ALPHANUMERIC_MESSAGE 0x33
+#define UECP_EPP_NATIONAL_INTERNATIONAL_CALL_VARIABLE_LENGTH_NUMERIC_MESSAGE 0x34
+#define UECP_EPP_NATIONAL_INTERNATIONAL_CALL_VARIABLE_LENGTH_FUNCTIONS_MESSAGE 0x35
+
+/// Clock setting and control
+#define UECP_CLOCK_RTC 0x0D // Real time clock
+#define UECP_CLOCK_RTC_CORR 0x09 // Real time clock correction
+#define UECP_CLOCK_CT_ON_OFF 0x19 // CT On/Off
+
+/// RDS adjustment and control
+#define RDS_ON_OFF 0x1E
+#define RDS_PHASE 0x22
+#define RDS_LEVEL 0x0E
+
+/// ARI adjustment and control
+#define UECP_ARI_ARI_ON_OFF 0x21
+#define UECP_ARI_ARI_AREA (BK) 0x0F
+#define UECP_ARI_ARI_LEVEL 0x1F
+
+/// Control and set up commands
+#define UECP_CTR_SITE_ADDRESS 0x23
+#define UECP_CTR_ENCODER_ADDRESS 0x27
+#define UECP_CTR_MAKE_PSN_LIST 0x28
+#define UECP_CTR_PSN_ENABLE_DISABLE 0x0B
+#define UECP_CTR_COMMUNICATION_MODE 0x2C
+#define UECP_CTR_TA_CONTROL 0x2A
+#define UECP_CTR_EON_TA_CONTROL 0x15
+#define UECP_CTR_REFERENCE_INPUT_SEL 0x1D
+#define UECP_CTR_DATA_SET_SELECT 0x1C // Data set select
+#define UECP_CTR_GROUP_SEQUENCE 0x16
+#define UECP_CTR_GROUP_VAR_CODE_SEQ 0x29
+#define UECP_CTR_EXTENDED_GROUP_SEQ 0x38
+#define UECP_CTR_PS_CHAR_CODE_TBL_SEL 0x2F
+#define UECP_CTR_ENCODER_ACCESS_RIGHT 0x3A
+#define UECP_CTR_COM_PORT_CONF_MODE 0x3B
+#define UECP_CTR_COM_PORT_CONF_SPEED 0x3C
+#define UECP_CTR_COM_PORT_CONF_TMEOUT 0x3D
+
+/// Other commands
+#define UECP_OTHER_RASS 0xda
+
+/// Bi-directional commands (Remote and configuration commands)
+#define BIDIR_MESSAGE_ACKNOWLEDGMENT 0x18
+#define BIDIR_REQUEST_MESSAGE 0x17
+
+/// Specific message commands
+#define SPEC_MFG_SPECIFIC_CMD 0x2D
+
+/**
+ * RDS and RBDS relevant
+ */
+
+/// RDS Program type id's
+enum {
+ RDS_PTY_NONE = 0,
+ RDS_PTY_NEWS,
+ RDS_PTY_CURRENT_AFFAIRS,
+ RDS_PTY_INFORMATION,
+ RDS_PTY_SPORT,
+ RDS_PTY_EDUCATION,
+ RDS_PTY_DRAMA,
+ RDS_PTY_CULTURE,
+ RDS_PTY_SCIENCE,
+ RDS_PTY_VARIED,
+ RDS_PTY_POP_MUSIC,
+ RDS_PTY_ROCK_MUSIC,
+ RDS_PTY_MOR_MUSIC,
+ RDS_PTY_LIGHT_CLASSICAL,
+ RDS_PTY_SERIOUS_CLASSICAL,
+ RDS_PTY_OTHER_MUSIC,
+ RDS_PTY_WEATHER,
+ RDS_PTY_FINANCE,
+ RDS_PTY_CHILDRENS_PROGRAMMES,
+ RDS_PTY_SOCIAL_AFFAIRS,
+ RDS_PTY_RELIGION,
+ RDS_PTY_PHONE_IN,
+ RDS_PTY_TRAVEL,
+ RDS_PTY_LEISURE,
+ RDS_PTY_JAZZ_MUSIC,
+ RDS_PTY_COUNTRY_MUSIC,
+ RDS_PTY_NATIONAL_MUSIC,
+ RDS_PTY_OLDIES_MUSIC,
+ RDS_PTY_FOLK_MUSIC,
+ RDS_PTY_DOCUMENTARY,
+ RDS_PTY_ALARM_TEST,
+ RDS_PTY_ALARM
+};
+
+/// RBDS Program type id's
+enum {
+ RBDS_PTY_NONE = 0,
+ RBDS_PTY_NEWS,
+ RBDS_PTY_INFORMATION,
+ RBDS_PTY_SPORT,
+ RBDS_PTY_TALK,
+ RBDS_PTY_ROCK_MUSIC,
+ RBDS_PTY_CLASSIC_ROCK_MUSIC,
+ RBDS_PTY_ADULT_HITS,
+ RBDS_PTY_SOFT_ROCK,
+ RBDS_PTY_TOP_40,
+ RBDS_PTY_COUNTRY,
+ RBDS_PTY_OLDIES,
+ RBDS_PTY_SOFT,
+ RBDS_PTY_NOSTALGIA,
+ RBDS_PTY_JAZZ,
+ RBDS_PTY_CLASSICAL,
+ RBDS_PTY_R__B,
+ RBDS_PTY_SOFT_R__B,
+ RBDS_PTY_LANGUAGE,
+ RBDS_PTY_RELIGIOUS_MUSIC,
+ RBDS_PTY_RELIGIOUS_TALK,
+ RBDS_PTY_PERSONALITY,
+ RBDS_PTY_PUBLIC,
+ RBDS_PTY_COLLEGE,
+ RBDS_PTY_WEATHER = 29,
+ RBDS_PTY_EMERGENCY_TEST,
+ RBDS_PTY_EMERGENCY
+};
+
+/// RadioText+ message type id's
+enum {
+ RTPLUS_DUMMY_CLASS = 0,
+
+ RTPLUS_ITEM_TITLE = 1,
+ RTPLUS_ITEM_ALBUM = 2,
+ RTPLUS_ITEM_TRACKNUMBER = 3,
+ RTPLUS_ITEM_ARTIST = 4,
+ RTPLUS_ITEM_COMPOSITION = 5,
+ RTPLUS_ITEM_MOVEMENT = 6,
+ RTPLUS_ITEM_CONDUCTOR = 7,
+ RTPLUS_ITEM_COMPOSER = 8,
+ RTPLUS_ITEM_BAND = 9,
+ RTPLUS_ITEM_COMMENT = 10,
+ RTPLUS_ITEM_GENRE = 11,
+
+ RTPLUS_INFO_NEWS = 12,
+ RTPLUS_INFO_NEWS_LOCAL = 13,
+ RTPLUS_INFO_STOCKMARKET = 14,
+ RTPLUS_INFO_SPORT = 15,
+ RTPLUS_INFO_LOTTERY = 16,
+ RTPLUS_INFO_HOROSCOPE = 17,
+ RTPLUS_INFO_DAILY_DIVERSION = 18,
+ RTPLUS_INFO_HEALTH = 19,
+ RTPLUS_INFO_EVENT = 20,
+ RTPLUS_INFO_SZENE = 21,
+ RTPLUS_INFO_CINEMA = 22,
+ RTPLUS_INFO_STUPIDITY_MACHINE = 23,
+ RTPLUS_INFO_DATE_TIME = 24,
+ RTPLUS_INFO_WEATHER = 25,
+ RTPLUS_INFO_TRAFFIC = 26,
+ RTPLUS_INFO_ALARM = 27,
+ RTPLUS_INFO_ADVERTISEMENT = 28,
+ RTPLUS_INFO_URL = 29,
+ RTPLUS_INFO_OTHER = 30,
+
+ RTPLUS_STATIONNAME_SHORT = 31,
+ RTPLUS_STATIONNAME_LONG = 32,
+
+ RTPLUS_PROGRAMME_NOW = 33,
+ RTPLUS_PROGRAMME_NEXT = 34,
+ RTPLUS_PROGRAMME_PART = 35,
+ RTPLUS_PROGRAMME_HOST = 36,
+ RTPLUS_PROGRAMME_EDITORIAL_STAFF = 37,
+ RTPLUS_PROGRAMME_FREQUENCY= 38,
+ RTPLUS_PROGRAMME_HOMEPAGE = 39,
+ RTPLUS_PROGRAMME_SUBCHANNEL = 40,
+
+ RTPLUS_PHONE_HOTLINE = 41,
+ RTPLUS_PHONE_STUDIO = 42,
+ RTPLUS_PHONE_OTHER = 43,
+
+ RTPLUS_SMS_STUDIO = 44,
+ RTPLUS_SMS_OTHER = 45,
+
+ RTPLUS_EMAIL_HOTLINE = 46,
+ RTPLUS_EMAIL_STUDIO = 47,
+ RTPLUS_EMAIL_OTHER = 48,
+
+ RTPLUS_MMS_OTHER = 49,
+
+ RTPLUS_CHAT = 50,
+ RTPLUS_CHAT_CENTER = 51,
+
+ RTPLUS_VOTE_QUESTION = 52,
+ RTPLUS_VOTE_CENTER = 53,
+
+ RTPLUS_PLACE = 59,
+ RTPLUS_APPOINTMENT = 60,
+ RTPLUS_IDENTIFIER = 61,
+ RTPLUS_PURCHASE = 62,
+ RTPLUS_GET_DATA = 63
+};
+
+/* page 71, Annex D, table D.1 in the standard and Annex N */
+static const char *piCountryCodes_A[15][7]=
+{
+ // 0 1 2 3 4 5 6
+ {"US","__","AI","BO","GT","__","__"}, // 1
+ {"US","__","AG","CO","HN","__","__"}, // 2
+ {"US","__","EC","JM","AW","__","__"}, // 3
+ {"US","__","FK","MQ","__","__","__"}, // 4
+ {"US","__","BB","GF","MS","__","__"}, // 5
+ {"US","__","BZ","PY","TT","__","__"}, // 6
+ {"US","__","KY","NI","PE","__","__"}, // 7
+ {"US","__","CR","__","SR","__","__"}, // 8
+ {"US","__","CU","PA","UY","__","__"}, // 9
+ {"US","__","AR","DM","KN","__","__"}, // A
+ {"US","CA","BR","DO","LC","MX","__"}, // B
+ {"__","CA","BM","CL","SV","VC","__"}, // C
+ {"US","CA","AN","GD","HT","MX","__"}, // D
+ {"US","CA","GP","TC","VE","MX","__"}, // E
+ {"__","GL","BS","GY","__","VG","PM"} // F
+};
+
+static const char *piCountryCodes_D[15][7]=
+{
+ // 0 1 2 3 4 5 6
+ {"CM","NA","SL","__","__","__","__"}, // 1
+ {"CF","LR","ZW","__","__","__","__"}, // 2
+ {"DJ","GH","MZ","EH","__","__","__"}, // 3
+ {"MG","MR","UG","xx","__","__","__"}, // 4
+ {"ML","ST","SZ","RW","__","__","__"}, // 5
+ {"AO","CV","KE","LS","__","__","__"}, // 6
+ {"GQ","SN","SO","__","__","__","__"}, // 7
+ {"GA","GM","NE","SC","__","__","__"}, // 8
+ {"GN","BI","TD","__","__","__","__"}, // 9
+ {"ZA","AC","GW","MU","__","__","__"}, // A
+ {"BF","BW","ZR","__","__","__","__"}, // B
+ {"CG","KM","CI","SD","__","__","__"}, // C
+ {"TG","TZ","Zanzibar","__","__","__","__"}, // D
+ {"BJ","ET","ZM","__","__","__","__"}, // E
+ {"MW","NG","__","__","__","__","__"} // F
+};
+
+static const char *piCountryCodes_E[15][7]=
+{
+ // 0 1 2 3 4 5 6
+ {"DE","GR","MA","__","MD","__","__"},
+ {"DZ","CY","CZ","IE","EE","__","__"},
+ {"AD","SM","PL","TR","KG","__","__"},
+ {"IL","CH","VA","MK","__","__","__"},
+ {"IT","JO","SK","TJ","__","__","__"},
+ {"BE","FI","SY","__","UA","__","__"},
+ {"RU","LU","TN","__","__","__","__"},
+ {"PS","BG","__","NL","PT","__","__"},
+ {"AL","DK","LI","LV","SI","__","__"}, // 9
+ {"AT","GI","IS","LB","AM","__","__"}, // A
+ {"HU","IQ","MC","AZ","UZ","__","__"}, // B
+ {"MT","GB","LT","HR","GE","__","__"}, // C
+ {"DE","LY","YU","KZ","__","__","__"}, // D
+ {"__","RO","ES","SE","TM","__","__"}, // E
+ {"EG","FR","NO","BY","BA","__","__"} // F
+};
+
+static const char *piCountryCodes_F[15][7]=
+{
+ // 0 1 2 3 4 5 6
+ {"AU","KI","KW","LA","__","__","__"}, // 1
+ {"AU","BT","QA","TH","__","__","__"}, // 2
+ {"AU","BD","KH","TO","__","__","__"}, // 3
+ {"AU","PK","WS","__","__","__","__"}, // 4
+ {"AU","FJ","IN","__","__","__","__"}, // 5
+ {"AU","OM","MO","__","__","__","__"}, // 6
+ {"AU","NR","VN","__","__","__","__"}, // 7
+ {"AU","IR","PH","__","__","__","__"}, // 8
+ {"SA","NZ","JP","PG","__","__","__"}, // 9
+ {"AF","SB","SG","__","__","__","__"}, // A
+ {"MM","BN","MV","YE","__","__","__"}, // B
+ {"CN","LK","ID","__","__","__","__"}, // C
+ {"KP","TW","AE","__","__","__","__"}, // D
+ {"BH","KR","NP","FM","__","__","__"}, // E
+ {"MY","HK","VU","MN","__","__","__"} // F
+};
+
+/* see page 84, Annex J in the standard */
+static const std::string piRDSLanguageCodes[128]=
+{
+ // 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ "___", "alb", "bre", "cat", "hrv", "wel", "cze", "dan", "ger", "eng", "spa", "epo", "est", "baq", "fae", "fre", // 0
+ "fry", "gle", "gla", "glg", "ice", "ita", "smi", "lat", "lav", "ltz", "lit", "hun", "mlt", "dut", "nor", "oci", // 1
+ "pol", "por", "rum", "rom", "srp", "slo", "slv", "fin", "swe", "tur", "nld", "wln", "___", "___", "___", "___", // 2
+ "___", "___", "___", "___", "___", "___", "___", "___", "___", "___", "___", "___", "___", "___", "___", "___", // 3
+ "___", "___", "___", "___", "___", "zul", "vie", "uzb", "urd", "ukr", "tha", "tel", "tat", "tam", "tgk", "swa", // 4
+ "srn", "som", "sin", "sna", "scc", "rue", "rus", "que", "pus", "pan", "per", "pap", "ori", "nep", "nde", "mar", // 5
+ "mol", "mys", "mlg", "mkd", "_?_", "kor", "khm", "kaz", "kan", "jpn", "ind", "hin", "heb", "hau", "grn", "guj", // 6
+ "gre", "geo", "ful", "prs", "chv", "chi", "bur", "bul", "ben", "bel", "bam", "aze", "asm", "arm", "ara", "amh" // 7
+};
+
+/* ----------------------------------------------------------------------------------------------------------- */
+
+#define EntityChars 56
+static const char *entitystr[EntityChars] = { "&apos;", "&amp;", "&quot;", "&gt", "&lt", "&copy;", "&times;", "&nbsp;",
+ "&Auml;", "&auml;", "&Ouml;", "&ouml;", "&Uuml;", "&uuml;", "&szlig;", "&deg;",
+ "&Agrave;", "&Aacute;", "&Acirc;", "&Atilde;", "&agrave;", "&aacute;", "&acirc;", "&atilde;",
+ "&Egrave;", "&Eacute;", "&Ecirc;", "&Euml;", "&egrave;", "&eacute;", "&ecirc;", "&euml;",
+ "&Igrave;", "&Iacute;", "&Icirc;", "&Iuml;", "&igrave;", "&iacute;", "&icirc;", "&iuml;",
+ "&Ograve;", "&Oacute;", "&Ocirc;", "&Otilde;", "&ograve;", "&oacute;", "&ocirc;", "&otilde;",
+ "&Ugrave;", "&Uacute;", "&Ucirc;", "&Ntilde;", "&ugrave;", "&uacute;", "&ucirc;", "&ntilde;" };
+static const char *entitychar[EntityChars] = { "'", "&", "\"", ">", "<", "c", "*", " ",
+ "Ä", "ä", "Ö", "ö", "Ü", "ü", "ß", "°",
+ "À", "Á", "Â", "Ã", "à", "á", "â", "ã",
+ "È", "É", "Ê", "Ë", "è", "é", "ê", "ë",
+ "Ì", "Í", "Î", "Ï", "ì", "í", "î", "ï",
+ "Ò", "Ó", "Ô", "Õ", "ò", "ó", "ô", "õ",
+ "Ù", "Ú", "Û", "Ñ", "ù", "ú", "û", "ñ" };
+
+// RDS-Chartranslation: 0x80..0xff
+static unsigned char sRDSAddChar[128] =
+{
+ 0xe1, 0xe0, 0xe9, 0xe8, 0xed, 0xec, 0xf3, 0xf2,
+ 0xfa, 0xf9, 0xd1, 0xc7, 0x8c, 0xdf, 0x8e, 0x8f,
+ 0xe2, 0xe4, 0xea, 0xeb, 0xee, 0xef, 0xf4, 0xf6,
+ 0xfb, 0xfc, 0xf1, 0xe7, 0x9c, 0x9d, 0x9e, 0x9f,
+ 0xaa, 0xa1, 0xa9, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
+ 0xa8, 0xa9, 0xa3, 0xab, 0xac, 0xad, 0xae, 0xaf,
+ 0xba, 0xb9, 0xb2, 0xb3, 0xb1, 0xa1, 0xb6, 0xb7,
+ 0xb5, 0xbf, 0xf7, 0xb0, 0xbc, 0xbd, 0xbe, 0xa7,
+ 0xc1, 0xc0, 0xc9, 0xc8, 0xcd, 0xcc, 0xd3, 0xd2,
+ 0xda, 0xd9, 0xca, 0xcb, 0xcc, 0xcd, 0xd0, 0xcf,
+ 0xc2, 0xc4, 0xca, 0xcb, 0xce, 0xcf, 0xd4, 0xd6,
+ 0xdb, 0xdc, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf,
+ 0xc3, 0xc5, 0xc6, 0xe3, 0xe4, 0xdd, 0xd5, 0xd8,
+ 0xde, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xf0,
+ 0xe3, 0xe5, 0xe6, 0xf3, 0xf4, 0xfd, 0xf5, 0xf8,
+ 0xfe, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
+};
+
+static char *rds_entitychar(char *text)
+{
+ int i = 0, l, lof, lre, space;
+ char *temp;
+
+ while (i < EntityChars)
+ {
+ if ((temp = strstr(text, entitystr[i])) != NULL)
+ {
+ l = strlen(entitystr[i]);
+ lof = (temp-text);
+ if (strlen(text) < RT_MEL)
+ {
+ lre = strlen(text) - lof - l;
+ space = 1;
+ }
+ else
+ {
+ lre = RT_MEL - 1 - lof - l;
+ space = 0;
+ }
+ memmove(text+lof, entitychar[i], 1);
+ memmove(text+lof+1, temp+l, lre);
+ if (space != 0)
+ memmove(text+lof+1+lre, " ", l-1);
+ }
+ else
+ ++i;
+ }
+
+ return text;
+}
+
+static unsigned short crc16_ccitt(const unsigned char *data, int len, bool skipfirst)
+{
+ // CRC16-CCITT: x^16 + x^12 + x^5 + 1
+ // with start 0xffff and result inverse
+ unsigned short crc = 0xffff;
+
+ if (skipfirst)
+ ++data;
+
+ while (len--)
+ {
+ crc = (crc >> 8) | (crc << 8);
+ crc ^= *data++;
+ crc ^= (crc & 0xff) >> 4;
+ crc ^= (crc << 8) << 4;
+ crc ^= ((crc & 0xff) << 4) << 1;
+ }
+
+ return ~(crc);
+}
+
+
+/// --- CDVDRadioRDSData ------------------------------------------------------------
+
+CDVDRadioRDSData::CDVDRadioRDSData(CProcessInfo &processInfo)
+ : CThread("DVDRDSData")
+ , IDVDStreamPlayer(processInfo)
+ , m_speed(DVD_PLAYSPEED_NORMAL)
+ , m_messageQueue("rds")
+{
+ CLog::Log(LOGDEBUG, "Radio UECP (RDS) Processor - new {}", __FUNCTION__);
+
+ m_messageQueue.SetMaxDataSize(40 * 256 * 1024);
+}
+
+CDVDRadioRDSData::~CDVDRadioRDSData()
+{
+ CLog::Log(LOGDEBUG, "Radio UECP (RDS) Processor - delete {}", __FUNCTION__);
+ StopThread();
+}
+
+bool CDVDRadioRDSData::CheckStream(const CDVDStreamInfo& hints)
+{
+ if (hints.type == STREAM_RADIO_RDS)
+ return true;
+
+ return false;
+}
+
+bool CDVDRadioRDSData::OpenStream(CDVDStreamInfo hints)
+{
+ CloseStream(true);
+
+ m_messageQueue.Init();
+ if (hints.type == STREAM_RADIO_RDS)
+ {
+ Flush();
+ CLog::Log(LOGINFO, "Creating UECP (RDS) data thread");
+ Create();
+ return true;
+ }
+ return false;
+}
+
+void CDVDRadioRDSData::CloseStream(bool bWaitForBuffers)
+{
+ m_messageQueue.Abort();
+
+ // wait for decode_video thread to end
+ CLog::Log(LOGINFO, "Radio UECP (RDS) Processor - waiting for data thread to exit");
+
+ StopThread(); // will set this->m_bStop to true
+
+ m_messageQueue.End();
+ m_currentInfoTag.reset();
+ if (m_currentChannel)
+ m_currentChannel->SetRadioRDSInfoTag(m_currentInfoTag);
+ m_currentChannel.reset();
+}
+
+void CDVDRadioRDSData::ResetRDSCache()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_currentFileUpdate = false;
+
+ m_UECPDataStart = false;
+ m_UECPDatabStuff = false;
+ m_UECPDataIndex = 0;
+
+ m_RDS_IsRBDS = false;
+ m_RDS_SlowLabelingCodesPresent = false;
+
+ m_PI_Current = 0;
+ m_PI_CountryCode = 0;
+ m_PI_ProgramType = 0;
+ m_PI_ProgramReferenceNumber = 0;
+
+ m_EPP_TM_INFO_ExtendedCountryCode = 0;
+
+ m_DI_IsStereo = true;
+ m_DI_ArtificialHead = false;
+ m_DI_Compressed = false;
+ m_DI_DynamicPTY = false;
+
+ m_TA_TP_TrafficAdvisory = false;
+ m_TA_TP_TrafficVolume = 0.0;
+
+ m_MS_SpeechActive = false;
+
+ m_PTY = 0;
+ memset(m_PTYN, 0x20, 8);
+ m_PTYN[8] = 0;
+ m_PTYN_Present = false;
+
+ m_RT_NewItem = false;
+
+ m_RTPlus_TToggle = false;
+ m_RTPlus_Show = false;
+ m_RTPlus_iToggle = 0;
+ m_RTPlus_ItemToggle = 1;
+ m_RTPlus_Title[0] = 0;
+ m_RTPlus_Artist[0] = 0;
+ m_RTPlus_Starttime = time(NULL);
+ m_RTPlus_GenrePresent = false;
+
+ m_currentInfoTag = std::make_shared<CPVRRadioRDSInfoTag>();
+ m_currentChannel = g_application.CurrentFileItem().GetPVRChannelInfoTag();
+ if (m_currentChannel)
+ m_currentChannel->SetRadioRDSInfoTag(m_currentInfoTag);
+
+ // send a message to all windows to tell them to update the radiotext
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_RADIOTEXT);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CDVDRadioRDSData::Process()
+{
+ CLog::Log(LOGINFO, "Radio UECP (RDS) Processor - running thread");
+
+ while (!m_bStop)
+ {
+ std::shared_ptr<CDVDMsg> pMsg;
+ int iPriority = (m_speed == DVD_PLAYSPEED_PAUSE) ? 1 : 0;
+ MsgQueueReturnCode ret = m_messageQueue.Get(pMsg, 2000, iPriority);
+
+ if (ret == MSGQ_TIMEOUT)
+ {
+ /* Timeout for RDS is not a bad thing, so we continue without error */
+ continue;
+ }
+
+ if (MSGQ_IS_ERROR(ret))
+ {
+ if (!m_messageQueue.ReceivedAbortRequest())
+ CLog::Log(LOGERROR, "MSGQ_IS_ERROR returned true ({})", ret);
+
+ break;
+ }
+
+ if (pMsg->IsType(CDVDMsg::DEMUXER_PACKET))
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ DemuxPacket* pPacket = std::static_pointer_cast<CDVDMsgDemuxerPacket>(pMsg)->GetPacket();
+
+ ProcessUECP(pPacket->pData, pPacket->iSize);
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SETSPEED))
+ {
+ m_speed = std::static_pointer_cast<CDVDMsgInt>(pMsg)->m_value;
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_FLUSH)
+ || pMsg->IsType(CDVDMsg::GENERAL_RESET))
+ {
+ ResetRDSCache();
+ }
+ }
+}
+
+void CDVDRadioRDSData::Flush()
+{
+ if(!m_messageQueue.IsInited())
+ return;
+ /* flush using message as this get's called from VideoPlayer thread */
+ /* and any demux packet that has been taken out of queue need to */
+ /* be disposed of before we flush */
+ m_messageQueue.Flush();
+ m_messageQueue.Put(std::make_shared<CDVDMsg>(CDVDMsg::GENERAL_FLUSH));
+}
+
+void CDVDRadioRDSData::OnExit()
+{
+ CLog::Log(LOGINFO, "Radio UECP (RDS) Processor - thread end");
+}
+
+void CDVDRadioRDSData::SetRadioStyle(const std::string& genre)
+{
+ g_application.CurrentFileItem().GetMusicInfoTag()->SetGenre(genre);
+ m_currentInfoTag->SetProgStyle(genre);
+ m_currentFileUpdate = true;
+
+ CLog::Log(LOGDEBUG, "Radio UECP (RDS) Processor - {} - Stream genre set to {}", __FUNCTION__,
+ genre);
+}
+
+void CDVDRadioRDSData::ProcessUECP(const unsigned char *data, unsigned int len)
+{
+ for (unsigned int i = 0; i < len; ++i)
+ {
+ if (data[i] == UECP_DATA_START) //!< Start
+ {
+ m_UECPDataIndex = -1;
+ m_UECPDataStart = true;
+ m_UECPDatabStuff = false;
+ }
+
+ if (m_UECPDataStart)
+ {
+ //! byte-stuffing reverse: 0xfd00->0xfd, 0xfd01->0xfe, 0xfd02->0xff
+ if (m_UECPDatabStuff == true)
+ {
+ switch (data[i])
+ {
+ case 0x00: m_UECPData[m_UECPDataIndex] = 0xfd; break;
+ case 0x01: m_UECPData[m_UECPDataIndex] = 0xfe; break;
+ case 0x02: m_UECPData[m_UECPDataIndex] = 0xff; break;
+ default: m_UECPData[++m_UECPDataIndex] = data[i]; // should never be
+ }
+ m_UECPDatabStuff = false;
+ }
+ else
+ {
+ m_UECPData[++m_UECPDataIndex] = data[i];
+ }
+
+ if (data[i] == 0xfd && m_UECPDataIndex > 0) //!< stuffing found
+ m_UECPDatabStuff = true;
+
+ if (m_UECPDataIndex >= UECP_SIZE_MAX) //!< max. UECP data length, garbage ?
+ {
+ CLog::Log(LOGERROR, "Radio UECP (RDS) Processor - Error(TS): too long, garbage ?");
+ m_UECPDataStart = false;
+ }
+ }
+
+ if (m_UECPDataStart == true && data[i] == UECP_DATA_STOP && m_currentInfoTag) //!< End
+ {
+ m_UECPDataStart = false;
+
+ if (m_UECPDataIndex < 9)
+ {
+ CLog::Log(LOGERROR, "Radio UECP (RDS) Processor - Error(TS): too short -> garbage ?");
+ }
+ else
+ {
+ //! crc16-check
+ unsigned short crc16 = crc16_ccitt(m_UECPData, m_UECPDataIndex-3, true);
+ if (crc16 != (m_UECPData[m_UECPDataIndex-2]<<8) + m_UECPData[m_UECPDataIndex-1])
+ {
+ CLog::Log(LOGERROR,
+ "Radio UECP (RDS) Processor - Error(TS): wrong CRC # calc = {:04x} <> transmit "
+ "= {:02x}{:02x}",
+ crc16, m_UECPData[m_UECPDataIndex - 2], m_UECPData[m_UECPDataIndex - 1]);
+ }
+ else
+ {
+ m_UECPDataDeadBreak = false;
+
+ unsigned int ret = 0;
+ unsigned int ptr = 5;
+ unsigned int len = m_UECPDataIndex-7;
+ do
+ {
+ uint8_t *msg = m_UECPData+ptr; //!< Current selected UECP message element (increased if more as one element is in frame)
+ switch (msg[UECP_ME_MEC])
+ {
+ case UECP_RDS_PI: ret = DecodePI(msg); break; //!< Program Identification
+ case UECP_RDS_PS: ret = DecodePS(msg); break; //!< Program Service name (PS)
+ case UECP_RDS_DI: ret = DecodeDI(msg); break; //!< Decoder Identification and dynamic PTY indicator
+ case UECP_RDS_TA_TP: ret = DecodeTA_TP(msg); break; //!< Traffic Announcement and Traffic Programme bits.
+ case UECP_RDS_MS: ret = DecodeMS(msg); break; //!< Music/Speech switch
+ case UECP_RDS_PTY: ret = DecodePTY(msg); break; //!< Program Type
+ case UECP_RDS_PTYN: ret = DecodePTYN(msg); break; //!< Program Type Name
+ case UECP_RDS_RT: ret = DecodeRT(msg, len); break; //!< RadioText
+ case UECP_ODA_DATA: ret = DecodeODA(msg, len); break; //!< Open Data Application
+ case UECP_OTHER_RASS: m_UECPDataDeadBreak = true; break; //!< Radio screen show (RaSS) (not present, before on SWR radio)
+ case UECP_CLOCK_RTC: ret = DecodeRTC(msg); break; //!< Real time clock
+ case UECP_TDC_TMC: ret = DecodeTMC(msg, len); break; //!< Traffic message channel
+ case UECP_EPP_TM_INFO: ret = DecodeEPPTransmitterInfo(msg); break; //!< EPP transmitter information
+ case UECP_SLOW_LABEL_CODES: ret = DecodeSlowLabelingCodes(msg); break; //!< Slow Labeling codes
+ case UECP_DAB_DYN_LABEL_CMD: ret = DecodeDABDynLabelCmd(msg, len); break; //!< DAB: Dynamic Label command
+ case UECP_DAB_DYN_LABEL_MSG: ret = DecodeDABDynLabelMsg(msg, len); break; //!< DAB: Dynamic Label message (DL)
+ case UECP_RDS_AF: ret = DecodeAF(msg, len); break; //!< Alternative Frequencies list
+ case UECP_RDS_EON_AF: ret = DecodeEonAF(msg, len); break; //!< EON Alternative Frequencies list
+ case UECP_TDC_TDC: ret = DecodeTDC(msg, len); break; //!< Transparent Data Channel
+ case UECP_LINKAGE_INFO: ret = 5; break; //!< Linkage information
+ case UECP_TDC_EWS: ret = 6; break; //!< Emergency warning system
+ case UECP_RDS_PIN: ret = 5; break; //!< Program Item Number
+ case UECP_TDC_IH: ret = 7; break; //!< In-house applications (Should be ignored)
+ case UECP_TDC_FREE_FMT_GROUP: ret = 7; break; //!< Free-format group (unused)
+ case UECP_ODA_CONF_SHORT_MSG_CMD: ret = 8; break; //!< ODA Configuration and Short Message Command (unused)
+ case UECP_CLOCK_RTC_CORR: ret = 3; break; //!< Real time clock correction (unused)
+ case UECP_CLOCK_CT_ON_OFF: ret = 2; break; //!< Real time clock on/off (unused)
+ default:
+#ifdef RDS_IMPROVE_CHECK
+ printf("Unknown UECP data packet = 0x%02X\n", msg[UECP_ME_MEC]);
+#endif
+ m_UECPDataDeadBreak = true;
+ break;
+ }
+ ptr += ret;
+ len -= ret;
+ }
+ while (ptr < m_UECPDataIndex-5 && !m_UECPDataDeadBreak && !m_bStop);
+
+ if (m_currentFileUpdate && !m_bStop)
+ {
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(g_application.CurrentFileItem());
+ m_currentFileUpdate = false;
+ }
+ }
+ }
+ }
+ }
+}
+
+unsigned int CDVDRadioRDSData::DecodePI(const uint8_t* msgElement)
+{
+ uint16_t PICode = (msgElement[3] << 8) | msgElement[4];
+ if (m_PI_Current != PICode)
+ {
+ m_PI_Current = PICode;
+
+ m_PI_CountryCode = (m_PI_Current>>12) & 0x0F;
+ m_PI_ProgramType = (m_PI_Current>>8) & 0x0F;
+ m_PI_ProgramReferenceNumber = m_PI_Current & 0xFF;
+
+ CLog::Log(LOGINFO,
+ "Radio UECP (RDS) Processor - PI code changed to Country {:X}, Type {:X} and "
+ "reference no. {}",
+ m_PI_CountryCode, m_PI_ProgramType, m_PI_ProgramReferenceNumber);
+ }
+
+ return 5;
+}
+
+unsigned int CDVDRadioRDSData::DecodePS(uint8_t *msgElement)
+{
+ uint8_t *text = msgElement+3;
+
+ char decodedText[9] = {};
+ for (int i = 0; i < 8; ++i)
+ {
+ if (text[i] <= 0xfe)
+ decodedText[i] = (text[i] >= 0x80)
+ ? sRDSAddChar[text[i] - 0x80]
+ : text[i]; //!< additional rds-character, see RBDS-Standard, Annex E
+ }
+
+ m_currentInfoTag->SetProgramServiceText(decodedText);
+
+ return 11;
+}
+
+unsigned int CDVDRadioRDSData::DecodeDI(const uint8_t* msgElement)
+{
+ bool value;
+
+ value = (msgElement[3] & 1) != 0;
+ if (m_DI_IsStereo != value)
+ {
+ CLog::Log(LOGDEBUG, "Radio UECP (RDS) Processor - {} - Stream changed over to {}", __FUNCTION__,
+ value ? "Stereo" : "Mono");
+ m_DI_IsStereo = value;
+ }
+
+ value = (msgElement[3] & 2) != 0;
+ if (m_DI_ArtificialHead != value)
+ {
+ CLog::Log(LOGDEBUG,
+ "Radio UECP (RDS) Processor - {} - Stream changed over to {}Artificial Head",
+ __FUNCTION__, value ? "" : "Not ");
+ m_DI_ArtificialHead = value;
+ }
+
+ value = (msgElement[3] & 4) != 0;
+ if (m_DI_ArtificialHead != value)
+ {
+ CLog::Log(LOGDEBUG,
+ "Radio UECP (RDS) Processor - {} - Stream changed over to {}Compressed Head",
+ __FUNCTION__, value ? "" : "Not ");
+ m_DI_ArtificialHead = value;
+ }
+
+ value = (msgElement[3] & 8) != 0;
+ if (m_DI_DynamicPTY != value)
+ {
+ CLog::Log(LOGDEBUG, "Radio UECP (RDS) Processor - {} - Stream changed over to {} PTY",
+ __FUNCTION__, value ? "dynamic" : "static");
+ m_DI_DynamicPTY = value;
+ }
+
+ return 4;
+}
+
+unsigned int CDVDRadioRDSData::DecodeTA_TP(const uint8_t* msgElement)
+{
+ uint8_t dsn = msgElement[1];
+ bool traffic_announcement = (msgElement[3] & 1) != 0;
+ bool traffic_programme = (msgElement[3] & 2) != 0;
+
+ if (traffic_announcement && !m_TA_TP_TrafficAdvisory && traffic_programme && dsn == 0 && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool("pvrplayback.trafficadvisory"))
+ {
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, g_localizeStrings.Get(19021), g_localizeStrings.Get(29930));
+ m_TA_TP_TrafficAdvisory = true;
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ m_TA_TP_TrafficVolume = appVolume->GetVolumePercent();
+ float trafAdvVol = (float)CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt("pvrplayback.trafficadvisoryvolume");
+ if (trafAdvVol)
+ appVolume->SetVolume(m_TA_TP_TrafficVolume + trafAdvVol);
+
+ CVariant data(CVariant::VariantTypeObject);
+ data["on"] = true;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::PVR, "RDSRadioTA", data);
+ }
+
+ if (!traffic_announcement && m_TA_TP_TrafficAdvisory && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool("pvrplayback.trafficadvisory"))
+ {
+ m_TA_TP_TrafficAdvisory = false;
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume(m_TA_TP_TrafficVolume);
+
+ CVariant data(CVariant::VariantTypeObject);
+ data["on"] = false;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::PVR, "RDSRadioTA", data);
+ }
+
+ return 4;
+}
+
+unsigned int CDVDRadioRDSData::DecodeMS(const uint8_t* msgElement)
+{
+ bool speechActive = msgElement[3] == 0;
+ if (m_MS_SpeechActive != speechActive)
+ {
+ m_currentInfoTag->SetSpeechActive(m_MS_SpeechActive);
+ CLog::Log(LOGDEBUG, "Radio UECP (RDS) Processor - {} - Stream changed over to {}", __FUNCTION__,
+ speechActive ? "Speech" : "Music");
+ m_MS_SpeechActive = speechActive;
+ }
+
+ return 4;
+}
+
+/*!
+ * EBU - SPB 490 - 3.3.7 and 62106IEC:1999 - 3.2.1.2, Message Name: Programme Type
+ * Message Element Code: 07
+ */
+//! @todo Improve and test alarm message
+typedef struct { const char *style_name; int name; } pty_skin_info;
+pty_skin_info pty_skin_info_table[32][2] =
+{
+ { { "none", 29940 }, { "none", 29940 } },
+ { { "news", 29941 }, { "news", 29941 } },
+ { { "currentaffairs", 29942 }, { "information", 29943 } },
+ { { "information", 29943 }, { "sport", 29944 } },
+ { { "sport", 29944 }, { "talk", 29939 } },
+ { { "education", 29945 }, { "rockmusic", 29951 } },
+ { { "drama", 29946 }, { "classicrockmusic",29977 } },
+ { { "cultures", 29947 }, { "adulthits", 29937 } },
+ { { "science", 29948 }, { "softrock", 29938 } },
+ { { "variedspeech", 29949 }, { "top40", 29972 } },
+ { { "popmusic", 29950 }, { "countrymusic", 29965 } },
+ { { "rockmusic", 29951 }, { "oldiesmusic", 29967 } },
+ { { "easylistening", 29952 }, { "softmusic", 29936 } },
+ { { "lightclassics", 29953 }, { "nostalgia", 29979 } },
+ { { "seriousclassics",29954 }, { "jazzmusic", 29964 } },
+ { { "othermusic", 29955 }, { "classical", 29978 } },
+ { { "weather", 29956 }, { "randb", 29975 } },
+ { { "finance", 29957 }, { "softrandb", 29976 } },
+ { { "childrensprogs", 29958 }, { "language", 29932 } },
+ { { "socialaffairs", 29959 }, { "religiousmusic", 29973 } },
+ { { "religion", 29960 }, { "religioustalk", 29974 } },
+ { { "phonein", 29961 }, { "personality", 29934 } },
+ { { "travelandtouring",29962 },{ "public", 29935 } },
+ { { "leisureandhobby",29963 }, { "college", 29933 } },
+ { { "jazzmusic", 29964 }, { "spanishtalk", 29927 } },
+ { { "countrymusic", 29965 }, { "spanishmusic", 29928 } },
+ { { "nationalmusic", 29966 }, { "hiphop", 29929 } },
+ { { "oldiesmusic", 29967 }, { "", -1 } },
+ { { "folkmusic", 29968 }, { "", -1 } },
+ { { "documentary", 29969 }, { "weather", 29956 } },
+ { { "alarmtest", 29970 }, { "alarmtest", 29970 } },
+ { { "alarm-alarm", 29971 }, { "alarm-alarm", 29971 } }
+};
+
+unsigned int CDVDRadioRDSData::DecodePTY(const uint8_t* msgElement)
+{
+ int pty = msgElement[3];
+ if (pty >= 0 && pty < 32 && m_PTY != pty)
+ {
+ m_PTY = pty;
+
+ // save info
+ m_currentInfoTag->SetRadioStyle(pty_skin_info_table[m_PTY][m_RDS_IsRBDS].style_name);
+ if (!m_RTPlus_GenrePresent && !m_PTYN_Present)
+ SetRadioStyle(g_localizeStrings.Get(pty_skin_info_table[m_PTY][m_RDS_IsRBDS].name));
+
+ if (m_PTY == RDS_PTY_ALARM_TEST)
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(29931), g_localizeStrings.Get(29970), TOAST_DISPLAY_TIME, false);
+
+ if (m_PTY == RDS_PTY_ALARM)
+ {
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, g_localizeStrings.Get(29931), g_localizeStrings.Get(29971), TOAST_DISPLAY_TIME*2, true);
+ }
+ }
+
+ return 4;
+}
+
+unsigned int CDVDRadioRDSData::DecodePTYN(uint8_t *msgElement)
+{
+ // decode Text
+ uint8_t *text = msgElement+3;
+
+ for (int i = 0; i < 8; ++i)
+ {
+ if (text[i] <= 0xfe)
+ m_PTYN[i] = (text[i] >= 0x80) ? sRDSAddChar[text[i]-0x80] : text[i];
+ }
+
+ m_PTYN_Present = true;
+
+ if (!m_RTPlus_GenrePresent)
+ {
+ std::string progTypeName = StringUtils::Format(
+ "{}: {}", g_localizeStrings.Get(pty_skin_info_table[m_PTY][m_RDS_IsRBDS].name), m_PTYN);
+ SetRadioStyle(progTypeName);
+ }
+
+ return 11;
+}
+
+inline void rtrim_str(std::string &text)
+{
+ for (int i = text.length()-1; i >= 0; --i)
+ {
+ if (text[i] == ' ' || text[i] == '\t' || text[i] == '\n' || text[i] == '\r')
+ text[i] = 0;
+ else
+ break;
+ }
+}
+
+unsigned int CDVDRadioRDSData::DecodeRT(uint8_t *msgElement, unsigned int len)
+{
+ m_currentInfoTag->SetPlayingRadioText(true);
+
+ int bufConf = (msgElement[UECP_ME_DATA] >> 5) & 0x03;
+ unsigned int msgLength = msgElement[UECP_ME_MEL];
+ if (msgLength > len-2)
+ {
+ CLog::Log(LOGERROR,
+ "Radio UECP (RDS) - {} - RT-Error: Length=0 or not correct (MFL= {}, MEL= {})",
+ __FUNCTION__, len, msgLength);
+ m_UECPDataDeadBreak = true;
+ return 0;
+ }
+ else if (msgLength == 0 || (msgLength == 1 && bufConf == 0))
+ {
+ return msgLength + 4;
+ }
+ else
+ {
+ // bool flagToggle = msgElement[UECP_ME_DATA] & 0x01 ? true : false;
+ // int txQty = (msgElement[UECP_ME_DATA] >> 1) & 0x0F;
+ // int bufConf = (msgElement[UECP_ME_DATA] >> 5) & 0x03;
+
+ //! byte 4 = RT-Status bitcodet (0=AB-flagcontrol, 1-4=Transmission-Number, 5+6=Buffer-Config, ignored, always 0x01 ?)
+ char temptext[RT_MEL];
+ memset(temptext, 0x0, RT_MEL);
+ for (unsigned int i = 1, ii = 0; i < msgLength; ++i)
+ {
+ if (msgElement[UECP_ME_DATA+i] <= 0xfe) // additional rds-character, see RBDS-Standard, Annex E
+ temptext[ii++] = (msgElement[UECP_ME_DATA+i] >= 0x80) ? sRDSAddChar[msgElement[UECP_ME_DATA+i]-0x80] : msgElement[UECP_ME_DATA+i];
+ }
+
+ memcpy(m_RTPlus_WorkText, temptext, RT_MEL);
+ rds_entitychar(temptext);
+
+ m_currentInfoTag->SetRadioText(temptext);
+
+ m_RTPlus_iToggle = 0x03; // Bit 0/1 = Title/Artist
+ }
+ return msgLength+4;
+}
+
+#define UECP_CLOCK_YEAR 1
+#define UECP_CLOCK_MONTH 2
+#define UECP_CLOCK_DAY 3
+#define UECP_CLOCK_HOURS 4
+#define UECP_CLOCK_MINUTES 5
+#define UECP_CLOCK_SECONDS 6
+#define UECP_CLOCK_CENTSEC 7
+#define UECP_CLOCK_LOCALOFFSET 8
+unsigned int CDVDRadioRDSData::DecodeRTC(uint8_t *msgElement)
+{
+ uint8_t hours = msgElement[UECP_CLOCK_HOURS];
+ uint8_t minutes = msgElement[UECP_CLOCK_MINUTES];
+ bool minus = (msgElement[UECP_CLOCK_LOCALOFFSET] & 0x20) != 0;
+ if (minus)
+ {
+ if (msgElement[UECP_CLOCK_LOCALOFFSET] >> 1)
+ hours -= msgElement[UECP_CLOCK_LOCALOFFSET] >> 1;
+ if (msgElement[UECP_CLOCK_LOCALOFFSET] & 1)
+ minutes -= 30;
+ }
+ else
+ {
+ if (msgElement[UECP_CLOCK_LOCALOFFSET] >> 1)
+ hours += msgElement[UECP_CLOCK_LOCALOFFSET] >> 1;
+ if (msgElement[UECP_CLOCK_LOCALOFFSET] & 1)
+ minutes += 30;
+ }
+ m_RTC_DateTime.SetDateTime(msgElement[UECP_CLOCK_YEAR], msgElement[UECP_CLOCK_MONTH], msgElement[UECP_CLOCK_DAY],
+ hours, minutes, msgElement[UECP_CLOCK_SECONDS]);
+
+ CLog::Log(LOGDEBUG,
+ "Radio UECP (RDS) - {} - Current RDS Data Time: {:02}.{:02}.{:02} - UTC: "
+ "{:02}:{:02}:{:02},0.{}s - Local: {}{} min",
+ __FUNCTION__, msgElement[UECP_CLOCK_DAY], msgElement[UECP_CLOCK_MONTH],
+ msgElement[UECP_CLOCK_YEAR], msgElement[UECP_CLOCK_HOURS],
+ msgElement[UECP_CLOCK_MINUTES], msgElement[UECP_CLOCK_SECONDS],
+ msgElement[UECP_CLOCK_CENTSEC], minus ? '-' : '+',
+ msgElement[UECP_CLOCK_LOCALOFFSET] * 30);
+
+ CVariant data(CVariant::VariantTypeObject);
+ data["dateTime"] = (m_RTC_DateTime.IsValid()) ? m_RTC_DateTime.GetAsRFC1123DateTime() : "";
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::PVR, "RDSRadioRTC", data);
+
+ return 8;
+}
+
+unsigned int CDVDRadioRDSData::DecodeODA(uint8_t *msgElement, unsigned int len)
+{
+ unsigned int procData = msgElement[1];
+ if (procData == 0 || procData > len-2)
+ {
+ CLog::Log(LOGERROR, "Radio UECP (RDS) - Invalid ODA data size");
+ m_UECPDataDeadBreak = true;
+ return 0;
+ }
+
+ switch ((msgElement[2]<<8)+msgElement[3]) // ODA-ID
+ {
+ case 0x4bd7: //!< RT+
+ procData = DecodeRTPlus(msgElement, len);
+ break;
+ case 0x0d45: //!< TMC Alert-C
+ case 0xcd46:
+ SendTMCSignal(msgElement[4], msgElement+5);
+ break;
+ default:
+ m_UECPDataDeadBreak = true;
+#ifdef RDS_IMPROVE_CHECK
+ printf("[RDS-ODA AID '%02x%02x' not used -> End]\n", msgElement[2], msgElement[3]);
+#endif // RDS_IMPROVE_CHECK
+ break;
+ }
+ return procData;
+}
+
+unsigned int CDVDRadioRDSData::DecodeRTPlus(uint8_t *msgElement, unsigned int len)
+{
+ if (m_RTPlus_iToggle == 0) // RTplus tags V2.1, only if RT
+ return 10;
+
+ m_currentInfoTag->SetPlayingRadioTextPlus(true);
+
+ if (msgElement[1] > len-2 || msgElement[1] != 8) // byte 6 = MEL, only 8 byte for 2 tags
+ {
+ CLog::Log(LOGERROR, "Radio UECP (RDS) - {} - RTp-Error: Length not correct (MEL= {})",
+ __FUNCTION__, msgElement[1]);
+ m_UECPDataDeadBreak = true;
+ return 0;
+ }
+ unsigned int rtp_typ[2], rtp_start[2], rtp_len[2];
+ // byte 2+3 = ApplicationID, always 0x4bd7
+ // byte 4 = Applicationgroup Typecode / PTY ?
+ // bit 10#4 = Item Togglebit
+ // bit 10#3 = Item Runningbit321
+ // Tag1: bit 10#2..11#5 = Contenttype, 11#4..12#7 = Startmarker, 12#6..12#1 = Length
+ rtp_typ[0] = (0x38 & msgElement[5]<<3) | msgElement[6]>>5;
+ rtp_start[0] = (0x3e & msgElement[6]<<1) | msgElement[7]>>7;
+ rtp_len[0] = 0x3f & msgElement[7]>>1;
+ // Tag2: bit 12#0..13#3 = Contenttype, 13#2..14#5 = Startmarker, 14#4..14#0 = Length(5bit)
+ rtp_typ[1] = (0x20 & msgElement[7]<<5) | msgElement[8]>>3;
+ rtp_start[1] = (0x38 & msgElement[8]<<3) | msgElement[9]>>5;
+ rtp_len[1] = 0x1f & msgElement[9];
+
+ /// Hack for error on BR Classic
+ if ((msgElement[5]&0x10) && (msgElement[5]&0x08) && rtp_typ[0] == RTPLUS_INFO_URL && rtp_typ[1] == RTPLUS_ITEM_ARTIST)
+ return 10;
+
+ // save info
+ MUSIC_INFO::CMusicInfoTag *currentMusic = g_application.CurrentFileItem().GetMusicInfoTag();
+
+ for (int i = 0; i < 2; ++i)
+ {
+ if (rtp_start[i]+rtp_len[i]+1 >= RT_MEL) // length-error
+ {
+ CLog::Log(
+ LOGERROR,
+ "Radio UECP (RDS) - {} - (tag#{} = Typ/Start/Len): {}/{}/{} (Start+Length > 'RT-MEL' !)",
+ __FUNCTION__, i + 1, rtp_typ[i], rtp_start[i], rtp_len[i]);
+ }
+ else
+ {
+ // +Memory
+ memset(m_RTPlus_Temptext, 0x20, RT_MEL);
+ memcpy(m_RTPlus_Temptext, m_RTPlus_WorkText+rtp_start[i], rtp_len[i]+1);
+ m_RTPlus_Temptext[rtp_len[i]+1] = 0;
+ rds_entitychar(m_RTPlus_Temptext);
+ switch (rtp_typ[i])
+ {
+ case RTPLUS_DUMMY_CLASS:
+ break;
+ case RTPLUS_ITEM_TITLE: // Item-Title...
+ if ((msgElement[5] & 0x08) > 0 && (m_RTPlus_iToggle & 0x01) == 0x01)
+ {
+ m_RTPlus_iToggle -= 0x01;
+ if (memcmp(m_RTPlus_Title, m_RTPlus_Temptext, RT_MEL) != 0 || (msgElement[5] & 0x10) != m_RTPlus_ItemToggle)
+ {
+ memcpy(m_RTPlus_Title, m_RTPlus_Temptext, RT_MEL);
+ if (m_RTPlus_Show && m_RTPlus_iTime.GetElapsedSeconds() > 1)
+ m_RTPlus_iDiffs = (int) m_RTPlus_iTime.GetElapsedSeconds();
+ if (!m_RT_NewItem)
+ {
+ m_RTPlus_Starttime = time(NULL);
+ m_RTPlus_iTime.StartZero();
+ m_RTPlus_Artist[0] = 0;
+ }
+ m_RT_NewItem = (!m_RT_NewItem) ? true : false;
+ m_RTPlus_Show = m_RTPlus_TToggle = true;
+ }
+ }
+ break;
+ case RTPLUS_ITEM_ALBUM:
+ m_currentInfoTag->SetAlbum(m_RTPlus_Temptext);
+ currentMusic->SetAlbum(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_ITEM_TRACKNUMBER:
+ m_currentInfoTag->SetAlbumTrackNumber(atoi(m_RTPlus_Temptext));
+ currentMusic->SetAlbumId(atoi(m_RTPlus_Temptext));
+ break;
+ case RTPLUS_ITEM_ARTIST: // Item-Artist..
+ if ((msgElement[5] & 0x08) > 0 && (m_RTPlus_iToggle & 0x02) == 0x02)
+ {
+ m_RTPlus_iToggle -= 0x02;
+ if (memcmp(m_RTPlus_Artist, m_RTPlus_Temptext, RT_MEL) != 0 || (msgElement[5] & 0x10) != m_RTPlus_ItemToggle)
+ {
+ memcpy(m_RTPlus_Artist, m_RTPlus_Temptext, RT_MEL);
+ if (m_RTPlus_Show && m_RTPlus_iTime.GetElapsedSeconds() > 1)
+ m_RTPlus_iDiffs = (int) m_RTPlus_iTime.GetElapsedSeconds();
+ if (!m_RT_NewItem)
+ {
+ m_RTPlus_Starttime = time(NULL);
+ m_RTPlus_iTime.StartZero();
+ m_RTPlus_Title[0] = 0;
+ }
+ m_RT_NewItem = (!m_RT_NewItem) ? true : false;
+ m_RTPlus_Show = m_RTPlus_TToggle = true;
+ }
+ }
+ break;
+ case RTPLUS_ITEM_CONDUCTOR:
+ m_currentInfoTag->SetConductor(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_ITEM_COMPOSER:
+ case RTPLUS_ITEM_COMPOSITION:
+ m_currentInfoTag->SetComposer(m_RTPlus_Temptext);
+ if (m_currentInfoTag->GetRadioStyle() == "unknown")
+ m_currentInfoTag->SetRadioStyle("classical");
+ break;
+ case RTPLUS_ITEM_BAND:
+ m_currentInfoTag->SetBand(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_ITEM_COMMENT:
+ m_currentInfoTag->SetComment(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_ITEM_GENRE:
+ {
+ std::string str = m_RTPlus_Temptext;
+ g_charsetConverter.unknownToUTF8(str);
+ m_RTPlus_GenrePresent = true;
+ m_currentInfoTag->SetProgStyle(str);
+ }
+ break;
+ case RTPLUS_INFO_NEWS: // Info_News
+ m_currentInfoTag->SetInfoNews(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_INFO_NEWS_LOCAL: // Info_NewsLocal
+ m_currentInfoTag->SetInfoNewsLocal(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_INFO_STOCKMARKET: // Info_Stockmarket
+ m_currentInfoTag->SetInfoStock(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_INFO_SPORT: // Info_Sport
+ m_currentInfoTag->SetInfoSport(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_INFO_LOTTERY: // Info_Lottery
+ m_currentInfoTag->SetInfoLottery(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_INFO_HOROSCOPE:
+ m_currentInfoTag->SetInfoHoroscope(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_INFO_CINEMA:
+ m_currentInfoTag->SetInfoCinema(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_INFO_WEATHER: // Info_Weather/
+ m_currentInfoTag->SetInfoWeather(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_INFO_URL: // Info_Url
+ if (m_currentInfoTag->GetProgWebsite().empty())
+ m_currentInfoTag->SetProgWebsite(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_INFO_OTHER: // Info_Other
+ m_currentInfoTag->SetInfoOther(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_STATIONNAME_LONG: // Programme_Stationname.Long
+ m_currentInfoTag->SetProgStation(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_PROGRAMME_NOW: // Programme_Now
+ m_currentInfoTag->SetProgNow(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_PROGRAMME_NEXT: // Programme_Next
+ m_currentInfoTag->SetProgNext(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_PROGRAMME_HOST: // Programme_Host
+ m_currentInfoTag->SetProgHost(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_PROGRAMME_EDITORIAL_STAFF: // Programme_EditorialStaff
+ m_currentInfoTag->SetEditorialStaff(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_PROGRAMME_HOMEPAGE: // Programme_Homepage
+ m_currentInfoTag->SetProgWebsite(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_PHONE_HOTLINE: // Phone_Hotline
+ m_currentInfoTag->SetPhoneHotline(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_PHONE_STUDIO: // Phone_Studio
+ m_currentInfoTag->SetPhoneStudio(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_SMS_STUDIO: // SMS_Studio
+ m_currentInfoTag->SetSMSStudio(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_EMAIL_HOTLINE: // Email_Hotline
+ m_currentInfoTag->SetEMailHotline(m_RTPlus_Temptext);
+ break;
+ case RTPLUS_EMAIL_STUDIO: // Email_Studio
+ m_currentInfoTag->SetEMailStudio(m_RTPlus_Temptext);
+ break;
+ /**
+ * Currently unused radiotext plus messages
+ * Must be check where present and if it is usable
+ */
+ case RTPLUS_ITEM_MOVEMENT:
+ case RTPLUS_INFO_DAILY_DIVERSION:
+ case RTPLUS_INFO_HEALTH:
+ case RTPLUS_INFO_EVENT:
+ case RTPLUS_INFO_SZENE:
+ case RTPLUS_INFO_STUPIDITY_MACHINE:
+ case RTPLUS_INFO_TRAFFIC:
+ case RTPLUS_INFO_ALARM:
+ case RTPLUS_INFO_ADVERTISEMENT:
+ case RTPLUS_PROGRAMME_PART:
+ case RTPLUS_PROGRAMME_FREQUENCY:
+ case RTPLUS_PROGRAMME_SUBCHANNEL:
+ case RTPLUS_PHONE_OTHER:
+ case RTPLUS_SMS_OTHER:
+ case RTPLUS_EMAIL_OTHER:
+ case RTPLUS_MMS_OTHER:
+ case RTPLUS_CHAT:
+ case RTPLUS_CHAT_CENTER:
+ case RTPLUS_VOTE_QUESTION:
+ case RTPLUS_VOTE_CENTER:
+ case RTPLUS_PLACE:
+ case RTPLUS_APPOINTMENT:
+ case RTPLUS_IDENTIFIER:
+ case RTPLUS_PURCHASE:
+ case RTPLUS_GET_DATA:
+#ifdef RDS_IMPROVE_CHECK
+ printf(" RTp-Unkn. : %02i - %s\n", rtp_typ[i], m_RTPlus_Temptext);
+ break;
+#endif // RDS_IMPROVE_CHECK
+ /// Unused and not needed data information
+ case RTPLUS_STATIONNAME_SHORT: //!< Must be rechecked under DAB
+ case RTPLUS_INFO_DATE_TIME:
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ // Title-end @ no Item-Running'
+ if ((msgElement[5] & 0x08) == 0)
+ {
+ m_RTPlus_Title[0] = 0;
+ m_RTPlus_Artist[0] = 0;
+ m_currentInfoTag->ResetSongInformation();
+ currentMusic->SetAlbum("");
+ if (m_RTPlus_GenrePresent)
+ {
+ m_currentInfoTag->SetProgStyle("");
+ m_RTPlus_GenrePresent = false;
+ }
+
+ if (m_RTPlus_Show)
+ {
+ m_RTPlus_Show = false;
+ m_RTPlus_TToggle = true;
+ m_RTPlus_iDiffs = (int) m_RTPlus_iTime.GetElapsedSeconds();
+ m_RTPlus_Starttime = time(NULL);
+ }
+ m_RT_NewItem = false;
+ }
+
+ if (m_RTPlus_TToggle)
+ {
+#ifdef RDS_IMPROVE_CHECK
+ {
+ struct tm tm_store;
+ struct tm *ts = localtime_r(&m_RTPlus_Starttime, &tm_store);
+ if (m_RTPlus_iDiffs > 0)
+ printf(" StartTime : %02d:%02d:%02d (last Title elapsed = %d s)\n", ts->tm_hour, ts->tm_min, ts->tm_sec, m_RTPlus_iDiffs);
+ else
+ printf(" StartTime : %02d:%02d:%02d\n", ts->tm_hour, ts->tm_min, ts->tm_sec);
+ printf(" RTp-Title : %s\n RTp-Artist: %s\n", m_RTPlus_Title, m_RTPlus_Artist);
+ }
+#endif // RDS_IMPROVE_CHECK
+ m_RTPlus_ItemToggle = msgElement[5] & 0x10;
+ m_RTPlus_TToggle = false;
+ m_RTPlus_iDiffs = 0;
+
+ std::string str;
+
+ str = m_RTPlus_Artist;
+ m_currentInfoTag->SetArtist(str);
+ if (str.empty() && !m_currentInfoTag->GetComposer().empty())
+ str = m_currentInfoTag->GetComposer();
+ else if (str.empty() && !m_currentInfoTag->GetConductor().empty())
+ str = m_currentInfoTag->GetConductor();
+ else if (str.empty() && !m_currentInfoTag->GetBand().empty())
+ str = m_currentInfoTag->GetBand();
+
+ if (!str.empty())
+ g_charsetConverter.unknownToUTF8(str);
+ currentMusic->SetArtist(str);
+
+ str = m_RTPlus_Title;
+ g_charsetConverter.unknownToUTF8(str);
+ currentMusic->SetTitle(str);
+ m_currentInfoTag->SetTitle(str);
+ m_currentFileUpdate = true;
+ }
+ m_RTPlus_iToggle = 0;
+
+ return 10;
+}
+
+unsigned int CDVDRadioRDSData::DecodeTMC(uint8_t *msgElement, unsigned int len)
+{
+ unsigned int msgElementLength = msgElement[1];
+ if (msgElementLength == 0)
+ msgElementLength = 6;
+ if (msgElementLength + 2 > len)
+ {
+ m_UECPDataDeadBreak = true;
+ return 0;
+ }
+
+ for (unsigned int i = 0; i < msgElementLength; i += 5)
+ SendTMCSignal(msgElement[2], msgElement+3+i);
+
+ return msgElementLength + 2;
+}
+
+unsigned int CDVDRadioRDSData::DecodeEPPTransmitterInfo(const uint8_t* msgElement)
+{
+ if (!m_RDS_SlowLabelingCodesPresent && m_PI_CountryCode != 0)
+ {
+ int codeHigh = msgElement[2]&0xF0;
+ int codeLow = msgElement[2]&0x0F;
+ if (codeLow > 7)
+ {
+ CLog::Log(LOGERROR, "Radio RDS - {} - invalid country code {:#02X}{:02X}", __FUNCTION__,
+ codeHigh, codeLow);
+ return 7;
+ }
+
+ std::string countryName;
+ switch (codeHigh)
+ {
+ case 0xA0:
+ countryName = piCountryCodes_A[m_PI_CountryCode-1][codeLow];
+ break;
+ case 0xD0:
+ countryName = piCountryCodes_D[m_PI_CountryCode-1][codeLow];
+ break;
+ case 0xE0:
+ countryName = piCountryCodes_E[m_PI_CountryCode-1][codeLow];
+ break;
+ case 0xF0:
+ countryName = piCountryCodes_F[m_PI_CountryCode-1][codeLow];
+ break;
+ default:
+ CLog::Log(LOGERROR, "Radio RDS - {} - invalid extended country region code:{:02X}{:02X}",
+ __FUNCTION__, codeHigh, codeLow);
+ return 7;
+ }
+
+ // The United States, Canada, and Mexico use the RBDS standard
+ m_RDS_IsRBDS = (countryName == "US" || countryName == "CA" || countryName == "MX");
+
+ m_currentInfoTag->SetCountry(countryName);
+ }
+
+ return 7;
+}
+
+/* SLOW LABELLING: see page 23 in the standard
+ * for paging see page 90, Annex M in the standard (NOT IMPLEMENTED)
+ * for extended country codes see page 69, Annex D.2 in the standard
+ * for language codes see page 84, Annex J in the standard
+ * for emergency warning systems (EWS) see page 53 in the standard */
+#define VARCODE_PAGING_EXTCOUNTRYCODE 0
+#define VARCODE_TMC_IDENT 1
+#define VARCODE_PAGING_IDENT 2
+#define VARCODE_LANGUAGE_CODES 3
+#define VARCODE_OWN_BROADCASTER 6
+#define VARCODE_EWS_CHANNEL_IDENT 7
+unsigned int CDVDRadioRDSData::DecodeSlowLabelingCodes(const uint8_t* msgElement)
+{
+ uint16_t slowLabellingCode = (msgElement[2]<<8 | msgElement[3]) & 0xfff;
+ int VariantCode = (msgElement[2]>>4) & 0x7;
+
+ switch (VariantCode)
+ {
+ case VARCODE_PAGING_EXTCOUNTRYCODE: // paging + ecc
+ {
+ // int paging = (slowLabellingCode>>8)&0x0f; unused
+
+ if (m_PI_CountryCode != 0)
+ {
+ int codeHigh = slowLabellingCode&0xF0;
+ int codeLow = slowLabellingCode&0x0F;
+ if (codeLow > 5)
+ {
+ CLog::Log(LOGERROR, "Radio RDS - {} - invalid country code {:#02X}{:02X}", __FUNCTION__,
+ codeHigh, codeLow);
+ return 4;
+ }
+
+ std::string countryName;
+ switch (codeHigh)
+ {
+ case 0xA0:
+ countryName = piCountryCodes_A[m_PI_CountryCode-1][codeLow];
+ break;
+ case 0xD0:
+ countryName = piCountryCodes_D[m_PI_CountryCode-1][codeLow];
+ break;
+ case 0xE0:
+ countryName = piCountryCodes_E[m_PI_CountryCode-1][codeLow];
+ break;
+ case 0xF0:
+ countryName = piCountryCodes_F[m_PI_CountryCode-1][codeLow];
+ break;
+ default:
+ CLog::Log(LOGERROR,
+ "Radio RDS - {} - invalid extended country region code:{:02X}{:02X}",
+ __FUNCTION__, codeHigh, codeLow);
+ return 4;
+ }
+
+ m_currentInfoTag->SetCountry(countryName);
+ }
+ break;
+ }
+ case VARCODE_LANGUAGE_CODES: // language codes
+ if (slowLabellingCode > 1 && slowLabellingCode < 0x80)
+ m_currentInfoTag->SetLanguage(piRDSLanguageCodes[slowLabellingCode]);
+ else
+ CLog::Log(LOGERROR, "Radio RDS - {} - invalid language code {}", __FUNCTION__,
+ slowLabellingCode);
+ break;
+
+ case VARCODE_TMC_IDENT: // TMC identification
+ case VARCODE_PAGING_IDENT: // Paging identification
+ case VARCODE_OWN_BROADCASTER:
+ case VARCODE_EWS_CHANNEL_IDENT:
+ default:
+ break;
+ }
+
+ m_RDS_SlowLabelingCodesPresent = true;
+ return 4;
+}
+
+/*!
+ * currently unused need to be checked on DAB, processed here to have length of it
+ */
+unsigned int CDVDRadioRDSData::DecodeDABDynLabelCmd(const uint8_t* msgElement, unsigned int len)
+{
+ unsigned int msgElementLength = msgElement[1];
+ if (msgElementLength < 1 || msgElementLength + 2 > len)
+ {
+ m_UECPDataDeadBreak = true;
+ return 0;
+ }
+
+ return msgElementLength+2;
+}
+
+/*!
+ * currently unused need to be checked on DAB, processed here to have length of it
+ */
+unsigned int CDVDRadioRDSData::DecodeDABDynLabelMsg(const uint8_t* msgElement, unsigned int len)
+{
+ unsigned int msgElementLength = msgElement[1];
+ if (msgElementLength < 2 || msgElementLength + 2 > len)
+ {
+ m_UECPDataDeadBreak = true;
+ return 0;
+ }
+
+ return msgElementLength+2;
+}
+
+/*!
+ * unused processed here to have length of it
+ */
+unsigned int CDVDRadioRDSData::DecodeAF(uint8_t *msgElement, unsigned int len)
+{
+ unsigned int msgElementLength = msgElement[3];
+ if (msgElementLength < 3 || msgElementLength + 4 > len)
+ {
+ m_UECPDataDeadBreak = true;
+ return 0;
+ }
+
+ return msgElementLength+4;
+}
+
+/*!
+ * unused processed here to have length of it
+ */
+unsigned int CDVDRadioRDSData::DecodeEonAF(uint8_t *msgElement, unsigned int len)
+{
+ unsigned int msgElementLength = msgElement[3];
+ if (msgElementLength < 4 || msgElementLength + 4 > len)
+ {
+ m_UECPDataDeadBreak = true;
+ return 0;
+ }
+
+ return msgElementLength+4;
+}
+
+/*!
+ * unused processed here to have length of it
+ */
+unsigned int CDVDRadioRDSData::DecodeTDC(uint8_t *msgElement, unsigned int len)
+{
+ unsigned int msgElementLength = msgElement[1];
+ if (msgElementLength < 2 || msgElementLength+2 > len)
+ {
+ m_UECPDataDeadBreak = true;
+ return 0;
+ }
+
+ return msgElementLength+2;
+}
+
+void CDVDRadioRDSData::SendTMCSignal(unsigned int flags, uint8_t *data)
+{
+ if (!(flags & 0x80) && (memcmp(data, m_TMC_LastData, 5) == 0))
+ return;
+
+ memcpy(m_TMC_LastData, data, 5);
+
+ if (m_currentChannel)
+ {
+ CVariant msg(CVariant::VariantTypeObject);
+ msg["channel"] = m_currentChannel->ChannelName();
+ msg["ident"] = m_PI_Current;
+ msg["flags"] = flags;
+ msg["x"] = m_TMC_LastData[0];
+ msg["y"] = (unsigned int)(m_TMC_LastData[1]<<8 | m_TMC_LastData[2]);
+ msg["z"] = (unsigned int)(m_TMC_LastData[3]<<8 | m_TMC_LastData[4]);
+
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::PVR, "RDSRadioTMC", msg);
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerRadioRDS.h b/xbmc/cores/VideoPlayer/VideoPlayerRadioRDS.h
new file mode 100644
index 0000000..0e593c1
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerRadioRDS.h
@@ -0,0 +1,144 @@
+/*
+ * 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 "DVDMessageQueue.h"
+#include "IVideoPlayer.h"
+#include "threads/Thread.h"
+#include "utils/Stopwatch.h"
+
+#include <memory>
+
+class CDVDStreamInfo;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRRadioRDSInfoTag;
+}
+
+/// --- CDVDRadioRDSData ------------------------------------------------------------
+
+#define UECP_DATA_START 0xFE /*!< A data record starts with the start byte */
+#define UECP_DATA_STOP 0xFF /*!< A data record stops with the stop byte */
+#define UECP_SIZE_MAX 263 /*!< The Max possible size of a UECP packet
+ max. 255(MSG)+4(ADD/SQC/MFL)+2(CRC)+2(Start/Stop) of RDS-data */
+#define RT_MEL 65
+#define MAX_RTPC 50
+
+class CDVDRadioRDSData : public CThread, public IDVDStreamPlayer
+{
+public:
+ explicit CDVDRadioRDSData(CProcessInfo &processInfo);
+ ~CDVDRadioRDSData() override;
+
+ bool CheckStream(const CDVDStreamInfo& hints);
+ bool OpenStream(CDVDStreamInfo hints) override;
+ void CloseStream(bool bWaitForBuffers) override;
+ void Flush();
+
+ // waits until all available data has been rendered
+ void WaitForBuffers() { m_messageQueue.WaitUntilEmpty(); }
+ bool AcceptsData() const override { return !m_messageQueue.IsFull(); }
+ void SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority = 0) override
+ {
+ if (m_messageQueue.IsInited())
+ m_messageQueue.Put(pMsg, priority);
+ }
+ void FlushMessages() override { m_messageQueue.Flush(); }
+ bool IsInited() const override { return true; }
+ bool IsStalled() const override { return true; }
+
+protected:
+ void OnExit() override;
+ void Process() override;
+
+private:
+ void ResetRDSCache();
+ void ProcessUECP(const unsigned char *Data, unsigned int Length);
+
+ inline unsigned int DecodePI(const uint8_t* msgElement);
+ inline unsigned int DecodePS(uint8_t *msgElement);
+ inline unsigned int DecodeDI(const uint8_t* msgElement);
+ inline unsigned int DecodeTA_TP(const uint8_t* msgElement);
+ inline unsigned int DecodeMS(const uint8_t* msgElement);
+ inline unsigned int DecodePTY(const uint8_t* msgElement);
+ inline unsigned int DecodePTYN(uint8_t *msgElement);
+ inline unsigned int DecodeRT(uint8_t *msgElement, unsigned int len);
+ inline unsigned int DecodeRTC(uint8_t *msgElement);
+ inline unsigned int DecodeODA(uint8_t *msgElement, unsigned int len);
+ inline unsigned int DecodeRTPlus(uint8_t *msgElement, unsigned int len);
+ inline unsigned int DecodeTMC(uint8_t *msgElement, unsigned int len);
+ inline unsigned int DecodeEPPTransmitterInfo(const uint8_t* msgElement);
+ inline unsigned int DecodeSlowLabelingCodes(const uint8_t* msgElement);
+ inline unsigned int DecodeDABDynLabelCmd(const uint8_t* msgElement, unsigned int len);
+ inline unsigned int DecodeDABDynLabelMsg(const uint8_t* msgElement, unsigned int len);
+ inline unsigned int DecodeAF(uint8_t *msgElement, unsigned int len);
+ inline unsigned int DecodeEonAF(uint8_t *msgElement, unsigned int len);
+ inline unsigned int DecodeTDC(uint8_t *msgElement, unsigned int len);
+
+ void SendTMCSignal(unsigned int flags, uint8_t *data);
+ void SetRadioStyle(const std::string& genre);
+
+ std::shared_ptr<PVR::CPVRRadioRDSInfoTag> m_currentInfoTag;
+ std::shared_ptr<PVR::CPVRChannel> m_currentChannel;
+ bool m_currentFileUpdate;
+ int m_speed;
+ CCriticalSection m_critSection;
+ CDVDMessageQueue m_messageQueue;
+
+ uint8_t m_UECPData[UECP_SIZE_MAX+1];
+ unsigned int m_UECPDataIndex;
+ bool m_UECPDataStart;
+ bool m_UECPDatabStuff;
+ bool m_UECPDataDeadBreak;
+
+ bool m_RDS_IsRBDS;
+ bool m_RDS_SlowLabelingCodesPresent;
+
+ uint16_t m_PI_Current;
+ unsigned int m_PI_CountryCode;
+ unsigned int m_PI_ProgramType;
+ unsigned int m_PI_ProgramReferenceNumber;
+
+ unsigned int m_EPP_TM_INFO_ExtendedCountryCode;
+
+ bool m_DI_IsStereo;
+ bool m_DI_ArtificialHead;
+ bool m_DI_Compressed;
+ bool m_DI_DynamicPTY;
+
+ bool m_TA_TP_TrafficAdvisory;
+ float m_TA_TP_TrafficVolume;
+
+ bool m_MS_SpeechActive;
+
+ int m_PTY;
+ char m_PTYN[9];
+ bool m_PTYN_Present;
+
+ bool m_RT_NewItem;
+
+ uint8_t m_RTPlus_WorkText[RT_MEL+1];
+ bool m_RTPlus_TToggle;
+ int m_RTPlus_iDiffs;
+ CStopWatch m_RTPlus_iTime;
+ bool m_RTPlus_GenrePresent;
+ char m_RTPlus_Temptext[RT_MEL];
+ bool m_RTPlus_Show;
+ char m_RTPlus_Title[RT_MEL];
+ char m_RTPlus_Artist[RT_MEL];
+ int m_RTPlus_iToggle;
+ unsigned int m_RTPlus_ItemToggle;
+ time_t m_RTPlus_Starttime;
+
+ CDateTime m_RTC_DateTime; ///< From RDS transmitted date / time data
+
+ uint8_t m_TMC_LastData[5];
+};
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerSubtitle.cpp b/xbmc/cores/VideoPlayer/VideoPlayerSubtitle.cpp
new file mode 100644
index 0000000..2632090
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerSubtitle.cpp
@@ -0,0 +1,215 @@
+/*
+ * 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 "VideoPlayerSubtitle.h"
+
+#include "DVDCodecs/DVDFactoryCodec.h"
+#include "DVDCodecs/Overlay/DVDOverlay.h"
+#include "DVDCodecs/Overlay/DVDOverlayCodec.h"
+#include "DVDCodecs/Overlay/DVDOverlaySpu.h"
+#include "DVDSubtitles/DVDSubtitleParser.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+CVideoPlayerSubtitle::CVideoPlayerSubtitle(CDVDOverlayContainer* pOverlayContainer, CProcessInfo &processInfo)
+: IDVDStreamPlayer(processInfo)
+{
+ m_pOverlayContainer = pOverlayContainer;
+ m_lastPts = DVD_NOPTS_VALUE;
+}
+
+CVideoPlayerSubtitle::~CVideoPlayerSubtitle()
+{
+ CloseStream(true);
+}
+
+
+void CVideoPlayerSubtitle::Flush()
+{
+ SendMessage(std::make_shared<CDVDMsg>(CDVDMsg::GENERAL_FLUSH), 0);
+}
+
+void CVideoPlayerSubtitle::SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (pMsg->IsType(CDVDMsg::DEMUXER_PACKET))
+ {
+ auto pMsgDemuxerPacket = std::static_pointer_cast<CDVDMsgDemuxerPacket>(pMsg);
+ DemuxPacket* pPacket = pMsgDemuxerPacket->GetPacket();
+
+ if (m_pOverlayCodec)
+ {
+ OverlayMessage result = m_pOverlayCodec->Decode(pPacket);
+
+ if (result == OverlayMessage::OC_OVERLAY)
+ {
+ CDVDOverlay* overlay;
+
+ while ((overlay = m_pOverlayCodec->GetOverlay()))
+ {
+ m_pOverlayContainer->ProcessAndAddOverlayIfValid(overlay);
+ overlay->Release();
+ }
+ }
+ }
+ else if (m_streaminfo.codec == AV_CODEC_ID_DVD_SUBTITLE)
+ {
+ CDVDOverlaySpu* pSPUInfo = m_dvdspus.AddData(pPacket->pData, pPacket->iSize, pPacket->pts);
+ if (pSPUInfo)
+ {
+ CLog::Log(LOGDEBUG, "CVideoPlayer::ProcessSubData: Got complete SPU packet");
+ m_pOverlayContainer->ProcessAndAddOverlayIfValid(pSPUInfo);
+ pSPUInfo->Release();
+ }
+ }
+
+ }
+ else if( pMsg->IsType(CDVDMsg::SUBTITLE_CLUTCHANGE) )
+ {
+ auto pData = std::static_pointer_cast<CDVDMsgSubtitleClutChange>(pMsg);
+ for (int i = 0; i < 16; i++)
+ {
+ uint8_t* color = m_dvdspus.m_clut[i];
+ uint8_t* t = (uint8_t*)pData->m_data[i];
+
+// pData->m_data[i] points to an uint32_t
+// Byte swapping is needed between big and little endian systems
+#ifdef WORDS_BIGENDIAN
+ color[0] = t[1]; // Y
+ color[1] = t[2]; // Cr
+ color[2] = t[3]; // Cb
+#else
+ color[0] = t[2]; // Y
+ color[1] = t[0]; // Cr
+ color[2] = t[1]; // Cb
+#endif
+ }
+ m_dvdspus.m_bHasClut = true;
+ }
+ else if( pMsg->IsType(CDVDMsg::GENERAL_FLUSH)
+ || pMsg->IsType(CDVDMsg::GENERAL_RESET) )
+ {
+ m_dvdspus.Reset();
+ if (m_pSubtitleFileParser)
+ m_pSubtitleFileParser->Reset();
+
+ if (m_pOverlayCodec)
+ m_pOverlayCodec->Flush();
+
+ /* We must flush active overlays on flush or if we have a file
+ * parser since it will re-populate active items. */
+ if (pMsg->IsType(CDVDMsg::GENERAL_FLUSH) || m_pSubtitleFileParser)
+ m_pOverlayContainer->Flush();
+
+ m_lastPts = DVD_NOPTS_VALUE;
+ }
+}
+
+bool CVideoPlayerSubtitle::OpenStream(CDVDStreamInfo &hints, std::string &filename)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ CloseStream(false);
+ m_streaminfo = hints;
+
+ // okey check if this is a filesubtitle
+ if(filename.size() && filename != "dvd" )
+ {
+ m_pSubtitleFileParser.reset(CDVDFactorySubtitle::CreateParser(filename));
+ if (!m_pSubtitleFileParser)
+ {
+ CLog::Log(LOGERROR, "{} - Unable to create subtitle parser", __FUNCTION__);
+ CloseStream(true);
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "Created subtitles parser: {}", m_pSubtitleFileParser->GetName());
+
+ if (!m_pSubtitleFileParser->Open(hints))
+ {
+ CLog::Log(LOGERROR, "{} - Unable to init subtitle parser", __FUNCTION__);
+ CloseStream(true);
+ return false;
+ }
+ m_pSubtitleFileParser->Reset();
+ return true;
+ }
+
+ // dvd's use special subtitle decoder
+ if(hints.codec == AV_CODEC_ID_DVD_SUBTITLE && filename == "dvd")
+ return true;
+
+ m_pOverlayCodec = CDVDFactoryCodec::CreateOverlayCodec(hints);
+ if (m_pOverlayCodec)
+ {
+ CLog::Log(LOGDEBUG, "Created subtitles overlay codec: {}", m_pOverlayCodec->GetName());
+ return true;
+ }
+
+ CLog::Log(LOGERROR, "{} - Unable to init overlay codec", __FUNCTION__);
+ return false;
+}
+
+void CVideoPlayerSubtitle::CloseStream(bool bWaitForBuffers)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ m_pSubtitleFileParser.reset();
+ m_pOverlayCodec.reset();
+
+ m_dvdspus.FlushCurrentPacket();
+
+ if (!bWaitForBuffers)
+ m_pOverlayContainer->Clear();
+}
+
+void CVideoPlayerSubtitle::Process(double pts, double offset)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (m_pSubtitleFileParser)
+ {
+ if(pts == DVD_NOPTS_VALUE)
+ return;
+
+ if (pts + DVD_SEC_TO_TIME(1) < m_lastPts)
+ {
+ m_pOverlayContainer->Clear();
+ m_pSubtitleFileParser->Reset();
+ }
+
+ if(m_pOverlayContainer->GetSize() >= 5)
+ return;
+
+ CDVDOverlay* pOverlay = m_pSubtitleFileParser->Parse(pts);
+ // add all overlays which fit the pts
+ while(pOverlay)
+ {
+ pOverlay->iPTSStartTime -= offset;
+ if(pOverlay->iPTSStopTime != 0.0)
+ pOverlay->iPTSStopTime -= offset;
+
+ m_pOverlayContainer->ProcessAndAddOverlayIfValid(pOverlay);
+ pOverlay->Release();
+ pOverlay = m_pSubtitleFileParser->Parse(pts);
+ }
+
+ m_lastPts = pts;
+ }
+}
+
+bool CVideoPlayerSubtitle::AcceptsData() const
+{
+ // FIXME : This may still be causing problems + magic number :(
+ return m_pOverlayContainer->GetSize() < 5;
+}
+
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerSubtitle.h b/xbmc/cores/VideoPlayer/VideoPlayerSubtitle.h
new file mode 100644
index 0000000..23baecc
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerSubtitle.h
@@ -0,0 +1,69 @@
+/*
+ * 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 "DVDDemuxSPU.h"
+#include "DVDMessageQueue.h"
+#include "DVDOverlayContainer.h"
+#include "DVDStreamInfo.h"
+#include "DVDSubtitles/DVDFactorySubtitle.h"
+#include "IVideoPlayer.h"
+
+class CDVDInputStream;
+class CDVDSubtitleStream;
+class CDVDSubtitleParser;
+class CDVDInputStreamNavigator;
+class CDVDOverlayCodec;
+
+class CVideoPlayerSubtitle : public IDVDStreamPlayer
+{
+public:
+ CVideoPlayerSubtitle(CDVDOverlayContainer* pOverlayContainer, CProcessInfo &processInfo);
+ ~CVideoPlayerSubtitle() override;
+
+ void Process(double pts, double offset);
+ void Flush();
+ void FindSubtitles(const char* strFilename);
+ int GetSubtitleCount();
+
+ void UpdateOverlayInfo(const std::shared_ptr<CDVDInputStreamNavigator>& pStream, int iAction)
+ {
+ m_pOverlayContainer->UpdateOverlayInfo(pStream, &m_dvdspus, iAction);
+ }
+
+ bool AcceptsData() const override;
+ void SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority = 0) override;
+ void FlushMessages() override {}
+ bool OpenStream(CDVDStreamInfo hints) override { return OpenStream(hints, hints.filename); }
+ bool OpenStream(CDVDStreamInfo &hints, std::string& filename);
+ void CloseStream(bool bWaitForBuffers) override;
+
+ bool IsInited() const override { return true; }
+ bool IsStalled() const override { return m_pOverlayContainer->GetSize() == 0; }
+private:
+ CDVDOverlayContainer* m_pOverlayContainer;
+
+ std::unique_ptr<CDVDSubtitleParser> m_pSubtitleFileParser;
+ std::unique_ptr<CDVDOverlayCodec> m_pOverlayCodec;
+ CDVDDemuxSPU m_dvdspus;
+
+ CDVDStreamInfo m_streaminfo;
+ double m_lastPts;
+
+
+ CCriticalSection m_section;
+};
+
+
+//typedef struct SubtitleInfo
+//{
+
+//
+//} SubtitleInfo;
+
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerTeletext.cpp b/xbmc/cores/VideoPlayer/VideoPlayerTeletext.cpp
new file mode 100644
index 0000000..289cc29
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerTeletext.cpp
@@ -0,0 +1,760 @@
+/*
+ * 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 "VideoPlayerTeletext.h"
+
+#include "DVDStreamInfo.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+const uint8_t rev_lut[32] =
+{
+ 0x00,0x08,0x04,0x0c, /* upper nibble */
+ 0x02,0x0a,0x06,0x0e,
+ 0x01,0x09,0x05,0x0d,
+ 0x03,0x0b,0x07,0x0f,
+ 0x00,0x80,0x40,0xc0, /* lower nibble */
+ 0x20,0xa0,0x60,0xe0,
+ 0x10,0x90,0x50,0xd0,
+ 0x30,0xb0,0x70,0xf0
+};
+
+void CDVDTeletextTools::NextDec(int *i) /* skip to next decimal */
+{
+ (*i)++;
+
+ if ((*i & 0x0F) > 0x09)
+ *i += 0x06;
+
+ if ((*i & 0xF0) > 0x90)
+ *i += 0x60;
+
+ if (*i > 0x899)
+ *i = 0x100;
+}
+
+void CDVDTeletextTools::PrevDec(int *i) /* counting down */
+{
+ (*i)--;
+
+ if ((*i & 0x0F) > 0x09)
+ *i -= 0x06;
+
+ if ((*i & 0xF0) > 0x90)
+ *i -= 0x60;
+
+ if (*i < 0x100)
+ *i = 0x899;
+}
+
+/* print hex-number into string, s points to last digit, caller has to provide enough space, no termination */
+void CDVDTeletextTools::Hex2Str(char *s, unsigned int n)
+{
+ do {
+ char c = (n & 0xF);
+ *s-- = number2char(c);
+ n >>= 4;
+ } while (n);
+}
+
+signed int CDVDTeletextTools::deh24(unsigned char *p)
+{
+ int e = hamm24par[0][p[0]]
+ ^ hamm24par[1][p[1]]
+ ^ hamm24par[2][p[2]];
+
+ int x = hamm24val[p[0]]
+ + (p[1] & 127) * 16
+ + (p[2] & 127) * 2048;
+
+ return (x ^ hamm24cor[e]) | hamm24err[e];
+}
+
+
+CDVDTeletextData::CDVDTeletextData(CProcessInfo &processInfo)
+: CThread("DVDTeletextData")
+, IDVDStreamPlayer(processInfo)
+, m_messageQueue("teletext")
+{
+ m_speed = DVD_PLAYSPEED_NORMAL;
+
+ m_messageQueue.SetMaxDataSize(40 * 256 * 1024);
+
+ /* Initialize Data structures */
+ memset(&m_TXTCache->astCachetable, 0, sizeof(m_TXTCache->astCachetable));
+ memset(&m_TXTCache->astP29, 0, sizeof(m_TXTCache->astP29));
+ ResetTeletextCache();
+}
+
+CDVDTeletextData::~CDVDTeletextData()
+{
+ StopThread();
+ ResetTeletextCache();
+}
+
+bool CDVDTeletextData::CheckStream(CDVDStreamInfo &hints)
+{
+ if (hints.codec == AV_CODEC_ID_DVB_TELETEXT)
+ return true;
+
+ return false;
+}
+
+bool CDVDTeletextData::OpenStream(CDVDStreamInfo hints)
+{
+ CloseStream(true);
+
+ m_messageQueue.Init();
+
+ if (hints.codec == AV_CODEC_ID_DVB_TELETEXT)
+ {
+ CLog::Log(LOGINFO, "Creating teletext data thread");
+ Create();
+ return true;
+ }
+
+ return false;
+}
+
+void CDVDTeletextData::CloseStream(bool bWaitForBuffers)
+{
+ m_messageQueue.Abort();
+
+ // wait for decode_video thread to end
+ CLog::Log(LOGINFO, "waiting for teletext data thread to exit");
+
+ StopThread(); // will set this->m_bStop to true
+
+ m_messageQueue.End();
+ ResetTeletextCache();
+}
+
+
+void CDVDTeletextData::ResetTeletextCache()
+{
+ std::unique_lock<CCriticalSection> lock(m_TXTCache->m_critSection);
+
+ /* Reset Data structures */
+ for (auto& pages : m_TXTCache->astCachetable)
+ {
+ for (TextCachedPage_t*& page : pages)
+ {
+ if (page)
+ {
+ TextPageinfo_t *p = &(page->pageinfo);
+ if (p->p24)
+ free(p->p24);
+
+ if (p->ext)
+ {
+ if (p->ext->p27)
+ free(p->ext->p27);
+
+ for (unsigned char* const d26 : p->ext->p26)
+ {
+ if (d26)
+ free(d26);
+ }
+ free(p->ext);
+ }
+ delete page;
+ page = 0;
+ }
+ }
+ }
+
+ for (int i = 0; i < 9; i++)
+ {
+ if (m_TXTCache->astP29[i])
+ {
+ if (m_TXTCache->astP29[i]->p27)
+ free(m_TXTCache->astP29[i]->p27);
+
+ for (unsigned char* const d26 : m_TXTCache->astP29[i]->p26)
+ {
+ if (d26)
+ free(d26);
+ }
+ free(m_TXTCache->astP29[i]);
+ m_TXTCache->astP29[i] = 0;
+ }
+ m_TXTCache->CurrentPage[i] = -1;
+ m_TXTCache->CurrentSubPage[i] = -1;
+ }
+
+ memset(&m_TXTCache->SubPageTable, 0xFF, sizeof(m_TXTCache->SubPageTable));
+ memset(&m_TXTCache->astP29, 0, sizeof(m_TXTCache->astP29));
+ memset(&m_TXTCache->BasicTop, 0, sizeof(m_TXTCache->BasicTop));
+ memset(&m_TXTCache->ADIPTable, 0, sizeof(m_TXTCache->ADIPTable));
+ memset(&m_TXTCache->FlofPages, 0, sizeof(m_TXTCache->FlofPages));
+ memset(&m_TXTCache->SubtitlePages, 0, sizeof(m_TXTCache->SubtitlePages));
+ memset(&m_TXTCache->astCachetable, 0, sizeof(m_TXTCache->astCachetable));
+ memset(&m_TXTCache->TimeString, 0x20, 8);
+
+ m_TXTCache->NationalSubset = NAT_DEFAULT;/* default */
+ m_TXTCache->NationalSubsetSecondary = NAT_DEFAULT;
+ m_TXTCache->ZapSubpageManual = false;
+ m_TXTCache->PageUpdate = false;
+ m_TXTCache->ADIP_PgMax = -1;
+ m_TXTCache->BTTok = false;
+ m_TXTCache->CachedPages = 0;
+ m_TXTCache->PageReceiving = -1;
+ m_TXTCache->Page = 0x100;
+ m_TXTCache->SubPage = m_TXTCache->SubPageTable[m_TXTCache->Page];
+ m_TXTCache->line30 = "";
+ if (m_TXTCache->SubPage == 0xff)
+ m_TXTCache->SubPage = 0;
+}
+
+void CDVDTeletextData::Process()
+{
+ int b1, b2, b3, b4;
+ int packet_number;
+ TextPageinfo_t *pageinfo_thread;
+ unsigned char vtxt_row[42];
+ unsigned char pagedata[9][23*40];
+ unsigned char magazine = 0xff;
+// int doupdate = 0;
+
+ CLog::Log(LOGINFO, "running thread: CDVDTeletextData");
+
+ while (!m_bStop)
+ {
+ std::shared_ptr<CDVDMsg> pMsg;
+ int iPriority = (m_speed == DVD_PLAYSPEED_PAUSE) ? 1 : 0;
+ MsgQueueReturnCode ret = m_messageQueue.Get(pMsg, 2000, iPriority);
+
+ if (ret == MSGQ_TIMEOUT)
+ {
+ /* Timeout for Teletext is not a bad thing, so we continue without error */
+ continue;
+ }
+
+ if (MSGQ_IS_ERROR(ret))
+ {
+ if (!m_messageQueue.ReceivedAbortRequest())
+ CLog::Log(LOGERROR, "MSGQ_IS_ERROR returned true ({})", ret);
+
+ break;
+ }
+
+ if (pMsg->IsType(CDVDMsg::DEMUXER_PACKET))
+ {
+ std::unique_lock<CCriticalSection> lock(m_TXTCache->m_critSection);
+
+ DemuxPacket* pPacket = std::static_pointer_cast<CDVDMsgDemuxerPacket>(pMsg)->GetPacket();
+ uint8_t *Datai = pPacket->pData;
+ int rows = (pPacket->iSize - 1) / 46;
+
+ /* Is it a ITU-R System B Teletext stream in acc. to EN 300 472 */
+ if (Datai[0] >= 0x10 && Datai[0] <= 0x1F) /* Check we have a valid data identifier */
+ {
+ /* Go thru the pages stored inside this frame */
+ for (int row=0; row < rows; row++)
+ {
+ uint8_t *vtx_rowbyte = &Datai[(row*46)+1];
+
+ /* Check for valid data_unit_id */
+ if ((vtx_rowbyte[0] == 0x02 || vtx_rowbyte[0] == 0x03) && (vtx_rowbyte[1] == 0x2C))
+ {
+ /* clear rowbuffer */
+ /* convert row from lsb to msb (begin with magazine number) */
+ for (int i = 4; i < 46; i++)
+ {
+ uint8_t upper = (vtx_rowbyte[i] >> 4) & 0xf;
+ uint8_t lower = vtx_rowbyte[i] & 0xf;
+ vtxt_row[i-4] = (rev_lut[upper]) | (rev_lut[lower+16]);
+ }
+
+ /* get packet number */
+ b1 = dehamming[vtxt_row[0]];
+ b2 = dehamming[vtxt_row[1]];
+
+ if (b1 == 0xFF || b2 == 0xFF)
+ continue;
+
+ b1 &= 8;
+
+ /* get packet and magazine number */
+ packet_number = b1>>3 | b2<<1;
+ magazine = dehamming[vtxt_row[0]] & 7;
+ if (!magazine) magazine = 8;
+
+ if (packet_number == 0 && m_TXTCache->CurrentPage[magazine] != -1 && m_TXTCache->CurrentSubPage[magazine] != -1)
+ SavePage(m_TXTCache->CurrentPage[magazine], m_TXTCache->CurrentSubPage[magazine], pagedata[magazine]);
+
+ /* analyze row */
+ if (packet_number == 0)
+ {
+ /* get pagenumber */
+ b2 = dehamming[vtxt_row[3]];
+ b3 = dehamming[vtxt_row[2]];
+
+ if (b2 == 0xFF || b3 == 0xFF)
+ {
+ m_TXTCache->CurrentPage[magazine] = m_TXTCache->PageReceiving = -1;
+ continue;
+ }
+
+ m_TXTCache->CurrentPage[magazine] = m_TXTCache->PageReceiving = magazine<<8 | b2<<4 | b3;
+
+ if (b2 == 0x0f && b3 == 0x0f)
+ {
+ m_TXTCache->CurrentSubPage[magazine] = -1; /* ?ff: ignore data transmissions */
+ continue;
+ }
+
+ /* get subpagenumber */
+ b1 = dehamming[vtxt_row[7]];
+ b2 = dehamming[vtxt_row[6]];
+ b3 = dehamming[vtxt_row[5]];
+ b4 = dehamming[vtxt_row[4]];
+
+ if (b1 == 0xFF || b2 == 0xFF || b3 == 0xFF || b4 == 0xFF)
+ {
+ m_TXTCache->CurrentSubPage[magazine] = -1;
+ continue;
+ }
+
+ b1 &= 3;
+ b3 &= 7;
+
+ if (IsDec(m_TXTCache->PageReceiving)) /* ignore other subpage bits for hex pages */
+ m_TXTCache->CurrentSubPage[magazine] = b3<<4 | b4;
+ else
+ m_TXTCache->CurrentSubPage[magazine] = b4; /* max 16 subpages for hex pages */
+
+ /* store current subpage for this page */
+ m_TXTCache->SubPageTable[m_TXTCache->CurrentPage[magazine]] = m_TXTCache->CurrentSubPage[magazine];
+
+ AllocateCache(magazine);
+ LoadPage(m_TXTCache->CurrentPage[magazine], m_TXTCache->CurrentSubPage[magazine], pagedata[magazine]);
+ pageinfo_thread = &(m_TXTCache->astCachetable[m_TXTCache->CurrentPage[magazine]][m_TXTCache->CurrentSubPage[magazine]]->pageinfo);
+ if (!pageinfo_thread)
+ continue;
+
+ if ((m_TXTCache->PageReceiving & 0xff) == 0xfe) /* ?fe: magazine organization table (MOT) */
+ pageinfo_thread->function = FUNC_MOT;
+
+ /* check controlbits */
+ if (dehamming[vtxt_row[5]] & 8) /* C4 -> erase page */
+ {
+ memset(m_TXTCache->astCachetable[m_TXTCache->CurrentPage[magazine]][m_TXTCache->CurrentSubPage[magazine]]->data, ' ', 23*40);
+ memset(pagedata[magazine],' ', 23*40);
+ }
+// if (dehamming[vtxt_row[9]] & 8) /* C8 -> update page */
+// doupdate = m_TXTCache->PageReceiving;
+
+ pageinfo_thread->boxed = !!(dehamming[vtxt_row[7]] & 0x0c);
+
+ /* get country control bits */
+ b1 = dehamming[vtxt_row[9]];
+ if (b1 != 0xFF)
+ {
+ pageinfo_thread->nationalvalid = 1;
+ pageinfo_thread->national = rev_lut[b1] & 0x07;
+ }
+
+ if (dehamming[vtxt_row[7]] & 0x08)// subtitle page
+ {
+ int i = 0, found = -1, use = -1;
+ for (; i < 8; i++)
+ {
+ if (use == -1 && !m_TXTCache->SubtitlePages[i].page)
+ use = i;
+ else if (m_TXTCache->SubtitlePages[i].page == m_TXTCache->PageReceiving)
+ {
+ found = i;
+ use = i;
+ break;
+ }
+ }
+ if (found == -1 && use != -1)
+ m_TXTCache->SubtitlePages[use].page = m_TXTCache->PageReceiving;
+ if (use != -1)
+ m_TXTCache->SubtitlePages[use].language = CountryConversionTable[pageinfo_thread->national];
+ }
+
+ /* check parity, copy line 0 to cache (start and end 8 bytes are not needed and used otherwise) */
+ unsigned char *p = m_TXTCache->astCachetable[m_TXTCache->CurrentPage[magazine]][m_TXTCache->CurrentSubPage[magazine]]->p0;
+ for (int i = 10; i < 42-8; i++)
+ *p++ = deparity[vtxt_row[i]];
+
+ if (!IsDec(m_TXTCache->PageReceiving))
+ continue; /* valid hex page number: just copy headline, ignore TimeString */
+
+ /* copy TimeString */
+ p = m_TXTCache->TimeString;
+ for (int i = 42-8; i < 42; i++)
+ *p++ = deparity[vtxt_row[i]];
+ }
+ else if (packet_number == 29 && dehamming[vtxt_row[2]]== 0) /* packet 29/0 replaces 28/0 for a whole magazine */
+ {
+ Decode_p2829(vtxt_row, &(m_TXTCache->astP29[magazine]));
+ }
+ else if (m_TXTCache->CurrentPage[magazine] != -1 && m_TXTCache->CurrentSubPage[magazine] != -1)
+ /* packet>0, 0 has been correctly received, buffer allocated */
+ {
+ pageinfo_thread = &(m_TXTCache->astCachetable[m_TXTCache->CurrentPage[magazine]][m_TXTCache->CurrentSubPage[magazine]]->pageinfo);
+ if (!pageinfo_thread)
+ continue;
+
+ /* pointer to current info struct */
+ if (packet_number <= 25)
+ {
+ unsigned char *p = NULL;
+ if (packet_number < 24)
+ {
+ p = pagedata[magazine] + 40*(packet_number-1);
+ }
+ else
+ {
+ if (!(pageinfo_thread->p24))
+ pageinfo_thread->p24 = (unsigned char*) calloc(2, 40);
+ if (pageinfo_thread->p24)
+ p = pageinfo_thread->p24 + (packet_number - 24) * 40;
+ }
+ if (p)
+ {
+ if (IsDec(m_TXTCache->CurrentPage[magazine]))
+ {
+ for (int i = 2; i < 42; i++)
+ {
+ *p++ = vtxt_row[i] & 0x7f; /* allow values with parity errors as some channels don't care :( */
+ }
+ }
+ else if ((m_TXTCache->CurrentPage[magazine] & 0xff) == 0xfe)
+ {
+ for (int i = 2; i < 42; i++)
+ {
+ *p++ = dehamming[vtxt_row[i]]; /* decode hamming 8/4 */
+ }
+ }
+ else /* other hex page: no parity check, just copy */
+ memcpy(p, &vtxt_row[2], 40);
+ }
+ }
+ else if (packet_number == 27)
+ {
+ int descode = dehamming[vtxt_row[2]]; /* designation code (0..15) */
+ if (descode == 0xff)
+ continue;
+
+ if (descode == 0) // reading FLOF-Pagelinks
+ {
+ b1 = dehamming[vtxt_row[0]];
+ if (b1 != 0xff)
+ {
+ b1 &= 7;
+
+ for (int i = 0; i < FLOFSIZE; i++)
+ {
+ b2 = dehamming[vtxt_row[4+i*6]];
+ b3 = dehamming[vtxt_row[3+i*6]];
+
+ if (b2 != 0xff && b3 != 0xff)
+ {
+ b4 = ((b1 ^ (dehamming[vtxt_row[8+i*6]]>>1)) & 6) | ((b1 ^ (dehamming[vtxt_row[6+i*6]]>>3)) & 1);
+ if (b4 == 0)
+ b4 = 8;
+ if (b2 <= 9 && b3 <= 9)
+ m_TXTCache->FlofPages[m_TXTCache->CurrentPage[magazine] ][i] = b4<<8 | b2<<4 | b3;
+ }
+ }
+
+ /* copy last 2 links to ADIPTable for TOP-Index */
+ if (pageinfo_thread->p24) /* packet 24 received */
+ {
+ int a, a1, e=39, l=3;
+ unsigned char *p = pageinfo_thread->p24;
+ do
+ {
+ for (;
+ l >= 2 && 0 == m_TXTCache->FlofPages[m_TXTCache->CurrentPage[magazine]][l];
+ l--)
+ ; /* find used linkindex */
+ for (;
+ e >= 1 && !isalnum(p[e]);
+ e--)
+ ; /* find end */
+ for (a = a1 = e - 1;
+ a >= 0 && p[a] >= ' ';
+ a--) /* find start */
+ if (p[a] > ' ')
+ a1 = a; /* first non-space */
+ if (a >= 0 && l >= 2)
+ {
+ strncpy(m_TXTCache->ADIPTable[m_TXTCache->FlofPages[m_TXTCache->CurrentPage[magazine]][l]], (const char*) &p[a1], 12);
+ if (e-a1 < 11)
+ m_TXTCache->ADIPTable[m_TXTCache->FlofPages[m_TXTCache->CurrentPage[magazine]][l]][e-a1+1] = '\0';
+ }
+ e = a - 1;
+ l--;
+ } while (l >= 2);
+ }
+ }
+ }
+ else if (descode == 4) /* level 2.5 links (ignore level 3.5 links of /4 and /5) */
+ {
+ int i;
+ Textp27_t *p;
+
+ if (!pageinfo_thread->ext)
+ pageinfo_thread->ext = (TextExtData_t*) calloc(1, sizeof(TextExtData_t));
+ if (!pageinfo_thread->ext)
+ continue;
+ if (!(pageinfo_thread->ext->p27))
+ pageinfo_thread->ext->p27 = (Textp27_t*) calloc(4, sizeof(Textp27_t));
+ if (!(pageinfo_thread->ext->p27))
+ continue;
+ p = pageinfo_thread->ext->p27;
+ for (i = 0; i < 4; i++)
+ {
+ int d1 = CDVDTeletextTools::deh24(&vtxt_row[6*i + 3]);
+ int d2 = CDVDTeletextTools::deh24(&vtxt_row[6*i + 6]);
+ if (d1 < 0 || d2 < 0)
+ continue;
+
+ p->local = i & 0x01;
+ p->drcs = !!(i & 0x02);
+ p->l25 = !!(d1 & 0x04);
+ p->l35 = !!(d1 & 0x08);
+ p->page =
+ (((d1 & 0x000003c0) >> 6) |
+ ((d1 & 0x0003c000) >> (14-4)) |
+ ((d1 & 0x00003800) >> (11-8))) ^
+ (dehamming[vtxt_row[0]] << 8);
+ if (p->page < 0x100)
+ p->page += 0x800;
+ p->subpage = d2 >> 2;
+ if ((p->page & 0xff) == 0xff)
+ p->page = 0;
+ else if (p->page > 0x899)
+ {
+ // workaround for crash on RTL Shop ...
+ // sorry.. i dont understand whats going wrong here :)
+ continue;
+ }
+ else if (m_TXTCache->astCachetable[p->page][0]) /* link valid && linked page cached */
+ {
+ TextPageinfo_t *pageinfo_link = &(m_TXTCache->astCachetable[p->page][0]->pageinfo);
+ if (p->local)
+ pageinfo_link->function = p->drcs ? FUNC_DRCS : FUNC_POP;
+ else
+ pageinfo_link->function = p->drcs ? FUNC_GDRCS : FUNC_GPOP;
+ }
+ p++; /* */
+ }
+ }
+ }
+ else if (packet_number == 26)
+ {
+ int descode = dehamming[vtxt_row[2]]; /* designation code (0..15) */
+ if (descode == 0xff)
+ continue;
+
+ if (!pageinfo_thread->ext)
+ pageinfo_thread->ext = (TextExtData_t*) calloc(1, sizeof(TextExtData_t));
+ if (!pageinfo_thread->ext)
+ continue;
+ if (!(pageinfo_thread->ext->p26[descode]))
+ pageinfo_thread->ext->p26[descode] = (unsigned char*) malloc(13 * 3);
+ if (pageinfo_thread->ext->p26[descode])
+ memcpy(pageinfo_thread->ext->p26[descode], &vtxt_row[3], 13 * 3);
+ }
+ else if (packet_number == 28)
+ {
+ int descode = dehamming[vtxt_row[2]]; /* designation code (0..15) */
+
+ if (descode == 0xff)
+ continue;
+
+ if (descode != 2)
+ {
+ int t1 = CDVDTeletextTools::deh24(&vtxt_row[7-4]);
+ pageinfo_thread->function = t1 & 0x0f;
+ if (!pageinfo_thread->nationalvalid)
+ {
+ pageinfo_thread->nationalvalid = 1;
+ pageinfo_thread->national = (t1>>4) & 0x07;
+ }
+ }
+
+ switch (descode) /* designation code */
+ {
+ case 0: /* basic level 1 page */
+ Decode_p2829(vtxt_row, &(pageinfo_thread->ext));
+ break;
+ case 1: /* G0/G1 designation for older decoders, level 3.5: DCLUT4/16, colors for multicolored bitmaps */
+ break; /* ignore */
+ case 2: /* page key */
+ break; /* ignore */
+ case 3: /* types of PTUs in DRCS */
+ break; //! @todo implement
+ case 4: /* CLUTs 0/1, only level 3.5 */
+ break; /* ignore */
+ default:
+ break; /* invalid, ignore */
+ } /* switch designation code */
+ }
+ else if (packet_number == 30)
+ {
+ m_TXTCache->line30 = "";
+ for (int i=26-4; i <= 45-4; i++) /* station ID */
+ m_TXTCache->line30.append(1, deparity[vtxt_row[i]]);
+ }
+ }
+
+ /* set update flag */
+ if (m_TXTCache->CurrentPage[magazine] == m_TXTCache->Page && m_TXTCache->CurrentSubPage[magazine] != -1)
+ {
+ SavePage(m_TXTCache->CurrentPage[magazine], m_TXTCache->CurrentSubPage[magazine], pagedata[magazine]);
+ m_TXTCache->PageUpdate = true;
+// doupdate = 0;
+ if (!m_TXTCache->ZapSubpageManual)
+ m_TXTCache->SubPage = m_TXTCache->CurrentSubPage[magazine];
+ }
+ }
+ }
+ }
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SETSPEED))
+ {
+ m_speed = std::static_pointer_cast<CDVDMsgInt>(pMsg)->m_value;
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_FLUSH)
+ || pMsg->IsType(CDVDMsg::GENERAL_RESET))
+ {
+ ResetTeletextCache();
+ }
+ }
+}
+
+void CDVDTeletextData::OnExit()
+{
+ CLog::Log(LOGINFO, "thread end: data_thread");
+}
+
+void CDVDTeletextData::Flush()
+{
+ if(!m_messageQueue.IsInited())
+ return;
+ /* flush using message as this get's called from VideoPlayer thread */
+ /* and any demux packet that has been taken out of queue need to */
+ /* be disposed of before we flush */
+ m_messageQueue.Flush();
+ m_messageQueue.Put(std::make_shared<CDVDMsg>(CDVDMsg::GENERAL_FLUSH));
+}
+
+void CDVDTeletextData::Decode_p2829(unsigned char *vtxt_row, TextExtData_t **ptExtData)
+{
+ unsigned int bitsleft, colorindex;
+ unsigned char *p;
+ int t1 = CDVDTeletextTools::deh24(&vtxt_row[7-4]);
+ int t2 = CDVDTeletextTools::deh24(&vtxt_row[10-4]);
+
+ if (t1 < 0 || t2 < 0)
+ return;
+
+ if (!(*ptExtData))
+ (*ptExtData) = (TextExtData_t*) calloc(1, sizeof(TextExtData_t));
+ if (!(*ptExtData))
+ return;
+
+ (*ptExtData)->p28Received = 1;
+ (*ptExtData)->DefaultCharset = (t1>>7) & 0x7f;
+ (*ptExtData)->SecondCharset = ((t1>>14) & 0x0f) | ((t2<<4) & 0x70);
+ (*ptExtData)->LSP = !!(t2 & 0x08);
+ (*ptExtData)->RSP = !!(t2 & 0x10);
+ (*ptExtData)->SPL25 = !!(t2 & 0x20);
+ (*ptExtData)->LSPColumns = (t2>>6) & 0x0f;
+
+ bitsleft = 8; /* # of bits not evaluated in val */
+ t2 >>= 10; /* current data */
+ p = &vtxt_row[13-4]; /* pointer to next data triplet */
+ for (colorindex = 0; colorindex < 16; colorindex++)
+ {
+ if (bitsleft < 12)
+ {
+ t2 |= CDVDTeletextTools::deh24(p) << bitsleft;
+ if (t2 < 0) /* hamming error */
+ break;
+ p += 3;
+ bitsleft += 18;
+ }
+ (*ptExtData)->bgr[colorindex] = t2 & 0x0fff;
+ bitsleft -= 12;
+ t2 >>= 12;
+ }
+ if (t2 < 0 || bitsleft != 14)
+ {
+ (*ptExtData)->p28Received = 0;
+ return;
+ }
+ (*ptExtData)->DefScreenColor = t2 & 0x1f;
+ t2 >>= 5;
+ (*ptExtData)->DefRowColor = t2 & 0x1f;
+ (*ptExtData)->BlackBgSubst = !!(t2 & 0x20);
+ t2 >>= 6;
+ (*ptExtData)->ColorTableRemapping = t2 & 0x07;
+}
+
+void CDVDTeletextData::SavePage(int p, int sp, unsigned char* buffer)
+{
+ std::unique_lock<CCriticalSection> lock(m_TXTCache->m_critSection);
+ TextCachedPage_t* pg = m_TXTCache->astCachetable[p][sp];
+ if (!pg)
+ {
+ CLog::Log(LOGERROR, "CDVDTeletextData: trying to save a not allocated page!!");
+ return;
+ }
+
+ memcpy(pg->data, buffer, 23*40);
+}
+
+void CDVDTeletextData::LoadPage(int p, int sp, unsigned char* buffer)
+{
+ std::unique_lock<CCriticalSection> lock(m_TXTCache->m_critSection);
+ TextCachedPage_t* pg = m_TXTCache->astCachetable[p][sp];
+ if (!pg)
+ {
+ CLog::Log(LOGERROR, "CDVDTeletextData: trying to load a not allocated page!!");
+ return;
+ }
+
+ memcpy(buffer, pg->data, 23*40);
+}
+
+void CDVDTeletextData::ErasePage(int magazine)
+{
+ std::unique_lock<CCriticalSection> lock(m_TXTCache->m_critSection);
+ TextCachedPage_t* pg = m_TXTCache->astCachetable[m_TXTCache->CurrentPage[magazine]][m_TXTCache->CurrentSubPage[magazine]];
+ if (pg)
+ {
+ memset(&(pg->pageinfo), 0, sizeof(TextPageinfo_t)); /* struct pageinfo */
+ memset(pg->p0, ' ', 24);
+ memset(pg->data, ' ', 23*40);
+ }
+}
+
+void CDVDTeletextData::AllocateCache(int magazine)
+{
+ /* check cachetable and allocate memory if needed */
+ if (m_TXTCache->astCachetable[m_TXTCache->CurrentPage[magazine]][m_TXTCache->CurrentSubPage[magazine]] == 0)
+ {
+ m_TXTCache->astCachetable[m_TXTCache->CurrentPage[magazine]][m_TXTCache->CurrentSubPage[magazine]] = new TextCachedPage_t;
+ if (m_TXTCache->astCachetable[m_TXTCache->CurrentPage[magazine]][m_TXTCache->CurrentSubPage[magazine]] )
+ {
+ ErasePage(magazine);
+ m_TXTCache->CachedPages++;
+ }
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerTeletext.h b/xbmc/cores/VideoPlayer/VideoPlayerTeletext.h
new file mode 100644
index 0000000..b613b82
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerTeletext.h
@@ -0,0 +1,59 @@
+/*
+ * 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 "DVDMessageQueue.h"
+#include "IVideoPlayer.h"
+#include "threads/Thread.h"
+#include "video/TeletextDefines.h"
+
+class CDVDStreamInfo;
+
+class CDVDTeletextData : public CThread, public IDVDStreamPlayer
+{
+public:
+ explicit CDVDTeletextData(CProcessInfo &processInfo);
+ ~CDVDTeletextData() override;
+
+ bool CheckStream(CDVDStreamInfo &hints);
+ bool OpenStream(CDVDStreamInfo hints) override;
+ void CloseStream(bool bWaitForBuffers) override;
+ void Flush();
+
+ // waits until all available data has been rendered
+ void WaitForBuffers() { m_messageQueue.WaitUntilEmpty(); }
+ bool AcceptsData() const override { return !m_messageQueue.IsFull(); }
+ void SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority = 0) override
+ {
+ if (m_messageQueue.IsInited())
+ m_messageQueue.Put(pMsg, priority);
+ }
+ void FlushMessages() override { m_messageQueue.Flush(); }
+ bool IsInited() const override { return true; }
+ bool IsStalled() const override { return true; }
+
+ std::shared_ptr<TextCacheStruct_t> GetTeletextCache() { return m_TXTCache; }
+ void LoadPage(int p, int sp, unsigned char* buffer);
+
+protected:
+ void OnExit() override;
+ void Process() override;
+
+private:
+ void ResetTeletextCache();
+ void Decode_p2829(unsigned char *vtxt_row, TextExtData_t **ptExtData);
+ void SavePage(int p, int sp, unsigned char* buffer);
+ void ErasePage(int magazine);
+ void AllocateCache(int magazine);
+
+ int m_speed;
+ std::shared_ptr<TextCacheStruct_t> m_TXTCache = std::make_shared<TextCacheStruct_t>();
+ CDVDMessageQueue m_messageQueue;
+};
+
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerVideo.cpp b/xbmc/cores/VideoPlayer/VideoPlayerVideo.cpp
new file mode 100644
index 0000000..20f6b3b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerVideo.cpp
@@ -0,0 +1,1166 @@
+/*
+ * 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 "VideoPlayerVideo.h"
+
+#include "DVDCodecs/DVDCodecUtils.h"
+#include "DVDCodecs/DVDFactoryCodec.h"
+#include "DVDCodecs/Video/DVDVideoCodecFFmpeg.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/Interface/DemuxPacket.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/MathUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include <iomanip>
+#include <iterator>
+#include <mutex>
+#include <numeric>
+#include <sstream>
+
+using namespace std::chrono_literals;
+
+class CDVDMsgVideoCodecChange : public CDVDMsg
+{
+public:
+ CDVDMsgVideoCodecChange(const CDVDStreamInfo& hints, std::unique_ptr<CDVDVideoCodec> codec)
+ : CDVDMsg(GENERAL_STREAMCHANGE), m_codec(std::move(codec)), m_hints(hints)
+ {}
+ ~CDVDMsgVideoCodecChange() override = default;
+
+ std::unique_ptr<CDVDVideoCodec> m_codec;
+ CDVDStreamInfo m_hints;
+};
+
+
+CVideoPlayerVideo::CVideoPlayerVideo(CDVDClock* pClock
+ ,CDVDOverlayContainer* pOverlayContainer
+ ,CDVDMessageQueue& parent
+ ,CRenderManager& renderManager
+ ,CProcessInfo &processInfo)
+: CThread("VideoPlayerVideo")
+, IDVDStreamPlayerVideo(processInfo)
+, m_messageQueue("video")
+, m_messageParent(parent)
+, m_renderManager(renderManager)
+{
+ m_pClock = pClock;
+ m_pOverlayContainer = pOverlayContainer;
+ m_speed = DVD_PLAYSPEED_NORMAL;
+
+ m_bRenderSubs = false;
+ m_paused = false;
+ m_syncState = IDVDStreamPlayer::SYNC_STARTING;
+ m_iSubtitleDelay = 0;
+ m_iLateFrames = 0;
+ m_iDroppedRequest = 0;
+ m_fForcedAspectRatio = 0;
+ m_messageQueue.SetMaxDataSize(40 * 1024 * 1024);
+ m_messageQueue.SetMaxTimeSize(8.0);
+
+ m_iDroppedFrames = 0;
+ m_fFrameRate = 25;
+ m_fStableFrameRate = 0.0;
+ m_iFrameRateCount = 0;
+ m_bAllowDrop = false;
+ m_iFrameRateErr = 0;
+ m_iFrameRateLength = 0;
+ m_bFpsInvalid = false;
+}
+
+CVideoPlayerVideo::~CVideoPlayerVideo()
+{
+ m_bAbortOutput = true;
+ StopThread();
+}
+
+double CVideoPlayerVideo::GetOutputDelay()
+{
+ double time = m_messageQueue.GetPacketCount(CDVDMsg::DEMUXER_PACKET);
+ if( m_fFrameRate )
+ time = (time * DVD_TIME_BASE) / m_fFrameRate;
+ else
+ time = 0.0;
+
+ if( m_speed != 0 )
+ time = time * DVD_PLAYSPEED_NORMAL / abs(m_speed);
+
+ return time;
+}
+
+bool CVideoPlayerVideo::OpenStream(CDVDStreamInfo hint)
+{
+ if (hint.flags & AV_DISPOSITION_ATTACHED_PIC)
+ return false;
+ if (hint.extrasize == 0)
+ {
+ // codecs which require extradata
+ // clang-format off
+ if (hint.codec == AV_CODEC_ID_NONE ||
+ hint.codec == AV_CODEC_ID_MPEG1VIDEO ||
+ hint.codec == AV_CODEC_ID_MPEG2VIDEO ||
+ hint.codec == AV_CODEC_ID_H264 ||
+ hint.codec == AV_CODEC_ID_HEVC ||
+ hint.codec == AV_CODEC_ID_MPEG4 ||
+ hint.codec == AV_CODEC_ID_WMV3 ||
+ hint.codec == AV_CODEC_ID_VC1 ||
+ hint.codec == AV_CODEC_ID_AV1)
+ // clang-format on
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "Creating video codec with codec id: {}", hint.codec);
+
+ if (m_messageQueue.IsInited())
+ {
+ if (m_pVideoCodec && !m_processInfo.IsVideoHwDecoder())
+ {
+ hint.codecOptions |= CODEC_ALLOW_FALLBACK;
+ }
+
+ std::unique_ptr<CDVDVideoCodec> codec = CDVDFactoryCodec::CreateVideoCodec(hint, m_processInfo);
+ if (!codec)
+ {
+ CLog::Log(LOGINFO, "CVideoPlayerVideo::OpenStream - could not open video codec");
+ }
+
+ SendMessage(std::make_shared<CDVDMsgVideoCodecChange>(hint, std::move(codec)), 0);
+ }
+ else
+ {
+ m_processInfo.ResetVideoCodecInfo();
+ hint.codecOptions |= CODEC_ALLOW_FALLBACK;
+
+ std::unique_ptr<CDVDVideoCodec> codec = CDVDFactoryCodec::CreateVideoCodec(hint, m_processInfo);
+ if (!codec)
+ {
+ CLog::Log(LOGERROR, "CVideoPlayerVideo::OpenStream - could not open video codec");
+ return false;
+ }
+
+ OpenStream(hint, std::move(codec));
+ CLog::Log(LOGINFO, "Creating video thread");
+ m_messageQueue.Init();
+ m_processInfo.SetLevelVQ(0);
+ Create();
+ }
+ return true;
+}
+
+void CVideoPlayerVideo::OpenStream(CDVDStreamInfo& hint, std::unique_ptr<CDVDVideoCodec> codec)
+{
+ CLog::Log(LOGDEBUG, "CVideoPlayerVideo::OpenStream - open stream with codec id: {}", hint.codec);
+
+ m_processInfo.GetVideoBufferManager().ReleasePools();
+
+ //reported fps is usually not completely correct
+ if (hint.fpsrate && hint.fpsscale)
+ {
+ m_fFrameRate = DVD_TIME_BASE / CDVDCodecUtils::NormalizeFrameduration((double)DVD_TIME_BASE * hint.fpsscale / hint.fpsrate);
+ m_bFpsInvalid = false;
+ m_processInfo.SetVideoFps(static_cast<float>(m_fFrameRate));
+ }
+ else
+ {
+ m_fFrameRate = 25;
+ m_bFpsInvalid = true;
+ m_processInfo.SetVideoFps(0);
+ }
+
+ m_ptsTracker.ResetVFRDetection();
+ ResetFrameRateCalc();
+
+ m_iDroppedRequest = 0;
+ m_iLateFrames = 0;
+
+ if( m_fFrameRate > 120 || m_fFrameRate < 5 )
+ {
+ CLog::Log(LOGERROR,
+ "CVideoPlayerVideo::OpenStream - Invalid framerate {}, using forced 25fps and just "
+ "trust timestamps",
+ (int)m_fFrameRate);
+ m_fFrameRate = 25;
+ }
+
+ // use aspect in stream if available
+ if (hint.forced_aspect)
+ m_fForcedAspectRatio = static_cast<float>(hint.aspect);
+ else
+ m_fForcedAspectRatio = 0.0f;
+
+ if (m_pVideoCodec && m_pVideoCodec->Reconfigure(hint))
+ {
+ // reuse old decoder
+ codec = std::move(m_pVideoCodec);
+ }
+
+ m_pVideoCodec.reset();
+
+ if (!codec)
+ {
+ CLog::Log(LOGINFO, "Creating video codec with codec id: {}", hint.codec);
+ hint.codecOptions |= CODEC_ALLOW_FALLBACK;
+ codec = CDVDFactoryCodec::CreateVideoCodec(hint, m_processInfo);
+ if (!codec)
+ {
+ CLog::Log(LOGERROR, "CVideoPlayerVideo::OpenStream - could not open video codec");
+ m_messageParent.Put(std::make_shared<CDVDMsg>(CDVDMsg::PLAYER_ABORT));
+ StopThread();
+ }
+ }
+
+ m_pVideoCodec = std::move(codec);
+ m_hints = hint;
+ m_stalled = m_messageQueue.GetPacketCount(CDVDMsg::DEMUXER_PACKET) == 0;
+ m_rewindStalled = false;
+ m_packets.clear();
+ m_syncState = IDVDStreamPlayer::SYNC_STARTING;
+ m_renderManager.ShowVideo(false);
+}
+
+void CVideoPlayerVideo::CloseStream(bool bWaitForBuffers)
+{
+ // wait until buffers are empty
+ if (bWaitForBuffers && m_speed > 0)
+ {
+ SendMessage(std::make_shared<CDVDMsg>(CDVDMsg::VIDEO_DRAIN), 0);
+ m_messageQueue.WaitUntilEmpty();
+ }
+
+ m_messageQueue.Abort();
+
+ // wait for decode_video thread to end
+ CLog::Log(LOGINFO, "waiting for video thread to exit");
+
+ m_bAbortOutput = true;
+ StopThread();
+
+ m_messageQueue.End();
+
+ CLog::Log(LOGINFO, "deleting video codec");
+ m_pVideoCodec.reset();
+
+ if (m_picture.videoBuffer)
+ {
+ m_picture.videoBuffer->Release();
+ m_picture.videoBuffer = nullptr;
+ }
+}
+
+bool CVideoPlayerVideo::AcceptsData() const
+{
+ bool full = m_messageQueue.IsFull();
+ return !full;
+}
+
+bool CVideoPlayerVideo::HasData() const
+{
+ return m_messageQueue.GetDataSize() > 0;
+}
+
+bool CVideoPlayerVideo::IsInited() const
+{
+ return m_messageQueue.IsInited();
+}
+
+inline void CVideoPlayerVideo::SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority)
+{
+ m_messageQueue.Put(pMsg, priority);
+ m_processInfo.SetLevelVQ(m_messageQueue.GetLevel());
+}
+
+inline void CVideoPlayerVideo::SendMessageBack(const std::shared_ptr<CDVDMsg>& pMsg, int priority)
+{
+ m_messageQueue.PutBack(pMsg, priority);
+ m_processInfo.SetLevelVQ(m_messageQueue.GetLevel());
+}
+
+inline void CVideoPlayerVideo::FlushMessages()
+{
+ m_messageQueue.Flush();
+ m_processInfo.SetLevelVQ(m_messageQueue.GetLevel());
+}
+
+inline MsgQueueReturnCode CVideoPlayerVideo::GetMessage(std::shared_ptr<CDVDMsg>& pMsg,
+ unsigned int iTimeoutInMilliSeconds,
+ int& priority)
+{
+ MsgQueueReturnCode ret = m_messageQueue.Get(pMsg, iTimeoutInMilliSeconds, priority);
+ m_processInfo.SetLevelVQ(m_messageQueue.GetLevel());
+ return ret;
+}
+
+void CVideoPlayerVideo::Process()
+{
+ CLog::Log(LOGINFO, "running thread: video_thread");
+
+ double pts = 0;
+ double frametime = (double)DVD_TIME_BASE / m_fFrameRate;
+
+ bool bRequestDrop = false;
+ int iDropDirective;
+ bool onlyPrioMsgs = false;
+
+ m_videoStats.Start();
+ m_droppingStats.Reset();
+ m_iDroppedFrames = 0;
+ m_rewindStalled = false;
+ m_outputSate = OUTPUT_NORMAL;
+
+ while (!m_bStop)
+ {
+ int iQueueTimeOut = (int)(m_stalled ? frametime : frametime * 10) / 1000;
+ int iPriority = 0;
+
+ if (m_syncState == IDVDStreamPlayer::SYNC_WAITSYNC)
+ iPriority = 1;
+
+ if (m_paused)
+ iPriority = 1;
+
+ if (onlyPrioMsgs)
+ {
+ iPriority = 1;
+ iQueueTimeOut = 1;
+ }
+
+ std::shared_ptr<CDVDMsg> pMsg;
+ MsgQueueReturnCode ret = GetMessage(pMsg, iQueueTimeOut, iPriority);
+
+ onlyPrioMsgs = false;
+
+ if (MSGQ_IS_ERROR(ret))
+ {
+ if (!m_messageQueue.ReceivedAbortRequest())
+ CLog::Log(LOGERROR, "MSGQ_IS_ERROR returned true ({})", ret);
+
+ break;
+ }
+ else if (ret == MSGQ_TIMEOUT)
+ {
+ if (m_outputSate == OUTPUT_AGAIN &&
+ m_picture.videoBuffer)
+ {
+ m_outputSate = OutputPicture(&m_picture);
+ if (m_outputSate == OUTPUT_AGAIN)
+ {
+ onlyPrioMsgs = true;
+ continue;
+ }
+ }
+ // don't ask for a new frame if we can't deliver it to renderer
+ else if ((m_speed != DVD_PLAYSPEED_PAUSE ||
+ m_processInfo.IsFrameAdvance() ||
+ m_syncState != IDVDStreamPlayer::SYNC_INSYNC) && !m_paused)
+ {
+ if (ProcessDecoderOutput(frametime, pts))
+ {
+ onlyPrioMsgs = true;
+ continue;
+ }
+ }
+
+ // if we only wanted priority messages, this isn't a stall
+ if (iPriority)
+ continue;
+
+ //Okey, start rendering at stream fps now instead, we are likely in a stillframe
+ if (!m_stalled)
+ {
+ // squeeze pictures out
+ while (!m_bStop && m_pVideoCodec)
+ {
+ m_pVideoCodec->SetCodecControl(DVD_CODEC_CTRL_DRAIN);
+ if (!ProcessDecoderOutput(frametime, pts))
+ break;
+ }
+
+ CLog::Log(LOGDEBUG, "CVideoPlayerVideo - Stillframe detected, switching to forced {:f} fps",
+ m_fFrameRate);
+ m_stalled = true;
+ pts += frametime * 4;
+ }
+
+ // Waiting timed out, output last picture
+ if (m_picture.videoBuffer)
+ {
+ m_picture.pts = pts;
+ m_outputSate = OutputPicture(&m_picture);
+ pts += frametime;
+ }
+
+ continue;
+ }
+
+ if (pMsg->IsType(CDVDMsg::GENERAL_SYNCHRONIZE))
+ {
+ if (std::static_pointer_cast<CDVDMsgGeneralSynchronize>(pMsg)->Wait(100ms, SYNCSOURCE_VIDEO))
+ {
+ CLog::Log(LOGDEBUG, "CVideoPlayerVideo - CDVDMsg::GENERAL_SYNCHRONIZE");
+ }
+ else
+ SendMessage(pMsg, 1); /* push back as prio message, to process other prio messages */
+ m_droppingStats.Reset();
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_RESYNC))
+ {
+ pts = std::static_pointer_cast<CDVDMsgDouble>(pMsg)->m_value;
+
+ m_syncState = IDVDStreamPlayer::SYNC_INSYNC;
+ m_droppingStats.Reset();
+ m_rewindStalled = false;
+ m_renderManager.ShowVideo(true);
+
+ CLog::Log(LOGDEBUG, "CVideoPlayerVideo - CDVDMsg::GENERAL_RESYNC({:f})", pts);
+ }
+ else if (pMsg->IsType(CDVDMsg::VIDEO_SET_ASPECT))
+ {
+ CLog::Log(LOGDEBUG, "CVideoPlayerVideo - CDVDMsg::VIDEO_SET_ASPECT");
+ m_fForcedAspectRatio = static_cast<float>(*std::static_pointer_cast<CDVDMsgDouble>(pMsg));
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_RESET))
+ {
+ if(m_pVideoCodec)
+ m_pVideoCodec->Reset();
+
+ if (m_picture.videoBuffer)
+ {
+ m_picture.videoBuffer->Release();
+ m_picture.videoBuffer = nullptr;
+ }
+ m_packets.clear();
+ m_droppingStats.Reset();
+ m_syncState = IDVDStreamPlayer::SYNC_STARTING;
+ m_renderManager.ShowVideo(false);
+ m_rewindStalled = false;
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_FLUSH)) // private message sent by (CVideoPlayerVideo::Flush())
+ {
+ bool sync = std::static_pointer_cast<CDVDMsgBool>(pMsg)->m_value;
+ if(m_pVideoCodec)
+ m_pVideoCodec->Reset();
+
+ if (m_picture.videoBuffer)
+ {
+ m_picture.videoBuffer->Release();
+ m_picture.videoBuffer = nullptr;
+ }
+ m_packets.clear();
+ pts = 0;
+ m_rewindStalled = false;
+
+ m_ptsTracker.Flush();
+ //we need to recalculate the framerate
+ //! @todo this needs to be set on a streamchange instead
+ ResetFrameRateCalc();
+ m_droppingStats.Reset();
+
+ m_stalled = true;
+ if (sync)
+ {
+ m_syncState = IDVDStreamPlayer::SYNC_STARTING;
+ m_renderManager.ShowVideo(false);
+ }
+
+ m_renderManager.DiscardBuffer();
+ FlushMessages();
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_SETSPEED))
+ {
+ m_speed = std::static_pointer_cast<CDVDMsgInt>(pMsg)->m_value;
+ if (m_pVideoCodec)
+ m_pVideoCodec->SetSpeed(m_speed);
+
+ m_droppingStats.Reset();
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_STREAMCHANGE))
+ {
+ auto msg = std::static_pointer_cast<CDVDMsgVideoCodecChange>(pMsg);
+
+ while (!m_bStop && m_pVideoCodec)
+ {
+ m_pVideoCodec->SetCodecControl(DVD_CODEC_CTRL_DRAIN);
+ bool cont = ProcessDecoderOutput(frametime, pts);
+
+ if (!cont)
+ break;
+ }
+
+ OpenStream(msg->m_hints, std::move(msg->m_codec));
+ msg->m_codec = NULL;
+ if (m_picture.videoBuffer)
+ {
+ m_picture.videoBuffer->Release();
+ m_picture.videoBuffer = nullptr;
+ }
+ }
+ else if (pMsg->IsType(CDVDMsg::VIDEO_DRAIN))
+ {
+ while (!m_bStop && m_pVideoCodec)
+ {
+ m_pVideoCodec->SetCodecControl(DVD_CODEC_CTRL_DRAIN);
+ if (!ProcessDecoderOutput(frametime, pts))
+ break;
+ }
+ }
+ else if (pMsg->IsType(CDVDMsg::GENERAL_PAUSE))
+ {
+ m_paused = std::static_pointer_cast<CDVDMsgBool>(pMsg)->m_value;
+ CLog::Log(LOGDEBUG, "CVideoPlayerVideo - CDVDMsg::GENERAL_PAUSE: {}", m_paused);
+ }
+ else if (pMsg->IsType(CDVDMsg::PLAYER_REQUEST_STATE))
+ {
+ SStateMsg msg;
+ msg.player = VideoPlayer_VIDEO;
+ msg.syncState = m_syncState;
+ m_messageParent.Put(
+ std::make_shared<CDVDMsgType<SStateMsg>>(CDVDMsg::PLAYER_REPORT_STATE, msg));
+ }
+ else if (pMsg->IsType(CDVDMsg::DEMUXER_PACKET))
+ {
+ DemuxPacket* pPacket = std::static_pointer_cast<CDVDMsgDemuxerPacket>(pMsg)->GetPacket();
+ bool bPacketDrop = std::static_pointer_cast<CDVDMsgDemuxerPacket>(pMsg)->GetPacketDrop();
+
+ if (m_stalled)
+ {
+ CLog::Log(LOGDEBUG, "CVideoPlayerVideo - Stillframe left, switching to normal playback");
+ m_stalled = false;
+ }
+
+ bRequestDrop = false;
+ iDropDirective = CalcDropRequirement(pts);
+ if ((iDropDirective & DROP_VERYLATE) &&
+ m_bAllowDrop &&
+ !bPacketDrop)
+ {
+ bRequestDrop = true;
+ }
+ if (iDropDirective & DROP_DROPPED)
+ {
+ m_iDroppedFrames++;
+ m_ptsTracker.Flush();
+ }
+ if (m_messageQueue.GetDataSize() == 0 || m_speed < 0)
+ {
+ bRequestDrop = false;
+ m_iDroppedRequest = 0;
+ m_iLateFrames = 0;
+ }
+
+ int codecControl = 0;
+ if (iDropDirective & DROP_BUFFER_LEVEL)
+ codecControl |= DVD_CODEC_CTRL_HURRY;
+ if (m_speed > DVD_PLAYSPEED_NORMAL)
+ codecControl |= DVD_CODEC_CTRL_NO_POSTPROC;
+ if (bPacketDrop)
+ codecControl |= DVD_CODEC_CTRL_DROP;
+ if (bRequestDrop)
+ codecControl |= DVD_CODEC_CTRL_DROP_ANY;
+ if (!m_renderManager.Supports(RENDERFEATURE_ROTATION))
+ codecControl |= DVD_CODEC_CTRL_ROTATE;
+ m_pVideoCodec->SetCodecControl(codecControl);
+
+ if (m_pVideoCodec->AddData(*pPacket))
+ {
+ // buffer packets so we can recover should decoder flush for some reason
+ if (m_pVideoCodec->GetConvergeCount() > 0)
+ {
+ m_packets.emplace_back(pMsg, 0);
+ if (m_packets.size() > m_pVideoCodec->GetConvergeCount() ||
+ m_packets.size() * frametime > DVD_SEC_TO_TIME(10))
+ m_packets.pop_front();
+ }
+
+ m_videoStats.AddSampleBytes(pPacket->iSize);
+
+ if (ProcessDecoderOutput(frametime, pts))
+ {
+ onlyPrioMsgs = true;
+ }
+ }
+ else
+ {
+ SendMessageBack(pMsg);
+ onlyPrioMsgs = true;
+ }
+ }
+ }
+}
+
+bool CVideoPlayerVideo::ProcessDecoderOutput(double &frametime, double &pts)
+{
+ CDVDVideoCodec::VCReturn decoderState = m_pVideoCodec->GetPicture(&m_picture);
+
+ if (decoderState == CDVDVideoCodec::VC_BUFFER)
+ {
+ return false;
+ }
+
+ // if decoder was flushed, we need to seek back again to resume rendering
+ if (decoderState == CDVDVideoCodec::VC_FLUSHED)
+ {
+ CLog::Log(LOGDEBUG, "CVideoPlayerVideo - video decoder was flushed");
+ while (!m_packets.empty())
+ {
+ auto msg = std::static_pointer_cast<CDVDMsgDemuxerPacket>(m_packets.front().message);
+ m_packets.pop_front();
+
+ SendMessage(msg, 10);
+ }
+
+ m_pVideoCodec->Reset();
+ m_packets.clear();
+ //picture.iFlags &= ~DVP_FLAG_ALLOCATED;
+ m_renderManager.DiscardBuffer();
+ return false;
+ }
+
+ if (decoderState == CDVDVideoCodec::VC_REOPEN)
+ {
+ while (!m_packets.empty())
+ {
+ auto msg = std::static_pointer_cast<CDVDMsgDemuxerPacket>(m_packets.front().message);
+ m_packets.pop_front();
+ SendMessage(msg, 10);
+ }
+
+ m_pVideoCodec->Reopen();
+ m_packets.clear();
+ m_renderManager.DiscardBuffer();
+ return false;
+ }
+
+ // if decoder had an error, tell it to reset to avoid more problems
+ if (decoderState == CDVDVideoCodec::VC_ERROR)
+ {
+ CLog::Log(LOGDEBUG, "CVideoPlayerVideo - video decoder returned error");
+ return false;
+ }
+
+ if (decoderState == CDVDVideoCodec::VC_EOF)
+ {
+ if (m_syncState == IDVDStreamPlayer::SYNC_STARTING)
+ {
+ SStartMsg msg;
+ msg.player = VideoPlayer_VIDEO;
+ msg.cachetime = DVD_MSEC_TO_TIME(50);
+ msg.cachetotal = DVD_MSEC_TO_TIME(100);
+ msg.timestamp = DVD_NOPTS_VALUE;
+ m_messageParent.Put(std::make_shared<CDVDMsgType<SStartMsg>>(CDVDMsg::PLAYER_STARTED, msg));
+ }
+ return false;
+ }
+
+ // check for a new picture
+ if (decoderState == CDVDVideoCodec::VC_PICTURE)
+ {
+ bool hasTimestamp = true;
+
+ m_picture.iDuration = frametime;
+
+ // validate picture timing,
+ // if both dts/pts invalid, use pts calculated from picture.iDuration
+ // if pts invalid use dts, else use picture.pts as passed
+ if (m_picture.dts == DVD_NOPTS_VALUE && m_picture.pts == DVD_NOPTS_VALUE)
+ {
+ m_picture.pts = pts;
+ hasTimestamp = false;
+ }
+ else if (m_picture.pts == DVD_NOPTS_VALUE)
+ m_picture.pts = m_picture.dts;
+
+ // use forced aspect if any
+ if (m_fForcedAspectRatio != 0.0f)
+ {
+ m_picture.iDisplayWidth = (int) (m_picture.iDisplayHeight * m_fForcedAspectRatio);
+ if (m_picture.iDisplayWidth > m_picture.iWidth)
+ {
+ m_picture.iDisplayWidth = m_picture.iWidth;
+ m_picture.iDisplayHeight = (int) (m_picture.iDisplayWidth / m_fForcedAspectRatio);
+ }
+ }
+
+ // set stereo mode if not set by decoder
+ if (m_picture.stereoMode.empty())
+ {
+ std::string stereoMode;
+ switch(m_processInfo.GetVideoSettings().m_StereoMode)
+ {
+ case RENDER_STEREO_MODE_SPLIT_VERTICAL:
+ stereoMode = "left_right";
+ if (m_processInfo.GetVideoSettings().m_StereoInvert)
+ stereoMode = "right_left";
+ break;
+ case RENDER_STEREO_MODE_SPLIT_HORIZONTAL:
+ stereoMode = "top_bottom";
+ if (m_processInfo.GetVideoSettings().m_StereoInvert)
+ stereoMode = "bottom_top";
+ break;
+ default:
+ stereoMode = m_hints.stereo_mode;
+ break;
+ }
+ if (!stereoMode.empty() && stereoMode != "mono")
+ {
+ m_picture.stereoMode = stereoMode;
+ }
+ }
+
+ // if frame has a pts (usually originating from demux packet), use that
+ if (m_picture.pts != DVD_NOPTS_VALUE)
+ {
+ pts = m_picture.pts;
+ }
+
+ double extraDelay = 0.0;
+ if (m_picture.iRepeatPicture)
+ {
+ extraDelay = m_picture.iRepeatPicture * m_picture.iDuration;
+ m_picture.iDuration += extraDelay;
+ }
+
+ m_picture.pts = pts + extraDelay;
+ // guess next frame pts. iDuration is always valid
+ if (m_speed != 0)
+ pts += m_picture.iDuration * m_speed / abs(m_speed);
+
+ m_outputSate = OutputPicture(&m_picture);
+
+ if (m_outputSate == OUTPUT_AGAIN)
+ {
+ return true;
+ }
+ else if (m_outputSate == OUTPUT_ABORT)
+ {
+ return false;
+ }
+ else if ((m_outputSate == OUTPUT_DROPPED) && !(m_picture.iFlags & DVP_FLAG_DROPPED))
+ {
+ m_iDroppedFrames++;
+ m_ptsTracker.Flush();
+ }
+
+ if (m_syncState == IDVDStreamPlayer::SYNC_STARTING &&
+ m_outputSate != OUTPUT_DROPPED &&
+ !(m_picture.iFlags & DVP_FLAG_DROPPED))
+ {
+ m_syncState = IDVDStreamPlayer::SYNC_WAITSYNC;
+ SStartMsg msg;
+ msg.player = VideoPlayer_VIDEO;
+ msg.cachetime = DVD_MSEC_TO_TIME(50); //! @todo implement
+ msg.cachetotal = DVD_MSEC_TO_TIME(100); //! @todo implement
+ msg.timestamp = hasTimestamp ? (pts + m_renderManager.GetDelay() * 1000) : DVD_NOPTS_VALUE;
+ m_messageParent.Put(std::make_shared<CDVDMsgType<SStartMsg>>(CDVDMsg::PLAYER_STARTED, msg));
+ }
+
+ frametime = (double)DVD_TIME_BASE / m_fFrameRate;
+ }
+
+ return true;
+}
+
+void CVideoPlayerVideo::OnExit()
+{
+ CLog::Log(LOGINFO, "thread end: video_thread");
+}
+
+void CVideoPlayerVideo::SetSpeed(int speed)
+{
+ if(m_messageQueue.IsInited())
+ SendMessage(std::make_shared<CDVDMsgInt>(CDVDMsg::PLAYER_SETSPEED, speed), 1);
+ else
+ m_speed = speed;
+}
+
+void CVideoPlayerVideo::Flush(bool sync)
+{
+ /* flush using message as this get's called from VideoPlayer thread */
+ /* and any demux packet that has been taken out of queue need to */
+ /* be disposed of before we flush */
+ SendMessage(std::make_shared<CDVDMsgBool>(CDVDMsg::GENERAL_FLUSH, sync), 1);
+ m_bAbortOutput = true;
+}
+
+void CVideoPlayerVideo::ProcessOverlays(const VideoPicture* pSource, double pts)
+{
+ // remove any overlays that are out of time
+ if (m_syncState == IDVDStreamPlayer::SYNC_INSYNC)
+ m_pOverlayContainer->CleanUp(pts - m_iSubtitleDelay);
+
+ VecOverlays overlays;
+
+ {
+ std::unique_lock<CCriticalSection> lock(*m_pOverlayContainer);
+
+ VecOverlays* pVecOverlays = m_pOverlayContainer->GetOverlays();
+ VecOverlaysIter it = pVecOverlays->begin();
+
+ //Check all overlays and render those that should be rendered, based on time and forced
+ //Both forced and subs should check timing
+ while (it != pVecOverlays->end())
+ {
+ CDVDOverlay* pOverlay = *it++;
+ if(!pOverlay->bForced && !m_bRenderSubs)
+ continue;
+
+ double pts2 = pOverlay->bForced ? pts : pts - m_iSubtitleDelay;
+
+ if((pOverlay->iPTSStartTime <= pts2 && (pOverlay->iPTSStopTime > pts2 || pOverlay->iPTSStopTime == 0LL)))
+ {
+ if(pOverlay->IsOverlayType(DVDOVERLAY_TYPE_GROUP))
+ overlays.insert(overlays.end(), static_cast<CDVDOverlayGroup*>(pOverlay)->m_overlays.begin()
+ , static_cast<CDVDOverlayGroup*>(pOverlay)->m_overlays.end());
+ else
+ overlays.push_back(pOverlay);
+ }
+ }
+
+ for(it = overlays.begin(); it != overlays.end(); ++it)
+ {
+ double pts2 = (*it)->bForced ? pts : pts - m_iSubtitleDelay;
+ m_renderManager.AddOverlay(*it, pts2);
+ }
+ }
+}
+
+CVideoPlayerVideo::EOutputState CVideoPlayerVideo::OutputPicture(const VideoPicture* pPicture)
+{
+ m_bAbortOutput = false;
+
+ if (m_processInfo.GetVideoStereoMode() != pPicture->stereoMode)
+ {
+ m_processInfo.SetVideoStereoMode(pPicture->stereoMode);
+ // signal about changes in video parameters
+ m_messageParent.Put(std::make_shared<CDVDMsg>(CDVDMsg::PLAYER_AVCHANGE));
+ }
+
+ double config_framerate = m_bFpsInvalid ? 0.0 : m_fFrameRate;
+ if (m_processInfo.GetVideoInterlaced())
+ {
+ if (MathUtils::FloatEquals(config_framerate, 25.0, 0.02))
+ config_framerate = 50.0;
+ else if (MathUtils::FloatEquals(config_framerate, 29.97, 0.02))
+ config_framerate = 59.94;
+ }
+
+ int sorient = m_processInfo.GetVideoSettings().m_Orientation;
+ int orientation = sorient != 0 ? (sorient + m_hints.orientation) % 360
+ : m_hints.orientation;
+
+ if (!m_renderManager.Configure(*pPicture,
+ static_cast<float>(config_framerate),
+ orientation,
+ m_pVideoCodec->GetAllowedReferences()))
+ {
+ CLog::Log(LOGERROR, "{} - failed to configure renderer", __FUNCTION__);
+ return OUTPUT_ABORT;
+ }
+
+ //try to calculate the framerate
+ m_ptsTracker.Add(pPicture->pts);
+ if (!m_stalled)
+ CalcFrameRate();
+
+ // signal to clock what our framerate is, it may want to adjust it's
+ // speed to better match with our video renderer's output speed
+ m_pClock->UpdateFramerate(m_fFrameRate);
+
+ // calculate the time we need to delay this picture before displaying
+ double iPlayingClock, iCurrentClock;
+
+ iPlayingClock = m_pClock->GetClock(iCurrentClock, false); // snapshot current clock
+
+ if (m_speed < 0)
+ {
+ double renderPts;
+ int queued, discard;
+ int lateframes;
+ double inputPts = m_droppingStats.m_lastPts;
+ m_renderManager.GetStats(lateframes, renderPts, queued, discard);
+ if (pPicture->pts > renderPts || queued > 0)
+ {
+ if (inputPts >= renderPts)
+ {
+ m_rewindStalled = true;
+ CThread::Sleep(50ms);
+ }
+ return OUTPUT_DROPPED;
+ }
+ else if (pPicture->pts < iPlayingClock)
+ {
+ return OUTPUT_DROPPED;
+ }
+ }
+
+ if ((pPicture->iFlags & DVP_FLAG_DROPPED))
+ {
+ m_droppingStats.AddOutputDropGain(pPicture->pts, 1);
+ CLog::Log(LOGDEBUG, "{} - dropped in output", __FUNCTION__);
+ return OUTPUT_DROPPED;
+ }
+
+ auto timeToDisplay = std::chrono::milliseconds(DVD_TIME_TO_MSEC(pPicture->pts - iPlayingClock));
+
+ // make sure waiting time is not negative
+ std::chrono::milliseconds maxWaitTime = std::min(std::max(timeToDisplay + 500ms, 50ms), 500ms);
+ // don't wait when going ff
+ if (m_speed > DVD_PLAYSPEED_NORMAL)
+ maxWaitTime = std::max(timeToDisplay, 0ms);
+ int buffer = m_renderManager.WaitForBuffer(m_bAbortOutput, maxWaitTime);
+ if (buffer < 0)
+ {
+ if (m_speed != DVD_PLAYSPEED_PAUSE)
+ CLog::Log(LOGWARNING, "{} - timeout waiting for buffer", __FUNCTION__);
+ return OUTPUT_AGAIN;
+ }
+
+ ProcessOverlays(pPicture, pPicture->pts);
+
+ EINTERLACEMETHOD deintMethod = EINTERLACEMETHOD::VS_INTERLACEMETHOD_NONE;
+ deintMethod = m_processInfo.GetVideoSettings().m_InterlaceMethod;
+ if (!m_processInfo.Supports(deintMethod))
+ deintMethod = m_processInfo.GetDeinterlacingMethodDefault();
+
+ if (!m_renderManager.AddVideoPicture(*pPicture, m_bAbortOutput, deintMethod, (m_syncState == ESyncState::SYNC_STARTING)))
+ {
+ m_droppingStats.AddOutputDropGain(pPicture->pts, 1);
+ return OUTPUT_DROPPED;
+ }
+
+ return OUTPUT_NORMAL;
+}
+
+std::string CVideoPlayerVideo::GetPlayerInfo()
+{
+ std::ostringstream s;
+ s << "vq:" << std::setw(2) << std::min(99, m_processInfo.GetLevelVQ()) << "%";
+ s << ", Mb/s:" << std::fixed << std::setprecision(2) << (double)GetVideoBitrate() / (1024.0*1024.0);
+ s << ", fr:" << std::fixed << std::setprecision(3) << m_fFrameRate;
+ s << ", drop:" << m_iDroppedFrames;
+ s << ", skip:" << m_renderManager.GetSkippedFrames();
+
+ int pc = m_ptsTracker.GetPatternLength();
+ if (pc > 0)
+ s << ", pc:" << pc;
+ else
+ s << ", pc:none";
+
+ return s.str();
+}
+
+int CVideoPlayerVideo::GetVideoBitrate()
+{
+ return (int)m_videoStats.GetBitrate();
+}
+
+void CVideoPlayerVideo::ResetFrameRateCalc()
+{
+ m_fStableFrameRate = 0.0;
+ m_iFrameRateCount = 0;
+ m_iFrameRateLength = 1;
+ m_iFrameRateErr = 0;
+ m_bAllowDrop = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoFpsDetect == 0;
+}
+
+double CVideoPlayerVideo::GetCurrentPts()
+{
+ double renderPts;
+ int sleepTime;
+ int queued, discard;
+
+ // get render stats
+ m_renderManager.GetStats(sleepTime, renderPts, queued, discard);
+
+ if (renderPts == DVD_NOPTS_VALUE)
+ return DVD_NOPTS_VALUE;
+ else if (m_stalled)
+ return DVD_NOPTS_VALUE;
+ else if (m_speed == DVD_PLAYSPEED_NORMAL)
+ {
+ if (renderPts < 0)
+ renderPts = 0;
+ }
+ return renderPts;
+}
+
+#define MAXFRAMERATEDIFF 0.01
+#define MAXFRAMESERR 1000
+
+void CVideoPlayerVideo::CalcFrameRate()
+{
+ if (m_iFrameRateLength >= 128 || CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoFpsDetect == 0)
+ return; //don't calculate the fps
+
+ if (!m_ptsTracker.HasFullBuffer())
+ return; //we can only calculate the frameduration if m_pullupCorrection has a full buffer
+
+ //see if m_pullupCorrection was able to detect a pattern in the timestamps
+ //and is able to calculate the correct frame duration from it
+ double frameduration = m_ptsTracker.GetFrameDuration();
+ if (m_ptsTracker.VFRDetection())
+ frameduration = m_ptsTracker.GetMinFrameDuration();
+
+ if ((frameduration==DVD_NOPTS_VALUE) ||
+ ((CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoFpsDetect == 1) && ((m_ptsTracker.GetPatternLength() > 1) && !m_ptsTracker.VFRDetection())))
+ {
+ //reset the stored framerates if no good framerate was detected
+ m_fStableFrameRate = 0.0;
+ m_iFrameRateCount = 0;
+ m_iFrameRateErr++;
+
+ if (m_iFrameRateErr == MAXFRAMESERR && m_iFrameRateLength == 1)
+ {
+ CLog::Log(LOGDEBUG,
+ "{} counted {} frames without being able to calculate the framerate, giving up",
+ __FUNCTION__, m_iFrameRateErr);
+ m_bAllowDrop = true;
+ m_iFrameRateLength = 128;
+ }
+ return;
+ }
+
+ double framerate = DVD_TIME_BASE / frameduration;
+
+ //store the current calculated framerate if we don't have any yet
+ if (m_iFrameRateCount == 0)
+ {
+ m_fStableFrameRate = framerate;
+ m_iFrameRateCount++;
+ }
+ //check if the current detected framerate matches with the stored ones
+ else if (fabs(m_fStableFrameRate / m_iFrameRateCount - framerate) <= MAXFRAMERATEDIFF)
+ {
+ m_fStableFrameRate += framerate; //store the calculated framerate
+ m_iFrameRateCount++;
+
+ //if we've measured m_iFrameRateLength seconds of framerates,
+ if (m_iFrameRateCount >= MathUtils::round_int(framerate) * m_iFrameRateLength)
+ {
+ //store the calculated framerate if it differs too much from m_fFrameRate
+ if (fabs(m_fFrameRate - (m_fStableFrameRate / m_iFrameRateCount)) > MAXFRAMERATEDIFF || m_bFpsInvalid)
+ {
+ CLog::Log(LOGDEBUG, "{} framerate was:{:f} calculated:{:f}", __FUNCTION__, m_fFrameRate,
+ m_fStableFrameRate / m_iFrameRateCount);
+ m_fFrameRate = m_fStableFrameRate / m_iFrameRateCount;
+ m_bFpsInvalid = false;
+ m_processInfo.SetVideoFps(static_cast<float>(m_fFrameRate));
+ }
+
+ //reset the stored framerates
+ m_fStableFrameRate = 0.0;
+ m_iFrameRateCount = 0;
+ m_iFrameRateLength *= 2; //double the length we should measure framerates
+
+ //we're allowed to drop frames because we calculated a good framerate
+ m_bAllowDrop = true;
+ }
+ }
+ else //the calculated framerate didn't match, reset the stored ones
+ {
+ m_fStableFrameRate = 0.0;
+ m_iFrameRateCount = 0;
+ }
+}
+
+int CVideoPlayerVideo::CalcDropRequirement(double pts)
+{
+ int result = 0;
+ int lateframes;
+ double iDecoderPts, iRenderPts;
+ int iSkippedPicture = -1;
+ int iDroppedFrames = -1;
+ int iBufferLevel;
+ int queued, discard;
+
+ m_droppingStats.m_lastPts = pts;
+
+ // get decoder stats
+ if (!m_pVideoCodec->GetCodecStats(iDecoderPts, iDroppedFrames, iSkippedPicture))
+ iDecoderPts = pts;
+ if (iDecoderPts == DVD_NOPTS_VALUE)
+ iDecoderPts = pts;
+
+ // get render stats
+ m_renderManager.GetStats(lateframes, iRenderPts, queued, discard);
+ iBufferLevel = queued + discard;
+
+ if (iBufferLevel < 0)
+ result |= DROP_BUFFER_LEVEL;
+ else if (iBufferLevel < 2)
+ {
+ result |= DROP_BUFFER_LEVEL;
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CVideoPlayerVideo::CalcDropRequirement - hurry: {}",
+ iBufferLevel);
+ }
+
+ if (m_bAllowDrop)
+ {
+ if (iSkippedPicture > 0)
+ {
+ CDroppingStats::CGain gain;
+ gain.frames = iSkippedPicture;
+ gain.pts = iDecoderPts;
+ m_droppingStats.m_gain.push_back(gain);
+ m_droppingStats.m_totalGain += gain.frames;
+ result |= DROP_DROPPED;
+ CLog::Log(LOGDEBUG, LOGVIDEO,
+ "CVideoPlayerVideo::CalcDropRequirement - dropped pictures, lateframes: {}, "
+ "Bufferlevel: {}, dropped: {}",
+ lateframes, iBufferLevel, iSkippedPicture);
+ }
+ if (iDroppedFrames > 0)
+ {
+ CDroppingStats::CGain gain;
+ gain.frames = iDroppedFrames;
+ gain.pts = iDecoderPts;
+ m_droppingStats.m_gain.push_back(gain);
+ m_droppingStats.m_totalGain += iDroppedFrames;
+ result |= DROP_DROPPED;
+ CLog::Log(LOGDEBUG, LOGVIDEO,
+ "CVideoPlayerVideo::CalcDropRequirement - dropped in decoder, lateframes: {}, "
+ "Bufferlevel: {}, dropped: {}",
+ lateframes, iBufferLevel, iDroppedFrames);
+ }
+ }
+
+ // subtract gains
+ while (!m_droppingStats.m_gain.empty() &&
+ iRenderPts >= m_droppingStats.m_gain.front().pts)
+ {
+ m_droppingStats.m_totalGain -= m_droppingStats.m_gain.front().frames;
+ m_droppingStats.m_gain.pop_front();
+ }
+
+ // calculate lateness
+ int lateness = lateframes - m_droppingStats.m_totalGain;
+
+ if (lateness > 0 && m_speed)
+ {
+ result |= DROP_VERYLATE;
+ }
+ return result;
+}
+
+void CDroppingStats::Reset()
+{
+ m_gain.clear();
+ m_totalGain = 0;
+}
+
+void CDroppingStats::AddOutputDropGain(double pts, int frames)
+{
+ CDroppingStats::CGain gain;
+ gain.frames = frames;
+ gain.pts = pts;
+ m_gain.push_back(gain);
+ m_totalGain += frames;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoPlayerVideo.h b/xbmc/cores/VideoPlayer/VideoPlayerVideo.h
new file mode 100644
index 0000000..e5d7254
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoPlayerVideo.h
@@ -0,0 +1,144 @@
+/*
+ * 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 "DVDClock.h"
+#include "DVDCodecs/Video/DVDVideoCodec.h"
+#include "DVDMessageQueue.h"
+#include "DVDOverlayContainer.h"
+#include "DVDStreamInfo.h"
+#include "IVideoPlayer.h"
+#include "PTSTracker.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderManager.h"
+#include "threads/Thread.h"
+#include "utils/BitstreamStats.h"
+
+#include <atomic>
+
+#define DROP_DROPPED 1
+#define DROP_VERYLATE 2
+#define DROP_BUFFER_LEVEL 4
+
+class CDemuxStreamVideo;
+
+class CDroppingStats
+{
+public:
+ void Reset();
+ void AddOutputDropGain(double pts, int frames);
+ struct CGain
+ {
+ int frames;
+ double pts;
+ };
+ std::deque<CGain> m_gain;
+ int m_totalGain;
+ double m_lastPts;
+};
+
+class CVideoPlayerVideo : public CThread, public IDVDStreamPlayerVideo
+{
+public:
+ CVideoPlayerVideo(CDVDClock* pClock
+ ,CDVDOverlayContainer* pOverlayContainer
+ ,CDVDMessageQueue& parent
+ ,CRenderManager& renderManager,
+ CProcessInfo &processInfo);
+ ~CVideoPlayerVideo() override;
+
+ bool OpenStream(CDVDStreamInfo hint) override;
+ void CloseStream(bool bWaitForBuffers) override;
+ void Flush(bool sync) override;
+ bool AcceptsData() const override;
+ bool HasData() const override;
+ bool IsInited() const override;
+ void SendMessage(std::shared_ptr<CDVDMsg> pMsg, int priority = 0) override;
+ void FlushMessages() override;
+
+ void EnableSubtitle(bool bEnable) override { m_bRenderSubs = bEnable; }
+ bool IsSubtitleEnabled() override { return m_bRenderSubs; }
+ double GetSubtitleDelay() override { return m_iSubtitleDelay; }
+ void SetSubtitleDelay(double delay) override { m_iSubtitleDelay = delay; }
+ bool IsStalled() const override { return m_stalled; }
+ bool IsRewindStalled() const override { return m_rewindStalled; }
+ double GetCurrentPts() override;
+ double GetOutputDelay() override; /* returns the expected delay, from that a packet is put in queue */
+ std::string GetPlayerInfo() override;
+ int GetVideoBitrate() override;
+ void SetSpeed(int iSpeed) override;
+
+ // classes
+ CDVDOverlayContainer* m_pOverlayContainer;
+ CDVDClock* m_pClock;
+
+protected:
+
+ enum EOutputState
+ {
+ OUTPUT_NORMAL,
+ OUTPUT_ABORT,
+ OUTPUT_DROPPED,
+ OUTPUT_AGAIN
+ };
+
+ void OnExit() override;
+ void Process() override;
+
+ bool ProcessDecoderOutput(double &frametime, double &pts);
+ void SendMessageBack(const std::shared_ptr<CDVDMsg>& pMsg, int priority = 0);
+ MsgQueueReturnCode GetMessage(std::shared_ptr<CDVDMsg>& pMsg,
+ unsigned int iTimeoutInMilliSeconds,
+ int& priority);
+
+ EOutputState OutputPicture(const VideoPicture* src);
+ void ProcessOverlays(const VideoPicture* pSource, double pts);
+ void OpenStream(CDVDStreamInfo& hint, std::unique_ptr<CDVDVideoCodec> codec);
+
+ void ResetFrameRateCalc();
+ void CalcFrameRate();
+ int CalcDropRequirement(double pts);
+
+ double m_iSubtitleDelay;
+
+ int m_iLateFrames;
+ int m_iDroppedFrames;
+ int m_iDroppedRequest;
+
+ double m_fFrameRate; //framerate of the video currently playing
+ double m_fStableFrameRate; //place to store calculated framerates
+ int m_iFrameRateCount; //how many calculated framerates we stored in m_fStableFrameRate
+ bool m_bAllowDrop; //we can't drop frames until we've calculated the framerate
+ int m_iFrameRateErr; //how many frames we couldn't calculate the framerate, we give up after a while
+ int m_iFrameRateLength; //how many seconds we should measure the framerate
+ //this is increased exponentially from CVideoPlayerVideo::CalcFrameRate()
+
+ bool m_bFpsInvalid; // needed to ignore fps (e.g. dvd stills)
+ bool m_bRenderSubs;
+ float m_fForcedAspectRatio;
+ int m_speed;
+ std::atomic_bool m_stalled = false;
+ std::atomic_bool m_rewindStalled;
+ bool m_paused;
+ IDVDStreamPlayer::ESyncState m_syncState;
+ std::atomic_bool m_bAbortOutput;
+
+ BitstreamStats m_videoStats;
+
+ CDVDMessageQueue m_messageQueue;
+ CDVDMessageQueue& m_messageParent;
+ CDVDStreamInfo m_hints;
+ std::unique_ptr<CDVDVideoCodec> m_pVideoCodec;
+ CPtsTracker m_ptsTracker;
+ std::list<DVDMessageListItem> m_packets;
+ CDroppingStats m_droppingStats;
+ CRenderManager& m_renderManager;
+ VideoPicture m_picture;
+
+ EOutputState m_outputSate;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoReferenceClock.cpp b/xbmc/cores/VideoPlayer/VideoReferenceClock.cpp
new file mode 100644
index 0000000..7508978
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoReferenceClock.cpp
@@ -0,0 +1,280 @@
+/*
+ * 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 "VideoReferenceClock.h"
+
+#include "ServiceBroker.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/MathUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/VideoSync.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+
+CVideoReferenceClock::CVideoReferenceClock() : CThread("RefClock")
+{
+ m_SystemFrequency = CurrentHostFrequency();
+ m_ClockSpeed = 1.0;
+ m_TotalMissedVblanks = 0;
+ m_UseVblank = false;
+
+ m_CurrTime = 0;
+ m_LastIntTime = 0;
+ m_CurrTimeFract = 0.0;
+ m_RefreshRate = 0.0;
+ m_MissedVblanks = 0;
+ m_VblankTime = 0;
+ m_vsyncStopEvent.Reset();
+
+ Start();
+}
+
+CVideoReferenceClock::~CVideoReferenceClock()
+{
+ m_bStop = true;
+ m_vsyncStopEvent.Set();
+ StopThread();
+}
+
+void CVideoReferenceClock::Start()
+{
+ if(CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_USEDISPLAYASCLOCK) && !IsRunning())
+ Create();
+}
+
+void CVideoReferenceClock::CBUpdateClock(int NrVBlanks, uint64_t time, void *clock)
+{
+ {
+ CVideoReferenceClock *refClock = static_cast<CVideoReferenceClock*>(clock);
+ std::unique_lock<CCriticalSection> lock(refClock->m_CritSection);
+ refClock->m_VblankTime = time;
+ refClock->UpdateClock(NrVBlanks, true);
+ }
+}
+
+void CVideoReferenceClock::Process()
+{
+ bool SetupSuccess = false;
+ int64_t Now;
+
+ while(!m_bStop)
+ {
+ m_pVideoSync = CServiceBroker::GetWinSystem()->GetVideoSync(this);
+
+ if (m_pVideoSync)
+ {
+ SetupSuccess = m_pVideoSync->Setup(CBUpdateClock);
+ UpdateRefreshrate();
+ }
+
+ std::unique_lock<CCriticalSection> SingleLock(m_CritSection);
+ Now = CurrentHostCounter();
+ m_CurrTime = Now;
+ m_LastIntTime = m_CurrTime;
+ m_CurrTimeFract = 0.0;
+ m_ClockSpeed = 1.0;
+ m_TotalMissedVblanks = 0;
+ m_MissedVblanks = 0;
+
+ if (SetupSuccess)
+ {
+ m_UseVblank = true; //tell other threads we're using vblank as clock
+ m_VblankTime = Now; //initialize the timestamp of the last vblank
+ SingleLock.unlock();
+
+ // we might got signalled while we did not wait
+ if (!m_vsyncStopEvent.Signaled())
+ {
+ //run the clock
+ m_pVideoSync->Run(m_vsyncStopEvent);
+ m_vsyncStopEvent.Reset();
+ }
+ }
+ else
+ {
+ SingleLock.unlock();
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: Setup failed, falling back to CurrentHostCounter()");
+ }
+
+ SingleLock.lock();
+ m_UseVblank = false; //we're back to using the systemclock
+ SingleLock.unlock();
+
+ //clean up the vblank clock
+ if (m_pVideoSync)
+ {
+ m_pVideoSync->Cleanup();
+ m_pVideoSync.reset();
+ }
+
+ if (!SetupSuccess)
+ break;
+ }
+}
+
+//this is called from the vblank run function and from CVideoReferenceClock::Wait in case of a late update
+void CVideoReferenceClock::UpdateClock(int NrVBlanks, bool CheckMissed)
+{
+ if (CheckMissed) //set to true from the vblank run function, set to false from Wait and GetTime
+ {
+ if (NrVBlanks < m_MissedVblanks) //if this is true the vblank detection in the run function is wrong
+ CLog::Log(
+ LOGDEBUG,
+ "CVideoReferenceClock: detected {} vblanks, missed {}, refreshrate might have changed",
+ NrVBlanks, m_MissedVblanks);
+
+ NrVBlanks -= m_MissedVblanks; //subtract the vblanks we missed
+ m_MissedVblanks = 0;
+ }
+ else
+ {
+ m_MissedVblanks += NrVBlanks; //tell the vblank clock how many vblanks it missed
+ m_TotalMissedVblanks += NrVBlanks; //for the codec information screen
+ m_VblankTime += m_SystemFrequency * static_cast<int64_t>(NrVBlanks) / MathUtils::round_int(m_RefreshRate); //set the vblank time forward
+ }
+
+ if (NrVBlanks > 0) //update the clock with the adjusted frequency if we have any vblanks
+ {
+ double increment = UpdateInterval() * NrVBlanks;
+ double integer = floor(increment);
+ m_CurrTime += static_cast<int64_t>(integer + 0.5); //make sure it gets correctly converted to int
+
+ //accumulate what we lost due to rounding in m_CurrTimeFract, then add the integer part of that to m_CurrTime
+ m_CurrTimeFract += increment - integer;
+ integer = floor(m_CurrTimeFract);
+ m_CurrTime += static_cast<int64_t>(integer + 0.5);
+ m_CurrTimeFract -= integer;
+ }
+}
+
+double CVideoReferenceClock::UpdateInterval() const
+{
+ return m_ClockSpeed / m_RefreshRate * static_cast<double>(m_SystemFrequency);
+}
+
+//called from dvdclock to get the time
+int64_t CVideoReferenceClock::GetTime(bool interpolated /* = true*/)
+{
+ std::unique_lock<CCriticalSection> SingleLock(m_CritSection);
+
+ //when using vblank, get the time from that, otherwise use the systemclock
+ if (m_UseVblank)
+ {
+ int64_t NextVblank;
+ int64_t Now;
+
+ Now = CurrentHostCounter(); //get current system time
+ NextVblank = TimeOfNextVblank(); //get time when the next vblank should happen
+
+ while(Now >= NextVblank) //keep looping until the next vblank is in the future
+ {
+ UpdateClock(1, false); //update clock when next vblank should have happened already
+ NextVblank = TimeOfNextVblank(); //get time when the next vblank should happen
+ }
+
+ if (interpolated)
+ {
+ //interpolate from the last time the clock was updated
+ double elapsed = static_cast<double>(Now - m_VblankTime) * m_ClockSpeed;
+ //don't interpolate more than 2 vblank periods
+ elapsed = std::min(elapsed, UpdateInterval() * 2.0);
+
+ //make sure the clock doesn't go backwards
+ int64_t intTime = m_CurrTime + static_cast<int64_t>(elapsed);
+ if (intTime > m_LastIntTime)
+ m_LastIntTime = intTime;
+
+ return m_LastIntTime;
+ }
+ else
+ {
+ return m_CurrTime;
+ }
+ }
+ else
+ {
+ return CurrentHostCounter();
+ }
+}
+
+void CVideoReferenceClock::SetSpeed(double Speed)
+{
+ std::unique_lock<CCriticalSection> SingleLock(m_CritSection);
+ //VideoPlayer can change the speed to fit the rereshrate
+ if (m_UseVblank)
+ {
+ if (Speed != m_ClockSpeed)
+ {
+ m_ClockSpeed = Speed;
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: Clock speed {:0.2f} %", m_ClockSpeed * 100.0);
+ }
+ }
+}
+
+double CVideoReferenceClock::GetSpeed()
+{
+ std::unique_lock<CCriticalSection> SingleLock(m_CritSection);
+
+ //VideoPlayer needs to know the speed for the resampler
+ if (m_UseVblank)
+ return m_ClockSpeed;
+ else
+ return 1.0;
+}
+
+void CVideoReferenceClock::UpdateRefreshrate()
+{
+ std::unique_lock<CCriticalSection> SingleLock(m_CritSection);
+ m_RefreshRate = static_cast<double>(m_pVideoSync->GetFps());
+ m_ClockSpeed = 1.0;
+
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: Detected refreshrate: {:.3f} hertz", m_RefreshRate);
+}
+
+//VideoPlayer needs to know the refreshrate for matching the fps of the video playing to it
+double CVideoReferenceClock::GetRefreshRate(double* interval /*= NULL*/)
+{
+ std::unique_lock<CCriticalSection> SingleLock(m_CritSection);
+
+ if (m_UseVblank)
+ {
+ if (interval)
+ *interval = m_ClockSpeed / m_RefreshRate;
+
+ return m_RefreshRate;
+ }
+ else
+ return -1;
+}
+
+#define MAXVBLANKDELAY 13LL
+//guess when the next vblank should happen,
+//based on the refreshrate and when the previous one happened
+//increase that by 30% to allow for errors
+int64_t CVideoReferenceClock::TimeOfNextVblank() const
+{
+ return m_VblankTime + (m_SystemFrequency / MathUtils::round_int(m_RefreshRate) * MAXVBLANKDELAY / 10LL);
+}
+
+//for the codec information screen
+bool CVideoReferenceClock::GetClockInfo(int& MissedVblanks, double& ClockSpeed, double& RefreshRate) const
+{
+ std::unique_lock<CCriticalSection> SingleLock(m_CritSection);
+
+ if (m_UseVblank)
+ {
+ MissedVblanks = m_TotalMissedVblanks;
+ ClockSpeed = m_ClockSpeed;
+ RefreshRate = m_RefreshRate;
+ return true;
+ }
+ return false;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoReferenceClock.h b/xbmc/cores/VideoPlayer/VideoReferenceClock.h
new file mode 100644
index 0000000..4419b6d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoReferenceClock.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 "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+
+#include <memory>
+
+class CVideoSync;
+
+class CVideoReferenceClock : CThread
+{
+ public:
+ CVideoReferenceClock();
+ ~CVideoReferenceClock() override;
+
+ int64_t GetTime(bool interpolated = true);
+ void SetSpeed(double Speed);
+ double GetSpeed();
+ double GetRefreshRate(double* interval = nullptr);
+ bool GetClockInfo(int& MissedVblanks, double& ClockSpeed, double& RefreshRate) const;
+
+ private:
+ void Process() override;
+ void Start();
+ void UpdateRefreshrate();
+ void UpdateClock(int NrVBlanks, bool CheckMissed);
+ double UpdateInterval() const;
+ int64_t TimeOfNextVblank() const;
+ static void CBUpdateClock(int NrVBlanks, uint64_t time, void *clock);
+
+ int64_t m_CurrTime; //the current time of the clock when using vblank as clock source
+ int64_t m_LastIntTime; //last interpolated clock value, to make sure the clock doesn't go backwards
+ double m_CurrTimeFract; //fractional part that is lost due to rounding when updating the clock
+ double m_ClockSpeed; //the frequency of the clock set by VideoPlayer
+ int64_t m_SystemFrequency; //frequency of the systemclock
+
+ bool m_UseVblank; //set to true when vblank is used as clock source
+ double m_RefreshRate; //current refreshrate
+ int m_MissedVblanks; //number of clock updates missed by the vblank clock
+ int m_TotalMissedVblanks;//total number of clock updates missed, used by codec information screen
+ int64_t m_VblankTime; //last time the clock was updated when using vblank as clock
+
+ CEvent m_vsyncStopEvent;
+
+ mutable CCriticalSection m_CritSection;
+
+ std::unique_ptr<CVideoSync> m_pVideoSync;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/BaseRenderer.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/BaseRenderer.cpp
new file mode 100644
index 0000000..b8649da
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/BaseRenderer.cpp
@@ -0,0 +1,522 @@
+/*
+ * 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 "BaseRenderer.h"
+
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFlags.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "utils/MathUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <algorithm>
+#include <cstdlib> // std::abs(int) prototype
+
+
+CBaseRenderer::CBaseRenderer()
+{
+ for (int i=0; i < 4; i++)
+ {
+ m_rotatedDestCoords[i].x = 0;
+ m_rotatedDestCoords[i].y = 0;
+ m_savedRotatedDestCoords[i].x = 0;
+ m_savedRotatedDestCoords[i].y = 0;
+ }
+}
+
+CBaseRenderer::~CBaseRenderer() = default;
+
+float CBaseRenderer::GetAspectRatio() const
+{
+ float width = (float)m_sourceWidth;
+ float height = (float)m_sourceHeight;
+ return m_sourceFrameRatio * width / height * m_sourceHeight / m_sourceWidth;
+}
+
+void CBaseRenderer::GetVideoRect(CRect& source, CRect& dest, CRect& view) const
+{
+ source = m_sourceRect;
+ dest = m_destRect;
+ view = m_viewRect;
+}
+
+inline void CBaseRenderer::ReorderDrawPoints()
+{
+ // 0 - top left, 1 - top right, 2 - bottom right, 3 - bottom left
+ float origMat[4][2] = {{m_destRect.x1, m_destRect.y1},
+ {m_destRect.x2, m_destRect.y1},
+ {m_destRect.x2, m_destRect.y2},
+ {m_destRect.x1, m_destRect.y2}};
+
+ int pointOffset = m_renderOrientation / 90;
+
+ // if renderer doesn't support rotation
+ // treat orientation as 0 degree so that
+ // ffmpeg might handle it.
+ if (!Supports(RENDERFEATURE_ROTATION))
+ {
+ pointOffset = 0;
+ }
+
+ for (int destIdx=0, srcIdx=pointOffset; destIdx < 4; destIdx++)
+ {
+ m_rotatedDestCoords[destIdx].x = origMat[srcIdx][0];
+ m_rotatedDestCoords[destIdx].y = origMat[srcIdx][1];
+
+ srcIdx++;
+ srcIdx = srcIdx % 4;
+ }
+}
+
+void CBaseRenderer::saveRotatedCoords()
+{
+ for (int i = 0; i < 4; i++)
+ m_savedRotatedDestCoords[i] = m_rotatedDestCoords[i];
+}
+
+void CBaseRenderer::syncDestRectToRotatedPoints()
+{
+ m_rotatedDestCoords[0].x = m_destRect.x1;
+ m_rotatedDestCoords[0].y = m_destRect.y1;
+ m_rotatedDestCoords[1].x = m_destRect.x2;
+ m_rotatedDestCoords[1].y = m_destRect.y1;
+ m_rotatedDestCoords[2].x = m_destRect.x2;
+ m_rotatedDestCoords[2].y = m_destRect.y2;
+ m_rotatedDestCoords[3].x = m_destRect.x1;
+ m_rotatedDestCoords[3].y = m_destRect.y2;
+}
+
+void CBaseRenderer::restoreRotatedCoords()
+{
+ for (int i = 0; i < 4; i++)
+ m_rotatedDestCoords[i] = m_savedRotatedDestCoords[i];
+}
+
+void CBaseRenderer::CalcDestRect(float offsetX,
+ float offsetY,
+ float width,
+ float height,
+ float inputFrameRatio,
+ float zoomAmount,
+ float verticalShift,
+ CRect& destRect)
+{
+ // if view window is empty, set empty destination
+ if (height == 0 || width == 0)
+ {
+ destRect.SetRect(0.0f, 0.0f, 0.0f, 0.0f);
+ return;
+ }
+
+ // scale up image as much as possible
+ // and keep the aspect ratio (introduces with black bars)
+ // calculate the correct output frame ratio (using the users pixel ratio setting
+ // and the output pixel ratio setting)
+
+ float outputFrameRatio = inputFrameRatio / CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo().fPixelRatio;
+
+ // allow a certain error to maximize size of render area
+ float fCorrection = width / height / outputFrameRatio - 1.0f;
+ float fAllowed = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_ERRORINASPECT) * 0.01f;
+ if (fCorrection > fAllowed)
+ fCorrection = fAllowed;
+ if (fCorrection < -fAllowed)
+ fCorrection = - fAllowed;
+
+ outputFrameRatio *= 1.0f + fCorrection;
+
+ bool isRotated = false;
+ if (m_renderOrientation == 90 ||
+ m_renderOrientation == 270)
+ isRotated = true;
+
+ float newWidth;
+ float newHeight;
+
+ if (!isRotated)
+ {
+ // maximize the movie width
+ newWidth = width;
+ newHeight = newWidth / outputFrameRatio;
+ if (newHeight > height)
+ {
+ newHeight = height;
+ newWidth = newHeight * outputFrameRatio;
+ }
+ }
+ else
+ {
+ // maximize the movie height
+ newHeight = std::min(width, height);
+ newWidth = newHeight / outputFrameRatio;
+ if (newWidth > width)
+ {
+ newWidth = std::min(width, height);
+ newHeight = newWidth * outputFrameRatio;
+ }
+ }
+
+ // Scale the movie up by set zoom amount
+ newWidth *= zoomAmount;
+ newHeight *= zoomAmount;
+
+ // if we are less than one pixel off use the complete screen instead
+ if (std::abs(newWidth - width) < 1.0f)
+ newWidth = width;
+ if (std::abs(newHeight - height) < 1.0f)
+ newHeight = height;
+
+ // Centre the movie
+ float posY = (height - newHeight) / 2;
+ float posX = (width - newWidth) / 2;
+
+ // vertical shift range -1 to 1 shifts within the top and bottom black bars
+ // if there are no top and bottom black bars, this range does nothing
+ float blackBarSize = std::max((height - newHeight) / 2.0f, 0.0f);
+ posY += blackBarSize * std::max(std::min(verticalShift, 1.0f), -1.0f);
+
+ // vertical shift ranges -2 to -1 and 1 to 2 will shift the image out of the screen
+ // if vertical shift is -2 it will be completely shifted out the top,
+ // if it's 2 it will be completely shifted out the bottom
+ float shiftRange = std::min(newHeight, newHeight - (newHeight - height) / 2.0f);
+ if (verticalShift > 1.0f)
+ posY += shiftRange * (verticalShift - 1.0f);
+ else if (verticalShift < -1.0f)
+ posY += shiftRange * (verticalShift + 1.0f);
+
+ destRect.x1 = static_cast<float>(MathUtils::round_int(static_cast<double>(posX + offsetX)));
+ destRect.x2 = destRect.x1 + MathUtils::round_int(static_cast<double>(newWidth));
+ destRect.y1 = static_cast<float>(MathUtils::round_int(static_cast<double>(posY + offsetY)));
+ destRect.y2 = destRect.y1 + MathUtils::round_int(static_cast<double>(newHeight));
+}
+
+void CBaseRenderer::CalcNormalRenderRect(float offsetX,
+ float offsetY,
+ float width,
+ float height,
+ float inputFrameRatio,
+ float zoomAmount,
+ float verticalShift)
+{
+ CalcDestRect(offsetX, offsetY, width, height, inputFrameRatio, zoomAmount, verticalShift,
+ m_destRect);
+
+ // bail out if view window is empty
+ if (height == 0 || width == 0)
+ return;
+
+ // clip as needed
+ if (!(CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenVideo() || CServiceBroker::GetWinSystem()->GetGfxContext().IsCalibrating()))
+ {
+ CRect original(m_destRect);
+ m_destRect.Intersect(CRect(offsetX, offsetY, offsetX + width, offsetY + height));
+ if (m_destRect != original)
+ {
+ float scaleX = m_sourceRect.Width() / original.Width();
+ float scaleY = m_sourceRect.Height() / original.Height();
+ m_sourceRect.x1 += (m_destRect.x1 - original.x1) * scaleX;
+ m_sourceRect.y1 += (m_destRect.y1 - original.y1) * scaleY;
+ m_sourceRect.x2 += (m_destRect.x2 - original.x2) * scaleX;
+ m_sourceRect.y2 += (m_destRect.y2 - original.y2) * scaleY;
+ }
+ }
+
+ ReorderDrawPoints();
+}
+
+//***************************************************************************************
+// CalculateFrameAspectRatio()
+//
+// Considers the source frame size and output frame size (as suggested by mplayer)
+// to determine if the pixels in the source are not square. It calculates the aspect
+// ratio of the output frame. We consider the cases of VCD, SVCD and DVD separately,
+// as these are intended to be viewed on a non-square pixel TV set, so the pixels are
+// defined to be the same ratio as the intended display pixels.
+// These formats are determined by frame size.
+//***************************************************************************************
+void CBaseRenderer::CalculateFrameAspectRatio(unsigned int desired_width, unsigned int desired_height)
+{
+ m_sourceFrameRatio = (float)desired_width / desired_height;
+
+ // Check whether mplayer has decided that the size of the video file should be changed
+ // This indicates either a scaling has taken place (which we didn't ask for) or it has
+ // found an aspect ratio parameter from the file, and is changing the frame size based
+ // on that.
+ if (m_sourceWidth == desired_width && m_sourceHeight == desired_height)
+ return ;
+
+ // mplayer is scaling in one or both directions. We must alter our Source Pixel Ratio
+ float imageFrameRatio = (float)m_sourceWidth / m_sourceHeight;
+
+ // OK, most sources will be correct now, except those that are intended
+ // to be displayed on non-square pixel based output devices (ie PAL or NTSC TVs)
+ // This includes VCD, SVCD, and DVD (and possibly others that we are not doing yet)
+ // For this, we can base the pixel ratio on the pixel ratios of PAL and NTSC,
+ // though we will need to adjust for anamorphic sources (ie those whose
+ // output frame ratio is not 4:3) and for SVCDs which have 2/3rds the
+ // horizontal resolution of the default NTSC or PAL frame sizes
+
+ // The following are the defined standard ratios for PAL and NTSC pixels
+ // NOTE: These aren't technically (in terms of BT601) correct - the commented values are,
+ // but it seems that many DVDs nowadays are mastered incorrectly, so two wrongs
+ // may indeed make a right. The "wrong" values here ensure the output frame is
+ // 4x3 (or 16x9)
+ const float PALPixelRatio = 16.0f / 15.0f; // 128.0f / 117.0f;
+ const float NTSCPixelRatio = 8.0f / 9.0f; // 4320.0f / 4739.0f;
+
+ // Calculate the correction needed for anamorphic sources
+ float Non4by3Correction = m_sourceFrameRatio / (4.0f / 3.0f);
+
+ // Finally, check for a VCD, SVCD or DVD frame size as these need special aspect ratios
+ if (m_sourceWidth == 352)
+ { // VCD?
+ if (m_sourceHeight == 240) // NTSC
+ m_sourceFrameRatio = imageFrameRatio * NTSCPixelRatio;
+ if (m_sourceHeight == 288) // PAL
+ m_sourceFrameRatio = imageFrameRatio * PALPixelRatio;
+ }
+ if (m_sourceWidth == 480)
+ { // SVCD?
+ if (m_sourceHeight == 480) // NTSC
+ m_sourceFrameRatio = imageFrameRatio * 3.0f / 2.0f * NTSCPixelRatio * Non4by3Correction;
+ if (m_sourceHeight == 576) // PAL
+ m_sourceFrameRatio = imageFrameRatio * 3.0f / 2.0f * PALPixelRatio * Non4by3Correction;
+ }
+ if (m_sourceWidth == 720)
+ { // DVD?
+ if (m_sourceHeight == 480) // NTSC
+ m_sourceFrameRatio = imageFrameRatio * NTSCPixelRatio * Non4by3Correction;
+ if (m_sourceHeight == 576) // PAL
+ m_sourceFrameRatio = imageFrameRatio * PALPixelRatio * Non4by3Correction;
+ }
+}
+
+void CBaseRenderer::ManageRenderArea()
+{
+ m_viewRect = CServiceBroker::GetWinSystem()->GetGfxContext().GetViewWindow();
+
+ m_sourceRect.x1 = 0.0f;
+ m_sourceRect.y1 = 0.0f;
+ m_sourceRect.x2 = (float)m_sourceWidth;
+ m_sourceRect.y2 = (float)m_sourceHeight;
+
+ unsigned int stereo_mode = CONF_FLAGS_STEREO_MODE_MASK(m_iFlags);
+ int stereo_view = CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoView();
+
+ if(CONF_FLAGS_STEREO_CADENCE(m_iFlags) == CONF_FLAGS_STEREO_CADANCE_RIGHT_LEFT)
+ {
+ if (stereo_view == RENDER_STEREO_VIEW_LEFT) stereo_view = RENDER_STEREO_VIEW_RIGHT;
+ else if(stereo_view == RENDER_STEREO_VIEW_RIGHT) stereo_view = RENDER_STEREO_VIEW_LEFT;
+ }
+
+ switch(stereo_mode)
+ {
+ case CONF_FLAGS_STEREO_MODE_TAB:
+ if (stereo_view == RENDER_STEREO_VIEW_LEFT)
+ m_sourceRect.y2 *= 0.5f;
+ else if(stereo_view == RENDER_STEREO_VIEW_RIGHT)
+ m_sourceRect.y1 += m_sourceRect.y2*0.5f;
+ break;
+
+ case CONF_FLAGS_STEREO_MODE_SBS:
+ if (stereo_view == RENDER_STEREO_VIEW_LEFT)
+ m_sourceRect.x2 *= 0.5f;
+ else if(stereo_view == RENDER_STEREO_VIEW_RIGHT)
+ m_sourceRect.x1 += m_sourceRect.x2*0.5f;
+ break;
+
+ default:
+ break;
+ }
+
+ CalcNormalRenderRect(m_viewRect.x1, m_viewRect.y1, m_viewRect.Width(), m_viewRect.Height(),
+ GetAspectRatio() * CDisplaySettings::GetInstance().GetPixelRatio(),
+ CDisplaySettings::GetInstance().GetZoomAmount(),
+ CDisplaySettings::GetInstance().GetVerticalShift());
+}
+
+EShaderFormat CBaseRenderer::GetShaderFormat()
+{
+ EShaderFormat ret = SHADER_NONE;
+
+ if (m_format == AV_PIX_FMT_YUV420P)
+ ret = SHADER_YV12;
+ else if (m_format == AV_PIX_FMT_YUV420P9)
+ ret = SHADER_YV12_9;
+ else if (m_format == AV_PIX_FMT_YUV420P10)
+ ret = SHADER_YV12_10;
+ else if (m_format == AV_PIX_FMT_YUV420P12)
+ ret = SHADER_YV12_12;
+ else if (m_format == AV_PIX_FMT_YUV420P14)
+ ret = SHADER_YV12_14;
+ else if (m_format == AV_PIX_FMT_YUV420P16)
+ ret = SHADER_YV12_16;
+ else if (m_format == AV_PIX_FMT_NV12)
+ ret = SHADER_NV12;
+ else if (m_format == AV_PIX_FMT_YUYV422)
+ ret = SHADER_YUY2;
+ else if (m_format == AV_PIX_FMT_UYVY422)
+ ret = SHADER_UYVY;
+ else
+ CLog::Log(LOGERROR, "CBaseRenderer::GetShaderFormat - unsupported format {}", m_format);
+
+ return ret;
+}
+
+void CBaseRenderer::SetViewMode(int viewMode)
+{
+ if (viewMode < ViewModeNormal || viewMode > ViewModeZoom110Width)
+ viewMode = ViewModeNormal;
+
+ m_videoSettings.m_ViewMode = viewMode;
+
+ // get our calibrated full screen resolution
+ RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ float screenWidth = (float)(info.Overscan.right - info.Overscan.left);
+ float screenHeight = (float)(info.Overscan.bottom - info.Overscan.top);
+
+ // and the source frame ratio
+ float sourceFrameRatio = GetAspectRatio();
+
+ bool is43 = (sourceFrameRatio < 8.f/(3.f*sqrt(3.f)) &&
+ m_videoSettings.m_ViewMode == ViewModeNormal);
+
+ // Splitres scaling factor
+ float xscale = (float)info.iScreenWidth / (float)info.iWidth;
+ float yscale = (float)info.iScreenHeight / (float)info.iHeight;
+
+ screenWidth *= xscale;
+ screenHeight *= yscale;
+
+ CDisplaySettings::GetInstance().SetVerticalShift(0.0f);
+ CDisplaySettings::GetInstance().SetNonLinearStretched(false);
+
+ if (m_videoSettings.m_ViewMode == ViewModeZoom ||
+ (is43 && CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_STRETCH43) == ViewModeZoom))
+ { // zoom image so no black bars
+ CDisplaySettings::GetInstance().SetPixelRatio(1.0);
+ // calculate the desired output ratio
+ float outputFrameRatio = sourceFrameRatio * CDisplaySettings::GetInstance().GetPixelRatio() / info.fPixelRatio;
+ // now calculate the correct zoom amount. First zoom to full height.
+ float newHeight = screenHeight;
+ float newWidth = newHeight * outputFrameRatio;
+ CDisplaySettings::GetInstance().SetZoomAmount(newWidth / screenWidth);
+ if (newWidth < screenWidth)
+ { // zoom to full width
+ newWidth = screenWidth;
+ newHeight = newWidth / outputFrameRatio;
+ CDisplaySettings::GetInstance().SetZoomAmount(newHeight / screenHeight);
+ }
+ }
+ else if (m_videoSettings.m_ViewMode == ViewModeStretch4x3)
+ { // stretch image to 4:3 ratio
+ CDisplaySettings::GetInstance().SetZoomAmount(1.0);
+ // now we need to set CDisplaySettings::GetInstance().GetPixelRatio() so that
+ // fOutputFrameRatio = 4:3.
+ CDisplaySettings::GetInstance().SetPixelRatio((4.0f / 3.0f) / sourceFrameRatio);
+ }
+ else if (m_videoSettings.m_ViewMode == ViewModeWideZoom ||
+ (is43 && CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_STRETCH43) == ViewModeWideZoom))
+ { // super zoom
+ float stretchAmount = (screenWidth / screenHeight) * info.fPixelRatio / sourceFrameRatio;
+ CDisplaySettings::GetInstance().SetPixelRatio(pow(stretchAmount, float(2.0/3.0)));
+ CDisplaySettings::GetInstance().SetZoomAmount(
+ pow(stretchAmount, float((stretchAmount < 1.0f) ? -1.0 / 3.0 : 1.0 / 3.0)));
+ CDisplaySettings::GetInstance().SetNonLinearStretched(true);
+ }
+ else if (m_videoSettings.m_ViewMode == ViewModeStretch16x9 ||
+ m_videoSettings.m_ViewMode == ViewModeStretch16x9Nonlin ||
+ (is43 && (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_STRETCH43) == ViewModeStretch16x9 ||
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_STRETCH43) == ViewModeStretch16x9Nonlin)))
+ { // stretch image to 16:9 ratio
+ CDisplaySettings::GetInstance().SetZoomAmount(1.0);
+ // stretch to the limits of the 16:9 screen.
+ // incorrect behaviour, but it's what the users want, so...
+ CDisplaySettings::GetInstance().SetPixelRatio((screenWidth / screenHeight) * info.fPixelRatio / sourceFrameRatio);
+ bool nonlin = (is43 && CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_STRETCH43) == ViewModeStretch16x9Nonlin) ||
+ m_videoSettings.m_ViewMode == ViewModeStretch16x9Nonlin;
+ CDisplaySettings::GetInstance().SetNonLinearStretched(nonlin);
+ }
+ else if (m_videoSettings.m_ViewMode == ViewModeOriginal)
+ { // zoom image so that the height is the original size
+ CDisplaySettings::GetInstance().SetPixelRatio(1.0);
+ // get the size of the media file
+ // calculate the desired output ratio
+ float outputFrameRatio = sourceFrameRatio * CDisplaySettings::GetInstance().GetPixelRatio() / info.fPixelRatio;
+ // now calculate the correct zoom amount. First zoom to full width.
+ float newHeight = screenWidth / outputFrameRatio;
+ if (newHeight > screenHeight)
+ { // zoom to full height
+ newHeight = screenHeight;
+ }
+ // now work out the zoom amount so that no zoom is done
+ CDisplaySettings::GetInstance().SetZoomAmount(m_sourceHeight / newHeight);
+ }
+ else if (m_videoSettings.m_ViewMode == ViewModeCustom)
+ {
+ CDisplaySettings::GetInstance().SetZoomAmount(m_videoSettings.m_CustomZoomAmount);
+ CDisplaySettings::GetInstance().SetPixelRatio(m_videoSettings.m_CustomPixelRatio);
+ CDisplaySettings::GetInstance().SetNonLinearStretched(m_videoSettings.m_CustomNonLinStretch);
+ CDisplaySettings::GetInstance().SetVerticalShift(m_videoSettings.m_CustomVerticalShift);
+ }
+ else if (m_videoSettings.m_ViewMode == ViewModeZoom120Width)
+ {
+ float fitHeightZoom = sourceFrameRatio * screenHeight / (info.fPixelRatio * screenWidth);
+ CDisplaySettings::GetInstance().SetPixelRatio(1.0f);
+ CDisplaySettings::GetInstance().SetZoomAmount(fitHeightZoom < 1.0f ? 1.0f : (fitHeightZoom > 1.2f ? 1.2f : fitHeightZoom));
+ }
+ else if (m_videoSettings.m_ViewMode == ViewModeZoom110Width)
+ {
+ float fitHeightZoom = sourceFrameRatio * screenHeight / (info.fPixelRatio * screenWidth);
+ CDisplaySettings::GetInstance().SetPixelRatio(1.0f);
+ CDisplaySettings::GetInstance().SetZoomAmount(fitHeightZoom < 1.0f ? 1.0f : (fitHeightZoom > 1.1f ? 1.1f : fitHeightZoom));
+ }
+ else // if (CMediaSettings::GetInstance().GetCurrentVideoSettings().m_ViewMode == ViewModeNormal)
+ {
+ CDisplaySettings::GetInstance().SetPixelRatio(1.0);
+ CDisplaySettings::GetInstance().SetZoomAmount(1.0);
+ }
+
+ //@TODO
+ m_videoSettings.m_CustomZoomAmount = CDisplaySettings::GetInstance().GetZoomAmount();
+ m_videoSettings.m_CustomPixelRatio = CDisplaySettings::GetInstance().GetPixelRatio();
+ m_videoSettings.m_CustomNonLinStretch = CDisplaySettings::GetInstance().IsNonLinearStretched();
+ m_videoSettings.m_CustomVerticalShift = CDisplaySettings::GetInstance().GetVerticalShift();
+}
+
+void CBaseRenderer::MarkDirty()
+{
+ CServiceBroker::GetGUI()->GetWindowManager().MarkDirty(m_destRect);
+}
+
+void CBaseRenderer::SetVideoSettings(const CVideoSettings &settings)
+{
+ m_videoSettings = settings;
+}
+
+void CBaseRenderer::SettingOptionsRenderMethodsFiller(
+ const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(13416), RENDER_METHOD_AUTO);
+
+#ifdef HAS_DX
+ list.push_back(IntegerSettingOption(g_localizeStrings.Get(16319), RENDER_METHOD_DXVA));
+ list.push_back(IntegerSettingOption(g_localizeStrings.Get(13431), RENDER_METHOD_D3D_PS));
+ list.push_back(IntegerSettingOption(g_localizeStrings.Get(13419), RENDER_METHOD_SOFTWARE));
+#endif
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/BaseRenderer.h b/xbmc/cores/VideoPlayer/VideoRenderers/BaseRenderer.h
new file mode 100644
index 0000000..67871f9
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/BaseRenderer.h
@@ -0,0 +1,144 @@
+/*
+ * 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 "DebugInfo.h"
+#include "RenderCapture.h"
+#include "RenderInfo.h"
+#include "VideoShaders/ShaderFormats.h"
+#include "cores/IPlayer.h"
+#include "cores/VideoPlayer/Buffers/VideoBuffer.h"
+#include "utils/Geometry.h"
+
+#include <utility>
+#include <vector>
+
+#define MAX_FIELDS 3
+#define NUM_BUFFERS 6
+
+class CSetting;
+struct IntegerSettingOption;
+
+enum EFIELDSYNC
+{
+ FS_NONE,
+ FS_TOP,
+ FS_BOT
+};
+
+// Render Methods
+enum RenderMethods
+{
+ RENDER_METHOD_AUTO = 0,
+ RENDER_METHOD_GLSL,
+ RENDER_METHOD_SOFTWARE,
+ RENDER_METHOD_D3D_PS,
+ RENDER_METHOD_DXVA,
+ RENDER_OVERLAYS = 99 // to retain compatibility
+};
+
+struct VideoPicture;
+
+class CBaseRenderer
+{
+public:
+ CBaseRenderer();
+ virtual ~CBaseRenderer();
+
+ // Player functions
+ virtual bool Configure(const VideoPicture &picture, float fps, unsigned int orientation) = 0;
+ virtual bool IsConfigured() = 0;
+ virtual void AddVideoPicture(const VideoPicture &picture, int index) = 0;
+ virtual bool IsPictureHW(const VideoPicture& picture) { return false; }
+ virtual void UnInit() = 0;
+ virtual bool Flush(bool saveBuffers) { return false; }
+ virtual void SetBufferSize(int numBuffers) { }
+ virtual void ReleaseBuffer(int idx) { }
+ virtual bool NeedBuffer(int idx) { return false; }
+ virtual bool IsGuiLayer() { return true; }
+ // Render info, can be called before configure
+ virtual CRenderInfo GetRenderInfo() { return CRenderInfo(); }
+ virtual void Update() = 0;
+ virtual void RenderUpdate(int index, int index2, bool clear, unsigned int flags, unsigned int alpha) = 0;
+ virtual bool RenderCapture(CRenderCapture* capture) = 0;
+ virtual bool ConfigChanged(const VideoPicture &picture) = 0;
+
+ // Feature support
+ virtual bool SupportsMultiPassRendering() = 0;
+ virtual bool Supports(ERENDERFEATURE feature) const { return false; }
+ virtual bool Supports(ESCALINGMETHOD method) const = 0;
+
+ virtual bool WantsDoublePass() { return false; }
+
+ void SetViewMode(int viewMode);
+
+ /*! \brief Get video rectangle and view window
+ \param source is original size of the video
+ \param dest is the target rendering area honoring aspect ratio of source
+ \param view is the entire target rendering area for the video (including black bars)
+ */
+ void GetVideoRect(CRect& source, CRect& dest, CRect& view) const;
+ float GetAspectRatio() const;
+
+ static void SettingOptionsRenderMethodsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ void SetVideoSettings(const CVideoSettings &settings);
+
+ // Gets debug info from render buffer
+ virtual DEBUG_INFO_VIDEO GetDebugInfo(int idx) { return {}; }
+
+ virtual CRenderCapture* GetRenderCapture() { return nullptr; }
+
+protected:
+ void CalcDestRect(float offsetX,
+ float offsetY,
+ float width,
+ float height,
+ float inputFrameRatio,
+ float zoomAmount,
+ float verticalShift,
+ CRect& destRect);
+ void CalcNormalRenderRect(float offsetX, float offsetY, float width, float height,
+ float inputFrameRatio, float zoomAmount, float verticalShift);
+ void CalculateFrameAspectRatio(unsigned int desired_width, unsigned int desired_height);
+ virtual void ManageRenderArea();
+ virtual void ReorderDrawPoints();
+ virtual EShaderFormat GetShaderFormat();
+ void MarkDirty();
+
+ //@todo drop those
+ void saveRotatedCoords();//saves the current state of m_rotatedDestCoords
+ void syncDestRectToRotatedPoints();//sync any changes of m_destRect to m_rotatedDestCoords
+ void restoreRotatedCoords();//restore the current state of m_rotatedDestCoords from saveRotatedCoords
+
+ unsigned int m_sourceWidth = 720;
+ unsigned int m_sourceHeight = 480;
+ float m_sourceFrameRatio = 1.0f;
+ float m_fps = 0.0f;
+
+ unsigned int m_renderOrientation = 0; // orientation of the video in degrees counter clockwise
+ // for drawing the texture with glVertex4f (holds all 4 corner points of the destination rect
+ // with correct orientation based on m_renderOrientation
+ // 0 - top left, 1 - top right, 2 - bottom right, 3 - bottom left
+ CPoint m_rotatedDestCoords[4];
+ CPoint m_savedRotatedDestCoords[4];//saved points from saveRotatedCoords call
+
+ CRect m_destRect;
+ CRect m_sourceRect;
+ CRect m_viewRect;
+
+ // rendering flags
+ unsigned m_iFlags = 0;
+ AVPixelFormat m_format = AV_PIX_FMT_NONE;
+
+ CVideoSettings m_videoSettings;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/CMakeLists.txt b/xbmc/cores/VideoPlayer/VideoRenderers/CMakeLists.txt
new file mode 100644
index 0000000..edafa9e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/CMakeLists.txt
@@ -0,0 +1,58 @@
+set(SOURCES BaseRenderer.cpp
+ ColorManager.cpp
+ OverlayRenderer.cpp
+ OverlayRendererUtil.cpp
+ RenderCapture.cpp
+ RenderFactory.cpp
+ RenderFlags.cpp
+ RenderManager.cpp
+ DebugRenderer.cpp)
+
+set(HEADERS BaseRenderer.h
+ ColorManager.h
+ DebugInfo.h
+ OverlayRenderer.h
+ OverlayRendererUtil.h
+ RenderCapture.h
+ RenderFactory.h
+ RenderFlags.h
+ RenderInfo.h
+ RenderManager.h
+ DebugRenderer.h)
+
+if(CORE_SYSTEM_NAME STREQUAL windows OR CORE_SYSTEM_NAME STREQUAL windowsstore)
+ list(APPEND SOURCES WinRenderer.cpp
+ OverlayRendererDX.cpp
+ RenderCaptureDX.cpp)
+ list(APPEND HEADERS WinRenderer.h
+ OverlayRendererDX.h
+ RenderCapture.h)
+endif()
+
+if (OPENGL_FOUND OR OPENGLES_FOUND)
+ list(APPEND SOURCES OverlayRendererGL.cpp
+ FrameBufferObject.cpp)
+ list(APPEND HEADERS OverlayRendererGL.h
+ FrameBufferObject.h)
+endif()
+
+if(OPENGL_FOUND)
+ list(APPEND SOURCES LinuxRendererGL.cpp
+ RenderCaptureGL.cpp)
+ list(APPEND HEADERS LinuxRendererGL.h
+ RenderCaptureGL.h)
+endif()
+
+if(OPENGLES_FOUND AND ("android" IN_LIST CORE_PLATFORM_NAME_LC OR
+ "ios" IN_LIST CORE_PLATFORM_NAME_LC OR
+ "tvos" IN_LIST CORE_PLATFORM_NAME_LC OR
+ "gbm" IN_LIST CORE_PLATFORM_NAME_LC OR
+ "x11" IN_LIST CORE_PLATFORM_NAME_LC OR
+ "wayland" IN_LIST CORE_PLATFORM_NAME_LC))
+ list(APPEND SOURCES LinuxRendererGLES.cpp
+ RenderCaptureGLES.cpp)
+ list(APPEND HEADERS LinuxRendererGLES.h
+ RenderCaptureGLES.h)
+endif()
+
+core_add_library(videorenderers)
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/ColorManager.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/ColorManager.cpp
new file mode 100644
index 0000000..e188881
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/ColorManager.cpp
@@ -0,0 +1,618 @@
+/*
+ * Copyright (C) 2016 Lauri Mylläri
+ * http://kodi.org
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ColorManager.h"
+
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+
+#include <cstdint>
+#include <math.h>
+#include <vector>
+
+using namespace XFILE;
+
+CColorManager::CColorManager()
+{
+ m_curVideoPrimaries = CMS_PRIMARIES_AUTO;
+ m_curClutSize = 0;
+ m_curCmsToken = 0;
+ m_curCmsMode = 0;
+}
+
+#if defined(HAVE_LCMS2)
+CColorManager::~CColorManager()
+{
+ if (m_hProfile)
+ {
+ cmsCloseProfile(m_hProfile);
+ m_hProfile = nullptr;
+ }
+}
+#else
+CColorManager::~CColorManager() = default;
+#endif //defined(HAVE_LCMS2)
+
+bool CColorManager::IsEnabled() const
+{
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool("videoscreen.cmsenabled") && IsValid();
+}
+
+bool CColorManager::IsValid() const
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ if (!settings->GetBool("videoscreen.cmsenabled"))
+ return true;
+
+ int cmsmode = settings->GetInt("videoscreen.cmsmode");
+ switch (cmsmode)
+ {
+ case CMS_MODE_3DLUT:
+ {
+ std::string fileName = settings->GetString("videoscreen.cms3dlut");
+ if (fileName.empty())
+ return false;
+ if (!CFile::Exists(fileName))
+ return false;
+ return true;
+ }
+#if defined(HAVE_LCMS2)
+ case CMS_MODE_PROFILE:
+ {
+ int cmslutsize = settings->GetInt("videoscreen.cmslutsize");
+ if (cmslutsize <= 0)
+ return false;
+ return true;
+ }
+#endif
+ default:
+ return false;
+ }
+}
+
+CMS_PRIMARIES avColorToCmsPrimaries(AVColorPrimaries primaries)
+{
+ switch (primaries)
+ {
+ case AVCOL_PRI_BT709:
+ return CMS_PRIMARIES_BT709;
+ case AVCOL_PRI_BT470M:
+ return CMS_PRIMARIES_BT470M;
+ case AVCOL_PRI_BT470BG:
+ return CMS_PRIMARIES_BT470BG;
+ case AVCOL_PRI_SMPTE170M:
+ return CMS_PRIMARIES_170M;
+ case AVCOL_PRI_SMPTE240M:
+ return CMS_PRIMARIES_240M;
+ case AVCOL_PRI_BT2020:
+ return CMS_PRIMARIES_BT2020;
+ default:
+ return CMS_PRIMARIES_BT709;
+ }
+}
+
+bool CColorManager::Get3dLutSize(CMS_DATA_FORMAT format, int *clutSize, int *dataSize)
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ int cmsmode = settings->GetInt("videoscreen.cmsmode");
+ switch (cmsmode)
+ {
+ case CMS_MODE_3DLUT:
+ {
+ std::string fileName = settings->GetString("videoscreen.cms3dlut");
+ if (fileName.empty())
+ return false;
+
+ int clutDimention;
+ if (!Probe3dLut(fileName, &clutDimention))
+ return false;
+
+ if (clutSize)
+ *clutSize = clutDimention;
+
+ if (dataSize)
+ {
+ int bytesInSample = format == CMS_DATA_FMT_RGBA ? 4 : 3;
+ *dataSize = sizeof(uint16_t) * clutDimention * clutDimention * clutDimention * bytesInSample;
+ }
+ return true;
+ }
+ case CMS_MODE_PROFILE:
+ {
+ int cmslutsize = settings->GetInt("videoscreen.cmslutsize");
+ if (cmslutsize <= 0)
+ return false;
+
+ int clutDimention = 1 << cmslutsize;
+ if (clutSize)
+ *clutSize = clutDimention;
+
+ if (dataSize)
+ {
+ int bytesInSample = format == CMS_DATA_FMT_RGBA ? 4 : 3;
+ *dataSize = sizeof(uint16_t) * clutDimention * clutDimention * clutDimention * bytesInSample;
+ }
+ return true;
+ }
+ default:
+ CLog::Log(LOGDEBUG, "ColorManager: unknown CMS mode {}", cmsmode);
+ return false;
+ }
+}
+
+bool CColorManager::GetVideo3dLut(AVColorPrimaries srcPrimaries, int* cmsToken,
+ CMS_DATA_FORMAT format, int clutSize, uint16_t* clutData)
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ CMS_PRIMARIES videoPrimaries = avColorToCmsPrimaries(srcPrimaries);
+ CLog::Log(LOGDEBUG, "ColorManager: video primaries: {}", (int)videoPrimaries);
+ switch (settings->GetInt("videoscreen.cmsmode"))
+ {
+ case CMS_MODE_3DLUT:
+ CLog::Log(LOGDEBUG, "ColorManager: CMS_MODE_3DLUT");
+ m_cur3dlutFile = settings->GetString("videoscreen.cms3dlut");
+ if (!Load3dLut(m_cur3dlutFile, format, clutSize, clutData))
+ return false;
+ m_curCmsMode = CMS_MODE_3DLUT;
+ break;
+
+ case CMS_MODE_PROFILE:
+ CLog::Log(LOGDEBUG, "ColorManager: CMS_MODE_PROFILE");
+#if defined(HAVE_LCMS2)
+ {
+ // check if display profile is not loaded, or has changed
+ if (m_curIccProfile != settings->GetString("videoscreen.displayprofile"))
+ {
+ // free old profile if there is one
+ if (m_hProfile)
+ cmsCloseProfile(m_hProfile);
+ // load profile
+ m_hProfile = LoadIccDisplayProfile(settings->GetString("videoscreen.displayprofile"));
+ if (!m_hProfile)
+ return false;
+ // detect blackpoint
+ if (cmsDetectBlackPoint(&m_blackPoint, m_hProfile, INTENT_PERCEPTUAL, 0))
+ {
+ CLog::Log(LOGDEBUG, "ColorManager: black point: {:f}", m_blackPoint.Y);
+ }
+ m_curIccProfile = settings->GetString("videoscreen.displayprofile");
+ }
+ // create gamma curve
+ cmsToneCurve* gammaCurve;
+ m_m_curIccGammaMode = static_cast<CMS_TRC_TYPE>(settings->GetInt("videoscreen.cmsgammamode"));
+ m_curIccGamma = settings->GetInt("videoscreen.cmsgamma");
+ gammaCurve = CreateToneCurve(m_m_curIccGammaMode, m_curIccGamma / 100.0, m_blackPoint);
+
+ // create source profile
+ m_curIccWhitePoint = static_cast<CMS_WHITEPOINT>(settings->GetInt("videoscreen.cmswhitepoint"));
+ m_curIccPrimaries = static_cast<CMS_PRIMARIES>(settings->GetInt("videoscreen.cmsprimaries"));
+ CLog::Log(LOGDEBUG, "ColorManager: primaries setting: {}", (int)m_curIccPrimaries);
+ if (m_curIccPrimaries == CMS_PRIMARIES_AUTO)
+ m_curIccPrimaries = videoPrimaries;
+ CLog::Log(LOGDEBUG, "ColorManager: source profile primaries: {}", (int)m_curIccPrimaries);
+ cmsHPROFILE sourceProfile = CreateSourceProfile(m_curIccPrimaries, gammaCurve, m_curIccWhitePoint);
+
+ // link profiles
+ // TODO: intent selection, switch output to 16 bits?
+ cmsSetAdaptationState(0.0);
+ uint32_t fmt = format == CMS_DATA_FMT_RGBA ? TYPE_RGBA_FLT : TYPE_RGB_FLT;
+ cmsHTRANSFORM deviceLink = cmsCreateTransform(sourceProfile, fmt, m_hProfile, fmt, INTENT_ABSOLUTE_COLORIMETRIC, 0);
+
+ // sample the transformation
+ if (deviceLink)
+ Create3dLut(deviceLink, format, clutSize, clutData);
+
+ // free gamma curve, source profile and transformation
+ if (deviceLink)
+ cmsDeleteTransform(deviceLink);
+ cmsCloseProfile(sourceProfile);
+ cmsFreeToneCurve(gammaCurve);
+ }
+
+ m_curCmsMode = CMS_MODE_PROFILE;
+ break;
+#else //defined(HAVE_LCMS2)
+ return false;
+#endif //defined(HAVE_LCMS2)
+
+ default:
+ CLog::Log(LOGDEBUG, "ColorManager: unknown CMS mode {}",
+ settings->GetInt("videoscreen.cmsmode"));
+ return false;
+ }
+
+ // set current state
+ m_curVideoPrimaries = videoPrimaries;
+ m_curClutSize = clutSize;
+ *cmsToken = ++m_curCmsToken;
+ return true;
+}
+
+bool CColorManager::CheckConfiguration(int cmsToken, AVColorPrimaries srcPrimaries)
+{
+ if (cmsToken != m_curCmsToken)
+ return false;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (m_curCmsMode != settings->GetInt("videoscreen.cmsmode"))
+ return false; // CMS mode has changed
+ switch (m_curCmsMode)
+ {
+ case CMS_MODE_3DLUT:
+ if (m_cur3dlutFile != settings->GetString("videoscreen.cms3dlut"))
+ return false; // different 3dlut file selected
+ break;
+ case CMS_MODE_PROFILE:
+#if defined(HAVE_LCMS2)
+ if (m_curIccProfile != settings->GetString("videoscreen.displayprofile"))
+ return false; // different ICC profile selected
+ if (m_curIccWhitePoint != settings->GetInt("videoscreen.cmswhitepoint"))
+ return false; // whitepoint changed
+ {
+ CMS_PRIMARIES primaries = static_cast<CMS_PRIMARIES>(settings->GetInt("videoscreen.cmsprimaries"));
+ if (primaries == CMS_PRIMARIES_AUTO)
+ primaries = avColorToCmsPrimaries(srcPrimaries);
+ if (m_curIccPrimaries != primaries)
+ return false; // primaries changed
+ }
+ if (m_m_curIccGammaMode != static_cast<CMS_TRC_TYPE>(settings->GetInt("videoscreen.cmsgammamode")))
+ return false; // gamma mode changed
+ if (m_curIccGamma != settings->GetInt("videoscreen.cmsgamma"))
+ return false; // effective gamma changed
+ if (m_curClutSize != 1 << settings->GetInt("videoscreen.cmslutsize"))
+ return false; // CLUT size changed
+ // TODO: check other parameters
+#else //defined(HAVE_LCMS2)
+ return true;
+#endif //defined(HAVE_LCMS2)
+ break;
+ default:
+ CLog::Log(LOGERROR, "ColorManager: unexpected CMS mode: {}", m_curCmsMode);
+ return false;
+ }
+ return true;
+}
+
+
+
+// madvr 3dlut file format support
+struct H3DLUT
+{
+ char signature[4]; // file signature; must be: '3DLT'
+ uint32_t fileVersion; // file format version number (currently "1")
+ char programName[32]; // name of the program that created the file
+ uint64_t programVersion; // version number of the program that created the file
+ uint32_t inputBitDepth[3]; // input bit depth per component (Y,Cb,Cr or R,G,B)
+ uint32_t inputColorEncoding; // input color encoding standard
+ uint32_t outputBitDepth; // output bit depth for all components (valid values are 8, 16 and 32)
+ uint32_t outputColorEncoding; // output color encoding standard
+ uint32_t parametersFileOffset;// number of bytes between the beginning of the file and array parametersData
+ uint32_t parametersSize; // size in bytes of the array parametersData
+ uint32_t lutFileOffset; // number of bytes between the beginning of the file and array lutData
+ uint32_t lutCompressionMethod;// type of compression used if any (0 = none, ...)
+ uint32_t lutCompressedSize; // size in bytes of the array lutData inside the file, whether compressed or not
+ uint32_t lutUncompressedSize; // true size in bytes of the array lutData when in memory for usage (outside the file)
+ // This header is followed by the char array 'parametersData', of length 'parametersSize',
+ // and by the array 'lutDataxx', of length 'lutCompressedSize'.
+};
+
+bool CColorManager::Probe3dLut(const std::string& filename, int* clutSize)
+{
+ struct H3DLUT header;
+ CFile lutFile;
+
+ if (!lutFile.Open(filename))
+ {
+ CLog::Log(LOGERROR, "{}: Could not open 3DLUT file: {}", __FUNCTION__, filename);
+ return false;
+ }
+
+ if (lutFile.Read(&header, sizeof(header)) < static_cast<ssize_t>(sizeof(header)))
+ {
+ CLog::Log(LOGERROR, "{}: Could not read 3DLUT header: {}", __FUNCTION__, filename);
+ return false;
+ }
+
+ if ( !(header.signature[0]=='3'
+ && header.signature[1]=='D'
+ && header.signature[2]=='L'
+ && header.signature[3]=='T') )
+ {
+ CLog::Log(LOGERROR, "{}: Not a 3DLUT file: {}", __FUNCTION__, filename);
+ return false;
+ }
+
+ if ( header.fileVersion != 1
+ || header.lutCompressionMethod != 0
+ || header.inputColorEncoding != 0
+ || header.outputColorEncoding != 0 )
+ {
+ CLog::Log(LOGERROR, "{}: Unsupported 3DLUT file: {}", __FUNCTION__, filename);
+ return false;
+ }
+
+ int rSize = 1 << header.inputBitDepth[0];
+ int gSize = 1 << header.inputBitDepth[1];
+ int bSize = 1 << header.inputBitDepth[2];
+ if (rSize != gSize || rSize != bSize)
+ {
+ CLog::Log(LOGERROR, "{}: Different channel resolutions unsupported: {}", __FUNCTION__,
+ filename);
+ return false;
+ }
+
+ if (clutSize)
+ *clutSize = rSize;
+
+ lutFile.Close();
+ return true;
+}
+
+bool CColorManager::Load3dLut(const std::string& filename,
+ CMS_DATA_FORMAT format,
+ int CLUTsize,
+ uint16_t* clutData)
+{
+ struct H3DLUT header;
+ CFile lutFile;
+
+ if (!lutFile.Open(filename))
+ {
+ CLog::Log(LOGERROR, "{}: Could not open 3DLUT file: {}", __FUNCTION__, filename);
+ return false;
+ }
+
+ if (lutFile.Read(&header, sizeof(header)) < static_cast<ssize_t>(sizeof(header)))
+ {
+ CLog::Log(LOGERROR, "{}: Could not read 3DLUT header: {}", __FUNCTION__, filename);
+ return false;
+ }
+
+ int rSize = 1 << header.inputBitDepth[0];
+ int gSize = 1 << header.inputBitDepth[1];
+ int bSize = 1 << header.inputBitDepth[2];
+
+ if ( rSize != CLUTsize || rSize != gSize || rSize != bSize)
+ {
+ CLog::Log(LOGERROR, "{}: Different channel resolutions unsupported: {}", __FUNCTION__,
+ filename);
+ return false;
+ }
+
+ lutFile.Seek(header.lutFileOffset, SEEK_SET);
+
+ int components = format == CMS_DATA_FMT_RGBA ? 4 : 3;
+ for (int rIndex = 0; rIndex < rSize; rIndex++)
+ {
+ for (int gIndex = 0; gIndex < gSize; gIndex++)
+ {
+ std::vector<uint16_t> input(bSize * 3); // always 3 components
+ lutFile.Read(input.data(), input.size() * sizeof(input[0]));
+ int index = (rIndex + gIndex * rSize) * components;
+ for (int bIndex = 0; bIndex < bSize; bIndex++)
+ {
+ int offset = index + bIndex * rSize * gSize * components;
+ clutData[offset + 0] = input[bIndex * 3 + 2];
+ clutData[offset + 1] = input[bIndex * 3 + 1];
+ clutData[offset + 2] = input[bIndex * 3 + 0];
+ if (format == CMS_DATA_FMT_RGBA)
+ clutData[offset + 3] = 0xFFFF;
+ }
+ }
+ }
+
+ lutFile.Close();
+ return true;
+}
+
+#if defined(HAVE_LCMS2)
+// ICC profile support
+
+cmsHPROFILE CColorManager::LoadIccDisplayProfile(const std::string& filename)
+{
+ cmsHPROFILE hProfile;
+
+ hProfile = cmsOpenProfileFromFile(filename.c_str(), "r");
+ if (!hProfile)
+ {
+ CLog::Log(LOGERROR, "ICC profile not found");
+ }
+ return hProfile;
+}
+
+
+cmsToneCurve* CColorManager::CreateToneCurve(CMS_TRC_TYPE gammaType,
+ double gammaValue,
+ cmsCIEXYZ blackPoint)
+{
+ const int tableSize = 1024;
+ cmsFloat32Number gammaTable[tableSize];
+
+ switch (gammaType)
+ {
+ case CMS_TRC_INPUT_OFFSET:
+ // calculate gamma to match effective gamma provided, then fall through to bt.1886
+ {
+ double effectiveGamma = gammaValue;
+ double gammaLow = effectiveGamma; // low limit for infinite contrast ratio
+ double gammaHigh = 3.2; // high limit for 2.4 gamma on 200:1 contrast ratio
+ double gammaGuess = 0.0;
+#define TARGET(gamma) (pow(0.5, (gamma)))
+#define GAIN(bkpt, gamma) (pow(1-pow((bkpt), 1/(gamma)), (gamma)))
+#define LIFT(bkpt, gamma) (pow((bkpt), 1/(gamma)) / (1-pow((bkpt), 1/(gamma))))
+#define HALFPT(bkpt, gamma) (GAIN(bkpt, gamma)*pow(0.5+LIFT(bkpt, gamma), gamma))
+ for (int i=0; i<3; i++)
+ {
+ // calculate 50% output for gammaLow and gammaHigh, compare to target 50% output
+ gammaGuess = gammaLow + (gammaHigh-gammaLow)
+ * ((HALFPT(blackPoint.Y, gammaLow)-TARGET(effectiveGamma))
+ / (HALFPT(blackPoint.Y, gammaLow)-HALFPT(blackPoint.Y, gammaHigh)));
+ if (HALFPT(blackPoint.Y, gammaGuess) < TARGET(effectiveGamma))
+ {
+ // guess is too high
+ // move low limit half way to guess
+ gammaLow = gammaLow + (gammaGuess-gammaLow)/2;
+ // set high limit to guess
+ gammaHigh = gammaGuess;
+ }
+ else
+ {
+ // guess is too low
+ // set low limit to guess
+ gammaLow = gammaGuess;
+ // move high limit half way to guess
+ gammaHigh = gammaHigh + (gammaGuess-gammaLow)/2;
+ }
+ }
+ gammaValue = gammaGuess;
+ CLog::Log(LOGINFO, "calculated technical gamma {:0.3f} (50% target {:0.4f}, output {:0.4f})",
+ gammaValue, TARGET(effectiveGamma), HALFPT(blackPoint.Y, gammaValue));
+#undef TARGET
+#undef GAIN
+#undef LIFT
+#undef HALFPT
+ }
+ // fall through to bt.1886 with calculated technical gamma
+ [[fallthrough]];
+
+ case CMS_TRC_BT1886:
+ {
+ double bkipow = pow(blackPoint.Y, 1.0/gammaValue);
+ double wtipow = 1.0;
+ double lift = bkipow / (wtipow - bkipow);
+ double gain = pow(wtipow - bkipow, gammaValue);
+ for (int i=0; i<tableSize; i++)
+ {
+ gammaTable[i] = gain * pow(((double) i)/(tableSize-1) + lift, gammaValue);
+ }
+ }
+ break;
+
+ case CMS_TRC_OUTPUT_OFFSET:
+ {
+ double gain = 1-blackPoint.Y;
+ // TODO: here gamma is adjusted to match absolute gamma output at 50%
+ // - is it a good idea or should the provided gamma be kept?
+ double adjustedGamma = log(gain/(gain+pow(2,-gammaValue)-1))/log(2);
+ for (int i=0; i<tableSize; i++)
+ {
+ gammaTable[i] = gain * pow(((double) i)/(tableSize-1), adjustedGamma) + blackPoint.Y;
+ }
+ }
+ break;
+
+ case CMS_TRC_ABSOLUTE:
+ {
+ for (int i=0; i<tableSize; i++)
+ {
+ gammaTable[i] = fmax(blackPoint.Y, pow(((double) i)/(tableSize-1), gammaValue));
+ }
+ }
+ break;
+
+ default:
+ CLog::Log(LOGERROR, "gamma type {} not implemented", gammaType);
+ }
+
+ cmsToneCurve* result = cmsBuildTabulatedToneCurveFloat(0,
+ tableSize,
+ gammaTable);
+ return result;
+}
+
+
+cmsHPROFILE CColorManager::CreateSourceProfile(CMS_PRIMARIES primaries, cmsToneCurve *gamma, CMS_WHITEPOINT whitepoint)
+{
+ cmsToneCurve* Gamma3[3];
+ cmsHPROFILE hProfile;
+ cmsCIExyY whiteCoords[] = {
+ { 0.3127, 0.3290, 1.0 }, // D65 as specified in BT.709
+ { 0.2830, 0.2980, 1.0 } // Japanese D93 - is there a definitive source? NHK? ARIB TR-B9?
+ };
+ cmsCIExyYTRIPLE primaryCoords[] = {
+ {{0.640, 0.330, 1.0}, // auto setting, these should not be used (BT.709 just in case)
+ {0.300, 0.600, 1.0},
+ {0.150, 0.060, 1.0}},
+ {{0.640, 0.330, 1.0}, // BT.709 (HDTV, sRGB)
+ {0.300, 0.600, 1.0},
+ {0.150, 0.060, 1.0}},
+ {{0.630, 0.340, 1.0}, // SMPTE 170M (SDTV)
+ {0.310, 0.595, 1.0},
+ {0.155, 0.070, 1.0}},
+ {{0.670, 0.330, 1.0}, // BT.470 M (obsolete NTSC 1953)
+ {0.210, 0.710, 1.0},
+ {0.140, 0.080, 1.0}},
+ {{0.640, 0.330, 1.0}, // BT.470 B/G (obsolete PAL/SECAM 1975)
+ {0.290, 0.600, 1.0},
+ {0.150, 0.060, 1.0}},
+ {{0.630, 0.340, 1.0}, // SMPTE 240M (obsolete HDTV 1988)
+ {0.310, 0.595, 1.0},
+ {0.155, 0.070, 1.0}},
+ {{0.708, 0.292, 1.0}, // BT.2020 UHDTV
+ {0.170, 0.797, 1.0},
+ {0.131, 0.046, 1.0}}};
+
+ Gamma3[0] = Gamma3[1] = Gamma3[2] = gamma;
+ hProfile = cmsCreateRGBProfile(&whiteCoords[whitepoint],
+ &primaryCoords[primaries],
+ Gamma3);
+ return hProfile;
+}
+
+
+void CColorManager::Create3dLut(cmsHTRANSFORM transform, CMS_DATA_FORMAT format, int clutSize, uint16_t *clutData)
+{
+ const int lutResolution = clutSize;
+ int components = format == CMS_DATA_FMT_RGBA ? 4 : 3;
+
+ cmsFloat32Number *input = new cmsFloat32Number[components*lutResolution];
+ cmsFloat32Number *output = new cmsFloat32Number[components*lutResolution];
+
+#define clamp(x, l, h) ( ((x) < (l)) ? (l) : ( ((x) > (h)) ? (h) : (x) ) )
+#define videoToPC(x) ( clamp((((x)*255)-16)/219,0,1) )
+#define PCToVideo(x) ( (((x)*219)+16)/255 )
+
+ for (int bIndex=0; bIndex<lutResolution; bIndex++)
+ {
+ for (int gIndex=0; gIndex<lutResolution; gIndex++)
+ {
+ for (int rIndex=0; rIndex<lutResolution; rIndex++)
+ {
+ int offset = rIndex * components;
+ input[offset + 0] = videoToPC(rIndex / (lutResolution-1.0));
+ input[offset + 1] = videoToPC(gIndex / (lutResolution-1.0));
+ input[offset + 2] = videoToPC(bIndex / (lutResolution-1.0));
+ if (format == CMS_DATA_FMT_RGBA)
+ input[offset + 3] = 0.0f;
+ }
+ int index = (bIndex*lutResolution*lutResolution + gIndex*lutResolution)*components;
+ cmsDoTransform(transform, input, output, lutResolution);
+ for (int i=0; i < lutResolution * components; i++)
+ {
+ clutData[index + i] = PCToVideo(output[i]) * 65535;
+ }
+ }
+ }
+
+ for (int y=0; y<lutResolution; y+=1)
+ {
+ int index = components*(y*lutResolution*lutResolution + y*lutResolution + y);
+ CLog::Log(LOGDEBUG, " {} ({}): {} {} {}", (int)round(y * 255 / (lutResolution - 1.0)), y,
+ (int)round(clutData[index + 0]), (int)round(clutData[index + 1]),
+ (int)round(clutData[index + 2]));
+ }
+ delete[] input;
+ delete[] output;
+}
+
+#endif //defined(HAVE_LCMS2)
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/ColorManager.h b/xbmc/cores/VideoPlayer/VideoRenderers/ColorManager.h
new file mode 100644
index 0000000..1366c3a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/ColorManager.h
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2016 Lauri Mylläri
+ * http://kodi.org
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#if defined(HAVE_LCMS2)
+#include <lcms2.h>
+#endif
+
+#include <cstdint>
+#include <string>
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+enum CMS_DATA_FORMAT
+{
+ CMS_DATA_FMT_RGB,
+ CMS_DATA_FMT_RGBA,
+ CMS_DATA_FMT_COUNT
+};
+
+enum CMS_MODE
+{
+ CMS_MODE_3DLUT,
+ CMS_MODE_PROFILE,
+ CMS_MODE_COUNT
+};
+
+enum CMS_WHITEPOINT
+{
+ CMS_WHITEPOINT_D65,
+ CMS_WHITEPOINT_D93,
+ CMS_WHITEPOINT_COUNT
+};
+
+enum CMS_PRIMARIES
+{
+ CMS_PRIMARIES_AUTO,
+ CMS_PRIMARIES_BT709, // HDTV
+ CMS_PRIMARIES_170M, // SDTV
+ CMS_PRIMARIES_BT470M, // old NTSC (1953)
+ CMS_PRIMARIES_BT470BG, // old PAL/SECAM (1975)
+ CMS_PRIMARIES_240M, // old HDTV (1988)
+ CMS_PRIMARIES_BT2020, // UHDTV
+ CMS_PRIMARIES_COUNT
+};
+
+enum CMS_TRC_TYPE
+{
+ CMS_TRC_BT1886,
+ CMS_TRC_INPUT_OFFSET,
+ CMS_TRC_OUTPUT_OFFSET,
+ CMS_TRC_ABSOLUTE,
+ CMS_TRC_COUNT
+};
+
+class CColorManager
+{
+public:
+ CColorManager();
+ virtual ~CColorManager();
+
+ /*!
+ \brief Check if user has requested color management
+ \return true on enabled, false otherwise
+ */
+ bool IsEnabled() const;
+
+ /*!
+ \brief Check if configuration of color management is valid
+ \return true on valid, false otherwise
+ */
+ bool IsValid() const;
+
+ /*!
+ \brief Get a 3D LUT for video color correction
+ \param srcPrimaries video primaries (see AVColorPrimaries)
+ \param cmsToken pointer to a color manager configuration token
+ \param format of CLUT data
+ \param clutSize CLUT resolution
+ \param clutData pointer to CLUT data
+ \return true on success, false otherwise
+ */
+ bool GetVideo3dLut(AVColorPrimaries srcPrimaries, int* cmsToken, CMS_DATA_FORMAT format,
+ int clutSize, uint16_t* clutData);
+
+ /*!
+ \brief Check if a 3D LUT is still valid
+ \param cmsToken pointer to a color manager configuration token
+ \param srcPrimaries video primaries (see AVColorPrimaries)
+ \return true on valid, false if 3D LUT should be reloaded
+ */
+ bool CheckConfiguration(int cmsToken, AVColorPrimaries srcPrimaries);
+
+ /*!
+ \brief Get a 3D LUT dimension and data size for video color correction
+ \param format required format of CLUT data
+ \param clutSize pointer to CLUT resolution
+ \param dataSize pointer to CLUT data size
+ \return true on success, false otherwise
+ */
+ static bool Get3dLutSize(CMS_DATA_FORMAT format, int *clutSize, int *dataSize);
+
+private:
+ /*! \brief Check .3dlut file validity
+ \param filename full path and filename
+ \param clutSize pointer to CLUT resolution
+ \return true if the file can be loaded, false otherwise
+ */
+ static bool Probe3dLut(const std::string& filename, int* clutSize);
+
+ /*! \brief Load a .3dlut file
+ \param filename full path and filename
+ \param format of CLUT data
+ \param clutSize CLUT resolution
+ \param clutData pointer to CLUT data
+ \return true on success, false otherwise
+ */
+ static bool Load3dLut(const std::string& filename,
+ CMS_DATA_FORMAT format,
+ int clutSize,
+ uint16_t* clutData);
+
+
+#if defined(HAVE_LCMS2)
+ // ProbeIccDisplayProfile
+
+ // ProbeIccDeviceLink (?)
+
+
+ /* \brief Load an ICC display profile
+ \param filename full path and filename
+ \return display profile (cmsHPROFILE)
+ */
+ cmsHPROFILE LoadIccDisplayProfile(const std::string& filename);
+
+ /* \brief Load an ICC device link
+ \param filename full path and filename
+ \return device link (cmsHTRANSFORM)
+ */
+ // LoadIccDeviceLink (?)
+
+
+ // create a gamma curve
+ cmsToneCurve* CreateToneCurve(CMS_TRC_TYPE gammaType, double gammaValue, cmsCIEXYZ blackPoint);
+
+ // create a source profile
+ cmsHPROFILE CreateSourceProfile(CMS_PRIMARIES primaries, cmsToneCurve *gamma, CMS_WHITEPOINT whitepoint);
+
+
+ /* \brief Create 3D LUT
+ Samples a cmsHTRANSFORM object to create a 3D LUT of specified resolution
+ \param transform cmsHTRANSFORM object to sample
+ \param format of CLUT data
+ \param resolution size of the 3D LUT to create
+ \param clut pointer to LUT data
+ */
+ void Create3dLut(cmsHTRANSFORM transform, CMS_DATA_FORMAT format, int clutSize, uint16_t *clutData);
+
+ // keep current display profile loaded here
+ cmsHPROFILE m_hProfile = nullptr;
+ cmsCIEXYZ m_blackPoint = { 0, 0, 0 };
+
+ // display parameters (gamma, input/output offset, primaries, whitepoint, intent?)
+ CMS_WHITEPOINT m_curIccWhitePoint;
+ CMS_PRIMARIES m_curIccPrimaries;
+ CMS_TRC_TYPE m_m_curIccGammaMode;
+ int m_curIccGamma; // gamma multiplied by 100
+#endif // defined(HAVE_LCMS2)
+
+ // current configuration:
+ CMS_PRIMARIES m_curVideoPrimaries;
+ int m_curClutSize;
+ int m_curCmsToken;
+ // (compare the following to system settings to see if configuration is still valid)
+ int m_curCmsMode;
+ std::string m_cur3dlutFile;
+ std::string m_curIccProfile;
+
+};
+
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/DebugInfo.h b/xbmc/cores/VideoPlayer/VideoRenderers/DebugInfo.h
new file mode 100644
index 0000000..d800c1a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/DebugInfo.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2005-2021 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 <string>
+
+struct DEBUG_INFO_PLAYER
+{
+ std::string audio;
+ std::string video;
+ std::string player;
+ std::string vsync;
+};
+
+struct DEBUG_INFO_VIDEO
+{
+ std::string videoSource;
+ std::string metaPrim;
+ std::string metaLight;
+ std::string shader;
+};
+
+struct DEBUG_INFO_RENDER
+{
+ std::string renderFlags;
+ std::string videoOutput;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/DebugRenderer.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/DebugRenderer.cpp
new file mode 100644
index 0000000..87acd24
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/DebugRenderer.cpp
@@ -0,0 +1,152 @@
+/*
+ * 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 "DebugRenderer.h"
+
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayLibass.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "settings/SubtitlesSettings.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+using namespace OVERLAY;
+
+CDebugRenderer::CDebugRenderer()
+{
+}
+
+CDebugRenderer::~CDebugRenderer()
+{
+ Dispose();
+}
+
+void CDebugRenderer::Initialize()
+{
+ if (m_isInitialized)
+ return;
+
+ m_adapter = new CSubtitlesAdapter();
+
+ m_isInitialized = m_adapter->Initialize();
+ if (!m_isInitialized)
+ {
+ CLog::Log(LOGERROR, "{} - Failed to configure OSD info debug renderer", __FUNCTION__);
+ delete m_adapter;
+ m_adapter = nullptr;
+ return;
+ }
+
+ // We create only a single overlay with a fixed PTS for each rendered frame
+ m_overlay = m_adapter->CreateOverlay();
+ m_overlayRenderer.AddOverlay(m_overlay, 1000000., 0);
+}
+
+void CDebugRenderer::Dispose()
+{
+ m_isInitialized = false;
+ m_overlayRenderer.Flush();
+ if (m_overlay)
+ {
+ m_overlay->Release();
+ m_overlay = nullptr;
+ }
+ if (m_adapter)
+ {
+ delete m_adapter;
+ m_adapter = nullptr;
+ }
+}
+
+void CDebugRenderer::SetInfo(DEBUG_INFO_PLAYER& info)
+{
+ if (!m_isInitialized)
+ return;
+
+ // FIXME: We currently force ASS_Event's and the current PTS
+ // of rendering with fixed values to allow perpetual
+ // display of on-screen text. It would be appropriate for Libass
+ // provide a way to allow fixed on-screen text display
+ // without use all these fixed values.
+ m_adapter->FlushSubtitles();
+ m_adapter->AddSubtitle(info.audio, 0., 5000000.);
+ m_adapter->AddSubtitle(info.video, 0., 5000000.);
+ m_adapter->AddSubtitle(info.player, 0., 5000000.);
+ m_adapter->AddSubtitle(info.vsync, 0., 5000000.);
+}
+
+void CDebugRenderer::SetInfo(DEBUG_INFO_VIDEO& video, DEBUG_INFO_RENDER& render)
+{
+ if (!m_isInitialized)
+ return;
+
+ // FIXME: We currently force ASS_Event's and the current PTS
+ // of rendering with fixed values to allow perpetual
+ // display of on-screen text. It would be appropriate for Libass
+ // provide a way to allow fixed on-screen text display
+ // without use all these fixed values.
+ m_adapter->FlushSubtitles();
+ m_adapter->AddSubtitle(video.videoSource, 0., 5000000.);
+ m_adapter->AddSubtitle(video.metaPrim, 0., 5000000.);
+ m_adapter->AddSubtitle(video.metaLight, 0., 5000000.);
+ m_adapter->AddSubtitle(video.shader, 0., 5000000.);
+ m_adapter->AddSubtitle(render.renderFlags, 0., 5000000.);
+ m_adapter->AddSubtitle(render.videoOutput, 0., 5000000.);
+}
+
+void CDebugRenderer::Render(CRect& src, CRect& dst, CRect& view)
+{
+ if (!m_isInitialized)
+ return;
+
+ m_overlayRenderer.SetVideoRect(src, dst, view);
+ m_overlayRenderer.Render(0);
+}
+
+void CDebugRenderer::Flush()
+{
+ if (!m_isInitialized)
+ return;
+
+ m_adapter->FlushSubtitles();
+}
+
+CDebugRenderer::CRenderer::CRenderer() : OVERLAY::CRenderer()
+{
+}
+
+void CDebugRenderer::CRenderer::Render(int idx)
+{
+ std::vector<SElement>& list = m_buffers[idx];
+ for (std::vector<SElement>::iterator it = list.begin(); it != list.end(); ++it)
+ {
+ if (it->overlay_dvd)
+ {
+ CDVDOverlayLibass* ovAss = static_cast<CDVDOverlayLibass*>(it->overlay_dvd);
+ if (!ovAss || !ovAss->GetLibassHandler())
+ continue;
+
+ bool updateStyle = !m_debugOverlayStyle;
+ if (updateStyle)
+ CreateSubtitlesStyle();
+
+ COverlay* o = ConvertLibass(ovAss, it->pts, updateStyle, m_debugOverlayStyle);
+
+ if (o)
+ OVERLAY::CRenderer::Render(o);
+ }
+ }
+ ReleaseUnused();
+}
+
+void CDebugRenderer::CRenderer::CreateSubtitlesStyle()
+{
+ m_debugOverlayStyle = std::make_shared<KODI::SUBTITLES::STYLE::style>();
+ m_debugOverlayStyle->fontName = KODI::SUBTITLES::FONT_DEFAULT_FAMILYNAME;
+ m_debugOverlayStyle->fontSize = 20.0;
+ m_debugOverlayStyle->marginVertical = 12;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/DebugRenderer.h b/xbmc/cores/VideoPlayer/VideoRenderers/DebugRenderer.h
new file mode 100644
index 0000000..718d1b4
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/DebugRenderer.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "DebugInfo.h"
+#include "OverlayRenderer.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlay.h"
+#include "cores/VideoPlayer/DVDSubtitles/SubtitlesAdapter.h"
+
+#include <atomic>
+#include <memory>
+#include <string>
+
+class CDebugRenderer
+{
+public:
+ CDebugRenderer();
+ virtual ~CDebugRenderer();
+ void Initialize();
+ void Dispose();
+ void SetInfo(DEBUG_INFO_PLAYER& info);
+ void SetInfo(DEBUG_INFO_VIDEO& video, DEBUG_INFO_RENDER& render);
+ void Render(CRect& src, CRect& dst, CRect& view);
+ void Flush();
+
+protected:
+ class CRenderer : public OVERLAY::CRenderer
+ {
+ public:
+ CRenderer();
+ void Render(int idx) override;
+ void CreateSubtitlesStyle();
+
+ private:
+ // Implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override{};
+
+ std::shared_ptr<struct KODI::SUBTITLES::STYLE::style> m_debugOverlayStyle;
+ };
+
+ CRenderer m_overlayRenderer;
+
+private:
+ CSubtitlesAdapter* m_adapter{nullptr};
+ std::atomic_bool m_isInitialized{false};
+ CDVDOverlay* m_overlay{nullptr};
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/FrameBufferObject.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/FrameBufferObject.cpp
new file mode 100644
index 0000000..58c3410
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/FrameBufferObject.cpp
@@ -0,0 +1,127 @@
+/*
+ * 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 "FrameBufferObject.h"
+
+#include "ServiceBroker.h"
+#include "rendering/RenderSystem.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+
+//////////////////////////////////////////////////////////////////////
+// CFrameBufferObject
+//////////////////////////////////////////////////////////////////////
+
+CFrameBufferObject::CFrameBufferObject()
+{
+ m_valid = false;
+ m_supported = false;
+ m_bound = false;
+}
+
+bool CFrameBufferObject::IsSupported()
+{
+ if(CServiceBroker::GetRenderSystem()->IsExtSupported("GL_EXT_framebuffer_object"))
+ m_supported = true;
+ else
+ m_supported = false;
+ return m_supported;
+}
+
+bool CFrameBufferObject::Initialize()
+{
+ if (!IsSupported())
+ return false;
+
+ Cleanup();
+
+ glGenFramebuffers(1, &m_fbo);
+ VerifyGLState();
+
+ if (!m_fbo)
+ return false;
+
+ m_valid = true;
+ return true;
+}
+
+void CFrameBufferObject::Cleanup()
+{
+ if (!IsValid())
+ return;
+
+ if (m_fbo)
+ glDeleteFramebuffers(1, &m_fbo);
+
+ if (m_texid)
+ glDeleteTextures(1, &m_texid);
+
+ m_texid = 0;
+ m_fbo = 0;
+ m_valid = false;
+ m_bound = false;
+}
+
+bool CFrameBufferObject::CreateAndBindToTexture(GLenum target, int width, int height, GLenum format, GLenum type,
+ GLenum filter, GLenum clampmode)
+{
+ if (!IsValid())
+ return false;
+
+ if (m_texid)
+ glDeleteTextures(1, &m_texid);
+
+ glGenTextures(1, &m_texid);
+ glBindTexture(target, m_texid);
+ glTexImage2D(target, 0, format, width, height, 0, GL_RGBA, type, NULL);
+ glTexParameteri(target, GL_TEXTURE_WRAP_S, clampmode);
+ glTexParameteri(target, GL_TEXTURE_WRAP_T, clampmode);
+ glTexParameteri(target, GL_TEXTURE_MAG_FILTER, filter);
+ glTexParameteri(target, GL_TEXTURE_MIN_FILTER, filter);
+ VerifyGLState();
+
+ m_bound = false;
+ glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
+ glBindTexture(target, m_texid);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, m_texid, 0);
+ VerifyGLState();
+ GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ if (status != GL_FRAMEBUFFER_COMPLETE)
+ {
+ VerifyGLState();
+ return false;
+ }
+ m_bound = true;
+ return true;
+}
+
+void CFrameBufferObject::SetFiltering(GLenum target, GLenum mode)
+{
+ glBindTexture(target, m_texid);
+ glTexParameteri(target, GL_TEXTURE_MAG_FILTER, mode);
+ glTexParameteri(target, GL_TEXTURE_MIN_FILTER, mode);
+}
+
+// Begin rendering to FBO
+bool CFrameBufferObject::BeginRender()
+{
+ if (IsValid() && IsBound())
+ {
+ glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
+ return true;
+ }
+ return false;
+}
+
+// Finish rendering to FBO
+void CFrameBufferObject::EndRender() const
+{
+ if (IsValid())
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/FrameBufferObject.h b/xbmc/cores/VideoPlayer/VideoRenderers/FrameBufferObject.h
new file mode 100644
index 0000000..a284c82
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/FrameBufferObject.h
@@ -0,0 +1,76 @@
+/*
+ * 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 "system_gl.h"
+
+//
+// CFrameBufferObject
+// A class that abstracts FBOs to facilitate Render To Texture
+//
+// Requires OpenGL 1.5+ or the GL_EXT_framebuffer_object extension.
+//
+// Usage:
+//
+// CFrameBufferObject *fbo = new CFrameBufferObject();
+// fbo->Initialize();
+// fbo->CreateAndBindToTexture(GL_TEXTURE_2D, 256, 256, GL_RGBA);
+// OR fbo->BindToTexture(GL_TEXTURE_2D, <existing texture ID>);
+// fbo->BeginRender();
+// <normal GL rendering calls>
+// fbo->EndRender();
+// bind and use texture anywhere
+// glBindTexture(GL_TEXTURE_2D, fbo->Texture());
+//
+
+class CFrameBufferObject
+{
+public:
+ // Constructor
+ CFrameBufferObject();
+
+ // returns true if FBO support is detected
+ bool IsSupported();
+
+ // returns true if FBO has been initialized
+ bool IsValid() const { return m_valid; }
+
+ // returns true if FBO has a texture bound to it
+ bool IsBound() const { return m_bound; }
+
+ // initialize the FBO
+ bool Initialize();
+
+ // Cleanup
+ void Cleanup();
+
+ // Set texture filtering
+ void SetFiltering(GLenum target, GLenum mode);
+
+ // Create a new texture and bind to it
+ bool CreateAndBindToTexture(GLenum target, int width, int height, GLenum format, GLenum type=GL_UNSIGNED_BYTE,
+ GLenum filter=GL_LINEAR, GLenum clampmode=GL_CLAMP_TO_EDGE);
+
+ // Return the internally created texture ID
+ GLuint Texture() const { return m_texid; }
+
+ // Begin rendering to FBO
+ bool BeginRender();
+ // Finish rendering to FBO
+ void EndRender() const;
+
+private:
+ GLuint m_fbo = 0;
+ bool m_valid;
+ bool m_bound;
+ bool m_supported;
+ GLuint m_texid = 0;
+};
+
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/CMakeLists.txt b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/CMakeLists.txt
new file mode 100644
index 0000000..b128623
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/CMakeLists.txt
@@ -0,0 +1,69 @@
+if(CORE_SYSTEM_NAME STREQUAL windows OR CORE_SYSTEM_NAME STREQUAL windowsstore)
+ list(APPEND SOURCES DXVAHD.cpp)
+ list(APPEND HEADERS DXVAHD.h)
+endif()
+
+if(VAAPI_FOUND)
+ if(OPENGL_FOUND)
+ list(APPEND SOURCES RendererVAAPIGL.cpp)
+ list(APPEND HEADERS RendererVAAPIGL.h)
+ endif()
+ if(OPENGLES_FOUND)
+ list(APPEND SOURCES RendererVAAPIGLES.cpp)
+ list(APPEND HEADERS RendererVAAPIGLES.h)
+ endif()
+ if(EGL_FOUND)
+ list(APPEND SOURCES VaapiEGL.cpp)
+ list(APPEND HEADERS VaapiEGL.h)
+ endif()
+endif()
+
+if(VDPAU_FOUND)
+ list(APPEND SOURCES RendererVDPAU.cpp
+ VdpauGL.cpp)
+ list(APPEND HEADERS RendererVDPAU.h
+ VdpauGL.h)
+endif()
+
+if(CORE_SYSTEM_NAME STREQUAL osx)
+ if(OPENGL_FOUND)
+ list(APPEND SOURCES RendererVTBGL.cpp)
+ list(APPEND HEADERS RendererVTBGL.h)
+ endif()
+endif()
+
+if(CORE_SYSTEM_NAME STREQUAL darwin_embedded)
+ if(OPENGLES_FOUND)
+ list(APPEND SOURCES RendererVTBGLES.cpp)
+ list(APPEND HEADERS RendererVTBGLES.h)
+ endif()
+endif()
+
+if(CORE_SYSTEM_NAME STREQUAL android)
+ list(APPEND SOURCES RendererMediaCodec.cpp
+ RendererMediaCodecSurface.cpp)
+ list(APPEND HEADERS RendererMediaCodec.h
+ RendererMediaCodecSurface.h)
+endif()
+
+if("gbm" IN_LIST CORE_PLATFORM_NAME_LC OR "wayland" IN_LIST CORE_PLATFORM_NAME_LC)
+ if("gbm" IN_LIST CORE_PLATFORM_NAME_LC)
+ list(APPEND SOURCES RendererDRMPRIME.cpp
+ VideoLayerBridgeDRMPRIME.cpp)
+ list(APPEND HEADERS RendererDRMPRIME.h
+ VideoLayerBridgeDRMPRIME.h)
+ endif()
+
+ if(OPENGLES_FOUND)
+ list(APPEND SOURCES RendererDRMPRIMEGLES.cpp
+ DRMPRIMEEGL.cpp)
+ list(APPEND HEADERS RendererDRMPRIMEGLES.h
+ DRMPRIMEEGL.h)
+ endif()
+endif()
+
+# we might want to build on linux systems
+# with ENABLE_VDPAU=OFF and ENABLE_VAAPI=OFF
+if(SOURCES)
+ core_add_library(videorenderers_hwdec)
+endif()
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DRMPRIMEEGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DRMPRIMEEGL.cpp
new file mode 100644
index 0000000..ee540c9
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DRMPRIMEEGL.cpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2007-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 "DRMPRIMEEGL.h"
+
+#include "utils/log.h"
+
+using namespace DRMPRIME;
+
+namespace
+{
+
+int GetEGLColorSpace(const VideoPicture& picture)
+{
+ switch (picture.color_space)
+ {
+ case AVCOL_SPC_BT2020_CL:
+ case AVCOL_SPC_BT2020_NCL:
+ return EGL_ITU_REC2020_EXT;
+ case AVCOL_SPC_SMPTE170M:
+ case AVCOL_SPC_BT470BG:
+ case AVCOL_SPC_FCC:
+ return EGL_ITU_REC601_EXT;
+ case AVCOL_SPC_BT709:
+ return EGL_ITU_REC709_EXT;
+ case AVCOL_SPC_RESERVED:
+ case AVCOL_SPC_UNSPECIFIED:
+ default:
+ if (picture.iWidth > 1024 || picture.iHeight >= 600)
+ return EGL_ITU_REC709_EXT;
+ else
+ return EGL_ITU_REC601_EXT;
+ }
+}
+
+int GetEGLColorRange(const VideoPicture& picture)
+{
+ if (picture.color_range)
+ return EGL_YUV_FULL_RANGE_EXT;
+
+ return EGL_YUV_NARROW_RANGE_EXT;
+}
+
+} // namespace
+
+CDRMPRIMETexture::~CDRMPRIMETexture()
+{
+ glDeleteTextures(1, &m_texture);
+}
+
+void CDRMPRIMETexture::Init(EGLDisplay eglDisplay)
+{
+ m_eglImage.reset(new CEGLImage(eglDisplay));
+}
+
+bool CDRMPRIMETexture::Map(CVideoBufferDRMPRIME* buffer)
+{
+ if (m_primebuffer)
+ return true;
+
+ if (!buffer->AcquireDescriptor())
+ {
+ CLog::Log(LOGERROR, "CDRMPRIMETexture::{} - failed to acquire descriptor", __FUNCTION__);
+ return false;
+ }
+
+ m_texWidth = buffer->GetWidth();
+ m_texHeight = buffer->GetHeight();
+
+ AVDRMFrameDescriptor* descriptor = buffer->GetDescriptor();
+ if (descriptor && descriptor->nb_layers)
+ {
+ AVDRMLayerDescriptor* layer = &descriptor->layers[0];
+
+ std::array<CEGLImage::EglPlane, CEGLImage::MAX_NUM_PLANES> planes;
+
+ for (int i = 0; i < layer->nb_planes; i++)
+ {
+ planes[i].fd = descriptor->objects[layer->planes[i].object_index].fd;
+ planes[i].modifier = descriptor->objects[layer->planes[i].object_index].format_modifier;
+ planes[i].offset = layer->planes[i].offset;
+ planes[i].pitch = layer->planes[i].pitch;
+ }
+
+ CEGLImage::EglAttrs attribs;
+
+ attribs.width = m_texWidth;
+ attribs.height = m_texHeight;
+ attribs.format = layer->format;
+ attribs.colorSpace = GetEGLColorSpace(buffer->GetPicture());
+ attribs.colorRange = GetEGLColorRange(buffer->GetPicture());
+ attribs.planes = planes;
+
+ if (!m_eglImage->CreateImage(attribs))
+ {
+ buffer->ReleaseDescriptor();
+ return false;
+ }
+
+ if (!glIsTexture(m_texture))
+ glGenTextures(1, &m_texture);
+ glBindTexture(m_textureTarget, m_texture);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ m_eglImage->UploadImage(m_textureTarget);
+ glBindTexture(m_textureTarget, 0);
+ }
+
+ m_primebuffer = buffer;
+ m_primebuffer->Acquire();
+
+ return true;
+}
+
+void CDRMPRIMETexture::Unmap()
+{
+ if (!m_primebuffer)
+ return;
+
+ m_eglImage->DestroyImage();
+
+ m_primebuffer->ReleaseDescriptor();
+
+ m_primebuffer->Release();
+ m_primebuffer = nullptr;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DRMPRIMEEGL.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DRMPRIMEEGL.h
new file mode 100644
index 0000000..e1e1310
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DRMPRIMEEGL.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2007-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/VideoPlayer/Buffers/VideoBufferDRMPRIME.h"
+#include "utils/EGLImage.h"
+#include "utils/Geometry.h"
+
+#include "system_gl.h"
+
+class CDRMPRIMETexture
+{
+public:
+ CDRMPRIMETexture() = default;
+ ~CDRMPRIMETexture();
+
+ bool Map(CVideoBufferDRMPRIME* buffer);
+ void Unmap();
+ void Init(EGLDisplay eglDisplay);
+
+ GLuint GetTexture() { return m_texture; }
+ CSizeInt GetTextureSize() { return {m_texWidth, m_texHeight}; }
+
+protected:
+ CVideoBufferDRMPRIME* m_primebuffer{nullptr};
+ std::unique_ptr<CEGLImage> m_eglImage;
+
+ const GLenum m_textureTarget{GL_TEXTURE_EXTERNAL_OES};
+ GLuint m_texture{0};
+ int m_texWidth{0};
+ int m_texHeight{0};
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DXVAHD.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DXVAHD.cpp
new file mode 100644
index 0000000..486c1f4
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DXVAHD.cpp
@@ -0,0 +1,631 @@
+/*
+ * 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.
+ */
+
+// setting that here because otherwise SampleFormat is defined to AVSampleFormat
+// which we don't use here
+#define FF_API_OLD_SAMPLE_FMT 0
+#define DEFAULT_STREAM_INDEX (0)
+
+#include "DXVAHD.h"
+
+#include "VideoRenderers/RenderFlags.h"
+#include "VideoRenderers/RenderManager.h"
+#include "VideoRenderers/windows/RendererBase.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#include <Windows.h>
+#include <d3d11_4.h>
+#include <dxgi1_5.h>
+
+using namespace DXVA;
+using namespace Microsoft::WRL;
+
+#define LOGIFERROR(a) \
+ do \
+ { \
+ HRESULT res = a; \
+ if (FAILED(res)) \
+ { \
+ CLog::LogF(LOGERROR, "failed executing " #a " at line {} with error {:x}", __LINE__, res); \
+ } \
+ } while (0);
+
+CProcessorHD::CProcessorHD()
+{
+ DX::Windowing()->Register(this);
+}
+
+CProcessorHD::~CProcessorHD()
+{
+ DX::Windowing()->Unregister(this);
+ UnInit();
+}
+
+void CProcessorHD::UnInit()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ Close();
+}
+
+void CProcessorHD::Close()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_pEnumerator = nullptr;
+ m_pVideoProcessor = nullptr;
+ m_pVideoContext = nullptr;
+ m_pVideoDevice = nullptr;
+}
+
+bool CProcessorHD::PreInit() const
+{
+ ComPtr<ID3D11VideoDevice> pVideoDevice;
+ ComPtr<ID3D11VideoProcessorEnumerator> pEnumerator;
+ ComPtr<ID3D11Device> pD3DDevice = DX::DeviceResources::Get()->GetD3DDevice();
+
+ if (FAILED(pD3DDevice.As(&pVideoDevice)))
+ {
+ CLog::LogF(LOGWARNING, "failed to get video device.");
+ return false;
+ }
+
+ D3D11_VIDEO_PROCESSOR_CONTENT_DESC desc1 = {};
+ desc1.InputFrameFormat = D3D11_VIDEO_FRAME_FORMAT_INTERLACED_TOP_FIELD_FIRST;
+ desc1.InputWidth = 640;
+ desc1.InputHeight = 480;
+ desc1.OutputWidth = 640;
+ desc1.OutputHeight = 480;
+ desc1.Usage = D3D11_VIDEO_USAGE_PLAYBACK_NORMAL;
+
+ // try to create video enum
+ if (FAILED(pVideoDevice->CreateVideoProcessorEnumerator(&desc1, &pEnumerator)))
+ {
+ CLog::LogF(LOGWARNING, "failed to create Video Enumerator.");
+ return false;
+ }
+ return true;
+}
+
+bool CProcessorHD::InitProcessor()
+{
+ m_pVideoDevice = nullptr;
+ m_pVideoContext = nullptr;
+ m_pEnumerator = nullptr;
+
+ ComPtr<ID3D11DeviceContext1> pD3DDeviceContext = DX::DeviceResources::Get()->GetImmediateContext();
+ ComPtr<ID3D11Device> pD3DDevice = DX::DeviceResources::Get()->GetD3DDevice();
+
+ if (FAILED(pD3DDeviceContext.As(&m_pVideoContext)))
+ {
+ CLog::LogF(LOGWARNING, "video context initialization is failed.");
+ return false;
+ }
+ if (FAILED(pD3DDevice.As(&m_pVideoDevice)))
+ {
+ CLog::LogF(LOGWARNING, "video device initialization is failed.");
+ return false;
+ }
+
+ CLog::LogF(LOGDEBUG, "initing video enumerator with params: {}x{}.", m_width, m_height);
+
+ D3D11_VIDEO_PROCESSOR_CONTENT_DESC contentDesc = {};
+ contentDesc.InputFrameFormat = D3D11_VIDEO_FRAME_FORMAT_INTERLACED_TOP_FIELD_FIRST;
+ contentDesc.InputWidth = m_width;
+ contentDesc.InputHeight = m_height;
+ contentDesc.OutputWidth = m_width;
+ contentDesc.OutputHeight = m_height;
+ contentDesc.Usage = D3D11_VIDEO_USAGE_PLAYBACK_NORMAL;
+
+ if (FAILED(m_pVideoDevice->CreateVideoProcessorEnumerator(&contentDesc, m_pEnumerator.ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGWARNING, "failed to init video enumerator with params: {}x{}.", m_width,
+ m_height);
+ return false;
+ }
+
+ if (FAILED(m_pEnumerator->GetVideoProcessorCaps(&m_vcaps)))
+ {
+ CLog::LogF(LOGWARNING, "failed to get processor caps.");
+ return false;
+ }
+
+ CLog::LogF(LOGDEBUG, "video processor has {} rate conversion.", m_vcaps.RateConversionCapsCount);
+ CLog::LogF(LOGDEBUG, "video processor has {:#x} feature caps.", m_vcaps.FeatureCaps);
+ CLog::LogF(LOGDEBUG, "video processor has {:#x} device caps.", m_vcaps.DeviceCaps);
+ CLog::LogF(LOGDEBUG, "video processor has {:#x} input format caps.", m_vcaps.InputFormatCaps);
+ CLog::LogF(LOGDEBUG, "video processor has {} max input streams.", m_vcaps.MaxInputStreams);
+ CLog::LogF(LOGDEBUG, "video processor has {} max stream states.", m_vcaps.MaxStreamStates);
+ if (m_vcaps.FeatureCaps & D3D11_VIDEO_PROCESSOR_FEATURE_CAPS_METADATA_HDR10)
+ CLog::LogF(LOGDEBUG, "video processor supports HDR10.");
+
+ if (0 != (m_vcaps.FeatureCaps & D3D11_VIDEO_PROCESSOR_FEATURE_CAPS_LEGACY))
+ CLog::LogF(LOGWARNING, "the video driver does not support full video processing capabilities.");
+
+ m_max_back_refs = 0;
+ m_max_fwd_refs = 0;
+ m_procIndex = 0;
+
+ unsigned maxProcCaps = 0;
+ // try to find best processor
+ for (unsigned int i = 0; i < m_vcaps.RateConversionCapsCount; i++)
+ {
+ D3D11_VIDEO_PROCESSOR_RATE_CONVERSION_CAPS convCaps;
+ LOGIFERROR(m_pEnumerator->GetVideoProcessorRateConversionCaps(i, &convCaps))
+
+ // check only deinterlace caps
+ if ((convCaps.ProcessorCaps & 15) > maxProcCaps)
+ {
+ m_procIndex = i;
+ maxProcCaps = convCaps.ProcessorCaps & 15;
+ }
+ }
+
+ CLog::LogF(LOGDEBUG, "selected video processor index: {}.", m_procIndex);
+
+ LOGIFERROR(m_pEnumerator->GetVideoProcessorRateConversionCaps(m_procIndex, &m_rateCaps))
+ m_max_fwd_refs = std::min(m_rateCaps.FutureFrames, 2u);
+ m_max_back_refs = std::min(m_rateCaps.PastFrames, 4u);
+
+ CLog::LogF(LOGINFO, "supported deinterlace methods: blend:{}, bob:{}, adaptive:{}, mocomp:{}.",
+ (m_rateCaps.ProcessorCaps & 0x1) != 0 ? "yes" : "no", // BLEND
+ (m_rateCaps.ProcessorCaps & 0x2) != 0 ? "yes" : "no", // BOB
+ (m_rateCaps.ProcessorCaps & 0x4) != 0 ? "yes" : "no", // ADAPTIVE
+ (m_rateCaps.ProcessorCaps & 0x8) != 0 ? "yes" : "no" // MOTION_COMPENSATION
+ );
+
+ CLog::LogF(LOGDEBUG, "selected video processor allows {} future frames and {} past frames.",
+ m_rateCaps.FutureFrames, m_rateCaps.PastFrames);
+
+ //m_size = m_max_back_refs + 1 + m_max_fwd_refs; // refs + 1 display
+
+ // Get the image filtering capabilities.
+ for (size_t i = 0; i < NUM_FILTERS; i++)
+ {
+ if (m_vcaps.FilterCaps & (1 << i))
+ {
+ m_Filters[i].Range = {};
+ m_Filters[i].bSupported = SUCCEEDED(m_pEnumerator->GetVideoProcessorFilterRange(PROCAMP_FILTERS[i], &m_Filters[i].Range));
+
+ if (m_Filters[i].bSupported)
+ {
+ CLog::LogF(LOGDEBUG, "filter {} has following params - max: {}, min: {}, default: {}",
+ PROCAMP_FILTERS[i], m_Filters[i].Range.Maximum, m_Filters[i].Range.Minimum,
+ m_Filters[i].Range.Default);
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGDEBUG, "filter {} not supported by processor.", PROCAMP_FILTERS[i]);
+ m_Filters[i].bSupported = false;
+ }
+ }
+
+ ComPtr<ID3D11VideoProcessorEnumerator1> pEnumerator1;
+
+ if (SUCCEEDED(m_pEnumerator.As(&pEnumerator1)))
+ {
+ DXGI_FORMAT format = DX::Windowing()->GetBackBuffer().GetFormat();
+ BOOL supported = 0;
+ HRESULT hr;
+
+ // Check if HLG color space conversion is supported by driver
+ hr = pEnumerator1->CheckVideoProcessorFormatConversion(
+ DXGI_FORMAT_P010, DXGI_COLOR_SPACE_YCBCR_STUDIO_GHLG_TOPLEFT_P2020, format,
+ DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709, &supported);
+ m_bSupportHLG = SUCCEEDED(hr) && !!supported;
+
+ // Check if HDR10 RGB limited range output is supported by driver
+ hr = pEnumerator1->CheckVideoProcessorFormatConversion(
+ DXGI_FORMAT_P010, DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020, format,
+ DXGI_COLOR_SPACE_RGB_STUDIO_G2084_NONE_P2020, &supported);
+ m_bSupportHDR10Limited = SUCCEEDED(hr) && !!supported;
+ }
+
+ CLog::LogF(LOGDEBUG, "HLG color space conversion is{}supported.", m_bSupportHLG ? " " : " NOT ");
+ CLog::LogF(LOGDEBUG, "HDR10 RGB limited range output is{}supported.",
+ m_bSupportHDR10Limited ? " " : " NOT ");
+
+ return true;
+}
+
+bool CProcessorHD::IsFormatSupported(DXGI_FORMAT format, D3D11_VIDEO_PROCESSOR_FORMAT_SUPPORT support) const
+{
+ UINT uiFlags;
+ if (S_OK == m_pEnumerator->CheckVideoProcessorFormat(format, &uiFlags))
+ {
+ if (uiFlags & support)
+ return true;
+ }
+
+ CLog::LogF(LOGERROR, "unsupported format {} for {}.", format, support);
+ return false;
+}
+
+bool CProcessorHD::CheckFormats() const
+{
+ // check default output format (as render target)
+ return IsFormatSupported(DX::Windowing()->GetBackBuffer().GetFormat(), D3D11_VIDEO_PROCESSOR_FORMAT_SUPPORT_OUTPUT);
+}
+
+bool CProcessorHD::Open(UINT width, UINT height)
+{
+ Close();
+
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ m_width = width;
+ m_height = height;
+
+ if (!InitProcessor())
+ return false;
+
+ if (!CheckFormats())
+ return false;
+
+ return OpenProcessor();
+}
+
+bool CProcessorHD::ReInit()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ Close();
+
+ if (!InitProcessor())
+ return false;
+
+ if (!CheckFormats())
+ return false;
+
+ return true;
+}
+
+bool CProcessorHD::OpenProcessor()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ // restore the device if it was lost
+ if (!m_pEnumerator && !ReInit())
+ return false;
+
+ CLog::LogF(LOGDEBUG, "creating processor.");
+
+ // create processor
+ HRESULT hr = m_pVideoDevice->CreateVideoProcessor(m_pEnumerator.Get(), m_procIndex, m_pVideoProcessor.ReleaseAndGetAddressOf());
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGDEBUG, "failed creating video processor with error {:x}.", hr);
+ return false;
+ }
+
+ // Output background color (black)
+ D3D11_VIDEO_COLOR color;
+ color.YCbCr = { 0.0625f, 0.5f, 0.5f, 1.0f }; // black color
+ m_pVideoContext->VideoProcessorSetOutputBackgroundColor(m_pVideoProcessor.Get(), TRUE, &color);
+
+ return true;
+}
+
+void CProcessorHD::ApplyFilter(D3D11_VIDEO_PROCESSOR_FILTER filter, int value, int min, int max, int def) const
+{
+ if (filter >= static_cast<D3D11_VIDEO_PROCESSOR_FILTER>(NUM_FILTERS))
+ return;
+
+ // Unsupported filter. Ignore.
+ if (!m_Filters[filter].bSupported)
+ return;
+
+ D3D11_VIDEO_PROCESSOR_FILTER_RANGE range = m_Filters[filter].Range;
+ int val;
+
+ if(value > def)
+ val = range.Default + (range.Maximum - range.Default) * (value - def) / (max - def);
+ else if(value < def)
+ val = range.Default + (range.Minimum - range.Default) * (value - def) / (min - def);
+ else
+ val = range.Default;
+
+ m_pVideoContext->VideoProcessorSetStreamFilter(m_pVideoProcessor.Get(), DEFAULT_STREAM_INDEX, filter, val != range.Default, val);
+}
+
+ID3D11VideoProcessorInputView* CProcessorHD::GetInputView(CRenderBuffer* view) const
+{
+ ComPtr<ID3D11VideoProcessorInputView> inputView;
+ D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC vpivd = {0, D3D11_VPIV_DIMENSION_TEXTURE2D, {0, 0}};
+
+ ComPtr<ID3D11Resource> resource;
+ unsigned arrayIdx = 0;
+ HRESULT hr = view->GetResource(resource.GetAddressOf(), &arrayIdx);
+ if (SUCCEEDED(hr))
+ {
+ vpivd.Texture2D.ArraySlice = arrayIdx;
+ hr = m_pVideoDevice->CreateVideoProcessorInputView(resource.Get(), m_pEnumerator.Get(), &vpivd, inputView.GetAddressOf());
+ }
+
+ if (FAILED(hr) || hr == S_FALSE)
+ CLog::LogF(LOGERROR, "cannot create processor input view.");
+
+ return inputView.Detach();
+}
+
+DXGI_COLOR_SPACE_TYPE CProcessorHD::GetDXGIColorSpaceSource(CRenderBuffer* view, bool supportHDR, bool supportHLG)
+{
+ // RGB
+ if (view->color_space == AVCOL_SPC_RGB)
+ {
+ if (!view->full_range)
+ {
+ if (view->primaries == AVCOL_PRI_BT2020)
+ {
+ if (view->color_transfer == AVCOL_TRC_SMPTEST2084 && supportHDR)
+ return DXGI_COLOR_SPACE_RGB_STUDIO_G2084_NONE_P2020;
+
+ return DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P2020;
+ }
+ return DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P709;
+ }
+
+ if (view->primaries == AVCOL_PRI_BT2020)
+ {
+ if (view->color_transfer == AVCOL_TRC_SMPTEST2084)
+ return DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020;
+
+ return DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020;
+ }
+ if (view->color_transfer == AVCOL_TRC_LINEAR ||
+ view->color_transfer == AVCOL_TRC_LOG)
+ return DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709;
+
+ return DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
+ }
+ // UHDTV
+ if (view->primaries == AVCOL_PRI_BT2020)
+ {
+ // Windows 10 doesn't support HLG passthrough, always is used PQ for HDR passthrough
+ if ((view->color_transfer == AVCOL_TRC_SMPTEST2084 ||
+ view->color_transfer == AVCOL_TRC_ARIB_STD_B67) && supportHDR) // is HDR display ON
+ return DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020;
+
+ // HLG transfer can be used for HLG source in SDR display if is supported
+ if (view->color_transfer == AVCOL_TRC_ARIB_STD_B67 && supportHLG) // driver supports HLG
+ return DXGI_COLOR_SPACE_YCBCR_STUDIO_GHLG_TOPLEFT_P2020;
+
+ if (view->full_range)
+ return DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P2020;
+
+ return DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020;
+ }
+ // SDTV
+ if (view->primaries == AVCOL_PRI_BT470BG ||
+ view->primaries == AVCOL_PRI_SMPTE170M)
+ {
+ if (view->full_range)
+ return DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P601;
+
+ return DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P601;
+ }
+ // HDTV
+ if (view->full_range)
+ {
+ if (view->color_transfer == AVCOL_TRC_SMPTE170M)
+ return DXGI_COLOR_SPACE_YCBCR_FULL_G22_NONE_P709_X601;
+
+ return DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709;
+ }
+
+ return DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709;
+}
+
+DXGI_COLOR_SPACE_TYPE CProcessorHD::GetDXGIColorSpaceTarget(CRenderBuffer* view, bool supportHDR)
+{
+ DXGI_COLOR_SPACE_TYPE color;
+
+ color = DX::Windowing()->UseLimitedColor() ? DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P709
+ : DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
+
+ if (!DX::Windowing()->IsHDROutput())
+ return color;
+
+ // HDR10 or HLG
+ if (view->primaries == AVCOL_PRI_BT2020 && (view->color_transfer == AVCOL_TRC_SMPTE2084 ||
+ view->color_transfer == AVCOL_TRC_ARIB_STD_B67))
+ {
+ if (supportHDR)
+ {
+ color = DX::Windowing()->UseLimitedColor() ? DXGI_COLOR_SPACE_RGB_STUDIO_G2084_NONE_P2020
+ : DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020;
+ }
+ else
+ {
+ color = DX::Windowing()->UseLimitedColor() ? DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P2020
+ : DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020;
+ }
+ }
+
+ return color;
+}
+
+bool CProcessorHD::Render(CRect src, CRect dst, ID3D11Resource* target, CRenderBuffer** views, DWORD flags, UINT frameIdx, UINT rotation, float contrast, float brightness)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ // restore processor if it was lost
+ if (!m_pVideoProcessor && !OpenProcessor())
+ return false;
+
+ if (!views[2])
+ return false;
+
+ RECT sourceRECT = {static_cast<LONG>(src.x1), static_cast<LONG>(src.y1),
+ static_cast<LONG>(src.x2), static_cast<LONG>(src.y2)};
+ RECT dstRECT = {static_cast<LONG>(dst.x1), static_cast<LONG>(dst.y1), static_cast<LONG>(dst.x2),
+ static_cast<LONG>(dst.y2)};
+
+ D3D11_VIDEO_FRAME_FORMAT dxvaFrameFormat = D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE;
+
+ unsigned int providedPast = 0;
+ for (int i = 3; i < 8; i++)
+ {
+ if (views[i])
+ providedPast++;
+ }
+ unsigned int providedFuture = 0;
+ for (int i = 1; i >= 0; i--)
+ {
+ if (views[i])
+ providedFuture++;
+ }
+ const int futureFrames = std::min(providedFuture, m_rateCaps.FutureFrames);
+ const int pastFrames = std::min(providedPast, m_rateCaps.PastFrames);
+ std::vector<ID3D11VideoProcessorInputView*> pastViews(pastFrames, nullptr);
+ std::vector<ID3D11VideoProcessorInputView*> futureViews(futureFrames, nullptr);
+
+ D3D11_VIDEO_PROCESSOR_STREAM stream_data = {};
+ stream_data.Enable = TRUE;
+ stream_data.PastFrames = pastFrames;
+ stream_data.FutureFrames = futureFrames;
+ stream_data.ppPastSurfaces = pastViews.data();
+ stream_data.ppFutureSurfaces = futureViews.data();
+
+ std::vector<ComPtr<ID3D11VideoProcessorInputView>> all_views;
+ const int start = 2 - futureFrames;
+ const int end = 2 + pastFrames;
+ int count = 0;
+
+ for (int i = start; i <= end; i++)
+ {
+ if (!views[i])
+ continue;
+
+ ComPtr<ID3D11VideoProcessorInputView> view;
+ view.Attach(GetInputView(views[i]));
+
+ if (i > 2)
+ {
+ // frames order should be { ?, T-3, T-2, T-1 }
+ pastViews[2 + pastFrames - i] = view.Get();
+ }
+ else if (i == 2)
+ {
+ stream_data.pInputSurface = view.Get();
+ }
+ else if (i < 2)
+ {
+ // frames order should be { T+1, T+2, T+3, .. }
+ futureViews[1 - i] = view.Get();
+ }
+ if (view)
+ {
+ count++;
+ all_views.push_back(view);
+ }
+ }
+
+ if (count != pastFrames + futureFrames + 1)
+ {
+ CLog::LogF(LOGERROR, "incomplete views set.");
+ return false;
+ }
+
+ if (flags & RENDER_FLAG_FIELD0 && flags & RENDER_FLAG_TOP)
+ dxvaFrameFormat = D3D11_VIDEO_FRAME_FORMAT_INTERLACED_TOP_FIELD_FIRST;
+ else if (flags & RENDER_FLAG_FIELD1 && flags & RENDER_FLAG_BOT)
+ dxvaFrameFormat = D3D11_VIDEO_FRAME_FORMAT_INTERLACED_TOP_FIELD_FIRST;
+ if (flags & RENDER_FLAG_FIELD0 && flags & RENDER_FLAG_BOT)
+ dxvaFrameFormat = D3D11_VIDEO_FRAME_FORMAT_INTERLACED_BOTTOM_FIELD_FIRST;
+ if (flags & RENDER_FLAG_FIELD1 && flags & RENDER_FLAG_TOP)
+ dxvaFrameFormat = D3D11_VIDEO_FRAME_FORMAT_INTERLACED_BOTTOM_FIELD_FIRST;
+
+ bool frameProgressive = dxvaFrameFormat == D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE;
+
+ // Progressive or Interlaced video at normal rate.
+ stream_data.InputFrameOrField = frameIdx;
+ stream_data.OutputIndex = flags & RENDER_FLAG_FIELD1 && !frameProgressive ? 1 : 0;
+
+ // input format
+ m_pVideoContext->VideoProcessorSetStreamFrameFormat(m_pVideoProcessor.Get(), DEFAULT_STREAM_INDEX, dxvaFrameFormat);
+ // Source rect
+ m_pVideoContext->VideoProcessorSetStreamSourceRect(m_pVideoProcessor.Get(), DEFAULT_STREAM_INDEX, TRUE, &sourceRECT);
+ // Stream dest rect
+ m_pVideoContext->VideoProcessorSetStreamDestRect(m_pVideoProcessor.Get(), DEFAULT_STREAM_INDEX, TRUE, &dstRECT);
+ // Output rect
+ m_pVideoContext->VideoProcessorSetOutputTargetRect(m_pVideoProcessor.Get(), TRUE, &dstRECT);
+
+ ComPtr<ID3D11VideoContext1> videoCtx1;
+ if (SUCCEEDED(m_pVideoContext.As(&videoCtx1)))
+ {
+ bool supportHDR = DX::Windowing()->IsHDROutput() &&
+ (m_bSupportHDR10Limited || !DX::Windowing()->UseLimitedColor());
+
+ const DXGI_COLOR_SPACE_TYPE sourceColor =
+ GetDXGIColorSpaceSource(views[2], supportHDR, m_bSupportHLG);
+ const DXGI_COLOR_SPACE_TYPE targetColor = GetDXGIColorSpaceTarget(views[2], supportHDR);
+
+ videoCtx1->VideoProcessorSetStreamColorSpace1(m_pVideoProcessor.Get(), DEFAULT_STREAM_INDEX,
+ sourceColor);
+ videoCtx1->VideoProcessorSetOutputColorSpace1(m_pVideoProcessor.Get(), targetColor);
+ // makes target available for processing in shaders
+ videoCtx1->VideoProcessorSetOutputShaderUsage(m_pVideoProcessor.Get(), 1);
+ }
+ else
+ {
+ // input colorspace
+ bool isBT601 = views[2]->color_space == AVCOL_SPC_BT470BG || views[2]->color_space == AVCOL_SPC_SMPTE170M;
+ // clang-format off
+ D3D11_VIDEO_PROCESSOR_COLOR_SPACE colorSpace
+ {
+ 0u, // 0 - Playback, 1 - Processing
+ views[2]->full_range ? 0u : 1u, // 0 - Full (0-255), 1 - Limited (16-235) (RGB)
+ isBT601 ? 1u : 0u, // 0 - BT.601, 1 - BT.709
+ 0u, // 0 - Conventional YCbCr, 1 - xvYCC
+ views[2]->full_range ? 2u : 1u // 0 - driver defaults, 2 - Full range [0-255], 1 - Studio range [16-235] (YUV)
+ };
+ // clang-format on
+ m_pVideoContext->VideoProcessorSetStreamColorSpace(m_pVideoProcessor.Get(), DEFAULT_STREAM_INDEX, &colorSpace);
+ // Output color space
+ // don't apply any color range conversion, this will be fixed at later stage.
+ colorSpace.Usage = 0; // 0 - playback, 1 - video processing
+ colorSpace.RGB_Range = DX::Windowing()->UseLimitedColor() ? 1 : 0; // 0 - 0-255, 1 - 16-235
+ colorSpace.YCbCr_Matrix = 1; // 0 - BT.601, 1 = BT.709
+ colorSpace.YCbCr_xvYCC = 1; // 0 - Conventional YCbCr, 1 - xvYCC
+ colorSpace.Nominal_Range = 0; // 2 - 0-255, 1 = 16-235, 0 - undefined
+ m_pVideoContext->VideoProcessorSetOutputColorSpace(m_pVideoProcessor.Get(), &colorSpace);
+ }
+
+ // brightness
+ ApplyFilter(D3D11_VIDEO_PROCESSOR_FILTER_BRIGHTNESS, static_cast<int>(brightness), 0, 100, 50);
+ // contrast
+ ApplyFilter(D3D11_VIDEO_PROCESSOR_FILTER_CONTRAST, static_cast<int>(contrast), 0, 100, 50);
+ // unused filters
+ ApplyFilter(D3D11_VIDEO_PROCESSOR_FILTER_HUE, 50, 0, 100, 50);
+ ApplyFilter(D3D11_VIDEO_PROCESSOR_FILTER_SATURATION, 50, 0, 100, 50);
+ // Rotation
+ m_pVideoContext->VideoProcessorSetStreamRotation(m_pVideoProcessor.Get(), DEFAULT_STREAM_INDEX, rotation != 0,
+ static_cast<D3D11_VIDEO_PROCESSOR_ROTATION>(rotation / 90));
+
+ // create output view for surface.
+ D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC OutputViewDesc = { D3D11_VPOV_DIMENSION_TEXTURE2D, { 0 }};
+ ComPtr<ID3D11VideoProcessorOutputView> pOutputView;
+ HRESULT hr = m_pVideoDevice->CreateVideoProcessorOutputView(target, m_pEnumerator.Get(), &OutputViewDesc, &pOutputView);
+ if (S_OK != hr)
+ CLog::LogF(FAILED(hr) ? LOGERROR : LOGWARNING,
+ "video device returns result '{:x}' while creating processor output view.", hr);
+
+ if (SUCCEEDED(hr))
+ {
+ hr = m_pVideoContext->VideoProcessorBlt(m_pVideoProcessor.Get(), pOutputView.Get(), frameIdx, 1, &stream_data);
+ if (S_OK != hr)
+ {
+ CLog::LogF(FAILED(hr) ? LOGERROR : LOGWARNING,
+ "video device returns result '{:x}' while VideoProcessorBlt execution.", hr);
+ }
+ }
+
+ return !FAILED(hr);
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DXVAHD.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DXVAHD.h
new file mode 100644
index 0000000..8a30fb6
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/DXVAHD.h
@@ -0,0 +1,92 @@
+/*
+ * 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 "DVDCodecs/Video/DVDVideoCodecFFmpeg.h"
+#include "DVDCodecs/Video/DXVA.h"
+#include "guilib/D3DResource.h"
+#include "utils/Geometry.h"
+
+#include <mutex>
+
+#include <wrl/client.h>
+
+class CRenderBuffer;
+
+namespace DXVA {
+
+// ProcAmp filters d3d11 filters
+const D3D11_VIDEO_PROCESSOR_FILTER PROCAMP_FILTERS[] =
+{
+ D3D11_VIDEO_PROCESSOR_FILTER_BRIGHTNESS,
+ D3D11_VIDEO_PROCESSOR_FILTER_CONTRAST,
+ D3D11_VIDEO_PROCESSOR_FILTER_HUE,
+ D3D11_VIDEO_PROCESSOR_FILTER_SATURATION
+};
+
+const size_t NUM_FILTERS = ARRAYSIZE(PROCAMP_FILTERS);
+
+class CProcessorHD : public ID3DResource
+{
+public:
+ explicit CProcessorHD();
+ ~CProcessorHD();
+
+ bool PreInit() const;
+ void UnInit();
+ bool Open(UINT width, UINT height);
+ void Close();
+ bool Render(CRect src, CRect dst, ID3D11Resource* target, CRenderBuffer **views, DWORD flags, UINT frameIdx, UINT rotation, float contrast, float brightness);
+ uint8_t PastRefs() const { return m_max_back_refs; }
+ bool IsFormatSupported(DXGI_FORMAT format, D3D11_VIDEO_PROCESSOR_FORMAT_SUPPORT support) const;
+
+ // ID3DResource overrides
+ void OnCreateDevice() override {}
+ void OnDestroyDevice(bool) override
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ UnInit();
+ }
+
+ static DXGI_COLOR_SPACE_TYPE GetDXGIColorSpaceSource(CRenderBuffer* view, bool supportHDR, bool supportHLG);
+ static DXGI_COLOR_SPACE_TYPE GetDXGIColorSpaceTarget(CRenderBuffer* view, bool supportHDR);
+
+protected:
+ bool ReInit();
+ bool InitProcessor();
+ bool CheckFormats() const;
+ bool OpenProcessor();
+ void ApplyFilter(D3D11_VIDEO_PROCESSOR_FILTER filter, int value, int min, int max, int def) const;
+ ID3D11VideoProcessorInputView* GetInputView(CRenderBuffer* view) const;
+
+ CCriticalSection m_section;
+
+ uint32_t m_width = 0;
+ uint32_t m_height = 0;
+ uint8_t m_max_back_refs = 0;
+ uint8_t m_max_fwd_refs = 0;
+ uint32_t m_procIndex = 0;
+ D3D11_VIDEO_PROCESSOR_CAPS m_vcaps = {};
+ D3D11_VIDEO_PROCESSOR_RATE_CONVERSION_CAPS m_rateCaps = {};
+ bool m_bSupportHLG = false;
+ bool m_bSupportHDR10Limited = false;
+
+ struct ProcAmpInfo
+ {
+ bool bSupported;
+ D3D11_VIDEO_PROCESSOR_FILTER_RANGE Range;
+ };
+ ProcAmpInfo m_Filters[NUM_FILTERS]{};
+ Microsoft::WRL::ComPtr<ID3D11VideoDevice> m_pVideoDevice;
+ Microsoft::WRL::ComPtr<ID3D11VideoContext> m_pVideoContext;
+ Microsoft::WRL::ComPtr<ID3D11VideoProcessorEnumerator> m_pEnumerator;
+ Microsoft::WRL::ComPtr<ID3D11VideoProcessor> m_pVideoProcessor;
+};
+
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIME.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIME.cpp
new file mode 100644
index 0000000..a07b4f7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIME.cpp
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2017-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 "RendererDRMPRIME.h"
+
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/VideoLayerBridgeDRMPRIME.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderCapture.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFlags.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/gbm/WinSystemGbm.h"
+#include "windowing/gbm/drm/DRMAtomic.h"
+
+using namespace KODI::WINDOWING::GBM;
+
+const std::string SETTING_VIDEOPLAYER_USEPRIMERENDERER = "videoplayer.useprimerenderer";
+
+CRendererDRMPRIME::~CRendererDRMPRIME()
+{
+ Flush(false);
+}
+
+CBaseRenderer* CRendererDRMPRIME::Create(CVideoBuffer* buffer)
+{
+ if (buffer && CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ SETTING_VIDEOPLAYER_USEPRIMERENDERER) == 0)
+ {
+ auto buf = dynamic_cast<CVideoBufferDRMPRIME*>(buffer);
+ if (!buf)
+ return nullptr;
+
+ auto winSystem = static_cast<CWinSystemGbm*>(CServiceBroker::GetWinSystem());
+ if (!winSystem)
+ return nullptr;
+
+ auto drm = std::static_pointer_cast<CDRMAtomic>(winSystem->GetDrm());
+ if (!drm)
+ return nullptr;
+
+ if (!buf->AcquireDescriptor())
+ return nullptr;
+
+ AVDRMFrameDescriptor* desc = buf->GetDescriptor();
+ if (!desc)
+ {
+ buf->ReleaseDescriptor();
+ return nullptr;
+ }
+
+ AVDRMLayerDescriptor* layer = &desc->layers[0];
+ uint32_t format = layer->format;
+ uint64_t modifier = desc->objects[0].format_modifier;
+
+ buf->ReleaseDescriptor();
+
+ auto gui = drm->GetGuiPlane();
+ if (!gui)
+ return nullptr;
+
+ if (!gui->SupportsFormat(CDRMUtils::FourCCWithAlpha(gui->GetFormat())))
+ return nullptr;
+
+ auto plane = drm->GetVideoPlane();
+ if (!plane)
+ return nullptr;
+
+ if (!plane->SupportsFormatAndModifier(format, modifier))
+ return nullptr;
+
+ return new CRendererDRMPRIME();
+ }
+
+ return nullptr;
+}
+
+void CRendererDRMPRIME::Register()
+{
+ CWinSystemGbm* winSystem = dynamic_cast<CWinSystemGbm*>(CServiceBroker::GetWinSystem());
+ if (winSystem && winSystem->GetDrm()->GetVideoPlane() &&
+ std::dynamic_pointer_cast<CDRMAtomic>(winSystem->GetDrm()))
+ {
+ CServiceBroker::GetSettingsComponent()
+ ->GetSettings()
+ ->GetSetting(SETTING_VIDEOPLAYER_USEPRIMERENDERER)
+ ->SetVisible(true);
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("drm_prime", CRendererDRMPRIME::Create);
+ return;
+ }
+}
+
+bool CRendererDRMPRIME::Configure(const VideoPicture& picture, float fps, unsigned int orientation)
+{
+ m_format = picture.videoBuffer->GetFormat();
+ m_sourceWidth = picture.iWidth;
+ m_sourceHeight = picture.iHeight;
+ m_renderOrientation = orientation;
+
+ m_iFlags = GetFlagsChromaPosition(picture.chroma_position) |
+ GetFlagsColorMatrix(picture.color_space, picture.iWidth, picture.iHeight) |
+ GetFlagsColorPrimaries(picture.color_primaries) |
+ GetFlagsStereoMode(picture.stereoMode);
+
+ // Calculate the input frame aspect ratio.
+ CalculateFrameAspectRatio(picture.iDisplayWidth, picture.iDisplayHeight);
+ SetViewMode(m_videoSettings.m_ViewMode);
+ ManageRenderArea();
+
+ Flush(false);
+
+ m_bConfigured = true;
+ return true;
+}
+
+void CRendererDRMPRIME::ManageRenderArea()
+{
+ CBaseRenderer::ManageRenderArea();
+
+ RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ if (info.iScreenWidth != info.iWidth)
+ {
+ CalcDestRect(0, 0, info.iScreenWidth, info.iScreenHeight,
+ GetAspectRatio() * CDisplaySettings::GetInstance().GetPixelRatio(),
+ CDisplaySettings::GetInstance().GetZoomAmount(),
+ CDisplaySettings::GetInstance().GetVerticalShift(), m_planeDestRect);
+ }
+ else
+ {
+ m_planeDestRect = m_destRect;
+ }
+}
+
+void CRendererDRMPRIME::AddVideoPicture(const VideoPicture& picture, int index)
+{
+ BUFFER& buf = m_buffers[index];
+ if (buf.videoBuffer)
+ {
+ CLog::LogF(LOGERROR, "unreleased video buffer");
+ buf.videoBuffer->Release();
+ }
+ buf.videoBuffer = picture.videoBuffer;
+ buf.videoBuffer->Acquire();
+}
+
+bool CRendererDRMPRIME::Flush(bool saveBuffers)
+{
+ if (!saveBuffers)
+ for (int i = 0; i < NUM_BUFFERS; i++)
+ ReleaseBuffer(i);
+
+ m_iLastRenderBuffer = -1;
+ return saveBuffers;
+}
+
+void CRendererDRMPRIME::ReleaseBuffer(int index)
+{
+ BUFFER& buf = m_buffers[index];
+ if (buf.videoBuffer)
+ {
+ buf.videoBuffer->Release();
+ buf.videoBuffer = nullptr;
+ }
+}
+
+bool CRendererDRMPRIME::NeedBuffer(int index)
+{
+ if (m_iLastRenderBuffer == index)
+ return true;
+
+ return false;
+}
+
+CRenderInfo CRendererDRMPRIME::GetRenderInfo()
+{
+ CRenderInfo info;
+ info.max_buffer_size = NUM_BUFFERS;
+ return info;
+}
+
+void CRendererDRMPRIME::Update()
+{
+ if (!m_bConfigured)
+ return;
+
+ ManageRenderArea();
+}
+
+void CRendererDRMPRIME::RenderUpdate(
+ int index, int index2, bool clear, unsigned int flags, unsigned int alpha)
+{
+ if (m_iLastRenderBuffer == index && m_videoLayerBridge)
+ {
+ m_videoLayerBridge->UpdateVideoPlane();
+ return;
+ }
+
+ CVideoBufferDRMPRIME* buffer = dynamic_cast<CVideoBufferDRMPRIME*>(m_buffers[index].videoBuffer);
+ if (!buffer || !buffer->IsValid())
+ return;
+
+ if (!m_videoLayerBridge)
+ {
+ CWinSystemGbm* winSystem = static_cast<CWinSystemGbm*>(CServiceBroker::GetWinSystem());
+ m_videoLayerBridge =
+ std::dynamic_pointer_cast<CVideoLayerBridgeDRMPRIME>(winSystem->GetVideoLayerBridge());
+ if (!m_videoLayerBridge)
+ m_videoLayerBridge = std::make_shared<CVideoLayerBridgeDRMPRIME>(
+ std::dynamic_pointer_cast<CDRMAtomic>(winSystem->GetDrm()));
+ winSystem->RegisterVideoLayerBridge(m_videoLayerBridge);
+ }
+
+ if (m_iLastRenderBuffer == -1)
+ m_videoLayerBridge->Configure(buffer);
+
+ m_videoLayerBridge->SetVideoPlane(buffer, m_planeDestRect);
+
+ m_iLastRenderBuffer = index;
+}
+
+bool CRendererDRMPRIME::RenderCapture(CRenderCapture* capture)
+{
+ capture->BeginRender();
+ capture->EndRender();
+ return true;
+}
+
+bool CRendererDRMPRIME::ConfigChanged(const VideoPicture& picture)
+{
+ if (picture.videoBuffer->GetFormat() != m_format)
+ return true;
+
+ return false;
+}
+
+bool CRendererDRMPRIME::Supports(ERENDERFEATURE feature) const
+{
+ switch (feature)
+ {
+ case RENDERFEATURE_STRETCH:
+ case RENDERFEATURE_ZOOM:
+ case RENDERFEATURE_VERTICAL_SHIFT:
+ case RENDERFEATURE_PIXEL_RATIO:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool CRendererDRMPRIME::Supports(ESCALINGMETHOD method) const
+{
+ return false;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIME.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIME.h
new file mode 100644
index 0000000..c3953cc
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIME.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017-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/VideoPlayer/VideoRenderers/BaseRenderer.h"
+
+class CVideoBuffer;
+class CVideoLayerBridgeDRMPRIME;
+
+class CRendererDRMPRIME : public CBaseRenderer
+{
+public:
+ CRendererDRMPRIME() = default;
+ ~CRendererDRMPRIME() override;
+
+ // Registration
+ static CBaseRenderer* Create(CVideoBuffer* buffer);
+ static void Register();
+
+ // Player functions
+ bool Configure(const VideoPicture& picture, float fps, unsigned int orientation) override;
+ bool IsConfigured() override { return m_bConfigured; }
+ void AddVideoPicture(const VideoPicture& picture, int index) override;
+ void UnInit() override {}
+ bool Flush(bool saveBuffers) override;
+ void ReleaseBuffer(int idx) override;
+ bool NeedBuffer(int idx) override;
+ bool IsGuiLayer() override { return false; }
+ CRenderInfo GetRenderInfo() override;
+ void Update() override;
+ void RenderUpdate(
+ int index, int index2, bool clear, unsigned int flags, unsigned int alpha) override;
+ bool RenderCapture(CRenderCapture* capture) override;
+ bool ConfigChanged(const VideoPicture& picture) override;
+
+ // Feature support
+ bool SupportsMultiPassRendering() override { return false; }
+ bool Supports(ERENDERFEATURE feature) const override;
+ bool Supports(ESCALINGMETHOD method) const override;
+
+protected:
+ void ManageRenderArea() override;
+
+private:
+ bool m_bConfigured = false;
+ int m_iLastRenderBuffer = -1;
+ CRect m_planeDestRect;
+
+ std::shared_ptr<CVideoLayerBridgeDRMPRIME> m_videoLayerBridge;
+
+ struct BUFFER
+ {
+ CVideoBuffer* videoBuffer = nullptr;
+ } m_buffers[NUM_BUFFERS];
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIMEGLES.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIMEGLES.cpp
new file mode 100644
index 0000000..2fcf54f
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIMEGLES.cpp
@@ -0,0 +1,417 @@
+/*
+ * Copyright (C) 2007-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 "RendererDRMPRIMEGLES.h"
+
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderCapture.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFlags.h"
+#include "rendering/gles/RenderSystemGLES.h"
+#include "utils/EGLFence.h"
+#include "utils/EGLImage.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+#include "windowing/linux/WinSystemEGL.h"
+
+using namespace KODI::UTILS::EGL;
+
+CRendererDRMPRIMEGLES::~CRendererDRMPRIMEGLES()
+{
+ Flush(false);
+}
+
+CBaseRenderer* CRendererDRMPRIMEGLES::Create(CVideoBuffer* buffer)
+{
+ if (!buffer)
+ return nullptr;
+
+ auto buf = dynamic_cast<CVideoBufferDRMPRIME*>(buffer);
+ if (!buf)
+ return nullptr;
+
+#if defined(EGL_EXT_image_dma_buf_import_modifiers)
+ if (!buf->AcquireDescriptor())
+ return nullptr;
+
+ auto desc = buf->GetDescriptor();
+ if (!desc)
+ {
+ buf->ReleaseDescriptor();
+ return nullptr;
+ }
+
+ uint64_t modifier = desc->objects[0].format_modifier;
+ uint32_t format = desc->layers[0].format;
+
+ buf->ReleaseDescriptor();
+
+ auto winSystemEGL =
+ dynamic_cast<KODI::WINDOWING::LINUX::CWinSystemEGL*>(CServiceBroker::GetWinSystem());
+ if (!winSystemEGL)
+ return nullptr;
+
+ CEGLImage image{winSystemEGL->GetEGLDisplay()};
+ if (!image.SupportsFormatAndModifier(format, modifier))
+ return nullptr;
+#endif
+
+ return new CRendererDRMPRIMEGLES();
+}
+
+void CRendererDRMPRIMEGLES::Register()
+{
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("drm_prime_gles", CRendererDRMPRIMEGLES::Create);
+}
+
+bool CRendererDRMPRIMEGLES::Configure(const VideoPicture& picture,
+ float fps,
+ unsigned int orientation)
+{
+ m_format = picture.videoBuffer->GetFormat();
+ m_sourceWidth = picture.iWidth;
+ m_sourceHeight = picture.iHeight;
+ m_renderOrientation = orientation;
+
+ m_iFlags = GetFlagsChromaPosition(picture.chroma_position) |
+ GetFlagsColorMatrix(picture.color_space, picture.iWidth, picture.iHeight) |
+ GetFlagsColorPrimaries(picture.color_primaries) |
+ GetFlagsStereoMode(picture.stereoMode);
+
+ // Calculate the input frame aspect ratio.
+ CalculateFrameAspectRatio(picture.iDisplayWidth, picture.iDisplayHeight);
+ SetViewMode(m_videoSettings.m_ViewMode);
+ ManageRenderArea();
+
+ Flush(false);
+
+ auto winSystem = CServiceBroker::GetWinSystem();
+
+ if (!winSystem)
+ return false;
+
+ auto winSystemEGL = dynamic_cast<KODI::WINDOWING::LINUX::CWinSystemEGL*>(winSystem);
+
+ if (!winSystemEGL)
+ return false;
+
+ EGLDisplay eglDisplay = winSystemEGL->GetEGLDisplay();
+
+ for (auto&& buf : m_buffers)
+ {
+ if (!buf.fence)
+ {
+ buf.texture.Init(eglDisplay);
+ buf.fence.reset(new CEGLFence(eglDisplay));
+ }
+ }
+
+ m_clearColour = winSystem->UseLimitedColor() ? (16.0f / 0xff) : 0.0f;
+
+ m_configured = true;
+ return true;
+}
+
+void CRendererDRMPRIMEGLES::AddVideoPicture(const VideoPicture& picture, int index)
+{
+ BUFFER& buf = m_buffers[index];
+ if (buf.videoBuffer)
+ {
+ CLog::LogF(LOGERROR, "unreleased video buffer");
+ if (buf.fence)
+ buf.fence->DestroyFence();
+ buf.texture.Unmap();
+ buf.videoBuffer->Release();
+ }
+ buf.videoBuffer = picture.videoBuffer;
+ buf.videoBuffer->Acquire();
+}
+
+bool CRendererDRMPRIMEGLES::Flush(bool saveBuffers)
+{
+ if (!saveBuffers)
+ for (int i = 0; i < NUM_BUFFERS; i++)
+ ReleaseBuffer(i);
+
+ return saveBuffers;
+}
+
+void CRendererDRMPRIMEGLES::ReleaseBuffer(int index)
+{
+ BUFFER& buf = m_buffers[index];
+
+ if (buf.fence)
+ buf.fence->DestroyFence();
+
+ buf.texture.Unmap();
+
+ if (buf.videoBuffer)
+ {
+ buf.videoBuffer->Release();
+ buf.videoBuffer = nullptr;
+ }
+}
+
+bool CRendererDRMPRIMEGLES::NeedBuffer(int index)
+{
+ return !m_buffers[index].fence->IsSignaled();
+}
+
+CRenderInfo CRendererDRMPRIMEGLES::GetRenderInfo()
+{
+ CRenderInfo info;
+ info.max_buffer_size = NUM_BUFFERS;
+ return info;
+}
+
+void CRendererDRMPRIMEGLES::Update()
+{
+ if (!m_configured)
+ return;
+
+ ManageRenderArea();
+}
+
+void CRendererDRMPRIMEGLES::DrawBlackBars()
+{
+ CRect windowRect(0, 0, CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(),
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight());
+
+ auto quads = windowRect.SubtractRect(m_destRect);
+
+ struct Svertex
+ {
+ float x, y;
+ };
+
+ std::vector<Svertex> vertices(6 * quads.size());
+
+ GLubyte count = 0;
+ for (const auto& quad : quads)
+ {
+ vertices[count + 1].x = quad.x1;
+ vertices[count + 1].y = quad.y1;
+
+ vertices[count + 0].x = vertices[count + 5].x = quad.x1;
+ vertices[count + 0].y = vertices[count + 5].y = quad.y2;
+
+ vertices[count + 2].x = vertices[count + 3].x = quad.x2;
+ vertices[count + 2].y = vertices[count + 3].y = quad.y1;
+
+ vertices[count + 4].x = quad.x2;
+ vertices[count + 4].y = quad.y2;
+
+ count += 6;
+ }
+
+ glDisable(GL_BLEND);
+
+ CRenderSystemGLES* renderSystem =
+ dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+ if (!renderSystem)
+ return;
+
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_DEFAULT);
+ GLint posLoc = renderSystem->GUIShaderGetPos();
+ GLint uniCol = renderSystem->GUIShaderGetUniCol();
+
+ glUniform4f(uniCol, m_clearColour / 255.0f, m_clearColour / 255.0f, m_clearColour / 255.0f, 1.0f);
+
+ GLuint vertexVBO;
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(Svertex) * vertices.size(), vertices.data(), GL_STATIC_DRAW);
+
+ glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, sizeof(Svertex), 0);
+ glEnableVertexAttribArray(posLoc);
+
+ glDrawArrays(GL_TRIANGLES, 0, vertices.size());
+
+ glDisableVertexAttribArray(posLoc);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+
+ renderSystem->DisableGUIShader();
+}
+
+void CRendererDRMPRIMEGLES::RenderUpdate(
+ int index, int index2, bool clear, unsigned int flags, unsigned int alpha)
+{
+ if (!m_configured)
+ return;
+
+ ManageRenderArea();
+
+ if (clear)
+ {
+ if (alpha == 255)
+ DrawBlackBars();
+ else
+ {
+ glClearColor(m_clearColour, m_clearColour, m_clearColour, 0);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glClearColor(0, 0, 0, 0);
+ }
+ }
+
+ if (alpha < 255)
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ }
+ else
+ {
+ glDisable(GL_BLEND);
+ }
+
+ Render(flags, index);
+
+ VerifyGLState();
+ glEnable(GL_BLEND);
+}
+
+bool CRendererDRMPRIMEGLES::RenderCapture(CRenderCapture* capture)
+{
+ capture->BeginRender();
+ capture->EndRender();
+ return true;
+}
+
+bool CRendererDRMPRIMEGLES::ConfigChanged(const VideoPicture& picture)
+{
+ if (picture.videoBuffer->GetFormat() != m_format)
+ return true;
+
+ return false;
+}
+
+void CRendererDRMPRIMEGLES::Render(unsigned int flags, int index)
+{
+ BUFFER& buf = m_buffers[index];
+
+ CVideoBufferDRMPRIME* buffer = dynamic_cast<CVideoBufferDRMPRIME*>(buf.videoBuffer);
+ if (!buffer || !buffer->IsValid())
+ return;
+
+ CRenderSystemGLES* renderSystem =
+ dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+ if (!renderSystem)
+ return;
+
+ if (!buf.texture.Map(buffer))
+ return;
+
+ glBindTexture(GL_TEXTURE_EXTERNAL_OES, buf.texture.GetTexture());
+
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_TEXTURE_RGBA_OES);
+
+ GLubyte idx[4] = {0, 1, 3, 2}; // Determines order of triangle strip
+ GLuint vertexVBO;
+ GLuint indexVBO;
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ };
+
+ std::array<PackedVertex, 4> vertex;
+
+ GLint vertLoc = renderSystem->GUIShaderGetPos();
+ GLint loc = renderSystem->GUIShaderGetCoord0();
+
+ // top left
+ vertex[0].x = m_rotatedDestCoords[0].x;
+ vertex[0].y = m_rotatedDestCoords[0].y;
+ vertex[0].z = 0.0f;
+ vertex[0].u1 = 0.0f;
+ vertex[0].v1 = 0.0f;
+
+ // top right
+ vertex[1].x = m_rotatedDestCoords[1].x;
+ vertex[1].y = m_rotatedDestCoords[1].y;
+ vertex[1].z = 0.0f;
+ vertex[1].u1 = 1.0f;
+ vertex[1].v1 = 0.0f;
+
+ // bottom right
+ vertex[2].x = m_rotatedDestCoords[2].x;
+ vertex[2].y = m_rotatedDestCoords[2].y;
+ vertex[2].z = 0.0f;
+ vertex[2].u1 = 1.0f;
+ vertex[2].v1 = 1.0f;
+
+ // bottom left
+ vertex[3].x = m_rotatedDestCoords[3].x;
+ vertex[3].y = m_rotatedDestCoords[3].y;
+ vertex[3].z = 0.0f;
+ vertex[3].u1 = 0.0f;
+ vertex[3].v1 = 1.0f;
+
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex) * vertex.size(), vertex.data(),
+ GL_STATIC_DRAW);
+
+ glVertexAttribPointer(vertLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glVertexAttribPointer(loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+
+ glEnableVertexAttribArray(vertLoc);
+ glEnableVertexAttribArray(loc);
+
+ glGenBuffers(1, &indexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte) * 4, idx, GL_STATIC_DRAW);
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+
+ glDisableVertexAttribArray(vertLoc);
+ glDisableVertexAttribArray(loc);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &indexVBO);
+
+ renderSystem->DisableGUIShader();
+
+ glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
+
+ buf.fence->DestroyFence();
+ buf.fence->CreateFence();
+}
+
+bool CRendererDRMPRIMEGLES::Supports(ERENDERFEATURE feature) const
+{
+ switch (feature)
+ {
+ case RENDERFEATURE_STRETCH:
+ case RENDERFEATURE_ZOOM:
+ case RENDERFEATURE_VERTICAL_SHIFT:
+ case RENDERFEATURE_PIXEL_RATIO:
+ case RENDERFEATURE_ROTATION:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool CRendererDRMPRIMEGLES::Supports(ESCALINGMETHOD method) const
+{
+ switch (method)
+ {
+ case VS_SCALINGMETHOD_LINEAR:
+ return true;
+ default:
+ return false;
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIMEGLES.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIMEGLES.h
new file mode 100644
index 0000000..7b2cb07
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIMEGLES.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2007-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 "DRMPRIMEEGL.h"
+#include "cores/VideoPlayer/VideoRenderers/BaseRenderer.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace UTILS
+{
+namespace EGL
+{
+class CEGLFence;
+}
+} // namespace UTILS
+} // namespace KODI
+
+class CRendererDRMPRIMEGLES : public CBaseRenderer
+{
+public:
+ CRendererDRMPRIMEGLES() = default;
+ ~CRendererDRMPRIMEGLES() override;
+
+ // Registration
+ static CBaseRenderer* Create(CVideoBuffer* buffer);
+ static void Register();
+
+ // Player functions
+ bool Configure(const VideoPicture& picture, float fps, unsigned int orientation) override;
+ bool IsConfigured() override { return m_configured; }
+ void AddVideoPicture(const VideoPicture& picture, int index) override;
+ void UnInit() override {}
+ bool Flush(bool saveBuffers) override;
+ void ReleaseBuffer(int idx) override;
+ bool NeedBuffer(int idx) override;
+ CRenderInfo GetRenderInfo() override;
+ void Update() override;
+ void RenderUpdate(
+ int index, int index2, bool clear, unsigned int flags, unsigned int alpha) override;
+ bool RenderCapture(CRenderCapture* capture) override;
+ bool ConfigChanged(const VideoPicture& picture) override;
+
+ // Feature support
+ bool SupportsMultiPassRendering() override { return false; }
+ bool Supports(ERENDERFEATURE feature) const override;
+ bool Supports(ESCALINGMETHOD method) const override;
+
+private:
+ void DrawBlackBars();
+ void Render(unsigned int flags, int index);
+
+ bool m_configured = false;
+ float m_clearColour{0.0f};
+
+ struct BUFFER
+ {
+ CVideoBuffer* videoBuffer = nullptr;
+ std::unique_ptr<KODI::UTILS::EGL::CEGLFence> fence;
+ CDRMPRIMETexture texture;
+ } m_buffers[NUM_BUFFERS];
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodec.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodec.cpp
new file mode 100644
index 0000000..a655b89
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodec.cpp
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2007-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 "RendererMediaCodec.h"
+
+#include "../RenderFactory.h"
+#include "DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.h"
+#include "ServiceBroker.h"
+#include "rendering/gles/RenderSystemGLES.h"
+#include "settings/MediaSettings.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+
+#if defined(EGL_KHR_reusable_sync) && !defined(EGL_EGLEXT_PROTOTYPES)
+static PFNEGLCREATESYNCKHRPROC eglCreateSyncKHR;
+static PFNEGLDESTROYSYNCKHRPROC eglDestroySyncKHR;
+static PFNEGLCLIENTWAITSYNCKHRPROC eglClientWaitSyncKHR;
+#endif
+
+CRendererMediaCodec::CRendererMediaCodec()
+{
+ CLog::Log(LOGINFO, "Instancing CRendererMediaCodec");
+#if defined(EGL_KHR_reusable_sync) && !defined(EGL_EGLEXT_PROTOTYPES)
+ if (!eglCreateSyncKHR) {
+ eglCreateSyncKHR = (PFNEGLCREATESYNCKHRPROC) eglGetProcAddress("eglCreateSyncKHR");
+ }
+ if (!eglDestroySyncKHR) {
+ eglDestroySyncKHR = (PFNEGLDESTROYSYNCKHRPROC) eglGetProcAddress("eglDestroySyncKHR");
+ }
+ if (!eglClientWaitSyncKHR) {
+ eglClientWaitSyncKHR = (PFNEGLCLIENTWAITSYNCKHRPROC) eglGetProcAddress("eglClientWaitSyncKHR");
+ }
+#endif
+}
+
+CRendererMediaCodec::~CRendererMediaCodec()
+{
+ for (int i(0); i < NUM_BUFFERS; ++i)
+ ReleaseBuffer(i);
+}
+
+CBaseRenderer* CRendererMediaCodec::Create(CVideoBuffer *buffer)
+{
+ if (buffer && dynamic_cast<CMediaCodecVideoBuffer*>(buffer) && dynamic_cast<CMediaCodecVideoBuffer*>(buffer)->HasSurfaceTexture())
+ return new CRendererMediaCodec();
+ return nullptr;
+}
+
+bool CRendererMediaCodec::Register()
+{
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("mediacodec_egl", CRendererMediaCodec::Create);
+ return true;
+}
+
+void CRendererMediaCodec::AddVideoPicture(const VideoPicture &picture, int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ CMediaCodecVideoBuffer *videoBuffer;
+ if (picture.videoBuffer && (videoBuffer = dynamic_cast<CMediaCodecVideoBuffer*>(picture.videoBuffer)))
+ {
+ CPictureBuffer &buf = m_buffers[index];
+ buf.videoBuffer = picture.videoBuffer;
+ buf.fields[0][0].id = videoBuffer->GetTextureId();
+ videoBuffer->Acquire();
+
+ // releaseOutputBuffer must be in same thread as
+ // dequeueOutputBuffer. We are in VideoPlayerVideo
+ // thread here, so we are safe.
+ videoBuffer->ReleaseOutputBuffer(true, 0);
+ }
+ else
+ buf.fields[0][0].id = 0;
+}
+
+void CRendererMediaCodec::ReleaseBuffer(int idx)
+{
+ CPictureBuffer &buf = m_buffers[idx];
+ CMediaCodecVideoBuffer* videoBuffer;
+ if (buf.videoBuffer && (videoBuffer = dynamic_cast<CMediaCodecVideoBuffer*>(buf.videoBuffer)))
+ {
+ // The media buffer has been queued to the SurfaceView but we didn't render it
+ // We have to do to the updateTexImage or it will get stuck
+ videoBuffer->UpdateTexImage();
+ videoBuffer->GetTransformMatrix(m_textureMatrix);
+ videoBuffer->Release();
+ buf.videoBuffer = NULL;
+ }
+}
+
+CRenderInfo CRendererMediaCodec::GetRenderInfo()
+{
+ CRenderInfo info;
+ info.max_buffer_size = 4;
+ info.optimal_buffer_size = 3;
+ return info;
+}
+
+bool CRendererMediaCodec::LoadShadersHook()
+{
+ CLog::Log(LOGINFO, "GL: Using MediaCodec render method");
+ m_textureTarget = GL_TEXTURE_2D;
+ m_renderMethod = RENDER_CUSTOM;
+ return true;
+}
+
+bool CRendererMediaCodec::RenderHook(int index)
+{
+ CYuvPlane &plane = m_buffers[index].fields[0][0];
+ CYuvPlane &planef = m_buffers[index].fields[m_currentField][0];
+
+ glDisable(GL_DEPTH_TEST);
+
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_EXTERNAL_OES, plane.id);
+
+ CRenderSystemGLES* renderSystem = dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+
+ if (m_currentField != FIELD_FULL)
+ {
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_TEXTURE_RGBA_BOB_OES);
+ GLint fieldLoc = renderSystem->GUIShaderGetField();
+ GLint stepLoc = renderSystem->GUIShaderGetStep();
+
+ // Y is inverted, so invert fields
+ if (m_currentField == FIELD_TOP)
+ glUniform1i(fieldLoc, 0);
+ else if(m_currentField == FIELD_BOT)
+ glUniform1i(fieldLoc, 1);
+ glUniform1f(stepLoc, 1.0f / (float)plane.texheight);
+ }
+ else
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_TEXTURE_RGBA_OES);
+
+ GLint contrastLoc = renderSystem->GUIShaderGetContrast();
+ glUniform1f(contrastLoc, m_videoSettings.m_Contrast * 0.02f);
+ GLint brightnessLoc = renderSystem->GUIShaderGetBrightness();
+ glUniform1f(brightnessLoc, m_videoSettings.m_Brightness * 0.01f - 0.5f);
+
+ glUniformMatrix4fv(renderSystem->GUIShaderGetCoord0Matrix(), 1, GL_FALSE, m_textureMatrix);
+
+ GLubyte idx[4] = {0, 1, 3, 2}; //determines order of triangle strip
+ GLfloat ver[4][4];
+ GLfloat tex[4][4];
+
+ GLint posLoc = renderSystem->GUIShaderGetPos();
+ GLint texLoc = renderSystem->GUIShaderGetCoord0();
+
+
+ glVertexAttribPointer(posLoc, 4, GL_FLOAT, 0, 0, ver);
+ glVertexAttribPointer(texLoc, 4, GL_FLOAT, 0, 0, tex);
+
+ glEnableVertexAttribArray(posLoc);
+ glEnableVertexAttribArray(texLoc);
+
+ // Set vertex coordinates
+ for(int i = 0; i < 4; i++)
+ {
+ ver[i][0] = m_rotatedDestCoords[i].x;
+ ver[i][1] = m_rotatedDestCoords[i].y;
+ ver[i][2] = 0.0f; // set z to 0
+ ver[i][3] = 1.0f;
+ }
+
+ // Set texture coordinates (MediaCodec is flipped in y)
+ if (m_currentField == FIELD_FULL)
+ {
+ tex[0][0] = tex[3][0] = plane.rect.x1;
+ tex[0][1] = tex[1][1] = plane.rect.y2;
+ tex[1][0] = tex[2][0] = plane.rect.x2;
+ tex[2][1] = tex[3][1] = plane.rect.y1;
+ }
+ else
+ {
+ tex[0][0] = tex[3][0] = planef.rect.x1;
+ tex[0][1] = tex[1][1] = planef.rect.y2 * 2.0f;
+ tex[1][0] = tex[2][0] = planef.rect.x2;
+ tex[2][1] = tex[3][1] = planef.rect.y1 * 2.0f;
+ }
+
+ for(int i = 0; i < 4; i++)
+ {
+ tex[i][2] = 0.0f;
+ tex[i][3] = 1.0f;
+ }
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, idx);
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(texLoc);
+
+ const float identity[16] = {
+ 1.0f, 0.0f, 0.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f, 0.0f,
+ 0.0f, 0.0f, 1.0f, 0.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f
+ };
+ glUniformMatrix4fv(renderSystem->GUIShaderGetCoord0Matrix(), 1, GL_FALSE, identity);
+
+ renderSystem->DisableGUIShader();
+ VerifyGLState();
+
+ glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
+ VerifyGLState();
+
+ return true;
+}
+
+bool CRendererMediaCodec::CreateTexture(int index)
+{
+ CPictureBuffer &buf(m_buffers[index]);
+
+ buf.image.height = m_sourceHeight;
+ buf.image.width = m_sourceWidth;
+
+ for (int f=0; f<3; ++f)
+ {
+ CYuvPlane &plane = buf.fields[f][0];
+
+ plane.texwidth = m_sourceWidth;
+ plane.texheight = m_sourceHeight;
+ plane.pixpertex_x = 1;
+ plane.pixpertex_y = 1;
+ }
+
+ return true;
+}
+
+void CRendererMediaCodec::DeleteTexture(int index)
+{
+ ReleaseBuffer(index);
+}
+
+bool CRendererMediaCodec::UploadTexture(int index)
+{
+ ReleaseBuffer(index);
+ CalculateTextureSourceRects(index, 1);
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodec.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodec.h
new file mode 100644
index 0000000..7b4c3bf
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodec.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007-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/VideoPlayer/VideoRenderers/LinuxRendererGLES.h"
+
+class CRendererMediaCodec : public CLinuxRendererGLES
+{
+public:
+ CRendererMediaCodec();
+ ~CRendererMediaCodec() override;
+
+ // Registration
+ static CBaseRenderer* Create(CVideoBuffer *buffer);
+ static bool Register();
+
+ // Player functions
+ void AddVideoPicture(const VideoPicture& picture, int index) override;
+ void ReleaseBuffer(int idx) override;
+
+ // Feature support
+ CRenderInfo GetRenderInfo() override;
+
+protected:
+ // textures
+ bool UploadTexture(int index) override;
+ void DeleteTexture(int index) override;
+ bool CreateTexture(int index) override;
+
+ // hooks for hw dec renderer
+ bool LoadShadersHook() override;
+ bool RenderHook(int index) override;
+
+private:
+ float m_textureMatrix[16];
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodecSurface.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodecSurface.cpp
new file mode 100644
index 0000000..1a9c278
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodecSurface.cpp
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2007-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 "RendererMediaCodecSurface.h"
+
+#include "../RenderCapture.h"
+#include "../RenderFactory.h"
+#include "../RenderFlags.h"
+#include "DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.h"
+#include "rendering/RenderSystem.h"
+#include "settings/MediaSettings.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include "platform/android/activity/XBMCApp.h"
+
+#include <chrono>
+#include <thread>
+
+CRendererMediaCodecSurface::CRendererMediaCodecSurface()
+{
+ CLog::Log(LOGINFO, "Instancing CRendererMediaCodecSurface");
+}
+
+CRendererMediaCodecSurface::~CRendererMediaCodecSurface()
+{
+ Reset();
+}
+
+CBaseRenderer* CRendererMediaCodecSurface::Create(CVideoBuffer *buffer)
+{
+ if (buffer && dynamic_cast<CMediaCodecVideoBuffer*>(buffer) && !dynamic_cast<CMediaCodecVideoBuffer*>(buffer)->HasSurfaceTexture())
+ return new CRendererMediaCodecSurface();
+ return nullptr;
+}
+
+bool CRendererMediaCodecSurface::Register()
+{
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("mediacodec_surface", CRendererMediaCodecSurface::Create);
+ return true;
+}
+
+bool CRendererMediaCodecSurface::Configure(const VideoPicture &picture, float fps, unsigned int orientation)
+{
+ CLog::Log(LOGINFO, "CRendererMediaCodecSurface::Configure");
+
+ m_sourceWidth = picture.iWidth;
+ m_sourceHeight = picture.iHeight;
+ m_renderOrientation = orientation;
+
+ m_iFlags = GetFlagsChromaPosition(picture.chroma_position) |
+ GetFlagsColorMatrix(picture.color_space, picture.iWidth, picture.iHeight) |
+ GetFlagsColorPrimaries(picture.color_primaries) |
+ GetFlagsStereoMode(picture.stereoMode);
+
+ // Calculate the input frame aspect ratio.
+ CalculateFrameAspectRatio(picture.iDisplayWidth, picture.iDisplayHeight);
+ SetViewMode(m_videoSettings.m_ViewMode);
+
+ return true;
+}
+
+CRenderInfo CRendererMediaCodecSurface::GetRenderInfo()
+{
+ CRenderInfo info;
+ info.max_buffer_size = info.optimal_buffer_size = 4;
+ return info;
+}
+
+bool CRendererMediaCodecSurface::RenderCapture(CRenderCapture* capture)
+{
+ capture->BeginRender();
+ capture->EndRender();
+ return true;
+}
+
+void CRendererMediaCodecSurface::AddVideoPicture(const VideoPicture &picture, int index)
+{
+ ReleaseBuffer(index);
+
+ BUFFER &buf(m_buffers[index]);
+ if (picture.videoBuffer)
+ {
+ buf.videoBuffer = picture.videoBuffer;
+ buf.videoBuffer->Acquire();
+ }
+}
+
+void CRendererMediaCodecSurface::ReleaseVideoBuffer(int idx, bool render)
+{
+ BUFFER &buf(m_buffers[idx]);
+ if (buf.videoBuffer)
+ {
+ CMediaCodecVideoBuffer *mcvb(dynamic_cast<CMediaCodecVideoBuffer*>(buf.videoBuffer));
+ if (mcvb)
+ {
+ if (render && m_bConfigured)
+ mcvb->RenderUpdate(m_surfDestRect, CXBMCApp::Get().GetNextFrameTime());
+ else
+ mcvb->ReleaseOutputBuffer(render, 0);
+ }
+ buf.videoBuffer->Release();
+ buf.videoBuffer = nullptr;
+ }
+}
+
+void CRendererMediaCodecSurface::ReleaseBuffer(int idx)
+{
+ ReleaseVideoBuffer(idx, false);
+}
+
+bool CRendererMediaCodecSurface::Supports(ERENDERFEATURE feature) const
+{
+ if (feature == RENDERFEATURE_ZOOM || feature == RENDERFEATURE_STRETCH ||
+ feature == RENDERFEATURE_PIXEL_RATIO || feature == RENDERFEATURE_VERTICAL_SHIFT ||
+ feature == RENDERFEATURE_ROTATION)
+ return true;
+
+ return false;
+}
+
+void CRendererMediaCodecSurface::Reset()
+{
+ for (int i = 0 ; i < 4 ; ++i)
+ ReleaseVideoBuffer(i, false);
+ m_lastIndex = -1;
+}
+
+void CRendererMediaCodecSurface::RenderUpdate(int index, int index2, bool clear, unsigned int flags, unsigned int alpha)
+{
+ m_bConfigured = true;
+
+ // this hack is needed to get the 2D mode of a 3D movie going
+ RENDER_STEREO_MODE stereo_mode = CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode();
+ if (stereo_mode)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetStereoView(RENDER_STEREO_VIEW_LEFT);
+
+ ManageRenderArea();
+
+ if (stereo_mode)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetStereoView(RENDER_STEREO_VIEW_OFF);
+
+ m_surfDestRect = m_destRect;
+ switch (stereo_mode)
+ {
+ case RENDER_STEREO_MODE_SPLIT_HORIZONTAL:
+ m_surfDestRect.y2 *= 2.0;
+ break;
+ case RENDER_STEREO_MODE_SPLIT_VERTICAL:
+ m_surfDestRect.x2 *= 2.0;
+ break;
+ case RENDER_STEREO_MODE_MONO:
+ if (CONF_FLAGS_STEREO_MODE_MASK(m_iFlags) == CONF_FLAGS_STEREO_MODE_TAB)
+ m_surfDestRect.y2 = m_surfDestRect.y2 * 2.0f;
+ else
+ m_surfDestRect.x2 = m_surfDestRect.x2 * 2.0f;
+ break;
+ default:
+ break;
+ }
+
+ if (index != m_lastIndex)
+ {
+ ReleaseVideoBuffer(index, true);
+ m_lastIndex = index;
+ }
+}
+
+void CRendererMediaCodecSurface::ReorderDrawPoints()
+{
+ CBaseRenderer::ReorderDrawPoints();
+
+ // Handle orientation
+ switch (m_renderOrientation)
+ {
+ case 90:
+ case 270:
+ {
+ double scale = static_cast<double>(m_surfDestRect.Height() / m_surfDestRect.Width());
+ int diff = static_cast<int>(static_cast<double>(m_surfDestRect.Height()) * scale -
+ static_cast<double>(m_surfDestRect.Width())) /
+ 2;
+ m_surfDestRect = CRect(m_surfDestRect.x1 - diff, m_surfDestRect.y1, m_surfDestRect.x2 + diff, m_surfDestRect.y2);
+ }
+ default:
+ break;
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodecSurface.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodecSurface.h
new file mode 100644
index 0000000..d27601e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodecSurface.h
@@ -0,0 +1,58 @@
+/*
+ * 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/VideoPlayer/VideoRenderers/BaseRenderer.h"
+
+class CMediaCodecVideoBuffer;
+
+class CRendererMediaCodecSurface : public CBaseRenderer
+{
+public:
+ CRendererMediaCodecSurface();
+ ~CRendererMediaCodecSurface() override;
+
+ static CBaseRenderer* Create(CVideoBuffer *buffer);
+ static bool Register();
+
+ bool RenderCapture(CRenderCapture* capture) override;
+ void AddVideoPicture(const VideoPicture& picture, int index) override;
+ void ReleaseBuffer(int idx) override;
+ bool Configure(const VideoPicture& picture, float fps, unsigned int orientation) override;
+ bool IsConfigured() override { return m_bConfigured; }
+ bool ConfigChanged(const VideoPicture& picture) override { return false; }
+ CRenderInfo GetRenderInfo() override;
+ void UnInit() override{};
+ void Update() override{};
+ void RenderUpdate(int index, int index2, bool clear, unsigned int flags, unsigned int alpha) override;
+ bool SupportsMultiPassRendering() override { return false; }
+
+ // Player functions
+ bool IsGuiLayer() override { return false; }
+
+ // Feature support
+ bool Supports(ESCALINGMETHOD method) const override { return false; }
+ bool Supports(ERENDERFEATURE feature) const override;
+
+protected:
+ void ReorderDrawPoints() override;
+
+private:
+ void Reset();
+ void ReleaseVideoBuffer(int idx, bool render);
+
+ bool m_bConfigured = false;
+ CRect m_surfDestRect;
+ int m_lastIndex = -1;
+
+ struct BUFFER
+ {
+ CVideoBuffer *videoBuffer = nullptr;
+ } m_buffers[4];
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.cpp
new file mode 100644
index 0000000..4b2a19b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.cpp
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2007-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 "RendererVAAPIGL.h"
+
+#include "../RenderFactory.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/VAAPI.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+
+using namespace VAAPI;
+
+IVaapiWinSystem* CRendererVAAPIGL::m_pWinSystem = nullptr;
+
+CBaseRenderer* CRendererVAAPIGL::Create(CVideoBuffer* buffer)
+{
+ CVaapiRenderPicture *vb = dynamic_cast<CVaapiRenderPicture*>(buffer);
+ if (vb)
+ return new CRendererVAAPIGL();
+
+ return nullptr;
+}
+
+void CRendererVAAPIGL::Register(IVaapiWinSystem* winSystem,
+ VADisplay vaDpy,
+ EGLDisplay eglDisplay,
+ bool& general,
+ bool& deepColor)
+{
+ general = deepColor = false;
+
+ int major_version, minor_version;
+ if (vaInitialize(vaDpy, &major_version, &minor_version) != VA_STATUS_SUCCESS)
+ {
+ vaTerminate(vaDpy);
+ return;
+ }
+
+ CVaapi2Texture::TestInterop(vaDpy, eglDisplay, general, deepColor);
+ CLog::Log(LOGDEBUG, "Vaapi2 EGL interop test results: general {}, deepColor {}",
+ general ? "yes" : "no", deepColor ? "yes" : "no");
+ if (!general)
+ {
+ CVaapi1Texture::TestInterop(vaDpy, eglDisplay, general, deepColor);
+ CLog::Log(LOGDEBUG, "Vaapi1 EGL interop test results: general {}, deepColor {}",
+ general ? "yes" : "no", deepColor ? "yes" : "no");
+ }
+
+ vaTerminate(vaDpy);
+
+ if (general)
+ {
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("vaapi", CRendererVAAPIGL::Create);
+ m_pWinSystem = winSystem;
+ }
+}
+
+CRendererVAAPIGL::CRendererVAAPIGL() = default;
+
+CRendererVAAPIGL::~CRendererVAAPIGL()
+{
+ for (int i = 0; i < NUM_BUFFERS; ++i)
+ {
+ DeleteTexture(i);
+ }
+}
+
+bool CRendererVAAPIGL::Configure(const VideoPicture& picture, float fps, unsigned int orientation)
+{
+ CVaapiRenderPicture *pic = dynamic_cast<CVaapiRenderPicture*>(picture.videoBuffer);
+ if (pic->procPic.videoSurface == VA_INVALID_ID)
+ {
+ m_isVAAPIBuffer = false;
+ }
+ else
+ {
+ m_isVAAPIBuffer = true;
+
+ InteropInfo interop;
+ interop.textureTarget = GL_TEXTURE_2D;
+ interop.eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
+ interop.eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
+ interop.glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
+ interop.eglDisplay = CRendererVAAPIGL::m_pWinSystem->GetEGLDisplay();
+
+ bool useVaapi2 = VAAPI::CVaapi2Texture::TestInteropGeneral(
+ pic->vadsp, CRendererVAAPIGL::m_pWinSystem->GetEGLDisplay());
+
+ for (auto &tex : m_vaapiTextures)
+ {
+ if (useVaapi2)
+ {
+ tex.reset(new VAAPI::CVaapi2Texture);
+ }
+ else
+ {
+ tex.reset(new VAAPI::CVaapi1Texture);
+ }
+ tex->Init(interop);
+ }
+ }
+
+ for (auto &fence : m_fences)
+ {
+ fence = {};
+ }
+
+ return CLinuxRendererGL::Configure(picture, fps, orientation);
+}
+
+bool CRendererVAAPIGL::Flush(bool saveBuffers)
+{
+ for (auto &vaapiTexture : m_vaapiTextures)
+ {
+ if (m_isVAAPIBuffer)
+ {
+ vaapiTexture->Unmap();
+ }
+ }
+ return CLinuxRendererGL::Flush(saveBuffers);
+}
+
+bool CRendererVAAPIGL::ConfigChanged(const VideoPicture& picture)
+{
+ CVaapiRenderPicture *pic = dynamic_cast<CVaapiRenderPicture*>(picture.videoBuffer);
+ if ((pic->procPic.videoSurface != VA_INVALID_ID && !m_isVAAPIBuffer) ||
+ (pic->procPic.videoSurface == VA_INVALID_ID && m_isVAAPIBuffer))
+ return true;
+
+ return false;
+}
+
+bool CRendererVAAPIGL::Supports(ERENDERFEATURE feature) const
+{
+ return CLinuxRendererGL::Supports(feature);
+}
+
+bool CRendererVAAPIGL::Supports(ESCALINGMETHOD method) const
+{
+ return CLinuxRendererGL::Supports(method);
+}
+
+EShaderFormat CRendererVAAPIGL::GetShaderFormat()
+{
+ return SHADER_NV12;
+}
+
+bool CRendererVAAPIGL::LoadShadersHook()
+{
+ return false;
+}
+
+bool CRendererVAAPIGL::RenderHook(int idx)
+{
+ return false;
+}
+
+bool CRendererVAAPIGL::CreateTexture(int index)
+{
+ if (!m_isVAAPIBuffer)
+ {
+ return CreateNV12Texture(index);
+ }
+
+ CPictureBuffer &buf = m_buffers[index];
+ YuvImage &im = buf.image;
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = buf.fields[0];
+
+ DeleteTexture(index);
+
+ memset(&im, 0, sizeof(im));
+ memset(&planes, 0, sizeof(CYuvPlane[YuvImage::MAX_PLANES]));
+ im.height = m_sourceHeight;
+ im.width = m_sourceWidth;
+ im.cshift_x = 1;
+ im.cshift_y = 1;
+
+ planes[0].id = 1;
+
+ return true;
+}
+
+void CRendererVAAPIGL::DeleteTexture(int index)
+{
+ ReleaseBuffer(index);
+
+ if (!m_isVAAPIBuffer)
+ {
+ DeleteNV12Texture(index);
+ return;
+ }
+
+ CPictureBuffer &buf = m_buffers[index];
+ buf.fields[FIELD_FULL][0].id = 0;
+ buf.fields[FIELD_FULL][1].id = 0;
+ buf.fields[FIELD_FULL][2].id = 0;
+}
+
+bool CRendererVAAPIGL::UploadTexture(int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ CVaapiRenderPicture *pic = dynamic_cast<CVaapiRenderPicture*>(buf.videoBuffer);
+
+ if (!pic || !pic->valid)
+ {
+ return false;
+ }
+
+ if (!m_isVAAPIBuffer)
+ {
+ if (!m_buffers[index].loaded)
+ {
+ YuvImage &dst = m_buffers[index].image;
+ YuvImage src;
+ pic->GetPlanes(src.plane);
+ pic->GetStrides(src.stride);
+ UnBindPbo(m_buffers[index]);
+ CVideoBuffer::CopyNV12Picture(&dst, &src);
+ BindPbo(m_buffers[index]);
+ }
+ CalculateTextureSourceRects(index, 3);
+ return UploadNV12Texture(index);
+ }
+
+ m_vaapiTextures[index]->Map(pic);
+
+ const YuvImage& im = buf.image;
+ CYuvPlane (&planes)[3] = buf.fields[0];
+
+ auto size = m_vaapiTextures[index]->GetTextureSize();
+ planes[0].texwidth = size.Width();
+ planes[0].texheight = size.Height();
+
+ planes[1].texwidth = planes[0].texwidth >> im.cshift_x;
+ planes[1].texheight = planes[0].texheight >> im.cshift_y;
+ planes[2].texwidth = planes[1].texwidth;
+ planes[2].texheight = planes[1].texheight;
+
+ for (int p = 0; p < 3; p++)
+ {
+ planes[p].pixpertex_x = 1;
+ planes[p].pixpertex_y = 1;
+ }
+
+ // set textures
+ planes[0].id = m_vaapiTextures[index]->GetTextureY();
+ planes[1].id = m_vaapiTextures[index]->GetTextureVU();
+ planes[2].id = m_vaapiTextures[index]->GetTextureVU();
+
+ for (int p=0; p<2; p++)
+ {
+ glBindTexture(m_textureTarget, planes[p].id);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glBindTexture(m_textureTarget, 0);
+ VerifyGLState();
+ }
+
+ CalculateTextureSourceRects(index, 3);
+ return true;
+}
+
+void CRendererVAAPIGL::AfterRenderHook(int idx)
+{
+ if (glIsSync(m_fences[idx]))
+ {
+ glDeleteSync(m_fences[idx]);
+ m_fences[idx] = {};
+ }
+ m_fences[idx] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+}
+
+bool CRendererVAAPIGL::NeedBuffer(int idx)
+{
+ if (glIsSync(m_fences[idx]))
+ {
+ GLint state;
+ GLsizei length;
+ glGetSynciv(m_fences[idx], GL_SYNC_STATUS, 1, &length, &state);
+ if (state == GL_SIGNALED)
+ {
+ glDeleteSync(m_fences[idx]);
+ m_fences[idx] = {};
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CRendererVAAPIGL::ReleaseBuffer(int idx)
+{
+ if (glIsSync(m_fences[idx]))
+ {
+ glDeleteSync(m_fences[idx]);
+ m_fences[idx] = {};
+ }
+ if (m_isVAAPIBuffer)
+ {
+ m_vaapiTextures[idx]->Unmap();
+ }
+ CLinuxRendererGL::ReleaseBuffer(idx);
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.h
new file mode 100644
index 0000000..eacba07
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2007-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 "VaapiEGL.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h"
+
+#include <memory>
+
+namespace VAAPI
+{
+class IVaapiWinSystem;
+}
+
+class CRendererVAAPIGL : public CLinuxRendererGL
+{
+public:
+ CRendererVAAPIGL();
+ ~CRendererVAAPIGL() override;
+
+ static CBaseRenderer* Create(CVideoBuffer *buffer);
+ static void Register(VAAPI::IVaapiWinSystem *winSystem, VADisplay vaDpy, EGLDisplay eglDisplay, bool &general, bool &deepColor);
+
+ bool Configure(const VideoPicture &picture, float fps, unsigned int orientation) override;
+
+ // Player functions
+ bool ConfigChanged(const VideoPicture &picture) override;
+ void ReleaseBuffer(int idx) override;
+ bool NeedBuffer(int idx) override;
+ bool Flush(bool saveBuffers) override;
+
+ // Feature support
+ bool Supports(ERENDERFEATURE feature) const override;
+ bool Supports(ESCALINGMETHOD method) const override;
+
+protected:
+ bool LoadShadersHook() override;
+ bool RenderHook(int idx) override;
+ void AfterRenderHook(int idx) override;
+
+ // textures
+ bool UploadTexture(int index) override;
+ void DeleteTexture(int index) override;
+ bool CreateTexture(int index) override;
+
+ EShaderFormat GetShaderFormat() override;
+
+ bool m_isVAAPIBuffer = true;
+ std::unique_ptr<VAAPI::CVaapiTexture> m_vaapiTextures[NUM_BUFFERS];
+ GLsync m_fences[NUM_BUFFERS];
+ static VAAPI::IVaapiWinSystem *m_pWinSystem;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.cpp
new file mode 100644
index 0000000..65e1e84
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.cpp
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2007-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 "RendererVAAPIGLES.h"
+
+#include "../RenderFactory.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDCodecUtils.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/VAAPI.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "utils/EGLFence.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+
+using namespace VAAPI;
+using namespace KODI::UTILS::EGL;
+
+IVaapiWinSystem* CRendererVAAPIGLES::m_pWinSystem = nullptr;
+
+CBaseRenderer* CRendererVAAPIGLES::Create(CVideoBuffer* buffer)
+{
+ CVaapiRenderPicture *vb = dynamic_cast<CVaapiRenderPicture*>(buffer);
+ if (vb)
+ return new CRendererVAAPIGLES();
+
+ return nullptr;
+}
+
+void CRendererVAAPIGLES::Register(IVaapiWinSystem* winSystem,
+ VADisplay vaDpy,
+ EGLDisplay eglDisplay,
+ bool& general,
+ bool& deepColor)
+{
+ general = deepColor = false;
+
+ int major_version, minor_version;
+ if (vaInitialize(vaDpy, &major_version, &minor_version) != VA_STATUS_SUCCESS)
+ {
+ vaTerminate(vaDpy);
+ return;
+ }
+
+ CVaapi2Texture::TestInterop(vaDpy, eglDisplay, general, deepColor);
+ CLog::Log(LOGDEBUG, "Vaapi2 EGL interop test results: general {}, deepColor {}",
+ general ? "yes" : "no", deepColor ? "yes" : "no");
+ if (!general)
+ {
+ CVaapi1Texture::TestInterop(vaDpy, eglDisplay, general, deepColor);
+ CLog::Log(LOGDEBUG, "Vaapi1 EGL interop test results: general {}, deepColor {}",
+ general ? "yes" : "no", deepColor ? "yes" : "no");
+ }
+
+ vaTerminate(vaDpy);
+
+ if (general)
+ {
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("vaapi", CRendererVAAPIGLES::Create);
+ m_pWinSystem = winSystem;
+ }
+}
+
+CRendererVAAPIGLES::CRendererVAAPIGLES() = default;
+
+CRendererVAAPIGLES::~CRendererVAAPIGLES()
+{
+ for (int i = 0; i < NUM_BUFFERS; ++i)
+ {
+ DeleteTexture(i);
+ }
+}
+
+bool CRendererVAAPIGLES::Configure(const VideoPicture& picture, float fps, unsigned int orientation)
+{
+ CVaapiRenderPicture *pic = dynamic_cast<CVaapiRenderPicture*>(picture.videoBuffer);
+ if (pic->procPic.videoSurface != VA_INVALID_ID)
+ m_isVAAPIBuffer = true;
+ else
+ m_isVAAPIBuffer = false;
+
+ InteropInfo interop;
+ interop.textureTarget = GL_TEXTURE_2D;
+ interop.eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
+ interop.eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
+ interop.glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
+ interop.eglDisplay = m_pWinSystem->GetEGLDisplay();
+
+ bool useVaapi2 = VAAPI::CVaapi2Texture::TestInteropGeneral(
+ pic->vadsp, CRendererVAAPIGLES::m_pWinSystem->GetEGLDisplay());
+
+ for (auto &tex : m_vaapiTextures)
+ {
+ if (useVaapi2)
+ {
+ tex.reset(new VAAPI::CVaapi2Texture);
+ }
+ else
+ {
+ tex.reset(new VAAPI::CVaapi1Texture);
+ }
+ tex->Init(interop);
+ }
+
+ for (auto& fence : m_fences)
+ {
+ fence.reset(new CEGLFence(CRendererVAAPIGLES::m_pWinSystem->GetEGLDisplay()));
+ }
+
+ return CLinuxRendererGLES::Configure(picture, fps, orientation);
+}
+
+bool CRendererVAAPIGLES::ConfigChanged(const VideoPicture& picture)
+{
+ CVaapiRenderPicture *pic = dynamic_cast<CVaapiRenderPicture*>(picture.videoBuffer);
+ if (pic->procPic.videoSurface != VA_INVALID_ID && !m_isVAAPIBuffer)
+ return true;
+
+ return false;
+}
+
+EShaderFormat CRendererVAAPIGLES::GetShaderFormat()
+{
+ EShaderFormat ret = SHADER_NONE;
+
+ if (m_isVAAPIBuffer)
+ ret = SHADER_NV12_RRG;
+ else
+ ret = SHADER_NV12;
+
+ return ret;
+}
+
+bool CRendererVAAPIGLES::LoadShadersHook()
+{
+ return false;
+}
+
+bool CRendererVAAPIGLES::RenderHook(int idx)
+{
+ return false;
+}
+
+bool CRendererVAAPIGLES::CreateTexture(int index)
+{
+ if (!m_isVAAPIBuffer)
+ {
+ return CreateNV12Texture(index);
+ }
+
+ CPictureBuffer &buf = m_buffers[index];
+ YuvImage &im = buf.image;
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = buf.fields[0];
+
+ DeleteTexture(index);
+
+ im = {};
+ std::fill(std::begin(planes), std::end(planes), CYuvPlane{});
+ im.height = m_sourceHeight;
+ im.width = m_sourceWidth;
+ im.cshift_x = 1;
+ im.cshift_y = 1;
+
+ planes[0].id = 1;
+
+ return true;
+}
+
+void CRendererVAAPIGLES::DeleteTexture(int index)
+{
+ ReleaseBuffer(index);
+
+ if (!m_isVAAPIBuffer)
+ {
+ DeleteNV12Texture(index);
+ return;
+ }
+
+ CPictureBuffer &buf = m_buffers[index];
+ buf.fields[FIELD_FULL][0].id = 0;
+ buf.fields[FIELD_FULL][1].id = 0;
+ buf.fields[FIELD_FULL][2].id = 0;
+}
+
+bool CRendererVAAPIGLES::UploadTexture(int index)
+{
+ if (!m_isVAAPIBuffer)
+ {
+ return UploadNV12Texture(index);
+ }
+
+ CPictureBuffer &buf = m_buffers[index];
+
+ CVaapiRenderPicture *pic = dynamic_cast<CVaapiRenderPicture*>(buf.videoBuffer);
+
+ if (!pic || !pic->valid)
+ {
+ return false;
+ }
+
+ m_vaapiTextures[index]->Map(pic);
+
+ YuvImage &im = buf.image;
+ CYuvPlane (&planes)[3] = buf.fields[0];
+
+ auto size = m_vaapiTextures[index]->GetTextureSize();
+ planes[0].texwidth = size.Width();
+ planes[0].texheight = size.Height();
+
+ planes[1].texwidth = planes[0].texwidth >> im.cshift_x;
+ planes[1].texheight = planes[0].texheight >> im.cshift_y;
+ planes[2].texwidth = planes[1].texwidth;
+ planes[2].texheight = planes[1].texheight;
+
+ for (int p = 0; p < 3; p++)
+ {
+ planes[p].pixpertex_x = 1;
+ planes[p].pixpertex_y = 1;
+ }
+
+ // set textures
+ planes[0].id = m_vaapiTextures[index]->GetTextureY();
+ planes[1].id = m_vaapiTextures[index]->GetTextureVU();
+ planes[2].id = m_vaapiTextures[index]->GetTextureVU();
+
+ for (int p=0; p<2; p++)
+ {
+ glBindTexture(m_textureTarget, planes[p].id);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glBindTexture(m_textureTarget, 0);
+ VerifyGLState();
+ }
+
+ CalculateTextureSourceRects(index, 3);
+ return true;
+}
+
+void CRendererVAAPIGLES::AfterRenderHook(int index)
+{
+ m_fences[index]->CreateFence();
+}
+
+bool CRendererVAAPIGLES::NeedBuffer(int index)
+{
+ return !m_fences[index]->IsSignaled();
+}
+
+void CRendererVAAPIGLES::ReleaseBuffer(int index)
+{
+ m_fences[index]->DestroyFence();
+
+ if (m_isVAAPIBuffer)
+ {
+ m_vaapiTextures[index]->Unmap();
+ }
+
+ CLinuxRendererGLES::ReleaseBuffer(index);
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.h
new file mode 100644
index 0000000..26b7865
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2007-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 "VaapiEGL.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h"
+
+#include <array>
+#include <memory>
+
+namespace KODI
+{
+namespace UTILS
+{
+namespace EGL
+{
+class CEGLFence;
+}
+}
+}
+
+namespace VAAPI
+{
+class IVaapiWinSystem;
+}
+
+class CRendererVAAPIGLES : public CLinuxRendererGLES
+{
+public:
+ CRendererVAAPIGLES();
+ ~CRendererVAAPIGLES() override;
+
+ static CBaseRenderer* Create(CVideoBuffer *buffer);
+ static void Register(VAAPI::IVaapiWinSystem *winSystem, VADisplay vaDpy, EGLDisplay eglDisplay, bool &general, bool &deepColor);
+
+ bool Configure(const VideoPicture &picture, float fps, unsigned int orientation) override;
+
+ // Player functions
+ bool ConfigChanged(const VideoPicture &picture) override;
+ void ReleaseBuffer(int idx) override;
+ bool NeedBuffer(int idx) override;
+
+protected:
+ bool LoadShadersHook() override;
+ bool RenderHook(int idx) override;
+ void AfterRenderHook(int idx) override;
+
+ // textures
+ bool UploadTexture(int index) override;
+ void DeleteTexture(int index) override;
+ bool CreateTexture(int index) override;
+
+ EShaderFormat GetShaderFormat() override;
+
+ bool m_isVAAPIBuffer = true;
+ std::unique_ptr<VAAPI::CVaapiTexture> m_vaapiTextures[NUM_BUFFERS];
+ std::array<std::unique_ptr<KODI::UTILS::EGL::CEGLFence>, NUM_BUFFERS> m_fences;
+ static VAAPI::IVaapiWinSystem *m_pWinSystem;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVDPAU.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVDPAU.cpp
new file mode 100644
index 0000000..048323c
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVDPAU.cpp
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2007-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 "RendererVDPAU.h"
+
+#include "../RenderFactory.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/VDPAU.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+
+using namespace VDPAU;
+
+CBaseRenderer* CRendererVDPAU::Create(CVideoBuffer *buffer)
+{
+ CVdpauRenderPicture *vb = dynamic_cast<CVdpauRenderPicture*>(buffer);
+ if (vb)
+ return new CRendererVDPAU();
+
+ return nullptr;
+}
+
+bool CRendererVDPAU::Register()
+{
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("vdpau", CRendererVDPAU::Create);
+ return true;
+}
+
+CRendererVDPAU::CRendererVDPAU() = default;
+
+CRendererVDPAU::~CRendererVDPAU()
+{
+ for (int i = 0; i < NUM_BUFFERS; ++i)
+ {
+ DeleteTexture(i);
+ }
+ m_interopState.Finish();
+}
+
+bool CRendererVDPAU::Configure(const VideoPicture &picture, float fps, unsigned int orientation)
+{
+ CVdpauRenderPicture *pic = dynamic_cast<CVdpauRenderPicture*>(picture.videoBuffer);
+ if (pic->procPic.isYuv)
+ m_isYuv = true;
+ else
+ m_isYuv = false;
+
+ if (!m_interopState.Init(pic->device, pic->procFunc, pic->ident))
+ return false;
+
+ for (auto &tex : m_vdpauTextures)
+ {
+ tex.Init(m_interopState.GetInterop());
+ }
+ for (auto &fence : m_fences)
+ {
+ fence = {};
+ }
+
+ return CLinuxRendererGL::Configure(picture, fps, orientation);
+}
+
+bool CRendererVDPAU::ConfigChanged(const VideoPicture &picture)
+{
+ CVdpauRenderPicture *pic = dynamic_cast<CVdpauRenderPicture*>(picture.videoBuffer);
+ if (pic->procPic.isYuv && !m_isYuv)
+ return true;
+
+ if (m_interopState.NeedInit(pic->device, pic->procFunc, pic->ident))
+ return true;
+
+ return false;
+}
+
+bool CRendererVDPAU::NeedBuffer(int idx)
+{
+ if (glIsSync(m_fences[idx]))
+ {
+ GLint state;
+ GLsizei length;
+ glGetSynciv(m_fences[idx], GL_SYNC_STATUS, 1, &length, &state);
+ if (state == GL_SIGNALED)
+ {
+ glDeleteSync(m_fences[idx]);
+ m_fences[idx] = {};
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CRendererVDPAU::Flush(bool saveBuffers)
+{
+ for (int i = 0; i < NUM_BUFFERS; i++)
+ m_vdpauTextures[i].Unmap();
+
+ return CLinuxRendererGL::Flush(saveBuffers);
+}
+
+
+void CRendererVDPAU::ReleaseBuffer(int idx)
+{
+ if (glIsSync(m_fences[idx]))
+ {
+ glDeleteSync(m_fences[idx]);
+ m_fences[idx] = {};
+ }
+ m_vdpauTextures[idx].Unmap();
+ CLinuxRendererGL::ReleaseBuffer(idx);
+}
+
+bool CRendererVDPAU::Supports(ERENDERFEATURE feature) const
+{
+ if(feature == RENDERFEATURE_BRIGHTNESS ||
+ feature == RENDERFEATURE_CONTRAST)
+ {
+ if (!m_isYuv && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE))
+ return true;
+
+ return (m_renderMethod & RENDER_GLSL);
+ }
+ else if (feature == RENDERFEATURE_NOISE ||
+ feature == RENDERFEATURE_SHARPNESS)
+ {
+ if (!m_isYuv)
+ return true;
+ }
+ else if (feature == RENDERFEATURE_STRETCH ||
+ feature == RENDERFEATURE_ZOOM ||
+ feature == RENDERFEATURE_VERTICAL_SHIFT ||
+ feature == RENDERFEATURE_PIXEL_RATIO ||
+ feature == RENDERFEATURE_POSTPROCESS ||
+ feature == RENDERFEATURE_ROTATION ||
+ feature == RENDERFEATURE_NONLINSTRETCH)
+ return true;
+
+ return false;
+}
+
+bool CRendererVDPAU::Supports(ESCALINGMETHOD method) const
+{
+ if (m_isYuv)
+ return CLinuxRendererGL::Supports(method);
+
+ if (method == VS_SCALINGMETHOD_NEAREST)
+ return true;
+
+ if (method == VS_SCALINGMETHOD_LINEAR ||
+ method == VS_SCALINGMETHOD_AUTO)
+ return true;
+
+ if(method == VS_SCALINGMETHOD_CUBIC_B_SPLINE
+ || method == VS_SCALINGMETHOD_CUBIC_MITCHELL
+ || method == VS_SCALINGMETHOD_CUBIC_CATMULL
+ || method == VS_SCALINGMETHOD_CUBIC_0_075
+ || method == VS_SCALINGMETHOD_CUBIC_0_1
+ || method == VS_SCALINGMETHOD_LANCZOS2
+ || method == VS_SCALINGMETHOD_SPLINE36_FAST
+ || method == VS_SCALINGMETHOD_LANCZOS3_FAST
+ || method == VS_SCALINGMETHOD_SPLINE36
+ || method == VS_SCALINGMETHOD_LANCZOS3)
+ {
+ // if scaling is below level, avoid hq scaling
+ float scaleX = fabs(((float)m_sourceWidth - m_destRect.Width())/m_sourceWidth)*100;
+ float scaleY = fabs(((float)m_sourceHeight - m_destRect.Height())/m_sourceHeight)*100;
+ int minScale = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt("videoplayer.hqscalers");
+ if (scaleX < minScale && scaleY < minScale)
+ return false;
+ }
+
+ return false;
+}
+
+EShaderFormat CRendererVDPAU::GetShaderFormat()
+{
+ EShaderFormat ret = SHADER_NONE;
+
+ if (m_isYuv)
+ ret = SHADER_NV12;
+
+ return ret;
+}
+
+bool CRendererVDPAU::LoadShadersHook()
+{
+ if (!m_isYuv)
+ {
+ CLog::Log(LOGINFO, "GL: Using VDPAU render method");
+ m_renderMethod = RENDER_CUSTOM;
+ m_fullRange = false;
+ return true;
+ }
+ return false;
+}
+
+bool CRendererVDPAU::RenderHook(int idx)
+{
+ UpdateVideoFilter();
+
+ if (m_isYuv)
+ {
+ switch(m_renderQuality)
+ {
+ case RQ_LOW:
+ case RQ_SINGLEPASS:
+ if (m_currentField == FIELD_FULL)
+ RenderProgressiveWeave(idx, m_currentField);
+ else
+ RenderSinglePass(idx, m_currentField);
+ VerifyGLState();
+ break;
+
+ case RQ_MULTIPASS:
+ if (m_currentField == FIELD_FULL)
+ RenderProgressiveWeave(idx, m_currentField);
+ else
+ {
+ RenderToFBO(idx, m_currentField);
+ RenderFromFBO();
+ }
+ VerifyGLState();
+ break;
+ }
+ }
+ else
+ {
+ RenderRGB(idx, m_currentField);
+ }
+
+ return true;
+}
+
+void CRendererVDPAU::AfterRenderHook(int idx)
+{
+ if (glIsSync(m_fences[idx]))
+ {
+ glDeleteSync(m_fences[idx]);
+ m_fences[idx] = None;
+ }
+ m_fences[idx] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+}
+
+bool CRendererVDPAU::CreateTexture(int index)
+{
+ if (!m_isYuv)
+ return CreateVDPAUTexture(index);
+ else
+ return CreateVDPAUTexture420(index);
+}
+
+void CRendererVDPAU::DeleteTexture(int index)
+{
+ ReleaseBuffer(index);
+
+ if (!m_isYuv)
+ DeleteVDPAUTexture(index);
+ else
+ DeleteVDPAUTexture420(index);
+}
+
+bool CRendererVDPAU::UploadTexture(int index)
+{
+ if (!m_isYuv)
+ return UploadVDPAUTexture(index);
+ else
+ return UploadVDPAUTexture420(index);
+}
+
+bool CRendererVDPAU::CreateVDPAUTexture(int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ YuvImage &im = buf.image;
+ CYuvPlane &plane = buf.fields[FIELD_FULL][0];
+
+ DeleteVDPAUTexture(index);
+
+ memset(&im, 0, sizeof(im));
+ plane = {};
+ im.height = m_sourceHeight;
+ im.width = m_sourceWidth;
+
+ plane.texwidth = im.width;
+ plane.texheight = im.height;
+
+ plane.pixpertex_x = 1;
+ plane.pixpertex_y = 1;
+
+ plane.id = 1;
+ return true;
+}
+
+void CRendererVDPAU::DeleteVDPAUTexture(int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ CYuvPlane &plane = buf.fields[FIELD_FULL][0];
+
+ plane.id = 0;
+}
+
+bool CRendererVDPAU::UploadVDPAUTexture(int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ VDPAU::CVdpauRenderPicture *pic = dynamic_cast<VDPAU::CVdpauRenderPicture*>(buf.videoBuffer);
+
+ CYuvPlane &plane = buf.fields[FIELD_FULL][0];
+
+ if (!pic)
+ {
+ return false;
+ }
+
+ if (!m_vdpauTextures[index].Map(pic))
+ return false;
+
+ // in stereoscopic mode sourceRect may only
+ // be a part of the source video surface
+ plane.rect = m_sourceRect;
+
+ // clip rect
+ if (pic->crop.x1 > plane.rect.x1)
+ plane.rect.x1 = pic->crop.x1;
+ if (pic->crop.x2 < plane.rect.x2)
+ plane.rect.x2 = pic->crop.x2;
+ if (pic->crop.y1 > plane.rect.y1)
+ plane.rect.y1 = pic->crop.y1;
+ if (pic->crop.y2 < plane.rect.y2)
+ plane.rect.y2 = pic->crop.y2;
+
+ plane.texheight = m_vdpauTextures[index].m_texHeight;
+ plane.texwidth = m_vdpauTextures[index].m_texWidth;
+
+ if (m_textureTarget == GL_TEXTURE_2D)
+ {
+ plane.rect.y1 /= plane.texheight;
+ plane.rect.y2 /= plane.texheight;
+ plane.rect.x1 /= plane.texwidth;
+ plane.rect.x2 /= plane.texwidth;
+ }
+
+ // set texture
+ plane.id = m_vdpauTextures[index].m_texture;
+
+ return true;
+}
+
+bool CRendererVDPAU::CreateVDPAUTexture420(int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ YuvImage &im = buf.image;
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = buf.fields[0];
+ GLuint *pbo = buf.pbo;
+
+ DeleteVDPAUTexture420(index);
+
+ memset(&im, 0, sizeof(im));
+ memset(&planes, 0, sizeof(CYuvPlane[YuvImage::MAX_PLANES]));
+
+ im.cshift_x = 1;
+ im.cshift_y = 1;
+
+ im.plane[0] = nullptr;
+ im.plane[1] = nullptr;
+ im.plane[2] = nullptr;
+
+ for(int p=0; p<3; p++)
+ {
+ pbo[p] = None;
+ }
+
+ planes[0].id = 1;
+
+ return true;
+}
+
+void CRendererVDPAU::DeleteVDPAUTexture420(int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+
+ buf.fields[0][0].id = 0;
+ buf.fields[1][0].id = 0;
+ buf.fields[1][1].id = 0;
+ buf.fields[2][0].id = 0;
+ buf.fields[2][1].id = 0;
+}
+
+bool CRendererVDPAU::UploadVDPAUTexture420(int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ YuvImage &im = buf.image;
+
+ VDPAU::CVdpauRenderPicture *pic = dynamic_cast<VDPAU::CVdpauRenderPicture*>(buf.videoBuffer);
+
+ if (!pic)
+ {
+ return false;
+ }
+
+ if (!m_vdpauTextures[index].Map(pic))
+ return false;
+
+ im.height = m_vdpauTextures[index].m_texHeight;
+ im.width = m_vdpauTextures[index].m_texWidth;
+
+ // YUV
+ for (int f = FIELD_TOP; f<=FIELD_BOT ; f++)
+ {
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = buf.fields[f];
+
+ planes[0].texwidth = im.width;
+ planes[0].texheight = im.height >> 1;
+
+ planes[1].texwidth = planes[0].texwidth >> im.cshift_x;
+ planes[1].texheight = planes[0].texheight >> im.cshift_y;
+ planes[2].texwidth = planes[1].texwidth;
+ planes[2].texheight = planes[1].texheight;
+
+ for (int p = 0; p < 3; p++)
+ {
+ planes[p].pixpertex_x = 1;
+ planes[p].pixpertex_y = 1;
+ }
+ }
+ // crop
+// m_sourceRect.x1 += vdpau->crop.x1;
+// m_sourceRect.x2 -= vdpau->crop.x2;
+// m_sourceRect.y1 += vdpau->crop.y1;
+// m_sourceRect.y2 -= vdpau->crop.y2;
+
+ // set textures
+ buf.fields[1][0].id = m_vdpauTextures[index].m_textureTopY;
+ buf.fields[1][1].id = m_vdpauTextures[index].m_textureTopUV;
+ buf.fields[1][2].id = m_vdpauTextures[index].m_textureTopUV;
+ buf.fields[2][0].id = m_vdpauTextures[index].m_textureBotY;
+ buf.fields[2][1].id = m_vdpauTextures[index].m_textureBotUV;
+ buf.fields[2][2].id = m_vdpauTextures[index].m_textureBotUV;
+
+ for (int f = FIELD_TOP; f <= FIELD_BOT; f++)
+ {
+ for (int p=0; p<2; p++)
+ {
+ glBindTexture(m_textureTarget, buf.fields[f][p].id);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glBindTexture(m_textureTarget,0);
+ VerifyGLState();
+ }
+ }
+ CalculateTextureSourceRects(index, 3);
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVDPAU.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVDPAU.h
new file mode 100644
index 0000000..71cac3d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVDPAU.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2007-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 "VdpauGL.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h"
+
+class CRendererVDPAU : public CLinuxRendererGL
+{
+public:
+ CRendererVDPAU();
+ ~CRendererVDPAU() override;
+
+ static CBaseRenderer* Create(CVideoBuffer *buffer);
+ static bool Register();
+
+ bool Configure(const VideoPicture &picture, float fps, unsigned int orientation) override;
+
+ // Player functions
+ void ReleaseBuffer(int idx) override;
+ bool ConfigChanged(const VideoPicture &picture) override;
+ bool NeedBuffer(int idx) override;
+ bool Flush(bool saveBuffers) override;
+
+ // Feature support
+ bool Supports(ERENDERFEATURE feature) const override;
+ bool Supports(ESCALINGMETHOD method) const override;
+
+protected:
+ bool LoadShadersHook() override;
+ bool RenderHook(int idx) override;
+ void AfterRenderHook(int idx) override;
+
+ // textures
+ bool UploadTexture(int index) override;
+ void DeleteTexture(int index) override;
+ bool CreateTexture(int index) override;
+
+ bool CreateVDPAUTexture(int index);
+ void DeleteVDPAUTexture(int index);
+ bool UploadVDPAUTexture(int index);
+
+ bool CreateVDPAUTexture420(int index);
+ void DeleteVDPAUTexture420(int index);
+ bool UploadVDPAUTexture420(int index);
+
+ EShaderFormat GetShaderFormat() override;
+
+ bool CanSaveBuffers() override { return false; }
+
+ bool m_isYuv = false;
+
+ VDPAU::CInteropState m_interopState;
+ VDPAU::CVdpauTexture m_vdpauTextures[NUM_BUFFERS];
+ GLsync m_fences[NUM_BUFFERS];
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.cpp
new file mode 100644
index 0000000..0069425
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.cpp
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2007-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 "RendererVTBGL.h"
+
+#include "../RenderFactory.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/VTB.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+#if defined(HAS_SDL)
+#include "windowing/osx/SDL/WinSystemOSXSDL.h"
+#else
+#include "windowing/osx/WinSystemOSX.h"
+#endif
+
+#include "platform/darwin/osx/CocoaInterface.h"
+
+#include <CoreVideo/CoreVideo.h>
+#include <OpenGL/CGLIOSurface.h>
+
+CBaseRenderer* CRendererVTB::Create(CVideoBuffer *buffer)
+{
+ VTB::CVideoBufferVTB *vb = dynamic_cast<VTB::CVideoBufferVTB*>(buffer);
+ if (vb)
+ return new CRendererVTB();
+
+ return nullptr;
+}
+
+bool CRendererVTB::Register()
+{
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("vtbgl", CRendererVTB::Create);
+ return true;
+}
+
+CRendererVTB::~CRendererVTB()
+{
+ for (int i = 0; i < NUM_BUFFERS; ++i)
+ {
+ ReleaseBuffer(i);
+ DeleteTexture(i);
+ }
+}
+
+void CRendererVTB::ReleaseBuffer(int idx)
+{
+ CPictureBuffer &buf = m_buffers[idx];
+ if (buf.videoBuffer)
+ {
+ VTB::CVideoBufferVTB *vb = dynamic_cast<VTB::CVideoBufferVTB*>(buf.videoBuffer);
+ if (vb)
+ {
+ if (vb->m_fence && glIsFenceAPPLE(vb->m_fence))
+ {
+ glDeleteFencesAPPLE(1, &vb->m_fence);
+ vb->m_fence = 0;
+ }
+ }
+ vb->Release();
+ buf.videoBuffer = nullptr;
+ }
+}
+
+EShaderFormat CRendererVTB::GetShaderFormat()
+{
+ return SHADER_NV12;
+}
+
+bool CRendererVTB::LoadShadersHook()
+{
+ CLog::Log(LOGINFO, "GL: Using CVBREF render method");
+ m_textureTarget = GL_TEXTURE_RECTANGLE;
+ return false;
+}
+
+bool CRendererVTB::CreateTexture(int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ YuvImage &im = buf.image;
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = buf.fields[0];
+
+ ReleaseBuffer(index);
+ DeleteTexture(index);
+
+ memset(&im , 0, sizeof(im));
+ memset(&planes, 0, sizeof(CYuvPlane[YuvImage::MAX_PLANES]));
+
+ im.bpp = 1;
+ im.width = m_sourceWidth;
+ im.height = m_sourceHeight;
+ im.cshift_x = 1;
+ im.cshift_y = 1;
+
+ planes[0].texwidth = im.width;
+ planes[0].texheight = im.height;
+
+ planes[1].texwidth = planes[0].texwidth >> im.cshift_x;
+ planes[1].texheight = planes[0].texheight >> im.cshift_y;
+ planes[2].texwidth = planes[1].texwidth;
+ planes[2].texheight = planes[1].texheight;
+
+ for (int p = 0; p < 3; p++)
+ {
+ planes[p].pixpertex_x = 1;
+ planes[p].pixpertex_y = 1;
+ }
+
+ glGenTextures(1, &planes[0].id);
+ glGenTextures(1, &planes[1].id);
+ planes[2].id = planes[1].id;
+
+ return true;
+}
+
+void CRendererVTB::DeleteTexture(int index)
+{
+ CPictureBuffer& buf = m_buffers[index];
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = buf.fields[0];
+ buf.loaded = false;
+
+ if (planes[0].id && glIsTexture(planes[0].id))
+ {
+ glDeleteTextures(1, &planes[0].id);
+ }
+ if (planes[1].id && glIsTexture(planes[1].id))
+ {
+ glDeleteTextures(1, &planes[1].id);
+ }
+ planes[0].id = 0;
+ planes[1].id = 0;
+ planes[2].id = 0;
+}
+
+bool CRendererVTB::UploadTexture(int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = m_buffers[index].fields[0];
+
+ VTB::CVideoBufferVTB *vb = dynamic_cast<VTB::CVideoBufferVTB*>(buf.videoBuffer);
+ if (!vb)
+ {
+ return false;
+ }
+
+ CVImageBufferRef cvBufferRef = vb->GetPB();
+
+ // It is the fastest way to render a CVPixelBuffer backed
+ // with an IOSurface as there is no CPU -> GPU upload.
+ CWinSystemOSX* winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem());
+ CGLContextObj cgl_ctx = (CGLContextObj)winSystem->GetCGLContextObj();
+ IOSurfaceRef surface = CVPixelBufferGetIOSurface(cvBufferRef);
+ OSType format_type = IOSurfaceGetPixelFormat(surface);
+
+ if (format_type != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)
+ {
+ return false;
+ }
+
+ GLsizei surfplanes = IOSurfaceGetPlaneCount(surface);
+
+ if (surfplanes != 2)
+ {
+ return false;
+ }
+
+ GLsizei widthY = IOSurfaceGetWidthOfPlane(surface, 0);
+ GLsizei widthUV = IOSurfaceGetWidthOfPlane(surface, 1);
+ GLsizei heightY = IOSurfaceGetHeightOfPlane(surface, 0);
+ GLsizei heightUV = IOSurfaceGetHeightOfPlane(surface, 1);
+
+ glBindTexture(m_textureTarget, planes[0].id);
+
+ CGLTexImageIOSurface2D(cgl_ctx, m_textureTarget, GL_RED,
+ widthY, heightY, GL_RED, GL_UNSIGNED_BYTE, surface, 0);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glBindTexture(m_textureTarget, planes[1].id);
+
+ CGLTexImageIOSurface2D(cgl_ctx, m_textureTarget, GL_RG,
+ widthUV, heightUV, GL_RG, GL_UNSIGNED_BYTE, surface, 1);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glBindTexture(m_textureTarget, 0);
+
+ CalculateTextureSourceRects(index, 3);
+
+ return true;
+}
+
+void CRendererVTB::AfterRenderHook(int idx)
+{
+ CPictureBuffer &buf = m_buffers[idx];
+ VTB::CVideoBufferVTB *vb = dynamic_cast<VTB::CVideoBufferVTB*>(buf.videoBuffer);
+ if (!vb)
+ {
+ return;
+ }
+
+ if (vb->m_fence && glIsFenceAPPLE(vb->m_fence))
+ {
+ glDeleteFencesAPPLE(1, &vb->m_fence);
+ }
+ glGenFencesAPPLE(1, &vb->m_fence);
+ glSetFenceAPPLE(vb->m_fence);
+}
+
+bool CRendererVTB::NeedBuffer(int idx)
+{
+ CPictureBuffer &buf = m_buffers[idx];
+ VTB::CVideoBufferVTB *vb = dynamic_cast<VTB::CVideoBufferVTB*>(buf.videoBuffer);
+ if (!vb)
+ {
+ return false;
+ }
+
+ if (vb->m_fence && glIsFenceAPPLE(vb->m_fence))
+ {
+ if (!glTestFenceAPPLE(vb->m_fence))
+ return true;
+ }
+
+ return false;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.h
new file mode 100644
index 0000000..ba2cdb0
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2007-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/VideoPlayer/VideoRenderers/LinuxRendererGL.h"
+
+
+class CRendererVTB : public CLinuxRendererGL
+{
+public:
+ CRendererVTB() = default;
+ ~CRendererVTB() override;
+
+ static CBaseRenderer* Create(CVideoBuffer *buffer);
+ static bool Register();
+
+ // Player functions
+ void ReleaseBuffer(int idx) override;
+ bool NeedBuffer(int idx) override;
+
+protected:
+ bool LoadShadersHook() override;
+ void AfterRenderHook(int idx) override;
+ EShaderFormat GetShaderFormat() override;
+
+ // textures
+ bool UploadTexture(int index) override;
+ void DeleteTexture(int index) override;
+ bool CreateTexture(int index) override;
+};
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.cpp
new file mode 100644
index 0000000..4515a9a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.cpp
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2007-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 "RendererVTBGLES.h"
+
+#include "../RenderFactory.h"
+#include "ServiceBroker.h"
+#include "cores/IPlayer.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/VTB.h"
+#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>
+#include <OpenGLES/ES2/glext.h>
+
+CBaseRenderer* CRendererVTB::Create(CVideoBuffer *buffer)
+{
+ VTB::CVideoBufferVTB *vb = dynamic_cast<VTB::CVideoBufferVTB*>(buffer);
+ if (vb)
+ return new CRendererVTB();
+
+ return nullptr;
+}
+
+bool CRendererVTB::Register()
+{
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("vtbgles", CRendererVTB::Create);
+ return true;
+}
+
+CRendererVTB::CRendererVTB()
+{
+ auto winSystem = dynamic_cast<WIN_SYSTEM_CLASS*>(CServiceBroker::GetWinSystem());
+ m_glContext = winSystem->GetEAGLContextObj();
+ CVReturn ret = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault,
+ NULL,
+ m_glContext,
+ NULL,
+ &m_textureCache);
+ if (ret != kCVReturnSuccess)
+ {
+ CLog::Log(LOGERROR, "CRendererVTB::CRendererVTB - Error creating texture cache (err: {})", ret);
+ }
+
+ for (auto &buf : m_vtbBuffers)
+ {
+ buf.m_textureY = nullptr;
+ buf.m_textureUV = nullptr;
+ buf.m_videoBuffer = nullptr;
+ buf.m_fence = nullptr;
+ }
+}
+
+CRendererVTB::~CRendererVTB()
+{
+ if (m_textureCache)
+ CFRelease(m_textureCache);
+
+ for (int i = 0; i < NUM_BUFFERS; ++i)
+ {
+ DeleteTexture(i);
+ }
+}
+
+void CRendererVTB::ReleaseBuffer(int idx)
+{
+ CPictureBuffer &buf = m_buffers[idx];
+ CRenderBuffer &renderBuf = m_vtbBuffers[idx];
+ if (buf.videoBuffer)
+ {
+ if (renderBuf.m_fence && glIsSyncAPPLE(renderBuf.m_fence))
+ {
+ glDeleteSyncAPPLE(renderBuf.m_fence);
+ renderBuf.m_fence = 0;
+ }
+ buf.videoBuffer->Release();
+ buf.videoBuffer = nullptr;
+ }
+}
+
+EShaderFormat CRendererVTB::GetShaderFormat()
+{
+ return SHADER_YV12;
+}
+
+bool CRendererVTB::LoadShadersHook()
+{
+ CLog::Log(LOGINFO, "GL: Using CVBREF render method");
+ m_textureTarget = GL_TEXTURE_2D;
+ m_renderMethod = RENDER_CUSTOM;
+
+ if (!m_textureCache)
+ {
+ CLog::Log(LOGINFO, "CRendererVTB::LoadShadersHook: no texture cache");
+ return false;
+ }
+
+ CVReturn ret = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault,
+ NULL,
+ m_glContext,
+ NULL,
+ &m_textureCache);
+ if (ret != kCVReturnSuccess)
+ return false;
+
+ return false;
+}
+
+bool CRendererVTB::CreateTexture(int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ YuvImage &im = buf.image;
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = buf.fields[0];
+
+ DeleteTexture(index);
+
+ memset(&im , 0, sizeof(im));
+ memset(&planes, 0, sizeof(CYuvPlane[YuvImage::MAX_PLANES]));
+
+ im.height = m_sourceHeight;
+ im.width = m_sourceWidth;
+
+ planes[0].texwidth = im.width;
+ planes[0].texheight = im.height;
+ planes[1].texwidth = planes[0].texwidth >> im.cshift_x;
+ planes[1].texheight = planes[0].texheight >> im.cshift_y;
+ planes[2].texwidth = planes[1].texwidth;
+ planes[2].texheight = planes[1].texheight;
+
+ for (int p = 0; p < 3; p++)
+ {
+ planes[p].pixpertex_x = 1;
+ planes[p].pixpertex_y = 1;
+ }
+
+ planes[0].id = 1;
+ return true;
+}
+
+void CRendererVTB::DeleteTexture(int index)
+{
+ CRenderBuffer &renderBuf = m_vtbBuffers[index];
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = m_buffers[index].fields[0];
+
+ if (renderBuf.m_textureY)
+ CFRelease(renderBuf.m_textureY);
+ renderBuf.m_textureY = nullptr;
+
+ if (renderBuf.m_textureUV)
+ CFRelease(renderBuf.m_textureUV);
+ renderBuf.m_textureUV = nullptr;
+
+ ReleaseBuffer(index);
+
+ planes[0].id = 0;
+ planes[1].id = 0;
+ planes[2].id = 0;
+}
+
+bool CRendererVTB::UploadTexture(int index)
+{
+ CRenderBuffer &renderBuf = m_vtbBuffers[index];
+ CPictureBuffer &buf = m_buffers[index];
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = m_buffers[index].fields[0];
+ YuvImage &im = m_buffers[index].image;
+
+ VTB::CVideoBufferVTB *vb = dynamic_cast<VTB::CVideoBufferVTB*>(buf.videoBuffer);
+ if (!vb)
+ {
+ return false;
+ }
+
+ CVOpenGLESTextureCacheFlush(m_textureCache, 0);
+
+ if (renderBuf.m_textureY)
+ CFRelease(renderBuf.m_textureY);
+ renderBuf.m_textureY = nullptr;
+
+ if (renderBuf.m_textureUV)
+ CFRelease(renderBuf.m_textureUV);
+ renderBuf.m_textureUV = nullptr;
+
+ CVReturn ret;
+ ret = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
+ m_textureCache,
+ vb->GetPB(), nullptr, GL_TEXTURE_2D, GL_LUMINANCE,
+ im.width, im.height, GL_LUMINANCE, GL_UNSIGNED_BYTE,
+ 0,
+ &renderBuf.m_textureY);
+
+ if (ret != kCVReturnSuccess)
+ {
+ CLog::Log(LOGERROR, "CRendererVTB::UploadTexture - Error uploading texture Y (err: {})", ret);
+ return false;
+ }
+
+ ret = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
+ m_textureCache,
+ vb->GetPB(), nullptr, GL_TEXTURE_2D, GL_LUMINANCE_ALPHA,
+ im.width/2, im.height/2, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE,
+ 1,
+ &renderBuf.m_textureUV);
+
+ if (ret != kCVReturnSuccess)
+ {
+ CLog::Log(LOGERROR, "CRendererVTB::UploadTexture - Error uploading texture UV (err: {})", ret);
+ return false;
+ }
+
+ // set textures
+ planes[0].id = CVOpenGLESTextureGetName(renderBuf.m_textureY);
+ planes[1].id = CVOpenGLESTextureGetName(renderBuf.m_textureUV);
+ planes[2].id = CVOpenGLESTextureGetName(renderBuf.m_textureUV);
+
+ for (int p=0; p<2; p++)
+ {
+ glBindTexture(m_textureTarget, planes[p].id);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ glBindTexture(m_textureTarget, 0);
+ VerifyGLState();
+ }
+
+ CalculateTextureSourceRects(index, 3);
+ return true;
+}
+
+void CRendererVTB::AfterRenderHook(int idx)
+{
+ CRenderBuffer &renderBuf = m_vtbBuffers[idx];
+ if (renderBuf.m_fence && glIsSyncAPPLE(renderBuf.m_fence))
+ {
+ glDeleteSyncAPPLE(renderBuf.m_fence);
+ }
+ renderBuf.m_fence = glFenceSyncAPPLE(GL_SYNC_GPU_COMMANDS_COMPLETE_APPLE, 0);
+}
+
+bool CRendererVTB::NeedBuffer(int idx)
+{
+ CRenderBuffer &renderBuf = m_vtbBuffers[idx];
+ if (renderBuf.m_fence && glIsSyncAPPLE(renderBuf.m_fence))
+ {
+ int syncState = GL_UNSIGNALED_APPLE;
+ glGetSyncivAPPLE(renderBuf.m_fence, GL_SYNC_STATUS_APPLE, 1, nullptr, &syncState);
+ if (syncState != GL_SIGNALED_APPLE)
+ return true;
+ }
+
+ return false;
+}
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.h
new file mode 100644
index 0000000..8be4926
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2007-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/VideoPlayer/VideoRenderers/LinuxRendererGLES.h"
+
+#include <CoreVideo/CVOpenGLESTextureCache.h>
+
+class CRendererVTB : public CLinuxRendererGLES
+{
+public:
+ CRendererVTB();
+ ~CRendererVTB() override;
+
+ static CBaseRenderer* Create(CVideoBuffer *buffer);
+ static bool Register();
+
+ // Player functions
+ void ReleaseBuffer(int idx) override;
+ bool NeedBuffer(int idx) override;
+
+protected:
+ // hooks for hw dec renderer
+ bool LoadShadersHook() override;
+ void AfterRenderHook(int idx) override;
+ EShaderFormat GetShaderFormat() override;
+
+ // textures
+ bool UploadTexture(int index) override;
+ void DeleteTexture(int index) override;
+ bool CreateTexture(int index) override;
+
+ CVOpenGLESTextureCacheRef m_textureCache = nullptr;
+ struct CRenderBuffer
+ {
+ CVOpenGLESTextureRef m_textureY;
+ CVOpenGLESTextureRef m_textureUV;
+ CVBufferRef m_videoBuffer;
+ GLsync m_fence;
+ };
+ CRenderBuffer m_vtbBuffers[NUM_BUFFERS];
+ CVEAGLContext m_glContext;
+};
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VaapiEGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VaapiEGL.cpp
new file mode 100644
index 0000000..5ee5891
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VaapiEGL.cpp
@@ -0,0 +1,691 @@
+/*
+ * Copyright (C) 2007-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 "VaapiEGL.h"
+
+#include "cores/VideoPlayer/DVDCodecs/Video/VAAPI.h"
+#include "utils/EGLUtils.h"
+#include "utils/log.h"
+
+#include <drm_fourcc.h>
+#include <va/va_drmcommon.h>
+
+#define HAVE_VAEXPORTSURFACHEHANDLE VA_CHECK_VERSION(1, 1, 0)
+
+using namespace VAAPI;
+
+void CVaapi1Texture::Init(InteropInfo &interop)
+{
+ m_interop = interop;
+}
+
+bool CVaapi1Texture::Map(CVaapiRenderPicture *pic)
+{
+ VAStatus status;
+
+ if (m_vaapiPic)
+ return true;
+
+ vaSyncSurface(pic->vadsp, pic->procPic.videoSurface);
+
+ status = vaDeriveImage(pic->vadsp, pic->procPic.videoSurface, &m_glSurface.vaImage);
+ if (status != VA_STATUS_SUCCESS)
+ {
+ CLog::Log(LOGERROR, "CVaapiTexture::{} - Error: {}({})", __FUNCTION__, vaErrorStr(status),
+ status);
+ return false;
+ }
+ memset(&m_glSurface.vBufInfo, 0, sizeof(m_glSurface.vBufInfo));
+ m_glSurface.vBufInfo.mem_type = VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME;
+ status = vaAcquireBufferHandle(pic->vadsp, m_glSurface.vaImage.buf, &m_glSurface.vBufInfo);
+ if (status != VA_STATUS_SUCCESS)
+ {
+ CLog::Log(LOGERROR, "CVaapiTexture::{} - Error: {}({})", __FUNCTION__, vaErrorStr(status),
+ status);
+ return false;
+ }
+
+ m_texWidth = m_glSurface.vaImage.width;
+ m_texHeight = m_glSurface.vaImage.height;
+
+ GLint attribs[23], *attrib;
+
+ switch (m_glSurface.vaImage.format.fourcc)
+ {
+ case VA_FOURCC('N','V','1','2'):
+ {
+ attrib = attribs;
+ *attrib++ = EGL_LINUX_DRM_FOURCC_EXT;
+ *attrib++ = fourcc_code('R', '8', ' ', ' ');
+ *attrib++ = EGL_WIDTH;
+ *attrib++ = m_glSurface.vaImage.width;
+ *attrib++ = EGL_HEIGHT;
+ *attrib++ = m_glSurface.vaImage.height;
+ *attrib++ = EGL_DMA_BUF_PLANE0_FD_EXT;
+ *attrib++ = (intptr_t)m_glSurface.vBufInfo.handle;
+ *attrib++ = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
+ *attrib++ = m_glSurface.vaImage.offsets[0];
+ *attrib++ = EGL_DMA_BUF_PLANE0_PITCH_EXT;
+ *attrib++ = m_glSurface.vaImage.pitches[0];
+ *attrib++ = EGL_NONE;
+ m_glSurface.eglImageY = m_interop.eglCreateImageKHR(m_interop.eglDisplay,
+ EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)NULL,
+ attribs);
+ if (!m_glSurface.eglImageY)
+ {
+ EGLint err = eglGetError();
+ CLog::Log(LOGERROR, "failed to import VA buffer NV12 into EGL image: {}", err);
+ return false;
+ }
+
+ attrib = attribs;
+ *attrib++ = EGL_LINUX_DRM_FOURCC_EXT;
+ *attrib++ = fourcc_code('G', 'R', '8', '8');
+ *attrib++ = EGL_WIDTH;
+ *attrib++ = (m_glSurface.vaImage.width + 1) >> 1;
+ *attrib++ = EGL_HEIGHT;
+ *attrib++ = (m_glSurface.vaImage.height + 1) >> 1;
+ *attrib++ = EGL_DMA_BUF_PLANE0_FD_EXT;
+ *attrib++ = (intptr_t)m_glSurface.vBufInfo.handle;
+ *attrib++ = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
+ *attrib++ = m_glSurface.vaImage.offsets[1];
+ *attrib++ = EGL_DMA_BUF_PLANE0_PITCH_EXT;
+ *attrib++ = m_glSurface.vaImage.pitches[1];
+ *attrib++ = EGL_NONE;
+ m_glSurface.eglImageVU = m_interop.eglCreateImageKHR(m_interop.eglDisplay,
+ EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)NULL,
+ attribs);
+ if (!m_glSurface.eglImageVU)
+ {
+ EGLint err = eglGetError();
+ CLog::Log(LOGERROR, "failed to import VA buffer NV12 into EGL image: {}", err);
+ return false;
+ }
+
+ glGenTextures(1, &m_textureY);
+ glBindTexture(m_interop.textureTarget, m_textureY);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ m_interop.glEGLImageTargetTexture2DOES(m_interop.textureTarget, m_glSurface.eglImageY);
+
+ glGenTextures(1, &m_textureVU);
+ glBindTexture(m_interop.textureTarget, m_textureVU);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ m_interop.glEGLImageTargetTexture2DOES(m_interop.textureTarget, m_glSurface.eglImageVU);
+
+ glBindTexture(m_interop.textureTarget, 0);
+
+ break;
+ }
+ case VA_FOURCC('P','0','1','0'):
+ {
+ attrib = attribs;
+ *attrib++ = EGL_LINUX_DRM_FOURCC_EXT;
+ *attrib++ = fourcc_code('R', '1', '6', ' ');
+ *attrib++ = EGL_WIDTH;
+ *attrib++ = m_glSurface.vaImage.width;
+ *attrib++ = EGL_HEIGHT;
+ *attrib++ = m_glSurface.vaImage.height;
+ *attrib++ = EGL_DMA_BUF_PLANE0_FD_EXT;
+ *attrib++ = (intptr_t)m_glSurface.vBufInfo.handle;
+ *attrib++ = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
+ *attrib++ = m_glSurface.vaImage.offsets[0];
+ *attrib++ = EGL_DMA_BUF_PLANE0_PITCH_EXT;
+ *attrib++ = m_glSurface.vaImage.pitches[0];
+ *attrib++ = EGL_NONE;
+ m_glSurface.eglImageY = m_interop.eglCreateImageKHR(m_interop.eglDisplay,
+ EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)NULL,
+ attribs);
+ if (!m_glSurface.eglImageY)
+ {
+ EGLint err = eglGetError();
+ CLog::Log(LOGERROR, "failed to import VA buffer P010 into EGL image: {}", err);
+ return false;
+ }
+
+ attrib = attribs;
+ *attrib++ = EGL_LINUX_DRM_FOURCC_EXT;
+ *attrib++ = fourcc_code('G', 'R', '3', '2');
+ *attrib++ = EGL_WIDTH;
+ *attrib++ = (m_glSurface.vaImage.width + 1) >> 1;
+ *attrib++ = EGL_HEIGHT;
+ *attrib++ = (m_glSurface.vaImage.height + 1) >> 1;
+ *attrib++ = EGL_DMA_BUF_PLANE0_FD_EXT;
+ *attrib++ = (intptr_t)m_glSurface.vBufInfo.handle;
+ *attrib++ = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
+ *attrib++ = m_glSurface.vaImage.offsets[1];
+ *attrib++ = EGL_DMA_BUF_PLANE0_PITCH_EXT;
+ *attrib++ = m_glSurface.vaImage.pitches[1];
+ *attrib++ = EGL_NONE;
+ m_glSurface.eglImageVU = m_interop.eglCreateImageKHR(m_interop.eglDisplay,
+ EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)NULL,
+ attribs);
+ if (!m_glSurface.eglImageVU)
+ {
+ EGLint err = eglGetError();
+ CLog::Log(LOGERROR, "failed to import VA buffer P010 into EGL image: {}", err);
+ return false;
+ }
+
+ glGenTextures(1, &m_textureY);
+ glBindTexture(m_interop.textureTarget, m_textureY);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ m_interop.glEGLImageTargetTexture2DOES(m_interop.textureTarget, m_glSurface.eglImageY);
+
+ glGenTextures(1, &m_textureVU);
+ glBindTexture(m_interop.textureTarget, m_textureVU);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ m_interop.glEGLImageTargetTexture2DOES(m_interop.textureTarget, m_glSurface.eglImageVU);
+
+ glBindTexture(m_interop.textureTarget, 0);
+
+ break;
+ }
+ case VA_FOURCC('B','G','R','A'):
+ {
+ attrib = attribs;
+ *attrib++ = EGL_DRM_BUFFER_FORMAT_MESA;
+ *attrib++ = EGL_DRM_BUFFER_FORMAT_ARGB32_MESA;
+ *attrib++ = EGL_WIDTH;
+ *attrib++ = m_glSurface.vaImage.width;
+ *attrib++ = EGL_HEIGHT;
+ *attrib++ = m_glSurface.vaImage.height;
+ *attrib++ = EGL_DRM_BUFFER_STRIDE_MESA;
+ *attrib++ = m_glSurface.vaImage.pitches[0] / 4;
+ *attrib++ = EGL_NONE;
+ m_glSurface.eglImage = m_interop.eglCreateImageKHR(m_interop.eglDisplay, EGL_NO_CONTEXT,
+ EGL_DRM_BUFFER_MESA,
+ (EGLClientBuffer)m_glSurface.vBufInfo.handle,
+ attribs);
+ if (!m_glSurface.eglImage)
+ {
+ EGLint err = eglGetError();
+ CLog::Log(LOGERROR, "failed to import VA buffer BGRA into EGL image: {}", err);
+ return false;
+ }
+
+ glGenTextures(1, &m_texture);
+ glBindTexture(m_interop.textureTarget, m_texture);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_interop.textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ m_interop.glEGLImageTargetTexture2DOES(m_interop.textureTarget, m_glSurface.eglImage);
+
+ glBindTexture(m_interop.textureTarget, 0);
+
+ break;
+ }
+ default:
+ return false;
+ }
+
+ m_vaapiPic = pic;
+ m_vaapiPic->Acquire();
+ return true;
+}
+
+void CVaapi1Texture::Unmap()
+{
+ if (!m_vaapiPic)
+ return;
+
+ if (m_glSurface.vaImage.image_id == VA_INVALID_ID)
+ return;
+
+ m_interop.eglDestroyImageKHR(m_interop.eglDisplay, m_glSurface.eglImageY);
+ m_interop.eglDestroyImageKHR(m_interop.eglDisplay, m_glSurface.eglImageVU);
+
+ VAStatus status;
+ status = vaReleaseBufferHandle(m_vaapiPic->vadsp, m_glSurface.vaImage.buf);
+ if (status != VA_STATUS_SUCCESS)
+ {
+ CLog::Log(LOGERROR, "VAAPI::{} - Error: {}({})", __FUNCTION__, vaErrorStr(status), status);
+ }
+
+ status = vaDestroyImage(m_vaapiPic->vadsp, m_glSurface.vaImage.image_id);
+ if (status != VA_STATUS_SUCCESS)
+ {
+ CLog::Log(LOGERROR, "VAAPI::{} - Error: {}({})", __FUNCTION__, vaErrorStr(status), status);
+ }
+
+ m_glSurface.vaImage.image_id = VA_INVALID_ID;
+
+ glDeleteTextures(1, &m_textureY);
+ glDeleteTextures(1, &m_textureVU);
+
+ m_vaapiPic->Release();
+ m_vaapiPic = nullptr;
+}
+
+GLuint CVaapi1Texture::GetTextureY()
+{
+ return m_textureY;
+}
+
+GLuint CVaapi1Texture::GetTextureVU()
+{
+ return m_textureVU;
+}
+
+CSizeInt CVaapi1Texture::GetTextureSize()
+{
+ return {m_texWidth, m_texHeight};
+}
+
+void CVaapi1Texture::TestInterop(VADisplay vaDpy, EGLDisplay eglDisplay, bool &general, bool &deepColor)
+{
+ general = false;
+ deepColor = false;
+
+ PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
+ PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
+ if (!eglCreateImageKHR || !eglDestroyImageKHR)
+ {
+ return;
+ }
+
+ int width = 1920;
+ int height = 1080;
+
+ // create surfaces
+ VASurfaceID surface;
+ VAStatus status;
+ VAImage image;
+ VABufferInfo bufferInfo;
+
+ if (vaCreateSurfaces(vaDpy, VA_RT_FORMAT_YUV420,
+ width, height,
+ &surface, 1, NULL, 0) != VA_STATUS_SUCCESS)
+ {
+ return;
+ }
+
+ // check interop
+
+ status = vaDeriveImage(vaDpy, surface, &image);
+ if (status == VA_STATUS_SUCCESS)
+ {
+ memset(&bufferInfo, 0, sizeof(bufferInfo));
+ bufferInfo.mem_type = VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME;
+ status = vaAcquireBufferHandle(vaDpy, image.buf, &bufferInfo);
+ if (status == VA_STATUS_SUCCESS)
+ {
+ EGLImageKHR eglImage;
+ EGLint attribs[] = {
+ EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_R8,
+ EGL_WIDTH, image.width,
+ EGL_HEIGHT, image.height,
+ EGL_DMA_BUF_PLANE0_FD_EXT, static_cast<EGLint> (bufferInfo.handle),
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, static_cast<EGLint> (image.offsets[0]),
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, static_cast<EGLint> (image.pitches[0]),
+ EGL_NONE
+ };
+
+ eglImage = eglCreateImageKHR(eglDisplay,
+ EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)NULL,
+ attribs);
+ if (eglImage)
+ {
+ eglDestroyImageKHR(eglDisplay, eglImage);
+ general = true;
+ }
+ }
+ vaDestroyImage(vaDpy, image.image_id);
+ }
+ vaDestroySurfaces(vaDpy, &surface, 1);
+
+ if (general)
+ {
+ deepColor = TestInteropDeepColor(vaDpy, eglDisplay);
+ }
+}
+
+bool CVaapi1Texture::TestInteropDeepColor(VADisplay vaDpy, EGLDisplay eglDisplay)
+{
+ bool ret = false;
+
+ PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
+ PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
+ if (!eglCreateImageKHR || !eglDestroyImageKHR)
+ {
+ return false;
+ }
+
+ int width = 1920;
+ int height = 1080;
+
+ // create surfaces
+ VASurfaceID surface;
+ VAStatus status;
+ VAImage image;
+ VABufferInfo bufferInfo;
+
+ VASurfaceAttrib attribs = {};
+ attribs.flags = VA_SURFACE_ATTRIB_SETTABLE;
+ attribs.type = VASurfaceAttribPixelFormat;
+ attribs.value.type = VAGenericValueTypeInteger;
+ attribs.value.value.i = VA_FOURCC_P010;
+
+ if (vaCreateSurfaces(vaDpy, VA_RT_FORMAT_YUV420_10BPP,
+ width, height,
+ &surface, 1, &attribs, 1) != VA_STATUS_SUCCESS)
+ {
+ return false;
+ }
+
+ // check interop
+ status = vaDeriveImage(vaDpy, surface, &image);
+ if (status == VA_STATUS_SUCCESS)
+ {
+ memset(&bufferInfo, 0, sizeof(bufferInfo));
+ bufferInfo.mem_type = VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME;
+ status = vaAcquireBufferHandle(vaDpy, image.buf, &bufferInfo);
+ if (status == VA_STATUS_SUCCESS)
+ {
+ EGLImageKHR eglImage;
+ EGLint attribs[] = {
+ EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_GR1616,
+ EGL_WIDTH, (image.width + 1) >> 1,
+ EGL_HEIGHT, (image.height + 1) >> 1,
+ EGL_DMA_BUF_PLANE0_FD_EXT, static_cast<EGLint> (bufferInfo.handle),
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, static_cast<EGLint> (image.offsets[1]),
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, static_cast<EGLint> (image.pitches[1]),
+ EGL_NONE
+ };
+
+ eglImage = eglCreateImageKHR(eglDisplay,
+ EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)NULL,
+ attribs);
+ if (eglImage)
+ {
+ eglDestroyImageKHR(eglDisplay, eglImage);
+ ret = true;
+ }
+
+ }
+ vaDestroyImage(vaDpy, image.image_id);
+ }
+
+ vaDestroySurfaces(vaDpy, &surface, 1);
+
+ return ret;
+}
+
+void CVaapi2Texture::Init(InteropInfo& interop)
+{
+ m_interop = interop;
+ m_hasPlaneModifiers = CEGLUtils::HasExtension(m_interop.eglDisplay, "EGL_EXT_image_dma_buf_import_modifiers");
+}
+
+bool CVaapi2Texture::Map(CVaapiRenderPicture* pic)
+{
+#if HAVE_VAEXPORTSURFACHEHANDLE
+ if (m_vaapiPic)
+ return true;
+
+ m_vaapiPic = pic;
+ m_vaapiPic->Acquire();
+
+ VAStatus status;
+
+ VADRMPRIMESurfaceDescriptor surface;
+
+ status = vaExportSurfaceHandle(pic->vadsp, pic->procPic.videoSurface,
+ VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
+ VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS,
+ &surface);
+
+ if (status != VA_STATUS_SUCCESS)
+ {
+ CLog::Log(LOGWARNING, "CVaapi2Texture::Map: vaExportSurfaceHandle failed - Error: {} ({})",
+ vaErrorStr(status), status);
+ return false;
+ }
+
+ // Remember fds to close them later
+ if (surface.num_objects > m_drmFDs.size())
+ throw std::logic_error("Too many fds returned by vaExportSurfaceHandle");
+
+ for (uint32_t object = 0; object < surface.num_objects; object++)
+ {
+ m_drmFDs[object].attach(surface.objects[object].fd);
+ }
+
+ status = vaSyncSurface(pic->vadsp, pic->procPic.videoSurface);
+ if (status != VA_STATUS_SUCCESS)
+ {
+ CLog::Log(LOGERROR, "CVaapi2Texture::Map: vaSyncSurface - Error: {} ({})", vaErrorStr(status),
+ status);
+ return false;
+ }
+
+ m_textureSize.Set(pic->DVDPic.iWidth, pic->DVDPic.iHeight);
+
+ for (uint32_t layerNo = 0; layerNo < surface.num_layers; layerNo++)
+ {
+ int plane = 0;
+ auto const& layer = surface.layers[layerNo];
+ if (layer.num_planes != 1)
+ {
+ CLog::Log(LOGDEBUG,
+ "CVaapi2Texture::Map: DRM-exported layer has {} planes - only 1 supported",
+ layer.num_planes);
+ return false;
+ }
+ auto const& object = surface.objects[layer.object_index[plane]];
+
+ MappedTexture* texture{};
+ EGLint width{m_textureSize.Width()};
+ EGLint height{m_textureSize.Height()};
+
+ switch (surface.num_layers)
+ {
+ case 2:
+ switch (layerNo)
+ {
+ case 0:
+ texture = &m_y;
+ break;
+ case 1:
+ texture = &m_vu;
+ if (surface.fourcc == VA_FOURCC_NV12 || surface.fourcc == VA_FOURCC_P010 || surface.fourcc == VA_FOURCC_P016)
+ {
+ // Adjust w/h for 4:2:0 subsampling on UV plane
+ width = (width + 1) >> 1;
+ height = (height + 1) >> 1;
+ }
+ break;
+ default:
+ throw std::logic_error("Impossible layer number");
+ }
+ break;
+ default:
+ CLog::Log(LOGDEBUG,
+ "CVaapi2Texture::Map: DRM-exported surface {} layers - only 2 supported",
+ surface.num_layers);
+ return false;
+ }
+
+ CEGLAttributes<8> attribs; // 6 static + 2 modifiers
+ attribs.Add({{EGL_LINUX_DRM_FOURCC_EXT, static_cast<EGLint>(layer.drm_format)},
+ {EGL_WIDTH, width},
+ {EGL_HEIGHT, height},
+ {EGL_DMA_BUF_PLANE0_FD_EXT, object.fd},
+ {EGL_DMA_BUF_PLANE0_OFFSET_EXT, static_cast<EGLint>(layer.offset[plane])},
+ {EGL_DMA_BUF_PLANE0_PITCH_EXT, static_cast<EGLint>(layer.pitch[plane])}});
+
+ if (m_hasPlaneModifiers)
+ {
+ attribs.Add({{EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, static_cast<EGLint>(object.drm_format_modifier)},
+ {EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, static_cast<EGLint>(object.drm_format_modifier >> 32)}});
+ }
+
+ texture->eglImage = m_interop.eglCreateImageKHR(m_interop.eglDisplay,
+ EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr,
+ attribs.Get());
+ if (!texture->eglImage)
+ {
+ CEGLUtils::Log(LOGERROR, "Failed to import VA DRM surface into EGL image");
+ return false;
+ }
+
+ glGenTextures(1, &texture->glTexture);
+ glBindTexture(m_interop.textureTarget, texture->glTexture);
+ m_interop.glEGLImageTargetTexture2DOES(m_interop.textureTarget, texture->eglImage);
+ glBindTexture(m_interop.textureTarget, 0);
+ }
+
+ return true;
+#else
+ return false;
+#endif
+}
+
+void CVaapi2Texture::Unmap()
+{
+ if (!m_vaapiPic)
+ return;
+
+ for (auto texture : {&m_y, &m_vu})
+ {
+ if (texture->eglImage != EGL_NO_IMAGE_KHR)
+ {
+ m_interop.eglDestroyImageKHR(m_interop.eglDisplay, texture->eglImage);
+ texture->eglImage = EGL_NO_IMAGE_KHR;
+ glDeleteTextures(1, &texture->glTexture);
+ }
+ }
+
+ for (auto& fd : m_drmFDs)
+ {
+ fd.reset();
+ }
+
+ m_vaapiPic->Release();
+ m_vaapiPic = nullptr;
+}
+
+GLuint CVaapi2Texture::GetTextureY()
+{
+ return m_y.glTexture;
+}
+
+GLuint CVaapi2Texture::GetTextureVU()
+{
+ return m_vu.glTexture;
+}
+
+CSizeInt CVaapi2Texture::GetTextureSize()
+{
+ return m_textureSize;
+}
+
+bool CVaapi2Texture::TestEsh(VADisplay vaDpy, EGLDisplay eglDisplay, std::uint32_t rtFormat, std::int32_t pixelFormat)
+{
+#if HAVE_VAEXPORTSURFACHEHANDLE
+ int width = 1920;
+ int height = 1080;
+
+ PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC) eglGetProcAddress("eglCreateImageKHR");
+ PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC) eglGetProcAddress("eglDestroyImageKHR");
+ if (!eglCreateImageKHR || !eglDestroyImageKHR)
+ {
+ return false;
+ }
+
+ // create surfaces
+ VASurfaceID surface;
+ VAStatus status;
+
+ VASurfaceAttrib attribs = {};
+ attribs.flags = VA_SURFACE_ATTRIB_SETTABLE;
+ attribs.type = VASurfaceAttribPixelFormat;
+ attribs.value.type = VAGenericValueTypeInteger;
+ attribs.value.value.i = pixelFormat;
+
+ if (vaCreateSurfaces(vaDpy, rtFormat,
+ width, height,
+ &surface, 1, &attribs, 1) != VA_STATUS_SUCCESS)
+ {
+ return false;
+ }
+
+ // check interop
+ VADRMPRIMESurfaceDescriptor drmPrimeSurface;
+ status = vaExportSurfaceHandle(vaDpy, surface,
+ VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
+ VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS,
+ &drmPrimeSurface);
+
+ bool result = false;
+
+ if (status == VA_STATUS_SUCCESS)
+ {
+ auto const& layer = drmPrimeSurface.layers[0];
+ auto const& object = drmPrimeSurface.objects[layer.object_index[0]];
+ EGLint attribs[] = {
+ EGL_LINUX_DRM_FOURCC_EXT, static_cast<EGLint>(drmPrimeSurface.layers[0].drm_format),
+ EGL_WIDTH, width,
+ EGL_HEIGHT, height,
+ EGL_DMA_BUF_PLANE0_FD_EXT, static_cast<EGLint>(object.fd),
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, static_cast<EGLint>(layer.offset[0]),
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, static_cast<EGLint>(layer.pitch[0]),
+ EGL_NONE};
+
+ EGLImageKHR eglImage = eglCreateImageKHR(eglDisplay,
+ EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr,
+ attribs);
+ if (eglImage)
+ {
+ eglDestroyImageKHR(eglDisplay, eglImage);
+ result = true;
+ }
+
+ for (uint32_t object = 0; object < drmPrimeSurface.num_objects; object++)
+ {
+ close(drmPrimeSurface.objects[object].fd);
+ }
+ }
+
+ vaDestroySurfaces(vaDpy, &surface, 1);
+
+ return result;
+#else
+ return false;
+#endif
+}
+
+void CVaapi2Texture::TestInterop(VADisplay vaDpy, EGLDisplay eglDisplay, bool& general, bool& deepColor)
+{
+ general = false;
+ deepColor = false;
+
+ general = TestInteropGeneral(vaDpy, eglDisplay);
+ if (general)
+ {
+ deepColor = TestEsh(vaDpy, eglDisplay, VA_RT_FORMAT_YUV420_10BPP, VA_FOURCC_P010);
+ }
+}
+
+bool CVaapi2Texture::TestInteropGeneral(VADisplay vaDpy, EGLDisplay eglDisplay)
+{
+ return TestEsh(vaDpy, eglDisplay, VA_RT_FORMAT_YUV420, VA_FOURCC_NV12);
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VaapiEGL.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VaapiEGL.h
new file mode 100644
index 0000000..a8102cc
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VaapiEGL.h
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2007-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 <array>
+#include <cstdint>
+
+#if defined(HAS_GL)
+// always define GL_GLEXT_PROTOTYPES before include gl headers
+#if !defined(GL_GLEXT_PROTOTYPES)
+#define GL_GLEXT_PROTOTYPES
+#endif
+#include <GL/gl.h>
+#elif defined(HAS_GLES)
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+#include <GLES3/gl3.h>
+#endif
+
+#include "system_egl.h"
+#include "utils/Geometry.h"
+
+#include "platform/posix/utils/FileHandle.h"
+
+#include <EGL/eglext.h>
+#include <va/va.h>
+
+namespace VAAPI
+{
+
+class CVaapiRenderPicture;
+
+struct InteropInfo
+{
+ EGLDisplay eglDisplay = nullptr;
+ PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR;
+ PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR;
+ PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES;
+ GLenum textureTarget;
+};
+
+class CVaapiTexture
+{
+public:
+ CVaapiTexture() = default;
+ virtual ~CVaapiTexture() = default;
+
+ virtual void Init(InteropInfo &interop) = 0;
+ virtual bool Map(CVaapiRenderPicture *pic) = 0;
+ virtual void Unmap() = 0;
+
+ virtual GLuint GetTextureY() = 0;
+ virtual GLuint GetTextureVU() = 0;
+ virtual CSizeInt GetTextureSize() = 0;
+};
+
+class CVaapi1Texture : public CVaapiTexture
+{
+public:
+ CVaapi1Texture() = default;
+
+ bool Map(CVaapiRenderPicture *pic) override;
+ void Unmap() override;
+ void Init(InteropInfo &interop) override;
+
+ GLuint GetTextureY() override;
+ GLuint GetTextureVU() override;
+ CSizeInt GetTextureSize() override;
+
+ static void TestInterop(VADisplay vaDpy, EGLDisplay eglDisplay, bool &general, bool &deepColor);
+
+ GLuint m_texture = 0;
+ GLuint m_textureY = 0;
+ GLuint m_textureVU = 0;
+ int m_texWidth = 0;
+ int m_texHeight = 0;
+
+protected:
+ static bool TestInteropDeepColor(VADisplay vaDpy, EGLDisplay eglDisplay);
+
+ InteropInfo m_interop;
+ CVaapiRenderPicture *m_vaapiPic = nullptr;
+ struct GLSurface
+ {
+ VAImage vaImage;
+ VABufferInfo vBufInfo;
+ EGLImageKHR eglImage;
+ EGLImageKHR eglImageY, eglImageVU;
+ } m_glSurface;
+};
+
+class CVaapi2Texture : public CVaapiTexture
+{
+public:
+ bool Map(CVaapiRenderPicture *pic) override;
+ void Unmap() override;
+ void Init(InteropInfo &interop) override;
+
+ GLuint GetTextureY() override;
+ GLuint GetTextureVU() override;
+ CSizeInt GetTextureSize() override;
+
+ static void TestInterop(VADisplay vaDpy, EGLDisplay eglDisplay, bool &general, bool &deepColor);
+ static bool TestInteropGeneral(VADisplay vaDpy, EGLDisplay eglDisplay);
+
+private:
+ static bool TestEsh(VADisplay vaDpy, EGLDisplay eglDisplay, std::uint32_t rtFormat, std::int32_t pixelFormat);
+
+ struct MappedTexture
+ {
+ EGLImageKHR eglImage{EGL_NO_IMAGE_KHR};
+ GLuint glTexture{};
+ };
+
+ InteropInfo m_interop;
+ CVaapiRenderPicture* m_vaapiPic{};
+ bool m_hasPlaneModifiers{false};
+ std::array<KODI::UTILS::POSIX::CFileHandle, 4> m_drmFDs;
+ MappedTexture m_y, m_vu;
+ CSizeInt m_textureSize;
+};
+
+}
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VdpauGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VdpauGL.cpp
new file mode 100644
index 0000000..2037acb
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VdpauGL.cpp
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2007-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 "VdpauGL.h"
+
+#include "cores/VideoPlayer/DVDCodecs/Video/VDPAU.h"
+#include "utils/log.h"
+
+#include <GL/glx.h>
+
+using namespace VDPAU;
+
+//-----------------------------------------------------------------------------
+// interop state
+//-----------------------------------------------------------------------------
+
+bool CInteropState::Init(void *device, void *procFunc, int64_t ident)
+{
+ m_device = device;
+ m_procFunc = procFunc;
+ m_ident = ident;
+
+ m_interop.glVDPAUInitNV = (PFNGLVDPAUINITNVPROC)glXGetProcAddress((const GLubyte *) "glVDPAUInitNV");
+ m_interop.glVDPAUFiniNV = (PFNGLVDPAUFININVPROC)glXGetProcAddress((const GLubyte *) "glVDPAUFiniNV");
+ m_interop.glVDPAURegisterOutputSurfaceNV = (PFNGLVDPAUREGISTEROUTPUTSURFACENVPROC)glXGetProcAddress((const GLubyte *) "glVDPAURegisterOutputSurfaceNV");
+ m_interop.glVDPAURegisterVideoSurfaceNV = (PFNGLVDPAUREGISTERVIDEOSURFACENVPROC)glXGetProcAddress((const GLubyte *) "glVDPAURegisterVideoSurfaceNV");
+ m_interop.glVDPAUIsSurfaceNV = (PFNGLVDPAUISSURFACENVPROC)glXGetProcAddress((const GLubyte *) "glVDPAUIsSurfaceNV");
+ m_interop.glVDPAUUnregisterSurfaceNV = (PFNGLVDPAUUNREGISTERSURFACENVPROC)glXGetProcAddress((const GLubyte *) "glVDPAUUnregisterSurfaceNV");
+ m_interop.glVDPAUSurfaceAccessNV = (PFNGLVDPAUSURFACEACCESSNVPROC)glXGetProcAddress((const GLubyte *) "glVDPAUSurfaceAccessNV");
+ m_interop.glVDPAUMapSurfacesNV = (PFNGLVDPAUMAPSURFACESNVPROC)glXGetProcAddress((const GLubyte *) "glVDPAUMapSurfacesNV");
+ m_interop.glVDPAUUnmapSurfacesNV = (PFNGLVDPAUUNMAPSURFACESNVPROC)glXGetProcAddress((const GLubyte *) "glVDPAUUnmapSurfacesNV");
+ m_interop.glVDPAUGetSurfaceivNV = (PFNGLVDPAUGETSURFACEIVNVPROC)glXGetProcAddress((const GLubyte *) "glVDPAUGetSurfaceivNV");
+
+ while (glGetError() != GL_NO_ERROR);
+ m_interop.glVDPAUInitNV(m_device, m_procFunc);
+ if (glGetError() != GL_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "CInteropState::Init - GLInitInterop glVDPAUInitNV failed");
+ return false;
+ }
+ CLog::Log(LOGINFO, "CInteropState::Init: vdpau gl interop initialized");
+
+ m_interop.textureTarget = GL_TEXTURE_2D;
+
+ return true;
+}
+
+void CInteropState::Finish()
+{
+ m_interop.glVDPAUFiniNV();
+ m_device = nullptr;
+ m_procFunc = nullptr;
+}
+
+InteropInfo &CInteropState::GetInterop()
+{
+ return m_interop;
+}
+
+bool CInteropState::NeedInit(void *device, void *procFunc, int64_t ident)
+{
+ if (m_device != device)
+ return true;
+ if (m_procFunc != procFunc)
+ return true;
+ if (m_ident != ident)
+ return true;
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+// textures
+//-----------------------------------------------------------------------------
+
+void CVdpauTexture::Init(InteropInfo &interop)
+{
+ m_interop = interop;
+}
+
+bool CVdpauTexture::Map(CVdpauRenderPicture *pic)
+{
+
+ if (m_vdpauPic)
+ return true;
+
+ m_vdpauPic = pic;
+ m_vdpauPic->Acquire();
+
+ m_texWidth = pic->width;
+ m_texHeight = pic->height;
+
+ bool success = false;
+ if (m_vdpauPic->procPic.isYuv)
+ success = MapNV12();
+ else
+ success = MapRGB();
+
+ if (!success)
+ {
+ m_vdpauPic->Release();
+ m_vdpauPic = nullptr;
+ }
+
+ return success;
+}
+
+void CVdpauTexture::Unmap()
+{
+ if (!m_vdpauPic)
+ return;
+
+ if (m_vdpauPic->procPic.isYuv)
+ UnmapNV12();
+ else
+ UnmapRGB();
+
+ m_vdpauPic->Release();
+ m_vdpauPic = nullptr;
+}
+
+bool CVdpauTexture::MapNV12()
+{
+ GLuint textures[4];
+ while (glGetError() != GL_NO_ERROR) ;
+ glGenTextures(4, textures);
+ if (glGetError() != GL_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "CVdpauTexture::MapNV12 error creating texture");
+ return false;
+ }
+
+ const void *videoSurface = reinterpret_cast<void*>(m_vdpauPic->procPic.videoSurface);
+ m_glSurface.glVdpauSurface = m_interop.glVDPAURegisterVideoSurfaceNV(videoSurface,
+ m_interop.textureTarget, 4, textures);
+ if (glGetError() != GL_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "CVdpauTexture::MapNV12 error register video surface");
+ glDeleteTextures(4, textures);
+ return false;
+ }
+
+ m_interop.glVDPAUSurfaceAccessNV(m_glSurface.glVdpauSurface, GL_READ_ONLY);
+ if (glGetError() != GL_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "CVdpauTexture::MapNV12 error setting access");
+ glDeleteTextures(4, textures);
+ return false;
+ }
+
+ m_interop.glVDPAUMapSurfacesNV(1, &m_glSurface.glVdpauSurface);
+ if (glGetError() != GL_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "CVdpauTexture::MapNV12 error mapping surface");
+ glDeleteTextures(4, textures);
+ return false;
+ }
+
+ m_interop.glVDPAUUnregisterSurfaceNV(m_glSurface.glVdpauSurface);
+
+ m_textureTopY = textures[0];
+ m_textureTopUV = textures[2];
+ m_textureBotY = textures[1];
+ m_textureBotUV = textures[3];
+
+ return true;
+}
+
+void CVdpauTexture::UnmapNV12()
+{
+ m_interop.glVDPAUUnmapSurfacesNV(1, &m_glSurface.glVdpauSurface);
+ glDeleteTextures(1, &m_textureTopY);
+ glDeleteTextures(1, &m_textureTopUV);
+ glDeleteTextures(1, &m_textureBotY);
+ glDeleteTextures(1, &m_textureBotUV);
+}
+
+bool CVdpauTexture::MapRGB()
+{
+ glGenTextures(1, &m_texture);
+ const void *outSurface = reinterpret_cast<void*>(m_vdpauPic->procPic.outputSurface);
+ m_glSurface.glVdpauSurface = m_interop.glVDPAURegisterOutputSurfaceNV(outSurface,
+ m_interop.textureTarget, 1, &m_texture);
+ GLenum err = glGetError();
+ if (err != GL_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "CVdpauTexture::MapRGB error register output surface: {}", err);
+ return false;
+ }
+
+ m_interop.glVDPAUSurfaceAccessNV(m_glSurface.glVdpauSurface, GL_READ_ONLY);
+ if (glGetError() != GL_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "CVdpauTexture::MapRGB error setting access");
+ glDeleteTextures(1, &m_texture);
+ return false;
+ }
+
+ while (glGetError() != GL_NO_ERROR) ;
+ m_interop.glVDPAUMapSurfacesNV(1, &m_glSurface.glVdpauSurface);
+ if (glGetError() != GL_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "VDPAU::COutput error mapping surface");
+ glDeleteTextures(1, &m_texture);
+ return false;
+ }
+
+ return true;
+}
+
+void CVdpauTexture::UnmapRGB()
+{
+ m_interop.glVDPAUUnmapSurfacesNV(1, &m_glSurface.glVdpauSurface);
+ m_interop.glVDPAUUnregisterSurfaceNV(m_glSurface.glVdpauSurface);
+ glDeleteTextures(1, &m_texture);
+}
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VdpauGL.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VdpauGL.h
new file mode 100644
index 0000000..2583816
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VdpauGL.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2007-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
+
+// always define GL_GLEXT_PROTOTYPES before include gl headers
+#if !defined(GL_GLEXT_PROTOTYPES)
+#define GL_GLEXT_PROTOTYPES
+#endif
+
+#include <GL/gl.h>
+#include <GL/glext.h>
+
+namespace VDPAU
+{
+class CVdpauRenderPicture;
+
+
+struct InteropInfo
+{
+ PFNGLVDPAUINITNVPROC glVDPAUInitNV;
+ PFNGLVDPAUFININVPROC glVDPAUFiniNV;
+ PFNGLVDPAUREGISTEROUTPUTSURFACENVPROC glVDPAURegisterOutputSurfaceNV;
+ PFNGLVDPAUREGISTERVIDEOSURFACENVPROC glVDPAURegisterVideoSurfaceNV;
+ PFNGLVDPAUISSURFACENVPROC glVDPAUIsSurfaceNV;
+ PFNGLVDPAUUNREGISTERSURFACENVPROC glVDPAUUnregisterSurfaceNV;
+ PFNGLVDPAUSURFACEACCESSNVPROC glVDPAUSurfaceAccessNV;
+ PFNGLVDPAUMAPSURFACESNVPROC glVDPAUMapSurfacesNV;
+ PFNGLVDPAUUNMAPSURFACESNVPROC glVDPAUUnmapSurfacesNV;
+ PFNGLVDPAUGETSURFACEIVNVPROC glVDPAUGetSurfaceivNV;
+ GLenum textureTarget;
+};
+
+class CInteropState
+{
+public:
+ bool Init(void *device, void *procFunc, int64_t ident);
+ void Finish();
+ InteropInfo &GetInterop();
+ bool NeedInit(void *device, void *procFunc, int64_t ident);
+
+protected:
+ void *m_device = nullptr;
+ void *m_procFunc = nullptr;
+ int64_t m_ident = 0;
+ InteropInfo m_interop;
+};
+
+class CVdpauTexture
+{
+public:
+ bool Map(VDPAU::CVdpauRenderPicture *pic);
+ void Unmap();
+ void Init(InteropInfo &interop);
+
+ GLuint m_texture = 0;
+ GLuint m_textureTopY = 0;
+ GLuint m_textureTopUV = 0;
+ GLuint m_textureBotY = 0;
+ GLuint m_textureBotUV = 0;
+ int m_texWidth = 0;
+ int m_texHeight = 0;
+
+protected:
+ bool MapNV12();
+ void UnmapNV12();
+ bool MapRGB();
+ void UnmapRGB();
+ InteropInfo m_interop;
+ CVdpauRenderPicture *m_vdpauPic = nullptr;
+ struct GLSurface
+ {
+ GLvdpauSurfaceNV glVdpauSurface;
+ } m_glSurface;
+};
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VideoLayerBridgeDRMPRIME.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VideoLayerBridgeDRMPRIME.cpp
new file mode 100644
index 0000000..4b8ee5a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VideoLayerBridgeDRMPRIME.cpp
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2017-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 "VideoLayerBridgeDRMPRIME.h"
+
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+#include "windowing/gbm/drm/DRMAtomic.h"
+
+#include <utility>
+
+using namespace KODI::WINDOWING::GBM;
+using namespace DRMPRIME;
+
+CVideoLayerBridgeDRMPRIME::CVideoLayerBridgeDRMPRIME(std::shared_ptr<CDRMAtomic> drm)
+ : m_DRM(std::move(drm))
+{
+}
+
+CVideoLayerBridgeDRMPRIME::~CVideoLayerBridgeDRMPRIME()
+{
+ Release(m_prev_buffer);
+ Release(m_buffer);
+}
+
+void CVideoLayerBridgeDRMPRIME::Disable()
+{
+ // disable video plane
+ auto plane = m_DRM->GetVideoPlane();
+ m_DRM->AddProperty(plane, "FB_ID", 0);
+ m_DRM->AddProperty(plane, "CRTC_ID", 0);
+
+ auto winSystem = CServiceBroker::GetWinSystem();
+ if (!winSystem)
+ return;
+
+ // disable HDR metadata
+ winSystem->SetHDR(nullptr);
+}
+
+void CVideoLayerBridgeDRMPRIME::Acquire(CVideoBufferDRMPRIME* buffer)
+{
+ // release the buffer that is no longer presented on screen
+ Release(m_prev_buffer);
+
+ // release the buffer currently being presented next call
+ m_prev_buffer = m_buffer;
+
+ // reference count the buffer that is going to be presented on screen
+ m_buffer = buffer;
+ m_buffer->Acquire();
+}
+
+void CVideoLayerBridgeDRMPRIME::Release(CVideoBufferDRMPRIME* buffer)
+{
+ if (!buffer)
+ return;
+
+ Unmap(buffer);
+ buffer->Release();
+}
+
+bool CVideoLayerBridgeDRMPRIME::Map(CVideoBufferDRMPRIME* buffer)
+{
+ if (buffer->m_fb_id)
+ return true;
+
+ if (!buffer->AcquireDescriptor())
+ {
+ CLog::Log(LOGERROR, "CVideoLayerBridgeDRMPRIME::{} - failed to acquire descriptor",
+ __FUNCTION__);
+ return false;
+ }
+
+ AVDRMFrameDescriptor* descriptor = buffer->GetDescriptor();
+ uint32_t handles[4] = {}, pitches[4] = {}, offsets[4] = {}, flags = 0;
+ uint64_t modifier[4] = {};
+ int ret;
+
+ // convert Prime FD to GEM handle
+ for (int object = 0; object < descriptor->nb_objects; object++)
+ {
+ ret = drmPrimeFDToHandle(m_DRM->GetFileDescriptor(), descriptor->objects[object].fd,
+ &buffer->m_handles[object]);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR,
+ "CVideoLayerBridgeDRMPRIME::{} - failed to convert prime fd {} to gem handle {}, "
+ "ret = {}",
+ __FUNCTION__, descriptor->objects[object].fd, buffer->m_handles[object], ret);
+ return false;
+ }
+ }
+
+ AVDRMLayerDescriptor* layer = &descriptor->layers[0];
+
+ for (int plane = 0; plane < layer->nb_planes; plane++)
+ {
+ int object = layer->planes[plane].object_index;
+ uint32_t handle = buffer->m_handles[object];
+ if (handle && layer->planes[plane].pitch)
+ {
+ handles[plane] = handle;
+ pitches[plane] = layer->planes[plane].pitch;
+ offsets[plane] = layer->planes[plane].offset;
+ modifier[plane] = descriptor->objects[object].format_modifier;
+ }
+ }
+
+ if (modifier[0] && modifier[0] != DRM_FORMAT_MOD_INVALID)
+ flags = DRM_MODE_FB_MODIFIERS;
+
+ // add the video frame FB
+ ret = drmModeAddFB2WithModifiers(m_DRM->GetFileDescriptor(), buffer->GetWidth(),
+ buffer->GetHeight(), layer->format, handles, pitches, offsets,
+ modifier, &buffer->m_fb_id, flags);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CVideoLayerBridgeDRMPRIME::{} - failed to add fb {}, ret = {}",
+ __FUNCTION__, buffer->m_fb_id, ret);
+ return false;
+ }
+
+ Acquire(buffer);
+ return true;
+}
+
+void CVideoLayerBridgeDRMPRIME::Unmap(CVideoBufferDRMPRIME* buffer)
+{
+ if (buffer->m_fb_id)
+ {
+ drmModeRmFB(m_DRM->GetFileDescriptor(), buffer->m_fb_id);
+ buffer->m_fb_id = 0;
+ }
+
+ for (int i = 0; i < AV_DRM_MAX_PLANES; i++)
+ {
+ if (buffer->m_handles[i])
+ {
+ struct drm_gem_close gem_close;
+ gem_close.handle = buffer->m_handles[i];
+ drmIoctl(m_DRM->GetFileDescriptor(), DRM_IOCTL_GEM_CLOSE, &gem_close);
+ buffer->m_handles[i] = 0;
+ }
+ }
+
+ buffer->ReleaseDescriptor();
+}
+
+void CVideoLayerBridgeDRMPRIME::Configure(CVideoBufferDRMPRIME* buffer)
+{
+ auto winSystem = CServiceBroker::GetWinSystem();
+ if (!winSystem)
+ return;
+
+ const VideoPicture& picture = buffer->GetPicture();
+
+ winSystem->SetHDR(&picture);
+
+ auto plane = m_DRM->GetVideoPlane();
+
+ bool result;
+ uint64_t value;
+ std::tie(result, value) = plane->GetPropertyValue("COLOR_ENCODING", GetColorEncoding(picture));
+ if (result)
+ m_DRM->AddProperty(plane, "COLOR_ENCODING", value);
+
+ std::tie(result, value) = plane->GetPropertyValue("COLOR_RANGE", GetColorRange(picture));
+ if (result)
+ m_DRM->AddProperty(plane, "COLOR_RANGE", value);
+}
+
+void CVideoLayerBridgeDRMPRIME::SetVideoPlane(CVideoBufferDRMPRIME* buffer, const CRect& destRect)
+{
+ if (!Map(buffer))
+ {
+ Unmap(buffer);
+ return;
+ }
+
+ auto plane = m_DRM->GetVideoPlane();
+ m_DRM->AddProperty(plane, "FB_ID", buffer->m_fb_id);
+ m_DRM->AddProperty(plane, "CRTC_ID", m_DRM->GetCrtc()->GetCrtcId());
+ m_DRM->AddProperty(plane, "SRC_X", 0);
+ m_DRM->AddProperty(plane, "SRC_Y", 0);
+ m_DRM->AddProperty(plane, "SRC_W", buffer->GetWidth() << 16);
+ m_DRM->AddProperty(plane, "SRC_H", buffer->GetHeight() << 16);
+ m_DRM->AddProperty(plane, "CRTC_X", static_cast<int32_t>(destRect.x1) & ~1);
+ m_DRM->AddProperty(plane, "CRTC_Y", static_cast<int32_t>(destRect.y1) & ~1);
+ m_DRM->AddProperty(plane, "CRTC_W", (static_cast<uint32_t>(destRect.Width()) + 1) & ~1);
+ m_DRM->AddProperty(plane, "CRTC_H", (static_cast<uint32_t>(destRect.Height()) + 1) & ~1);
+}
+
+void CVideoLayerBridgeDRMPRIME::UpdateVideoPlane()
+{
+ if (!m_buffer || !m_buffer->m_fb_id)
+ return;
+
+ auto plane = m_DRM->GetVideoPlane();
+ m_DRM->AddProperty(plane, "FB_ID", m_buffer->m_fb_id);
+ m_DRM->AddProperty(plane, "CRTC_ID", m_DRM->GetCrtc()->GetCrtcId());
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VideoLayerBridgeDRMPRIME.h b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VideoLayerBridgeDRMPRIME.h
new file mode 100644
index 0000000..8d9758f
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/VideoLayerBridgeDRMPRIME.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 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/VideoPlayer/Interface/StreamInfo.h"
+#include "windowing/gbm/VideoLayerBridge.h"
+
+#include <memory>
+
+#include <drm_mode.h>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+class CDRMAtomic;
+}
+} // namespace WINDOWING
+} // namespace KODI
+
+class CVideoBufferDRMPRIME;
+
+class CVideoLayerBridgeDRMPRIME : public KODI::WINDOWING::GBM::CVideoLayerBridge
+{
+public:
+ CVideoLayerBridgeDRMPRIME(std::shared_ptr<KODI::WINDOWING::GBM::CDRMAtomic> drm);
+ ~CVideoLayerBridgeDRMPRIME() override;
+ void Disable() override;
+
+ virtual void Configure(CVideoBufferDRMPRIME* buffer);
+ virtual void SetVideoPlane(CVideoBufferDRMPRIME* buffer, const CRect& destRect);
+ virtual void UpdateVideoPlane();
+
+protected:
+ std::shared_ptr<KODI::WINDOWING::GBM::CDRMAtomic> m_DRM;
+
+private:
+ void Acquire(CVideoBufferDRMPRIME* buffer);
+ void Release(CVideoBufferDRMPRIME* buffer);
+ bool Map(CVideoBufferDRMPRIME* buffer);
+ void Unmap(CVideoBufferDRMPRIME* buffer);
+
+ CVideoBufferDRMPRIME* m_buffer = nullptr;
+ CVideoBufferDRMPRIME* m_prev_buffer = nullptr;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.cpp
new file mode 100644
index 0000000..f0c9496
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.cpp
@@ -0,0 +1,2757 @@
+/*
+ * Copyright (c) 2007 Frodo/jcmarshall/vulkanr/d4rk
+ * Based on XBoxRenderer by Frodo/jcmarshall
+ * Portions Copyright (c) by the authors of ffmpeg / xvid /mplayer
+ * Copyright (C) 2007-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 "LinuxRendererGL.h"
+
+#include "RenderCapture.h"
+#include "RenderCaptureGL.h"
+#include "RenderFactory.h"
+#include "ServiceBroker.h"
+#include "VideoShaders/VideoFilterShaderGL.h"
+#include "VideoShaders/YUV2RGBShaderGL.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/IPlayer.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFlags.h"
+#include "rendering/MatrixGL.h"
+#include "rendering/gl/RenderSystemGL.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include <locale.h>
+#include <mutex>
+
+#ifdef TARGET_DARWIN_OSX
+#include "platform/darwin/osx/CocoaInterface.h"
+#include <CoreVideo/CoreVideo.h>
+#include <OpenGL/CGLIOSurface.h>
+#endif
+
+//! @bug
+//! due to a bug on osx nvidia, using gltexsubimage2d with a pbo bound and a null pointer
+//! screws up the alpha, an offset fixes this, there might still be a problem if stride + PBO_OFFSET
+//! is a multiple of 128 and deinterlacing is on
+#define PBO_OFFSET 16
+
+using namespace Shaders;
+using namespace Shaders::GL;
+
+static const GLubyte stipple_weave[] = {
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+};
+
+CLinuxRendererGL::CPictureBuffer::CPictureBuffer()
+{
+ memset(&fields, 0, sizeof(fields));
+ memset(&image , 0, sizeof(image));
+ memset(&pbo , 0, sizeof(pbo));
+ videoBuffer = nullptr;
+ loaded = false;
+}
+
+CBaseRenderer* CLinuxRendererGL::Create(CVideoBuffer *buffer)
+{
+ return new CLinuxRendererGL();
+}
+
+bool CLinuxRendererGL::Register()
+{
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("default", CLinuxRendererGL::Create);
+ return true;
+}
+
+CLinuxRendererGL::CLinuxRendererGL()
+{
+ m_iFlags = 0;
+ m_format = AV_PIX_FMT_NONE;
+
+ m_useDithering = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool("videoscreen.dither");
+ m_ditherDepth = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt("videoscreen.ditherdepth");
+ m_fullRange = !CServiceBroker::GetWinSystem()->UseLimitedColor();
+
+ m_fbo.width = 0.0;
+ m_fbo.height = 0.0;
+
+ m_ColorManager.reset(new CColorManager());
+ m_tCLUTTex = 0;
+ m_CLUT = NULL;
+ m_CLUTsize = 0;
+ m_cmsToken = -1;
+ m_cmsOn = false;
+
+ m_renderSystem = dynamic_cast<CRenderSystemGL*>(CServiceBroker::GetRenderSystem());
+}
+
+CLinuxRendererGL::~CLinuxRendererGL()
+{
+ UnInit();
+
+ if (m_pYUVShader)
+ {
+ delete m_pYUVShader;
+ m_pYUVShader = nullptr;
+ }
+
+ if (m_pVideoFilterShader)
+ {
+ delete m_pVideoFilterShader;
+ m_pVideoFilterShader = nullptr;
+ }
+}
+
+bool CLinuxRendererGL::ValidateRenderer()
+{
+ if (!m_bConfigured)
+ return false;
+
+ // if its first pass, just init textures and return
+ if (ValidateRenderTarget())
+ return false;
+
+ int index = m_iYV12RenderBuffer;
+ const CPictureBuffer& buf = m_buffers[index];
+
+ if (!buf.fields[FIELD_FULL][0].id)
+ return false;
+
+ return true;
+}
+
+bool CLinuxRendererGL::ValidateRenderTarget()
+{
+ if (!m_bValidated)
+ {
+ // function pointer for texture might change in
+ // call to LoadShaders
+ glFinish();
+ for (int i = 0 ; i < NUM_BUFFERS ; i++)
+ {
+ DeleteTexture(i);
+ }
+
+ // trigger update of video filters
+ m_scalingMethodGui = (ESCALINGMETHOD)-1;
+
+ // create the yuv textures
+ UpdateVideoFilter();
+ LoadShaders();
+ if (m_renderMethod < 0)
+ return false;
+
+ if (m_textureTarget == GL_TEXTURE_RECTANGLE)
+ CLog::Log(LOGINFO, "Using GL_TEXTURE_RECTANGLE");
+ else
+ CLog::Log(LOGINFO, "Using GL_TEXTURE_2D");
+
+ for (int i = 0 ; i < m_NumYV12Buffers ; i++)
+ CreateTexture(i);
+
+ m_bValidated = true;
+ return true;
+ }
+ return false;
+}
+
+bool CLinuxRendererGL::Configure(const VideoPicture &picture, float fps, unsigned int orientation)
+{
+ m_format = picture.videoBuffer->GetFormat();
+ m_sourceWidth = picture.iWidth;
+ m_sourceHeight = picture.iHeight;
+ m_renderOrientation = orientation;
+ m_fps = fps;
+
+ m_iFlags = GetFlagsChromaPosition(picture.chroma_position) |
+ GetFlagsStereoMode(picture.stereoMode);
+
+ m_srcPrimaries = GetSrcPrimaries(static_cast<AVColorPrimaries>(picture.color_primaries),
+ picture.iWidth, picture.iHeight);
+ m_toneMap = false;
+
+ // Calculate the input frame aspect ratio.
+ CalculateFrameAspectRatio(picture.iDisplayWidth, picture.iDisplayHeight);
+ SetViewMode(m_videoSettings.m_ViewMode);
+ ManageRenderArea();
+
+ m_bConfigured = true;
+ m_scalingMethodGui = (ESCALINGMETHOD)-1;
+
+ // Ensure that textures are recreated and rendering starts only after the 1st
+ // frame is loaded after every call to Configure().
+ m_bValidated = false;
+
+ m_nonLinStretch = false;
+ m_nonLinStretchGui = false;
+ m_pixelRatio = 1.0;
+
+ m_pboSupported = CServiceBroker::GetRenderSystem()->IsExtSupported("GL_ARB_pixel_buffer_object");
+
+ // setup the background colour
+ m_clearColour = CServiceBroker::GetWinSystem()->UseLimitedColor() ? (16.0f / 0xff) : 0.0f;
+
+ // load 3DLUT
+ if (m_ColorManager->IsEnabled())
+ {
+ if (!m_ColorManager->CheckConfiguration(m_cmsToken, m_srcPrimaries))
+ {
+ CLog::Log(LOGDEBUG, "CMS configuration changed, reload LUT");
+ if (!LoadCLUT())
+ return false;
+ }
+ m_cmsOn = true;
+ }
+ else
+ {
+ m_cmsOn = false;
+ }
+
+ return true;
+}
+
+bool CLinuxRendererGL::ConfigChanged(const VideoPicture &picture)
+{
+ if (picture.videoBuffer->GetFormat() != m_format)
+ return true;
+
+ return false;
+}
+
+void CLinuxRendererGL::AddVideoPicture(const VideoPicture &picture, int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ if (buf.videoBuffer)
+ {
+ CLog::LogF(LOGERROR, "unreleased video buffer");
+ buf.videoBuffer->Release();
+ }
+ buf.videoBuffer = picture.videoBuffer;
+ buf.videoBuffer->Acquire();
+ buf.loaded = false;
+ buf.m_srcPrimaries = static_cast<AVColorPrimaries>(picture.color_primaries);
+ buf.m_srcColSpace = static_cast<AVColorSpace>(picture.color_space);
+ buf.m_srcFullRange = picture.color_range == 1;
+ buf.m_srcBits = picture.colorBits;
+
+ buf.hasDisplayMetadata = picture.hasDisplayMetadata;
+ buf.displayMetadata = picture.displayMetadata;
+ buf.lightMetadata = picture.lightMetadata;
+ if (picture.hasLightMetadata && picture.lightMetadata.MaxCLL)
+ buf.hasLightMetadata = picture.hasLightMetadata;
+}
+
+void CLinuxRendererGL::ReleaseBuffer(int idx)
+{
+ CPictureBuffer &buf = m_buffers[idx];
+ if (buf.videoBuffer)
+ {
+ buf.videoBuffer->Release();
+ buf.videoBuffer = nullptr;
+ }
+}
+
+void CLinuxRendererGL::GetPlaneTextureSize(CYuvPlane& plane)
+{
+ /* texture is assumed to be bound */
+ GLint width = 0
+ , height = 0
+ , border = 0;
+ glGetTexLevelParameteriv(m_textureTarget, 0, GL_TEXTURE_WIDTH , &width);
+ glGetTexLevelParameteriv(m_textureTarget, 0, GL_TEXTURE_HEIGHT, &height);
+ glGetTexLevelParameteriv(m_textureTarget, 0, GL_TEXTURE_BORDER, &border);
+ plane.texwidth = width - 2 * border;
+ plane.texheight = height - 2 * border;
+ if(plane.texwidth <= 0 || plane.texheight <= 0)
+ {
+ CLog::Log(LOGDEBUG, "CLinuxRendererGL::GetPlaneTextureSize - invalid size {}x{} - {}", width,
+ height, border);
+ /* to something that avoid division by zero */
+ plane.texwidth = 1;
+ plane.texheight = 1;
+ }
+}
+
+void CLinuxRendererGL::CalculateTextureSourceRects(int source, int num_planes)
+{
+ CPictureBuffer& buf = m_buffers[source];
+ YuvImage* im = &buf.image;
+
+ // calculate the source rectangle
+ for(int field = 0; field < 3; field++)
+ {
+ for(int plane = 0; plane < num_planes; plane++)
+ {
+ CYuvPlane& p = buf.fields[field][plane];
+
+ p.rect = m_sourceRect;
+ p.width = im->width;
+ p.height = im->height;
+
+ if(field != FIELD_FULL)
+ {
+ /* correct for field offsets and chroma offsets */
+ float offset_y = 0.5f;
+ if(plane != 0)
+ offset_y += 0.5f;
+ if(field == FIELD_BOT)
+ offset_y *= -1;
+
+ p.rect.y1 += offset_y;
+ p.rect.y2 += offset_y;
+
+ /* half the height if this is a field */
+ p.height *= 0.5f;
+ p.rect.y1 *= 0.5f;
+ p.rect.y2 *= 0.5f;
+ }
+
+ if(plane != 0)
+ {
+ p.width /= 1 << im->cshift_x;
+ p.height /= 1 << im->cshift_y;
+
+ p.rect.x1 /= 1 << im->cshift_x;
+ p.rect.x2 /= 1 << im->cshift_x;
+ p.rect.y1 /= 1 << im->cshift_y;
+ p.rect.y2 /= 1 << im->cshift_y;
+ }
+
+ // protect against division by zero
+ if (p.texheight == 0 || p.texwidth == 0 ||
+ p.pixpertex_x == 0 || p.pixpertex_y == 0)
+ {
+ continue;
+ }
+
+ p.height /= p.pixpertex_y;
+ p.rect.y1 /= p.pixpertex_y;
+ p.rect.y2 /= p.pixpertex_y;
+ p.width /= p.pixpertex_x;
+ p.rect.x1 /= p.pixpertex_x;
+ p.rect.x2 /= p.pixpertex_x;
+
+ if (m_textureTarget == GL_TEXTURE_2D)
+ {
+ p.height /= p.texheight;
+ p.rect.y1 /= p.texheight;
+ p.rect.y2 /= p.texheight;
+ p.width /= p.texwidth;
+ p.rect.x1 /= p.texwidth;
+ p.rect.x2 /= p.texwidth;
+ }
+ }
+ }
+}
+
+void CLinuxRendererGL::LoadPlane(CYuvPlane& plane, int type,
+ unsigned width, unsigned height,
+ int stride, int bpp, void* data)
+{
+
+ if (plane.pbo)
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, plane.pbo);
+
+ int bps = bpp * KODI::UTILS::GL::glFormatElementByteCount(type);
+
+ unsigned datatype;
+ if (bpp == 2)
+ datatype = GL_UNSIGNED_SHORT;
+ else
+ datatype = GL_UNSIGNED_BYTE;
+
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, stride / bps);
+ glBindTexture(m_textureTarget, plane.id);
+ glTexSubImage2D(m_textureTarget, 0, 0, 0, width, height, type, datatype, data);
+
+ /* check if we need to load any border pixels */
+ if (height < plane.texheight)
+ glTexSubImage2D( m_textureTarget, 0
+ , 0, height, width, 1
+ , type, datatype
+ , (unsigned char*)data + stride * (height-1));
+
+ if (width < plane.texwidth)
+ glTexSubImage2D( m_textureTarget, 0
+ , width, 0, 1, height
+ , type, datatype
+ , (unsigned char*)data + bps * (width-1));
+
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+ glBindTexture(m_textureTarget, 0);
+ if (plane.pbo)
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+}
+
+bool CLinuxRendererGL::Flush(bool saveBuffers)
+{
+ bool safe = saveBuffers && CanSaveBuffers();
+ glFinish();
+
+ for (int i = 0 ; i < m_NumYV12Buffers ; i++)
+ {
+ if (!safe)
+ ReleaseBuffer(i);
+ DeleteTexture(i);
+ }
+
+ delete m_pYUVShader;
+ m_pYUVShader = nullptr;
+ delete m_pVideoFilterShader;
+ m_pVideoFilterShader = nullptr;
+
+ glFinish();
+ m_bValidated = false;
+ m_fbo.fbo.Cleanup();
+ m_iYV12RenderBuffer = 0;
+
+ return safe;
+}
+
+void CLinuxRendererGL::Update()
+{
+ if (!m_bConfigured)
+ return;
+ ManageRenderArea();
+ m_scalingMethodGui = (ESCALINGMETHOD)-1;
+
+ ValidateRenderTarget();
+}
+
+void CLinuxRendererGL::RenderUpdate(int index, int index2, bool clear, unsigned int flags, unsigned int alpha)
+{
+ if (index2 >= 0)
+ m_iYV12RenderBuffer = index2;
+ else
+ m_iYV12RenderBuffer = index;
+
+ if (!ValidateRenderer())
+ {
+ if (clear) //if clear is set, we're expected to overwrite all backbuffer pixels, even if we have nothing to render
+ ClearBackBuffer();
+
+ return;
+ }
+
+ ManageRenderArea();
+
+ if (clear)
+ {
+ //draw black bars when video is not transparent, clear the entire backbuffer when it is
+ if (alpha == 255)
+ DrawBlackBars();
+ else
+ ClearBackBuffer();
+ }
+
+ if (alpha < 255)
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ }
+ else
+ {
+ glDisable(GL_BLEND);
+ }
+
+ if (m_pYUVShader)
+ m_pYUVShader->SetAlpha(alpha/255);
+ if (m_pVideoFilterShader)
+ m_pVideoFilterShader->SetAlpha(alpha/255);
+
+ if (!Render(flags, m_iYV12RenderBuffer) && clear)
+ ClearBackBuffer();
+
+ if (index2 >= 0)
+ {
+ m_iYV12RenderBuffer = index;
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ if (m_pYUVShader)
+ m_pYUVShader->SetAlpha(alpha/255/2);
+ if (m_pVideoFilterShader)
+ m_pVideoFilterShader->SetAlpha(alpha/255/2);
+
+ Render(flags, m_iYV12RenderBuffer);
+ }
+
+ VerifyGLState();
+ glEnable(GL_BLEND);
+ glFlush();
+}
+
+void CLinuxRendererGL::ClearBackBuffer()
+{
+ //set the entire backbuffer to black
+ glClearColor(m_clearColour, m_clearColour, m_clearColour, 0);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glClearColor(0,0,0,0);
+}
+
+//draw black bars around the video quad, this is more efficient than glClear()
+//since it only sets pixels to black that aren't going to be overwritten by the video
+void CLinuxRendererGL::DrawBlackBars()
+{
+ glDisable(GL_BLEND);
+
+ struct Svertex
+ {
+ float x,y,z;
+ };
+ Svertex vertices[24];
+ GLubyte count = 0;
+
+ m_renderSystem->EnableShader(ShaderMethodGL::SM_DEFAULT);
+ GLint posLoc = m_renderSystem->ShaderGetPos();
+ GLint uniCol = m_renderSystem->ShaderGetUniCol();
+
+ glUniform4f(uniCol, m_clearColour / 255.0f, m_clearColour / 255.0f, m_clearColour / 255.0f, 1.0f);
+
+ int osWindowWidth = CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth();
+ int osWindowHeight = CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight();
+
+ //top quad
+ if (m_destRect.y1 > 0.0f)
+ {
+ GLubyte quad = count;
+ vertices[quad].x = 0.0;
+ vertices[quad].y = 0.0;
+ vertices[quad].z = 0;
+ vertices[quad+1].x = osWindowWidth;
+ vertices[quad+1].y = 0;
+ vertices[quad+1].z = 0;
+ vertices[quad+2].x = osWindowWidth;
+ vertices[quad+2].y = m_destRect.y1;
+ vertices[quad+2].z = 0;
+ vertices[quad+3] = vertices[quad+2];
+ vertices[quad+4].x = 0;
+ vertices[quad+4].y = m_destRect.y1;
+ vertices[quad+4].z = 0;
+ vertices[quad+5] = vertices[quad];
+ count += 6;
+ }
+
+ // bottom quad
+ if (m_destRect.y2 < osWindowHeight)
+ {
+ GLubyte quad = count;
+ vertices[quad].x = 0.0;
+ vertices[quad].y = m_destRect.y2;
+ vertices[quad].z = 0;
+ vertices[quad+1].x = osWindowWidth;
+ vertices[quad+1].y = m_destRect.y2;
+ vertices[quad+1].z = 0;
+ vertices[quad+2].x = osWindowWidth;
+ vertices[quad+2].y = osWindowHeight;
+ vertices[quad+2].z = 0;
+ vertices[quad+3] = vertices[quad+2];
+ vertices[quad+4].x = 0;
+ vertices[quad+4].y = osWindowHeight;
+ vertices[quad+4].z = 0;
+ vertices[quad+5] = vertices[quad];
+ count += 6;
+ }
+
+ // left quad
+ if (m_destRect.x1 > 0.0f)
+ {
+ GLubyte quad = count;
+ vertices[quad].x = 0.0;
+ vertices[quad].y = m_destRect.y1;
+ vertices[quad].z = 0;
+ vertices[quad+1].x = m_destRect.x1;
+ vertices[quad+1].y = m_destRect.y1;
+ vertices[quad+1].z = 0;
+ vertices[quad+2].x = m_destRect.x1;
+ vertices[quad+2].y = m_destRect.y2;
+ vertices[quad+2].z = 0;
+ vertices[quad+3] = vertices[quad+2];
+ vertices[quad+4].x = 0;
+ vertices[quad+4].y = m_destRect.y2;
+ vertices[quad+4].z = 0;
+ vertices[quad+5] = vertices[quad];
+ count += 6;
+ }
+
+ // right quad
+ if (m_destRect.x2 < osWindowWidth)
+ {
+ GLubyte quad = count;
+ vertices[quad].x = m_destRect.x2;
+ vertices[quad].y = m_destRect.y1;
+ vertices[quad].z = 0;
+ vertices[quad+1].x = osWindowWidth;
+ vertices[quad+1].y = m_destRect.y1;
+ vertices[quad+1].z = 0;
+ vertices[quad+2].x = osWindowWidth;
+ vertices[quad+2].y = m_destRect.y2;
+ vertices[quad+2].z = 0;
+ vertices[quad+3] = vertices[quad+2];
+ vertices[quad+4].x = m_destRect.x2;
+ vertices[quad+4].y = m_destRect.y2;
+ vertices[quad+4].z = 0;
+ vertices[quad+5] = vertices[quad];
+ count += 6;
+ }
+
+ GLuint vertexVBO;
+
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(Svertex)*count, &vertices[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, GL_FALSE, sizeof(Svertex), 0);
+ glEnableVertexAttribArray(posLoc);
+
+ glDrawArrays(GL_TRIANGLES, 0, count);
+
+ glDisableVertexAttribArray(posLoc);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+
+ m_renderSystem->DisableShader();
+}
+
+void CLinuxRendererGL::UpdateVideoFilter()
+{
+ if (!m_pVideoFilterShader)
+ {
+ m_pVideoFilterShader = new DefaultFilterShader();
+ if (!m_pVideoFilterShader->CompileAndLink())
+ {
+ CLog::Log(LOGERROR, "CLinuxRendererGL::UpdateVideoFilter: Error compiling and linking video filter shader");
+ return;
+ }
+ }
+
+ bool pixelRatioChanged = (CDisplaySettings::GetInstance().GetPixelRatio() > 1.001f ||
+ CDisplaySettings::GetInstance().GetPixelRatio() < 0.999f) !=
+ (m_pixelRatio > 1.001f || m_pixelRatio < 0.999f);
+ bool nonLinStretchChanged = false;
+ bool cmsChanged = (m_cmsOn != m_ColorManager->IsEnabled()) ||
+ (m_cmsOn && !m_ColorManager->CheckConfiguration(m_cmsToken, m_srcPrimaries));
+ if (m_nonLinStretchGui != CDisplaySettings::GetInstance().IsNonLinearStretched() || pixelRatioChanged)
+ {
+ m_nonLinStretchGui = CDisplaySettings::GetInstance().IsNonLinearStretched();
+ m_pixelRatio = CDisplaySettings::GetInstance().GetPixelRatio();
+ m_reloadShaders = 1;
+ nonLinStretchChanged = true;
+
+ if (m_nonLinStretchGui && (m_pixelRatio < 0.999f || m_pixelRatio > 1.001f) && Supports(RENDERFEATURE_NONLINSTRETCH))
+ {
+ m_nonLinStretch = true;
+ CLog::Log(LOGDEBUG, "GL: Enabling non-linear stretch");
+ }
+ else
+ {
+ m_nonLinStretch = false;
+ CLog::Log(LOGDEBUG, "GL: Disabling non-linear stretch");
+ }
+ }
+
+ CRect srcRect, dstRect, viewRect;
+ GetVideoRect(srcRect, dstRect, viewRect);
+
+ if (m_scalingMethodGui == m_videoSettings.m_ScalingMethod &&
+ viewRect.Height() == m_viewRect.Height() &&
+ viewRect.Width() == m_viewRect.Width() &&
+ !nonLinStretchChanged && !cmsChanged)
+ return;
+ else
+ m_reloadShaders = 1;
+
+ // recompile YUV shader when non-linear stretch is turned on/off
+ // or when it's on and the scaling method changed
+ if (m_nonLinStretch || nonLinStretchChanged)
+ m_reloadShaders = 1;
+
+ if (cmsChanged)
+ {
+ if (m_ColorManager->IsEnabled())
+ {
+ if (!m_ColorManager->CheckConfiguration(m_cmsToken, m_srcPrimaries))
+ {
+ CLog::Log(LOGDEBUG, "CMS configuration changed, reload LUT");
+ LoadCLUT();
+ }
+ m_cmsOn = true;
+ }
+ else
+ {
+ m_cmsOn = false;
+ }
+ }
+
+ m_scalingMethodGui = m_videoSettings.m_ScalingMethod;
+ m_scalingMethod = m_scalingMethodGui;
+ m_viewRect = viewRect;
+
+ if (!Supports(m_scalingMethod))
+ {
+ CLog::Log(LOGWARNING,
+ "CLinuxRendererGL::UpdateVideoFilter - chosen scaling method {}, is not supported by "
+ "renderer",
+ m_scalingMethod);
+ m_scalingMethod = VS_SCALINGMETHOD_LINEAR;
+ }
+
+ if (m_pVideoFilterShader)
+ {
+ delete m_pVideoFilterShader;
+ m_pVideoFilterShader = nullptr;
+ }
+ m_fbo.fbo.Cleanup();
+
+ VerifyGLState();
+
+ if (m_scalingMethod == VS_SCALINGMETHOD_AUTO)
+ {
+ bool scaleSD = m_sourceHeight < 720 && m_sourceWidth < 1280;
+ bool scaleUp = (int)m_sourceHeight < m_viewRect.Height() && (int)m_sourceWidth < m_viewRect.Width();
+ bool scaleFps = m_fps < CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAutoScaleMaxFps + 0.01f;
+
+ if (Supports(VS_SCALINGMETHOD_LANCZOS3_FAST) && scaleSD && scaleUp && scaleFps)
+ m_scalingMethod = VS_SCALINGMETHOD_LANCZOS3_FAST;
+ else
+ m_scalingMethod = VS_SCALINGMETHOD_LINEAR;
+ }
+
+ switch (m_scalingMethod)
+ {
+ case VS_SCALINGMETHOD_NEAREST:
+ case VS_SCALINGMETHOD_LINEAR:
+ SetTextureFilter(m_scalingMethod == VS_SCALINGMETHOD_NEAREST ? GL_NEAREST : GL_LINEAR);
+ m_renderQuality = RQ_SINGLEPASS;
+ if (m_nonLinStretch)
+ {
+ m_pVideoFilterShader = new StretchFilterShader();
+ if (!m_pVideoFilterShader->CompileAndLink())
+ {
+ CLog::Log(LOGERROR, "GL: Error compiling and linking video filter shader");
+ break;
+ }
+ }
+ else
+ {
+ m_pVideoFilterShader = new DefaultFilterShader();
+ if (!m_pVideoFilterShader->CompileAndLink())
+ {
+ CLog::Log(LOGERROR, "GL: Error compiling and linking video filter shader");
+ break;
+ }
+ }
+ return;
+
+ case VS_SCALINGMETHOD_LANCZOS3_FAST:
+ case VS_SCALINGMETHOD_SPLINE36_FAST:
+ {
+ EShaderFormat fmt = GetShaderFormat();
+ if (fmt == SHADER_NV12 || (fmt >= SHADER_YV12 && fmt <= SHADER_YV12_16))
+ {
+ unsigned int major, minor;
+ m_renderSystem->GetRenderVersion(major, minor);
+ if (major >= 4)
+ {
+ SetTextureFilter(GL_LINEAR);
+ m_renderQuality = RQ_SINGLEPASS;
+ return;
+ }
+ }
+
+ [[fallthrough]];
+ }
+
+ case VS_SCALINGMETHOD_LANCZOS2:
+ case VS_SCALINGMETHOD_SPLINE36:
+ case VS_SCALINGMETHOD_LANCZOS3:
+ case VS_SCALINGMETHOD_CUBIC_B_SPLINE:
+ case VS_SCALINGMETHOD_CUBIC_MITCHELL:
+ case VS_SCALINGMETHOD_CUBIC_CATMULL:
+ case VS_SCALINGMETHOD_CUBIC_0_075:
+ case VS_SCALINGMETHOD_CUBIC_0_1:
+ if (m_renderMethod & RENDER_GLSL)
+ {
+ if (!m_fbo.fbo.Initialize())
+ {
+ CLog::Log(LOGERROR, "GL: Error initializing FBO");
+ break;
+ }
+
+ if (!m_fbo.fbo.CreateAndBindToTexture(GL_TEXTURE_2D, m_sourceWidth, m_sourceHeight, GL_RGBA16, GL_SHORT))
+ {
+ CLog::Log(LOGERROR, "GL: Error creating texture and binding to FBO");
+ break;
+ }
+ }
+
+ GLSLOutput *out;
+ out = new GLSLOutput(3,
+ m_useDithering,
+ m_ditherDepth,
+ m_cmsOn ? m_fullRange : false,
+ m_cmsOn ? m_tCLUTTex : 0,
+ m_CLUTsize);
+ m_pVideoFilterShader = new ConvolutionFilterShader(m_scalingMethod, m_nonLinStretch, out);
+ if (!m_pVideoFilterShader->CompileAndLink())
+ {
+ CLog::Log(LOGERROR, "GL: Error compiling and linking video filter shader");
+ break;
+ }
+
+ SetTextureFilter(GL_LINEAR);
+ m_renderQuality = RQ_MULTIPASS;
+ return;
+
+ case VS_SCALINGMETHOD_BICUBIC_SOFTWARE:
+ case VS_SCALINGMETHOD_LANCZOS_SOFTWARE:
+ case VS_SCALINGMETHOD_SINC_SOFTWARE:
+ case VS_SCALINGMETHOD_SINC8:
+ CLog::Log(LOGERROR, "GL: TODO: This scaler has not yet been implemented");
+ break;
+
+ default:
+ break;
+ }
+
+ CLog::Log(LOGERROR, "GL: Falling back to bilinear due to failure to init scaler");
+ if (m_pVideoFilterShader)
+ {
+ delete m_pVideoFilterShader;
+ m_pVideoFilterShader = nullptr;
+ }
+ m_fbo.fbo.Cleanup();
+
+ m_pVideoFilterShader = new DefaultFilterShader();
+ if (!m_pVideoFilterShader->CompileAndLink())
+ {
+ CLog::Log(LOGERROR, "CLinuxRendererGL::UpdateVideoFilter: Error compiling and linking defauilt video filter shader");
+ }
+
+ SetTextureFilter(GL_LINEAR);
+ m_renderQuality = RQ_SINGLEPASS;
+}
+
+void CLinuxRendererGL::LoadShaders(int field)
+{
+ m_reloadShaders = 0;
+
+ if (!LoadShadersHook())
+ {
+ int requestedMethod = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_RENDERMETHOD);
+ CLog::Log(LOGDEBUG, "GL: Requested render method: {}", requestedMethod);
+
+ if (m_pYUVShader)
+ {
+ delete m_pYUVShader;
+ m_pYUVShader = NULL;
+ }
+
+ // create regular progressive scan shader
+ // if single pass, create GLSLOutput helper and pass it to YUV2RGB shader
+ EShaderFormat shaderFormat = GetShaderFormat();
+ std::shared_ptr<GLSLOutput> out;
+ m_toneMapMethod = m_videoSettings.m_ToneMapMethod;
+ if (m_renderQuality == RQ_SINGLEPASS)
+ {
+ out = std::make_shared<GLSLOutput>(GLSLOutput(4, m_useDithering, m_ditherDepth,
+ m_cmsOn ? m_fullRange : false,
+ m_cmsOn ? m_tCLUTTex : 0,
+ m_CLUTsize));
+
+ if (m_scalingMethod == VS_SCALINGMETHOD_LANCZOS3_FAST || m_scalingMethod == VS_SCALINGMETHOD_SPLINE36_FAST)
+ {
+ m_pYUVShader = new YUV2RGBFilterShader4(m_textureTarget == GL_TEXTURE_RECTANGLE,
+ shaderFormat, m_nonLinStretch,
+ AVColorPrimaries::AVCOL_PRI_BT709, m_srcPrimaries,
+ m_toneMap,
+ m_toneMapMethod,
+ m_scalingMethod, out);
+ if (!m_cmsOn)
+ m_pYUVShader->SetConvertFullColorRange(m_fullRange);
+
+ CLog::Log(LOGINFO, "GL: Selecting YUV 2 RGB shader with filter");
+
+ if (m_pYUVShader && m_pYUVShader->CompileAndLink())
+ {
+ m_renderMethod = RENDER_GLSL;
+ UpdateVideoFilter();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "GL: Error enabling YUV2RGB GLSL shader");
+ delete m_pYUVShader;
+ m_pYUVShader = nullptr;
+ }
+ }
+ }
+
+ if (!m_pYUVShader)
+ {
+ m_pYUVShader = new YUV2RGBProgressiveShader(m_textureTarget == GL_TEXTURE_RECTANGLE, shaderFormat,
+ m_nonLinStretch && m_renderQuality == RQ_SINGLEPASS,
+ AVColorPrimaries::AVCOL_PRI_BT709, m_srcPrimaries, m_toneMap, m_toneMapMethod, out);
+
+ if (!m_cmsOn)
+ m_pYUVShader->SetConvertFullColorRange(m_fullRange);
+
+ CLog::Log(LOGINFO, "GL: Selecting YUV 2 RGB shader");
+
+ if (m_pYUVShader && m_pYUVShader->CompileAndLink())
+ {
+ m_renderMethod = RENDER_GLSL;
+ UpdateVideoFilter();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "GL: Error enabling YUV2RGB GLSL shader");
+ }
+ }
+ }
+
+ if (m_pboSupported)
+ {
+ CLog::Log(LOGINFO, "GL: Using GL_ARB_pixel_buffer_object");
+ m_pboUsed = true;
+ }
+ else
+ m_pboUsed = false;
+}
+
+void CLinuxRendererGL::UnInit()
+{
+ CLog::Log(LOGDEBUG, "LinuxRendererGL: Cleaning up GL resources");
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ glFinish();
+
+ // YV12 textures
+ for (int i = 0; i < NUM_BUFFERS; ++i)
+ {
+ ReleaseBuffer(i);
+ DeleteTexture(i);
+ }
+
+ DeleteCLUT();
+
+ // cleanup framebuffer object if it was in use
+ m_fbo.fbo.Cleanup();
+ m_bValidated = false;
+ m_bConfigured = false;
+}
+
+bool CLinuxRendererGL::Render(unsigned int flags, int renderBuffer)
+{
+ // obtain current field, if interlaced
+ if( flags & RENDER_FLAG_TOP)
+ m_currentField = FIELD_TOP;
+
+ else if (flags & RENDER_FLAG_BOT)
+ m_currentField = FIELD_BOT;
+
+ else
+ m_currentField = FIELD_FULL;
+
+ // call texture load function
+ if (!UploadTexture(renderBuffer))
+ {
+ return false;
+ }
+
+ if (RenderHook(renderBuffer))
+ ;
+ else if (m_renderMethod & RENDER_GLSL)
+ {
+ UpdateVideoFilter();
+ switch(m_renderQuality)
+ {
+ case RQ_LOW:
+ case RQ_SINGLEPASS:
+ RenderSinglePass(renderBuffer, m_currentField);
+ VerifyGLState();
+ break;
+
+ case RQ_MULTIPASS:
+ RenderToFBO(renderBuffer, m_currentField);
+ RenderFromFBO();
+ VerifyGLState();
+ break;
+ }
+ }
+ else
+ {
+ return false;
+ }
+
+ AfterRenderHook(renderBuffer);
+ return true;
+}
+
+void CLinuxRendererGL::RenderSinglePass(int index, int field)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = m_buffers[index].fields[field];
+
+ CheckVideoParameters(index);
+
+ if (m_reloadShaders)
+ {
+ m_reloadShaders = 0;
+ LoadShaders(field);
+ }
+
+ glDisable(GL_DEPTH_TEST);
+
+ // Y
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(m_textureTarget, planes[0].id);
+
+ // U
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(m_textureTarget, planes[1].id);
+
+ // V
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(m_textureTarget, planes[2].id);
+
+ glActiveTexture(GL_TEXTURE0);
+ VerifyGLState();
+
+ m_pYUVShader->SetBlack(m_videoSettings.m_Brightness * 0.01f - 0.5f);
+ m_pYUVShader->SetContrast(m_videoSettings.m_Contrast * 0.02f);
+ m_pYUVShader->SetWidth(planes[0].texwidth);
+ m_pYUVShader->SetHeight(planes[0].texheight);
+ m_pYUVShader->SetColParams(buf.m_srcColSpace, buf.m_srcBits, !buf.m_srcFullRange, buf.m_srcTextureBits);
+ m_pYUVShader->SetDisplayMetadata(buf.hasDisplayMetadata, buf.displayMetadata,
+ buf.hasLightMetadata, buf.lightMetadata);
+ m_pYUVShader->SetToneMapParam(m_toneMapMethod, m_videoSettings.m_ToneMapParam);
+
+ //disable non-linear stretch when a dvd menu is shown, parts of the menu are rendered through the overlay renderer
+ //having non-linear stretch on breaks the alignment
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsInMenu())
+ m_pYUVShader->SetNonLinStretch(1.0);
+ else
+ m_pYUVShader->SetNonLinStretch(pow(CDisplaySettings::GetInstance().GetPixelRatio(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoNonLinStretchRatio));
+
+ if (field == FIELD_TOP)
+ m_pYUVShader->SetField(1);
+ else if(field == FIELD_BOT)
+ m_pYUVShader->SetField(0);
+
+ m_pYUVShader->SetMatrices(glMatrixProject.Get(), glMatrixModview.Get());
+ m_pYUVShader->Enable();
+
+ GLubyte idx[4] = {0, 1, 3, 2}; //determines order of the vertices
+ GLuint vertexVBO;
+ GLuint indexVBO;
+
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ float u2, v2;
+ float u3, v3;
+ }vertex[4];
+
+ GLint vertLoc = m_pYUVShader->GetVertexLoc();
+ GLint Yloc = m_pYUVShader->GetYcoordLoc();
+ GLint Uloc = m_pYUVShader->GetUcoordLoc();
+ GLint Vloc = m_pYUVShader->GetVcoordLoc();
+
+ // Setup vertex position values
+ for(int i = 0; i < 4; i++)
+ {
+ vertex[i].x = m_rotatedDestCoords[i].x;
+ vertex[i].y = m_rotatedDestCoords[i].y;
+ vertex[i].z = 0.0f;// set z to 0
+ }
+
+ // bottom left
+ vertex[0].u1 = planes[0].rect.x1;
+ vertex[0].v1 = planes[0].rect.y1;
+ vertex[0].u2 = planes[1].rect.x1;
+ vertex[0].v2 = planes[1].rect.y1;
+ vertex[0].u3 = planes[2].rect.x1;
+ vertex[0].v3 = planes[2].rect.y1;
+
+ // bottom right
+ vertex[1].u1 = planes[0].rect.x2;
+ vertex[1].v1 = planes[0].rect.y1;
+ vertex[1].u2 = planes[1].rect.x2;
+ vertex[1].v2 = planes[1].rect.y1;
+ vertex[1].u3 = planes[2].rect.x2;
+ vertex[1].v3 = planes[2].rect.y1;
+
+ // top right
+ vertex[2].u1 = planes[0].rect.x2;
+ vertex[2].v1 = planes[0].rect.y2;
+ vertex[2].u2 = planes[1].rect.x2;
+ vertex[2].v2 = planes[1].rect.y2;
+ vertex[2].u3 = planes[2].rect.x2;
+ vertex[2].v3 = planes[2].rect.y2;
+
+ // top left
+ vertex[3].u1 = planes[0].rect.x1;
+ vertex[3].v1 = planes[0].rect.y2;
+ vertex[3].u2 = planes[1].rect.x1;
+ vertex[3].v2 = planes[1].rect.y2;
+ vertex[3].u3 = planes[2].rect.x1;
+ vertex[3].v3 = planes[2].rect.y2;
+
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex)*4, &vertex[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(vertLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glVertexAttribPointer(Yloc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+ glVertexAttribPointer(Uloc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u2)));
+ if (Vloc != -1)
+ glVertexAttribPointer(Vloc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u3)));
+
+ glEnableVertexAttribArray(vertLoc);
+ glEnableVertexAttribArray(Yloc);
+ glEnableVertexAttribArray(Uloc);
+ if (Vloc != -1)
+ glEnableVertexAttribArray(Vloc);
+
+ glGenBuffers(1, &indexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte)*4, idx, GL_STATIC_DRAW);
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+ VerifyGLState();
+
+ glDisableVertexAttribArray(vertLoc);
+ glDisableVertexAttribArray(Yloc);
+ glDisableVertexAttribArray(Uloc);
+ if (Vloc != -1)
+ glDisableVertexAttribArray(Vloc);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &indexVBO);
+
+ m_pYUVShader->Disable();
+ VerifyGLState();
+
+ VerifyGLState();
+}
+
+void CLinuxRendererGL::RenderToFBO(int index, int field, bool weave /*= false*/)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = m_buffers[index].fields[field];
+
+ CheckVideoParameters(index);
+
+ if (m_reloadShaders)
+ {
+ m_reloadShaders = 0;
+ LoadShaders(m_currentField);
+ }
+
+ if (!m_fbo.fbo.IsValid())
+ {
+ if (!m_fbo.fbo.Initialize())
+ {
+ CLog::Log(LOGERROR, "GL: Error initializing FBO");
+ return;
+ }
+
+ if (!m_fbo.fbo.CreateAndBindToTexture(GL_TEXTURE_2D, m_sourceWidth, m_sourceHeight, GL_RGBA, GL_SHORT))
+ {
+ CLog::Log(LOGERROR, "GL: Error creating texture and binding to FBO");
+ return;
+ }
+ }
+
+ glDisable(GL_DEPTH_TEST);
+
+ // Y
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(m_textureTarget, planes[0].id);
+ VerifyGLState();
+
+ // U
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(m_textureTarget, planes[1].id);
+ VerifyGLState();
+
+ // V
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(m_textureTarget, planes[2].id);
+ VerifyGLState();
+
+ glActiveTexture(GL_TEXTURE0);
+ VerifyGLState();
+
+ // make sure the yuv shader is loaded and ready to go
+ if (!m_pYUVShader || (!m_pYUVShader->OK()))
+ {
+ CLog::Log(LOGERROR, "GL: YUV shader not active, cannot do multipass render");
+ return;
+ }
+
+ m_fbo.fbo.BeginRender();
+ VerifyGLState();
+
+ m_pYUVShader->SetBlack(m_videoSettings.m_Brightness * 0.01f - 0.5f);
+ m_pYUVShader->SetContrast(m_videoSettings.m_Contrast * 0.02f);
+ m_pYUVShader->SetWidth(planes[0].texwidth);
+ m_pYUVShader->SetHeight(planes[0].texheight);
+ m_pYUVShader->SetNonLinStretch(1.0);
+ m_pYUVShader->SetColParams(buf.m_srcColSpace, buf.m_srcBits, !buf.m_srcFullRange, buf.m_srcTextureBits);
+ m_pYUVShader->SetDisplayMetadata(buf.hasDisplayMetadata, buf.displayMetadata,
+ buf.hasLightMetadata, buf.lightMetadata);
+ m_pYUVShader->SetToneMapParam(m_toneMapMethod, m_videoSettings.m_ToneMapParam);
+
+ if (field == FIELD_TOP)
+ m_pYUVShader->SetField(1);
+ else if (field == FIELD_BOT)
+ m_pYUVShader->SetField(0);
+
+ VerifyGLState();
+
+ glMatrixModview.Push();
+ glMatrixModview->LoadIdentity();
+ glMatrixModview.Load();
+
+ glMatrixProject.Push();
+ glMatrixProject->LoadIdentity();
+ glMatrixProject->Ortho2D(0, m_sourceWidth, 0, m_sourceHeight);
+ glMatrixProject.Load();
+
+ CRect viewport;
+ m_renderSystem->GetViewPort(viewport);
+ glViewport(0, 0, m_sourceWidth, m_sourceHeight);
+ glScissor (0, 0, m_sourceWidth, m_sourceHeight);
+
+ m_pYUVShader->SetMatrices(glMatrixProject.Get(), glMatrixModview.Get());
+ if (!m_pYUVShader->Enable())
+ {
+ CLog::Log(LOGERROR, "GL: Error enabling YUV shader");
+ }
+
+ m_fbo.width = planes[0].rect.x2 - planes[0].rect.x1;
+ m_fbo.height = planes[0].rect.y2 - planes[0].rect.y1;
+
+ if (m_textureTarget == GL_TEXTURE_2D)
+ {
+ m_fbo.width *= planes[0].texwidth;
+ m_fbo.height *= planes[0].texheight;
+ }
+ m_fbo.width *= planes[0].pixpertex_x;
+ m_fbo.height *= planes[0].pixpertex_y;
+ if (weave)
+ m_fbo.height *= 2;
+
+ // 1st Pass to video frame size
+ GLubyte idx[4] = {0, 1, 3, 2}; //determines order of the vertices
+ GLuint vertexVBO;
+ GLuint indexVBO;
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ float u2, v2;
+ float u3, v3;
+ } vertex[4];
+
+ GLint vertLoc = m_pYUVShader->GetVertexLoc();
+ GLint Yloc = m_pYUVShader->GetYcoordLoc();
+ GLint Uloc = m_pYUVShader->GetUcoordLoc();
+ GLint Vloc = m_pYUVShader->GetVcoordLoc();
+
+ // top left
+ vertex[0].x = 0.0f;
+ vertex[0].y = 0.0f;
+ vertex[0].z = 0.0f;
+ vertex[0].u1 = planes[0].rect.x1;
+ vertex[0].v1 = planes[0].rect.y1;
+ vertex[0].u2 = planes[1].rect.x1;
+ vertex[0].v2 = planes[1].rect.y1;
+ vertex[0].u3 = planes[2].rect.x1;
+ vertex[0].v3 = planes[2].rect.y1;
+
+ // top right
+ vertex[1].x = m_fbo.width;
+ vertex[1].y = 0.0f;
+ vertex[1].z = 0.0f;
+ vertex[1].u1 = planes[0].rect.x2;
+ vertex[1].v1 = planes[0].rect.y1;
+ vertex[1].u2 = planes[1].rect.x2;
+ vertex[1].v2 = planes[1].rect.y1;
+ vertex[1].u3 = planes[2].rect.x2;
+ vertex[1].v3 = planes[2].rect.y1;
+
+ // bottom right
+ vertex[2].x = m_fbo.width;
+ vertex[2].y = m_fbo.height;
+ vertex[2].z = 0.0f;
+ vertex[2].u1 = planes[0].rect.x2;
+ vertex[2].v1 = planes[0].rect.y2;
+ vertex[2].u2 = planes[1].rect.x2;
+ vertex[2].v2 = planes[1].rect.y2;
+ vertex[2].u3 = planes[2].rect.x2;
+ vertex[2].v3 = planes[2].rect.y2;
+
+ // bottom left
+ vertex[3].x = 0.0f;
+ vertex[3].y = m_fbo.height;
+ vertex[3].z = 0.0f;
+ vertex[3].u1 = planes[0].rect.x1;
+ vertex[3].v1 = planes[0].rect.y2;
+ vertex[3].u2 = planes[1].rect.x1;
+ vertex[3].v2 = planes[1].rect.y2;
+ vertex[3].u3 = planes[2].rect.x1;
+ vertex[3].v3 = planes[2].rect.y2;
+
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex)*4, &vertex[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(vertLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glVertexAttribPointer(Yloc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+ glVertexAttribPointer(Uloc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u2)));
+ if (Vloc != -1)
+ glVertexAttribPointer(Vloc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u3)));
+
+ glEnableVertexAttribArray(vertLoc);
+ glEnableVertexAttribArray(Yloc);
+ glEnableVertexAttribArray(Uloc);
+ if (Vloc != -1)
+ glEnableVertexAttribArray(Vloc);
+
+ glGenBuffers(1, &indexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte)*4, idx, GL_STATIC_DRAW);
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+ VerifyGLState();
+
+ glDisableVertexAttribArray(vertLoc);
+ glDisableVertexAttribArray(Yloc);
+ glDisableVertexAttribArray(Uloc);
+ if (Vloc != -1)
+ glDisableVertexAttribArray(Vloc);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &indexVBO);
+
+ m_pYUVShader->Disable();
+
+ glMatrixModview.PopLoad();
+ glMatrixProject.PopLoad();
+
+ m_renderSystem->SetViewPort(viewport);
+
+ m_fbo.fbo.EndRender();
+
+ VerifyGLState();
+}
+
+void CLinuxRendererGL::RenderFromFBO()
+{
+ glActiveTexture(GL_TEXTURE0);
+ VerifyGLState();
+
+ // Use regular normalized texture coordinates
+ // 2nd Pass to screen size with optional video filter
+
+ if (!m_pVideoFilterShader)
+ {
+ CLog::Log(LOGERROR, "CLinuxRendererGL::RenderFromFBO - no videofilter shader");
+ return;
+ }
+
+ GLint filter;
+ if (!m_pVideoFilterShader->GetTextureFilter(filter))
+ filter = m_scalingMethod == VS_SCALINGMETHOD_NEAREST ? GL_NEAREST : GL_LINEAR;
+
+ m_fbo.fbo.SetFiltering(GL_TEXTURE_2D, filter);
+ m_pVideoFilterShader->SetSourceTexture(0);
+ m_pVideoFilterShader->SetWidth(m_sourceWidth);
+ m_pVideoFilterShader->SetHeight(m_sourceHeight);
+
+ //disable non-linear stretch when a dvd menu is shown, parts of the menu are rendered through the overlay renderer
+ //having non-linear stretch on breaks the alignment
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsInMenu())
+ m_pVideoFilterShader->SetNonLinStretch(1.0);
+ else
+ m_pVideoFilterShader->SetNonLinStretch(pow(CDisplaySettings::GetInstance().GetPixelRatio(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoNonLinStretchRatio));
+
+ m_pVideoFilterShader->SetMatrices(glMatrixProject.Get(), glMatrixModview.Get());
+ m_pVideoFilterShader->Enable();
+
+ VerifyGLState();
+
+ float imgwidth = m_fbo.width / m_sourceWidth;
+ float imgheight = m_fbo.height / m_sourceHeight;
+
+ GLubyte idx[4] = {0, 1, 3, 2}; //determines order of the vertices
+ GLuint vertexVBO;
+ GLuint indexVBO;
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ } vertex[4];
+
+ GLint vertLoc = m_pVideoFilterShader->GetVertexLoc();
+ GLint loc = m_pVideoFilterShader->GetCoordLoc();
+
+ // Setup vertex position values
+ // top left
+ vertex[0].x = m_rotatedDestCoords[0].x;
+ vertex[0].y = m_rotatedDestCoords[0].y;
+ vertex[0].z = 0.0f;
+ vertex[0].u1 = 0.0;
+ vertex[0].v1 = 0.0;
+
+ // top right
+ vertex[1].x = m_rotatedDestCoords[1].x;
+ vertex[1].y = m_rotatedDestCoords[1].y;
+ vertex[1].z = 0.0f;
+ vertex[1].u1 = imgwidth;
+ vertex[1].v1 = 0.0f;
+
+ // bottom right
+ vertex[2].x = m_rotatedDestCoords[2].x;
+ vertex[2].y = m_rotatedDestCoords[2].y;
+ vertex[2].z = 0.0f;
+ vertex[2].u1 = imgwidth;
+ vertex[2].v1 = imgheight;
+
+ // bottom left
+ vertex[3].x = m_rotatedDestCoords[3].x;
+ vertex[3].y = m_rotatedDestCoords[3].y;
+ vertex[3].z = 0.0f;
+ vertex[3].u1 = 0.0f;
+ vertex[3].v1 = imgheight;
+
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex)*4, &vertex[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(vertLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glVertexAttribPointer(loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+
+ glEnableVertexAttribArray(vertLoc);
+ glEnableVertexAttribArray(loc);
+
+ glGenBuffers(1, &indexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte)*4, idx, GL_STATIC_DRAW);
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+ VerifyGLState();
+
+ glDisableVertexAttribArray(loc);
+ glDisableVertexAttribArray(vertLoc);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &indexVBO);
+
+ m_pVideoFilterShader->Disable();
+
+ VerifyGLState();
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+ VerifyGLState();
+}
+
+void CLinuxRendererGL::RenderProgressiveWeave(int index, int field)
+{
+ bool scale = (int)m_sourceHeight != m_destRect.Height() ||
+ (int)m_sourceWidth != m_destRect.Width();
+
+ if (m_fbo.fbo.IsSupported() && (scale || m_renderQuality == RQ_MULTIPASS))
+ {
+ glEnable(GL_POLYGON_STIPPLE);
+ glPolygonStipple(stipple_weave);
+ RenderToFBO(index, FIELD_TOP, true);
+ glPolygonStipple(stipple_weave+4);
+ RenderToFBO(index, FIELD_BOT, true);
+ glDisable(GL_POLYGON_STIPPLE);
+ RenderFromFBO();
+ }
+ else
+ {
+ glEnable(GL_POLYGON_STIPPLE);
+ glPolygonStipple(stipple_weave);
+ RenderSinglePass(index, FIELD_TOP);
+ glPolygonStipple(stipple_weave+4);
+ RenderSinglePass(index, FIELD_BOT);
+ glDisable(GL_POLYGON_STIPPLE);
+ }
+}
+
+void CLinuxRendererGL::RenderRGB(int index, int field)
+{
+ CYuvPlane &plane = m_buffers[index].fields[FIELD_FULL][0];
+
+ glActiveTexture(GL_TEXTURE0);
+
+ glBindTexture(m_textureTarget, plane.id);
+
+ // make sure we know the correct texture size
+ GetPlaneTextureSize(plane);
+
+ if (!m_pVideoFilterShader)
+ {
+ CLog::Log(LOGERROR, "CLinuxRendererGL::RenderRGB - no videofilter shader");
+ return;
+ }
+
+ GLint filter;
+ if (!m_pVideoFilterShader->GetTextureFilter(filter))
+ filter = m_scalingMethod == VS_SCALINGMETHOD_NEAREST ? GL_NEAREST : GL_LINEAR;
+
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, filter);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, filter);
+ m_pVideoFilterShader->SetSourceTexture(0);
+ m_pVideoFilterShader->SetWidth(m_sourceWidth);
+ m_pVideoFilterShader->SetHeight(m_sourceHeight);
+
+ //disable non-linear stretch when a dvd menu is shown, parts of the menu are rendered through the overlay renderer
+ //having non-linear stretch on breaks the alignment
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsInMenu())
+ m_pVideoFilterShader->SetNonLinStretch(1.0);
+ else
+ m_pVideoFilterShader->SetNonLinStretch(pow(CDisplaySettings::GetInstance().GetPixelRatio(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoNonLinStretchRatio));
+
+ m_pVideoFilterShader->SetMatrices(glMatrixProject.Get(), glMatrixModview.Get());
+ m_pVideoFilterShader->Enable();
+
+ GLubyte idx[4] = {0, 1, 3, 2}; //determines order of the vertices
+ GLuint vertexVBO;
+ GLuint indexVBO;
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ } vertex[4];
+
+ GLint vertLoc = m_pVideoFilterShader->GetVertexLoc();
+ GLint loc = m_pVideoFilterShader->GetCoordLoc();
+
+ // Setup vertex position values
+ // top left
+ vertex[0].x = m_rotatedDestCoords[0].x;
+ vertex[0].y = m_rotatedDestCoords[0].y;
+ vertex[0].z = 0.0f;
+ vertex[0].u1 = plane.rect.x1;
+ vertex[0].v1 = plane.rect.y1;
+
+ // top right
+ vertex[1].x = m_rotatedDestCoords[1].x;
+ vertex[1].y = m_rotatedDestCoords[1].y;
+ vertex[1].z = 0.0f;
+ vertex[1].u1 = plane.rect.x2;
+ vertex[1].v1 = plane.rect.y1;
+
+ // bottom right
+ vertex[2].x = m_rotatedDestCoords[2].x;
+ vertex[2].y = m_rotatedDestCoords[2].y;
+ vertex[2].z = 0.0f;
+ vertex[2].u1 = plane.rect.x2;
+ vertex[2].v1 = plane.rect.y2;
+
+ // bottom left
+ vertex[3].x = m_rotatedDestCoords[3].x;
+ vertex[3].y = m_rotatedDestCoords[3].y;
+ vertex[3].z = 0.0f;
+ vertex[3].u1 = plane.rect.x1;
+ vertex[3].v1 = plane.rect.y2;
+
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex)*4, &vertex[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(vertLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glVertexAttribPointer(loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+
+ glEnableVertexAttribArray(vertLoc);
+ glEnableVertexAttribArray(loc);
+
+ glGenBuffers(1, &indexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte)*4, idx, GL_STATIC_DRAW);
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &indexVBO);
+
+ VerifyGLState();
+
+ m_pVideoFilterShader->Disable();
+
+ glBindTexture(m_textureTarget, 0);
+}
+
+bool CLinuxRendererGL::RenderCapture(CRenderCapture* capture)
+{
+ if (!m_bValidated)
+ return false;
+
+ // save current video rect
+ CRect saveSize = m_destRect;
+
+ saveRotatedCoords();//backup current m_rotatedDestCoords
+
+ // new video rect is capture size
+ m_destRect.SetRect(0, 0, (float)capture->GetWidth(), (float)capture->GetHeight());
+ MarkDirty();
+ syncDestRectToRotatedPoints();//syncs the changed destRect to m_rotatedDestCoords
+
+ //invert Y axis to get non-inverted image
+ glDisable(GL_BLEND);
+ glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
+
+ glMatrixModview.Push();
+ glMatrixModview->Translatef(0.0f, capture->GetHeight(), 0.0f);
+ glMatrixModview->Scalef(1.0f, -1.0f, 1.0f);
+ glMatrixModview.Load();
+
+ capture->BeginRender();
+
+ Render(RENDER_FLAG_NOOSD, m_iYV12RenderBuffer);
+ // read pixels
+ glReadPixels(0, CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight() - capture->GetHeight(), capture->GetWidth(), capture->GetHeight(),
+ GL_BGRA, GL_UNSIGNED_BYTE, capture->GetRenderBuffer());
+
+ capture->EndRender();
+
+ // revert model view matrix
+ glMatrixModview.PopLoad();
+
+ // restore original video rect
+ m_destRect = saveSize;
+ restoreRotatedCoords();//restores the previous state of the rotated dest coords
+
+ return true;
+}
+
+
+GLint CLinuxRendererGL::GetInternalFormat(GLint format, int bpp)
+{
+ unsigned int major, minor;
+ m_renderSystem->GetRenderVersion(major, minor);
+ if (bpp == 2)
+ {
+ if (format == GL_RED)
+ {
+ if (major > 2)
+ return GL_R16;
+ else
+ return GL_LUMINANCE16;
+ }
+ }
+ else
+ {
+ if (format == GL_RED)
+ {
+ if (major > 2)
+ return GL_RED;
+ else
+ return GL_LUMINANCE;
+ }
+ }
+
+ return format;
+}
+
+//-----------------------------------------------------------------------------
+// Textures
+//-----------------------------------------------------------------------------
+
+bool CLinuxRendererGL::CreateTexture(int index)
+{
+ if (m_format == AV_PIX_FMT_NV12)
+ return CreateNV12Texture(index);
+ else if (m_format == AV_PIX_FMT_YUYV422 ||
+ m_format == AV_PIX_FMT_UYVY422)
+ return CreateYUV422PackedTexture(index);
+ else
+ return CreateYV12Texture(index);
+}
+
+void CLinuxRendererGL::DeleteTexture(int index)
+{
+ CPictureBuffer& buf = m_buffers[index];
+ buf.loaded = false;
+
+ if (m_format == AV_PIX_FMT_NV12)
+ DeleteNV12Texture(index);
+ else if (m_format == AV_PIX_FMT_YUYV422 ||
+ m_format == AV_PIX_FMT_UYVY422)
+ DeleteYUV422PackedTexture(index);
+ else
+ DeleteYV12Texture(index);
+}
+
+bool CLinuxRendererGL::UploadTexture(int index)
+{
+ if (!m_buffers[index].videoBuffer)
+ return false;
+
+ bool ret = true;
+
+ if (!m_buffers[index].loaded)
+ {
+ YuvImage &dst = m_buffers[index].image;
+ YuvImage src;
+ m_buffers[index].videoBuffer->GetPlanes(src.plane);
+ m_buffers[index].videoBuffer->GetStrides(src.stride);
+
+ UnBindPbo(m_buffers[index]);
+
+ if (m_format == AV_PIX_FMT_NV12)
+ {
+ CVideoBuffer::CopyNV12Picture(&dst, &src);
+ BindPbo(m_buffers[index]);
+ ret = UploadNV12Texture(index);
+ }
+ else if (m_format == AV_PIX_FMT_YUYV422 ||
+ m_format == AV_PIX_FMT_UYVY422)
+ {
+ CVideoBuffer::CopyYUV422PackedPicture(&dst, &src);
+ BindPbo(m_buffers[index]);
+ ret = UploadYUV422PackedTexture(index);
+ }
+ else
+ {
+ CVideoBuffer::CopyPicture(&dst, &src);
+ BindPbo(m_buffers[index]);
+ ret = UploadYV12Texture(index);
+ }
+
+ if (ret)
+ m_buffers[index].loaded = true;
+ }
+
+ if (ret)
+ CalculateTextureSourceRects(index, 3);
+
+ return ret;
+}
+
+//********************************************************************************************************
+// YV12 Texture creation, deletion, copying + clearing
+//********************************************************************************************************
+
+bool CLinuxRendererGL::CreateYV12Texture(int index)
+{
+ /* since we also want the field textures, pitch must be texture aligned */
+ unsigned p;
+
+ CPictureBuffer &buf = m_buffers[index];
+ YuvImage &im = m_buffers[index].image;
+ GLuint *pbo = m_buffers[index].pbo;
+
+ DeleteYV12Texture(index);
+
+ im.height = m_sourceHeight;
+ im.width = m_sourceWidth;
+ im.cshift_x = 1;
+ im.cshift_y = 1;
+
+ switch (m_format)
+ {
+ case AV_PIX_FMT_YUV420P16:
+ buf.m_srcTextureBits = 16;
+ break;
+ case AV_PIX_FMT_YUV420P14:
+ buf.m_srcTextureBits = 14;
+ break;
+ case AV_PIX_FMT_YUV420P12:
+ buf.m_srcTextureBits = 12;
+ break;
+ case AV_PIX_FMT_YUV420P10:
+ buf.m_srcTextureBits = 10;
+ break;
+ case AV_PIX_FMT_YUV420P9:
+ buf.m_srcTextureBits = 9;
+ break;
+ default:
+ break;
+ }
+ if (buf.m_srcTextureBits > 8)
+ im.bpp = 2;
+ else
+ im.bpp = 1;
+
+ im.stride[0] = im.bpp * im.width;
+ im.stride[1] = im.bpp * (im.width >> im.cshift_x);
+ im.stride[2] = im.bpp * (im.width >> im.cshift_x);
+
+ im.planesize[0] = im.stride[0] * im.height;
+ im.planesize[1] = im.stride[1] * (im.height >> im.cshift_y);
+ im.planesize[2] = im.stride[2] * (im.height >> im.cshift_y);
+
+ bool pboSetup = false;
+ if (m_pboUsed)
+ {
+ pboSetup = true;
+ glGenBuffers(3, pbo);
+
+ for (int i = 0; i < 3; i++)
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo[i]);
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, im.planesize[i] + PBO_OFFSET, 0, GL_STREAM_DRAW);
+ void* pboPtr = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
+ if (pboPtr)
+ {
+ im.plane[i] = (uint8_t*) pboPtr + PBO_OFFSET;
+ memset(im.plane[i], 0, im.planesize[i]);
+ }
+ else
+ {
+ CLog::Log(LOGWARNING,"GL: failed to set up pixel buffer object");
+ pboSetup = false;
+ break;
+ }
+ }
+
+ if (!pboSetup)
+ {
+ for (int i = 0; i < 3; i++)
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo[i]);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ }
+ glDeleteBuffers(3, pbo);
+ memset(m_buffers[index].pbo, 0, sizeof(m_buffers[index].pbo));
+ }
+
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+ }
+
+ if (!pboSetup)
+ {
+ for (int i = 0; i < 3; i++)
+ im.plane[i] = new uint8_t[im.planesize[i]];
+ }
+
+ for(int f = 0;f<MAX_FIELDS;f++)
+ {
+ for(p = 0;p<YuvImage::MAX_PLANES;p++)
+ {
+ if (!glIsTexture(m_buffers[index].fields[f][p].id))
+ {
+ glGenTextures(1, &m_buffers[index].fields[f][p].id);
+ VerifyGLState();
+ }
+ m_buffers[index].fields[f][p].pbo = pbo[p];
+ }
+ }
+
+ // YUV
+ for (int f = FIELD_FULL; f<=FIELD_BOT ; f++)
+ {
+ int fieldshift = (f==FIELD_FULL) ? 0 : 1;
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = m_buffers[index].fields[f];
+
+ planes[0].texwidth = im.width;
+ planes[0].texheight = im.height >> fieldshift;
+
+ planes[1].texwidth = planes[0].texwidth >> im.cshift_x;
+ planes[1].texheight = planes[0].texheight >> im.cshift_y;
+ planes[2].texwidth = planes[0].texwidth >> im.cshift_x;
+ planes[2].texheight = planes[0].texheight >> im.cshift_y;
+
+ for (int p = 0; p < 3; p++)
+ {
+ planes[p].pixpertex_x = 1;
+ planes[p].pixpertex_y = 1;
+ }
+
+ for (int p = 0; p < 3; p++)
+ {
+ CYuvPlane &plane = planes[p];
+ if (plane.texwidth * plane.texheight == 0)
+ continue;
+
+ glBindTexture(m_textureTarget, plane.id);
+ GLint internalformat;
+ internalformat = GetInternalFormat(GL_RED, im.bpp);
+ if (im.bpp == 2)
+ glTexImage2D(m_textureTarget, 0, internalformat, plane.texwidth, plane.texheight, 0, GL_RED, GL_UNSIGNED_SHORT, NULL);
+ else
+ glTexImage2D(m_textureTarget, 0, internalformat, plane.texwidth, plane.texheight, 0, GL_RED, GL_UNSIGNED_BYTE, NULL);
+
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ VerifyGLState();
+ }
+ }
+ return true;
+}
+
+bool CLinuxRendererGL::UploadYV12Texture(int source)
+{
+ CPictureBuffer& buf = m_buffers[source];
+ YuvImage* im = &buf.image;
+
+ bool deinterlacing;
+ if (m_currentField == FIELD_FULL)
+ deinterlacing = false;
+ else
+ deinterlacing = true;
+
+ VerifyGLState();
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT,1);
+
+ if (deinterlacing)
+ {
+ // Load Even Y Field
+ LoadPlane(buf.fields[FIELD_TOP][0] , GL_RED,
+ im->width, im->height >> 1,
+ im->stride[0]*2, im->bpp, im->plane[0] );
+
+ //load Odd Y Field
+ LoadPlane(buf.fields[FIELD_BOT][0], GL_RED,
+ im->width, im->height >> 1,
+ im->stride[0]*2, im->bpp, im->plane[0] + im->stride[0]);
+
+ // Load Even U & V Fields
+ LoadPlane(buf.fields[FIELD_TOP][1], GL_RED,
+ im->width >> im->cshift_x, im->height >> (im->cshift_y + 1),
+ im->stride[1]*2, im->bpp, im->plane[1]);
+
+ LoadPlane(buf.fields[FIELD_TOP][2], GL_RED,
+ im->width >> im->cshift_x, im->height >> (im->cshift_y + 1),
+ im->stride[2]*2, im->bpp, im->plane[2]);
+
+ // Load Odd U & V Fields
+ LoadPlane(buf.fields[FIELD_BOT][1], GL_RED,
+ im->width >> im->cshift_x, im->height >> (im->cshift_y + 1),
+ im->stride[1]*2, im->bpp, im->plane[1] + im->stride[1]);
+
+ LoadPlane(buf.fields[FIELD_BOT][2], GL_RED,
+ im->width >> im->cshift_x, im->height >> (im->cshift_y + 1),
+ im->stride[2]*2, im->bpp, im->plane[2] + im->stride[2]);
+ }
+ else
+ {
+ //Load Y plane
+ LoadPlane(buf.fields[FIELD_FULL][0], GL_RED,
+ im->width, im->height,
+ im->stride[0], im->bpp, im->plane[0]);
+
+ //load U plane
+ LoadPlane(buf.fields[FIELD_FULL][1], GL_RED,
+ im->width >> im->cshift_x, im->height >> im->cshift_y,
+ im->stride[1], im->bpp, im->plane[1]);
+
+ //load V plane
+ LoadPlane(buf.fields[FIELD_FULL][2], GL_RED,
+ im->width >> im->cshift_x, im->height >> im->cshift_y,
+ im->stride[2], im->bpp, im->plane[2]);
+ }
+
+ VerifyGLState();
+
+ return true;
+}
+
+void CLinuxRendererGL::DeleteYV12Texture(int index)
+{
+ YuvImage &im = m_buffers[index].image;
+ GLuint *pbo = m_buffers[index].pbo;
+
+ if (m_buffers[index].fields[FIELD_FULL][0].id == 0)
+ return;
+
+ /* finish up all textures, and delete them */
+ for(int f = 0;f<MAX_FIELDS;f++)
+ {
+ for(int p = 0;p<YuvImage::MAX_PLANES;p++)
+ {
+ if (m_buffers[index].fields[f][p].id)
+ {
+ if (glIsTexture(m_buffers[index].fields[f][p].id))
+ glDeleteTextures(1, &m_buffers[index].fields[f][p].id);
+ m_buffers[index].fields[f][p].id = 0;
+ }
+ }
+ }
+
+ for(int p = 0;p<YuvImage::MAX_PLANES;p++)
+ {
+ if (pbo[p])
+ {
+ if (im.plane[p])
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo[p]);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ im.plane[p] = NULL;
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+ }
+ glDeleteBuffers(1, pbo + p);
+ pbo[p] = 0;
+ }
+ else
+ {
+ if (im.plane[p])
+ {
+ delete[] im.plane[p];
+ im.plane[p] = NULL;
+ }
+ }
+ }
+}
+
+//********************************************************************************************************
+// NV12 Texture loading, creation and deletion
+//********************************************************************************************************
+bool CLinuxRendererGL::UploadNV12Texture(int source)
+{
+ CPictureBuffer& buf = m_buffers[source];
+ YuvImage* im = &buf.image;
+
+ bool deinterlacing;
+ if (m_currentField == FIELD_FULL)
+ deinterlacing = false;
+ else
+ deinterlacing = true;
+
+ VerifyGLState();
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT, im->bpp);
+
+ if (deinterlacing)
+ {
+ // Load Odd Y field
+ LoadPlane(buf.fields[FIELD_TOP][0] , GL_RED,
+ im->width, im->height >> 1,
+ im->stride[0]*2, im->bpp, im->plane[0]);
+
+ // Load Even Y field
+ LoadPlane(buf.fields[FIELD_BOT][0], GL_RED,
+ im->width, im->height >> 1,
+ im->stride[0]*2, im->bpp, im->plane[0] + im->stride[0]) ;
+
+ // Load Odd UV Fields
+ LoadPlane(buf.fields[FIELD_TOP][1], GL_RG,
+ im->width >> im->cshift_x, im->height >> (im->cshift_y + 1),
+ im->stride[1]*2, im->bpp, im->plane[1]);
+
+ // Load Even UV Fields
+ LoadPlane(buf.fields[FIELD_BOT][1], GL_RG,
+ im->width >> im->cshift_x, im->height >> (im->cshift_y + 1),
+ im->stride[1]*2, im->bpp, im->plane[1] + im->stride[1]);
+
+ }
+ else
+ {
+ // Load Y plane
+ LoadPlane(buf. fields[FIELD_FULL][0], GL_RED,
+ im->width, im->height,
+ im->stride[0], im->bpp, im->plane[0]);
+
+ // Load UV plane
+ LoadPlane(buf.fields[FIELD_FULL][1], GL_RG,
+ im->width >> im->cshift_x, im->height >> im->cshift_y,
+ im->stride[1], im->bpp, im->plane[1]);
+ }
+
+ VerifyGLState();
+
+ return true;
+}
+
+bool CLinuxRendererGL::CreateNV12Texture(int index)
+{
+ // since we also want the field textures, pitch must be texture aligned
+ CPictureBuffer& buf = m_buffers[index];
+ YuvImage &im = buf.image;
+ GLuint *pbo = buf.pbo;
+
+ // Delete any old texture
+ DeleteNV12Texture(index);
+
+ im.height = m_sourceHeight;
+ im.width = m_sourceWidth;
+ im.cshift_x = 1;
+ im.cshift_y = 1;
+ im.bpp = 1;
+
+ im.stride[0] = im.width;
+ im.stride[1] = im.width;
+ im.stride[2] = 0;
+
+ im.plane[0] = NULL;
+ im.plane[1] = NULL;
+ im.plane[2] = NULL;
+
+ // Y plane
+ im.planesize[0] = im.stride[0] * im.height;
+ // packed UV plane
+ im.planesize[1] = im.stride[1] * im.height / 2;
+ // third plane is not used
+ im.planesize[2] = 0;
+
+ bool pboSetup = false;
+ if (m_pboUsed)
+ {
+ pboSetup = true;
+ glGenBuffers(2, pbo);
+
+ for (int i = 0; i < 2; i++)
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo[i]);
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, im.planesize[i] + PBO_OFFSET, 0, GL_STREAM_DRAW);
+ void* pboPtr = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
+ if (pboPtr)
+ {
+ im.plane[i] = (uint8_t*)pboPtr + PBO_OFFSET;
+ memset(im.plane[i], 0, im.planesize[i]);
+ }
+ else
+ {
+ CLog::Log(LOGWARNING,"GL: failed to set up pixel buffer object");
+ pboSetup = false;
+ break;
+ }
+ }
+
+ if (!pboSetup)
+ {
+ for (int i = 0; i < 2; i++)
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo[i]);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ }
+ glDeleteBuffers(2, pbo);
+ memset(m_buffers[index].pbo, 0, sizeof(m_buffers[index].pbo));
+ }
+
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+ }
+
+ if (!pboSetup)
+ {
+ for (int i = 0; i < 2; i++)
+ im.plane[i] = new uint8_t[im.planesize[i]];
+ }
+
+ for(int f = 0;f<MAX_FIELDS;f++)
+ {
+ for(int p = 0;p<2;p++)
+ {
+ if (!glIsTexture(buf.fields[f][p].id))
+ {
+ glGenTextures(1, &buf.fields[f][p].id);
+ VerifyGLState();
+ }
+ buf.fields[f][p].pbo = pbo[p];
+ }
+ buf.fields[f][2].id = buf.fields[f][1].id;
+ }
+
+ // YUV
+ for (int f = FIELD_FULL; f<=FIELD_BOT ; f++)
+ {
+ int fieldshift = (f==FIELD_FULL) ? 0 : 1;
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = buf.fields[f];
+
+ planes[0].texwidth = im.width;
+ planes[0].texheight = im.height >> fieldshift;
+
+ planes[1].texwidth = planes[0].texwidth >> im.cshift_x;
+ planes[1].texheight = planes[0].texheight >> im.cshift_y;
+ planes[2].texwidth = planes[1].texwidth;
+ planes[2].texheight = planes[1].texheight;
+
+ for (int p = 0; p < 3; p++)
+ {
+ planes[p].pixpertex_x = 1;
+ planes[p].pixpertex_y = 1;
+ }
+
+ for(int p = 0; p < 2; p++)
+ {
+ CYuvPlane &plane = planes[p];
+ if (plane.texwidth * plane.texheight == 0)
+ continue;
+
+ glBindTexture(m_textureTarget, plane.id);
+ if (p == 1)
+ glTexImage2D(m_textureTarget, 0, GL_RG, plane.texwidth, plane.texheight, 0, GL_RG, GL_UNSIGNED_BYTE, NULL);
+ else
+ glTexImage2D(m_textureTarget, 0, GL_RED, plane.texwidth, plane.texheight, 0, GL_RED, GL_UNSIGNED_BYTE, NULL);
+
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ VerifyGLState();
+ }
+ }
+
+ return true;
+}
+
+void CLinuxRendererGL::DeleteNV12Texture(int index)
+{
+ CPictureBuffer& buf = m_buffers[index];
+ YuvImage &im = buf.image;
+ GLuint *pbo = buf.pbo;
+
+ if (buf.fields[FIELD_FULL][0].id == 0)
+ return;
+
+ // finish up all textures, and delete them
+ for(int f = 0;f<MAX_FIELDS;f++)
+ {
+ for(int p = 0;p<2;p++)
+ {
+ if (buf.fields[f][p].id)
+ {
+ if (glIsTexture(buf.fields[f][p].id))
+ {
+ glDeleteTextures(1, &buf.fields[f][p].id);
+ }
+ buf.fields[f][p].id = 0;
+ }
+ }
+ buf.fields[f][2].id = 0;
+ }
+
+ for(int p = 0;p<2;p++)
+ {
+ if (pbo[p])
+ {
+ if (im.plane[p])
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo[p]);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ im.plane[p] = NULL;
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+ }
+ glDeleteBuffers(1, pbo + p);
+ pbo[p] = 0;
+ }
+ else
+ {
+ if (im.plane[p])
+ {
+ delete[] im.plane[p];
+ im.plane[p] = NULL;
+ }
+ }
+ }
+}
+
+bool CLinuxRendererGL::UploadYUV422PackedTexture(int source)
+{
+ CPictureBuffer& buf = m_buffers[source];
+ YuvImage* im = &buf.image;
+
+ bool deinterlacing;
+ if (m_currentField == FIELD_FULL)
+ deinterlacing = false;
+ else
+ deinterlacing = true;
+
+ VerifyGLState();
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT,1);
+
+ if (deinterlacing)
+ {
+ // Load YUYV fields
+ LoadPlane(buf.fields[FIELD_TOP][0], GL_BGRA,
+ im->width / 2, im->height >> 1,
+ im->stride[0] * 2, im->bpp, im->plane[0]);
+
+ LoadPlane(buf.fields[FIELD_BOT][0], GL_BGRA,
+ im->width / 2, im->height >> 1,
+ im->stride[0] * 2, im->bpp, im->plane[0] + im->stride[0]);
+ }
+ else
+ {
+ // Load YUYV plane
+ LoadPlane(buf.fields[FIELD_FULL][0], GL_BGRA,
+ im->width / 2, im->height,
+ im->stride[0], im->bpp, im->plane[0]);
+ }
+
+ VerifyGLState();
+
+ return true;
+}
+
+void CLinuxRendererGL::DeleteYUV422PackedTexture(int index)
+{
+ CPictureBuffer& buf = m_buffers[index];
+ YuvImage &im = buf.image;
+ GLuint *pbo = buf.pbo;
+
+ if (buf.fields[FIELD_FULL][0].id == 0)
+ return;
+
+ // finish up all textures, and delete them
+ for (int f = 0;f<MAX_FIELDS;f++)
+ {
+ if (buf.fields[f][0].id)
+ {
+ if (glIsTexture(buf.fields[f][0].id))
+ {
+ glDeleteTextures(1, &buf.fields[f][0].id);
+ }
+ buf.fields[f][0].id = 0;
+ }
+ buf.fields[f][1].id = 0;
+ buf.fields[f][2].id = 0;
+ }
+
+ if (pbo[0])
+ {
+ if (im.plane[0])
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo[0]);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ im.plane[0] = NULL;
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+ }
+ glDeleteBuffers(1, pbo);
+ pbo[0] = 0;
+ }
+ else
+ {
+ if (im.plane[0])
+ {
+ delete[] im.plane[0];
+ im.plane[0] = NULL;
+ }
+ }
+}
+
+bool CLinuxRendererGL::CreateYUV422PackedTexture(int index)
+{
+ // since we also want the field textures, pitch must be texture aligned
+ CPictureBuffer& buf = m_buffers[index];
+ YuvImage &im = buf.image;
+ GLuint *pbo = buf.pbo;
+
+ // Delete any old texture
+ DeleteYUV422PackedTexture(index);
+
+ im.height = m_sourceHeight;
+ im.width = m_sourceWidth;
+ im.cshift_x = 0;
+ im.cshift_y = 0;
+ im.bpp = 1;
+
+ im.stride[0] = im.width * 2;
+ im.stride[1] = 0;
+ im.stride[2] = 0;
+
+ im.plane[0] = NULL;
+ im.plane[1] = NULL;
+ im.plane[2] = NULL;
+
+ // packed YUYV plane
+ im.planesize[0] = im.stride[0] * im.height;
+ // second plane is not used
+ im.planesize[1] = 0;
+ // third plane is not used
+ im.planesize[2] = 0;
+
+ bool pboSetup = false;
+ if (m_pboUsed)
+ {
+ pboSetup = true;
+ glGenBuffers(1, pbo);
+
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo[0]);
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, im.planesize[0] + PBO_OFFSET, 0, GL_STREAM_DRAW);
+ void* pboPtr = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
+ if (pboPtr)
+ {
+ im.plane[0] = (uint8_t*)pboPtr + PBO_OFFSET;
+ memset(im.plane[0], 0, im.planesize[0]);
+ }
+ else
+ {
+ CLog::Log(LOGWARNING,"GL: failed to set up pixel buffer object");
+ pboSetup = false;
+ }
+
+ if (!pboSetup)
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, *pbo);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ glDeleteBuffers(1, pbo);
+ memset(m_buffers[index].pbo, 0, sizeof(m_buffers[index].pbo));
+ }
+
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+ }
+
+ if (!pboSetup)
+ {
+ im.plane[0] = new uint8_t[im.planesize[0]];
+ }
+
+ for(int f = 0;f<MAX_FIELDS;f++)
+ {
+ if (!glIsTexture(buf.fields[f][0].id))
+ {
+ glGenTextures(1, &buf.fields[f][0].id);
+ VerifyGLState();
+ }
+ buf.fields[f][0].pbo = pbo[0];
+ buf.fields[f][1].id = buf.fields[f][0].id;
+ buf.fields[f][2].id = buf.fields[f][1].id;
+ }
+
+ // YUV
+ for (int f = FIELD_FULL; f<=FIELD_BOT ; f++)
+ {
+ int fieldshift = (f==FIELD_FULL) ? 0 : 1;
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = buf.fields[f];
+
+ planes[0].texwidth = im.width / 2;
+ planes[0].texheight = im.height >> fieldshift;
+ planes[1].texwidth = planes[0].texwidth;
+ planes[1].texheight = planes[0].texheight;
+ planes[2].texwidth = planes[1].texwidth;
+ planes[2].texheight = planes[1].texheight;
+
+ for (int p = 0; p < 3; p++)
+ {
+ planes[p].pixpertex_x = 2;
+ planes[p].pixpertex_y = 1;
+ }
+
+ CYuvPlane &plane = planes[0];
+ if (plane.texwidth * plane.texheight == 0)
+ continue;
+
+ glBindTexture(m_textureTarget, plane.id);
+
+ glTexImage2D(m_textureTarget, 0, GL_RGBA, plane.texwidth, plane.texheight, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL);
+
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ VerifyGLState();
+ }
+
+ return true;
+}
+
+void CLinuxRendererGL::SetTextureFilter(GLenum method)
+{
+ for (int i = 0 ; i<m_NumYV12Buffers ; i++)
+ {
+ CPictureBuffer& buf = m_buffers[i];
+
+ for (int f = FIELD_FULL; f<=FIELD_BOT ; f++)
+ {
+ for (int p = 0; p < 3; p++)
+ {
+ if(glIsTexture(buf.fields[f][p].id))
+ {
+ glBindTexture(m_textureTarget, buf.fields[f][p].id);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, method);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, method);
+ VerifyGLState();
+ }
+ }
+ }
+ }
+}
+
+bool CLinuxRendererGL::Supports(ERENDERFEATURE feature) const
+{
+ if (feature == RENDERFEATURE_STRETCH ||
+ feature == RENDERFEATURE_NONLINSTRETCH ||
+ feature == RENDERFEATURE_ZOOM ||
+ feature == RENDERFEATURE_VERTICAL_SHIFT ||
+ feature == RENDERFEATURE_PIXEL_RATIO ||
+ feature == RENDERFEATURE_POSTPROCESS ||
+ feature == RENDERFEATURE_ROTATION ||
+ feature == RENDERFEATURE_BRIGHTNESS ||
+ feature == RENDERFEATURE_CONTRAST ||
+ feature == RENDERFEATURE_TONEMAP)
+ return true;
+
+ return false;
+}
+
+bool CLinuxRendererGL::SupportsMultiPassRendering()
+{
+ return m_renderSystem->IsExtSupported("GL_EXT_framebuffer_object");
+}
+
+bool CLinuxRendererGL::Supports(ESCALINGMETHOD method) const
+{
+ //nearest neighbor doesn't work on YUY2 and UYVY
+ if (method == VS_SCALINGMETHOD_NEAREST &&
+ m_format != AV_PIX_FMT_YUYV422 &&
+ m_format != AV_PIX_FMT_UYVY422)
+ return true;
+
+ if (method == VS_SCALINGMETHOD_LINEAR ||
+ method == VS_SCALINGMETHOD_AUTO)
+ return true;
+
+ if (method == VS_SCALINGMETHOD_CUBIC_B_SPLINE ||
+ method == VS_SCALINGMETHOD_CUBIC_MITCHELL ||
+ method == VS_SCALINGMETHOD_CUBIC_CATMULL ||
+ method == VS_SCALINGMETHOD_CUBIC_0_075 ||
+ method == VS_SCALINGMETHOD_CUBIC_0_1 ||
+ method == VS_SCALINGMETHOD_LANCZOS2 ||
+ method == VS_SCALINGMETHOD_SPLINE36_FAST ||
+ method == VS_SCALINGMETHOD_LANCZOS3_FAST ||
+ method == VS_SCALINGMETHOD_SPLINE36 ||
+ method == VS_SCALINGMETHOD_LANCZOS3)
+ {
+ // if scaling is below level, avoid hq scaling
+ float scaleX = fabs(((float)m_sourceWidth - m_destRect.Width())/m_sourceWidth)*100;
+ float scaleY = fabs(((float)m_sourceHeight - m_destRect.Height())/m_sourceHeight)*100;
+ int minScale = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_HQSCALERS);
+ if (scaleX < minScale && scaleY < minScale)
+ return false;
+
+ bool hasFramebuffer = false;
+ unsigned int major, minor;
+ m_renderSystem->GetRenderVersion(major, minor);
+ if (major > 3 ||
+ (major == 3 && minor >= 2))
+ hasFramebuffer = true;
+ if (m_renderSystem->IsExtSupported("GL_EXT_framebuffer_object"))
+ hasFramebuffer = true;
+ if (hasFramebuffer && (m_renderMethod & RENDER_GLSL))
+ return true;
+ }
+
+ return false;
+}
+
+void CLinuxRendererGL::BindPbo(CPictureBuffer& buff)
+{
+ bool pbo = false;
+ for(int plane = 0; plane < YuvImage::MAX_PLANES; plane++)
+ {
+ if(!buff.pbo[plane] || buff.image.plane[plane] == (uint8_t*)PBO_OFFSET)
+ continue;
+ pbo = true;
+
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buff.pbo[plane]);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ buff.image.plane[plane] = (uint8_t*)PBO_OFFSET;
+ }
+ if (pbo)
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+}
+
+void CLinuxRendererGL::UnBindPbo(CPictureBuffer& buff)
+{
+ bool pbo = false;
+ for(int plane = 0; plane < YuvImage::MAX_PLANES; plane++)
+ {
+ if(!buff.pbo[plane] || buff.image.plane[plane] != (uint8_t*)PBO_OFFSET)
+ continue;
+ pbo = true;
+
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buff.pbo[plane]);
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, buff.image.planesize[plane] + PBO_OFFSET, NULL, GL_STREAM_DRAW);
+ buff.image.plane[plane] = (uint8_t*)glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY) + PBO_OFFSET;
+ }
+ if (pbo)
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+}
+
+CRenderInfo CLinuxRendererGL::GetRenderInfo()
+{
+ CRenderInfo info;
+ info.max_buffer_size = NUM_BUFFERS;
+ return info;
+}
+
+// Color management helpers
+
+bool CLinuxRendererGL::LoadCLUT()
+{
+ DeleteCLUT();
+
+ int clutSize, dataSize;
+ if (!CColorManager::Get3dLutSize(CMS_DATA_FMT_RGB, &clutSize, &dataSize))
+ return false;
+
+ // allocate buffer
+ m_CLUTsize = clutSize;
+ m_CLUT = static_cast<uint16_t*>(malloc(dataSize));
+
+ // load 3DLUT
+ if (!m_ColorManager->GetVideo3dLut(m_srcPrimaries, &m_cmsToken, CMS_DATA_FMT_RGB, m_CLUTsize,
+ m_CLUT))
+ {
+ free(m_CLUT);
+ CLog::Log(LOGERROR, "Error loading the LUT");
+ return false;
+ }
+
+ // create 3DLUT texture
+ CLog::Log(LOGDEBUG, "LinuxRendererGL: creating 3DLUT");
+ glGenTextures(1, &m_tCLUTTex);
+ glActiveTexture(GL_TEXTURE4);
+ if (m_tCLUTTex <= 0)
+ {
+ CLog::Log(LOGERROR, "Error creating 3DLUT texture");
+ return false;
+ }
+
+ // bind and set 3DLUT texture parameters
+ glBindTexture(GL_TEXTURE_3D, m_tCLUTTex);
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
+
+ // load 3DLUT data
+ glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB16, m_CLUTsize, m_CLUTsize, m_CLUTsize, 0, GL_RGB,
+ GL_UNSIGNED_SHORT, m_CLUT);
+ free(m_CLUT);
+ glActiveTexture(GL_TEXTURE0);
+ return true;
+}
+
+void CLinuxRendererGL::DeleteCLUT()
+{
+ if (m_tCLUTTex)
+ {
+ CLog::Log(LOGDEBUG, "LinuxRendererGL: deleting 3DLUT");
+ glDeleteTextures(1, &m_tCLUTTex);
+ m_tCLUTTex = 0;
+ }
+}
+
+void CLinuxRendererGL::CheckVideoParameters(int index)
+{
+ const CPictureBuffer& buf = m_buffers[index];
+ ETONEMAPMETHOD method = m_videoSettings.m_ToneMapMethod;
+
+ AVColorPrimaries srcPrim = GetSrcPrimaries(buf.m_srcPrimaries, buf.image.width, buf.image.height);
+ if (srcPrim != m_srcPrimaries)
+ {
+ m_srcPrimaries = srcPrim;
+ m_reloadShaders = true;
+ }
+
+ bool toneMap = false;
+ if (method != VS_TONEMAPMETHOD_OFF)
+ {
+ if (buf.hasLightMetadata || (buf.hasDisplayMetadata && buf.displayMetadata.has_luminance))
+ {
+ toneMap = true;
+ }
+ }
+
+ if (toneMap != m_toneMap || (m_toneMapMethod != method))
+ {
+ m_reloadShaders = true;
+ }
+ m_toneMap = toneMap;
+ m_toneMapMethod = method;
+}
+
+AVColorPrimaries CLinuxRendererGL::GetSrcPrimaries(AVColorPrimaries srcPrimaries, unsigned int width, unsigned int height)
+{
+ AVColorPrimaries ret = srcPrimaries;
+ if (ret == AVCOL_PRI_UNSPECIFIED)
+ {
+ if (width > 1024 || height >= 600)
+ ret = AVCOL_PRI_BT709;
+ else
+ ret = AVCOL_PRI_BT470BG;
+ }
+ return ret;
+}
+
+CRenderCapture* CLinuxRendererGL::GetRenderCapture()
+{
+ return new CRenderCaptureGL;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h b/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h
new file mode 100644
index 0000000..026d951
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2007-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 <vector>
+
+#include "system_gl.h"
+
+#include "FrameBufferObject.h"
+#include "cores/VideoSettings.h"
+#include "RenderInfo.h"
+#include "BaseRenderer.h"
+#include "ColorManager.h"
+#include "utils/Geometry.h"
+
+extern "C" {
+#include <libavutil/mastering_display_metadata.h>
+}
+
+class CRenderCapture;
+class CRenderSystemGL;
+
+class CTexture;
+namespace Shaders
+{
+namespace GL
+{
+class BaseYUV2RGBGLSLShader;
+class BaseVideoFilterShader;
+}
+} // namespace Shaders
+
+enum RenderMethod
+{
+ RENDER_GLSL=0x01,
+ RENDER_CUSTOM=0x02
+};
+
+enum RenderQuality
+{
+ RQ_LOW=1,
+ RQ_SINGLEPASS,
+ RQ_MULTIPASS,
+};
+
+#define FIELD_FULL 0
+#define FIELD_TOP 1
+#define FIELD_BOT 2
+
+class CLinuxRendererGL : public CBaseRenderer
+{
+public:
+ CLinuxRendererGL();
+ ~CLinuxRendererGL() override;
+
+ static CBaseRenderer* Create(CVideoBuffer *buffer);
+ static bool Register();
+
+ // Player functions
+ bool Configure(const VideoPicture &picture, float fps, unsigned int orientation) override;
+ bool IsConfigured() override { return m_bConfigured; }
+ void AddVideoPicture(const VideoPicture &picture, int index) override;
+ void UnInit() override;
+ bool Flush(bool saveBuffers) override;
+ void SetBufferSize(int numBuffers) override { m_NumYV12Buffers = numBuffers; }
+ void ReleaseBuffer(int idx) override;
+ void RenderUpdate(int index, int index2, bool clear, unsigned int flags, unsigned int alpha) override;
+ void Update() override;
+ bool RenderCapture(CRenderCapture* capture) override;
+ CRenderInfo GetRenderInfo() override;
+ bool ConfigChanged(const VideoPicture &picture) override;
+
+ // Feature support
+ bool SupportsMultiPassRendering() override;
+ bool Supports(ERENDERFEATURE feature) const override;
+ bool Supports(ESCALINGMETHOD method) const override;
+
+ CRenderCapture* GetRenderCapture() override;
+
+protected:
+
+ bool Render(unsigned int flags, int renderBuffer);
+ void ClearBackBuffer();
+ void DrawBlackBars();
+
+ bool ValidateRenderer();
+ virtual bool ValidateRenderTarget();
+ virtual void LoadShaders(int field=FIELD_FULL);
+ void SetTextureFilter(GLenum method);
+ void UpdateVideoFilter();
+ void CheckVideoParameters(int index);
+ AVColorPrimaries GetSrcPrimaries(AVColorPrimaries srcPrimaries, unsigned int width, unsigned int height);
+
+ // textures
+ virtual bool UploadTexture(int index);
+ virtual void DeleteTexture(int index);
+ virtual bool CreateTexture(int index);
+
+ bool UploadYV12Texture(int index);
+ void DeleteYV12Texture(int index);
+ bool CreateYV12Texture(int index);
+
+ bool UploadNV12Texture(int index);
+ void DeleteNV12Texture(int index);
+ bool CreateNV12Texture(int index);
+
+ bool UploadYUV422PackedTexture(int index);
+ void DeleteYUV422PackedTexture(int index);
+ bool CreateYUV422PackedTexture(int index);
+
+ void CalculateTextureSourceRects(int source, int num_planes);
+
+ // renderers
+ void RenderToFBO(int renderBuffer, int field, bool weave = false);
+ void RenderFromFBO();
+ void RenderSinglePass(int renderBuffer, int field); // single pass glsl renderer
+ void RenderRGB(int renderBuffer, int field); // render using vdpau/vaapi hardware
+ void RenderProgressiveWeave(int renderBuffer, int field); // render using vdpau hardware
+
+ struct CYuvPlane;
+ struct CPictureBuffer;
+
+ void BindPbo(CPictureBuffer& buff);
+ void UnBindPbo(CPictureBuffer& buff);
+ void LoadPlane(CYuvPlane& plane, int type,
+ unsigned width, unsigned height,
+ int stride, int bpp, void* data);
+ void GetPlaneTextureSize(CYuvPlane& plane);
+ GLint GetInternalFormat(GLint format, int bpp);
+
+ // hooks for HwDec Renderer
+ virtual bool LoadShadersHook() { return false; }
+ virtual bool RenderHook(int idx) { return false; }
+ virtual void AfterRenderHook(int idx) {}
+ virtual bool CanSaveBuffers() { return true; }
+
+ struct
+ {
+ CFrameBufferObject fbo;
+ float width, height;
+ } m_fbo;
+
+ int m_iYV12RenderBuffer = 0;
+ int m_NumYV12Buffers = 0;
+
+ bool m_bConfigured = false;
+ bool m_bValidated = false;
+ GLenum m_textureTarget = GL_TEXTURE_2D;
+ int m_renderMethod = RENDER_GLSL;
+ RenderQuality m_renderQuality = RQ_SINGLEPASS;
+ CRenderSystemGL *m_renderSystem = nullptr;
+
+ // Raw data used by renderer
+ int m_currentField = FIELD_FULL;
+ int m_reloadShaders = 0;
+
+ struct CYuvPlane
+ {
+ GLuint id;
+ GLuint pbo;
+ CRect rect;
+ float width;
+ float height;
+ unsigned texwidth;
+ unsigned texheight;
+ //pixels per texel
+ unsigned pixpertex_x;
+ unsigned pixpertex_y;
+ };
+
+ struct CPictureBuffer
+ {
+ CPictureBuffer();
+ ~CPictureBuffer() = default;
+
+ CYuvPlane fields[MAX_FIELDS][YuvImage::MAX_PLANES];
+ YuvImage image;
+ GLuint pbo[3]; // one pbo for 3 planes
+
+ CVideoBuffer *videoBuffer;
+ bool loaded;
+
+ AVColorPrimaries m_srcPrimaries;
+ AVColorSpace m_srcColSpace;
+ int m_srcBits = 8;
+ int m_srcTextureBits = 8;
+ bool m_srcFullRange;
+
+ bool hasDisplayMetadata = false;
+ AVMasteringDisplayMetadata displayMetadata;
+ bool hasLightMetadata = false;
+ AVContentLightMetadata lightMetadata;
+ };
+
+ // YV12 decoder textures
+ // field index 0 is full image, 1 is odd scanlines, 2 is even scanlines
+ CPictureBuffer m_buffers[NUM_BUFFERS];
+
+ Shaders::GL::BaseYUV2RGBGLSLShader* m_pYUVShader = nullptr;
+ Shaders::GL::BaseVideoFilterShader* m_pVideoFilterShader = nullptr;
+ ESCALINGMETHOD m_scalingMethod = VS_SCALINGMETHOD_LINEAR;
+ ESCALINGMETHOD m_scalingMethodGui = VS_SCALINGMETHOD_MAX;
+ bool m_useDithering;
+ unsigned int m_ditherDepth;
+ bool m_fullRange;
+ AVColorPrimaries m_srcPrimaries;
+ bool m_toneMap = false;
+ ETONEMAPMETHOD m_toneMapMethod = VS_TONEMAPMETHOD_OFF;
+ float m_clearColour = 0.0f;
+ bool m_pboSupported = true;
+ bool m_pboUsed = false;
+ bool m_nonLinStretch = false;
+ bool m_nonLinStretchGui = false;
+ float m_pixelRatio = 0.0f;
+ CRect m_viewRect;
+
+ // color management
+ std::unique_ptr<CColorManager> m_ColorManager;
+ GLuint m_tCLUTTex;
+ uint16_t *m_CLUT;
+ int m_CLUTsize;
+ int m_cmsToken;
+ bool m_cmsOn;
+
+ bool LoadCLUT();
+ void DeleteCLUT();
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.cpp
new file mode 100644
index 0000000..e164f4c
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.cpp
@@ -0,0 +1,1782 @@
+/*
+ * 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 "LinuxRendererGLES.h"
+
+#include "RenderCapture.h"
+#include "RenderCaptureGLES.h"
+#include "RenderFactory.h"
+#include "ServiceBroker.h"
+#include "VideoShaders/VideoFilterShaderGLES.h"
+#include "VideoShaders/YUV2RGBShaderGLES.h"
+#include "application/Application.h"
+#include "cores/IPlayer.h"
+#include "guilib/Texture.h"
+#include "rendering/MatrixGL.h"
+#include "rendering/gles/RenderSystemGLES.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/DisplaySettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/GLUtils.h"
+#include "utils/MathUtils.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+
+using namespace Shaders;
+using namespace Shaders::GLES;
+
+CLinuxRendererGLES::CLinuxRendererGLES()
+{
+ m_format = AV_PIX_FMT_NONE;
+
+ m_fullRange = !CServiceBroker::GetWinSystem()->UseLimitedColor();
+
+ m_renderSystem = dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+
+#if defined (GL_UNPACK_ROW_LENGTH_EXT)
+ if (m_renderSystem->IsExtSupported("GL_EXT_unpack_subimage"))
+ {
+ m_pixelStoreKey = GL_UNPACK_ROW_LENGTH_EXT;
+ }
+#endif
+}
+
+CLinuxRendererGLES::~CLinuxRendererGLES()
+{
+ UnInit();
+
+ ReleaseShaders();
+
+ free(m_planeBuffer);
+ m_planeBuffer = nullptr;
+}
+
+CBaseRenderer* CLinuxRendererGLES::Create(CVideoBuffer *buffer)
+{
+ return new CLinuxRendererGLES();
+}
+
+bool CLinuxRendererGLES::Register()
+{
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("default", CLinuxRendererGLES::Create);
+ return true;
+}
+
+bool CLinuxRendererGLES::ValidateRenderTarget()
+{
+ if (!m_bValidated)
+ {
+ // function pointer for texture might change in
+ // call to LoadShaders
+ glFinish();
+
+ for (int i = 0 ; i < NUM_BUFFERS ; i++)
+ {
+ DeleteTexture(i);
+ }
+
+ // create the yuv textures
+ UpdateVideoFilter();
+ LoadShaders();
+
+ if (m_renderMethod < 0)
+ {
+ return false;
+ }
+
+ for (int i = 0 ; i < m_NumYV12Buffers ; i++)
+ {
+ CreateTexture(i);
+ }
+
+ m_bValidated = true;
+
+ return true;
+ }
+
+ return false;
+}
+
+bool CLinuxRendererGLES::Configure(const VideoPicture &picture, float fps, unsigned int orientation)
+{
+ CLog::Log(LOGDEBUG, "LinuxRendererGLES::Configure: fps: {:0.3f}", fps);
+ m_format = picture.videoBuffer->GetFormat();
+ m_sourceWidth = picture.iWidth;
+ m_sourceHeight = picture.iHeight;
+ m_renderOrientation = orientation;
+
+ m_srcPrimaries = GetSrcPrimaries(static_cast<AVColorPrimaries>(picture.color_primaries),
+ picture.iWidth, picture.iHeight);
+ m_toneMap = false;
+
+ // Calculate the input frame aspect ratio.
+ CalculateFrameAspectRatio(picture.iDisplayWidth, picture.iDisplayHeight);
+ SetViewMode(m_videoSettings.m_ViewMode);
+ ManageRenderArea();
+
+ m_bConfigured = true;
+ m_scalingMethodGui = (ESCALINGMETHOD)-1;
+
+ // Ensure that textures are recreated and rendering starts only after the 1st
+ // frame is loaded after every call to Configure().
+ m_bValidated = false;
+
+ // setup the background colour
+ m_clearColour = CServiceBroker::GetWinSystem()->UseLimitedColor() ? (16.0f / 0xff) : 0.0f;
+
+ if (picture.hasDisplayMetadata && picture.hasLightMetadata)
+ {
+ m_passthroughHDR = CServiceBroker::GetWinSystem()->SetHDR(&picture);
+ CLog::Log(LOGDEBUG, "LinuxRendererGLES::Configure: HDR passthrough: {}",
+ m_passthroughHDR ? "on" : "off");
+ }
+
+ return true;
+}
+
+bool CLinuxRendererGLES::ConfigChanged(const VideoPicture &picture)
+{
+ if (picture.videoBuffer->GetFormat() != m_format)
+ {
+ return true;
+ }
+
+ return false;
+}
+
+int CLinuxRendererGLES::NextYV12Texture()
+{
+ return (m_iYV12RenderBuffer + 1) % m_NumYV12Buffers;
+}
+
+void CLinuxRendererGLES::AddVideoPicture(const VideoPicture &picture, int index)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ if (buf.videoBuffer)
+ {
+ CLog::LogF(LOGERROR, "unreleased video buffer");
+ buf.videoBuffer->Release();
+ }
+ buf.videoBuffer = picture.videoBuffer;
+ buf.videoBuffer->Acquire();
+ buf.loaded = false;
+ buf.m_srcPrimaries = static_cast<AVColorPrimaries>(picture.color_primaries);
+ buf.m_srcColSpace = static_cast<AVColorSpace>(picture.color_space);
+ buf.m_srcFullRange = picture.color_range == 1;
+ buf.m_srcBits = picture.colorBits;
+
+ buf.hasDisplayMetadata = picture.hasDisplayMetadata;
+ buf.displayMetadata = picture.displayMetadata;
+ buf.lightMetadata = picture.lightMetadata;
+ if (picture.hasLightMetadata && picture.lightMetadata.MaxCLL)
+ {
+ buf.hasLightMetadata = picture.hasLightMetadata;
+ }
+}
+
+void CLinuxRendererGLES::ReleaseBuffer(int idx)
+{
+ CPictureBuffer &buf = m_buffers[idx];
+ if (buf.videoBuffer)
+ {
+ buf.videoBuffer->Release();
+ buf.videoBuffer = nullptr;
+ }
+}
+
+void CLinuxRendererGLES::CalculateTextureSourceRects(int source, int num_planes)
+{
+ CPictureBuffer& buf = m_buffers[source];
+ YuvImage* im = &buf.image;
+
+ // calculate the source rectangle
+ for(int field = 0; field < 3; field++)
+ {
+ for(int plane = 0; plane < num_planes; plane++)
+ {
+ CYuvPlane& p = buf.fields[field][plane];
+
+ p.rect = m_sourceRect;
+ p.width = im->width;
+ p.height = im->height;
+
+ if(field != FIELD_FULL)
+ {
+ // correct for field offsets and chroma offsets
+ float offset_y = 0.5;
+ if(plane != 0)
+ {
+ offset_y += 0.5f;
+ }
+
+ if(field == FIELD_BOT)
+ {
+ offset_y *= -1;
+ }
+
+ p.rect.y1 += offset_y;
+ p.rect.y2 += offset_y;
+
+ // half the height if this is a field
+ p.height *= 0.5f;
+ p.rect.y1 *= 0.5f;
+ p.rect.y2 *= 0.5f;
+ }
+
+ if(plane != 0)
+ {
+ p.width /= 1 << im->cshift_x;
+ p.height /= 1 << im->cshift_y;
+
+ p.rect.x1 /= 1 << im->cshift_x;
+ p.rect.x2 /= 1 << im->cshift_x;
+ p.rect.y1 /= 1 << im->cshift_y;
+ p.rect.y2 /= 1 << im->cshift_y;
+ }
+
+ // protect against division by zero
+ if (p.texheight == 0 || p.texwidth == 0 ||
+ p.pixpertex_x == 0 || p.pixpertex_y == 0)
+ {
+ continue;
+ }
+
+ p.height /= p.pixpertex_y;
+ p.rect.y1 /= p.pixpertex_y;
+ p.rect.y2 /= p.pixpertex_y;
+ p.width /= p.pixpertex_x;
+ p.rect.x1 /= p.pixpertex_x;
+ p.rect.x2 /= p.pixpertex_x;
+
+ if (m_textureTarget == GL_TEXTURE_2D)
+ {
+ p.height /= p.texheight;
+ p.rect.y1 /= p.texheight;
+ p.rect.y2 /= p.texheight;
+ p.width /= p.texwidth;
+ p.rect.x1 /= p.texwidth;
+ p.rect.x2 /= p.texwidth;
+ }
+ }
+ }
+}
+
+void CLinuxRendererGLES::LoadPlane(CYuvPlane& plane, int type,
+ unsigned width, unsigned height,
+ int stride, int bpp, void* data)
+{
+ const GLvoid *pixelData = data;
+ int bps = bpp * KODI::UTILS::GL::glFormatElementByteCount(type);
+
+ glBindTexture(m_textureTarget, plane.id);
+
+ bool pixelStoreChanged = false;
+ if (stride != static_cast<int>(width * bps))
+ {
+ if (m_pixelStoreKey > 0)
+ {
+ pixelStoreChanged = true;
+ glPixelStorei(m_pixelStoreKey, stride);
+ }
+ else
+ {
+ size_t planeSize = width * height * bps;
+ if (m_planeBufferSize < planeSize)
+ {
+ m_planeBuffer = static_cast<unsigned char*>(realloc(m_planeBuffer, planeSize));
+ m_planeBufferSize = planeSize;
+ }
+
+ unsigned char *src(static_cast<unsigned char*>(data)),
+ *dst(m_planeBuffer);
+
+ for (unsigned int y = 0; y < height; ++y, src += stride, dst += width * bps)
+ memcpy(dst, src, width * bps);
+
+ pixelData = m_planeBuffer;
+ }
+ }
+ glTexSubImage2D(m_textureTarget, 0, 0, 0, width, height, type, GL_UNSIGNED_BYTE, pixelData);
+
+ if (m_pixelStoreKey > 0 && pixelStoreChanged)
+ glPixelStorei(m_pixelStoreKey, 0);
+
+ // check if we need to load any border pixels
+ if (height < plane.texheight)
+ {
+ glTexSubImage2D(m_textureTarget, 0,
+ 0, height, width, 1,
+ type, GL_UNSIGNED_BYTE,
+ static_cast<const unsigned char*>(pixelData) + stride * (height - 1));
+ }
+
+ if (width < plane.texwidth)
+ {
+ glTexSubImage2D(m_textureTarget, 0,
+ width, 0, 1, height,
+ type, GL_UNSIGNED_BYTE,
+ static_cast<const unsigned char*>(pixelData) + bps * (width - 1));
+ }
+
+ glBindTexture(m_textureTarget, 0);
+}
+
+bool CLinuxRendererGLES::Flush(bool saveBuffers)
+{
+ glFinish();
+
+ for (int i = 0 ; i < m_NumYV12Buffers ; i++)
+ {
+ DeleteTexture(i);
+ }
+
+ glFinish();
+ m_bValidated = false;
+ m_fbo.fbo.Cleanup();
+ m_iYV12RenderBuffer = 0;
+
+ return false;
+}
+
+void CLinuxRendererGLES::Update()
+{
+ if (!m_bConfigured)
+ {
+ return;
+ }
+
+ ManageRenderArea();
+ ValidateRenderTarget();
+}
+
+void CLinuxRendererGLES::DrawBlackBars()
+{
+ CRect windowRect(0, 0, CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(),
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight());
+
+ auto quads = windowRect.SubtractRect(m_destRect);
+
+ struct Svertex
+ {
+ float x, y;
+ };
+
+ std::vector<Svertex> vertices(6 * quads.size());
+
+ GLubyte count = 0;
+ for (const auto& quad : quads)
+ {
+ vertices[count + 1].x = quad.x1;
+ vertices[count + 1].y = quad.y1;
+
+ vertices[count + 0].x = vertices[count + 5].x = quad.x1;
+ vertices[count + 0].y = vertices[count + 5].y = quad.y2;
+
+ vertices[count + 2].x = vertices[count + 3].x = quad.x2;
+ vertices[count + 2].y = vertices[count + 3].y = quad.y1;
+
+ vertices[count + 4].x = quad.x2;
+ vertices[count + 4].y = quad.y2;
+
+ count += 6;
+ }
+
+ glDisable(GL_BLEND);
+
+ CRenderSystemGLES* renderSystem =
+ dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+ if (!renderSystem)
+ return;
+
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_DEFAULT);
+ GLint posLoc = renderSystem->GUIShaderGetPos();
+ GLint uniCol = renderSystem->GUIShaderGetUniCol();
+
+ glUniform4f(uniCol, m_clearColour / 255.0f, m_clearColour / 255.0f, m_clearColour / 255.0f, 1.0f);
+
+ GLuint vertexVBO;
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(Svertex) * vertices.size(), vertices.data(), GL_STATIC_DRAW);
+
+ glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, sizeof(Svertex), 0);
+ glEnableVertexAttribArray(posLoc);
+
+ glDrawArrays(GL_TRIANGLES, 0, vertices.size());
+
+ glDisableVertexAttribArray(posLoc);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+
+ renderSystem->DisableGUIShader();
+}
+
+void CLinuxRendererGLES::RenderUpdate(int index, int index2, bool clear, unsigned int flags, unsigned int alpha)
+{
+ m_iYV12RenderBuffer = index;
+
+ if (!m_bConfigured)
+ {
+ return;
+ }
+
+ // if its first pass, just init textures and return
+ if (ValidateRenderTarget())
+ {
+ return;
+ }
+
+ if (!IsGuiLayer())
+ {
+ RenderUpdateVideo(clear, flags, alpha);
+ return;
+ }
+
+ CPictureBuffer& buf = m_buffers[index];
+
+ if (!buf.fields[FIELD_FULL][0].id)
+ {
+ return;
+ }
+
+ ManageRenderArea();
+
+ if (clear)
+ {
+ if (alpha == 255)
+ DrawBlackBars();
+ else
+ {
+ glClearColor(m_clearColour, m_clearColour, m_clearColour, 0);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glClearColor(0, 0, 0, 0);
+ }
+ }
+
+ if (alpha < 255)
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ if (m_pYUVProgShader)
+ {
+ m_pYUVProgShader->SetAlpha(alpha / 255.0f);
+ }
+
+ if (m_pYUVBobShader)
+ {
+ m_pYUVBobShader->SetAlpha(alpha / 255.0f);
+ }
+ }
+ else
+ {
+ glDisable(GL_BLEND);
+ if (m_pYUVProgShader)
+ {
+ m_pYUVProgShader->SetAlpha(1.0f);
+ }
+
+ if (m_pYUVBobShader)
+ {
+ m_pYUVBobShader->SetAlpha(1.0f);
+ }
+ }
+
+ Render(flags, index);
+
+ VerifyGLState();
+ glEnable(GL_BLEND);
+}
+
+void CLinuxRendererGLES::RenderUpdateVideo(bool clear, unsigned int flags, unsigned int alpha)
+{
+ if (!m_bConfigured)
+ {
+ return;
+ }
+
+ if (IsGuiLayer())
+ {
+ return;
+ }
+}
+
+void CLinuxRendererGLES::UpdateVideoFilter()
+{
+ CRect srcRect;
+ CRect dstRect;
+ CRect viewRect;
+ GetVideoRect(srcRect, dstRect, viewRect);
+
+ if (m_scalingMethodGui == m_videoSettings.m_ScalingMethod &&
+ viewRect.Height() == m_viewRect.Height() &&
+ viewRect.Width() == m_viewRect.Width())
+ {
+ return;
+ }
+
+ m_scalingMethodGui = m_videoSettings.m_ScalingMethod;
+ m_scalingMethod = m_scalingMethodGui;
+ m_viewRect = viewRect;
+
+ if(!Supports(m_scalingMethod))
+ {
+ CLog::Log(LOGWARNING,
+ "CLinuxRendererGLES::UpdateVideoFilter - chosen scaling method {}, is not supported "
+ "by renderer",
+ m_scalingMethod);
+ m_scalingMethod = VS_SCALINGMETHOD_LINEAR;
+ }
+
+ if (m_pVideoFilterShader)
+ {
+ delete m_pVideoFilterShader;
+ m_pVideoFilterShader = nullptr;
+ }
+
+ m_fbo.fbo.Cleanup();
+
+ VerifyGLState();
+
+ switch (m_scalingMethod)
+ {
+ case VS_SCALINGMETHOD_NEAREST:
+ {
+ CLog::Log(LOGINFO, "GLES: Selecting single pass rendering");
+ SetTextureFilter(GL_NEAREST);
+ m_renderQuality = RQ_SINGLEPASS;
+ return;
+ }
+ case VS_SCALINGMETHOD_LINEAR:
+ {
+ CLog::Log(LOGINFO, "GLES: Selecting single pass rendering");
+ SetTextureFilter(GL_LINEAR);
+ m_renderQuality = RQ_SINGLEPASS;
+ return;
+ }
+ case VS_SCALINGMETHOD_LANCZOS2:
+ case VS_SCALINGMETHOD_SPLINE36_FAST:
+ case VS_SCALINGMETHOD_LANCZOS3_FAST:
+ case VS_SCALINGMETHOD_SPLINE36:
+ case VS_SCALINGMETHOD_LANCZOS3:
+ case VS_SCALINGMETHOD_CUBIC_B_SPLINE:
+ case VS_SCALINGMETHOD_CUBIC_MITCHELL:
+ case VS_SCALINGMETHOD_CUBIC_CATMULL:
+ case VS_SCALINGMETHOD_CUBIC_0_075:
+ case VS_SCALINGMETHOD_CUBIC_0_1:
+ {
+ if (m_renderMethod & RENDER_GLSL)
+ {
+ if (!m_fbo.fbo.Initialize())
+ {
+ CLog::Log(LOGERROR, "GLES: Error initializing FBO");
+ break;
+ }
+
+ if (!m_fbo.fbo.CreateAndBindToTexture(GL_TEXTURE_2D, m_sourceWidth, m_sourceHeight, GL_RGBA))
+ {
+ CLog::Log(LOGERROR, "GLES: Error creating texture and binding to FBO");
+ break;
+ }
+ }
+
+ m_pVideoFilterShader = new ConvolutionFilterShader(m_scalingMethod);
+ if (!m_pVideoFilterShader->CompileAndLink())
+ {
+ CLog::Log(LOGERROR, "GLES: Error compiling and linking video filter shader");
+ break;
+ }
+
+ CLog::Log(LOGINFO, "GLES: Selecting multi pass rendering");
+ SetTextureFilter(GL_LINEAR);
+ m_renderQuality = RQ_MULTIPASS;
+ return;
+ }
+ case VS_SCALINGMETHOD_BICUBIC_SOFTWARE:
+ case VS_SCALINGMETHOD_LANCZOS_SOFTWARE:
+ case VS_SCALINGMETHOD_SINC_SOFTWARE:
+ case VS_SCALINGMETHOD_SINC8:
+ {
+ CLog::Log(LOGERROR, "GLES: TODO: This scaler has not yet been implemented");
+ break;
+ }
+ default:
+ break;
+ }
+
+ CLog::Log(LOGERROR, "GLES: Falling back to bilinear due to failure to init scaler");
+ if (m_pVideoFilterShader)
+ {
+ delete m_pVideoFilterShader;
+ m_pVideoFilterShader = nullptr;
+ }
+
+ m_fbo.fbo.Cleanup();
+
+ SetTextureFilter(GL_LINEAR);
+ m_renderQuality = RQ_SINGLEPASS;
+}
+
+void CLinuxRendererGLES::LoadShaders(int field)
+{
+ m_reloadShaders = 0;
+
+ if (!LoadShadersHook())
+ {
+ int requestedMethod = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_RENDERMETHOD);
+ CLog::Log(LOGDEBUG, "GLES: Requested render method: {}", requestedMethod);
+
+ ReleaseShaders();
+
+ switch(requestedMethod)
+ {
+ case RENDER_METHOD_AUTO:
+ case RENDER_METHOD_GLSL:
+ {
+ // Try GLSL shaders if supported and user requested auto or GLSL.
+ if (glCreateProgram())
+ {
+ // create regular scan shader
+ CLog::Log(LOGINFO, "GLES: Selecting YUV 2 RGB shader");
+
+ EShaderFormat shaderFormat = GetShaderFormat();
+ m_toneMapMethod = m_videoSettings.m_ToneMapMethod;
+ m_pYUVProgShader = new YUV2RGBProgressiveShader(
+ shaderFormat, m_passthroughHDR ? m_srcPrimaries : AVColorPrimaries::AVCOL_PRI_BT709,
+ m_srcPrimaries, m_toneMap, m_toneMapMethod);
+ m_pYUVProgShader->SetConvertFullColorRange(m_fullRange);
+ m_pYUVBobShader = new YUV2RGBBobShader(
+ shaderFormat, m_passthroughHDR ? m_srcPrimaries : AVColorPrimaries::AVCOL_PRI_BT709,
+ m_srcPrimaries, m_toneMap, m_toneMapMethod);
+ m_pYUVBobShader->SetConvertFullColorRange(m_fullRange);
+
+ if ((m_pYUVProgShader && m_pYUVProgShader->CompileAndLink())
+ && (m_pYUVBobShader && m_pYUVBobShader->CompileAndLink()))
+ {
+ m_renderMethod = RENDER_GLSL;
+ UpdateVideoFilter();
+ break;
+ }
+ else
+ {
+ ReleaseShaders();
+ CLog::Log(LOGERROR, "GLES: Error enabling YUV2RGB GLSL shader");
+ m_renderMethod = -1;
+ break;
+ }
+ }
+
+ break;
+ }
+ default:
+ {
+ m_renderMethod = -1 ;
+ CLog::Log(LOGERROR, "GLES: render method not supported");
+ }
+ }
+ }
+}
+
+void CLinuxRendererGLES::ReleaseShaders()
+{
+ if (m_pYUVProgShader)
+ {
+ delete m_pYUVProgShader;
+ m_pYUVProgShader = nullptr;
+ }
+
+ if (m_pYUVBobShader)
+ {
+ delete m_pYUVBobShader;
+ m_pYUVBobShader = nullptr;
+ }
+}
+
+void CLinuxRendererGLES::UnInit()
+{
+ CLog::Log(LOGDEBUG, "LinuxRendererGLES: Cleaning up GLES resources");
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ glFinish();
+
+ // YV12 textures
+ for (int i = 0; i < NUM_BUFFERS; ++i)
+ {
+ DeleteTexture(i);
+ }
+
+ // cleanup framebuffer object if it was in use
+ m_fbo.fbo.Cleanup();
+ m_bValidated = false;
+ m_bConfigured = false;
+
+ CServiceBroker::GetWinSystem()->SetHDR(nullptr);
+}
+
+bool CLinuxRendererGLES::CreateTexture(int index)
+{
+ if (m_format == AV_PIX_FMT_NV12)
+ {
+ return CreateNV12Texture(index);
+ }
+ else
+ {
+ return CreateYV12Texture(index);
+ }
+}
+
+void CLinuxRendererGLES::DeleteTexture(int index)
+{
+ ReleaseBuffer(index);
+
+ if (m_format == AV_PIX_FMT_NV12)
+ {
+ DeleteNV12Texture(index);
+ }
+ else
+ {
+ DeleteYV12Texture(index);
+ }
+}
+
+bool CLinuxRendererGLES::UploadTexture(int index)
+{
+ if (!m_buffers[index].videoBuffer)
+ {
+ return false;
+ }
+
+ if (m_buffers[index].loaded)
+ {
+ return true;
+ }
+
+ bool ret{false};
+
+ YuvImage &dst = m_buffers[index].image;
+ m_buffers[index].videoBuffer->GetPlanes(dst.plane);
+ m_buffers[index].videoBuffer->GetStrides(dst.stride);
+
+ if (m_format == AV_PIX_FMT_NV12)
+ {
+ ret = UploadNV12Texture(index);
+ }
+ else
+ {
+ // default to YV12 texture handlers
+ ret = UploadYV12Texture(index);
+ }
+
+ if (ret)
+ {
+ m_buffers[index].loaded = true;
+ }
+
+ return ret;
+}
+
+void CLinuxRendererGLES::Render(unsigned int flags, int index)
+{
+ // obtain current field, if interlaced
+ if( flags & RENDER_FLAG_TOP)
+ {
+ m_currentField = FIELD_TOP;
+ }
+ else if (flags & RENDER_FLAG_BOT)
+ {
+ m_currentField = FIELD_BOT;
+ }
+ else
+ {
+ m_currentField = FIELD_FULL;
+ }
+
+ // call texture load function
+ if (!UploadTexture(index))
+ {
+ return;
+ }
+
+ if (RenderHook(index))
+ {
+ ;
+ }
+ else if (m_renderMethod & RENDER_GLSL)
+ {
+ UpdateVideoFilter();
+ switch(m_renderQuality)
+ {
+ case RQ_LOW:
+ case RQ_SINGLEPASS:
+ {
+ RenderSinglePass(index, m_currentField);
+ VerifyGLState();
+ break;
+ }
+ case RQ_MULTIPASS:
+ {
+ RenderToFBO(index, m_currentField);
+ RenderFromFBO();
+ VerifyGLState();
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ AfterRenderHook(index);
+}
+
+void CLinuxRendererGLES::RenderSinglePass(int index, int field)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = m_buffers[index].fields[field];
+
+ AVColorPrimaries srcPrim = GetSrcPrimaries(buf.m_srcPrimaries, buf.image.width, buf.image.height);
+ if (srcPrim != m_srcPrimaries)
+ {
+ m_srcPrimaries = srcPrim;
+ m_reloadShaders = true;
+ }
+
+ bool toneMap = false;
+ ETONEMAPMETHOD toneMapMethod = m_videoSettings.m_ToneMapMethod;
+
+ if (!m_passthroughHDR && toneMapMethod != VS_TONEMAPMETHOD_OFF)
+ {
+ if (buf.hasLightMetadata || (buf.hasDisplayMetadata && buf.displayMetadata.has_luminance))
+ {
+ toneMap = true;
+ }
+ }
+
+ if (toneMap != m_toneMap || toneMapMethod != m_toneMapMethod)
+ {
+ m_reloadShaders = true;
+ }
+
+ m_toneMap = toneMap;
+ m_toneMapMethod = toneMapMethod;
+
+ if (m_reloadShaders)
+ {
+ LoadShaders(field);
+ }
+
+ glDisable(GL_DEPTH_TEST);
+
+ // Y
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(m_textureTarget, planes[0].id);
+
+ // U
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(m_textureTarget, planes[1].id);
+
+ // V
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(m_textureTarget, planes[2].id);
+
+ glActiveTexture(GL_TEXTURE0);
+ VerifyGLState();
+
+ Shaders::GLES::BaseYUV2RGBGLSLShader* pYUVShader;
+ if (field != FIELD_FULL)
+ {
+ pYUVShader = m_pYUVBobShader;
+ }
+ else
+ {
+ pYUVShader = m_pYUVProgShader;
+ }
+
+ pYUVShader->SetBlack(m_videoSettings.m_Brightness * 0.01f - 0.5f);
+ pYUVShader->SetContrast(m_videoSettings.m_Contrast * 0.02f);
+ pYUVShader->SetWidth(planes[0].texwidth);
+ pYUVShader->SetHeight(planes[0].texheight);
+ pYUVShader->SetColParams(buf.m_srcColSpace, buf.m_srcBits, !buf.m_srcFullRange, buf.m_srcTextureBits);
+ pYUVShader->SetDisplayMetadata(buf.hasDisplayMetadata, buf.displayMetadata,
+ buf.hasLightMetadata, buf.lightMetadata);
+ pYUVShader->SetToneMapParam(m_videoSettings.m_ToneMapParam);
+
+ if (field == FIELD_TOP)
+ {
+ pYUVShader->SetField(1);
+ }
+ else if(field == FIELD_BOT)
+ {
+ pYUVShader->SetField(0);
+ }
+
+ pYUVShader->SetMatrices(glMatrixProject.Get(), glMatrixModview.Get());
+ pYUVShader->Enable();
+
+ GLubyte idx[4] = {0, 1, 3, 2}; // determines order of triangle strip
+ GLfloat m_vert[4][3];
+ GLfloat m_tex[3][4][2];
+
+ GLint vertLoc = pYUVShader->GetVertexLoc();
+ GLint Yloc = pYUVShader->GetYcoordLoc();
+ GLint Uloc = pYUVShader->GetUcoordLoc();
+ GLint Vloc = pYUVShader->GetVcoordLoc();
+
+ glVertexAttribPointer(vertLoc, 3, GL_FLOAT, 0, 0, m_vert);
+ glVertexAttribPointer(Yloc, 2, GL_FLOAT, 0, 0, m_tex[0]);
+ glVertexAttribPointer(Uloc, 2, GL_FLOAT, 0, 0, m_tex[1]);
+ glVertexAttribPointer(Vloc, 2, GL_FLOAT, 0, 0, m_tex[2]);
+
+ glEnableVertexAttribArray(vertLoc);
+ glEnableVertexAttribArray(Yloc);
+ glEnableVertexAttribArray(Uloc);
+ glEnableVertexAttribArray(Vloc);
+
+ // Setup vertex position values
+ for(int i = 0; i < 4; i++)
+ {
+ m_vert[i][0] = m_rotatedDestCoords[i].x;
+ m_vert[i][1] = m_rotatedDestCoords[i].y;
+ m_vert[i][2] = 0.0f;// set z to 0
+ }
+
+ // Setup texture coordinates
+ for (int i = 0; i < 3; i++)
+ {
+ m_tex[i][0][0] = m_tex[i][3][0] = planes[i].rect.x1;
+ m_tex[i][0][1] = m_tex[i][1][1] = planes[i].rect.y1;
+ m_tex[i][1][0] = m_tex[i][2][0] = planes[i].rect.x2;
+ m_tex[i][2][1] = m_tex[i][3][1] = planes[i].rect.y2;
+ }
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, idx);
+
+ VerifyGLState();
+
+ pYUVShader->Disable();
+ VerifyGLState();
+
+ glDisableVertexAttribArray(vertLoc);
+ glDisableVertexAttribArray(Yloc);
+ glDisableVertexAttribArray(Uloc);
+ glDisableVertexAttribArray(Vloc);
+
+ VerifyGLState();
+}
+
+void CLinuxRendererGLES::RenderToFBO(int index, int field)
+{
+ CPictureBuffer &buf = m_buffers[index];
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = m_buffers[index].fields[field];
+
+ AVColorPrimaries srcPrim = GetSrcPrimaries(buf.m_srcPrimaries, buf.image.width, buf.image.height);
+ if (srcPrim != m_srcPrimaries)
+ {
+ m_srcPrimaries = srcPrim;
+ m_reloadShaders = true;
+ }
+
+ bool toneMap = false;
+ ETONEMAPMETHOD toneMapMethod = m_videoSettings.m_ToneMapMethod;
+
+ if (toneMapMethod != VS_TONEMAPMETHOD_OFF)
+ {
+ if (buf.hasLightMetadata || (buf.hasDisplayMetadata && buf.displayMetadata.has_luminance))
+ {
+ toneMap = true;
+ }
+ }
+
+ if (toneMap != m_toneMap || m_toneMapMethod != toneMapMethod)
+ {
+ m_reloadShaders = true;
+ }
+
+ m_toneMap = toneMap;
+ m_toneMapMethod = toneMapMethod;
+
+ if (m_reloadShaders)
+ {
+ m_reloadShaders = 0;
+ LoadShaders(m_currentField);
+ }
+
+ if (!m_fbo.fbo.IsValid())
+ {
+ if (!m_fbo.fbo.Initialize())
+ {
+ CLog::Log(LOGERROR, "GLES: Error initializing FBO");
+ return;
+ }
+
+ if (!m_fbo.fbo.CreateAndBindToTexture(GL_TEXTURE_2D, m_sourceWidth, m_sourceHeight, GL_RGBA))
+ {
+ CLog::Log(LOGERROR, "GLES: Error creating texture and binding to FBO");
+ return;
+ }
+ }
+
+ glDisable(GL_DEPTH_TEST);
+
+ // Y
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(m_textureTarget, planes[0].id);
+ VerifyGLState();
+
+ // U
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(m_textureTarget, planes[1].id);
+ VerifyGLState();
+
+ // V
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(m_textureTarget, planes[2].id);
+ VerifyGLState();
+
+ glActiveTexture(GL_TEXTURE0);
+ VerifyGLState();
+
+ Shaders::GLES::BaseYUV2RGBGLSLShader* pYUVShader = m_pYUVProgShader;
+ // make sure the yuv shader is loaded and ready to go
+ if (!pYUVShader || (!pYUVShader->OK()))
+ {
+ CLog::Log(LOGERROR, "GLES: YUV shader not active, cannot do multipass render");
+ return;
+ }
+
+ m_fbo.fbo.BeginRender();
+ VerifyGLState();
+
+ pYUVShader->SetBlack(m_videoSettings.m_Brightness * 0.01f - 0.5f);
+ pYUVShader->SetContrast(m_videoSettings.m_Contrast * 0.02f);
+ pYUVShader->SetWidth(planes[0].texwidth);
+ pYUVShader->SetHeight(planes[0].texheight);
+ pYUVShader->SetColParams(buf.m_srcColSpace, buf.m_srcBits, !buf.m_srcFullRange, buf.m_srcTextureBits);
+ pYUVShader->SetDisplayMetadata(buf.hasDisplayMetadata, buf.displayMetadata,
+ buf.hasLightMetadata, buf.lightMetadata);
+ pYUVShader->SetToneMapParam(m_videoSettings.m_ToneMapParam);
+
+ if (field == FIELD_TOP)
+ {
+ pYUVShader->SetField(1);
+ }
+ else if(field == FIELD_BOT)
+ {
+ pYUVShader->SetField(0);
+ }
+
+ VerifyGLState();
+
+ glMatrixModview.Push();
+ glMatrixModview->LoadIdentity();
+ glMatrixModview.Load();
+
+ glMatrixProject.Push();
+ glMatrixProject->LoadIdentity();
+ glMatrixProject->Ortho2D(0, m_sourceWidth, 0, m_sourceHeight);
+ glMatrixProject.Load();
+
+ pYUVShader->SetMatrices(glMatrixProject.Get(), glMatrixModview.Get());
+
+ CRect viewport;
+ m_renderSystem->GetViewPort(viewport);
+ glViewport(0, 0, m_sourceWidth, m_sourceHeight);
+ glScissor(0, 0, m_sourceWidth, m_sourceHeight);
+
+ if (!pYUVShader->Enable())
+ {
+ CLog::Log(LOGERROR, "GLES: Error enabling YUV shader");
+ }
+
+ m_fbo.width = planes[0].rect.x2 - planes[0].rect.x1;
+ m_fbo.height = planes[0].rect.y2 - planes[0].rect.y1;
+
+ if (m_textureTarget == GL_TEXTURE_2D)
+ {
+ m_fbo.width *= planes[0].texwidth;
+ m_fbo.height *= planes[0].texheight;
+ }
+
+ m_fbo.width *= planes[0].pixpertex_x;
+ m_fbo.height *= planes[0].pixpertex_y;
+
+ // 1st Pass to video frame size
+ GLubyte idx[4] = {0, 1, 3, 2}; // determines order of triangle strip
+ GLfloat vert[4][3];
+ GLfloat tex[3][4][2];
+
+ GLint vertLoc = pYUVShader->GetVertexLoc();
+ GLint Yloc = pYUVShader->GetYcoordLoc();
+ GLint Uloc = pYUVShader->GetUcoordLoc();
+ GLint Vloc = pYUVShader->GetVcoordLoc();
+
+ glVertexAttribPointer(vertLoc, 3, GL_FLOAT, 0, 0, vert);
+ glVertexAttribPointer(Yloc, 2, GL_FLOAT, 0, 0, tex[0]);
+ glVertexAttribPointer(Uloc, 2, GL_FLOAT, 0, 0, tex[1]);
+ glVertexAttribPointer(Vloc, 2, GL_FLOAT, 0, 0, tex[2]);
+
+ glEnableVertexAttribArray(vertLoc);
+ glEnableVertexAttribArray(Yloc);
+ glEnableVertexAttribArray(Uloc);
+ glEnableVertexAttribArray(Vloc);
+
+ // Setup vertex position values
+ // Set vertex coordinates
+ vert[0][0] = vert[3][0] = 0.0f;
+ vert[0][1] = vert[1][1] = 0.0f;
+ vert[1][0] = vert[2][0] = m_fbo.width;
+ vert[2][1] = vert[3][1] = m_fbo.height;
+ vert[0][2] = vert[1][2] = vert[2][2] = vert[3][2] = 0.0f;
+
+ // Setup texture coordinates
+ for (int i = 0; i < 3; i++)
+ {
+ tex[i][0][0] = tex[i][3][0] = planes[i].rect.x1;
+ tex[i][0][1] = tex[i][1][1] = planes[i].rect.y1;
+ tex[i][1][0] = tex[i][2][0] = planes[i].rect.x2;
+ tex[i][2][1] = tex[i][3][1] = planes[i].rect.y2;
+ }
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, idx);
+
+ VerifyGLState();
+
+ pYUVShader->Disable();
+
+ glMatrixModview.PopLoad();
+ glMatrixProject.PopLoad();
+
+ VerifyGLState();
+
+ glDisableVertexAttribArray(vertLoc);
+ glDisableVertexAttribArray(Yloc);
+ glDisableVertexAttribArray(Uloc);
+ glDisableVertexAttribArray(Vloc);
+
+ m_renderSystem->SetViewPort(viewport);
+
+ m_fbo.fbo.EndRender();
+
+ VerifyGLState();
+}
+
+void CLinuxRendererGLES::RenderFromFBO()
+{
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, m_fbo.fbo.Texture());
+ VerifyGLState();
+
+ // Use regular normalized texture coordinates
+
+ // 2nd Pass to screen size with optional video filter
+
+ if (m_pVideoFilterShader)
+ {
+ GLint filter;
+ if (!m_pVideoFilterShader->GetTextureFilter(filter))
+ {
+ filter = m_scalingMethod == VS_SCALINGMETHOD_NEAREST ? GL_NEAREST : GL_LINEAR;
+ }
+
+ m_fbo.fbo.SetFiltering(GL_TEXTURE_2D, filter);
+ m_pVideoFilterShader->SetSourceTexture(0);
+ m_pVideoFilterShader->SetWidth(m_sourceWidth);
+ m_pVideoFilterShader->SetHeight(m_sourceHeight);
+ m_pVideoFilterShader->SetAlpha(1.0f);
+ m_pVideoFilterShader->SetMatrices(glMatrixProject.Get(), glMatrixModview.Get());
+ m_pVideoFilterShader->Enable();
+ }
+ else
+ {
+ GLint filter = m_scalingMethod == VS_SCALINGMETHOD_NEAREST ? GL_NEAREST : GL_LINEAR;
+ m_fbo.fbo.SetFiltering(GL_TEXTURE_2D, filter);
+ }
+
+ VerifyGLState();
+
+ float imgwidth = m_fbo.width / m_sourceWidth;
+ float imgheight = m_fbo.height / m_sourceHeight;
+
+ GLubyte idx[4] = {0, 1, 3, 2}; // determines order of triangle strip
+ GLfloat vert[4][3];
+ GLfloat tex[4][2];
+
+ GLint vertLoc = m_pVideoFilterShader->GetVertexLoc();
+ GLint loc = m_pVideoFilterShader->GetcoordLoc();
+
+ glVertexAttribPointer(vertLoc, 3, GL_FLOAT, 0, 0, vert);
+ glVertexAttribPointer(loc, 2, GL_FLOAT, 0, 0, tex);
+
+ glEnableVertexAttribArray(vertLoc);
+ glEnableVertexAttribArray(loc);
+
+ // Setup vertex position values
+ for(int i = 0; i < 4; i++)
+ {
+ vert[i][0] = m_rotatedDestCoords[i].x;
+ vert[i][1] = m_rotatedDestCoords[i].y;
+ vert[i][2] = 0.0f; // set z to 0
+ }
+
+ // Setup texture coordinates
+ tex[0][0] = tex[3][0] = 0.0f;
+ tex[0][1] = tex[1][1] = 0.0f;
+ tex[1][0] = tex[2][0] = imgwidth;
+ tex[2][1] = tex[3][1] = imgheight;
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, idx);
+
+ VerifyGLState();
+
+ if (m_pVideoFilterShader)
+ {
+ m_pVideoFilterShader->Disable();
+ }
+
+ VerifyGLState();
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+ VerifyGLState();
+}
+
+bool CLinuxRendererGLES::RenderCapture(CRenderCapture* capture)
+{
+ if (!m_bValidated)
+ {
+ return false;
+ }
+
+ // save current video rect
+ CRect saveSize = m_destRect;
+ saveRotatedCoords(); // backup current m_rotatedDestCoords
+
+ // new video rect is thumbnail size
+ m_destRect.SetRect(0, 0, static_cast<float>(capture->GetWidth()), static_cast<float>(capture->GetHeight()));
+ MarkDirty();
+ syncDestRectToRotatedPoints(); // syncs the changed destRect to m_rotatedDestCoords
+
+ // clear framebuffer and invert Y axis to get non-inverted image
+ glDisable(GL_BLEND);
+
+ glMatrixModview.Push();
+ glMatrixModview->Translatef(0.0f, capture->GetHeight(), 0.0f);
+ glMatrixModview->Scalef(1.0f, -1.0f, 1.0f);
+ glMatrixModview.Load();
+
+ capture->BeginRender();
+
+ Render(RENDER_FLAG_NOOSD, m_iYV12RenderBuffer);
+ // read pixels
+ glReadPixels(0, CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight() - capture->GetHeight(), capture->GetWidth(), capture->GetHeight(),
+ GL_RGBA, GL_UNSIGNED_BYTE, capture->GetRenderBuffer());
+
+ // OpenGLES returns in RGBA order but CRenderCapture needs BGRA order
+ // XOR Swap RGBA -> BGRA
+ unsigned char* pixels = static_cast<unsigned char*>(capture->GetRenderBuffer());
+ for (unsigned int i = 0; i < capture->GetWidth() * capture->GetHeight(); i++, pixels += 4)
+ {
+ std::swap(pixels[0], pixels[2]);
+ }
+
+ capture->EndRender();
+
+ // revert model view matrix
+ glMatrixModview.PopLoad();
+
+ // restore original video rect
+ m_destRect = saveSize;
+ restoreRotatedCoords(); // restores the previous state of the rotated dest coords
+
+ return true;
+}
+
+//********************************************************************************************************/
+// YV12 Texture creation, deletion, copying + clearing
+//********************************************************************************************************/
+bool CLinuxRendererGLES::UploadYV12Texture(int source)
+{
+ CPictureBuffer& buf = m_buffers[source];
+ YuvImage* im = &buf.image;
+
+ VerifyGLState();
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT,1);
+
+ // load Y plane
+ LoadPlane(buf.fields[FIELD_FULL][0], GL_LUMINANCE,
+ im->width, im->height,
+ im->stride[0], im->bpp, im->plane[0]);
+
+ // load U plane
+ LoadPlane(buf.fields[FIELD_FULL][1], GL_LUMINANCE,
+ im->width >> im->cshift_x, im->height >> im->cshift_y,
+ im->stride[1], im->bpp, im->plane[1]);
+
+ // load V plane
+ LoadPlane(buf.fields[FIELD_FULL][2], GL_ALPHA,
+ im->width >> im->cshift_x, im->height >> im->cshift_y,
+ im->stride[2], im->bpp, im->plane[2]);
+
+ VerifyGLState();
+
+ CalculateTextureSourceRects(source, 3);
+
+ return true;
+}
+
+void CLinuxRendererGLES::DeleteYV12Texture(int index)
+{
+ YuvImage &im = m_buffers[index].image;
+
+ if (m_buffers[index].fields[FIELD_FULL][0].id == 0)
+ {
+ return;
+ }
+
+ // finish up all textures, and delete them
+ for(int f = 0; f < MAX_FIELDS; f++)
+ {
+ for(int p = 0; p < YuvImage::MAX_PLANES; p++)
+ {
+ if (m_buffers[index].fields[f][p].id)
+ {
+ if (glIsTexture(m_buffers[index].fields[f][p].id))
+ {
+ glDeleteTextures(1, &m_buffers[index].fields[f][p].id);
+ }
+
+ m_buffers[index].fields[f][p].id = 0;
+ }
+ }
+ }
+
+ for(int p = 0; p < YuvImage::MAX_PLANES; p++)
+ {
+ im.plane[p] = nullptr;
+ }
+}
+
+bool CLinuxRendererGLES::CreateYV12Texture(int index)
+{
+ // since we also want the field textures, pitch must be texture aligned
+ unsigned p;
+ YuvImage &im = m_buffers[index].image;
+
+ DeleteYV12Texture(index);
+
+ im.height = m_sourceHeight;
+ im.width = m_sourceWidth;
+ im.cshift_x = 1;
+ im.cshift_y = 1;
+ im.bpp = 1;
+
+ im.stride[0] = im.bpp * im.width;
+ im.stride[1] = im.bpp * (im.width >> im.cshift_x);
+ im.stride[2] = im.bpp * (im.width >> im.cshift_x);
+
+ im.planesize[0] = im.stride[0] * im.height;
+ im.planesize[1] = im.stride[1] * (im.height >> im.cshift_y);
+ im.planesize[2] = im.stride[2] * (im.height >> im.cshift_y);
+
+ for (int i = 0; i < 3; i++)
+ {
+ im.plane[i] = nullptr; // will be set in UploadTexture()
+ }
+
+ for(int f = 0; f < MAX_FIELDS; f++)
+ {
+ for(p = 0; p < YuvImage::MAX_PLANES; p++)
+ {
+ if (!glIsTexture(m_buffers[index].fields[f][p].id))
+ {
+ glGenTextures(1, &m_buffers[index].fields[f][p].id);
+ VerifyGLState();
+ }
+ }
+ }
+
+ // YUV
+ for (int f = FIELD_FULL; f <= FIELD_BOT ; f++)
+ {
+ int fieldshift = (f == FIELD_FULL) ? 0 : 1;
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = m_buffers[index].fields[f];
+
+ planes[0].texwidth = im.width;
+ planes[0].texheight = im.height >> fieldshift;
+
+ planes[1].texwidth = planes[0].texwidth >> im.cshift_x;
+ planes[1].texheight = planes[0].texheight >> im.cshift_y;
+ planes[2].texwidth = planes[0].texwidth >> im.cshift_x;
+ planes[2].texheight = planes[0].texheight >> im.cshift_y;
+
+ for (int p = 0; p < 3; p++)
+ {
+ planes[p].pixpertex_x = 1;
+ planes[p].pixpertex_y = 1;
+ }
+
+ for(int p = 0; p < 3; p++)
+ {
+ CYuvPlane &plane = planes[p];
+ if (plane.texwidth * plane.texheight == 0)
+ {
+ continue;
+ }
+
+ glBindTexture(m_textureTarget, plane.id);
+
+ GLint format;
+ if (p == 2) // V plane needs an alpha texture
+ {
+ format = GL_ALPHA;
+ }
+ else
+ {
+ format = GL_LUMINANCE;
+ }
+
+ glTexImage2D(m_textureTarget, 0, format, plane.texwidth, plane.texheight, 0, format, GL_UNSIGNED_BYTE, nullptr);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ VerifyGLState();
+ }
+ }
+ return true;
+}
+
+//********************************************************************************************************
+// NV12 Texture loading, creation and deletion
+//********************************************************************************************************
+bool CLinuxRendererGLES::UploadNV12Texture(int source)
+{
+ CPictureBuffer& buf = m_buffers[source];
+ YuvImage* im = &buf.image;
+
+ bool deinterlacing;
+ if (m_currentField == FIELD_FULL)
+ {
+ deinterlacing = false;
+ }
+ else
+ {
+ deinterlacing = true;
+ }
+
+ VerifyGLState();
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT, im->bpp);
+
+ if (deinterlacing)
+ {
+ // Load Odd Y field
+ LoadPlane(buf.fields[FIELD_TOP][0] , GL_LUMINANCE,
+ im->width, im->height >> 1,
+ im->stride[0]*2, im->bpp, im->plane[0]);
+
+ // Load Even Y field
+ LoadPlane(buf.fields[FIELD_BOT][0], GL_LUMINANCE,
+ im->width, im->height >> 1,
+ im->stride[0]*2, im->bpp, im->plane[0] + im->stride[0]) ;
+
+ // Load Odd UV Fields
+ LoadPlane(buf.fields[FIELD_TOP][1], GL_LUMINANCE_ALPHA,
+ im->width >> im->cshift_x, im->height >> (im->cshift_y + 1),
+ im->stride[1]*2, im->bpp, im->plane[1]);
+
+ // Load Even UV Fields
+ LoadPlane(buf.fields[FIELD_BOT][1], GL_LUMINANCE_ALPHA,
+ im->width >> im->cshift_x, im->height >> (im->cshift_y + 1),
+ im->stride[1]*2, im->bpp, im->plane[1] + im->stride[1]);
+
+ }
+ else
+ {
+ // Load Y plane
+ LoadPlane(buf. fields[FIELD_FULL][0], GL_LUMINANCE,
+ im->width, im->height,
+ im->stride[0], im->bpp, im->plane[0]);
+
+ // Load UV plane
+ LoadPlane(buf.fields[FIELD_FULL][1], GL_LUMINANCE_ALPHA,
+ im->width >> im->cshift_x, im->height >> im->cshift_y,
+ im->stride[1], im->bpp, im->plane[1]);
+ }
+
+ VerifyGLState();
+
+ CalculateTextureSourceRects(source, 3);
+
+ return true;
+}
+
+bool CLinuxRendererGLES::CreateNV12Texture(int index)
+{
+ // since we also want the field textures, pitch must be texture aligned
+ CPictureBuffer& buf = m_buffers[index];
+ YuvImage &im = buf.image;
+
+ // Delete any old texture
+ DeleteNV12Texture(index);
+
+ im.height = m_sourceHeight;
+ im.width = m_sourceWidth;
+ im.cshift_x = 1;
+ im.cshift_y = 1;
+ im.bpp = 1;
+
+ im.stride[0] = im.width;
+ im.stride[1] = im.width;
+ im.stride[2] = 0;
+
+ im.plane[0] = nullptr;
+ im.plane[1] = nullptr;
+ im.plane[2] = nullptr;
+
+ // Y plane
+ im.planesize[0] = im.stride[0] * im.height;
+ // packed UV plane
+ im.planesize[1] = im.stride[1] * im.height / 2;
+ // third plane is not used
+ im.planesize[2] = 0;
+
+ for (int i = 0; i < 2; i++)
+ {
+ im.plane[i] = nullptr; // will be set in UploadTexture()
+ }
+
+ for(int f = 0; f < MAX_FIELDS; f++)
+ {
+ for(int p = 0; p < 2; p++)
+ {
+ if (!glIsTexture(buf.fields[f][p].id))
+ {
+ glGenTextures(1, &buf.fields[f][p].id);
+ VerifyGLState();
+ }
+ }
+
+ buf.fields[f][2].id = buf.fields[f][1].id;
+ }
+
+ // YUV
+ for (int f = FIELD_FULL; f <= FIELD_BOT; f++)
+ {
+ int fieldshift = (f == FIELD_FULL) ? 0 : 1;
+ CYuvPlane (&planes)[YuvImage::MAX_PLANES] = buf.fields[f];
+
+ planes[0].texwidth = im.width;
+ planes[0].texheight = im.height >> fieldshift;
+
+ planes[1].texwidth = planes[0].texwidth >> im.cshift_x;
+ planes[1].texheight = planes[0].texheight >> im.cshift_y;
+ planes[2].texwidth = planes[1].texwidth;
+ planes[2].texheight = planes[1].texheight;
+
+ for (int p = 0; p < 3; p++)
+ {
+ planes[p].pixpertex_x = 1;
+ planes[p].pixpertex_y = 1;
+ }
+
+ for(int p = 0; p < 2; p++)
+ {
+ CYuvPlane &plane = planes[p];
+ if (plane.texwidth * plane.texheight == 0)
+ {
+ continue;
+ }
+
+ glBindTexture(m_textureTarget, plane.id);
+
+ if (p == 1)
+ {
+ glTexImage2D(m_textureTarget, 0, GL_LUMINANCE_ALPHA, plane.texwidth, plane.texheight, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, nullptr);
+ }
+ else
+ {
+ glTexImage2D(m_textureTarget, 0, GL_LUMINANCE, plane.texwidth, plane.texheight, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, nullptr);
+ }
+
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ VerifyGLState();
+ }
+ }
+
+ return true;
+}
+
+void CLinuxRendererGLES::DeleteNV12Texture(int index)
+{
+ CPictureBuffer& buf = m_buffers[index];
+ YuvImage &im = buf.image;
+
+ if (buf.fields[FIELD_FULL][0].id == 0)
+ {
+ return;
+ }
+
+ // finish up all textures, and delete them
+ for(int f = 0; f < MAX_FIELDS; f++)
+ {
+ for(int p = 0; p < 2; p++)
+ {
+ if (buf.fields[f][p].id)
+ {
+ if (glIsTexture(buf.fields[f][p].id))
+ {
+ glDeleteTextures(1, &buf.fields[f][p].id);
+ }
+
+ buf.fields[f][p].id = 0;
+ }
+ }
+
+ buf.fields[f][2].id = 0;
+ }
+
+ for(int p = 0; p < 2; p++)
+ {
+ im.plane[p] = nullptr;
+ }
+}
+
+//********************************************************************************************************
+// SurfaceTexture creation, deletion, copying + clearing
+//********************************************************************************************************
+void CLinuxRendererGLES::SetTextureFilter(GLenum method)
+{
+ for (int i = 0 ; i < m_NumYV12Buffers; i++)
+ {
+ CPictureBuffer& buf = m_buffers[i];
+
+ for (int f = FIELD_FULL; f <= FIELD_BOT; f++)
+ {
+ for (int p = 0; p < 3; p++)
+ {
+ if(glIsTexture(buf.fields[f][p].id))
+ {
+ glBindTexture(m_textureTarget, buf.fields[f][p].id);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MIN_FILTER, method);
+ glTexParameteri(m_textureTarget, GL_TEXTURE_MAG_FILTER, method);
+ VerifyGLState();
+ }
+ }
+ }
+ }
+}
+
+bool CLinuxRendererGLES::Supports(ERENDERFEATURE feature) const
+{
+ if (feature == RENDERFEATURE_GAMMA ||
+ feature == RENDERFEATURE_NOISE ||
+ feature == RENDERFEATURE_SHARPNESS ||
+ feature == RENDERFEATURE_NONLINSTRETCH)
+ {
+ return false;
+ }
+
+ if (feature == RENDERFEATURE_STRETCH ||
+ feature == RENDERFEATURE_ZOOM ||
+ feature == RENDERFEATURE_VERTICAL_SHIFT ||
+ feature == RENDERFEATURE_PIXEL_RATIO ||
+ feature == RENDERFEATURE_POSTPROCESS ||
+ feature == RENDERFEATURE_ROTATION ||
+ feature == RENDERFEATURE_BRIGHTNESS ||
+ feature == RENDERFEATURE_CONTRAST ||
+ feature == RENDERFEATURE_TONEMAP)
+ {
+ return true;
+ }
+
+ return false;
+}
+
+bool CLinuxRendererGLES::SupportsMultiPassRendering()
+{
+ return true;
+}
+
+bool CLinuxRendererGLES::Supports(ESCALINGMETHOD method) const
+{
+ if(method == VS_SCALINGMETHOD_NEAREST ||
+ method == VS_SCALINGMETHOD_LINEAR)
+ {
+ return true;
+ }
+
+ if (method == VS_SCALINGMETHOD_CUBIC_B_SPLINE ||
+ method == VS_SCALINGMETHOD_CUBIC_MITCHELL ||
+ method == VS_SCALINGMETHOD_CUBIC_CATMULL ||
+ method == VS_SCALINGMETHOD_CUBIC_0_075 ||
+ method == VS_SCALINGMETHOD_CUBIC_0_1 ||
+ method == VS_SCALINGMETHOD_LANCZOS2 ||
+ method == VS_SCALINGMETHOD_SPLINE36_FAST ||
+ method == VS_SCALINGMETHOD_LANCZOS3_FAST ||
+ method == VS_SCALINGMETHOD_SPLINE36 ||
+ method == VS_SCALINGMETHOD_LANCZOS3)
+ {
+ // if scaling is below level, avoid hq scaling
+ float scaleX = fabs((static_cast<float>(m_sourceWidth) - m_destRect.Width()) / m_sourceWidth) * 100;
+ float scaleY = fabs((static_cast<float>(m_sourceHeight) - m_destRect.Height()) / m_sourceHeight) * 100;
+ int minScale = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_HQSCALERS);
+ if (scaleX < minScale && scaleY < minScale)
+ {
+ return false;
+ }
+
+ if (m_renderMethod & RENDER_GLSL)
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+CRenderInfo CLinuxRendererGLES::GetRenderInfo()
+{
+ CRenderInfo info;
+ info.max_buffer_size = NUM_BUFFERS;
+
+ return info;
+}
+
+bool CLinuxRendererGLES::IsGuiLayer()
+{
+ return true;
+}
+
+AVColorPrimaries CLinuxRendererGLES::GetSrcPrimaries(AVColorPrimaries srcPrimaries, unsigned int width, unsigned int height)
+{
+ AVColorPrimaries ret = srcPrimaries;
+ if (ret == AVCOL_PRI_UNSPECIFIED)
+ {
+ if (width > 1024 || height >= 600)
+ {
+ ret = AVCOL_PRI_BT709;
+ }
+ else
+ {
+ ret = AVCOL_PRI_BT470BG;
+ }
+ }
+
+ return ret;
+}
+
+CRenderCapture* CLinuxRendererGLES::GetRenderCapture()
+{
+ return new CRenderCaptureGLES;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h b/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h
new file mode 100644
index 0000000..8db3ed3
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h
@@ -0,0 +1,216 @@
+/*
+ * 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 <vector>
+
+#include "system_gl.h"
+
+#include "BaseRenderer.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+#include "cores/VideoSettings.h"
+#include "FrameBufferObject.h"
+#include "guilib/Shader.h"
+#include "RenderFlags.h"
+#include "RenderInfo.h"
+#include "windowing/GraphicContext.h"
+
+extern "C" {
+#include <libavutil/mastering_display_metadata.h>
+}
+
+class CRenderCapture;
+class CRenderSystemGLES;
+
+class CTexture;
+namespace Shaders
+{
+namespace GLES
+{
+class BaseYUV2RGBGLSLShader;
+class BaseVideoFilterShader;
+}
+} // namespace Shaders
+
+enum RenderMethod
+{
+ RENDER_GLSL = 0x01,
+ RENDER_CUSTOM = 0x02,
+};
+
+enum RenderQuality
+{
+ RQ_LOW = 1,
+ RQ_SINGLEPASS,
+ RQ_MULTIPASS,
+ RQ_SOFTWARE
+};
+
+class CEvent;
+
+class CLinuxRendererGLES : public CBaseRenderer
+{
+public:
+ CLinuxRendererGLES();
+ ~CLinuxRendererGLES() override;
+
+ // Registration
+ static CBaseRenderer* Create(CVideoBuffer *buffer);
+ static bool Register();
+
+ // Player functions
+ bool Configure(const VideoPicture& picture, float fps, unsigned int orientation) override;
+ bool IsConfigured() override { return m_bConfigured; }
+ void AddVideoPicture(const VideoPicture& picture, int index) override;
+ void UnInit() override;
+ bool Flush(bool saveBuffers) override;
+ void SetBufferSize(int numBuffers) override { m_NumYV12Buffers = numBuffers; }
+ bool IsGuiLayer() override;
+ void ReleaseBuffer(int idx) override;
+ void RenderUpdate(int index, int index2, bool clear, unsigned int flags, unsigned int alpha) override;
+ void Update() override;
+ bool RenderCapture(CRenderCapture* capture) override;
+ CRenderInfo GetRenderInfo() override;
+ bool ConfigChanged(const VideoPicture& picture) override;
+
+ // Feature support
+ bool SupportsMultiPassRendering() override;
+ bool Supports(ERENDERFEATURE feature) const override;
+ bool Supports(ESCALINGMETHOD method) const override;
+
+ CRenderCapture* GetRenderCapture() override;
+
+protected:
+ static const int FIELD_FULL{0};
+ static const int FIELD_TOP{1};
+ static const int FIELD_BOT{2};
+
+ virtual void Render(unsigned int flags, int index);
+ virtual void RenderUpdateVideo(bool clear, unsigned int flags = 0, unsigned int alpha = 255);
+
+ int NextYV12Texture();
+ virtual bool ValidateRenderTarget();
+ virtual void LoadShaders(int field=FIELD_FULL);
+ virtual void ReleaseShaders();
+ void SetTextureFilter(GLenum method);
+ void UpdateVideoFilter();
+ AVColorPrimaries GetSrcPrimaries(AVColorPrimaries srcPrimaries, unsigned int width, unsigned int height);
+
+ // textures
+ virtual bool UploadTexture(int index);
+ virtual void DeleteTexture(int index);
+ virtual bool CreateTexture(int index);
+
+ bool UploadYV12Texture(int index);
+ void DeleteYV12Texture(int index);
+ bool CreateYV12Texture(int index);
+ virtual bool SkipUploadYV12(int index) { return false; }
+
+ bool UploadNV12Texture(int index);
+ void DeleteNV12Texture(int index);
+ bool CreateNV12Texture(int index);
+
+ void CalculateTextureSourceRects(int source, int num_planes);
+
+ // renderers
+ void RenderToFBO(int index, int field);
+ void RenderFromFBO();
+ void RenderSinglePass(int index, int field); // single pass glsl renderer
+
+ // hooks for HwDec Renderered
+ virtual bool LoadShadersHook() { return false; }
+ virtual bool RenderHook(int idx) { return false; }
+ virtual void AfterRenderHook(int idx) {}
+
+ struct
+ {
+ CFrameBufferObject fbo;
+ float width{0.0};
+ float height{0.0};
+ } m_fbo;
+
+ int m_iYV12RenderBuffer{0};
+ int m_NumYV12Buffers{0};
+
+ bool m_bConfigured{false};
+ bool m_bValidated{false};
+ GLenum m_textureTarget = GL_TEXTURE_2D;
+ int m_renderMethod{RENDER_GLSL};
+ RenderQuality m_renderQuality{RQ_SINGLEPASS};
+
+ // Raw data used by renderer
+ int m_currentField{FIELD_FULL};
+ int m_reloadShaders{0};
+ CRenderSystemGLES *m_renderSystem{nullptr};
+ GLenum m_pixelStoreKey{0};
+
+ struct CYuvPlane
+ {
+ GLuint id{0};
+ CRect rect{0, 0, 0, 0};
+
+ float width{0.0};
+ float height{0.0};
+
+ unsigned texwidth{0};
+ unsigned texheight{0};
+
+ //pixels per texel
+ unsigned pixpertex_x{0};
+ unsigned pixpertex_y{0};
+ };
+
+ struct CPictureBuffer
+ {
+ CYuvPlane fields[MAX_FIELDS][YuvImage::MAX_PLANES];
+ YuvImage image;
+
+ CVideoBuffer *videoBuffer{nullptr};
+ bool loaded{false};
+
+ AVColorPrimaries m_srcPrimaries;
+ AVColorSpace m_srcColSpace;
+ int m_srcBits{8};
+ int m_srcTextureBits{8};
+ bool m_srcFullRange;
+
+ bool hasDisplayMetadata{false};
+ AVMasteringDisplayMetadata displayMetadata;
+ bool hasLightMetadata{false};
+ AVContentLightMetadata lightMetadata;
+ };
+
+ // YV12 decoder textures
+ // field index 0 is full image, 1 is odd scanlines, 2 is even scanlines
+ CPictureBuffer m_buffers[NUM_BUFFERS];
+
+ void LoadPlane(CYuvPlane& plane, int type,
+ unsigned width, unsigned height,
+ int stride, int bpp, void* data);
+
+ Shaders::GLES::BaseYUV2RGBGLSLShader* m_pYUVProgShader{nullptr};
+ Shaders::GLES::BaseYUV2RGBGLSLShader* m_pYUVBobShader{nullptr};
+ Shaders::GLES::BaseVideoFilterShader* m_pVideoFilterShader{nullptr};
+ ESCALINGMETHOD m_scalingMethod{VS_SCALINGMETHOD_LINEAR};
+ ESCALINGMETHOD m_scalingMethodGui{VS_SCALINGMETHOD_MAX};
+ bool m_fullRange;
+ AVColorPrimaries m_srcPrimaries;
+ bool m_toneMap = false;
+ ETONEMAPMETHOD m_toneMapMethod = VS_TONEMAPMETHOD_OFF;
+ bool m_passthroughHDR = false;
+ unsigned char* m_planeBuffer = nullptr;
+ size_t m_planeBufferSize = 0;
+
+ // clear colour for "black" bars
+ float m_clearColour{0.0f};
+ CRect m_viewRect;
+
+private:
+ void DrawBlackBars();
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRenderer.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRenderer.cpp
new file mode 100644
index 0000000..b6f5e5e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRenderer.cpp
@@ -0,0 +1,638 @@
+/*
+ * Initial code sponsored by: Voddler Inc (voddler.com)
+ * 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 "OverlayRenderer.h"
+
+#include "OverlayRendererUtil.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlay.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayImage.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayLibass.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySpu.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "windowing/GraphicContext.h"
+
+#include <mutex>
+#if defined(HAS_GL) || defined(HAS_GLES)
+#include "OverlayRendererGL.h"
+#elif defined(HAS_DX)
+#include "OverlayRendererDX.h"
+#endif
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace OVERLAY;
+
+COverlay::COverlay()
+{
+ m_x = 0.0f;
+ m_y = 0.0f;
+ m_width = 0.0f;
+ m_height = 0.0f;
+ m_type = TYPE_NONE;
+ m_align = ALIGN_SCREEN;
+ m_pos = POSITION_RELATIVE;
+}
+
+COverlay::~COverlay() = default;
+
+unsigned int CRenderer::m_textureid = 1;
+
+CRenderer::CRenderer()
+{
+ CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()->RegisterObserver(this);
+}
+
+CRenderer::~CRenderer()
+{
+ CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()->UnregisterObserver(this);
+ Flush();
+}
+
+void CRenderer::AddOverlay(CDVDOverlay* o, double pts, int index)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ SElement e;
+ e.pts = pts;
+ e.overlay_dvd = o->Acquire();
+ m_buffers[index].push_back(e);
+}
+
+void CRenderer::Release(std::vector<SElement>& list)
+{
+ std::vector<SElement> l = list;
+ list.clear();
+
+ for (auto &elem : l)
+ {
+ if (elem.overlay_dvd)
+ elem.overlay_dvd->Release();
+ }
+}
+
+void CRenderer::UnInit()
+{
+ if (m_saveSubtitlePosition)
+ {
+ m_saveSubtitlePosition = false;
+ CDisplaySettings::GetInstance().UpdateCalibrations();
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ }
+
+ Flush();
+}
+
+void CRenderer::Flush()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ for(std::vector<SElement>& buffer : m_buffers)
+ Release(buffer);
+
+ ReleaseCache();
+ Reset();
+}
+
+void CRenderer::Reset()
+{
+ m_subtitlePosition = 0;
+ m_subtitlePosResInfo = -1;
+}
+
+void CRenderer::Release(int idx)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ Release(m_buffers[idx]);
+}
+
+void CRenderer::ReleaseCache()
+{
+ for (auto& overlay : m_textureCache)
+ {
+ delete overlay.second;
+ }
+ m_textureCache.clear();
+ m_textureid++;
+}
+
+void CRenderer::ReleaseUnused()
+{
+ for (auto it = m_textureCache.begin(); it != m_textureCache.end(); )
+ {
+ bool found = false;
+ for (auto& buffer : m_buffers)
+ {
+ for (auto& dvdoverlay : buffer)
+ {
+ if (dvdoverlay.overlay_dvd && dvdoverlay.overlay_dvd->m_textureid == it->first)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (found)
+ break;
+ }
+ if (!found)
+ {
+ delete it->second;
+ it = m_textureCache.erase(it);
+ }
+ else
+ ++it;
+ }
+}
+
+void CRenderer::Render(int idx)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ std::vector<SElement>& list = m_buffers[idx];
+ for(std::vector<SElement>::iterator it = list.begin(); it != list.end(); ++it)
+ {
+ if (it->overlay_dvd)
+ {
+ COverlay* o = Convert(it->overlay_dvd, it->pts);
+
+ if (o)
+ Render(o);
+ }
+ }
+
+ ReleaseUnused();
+}
+
+void CRenderer::Render(COverlay* o)
+{
+ SRenderState state;
+ state.x = o->m_x;
+ state.y = o->m_y;
+ state.width = o->m_width;
+ state.height = o->m_height;
+
+ COverlay::EPosition pos = o->m_pos;
+ COverlay::EAlign align = o->m_align;
+
+ if (pos == COverlay::POSITION_RELATIVE)
+ {
+ float scale_x = 1.0;
+ float scale_y = 1.0;
+ float scale_w = 1.0;
+ float scale_h = 1.0;
+
+ if (align == COverlay::ALIGN_SCREEN || align == COverlay::ALIGN_SUBTITLE)
+ {
+ scale_x = m_rv.Width();
+ scale_y = m_rv.Height();
+ scale_w = scale_x;
+ scale_h = scale_y;
+ }
+ else if (align == COverlay::ALIGN_SCREEN_AR)
+ {
+ // Align to screen by keeping aspect ratio to fit into the screen area
+ float source_width = o->m_source_width > 0 ? o->m_source_width : m_rs.Width();
+ float source_height = o->m_source_height > 0 ? o->m_source_height : m_rs.Height();
+ float ratio = std::min<float>(m_rv.Width() / source_width, m_rv.Height() / source_height);
+ scale_x = m_rv.Width();
+ scale_y = m_rv.Height();
+ scale_w = ratio;
+ scale_h = ratio;
+ }
+ else if (align == COverlay::ALIGN_VIDEO)
+ {
+ scale_x = m_rs.Width();
+ scale_y = m_rs.Height();
+ scale_w = scale_x;
+ scale_h = scale_y;
+ }
+
+ state.x *= scale_x;
+ state.y *= scale_y;
+ state.width *= scale_w;
+ state.height *= scale_h;
+
+ pos = COverlay::POSITION_ABSOLUTE;
+ }
+
+ if (pos == COverlay::POSITION_ABSOLUTE)
+ {
+ if (align == COverlay::ALIGN_SCREEN || align == COverlay::ALIGN_SCREEN_AR ||
+ align == COverlay::ALIGN_SUBTITLE)
+ {
+ if (align == COverlay::ALIGN_SUBTITLE)
+ {
+ RESOLUTION_INFO resInfo = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ state.x += m_rv.x1 + m_rv.Width() * 0.5f;
+ state.y += m_rv.y1 + (resInfo.iSubtitles - resInfo.Overscan.top);
+ }
+ else
+ {
+ state.x += m_rv.x1;
+ state.y += m_rv.y1;
+ }
+ }
+ else if (align == COverlay::ALIGN_VIDEO)
+ {
+ float scale_x = m_rd.Width() / m_rs.Width();
+ float scale_y = m_rd.Height() / m_rs.Height();
+
+ state.x *= scale_x;
+ state.y *= scale_y;
+ state.width *= scale_x;
+ state.height *= scale_y;
+
+ state.x += m_rd.x1;
+ state.y += m_rd.y1;
+ }
+ }
+
+ state.x += GetStereoscopicDepth();
+
+ o->Render(state);
+}
+
+bool CRenderer::HasOverlay(int idx)
+{
+ bool hasOverlay = false;
+
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ std::vector<SElement>& list = m_buffers[idx];
+ for(std::vector<SElement>::iterator it = list.begin(); it != list.end(); ++it)
+ {
+ if (it->overlay_dvd)
+ {
+ hasOverlay = true;
+ break;
+ }
+ }
+ return hasOverlay;
+}
+
+void CRenderer::SetVideoRect(CRect &source, CRect &dest, CRect &view)
+{
+ if (m_rv != view) // Screen resolution is changed
+ {
+ m_rv = view;
+ OnViewChange();
+ }
+ m_rs = source;
+ m_rd = dest;
+}
+
+void CRenderer::OnViewChange()
+{
+ m_isSettingsChanged = true;
+}
+
+void CRenderer::SetStereoMode(const std::string &stereomode)
+{
+ m_stereomode = stereomode;
+}
+
+void CRenderer::SetSubtitleVerticalPosition(const int value, bool save)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_subtitlePosition = value;
+
+ if (save && m_subtitleAlign == SUBTITLES::Align::MANUAL)
+ {
+ m_subtitlePosResInfo = POSRESINFO_SAVE_CHANGES;
+ // We save the value to XML file settings when playback is stopped
+ // to avoid saving to disk too many times
+ m_saveSubtitlePosition = true;
+ }
+}
+
+void CRenderer::ResetSubtitlePosition()
+{
+ // In the 'pos' var the vertical margin has been substracted because
+ // we need to know the actual text baseline position on screen
+ int pos{0};
+ m_saveSubtitlePosition = false;
+ RESOLUTION_INFO resInfo = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+
+ if (m_subtitleAlign == SUBTITLES::Align::MANUAL)
+ {
+ // The position must be fixed to match the subtitle calibration bar
+ m_subtitleVerticalMargin = static_cast<int>(
+ static_cast<float>(resInfo.iHeight) / 100 *
+ CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()->GetVerticalMarginPerc());
+
+ m_subtitlePosResInfo = resInfo.iSubtitles;
+ pos = resInfo.iSubtitles - m_subtitleVerticalMargin;
+ }
+ else
+ {
+ // The position must be relative to the screen frame
+ m_subtitleVerticalMargin = static_cast<int>(
+ static_cast<float>(m_rv.Height()) / 100 *
+ CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()->GetVerticalMarginPerc());
+
+ m_subtitlePosResInfo = static_cast<int>(m_rv.Height());
+ pos = static_cast<int>(m_rv.Height()) - m_subtitleVerticalMargin + resInfo.Overscan.top;
+ }
+
+ // Update player value (and callback to CRenderer::SetSubtitleVerticalPosition)
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->SetSubtitleVerticalPosition(pos, false);
+}
+
+void CRenderer::CreateSubtitlesStyle()
+{
+ m_overlayStyle = std::make_shared<SUBTITLES::STYLE::style>();
+ const auto settings{CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()};
+
+ m_overlayStyle->fontName = settings->GetFontName();
+ m_overlayStyle->fontSize = static_cast<double>(settings->GetFontSize());
+
+ SUBTITLES::FontStyle fontStyle = settings->GetFontStyle();
+ if (fontStyle == SUBTITLES::FontStyle::BOLD_ITALIC)
+ m_overlayStyle->fontStyle = SUBTITLES::STYLE::FontStyle::BOLD_ITALIC;
+ else if (fontStyle == SUBTITLES::FontStyle::BOLD)
+ m_overlayStyle->fontStyle = SUBTITLES::STYLE::FontStyle::BOLD;
+ else if (fontStyle == SUBTITLES::FontStyle::ITALIC)
+ m_overlayStyle->fontStyle = SUBTITLES::STYLE::FontStyle::ITALIC;
+
+ m_overlayStyle->fontColor = settings->GetFontColor();
+ m_overlayStyle->fontBorderSize = settings->GetBorderSize();
+ m_overlayStyle->fontBorderColor = settings->GetBorderColor();
+ m_overlayStyle->fontOpacity = settings->GetFontOpacity();
+
+ SUBTITLES::BackgroundType backgroundType = settings->GetBackgroundType();
+ if (backgroundType == SUBTITLES::BackgroundType::NONE)
+ m_overlayStyle->borderStyle = SUBTITLES::STYLE::BorderType::OUTLINE_NO_SHADOW;
+ else if (backgroundType == SUBTITLES::BackgroundType::SHADOW)
+ m_overlayStyle->borderStyle = SUBTITLES::STYLE::BorderType::OUTLINE;
+ else if (backgroundType == SUBTITLES::BackgroundType::BOX)
+ m_overlayStyle->borderStyle = SUBTITLES::STYLE::BorderType::BOX;
+ else if (backgroundType == SUBTITLES::BackgroundType::SQUAREBOX)
+ m_overlayStyle->borderStyle = SUBTITLES::STYLE::BorderType::SQUARE_BOX;
+
+ m_overlayStyle->backgroundColor = settings->GetBackgroundColor();
+ m_overlayStyle->backgroundOpacity = settings->GetBackgroundOpacity();
+
+ m_overlayStyle->shadowColor = settings->GetShadowColor();
+ m_overlayStyle->shadowOpacity = settings->GetShadowOpacity();
+ m_overlayStyle->shadowSize = settings->GetShadowSize();
+
+ SUBTITLES::Align subAlign = settings->GetAlignment();
+ if (subAlign == SUBTITLES::Align::TOP_INSIDE || subAlign == SUBTITLES::Align::TOP_OUTSIDE)
+ m_overlayStyle->alignment = SUBTITLES::STYLE::FontAlign::TOP_CENTER;
+ else
+ m_overlayStyle->alignment = SUBTITLES::STYLE::FontAlign::SUB_CENTER;
+
+ m_overlayStyle->assOverrideFont = settings->IsOverrideFonts();
+
+ SUBTITLES::OverrideStyles overrideStyles = settings->GetOverrideStyles();
+ if (overrideStyles == SUBTITLES::OverrideStyles::POSITIONS)
+ m_overlayStyle->assOverrideStyles = SUBTITLES::STYLE::OverrideStyles::POSITIONS;
+ else if (overrideStyles == SUBTITLES::OverrideStyles::STYLES)
+ m_overlayStyle->assOverrideStyles = SUBTITLES::STYLE::OverrideStyles::STYLES;
+ else if (overrideStyles == SUBTITLES::OverrideStyles::STYLES_POSITIONS)
+ m_overlayStyle->assOverrideStyles = SUBTITLES::STYLE::OverrideStyles::STYLES_POSITIONS;
+ else
+ m_overlayStyle->assOverrideStyles = SUBTITLES::STYLE::OverrideStyles::DISABLED;
+
+ // Changing vertical margin while in playback causes side effects when you
+ // rewind the video, displaying the previous text position (test Libass 15.2)
+ // for now vertical margin setting will be disabled during playback
+ m_overlayStyle->marginVertical =
+ static_cast<int>(SUBTITLES::STYLE::VIEWPORT_HEIGHT / 100 *
+ static_cast<double>(settings->GetVerticalMarginPerc()));
+
+ m_overlayStyle->blur = settings->GetBlurSize();
+}
+
+COverlay* CRenderer::ConvertLibass(
+ CDVDOverlayLibass* o,
+ double pts,
+ bool updateStyle,
+ const std::shared_ptr<struct SUBTITLES::STYLE::style>& overlayStyle)
+{
+ SUBTITLES::STYLE::renderOpts rOpts;
+
+ // libass render in a target area which named as frame. the frame size may bigger than video size,
+ // and including margins between video to frame edge. libass allow to render subtitles into the margins.
+ // this has been used to show subtitles in the top or bottom "black bar" between video to frame border.
+ rOpts.sourceWidth = m_rs.Width();
+ rOpts.sourceHeight = m_rs.Height();
+ rOpts.videoWidth = m_rd.Width();
+ rOpts.videoHeight = m_rd.Height();
+ rOpts.frameWidth = m_rv.Width();
+ rOpts.frameHeight = m_rv.Height();
+
+ // Render subtitle of half-sbs and half-ou video in full screen, not in half screen
+ if (m_stereomode == "left_right" || m_stereomode == "right_left")
+ {
+ // only half-sbs video, sbs video don't need to change source size
+ if (rOpts.sourceWidth / rOpts.sourceHeight < 1.2f)
+ rOpts.sourceWidth = m_rs.Width() * 2;
+ }
+ else if (m_stereomode == "top_bottom" || m_stereomode == "bottom_top")
+ {
+ // only half-ou video, ou video don't need to change source size
+ if (rOpts.sourceWidth / rOpts.sourceHeight > 2.5f)
+ rOpts.sourceHeight = m_rs.Height() * 2;
+ }
+
+ // Set position of subtitles based on video calibration settings
+ RESOLUTION_INFO resInfo = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ // Keep track of subtitle position value change,
+ // can be changed by GUI Calibration or by window mode/resolution change or
+ // by user manual change (e.g. keyboard shortcut)
+ if (m_subtitlePosResInfo != resInfo.iSubtitles)
+ {
+ if (m_subtitlePosResInfo == POSRESINFO_SAVE_CHANGES)
+ {
+ // m_subtitlePosition has been changed
+ // and has been requested to save the value to resInfo
+ resInfo.iSubtitles = m_subtitlePosition + m_subtitleVerticalMargin;
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetResInfo(
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(), resInfo);
+ m_subtitlePosResInfo = m_subtitlePosition + m_subtitleVerticalMargin;
+ }
+ else
+ ResetSubtitlePosition();
+ }
+
+ rOpts.m_par = resInfo.fPixelRatio;
+
+ // rOpts.position and margins (set to style) can invalidate the text
+ // positions to subtitles type that make use of margins to position text on
+ // the screen (e.g. ASS/WebVTT) then we allow to set them when position
+ // override setting is enabled only
+ if (o->IsForcedMargins())
+ {
+ rOpts.marginsMode = SUBTITLES::STYLE::MarginsMode::DISABLED;
+ }
+ else if (m_subtitleAlign == SUBTITLES::Align::MANUAL)
+ {
+ // When vertical margins are used Libass apply a displacement in percentage
+ // of the height available to line position, this displacement causes
+ // problems with subtitle calibration bar on Video Calibration window,
+ // so when you moving the subtitle bar of the GUI the text will no longer
+ // match the bar, this calculation compensates for the displacement.
+ // Note also that the displacement compensation will cause a different
+ // default position of the text, different from the other alignment positions
+ double posPx = static_cast<double>(m_subtitlePosition - resInfo.Overscan.top);
+
+ int assPlayResY = o->GetLibassHandler()->GetPlayResY();
+ double assVertMargin = static_cast<double>(overlayStyle->marginVertical) *
+ (static_cast<double>(assPlayResY) / 720);
+ double vertMarginScaled = assVertMargin / assPlayResY * static_cast<double>(rOpts.frameHeight);
+
+ double pos = posPx / (static_cast<double>(rOpts.frameHeight) - vertMarginScaled);
+ rOpts.position = 100 - pos * 100;
+ }
+ else if (m_subtitleAlign == SUBTITLES::Align::BOTTOM_OUTSIDE)
+ {
+ // To keep consistent the position of text as other alignment positions
+ // we avoid apply the displacement compensation
+ double posPx =
+ static_cast<double>(m_subtitlePosition + m_subtitleVerticalMargin - resInfo.Overscan.top);
+ rOpts.position = 100 - posPx / static_cast<double>(rOpts.frameHeight) * 100;
+ }
+ else if (m_subtitleAlign == SUBTITLES::Align::BOTTOM_INSIDE ||
+ m_subtitleAlign == SUBTITLES::Align::TOP_INSIDE)
+ {
+ rOpts.marginsMode = SUBTITLES::STYLE::MarginsMode::INSIDE_VIDEO;
+ }
+
+ // Set the horizontal text alignment (currently used to improve readability on CC subtitles only)
+ // This setting influence style->alignment property
+ if (o->IsTextAlignEnabled())
+ {
+ if (m_subtitleHorizontalAlign == SUBTITLES::HorizontalAlign::LEFT)
+ rOpts.horizontalAlignment = SUBTITLES::STYLE::HorizontalAlign::LEFT;
+ else if (m_subtitleHorizontalAlign == SUBTITLES::HorizontalAlign::RIGHT)
+ rOpts.horizontalAlignment = SUBTITLES::STYLE::HorizontalAlign::RIGHT;
+ else
+ rOpts.horizontalAlignment = SUBTITLES::STYLE::HorizontalAlign::CENTER;
+ }
+
+ // changes: Detect changes from previously rendered images, if > 0 they are changed
+ int changes = 0;
+ ASS_Image* images =
+ o->GetLibassHandler()->RenderImage(pts, rOpts, updateStyle, overlayStyle, &changes);
+
+ // If no images not execute the renderer
+ if (!images)
+ return nullptr;
+
+ if (o->m_textureid)
+ {
+ if (changes == 0)
+ {
+ std::map<unsigned int, COverlay*>::iterator it = m_textureCache.find(o->m_textureid);
+ if (it != m_textureCache.end())
+ return it->second;
+ }
+ }
+
+ COverlay* overlay = NULL;
+#if defined(HAS_GL) || defined(HAS_GLES)
+ overlay = new COverlayGlyphGL(images, rOpts.frameWidth, rOpts.frameHeight);
+#elif defined(HAS_DX)
+ overlay = new COverlayQuadsDX(images, rOpts.frameWidth, rOpts.frameHeight);
+#endif
+
+ m_textureCache[m_textureid] = overlay;
+ o->m_textureid = m_textureid;
+ m_textureid++;
+ return overlay;
+}
+
+COverlay* CRenderer::Convert(CDVDOverlay* o, double pts)
+{
+ COverlay* r = NULL;
+
+ if (o->IsOverlayType(DVDOVERLAY_TYPE_TEXT) || o->IsOverlayType(DVDOVERLAY_TYPE_SSA))
+ {
+ CDVDOverlayLibass* ovAss = static_cast<CDVDOverlayLibass*>(o);
+ if (!ovAss || !ovAss->GetLibassHandler())
+ return nullptr;
+ bool updateStyle = !m_overlayStyle || m_isSettingsChanged;
+ if (updateStyle)
+ {
+ m_isSettingsChanged = false;
+ LoadSettings();
+ CreateSubtitlesStyle();
+ }
+
+ r = ConvertLibass(ovAss, pts, updateStyle, m_overlayStyle);
+
+ if (!r)
+ return nullptr;
+ }
+ else if (o->m_textureid)
+ {
+ std::map<unsigned int, COverlay*>::iterator it = m_textureCache.find(o->m_textureid);
+ if (it != m_textureCache.end())
+ r = it->second;
+ }
+
+ if (r)
+ {
+ return r;
+ }
+
+#if defined(HAS_GL) || defined(HAS_GLES)
+ if (o->IsOverlayType(DVDOVERLAY_TYPE_IMAGE))
+ r = new COverlayTextureGL(static_cast<CDVDOverlayImage*>(o), m_rs);
+ else if (o->IsOverlayType(DVDOVERLAY_TYPE_SPU))
+ r = new COverlayTextureGL(static_cast<CDVDOverlaySpu*>(o));
+#elif defined(HAS_DX)
+ if (o->IsOverlayType(DVDOVERLAY_TYPE_IMAGE))
+ r = new COverlayImageDX(static_cast<CDVDOverlayImage*>(o), m_rs);
+ else if (o->IsOverlayType(DVDOVERLAY_TYPE_SPU))
+ r = new COverlayImageDX(static_cast<CDVDOverlaySpu*>(o));
+#endif
+
+ m_textureCache[m_textureid] = r;
+ o->m_textureid = m_textureid;
+ m_textureid++;
+
+ return r;
+}
+
+void CRenderer::Notify(const Observable& obs, const ObservableMessage msg)
+{
+ switch (msg)
+ {
+ case ObservableMessageSettingsChanged:
+ {
+ m_isSettingsChanged = true;
+ break;
+ }
+ case ObservableMessagePositionChanged:
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_subtitlePosResInfo = POSRESINFO_UNSET;
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void CRenderer::LoadSettings()
+{
+ const auto settings{CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()};
+ m_subtitleHorizontalAlign = settings->GetHorizontalAlignment();
+ m_subtitleAlign = settings->GetAlignment();
+ ResetSubtitlePosition();
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRenderer.h b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRenderer.h
new file mode 100644
index 0000000..54e48c0
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRenderer.h
@@ -0,0 +1,198 @@
+/*
+ * Initial code sponsored by: Voddler Inc (voddler.com)
+ * 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 "BaseRenderer.h"
+#include "cores/VideoPlayer/DVDSubtitles/SubtitlesStyle.h"
+#include "settings/SubtitlesSettings.h"
+#include "threads/CriticalSection.h"
+#include "utils/Observer.h"
+
+#include <atomic>
+#include <map>
+#include <memory>
+#include <vector>
+
+class CDVDOverlay;
+class CDVDOverlayLibass;
+class CDVDOverlayImage;
+class CDVDOverlaySpu;
+class CDVDOverlaySSA;
+class CDVDOverlayText;
+
+namespace OVERLAY {
+
+ struct SRenderState
+ {
+ float x;
+ float y;
+ float width;
+ float height;
+ };
+
+ class COverlay
+ {
+ public:
+ COverlay();
+ virtual ~COverlay();
+
+ virtual void Render(SRenderState& state) = 0;
+
+ enum EType
+ {
+ TYPE_NONE,
+ TYPE_TEXTURE
+ } m_type;
+
+ enum EAlign
+ {
+ ALIGN_SCREEN,
+ ALIGN_SCREEN_AR,
+ ALIGN_VIDEO,
+ ALIGN_SUBTITLE
+ } m_align;
+
+ enum EPosition
+ {
+ POSITION_ABSOLUTE,
+ POSITION_ABSOLUTE_SCREEN,
+ POSITION_RELATIVE
+ } m_pos;
+
+ float m_x{0};
+ float m_y{0};
+ float m_width{1.0f};
+ float m_height{1.0f};
+ float m_source_width{0}; // Video source width resolution used to calculate aspect ratio
+ float m_source_height{0}; // Video source height resolution used to calculate aspect ratio
+
+ protected:
+ /*!
+ * \brief Given the resolution ratio determines if it is a 4/3 resolution
+ * \param resRatio The resolution ratio (the results of width / height)
+ * \return True if the ratio refer to a 4/3 resolution, otherwise false
+ */
+ bool IsSquareResolution(float resRatio) { return resRatio > 1.22f && resRatio < 1.34f; }
+ };
+
+ class CRenderer : public Observer
+ {
+ public:
+ CRenderer();
+ virtual ~CRenderer();
+
+ // Implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override;
+
+ void AddOverlay(CDVDOverlay* o, double pts, int index);
+ virtual void Render(int idx);
+
+ /*!
+ * \brief Release resources
+ */
+ void UnInit();
+
+ void Flush();
+
+ /*!
+ * \brief Reset to default values
+ */
+ void Reset();
+
+ void Release(int idx);
+ bool HasOverlay(int idx);
+ void SetVideoRect(CRect &source, CRect &dest, CRect &view);
+ void SetStereoMode(const std::string &stereomode);
+
+ /*!
+ * \brief Set the subtitle vertical position,
+ * it depends on current screen resolution
+ * \param value The subtitle position in pixels
+ * \param save If true, the value will be saved to resolution info
+ */
+ void SetSubtitleVerticalPosition(const int value, bool save);
+
+ protected:
+ /*!
+ * \brief Reset the subtitle position to default value
+ */
+ void ResetSubtitlePosition();
+
+ /*!
+ * \brief Called when the screen resolution is changed
+ */
+ void OnViewChange();
+
+ struct SElement
+ {
+ SElement()
+ {
+ overlay_dvd = NULL;
+ pts = 0.0;
+ }
+ double pts;
+ CDVDOverlay* overlay_dvd;
+ };
+
+ void Render(COverlay* o);
+ COverlay* Convert(CDVDOverlay* o, double pts);
+ /*!
+ * \brief Convert the overlay to a overlay renderer
+ * \param o The overlay to convert
+ * \param pts The current PTS time
+ * \param subStyle The style to be used, MUST BE SET ONLY at the first call or when user change settings
+ * \return True if success, false if error
+ */
+ COverlay* ConvertLibass(
+ CDVDOverlayLibass* o,
+ double pts,
+ bool updateStyle,
+ const std::shared_ptr<struct KODI::SUBTITLES::STYLE::style>& overlayStyle);
+
+ void CreateSubtitlesStyle();
+
+ void Release(std::vector<SElement>& list);
+ void ReleaseCache();
+ void ReleaseUnused();
+
+ /*!
+ * \brief Load and store settings locally
+ */
+ void LoadSettings();
+
+ enum PositonResInfoState
+ {
+ POSRESINFO_UNSET = -1,
+ POSRESINFO_SAVE_CHANGES = -2,
+ };
+
+ CCriticalSection m_section;
+ std::vector<SElement> m_buffers[NUM_BUFFERS];
+ std::map<unsigned int, COverlay*> m_textureCache;
+ static unsigned int m_textureid;
+ CRect m_rv; // Frame size
+ CRect m_rs; // Source size
+ CRect m_rd; // Video size, may be influenced by video settings (e.g. zoom)
+ std::string m_stereomode;
+ // Current subtitle position
+ int m_subtitlePosition{0};
+ // Current subtitle position from resolution info,
+ // or PositonResInfoState enum values for deferred processing
+ int m_subtitlePosResInfo{POSRESINFO_UNSET};
+ int m_subtitleVerticalMargin{0};
+ bool m_saveSubtitlePosition{false}; // To save subtitle position permanently
+ KODI::SUBTITLES::HorizontalAlign m_subtitleHorizontalAlign{
+ KODI::SUBTITLES::HorizontalAlign::CENTER};
+ KODI::SUBTITLES::Align m_subtitleAlign{KODI::SUBTITLES::Align::BOTTOM_OUTSIDE};
+
+ std::shared_ptr<struct KODI::SUBTITLES::STYLE::style> m_overlayStyle;
+ std::atomic<bool> m_isSettingsChanged{false};
+ };
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererDX.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererDX.cpp
new file mode 100644
index 0000000..7cc3d19
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererDX.cpp
@@ -0,0 +1,353 @@
+/*
+ * 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 "OverlayRendererDX.h"
+
+#include "OverlayRenderer.h"
+#include "OverlayRendererUtil.h"
+#include "application/Application.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayImage.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySSA.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySpu.h"
+#include "guilib/D3DResource.h"
+#include "guilib/GUIShaderDX.h"
+#include "rendering/dx/DeviceResources.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#ifndef ASSERT
+#include <crtdbg.h>
+#define ASSERT(f) _ASSERTE((f))
+#endif
+
+#include <cmath>
+
+using namespace OVERLAY;
+using namespace DirectX;
+
+#define USE_PREMULTIPLIED_ALPHA 1
+#define ALPHA_CHANNEL_OFFSET 3
+
+static bool LoadTexture(int width, int height, int stride
+ , DXGI_FORMAT format
+ , const void* pixels
+ , float* u, float* v
+ , CD3DTexture* texture)
+{
+ if (!texture->Create(width, height, 1, D3D11_USAGE_IMMUTABLE, format, pixels, stride))
+ {
+ CLog::Log(LOGERROR, "{} - failed to allocate texture.", __FUNCTION__);
+ return false;
+ }
+
+ D3D11_TEXTURE2D_DESC desc = {};
+ if (!texture->GetDesc(&desc))
+ {
+ CLog::Log(LOGERROR, "{} - failed to get texture description.", __FUNCTION__);
+ texture->Release();
+ return false;
+ }
+ ASSERT(format == desc.Format || (format == DXGI_FORMAT_R8_UNORM && desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM));
+
+ *u = float(width) / desc.Width;
+ *v = float(height) / desc.Height;
+
+ return true;
+}
+
+COverlayQuadsDX::COverlayQuadsDX(ASS_Image* images, float width, float height)
+{
+ m_width = 1.0;
+ m_height = 1.0;
+ m_align = ALIGN_SCREEN;
+ m_pos = POSITION_RELATIVE;
+ m_x = 0.0f;
+ m_y = 0.0f;
+ m_count = 0;
+
+ SQuads quads;
+ if (!convert_quad(images, quads, static_cast<int>(width)))
+ return;
+
+ float u, v;
+ if (!LoadTexture(quads.size_x, quads.size_y, quads.size_x, DXGI_FORMAT_R8_UNORM,
+ quads.texture.data(), &u, &v, &m_texture))
+ {
+ return;
+ }
+
+ Vertex* vt = new Vertex[6 * quads.quad.size()];
+ Vertex* vt_orig = vt;
+ SQuad* vs = quads.quad.data();
+
+ float scale_u = u / quads.size_x;
+ float scale_v = v / quads.size_y;
+
+ float scale_x = 1.0f / width;
+ float scale_y = 1.0f / height;
+
+ for (size_t i = 0; i < quads.quad.size(); i++)
+ {
+ for (int s = 0; s < 6; s++)
+ {
+ CD3DHelper::XMStoreColor(&vt[s].color, vs->a, vs->r, vs->g, vs->b);
+ vt[s].pos = XMFLOAT3(scale_x, scale_y, 0.0f);
+ vt[s].texCoord = XMFLOAT2(scale_u, scale_v);
+ vt[s].texCoord2 = XMFLOAT2(0.0f, 0.0f);
+ }
+
+ vt[0].pos.x *= vs->x;
+ vt[0].texCoord.x *= vs->u;
+ vt[0].pos.y *= vs->y;
+ vt[0].texCoord.y *= vs->v;
+
+ vt[1].pos.x *= vs->x + vs->w;
+ vt[1].texCoord.x *= vs->u + vs->w;
+ vt[1].pos.y *= vs->y;
+ vt[1].texCoord.y *= vs->v;
+
+ vt[2].pos.x *= vs->x;
+ vt[2].texCoord.x *= vs->u;
+ vt[2].pos.y *= vs->y + vs->h;
+ vt[2].texCoord.y *= vs->v + vs->h;
+
+ vt[3] = vt[1];
+
+ vt[4].pos.x *= vs->x + vs->w;
+ vt[4].texCoord.x *= vs->u + vs->w;
+ vt[4].pos.y *= vs->y + vs->h;
+ vt[4].texCoord.y *= vs->v + vs->h;
+
+ vt[5] = vt[2];
+
+ vs += 1;
+ vt += 6;
+ }
+
+ vt = vt_orig;
+ m_count = static_cast<unsigned int>(quads.quad.size());
+
+ if (!m_vertex.Create(D3D11_BIND_VERTEX_BUFFER, 6 * m_count, sizeof(Vertex), DXGI_FORMAT_UNKNOWN,
+ D3D11_USAGE_IMMUTABLE, vt))
+ {
+ CLog::Log(LOGERROR, "{} - failed to create vertex buffer", __FUNCTION__);
+ m_texture.Release();
+ }
+
+ delete[] vt;
+}
+
+COverlayQuadsDX::~COverlayQuadsDX()
+{
+}
+
+void COverlayQuadsDX::Render(SRenderState &state)
+{
+ if (m_count == 0)
+ return;
+
+ ID3D11Buffer* vertexBuffer = m_vertex.Get();
+ if (vertexBuffer == nullptr)
+ return;
+
+ ID3D11DeviceContext* pContext = DX::DeviceResources::Get()->GetD3DContext();
+ CGUIShaderDX* pGUIShader = DX::Windowing()->GetGUIShader();
+
+ XMMATRIX world, view, proj;
+ pGUIShader->GetWVP(world, view, proj);
+
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_SPLIT_HORIZONTAL
+ || CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_SPLIT_VERTICAL)
+ {
+ CRect rect;
+ DX::Windowing()->GetViewPort(rect);
+ DX::Windowing()->SetCameraPosition(CPoint(rect.Width() * 0.5f, rect.Height() * 0.5f),
+ static_cast<int>(rect.Width()),
+ static_cast<int>(rect.Height()));
+ }
+
+ XMMATRIX trans = XMMatrixTranslation(state.x, state.y, 0.0f);
+ XMMATRIX scale = XMMatrixScaling(state.width, state.height, 1.0f);
+
+ pGUIShader->SetWorld(XMMatrixMultiply(XMMatrixMultiply(world, scale), trans));
+
+ const unsigned stride = sizeof(Vertex);
+ const unsigned offset = 0;
+
+ // Set the vertex buffer to active in the input assembler so it can be rendered.
+ pContext->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);
+ // Set the type of primitive that should be rendered from this vertex buffer, in this case triangles.
+ pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+ DX::Windowing()->SetAlphaBlendEnable(true);
+ pGUIShader->Begin(SHADER_METHOD_RENDER_FONT);
+
+ pGUIShader->SetShaderViews(1, m_texture.GetAddressOfSRV());
+ pGUIShader->Draw(m_count * 6, 0);
+
+ // restoring transformation
+ pGUIShader->SetWVP(world, view, proj);
+ pGUIShader->RestoreBuffers();
+}
+
+COverlayImageDX::~COverlayImageDX()
+{
+}
+
+COverlayImageDX::COverlayImageDX(CDVDOverlayImage* o, CRect& rSource)
+{
+ if (o->palette.empty())
+ {
+ m_pma = false;
+ uint32_t* rgba = reinterpret_cast<uint32_t*>(o->pixels.data());
+ Load(rgba, o->width, o->height, o->linesize);
+ }
+ else
+ {
+ std::vector<uint32_t> rgba(o->width * o->height);
+ m_pma = !!USE_PREMULTIPLIED_ALPHA;
+ convert_rgba(o, m_pma, rgba);
+ Load(rgba.data(), o->width, o->height, o->width * 4);
+ }
+
+ if (o->source_width > 0 && o->source_height > 0)
+ {
+ m_pos = POSITION_RELATIVE;
+ m_x = (0.5f * o->width + o->x) / o->source_width;
+ m_y = (0.5f * o->height + o->y) / o->source_height;
+
+ const float subRatio{static_cast<float>(o->source_width) / o->source_height};
+ const float vidRatio{rSource.Width() / rSource.Height()};
+
+ // We always consider aligning 4/3 subtitles to the video,
+ // for example SD DVB subtitles (4/3) must be stretched on fullhd video
+
+ if (std::fabs(subRatio - vidRatio) < 0.001f || IsSquareResolution(subRatio))
+ {
+ m_align = ALIGN_VIDEO;
+ m_width = static_cast<float>(o->width) / o->source_width;
+ m_height = static_cast<float>(o->height) / o->source_height;
+ }
+ else
+ {
+ // We should have a re-encoded/cropped (removed black bars) video source.
+ // Then we cannot align to video otherwise the subtitles will be deformed
+ // better align to screen by keeping the aspect-ratio.
+ m_align = ALIGN_SCREEN_AR;
+ m_width = static_cast<float>(o->width);
+ m_height = static_cast<float>(o->height);
+ m_source_width = static_cast<float>(o->source_width);
+ m_source_height = static_cast<float>(o->source_height);
+ }
+ }
+ else
+ {
+ m_align = ALIGN_VIDEO;
+ m_pos = POSITION_ABSOLUTE;
+ m_x = static_cast<float>(o->x);
+ m_y = static_cast<float>(o->y);
+ m_width = static_cast<float>(o->width);
+ m_height = static_cast<float>(o->height);
+ }
+}
+
+COverlayImageDX::COverlayImageDX(CDVDOverlaySpu* o)
+{
+ int min_x, max_x, min_y, max_y;
+ std::vector<uint32_t> rgba(o->width * o->height);
+
+ convert_rgba(o, USE_PREMULTIPLIED_ALPHA, min_x, max_x, min_y, max_y, rgba);
+ Load(rgba.data() + min_x + min_y * o->width, max_x - min_x, max_y - min_y, o->width * 4);
+
+ m_align = ALIGN_VIDEO;
+ m_pos = POSITION_ABSOLUTE;
+ m_x = static_cast<float>(min_x + o->x);
+ m_y = static_cast<float>(min_y + o->y);
+ m_width = static_cast<float>(max_x - min_x);
+ m_height = static_cast<float>(max_y - min_y);
+}
+
+void COverlayImageDX::Load(uint32_t* rgba, int width, int height, int stride)
+{
+ float u, v;
+ if(!LoadTexture(width
+ , height
+ , stride
+ , DXGI_FORMAT_B8G8R8A8_UNORM
+ , rgba
+ , &u, &v
+ , &m_texture))
+ return;
+
+ Vertex vt[4];
+
+ vt[0].texCoord = XMFLOAT2(u, 0.0f);
+ vt[0].pos = XMFLOAT3(1.0f, 0.0f, 0.0f);
+
+ vt[1].texCoord = XMFLOAT2(u, v);
+ vt[1].pos = XMFLOAT3(1.0f, 1.0f, 0.0f);
+
+ vt[2].texCoord = XMFLOAT2(0.0f, 0.0f);
+ vt[2].pos = XMFLOAT3(0.0f, 0.0f, 0.0f);
+
+ vt[3].texCoord = XMFLOAT2(0.0f, v);
+ vt[3].pos = XMFLOAT3(0.0f, 1.0f, 0.0f);
+
+ if (!m_vertex.Create(D3D11_BIND_VERTEX_BUFFER, 4, sizeof(Vertex), DXGI_FORMAT_UNKNOWN, D3D11_USAGE_IMMUTABLE, vt))
+ {
+ CLog::Log(LOGERROR, "{} - failed to create vertex buffer", __FUNCTION__);
+ m_texture.Release();
+ }
+}
+
+void COverlayImageDX::Render(SRenderState &state)
+{
+ ID3D11Buffer* vertexBuffer = m_vertex.Get();
+ if (vertexBuffer == nullptr)
+ return;
+
+ ID3D11DeviceContext* pContext = DX::DeviceResources::Get()->GetD3DContext();
+ CGUIShaderDX* pGUIShader = DX::Windowing()->GetGUIShader();
+
+ XMMATRIX world, view, proj;
+ pGUIShader->GetWVP(world, view, proj);
+
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_SPLIT_HORIZONTAL
+ || CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_SPLIT_VERTICAL)
+ {
+ CRect rect;
+ DX::Windowing()->GetViewPort(rect);
+ DX::Windowing()->SetCameraPosition(CPoint(rect.Width() * 0.5f, rect.Height() * 0.5f),
+ static_cast<int>(rect.Width()),
+ static_cast<int>(rect.Height()));
+ }
+
+ XMMATRIX trans = m_pos == POSITION_RELATIVE
+ ? XMMatrixTranslation(state.x - state.width * 0.5f, state.y - state.height * 0.5f, 0.0f)
+ : XMMatrixTranslation(state.x, state.y, 0.0f),
+ scale = XMMatrixScaling(state.width, state.height, 1.0f);
+
+ pGUIShader->SetWorld(XMMatrixMultiply(XMMatrixMultiply(world, scale), trans));
+
+ const unsigned stride = m_vertex.GetStride();
+ const unsigned offset = 0;
+
+ pContext->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);
+ pContext->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
+
+ pGUIShader->Begin(SHADER_METHOD_RENDER_TEXTURE_NOBLEND);
+ DX::Windowing()->SetAlphaBlendEnable(true);
+
+ pGUIShader->SetShaderViews(1, m_texture.GetAddressOfSRV());
+ pGUIShader->Draw(4, 0);
+
+ // restoring transformation
+ pGUIShader->SetWVP(world, view, proj);
+ pGUIShader->RestoreBuffers();
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererDX.h b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererDX.h
new file mode 100644
index 0000000..fa1a96c
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererDX.h
@@ -0,0 +1,56 @@
+/*
+ * 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 "OverlayRenderer.h"
+#include "guilib/D3DResource.h"
+
+class CDVDOverlay;
+class CDVDOverlayImage;
+class CDVDOverlaySpu;
+class CDVDOverlaySSA;
+typedef struct ass_image ASS_Image;
+
+namespace OVERLAY {
+
+ class COverlayQuadsDX
+ : public COverlay
+ {
+ public:
+ COverlayQuadsDX(ASS_Image* images, float width, float height);
+ virtual ~COverlayQuadsDX();
+
+ void Render(SRenderState& state);
+
+ unsigned int m_count;
+ CD3DTexture m_texture;
+ CD3DBuffer m_vertex;
+ };
+
+ class COverlayImageDX
+ : public COverlay
+ {
+ public:
+ /*! \brief Create the overlay for rendering
+ * \param o The overlay image
+ * \param rSource The video source rect size
+ */
+ explicit COverlayImageDX(CDVDOverlayImage* o, CRect& rSource);
+ explicit COverlayImageDX(CDVDOverlaySpu* o);
+ virtual ~COverlayImageDX();
+
+ void Load(uint32_t* rgba, int width, int height, int stride);
+ void Render(SRenderState& state);
+
+ CD3DTexture m_texture;
+ CD3DBuffer m_vertex;
+ bool m_pma{false};
+ };
+
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererGL.cpp
new file mode 100644
index 0000000..12d6fb4
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererGL.cpp
@@ -0,0 +1,619 @@
+/*
+ * Initial code sponsored by: Voddler Inc (voddler.com)
+ * 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 "OverlayRenderer.h"
+#include "OverlayRendererUtil.h"
+#include "OverlayRendererGL.h"
+#ifdef HAS_GL
+#include "LinuxRendererGL.h"
+#include "rendering/gl/RenderSystemGL.h"
+#elif HAS_GLES >= 2
+#include "LinuxRendererGLES.h"
+#include "rendering/gles/RenderSystemGLES.h"
+#endif
+#include "rendering/MatrixGL.h"
+#include "RenderManager.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayImage.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySpu.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySSA.h"
+#include "windowing/WinSystem.h"
+#include "utils/MathUtils.h"
+#include "utils/log.h"
+#include "utils/GLUtils.h"
+
+#include <cmath>
+
+#if HAS_GLES >= 2
+// GLES2.0 cant do CLAMP, but can do CLAMP_TO_EDGE.
+#define GL_CLAMP GL_CLAMP_TO_EDGE
+#endif
+
+#define USE_PREMULTIPLIED_ALPHA 1
+
+using namespace OVERLAY;
+
+static void LoadTexture(GLenum target
+ , GLsizei width, GLsizei height, GLsizei stride
+ , GLfloat* u, GLfloat* v
+ , bool alpha, const GLvoid* pixels)
+{
+ int width2 = width;
+ int height2 = height;
+ char *pixelVector = NULL;
+ const GLvoid *pixelData = pixels;
+
+#ifdef HAS_GLES
+ GLenum internalFormat = alpha ? GL_ALPHA : GL_RGBA;
+ GLenum externalFormat = alpha ? GL_ALPHA : GL_RGBA;
+#else
+ GLenum internalFormat = alpha ? GL_RED : GL_RGBA;
+ GLenum externalFormat = alpha ? GL_RED : GL_BGRA;
+#endif
+
+ int bytesPerPixel = KODI::UTILS::GL::glFormatElementByteCount(externalFormat);
+
+#ifdef HAS_GLES
+ bool bgraSupported = false;
+ CRenderSystemGLES* renderSystem = dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+
+ if (!alpha)
+ {
+ if (renderSystem->IsExtSupported("GL_EXT_texture_format_BGRA8888") ||
+ renderSystem->IsExtSupported("GL_IMG_texture_format_BGRA8888"))
+ {
+ bgraSupported = true;
+ internalFormat = externalFormat = GL_BGRA_EXT;
+ }
+ else if (renderSystem->IsExtSupported("GL_APPLE_texture_format_BGRA8888"))
+ {
+ // Apple's implementation does not conform to spec. Instead, they require
+ // differing format/internalformat, more like GL.
+ bgraSupported = true;
+ externalFormat = GL_BGRA_EXT;
+ }
+ }
+
+ int bytesPerLine = bytesPerPixel * width;
+
+ if (!alpha && !bgraSupported)
+ {
+ pixelVector = (char *)malloc(bytesPerLine * height);
+
+ const char *src = (const char*)pixels;
+ char *dst = pixelVector;
+ for (int y = 0;y < height;++y)
+ {
+ src = (const char*)pixels + y * stride;
+ dst = pixelVector + y * bytesPerLine;
+
+ for (GLsizei i = 0; i < width; i++, src+=4, dst+=4)
+ {
+ dst[0] = src[2];
+ dst[1] = src[1];
+ dst[2] = src[0];
+ dst[3] = src[3];
+ }
+ }
+
+ pixelData = pixelVector;
+ stride = width;
+ }
+ /** OpenGL ES does not support strided texture input. Make a copy without stride **/
+ else if (stride != bytesPerLine)
+ {
+ pixelVector = (char *)malloc(bytesPerLine * height);
+
+ const char *src = (const char*)pixels;
+ char *dst = pixelVector;
+ for (int y = 0;y < height;++y)
+ {
+ memcpy(dst, src, bytesPerLine);
+ src += stride;
+ dst += bytesPerLine;
+ }
+
+ pixelData = pixelVector;
+ stride = bytesPerLine;
+ }
+#else
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, stride / bytesPerPixel);
+#endif
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+
+ glTexImage2D (target, 0, internalFormat
+ , width2, height2, 0
+ , externalFormat, GL_UNSIGNED_BYTE, NULL);
+
+ glTexSubImage2D(target, 0
+ , 0, 0, width, height
+ , externalFormat, GL_UNSIGNED_BYTE
+ , pixelData);
+
+ if(height < height2)
+ glTexSubImage2D( target, 0
+ , 0, height, width, 1
+ , externalFormat, GL_UNSIGNED_BYTE
+ , (const unsigned char*)pixelData + stride * (height-1));
+
+ if(width < width2)
+ glTexSubImage2D( target, 0
+ , width, 0, 1, height
+ , externalFormat, GL_UNSIGNED_BYTE
+ , (const unsigned char*)pixelData + bytesPerPixel * (width-1));
+
+#ifndef HAS_GLES
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+#endif
+
+ free(pixelVector);
+
+ *u = (GLfloat)width / width2;
+ *v = (GLfloat)height / height2;
+}
+
+COverlayTextureGL::COverlayTextureGL(CDVDOverlayImage* o, CRect& rSource)
+{
+ glGenTextures(1, &m_texture);
+ glBindTexture(GL_TEXTURE_2D, m_texture);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ if (o->palette.empty())
+ {
+ m_pma = false;
+ uint32_t* rgba = reinterpret_cast<uint32_t*>(o->pixels.data());
+ LoadTexture(GL_TEXTURE_2D, o->width, o->height, o->linesize, &m_u, &m_v, false, rgba);
+ }
+ else
+ {
+ std::vector<uint32_t> rgba(o->width * o->height);
+ m_pma = !!USE_PREMULTIPLIED_ALPHA;
+ convert_rgba(o, m_pma, rgba);
+ LoadTexture(GL_TEXTURE_2D, o->width, o->height, o->width * 4, &m_u, &m_v, false, rgba.data());
+ }
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ if (o->source_width > 0 && o->source_height > 0)
+ {
+ m_pos = POSITION_RELATIVE;
+ m_x = (0.5f * o->width + o->x) / o->source_width;
+ m_y = (0.5f * o->height + o->y) / o->source_height;
+
+ const float subRatio{static_cast<float>(o->source_width) / o->source_height};
+ const float vidRatio{rSource.Width() / rSource.Height()};
+
+ // We always consider aligning 4/3 subtitles to the video,
+ // for example SD DVB subtitles (4/3) must be stretched on fullhd video
+
+ if (std::fabs(subRatio - vidRatio) < 0.001f || IsSquareResolution(subRatio))
+ {
+ m_align = ALIGN_VIDEO;
+ m_width = static_cast<float>(o->width) / o->source_width;
+ m_height = static_cast<float>(o->height) / o->source_height;
+ }
+ else
+ {
+ // We should have a re-encoded/cropped (removed black bars) video source.
+ // Then we cannot align to video otherwise the subtitles will be deformed
+ // better align to screen by keeping the aspect-ratio.
+ m_align = ALIGN_SCREEN_AR;
+ m_width = static_cast<float>(o->width);
+ m_height = static_cast<float>(o->height);
+ m_source_width = static_cast<float>(o->source_width);
+ m_source_height = static_cast<float>(o->source_height);
+ }
+ }
+ else
+ {
+ m_align = ALIGN_VIDEO;
+ m_pos = POSITION_ABSOLUTE;
+ m_x = static_cast<float>(o->x);
+ m_y = static_cast<float>(o->y);
+ m_width = static_cast<float>(o->width);
+ m_height = static_cast<float>(o->height);
+ }
+}
+
+COverlayTextureGL::COverlayTextureGL(CDVDOverlaySpu* o)
+{
+ int min_x, max_x, min_y, max_y;
+ std::vector<uint32_t> rgba(o->width * o->height);
+
+ convert_rgba(o, USE_PREMULTIPLIED_ALPHA, min_x, max_x, min_y, max_y, rgba);
+
+ glGenTextures(1, &m_texture);
+ glBindTexture(GL_TEXTURE_2D, m_texture);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ LoadTexture(GL_TEXTURE_2D, max_x - min_x, max_y - min_y, o->width * 4, &m_u, &m_v, false,
+ rgba.data() + min_x + min_y * o->width);
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ m_align = ALIGN_VIDEO;
+ m_pos = POSITION_ABSOLUTE;
+ m_x = static_cast<float>(min_x + o->x);
+ m_y = static_cast<float>(min_y + o->y);
+ m_width = static_cast<float>(max_x - min_x);
+ m_height = static_cast<float>(max_y - min_y);
+ m_pma = !!USE_PREMULTIPLIED_ALPHA;
+}
+
+COverlayGlyphGL::COverlayGlyphGL(ASS_Image* images, float width, float height)
+{
+ m_width = 1.0;
+ m_height = 1.0;
+ m_align = ALIGN_SCREEN;
+ m_pos = POSITION_RELATIVE;
+ m_x = 0.0f;
+ m_y = 0.0f;
+
+ SQuads quads;
+ if (!convert_quad(images, quads, static_cast<int>(width)))
+ return;
+
+ glGenTextures(1, &m_texture);
+ glBindTexture(GL_TEXTURE_2D, m_texture);
+
+ LoadTexture(GL_TEXTURE_2D, quads.size_x, quads.size_y, quads.size_x, &m_u, &m_v, true,
+ quads.texture.data());
+
+
+ float scale_u = m_u / quads.size_x;
+ float scale_v = m_v / quads.size_y;
+
+ float scale_x = 1.0f / width;
+ float scale_y = 1.0f / height;
+
+ m_vertex.resize(quads.quad.size() * 4);
+
+ VERTEX* vt = m_vertex.data();
+ SQuad* vs = quads.quad.data();
+
+ for (size_t i = 0; i < quads.quad.size(); i++)
+ {
+ for(int s = 0; s < 4; s++)
+ {
+ vt[s].a = vs->a;
+ vt[s].r = vs->r;
+ vt[s].g = vs->g;
+ vt[s].b = vs->b;
+
+ vt[s].x = scale_x;
+ vt[s].y = scale_y;
+ vt[s].z = 0.0f;
+ vt[s].u = scale_u;
+ vt[s].v = scale_v;
+ }
+
+ vt[0].x *= vs->x;
+ vt[0].u *= vs->u;
+ vt[0].y *= vs->y;
+ vt[0].v *= vs->v;
+
+ vt[1].x *= vs->x;
+ vt[1].u *= vs->u;
+ vt[1].y *= vs->y + vs->h;
+ vt[1].v *= vs->v + vs->h;
+
+ vt[2].x *= vs->x + vs->w;
+ vt[2].u *= vs->u + vs->w;
+ vt[2].y *= vs->y;
+ vt[2].v *= vs->v;
+
+ vt[3].x *= vs->x + vs->w;
+ vt[3].u *= vs->u + vs->w;
+ vt[3].y *= vs->y + vs->h;
+ vt[3].v *= vs->v + vs->h;
+
+ vs += 1;
+ vt += 4;
+ }
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+COverlayGlyphGL::~COverlayGlyphGL()
+{
+ glDeleteTextures(1, &m_texture);
+}
+
+void COverlayGlyphGL::Render(SRenderState& state)
+{
+ if ((m_texture == 0) || (m_vertex.size() == 0))
+ return;
+
+ glEnable(GL_BLEND);
+
+ glBindTexture(GL_TEXTURE_2D, m_texture);
+ glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ glMatrixModview.Push();
+ glMatrixModview->Translatef(state.x, state.y, 0.0f);
+ glMatrixModview->Scalef(state.width, state.height, 1.0f);
+ glMatrixModview.Load();
+
+#ifdef HAS_GL
+ CRenderSystemGL* renderSystem = dynamic_cast<CRenderSystemGL*>(CServiceBroker::GetRenderSystem());
+ renderSystem->EnableShader(ShaderMethodGL::SM_FONTS);
+ GLint posLoc = renderSystem->ShaderGetPos();
+ GLint colLoc = renderSystem->ShaderGetCol();
+ GLint tex0Loc = renderSystem->ShaderGetCoord0();
+
+ std::vector<VERTEX> vecVertices(6 * m_vertex.size() / 4);
+ VERTEX* vertices = vecVertices.data();
+
+ for (size_t i = 0; i < m_vertex.size(); i += 4)
+ {
+ *vertices++ = m_vertex[i];
+ *vertices++ = m_vertex[i+1];
+ *vertices++ = m_vertex[i+2];
+
+ *vertices++ = m_vertex[i+1];
+ *vertices++ = m_vertex[i+3];
+ *vertices++ = m_vertex[i+2];
+ }
+ GLuint VertexVBO;
+
+ glGenBuffers(1, &VertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, VertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(VERTEX) * vecVertices.size(), vecVertices.data(),
+ GL_STATIC_DRAW);
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, GL_FALSE, sizeof(VERTEX),
+ reinterpret_cast<const GLvoid*>(offsetof(VERTEX, x)));
+ glVertexAttribPointer(colLoc, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(VERTEX),
+ reinterpret_cast<const GLvoid*>(offsetof(VERTEX, r)));
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, GL_FALSE, sizeof(VERTEX),
+ reinterpret_cast<const GLvoid*>(offsetof(VERTEX, u)));
+
+ glEnableVertexAttribArray(posLoc);
+ glEnableVertexAttribArray(colLoc);
+ glEnableVertexAttribArray(tex0Loc);
+
+ glDrawArrays(GL_TRIANGLES, 0, vecVertices.size());
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(colLoc);
+ glDisableVertexAttribArray(tex0Loc);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &VertexVBO);
+
+ renderSystem->DisableShader();
+
+#else
+ CRenderSystemGLES* renderSystem = dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_FONTS);
+ GLint posLoc = renderSystem->GUIShaderGetPos();
+ GLint colLoc = renderSystem->GUIShaderGetCol();
+ GLint tex0Loc = renderSystem->GUIShaderGetCoord0();
+
+ // stack object until VBOs will be used
+ std::vector<VERTEX> vecVertices(6 * m_vertex.size() / 4);
+ VERTEX* vertices = vecVertices.data();
+
+ for (size_t i = 0; i < m_vertex.size(); i += 4)
+ {
+ *vertices++ = m_vertex[i];
+ *vertices++ = m_vertex[i+1];
+ *vertices++ = m_vertex[i+2];
+
+ *vertices++ = m_vertex[i+1];
+ *vertices++ = m_vertex[i+3];
+ *vertices++ = m_vertex[i+2];
+ }
+
+ vertices = vecVertices.data();
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, GL_FALSE, sizeof(VERTEX), (char*)vertices + offsetof(VERTEX, x));
+ glVertexAttribPointer(colLoc, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(VERTEX), (char*)vertices + offsetof(VERTEX, r));
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, GL_FALSE, sizeof(VERTEX), (char*)vertices + offsetof(VERTEX, u));
+
+ glEnableVertexAttribArray(posLoc);
+ glEnableVertexAttribArray(colLoc);
+ glEnableVertexAttribArray(tex0Loc);
+
+ glDrawArrays(GL_TRIANGLES, 0, vecVertices.size());
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(colLoc);
+ glDisableVertexAttribArray(tex0Loc);
+
+ renderSystem->DisableGUIShader();
+#endif
+
+ glMatrixModview.PopLoad();
+
+ glDisable(GL_BLEND);
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+
+COverlayTextureGL::~COverlayTextureGL()
+{
+ glDeleteTextures(1, &m_texture);
+}
+
+void COverlayTextureGL::Render(SRenderState& state)
+{
+ glEnable(GL_BLEND);
+
+ glBindTexture(GL_TEXTURE_2D, m_texture);
+ if(m_pma)
+ glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+ else
+ glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ CRect rd;
+ if (m_pos == POSITION_RELATIVE)
+ {
+ float top = state.y - state.height * 0.5f;
+ float bottom = state.y + state.height * 0.5f;
+ float left = state.x - state.width * 0.5f;
+ float right = state.x + state.width * 0.5f;
+
+ rd.SetRect(left, top, right, bottom);
+ }
+ else
+ {
+ float top = state.y;
+ float bottom = state.y + state.height;
+ float left = state.x;
+ float right = state.x + state.width;
+
+ rd.SetRect(left, top, right, bottom);
+ }
+
+#if defined(HAS_GL)
+ CRenderSystemGL* renderSystem = dynamic_cast<CRenderSystemGL*>(CServiceBroker::GetRenderSystem());
+
+ int glslMajor, glslMinor;
+ renderSystem->GetGLSLVersion(glslMajor, glslMinor);
+ if (glslMajor >= 2 || (glslMajor == 1 && glslMinor >= 50))
+ renderSystem->EnableShader(ShaderMethodGL::SM_TEXTURE_LIM);
+ else
+ renderSystem->EnableShader(ShaderMethodGL::SM_TEXTURE);
+
+ GLint posLoc = renderSystem->ShaderGetPos();
+ GLint tex0Loc = renderSystem->ShaderGetCoord0();
+ GLint uniColLoc = renderSystem->ShaderGetUniCol();
+
+ GLfloat col[4] = {1.0f, 1.0f, 1.0f, 1.0f};
+
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ } vertex[4];
+ GLubyte idx[4] = {0, 1, 3, 2}; //determines order of the vertices
+ GLuint vertexVBO;
+ GLuint indexVBO;
+
+ glUniform4f(uniColLoc,(col[0]), (col[1]), (col[2]), (col[3]));
+
+ // Setup vertex position values
+ vertex[0].x = rd.x1;
+ vertex[0].y = rd.y1;
+ vertex[0].z = 0;
+ vertex[0].u1 = 0.0f;
+ vertex[0].v1 = 0.0;
+
+ vertex[1].x = rd.x2;
+ vertex[1].y = rd.y1;
+ vertex[1].z = 0;
+ vertex[1].u1 = m_u;
+ vertex[1].v1 = 0.0f;
+
+ vertex[2].x = rd.x2;
+ vertex[2].y = rd.y2;
+ vertex[2].z = 0;
+ vertex[2].u1 = m_u;
+ vertex[2].v1 = m_v;
+
+ vertex[3].x = rd.x1;
+ vertex[3].y = rd.y2;
+ vertex[3].z = 0;
+ vertex[3].u1 = 0.0f;
+ vertex[3].v1 = m_v;
+
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex)*4, &vertex[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(posLoc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+
+ glEnableVertexAttribArray(posLoc);
+ glEnableVertexAttribArray(tex0Loc);
+
+ glGenBuffers(1, &indexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte)*4, idx, GL_STATIC_DRAW);
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(tex0Loc);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &indexVBO);
+
+ renderSystem->DisableShader();
+
+#else
+ CRenderSystemGLES* renderSystem = dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_TEXTURE);
+ GLint posLoc = renderSystem->GUIShaderGetPos();
+ GLint colLoc = renderSystem->GUIShaderGetCol();
+ GLint tex0Loc = renderSystem->GUIShaderGetCoord0();
+ GLint uniColLoc = renderSystem->GUIShaderGetUniCol();
+
+ GLfloat col[4] = {1.0f, 1.0f, 1.0f, 1.0f};
+ GLfloat ver[4][2];
+ GLfloat tex[4][2];
+ GLubyte idx[4] = {0, 1, 3, 2}; //determines order of triangle strip
+
+ glVertexAttribPointer(posLoc, 2, GL_FLOAT, 0, 0, ver);
+ glVertexAttribPointer(colLoc, 4, GL_FLOAT, 0, 0, col);
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, 0, tex);
+
+ glEnableVertexAttribArray(posLoc);
+ glEnableVertexAttribArray(colLoc);
+ glEnableVertexAttribArray(tex0Loc);
+
+ glUniform4f(uniColLoc,(col[0]), (col[1]), (col[2]), (col[3]));
+ // Setup vertex position values
+ ver[0][0] = ver[3][0] = rd.x1;
+ ver[0][1] = ver[1][1] = rd.y1;
+ ver[1][0] = ver[2][0] = rd.x2;
+ ver[2][1] = ver[3][1] = rd.y2;
+
+ // Setup texture coordinates
+ tex[0][0] = tex[0][1] = tex[1][1] = tex[3][0] = 0.0f;
+ tex[1][0] = tex[2][0] = m_u;
+ tex[2][1] = tex[3][1] = m_v;
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, idx);
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(colLoc);
+ glDisableVertexAttribArray(tex0Loc);
+
+ renderSystem->DisableGUIShader();
+#endif
+
+ glDisable(GL_BLEND);
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererGL.h b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererGL.h
new file mode 100644
index 0000000..cd06e22
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererGL.h
@@ -0,0 +1,66 @@
+/*
+ * Initial code sponsored by: Voddler Inc (voddler.com)
+ * 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 "OverlayRenderer.h"
+
+#include "system_gl.h"
+
+class CDVDOverlay;
+class CDVDOverlayImage;
+class CDVDOverlaySpu;
+class CDVDOverlaySSA;
+typedef struct ass_image ASS_Image;
+
+namespace OVERLAY {
+
+ class COverlayTextureGL : public COverlay
+ {
+ public:
+ /*! \brief Create the overlay for rendering
+ * \param o The overlay image
+ * \param rSource The video source rect size
+ */
+ explicit COverlayTextureGL(CDVDOverlayImage* o, CRect& rSource);
+ explicit COverlayTextureGL(CDVDOverlaySpu* o);
+ ~COverlayTextureGL() override;
+
+ void Render(SRenderState& state) override;
+
+ GLuint m_texture = 0;
+ float m_u;
+ float m_v;
+ bool m_pma; /*< is alpha in texture premultiplied in the values */
+ };
+
+ class COverlayGlyphGL : public COverlay
+ {
+ public:
+ COverlayGlyphGL(ASS_Image* images, float width, float height);
+
+ ~COverlayGlyphGL() override;
+
+ void Render(SRenderState& state) override;
+
+ struct VERTEX
+ {
+ GLfloat u, v;
+ GLubyte r, g, b, a;
+ GLfloat x, y, z;
+ };
+
+ std::vector<VERTEX> m_vertex;
+
+ GLuint m_texture = 0;
+ float m_u;
+ float m_v;
+ };
+
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererUtil.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererUtil.cpp
new file mode 100644
index 0000000..a21800e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererUtil.cpp
@@ -0,0 +1,290 @@
+/*
+ * 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 "OverlayRendererUtil.h"
+
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlayImage.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySSA.h"
+#include "cores/VideoPlayer/DVDCodecs/Overlay/DVDOverlaySpu.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "windowing/GraphicContext.h"
+
+namespace OVERLAY
+{
+
+static uint32_t build_rgba(int a, int r, int g, int b, bool mergealpha)
+{
+ if(mergealpha)
+ return a << PIXEL_ASHIFT
+ | (r * a / 255) << PIXEL_RSHIFT
+ | (g * a / 255) << PIXEL_GSHIFT
+ | (b * a / 255) << PIXEL_BSHIFT;
+ else
+ return a << PIXEL_ASHIFT
+ | r << PIXEL_RSHIFT
+ | g << PIXEL_GSHIFT
+ | b << PIXEL_BSHIFT;
+}
+
+#define clamp(x) (x) > 255.0 ? 255 : ((x) < 0.0 ? 0 : (int)(x + 0.5))
+static uint32_t build_rgba(int yuv[3], int alpha, bool mergealpha)
+{
+ int a = alpha + ( (alpha << 4) & 0xff );
+ double r = 1.164 * (yuv[0] - 16) + 1.596 * (yuv[2] - 128);
+ double g = 1.164 * (yuv[0] - 16) - 0.391 * (yuv[1] - 128) - 0.813 * (yuv[2] - 128);
+ double b = 1.164 * (yuv[0] - 16) + 2.018 * (yuv[1] - 128);
+ return build_rgba(a, clamp(r), clamp(g), clamp(b), mergealpha);
+}
+#undef clamp
+
+void convert_rgba(CDVDOverlayImage* o, bool mergealpha, std::vector<uint32_t>& rgba)
+{
+ uint32_t palette[256] = {};
+ for (size_t i = 0; i < o->palette.size(); i++)
+ palette[i] = build_rgba((o->palette[i] >> PIXEL_ASHIFT) & 0xff
+ , (o->palette[i] >> PIXEL_RSHIFT) & 0xff
+ , (o->palette[i] >> PIXEL_GSHIFT) & 0xff
+ , (o->palette[i] >> PIXEL_BSHIFT) & 0xff
+ , mergealpha);
+
+ for (int row = 0; row < o->height; row++)
+ for (int col = 0; col < o->width; col++)
+ rgba[row * o->width + col] = palette[o->pixels[row * o->linesize + col]];
+}
+
+void convert_rgba(CDVDOverlaySpu* o,
+ bool mergealpha,
+ int& min_x,
+ int& max_x,
+ int& min_y,
+ int& max_y,
+ std::vector<uint32_t>& rgba)
+{
+ uint32_t palette[8];
+ for (int i = 0; i < 4; i++)
+ {
+ palette[i] = build_rgba(o->color[i] , o->alpha[i] , mergealpha);
+ palette[i+4] = build_rgba(o->highlight_color[i], o->highlight_alpha[i], mergealpha);
+ }
+
+ uint32_t color;
+ uint32_t* trg;
+ uint16_t* src;
+
+ int len, idx, draw;
+
+ int btn_x_start = 0
+ , btn_x_end = 0
+ , btn_y_start = 0
+ , btn_y_end = 0;
+
+ if(o->bForced)
+ {
+ btn_x_start = o->crop_i_x_start - o->x;
+ btn_x_end = o->crop_i_x_end - o->x;
+ btn_y_start = o->crop_i_y_start - o->y;
+ btn_y_end = o->crop_i_y_end - o->y;
+ }
+
+ min_x = o->width;
+ max_x = 0;
+ min_y = o->height;
+ max_y = 0;
+
+ trg = rgba.data();
+ src = (uint16_t*)o->result;
+
+ for (int y = 0; y < o->height; y++)
+ {
+ for (int x = 0; x < o->width ; x += len)
+ {
+ /* Get the RLE part, then draw the line */
+ idx = *src & 0x3;
+ len = *src++ >> 2;
+
+ while( len > 0 )
+ {
+ draw = len;
+ color = palette[idx];
+
+ if (y >= btn_y_start && y <= btn_y_end)
+ {
+ if ( x < btn_x_start && x + len >= btn_x_start) // starts outside
+ draw = btn_x_start - x;
+ else if( x >= btn_x_start && x <= btn_x_end) // starts inside
+ {
+ color = palette[idx + 4];
+ draw = btn_x_end - x + 1;
+ }
+ }
+ /* make sure we are not requested to draw to far */
+ /* that part will be taken care of in next pass */
+ if( draw > len )
+ draw = len;
+
+ /* calculate cropping */
+ if(color & 0xff000000)
+ {
+ if(x < min_x)
+ min_x = x;
+ if(y < min_y)
+ min_y = y;
+ if(x + draw > max_x)
+ max_x = x + draw;
+ if(y + 1 > max_y)
+ max_y = y + 1;
+ }
+
+ for(int i = 0; i < draw; i++)
+ trg[x + i] = color;
+
+ len -= draw;
+ x += draw;
+ }
+ }
+ trg += o->width;
+ }
+
+ /* if nothing visible, just output a dummy pixel */
+ if(max_x <= min_x
+ || max_y <= min_y)
+ {
+ max_y = max_x = 1;
+ min_y = min_x = 0;
+ }
+}
+
+bool convert_quad(ASS_Image* images, SQuads& quads, int max_x)
+{
+ ASS_Image* img;
+ int count = 0;
+
+ if (!images)
+ return false;
+
+ // first calculate how many glyph we have and the total x length
+
+ for(img = images; img; img = img->next)
+ {
+ // fully transparent or width or height is 0 -> not displayed
+ if((img->color & 0xff) == 0xff || img->w == 0 || img->h == 0)
+ continue;
+
+ quads.size_x += img->w + 1;
+ count++;
+ }
+
+ if (count == 0)
+ return false;
+
+ if (quads.size_x > max_x)
+ quads.size_x = max_x;
+
+ int curr_x = 0;
+ int curr_y = 0;
+
+ // calculate the y size of the texture
+
+ for(img = images; img; img = img->next)
+ {
+ if((img->color & 0xff) == 0xff || img->w == 0 || img->h == 0)
+ continue;
+
+ // check if we need to split to new line
+ if (curr_x + img->w >= quads.size_x)
+ {
+ quads.size_y += curr_y + 1;
+ curr_x = 0;
+ curr_y = 0;
+ }
+
+ curr_x += img->w + 1;
+
+ if (img->h > curr_y)
+ curr_y = img->h;
+ }
+
+ quads.size_y += curr_y + 1;
+
+ // allocate space for the glyph positions and texturedata
+ quads.quad.resize(count);
+ quads.texture.resize(quads.size_x * quads.size_y);
+
+ SQuad* v = quads.quad.data();
+ uint8_t* data = quads.texture.data();
+
+ int y = 0;
+
+ curr_x = 0;
+ curr_y = 0;
+
+ for (img = images; img; img = img->next)
+ {
+ if ((img->color & 0xff) == 0xff || img->w == 0 || img->h == 0)
+ continue;
+
+ unsigned int color = img->color;
+ unsigned int alpha = (color & 0xff);
+
+ if (curr_x + img->w >= quads.size_x)
+ {
+ curr_y += y + 1;
+ curr_x = 0;
+ y = 0;
+ data = quads.texture.data() + curr_y * quads.size_x;
+ }
+
+ unsigned int r = ((color >> 24) & 0xff);
+ unsigned int g = ((color >> 16) & 0xff);
+ unsigned int b = ((color >> 8 ) & 0xff);
+
+ v->a = 255 - alpha;
+ v->r = r;
+ v->g = g;
+ v->b = b;
+
+ v->u = curr_x;
+ v->v = curr_y;
+
+ v->x = img->dst_x;
+ v->y = img->dst_y;
+
+ v->w = img->w;
+ v->h = img->h;
+
+ v++;
+
+ for (int i = 0; i < img->h; i++)
+ memcpy(data + quads.size_x * i, img->bitmap + img->stride * i, img->w);
+
+ if (img->h > y)
+ y = img->h;
+
+ curr_x += img->w + 1;
+ data += img->w + 1;
+ }
+ return true;
+}
+
+int GetStereoscopicDepth()
+{
+ int depth = 0;
+
+ if(CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() != RENDER_STEREO_MODE_MONO
+ && CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() != RENDER_STEREO_MODE_OFF)
+ {
+ depth = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SUBTITLES_STEREOSCOPICDEPTH);
+ depth *= (CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoView() == RENDER_STEREO_VIEW_LEFT ? 1 : -1);
+ }
+
+ return depth;
+}
+
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererUtil.h b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererUtil.h
new file mode 100644
index 0000000..96db739
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/OverlayRendererUtil.h
@@ -0,0 +1,50 @@
+/*
+ * 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 <stdint.h>
+#include <stdlib.h>
+#include <vector>
+
+class CDVDOverlayImage;
+class CDVDOverlaySpu;
+class CDVDOverlaySSA;
+typedef struct ass_image ASS_Image;
+
+namespace OVERLAY
+{
+
+struct SQuad
+{
+ int u, v;
+ unsigned char r, g, b, a;
+ int x, y;
+ int w, h;
+};
+
+struct SQuads
+{
+ int size_x{0};
+ int size_y{0};
+ std::vector<uint8_t> texture;
+ std::vector<SQuad> quad;
+};
+
+void convert_rgba(CDVDOverlayImage* o, bool mergealpha, std::vector<uint32_t>& rgba);
+void convert_rgba(CDVDOverlaySpu* o,
+ bool mergealpha,
+ int& min_x,
+ int& max_x,
+ int& min_y,
+ int& max_y,
+ std::vector<uint32_t>& rgba);
+bool convert_quad(ASS_Image* images, SQuads& quads, int max_x);
+int GetStereoscopicDepth();
+
+} // namespace OVERLAY
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderCapture.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCapture.cpp
new file mode 100644
index 0000000..461d413
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCapture.cpp
@@ -0,0 +1,24 @@
+/*
+ * 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 "RenderCapture.h"
+
+#include "ServiceBroker.h"
+#include "cores/IPlayer.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+
+bool CRenderCapture::UseOcclusionQuery()
+{
+ if (m_flags & CAPTUREFLAG_IMMEDIATELY)
+ return false;
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoCaptureUseOcclusionQuery == 0)
+ return false;
+ else
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderCapture.h b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCapture.h
new file mode 100644
index 0000000..3f01189
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCapture.h
@@ -0,0 +1,102 @@
+/*
+ * 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 "threads/Event.h"
+
+enum ECAPTURESTATE
+{
+ CAPTURESTATE_WORKING,
+ CAPTURESTATE_NEEDSRENDER,
+ CAPTURESTATE_NEEDSREADOUT,
+ CAPTURESTATE_DONE,
+ CAPTURESTATE_FAILED,
+ CAPTURESTATE_NEEDSDELETE
+};
+
+class CRenderCapture
+{
+ public:
+ CRenderCapture() = default;
+ virtual ~CRenderCapture() = default;
+
+ virtual void BeginRender() = 0;
+ virtual void EndRender() = 0;
+
+ virtual void ReadOut() {}
+ virtual void* GetRenderBuffer() { return m_pixels; }
+
+ /* \brief Called by the rendermanager to set the state, should not be called by anything else */
+ void SetState(ECAPTURESTATE state) { m_state = state; }
+
+ /* \brief Called by the rendermanager to get the state, should not be called by anything else */
+ ECAPTURESTATE GetState() { return m_state;}
+
+ /* \brief Called by the rendermanager to set the userstate, should not be called by anything else */
+ void SetUserState(ECAPTURESTATE state) { m_userState = state; }
+
+ /* \brief Called by the code requesting the capture
+ \return CAPTURESTATE_WORKING when the capture is in progress,
+ CAPTURESTATE_DONE when the capture has succeeded,
+ CAPTURESTATE_FAILED when the capture has failed
+ */
+ ECAPTURESTATE GetUserState() { return m_userState; }
+
+ /* \brief The internal event will be set when the rendermanager has captured and read a videoframe, or when it has failed
+ \return A reference to m_event
+ */
+ CEvent& GetEvent() { return m_event; }
+
+ /* \brief Called by the rendermanager to set the flags, should not be called by anything else */
+ void SetFlags(int flags) { m_flags = flags; }
+
+ /* \brief Called by the rendermanager to get the flags, should not be called by anything else */
+ int GetFlags() { return m_flags; }
+
+ /* \brief Called by the rendermanager to set the width, should not be called by anything else */
+ void SetWidth(unsigned int width) { m_width = width; }
+
+ /* \brief Called by the rendermanager to set the height, should not be called by anything else */
+ void SetHeight(unsigned int height) { m_height = height; }
+
+ /* \brief Called by the code requesting the capture to get the width */
+ unsigned int GetWidth() { return m_width; }
+
+ /* \brief Called by the code requesting the capture to get the height */
+ unsigned int GetHeight() { return m_height; }
+
+ /* \brief Called by the code requesting the capture to get the buffer where the videoframe is stored,
+ the format is BGRA, this buffer is only valid when GetUserState returns CAPTURESTATE_DONE.
+ The size of the buffer is GetWidth() * GetHeight() * 4.
+ */
+ uint8_t* GetPixels() const { return m_pixels; }
+
+ /* \brief Called by the rendermanager to know if the capture is readout async (using dma for example),
+ should not be called by anything else.
+ */
+ bool IsAsync() { return m_asyncSupported; }
+
+ protected:
+ bool UseOcclusionQuery();
+
+ ECAPTURESTATE m_state{CAPTURESTATE_FAILED}; //state for the rendermanager
+ ECAPTURESTATE m_userState{CAPTURESTATE_FAILED}; //state for the thread that wants the capture
+ int m_flags{0};
+ CEvent m_event;
+
+ uint8_t* m_pixels{nullptr};
+ unsigned int m_width{0};
+ unsigned int m_height{0};
+ unsigned int m_bufferSize{0};
+
+ // this is set after the first render
+ bool m_asyncSupported{false};
+ bool m_asyncChecked{false};
+};
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureDX.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureDX.cpp
new file mode 100644
index 0000000..81d2260
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureDX.cpp
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2005-2021 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 "RenderCaptureDX.h"
+
+#include "cores/IPlayer.h"
+#include "rendering/dx/DeviceResources.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/log.h"
+
+extern "C"
+{
+#include <libavutil/mem.h>
+}
+
+CRenderCaptureDX::CRenderCaptureDX() : CRenderCapture()
+{
+ DX::Windowing()->Register(this);
+}
+
+CRenderCaptureDX::~CRenderCaptureDX()
+{
+ CleanupDX();
+ av_freep(&m_pixels);
+ DX::Windowing()->Unregister(this);
+}
+
+void CRenderCaptureDX::BeginRender()
+{
+ Microsoft::WRL::ComPtr<ID3D11DeviceContext> pContext =
+ DX::DeviceResources::Get()->GetD3DContext();
+ Microsoft::WRL::ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ CD3D11_QUERY_DESC queryDesc(D3D11_QUERY_EVENT);
+
+ if (!m_asyncChecked)
+ {
+ m_asyncSupported = SUCCEEDED(pDevice->CreateQuery(&queryDesc, nullptr));
+ if (m_flags & CAPTUREFLAG_CONTINUOUS)
+ {
+ if (!m_asyncSupported)
+ CLog::Log(LOGWARNING, "{}: D3D11_QUERY_OCCLUSION not supported, performance might suffer.",
+ __FUNCTION__);
+ if (!UseOcclusionQuery())
+ CLog::Log(LOGWARNING, "{}: D3D11_QUERY_OCCLUSION disabled, performance might suffer.",
+ __FUNCTION__);
+ }
+ m_asyncChecked = true;
+ }
+
+ HRESULT result;
+
+ if (m_surfaceWidth != m_width || m_surfaceHeight != m_height)
+ {
+ m_renderTex.Release();
+ m_copyTex.Release();
+
+ if (!m_renderTex.Create(m_width, m_height, 1, D3D11_USAGE_DEFAULT, DXGI_FORMAT_B8G8R8A8_UNORM))
+ {
+ CLog::LogF(LOGERROR, "CreateTexture2D (RENDER_TARGET) failed.");
+ SetState(CAPTURESTATE_FAILED);
+ return;
+ }
+
+ if (!m_copyTex.Create(m_width, m_height, 1, D3D11_USAGE_STAGING, DXGI_FORMAT_B8G8R8A8_UNORM))
+ {
+ CLog::LogF(LOGERROR, "CreateRenderTargetView failed.");
+ SetState(CAPTURESTATE_FAILED);
+ return;
+ }
+
+ m_surfaceWidth = m_width;
+ m_surfaceHeight = m_height;
+ }
+
+ if (m_bufferSize != m_width * m_height * 4)
+ {
+ m_bufferSize = m_width * m_height * 4;
+ av_freep(&m_pixels);
+ m_pixels = (uint8_t*)av_malloc(m_bufferSize);
+ }
+
+ if (m_asyncSupported && UseOcclusionQuery())
+ {
+ //generate an occlusion query if we don't have one
+ if (!m_query)
+ {
+ result = pDevice->CreateQuery(&queryDesc, m_query.ReleaseAndGetAddressOf());
+ if (FAILED(result))
+ {
+ CLog::LogF(LOGERROR, "CreateQuery failed {}", DX::GetErrorDescription(result));
+ m_asyncSupported = false;
+ m_query = nullptr;
+ }
+ }
+ }
+ else
+ {
+ //don't use an occlusion query, clean up any old one
+ m_query = nullptr;
+ }
+}
+
+void CRenderCaptureDX::EndRender()
+{
+ // send commands to the GPU queue
+ auto deviceResources = DX::DeviceResources::Get();
+ deviceResources->FinishCommandList();
+ Microsoft::WRL::ComPtr<ID3D11DeviceContext> pContext = deviceResources->GetImmediateContext();
+
+ pContext->CopyResource(m_copyTex.Get(), m_renderTex.Get());
+
+ if (m_query)
+ {
+ pContext->End(m_query.Get());
+ }
+
+ if (m_flags & CAPTUREFLAG_IMMEDIATELY)
+ SurfaceToBuffer();
+ else
+ SetState(CAPTURESTATE_NEEDSREADOUT);
+}
+
+void CRenderCaptureDX::ReadOut()
+{
+ if (m_query)
+ {
+ //if the result of the occlusion query is available, the data is probably also written into m_copySurface
+ HRESULT result =
+ DX::DeviceResources::Get()->GetImmediateContext()->GetData(m_query.Get(), nullptr, 0, 0);
+ if (SUCCEEDED(result))
+ {
+ if (S_OK == result)
+ SurfaceToBuffer();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{}: GetData failed.", __FUNCTION__);
+ SurfaceToBuffer();
+ }
+ }
+ else
+ {
+ SurfaceToBuffer();
+ }
+}
+
+void CRenderCaptureDX::SurfaceToBuffer()
+{
+ D3D11_MAPPED_SUBRESOURCE lockedRect;
+ if (m_copyTex.LockRect(0, &lockedRect, D3D11_MAP_READ))
+ {
+ //if pitch is same, do a direct copy, otherwise copy one line at a time
+ if (lockedRect.RowPitch == m_width * 4)
+ {
+ memcpy(m_pixels, lockedRect.pData, m_width * m_height * 4);
+ }
+ else
+ {
+ for (unsigned int y = 0; y < m_height; y++)
+ memcpy(m_pixels + y * m_width * 4, (uint8_t*)lockedRect.pData + y * lockedRect.RowPitch,
+ m_width * 4);
+ }
+ m_copyTex.UnlockRect(0);
+ SetState(CAPTURESTATE_DONE);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{}: locking m_copySurface failed.", __FUNCTION__);
+ SetState(CAPTURESTATE_FAILED);
+ }
+}
+
+void CRenderCaptureDX::OnDestroyDevice(bool fatal)
+{
+ CleanupDX();
+ SetState(CAPTURESTATE_FAILED);
+}
+
+void CRenderCaptureDX::CleanupDX()
+{
+ m_renderTex.Release();
+ m_copyTex.Release();
+ m_query = nullptr;
+ m_surfaceWidth = 0;
+ m_surfaceHeight = 0;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureDX.h b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureDX.h
new file mode 100644
index 0000000..ea18f5c
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureDX.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2005-2021 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 "RenderCapture.h"
+#include "guilib/D3DResource.h"
+
+#include <wrl/client.h>
+
+class CRenderCaptureDX : public CRenderCapture, public ID3DResource
+{
+public:
+ CRenderCaptureDX();
+ ~CRenderCaptureDX() override;
+
+ void BeginRender() override;
+ void EndRender() override;
+ void ReadOut() override;
+
+ void OnDestroyDevice(bool fatal) override;
+ void OnCreateDevice() override{};
+ CD3DTexture& GetTarget() { return m_renderTex; }
+
+private:
+ void SurfaceToBuffer();
+ void CleanupDX();
+
+ unsigned int m_surfaceWidth{0};
+ unsigned int m_surfaceHeight{0};
+ Microsoft::WRL::ComPtr<ID3D11Query> m_query{nullptr};
+ CD3DTexture m_renderTex;
+ CD3DTexture m_copyTex;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGL.cpp
new file mode 100644
index 0000000..bbc36ae
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGL.cpp
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2005-2021 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 "RenderCaptureGL.h"
+
+#include "ServiceBroker.h"
+#include "cores/IPlayer.h"
+#include "rendering/RenderSystem.h"
+#include "utils/log.h"
+
+CRenderCaptureGL::~CRenderCaptureGL()
+{
+ if (m_asyncSupported)
+ {
+ if (m_pbo)
+ {
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, m_pbo);
+ glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+ glDeleteBuffers(1, &m_pbo);
+ }
+
+ if (m_query)
+ glDeleteQueries(1, &m_query);
+ }
+
+ delete[] m_pixels;
+}
+
+void CRenderCaptureGL::BeginRender()
+{
+ if (!m_asyncChecked)
+ {
+ unsigned int major, minor, glversion;
+ CServiceBroker::GetRenderSystem()->GetRenderVersion(major, minor);
+ glversion = 10 * major + minor;
+ if (glversion >= 21)
+ {
+ m_asyncSupported = true;
+ m_occlusionQuerySupported = true;
+ }
+ else if (glversion > 14)
+ {
+ m_occlusionQuerySupported = true;
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "CRenderCaptureGL: Occlusion_query not supported, upgrade your GL "
+ "drivers to support at least GL 2.1");
+ }
+ if (m_flags & CAPTUREFLAG_CONTINUOUS)
+ {
+ if (!m_occlusionQuerySupported)
+ CLog::Log(LOGWARNING,
+ "CRenderCaptureGL: Occlusion_query not supported, performance might suffer");
+ if (!CServiceBroker::GetRenderSystem()->IsExtSupported("GL_ARB_pixel_buffer_object"))
+ CLog::Log(
+ LOGWARNING,
+ "CRenderCaptureGL: GL_ARB_pixel_buffer_object not supported, performance might suffer");
+ if (!UseOcclusionQuery())
+ CLog::Log(LOGWARNING,
+ "CRenderCaptureGL: GL_ARB_occlusion_query disabled, performance might suffer");
+ }
+
+ m_asyncChecked = true;
+ }
+
+ if (m_asyncSupported)
+ {
+ if (!m_pbo)
+ glGenBuffers(1, &m_pbo);
+
+ if (UseOcclusionQuery() && m_occlusionQuerySupported)
+ {
+ //generate an occlusion query if we don't have one
+ if (!m_query)
+ glGenQueries(1, &m_query);
+ }
+ else
+ {
+ //don't use an occlusion query, clean up any old one
+ if (m_query)
+ {
+ glDeleteQueries(1, &m_query);
+ m_query = 0;
+ }
+ }
+
+ //start the occlusion query
+ if (m_query)
+ glBeginQuery(GL_SAMPLES_PASSED, m_query);
+
+ //allocate data on the pbo and pixel buffer
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, m_pbo);
+ if (m_bufferSize != m_width * m_height * 4)
+ {
+ m_bufferSize = m_width * m_height * 4;
+ glBufferData(GL_PIXEL_PACK_BUFFER, m_bufferSize, 0, GL_STREAM_READ);
+ delete[] m_pixels;
+ m_pixels = new uint8_t[m_bufferSize];
+ }
+ }
+ else
+ {
+ if (m_bufferSize != m_width * m_height * 4)
+ {
+ delete[] m_pixels;
+ m_bufferSize = m_width * m_height * 4;
+ m_pixels = new uint8_t[m_bufferSize];
+ }
+ }
+}
+
+void CRenderCaptureGL::EndRender()
+{
+ if (m_asyncSupported)
+ {
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+
+ if (m_query)
+ glEndQuery(GL_SAMPLES_PASSED);
+
+ if (m_flags & CAPTUREFLAG_IMMEDIATELY)
+ PboToBuffer();
+ else
+ SetState(CAPTURESTATE_NEEDSREADOUT);
+ }
+ else
+ {
+ SetState(CAPTURESTATE_DONE);
+ }
+}
+
+void* CRenderCaptureGL::GetRenderBuffer()
+{
+ if (m_asyncSupported)
+ {
+ return nullptr; //offset into the pbo
+ }
+ else
+ {
+ return m_pixels;
+ }
+}
+
+void CRenderCaptureGL::ReadOut()
+{
+ if (m_asyncSupported)
+ {
+ //we don't care about the occlusion query, we just want to know if the result is available
+ //when it is, the write into the pbo is probably done as well,
+ //so it can be mapped and read without a busy wait
+
+ GLuint readout = 1;
+ if (m_query)
+ glGetQueryObjectuiv(m_query, GL_QUERY_RESULT_AVAILABLE, &readout);
+
+ if (readout)
+ PboToBuffer();
+ }
+}
+
+void CRenderCaptureGL::PboToBuffer()
+{
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, m_pbo);
+ GLvoid* pboPtr = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
+
+ if (pboPtr)
+ {
+ memcpy(m_pixels, pboPtr, m_bufferSize);
+ SetState(CAPTURESTATE_DONE);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "CRenderCaptureGL::PboToBuffer: glMapBuffer failed");
+ SetState(CAPTURESTATE_FAILED);
+ }
+
+ glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGL.h b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGL.h
new file mode 100644
index 0000000..feafccb
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGL.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2005-2021 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 "RenderCapture.h"
+
+#include "system_gl.h"
+
+class CRenderCaptureGL : public CRenderCapture
+{
+public:
+ CRenderCaptureGL() = default;
+ ~CRenderCaptureGL() override;
+
+ void BeginRender() override;
+ void EndRender() override;
+ void ReadOut() override;
+
+ void* GetRenderBuffer() override;
+
+private:
+ void PboToBuffer();
+ GLuint m_pbo{0};
+ GLuint m_query{0};
+ bool m_occlusionQuerySupported{false};
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGLES.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGLES.cpp
new file mode 100644
index 0000000..0c1ce14
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGLES.cpp
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2005-2021 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 "RenderCaptureGLES.h"
+
+CRenderCaptureGLES::~CRenderCaptureGLES()
+{
+ delete[] m_pixels;
+}
+
+void CRenderCaptureGLES::BeginRender()
+{
+ if (m_bufferSize != m_width * m_height * 4)
+ {
+ delete[] m_pixels;
+ m_bufferSize = m_width * m_height * 4;
+ m_pixels = new uint8_t[m_bufferSize];
+ }
+}
+
+void CRenderCaptureGLES::EndRender()
+{
+ SetState(CAPTURESTATE_DONE);
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGLES.h b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGLES.h
new file mode 100644
index 0000000..db0e3fc
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderCaptureGLES.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2005-2021 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 "RenderCapture.h"
+
+#include "system_gl.h"
+
+class CRenderCaptureGLES : public CRenderCapture
+{
+public:
+ CRenderCaptureGLES() = default;
+ ~CRenderCaptureGLES() override;
+
+ void BeginRender() override;
+ void EndRender() override;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderFactory.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/RenderFactory.cpp
new file mode 100644
index 0000000..0441697
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderFactory.cpp
@@ -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.
+ */
+
+#include "RenderFactory.h"
+
+#include <mutex>
+
+
+using namespace VIDEOPLAYER;
+
+CCriticalSection renderSection;
+std::map<std::string, VIDEOPLAYER::CreateRenderer> CRendererFactory::m_renderers;
+
+CBaseRenderer* CRendererFactory::CreateRenderer(const std::string& id, CVideoBuffer* buffer)
+{
+ std::unique_lock<CCriticalSection> lock(renderSection);
+
+ auto it = m_renderers.find(id);
+ if (it != m_renderers.end())
+ {
+ return it->second(buffer);
+ }
+
+ return nullptr;
+}
+
+std::vector<std::string> CRendererFactory::GetRenderers()
+{
+ std::unique_lock<CCriticalSection> lock(renderSection);
+
+ std::vector<std::string> ret;
+ ret.reserve(m_renderers.size());
+ for (auto &renderer : m_renderers)
+ {
+ ret.push_back(renderer.first);
+ }
+ return ret;
+}
+
+void CRendererFactory::RegisterRenderer(const std::string& id, ::CreateRenderer createFunc)
+{
+ std::unique_lock<CCriticalSection> lock(renderSection);
+
+ m_renderers[id] = createFunc;
+}
+
+void CRendererFactory::ClearRenderer()
+{
+ std::unique_lock<CCriticalSection> lock(renderSection);
+
+ m_renderers.clear();
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderFactory.h b/xbmc/cores/VideoPlayer/VideoRenderers/RenderFactory.h
new file mode 100644
index 0000000..59b5561
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderFactory.h
@@ -0,0 +1,36 @@
+/*
+ * 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 "BaseRenderer.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+namespace VIDEOPLAYER
+{
+
+typedef CBaseRenderer* (*CreateRenderer)(CVideoBuffer *buffer);
+
+class CRendererFactory
+{
+public:
+ static CBaseRenderer* CreateRenderer(const std::string& id, CVideoBuffer* buffer);
+
+ static void RegisterRenderer(const std::string& id, VIDEOPLAYER::CreateRenderer createFunc);
+ static std::vector<std::string> GetRenderers();
+ static void ClearRenderer();
+
+protected:
+
+ static std::map<std::string, VIDEOPLAYER::CreateRenderer> m_renderers;
+};
+
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderFlags.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/RenderFlags.cpp
new file mode 100644
index 0000000..5a4a2e6
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderFlags.cpp
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "RenderFlags.h"
+
+#include <map>
+#include <string>
+
+unsigned int GetFlagsColorMatrix(unsigned int color_matrix, unsigned width, unsigned height)
+{
+ switch(color_matrix)
+ {
+ case 10: // BT2020_CL
+ case 9: // BT2020_NCL
+ return CONF_FLAGS_YUVCOEF_BT2020;
+ case 7: // SMPTE 240M (1987)
+ return CONF_FLAGS_YUVCOEF_240M;
+ case 6: // SMPTE 170M
+ case 5: // ITU-R BT.470-2
+ case 4: // FCC
+ return CONF_FLAGS_YUVCOEF_BT601;
+ case 1: // ITU-R Rec.709 (1990) -- BT.709
+ return CONF_FLAGS_YUVCOEF_BT709;
+ case 3: // RESERVED
+ case 2: // UNSPECIFIED
+ default:
+ if(width > 1024 || height >= 600)
+ return CONF_FLAGS_YUVCOEF_BT709;
+ else
+ return CONF_FLAGS_YUVCOEF_BT601;
+ break;
+ }
+}
+
+unsigned int GetFlagsChromaPosition(unsigned int chroma_position)
+{
+ switch(chroma_position)
+ {
+ case 1: return CONF_FLAGS_CHROMA_LEFT;
+ case 2: return CONF_FLAGS_CHROMA_CENTER;
+ case 3: return CONF_FLAGS_CHROMA_TOPLEFT;
+ }
+ return 0;
+}
+
+unsigned int GetFlagsColorPrimaries(unsigned int color_primaries)
+{
+ switch(color_primaries)
+ {
+ case 1: return CONF_FLAGS_COLPRI_BT709;
+ case 4: return CONF_FLAGS_COLPRI_BT470M;
+ case 5: return CONF_FLAGS_COLPRI_BT470BG;
+ case 6: return CONF_FLAGS_COLPRI_170M;
+ case 7: return CONF_FLAGS_COLPRI_240M;
+ case 9: return CONF_FLAGS_COLPRI_BT2020;
+ }
+ return 0;
+}
+
+unsigned int GetFlagsStereoMode(const std::string& mode)
+{
+ static std::map<std::string, unsigned int> convert;
+ if(convert.empty())
+ {
+ convert["mono"] = 0u;
+ convert["left_right"] = CONF_FLAGS_STEREO_MODE_SBS | CONF_FLAGS_STEREO_CADANCE_LEFT_RIGHT;
+ convert["bottom_top"] = CONF_FLAGS_STEREO_MODE_TAB | CONF_FLAGS_STEREO_CADANCE_RIGHT_LEFT;
+ convert["top_bottom"] = CONF_FLAGS_STEREO_MODE_TAB | CONF_FLAGS_STEREO_CADANCE_LEFT_RIGHT;
+ convert["checkerboard_rl"] = 0u;
+ convert["checkerboard_lr"] = 0u;
+ convert["row_interleaved_rl"] = 0u;
+ convert["row_interleaved_lr"] = 0u;
+ convert["col_interleaved_rl"] = 0u;
+ convert["col_interleaved_lr"] = 0u;
+ convert["anaglyph_cyan_red"] = 0u;
+ convert["right_left"] = CONF_FLAGS_STEREO_MODE_SBS | CONF_FLAGS_STEREO_CADANCE_RIGHT_LEFT;
+ convert["anaglyph_green_magenta"] = 0u;
+ convert["anaglyph_yellow_blue"] = 0u;
+ convert["block_lr"] = 0u;
+ convert["block_rl"] = 0u;
+ }
+ return convert[mode];
+}
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderFlags.h b/xbmc/cores/VideoPlayer/VideoRenderers/RenderFlags.h
new file mode 100644
index 0000000..54f0524
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderFlags.h
@@ -0,0 +1,68 @@
+/*
+ * 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 <string>
+
+#define RENDER_FLAG_BOT 0x01
+#define RENDER_FLAG_TOP 0x02
+#define RENDER_FLAG_BOTH (RENDER_FLAG_BOT | RENDER_FLAG_TOP)
+#define RENDER_FLAG_FIELDMASK 0x03
+
+#define RENDER_FLAG_FIELD0 0x80
+#define RENDER_FLAG_FIELD1 0x100
+
+// #define RENDER_FLAG_LAST 0x40
+
+#define RENDER_FLAG_NOOSD 0x04 /* don't draw any osd */
+#define RENDER_FLAG_NOOSDALPHA 0x08 /* don't allow alpha when osd is drawn */
+
+/* these two flags will be used if we need to render same image twice (bob deinterlacing) */
+#define RENDER_FLAG_NOLOCK 0x10 /* don't attempt to lock texture before rendering */
+#define RENDER_FLAG_NOUNLOCK 0x20 /* don't unlock texture after rendering */
+
+/* this defines what color translation coefficients */
+#define CONF_FLAGS_YUVCOEF_MASK(a) ((a) & 0x07)
+#define CONF_FLAGS_YUVCOEF_BT709 0x01
+#define CONF_FLAGS_YUVCOEF_BT601 0x02
+#define CONF_FLAGS_YUVCOEF_240M 0x03
+#define CONF_FLAGS_YUVCOEF_EBU 0x04
+#define CONF_FLAGS_YUVCOEF_BT2020 0x05
+
+#define CONF_FLAGS_YUV_FULLRANGE 0x08
+#define CONF_FLAGS_FULLSCREEN 0x10
+
+/* defines color primaries */
+#define CONF_FLAGS_COLPRI_MASK(a) ((a) & 0xe0)
+#define CONF_FLAGS_COLPRI_BT709 0x20 // sRGB, HDTV (ITU-R BT.709)
+#define CONF_FLAGS_COLPRI_BT470M 0x40 // NTSC (1953) (FCC 1953, ITU-R BT.470 System M)
+#define CONF_FLAGS_COLPRI_BT470BG 0x60 // PAL/SECAM (1970) (EBU Tech. 3213, ITU-R BT.470 System B, G)
+#define CONF_FLAGS_COLPRI_170M 0x80 // NTSC (1987) (SMPTE RP 145 "SMPTE C", SMPTE 170M)
+#define CONF_FLAGS_COLPRI_240M 0xa0 // SMPTE-240M
+#define CONF_FLAGS_COLPRI_BT2020 0xc0 // UHDTV (ITU-R BT.2020)
+
+/* defines chroma subsampling sample location */
+#define CONF_FLAGS_CHROMA_MASK(a) ((a) & 0x0300)
+#define CONF_FLAGS_CHROMA_LEFT 0x0100
+#define CONF_FLAGS_CHROMA_CENTER 0x0200
+#define CONF_FLAGS_CHROMA_TOPLEFT 0x0300
+
+/* defines 3d modes */
+#define CONF_FLAGS_STEREO_MODE_MASK(a) ((a) & 0x007000)
+#define CONF_FLAGS_STEREO_MODE_SBS 0x001000
+#define CONF_FLAGS_STEREO_MODE_TAB 0x002000
+
+#define CONF_FLAGS_STEREO_CADENCE(a) ((a) & 0x008000)
+#define CONF_FLAGS_STEREO_CADANCE_LEFT_RIGHT 0x000000
+#define CONF_FLAGS_STEREO_CADANCE_RIGHT_LEFT 0x008000
+
+unsigned int GetFlagsColorMatrix(unsigned int color_matrix, unsigned width, unsigned height);
+unsigned int GetFlagsChromaPosition(unsigned int chroma_position);
+unsigned int GetFlagsColorPrimaries(unsigned int color_primaries);
+unsigned int GetFlagsStereoMode(const std::string& mode);
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderInfo.h b/xbmc/cores/VideoPlayer/VideoRenderers/RenderInfo.h
new file mode 100644
index 0000000..6945ec4
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderInfo.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <cstddef>
+#include <vector>
+#include "cores/IPlayer.h"
+
+extern "C" {
+#include <libavutil/pixfmt.h>
+}
+
+struct CRenderInfo
+{
+ CRenderInfo()
+ {
+ Reset();
+ }
+ void Reset()
+ {
+ optimal_buffer_size = 0;
+ max_buffer_size = 0;
+ opaque_pointer = nullptr;
+ m_deintMethods.clear();
+ formats.clear();
+ }
+ unsigned int optimal_buffer_size;
+ unsigned int max_buffer_size;
+ // Supported pixel formats, can be called before configure
+ std::vector<AVPixelFormat> formats;
+ std::vector<EINTERLACEMETHOD> m_deintMethods;
+ // Can be used for initialising video codec with information from renderer (e.g. a shared image pool)
+ void *opaque_pointer;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderManager.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/RenderManager.cpp
new file mode 100644
index 0000000..ceeded1
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderManager.cpp
@@ -0,0 +1,1309 @@
+/*
+ * 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 "RenderManager.h"
+
+#include "RenderCapture.h"
+#include "RenderFactory.h"
+#include "RenderFlags.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "cores/VideoPlayer/Interface/TimingConstants.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SingleLock.h"
+#include "utils/StringUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+
+/* to use the same as player */
+#include "../VideoPlayer/DVDClock.h"
+#include "../VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+
+using namespace std::chrono_literals;
+
+void CRenderManager::CClockSync::Reset()
+{
+ m_error = 0;
+ m_errCount = 0;
+ m_syncOffset = 0;
+ m_enabled = false;
+}
+
+unsigned int CRenderManager::m_nextCaptureId = 0;
+
+CRenderManager::CRenderManager(CDVDClock &clock, IRenderMsg *player) :
+ m_dvdClock(clock),
+ m_playerPort(player)
+{
+}
+
+CRenderManager::~CRenderManager()
+{
+ delete m_pRenderer;
+}
+
+void CRenderManager::GetVideoRect(CRect& source, CRect& dest, CRect& view) const
+{
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ if (m_pRenderer)
+ m_pRenderer->GetVideoRect(source, dest, view);
+}
+
+float CRenderManager::GetAspectRatio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ if (m_pRenderer)
+ return m_pRenderer->GetAspectRatio();
+ else
+ return 1.0f;
+}
+
+void CRenderManager::SetVideoSettings(const CVideoSettings& settings)
+{
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ if (m_pRenderer)
+ {
+ m_pRenderer->SetVideoSettings(settings);
+ }
+}
+
+bool CRenderManager::Configure(const VideoPicture& picture, float fps, unsigned int orientation, int buffers)
+{
+
+ // check if something has changed
+ {
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+
+ if (!m_bRenderGUI)
+ return true;
+
+ if (m_width == picture.iWidth &&
+ m_height == picture.iHeight &&
+ m_dwidth == picture.iDisplayWidth &&
+ m_dheight == picture.iDisplayHeight &&
+ m_fps == fps &&
+ m_orientation == orientation &&
+ m_stereomode == picture.stereoMode &&
+ m_NumberBuffers == buffers &&
+ m_pRenderer != nullptr &&
+ !m_pRenderer->ConfigChanged(picture))
+ {
+ return true;
+ }
+ }
+
+ CLog::Log(LOGDEBUG,
+ "CRenderManager::Configure - change configuration. {}x{}. display: {}x{}. framerate: "
+ "{:4.2f}.",
+ picture.iWidth, picture.iHeight, picture.iDisplayWidth, picture.iDisplayHeight, fps);
+
+ // make sure any queued frame was fully presented
+ {
+ std::unique_lock<CCriticalSection> lock(m_presentlock);
+ XbmcThreads::EndTime<> endtime(5000ms);
+ m_forceNext = true;
+ while (m_presentstep != PRESENT_IDLE)
+ {
+ if(endtime.IsTimePast())
+ {
+ CLog::Log(LOGWARNING, "CRenderManager::Configure - timeout waiting for state");
+ m_forceNext = false;
+ return false;
+ }
+ m_presentevent.wait(lock, endtime.GetTimeLeft());
+ }
+ m_forceNext = false;
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ m_width = picture.iWidth;
+ m_height = picture.iHeight,
+ m_dwidth = picture.iDisplayWidth;
+ m_dheight = picture.iDisplayHeight;
+ m_fps = fps;
+ m_orientation = orientation;
+ m_stereomode = picture.stereoMode;
+ m_NumberBuffers = buffers;
+ m_renderState = STATE_CONFIGURING;
+ m_stateEvent.Reset();
+ m_clockSync.Reset();
+ m_dvdClock.SetVsyncAdjust(0);
+ m_pConfigPicture.reset(new VideoPicture());
+ m_pConfigPicture->CopyRef(picture);
+
+ std::unique_lock<CCriticalSection> lock2(m_presentlock);
+ m_presentstep = PRESENT_READY;
+ m_presentevent.notifyAll();
+ }
+
+ if (!m_stateEvent.Wait(1000ms))
+ {
+ CLog::Log(LOGWARNING, "CRenderManager::Configure - timeout waiting for configure");
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ return false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ if (m_renderState != STATE_CONFIGURED)
+ {
+ CLog::Log(LOGWARNING, "CRenderManager::Configure - failed to configure");
+ return false;
+ }
+
+ return true;
+}
+
+bool CRenderManager::Configure()
+{
+ // lock all interfaces
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ std::unique_lock<CCriticalSection> lock2(m_presentlock);
+ std::unique_lock<CCriticalSection> lock3(m_datalock);
+
+ if (m_pRenderer)
+ {
+ DeleteRenderer();
+ }
+
+ if (!m_pRenderer)
+ {
+ CreateRenderer();
+ if (!m_pRenderer)
+ return false;
+ }
+
+ m_pRenderer->SetVideoSettings(m_playerPort->GetVideoSettings());
+ bool result = m_pRenderer->Configure(*m_pConfigPicture, m_fps, m_orientation);
+ if (result)
+ {
+ CRenderInfo info = m_pRenderer->GetRenderInfo();
+ int renderbuffers = info.max_buffer_size;
+ m_QueueSize = renderbuffers;
+ if (m_NumberBuffers > 0)
+ m_QueueSize = std::min(m_NumberBuffers, renderbuffers);
+
+ if(m_QueueSize < 2)
+ {
+ m_QueueSize = 2;
+ CLog::Log(LOGWARNING, "CRenderManager::Configure - queue size too small ({}, {}, {})",
+ m_QueueSize, renderbuffers, m_NumberBuffers);
+ }
+
+ m_pRenderer->SetBufferSize(m_QueueSize);
+ m_pRenderer->Update();
+
+ m_playerPort->UpdateRenderInfo(info);
+ m_playerPort->UpdateGuiRender(true);
+ m_playerPort->UpdateVideoRender(!m_pRenderer->IsGuiLayer());
+
+ m_queued.clear();
+ m_discard.clear();
+ m_free.clear();
+ m_presentsource = 0;
+ m_presentsourcePast = -1;
+ for (int i=1; i < m_QueueSize; i++)
+ m_free.push_back(i);
+
+ m_bRenderGUI = true;
+ m_bTriggerUpdateResolution = true;
+ m_presentstep = PRESENT_IDLE;
+ m_presentpts = DVD_NOPTS_VALUE;
+ m_lateframes = -1;
+ m_presentevent.notifyAll();
+ m_renderedOverlay = false;
+ m_renderDebug = false;
+ m_clockSync.Reset();
+ m_dvdClock.SetVsyncAdjust(0);
+ m_overlays.Reset();
+ m_overlays.SetStereoMode(m_stereomode);
+
+ m_renderState = STATE_CONFIGURED;
+
+ CLog::Log(LOGDEBUG, "CRenderManager::Configure - {}", m_QueueSize);
+ }
+ else
+ m_renderState = STATE_UNCONFIGURED;
+
+ m_pConfigPicture.reset();
+
+ m_stateEvent.Set();
+ m_playerPort->VideoParamsChange();
+ return result;
+}
+
+bool CRenderManager::IsConfigured() const
+{
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ if (m_renderState == STATE_CONFIGURED)
+ return true;
+ else
+ return false;
+}
+
+void CRenderManager::ShowVideo(bool enable)
+{
+ m_showVideo = enable;
+ if (!enable)
+ DiscardBuffer();
+}
+
+void CRenderManager::FrameWait(std::chrono::milliseconds duration)
+{
+ XbmcThreads::EndTime<> timeout{duration};
+ std::unique_lock<CCriticalSection> lock(m_presentlock);
+ while(m_presentstep == PRESENT_IDLE && !timeout.IsTimePast())
+ m_presentevent.wait(lock, timeout.GetTimeLeft());
+}
+
+bool CRenderManager::IsPresenting()
+{
+ if (!IsConfigured())
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_presentlock);
+ if (!m_presentTimer.IsTimePast())
+ return true;
+ else
+ return false;
+}
+
+void CRenderManager::FrameMove()
+{
+ bool firstFrame = false;
+ UpdateResolution();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+
+ if (m_renderState == STATE_UNCONFIGURED)
+ return;
+ else if (m_renderState == STATE_CONFIGURING)
+ {
+ lock.unlock();
+ if (!Configure())
+ return;
+
+ firstFrame = true;
+ FrameWait(50ms);
+ }
+
+ CheckEnableClockSync();
+ }
+ {
+ std::unique_lock<CCriticalSection> lock2(m_presentlock);
+
+ if (m_queued.empty())
+ {
+ m_presentstep = PRESENT_IDLE;
+ }
+ else
+ {
+ m_presentTimer.Set(1000ms);
+ }
+
+ if (m_presentstep == PRESENT_READY)
+ PrepareNextRender();
+
+ if (m_presentstep == PRESENT_FLIP)
+ {
+ m_presentstep = PRESENT_FRAME;
+ m_presentevent.notifyAll();
+ }
+
+ // release all previous
+ for (std::deque<int>::iterator it = m_discard.begin(); it != m_discard.end(); )
+ {
+ // renderer may want to keep the frame for postprocessing
+ if (!m_pRenderer->NeedBuffer(*it) || !m_bRenderGUI)
+ {
+ m_pRenderer->ReleaseBuffer(*it);
+ m_overlays.Release(*it);
+ m_free.push_back(*it);
+ it = m_discard.erase(it);
+ }
+ else
+ ++it;
+ }
+
+ m_playerPort->UpdateRenderBuffers(m_queued.size(), m_discard.size(), m_free.size());
+ m_bRenderGUI = true;
+ }
+
+ m_playerPort->UpdateGuiRender(IsGuiLayer() || firstFrame);
+
+ ManageCaptures();
+}
+
+void CRenderManager::PreInit()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ if (m_renderState != STATE_UNCONFIGURED)
+ return;
+ }
+
+ if (!CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ m_initEvent.Reset();
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_RENDERER_PREINIT);
+ if (!m_initEvent.Wait(2000ms))
+ {
+ CLog::Log(LOGERROR, "{} - timed out waiting for renderer to preinit", __FUNCTION__);
+ }
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+
+ if (!m_pRenderer)
+ {
+ CreateRenderer();
+ }
+
+ UpdateLatencyTweak();
+
+ m_QueueSize = 2;
+ m_QueueSkip = 0;
+ m_presentstep = PRESENT_IDLE;
+ m_bRenderGUI = true;
+
+ m_initEvent.Set();
+}
+
+void CRenderManager::UnInit()
+{
+ if (!CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ m_initEvent.Reset();
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_RENDERER_UNINIT);
+ if (!m_initEvent.Wait(2000ms))
+ {
+ CLog::Log(LOGERROR, "{} - timed out waiting for renderer to uninit", __FUNCTION__);
+ }
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+
+ m_overlays.UnInit();
+ m_debugRenderer.Dispose();
+
+ DeleteRenderer();
+
+ m_renderState = STATE_UNCONFIGURED;
+ m_width = 0;
+ m_height = 0;
+ m_bRenderGUI = false;
+ RemoveCaptures();
+
+ m_initEvent.Set();
+}
+
+bool CRenderManager::Flush(bool wait, bool saveBuffers)
+{
+ if (!m_pRenderer)
+ return true;
+
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ CLog::Log(LOGDEBUG, "{} - flushing renderer", __FUNCTION__);
+
+// fix deadlock on Windows only when is enabled 'Sync playback to display'
+#ifndef TARGET_WINDOWS
+ CSingleExit exitlock(CServiceBroker::GetWinSystem()->GetGfxContext());
+#endif
+
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ std::unique_lock<CCriticalSection> lock2(m_presentlock);
+ std::unique_lock<CCriticalSection> lock3(m_datalock);
+
+ if (m_pRenderer)
+ {
+ m_overlays.Flush();
+ m_debugRenderer.Flush();
+
+ if (!m_pRenderer->Flush(saveBuffers))
+ {
+ m_queued.clear();
+ m_discard.clear();
+ m_free.clear();
+ m_presentsource = 0;
+ m_presentsourcePast = -1;
+ m_presentstep = PRESENT_IDLE;
+ for (int i = 1; i < m_QueueSize; i++)
+ m_free.push_back(i);
+ }
+
+ m_flushEvent.Set();
+ }
+ }
+ else
+ {
+ m_flushEvent.Reset();
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_RENDERER_FLUSH);
+ if (wait)
+ {
+ if (!m_flushEvent.Wait(1000ms))
+ {
+ CLog::Log(LOGERROR, "{} - timed out waiting for renderer to flush", __FUNCTION__);
+ return false;
+ }
+ else
+ return true;
+ }
+ }
+ return true;
+}
+
+void CRenderManager::CreateRenderer()
+{
+ if (!m_pRenderer)
+ {
+ CVideoBuffer *buffer = nullptr;
+ if (m_pConfigPicture)
+ buffer = m_pConfigPicture->videoBuffer;
+
+ auto renderers = VIDEOPLAYER::CRendererFactory::GetRenderers();
+ for (auto &id : renderers)
+ {
+ if (id == "default")
+ continue;
+
+ m_pRenderer = VIDEOPLAYER::CRendererFactory::CreateRenderer(id, buffer);
+ if (m_pRenderer)
+ {
+ return;
+ }
+ }
+ m_pRenderer = VIDEOPLAYER::CRendererFactory::CreateRenderer("default", buffer);
+ }
+}
+
+void CRenderManager::DeleteRenderer()
+{
+ if (m_pRenderer)
+ {
+ CLog::Log(LOGDEBUG, "{} - deleting renderer", __FUNCTION__);
+
+ delete m_pRenderer;
+ m_pRenderer = NULL;
+ }
+}
+
+unsigned int CRenderManager::AllocRenderCapture()
+{
+ if (m_pRenderer)
+ {
+ CRenderCapture* capture = m_pRenderer->GetRenderCapture();
+ if (capture)
+ {
+ m_captures[m_nextCaptureId] = capture;
+ return m_nextCaptureId++;
+ }
+ }
+
+ return m_nextCaptureId;
+}
+
+void CRenderManager::ReleaseRenderCapture(unsigned int captureId)
+{
+ std::unique_lock<CCriticalSection> lock(m_captCritSect);
+
+ std::map<unsigned int, CRenderCapture*>::iterator it;
+ it = m_captures.find(captureId);
+
+ if (it != m_captures.end())
+ it->second->SetState(CAPTURESTATE_NEEDSDELETE);
+}
+
+void CRenderManager::StartRenderCapture(unsigned int captureId, unsigned int width, unsigned int height, int flags)
+{
+ std::unique_lock<CCriticalSection> lock(m_captCritSect);
+
+ std::map<unsigned int, CRenderCapture*>::iterator it;
+ it = m_captures.find(captureId);
+ if (it == m_captures.end())
+ {
+ CLog::Log(LOGERROR, "CRenderManager::Capture - unknown capture id: {}", captureId);
+ return;
+ }
+
+ CRenderCapture *capture = it->second;
+
+ capture->SetState(CAPTURESTATE_NEEDSRENDER);
+ capture->SetUserState(CAPTURESTATE_WORKING);
+ capture->SetWidth(width);
+ capture->SetHeight(height);
+ capture->SetFlags(flags);
+ capture->GetEvent().Reset();
+
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ if (flags & CAPTUREFLAG_IMMEDIATELY)
+ {
+ //render capture and read out immediately
+ RenderCapture(capture);
+ capture->SetUserState(capture->GetState());
+ capture->GetEvent().Set();
+ }
+ }
+
+ if (!m_captures.empty())
+ m_hasCaptures = true;
+}
+
+bool CRenderManager::RenderCaptureGetPixels(unsigned int captureId, unsigned int millis, uint8_t *buffer, unsigned int size)
+{
+ std::unique_lock<CCriticalSection> lock(m_captCritSect);
+
+ std::map<unsigned int, CRenderCapture*>::iterator it;
+ it = m_captures.find(captureId);
+ if (it == m_captures.end())
+ return false;
+
+ m_captureWaitCounter++;
+
+ {
+ if (!millis)
+ millis = 1000;
+
+ CSingleExit exitlock(m_captCritSect);
+ if (!it->second->GetEvent().Wait(std::chrono::milliseconds(millis)))
+ {
+ m_captureWaitCounter--;
+ return false;
+ }
+ }
+
+ m_captureWaitCounter--;
+
+ if (it->second->GetUserState() != CAPTURESTATE_DONE)
+ return false;
+
+ unsigned int srcSize = it->second->GetWidth() * it->second->GetHeight() * 4;
+ unsigned int bytes = std::min(srcSize, size);
+
+ memcpy(buffer, it->second->GetPixels(), bytes);
+ return true;
+}
+
+void CRenderManager::ManageCaptures()
+{
+ //no captures, return here so we don't do an unnecessary lock
+ if (!m_hasCaptures)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_captCritSect);
+
+ std::map<unsigned int, CRenderCapture*>::iterator it = m_captures.begin();
+ while (it != m_captures.end())
+ {
+ CRenderCapture* capture = it->second;
+
+ if (capture->GetState() == CAPTURESTATE_NEEDSDELETE)
+ {
+ delete capture;
+ it = m_captures.erase(it);
+ continue;
+ }
+
+ if (capture->GetState() == CAPTURESTATE_NEEDSRENDER)
+ RenderCapture(capture);
+ else if (capture->GetState() == CAPTURESTATE_NEEDSREADOUT)
+ capture->ReadOut();
+
+ if (capture->GetState() == CAPTURESTATE_DONE || capture->GetState() == CAPTURESTATE_FAILED)
+ {
+ //tell the thread that the capture is done or has failed
+ capture->SetUserState(capture->GetState());
+ capture->GetEvent().Set();
+
+ if (capture->GetFlags() & CAPTUREFLAG_CONTINUOUS)
+ {
+ capture->SetState(CAPTURESTATE_NEEDSRENDER);
+
+ //if rendering this capture continuously, and readout is async, render a new capture immediately
+ if (capture->IsAsync() && !(capture->GetFlags() & CAPTUREFLAG_IMMEDIATELY))
+ RenderCapture(capture);
+ }
+ ++it;
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ if (m_captures.empty())
+ m_hasCaptures = false;
+}
+
+void CRenderManager::RenderCapture(CRenderCapture* capture)
+{
+ if (!m_pRenderer || !m_pRenderer->RenderCapture(capture))
+ capture->SetState(CAPTURESTATE_FAILED);
+}
+
+void CRenderManager::RemoveCaptures()
+{
+ std::unique_lock<CCriticalSection> lock(m_captCritSect);
+
+ while (m_captureWaitCounter > 0)
+ {
+ for (auto entry : m_captures)
+ {
+ entry.second->GetEvent().Set();
+ }
+ CSingleExit lockexit(m_captCritSect);
+ KODI::TIME::Sleep(10ms);
+ }
+
+ for (auto entry : m_captures)
+ {
+ delete entry.second;
+ }
+ m_captures.clear();
+}
+
+void CRenderManager::SetViewMode(int iViewMode)
+{
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ if (m_pRenderer)
+ m_pRenderer->SetViewMode(iViewMode);
+ m_playerPort->VideoParamsChange();
+}
+
+RESOLUTION CRenderManager::GetResolution()
+{
+ RESOLUTION res = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution();
+
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ if (m_renderState == STATE_UNCONFIGURED)
+ return res;
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE) != ADJUST_REFRESHRATE_OFF)
+ res = CResolutionUtils::ChooseBestResolution(m_fps, m_width, m_height, !m_stereomode.empty());
+
+ return res;
+}
+
+void CRenderManager::Render(bool clear, DWORD flags, DWORD alpha, bool gui)
+{
+ CSingleExit exitLock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ if (m_renderState != STATE_CONFIGURED)
+ return;
+ }
+
+ if (!gui && m_pRenderer->IsGuiLayer())
+ return;
+
+ if (!gui || m_pRenderer->IsGuiLayer())
+ {
+ SPresent& m = m_Queue[m_presentsource];
+
+ if( m.presentmethod == PRESENT_METHOD_BOB )
+ PresentFields(clear, flags, alpha);
+ else if( m.presentmethod == PRESENT_METHOD_BLEND )
+ PresentBlend(clear, flags, alpha);
+ else
+ PresentSingle(clear, flags, alpha);
+ }
+
+ if (gui)
+ {
+ if (!m_pRenderer->IsGuiLayer())
+ m_pRenderer->Update();
+
+ m_renderedOverlay = m_overlays.HasOverlay(m_presentsource);
+ CRect src, dst, view;
+ m_pRenderer->GetVideoRect(src, dst, view);
+ m_overlays.SetVideoRect(src, dst, view);
+ m_overlays.Render(m_presentsource);
+
+ if (m_renderDebug)
+ {
+ if (m_renderDebugVideo)
+ {
+ DEBUG_INFO_VIDEO video = m_pRenderer->GetDebugInfo(m_presentsource);
+ DEBUG_INFO_RENDER render = CServiceBroker::GetWinSystem()->GetDebugInfo();
+
+ m_debugRenderer.SetInfo(video, render);
+ }
+ else
+ {
+ DEBUG_INFO_PLAYER info;
+
+ m_playerPort->GetDebugInfo(info.audio, info.video, info.player);
+
+ double refreshrate, clockspeed;
+ int missedvblanks;
+ info.vsync = StringUtils::Format("VSyncOff: {:.1f} latency: {:.3f} ",
+ m_clockSync.m_syncOffset / 1000,
+ DVD_TIME_TO_MSEC(m_displayLatency) / 1000.0f);
+ if (m_dvdClock.GetClockInfo(missedvblanks, clockspeed, refreshrate))
+ {
+ info.vsync += StringUtils::Format("VSync: refresh:{:.3f} missed:{} speed:{:.3f}%",
+ refreshrate, missedvblanks, clockspeed * 100);
+ }
+
+ m_debugRenderer.SetInfo(info);
+ }
+
+ m_debugRenderer.Render(src, dst, view);
+
+ m_debugTimer.Set(1000ms);
+ m_renderedOverlay = true;
+ }
+ }
+
+ const SPresent& m = m_Queue[m_presentsource];
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_presentlock);
+
+ if (m_presentstep == PRESENT_FRAME)
+ {
+ if (m.presentmethod == PRESENT_METHOD_BOB)
+ m_presentstep = PRESENT_FRAME2;
+ else
+ m_presentstep = PRESENT_IDLE;
+ }
+ else if (m_presentstep == PRESENT_FRAME2)
+ m_presentstep = PRESENT_IDLE;
+
+ if (m_presentstep == PRESENT_IDLE)
+ {
+ if (!m_queued.empty())
+ m_presentstep = PRESENT_READY;
+ }
+
+ m_presentevent.notifyAll();
+ }
+}
+
+bool CRenderManager::IsGuiLayer()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+
+ if (!m_pRenderer)
+ return false;
+
+ if ((m_pRenderer->IsGuiLayer() && IsPresenting()) ||
+ m_renderedOverlay || m_overlays.HasOverlay(m_presentsource))
+ return true;
+
+ if (m_renderDebug && m_debugTimer.IsTimePast())
+ return true;
+ }
+ return false;
+}
+
+bool CRenderManager::IsVideoLayer()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+
+ if (!m_pRenderer)
+ return false;
+
+ if (!m_pRenderer->IsGuiLayer())
+ return true;
+ }
+ return false;
+}
+
+/* simple present method */
+void CRenderManager::PresentSingle(bool clear, DWORD flags, DWORD alpha)
+{
+ const SPresent& m = m_Queue[m_presentsource];
+
+ if (m.presentfield == FS_BOT)
+ m_pRenderer->RenderUpdate(m_presentsource, m_presentsourcePast, clear, flags | RENDER_FLAG_BOT, alpha);
+ else if (m.presentfield == FS_TOP)
+ m_pRenderer->RenderUpdate(m_presentsource, m_presentsourcePast, clear, flags | RENDER_FLAG_TOP, alpha);
+ else
+ m_pRenderer->RenderUpdate(m_presentsource, m_presentsourcePast, clear, flags, alpha);
+}
+
+/* new simpler method of handling interlaced material, *
+ * we just render the two fields right after eachother */
+void CRenderManager::PresentFields(bool clear, DWORD flags, DWORD alpha)
+{
+ const SPresent& m = m_Queue[m_presentsource];
+
+ if(m_presentstep == PRESENT_FRAME)
+ {
+ if( m.presentfield == FS_BOT)
+ m_pRenderer->RenderUpdate(m_presentsource, m_presentsourcePast, clear, flags | RENDER_FLAG_BOT | RENDER_FLAG_FIELD0, alpha);
+ else
+ m_pRenderer->RenderUpdate(m_presentsource, m_presentsourcePast, clear, flags | RENDER_FLAG_TOP | RENDER_FLAG_FIELD0, alpha);
+ }
+ else
+ {
+ if( m.presentfield == FS_TOP)
+ m_pRenderer->RenderUpdate(m_presentsource, m_presentsourcePast, clear, flags | RENDER_FLAG_BOT | RENDER_FLAG_FIELD1, alpha);
+ else
+ m_pRenderer->RenderUpdate(m_presentsource, m_presentsourcePast, clear, flags | RENDER_FLAG_TOP | RENDER_FLAG_FIELD1, alpha);
+ }
+}
+
+void CRenderManager::PresentBlend(bool clear, DWORD flags, DWORD alpha)
+{
+ const SPresent& m = m_Queue[m_presentsource];
+
+ if( m.presentfield == FS_BOT )
+ {
+ m_pRenderer->RenderUpdate(m_presentsource, m_presentsourcePast, clear, flags | RENDER_FLAG_BOT | RENDER_FLAG_NOOSD, alpha);
+ m_pRenderer->RenderUpdate(m_presentsource, m_presentsourcePast, false, flags | RENDER_FLAG_TOP, alpha / 2);
+ }
+ else
+ {
+ m_pRenderer->RenderUpdate(m_presentsource, m_presentsourcePast, clear, flags | RENDER_FLAG_TOP | RENDER_FLAG_NOOSD, alpha);
+ m_pRenderer->RenderUpdate(m_presentsource, m_presentsourcePast, false, flags | RENDER_FLAG_BOT, alpha / 2);
+ }
+}
+
+void CRenderManager::UpdateLatencyTweak()
+{
+ float fps = CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS();
+ float refresh = fps;
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution() == RES_WINDOW)
+ refresh = 0; // No idea about refresh rate when windowed, just get the default latency
+ m_latencyTweak = static_cast<double>(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->GetLatencyTweak(refresh));
+}
+
+void CRenderManager::UpdateResolution()
+{
+ if (m_bTriggerUpdateResolution)
+ {
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenVideo() && CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot())
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE) != ADJUST_REFRESHRATE_OFF && m_fps > 0.0f)
+ {
+ RESOLUTION res = CResolutionUtils::ChooseBestResolution(m_fps, m_width, m_height, !m_stereomode.empty());
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(res, false);
+ UpdateLatencyTweak();
+ if (m_pRenderer)
+ m_pRenderer->Update();
+ }
+ m_bTriggerUpdateResolution = false;
+ m_playerPort->VideoParamsChange();
+ }
+ }
+}
+
+void CRenderManager::TriggerUpdateResolution(float fps, int width, int height, std::string &stereomode)
+{
+ if (width)
+ {
+ m_fps = fps;
+ m_width = width;
+ m_height = height;
+ m_stereomode = stereomode;
+ }
+ m_bTriggerUpdateResolution = true;
+}
+
+void CRenderManager::ToggleDebug()
+{
+ bool isEnabled = !m_renderDebug;
+ if (isEnabled)
+ m_debugRenderer.Initialize();
+ else
+ m_debugRenderer.Dispose();
+
+ m_renderDebug = isEnabled;
+ m_debugTimer.SetExpired();
+ m_renderDebugVideo = false;
+}
+
+void CRenderManager::ToggleDebugVideo()
+{
+ bool isEnabled = !m_renderDebug;
+ if (isEnabled)
+ m_debugRenderer.Initialize();
+ else
+ m_debugRenderer.Dispose();
+
+ m_renderDebug = isEnabled;
+ m_debugTimer.SetExpired();
+ m_renderDebugVideo = true;
+}
+
+void CRenderManager::SetSubtitleVerticalPosition(int value, bool save)
+{
+ m_overlays.SetSubtitleVerticalPosition(value, save);
+}
+
+bool CRenderManager::AddVideoPicture(const VideoPicture& picture, volatile std::atomic_bool& bStop, EINTERLACEMETHOD deintMethod, bool wait)
+{
+ std::unique_lock<CCriticalSection> lock(m_presentlock);
+
+ if (m_free.empty())
+ return false;
+
+ int index = m_free.front();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_datalock);
+ if (!m_pRenderer)
+ return false;
+
+ m_pRenderer->AddVideoPicture(picture, index);
+ }
+
+
+ // set fieldsync if picture is interlaced
+ EFIELDSYNC displayField = FS_NONE;
+ if (picture.iFlags & DVP_FLAG_INTERLACED)
+ {
+ if (deintMethod != EINTERLACEMETHOD::VS_INTERLACEMETHOD_NONE)
+ {
+ if (picture.iFlags & DVP_FLAG_TOP_FIELD_FIRST)
+ displayField = FS_TOP;
+ else
+ displayField = FS_BOT;
+ }
+ }
+
+ EPRESENTMETHOD presentmethod = PRESENT_METHOD_SINGLE;
+ if (deintMethod == VS_INTERLACEMETHOD_NONE)
+ {
+ presentmethod = PRESENT_METHOD_SINGLE;
+ displayField = FS_NONE;
+ }
+ else
+ {
+ if (displayField == FS_NONE)
+ presentmethod = PRESENT_METHOD_SINGLE;
+ else
+ {
+ if (deintMethod == VS_INTERLACEMETHOD_RENDER_BLEND)
+ presentmethod = PRESENT_METHOD_BLEND;
+ else if (deintMethod == VS_INTERLACEMETHOD_RENDER_BOB)
+ presentmethod = PRESENT_METHOD_BOB;
+ else
+ {
+ if (!m_pRenderer->WantsDoublePass())
+ presentmethod = PRESENT_METHOD_SINGLE;
+ else
+ presentmethod = PRESENT_METHOD_BOB;
+ }
+ }
+ }
+
+
+ SPresent& m = m_Queue[index];
+ m.presentfield = displayField;
+ m.presentmethod = presentmethod;
+ m.pts = picture.pts;
+ m_queued.push_back(m_free.front());
+ m_free.pop_front();
+ m_playerPort->UpdateRenderBuffers(m_queued.size(), m_discard.size(), m_free.size());
+
+ // signal to any waiters to check state
+ if (m_presentstep == PRESENT_IDLE)
+ {
+ m_presentstep = PRESENT_READY;
+ m_presentevent.notifyAll();
+ }
+
+ if (wait)
+ {
+ m_forceNext = true;
+ XbmcThreads::EndTime<> endtime(200ms);
+ while (m_presentstep == PRESENT_READY)
+ {
+ m_presentevent.wait(lock, 20ms);
+ if(endtime.IsTimePast() || bStop)
+ {
+ if (!bStop)
+ {
+ CLog::Log(LOGWARNING, "CRenderManager::AddVideoPicture - timeout waiting for render");
+ }
+ break;
+ }
+ }
+ m_forceNext = false;
+ }
+
+ return true;
+}
+
+void CRenderManager::AddOverlay(CDVDOverlay* o, double pts)
+{
+ int idx;
+ {
+ std::unique_lock<CCriticalSection> lock(m_presentlock);
+ if (m_free.empty())
+ return;
+ idx = m_free.front();
+ }
+ std::unique_lock<CCriticalSection> lock(m_datalock);
+ m_overlays.AddOverlay(o, pts, idx);
+}
+
+bool CRenderManager::Supports(ERENDERFEATURE feature) const
+{
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ if (m_pRenderer)
+ return m_pRenderer->Supports(feature);
+ else
+ return false;
+}
+
+bool CRenderManager::Supports(ESCALINGMETHOD method) const
+{
+ std::unique_lock<CCriticalSection> lock(m_statelock);
+ if (m_pRenderer)
+ return m_pRenderer->Supports(method);
+ else
+ return false;
+}
+
+int CRenderManager::WaitForBuffer(volatile std::atomic_bool& bStop,
+ std::chrono::milliseconds timeout)
+{
+ std::unique_lock<CCriticalSection> lock(m_presentlock);
+
+ // check if gui is active and discard buffer if not
+ // this keeps videoplayer going
+ if (!m_bRenderGUI || !g_application.GetRenderGUI())
+ {
+ m_bRenderGUI = false;
+ double presenttime = 0;
+ double clock = m_dvdClock.GetClock();
+ if (!m_queued.empty())
+ {
+ int idx = m_queued.front();
+ presenttime = m_Queue[idx].pts;
+ }
+ else
+ presenttime = clock + 0.02;
+
+ auto sleeptime = std::chrono::milliseconds(static_cast<int>((presenttime - clock) * 1000));
+ if (sleeptime < 0ms)
+ sleeptime = 0ms;
+ sleeptime = std::min(sleeptime, 20ms);
+ m_presentevent.wait(lock, sleeptime);
+ DiscardBuffer();
+ return 0;
+ }
+
+ XbmcThreads::EndTime<> endtime{timeout};
+ while(m_free.empty())
+ {
+ m_presentevent.wait(lock, std::min(50ms, timeout));
+ if (endtime.IsTimePast() || bStop)
+ {
+ return -1;
+ }
+ }
+
+ // make sure overlay buffer is released, this won't happen on AddOverlay
+ m_overlays.Release(m_free.front());
+
+ // return buffer level
+ return m_queued.size() + m_discard.size();
+}
+
+void CRenderManager::PrepareNextRender()
+{
+ if (m_queued.empty())
+ {
+ CLog::Log(LOGERROR, "CRenderManager::PrepareNextRender - asked to prepare with nothing available");
+ m_presentstep = PRESENT_IDLE;
+ m_presentevent.notifyAll();
+ return;
+ }
+
+ if (!m_showVideo && !m_forceNext)
+ return;
+
+ double frameOnScreen = m_dvdClock.GetClock();
+ double frametime = 1.0 /
+ static_cast<double>(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS()) *
+ DVD_TIME_BASE;
+
+ m_displayLatency = DVD_MSEC_TO_TIME(
+ m_latencyTweak +
+ static_cast<double>(CServiceBroker::GetWinSystem()->GetGfxContext().GetDisplayLatency()) -
+ m_videoDelay -
+ static_cast<double>(CServiceBroker::GetWinSystem()->GetFrameLatencyAdjustment()));
+
+ double renderPts = frameOnScreen + m_displayLatency;
+
+ double nextFramePts = m_Queue[m_queued.front()].pts;
+ if (m_dvdClock.GetClockSpeed() < 0)
+ nextFramePts = renderPts;
+
+ if (m_clockSync.m_enabled)
+ {
+ double err = fmod(renderPts - nextFramePts, frametime);
+ m_clockSync.m_error += err;
+ m_clockSync.m_errCount ++;
+ if (m_clockSync.m_errCount > 30)
+ {
+ double average = m_clockSync.m_error / m_clockSync.m_errCount;
+ m_clockSync.m_syncOffset = average;
+ m_clockSync.m_error = 0;
+ m_clockSync.m_errCount = 0;
+
+ m_dvdClock.SetVsyncAdjust(-average);
+ }
+ renderPts += frametime / 2 - m_clockSync.m_syncOffset;
+ }
+ else
+ {
+ m_dvdClock.SetVsyncAdjust(0);
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGAVTIMING,
+ "frameOnScreen: {:f} renderPts: {:f} nextFramePts: {:f} -> diff: {:f} render: {} "
+ "forceNext: {}",
+ frameOnScreen, renderPts, nextFramePts, (renderPts - nextFramePts),
+ renderPts >= nextFramePts, m_forceNext);
+
+ bool combined = false;
+ if (m_presentsourcePast >= 0)
+ {
+ m_discard.push_back(m_presentsourcePast);
+ m_presentsourcePast = -1;
+ combined = true;
+ }
+
+ if (renderPts >= nextFramePts || m_forceNext)
+ {
+ // see if any future queued frames are already due
+ auto iter = m_queued.begin();
+ int idx = *iter;
+ ++iter;
+ while (iter != m_queued.end())
+ {
+ // the slot for rendering in time is [pts .. (pts + x * frametime)]
+ // renderer/drivers have internal queues, being slightly late here does not mean that
+ // we are really late. The likelihood that we recover decreases the greater m_lateframes
+ // get. Skipping a frame is easier than having decoder dropping one (lateframes > 10)
+ double x = (m_lateframes <= 6) ? 0.98 : 0;
+ if (renderPts < m_Queue[*iter].pts + x * frametime)
+ break;
+ idx = *iter;
+ ++iter;
+ }
+
+ // skip late frames
+ while (m_queued.front() != idx)
+ {
+ if (m_presentsourcePast >= 0)
+ {
+ m_discard.push_back(m_presentsourcePast);
+ m_QueueSkip++;
+ }
+ m_presentsourcePast = m_queued.front();
+ m_queued.pop_front();
+ }
+
+ int lateframes = static_cast<int>((renderPts - m_Queue[idx].pts) *
+ static_cast<double>(m_fps / DVD_TIME_BASE));
+ if (lateframes)
+ m_lateframes += lateframes;
+ else
+ m_lateframes = 0;
+
+ m_presentstep = PRESENT_FLIP;
+ m_discard.push_back(m_presentsource);
+ m_presentsource = idx;
+ m_queued.pop_front();
+ m_presentpts = m_Queue[idx].pts - m_displayLatency;
+ m_presentevent.notifyAll();
+
+ m_playerPort->UpdateRenderBuffers(m_queued.size(), m_discard.size(), m_free.size());
+ }
+ else if (!combined && renderPts > (nextFramePts - frametime))
+ {
+ m_lateframes = 0;
+ m_presentstep = PRESENT_FLIP;
+ m_presentsourcePast = m_presentsource;
+ m_presentsource = m_queued.front();
+ m_queued.pop_front();
+ m_presentpts = m_Queue[m_presentsource].pts - m_displayLatency - frametime / 2;
+ m_presentevent.notifyAll();
+ }
+}
+
+void CRenderManager::DiscardBuffer()
+{
+ std::unique_lock<CCriticalSection> lock2(m_presentlock);
+
+ while(!m_queued.empty())
+ {
+ m_discard.push_back(m_queued.front());
+ m_queued.pop_front();
+ }
+
+ if(m_presentstep == PRESENT_READY)
+ m_presentstep = PRESENT_IDLE;
+ m_presentevent.notifyAll();
+}
+
+bool CRenderManager::GetStats(int &lateframes, double &pts, int &queued, int &discard)
+{
+ std::unique_lock<CCriticalSection> lock(m_presentlock);
+ lateframes = m_lateframes / 10;
+ pts = m_presentpts;
+ queued = m_queued.size();
+ discard = m_discard.size();
+ return true;
+}
+
+void CRenderManager::CheckEnableClockSync()
+{
+ // refresh rate can be a multiple of video fps
+ double diff = 1.0;
+
+ if (m_fps != 0)
+ {
+ double fps = static_cast<double>(m_fps);
+ double refreshrate, clockspeed;
+ int missedvblanks;
+ if (m_dvdClock.GetClockInfo(missedvblanks, clockspeed, refreshrate))
+ {
+ fps *= clockspeed;
+ }
+
+ diff = static_cast<double>(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS()) / fps;
+ if (diff < 1.0)
+ diff = 1.0 / diff;
+
+ // Calculate distance from nearest integer proportion
+ diff = std::abs(std::round(diff) - diff);
+ }
+
+ if (diff < 0.0005)
+ {
+ m_clockSync.m_enabled = true;
+ }
+ else
+ {
+ m_clockSync.m_enabled = false;
+ m_dvdClock.SetVsyncAdjust(0);
+ }
+
+ m_playerPort->UpdateClockSync(m_clockSync.m_enabled);
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/RenderManager.h b/xbmc/cores/VideoPlayer/VideoRenderers/RenderManager.h
new file mode 100644
index 0000000..cc4a00d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/RenderManager.h
@@ -0,0 +1,247 @@
+/*
+ * 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 "DVDClock.h"
+#include "DebugRenderer.h"
+#include "cores/VideoPlayer/VideoRenderers/BaseRenderer.h"
+#include "cores/VideoPlayer/VideoRenderers/OverlayRenderer.h"
+#include "cores/VideoSettings.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/SystemClock.h"
+#include "utils/Geometry.h"
+#include "windowing/Resolution.h"
+
+#include <atomic>
+#include <deque>
+#include <list>
+#include <map>
+
+#include "PlatformDefs.h"
+
+class CRenderCapture;
+struct VideoPicture;
+
+class CWinRenderer;
+class CLinuxRenderer;
+class CLinuxRendererGL;
+class CLinuxRendererGLES;
+class CRenderManager;
+
+class IRenderMsg
+{
+ friend CRenderManager;
+public:
+ virtual ~IRenderMsg() = default;
+protected:
+ virtual void VideoParamsChange() = 0;
+ virtual void GetDebugInfo(std::string &audio, std::string &video, std::string &general) = 0;
+ virtual void UpdateClockSync(bool enabled) = 0;
+ virtual void UpdateRenderInfo(CRenderInfo &info) = 0;
+ virtual void UpdateRenderBuffers(int queued, int discard, int free) = 0;
+ virtual void UpdateGuiRender(bool gui) = 0;
+ virtual void UpdateVideoRender(bool video) = 0;
+ virtual CVideoSettings GetVideoSettings() const = 0;
+};
+
+class CRenderManager
+{
+public:
+ CRenderManager(CDVDClock &clock, IRenderMsg *player);
+ virtual ~CRenderManager();
+
+ // Functions called from render thread
+ void GetVideoRect(CRect& source, CRect& dest, CRect& view) const;
+ float GetAspectRatio() const;
+ void FrameMove();
+ void FrameWait(std::chrono::milliseconds duration);
+ void Render(bool clear, DWORD flags = 0, DWORD alpha = 255, bool gui = true);
+ bool IsVideoLayer();
+ RESOLUTION GetResolution();
+ void UpdateResolution();
+ void TriggerUpdateResolution(float fps, int width, int height, std::string &stereomode);
+ void SetViewMode(int iViewMode);
+ void PreInit();
+ void UnInit();
+ bool Flush(bool wait, bool saveBuffers);
+ bool IsConfigured() const;
+ void ToggleDebug();
+ void ToggleDebugVideo();
+
+ /*!
+ * \brief Set the subtitle vertical position,
+ * it depends on current screen resolution
+ * \param value The subtitle position in pixels
+ * \param save If true, the value will be saved to resolution info
+ */
+ void SetSubtitleVerticalPosition(const int value, bool save);
+
+ unsigned int AllocRenderCapture();
+ void ReleaseRenderCapture(unsigned int captureId);
+ void StartRenderCapture(unsigned int captureId, unsigned int width, unsigned int height, int flags);
+ bool RenderCaptureGetPixels(unsigned int captureId, unsigned int millis, uint8_t *buffer, unsigned int size);
+
+ // Functions called from GUI
+ bool Supports(ERENDERFEATURE feature) const;
+ bool Supports(ESCALINGMETHOD method) const;
+
+ int GetSkippedFrames() { return m_QueueSkip; }
+
+ bool Configure(const VideoPicture& picture, float fps, unsigned int orientation, int buffers = 0);
+ bool AddVideoPicture(const VideoPicture& picture, volatile std::atomic_bool& bStop, EINTERLACEMETHOD deintMethod, bool wait);
+ void AddOverlay(CDVDOverlay* o, double pts);
+ void ShowVideo(bool enable);
+
+ /**
+ * If player uses buffering it has to wait for a buffer before it calls
+ * AddVideoPicture and AddOverlay. It waits for max 50 ms before it returns -1
+ * in case no buffer is available. Player may call this in a loop and decides
+ * by itself when it wants to drop a frame.
+ */
+ int WaitForBuffer(volatile std::atomic_bool& bStop,
+ std::chrono::milliseconds timeout = std::chrono::milliseconds(100));
+
+ /**
+ * Can be called by player for lateness detection. This is done best by
+ * looking at the end of the queue.
+ */
+ bool GetStats(int &lateframes, double &pts, int &queued, int &discard);
+
+ /**
+ * Video player call this on flush in oder to discard any queued frames
+ */
+ void DiscardBuffer();
+
+ void SetDelay(int delay) { m_videoDelay = delay; }
+ int GetDelay() { return m_videoDelay; }
+
+ void SetVideoSettings(const CVideoSettings& settings);
+
+protected:
+
+ void PresentSingle(bool clear, DWORD flags, DWORD alpha);
+ void PresentFields(bool clear, DWORD flags, DWORD alpha);
+ void PresentBlend(bool clear, DWORD flags, DWORD alpha);
+
+ void PrepareNextRender();
+ bool IsPresenting();
+ bool IsGuiLayer();
+
+ bool Configure();
+ void CreateRenderer();
+ void DeleteRenderer();
+ void ManageCaptures();
+
+ void UpdateLatencyTweak();
+ void CheckEnableClockSync();
+
+ CBaseRenderer *m_pRenderer = nullptr;
+ OVERLAY::CRenderer m_overlays;
+ CDebugRenderer m_debugRenderer;
+ mutable CCriticalSection m_statelock;
+ CCriticalSection m_presentlock;
+ CCriticalSection m_datalock;
+ bool m_bTriggerUpdateResolution = false;
+ bool m_bRenderGUI = true;
+ bool m_renderedOverlay = false;
+ bool m_renderDebug = false;
+ bool m_renderDebugVideo = false;
+ XbmcThreads::EndTime<> m_debugTimer;
+ std::atomic_bool m_showVideo = {false};
+
+ enum EPRESENTSTEP
+ {
+ PRESENT_IDLE = 0
+ , PRESENT_FLIP
+ , PRESENT_FRAME
+ , PRESENT_FRAME2
+ , PRESENT_READY
+ };
+
+ enum EPRESENTMETHOD
+ {
+ PRESENT_METHOD_SINGLE = 0,
+ PRESENT_METHOD_BLEND,
+ PRESENT_METHOD_BOB,
+ };
+
+ enum ERENDERSTATE
+ {
+ STATE_UNCONFIGURED = 0,
+ STATE_CONFIGURING,
+ STATE_CONFIGURED,
+ };
+ ERENDERSTATE m_renderState = STATE_UNCONFIGURED;
+ CEvent m_stateEvent;
+
+ /// Display latency tweak value from AdvancedSettings for the current refresh rate
+ /// in milliseconds
+ double m_latencyTweak = 0.0;
+ /// Display latency updated in PrepareNextRender in DVD clock units, includes m_latencyTweak
+ double m_displayLatency = 0.0;
+ std::atomic_int m_videoDelay = {};
+
+ int m_QueueSize = 2;
+ int m_QueueSkip = 0;
+
+ struct SPresent
+ {
+ double pts;
+ EFIELDSYNC presentfield;
+ EPRESENTMETHOD presentmethod;
+ } m_Queue[NUM_BUFFERS]{};
+
+ std::deque<int> m_free;
+ std::deque<int> m_queued;
+ std::deque<int> m_discard;
+
+ std::unique_ptr<VideoPicture> m_pConfigPicture;
+ unsigned int m_width = 0;
+ unsigned int m_height = 0;
+ unsigned int m_dwidth = 0;
+ unsigned int m_dheight = 0;
+ float m_fps = 0.0;
+ unsigned int m_orientation = 0;
+ int m_NumberBuffers = 0;
+ std::string m_stereomode;
+
+ int m_lateframes = -1;
+ double m_presentpts = 0.0;
+ EPRESENTSTEP m_presentstep = PRESENT_IDLE;
+ XbmcThreads::EndTime<> m_presentTimer;
+ bool m_forceNext = false;
+ int m_presentsource = 0;
+ int m_presentsourcePast = -1;
+ XbmcThreads::ConditionVariable m_presentevent;
+ CEvent m_flushEvent;
+ CEvent m_initEvent;
+ CDVDClock &m_dvdClock;
+ IRenderMsg *m_playerPort;
+
+ struct CClockSync
+ {
+ void Reset();
+ double m_error;
+ int m_errCount;
+ double m_syncOffset;
+ bool m_enabled;
+ };
+ CClockSync m_clockSync;
+
+ void RenderCapture(CRenderCapture* capture);
+ void RemoveCaptures();
+ CCriticalSection m_captCritSect;
+ std::map<unsigned int, CRenderCapture*> m_captures;
+ static unsigned int m_nextCaptureId;
+ unsigned int m_captureWaitCounter = 0;
+ //set to true when adding something to m_captures, set to false when m_captures is made empty
+ //std::list::empty() isn't thread safe, using an extra bool will save a lock per render when no captures are requested
+ bool m_hasCaptures = false;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/CMakeLists.txt b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/CMakeLists.txt
new file mode 100644
index 0000000..6b71389
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/CMakeLists.txt
@@ -0,0 +1,41 @@
+set(SOURCES ConvolutionKernels.cpp)
+
+set(HEADERS ConvolutionKernels.h
+ dither.h
+ ShaderFormats.h)
+
+if(CORE_SYSTEM_NAME STREQUAL windows OR CORE_SYSTEM_NAME STREQUAL windowsstore)
+ list(APPEND SOURCES ConversionMatrix.cpp
+ WinVideoFilter.cpp)
+ list(APPEND HEADERS ConversionMatrix.h
+ WinVideoFilter.h)
+endif()
+
+
+if(OPENGL_FOUND OR OPENGLES_FOUND)
+ list(APPEND SOURCES ConversionMatrix.cpp)
+ list(APPEND HEADERS ConversionMatrix.h)
+endif()
+
+if(OPENGL_FOUND)
+ list(APPEND SOURCES GLSLOutput.cpp
+ VideoFilterShaderGL.cpp
+ YUV2RGBShaderGL.cpp)
+ list(APPEND HEADERS GLSLOutput.h
+ VideoFilterShaderGL.h
+ YUV2RGBShaderGL.h)
+endif()
+
+if(OPENGLES_FOUND AND ("android" IN_LIST CORE_PLATFORM_NAME_LC OR
+ "ios" IN_LIST CORE_PLATFORM_NAME_LC OR
+ "tvos" IN_LIST CORE_PLATFORM_NAME_LC OR
+ "gbm" IN_LIST CORE_PLATFORM_NAME_LC OR
+ "x11" IN_LIST CORE_PLATFORM_NAME_LC OR
+ "wayland" IN_LIST CORE_PLATFORM_NAME_LC))
+ list(APPEND SOURCES VideoFilterShaderGLES.cpp
+ YUV2RGBShaderGLES.cpp)
+ list(APPEND HEADERS VideoFilterShaderGLES.h
+ YUV2RGBShaderGLES.h)
+endif()
+
+core_add_library(videoshaders)
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConversionMatrix.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConversionMatrix.cpp
new file mode 100644
index 0000000..270c263
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConversionMatrix.cpp
@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ConversionMatrix.h"
+
+#include <stdexcept>
+#include <string>
+
+//------------------------------------------------------------------------------
+// constants for primaries and transfers functions and color models
+//------------------------------------------------------------------------------
+
+// source: https://www.khronos.org/registry/DataFormat/specs/1.2/dataformat.1.2.html#PRIMARY_CONVERSION
+
+namespace
+{
+struct ConvYCbCr
+{
+ float Kr, Kb;
+};
+
+struct Primaries
+{
+ float primaries[3][2];
+ float whitepoint[2];
+};
+
+constexpr ConvYCbCr BT709YCbCr = {0.2126, 0.0722};
+constexpr ConvYCbCr BT601YCbCr = {0.299, 0.114};
+constexpr ConvYCbCr BT2020YCbCr = {0.2627, 0.0593};
+constexpr ConvYCbCr ST240YCbCr = {0.212, 0.087};
+
+constexpr Primaries PrimariesBT709 = {{{0.640, 0.330}, {0.300, 0.600}, {0.150, 0.060}},
+ {0.3127, 0.3290}};
+constexpr Primaries PrimariesBT610_525 = {{{0.640, 0.340}, {0.310, 0.595}, {0.155, 0.070}},
+ {0.3127, 0.3290}};
+constexpr Primaries PrimariesBT610_625 = {{{0.640, 0.330}, {0.290, 0.600}, {0.150, 0.060}},
+ {0.3127, 0.3290}};
+constexpr Primaries PrimariesBT2020 = {{{0.708, 0.292}, {0.170, 0.797}, {0.131, 0.046}},
+ {0.3127, 0.3290}};
+} // namespace
+
+//------------------------------------------------------------------------------
+// Matrix helpers
+//------------------------------------------------------------------------------
+// source: http://timjones.io/blog/archive/2014/10/20/the-matrix-inverted
+
+template<uint8_t Order>
+float CalculateDeterminant(const std::array<std::array<float, Order>, Order>& src);
+
+template<uint8_t Order>
+std::array<std::array<float, Order - 1>, Order - 1> GetSubmatrix(
+ const std::array<std::array<float, Order>, Order>& src, uint8_t row, uint8_t col)
+{
+ uint8_t colCount = 0;
+ uint8_t rowCount = 0;
+
+ std::array<std::array<float, Order - 1>, Order - 1> dest;
+
+ for (int i = 0; i < Order; i++)
+ {
+ if (i != row)
+ {
+ colCount = 0;
+ for (int j = 0; j < Order; j++)
+ {
+ if (j != col)
+ {
+ dest[rowCount][colCount] = src[i][j];
+ colCount++;
+ }
+ }
+ rowCount++;
+ }
+ }
+
+ return dest;
+}
+
+template<uint8_t Order>
+float CalculateMinor(const std::array<std::array<float, Order>, Order>& src,
+ uint8_t row,
+ uint8_t col)
+{
+ const std::array<std::array<float, Order - 1>, Order - 1> sub =
+ GetSubmatrix<Order>(src, row, col);
+ return CalculateDeterminant<Order - 1>(sub);
+}
+
+template<uint8_t Order>
+float CalculateDeterminant(const std::array<std::array<float, Order>, Order>& src)
+{
+ float det = 0.0f;
+
+ for (int i = 0; i < Order; i++)
+ {
+ // Get minor of element (0, i)
+ float minor = CalculateMinor<Order>(src, 0, i);
+
+ // If this is an odd-numbered row, negate the value.
+ float factor = (i % 2 == 1) ? -1.0f : 1.0f;
+
+ det += factor * src[0][i] * minor;
+ }
+ return det;
+}
+
+template<>
+float CalculateDeterminant<2>(const std::array<std::array<float, 2>, 2>& src)
+{
+ return src[0][0] * src[1][1] - src[0][1] * src[1][0];
+}
+
+//------------------------------------------------------------------------------
+// Matrix classes
+//------------------------------------------------------------------------------
+
+template<uint8_t Order>
+CMatrix<Order>::CMatrix(const std::array<std::array<float, Order - 1>, Order - 1>& other)
+{
+ *this = other;
+}
+
+template<uint8_t Order>
+CMatrix<Order>::CMatrix(const CMatrix<Order - 1>& other)
+{
+ *this = other.Get();
+}
+
+template<uint8_t Order>
+CMatrix<Order>& CMatrix<Order>::operator=(
+ const std::array<std::array<float, Order - 1>, Order - 1>& other)
+{
+ for (int i = 0; i < Order - 1; ++i)
+ for (int j = 0; j < Order - 1; ++j)
+ m_mat[i][j] = other[i][j];
+
+ for (int i = 0; i < Order; ++i)
+ m_mat[i][Order-1] = 0;
+
+ for (int i = 0; i < Order; ++i)
+ m_mat[Order-1][i] = 0;
+
+ return *this;
+}
+
+template<uint8_t Order>
+std::array<std::array<float, Order>, Order>& CMatrix<Order>::Get()
+{
+ return m_mat;
+}
+
+template<uint8_t Order>
+const std::array<std::array<float, Order>, Order>& CMatrix<Order>::Get() const
+{
+ return m_mat;
+}
+
+template<uint8_t Order>
+CMatrix<Order> CMatrix<Order>::operator*(const CMatrix& other)
+{
+ return *this * other.m_mat;
+}
+
+template<uint8_t Order>
+CMatrix<Order>& CMatrix<Order>::operator*=(const CMatrix& other)
+{
+ CMatrix<Order> tmp = *this * other.m_mat;
+ *this = tmp;
+ return *this;
+}
+
+template<uint8_t Order>
+CMatrix<Order> CMatrix<Order>::operator*(const std::array<std::array<float, Order>, Order>& other)
+{
+ CMatrix<Order> ret;
+ for (int i = 0; i < Order; ++i)
+ for (int j = 0; j < Order; ++j)
+ for (int k = 0; k < Order; ++k)
+ ret.m_mat[i][j] += m_mat[i][k] * other[k][j];
+
+ return ret;
+}
+
+template<uint8_t Order>
+CMatrix<Order>& CMatrix<Order>::Invert()
+{
+ CMatrix<Order> tmp;
+ tmp.m_mat = Invert(m_mat);
+ *this = tmp;
+ return *this;
+}
+
+template<uint8_t Order>
+std::array<std::array<float, Order>, Order> CMatrix<Order>::Invert(
+ std::array<std::array<float, Order>, Order>& other) const
+{
+ // Calculate the inverse of the determinant of src.
+ float det = CalculateDeterminant<Order>(other);
+ float inverseDet = 1.0f / det;
+
+ std::array<std::array<float, Order>, Order> dst;
+
+ for (int j = 0; j < Order; j++)
+ {
+ for (int i = 0; i < Order; i++)
+ {
+ // Get minor of element (j, i) - not (i, j) because
+ // this is where the transpose happens.
+ float minor = CalculateMinor<Order>(other, j, i);
+
+ // Multiply by (−1)^{i+j}
+ float factor = ((i + j) % 2 == 1) ? -1.0f : 1.0f;
+ float cofactor = minor * factor;
+
+ dst[i][j] = inverseDet * cofactor;
+ }
+ }
+
+ return dst;
+}
+
+CGlMatrix::CGlMatrix(const CMatrix<3>& other) : CMatrix<4>(other)
+{
+}
+
+CGlMatrix::CGlMatrix(const std::array<std::array<float, 3>, 3>& other) : CMatrix<4>(other)
+{
+}
+
+CGlMatrix::CMatrix CGlMatrix::operator*(const std::array<std::array<float, 4>, 4>& other)
+{
+ CGlMatrix ret;
+
+ std::array<std::array<float, 4>, 4>& left = m_mat;
+ const std::array<std::array<float, 4>, 4>& right = other;
+
+ ret.m_mat[0][0] = left[0][0] * right[0][0] + left[0][1] * right[1][0] + left[0][2] * right[2][0];
+ ret.m_mat[0][1] = left[0][0] * right[0][1] + left[0][1] * right[1][1] + left[0][2] * right[2][1];
+ ret.m_mat[0][2] = left[0][0] * right[0][2] + left[0][1] * right[1][2] + left[0][2] * right[2][2];
+ ret.m_mat[0][3] = left[0][0] * right[0][3] + left[0][1] * right[1][3] + left[0][2] * right[2][3] + left[0][3];
+ ret.m_mat[1][0] = left[1][0] * right[0][0] + left[1][1] * right[1][0] + left[1][2] * right[2][0];
+ ret.m_mat[1][1] = left[1][0] * right[0][1] + left[1][1] * right[1][1] + left[1][2] * right[2][1];
+ ret.m_mat[1][2] = left[1][0] * right[0][2] + left[1][1] * right[1][2] + left[1][2] * right[2][2];
+ ret.m_mat[1][3] = left[1][0] * right[0][3] + left[1][1] * right[1][3] + left[1][2] * right[2][3] + left[1][3];
+ ret.m_mat[2][0] = left[2][0] * right[0][0] + left[2][1] * right[1][0] + left[2][2] * right[2][0];
+ ret.m_mat[2][1] = left[2][0] * right[0][1] + left[2][1] * right[1][1] + left[2][2] * right[2][1];
+ ret.m_mat[2][2] = left[2][0] * right[0][2] + left[2][1] * right[1][2] + left[2][2] * right[2][2];
+ ret.m_mat[2][3] = left[2][0] * right[0][3] + left[2][1] * right[1][3] + left[2][2] * right[2][3] + left[2][3];
+
+ return ret;
+}
+
+CScale::CScale(float x, float y, float z)
+{
+ m_mat[0][0] = x;
+ m_mat[1][1] = y;
+ m_mat[2][2] = z;
+ m_mat[3][3] = 1;
+}
+
+CTranslate::CTranslate(float x, float y, float z)
+{
+ m_mat[0][0] = 1;
+ m_mat[1][1] = 1;
+ m_mat[2][2] = 1;
+ m_mat[3][3] = 1;
+ m_mat[0][3] = x;
+ m_mat[1][3] = y;
+ m_mat[2][3] = z;
+}
+
+//------------------------------------------------------------------------------
+// Conversion classes
+//------------------------------------------------------------------------------
+
+ConversionToRGB::ConversionToRGB(float Kr, float Kb)
+{
+ float Kg = 1-Kr-Kb;
+ a11 = Kr;
+ a12 = Kg;
+ a13 = Kb;
+ CbDen = 2*(1-Kb);
+ CrDen = 2*(1-Kr);
+
+ m_mat[0][0] = a11; m_mat[0][1] = a12; m_mat[0][2] = a13;
+ m_mat[1][0] = -Kr/CbDen; m_mat[1][1] = -Kg/CbDen; m_mat[1][2] = 0.5;
+ m_mat[2][0] = 0.5; m_mat[2][1] = -Kg/CrDen; m_mat[2][2] = -Kb/CrDen;
+
+ m_mat = Invert(m_mat);
+};
+
+PrimaryToXYZ::PrimaryToXYZ(const float (&primaries)[3][2], const float (&whitepoint)[2])
+{
+ float By = CalcBy(primaries, whitepoint);
+ float Gy = CalcGy(primaries, whitepoint, By);
+ float Ry = CalcRy(By, Gy);
+
+ m_mat[0][0] = Ry*primaries[0][0]/primaries[0][1];
+ m_mat[0][1] = Gy*primaries[1][0]/primaries[1][1];
+ m_mat[0][2] = By*primaries[2][0]/primaries[2][1];
+ m_mat[1][0] = Ry;
+ m_mat[1][1] = Gy;
+ m_mat[1][2] = By;
+ m_mat[2][0] = Ry/primaries[0][1] * (1- primaries[0][0] - primaries[0][1]);
+ m_mat[2][1] = Gy/primaries[1][1] * (1- primaries[1][0] - primaries[1][1]);
+ m_mat[2][2] = By/primaries[2][1] * (1- primaries[2][0] - primaries[2][1]);
+}
+
+float PrimaryToXYZ::CalcBy(const float p[3][2], const float w[2])
+{
+ float val = ((1-w[0])/w[1] - (1-p[0][0])/p[0][1]) * (p[1][0]/p[1][1] - p[0][0]/p[0][1]) -
+ (w[0]/w[1] - p[0][0]/p[0][1]) * ((1-p[1][0])/p[1][1] - (1-p[0][0])/p[0][1]);
+
+ val /= ((1-p[2][0])/p[2][1] - (1-p[0][0])/p[0][1]) * (p[1][0]/p[1][1] - p[0][0]/p[0][1]) -
+ (p[2][0]/p[2][1] - p[0][0]/p[0][1]) * ((1-p[1][0])/p[1][1] - (1-p[0][0])/p[0][1]);
+
+ return val;
+}
+
+float PrimaryToXYZ::CalcGy(const float p[3][2], const float w[2], const float By)
+{
+ float val = w[0]/w[1] - p[0][0]/p[0][1] - By * (p[2][0]/p[2][1] - p[0][0]/p[0][1]);
+ val /= p[1][0]/p[1][1] - p[0][0]/p[0][1];
+
+ return val;
+}
+
+float PrimaryToXYZ::CalcRy(const float By, const float Gy)
+{
+ return 1.0f - Gy - By;
+}
+
+PrimaryToRGB::PrimaryToRGB(float (&primaries)[3][2], float (&whitepoint)[2]) : PrimaryToXYZ(primaries, whitepoint)
+{
+ m_mat = Invert(m_mat);
+}
+
+//------------------------------------------------------------------------------
+
+CConvertMatrix& CConvertMatrix::SetSourceColorSpace(AVColorSpace colorSpace)
+{
+ if (m_colSpace != colorSpace)
+ m_mat.reset();
+
+ m_colSpace = colorSpace;
+ return *this;
+}
+
+CConvertMatrix& CConvertMatrix::SetSourceBitDepth(int bits)
+{
+ if (m_srcBits != bits)
+ m_mat.reset();
+
+ m_srcBits = bits;
+ return *this;
+}
+
+CConvertMatrix& CConvertMatrix::SetSourceLimitedRange(bool limited)
+{
+ if (m_limitedSrc != limited)
+ m_mat.reset();
+
+ m_limitedSrc = limited;
+ return *this;
+}
+
+CConvertMatrix& CConvertMatrix::SetSourceTextureBitDepth(int textureBits)
+{
+ if (m_srcTextureBits != textureBits)
+ m_mat.reset();
+
+ m_srcTextureBits = textureBits;
+ return *this;
+}
+
+CConvertMatrix& CConvertMatrix::SetSourceColorPrimaries(AVColorPrimaries src)
+{
+ if (m_colPrimariesSrc != src)
+ m_matPrim.reset();
+
+ m_colPrimariesSrc = src;
+ return *this;
+}
+
+CConvertMatrix& CConvertMatrix::SetDestinationColorPrimaries(AVColorPrimaries dst)
+{
+ if (m_colPrimariesDst != dst)
+ m_matPrim.reset();
+
+ m_colPrimariesDst = dst;
+ return *this;
+}
+
+CConvertMatrix& CConvertMatrix::SetDestinationContrast(float contrast)
+{
+ m_contrast = contrast;
+ return *this;
+}
+
+CConvertMatrix& CConvertMatrix::SetDestinationBlack(float black)
+{
+ m_black = black;
+ return *this;
+}
+
+CConvertMatrix& CConvertMatrix::SetDestinationLimitedRange(bool limited)
+{
+ m_limitedDst = limited;
+ return *this;
+}
+
+const CMatrix<3>& CConvertMatrix::GenPrimMat()
+{
+ if (m_matPrim)
+ return *m_matPrim;
+
+ Primaries primToRGB;
+ Primaries primToXYZ;
+ switch (m_colPrimariesSrc)
+ {
+ case AVCOL_PRI_BT709:
+ primToXYZ = PrimariesBT709;
+ m_gammaSrc = 2.2;
+ break;
+ case AVCOL_PRI_BT470BG:
+ primToXYZ = PrimariesBT610_625;
+ m_gammaSrc = 2.2;
+ break;
+ case AVCOL_PRI_SMPTE170M:
+ case AVCOL_PRI_SMPTE240M:
+ primToXYZ = PrimariesBT610_525;
+ m_gammaSrc = 2.2;
+ break;
+ case AVCOL_PRI_BT2020:
+ primToXYZ = PrimariesBT2020;
+ m_gammaSrc = 2.4;
+ break;
+ default:
+ primToXYZ = PrimariesBT709;
+ m_gammaSrc = 2.2;
+ break;
+ }
+ switch (m_colPrimariesDst)
+ {
+ case AVCOL_PRI_BT709:
+ primToRGB = PrimariesBT709;
+ m_gammaDst = 2.2;
+ break;
+ case AVCOL_PRI_BT470BG:
+ primToRGB = PrimariesBT610_625;
+ m_gammaDst = 2.2;
+ break;
+ case AVCOL_PRI_SMPTE170M:
+ case AVCOL_PRI_SMPTE240M:
+ primToRGB = PrimariesBT610_525;
+ m_gammaDst = 2.2;
+ break;
+ case AVCOL_PRI_BT2020:
+ primToRGB = PrimariesBT2020;
+ m_gammaDst = 2.4;
+ break;
+ default:
+ primToRGB = PrimariesBT709;
+ m_gammaDst = 2.2;
+ break;
+ }
+ PrimaryToXYZ toXYZ(primToXYZ.primaries, primToXYZ.whitepoint);
+ PrimaryToRGB toRGB(primToRGB.primaries, primToRGB.whitepoint);
+
+ m_matPrim = std::make_unique<CMatrix<3>>(toRGB * toXYZ);
+
+ return *m_matPrim;
+}
+
+const CGlMatrix& CConvertMatrix::GenMat()
+{
+ if (m_mat)
+ return *m_mat;
+
+ ConvYCbCr convYCbCr;
+ switch (m_colSpace)
+ {
+ case AVCOL_SPC_BT709:
+ convYCbCr = BT709YCbCr;
+ break;
+ case AVCOL_SPC_BT470BG:
+ case AVCOL_SPC_SMPTE170M:
+ convYCbCr = BT601YCbCr;
+ break;
+ case AVCOL_SPC_SMPTE240M:
+ convYCbCr = ST240YCbCr;
+ break;
+ case AVCOL_SPC_BT2020_NCL:
+ case AVCOL_SPC_BT2020_CL:
+ convYCbCr = BT2020YCbCr;
+ break;
+ default:
+ convYCbCr = BT709YCbCr;
+ break;
+ }
+
+ ConversionToRGB mConvRGB(convYCbCr.Kr, convYCbCr.Kb);
+ CGlMatrix mat(mConvRGB);
+
+ CTranslate trans(0, -0.5, -0.5);
+ mat *= trans;
+
+ if (m_limitedSrc)
+ {
+ if (m_srcBits >= 12)
+ {
+ CScale scale(4080.0f / (3760 - 256), 4080.0f / (3840 - 256), 4080.0f / (3840 - 256));
+ CTranslate trans(- 256.0f / 4080, - 256.0f / 4080, - 256.0f / 4080);
+ mat *= scale;
+ mat *= trans;
+ }
+ else if (m_srcBits == 10)
+ {
+ CScale scale(1020.0f / (940 - 64), 1020.0f / (960 - 64), 1020.0f / (960 - 64));
+ CTranslate trans(- 64.0f / 1020, - 64.0f / 1020, - 64.0f / 1020);
+ mat *= scale;
+ mat *= trans;
+ }
+ else
+ {
+ CScale scale(255.0f / (235 - 16), 255.0f / (240 - 16), 255.0f / (240 - 16));
+ CTranslate trans(- 16.0f / 255, - 16.0f / 255, - 16.0f / 255);
+ mat *= scale;
+ mat *= trans;
+ }
+ }
+
+ if (m_srcTextureBits > 8)
+ {
+ float val = 65535.0f / ((1 << m_srcTextureBits) - 1);
+ CScale scale(val, val, val);
+ mat *= scale;
+ }
+
+ m_mat = std::make_unique<CGlMatrix>(mat);
+
+ return *m_mat;
+}
+
+Matrix4 CConvertMatrix::GetYuvMat()
+{
+ const CGlMatrix& mat = GenMat();
+
+ CScale contrast(m_contrast, m_contrast, m_contrast);
+ CTranslate black(m_black, m_black, m_black);
+
+ CGlMatrix ret = contrast;
+ ret *= black;
+ if (m_limitedDst)
+ {
+ float valScale = (235 - 16) / 255.0f;
+ float valTrans = 16.0f / 255;
+ CScale scale(valScale, valScale, valScale);
+ CTranslate trans(valTrans, valTrans, valTrans);
+ ret *= trans;
+ ret *= scale;
+ }
+
+ ret *= mat;
+
+ Matrix4 dst;
+
+ for (int i = 0; i < 4; ++i)
+ for (int j = 0; j < 4; ++j)
+ dst[i][j] = ret[j][i];
+
+ dst[0][3] = 0.0f;
+ dst[1][3] = 0.0f;
+ dst[2][3] = 0.0f;
+ dst[3][3] = 1.0f;
+
+ return dst;
+}
+
+Matrix3 CConvertMatrix::GetPrimMat()
+{
+ if (m_colPrimariesDst == m_colPrimariesSrc)
+ return Matrix3();
+
+ const Matrix3& matPrim = GenPrimMat();
+
+ Matrix3 dst;
+
+ for (int i = 0; i < 3; ++i)
+ for (int j = 0; j < 3; ++j)
+ dst[i][j] = matPrim[j][i];
+
+ return dst;
+}
+
+float CConvertMatrix::GetGammaSrc()
+{
+ return m_gammaSrc;
+}
+
+float CConvertMatrix::GetGammaDst()
+{
+ return m_gammaDst;
+}
+
+Matrix3x1 CConvertMatrix::GetRGBYuvCoefs(AVColorSpace colspace)
+{
+ Matrix3x1 coefs;
+
+ switch (colspace)
+ {
+ case AVCOL_SPC_BT709:
+ coefs[0] = BT709YCbCr.Kr;
+ coefs[1] = 1 - BT709YCbCr.Kr - BT709YCbCr.Kb;
+ coefs[2] = BT709YCbCr.Kb;
+ break;
+ case AVCOL_SPC_BT470BG:
+ case AVCOL_SPC_SMPTE170M:
+ coefs[0] = BT601YCbCr.Kr;
+ coefs[1] = 1 - BT601YCbCr.Kr - BT601YCbCr.Kb;
+ coefs[2] = BT601YCbCr.Kb;
+ break;
+ case AVCOL_SPC_SMPTE240M:
+ coefs[0] = ST240YCbCr.Kr;
+ coefs[1] = 1 - ST240YCbCr.Kr - ST240YCbCr.Kb;
+ coefs[2] = ST240YCbCr.Kb;
+ break;
+ case AVCOL_SPC_BT2020_NCL:
+ case AVCOL_SPC_BT2020_CL:
+ coefs[0] = BT2020YCbCr.Kr;
+ coefs[1] = 1 - BT2020YCbCr.Kr - BT2020YCbCr.Kb;
+ coefs[2] = BT2020YCbCr.Kb;
+ break;
+ default:
+ throw std::invalid_argument("unknown colorspace: " + std::to_string(colspace));
+ }
+
+ return coefs;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConversionMatrix.h b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConversionMatrix.h
new file mode 100644
index 0000000..b2c66a7
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConversionMatrix.h
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <array>
+#include <cmath>
+#include <memory>
+
+extern "C" {
+#include <libavutil/pixfmt.h>
+}
+
+template<uint8_t Order>
+class CMatrix
+{
+public:
+ CMatrix() = default;
+ explicit CMatrix(const CMatrix<Order - 1>& other);
+ explicit CMatrix(const std::array<std::array<float, Order>, Order>& other) : m_mat(other) {}
+ explicit CMatrix(const std::array<std::array<float, Order - 1>, Order - 1>& other);
+ virtual ~CMatrix() = default;
+
+ virtual CMatrix operator*(const std::array<std::array<float, Order>, Order>& other);
+
+ CMatrix operator*(const CMatrix& other);
+ CMatrix& operator*=(const CMatrix& other);
+
+ CMatrix& operator=(const std::array<std::array<float, Order - 1>, Order - 1>& other);
+
+ bool operator==(const CMatrix<Order>& other) const
+ {
+ for (int i = 0; i < Order; ++i)
+ {
+ for (int j = 0; j < Order; ++j)
+ {
+ if (m_mat[i][j] == other.m_mat[i][j])
+ continue;
+
+ // some floating point comparisons should be done by checking if the difference is within a tolerance
+ if (std::abs(m_mat[i][j] - other.m_mat[i][j]) <=
+ (std::max(std::abs(other.m_mat[i][j]), std::abs(m_mat[i][j])) * 1e-2f))
+ continue;
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ std::array<float, Order>& operator[](int index) { return m_mat[index]; }
+
+ const std::array<float, Order>& operator[](int index) const { return m_mat[index]; }
+
+ std::array<std::array<float, Order>, Order>& Get();
+
+ const std::array<std::array<float, Order>, Order>& Get() const;
+
+ CMatrix& Invert();
+
+ float* ToRaw() { return &m_mat[0][0]; }
+
+protected:
+ std::array<std::array<float, Order>, Order> Invert(
+ std::array<std::array<float, Order>, Order>& other) const;
+
+ std::array<std::array<float, Order>, Order> m_mat{{}};
+};
+
+class CGlMatrix : public CMatrix<4>
+{
+public:
+ CGlMatrix() = default;
+ explicit CGlMatrix(const CMatrix<3>& other);
+ explicit CGlMatrix(const std::array<std::array<float, 3>, 3>& other);
+ ~CGlMatrix() override = default;
+ CMatrix operator*(const std::array<std::array<float, 4>, 4>& other) override;
+};
+
+class CScale : public CGlMatrix
+{
+public:
+ CScale(float x, float y, float z);
+ ~CScale() override = default;
+};
+
+class CTranslate : public CGlMatrix
+{
+public:
+ CTranslate(float x, float y, float z);
+ ~CTranslate() override = default;
+};
+
+class ConversionToRGB : public CMatrix<3>
+{
+public:
+ ConversionToRGB(float Kr, float Kb);
+ ~ConversionToRGB() override = default;
+
+protected:
+ ConversionToRGB() = default;
+
+ float a11, a12, a13;
+ float CbDen, CrDen;
+};
+
+class PrimaryToXYZ : public CMatrix<3>
+{
+public:
+ PrimaryToXYZ(const float (&primaries)[3][2], const float (&whitepoint)[2]);
+ ~PrimaryToXYZ() override = default;
+
+protected:
+ PrimaryToXYZ() = default;
+ float CalcBy(const float p[3][2], const float w[2]);
+ float CalcGy(const float p[3][2], const float w[2], const float By);
+ float CalcRy(const float By, const float Gy);
+};
+
+class PrimaryToRGB : public PrimaryToXYZ
+{
+public:
+ PrimaryToRGB(float (&primaries)[3][2], float (&whitepoint)[2]);
+ ~PrimaryToRGB() override = default;
+};
+
+//------------------------------------------------------------------------------
+
+using Matrix4 = CMatrix<4>;
+using Matrix3 = CMatrix<3>;
+using Matrix3x1 = std::array<float, 3>;
+
+/**
+ * @brief Helper class used for YUV to RGB conversions. This class can
+ * take into account different source/destination primaries and
+ * various other parameters.
+ */
+class CConvertMatrix
+{
+public:
+ CConvertMatrix() = default;
+ ~CConvertMatrix() = default;
+
+ /**
+ * @brief Set the source color space.
+ */
+ CConvertMatrix& SetSourceColorSpace(AVColorSpace colorSpace);
+
+ /**
+ * @brief Set the source bit depth.
+ */
+ CConvertMatrix& SetSourceBitDepth(int bits);
+
+ /**
+ * @brief Set the source limited range boolean.
+ */
+ CConvertMatrix& SetSourceLimitedRange(bool limited);
+
+ /**
+ * @brief Set the source texture bit depth. This is need to normalize values
+ * when using > 8 bit texture formats in OpenGL/DirectX. For example
+ * GL_R16 is a 16 bit texture which needs to normalize the 10 bit format.
+ */
+ CConvertMatrix& SetSourceTextureBitDepth(int textureBits);
+
+ /**
+ * @brief Set the source color primaries.
+ */
+ CConvertMatrix& SetSourceColorPrimaries(AVColorPrimaries src);
+
+ /**
+ * @brief Set the destination color primaries.
+ */
+ CConvertMatrix& SetDestinationColorPrimaries(AVColorPrimaries dst);
+
+ /**
+ * @brief Set the destination contrast.
+ */
+ CConvertMatrix& SetDestinationContrast(float contrast);
+
+ /**
+ * @brief Set the destination black level.
+ */
+ CConvertMatrix& SetDestinationBlack(float black);
+
+ /**
+ * @brief Set the destination limited range boolean.
+ */
+ CConvertMatrix& SetDestinationLimitedRange(bool limited);
+
+ /**
+ * @brief Get the YUV Matrix for the YUV to RGB conversion.
+ */
+ Matrix4 GetYuvMat();
+
+ /**
+ * @brief Get the Primaries Matrix for the primaries conversion.
+ */
+ Matrix3 GetPrimMat();
+
+ /**
+ * @brief Get the gamma source value. Used for color primary conversion..
+ */
+ float GetGammaSrc();
+
+ /**
+ * @brief Get the gamma destination value. Used for color primary conversion.
+ */
+ float GetGammaDst();
+
+ /**
+ * @brief Get the YUV coeffecients used for tonemapping.
+ */
+ static Matrix3x1 GetRGBYuvCoefs(AVColorSpace colspace);
+
+private:
+ const CGlMatrix& GenMat();
+ const CMatrix<3>& GenPrimMat();
+
+ std::unique_ptr<CGlMatrix> m_mat;
+ std::unique_ptr<CMatrix<3>> m_matPrim;
+
+ AVColorSpace m_colSpace = AVCOL_SPC_BT709;
+ AVColorPrimaries m_colPrimariesSrc = AVCOL_PRI_BT709;
+ float m_gammaSrc = 2.2f;
+ bool m_limitedSrc = true;
+ AVColorPrimaries m_colPrimariesDst = AVCOL_PRI_BT709;
+ float m_gammaDst = 2.2f;
+ bool m_limitedDst = false;
+ int m_srcBits = 8;
+ int m_srcTextureBits = 8;
+ float m_contrast = 1.0;
+ float m_black = 0.0;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConvolutionKernels.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConvolutionKernels.cpp
new file mode 100644
index 0000000..93a4f4a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConvolutionKernels.cpp
@@ -0,0 +1,298 @@
+/*
+ * 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.
+ */
+
+#ifdef TARGET_WINDOWS
+ #define _USE_MATH_DEFINES
+#endif
+
+#include "ConvolutionKernels.h"
+#include "utils/MathUtils.h"
+
+#ifndef M_PI
+ #define M_PI 3.14159265358979323846
+#endif
+
+#define SINC(x) (sin(M_PI * (x)) / (M_PI * (x)))
+
+CConvolutionKernel::CConvolutionKernel(ESCALINGMETHOD method, int size)
+{
+ m_size = size;
+ m_floatpixels = new float[m_size * 4];
+
+ if (method == VS_SCALINGMETHOD_LANCZOS2)
+ Lanczos2();
+ else if (method == VS_SCALINGMETHOD_SPLINE36_FAST)
+ Spline36Fast();
+ else if (method == VS_SCALINGMETHOD_LANCZOS3_FAST)
+ Lanczos3Fast();
+ else if (method == VS_SCALINGMETHOD_SPLINE36)
+ Spline36();
+ else if (method == VS_SCALINGMETHOD_LANCZOS3)
+ Lanczos3();
+ else if (method == VS_SCALINGMETHOD_CUBIC_B_SPLINE)
+ Bicubic(1.0, 0.0);
+ else if (method == VS_SCALINGMETHOD_CUBIC_MITCHELL)
+ Bicubic(1.0 / 3.0, 1.0 / 3.0);
+ else if (method == VS_SCALINGMETHOD_CUBIC_CATMULL)
+ Bicubic(0.0, 0.5);
+ else if (method == VS_SCALINGMETHOD_CUBIC_0_075)
+ Bicubic(0.0, 0.75);
+ else if (method == VS_SCALINGMETHOD_CUBIC_0_1)
+ Bicubic(0.0, 1.0);
+
+ ToIntFract();
+ ToUint8();
+}
+
+CConvolutionKernel::~CConvolutionKernel()
+{
+ delete [] m_floatpixels;
+ delete [] m_intfractpixels;
+ delete [] m_uint8pixels;
+}
+
+//generate a lanczos2 kernel which can be loaded with RGBA format
+//each value of RGBA has one tap, so a shader can load 4 taps with a single pixel lookup
+void CConvolutionKernel::Lanczos2()
+{
+ for (int i = 0; i < m_size; i++)
+ {
+ double x = (double)i / (double)m_size;
+
+ //generate taps
+ for (int j = 0; j < 4; j++)
+ m_floatpixels[i * 4 + j] = (float)LanczosWeight(x + (double)(j - 2), 2.0);
+
+ //any collection of 4 taps added together needs to be exactly 1.0
+ //for lanczos this is not always the case, so we take each collection of 4 taps
+ //and divide those taps by the sum of the taps
+ float weight = 0.0;
+ for (int j = 0; j < 4; j++)
+ weight += m_floatpixels[i * 4 + j];
+
+ for (int j = 0; j < 4; j++)
+ m_floatpixels[i * 4 + j] /= weight;
+ }
+}
+
+//generate a lanczos3 kernel which can be loaded with RGBA format
+//each value of RGBA has one tap, so a shader can load 4 taps with a single pixel lookup
+//the two outer lobes of the lanczos3 kernel are added to the two lobes one step to the middle
+//this basically looks the same as lanczos3, but the kernel only has 4 taps,
+//so it can use the 4x4 convolution shader which is twice as fast as the 6x6 one
+void CConvolutionKernel::Lanczos3Fast()
+{
+ for (int i = 0; i < m_size; i++)
+ {
+ double a = 3.0;
+ double x = (double)i / (double)m_size;
+
+ //generate taps
+ m_floatpixels[i * 4 + 0] = (float)(LanczosWeight(x - 2.0, a) + LanczosWeight(x - 3.0, a));
+ m_floatpixels[i * 4 + 1] = (float) LanczosWeight(x - 1.0, a);
+ m_floatpixels[i * 4 + 2] = (float) LanczosWeight(x , a);
+ m_floatpixels[i * 4 + 3] = (float)(LanczosWeight(x + 1.0, a) + LanczosWeight(x + 2.0, a));
+
+ //any collection of 4 taps added together needs to be exactly 1.0
+ //for lanczos this is not always the case, so we take each collection of 4 taps
+ //and divide those taps by the sum of the taps
+ float weight = 0.0;
+ for (int j = 0; j < 4; j++)
+ weight += m_floatpixels[i * 4 + j];
+
+ for (int j = 0; j < 4; j++)
+ m_floatpixels[i * 4 + j] /= weight;
+ }
+}
+
+//generate a lanczos3 kernel which can be loaded with RGBA format
+//each value of RGB has one tap, so a shader can load 3 taps with a single pixel lookup
+void CConvolutionKernel::Lanczos3()
+{
+ for (int i = 0; i < m_size; i++)
+ {
+ double x = (double)i / (double)m_size;
+
+ //generate taps
+ for (int j = 0; j < 3; j++)
+ m_floatpixels[i * 4 + j] = (float)LanczosWeight(x * 2.0 + (double)(j * 2 - 3), 3.0);
+
+ m_floatpixels[i * 4 + 3] = 0.0;
+ }
+
+ //any collection of 6 taps added together needs to be exactly 1.0
+ //for lanczos this is not always the case, so we take each collection of 6 taps
+ //and divide those taps by the sum of the taps
+ for (int i = 0; i < m_size / 2; i++)
+ {
+ float weight = 0.0;
+ for (int j = 0; j < 3; j++)
+ {
+ weight += m_floatpixels[i * 4 + j];
+ weight += m_floatpixels[(i + m_size / 2) * 4 + j];
+ }
+ for (int j = 0; j < 3; j++)
+ {
+ m_floatpixels[i * 4 + j] /= weight;
+ m_floatpixels[(i + m_size / 2) * 4 + j] /= weight;
+ }
+ }
+}
+
+void CConvolutionKernel::Spline36Fast()
+{
+ for (int i = 0; i < m_size; i++)
+ {
+ double x = (double)i / (double)m_size;
+
+ //generate taps
+ m_floatpixels[i * 4 + 0] = (float)(Spline36Weight(x - 2.0) + Spline36Weight(x - 3.0));
+ m_floatpixels[i * 4 + 1] = (float) Spline36Weight(x - 1.0);
+ m_floatpixels[i * 4 + 2] = (float) Spline36Weight(x );
+ m_floatpixels[i * 4 + 3] = (float)(Spline36Weight(x + 1.0) + Spline36Weight(x + 2.0));
+
+ float weight = 0.0;
+ for (int j = 0; j < 4; j++)
+ weight += m_floatpixels[i * 4 + j];
+
+ for (int j = 0; j < 4; j++)
+ m_floatpixels[i * 4 + j] /= weight;
+ }
+}
+
+void CConvolutionKernel::Spline36()
+{
+ for (int i = 0; i < m_size; i++)
+ {
+ double x = (double)i / (double)m_size;
+
+ //generate taps
+ for (int j = 0; j < 3; j++)
+ m_floatpixels[i * 4 + j] = (float)Spline36Weight(x * 2.0 + (double)(j * 2 - 3));
+
+ m_floatpixels[i * 4 + 3] = 0.0;
+ }
+
+ for (int i = 0; i < m_size / 2; i++)
+ {
+ float weight = 0.0;
+ for (int j = 0; j < 3; j++)
+ {
+ weight += m_floatpixels[i * 4 + j];
+ weight += m_floatpixels[(i + m_size / 2) * 4 + j];
+ }
+ for (int j = 0; j < 3; j++)
+ {
+ m_floatpixels[i * 4 + j] /= weight;
+ m_floatpixels[(i + m_size / 2) * 4 + j] /= weight;
+ }
+ }
+}
+
+//generate a bicubic kernel which can be loaded with RGBA format
+//each value of RGBA has one tap, so a shader can load 4 taps with a single pixel lookup
+void CConvolutionKernel::Bicubic(double B, double C)
+{
+ for (int i = 0; i < m_size; i++)
+ {
+ double x = (double)i / (double)m_size;
+
+ //generate taps
+ for (int j = 0; j < 4; j++)
+ m_floatpixels[i * 4 + j] = (float)BicubicWeight(x + (double)(j - 2), B, C);
+ }
+}
+
+double CConvolutionKernel::LanczosWeight(double x, double radius)
+{
+ double ax = fabs(x);
+
+ if (ax == 0.0)
+ return 1.0;
+ else if (ax < radius)
+ return SINC(ax) * SINC(ax / radius);
+ else
+ return 0.0;
+}
+
+double CConvolutionKernel::BicubicWeight(double x, double B, double C)
+{
+ double ax = fabs(x);
+
+ if (ax<1.0)
+ {
+ return ((12 - 9*B - 6*C) * ax * ax * ax +
+ (-18 + 12*B + 6*C) * ax * ax +
+ (6 - 2*B))/6;
+ }
+ else if (ax<2.0)
+ {
+ return ((-B - 6*C) * ax * ax * ax +
+ (6*B + 30*C) * ax * ax + (-12*B - 48*C) *
+ ax + (8*B + 24*C)) / 6;
+ }
+ else
+ {
+ return 0.0;
+ }
+}
+
+double CConvolutionKernel::Spline36Weight(double x)
+{
+ double ax = fabs(x);
+
+ if ( ax < 1.0 )
+ return ( ( 13.0 / 11.0 * (ax ) - 453.0 / 209.0 ) * (ax ) - 3.0 / 209.0 ) * (ax ) + 1.0;
+ else if ( ax < 2.0 )
+ return ( ( -6.0 / 11.0 * (ax - 1.0) + 270.0 / 209.0 ) * (ax - 1.0) - 156.0 / 209.0 ) * (ax - 1.0);
+ else if ( ax < 3.0 )
+ return ( ( 1.0 / 11.0 * (ax - 2.0) - 45.0 / 209.0 ) * (ax - 2.0) + 26.0 / 209.0 ) * (ax - 2.0);
+ return 0.0;
+}
+
+//convert float to high byte/low byte, so the kernel can be loaded into an 8 bit texture
+//with height 2 and converted back to real float in the shader
+//it only works when the kernel texture uses nearest neighbour, but there's almost no difference
+//between that and linear interpolation
+void CConvolutionKernel::ToIntFract()
+{
+ m_intfractpixels = new uint8_t[m_size * 8];
+
+ for (int i = 0; i < m_size * 4; i++)
+ {
+ int value = MathUtils::round_int((static_cast<double>(m_floatpixels[i]) + 1.0) / 2.0 * 65535.0);
+ if (value < 0)
+ value = 0;
+ else if (value > 65535)
+ value = 65535;
+
+ int integer = value / 256;
+ int fract = value % 256;
+
+ m_intfractpixels[i] = (uint8_t)integer;
+ m_intfractpixels[i + m_size * 4] = (uint8_t)fract;
+ }
+}
+
+//convert to 8 bits unsigned
+void CConvolutionKernel::ToUint8()
+{
+ m_uint8pixels = new uint8_t[m_size * 4];
+
+ for (int i = 0; i < m_size * 4; i++)
+ {
+ int value = MathUtils::round_int((static_cast<double>(m_floatpixels[i]) * 0.5 + 0.5) * 255.0);
+ if (value < 0)
+ value = 0;
+ else if (value > 255)
+ value = 255;
+
+ m_uint8pixels[i] = (uint8_t)value;
+ }
+}
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConvolutionKernels.h b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConvolutionKernels.h
new file mode 100644
index 0000000..d39e0c8
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ConvolutionKernels.h
@@ -0,0 +1,47 @@
+/*
+ * 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/VideoSettings.h"
+
+#include <stdint.h>
+
+class CConvolutionKernel
+{
+ public:
+ CConvolutionKernel(ESCALINGMETHOD method, int size);
+ ~CConvolutionKernel();
+
+ int GetSize() { return m_size; }
+ float* GetFloatPixels() { return m_floatpixels; }
+ uint8_t* GetIntFractPixels() { return m_intfractpixels; }
+ uint8_t* GetUint8Pixels() { return m_uint8pixels; }
+
+ private:
+ CConvolutionKernel(const CConvolutionKernel&) = delete;
+ CConvolutionKernel& operator=(const CConvolutionKernel&) = delete;
+ void Lanczos2();
+ void Lanczos3Fast();
+ void Lanczos3();
+ void Spline36Fast();
+ void Spline36();
+ void Bicubic(double B, double C);
+
+ static double LanczosWeight(double x, double radius);
+ static double Spline36Weight(double x);
+ static double BicubicWeight(double x, double B, double C);
+
+ void ToIntFract();
+ void ToUint8();
+
+ int m_size;
+ float* m_floatpixels;
+ uint8_t* m_intfractpixels;
+ uint8_t* m_uint8pixels;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/GLSLOutput.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/GLSLOutput.cpp
new file mode 100644
index 0000000..e70cd9d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/GLSLOutput.cpp
@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2007 d4rk
+ * Copyright (C) 2007-2018 Team Kodi
+ * Copyright (C) 2015 Lauri Mylläri
+ * 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 "GLSLOutput.h"
+
+#include "dither.h"
+#include "utils/log.h"
+
+#include "system_gl.h"
+
+using namespace Shaders;
+
+GLSLOutput::GLSLOutput(int texunit,
+ bool useDithering,
+ unsigned int ditherDepth,
+ bool fullrange,
+ GLuint clutTex,
+ int clutSize)
+ : m_tCLUTTex(clutTex)
+{
+ // set member variable initial values
+ m_1stTexUnit = texunit;
+ m_uDither = m_1stTexUnit+0;
+ m_uCLUT = m_1stTexUnit+1;
+
+ m_dither = useDithering;
+ m_ditherDepth = ditherDepth;
+ m_fullRange = fullrange;
+ // make sure CMS is enabled - this allows us to keep the texture
+ // around to quickly switch between CMS on and off
+ m_3DLUT = clutTex > 0;
+ m_uCLUTSize = clutSize;
+}
+
+std::string GLSLOutput::GetDefines()
+{
+ std::string defines;
+ if (m_dither)
+ defines += "#define XBMC_DITHER\n";
+ if (m_fullRange)
+ defines += "#define XBMC_FULLRANGE\n";
+ if (m_3DLUT)
+ defines += "#define KODI_3DLUT\n";
+ return defines;
+}
+
+void GLSLOutput::OnCompiledAndLinked(GLuint programHandle)
+{
+ FreeTextures();
+
+ // get uniform locations
+ // dithering
+ if (m_dither)
+ {
+ m_hDither = glGetUniformLocation(programHandle, "m_dither");
+ m_hDitherQuant = glGetUniformLocation(programHandle, "m_ditherquant");
+ m_hDitherSize = glGetUniformLocation(programHandle, "m_dithersize");
+ }
+ // 3DLUT
+ if (m_3DLUT)
+ {
+ m_hCLUT = glGetUniformLocation(programHandle, "m_CLUT");
+ m_hCLUTSize = glGetUniformLocation(programHandle, "m_CLUTsize");
+ }
+
+ if (m_dither)
+ {
+ //! @todo create a dither pattern
+
+ // create a dither texture
+ glGenTextures(1, &m_tDitherTex);
+ if ( m_tDitherTex <= 0 )
+ {
+ CLog::Log(LOGERROR, "Error creating dither texture");
+ return;
+ }
+ // bind and set texture parameters
+ glActiveTexture(GL_TEXTURE0 + m_uDither);
+ glBindTexture(GL_TEXTURE_2D, m_tDitherTex);
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+
+ // load dither texture data
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_R16, dither_size, dither_size, 0, GL_RED, GL_UNSIGNED_SHORT, dither_matrix);
+ }
+
+ glActiveTexture(GL_TEXTURE0);
+
+ VerifyGLState();
+}
+
+bool GLSLOutput::OnEnabled()
+{
+
+ if (m_dither)
+ {
+ // set texture units
+ glUniform1i(m_hDither, m_uDither);
+ VerifyGLState();
+
+ // bind textures
+ glActiveTexture(GL_TEXTURE0 + m_uDither);
+ glBindTexture(GL_TEXTURE_2D, m_tDitherTex);
+ glActiveTexture(GL_TEXTURE0);
+ VerifyGLState();
+
+ // dither settings
+ glUniform1f(m_hDitherQuant, (1<<m_ditherDepth)-1.0);
+ VerifyGLState();
+ glUniform2f(m_hDitherSize, dither_size, dither_size);
+ VerifyGLState();
+ }
+
+ if (m_3DLUT)
+ {
+ // set texture units
+ glUniform1i(m_hCLUT, m_uCLUT);
+ glUniform1f(m_hCLUTSize, m_uCLUTSize);
+ VerifyGLState();
+
+ // bind textures
+ glActiveTexture(GL_TEXTURE0 + m_uCLUT);
+ glBindTexture(GL_TEXTURE_3D, m_tCLUTTex);
+ glActiveTexture(GL_TEXTURE0);
+ VerifyGLState();
+ }
+
+ VerifyGLState();
+ return true;
+}
+
+void GLSLOutput::OnDisabled()
+{
+ // disable textures
+ if (m_dither)
+ {
+ glActiveTexture(GL_TEXTURE0 + m_uDither);
+ }
+ if (m_3DLUT)
+ {
+ glActiveTexture(GL_TEXTURE0 + m_uCLUT);
+ glDisable(GL_TEXTURE_3D);
+ }
+ glActiveTexture(GL_TEXTURE0);
+ VerifyGLState();
+}
+
+void GLSLOutput::Free()
+{
+ FreeTextures();
+}
+
+void GLSLOutput::FreeTextures()
+{
+ if (m_tDitherTex)
+ {
+ glDeleteTextures(1, &m_tDitherTex);
+ m_tDitherTex = 0;
+ }
+}
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/GLSLOutput.h b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/GLSLOutput.h
new file mode 100644
index 0000000..3f2bd9f
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/GLSLOutput.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2007-2018 Team Kodi
+ * Copyright (C) 2015 Lauri Mylläri
+ * 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 "utils/GLUtils.h"
+
+#include <string>
+
+namespace Shaders
+{
+ class GLSLOutput
+ {
+ public:
+ // take the 1st available texture unit as a parameter
+ GLSLOutput(
+ int texunit,
+ bool useDithering,
+ unsigned int ditherDepth,
+ bool fullrange,
+ GLuint clutTex,
+ int clutSize);
+ std::string GetDefines();
+ void OnCompiledAndLinked(GLuint programHandle);
+ bool OnEnabled();
+ void OnDisabled();
+ void Free();
+
+ private:
+ void FreeTextures();
+
+ bool m_dither;
+ unsigned int m_ditherDepth;
+ bool m_fullRange;
+ bool m_3DLUT;
+ // first texture unit available to us
+ int m_1stTexUnit;
+ int m_uDither;
+ int m_uCLUT;
+ int m_uCLUTSize;
+
+ // defines
+
+ // attribute locations
+ GLint m_hDither = -1;
+ GLint m_hDitherQuant = -1;
+ GLint m_hDitherSize = -1;
+ GLint m_hCLUT = -1;
+ GLint m_hCLUTSize = -1;
+
+ // textures
+ GLuint m_tDitherTex = 0;
+ GLuint m_tCLUTTex;
+ };
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ShaderFormats.h b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ShaderFormats.h
new file mode 100644
index 0000000..f5d03a2
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/ShaderFormats.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2007-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 "utils/Map.h"
+
+#include <fmt/format.h>
+
+enum EShaderFormat
+{
+ SHADER_NONE,
+ SHADER_YV12,
+ SHADER_YV12_9,
+ SHADER_YV12_10,
+ SHADER_YV12_12,
+ SHADER_YV12_14,
+ SHADER_YV12_16,
+ SHADER_NV12,
+ SHADER_YUY2,
+ SHADER_UYVY,
+ SHADER_NV12_RRG,
+ SHADER_MAX,
+};
+
+template<>
+struct fmt::formatter<EShaderFormat> : fmt::formatter<std::string_view>
+{
+ template<typename FormatContext>
+ constexpr auto format(const EShaderFormat& shaderFormat, FormatContext& ctx)
+ {
+ const auto it = shaderFormatMap.find(shaderFormat);
+ if (it == shaderFormatMap.cend())
+ throw std::range_error("no shader format string found");
+
+ return fmt::formatter<string_view>::format(it->second, ctx);
+ }
+
+private:
+ static constexpr auto shaderFormatMap = make_map<EShaderFormat, std::string_view>({
+ {SHADER_NONE, "none"},
+ {SHADER_YV12, "YV12"},
+ {SHADER_YV12_9, "YV12 9bit"},
+ {SHADER_YV12_10, "YV12 10bit"},
+ {SHADER_YV12_12, "YV12 12bit"},
+ {SHADER_YV12_14, "YV12 14bit"},
+ {SHADER_YV12_16, "YV12 16bit"},
+ {SHADER_NV12, "NV12"},
+ {SHADER_YUY2, "YUY2"},
+ {SHADER_UYVY, "UYVY"},
+ {SHADER_NV12_RRG, "NV12 red/red/green"},
+ });
+
+ static_assert(SHADER_MAX == shaderFormatMap.size(),
+ "shaderFormatMap doesn't match the size of EShaderFormat, did you forget to "
+ "add/remove a mapping?");
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGL.cpp
new file mode 100644
index 0000000..0bfa324
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGL.cpp
@@ -0,0 +1,266 @@
+/*
+ * Copyright (c) 2007 d4rk
+ * Copyright (C) 2007-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 "VideoFilterShaderGL.h"
+
+#include "ConvolutionKernels.h"
+#include "ServiceBroker.h"
+#include "rendering/RenderSystem.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+
+#include <math.h>
+#include <string>
+
+#define TEXTARGET GL_TEXTURE_1D
+
+using namespace Shaders::GL;
+
+//////////////////////////////////////////////////////////////////////
+// BaseVideoFilterShader - base class for video filter shaders
+//////////////////////////////////////////////////////////////////////
+
+BaseVideoFilterShader::BaseVideoFilterShader()
+{
+ m_width = 1;
+ m_height = 1;
+ m_stepX = 0;
+ m_stepY = 0;
+ m_stretch = 0.0f;
+
+ VertexShader()->LoadSource("gl_videofilter_vertex.glsl");
+
+ PixelShader()->LoadSource("gl_videofilter_frag.glsl");
+}
+
+BaseVideoFilterShader::~BaseVideoFilterShader()
+{
+ Free();
+}
+
+//////////////////////////////////////////////////////////////////////
+// ConvolutionFilterShader - base class for video filter shaders
+//////////////////////////////////////////////////////////////////////
+
+ConvolutionFilterShader::ConvolutionFilterShader(ESCALINGMETHOD method, bool stretch, GLSLOutput *output)
+{
+ m_method = method;
+
+ std::string shadername;
+ std::string defines;
+
+ m_floattex = CServiceBroker::GetRenderSystem()->IsExtSupported("GL_ARB_texture_float");
+
+ if (m_method == VS_SCALINGMETHOD_CUBIC_B_SPLINE ||
+ m_method == VS_SCALINGMETHOD_CUBIC_MITCHELL ||
+ m_method == VS_SCALINGMETHOD_CUBIC_CATMULL ||
+ m_method == VS_SCALINGMETHOD_CUBIC_0_075 ||
+ m_method == VS_SCALINGMETHOD_CUBIC_0_1 ||
+ m_method == VS_SCALINGMETHOD_LANCZOS2 ||
+ m_method == VS_SCALINGMETHOD_SPLINE36_FAST ||
+ m_method == VS_SCALINGMETHOD_LANCZOS3_FAST)
+ {
+ shadername = "gl_convolution-4x4.glsl";
+
+ if (m_floattex)
+ m_internalformat = GL_RGBA16F;
+ else
+ m_internalformat = GL_RGBA;
+ }
+ else if (m_method == VS_SCALINGMETHOD_SPLINE36 ||
+ m_method == VS_SCALINGMETHOD_LANCZOS3)
+ {
+ shadername = "gl_convolution-6x6.glsl";
+
+ if (m_floattex)
+ m_internalformat = GL_RGB16F;
+ else
+ m_internalformat = GL_RGB;
+ }
+
+ if (m_floattex)
+ defines = "#define HAS_FLOAT_TEXTURE\n";
+
+ //don't compile in stretch support when it's not needed
+ if (stretch)
+ defines += "#define XBMC_STRETCH 1\n";
+ else
+ defines += "#define XBMC_STRETCH 0\n";
+
+ // get defines from the output stage if used
+ m_glslOutput = output;
+ if (m_glslOutput) {
+ defines += m_glslOutput->GetDefines();
+ }
+
+ CLog::Log(LOGDEBUG, "GL: using scaling method: {}", m_method);
+ CLog::Log(LOGDEBUG, "GL: using shader: {}", shadername);
+
+ PixelShader()->LoadSource(shadername, defines);
+ PixelShader()->AppendSource("gl_output.glsl");
+}
+
+ConvolutionFilterShader::~ConvolutionFilterShader()
+{
+ Free();
+ delete m_glslOutput;
+}
+
+void ConvolutionFilterShader::OnCompiledAndLinked()
+{
+ // obtain shader attribute handles on successful compilation
+ m_hSourceTex = glGetUniformLocation(ProgramHandle(), "img");
+ m_hStepXY = glGetUniformLocation(ProgramHandle(), "stepxy");
+ m_hKernTex = glGetUniformLocation(ProgramHandle(), "kernelTex");
+ m_hStretch = glGetUniformLocation(ProgramHandle(), "m_stretch");
+ m_hAlpha = glGetUniformLocation(ProgramHandle(), "m_alpha");
+ m_hProj = glGetUniformLocation(ProgramHandle(), "m_proj");
+ m_hModel = glGetUniformLocation(ProgramHandle(), "m_model");
+ m_hVertex = glGetAttribLocation(ProgramHandle(), "m_attrpos");
+ m_hCoord = glGetAttribLocation(ProgramHandle(), "m_attrcord");
+
+ CConvolutionKernel kernel(m_method, 256);
+
+ if (m_kernelTex1)
+ {
+ glDeleteTextures(1, &m_kernelTex1);
+ m_kernelTex1 = 0;
+ }
+
+ glGenTextures(1, &m_kernelTex1);
+
+ if ((m_kernelTex1<=0))
+ {
+ CLog::Log(LOGERROR, "GL: ConvolutionFilterShader: Error creating kernel texture");
+ return;
+ }
+
+ //make a kernel texture on GL_TEXTURE2 and set clamping and interpolation
+ //TEXTARGET is set to GL_TEXTURE_1D or GL_TEXTURE_2D
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(TEXTARGET, m_kernelTex1);
+ glTexParameteri(TEXTARGET, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(TEXTARGET, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(TEXTARGET, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(TEXTARGET, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ //if float textures are supported, we can load the kernel as a float texture
+ //if not we load it as 8 bit unsigned which gets converted back to float in the shader
+ GLenum format;
+ GLvoid* data;
+ if (m_floattex)
+ {
+ format = GL_FLOAT;
+ data = (GLvoid*)kernel.GetFloatPixels();
+ }
+ else
+ {
+ format = GL_UNSIGNED_BYTE;
+ data = (GLvoid*)kernel.GetUint8Pixels();
+ }
+
+ glTexImage1D(TEXTARGET, 0, m_internalformat, kernel.GetSize(), 0, GL_RGBA, format, data);
+
+ glActiveTexture(GL_TEXTURE0);
+
+ VerifyGLState();
+
+ if (m_glslOutput)
+ m_glslOutput->OnCompiledAndLinked(ProgramHandle());
+}
+
+bool ConvolutionFilterShader::OnEnabled()
+{
+ // set shader attributes once enabled
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(TEXTARGET, m_kernelTex1);
+
+ glActiveTexture(GL_TEXTURE0);
+ glUniform1i(m_hSourceTex, m_sourceTexUnit);
+ glUniform1i(m_hKernTex, 2);
+ glUniform2f(m_hStepXY, m_stepX, m_stepY);
+ glUniform1f(m_hStretch, m_stretch);
+ glUniform1f(m_hAlpha, m_alpha);
+
+ glUniformMatrix4fv(m_hProj, 1, GL_FALSE, m_proj);
+ glUniformMatrix4fv(m_hModel, 1, GL_FALSE, m_model);
+
+ VerifyGLState();
+ if (m_glslOutput) m_glslOutput->OnEnabled();
+ return true;
+}
+
+void ConvolutionFilterShader::OnDisabled()
+{
+ if (m_glslOutput) m_glslOutput->OnDisabled();
+}
+
+void ConvolutionFilterShader::Free()
+{
+ if (m_kernelTex1)
+ glDeleteTextures(1, &m_kernelTex1);
+ m_kernelTex1 = 0;
+ if (m_glslOutput) m_glslOutput->Free();
+ BaseVideoFilterShader::Free();
+}
+
+//////////////////////////////////////////////////////////////////////
+// StretchFilterShader - base class for video filter shaders
+//////////////////////////////////////////////////////////////////////
+
+StretchFilterShader::StretchFilterShader()
+{
+ PixelShader()->LoadSource("gl_stretch.glsl");
+}
+
+void StretchFilterShader::OnCompiledAndLinked()
+{
+ m_hSourceTex = glGetUniformLocation(ProgramHandle(), "img");
+ m_hStretch = glGetUniformLocation(ProgramHandle(), "m_stretch");
+ m_hAlpha = glGetUniformLocation(ProgramHandle(), "m_alpha");
+ m_hProj = glGetUniformLocation(ProgramHandle(), "m_proj");
+ m_hModel = glGetUniformLocation(ProgramHandle(), "m_model");
+ m_hVertex = glGetAttribLocation(ProgramHandle(), "m_attrpos");
+ m_hCoord = glGetAttribLocation(ProgramHandle(), "m_attrcord");
+}
+
+bool StretchFilterShader::OnEnabled()
+{
+ glUniform1i(m_hSourceTex, m_sourceTexUnit);
+ glUniform1f(m_hStretch, m_stretch);
+ glUniform1f(m_hAlpha, m_alpha);
+ glUniformMatrix4fv(m_hProj, 1, GL_FALSE, m_proj);
+ glUniformMatrix4fv(m_hModel, 1, GL_FALSE, m_model);
+ VerifyGLState();
+ return true;
+}
+
+//////////////////////////////////////////////////////////////////////
+// DefaultFilterShader - base class for video filter shaders
+//////////////////////////////////////////////////////////////////////
+
+void DefaultFilterShader::OnCompiledAndLinked()
+{
+ m_hSourceTex = glGetUniformLocation(ProgramHandle(), "img");
+ m_hProj = glGetUniformLocation(ProgramHandle(), "m_proj");
+ m_hModel = glGetUniformLocation(ProgramHandle(), "m_model");
+ m_hVertex = glGetAttribLocation(ProgramHandle(), "m_attrpos");
+ m_hCoord = glGetAttribLocation(ProgramHandle(), "m_attrcord");
+ m_hAlpha = glGetUniformLocation(ProgramHandle(), "m_alpha");
+}
+
+bool DefaultFilterShader::OnEnabled()
+{
+ glUniform1i(m_hSourceTex, m_sourceTexUnit);
+ glUniform1f(m_hAlpha, m_alpha);
+ glUniformMatrix4fv(m_hProj, 1, GL_FALSE, m_proj);
+ glUniformMatrix4fv(m_hModel, 1, GL_FALSE, m_model);
+ VerifyGLState();
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGL.h b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGL.h
new file mode 100644
index 0000000..b23d4e2
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGL.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2007-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 "GLSLOutput.h"
+#include "cores/VideoSettings.h"
+#include "guilib/Shader.h"
+
+#include "system_gl.h"
+
+namespace Shaders {
+
+namespace GL
+{
+
+class BaseVideoFilterShader : public CGLSLShaderProgram
+{
+public:
+ BaseVideoFilterShader();
+ ~BaseVideoFilterShader() override;
+ virtual bool GetTextureFilter(GLint& filter) { return false; }
+
+ void SetSourceTexture(GLint ytex) { m_sourceTexUnit = ytex; }
+ void SetWidth(int w)
+ {
+ m_width = w;
+ m_stepX = w > 0 ? 1.0f / w : 0;
+ }
+ void SetHeight(int h)
+ {
+ m_height = h;
+ m_stepY = h > 0 ? 1.0f / h : 0;
+ }
+ void SetNonLinStretch(float stretch) { m_stretch = stretch; }
+ void SetAlpha(GLfloat alpha) { m_alpha = alpha; }
+
+ GLint GetVertexLoc() { return m_hVertex; }
+ GLint GetCoordLoc() { return m_hCoord; }
+
+ void SetMatrices(const GLfloat* p, const GLfloat* m)
+ {
+ m_proj = p;
+ m_model = m;
+ }
+
+protected:
+ int m_width;
+ int m_height;
+ float m_stepX;
+ float m_stepY;
+ float m_stretch;
+ GLfloat m_alpha;
+ GLint m_sourceTexUnit = 0;
+ const GLfloat* m_proj = nullptr;
+ const GLfloat* m_model = nullptr;
+
+ // shader attribute handles
+ GLint m_hSourceTex = 0;
+ GLint m_hStepXY = 0;
+ GLint m_hStretch = -1;
+ GLint m_hAlpha = -1;
+ GLint m_hVertex = -1;
+ GLint m_hCoord = -1;
+ GLint m_hProj = -1;
+ GLint m_hModel = -1;
+ };
+
+ class ConvolutionFilterShader : public BaseVideoFilterShader
+ {
+ public:
+ ConvolutionFilterShader(ESCALINGMETHOD method, bool stretch, GLSLOutput *output=NULL);
+ ~ConvolutionFilterShader() override;
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+ void OnDisabled() override;
+ void Free();
+
+ bool GetTextureFilter(GLint& filter) override { filter = GL_NEAREST; return true; }
+
+ protected:
+ // kernel textures
+ GLuint m_kernelTex1 = 0;
+
+ // shader handles to kernel textures
+ GLint m_hKernTex;
+
+ ESCALINGMETHOD m_method;
+ bool m_floattex; //if float textures are supported
+ GLint m_internalformat;
+
+ GLSLOutput* m_glslOutput;
+ };
+
+ class StretchFilterShader : public BaseVideoFilterShader
+ {
+ public:
+ StretchFilterShader();
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+ };
+
+ class DefaultFilterShader : public BaseVideoFilterShader
+ {
+ public:
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+ };
+
+ } // namespace GL
+} // end namespace
+
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGLES.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGLES.cpp
new file mode 100644
index 0000000..4f6ffc0
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGLES.cpp
@@ -0,0 +1,212 @@
+/*
+ * Copyright (c) 2007 d4rk
+ * Copyright (C) 2007-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 "VideoFilterShaderGLES.h"
+
+#include "ConvolutionKernels.h"
+#include "ServiceBroker.h"
+#include "rendering/gles/RenderSystemGLES.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+
+#include <math.h>
+#include <string>
+
+using namespace Shaders::GLES;
+
+//////////////////////////////////////////////////////////////////////
+// BaseVideoFilterShader - base class for video filter shaders
+//////////////////////////////////////////////////////////////////////
+
+BaseVideoFilterShader::BaseVideoFilterShader()
+{
+ m_width = 1;
+ m_height = 1;
+ m_stepX = 0;
+ m_stepY = 0;
+
+ m_proj = nullptr;
+ m_model = nullptr;
+
+ VertexShader()->LoadSource("gles_videofilter.vert");
+
+ PixelShader()->LoadSource("gles_videofilter.frag");
+}
+
+void BaseVideoFilterShader::OnCompiledAndLinked()
+{
+ m_hVertex = glGetAttribLocation(ProgramHandle(), "m_attrpos");
+ m_hcoord = glGetAttribLocation(ProgramHandle(), "m_attrcord");
+ m_hAlpha = glGetUniformLocation(ProgramHandle(), "m_alpha");
+ m_hProj = glGetUniformLocation(ProgramHandle(), "m_proj");
+ m_hModel = glGetUniformLocation(ProgramHandle(), "m_model");
+}
+
+bool BaseVideoFilterShader::OnEnabled()
+{
+ glUniformMatrix4fv(m_hProj, 1, GL_FALSE, m_proj);
+ glUniformMatrix4fv(m_hModel, 1, GL_FALSE, m_model);
+ glUniform1f(m_hAlpha, m_alpha);
+ return true;
+}
+
+ConvolutionFilterShader::ConvolutionFilterShader(ESCALINGMETHOD method)
+{
+ m_method = method;
+
+ std::string shadername;
+ std::string defines;
+
+ if (CServiceBroker::GetRenderSystem()->IsExtSupported("GL_EXT_color_buffer_float"))
+ {
+ m_floattex = true;
+ }
+ else
+ {
+ m_floattex = false;
+ }
+
+ if (m_method == VS_SCALINGMETHOD_CUBIC_B_SPLINE ||
+ m_method == VS_SCALINGMETHOD_CUBIC_MITCHELL ||
+ m_method == VS_SCALINGMETHOD_CUBIC_CATMULL ||
+ m_method == VS_SCALINGMETHOD_CUBIC_0_075 ||
+ m_method == VS_SCALINGMETHOD_CUBIC_0_1 ||
+ m_method == VS_SCALINGMETHOD_LANCZOS2 ||
+ m_method == VS_SCALINGMETHOD_SPLINE36_FAST ||
+ m_method == VS_SCALINGMETHOD_LANCZOS3_FAST)
+ {
+ shadername = "gles_convolution-4x4.frag";
+ }
+ else if (m_method == VS_SCALINGMETHOD_SPLINE36 ||
+ m_method == VS_SCALINGMETHOD_LANCZOS3)
+ {
+ shadername = "gles_convolution-6x6.frag";
+ }
+
+ if (m_floattex)
+ {
+ m_internalformat = GL_RGBA16F_EXT;
+ defines = "#define HAS_FLOAT_TEXTURE\n";
+ }
+ else
+ {
+ m_internalformat = GL_RGBA;
+ }
+
+ CLog::Log(LOGDEBUG, "GLES: using scaling method: {}", m_method);
+ CLog::Log(LOGDEBUG, "GLES: using shader: {}", shadername);
+
+ PixelShader()->LoadSource(shadername, defines);
+}
+
+ConvolutionFilterShader::~ConvolutionFilterShader()
+{
+ Free();
+}
+
+void ConvolutionFilterShader::OnCompiledAndLinked()
+{
+ BaseVideoFilterShader::OnCompiledAndLinked();
+
+ // obtain shader attribute handles on successful compilation
+ m_hSourceTex = glGetUniformLocation(ProgramHandle(), "img");
+ m_hStepXY = glGetUniformLocation(ProgramHandle(), "stepxy");
+ m_hKernTex = glGetUniformLocation(ProgramHandle(), "kernelTex");
+
+ CConvolutionKernel kernel(m_method, 256);
+
+ if (m_kernelTex1)
+ {
+ glDeleteTextures(1, &m_kernelTex1);
+ m_kernelTex1 = 0;
+ }
+
+ glGenTextures(1, &m_kernelTex1);
+
+ if ((m_kernelTex1<=0))
+ {
+ CLog::Log(LOGERROR, "GL: ConvolutionFilterShader: Error creating kernel texture");
+ return;
+ }
+
+ //make a kernel texture on GL_TEXTURE2 and set clamping and interpolation
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, m_kernelTex1);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ //if float textures are supported, we can load the kernel as a float texture
+ //if not we load it as 8 bit unsigned which gets converted back to float in the shader
+ GLenum format;
+ GLvoid* data;
+ if (m_floattex)
+ {
+ format = GL_FLOAT;
+ data = (GLvoid*)kernel.GetFloatPixels();
+ }
+ else
+ {
+ format = GL_UNSIGNED_BYTE;
+ data = (GLvoid*)kernel.GetUint8Pixels();
+ }
+
+ //upload as 2D texture with height of 1
+ glTexImage2D(GL_TEXTURE_2D, 0, m_internalformat, kernel.GetSize(), 1, 0, GL_RGBA, format, data);
+
+ glActiveTexture(GL_TEXTURE0);
+
+ VerifyGLState();
+}
+
+bool ConvolutionFilterShader::OnEnabled()
+{
+ BaseVideoFilterShader::OnEnabled();
+
+ // set shader attributes once enabled
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, m_kernelTex1);
+
+ glActiveTexture(GL_TEXTURE0);
+ glUniform1i(m_hSourceTex, m_sourceTexUnit);
+ glUniform1i(m_hKernTex, 2);
+ glUniform2f(m_hStepXY, m_stepX, m_stepY);
+ VerifyGLState();
+
+ return true;
+}
+
+void ConvolutionFilterShader::OnDisabled()
+{
+}
+
+void ConvolutionFilterShader::Free()
+{
+ if (m_kernelTex1)
+ glDeleteTextures(1, &m_kernelTex1);
+ m_kernelTex1 = 0;
+ BaseVideoFilterShader::Free();
+}
+
+void DefaultFilterShader::OnCompiledAndLinked()
+{
+ BaseVideoFilterShader::OnCompiledAndLinked();
+
+ m_hSourceTex = glGetUniformLocation(ProgramHandle(), "img");
+}
+
+bool DefaultFilterShader::OnEnabled()
+{
+ BaseVideoFilterShader::OnEnabled();
+
+ glUniform1i(m_hSourceTex, m_sourceTexUnit);
+ VerifyGLState();
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGLES.h b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGLES.h
new file mode 100644
index 0000000..8cb424b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/VideoFilterShaderGLES.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2007-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/VideoSettings.h"
+#include "guilib/Shader.h"
+
+#include "system_gl.h"
+
+namespace Shaders {
+
+namespace GLES
+{
+class BaseVideoFilterShader : public CGLSLShaderProgram
+{
+public:
+ BaseVideoFilterShader();
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+ virtual void SetSourceTexture(GLint ytex) { m_sourceTexUnit = ytex; }
+ virtual void SetWidth(int w)
+ {
+ m_width = w;
+ m_stepX = w > 0 ? 1.0f / w : 0;
+ }
+ virtual void SetHeight(int h)
+ {
+ m_height = h;
+ m_stepY = h > 0 ? 1.0f / h : 0;
+ }
+ virtual bool GetTextureFilter(GLint& filter) { return false; }
+ virtual GLint GetVertexLoc() { return m_hVertex; }
+ virtual GLint GetcoordLoc() { return m_hcoord; }
+ virtual void SetMatrices(const GLfloat* p, const GLfloat* m)
+ {
+ m_proj = p;
+ m_model = m;
+ }
+ virtual void SetAlpha(GLfloat alpha) { m_alpha = alpha; }
+
+protected:
+ int m_width;
+ int m_height;
+ float m_stepX;
+ float m_stepY;
+ GLint m_sourceTexUnit = 0;
+
+ // shader attribute handles
+ GLint m_hSourceTex = 0;
+ GLint m_hStepXY = 0;
+
+ GLint m_hVertex = -1;
+ GLint m_hcoord = -1;
+ GLint m_hProj = -1;
+ GLint m_hModel = -1;
+ GLint m_hAlpha = -1;
+
+ const GLfloat* m_proj;
+ const GLfloat* m_model;
+ GLfloat m_alpha = -1;
+ };
+
+ class ConvolutionFilterShader : public BaseVideoFilterShader
+ {
+ public:
+ ConvolutionFilterShader(ESCALINGMETHOD method);
+ ~ConvolutionFilterShader() override;
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+ void OnDisabled() override;
+ void Free();
+
+ bool GetTextureFilter(GLint& filter) override { filter = GL_NEAREST; return true; }
+
+ protected:
+ // kernel textures
+ GLuint m_kernelTex1 = 0;
+
+ // shader handles to kernel textures
+ GLint m_hKernTex = -1;
+
+ ESCALINGMETHOD m_method;
+ bool m_floattex; //if float textures are supported
+ GLint m_internalformat;
+ };
+
+ class DefaultFilterShader : public BaseVideoFilterShader
+ {
+ public:
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+ };
+
+ } // namespace GLES
+} // end namespace
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/WinVideoFilter.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/WinVideoFilter.cpp
new file mode 100644
index 0000000..511ed51
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/WinVideoFilter.cpp
@@ -0,0 +1,1228 @@
+/*
+ * Copyright (C) 2007-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 "WinVideoFilter.h"
+
+#include "ConvolutionKernels.h"
+#include "Util.h"
+#include "VideoRenderers/windows/RendererBase.h"
+#include "cores/VideoPlayer/VideoRenderers/VideoShaders/dither.h"
+#include "filesystem/File.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/MemUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include "platform/win32/WIN32Util.h"
+
+#include <map>
+
+#include <DirectXPackedVector.h>
+
+using namespace DirectX::PackedVector;
+using namespace Microsoft::WRL;
+
+//===================================================================
+
+bool CWinShader::CreateVertexBuffer(unsigned int count, unsigned int size)
+{
+ if (!m_vb.Create(D3D11_BIND_VERTEX_BUFFER, count, size, DXGI_FORMAT_UNKNOWN, D3D11_USAGE_DYNAMIC))
+ return false;
+
+ uint16_t id[4] = { 3, 0, 2, 1 };
+ if (!m_ib.Create(D3D11_BIND_INDEX_BUFFER, ARRAYSIZE(id), sizeof(uint16_t), DXGI_FORMAT_R16_UINT, D3D11_USAGE_IMMUTABLE, id))
+ return false;
+
+ m_vbsize = count * size;
+ m_vertsize = size;
+
+ return true;
+}
+
+bool CWinShader::CreateInputLayout(D3D11_INPUT_ELEMENT_DESC *layout, unsigned numElements)
+{
+ D3DX11_PASS_DESC desc = {};
+ if (FAILED(m_effect.Get()->GetTechniqueByIndex(0)->GetPassByIndex(0)->GetDesc(&desc)))
+ {
+ CLog::LogF(LOGERROR, "Failed to get first pass description.");
+ return false;
+ }
+
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ return SUCCEEDED(pDevice->CreateInputLayout(layout, numElements, desc.pIAInputSignature, desc.IAInputSignatureSize, &m_inputLayout));
+}
+
+void CWinShader::SetTarget(CD3DTexture* target)
+{
+ m_target = target;
+ if (m_target)
+ DX::DeviceResources::Get()->GetD3DContext()->OMSetRenderTargets(1, target->GetAddressOfRTV(), nullptr);
+}
+
+bool CWinShader::LockVertexBuffer(void **data)
+{
+ if (!m_vb.Map(data))
+ {
+ CLog::LogF(LOGERROR, "failed to lock vertex buffer");
+ return false;
+ }
+ return true;
+}
+
+bool CWinShader::UnlockVertexBuffer()
+{
+ if (!m_vb.Unmap())
+ {
+ CLog::LogF(LOGERROR, "failed to unlock vertex buffer");
+ return false;
+ }
+ return true;
+}
+
+bool CWinShader::LoadEffect(const std::string& filename, DefinesMap* defines)
+{
+ CLog::LogF(LOGDEBUG, "loading shader {}", filename);
+
+ XFILE::CFileStream file;
+ if(!file.Open(filename))
+ {
+ CLog::LogF(LOGERROR, "failed to open file {}", filename);
+ return false;
+ }
+
+ std::string pStrEffect;
+ getline(file, pStrEffect, '\0');
+
+ if (!m_effect.Create(pStrEffect, defines))
+ {
+ CLog::LogF(LOGERROR, "{} failed", pStrEffect);
+ return false;
+ }
+
+ return true;
+}
+
+bool CWinShader::Execute(const std::vector<CD3DTexture*> &targets, unsigned int vertexIndexStep)
+{
+ ID3D11DeviceContext* pContext = DX::DeviceResources::Get()->GetD3DContext();
+ ComPtr<ID3D11RenderTargetView> oldRT;
+
+ // The render target will be overridden: save the caller's original RT
+ if (!targets.empty())
+ pContext->OMGetRenderTargets(1, &oldRT, nullptr);
+
+ unsigned cPasses;
+ if (!m_effect.Begin(&cPasses, 0))
+ {
+ CLog::LogF(LOGERROR, "failed to begin d3d effect");
+ return false;
+ }
+
+ ID3D11Buffer* vertexBuffer = m_vb.Get();
+ ID3D11Buffer* indexBuffer = m_ib.Get();
+ unsigned int stride = m_vb.GetStride();
+ unsigned int offset = 0;
+ pContext->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);
+ pContext->IASetIndexBuffer(indexBuffer, m_ib.GetFormat(), 0);
+ pContext->IASetInputLayout(m_inputLayout.Get());
+ pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
+
+ for (unsigned iPass = 0; iPass < cPasses; iPass++)
+ {
+ SetTarget(targets.size() > iPass ? targets.at(iPass) : nullptr);
+ SetStepParams(iPass);
+
+ if (!m_effect.BeginPass(iPass))
+ {
+ CLog::LogF(LOGERROR, "failed to begin d3d effect pass");
+ break;
+ }
+
+ pContext->DrawIndexed(4, 0, iPass * vertexIndexStep);
+
+ if (!m_effect.EndPass())
+ CLog::LogF(LOGERROR, "failed to end d3d effect pass");
+
+ CD3DHelper::PSClearShaderResources(pContext);
+ }
+ if (!m_effect.End())
+ CLog::LogF(LOGERROR, "failed to end d3d effect");
+
+ if (oldRT)
+ pContext->OMSetRenderTargets(1, oldRT.GetAddressOf(), nullptr);
+
+ return true;
+}
+
+//==================================================================================
+
+void COutputShader::ApplyEffectParameters(CD3DEffect &effect, unsigned sourceWidth, unsigned sourceHeight)
+{
+ if (m_useLut && HasLUT())
+ {
+ float lut_params[2] = {(m_lutSize - 1) / static_cast<float>(m_lutSize),
+ 0.5f / static_cast<float>(m_lutSize)};
+ effect.SetResources("m_LUT", m_pLUTView.GetAddressOf(), 1);
+ effect.SetFloatArray("m_LUTParams", lut_params, 2);
+ }
+ if (m_useDithering)
+ {
+ float ditherParams[3] =
+ {
+ static_cast<float>(sourceWidth) / dither_size,
+ static_cast<float>(sourceHeight) / dither_size,
+ static_cast<float>(1 << m_ditherDepth) - 1.0f
+ };
+ effect.SetResources("m_ditherMatrix", m_pDitherView.GetAddressOf(), 1);
+ effect.SetFloatArray("m_ditherParams", ditherParams, 3);
+ }
+ if (m_toneMapping && m_toneMappingMethod == VS_TONEMAPMETHOD_REINHARD)
+ {
+ const float def_param = log10(100.0f) / log10(600.0f); // 600 nits --> 0.72
+ float param = def_param;
+
+ if (m_hasLightMetadata)
+ param = static_cast<float>(log10(100) / log10(m_lightMetadata.MaxCLL));
+ else if (m_hasDisplayMetadata && m_displayMetadata.has_luminance)
+ param = static_cast<float>(log10(100) / log10(m_displayMetadata.max_luminance.num /
+ m_displayMetadata.max_luminance.den));
+
+ // Sanity check
+ if (param < 0.1f || param > 5.0f)
+ param = def_param;
+
+ param *= m_toneMappingParam;
+
+ Matrix3x1 coefs = CConvertMatrix::GetRGBYuvCoefs(AVCOL_SPC_BT709);
+
+ effect.SetScalar("g_toneP1", param);
+ effect.SetFloatArray("g_coefsDst", coefs.data(), coefs.size());
+ m_toneMappingDebug = param;
+ }
+ else if (m_toneMapping && m_toneMappingMethod == VS_TONEMAPMETHOD_ACES)
+ {
+ float lumin = GetLuminanceValue();
+ effect.SetScalar("g_luminance", lumin);
+ effect.SetScalar("g_toneP1", m_toneMappingParam);
+ m_toneMappingDebug = lumin;
+ }
+ else if (m_toneMapping && m_toneMappingMethod == VS_TONEMAPMETHOD_HABLE)
+ {
+ float lumin = GetLuminanceValue();
+ float lumin_factor = (10000.0f / lumin) * (2.0f / m_toneMappingParam);
+ float lumin_div100 = lumin / (100.0f * m_toneMappingParam);
+ effect.SetScalar("g_toneP1", lumin_factor);
+ effect.SetScalar("g_toneP2", lumin_div100);
+ m_toneMappingDebug = lumin;
+ }
+}
+
+float COutputShader::GetLuminanceValue() const
+{
+ float lum1 = 400.0f; // default for bad quality HDR-PQ sources (with no metadata)
+ float lum2 = lum1;
+ float lum3 = lum1;
+
+ if (m_hasLightMetadata)
+ {
+ uint16_t lum = m_displayMetadata.max_luminance.num / m_displayMetadata.max_luminance.den;
+ if (m_lightMetadata.MaxCLL >= lum)
+ {
+ lum1 = static_cast<float>(lum);
+ lum2 = static_cast<float>(m_lightMetadata.MaxCLL);
+ }
+ else
+ {
+ lum1 = static_cast<float>(m_lightMetadata.MaxCLL);
+ lum2 = static_cast<float>(lum);
+ }
+ lum3 = static_cast<float>(m_lightMetadata.MaxFALL);
+ lum1 = (lum1 * 0.5f) + (lum2 * 0.2f) + (lum3 * 0.3f);
+ }
+ else if (m_hasDisplayMetadata && m_displayMetadata.has_luminance)
+ {
+ uint16_t lum = m_displayMetadata.max_luminance.num / m_displayMetadata.max_luminance.den;
+ lum1 = static_cast<float>(lum);
+ }
+
+ return lum1;
+}
+
+void COutputShader::GetDefines(DefinesMap& map) const
+{
+ if (m_useLut)
+ {
+ map["KODI_3DLUT"] = "";
+ }
+ if (m_useDithering)
+ {
+ map["KODI_DITHER"] = "";
+ }
+ if (m_toneMapping && m_toneMappingMethod == VS_TONEMAPMETHOD_REINHARD)
+ {
+ map["KODI_TONE_MAPPING_REINHARD"] = "";
+ }
+ else if (m_toneMapping && m_toneMappingMethod == VS_TONEMAPMETHOD_ACES)
+ {
+ map["KODI_TONE_MAPPING_ACES"] = "";
+ }
+ else if (m_toneMapping && m_toneMappingMethod == VS_TONEMAPMETHOD_HABLE)
+ {
+ map["KODI_TONE_MAPPING_HABLE"] = "";
+ }
+ if (m_useHLGtoPQ)
+ {
+ map["KODI_HLG_TO_PQ"] = "";
+ }
+}
+
+bool COutputShader::Create(bool useLUT,
+ bool useDithering,
+ int ditherDepth,
+ bool toneMapping,
+ ETONEMAPMETHOD toneMethod,
+ bool HLGtoPQ)
+{
+ m_useLut = useLUT;
+ m_ditherDepth = ditherDepth;
+ m_toneMapping = toneMapping;
+ m_useHLGtoPQ = HLGtoPQ;
+ m_toneMappingMethod = toneMethod;
+
+ CWinShader::CreateVertexBuffer(4, sizeof(Vertex));
+
+ if (useDithering)
+ CreateDitherView();
+
+ DefinesMap defines;
+ defines["KODI_OUTPUT_T"] = "";
+ GetDefines(defines);
+
+ std::string effectString("special://xbmc/system/shaders/output_d3d.fx");
+
+ if (!LoadEffect(effectString, &defines))
+ {
+ CLog::LogF(LOGERROR, "Failed to load shader {}.", effectString);
+ return false;
+ }
+
+ // Create input layout
+ D3D11_INPUT_ELEMENT_DESC layout[] =
+ {
+ { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ };
+ return CWinShader::CreateInputLayout(layout, ARRAYSIZE(layout));
+}
+
+void COutputShader::Render(CD3DTexture& sourceTexture, CRect sourceRect, const CPoint points[4]
+ , CD3DTexture& target, unsigned range, float contrast, float brightness)
+{
+ PrepareParameters(sourceTexture.GetWidth(), sourceTexture.GetHeight(), sourceRect, points);
+ SetShaderParameters(sourceTexture, range, contrast, brightness);
+ Execute({ &target }, 4);
+}
+
+void COutputShader::Render(CD3DTexture& sourceTexture, CRect sourceRect, CRect destRect
+ , CD3DTexture& target, unsigned range, float contrast, float brightness)
+{
+ CPoint points[] =
+ {
+ { destRect.x1, destRect.y1 },
+ { destRect.x2, destRect.y1 },
+ { destRect.x2, destRect.y2 },
+ { destRect.x1, destRect.y2 },
+ };
+ Render(sourceTexture, sourceRect, points, target, range, contrast, brightness);
+}
+
+void COutputShader::SetLUT(int lutSize, ID3D11ShaderResourceView* pLUTView)
+{
+ m_lutSize = lutSize;
+ m_pLUTView = pLUTView;
+}
+
+void COutputShader::SetDisplayMetadata(bool hasDisplayMetadata, AVMasteringDisplayMetadata displayMetadata, bool hasLightMetadata, AVContentLightMetadata lightMetadata)
+{
+ m_hasDisplayMetadata = hasDisplayMetadata;
+ m_displayMetadata = displayMetadata;
+ m_hasLightMetadata = hasLightMetadata;
+ m_lightMetadata = lightMetadata;
+}
+
+void COutputShader::SetToneMapParam(ETONEMAPMETHOD method, float param)
+{
+ m_toneMappingMethod = method;
+ m_toneMappingParam = param;
+}
+
+bool COutputShader::CreateLUTView(int lutSize, uint16_t* lutData, bool isRGB, ID3D11ShaderResourceView** ppLUTView)
+{
+ if (!lutSize || !lutData)
+ return false;
+
+ uint16_t* cData;
+ if (isRGB)
+ {
+ // repack data to RGBA
+ const unsigned samples = lutSize * lutSize * lutSize;
+ cData = reinterpret_cast<uint16_t*>(KODI::MEMORY::AlignedMalloc(samples * sizeof(uint16_t) * 4, 16));
+ auto rgba = static_cast<uint16_t*>(cData);
+ for (unsigned i = 0; i < samples - 1; ++i, rgba += 4, lutData += 3)
+ {
+ *reinterpret_cast<uint64_t*>(rgba) = *reinterpret_cast<uint64_t*>(lutData);
+ }
+ // and last one
+ rgba[0] = lutData[0]; rgba[1] = lutData[1]; rgba[2] = lutData[2]; rgba[3] = 0xFFFF;
+ }
+ else
+ cData = lutData;
+
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetImmediateContext();
+ CD3D11_TEXTURE3D_DESC txDesc(DXGI_FORMAT_R16G16B16A16_UNORM, lutSize, lutSize, lutSize, 1,
+ D3D11_BIND_SHADER_RESOURCE, D3D11_USAGE_IMMUTABLE);
+
+ ComPtr<ID3D11Texture3D> pLUTTex;
+ D3D11_SUBRESOURCE_DATA texData;
+ texData.pSysMem = cData;
+ texData.SysMemPitch = lutSize * sizeof(uint16_t) * 4;
+ texData.SysMemSlicePitch = texData.SysMemPitch * lutSize;
+
+ HRESULT hr = pDevice->CreateTexture3D(&txDesc, &texData, pLUTTex.GetAddressOf());
+ if (isRGB)
+ KODI::MEMORY::AlignedFree(cData);
+
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGDEBUG, "unable to create 3dlut texture cube.");
+ return false;
+ }
+
+ ComPtr<ID3D11ShaderResourceView> lutView;
+ CD3D11_SHADER_RESOURCE_VIEW_DESC srvDesc(D3D11_SRV_DIMENSION_TEXTURE3D, DXGI_FORMAT_R16G16B16A16_UNORM, 0, 1);
+ hr = pDevice->CreateShaderResourceView(pLUTTex.Get(), &srvDesc, &lutView);
+ pContext->Flush();
+
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGDEBUG, "unable to create view for 3dlut texture cube.");
+ return false;
+ }
+
+ *ppLUTView = lutView.Detach();
+ return true;
+}
+
+void COutputShader::PrepareParameters(unsigned sourceWidth, unsigned sourceHeight, CRect sourceRect, const CPoint points[4])
+{
+ bool changed = false;
+ for(int i= 0; i < 4 && !changed; ++i)
+ changed = points[i] != m_destPoints[i];
+
+ if (m_sourceWidth != sourceWidth || m_sourceHeight != sourceHeight
+ || m_sourceRect != sourceRect || changed)
+ {
+ m_sourceWidth = sourceWidth;
+ m_sourceHeight = sourceHeight;
+ m_sourceRect = sourceRect;
+ for (int i = 0; i < 4; ++i)
+ m_destPoints[i] = points[i];
+
+ Vertex* v;
+ CWinShader::LockVertexBuffer(reinterpret_cast<void**>(&v));
+
+ v[0].x = m_destPoints[0].x;
+ v[0].y = m_destPoints[0].y;
+ v[0].z = 0;
+ v[0].tu = m_sourceRect.x1 / m_sourceWidth;
+ v[0].tv = m_sourceRect.y1 / m_sourceHeight;
+
+ v[1].x = m_destPoints[1].x;
+ v[1].y = m_destPoints[1].y;
+ v[1].z = 0;
+ v[1].tu = m_sourceRect.x2 / m_sourceWidth;
+ v[1].tv = m_sourceRect.y1 / m_sourceHeight;
+
+ v[2].x = m_destPoints[2].x;
+ v[2].y = m_destPoints[2].y;
+ v[2].z = 0;
+ v[2].tu = m_sourceRect.x2 / m_sourceWidth;
+ v[2].tv = m_sourceRect.y2 / m_sourceHeight;
+
+ v[3].x = m_destPoints[3].x;
+ v[3].y = m_destPoints[3].y;
+ v[3].z = 0;
+ v[3].tu = m_sourceRect.x1 / m_sourceWidth;
+ v[3].tv = m_sourceRect.y2 / m_sourceHeight;
+
+ CWinShader::UnlockVertexBuffer();
+ }
+}
+
+void COutputShader::SetShaderParameters(CD3DTexture& sourceTexture, unsigned range, float contrast, float brightness)
+{
+ m_effect.SetTechnique("OUTPUT_T");
+ m_effect.SetResources("g_Texture", sourceTexture.GetAddressOfSRV(), 1);
+
+ unsigned numPorts = 1;
+ D3D11_VIEWPORT viewPort;
+ DX::DeviceResources::Get()->GetD3DContext()->RSGetViewports(&numPorts, &viewPort);
+ m_effect.SetFloatArray("g_viewPort", &viewPort.Width, 2);
+
+ float params[3] = { static_cast<float>(range), contrast, brightness };
+ m_effect.SetFloatArray("m_params", params, 3);
+
+ ApplyEffectParameters(m_effect, sourceTexture.GetWidth(), sourceTexture.GetHeight());
+}
+
+void COutputShader::CreateDitherView()
+{
+ ID3D11Device* pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+
+ CD3D11_TEXTURE2D_DESC txDesc(DXGI_FORMAT_R16_UNORM, dither_size, dither_size, 1, 1);
+ D3D11_SUBRESOURCE_DATA resData;
+ resData.pSysMem = dither_matrix;
+ resData.SysMemPitch = dither_size * sizeof(uint16_t);
+ resData.SysMemSlicePitch = resData.SysMemPitch * dither_size;
+
+ ComPtr<ID3D11Texture2D> pDitherTex;
+ HRESULT hr = pDevice->CreateTexture2D(&txDesc, &resData, &pDitherTex);
+
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGDEBUG, "unable to create dither texture cube.");
+ m_useDithering = false;
+ return;
+ }
+
+ CD3D11_SHADER_RESOURCE_VIEW_DESC srvDesc(D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R16_UNORM, 0, 1);
+ hr = pDevice->CreateShaderResourceView(pDitherTex.Get(), &srvDesc, &m_pDitherView);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGDEBUG, "unable to create view for dither texture cube.");
+ m_useDithering = false;
+ return;
+ }
+ m_useDithering = true;
+}
+
+std::string COutputShader::GetDebugInfo()
+{
+ std::string tone = "OFF";
+ std::string hlg = "OFF";
+ std::string lut = "OFF";
+ std::string dither = "OFF";
+
+ if (m_toneMapping)
+ {
+ std::string method;
+ switch (m_toneMappingMethod)
+ {
+ case VS_TONEMAPMETHOD_REINHARD:
+ method = "Reinhard";
+ break;
+ case VS_TONEMAPMETHOD_ACES:
+ method = "ACES";
+ break;
+ case VS_TONEMAPMETHOD_HABLE:
+ method = "Hable";
+ break;
+ }
+ tone = StringUtils::Format("ON ({}, {:.2f}, {:.2f}{})", method, m_toneMappingParam,
+ m_toneMappingDebug, (m_toneMappingMethod == 1) ? "" : " nits");
+ }
+
+ if (m_useHLGtoPQ)
+ hlg = "ON (peak 1000 nits)";
+
+ if (m_useLut)
+ lut = StringUtils::Format("ON (size {})", m_lutSize);
+
+ if (m_useDithering)
+ dither = StringUtils::Format("ON (depth {})", m_ditherDepth);
+
+ return StringUtils::Format("Tone mapping: {} | HLG to PQ: {} | 3D LUT: {} | Dithering: {}", tone,
+ hlg, lut, dither);
+}
+
+//==================================================================================
+
+bool CYUV2RGBShader::Create(AVPixelFormat fmt, AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries, const std::shared_ptr<COutputShader>& pOutputShader)
+{
+ m_format = fmt;
+ m_pOutShader = pOutputShader;
+
+ DefinesMap defines;
+
+ switch (fmt)
+ {
+ case AV_PIX_FMT_YUV420P:
+ case AV_PIX_FMT_YUV420P10:
+ case AV_PIX_FMT_YUV420P16:
+ defines["XBMC_YV12"] = "";
+ break;
+ case AV_PIX_FMT_D3D11VA_VLD:
+ case AV_PIX_FMT_NV12:
+ case AV_PIX_FMT_P010:
+ case AV_PIX_FMT_P016:
+ defines["XBMC_NV12"] = "";
+ // FL 9.x doesn't support DXGI_FORMAT_R8G8_UNORM, so we have to use SNORM and correct values in shader
+ if (!DX::Windowing()->IsFormatSupport(DXGI_FORMAT_R8G8_UNORM, D3D11_FORMAT_SUPPORT_TEXTURE2D))
+ defines["NV12_SNORM_UV"] = "";
+ break;
+ case AV_PIX_FMT_UYVY422:
+ defines["XBMC_UYVY"] = "";
+ break;
+ case AV_PIX_FMT_YUYV422:
+ defines["XBMC_YUY2"] = "";
+ break;
+ default:
+ return false;
+ }
+
+ if (srcPrimaries != dstPrimaries)
+ {
+ m_colorConversion = true;
+ defines["XBMC_COL_CONVERSION"] = "";
+ }
+
+ if (m_pOutShader)
+ m_pOutShader->GetDefines(defines);
+
+ std::string effectString = "special://xbmc/system/shaders/yuv2rgb_d3d.fx";
+
+ if(!LoadEffect(effectString, &defines))
+ {
+ CLog::LogF(LOGERROR, "Failed to load shader {}.", effectString);
+ return false;
+ }
+
+ CWinShader::CreateVertexBuffer(4, sizeof(Vertex));
+ // Create input layout
+ D3D11_INPUT_ELEMENT_DESC layout[] =
+ {
+ { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ { "TEXCOORD", 1, DXGI_FORMAT_R32G32_FLOAT, 0, 20, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ };
+
+ if (!CWinShader::CreateInputLayout(layout, ARRAYSIZE(layout)))
+ {
+ CLog::LogF(LOGERROR, "Failed to create input layout for Input Assembler.");
+ return false;
+ }
+
+ m_convMatrix.SetDestinationColorPrimaries(dstPrimaries).SetSourceColorPrimaries(srcPrimaries);
+
+ return true;
+}
+
+void CYUV2RGBShader::Render(CRect sourceRect, CPoint dest[], CRenderBuffer* videoBuffer, CD3DTexture& target)
+{
+ PrepareParameters(videoBuffer, sourceRect, dest);
+ SetShaderParameters(videoBuffer);
+ Execute({ &target }, 4);
+}
+
+void CYUV2RGBShader::SetParams(float contrast, float black, bool limited)
+{
+ m_convMatrix.SetDestinationContrast(contrast * 0.02f)
+ .SetDestinationBlack(black * 0.01f - 0.5f)
+ .SetDestinationLimitedRange(limited);
+}
+
+void CYUV2RGBShader::SetColParams(AVColorSpace colSpace, int bits, bool limited, int texBits)
+{
+ if (colSpace == AVCOL_SPC_UNSPECIFIED)
+ {
+ if (m_sourceWidth > 1024 || m_sourceHeight >= 600)
+ colSpace = AVCOL_SPC_BT709;
+ else
+ colSpace = AVCOL_SPC_BT470BG;
+ }
+
+ m_convMatrix.SetSourceColorSpace(colSpace)
+ .SetSourceBitDepth(bits)
+ .SetSourceLimitedRange(limited)
+ .SetSourceTextureBitDepth(texBits);
+}
+
+void CYUV2RGBShader::PrepareParameters(CRenderBuffer* videoBuffer, CRect sourceRect, CPoint dest[])
+{
+ if (m_sourceRect != sourceRect
+ || m_dest[0] != dest[0] || m_dest[1] != dest[1]
+ || m_dest[2] != dest[2] || m_dest[3] != dest[3]
+ || videoBuffer->GetWidth() != m_sourceWidth
+ || videoBuffer->GetHeight() != m_sourceHeight)
+ {
+ m_sourceRect = sourceRect;
+ for (size_t i = 0; i < 4; ++i)
+ m_dest[i] = dest[i];
+ m_sourceWidth = videoBuffer->GetWidth();
+ m_sourceHeight = videoBuffer->GetHeight();
+
+ Vertex* v;
+ CWinShader::LockVertexBuffer(reinterpret_cast<void**>(&v));
+
+ v[0].x = m_dest[0].x;
+ v[0].y = m_dest[0].y;
+ v[0].z = 0.0f;
+ v[0].tu = sourceRect.x1 / m_sourceWidth;
+ v[0].tv = sourceRect.y1 / m_sourceHeight;
+ v[0].tu2 = (sourceRect.x1 / 2.0f) / (m_sourceWidth>>1);
+ v[0].tv2 = (sourceRect.y1 / 2.0f) / (m_sourceHeight>>1);
+
+ v[1].x = m_dest[1].x;
+ v[1].y = m_dest[1].y;
+ v[1].z = 0.0f;
+ v[1].tu = sourceRect.x2 / m_sourceWidth;
+ v[1].tv = sourceRect.y1 / m_sourceHeight;
+ v[1].tu2 = (sourceRect.x2 / 2.0f) / (m_sourceWidth>>1);
+ v[1].tv2 = (sourceRect.y1 / 2.0f) / (m_sourceHeight>>1);
+
+ v[2].x = m_dest[2].x;
+ v[2].y = m_dest[2].y;
+ v[2].z = 0.0f;
+ v[2].tu = sourceRect.x2 / m_sourceWidth;
+ v[2].tv = sourceRect.y2 / m_sourceHeight;
+ v[2].tu2 = (sourceRect.x2 / 2.0f) / (m_sourceWidth>>1);
+ v[2].tv2 = (sourceRect.y2 / 2.0f) / (m_sourceHeight>>1);
+
+ v[3].x = m_dest[3].x;
+ v[3].y = m_dest[3].y;
+ v[3].z = 0.0f;
+ v[3].tu = sourceRect.x1 / m_sourceWidth;
+ v[3].tv = sourceRect.y2 / m_sourceHeight;
+ v[3].tu2 = (sourceRect.x1 / 2.0f) / (m_sourceWidth>>1);
+ v[3].tv2 = (sourceRect.y2 / 2.0f) / (m_sourceHeight>>1);
+
+ CWinShader::UnlockVertexBuffer();
+ }
+
+ unsigned int texWidth = m_sourceWidth;
+ if ( m_format == AV_PIX_FMT_UYVY422
+ || m_format == AV_PIX_FMT_YUYV422)
+ texWidth = texWidth >> 1;
+
+ m_texSteps[0] = 1.0f / texWidth;
+ m_texSteps[1] = 1.0f / m_sourceHeight;
+}
+
+void CYUV2RGBShader::SetShaderParameters(CRenderBuffer* videoBuffer)
+{
+ m_effect.SetTechnique("YUV2RGB_T");
+ ID3D11ShaderResourceView* ppSRView[3] = {};
+ for (unsigned i = 0, max_i = videoBuffer->GetViewCount(); i < max_i; i++)
+ ppSRView[i] = reinterpret_cast<ID3D11ShaderResourceView*>(videoBuffer->GetView(i));
+ m_effect.SetResources("g_Texture", ppSRView, videoBuffer->GetViewCount());
+ m_effect.SetFloatArray("g_StepXY", m_texSteps.data(), m_texSteps.size());
+
+ Matrix4 yuvMat = m_convMatrix.GetYuvMat();
+ m_effect.SetMatrix("g_ColorMatrix", yuvMat.ToRaw());
+
+ if (m_colorConversion)
+ {
+ Matrix4 primMat(m_convMatrix.GetPrimMat());
+
+ // looks like FX11 doesn't like 3x3 matrix, so used 4x4 for its happiness
+ m_effect.SetMatrix("g_primMat", primMat.ToRaw());
+ m_effect.SetScalar("g_gammaSrc", m_convMatrix.GetGammaSrc());
+ m_effect.SetScalar("g_gammaDstInv", 1 / m_convMatrix.GetGammaDst());
+ }
+
+ unsigned numPorts = 1;
+ D3D11_VIEWPORT viewPort;
+ DX::DeviceResources::Get()->GetD3DContext()->RSGetViewports(&numPorts, &viewPort);
+ m_effect.SetFloatArray("g_viewPort", &viewPort.Width, 2);
+ if (m_pOutShader)
+ m_pOutShader->ApplyEffectParameters(m_effect, m_sourceWidth, m_sourceHeight);
+}
+
+//==================================================================================
+
+bool CConvolutionShader::ChooseKernelD3DFormat()
+{
+ if (DX::Windowing()->IsFormatSupport(DXGI_FORMAT_R16G16B16A16_FLOAT, D3D11_FORMAT_SUPPORT_SHADER_SAMPLE))
+ {
+ m_KernelFormat = DXGI_FORMAT_R16G16B16A16_FLOAT;
+ m_floattex = true;
+ m_rgba = true;
+ }
+ else if (DX::Windowing()->IsFormatSupport(DXGI_FORMAT_R8G8B8A8_UNORM, D3D11_FORMAT_SUPPORT_SHADER_SAMPLE))
+ {
+ m_KernelFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
+ m_floattex = false;
+ m_rgba = true;
+ }
+ else if (DX::Windowing()->IsFormatSupport(DXGI_FORMAT_B8G8R8A8_UNORM, D3D11_FORMAT_SUPPORT_SHADER_SAMPLE))
+ {
+ m_KernelFormat = DXGI_FORMAT_B8G8R8A8_UNORM;
+ m_floattex = false;
+ m_rgba = false;
+ }
+ else
+ return false;
+
+ return true;
+}
+
+bool CConvolutionShader::CreateHQKernel(ESCALINGMETHOD method)
+{
+ CConvolutionKernel kern(method, 256);
+ void *kernel;
+ int kernelSize;
+
+ if (m_floattex)
+ {
+ float *data = kern.GetFloatPixels();
+ const auto float16 = new uint16_t[kern.GetSize() * 4];
+
+ XMConvertFloatToHalfStream(float16, sizeof(uint16_t), data, sizeof(float), kern.GetSize()*4);
+
+ kernel = float16;
+ kernelSize = sizeof(uint16_t)*kern.GetSize() * 4;
+ }
+ else
+ {
+ kernel = kern.GetUint8Pixels();
+ kernelSize = sizeof(uint8_t)*kern.GetSize() * 4;
+ }
+
+ if (!m_HQKernelTexture.Create(kern.GetSize(), 1, 1, D3D11_USAGE_IMMUTABLE, m_KernelFormat, kernel, kernelSize))
+ {
+ CLog::LogF(LOGERROR, "Failed to create kernel texture.");
+ return false;
+ }
+
+ if (m_floattex)
+ delete[] static_cast<uint16_t*>(kernel);
+
+ return true;
+}
+//==================================================================================
+bool CConvolutionShader1Pass::Create(ESCALINGMETHOD method, const std::shared_ptr<COutputShader>& pOutputShader)
+{
+ m_pOutShader = pOutputShader;
+
+ std::string effectString;
+ switch(method)
+ {
+ case VS_SCALINGMETHOD_CUBIC_MITCHELL:
+ case VS_SCALINGMETHOD_LANCZOS2:
+ case VS_SCALINGMETHOD_SPLINE36_FAST:
+ case VS_SCALINGMETHOD_LANCZOS3_FAST:
+ effectString = "special://xbmc/system/shaders/convolution-4x4_d3d.fx";
+ break;
+ case VS_SCALINGMETHOD_SPLINE36:
+ case VS_SCALINGMETHOD_LANCZOS3:
+ effectString = "special://xbmc/system/shaders/convolution-6x6_d3d.fx";
+ break;
+ default:
+ CLog::LogF(LOGERROR, "scaling method {} not supported.", method);
+ return false;
+ }
+
+ if (!ChooseKernelD3DFormat())
+ {
+ CLog::LogF(LOGERROR, "failed to find a compatible texture format for the kernel.");
+ return false;
+ }
+
+ CWinShader::CreateVertexBuffer(4, sizeof(Vertex));
+
+ DefinesMap defines;
+ if (m_floattex)
+ defines["HAS_FLOAT_TEXTURE"] = "";
+ if (m_rgba)
+ defines["HAS_RGBA"] = "";
+
+ if (m_pOutShader)
+ m_pOutShader->GetDefines(defines);
+
+ if(!LoadEffect(effectString, &defines))
+ {
+ CLog::LogF(LOGERROR, "Failed to load shader {}.", effectString);
+ return false;
+ }
+
+ if (!CreateHQKernel(method))
+ return false;
+
+ // Create input layout
+ D3D11_INPUT_ELEMENT_DESC layout[] =
+ {
+ { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ };
+ return CWinShader::CreateInputLayout(layout, ARRAYSIZE(layout));
+}
+
+void CConvolutionShader1Pass::Render(CD3DTexture& sourceTexture, CD3DTexture& target,
+ CRect sourceRect, CRect destRect, bool useLimitRange)
+{
+ const unsigned int sourceWidth = sourceTexture.GetWidth();
+ const unsigned int sourceHeight = sourceTexture.GetHeight();
+
+ PrepareParameters(sourceWidth, sourceHeight, sourceRect, destRect);
+ std::array<float, 2> texSteps = {1.0f / static_cast<float>(sourceWidth),
+ 1.0f / static_cast<float>(sourceHeight)};
+ SetShaderParameters(sourceTexture, texSteps.data(), texSteps.size(), useLimitRange);
+ Execute({ &target }, 4);
+}
+
+void CConvolutionShader1Pass::PrepareParameters(unsigned int sourceWidth, unsigned int sourceHeight,
+ CRect sourceRect, CRect destRect)
+{
+ if(m_sourceWidth != sourceWidth || m_sourceHeight != sourceHeight
+ || m_sourceRect != sourceRect || m_destRect != destRect)
+ {
+ m_sourceWidth = sourceWidth;
+ m_sourceHeight = sourceHeight;
+ m_sourceRect = sourceRect;
+ m_destRect = destRect;
+
+ Vertex* v;
+ CWinShader::LockVertexBuffer(reinterpret_cast<void**>(&v));
+
+ v[0].x = destRect.x1;
+ v[0].y = destRect.y1;
+ v[0].z = 0;
+ v[0].tu = sourceRect.x1 / sourceWidth;
+ v[0].tv = sourceRect.y1 / sourceHeight;
+
+ v[1].x = destRect.x2;
+ v[1].y = destRect.y1;
+ v[1].z = 0;
+ v[1].tu = sourceRect.x2 / sourceWidth;
+ v[1].tv = sourceRect.y1 / sourceHeight;
+
+ v[2].x = destRect.x2;
+ v[2].y = destRect.y2;
+ v[2].z = 0;
+ v[2].tu = sourceRect.x2 / sourceWidth;
+ v[2].tv = sourceRect.y2 / sourceHeight;
+
+ v[3].x = destRect.x1;
+ v[3].y = destRect.y2;
+ v[3].z = 0;
+ v[3].tu = sourceRect.x1 / sourceWidth;
+ v[3].tv = sourceRect.y2 / sourceHeight;
+
+ CWinShader::UnlockVertexBuffer();
+ }
+}
+
+void CConvolutionShader1Pass::SetShaderParameters(CD3DTexture &sourceTexture, float* texSteps, int texStepsCount, bool useLimitRange)
+{
+ m_effect.SetTechnique( "SCALER_T" );
+ m_effect.SetTexture( "g_Texture", sourceTexture ) ;
+ m_effect.SetTexture( "g_KernelTexture", m_HQKernelTexture );
+ m_effect.SetFloatArray("g_StepXY", texSteps, texStepsCount);
+ unsigned numVP = 1;
+ D3D11_VIEWPORT viewPort = {};
+ DX::DeviceResources::Get()->GetD3DContext()->RSGetViewports(&numVP, &viewPort);
+ m_effect.SetFloatArray("g_viewPort", &viewPort.Width, 2);
+ float colorRange[2] =
+ {
+ (useLimitRange ? 16.f / 255.f : 0.f),
+ (useLimitRange ? 219.f / 255.f : 1.f),
+ };
+ m_effect.SetFloatArray("g_colorRange", colorRange, _countof(colorRange));
+ if (m_pOutShader)
+ m_pOutShader->ApplyEffectParameters(m_effect, sourceTexture.GetWidth(), sourceTexture.GetHeight());
+}
+
+//==================================================================================
+
+bool CConvolutionShaderSeparable::Create(ESCALINGMETHOD method, const std::shared_ptr<COutputShader>& pOutputShader)
+{
+ m_pOutShader = pOutputShader;
+
+ std::string effectString;
+ switch(method)
+ {
+ case VS_SCALINGMETHOD_CUBIC_MITCHELL:
+ case VS_SCALINGMETHOD_LANCZOS2:
+ case VS_SCALINGMETHOD_SPLINE36_FAST:
+ case VS_SCALINGMETHOD_LANCZOS3_FAST:
+ effectString = "special://xbmc/system/shaders/convolutionsep-4x4_d3d.fx";
+ break;
+ case VS_SCALINGMETHOD_SPLINE36:
+ case VS_SCALINGMETHOD_LANCZOS3:
+ effectString = "special://xbmc/system/shaders/convolutionsep-6x6_d3d.fx";
+ break;
+ default:
+ CLog::LogF(LOGERROR, "scaling method {} not supported.", method);
+ return false;
+ }
+
+ if (!ChooseIntermediateD3DFormat())
+ {
+ CLog::LogF(LOGERROR, "failed to find a compatible texture format for the intermediate render target.");
+ return false;
+ }
+
+ if (!ChooseKernelD3DFormat())
+ {
+ CLog::LogF(LOGERROR, "failed to find a compatible texture format for the kernel.");
+ return false;
+ }
+
+ CWinShader::CreateVertexBuffer(8, sizeof(Vertex));
+
+ DefinesMap defines;
+ if (m_floattex)
+ defines["HAS_FLOAT_TEXTURE"] = "";
+ if (m_rgba)
+ defines["HAS_RGBA"] = "";
+
+ if (m_pOutShader)
+ m_pOutShader->GetDefines(defines);
+
+ if(!LoadEffect(effectString, &defines))
+ {
+ CLog::LogF(LOGERROR, "Failed to load shader {}.", effectString);
+ return false;
+ }
+
+ if (!CreateHQKernel(method))
+ return false;
+
+ // Create input layout
+ D3D11_INPUT_ELEMENT_DESC layout[] =
+ {
+ { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ };
+ return CWinShader::CreateInputLayout(layout, ARRAYSIZE(layout));
+}
+
+void CConvolutionShaderSeparable::Render(CD3DTexture& sourceTexture, CD3DTexture& target,
+ CRect sourceRect, CRect destRect, bool useLimitRange)
+{
+ const unsigned int sourceWidth = sourceTexture.GetWidth();
+ const unsigned int sourceHeight = sourceTexture.GetHeight();
+
+ const unsigned int destWidth = target.GetWidth();
+ const unsigned int destHeight = target.GetHeight();
+
+ if(m_destWidth != destWidth || m_sourceHeight != sourceHeight)
+ CreateIntermediateRenderTarget(destWidth, sourceHeight);
+
+ PrepareParameters(sourceWidth, sourceHeight, destWidth, destHeight, sourceRect, destRect);
+ float texSteps[] =
+ {
+ 1.0f / static_cast<float>(sourceWidth),
+ 1.0f / static_cast<float>(sourceHeight),
+ 1.0f / static_cast<float>(destWidth),
+ 1.0f / static_cast<float>(sourceHeight)
+ };
+ SetShaderParameters(sourceTexture, texSteps, 4, useLimitRange);
+
+ Execute({ &m_IntermediateTarget, &target }, 4);
+}
+
+bool CConvolutionShaderSeparable::ChooseIntermediateD3DFormat()
+{
+ const D3D11_FORMAT_SUPPORT usage = D3D11_FORMAT_SUPPORT_RENDER_TARGET;
+
+ // Need a float texture, as the output of the first pass can contain negative values.
+ if (DX::Windowing()->IsFormatSupport(DXGI_FORMAT_R16G16B16A16_FLOAT, usage))
+ m_IntermediateFormat = DXGI_FORMAT_R16G16B16A16_FLOAT;
+ else if (DX::Windowing()->IsFormatSupport(DXGI_FORMAT_R32G32B32A32_FLOAT, usage))
+ m_IntermediateFormat = DXGI_FORMAT_R32G32B32A32_FLOAT;
+ else
+ {
+ CLog::LogF(LOGINFO, "no float format available for the intermediate render target");
+ return false;
+ }
+
+ CLog::LogF(LOGDEBUG, "format {}", m_IntermediateFormat);
+ return true;
+}
+
+bool CConvolutionShaderSeparable::CreateIntermediateRenderTarget(unsigned int width, unsigned int height)
+{
+ if (m_IntermediateTarget.Get())
+ m_IntermediateTarget.Release();
+
+ if (!m_IntermediateTarget.Create(width, height, 1, D3D11_USAGE_DEFAULT, m_IntermediateFormat))
+ {
+ CLog::LogF(LOGERROR, "render target creation failed.");
+ return false;
+ }
+ return true;
+}
+
+bool CConvolutionShaderSeparable::ClearIntermediateRenderTarget()
+{
+ float color[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
+ DX::DeviceResources::Get()->GetD3DContext()->ClearRenderTargetView(m_IntermediateTarget.GetRenderTarget(), color);
+ return true;
+}
+
+void CConvolutionShaderSeparable::PrepareParameters(unsigned int sourceWidth, unsigned int sourceHeight,
+ unsigned int destWidth, unsigned int destHeight,
+ CRect sourceRect, CRect destRect)
+{
+ if(m_sourceWidth != sourceWidth || m_sourceHeight != sourceHeight
+ || m_destWidth != destWidth || m_destHeight != destHeight
+ || m_sourceRect != sourceRect || m_destRect != destRect)
+ {
+ ClearIntermediateRenderTarget();
+
+ m_sourceWidth = sourceWidth;
+ m_sourceHeight = sourceHeight;
+ m_destWidth = destWidth;
+ m_destHeight = destHeight;
+ m_sourceRect = sourceRect;
+ m_destRect = destRect;
+
+ Vertex* v;
+ CWinShader::LockVertexBuffer(reinterpret_cast<void**>(&v));
+
+ // Alter rectangles the destination rectangle exceeds the intermediate target width when zooming and causes artifacts.
+ // Work on the parameters rather than the members to avoid disturbing the parameter change detection the next time the function is called
+ const CRect target(0, 0, static_cast<float>(destWidth), static_cast<float>(destHeight));
+ CWIN32Util::CropSource(sourceRect, destRect, target);
+
+ // Manipulate the coordinates to work only on the active parts of the textures,
+ // and therefore avoid the need to clear surfaces/render targets
+
+ // Pass 1:
+ // Horizontal dimension: crop/zoom, so that it is completely done with the convolution shader. Scaling to display width in pass1 and
+ // cropping/zooming in pass 2 would use bilinear in pass2, which we don't want.
+ // Vertical dimension: crop using sourceRect to save memory bandwidth for high zoom values, but don't stretch/shrink in any way in this pass.
+
+ v[0].x = 0;
+ v[0].y = 0;
+ v[0].z = 0;
+ v[0].tu = sourceRect.x1 / sourceWidth;
+ v[0].tv = sourceRect.y1 / sourceHeight;
+
+ v[1].x = destRect.x2 - destRect.x1;
+ v[1].y = 0;
+ v[1].z = 0;
+ v[1].tu = sourceRect.x2 / sourceWidth;
+ v[1].tv = sourceRect.y1 / sourceHeight;
+
+ v[2].x = destRect.x2 - destRect.x1;
+ v[2].y = sourceRect.y2 - sourceRect.y1;
+ v[2].z = 0;
+ v[2].tu = sourceRect.x2 / sourceWidth;
+ v[2].tv = sourceRect.y2 / sourceHeight;
+
+ v[3].x = 0;
+ v[3].y = sourceRect.y2 - sourceRect.y1;
+ v[3].z = 0;
+ v[3].tu = sourceRect.x1 / sourceWidth;
+ v[3].tv = sourceRect.y2 / sourceHeight;
+
+ // Pass 2: pass the horizontal data untouched, resize vertical dimension for final result.
+
+ v[4].x = destRect.x1;
+ v[4].y = destRect.y1;
+ v[4].z = 0;
+ v[4].tu = 0;
+ v[4].tv = 0;
+
+ v[5].x = destRect.x2;
+ v[5].y = destRect.y1;
+ v[5].z = 0;
+ v[5].tu = (destRect.x2 - destRect.x1) / destWidth;
+ v[5].tv = 0;
+
+ v[6].x = destRect.x2;
+ v[6].y = destRect.y2;
+ v[6].z = 0;
+ v[6].tu = (destRect.x2 - destRect.x1) / destWidth;
+ v[6].tv = (sourceRect.y2 - sourceRect.y1) / sourceHeight;
+
+ v[7].x = destRect.x1;
+ v[7].y = destRect.y2;
+ v[7].z = 0;
+ v[7].tu = 0;
+ v[7].tv = (sourceRect.y2 - sourceRect.y1) / sourceHeight;
+
+ CWinShader::UnlockVertexBuffer();
+ }
+}
+
+void CConvolutionShaderSeparable::SetShaderParameters(CD3DTexture &sourceTexture, float* texSteps, int texStepsCount, bool useLimitRange)
+{
+ m_effect.SetTechnique( "SCALER_T" );
+ m_effect.SetTexture( "g_Texture", sourceTexture );
+ m_effect.SetTexture( "g_KernelTexture", m_HQKernelTexture );
+ m_effect.SetFloatArray("g_StepXY", texSteps, texStepsCount);
+ float colorRange[2] =
+ {
+ (useLimitRange ? 16.f / 255.f : 0.f),
+ (useLimitRange ? 219.f / 255.f : 1.f)
+ };
+ m_effect.SetFloatArray("g_colorRange", colorRange, _countof(colorRange));
+ if (m_pOutShader)
+ m_pOutShader->ApplyEffectParameters(m_effect, sourceTexture.GetWidth(), sourceTexture.GetHeight());
+}
+
+void CConvolutionShaderSeparable::SetStepParams(unsigned iPass)
+{
+ ID3D11DeviceContext* pContext = DX::DeviceResources::Get()->GetD3DContext();
+
+ CD3D11_VIEWPORT viewPort = CD3D11_VIEWPORT(
+ 0.0f,
+ 0.0f,
+ static_cast<float>(m_target->GetWidth()),
+ static_cast<float>(m_target->GetHeight()));
+
+ if (iPass == 0)
+ {
+ // reset scissor
+ DX::Windowing()->ResetScissors();
+ }
+ else if (iPass == 1)
+ {
+ // at the second pass m_IntermediateTarget is a source of data
+ m_effect.SetTexture("g_Texture", m_IntermediateTarget);
+ // restore scissor
+ DX::Windowing()->SetScissors(CServiceBroker::GetWinSystem()->GetGfxContext().StereoCorrection(CServiceBroker::GetWinSystem()->GetGfxContext().GetScissors()));
+ }
+ // setting view port
+ pContext->RSSetViewports(1, &viewPort);
+ // pass viewport dimension to the shaders
+ m_effect.SetFloatArray("g_viewPort", &viewPort.Width, 2);
+}
+
+//==========================================================
+#define SHADER_SOURCE(...) #__VA_ARGS__
+
+bool CTestShader::Create()
+{
+ std::string strShader = SHADER_SOURCE(
+ float4 TEST() : SV_TARGET
+ {
+ return float4(0.0, 0.0, 0.0, 0.0);
+ }
+
+ technique11 TEST_T
+ {
+ pass P0
+ {
+ SetPixelShader(CompileShader(ps_4_0_level_9_1, TEST()));
+ }
+ };
+ );
+
+ if (!m_effect.Create(strShader, nullptr))
+ {
+ CLog::LogF(LOGERROR, "Failed to create test shader: {}", strShader);
+ return false;
+ }
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/WinVideoFilter.h b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/WinVideoFilter.h
new file mode 100644
index 0000000..de901af
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/WinVideoFilter.h
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2007-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/IPlayer.h"
+#include "cores/VideoPlayer/VideoRenderers/VideoShaders/ConversionMatrix.h"
+#include "guilib/D3DResource.h"
+#include "utils/Geometry.h"
+
+#include <array>
+#include <vector>
+
+#include <DirectXMath.h>
+#include <wrl/client.h>
+
+extern "C" {
+#include <libavutil/pixfmt.h>
+#include <libavutil/mastering_display_metadata.h>
+}
+class CRenderBuffer;
+
+using namespace DirectX;
+
+class CWinShader
+{
+protected:
+ CWinShader() = default;
+ virtual ~CWinShader() = default;
+
+ virtual bool CreateVertexBuffer(unsigned int vertCount, unsigned int vertSize);
+ virtual bool LockVertexBuffer(void **data);
+ virtual bool UnlockVertexBuffer();
+ virtual bool LoadEffect(const std::string& filename, DefinesMap* defines);
+ virtual bool Execute(const std::vector<CD3DTexture*>& targets, unsigned int vertexIndexStep);
+ virtual void SetStepParams(unsigned stepIndex) { }
+ virtual bool CreateInputLayout(D3D11_INPUT_ELEMENT_DESC *layout, unsigned numElements);
+
+ CD3DEffect m_effect;
+ CD3DTexture* m_target = nullptr;
+
+private:
+ void SetTarget(CD3DTexture* target);
+
+ CD3DBuffer m_vb;
+ CD3DBuffer m_ib;
+ unsigned int m_vbsize = 0;
+ unsigned int m_vertsize = 0;
+ Microsoft::WRL::ComPtr<ID3D11InputLayout> m_inputLayout = nullptr;
+};
+
+class COutputShader : public CWinShader
+{
+public:
+ explicit COutputShader() = default;
+ ~COutputShader() = default;
+
+ void ApplyEffectParameters(CD3DEffect &effect, unsigned sourceWidth, unsigned sourceHeight);
+ void GetDefines(DefinesMap &map) const;
+ bool Create(bool useLUT,
+ bool useDithering,
+ int ditherDepth,
+ bool toneMapping,
+ ETONEMAPMETHOD toneMethod,
+ bool HLGtoPQ);
+ void Render(CD3DTexture& sourceTexture, CRect sourceRect, const CPoint points[4]
+ , CD3DTexture& target, unsigned range = 0, float contrast = 0.5f, float brightness = 0.5f);
+ void Render(CD3DTexture& sourceTexture, CRect sourceRect, CRect destRect
+ , CD3DTexture& target, unsigned range = 0, float contrast = 0.5f, float brightness = 0.5f);
+ void SetLUT(int lutSize, ID3D11ShaderResourceView *pLUTView);
+ void SetDisplayMetadata(bool hasDisplayMetadata, AVMasteringDisplayMetadata displayMetadata,
+ bool hasLightMetadata, AVContentLightMetadata lightMetadata);
+ void SetToneMapParam(ETONEMAPMETHOD method, float param);
+ std::string GetDebugInfo();
+
+ static bool CreateLUTView(int lutSize, uint16_t* lutData, bool isRGB, ID3D11ShaderResourceView** ppLUTView);
+
+private:
+ struct Vertex
+ {
+ float x, y, z;
+ float tu, tv;
+ };
+
+ bool HasLUT() const { return m_lutSize && m_pLUTView; }
+ void PrepareParameters(unsigned sourceWidth, unsigned sourceHeight, CRect sourceRect, const CPoint points[4]);
+ void SetShaderParameters(CD3DTexture &sourceTexture, unsigned range, float contrast, float brightness);
+ void CreateDitherView();
+ float GetLuminanceValue() const;
+
+ bool m_useLut = false;
+ bool m_useDithering = false;
+ bool m_toneMapping = false;
+ bool m_useHLGtoPQ = false;
+
+ bool m_hasDisplayMetadata = false;
+ bool m_hasLightMetadata = false;
+ unsigned m_sourceWidth = 0;
+ unsigned m_sourceHeight = 0;
+ int m_lutSize = 0;
+ int m_ditherDepth = 0;
+ ETONEMAPMETHOD m_toneMappingMethod = VS_TONEMAPMETHOD_OFF;
+ float m_toneMappingParam = 1.0f;
+ float m_toneMappingDebug = .0f;
+
+ CRect m_sourceRect = {};
+ CPoint m_destPoints[4] = {};
+ Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_pDitherView = nullptr;
+ Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_pLUTView = nullptr;
+
+ AVMasteringDisplayMetadata m_displayMetadata = {};
+ AVContentLightMetadata m_lightMetadata = {};
+};
+
+class CYUV2RGBShader : public CWinShader
+{
+public:
+ explicit CYUV2RGBShader() = default;
+ ~CYUV2RGBShader() = default;
+
+ bool Create(AVPixelFormat fmt,
+ AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries,
+ const std::shared_ptr<COutputShader>& pOutShader = nullptr);
+ void Render(CRect sourceRect, CPoint dest[], CRenderBuffer* videoBuffer, CD3DTexture& target);
+ void SetParams(float contrast, float black, bool limited);
+ void SetColParams(AVColorSpace colSpace, int bits, bool limited, int texBits);
+
+protected:
+ void PrepareParameters(CRenderBuffer* videoBuffer, CRect sourceRect, CPoint dest[]);
+ void SetShaderParameters(CRenderBuffer* videoBuffer);
+
+private:
+ struct Vertex {
+ float x, y, z;
+ float tu, tv; // Y Texture coordinates
+ float tu2, tv2; // U and V Textures coordinates
+ };
+
+ unsigned int m_sourceWidth = 0;
+ unsigned int m_sourceHeight = 0;
+ CRect m_sourceRect = {};
+ CPoint m_dest[4] = {};
+ AVPixelFormat m_format = AV_PIX_FMT_NONE;
+ std::array<float, 2> m_texSteps = {};
+ std::shared_ptr<COutputShader> m_pOutShader = nullptr;
+ CConvertMatrix m_convMatrix;
+ bool m_colorConversion{false};
+};
+
+class CConvolutionShader : public CWinShader
+{
+public:
+ virtual ~CConvolutionShader() = default;
+ virtual bool Create(ESCALINGMETHOD method, const std::shared_ptr<COutputShader>& pOutShader = nullptr) = 0;
+ virtual void Render(CD3DTexture& sourceTexture, CD3DTexture& target,
+ CRect sourceRect, CRect destRect, bool useLimitRange) = 0;
+protected:
+ struct Vertex {
+ float x, y, z;
+ float tu, tv;
+ };
+
+ CConvolutionShader() = default;
+
+ virtual bool ChooseKernelD3DFormat();
+ virtual bool CreateHQKernel(ESCALINGMETHOD method);
+ virtual void SetShaderParameters(CD3DTexture &sourceTexture, float* texSteps, int texStepsCount, bool useLimitRange) = 0;
+
+ bool m_floattex = false;
+ bool m_rgba = false;
+ DXGI_FORMAT m_KernelFormat = DXGI_FORMAT_UNKNOWN;
+ CD3DTexture m_HQKernelTexture;
+ std::shared_ptr<COutputShader> m_pOutShader = nullptr;
+};
+
+class CConvolutionShader1Pass : public CConvolutionShader
+{
+public:
+ explicit CConvolutionShader1Pass() = default;
+
+ bool Create(ESCALINGMETHOD method, const std::shared_ptr<COutputShader>& pOutputShader = nullptr) override;
+ void Render(CD3DTexture& sourceTexture, CD3DTexture& target,
+ CRect sourceRect, CRect destRect, bool useLimitRange) override;
+
+protected:
+ void PrepareParameters(unsigned int sourceWidth, unsigned int sourceHeight,
+ CRect sourceRect, CRect destRect);
+ void SetShaderParameters(CD3DTexture &sourceTexture, float* texSteps,
+ int texStepsCount, bool useLimitRange) override;
+
+private:
+ unsigned int m_sourceWidth = 0;
+ unsigned int m_sourceHeight = 0;
+ CRect m_sourceRect = {};
+ CRect m_destRect = {};
+};
+
+class CConvolutionShaderSeparable : public CConvolutionShader
+{
+public:
+ explicit CConvolutionShaderSeparable() = default;
+ ~CConvolutionShaderSeparable() = default;
+
+ bool Create(ESCALINGMETHOD method, const std::shared_ptr<COutputShader>& pOutputShader = nullptr) override;
+ void Render(CD3DTexture& sourceTexture, CD3DTexture& target,
+ CRect sourceRect, CRect destRect, bool useLimitRange) override;
+
+protected:
+ bool ChooseIntermediateD3DFormat();
+ bool CreateIntermediateRenderTarget(unsigned int width, unsigned int height);
+ bool ClearIntermediateRenderTarget();
+ void PrepareParameters(unsigned int sourceWidth, unsigned int sourceHeight,
+ unsigned int destWidth, unsigned int destHeight,
+ CRect sourceRect, CRect destRect);
+ void SetShaderParameters(CD3DTexture &sourceTexture, float* texSteps,
+ int texStepsCount, bool useLimitRange) override;
+ void SetStepParams(unsigned iPass) override;
+
+private:
+ CD3DTexture m_IntermediateTarget;
+ DXGI_FORMAT m_IntermediateFormat = DXGI_FORMAT_UNKNOWN;
+ unsigned int m_sourceWidth = 0;
+ unsigned int m_sourceHeight = 0;
+ unsigned int m_destWidth = 0;
+ unsigned int m_destHeight = 0;
+ CRect m_sourceRect = {};
+ CRect m_destRect = {};
+};
+
+class CTestShader : public CWinShader
+{
+public:
+ virtual bool Create();
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.cpp
new file mode 100644
index 0000000..6753ca1
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.cpp
@@ -0,0 +1,395 @@
+/*
+ * Copyright (c) 2007 d4rk
+ * Copyright (C) 2007-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 "YUV2RGBShaderGL.h"
+
+#include "../RenderFlags.h"
+#include "ConvolutionKernels.h"
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+
+#include <sstream>
+#include <string>
+#include <utility>
+
+using namespace Shaders::GL;
+
+//////////////////////////////////////////////////////////////////////
+// BaseYUV2RGBGLSLShader - base class for GLSL YUV2RGB shaders
+//////////////////////////////////////////////////////////////////////
+
+BaseYUV2RGBGLSLShader::BaseYUV2RGBGLSLShader(bool rect,
+ EShaderFormat format,
+ bool stretch,
+ AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod,
+ std::shared_ptr<GLSLOutput> output)
+{
+ m_width = 1;
+ m_height = 1;
+ m_field = 0;
+ m_format = format;
+ m_black = 0.0f;
+ m_contrast = 1.0f;
+ m_stretch = 0.0f;
+
+ // get defines from the output stage if used
+ m_glslOutput = std::move(output);
+ if (m_glslOutput)
+ {
+ m_defines += m_glslOutput->GetDefines();
+ }
+
+ if (rect)
+ m_defines += "#define XBMC_texture_rectangle 1\n";
+ else
+ m_defines += "#define XBMC_texture_rectangle 0\n";
+
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_GLRectangleHack)
+ m_defines += "#define XBMC_texture_rectangle_hack 1\n";
+ else
+ m_defines += "#define XBMC_texture_rectangle_hack 0\n";
+
+ //don't compile in stretch support when it's not needed
+ if (stretch)
+ m_defines += "#define XBMC_STRETCH 1\n";
+ else
+ m_defines += "#define XBMC_STRETCH 0\n";
+
+ if (m_format == SHADER_YV12 ||
+ m_format == SHADER_YV12_9 ||
+ m_format == SHADER_YV12_10 ||
+ m_format == SHADER_YV12_12 ||
+ m_format == SHADER_YV12_14 ||
+ m_format == SHADER_YV12_16)
+ m_defines += "#define XBMC_YV12\n";
+ else if (m_format == SHADER_NV12)
+ m_defines += "#define XBMC_NV12\n";
+ else if (m_format == SHADER_YUY2)
+ m_defines += "#define XBMC_YUY2\n";
+ else if (m_format == SHADER_UYVY)
+ m_defines += "#define XBMC_UYVY\n";
+ else
+ CLog::Log(LOGERROR, "GL: BaseYUV2RGBGLSLShader - unsupported format {}", m_format);
+
+ if (dstPrimaries != srcPrimaries)
+ {
+ m_colorConversion = true;
+ m_defines += "#define XBMC_COL_CONVERSION\n";
+ }
+
+ if (toneMap)
+ {
+ m_toneMapping = true;
+ m_toneMappingMethod = toneMapMethod;
+ m_defines += "#define XBMC_TONE_MAPPING\n";
+ if (toneMapMethod == VS_TONEMAPMETHOD_REINHARD)
+ m_defines += "#define KODI_TONE_MAPPING_REINHARD\n";
+ else if (toneMapMethod == VS_TONEMAPMETHOD_ACES)
+ m_defines += "#define KODI_TONE_MAPPING_ACES\n";
+ else if (toneMapMethod == VS_TONEMAPMETHOD_HABLE)
+ m_defines += "#define KODI_TONE_MAPPING_HABLE\n";
+ }
+
+ VertexShader()->LoadSource("gl_yuv2rgb_vertex.glsl", m_defines);
+
+ CLog::Log(LOGDEBUG, "GL: using shader format: {}", m_format);
+ CLog::Log(LOGDEBUG, "GL: using tonemap method: {}", toneMapMethod);
+
+ m_convMatrix.SetDestinationColorPrimaries(dstPrimaries).SetSourceColorPrimaries(srcPrimaries);
+}
+
+BaseYUV2RGBGLSLShader::~BaseYUV2RGBGLSLShader()
+{
+ Free();
+ m_glslOutput.reset();
+}
+
+void BaseYUV2RGBGLSLShader::OnCompiledAndLinked()
+{
+ m_hYTex = glGetUniformLocation(ProgramHandle(), "m_sampY");
+ m_hUTex = glGetUniformLocation(ProgramHandle(), "m_sampU");
+ m_hVTex = glGetUniformLocation(ProgramHandle(), "m_sampV");
+ m_hYuvMat = glGetUniformLocation(ProgramHandle(), "m_yuvmat");
+ m_hStretch = glGetUniformLocation(ProgramHandle(), "m_stretch");
+ m_hStep = glGetUniformLocation(ProgramHandle(), "m_step");
+ m_hVertex = glGetAttribLocation(ProgramHandle(), "m_attrpos");
+ m_hYcoord = glGetAttribLocation(ProgramHandle(), "m_attrcordY");
+ m_hUcoord = glGetAttribLocation(ProgramHandle(), "m_attrcordU");
+ m_hVcoord = glGetAttribLocation(ProgramHandle(), "m_attrcordV");
+ m_hProj = glGetUniformLocation(ProgramHandle(), "m_proj");
+ m_hModel = glGetUniformLocation(ProgramHandle(), "m_model");
+ m_hAlpha = glGetUniformLocation(ProgramHandle(), "m_alpha");
+ m_hPrimMat = glGetUniformLocation(ProgramHandle(), "m_primMat");
+ m_hGammaSrc = glGetUniformLocation(ProgramHandle(), "m_gammaSrc");
+ m_hGammaDstInv = glGetUniformLocation(ProgramHandle(), "m_gammaDstInv");
+ m_hCoefsDst = glGetUniformLocation(ProgramHandle(), "m_coefsDst");
+ m_hToneP1 = glGetUniformLocation(ProgramHandle(), "m_toneP1");
+ m_hLuminance = glGetUniformLocation(ProgramHandle(), "m_luminance");
+ VerifyGLState();
+
+ if (m_glslOutput)
+ m_glslOutput->OnCompiledAndLinked(ProgramHandle());
+}
+
+bool BaseYUV2RGBGLSLShader::OnEnabled()
+{
+ // set shader attributes once enabled
+ glUniform1i(m_hYTex, 0);
+ glUniform1i(m_hUTex, 1);
+ glUniform1i(m_hVTex, 2);
+ glUniform1f(m_hStretch, m_stretch);
+ glUniform2f(m_hStep, 1.0 / m_width, 1.0 / m_height);
+
+ m_convMatrix.SetDestinationContrast(m_contrast)
+ .SetDestinationBlack(m_black)
+ .SetDestinationLimitedRange(!m_convertFullRange);
+
+ Matrix4 yuvMat = m_convMatrix.GetYuvMat();
+ glUniformMatrix4fv(m_hYuvMat, 1, GL_FALSE, reinterpret_cast<GLfloat*>(yuvMat.ToRaw()));
+ glUniformMatrix4fv(m_hProj, 1, GL_FALSE, m_proj);
+ glUniformMatrix4fv(m_hModel, 1, GL_FALSE, m_model);
+ glUniform1f(m_hAlpha, m_alpha);
+
+ if (m_colorConversion)
+ {
+ Matrix3 primMat = m_convMatrix.GetPrimMat();
+ glUniformMatrix3fv(m_hPrimMat, 1, GL_FALSE, reinterpret_cast<GLfloat*>(primMat.ToRaw()));
+ glUniform1f(m_hGammaSrc, m_convMatrix.GetGammaSrc());
+ glUniform1f(m_hGammaDstInv, 1 / m_convMatrix.GetGammaDst());
+ }
+
+ if (m_toneMapping)
+ {
+ if (m_toneMappingMethod == VS_TONEMAPMETHOD_REINHARD)
+ {
+ float param = 0.7;
+ if (m_hasLightMetadata)
+ param = log10(100) / log10(m_lightMetadata.MaxCLL);
+ else if (m_hasDisplayMetadata && m_displayMetadata.has_luminance)
+ param = log10(100) / log10(m_displayMetadata.max_luminance.num/m_displayMetadata.max_luminance.den);
+
+ // Sanity check
+ if (param < 0.1f || param > 5.0f)
+ param = 0.7f;
+
+ param *= m_toneMappingParam;
+
+ Matrix3x1 coefs = m_convMatrix.GetRGBYuvCoefs(AVColorSpace::AVCOL_SPC_BT709);
+ glUniform3f(m_hCoefsDst, coefs[0], coefs[1], coefs[2]);
+ glUniform1f(m_hToneP1, param);
+ }
+ else if (m_toneMappingMethod == VS_TONEMAPMETHOD_ACES)
+ {
+ glUniform1f(m_hLuminance, GetLuminanceValue());
+ glUniform1f(m_hToneP1, m_toneMappingParam);
+ }
+ else if (m_toneMappingMethod == VS_TONEMAPMETHOD_HABLE)
+ {
+ float lumin = GetLuminanceValue();
+ float param = (10000.0f / lumin) * (2.0f / m_toneMappingParam);
+ glUniform1f(m_hLuminance, lumin);
+ glUniform1f(m_hToneP1, param);
+ }
+ }
+
+ VerifyGLState();
+ if (m_glslOutput)
+ m_glslOutput->OnEnabled();
+ return true;
+}
+
+void BaseYUV2RGBGLSLShader::OnDisabled()
+{
+ if (m_glslOutput)
+ m_glslOutput->OnDisabled();
+}
+
+void BaseYUV2RGBGLSLShader::Free()
+{
+ if (m_glslOutput)
+ m_glslOutput->Free();
+}
+
+void BaseYUV2RGBGLSLShader::SetColParams(AVColorSpace colSpace, int bits, bool limited,
+ int textureBits)
+{
+ if (colSpace == AVCOL_SPC_UNSPECIFIED)
+ {
+ if (m_width > 1024 || m_height >= 600)
+ colSpace = AVCOL_SPC_BT709;
+ else
+ colSpace = AVCOL_SPC_BT470BG;
+ }
+ m_convMatrix.SetSourceColorSpace(colSpace)
+ .SetSourceBitDepth(bits)
+ .SetSourceLimitedRange(limited)
+ .SetSourceTextureBitDepth(textureBits);
+}
+
+void BaseYUV2RGBGLSLShader::SetDisplayMetadata(bool hasDisplayMetadata, AVMasteringDisplayMetadata displayMetadata,
+ bool hasLightMetadata, AVContentLightMetadata lightMetadata)
+{
+ m_hasDisplayMetadata = hasDisplayMetadata;
+ m_displayMetadata = displayMetadata;
+ m_hasLightMetadata = hasLightMetadata;
+ m_lightMetadata = lightMetadata;
+}
+
+
+void BaseYUV2RGBGLSLShader::SetToneMapParam(ETONEMAPMETHOD method, float param)
+{
+ m_toneMappingMethod = method;
+ m_toneMappingParam = param;
+}
+
+float BaseYUV2RGBGLSLShader::GetLuminanceValue() const //Maybe move this to linuxrenderer?! same as in baserenderer
+{
+ float lum1 = 400.0f; // default for bad quality HDR-PQ sources (with no metadata)
+ float lum2 = lum1;
+ float lum3 = lum1;
+
+ if (m_hasLightMetadata)
+ {
+ uint16_t lum = m_displayMetadata.max_luminance.num / m_displayMetadata.max_luminance.den;
+ if (m_lightMetadata.MaxCLL >= lum)
+ {
+ lum1 = static_cast<float>(lum);
+ lum2 = static_cast<float>(m_lightMetadata.MaxCLL);
+ }
+ else
+ {
+ lum1 = static_cast<float>(m_lightMetadata.MaxCLL);
+ lum2 = static_cast<float>(lum);
+ }
+ lum3 = static_cast<float>(m_lightMetadata.MaxFALL);
+ lum1 = (lum1 * 0.5f) + (lum2 * 0.2f) + (lum3 * 0.3f);
+ }
+ else if (m_hasDisplayMetadata && m_displayMetadata.has_luminance &&
+ m_displayMetadata.max_luminance.num)
+ {
+ uint16_t lum = m_displayMetadata.max_luminance.num / m_displayMetadata.max_luminance.den;
+ lum1 = static_cast<float>(lum);
+ }
+
+ return lum1;
+}
+
+//////////////////////////////////////////////////////////////////////
+// YUV2RGBProgressiveShader - YUV2RGB with no deinterlacing
+// Use for weave deinterlacing / progressive
+//////////////////////////////////////////////////////////////////////
+
+YUV2RGBProgressiveShader::YUV2RGBProgressiveShader(bool rect,
+ EShaderFormat format,
+ bool stretch,
+ AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod,
+ std::shared_ptr<GLSLOutput> output)
+ : BaseYUV2RGBGLSLShader(rect,
+ format,
+ stretch,
+ dstPrimaries,
+ srcPrimaries,
+ toneMap,
+ toneMapMethod,
+ std::move(output))
+{
+ PixelShader()->LoadSource("gl_yuv2rgb_basic.glsl", m_defines);
+ PixelShader()->AppendSource("gl_output.glsl");
+
+ PixelShader()->InsertSource("gl_tonemap.glsl", "vec4 process()");
+}
+
+//------------------------------------------------------------------------------
+// YUV2RGBFilterShader4
+//------------------------------------------------------------------------------
+
+YUV2RGBFilterShader4::YUV2RGBFilterShader4(bool rect,
+ EShaderFormat format,
+ bool stretch,
+ AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod,
+ ESCALINGMETHOD method,
+ std::shared_ptr<GLSLOutput> output)
+ : BaseYUV2RGBGLSLShader(rect,
+ format,
+ stretch,
+ dstPrimaries,
+ srcPrimaries,
+ toneMap,
+ toneMapMethod,
+ std::move(output))
+{
+ m_scaling = method;
+ PixelShader()->LoadSource("gl_yuv2rgb_filter4.glsl", m_defines);
+ PixelShader()->AppendSource("gl_output.glsl");
+
+ PixelShader()->InsertSource("gl_tonemap.glsl", "vec4 process()");
+}
+
+YUV2RGBFilterShader4::~YUV2RGBFilterShader4()
+{
+ if (m_kernelTex)
+ glDeleteTextures(1, &m_kernelTex);
+ m_kernelTex = 0;
+}
+
+void YUV2RGBFilterShader4::OnCompiledAndLinked()
+{
+ BaseYUV2RGBGLSLShader::OnCompiledAndLinked();
+ m_hKernTex = glGetUniformLocation(ProgramHandle(), "m_kernelTex");
+
+ if (m_scaling != VS_SCALINGMETHOD_LANCZOS3_FAST && m_scaling != VS_SCALINGMETHOD_SPLINE36_FAST)
+ {
+ CLog::Log(LOGERROR, "GL: BaseYUV2RGBGLSLShader4 - unsupported scaling {} will fallback",
+ m_scaling);
+ m_scaling = VS_SCALINGMETHOD_LANCZOS3_FAST;
+ }
+
+ CConvolutionKernel kernel(m_scaling, 256);
+
+ if (m_kernelTex)
+ {
+ glDeleteTextures(1, &m_kernelTex);
+ m_kernelTex = 0;
+ }
+ glGenTextures(1, &m_kernelTex);
+
+ //make a kernel texture on GL_TEXTURE2 and set clamping and interpolation
+ //TEXTARGET is set to GL_TEXTURE_1D or GL_TEXTURE_2D
+ glActiveTexture(GL_TEXTURE3);
+ glBindTexture(GL_TEXTURE_1D, m_kernelTex);
+ glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+
+ GLvoid* data = (GLvoid*)kernel.GetFloatPixels();
+ glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA32F, kernel.GetSize(), 0, GL_RGBA, GL_FLOAT, data);
+ glActiveTexture(GL_TEXTURE0);
+ VerifyGLState();
+}
+
+bool YUV2RGBFilterShader4::OnEnabled()
+{
+ glActiveTexture(GL_TEXTURE3);
+ glBindTexture(GL_TEXTURE_1D, m_kernelTex);
+ glUniform1i(m_hKernTex, 3);
+
+ return BaseYUV2RGBGLSLShader::OnEnabled();
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.h b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.h
new file mode 100644
index 0000000..476933b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGL.h
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2007-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 "ConversionMatrix.h"
+#include "GLSLOutput.h"
+#include "ShaderFormats.h"
+#include "cores/VideoSettings.h"
+#include "guilib/Shader.h"
+#include "utils/TransformMatrix.h"
+
+#include <memory>
+
+extern "C" {
+#include <libavutil/pixfmt.h>
+#include <libavutil/mastering_display_metadata.h>
+}
+
+class CConvertMatrix;
+
+namespace Shaders {
+namespace GL
+{
+
+class BaseYUV2RGBGLSLShader : public CGLSLShaderProgram
+{
+public:
+ BaseYUV2RGBGLSLShader(bool rect,
+ EShaderFormat format,
+ bool stretch,
+ AVColorPrimaries dst,
+ AVColorPrimaries src,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod,
+ std::shared_ptr<GLSLOutput> output);
+ ~BaseYUV2RGBGLSLShader() override;
+
+ void SetField(int field) { m_field = field; }
+ void SetWidth(int w) { m_width = w; }
+ void SetHeight(int h) { m_height = h; }
+
+ void SetColParams(AVColorSpace colSpace, int bits, bool limited, int textureBits);
+ void SetBlack(float black) { m_black = black; }
+ void SetContrast(float contrast) { m_contrast = contrast; }
+ void SetNonLinStretch(float stretch) { m_stretch = stretch; }
+ void SetDisplayMetadata(bool hasDisplayMetadata, AVMasteringDisplayMetadata displayMetadata,
+ bool hasLightMetadata, AVContentLightMetadata lightMetadata);
+ void SetToneMapParam(ETONEMAPMETHOD method, float param);
+ float GetLuminanceValue() const;
+
+ void SetConvertFullColorRange(bool convertFullRange) { m_convertFullRange = convertFullRange; }
+
+ GLint GetVertexLoc() { return m_hVertex; }
+ GLint GetYcoordLoc() { return m_hYcoord; }
+ GLint GetUcoordLoc() { return m_hUcoord; }
+ GLint GetVcoordLoc() { return m_hVcoord; }
+
+ void SetMatrices(const GLfloat *p, const GLfloat *m) { m_proj = p; m_model = m; }
+ void SetAlpha(GLfloat alpha) { m_alpha = alpha; }
+
+protected:
+
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+ void OnDisabled() override;
+ void Free();
+
+ bool m_convertFullRange;
+ EShaderFormat m_format;
+ int m_width;
+ int m_height;
+ int m_field;
+ bool m_hasDisplayMetadata = false;
+ AVMasteringDisplayMetadata m_displayMetadata;
+ bool m_hasLightMetadata = false;
+ AVContentLightMetadata m_lightMetadata;
+ bool m_toneMapping = false;
+ ETONEMAPMETHOD m_toneMappingMethod = VS_TONEMAPMETHOD_REINHARD;
+ float m_toneMappingParam = 1.0;
+
+ bool m_colorConversion{false};
+
+ float m_black;
+ float m_contrast;
+ float m_stretch;
+
+ const GLfloat *m_proj = nullptr;
+ const GLfloat *m_model = nullptr;
+ GLfloat m_alpha = 1.0f;
+
+ std::string m_defines;
+
+ std::shared_ptr<Shaders::GLSLOutput> m_glslOutput;
+ CConvertMatrix m_convMatrix;
+
+ // pixel shader attribute handles
+ GLint m_hYTex = -1;
+ GLint m_hUTex = -1;
+ GLint m_hVTex = -1;
+ GLint m_hYuvMat = -1;
+ GLint m_hStretch = -1;
+ GLint m_hStep = -1;
+ GLint m_hGammaSrc = -1;
+ GLint m_hGammaDstInv = -1;
+ GLint m_hPrimMat = -1;
+ GLint m_hToneP1 = -1;
+ GLint m_hCoefsDst = -1;
+ GLint m_hLuminance = -1;
+
+ // vertex shader attribute handles
+ GLint m_hVertex = -1;
+ GLint m_hYcoord = -1;
+ GLint m_hUcoord = -1;
+ GLint m_hVcoord = -1;
+ GLint m_hProj = -1;
+ GLint m_hModel = -1;
+ GLint m_hAlpha = -1;
+};
+
+class YUV2RGBProgressiveShader : public BaseYUV2RGBGLSLShader
+{
+public:
+ YUV2RGBProgressiveShader(bool rect,
+ EShaderFormat format,
+ bool stretch,
+ AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod,
+ std::shared_ptr<GLSLOutput> output);
+};
+
+class YUV2RGBFilterShader4 : public BaseYUV2RGBGLSLShader
+{
+public:
+ YUV2RGBFilterShader4(bool rect,
+ EShaderFormat format,
+ bool stretch,
+ AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod,
+ ESCALINGMETHOD method,
+ std::shared_ptr<GLSLOutput> output);
+ ~YUV2RGBFilterShader4() override;
+
+protected:
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+
+ GLuint m_kernelTex = 0;
+ GLint m_hKernTex = -1;
+ ESCALINGMETHOD m_scaling = VS_SCALINGMETHOD_LANCZOS3_FAST;
+};
+
+} // namespace GL
+} // end namespace
+
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGLES.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGLES.cpp
new file mode 100644
index 0000000..48987e0
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGLES.cpp
@@ -0,0 +1,295 @@
+/*
+ * Copyright (c) 2007 d4rk
+ * Copyright (C) 2007-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 "YUV2RGBShaderGLES.h"
+
+#include "../RenderFlags.h"
+#include "settings/AdvancedSettings.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+
+#include <sstream>
+#include <string>
+
+using namespace Shaders::GLES;
+
+//////////////////////////////////////////////////////////////////////
+// BaseYUV2RGBGLSLShader - base class for GLSL YUV2RGB shaders
+//////////////////////////////////////////////////////////////////////
+
+BaseYUV2RGBGLSLShader::BaseYUV2RGBGLSLShader(EShaderFormat format,
+ AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod)
+{
+ m_width = 1;
+ m_height = 1;
+ m_field = 0;
+ m_format = format;
+
+ m_black = 0.0f;
+ m_contrast = 1.0f;
+
+ m_convertFullRange = false;
+
+ if (m_format == SHADER_YV12)
+ m_defines += "#define XBMC_YV12\n";
+ else if (m_format == SHADER_NV12)
+ m_defines += "#define XBMC_NV12\n";
+ else if (m_format == SHADER_NV12_RRG)
+ m_defines += "#define XBMC_NV12_RRG\n";
+ else
+ CLog::Log(LOGERROR, "GLES: BaseYUV2RGBGLSLShader - unsupported format {}", m_format);
+
+ if (dstPrimaries != srcPrimaries)
+ {
+ m_colorConversion = true;
+ m_defines += "#define XBMC_COL_CONVERSION\n";
+ }
+
+ if (toneMap)
+ {
+ m_toneMapping = true;
+ m_toneMappingMethod = toneMapMethod;
+ if (toneMapMethod == VS_TONEMAPMETHOD_REINHARD)
+ m_defines += "#define KODI_TONE_MAPPING_REINHARD\n";
+ else if (toneMapMethod == VS_TONEMAPMETHOD_ACES)
+ m_defines += "#define KODI_TONE_MAPPING_ACES\n";
+ else if (toneMapMethod == VS_TONEMAPMETHOD_HABLE)
+ m_defines += "#define KODI_TONE_MAPPING_HABLE\n";
+ }
+
+ VertexShader()->LoadSource("gles_yuv2rgb.vert", m_defines);
+
+ CLog::Log(LOGDEBUG, "GLES: using shader format: {}", m_format);
+ CLog::Log(LOGDEBUG, "GLES: using tonemap method: {}", m_toneMappingMethod);
+
+ m_convMatrix.SetSourceColorPrimaries(srcPrimaries).SetDestinationColorPrimaries(dstPrimaries);
+}
+
+BaseYUV2RGBGLSLShader::~BaseYUV2RGBGLSLShader()
+{
+ Free();
+}
+
+void BaseYUV2RGBGLSLShader::OnCompiledAndLinked()
+{
+ m_hVertex = glGetAttribLocation(ProgramHandle(), "m_attrpos");
+ m_hYcoord = glGetAttribLocation(ProgramHandle(), "m_attrcordY");
+ m_hUcoord = glGetAttribLocation(ProgramHandle(), "m_attrcordU");
+ m_hVcoord = glGetAttribLocation(ProgramHandle(), "m_attrcordV");
+ m_hProj = glGetUniformLocation(ProgramHandle(), "m_proj");
+ m_hModel = glGetUniformLocation(ProgramHandle(), "m_model");
+ m_hAlpha = glGetUniformLocation(ProgramHandle(), "m_alpha");
+ m_hYTex = glGetUniformLocation(ProgramHandle(), "m_sampY");
+ m_hUTex = glGetUniformLocation(ProgramHandle(), "m_sampU");
+ m_hVTex = glGetUniformLocation(ProgramHandle(), "m_sampV");
+ m_hYuvMat = glGetUniformLocation(ProgramHandle(), "m_yuvmat");
+ m_hStep = glGetUniformLocation(ProgramHandle(), "m_step");
+ m_hPrimMat = glGetUniformLocation(ProgramHandle(), "m_primMat");
+ m_hGammaSrc = glGetUniformLocation(ProgramHandle(), "m_gammaSrc");
+ m_hGammaDstInv = glGetUniformLocation(ProgramHandle(), "m_gammaDstInv");
+ m_hCoefsDst = glGetUniformLocation(ProgramHandle(), "m_coefsDst");
+ m_hToneP1 = glGetUniformLocation(ProgramHandle(), "m_toneP1");
+ m_hLuminance = glGetUniformLocation(ProgramHandle(), "m_luminance");
+ VerifyGLState();
+}
+
+bool BaseYUV2RGBGLSLShader::OnEnabled()
+{
+ // set shader attributes once enabled
+ glUniform1i(m_hYTex, 0);
+ glUniform1i(m_hUTex, 1);
+ glUniform1i(m_hVTex, 2);
+ glUniform2f(m_hStep, 1.0 / m_width, 1.0 / m_height);
+
+ m_convMatrix.SetDestinationContrast(m_contrast)
+ .SetDestinationBlack(m_black)
+ .SetDestinationLimitedRange(!m_convertFullRange);
+
+ Matrix4 yuvMat = m_convMatrix.GetYuvMat();
+ glUniformMatrix4fv(m_hYuvMat, 1, GL_FALSE, yuvMat.ToRaw());
+ glUniformMatrix4fv(m_hProj, 1, GL_FALSE, m_proj);
+ glUniformMatrix4fv(m_hModel, 1, GL_FALSE, m_model);
+ glUniform1f(m_hAlpha, m_alpha);
+
+ if (m_colorConversion)
+ {
+ Matrix3 primMat = m_convMatrix.GetPrimMat();
+ glUniformMatrix3fv(m_hPrimMat, 1, GL_FALSE, primMat.ToRaw());
+ glUniform1f(m_hGammaSrc, m_convMatrix.GetGammaSrc());
+ glUniform1f(m_hGammaDstInv, 1 / m_convMatrix.GetGammaDst());
+ }
+
+ if (m_toneMapping)
+ {
+ if (m_toneMappingMethod == VS_TONEMAPMETHOD_REINHARD)
+ {
+ float param = 0.7;
+
+ if (m_hasLightMetadata)
+ {
+ param = log10(100) / log10(m_lightMetadata.MaxCLL);
+ }
+ else if (m_hasDisplayMetadata && m_displayMetadata.has_luminance)
+ {
+ param = log10(100) /
+ log10(m_displayMetadata.max_luminance.num / m_displayMetadata.max_luminance.den);
+ }
+
+ // Sanity check
+ if (param < 0.1f || param > 5.0f)
+ {
+ param = 0.7f;
+ }
+
+ param *= m_toneMappingParam;
+
+ Matrix3x1 coefs = m_convMatrix.GetRGBYuvCoefs(AVColorSpace::AVCOL_SPC_BT709);
+ glUniform3f(m_hCoefsDst, coefs[0], coefs[1], coefs[2]);
+ glUniform1f(m_hToneP1, param);
+ }
+ else if (m_toneMappingMethod == VS_TONEMAPMETHOD_ACES)
+ {
+ glUniform1f(m_hLuminance, GetLuminanceValue());
+ glUniform1f(m_hToneP1, m_toneMappingParam);
+ }
+ else if (m_toneMappingMethod == VS_TONEMAPMETHOD_HABLE)
+ {
+ float lumin = GetLuminanceValue();
+ float param = (10000.0f / lumin) * (2.0f / m_toneMappingParam);
+ glUniform1f(m_hLuminance, lumin);
+ glUniform1f(m_hToneP1, param);
+ }
+ }
+
+ VerifyGLState();
+
+ return true;
+}
+
+void BaseYUV2RGBGLSLShader::OnDisabled()
+{
+}
+
+void BaseYUV2RGBGLSLShader::Free()
+{
+}
+
+void BaseYUV2RGBGLSLShader::SetColParams(AVColorSpace colSpace, int bits, bool limited,
+ int textureBits)
+{
+ if (colSpace == AVCOL_SPC_UNSPECIFIED)
+ {
+ if (m_width > 1024 || m_height >= 600)
+ colSpace = AVCOL_SPC_BT709;
+ else
+ colSpace = AVCOL_SPC_BT470BG;
+ }
+
+ m_convMatrix.SetSourceColorSpace(colSpace)
+ .SetSourceBitDepth(bits)
+ .SetSourceLimitedRange(limited)
+ .SetSourceTextureBitDepth(textureBits);
+}
+
+void BaseYUV2RGBGLSLShader::SetDisplayMetadata(bool hasDisplayMetadata, AVMasteringDisplayMetadata displayMetadata,
+ bool hasLightMetadata, AVContentLightMetadata lightMetadata)
+{
+ m_hasDisplayMetadata = hasDisplayMetadata;
+ m_displayMetadata = displayMetadata;
+ m_hasLightMetadata = hasLightMetadata;
+ m_lightMetadata = lightMetadata;
+}
+
+float BaseYUV2RGBGLSLShader::GetLuminanceValue() const
+{
+ float lum1 = 400.0f; // default for bad quality HDR-PQ sources (with no metadata)
+ float lum2 = lum1;
+ float lum3 = lum1;
+
+ if (m_hasLightMetadata)
+ {
+ uint16_t lum = m_displayMetadata.max_luminance.num / m_displayMetadata.max_luminance.den;
+ if (m_lightMetadata.MaxCLL >= lum)
+ {
+ lum1 = static_cast<float>(lum);
+ lum2 = static_cast<float>(m_lightMetadata.MaxCLL);
+ }
+ else
+ {
+ lum1 = static_cast<float>(m_lightMetadata.MaxCLL);
+ lum2 = static_cast<float>(lum);
+ }
+ lum3 = static_cast<float>(m_lightMetadata.MaxFALL);
+ lum1 = (lum1 * 0.5f) + (lum2 * 0.2f) + (lum3 * 0.3f);
+ }
+ else if (m_hasDisplayMetadata && m_displayMetadata.has_luminance &&
+ m_displayMetadata.max_luminance.num > 0)
+ {
+ uint16_t lum = m_displayMetadata.max_luminance.num / m_displayMetadata.max_luminance.den;
+ lum1 = static_cast<float>(lum);
+ }
+
+ return lum1;
+}
+
+//////////////////////////////////////////////////////////////////////
+// YUV2RGBProgressiveShader - YUV2RGB with no deinterlacing
+// Use for weave deinterlacing / progressive
+//////////////////////////////////////////////////////////////////////
+
+YUV2RGBProgressiveShader::YUV2RGBProgressiveShader(EShaderFormat format,
+ AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod)
+ : BaseYUV2RGBGLSLShader(format, dstPrimaries, srcPrimaries, toneMap, toneMapMethod)
+{
+ PixelShader()->LoadSource("gles_yuv2rgb_basic.frag", m_defines);
+ PixelShader()->InsertSource("gles_tonemap.frag", "void main()");
+}
+
+
+//////////////////////////////////////////////////////////////////////
+// YUV2RGBBobShader - YUV2RGB with Bob deinterlacing
+//////////////////////////////////////////////////////////////////////
+
+YUV2RGBBobShader::YUV2RGBBobShader(EShaderFormat format,
+ AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod)
+ : BaseYUV2RGBGLSLShader(format, dstPrimaries, srcPrimaries, toneMap, toneMapMethod)
+{
+ PixelShader()->LoadSource("gles_yuv2rgb_bob.frag", m_defines);
+ PixelShader()->InsertSource("gles_tonemap.frag", "void main()");
+}
+
+void YUV2RGBBobShader::OnCompiledAndLinked()
+{
+ BaseYUV2RGBGLSLShader::OnCompiledAndLinked();
+ m_hStepX = glGetUniformLocation(ProgramHandle(), "m_stepX");
+ m_hStepY = glGetUniformLocation(ProgramHandle(), "m_stepY");
+ m_hField = glGetUniformLocation(ProgramHandle(), "m_field");
+ VerifyGLState();
+}
+
+bool YUV2RGBBobShader::OnEnabled()
+{
+ if(!BaseYUV2RGBGLSLShader::OnEnabled())
+ return false;
+
+ glUniform1i(m_hField, m_field);
+ glUniform1f(m_hStepX, 1.0f / (float)m_width);
+ glUniform1f(m_hStepY, 1.0f / (float)m_height);
+ VerifyGLState();
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGLES.h b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGLES.h
new file mode 100644
index 0000000..07b8a3c
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/YUV2RGBShaderGLES.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2007-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 "ConversionMatrix.h"
+#include "ShaderFormats.h"
+#include "cores/VideoSettings.h"
+#include "guilib/Shader.h"
+#include "utils/TransformMatrix.h"
+
+extern "C" {
+#include <libavutil/mastering_display_metadata.h>
+#include <libavutil/pixfmt.h>
+}
+
+namespace Shaders {
+namespace GLES
+{
+
+class BaseYUV2RGBGLSLShader : public CGLSLShaderProgram
+{
+ public:
+ BaseYUV2RGBGLSLShader(EShaderFormat format,
+ AVColorPrimaries dst,
+ AVColorPrimaries src,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod);
+ ~BaseYUV2RGBGLSLShader() override;
+ void SetField(int field) { m_field = field; }
+ void SetWidth(int w) { m_width = w; }
+ void SetHeight(int h) { m_height = h; }
+
+ void SetColParams(AVColorSpace colSpace, int bits, bool limited, int textureBits);
+ void SetBlack(float black) { m_black = black; }
+ void SetContrast(float contrast) { m_contrast = contrast; }
+ void SetConvertFullColorRange(bool convertFullRange) { m_convertFullRange = convertFullRange; }
+ void SetDisplayMetadata(bool hasDisplayMetadata, AVMasteringDisplayMetadata displayMetadata,
+ bool hasLightMetadata, AVContentLightMetadata lightMetadata);
+ void SetToneMapParam(float param) { m_toneMappingParam = param; }
+ float GetLuminanceValue() const;
+
+ GLint GetVertexLoc() { return m_hVertex; }
+ GLint GetYcoordLoc() { return m_hYcoord; }
+ GLint GetUcoordLoc() { return m_hUcoord; }
+ GLint GetVcoordLoc() { return m_hVcoord; }
+
+ void SetMatrices(const GLfloat *p, const GLfloat *m) { m_proj = p; m_model = m; }
+ void SetAlpha(GLfloat alpha) { m_alpha = alpha; }
+
+ protected:
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+ void OnDisabled() override;
+ void Free();
+
+ EShaderFormat m_format;
+ int m_width;
+ int m_height;
+ int m_field;
+ bool m_hasDisplayMetadata{false};
+ AVMasteringDisplayMetadata m_displayMetadata;
+ bool m_hasLightMetadata{false};
+ AVContentLightMetadata m_lightMetadata;
+ bool m_toneMapping{false};
+ ETONEMAPMETHOD m_toneMappingMethod{VS_TONEMAPMETHOD_REINHARD};
+ float m_toneMappingParam{1.0};
+
+ bool m_colorConversion{false};
+
+ float m_black;
+ float m_contrast;
+
+ std::string m_defines;
+
+ CConvertMatrix m_convMatrix;
+
+ // shader attribute handles
+ GLint m_hYTex{-1};
+ GLint m_hUTex{-1};
+ GLint m_hVTex{-1};
+ GLint m_hYuvMat{-1};
+ GLint m_hStep{-1};
+ GLint m_hGammaSrc{-1};
+ GLint m_hGammaDstInv{-1};
+ GLint m_hPrimMat{-1};
+ GLint m_hToneP1{-1};
+ GLint m_hCoefsDst{-1};
+ GLint m_hLuminance = -1;
+
+ GLint m_hVertex{-1};
+ GLint m_hYcoord{-1};
+ GLint m_hUcoord{-1};
+ GLint m_hVcoord{-1};
+ GLint m_hProj{-1};
+ GLint m_hModel{-1};
+ GLint m_hAlpha{-1};
+
+ const GLfloat *m_proj{nullptr};
+ const GLfloat *m_model{nullptr};
+ GLfloat m_alpha{1.0f};
+
+ bool m_convertFullRange;
+ };
+
+ class YUV2RGBProgressiveShader : public BaseYUV2RGBGLSLShader
+ {
+ public:
+ YUV2RGBProgressiveShader(EShaderFormat format,
+ AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod);
+ };
+
+ class YUV2RGBBobShader : public BaseYUV2RGBGLSLShader
+ {
+ public:
+ YUV2RGBBobShader(EShaderFormat format,
+ AVColorPrimaries dstPrimaries,
+ AVColorPrimaries srcPrimaries,
+ bool toneMap,
+ ETONEMAPMETHOD toneMapMethod);
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+
+ GLint m_hStepX = -1;
+ GLint m_hStepY = -1;
+ GLint m_hField = -1;
+ };
+
+ } // namespace GLES
+} // end namespace
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/dither.h b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/dither.h
new file mode 100644
index 0000000..dc2cf1d
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/dither.h
@@ -0,0 +1,74 @@
+// Generated with https://git.fruit.je/src?a=blob;f=dither/dither.c;h=913d06c11f8d7cbd24121ecd772396b7c10f4826;hb=HEAD
+
+#pragma once
+
+#include <stdint.h>
+
+static const int dither_size = 64;
+static const int dither_size2 = 4096;
+static const uint16_t dither_matrix[] = {
+ 3, 2603, 743, 2361, 199, 2711, 806, 3181, 2113, 3820, 988, 3485, 1560, 2650, 327, 3063, 1660, 3340, 356, 1055, 2218, 2796, 3278, 528, 2104, 2909, 1465, 4076, 911, 3358, 1586, 3039, 41, 4038, 690, 3493, 155, 3754, 636, 3403, 271, 1355, 2973, 1816, 1287, 3908, 1095, 2549, 4086, 984, 3452, 357, 3560, 1662, 39, 1362, 2954, 1538, 552, 2823, 1871, 319, 2964, 792,
+ 3590, 1799, 4095, 1197, 3302, 2009, 3974, 1602, 475, 1433, 2801, 2059, 551, 4067, 1916, 914, 3887, 1308, 2969, 1817, 3780, 790, 1262, 1828, 3421, 802, 2454, 126, 3006, 2212, 536, 2513, 1029, 2142, 2920, 1641, 3178, 1902, 2848, 966, 2579, 3567, 2252, 142, 2798, 388, 3014, 2005, 216, 2285, 1583, 2994, 1091, 2813, 2002, 3712, 856, 4069, 2292, 1256, 3682, 2405, 1359, 3225,
+ 1046, 2855, 404, 2646, 1698, 545, 1138, 2982, 1985, 3610, 133, 3256, 1794, 971, 3167, 2251, 483, 2613, 668, 3468, 75, 3156, 2398, 3838, 201, 2690, 1060, 3599, 1820, 1168, 3890, 1743, 3564, 1487, 372, 2617, 1078, 462, 2193, 3915, 1659, 567, 1123, 3806, 1897, 3623, 849, 1483, 3513, 2857, 777, 2164, 3993, 584, 3338, 461, 2748, 1806, 206, 3264, 729, 1993, 3925, 492,
+ 2269, 1508, 3497, 927, 3743, 2109, 3469, 248, 2697, 1041, 2404, 1260, 3741, 2162, 52, 3526, 1214, 3722, 2085, 1449, 2557, 1938, 442, 1418, 2987, 1612, 3232, 2139, 654, 2884, 245, 2754, 621, 3060, 3858, 1341, 3698, 2948, 1436, 64, 3194, 2025, 3092, 1506, 595, 2216, 3252, 2632, 484, 1239, 3625, 122, 1864, 2589, 1475, 2360, 1195, 3588, 2536, 1010, 2890, 110, 2628, 1735,
+ 3110, 217, 1939, 2913, 101, 2483, 1274, 3124, 1636, 3791, 810, 3049, 417, 2828, 1416, 2515, 1992, 219, 3088, 844, 3343, 1171, 3632, 2201, 637, 3939, 354, 1330, 3800, 1943, 3284, 1390, 2347, 947, 2047, 200, 2128, 739, 3511, 2707, 1177, 4051, 336, 2501, 3472, 1316, 17, 2087, 3801, 1892, 2466, 1409, 3241, 1031, 3844, 152, 3123, 631, 1485, 3835, 1664, 3396, 1181, 3569,
+ 890, 3790, 2353, 1098, 3229, 820, 3910, 448, 2319, 579, 2618, 1903, 3502, 873, 3988, 585, 3015, 1639, 2261, 4055, 292, 2809, 894, 3168, 1959, 995, 2627, 3128, 2282, 506, 1050, 3986, 125, 3537, 2804, 1533, 3291, 2523, 1860, 539, 2328, 829, 2866, 1965, 941, 2718, 4021, 1444, 877, 3210, 695, 3755, 391, 2980, 839, 3378, 1739, 2217, 3031, 351, 2334, 760, 2468, 413,
+ 2833, 1382, 553, 4013, 1886, 2660, 1512, 2092, 3541, 1391, 3950, 202, 1543, 2705, 1734, 3312, 990, 3765, 607, 1317, 2458, 1813, 3906, 134, 2390, 3415, 1671, 38, 1218, 3668, 2535, 1552, 2657, 1883, 518, 4094, 1028, 304, 3884, 1303, 3557, 1680, 3406, 203, 3717, 1854, 422, 2304, 2944, 249, 2651, 1757, 2297, 1576, 2706, 2014, 524, 3970, 1139, 2063, 3639, 1417, 4053, 1763,
+ 2150, 3450, 2616, 1247, 334, 3355, 650, 2955, 20, 3187, 885, 2178, 3352, 507, 2351, 162, 2735, 1492, 3254, 2030, 3544, 565, 1575, 2921, 1376, 547, 4037, 2017, 3337, 1730, 287, 3247, 660, 3048, 1201, 2383, 1935, 3138, 1594, 2902, 132, 2758, 1358, 2279, 763, 3203, 1574, 3527, 1157, 3938, 958, 3383, 1130, 4012, 234, 3545, 1335, 2558, 27, 3206, 651, 2949, 209, 3170,
+ 977, 50, 1647, 3087, 2189, 1733, 3768, 1331, 2012, 2470, 1484, 2999, 979, 3863, 1288, 3603, 2147, 447, 2876, 6, 1122, 2673, 3368, 837, 3769, 2529, 965, 2699, 575, 2914, 2149, 954, 3693, 1635, 3441, 11, 3725, 780, 2425, 953, 3797, 2171, 677, 3957, 2587, 1108, 2849, 656, 2129, 1650, 2881, 63, 2612, 735, 2348, 951, 3106, 1614, 3490, 1834, 1223, 2597, 1523, 2306,
+ 3851, 1983, 3714, 599, 3524, 179, 2408, 904, 4050, 688, 3427, 369, 2671, 1994, 2931, 645, 1686, 3931, 1221, 2553, 3842, 2071, 305, 2276, 1829, 221, 3227, 1452, 3552, 1096, 3956, 1472, 2438, 335, 2187, 2889, 1428, 2746, 415, 3224, 1851, 1086, 2957, 350, 1764, 3393, 116, 2516, 3662, 398, 2209, 3716, 1310, 3464, 1480, 3762, 381, 2853, 772, 2374, 3943, 487, 3542, 616,
+ 1302, 2963, 925, 2731, 1148, 2869, 1414, 3026, 310, 2740, 1744, 3699, 1573, 76, 1126, 3300, 2493, 878, 3438, 1849, 785, 1478, 3653, 2765, 1085, 3605, 2246, 383, 1878, 2479, 106, 2841, 801, 3901, 1842, 610, 3518, 1104, 3996, 2270, 549, 3492, 2105, 3640, 931, 2224, 3916, 1866, 816, 3193, 1024, 1845, 3161, 502, 2990, 1942, 2475, 1035, 3673, 243, 1977, 3097, 1679, 2121,
+ 3318, 363, 2287, 1590, 3989, 500, 3453, 1796, 3659, 1240, 2331, 834, 3115, 2537, 4070, 2050, 212, 3079, 2267, 371, 2834, 3223, 1270, 527, 3130, 2052, 938, 2757, 3775, 740, 3471, 1656, 3281, 1062, 3102, 1349, 2512, 2090, 173, 1577, 3100, 1412, 40, 1931, 3152, 520, 1253, 2906, 1467, 2366, 4081, 254, 2089, 2522, 1174, 123, 4040, 2205, 1557, 3196, 1076, 2686, 103, 3920,
+ 850, 2585, 3651, 147, 1909, 2562, 989, 2275, 761, 3235, 186, 3852, 1212, 1888, 603, 1399, 3749, 1561, 711, 3969, 2138, 158, 1997, 4079, 1615, 65, 3913, 1778, 1301, 3072, 2083, 394, 2599, 1990, 154, 3818, 480, 3351, 1827, 3665, 853, 2734, 4066, 1128, 2538, 1619, 3257, 192, 3606, 557, 1625, 2793, 867, 3894, 1791, 3366, 1384, 472, 2779, 843, 3802, 1332, 2434, 1453,
+ 2867, 1807, 1198, 2181, 3244, 1311, 3823, 81, 2649, 1381, 2776, 2046, 449, 3574, 2161, 3237, 1067, 2118, 2996, 1700, 1044, 3696, 2654, 759, 2455, 2966, 707, 3157, 256, 2362, 920, 4058, 671, 3601, 2263, 1528, 2976, 922, 2630, 373, 2432, 1971, 580, 3385, 289, 3770, 1960, 2598, 992, 2160, 3069, 1131, 3240, 370, 2745, 899, 3036, 1863, 3575, 2268, 399, 2953, 724, 3270,
+ 314, 3516, 658, 3795, 412, 2997, 809, 3349, 1771, 3999, 702, 3449, 1657, 2621, 280, 2795, 512, 3615, 66, 2531, 3420, 477, 1568, 3357, 1250, 2100, 3508, 1001, 2006, 3727, 1397, 2988, 1812, 1135, 2800, 812, 1922, 3587, 1395, 3935, 1167, 3055, 1554, 2231, 2858, 1203, 773, 3964, 1703, 3433, 5, 3812, 1918, 2393, 1457, 3778, 664, 2569, 53, 1468, 3387, 1727, 4077, 2222,
+ 1037, 1966, 3089, 1367, 2762, 1694, 2355, 1087, 2837, 328, 2373, 1286, 3126, 961, 3928, 1746, 2430, 1230, 3177, 857, 1435, 2888, 2198, 264, 3777, 456, 1539, 2578, 3371, 592, 2655, 36, 2173, 3345, 297, 4010, 2397, 73, 2832, 661, 3310, 168, 3830, 691, 1420, 3547, 2305, 387, 2912, 742, 2643, 1544, 614, 3619, 164, 2172, 3316, 1259, 3978, 2042, 613, 2743, 210, 1413,
+ 3680, 2637, 13, 2316, 709, 4059, 220, 3670, 1569, 3285, 903, 3786, 31, 2293, 1354, 3342, 738, 4033, 2221, 1877, 3879, 728, 3246, 1926, 1141, 2821, 4006, 156, 1193, 1907, 3231, 1507, 3753, 948, 3083, 1667, 718, 3142, 1780, 2278, 1338, 2747, 1716, 3139, 2136, 94, 3085, 1978, 1356, 3723, 1065, 2101, 3008, 1300, 2831, 1899, 455, 3113, 939, 2898, 3608, 1097, 2126, 3147,
+ 532, 1527, 3911, 1769, 3322, 1296, 2656, 2078, 563, 2000, 2586, 1714, 2926, 782, 2677, 181, 2938, 1535, 396, 2840, 197, 2435, 1032, 3626, 2542, 653, 2235, 1800, 2875, 3872, 353, 2456, 644, 2606, 1351, 2132, 3514, 1100, 3857, 324, 3451, 796, 2526, 419, 3747, 1766, 981, 3362, 2400, 187, 2728, 4023, 416, 3499, 881, 3849, 1521, 2431, 1841, 333, 2299, 1567, 3831, 964,
+ 2402, 3411, 1068, 2543, 340, 3109, 907, 3460, 1372, 3942, 259, 3408, 1103, 3631, 1632, 3813, 1049, 2043, 3581, 1344, 3332, 1604, 2983, 21, 1663, 3305, 1272, 3664, 519, 2208, 1048, 3555, 2029, 3949, 465, 2896, 228, 2623, 1579, 2478, 1450, 4045, 1147, 3326, 909, 2573, 4003, 513, 1249, 3532, 1775, 910, 2340, 1717, 2610, 624, 3391, 194, 3912, 2704, 765, 3243, 108, 2919,
+ 1689, 291, 2959, 826, 3779, 1626, 2250, 74, 2694, 847, 3013, 1460, 2401, 382, 2864, 627, 2463, 3140, 797, 2666, 515, 3990, 824, 2156, 3846, 341, 2911, 932, 2723, 1638, 3080, 1387, 149, 1748, 3266, 1202, 3711, 942, 3248, 744, 2970, 24, 2344, 1910, 2977, 232, 2107, 2892, 1850, 3047, 343, 2037, 3402, 85, 3220, 1278, 2200, 2854, 905, 1430, 3746, 1975, 2506, 686,
+ 3997, 2247, 1255, 3484, 2152, 544, 3034, 3870, 1520, 3528, 2180, 606, 4090, 1781, 2123, 3354, 1591, 80, 3767, 1955, 1245, 2363, 1750, 3165, 1089, 2472, 1566, 3455, 83, 4085, 692, 2685, 3475, 2356, 741, 2769, 1693, 2314, 395, 3637, 2070, 1658, 3467, 481, 1407, 3660, 1531, 753, 3819, 1047, 2532, 3902, 1161, 2491, 1621, 4065, 433, 1676, 3573, 2226, 490, 1189, 3091, 1790,
+ 944, 3309, 1920, 135, 1440, 2799, 1859, 1115, 2427, 443, 1847, 2816, 993, 3041, 238, 1199, 3981, 2254, 949, 2900, 3456, 237, 3658, 548, 1961, 3024, 708, 2313, 1976, 1178, 3301, 2084, 556, 1321, 3880, 2015, 90, 4016, 1914, 2680, 970, 3118, 1194, 3930, 2508, 937, 3282, 2439, 57, 2777, 1578, 510, 3127, 769, 2939, 957, 2041, 3238, 28, 3019, 1890, 3389, 263, 3676,
+ 2076, 474, 3067, 2450, 4043, 800, 3556, 250, 3365, 1306, 3841, 107, 3419, 1410, 3570, 2197, 499, 3093, 1831, 425, 1550, 2652, 924, 2414, 4030, 190, 3546, 1007, 3708, 2645, 290, 1607, 3728, 2591, 358, 3205, 1162, 2863, 1346, 633, 3874, 279, 2766, 766, 2082, 3025, 438, 1946, 3629, 808, 3473, 2307, 1343, 3796, 265, 2415, 3729, 1323, 2473, 793, 3836, 1350, 2782, 1070,
+ 2624, 3860, 864, 1741, 437, 3098, 985, 2607, 1934, 2933, 828, 2703, 1889, 466, 2572, 1493, 2794, 1136, 3643, 2056, 3914, 1172, 3412, 1826, 1327, 3146, 1651, 2530, 440, 1486, 3073, 2242, 1033, 1815, 2972, 1515, 3463, 489, 2469, 3347, 1454, 2311, 1726, 3197, 124, 1622, 4091, 1156, 2170, 2946, 1835, 167, 2700, 1968, 3320, 1227, 469, 3121, 1018, 2727, 1761, 428, 1969, 3265,
+ 60, 1524, 2301, 3593, 2683, 1269, 2207, 3891, 593, 1564, 3633, 1176, 2375, 3745, 967, 3904, 752, 3313, 180, 2452, 701, 2820, 59, 2923, 467, 2163, 866, 3941, 1898, 3392, 657, 3991, 2, 3592, 831, 2378, 982, 3798, 2099, 140, 3062, 876, 3737, 1279, 3548, 2376, 725, 3344, 301, 1318, 3961, 1107, 3562, 578, 1517, 2861, 2184, 1643, 3922, 193, 3479, 2309, 4088, 840,
+ 2487, 3498, 1235, 272, 2031, 3707, 33, 1394, 3148, 2120, 225, 3290, 731, 2044, 3208, 8, 2349, 1691, 2715, 1371, 3162, 1631, 2233, 3821, 1079, 3481, 2806, 112, 2929, 1054, 2420, 1446, 2847, 1933, 3180, 223, 2736, 1879, 1211, 3582, 1773, 2441, 424, 2648, 588, 1927, 2788, 1500, 3117, 2447, 602, 2880, 2190, 1856, 4015, 96, 3628, 604, 1913, 2882, 1319, 628, 1466, 3027,
+ 1688, 564, 2732, 3236, 715, 1613, 3306, 2273, 430, 3980, 2548, 1627, 2865, 348, 1722, 2958, 1309, 3690, 860, 4072, 397, 3561, 634, 1438, 2674, 1870, 663, 2377, 1295, 3825, 274, 3487, 1173, 550, 2169, 4060, 756, 3263, 367, 2611, 623, 4032, 2067, 3356, 1064, 3885, 178, 3645, 818, 1809, 3377, 1456, 375, 3137, 859, 2682, 1073, 3400, 2437, 913, 3242, 2604, 3638, 325,
+ 2206, 3898, 1982, 1084, 4018, 2521, 1020, 2856, 1832, 1234, 886, 3480, 1277, 4046, 2215, 1045, 3428, 283, 3021, 2027, 1284, 2642, 1912, 3262, 257, 3917, 1601, 3595, 2065, 716, 3212, 1633, 2687, 3703, 1021, 1674, 3029, 1378, 3896, 1609, 3018, 1365, 43, 1708, 2829, 1553, 2514, 1266, 2300, 3829, 14, 2525, 3720, 1292, 2380, 1718, 3050, 1464, 303, 3994, 2055, 114, 1923, 1142,
+ 3184, 1353, 145, 3001, 2102, 313, 3517, 679, 3685, 2016, 2915, 95, 2459, 641, 3163, 454, 2600, 1767, 2277, 586, 3444, 163, 3764, 962, 2429, 1140, 3191, 320, 2981, 1753, 2564, 845, 2054, 307, 3304, 2232, 79, 2659, 827, 2294, 1026, 3489, 2560, 3804, 361, 3287, 687, 3053, 408, 1375, 2985, 1774, 778, 3272, 262, 3900, 517, 2103, 3151, 726, 1565, 3759, 2502, 3520,
+ 784, 2324, 3740, 1758, 955, 3131, 1546, 2413, 195, 3211, 786, 3757, 1785, 3535, 1434, 2049, 3882, 1124, 3735, 1459, 2554, 2079, 1505, 3040, 751, 2752, 1419, 2329, 972, 3736, 131, 3104, 3973, 1290, 2770, 672, 3448, 1762, 3694, 231, 3222, 521, 1963, 855, 2326, 1160, 3979, 1697, 3509, 2066, 617, 4052, 2290, 1600, 2810, 1158, 2570, 3549, 998, 2288, 2993, 1237, 503, 1777,
+ 2860, 414, 2726, 570, 3649, 2256, 841, 3862, 1322, 2561, 1652, 2280, 1069, 2583, 170, 2886, 673, 2443, 68, 3334, 734, 3953, 364, 2214, 3677, 32, 4063, 573, 3279, 1469, 2407, 1911, 511, 2343, 1504, 3807, 1053, 2381, 1297, 2785, 1653, 2417, 3646, 1445, 3169, 1996, 113, 2771, 999, 2546, 3253, 1188, 153, 3641, 591, 3414, 1900, 46, 1782, 3814, 240, 2196, 3417, 969,
+ 4047, 2114, 1481, 3381, 1952, 87, 2952, 2053, 3360, 589, 4022, 337, 3380, 833, 3960, 1603, 3293, 963, 3046, 1981, 2802, 991, 3174, 1824, 1164, 3132, 1620, 2676, 2081, 378, 3883, 1094, 2960, 3580, 174, 2034, 2878, 379, 3315, 612, 3983, 1238, 160, 2842, 562, 3404, 2399, 798, 3731, 269, 1562, 2907, 1973, 2631, 1403, 2320, 926, 3683, 2764, 1133, 3195, 1443, 2667, 1872,
+ 0, 3099, 1027, 2462, 1191, 3982, 1617, 365, 1118, 3077, 1389, 2713, 1811, 2995, 2127, 482, 2011, 3598, 1655, 470, 1369, 3565, 2332, 649, 2850, 1930, 794, 3572, 1120, 3394, 887, 2609, 1587, 733, 1846, 3228, 836, 3888, 2010, 2688, 945, 2956, 2143, 1677, 3704, 1282, 1855, 2968, 1431, 2308, 3919, 453, 3386, 720, 3972, 345, 3221, 2001, 431, 2426, 667, 3893, 377, 3617,
+ 2039, 746, 3832, 296, 3286, 615, 2357, 3613, 1940, 2262, 37, 3578, 568, 1326, 3721, 1153, 2789, 270, 2541, 4035, 2203, 146, 1534, 3375, 306, 3871, 2186, 214, 2556, 1893, 3071, 56, 3350, 2074, 4089, 1186, 2626, 1491, 19, 1713, 3470, 390, 3803, 722, 2566, 299, 4075, 497, 3437, 1102, 1802, 2580, 1144, 1862, 2852, 1580, 2485, 1271, 4073, 1729, 2870, 1222, 2339, 872,
+ 3297, 2551, 1294, 2897, 1737, 2719, 1313, 3042, 771, 3810, 1490, 2411, 1949, 2664, 130, 3116, 1494, 3787, 1246, 598, 3260, 1848, 3773, 1112, 2480, 946, 2941, 1532, 4009, 611, 1682, 3824, 1226, 2729, 285, 3028, 534, 3531, 2509, 3150, 1265, 2385, 1405, 3329, 1768, 3108, 1190, 1987, 2724, 54, 3577, 842, 3808, 2219, 119, 3751, 700, 2986, 884, 3482, 100, 3215, 1408, 2759,
+ 1593, 478, 3558, 2062, 817, 3734, 207, 1797, 2581, 468, 3158, 815, 3945, 980, 3486, 2259, 770, 1788, 3185, 2370, 1030, 2903, 748, 2708, 1805, 3602, 530, 3218, 1248, 2238, 2894, 486, 2396, 851, 3622, 1666, 2354, 1137, 1947, 712, 4031, 902, 3009, 78, 2317, 803, 3533, 2486, 879, 3143, 2115, 3017, 317, 3209, 1244, 2058, 3407, 253, 2266, 1526, 2517, 767, 3657, 196,
+ 4011, 1861, 2346, 109, 3086, 1011, 2112, 4061, 1229, 3447, 1683, 2767, 282, 2891, 1843, 494, 3372, 2639, 16, 1951, 3705, 312, 2068, 4002, 70, 1455, 2756, 2003, 157, 3454, 1117, 3687, 1366, 3207, 2144, 674, 3869, 182, 3671, 2188, 277, 2684, 1598, 3881, 1061, 2822, 198, 1616, 3760, 1393, 569, 1673, 2712, 1522, 3591, 861, 2575, 1752, 3288, 1025, 3948, 1814, 2134, 3038,
+ 933, 3239, 1213, 3897, 1582, 2423, 3182, 626, 2681, 136, 2327, 1127, 3612, 2179, 1182, 4093, 2095, 935, 3895, 697, 2528, 1597, 3033, 1152, 2359, 3325, 1016, 3799, 1756, 2593, 342, 2008, 2817, 121, 1497, 3280, 1760, 2786, 1285, 3084, 1732, 3597, 576, 1917, 3426, 1357, 3940, 2091, 376, 3335, 2013, 3877, 819, 2440, 401, 3070, 1345, 3866, 541, 2797, 288, 3333, 574, 1529,
+ 2457, 405, 2846, 659, 3395, 346, 1184, 3483, 1595, 3853, 901, 3275, 1755, 680, 3056, 229, 1581, 2975, 1348, 3330, 1233, 3609, 485, 3435, 705, 2048, 322, 2461, 736, 3944, 1545, 3341, 936, 4017, 2614, 402, 2428, 776, 3348, 488, 2584, 1228, 2158, 3061, 318, 2391, 632, 3022, 2453, 1043, 2778, 141, 2961, 1175, 4044, 1919, 25, 2159, 3145, 1624, 2336, 1225, 2629, 3840,
+ 1106, 3501, 2148, 1479, 2641, 1830, 2922, 1998, 504, 2096, 3005, 432, 2488, 3826, 1402, 2448, 3692, 366, 2225, 2772, 175, 2140, 2826, 1334, 2608, 3739, 1571, 3495, 2108, 1232, 2678, 559, 2325, 1905, 1145, 3503, 1360, 3955, 2124, 994, 3855, 151, 3370, 1013, 2737, 1709, 3630, 1462, 783, 4020, 1801, 1307, 3689, 2274, 629, 2670, 3538, 787, 1289, 3585, 917, 3742, 1684, 51,
+ 3002, 1712, 218, 3642, 749, 3954, 26, 3600, 1000, 2721, 1361, 3656, 1210, 58, 3153, 640, 1974, 3213, 898, 1819, 4026, 928, 1779, 3839, 139, 1056, 2989, 525, 3234, 10, 3553, 1648, 3792, 258, 3101, 699, 2838, 49, 1474, 2945, 1772, 2412, 1547, 4062, 822, 3245, 7, 2035, 3135, 298, 3384, 2192, 498, 1838, 3364, 1038, 1556, 3023, 2444, 177, 2716, 501, 2905, 2061,
+ 883, 4084, 2602, 1200, 2386, 1392, 2812, 2248, 1728, 4027, 242, 1875, 2873, 2166, 1644, 3554, 1080, 2524, 3772, 505, 2369, 3160, 434, 2286, 3192, 1882, 2392, 1363, 2720, 1970, 1003, 2918, 895, 2763, 2106, 3730, 1585, 2379, 3589, 418, 3259, 669, 2773, 352, 2291, 1092, 2691, 3724, 1170, 2661, 952, 2862, 1518, 3043, 222, 2518, 3905, 360, 1692, 4019, 1936, 3432, 973, 3258,
+ 2245, 555, 1925, 3074, 444, 3319, 1066, 392, 3204, 813, 2358, 3461, 865, 3976, 458, 2644, 1839, 127, 1514, 2917, 1163, 2028, 3519, 1220, 642, 4068, 278, 3604, 605, 3975, 2503, 406, 3359, 1793, 1264, 471, 3328, 1059, 1921, 2672, 1254, 3794, 1364, 3621, 1789, 3488, 1563, 652, 2337, 1695, 3822, 82, 3465, 1231, 3672, 2045, 940, 2223, 3120, 685, 1320, 2367, 1784, 326,
+ 3766, 1519, 3424, 923, 3706, 1867, 2155, 3783, 1451, 2937, 1608, 543, 2596, 1477, 3336, 960, 3899, 2032, 3425, 863, 3661, 44, 1589, 2893, 2135, 1511, 3064, 1759, 2310, 1293, 1833, 3761, 2227, 102, 4078, 2033, 2702, 239, 3984, 732, 2228, 98, 2992, 713, 2590, 233, 2433, 3992, 384, 2965, 755, 2477, 1881, 681, 2775, 529, 1804, 3373, 1155, 2550, 3793, 148, 3507, 2693,
+ 1057, 2924, 93, 2714, 1588, 204, 2971, 694, 2544, 104, 3892, 1958, 3250, 189, 2130, 3003, 355, 3122, 648, 2582, 1740, 2213, 3932, 331, 3331, 891, 2533, 781, 3176, 185, 3004, 721, 1180, 3125, 2333, 908, 1654, 3096, 1421, 3431, 2825, 1736, 2125, 3339, 1423, 3058, 805, 1932, 3226, 1386, 3571, 1105, 4092, 2167, 1548, 3771, 2899, 69, 3607, 464, 1724, 3045, 1204, 1964,
+ 2389, 706, 3971, 1132, 2449, 3859, 1166, 3369, 1280, 3476, 930, 2422, 1268, 3718, 745, 1705, 2717, 1379, 2271, 3847, 450, 2814, 1022, 2692, 1315, 3647, 89, 3845, 1052, 3536, 1584, 2157, 3652, 1510, 682, 2868, 3624, 538, 2489, 1006, 362, 3876, 1051, 435, 3774, 1146, 3543, 1005, 2237, 191, 2023, 2636, 410, 3200, 183, 2321, 889, 2133, 1488, 2874, 2234, 643, 4041, 389,
+ 3594, 1642, 2094, 3295, 523, 2040, 2675, 359, 2295, 1853, 3051, 316, 2824, 1002, 2500, 4071, 1114, 3529, 211, 1304, 3075, 1441, 3382, 693, 2387, 1665, 2901, 1263, 2445, 1915, 581, 3308, 329, 2481, 3834, 188, 2165, 1217, 3750, 1876, 3283, 1470, 2725, 2281, 1702, 2815, 77, 2594, 3929, 1629, 3324, 906, 2940, 1267, 3523, 1388, 4008, 2662, 762, 3702, 1063, 2749, 1525, 3159,
+ 852, 2998, 261, 2739, 1822, 976, 3550, 1672, 4057, 554, 1374, 3828, 1894, 3202, 4, 2220, 572, 1884, 3198, 2119, 723, 3618, 176, 2036, 3968, 427, 2183, 3401, 368, 4036, 2871, 1324, 2709, 1880, 1072, 3149, 1723, 2811, 23, 2394, 757, 2984, 159, 3966, 582, 2093, 3268, 1463, 618, 2859, 463, 3715, 1725, 2283, 655, 3068, 386, 1675, 3011, 266, 1986, 3410, 12, 2073,
+ 2474, 1216, 3732, 900, 3924, 2364, 47, 2830, 871, 2605, 3255, 2191, 597, 1610, 3443, 1406, 3655, 2555, 875, 3995, 1570, 2471, 1731, 3000, 804, 3230, 975, 1798, 2620, 897, 2211, 48, 3923, 835, 3446, 2257, 583, 3311, 1549, 4054, 2098, 1273, 3405, 1967, 1340, 3663, 862, 2007, 3748, 1219, 1924, 2416, 30, 3861, 1165, 2467, 1929, 3379, 1121, 3952, 2567, 915, 1787, 3909,
+ 509, 3442, 1501, 2563, 457, 1559, 3353, 1352, 3654, 2060, 150, 1125, 3579, 2574, 1039, 2962, 323, 1628, 3010, 88, 2753, 535, 3713, 1040, 2663, 1891, 3811, 213, 3616, 1429, 3439, 1715, 2117, 2932, 380, 1385, 3889, 1110, 2592, 608, 3094, 411, 2547, 807, 3119, 338, 2315, 3037, 224, 3398, 2710, 1017, 3103, 1516, 3292, 236, 3744, 719, 2382, 1555, 537, 3522, 2647, 1305,
+ 2774, 2024, 128, 3190, 1945, 2947, 646, 2510, 421, 1236, 3936, 2698, 1792, 284, 3756, 832, 2322, 3867, 1090, 2241, 3323, 1333, 2137, 3141, 22, 1299, 2803, 2080, 768, 2835, 452, 3066, 639, 1243, 3679, 2635, 2019, 321, 3627, 1312, 1989, 3726, 1489, 3551, 2122, 1605, 4064, 1081, 1770, 2204, 666, 3998, 1858, 540, 2111, 2668, 1427, 2936, 97, 3129, 1895, 2210, 241, 3719,
+ 1009, 3307, 1810, 934, 3634, 1119, 3959, 1719, 3445, 2916, 1599, 533, 3179, 2365, 1447, 2844, 1953, 526, 3430, 1887, 789, 3815, 347, 1513, 4082, 2460, 542, 3172, 1670, 2260, 3987, 1368, 3510, 2240, 1808, 115, 3035, 1618, 2146, 3361, 184, 2335, 662, 2783, 42, 2925, 596, 2571, 3596, 956, 2877, 295, 2552, 3494, 987, 3926, 893, 1840, 3667, 1241, 3856, 1083, 3216, 727,
+ 2342, 409, 4080, 2436, 300, 2741, 2185, 171, 2026, 683, 2230, 3738, 1339, 747, 4005, 138, 3219, 1502, 2565, 255, 3076, 1699, 2368, 3418, 638, 1747, 3500, 1113, 3763, 120, 1023, 2689, 252, 2792, 774, 4025, 1008, 3217, 791, 2665, 1150, 4004, 1837, 997, 3850, 1437, 3413, 1984, 129, 3171, 1606, 3688, 1206, 2244, 1803, 429, 3112, 2418, 811, 2696, 344, 2496, 1749, 2967,
+ 1377, 2839, 1099, 2077, 3271, 838, 1442, 3583, 2679, 1034, 3054, 62, 2069, 3267, 1852, 2519, 1205, 3695, 869, 4024, 2176, 1101, 2974, 918, 2151, 2843, 267, 2406, 1496, 2951, 2174, 1690, 3817, 1143, 3299, 1979, 2495, 476, 3868, 1498, 2979, 493, 3134, 2490, 1937, 445, 2350, 854, 3946, 1169, 2341, 775, 3007, 84, 2805, 3563, 2097, 215, 3294, 1640, 3477, 870, 4014, 71,
+ 3678, 1711, 3504, 670, 1623, 3864, 2883, 546, 1645, 4034, 1396, 3457, 2634, 473, 1015, 3559, 423, 2202, 2887, 1611, 558, 3374, 137, 1896, 3644, 1075, 3937, 825, 3458, 460, 3691, 590, 2075, 3081, 439, 1337, 3440, 1742, 2318, 67, 1954, 3491, 1401, 230, 3648, 2751, 1328, 3012, 1687, 2658, 374, 3314, 1704, 4074, 1448, 620, 1329, 3958, 1999, 560, 2088, 2791, 1257, 2284,
+ 821, 2492, 227, 3057, 2540, 29, 1258, 2239, 3016, 273, 2424, 635, 1251, 3684, 2255, 1681, 3105, 1432, 45, 3506, 2619, 1261, 3886, 2484, 436, 3107, 1701, 2625, 1380, 2352, 1551, 3199, 929, 2410, 1865, 3781, 246, 2942, 1185, 3681, 2588, 912, 2264, 3269, 1208, 714, 3752, 268, 3466, 730, 3837, 1242, 2722, 919, 2372, 3416, 2615, 880, 2851, 1476, 3788, 286, 3289, 1668,
+ 3399, 1314, 3921, 1795, 1036, 3478, 1948, 3697, 921, 3397, 1707, 3878, 2004, 2943, 166, 2733, 814, 3918, 2086, 978, 1957, 3155, 622, 1461, 2750, 2064, 61, 3346, 676, 3032, 205, 1821, 3977, 1, 2819, 892, 2442, 754, 3233, 566, 1537, 3963, 678, 1696, 2895, 1904, 2465, 1458, 2131, 2904, 1776, 2298, 226, 3710, 1988, 308, 1630, 3636, 34, 3095, 1116, 2464, 703, 2638,
+ 385, 2781, 600, 2323, 3175, 764, 2808, 403, 1825, 2695, 1082, 2845, 339, 1499, 4087, 1179, 3376, 459, 2827, 3758, 294, 2345, 1818, 3409, 959, 3784, 1276, 1857, 4042, 1149, 3534, 2520, 1209, 3363, 1422, 3525, 1634, 4083, 2194, 1823, 3082, 293, 2633, 3530, 99, 3927, 531, 3114, 1058, 9, 3620, 874, 3277, 684, 3052, 1215, 3188, 1908, 2265, 758, 3434, 1885, 3967, 968,
+ 3701, 2022, 3459, 1503, 332, 4001, 1473, 2168, 3833, 117, 2338, 779, 3173, 2153, 609, 2576, 1637, 2409, 846, 1542, 3321, 1077, 4056, 169, 2978, 561, 3214, 2253, 400, 2807, 1928, 495, 2991, 689, 2177, 420, 2885, 1088, 172, 2761, 1281, 3422, 1991, 1109, 2388, 943, 3317, 1669, 4029, 2653, 1398, 2497, 1540, 2818, 1836, 3865, 522, 983, 4049, 2601, 1536, 165, 2182, 3164,
+ 1572, 92, 1183, 2927, 1873, 2451, 1093, 3201, 888, 3044, 1439, 3789, 1151, 3512, 1783, 3650, 251, 3274, 1868, 2669, 710, 2872, 1370, 2446, 2038, 1646, 2577, 1012, 3635, 2145, 974, 3805, 2272, 1738, 3933, 2527, 1342, 3303, 1944, 3843, 514, 2296, 788, 3709, 1592, 2780, 2020, 281, 2249, 665, 3462, 315, 3985, 1074, 111, 2534, 2018, 2930, 1411, 441, 3576, 2836, 1347, 508,
+ 2744, 2243, 3951, 696, 3666, 161, 3521, 516, 2494, 1962, 3436, 491, 2476, 15, 2760, 868, 2258, 1291, 3875, 91, 3700, 2141, 393, 3611, 750, 3903, 275, 3296, 1596, 105, 3144, 1541, 260, 3183, 1042, 118, 3686, 594, 2384, 996, 3539, 1710, 3133, 235, 3030, 587, 3785, 1154, 3020, 1941, 1196, 3154, 1720, 2236, 3474, 1325, 3614, 247, 3249, 1844, 2229, 1014, 3776, 1869,
+ 3515, 986, 1956, 2545, 1336, 3078, 2051, 1649, 4048, 309, 1192, 2787, 1721, 3388, 1426, 3965, 2928, 496, 2175, 3111, 1019, 1745, 3065, 1482, 3186, 1252, 2879, 698, 2507, 3947, 896, 2742, 3586, 1400, 2110, 2934, 1706, 2738, 1383, 2950, 35, 2482, 1425, 4039, 1187, 2505, 1509, 3390, 479, 3873, 2395, 848, 2768, 407, 2910, 619, 1754, 2755, 1207, 3816, 717, 3059, 276, 2498,
+ 647, 2908, 311, 3166, 571, 2303, 916, 2935, 1283, 3276, 1874, 3907, 823, 2302, 577, 1995, 1129, 3566, 1415, 601, 2539, 3540, 858, 2595, 18, 2330, 1751, 3505, 1111, 1906, 2403, 625, 2199, 446, 3782, 799, 3367, 349, 4007, 830, 3251, 1071, 2730, 426, 1972, 3568, 72, 2154, 2790, 1404, 144, 3674, 1471, 3854, 1004, 2072, 3934, 882, 2419, 86, 2640, 1685, 3298, 1530,
+ 3848, 1373, 3429, 1661, 3809, 1424, 3584, 55, 2622, 675, 2421, 208, 3090, 1224, 3675, 2504, 143, 2057, 2701, 4028, 1558, 244, 1980, 3962, 1159, 3669, 451, 2021, 2784, 302, 3733, 1298, 3261, 1786, 2568, 1275, 2289, 1495, 2499, 1765, 2116, 3827, 704, 3423, 2371, 795, 3189, 1678, 630, 3136, 1901, 2559, 737, 2195, 3273, 2511, 330, 3327, 1950, 3496, 950, 4000, 1134, 2312,
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/test/CMakeLists.txt b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/test/CMakeLists.txt
new file mode 100644
index 0000000..5acdc09
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/test/CMakeLists.txt
@@ -0,0 +1,3 @@
+set(SOURCES TestConversionMatrix.cpp)
+
+core_add_test_library(videoshaders_test)
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/test/TestConversionMatrix.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/test/TestConversionMatrix.cpp
new file mode 100644
index 0000000..f6bc7f0
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/test/TestConversionMatrix.cpp
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2005-2020 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 "cores/VideoPlayer/VideoRenderers/VideoShaders/ConversionMatrix.h"
+#include "xbmc/utils/MathUtils.h"
+
+#include <iostream>
+
+#include <gtest/gtest.h>
+
+// clang-format off
+
+namespace
+{
+
+constexpr bool DEBUG_PRINT{false};
+
+void DebugPrint(Matrix3 matrix)
+{
+ if (!DEBUG_PRINT)
+ return;
+
+ std::cout << std::fixed;
+
+ std::cout << "===== Matrix Start =====" << std::endl;
+
+ for (unsigned int i = 0; i < 3; i++)
+ {
+ std::cout << std::setprecision(10)
+ << "{" << matrix[i][0] << ", "
+ << matrix[i][1] << ", "
+ << matrix[i][2] << "}," << std::endl;
+ }
+
+ std::cout << "===== Matrix End =====" << std::endl;
+}
+
+void DebugPrint(Matrix4 matrix)
+{
+ if (!DEBUG_PRINT)
+ return;
+
+ std::cout << std::fixed;
+
+ std::cout << "===== Matrix Start =====" << std::endl;
+
+ for (unsigned int i = 0; i < 4; i++)
+ {
+ std::cout << std::setprecision(10)
+ << "{" << matrix[i][0] << ", "
+ << matrix[i][1] << ", "
+ << matrix[i][2] << ", "
+ << matrix[i][3] << "}," << std::endl;
+ }
+
+ std::cout << "===== Matrix End =====" << std::endl;
+}
+
+template<uint8_t Order>
+bool CompareMatrices(CMatrix<Order>& matrixA, CMatrix<Order>& matrixB, float threshold)
+{
+ bool result = true;
+ for (unsigned int i = 0; i < Order; i++)
+ {
+ for (unsigned int j = 0; j < Order; j++)
+ {
+ result &= MathUtils::FloatEquals(matrixA[i][j], matrixB[i][j], threshold);
+ }
+ }
+ return result;
+}
+
+std::array<std::array<float, 4>, 4> bt709_8bit =
+{{
+ {1.1643834114, 1.1643834114, 1.1643834114, 0.0000000000},
+ {0.0000000000, -0.2132485956, 2.1124014854, 0.0000000000},
+ {1.7927408218, -0.5329092145, 0.0000000000, 0.0000000000},
+ {-0.9729449749, 0.3014826477, -1.1334021091, 1.0000000000}
+}};
+
+Matrix4 bt709Mat_8bit(bt709_8bit);
+
+std::array<std::array<float, 4>, 4> bt709_10bit =
+{{
+ {1.1643834114, 1.1643834114, 1.1643834114, 0.0000000000},
+ {0.0000000000, -0.2132485956, 2.1124014854, 0.0000000000},
+ {1.7927408218, -0.5329092145, 0.0000000000, 0.0000000000},
+ {-0.9729449749, 0.3014826477, -1.1334021091, 1.0000000000},
+}};
+
+Matrix4 bt709Mat_10bit(bt709_10bit);
+
+std::array<std::array<float, 4>, 4> bt709_10bit_texture =
+{{
+ {74.5922470093, 74.5922470093, 74.5922470093, 0.0000000000},
+ {0.0000000000, -13.6610431671, 135.3237915039, 0.0000000000},
+ {114.8458175659, -34.1390075684, 0.0000000000, 0.0000000000},
+ {-0.9729449749, 0.3014826477, -1.1334021091, 1.0000000000},
+}};
+
+Matrix4 bt709Mat_10bit_texture(bt709_10bit_texture);
+
+std::array<std::array<float, 3>, 3> bt601_to_bt709 =
+{{
+ {1.0440435410, -0.0000000230, 0.0000000056},
+ {-0.0440433398, 0.9999997616, 0.0117934495},
+ {-0.0000000298, -0.0000000075, 0.9882065654},
+}};
+
+Matrix3 bt601_to_bt709_Mat(bt601_to_bt709);
+
+std::array<std::array<float, 3>, 3> bt2020_to_bt709 =
+{{
+ {1.6604905128, -0.1245504916, -0.0181507617},
+ {-0.5876411796, 1.1328998804, -0.1005788445},
+ {-0.0728498399, -0.0083493963, 1.1187294722},
+}};
+
+Matrix3 bt2020_to_bt709_Mat(bt2020_to_bt709);
+
+} // namespace
+
+TEST(TestConvertMatrix, YUV2RGB)
+{
+ CConvertMatrix convMat;
+ convMat.SetSourceColorSpace(AVCOL_SPC_BT709)
+ .SetSourceLimitedRange(true);
+
+ convMat.SetDestinationBlack(0.0f)
+ .SetDestinationContrast(1.0f)
+ .SetDestinationLimitedRange(false);
+
+ Matrix4 yuvMat;
+
+ convMat.SetSourceBitDepth(8)
+ .SetSourceTextureBitDepth(8);
+ yuvMat = convMat.GetYuvMat();
+ DebugPrint(yuvMat);
+
+ EXPECT_TRUE(CompareMatrices(bt709Mat_8bit, yuvMat, 0.0001f));
+
+ convMat.SetSourceBitDepth(10)
+ .SetSourceTextureBitDepth(8);
+ yuvMat = convMat.GetYuvMat();
+ DebugPrint(yuvMat);
+
+ EXPECT_TRUE(CompareMatrices(bt709Mat_10bit, yuvMat, 0.0001f));
+
+ convMat.SetSourceBitDepth(10)
+ .SetSourceTextureBitDepth(10);
+ yuvMat = convMat.GetYuvMat();
+ DebugPrint(yuvMat);
+
+ EXPECT_TRUE(CompareMatrices(bt709Mat_10bit_texture, yuvMat, 0.0001f));
+}
+
+TEST(TestConvertMatrix, ColorSpaceConversion)
+{
+ CConvertMatrix convMat;
+ Matrix3 primMat;
+
+ convMat.SetSourceColorPrimaries(AVCOL_PRI_BT470BG)
+ .SetDestinationColorPrimaries(AVCOL_PRI_BT709);
+ primMat = convMat.GetPrimMat();
+ DebugPrint(primMat);
+
+ EXPECT_TRUE(CompareMatrices(bt601_to_bt709_Mat, primMat, 0.0001f));
+
+ convMat.SetSourceColorPrimaries(AVCOL_PRI_BT2020)
+ .SetDestinationColorPrimaries(AVCOL_PRI_BT709);
+ primMat = convMat.GetPrimMat();
+ DebugPrint(primMat);
+
+ EXPECT_TRUE(CompareMatrices(bt2020_to_bt709_Mat, primMat, 0.0001f));
+}
+
+// clang-format on
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/WinRenderer.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/WinRenderer.cpp
new file mode 100644
index 0000000..3ec27f8
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/WinRenderer.cpp
@@ -0,0 +1,353 @@
+/*
+ * 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 "WinRenderer.h"
+
+#include "RenderCapture.h"
+#include "RenderCaptureDX.h"
+#include "RenderFactory.h"
+#include "RenderFlags.h"
+#include "rendering/dx/RenderContext.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+#include "windows/RendererDXVA.h"
+#include "windows/RendererShaders.h"
+#include "windows/RendererSoftware.h"
+
+#include <mutex>
+
+struct render_details
+{
+ using map = std::map<RenderMethod, int>;
+ using weights_fn = std::function<void(map&, const VideoPicture&)>;
+ using create_fn = std::function<CRendererBase*(CVideoSettings&)>;
+
+ RenderMethod method;
+ std::string name;
+ create_fn create;
+ weights_fn weights;
+
+ template<class T>
+ constexpr static render_details get(RenderMethod method, const std::string& name)
+ {
+ return { method, name, T::Create, T::GetWeight };
+ }
+};
+
+static std::vector<render_details> RenderMethodDetails =
+{
+ render_details::get<CRendererSoftware>(RENDER_SW, "Software"),
+ render_details::get<CRendererShaders>(RENDER_PS, "Pixel Shaders"),
+ render_details::get<CRendererDXVA>(RENDER_DXVA, "DXVA"),
+};
+
+CBaseRenderer* CWinRenderer::Create(CVideoBuffer*)
+{
+ return new CWinRenderer();
+}
+
+bool CWinRenderer::Register()
+{
+ VIDEOPLAYER::CRendererFactory::RegisterRenderer("default", Create);
+ return true;
+}
+
+CWinRenderer::CWinRenderer()
+{
+ m_format = AV_PIX_FMT_NONE;
+ PreInit();
+}
+
+CWinRenderer::~CWinRenderer()
+{
+ CWinRenderer::UnInit();
+}
+
+CRendererBase* CWinRenderer::SelectRenderer(const VideoPicture& picture)
+{
+ int iRequestedMethod = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_RENDERMETHOD);
+ CLog::LogF(LOGDEBUG, "requested render method: {}", iRequestedMethod);
+
+ std::map<RenderMethod, int> weights;
+ for (auto& details : RenderMethodDetails)
+ details.weights(weights, picture);
+
+ RenderMethod method;
+ switch (iRequestedMethod)
+ {
+ case RENDER_METHOD_SOFTWARE:
+ if (weights[RENDER_SW])
+ {
+ method = RENDER_SW;
+ break;
+ }
+ // fallback to PS
+ case RENDER_METHOD_D3D_PS:
+ if (weights[RENDER_PS])
+ {
+ method = RENDER_PS;
+ break;
+ }
+ //fallback to DXVA
+ case RENDER_METHOD_DXVA:
+ if (weights[RENDER_DXVA])
+ {
+ method = RENDER_DXVA;
+ break;
+ }
+ // fallback to AUTO
+ case RENDER_METHOD_AUTO:
+ default:
+ {
+ const auto it = std::max_element(weights.begin(), weights.end(),
+ [](auto& w1, auto& w2) { return w1.second < w2.second; });
+
+ if (it != weights.end())
+ {
+ method = it->first;
+ break;
+ }
+
+ // there is no elements in weights, so no renderer which supports incoming video buffer
+ CLog::LogF(LOGERROR, "unable to select render method for video buffer");
+ return nullptr;
+ }
+ }
+
+ const auto it = std::find_if(RenderMethodDetails.begin(), RenderMethodDetails.end(),
+ [method](render_details& d) { return d.method == method; });
+
+ if (it != RenderMethodDetails.end())
+ {
+ CLog::LogF(LOGDEBUG, "selected render method: {}", it->name);
+ return it->create(m_videoSettings);
+ }
+
+ // something goes really wrong
+ return nullptr;
+}
+
+CRect CWinRenderer::GetScreenRect() const
+{
+ CRect screenRect(0.f, 0.f,
+ static_cast<float>(CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth()),
+ static_cast<float>(CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight()));
+
+ switch (CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode())
+ {
+ case RENDER_STEREO_MODE_SPLIT_HORIZONTAL:
+ screenRect.y2 *= 2;
+ break;
+ case RENDER_STEREO_MODE_SPLIT_VERTICAL:
+ screenRect.x2 *= 2;
+ break;
+ default:
+ break;
+ }
+
+ return screenRect;
+}
+
+bool CWinRenderer::Configure(const VideoPicture &picture, float fps, unsigned int orientation)
+{
+ m_sourceWidth = picture.iWidth;
+ m_sourceHeight = picture.iHeight;
+ m_renderOrientation = orientation;
+ m_fps = fps;
+ m_iFlags = GetFlagsChromaPosition(picture.chroma_position)
+ | GetFlagsColorMatrix(picture.color_space, picture.iWidth, picture.iHeight)
+ | GetFlagsColorPrimaries(picture.color_primaries)
+ | GetFlagsStereoMode(picture.stereoMode);
+ m_format = picture.videoBuffer->GetFormat();
+
+ // calculate the input frame aspect ratio
+ CalculateFrameAspectRatio(picture.iDisplayWidth, picture.iDisplayHeight);
+ SetViewMode(m_videoSettings.m_ViewMode);
+ ManageRenderArea();
+
+ m_renderer.reset(SelectRenderer(picture));
+ if (!m_renderer || !m_renderer->Configure(picture, fps, orientation))
+ {
+ m_renderer.reset();
+ return false;
+ }
+
+ m_bConfigured = true;
+ return true;
+}
+
+int CWinRenderer::NextBuffer() const
+{
+ return m_renderer->NextBuffer();
+}
+
+void CWinRenderer::AddVideoPicture(const VideoPicture &picture, int index)
+{
+ m_renderer->AddVideoPicture(picture, index);
+}
+
+void CWinRenderer::Update()
+{
+ if (!m_bConfigured)
+ return;
+
+ ManageRenderArea();
+ m_renderer->ManageTextures();
+}
+
+void CWinRenderer::RenderUpdate(int index, int index2, bool clear, unsigned int flags, unsigned int alpha)
+{
+ if (!m_bConfigured)
+ return;
+
+ if (clear)
+ CServiceBroker::GetWinSystem()->GetGfxContext().Clear(DX::Windowing()->UseLimitedColor() ? 0x101010 : 0);
+ DX::Windowing()->SetAlphaBlendEnable(alpha < 255);
+
+ ManageRenderArea();
+ m_renderer->Render(index, index2, DX::Windowing()->GetBackBuffer(), m_sourceRect, m_destRect,
+ GetScreenRect(), flags);
+ DX::Windowing()->SetAlphaBlendEnable(true);
+}
+
+bool CWinRenderer::RenderCapture(CRenderCapture* capture)
+{
+ if (!m_bConfigured)
+ return false;
+
+ capture->BeginRender();
+ if (capture->GetState() != CAPTURESTATE_FAILED)
+ {
+ const CRect destRect(0, 0, static_cast<float>(capture->GetWidth()), static_cast<float>(capture->GetHeight()));
+
+ auto cap = static_cast<CRenderCaptureDX*>(capture);
+
+ m_renderer->Render(cap->GetTarget(), m_sourceRect, destRect, GetScreenRect());
+ capture->EndRender();
+
+ return true;
+ }
+
+ return false;
+}
+
+void CWinRenderer::SetBufferSize(int numBuffers)
+{
+ if (!m_bConfigured)
+ return;
+
+ m_renderer->SetBufferSize(numBuffers);
+}
+
+void CWinRenderer::PreInit()
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ m_bConfigured = false;
+ UnInit();
+}
+
+void CWinRenderer::UnInit()
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ m_renderer.reset();
+ m_bConfigured = false;
+}
+
+bool CWinRenderer::Flush(bool saveBuffers)
+{
+ if (!m_bConfigured)
+ return false;
+
+ return m_renderer->Flush(saveBuffers);
+}
+
+bool CWinRenderer::Supports(ERENDERFEATURE feature) const
+{
+ if(feature == RENDERFEATURE_BRIGHTNESS)
+ return true;
+
+ if(feature == RENDERFEATURE_CONTRAST)
+ return true;
+
+ if (feature == RENDERFEATURE_STRETCH ||
+ feature == RENDERFEATURE_NONLINSTRETCH ||
+ feature == RENDERFEATURE_ZOOM ||
+ feature == RENDERFEATURE_VERTICAL_SHIFT ||
+ feature == RENDERFEATURE_PIXEL_RATIO ||
+ feature == RENDERFEATURE_ROTATION ||
+ feature == RENDERFEATURE_POSTPROCESS ||
+ feature == RENDERFEATURE_TONEMAP)
+ return true;
+
+ return false;
+}
+
+bool CWinRenderer::Supports(ESCALINGMETHOD method) const
+{
+ if (!m_bConfigured)
+ return false;
+
+ return m_renderer->Supports(method);
+}
+
+bool CWinRenderer::WantsDoublePass()
+{
+ if (!m_bConfigured)
+ return false;
+
+ return m_renderer->WantsDoublePass();
+}
+
+bool CWinRenderer::ConfigChanged(const VideoPicture& picture)
+{
+ if (!m_bConfigured)
+ return true;
+
+ return picture.videoBuffer->GetFormat() != m_format;
+}
+
+CRenderInfo CWinRenderer::GetRenderInfo()
+{
+ if (!m_bConfigured)
+ return {};
+
+ return m_renderer->GetRenderInfo();
+}
+
+void CWinRenderer::ReleaseBuffer(int idx)
+{
+ if (!m_bConfigured)
+ return;
+
+ m_renderer->ReleaseBuffer(idx);
+}
+
+bool CWinRenderer::NeedBuffer(int idx)
+{
+ if (!m_bConfigured)
+ return false;
+
+ return m_renderer->NeedBuffer(idx);
+}
+
+DEBUG_INFO_VIDEO CWinRenderer::GetDebugInfo(int idx)
+{
+ if (!m_bConfigured)
+ return {};
+
+ return m_renderer->GetDebugInfo(idx);
+}
+
+CRenderCapture* CWinRenderer::GetRenderCapture()
+{
+ return new CRenderCaptureDX;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/WinRenderer.h b/xbmc/cores/VideoPlayer/VideoRenderers/WinRenderer.h
new file mode 100644
index 0000000..731f3ff
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/WinRenderer.h
@@ -0,0 +1,62 @@
+/*
+ * 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 "BaseRenderer.h"
+#include "windows/RendererBase.h"
+
+struct VideoPicture;
+class CRenderCapture;
+
+class CWinRenderer : public CBaseRenderer
+{
+public:
+ CWinRenderer();
+ ~CWinRenderer();
+
+ static CBaseRenderer* Create(CVideoBuffer *buffer);
+ static bool Register();
+
+ void Update() override;
+ bool RenderCapture(CRenderCapture* capture) override;
+
+ // Player functions
+ bool Configure(const VideoPicture &picture, float fps, unsigned int orientation) override;
+ void AddVideoPicture(const VideoPicture &picture, int index) override;
+ void UnInit() override;
+ bool IsConfigured() override { return m_bConfigured; }
+ bool Flush(bool saveBuffers) override;
+ CRenderInfo GetRenderInfo() override;
+ void RenderUpdate(int index, int index2, bool clear, unsigned int flags, unsigned int alpha) override;
+ void SetBufferSize(int numBuffers) override;
+ void ReleaseBuffer(int idx) override;
+ bool NeedBuffer(int idx) override;
+
+ // Feature support
+ bool SupportsMultiPassRendering() override { return false; }
+ bool Supports(ERENDERFEATURE feature) const override;
+ bool Supports(ESCALINGMETHOD method) const override;
+
+ bool WantsDoublePass() override;
+ bool ConfigChanged(const VideoPicture& picture) override;
+
+ // Debug info video
+ DEBUG_INFO_VIDEO GetDebugInfo(int idx) override;
+
+ CRenderCapture* GetRenderCapture() override;
+
+protected:
+ void PreInit();
+ int NextBuffer() const;
+ CRendererBase* SelectRenderer(const VideoPicture &picture);
+ CRect GetScreenRect() const;
+
+ bool m_bConfigured = false;
+ std::unique_ptr<CRendererBase> m_renderer;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/windows/CMakeLists.txt b/xbmc/cores/VideoPlayer/VideoRenderers/windows/CMakeLists.txt
new file mode 100644
index 0000000..3747b80
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/windows/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES RendererBase.cpp
+ RendererDXVA.cpp
+ RendererHQ.cpp
+ RendererShaders.cpp
+ RendererSoftware.cpp)
+
+set(HEADERS RendererBase.h
+ RendererDXVA.h
+ RendererHQ.h
+ RendererShaders.h
+ RendererSoftware.h)
+
+core_add_library(videorenderers-windows)
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererBase.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererBase.cpp
new file mode 100644
index 0000000..0c422b6
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererBase.cpp
@@ -0,0 +1,726 @@
+/*
+ * 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.
+ */
+
+#include "RendererBase.h"
+
+#include "DVDCodecs/Video/DVDVideoCodec.h"
+#include "DVDCodecs/Video/DXVA.h"
+#include "ServiceBroker.h"
+#include "VideoRenderers/BaseRenderer.h"
+#include "VideoRenderers/RenderFlags.h"
+#include "cores/VideoPlayer/Buffers/VideoBuffer.h"
+#include "rendering/dx/RenderContext.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/MemUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+using namespace Microsoft::WRL;
+
+void CRenderBuffer::AppendPicture(const VideoPicture& picture)
+{
+ videoBuffer = picture.videoBuffer;
+ videoBuffer->Acquire();
+
+ pictureFlags = picture.iFlags;
+ primaries = static_cast<AVColorPrimaries>(picture.color_primaries);
+ color_space = static_cast<AVColorSpace>(picture.color_space);
+ color_transfer = static_cast<AVColorTransferCharacteristic>(picture.color_transfer);
+ full_range = picture.color_range == 1;
+ bits = picture.colorBits;
+ stereoMode = picture.stereoMode;
+ pixelFormat = picture.pixelFormat;
+
+ hasDisplayMetadata = picture.hasDisplayMetadata;
+ displayMetadata = picture.displayMetadata;
+ lightMetadata = picture.lightMetadata;
+ hasLightMetadata = picture.hasLightMetadata && picture.lightMetadata.MaxCLL;
+ if (hasDisplayMetadata && displayMetadata.has_luminance && !displayMetadata.max_luminance.num)
+ displayMetadata.has_luminance = 0;
+}
+
+void CRenderBuffer::ReleasePicture()
+{
+ if (videoBuffer)
+ videoBuffer->Release();
+ videoBuffer = nullptr;
+ m_bLoaded = false;
+}
+
+CRenderBuffer::CRenderBuffer(AVPixelFormat av_pix_format, unsigned width, unsigned height)
+ : av_format(av_pix_format) , m_width(width) , m_height(height), m_widthTex(width), m_heightTex(height)
+{
+}
+
+HRESULT CRenderBuffer::GetResource(ID3D11Resource** ppResource, unsigned* index) const
+{
+ if (!ppResource)
+ return E_POINTER;
+ if (!index)
+ return E_POINTER;
+
+ auto dxva = dynamic_cast<DXVA::CVideoBuffer*>(videoBuffer);
+ if (!dxva)
+ return E_NOT_SET;
+
+ ComPtr<ID3D11Resource> pResource;
+ const HRESULT hr = dxva->GetResource(&pResource);
+ if (SUCCEEDED(hr))
+ {
+ *ppResource = pResource.Detach();
+ *index = dxva->GetIdx();
+ }
+
+ return hr;
+}
+
+void CRenderBuffer::QueueCopyFromGPU()
+{
+ if (!videoBuffer)
+ return;
+
+ unsigned index;
+ ComPtr<ID3D11Resource> pResource;
+ const HRESULT hr = GetResource(&pResource, &index);
+
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "unable to open d3d11va resource.");
+ return;
+ }
+
+ if (!m_staging)
+ {
+ // create staging texture
+ ComPtr<ID3D11Texture2D> surface;
+ if (SUCCEEDED(pResource.As(&surface)))
+ {
+ D3D11_TEXTURE2D_DESC tDesc;
+ surface->GetDesc(&tDesc);
+
+ CD3D11_TEXTURE2D_DESC sDesc(tDesc);
+ sDesc.ArraySize = 1;
+ sDesc.Usage = D3D11_USAGE_STAGING;
+ sDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ sDesc.BindFlags = 0;
+ sDesc.MiscFlags = 0;
+
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ if (SUCCEEDED(pDevice->CreateTexture2D(&sDesc, nullptr, &m_staging)))
+ m_sDesc = sDesc;
+ }
+ }
+
+ if (m_staging)
+ {
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetImmediateContext();
+ // queue copying content from decoder texture to temporary texture.
+ // actual data copying will be performed before rendering
+ pContext->CopySubresourceRegion(m_staging.Get(), D3D11CalcSubresource(0, 0, 1), 0, 0, 0,
+ pResource.Get(), D3D11CalcSubresource(0, index, 1), nullptr);
+ m_bPending = true;
+ }
+}
+
+
+CRendererBase::CRendererBase(CVideoSettings& videoSettings)
+ : m_videoSettings(videoSettings)
+{
+ m_colorManager.reset(new CColorManager());
+}
+
+CRendererBase::~CRendererBase()
+{
+ if (DX::Windowing()->IsHDROutput())
+ {
+ CLog::LogF(LOGDEBUG, "Restoring SDR rendering");
+ DX::Windowing()->SetHdrColorSpace(DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709);
+ if (m_AutoSwitchHDR)
+ DX::Windowing()->ToggleHDR(); // Toggle display HDR OFF
+ }
+ Flush(false);
+}
+
+CRenderInfo CRendererBase::GetRenderInfo()
+{
+ CRenderInfo info;
+ info.formats =
+ {
+ AV_PIX_FMT_D3D11VA_VLD,
+ AV_PIX_FMT_NV12,
+ AV_PIX_FMT_P010,
+ AV_PIX_FMT_P016,
+ AV_PIX_FMT_YUV420P,
+ AV_PIX_FMT_YUV420P10,
+ AV_PIX_FMT_YUV420P16
+ };
+ info.max_buffer_size = NUM_BUFFERS;
+ info.optimal_buffer_size = 4;
+
+ return info;
+}
+
+bool CRendererBase::Configure(const VideoPicture& picture, float fps, unsigned orientation)
+{
+ m_iNumBuffers = 0;
+ m_iBufferIndex = 0;
+
+ m_sourceWidth = picture.iWidth;
+ m_sourceHeight = picture.iHeight;
+ m_fps = fps;
+ m_renderOrientation = orientation;
+
+ m_useDithering = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool("videoscreen.dither");
+ m_ditherDepth = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt("videoscreen.ditherdepth");
+
+ m_lastHdr10 = {};
+ m_HdrType = HDR_TYPE::HDR_NONE_SDR;
+ m_useHLGtoPQ = false;
+ m_AutoSwitchHDR = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ DX::Windowing()->SETTING_WINSYSTEM_IS_HDR_DISPLAY) &&
+ DX::Windowing()->IsHDRDisplay();
+
+ // Auto switch HDR only if supported and "Settings/Player/Use HDR display capabilities" = ON
+ if (m_AutoSwitchHDR)
+ {
+ bool streamIsHDR = (picture.color_primaries == AVCOL_PRI_BT2020) &&
+ (picture.color_transfer == AVCOL_TRC_SMPTE2084 ||
+ picture.color_transfer == AVCOL_TRC_ARIB_STD_B67);
+
+ if (streamIsHDR != DX::Windowing()->IsHDROutput())
+ DX::Windowing()->ToggleHDR();
+ }
+
+ return true;
+}
+
+void CRendererBase::AddVideoPicture(const VideoPicture& picture, int index)
+{
+ if (m_renderBuffers[index])
+ {
+ m_renderBuffers[index]->AppendPicture(picture);
+ m_renderBuffers[index]->frameIdx = m_frameIdx;
+ m_frameIdx += 2;
+ }
+}
+
+void CRendererBase::Render(int index,
+ int index2,
+ CD3DTexture& target,
+ const CRect& sourceRect,
+ const CRect& destRect,
+ const CRect& viewRect,
+ unsigned flags)
+{
+ m_iBufferIndex = index;
+ ManageTextures();
+ Render(target, sourceRect, destRect, viewRect, flags);
+}
+
+void CRendererBase::Render(CD3DTexture& target, const CRect& sourceRect, const CRect& destRect, const CRect& viewRect, unsigned flags)
+{
+ if (m_iNumBuffers == 0)
+ return;
+
+ CRenderBuffer* buf = m_renderBuffers[m_iBufferIndex];
+ if (!buf->IsLoaded())
+ {
+ if (!buf->UploadBuffer())
+ return;
+ }
+
+ ProcessHDR(buf);
+
+ if (m_viewWidth != static_cast<unsigned>(viewRect.Width()) ||
+ m_viewHeight != static_cast<unsigned>(viewRect.Height()))
+ {
+ m_viewWidth = static_cast<unsigned>(viewRect.Width());
+ m_viewHeight = static_cast<unsigned>(viewRect.Height());
+
+ OnViewSizeChanged();
+ }
+
+ CheckVideoParameters();
+ UpdateVideoFilters();
+
+ CPoint dest[4];
+ CRect source = sourceRect; // can be changed
+ CRect(destRect).GetQuad(dest); // can be changed
+
+ RenderImpl(m_IntermediateTarget, source, dest, flags);
+
+ if (m_toneMapping)
+ {
+ m_outputShader->SetDisplayMetadata(buf->hasDisplayMetadata, buf->displayMetadata, buf->hasLightMetadata, buf->lightMetadata);
+ m_outputShader->SetToneMapParam(m_toneMapMethod, m_videoSettings.m_ToneMapParam);
+ }
+
+ FinalOutput(m_IntermediateTarget, target, source, dest);
+
+ // Restore our view port.
+ DX::Windowing()->RestoreViewPort();
+ DX::Windowing()->ApplyStateBlock();
+}
+
+void CRendererBase::FinalOutput(CD3DTexture& source, CD3DTexture& target, const CRect& src, const CPoint(&destPoints)[4])
+{
+ m_outputShader->Render(source, src, destPoints, target);
+}
+
+void CRendererBase::ManageTextures()
+{
+ if (m_iNumBuffers < m_iBuffersRequired)
+ {
+ for (int i = m_iNumBuffers; i < m_iBuffersRequired; i++)
+ CreateRenderBuffer(i);
+
+ m_iNumBuffers = m_iBuffersRequired;
+ }
+ else if (m_iNumBuffers > m_iBuffersRequired)
+ {
+ for (int i = m_iNumBuffers - 1; i >= m_iBuffersRequired; i--)
+ DeleteRenderBuffer(i);
+
+ m_iNumBuffers = m_iBuffersRequired;
+ m_iBufferIndex = m_iBufferIndex % m_iNumBuffers;
+ }
+}
+
+int CRendererBase::NextBuffer() const
+{
+ if (m_iNumBuffers)
+ return (m_iBufferIndex + 1) % m_iNumBuffers;
+ return -1;
+}
+
+void CRendererBase::ReleaseBuffer(int idx)
+{
+ if (m_renderBuffers[idx])
+ m_renderBuffers[idx]->ReleasePicture();
+}
+
+bool CRendererBase::Flush(bool saveBuffers)
+{
+ if (!saveBuffers)
+ {
+ for (int i = 0; i < NUM_BUFFERS; i++)
+ DeleteRenderBuffer(i);
+
+ m_iBufferIndex = 0;
+ m_iNumBuffers = 0;
+ }
+
+ return true;
+}
+
+bool CRendererBase::CreateRenderBuffer(int index)
+{
+ m_renderBuffers.insert(std::make_pair(index, CreateBuffer()));
+ return true;
+}
+
+void CRendererBase::DeleteRenderBuffer(int index)
+{
+ if (m_renderBuffers[index])
+ {
+ delete m_renderBuffers[index];
+ m_renderBuffers.erase(index);
+ }
+}
+
+bool CRendererBase::CreateIntermediateTarget(unsigned width, unsigned height, bool dynamic)
+{
+ DXGI_FORMAT format = DX::Windowing()->GetBackBuffer().GetFormat();
+
+ // don't create new one if it exists with requested size and format
+ if (m_IntermediateTarget.Get() && m_IntermediateTarget.GetFormat() == format
+ && m_IntermediateTarget.GetWidth() == width && m_IntermediateTarget.GetHeight() == height)
+ return true;
+
+ if (m_IntermediateTarget.Get())
+ m_IntermediateTarget.Release();
+
+ CLog::LogF(LOGDEBUG, "intermediate target format {}.", format);
+
+ if (!m_IntermediateTarget.Create(width, height, 1, dynamic ? D3D11_USAGE_DYNAMIC : D3D11_USAGE_DEFAULT, format))
+ {
+ CLog::LogF(LOGERROR, "intermediate target creation failed.");
+ return false;
+ }
+ return true;
+}
+
+void CRendererBase::OnCMSConfigChanged(AVColorPrimaries srcPrimaries)
+{
+ m_lutSize = 0;
+ m_lutIsLoading = true;
+
+ auto loadLutTask = Concurrency::create_task([this, srcPrimaries] {
+ // load 3DLUT data
+ int lutSize, dataSize;
+ if (!CColorManager::Get3dLutSize(CMS_DATA_FMT_RGBA, &lutSize, &dataSize))
+ return 0;
+
+ const auto lutData = static_cast<uint16_t*>(KODI::MEMORY::AlignedMalloc(dataSize, 16));
+ bool success = m_colorManager->GetVideo3dLut(srcPrimaries, &m_cmsToken, CMS_DATA_FMT_RGBA,
+ lutSize, lutData);
+ if (success)
+ {
+ success = COutputShader::CreateLUTView(lutSize, lutData, false, m_pLUTView.ReleaseAndGetAddressOf());
+ }
+ else
+ CLog::Log(LOGERROR, "CRendererBase::OnCMSConfigChanged: unable to loading the 3dlut data.");
+
+ KODI::MEMORY::AlignedFree(lutData);
+ if (!success)
+ return 0;
+
+ return lutSize;
+ });
+
+ loadLutTask.then([&](const int lutSize) {
+ m_lutSize = lutSize;
+ if (m_outputShader)
+ m_outputShader->SetLUT(m_lutSize, m_pLUTView.Get());
+ m_lutIsLoading = false;
+ });
+}
+
+// this is copy from CBaseRenderer::ReorderDrawPoints()
+void CRendererBase::ReorderDrawPoints(const CRect& destRect, CPoint(&rotatedPoints)[4]) const
+{
+ // 0 - top left, 1 - top right, 2 - bottom right, 3 - bottom left
+ float origMat[4][2] = {{destRect.x1, destRect.y1},
+ {destRect.x2, destRect.y1},
+ {destRect.x2, destRect.y2},
+ {destRect.x1, destRect.y2}};
+
+ const int pointOffset = m_renderOrientation / 90;
+
+ for (int destIdx = 0, srcIdx = pointOffset; destIdx < 4; destIdx++)
+ {
+ rotatedPoints[destIdx].x = origMat[srcIdx][0];
+ rotatedPoints[destIdx].y = origMat[srcIdx][1];
+
+ srcIdx++;
+ srcIdx = srcIdx % 4;
+ }
+}
+
+void CRendererBase::UpdateVideoFilters()
+{
+ if (!m_outputShader)
+ {
+ m_outputShader = std::make_shared<COutputShader>();
+ if (!m_outputShader->Create(m_cmsOn, m_useDithering, m_ditherDepth, m_toneMapping,
+ m_toneMapMethod, m_useHLGtoPQ))
+ {
+ CLog::LogF(LOGDEBUG, "unable to create output shader.");
+ m_outputShader.reset();
+ }
+ else if (m_pLUTView && m_lutSize)
+ {
+ m_outputShader->SetLUT(m_lutSize, m_pLUTView.Get());
+ }
+ }
+}
+
+void CRendererBase::CheckVideoParameters()
+{
+ CRenderBuffer* buf = m_renderBuffers[m_iBufferIndex];
+ ETONEMAPMETHOD method = m_videoSettings.m_ToneMapMethod;
+
+ bool isHDRPQ = (buf->color_transfer == AVCOL_TRC_SMPTE2084 && buf->primaries == AVCOL_PRI_BT2020);
+
+ bool toneMap = (isHDRPQ && m_HdrType == HDR_TYPE::HDR_NONE_SDR && method != VS_TONEMAPMETHOD_OFF);
+
+ bool hlg = (m_HdrType == HDR_TYPE::HDR_HLG);
+
+ if (toneMap != m_toneMapping || m_cmsOn != m_colorManager->IsEnabled() || hlg != m_useHLGtoPQ ||
+ method != m_toneMapMethod)
+ {
+ m_toneMapping = toneMap;
+ m_cmsOn = m_colorManager->IsEnabled();
+ m_useHLGtoPQ = hlg;
+ m_toneMapMethod = method;
+
+ m_outputShader.reset();
+ OnOutputReset();
+ }
+
+ if (m_cmsOn && !m_lutIsLoading)
+ {
+ const AVColorPrimaries color_primaries = static_cast<AVColorPrimaries>(buf->primaries);
+
+ if (!m_colorManager->CheckConfiguration(m_cmsToken, color_primaries))
+ OnCMSConfigChanged(color_primaries);
+ }
+}
+
+DXGI_FORMAT CRendererBase::GetDXGIFormat(const VideoPicture& picture)
+{
+ if (picture.videoBuffer && picture.videoBuffer->GetFormat() == AV_PIX_FMT_D3D11VA_VLD)
+ return GetDXGIFormat(picture.videoBuffer);
+
+ return DXGI_FORMAT_UNKNOWN;
+}
+
+DXGI_FORMAT CRendererBase::GetDXGIFormat(CVideoBuffer* videoBuffer)
+{
+ const auto dxva_buf = dynamic_cast<DXVA::CVideoBuffer*>(videoBuffer);
+ if (dxva_buf)
+ return dxva_buf->format;
+
+ return DXGI_FORMAT_UNKNOWN;
+}
+
+AVPixelFormat CRendererBase::GetAVFormat(DXGI_FORMAT dxgi_format)
+{
+ switch (dxgi_format)
+ {
+ case DXGI_FORMAT_NV12:
+ return AV_PIX_FMT_NV12;
+ case DXGI_FORMAT_P010:
+ return AV_PIX_FMT_P010;
+ case DXGI_FORMAT_P016:
+ return AV_PIX_FMT_P016;
+ default:
+ return AV_PIX_FMT_NONE;
+ }
+}
+
+DXGI_HDR_METADATA_HDR10 CRendererBase::GetDXGIHDR10MetaData(CRenderBuffer* rb)
+{
+ DXGI_HDR_METADATA_HDR10 hdr = {};
+
+ constexpr int FACTOR_1 = 50000;
+ constexpr int FACTOR_2 = 10000;
+
+ if (rb->hasDisplayMetadata && rb->displayMetadata.has_primaries)
+ {
+ if (rb->displayMetadata.display_primaries[0][0].den == FACTOR_1 &&
+ rb->displayMetadata.white_point[0].den == FACTOR_1)
+ {
+ hdr.RedPrimary[0] = static_cast<uint16_t>(rb->displayMetadata.display_primaries[0][0].num);
+ hdr.RedPrimary[1] = static_cast<uint16_t>(rb->displayMetadata.display_primaries[0][1].num);
+ hdr.GreenPrimary[0] = static_cast<uint16_t>(rb->displayMetadata.display_primaries[1][0].num);
+ hdr.GreenPrimary[1] = static_cast<uint16_t>(rb->displayMetadata.display_primaries[1][1].num);
+ hdr.BluePrimary[0] = static_cast<uint16_t>(rb->displayMetadata.display_primaries[2][0].num);
+ hdr.BluePrimary[1] = static_cast<uint16_t>(rb->displayMetadata.display_primaries[2][1].num);
+ hdr.WhitePoint[0] = static_cast<uint16_t>(rb->displayMetadata.white_point[0].num);
+ hdr.WhitePoint[1] = static_cast<uint16_t>(rb->displayMetadata.white_point[1].num);
+ }
+ else
+ {
+ hdr.RedPrimary[0] =
+ static_cast<uint16_t>(FACTOR_1 * av_q2d(rb->displayMetadata.display_primaries[0][0]));
+ hdr.RedPrimary[1] =
+ static_cast<uint16_t>(FACTOR_1 * av_q2d(rb->displayMetadata.display_primaries[0][1]));
+ hdr.GreenPrimary[0] =
+ static_cast<uint16_t>(FACTOR_1 * av_q2d(rb->displayMetadata.display_primaries[1][0]));
+ hdr.GreenPrimary[1] =
+ static_cast<uint16_t>(FACTOR_1 * av_q2d(rb->displayMetadata.display_primaries[1][1]));
+ hdr.BluePrimary[0] =
+ static_cast<uint16_t>(FACTOR_1 * av_q2d(rb->displayMetadata.display_primaries[2][0]));
+ hdr.BluePrimary[1] =
+ static_cast<uint16_t>(FACTOR_1 * av_q2d(rb->displayMetadata.display_primaries[2][1]));
+ hdr.WhitePoint[0] =
+ static_cast<uint16_t>(FACTOR_1 * av_q2d(rb->displayMetadata.white_point[0]));
+ hdr.WhitePoint[1] =
+ static_cast<uint16_t>(FACTOR_1 * av_q2d(rb->displayMetadata.white_point[1]));
+ }
+ }
+
+ if (rb->hasDisplayMetadata && rb->displayMetadata.has_luminance)
+ {
+ if (rb->displayMetadata.max_luminance.den == FACTOR_2 &&
+ rb->displayMetadata.min_luminance.den == FACTOR_2)
+ {
+ hdr.MaxMasteringLuminance = static_cast<uint32_t>(rb->displayMetadata.max_luminance.num);
+ hdr.MinMasteringLuminance = static_cast<uint32_t>(rb->displayMetadata.min_luminance.num);
+ }
+ else
+ {
+ hdr.MaxMasteringLuminance =
+ static_cast<uint32_t>(FACTOR_2 * av_q2d(rb->displayMetadata.max_luminance));
+ hdr.MinMasteringLuminance =
+ static_cast<uint32_t>(FACTOR_2 * av_q2d(rb->displayMetadata.min_luminance));
+ }
+ }
+
+ if (rb->hasLightMetadata)
+ {
+ hdr.MaxContentLightLevel = static_cast<uint16_t>(rb->lightMetadata.MaxCLL);
+ hdr.MaxFrameAverageLightLevel = static_cast<uint16_t>(rb->lightMetadata.MaxFALL);
+ }
+
+ return hdr;
+}
+
+void CRendererBase::ProcessHDR(CRenderBuffer* rb)
+{
+ if (m_AutoSwitchHDR && rb->primaries == AVCOL_PRI_BT2020 &&
+ (rb->color_transfer == AVCOL_TRC_SMPTE2084 || rb->color_transfer == AVCOL_TRC_ARIB_STD_B67) &&
+ !DX::Windowing()->IsHDROutput())
+ {
+ DX::Windowing()->ToggleHDR(); // Toggle display HDR ON
+ }
+
+ if (!DX::Windowing()->IsHDROutput())
+ {
+ if (m_HdrType != HDR_TYPE::HDR_NONE_SDR)
+ {
+ m_HdrType = HDR_TYPE::HDR_NONE_SDR;
+ m_lastHdr10 = {};
+ }
+ return;
+ }
+
+ // HDR10
+ if (rb->color_transfer == AVCOL_TRC_SMPTE2084 && rb->primaries == AVCOL_PRI_BT2020)
+ {
+ DXGI_HDR_METADATA_HDR10 hdr10 = GetDXGIHDR10MetaData(rb);
+ if (m_HdrType == HDR_TYPE::HDR_HDR10)
+ {
+ // Sets HDR10 metadata only if it differs from previous
+ if (0 != std::memcmp(&hdr10, &m_lastHdr10, sizeof(hdr10)))
+ {
+ DX::Windowing()->SetHdrMetaData(hdr10);
+ m_lastHdr10 = hdr10;
+ }
+ }
+ else
+ {
+ // Sets HDR10 metadata and enables HDR10 color space (switch to HDR rendering)
+ DX::Windowing()->SetHdrMetaData(hdr10);
+ CLog::LogF(LOGINFO, "Switching to HDR rendering");
+ DX::Windowing()->SetHdrColorSpace(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020);
+ m_HdrType = HDR_TYPE::HDR_HDR10;
+ m_lastHdr10 = hdr10;
+ }
+ }
+ // HLG
+ else if (rb->color_transfer == AVCOL_TRC_ARIB_STD_B67 && rb->primaries == AVCOL_PRI_BT2020)
+ {
+ if (m_HdrType != HDR_TYPE::HDR_HLG)
+ {
+ // Windows 10 doesn't support HLG HDR passthrough
+ // It's used HDR10 with reference metadata and shaders to convert HLG transfer to PQ transfer
+ // Values according BT.2100 recommendations
+ DXGI_HDR_METADATA_HDR10 hdr10 = {};
+ hdr10.RedPrimary[0] = 34000; // Display P3 primaries
+ hdr10.RedPrimary[1] = 16000;
+ hdr10.GreenPrimary[0] = 13250;
+ hdr10.GreenPrimary[1] = 34500;
+ hdr10.BluePrimary[0] = 7500;
+ hdr10.BluePrimary[1] = 3000;
+ hdr10.WhitePoint[0] = 15635;
+ hdr10.WhitePoint[1] = 16450;
+ hdr10.MaxMasteringLuminance = 1000 * 10000; // 1000 nits
+ hdr10.MinMasteringLuminance = 50; // 0.005 nits
+ DX::Windowing()->SetHdrMetaData(hdr10);
+ CLog::LogF(LOGINFO, "Switching to HDR rendering");
+ DX::Windowing()->SetHdrColorSpace(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020);
+ m_HdrType = HDR_TYPE::HDR_HLG;
+ }
+ }
+ // SDR
+ else
+ {
+ if (m_HdrType != HDR_TYPE::HDR_NONE_SDR)
+ {
+ // Switch to SDR rendering
+ CLog::LogF(LOGINFO, "Switching to SDR rendering");
+ DX::Windowing()->SetHdrColorSpace(DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709);
+ m_HdrType = HDR_TYPE::HDR_NONE_SDR;
+ m_lastHdr10 = {};
+ if (m_AutoSwitchHDR)
+ DX::Windowing()->ToggleHDR(); // Toggle display HDR OFF
+ }
+ }
+}
+
+DEBUG_INFO_VIDEO CRendererBase::GetDebugInfo(int idx)
+{
+ CRenderBuffer* rb = m_renderBuffers[idx];
+
+ const char* px = av_get_pix_fmt_name(rb->pixelFormat);
+ const char* pr = av_color_primaries_name(rb->primaries);
+ const char* tr = av_color_transfer_name(rb->color_transfer);
+
+ const std::string pixel = px ? px : "unknown";
+ const std::string prim = pr ? pr : "unknown";
+ const std::string trans = tr ? tr : "unknown";
+
+ const int max = static_cast<int>(std::exp2(rb->bits));
+ const int range_min = rb->full_range ? 0 : (max * 16) / 256;
+ const int range_max = rb->full_range ? max - 1 : (max * 235) / 256;
+
+ DEBUG_INFO_VIDEO info;
+
+ info.videoSource = StringUtils::Format(
+ "Source: {}x{}{}, fr: {:.3f}, pixel: {} {}-bit, range: {}-{}, matx: {}, trc: {}",
+ m_sourceWidth, m_sourceHeight, (rb->pictureFlags & DVP_FLAG_INTERLACED) ? "i" : "p", m_fps,
+ pixel, rb->bits, range_min, range_max, prim, trans);
+
+ info.metaPrim = "Primaries (meta): ";
+ info.metaLight = "HDR light (meta): ";
+
+ if (rb->hasDisplayMetadata && rb->displayMetadata.has_primaries &&
+ rb->displayMetadata.display_primaries[0][0].num)
+ {
+ double prim[3][2];
+ double wp[2];
+
+ for (int i = 0; i < 3; i++)
+ {
+ for (int j = 0; j < 2; j++)
+ prim[i][j] = static_cast<double>(rb->displayMetadata.display_primaries[i][j].num) /
+ static_cast<double>(rb->displayMetadata.display_primaries[i][j].den);
+ }
+
+ for (int j = 0; j < 2; j++)
+ wp[j] = static_cast<double>(rb->displayMetadata.white_point[j].num) /
+ static_cast<double>(rb->displayMetadata.white_point[j].den);
+
+ info.metaPrim += StringUtils::Format(
+ "R({:.3f} {:.3f}), G({:.3f} {:.3f}), B({:.3f} {:.3f}), WP({:.3f} {:.3f})", prim[0][0],
+ prim[0][1], prim[1][0], prim[1][1], prim[2][0], prim[2][1], wp[0], wp[1]);
+ }
+ else
+ {
+ info.metaPrim += "none";
+ }
+
+ if (rb->hasDisplayMetadata && rb->displayMetadata.has_luminance &&
+ rb->displayMetadata.max_luminance.num)
+ {
+ double maxML = static_cast<double>(rb->displayMetadata.max_luminance.num) /
+ static_cast<double>(rb->displayMetadata.max_luminance.den);
+ double minML = static_cast<double>(rb->displayMetadata.min_luminance.num) /
+ static_cast<double>(rb->displayMetadata.min_luminance.den);
+
+ info.metaLight += StringUtils::Format("max ML: {:.0f}, min ML: {:.4f}", maxML, minML);
+
+ if (rb->hasLightMetadata && rb->lightMetadata.MaxCLL)
+ {
+ info.metaLight += StringUtils::Format(", max CLL: {}, max FALL: {}", rb->lightMetadata.MaxCLL,
+ rb->lightMetadata.MaxFALL);
+ }
+ }
+ else
+ {
+ info.metaLight += "none";
+ }
+
+ if (m_outputShader)
+ info.shader = m_outputShader->GetDebugInfo();
+
+ return info;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererBase.h b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererBase.h
new file mode 100644
index 0000000..d4f401a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererBase.h
@@ -0,0 +1,191 @@
+/*
+ * 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 "VideoRenderers/ColorManager.h"
+#include "VideoRenderers/DebugInfo.h"
+#include "VideoRenderers/RenderInfo.h"
+#include "VideoRenderers/VideoShaders/WinVideoFilter.h"
+#include "cores/VideoSettings.h"
+#include "guilib/D3DResource.h"
+
+#include <vector>
+
+#include <d3d11_4.h>
+#include <dxgi1_5.h>
+extern "C" {
+#include <libavutil/mastering_display_metadata.h>
+#include <libavutil/pixdesc.h>
+}
+
+struct VideoPicture;
+class CVideoBuffer;
+
+namespace win
+{
+ namespace helpers
+ {
+ template<typename T>
+ bool contains(std::vector<T> vector, T item)
+ {
+ return find(vector.begin(), vector.end(), item) != vector.end();
+ }
+ }
+}
+
+enum RenderMethod
+{
+ RENDER_INVALID = 0,
+ RENDER_DXVA = 1,
+ RENDER_PS = 2,
+ RENDER_SW = 3
+};
+
+enum class HDR_TYPE
+{
+ HDR_NONE_SDR = 0,
+ HDR_HDR10 = 1,
+ HDR_HLG = 2
+};
+
+class CRenderBuffer
+{
+public:
+ virtual ~CRenderBuffer() = default;
+
+ unsigned GetWidth() const { return m_widthTex; }
+ unsigned GetHeight() const { return m_heightTex; }
+ bool IsLoaded() { return m_bLoaded; }
+
+ virtual void AppendPicture(const VideoPicture& picture);
+ virtual void ReleasePicture();
+ virtual bool UploadBuffer() { return false; }
+ virtual HRESULT GetResource(ID3D11Resource** ppResource, unsigned* index) const;
+
+ // implementation specified
+ virtual bool GetDataPlanes(uint8_t*(&planes)[3], int(&strides)[3]) { return false; }
+ virtual unsigned GetViewCount() const { return 0; }
+ virtual ID3D11View* GetView(unsigned viewIdx) { return nullptr; }
+
+ AVPixelFormat av_format;
+ CVideoBuffer* videoBuffer = nullptr;
+ unsigned int pictureFlags = 0;
+ AVColorPrimaries primaries = AVCOL_PRI_BT709;
+ AVColorSpace color_space = AVCOL_SPC_BT709;
+ AVColorTransferCharacteristic color_transfer = AVCOL_TRC_BT709;
+ bool full_range = false;
+ int bits = 8;
+ uint8_t texBits = 8;
+ AVPixelFormat pixelFormat = AV_PIX_FMT_NONE; // source pixel format
+ bool hasDisplayMetadata = false;
+ bool hasLightMetadata = false;
+ AVMasteringDisplayMetadata displayMetadata = {};
+ AVContentLightMetadata lightMetadata = {};
+ std::string stereoMode;
+ uint64_t frameIdx = 0;
+
+protected:
+ CRenderBuffer(AVPixelFormat av_pix_format, unsigned width, unsigned height);
+ void QueueCopyFromGPU();
+
+ // video buffer size
+ unsigned int m_width;
+ unsigned int m_height;
+ // real texture size
+ unsigned int m_widthTex;
+ unsigned int m_heightTex;
+ // copy from GPU mem
+ Microsoft::WRL::ComPtr<ID3D11Texture2D> m_staging;
+ D3D11_TEXTURE2D_DESC m_sDesc{};
+ bool m_bPending = false;
+ bool m_bLoaded = false;
+};
+
+class CRendererBase
+{
+public:
+ virtual ~CRendererBase();
+
+ virtual CRenderInfo GetRenderInfo();
+ virtual bool Configure(const VideoPicture &picture, float fps, unsigned int orientation);
+ virtual bool Supports(ESCALINGMETHOD method) const = 0;
+ virtual bool WantsDoublePass() { return false; }
+ virtual bool NeedBuffer(int idx) { return false; }
+
+ void AddVideoPicture(const VideoPicture &picture, int index);
+ void Render(int index, int index2, CD3DTexture& target, const CRect& sourceRect,
+ const CRect& destRect, const CRect& viewRect, unsigned flags);
+ void Render(CD3DTexture& target, const CRect& sourceRect, const CRect& destRect,
+ const CRect& viewRect, unsigned flags = 0);
+
+ void ManageTextures();
+ int NextBuffer() const;
+ void ReleaseBuffer(int idx);
+ bool Flush(bool saveBuffers);
+ void SetBufferSize(int numBuffers) { m_iBuffersRequired = numBuffers; }
+
+ DEBUG_INFO_VIDEO GetDebugInfo(int idx);
+
+ static DXGI_FORMAT GetDXGIFormat(const VideoPicture &picture);
+ static DXGI_FORMAT GetDXGIFormat(CVideoBuffer* videoBuffer);
+ static AVPixelFormat GetAVFormat(DXGI_FORMAT dxgi_format);
+ static DXGI_HDR_METADATA_HDR10 GetDXGIHDR10MetaData(CRenderBuffer* rb);
+
+protected:
+ explicit CRendererBase(CVideoSettings& videoSettings);
+
+ bool CreateIntermediateTarget(unsigned int width, unsigned int height, bool dynamic = false);
+ void OnCMSConfigChanged(AVColorPrimaries srcPrimaries);
+ void ReorderDrawPoints(const CRect& destRect, CPoint(&rotatedPoints)[4]) const;
+ bool CreateRenderBuffer(int index);
+ void DeleteRenderBuffer(int index);
+
+ void ProcessHDR(CRenderBuffer* rb);
+
+ virtual void RenderImpl(CD3DTexture& target, CRect& sourceRect, CPoint (&destPoints)[4], uint32_t flags) = 0;
+ virtual void FinalOutput(CD3DTexture& source, CD3DTexture& target, const CRect& sourceRect, const CPoint(&destPoints)[4]);
+
+ virtual CRenderBuffer* CreateBuffer() = 0;
+ virtual void UpdateVideoFilters();
+ virtual void CheckVideoParameters();
+ virtual void OnViewSizeChanged() {}
+ virtual void OnOutputReset() {}
+
+ bool m_toneMapping = false;
+ bool m_useDithering = false;
+ bool m_cmsOn = false;
+ bool m_lutIsLoading = false;
+ bool m_useHLGtoPQ = false;
+ ETONEMAPMETHOD m_toneMapMethod = VS_TONEMAPMETHOD_OFF;
+
+ int m_iBufferIndex = 0;
+ int m_iNumBuffers = 0;
+ int m_iBuffersRequired = 0;
+ int m_ditherDepth = 0;
+ int m_cmsToken = -1;
+ int m_lutSize = 0;
+ unsigned m_sourceWidth = 0;
+ unsigned m_sourceHeight = 0;
+ unsigned m_viewWidth = 0;
+ unsigned m_viewHeight = 0;
+ unsigned m_renderOrientation = 0;
+ float m_fps = 0.0f;
+ uint64_t m_frameIdx = 0;
+
+ AVPixelFormat m_format = AV_PIX_FMT_NONE;
+ CD3DTexture m_IntermediateTarget;
+ std::shared_ptr<COutputShader> m_outputShader;
+ std::unique_ptr<CColorManager> m_colorManager;
+ Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_pLUTView;
+ CVideoSettings& m_videoSettings;
+ std::map<int, CRenderBuffer*> m_renderBuffers;
+
+ DXGI_HDR_METADATA_HDR10 m_lastHdr10 = {};
+ HDR_TYPE m_HdrType = HDR_TYPE::HDR_NONE_SDR;
+ bool m_AutoSwitchHDR = false;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererDXVA.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererDXVA.cpp
new file mode 100644
index 0000000..a6d2c1b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererDXVA.cpp
@@ -0,0 +1,383 @@
+/*
+ * 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.
+ */
+
+#include "RendererDXVA.h"
+
+#include "DVDCodecs/Video/DVDVideoCodec.h"
+#include "VideoRenderers/BaseRenderer.h"
+#include "WIN32Util.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/log.h"
+#include "utils/memcpy_sse2.h"
+#include "windowing/GraphicContext.h"
+
+#include <ppl.h>
+
+using namespace Microsoft::WRL;
+
+CRendererBase* CRendererDXVA::Create(CVideoSettings& videoSettings)
+{
+ return new CRendererDXVA(videoSettings);
+}
+
+void CRendererDXVA::GetWeight(std::map<RenderMethod, int>& weights, const VideoPicture& picture)
+{
+ unsigned weight = 0;
+ const AVPixelFormat av_pixel_format = picture.videoBuffer->GetFormat();
+
+ if (av_pixel_format == AV_PIX_FMT_D3D11VA_VLD)
+ weight += 1000;
+ else
+ {
+ // check format for buffer
+ const DXGI_FORMAT dxgi_format = CRenderBufferImpl::GetDXGIFormat(av_pixel_format, GetDXGIFormat(picture));
+ if (dxgi_format == DXGI_FORMAT_UNKNOWN)
+ return;
+
+ CD3D11_TEXTURE2D_DESC texDesc(
+ dxgi_format,
+ FFALIGN(picture.iWidth, 32),
+ FFALIGN(picture.iHeight, 32),
+ 1, 1,
+ D3D11_BIND_DECODER,
+ D3D11_USAGE_DYNAMIC,
+ D3D11_CPU_ACCESS_WRITE
+ );
+
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ if (FAILED(pDevice->CreateTexture2D(&texDesc, nullptr, nullptr)))
+ {
+ CLog::LogF(LOGWARNING, "Texture format {} is not supported.", dxgi_format);
+ return;
+ }
+
+ if (av_pixel_format == AV_PIX_FMT_NV12 ||
+ av_pixel_format == AV_PIX_FMT_P010 ||
+ av_pixel_format == AV_PIX_FMT_P016)
+ weight += 500; // single copying
+
+ else if (av_pixel_format == AV_PIX_FMT_YUV420P ||
+ av_pixel_format == AV_PIX_FMT_YUV420P10 ||
+ av_pixel_format == AV_PIX_FMT_YUV420P16)
+ weight += 400; // single copying + convert
+ }
+
+ // prefer DXVA method for interlaced HW decoded material
+ if (av_pixel_format == AV_PIX_FMT_D3D11VA_VLD &&
+ picture.iFlags & DVP_FLAG_INTERLACED)
+ weight += 1000;
+
+ if (weight > 0)
+ weights[RENDER_DXVA] = weight;
+}
+
+CRenderInfo CRendererDXVA::GetRenderInfo()
+{
+ auto info = __super::GetRenderInfo();
+
+ const int buffers = NUM_BUFFERS + m_processor->PastRefs();
+ info.optimal_buffer_size = std::min(NUM_BUFFERS, buffers);
+ info.m_deintMethods.push_back(VS_INTERLACEMETHOD_DXVA_AUTO);
+
+ return info;
+}
+
+bool CRendererDXVA::Configure(const VideoPicture& picture, float fps, unsigned orientation)
+{
+ const auto support_type = D3D11_VIDEO_PROCESSOR_FORMAT_SUPPORT_INPUT;
+
+ if (__super::Configure(picture, fps, orientation))
+ {
+ m_format = picture.videoBuffer->GetFormat();
+ const DXGI_FORMAT dxgi_format = CRenderBufferImpl::GetDXGIFormat(m_format, GetDXGIFormat(picture));
+
+ // create processor
+ m_processor = std::make_unique<DXVA::CProcessorHD>();
+ if (m_processor->PreInit() && m_processor->Open(m_sourceWidth, m_sourceHeight) &&
+ m_processor->IsFormatSupported(dxgi_format, support_type))
+ {
+ return true;
+ }
+
+ CLog::LogF(LOGERROR, "unable to create DXVA processor");
+ m_processor.reset();
+ }
+ return false;
+}
+
+bool CRendererDXVA::NeedBuffer(int idx)
+{
+ if (m_renderBuffers[idx]->IsLoaded() && m_renderBuffers[idx]->pictureFlags & DVP_FLAG_INTERLACED)
+ {
+ if (m_renderBuffers[idx]->frameIdx + (m_processor->PastRefs() * 2u) >=
+ m_renderBuffers[m_iBufferIndex]->frameIdx)
+ return true;
+ }
+
+ return false;
+}
+
+void CRendererDXVA::CheckVideoParameters()
+{
+ __super::CheckVideoParameters();
+
+ CreateIntermediateTarget(
+ HasHQScaler() ? m_sourceWidth : m_viewWidth,
+ HasHQScaler() ? m_sourceHeight : m_viewHeight);
+}
+
+void CRendererDXVA::RenderImpl(CD3DTexture& target, CRect& sourceRect, CPoint(&destPoints)[4], uint32_t flags)
+{
+ CRect src = sourceRect;
+ CRect dst = HasHQScaler() ? sourceRect : ApplyTransforms(CRect(destPoints[0], destPoints[2]));
+ const CRect trg(0.0f, 0.0f, static_cast<float>(target.GetWidth()), static_cast<float>(target.GetHeight()));
+
+ CWIN32Util::CropSource(src, dst, trg, m_renderOrientation);
+
+ CRenderBuffer* buf = m_renderBuffers[m_iBufferIndex];
+ CRenderBuffer* views[8] = {};
+ FillBuffersSet(views);
+
+ m_processor->Render(src, dst, target.Get(), views,
+ flags, buf->frameIdx % UINT32_MAX, m_renderOrientation,
+ m_videoSettings.m_Contrast,
+ m_videoSettings.m_Brightness);
+
+ if (!HasHQScaler())
+ {
+ // change src and dest in case of dxva scale
+ dst.GetQuad(destPoints);
+ sourceRect = dst;
+ }
+}
+
+CRect CRendererDXVA::ApplyTransforms(const CRect& destRect) const
+{
+ CRect result;
+ CPoint rotated[4];
+ ReorderDrawPoints(destRect, rotated);
+
+ switch (m_renderOrientation)
+ {
+ case 90:
+ result = { rotated[3], rotated[1] };
+ break;
+ case 180:
+ result = destRect;
+ break;
+ case 270:
+ result = { rotated[1], rotated[3] };
+ break;
+ default:
+ result = CServiceBroker::GetWinSystem()->GetGfxContext().StereoCorrection(destRect);
+ break;
+ }
+
+ return result;
+}
+
+void CRendererDXVA::FillBuffersSet(CRenderBuffer* (&buffers)[8])
+{
+ int past = 0;
+ int future = 0;
+
+ CRenderBuffer* buf = m_renderBuffers[m_iBufferIndex];
+ buffers[2] = buf;
+
+ // set future frames
+ while (future < 2)
+ {
+ bool found = false;
+ for (int i = 0; i < m_iNumBuffers; i++)
+ {
+ if (m_renderBuffers[i]->frameIdx == buf->frameIdx + (future * 2 + 2))
+ {
+ // a future frame may not be loaded yet
+ if (m_renderBuffers[i]->IsLoaded() || m_renderBuffers[i]->UploadBuffer())
+ {
+ buffers[1 - future++] = m_renderBuffers[i];
+ found = true;
+ break;
+ }
+ }
+ }
+ if (!found)
+ break;
+ }
+
+ // set past frames
+ while (past < 4)
+ {
+ bool found = false;
+ for (int i = 0; i < m_iNumBuffers; i++)
+ {
+ if (m_renderBuffers[i]->frameIdx == buf->frameIdx - (past * 2 + 2))
+ {
+ if (m_renderBuffers[i]->IsLoaded())
+ {
+ buffers[3 + past++] = m_renderBuffers[i];
+ found = true;
+ break;
+ }
+ }
+ }
+ if (!found)
+ break;
+ }
+}
+
+bool CRendererDXVA::Supports(ESCALINGMETHOD method) const
+{
+ if (method == VS_SCALINGMETHOD_DXVA_HARDWARE)
+ return true;
+
+ return __super::Supports(method);
+}
+
+CRenderBuffer* CRendererDXVA::CreateBuffer()
+{
+ return new CRenderBufferImpl(m_format, m_sourceWidth, m_sourceHeight);
+}
+
+CRendererDXVA::CRenderBufferImpl::CRenderBufferImpl(AVPixelFormat av_pix_format, unsigned width, unsigned height)
+ : CRenderBuffer(av_pix_format, width, height)
+{
+ const auto dxgi_format = GetDXGIFormat(av_pix_format);
+ if (dxgi_format == DXGI_FORMAT_UNKNOWN)
+ return;
+
+ m_widthTex = FFALIGN(width, 32);
+ m_heightTex = FFALIGN(height, 32);
+
+ m_texture.Create(m_widthTex, m_heightTex, 1, D3D11_USAGE_DYNAMIC, dxgi_format);
+}
+
+CRendererDXVA::CRenderBufferImpl::~CRenderBufferImpl()
+{
+ CRenderBufferImpl::ReleasePicture();
+}
+
+bool CRendererDXVA::CRenderBufferImpl::UploadBuffer()
+{
+ if (!videoBuffer)
+ return false;
+
+ if (videoBuffer->GetFormat() == AV_PIX_FMT_D3D11VA_VLD)
+ {
+ m_bLoaded = true;
+ return true;
+ }
+
+ return UploadToTexture();
+}
+
+HRESULT CRendererDXVA::CRenderBufferImpl::GetResource(ID3D11Resource** ppResource, unsigned* index) const
+{
+ if (!ppResource)
+ return E_POINTER;
+ if (!index)
+ return E_POINTER;
+
+ if (videoBuffer->GetFormat() == AV_PIX_FMT_D3D11VA_VLD)
+ return __super::GetResource(ppResource, index);
+
+ ComPtr<ID3D11Resource> pResource = m_texture.Get();
+ *ppResource = pResource.Detach();
+ *index = 0;
+
+ return S_OK;
+}
+
+DXGI_FORMAT CRendererDXVA::CRenderBufferImpl::GetDXGIFormat(AVPixelFormat format, DXGI_FORMAT default_fmt)
+{
+ switch (format)
+ {
+ case AV_PIX_FMT_NV12:
+ case AV_PIX_FMT_YUV420P:
+ return DXGI_FORMAT_NV12;
+ case AV_PIX_FMT_P010:
+ case AV_PIX_FMT_YUV420P10:
+ return DXGI_FORMAT_P010;
+ case AV_PIX_FMT_P016:
+ case AV_PIX_FMT_YUV420P16:
+ return DXGI_FORMAT_P016;
+ default:
+ return default_fmt;
+ }
+}
+
+bool CRendererDXVA::CRenderBufferImpl::UploadToTexture()
+{
+ D3D11_MAPPED_SUBRESOURCE rect;
+ if (!m_texture.LockRect(0, &rect, D3D11_MAP_WRITE_DISCARD))
+ return false;
+
+ // destination
+ uint8_t* pData = static_cast<uint8_t*>(rect.pData);
+ uint8_t* dst[] = { pData, pData + m_texture.GetHeight() * rect.RowPitch };
+ int dstStride[] = { static_cast<int>(rect.RowPitch), static_cast<int>(rect.RowPitch) };
+
+ // source
+ uint8_t* src[3];
+ int srcStrides[3];
+ videoBuffer->GetPlanes(src);
+ videoBuffer->GetStrides(srcStrides);
+
+ const unsigned width = m_width;
+ const unsigned height = m_height;
+
+ const AVPixelFormat buffer_format = videoBuffer->GetFormat();
+ // copy to texture
+ if (buffer_format == AV_PIX_FMT_NV12 ||
+ buffer_format == AV_PIX_FMT_P010 ||
+ buffer_format == AV_PIX_FMT_P016)
+ {
+ Concurrency::parallel_invoke([&]() {
+ // copy Y
+ copy_plane(src[0], srcStrides[0], height, width, dst[0], dstStride[0]);
+ }, [&]() {
+ // copy UV
+ copy_plane(src[1], srcStrides[1], height >> 1, width, dst[1], dstStride[1]);
+ });
+ // copy cache size of UV line again to fix Intel cache issue
+ copy_plane(src[1], srcStrides[1], 1, 32, dst[1], dstStride[1]);
+ }
+ // convert 8bit
+ else if (buffer_format == AV_PIX_FMT_YUV420P)
+ {
+ Concurrency::parallel_invoke([&]() {
+ // copy Y
+ copy_plane(src[0], srcStrides[0], height, width, dst[0], dstStride[0]);
+ }, [&]() {
+ // convert U+V -> UV
+ convert_yuv420_nv12_chrome(&src[1], &srcStrides[1], height, width, dst[1], dstStride[1]);
+ });
+ // copy cache size of UV line again to fix Intel cache issue
+ // height and width multiplied by two because they will be divided by func
+ convert_yuv420_nv12_chrome(&src[1], &srcStrides[1], 2, 64, dst[1], dstStride[1]);
+ }
+ // convert 10/16bit
+ else if (buffer_format == AV_PIX_FMT_YUV420P10 ||
+ buffer_format == AV_PIX_FMT_YUV420P16)
+ {
+ const uint8_t bpp = buffer_format == AV_PIX_FMT_YUV420P10 ? 10 : 16;
+ Concurrency::parallel_invoke([&]() {
+ // copy Y
+ copy_plane(src[0], srcStrides[0], height, width, dst[0], dstStride[0], bpp);
+ }, [&]() {
+ // convert U+V -> UV
+ convert_yuv420_p01x_chrome(&src[1], &srcStrides[1], height, width, dst[1], dstStride[1], bpp);
+ });
+ // copy cache size of UV line again to fix Intel cache issue
+ // height multiplied by two because it will be divided by func
+ convert_yuv420_p01x_chrome(&src[1], &srcStrides[1], 2, 32, dst[1], dstStride[1], bpp);
+ }
+
+ m_bLoaded = m_texture.UnlockRect(0);
+ return m_bLoaded;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererDXVA.h b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererDXVA.h
new file mode 100644
index 0000000..9412377
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererDXVA.h
@@ -0,0 +1,64 @@
+/*
+ * 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 "RendererHQ.h"
+#include "VideoRenderers/HwDecRender/DXVAHD.h"
+
+#include <map>
+
+#include <d3d11_4.h>
+#include <libavutil/pixfmt.h>
+
+enum RenderMethod;
+
+class CRendererDXVA : public CRendererHQ
+{
+ class CRenderBufferImpl;
+public:
+ ~CRendererDXVA() = default;
+
+ CRenderInfo GetRenderInfo() override;
+ bool Supports(ESCALINGMETHOD method) const override;
+ bool WantsDoublePass() override { return true; }
+ bool Configure(const VideoPicture& picture, float fps, unsigned orientation) override;
+ bool NeedBuffer(int idx) override;
+
+ static CRendererBase* Create(CVideoSettings& videoSettings);
+ static void GetWeight(std::map<RenderMethod, int>& weights, const VideoPicture& picture);
+
+protected:
+ explicit CRendererDXVA(CVideoSettings& videoSettings) : CRendererHQ(videoSettings) {}
+
+ void CheckVideoParameters() override;
+ void RenderImpl(CD3DTexture& target, CRect& sourceRect, CPoint(&destPoints)[4], uint32_t flags) override;
+ CRenderBuffer* CreateBuffer() override;
+
+private:
+ void FillBuffersSet(CRenderBuffer* (&buffers)[8]);
+ CRect ApplyTransforms(const CRect& destRect) const;
+
+ std::unique_ptr<DXVA::CProcessorHD> m_processor;
+};
+
+class CRendererDXVA::CRenderBufferImpl : public CRenderBuffer
+{
+public:
+ explicit CRenderBufferImpl(AVPixelFormat av_pix_format, unsigned width, unsigned height);
+ ~CRenderBufferImpl();
+
+ bool UploadBuffer() override;
+ HRESULT GetResource(ID3D11Resource** ppResource, unsigned* index) const override;
+
+ static DXGI_FORMAT GetDXGIFormat(AVPixelFormat format, DXGI_FORMAT default_fmt = DXGI_FORMAT_UNKNOWN);
+
+private:
+ bool UploadToTexture();
+
+ CD3DTexture m_texture;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererHQ.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererHQ.cpp
new file mode 100644
index 0000000..2fd83ff
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererHQ.cpp
@@ -0,0 +1,160 @@
+/*
+ * 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.
+ */
+
+#include "RendererHQ.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/LocalizeStrings.h"
+#include "rendering/dx/DeviceResources.h"
+#include "rendering/dx/RenderContext.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+
+bool CRendererHQ::Supports(ESCALINGMETHOD method) const
+{
+ if (method == VS_SCALINGMETHOD_AUTO)
+ return true;
+
+ if (DX::DeviceResources::Get()->GetDeviceFeatureLevel() >= D3D_FEATURE_LEVEL_9_3 && !m_renderOrientation)
+ {
+ if (method == VS_SCALINGMETHOD_CUBIC_MITCHELL ||
+ method == VS_SCALINGMETHOD_LANCZOS2 ||
+ method == VS_SCALINGMETHOD_SPLINE36_FAST ||
+ method == VS_SCALINGMETHOD_LANCZOS3_FAST ||
+ method == VS_SCALINGMETHOD_SPLINE36 ||
+ method == VS_SCALINGMETHOD_LANCZOS3)
+ {
+ // if scaling is below level, avoid hq scaling
+ const float scaleX = fabs((static_cast<float>(m_sourceWidth) - m_viewWidth) / m_sourceWidth) * 100;
+ const float scaleY = fabs((static_cast<float>(m_sourceHeight) - m_viewHeight) / m_sourceHeight) * 100;
+ const int minScale = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_HQSCALERS);
+
+ return scaleX >= minScale || scaleY >= minScale;
+ }
+ }
+ return false;
+}
+
+void CRendererHQ::OnOutputReset()
+{
+ // re-create shader if output shader changed
+ m_scalerShader.reset();
+}
+
+void CRendererHQ::SelectPSVideoFilter()
+{
+ switch (m_scalingMethod)
+ {
+ case VS_SCALINGMETHOD_CUBIC_MITCHELL:
+ case VS_SCALINGMETHOD_LANCZOS2:
+ case VS_SCALINGMETHOD_SPLINE36_FAST:
+ case VS_SCALINGMETHOD_LANCZOS3_FAST:
+ case VS_SCALINGMETHOD_SPLINE36:
+ case VS_SCALINGMETHOD_LANCZOS3:
+ m_bUseHQScaler = true;
+ break;
+ default:
+ m_bUseHQScaler = false;
+ break;
+ }
+
+ if (m_scalingMethod == VS_SCALINGMETHOD_AUTO)
+ {
+ const bool scaleSD = m_sourceHeight < 720 && m_sourceWidth < 1280;
+ const bool scaleUp = m_sourceHeight < m_viewHeight && m_sourceWidth < m_viewWidth;
+ const bool scaleFps = m_fps < CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAutoScaleMaxFps + 0.01f;
+
+ if (scaleSD && scaleUp && scaleFps && Supports(VS_SCALINGMETHOD_LANCZOS3_FAST))
+ {
+ m_scalingMethod = VS_SCALINGMETHOD_LANCZOS3_FAST;
+ m_bUseHQScaler = true;
+ }
+ }
+ if (m_renderOrientation)
+ m_bUseHQScaler = false;
+}
+
+bool CRendererHQ::HasHQScaler() const
+{
+ return m_bUseHQScaler && m_scalerShader;
+}
+
+void CRendererHQ::CheckVideoParameters()
+{
+ __super::CheckVideoParameters();
+
+ if (m_scalingMethodGui != m_videoSettings.m_ScalingMethod)
+ {
+ m_scalingMethodGui = m_videoSettings.m_ScalingMethod;
+ m_scalingMethod = m_scalingMethodGui;
+
+ if (!Supports(m_scalingMethod))
+ {
+ CLog::LogF(LOGWARNING, "chosen scaling method {} is not supported by renderer",
+ static_cast<int>(m_scalingMethod));
+ m_scalingMethod = VS_SCALINGMETHOD_AUTO;
+ }
+
+ SelectPSVideoFilter();
+ m_scalerShader.reset();
+ }
+}
+
+void CRendererHQ::UpdateVideoFilters()
+{
+ __super::UpdateVideoFilters();
+
+ if (m_bUseHQScaler && !m_scalerShader)
+ {
+ // firstly try the more efficient two pass convolution shader
+ m_scalerShader = std::make_unique<CConvolutionShaderSeparable>();
+
+ if (!m_scalerShader->Create(m_scalingMethod, m_outputShader))
+ {
+ m_scalerShader.reset();
+ CLog::LogF(LOGINFO, "two pass convolution shader init problem, falling back to one pass.");
+ }
+
+ // fallback on the one pass version
+ if (!m_scalerShader)
+ {
+ m_scalerShader = std::make_unique<CConvolutionShader1Pass>();
+
+ if (!m_scalerShader->Create(m_scalingMethod, m_outputShader))
+ {
+ // we are in a big trouble
+ m_scalerShader.reset();
+ m_bUseHQScaler = false;
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(34400), g_localizeStrings.Get(34401));
+ }
+ }
+ }
+}
+
+void CRendererHQ::FinalOutput(CD3DTexture& source, CD3DTexture& target, const CRect& sourceRect, const CPoint(&destPoints)[4])
+{
+ if (HasHQScaler())
+ {
+ const CRect destRect = CServiceBroker::GetWinSystem()->GetGfxContext().StereoCorrection(CRect(destPoints[0], destPoints[2]));
+ m_scalerShader->Render(source, target, sourceRect, destRect, false);
+ }
+ else
+ {
+ CD3D11_VIEWPORT viewPort(0.f, 0.f, static_cast<float>(target.GetWidth()), static_cast<float>(target.GetHeight()));
+ // restore view port
+ DX::DeviceResources::Get()->GetD3DContext()->RSSetViewports(1, &viewPort);
+ // restore scissors
+ auto& context = CServiceBroker::GetWinSystem()->GetGfxContext();
+ DX::Windowing()->SetScissors(context.StereoCorrection(context.GetScissors()));
+ // render frame
+ __super::FinalOutput(source, target, sourceRect, destPoints);
+ }
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererHQ.h b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererHQ.h
new file mode 100644
index 0000000..0f4270b
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererHQ.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "RendererBase.h"
+
+class CRendererHQ : public CRendererBase
+{
+public:
+ bool Supports(ESCALINGMETHOD method) const override;
+
+protected:
+ explicit CRendererHQ(CVideoSettings& videoSettings) : CRendererBase(videoSettings) {}
+ virtual ~CRendererHQ() = default;
+
+ void OnOutputReset() override;
+ void CheckVideoParameters() override;
+ void UpdateVideoFilters() override;
+ void FinalOutput(CD3DTexture& source, CD3DTexture& target, const CRect& sourceRect, const CPoint(&destPoints)[4]) override;
+
+ void SelectPSVideoFilter();
+ bool HasHQScaler() const;
+
+ ESCALINGMETHOD m_scalingMethod = VS_SCALINGMETHOD_AUTO;
+ ESCALINGMETHOD m_scalingMethodGui = VS_SCALINGMETHOD_AUTO;
+ std::unique_ptr<CConvolutionShader> m_scalerShader = nullptr;
+ bool m_bUseHQScaler = false;
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererShaders.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererShaders.cpp
new file mode 100644
index 0000000..32bce25
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererShaders.cpp
@@ -0,0 +1,474 @@
+/*
+ * 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.
+ */
+
+#include "RendererShaders.h"
+
+#include "DVDCodecs/Video/DXVA.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/CPUInfo.h"
+#ifndef _M_ARM
+ #include "utils/gpu_memcpy_sse4.h"
+#endif
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <ppl.h>
+
+using namespace Microsoft::WRL;
+static DXGI_FORMAT plane_formats[][2] =
+{
+ { DXGI_FORMAT_R8_UNORM, DXGI_FORMAT_R8G8_UNORM }, // NV12
+ { DXGI_FORMAT_R16_UNORM, DXGI_FORMAT_R16G16_UNORM }, // P010
+ { DXGI_FORMAT_R16_UNORM, DXGI_FORMAT_R16G16_UNORM } // P016
+};
+
+CRendererBase* CRendererShaders::Create(CVideoSettings& videoSettings)
+{
+ return new CRendererShaders(videoSettings);
+}
+
+void CRendererShaders::GetWeight(std::map<RenderMethod, int>& weights, const VideoPicture& picture)
+{
+ unsigned weight = 0;
+ const AVPixelFormat av_pixel_format = picture.videoBuffer->GetFormat();
+
+ switch (av_pixel_format)
+ {
+ case AV_PIX_FMT_D3D11VA_VLD:
+ if (IsHWPicSupported(picture))
+ weight += 1000; // support natively
+ else
+ weight += 200; // double copying (GPU->CPU->GPU)
+ break;
+ case AV_PIX_FMT_YUV420P:
+ case AV_PIX_FMT_NV12:
+ weight += 500; // single copying
+ break;
+ case AV_PIX_FMT_YUV420P10:
+ case AV_PIX_FMT_YUV420P16:
+ if (DX::Windowing()->IsFormatSupport(DXGI_FORMAT_R16_UNORM, D3D11_FORMAT_SUPPORT_TEXTURE2D))
+ weight += 500; // single copying
+ else
+ CLog::LogF(LOGWARNING, "Texture format DXGI_FORMAT_R16_UNORM is not supported.");
+ break;
+ case AV_PIX_FMT_P010:
+ case AV_PIX_FMT_P016:
+ if (DX::Windowing()->IsFormatSupport(DXGI_FORMAT_R16_UNORM, D3D11_FORMAT_SUPPORT_TEXTURE2D) &&
+ DX::Windowing()->IsFormatSupport(DXGI_FORMAT_R16G16_UNORM,
+ D3D11_FORMAT_SUPPORT_TEXTURE2D))
+ weight += 500; // single copying
+ else
+ CLog::LogF(LOGWARNING, "Texture format R16_UNORM / R16G16_UNORM is not supported.");
+ break;
+ }
+
+ if (weight > 0)
+ weights[RENDER_PS] = weight;
+}
+
+bool CRendererShaders::Supports(ESCALINGMETHOD method) const
+{
+ if (method == VS_SCALINGMETHOD_LINEAR)
+ return true;
+
+ return __super::Supports(method);
+}
+
+bool CRendererShaders::Configure(const VideoPicture& picture, float fps, unsigned orientation)
+{
+ if (__super::Configure(picture, fps, orientation))
+ {
+ m_format = picture.videoBuffer->GetFormat();
+ if (m_format == AV_PIX_FMT_D3D11VA_VLD)
+ {
+ const DXGI_FORMAT dxgi_format = GetDXGIFormat(picture);
+
+ // if decoded texture isn't supported in shaders
+ // then change format to supported via copying
+ if (!IsHWPicSupported(picture))
+ m_format = GetAVFormat(dxgi_format);
+ }
+
+ CreateIntermediateTarget(m_sourceWidth, m_sourceHeight);
+ return true;
+ }
+ return false;
+}
+
+void CRendererShaders::RenderImpl(CD3DTexture& target, CRect& sourceRect, CPoint(&destPoints)[4], uint32_t flags)
+{
+ if (!m_colorShader)
+ return;
+
+ // reset scissors and viewport
+ CD3D11_VIEWPORT viewPort(0.0f, 0.0f,
+ static_cast<float>(target.GetWidth()),
+ static_cast<float>(target.GetHeight()));
+ DX::DeviceResources::Get()->GetD3DContext()->RSSetViewports(1, &viewPort);
+ DX::Windowing()->ResetScissors();
+
+ CRenderBuffer* buf = m_renderBuffers[m_iBufferIndex];
+
+ CPoint srcPoints[4];
+ sourceRect.GetQuad(srcPoints);
+
+ m_colorShader->SetParams(m_videoSettings.m_Contrast, m_videoSettings.m_Brightness,
+ DX::Windowing()->UseLimitedColor());
+ m_colorShader->SetColParams(buf->color_space, buf->bits, !buf->full_range, buf->texBits);
+ m_colorShader->Render(sourceRect, srcPoints, buf, target);
+
+ if (!HasHQScaler())
+ ReorderDrawPoints(CRect(destPoints[0], destPoints[2]), destPoints);
+}
+
+void CRendererShaders::CheckVideoParameters()
+{
+ __super::CheckVideoParameters();
+
+ CRenderBuffer* buf = m_renderBuffers[m_iBufferIndex];
+ const AVColorPrimaries srcPrim = GetSrcPrimaries(buf->primaries, buf->GetWidth(), buf->GetHeight());
+ if (srcPrim != m_srcPrimaries)
+ {
+ // source params is changed, reset shader
+ m_srcPrimaries = srcPrim;
+ m_colorShader.reset();
+ }
+}
+
+void CRendererShaders::UpdateVideoFilters()
+{
+ __super::UpdateVideoFilters();
+
+ if (!m_colorShader)
+ {
+ m_colorShader = std::make_unique<CYUV2RGBShader>();
+
+ AVColorPrimaries dstPrimaries = AVCOL_PRI_BT709;
+
+ if (DX::Windowing()->IsHDROutput() &&
+ (m_srcPrimaries == AVCOL_PRI_BT709 || m_srcPrimaries == AVCOL_PRI_BT2020))
+ dstPrimaries = m_srcPrimaries;
+
+ if (!m_colorShader->Create(m_format, dstPrimaries, m_srcPrimaries))
+ {
+ // we are in a big trouble
+ CLog::LogF(LOGERROR, "unable to create YUV->RGB shader, rendering is not possible");
+ m_colorShader.reset();
+ }
+ }
+}
+
+bool CRendererShaders::IsHWPicSupported(const VideoPicture& picture)
+{
+ // checking support of decoder texture in shaders
+ const DXGI_FORMAT dxgi_format = GetDXGIFormat(picture);
+ if (dxgi_format != DXGI_FORMAT_UNKNOWN)
+ {
+ CD3D11_TEXTURE2D_DESC texDesc(
+ dxgi_format,
+ FFALIGN(picture.iWidth, 32),
+ FFALIGN(picture.iHeight, 32),
+ 1, 1,
+ D3D11_BIND_DECODER | D3D11_BIND_SHADER_RESOURCE,
+ D3D11_USAGE_DEFAULT
+ );
+
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ return SUCCEEDED(pDevice->CreateTexture2D(&texDesc, nullptr, nullptr));
+ }
+ return false;
+}
+
+AVColorPrimaries CRendererShaders::GetSrcPrimaries(AVColorPrimaries srcPrimaries, unsigned width, unsigned height)
+{
+ AVColorPrimaries ret = srcPrimaries;
+ if (ret == AVCOL_PRI_UNSPECIFIED)
+ {
+ if (width > 1024 || height >= 600)
+ ret = AVCOL_PRI_BT709;
+ else
+ ret = AVCOL_PRI_BT470BG;
+ }
+ return ret;
+}
+
+CRenderBuffer* CRendererShaders::CreateBuffer()
+{
+ return new CRenderBufferImpl(m_format, m_sourceWidth, m_sourceHeight);
+}
+
+CRendererShaders::CRenderBufferImpl::CRenderBufferImpl(AVPixelFormat av_pix_format, unsigned width, unsigned height)
+ : CRenderBuffer(av_pix_format, width, height)
+{
+ DXGI_FORMAT view_formats[YuvImage::MAX_PLANES] = {};
+
+ switch (av_format)
+ {
+ case AV_PIX_FMT_D3D11VA_VLD:
+ m_viewCount = 2;
+ break;
+ case AV_PIX_FMT_NV12:
+ {
+ view_formats[0] = DXGI_FORMAT_R8_UNORM;
+ view_formats[1] = DXGI_FORMAT_R8G8_UNORM;
+ // FL 9.x doesn't support DXGI_FORMAT_R8G8_UNORM, so we have to use SNORM and correct values in shader
+ if (!DX::Windowing()->IsFormatSupport(view_formats[1], D3D11_FORMAT_SUPPORT_TEXTURE2D))
+ view_formats[1] = DXGI_FORMAT_R8G8_SNORM;
+ m_viewCount = 2;
+ break;
+ }
+ case AV_PIX_FMT_P010:
+ case AV_PIX_FMT_P016:
+ {
+ view_formats[0] = DXGI_FORMAT_R16_UNORM;
+ view_formats[1] = DXGI_FORMAT_R16G16_UNORM;
+ m_viewCount = 2;
+ break;
+ }
+ case AV_PIX_FMT_YUV420P:
+ {
+ view_formats[0] = view_formats[1] = view_formats[2] = DXGI_FORMAT_R8_UNORM;
+ m_viewCount = 3;
+ break;
+ }
+ case AV_PIX_FMT_YUV420P10:
+ case AV_PIX_FMT_YUV420P16:
+ {
+ view_formats[0] = view_formats[1] = view_formats[2] = DXGI_FORMAT_R16_UNORM;
+ m_viewCount = 3;
+ texBits = av_format == AV_PIX_FMT_YUV420P10 ? 10 : 16;
+ break;
+ }
+ default:
+ // unsupported format
+ return;
+ }
+
+ if (av_format != AV_PIX_FMT_D3D11VA_VLD)
+ {
+ for (size_t i = 0; i < m_viewCount; i++)
+ {
+ const auto w = i ? m_width >> 1 : m_width;
+ const auto h = i ? m_height >> 1 : m_height;
+
+ if (!m_textures[i].Create(w, h, 1, D3D11_USAGE_DYNAMIC, view_formats[i]))
+ break;
+
+ // clear plane
+ D3D11_MAPPED_SUBRESOURCE mapping = {};
+ if (m_textures[i].LockRect(0, &mapping, D3D11_MAP_WRITE_DISCARD))
+ {
+ if (view_formats[i] == DXGI_FORMAT_R8_UNORM ||
+ view_formats[i] == DXGI_FORMAT_R8G8_UNORM ||
+ view_formats[i] == DXGI_FORMAT_R8G8_SNORM)
+ memset(mapping.pData, i ? 0x80 : 0, mapping.RowPitch * h);
+ else
+ wmemset(static_cast<wchar_t*>(mapping.pData), i ? 0x8000 : 0, mapping.RowPitch * h >> 1);
+
+ if (m_textures[i].UnlockRect(0)) {}
+ }
+ }
+ }
+}
+
+CRendererShaders::CRenderBufferImpl::~CRenderBufferImpl()
+{
+ CRenderBufferImpl::ReleasePicture();
+}
+
+void CRendererShaders::CRenderBufferImpl::AppendPicture(const VideoPicture& picture)
+{
+ __super::AppendPicture(picture);
+
+ if (videoBuffer->GetFormat() == AV_PIX_FMT_D3D11VA_VLD)
+ {
+ if (AV_PIX_FMT_D3D11VA_VLD != av_format)
+ QueueCopyFromGPU();
+
+ const auto hw = dynamic_cast<DXVA::CVideoBuffer*>(videoBuffer);
+ m_widthTex = hw->width;
+ m_heightTex = hw->height;
+ }
+}
+
+bool CRendererShaders::CRenderBufferImpl::UploadBuffer()
+{
+ if (!videoBuffer)
+ return false;
+
+ if (videoBuffer->GetFormat() == AV_PIX_FMT_D3D11VA_VLD)
+ {
+ if (AV_PIX_FMT_D3D11VA_VLD == av_format)
+ m_bLoaded = true;
+ else
+ m_bLoaded = UploadFromGPU();
+ }
+ else
+ m_bLoaded = UploadFromBuffer();
+
+ return m_bLoaded;
+}
+
+unsigned CRendererShaders::CRenderBufferImpl::GetViewCount() const
+{
+ return m_viewCount;
+}
+
+ID3D11View* CRendererShaders::CRenderBufferImpl::GetView(unsigned viewIdx)
+{
+ if (videoBuffer->GetFormat() == AV_PIX_FMT_D3D11VA_VLD &&
+ AV_PIX_FMT_D3D11VA_VLD == av_format)
+ {
+ if (m_planes[viewIdx])
+ return m_planes[viewIdx].Get();
+
+ unsigned arrayIdx;
+ ComPtr<ID3D11Resource> pResource;
+ if (FAILED(GetResource(&pResource, &arrayIdx)))
+ {
+ CLog::LogF(LOGERROR, "unable to open d3d11va resource.");
+ return nullptr;
+ }
+
+ const auto dxva_format = CRendererBase::GetDXGIFormat(videoBuffer);
+ // impossible but we check
+ if (dxva_format < DXGI_FORMAT_NV12 || dxva_format > DXGI_FORMAT_P016)
+ return nullptr;
+
+ CD3D11_SHADER_RESOURCE_VIEW_DESC srvDesc(
+ D3D11_SRV_DIMENSION_TEXTURE2DARRAY,
+ plane_formats[dxva_format - DXGI_FORMAT_NV12][viewIdx],
+ 0, 1, arrayIdx, 1
+ );
+
+ ComPtr<ID3D11Device> pD3DDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ if (FAILED(pD3DDevice->CreateShaderResourceView(pResource.Get(), &srvDesc, &m_planes[viewIdx])))
+ {
+ CLog::LogF(LOGERROR, "unable to create shader target for decoder texture.");
+ return nullptr;
+ }
+
+ return m_planes[viewIdx].Get();
+ }
+
+ return m_textures[viewIdx].GetShaderResource();
+}
+
+void CRendererShaders::CRenderBufferImpl::ReleasePicture()
+{
+ __super::ReleasePicture();
+
+ m_planes[0] = nullptr;
+ m_planes[1] = nullptr;
+}
+
+bool CRendererShaders::CRenderBufferImpl::UploadFromGPU()
+{
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetImmediateContext();
+ D3D11_MAPPED_SUBRESOURCE mapGPU;
+ D3D11_MAPPED_SUBRESOURCE mappings[2];
+
+ if (FAILED(pContext->Map(m_staging.Get(), 0, D3D11_MAP_READ, 0, &mapGPU)))
+ return false;
+
+ if (!m_textures[PLANE_Y].LockRect(0, &mappings[PLANE_Y], D3D11_MAP_WRITE_DISCARD) ||
+ !m_textures[PLANE_UV].LockRect(0, &mappings[PLANE_UV], D3D11_MAP_WRITE_DISCARD))
+ {
+ pContext->Unmap(m_staging.Get(), 0);
+ return false;
+ }
+
+ void* (*copy_func)(void* d, const void* s, size_t size) =
+#if defined(HAVE_SSE2)
+ ((CServiceBroker::GetCPUInfo()->GetCPUFeatures() & CPU_FEATURE_SSE4) != 0) ? gpu_memcpy :
+#endif
+ memcpy;
+
+ auto* s_y = static_cast<uint8_t*>(mapGPU.pData);
+ auto* s_uv = static_cast<uint8_t*>(mapGPU.pData) + m_sDesc.Height * mapGPU.RowPitch;
+ auto* d_y = static_cast<uint8_t*>(mappings[PLANE_Y].pData);
+ auto* d_uv = static_cast<uint8_t*>(mappings[PLANE_UV].pData);
+
+ if (mappings[PLANE_Y].RowPitch == mapGPU.RowPitch
+ && mappings[PLANE_UV].RowPitch == mapGPU.RowPitch)
+ {
+ Concurrency::parallel_invoke([&]() {
+ // copy Y
+ copy_func(d_y, s_y, mapGPU.RowPitch * m_height);
+ }, [&]() {
+ // copy UV
+ copy_func(d_uv, s_uv, mapGPU.RowPitch * m_height >> 1);
+ });
+ }
+ else
+ {
+ Concurrency::parallel_invoke([&]() {
+ // copy Y
+ for (unsigned y = 0; y < m_height; ++y)
+ {
+ copy_func(d_y, s_y, mappings[PLANE_Y].RowPitch);
+ s_y += mapGPU.RowPitch;
+ d_y += mappings[PLANE_Y].RowPitch;
+ }
+ }, [&]() {
+ // copy UV
+ for (unsigned y = 0; y < m_height >> 1; ++y)
+ {
+ copy_func(d_uv, s_uv, mappings[PLANE_UV].RowPitch);
+ s_uv += mapGPU.RowPitch;
+ d_uv += mappings[PLANE_UV].RowPitch;
+ }
+ });
+ }
+ pContext->Unmap(m_staging.Get(), 0);
+
+ return m_textures[PLANE_Y].UnlockRect(0) &&
+ m_textures[PLANE_UV].UnlockRect(0);
+}
+
+bool CRendererShaders::CRenderBufferImpl::UploadFromBuffer() const
+{
+ uint8_t* bufData[3];
+ int srcLines[3];
+ videoBuffer->GetPlanes(bufData);
+ videoBuffer->GetStrides(srcLines);
+
+ for (unsigned plane = 0; plane < m_viewCount; ++plane)
+ {
+ D3D11_MAPPED_SUBRESOURCE mapping = {};
+ if (!m_textures[plane].LockRect(0, &mapping, D3D11_MAP_WRITE_DISCARD))
+ break;
+
+ auto* dst = static_cast<uint8_t*>(mapping.pData);
+ auto* src = bufData[plane];
+ int srcLine = srcLines[plane];
+ int dstLine = mapping.RowPitch;
+ int height = plane ? m_height >> 1 : m_height;
+
+ if (srcLine == dstLine)
+ {
+ memcpy(dst, src, srcLine * height);
+ }
+ else
+ {
+ uint8_t* s = src;
+ uint8_t* d = dst;
+ for (int i = 0; i < height; ++i)
+ {
+ memcpy(d, s, std::min(srcLine, dstLine));
+ d += dstLine;
+ s += srcLine;
+ }
+ }
+ }
+
+ for (unsigned plane = 0; plane < m_viewCount; ++plane)
+ if (!m_textures[plane].UnlockRect(0)) {}
+
+ return true;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererShaders.h b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererShaders.h
new file mode 100644
index 0000000..945cadd
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererShaders.h
@@ -0,0 +1,71 @@
+/*
+ * 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 "RendererHQ.h"
+#include "cores/VideoPlayer/Buffers/VideoBuffer.h"
+
+#include <map>
+
+#include <d3d11_4.h>
+#include <libavutil/pixfmt.h>
+
+#define PLANE_Y 0
+#define PLANE_U 1
+#define PLANE_V 2
+#define PLANE_UV 1
+
+enum RenderMethod;
+
+class CRendererShaders : public CRendererHQ
+{
+ class CRenderBufferImpl;
+public:
+ ~CRendererShaders() = default;
+
+ bool Supports(ESCALINGMETHOD method) const override;
+ bool Configure(const VideoPicture& picture, float fps, unsigned orientation) override;
+
+ static CRendererBase* Create(CVideoSettings& videoSettings);
+ static void GetWeight(std::map<RenderMethod, int>& weights, const VideoPicture& picture);
+
+protected:
+ explicit CRendererShaders(CVideoSettings& videoSettings) : CRendererHQ(videoSettings) {}
+ void RenderImpl(CD3DTexture& target, CRect& sourceRect, CPoint(&destPoints)[4], uint32_t flags) override;
+ void CheckVideoParameters() override;
+ void UpdateVideoFilters() override;
+ CRenderBuffer* CreateBuffer() override;
+ static bool IsHWPicSupported(const VideoPicture& picture);
+
+private:
+ static AVColorPrimaries GetSrcPrimaries(AVColorPrimaries srcPrimaries, unsigned int width, unsigned int height);
+
+ AVColorPrimaries m_srcPrimaries = AVCOL_PRI_BT709;
+ std::unique_ptr<CYUV2RGBShader> m_colorShader;
+};
+
+class CRendererShaders::CRenderBufferImpl : public CRenderBuffer
+{
+public:
+ explicit CRenderBufferImpl(AVPixelFormat av_pix_format, unsigned width, unsigned height);
+ ~CRenderBufferImpl();
+
+ void AppendPicture(const VideoPicture& picture) override;
+ bool UploadBuffer() override;
+ unsigned GetViewCount() const override;
+ ID3D11View* GetView(unsigned viewIdx) override;
+ void ReleasePicture() override;
+
+private:
+ bool UploadFromGPU();
+ bool UploadFromBuffer() const;
+
+ unsigned m_viewCount = 0;
+ CD3DTexture m_textures[YuvImage::MAX_PLANES];
+ Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_planes[2];
+};
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererSoftware.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererSoftware.cpp
new file mode 100644
index 0000000..26aeb10
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererSoftware.cpp
@@ -0,0 +1,204 @@
+/*
+ * 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.
+ */
+
+#include "RendererSoftware.h"
+
+#include "DVDCodecs/Video/DVDVideoCodec.h"
+#include "rendering/dx/DeviceResources.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/log.h"
+
+using namespace Microsoft::WRL;
+
+CRendererBase* CRendererSoftware::Create(CVideoSettings& videoSettings)
+{
+ return new CRendererSoftware(videoSettings);
+}
+
+void CRendererSoftware::GetWeight(std::map<RenderMethod, int>& weights, const VideoPicture& picture)
+{
+ unsigned weight = 0;
+ const AVPixelFormat av_pixel_format = picture.videoBuffer->GetFormat();
+
+ if (av_pixel_format == AV_PIX_FMT_D3D11VA_VLD)
+ {
+ const AVPixelFormat sw_format = GetAVFormat(GetDXGIFormat(picture));
+ if (sws_isSupportedInput(sw_format))
+ // double copying (GPU->CPU->(converting)->GPU)
+ weight += 100;
+ }
+ else if (sws_isSupportedInput(av_pixel_format))
+ weight += 200;
+
+ if (weight > 0)
+ weights[RENDER_SW] = weight;
+}
+
+CRendererSoftware::~CRendererSoftware()
+{
+ if (m_sw_scale_ctx)
+ {
+ sws_freeContext(m_sw_scale_ctx);
+ m_sw_scale_ctx = nullptr;
+ }
+}
+
+bool CRendererSoftware::Configure(const VideoPicture& picture, float fps, unsigned orientation)
+{
+ if (__super::Configure(picture, fps, orientation))
+ {
+ if (!CreateIntermediateTarget(m_sourceWidth, m_sourceHeight, true))
+ return false;
+
+ m_format = picture.videoBuffer->GetFormat();
+ if (m_format == AV_PIX_FMT_D3D11VA_VLD)
+ m_format = GetAVFormat(GetDXGIFormat(picture));
+
+ return true;
+ }
+ return false;
+}
+
+bool CRendererSoftware::Supports(ESCALINGMETHOD method) const
+{
+ return method == VS_SCALINGMETHOD_AUTO
+ || method == VS_SCALINGMETHOD_LINEAR;
+}
+
+void CRendererSoftware::RenderImpl(CD3DTexture& target, CRect& sourceRect, CPoint(&destPoints)[4], uint32_t flags)
+{
+ // if creation failed
+ if (!m_outputShader)
+ return;
+
+ CRenderBuffer* buf = m_renderBuffers[m_iBufferIndex];
+
+ // 1. convert yuv to rgb
+ m_sw_scale_ctx = sws_getCachedContext(m_sw_scale_ctx,
+ buf->GetWidth(), buf->GetHeight(), buf->av_format,
+ buf->GetWidth(), buf->GetHeight(), AV_PIX_FMT_BGRA,
+ SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
+
+ if (!m_sw_scale_ctx)
+ return;
+
+ sws_setColorspaceDetails(m_sw_scale_ctx,
+ sws_getCoefficients(buf->color_space), buf->full_range,
+ sws_getCoefficients(AVCOL_SPC_BT709), buf->full_range,
+ 0, 1 << 16, 1 << 16);
+
+ uint8_t* src[YuvImage::MAX_PLANES];
+ int srcStride[YuvImage::MAX_PLANES];
+ buf->GetDataPlanes(src, srcStride);
+
+ D3D11_MAPPED_SUBRESOURCE mapping;
+ if (target.LockRect(0, &mapping, D3D11_MAP_WRITE_DISCARD))
+ {
+ uint8_t *dst[] = { static_cast<uint8_t*>(mapping.pData), nullptr, nullptr };
+ int dstStride[] = { static_cast<int>(mapping.RowPitch), 0, 0 };
+
+ sws_scale(m_sw_scale_ctx, src, srcStride, 0, std::min(target.GetHeight(), buf->GetHeight()), dst, dstStride);
+
+ if (!target.UnlockRect(0))
+ CLog::LogF(LOGERROR, "failed to unlock swtarget texture.");
+ }
+ else
+ CLog::LogF(LOGERROR, "failed to lock swtarget texture into memory.");
+
+ // rotate initial rect
+ ReorderDrawPoints(CRect(destPoints[0], destPoints[2]), destPoints);
+}
+
+void CRendererSoftware::FinalOutput(CD3DTexture& source, CD3DTexture& target, const CRect& src, const CPoint(&destPoints)[4])
+{
+ m_outputShader->Render(source, src, destPoints, target,
+ DX::Windowing()->UseLimitedColor(),
+ m_videoSettings.m_Contrast * 0.01f,
+ m_videoSettings.m_Brightness * 0.01f);
+}
+
+CRenderBuffer* CRendererSoftware::CreateBuffer()
+{
+ return new CRenderBufferImpl(m_format, m_sourceWidth, m_sourceHeight);
+}
+
+CRendererSoftware::CRenderBufferImpl::CRenderBufferImpl(AVPixelFormat av_pix_format, unsigned width, unsigned height)
+ : CRenderBuffer(av_pix_format, width, height)
+{
+}
+
+CRendererSoftware::CRenderBufferImpl::~CRenderBufferImpl()
+{
+ CRenderBufferImpl::ReleasePicture();
+}
+
+void CRendererSoftware::CRenderBufferImpl::AppendPicture(const VideoPicture& picture)
+{
+ __super::AppendPicture(picture);
+
+ if (videoBuffer->GetFormat() == AV_PIX_FMT_D3D11VA_VLD)
+ {
+ QueueCopyFromGPU();
+ m_widthTex = m_sDesc.Width;
+ m_heightTex = m_sDesc.Height;
+ }
+}
+
+bool CRendererSoftware::CRenderBufferImpl::GetDataPlanes(uint8_t*(&planes)[3], int(&strides)[3])
+{
+ if (!videoBuffer)
+ return false;
+
+ switch (videoBuffer->GetFormat())
+ {
+ case AV_PIX_FMT_D3D11VA_VLD:
+ planes[0] = reinterpret_cast<uint8_t*>(m_msr.pData);
+ planes[1] = reinterpret_cast<uint8_t*>(m_msr.pData) + m_msr.RowPitch * m_sDesc.Height;
+ strides[0] = strides[1] = m_msr.RowPitch;
+ break;
+ default:
+ videoBuffer->GetPlanes(planes);
+ videoBuffer->GetStrides(strides);
+ }
+
+ return true;
+}
+
+void CRendererSoftware::CRenderBufferImpl::ReleasePicture()
+{
+ if (m_staging && m_msr.pData != nullptr)
+ {
+ DX::DeviceResources::Get()->GetImmediateContext()->Unmap(m_staging.Get(), 0);
+ m_msr = {};
+ }
+ __super::ReleasePicture();
+}
+
+bool CRendererSoftware::CRenderBufferImpl::UploadBuffer()
+{
+ if (!videoBuffer)
+ return false;
+
+ if (videoBuffer->GetFormat() != AV_PIX_FMT_D3D11VA_VLD)
+ {
+ m_bLoaded = true;
+ return true;
+ }
+
+ if (!m_staging)
+ return false;
+
+ if (m_msr.pData == nullptr)
+ {
+ // map will finish copying data from GPU to CPU
+ m_bLoaded = SUCCEEDED(DX::DeviceResources::Get()->GetImmediateContext()->Map(
+ m_staging.Get(), 0, D3D11_MAP_READ, 0, &m_msr));
+ }
+
+ return m_bLoaded;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererSoftware.h b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererSoftware.h
new file mode 100644
index 0000000..25abeeb
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/windows/RendererSoftware.h
@@ -0,0 +1,54 @@
+/*
+ * 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 "RendererBase.h"
+
+#include <map>
+extern "C" {
+#include <libavutil/pixfmt.h>
+#include <libswscale/swscale.h>
+}
+
+class CRendererSoftware : public CRendererBase
+{
+ class CRenderBufferImpl;
+public:
+ ~CRendererSoftware();
+
+ bool Configure(const VideoPicture& picture, float fps, unsigned orientation) override;
+ bool Supports(ESCALINGMETHOD method) const override;
+
+ static CRendererBase* Create(CVideoSettings& videoSettings);
+ static void GetWeight(std::map<RenderMethod, int>& weights, const VideoPicture& picture);
+
+protected:
+ explicit CRendererSoftware(CVideoSettings& videoSettings) : CRendererBase(videoSettings) {}
+ CRenderBuffer* CreateBuffer() override;
+ void RenderImpl(CD3DTexture& target, CRect& sourceRect, CPoint(&destPoints)[4], uint32_t flags) override;
+ void FinalOutput(CD3DTexture& source, CD3DTexture& target, const CRect& src, const CPoint(&destPoints)[4]) override;
+
+private:
+ SwsContext* m_sw_scale_ctx = nullptr;
+};
+
+class CRendererSoftware::CRenderBufferImpl : public CRenderBuffer
+{
+public:
+ explicit CRenderBufferImpl(AVPixelFormat av_pix_format, unsigned width, unsigned height);
+ ~CRenderBufferImpl();
+
+ void AppendPicture(const VideoPicture& picture) override;
+ bool GetDataPlanes(uint8_t*(&planes)[3], int(&strides)[3]) override;
+
+ void ReleasePicture() override;
+ bool UploadBuffer() override;
+
+private:
+ D3D11_MAPPED_SUBRESOURCE m_msr{};
+};
diff --git a/xbmc/cores/VideoPlayer/test/edl/CMakeLists.txt b/xbmc/cores/VideoPlayer/test/edl/CMakeLists.txt
new file mode 100644
index 0000000..4f88a1e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/test/edl/CMakeLists.txt
@@ -0,0 +1,3 @@
+set(SOURCES TestEdl.cpp)
+
+core_add_test_library(edl_test)
diff --git a/xbmc/cores/VideoPlayer/test/edl/TestEdl.cpp b/xbmc/cores/VideoPlayer/test/edl/TestEdl.cpp
new file mode 100644
index 0000000..7098b91
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/test/edl/TestEdl.cpp
@@ -0,0 +1,461 @@
+/*
+ * Copyright (C) 2022- 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 "FileItem.h"
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/Edl.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "test/TestUtils.h"
+
+#include <cmath>
+
+#include <gtest/gtest.h>
+
+using namespace EDL;
+
+
+class TestEdl : public ::testing::Test
+{
+protected:
+ TestEdl() = default;
+};
+
+TEST_F(TestEdl, TestParsingMplayerTimeBasedEDL)
+{
+ CEdl edl;
+
+ // create a dummy "media" fileitem whose corresponding edl file is testdata/mplayertimebased.edl
+ CFileItem mediaItem;
+ mediaItem.SetPath(
+ XBMC_REF_FILE_PATH("xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebased.mkv"));
+ const bool found = edl.ReadEditDecisionLists(mediaItem, 0);
+ // expect kodi to be able to parse the file correctly
+ EXPECT_EQ(found, true);
+ // the file has 5 interest points: 2 scenemarkers, 1 mute, 1 cut and 1 commbreak
+ // edit list should contain any edit that is not a scene marker nor a cut, so 2.
+ // scenemarkers should be 4 (two "raw" scene markers and 2 from the commbreak - start and end of commbreaks)
+ EXPECT_EQ(edl.HasEdits(), true);
+ EXPECT_EQ(edl.HasSceneMarker(), true);
+ EXPECT_EQ(edl.GetEditList().size(), 2);
+ EXPECT_EQ(edl.GetSceneMarkers().size(), 4);
+
+ // cuts
+ // file has only 1 cut starting at 5.3 secs and ending at 7.1 secs
+ // so total cut time should be 1.8 seconds (1800 msec)
+ EXPECT_EQ(edl.HasCuts(), true);
+ EXPECT_EQ(edl.GetTotalCutTime(), 1.8 * 1000);
+ EXPECT_EQ(edl.GetCutMarkers().size(), 1);
+ EXPECT_EQ(edl.GetCutMarkers().at(0), 5.3 * 1000); // 5.3 secs
+ // When removing or restoring cuts, EDL adds (or removes) 1 msec to jump over the start or end boundary of the edit
+ EXPECT_EQ(edl.GetTimeWithoutCuts(edl.GetRawEditList().at(0).start),
+ edl.GetRawEditList().at(0).start + 1);
+ EXPECT_EQ(edl.GetTimeAfterRestoringCuts(edl.GetRawEditList().at(0).start),
+ edl.GetRawEditList().at(0).start);
+ EXPECT_EQ(edl.GetTimeAfterRestoringCuts(edl.GetRawEditList().at(0).start + 2),
+ edl.GetRawEditList().at(0).end + 2);
+
+ // the first edit (note editlist does not contain cuts) is a mute section starting at 15 seconds
+ // of the real file this should correspond to 13.2 secs on the kodi VideoPlayer timeline (start - cuttime)
+ // raw edit list contains cuts so mute should be index 1
+ const auto mute = edl.GetEditList().at(0);
+ const auto muteRaw = edl.GetRawEditList().at(1);
+ EXPECT_EQ(mute.action, Action::MUTE);
+ EXPECT_EQ(muteRaw.action, Action::MUTE);
+ EXPECT_EQ(edl.GetTimeWithoutCuts(mute.start), mute.start - edl.GetTotalCutTime());
+ EXPECT_EQ(edl.GetTimeAfterRestoringCuts(mute.start - edl.GetTotalCutTime()), mute.start);
+ EXPECT_EQ(muteRaw.start - edl.GetTotalCutTime(), mute.start);
+ EXPECT_EQ(edl.InEdit(muteRaw.start, nullptr), true);
+ EXPECT_EQ(edl.InEdit(mute.start, nullptr), false);
+
+ // scene markers
+ // one of the scenemarkers (the first) have start and end times defined, kodi should assume the marker at the END position (255.3 secs)
+ EXPECT_EQ(edl.GetSceneMarkers().at(0), edl.GetTimeWithoutCuts(255.3 * 1000));
+ // one of them only has start defined, at 720.1 secs
+ EXPECT_EQ(edl.GetSceneMarkers().at(1), edl.GetTimeWithoutCuts(720.1 * 1000));
+
+ // commbreaks
+ // the second edit on the file is a commbreak
+ const auto commbreak = edl.GetEditList().at(1);
+ EXPECT_EQ(commbreak.action, Action::COMM_BREAK);
+ // We should have a scenemarker at the commbreak start and another on commbreak end
+ int time;
+ // lets cycle to the next scenemarker if starting from 1 msec before the start (or end) of the commbreak
+ EXPECT_EQ(edl.GetNextSceneMarker(true, commbreak.start - 1, &time), true);
+ EXPECT_EQ(edl.GetTimeWithoutCuts(time), commbreak.start);
+ EXPECT_EQ(edl.GetNextSceneMarker(true, commbreak.end - 1, &time), true);
+ EXPECT_EQ(edl.GetTimeWithoutCuts(time), commbreak.end);
+ // same if we cycle backwards
+ EXPECT_EQ(edl.GetNextSceneMarker(false, commbreak.start + 1, &time), true);
+ EXPECT_EQ(edl.GetTimeWithoutCuts(time), commbreak.start);
+ EXPECT_EQ(edl.GetNextSceneMarker(false, commbreak.end + 1, &time), true);
+ EXPECT_EQ(edl.GetTimeWithoutCuts(time), commbreak.end);
+ // We should be in an edit if we are in the middle of a commbreak...
+ // lets check and confirm the edits match (after restoring cuts)
+ Edit thisEdit;
+ const int middleOfCommbreak = commbreak.start + (commbreak.end - commbreak.start) / 2;
+ EXPECT_EQ(edl.InEdit(edl.GetTimeWithoutCuts(middleOfCommbreak), &thisEdit), true);
+ EXPECT_EQ(thisEdit.action, Action::COMM_BREAK);
+ EXPECT_EQ(thisEdit.start, edl.GetTimeAfterRestoringCuts(commbreak.start));
+ EXPECT_EQ(thisEdit.end, edl.GetTimeAfterRestoringCuts(commbreak.end));
+}
+
+TEST_F(TestEdl, TestParsingMplayerTimeBasedInterleavedCutsEDL)
+{
+ CEdl edl;
+
+ // create a dummy "media" fileitem whose corresponding edl file is testdata/mplayertimebasedinterleavedcuts.edl
+ // this is an edl file with commbreaks interleaved with cuts
+ CFileItem mediaItem;
+ mediaItem.SetPath(XBMC_REF_FILE_PATH(
+ "xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebasedinterleavedcuts.mkv"));
+ const bool found = edl.ReadEditDecisionLists(mediaItem, 0);
+ // expect kodi to be able to parse the file correctly
+ EXPECT_EQ(found, true);
+ EXPECT_EQ(edl.GetEditList().size(), 2);
+ EXPECT_EQ(edl.HasCuts(), true);
+ EXPECT_EQ(edl.HasEdits(), true);
+ EXPECT_EQ(edl.GetCutMarkers().size(), 2);
+ // lets check the total cut time matches the sum of the two cut durations defined in the file
+ EXPECT_EQ(edl.GetTotalCutTime(), ((7.1 - 5.3) + (19 - 18)) * 1000);
+ // the first edit is after the first cut, so lets check the start time was adjusted exactly by the cut duration
+ EXPECT_EQ(edl.GetEditList().at(0).start, edl.GetRawEditList().at(1).start - ((7.1 - 5.3) * 1000));
+ EXPECT_EQ(edl.GetEditList().at(0).start,
+ edl.GetTimeWithoutCuts(edl.GetRawEditList().at(1).start));
+ EXPECT_EQ(edl.GetEditList().at(1).start,
+ edl.GetRawEditList().at(3).start - edl.GetTotalCutTime());
+ EXPECT_EQ(edl.GetEditList().at(1).start,
+ edl.GetTimeWithoutCuts(edl.GetRawEditList().at(3).start));
+}
+
+TEST_F(TestEdl, TestParsingMplayerFrameBasedEDL)
+{
+ CEdl edl;
+ // suppose we're playing a file with 60 fps
+ const float fps = 60;
+
+ // create a dummy "media" fileitem whose corresponding edl file is testdata/mplayerframebased.edl
+ // this is an edl file with frame based edit points
+ CFileItem mediaItem;
+ mediaItem.SetPath(
+ XBMC_REF_FILE_PATH("xbmc/cores/VideoPlayer/test/edl/testdata/mplayerframebased.mkv"));
+ const bool found = edl.ReadEditDecisionLists(mediaItem, fps);
+ // expect kodi to be able to parse the file correctly
+ EXPECT_EQ(found, true);
+ EXPECT_EQ(edl.HasEdits(), true);
+ EXPECT_EQ(edl.HasCuts(), true);
+ EXPECT_EQ(edl.HasSceneMarker(), true);
+ EXPECT_EQ(edl.GetEditList().size(), 2);
+ // check edit times are correctly calculated provided the fps
+ EXPECT_EQ(edl.GetEditList().at(0).start,
+ static_cast<int64_t>((360 / fps) * 1000) - edl.GetTotalCutTime());
+ EXPECT_EQ(edl.GetSceneMarkers().at(0),
+ static_cast<int64_t>((6127 / fps) * 1000) - edl.GetTotalCutTime());
+}
+
+TEST_F(TestEdl, TestParsingMplayerTimeBasedMixedEDL)
+{
+ CEdl edl;
+
+ // create a dummy "media" fileitem whose corresponding edl file is testdata/mplayertimebasedmixed.edl
+ // this file has edit points with seconds and others with seconds timestrings
+ CFileItem mediaItem;
+ mediaItem.SetPath(
+ XBMC_REF_FILE_PATH("xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebasedmixed.mkv"));
+ bool found = edl.ReadEditDecisionLists(mediaItem, 0);
+ // expect kodi to be able to parse the file correctly
+ EXPECT_EQ(found, true);
+ EXPECT_EQ(edl.HasEdits(), true);
+ EXPECT_EQ(edl.HasCuts(), true);
+ EXPECT_EQ(edl.HasSceneMarker(), true);
+ EXPECT_EQ(edl.GetSceneMarkers().size(), 4);
+ bool sceneFound = false;
+ // check we have correctly parsed the scene with 12:00.1 start point
+ for (const auto& scene : edl.GetSceneMarkers())
+ {
+ if (scene == (12 * 60 + 0.1) * 1000 - edl.GetTotalCutTime())
+ {
+ sceneFound = true;
+ break;
+ }
+ }
+ EXPECT_EQ(sceneFound, true);
+ // check that the first ordered edit starts at 15 secs
+ EXPECT_EQ(edl.GetEditList().front().start, (15 * 1000) - edl.GetTotalCutTime());
+}
+
+TEST_F(TestEdl, TestParsingVideoRedoEDL)
+{
+ CEdl edl;
+
+ // create a dummy "media" fileitem whose corresponding edl file is testdata/videoredo.Vprj
+ // this is an edl file in VideoReDo format
+ CFileItem mediaItem;
+ mediaItem.SetPath(XBMC_REF_FILE_PATH("xbmc/cores/VideoPlayer/test/edl/testdata/videoredo.mkv"));
+ bool found = edl.ReadEditDecisionLists(mediaItem, 0);
+ EXPECT_EQ(found, true);
+ EXPECT_EQ(edl.HasEdits(), true);
+ // videoredo only supports cuts or scenemarkers, hence the editlist should be empty. Raw editlist should contain the cuts.
+ EXPECT_EQ(edl.GetEditList().size(), 0);
+ EXPECT_GT(edl.GetRawEditList().size(), 0);
+ EXPECT_EQ(edl.HasCuts(), true);
+ EXPECT_EQ(edl.HasSceneMarker(), true);
+ EXPECT_EQ(edl.GetSceneMarkers().size(), 4);
+ EXPECT_EQ(edl.GetCutMarkers().size(), 3);
+ // in videoredo time processing is ms * 10000
+ // first cut in the file is at 4235230000 - let's confirm this corresponds to second 423.523
+ EXPECT_EQ(edl.GetCutMarkers().front(), 423.523 * 1000);
+}
+
+TEST_F(TestEdl, TestSnapStreamEDL)
+{
+ CEdl edl;
+
+ // create a dummy "media" fileitem whose corresponding edl file is testdata/snapstream.mkv.chapters.xml
+ // this is an edl file in SnapStream BeyondTV format
+ CFileItem mediaItem;
+ mediaItem.SetPath(XBMC_REF_FILE_PATH("xbmc/cores/VideoPlayer/test/edl/testdata/snapstream.mkv"));
+ const bool found = edl.ReadEditDecisionLists(mediaItem, 0);
+ EXPECT_EQ(found, true);
+ // this format only supports commbreak types
+ EXPECT_EQ(edl.HasEdits(), true);
+ EXPECT_EQ(edl.GetCutMarkers().empty(), true);
+ EXPECT_EQ(edl.GetEditList().size(), 3);
+ EXPECT_EQ(edl.GetSceneMarkers().size(), 3 * 2); // start and end of each commbreak
+ // snapstream beyond tv uses ms * 10000
+ // check if first commbreak (4235230000 - 5936600000) is 423.523 sec - 593.660 sec
+ EXPECT_EQ(edl.GetEditList().front().start, std::lround(423.523 * 1000));
+ EXPECT_EQ(edl.GetEditList().front().end, std::lround(593.660 * 1000));
+}
+
+TEST_F(TestEdl, TestComSkipVersion1EDL)
+{
+ CEdl edl;
+
+ // create a dummy "media" fileitem whose corresponding edl file is testdata/comskipversion1.txt
+ // this is an edl file in ComSkip (version 1) format
+ CFileItem mediaItem;
+ mediaItem.SetPath(
+ XBMC_REF_FILE_PATH("xbmc/cores/VideoPlayer/test/edl/testdata/comskipversion1.mkv"));
+ bool found = edl.ReadEditDecisionLists(mediaItem, 0);
+ // fps was not supplied, kodi will not be able to process the file
+ EXPECT_EQ(found, false);
+ // parse the file again this time supplying 60 fps
+ const float fps = 60;
+ found = edl.ReadEditDecisionLists(mediaItem, fps);
+ EXPECT_EQ(found, true);
+ EXPECT_EQ(edl.HasEdits(), true);
+ EXPECT_EQ(edl.GetCutMarkers().empty(), true);
+ EXPECT_EQ(edl.GetEditList().size(), 3);
+ EXPECT_EQ(edl.GetSceneMarkers().size(), 3 * 2); // start and end of each commbreak
+ EXPECT_EQ(edl.GetEditList().front().start, std::lround(12693 / fps * 1000));
+ EXPECT_EQ(edl.GetEditList().front().end, std::lround(17792 / fps * 1000));
+}
+
+TEST_F(TestEdl, TestComSkipVersion2EDL)
+{
+ CEdl edl;
+
+ // create a dummy "media" fileitem whose corresponding edl file is testdata/comskipversion2.txt
+ // this is an edl file in ComSkip (version 2) format where fps is obtained from the file
+ CFileItem mediaItem;
+ mediaItem.SetPath(
+ XBMC_REF_FILE_PATH("xbmc/cores/VideoPlayer/test/edl/testdata/comskipversion2.mkv"));
+ const bool found = edl.ReadEditDecisionLists(mediaItem, 0);
+ EXPECT_EQ(found, true);
+ // fps is obtained from the file as it always takes precedence (note we supplied 0 above),
+ // the EDL file has the value of 2500 for fps. kodi converts this to 25 fps by dividing by a factor of 100
+ const float fpsInEdlFile = 2500 / 100;
+ // this format only supports commbreak types
+ EXPECT_EQ(edl.HasEdits(), true);
+ EXPECT_EQ(edl.GetCutMarkers().empty(), true);
+ EXPECT_EQ(edl.GetEditList().size(), 3);
+ EXPECT_EQ(edl.GetSceneMarkers().size(), 3 * 2); // start and end of each commbreak
+ EXPECT_EQ(edl.GetEditList().front().start, std::lround(12693 / fpsInEdlFile * 1000));
+ EXPECT_EQ(edl.GetEditList().front().end, std::lround(17792 / fpsInEdlFile * 1000));
+}
+
+TEST_F(TestEdl, TestRuntimeSetEDL)
+{
+ // this is a simple test for SetLastEditTime, SetLastEditActionType and corresponding getters
+ CEdl edl;
+ edl.SetLastEditTime(1000);
+ edl.SetLastEditActionType(Action::COMM_BREAK);
+ EXPECT_EQ(edl.GetLastEditTime(), 1000);
+ EXPECT_EQ(edl.GetLastEditActionType(), Action::COMM_BREAK);
+}
+
+TEST_F(TestEdl, TestCommBreakAdvancedSettings)
+{
+ CEdl edl;
+ const std::shared_ptr<CAdvancedSettings> advancedSettings =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ // the test goal is to test m_iEdlCommBreakAutowait and m_iEdlCommBreakAutowind
+ // lets check first the behaviour with default values (both disabled)
+ // Keep all EDL advanced settings to default
+ advancedSettings->m_iEdlCommBreakAutowait = 0; // disabled (default)
+ advancedSettings->m_iEdlCommBreakAutowind = 0; // disabled (default)
+ advancedSettings->m_bEdlMergeShortCommBreaks = false; // disabled (default)
+ advancedSettings->m_iEdlMinCommBreakLength = 3 * 30; // 3*30 secs (default)
+ advancedSettings->m_iEdlMaxCommBreakLength = 8 * 30 + 10; // 8*30+10 secs (default value)
+ advancedSettings->m_iEdlMaxStartGap = 5 * 60; // 5 minutes (default)
+ EXPECT_EQ(advancedSettings->m_iEdlCommBreakAutowait, 0);
+ EXPECT_EQ(advancedSettings->m_iEdlCommBreakAutowind, 0);
+ EXPECT_EQ(advancedSettings->m_bEdlMergeShortCommBreaks, false);
+ EXPECT_EQ(advancedSettings->m_iEdlMinCommBreakLength, 3 * 30);
+ EXPECT_EQ(advancedSettings->m_iEdlMinCommBreakLength, 3 * 30);
+ EXPECT_EQ(advancedSettings->m_iEdlMaxCommBreakLength, 8 * 30 + 10);
+ EXPECT_EQ(advancedSettings->m_iEdlMaxStartGap, 5 * 60);
+
+ // create a dummy "media" fileitem whose corresponding edl file is testdata/edlautowindautowait.txt
+ CFileItem mediaItem;
+ mediaItem.SetPath(
+ XBMC_REF_FILE_PATH("xbmc/cores/VideoPlayer/test/edl/testdata/edlautowindautowait.mkv"));
+ bool found = edl.ReadEditDecisionLists(mediaItem, 0);
+ EXPECT_EQ(found, true);
+ // confirm the start and end times of all the commbreaks match
+ EXPECT_EQ(edl.GetEditList().size(), 5);
+ EXPECT_EQ(edl.GetEditList().at(0).start, 10 * 1000);
+ EXPECT_EQ(edl.GetEditList().at(0).end, 22 * 1000);
+ EXPECT_EQ(edl.GetEditList().at(1).start, 30 * 1000);
+ EXPECT_EQ(edl.GetEditList().at(1).end, 32 * 1000);
+ EXPECT_EQ(edl.GetEditList().at(2).start, 37 * 1000);
+ EXPECT_EQ(edl.GetEditList().at(2).end, 50 * 1000);
+ EXPECT_EQ(edl.GetEditList().at(3).start, 52 * 1000);
+ EXPECT_EQ(edl.GetEditList().at(3).end, 60 * 1000);
+ EXPECT_EQ(edl.GetEditList().at(4).start, 62 * 1000);
+ EXPECT_EQ(edl.GetEditList().at(4).end, std::lround(65.1 * 1000));
+ // now lets change autowait and autowind and check the edits are correcly adjusted
+ edl.Clear();
+ advancedSettings->m_iEdlCommBreakAutowait = 3; // secs
+ advancedSettings->m_iEdlCommBreakAutowind = 3; // secs
+ EXPECT_EQ(advancedSettings->m_iEdlCommBreakAutowait, 3);
+ EXPECT_EQ(advancedSettings->m_iEdlCommBreakAutowind, 3);
+ found = edl.ReadEditDecisionLists(mediaItem, 0);
+ EXPECT_EQ(edl.GetEditList().size(), 5);
+ // the second edit has a duration smaller than the autowait
+ // this moves the start time to the end of the edit
+ EXPECT_EQ(edl.GetEditList().at(1).start, 32 * 1000);
+ EXPECT_EQ(edl.GetEditList().at(1).end, edl.GetEditList().at(1).start);
+ // the others should be adjusted + 3 secs at the start and -3 secs at the end
+ // due to the provided values for autowait and autowind.
+ EXPECT_EQ(edl.GetEditList().at(0).start, (10 + 3) * 1000);
+ EXPECT_EQ(edl.GetEditList().at(0).end, (22 - 3) * 1000);
+ EXPECT_EQ(edl.GetEditList().at(2).start, (37 + 3) * 1000);
+ EXPECT_EQ(edl.GetEditList().at(2).end, (50 - 3) * 1000);
+ EXPECT_EQ(edl.GetEditList().at(3).start, (52 + 3) * 1000);
+ EXPECT_EQ(edl.GetEditList().at(3).end, (60 - 3) * 1000);
+ // since we adjust the start to second 65 and the autowind is 3 seconds kodi should
+ // shift the end time not by 3 seconds but by the "excess" time (in this case 0.1 sec)
+ // this means start and end will be exactly the same. The commbreak would be removed if
+ // mergeshortcommbreaks was active and advancedsetting m_iEdlMinCommBreakLength
+ // was set to a reasonable threshold.
+ EXPECT_EQ(edl.GetEditList().at(4).start, (62 + 3) * 1000);
+ EXPECT_EQ(edl.GetEditList().at(4).end, (65.1 - 0.1) * 1000);
+ EXPECT_EQ(edl.GetEditList().at(4).start, edl.GetEditList().at(4).end);
+}
+
+TEST_F(TestEdl, TestCommBreakAdvancedSettingsRemoveSmallCommbreaks)
+{
+ // this is a variation of TestCommBreakAdvancedSettings
+ // should make sure the number of commbreaks in the file is now 3 instead of 5
+ // since two of them have duration smaller than 1 sec
+ CEdl edl;
+ const std::shared_ptr<CAdvancedSettings> advancedSettings =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ // set EDL advanced settings specific for the test case
+ advancedSettings->m_iEdlCommBreakAutowait = 3; // secs
+ advancedSettings->m_iEdlCommBreakAutowind = 3; // secs
+ advancedSettings->m_bEdlMergeShortCommBreaks = true;
+ advancedSettings->m_iEdlMinCommBreakLength = 1; // sec
+ advancedSettings->m_iEdlMaxCommBreakLength = 0; // deactivate
+ advancedSettings->m_iEdlMaxStartGap = 0; // deactivate
+ EXPECT_EQ(advancedSettings->m_iEdlCommBreakAutowait, 3);
+ EXPECT_EQ(advancedSettings->m_iEdlCommBreakAutowind, 3);
+ EXPECT_EQ(advancedSettings->m_bEdlMergeShortCommBreaks, true);
+ EXPECT_EQ(advancedSettings->m_iEdlMinCommBreakLength, 1);
+ EXPECT_EQ(advancedSettings->m_iEdlMaxCommBreakLength, 0);
+ EXPECT_EQ(advancedSettings->m_iEdlMaxStartGap, 0);
+
+ CFileItem mediaItem;
+ mediaItem.SetPath(
+ XBMC_REF_FILE_PATH("xbmc/cores/VideoPlayer/test/edl/testdata/edlautowindautowait.mkv"));
+ const bool found = edl.ReadEditDecisionLists(mediaItem, 0);
+ EXPECT_EQ(found, true);
+ EXPECT_EQ(edl.GetEditList().size(), 3);
+}
+
+TEST_F(TestEdl, TestMergeSmallCommbreaks)
+{
+ CEdl edl;
+ const std::shared_ptr<CAdvancedSettings> advancedSettings =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ // set EDL advanced settings specific for the test case
+ advancedSettings->m_bEdlMergeShortCommBreaks = true;
+ EXPECT_EQ(advancedSettings->m_bEdlMergeShortCommBreaks, true);
+ // keep any other EDL advanced settings to default
+ advancedSettings->m_iEdlCommBreakAutowait = 0; // disabled (default)
+ advancedSettings->m_iEdlCommBreakAutowind = 0; // disabled (default)
+ advancedSettings->m_iEdlMinCommBreakLength = 3 * 30; // 3*30 secs (default)
+ advancedSettings->m_iEdlMaxCommBreakLength = 8 * 30 + 10; // 8*30+10 secs (default value)
+ advancedSettings->m_iEdlMaxStartGap = 5 * 60; // 5 minutes (default)
+ EXPECT_EQ(advancedSettings->m_iEdlCommBreakAutowait, 0);
+ EXPECT_EQ(advancedSettings->m_iEdlCommBreakAutowind, 0);
+ EXPECT_EQ(advancedSettings->m_iEdlMinCommBreakLength, 3 * 30);
+ EXPECT_EQ(advancedSettings->m_iEdlMaxCommBreakLength, 8 * 30 + 10);
+ EXPECT_EQ(advancedSettings->m_iEdlMaxStartGap, 5 * 60);
+
+ CFileItem mediaItem;
+ mediaItem.SetPath(
+ XBMC_REF_FILE_PATH("xbmc/cores/VideoPlayer/test/edl/testdata/edlautowindautowait.mkv"));
+ const bool found = edl.ReadEditDecisionLists(mediaItem, 0);
+ EXPECT_EQ(found, true);
+ // kodi should merge all commbreaks into a single one starting at the first point (0)
+ // and ending at the last edit time
+ EXPECT_EQ(edl.GetEditList().size(), 1);
+ EXPECT_EQ(edl.GetEditList().at(0).start, 0);
+ EXPECT_EQ(edl.GetEditList().at(0).end, std::lround(65.1 * 1000));
+}
+
+TEST_F(TestEdl, TestMergeSmallCommbreaksAdvanced)
+{
+ CEdl edl;
+ const std::shared_ptr<CAdvancedSettings> advancedSettings =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ // set EDL advanced settings specific for the test case
+ advancedSettings->m_bEdlMergeShortCommBreaks = true;
+ advancedSettings->m_iEdlMaxCommBreakLength = 30; // 30 secs
+ advancedSettings->m_iEdlMinCommBreakLength = 1; // 1 sec
+ advancedSettings->m_iEdlMaxStartGap = 2; // 2 secs
+ EXPECT_EQ(advancedSettings->m_bEdlMergeShortCommBreaks, true);
+ EXPECT_EQ(advancedSettings->m_iEdlMaxCommBreakLength, 30);
+ EXPECT_EQ(advancedSettings->m_iEdlMinCommBreakLength, 1);
+ EXPECT_EQ(advancedSettings->m_iEdlMaxStartGap, 2);
+ // keep any other EDL advanced settings to default
+ advancedSettings->m_iEdlCommBreakAutowait = 0; // disabled (default)
+ advancedSettings->m_iEdlCommBreakAutowind = 0; // disabled (default)
+ EXPECT_EQ(advancedSettings->m_iEdlCommBreakAutowait, 0);
+ EXPECT_EQ(advancedSettings->m_iEdlCommBreakAutowind, 0);
+
+ CFileItem mediaItem;
+ mediaItem.SetPath(
+ XBMC_REF_FILE_PATH("xbmc/cores/VideoPlayer/test/edl/testdata/edlautowindautowait.mkv"));
+ const bool found = edl.ReadEditDecisionLists(mediaItem, 0);
+ EXPECT_EQ(found, true);
+ // kodi should merge all commbreaks into two
+ EXPECT_EQ(edl.GetEditList().size(), 2);
+ // second edit of the original file + third one
+ EXPECT_EQ(edl.GetEditList().at(0).end - edl.GetEditList().at(0).start, (32 - 10) * 1000);
+ // 4th, 5th and 6th commbreaks joined
+ EXPECT_EQ(edl.GetEditList().at(1).end - edl.GetEditList().at(1).start,
+ std::lround((65.1 - 37) * 1000));
+}
diff --git a/xbmc/cores/VideoPlayer/test/edl/testdata/comskipversion1.txt b/xbmc/cores/VideoPlayer/test/edl/testdata/comskipversion1.txt
new file mode 100644
index 0000000..3716d5e
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/test/edl/testdata/comskipversion1.txt
@@ -0,0 +1,5 @@
+FILE PROCESSING COMPLETE
+------------------------
+12693 17792
+28578 34549
+43114 48222
diff --git a/xbmc/cores/VideoPlayer/test/edl/testdata/comskipversion2.txt b/xbmc/cores/VideoPlayer/test/edl/testdata/comskipversion2.txt
new file mode 100644
index 0000000..a54c789
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/test/edl/testdata/comskipversion2.txt
@@ -0,0 +1,5 @@
+FILE PROCESSING COMPLETE 678900 FRAMES AT 2500
+------------------------
+12693 17792
+28578 34549
+43114 48222
diff --git a/xbmc/cores/VideoPlayer/test/edl/testdata/edlautowindautowait.edl b/xbmc/cores/VideoPlayer/test/edl/testdata/edlautowindautowait.edl
new file mode 100644
index 0000000..8506acb
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/test/edl/testdata/edlautowindautowait.edl
@@ -0,0 +1,7 @@
+## first commbreak should be automatically removed
+0 1 3
+10 22 3
+30 32 3
+37 50 3
+52 60 3
+62 65.1 3
diff --git a/xbmc/cores/VideoPlayer/test/edl/testdata/mplayerframebased.edl b/xbmc/cores/VideoPlayer/test/edl/testdata/mplayerframebased.edl
new file mode 100644
index 0000000..d8aca36
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/test/edl/testdata/mplayerframebased.edl
@@ -0,0 +1,5 @@
+#127 #170 0
+#360 #400 1
+#10080 #19728 3
+#1 #6127 2
+#17282 2
diff --git a/xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebased.edl b/xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebased.edl
new file mode 100644
index 0000000..5bb4ad5
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebased.edl
@@ -0,0 +1,6 @@
+5.3 7.1 0
+15 16.7 1
+## This is a comment kodi should happily ignore when parsing
+420 822 3
+1 255.3 2
+720.1 2
diff --git a/xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebasedinterleavedcuts.edl b/xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebasedinterleavedcuts.edl
new file mode 100644
index 0000000..5a4f036
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebasedinterleavedcuts.edl
@@ -0,0 +1,4 @@
+5.3 7.1 0
+15 16.7 3
+18 19 0
+25 36.7 3
diff --git a/xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebasedmixed.edl b/xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebasedmixed.edl
new file mode 100644
index 0000000..383eb9a
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/test/edl/testdata/mplayertimebasedmixed.edl
@@ -0,0 +1,5 @@
+5.3 7.1 0
+15 16.7 1
+7:00 13:42 3
+1 4:15.3 2
+12:00.1 2
diff --git a/xbmc/cores/VideoPlayer/test/edl/testdata/snapstream.mkv.chapters.xml b/xbmc/cores/VideoPlayer/test/edl/testdata/snapstream.mkv.chapters.xml
new file mode 100644
index 0000000..cae6f37
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/test/edl/testdata/snapstream.mkv.chapters.xml
@@ -0,0 +1,14 @@
+<cutlist>
+ <Region>
+ <start comment="0:07:03.0000523">4235230000</start>
+ <end comment="0:09:53.0000660">5936600000</end>
+ </Region>
+ <Region>
+ <start comment="0:15:53.0000553">9535530000</start>
+ <end comment="0:19:12.0000786">11527860000</end>
+ </Region>
+ <Region>
+ <start comment="0:23:58.0000571">14385710000</start>
+ <end comment="0:26:49.0000009">16090090000</end>
+ </Region>
+</cutlist>
diff --git a/xbmc/cores/VideoPlayer/test/edl/testdata/videoredo.Vprj b/xbmc/cores/VideoPlayer/test/edl/testdata/videoredo.Vprj
new file mode 100644
index 0000000..72236b8
--- /dev/null
+++ b/xbmc/cores/VideoPlayer/test/edl/testdata/videoredo.Vprj
@@ -0,0 +1,13 @@
+<Version>2
+<Filename>C:\Path\To\Video.mpg
+<InputPIDList>
+<VideoStreamPID>224</VideoStreamPID>
+<AudioStreamPID>192</AudioStreamPID>
+</InputPIDList>
+<SceneMarker 1415932542>4235230000
+<SceneMarker 1415932543>4284610000
+<SceneMarker 1415932544>4585580000
+<SceneMarker 1415932545>5035360000
+<Cut>4235230000:5936600000
+<Cut>9535530000:11527860000
+<Cut>14385710000:16090090000
diff --git a/xbmc/cores/VideoSettings.cpp b/xbmc/cores/VideoSettings.cpp
new file mode 100644
index 0000000..ec474b3
--- /dev/null
+++ b/xbmc/cores/VideoSettings.cpp
@@ -0,0 +1,149 @@
+/*
+ * 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 "VideoSettings.h"
+
+#include "threads/CriticalSection.h"
+
+#include <mutex>
+
+CVideoSettings::CVideoSettings()
+{
+ m_InterlaceMethod = VS_INTERLACEMETHOD_AUTO;
+ m_ScalingMethod = VS_SCALINGMETHOD_LINEAR;
+ m_ViewMode = ViewModeNormal;
+ m_CustomZoomAmount = 1.0f;
+ m_CustomPixelRatio = 1.0f;
+ m_CustomVerticalShift = 0.0f;
+ m_CustomNonLinStretch = false;
+ m_AudioStream = -1;
+ m_SubtitleStream = -1;
+ m_SubtitleDelay = 0.0f;
+ m_subtitleVerticalPosition = 0;
+ m_subtitleVerticalPositionSave = false;
+ m_SubtitleOn = true;
+ m_Brightness = 50.0f;
+ m_Contrast = 50.0f;
+ m_Gamma = 20.0f;
+ m_Sharpness = 0.0f;
+ m_NoiseReduction = 0;
+ m_PostProcess = false;
+ m_VolumeAmplification = 0;
+ m_AudioDelay = 0.0f;
+ m_ResumeTime = 0;
+ m_StereoMode = 0;
+ m_StereoInvert = false;
+ m_VideoStream = -1;
+ m_ToneMapMethod = VS_TONEMAPMETHOD_REINHARD;
+ m_ToneMapParam = 1.0f;
+ m_Orientation = 0;
+ m_CenterMixLevel = 0;
+}
+
+bool CVideoSettings::operator!=(const CVideoSettings &right) const
+{
+ if (m_InterlaceMethod != right.m_InterlaceMethod) return true;
+ if (m_ScalingMethod != right.m_ScalingMethod) return true;
+ if (m_ViewMode != right.m_ViewMode) return true;
+ if (m_CustomZoomAmount != right.m_CustomZoomAmount) return true;
+ if (m_CustomPixelRatio != right.m_CustomPixelRatio) return true;
+ if (m_CustomVerticalShift != right.m_CustomVerticalShift) return true;
+ if (m_CustomNonLinStretch != right.m_CustomNonLinStretch) return true;
+ if (m_AudioStream != right.m_AudioStream) return true;
+ if (m_SubtitleStream != right.m_SubtitleStream) return true;
+ if (m_SubtitleDelay != right.m_SubtitleDelay) return true;
+ if (m_subtitleVerticalPosition != right.m_subtitleVerticalPosition)
+ return true;
+ if (m_subtitleVerticalPositionSave != right.m_subtitleVerticalPositionSave)
+ return true;
+ if (m_SubtitleOn != right.m_SubtitleOn) return true;
+ if (m_Brightness != right.m_Brightness) return true;
+ if (m_Contrast != right.m_Contrast) return true;
+ if (m_Gamma != right.m_Gamma) return true;
+ if (m_Sharpness != right.m_Sharpness) return true;
+ if (m_NoiseReduction != right.m_NoiseReduction) return true;
+ if (m_PostProcess != right.m_PostProcess) return true;
+ if (m_VolumeAmplification != right.m_VolumeAmplification) return true;
+ if (m_AudioDelay != right.m_AudioDelay) return true;
+ if (m_ResumeTime != right.m_ResumeTime) return true;
+ if (m_StereoMode != right.m_StereoMode) return true;
+ if (m_StereoInvert != right.m_StereoInvert) return true;
+ if (m_VideoStream != right.m_VideoStream) return true;
+ if (m_ToneMapMethod != right.m_ToneMapMethod) return true;
+ if (m_ToneMapParam != right.m_ToneMapParam) return true;
+ if (m_Orientation != right.m_Orientation) return true;
+ if (m_CenterMixLevel != right.m_CenterMixLevel) return true;
+ return false;
+}
+
+//------------------------------------------------------------------------------
+// CVideoSettingsLocked
+//------------------------------------------------------------------------------
+CVideoSettingsLocked::CVideoSettingsLocked(CVideoSettings &vs, CCriticalSection &critSection) :
+ m_videoSettings(vs), m_critSection(critSection)
+{
+}
+
+void CVideoSettingsLocked::SetSubtitleStream(int stream)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_videoSettings.m_SubtitleStream = stream;
+}
+
+void CVideoSettingsLocked::SetSubtitleVisible(bool visible)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_videoSettings.m_SubtitleOn = visible;
+}
+
+void CVideoSettingsLocked::SetAudioStream(int stream)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_videoSettings.m_AudioStream = stream;
+}
+
+void CVideoSettingsLocked::SetVideoStream(int stream)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_videoSettings.m_VideoStream = stream;
+}
+
+void CVideoSettingsLocked::SetAudioDelay(float delay)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_videoSettings.m_AudioDelay = delay;
+}
+
+void CVideoSettingsLocked::SetSubtitleDelay(float delay)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_videoSettings.m_SubtitleDelay = delay;
+}
+
+void CVideoSettingsLocked::SetSubtitleVerticalPosition(int value, bool save)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_videoSettings.m_subtitleVerticalPosition = value;
+ m_videoSettings.m_subtitleVerticalPositionSave = save;
+}
+
+void CVideoSettingsLocked::SetViewMode(int mode, float zoom, float par, float shift, bool stretch)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_videoSettings.m_ViewMode = mode;
+ m_videoSettings.m_CustomZoomAmount = zoom;
+ m_videoSettings.m_CustomPixelRatio = par;
+ m_videoSettings.m_CustomVerticalShift = shift;
+ m_videoSettings.m_CustomNonLinStretch = stretch;
+}
+
+void CVideoSettingsLocked::SetVolumeAmplification(float amp)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_videoSettings.m_VolumeAmplification = amp;
+}
diff --git a/xbmc/cores/VideoSettings.h b/xbmc/cores/VideoSettings.h
new file mode 100644
index 0000000..a7135da
--- /dev/null
+++ b/xbmc/cores/VideoSettings.h
@@ -0,0 +1,264 @@
+/*
+ * 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 "utils/Map.h"
+
+#include <string_view>
+
+#include <fmt/format.h>
+
+// VideoSettings.h: interface for the CVideoSettings class.
+//
+//////////////////////////////////////////////////////////////////////
+
+enum EINTERLACEMETHOD
+{
+ VS_INTERLACEMETHOD_NONE=0,
+ VS_INTERLACEMETHOD_AUTO=1,
+ VS_INTERLACEMETHOD_RENDER_BLEND=2,
+ VS_INTERLACEMETHOD_RENDER_WEAVE=4,
+ VS_INTERLACEMETHOD_RENDER_BOB=6,
+ VS_INTERLACEMETHOD_DEINTERLACE=7,
+ VS_INTERLACEMETHOD_VDPAU_BOB=8,
+ VS_INTERLACEMETHOD_VDPAU_INVERSE_TELECINE=11,
+ VS_INTERLACEMETHOD_VDPAU_TEMPORAL=12,
+ VS_INTERLACEMETHOD_VDPAU_TEMPORAL_HALF=13,
+ VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL=14,
+ VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL_HALF=15,
+ VS_INTERLACEMETHOD_DEINTERLACE_HALF=16,
+ VS_INTERLACEMETHOD_VAAPI_BOB = 22,
+ VS_INTERLACEMETHOD_VAAPI_MADI = 23,
+ VS_INTERLACEMETHOD_VAAPI_MACI = 24,
+ VS_INTERLACEMETHOD_DXVA_AUTO = 32,
+ VS_INTERLACEMETHOD_MAX // do not use and keep as last enum value.
+};
+
+template<>
+struct fmt::formatter<EINTERLACEMETHOD> : fmt::formatter<std::string_view>
+{
+ template<typename FormatContext>
+ constexpr auto format(const EINTERLACEMETHOD& interlaceMethod, FormatContext& ctx)
+ {
+ const auto it = interlaceMethodMap.find(interlaceMethod);
+ if (it == interlaceMethodMap.cend())
+ throw std::range_error("no interlace method string found");
+
+ return fmt::formatter<string_view>::format(it->second, ctx);
+ }
+
+private:
+ static constexpr auto interlaceMethodMap = make_map<EINTERLACEMETHOD, std::string_view>({
+ {VS_INTERLACEMETHOD_NONE, "none"},
+ {VS_INTERLACEMETHOD_AUTO, "auto"},
+ {VS_INTERLACEMETHOD_RENDER_BLEND, "render blend"},
+ {VS_INTERLACEMETHOD_RENDER_WEAVE, "render weave"},
+ {VS_INTERLACEMETHOD_RENDER_BOB, "render bob"},
+ {VS_INTERLACEMETHOD_DEINTERLACE, "deinterlace"},
+ {VS_INTERLACEMETHOD_VDPAU_BOB, "vdpau bob"},
+ {VS_INTERLACEMETHOD_VDPAU_INVERSE_TELECINE, "vdpau inverse telecine"},
+ {VS_INTERLACEMETHOD_VDPAU_TEMPORAL, "vdpau temporal"},
+ {VS_INTERLACEMETHOD_VDPAU_TEMPORAL_HALF, "vdpau temporal half"},
+ {VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL, "vdpau temporal spatial"},
+ {VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL_HALF, "vdpau temporal spatial half"},
+ {VS_INTERLACEMETHOD_DEINTERLACE_HALF, "deinterlace half"},
+ {VS_INTERLACEMETHOD_VAAPI_BOB, "vaapi bob"},
+ {VS_INTERLACEMETHOD_VAAPI_MADI, "vaapi madi"},
+ {VS_INTERLACEMETHOD_VAAPI_MACI, "vaapi maci"},
+ {VS_INTERLACEMETHOD_DXVA_AUTO, "dxva auto"},
+ });
+};
+
+enum ESCALINGMETHOD
+{
+ VS_SCALINGMETHOD_NEAREST=0,
+ VS_SCALINGMETHOD_LINEAR,
+ VS_SCALINGMETHOD_CUBIC_B_SPLINE,
+ VS_SCALINGMETHOD_CUBIC_MITCHELL,
+ VS_SCALINGMETHOD_CUBIC_CATMULL,
+ VS_SCALINGMETHOD_CUBIC_0_075,
+ VS_SCALINGMETHOD_CUBIC_0_1,
+ VS_SCALINGMETHOD_LANCZOS2,
+ VS_SCALINGMETHOD_LANCZOS3_FAST,
+ VS_SCALINGMETHOD_LANCZOS3,
+ VS_SCALINGMETHOD_SINC8,
+ VS_SCALINGMETHOD_BICUBIC_SOFTWARE,
+ VS_SCALINGMETHOD_LANCZOS_SOFTWARE,
+ VS_SCALINGMETHOD_SINC_SOFTWARE,
+ VS_SCALINGMETHOD_VDPAU_HARDWARE,
+ VS_SCALINGMETHOD_DXVA_HARDWARE,
+ VS_SCALINGMETHOD_AUTO,
+ VS_SCALINGMETHOD_SPLINE36_FAST,
+ VS_SCALINGMETHOD_SPLINE36,
+ VS_SCALINGMETHOD_MAX // do not use and keep as last enum value.
+};
+
+template<>
+struct fmt::formatter<ESCALINGMETHOD> : fmt::formatter<std::string_view>
+{
+public:
+ template<typename FormatContext>
+ constexpr auto format(const ESCALINGMETHOD& scalingMethod, FormatContext& ctx)
+ {
+ const auto it = scalingMethodMap.find(scalingMethod);
+ if (it == scalingMethodMap.cend())
+ throw std::range_error("no scaling method string found");
+
+ return fmt::formatter<string_view>::format(it->second, ctx);
+ }
+
+private:
+ static constexpr auto scalingMethodMap = make_map<ESCALINGMETHOD, std::string_view>({
+ {VS_SCALINGMETHOD_NEAREST, "nearest neighbour"},
+ {VS_SCALINGMETHOD_LINEAR, "linear"},
+ {VS_SCALINGMETHOD_CUBIC_B_SPLINE, "cubic b spline"},
+ {VS_SCALINGMETHOD_CUBIC_MITCHELL, "cubic mitchell"},
+ {VS_SCALINGMETHOD_CUBIC_CATMULL, "cubic catmull"},
+ {VS_SCALINGMETHOD_CUBIC_0_075, "cubic 0/075"},
+ {VS_SCALINGMETHOD_CUBIC_0_1, "cubic 0/1"},
+ {VS_SCALINGMETHOD_LANCZOS2, "lanczos2"},
+ {VS_SCALINGMETHOD_LANCZOS3_FAST, "lanczos3 fast"},
+ {VS_SCALINGMETHOD_LANCZOS3, "lanczos3"},
+ {VS_SCALINGMETHOD_SINC8, "sinc8"},
+ {VS_SCALINGMETHOD_BICUBIC_SOFTWARE, "bicubic software"},
+ {VS_SCALINGMETHOD_LANCZOS_SOFTWARE, "lanczos software"},
+ {VS_SCALINGMETHOD_SINC_SOFTWARE, "sinc software"},
+ {VS_SCALINGMETHOD_VDPAU_HARDWARE, "vdpau"},
+ {VS_SCALINGMETHOD_DXVA_HARDWARE, "dxva"},
+ {VS_SCALINGMETHOD_AUTO, "auto"},
+ {VS_SCALINGMETHOD_SPLINE36_FAST, "spline32 fast"},
+ {VS_SCALINGMETHOD_SPLINE36, "spline32"},
+ });
+
+ static_assert(VS_SCALINGMETHOD_MAX == scalingMethodMap.size(),
+ "scalingMethodMap doesn't match the size of ESCALINGMETHOD, did you forget to "
+ "add/remove a mapping?");
+};
+
+enum ETONEMAPMETHOD
+{
+ VS_TONEMAPMETHOD_OFF = 0,
+ VS_TONEMAPMETHOD_REINHARD = 1,
+ VS_TONEMAPMETHOD_ACES = 2,
+ VS_TONEMAPMETHOD_HABLE = 3,
+ VS_TONEMAPMETHOD_MAX
+};
+
+template<>
+struct fmt::formatter<ETONEMAPMETHOD> : fmt::formatter<std::string_view>
+{
+public:
+ template<typename FormatContext>
+ constexpr auto format(const ETONEMAPMETHOD& tonemapMethod, FormatContext& ctx)
+ {
+ const auto it = tonemapMethodMap.find(tonemapMethod);
+ if (it == tonemapMethodMap.cend())
+ throw std::range_error("no tonemap method string found");
+
+ return fmt::formatter<string_view>::format(it->second, ctx);
+ }
+
+private:
+ static constexpr auto tonemapMethodMap = make_map<ETONEMAPMETHOD, std::string_view>({
+ {VS_TONEMAPMETHOD_OFF, "off"},
+ {VS_TONEMAPMETHOD_REINHARD, "reinhard"},
+ {VS_TONEMAPMETHOD_ACES, "aces"},
+ {VS_TONEMAPMETHOD_HABLE, "hable"},
+ });
+
+ static_assert(VS_TONEMAPMETHOD_MAX == tonemapMethodMap.size(),
+ "tonemapMethodMap doesn't match the size of ETONEMAPMETHOD, did you forget to "
+ "add/remove a mapping?");
+};
+
+enum ViewMode
+{
+ ViewModeNormal = 0,
+ ViewModeZoom,
+ ViewModeStretch4x3,
+ ViewModeWideZoom,
+ ViewModeStretch16x9,
+ ViewModeOriginal,
+ ViewModeCustom,
+ ViewModeStretch16x9Nonlin,
+ ViewModeZoom120Width,
+ ViewModeZoom110Width
+};
+
+class CVideoSettings
+{
+public:
+ CVideoSettings();
+ ~CVideoSettings() = default;
+
+ bool operator!=(const CVideoSettings &right) const;
+
+ EINTERLACEMETHOD m_InterlaceMethod;
+ ESCALINGMETHOD m_ScalingMethod;
+ int m_ViewMode; // current view mode
+ float m_CustomZoomAmount; // custom setting zoom amount
+ float m_CustomPixelRatio; // custom setting pixel ratio
+ float m_CustomVerticalShift; // custom setting vertical shift
+ bool m_CustomNonLinStretch;
+ int m_AudioStream;
+ float m_VolumeAmplification;
+ int m_SubtitleStream;
+ float m_SubtitleDelay;
+ int m_subtitleVerticalPosition{0};
+ bool m_subtitleVerticalPositionSave{false};
+ bool m_SubtitleOn;
+ float m_Brightness;
+ float m_Contrast;
+ float m_Gamma;
+ float m_NoiseReduction;
+ bool m_PostProcess;
+ float m_Sharpness;
+ float m_AudioDelay;
+ int m_ResumeTime;
+ int m_StereoMode;
+ bool m_StereoInvert;
+ int m_VideoStream;
+ ETONEMAPMETHOD m_ToneMapMethod;
+ float m_ToneMapParam;
+ int m_Orientation;
+ int m_CenterMixLevel; // relative to metadata or default
+};
+
+class CCriticalSection;
+class CVideoSettingsLocked
+{
+public:
+ CVideoSettingsLocked(CVideoSettings &vs, CCriticalSection &critSection);
+ virtual ~CVideoSettingsLocked() = default;
+
+ CVideoSettingsLocked(CVideoSettingsLocked const &) = delete;
+ void operator=(CVideoSettingsLocked const &x) = delete;
+
+ void SetSubtitleStream(int stream);
+ void SetSubtitleVisible(bool visible);
+ void SetAudioStream(int stream);
+ void SetVideoStream(int stream);
+ void SetAudioDelay(float delay);
+ void SetSubtitleDelay(float delay);
+
+ /*!
+ * \brief Set the subtitle vertical position,
+ * it depends on current screen resolution
+ * \param value The subtitle position in pixels
+ * \param save If true, the value will be saved to resolution info
+ */
+ void SetSubtitleVerticalPosition(int value, bool save);
+
+ void SetViewMode(int mode, float zoom, float par, float shift, bool stretch);
+ void SetVolumeAmplification(float amp);
+
+protected:
+ CVideoSettings &m_videoSettings;
+ CCriticalSection &m_critSection;
+};
diff --git a/xbmc/cores/paplayer/AudioDecoder.cpp b/xbmc/cores/paplayer/AudioDecoder.cpp
new file mode 100644
index 0000000..03dc907
--- /dev/null
+++ b/xbmc/cores/paplayer/AudioDecoder.cpp
@@ -0,0 +1,401 @@
+/*
+ * 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 "AudioDecoder.h"
+
+#include "CodecFactory.h"
+#include "FileItem.h"
+#include "ICodec.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+
+#include <cmath>
+#include <mutex>
+
+CAudioDecoder::CAudioDecoder()
+{
+ m_codec = NULL;
+ m_rawBuffer = nullptr;
+
+ m_eof = false;
+
+ m_status = STATUS_NO_FILE;
+ m_canPlay = false;
+
+ // output buffer (for transferring data from the Pcm Buffer to the rest of the audio chain)
+ memset(&m_outputBuffer, 0, OUTPUT_SAMPLES * sizeof(float));
+ memset(&m_pcmInputBuffer, 0, INPUT_SIZE * sizeof(unsigned char));
+ memset(&m_inputBuffer, 0, INPUT_SAMPLES * sizeof(float));
+
+ m_rawBufferSize = 0;
+}
+
+CAudioDecoder::~CAudioDecoder()
+{
+ Destroy();
+}
+
+void CAudioDecoder::Destroy()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_status = STATUS_NO_FILE;
+
+ m_pcmBuffer.Destroy();
+
+ if ( m_codec )
+ delete m_codec;
+ m_codec = NULL;
+
+ m_canPlay = false;
+}
+
+bool CAudioDecoder::Create(const CFileItem &file, int64_t seekOffset)
+{
+ Destroy();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // reset our playback timing variables
+ m_eof = false;
+
+ // get correct cache size
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ unsigned int filecache = settings->GetInt(CSettings::SETTING_CACHEAUDIO_INTERNET);
+ if ( file.IsHD() )
+ filecache = settings->GetInt(CSettings::SETTING_CACHE_HARDDISK);
+ else if ( file.IsOnDVD() )
+ filecache = settings->GetInt(CSettings::SETTING_CACHEAUDIO_DVDROM);
+ else if ( file.IsOnLAN() )
+ filecache = settings->GetInt(CSettings::SETTING_CACHEAUDIO_LAN);
+
+ // create our codec
+ m_codec=CodecFactory::CreateCodecDemux(file, filecache * 1024);
+
+ if (!m_codec || !m_codec->Init(file, filecache * 1024))
+ {
+ CLog::Log(LOGERROR, "CAudioDecoder: Unable to Init Codec while loading file {}",
+ file.GetDynPath());
+ Destroy();
+ return false;
+ }
+ unsigned int blockSize = (m_codec->m_bitsPerSample >> 3) * m_codec->m_format.m_channelLayout.Count();
+
+ if (blockSize == 0)
+ {
+ CLog::Log(LOGERROR, "CAudioDecoder: Codec provided invalid parameters ({}-bit, {} channels)",
+ m_codec->m_bitsPerSample, GetFormat().m_channelLayout.Count());
+ return false;
+ }
+
+ /* allocate the pcmBuffer for 2 seconds of audio */
+ m_pcmBuffer.Create(2 * blockSize * m_codec->m_format.m_sampleRate);
+
+ if (file.HasMusicInfoTag())
+ {
+ // set total time from the given tag
+ if (file.GetMusicInfoTag()->GetDuration())
+ m_codec->SetTotalTime(file.GetMusicInfoTag()->GetDuration());
+
+ // update ReplayGain from the given tag if it's better then original (cuesheet)
+ ReplayGain rgInfo = m_codec->m_tag.GetReplayGain();
+ bool anySet = false;
+ if (!rgInfo.Get(ReplayGain::ALBUM).Valid()
+ && file.GetMusicInfoTag()->GetReplayGain().Get(ReplayGain::ALBUM).Valid())
+ {
+ rgInfo.Set(ReplayGain::ALBUM, file.GetMusicInfoTag()->GetReplayGain().Get(ReplayGain::ALBUM));
+ anySet = true;
+ }
+ if (!rgInfo.Get(ReplayGain::TRACK).Valid()
+ && file.GetMusicInfoTag()->GetReplayGain().Get(ReplayGain::TRACK).Valid())
+ {
+ rgInfo.Set(ReplayGain::TRACK, file.GetMusicInfoTag()->GetReplayGain().Get(ReplayGain::TRACK));
+ anySet = true;
+ }
+ if (anySet)
+ m_codec->m_tag.SetReplayGain(rgInfo);
+ }
+
+ if (seekOffset)
+ m_codec->Seek(seekOffset);
+
+ m_status = STATUS_QUEUING;
+
+ m_rawBufferSize = 0;
+
+ return true;
+}
+
+AEAudioFormat CAudioDecoder::GetFormat()
+{
+ AEAudioFormat format;
+ if (!m_codec)
+ return format;
+ return m_codec->m_format;
+}
+
+unsigned int CAudioDecoder::GetChannels()
+{
+ return GetFormat().m_channelLayout.Count();
+}
+
+int64_t CAudioDecoder::Seek(int64_t time)
+{
+ m_pcmBuffer.Clear();
+ m_rawBufferSize = 0;
+ if (!m_codec)
+ return 0;
+ if (time < 0) time = 0;
+ if (time > m_codec->m_TotalTime) time = m_codec->m_TotalTime;
+ return m_codec->Seek(time);
+}
+
+void CAudioDecoder::SetTotalTime(int64_t time)
+{
+ if (m_codec)
+ m_codec->m_TotalTime = time;
+}
+
+int64_t CAudioDecoder::TotalTime()
+{
+ if (m_codec)
+ return m_codec->m_TotalTime;
+ return 0;
+}
+
+unsigned int CAudioDecoder::GetDataSize(bool checkPktSize)
+{
+ if (m_status == STATUS_QUEUING || m_status == STATUS_NO_FILE)
+ return 0;
+
+ if (m_codec->m_format.m_dataFormat != AE_FMT_RAW)
+ {
+ // check for end of file and end of buffer
+ if (m_status == STATUS_ENDING)
+ {
+ if (m_pcmBuffer.getMaxReadSize() == 0)
+ m_status = STATUS_ENDED;
+ else if (checkPktSize && m_pcmBuffer.getMaxReadSize() < PACKET_SIZE)
+ m_status = STATUS_ENDED;
+ }
+ return std::min(m_pcmBuffer.getMaxReadSize() / (m_codec->m_bitsPerSample >> 3), (unsigned int)OUTPUT_SAMPLES);
+ }
+ else
+ {
+ if (m_status == STATUS_ENDING)
+ m_status = STATUS_ENDED;
+ return m_rawBufferSize;
+ }
+}
+
+void *CAudioDecoder::GetData(unsigned int samples)
+{
+ unsigned int size = samples * (m_codec->m_bitsPerSample >> 3);
+ if (size > sizeof(m_outputBuffer))
+ {
+ CLog::Log(LOGERROR, "CAudioDecoder::GetData - More data was requested then we have space to buffer!");
+ return NULL;
+ }
+
+ if (size > m_pcmBuffer.getMaxReadSize())
+ {
+ CLog::Log(
+ LOGWARNING,
+ "CAudioDecoder::GetData() more bytes/samples ({}) requested than we have to give ({})!",
+ size, m_pcmBuffer.getMaxReadSize());
+ size = m_pcmBuffer.getMaxReadSize();
+ }
+
+ if (m_pcmBuffer.ReadData((char *)m_outputBuffer, size))
+ {
+ if (m_status == STATUS_ENDING && m_pcmBuffer.getMaxReadSize() == 0)
+ m_status = STATUS_ENDED;
+
+ return m_outputBuffer;
+ }
+
+ CLog::Log(LOGERROR, "CAudioDecoder::GetData() ReadBinary failed with {} samples", samples);
+ return NULL;
+}
+
+uint8_t *CAudioDecoder::GetRawData(int &size)
+{
+ if (m_status == STATUS_ENDING)
+ m_status = STATUS_ENDED;
+
+ if (m_rawBufferSize)
+ {
+ size = m_rawBufferSize;
+ m_rawBufferSize = 0;
+ return m_rawBuffer;
+ }
+ return nullptr;
+}
+
+int CAudioDecoder::ReadSamples(int numsamples)
+{
+ if (m_status == STATUS_NO_FILE || m_status == STATUS_ENDING || m_status == STATUS_ENDED)
+ return RET_SLEEP; // nothing loaded yet
+
+ // start playing once we're fully queued and we're ready to go
+ if (m_status == STATUS_QUEUED && m_canPlay)
+ m_status = STATUS_PLAYING;
+
+ // grab a lock to ensure the codec is created at this point.
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_codec->m_format.m_dataFormat != AE_FMT_RAW)
+ {
+ // Read in more data
+ int maxsize = std::min<int>(INPUT_SAMPLES, m_pcmBuffer.getMaxWriteSize() / (m_codec->m_bitsPerSample >> 3));
+ numsamples = std::min<int>(numsamples, maxsize);
+ numsamples -= (numsamples % GetFormat().m_channelLayout.Count()); // make sure it's divisible by our number of channels
+ if (numsamples)
+ {
+ size_t readSize = 0;
+ int result = m_codec->ReadPCM(
+ m_pcmInputBuffer, static_cast<size_t>(numsamples * (m_codec->m_bitsPerSample >> 3)),
+ &readSize);
+
+ if (result != READ_ERROR && readSize)
+ {
+ // move it into our buffer
+ m_pcmBuffer.WriteData((char *)m_pcmInputBuffer, readSize);
+
+ // update status
+ if (m_status == STATUS_QUEUING && m_pcmBuffer.getMaxReadSize() > m_pcmBuffer.getSize() * 0.9)
+ {
+ CLog::Log(LOGINFO, "AudioDecoder: File is queued");
+ m_status = STATUS_QUEUED;
+ }
+
+ if (result == READ_EOF) // EOF reached
+ {
+ // setup ending if we're within set time of the end (currently just EOF)
+ m_eof = true;
+ if (m_status < STATUS_ENDING)
+ m_status = STATUS_ENDING;
+ }
+
+ return RET_SUCCESS;
+ }
+ if (result == READ_ERROR)
+ {
+ // error decoding, lets finish up and get out
+ CLog::Log(LOGERROR, "CAudioDecoder: Error while decoding {}", result);
+ return RET_ERROR;
+ }
+ if (result == READ_EOF)
+ {
+ m_eof = true;
+ // setup ending if we're within set time of the end (currently just EOF)
+ if (m_status < STATUS_ENDING)
+ m_status = STATUS_ENDING;
+ }
+ }
+ }
+ else
+ {
+ if (m_rawBufferSize == 0)
+ {
+ int result = m_codec->ReadRaw(&m_rawBuffer, &m_rawBufferSize);
+ if (result == READ_SUCCESS && m_rawBufferSize)
+ {
+ //! @todo trash this useless ringbuffer
+ if (m_status == STATUS_QUEUING)
+ {
+ m_status = STATUS_QUEUED;
+ }
+ return RET_SUCCESS;
+ }
+ else if (result == READ_ERROR)
+ {
+ // error decoding, lets finish up and get out
+ CLog::Log(LOGERROR, "CAudioDecoder: Error while decoding {}", result);
+ return RET_ERROR;
+ }
+ else if (result == READ_EOF)
+ {
+ m_eof = true;
+ // setup ending if we're within set time of the end (currently just EOF)
+ if (m_status < STATUS_ENDING)
+ m_status = STATUS_ENDING;
+ }
+ }
+ }
+ return RET_SLEEP; // nothing to do
+}
+
+bool CAudioDecoder::CanSeek()
+{
+ if (m_codec)
+ return m_codec->CanSeek();
+ else
+ return false;
+}
+
+float CAudioDecoder::GetReplayGain(float &peakVal)
+{
+#define REPLAY_GAIN_DEFAULT_LEVEL 89.0f
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+
+ const auto& replayGainSettings = appVolume->GetReplayGainSettings();
+ if (replayGainSettings.iType == ReplayGain::NONE)
+ return 1.0f;
+
+ // Compute amount of gain
+ float replaydB = (float)replayGainSettings.iNoGainPreAmp;
+ float peak = 1.0f;
+ const ReplayGain& rgInfo = m_codec->m_tag.GetReplayGain();
+ if (replayGainSettings.iType == ReplayGain::ALBUM)
+ {
+ if (rgInfo.Get(ReplayGain::ALBUM).HasGain())
+ {
+ replaydB = (float)replayGainSettings.iPreAmp + rgInfo.Get(ReplayGain::ALBUM).Gain();
+ if (rgInfo.Get(ReplayGain::ALBUM).HasPeak())
+ peak = rgInfo.Get(ReplayGain::ALBUM).Peak();
+ }
+ else if (rgInfo.Get(ReplayGain::TRACK).HasGain())
+ {
+ replaydB = (float)replayGainSettings.iPreAmp + rgInfo.Get(ReplayGain::TRACK).Gain();
+ if (rgInfo.Get(ReplayGain::TRACK).HasPeak())
+ peak = rgInfo.Get(ReplayGain::TRACK).Peak();
+ }
+ }
+ else if (replayGainSettings.iType == ReplayGain::TRACK)
+ {
+ if (rgInfo.Get(ReplayGain::TRACK).HasGain())
+ {
+ replaydB = (float)replayGainSettings.iPreAmp + rgInfo.Get(ReplayGain::TRACK).Gain();
+ if (rgInfo.Get(ReplayGain::TRACK).HasPeak())
+ peak = rgInfo.Get(ReplayGain::TRACK).Peak();
+ }
+ else if (rgInfo.Get(ReplayGain::ALBUM).HasGain())
+ {
+ replaydB = (float)replayGainSettings.iPreAmp + rgInfo.Get(ReplayGain::ALBUM).Gain();
+ if (rgInfo.Get(ReplayGain::ALBUM).HasPeak())
+ peak = rgInfo.Get(ReplayGain::ALBUM).Peak();
+ }
+ }
+ // convert to a gain type
+ float replaygain = std::pow(10.0f, (replaydB - REPLAY_GAIN_DEFAULT_LEVEL) * 0.05f);
+
+ CLog::Log(LOGDEBUG,
+ "AudioDecoder::GetReplayGain - Final Replaygain applied: {:f}, Track/Album Gain {:f}, "
+ "Peak {:f}",
+ replaygain, replaydB, peak);
+
+ peakVal = peak;
+ return replaygain;
+}
+
diff --git a/xbmc/cores/paplayer/AudioDecoder.h b/xbmc/cores/paplayer/AudioDecoder.h
new file mode 100644
index 0000000..9d0eac6
--- /dev/null
+++ b/xbmc/cores/paplayer/AudioDecoder.h
@@ -0,0 +1,91 @@
+/*
+ * 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 "threads/CriticalSection.h"
+#include "utils/RingBuffer.h"
+
+struct AEAudioFormat;
+class CFileItem;
+class ICodec;
+
+#define PACKET_SIZE 3840 // audio packet size - we keep 1 in reserve for gapless playback
+ // using a multiple of 1, 2, 3, 4, 5, 6 to guarantee track alignment
+ // note that 7 or higher channels won't work too well.
+
+#define INPUT_SIZE PACKET_SIZE * 3 // input data size we read from the codecs at a time
+ // * 3 to allow 24 bit audio
+
+#define OUTPUT_SAMPLES PACKET_SIZE // max number of output samples
+#define INPUT_SAMPLES PACKET_SIZE // number of input samples (distributed over channels)
+
+#define STATUS_NO_FILE 0
+#define STATUS_QUEUING 1
+#define STATUS_QUEUED 2
+#define STATUS_PLAYING 3
+#define STATUS_ENDING 4
+#define STATUS_ENDED 5
+
+// return codes from decoders
+#define RET_ERROR -1
+#define RET_SUCCESS 0
+#define RET_SLEEP 1
+
+class CAudioDecoder
+{
+public:
+ CAudioDecoder();
+ ~CAudioDecoder();
+
+ bool Create(const CFileItem &file, int64_t seekOffset);
+ void Destroy();
+
+ int ReadSamples(int numsamples);
+
+ bool CanSeek();
+ int64_t Seek(int64_t time);
+ int64_t TotalTime();
+ void SetTotalTime(int64_t time);
+ void Start() { m_canPlay = true;}; // cause a pre-buffered stream to start.
+ int GetStatus() { return m_status; }
+ void SetStatus(int status) { m_status = status; }
+
+ AEAudioFormat GetFormat();
+ unsigned int GetChannels();
+ // Data management
+ unsigned int GetDataSize(bool checkPktSize);
+ void *GetData(unsigned int samples);
+ uint8_t* GetRawData(int &size);
+ ICodec *GetCodec() const { return m_codec; }
+ float GetReplayGain(float &peakVal);
+
+private:
+ // pcm buffer
+ CRingBuffer m_pcmBuffer;
+
+ // output buffer (for transferring data from the Pcm Buffer to the rest of the audio chain)
+ float m_outputBuffer[OUTPUT_SAMPLES];
+
+ // input buffer (for transferring data from the Codecs to our Pcm Ringbuffer
+ uint8_t m_pcmInputBuffer[INPUT_SIZE];
+ float m_inputBuffer[INPUT_SAMPLES];
+
+ uint8_t *m_rawBuffer;
+ int m_rawBufferSize;
+
+ // status
+ bool m_eof;
+ int m_status;
+ bool m_canPlay;
+
+ // the codec we're using
+ ICodec* m_codec;
+
+ CCriticalSection m_critSection;
+};
diff --git a/xbmc/cores/paplayer/CMakeLists.txt b/xbmc/cores/paplayer/CMakeLists.txt
new file mode 100644
index 0000000..8da2e7c
--- /dev/null
+++ b/xbmc/cores/paplayer/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES AudioDecoder.cpp
+ CodecFactory.cpp
+ PAPlayer.cpp
+ VideoPlayerCodec.cpp)
+
+set(HEADERS AudioDecoder.h
+ CachingCodec.h
+ CodecFactory.h
+ ICodec.h
+ PAPlayer.h
+ VideoPlayerCodec.h)
+
+core_add_library(paplayer)
diff --git a/xbmc/cores/paplayer/CachingCodec.h b/xbmc/cores/paplayer/CachingCodec.h
new file mode 100644
index 0000000..55ac36b
--- /dev/null
+++ b/xbmc/cores/paplayer/CachingCodec.h
@@ -0,0 +1,18 @@
+/*
+ * 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 "ICodec.h"
+
+class CachingCodec : public ICodec
+{
+public:
+ virtual ~CachingCodec() {}
+ virtual int GetCacheLevel() const { return -1; }
+};
diff --git a/xbmc/cores/paplayer/CodecFactory.cpp b/xbmc/cores/paplayer/CodecFactory.cpp
new file mode 100644
index 0000000..70684d8
--- /dev/null
+++ b/xbmc/cores/paplayer/CodecFactory.cpp
@@ -0,0 +1,112 @@
+/*
+ * 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 "CodecFactory.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "VideoPlayerCodec.h"
+#include "addons/AudioDecoder.h"
+#include "addons/ExtsMimeSupportList.h"
+#include "addons/addoninfo/AddonType.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI::ADDONS;
+
+ICodec* CodecFactory::CreateCodec(const CURL& urlFile)
+{
+ std::string fileType = urlFile.GetFileType();
+ StringUtils::ToLower(fileType);
+
+ auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetExtensionSupportedAddonInfos(
+ "." + fileType, CExtsMimeSupportList::FilterSelect::all);
+ for (const auto& addonInfo : addonInfos)
+ {
+ // Check asked and given extension is supported by only for here allowed audiodecoder addons.
+ if (addonInfo.first == ADDON::AddonType::AUDIODECODER)
+ {
+ std::unique_ptr<CAudioDecoder> result = std::make_unique<CAudioDecoder>(addonInfo.second);
+ if (!result->CreateDecoder())
+ continue;
+
+ return result.release();
+ }
+ }
+
+ VideoPlayerCodec *dvdcodec = new VideoPlayerCodec();
+ return dvdcodec;
+}
+
+ICodec* CodecFactory::CreateCodecDemux(const CFileItem& file, unsigned int filecache)
+{
+ CURL urlFile(file.GetDynPath());
+ std::string content = file.GetMimeType();
+ StringUtils::ToLower(content);
+ if (!content.empty())
+ {
+ auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetMimetypeSupportedAddonInfos(
+ content, CExtsMimeSupportList::FilterSelect::all);
+ for (const auto& addonInfo : addonInfos)
+ {
+ // Check asked and given mime type is supported by only for here allowed audiodecoder addons.
+ if (addonInfo.first == ADDON::AddonType::AUDIODECODER)
+ {
+ std::unique_ptr<CAudioDecoder> result = std::make_unique<CAudioDecoder>(addonInfo.second);
+ if (!result->CreateDecoder() && result->SupportsFile(file.GetPath()))
+ continue;
+
+ return result.release();
+ }
+ }
+ }
+
+ if( content == "audio/mpeg" ||
+ content == "audio/mpeg3" ||
+ content == "audio/mp3" ||
+ content == "audio/aac" ||
+ content == "audio/aacp" ||
+ content == "audio/x-ms-wma" ||
+ content == "audio/x-ape" ||
+ content == "audio/ape" ||
+ content == "application/ogg" ||
+ content == "audio/ogg" ||
+ content == "audio/x-xbmc-pcm" ||
+ content == "audio/flac" ||
+ content == "audio/x-flac" ||
+ content == "application/x-flac"
+ )
+ {
+ VideoPlayerCodec *dvdcodec = new VideoPlayerCodec();
+ dvdcodec->SetContentType(content);
+ return dvdcodec;
+ }
+ else if (urlFile.IsProtocol("shout"))
+ {
+ VideoPlayerCodec *dvdcodec = new VideoPlayerCodec();
+ dvdcodec->SetContentType("audio/mp3");
+ return dvdcodec; // if we got this far with internet radio - content-type was wrong. gamble on mp3.
+ }
+ else if (urlFile.IsFileType("wav") ||
+ content == "audio/wav" ||
+ content == "audio/x-wav")
+ {
+ VideoPlayerCodec *dvdcodec = new VideoPlayerCodec();
+ dvdcodec->SetContentType("audio/x-spdif-compressed");
+ if (dvdcodec->Init(file, filecache))
+ {
+ return dvdcodec;
+ }
+
+ dvdcodec = new VideoPlayerCodec();
+ dvdcodec->SetContentType(content);
+ return dvdcodec;
+ }
+ else
+ return CreateCodec(urlFile);
+}
+
diff --git a/xbmc/cores/paplayer/CodecFactory.h b/xbmc/cores/paplayer/CodecFactory.h
new file mode 100644
index 0000000..01a2614
--- /dev/null
+++ b/xbmc/cores/paplayer/CodecFactory.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "ICodec.h"
+
+class CFileItem;
+
+class CodecFactory
+{
+public:
+ CodecFactory() = default;
+ virtual ~CodecFactory() = default;
+ static ICodec* CreateCodec(const CURL& urlFile);
+ static ICodec* CreateCodecDemux(const CFileItem& file, unsigned int filecache);
+};
+
diff --git a/xbmc/cores/paplayer/ICodec.h b/xbmc/cores/paplayer/ICodec.h
new file mode 100644
index 0000000..1b45280
--- /dev/null
+++ b/xbmc/cores/paplayer/ICodec.h
@@ -0,0 +1,82 @@
+/*
+ * 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/Utils/AEAudioFormat.h"
+#include "filesystem/File.h"
+#include "music/tags/MusicInfoTag.h"
+
+#include <string>
+
+#define READ_EOF -1
+#define READ_SUCCESS 0
+#define READ_ERROR 1
+
+class CFileItem;
+
+class ICodec
+{
+public:
+ ICodec()
+ {
+ m_TotalTime = 0;
+ m_bitRate = 0;
+ m_bitsPerSample = 0;
+ m_bitsPerCodedSample = 0;
+ };
+ virtual ~ICodec() = default;
+
+ // Virtual functions that all codecs should implement. Note that these may need
+ // enhancing and or refactoring at a later date. It works currently well for MP3 and
+ // APE codecs.
+
+ // Init(filename)
+ // This routine should handle any initialization necessary. At a minimum it needs to:
+ // 1. Load any dlls and make sure any buffers etc. are allocated.
+ // 2. Load the file (or at least attempt to load it)
+ // 3. Fill in the m_TotalTime, m_SampleRate, m_BitsPerSample and m_Channels parameters.
+ virtual bool Init(const CFileItem &file, unsigned int filecache)=0;
+
+ virtual bool CanSeek() {return true;}
+
+ // Seek()
+ // Should seek to the appropriate time (in ms) in the file, and return the
+ // time to which we managed to seek (in the case where seeking is problematic)
+ // This is used in FFwd/Rewd so can be called very often.
+ virtual bool Seek(int64_t iSeekTime)=0;
+
+ // ReadPCM()
+ // Decodes audio into pBuffer up to size bytes. The actual amount of returned data
+ // is given in actualsize. Returns READ_SUCCESS on success. Returns READ_EOF when
+ // the data has been exhausted, and READ_ERROR on error.
+ virtual int ReadPCM(uint8_t* pBuffer, size_t size, size_t* actualsize) = 0;
+
+ virtual int ReadRaw(uint8_t **pBuffer, int *bufferSize) { return READ_ERROR; }
+
+ // CanInit()
+ // Should return true if the codec can be initialized
+ // eg. check if a dll needed for the codec exists
+ virtual bool CanInit()=0;
+
+ // set the total time - useful when info comes from a preset tag
+ virtual void SetTotalTime(int64_t totaltime) {}
+
+ virtual bool IsCaching() const {return false;}
+ virtual int GetCacheLevel() const {return -1;}
+
+ int64_t m_TotalTime; // time in ms
+ int m_bitRate;
+ int m_bitsPerSample;
+ int m_bitsPerCodedSample;
+ std::string m_CodecName;
+ MUSIC_INFO::CMusicInfoTag m_tag;
+ XFILE::CFile m_file;
+ AEAudioFormat m_format;
+};
+
diff --git a/xbmc/cores/paplayer/PAPlayer.cpp b/xbmc/cores/paplayer/PAPlayer.cpp
new file mode 100644
index 0000000..05bf906
--- /dev/null
+++ b/xbmc/cores/paplayer/PAPlayer.cpp
@@ -0,0 +1,1207 @@
+/*
+ * 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 "PAPlayer.h"
+
+#include "FileItem.h"
+#include "ICodec.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "cores/AudioEngine/Interfaces/AEStream.h"
+#include "cores/AudioEngine/Utils/AEStreamData.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "cores/DataCacheCore.h"
+#include "cores/VideoPlayer/Process/ProcessInfo.h"
+#include "messaging/ApplicationMessenger.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SystemClock.h"
+#include "utils/JobManager.h"
+#include "utils/log.h"
+#include "video/Bookmark.h"
+
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+#define TIME_TO_CACHE_NEXT_FILE 5000 /* 5 seconds before end of song, start caching the next song */
+#define FAST_XFADE_TIME 80 /* 80 milliseconds */
+#define MAX_SKIP_XFADE_TIME 2000 /* max 2 seconds crossfade on track skip */
+
+// PAP: Psycho-acoustic Audio Player
+// Supporting all open audio codec standards.
+// First one being nullsoft's nsv audio decoder format
+
+PAPlayer::PAPlayer(IPlayerCallback& callback) :
+ IPlayer(callback),
+ CThread("PAPlayer"),
+ m_signalSpeedChange(false),
+ m_playbackSpeed(1 ),
+ m_isPlaying(false),
+ m_isPaused(false),
+ m_isFinished(false),
+ m_defaultCrossfadeMS (0),
+ m_upcomingCrossfadeMS(0),
+ m_audioCallback(NULL ),
+ m_jobCounter(0),
+ m_newForcedPlayerTime(-1),
+ m_newForcedTotalTime (-1)
+{
+ memset(&m_playerGUIData, 0, sizeof(m_playerGUIData));
+ m_processInfo.reset(CProcessInfo::CreateInstance());
+ m_processInfo->SetDataCache(&CServiceBroker::GetDataCacheCore());
+}
+
+PAPlayer::~PAPlayer()
+{
+ CloseFile();
+}
+
+void PAPlayer::SoftStart(bool wait/* = false */)
+{
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt)
+ {
+ StreamInfo* si = *itt;
+ if (si->m_fadeOutTriggered)
+ continue;
+
+ si->m_stream->Resume();
+ si->m_stream->FadeVolume(0.0f, 1.0f, FAST_XFADE_TIME);
+ }
+
+ if (wait)
+ {
+ /* wait for them to fade in */
+ lock.unlock();
+ CThread::Sleep(std::chrono::milliseconds(FAST_XFADE_TIME));
+ lock.lock();
+
+ /* be sure they have faded in */
+ while(wait)
+ {
+ wait = false;
+ for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt)
+ {
+ StreamInfo* si = *itt;
+ if (si->m_stream->IsFading())
+ {
+ lock.unlock();
+ wait = true;
+ CThread::Sleep(1ms);
+ lock.lock();
+ break;
+ }
+ }
+ }
+ }
+}
+
+void PAPlayer::SoftStop(bool wait/* = false */, bool close/* = true */)
+{
+ /* fade all the streams out fast for a nice soft stop */
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt)
+ {
+ StreamInfo* si = *itt;
+ if (si->m_stream)
+ si->m_stream->FadeVolume(1.0f, 0.0f, FAST_XFADE_TIME);
+
+ if (close)
+ {
+ si->m_prepareTriggered = true;
+ si->m_playNextTriggered = true;
+ si->m_fadeOutTriggered = true;
+ }
+ }
+
+ /* if we are going to wait for them to finish fading */
+ if(wait)
+ {
+ // fail safe timer, do not wait longer than 1000ms
+ XbmcThreads::EndTime<> timer(1000ms);
+
+ /* wait for them to fade out */
+ lock.unlock();
+ CThread::Sleep(std::chrono::milliseconds(FAST_XFADE_TIME));
+ lock.lock();
+
+ /* be sure they have faded out */
+ while(wait && !CServiceBroker::GetActiveAE()->IsSuspended() && !timer.IsTimePast())
+ {
+ wait = false;
+ for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt)
+ {
+ StreamInfo* si = *itt;
+ if (si->m_stream && si->m_stream->IsFading())
+ {
+ lock.unlock();
+ wait = true;
+ CThread::Sleep(1ms);
+ lock.lock();
+ break;
+ }
+ }
+ }
+
+ /* if we are not closing the streams, pause them */
+ if (!close)
+ {
+ for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt)
+ {
+ StreamInfo* si = *itt;
+ si->m_stream->Pause();
+ }
+ }
+ }
+}
+
+void PAPlayer::CloseAllStreams(bool fade/* = true */)
+{
+ if (!fade)
+ {
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ while (!m_streams.empty())
+ {
+ StreamInfo* si = m_streams.front();
+ m_streams.pop_front();
+
+ if (si->m_stream)
+ {
+ CloseFileCB(*si);
+ si->m_stream.reset();
+ }
+
+ si->m_decoder.Destroy();
+ delete si;
+ }
+
+ while (!m_finishing.empty())
+ {
+ StreamInfo* si = m_finishing.front();
+ m_finishing.pop_front();
+
+ if (si->m_stream)
+ {
+ CloseFileCB(*si);
+ si->m_stream.reset();
+ }
+
+ si->m_decoder.Destroy();
+ delete si;
+ }
+ m_currentStream = nullptr;
+ }
+ else
+ {
+ SoftStop(false, true);
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ m_currentStream = NULL;
+ }
+}
+
+bool PAPlayer::OpenFile(const CFileItem& file, const CPlayerOptions &options)
+{
+ m_defaultCrossfadeMS = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MUSICPLAYER_CROSSFADE) * 1000;
+ m_fullScreen = options.fullscreen;
+
+ if (m_streams.size() > 1 || !m_defaultCrossfadeMS || m_isPaused)
+ {
+ CloseAllStreams(!m_isPaused);
+ StopThread();
+ m_isPaused = false; // Make sure to reset the pause state
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ m_jobCounter++;
+ }
+ CServiceBroker::GetJobManager()->Submit([=]() { QueueNextFileEx(file, false); }, this,
+ CJob::PRIORITY_NORMAL);
+
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ if (m_streams.size() == 2)
+ {
+ //do a short crossfade on trackskip, set to max 2 seconds for these prev/next transitions
+ m_upcomingCrossfadeMS = std::min(m_defaultCrossfadeMS, (unsigned int)MAX_SKIP_XFADE_TIME);
+
+ //start transition to next track
+ StreamInfo* si = m_streams.front();
+ si->m_playNextAtFrame = si->m_framesSent; //start next track at current frame
+ si->m_prepareTriggered = true; //next track is ready to go
+ }
+ lock.unlock();
+
+ if (!IsRunning())
+ Create();
+
+ /* trigger playback start */
+ m_isPlaying = true;
+ m_startEvent.Set();
+
+ // OnPlayBackStarted to be made only once. Callback processing may be slower than player process
+ // so clear signal flag first otherwise async stream processing could also make callback
+ m_signalStarted = false;
+ m_callback.OnPlayBackStarted(file);
+
+ return true;
+}
+
+void PAPlayer::UpdateCrossfadeTime(const CFileItem& file)
+{
+ // we explicitly disable crossfading for audio cds
+ if (file.IsCDDA())
+ m_upcomingCrossfadeMS = 0;
+ else
+ m_upcomingCrossfadeMS = m_defaultCrossfadeMS = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MUSICPLAYER_CROSSFADE) * 1000;
+
+ if (m_upcomingCrossfadeMS)
+ {
+ if (!m_currentStream || (file.HasMusicInfoTag() &&
+ !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICPLAYER_CROSSFADEALBUMTRACKS) &&
+ m_currentStream->m_fileItem->HasMusicInfoTag() &&
+ (m_currentStream->m_fileItem->GetMusicInfoTag()->GetAlbum() != "") &&
+ (m_currentStream->m_fileItem->GetMusicInfoTag()->GetAlbum() ==
+ file.GetMusicInfoTag()->GetAlbum()) &&
+ (m_currentStream->m_fileItem->GetMusicInfoTag()->GetDiscNumber() ==
+ file.GetMusicInfoTag()->GetDiscNumber()) &&
+ (m_currentStream->m_fileItem->GetMusicInfoTag()->GetTrackNumber() ==
+ file.GetMusicInfoTag()->GetTrackNumber() - 1)))
+ {
+ //do not crossfade when playing consecutive albumtracks
+ m_upcomingCrossfadeMS = 0;
+ }
+ }
+}
+
+bool PAPlayer::QueueNextFile(const CFileItem &file)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ m_jobCounter++;
+ }
+ CServiceBroker::GetJobManager()->Submit([this, file]() { QueueNextFileEx(file, true); }, this,
+ CJob::PRIORITY_NORMAL);
+
+ return true;
+}
+
+bool PAPlayer::QueueNextFileEx(const CFileItem &file, bool fadeIn)
+{
+ if (m_currentStream)
+ {
+ // check if we advance a track of a CUE sheet
+ // if this is the case we don't need to open a new stream
+ std::string newURL = file.GetDynURL().GetFileName();
+ std::string oldURL = m_currentStream->m_fileItem->GetDynURL().GetFileName();
+ if (newURL.compare(oldURL) == 0 && file.GetStartOffset() &&
+ file.GetStartOffset() == m_currentStream->m_fileItem->GetEndOffset() && m_currentStream &&
+ m_currentStream->m_prepareTriggered)
+ {
+ m_currentStream->m_nextFileItem.reset(new CFileItem(file));
+ m_upcomingCrossfadeMS = 0;
+ return true;
+ }
+ m_currentStream->m_nextFileItem.reset();
+ }
+
+ StreamInfo *si = new StreamInfo();
+ si->m_fileItem = std::make_unique<CFileItem>(file);
+
+ // Start stream at zero offset
+ si->m_startOffset = 0;
+ //File item start offset defines where in song to resume
+ double starttime = CUtil::ConvertMilliSecsToSecs(si->m_fileItem->GetStartOffset());
+
+ // Music from cuesheet => "item_start" and offset match
+ // Start offset defines where this song starts in file of multiple songs
+ if (si->m_fileItem->HasProperty("item_start") &&
+ (si->m_fileItem->GetProperty("item_start").asInteger() == si->m_fileItem->GetStartOffset()))
+ {
+ // Start stream at offset from cuesheet
+ si->m_startOffset = si->m_fileItem->GetStartOffset();
+ starttime = 0; // No resume point
+ }
+
+ if (!si->m_decoder.Create(file, si->m_startOffset))
+ {
+ CLog::Log(LOGWARNING, "PAPlayer::QueueNextFileEx - Failed to create the decoder");
+
+ // advance playlist
+ AdvancePlaylistOnError(*si->m_fileItem);
+ m_callback.OnQueueNextItem();
+
+ delete si;
+ return false;
+ }
+
+ /* decode until there is data-available */
+ si->m_decoder.Start();
+ while (si->m_decoder.GetDataSize(true) == 0)
+ {
+ int status = si->m_decoder.GetStatus();
+ if (status == STATUS_ENDED ||
+ status == STATUS_NO_FILE ||
+ si->m_decoder.ReadSamples(PACKET_SIZE) == RET_ERROR)
+ {
+ CLog::Log(LOGINFO, "PAPlayer::QueueNextFileEx - Error reading samples");
+
+ si->m_decoder.Destroy();
+ // advance playlist
+ AdvancePlaylistOnError(*si->m_fileItem);
+ m_callback.OnQueueNextItem();
+ delete si;
+ return false;
+ }
+
+ /* yield our time so that the main PAP thread doesn't stall */
+ CThread::Sleep(1ms);
+ }
+
+ // set m_upcomingCrossfadeMS depending on type of file and user settings
+ UpdateCrossfadeTime(*si->m_fileItem);
+
+ /* init the streaminfo struct */
+ si->m_audioFormat = si->m_decoder.GetFormat();
+ // si->m_startOffset already initialized
+ si->m_endOffset = file.GetEndOffset();
+ si->m_bytesPerSample = CAEUtil::DataFormatToBits(si->m_audioFormat.m_dataFormat) >> 3;
+ si->m_bytesPerFrame = si->m_bytesPerSample * si->m_audioFormat.m_channelLayout.Count();
+ si->m_started = false;
+ si->m_finishing = false;
+ si->m_framesSent = 0;
+ si->m_seekNextAtFrame = 0;
+ si->m_seekFrame = -1;
+ si->m_stream = NULL;
+ si->m_volume = (fadeIn && m_upcomingCrossfadeMS) ? 0.0f : 1.0f;
+ si->m_fadeOutTriggered = false;
+ si->m_isSlaved = false;
+
+ si->m_decoderTotal = si->m_decoder.TotalTime();
+ int64_t streamTotalTime = si->m_decoderTotal;
+ if (si->m_endOffset)
+ streamTotalTime = si->m_endOffset - si->m_startOffset;
+
+ // Seek to a resume point
+ if (si->m_fileItem->HasProperty("StartPercent") &&
+ (si->m_fileItem->GetProperty("StartPercent").asDouble() > 0) &&
+ (si->m_fileItem->GetProperty("StartPercent").asDouble() <= 100))
+ {
+ si->m_seekFrame =
+ si->m_audioFormat.m_sampleRate *
+ CUtil::ConvertMilliSecsToSecs(static_cast<int>(+(static_cast<double>(
+ streamTotalTime * (si->m_fileItem->GetProperty("StartPercent").asDouble() / 100.0)))));
+ }
+ else if (starttime > 0)
+ si->m_seekFrame = si->m_audioFormat.m_sampleRate * starttime;
+ else if (si->m_fileItem->HasProperty("audiobook_bookmark"))
+ si->m_seekFrame = si->m_audioFormat.m_sampleRate *
+ CUtil::ConvertMilliSecsToSecs(
+ si->m_fileItem->GetProperty("audiobook_bookmark").asInteger());
+
+ si->m_prepareNextAtFrame = 0;
+ // cd drives don't really like it to be crossfaded or prepared
+ if (!file.IsCDDA())
+ {
+ if (streamTotalTime >= TIME_TO_CACHE_NEXT_FILE + m_defaultCrossfadeMS)
+ si->m_prepareNextAtFrame = (int)((streamTotalTime - TIME_TO_CACHE_NEXT_FILE - m_defaultCrossfadeMS) * si->m_audioFormat.m_sampleRate / 1000.0f);
+ }
+
+ if (m_currentStream && ((m_currentStream->m_audioFormat.m_dataFormat == AE_FMT_RAW) || (si->m_audioFormat.m_dataFormat == AE_FMT_RAW)))
+ {
+ m_currentStream->m_prepareTriggered = false;
+ m_currentStream->m_waitOnDrain = true;
+ m_currentStream->m_prepareNextAtFrame = 0;
+ si->m_decoder.Destroy();
+ delete si;
+ return false;
+ }
+
+ si->m_prepareTriggered = false;
+ si->m_playNextAtFrame = 0;
+ si->m_playNextTriggered = false;
+ si->m_waitOnDrain = false;
+
+ if (!PrepareStream(si))
+ {
+ CLog::Log(LOGINFO, "PAPlayer::QueueNextFileEx - Error preparing stream");
+
+ si->m_decoder.Destroy();
+ // advance playlist
+ AdvancePlaylistOnError(*si->m_fileItem);
+ m_callback.OnQueueNextItem();
+ delete si;
+ return false;
+ }
+
+ /* add the stream to the list */
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ m_streams.push_back(si);
+ //update the current stream to start playing the next track at the correct frame.
+ UpdateStreamInfoPlayNextAtFrame(m_currentStream, m_upcomingCrossfadeMS);
+
+ return true;
+}
+
+void PAPlayer::UpdateStreamInfoPlayNextAtFrame(StreamInfo *si, unsigned int crossFadingTime)
+{
+ // if no crossfading or cue sheet, wait for eof
+ if (si && (crossFadingTime || si->m_endOffset))
+ {
+ int64_t streamTotalTime = si->m_decoder.TotalTime();
+ if (si->m_endOffset)
+ streamTotalTime = si->m_endOffset - si->m_startOffset;
+ if (streamTotalTime < crossFadingTime)
+ si->m_playNextAtFrame = (int)((streamTotalTime / 2) * si->m_audioFormat.m_sampleRate / 1000.0f);
+ else
+ si->m_playNextAtFrame = (int)((streamTotalTime - crossFadingTime) * si->m_audioFormat.m_sampleRate / 1000.0f);
+ }
+}
+
+inline bool PAPlayer::PrepareStream(StreamInfo *si)
+{
+ /* if we have a stream we are already prepared */
+ if (si->m_stream)
+ return true;
+
+ /* get a paused stream */
+ AEAudioFormat format = si->m_audioFormat;
+ si->m_stream = CServiceBroker::GetActiveAE()->MakeStream(
+ format,
+ AESTREAM_PAUSED
+ );
+
+ if (!si->m_stream)
+ {
+ CLog::Log(LOGDEBUG, "PAPlayer::PrepareStream - Failed to get IAEStream");
+ return false;
+ }
+
+ si->m_stream->SetVolume(si->m_volume);
+ float peak = 1.0;
+ float gain = si->m_decoder.GetReplayGain(peak);
+ if (peak * gain <= 1.0f)
+ // No clipping protection needed
+ si->m_stream->SetReplayGain(gain);
+ else if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICPLAYER_REPLAYGAINAVOIDCLIPPING))
+ // Normalise volume reducing replaygain to avoid needing clipping protection, plays file at lower level
+ si->m_stream->SetReplayGain(1.0f / fabs(peak));
+ else
+ // Clipping protection (when enabled in AE) by audio limiting, applied just where needed
+ si->m_stream->SetAmplification(gain);
+
+ /* if its not the first stream and crossfade is not enabled */
+ if (m_currentStream && m_currentStream != si && !m_upcomingCrossfadeMS)
+ {
+ /* slave the stream for gapless */
+ si->m_isSlaved = true;
+ m_currentStream->m_stream->RegisterSlave(si->m_stream.get());
+ }
+
+ /* fill the stream's buffer */
+ while(si->m_stream->IsBuffering())
+ {
+ int status = si->m_decoder.GetStatus();
+ if (status == STATUS_ENDED ||
+ status == STATUS_NO_FILE ||
+ si->m_decoder.ReadSamples(PACKET_SIZE) == RET_ERROR)
+ {
+ CLog::Log(LOGINFO, "PAPlayer::PrepareStream - Stream Finished");
+ break;
+ }
+
+ if (!QueueData(si))
+ break;
+
+ /* yield our time so that the main PAP thread doesn't stall */
+ CThread::Sleep(1ms);
+ }
+
+ CLog::Log(LOGINFO, "PAPlayer::PrepareStream - Ready");
+
+ return true;
+}
+
+bool PAPlayer::CloseFile(bool reopen)
+{
+ if (reopen)
+ CServiceBroker::GetActiveAE()->KeepConfiguration(3000);
+
+ if (!m_isPaused)
+ SoftStop(true, true);
+ CloseAllStreams(false);
+
+ /* wait for the thread to terminate */
+ StopThread(true);//true - wait for end of thread
+
+ // wait for any pending jobs to complete
+ {
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ while (m_jobCounter > 0)
+ {
+ lock.unlock();
+ m_jobEvent.Wait(100ms);
+ lock.lock();
+ }
+ }
+
+ return true;
+}
+
+void PAPlayer::Process()
+{
+ if (!m_startEvent.Wait(100ms))
+ {
+ CLog::Log(LOGDEBUG, "PAPlayer::Process - Failed to receive start event");
+ return;
+ }
+
+ CLog::Log(LOGDEBUG, "PAPlayer::Process - Playback started");
+ while(m_isPlaying && !m_bStop)
+ {
+ /* this needs to happen outside of any locks to prevent deadlocks */
+ if (m_signalSpeedChange)
+ {
+ m_callback.OnPlayBackSpeedChanged(m_playbackSpeed);
+ m_signalSpeedChange = false;
+ }
+
+ double freeBufferTime = 0.0;
+ ProcessStreams(freeBufferTime);
+
+ // if none of our streams wants at least 10ms of data, we sleep
+ if (freeBufferTime < 0.01)
+ {
+ CThread::Sleep(10ms);
+ }
+
+ if (m_newForcedPlayerTime != -1)
+ {
+ if (SetTimeInternal(m_newForcedPlayerTime))
+ {
+ m_newForcedPlayerTime = -1;
+ }
+ }
+
+ if (m_newForcedTotalTime != -1)
+ {
+ if (SetTotalTimeInternal(m_newForcedTotalTime))
+ {
+ m_newForcedTotalTime = -1;
+ }
+ }
+
+ GetTimeInternal(); //update for GUI
+ }
+ m_isPlaying = false;
+}
+
+inline void PAPlayer::ProcessStreams(double &freeBufferTime)
+{
+ std::unique_lock<CCriticalSection> sharedLock(m_streamsLock);
+ if (m_isFinished && m_streams.empty() && m_finishing.empty())
+ {
+ m_isPlaying = false;
+ freeBufferTime = 1.0;
+ return;
+ }
+
+ /* destroy any drained streams */
+ for (auto itt = m_finishing.begin(); itt != m_finishing.end();)
+ {
+ StreamInfo* si = *itt;
+ if (si->m_stream->IsDrained())
+ {
+ itt = m_finishing.erase(itt);
+ CloseFileCB(*si);
+ delete si;
+ CLog::Log(LOGDEBUG, "PAPlayer::ProcessStreams - Stream Freed");
+ }
+ else
+ ++itt;
+ }
+
+ sharedLock.unlock();
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+
+ for(StreamList::iterator itt = m_streams.begin(); itt != m_streams.end(); ++itt)
+ {
+ StreamInfo* si = *itt;
+ if (!m_currentStream && !si->m_started)
+ {
+ m_currentStream = si;
+ UpdateGUIData(si); //update for GUI
+ }
+ /* if the stream is finishing */
+ if ((si->m_playNextTriggered && si->m_stream && !si->m_stream->IsFading()) || !ProcessStream(si, freeBufferTime))
+ {
+ if (!si->m_prepareTriggered)
+ {
+ if (si->m_waitOnDrain)
+ {
+ si->m_stream->Drain(true);
+ si->m_waitOnDrain = false;
+ }
+ si->m_prepareTriggered = true;
+ m_callback.OnQueueNextItem();
+ }
+
+ /* remove the stream */
+ itt = m_streams.erase(itt);
+ /* if its the current stream */
+ if (si == m_currentStream)
+ {
+ /* if it was the last stream */
+ if (itt == m_streams.end())
+ {
+ /* if it didn't trigger the next queue item */
+ if (!si->m_prepareTriggered)
+ {
+ if (si->m_waitOnDrain)
+ {
+ si->m_stream->Drain(true);
+ si->m_waitOnDrain = false;
+ }
+ m_callback.OnQueueNextItem();
+ si->m_prepareTriggered = true;
+ }
+ m_currentStream = NULL;
+ }
+ else
+ {
+ m_currentStream = *itt;
+ UpdateGUIData(*itt); //update for GUI
+ }
+ }
+
+ /* unregister the audio callback */
+ si->m_stream->UnRegisterAudioCallback();
+ si->m_decoder.Destroy();
+ si->m_stream->Drain(false);
+ m_finishing.push_back(si);
+ return;
+ }
+
+ if (!si->m_started)
+ continue;
+
+ // is it time to prepare the next stream?
+ if (si->m_prepareNextAtFrame > 0 && !si->m_prepareTriggered && si->m_framesSent >= si->m_prepareNextAtFrame)
+ {
+ si->m_prepareTriggered = true;
+ m_callback.OnQueueNextItem();
+ }
+
+ // it is time to start playing the next stream?
+ if (si->m_playNextAtFrame > 0 && !si->m_playNextTriggered && !si->m_nextFileItem && si->m_framesSent >= si->m_playNextAtFrame)
+ {
+ if (!si->m_prepareTriggered)
+ {
+ si->m_prepareTriggered = true;
+ m_callback.OnQueueNextItem();
+ }
+
+ if (!m_isFinished)
+ {
+ if (m_upcomingCrossfadeMS)
+ {
+ si->m_stream->FadeVolume(1.0f, 0.0f, m_upcomingCrossfadeMS);
+ si->m_fadeOutTriggered = true;
+ }
+ m_currentStream = NULL;
+
+ /* unregister the audio callback */
+ si->m_stream->UnRegisterAudioCallback();
+ }
+
+ si->m_playNextTriggered = true;
+ }
+ }
+}
+
+inline bool PAPlayer::ProcessStream(StreamInfo *si, double &freeBufferTime)
+{
+ /* if playback needs to start on this stream, do it */
+ if (si == m_currentStream && !si->m_started)
+ {
+ si->m_started = true;
+ si->m_stream->RegisterAudioCallback(m_audioCallback);
+ if (!si->m_isSlaved)
+ si->m_stream->Resume();
+ si->m_stream->FadeVolume(0.0f, 1.0f, m_upcomingCrossfadeMS);
+ if (m_signalStarted)
+ m_callback.OnPlayBackStarted(*si->m_fileItem);
+ m_signalStarted = true;
+ if (m_fullScreen)
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SWITCHTOFULLSCREEN);
+ m_fullScreen = false;
+ }
+ m_callback.OnAVStarted(*si->m_fileItem);
+ }
+
+ /* if we have not started yet and the stream has been primed */
+ unsigned int space = si->m_stream->GetSpace();
+ if (!si->m_started && !space)
+ return true;
+
+ if (!m_playbackSpeed)
+ return true;
+
+ /* see if it is time yet to FF/RW or a direct seek */
+ if (!si->m_playNextTriggered && ((m_playbackSpeed != 1 && si->m_framesSent >= si->m_seekNextAtFrame) || si->m_seekFrame > -1))
+ {
+ int64_t time = (int64_t)0;
+ /* if its a direct seek */
+ if (si->m_seekFrame > -1)
+ {
+ time = (int64_t)((float)si->m_seekFrame / (float)si->m_audioFormat.m_sampleRate * 1000.0f);
+ si->m_framesSent = (int)(si->m_seekFrame - ((float)si->m_startOffset * (float)si->m_audioFormat.m_sampleRate) / 1000.0f);
+ si->m_seekFrame = -1;
+ m_playerGUIData.m_time = time; //update for GUI
+ si->m_seekNextAtFrame = 0;
+ CDataCacheCore::GetInstance().SetPlayTimes(0, time, 0, m_playerGUIData.m_totalTime);
+ }
+ /* if its FF/RW */
+ else
+ {
+ si->m_framesSent += si->m_audioFormat.m_sampleRate * (m_playbackSpeed - 1);
+ si->m_seekNextAtFrame = si->m_framesSent + si->m_audioFormat.m_sampleRate / 2;
+ time = (int64_t)(((float)si->m_framesSent / (float)si->m_audioFormat.m_sampleRate * 1000.0f) + (float)si->m_startOffset);
+ }
+
+ /* if we are seeking back before the start of the track start normal playback */
+ if (time < si->m_startOffset || si->m_framesSent < 0)
+ {
+ time = si->m_startOffset;
+ si->m_framesSent = 0;
+ si->m_seekNextAtFrame = 0;
+ SetSpeed(1);
+ }
+
+ si->m_decoder.Seek(time);
+ }
+
+ int status = si->m_decoder.GetStatus();
+ if (status == STATUS_ENDED ||
+ status == STATUS_NO_FILE ||
+ si->m_decoder.ReadSamples(PACKET_SIZE) == RET_ERROR ||
+ ((si->m_endOffset) && (si->m_framesSent / si->m_audioFormat.m_sampleRate >= (si->m_endOffset - si->m_startOffset) / 1000)))
+ {
+ if (si == m_currentStream && si->m_nextFileItem)
+ {
+ CloseFileCB(*si);
+
+ // update current stream with info of next track
+ si->m_startOffset = si->m_nextFileItem->GetStartOffset();
+ if (si->m_nextFileItem->GetEndOffset())
+ si->m_endOffset = si->m_nextFileItem->GetEndOffset();
+ else
+ si->m_endOffset = 0;
+ si->m_framesSent = 0;
+
+ *si->m_fileItem = *si->m_nextFileItem;
+ si->m_nextFileItem.reset();
+
+ int64_t streamTotalTime = si->m_decoder.TotalTime() - si->m_startOffset;
+ if (si->m_endOffset)
+ streamTotalTime = si->m_endOffset - si->m_startOffset;
+
+ // calculate time when to prepare next stream
+ si->m_prepareNextAtFrame = 0;
+ if (streamTotalTime >= TIME_TO_CACHE_NEXT_FILE + m_defaultCrossfadeMS)
+ si->m_prepareNextAtFrame = (int)((streamTotalTime - TIME_TO_CACHE_NEXT_FILE - m_defaultCrossfadeMS) * si->m_audioFormat.m_sampleRate / 1000.0f);
+
+ si->m_prepareTriggered = false;
+ si->m_playNextAtFrame = 0;
+ si->m_playNextTriggered = false;
+ si->m_seekNextAtFrame = 0;
+
+ //update the current stream to start playing the next track at the correct frame.
+ UpdateStreamInfoPlayNextAtFrame(m_currentStream, m_upcomingCrossfadeMS);
+
+ UpdateGUIData(si);
+ if (m_signalStarted)
+ m_callback.OnPlayBackStarted(*si->m_fileItem);
+ m_signalStarted = true;
+ m_callback.OnAVStarted(*si->m_fileItem);
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "PAPlayer::ProcessStream - Stream Finished");
+ return false;
+ }
+ }
+
+ if (!QueueData(si))
+ return false;
+
+ /* update free buffer time if we are running */
+ if (si->m_started)
+ {
+ if (si->m_stream->IsBuffering())
+ freeBufferTime = 1.0;
+ else
+ {
+ double free_space;
+ if (si->m_audioFormat.m_dataFormat != AE_FMT_RAW)
+ free_space = (double)(si->m_stream->GetSpace() / si->m_bytesPerSample) / si->m_audioFormat.m_sampleRate;
+ else
+ {
+ if (si->m_audioFormat.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD &&
+ !m_processInfo->WantsRawPassthrough())
+ {
+ free_space = static_cast<double>(si->m_stream->GetSpace()) *
+ si->m_audioFormat.m_streamInfo.GetDuration() / 1000 / 2;
+ }
+ else
+ {
+ free_space = static_cast<double>(si->m_stream->GetSpace()) *
+ si->m_audioFormat.m_streamInfo.GetDuration() / 1000;
+ }
+ }
+
+ freeBufferTime = std::max(freeBufferTime , free_space);
+ }
+ }
+
+ return true;
+}
+
+bool PAPlayer::QueueData(StreamInfo *si)
+{
+ unsigned int space = si->m_stream->GetSpace();
+
+ if (si->m_audioFormat.m_dataFormat != AE_FMT_RAW)
+ {
+ unsigned int samples = std::min(si->m_decoder.GetDataSize(false), space / si->m_bytesPerSample);
+ if (!samples)
+ return true;
+
+ // we want complete frames
+ samples -= samples % si->m_audioFormat.m_channelLayout.Count();
+
+ uint8_t* data = (uint8_t*)si->m_decoder.GetData(samples);
+ if (!data)
+ {
+ CLog::Log(LOGERROR, "PAPlayer::QueueData - Failed to get data from the decoder");
+ return false;
+ }
+
+ unsigned int frames = samples/si->m_audioFormat.m_channelLayout.Count();
+ unsigned int added = si->m_stream->AddData(&data, 0, frames, nullptr);
+ si->m_framesSent += added;
+ }
+ else
+ {
+ if (!space)
+ return true;
+
+ int size;
+ uint8_t *data = si->m_decoder.GetRawData(size);
+ if (data && size)
+ {
+ int added = si->m_stream->AddData(&data, 0, size, nullptr);
+ if (added != size)
+ {
+ CLog::Log(LOGERROR, "PAPlayer::QueueData - unknown error");
+ return false;
+ }
+ if (si->m_audioFormat.m_streamInfo.m_type == CAEStreamInfo::STREAM_TYPE_TRUEHD &&
+ !m_processInfo->WantsRawPassthrough())
+ {
+ si->m_framesSent += si->m_audioFormat.m_streamInfo.GetDuration() / 1000 / 2 *
+ si->m_audioFormat.m_streamInfo.m_sampleRate;
+ }
+ else
+ {
+ si->m_framesSent += si->m_audioFormat.m_streamInfo.GetDuration() / 1000 *
+ si->m_audioFormat.m_streamInfo.m_sampleRate;
+ }
+ }
+ }
+
+ const ICodec* codec = si->m_decoder.GetCodec();
+ m_playerGUIData.m_cacheLevel = codec ? codec->GetCacheLevel() : 0; //update for GUI
+
+ return true;
+}
+
+void PAPlayer::OnExit()
+{
+ //@todo signal OnPlayBackError if there was an error on last stream
+ if (m_isFinished && !m_bStop)
+ m_callback.OnPlayBackEnded();
+ else
+ m_callback.OnPlayBackStopped();
+}
+
+void PAPlayer::OnNothingToQueueNotify()
+{
+ m_isFinished = true;
+}
+
+bool PAPlayer::IsPlaying() const
+{
+ return m_isPlaying;
+}
+
+void PAPlayer::Pause()
+{
+ if (m_isPaused)
+ {
+ SetSpeed(1);
+ }
+ else
+ {
+ SetSpeed(0);
+ }
+}
+
+void PAPlayer::SetVolume(float volume)
+{
+
+}
+
+void PAPlayer::SetDynamicRangeCompression(long drc)
+{
+
+}
+
+void PAPlayer::SetSpeed(float speed)
+{
+ m_playbackSpeed = static_cast<int>(speed);
+ CDataCacheCore::GetInstance().SetSpeed(1.0, speed);
+ if (m_playbackSpeed != 0 && m_isPaused)
+ {
+ m_isPaused = false;
+ SoftStart();
+ m_callback.OnPlayBackResumed();
+ }
+ else if (m_playbackSpeed == 0 && !m_isPaused)
+ {
+ m_isPaused = true;
+ SoftStop(true, false);
+ m_callback.OnPlayBackPaused();
+ }
+ m_signalSpeedChange = true;
+}
+
+int64_t PAPlayer::GetTimeInternal()
+{
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ if (!m_currentStream)
+ return 0;
+
+ double time = ((double)m_currentStream->m_framesSent / (double)m_currentStream->m_audioFormat.m_sampleRate);
+ if (m_currentStream->m_stream)
+ time -= m_currentStream->m_stream->GetDelay();
+ time = time * 1000.0;
+
+ m_playerGUIData.m_time = (int64_t)time; //update for GUI
+ CDataCacheCore::GetInstance().SetPlayTimes(0, time, 0, m_playerGUIData.m_totalTime);
+
+ return (int64_t)time;
+}
+
+bool PAPlayer::SetTotalTimeInternal(int64_t time)
+{
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ if (!m_currentStream)
+ {
+ return false;
+ }
+
+ m_currentStream->m_decoder.SetTotalTime(time);
+ UpdateGUIData(m_currentStream);
+
+ return true;
+}
+
+bool PAPlayer::SetTimeInternal(int64_t time)
+{
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ if (!m_currentStream)
+ return false;
+
+ m_currentStream->m_framesSent = time / 1000 * m_currentStream->m_audioFormat.m_sampleRate;
+
+ if (m_currentStream->m_stream)
+ m_currentStream->m_framesSent += m_currentStream->m_stream->GetDelay() * m_currentStream->m_audioFormat.m_sampleRate;
+
+ return true;
+}
+
+void PAPlayer::SetTime(int64_t time)
+{
+ m_newForcedPlayerTime = time;
+}
+
+int64_t PAPlayer::GetTotalTime64()
+{
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ if (!m_currentStream)
+ return 0;
+
+ int64_t total = m_currentStream->m_decoder.TotalTime();
+ if (m_currentStream->m_endOffset)
+ total = m_currentStream->m_endOffset;
+ total -= m_currentStream->m_startOffset;
+ return total;
+}
+
+void PAPlayer::SetTotalTime(int64_t time)
+{
+ m_newForcedTotalTime = time;
+}
+
+int PAPlayer::GetCacheLevel() const
+{
+ return m_playerGUIData.m_cacheLevel;
+}
+
+void PAPlayer::GetAudioStreamInfo(int index, AudioStreamInfo& info) const
+{
+ info.bitrate = m_playerGUIData.m_audioBitrate;
+ info.channels = m_playerGUIData.m_channelCount;
+ info.codecName = m_playerGUIData.m_codec;
+ info.samplerate = m_playerGUIData.m_sampleRate;
+ info.bitspersample = m_playerGUIData.m_bitsPerSample;
+}
+
+bool PAPlayer::CanSeek() const
+{
+ return m_playerGUIData.m_canSeek;
+}
+
+void PAPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride)
+{
+ if (!CanSeek()) return;
+
+ long long seek;
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+ if (advancedSettings->m_musicUseTimeSeeking && m_playerGUIData.m_totalTime > 2 * advancedSettings->m_musicTimeSeekForwardBig)
+ {
+ if (bLargeStep)
+ seek = bPlus ? advancedSettings->m_musicTimeSeekForwardBig : advancedSettings->m_musicTimeSeekBackwardBig;
+ else
+ seek = bPlus ? advancedSettings->m_musicTimeSeekForward : advancedSettings->m_musicTimeSeekBackward;
+ seek *= 1000;
+ seek += m_playerGUIData.m_time;
+ }
+ else
+ {
+ float percent;
+ if (bLargeStep)
+ percent = bPlus ? static_cast<float>(advancedSettings->m_musicPercentSeekForwardBig) : static_cast<float>(advancedSettings->m_musicPercentSeekBackwardBig);
+ else
+ percent = bPlus ? static_cast<float>(advancedSettings->m_musicPercentSeekForward) : static_cast<float>(advancedSettings->m_musicPercentSeekBackward);
+ seek = static_cast<long long>(GetTotalTime64() * (GetPercentage() + percent) / 100);
+ }
+
+ SeekTime(seek);
+}
+
+void PAPlayer::SeekTime(int64_t iTime /*=0*/)
+{
+ if (!CanSeek()) return;
+
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ if (!m_currentStream)
+ return;
+
+ int64_t seekOffset = iTime - GetTimeInternal();
+
+ if (m_playbackSpeed != 1)
+ SetSpeed(1);
+
+ m_currentStream->m_seekFrame = (int)((float)m_currentStream->m_audioFormat.m_sampleRate * ((float)iTime + (float)m_currentStream->m_startOffset) / 1000.0f);
+ m_callback.OnPlayBackSeek(iTime, seekOffset);
+}
+
+void PAPlayer::SeekPercentage(float fPercent /*=0*/)
+{
+ if (fPercent < 0.0f ) fPercent = 0.0f;
+ if (fPercent > 100.0f) fPercent = 100.0f;
+ SeekTime((int64_t)(fPercent * 0.01f * (float)GetTotalTime64()));
+}
+
+float PAPlayer::GetPercentage()
+{
+ if (m_playerGUIData.m_totalTime > 0)
+ return m_playerGUIData.m_time * 100.0f / m_playerGUIData.m_totalTime;
+
+ return 0.0f;
+}
+
+void PAPlayer::UpdateGUIData(StreamInfo *si)
+{
+ /* Store data need by external threads in member
+ * structure to prevent locking conflicts when
+ * data required by GUI and main application
+ */
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+
+ m_playerGUIData.m_sampleRate = si->m_audioFormat.m_sampleRate;
+ m_playerGUIData.m_channelCount = si->m_audioFormat.m_channelLayout.Count();
+ m_playerGUIData.m_canSeek = si->m_decoder.CanSeek();
+
+ const ICodec* codec = si->m_decoder.GetCodec();
+
+ m_playerGUIData.m_audioBitrate = codec ? codec->m_bitRate : 0;
+ strncpy(m_playerGUIData.m_codec,codec ? codec->m_CodecName.c_str() : "",20);
+ m_playerGUIData.m_cacheLevel = codec ? codec->GetCacheLevel() : 0;
+ m_playerGUIData.m_bitsPerSample = (codec && codec->m_bitsPerCodedSample) ? codec->m_bitsPerCodedSample : si->m_bytesPerSample << 3;
+
+ int64_t total = si->m_decoder.TotalTime();
+ if (si->m_endOffset)
+ total = m_currentStream->m_endOffset;
+ total -= m_currentStream->m_startOffset;
+ m_playerGUIData.m_totalTime = total;
+
+ CServiceBroker::GetDataCacheCore().SignalAudioInfoChange();
+}
+
+void PAPlayer::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ std::unique_lock<CCriticalSection> lock(m_streamsLock);
+ m_jobCounter--;
+ m_jobEvent.Set();
+}
+
+void PAPlayer::CloseFileCB(StreamInfo &si)
+{
+ IPlayerCallback *cb = &m_callback;
+ CFileItem fileItem(*si.m_fileItem);
+ CBookmark bookmark;
+ double total = si.m_decoderTotal;
+ if (si.m_endOffset)
+ total = si.m_endOffset;
+ total -= si.m_startOffset;
+ bookmark.totalTimeInSeconds = total / 1000;
+ bookmark.timeInSeconds = (static_cast<double>(si.m_framesSent) /
+ static_cast<double>(si.m_audioFormat.m_sampleRate));
+ bookmark.timeInSeconds -= si.m_stream->GetDelay();
+ bookmark.player = m_name;
+ bookmark.playerState = GetPlayerState();
+ CServiceBroker::GetJobManager()->Submit([=]() { cb->OnPlayerCloseFile(fileItem, bookmark); },
+ CJob::PRIORITY_NORMAL);
+}
+
+void PAPlayer::AdvancePlaylistOnError(CFileItem &fileItem)
+{
+ if (m_signalStarted)
+ m_callback.OnPlayBackStarted(fileItem);
+ m_signalStarted = true;
+ m_callback.OnAVStarted(fileItem);
+}
diff --git a/xbmc/cores/paplayer/PAPlayer.h b/xbmc/cores/paplayer/PAPlayer.h
new file mode 100644
index 0000000..fe2489d
--- /dev/null
+++ b/xbmc/cores/paplayer/PAPlayer.h
@@ -0,0 +1,154 @@
+/*
+ * 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 "AudioDecoder.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "cores/AudioEngine/Interfaces/IAudioCallback.h"
+#include "cores/IPlayer.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+#include "utils/Job.h"
+
+#include <atomic>
+#include <list>
+#include <vector>
+
+class IAEStream;
+class CFileItem;
+class CProcessInfo;
+
+class PAPlayer : public IPlayer, public CThread, public IJobCallback
+{
+friend class CQueueNextFileJob;
+public:
+ explicit PAPlayer(IPlayerCallback& callback);
+ ~PAPlayer() override;
+
+ bool OpenFile(const CFileItem& file, const CPlayerOptions &options) override;
+ bool QueueNextFile(const CFileItem &file) override;
+ void OnNothingToQueueNotify() override;
+ bool CloseFile(bool reopen = false) override;
+ bool IsPlaying() const override;
+ void Pause() override;
+ bool HasVideo() const override { return false; }
+ bool HasAudio() const override { return true; }
+ bool CanSeek() const override;
+ void Seek(bool bPlus = true, bool bLargeStep = false, bool bChapterOverride = false) override;
+ void SeekPercentage(float fPercent = 0.0f) override;
+ void SetVolume(float volume) override;
+ void SetDynamicRangeCompression(long drc) override;
+ void SetSpeed(float speed = 0) override;
+ int GetCacheLevel() const override;
+ void SetTotalTime(int64_t time) override;
+ void GetAudioStreamInfo(int index, AudioStreamInfo& info) const override;
+ void SetTime(int64_t time) override;
+ void SeekTime(int64_t iTime = 0) override;
+ void GetAudioCapabilities(std::vector<int>& audioCaps) const override {}
+
+ int GetAudioStreamCount() const override { return 1; }
+ int GetAudioStream() override { return 0; }
+
+ // implementation of IJobCallback
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ struct
+ {
+ char m_codec[21];
+ int64_t m_time;
+ int64_t m_totalTime;
+ int m_channelCount;
+ int m_bitsPerSample;
+ int m_sampleRate;
+ int m_audioBitrate;
+ int m_cacheLevel;
+ bool m_canSeek;
+ } m_playerGUIData;
+
+protected:
+ // implementation of CThread
+ void OnStartup() override {}
+ void Process() override;
+ void OnExit() override;
+ float GetPercentage();
+
+private:
+ struct StreamInfo
+ {
+ std::unique_ptr<CFileItem> m_fileItem;
+ std::unique_ptr<CFileItem> m_nextFileItem;
+ CAudioDecoder m_decoder; /* the stream decoder */
+ int64_t m_startOffset; /* the stream start offset */
+ int64_t m_endOffset; /* the stream end offset */
+ int64_t m_decoderTotal = 0;
+ AEAudioFormat m_audioFormat;
+ unsigned int m_bytesPerSample; /* number of bytes per audio sample */
+ unsigned int m_bytesPerFrame; /* number of bytes per audio frame */
+
+ bool m_started; /* if playback of this stream has been started */
+ bool m_finishing; /* if this stream is finishing */
+ int m_framesSent; /* number of frames sent to the stream */
+ int m_prepareNextAtFrame; /* when to prepare the next stream */
+ bool m_prepareTriggered; /* if the next stream has been prepared */
+ int m_playNextAtFrame; /* when to start playing the next stream */
+ bool m_playNextTriggered; /* if this stream has started the next one */
+ bool m_fadeOutTriggered; /* if the stream has been told to fade out */
+ int m_seekNextAtFrame; /* the FF/RR sample to seek at */
+ int m_seekFrame; /* the exact position to seek too, -1 for none */
+
+ IAE::StreamPtr m_stream; /* the playback stream */
+ float m_volume; /* the initial volume level to set the stream to on creation */
+
+ bool m_isSlaved; /* true if the stream has been slaved to another */
+ bool m_waitOnDrain; /* wait for stream being drained in AE */
+ };
+
+ typedef std::list<StreamInfo*> StreamList;
+
+ bool m_signalSpeedChange; /* true if OnPlaybackSpeedChange needs to be called */
+ bool m_signalStarted = true;
+ std::atomic_int m_playbackSpeed; /* the playback speed (1 = normal) */
+ bool m_isPlaying;
+ bool m_isPaused;
+ bool m_isFinished; /* if there are no more songs in the queue */
+ bool m_fullScreen;
+ unsigned int m_defaultCrossfadeMS; /* how long the default crossfade is in ms */
+ unsigned int m_upcomingCrossfadeMS; /* how long the upcoming crossfade is in ms */
+ CEvent m_startEvent; /* event for playback start */
+ StreamInfo* m_currentStream = nullptr;
+ IAudioCallback* m_audioCallback; /* the viz audio callback */
+
+ CCriticalSection m_streamsLock; /* lock for the stream list */
+ StreamList m_streams; /* playing streams */
+ StreamList m_finishing; /* finishing streams */
+ int m_jobCounter;
+ CEvent m_jobEvent;
+ int64_t m_newForcedPlayerTime;
+ int64_t m_newForcedTotalTime;
+ std::unique_ptr<CProcessInfo> m_processInfo;
+
+ bool QueueNextFileEx(const CFileItem &file, bool fadeIn);
+ void SoftStart(bool wait = false);
+ void SoftStop(bool wait = false, bool close = true);
+ void CloseAllStreams(bool fade = true);
+ void ProcessStreams(double &freeBufferTime);
+ bool PrepareStream(StreamInfo *si);
+ bool ProcessStream(StreamInfo *si, double &freeBufferTime);
+ bool QueueData(StreamInfo *si);
+ int64_t GetTotalTime64();
+ void UpdateCrossfadeTime(const CFileItem& file);
+ void UpdateStreamInfoPlayNextAtFrame(StreamInfo *si, unsigned int crossFadingTime);
+ void UpdateGUIData(StreamInfo *si);
+ int64_t GetTimeInternal();
+ bool SetTimeInternal(int64_t time);
+ bool SetTotalTimeInternal(int64_t time);
+ void CloseFileCB(StreamInfo &si);
+ void AdvancePlaylistOnError(CFileItem &fileItem);
+};
+
diff --git a/xbmc/cores/paplayer/VideoPlayerCodec.cpp b/xbmc/cores/paplayer/VideoPlayerCodec.cpp
new file mode 100644
index 0000000..a77a27d
--- /dev/null
+++ b/xbmc/cores/paplayer/VideoPlayerCodec.cpp
@@ -0,0 +1,533 @@
+/*
+ * 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 "VideoPlayerCodec.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "cores/AudioEngine/AEResampleFactory.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.h"
+#include "cores/VideoPlayer/DVDDemuxers/DVDFactoryDemuxer.h"
+#include "cores/VideoPlayer/DVDInputStreams/DVDFactoryInputStream.h"
+#include "cores/VideoPlayer/DVDStreamInfo.h"
+#include "music/tags/TagLoaderTagLib.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+VideoPlayerCodec::VideoPlayerCodec() : m_processInfo(CProcessInfo::CreateInstance())
+{
+ m_CodecName = "VideoPlayer";
+}
+
+VideoPlayerCodec::~VideoPlayerCodec()
+{
+ DeInit();
+}
+
+AEAudioFormat VideoPlayerCodec::GetFormat()
+{
+ AEAudioFormat format;
+ if (m_pAudioCodec)
+ {
+ format = m_pAudioCodec->GetFormat();
+ }
+ return format;
+}
+
+void VideoPlayerCodec::SetContentType(const std::string &strContent)
+{
+ m_strContentType = strContent;
+ StringUtils::ToLower(m_strContentType);
+}
+
+void VideoPlayerCodec::SetPassthroughStreamType(CAEStreamInfo::DataType streamType)
+{
+ m_srcFormat.m_streamInfo.m_type = streamType;
+}
+
+bool VideoPlayerCodec::Init(const CFileItem &file, unsigned int filecache)
+{
+ // take precaution if Init()ialized earlier
+ if (m_bInited)
+ {
+ // keep things as is if Init() was done with known strFile
+ if (m_strFileName == file.GetDynPath())
+ return true;
+
+ // got differing filename, so cleanup before starting over
+ DeInit();
+ }
+
+ m_nDecodedLen = 0;
+
+ CFileItem fileitem(file);
+ fileitem.SetMimeType(m_strContentType);
+ fileitem.SetMimeTypeForInternetFile();
+ m_pInputStream = CDVDFactoryInputStream::CreateInputStream(NULL, fileitem);
+ if (!m_pInputStream)
+ {
+ CLog::Log(LOGERROR, "{}: Error creating input stream for {}", __FUNCTION__, file.GetDynPath());
+ return false;
+ }
+
+ //! @todo
+ //! convey CFileItem::ContentLookup() into Open()
+ if (!m_pInputStream->Open())
+ {
+ CLog::Log(LOGERROR, "{}: Error opening file {}", __FUNCTION__, file.GetDynPath());
+ if (m_pInputStream.use_count() > 1)
+ throw std::runtime_error("m_pInputStream reference count is greater than 1");
+ m_pInputStream.reset();
+ return false;
+ }
+
+ m_pDemuxer = NULL;
+
+ try
+ {
+ m_pDemuxer = CDVDFactoryDemuxer::CreateDemuxer(m_pInputStream);
+ if (!m_pDemuxer)
+ {
+ if (m_pInputStream.use_count() > 1)
+ throw std::runtime_error("m_pInputStream reference count is greater than 1");
+ m_pInputStream.reset();
+ CLog::Log(LOGERROR, "{}: Error creating demuxer", __FUNCTION__);
+ return false;
+ }
+ }
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "{}: Exception thrown when opening demuxer", __FUNCTION__);
+ if (m_pDemuxer)
+ {
+ delete m_pDemuxer;
+ m_pDemuxer = NULL;
+ }
+ return false;
+ }
+
+ CDemuxStream* pStream = NULL;
+ m_nAudioStream = -1;
+ int64_t demuxerId = -1;
+ for (auto stream : m_pDemuxer->GetStreams())
+ {
+ if (stream && stream->type == STREAM_AUDIO)
+ {
+ m_nAudioStream = stream->uniqueId;
+ demuxerId = stream->demuxerId;
+ pStream = stream;
+ break;
+ }
+ }
+
+ if (m_nAudioStream == -1)
+ {
+ CLog::Log(LOGERROR, "{}: Could not find audio stream", __FUNCTION__);
+ delete m_pDemuxer;
+ m_pDemuxer = NULL;
+ if (m_pInputStream.use_count() > 1)
+ throw std::runtime_error("m_pInputStream reference count is greater than 1");
+ m_pInputStream.reset();
+ return false;
+ }
+
+ CDVDStreamInfo hint(*pStream, true);
+
+ CAEStreamInfo::DataType ptStreamTye =
+ GetPassthroughStreamType(hint.codec, hint.samplerate, hint.profile);
+ m_pAudioCodec = CDVDFactoryCodec::CreateAudioCodec(hint, *m_processInfo, true, true, ptStreamTye);
+ if (!m_pAudioCodec)
+ {
+ CLog::Log(LOGERROR, "{}: Could not create audio codec", __FUNCTION__);
+ delete m_pDemuxer;
+ m_pDemuxer = NULL;
+ if (m_pInputStream.use_count() > 1)
+ throw std::runtime_error("m_pInputStream reference count is greater than 1");
+ m_pInputStream.reset();
+ return false;
+ }
+
+ // Extract ReplayGain info
+ // tagLoaderTagLib.Load will try to determine tag type by file extension, so set fallback by contentType
+ std::string strFallbackFileExtension = "";
+ if (m_strContentType == "audio/aacp" ||
+ m_strContentType == "audio/aac")
+ strFallbackFileExtension = "m4a";
+ else if (m_strContentType == "audio/x-ms-wma")
+ strFallbackFileExtension = "wma";
+ else if (m_strContentType == "audio/x-ape" ||
+ m_strContentType == "audio/ape")
+ strFallbackFileExtension = "ape";
+ CTagLoaderTagLib tagLoaderTagLib;
+ tagLoaderTagLib.Load(file.GetDynPath(), m_tag, strFallbackFileExtension);
+
+ // we have to decode initial data in order to get channels/samplerate
+ // for sanity - we read no more than 10 packets
+ int nErrors = 0;
+ for (int nPacket = 0;
+ nPacket < 10 && (m_channels == 0 || m_format.m_sampleRate == 0 || m_format.m_frameSize == 0);
+ nPacket++)
+ {
+ uint8_t dummy[256];
+ size_t nSize = 256;
+ if (ReadPCM(dummy, nSize, &nSize) == READ_ERROR)
+ ++nErrors;
+
+ m_srcFormat = m_pAudioCodec->GetFormat();
+ m_format = m_srcFormat;
+ m_channels = m_srcFormat.m_channelLayout.Count();
+ m_bitsPerSample = CAEUtil::DataFormatToBits(m_srcFormat.m_dataFormat);
+ m_bitsPerCodedSample = static_cast<CDemuxStreamAudio*>(pStream)->iBitsPerSample;
+ }
+ if (nErrors >= 10)
+ {
+ CLog::Log(LOGDEBUG, "{}: Could not decode data", __FUNCTION__);
+ return false;
+ }
+
+ // test if seeking is supported
+ m_bCanSeek = false;
+ if (m_pInputStream->Seek(0, SEEK_POSSIBLE))
+ {
+ if (Seek(1))
+ {
+ // rewind stream to beginning
+ Seek(0);
+ m_bCanSeek = true;
+ }
+ else
+ {
+ m_pInputStream->Seek(0, SEEK_SET);
+ if (!m_pDemuxer->Reset())
+ return false;
+ }
+ }
+
+ if (m_channels == 0) // no data - just guess and hope for the best
+ {
+ m_srcFormat.m_channelLayout = CAEChannelInfo(AE_CH_LAYOUT_2_0);
+ m_channels = m_srcFormat.m_channelLayout.Count();
+ }
+
+ if (m_srcFormat.m_sampleRate == 0)
+ m_srcFormat.m_sampleRate = 44100;
+
+ m_TotalTime = m_pDemuxer->GetStreamLength();
+ m_bitRate = m_pAudioCodec->GetBitRate();
+ if (!m_bitRate && m_TotalTime)
+ {
+ m_bitRate = (int)(((m_pInputStream->GetLength()*1000) / m_TotalTime) * 8);
+ }
+ m_CodecName = m_pDemuxer->GetStreamCodecName(demuxerId, m_nAudioStream);
+
+ m_needConvert = false;
+ if (NeedConvert(m_srcFormat.m_dataFormat))
+ {
+ m_needConvert = true;
+ // if we don't know the framesize yet, we will fail when converting
+ if (m_srcFormat.m_frameSize == 0)
+ return false;
+
+ m_pResampler = ActiveAE::CAEResampleFactory::Create();
+
+ SampleConfig dstConfig, srcConfig;
+ dstConfig.channel_layout = CAEUtil::GetAVChannelLayout(m_srcFormat.m_channelLayout);
+ dstConfig.channels = m_channels;
+ dstConfig.sample_rate = m_srcFormat.m_sampleRate;
+ dstConfig.fmt = CAEUtil::GetAVSampleFormat(AE_FMT_FLOAT);
+ dstConfig.bits_per_sample = CAEUtil::DataFormatToUsedBits(AE_FMT_FLOAT);
+ dstConfig.dither_bits = CAEUtil::DataFormatToDitherBits(AE_FMT_FLOAT);
+
+ srcConfig.channel_layout = CAEUtil::GetAVChannelLayout(m_srcFormat.m_channelLayout);
+ srcConfig.channels = m_channels;
+ srcConfig.sample_rate = m_srcFormat.m_sampleRate;
+ srcConfig.fmt = CAEUtil::GetAVSampleFormat(m_srcFormat.m_dataFormat);
+ srcConfig.bits_per_sample = CAEUtil::DataFormatToUsedBits(m_srcFormat.m_dataFormat);
+ srcConfig.dither_bits = CAEUtil::DataFormatToDitherBits(m_srcFormat.m_dataFormat);
+
+ m_pResampler->Init(dstConfig, srcConfig,
+ false,
+ false,
+ M_SQRT1_2,
+ NULL,
+ AE_QUALITY_UNKNOWN,
+ false);
+
+ m_planes = AE_IS_PLANAR(m_srcFormat.m_dataFormat) ? m_channels : 1;
+ m_format = m_srcFormat;
+ m_format.m_dataFormat = AE_FMT_FLOAT;
+ m_bitsPerSample = CAEUtil::DataFormatToBits(m_format.m_dataFormat);
+ }
+
+ m_strFileName = file.GetDynPath();
+ m_bInited = true;
+
+ return true;
+}
+
+void VideoPlayerCodec::DeInit()
+{
+ if (m_pDemuxer != NULL)
+ {
+ delete m_pDemuxer;
+ m_pDemuxer = NULL;
+ }
+
+ if (m_pInputStream.use_count() > 1)
+ throw std::runtime_error("m_pInputStream reference count is greater than 1");
+ m_pInputStream.reset();
+
+ m_pAudioCodec.reset();
+
+ delete m_pResampler;
+ m_pResampler = NULL;
+
+ // cleanup format information
+ m_TotalTime = 0;
+ m_bitsPerSample = 0;
+ m_bitRate = 0;
+ m_channels = 0;
+ m_format.m_dataFormat = AE_FMT_INVALID;
+
+ m_nDecodedLen = 0;
+
+ m_strFileName = "";
+ m_bInited = false;
+}
+
+bool VideoPlayerCodec::Seek(int64_t iSeekTime)
+{
+ // default to announce backwards seek if !m_pPacket to not make FFmpeg
+ // skip mpeg audio frames at playback start
+ bool seekback = true;
+
+ bool ret = m_pDemuxer->SeekTime((int)iSeekTime, seekback);
+ m_pAudioCodec->Reset();
+
+ m_nDecodedLen = 0;
+
+ return ret;
+}
+
+int VideoPlayerCodec::ReadPCM(uint8_t* pBuffer, size_t size, size_t* actualsize)
+{
+ if (m_nDecodedLen > 0)
+ {
+ size_t nLen = (size < m_nDecodedLen) ? size : m_nDecodedLen;
+ *actualsize = nLen;
+ if (m_needConvert)
+ {
+ int samples = *actualsize / (m_bitsPerSample>>3);
+ int frames = samples / m_channels;
+ m_pResampler->Resample(&pBuffer, frames, m_audioFrame.data, frames, 1.0);
+ for (int i=0; i<m_planes; i++)
+ {
+ m_audioFrame.data[i] += frames*m_srcFormat.m_frameSize/m_planes;
+ }
+ }
+ else
+ {
+ memcpy(pBuffer, m_audioFrame.data[0], *actualsize);
+ m_audioFrame.data[0] += (*actualsize);
+ }
+ m_nDecodedLen -= nLen;
+ return READ_SUCCESS;
+ }
+
+ m_nDecodedLen = 0;
+ m_pAudioCodec->GetData(m_audioFrame);
+ int bytes = m_audioFrame.nb_frames * m_audioFrame.framesize;
+
+ if (!bytes)
+ {
+ DemuxPacket* pPacket = nullptr;
+ do
+ {
+ if (pPacket)
+ CDVDDemuxUtils::FreeDemuxPacket(pPacket);
+ pPacket = m_pDemuxer->Read();
+ } while (pPacket && pPacket->iStreamId != m_nAudioStream);
+
+ if (!pPacket)
+ {
+ return READ_EOF;
+ }
+
+ pPacket->pts = DVD_NOPTS_VALUE;
+ pPacket->dts = DVD_NOPTS_VALUE;
+
+ int ret = m_pAudioCodec->AddData(*pPacket);
+ CDVDDemuxUtils::FreeDemuxPacket(pPacket);
+ if (ret < 0)
+ {
+ return READ_ERROR;
+ }
+
+ m_pAudioCodec->GetData(m_audioFrame);
+ bytes = m_audioFrame.nb_frames * m_audioFrame.framesize;
+ }
+
+ m_nDecodedLen = bytes;
+ // scale decoded bytes to destination format
+ if (m_needConvert)
+ m_nDecodedLen *= (m_bitsPerSample>>3) / (m_srcFormat.m_frameSize / m_channels);
+
+ *actualsize = (m_nDecodedLen <= size) ? m_nDecodedLen : size;
+ if (*actualsize > 0)
+ {
+ if (m_needConvert)
+ {
+ int samples = *actualsize / (m_bitsPerSample>>3);
+ int frames = samples / m_channels;
+ m_pResampler->Resample(&pBuffer, frames, m_audioFrame.data, frames, 1.0);
+ for (int i=0; i<m_planes; i++)
+ {
+ m_audioFrame.data[i] += frames*m_srcFormat.m_frameSize/m_planes;
+ }
+ }
+ else
+ {
+ memcpy(pBuffer, m_audioFrame.data[0], *actualsize);
+ m_audioFrame.data[0] += *actualsize;
+ }
+ m_nDecodedLen -= *actualsize;
+ }
+
+ return READ_SUCCESS;
+}
+
+int VideoPlayerCodec::ReadRaw(uint8_t **pBuffer, int *bufferSize)
+{
+ DemuxPacket* pPacket;
+
+ m_nDecodedLen = 0;
+ DVDAudioFrame audioframe;
+
+ m_pAudioCodec->GetData(audioframe);
+ if (audioframe.nb_frames)
+ {
+ return READ_SUCCESS;
+ }
+
+ do
+ {
+ pPacket = m_pDemuxer->Read();
+ } while (pPacket && pPacket->iStreamId != m_nAudioStream);
+
+ if (!pPacket)
+ {
+ return READ_EOF;
+ }
+ pPacket->pts = DVD_NOPTS_VALUE;
+ pPacket->dts = DVD_NOPTS_VALUE;
+ int ret = m_pAudioCodec->AddData(*pPacket);
+ CDVDDemuxUtils::FreeDemuxPacket(pPacket);
+ if (ret < 0)
+ {
+ return READ_ERROR;
+ }
+
+ m_pAudioCodec->GetData(audioframe);
+ if (audioframe.nb_frames)
+ {
+ *bufferSize = audioframe.nb_frames;
+ *pBuffer = audioframe.data[0];
+ }
+ else
+ {
+ *bufferSize = 0;
+ }
+
+ return READ_SUCCESS;
+}
+
+bool VideoPlayerCodec::CanInit()
+{
+ return true;
+}
+
+bool VideoPlayerCodec::CanSeek()
+{
+ return m_bCanSeek;
+}
+
+bool VideoPlayerCodec::NeedConvert(AEDataFormat fmt)
+{
+ if (fmt == AE_FMT_RAW)
+ return false;
+
+ switch(fmt)
+ {
+ case AE_FMT_U8:
+ case AE_FMT_S16NE:
+ case AE_FMT_S32NE:
+ case AE_FMT_FLOAT:
+ case AE_FMT_DOUBLE:
+ return false;
+ default:
+ return true;
+ }
+}
+
+CAEStreamInfo::DataType VideoPlayerCodec::GetPassthroughStreamType(AVCodecID codecId,
+ int samplerate,
+ int profile)
+{
+ AEAudioFormat format;
+ format.m_dataFormat = AE_FMT_RAW;
+ format.m_sampleRate = samplerate;
+ format.m_streamInfo.m_type = CAEStreamInfo::DataType::STREAM_TYPE_NULL;
+ switch (codecId)
+ {
+ case AV_CODEC_ID_AC3:
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_AC3;
+ format.m_streamInfo.m_sampleRate = samplerate;
+ break;
+
+ case AV_CODEC_ID_EAC3:
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_EAC3;
+ format.m_streamInfo.m_sampleRate = samplerate;
+ break;
+
+ case AV_CODEC_ID_DTS:
+ if (profile == FF_PROFILE_DTS_HD_HRA)
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD;
+ else if (profile == FF_PROFILE_DTS_HD_MA)
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD_MA;
+ else
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD_CORE;
+ format.m_streamInfo.m_sampleRate = samplerate;
+ break;
+
+ case AV_CODEC_ID_TRUEHD:
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_TRUEHD;
+ format.m_streamInfo.m_sampleRate = samplerate;
+ break;
+
+ default:
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_NULL;
+ }
+
+ bool supports = CServiceBroker::GetActiveAE()->SupportsRaw(format);
+
+ if (!supports && codecId == AV_CODEC_ID_DTS &&
+ format.m_streamInfo.m_type != CAEStreamInfo::STREAM_TYPE_DTSHD_CORE &&
+ CServiceBroker::GetActiveAE()->UsesDtsCoreFallback())
+ {
+ format.m_streamInfo.m_type = CAEStreamInfo::STREAM_TYPE_DTSHD_CORE;
+ supports = CServiceBroker::GetActiveAE()->SupportsRaw(format);
+ }
+
+ if (supports)
+ return format.m_streamInfo.m_type;
+ else
+ return CAEStreamInfo::DataType::STREAM_TYPE_NULL;
+}
diff --git a/xbmc/cores/paplayer/VideoPlayerCodec.h b/xbmc/cores/paplayer/VideoPlayerCodec.h
new file mode 100644
index 0000000..08a9c2f
--- /dev/null
+++ b/xbmc/cores/paplayer/VideoPlayerCodec.h
@@ -0,0 +1,65 @@
+/*
+ * 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 "ICodec.h"
+#include "cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodec.h"
+#include "cores/VideoPlayer/DVDDemuxers/DVDDemux.h"
+#include "cores/VideoPlayer/DVDInputStreams/DVDInputStream.h"
+
+namespace ActiveAE
+{
+ class IAEResample;
+};
+
+class VideoPlayerCodec : public ICodec
+{
+public:
+ VideoPlayerCodec();
+ ~VideoPlayerCodec() override;
+
+ bool Init(const CFileItem &file, unsigned int filecache) override;
+ bool Seek(int64_t iSeekTime) override;
+ int ReadPCM(uint8_t* pBuffer, size_t size, size_t* actualsize) override;
+ int ReadRaw(uint8_t **pBuffer, int *bufferSize) override;
+ bool CanInit() override;
+ bool CanSeek() override;
+
+ void DeInit();
+ AEAudioFormat GetFormat();
+ void SetContentType(const std::string &strContent);
+
+ bool NeedConvert(AEDataFormat fmt);
+ void SetPassthroughStreamType(CAEStreamInfo::DataType streamType);
+
+private:
+ CAEStreamInfo::DataType GetPassthroughStreamType(AVCodecID codecId, int samplerate, int profile);
+
+ CDVDDemux* m_pDemuxer{nullptr};
+ std::shared_ptr<CDVDInputStream> m_pInputStream;
+ std::unique_ptr<CDVDAudioCodec> m_pAudioCodec;
+
+ std::string m_strContentType;
+ std::string m_strFileName;
+ int m_nAudioStream{-1};
+ size_t m_nDecodedLen{0};
+
+ bool m_bInited{false};
+ bool m_bCanSeek{false};
+
+ ActiveAE::IAEResample* m_pResampler{nullptr};
+ DVDAudioFrame m_audioFrame{};
+ int m_planes{0};
+ bool m_needConvert{false};
+ AEAudioFormat m_srcFormat{};
+ int m_channels{0};
+
+ std::unique_ptr<CProcessInfo> m_processInfo;
+};
+
diff --git a/xbmc/cores/playercorefactory/CMakeLists.txt b/xbmc/cores/playercorefactory/CMakeLists.txt
new file mode 100644
index 0000000..9834ce2
--- /dev/null
+++ b/xbmc/cores/playercorefactory/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES PlayerCoreConfig.cpp
+ PlayerCoreFactory.cpp
+ PlayerSelectionRule.cpp)
+
+set(HEADERS PlayerCoreConfig.h
+ PlayerCoreFactory.h
+ PlayerSelectionRule.h)
+
+core_add_library(playercorefactory)
diff --git a/xbmc/cores/playercorefactory/PlayerCoreConfig.cpp b/xbmc/cores/playercorefactory/PlayerCoreConfig.cpp
new file mode 100644
index 0000000..545062e
--- /dev/null
+++ b/xbmc/cores/playercorefactory/PlayerCoreConfig.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2022 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 "PlayerCoreConfig.h"
+
+#include "cores/ExternalPlayer/ExternalPlayer.h"
+#include "cores/IPlayer.h"
+#include "cores/RetroPlayer/RetroPlayer.h"
+#include "cores/VideoPlayer/VideoPlayer.h"
+#include "cores/paplayer/PAPlayer.h"
+#ifdef HAS_UPNP
+#include "network/upnp/UPnPPlayer.h"
+#endif
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <utility>
+
+CPlayerCoreConfig::CPlayerCoreConfig(std::string name,
+ std::string type,
+ const TiXmlElement* pConfig,
+ const std::string& id /* = "" */)
+ : m_name(std::move(name)), m_id(id), m_type(std::move(type))
+{
+ if (pConfig)
+ {
+ m_config.reset(static_cast<TiXmlElement*>(pConfig->Clone()));
+ const char* sAudio = pConfig->Attribute("audio");
+ const char* sVideo = pConfig->Attribute("video");
+ m_bPlaysAudio = sAudio && StringUtils::CompareNoCase(sAudio, "true") == 0;
+ m_bPlaysVideo = sVideo && StringUtils::CompareNoCase(sVideo, "true") == 0;
+ }
+
+ CLog::Log(LOGDEBUG, "CPlayerCoreConfig::<ctor>: created player {}", m_name);
+}
+
+std::shared_ptr<IPlayer> CPlayerCoreConfig::CreatePlayer(IPlayerCallback& callback) const
+{
+ std::shared_ptr<IPlayer> player;
+
+ if (m_type.compare("video") == 0)
+ {
+ player = std::make_shared<CVideoPlayer>(callback);
+ }
+ else if (m_type.compare("music") == 0)
+ {
+ player = std::make_shared<PAPlayer>(callback);
+ }
+ else if (m_type.compare("game") == 0)
+ {
+ player = std::make_shared<KODI::RETRO::CRetroPlayer>(callback);
+ }
+ else if (m_type.compare("external") == 0)
+ {
+ player = std::make_shared<CExternalPlayer>(callback);
+ }
+
+#if defined(HAS_UPNP)
+ else if (m_type.compare("remote") == 0)
+ {
+ player = std::make_shared<UPNP::CUPnPPlayer>(callback, m_id.c_str());
+ }
+#endif
+ else
+ return nullptr;
+
+ if (!player)
+ return nullptr;
+
+ player->m_name = m_name;
+ player->m_type = m_type;
+
+ if (player->Initialize(m_config.get()))
+ return player;
+
+ return nullptr;
+}
diff --git a/xbmc/cores/playercorefactory/PlayerCoreConfig.h b/xbmc/cores/playercorefactory/PlayerCoreConfig.h
new file mode 100644
index 0000000..09c2f4a
--- /dev/null
+++ b/xbmc/cores/playercorefactory/PlayerCoreConfig.h
@@ -0,0 +1,56 @@
+/*
+ * 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 <memory>
+#include <string>
+
+class IPlayer;
+class IPlayerCallback;
+class TiXmlElement;
+
+class CPlayerCoreConfig
+{
+public:
+ CPlayerCoreConfig(std::string name,
+ std::string type,
+ const TiXmlElement* pConfig,
+ const std::string& id = "");
+
+ ~CPlayerCoreConfig() = default;
+
+ const std::string& GetName() const
+ {
+ return m_name;
+ }
+
+ const std::string& GetId() const
+ {
+ return m_id;
+ }
+
+ bool PlaysAudio() const
+ {
+ return m_bPlaysAudio;
+ }
+
+ bool PlaysVideo() const
+ {
+ return m_bPlaysVideo;
+ }
+
+ std::shared_ptr<IPlayer> CreatePlayer(IPlayerCallback& callback) const;
+
+ std::string m_name;
+ std::string m_id; // uuid for upnp
+ std::string m_type;
+ bool m_bPlaysAudio{false};
+ bool m_bPlaysVideo{false};
+ std::unique_ptr<TiXmlElement> m_config;
+};
diff --git a/xbmc/cores/playercorefactory/PlayerCoreFactory.cpp b/xbmc/cores/playercorefactory/PlayerCoreFactory.cpp
new file mode 100644
index 0000000..3042774
--- /dev/null
+++ b/xbmc/cores/playercorefactory/PlayerCoreFactory.cpp
@@ -0,0 +1,477 @@
+/*
+ * 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 "PlayerCoreFactory.h"
+
+#include "FileItem.h"
+#include "PlayerCoreConfig.h"
+#include "PlayerSelectionRule.h"
+#include "URL.h"
+#include "cores/IPlayerCallback.h"
+#include "cores/VideoPlayer/Interface/InputStreamConstants.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "guilib/LocalizeStrings.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <sstream>
+
+#define PLAYERCOREFACTORY_XML "playercorefactory.xml"
+
+CPlayerCoreFactory::CPlayerCoreFactory(const CProfileManager &profileManager) :
+ m_profileManager(profileManager)
+{
+ m_settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ if (m_settings->IsLoaded())
+ OnSettingsLoaded();
+
+ m_settings->GetSettingsManager()->RegisterSettingsHandler(this);
+}
+
+CPlayerCoreFactory::~CPlayerCoreFactory()
+{
+ m_settings->GetSettingsManager()->UnregisterSettingsHandler(this);
+}
+
+void CPlayerCoreFactory::OnSettingsLoaded()
+{
+ LoadConfiguration("special://xbmc/system/" PLAYERCOREFACTORY_XML, true);
+ LoadConfiguration(m_profileManager.GetUserDataItem(PLAYERCOREFACTORY_XML), false);
+}
+
+std::shared_ptr<IPlayer> CPlayerCoreFactory::CreatePlayer(const std::string& nameId,
+ IPlayerCallback& callback) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ size_t idx = GetPlayerIndex(nameId);
+
+ if (m_vecPlayerConfigs.empty() || idx > m_vecPlayerConfigs.size())
+ return nullptr;
+
+ return m_vecPlayerConfigs[idx]->CreatePlayer(callback);
+}
+
+void CPlayerCoreFactory::GetPlayers(std::vector<std::string>&players) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ players.clear();
+ for (auto& conf : m_vecPlayerConfigs)
+ {
+ if (conf->m_bPlaysAudio || conf->m_bPlaysVideo)
+ players.emplace_back(conf->m_name);
+ }
+}
+
+void CPlayerCoreFactory::GetPlayers(std::vector<std::string>&players, const bool audio, const bool video) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ CLog::Log(LOGDEBUG, "CPlayerCoreFactory::GetPlayers: for video={}, audio={}", video, audio);
+
+ for (auto& conf : m_vecPlayerConfigs)
+ {
+ if (audio == conf->m_bPlaysAudio && video == conf->m_bPlaysVideo)
+ {
+ if (std::find(players.begin(), players.end(), conf->m_name) != players.end())
+ continue;
+
+ CLog::Log(LOGDEBUG, "CPlayerCoreFactory::GetPlayers: adding player: {}", conf->m_name);
+ players.emplace_back(conf->m_name);
+ }
+ }
+}
+
+void CPlayerCoreFactory::GetPlayers(const CFileItem& item, std::vector<std::string>&players) const
+{
+ CURL url(item.GetDynPath());
+
+ CLog::Log(LOGDEBUG, "CPlayerCoreFactory::GetPlayers({})", CURL::GetRedacted(item.GetDynPath()));
+
+ enum class ForcedPlayer
+ {
+ NONE,
+ VIDEO_DEFAULT,
+ AUDIO_DEFAULT
+ };
+
+ ForcedPlayer defaultInputstreamPlayerOverride = ForcedPlayer::NONE;
+
+ // If we are using an inpustream add-on
+ if (!item.GetProperty(STREAM_PROPERTY_INPUTSTREAM).empty())
+ {
+ if (!item.GetProperty(STREAM_PROPERTY_INPUTSTREAM_PLAYER).empty())
+ {
+ const std::string inputstreamPlayerOverride =
+ item.GetProperty(STREAM_PROPERTY_INPUTSTREAM_PLAYER).asString();
+
+ if (inputstreamPlayerOverride == "videodefaultplayer")
+ defaultInputstreamPlayerOverride = ForcedPlayer::VIDEO_DEFAULT;
+ else if ((inputstreamPlayerOverride == "audiodefaultplayer"))
+ defaultInputstreamPlayerOverride = ForcedPlayer::AUDIO_DEFAULT;
+ }
+ }
+
+ std::vector<std::string>validPlayers;
+ GetPlayers(validPlayers);
+
+ // Process rules
+ for (auto& rule : m_vecCoreSelectionRules)
+ rule->GetPlayers(item, validPlayers, players);
+
+ CLog::Log(LOGDEBUG, "CPlayerCoreFactory::GetPlayers: matched {0} rules with players", players.size());
+
+ // Process defaults
+
+ // Set video default player. Check whether it's video first (overrule audio and
+ // game check). Also push these players in case it is NOT audio or game either.
+ //
+ // If an inputstream add-on is used, first check if we have an override to use
+ // "videodefaultplayer"
+ if (defaultInputstreamPlayerOverride == ForcedPlayer::VIDEO_DEFAULT ||
+ (defaultInputstreamPlayerOverride == ForcedPlayer::NONE &&
+ (item.IsVideo() || (!item.IsAudio() && !item.IsGame()))))
+ {
+ int idx = GetPlayerIndex("videodefaultplayer");
+ if (idx > -1)
+ {
+ std::string eVideoDefault = GetPlayerName(idx);
+ CLog::Log(LOGDEBUG, "CPlayerCoreFactory::GetPlayers: adding videodefaultplayer ({})",
+ eVideoDefault);
+ players.push_back(eVideoDefault);
+ }
+ GetPlayers(players, false, true); // Video-only players
+ GetPlayers(players, true, true); // Audio & video players
+ }
+
+ // Set audio default player
+ // Pushback all audio players in case we don't know the type
+ if (defaultInputstreamPlayerOverride == ForcedPlayer::AUDIO_DEFAULT ||
+ (defaultInputstreamPlayerOverride == ForcedPlayer::NONE && item.IsAudio()))
+ {
+ int idx = GetPlayerIndex("audiodefaultplayer");
+ if (idx > -1)
+ {
+ std::string eAudioDefault = GetPlayerName(idx);
+ CLog::Log(LOGDEBUG, "CPlayerCoreFactory::GetPlayers: adding audiodefaultplayer ({})",
+ eAudioDefault);
+ players.push_back(eAudioDefault);
+ }
+ GetPlayers(players, true, false); // Audio-only players
+ GetPlayers(players, true, true); // Audio & video players
+ }
+
+ if (item.IsGame())
+ {
+ CLog::Log(LOGDEBUG, "CPlayerCoreFactory::GetPlayers: adding retroplayer");
+ players.emplace_back("RetroPlayer");
+ }
+
+ CLog::Log(LOGDEBUG, "CPlayerCoreFactory::GetPlayers: added {0} players", players.size());
+}
+
+int CPlayerCoreFactory::GetPlayerIndex(const std::string& strCoreName) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!strCoreName.empty())
+ {
+ // Dereference "*default*player" aliases
+ std::string strRealCoreName;
+ if (StringUtils::EqualsNoCase(strCoreName, "audiodefaultplayer"))
+ strRealCoreName = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioDefaultPlayer;
+ else if (StringUtils::EqualsNoCase(strCoreName, "videodefaultplayer"))
+ strRealCoreName = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoDefaultPlayer;
+ else
+ strRealCoreName = strCoreName;
+
+ for(size_t i = 0; i < m_vecPlayerConfigs.size(); i++)
+ {
+ if (StringUtils::EqualsNoCase(m_vecPlayerConfigs[i]->GetName(), strRealCoreName))
+ return i;
+ }
+ CLog::Log(LOGWARNING, "CPlayerCoreFactory::GetPlayer({}): no such player: {}", strCoreName,
+ strRealCoreName);
+ }
+ return -1;
+}
+
+std::string CPlayerCoreFactory::GetPlayerName(size_t idx) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_vecPlayerConfigs.empty() || idx > m_vecPlayerConfigs.size())
+ return "";
+
+ return m_vecPlayerConfigs[idx]->m_name;
+}
+
+void CPlayerCoreFactory::GetPlayers(std::vector<std::string>&players, std::string &type) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for (auto& config : m_vecPlayerConfigs)
+ {
+ if (config->m_type != type)
+ continue;
+ players.emplace_back(config->m_name);
+ }
+}
+
+void CPlayerCoreFactory::GetRemotePlayers(std::vector<std::string>&players) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for (auto& config : m_vecPlayerConfigs)
+ {
+ if (config->m_type != "remote")
+ continue;
+ players.emplace_back(config->m_name);
+ }
+}
+
+std::string CPlayerCoreFactory::GetPlayerType(const std::string& player) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ size_t idx = GetPlayerIndex(player);
+
+ if (m_vecPlayerConfigs.empty() || idx > m_vecPlayerConfigs.size())
+ return "";
+
+ return m_vecPlayerConfigs[idx]->m_type;
+}
+
+bool CPlayerCoreFactory::PlaysAudio(const std::string& player) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ size_t idx = GetPlayerIndex(player);
+
+ if (m_vecPlayerConfigs.empty() || idx > m_vecPlayerConfigs.size())
+ return false;
+
+ return m_vecPlayerConfigs[idx]->m_bPlaysAudio;
+}
+
+bool CPlayerCoreFactory::PlaysVideo(const std::string& player) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ size_t idx = GetPlayerIndex(player);
+
+ if (m_vecPlayerConfigs.empty() || idx > m_vecPlayerConfigs.size())
+ return false;
+
+ return m_vecPlayerConfigs[idx]->m_bPlaysVideo;
+}
+
+std::string CPlayerCoreFactory::GetDefaultPlayer(const CFileItem& item) const
+{
+ std::vector<std::string>players;
+ GetPlayers(item, players);
+
+ //If we have any players return the first one
+ if (!players.empty())
+ return players.at(0);
+
+ return "";
+}
+
+std::string CPlayerCoreFactory::SelectPlayerDialog(const std::vector<std::string>&players, float posX, float posY) const
+{
+ CContextButtons choices;
+ if (players.size())
+ {
+ //Add default player
+ std::string strCaption = players[0];
+ strCaption += " (";
+ strCaption += g_localizeStrings.Get(13278);
+ strCaption += ")";
+ choices.Add(0, strCaption);
+
+ //Add all other players
+ for (unsigned int i = 1; i < players.size(); i++)
+ choices.Add(i, players[i]);
+
+ int choice = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (choice >= 0)
+ return players[choice];
+ }
+ return "";
+}
+
+std::string CPlayerCoreFactory::SelectPlayerDialog(float posX, float posY) const
+{
+ std::vector<std::string>players;
+ GetPlayers(players);
+ return SelectPlayerDialog(players, posX, posY);
+}
+
+bool CPlayerCoreFactory::LoadConfiguration(const std::string &file, bool clear)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ CLog::Log(LOGINFO, "Loading player core factory settings from {}.", file);
+ if (!CFileUtils::Exists(file))
+ { // tell the user it doesn't exist
+ CLog::Log(LOGINFO, "{} does not exist. Skipping.", file);
+ return false;
+ }
+
+ CXBMCTinyXML playerCoreFactoryXML;
+ if (!playerCoreFactoryXML.LoadFile(file))
+ {
+ CLog::Log(LOGERROR, "Error loading {}, Line {} ({})", file, playerCoreFactoryXML.ErrorRow(),
+ playerCoreFactoryXML.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement *pConfig = playerCoreFactoryXML.RootElement();
+ if (pConfig == NULL)
+ {
+ CLog::Log(LOGERROR, "Error loading {}, Bad structure", file);
+ return false;
+ }
+
+ if (clear)
+ {
+ m_vecPlayerConfigs.clear();
+ m_vecCoreSelectionRules.clear();
+
+ // Builtin players
+ auto VideoPlayer = std::make_unique<CPlayerCoreConfig>("VideoPlayer", "video", nullptr);
+ VideoPlayer->m_bPlaysAudio = true;
+ VideoPlayer->m_bPlaysVideo = true;
+ m_vecPlayerConfigs.emplace_back(std::move(VideoPlayer));
+
+ auto paplayer = std::make_unique<CPlayerCoreConfig>("PAPlayer", "music", nullptr);
+ paplayer->m_bPlaysAudio = true;
+ m_vecPlayerConfigs.emplace_back(std::move(paplayer));
+
+ auto retroPlayer = std::make_unique<CPlayerCoreConfig>("RetroPlayer", "game", nullptr);
+ m_vecPlayerConfigs.emplace_back(std::move(retroPlayer));
+ }
+
+ if (StringUtils::CompareNoCase(pConfig->Value(), "playercorefactory") != 0)
+ {
+ CLog::Log(LOGERROR, "Error loading configuration, no <playercorefactory> node");
+ return false;
+ }
+
+ TiXmlElement *pPlayers = pConfig->FirstChildElement("players");
+ if (pPlayers)
+ {
+ TiXmlElement* pPlayer = pPlayers->FirstChildElement("player");
+ while (pPlayer)
+ {
+ std::string name = XMLUtils::GetAttribute(pPlayer, "name");
+ std::string type = XMLUtils::GetAttribute(pPlayer, "type");
+ if (type.empty()) type = name;
+ StringUtils::ToLower(type);
+
+ std::string internaltype;
+ if (type == "videoplayer")
+ internaltype = "video";
+ else if (type == "paplayer")
+ internaltype = "music";
+ else if (type == "externalplayer")
+ internaltype = "external";
+
+ int count = 0;
+ std::string playername = name;
+ while (GetPlayerIndex(playername) >= 0)
+ {
+ count++;
+ std::stringstream itoa;
+ itoa << count;
+ playername = name + itoa.str();
+ }
+
+ if (!internaltype.empty())
+ {
+ m_vecPlayerConfigs.emplace_back(
+ std::make_unique<CPlayerCoreConfig>(playername, internaltype, pPlayer));
+ }
+
+ pPlayer = pPlayer->NextSiblingElement("player");
+ }
+ }
+
+ TiXmlElement *pRule = pConfig->FirstChildElement("rules");
+ while (pRule)
+ {
+ const char* szAction = pRule->Attribute("action");
+ if (szAction)
+ {
+ if (StringUtils::CompareNoCase(szAction, "append") == 0)
+ {
+ m_vecCoreSelectionRules.emplace_back(std::make_unique<CPlayerSelectionRule>(pRule));
+ }
+ else if (StringUtils::CompareNoCase(szAction, "prepend") == 0)
+ {
+ m_vecCoreSelectionRules.emplace_front(std::make_unique<CPlayerSelectionRule>(pRule));
+ }
+ else
+ {
+ m_vecCoreSelectionRules.clear();
+ m_vecCoreSelectionRules.emplace_back(std::make_unique<CPlayerSelectionRule>(pRule));
+ }
+ }
+ else
+ {
+ m_vecCoreSelectionRules.emplace_back(std::make_unique<CPlayerSelectionRule>(pRule));
+ }
+
+ pRule = pRule->NextSiblingElement("rules");
+ }
+
+ // succeeded - tell the user it worked
+ CLog::Log(LOGINFO, "Loaded playercorefactory configuration");
+
+ return true;
+}
+
+void CPlayerCoreFactory::OnPlayerDiscovered(const std::string& id, const std::string& name)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for (auto& playerConfig : m_vecPlayerConfigs)
+ {
+ if (playerConfig->GetId() == id)
+ {
+ playerConfig->m_name = name;
+ playerConfig->m_type = "remote";
+ return;
+ }
+ }
+
+ int count = 0;
+ std::string playername = name;
+ while (GetPlayerIndex(playername) >= 0)
+ {
+ count++;
+ std::stringstream itoa;
+ itoa << count;
+ playername = name + itoa.str();
+ }
+
+ auto player = std::make_unique<CPlayerCoreConfig>(playername, "remote", nullptr, id);
+ player->m_bPlaysAudio = true;
+ player->m_bPlaysVideo = true;
+ m_vecPlayerConfigs.emplace_back(std::move(player));
+}
+
+void CPlayerCoreFactory::OnPlayerRemoved(const std::string& id)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for (auto& playerConfig : m_vecPlayerConfigs)
+ {
+ if (playerConfig->GetId() == id)
+ playerConfig->m_type = "";
+ }
+}
diff --git a/xbmc/cores/playercorefactory/PlayerCoreFactory.h b/xbmc/cores/playercorefactory/PlayerCoreFactory.h
new file mode 100644
index 0000000..e2963e8
--- /dev/null
+++ b/xbmc/cores/playercorefactory/PlayerCoreFactory.h
@@ -0,0 +1,69 @@
+/*
+ * 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 "settings/lib/ISettingsHandler.h"
+#include "threads/CriticalSection.h"
+
+#include <list>
+#include <memory>
+#include <string>
+#include <vector>
+
+// forward references
+
+class TiXmlElement;
+class CFileItem;
+class CPlayerCoreConfig;
+class CPlayerSelectionRule;
+class CProfileManager;
+class CSettings;
+class IPlayer;
+class IPlayerCallback;
+
+class CPlayerCoreFactory : public ISettingsHandler
+{
+public:
+ CPlayerCoreFactory(const CProfileManager &profileManager);
+ CPlayerCoreFactory(const CPlayerCoreFactory&) = delete;
+ CPlayerCoreFactory& operator=(CPlayerCoreFactory const&) = delete;
+ ~CPlayerCoreFactory() override;
+
+ void OnSettingsLoaded() override;
+
+ std::shared_ptr<IPlayer> CreatePlayer(const std::string& nameId, IPlayerCallback& callback) const;
+ void GetPlayers(const CFileItem& item, std::vector<std::string>&players) const; //Players supporting the specified file
+ void GetPlayers(std::vector<std::string>&players, bool audio, bool video) const; //All audio players and/or video players
+ void GetPlayers(std::vector<std::string>&players) const; //All players
+ void GetPlayers(std::vector<std::string>&players, std::string &type) const;
+ void GetRemotePlayers(std::vector<std::string>&players) const; //All remote players we can attach to
+ std::string GetPlayerType(const std::string &player) const;
+ bool PlaysAudio(const std::string &player) const;
+ bool PlaysVideo(const std::string &player) const;
+
+ std::string GetDefaultPlayer(const CFileItem& item) const;
+ std::string SelectPlayerDialog(const std::vector<std::string>&players, float posX = 0, float posY = 0) const;
+ std::string SelectPlayerDialog(float posX, float posY) const;
+ void OnPlayerDiscovered(const std::string& id, const std::string& name);
+ void OnPlayerRemoved(const std::string& id);
+
+private:
+ // Construction parameters
+ std::shared_ptr<CSettings> m_settings;
+ const CProfileManager &m_profileManager;
+
+ int GetPlayerIndex(const std::string& strCoreName) const;
+ std::string GetPlayerName(size_t idx) const;
+
+ bool LoadConfiguration(const std::string &file, bool clear);
+
+ std::vector<std::unique_ptr<CPlayerCoreConfig>> m_vecPlayerConfigs;
+ std::list<std::unique_ptr<CPlayerSelectionRule>> m_vecCoreSelectionRules;
+ mutable CCriticalSection m_section;
+};
diff --git a/xbmc/cores/playercorefactory/PlayerSelectionRule.cpp b/xbmc/cores/playercorefactory/PlayerSelectionRule.cpp
new file mode 100644
index 0000000..cad7399
--- /dev/null
+++ b/xbmc/cores/playercorefactory/PlayerSelectionRule.cpp
@@ -0,0 +1,198 @@
+/*
+ * 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 "PlayerSelectionRule.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/RegExp.h"
+#include "utils/StreamDetails.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <algorithm>
+
+CPlayerSelectionRule::CPlayerSelectionRule(TiXmlElement* pRule)
+{
+ Initialize(pRule);
+}
+
+void CPlayerSelectionRule::Initialize(TiXmlElement* pRule)
+{
+ m_name = XMLUtils::GetAttribute(pRule, "name");
+ if (m_name.empty())
+ m_name = "un-named";
+
+ CLog::Log(LOGDEBUG, "CPlayerSelectionRule::Initialize: creating rule: {}", m_name);
+
+ m_tInternetStream = GetTristate(pRule->Attribute("internetstream"));
+ m_tRemote = GetTristate(pRule->Attribute("remote"));
+ m_tAudio = GetTristate(pRule->Attribute("audio"));
+ m_tVideo = GetTristate(pRule->Attribute("video"));
+ m_tGame = GetTristate(pRule->Attribute("game"));
+
+ m_tBD = GetTristate(pRule->Attribute("bd"));
+ m_tDVD = GetTristate(pRule->Attribute("dvd"));
+ m_tDVDFile = GetTristate(pRule->Attribute("dvdfile"));
+ m_tDiscImage = GetTristate(pRule->Attribute("discimage"));
+ if (m_tDiscImage < 0)
+ {
+ m_tDiscImage = GetTristate(pRule->Attribute("dvdimage"));
+ if (m_tDiscImage >= 0)
+ CLog::Log(LOGWARNING, "\"dvdimage\" tag is deprecated. use \"discimage\"");
+ }
+
+ m_protocols = XMLUtils::GetAttribute(pRule, "protocols");
+ m_fileTypes = XMLUtils::GetAttribute(pRule, "filetypes");
+ m_mimeTypes = XMLUtils::GetAttribute(pRule, "mimetypes");
+ m_fileName = XMLUtils::GetAttribute(pRule, "filename");
+
+ m_audioCodec = XMLUtils::GetAttribute(pRule, "audiocodec");
+ m_audioChannels = XMLUtils::GetAttribute(pRule, "audiochannels");
+ m_videoCodec = XMLUtils::GetAttribute(pRule, "videocodec");
+ m_videoResolution = XMLUtils::GetAttribute(pRule, "videoresolution");
+ m_videoAspect = XMLUtils::GetAttribute(pRule, "videoaspect");
+
+ m_bStreamDetails = m_audioCodec.length() > 0 || m_audioChannels.length() > 0 ||
+ m_videoCodec.length() > 0 || m_videoResolution.length() > 0 || m_videoAspect.length() > 0;
+
+ if (m_bStreamDetails && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS))
+ {
+ CLog::Log(LOGWARNING,
+ "CPlayerSelectionRule::Initialize: rule: {} needs media flagging, which is disabled",
+ m_name);
+ }
+
+ m_playerName = XMLUtils::GetAttribute(pRule, "player");
+
+ TiXmlElement* pSubRule = pRule->FirstChildElement("rule");
+ while (pSubRule)
+ {
+ vecSubRules.emplace_back(std::make_unique<CPlayerSelectionRule>(pSubRule));
+ pSubRule = pSubRule->NextSiblingElement("rule");
+ }
+}
+
+int CPlayerSelectionRule::GetTristate(const char* szValue)
+{
+ if (szValue)
+ {
+ if (StringUtils::CompareNoCase(szValue, "true") == 0)
+ return 1;
+ if (StringUtils::CompareNoCase(szValue, "false") == 0)
+ return 0;
+ }
+ return -1;
+}
+
+bool CPlayerSelectionRule::CompileRegExp(const std::string& str, CRegExp& regExp)
+{
+ return !str.empty() && regExp.RegComp(str.c_str());
+}
+
+bool CPlayerSelectionRule::MatchesRegExp(const std::string& str, CRegExp& regExp)
+{
+ return regExp.RegFind(str, 0) == 0;
+}
+
+void CPlayerSelectionRule::GetPlayers(const CFileItem& item, std::vector<std::string>&validPlayers, std::vector<std::string>&players)
+{
+ CLog::Log(LOGDEBUG, "CPlayerSelectionRule::GetPlayers: considering rule: {}", m_name);
+
+ if (m_bStreamDetails && !item.HasVideoInfoTag())
+ return;
+ if (m_tAudio >= 0 && (m_tAudio > 0) != item.IsAudio())
+ return;
+ if (m_tVideo >= 0 && (m_tVideo > 0) != item.IsVideo())
+ return;
+ if (m_tGame >= 0 && (m_tGame > 0) != item.IsGame())
+ return;
+ if (m_tInternetStream >= 0 && (m_tInternetStream > 0) != item.IsInternetStream())
+ return;
+ if (m_tRemote >= 0 && (m_tRemote > 0) != item.IsRemote())
+ return;
+
+ if (m_tBD >= 0 && (m_tBD > 0) != (item.IsBDFile() && item.IsOnDVD()))
+ return;
+ if (m_tDVD >= 0 && (m_tDVD > 0) != item.IsDVD())
+ return;
+ if (m_tDVDFile >= 0 && (m_tDVDFile > 0) != item.IsDVDFile())
+ return;
+ if (m_tDiscImage >= 0 && (m_tDiscImage > 0) != item.IsDiscImage())
+ return;
+
+ CRegExp regExp(false, CRegExp::autoUtf8);
+
+ if (m_bStreamDetails)
+ {
+ if (!item.GetVideoInfoTag()->HasStreamDetails())
+ {
+ CLog::Log(LOGDEBUG,
+ "CPlayerSelectionRule::GetPlayers: cannot check rule: {}, no StreamDetails",
+ m_name);
+ return;
+ }
+
+ CStreamDetails streamDetails = item.GetVideoInfoTag()->m_streamDetails;
+
+ if (CompileRegExp(m_audioCodec, regExp) && !MatchesRegExp(streamDetails.GetAudioCodec(), regExp))
+ return;
+
+ std::stringstream itoa;
+ itoa << streamDetails.GetAudioChannels();
+ std::string audioChannelsstr = itoa.str();
+
+ if (CompileRegExp(m_audioChannels, regExp) && !MatchesRegExp(audioChannelsstr, regExp))
+ return;
+
+ if (CompileRegExp(m_videoCodec, regExp) && !MatchesRegExp(streamDetails.GetVideoCodec(), regExp))
+ return;
+
+ if (CompileRegExp(m_videoResolution, regExp) &&
+ !MatchesRegExp(CStreamDetails::VideoDimsToResolutionDescription(streamDetails.GetVideoWidth(), streamDetails.GetVideoHeight()), regExp))
+ return;
+
+ if (CompileRegExp(m_videoAspect, regExp) &&
+ !MatchesRegExp(CStreamDetails::VideoAspectToAspectDescription(streamDetails.GetVideoAspect()), regExp))
+ return;
+ }
+
+ CURL url(item.GetDynPath());
+
+ if (CompileRegExp(m_fileTypes, regExp) && !MatchesRegExp(url.GetFileType(), regExp))
+ return;
+
+ if (CompileRegExp(m_protocols, regExp) && !MatchesRegExp(url.GetProtocol(), regExp))
+ return;
+
+ if (CompileRegExp(m_mimeTypes, regExp) && !MatchesRegExp(item.GetMimeType(), regExp))
+ return;
+
+ if (CompileRegExp(m_fileName, regExp) && !MatchesRegExp(item.GetDynPath(), regExp))
+ return;
+
+ CLog::Log(LOGDEBUG, "CPlayerSelectionRule::GetPlayers: matches rule: {}", m_name);
+
+ for (const auto& rule : vecSubRules)
+ rule->GetPlayers(item, validPlayers, players);
+
+ if (std::find(validPlayers.begin(), validPlayers.end(), m_playerName) != validPlayers.end())
+ {
+ CLog::Log(LOGDEBUG, "CPlayerSelectionRule::GetPlayers: adding player: {} for rule: {}",
+ m_playerName, m_name);
+ players.push_back(m_playerName);
+ }
+}
+
+
diff --git a/xbmc/cores/playercorefactory/PlayerSelectionRule.h b/xbmc/cores/playercorefactory/PlayerSelectionRule.h
new file mode 100644
index 0000000..b818d8c
--- /dev/null
+++ b/xbmc/cores/playercorefactory/PlayerSelectionRule.h
@@ -0,0 +1,62 @@
+/*
+ * 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 "PlayerCoreFactory.h"
+
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CRegExp;
+class TiXmlElement;
+
+class CPlayerSelectionRule
+{
+public:
+ explicit CPlayerSelectionRule(TiXmlElement* rule);
+ virtual ~CPlayerSelectionRule() = default;
+
+ void GetPlayers(const CFileItem& item, std::vector<std::string>&validPlayers, std::vector<std::string>&players);
+
+private:
+ static int GetTristate(const char* szValue);
+ static bool CompileRegExp(const std::string& str, CRegExp& regExp);
+ static bool MatchesRegExp(const std::string& str, CRegExp& regExp);
+ void Initialize(TiXmlElement* pRule);
+
+ std::string m_name;
+
+ int m_tAudio;
+ int m_tVideo;
+ int m_tGame;
+ int m_tInternetStream;
+ int m_tRemote;
+
+ int m_tBD;
+ int m_tDVD;
+ int m_tDVDFile;
+ int m_tDiscImage;
+
+ std::string m_protocols;
+ std::string m_fileTypes;
+ std::string m_mimeTypes;
+ std::string m_fileName;
+
+ bool m_bStreamDetails;
+ std::string m_audioCodec;
+ std::string m_audioChannels;
+ std::string m_videoCodec;
+ std::string m_videoResolution;
+ std::string m_videoAspect;
+
+ std::string m_playerName;
+
+ std::vector<std::unique_ptr<CPlayerSelectionRule>> vecSubRules;
+};
diff --git a/xbmc/dbwrappers/CMakeLists.txt b/xbmc/dbwrappers/CMakeLists.txt
new file mode 100644
index 0000000..16a03f2
--- /dev/null
+++ b/xbmc/dbwrappers/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(SOURCES Database.cpp
+ DatabaseQuery.cpp
+ dataset.cpp
+ qry_dat.cpp
+ sqlitedataset.cpp)
+
+set(HEADERS Database.h
+ DatabaseQuery.h
+ dataset.h
+ qry_dat.h
+ sqlitedataset.h)
+
+if(MYSQLCLIENT_FOUND OR MARIADBCLIENT_FOUND)
+ list(APPEND SOURCES mysqldataset.cpp)
+ list(APPEND HEADERS mysqldataset.h)
+endif()
+
+core_add_library(dbwrappers)
diff --git a/xbmc/dbwrappers/Database.cpp b/xbmc/dbwrappers/Database.cpp
new file mode 100644
index 0000000..1c2c48c
--- /dev/null
+++ b/xbmc/dbwrappers/Database.cpp
@@ -0,0 +1,858 @@
+/*
+ * 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 "Database.h"
+
+#include "DatabaseManager.h"
+#include "DbUrl.h"
+#include "ServiceBroker.h"
+#include "filesystem/SpecialProtocol.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "sqlitedataset.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#if defined(HAS_MYSQL) || defined(HAS_MARIADB)
+#include "mysqldataset.h"
+#endif
+
+#ifdef TARGET_POSIX
+#include "platform/posix/ConvUtils.h"
+#endif
+
+using namespace dbiplus;
+
+#define MAX_COMPRESS_COUNT 20
+
+void CDatabase::Filter::AppendField(const std::string& strField)
+{
+ if (strField.empty())
+ return;
+
+ if (fields.empty() || fields == "*")
+ fields = strField;
+ else
+ fields += ", " + strField;
+}
+
+void CDatabase::Filter::AppendJoin(const std::string& strJoin)
+{
+ if (strJoin.empty())
+ return;
+
+ if (join.empty())
+ join = strJoin;
+ else
+ join += " " + strJoin;
+}
+
+void CDatabase::Filter::AppendWhere(const std::string& strWhere, bool combineWithAnd /* = true */)
+{
+ if (strWhere.empty())
+ return;
+
+ if (where.empty())
+ where = strWhere;
+ else
+ {
+ where = "(" + where + ") ";
+ where += combineWithAnd ? "AND" : "OR";
+ where += " (" + strWhere + ")";
+ }
+}
+
+void CDatabase::Filter::AppendOrder(const std::string& strOrder)
+{
+ if (strOrder.empty())
+ return;
+
+ if (order.empty())
+ order = strOrder;
+ else
+ order += ", " + strOrder;
+}
+
+void CDatabase::Filter::AppendGroup(const std::string& strGroup)
+{
+ if (strGroup.empty())
+ return;
+
+ if (group.empty())
+ group = strGroup;
+ else
+ group += ", " + strGroup;
+}
+
+void CDatabase::ExistsSubQuery::AppendJoin(const std::string& strJoin)
+{
+ if (strJoin.empty())
+ return;
+
+ if (join.empty())
+ join = strJoin;
+ else
+ join += " " + strJoin;
+}
+
+void CDatabase::ExistsSubQuery::AppendWhere(const std::string& strWhere,
+ bool combineWithAnd /* = true */)
+{
+ if (strWhere.empty())
+ return;
+
+ if (where.empty())
+ where = strWhere;
+ else
+ {
+ where += combineWithAnd ? " AND " : " OR ";
+ where += strWhere;
+ }
+}
+
+bool CDatabase::ExistsSubQuery::BuildSQL(std::string& strSQL)
+{
+ if (tablename.empty())
+ return false;
+ strSQL = "EXISTS (SELECT 1 FROM " + tablename;
+ if (!join.empty())
+ strSQL += " " + join;
+ std::string strWhere;
+ if (!param.empty())
+ strWhere = param;
+ if (!where.empty())
+ {
+ if (!strWhere.empty())
+ strWhere += " AND ";
+ strWhere += where;
+ }
+ if (!strWhere.empty())
+ strSQL += " WHERE " + strWhere;
+
+ strSQL += ")";
+ return true;
+}
+
+CDatabase::DatasetLayout::DatasetLayout(size_t totalfields)
+{
+ m_fields.resize(totalfields, DatasetFieldInfo(false, false, -1));
+}
+
+void CDatabase::DatasetLayout::SetField(int fieldNo,
+ const std::string& strField,
+ bool bOutput /*= false*/)
+{
+ if (fieldNo >= 0 && fieldNo < static_cast<int>(m_fields.size()))
+ {
+ m_fields[fieldNo].strField = strField;
+ m_fields[fieldNo].fetch = true;
+ m_fields[fieldNo].output = bOutput;
+ }
+}
+
+void CDatabase::DatasetLayout::AdjustRecordNumbers(int offset)
+{
+ int recno = 0;
+ for (auto& field : m_fields)
+ {
+ if (field.fetch)
+ {
+ field.recno = recno + offset;
+ ++recno;
+ }
+ }
+}
+
+bool CDatabase::DatasetLayout::GetFetch(int fieldno)
+{
+ if (fieldno >= 0 && fieldno < static_cast<int>(m_fields.size()))
+ return m_fields[fieldno].fetch;
+ return false;
+}
+
+void CDatabase::DatasetLayout::SetFetch(int fieldno, bool bFetch /*= true*/)
+{
+ if (fieldno >= 0 && fieldno < static_cast<int>(m_fields.size()))
+ m_fields[fieldno].fetch = bFetch;
+}
+
+bool CDatabase::DatasetLayout::GetOutput(int fieldno)
+{
+ if (fieldno >= 0 && fieldno < static_cast<int>(m_fields.size()))
+ return m_fields[fieldno].output;
+ return false;
+}
+
+int CDatabase::DatasetLayout::GetRecNo(int fieldno)
+{
+ if (fieldno >= 0 && fieldno < static_cast<int>(m_fields.size()))
+ return m_fields[fieldno].recno;
+ return -1;
+}
+
+const std::string CDatabase::DatasetLayout::GetFields()
+{
+ std::string strSQL;
+ for (const auto& field : m_fields)
+ {
+ if (!field.strField.empty() && field.fetch)
+ {
+ if (strSQL.empty())
+ strSQL = field.strField;
+ else
+ strSQL += ", " + field.strField;
+ }
+ }
+
+ return strSQL;
+}
+
+bool CDatabase::DatasetLayout::HasFilterFields()
+{
+ for (const auto& field : m_fields)
+ {
+ if (field.fetch)
+ return true;
+ }
+ return false;
+}
+
+CDatabase::CDatabase()
+ : m_profileManager(*CServiceBroker::GetSettingsComponent()->GetProfileManager())
+{
+ m_openCount = 0;
+ m_sqlite = true;
+ m_multipleExecute = false;
+}
+
+CDatabase::~CDatabase(void)
+{
+ Close();
+}
+
+void CDatabase::Split(const std::string& strFileNameAndPath,
+ std::string& strPath,
+ std::string& strFileName)
+{
+ strFileName = "";
+ strPath = "";
+ int i = strFileNameAndPath.size() - 1;
+ while (i > 0)
+ {
+ char ch = strFileNameAndPath[i];
+ if (ch == ':' || ch == '/' || ch == '\\')
+ break;
+ else
+ i--;
+ }
+ strPath = strFileNameAndPath.substr(0, i);
+ strFileName = strFileNameAndPath.substr(i);
+}
+
+std::string CDatabase::PrepareSQL(std::string strStmt, ...) const
+{
+ std::string strResult = "";
+
+ if (nullptr != m_pDB)
+ {
+ va_list args;
+ va_start(args, strStmt);
+ strResult = m_pDB->vprepare(strStmt.c_str(), args);
+ va_end(args);
+ }
+
+ return strResult;
+}
+
+std::string CDatabase::GetSingleValue(const std::string& query, std::unique_ptr<Dataset>& ds)
+{
+ std::string ret;
+ try
+ {
+ if (!m_pDB || !ds)
+ return ret;
+
+ if (ds->query(query) && ds->num_rows() > 0)
+ ret = ds->fv(0).get_asString();
+
+ ds->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} - failed on query '{}'", __FUNCTION__, query);
+ }
+ return ret;
+}
+
+std::string CDatabase::GetSingleValue(const std::string& strTable,
+ const std::string& strColumn,
+ const std::string& strWhereClause /* = std::string() */,
+ const std::string& strOrderBy /* = std::string() */)
+{
+ std::string query = PrepareSQL("SELECT %s FROM %s", strColumn.c_str(), strTable.c_str());
+ if (!strWhereClause.empty())
+ query += " WHERE " + strWhereClause;
+ if (!strOrderBy.empty())
+ query += " ORDER BY " + strOrderBy;
+ query += " LIMIT 1";
+ return GetSingleValue(query, m_pDS);
+}
+
+std::string CDatabase::GetSingleValue(const std::string& query)
+{
+ return GetSingleValue(query, m_pDS);
+}
+
+int CDatabase::GetSingleValueInt(const std::string& query, std::unique_ptr<Dataset>& ds)
+{
+ int ret = 0;
+ try
+ {
+ if (!m_pDB || !ds)
+ return ret;
+
+ if (ds->query(query) && ds->num_rows() > 0)
+ ret = ds->fv(0).get_asInt();
+
+ ds->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} - failed on query '{}'", __FUNCTION__, query);
+ }
+ return ret;
+}
+
+int CDatabase::GetSingleValueInt(const std::string& strTable,
+ const std::string& strColumn,
+ const std::string& strWhereClause /* = std::string() */,
+ const std::string& strOrderBy /* = std::string() */)
+{
+ std::string strResult = GetSingleValue(strTable, strColumn, strWhereClause, strOrderBy);
+ return static_cast<int>(strtol(strResult.c_str(), NULL, 10));
+}
+
+int CDatabase::GetSingleValueInt(const std::string& query)
+{
+ return GetSingleValueInt(query, m_pDS);
+}
+
+bool CDatabase::DeleteValues(const std::string& strTable, const Filter& filter /* = Filter() */)
+{
+ std::string strQuery;
+ BuildSQL(PrepareSQL("DELETE FROM %s ", strTable.c_str()), filter, strQuery);
+ return ExecuteQuery(strQuery);
+}
+
+bool CDatabase::BeginMultipleExecute()
+{
+ m_multipleExecute = true;
+ m_multipleQueries.clear();
+ return true;
+}
+
+bool CDatabase::CommitMultipleExecute()
+{
+ m_multipleExecute = false;
+ BeginTransaction();
+ for (const auto& i : m_multipleQueries)
+ {
+ if (!ExecuteQuery(i))
+ {
+ RollbackTransaction();
+ return false;
+ }
+ }
+ m_multipleQueries.clear();
+ return CommitTransaction();
+}
+
+bool CDatabase::ExecuteQuery(const std::string& strQuery)
+{
+ if (m_multipleExecute)
+ {
+ m_multipleQueries.push_back(strQuery);
+ return true;
+ }
+
+ bool bReturn = false;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return bReturn;
+ if (nullptr == m_pDS)
+ return bReturn;
+ m_pDS->exec(strQuery);
+ bReturn = true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} - failed to execute query '{}'", __FUNCTION__, strQuery);
+ }
+
+ return bReturn;
+}
+
+bool CDatabase::ResultQuery(const std::string& strQuery) const
+{
+ bool bReturn = false;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return bReturn;
+ if (nullptr == m_pDS)
+ return bReturn;
+
+ std::string strPreparedQuery = PrepareSQL(strQuery);
+
+ bReturn = m_pDS->query(strPreparedQuery);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} - failed to execute query '{}'", __FUNCTION__, strQuery);
+ }
+
+ return bReturn;
+}
+
+bool CDatabase::QueueInsertQuery(const std::string& strQuery)
+{
+ if (strQuery.empty())
+ return false;
+
+ if (!m_bMultiInsert)
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false;
+
+ m_bMultiInsert = true;
+ m_pDS2->insert();
+ }
+
+ m_pDS2->add_insert_sql(strQuery);
+
+ return true;
+}
+
+bool CDatabase::CommitInsertQueries()
+{
+ bool bReturn = true;
+
+ if (m_bMultiInsert)
+ {
+ try
+ {
+ m_bMultiInsert = false;
+ m_pDS2->post();
+ m_pDS2->clear_insert_sql();
+ }
+ catch (...)
+ {
+ bReturn = false;
+ CLog::Log(LOGERROR, "{} - failed to execute queries", __FUNCTION__);
+ }
+ }
+
+ return bReturn;
+}
+
+size_t CDatabase::GetInsertQueriesCount()
+{
+ return m_pDS2->insert_sql_count();
+}
+
+bool CDatabase::QueueDeleteQuery(const std::string& strQuery)
+{
+ if (strQuery.empty() || !m_pDB || !m_pDS)
+ return false;
+
+ m_bMultiDelete = true;
+ m_pDS->del();
+ m_pDS->add_delete_sql(strQuery);
+ return true;
+}
+
+bool CDatabase::CommitDeleteQueries()
+{
+ bool bReturn = true;
+
+ if (m_bMultiDelete)
+ {
+ try
+ {
+ m_bMultiDelete = false;
+ m_pDS->deletion();
+ m_pDS->clear_delete_sql();
+ }
+ catch (...)
+ {
+ bReturn = false;
+ CLog::Log(LOGERROR, "{} - failed to execute queries", __FUNCTION__);
+ }
+ }
+
+ return bReturn;
+}
+
+size_t CDatabase::GetDeleteQueriesCount()
+{
+ return m_pDS->delete_sql_count();
+}
+
+bool CDatabase::Open()
+{
+ DatabaseSettings db_fallback;
+ return Open(db_fallback);
+}
+
+bool CDatabase::Open(const DatabaseSettings& settings)
+{
+ if (IsOpen())
+ {
+ m_openCount++;
+ return true;
+ }
+
+ // check our database manager to see if this database can be opened
+ if (!CServiceBroker::GetDatabaseManager().CanOpen(GetBaseDBName()))
+ return false;
+
+ DatabaseSettings dbSettings = settings;
+ InitSettings(dbSettings);
+
+ std::string dbName = dbSettings.name;
+ dbName += std::to_string(GetSchemaVersion());
+ return Connect(dbName, dbSettings, false);
+}
+
+void CDatabase::InitSettings(DatabaseSettings& dbSettings)
+{
+ m_sqlite = true;
+
+#if defined(HAS_MYSQL) || defined(HAS_MARIADB)
+ if (dbSettings.type == "mysql")
+ {
+ // check we have all information before we cancel the fallback
+ if (!(dbSettings.host.empty() || dbSettings.user.empty() || dbSettings.pass.empty()))
+ m_sqlite = false;
+ else
+ CLog::Log(LOGINFO, "Essential mysql database information is missing. Require at least host, "
+ "user and pass defined.");
+ }
+ else
+#else
+ if (dbSettings.type == "mysql")
+ CLog::Log(
+ LOGERROR,
+ "MySQL library requested but MySQL support is not compiled in. Falling back to sqlite3.");
+#endif
+ {
+ dbSettings.type = "sqlite3";
+ if (dbSettings.host.empty())
+ dbSettings.host = CSpecialProtocol::TranslatePath(m_profileManager.GetDatabaseFolder());
+ }
+
+ // use separate, versioned database
+ if (dbSettings.name.empty())
+ dbSettings.name = GetBaseDBName();
+}
+
+void CDatabase::CopyDB(const std::string& latestDb)
+{
+ m_pDB->copy(latestDb.c_str());
+}
+
+void CDatabase::DropAnalytics()
+{
+ m_pDB->drop_analytics();
+}
+
+bool CDatabase::Connect(const std::string& dbName, const DatabaseSettings& dbSettings, bool create)
+{
+ // create the appropriate database structure
+ if (dbSettings.type == "sqlite3")
+ {
+ m_pDB.reset(new SqliteDatabase());
+ }
+#if defined(HAS_MYSQL) || defined(HAS_MARIADB)
+ else if (dbSettings.type == "mysql")
+ {
+ m_pDB.reset(new MysqlDatabase());
+ }
+#endif
+ else
+ {
+ CLog::Log(LOGERROR, "Unable to determine database type: {}", dbSettings.type);
+ return false;
+ }
+
+ // host name is always required
+ m_pDB->setHostName(dbSettings.host.c_str());
+
+ if (!dbSettings.port.empty())
+ m_pDB->setPort(dbSettings.port.c_str());
+
+ if (!dbSettings.user.empty())
+ m_pDB->setLogin(dbSettings.user.c_str());
+
+ if (!dbSettings.pass.empty())
+ m_pDB->setPasswd(dbSettings.pass.c_str());
+
+ // database name is always required
+ m_pDB->setDatabase(dbName.c_str());
+
+ // set configuration regardless if any are empty
+ m_pDB->setConfig(dbSettings.key.c_str(), dbSettings.cert.c_str(), dbSettings.ca.c_str(),
+ dbSettings.capath.c_str(), dbSettings.ciphers.c_str(), dbSettings.compression);
+
+ // create the datasets
+ m_pDS.reset(m_pDB->CreateDataset());
+ m_pDS2.reset(m_pDB->CreateDataset());
+
+ if (m_pDB->connect(create) != DB_CONNECTION_OK)
+ return false;
+
+ try
+ {
+ // test if db already exists, if not we need to create the tables
+ if (!m_pDB->exists() && create)
+ {
+ if (dbSettings.type == "sqlite3")
+ {
+ // Modern file systems have a cluster/block size of 4k.
+ // To gain better performance when performing write
+ // operations to the database, set the page size of the
+ // database file to 4k.
+ // This needs to be done before any table is created.
+ m_pDS->exec("PRAGMA page_size=4096\n");
+
+ // Also set the memory cache size to 16k
+ m_pDS->exec("PRAGMA default_cache_size=4096\n");
+ }
+ CreateDatabase();
+ }
+
+ // sqlite3 post connection operations
+ if (dbSettings.type == "sqlite3")
+ {
+ m_pDS->exec("PRAGMA cache_size=4096\n");
+ m_pDS->exec("PRAGMA synchronous='NORMAL'\n");
+ m_pDS->exec("PRAGMA count_changes='OFF'\n");
+ }
+ }
+ catch (DbErrors& error)
+ {
+ CLog::Log(LOGERROR, "{} failed with '{}'", __FUNCTION__, error.getMsg());
+ m_openCount = 1; // set to open so we can execute Close()
+ Close();
+ return false;
+ }
+
+ m_openCount = 1; // our database is open
+ return true;
+}
+
+int CDatabase::GetDBVersion()
+{
+ m_pDS->query("SELECT idVersion FROM version\n");
+ if (m_pDS->num_rows() > 0)
+ return m_pDS->fv("idVersion").get_asInt();
+ return 0;
+}
+
+bool CDatabase::IsOpen()
+{
+ return m_openCount > 0;
+}
+
+void CDatabase::Close()
+{
+ if (m_openCount == 0)
+ return;
+
+ if (m_openCount > 1)
+ {
+ m_openCount--;
+ return;
+ }
+
+ m_openCount = 0;
+ m_multipleExecute = false;
+
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr != m_pDS)
+ m_pDS->close();
+ m_pDB->disconnect();
+ m_pDB.reset();
+ m_pDS.reset();
+ m_pDS2.reset();
+}
+
+bool CDatabase::Compress(bool bForce /* =true */)
+{
+ if (!m_sqlite)
+ return true;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ if (!bForce)
+ {
+ m_pDS->query("select iCompressCount from version");
+ if (!m_pDS->eof())
+ {
+ int iCount = m_pDS->fv(0).get_asInt();
+ if (iCount > MAX_COMPRESS_COUNT)
+ iCount = -1;
+ m_pDS->close();
+ std::string strSQL = PrepareSQL("update version set iCompressCount=%i\n", ++iCount);
+ m_pDS->exec(strSQL);
+ if (iCount != 0)
+ return true;
+ }
+ }
+
+ if (!m_pDS->exec("vacuum\n"))
+ return false;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} - Compressing the database failed", __FUNCTION__);
+ return false;
+ }
+ return true;
+}
+
+void CDatabase::Interrupt()
+{
+ m_pDS->interrupt();
+}
+
+void CDatabase::BeginTransaction()
+{
+ try
+ {
+ if (nullptr != m_pDB)
+ m_pDB->start_transaction();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "database:begintransaction failed");
+ }
+}
+
+bool CDatabase::CommitTransaction()
+{
+ try
+ {
+ if (nullptr != m_pDB)
+ m_pDB->commit_transaction();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "database:committransaction failed");
+ return false;
+ }
+ return true;
+}
+
+void CDatabase::RollbackTransaction()
+{
+ try
+ {
+ if (nullptr != m_pDB)
+ m_pDB->rollback_transaction();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "database:rollbacktransaction failed");
+ }
+}
+
+bool CDatabase::CreateDatabase()
+{
+ BeginTransaction();
+ try
+ {
+ CLog::Log(LOGINFO, "creating version table");
+ m_pDS->exec("CREATE TABLE version (idVersion integer, iCompressCount integer)\n");
+ std::string strSQL = PrepareSQL("INSERT INTO version (idVersion,iCompressCount) values(%i,0)\n",
+ GetSchemaVersion());
+ m_pDS->exec(strSQL);
+
+ CreateTables();
+ CreateAnalytics();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} unable to create database:{}", __FUNCTION__, (int)GetLastError());
+ RollbackTransaction();
+ return false;
+ }
+
+ return CommitTransaction();
+}
+
+void CDatabase::UpdateVersionNumber()
+{
+ std::string strSQL = PrepareSQL("UPDATE version SET idVersion=%i\n", GetSchemaVersion());
+ m_pDS->exec(strSQL);
+}
+
+bool CDatabase::BuildSQL(const std::string& strQuery, const Filter& filter, std::string& strSQL)
+{
+ strSQL = strQuery;
+
+ if (!filter.join.empty())
+ strSQL += filter.join;
+ if (!filter.where.empty())
+ strSQL += " WHERE " + filter.where;
+ if (!filter.group.empty())
+ strSQL += " GROUP BY " + filter.group;
+ if (!filter.order.empty())
+ strSQL += " ORDER BY " + filter.order;
+ if (!filter.limit.empty())
+ strSQL += " LIMIT " + filter.limit;
+
+ return true;
+}
+
+bool CDatabase::BuildSQL(const std::string& strBaseDir,
+ const std::string& strQuery,
+ Filter& filter,
+ std::string& strSQL,
+ CDbUrl& dbUrl)
+{
+ SortDescription sorting;
+ return BuildSQL(strBaseDir, strQuery, filter, strSQL, dbUrl, sorting);
+}
+
+bool CDatabase::BuildSQL(const std::string& strBaseDir,
+ const std::string& strQuery,
+ Filter& filter,
+ std::string& strSQL,
+ CDbUrl& dbUrl,
+ SortDescription& sorting /* = SortDescription() */)
+{
+ // parse the base path to get additional filters
+ dbUrl.Reset();
+ if (!dbUrl.FromString(strBaseDir) || !GetFilter(dbUrl, filter, sorting))
+ return false;
+
+ return BuildSQL(strQuery, filter, strSQL);
+}
diff --git a/xbmc/dbwrappers/Database.h b/xbmc/dbwrappers/Database.h
new file mode 100644
index 0000000..2f3d35d
--- /dev/null
+++ b/xbmc/dbwrappers/Database.h
@@ -0,0 +1,321 @@
+/*
+ * 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
+
+namespace dbiplus
+{
+class Database;
+class Dataset;
+} // namespace dbiplus
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class DatabaseSettings; // forward
+class CDbUrl;
+class CProfileManager;
+struct SortDescription;
+
+class CDatabase
+{
+public:
+ class Filter
+ {
+ public:
+ Filter() : fields("*") {}
+ explicit Filter(const char* w) : fields("*"), where(w) {}
+ explicit Filter(const std::string& w) : fields("*"), where(w) {}
+
+ void AppendField(const std::string& strField);
+ void AppendJoin(const std::string& strJoin);
+ void AppendWhere(const std::string& strWhere, bool combineWithAnd = true);
+ void AppendOrder(const std::string& strOrder);
+ void AppendGroup(const std::string& strGroup);
+
+ std::string fields;
+ std::string join;
+ std::string where;
+ std::string order;
+ std::string group;
+ std::string limit;
+ };
+
+ struct DatasetFieldInfo
+ {
+ DatasetFieldInfo(bool fetch, bool output, int recno)
+ : fetch(fetch), output(output), recno(recno)
+ {
+ }
+
+ bool fetch;
+ bool output;
+ int recno;
+ std::string strField;
+ };
+
+ class DatasetLayout
+ {
+ public:
+ DatasetLayout(size_t totalfields);
+ void SetField(int fieldNo, const std::string& strField, bool bOutput = false);
+ void AdjustRecordNumbers(int offset);
+ bool GetFetch(int fieldno);
+ void SetFetch(int fieldno, bool bFetch = true);
+ bool GetOutput(int fieldno);
+ int GetRecNo(int fieldno);
+ const std::string GetFields();
+ bool HasFilterFields();
+
+ private:
+ std::vector<DatasetFieldInfo> m_fields;
+ };
+
+ class ExistsSubQuery
+ {
+ public:
+ explicit ExistsSubQuery(const std::string& table) : tablename(table) {}
+ ExistsSubQuery(const std::string& table, const std::string& parameter)
+ : tablename(table), param(parameter)
+ {
+ }
+ void AppendJoin(const std::string& strJoin);
+ void AppendWhere(const std::string& strWhere, bool combineWithAnd = true);
+ bool BuildSQL(std::string& strSQL);
+
+ std::string tablename;
+ std::string param;
+ std::string join;
+ std::string where;
+ };
+
+ CDatabase();
+ virtual ~CDatabase(void);
+ bool IsOpen();
+ virtual void Close();
+ bool Compress(bool bForce = true);
+ void Interrupt();
+
+ bool Open(const DatabaseSettings& db);
+
+ void BeginTransaction();
+ virtual bool CommitTransaction();
+ void RollbackTransaction();
+ void CopyDB(const std::string& latestDb);
+ void DropAnalytics();
+
+ std::string PrepareSQL(std::string strStmt, ...) const;
+
+ /*!
+ * @brief Get a single value from a table.
+ * @remarks The values of the strWhereClause and strOrderBy parameters have to be FormatSQL'ed when used.
+ * @param strTable The table to get the value from.
+ * @param strColumn The column to get.
+ * @param strWhereClause If set, use this WHERE clause.
+ * @param strOrderBy If set, use this ORDER BY clause.
+ * @return The requested value or an empty string if it wasn't found.
+ */
+ std::string GetSingleValue(const std::string& strTable,
+ const std::string& strColumn,
+ const std::string& strWhereClause = std::string(),
+ const std::string& strOrderBy = std::string());
+ std::string GetSingleValue(const std::string& query);
+
+ /*! \brief Get a single value from a query on a dataset.
+ \param query the query in question.
+ \param ds the dataset to use for the query.
+ \return the value from the query, empty on failure.
+ */
+ std::string GetSingleValue(const std::string& query, std::unique_ptr<dbiplus::Dataset>& ds);
+
+ /*!
+ * @brief Get a single integer value from a table.
+ * @remarks The values of the strWhereClause and strOrderBy parameters have to be FormatSQL'ed when used.
+ * @param strTable The table to get the value from.
+ * @param strColumn The column to get.
+ * @param strWhereClause If set, use this WHERE clause.
+ * @param strOrderBy If set, use this ORDER BY clause.
+ * @return The requested value or 0 if it wasn't found.
+ */
+ int GetSingleValueInt(const std::string& strTable,
+ const std::string& strColumn,
+ const std::string& strWhereClause = std::string(),
+ const std::string& strOrderBy = std::string());
+ int GetSingleValueInt(const std::string& query);
+
+ /*! \brief Get a single integer value from a query on a dataset.
+ \param query the query in question.
+ \param ds the dataset to use for the query.
+ \return the value from the query, 0 on failure.
+ */
+ int GetSingleValueInt(const std::string& query, std::unique_ptr<dbiplus::Dataset>& ds);
+
+ /*!
+ * @brief Delete values from a table.
+ * @param strTable The table to delete the values from.
+ * @param filter The Filter to apply to this query.
+ * @return True if the query was executed successfully, false otherwise.
+ */
+ bool DeleteValues(const std::string& strTable, const Filter& filter = Filter());
+
+ /*!
+ * @brief Execute a query that does not return any result.
+ * Note that if BeginMultipleExecute() has been called, the
+ * query will be queued until CommitMultipleExecute() is called.
+ * @param strQuery The query to execute.
+ * @return True if the query was executed successfully, false otherwise.
+ * @sa BeginMultipleExecute, CommitMultipleExecute
+ */
+ bool ExecuteQuery(const std::string& strQuery);
+
+ /*!
+ * @brief Execute a query that returns a result.
+ * @remarks Call m_pDS->close(); to clean up the dataset when done.
+ * @param strQuery The query to execute.
+ * @return True if the query was executed successfully, false otherwise.
+ */
+ bool ResultQuery(const std::string& strQuery) const;
+
+ /*!
+ * @brief Start a multiple execution queue. Any ExecuteQuery() function
+ * following this call will be queued rather than executed until
+ * CommitMultipleExecute() is performed.
+ * NOTE: Queries that rely on any queued execute query will not
+ * function as expected during this period!
+ * @return true if we could start a multiple execution queue, false otherwise.
+ * @sa CommitMultipleExecute, ExecuteQuery
+ */
+ bool BeginMultipleExecute();
+
+ /*!
+ * @brief Commit the multiple execution queue to the database.
+ * Queries are performed within a transaction, and the transaction
+ * is rolled back should any one query fail.
+ * @return True if the queries were executed successfully, false otherwise.
+ * @sa BeginMultipleExecute, ExecuteQuery
+ */
+ bool CommitMultipleExecute();
+
+ /*!
+ * @brief Put an INSERT or REPLACE query in the queue.
+ * @param strQuery The query to queue.
+ * @return True if the query was added successfully, false otherwise.
+ */
+ bool QueueInsertQuery(const std::string& strQuery);
+
+ /*!
+ * @brief Commit all queries in the queue.
+ * @return True if all queries were executed successfully, false otherwise.
+ */
+ bool CommitInsertQueries();
+
+ /*!
+ * @brief Get the number of INSERT queries in the queue.
+ * @return The number of queries.
+ */
+ size_t GetInsertQueriesCount();
+
+ /*!
+ * @brief Put a DELETE query in the queue.
+ * @param strQuery The query to queue.
+ * @return True if the query was added successfully, false otherwise.
+ */
+ bool QueueDeleteQuery(const std::string& strQuery);
+
+ /*!
+ * @brief Commit all queued DELETE queries.
+ * @return True if all queries were executed successfully, false otherwise.
+ */
+ bool CommitDeleteQueries();
+
+ /*!
+ * @brief Get the number of DELETE queries in the queue.
+ * @return The number of queries.
+ */
+ size_t GetDeleteQueriesCount();
+
+ virtual bool GetFilter(CDbUrl& dbUrl, Filter& filter, SortDescription& sorting) { return true; }
+ virtual bool BuildSQL(const std::string& strBaseDir,
+ const std::string& strQuery,
+ Filter& filter,
+ std::string& strSQL,
+ CDbUrl& dbUrl);
+ virtual bool BuildSQL(const std::string& strBaseDir,
+ const std::string& strQuery,
+ Filter& filter,
+ std::string& strSQL,
+ CDbUrl& dbUrl,
+ SortDescription& sorting);
+
+ bool Connect(const std::string& dbName, const DatabaseSettings& db, bool create);
+
+protected:
+ friend class CDatabaseManager;
+
+ void Split(const std::string& strFileNameAndPath, std::string& strPath, std::string& strFileName);
+
+ virtual bool Open();
+
+ /*! \brief Create database tables and analytics as needed.
+ Calls CreateTables() and CreateAnalytics() on child classes.
+ */
+ bool CreateDatabase();
+
+ /* \brief Create tables for the current database schema.
+ Will be called on database creation.
+ */
+ virtual void CreateTables() = 0;
+
+ /* \brief Create views, indices and triggers for the current database schema.
+ Will be called on database creation and database update.
+ */
+ virtual void CreateAnalytics() = 0;
+
+ /* \brief Update database tables to the current version.
+ Note that analytics (views, indices, triggers) are not present during this
+ function, so don't rely on them.
+ */
+ virtual void UpdateTables(int version) {}
+
+ /* \brief The minimum schema version that we support updating from.
+ */
+ virtual int GetMinSchemaVersion() const { return 0; }
+
+ /* \brief The current schema version.
+ */
+ virtual int GetSchemaVersion() const = 0;
+ virtual const char* GetBaseDBName() const = 0;
+
+ int GetDBVersion();
+
+ bool BuildSQL(const std::string& strQuery, const Filter& filter, std::string& strSQL);
+
+ bool m_sqlite; ///< \brief whether we use sqlite (defaults to true)
+
+ std::unique_ptr<dbiplus::Database> m_pDB;
+ std::unique_ptr<dbiplus::Dataset> m_pDS;
+ std::unique_ptr<dbiplus::Dataset> m_pDS2;
+
+protected:
+ // Construction parameters
+ const CProfileManager& m_profileManager;
+
+private:
+ void InitSettings(DatabaseSettings& dbSettings);
+ void UpdateVersionNumber();
+
+ bool m_bMultiInsert =
+ false; /*!< True if there are any queries in the insert queue, false otherwise */
+ bool m_bMultiDelete =
+ false; /*!< True if there are any queries in the delete queue, false otherwise */
+ unsigned int m_openCount;
+
+ bool m_multipleExecute;
+ std::vector<std::string> m_multipleQueries;
+};
diff --git a/xbmc/dbwrappers/DatabaseQuery.cpp b/xbmc/dbwrappers/DatabaseQuery.cpp
new file mode 100644
index 0000000..c1432be
--- /dev/null
+++ b/xbmc/dbwrappers/DatabaseQuery.cpp
@@ -0,0 +1,571 @@
+/*
+ * Copyright (C) 2013-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 "DatabaseQuery.h"
+
+#include "Database.h"
+#include "XBDateTime.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/XBMCTinyXML.h"
+
+typedef struct
+{
+ char string[15];
+ CDatabaseQueryRule::SEARCH_OPERATOR op;
+ int localizedString;
+} operatorField;
+
+static const operatorField operators[] = {
+ {"contains", CDatabaseQueryRule::OPERATOR_CONTAINS, 21400},
+ {"doesnotcontain", CDatabaseQueryRule::OPERATOR_DOES_NOT_CONTAIN, 21401},
+ {"is", CDatabaseQueryRule::OPERATOR_EQUALS, 21402},
+ {"isnot", CDatabaseQueryRule::OPERATOR_DOES_NOT_EQUAL, 21403},
+ {"startswith", CDatabaseQueryRule::OPERATOR_STARTS_WITH, 21404},
+ {"endswith", CDatabaseQueryRule::OPERATOR_ENDS_WITH, 21405},
+ {"greaterthan", CDatabaseQueryRule::OPERATOR_GREATER_THAN, 21406},
+ {"lessthan", CDatabaseQueryRule::OPERATOR_LESS_THAN, 21407},
+ {"after", CDatabaseQueryRule::OPERATOR_AFTER, 21408},
+ {"before", CDatabaseQueryRule::OPERATOR_BEFORE, 21409},
+ {"inthelast", CDatabaseQueryRule::OPERATOR_IN_THE_LAST, 21410},
+ {"notinthelast", CDatabaseQueryRule::OPERATOR_NOT_IN_THE_LAST, 21411},
+ {"true", CDatabaseQueryRule::OPERATOR_TRUE, 20122},
+ {"false", CDatabaseQueryRule::OPERATOR_FALSE, 20424},
+ {"between", CDatabaseQueryRule::OPERATOR_BETWEEN, 21456}};
+
+CDatabaseQueryRule::CDatabaseQueryRule()
+{
+ m_field = 0;
+ m_operator = OPERATOR_CONTAINS;
+}
+
+bool CDatabaseQueryRule::Load(const TiXmlNode* node, const std::string& encoding /* = "UTF-8" */)
+{
+ if (node == NULL)
+ return false;
+
+ const TiXmlElement* element = node->ToElement();
+ if (element == NULL)
+ return false;
+
+ // format is:
+ // <rule field="Genre" operator="contains">parameter</rule>
+ // where parameter can either be a string or a list of
+ // <value> tags containing a string
+ const char* field = element->Attribute("field");
+ const char* oper = element->Attribute("operator");
+ if (field == NULL || oper == NULL)
+ return false;
+
+ m_field = TranslateField(field);
+ m_operator = TranslateOperator(oper);
+
+ if (m_operator == OPERATOR_TRUE || m_operator == OPERATOR_FALSE)
+ return true;
+
+ const TiXmlNode* parameter = element->FirstChild();
+ if (parameter == NULL)
+ return false;
+
+ if (parameter->Type() == TiXmlNode::TINYXML_TEXT)
+ {
+ std::string utf8Parameter;
+ if (encoding.empty()) // utf8
+ utf8Parameter = parameter->ValueStr();
+ else
+ g_charsetConverter.ToUtf8(encoding, parameter->ValueStr(), utf8Parameter);
+
+ if (!utf8Parameter.empty())
+ m_parameter.push_back(utf8Parameter);
+ }
+ else if (parameter->Type() == TiXmlNode::TINYXML_ELEMENT)
+ {
+ const TiXmlNode* valueNode = element->FirstChild("value");
+ while (valueNode != NULL)
+ {
+ const TiXmlNode* value = valueNode->FirstChild();
+ if (value != NULL && value->Type() == TiXmlNode::TINYXML_TEXT)
+ {
+ std::string utf8Parameter;
+ if (encoding.empty()) // utf8
+ utf8Parameter = value->ValueStr();
+ else
+ g_charsetConverter.ToUtf8(encoding, value->ValueStr(), utf8Parameter);
+
+ if (!utf8Parameter.empty())
+ m_parameter.push_back(utf8Parameter);
+ }
+
+ valueNode = valueNode->NextSibling("value");
+ }
+ }
+ else
+ return false;
+
+ return true;
+}
+
+bool CDatabaseQueryRule::Load(const CVariant& obj)
+{
+ if (!obj.isMember("field") || !obj["field"].isString() || !obj.isMember("operator") ||
+ !obj["operator"].isString())
+ return false;
+
+ m_field = TranslateField(obj["field"].asString().c_str());
+ m_operator = TranslateOperator(obj["operator"].asString().c_str());
+
+ if (m_operator == OPERATOR_TRUE || m_operator == OPERATOR_FALSE)
+ return true;
+
+ if (!obj.isMember("value") || (!obj["value"].isString() && !obj["value"].isArray()))
+ return false;
+
+ const CVariant& value = obj["value"];
+ if (value.isString())
+ m_parameter.push_back(value.asString());
+ else if (value.isArray())
+ {
+ for (CVariant::const_iterator_array val = value.begin_array(); val != value.end_array(); ++val)
+ {
+ if (val->isString() && !val->asString().empty())
+ m_parameter.push_back(val->asString());
+ }
+ if (m_parameter.empty())
+ m_parameter.emplace_back("");
+ }
+ else
+ return false;
+
+ return true;
+}
+
+bool CDatabaseQueryRule::Save(TiXmlNode* parent) const
+{
+ if (parent == NULL ||
+ (m_parameter.empty() && m_operator != OPERATOR_TRUE && m_operator != OPERATOR_FALSE))
+ return false;
+
+ TiXmlElement rule("rule");
+ rule.SetAttribute("field", TranslateField(m_field).c_str());
+ rule.SetAttribute("operator", TranslateOperator(m_operator).c_str());
+
+ for (const auto& it : m_parameter)
+ {
+ TiXmlElement value("value");
+ TiXmlText text(it);
+ value.InsertEndChild(text);
+ rule.InsertEndChild(value);
+ }
+
+ parent->InsertEndChild(rule);
+
+ return true;
+}
+
+bool CDatabaseQueryRule::Save(CVariant& obj) const
+{
+ if (obj.isNull() ||
+ (m_parameter.empty() && m_operator != OPERATOR_TRUE && m_operator != OPERATOR_FALSE))
+ return false;
+
+ obj["field"] = TranslateField(m_field);
+ obj["operator"] = TranslateOperator(m_operator);
+ obj["value"] = m_parameter;
+
+ return true;
+}
+
+CDatabaseQueryRule::SEARCH_OPERATOR CDatabaseQueryRule::TranslateOperator(const char* oper)
+{
+ for (const operatorField& o : operators)
+ if (StringUtils::EqualsNoCase(oper, o.string))
+ return o.op;
+ return OPERATOR_CONTAINS;
+}
+
+std::string CDatabaseQueryRule::TranslateOperator(SEARCH_OPERATOR oper)
+{
+ for (const operatorField& o : operators)
+ if (oper == o.op)
+ return o.string;
+ return "contains";
+}
+
+std::string CDatabaseQueryRule::GetLocalizedOperator(SEARCH_OPERATOR oper)
+{
+ for (const operatorField& o : operators)
+ if (oper == o.op)
+ return g_localizeStrings.Get(o.localizedString);
+ return g_localizeStrings.Get(16018);
+}
+
+void CDatabaseQueryRule::GetAvailableOperators(std::vector<std::string>& operatorList)
+{
+ for (const operatorField& o : operators)
+ operatorList.emplace_back(o.string);
+}
+
+std::string CDatabaseQueryRule::GetParameter() const
+{
+ return StringUtils::Join(m_parameter, DATABASEQUERY_RULE_VALUE_SEPARATOR);
+}
+
+void CDatabaseQueryRule::SetParameter(const std::string& value)
+{
+ m_parameter = StringUtils::Split(value, DATABASEQUERY_RULE_VALUE_SEPARATOR);
+}
+
+void CDatabaseQueryRule::SetParameter(const std::vector<std::string>& values)
+{
+ m_parameter.assign(values.begin(), values.end());
+}
+
+std::string CDatabaseQueryRule::ValidateParameter(const std::string& parameter) const
+{
+ if ((GetFieldType(m_field) == REAL_FIELD || GetFieldType(m_field) == NUMERIC_FIELD ||
+ GetFieldType(m_field) == SECONDS_FIELD) &&
+ parameter.empty())
+ return "0"; // interpret empty fields as 0
+ return parameter;
+}
+
+std::string CDatabaseQueryRule::FormatParameter(const std::string& operatorString,
+ const std::string& param,
+ const CDatabase& db,
+ const std::string& strType) const
+{
+ std::string parameter;
+ if (GetFieldType(m_field) == TEXTIN_FIELD)
+ {
+ std::vector<std::string> split = StringUtils::Split(param, ',');
+ for (std::string& itIn : split)
+ {
+ if (!parameter.empty())
+ parameter += ",";
+ parameter += db.PrepareSQL("'%s'", StringUtils::Trim(itIn).c_str());
+ }
+ parameter = " IN (" + parameter + ")";
+ }
+ else
+ parameter = db.PrepareSQL(operatorString, ValidateParameter(param).c_str());
+
+ if (GetFieldType(m_field) == DATE_FIELD)
+ {
+ if (m_operator == OPERATOR_IN_THE_LAST || m_operator == OPERATOR_NOT_IN_THE_LAST)
+ { // translate time period
+ CDateTime date = CDateTime::GetCurrentDateTime();
+ CDateTimeSpan span;
+ span.SetFromPeriod(param);
+ date -= span;
+ parameter = db.PrepareSQL(operatorString, date.GetAsDBDate().c_str());
+ }
+ }
+ return parameter;
+}
+
+std::string CDatabaseQueryRule::GetOperatorString(SEARCH_OPERATOR op) const
+{
+ std::string operatorString;
+ if (GetFieldType(m_field) != TEXTIN_FIELD)
+ {
+ // the comparison piece
+ switch (op)
+ {
+ case OPERATOR_CONTAINS:
+ operatorString = " LIKE '%%%s%%'";
+ break;
+ case OPERATOR_DOES_NOT_CONTAIN:
+ operatorString = " LIKE '%%%s%%'";
+ break;
+ case OPERATOR_EQUALS:
+ if (GetFieldType(m_field) == REAL_FIELD || GetFieldType(m_field) == NUMERIC_FIELD ||
+ GetFieldType(m_field) == SECONDS_FIELD)
+ operatorString = " = %s";
+ else
+ operatorString = " LIKE '%s'";
+ break;
+ case OPERATOR_DOES_NOT_EQUAL:
+ if (GetFieldType(m_field) == REAL_FIELD || GetFieldType(m_field) == NUMERIC_FIELD ||
+ GetFieldType(m_field) == SECONDS_FIELD)
+ operatorString = " != %s";
+ else
+ operatorString = " LIKE '%s'";
+ break;
+ case OPERATOR_STARTS_WITH:
+ operatorString = " LIKE '%s%%'";
+ break;
+ case OPERATOR_ENDS_WITH:
+ operatorString = " LIKE '%%%s'";
+ break;
+ case OPERATOR_AFTER:
+ case OPERATOR_GREATER_THAN:
+ case OPERATOR_IN_THE_LAST:
+ operatorString = " > ";
+ if (GetFieldType(m_field) == REAL_FIELD || GetFieldType(m_field) == NUMERIC_FIELD ||
+ GetFieldType(m_field) == SECONDS_FIELD)
+ operatorString += "%s";
+ else
+ operatorString += "'%s'";
+ break;
+ case OPERATOR_BEFORE:
+ case OPERATOR_LESS_THAN:
+ case OPERATOR_NOT_IN_THE_LAST:
+ operatorString = " < ";
+ if (GetFieldType(m_field) == REAL_FIELD || GetFieldType(m_field) == NUMERIC_FIELD ||
+ GetFieldType(m_field) == SECONDS_FIELD)
+ operatorString += "%s";
+ else
+ operatorString += "'%s'";
+ break;
+ case OPERATOR_TRUE:
+ operatorString = " = 1";
+ break;
+ case OPERATOR_FALSE:
+ operatorString = " = 0";
+ break;
+ default:
+ break;
+ }
+ }
+ return operatorString;
+}
+
+std::string CDatabaseQueryRule::GetWhereClause(const CDatabase& db,
+ const std::string& strType) const
+{
+ SEARCH_OPERATOR op = GetOperator(strType);
+
+ std::string operatorString = GetOperatorString(op);
+ std::string negate;
+ if (op == OPERATOR_DOES_NOT_CONTAIN || op == OPERATOR_FALSE ||
+ (op == OPERATOR_DOES_NOT_EQUAL && GetFieldType(m_field) != REAL_FIELD &&
+ GetFieldType(m_field) != NUMERIC_FIELD && GetFieldType(m_field) != SECONDS_FIELD))
+ negate = " NOT ";
+
+ // boolean operators don't have any values in m_parameter, they work on the operator
+ if (m_operator == OPERATOR_FALSE || m_operator == OPERATOR_TRUE)
+ return GetBooleanQuery(negate, strType);
+
+ // Process boolean field with (not) EQUAL/CONTAINS "true"/"false" parameter too
+ if (GetFieldType(m_field) == BOOLEAN_FIELD &&
+ (m_parameter[0] == "true" || m_parameter[0] == "false") &&
+ (op == OPERATOR_CONTAINS || op == OPERATOR_EQUALS || op == OPERATOR_DOES_NOT_CONTAIN ||
+ op == OPERATOR_DOES_NOT_EQUAL))
+ {
+ if (m_parameter[0] == "false")
+ {
+ if (!negate.empty())
+ negate.clear();
+ else
+ negate = " NOT ";
+ }
+ return GetBooleanQuery(negate, strType);
+ }
+
+ // The BETWEEN operator is handled special
+ if (op == OPERATOR_BETWEEN)
+ {
+ if (m_parameter.size() != 2)
+ return "";
+
+ FIELD_TYPE fieldType = GetFieldType(m_field);
+ if (fieldType == REAL_FIELD)
+ return db.PrepareSQL("%s BETWEEN %s AND %s", GetField(m_field, strType).c_str(),
+ m_parameter[0].c_str(), m_parameter[1].c_str());
+ else if (fieldType == NUMERIC_FIELD)
+ return db.PrepareSQL("CAST(%s as DECIMAL(5,1)) BETWEEN %s AND %s",
+ GetField(m_field, strType).c_str(), m_parameter[0].c_str(),
+ m_parameter[1].c_str());
+ else if (fieldType == SECONDS_FIELD)
+ return db.PrepareSQL("CAST(%s as INTEGER) BETWEEN %s AND %s",
+ GetField(m_field, strType).c_str(), m_parameter[0].c_str(),
+ m_parameter[1].c_str());
+ else
+ return db.PrepareSQL("%s BETWEEN '%s' AND '%s'", GetField(m_field, strType).c_str(),
+ m_parameter[0].c_str(), m_parameter[1].c_str());
+ }
+
+ // now the query parameter
+ std::string wholeQuery;
+ for (std::vector<std::string>::const_iterator it = m_parameter.begin(); it != m_parameter.end();
+ ++it)
+ {
+ std::string query = '(' + FormatWhereClause(negate, operatorString, *it, db, strType) + ')';
+
+ if (it + 1 != m_parameter.end())
+ {
+ if (negate.empty())
+ query += " OR ";
+ else
+ query += " AND ";
+ }
+
+ wholeQuery += query;
+ }
+
+ return wholeQuery;
+}
+
+std::string CDatabaseQueryRule::FormatWhereClause(const std::string& negate,
+ const std::string& oper,
+ const std::string& param,
+ const CDatabase& db,
+ const std::string& strType) const
+{
+ std::string parameter = FormatParameter(oper, param, db, strType);
+
+ std::string query;
+ if (m_field != 0)
+ {
+ std::string fmt = "{}";
+ if (GetFieldType(m_field) == NUMERIC_FIELD)
+ fmt = "CAST({} as DECIMAL(6,1))";
+ else if (GetFieldType(m_field) == SECONDS_FIELD)
+ fmt = "CAST({} as INTEGER)";
+
+ query = StringUtils::Format(fmt, GetField(m_field, strType));
+ query += negate + parameter;
+
+ // special case for matching parameters in fields that might be either empty or NULL.
+ if ((param.empty() && negate.empty()) || (!param.empty() && !negate.empty()))
+ query += " OR " + GetField(m_field, strType) + " IS NULL";
+ }
+
+ if (query == negate + parameter)
+ query = "1";
+ return query;
+}
+
+void CDatabaseQueryRuleCombination::clear()
+{
+ m_combinations.clear();
+ m_rules.clear();
+ m_type = CombinationAnd;
+}
+
+std::string CDatabaseQueryRuleCombination::GetWhereClause(const CDatabase& db,
+ const std::string& strType) const
+{
+ std::string rule;
+
+ // translate the combinations into SQL
+ for (CDatabaseQueryRuleCombinations::const_iterator it = m_combinations.begin();
+ it != m_combinations.end(); ++it)
+ {
+ if (it != m_combinations.begin())
+ rule += m_type == CombinationAnd ? " AND " : " OR ";
+ rule += "(" + (*it)->GetWhereClause(db, strType) + ")";
+ }
+
+ // translate the rules into SQL
+ for (const auto& it : m_rules)
+ {
+ if (!rule.empty())
+ rule += m_type == CombinationAnd ? " AND " : " OR ";
+ rule += "(";
+ std::string currentRule = it->GetWhereClause(db, strType);
+ // if we don't get a rule, we add '1' or '0' so the query is still valid and doesn't fail
+ if (currentRule.empty())
+ currentRule = m_type == CombinationAnd ? "'1'" : "'0'";
+ rule += currentRule;
+ rule += ")";
+ }
+
+ return rule;
+}
+
+bool CDatabaseQueryRuleCombination::Load(const CVariant& obj,
+ const IDatabaseQueryRuleFactory* factory)
+{
+ if (!obj.isObject() && !obj.isArray())
+ return false;
+
+ CVariant child;
+ if (obj.isObject())
+ {
+ if (obj.isMember("and") && obj["and"].isArray())
+ {
+ m_type = CombinationAnd;
+ child = obj["and"];
+ }
+ else if (obj.isMember("or") && obj["or"].isArray())
+ {
+ m_type = CombinationOr;
+ child = obj["or"];
+ }
+ else
+ return false;
+ }
+ else
+ child = obj;
+
+ for (CVariant::const_iterator_array it = child.begin_array(); it != child.end_array(); ++it)
+ {
+ if (!it->isObject())
+ continue;
+
+ if (it->isMember("and") || it->isMember("or"))
+ {
+ std::shared_ptr<CDatabaseQueryRuleCombination> combo(factory->CreateCombination());
+ if (combo && combo->Load(*it, factory))
+ m_combinations.push_back(combo);
+ }
+ else
+ {
+ std::shared_ptr<CDatabaseQueryRule> rule(factory->CreateRule());
+ if (rule && rule->Load(*it))
+ m_rules.push_back(rule);
+ }
+ }
+
+ return true;
+}
+
+bool CDatabaseQueryRuleCombination::Save(TiXmlNode* parent) const
+{
+ for (const auto& it : m_rules)
+ it->Save(parent);
+ return true;
+}
+
+bool CDatabaseQueryRuleCombination::Save(CVariant& obj) const
+{
+ if (!obj.isObject() || (m_combinations.empty() && m_rules.empty()))
+ return false;
+
+ CVariant comboArray(CVariant::VariantTypeArray);
+ if (!m_combinations.empty())
+ {
+ for (const auto& combo : m_combinations)
+ {
+ CVariant comboObj(CVariant::VariantTypeObject);
+ if (combo->Save(comboObj))
+ comboArray.push_back(comboObj);
+ }
+ }
+ if (!m_rules.empty())
+ {
+ for (const auto& rule : m_rules)
+ {
+ CVariant ruleObj(CVariant::VariantTypeObject);
+ if (rule->Save(ruleObj))
+ comboArray.push_back(ruleObj);
+ }
+ }
+
+ obj[TranslateCombinationType()] = comboArray;
+
+ return true;
+}
+
+std::string CDatabaseQueryRuleCombination::TranslateCombinationType() const
+{
+ return m_type == CombinationAnd ? "and" : "or";
+}
diff --git a/xbmc/dbwrappers/DatabaseQuery.h b/xbmc/dbwrappers/DatabaseQuery.h
new file mode 100644
index 0000000..a15f377
--- /dev/null
+++ b/xbmc/dbwrappers/DatabaseQuery.h
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2013-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 <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+#define DATABASEQUERY_RULE_VALUE_SEPARATOR " / "
+
+class CDatabase;
+class CVariant;
+class TiXmlNode;
+
+class CDatabaseQueryRule
+{
+public:
+ CDatabaseQueryRule();
+ virtual ~CDatabaseQueryRule() = default;
+
+ enum SEARCH_OPERATOR
+ {
+ OPERATOR_START = 0,
+ OPERATOR_CONTAINS,
+ OPERATOR_DOES_NOT_CONTAIN,
+ OPERATOR_EQUALS,
+ OPERATOR_DOES_NOT_EQUAL,
+ OPERATOR_STARTS_WITH,
+ OPERATOR_ENDS_WITH,
+ OPERATOR_GREATER_THAN,
+ OPERATOR_LESS_THAN,
+ OPERATOR_AFTER,
+ OPERATOR_BEFORE,
+ OPERATOR_IN_THE_LAST,
+ OPERATOR_NOT_IN_THE_LAST,
+ OPERATOR_TRUE,
+ OPERATOR_FALSE,
+ OPERATOR_BETWEEN,
+ OPERATOR_END
+ };
+
+ enum FIELD_TYPE
+ {
+ TEXT_FIELD = 0,
+ REAL_FIELD,
+ NUMERIC_FIELD,
+ DATE_FIELD,
+ PLAYLIST_FIELD,
+ SECONDS_FIELD,
+ BOOLEAN_FIELD,
+ TEXTIN_FIELD
+ };
+
+ virtual bool Load(const TiXmlNode* node, const std::string& encoding = "UTF-8");
+ virtual bool Load(const CVariant& obj);
+ virtual bool Save(TiXmlNode* parent) const;
+ virtual bool Save(CVariant& obj) const;
+
+ static std::string GetLocalizedOperator(SEARCH_OPERATOR oper);
+ static void GetAvailableOperators(std::vector<std::string>& operatorList);
+
+ std::string GetParameter() const;
+ void SetParameter(const std::string& value);
+ void SetParameter(const std::vector<std::string>& values);
+
+ virtual std::string GetWhereClause(const CDatabase& db, const std::string& strType) const;
+
+ int m_field;
+ SEARCH_OPERATOR m_operator;
+ std::vector<std::string> m_parameter;
+
+protected:
+ virtual std::string GetField(int field, const std::string& type) const = 0;
+ virtual FIELD_TYPE GetFieldType(int field) const = 0;
+ virtual int TranslateField(const char* field) const = 0;
+ virtual std::string TranslateField(int field) const = 0;
+ std::string ValidateParameter(const std::string& parameter) const;
+ virtual std::string FormatParameter(const std::string& negate,
+ const std::string& oper,
+ const CDatabase& db,
+ const std::string& type) const;
+ virtual std::string FormatWhereClause(const std::string& negate,
+ const std::string& oper,
+ const std::string& param,
+ const CDatabase& db,
+ const std::string& type) const;
+ virtual SEARCH_OPERATOR GetOperator(const std::string& type) const { return m_operator; }
+ virtual std::string GetOperatorString(SEARCH_OPERATOR op) const;
+ virtual std::string GetBooleanQuery(const std::string& negate, const std::string& strType) const
+ {
+ return "";
+ }
+
+ static SEARCH_OPERATOR TranslateOperator(const char* oper);
+ static std::string TranslateOperator(SEARCH_OPERATOR oper);
+};
+
+class CDatabaseQueryRuleCombination;
+
+typedef std::vector<std::shared_ptr<CDatabaseQueryRule>> CDatabaseQueryRules;
+typedef std::vector<std::shared_ptr<CDatabaseQueryRuleCombination>> CDatabaseQueryRuleCombinations;
+
+class IDatabaseQueryRuleFactory
+{
+public:
+ virtual ~IDatabaseQueryRuleFactory() = default;
+ virtual CDatabaseQueryRule* CreateRule() const = 0;
+ virtual CDatabaseQueryRuleCombination* CreateCombination() const = 0;
+};
+
+class CDatabaseQueryRuleCombination
+{
+public:
+ virtual ~CDatabaseQueryRuleCombination() = default;
+
+ typedef enum
+ {
+ CombinationOr = 0,
+ CombinationAnd
+ } Combination;
+
+ void clear();
+ virtual bool Load(const TiXmlNode* node, const std::string& encoding = "UTF-8") { return false; }
+ virtual bool Load(const CVariant& obj, const IDatabaseQueryRuleFactory* factory);
+ virtual bool Save(TiXmlNode* parent) const;
+ virtual bool Save(CVariant& obj) const;
+
+ std::string GetWhereClause(const CDatabase& db, const std::string& strType) const;
+ std::string TranslateCombinationType() const;
+
+ Combination GetType() const { return m_type; }
+ void SetType(Combination combination) { m_type = combination; }
+
+ bool empty() const { return m_combinations.empty() && m_rules.empty(); }
+
+protected:
+ friend class CGUIDialogSmartPlaylistEditor;
+ friend class CGUIDialogMediaFilter;
+
+ Combination m_type = CombinationAnd;
+ CDatabaseQueryRuleCombinations m_combinations;
+ CDatabaseQueryRules m_rules;
+};
diff --git a/xbmc/dbwrappers/dataset.cpp b/xbmc/dbwrappers/dataset.cpp
new file mode 100644
index 0000000..4b4acb4
--- /dev/null
+++ b/xbmc/dbwrappers/dataset.cpp
@@ -0,0 +1,625 @@
+/*
+ * Copyright (C) 2004, Leo Seib, Hannover
+ *
+ * Project: C++ Dynamic Library
+ * Module: Dataset abstraction layer realisation file
+ * Author: Leo Seib E-Mail: leoseib@web.de
+ * Begin: 5/04/2002
+ *
+ * SPDX-License-Identifier: MIT
+ * See LICENSES/README.md for more information.
+ */
+
+#include "dataset.h"
+
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cstring>
+
+#ifndef __GNUC__
+#pragma warning(disable : 4800)
+#endif
+
+namespace dbiplus
+{
+//************* Database implementation ***************
+
+Database::Database()
+ : error(), //S_NO_CONNECTION,
+ host(),
+ port(),
+ db(),
+ login(),
+ passwd(),
+ sequence_table("db_sequence")
+{
+ active = false; // No connection yet
+ compression = false;
+}
+
+Database::~Database()
+{
+ disconnect(); // Disconnect if connected to database
+}
+
+int Database::connectFull(const char* newHost,
+ const char* newPort,
+ const char* newDb,
+ const char* newLogin,
+ const char* newPasswd,
+ const char* newKey,
+ const char* newCert,
+ const char* newCA,
+ const char* newCApath,
+ const char* newCiphers,
+ bool newCompression)
+{
+ host = newHost;
+ port = newPort;
+ db = newDb;
+ login = newLogin;
+ passwd = newPasswd;
+ key = newKey;
+ cert = newCert;
+ ca = newCA;
+ capath = newCApath;
+ ciphers = newCiphers;
+ compression = newCompression;
+ return connect(true);
+}
+
+std::string Database::prepare(const char* format, ...)
+{
+ va_list args;
+ va_start(args, format);
+ std::string result = vprepare(format, args);
+ va_end(args);
+
+ return result;
+}
+
+//************* Dataset implementation ***************
+
+Dataset::Dataset() : select_sql("")
+{
+
+ db = NULL;
+ haveError = active = false;
+ frecno = 0;
+ fbof = feof = true;
+ autocommit = true;
+
+ fields_object = new Fields();
+
+ edit_object = new Fields();
+}
+
+Dataset::Dataset(Database* newDb) : select_sql("")
+{
+
+ db = newDb;
+ haveError = active = false;
+ frecno = 0;
+ fbof = feof = true;
+ autocommit = true;
+
+ fields_object = new Fields();
+
+ edit_object = new Fields();
+}
+
+Dataset::~Dataset()
+{
+ update_sql.clear();
+ insert_sql.clear();
+ delete_sql.clear();
+
+ delete fields_object;
+ delete edit_object;
+}
+
+void Dataset::setSqlParams(sqlType t, const char* sqlFrmt, ...)
+{
+ va_list ap;
+ char sqlCmd[DB_BUFF_MAX + 1];
+
+ va_start(ap, sqlFrmt);
+#ifndef TARGET_POSIX
+ _vsnprintf(sqlCmd, DB_BUFF_MAX - 1, sqlFrmt, ap);
+#else
+ vsnprintf(sqlCmd, DB_BUFF_MAX - 1, sqlFrmt, ap);
+#endif
+ va_end(ap);
+
+ switch (t)
+ {
+ case sqlSelect:
+ set_select_sql(sqlCmd);
+ break;
+ case sqlUpdate:
+ add_update_sql(sqlCmd);
+ break;
+ case sqlInsert:
+ add_insert_sql(sqlCmd);
+ break;
+ case sqlDelete:
+ add_delete_sql(sqlCmd);
+ break;
+ case sqlExec:
+ sql = sqlCmd;
+ break;
+ }
+}
+
+void Dataset::set_select_sql(const char* sel_sql)
+{
+ select_sql = sel_sql;
+}
+
+void Dataset::set_select_sql(const std::string& sel_sql)
+{
+ select_sql = sel_sql;
+}
+
+void Dataset::parse_sql(std::string& sql)
+{
+ std::string fpattern, by_what;
+ for (unsigned int i = 0; i < fields_object->size(); i++)
+ {
+ fpattern = ":OLD_" + (*fields_object)[i].props.name;
+ by_what = "'" + (*fields_object)[i].val.get_asString() + "'";
+ int idx = 0;
+ int next_idx = 0;
+ while ((idx = sql.find(fpattern, next_idx)) >= 0)
+ {
+ next_idx = idx + fpattern.size();
+ if (sql.length() > ((unsigned int)next_idx))
+ if (isalnum(sql[next_idx]) || sql[next_idx] == '_')
+ {
+ continue;
+ }
+ sql.replace(idx, fpattern.size(), by_what);
+ } //while
+ } //for
+
+ for (unsigned int i = 0; i < edit_object->size(); i++)
+ {
+ fpattern = ":NEW_" + (*edit_object)[i].props.name;
+ by_what = "'" + (*edit_object)[i].val.get_asString() + "'";
+ int idx = 0;
+ int next_idx = 0;
+ while ((idx = sql.find(fpattern, next_idx)) >= 0)
+ {
+ next_idx = idx + fpattern.size();
+ if (sql.length() > ((unsigned int)next_idx))
+ if (isalnum(sql[next_idx]) || sql[next_idx] == '_')
+ {
+ continue;
+ }
+ sql.replace(idx, fpattern.size(), by_what);
+ } //while
+ } //for
+}
+
+void Dataset::close(void)
+{
+ haveError = false;
+ frecno = 0;
+ fbof = feof = true;
+ active = false;
+
+ name2indexMap.clear();
+}
+
+bool Dataset::seek(int pos)
+{
+ frecno = (pos < num_rows() - 1) ? pos : num_rows() - 1;
+ frecno = (frecno < 0) ? 0 : frecno;
+ fbof = feof = (num_rows() == 0) ? true : false;
+ return ((bool)frecno);
+}
+
+void Dataset::refresh()
+{
+ int row = frecno;
+ if ((row != 0) && active)
+ {
+ close();
+ open();
+ seek(row);
+ }
+ else
+ open();
+}
+
+void Dataset::first()
+{
+ if (ds_state == dsSelect)
+ {
+ frecno = 0;
+ feof = fbof = (num_rows() > 0) ? false : true;
+ }
+}
+
+void Dataset::next()
+{
+ if (ds_state == dsSelect)
+ {
+ fbof = false;
+ if (frecno < num_rows() - 1)
+ {
+ frecno++;
+ feof = false;
+ }
+ else
+ feof = true;
+ if (num_rows() <= 0)
+ fbof = feof = true;
+ }
+}
+
+void Dataset::prev()
+{
+ if (ds_state == dsSelect)
+ {
+ feof = false;
+ if (frecno)
+ {
+ frecno--;
+ fbof = false;
+ }
+ else
+ fbof = true;
+ if (num_rows() <= 0)
+ fbof = feof = true;
+ }
+}
+
+void Dataset::last()
+{
+ if (ds_state == dsSelect)
+ {
+ frecno = (num_rows() > 0) ? num_rows() - 1 : 0;
+ feof = fbof = (num_rows() > 0) ? false : true;
+ }
+}
+
+bool Dataset::goto_rec(int pos)
+{
+ if (ds_state == dsSelect)
+ {
+ return seek(pos - 1);
+ }
+ return false;
+}
+
+void Dataset::insert()
+{
+ edit_object->resize(field_count());
+ for (int i = 0; i < field_count(); i++)
+ {
+ (*fields_object)[i].val = "";
+ (*edit_object)[i].val = "";
+ (*edit_object)[i].props = (*fields_object)[i].props;
+ }
+ ds_state = dsInsert;
+}
+
+void Dataset::edit()
+{
+ if (ds_state != dsSelect)
+ {
+ throw DbErrors("Editing is possible only when query exists!");
+ }
+ edit_object->resize(field_count());
+ for (unsigned int i = 0; i < fields_object->size(); i++)
+ {
+ (*edit_object)[i].props = (*fields_object)[i].props;
+ (*edit_object)[i].val = (*fields_object)[i].val;
+ }
+ ds_state = dsEdit;
+}
+
+void Dataset::post()
+{
+ if (ds_state == dsInsert)
+ make_insert();
+ else if (ds_state == dsEdit)
+ make_edit();
+}
+
+void Dataset::del()
+{
+ ds_state = dsDelete;
+}
+
+void Dataset::deletion()
+{
+ if (ds_state == dsDelete)
+ make_deletion();
+}
+
+bool Dataset::set_field_value(const char* f_name, const field_value& value)
+{
+ if ((ds_state == dsInsert) || (ds_state == dsEdit))
+ {
+ const int idx = fieldIndex(f_name);
+ if (idx >= 0)
+ {
+ (*edit_object)[idx].val = value;
+ return true;
+ }
+ throw DbErrors("Field not found: %s", f_name);
+ }
+ throw DbErrors("Not in Insert or Edit state");
+ // return false;
+}
+
+const field_value Dataset::get_field_value(const char* f_name)
+{
+ if (ds_state != dsInactive)
+ {
+ if (ds_state == dsEdit || ds_state == dsInsert)
+ {
+ const int idx = fieldIndex(f_name);
+ if (idx >= 0)
+ return (*edit_object)[idx].val;
+
+ throw DbErrors("Field not found: %s", f_name);
+ }
+ else
+ {
+ int idx = fieldIndex(f_name);
+ if (idx < 0)
+ {
+ const char* name = strstr(f_name, ".");
+ if (name)
+ name++;
+
+ if (name)
+ idx = fieldIndex(name);
+ }
+
+ if (idx >= 0)
+ return (*fields_object)[idx].val;
+
+ throw DbErrors("Field not found: %s", f_name);
+ }
+ }
+ throw DbErrors("Dataset state is Inactive");
+}
+
+const field_value Dataset::get_field_value(int index)
+{
+ if (ds_state != dsInactive)
+ {
+ if (ds_state == dsEdit || ds_state == dsInsert)
+ {
+ if (index < 0 || index >= field_count())
+ throw DbErrors("Field index not found: %d", index);
+
+ return (*edit_object)[index].val;
+ }
+ else
+ {
+ if (index < 0 || index >= field_count())
+ throw DbErrors("Field index not found: %d", index);
+
+ return (*fields_object)[index].val;
+ }
+ }
+ throw DbErrors("Dataset state is Inactive");
+}
+
+const sql_record* Dataset::get_sql_record()
+{
+ if (result.records.empty() || frecno >= (int)result.records.size())
+ return NULL;
+
+ return result.records[frecno];
+}
+
+const field_value Dataset::f_old(const char* f_name)
+{
+ if (ds_state != dsInactive)
+ for (int unsigned i = 0; i < fields_object->size(); i++)
+ if ((*fields_object)[i].props.name == f_name)
+ return (*fields_object)[i].val;
+ field_value fv;
+ return fv;
+}
+
+void Dataset::setParamList(const ParamList& params)
+{
+ plist = params;
+}
+
+bool Dataset::locate()
+{
+ bool result;
+ if (plist.empty())
+ return false;
+
+ std::map<std::string, field_value>::const_iterator i;
+ first();
+ while (!eof())
+ {
+ result = true;
+ for (i = plist.begin(); i != plist.end(); ++i)
+ if (fv(i->first.c_str()).get_asString() == i->second.get_asString())
+ {
+ continue;
+ }
+ else
+ {
+ result = false;
+ break;
+ }
+ if (result)
+ {
+ return result;
+ }
+ next();
+ }
+ return false;
+}
+
+bool Dataset::locate(const ParamList& params)
+{
+ plist = params;
+ return locate();
+}
+
+bool Dataset::findNext(void)
+{
+ bool result;
+ if (plist.empty())
+ return false;
+
+ std::map<std::string, field_value>::const_iterator i;
+ while (!eof())
+ {
+ result = true;
+ for (i = plist.begin(); i != plist.end(); ++i)
+ if (fv(i->first.c_str()).get_asString() == i->second.get_asString())
+ {
+ continue;
+ }
+ else
+ {
+ result = false;
+ break;
+ }
+ if (result)
+ {
+ return result;
+ }
+ next();
+ }
+ return false;
+}
+
+void Dataset::add_update_sql(const char* upd_sql)
+{
+ std::string s = upd_sql;
+ update_sql.push_back(s);
+}
+
+void Dataset::add_update_sql(const std::string& upd_sql)
+{
+ update_sql.push_back(upd_sql);
+}
+
+void Dataset::add_insert_sql(const char* ins_sql)
+{
+ std::string s = ins_sql;
+ insert_sql.push_back(s);
+}
+
+void Dataset::add_insert_sql(const std::string& ins_sql)
+{
+ insert_sql.push_back(ins_sql);
+}
+
+void Dataset::add_delete_sql(const char* del_sql)
+{
+ std::string s = del_sql;
+ delete_sql.push_back(s);
+}
+
+void Dataset::add_delete_sql(const std::string& del_sql)
+{
+ delete_sql.push_back(del_sql);
+}
+
+void Dataset::clear_update_sql()
+{
+ update_sql.clear();
+}
+
+void Dataset::clear_insert_sql()
+{
+ insert_sql.clear();
+}
+
+void Dataset::clear_delete_sql()
+{
+ delete_sql.clear();
+}
+
+size_t Dataset::insert_sql_count()
+{
+ return insert_sql.size();
+}
+
+size_t Dataset::delete_sql_count()
+{
+ return delete_sql.size();
+}
+
+int Dataset::field_count()
+{
+ return fields_object->size();
+}
+int Dataset::fieldCount()
+{
+ return fields_object->size();
+}
+
+const char* Dataset::fieldName(int n)
+{
+ if (n < field_count() && n >= 0)
+ return (*fields_object)[n].props.name.c_str();
+ else
+ return NULL;
+}
+
+char* Dataset::str_toLower(char* s)
+{
+ for (char* p = s; *p; p++)
+ *p = std::tolower(*p);
+
+ return s;
+}
+
+int Dataset::fieldIndex(const char* fn)
+{
+ std::string name(fn);
+ const auto it = name2indexMap.find(str_toLower(name.data()));
+ if (it != name2indexMap.end())
+ return (*it).second;
+ else
+ return -1;
+}
+
+//************* DbErrors implementation ***************
+
+DbErrors::DbErrors() : msg_("Unknown Database Error")
+{
+}
+
+DbErrors::DbErrors(const char* msg, ...)
+{
+ va_list vl;
+ va_start(vl, msg);
+ char buf[DB_BUFF_MAX] = "";
+#ifndef TARGET_POSIX
+ _vsnprintf(buf, DB_BUFF_MAX - 1, msg, vl);
+#else
+ vsnprintf(buf, DB_BUFF_MAX - 1, msg, vl);
+#endif
+ va_end(vl);
+ msg_ = "SQL: ";
+ msg_ += buf;
+
+ CLog::Log(LOGERROR, "{}", msg_);
+}
+
+const char* DbErrors::getMsg()
+{
+ return msg_.c_str();
+}
+
+} // namespace dbiplus
diff --git a/xbmc/dbwrappers/dataset.h b/xbmc/dbwrappers/dataset.h
new file mode 100644
index 0000000..5e420b1
--- /dev/null
+++ b/xbmc/dbwrappers/dataset.h
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2002, Leo Seib, Hannover
+ *
+ * Project:Dataset C++ Dynamic Library
+ * Module: Dataset abstraction layer header file
+ * Author: Leo Seib E-Mail: leoseib@web.de
+ * Begin: 5/04/2002
+ *
+ * SPDX-License-Identifier: MIT
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "qry_dat.h"
+
+#include <cstdio>
+#include <list>
+#include <map>
+#include <stdarg.h>
+#include <string>
+#include <unordered_map>
+
+namespace dbiplus
+{
+class Dataset; // forward declaration of class Dataset
+
+#define S_NO_CONNECTION "No active connection";
+
+#define DB_BUFF_MAX 8 * 1024 // Maximum buffer's capacity
+
+#define DB_CONNECTION_NONE 0
+#define DB_CONNECTION_OK 1
+#define DB_CONNECTION_BAD 2
+
+#define DB_COMMAND_OK 0 // OK - command executed
+#define DB_EMPTY_QUERY 1 // Query didn't return tuples
+#define DB_TUPLES_OK 2 // Query returned tuples
+#define DB_ERROR 5
+#define DB_BAD_RESPONSE 6
+#define DB_UNEXPECTED 7 // This shouldn't ever happen
+#define DB_UNEXPECTED_RESULT -1 //For integer functions
+
+/******************* Class Database definition ********************
+
+ represents connection with database server;
+
+******************************************************************/
+class Database
+{
+protected:
+ bool active;
+ bool compression;
+ std::string error, // Error description
+ host, port, db, login, passwd, //Login info
+ sequence_table, //Sequence table for nextid
+ default_charset, //Default character set
+ key, cert, ca, capath, ciphers; //SSL - Encryption info
+
+public:
+ /* constructor */
+ Database();
+ /* destructor */
+ virtual ~Database();
+ virtual Dataset* CreateDataset() const = 0;
+ /* sets a new host name */
+ virtual void setHostName(const char* newHost) { host = newHost; }
+ /* gets a host name */
+ const char* getHostName(void) const { return host.c_str(); }
+ /* sets a new port */
+ void setPort(const char* newPort) { port = newPort; }
+ /* gets a port */
+ const char* getPort(void) const { return port.c_str(); }
+ /* sets a new database name */
+ virtual void setDatabase(const char* newDb) { db = newDb; }
+ /* gets a database name */
+ const char* getDatabase(void) const { return db.c_str(); }
+ /* sets a new login to database */
+ void setLogin(const char* newLogin) { login = newLogin; }
+ /* gets a login */
+ const char* getLogin(void) const { return login.c_str(); }
+ /* sets a password */
+ void setPasswd(const char* newPasswd) { passwd = newPasswd; }
+ /* gets a password */
+ const char* getPasswd(void) const { return passwd.c_str(); }
+ /* active status is OK state */
+ virtual bool isActive(void) const { return active; }
+ /* Set new name of sequence table */
+ void setSequenceTable(const char* new_seq_table) { sequence_table = new_seq_table; }
+ /* Get name of sequence table */
+ const char* getSequenceTable(void) { return sequence_table.c_str(); }
+ /* Get the default character set */
+ const char* getDefaultCharset(void) { return default_charset.c_str(); }
+ /* Sets configuration */
+ virtual void setConfig(const char* newKey,
+ const char* newCert,
+ const char* newCA,
+ const char* newCApath,
+ const char* newCiphers,
+ bool newCompression)
+ {
+ key = newKey;
+ cert = newCert;
+ ca = newCA;
+ capath = newCApath;
+ ciphers = newCiphers;
+ compression = newCompression;
+ }
+
+ /* virtual methods that must be overloaded in derived classes */
+
+ virtual int init(void) { return DB_COMMAND_OK; }
+ virtual int status(void) { return DB_CONNECTION_NONE; }
+ virtual int setErr(int err_code, const char* qry) = 0;
+ virtual const char* getErrorMsg(void) { return error.c_str(); }
+
+ virtual int connect(bool create) { return DB_COMMAND_OK; }
+ virtual int connectFull(const char* newDb,
+ const char* newHost = NULL,
+ const char* newLogin = NULL,
+ const char* newPasswd = NULL,
+ const char* newPort = NULL,
+ const char* newKey = NULL,
+ const char* newCert = NULL,
+ const char* newCA = NULL,
+ const char* newCApath = NULL,
+ const char* newCiphers = NULL,
+ bool newCompression = false);
+ virtual void disconnect(void) { active = false; }
+ virtual int reset(void) { return DB_COMMAND_OK; }
+ virtual int create(void) { return DB_COMMAND_OK; }
+ virtual int drop(void) { return DB_COMMAND_OK; }
+ virtual long nextid(const char* seq_name) = 0;
+
+ /* \brief copy database */
+ virtual int copy(const char* new_name) { return -1; }
+
+ /* \brief drop all extra analytics from database */
+ virtual int drop_analytics(void) { return -1; }
+
+ virtual bool exists(void) { return false; }
+
+ /* virtual methods for transaction */
+
+ virtual void start_transaction() {}
+ virtual void commit_transaction() {}
+ virtual void rollback_transaction() {}
+
+ /* virtual methods for formatting */
+
+ /*! \brief Prepare a SQL statement for execution or querying using C printf nomenclature.
+ \param format - C printf compliant format string
+ \param ... - optional comma separated list of variables for substitution in format string placeholders.
+ \return escaped and formatted string.
+ */
+ virtual std::string prepare(const char* format, ...);
+
+ /*! \brief Prepare a SQL statement for execution or querying using C printf nomenclature
+ \param format - C printf compliant format string
+ \param args - va_list of variables for substitution in format string placeholders.
+ \return escaped and formatted string.
+ */
+ virtual std::string vprepare(const char* format, va_list args) = 0;
+
+ virtual bool in_transaction() { return false; }
+};
+
+/******************* Class Dataset definition *********************
+
+ global abstraction for using Databases
+
+******************************************************************/
+
+// define Dataset States type
+enum dsStates
+{
+ dsSelect,
+ dsInsert,
+ dsEdit,
+ dsUpdate,
+ dsDelete,
+ dsInactive
+};
+enum sqlType
+{
+ sqlSelect,
+ sqlUpdate,
+ sqlInsert,
+ sqlDelete,
+ sqlExec
+};
+
+typedef std::list<std::string> StringList;
+typedef std::map<std::string, field_value> ParamList;
+
+class Dataset
+{
+protected:
+ /* char *Host = ""; //WORK_HOST;
+ char *Database = ""; //WORK_DATABASE;
+ char *User = ""; //WORK_USER;
+ char *Password = ""; //WORK_PASSWORD;
+*/
+
+ Database* db; // info about db connection
+ dsStates ds_state; // current state
+ Fields *fields_object, *edit_object;
+ std::unordered_map<std::string, unsigned int>
+ name2indexMap; // Lower case field name -> database index
+
+ /* query results*/
+ result_set result;
+ result_set exec_res;
+ bool autorefresh;
+
+ bool active; // Is Query Opened?
+ bool haveError;
+ int frecno; // number of current row bei bewegung
+ std::string sql;
+
+ ParamList plist; // Paramlist for locate
+ bool fbof, feof;
+ bool autocommit; // for transactions
+
+ /* Variables to store SQL statements */
+ std::string empty_sql; // Executed when result set is empty
+ std::string select_sql; // May be only single string variable
+
+ StringList update_sql; // May be an array in complex queries
+ /* Field values for updating must has prefix :NEW_ and :OLD_ and field name
+ Example:
+ update wt_story set idobject set idobject=:NEW_idobject,body=:NEW_body
+ where idobject=:OLD_idobject
+ Essentially fields idobject and body must present in the
+ result set (select_sql statement) */
+
+ StringList insert_sql; // May be an array in complex queries
+ /* Field values for inserting must has prefix :NEW_ and field name
+ Example:
+ insert into wt_story (idobject, body) values (:NEW_idobject, :NEW_body)
+ Essentially fields idobject and body must present in the
+ result set (select_sql statement) */
+
+ StringList delete_sql; // May be an array in complex queries
+ /* Field values for deleing must has prefix :OLD_ and field name
+ Example:
+ delete from wt_story where idobject=:OLD_idobject
+ Essentially field idobject must present in the
+ result set (select_sql statement) */
+
+ /* Arrays for searching */
+ // StringList names, values;
+
+ /* Makes direct inserts into database via mysql_query function */
+ virtual void make_insert() = 0;
+ /* Edit SQL */
+ virtual void make_edit() = 0;
+ /* Delete SQL */
+ virtual void make_deletion() = 0;
+
+ /* This function works only with MySQL database
+ Filling the fields information from select statement */
+ virtual void fill_fields(void) = 0;
+
+ /* Parse Sql - replacing fields with prefixes :OLD_ and :NEW_ with current values of OLD or NEW field. */
+ void parse_sql(std::string& sql);
+
+ /* Returns old field value (for :OLD) */
+ virtual const field_value f_old(const char* f);
+
+ /* fast string tolower helper */
+ char* str_toLower(char* s);
+
+public:
+ /* constructor */
+ Dataset();
+ explicit Dataset(Database* newDb);
+
+ /* destructor */
+ virtual ~Dataset();
+
+ /* sets a new value of connection to database */
+ void setDatabase(Database* newDb) { db = newDb; }
+ /* retrieves a database which connected */
+ Database* getDatabase(void) { return db; }
+
+ /* sets a new query string to database server */
+ void setExecSql(const char* newSql) { sql = newSql; }
+ /* retrieves a query string */
+ const char* getExecSql(void) { return sql.c_str(); }
+
+ /* status active is OK query */
+ virtual bool isActive(void) { return active; }
+
+ virtual void setSqlParams(sqlType t, const char* sqlFrmt, ...);
+
+ /* error handling */
+ // virtual void halt(const char *msg);
+
+ /* last inserted id */
+ virtual int64_t lastinsertid() = 0;
+ /* sequence numbers */
+ virtual long nextid(const char* seq_name) = 0;
+ /* sequence numbers */
+ virtual int num_rows() = 0;
+
+ /* Open SQL query */
+ virtual void open(const std::string& sql) = 0;
+ virtual void open() = 0;
+ /* func. executes a query without results to return */
+ virtual int exec(const std::string& sql) = 0;
+ virtual int exec() = 0;
+ virtual const void* getExecRes() = 0;
+ /* as open, but with our query exec Sql */
+ virtual bool query(const std::string& sql) = 0;
+ /* Close SQL Query*/
+ virtual void close();
+ /* This function looks for field Field_name with value equal Field_value
+ Returns true if found (position of dataset is set to founded position)
+ and false another way (position is not changed). */
+ // virtual bool lookup(char *field_name, char*field_value);
+ /* Refresh dataset (reopen it and set the same cursor position) */
+ virtual void refresh();
+
+ /*! \brief Drop an index from the database table, provided it exists.
+ \param table - name of the table the index to be dropped is associated with
+ \param index - name of the index to be dropped
+ \return true when the index is guaranteed to no longer exist in the database.
+ */
+ virtual bool dropIndex(const char* table, const char* index) { return false; }
+
+ /* Go to record No (starting with 0) */
+ virtual bool seek(int pos = 0);
+ /* Go to record No (starting with 1) */
+ virtual bool goto_rec(int pos = 1);
+ /* Go to the first record in dataset */
+ virtual void first();
+ /* Go to next record in dataset */
+ virtual void next();
+ /* Go to previous record */
+ virtual void prev();
+ /* Go to last record in dataset */
+ virtual void last();
+
+ /* Check for Ending dataset */
+ virtual bool eof(void) { return feof; }
+ /* Check for Beginning dataset */
+ virtual bool bof(void) { return fbof; }
+
+ /* Start the insert mode */
+ virtual void insert();
+ /* Start the insert mode (alias for insert() function) */
+ virtual void append() { insert(); }
+ /* Start the edit mode */
+ virtual void edit();
+ /* Start the delete mode */
+ virtual void del();
+
+ /* Add changes, that were made during insert or edit states of dataset into the database */
+ virtual void post();
+ /* Delete statements from database */
+ virtual void deletion();
+ /* Cancel changes, made in insert or edit states of dataset */
+ virtual void cancel() {}
+ /* interrupt any pending database operation */
+ virtual void interrupt() {}
+
+ virtual void setParamList(const ParamList& params);
+ virtual bool locate();
+ virtual bool locate(const ParamList& params);
+ virtual bool findNext();
+
+ /* func. retrieves a number of fields */
+ /* Number of fields in a record */
+ virtual int field_count();
+ virtual int fieldCount();
+ /* func. retrieves a field name with 'n' index */
+ virtual const char* fieldName(int n);
+ /* func. retrieves a field index with 'fn' field name,return -1 when field name not found */
+ virtual int fieldIndex(const char* fn);
+
+ /* Set field value */
+ virtual bool set_field_value(const char* f_name, const field_value& value);
+ /* alias for set_field_value */
+ virtual bool sf(const char* f, const field_value& v) { return set_field_value(f, v); }
+
+ /* Return field name by it index */
+ // virtual char *field_name(int f_index) { return field_by_index(f_index)->get_field_name(); }
+
+ /* Getting value of field for current record */
+ virtual const field_value get_field_value(const char* f_name);
+ virtual const field_value get_field_value(int index);
+ /* Alias to get_field_value */
+ const field_value fv(const char* f) { return get_field_value(f); }
+ const field_value fv(int index) { return get_field_value(index); }
+
+ /* ------------ for transaction ------------------- */
+ void set_autocommit(bool v) { autocommit = v; }
+ bool get_autocommit() { return autocommit; }
+
+ /* ----------------- for debug -------------------- */
+ Fields* get_fields_object() { return fields_object; }
+ Fields* get_edit_object() { return edit_object; }
+
+ /* --------------- for fast access ---------------- */
+ const result_set& get_result_set() { return result; }
+ const sql_record* get_sql_record();
+
+private:
+ Dataset(const Dataset&) = delete;
+ Dataset& operator=(const Dataset&) = delete;
+
+ /* Get the column index from a string field_value request */
+ bool get_index_map_entry(const char* f_name);
+
+ void set_ds_state(dsStates new_state) { ds_state = new_state; }
+
+public:
+ /* return ds_state value */
+ dsStates get_state() { return ds_state; }
+
+ /*add a new value to select_sql*/
+ void set_select_sql(const char* sel_sql);
+ void set_select_sql(const std::string& select_sql);
+ /*add a new value to update_sql*/
+ void add_update_sql(const char* upd_sql);
+ void add_update_sql(const std::string& upd_sql);
+ /*add a new value to insert_sql*/
+ void add_insert_sql(const char* ins_sql);
+ void add_insert_sql(const std::string& ins_sql);
+ /*add a new value to delete_sql*/
+ void add_delete_sql(const char* del_sql);
+ void add_delete_sql(const std::string& del_sql);
+
+ /*clear update_sql*/
+ void clear_update_sql();
+ /*clear insert_sql*/
+ void clear_insert_sql();
+ /*clear delete_sql*/
+ void clear_delete_sql();
+
+ /* size of insert_sql*/
+ size_t insert_sql_count();
+ /* size of delete_sql*/
+ size_t delete_sql_count();
+
+ /*get value of select_sql*/
+ const char* get_select_sql();
+};
+
+/******************** Class DbErrors definition *********************
+
+ error handling
+
+******************************************************************/
+class DbErrors
+{
+
+public:
+ /* constructor */
+ DbErrors();
+ DbErrors(const char* msg, ...);
+
+ const char* getMsg();
+
+private:
+ std::string msg_;
+};
+
+} // namespace dbiplus
diff --git a/xbmc/dbwrappers/mysqldataset.cpp b/xbmc/dbwrappers/mysqldataset.cpp
new file mode 100644
index 0000000..a1a2d9d
--- /dev/null
+++ b/xbmc/dbwrappers/mysqldataset.cpp
@@ -0,0 +1,2099 @@
+/*
+ * 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 "mysqldataset.h"
+
+#include "Util.h"
+#include "network/DNSNameCache.h"
+#include "network/WakeOnAccess.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <array>
+#include <iostream>
+#include <set>
+#include <string>
+#ifdef HAS_MYSQL
+#include <mysql/errmsg.h>
+#elif defined(HAS_MARIADB)
+#include <mariadb/errmsg.h>
+#endif
+
+#ifdef TARGET_POSIX
+#include "platform/posix/ConvUtils.h"
+#endif
+
+#define MYSQL_OK 0
+#define ER_BAD_DB_ERROR 1049
+
+namespace dbiplus
+{
+
+//************* MysqlDatabase implementation ***************
+
+MysqlDatabase::MysqlDatabase()
+{
+
+ active = false;
+ _in_transaction = false; // for transaction
+
+ error = "Unknown database error"; //S_NO_CONNECTION;
+ host = "localhost";
+ port = "3306";
+ db = "mysql";
+ login = "root";
+ passwd = "null";
+ conn = NULL;
+ default_charset = "";
+}
+
+MysqlDatabase::~MysqlDatabase()
+{
+ disconnect();
+}
+
+Dataset* MysqlDatabase::CreateDataset() const
+{
+ return new MysqlDataset(const_cast<MysqlDatabase*>(this));
+}
+
+int MysqlDatabase::status(void)
+{
+ if (active == false)
+ return DB_CONNECTION_NONE;
+ return DB_CONNECTION_OK;
+}
+
+int MysqlDatabase::setErr(int err_code, const char* qry)
+{
+ switch (err_code)
+ {
+ case MYSQL_OK:
+ error = "Successful result";
+ break;
+ case CR_COMMANDS_OUT_OF_SYNC:
+ error = "Commands were executed in an improper order";
+ break;
+ case CR_SERVER_GONE_ERROR:
+ error = "The MySQL server has gone away";
+ break;
+ case CR_SERVER_LOST:
+ error = "The connection to the server was lost during this query";
+ break;
+ case CR_UNKNOWN_ERROR:
+ error = "An unknown error occurred";
+ break;
+ case 1146: /* ER_NO_SUCH_TABLE */
+ error = "The table does not exist";
+ break;
+ default:
+ char err[256];
+ snprintf(err, 256, "Undefined MySQL error: Code (%d)", err_code);
+ error = err;
+ }
+ error = "[" + db + "] " + error;
+ error += "\nQuery: ";
+ error += qry;
+ error += "\n";
+ return err_code;
+}
+
+const char* MysqlDatabase::getErrorMsg()
+{
+ return error.c_str();
+}
+
+void MysqlDatabase::configure_connection()
+{
+ char sqlcmd[512];
+ int ret;
+
+ // MySQL 5.7.5+: See #8393
+ strcpy(sqlcmd,
+ "SET SESSION sql_mode = (SELECT REPLACE(@@SESSION.sql_mode,'ONLY_FULL_GROUP_BY',''))");
+ if ((ret = mysql_real_query(conn, sqlcmd, strlen(sqlcmd))) != MYSQL_OK)
+ throw DbErrors("Can't disable sql_mode ONLY_FULL_GROUP_BY: '%s' (%d)", db.c_str(), ret);
+
+ // MySQL 5.7.6+: See #8393. Non-fatal if error, as not supported by MySQL 5.0.x
+ strcpy(sqlcmd, "SELECT @@SESSION.optimizer_switch");
+ if ((ret = mysql_real_query(conn, sqlcmd, strlen(sqlcmd))) == MYSQL_OK)
+ {
+ MYSQL_RES* res = mysql_store_result(conn);
+ MYSQL_ROW row;
+
+ if (res)
+ {
+ if ((row = mysql_fetch_row(res)) != NULL)
+ {
+ std::string column = row[0];
+ std::vector<std::string> split = StringUtils::Split(column, ',');
+
+ for (std::string& itIn : split)
+ {
+ if (StringUtils::Trim(itIn) == "derived_merge=on")
+ {
+ strcpy(sqlcmd, "SET SESSION optimizer_switch = 'derived_merge=off'");
+ if ((ret = mysql_real_query(conn, sqlcmd, strlen(sqlcmd))) != MYSQL_OK)
+ throw DbErrors("Can't set optimizer_switch = '%s': '%s' (%d)",
+ StringUtils::Trim(itIn).c_str(), db.c_str(), ret);
+ break;
+ }
+ }
+ }
+ mysql_free_result(res);
+ }
+ }
+ else
+ CLog::Log(LOGWARNING, "Unable to query optimizer_switch: '{}' ({})", db, ret);
+}
+
+int MysqlDatabase::connect(bool create_new)
+{
+ if (host.empty() || db.empty())
+ return DB_CONNECTION_NONE;
+
+ std::string resolvedHost;
+ if (!StringUtils::EqualsNoCase(host, "localhost") && CDNSNameCache::Lookup(host, resolvedHost))
+ {
+ CLog::Log(LOGDEBUG, "{} replacing configured host {} with resolved host {}", __FUNCTION__, host,
+ resolvedHost);
+ host = resolvedHost;
+ }
+
+ try
+ {
+ disconnect();
+
+ if (conn == NULL)
+ {
+ conn = mysql_init(conn);
+ mysql_ssl_set(conn, key.empty() ? NULL : key.c_str(), cert.empty() ? NULL : cert.c_str(),
+ ca.empty() ? NULL : ca.c_str(), capath.empty() ? NULL : capath.c_str(),
+ ciphers.empty() ? NULL : ciphers.c_str());
+ }
+
+ if (!CWakeOnAccess::GetInstance().WakeUpHost(host, "MySQL : " + db))
+ return DB_CONNECTION_NONE;
+
+ // establish connection with just user credentials
+ if (mysql_real_connect(conn, host.c_str(), login.c_str(), passwd.c_str(), NULL,
+ atoi(port.c_str()), NULL, compression ? CLIENT_COMPRESS : 0) != NULL)
+ {
+ static bool showed_ver_info = false;
+ if (!showed_ver_info)
+ {
+ std::string version_string = mysql_get_server_info(conn);
+ CLog::Log(LOGINFO, "MYSQL: Connected to version {}", version_string);
+ showed_ver_info = true;
+ unsigned long version = mysql_get_server_version(conn);
+ // Minimum for MySQL: 5.6 (5.5 is EOL)
+ unsigned long min_version = 50600;
+ if (version_string.find("MariaDB") != std::string::npos)
+ {
+ // Minimum for MariaDB: 5.5 (still supported)
+ min_version = 50500;
+ }
+
+ if (version < min_version)
+ {
+ CLog::Log(
+ LOGWARNING,
+ "MYSQL: Your database server version {} is very old and might not be supported in "
+ "future Kodi versions. Please consider upgrading to MySQL 5.7 or MariaDB 10.2.",
+ version_string);
+ }
+ }
+
+ // disable mysql autocommit since we handle it
+ //mysql_autocommit(conn, false);
+
+ // enforce utf8 charset usage
+ default_charset = mysql_character_set_name(conn);
+ if (mysql_set_character_set(conn, "utf8")) // returns 0 on success
+ {
+ CLog::Log(LOGERROR, "Unable to set utf8 charset: {} [{}]({})", db, mysql_errno(conn),
+ mysql_error(conn));
+ }
+
+ configure_connection();
+
+ // check existence
+ if (exists())
+ {
+ // nothing to see here
+ }
+ else if (create_new)
+ {
+ char sqlcmd[512];
+ int ret;
+
+ snprintf(sqlcmd, sizeof(sqlcmd),
+ "CREATE DATABASE `%s` CHARACTER SET utf8 COLLATE utf8_general_ci", db.c_str());
+ if ((ret = query_with_reconnect(sqlcmd)) != MYSQL_OK)
+ {
+ throw DbErrors("Can't create new database: '%s' (%d)", db.c_str(), ret);
+ }
+ }
+
+ if (mysql_select_db(conn, db.c_str()) == 0)
+ {
+ active = true;
+ return DB_CONNECTION_OK;
+ }
+ }
+
+ // if we failed above, either credentials were incorrect or the database didn't exist
+ if (mysql_errno(conn) == ER_BAD_DB_ERROR && create_new)
+ {
+
+ if (create() == MYSQL_OK)
+ {
+ active = true;
+
+ return DB_CONNECTION_OK;
+ }
+ }
+
+ CLog::Log(LOGERROR, "Unable to open database: {} [{}]({})", db, mysql_errno(conn),
+ mysql_error(conn));
+
+ return DB_CONNECTION_NONE;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Unable to open database: {} ({})", db, GetLastError());
+ }
+ return DB_CONNECTION_NONE;
+}
+
+void MysqlDatabase::disconnect(void)
+{
+ if (conn != NULL)
+ {
+ mysql_close(conn);
+ conn = NULL;
+ }
+
+ active = false;
+}
+
+int MysqlDatabase::create()
+{
+ return connect(true);
+}
+
+int MysqlDatabase::drop()
+{
+ if (!active)
+ throw DbErrors("Can't drop database: no active connection...");
+ char sqlcmd[512];
+ int ret;
+ snprintf(sqlcmd, sizeof(sqlcmd), "DROP DATABASE `%s`", db.c_str());
+ if ((ret = query_with_reconnect(sqlcmd)) != MYSQL_OK)
+ {
+ throw DbErrors("Can't drop database: '%s' (%d)", db.c_str(), ret);
+ }
+ disconnect();
+ return DB_COMMAND_OK;
+}
+
+int MysqlDatabase::copy(const char* backup_name)
+{
+ if (!active || conn == NULL)
+ throw DbErrors("Can't copy database: no active connection...");
+
+ char sql[4096];
+ int ret;
+
+ // ensure we're connected to the db we are about to copy
+ if ((ret = mysql_select_db(conn, db.c_str())) != MYSQL_OK)
+ throw DbErrors("Can't connect to source database: '%s'", db.c_str());
+
+ // grab a list of base tables only (no views)
+ sprintf(sql, "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'");
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ throw DbErrors("Can't determine base tables for copy.");
+
+ // get list of all tables from old DB
+ MYSQL_RES* res = mysql_store_result(conn);
+
+ if (res)
+ {
+ if (mysql_num_rows(res) == 0)
+ {
+ mysql_free_result(res);
+ throw DbErrors("The source database was unexpectedly empty.");
+ }
+
+ // create the new database
+ snprintf(sql, sizeof(sql), "CREATE DATABASE `%s` CHARACTER SET utf8 COLLATE utf8_general_ci",
+ backup_name);
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ {
+ mysql_free_result(res);
+ throw DbErrors("Can't create database for copy: '%s' (%d)", db.c_str(), ret);
+ }
+
+ MYSQL_ROW row;
+
+ // duplicate each table from old db to new db
+ while ((row = mysql_fetch_row(res)) != NULL)
+ {
+ // copy the table definition
+ snprintf(sql, sizeof(sql), "CREATE TABLE `%s`.%s LIKE %s", backup_name, row[0], row[0]);
+
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ {
+ mysql_free_result(res);
+ throw DbErrors("Can't copy schema for table '%s'\nError: %d", row[0], ret);
+ }
+
+ // copy the table data
+ snprintf(sql, sizeof(sql), "INSERT INTO `%s`.%s SELECT * FROM %s", backup_name, row[0],
+ row[0]);
+
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ {
+ mysql_free_result(res);
+ throw DbErrors("Can't copy data for table '%s'\nError: %d", row[0], ret);
+ }
+ }
+ mysql_free_result(res);
+
+ // we don't recreate views, indices, or triggers on copy
+ // as we'll be dropping and recreating them anyway
+ }
+
+ return 1;
+}
+
+int MysqlDatabase::drop_analytics(void)
+{
+ if (!active || conn == NULL)
+ throw DbErrors("Can't clean database: no active connection...");
+
+ char sql[4096];
+ int ret;
+
+ // ensure we're connected to the db we are about to clean from stuff
+ if ((ret = mysql_select_db(conn, db.c_str())) != MYSQL_OK)
+ throw DbErrors("Can't connect to database: '%s'", db.c_str());
+
+ // getting a list of indexes in the database
+ snprintf(sql, sizeof(sql),
+ "SELECT DISTINCT table_name, index_name "
+ "FROM information_schema.statistics "
+ "WHERE index_name != 'PRIMARY' AND table_schema = '%s'",
+ db.c_str());
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ throw DbErrors("Can't determine list of indexes to drop.");
+
+ // we will acquire lists here
+ MYSQL_RES* res = mysql_store_result(conn);
+ MYSQL_ROW row;
+
+ if (res)
+ {
+ while ((row = mysql_fetch_row(res)) != NULL)
+ {
+ snprintf(sql, sizeof(sql), "ALTER TABLE `%s`.%s DROP INDEX %s", db.c_str(), row[0], row[1]);
+
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ {
+ mysql_free_result(res);
+ throw DbErrors("Can't drop index '%s'\nError: %d", row[0], ret);
+ }
+ }
+ mysql_free_result(res);
+ }
+
+ // next topic is a views list
+ snprintf(sql, sizeof(sql),
+ "SELECT table_name FROM information_schema.views WHERE table_schema = '%s'", db.c_str());
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ throw DbErrors("Can't determine list of views to drop.");
+
+ res = mysql_store_result(conn);
+
+ if (res)
+ {
+ while ((row = mysql_fetch_row(res)) != NULL)
+ {
+ /* we do not need IF EXISTS because these views are exist */
+ snprintf(sql, sizeof(sql), "DROP VIEW `%s`.%s", db.c_str(), row[0]);
+
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ {
+ mysql_free_result(res);
+ throw DbErrors("Can't drop view '%s'\nError: %d", row[0], ret);
+ }
+ }
+ mysql_free_result(res);
+ }
+
+ // triggers
+ snprintf(sql, sizeof(sql),
+ "SELECT trigger_name FROM information_schema.triggers WHERE event_object_schema = '%s'",
+ db.c_str());
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ throw DbErrors("Can't determine list of triggers to drop.");
+
+ res = mysql_store_result(conn);
+
+ if (res)
+ {
+ while ((row = mysql_fetch_row(res)) != NULL)
+ {
+ snprintf(sql, sizeof(sql), "DROP TRIGGER `%s`.%s", db.c_str(), row[0]);
+
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ {
+ mysql_free_result(res);
+ throw DbErrors("Can't drop trigger '%s'\nError: %d", row[0], ret);
+ }
+ }
+ mysql_free_result(res);
+ }
+
+ // Native functions
+ snprintf(sql, sizeof(sql),
+ "SELECT routine_name FROM information_schema.routines "
+ "WHERE routine_type = 'FUNCTION' and routine_schema = '%s'",
+ db.c_str());
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ throw DbErrors("Can't determine list of routines to drop.");
+
+ res = mysql_store_result(conn);
+
+ if (res)
+ {
+ while ((row = mysql_fetch_row(res)) != NULL)
+ {
+ snprintf(sql, sizeof(sql), "DROP FUNCTION `%s`.%s", db.c_str(), row[0]);
+
+ if ((ret = query_with_reconnect(sql)) != MYSQL_OK)
+ {
+ mysql_free_result(res);
+ throw DbErrors("Can't drop function '%s'\nError: %d", row[0], ret);
+ }
+ }
+ mysql_free_result(res);
+ }
+
+ return 1;
+}
+
+int MysqlDatabase::query_with_reconnect(const char* query)
+{
+ int attempts = 5;
+ int result;
+
+ // try to reconnect if server is gone
+ while (((result = mysql_real_query(conn, query, strlen(query))) != MYSQL_OK) &&
+ ((result = mysql_errno(conn)) == CR_SERVER_GONE_ERROR || result == CR_SERVER_LOST) &&
+ (attempts-- > 0))
+ {
+ CLog::Log(LOGINFO, "MYSQL server has gone. Will try {} more attempt(s) to reconnect.",
+ attempts);
+ active = false;
+ connect(true);
+ }
+
+ return result;
+}
+
+long MysqlDatabase::nextid(const char* sname)
+{
+ CLog::Log(LOGDEBUG, "MysqlDatabase::nextid for {}", sname);
+ if (!active)
+ return DB_UNEXPECTED_RESULT;
+ const char* seq_table = "sys_seq";
+ int id; /*,nrow,ncol;*/
+ MYSQL_RES* res;
+ char sqlcmd[512];
+ snprintf(sqlcmd, sizeof(sqlcmd), "SELECT nextid FROM %s WHERE seq_name = '%s'", seq_table, sname);
+ CLog::Log(LOGDEBUG, "MysqlDatabase::nextid will request");
+ if ((last_err = query_with_reconnect(sqlcmd)) != 0)
+ {
+ return DB_UNEXPECTED_RESULT;
+ }
+ res = mysql_store_result(conn);
+ if (res)
+ {
+ if (mysql_num_rows(res) == 0)
+ {
+ id = 1;
+ snprintf(sqlcmd, sizeof(sqlcmd), "INSERT INTO %s (nextid,seq_name) VALUES (%d,'%s')",
+ seq_table, id, sname);
+ mysql_free_result(res);
+ if ((last_err = query_with_reconnect(sqlcmd)) != 0)
+ return DB_UNEXPECTED_RESULT;
+ return id;
+ }
+ else
+ {
+ id = -1;
+ snprintf(sqlcmd, sizeof(sqlcmd), "UPDATE %s SET nextid=%d WHERE seq_name = '%s'", seq_table,
+ id, sname);
+ mysql_free_result(res);
+ if ((last_err = query_with_reconnect(sqlcmd)) != 0)
+ return DB_UNEXPECTED_RESULT;
+ return id;
+ }
+ }
+ return DB_UNEXPECTED_RESULT;
+}
+
+// methods for transactions
+// ---------------------------------------------
+void MysqlDatabase::start_transaction()
+{
+ if (active)
+ {
+ mysql_autocommit(conn, false);
+ CLog::Log(LOGDEBUG, "Mysql Start transaction");
+ _in_transaction = true;
+ }
+}
+
+void MysqlDatabase::commit_transaction()
+{
+ if (active)
+ {
+ mysql_commit(conn);
+ mysql_autocommit(conn, true);
+ CLog::Log(LOGDEBUG, "Mysql commit transaction");
+ _in_transaction = false;
+ }
+}
+
+void MysqlDatabase::rollback_transaction()
+{
+ if (active)
+ {
+ mysql_rollback(conn);
+ mysql_autocommit(conn, true);
+ CLog::Log(LOGDEBUG, "Mysql rollback transaction");
+ _in_transaction = false;
+ }
+}
+
+bool MysqlDatabase::exists(void)
+{
+ bool ret = false;
+
+ if (conn == NULL || mysql_ping(conn))
+ {
+ CLog::Log(LOGERROR, "Not connected to database, test of existence is not possible.");
+ return ret;
+ }
+
+ MYSQL_RES* result = mysql_list_dbs(conn, db.c_str());
+ if (result == NULL)
+ {
+ CLog::Log(LOGERROR, "Database is not present, does the user has CREATE DATABASE permission");
+ return false;
+ }
+
+ ret = (mysql_num_rows(result) > 0);
+ mysql_free_result(result);
+
+ // Check if there is some tables ( to permit user with no create database rights
+ if (ret)
+ {
+ result = mysql_list_tables(conn, NULL);
+ if (result != NULL)
+ ret = (mysql_num_rows(result) > 0);
+
+ mysql_free_result(result);
+ }
+
+ return ret;
+}
+
+// methods for formatting
+// ---------------------------------------------
+std::string MysqlDatabase::vprepare(const char* format, va_list args)
+{
+ std::string strFormat = format;
+ std::string strResult = "";
+ size_t pos;
+
+ // %q is the sqlite format string for %s.
+ // Any bad character, like "'", will be replaced with a proper one
+ pos = 0;
+ while ((pos = strFormat.find("%s", pos)) != std::string::npos)
+ strFormat.replace(pos++, 2, "%q");
+
+ strResult = mysql_vmprintf(strFormat.c_str(), args);
+ // RAND() is the mysql form of RANDOM()
+ pos = 0;
+ while ((pos = strResult.find("RANDOM()", pos)) != std::string::npos)
+ {
+ strResult.replace(pos++, 8, "RAND()");
+ pos += 6;
+ }
+
+ // Replace some dataypes in CAST statements:
+ // before: CAST(iFoo AS TEXT), CAST(foo AS INTEGER)
+ // after: CAST(iFoo AS CHAR), CAST(foo AS SIGNED INTEGER)
+ pos = strResult.find("CAST(");
+ while (pos != std::string::npos)
+ {
+ size_t pos2 = strResult.find(" AS TEXT)", pos + 1);
+ if (pos2 != std::string::npos)
+ strResult.replace(pos2, 9, " AS CHAR)");
+ else
+ {
+ pos2 = strResult.find(" AS INTEGER)", pos + 1);
+ if (pos2 != std::string::npos)
+ strResult.replace(pos2, 12, " AS SIGNED INTEGER)");
+ }
+ pos = strResult.find("CAST(", pos + 1);
+ }
+
+ // Remove COLLATE NOCASE the SQLite case insensitive collation.
+ // In MySQL all tables are defined with case insensitive collation utf8_general_ci
+ pos = 0;
+ while ((pos = strResult.find(" COLLATE NOCASE", pos)) != std::string::npos)
+ strResult.erase(pos++, 15);
+
+ // Remove COLLATE ALPHANUM the SQLite custom collation.
+ pos = 0;
+ while ((pos = strResult.find(" COLLATE ALPHANUM", pos)) != std::string::npos)
+ strResult.erase(pos++, 15);
+
+ return strResult;
+}
+
+/* vsprintf() functionality is based on sqlite3.c functions */
+
+/*
+** Conversion types fall into various categories as defined by the
+** following enumeration.
+*/
+#define etRADIX 1 /* Integer types. %d, %x, %o, and so forth */
+#define etFLOAT 2 /* Floating point. %f */
+#define etEXP 3 /* Exponential notation. %e and %E */
+#define etGENERIC 4 /* Floating or exponential, depending on exponent. %g */
+#define etSIZE 5 /* Return number of characters processed so far. %n */
+#define etSTRING 6 /* Strings. %s */
+#define etDYNSTRING 7 /* Dynamically allocated strings. %z */
+#define etPERCENT 8 /* Percent symbol. %% */
+#define etCHARX 9 /* Characters. %c */
+/* The rest are extensions, not normally found in printf() */
+#define etSQLESCAPE 10 /* Strings with '\'' doubled. Strings with '\\' escaped. %q */
+#define etSQLESCAPE2 \
+ 11 /* Strings with '\'' doubled and enclosed in '',
+ NULL pointers replaced by SQL NULL. %Q */
+#define etPOINTER 14 /* The %p conversion */
+#define etSQLESCAPE3 15 /* %w -> Strings with '\"' doubled */
+
+#define etINVALID 0 /* Any unrecognized conversion type */
+
+/*
+** An "etByte" is an 8-bit unsigned value.
+*/
+typedef unsigned char etByte;
+
+/*
+** Each builtin conversion character (ex: the 'd' in "%d") is described
+** by an instance of the following structure
+*/
+typedef struct et_info
+{ /* Information about each format field */
+ char fmttype; /* The format field code letter */
+ etByte base; /* The base for radix conversion */
+ etByte flags; /* One or more of FLAG_ constants below */
+ etByte type; /* Conversion paradigm */
+ etByte charset; /* Offset into aDigits[] of the digits string */
+ etByte prefix; /* Offset into aPrefix[] of the prefix string */
+} et_info;
+
+/*
+** An objected used to accumulate the text of a string where we
+** do not necessarily know how big the string will be in the end.
+*/
+struct StrAccum
+{
+ char* zBase; /* A base allocation. Not from malloc. */
+ char* zText; /* The string collected so far */
+ int nChar; /* Length of the string so far */
+ int nAlloc; /* Amount of space allocated in zText */
+ int mxAlloc; /* Maximum allowed string length */
+ bool mallocFailed; /* Becomes true if any memory allocation fails */
+ bool tooBig; /* Becomes true if string size exceeds limits */
+};
+
+/*
+** Allowed values for et_info.flags
+*/
+#define FLAG_SIGNED 1 /* True if the value to convert is signed */
+#define FLAG_INTERN 2 /* True if for internal use only */
+#define FLAG_STRING 4 /* Allow infinity precision */
+
+/*
+** The following table is searched linearly, so it is good to put the
+** most frequently used conversion types first.
+*/
+static const char aDigits[] = "0123456789ABCDEF0123456789abcdef";
+static const char aPrefix[] = "-x0\000X0";
+// clang-format off
+constexpr std::array<et_info, 20> fmtinfo = {{
+ {'d', 10, 1, etRADIX, 0, 0},
+ {'s', 0, 4, etSTRING, 0, 0},
+ {'g', 0, 1, etGENERIC, 30, 0},
+ {'z', 0, 4, etDYNSTRING, 0, 0},
+ {'q', 0, 4, etSQLESCAPE, 0, 0},
+ {'Q', 0, 4, etSQLESCAPE2, 0, 0},
+ {'w', 0, 4, etSQLESCAPE3, 0, 0},
+ {'c', 0, 0, etCHARX, 0, 0},
+ {'o', 8, 0, etRADIX, 0, 2},
+ {'u', 10, 0, etRADIX, 0, 0},
+ {'x', 16, 0, etRADIX, 16, 1},
+ {'X', 16, 0, etRADIX, 0, 4},
+ {'f', 0, 1, etFLOAT, 0, 0},
+ {'e', 0, 1, etEXP, 30, 0},
+ {'E', 0, 1, etEXP, 14, 0},
+ {'G', 0, 1, etGENERIC, 14, 0},
+ {'i', 10, 1, etRADIX, 0, 0},
+ {'n', 0, 0, etSIZE, 0, 0},
+ {'%', 0, 0, etPERCENT, 0, 0},
+ {'p', 16, 0, etPOINTER, 0, 1},
+}};
+// clang-format on
+
+/*
+** "*val" is a double such that 0.1 <= *val < 10.0
+** Return the ascii code for the leading digit of *val, then
+** multiply "*val" by 10.0 to renormalize.
+**
+** Example:
+** input: *val = 3.14159
+** output: *val = 1.4159 function return = '3'
+**
+** The counter *cnt is incremented each time. After counter exceeds
+** 16 (the number of significant digits in a 64-bit float) '0' is
+** always returned.
+*/
+char MysqlDatabase::et_getdigit(double* val, int* cnt)
+{
+ int digit;
+ double d;
+ if ((*cnt)++ >= 16)
+ return '0';
+ digit = (int)*val;
+ d = digit;
+ digit += '0';
+ *val = (*val - d) * 10.0;
+ return (char)digit;
+}
+
+/*
+** Append N space characters to the given string buffer.
+*/
+void MysqlDatabase::appendSpace(StrAccum* pAccum, int N)
+{
+ static const char zSpaces[] = " ";
+ while (N >= (int)sizeof(zSpaces) - 1)
+ {
+ mysqlStrAccumAppend(pAccum, zSpaces, sizeof(zSpaces) - 1);
+ N -= sizeof(zSpaces) - 1;
+ }
+ if (N > 0)
+ {
+ mysqlStrAccumAppend(pAccum, zSpaces, N);
+ }
+}
+
+#ifndef MYSQL_PRINT_BUF_SIZE
+#define MYSQL_PRINT_BUF_SIZE 350
+#endif
+
+#define etBUFSIZE MYSQL_PRINT_BUF_SIZE /* Size of the output buffer */
+
+/*
+** The maximum length of a TEXT or BLOB in bytes. This also
+** limits the size of a row in a table or index.
+**
+** The hard limit is the ability of a 32-bit signed integer
+** to count the size: 2^31-1 or 2147483647.
+*/
+#ifndef MYSQL_MAX_LENGTH
+#define MYSQL_MAX_LENGTH 1000000000
+#endif
+
+/*
+** The root program. All variations call this core.
+**
+** INPUTS:
+** func This is a pointer to a function taking three arguments
+** 1. A pointer to anything. Same as the "arg" parameter.
+** 2. A pointer to the list of characters to be output
+** (Note, this list is NOT null terminated.)
+** 3. An integer number of characters to be output.
+** (Note: This number might be zero.)
+**
+** arg This is the pointer to anything which will be passed as the
+** first argument to "func". Use it for whatever you like.
+**
+** fmt This is the format string, as in the usual print.
+**
+** ap This is a pointer to a list of arguments. Same as in
+** vfprint.
+**
+** OUTPUTS:
+** The return value is the total number of characters sent to
+** the function "func". Returns -1 on a error.
+**
+** Note that the order in which automatic variables are declared below
+** seems to make a big difference in determining how fast this beast
+** will run.
+*/
+void MysqlDatabase::mysqlVXPrintf(StrAccum* pAccum, /* Accumulate results here */
+ int useExtended, /* Allow extended %-conversions */
+ const char* fmt, /* Format string */
+ va_list ap /* arguments */
+)
+{
+ int c; /* Next character in the format string */
+ char* bufpt; /* Pointer to the conversion buffer */
+ int precision; /* Precision of the current field */
+ int length; /* Length of the field */
+ int idx; /* A general purpose loop counter */
+ int width; /* Width of the current field */
+ etByte flag_leftjustify; /* True if "-" flag is present */
+ etByte flag_plussign; /* True if "+" flag is present */
+ etByte flag_blanksign; /* True if " " flag is present */
+ etByte flag_alternateform; /* True if "#" flag is present */
+ etByte flag_altform2; /* True if "!" flag is present */
+ etByte flag_zeropad; /* True if field width constant starts with zero */
+ etByte flag_long; /* True if "l" flag is present */
+ etByte flag_longlong; /* True if the "ll" flag is present */
+ etByte done; /* Loop termination flag */
+ uint64_t longvalue; /* Value for integer types */
+ double realvalue; /* Value for real types */
+ const et_info* infop; /* Pointer to the appropriate info structure */
+ char buf[etBUFSIZE]; /* Conversion buffer */
+ char prefix; /* Prefix character. "+" or "-" or " " or '\0'. */
+ etByte xtype = 0; /* Conversion paradigm */
+ char* zExtra; /* Extra memory used for etTCLESCAPE conversions */
+ int exp, e2; /* exponent of real numbers */
+ double rounder; /* Used for rounding floating point values */
+ etByte flag_dp; /* True if decimal point should be shown */
+ etByte flag_rtz; /* True if trailing zeros should be removed */
+ etByte flag_exp; /* True to force display of the exponent */
+ int nsd; /* Number of significant digits returned */
+
+ length = 0;
+ bufpt = 0;
+ for (; (c = (*fmt)) != 0; ++fmt)
+ {
+ bool isLike = false;
+ if (c != '%')
+ {
+ int amt;
+ bufpt = const_cast<char*>(fmt);
+ amt = 1;
+ while ((c = (*++fmt)) != '%' && c != 0)
+ amt++;
+ isLike = mysqlStrAccumAppend(pAccum, bufpt, amt);
+ if (c == 0)
+ break;
+ }
+ if ((c = (*++fmt)) == 0)
+ {
+ mysqlStrAccumAppend(pAccum, "%", 1);
+ break;
+ }
+ /* Find out what flags are present */
+ flag_leftjustify = flag_plussign = flag_blanksign = flag_alternateform = flag_altform2 =
+ flag_zeropad = 0;
+ done = 0;
+ do
+ {
+ switch (c)
+ {
+ case '-':
+ flag_leftjustify = 1;
+ break;
+ case '+':
+ flag_plussign = 1;
+ break;
+ case ' ':
+ flag_blanksign = 1;
+ break;
+ case '#':
+ flag_alternateform = 1;
+ break;
+ case '!':
+ flag_altform2 = 1;
+ break;
+ case '0':
+ flag_zeropad = 1;
+ break;
+ default:
+ done = 1;
+ break;
+ }
+ } while (!done && (c = (*++fmt)) != 0);
+ /* Get the field width */
+ width = 0;
+ if (c == '*')
+ {
+ width = va_arg(ap, int);
+ if (width < 0)
+ {
+ flag_leftjustify = 1;
+ width = -width;
+ }
+ c = *++fmt;
+ }
+ else
+ {
+ while (c >= '0' && c <= '9')
+ {
+ width = width * 10 + c - '0';
+ c = *++fmt;
+ }
+ }
+ if (width > etBUFSIZE - 10)
+ {
+ width = etBUFSIZE - 10;
+ }
+ /* Get the precision */
+ if (c == '.')
+ {
+ precision = 0;
+ c = *++fmt;
+ if (c == '*')
+ {
+ precision = va_arg(ap, int);
+ if (precision < 0)
+ precision = -precision;
+ c = *++fmt;
+ }
+ else
+ {
+ while (c >= '0' && c <= '9')
+ {
+ precision = precision * 10 + c - '0';
+ c = *++fmt;
+ }
+ }
+ }
+ else
+ {
+ precision = -1;
+ }
+ /* Get the conversion type modifier */
+ if (c == 'l')
+ {
+ flag_long = 1;
+ c = *++fmt;
+ if (c == 'l')
+ {
+ flag_longlong = 1;
+ c = *++fmt;
+ }
+ else
+ {
+ flag_longlong = 0;
+ }
+ }
+ else
+ {
+ flag_long = flag_longlong = 0;
+ }
+ /* Fetch the info entry for the field */
+ infop = fmtinfo.data();
+ xtype = etINVALID;
+
+ for (const auto& info : fmtinfo)
+ {
+ if (c != info.fmttype)
+ continue;
+
+ infop = &info;
+
+ if (useExtended || (infop->flags & FLAG_INTERN) == 0)
+ {
+ xtype = infop->type;
+ }
+ else
+ {
+ return;
+ }
+
+ break;
+ }
+
+ zExtra = 0;
+
+ /* Limit the precision to prevent overflowing buf[] during conversion */
+ if (precision > etBUFSIZE - 40 && (infop->flags & FLAG_STRING) == 0)
+ {
+ precision = etBUFSIZE - 40;
+ }
+
+ /*
+ ** At this point, variables are initialized as follows:
+ **
+ ** flag_alternateform TRUE if a '#' is present.
+ ** flag_altform2 TRUE if a '!' is present.
+ ** flag_plussign TRUE if a '+' is present.
+ ** flag_leftjustify TRUE if a '-' is present or if the
+ ** field width was negative.
+ ** flag_zeropad TRUE if the width began with 0.
+ ** flag_long TRUE if the letter 'l' (ell) prefixed
+ ** the conversion character.
+ ** flag_longlong TRUE if the letter 'll' (ell ell) prefixed
+ ** the conversion character.
+ ** flag_blanksign TRUE if a ' ' is present.
+ ** width The specified field width. This is
+ ** always non-negative. Zero is the default.
+ ** precision The specified precision. The default
+ ** is -1.
+ ** xtype The class of the conversion.
+ ** infop Pointer to the appropriate info struct.
+ */
+ switch (xtype)
+ {
+ case etPOINTER:
+ flag_longlong = sizeof(char*) == sizeof(int64_t);
+ flag_long = sizeof(char*) == sizeof(long int);
+ /* Fall through into the next case */
+ [[fallthrough]];
+ case etRADIX:
+ if (infop->flags & FLAG_SIGNED)
+ {
+ int64_t v;
+ if (flag_longlong)
+ {
+ v = va_arg(ap, int64_t);
+ }
+ else if (flag_long)
+ {
+ v = va_arg(ap, long int);
+ }
+ else
+ {
+ v = va_arg(ap, int);
+ }
+ if (v < 0)
+ {
+ longvalue = -v;
+ prefix = '-';
+ }
+ else
+ {
+ longvalue = v;
+ if (flag_plussign)
+ prefix = '+';
+ else if (flag_blanksign)
+ prefix = ' ';
+ else
+ prefix = 0;
+ }
+ }
+ else
+ {
+ if (flag_longlong)
+ {
+ longvalue = va_arg(ap, uint64_t);
+ }
+ else if (flag_long)
+ {
+ longvalue = va_arg(ap, unsigned long int);
+ }
+ else
+ {
+ longvalue = va_arg(ap, unsigned int);
+ }
+ prefix = 0;
+ }
+ if (longvalue == 0)
+ flag_alternateform = 0;
+ if (flag_zeropad && precision < width - (prefix != 0))
+ {
+ precision = width - (prefix != 0);
+ }
+ bufpt = &buf[etBUFSIZE - 1];
+ {
+ const char* cset;
+ int base;
+ cset = &aDigits[infop->charset];
+ base = infop->base;
+ do
+ { /* Convert to ascii */
+ *(--bufpt) = cset[longvalue % base];
+ longvalue = longvalue / base;
+ } while (longvalue > 0);
+ }
+ length = (int)(&buf[etBUFSIZE - 1] - bufpt);
+ for (idx = precision - length; idx > 0; idx--)
+ {
+ *(--bufpt) = '0'; /* Zero pad */
+ }
+ if (prefix)
+ *(--bufpt) = prefix; /* Add sign */
+ if (flag_alternateform && infop->prefix)
+ { /* Add "0" or "0x" */
+ const char* pre;
+ char x;
+ pre = &aPrefix[infop->prefix];
+ for (; (x = (*pre)) != 0; pre++)
+ *(--bufpt) = x;
+ }
+ length = (int)(&buf[etBUFSIZE - 1] - bufpt);
+ bufpt[length] = 0;
+ break;
+ case etFLOAT:
+ case etEXP:
+ case etGENERIC:
+ realvalue = va_arg(ap, double);
+ if (precision < 0)
+ precision = 6; /* Set default precision */
+ if (precision > etBUFSIZE / 2 - 10)
+ precision = etBUFSIZE / 2 - 10;
+ if (realvalue < 0.0)
+ {
+ realvalue = -realvalue;
+ prefix = '-';
+ }
+ else
+ {
+ if (flag_plussign)
+ prefix = '+';
+ else if (flag_blanksign)
+ prefix = ' ';
+ else
+ prefix = 0;
+ }
+ if (xtype == etGENERIC && precision > 0)
+ precision--;
+ /* It makes more sense to use 0.5 */
+ for (idx = precision, rounder = 0.5; idx > 0; idx--, rounder *= 0.1)
+ {
+ }
+ if (xtype == etFLOAT)
+ realvalue += rounder;
+ /* Normalize realvalue to within 10.0 > realvalue >= 1.0 */
+ exp = 0;
+#if 0
+ if( mysqlIsNaN((double)realvalue) ){
+ bufpt = "NaN";
+ length = 3;
+ break;
+ }
+#endif
+ if (realvalue > 0.0)
+ {
+ while (realvalue >= 1e32 && exp <= 350)
+ {
+ realvalue *= 1e-32;
+ exp += 32;
+ }
+ while (realvalue >= 1e8 && exp <= 350)
+ {
+ realvalue *= 1e-8;
+ exp += 8;
+ }
+ while (realvalue >= 10.0 && exp <= 350)
+ {
+ realvalue *= 0.1;
+ exp++;
+ }
+ while (realvalue < 1e-8)
+ {
+ realvalue *= 1e8;
+ exp -= 8;
+ }
+ while (realvalue < 1.0)
+ {
+ realvalue *= 10.0;
+ exp--;
+ }
+ if (exp > 350)
+ {
+ if (prefix == '-')
+ {
+ bufpt = const_cast<char*>("-Inf");
+ }
+ else if (prefix == '+')
+ {
+ bufpt = const_cast<char*>("+Inf");
+ }
+ else
+ {
+ bufpt = const_cast<char*>("Inf");
+ }
+ length = strlen(bufpt);
+ break;
+ }
+ }
+ bufpt = buf;
+ /*
+ ** If the field type is etGENERIC, then convert to either etEXP
+ ** or etFLOAT, as appropriate.
+ */
+ flag_exp = xtype == etEXP;
+ if (xtype != etFLOAT)
+ {
+ realvalue += rounder;
+ if (realvalue >= 10.0)
+ {
+ realvalue *= 0.1;
+ exp++;
+ }
+ }
+ if (xtype == etGENERIC)
+ {
+ flag_rtz = !flag_alternateform;
+ if (exp < -4 || exp > precision)
+ {
+ xtype = etEXP;
+ }
+ else
+ {
+ precision = precision - exp;
+ xtype = etFLOAT;
+ }
+ }
+ else
+ {
+ flag_rtz = 0;
+ }
+ if (xtype == etEXP)
+ {
+ e2 = 0;
+ }
+ else
+ {
+ e2 = exp;
+ }
+ nsd = 0;
+ flag_dp = (precision > 0 ? 1 : 0) | flag_alternateform | flag_altform2;
+ /* The sign in front of the number */
+ if (prefix)
+ {
+ *(bufpt++) = prefix;
+ }
+ /* Digits prior to the decimal point */
+ if (e2 < 0)
+ {
+ *(bufpt++) = '0';
+ }
+ else
+ {
+ for (; e2 >= 0; e2--)
+ {
+ *(bufpt++) = et_getdigit(&realvalue, &nsd);
+ }
+ }
+ /* The decimal point */
+ if (flag_dp)
+ {
+ *(bufpt++) = '.';
+ }
+ /* "0" digits after the decimal point but before the first
+ ** significant digit of the number */
+ for (e2++; e2 < 0; precision--, e2++)
+ {
+ //ASSERT( precision>0 );
+ *(bufpt++) = '0';
+ }
+ /* Significant digits after the decimal point */
+ while ((precision--) > 0)
+ {
+ *(bufpt++) = et_getdigit(&realvalue, &nsd);
+ }
+ /* Remove trailing zeros and the "." if no digits follow the "." */
+ if (flag_rtz && flag_dp)
+ {
+ while (bufpt[-1] == '0')
+ *(--bufpt) = 0;
+ //ASSERT( bufpt>buf );
+ if (bufpt[-1] == '.')
+ {
+ if (flag_altform2)
+ {
+ *(bufpt++) = '0';
+ }
+ else
+ {
+ *(--bufpt) = 0;
+ }
+ }
+ }
+ /* Add the "eNNN" suffix */
+ if (flag_exp || xtype == etEXP)
+ {
+ *(bufpt++) = aDigits[infop->charset];
+ if (exp < 0)
+ {
+ *(bufpt++) = '-';
+ exp = -exp;
+ }
+ else
+ {
+ *(bufpt++) = '+';
+ }
+ if (exp >= 100)
+ {
+ *(bufpt++) = (char)((exp / 100) + '0'); /* 100's digit */
+ exp %= 100;
+ }
+ *(bufpt++) = (char)(exp / 10 + '0'); /* 10's digit */
+ *(bufpt++) = (char)(exp % 10 + '0'); /* 1's digit */
+ }
+ *bufpt = 0;
+
+ /* The converted number is in buf[] and zero terminated. Output it.
+ ** Note that the number is in the usual order, not reversed as with
+ ** integer conversions. */
+ length = (int)(bufpt - buf);
+ bufpt = buf;
+
+ /* Special case: Add leading zeros if the flag_zeropad flag is
+ ** set and we are not left justified */
+ if (flag_zeropad && !flag_leftjustify && length < width)
+ {
+ int i;
+ int nPad = width - length;
+ for (i = width; i >= nPad; i--)
+ {
+ bufpt[i] = bufpt[i - nPad];
+ }
+ i = prefix != 0;
+ while (nPad--)
+ bufpt[i++] = '0';
+ length = width;
+ }
+ break;
+ case etSIZE:
+ *(va_arg(ap, int*)) = pAccum->nChar;
+ length = width = 0;
+ break;
+ case etPERCENT:
+ buf[0] = '%';
+ bufpt = buf;
+ length = 1;
+ break;
+ case etCHARX:
+ c = va_arg(ap, int);
+ buf[0] = (char)c;
+ if (precision >= 0)
+ {
+ for (idx = 1; idx < precision; idx++)
+ buf[idx] = (char)c;
+ length = precision;
+ }
+ else
+ {
+ length = 1;
+ }
+ bufpt = buf;
+ break;
+ case etSTRING:
+ case etDYNSTRING:
+ bufpt = va_arg(ap, char*);
+ if (bufpt == 0)
+ {
+ bufpt = const_cast<char*>("");
+ }
+ else if (xtype == etDYNSTRING)
+ {
+ zExtra = bufpt;
+ }
+ if (precision >= 0)
+ {
+ for (length = 0; length < precision && bufpt[length]; length++)
+ {
+ }
+ }
+ else
+ {
+ length = strlen(bufpt);
+ }
+ break;
+ case etSQLESCAPE:
+ case etSQLESCAPE2:
+ case etSQLESCAPE3:
+ {
+ int i, j, k, n, isnull;
+ int needQuote;
+ char ch;
+ char q = ((xtype == etSQLESCAPE3) ? '"' : '\''); /* Quote character */
+ std::string arg = va_arg(ap, char*);
+ if (isLike)
+ StringUtils::Replace(arg, "\\", "\\\\");
+ const char* escarg = arg.c_str();
+
+ isnull = escarg == 0;
+ if (isnull)
+ escarg = (xtype == etSQLESCAPE2 ? "NULL" : "(NULL)");
+ k = precision;
+ for (i = 0; k != 0 && (ch = escarg[i]) != 0; i++, k--)
+ ;
+ needQuote = !isnull && xtype == etSQLESCAPE2;
+ n = i * 2 + 1 + needQuote * 2;
+ if (n > etBUFSIZE)
+ {
+ bufpt = zExtra = (char*)malloc(n);
+ if (bufpt == 0)
+ {
+ pAccum->mallocFailed = true;
+ return;
+ }
+ }
+ else
+ {
+ bufpt = buf;
+ }
+ j = 0;
+ if (needQuote)
+ bufpt[j++] = q;
+ k = i;
+ j += mysql_real_escape_string(conn, bufpt, escarg, k);
+ if (needQuote)
+ bufpt[j++] = q;
+ bufpt[j] = 0;
+ length = j;
+ /* The precision in %q and %Q means how many input characters to
+ ** consume, not the length of the output...
+ ** if( precision>=0 && precision<length ) length = precision; */
+ break;
+ }
+ default:
+ {
+ return;
+ }
+ } /* End switch over the format type */
+ /*
+ ** The text of the conversion is pointed to by "bufpt" and is
+ ** "length" characters long. The field width is "width". Do
+ ** the output.
+ */
+ if (!flag_leftjustify)
+ {
+ int nspace;
+ nspace = width - length;
+ if (nspace > 0)
+ {
+ appendSpace(pAccum, nspace);
+ }
+ }
+ if (length > 0)
+ {
+ mysqlStrAccumAppend(pAccum, bufpt, length);
+ }
+ if (flag_leftjustify)
+ {
+ int nspace;
+ nspace = width - length;
+ if (nspace > 0)
+ {
+ appendSpace(pAccum, nspace);
+ }
+ }
+ if (zExtra)
+ {
+ free(zExtra);
+ }
+ } /* End for loop over the format string */
+} /* End of function */
+
+/*
+** Append N bytes of text from z to the StrAccum object.
+*/
+bool MysqlDatabase::mysqlStrAccumAppend(StrAccum* p, const char* z, int N)
+{
+ if (p->tooBig | p->mallocFailed)
+ {
+ return false;
+ }
+ if (N < 0)
+ {
+ N = strlen(z);
+ }
+ if (N == 0 || z == 0)
+ {
+ return false;
+ }
+ if (p->nChar + N >= p->nAlloc)
+ {
+ char* zNew;
+ int szNew = p->nChar;
+ szNew += N + 1;
+ if (szNew > p->mxAlloc)
+ {
+ mysqlStrAccumReset(p);
+ p->tooBig = true;
+ return false;
+ }
+ else
+ {
+ p->nAlloc = szNew;
+ }
+ zNew = (char*)malloc(p->nAlloc);
+ if (zNew)
+ {
+ memcpy(zNew, p->zText, p->nChar);
+ mysqlStrAccumReset(p);
+ p->zText = zNew;
+ }
+ else
+ {
+ p->mallocFailed = true;
+ mysqlStrAccumReset(p);
+ return false;
+ }
+ }
+
+ bool isLike = false;
+ std::string testString(z, N);
+ if (testString.find("LIKE") != std::string::npos || testString.find("like") != std::string::npos)
+ {
+ CLog::Log(LOGDEBUG,
+ "This query part contains a like, we will double backslash in the next field: {}",
+ testString);
+ isLike = true;
+ }
+
+ memcpy(&p->zText[p->nChar], z, N);
+ p->nChar += N;
+ return isLike;
+}
+
+/*
+** Finish off a string by making sure it is zero-terminated.
+** Return a pointer to the resulting string. Return a NULL
+** pointer if any kind of error was encountered.
+*/
+char* MysqlDatabase::mysqlStrAccumFinish(StrAccum* p)
+{
+ if (p->zText)
+ {
+ p->zText[p->nChar] = 0;
+ if (p->zText == p->zBase)
+ {
+ p->zText = (char*)malloc(p->nChar + 1);
+ if (p->zText)
+ {
+ memcpy(p->zText, p->zBase, p->nChar + 1);
+ }
+ else
+ {
+ p->mallocFailed = true;
+ }
+ }
+ }
+ return p->zText;
+}
+
+/*
+** Reset an StrAccum string. Reclaim all malloced memory.
+*/
+void MysqlDatabase::mysqlStrAccumReset(StrAccum* p)
+{
+ if (p->zText != p->zBase)
+ {
+ free(p->zText);
+ }
+ p->zText = 0;
+}
+
+/*
+** Initialize a string accumulator
+*/
+void MysqlDatabase::mysqlStrAccumInit(StrAccum* p, char* zBase, int n, int mx)
+{
+ p->zText = p->zBase = zBase;
+ p->nChar = 0;
+ p->nAlloc = n;
+ p->mxAlloc = mx;
+ p->tooBig = false;
+ p->mallocFailed = false;
+}
+
+/*
+** Print into memory obtained from mysql_malloc(). Omit the internal
+** %-conversion extensions.
+*/
+std::string MysqlDatabase::mysql_vmprintf(const char* zFormat, va_list ap)
+{
+ char zBase[MYSQL_PRINT_BUF_SIZE];
+ StrAccum acc;
+
+ mysqlStrAccumInit(&acc, zBase, sizeof(zBase), MYSQL_MAX_LENGTH);
+ mysqlVXPrintf(&acc, 0, zFormat, ap);
+ return mysqlStrAccumFinish(&acc);
+}
+
+//************* MysqlDataset implementation ***************
+
+MysqlDataset::MysqlDataset() : Dataset()
+{
+ haveError = false;
+ db = NULL;
+ autorefresh = false;
+}
+
+MysqlDataset::MysqlDataset(MysqlDatabase* newDb) : Dataset(newDb)
+{
+ haveError = false;
+ db = newDb;
+ autorefresh = false;
+}
+
+MysqlDataset::~MysqlDataset()
+{
+}
+
+void MysqlDataset::set_autorefresh(bool val)
+{
+ autorefresh = val;
+}
+
+//--------- protected functions implementation -----------------//
+
+MYSQL* MysqlDataset::handle()
+{
+ if (db != NULL)
+ {
+ return static_cast<MysqlDatabase*>(db)->getHandle();
+ }
+
+ return NULL;
+}
+
+void MysqlDataset::make_query(StringList& _sql)
+{
+ std::string query;
+ if (db == NULL)
+ throw DbErrors("No Database Connection");
+ try
+ {
+ if (autocommit)
+ db->start_transaction();
+
+ for (const std::string& i : _sql)
+ {
+ query = i;
+ Dataset::parse_sql(query);
+ if ((static_cast<MysqlDatabase*>(db)->query_with_reconnect(query.c_str())) != MYSQL_OK)
+ {
+ throw DbErrors(db->getErrorMsg());
+ }
+ } // end of for
+
+ if (db->in_transaction() && autocommit)
+ db->commit_transaction();
+
+ active = true;
+ ds_state = dsSelect;
+ if (autorefresh)
+ refresh();
+ } // end of try
+ catch (...)
+ {
+ if (db->in_transaction())
+ db->rollback_transaction();
+ throw;
+ }
+}
+
+void MysqlDataset::make_insert()
+{
+ make_query(insert_sql);
+ last();
+}
+
+void MysqlDataset::make_edit()
+{
+ make_query(update_sql);
+}
+
+void MysqlDataset::make_deletion()
+{
+ make_query(delete_sql);
+}
+
+void MysqlDataset::fill_fields()
+{
+ if ((db == NULL) || (result.record_header.empty()) ||
+ (result.records.size() < (unsigned int)frecno))
+ return;
+
+ if (fields_object->size() == 0) // Filling columns name
+ {
+ const unsigned int ncols = result.record_header.size();
+ fields_object->resize(ncols);
+ for (unsigned int i = 0; i < ncols; i++)
+ {
+ (*fields_object)[i].props = result.record_header[i];
+ std::string name = result.record_header[i].name;
+ name2indexMap.insert({str_toLower(name.data()), i});
+ }
+ }
+
+ //Filling result
+ if (result.records.size() != 0)
+ {
+ const sql_record* row = result.records[frecno];
+ if (row)
+ {
+ const unsigned int ncols = row->size();
+ fields_object->resize(ncols);
+ for (unsigned int i = 0; i < ncols; i++)
+ (*fields_object)[i].val = row->at(i);
+ return;
+ }
+ }
+ const unsigned int ncols = result.record_header.size();
+ fields_object->resize(ncols);
+ for (unsigned int i = 0; i < ncols; i++)
+ (*fields_object)[i].val = "";
+}
+
+//------------- public functions implementation -----------------//
+bool MysqlDataset::dropIndex(const char* table, const char* index)
+{
+ std::string sql;
+ std::string sql_prepared;
+
+ sql = "SELECT * FROM information_schema.statistics WHERE TABLE_SCHEMA=DATABASE() AND "
+ "table_name='%s' AND index_name='%s'";
+ sql_prepared = static_cast<MysqlDatabase*>(db)->prepare(sql.c_str(), table, index);
+
+ if (!query(sql_prepared))
+ return false;
+
+ if (num_rows())
+ {
+ sql = "ALTER TABLE %s DROP INDEX %s";
+ sql_prepared = static_cast<MysqlDatabase*>(db)->prepare(sql.c_str(), table, index);
+
+ if (exec(sql_prepared) != MYSQL_OK)
+ return false;
+ }
+
+ return true;
+}
+
+static bool ci_test(char l, char r)
+{
+ return tolower(l) == tolower(r);
+}
+
+static size_t ci_find(const std::string& where, const std::string& what)
+{
+ std::string::const_iterator loc =
+ std::search(where.begin(), where.end(), what.begin(), what.end(), ci_test);
+ if (loc == where.end())
+ return std::string::npos;
+ else
+ return loc - where.begin();
+}
+
+int MysqlDataset::exec(const std::string& sql)
+{
+ if (!handle())
+ throw DbErrors("No Database Connection");
+ std::string qry = sql;
+ int res = 0;
+ exec_res.clear();
+
+ // enforce the "auto_increment" keyword to be appended to "integer primary key"
+ size_t loc;
+
+ if ((loc = ci_find(qry, "integer primary key")) != std::string::npos)
+ {
+ qry = qry.insert(loc + 19, " auto_increment ");
+ }
+
+ // force the charset and collation to UTF-8
+ if (ci_find(qry, "CREATE TABLE") != std::string::npos ||
+ ci_find(qry, "CREATE TEMPORARY TABLE") != std::string::npos)
+ {
+ // If CREATE TABLE ... SELECT Syntax is used we need to add the encoding after the table before the select
+ // e.g. CREATE TABLE x CHARACTER SET utf8 COLLATE utf8_general_ci [AS] SELECT * FROM y
+ if ((loc = qry.find(" AS SELECT ")) != std::string::npos ||
+ (loc = qry.find(" SELECT ")) != std::string::npos)
+ {
+ qry = qry.insert(loc, " CHARACTER SET utf8 COLLATE utf8_general_ci");
+ }
+ else
+ qry += " CHARACTER SET utf8 COLLATE utf8_general_ci";
+ }
+
+ CLog::Log(LOGDEBUG, "Mysql execute: {}", qry);
+
+ if (db->setErr(static_cast<MysqlDatabase*>(db)->query_with_reconnect(qry.c_str()), qry.c_str()) !=
+ MYSQL_OK)
+ {
+ throw DbErrors(db->getErrorMsg());
+ }
+ else
+ {
+ //! @todo collect results and store in exec_res
+ return res;
+ }
+}
+
+int MysqlDataset::exec()
+{
+ return exec(sql);
+}
+
+const void* MysqlDataset::getExecRes()
+{
+ return &exec_res;
+}
+
+bool MysqlDataset::query(const std::string& query)
+{
+ if (!handle())
+ throw DbErrors("No Database Connection");
+ std::string qry = query;
+ int fs = qry.find("select");
+ int fS = qry.find("SELECT");
+ if (!(fs >= 0 || fS >= 0))
+ throw DbErrors("MUST be select SQL!");
+
+ close();
+
+ size_t loc;
+
+ // mysql doesn't understand CAST(foo as integer) => change to CAST(foo as signed integer)
+ while ((loc = ci_find(qry, "as integer)")) != std::string::npos)
+ qry = qry.insert(loc + 3, "signed ");
+
+ MYSQL_RES* stmt = NULL;
+
+ if (static_cast<MysqlDatabase*>(db)->setErr(
+ static_cast<MysqlDatabase*>(db)->query_with_reconnect(qry.c_str()), qry.c_str()) !=
+ MYSQL_OK)
+ throw DbErrors(db->getErrorMsg());
+
+ MYSQL* conn = handle();
+ stmt = mysql_store_result(conn);
+ if (stmt == NULL)
+ throw DbErrors("Missing result set!");
+
+ // column headers
+ const unsigned int numColumns = mysql_num_fields(stmt);
+ MYSQL_FIELD* fields = mysql_fetch_fields(stmt);
+ MYSQL_ROW row;
+ result.record_header.resize(numColumns);
+ for (unsigned int i = 0; i < numColumns; i++)
+ result.record_header[i].name = fields[i].name;
+
+ // returned rows
+ while ((row = mysql_fetch_row(stmt)))
+ { // have a row of data
+ sql_record* res = new sql_record;
+ res->resize(numColumns);
+ for (unsigned int i = 0; i < numColumns; i++)
+ {
+ field_value& v = res->at(i);
+ switch (fields[i].type)
+ {
+ case MYSQL_TYPE_LONGLONG:
+ if (row[i] != nullptr)
+ {
+ v.set_asInt64(strtoll(row[i], nullptr, 10));
+ }
+ else
+ {
+ v.set_asInt64(0);
+ }
+ break;
+ case MYSQL_TYPE_DECIMAL:
+ case MYSQL_TYPE_NEWDECIMAL:
+ case MYSQL_TYPE_TINY:
+ case MYSQL_TYPE_SHORT:
+ case MYSQL_TYPE_INT24:
+ case MYSQL_TYPE_LONG:
+ if (row[i] != NULL)
+ {
+ v.set_asInt(atoi(row[i]));
+ }
+ else
+ {
+ v.set_asInt(0);
+ }
+ break;
+ case MYSQL_TYPE_FLOAT:
+ case MYSQL_TYPE_DOUBLE:
+ if (row[i] != NULL)
+ {
+ v.set_asDouble(atof(row[i]));
+ }
+ else
+ {
+ v.set_asDouble(0);
+ }
+ break;
+ case MYSQL_TYPE_STRING:
+ case MYSQL_TYPE_VAR_STRING:
+ case MYSQL_TYPE_VARCHAR:
+ if (row[i] != NULL)
+ v.set_asString((const char*)row[i]);
+ break;
+ case MYSQL_TYPE_TINY_BLOB:
+ case MYSQL_TYPE_MEDIUM_BLOB:
+ case MYSQL_TYPE_LONG_BLOB:
+ case MYSQL_TYPE_BLOB:
+ if (row[i] != NULL)
+ v.set_asString((const char*)row[i]);
+ break;
+ case MYSQL_TYPE_NULL:
+ default:
+ CLog::Log(LOGDEBUG, "MYSQL: Unknown field type: {}", fields[i].type);
+ v.set_asString("");
+ v.set_isNull();
+ break;
+ }
+ }
+ result.records.push_back(res);
+ }
+ mysql_free_result(stmt);
+ active = true;
+ ds_state = dsSelect;
+ this->first();
+ return true;
+}
+
+void MysqlDataset::open(const std::string& sql)
+{
+ set_select_sql(sql);
+ open();
+}
+
+void MysqlDataset::open()
+{
+ if (select_sql.size())
+ {
+ query(select_sql);
+ }
+ else
+ {
+ ds_state = dsInactive;
+ }
+}
+
+void MysqlDataset::close()
+{
+ Dataset::close();
+ result.clear();
+ edit_object->clear();
+ fields_object->clear();
+ ds_state = dsInactive;
+ active = false;
+}
+
+void MysqlDataset::cancel()
+{
+ if ((ds_state == dsInsert) || (ds_state == dsEdit))
+ {
+ if (result.record_header.size())
+ ds_state = dsSelect;
+ else
+ ds_state = dsInactive;
+ }
+}
+
+int MysqlDataset::num_rows()
+{
+ return result.records.size();
+}
+
+bool MysqlDataset::eof()
+{
+ return feof;
+}
+
+bool MysqlDataset::bof()
+{
+ return fbof;
+}
+
+void MysqlDataset::first()
+{
+ Dataset::first();
+ this->fill_fields();
+}
+
+void MysqlDataset::last()
+{
+ Dataset::last();
+ fill_fields();
+}
+
+void MysqlDataset::prev(void)
+{
+ Dataset::prev();
+ fill_fields();
+}
+
+void MysqlDataset::next(void)
+{
+ Dataset::next();
+ if (!eof())
+ fill_fields();
+}
+
+void MysqlDataset::free_row(void)
+{
+ if (frecno < 0 || (unsigned int)frecno >= result.records.size())
+ return;
+
+ sql_record* row = result.records[frecno];
+ if (row)
+ {
+ delete row;
+ result.records[frecno] = NULL;
+ }
+}
+
+bool MysqlDataset::seek(int pos)
+{
+ if (ds_state == dsSelect)
+ {
+ Dataset::seek(pos);
+ fill_fields();
+ return true;
+ }
+
+ return false;
+}
+
+int64_t MysqlDataset::lastinsertid()
+{
+ if (!handle())
+ throw DbErrors("No Database Connection");
+ return mysql_insert_id(handle());
+}
+
+long MysqlDataset::nextid(const char* seq_name)
+{
+ if (handle())
+ return db->nextid(seq_name);
+
+ return DB_UNEXPECTED_RESULT;
+}
+
+void MysqlDataset::interrupt()
+{
+ // Impossible
+}
+
+} // namespace dbiplus
diff --git a/xbmc/dbwrappers/mysqldataset.h b/xbmc/dbwrappers/mysqldataset.h
new file mode 100644
index 0000000..552368d
--- /dev/null
+++ b/xbmc/dbwrappers/mysqldataset.h
@@ -0,0 +1,167 @@
+/*
+ * 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 "dataset.h"
+
+#include <stdio.h>
+#ifdef HAS_MYSQL
+#include <mysql/mysql.h>
+#elif defined(HAS_MARIADB)
+#include <mariadb/mysql.h>
+#endif
+
+namespace dbiplus
+{
+/***************** Class MysqlDatabase definition ******************
+
+ class 'MysqlDatabase' connects with MySQL-server
+
+******************************************************************/
+class MysqlDatabase : public Database
+{
+protected:
+ /* connect descriptor */
+ MYSQL* conn;
+ bool _in_transaction;
+ int last_err;
+
+public:
+ /* default constructor */
+ MysqlDatabase();
+ /* destructor */
+ ~MysqlDatabase() override;
+
+ Dataset* CreateDataset() const override;
+
+ /* func. returns connection handle with MySQL-server */
+ MYSQL* getHandle() { return conn; }
+ /* func. returns current status about MySQL-server connection */
+ int status() override;
+ int setErr(int err_code, const char* qry) override;
+ /* func. returns error message if error occurs */
+ const char* getErrorMsg() override;
+
+ /* func. connects to database-server */
+ int connect(bool create) override;
+ /* func. disconnects from database-server */
+ void disconnect() override;
+ /* func. creates new database */
+ int create() override;
+ /* func. deletes database */
+ int drop() override;
+ /* check if database exists (ie has tables/views defined) */
+ bool exists() override;
+
+ /* \brief copy database */
+ int copy(const char* backup_name) override;
+
+ /* \brief drop all extra analytics from database */
+ int drop_analytics(void) override;
+
+ long nextid(const char* seq_name) override;
+
+ /* virtual methods for transaction */
+
+ void start_transaction() override;
+ void commit_transaction() override;
+ void rollback_transaction() override;
+
+ /* virtual methods for formatting */
+ std::string vprepare(const char* format, va_list args) override;
+
+ bool in_transaction() override { return _in_transaction; }
+ int query_with_reconnect(const char* query);
+ void configure_connection();
+
+private:
+ typedef struct StrAccum StrAccum;
+
+ char et_getdigit(double* val, int* cnt);
+ void appendSpace(StrAccum* pAccum, int N);
+ void mysqlVXPrintf(StrAccum* pAccum, int useExtended, const char* fmt, va_list ap);
+ bool mysqlStrAccumAppend(StrAccum* p, const char* z, int N);
+ char* mysqlStrAccumFinish(StrAccum* p);
+ void mysqlStrAccumReset(StrAccum* p);
+ void mysqlStrAccumInit(StrAccum* p, char* zBase, int n, int mx);
+ std::string mysql_vmprintf(const char* zFormat, va_list ap);
+};
+
+/***************** Class MysqlDataset definition *******************
+
+ class 'MysqlDataset' does a query to MySQL-server
+
+******************************************************************/
+
+class MysqlDataset : public Dataset
+{
+protected:
+ MYSQL* handle();
+
+ /* Makes direct queries to database */
+ virtual void make_query(StringList& _sql);
+ /* Makes direct inserts into database */
+ void make_insert() override;
+ /* Edit SQL */
+ void make_edit() override;
+ /* Delete SQL */
+ void make_deletion() override;
+
+ /* This function works only with MySQL database
+ Filling the fields information from select statement */
+ void fill_fields() override;
+ /* Changing field values during dataset navigation */
+ virtual void free_row(); // free the memory allocated for the current row
+
+public:
+ /* constructor */
+ MysqlDataset();
+ explicit MysqlDataset(MysqlDatabase* newDb);
+
+ /* destructor */
+ ~MysqlDataset() override;
+
+ /* set autorefresh boolean value (if true - refresh the data after edit()
+or insert() operations default = false) */
+ void set_autorefresh(bool val);
+
+ /* opens a query & then sets a query results */
+ void open() override;
+ void open(const std::string& sql) override;
+ /* func. executes a query without results to return */
+ int exec() override;
+ int exec(const std::string& sql) override;
+ const void* getExecRes() override;
+ /* as open, but with our query exec Sql */
+ bool query(const std::string& query) override;
+ /* func. closes a query */
+ void close(void) override;
+ /* Cancel changes, made in insert or edit states of dataset */
+ void cancel() override;
+ /* last insert id */
+ int64_t lastinsertid() override;
+ /* sequence numbers */
+ long nextid(const char* seq_name) override;
+ /* sequence numbers */
+ int num_rows() override;
+ /* interrupt any pending database operation */
+ void interrupt() override;
+
+ bool bof() override;
+ bool eof() override;
+ void first() override;
+ void last() override;
+ void prev() override;
+ void next() override;
+ /* Go to record No (starting with 0) */
+ bool seek(int pos = 0) override;
+
+ bool dropIndex(const char* table, const char* index) override;
+};
+} // namespace dbiplus
diff --git a/xbmc/dbwrappers/qry_dat.cpp b/xbmc/dbwrappers/qry_dat.cpp
new file mode 100644
index 0000000..1abee3f
--- /dev/null
+++ b/xbmc/dbwrappers/qry_dat.cpp
@@ -0,0 +1,902 @@
+/*
+ * Copyright (C) 2004, Leo Seib, Hannover
+ *
+ * Project: C++ Dynamic Library
+ * Module: FieldValue class realisation file
+ * Author: Leo Seib E-Mail: leoseib@web.de
+ * Begin: 5/04/2002
+ *
+ * SPDX-License-Identifier: MIT
+ * See LICENSES/README.md for more information.
+ */
+
+/**********************************************************************
+ * 2005-03-29 - Minor modifications to allow get_asBool to function on
+ * on string values that are 1 or 0
+ **********************************************************************/
+
+#include "qry_dat.h"
+
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#ifndef __GNUC__
+#pragma warning(disable : 4800)
+#pragma warning(disable : 4715)
+#endif
+
+namespace dbiplus
+{
+
+//Constructors
+field_value::field_value()
+{
+ field_type = ft_String;
+ is_null = false;
+}
+
+field_value::field_value(const char* s) : str_value(s)
+{
+ field_type = ft_String;
+ is_null = false;
+}
+
+field_value::field_value(const bool b)
+{
+ bool_value = b;
+ field_type = ft_Boolean;
+ is_null = false;
+}
+
+field_value::field_value(const char c)
+{
+ char_value = c;
+ field_type = ft_Char;
+ is_null = false;
+}
+
+field_value::field_value(const short s)
+{
+ short_value = s;
+ field_type = ft_Short;
+ is_null = false;
+}
+
+field_value::field_value(const unsigned short us)
+{
+ ushort_value = us;
+ field_type = ft_UShort;
+ is_null = false;
+}
+
+field_value::field_value(const int i)
+{
+ int_value = i;
+ field_type = ft_Int;
+ is_null = false;
+}
+
+field_value::field_value(const unsigned int ui)
+{
+ uint_value = ui;
+ field_type = ft_UInt;
+ is_null = false;
+}
+
+field_value::field_value(const float f)
+{
+ float_value = f;
+ field_type = ft_Float;
+ is_null = false;
+}
+
+field_value::field_value(const double d)
+{
+ double_value = d;
+ field_type = ft_Double;
+ is_null = false;
+}
+
+field_value::field_value(const int64_t i)
+{
+ int64_value = i;
+ field_type = ft_Int64;
+ is_null = false;
+}
+
+field_value::field_value(const field_value& fv)
+{
+ switch (fv.get_fType())
+ {
+ case ft_String:
+ {
+ set_asString(fv.get_asString());
+ break;
+ }
+ case ft_Boolean:
+ {
+ set_asBool(fv.get_asBool());
+ break;
+ }
+ case ft_Char:
+ {
+ set_asChar(fv.get_asChar());
+ break;
+ }
+ case ft_Short:
+ {
+ set_asShort(fv.get_asShort());
+ break;
+ }
+ case ft_UShort:
+ {
+ set_asUShort(fv.get_asUShort());
+ break;
+ }
+ case ft_Int:
+ {
+ set_asInt(fv.get_asInt());
+ break;
+ }
+ case ft_UInt:
+ {
+ set_asUInt(fv.get_asUInt());
+ break;
+ }
+ case ft_Float:
+ {
+ set_asFloat(fv.get_asFloat());
+ break;
+ }
+ case ft_Double:
+ {
+ set_asDouble(fv.get_asDouble());
+ break;
+ }
+ case ft_Int64:
+ {
+ set_asInt64(fv.get_asInt64());
+ break;
+ }
+ default:
+ break;
+ }
+ is_null = fv.get_isNull();
+}
+
+//empty destructor
+field_value::~field_value() = default;
+
+//Conversations functions
+std::string field_value::get_asString() const
+{
+ std::string tmp;
+ switch (field_type)
+ {
+ case ft_String:
+ {
+ tmp = str_value;
+ return tmp;
+ }
+ case ft_Boolean:
+ {
+ if (bool_value)
+ return tmp = "True";
+ else
+ return tmp = "False";
+ }
+ case ft_Char:
+ {
+ return tmp = char_value;
+ }
+ case ft_Short:
+ {
+ char t[10];
+ sprintf(t, "%i", short_value);
+ return tmp = t;
+ }
+ case ft_UShort:
+ {
+ char t[10];
+ sprintf(t, "%i", ushort_value);
+ return tmp = t;
+ }
+ case ft_Int:
+ {
+ char t[12];
+ sprintf(t, "%d", int_value);
+ return tmp = t;
+ }
+ case ft_UInt:
+ {
+ char t[12];
+ sprintf(t, "%u", uint_value);
+ return tmp = t;
+ }
+ case ft_Float:
+ {
+ char t[16];
+ sprintf(t, "%f", static_cast<double>(float_value));
+ return tmp = t;
+ }
+ case ft_Double:
+ {
+ char t[32];
+ sprintf(t, "%f", double_value);
+ return tmp = t;
+ }
+ case ft_Int64:
+ {
+ char t[23];
+ sprintf(t, "%" PRId64, int64_value);
+ return tmp = t;
+ }
+ default:
+ return tmp = "";
+ }
+}
+
+bool field_value::get_asBool() const
+{
+ switch (field_type)
+ {
+ case ft_String:
+ {
+ if (str_value == "True" || str_value == "true" || str_value == "1")
+ return true;
+ else
+ return false;
+ }
+ case ft_Boolean:
+ {
+ return bool_value;
+ }
+ case ft_Char:
+ {
+ if (char_value == 'T' || char_value == 't')
+ return true;
+ else
+ return false;
+ }
+ case ft_Short:
+ {
+ return (bool)short_value;
+ }
+ case ft_UShort:
+ {
+ return (bool)ushort_value;
+ }
+ case ft_Int:
+ {
+ return (bool)int_value;
+ }
+ case ft_UInt:
+ {
+ return (bool)uint_value;
+ }
+ case ft_Float:
+ {
+ return (bool)float_value;
+ }
+ case ft_Double:
+ {
+ return (bool)double_value;
+ }
+ case ft_Int64:
+ {
+ return (bool)int64_value;
+ }
+ default:
+ return false;
+ }
+}
+
+char field_value::get_asChar() const
+{
+ switch (field_type)
+ {
+ case ft_String:
+ {
+ return str_value[0];
+ }
+ case ft_Boolean:
+ {
+ if (bool_value)
+ return 'T';
+ else
+ return 'F';
+ }
+ case ft_Char:
+ {
+ return char_value;
+ }
+ case ft_Short:
+ {
+ char t[10];
+ sprintf(t, "%i", short_value);
+ return t[0];
+ }
+ case ft_UShort:
+ {
+ char t[10];
+ sprintf(t, "%i", ushort_value);
+ return t[0];
+ }
+ case ft_Int:
+ {
+ char t[12];
+ sprintf(t, "%d", int_value);
+ return t[0];
+ }
+ case ft_UInt:
+ {
+ char t[12];
+ sprintf(t, "%u", uint_value);
+ return t[0];
+ }
+ case ft_Float:
+ {
+ char t[16];
+ sprintf(t, "%f", static_cast<double>(float_value));
+ return t[0];
+ }
+ case ft_Double:
+ {
+ char t[32];
+ sprintf(t, "%f", double_value);
+ return t[0];
+ }
+ case ft_Int64:
+ {
+ char t[24];
+ sprintf(t, "%" PRId64, int64_value);
+ return t[0];
+ }
+ default:
+ return '\0';
+ }
+}
+
+short field_value::get_asShort() const
+{
+ switch (field_type)
+ {
+ case ft_String:
+ {
+ return (short)atoi(str_value.c_str());
+ }
+ case ft_Boolean:
+ {
+ return (short)bool_value;
+ }
+ case ft_Char:
+ {
+ return (short)char_value;
+ }
+ case ft_Short:
+ {
+ return short_value;
+ }
+ case ft_UShort:
+ {
+ return (short)ushort_value;
+ }
+ case ft_Int:
+ {
+ return (short)int_value;
+ }
+ case ft_UInt:
+ {
+ return (short)uint_value;
+ }
+ case ft_Float:
+ {
+ return (short)float_value;
+ }
+ case ft_Double:
+ {
+ return (short)double_value;
+ }
+ case ft_Int64:
+ {
+ return (short)int64_value;
+ }
+ default:
+ return 0;
+ }
+}
+
+unsigned short field_value::get_asUShort() const
+{
+ switch (field_type)
+ {
+ case ft_String:
+ {
+ return (unsigned short)atoi(str_value.c_str());
+ }
+ case ft_Boolean:
+ {
+ return (unsigned short)bool_value;
+ }
+ case ft_Char:
+ {
+ return (unsigned short)char_value;
+ }
+ case ft_Short:
+ {
+ return (unsigned short)short_value;
+ }
+ case ft_UShort:
+ {
+ return ushort_value;
+ }
+ case ft_Int:
+ {
+ return (unsigned short)int_value;
+ }
+ case ft_UInt:
+ {
+ return (unsigned short)uint_value;
+ }
+ case ft_Float:
+ {
+ return (unsigned short)float_value;
+ }
+ case ft_Double:
+ {
+ return (unsigned short)double_value;
+ }
+ case ft_Int64:
+ {
+ return (unsigned short)int64_value;
+ }
+ default:
+ return 0;
+ }
+}
+
+int field_value::get_asInt() const
+{
+ switch (field_type)
+ {
+ case ft_String:
+ {
+ return atoi(str_value.c_str());
+ }
+ case ft_Boolean:
+ {
+ return (int)bool_value;
+ }
+ case ft_Char:
+ {
+ return (int)char_value;
+ }
+ case ft_Short:
+ {
+ return (int)short_value;
+ }
+ case ft_UShort:
+ {
+ return (int)ushort_value;
+ }
+ case ft_Int:
+ {
+ return int_value;
+ }
+ case ft_UInt:
+ {
+ return (int)uint_value;
+ }
+ case ft_Float:
+ {
+ return (int)float_value;
+ }
+ case ft_Double:
+ {
+ return (int)double_value;
+ }
+ case ft_Int64:
+ {
+ return (int)int64_value;
+ }
+ default:
+ return 0;
+ }
+}
+
+unsigned int field_value::get_asUInt() const
+{
+ switch (field_type)
+ {
+ case ft_String:
+ {
+ return (unsigned int)atoi(str_value.c_str());
+ }
+ case ft_Boolean:
+ {
+ return (unsigned int)bool_value;
+ }
+ case ft_Char:
+ {
+ return (unsigned int)char_value;
+ }
+ case ft_Short:
+ {
+ return (unsigned int)short_value;
+ }
+ case ft_UShort:
+ {
+ return (unsigned int)ushort_value;
+ }
+ case ft_Int:
+ {
+ return (unsigned int)int_value;
+ }
+ case ft_UInt:
+ {
+ return uint_value;
+ }
+ case ft_Float:
+ {
+ return (unsigned int)float_value;
+ }
+ case ft_Double:
+ {
+ return (unsigned int)double_value;
+ }
+ case ft_Int64:
+ {
+ return (unsigned int)int64_value;
+ }
+ default:
+ return 0;
+ }
+}
+
+float field_value::get_asFloat() const
+{
+ switch (field_type)
+ {
+ case ft_String:
+ {
+ return (float)atof(str_value.c_str());
+ }
+ case ft_Boolean:
+ {
+ return (float)bool_value;
+ }
+ case ft_Char:
+ {
+ return (float)char_value;
+ }
+ case ft_Short:
+ {
+ return (float)short_value;
+ }
+ case ft_UShort:
+ {
+ return (float)ushort_value;
+ }
+ case ft_Int:
+ {
+ return (float)int_value;
+ }
+ case ft_UInt:
+ {
+ return (float)uint_value;
+ }
+ case ft_Float:
+ {
+ return float_value;
+ }
+ case ft_Double:
+ {
+ return (float)double_value;
+ }
+ case ft_Int64:
+ {
+ return (float)int64_value;
+ }
+ default:
+ return 0.0;
+ }
+}
+
+double field_value::get_asDouble() const
+{
+ switch (field_type)
+ {
+ case ft_String:
+ {
+ return atof(str_value.c_str());
+ }
+ case ft_Boolean:
+ {
+ return (double)bool_value;
+ }
+ case ft_Char:
+ {
+ return (double)char_value;
+ }
+ case ft_Short:
+ {
+ return (double)short_value;
+ }
+ case ft_UShort:
+ {
+ return (double)ushort_value;
+ }
+ case ft_Int:
+ {
+ return (double)int_value;
+ }
+ case ft_UInt:
+ {
+ return (double)uint_value;
+ }
+ case ft_Float:
+ {
+ return (double)float_value;
+ }
+ case ft_Double:
+ {
+ return (double)double_value;
+ }
+ case ft_Int64:
+ {
+ return (double)int64_value;
+ }
+ default:
+ return 0.0;
+ }
+}
+
+int64_t field_value::get_asInt64() const
+{
+ switch (field_type)
+ {
+ case ft_String:
+ {
+ return std::atoll(str_value.c_str());
+ }
+ case ft_Boolean:
+ {
+ return (int64_t)bool_value;
+ }
+ case ft_Char:
+ {
+ return (int64_t)char_value;
+ }
+ case ft_Short:
+ {
+ return (int64_t)short_value;
+ }
+ case ft_UShort:
+ {
+ return (int64_t)ushort_value;
+ }
+ case ft_Int:
+ {
+ return (int64_t)int_value;
+ }
+ case ft_UInt:
+ {
+ return (int64_t)uint_value;
+ }
+ case ft_Float:
+ {
+ return (int64_t)float_value;
+ }
+ case ft_Double:
+ {
+ return (int64_t)double_value;
+ }
+ case ft_Int64:
+ {
+ return int64_value;
+ }
+ default:
+ return 0;
+ }
+}
+
+field_value& field_value::operator=(const field_value& fv)
+{
+ if (this == &fv)
+ return *this;
+
+ is_null = fv.get_isNull();
+
+ switch (fv.get_fType())
+ {
+ case ft_String:
+ {
+ set_asString(fv.get_asString());
+ return *this;
+ break;
+ }
+ case ft_Boolean:
+ {
+ set_asBool(fv.get_asBool());
+ return *this;
+ break;
+ }
+ case ft_Char:
+ {
+ set_asChar(fv.get_asChar());
+ return *this;
+ break;
+ }
+ case ft_Short:
+ {
+ set_asShort(fv.get_asShort());
+ return *this;
+ break;
+ }
+ case ft_UShort:
+ {
+ set_asUShort(fv.get_asUShort());
+ return *this;
+ break;
+ }
+ case ft_Int:
+ {
+ set_asInt(fv.get_asInt());
+ return *this;
+ break;
+ }
+ case ft_UInt:
+ {
+ set_asUInt(fv.get_asUInt());
+ return *this;
+ break;
+ }
+ case ft_Float:
+ {
+ set_asFloat(fv.get_asFloat());
+ return *this;
+ break;
+ }
+ case ft_Double:
+ {
+ set_asDouble(fv.get_asDouble());
+ return *this;
+ break;
+ }
+ case ft_Int64:
+ {
+ set_asInt64(fv.get_asInt64());
+ return *this;
+ break;
+ }
+ default:
+ return *this;
+ }
+}
+
+//Set functions
+void field_value::set_asString(const char* s)
+{
+ str_value = s;
+ field_type = ft_String;
+}
+
+void field_value::set_asString(const std::string& s)
+{
+ str_value = s;
+ field_type = ft_String;
+}
+
+void field_value::set_asBool(const bool b)
+{
+ bool_value = b;
+ field_type = ft_Boolean;
+}
+
+void field_value::set_asChar(const char c)
+{
+ char_value = c;
+ field_type = ft_Char;
+}
+
+void field_value::set_asShort(const short s)
+{
+ short_value = s;
+ field_type = ft_Short;
+}
+
+void field_value::set_asUShort(const unsigned short us)
+{
+ ushort_value = us;
+ field_type = ft_UShort;
+}
+
+void field_value::set_asInt(const int i)
+{
+ int_value = i;
+ field_type = ft_Int;
+}
+
+void field_value::set_asUInt(const unsigned int ui)
+{
+ int_value = ui;
+ field_type = ft_UInt;
+}
+
+void field_value::set_asFloat(const float f)
+{
+ float_value = f;
+ field_type = ft_Float;
+}
+
+void field_value::set_asDouble(const double d)
+{
+ double_value = d;
+ field_type = ft_Double;
+}
+
+void field_value::set_asInt64(const int64_t i)
+{
+ int64_value = i;
+ field_type = ft_Int64;
+}
+
+fType field_value::get_field_type()
+{
+ return field_type;
+}
+
+std::string field_value::gft()
+{
+ std::string tmp;
+ switch (field_type)
+ {
+ case ft_String:
+ {
+ tmp.assign("string");
+ return tmp;
+ }
+ case ft_Boolean:
+ {
+ tmp.assign("bool");
+ return tmp;
+ }
+ case ft_Char:
+ {
+ tmp.assign("char");
+ return tmp;
+ }
+ case ft_Short:
+ {
+ tmp.assign("short");
+ return tmp;
+ }
+ case ft_Int:
+ {
+ tmp.assign("int");
+ return tmp;
+ }
+ case ft_Float:
+ {
+ tmp.assign("float");
+ return tmp;
+ }
+ case ft_Double:
+ {
+ tmp.assign("double");
+ return tmp;
+ }
+ case ft_Int64:
+ {
+ tmp.assign("int64");
+ return tmp;
+ }
+ default:
+ break;
+ }
+
+ return tmp;
+}
+
+} // namespace dbiplus
diff --git a/xbmc/dbwrappers/qry_dat.h b/xbmc/dbwrappers/qry_dat.h
new file mode 100644
index 0000000..150de2d
--- /dev/null
+++ b/xbmc/dbwrappers/qry_dat.h
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2004, Leo Seib, Hannover
+ *
+ * Project:Dataset C++ Dynamic Library
+ * Module: FieldValue class and result sets classes header file
+ * Author: Leo Seib E-Mail: leoseib@web.de
+ * Begin: 5/04/2002
+ *
+ * SPDX-License-Identifier: MIT
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include <iostream>
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+namespace dbiplus
+{
+
+enum fType
+{
+ ft_String,
+ ft_Boolean,
+ ft_Char,
+ ft_WChar,
+ ft_WideString,
+ ft_Short,
+ ft_UShort,
+ ft_Int,
+ ft_UInt,
+ ft_Float,
+ ft_Double,
+ ft_LongDouble,
+ ft_Int64,
+ ft_Object
+};
+
+#ifdef TARGET_WINDOWS_STORE
+#pragma pack(push)
+#pragma pack(8)
+#endif
+
+class field_value
+{
+private:
+ fType field_type;
+ std::string str_value;
+ union
+ {
+ bool bool_value;
+ char char_value;
+ short short_value;
+ unsigned short ushort_value;
+ int int_value;
+ unsigned int uint_value;
+ float float_value;
+ double double_value;
+ int64_t int64_value;
+ void* object_value;
+ };
+
+ bool is_null;
+
+public:
+ field_value();
+ explicit field_value(const char* s);
+ explicit field_value(const bool b);
+ explicit field_value(const char c);
+ explicit field_value(const short s);
+ explicit field_value(const unsigned short us);
+ explicit field_value(const int l);
+ explicit field_value(const unsigned int ul);
+ explicit field_value(const float f);
+ explicit field_value(const double d);
+ explicit field_value(const int64_t i);
+ field_value(const field_value& fv);
+ ~field_value();
+
+ fType get_fType() const { return field_type; }
+ bool get_isNull() const { return is_null; }
+ std::string get_asString() const;
+ bool get_asBool() const;
+ char get_asChar() const;
+ short get_asShort() const;
+ unsigned short get_asUShort() const;
+ int get_asInt() const;
+ unsigned int get_asUInt() const;
+ float get_asFloat() const;
+ double get_asDouble() const;
+ int64_t get_asInt64() const;
+
+ field_value& operator=(const char* s)
+ {
+ set_asString(s);
+ return *this;
+ }
+ field_value& operator=(const std::string& s)
+ {
+ set_asString(s);
+ return *this;
+ }
+ field_value& operator=(const bool b)
+ {
+ set_asBool(b);
+ return *this;
+ }
+ field_value& operator=(const short s)
+ {
+ set_asShort(s);
+ return *this;
+ }
+ field_value& operator=(const unsigned short us)
+ {
+ set_asUShort(us);
+ return *this;
+ }
+ field_value& operator=(const int l)
+ {
+ set_asInt(l);
+ return *this;
+ }
+ field_value& operator=(const unsigned int l)
+ {
+ set_asUInt(l);
+ return *this;
+ }
+ field_value& operator=(const float f)
+ {
+ set_asFloat(f);
+ return *this;
+ }
+ field_value& operator=(const double d)
+ {
+ set_asDouble(d);
+ return *this;
+ }
+ field_value& operator=(const int64_t i)
+ {
+ set_asInt64(i);
+ return *this;
+ }
+ field_value& operator=(const field_value& fv);
+
+ //class ostream;
+ friend std::ostream& operator<<(std::ostream& os, const field_value& fv)
+ {
+ switch (fv.get_fType())
+ {
+ case ft_String:
+ {
+ return os << fv.get_asString();
+ break;
+ }
+ case ft_Boolean:
+ {
+ return os << fv.get_asBool();
+ break;
+ }
+ case ft_Char:
+ {
+ return os << fv.get_asChar();
+ break;
+ }
+ case ft_Short:
+ {
+ return os << fv.get_asShort();
+ break;
+ }
+ case ft_UShort:
+ {
+ return os << fv.get_asUShort();
+ break;
+ }
+ case ft_Int:
+ {
+ return os << fv.get_asInt();
+ break;
+ }
+ case ft_UInt:
+ {
+ return os << fv.get_asUInt();
+ break;
+ }
+ case ft_Float:
+ {
+ return os << fv.get_asFloat();
+ break;
+ }
+ case ft_Double:
+ {
+ return os << fv.get_asDouble();
+ break;
+ }
+ case ft_Int64:
+ {
+ return os << fv.get_asInt64();
+ break;
+ }
+ default:
+ {
+ return os;
+ break;
+ }
+ }
+ }
+
+ void set_isNull() { is_null = true; }
+ void set_asString(const char* s);
+ void set_asString(const std::string& s);
+ void set_asBool(const bool b);
+ void set_asChar(const char c);
+ void set_asShort(const short s);
+ void set_asUShort(const unsigned short us);
+ void set_asInt(const int l);
+ void set_asUInt(const unsigned int l);
+ void set_asFloat(const float f);
+ void set_asDouble(const double d);
+ void set_asInt64(const int64_t i);
+
+ fType get_field_type();
+ std::string gft();
+};
+
+struct field_prop
+{
+ std::string name;
+};
+
+struct field
+{
+ field_prop props;
+ field_value val;
+};
+
+typedef std::vector<field> Fields;
+typedef std::vector<field_value> sql_record;
+typedef std::vector<field_prop> record_prop;
+typedef std::vector<sql_record*> query_data;
+typedef field_value variant;
+
+class result_set
+{
+public:
+ result_set() = default;
+ ~result_set() { clear(); };
+ void clear()
+ {
+ for (unsigned int i = 0; i < records.size(); i++)
+ if (records[i])
+ delete records[i];
+ records.clear();
+ record_header.clear();
+ };
+
+ record_prop record_header;
+ query_data records;
+};
+
+#ifdef TARGET_WINDOWS_STORE
+#pragma pack(pop)
+#endif
+} // namespace dbiplus
diff --git a/xbmc/dbwrappers/sqlitedataset.cpp b/xbmc/dbwrappers/sqlitedataset.cpp
new file mode 100644
index 0000000..ef9c927
--- /dev/null
+++ b/xbmc/dbwrappers/sqlitedataset.cpp
@@ -0,0 +1,1063 @@
+/**********************************************************************
+ * Copyright (C) 2004, Leo Seib, Hannover
+ *
+ * Project:SQLiteDataset C++ Dynamic Library
+ * Module: SQLiteDataset class realisation file
+ * Author: Leo Seib E-Mail: leoseib@web.de
+ * Begin: 5/04/2002
+ *
+ * SPDX-License-Identifier: MIT
+ * See LICENSES/README.md for more information.
+ */
+
+#include "sqlitedataset.h"
+
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <iostream>
+#include <map>
+#include <sstream>
+#include <string>
+
+using namespace std::chrono_literals;
+
+namespace
+{
+#define X(VAL) std::make_pair(VAL, #VAL)
+//!@todo Remove ifdefs when sqlite version requirement has been bumped to at least 3.26.0
+const std::map<int, const char*> g_SqliteErrorStrings = {
+ X(SQLITE_OK),
+ X(SQLITE_ERROR),
+ X(SQLITE_INTERNAL),
+ X(SQLITE_PERM),
+ X(SQLITE_ABORT),
+ X(SQLITE_BUSY),
+ X(SQLITE_LOCKED),
+ X(SQLITE_NOMEM),
+ X(SQLITE_READONLY),
+ X(SQLITE_INTERRUPT),
+ X(SQLITE_IOERR),
+ X(SQLITE_CORRUPT),
+ X(SQLITE_NOTFOUND),
+ X(SQLITE_FULL),
+ X(SQLITE_CANTOPEN),
+ X(SQLITE_PROTOCOL),
+ X(SQLITE_EMPTY),
+ X(SQLITE_SCHEMA),
+ X(SQLITE_TOOBIG),
+ X(SQLITE_CONSTRAINT),
+ X(SQLITE_MISMATCH),
+ X(SQLITE_MISUSE),
+ X(SQLITE_NOLFS),
+ X(SQLITE_AUTH),
+ X(SQLITE_FORMAT),
+ X(SQLITE_RANGE),
+ X(SQLITE_NOTADB),
+ X(SQLITE_NOTICE),
+ X(SQLITE_WARNING),
+ X(SQLITE_ROW),
+ X(SQLITE_DONE),
+#if defined(SQLITE_ERROR_MISSING_COLLSEQ)
+ X(SQLITE_ERROR_MISSING_COLLSEQ),
+#endif
+#if defined(SQLITE_ERROR_RETRY)
+ X(SQLITE_ERROR_RETRY),
+#endif
+#if defined(SQLITE_ERROR_SNAPSHOT)
+ X(SQLITE_ERROR_SNAPSHOT),
+#endif
+ X(SQLITE_IOERR_READ),
+ X(SQLITE_IOERR_SHORT_READ),
+ X(SQLITE_IOERR_WRITE),
+ X(SQLITE_IOERR_FSYNC),
+ X(SQLITE_IOERR_DIR_FSYNC),
+ X(SQLITE_IOERR_TRUNCATE),
+ X(SQLITE_IOERR_FSTAT),
+ X(SQLITE_IOERR_UNLOCK),
+ X(SQLITE_IOERR_RDLOCK),
+ X(SQLITE_IOERR_DELETE),
+ X(SQLITE_IOERR_BLOCKED),
+ X(SQLITE_IOERR_NOMEM),
+ X(SQLITE_IOERR_ACCESS),
+ X(SQLITE_IOERR_CHECKRESERVEDLOCK),
+ X(SQLITE_IOERR_LOCK),
+ X(SQLITE_IOERR_CLOSE),
+ X(SQLITE_IOERR_DIR_CLOSE),
+ X(SQLITE_IOERR_SHMOPEN),
+ X(SQLITE_IOERR_SHMSIZE),
+ X(SQLITE_IOERR_SHMLOCK),
+ X(SQLITE_IOERR_SHMMAP),
+ X(SQLITE_IOERR_SEEK),
+ X(SQLITE_IOERR_DELETE_NOENT),
+ X(SQLITE_IOERR_MMAP),
+ X(SQLITE_IOERR_GETTEMPPATH),
+ X(SQLITE_IOERR_CONVPATH),
+#if defined(SQLITE_IOERR_VNODE)
+ X(SQLITE_IOERR_VNODE),
+#endif
+#if defined(SQLITE_IOERR_AUTH)
+ X(SQLITE_IOERR_AUTH),
+#endif
+#if defined(SQLITE_IOERR_BEGIN_ATOMIC)
+ X(SQLITE_IOERR_BEGIN_ATOMIC),
+#endif
+#if defined(SQLITE_IOERR_COMMIT_ATOMIC)
+ X(SQLITE_IOERR_COMMIT_ATOMIC),
+#endif
+#if defined(SQLITE_IOERR_ROLLBACK_ATOMIC)
+ X(SQLITE_IOERR_ROLLBACK_ATOMIC),
+#endif
+ X(SQLITE_LOCKED_SHAREDCACHE),
+#if defined(SQLITE_LOCKED_VTAB)
+ X(SQLITE_LOCKED_VTAB),
+#endif
+ X(SQLITE_BUSY_RECOVERY),
+ X(SQLITE_BUSY_SNAPSHOT),
+ X(SQLITE_CANTOPEN_NOTEMPDIR),
+ X(SQLITE_CANTOPEN_ISDIR),
+ X(SQLITE_CANTOPEN_FULLPATH),
+ X(SQLITE_CANTOPEN_CONVPATH),
+#if defined(SQLITE_CANTOPEN_DIRTYWAL)
+ X(SQLITE_CANTOPEN_DIRTYWAL),
+#endif
+ X(SQLITE_CORRUPT_VTAB),
+#if defined(SQLITE_CORRUPT_SEQUENCE)
+ X(SQLITE_CORRUPT_SEQUENCE),
+#endif
+ X(SQLITE_READONLY_RECOVERY),
+ X(SQLITE_READONLY_CANTLOCK),
+ X(SQLITE_READONLY_ROLLBACK),
+ X(SQLITE_READONLY_DBMOVED),
+#if defined(SQLITE_READONLY_CANTINIT)
+ X(SQLITE_READONLY_CANTINIT),
+#endif
+#if defined(SQLITE_READONLY_DIRECTORY)
+ X(SQLITE_READONLY_DIRECTORY),
+#endif
+ X(SQLITE_ABORT_ROLLBACK),
+ X(SQLITE_CONSTRAINT_CHECK),
+ X(SQLITE_CONSTRAINT_COMMITHOOK),
+ X(SQLITE_CONSTRAINT_FOREIGNKEY),
+ X(SQLITE_CONSTRAINT_FUNCTION),
+ X(SQLITE_CONSTRAINT_NOTNULL),
+ X(SQLITE_CONSTRAINT_PRIMARYKEY),
+ X(SQLITE_CONSTRAINT_TRIGGER),
+ X(SQLITE_CONSTRAINT_UNIQUE),
+ X(SQLITE_CONSTRAINT_VTAB),
+ X(SQLITE_CONSTRAINT_ROWID),
+ X(SQLITE_NOTICE_RECOVER_WAL),
+ X(SQLITE_NOTICE_RECOVER_ROLLBACK),
+ X(SQLITE_WARNING_AUTOINDEX),
+ X(SQLITE_AUTH_USER),
+#if defined(SQLITE_OK_LOAD_PERMANENTLY)
+ X(SQLITE_OK_LOAD_PERMANENTLY),
+#endif
+};
+#undef X
+} // namespace
+
+namespace dbiplus
+{
+//************* Callback function ***************************
+
+int callback(void* res_ptr, int ncol, char** result, char** cols)
+{
+ result_set* r = static_cast<result_set*>(res_ptr);
+
+ if (!r->record_header.size())
+ {
+ r->record_header.reserve(ncol);
+ for (int i = 0; i < ncol; i++)
+ {
+ field_prop header;
+ header.name = cols[i];
+ r->record_header.push_back(header);
+ }
+ }
+
+ if (result != NULL)
+ {
+ sql_record* rec = new sql_record;
+ rec->resize(ncol);
+ for (int i = 0; i < ncol; i++)
+ {
+ field_value& v = rec->at(i);
+ if (result[i] == NULL)
+ {
+ v.set_asString("");
+ v.set_isNull();
+ }
+ else
+ {
+ v.set_asString(result[i]);
+ }
+ }
+ r->records.push_back(rec);
+ }
+ return 0;
+}
+
+static int busy_callback(void*, int busyCount)
+{
+ KODI::TIME::Sleep(100ms);
+ return 1;
+}
+
+//************* SqliteDatabase implementation ***************
+
+SqliteDatabase::SqliteDatabase()
+{
+
+ active = false;
+ _in_transaction = false; // for transaction
+
+ error = "Unknown database error"; //S_NO_CONNECTION;
+ host = "localhost";
+ port = "";
+ db = "sqlite.db";
+ login = "root";
+ passwd = "";
+}
+
+SqliteDatabase::~SqliteDatabase()
+{
+ disconnect();
+}
+
+Dataset* SqliteDatabase::CreateDataset() const
+{
+ return new SqliteDataset(const_cast<SqliteDatabase*>(this));
+}
+
+void SqliteDatabase::setHostName(const char* newHost)
+{
+ host = newHost;
+
+ // hostname is the relative folder to the database, ensure it's slash terminated
+ if (host[host.length() - 1] != '/' && host[host.length() - 1] != '\\')
+ host += "/";
+
+ // ensure the fully qualified path has slashes in the correct direction
+ if ((host[1] == ':') && isalpha(host[0]))
+ {
+ size_t pos = 0;
+ while ((pos = host.find('/', pos)) != std::string::npos)
+ host.replace(pos++, 1, "\\");
+ }
+ else
+ {
+ size_t pos = 0;
+ while ((pos = host.find('\\', pos)) != std::string::npos)
+ host.replace(pos++, 1, "/");
+ }
+}
+
+void SqliteDatabase::setDatabase(const char* newDb)
+{
+ db = newDb;
+
+ // db is the filename for the database, ensure it's not slash prefixed
+ if (newDb[0] == '/' || newDb[0] == '\\')
+ db = db.substr(1);
+
+ // ensure the ".db" extension is appended to the end
+ if (db.find(".db") != (db.length() - 3))
+ db += ".db";
+}
+
+int SqliteDatabase::status(void)
+{
+ if (active == false)
+ return DB_CONNECTION_NONE;
+ return DB_CONNECTION_OK;
+}
+
+int SqliteDatabase::setErr(int err_code, const char* qry)
+{
+ std::stringstream ss;
+ ss << "[" << db << "] ";
+ auto errorIt = g_SqliteErrorStrings.find(err_code);
+ if (errorIt != g_SqliteErrorStrings.end())
+ {
+ ss << "SQLite error " << errorIt->second;
+ }
+ else
+ {
+ ss << "Undefined SQLite error " << err_code;
+ }
+ if (conn)
+ ss << " (" << sqlite3_errmsg(conn) << ")";
+ ss << "\nQuery: " << qry;
+ error = ss.str();
+ return err_code;
+}
+
+const char* SqliteDatabase::getErrorMsg()
+{
+ return error.c_str();
+}
+
+static int AlphaNumericCollation(
+ void* not_used, int nKey1, const void* pKey1, int nKey2, const void* pKey2)
+{
+ return StringUtils::AlphaNumericCollation(nKey1, pKey1, nKey2, pKey2);
+}
+
+int SqliteDatabase::connect(bool create)
+{
+ if (host.empty() || db.empty())
+ return DB_CONNECTION_NONE;
+
+ //CLog::Log(LOGDEBUG, "Connecting to sqlite:{}:{}", host, db);
+
+ std::string db_fullpath = URIUtils::AddFileToFolder(host, db);
+
+ try
+ {
+ disconnect();
+ int flags = SQLITE_OPEN_READWRITE;
+ if (create)
+ flags |= SQLITE_OPEN_CREATE;
+ int errorCode = sqlite3_open_v2(db_fullpath.c_str(), &conn, flags, NULL);
+ if (create && errorCode == SQLITE_CANTOPEN)
+ {
+ CLog::Log(LOGFATAL, "SqliteDatabase: can't open {}", db_fullpath);
+ throw std::runtime_error("SqliteDatabase: can't open " + db_fullpath);
+ }
+ else if (errorCode == SQLITE_OK)
+ {
+ sqlite3_extended_result_codes(conn, 1);
+ sqlite3_busy_handler(conn, busy_callback, NULL);
+ if (setErr(sqlite3_exec(getHandle(), "PRAGMA empty_result_callbacks=ON", NULL, NULL, NULL),
+ "PRAGMA empty_result_callbacks=ON") != SQLITE_OK)
+ {
+ throw DbErrors("%s", getErrorMsg());
+ }
+ else if (sqlite3_db_readonly(conn, nullptr) == 1)
+ {
+ CLog::Log(LOGFATAL, "SqliteDatabase: {} is read only", db_fullpath);
+ throw std::runtime_error("SqliteDatabase: " + db_fullpath + " is read only");
+ }
+ errorCode = sqlite3_create_collation(conn, "ALPHANUM", SQLITE_UTF8, 0, AlphaNumericCollation);
+ if (errorCode != SQLITE_OK)
+ {
+ CLog::Log(LOGFATAL, "SqliteDatabase: can not register collation");
+ throw std::runtime_error("SqliteDatabase: can not register collation " + db_fullpath);
+ }
+ active = true;
+ return DB_CONNECTION_OK;
+ }
+ }
+ catch (const DbErrors&)
+ {
+ }
+
+ sqlite3_close(conn);
+
+ return DB_CONNECTION_NONE;
+}
+
+bool SqliteDatabase::exists(void)
+{
+ bool bRet = false;
+ if (!active)
+ return bRet;
+ result_set res;
+ char sqlcmd[512];
+
+ // performing a select all on the sqlite_master will return rows if there are tables
+ // defined indicating it's not empty and therefore must "exist".
+ sprintf(sqlcmd, "SELECT * FROM sqlite_master");
+ if ((last_err = sqlite3_exec(getHandle(), sqlcmd, &callback, &res, NULL)) == SQLITE_OK)
+ {
+ bRet = (res.records.size() > 0);
+ }
+
+ return bRet;
+}
+
+void SqliteDatabase::disconnect(void)
+{
+ if (active == false)
+ return;
+ sqlite3_close(conn);
+ active = false;
+}
+
+int SqliteDatabase::create()
+{
+ return connect(true);
+}
+
+int SqliteDatabase::copy(const char* backup_name)
+{
+ if (active == false)
+ throw DbErrors("Can't copy database: no active connection...");
+
+ CLog::Log(LOGDEBUG, "Copying from {} to {} at {}", db, backup_name, host);
+
+ int rc;
+ std::string backup_db = backup_name;
+
+ sqlite3* pFile; /* Database connection opened on zFilename */
+ sqlite3_backup* pBackup; /* Backup object used to copy data */
+
+ //
+ if (backup_name[0] == '/' || backup_name[0] == '\\')
+ backup_db = backup_db.substr(1);
+
+ // ensure the ".db" extension is appended to the end
+ if (backup_db.find(".db") != (backup_db.length() - 3))
+ backup_db += ".db";
+
+ std::string backup_path = host + backup_db;
+
+ /* Open the database file identified by zFilename. Exit early if this fails
+ ** for any reason. */
+ rc = sqlite3_open(backup_path.c_str(), &pFile);
+ if (rc == SQLITE_OK)
+ {
+ pBackup = sqlite3_backup_init(pFile, "main", getHandle(), "main");
+
+ if (pBackup)
+ {
+ (void)sqlite3_backup_step(pBackup, -1);
+ (void)sqlite3_backup_finish(pBackup);
+ }
+
+ rc = sqlite3_errcode(pFile);
+ }
+
+ (void)sqlite3_close(pFile);
+
+ if (rc != SQLITE_OK)
+ throw DbErrors("Can't copy database. (%d)", rc);
+
+ return rc;
+}
+
+int SqliteDatabase::drop_analytics(void)
+{
+ // SqliteDatabase::copy used a full database copy, so we have a new version
+ // with all the analytics stuff. We should clean database from everything but data
+ if (active == false)
+ throw DbErrors("Can't drop extras database: no active connection...");
+
+ char sqlcmd[4096];
+ result_set res;
+
+ CLog::Log(LOGDEBUG, "Cleaning indexes from database {} at {}", db, host);
+ sprintf(sqlcmd, "SELECT name FROM sqlite_master WHERE type == 'index' AND sql IS NOT NULL");
+ if ((last_err = sqlite3_exec(conn, sqlcmd, &callback, &res, NULL)) != SQLITE_OK)
+ return DB_UNEXPECTED_RESULT;
+
+ for (size_t i = 0; i < res.records.size(); i++)
+ {
+ sprintf(sqlcmd, "DROP INDEX '%s'", res.records[i]->at(0).get_asString().c_str());
+ if ((last_err = sqlite3_exec(conn, sqlcmd, NULL, NULL, NULL)) != SQLITE_OK)
+ return DB_UNEXPECTED_RESULT;
+ }
+ res.clear();
+
+ CLog::Log(LOGDEBUG, "Cleaning views from database {} at {}", db, host);
+ sprintf(sqlcmd, "SELECT name FROM sqlite_master WHERE type == 'view'");
+ if ((last_err = sqlite3_exec(conn, sqlcmd, &callback, &res, NULL)) != SQLITE_OK)
+ return DB_UNEXPECTED_RESULT;
+
+ for (size_t i = 0; i < res.records.size(); i++)
+ {
+ sprintf(sqlcmd, "DROP VIEW '%s'", res.records[i]->at(0).get_asString().c_str());
+ if ((last_err = sqlite3_exec(conn, sqlcmd, NULL, NULL, NULL)) != SQLITE_OK)
+ return DB_UNEXPECTED_RESULT;
+ }
+ res.clear();
+
+ CLog::Log(LOGDEBUG, "Cleaning triggers from database {} at {}", db, host);
+ sprintf(sqlcmd, "SELECT name FROM sqlite_master WHERE type == 'trigger'");
+ if ((last_err = sqlite3_exec(conn, sqlcmd, &callback, &res, NULL)) != SQLITE_OK)
+ return DB_UNEXPECTED_RESULT;
+
+ for (size_t i = 0; i < res.records.size(); i++)
+ {
+ sprintf(sqlcmd, "DROP TRIGGER '%s'", res.records[i]->at(0).get_asString().c_str());
+ if ((last_err = sqlite3_exec(conn, sqlcmd, NULL, NULL, NULL)) != SQLITE_OK)
+ return DB_UNEXPECTED_RESULT;
+ }
+ // res would be cleared on destruct
+
+ return DB_COMMAND_OK;
+}
+
+int SqliteDatabase::drop()
+{
+ if (active == false)
+ throw DbErrors("Can't drop database: no active connection...");
+ disconnect();
+ if (!unlink(db.c_str()))
+ {
+ throw DbErrors("Can't drop database: can't unlink the file %s,\nError: %s", db.c_str(),
+ strerror(errno));
+ }
+ return DB_COMMAND_OK;
+}
+
+long SqliteDatabase::nextid(const char* sname)
+{
+ if (!active)
+ return DB_UNEXPECTED_RESULT;
+ int id; /*,nrow,ncol;*/
+ result_set res;
+ char sqlcmd[512];
+ snprintf(sqlcmd, sizeof(sqlcmd), "SELECT nextid FROM %s WHERE seq_name = '%s'",
+ sequence_table.c_str(), sname);
+ if ((last_err = sqlite3_exec(getHandle(), sqlcmd, &callback, &res, NULL)) != SQLITE_OK)
+ {
+ return DB_UNEXPECTED_RESULT;
+ }
+ if (res.records.empty())
+ {
+ id = 1;
+ snprintf(sqlcmd, sizeof(sqlcmd), "INSERT INTO %s (nextid,seq_name) VALUES (%d,'%s')",
+ sequence_table.c_str(), id, sname);
+ if ((last_err = sqlite3_exec(conn, sqlcmd, NULL, NULL, NULL)) != SQLITE_OK)
+ return DB_UNEXPECTED_RESULT;
+ return id;
+ }
+ else
+ {
+ id = res.records[0]->at(0).get_asInt() + 1;
+ snprintf(sqlcmd, sizeof(sqlcmd), "UPDATE %s SET nextid=%d WHERE seq_name = '%s'",
+ sequence_table.c_str(), id, sname);
+ if ((last_err = sqlite3_exec(conn, sqlcmd, NULL, NULL, NULL)) != SQLITE_OK)
+ return DB_UNEXPECTED_RESULT;
+ return id;
+ }
+ return DB_UNEXPECTED_RESULT;
+}
+
+// methods for transactions
+// ---------------------------------------------
+void SqliteDatabase::start_transaction()
+{
+ if (active)
+ {
+ sqlite3_exec(conn, "begin IMMEDIATE", NULL, NULL, NULL);
+ _in_transaction = true;
+ }
+}
+
+void SqliteDatabase::commit_transaction()
+{
+ if (active)
+ {
+ sqlite3_exec(conn, "commit", NULL, NULL, NULL);
+ _in_transaction = false;
+ }
+}
+
+void SqliteDatabase::rollback_transaction()
+{
+ if (active)
+ {
+ sqlite3_exec(conn, "rollback", NULL, NULL, NULL);
+ _in_transaction = false;
+ }
+}
+
+// methods for formatting
+// ---------------------------------------------
+std::string SqliteDatabase::vprepare(const char* format, va_list args)
+{
+ std::string strFormat = format;
+ std::string strResult = "";
+ char* p;
+ size_t pos;
+
+ // %q is the sqlite format string for %s.
+ // Any bad character, like "'", will be replaced with a proper one
+ pos = 0;
+ while ((pos = strFormat.find("%s", pos)) != std::string::npos)
+ strFormat.replace(pos++, 2, "%q");
+
+ // the %I64 enhancement is not supported by sqlite3_vmprintf
+ // must be %ll instead
+ pos = 0;
+ while ((pos = strFormat.find("%I64", pos)) != std::string::npos)
+ strFormat.replace(pos++, 4, "%ll");
+
+ p = sqlite3_vmprintf(strFormat.c_str(), args);
+ if (p)
+ {
+ strResult = p;
+ sqlite3_free(p);
+ }
+
+ // Strip SEPARATOR from all GROUP_CONCAT statements:
+ // before: GROUP_CONCAT(field SEPARATOR '; ')
+ // after: GROUP_CONCAT(field, '; ')
+ // Can not specify separator when have DISTINCT, comma used by default
+ pos = strResult.find("GROUP_CONCAT(");
+ while (pos != std::string::npos)
+ {
+ size_t pos2 = strResult.find(" SEPARATOR ", pos + 1);
+ if (pos2 != std::string::npos)
+ strResult.replace(pos2, 10, ",");
+ pos = strResult.find("GROUP_CONCAT(", pos + 1);
+ }
+ // Replace CONCAT with || to concatenate text fields:
+ // before: CONCAT(field1, field2, field3)
+ // after: field1 || field2 || field3
+ // Avoid commas in substatements and within single quotes
+ // before: CONCAT(field1, ',', REPLACE(field2, ',', '-'), field3)
+ // after: field1 || ',' || REPLACE(field2, ',', '-') || field3
+ pos = strResult.find("CONCAT(");
+ while (pos != std::string::npos)
+ {
+ if (pos == 0 || strResult[pos - 1] == ' ') // Not GROUP_CONCAT
+ {
+ // Check each char for other bracket or single quote pairs
+ unsigned int brackets = 1;
+ bool quoted = false;
+ size_t index = pos + 7; // start after "CONCAT("
+ while (index < strResult.size() && brackets != 0)
+ {
+ if (strResult[index] == '(')
+ brackets++;
+ else if (strResult[index] == ')')
+ {
+ brackets--;
+ if (brackets == 0)
+ strResult.erase(index, 1); //Remove closing bracket of CONCAT
+ }
+ else if (strResult[index] == '\'')
+ quoted = !quoted;
+ else if (strResult[index] == ',' && brackets == 1 && !quoted)
+ strResult.replace(index, 1, "||");
+ index++;
+ }
+ strResult.erase(pos, 7); //Remove "CONCAT("
+ }
+ pos = strResult.find("CONCAT(", pos + 1);
+ }
+
+ return strResult;
+}
+
+//************* SqliteDataset implementation ***************
+
+SqliteDataset::SqliteDataset() : Dataset()
+{
+ haveError = false;
+ db = NULL;
+ autorefresh = false;
+}
+
+SqliteDataset::SqliteDataset(SqliteDatabase* newDb) : Dataset(newDb)
+{
+ haveError = false;
+ db = newDb;
+ autorefresh = false;
+}
+
+SqliteDataset::~SqliteDataset()
+{
+}
+
+void SqliteDataset::set_autorefresh(bool val)
+{
+ autorefresh = val;
+}
+
+//--------- protected functions implementation -----------------//
+
+sqlite3* SqliteDataset::handle()
+{
+ if (db != NULL)
+ {
+ return static_cast<SqliteDatabase*>(db)->getHandle();
+ }
+ else
+ return NULL;
+}
+
+void SqliteDataset::make_query(StringList& _sql)
+{
+ std::string query;
+ if (db == NULL)
+ throw DbErrors("No Database Connection");
+
+ try
+ {
+
+ if (autocommit)
+ db->start_transaction();
+
+ for (const std::string& i : _sql)
+ {
+ query = i;
+ char* err = NULL;
+ Dataset::parse_sql(query);
+ if (db->setErr(sqlite3_exec(this->handle(), query.c_str(), NULL, NULL, &err),
+ query.c_str()) != SQLITE_OK)
+ {
+ std::string message = db->getErrorMsg();
+ if (err)
+ {
+ message.append(" (");
+ message.append(err);
+ message.append(")");
+ sqlite3_free(err);
+ }
+ throw DbErrors("%s", message.c_str());
+ }
+ } // end of for
+
+ if (db->in_transaction() && autocommit)
+ db->commit_transaction();
+
+ active = true;
+ ds_state = dsSelect;
+ if (autorefresh)
+ refresh();
+
+ } // end of try
+ catch (...)
+ {
+ if (db->in_transaction())
+ db->rollback_transaction();
+ throw;
+ }
+}
+
+void SqliteDataset::make_insert()
+{
+ make_query(insert_sql);
+ last();
+}
+
+void SqliteDataset::make_edit()
+{
+ make_query(update_sql);
+}
+
+void SqliteDataset::make_deletion()
+{
+ make_query(delete_sql);
+}
+
+void SqliteDataset::fill_fields()
+{
+ //cout <<"rr "<<result.records.size()<<"|" << frecno <<"\n";
+ if ((db == NULL) || (result.record_header.empty()) ||
+ (result.records.size() < (unsigned int)frecno))
+ return;
+
+ if (fields_object->size() == 0) // Filling columns name
+ {
+ const unsigned int ncols = result.record_header.size();
+ fields_object->resize(ncols);
+ for (unsigned int i = 0; i < ncols; i++)
+ {
+ (*fields_object)[i].props = result.record_header[i];
+ std::string name = result.record_header[i].name;
+ name2indexMap.insert({str_toLower(name.data()), i});
+ }
+ }
+
+ //Filling result
+ if (result.records.size() != 0)
+ {
+ const sql_record* row = result.records[frecno];
+ if (row)
+ {
+ const unsigned int ncols = row->size();
+ fields_object->resize(ncols);
+ for (unsigned int i = 0; i < ncols; i++)
+ (*fields_object)[i].val = row->at(i);
+ return;
+ }
+ }
+ const unsigned int ncols = result.record_header.size();
+ fields_object->resize(ncols);
+ for (unsigned int i = 0; i < ncols; i++)
+ (*fields_object)[i].val = "";
+}
+
+//------------- public functions implementation -----------------//
+bool SqliteDataset::dropIndex(const char* table, const char* index)
+{
+ std::string sql;
+
+ sql = static_cast<SqliteDatabase*>(db)->prepare("DROP INDEX IF EXISTS %s", index);
+
+ return (exec(sql) == SQLITE_OK);
+}
+
+int SqliteDataset::exec(const std::string& sql)
+{
+ if (!handle())
+ throw DbErrors("No Database Connection");
+ std::string qry = sql;
+ int res;
+ exec_res.clear();
+
+ // Strip size constraints from indexes (not supported in sqlite)
+ //
+ // Example:
+ // before: CREATE UNIQUE INDEX ixPath ON path ( strPath(255) )
+ // after: CREATE UNIQUE INDEX ixPath ON path ( strPath )
+ //
+ // NOTE: unexpected results occur if brackets are not matched
+ if (qry.find("CREATE UNIQUE INDEX") != std::string::npos ||
+ (qry.find("CREATE INDEX") != std::string::npos))
+ {
+ size_t pos = 0;
+ size_t pos2 = 0;
+
+ if ((pos = qry.find('(')) != std::string::npos)
+ {
+ pos++;
+ while ((pos = qry.find('(', pos)) != std::string::npos)
+ {
+ if ((pos2 = qry.find(')', pos)) != std::string::npos)
+ {
+ qry.replace(pos, pos2 - pos + 1, "");
+ pos = pos2;
+ }
+ }
+ }
+ }
+ // Strip ON table from DROP INDEX statements:
+ // before: DROP INDEX foo ON table
+ // after: DROP INDEX foo
+ size_t pos = qry.find("DROP INDEX ");
+ if (pos != std::string::npos)
+ {
+ pos = qry.find(" ON ", pos + 1);
+
+ if (pos != std::string::npos)
+ qry = qry.substr(0, pos);
+ }
+
+ char* errmsg;
+ if ((res = db->setErr(sqlite3_exec(handle(), qry.c_str(), &callback, &exec_res, &errmsg),
+ qry.c_str())) == SQLITE_OK)
+ return res;
+ else
+ {
+ if (errmsg)
+ {
+ DbErrors err("%s (%s)", db->getErrorMsg(), errmsg);
+ sqlite3_free(errmsg);
+ throw err;
+ }
+ else
+ {
+ throw DbErrors("%s", db->getErrorMsg());
+ }
+ }
+}
+
+int SqliteDataset::exec()
+{
+ return exec(sql);
+}
+
+const void* SqliteDataset::getExecRes()
+{
+ return &exec_res;
+}
+
+bool SqliteDataset::query(const std::string& query)
+{
+ if (!handle())
+ throw DbErrors("No Database Connection");
+ const std::string& qry = query;
+ int fs = qry.find("select");
+ int fS = qry.find("SELECT");
+ if (!(fs >= 0 || fS >= 0))
+ throw DbErrors("MUST be select SQL!");
+
+ close();
+
+ sqlite3_stmt* stmt = NULL;
+ if (db->setErr(sqlite3_prepare_v2(handle(), query.c_str(), -1, &stmt, NULL), query.c_str()) !=
+ SQLITE_OK)
+ throw DbErrors("%s", db->getErrorMsg());
+
+ // column headers
+ const unsigned int numColumns = sqlite3_column_count(stmt);
+ result.record_header.resize(numColumns);
+ for (unsigned int i = 0; i < numColumns; i++)
+ result.record_header[i].name = sqlite3_column_name(stmt, i);
+
+ // returned rows
+ while (sqlite3_step(stmt) == SQLITE_ROW)
+ { // have a row of data
+ sql_record* res = new sql_record;
+ res->resize(numColumns);
+ for (unsigned int i = 0; i < numColumns; i++)
+ {
+ field_value& v = res->at(i);
+ switch (sqlite3_column_type(stmt, i))
+ {
+ case SQLITE_INTEGER:
+ v.set_asInt64(sqlite3_column_int64(stmt, i));
+ break;
+ case SQLITE_FLOAT:
+ v.set_asDouble(sqlite3_column_double(stmt, i));
+ break;
+ case SQLITE_TEXT:
+ v.set_asString((const char*)sqlite3_column_text(stmt, i));
+ break;
+ case SQLITE_BLOB:
+ v.set_asString((const char*)sqlite3_column_text(stmt, i));
+ break;
+ case SQLITE_NULL:
+ default:
+ v.set_asString("");
+ v.set_isNull();
+ break;
+ }
+ }
+ result.records.push_back(res);
+ }
+ if (db->setErr(sqlite3_finalize(stmt), query.c_str()) == SQLITE_OK)
+ {
+ active = true;
+ ds_state = dsSelect;
+ this->first();
+ return true;
+ }
+ else
+ {
+ throw DbErrors("%s", db->getErrorMsg());
+ }
+}
+
+void SqliteDataset::open(const std::string& sql)
+{
+ set_select_sql(sql);
+ open();
+}
+
+void SqliteDataset::open()
+{
+ if (select_sql.size())
+ {
+ query(select_sql);
+ }
+ else
+ {
+ ds_state = dsInactive;
+ }
+}
+
+void SqliteDataset::close()
+{
+ Dataset::close();
+ result.clear();
+ edit_object->clear();
+ fields_object->clear();
+ ds_state = dsInactive;
+ active = false;
+}
+
+void SqliteDataset::cancel()
+{
+ if ((ds_state == dsInsert) || (ds_state == dsEdit))
+ {
+ if (result.record_header.size())
+ ds_state = dsSelect;
+ else
+ ds_state = dsInactive;
+ }
+}
+
+int SqliteDataset::num_rows()
+{
+ return result.records.size();
+}
+
+bool SqliteDataset::eof()
+{
+ return feof;
+}
+
+bool SqliteDataset::bof()
+{
+ return fbof;
+}
+
+void SqliteDataset::first()
+{
+ Dataset::first();
+ this->fill_fields();
+}
+
+void SqliteDataset::last()
+{
+ Dataset::last();
+ fill_fields();
+}
+
+void SqliteDataset::prev(void)
+{
+ Dataset::prev();
+ fill_fields();
+}
+
+void SqliteDataset::next(void)
+{
+ Dataset::next();
+ if (!eof())
+ fill_fields();
+}
+
+void SqliteDataset::free_row(void)
+{
+ if (frecno < 0 || (unsigned int)frecno >= result.records.size())
+ return;
+
+ sql_record* row = result.records[frecno];
+ if (row)
+ {
+ delete row;
+ result.records[frecno] = NULL;
+ }
+}
+
+bool SqliteDataset::seek(int pos)
+{
+ if (ds_state == dsSelect)
+ {
+ Dataset::seek(pos);
+ fill_fields();
+ return true;
+ }
+ return false;
+}
+
+int64_t SqliteDataset::lastinsertid()
+{
+ if (!handle())
+ throw DbErrors("No Database Connection");
+ return sqlite3_last_insert_rowid(handle());
+}
+
+long SqliteDataset::nextid(const char* seq_name)
+{
+ if (handle())
+ return db->nextid(seq_name);
+ else
+ return DB_UNEXPECTED_RESULT;
+}
+
+void SqliteDataset::interrupt()
+{
+ sqlite3_interrupt(handle());
+}
+} // namespace dbiplus
diff --git a/xbmc/dbwrappers/sqlitedataset.h b/xbmc/dbwrappers/sqlitedataset.h
new file mode 100644
index 0000000..4695dfd
--- /dev/null
+++ b/xbmc/dbwrappers/sqlitedataset.h
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2004, Leo Seib, Hannover
+ *
+ * Project:SQLiteDataset C++ Dynamic Library
+ * Module: SQLiteDataset class header file
+ * Author: Leo Seib E-Mail: leoseib@web.de
+ * Begin: 5/04/2002
+ *
+ * SPDX-License-Identifier: MIT
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "dataset.h"
+
+#include <stdio.h>
+
+#include <sqlite3.h>
+
+namespace dbiplus
+{
+/***************** Class SqliteDatabase definition ******************
+
+ class 'SqliteDatabase' connects with Sqlite-server
+
+******************************************************************/
+class SqliteDatabase : public Database
+{
+protected:
+ /* connect descriptor */
+ sqlite3* conn;
+ bool _in_transaction;
+ int last_err;
+
+public:
+ /* default constructor */
+ SqliteDatabase();
+ /* destructor */
+ ~SqliteDatabase() override;
+
+ Dataset* CreateDataset() const override;
+
+ /* func. returns connection handle with SQLite-server */
+ sqlite3* getHandle() { return conn; }
+ /* func. returns current status about SQLite-server connection */
+ int status() override;
+ int setErr(int err_code, const char* qry) override;
+ /* func. returns error message if error occurs */
+ const char* getErrorMsg() override;
+ /* sets a new host name */
+ void setHostName(const char* newHost) override;
+ /* sets a database name */
+ void setDatabase(const char* newDb) override;
+
+ /* func. connects to database-server */
+
+ int connect(bool create) override;
+ /* func. disconnects from database-server */
+ void disconnect() override;
+ /* func. creates new database */
+ int create() override;
+ /* func. deletes database */
+ int drop() override;
+ /* check if database exists (ie has tables/views defined) */
+ bool exists() override;
+
+ /* \brief copy database */
+ int copy(const char* backup_name) override;
+
+ /* \brief drop all extra analytics from database */
+ int drop_analytics(void) override;
+
+ long nextid(const char* seq_name) override;
+
+ /* virtual methods for transaction */
+
+ void start_transaction() override;
+ void commit_transaction() override;
+ void rollback_transaction() override;
+
+ /* virtual methods for formatting */
+ std::string vprepare(const char* format, va_list args) override;
+
+ bool in_transaction() override { return _in_transaction; }
+};
+
+/***************** Class SqliteDataset definition *******************
+
+ class 'SqliteDataset' does a query to SQLite-server
+
+******************************************************************/
+
+class SqliteDataset : public Dataset
+{
+protected:
+ sqlite3* handle();
+
+ /* Makes direct queries to database */
+ virtual void make_query(StringList& _sql);
+ /* Makes direct inserts into database */
+ void make_insert() override;
+ /* Edit SQL */
+ void make_edit() override;
+ /* Delete SQL */
+ void make_deletion() override;
+
+ //static int sqlite_callback(void* res_ptr,int ncol, char** result, char** cols);
+
+ /* This function works only with MySQL database
+ Filling the fields information from select statement */
+ void fill_fields() override;
+ /* Changing field values during dataset navigation */
+ virtual void free_row(); // free the memory allocated for the current row
+
+public:
+ /* constructor */
+ SqliteDataset();
+ explicit SqliteDataset(SqliteDatabase* newDb);
+
+ /* destructor */
+ ~SqliteDataset() override;
+
+ /* set autorefresh boolean value (if true - refresh the data after edit()
+or insert() operations default = false) */
+ void set_autorefresh(bool val);
+
+ /* opens a query & then sets a query results */
+ void open() override;
+ void open(const std::string& sql) override;
+ /* func. executes a query without results to return */
+ int exec() override;
+ int exec(const std::string& sql) override;
+ const void* getExecRes() override;
+ /* as open, but with our query exec Sql */
+ bool query(const std::string& query) override;
+ /* func. closes a query */
+ void close(void) override;
+ /* Cancel changes, made in insert or edit states of dataset */
+ void cancel() override;
+ /* last inserted id */
+ int64_t lastinsertid() override;
+ /* sequence numbers */
+ long nextid(const char* seq_name) override;
+ /* sequence numbers */
+ int num_rows() override;
+ /* interrupt any pending database operation */
+ void interrupt() override;
+
+ bool bof() override;
+ bool eof() override;
+ void first() override;
+ void last() override;
+ void prev() override;
+ void next() override;
+ /* Go to record No (starting with 0) */
+ bool seek(int pos = 0) override;
+
+ bool dropIndex(const char* table, const char* index) override;
+};
+} // namespace dbiplus
diff --git a/xbmc/dialogs/CMakeLists.txt b/xbmc/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..bd14222
--- /dev/null
+++ b/xbmc/dialogs/CMakeLists.txt
@@ -0,0 +1,69 @@
+set(SOURCES GUIDialogBoxBase.cpp
+ GUIDialogBusy.cpp
+ GUIDialogBusyNoCancel.cpp
+ GUIDialogButtonMenu.cpp
+ GUIDialogCache.cpp
+ GUIDialogColorPicker.cpp
+ GUIDialogContextMenu.cpp
+ GUIDialogExtendedProgressBar.cpp
+ GUIDialogFileBrowser.cpp
+ GUIDialogGamepad.cpp
+ GUIDialogKaiToast.cpp
+ GUIDialogKeyboardGeneric.cpp
+ GUIDialogKeyboardTouch.cpp
+ GUIDialogMediaFilter.cpp
+ GUIDialogMediaSource.cpp
+ GUIDialogNumeric.cpp
+ GUIDialogOK.cpp
+ GUIDialogPlayerControls.cpp
+ GUIDialogPlayerProcessInfo.cpp
+ GUIDialogProgress.cpp
+ GUIDialogSeekBar.cpp
+ GUIDialogSelect.cpp
+ GUIDialogSimpleMenu.cpp
+ GUIDialogSlider.cpp
+ GUIDialogSmartPlaylistEditor.cpp
+ GUIDialogSmartPlaylistRule.cpp
+ GUIDialogSubMenu.cpp
+ GUIDialogTextViewer.cpp
+ GUIDialogVolumeBar.cpp
+ GUIDialogYesNo.cpp)
+
+set(HEADERS GUIDialogBoxBase.h
+ GUIDialogBusy.h
+ GUIDialogBusyNoCancel.h
+ GUIDialogButtonMenu.h
+ GUIDialogCache.h
+ GUIDialogColorPicker.h
+ GUIDialogContextMenu.h
+ GUIDialogExtendedProgressBar.h
+ GUIDialogFileBrowser.h
+ GUIDialogGamepad.h
+ GUIDialogKaiToast.h
+ GUIDialogKeyboardGeneric.h
+ GUIDialogKeyboardTouch.h
+ GUIDialogMediaFilter.h
+ GUIDialogMediaSource.h
+ GUIDialogNumeric.h
+ GUIDialogOK.h
+ GUIDialogPlayerControls.h
+ GUIDialogPlayerProcessInfo.h
+ GUIDialogProgress.h
+ GUIDialogSeekBar.h
+ GUIDialogSelect.h
+ GUIDialogSimpleMenu.h
+ GUIDialogSlider.h
+ GUIDialogSmartPlaylistEditor.h
+ GUIDialogSmartPlaylistRule.h
+ GUIDialogSubMenu.h
+ GUIDialogTextViewer.h
+ GUIDialogVolumeBar.h
+ GUIDialogYesNo.h
+ IGUIVolumeBarCallback.h)
+
+if(ENABLE_OPTICAL)
+ list(APPEND SOURCES GUIDialogPlayEject.cpp)
+ list(APPEND HEADERS GUIDialogPlayEject.h)
+endif()
+
+core_add_library(dialogs)
diff --git a/xbmc/dialogs/GUIDialogBoxBase.cpp b/xbmc/dialogs/GUIDialogBoxBase.cpp
new file mode 100644
index 0000000..1d87949
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogBoxBase.cpp
@@ -0,0 +1,187 @@
+/*
+ * 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 "GUIDialogBoxBase.h"
+
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <mutex>
+
+#define CONTROL_HEADING 1
+#define CONTROL_LINES_START 2
+#define CONTROL_TEXTBOX 9
+
+CGUIDialogBoxBase::CGUIDialogBoxBase(int id, const std::string &xmlFile)
+ : CGUIDialog(id, xmlFile)
+{
+ m_bConfirmed = false;
+ m_loadType = KEEP_IN_MEMORY;
+ m_hasTextbox = false;
+}
+
+CGUIDialogBoxBase::~CGUIDialogBoxBase(void) = default;
+
+bool CGUIDialogBoxBase::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CGUIDialog::OnMessage(message);
+ m_bConfirmed = false;
+ return true;
+ }
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogBoxBase::IsConfirmed() const
+{
+ return m_bConfirmed;
+}
+
+void CGUIDialogBoxBase::SetHeading(const CVariant& heading)
+{
+ std::string label = GetLocalized(heading);
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (label != m_strHeading)
+ {
+ m_strHeading = label;
+ SetInvalid();
+ }
+}
+
+void CGUIDialogBoxBase::SetLine(unsigned int iLine, const CVariant& line)
+{
+ std::string label = GetLocalized(line);
+ std::unique_lock<CCriticalSection> lock(m_section);
+ std::vector<std::string> lines = StringUtils::Split(m_text, '\n');
+ if (iLine >= lines.size())
+ lines.resize(iLine+1);
+ lines[iLine] = label;
+ std::string text = StringUtils::Join(lines, "\n");
+ SetText(text);
+}
+
+void CGUIDialogBoxBase::SetText(const CVariant& text)
+{
+ std::string label = GetLocalized(text);
+ std::unique_lock<CCriticalSection> lock(m_section);
+ StringUtils::Trim(label, "\n");
+ if (label != m_text)
+ {
+ m_text = label;
+ SetInvalid();
+ }
+}
+
+void CGUIDialogBoxBase::SetChoice(int iButton, const CVariant &choice) // iButton == 0 for no, 1 for yes
+{
+ if (iButton < 0 || iButton >= DIALOG_MAX_CHOICES)
+ return;
+
+ std::string label = GetLocalized(choice);
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (label != m_strChoices[iButton])
+ {
+ m_strChoices[iButton] = label;
+ SetInvalid();
+ }
+}
+
+void CGUIDialogBoxBase::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_bInvalidated)
+ { // take a copy of our labels to save holding the lock for too long
+ std::string heading, text;
+ std::vector<std::string> choices;
+ choices.reserve(DIALOG_MAX_CHOICES);
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ heading = m_strHeading;
+ text = m_text;
+ for (const std::string& choice : m_strChoices)
+ choices.push_back(choice);
+ }
+ SET_CONTROL_LABEL(CONTROL_HEADING, heading);
+ if (m_hasTextbox)
+ {
+ SET_CONTROL_LABEL(CONTROL_TEXTBOX, text);
+ }
+ else
+ {
+ std::vector<std::string> lines = StringUtils::Split(text, "\n", DIALOG_MAX_LINES);
+ lines.resize(DIALOG_MAX_LINES);
+ for (size_t i = 0 ; i < lines.size(); ++i)
+ SET_CONTROL_LABEL(CONTROL_LINES_START + i, lines[i]);
+ }
+ for (size_t i = 0 ; i < choices.size() ; ++i)
+ SET_CONTROL_LABEL(CONTROL_CHOICES_START + i, choices[i]);
+ }
+ CGUIDialog::Process(currentTime, dirtyregions);
+}
+
+void CGUIDialogBoxBase::OnInitWindow()
+{
+ // set focus to default
+ m_lastControlID = m_defaultControl;
+
+ m_hasTextbox = false;
+ const CGUIControl *control = GetControl(CONTROL_TEXTBOX);
+ if (control && control->GetControlType() == CGUIControl::GUICONTROL_TEXTBOX)
+ m_hasTextbox = true;
+
+ // set initial labels
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for (int i = 0 ; i < DIALOG_MAX_CHOICES ; ++i)
+ {
+ if (m_strChoices[i].empty())
+ m_strChoices[i] = GetDefaultLabel(CONTROL_CHOICES_START + i);
+ }
+ }
+ CGUIDialog::OnInitWindow();
+}
+
+void CGUIDialogBoxBase::OnDeinitWindow(int nextWindowID)
+{
+ // make sure we set default labels for heading, lines and choices
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_strHeading.clear();
+ m_text.clear();
+ for (std::string& choice : m_strChoices)
+ choice.clear();
+ }
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+}
+
+std::string CGUIDialogBoxBase::GetLocalized(const CVariant &var) const
+{
+ if (var.isString())
+ return var.asString();
+ else if (var.isInteger() && var.asInteger())
+ return g_localizeStrings.Get((uint32_t)var.asInteger());
+ return "";
+}
+
+std::string CGUIDialogBoxBase::GetDefaultLabel(int controlId) const
+{
+ int labelId = GetDefaultLabelID(controlId);
+ return labelId != -1 ? g_localizeStrings.Get(labelId) : "";
+}
+
+int CGUIDialogBoxBase::GetDefaultLabelID(int controlId) const
+{
+ return -1;
+}
diff --git a/xbmc/dialogs/GUIDialogBoxBase.h b/xbmc/dialogs/GUIDialogBoxBase.h
new file mode 100644
index 0000000..41ab31b
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogBoxBase.h
@@ -0,0 +1,61 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "threads/CriticalSection.h"
+
+#include <vector>
+
+#define CONTROL_CHOICES_START 10
+#define CONTROL_NO_BUTTON CONTROL_CHOICES_START
+#define CONTROL_YES_BUTTON CONTROL_CHOICES_START + 1
+#define CONTROL_CUSTOM_BUTTON CONTROL_CHOICES_START + 2
+#define CONTROL_PROGRESS_BAR 20
+
+#define DIALOG_MAX_LINES 3
+#define DIALOG_MAX_CHOICES 3
+
+class CVariant;
+
+class CGUIDialogBoxBase :
+ public CGUIDialog
+{
+public:
+ CGUIDialogBoxBase(int id, const std::string &xmlFile);
+ ~CGUIDialogBoxBase(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool IsConfirmed() const;
+ void SetLine(unsigned int iLine, const CVariant& line);
+ void SetText(const CVariant& text);
+ void SetHeading(const CVariant& heading);
+ void SetChoice(int iButton, const CVariant &choice);
+protected:
+ std::string GetDefaultLabel(int controlId) const;
+ virtual int GetDefaultLabelID(int controlId) const;
+ /*! \brief Get a localized string from a variant
+ If the variant is already a string we return directly, else if it's an integer we return the corresponding
+ localized string.
+ \param var the variant to localize.
+ */
+ std::string GetLocalized(const CVariant &var) const;
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ bool m_bConfirmed;
+ bool m_hasTextbox;
+
+ // actual strings
+ CCriticalSection m_section;
+ std::string m_strHeading;
+ std::string m_text;
+ std::string m_strChoices[DIALOG_MAX_CHOICES];
+};
diff --git a/xbmc/dialogs/GUIDialogBusy.cpp b/xbmc/dialogs/GUIDialogBusy.cpp
new file mode 100644
index 0000000..e53f1fc
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogBusy.cpp
@@ -0,0 +1,147 @@
+/*
+ * 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 "GUIDialogBusy.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "threads/IRunnable.h"
+#include "threads/Thread.h"
+#include "utils/log.h"
+
+using namespace std::chrono_literals;
+
+class CBusyWaiter : public CThread
+{
+ std::shared_ptr<CEvent> m_done;
+ IRunnable *m_runnable;
+public:
+ explicit CBusyWaiter(IRunnable *runnable) :
+ CThread(runnable, "waiting"), m_done(new CEvent()), m_runnable(runnable) { }
+
+ ~CBusyWaiter() override { StopThread(); }
+
+ bool Wait(unsigned int displaytime, bool allowCancel)
+ {
+ std::shared_ptr<CEvent> e_done(m_done);
+
+ Create();
+ auto start = std::chrono::steady_clock::now();
+ if (!CGUIDialogBusy::WaitOnEvent(*e_done, displaytime, allowCancel))
+ {
+ m_runnable->Cancel();
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ unsigned int remaining =
+ (duration.count() >= displaytime) ? 0 : displaytime - duration.count();
+ CGUIDialogBusy::WaitOnEvent(*e_done, remaining, false);
+ return false;
+ }
+ return true;
+ }
+
+ // 'this' is actually deleted from the thread where it's on the stack
+ void Process() override
+ {
+ std::shared_ptr<CEvent> e_done(m_done);
+
+ CThread::Process();
+ (*e_done).Set();
+ }
+
+};
+
+bool CGUIDialogBusy::Wait(IRunnable *runnable, unsigned int displaytime, bool allowCancel)
+{
+ if (!runnable)
+ return false;
+ CBusyWaiter waiter(runnable);
+ if (!waiter.Wait(displaytime, allowCancel))
+ {
+ return false;
+ }
+ return true;
+}
+
+bool CGUIDialogBusy::WaitOnEvent(CEvent &event, unsigned int displaytime /* = 100 */, bool allowCancel /* = true */)
+{
+ bool cancelled = false;
+ if (!event.Wait(std::chrono::milliseconds(displaytime)))
+ {
+ // throw up the progress
+ CGUIDialogBusy* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogBusy>(WINDOW_DIALOG_BUSY);
+ if (dialog)
+ {
+ if (dialog->IsDialogRunning())
+ {
+ CLog::Log(LOGFATAL, "Logic error due to two concurrent busydialogs, this is a known issue. "
+ "The application will exit.");
+ throw std::logic_error("busy dialog already running");
+ }
+
+ dialog->Open();
+
+ while (!event.Wait(1ms))
+ {
+ dialog->ProcessRenderLoop(false);
+ if (allowCancel && dialog->IsCanceled())
+ {
+ cancelled = true;
+ break;
+ }
+ }
+
+ dialog->Close(true);
+ }
+ }
+ return !cancelled;
+}
+
+CGUIDialogBusy::CGUIDialogBusy(void)
+ : CGUIDialog(WINDOW_DIALOG_BUSY, "DialogBusy.xml", DialogModalityType::MODAL)
+{
+ m_loadType = LOAD_ON_GUI_INIT;
+ m_bCanceled = false;
+}
+
+CGUIDialogBusy::~CGUIDialogBusy(void) = default;
+
+void CGUIDialogBusy::Open_Internal(bool bProcessRenderLoop, const std::string& param /* = "" */)
+{
+ m_bCanceled = false;
+ m_bLastVisible = true;
+
+ CGUIDialog::Open_Internal(false, param);
+}
+
+
+void CGUIDialogBusy::DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ bool visible = CServiceBroker::GetGUI()->GetWindowManager().IsModalDialogTopmost(WINDOW_DIALOG_BUSY);
+ if(!visible && m_bLastVisible)
+ dirtyregions.push_back(CDirtyRegion(m_renderRegion));
+ m_bLastVisible = visible;
+
+ CGUIDialog::DoProcess(currentTime, dirtyregions);
+}
+
+void CGUIDialogBusy::Render()
+{
+ if(!m_bLastVisible)
+ return;
+ CGUIDialog::Render();
+}
+
+bool CGUIDialogBusy::OnBack(int actionID)
+{
+ m_bCanceled = true;
+ return true;
+}
diff --git a/xbmc/dialogs/GUIDialogBusy.h b/xbmc/dialogs/GUIDialogBusy.h
new file mode 100644
index 0000000..586d6a3
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogBusy.h
@@ -0,0 +1,51 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class IRunnable;
+class CEvent;
+
+class CGUIDialogBusy: public CGUIDialog
+{
+public:
+ CGUIDialogBusy(void);
+ ~CGUIDialogBusy(void) override;
+ bool OnBack(int actionID) override;
+ void DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ /*! \brief set the current progress of the busy operation
+ \param progress a percentage of progress
+ */
+ bool IsCanceled() { return m_bCanceled; }
+
+ /*! \brief Wait for a runnable to execute off-thread.
+ Creates a thread to run the given runnable, and while waiting
+ it displays the busy dialog.
+ \param runnable the IRunnable to run.
+ \param displaytime the time in ms to wait prior to showing the busy dialog (defaults to 100ms)
+ \param allowCancel whether the user can cancel the wait, defaults to true.
+ \return true if the runnable completes, false if the user cancels early.
+ */
+ static bool Wait(IRunnable *runnable, unsigned int displaytime, bool allowCancel);
+
+ /*! \brief Wait on an event while displaying the busy dialog.
+ Throws up the busy dialog after the given time.
+ \param event the CEvent to wait on.
+ \param displaytime the time in ms to wait prior to showing the busy dialog (defaults to 100ms)
+ \param allowCancel whether the user can cancel the wait, defaults to true.
+ \return true if the event completed, false if cancelled.
+ */
+ static bool WaitOnEvent(CEvent &event, unsigned int displaytime = 100, bool allowCancel = true);
+protected:
+ void Open_Internal(bool bProcessRenderLoop, const std::string& param = "") override;
+ bool m_bCanceled;
+ bool m_bLastVisible = false;
+};
diff --git a/xbmc/dialogs/GUIDialogBusyNoCancel.cpp b/xbmc/dialogs/GUIDialogBusyNoCancel.cpp
new file mode 100644
index 0000000..2bddd01
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogBusyNoCancel.cpp
@@ -0,0 +1,47 @@
+/*
+ * 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 "GUIDialogBusyNoCancel.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "threads/Thread.h"
+
+CGUIDialogBusyNoCancel::CGUIDialogBusyNoCancel(void)
+ : CGUIDialog(WINDOW_DIALOG_BUSY_NOCANCEL, "DialogBusy.xml", DialogModalityType::MODAL)
+{
+ m_loadType = LOAD_ON_GUI_INIT;
+}
+
+CGUIDialogBusyNoCancel::~CGUIDialogBusyNoCancel(void) = default;
+
+void CGUIDialogBusyNoCancel::Open_Internal(bool bProcessRenderLoop,
+ const std::string& param /* = "" */)
+{
+ m_bLastVisible = true;
+
+ CGUIDialog::Open_Internal(false, param);
+}
+
+void CGUIDialogBusyNoCancel::DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ bool visible = CServiceBroker::GetGUI()->GetWindowManager().IsModalDialogTopmost(WINDOW_DIALOG_BUSY_NOCANCEL);
+ if (!visible && m_bLastVisible)
+ dirtyregions.push_back(CDirtyRegion(m_renderRegion));
+ m_bLastVisible = visible;
+
+ CGUIDialog::DoProcess(currentTime, dirtyregions);
+}
+
+void CGUIDialogBusyNoCancel::Render()
+{
+ if(!m_bLastVisible)
+ return;
+ CGUIDialog::Render();
+}
diff --git a/xbmc/dialogs/GUIDialogBusyNoCancel.h b/xbmc/dialogs/GUIDialogBusyNoCancel.h
new file mode 100644
index 0000000..b50a080
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogBusyNoCancel.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIDialogBusyNoCancel: public CGUIDialog
+{
+public:
+ CGUIDialogBusyNoCancel(void);
+ ~CGUIDialogBusyNoCancel(void) override;
+ void DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+
+protected:
+ void Open_Internal(bool bProcessRenderLoop, const std::string& param = "") override;
+ bool m_bLastVisible = false;
+};
diff --git a/xbmc/dialogs/GUIDialogButtonMenu.cpp b/xbmc/dialogs/GUIDialogButtonMenu.cpp
new file mode 100644
index 0000000..04ed916
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogButtonMenu.cpp
@@ -0,0 +1,46 @@
+/*
+ * 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 "GUIDialogButtonMenu.h"
+
+#include "guilib/GUIMessage.h"
+
+#define CONTROL_BUTTON_LABEL 3100
+
+CGUIDialogButtonMenu::CGUIDialogButtonMenu(int id, const std::string &xmlFile)
+: CGUIDialog(id, xmlFile.c_str())
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogButtonMenu::~CGUIDialogButtonMenu(void) = default;
+
+bool CGUIDialogButtonMenu::OnMessage(CGUIMessage &message)
+{
+ bool bRet = CGUIDialog::OnMessage(message);
+ if (message.GetMessage() == GUI_MSG_CLICKED)
+ {
+ // someone has been clicked - deinit...
+ Close();
+ return true;
+ }
+ return bRet;
+}
+
+void CGUIDialogButtonMenu::FrameMove()
+{
+ // get the active control, and put it's label into the label control
+ const CGUIControl *pControl = GetFocusedControl();
+ if (pControl && (pControl->GetControlType() == CGUIControl::GUICONTROL_BUTTON ||
+ pControl->GetControlType() == CGUIControl::GUICONTROL_TOGGLEBUTTON ||
+ pControl->GetControlType() == CGUIControl::GUICONTROL_COLORBUTTON))
+ {
+ SET_CONTROL_LABEL(CONTROL_BUTTON_LABEL, pControl->GetDescription());
+ }
+ CGUIDialog::FrameMove();
+}
diff --git a/xbmc/dialogs/GUIDialogButtonMenu.h b/xbmc/dialogs/GUIDialogButtonMenu.h
new file mode 100644
index 0000000..60cb10c
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogButtonMenu.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIDialogButtonMenu :
+ public CGUIDialog
+{
+public:
+ CGUIDialogButtonMenu(int id = WINDOW_DIALOG_BUTTON_MENU, const std::string &xmlFile = "DialogButtonMenu.xml");
+ ~CGUIDialogButtonMenu(void) override;
+ bool OnMessage(CGUIMessage &message) override;
+ void FrameMove() override;
+};
diff --git a/xbmc/dialogs/GUIDialogCache.cpp b/xbmc/dialogs/GUIDialogCache.cpp
new file mode 100644
index 0000000..6ec8b20
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogCache.cpp
@@ -0,0 +1,178 @@
+/*
+ * 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 "GUIDialogCache.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "threads/SystemClock.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+CGUIDialogCache::CGUIDialogCache(std::chrono::milliseconds delay,
+ const std::string& strHeader,
+ const std::string& strMsg)
+ : CThread("GUIDialogCache"), m_strHeader(strHeader), m_strLinePrev(strMsg)
+{
+ bSentCancel = false;
+
+ m_pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+
+ if (!m_pDlg)
+ return;
+
+ /* if progress dialog is already running, take it over */
+ if( m_pDlg->IsDialogRunning() )
+ delay = 0ms;
+
+ if (delay == 0ms)
+ OpenDialog();
+ else
+ m_endtime.Set(delay);
+
+ Create(true);
+}
+
+void CGUIDialogCache::Close(bool bForceClose)
+{
+ bSentCancel = true;
+
+ // we cannot wait for the app thread to process the close message
+ // as this might happen during player startup which leads to a deadlock
+ if (m_pDlg && m_pDlg->IsDialogRunning())
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_WINDOW_CLOSE, -1, bForceClose ? 1 : 0,
+ static_cast<void*>(m_pDlg));
+
+ //Set stop, this will kill this object, when thread stops
+ CThread::m_bStop = true;
+}
+
+CGUIDialogCache::~CGUIDialogCache()
+{
+ if(m_pDlg && m_pDlg->IsDialogRunning())
+ m_pDlg->Close();
+}
+
+void CGUIDialogCache::OpenDialog()
+{
+ if (m_pDlg)
+ {
+ if (m_strHeader.empty())
+ m_pDlg->SetHeading(CVariant{438});
+ else
+ m_pDlg->SetHeading(CVariant{m_strHeader});
+
+ m_pDlg->SetLine(2, CVariant{m_strLinePrev});
+ m_pDlg->Open();
+ }
+ bSentCancel = false;
+}
+
+void CGUIDialogCache::SetHeader(const std::string& strHeader)
+{
+ m_strHeader = strHeader;
+}
+
+void CGUIDialogCache::SetHeader(int nHeader)
+{
+ SetHeader(g_localizeStrings.Get(nHeader));
+}
+
+void CGUIDialogCache::SetMessage(const std::string& strMessage)
+{
+ if (m_pDlg)
+ {
+ m_pDlg->SetLine(0, CVariant{m_strLinePrev2});
+ m_pDlg->SetLine(1, CVariant{m_strLinePrev});
+ m_pDlg->SetLine(2, CVariant{strMessage});
+ }
+ m_strLinePrev2 = m_strLinePrev;
+ m_strLinePrev = strMessage;
+}
+
+bool CGUIDialogCache::OnFileCallback(void* pContext, int ipercent, float avgSpeed)
+{
+ if (m_pDlg)
+ {
+ m_pDlg->ShowProgressBar(true);
+ m_pDlg->SetPercentage(ipercent);
+ }
+
+ if( IsCanceled() )
+ return false;
+ else
+ return true;
+}
+
+void CGUIDialogCache::Process()
+{
+ if (!m_pDlg)
+ return;
+
+ while( true )
+ {
+
+ { //Section to make the lock go out of scope before sleep
+
+ if( CThread::m_bStop ) break;
+
+ try
+ {
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ m_pDlg->Progress();
+ if( bSentCancel )
+ {
+ CThread::Sleep(10ms);
+ continue;
+ }
+
+ if(m_pDlg->IsCanceled())
+ {
+ bSentCancel = true;
+ }
+ else if( !m_pDlg->IsDialogRunning() && m_endtime.IsTimePast()
+ && !CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_YES_NO) )
+ OpenDialog();
+ }
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "Exception in CGUIDialogCache::Process()");
+ }
+ }
+
+ CThread::Sleep(10ms);
+ }
+}
+
+void CGUIDialogCache::ShowProgressBar(bool bOnOff)
+{
+ if (m_pDlg)
+ m_pDlg->ShowProgressBar(bOnOff);
+}
+
+void CGUIDialogCache::SetPercentage(int iPercentage)
+{
+ if (m_pDlg)
+ m_pDlg->SetPercentage(iPercentage);
+}
+
+bool CGUIDialogCache::IsCanceled() const
+{
+ if (m_pDlg && m_pDlg->IsDialogRunning())
+ return m_pDlg->IsCanceled();
+ else
+ return false;
+}
diff --git a/xbmc/dialogs/GUIDialogCache.h b/xbmc/dialogs/GUIDialogCache.h
new file mode 100644
index 0000000..3166cbe
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogCache.h
@@ -0,0 +1,48 @@
+/*
+ * 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 "filesystem/IFileTypes.h"
+#include "threads/SystemClock.h"
+#include "threads/Thread.h"
+
+#include <string>
+
+class CGUIDialogProgress;
+
+class CGUIDialogCache : public CThread, public XFILE::IFileCallback
+{
+public:
+ CGUIDialogCache(std::chrono::milliseconds delay = std::chrono::milliseconds(100),
+ const std::string& strHeader = "",
+ const std::string& strMsg = "");
+ ~CGUIDialogCache() override;
+ void SetHeader(const std::string& strHeader);
+ void SetHeader(int nHeader);
+ void SetMessage(const std::string& strMessage);
+ bool IsCanceled() const;
+ void ShowProgressBar(bool bOnOff);
+ void SetPercentage(int iPercentage);
+
+ void Close(bool bForceClose = false);
+
+ void Process() override;
+ bool OnFileCallback(void* pContext, int ipercent, float avgSpeed) override;
+
+protected:
+
+ void OpenDialog();
+
+ XbmcThreads::EndTime<> m_endtime;
+ CGUIDialogProgress* m_pDlg;
+ std::string m_strHeader;
+ std::string m_strLinePrev;
+ std::string m_strLinePrev2;
+ bool bSentCancel;
+};
diff --git a/xbmc/dialogs/GUIDialogColorPicker.cpp b/xbmc/dialogs/GUIDialogColorPicker.cpp
new file mode 100644
index 0000000..77cee11
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogColorPicker.cpp
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2005-2021 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 "GUIDialogColorPicker.h"
+
+#include "FileItem.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIColorManager.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "utils/ColorUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <utility>
+#include <vector>
+
+#define CONTROL_HEADING 1
+#define CONTROL_NUMBER_OF_ITEMS 2
+#define CONTROL_ICON_LIST 6
+#define CONTROL_CANCEL_BUTTON 7
+
+CGUIDialogColorPicker::CGUIDialogColorPicker()
+ : CGUIDialogBoxBase(WINDOW_DIALOG_COLOR_PICKER, "DialogColorPicker.xml"),
+ m_vecList(new CFileItemList()),
+ m_focusToButton(false)
+{
+ m_bConfirmed = false;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogColorPicker::~CGUIDialogColorPicker()
+{
+ delete m_vecList;
+}
+
+bool CGUIDialogColorPicker::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ CGUIDialogBoxBase::OnMessage(message);
+ m_selectedColor.clear();
+ for (int i = 0; i < m_vecList->Size(); i++)
+ {
+ CFileItemPtr item = m_vecList->Get(i);
+ if (item->IsSelected())
+ {
+ m_selectedColor = item->GetLabel2();
+ break;
+ }
+ }
+ m_vecList->Clear();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_bConfirmed = false;
+ CGUIDialogBoxBase::OnMessage(message);
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (m_viewControl.HasControl(CONTROL_ICON_LIST))
+ {
+ int iAction = message.GetParam1();
+ if (ACTION_SELECT_ITEM == iAction || ACTION_MOUSE_LEFT_CLICK == iAction)
+ {
+ int iSelected = m_viewControl.GetSelectedItem();
+ if (iSelected >= 0 && iSelected < m_vecList->Size())
+ {
+ CFileItemPtr item(m_vecList->Get(iSelected));
+ for (int i = 0; i < m_vecList->Size(); i++)
+ m_vecList->Get(i)->Select(false);
+ item->Select(true);
+ OnSelect(iSelected);
+ }
+ }
+ }
+ if (iControl == CONTROL_CANCEL_BUTTON)
+ {
+ m_selectedColor.clear();
+ m_vecList->Clear();
+ m_bConfirmed = false;
+ Close();
+ }
+ }
+ break;
+
+ case GUI_MSG_SETFOCUS:
+ {
+ if (m_viewControl.HasControl(message.GetControlId()))
+ {
+ if (m_vecList->IsEmpty())
+ {
+ SET_CONTROL_FOCUS(CONTROL_CANCEL_BUTTON, 0);
+ return true;
+ }
+ if (m_viewControl.GetCurrentControl() != message.GetControlId())
+ {
+ m_viewControl.SetFocused();
+ return true;
+ }
+ }
+ }
+ break;
+ }
+
+ return CGUIDialogBoxBase::OnMessage(message);
+}
+
+void CGUIDialogColorPicker::OnSelect(int idx)
+{
+ m_bConfirmed = true;
+ Close();
+}
+
+bool CGUIDialogColorPicker::OnBack(int actionID)
+{
+ m_vecList->Clear();
+ m_bConfirmed = false;
+ return CGUIDialogBoxBase::OnBack(actionID);
+}
+
+void CGUIDialogColorPicker::Reset()
+{
+ m_focusToButton = false;
+ m_selectedColor.clear();
+ m_vecList->Clear();
+}
+
+void CGUIDialogColorPicker::AddItem(const CFileItem& item)
+{
+ m_vecList->Add(CFileItemPtr(new CFileItem(item)));
+}
+
+void CGUIDialogColorPicker::SetItems(const CFileItemList& pList)
+{
+ // need to make internal copy of list to be sure dialog is owner of it
+ m_vecList->Clear();
+ m_vecList->Copy(pList);
+}
+
+void CGUIDialogColorPicker::LoadColors()
+{
+ LoadColors(CSpecialProtocol::TranslatePathConvertCase("special://xbmc/system/dialogcolors.xml"));
+}
+
+void CGUIDialogColorPicker::LoadColors(const std::string& filePath)
+{
+ CGUIColorManager colorManager;
+ std::vector<std::pair<std::string, UTILS::COLOR::ColorInfo>> colors;
+ if (colorManager.LoadColorsListFromXML(filePath, colors, true))
+ {
+ for (auto& color : colors)
+ {
+ CFileItem* item = new CFileItem(color.first);
+ item->SetLabel2(StringUtils::Format("{:08X}", color.second.colorARGB));
+ m_vecList->Add(CFileItemPtr(item));
+ }
+ }
+ else
+ CLog::Log(LOGERROR, "{} - An error occurred when loading colours from the xml file.",
+ __FUNCTION__);
+}
+
+std::string CGUIDialogColorPicker::GetSelectedColor() const
+{
+ return m_selectedColor;
+}
+
+int CGUIDialogColorPicker::GetSelectedItem() const
+{
+ if (m_selectedColor.empty())
+ return -1;
+ for (int index = 0; index < m_vecList->Size(); index++)
+ {
+ if (StringUtils::CompareNoCase(m_selectedColor, m_vecList->Get(index)->GetLabel2()) == 0)
+ {
+ return index;
+ }
+ }
+ return -1;
+}
+
+void CGUIDialogColorPicker::SetSelectedColor(const std::string& colorHex)
+{
+ m_selectedColor = colorHex;
+}
+
+void CGUIDialogColorPicker::SetButtonFocus(bool buttonFocus)
+{
+ m_focusToButton = buttonFocus;
+}
+
+CGUIControl* CGUIDialogColorPicker::GetFirstFocusableControl(int id)
+{
+ if (m_viewControl.HasControl(id))
+ id = m_viewControl.GetCurrentControl();
+ return CGUIDialogBoxBase::GetFirstFocusableControl(id);
+}
+
+void CGUIDialogColorPicker::OnWindowLoaded()
+{
+ CGUIDialogBoxBase::OnWindowLoaded();
+ m_viewControl.Reset();
+ m_viewControl.SetParentWindow(GetID());
+ m_viewControl.AddView(GetControl(CONTROL_ICON_LIST));
+}
+
+void CGUIDialogColorPicker::OnInitWindow()
+{
+ if (!m_vecList || m_vecList->IsEmpty())
+ CLog::Log(LOGERROR, "{} - No colours have been added in the list.", __FUNCTION__);
+
+ m_viewControl.SetItems(*m_vecList);
+ m_viewControl.SetCurrentView(CONTROL_ICON_LIST);
+
+ SET_CONTROL_LABEL(CONTROL_NUMBER_OF_ITEMS,
+ StringUtils::Format("{} {}", m_vecList->Size(), g_localizeStrings.Get(127)));
+
+ SET_CONTROL_LABEL(CONTROL_CANCEL_BUTTON, g_localizeStrings.Get(222));
+
+ CGUIDialogBoxBase::OnInitWindow();
+
+ // focus one of the buttons if explicitly requested
+ // ATTENTION: this must be done after calling CGUIDialogBoxBase::OnInitWindow()
+ if (m_focusToButton)
+ {
+ SET_CONTROL_FOCUS(CONTROL_CANCEL_BUTTON, 0);
+ }
+
+ // if nothing is selected, select first item
+ m_viewControl.SetSelectedItem(std::max(GetSelectedItem(), 0));
+}
+
+void CGUIDialogColorPicker::OnDeinitWindow(int nextWindowID)
+{
+ m_viewControl.Clear();
+ CGUIDialogBoxBase::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIDialogColorPicker::OnWindowUnload()
+{
+ CGUIDialogBoxBase::OnWindowUnload();
+ m_viewControl.Reset();
+}
diff --git a/xbmc/dialogs/GUIDialogColorPicker.h b/xbmc/dialogs/GUIDialogColorPicker.h
new file mode 100644
index 0000000..1e7c65d
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogColorPicker.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2005-2021 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 "GUIDialogBoxBase.h"
+#include "view/GUIViewControl.h"
+
+#include <string>
+
+class CFileItem;
+class CFileItemList;
+
+class CGUIDialogColorPicker : public CGUIDialogBoxBase
+{
+public:
+ CGUIDialogColorPicker();
+ ~CGUIDialogColorPicker() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnBack(int actionID) override;
+ void Reset();
+ /*! \brief Add a color item (Label property must be the color name, Label2 property must be the color hex)
+ \param item The CFileItem
+ */
+ void AddItem(const CFileItem& item);
+ /*! \brief Set a list of color items (Label property must be the color name, Label2 property must be the color hex)
+ \param pList The CFileItemList
+ */
+ void SetItems(const CFileItemList& pList);
+ /*! \brief Load a list of colors from the default xml */
+ void LoadColors();
+ /*! \brief Load a list of colors from the specified xml file path
+ \param filePath The xml file path
+ */
+ void LoadColors(const std::string& filePath);
+ /*! \brief Get the hex value of the selected color */
+ std::string GetSelectedColor() const;
+ /*! \brief Set the selected color by hex value */
+ void SetSelectedColor(const std::string& hexColor);
+ /*! \brief Set the focus to the control button */
+ void SetButtonFocus(bool buttonFocus);
+
+protected:
+ CGUIControl* GetFirstFocusableControl(int id) override;
+ void OnWindowLoaded() override;
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ void OnWindowUnload() override;
+
+ virtual void OnSelect(int idx);
+
+private:
+ int GetSelectedItem() const;
+
+ CGUIViewControl m_viewControl;
+ CFileItemList* m_vecList;
+ bool m_focusToButton;
+ std::string m_selectedColor;
+};
diff --git a/xbmc/dialogs/GUIDialogContextMenu.cpp b/xbmc/dialogs/GUIDialogContextMenu.cpp
new file mode 100644
index 0000000..dcc9ede
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogContextMenu.cpp
@@ -0,0 +1,705 @@
+/*
+ * Copyright (C) 2005-2020 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 "GUIDialogContextMenu.h"
+
+#include "FileItem.h"
+#include "GUIDialogFileBrowser.h"
+#include "GUIDialogMediaSource.h"
+#include "GUIDialogYesNo.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/Scraper.h"
+#include "favourites/FavouritesService.h"
+#include "guilib/GUIButtonControl.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIControlGroupList.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "media/MediaLockState.h"
+#include "profiles/ProfileManager.h"
+#include "profiles/dialogs/GUIDialogLockSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+#define BACKGROUND_IMAGE 999
+#define GROUP_LIST 996
+#define BUTTON_TEMPLATE 1000
+#define BUTTON_START 1001
+#define BUTTON_END (BUTTON_START + (int)m_buttons.size() - 1)
+
+void CContextButtons::Add(unsigned int button, const std::string &label)
+{
+ for (const auto& i : *this)
+ if (i.first == button)
+ return; // already have this button
+ push_back(std::pair<unsigned int, std::string>(button, label));
+}
+
+void CContextButtons::Add(unsigned int button, int label)
+{
+ for (const auto& i : *this)
+ if (i.first == button)
+ return; // already have added this button
+ push_back(std::pair<unsigned int, std::string>(button, g_localizeStrings.Get(label)));
+}
+
+CGUIDialogContextMenu::CGUIDialogContextMenu(void)
+ : CGUIDialog(WINDOW_DIALOG_CONTEXT_MENU, "DialogContextMenu.xml")
+{
+ m_clickedButton = -1;
+ m_backgroundImageSize = 0;
+ m_loadType = KEEP_IN_MEMORY;
+ m_coordX = 0.0f;
+ m_coordY = 0.0f;
+}
+
+CGUIDialogContextMenu::~CGUIDialogContextMenu(void) = default;
+
+bool CGUIDialogContextMenu::OnMessage(CGUIMessage &message)
+{
+ if (message.GetMessage() == GUI_MSG_CLICKED)
+ { // someone has been clicked - deinit...
+ if (message.GetSenderId() >= BUTTON_START && message.GetSenderId() <= BUTTON_END)
+ m_clickedButton = message.GetSenderId() - BUTTON_START;
+ Close();
+ return true;
+ }
+ else if (message.GetMessage() == GUI_MSG_PLAYBACK_AVSTARTED)
+ {
+ // playback was just started from elsewhere - close the dialog
+ Close();
+ return true;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogContextMenu::OnAction(const CAction& action)
+{
+ if (action.GetID() == ACTION_CONTEXT_MENU ||
+ action.GetID() == ACTION_SWITCH_PLAYER)
+ {
+ Close();
+ return true;
+ }
+
+ return CGUIDialog::OnAction(action);
+}
+
+void CGUIDialogContextMenu::OnInitWindow()
+{
+ m_clickedButton = -1;
+ // set initial control focus
+ m_lastControlID = m_initiallyFocusedButtonIdx + BUTTON_START;
+ CGUIDialog::OnInitWindow();
+}
+
+void CGUIDialogContextMenu::SetupButtons()
+{
+ if (!m_buttons.size())
+ return;
+
+ // disable the template button control
+ CGUIButtonControl *pButtonTemplate = dynamic_cast<CGUIButtonControl *>(GetFirstFocusableControl(BUTTON_TEMPLATE));
+ if (!pButtonTemplate)
+ pButtonTemplate = dynamic_cast<CGUIButtonControl *>(GetControl(BUTTON_TEMPLATE));
+ if (!pButtonTemplate)
+ return;
+ pButtonTemplate->SetVisible(false);
+
+ CGUIControlGroupList* pGroupList = dynamic_cast<CGUIControlGroupList *>(GetControl(GROUP_LIST));
+
+ // add our buttons
+ if (pGroupList)
+ {
+ for (unsigned int i = 0; i < m_buttons.size(); i++)
+ {
+ CGUIButtonControl* pButton = new CGUIButtonControl(*pButtonTemplate);
+ if (pButton)
+ { // set the button's ID and position
+ int id = BUTTON_START + i;
+ pButton->SetID(id);
+ pButton->SetVisible(true);
+ pButton->SetLabel(m_buttons[i].second);
+ pButton->SetPosition(pButtonTemplate->GetXPosition(), pButtonTemplate->GetYPosition());
+ // try inserting context buttons at position specified by template
+ // button, if template button is not in grouplist fallback to adding
+ // new buttons at the end of grouplist
+ if (!pGroupList->InsertControl(pButton, pButtonTemplate))
+ pGroupList->AddControl(pButton);
+ }
+ }
+ }
+
+ // fix up background images placement and size
+ CGUIControl *pControl = GetControl(BACKGROUND_IMAGE);
+ if (pControl)
+ {
+ // first set size of background image
+ if (pGroupList)
+ {
+ if (pGroupList->GetOrientation() == VERTICAL)
+ {
+ // keep gap between bottom edges of grouplist and background image
+ pControl->SetHeight(m_backgroundImageSize - pGroupList->Size() + pGroupList->GetHeight());
+ }
+ else
+ {
+ // keep gap between right edges of grouplist and background image
+ pControl->SetWidth(m_backgroundImageSize - pGroupList->Size() + pGroupList->GetWidth());
+ }
+ }
+ }
+
+ // update our default control
+ if (pGroupList)
+ m_defaultControl = pGroupList->GetID();
+}
+
+void CGUIDialogContextMenu::SetPosition(float posX, float posY)
+{
+ if (posY + GetHeight() > m_coordsRes.iHeight)
+ posY = m_coordsRes.iHeight - GetHeight();
+ if (posY < 0) posY = 0;
+ if (posX + GetWidth() > m_coordsRes.iWidth)
+ posX = m_coordsRes.iWidth - GetWidth();
+ if (posX < 0) posX = 0;
+ CGUIDialog::SetPosition(posX, posY);
+}
+
+float CGUIDialogContextMenu::GetHeight() const
+{
+ if (m_backgroundImage)
+ return m_backgroundImage->GetHeight();
+ else
+ return CGUIDialog::GetHeight();
+}
+
+float CGUIDialogContextMenu::GetWidth() const
+{
+ if (m_backgroundImage)
+ return m_backgroundImage->GetWidth();
+ else
+ return CGUIDialog::GetWidth();
+}
+
+bool CGUIDialogContextMenu::SourcesMenu(const std::string &strType, const CFileItemPtr& item, float posX, float posY)
+{
+ //! @todo This should be callable even if we don't have any valid items
+ if (!item)
+ return false;
+
+ // grab our context menu
+ CContextButtons buttons;
+ GetContextButtons(strType, item, buttons);
+
+ int button = ShowAndGetChoice(buttons);
+ if (button >= 0)
+ return OnContextButton(strType, item, (CONTEXT_BUTTON)button);
+ return false;
+}
+
+void CGUIDialogContextMenu::GetContextButtons(const std::string &type, const CFileItemPtr& item, CContextButtons &buttons)
+{
+ // Add buttons to the ContextMenu that should be visible for both sources and autosourced items
+ // Optical removable drives automatically have the static Eject button added (see CEjectDisk).
+ // Here we only add the eject button to HDD drives
+ if (item && item->IsRemovable() && !item->IsDVD() && !item->IsCDDA())
+ {
+ buttons.Add(CONTEXT_BUTTON_EJECT_DRIVE, 13420); // Remove safely
+ }
+
+ // Next, Add buttons to the ContextMenu that should ONLY be visible for sources and not autosourced items
+ CMediaSource *share = GetShare(type, item.get());
+
+ if (CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetCurrentProfile().canWriteSources() || g_passwordManager.bMasterUser)
+ {
+ if (share)
+ {
+ // Note. from now on, remove source & disable plugin should mean the same thing
+ //! @todo might be smart to also combine editing source & plugin settings into one concept/dialog
+ // Note. Temporarily disabled ability to remove plugin sources until installer is operational
+
+ CURL url(share->strPath);
+ bool isAddon = ADDON::TranslateContent(url.GetProtocol()) != CONTENT_NONE;
+ if (!share->m_ignore && !isAddon)
+ buttons.Add(CONTEXT_BUTTON_EDIT_SOURCE, 1027); // Edit Source
+ if (type != "video")
+ buttons.Add(CONTEXT_BUTTON_SET_DEFAULT, 13335); // Set as Default
+ if (!share->m_ignore && !isAddon)
+ buttons.Add(CONTEXT_BUTTON_REMOVE_SOURCE, 522); // Remove Source
+
+ buttons.Add(CONTEXT_BUTTON_SET_THUMB, 20019);
+ }
+ if (!GetDefaultShareNameByType(type).empty())
+ buttons.Add(CONTEXT_BUTTON_CLEAR_DEFAULT, 13403); // Clear Default
+ }
+ if (share && LOCK_MODE_EVERYONE != CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetMasterProfile().getLockMode())
+ {
+ if (share->m_iHasLock == LOCK_STATE_NO_LOCK && (CServiceBroker::GetSettingsComponent()
+ ->GetProfileManager()
+ ->GetCurrentProfile()
+ .canWriteSources() ||
+ g_passwordManager.bMasterUser))
+ buttons.Add(CONTEXT_BUTTON_ADD_LOCK, 12332);
+ else if (share->m_iHasLock == LOCK_STATE_LOCK_BUT_UNLOCKED)
+ buttons.Add(CONTEXT_BUTTON_REMOVE_LOCK, 12335);
+ else if (share->m_iHasLock == LOCK_STATE_LOCKED)
+ {
+ buttons.Add(CONTEXT_BUTTON_REMOVE_LOCK, 12335);
+
+ bool maxRetryExceeded = false;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MASTERLOCK_MAXRETRIES) != 0)
+ maxRetryExceeded = (share->m_iBadPwdCount >= CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MASTERLOCK_MAXRETRIES));
+
+ if (maxRetryExceeded)
+ buttons.Add(CONTEXT_BUTTON_RESET_LOCK, 12334);
+ else
+ buttons.Add(CONTEXT_BUTTON_CHANGE_LOCK, 12356);
+ }
+ }
+ if (share && !g_passwordManager.bMasterUser && item->m_iHasLock == LOCK_STATE_LOCK_BUT_UNLOCKED)
+ buttons.Add(CONTEXT_BUTTON_REACTIVATE_LOCK, 12353);
+}
+
+bool CGUIDialogContextMenu::OnContextButton(const std::string &type, const CFileItemPtr& item, CONTEXT_BUTTON button)
+{
+ // buttons that are available on both sources and autosourced items
+ if (!item)
+ return false;
+
+ switch (button)
+ {
+ case CONTEXT_BUTTON_EJECT_DRIVE:
+ return CServiceBroker::GetMediaManager().Eject(item->GetPath());
+ default:
+ break;
+ }
+
+ // the rest of the operations require a valid share
+ CMediaSource *share = GetShare(type, item.get());
+ if (!share)
+ return false;
+
+ switch (button)
+ {
+ case CONTEXT_BUTTON_EDIT_SOURCE:
+ if (CServiceBroker::GetSettingsComponent()->GetProfileManager()->IsMasterProfile())
+ {
+ if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return false;
+ }
+ else if (!g_passwordManager.IsProfileLockUnlocked())
+ return false;
+
+ return CGUIDialogMediaSource::ShowAndEditMediaSource(type, *share);
+
+ case CONTEXT_BUTTON_REMOVE_SOURCE:
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetProfileManager()->IsMasterProfile())
+ {
+ if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return false;
+ }
+ else
+ {
+ if (!CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetCurrentProfile().canWriteSources() && !g_passwordManager.IsMasterLockUnlocked(false))
+ return false;
+ if (CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetCurrentProfile().canWriteSources() && !g_passwordManager.IsProfileLockUnlocked())
+ return false;
+ }
+ // prompt user if they want to really delete the source
+ if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{751}, CVariant{750}))
+ return false;
+
+ // check default before we delete, as deletion will kill the share object
+ std::string defaultSource(GetDefaultShareNameByType(type));
+ if (!defaultSource.empty())
+ {
+ if (share->strName == defaultSource)
+ ClearDefault(type);
+ }
+ CMediaSourceSettings::GetInstance().DeleteSource(type, share->strName, share->strPath);
+ return true;
+ }
+ case CONTEXT_BUTTON_SET_DEFAULT:
+ if (CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetCurrentProfile().canWriteSources() && !g_passwordManager.IsProfileLockUnlocked())
+ return false;
+ else if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return false;
+
+ // make share default
+ SetDefault(type, share->strName);
+ return true;
+
+ case CONTEXT_BUTTON_CLEAR_DEFAULT:
+ if (CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetCurrentProfile().canWriteSources() && !g_passwordManager.IsProfileLockUnlocked())
+ return false;
+ else if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return false;
+ // remove share default
+ ClearDefault(type);
+ return true;
+
+ case CONTEXT_BUTTON_SET_THUMB:
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetCurrentProfile().canWriteSources() && !g_passwordManager.IsProfileLockUnlocked())
+ return false;
+ else if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return false;
+
+ // setup our thumb list
+ CFileItemList items;
+
+ // add the current thumb, if available
+ if (!share->m_strThumbnailImage.empty())
+ {
+ CFileItemPtr current(new CFileItem("thumb://Current", false));
+ current->SetArt("thumb", share->m_strThumbnailImage);
+ current->SetLabel(g_localizeStrings.Get(20016));
+ items.Add(current);
+ }
+ else if (item->HasArt("thumb"))
+ { // already have a thumb that the share doesn't know about - must be a local one, so we mayaswell reuse it.
+ CFileItemPtr current(new CFileItem("thumb://Current", false));
+ current->SetArt("thumb", item->GetArt("thumb"));
+ current->SetLabel(g_localizeStrings.Get(20016));
+ items.Add(current);
+ }
+ // see if there's a local thumb for this item
+ std::string folderThumb = item->GetFolderThumb();
+ if (CFileUtils::Exists(folderThumb))
+ {
+ CFileItemPtr local(new CFileItem("thumb://Local", false));
+ local->SetArt("thumb", folderThumb);
+ local->SetLabel(g_localizeStrings.Get(20017));
+ items.Add(local);
+ }
+ // and add a "no thumb" entry as well
+ CFileItemPtr nothumb(new CFileItem("thumb://None", false));
+ nothumb->SetArt("icon", item->GetArt("icon"));
+ nothumb->SetLabel(g_localizeStrings.Get(20018));
+ items.Add(nothumb);
+
+ std::string strThumb;
+ VECSOURCES shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ if (!CGUIDialogFileBrowser::ShowAndGetImage(items, shares, g_localizeStrings.Get(1030), strThumb))
+ return false;
+
+ if (strThumb == "thumb://Current")
+ return true;
+
+ if (strThumb == "thumb://Local")
+ strThumb = folderThumb;
+
+ if (strThumb == "thumb://None")
+ strThumb = "";
+
+ if (!share->m_ignore)
+ {
+ CMediaSourceSettings::GetInstance().UpdateSource(type,share->strName,"thumbnail",strThumb);
+ CMediaSourceSettings::GetInstance().Save();
+ }
+ else if (!strThumb.empty())
+ { // this is some sort of an auto-share, so store in the texture database
+ CTextureDatabase db;
+ if (db.Open())
+ db.SetTextureForPath(item->GetPath(), "thumb", strThumb);
+ }
+
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_UPDATE_SOURCES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ return true;
+ }
+
+ case CONTEXT_BUTTON_ADD_LOCK:
+ {
+ // prompt user for mastercode when changing lock settings) only for default user
+ if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return false;
+
+ std::string strNewPassword = "";
+ if (!CGUIDialogLockSettings::ShowAndGetLock(share->m_iLockMode,strNewPassword))
+ return false;
+ // password entry and re-entry succeeded, write out the lock data
+ share->m_iHasLock = LOCK_STATE_LOCKED;
+ CMediaSourceSettings::GetInstance().UpdateSource(type, share->strName, "lockcode", strNewPassword);
+ strNewPassword = std::to_string(share->m_iLockMode);
+ CMediaSourceSettings::GetInstance().UpdateSource(type, share->strName, "lockmode", strNewPassword);
+ CMediaSourceSettings::GetInstance().UpdateSource(type, share->strName, "badpwdcount", "0");
+ CMediaSourceSettings::GetInstance().Save();
+
+ // lock of a mediasource has been added
+ // => refresh favourites due to possible visibility changes
+ CServiceBroker::GetFavouritesService().RefreshFavourites();
+
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_UPDATE_SOURCES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ return true;
+ }
+ case CONTEXT_BUTTON_RESET_LOCK:
+ {
+ // prompt user for profile lock when changing lock settings
+ if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return false;
+
+ CMediaSourceSettings::GetInstance().UpdateSource(type, share->strName, "badpwdcount", "0");
+ CMediaSourceSettings::GetInstance().Save();
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_UPDATE_SOURCES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ return true;
+ }
+ case CONTEXT_BUTTON_REMOVE_LOCK:
+ {
+ if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return false;
+
+ // prompt user if they want to really remove the lock
+ if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{12335}, CVariant{750}))
+ return false;
+
+ share->m_iHasLock = LOCK_STATE_NO_LOCK;
+ CMediaSourceSettings::GetInstance().UpdateSource(type, share->strName, "lockmode", "0");
+ CMediaSourceSettings::GetInstance().UpdateSource(type, share->strName, "lockcode", "0");
+ CMediaSourceSettings::GetInstance().UpdateSource(type, share->strName, "badpwdcount", "0");
+ CMediaSourceSettings::GetInstance().Save();
+
+ // lock of a mediasource has been removed
+ // => refresh favourites due to possible visibility changes
+ CServiceBroker::GetFavouritesService().RefreshFavourites();
+
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_UPDATE_SOURCES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ return true;
+ }
+ case CONTEXT_BUTTON_REACTIVATE_LOCK:
+ {
+ bool maxRetryExceeded = false;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MASTERLOCK_MAXRETRIES) != 0)
+ maxRetryExceeded = (share->m_iBadPwdCount >= CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MASTERLOCK_MAXRETRIES));
+ if (!maxRetryExceeded)
+ {
+ // don't prompt user for mastercode when reactivating a lock
+ g_passwordManager.LockSource(type, share->strName, true);
+
+ // lock of a mediasource has been reactivated
+ // => refresh favourites due to possible visibility changes
+ CServiceBroker::GetFavouritesService().RefreshFavourites();
+ return true;
+ }
+ return false;
+ }
+ case CONTEXT_BUTTON_CHANGE_LOCK:
+ {
+ if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return false;
+
+ std::string strNewPW;
+ std::string strNewLockMode;
+ if (CGUIDialogLockSettings::ShowAndGetLock(share->m_iLockMode,strNewPW))
+ strNewLockMode = std::to_string(share->m_iLockMode);
+ else
+ return false;
+ // password ReSet and re-entry succeeded, write out the lock data
+ CMediaSourceSettings::GetInstance().UpdateSource(type, share->strName, "lockcode", strNewPW);
+ CMediaSourceSettings::GetInstance().UpdateSource(type, share->strName, "lockmode", strNewLockMode);
+ CMediaSourceSettings::GetInstance().UpdateSource(type, share->strName, "badpwdcount", "0");
+ CMediaSourceSettings::GetInstance().Save();
+
+ // lock of a mediasource has been changed
+ // => refresh favourites due to possible visibility changes
+ CServiceBroker::GetFavouritesService().RefreshFavourites();
+
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_UPDATE_SOURCES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ return true;
+ }
+ default:
+ break;
+ }
+ return false;
+}
+
+CMediaSource *CGUIDialogContextMenu::GetShare(const std::string &type, const CFileItem *item)
+{
+ VECSOURCES *shares = CMediaSourceSettings::GetInstance().GetSources(type);
+ if (!shares || !item)
+ return nullptr;
+ for (unsigned int i = 0; i < shares->size(); i++)
+ {
+ CMediaSource &testShare = shares->at(i);
+ if (URIUtils::IsDVD(testShare.strPath))
+ {
+ if (!item->IsDVD())
+ continue;
+ }
+ else
+ {
+ if (!URIUtils::CompareWithoutSlashAtEnd(testShare.strPath, item->GetPath()))
+ continue;
+ }
+ // paths match, what about share name - only match the leftmost
+ // characters as the label may contain other info (status for instance)
+ if (StringUtils::StartsWithNoCase(item->GetLabel(), testShare.strName))
+ {
+ return &testShare;
+ }
+ }
+ return nullptr;
+}
+
+void CGUIDialogContextMenu::OnWindowLoaded()
+{
+ m_coordX = m_posX;
+ m_coordY = m_posY;
+
+ const CGUIControlGroupList* pGroupList = dynamic_cast<const CGUIControlGroupList *>(GetControl(GROUP_LIST));
+ m_backgroundImage = GetControl(BACKGROUND_IMAGE);
+ if (m_backgroundImage && pGroupList)
+ {
+ if (pGroupList->GetOrientation() == VERTICAL)
+ m_backgroundImageSize = m_backgroundImage->GetHeight();
+ else
+ m_backgroundImageSize = m_backgroundImage->GetWidth();
+ }
+
+ CGUIDialog::OnWindowLoaded();
+}
+
+void CGUIDialogContextMenu::OnDeinitWindow(int nextWindowID)
+{
+ //we can't be sure that controls are removed on window unload
+ //we have to remove them to be sure that they won't stay for next use of context menu
+ for (unsigned int i = 0; i < m_buttons.size(); i++)
+ {
+ const CGUIControl *control = GetControl(BUTTON_START + i);
+ if (control)
+ {
+ RemoveControl(control);
+ delete control;
+ }
+ }
+
+ m_buttons.clear();
+ m_initiallyFocusedButtonIdx = 0;
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+}
+
+std::string CGUIDialogContextMenu::GetDefaultShareNameByType(const std::string &strType)
+{
+ VECSOURCES *pShares = CMediaSourceSettings::GetInstance().GetSources(strType);
+ std::string strDefault = CMediaSourceSettings::GetInstance().GetDefaultSource(strType);
+
+ if (!pShares) return "";
+
+ bool bIsSourceName(false);
+ int iIndex = CUtil::GetMatchingSource(strDefault, *pShares, bIsSourceName);
+ if (iIndex < 0 || iIndex >= (int)pShares->size())
+ return "";
+
+ return pShares->at(iIndex).strName;
+}
+
+void CGUIDialogContextMenu::SetDefault(const std::string &strType, const std::string &strDefault)
+{
+ CMediaSourceSettings::GetInstance().SetDefaultSource(strType, strDefault);
+ CMediaSourceSettings::GetInstance().Save();
+}
+
+void CGUIDialogContextMenu::ClearDefault(const std::string &strType)
+{
+ SetDefault(strType, "");
+}
+
+void CGUIDialogContextMenu::SwitchMedia(const std::string& strType, const std::string& strPath)
+{
+ // create menu
+ CContextButtons choices;
+ if (strType != "music")
+ choices.Add(WINDOW_MUSIC_NAV, 2);
+ if (strType != "video")
+ choices.Add(WINDOW_VIDEO_NAV, 3);
+ if (strType != "pictures")
+ choices.Add(WINDOW_PICTURES, 1);
+ if (strType != "files")
+ choices.Add(WINDOW_FILES, 7);
+
+ int window = ShowAndGetChoice(choices);
+ if (window >= 0)
+ {
+ CUtil::DeleteDirectoryCache();
+ CServiceBroker::GetGUI()->GetWindowManager().ChangeActiveWindow(window, strPath);
+ }
+}
+
+int CGUIDialogContextMenu::Show(const CContextButtons& choices, int focusedButtonIdx /* = 0 */)
+{
+ auto dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogContextMenu>(WINDOW_DIALOG_CONTEXT_MENU);
+ if (!dialog)
+ return -1;
+
+ dialog->m_buttons = choices;
+ dialog->Initialize();
+ dialog->SetInitialVisibility();
+ dialog->SetupButtons();
+ dialog->PositionAtCurrentFocus();
+ dialog->m_initiallyFocusedButtonIdx = focusedButtonIdx;
+ dialog->Open();
+ return dialog->m_clickedButton;
+}
+
+int CGUIDialogContextMenu::ShowAndGetChoice(const CContextButtons &choices)
+{
+ if (choices.empty())
+ return -1;
+
+ CGUIDialogContextMenu *pMenu = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogContextMenu>(WINDOW_DIALOG_CONTEXT_MENU);
+ if (pMenu)
+ {
+ pMenu->m_buttons = choices;
+ pMenu->Initialize();
+ pMenu->SetInitialVisibility();
+ pMenu->SetupButtons();
+ pMenu->PositionAtCurrentFocus();
+ pMenu->Open();
+
+ int idx = pMenu->m_clickedButton;
+ if (idx != -1)
+ return choices[idx].first;
+ }
+ return -1;
+}
+
+void CGUIDialogContextMenu::PositionAtCurrentFocus()
+{
+ CGUIWindow *window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog());
+ if (window)
+ {
+ const CGUIControl *focusedControl = window->GetFocusedControl();
+ if (focusedControl)
+ {
+ CPoint pos = focusedControl->GetRenderPosition() + CPoint(focusedControl->GetWidth() * 0.5f, focusedControl->GetHeight() * 0.5f);
+ SetPosition(m_coordX + pos.x - GetWidth() * 0.5f, m_coordY + pos.y - GetHeight() * 0.5f);
+ return;
+ }
+ }
+ // no control to center at, so just center the window
+ CenterWindow();
+}
diff --git a/xbmc/dialogs/GUIDialogContextMenu.h b/xbmc/dialogs/GUIDialogContextMenu.h
new file mode 100644
index 0000000..420e458
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogContextMenu.h
@@ -0,0 +1,152 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+
+class CMediaSource;
+
+enum CONTEXT_BUTTON { CONTEXT_BUTTON_CANCELLED = 0,
+ CONTEXT_BUTTON_RENAME,
+ CONTEXT_BUTTON_DELETE,
+ CONTEXT_BUTTON_MOVE,
+ CONTEXT_BUTTON_SETTINGS,
+ CONTEXT_BUTTON_RIP_CD,
+ CONTEXT_BUTTON_CANCEL_RIP_CD,
+ CONTEXT_BUTTON_RIP_TRACK,
+ CONTEXT_BUTTON_EJECT_DRIVE,
+ CONTEXT_BUTTON_EDIT_SOURCE,
+ CONTEXT_BUTTON_REMOVE_SOURCE,
+ CONTEXT_BUTTON_SET_DEFAULT,
+ CONTEXT_BUTTON_CLEAR_DEFAULT,
+ CONTEXT_BUTTON_SET_THUMB,
+ CONTEXT_BUTTON_ADD_LOCK,
+ CONTEXT_BUTTON_REMOVE_LOCK,
+ CONTEXT_BUTTON_CHANGE_LOCK,
+ CONTEXT_BUTTON_RESET_LOCK,
+ CONTEXT_BUTTON_REACTIVATE_LOCK,
+ CONTEXT_BUTTON_VIEW_SLIDESHOW,
+ CONTEXT_BUTTON_RECURSIVE_SLIDESHOW,
+ CONTEXT_BUTTON_REFRESH_THUMBS,
+ CONTEXT_BUTTON_SWITCH_MEDIA,
+ CONTEXT_BUTTON_MOVE_ITEM,
+ CONTEXT_BUTTON_MOVE_HERE,
+ CONTEXT_BUTTON_CANCEL_MOVE,
+ CONTEXT_BUTTON_MOVE_ITEM_UP,
+ CONTEXT_BUTTON_MOVE_ITEM_DOWN,
+ CONTEXT_BUTTON_CLEAR,
+ CONTEXT_BUTTON_QUEUE_ITEM,
+ CONTEXT_BUTTON_PLAY_ITEM,
+ CONTEXT_BUTTON_PLAY_WITH,
+ CONTEXT_BUTTON_PLAY_PARTYMODE,
+ CONTEXT_BUTTON_PLAY_PART,
+ CONTEXT_BUTTON_RESUME_ITEM,
+ CONTEXT_BUTTON_EDIT,
+ CONTEXT_BUTTON_EDIT_SMART_PLAYLIST,
+ CONTEXT_BUTTON_INFO,
+ CONTEXT_BUTTON_INFO_ALL,
+ CONTEXT_BUTTON_CDDB,
+ CONTEXT_BUTTON_SCAN,
+ CONTEXT_BUTTON_SCAN_TO_LIBRARY,
+ CONTEXT_BUTTON_SET_ARTIST_THUMB,
+ CONTEXT_BUTTON_SET_SEASON_ART,
+ CONTEXT_BUTTON_CANCEL_PARTYMODE,
+ CONTEXT_BUTTON_MARK_WATCHED,
+ CONTEXT_BUTTON_MARK_UNWATCHED,
+ CONTEXT_BUTTON_SET_CONTENT,
+ CONTEXT_BUTTON_EDIT_PARTYMODE,
+ CONTEXT_BUTTON_LINK_MOVIE,
+ CONTEXT_BUTTON_UNLINK_MOVIE,
+ CONTEXT_BUTTON_GO_TO_ARTIST,
+ CONTEXT_BUTTON_GO_TO_ALBUM,
+ CONTEXT_BUTTON_PLAY_OTHER,
+ CONTEXT_BUTTON_SET_ACTOR_THUMB,
+ CONTEXT_BUTTON_UNLINK_BOOKMARK,
+ CONTEXT_BUTTON_ACTIVATE,
+ CONTEXT_BUTTON_GROUP_MANAGER,
+ CONTEXT_BUTTON_CHANNEL_MANAGER,
+ CONTEXT_BUTTON_SET_MOVIESET_ART,
+ CONTEXT_BUTTON_PLAY_AND_QUEUE,
+ CONTEXT_BUTTON_PLAY_ONLY_THIS,
+ CONTEXT_BUTTON_UPDATE_EPG,
+ CONTEXT_BUTTON_TAGS_ADD_ITEMS,
+ CONTEXT_BUTTON_TAGS_REMOVE_ITEMS,
+ CONTEXT_BUTTON_SET_MOVIESET,
+ CONTEXT_BUTTON_MOVIESET_ADD_REMOVE_ITEMS,
+ CONTEXT_BUTTON_BROWSE_INTO,
+ CONTEXT_BUTTON_EDIT_SORTTITLE,
+ CONTEXT_BUTTON_DELETE_ALL,
+ CONTEXT_BUTTON_HELP,
+ CONTEXT_BUTTON_PLAY_NEXT,
+ CONTEXT_BUTTON_NAVIGATE,
+ };
+
+class CContextButtons : public std::vector< std::pair<size_t, std::string> >
+{
+public:
+ void Add(unsigned int, const std::string &label);
+ void Add(unsigned int, int label);
+};
+
+class CGUIDialogContextMenu :
+ public CGUIDialog
+{
+public:
+ CGUIDialogContextMenu(void);
+ ~CGUIDialogContextMenu(void) override;
+ bool OnMessage(CGUIMessage &message) override;
+ bool OnAction(const CAction& action) override;
+ void SetPosition(float posX, float posY) override;
+
+ static bool SourcesMenu(const std::string &strType, const CFileItemPtr& item, float posX, float posY);
+ static void SwitchMedia(const std::string& strType, const std::string& strPath);
+
+ static void GetContextButtons(const std::string &type, const CFileItemPtr& item, CContextButtons &buttons);
+ static bool OnContextButton(const std::string &type, const CFileItemPtr& item, CONTEXT_BUTTON button);
+
+ /*! Show the context menu with the given choices and return the index of the selected item,
+ or -1 if cancelled.
+ */
+ static int Show(const CContextButtons& choices, int focusedButton = 0);
+
+ /*! Legacy method that returns the context menu id, or -1 on cancel */
+ static int ShowAndGetChoice(const CContextButtons &choices);
+
+protected:
+ void SetupButtons();
+
+ /*! \brief Position the context menu in the middle of the focused control.
+ If no control is available it is positioned in the middle of the screen.
+ */
+ void PositionAtCurrentFocus();
+
+ float GetWidth() const override;
+ float GetHeight() const override;
+ void OnInitWindow() override;
+ void OnWindowLoaded() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ static std::string GetDefaultShareNameByType(const std::string &strType);
+ static void SetDefault(const std::string &strType, const std::string &strDefault);
+ static void ClearDefault(const std::string &strType);
+ static CMediaSource *GetShare(const std::string &type, const CFileItem *item);
+
+private:
+ float m_coordX, m_coordY;
+ /// \brief Stored size of background image (height or width depending on grouplist orientation)
+ float m_backgroundImageSize;
+ int m_initiallyFocusedButtonIdx = 0;
+ int m_clickedButton;
+ CContextButtons m_buttons;
+ const CGUIControl *m_backgroundImage = nullptr;
+};
diff --git a/xbmc/dialogs/GUIDialogExtendedProgressBar.cpp b/xbmc/dialogs/GUIDialogExtendedProgressBar.cpp
new file mode 100644
index 0000000..9fc1c02
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogExtendedProgressBar.cpp
@@ -0,0 +1,155 @@
+/*
+ * 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 "GUIDialogExtendedProgressBar.h"
+
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIProgressControl.h"
+#include "utils/TimeUtils.h"
+
+#include <cmath>
+#include <mutex>
+
+#define CONTROL_LABELHEADER 30
+#define CONTROL_LABELTITLE 31
+#define CONTROL_PROGRESS 32
+
+#define ITEM_SWITCH_TIME_MS 2000
+
+std::string CGUIDialogProgressBarHandle::Text(void) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string retVal(m_strText);
+ return retVal;
+}
+
+void CGUIDialogProgressBarHandle::SetText(const std::string &strText)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strText = strText;
+}
+
+void CGUIDialogProgressBarHandle::SetTitle(const std::string &strTitle)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strTitle = strTitle;
+}
+
+void CGUIDialogProgressBarHandle::SetProgress(int currentItem, int itemCount)
+{
+ float fPercentage = (currentItem*100.0f)/itemCount;
+ if (!std::isnan(fPercentage))
+ m_fPercentage = std::min(100.0f, fPercentage);
+}
+
+CGUIDialogExtendedProgressBar::CGUIDialogExtendedProgressBar(void)
+ : CGUIDialog(WINDOW_DIALOG_EXT_PROGRESS, "DialogExtendedProgressBar.xml", DialogModalityType::MODELESS)
+{
+ m_loadType = LOAD_ON_GUI_INIT;
+ m_iLastSwitchTime = 0;
+ m_iCurrentItem = 0;
+}
+
+CGUIDialogProgressBarHandle *CGUIDialogExtendedProgressBar::GetHandle(const std::string &strTitle)
+{
+ CGUIDialogProgressBarHandle *handle = new CGUIDialogProgressBarHandle(strTitle);
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_handles.push_back(handle);
+ }
+
+ Open();
+
+ return handle;
+}
+
+bool CGUIDialogExtendedProgressBar::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_iLastSwitchTime = CTimeUtils::GetFrameTime();
+ m_iCurrentItem = 0;
+ CGUIDialog::OnMessage(message);
+
+ UpdateState(0);
+ return true;
+ }
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogExtendedProgressBar::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_active)
+ UpdateState(currentTime);
+
+ CGUIDialog::Process(currentTime, dirtyregions);
+}
+
+void CGUIDialogExtendedProgressBar::UpdateState(unsigned int currentTime)
+{
+ std::string strHeader;
+ std::string strTitle;
+ float fProgress(-1.0f);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // delete finished items
+ for (int iPtr = m_handles.size() - 1; iPtr >= 0; iPtr--)
+ {
+ if (m_handles.at(iPtr)->IsFinished())
+ {
+ delete m_handles.at(iPtr);
+ m_handles.erase(m_handles.begin() + iPtr);
+ }
+ }
+
+ if (!m_handles.size())
+ {
+ Close(false, 0, true, false);
+ return;
+ }
+
+ // ensure the current item is in our range
+ if (m_iCurrentItem >= m_handles.size())
+ m_iCurrentItem = m_handles.size() - 1;
+
+ // update the current item ptr
+ if (currentTime > m_iLastSwitchTime &&
+ currentTime - m_iLastSwitchTime >= ITEM_SWITCH_TIME_MS)
+ {
+ m_iLastSwitchTime = currentTime;
+
+ // select next item
+ if (++m_iCurrentItem > m_handles.size() - 1)
+ m_iCurrentItem = 0;
+ }
+
+ CGUIDialogProgressBarHandle *handle = m_handles.at(m_iCurrentItem);
+ if (handle)
+ {
+ strTitle = handle->Text();
+ strHeader = handle->Title();
+ fProgress = handle->Percentage();
+ }
+ }
+
+ SET_CONTROL_LABEL(CONTROL_LABELHEADER, strHeader);
+ SET_CONTROL_LABEL(CONTROL_LABELTITLE, strTitle);
+
+ if (fProgress > -1.0f)
+ {
+ SET_CONTROL_VISIBLE(CONTROL_PROGRESS);
+ CONTROL_SELECT_ITEM(CONTROL_PROGRESS, (unsigned int)fProgress);
+ }
+}
diff --git a/xbmc/dialogs/GUIDialogExtendedProgressBar.h b/xbmc/dialogs/GUIDialogExtendedProgressBar.h
new file mode 100644
index 0000000..c6a0609
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogExtendedProgressBar.h
@@ -0,0 +1,63 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+#include <string>
+#include <vector>
+
+class CGUIDialogProgressBarHandle
+{
+public:
+ explicit CGUIDialogProgressBarHandle(const std::string &strTitle) :
+ m_fPercentage(0),
+ m_strTitle(strTitle),
+ m_bFinished(false) {}
+ virtual ~CGUIDialogProgressBarHandle(void) = default;
+
+ const std::string &Title(void) { return m_strTitle; }
+ void SetTitle(const std::string &strTitle);
+
+ std::string Text(void) const;
+ void SetText(const std::string &strText);
+
+ bool IsFinished(void) const { return m_bFinished; }
+ void MarkFinished(void) { m_bFinished = true; }
+
+ float Percentage(void) const { return m_fPercentage;}
+ void SetPercentage(float fPercentage) { m_fPercentage = fPercentage; }
+ void SetProgress(int currentItem, int itemCount);
+
+private:
+ mutable CCriticalSection m_critSection;
+ float m_fPercentage;
+ std::string m_strTitle;
+ std::string m_strText;
+ bool m_bFinished;
+};
+
+class CGUIDialogExtendedProgressBar : public CGUIDialog
+{
+public:
+ CGUIDialogExtendedProgressBar(void);
+ ~CGUIDialogExtendedProgressBar(void) override = default;
+ bool OnMessage(CGUIMessage& message) override;
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+
+ CGUIDialogProgressBarHandle *GetHandle(const std::string &strTitle);
+
+protected:
+ void UpdateState(unsigned int currentTime);
+
+ CCriticalSection m_critSection;
+ unsigned int m_iCurrentItem;
+ unsigned int m_iLastSwitchTime;
+ std::vector<CGUIDialogProgressBarHandle *> m_handles;
+};
diff --git a/xbmc/dialogs/GUIDialogFileBrowser.cpp b/xbmc/dialogs/GUIDialogFileBrowser.cpp
new file mode 100644
index 0000000..a9f81cb
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogFileBrowser.cpp
@@ -0,0 +1,1020 @@
+/*
+ * 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 "GUIDialogFileBrowser.h"
+
+#include "AutoSwitch.h"
+#include "FileItem.h"
+#include "GUIDialogContextMenu.h"
+#include "GUIDialogMediaSource.h"
+#include "GUIDialogYesNo.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "network/GUIDialogNetworkSetup.h"
+#include "network/Network.h"
+#include "profiles/ProfileManager.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace KODI::MESSAGING;
+using namespace XFILE;
+
+#define CONTROL_LIST 450
+#define CONTROL_THUMBS 451
+#define CONTROL_HEADING_LABEL 411
+#define CONTROL_LABEL_PATH 412
+#define CONTROL_OK 413
+#define CONTROL_CANCEL 414
+#define CONTROL_NEWFOLDER 415
+#define CONTROL_FLIP 416
+
+CGUIDialogFileBrowser::CGUIDialogFileBrowser()
+ : CGUIDialog(WINDOW_DIALOG_FILE_BROWSER, "FileBrowser.xml")
+{
+ m_Directory = new CFileItem;
+ m_vecItems = new CFileItemList;
+ m_bConfirmed = false;
+ m_Directory->m_bIsFolder = true;
+ m_browsingForFolders = 0;
+ m_browsingForImages = false;
+ m_useFileDirectories = false;
+ m_addNetworkShareEnabled = false;
+ m_singleList = false;
+ m_thumbLoader.SetObserver(this);
+ m_flipEnabled = false;
+ m_bFlip = false;
+ m_multipleSelection = false;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogFileBrowser::~CGUIDialogFileBrowser()
+{
+ delete m_Directory;
+ delete m_vecItems;
+}
+
+bool CGUIDialogFileBrowser::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_PARENT_DIR)
+ {
+ GoParentFolder();
+ return true;
+ }
+ if ((action.GetID() == ACTION_CONTEXT_MENU || action.GetID() == ACTION_MOUSE_RIGHT_CLICK) && m_Directory->GetPath().empty())
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ if ((!m_addSourceType.empty() && iItem != m_vecItems->Size()-1))
+ return OnPopupMenu(iItem);
+ if (m_addNetworkShareEnabled && CServiceBroker::GetMediaManager().HasLocation(m_selectedPath))
+ {
+ // need to make sure this source is not an auto added location
+ // as users locations might have the same paths
+ CFileItemPtr pItem = (*m_vecItems)[iItem];
+ for (unsigned int i=0;i<m_shares.size();++i)
+ {
+ if (StringUtils::EqualsNoCase(m_shares[i].strName, pItem->GetLabel()) && m_shares[i].m_ignore)
+ return false;
+ }
+
+ return OnPopupMenu(iItem);
+ }
+
+ return false;
+ }
+
+ return CGUIDialog::OnAction(action);
+}
+
+bool CGUIDialogFileBrowser::OnBack(int actionID)
+{
+ if (actionID == ACTION_NAV_BACK && !m_vecItems->IsVirtualDirectoryRoot())
+ {
+ GoParentFolder();
+ return true;
+ }
+ return CGUIDialog::OnBack(actionID);
+}
+
+bool CGUIDialogFileBrowser::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+ CGUIDialog::OnMessage(message);
+ ClearFileItems();
+ m_addNetworkShareEnabled = false;
+ return true;
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_bConfirmed = false;
+ m_bFlip = false;
+ bool bIsDir = false;
+ // this code allows two different selection modes for directories
+ // end the path with a slash to start inside the directory
+ if (URIUtils::HasSlashAtEnd(m_selectedPath))
+ {
+ bIsDir = true;
+ bool bFool;
+ int iSource = CUtil::GetMatchingSource(m_selectedPath,m_shares,bFool);
+ bFool = true;
+ if (iSource > -1 && iSource < (int)m_shares.size())
+ {
+ if (URIUtils::PathEquals(m_shares[iSource].strPath, m_selectedPath))
+ bFool = false;
+ }
+
+ if (bFool && !CDirectory::Exists(m_selectedPath))
+ m_selectedPath.clear();
+ }
+ else
+ {
+ if (!CFile::Exists(m_selectedPath) && !CDirectory::Exists(m_selectedPath))
+ m_selectedPath.clear();
+ }
+
+ // find the parent folder if we are a file browser (don't do this for folders)
+ m_Directory->SetPath(m_selectedPath);
+ if (!m_browsingForFolders && !bIsDir)
+ m_Directory->SetPath(URIUtils::GetParentPath(m_selectedPath));
+ Update(m_Directory->GetPath());
+ m_viewControl.SetSelectedItem(m_selectedPath);
+ return CGUIDialog::OnMessage(message);
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ if (m_viewControl.HasControl(message.GetSenderId())) // list control
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ int iAction = message.GetParam1();
+ if (iItem < 0) break;
+ CFileItemPtr pItem = (*m_vecItems)[iItem];
+ if ((iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK) &&
+ (!m_multipleSelection || pItem->m_bIsShareOrDrive || pItem->m_bIsFolder))
+ {
+ OnClick(iItem);
+ return true;
+ }
+ else if ((iAction == ACTION_HIGHLIGHT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK || iAction == ACTION_SELECT_ITEM) &&
+ (m_multipleSelection && !pItem->m_bIsShareOrDrive && !pItem->m_bIsFolder))
+ {
+ pItem->Select(!pItem->IsSelected());
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), message.GetSenderId(), iItem + 1);
+ OnMessage(msg);
+ }
+ }
+ else if (message.GetSenderId() == CONTROL_OK)
+ {
+ if (m_browsingForFolders == 2)
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+
+ std::string strPath;
+ if (iItem == 0)
+ strPath = m_selectedPath;
+ else
+ strPath = (*m_vecItems)[iItem]->GetPath();
+
+ std::string strTest = URIUtils::AddFileToFolder(strPath, "1");
+ CFile file;
+ if (file.OpenForWrite(strTest,true))
+ {
+ file.Close();
+ CFile::Delete(strTest);
+ m_bConfirmed = true;
+ Close();
+ }
+ else
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{20072});
+ }
+ else
+ {
+ if (m_multipleSelection)
+ {
+ for (int iItem = 0; iItem < m_vecItems->Size(); ++iItem)
+ {
+ CFileItemPtr pItem = (*m_vecItems)[iItem];
+ if (pItem->IsSelected())
+ m_markedPath.push_back(pItem->GetPath());
+ }
+ }
+ m_bConfirmed = true;
+ Close();
+ }
+ return true;
+ }
+ else if (message.GetSenderId() == CONTROL_CANCEL)
+ {
+ Close();
+ return true;
+ }
+ else if (message.GetSenderId() == CONTROL_NEWFOLDER)
+ {
+ std::string strInput;
+ if (CGUIKeyboardFactory::ShowAndGetInput(strInput, CVariant{g_localizeStrings.Get(119)}, false))
+ {
+ std::string strPath = URIUtils::AddFileToFolder(m_vecItems->GetPath(), strInput);
+ if (CDirectory::Create(strPath))
+ Update(m_vecItems->GetPath());
+ else
+ HELPERS::ShowOKDialogText(CVariant{20069}, CVariant{20072});
+ }
+ }
+ else if (message.GetSenderId() == CONTROL_FLIP)
+ m_bFlip = !m_bFlip;
+ }
+ break;
+ case GUI_MSG_SETFOCUS:
+ {
+ if (m_viewControl.HasControl(message.GetControlId()) && m_viewControl.GetCurrentControl() != message.GetControlId())
+ {
+ m_viewControl.SetFocused();
+ return true;
+ }
+ }
+ break;
+ case GUI_MSG_NOTIFY_ALL:
+ { // Message is received only if this window is active
+ if (message.GetParam1() == GUI_MSG_REMOVED_MEDIA)
+ {
+ if (m_Directory->IsVirtualDirectoryRoot() && IsActive())
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ Update(m_Directory->GetPath());
+ m_viewControl.SetSelectedItem(iItem);
+ }
+ else if (m_Directory->IsRemovable())
+ { // check that we have this removable share still
+ if (!m_rootDir.IsInSource(m_Directory->GetPath()))
+ { // don't have this share any more
+ if (IsActive()) Update("");
+ else
+ {
+ m_history.ClearPathHistory();
+ m_Directory->SetPath("");
+ }
+ }
+ }
+ return true;
+ }
+ else if (message.GetParam1()==GUI_MSG_UPDATE_SOURCES)
+ { // State of the sources changed, so update our view
+ if (m_Directory->IsVirtualDirectoryRoot() && IsActive())
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ Update(m_Directory->GetPath());
+ m_viewControl.SetSelectedItem(iItem);
+ }
+ return true;
+ }
+ else if (message.GetParam1()==GUI_MSG_UPDATE_PATH)
+ {
+ if (IsActive())
+ {
+ if((message.GetStringParam() == m_Directory->GetPath()) ||
+ (m_Directory->IsMultiPath() && XFILE::CMultiPathDirectory::HasPath(m_Directory->GetPath(), message.GetStringParam())))
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ Update(m_Directory->GetPath());
+ m_viewControl.SetSelectedItem(iItem);
+ }
+ }
+ }
+ }
+ break;
+
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogFileBrowser::ClearFileItems()
+{
+ m_viewControl.Clear();
+ m_vecItems->Clear(); // will clean up everything
+}
+
+void CGUIDialogFileBrowser::OnSort()
+{
+ if (!m_singleList)
+ m_vecItems->Sort(SortByLabel, SortOrderAscending);
+}
+
+void CGUIDialogFileBrowser::Update(const std::string &strDirectory)
+{
+ const CURL pathToUrl(strDirectory);
+
+ if (m_browsingForImages && m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+ // get selected item
+ int iItem = m_viewControl.GetSelectedItem();
+ std::string strSelectedItem;
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ CFileItemPtr pItem = (*m_vecItems)[iItem];
+ if (!pItem->IsParentFolder())
+ {
+ strSelectedItem = pItem->GetPath();
+ URIUtils::RemoveSlashAtEnd(strSelectedItem);
+ m_history.SetSelectedItem(strSelectedItem, m_Directory->GetPath().empty()?"empty":m_Directory->GetPath());
+ }
+ }
+
+ if (!m_singleList)
+ {
+ CFileItemList items;
+ std::string strParentPath;
+
+ if (!m_rootDir.GetDirectory(pathToUrl, items, m_useFileDirectories, false))
+ {
+ CLog::Log(LOGERROR, "CGUIDialogFileBrowser::GetDirectory({}) failed",
+ pathToUrl.GetRedacted());
+
+ // We assume, we can get the parent
+ // directory again
+ std::string strParentPath = m_history.GetParentPath();
+ m_history.RemoveParentPath();
+ Update(strParentPath);
+ return;
+ }
+
+ // check if current directory is a root share
+ if (!m_rootDir.IsSource(strDirectory))
+ {
+ if (URIUtils::GetParentPath(strDirectory, strParentPath))
+ {
+ CFileItemPtr pItem(new CFileItem(".."));
+ pItem->SetPath(strParentPath);
+ pItem->m_bIsFolder = true;
+ pItem->m_bIsShareOrDrive = false;
+ items.AddFront(pItem, 0);
+ }
+ }
+ else
+ {
+ // yes, this is the root of a share
+ // add parent path to the virtual directory
+ CFileItemPtr pItem(new CFileItem(".."));
+ pItem->SetPath("");
+ pItem->m_bIsShareOrDrive = false;
+ pItem->m_bIsFolder = true;
+ items.AddFront(pItem, 0);
+ strParentPath = "";
+ }
+
+ ClearFileItems();
+ m_vecItems->Copy(items);
+ m_Directory->SetPath(strDirectory);
+ m_strParentPath = strParentPath;
+ }
+
+ // if we're getting the root source listing
+ // make sure the path history is clean
+ if (strDirectory.empty())
+ m_history.ClearPathHistory();
+
+ // some evil stuff don't work with the '/' mask, e.g. shoutcast directory - make sure no files are in there
+ if (m_browsingForFolders)
+ {
+ for (int i=0;i<m_vecItems->Size();++i)
+ if (!(*m_vecItems)[i]->m_bIsFolder)
+ {
+ m_vecItems->Remove(i);
+ i--;
+ }
+ }
+
+ // No need to set thumbs
+
+ m_vecItems->FillInDefaultIcons();
+
+ OnSort();
+
+ if (m_Directory->GetPath().empty() && m_addNetworkShareEnabled &&
+ (CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE ||
+ CServiceBroker::GetSettingsComponent()->GetProfileManager()->IsMasterProfile() || g_passwordManager.bMasterUser))
+ { // we are in the virtual directory - add the "Add Network Location" item
+ CFileItemPtr pItem(new CFileItem(g_localizeStrings.Get(1032)));
+ pItem->SetPath("net://");
+ pItem->m_bIsFolder = true;
+ m_vecItems->Add(pItem);
+ }
+ if (m_Directory->GetPath().empty() && !m_addSourceType.empty())
+ {
+ CFileItemPtr pItem(new CFileItem(g_localizeStrings.Get(21359)));
+ pItem->SetPath("source://");
+ pItem->m_bIsFolder = true;
+ m_vecItems->Add(pItem);
+ }
+
+ m_viewControl.SetItems(*m_vecItems);
+ m_viewControl.SetCurrentView((m_browsingForImages && CAutoSwitch::ByFileCount(*m_vecItems)) ? CONTROL_THUMBS : CONTROL_LIST);
+
+ std::string strPath2 = m_Directory->GetPath();
+ URIUtils::RemoveSlashAtEnd(strPath2);
+ strSelectedItem = m_history.GetSelectedItem(strPath2==""?"empty":strPath2);
+
+ bool bSelectedFound = false;
+ for (int i = 0; i < m_vecItems->Size(); ++i)
+ {
+ CFileItemPtr pItem = (*m_vecItems)[i];
+ strPath2 = pItem->GetPath();
+ URIUtils::RemoveSlashAtEnd(strPath2);
+ if (strPath2 == strSelectedItem)
+ {
+ m_viewControl.SetSelectedItem(i);
+ bSelectedFound = true;
+ break;
+ }
+ }
+
+ // if we haven't found the selected item, select the first item
+ if (!bSelectedFound)
+ m_viewControl.SetSelectedItem(0);
+
+ m_history.AddPath(m_Directory->GetPath());
+
+ if (m_browsingForImages)
+ m_thumbLoader.Load(*m_vecItems);
+}
+
+void CGUIDialogFileBrowser::FrameMove()
+{
+ int item = m_viewControl.GetSelectedItem();
+ if (item >= 0)
+ {
+ // if we are browsing for folders, and not in the root directory, then we use the parent path,
+ // else we use the current file's path
+ if (m_browsingForFolders && !m_Directory->IsVirtualDirectoryRoot())
+ m_selectedPath = m_Directory->GetPath();
+ else
+ m_selectedPath = (*m_vecItems)[item]->GetPath();
+ if (m_selectedPath == "net://")
+ {
+ SET_CONTROL_LABEL(CONTROL_LABEL_PATH, g_localizeStrings.Get(1032)); // "Add Network Location..."
+ }
+ else
+ {
+ // Update the current path label
+ CURL url(m_selectedPath);
+ std::string safePath = url.GetWithoutUserDetails();
+ SET_CONTROL_LABEL(CONTROL_LABEL_PATH, safePath);
+ }
+ if ((!m_browsingForFolders && (*m_vecItems)[item]->m_bIsFolder) || ((*m_vecItems)[item]->GetPath() == "image://Browse"))
+ {
+ CONTROL_DISABLE(CONTROL_OK);
+ }
+ else
+ {
+ CONTROL_ENABLE(CONTROL_OK);
+ }
+ if (m_browsingForFolders == 2)
+ {
+ CONTROL_ENABLE(CONTROL_NEWFOLDER);
+ }
+ else
+ {
+ CONTROL_DISABLE(CONTROL_NEWFOLDER);
+ }
+ if (m_flipEnabled)
+ {
+ CONTROL_ENABLE(CONTROL_FLIP);
+ }
+ else
+ {
+ CONTROL_DISABLE(CONTROL_FLIP);
+ }
+ }
+ CGUIDialog::FrameMove();
+}
+
+void CGUIDialogFileBrowser::OnClick(int iItem)
+{
+ if ( iItem < 0 || iItem >= m_vecItems->Size() ) return ;
+ CFileItemPtr pItem = (*m_vecItems)[iItem];
+ std::string strPath = pItem->GetPath();
+
+ if (pItem->m_bIsFolder)
+ {
+ if (pItem->GetPath() == "net://")
+ { // special "Add Network Location" item
+ OnAddNetworkLocation();
+ return;
+ }
+ if (pItem->GetPath() == "source://")
+ { // special "Add Source" item
+ OnAddMediaSource();
+ return;
+ }
+ if (!m_addSourceType.empty())
+ {
+ OnEditMediaSource(pItem.get());
+ return;
+ }
+ if ( pItem->m_bIsShareOrDrive )
+ {
+ if ( !HaveDiscOrConnection( pItem->m_iDriveType ) )
+ return ;
+ }
+ Update(strPath);
+ }
+ else if (!m_browsingForFolders)
+ {
+ m_selectedPath = pItem->GetPath();
+ m_bConfirmed = true;
+ Close();
+ }
+}
+
+bool CGUIDialogFileBrowser::HaveDiscOrConnection( int iDriveType )
+{
+ if ( iDriveType == CMediaSource::SOURCE_TYPE_DVD )
+ {
+ if (!CServiceBroker::GetMediaManager().IsDiscInDrive())
+ {
+ HELPERS::ShowOKDialogText(CVariant{218}, CVariant{219});
+ return false;
+ }
+ }
+ else if ( iDriveType == CMediaSource::SOURCE_TYPE_REMOTE )
+ {
+ //! @todo Handle not connected to a remote share
+ if ( !CServiceBroker::GetNetwork().IsConnected() )
+ {
+ HELPERS::ShowOKDialogText(CVariant{220}, CVariant{221});
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void CGUIDialogFileBrowser::GoParentFolder()
+{
+ std::string strPath(m_strParentPath);
+ if (strPath.size() == 2)
+ if (strPath[1] == ':')
+ URIUtils::AddSlashAtEnd(strPath);
+ Update(strPath);
+}
+
+void CGUIDialogFileBrowser::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+ m_viewControl.Reset();
+ m_viewControl.SetParentWindow(GetID());
+ m_viewControl.AddView(GetControl(CONTROL_LIST));
+ m_viewControl.AddView(GetControl(CONTROL_THUMBS));
+}
+
+void CGUIDialogFileBrowser::OnWindowUnload()
+{
+ CGUIDialog::OnWindowUnload();
+ m_viewControl.Reset();
+}
+
+bool CGUIDialogFileBrowser::ShowAndGetImage(const CFileItemList &items, const VECSOURCES &shares, const std::string &heading, std::string &result, bool* flip, int label)
+{
+ CGUIDialogFileBrowser *browser = new CGUIDialogFileBrowser();
+ if (!browser)
+ return false;
+ CServiceBroker::GetGUI()->GetWindowManager().AddUniqueInstance(browser);
+
+ browser->m_browsingForImages = true;
+ browser->m_singleList = true;
+ browser->m_vecItems->Clear();
+ browser->m_vecItems->Append(items);
+ if (true)
+ {
+ CFileItemPtr item(new CFileItem("image://Browse", false));
+ item->SetLabel(g_localizeStrings.Get(20153));
+ item->SetArt("icon", "DefaultFolder.png");
+ browser->m_vecItems->Add(item);
+ }
+ browser->SetHeading(heading);
+ browser->m_flipEnabled = flip?true:false;
+ browser->Open();
+ bool confirmed(browser->IsConfirmed());
+ if (confirmed)
+ {
+ result = browser->m_selectedPath;
+ if (result == "image://Browse")
+ { // "Browse for thumb"
+ CServiceBroker::GetGUI()->GetWindowManager().Remove(browser->GetID());
+ delete browser;
+ return ShowAndGetImage(shares, g_localizeStrings.Get(label), result);
+ }
+ }
+
+ if (flip)
+ *flip = browser->m_bFlip != 0;
+
+ CServiceBroker::GetGUI()->GetWindowManager().Remove(browser->GetID());
+ delete browser;
+
+ return confirmed;
+}
+
+bool CGUIDialogFileBrowser::ShowAndGetImage(const VECSOURCES &shares, const std::string &heading, std::string &path)
+{
+ return ShowAndGetFile(shares, CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), heading, path, true); // true for use thumbs
+}
+
+bool CGUIDialogFileBrowser::ShowAndGetImageList(const VECSOURCES &shares, const std::string &heading, std::vector<std::string> &path)
+{
+ return ShowAndGetFileList(shares, CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), heading, path, true); // true for use thumbs
+}
+
+bool CGUIDialogFileBrowser::ShowAndGetDirectory(const VECSOURCES &shares, const std::string &heading, std::string &path, bool bWriteOnly)
+{
+ // an extension mask of "/" ensures that no files are shown
+ if (bWriteOnly)
+ {
+ VECSOURCES shareWritable;
+ for (unsigned int i=0;i<shares.size();++i)
+ {
+ if (shares[i].IsWritable())
+ shareWritable.push_back(shares[i]);
+ }
+
+ return ShowAndGetFile(shareWritable, "/w", heading, path);
+ }
+
+ return ShowAndGetFile(shares, "/", heading, path);
+}
+
+bool CGUIDialogFileBrowser::ShowAndGetFile(const VECSOURCES &shares, const std::string &mask, const std::string &heading, std::string &path, bool useThumbs /* = false */, bool useFileDirectories /* = false */)
+{
+ CGUIDialogFileBrowser *browser = new CGUIDialogFileBrowser();
+ if (!browser)
+ return false;
+ CServiceBroker::GetGUI()->GetWindowManager().AddUniqueInstance(browser);
+
+ browser->m_useFileDirectories = useFileDirectories;
+
+ browser->m_browsingForImages = useThumbs;
+ browser->SetHeading(heading);
+ browser->SetSources(shares);
+ std::string strMask = mask;
+ if (mask == "/")
+ browser->m_browsingForFolders=1;
+ else
+ if (mask == "/w")
+ {
+ browser->m_browsingForFolders=2;
+ strMask = "/";
+ }
+ else
+ browser->m_browsingForFolders = 0;
+
+ browser->m_rootDir.SetMask(strMask);
+ browser->m_selectedPath = path;
+ browser->m_addNetworkShareEnabled = false;
+ browser->Open();
+ bool confirmed(browser->IsConfirmed());
+ if (confirmed)
+ path = browser->m_selectedPath;
+ CServiceBroker::GetGUI()->GetWindowManager().Remove(browser->GetID());
+ delete browser;
+ return confirmed;
+}
+
+// same as above, starting in a single directory
+bool CGUIDialogFileBrowser::ShowAndGetFile(const std::string &directory, const std::string &mask, const std::string &heading, std::string &path, bool useThumbs /* = false */, bool useFileDirectories /* = false */, bool singleList /* = false */)
+{
+ CGUIDialogFileBrowser *browser = new CGUIDialogFileBrowser();
+ if (!browser)
+ return false;
+ CServiceBroker::GetGUI()->GetWindowManager().AddUniqueInstance(browser);
+
+ browser->m_useFileDirectories = useFileDirectories;
+ browser->m_browsingForImages = useThumbs;
+ browser->SetHeading(heading);
+
+ // add a single share for this directory
+ if (!singleList)
+ {
+ VECSOURCES shares;
+ CMediaSource share;
+ share.strPath = directory;
+ URIUtils::RemoveSlashAtEnd(share.strPath); // this is needed for the dodgy code in WINDOW_INIT
+ shares.push_back(share);
+ browser->SetSources(shares);
+ }
+ else
+ {
+ browser->m_vecItems->Clear();
+ CDirectory::GetDirectory(directory,*browser->m_vecItems, "", DIR_FLAG_DEFAULTS);
+ CFileItemPtr item(new CFileItem("file://Browse", false));
+ item->SetLabel(g_localizeStrings.Get(20153));
+ item->SetArt("icon", "DefaultFolder.png");
+ browser->m_vecItems->Add(item);
+ browser->m_singleList = true;
+ }
+ std::string strMask = mask;
+ if (mask == "/")
+ browser->m_browsingForFolders=1;
+ else
+ if (mask == "/w")
+ {
+ browser->m_browsingForFolders=2;
+ strMask = "/";
+ }
+ else
+ browser->m_browsingForFolders = 0;
+
+ browser->m_rootDir.SetMask(strMask);
+ browser->m_selectedPath = directory;
+ browser->m_addNetworkShareEnabled = false;
+ browser->Open();
+ bool confirmed(browser->IsConfirmed());
+ if (confirmed)
+ path = browser->m_selectedPath;
+ if (path == "file://Browse")
+ { // "Browse for thumb"
+ CServiceBroker::GetGUI()->GetWindowManager().Remove(browser->GetID());
+ delete browser;
+ VECSOURCES shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+
+ return ShowAndGetFile(shares, mask, heading, path, useThumbs,useFileDirectories);
+ }
+ CServiceBroker::GetGUI()->GetWindowManager().Remove(browser->GetID());
+ delete browser;
+ return confirmed;
+}
+
+bool CGUIDialogFileBrowser::ShowAndGetFileList(const VECSOURCES &shares, const std::string &mask, const std::string &heading, std::vector<std::string> &path, bool useThumbs /* = false */, bool useFileDirectories /* = false */)
+{
+ CGUIDialogFileBrowser *browser = new CGUIDialogFileBrowser();
+ if (!browser)
+ return false;
+ CServiceBroker::GetGUI()->GetWindowManager().AddUniqueInstance(browser);
+
+ browser->m_useFileDirectories = useFileDirectories;
+ browser->m_multipleSelection = true;
+ browser->m_browsingForImages = useThumbs;
+ browser->SetHeading(heading);
+ browser->SetSources(shares);
+ browser->m_browsingForFolders = 0;
+ browser->m_rootDir.SetMask(mask);
+ browser->m_addNetworkShareEnabled = false;
+ browser->Open();
+ bool confirmed(browser->IsConfirmed());
+ if (confirmed)
+ {
+ if (browser->m_markedPath.size())
+ path = browser->m_markedPath;
+ else
+ path.push_back(browser->m_selectedPath);
+ }
+ CServiceBroker::GetGUI()->GetWindowManager().Remove(browser->GetID());
+ delete browser;
+ return confirmed;
+}
+
+void CGUIDialogFileBrowser::SetHeading(const std::string &heading)
+{
+ Initialize();
+ SET_CONTROL_LABEL(CONTROL_HEADING_LABEL, heading);
+}
+
+bool CGUIDialogFileBrowser::ShowAndGetSource(std::string &path, bool allowNetworkShares, VECSOURCES* additionalShare /* = NULL */, const std::string& strType /* = "" */)
+{
+ // Technique is
+ // 1. Show Filebrowser with currently defined local, and optionally the network locations.
+ // 2. Have the "Add Network Location" option in addition.
+ // 3a. If the "Add Network Location" is pressed, then:
+ // a) Fire up the network location dialog to grab the new location
+ // b) Check the location by doing a GetDirectory() - if it fails, prompt the user
+ // to allow them to add currently disconnected network shares.
+ // c) Save this location to our xml file (network.xml)
+ // d) Return to 1.
+ // 3b. If the "Add Source" is pressed, then:
+ // a) Fire up the media source dialog to add the new location
+ // 4. Optionally allow user to browse the local and network locations for their share.
+ // 5. On OK, return.
+
+ // Create a new filebrowser window
+ CGUIDialogFileBrowser *browser = new CGUIDialogFileBrowser();
+ if (!browser) return false;
+
+ // Add it to our window manager
+ CServiceBroker::GetGUI()->GetWindowManager().AddUniqueInstance(browser);
+
+ VECSOURCES shares;
+ if (!strType.empty())
+ {
+ if (additionalShare)
+ shares = *additionalShare;
+ browser->m_addSourceType = strType;
+ }
+ else
+ {
+ browser->SetHeading(g_localizeStrings.Get(1023));
+
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+
+ // Now the additional share if appropriate
+ if (additionalShare)
+ {
+ shares.insert(shares.end(),additionalShare->begin(),additionalShare->end());
+ }
+
+ // Now add the network shares...
+ if (allowNetworkShares)
+ {
+ CServiceBroker::GetMediaManager().GetNetworkLocations(shares);
+ }
+ }
+
+ browser->SetSources(shares);
+ browser->m_rootDir.SetMask("/");
+ browser->m_rootDir.AllowNonLocalSources(false); // don't allow plug n play shares
+ browser->m_browsingForFolders = 1;
+ browser->m_addNetworkShareEnabled = allowNetworkShares;
+ browser->m_selectedPath = "";
+ browser->Open();
+ bool confirmed = browser->IsConfirmed();
+ if (confirmed)
+ path = browser->m_selectedPath;
+
+ CServiceBroker::GetGUI()->GetWindowManager().Remove(browser->GetID());
+ delete browser;
+ return confirmed;
+}
+
+void CGUIDialogFileBrowser::SetSources(const VECSOURCES &shares)
+{
+ m_shares = shares;
+ if (!m_shares.size() && m_addSourceType.empty())
+ CServiceBroker::GetMediaManager().GetLocalDrives(m_shares);
+ m_rootDir.SetSources(m_shares);
+}
+
+void CGUIDialogFileBrowser::OnAddNetworkLocation()
+{
+ // ok, fire up the network location dialog
+ std::string path;
+ if (CGUIDialogNetworkSetup::ShowAndGetNetworkAddress(path))
+ {
+ // verify the path by doing a GetDirectory.
+ CFileItemList items;
+ if (CDirectory::GetDirectory(path, items, "", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_ALLOW_PROMPT) || CGUIDialogYesNo::ShowAndGetInput(CVariant{1001}, CVariant{1002}))
+ { // add the network location to the shares list
+ CMediaSource share;
+ share.strPath = path; //setPath(path);
+ CURL url(path);
+ share.strName = url.GetWithoutUserDetails();
+ URIUtils::RemoveSlashAtEnd(share.strName);
+ m_shares.push_back(share);
+ // add to our location manager...
+ CServiceBroker::GetMediaManager().AddNetworkLocation(path);
+ }
+ }
+ m_rootDir.SetSources(m_shares);
+ Update(m_vecItems->GetPath());
+}
+
+void CGUIDialogFileBrowser::OnAddMediaSource()
+{
+ if (CGUIDialogMediaSource::ShowAndAddMediaSource(m_addSourceType))
+ {
+ SetSources(*CMediaSourceSettings::GetInstance().GetSources(m_addSourceType));
+ Update("");
+ }
+}
+
+void CGUIDialogFileBrowser::OnEditMediaSource(CFileItem* pItem)
+{
+ if (CGUIDialogMediaSource::ShowAndEditMediaSource(m_addSourceType,pItem->GetLabel()))
+ {
+ SetSources(*CMediaSourceSettings::GetInstance().GetSources(m_addSourceType));
+ Update("");
+ }
+}
+
+bool CGUIDialogFileBrowser::OnPopupMenu(int iItem)
+{
+ CContextButtons choices;
+ choices.Add(1, m_addSourceType.empty() ? 20133 : 21364);
+ choices.Add(2, m_addSourceType.empty() ? 20134 : 21365);
+
+ int btnid = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (btnid == 1)
+ {
+ if (m_addNetworkShareEnabled)
+ {
+ std::string strOldPath=m_selectedPath,newPath=m_selectedPath;
+ VECSOURCES shares=m_shares;
+ if (CGUIDialogNetworkSetup::ShowAndGetNetworkAddress(newPath))
+ {
+ CServiceBroker::GetMediaManager().SetLocationPath(strOldPath, newPath);
+ CURL url(newPath);
+ for (unsigned int i=0;i<shares.size();++i)
+ {
+ if (URIUtils::CompareWithoutSlashAtEnd(shares[i].strPath, strOldPath))//getPath().Equals(strOldPath))
+ {
+ shares[i].strName = url.GetWithoutUserDetails();
+ shares[i].strPath = newPath;
+ URIUtils::RemoveSlashAtEnd(shares[i].strName);
+ break;
+ }
+ }
+ // refresh dialog content
+ SetSources(shares);
+ m_rootDir.SetMask("/");
+ m_browsingForFolders = 1;
+ m_addNetworkShareEnabled = true;
+ m_selectedPath = url.GetWithoutUserDetails();
+ Update(m_Directory->GetPath());
+ m_viewControl.SetSelectedItem(iItem);
+ }
+ }
+ else
+ {
+ CFileItemPtr item = m_vecItems->Get(iItem);
+ OnEditMediaSource(item.get());
+ }
+ }
+ if (btnid == 2)
+ {
+ if (m_addNetworkShareEnabled)
+ {
+ CServiceBroker::GetMediaManager().RemoveLocation(m_selectedPath);
+
+ for (unsigned int i=0;i<m_shares.size();++i)
+ {
+ if (URIUtils::CompareWithoutSlashAtEnd(m_shares[i].strPath, m_selectedPath) && !m_shares[i].m_ignore) // getPath().Equals(m_selectedPath))
+ {
+ m_shares.erase(m_shares.begin()+i);
+ break;
+ }
+ }
+ m_rootDir.SetSources(m_shares);
+ m_rootDir.SetMask("/");
+
+ m_browsingForFolders = 1;
+ m_addNetworkShareEnabled = true;
+ m_selectedPath = "";
+
+ Update(m_Directory->GetPath());
+ }
+ else
+ {
+ CMediaSourceSettings::GetInstance().DeleteSource(m_addSourceType,(*m_vecItems)[iItem]->GetLabel(),(*m_vecItems)[iItem]->GetPath());
+ SetSources(*CMediaSourceSettings::GetInstance().GetSources(m_addSourceType));
+ Update("");
+ }
+ }
+
+ return true;
+}
+
+CFileItemPtr CGUIDialogFileBrowser::GetCurrentListItem(int offset)
+{
+ int item = m_viewControl.GetSelectedItem();
+ if (item < 0 || !m_vecItems->Size()) return CFileItemPtr();
+
+ item = (item + offset) % m_vecItems->Size();
+ if (item < 0) item += m_vecItems->Size();
+ return (*m_vecItems)[item];
+}
+
+CGUIControl *CGUIDialogFileBrowser::GetFirstFocusableControl(int id)
+{
+ if (m_viewControl.HasControl(id))
+ id = m_viewControl.GetCurrentControl();
+ return CGUIWindow::GetFirstFocusableControl(id);
+}
+
+
diff --git a/xbmc/dialogs/GUIDialogFileBrowser.h b/xbmc/dialogs/GUIDialogFileBrowser.h
new file mode 100644
index 0000000..8cafaba
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogFileBrowser.h
@@ -0,0 +1,88 @@
+/*
+ * 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 "filesystem/DirectoryHistory.h"
+#include "filesystem/VirtualDirectory.h"
+#include "guilib/GUIDialog.h"
+#include "pictures/PictureThumbLoader.h"
+#include "view/GUIViewControl.h"
+
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CFileItemList;
+
+class CGUIDialogFileBrowser : public CGUIDialog, public IBackgroundLoaderObserver
+{
+public:
+ CGUIDialogFileBrowser(void);
+ ~CGUIDialogFileBrowser(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnBack(int actionID) override;
+ void FrameMove() override;
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ bool IsConfirmed() { return m_bConfirmed; }
+ void SetHeading(const std::string &heading);
+
+ static bool ShowAndGetDirectory(const VECSOURCES &shares, const std::string &heading, std::string &path, bool bWriteOnly=false);
+ static bool ShowAndGetFile(const VECSOURCES &shares, const std::string &mask, const std::string &heading, std::string &path, bool useThumbs = false, bool useFileDirectories = false);
+ static bool ShowAndGetFile(const std::string &directory, const std::string &mask, const std::string &heading, std::string &path, bool useThumbs = false, bool useFileDirectories = false, bool singleList = false);
+ static bool ShowAndGetSource(std::string &path, bool allowNetworkShares, VECSOURCES* additionalShare = NULL, const std::string& strType="");
+ static bool ShowAndGetFileList(const VECSOURCES &shares, const std::string &mask, const std::string &heading, std::vector<std::string> &path, bool useThumbs = false, bool useFileDirectories = false);
+ static bool ShowAndGetImage(const VECSOURCES &shares, const std::string &heading, std::string &path);
+ static bool ShowAndGetImage(const CFileItemList &items, const VECSOURCES &shares, const std::string &heading, std::string &path, bool* flip=NULL, int label=21371);
+ static bool ShowAndGetImageList(const VECSOURCES &shares, const std::string &heading, std::vector<std::string> &path);
+
+ void SetSources(const VECSOURCES &shares);
+
+ void OnItemLoaded(CFileItem *item) override {};
+
+ bool HasListItems() const override { return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+ int GetViewContainerID() const override { return m_viewControl.GetCurrentControl(); }
+
+protected:
+ void GoParentFolder();
+ void OnClick(int iItem);
+ void OnSort();
+ void ClearFileItems();
+ void Update(const std::string &strDirectory);
+ bool HaveDiscOrConnection( int iDriveType );
+ bool OnPopupMenu(int iItem);
+ void OnAddNetworkLocation();
+ void OnAddMediaSource();
+ void OnEditMediaSource(CFileItem* pItem);
+ CGUIControl *GetFirstFocusableControl(int id) override;
+
+ VECSOURCES m_shares;
+ XFILE::CVirtualDirectory m_rootDir;
+ CFileItemList* m_vecItems;
+ CFileItem* m_Directory;
+ std::string m_strParentPath;
+ std::string m_selectedPath;
+ CDirectoryHistory m_history;
+ int m_browsingForFolders; // 0 - no, 1 - yes, 2 - yes, only writable
+ bool m_bConfirmed;
+ int m_bFlip;
+ bool m_addNetworkShareEnabled;
+ bool m_flipEnabled;
+ std::string m_addSourceType;
+ bool m_browsingForImages;
+ bool m_useFileDirectories;
+ bool m_singleList; // if true, we have no shares or anything
+ bool m_multipleSelection;
+ std::vector<std::string> m_markedPath;
+
+ CPictureThumbLoader m_thumbLoader;
+ CGUIViewControl m_viewControl;
+};
diff --git a/xbmc/dialogs/GUIDialogGamepad.cpp b/xbmc/dialogs/GUIDialogGamepad.cpp
new file mode 100644
index 0000000..07f5809
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogGamepad.cpp
@@ -0,0 +1,326 @@
+/*
+ * 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 "GUIDialogGamepad.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIAudioManager.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/Digest.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <utility>
+
+using namespace KODI::MESSAGING;
+using KODI::UTILITY::CDigest;
+
+CGUIDialogGamepad::CGUIDialogGamepad(void)
+ : CGUIDialogBoxBase(WINDOW_DIALOG_GAMEPAD, "DialogConfirm.xml")
+{
+ m_bCanceled = false;
+ m_iRetries = 0;
+ m_bUserInputCleanup = true;
+ m_bHideInputChars = true;
+ m_cHideInputChar = '*';
+}
+
+CGUIDialogGamepad::~CGUIDialogGamepad(void) = default;
+
+void CGUIDialogGamepad::OnInitWindow()
+{
+ // hide all controls
+ for (int i = 0; i < DIALOG_MAX_CHOICES; ++i)
+ SET_CONTROL_HIDDEN(CONTROL_CHOICES_START + i);
+ SET_CONTROL_HIDDEN(CONTROL_PROGRESS_BAR);
+
+ CGUIDialogBoxBase::OnInitWindow();
+}
+
+bool CGUIDialogGamepad::OnAction(const CAction &action)
+{
+ if ((action.GetButtonCode() >= KEY_BUTTON_A &&
+ action.GetButtonCode() <= KEY_BUTTON_RIGHT_TRIGGER) ||
+ (action.GetButtonCode() >= KEY_BUTTON_DPAD_UP &&
+ action.GetButtonCode() <= KEY_BUTTON_DPAD_RIGHT) ||
+ (action.GetID() >= ACTION_MOVE_LEFT &&
+ action.GetID() <= ACTION_MOVE_DOWN) ||
+ action.GetID() == ACTION_PLAYER_PLAY
+ )
+ {
+ switch (action.GetButtonCode())
+ {
+ case KEY_BUTTON_A : m_strUserInput += "A"; break;
+ case KEY_BUTTON_B : m_strUserInput += "B"; break;
+ case KEY_BUTTON_X : m_strUserInput += "X"; break;
+ case KEY_BUTTON_Y : m_strUserInput += "Y"; break;
+ case KEY_BUTTON_BLACK : m_strUserInput += "K"; break;
+ case KEY_BUTTON_WHITE : m_strUserInput += "W"; break;
+ case KEY_BUTTON_LEFT_TRIGGER : m_strUserInput += "("; break;
+ case KEY_BUTTON_RIGHT_TRIGGER : m_strUserInput += ")"; break;
+ case KEY_BUTTON_DPAD_UP : m_strUserInput += "U"; break;
+ case KEY_BUTTON_DPAD_DOWN : m_strUserInput += "D"; break;
+ case KEY_BUTTON_DPAD_LEFT : m_strUserInput += "L"; break;
+ case KEY_BUTTON_DPAD_RIGHT : m_strUserInput += "R"; break;
+ default:
+ switch (action.GetID())
+ {
+ case ACTION_MOVE_LEFT: m_strUserInput += "L"; break;
+ case ACTION_MOVE_RIGHT: m_strUserInput += "R"; break;
+ case ACTION_MOVE_UP: m_strUserInput += "U"; break;
+ case ACTION_MOVE_DOWN: m_strUserInput += "D"; break;
+ case ACTION_PLAYER_PLAY: m_strUserInput += "P"; break;
+ default:
+ return true;
+ }
+ break;
+ }
+
+ std::string strHiddenInput(m_strUserInput);
+ for (int i = 0; i < (int)strHiddenInput.size(); i++)
+ {
+ strHiddenInput[i] = m_cHideInputChar;
+ }
+ SetLine(2, CVariant{std::move(strHiddenInput)});
+ return true;
+ }
+ else if (action.GetButtonCode() == KEY_BUTTON_BACK || action.GetID() == ACTION_PREVIOUS_MENU || action.GetID() == ACTION_NAV_BACK)
+ {
+ m_bConfirmed = false;
+ m_bCanceled = true;
+ m_strUserInput = "";
+ m_bHideInputChars = true;
+ Close();
+ return true;
+ }
+ else if (action.GetButtonCode() == KEY_BUTTON_START || action.GetID() == ACTION_SELECT_ITEM)
+ {
+ m_bConfirmed = false;
+ m_bCanceled = false;
+
+ std::string md5pword2 = CDigest::Calculate(CDigest::Type::MD5, m_strUserInput);
+
+ if (!StringUtils::EqualsNoCase(m_strPassword, md5pword2))
+ {
+ // incorrect password entered
+ m_iRetries--;
+
+ // don't clean up if the calling code wants the bad user input
+ if (m_bUserInputCleanup)
+ m_strUserInput = "";
+ else
+ m_bUserInputCleanup = true;
+
+ m_bHideInputChars = true;
+ Close();
+ return true;
+ }
+
+ // correct password entered
+ m_bConfirmed = true;
+ m_iRetries = 0;
+ m_strUserInput = "";
+ m_bHideInputChars = true;
+ Close();
+ return true;
+ }
+ else if (action.GetID() >= REMOTE_0 && action.GetID() <= REMOTE_9)
+ {
+ return true; // unhandled
+ }
+ else
+ {
+ return CGUIDialog::OnAction(action);
+ }
+}
+
+bool CGUIDialogGamepad::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_bConfirmed = false;
+ m_bCanceled = false;
+ m_cHideInputChar = g_localizeStrings.Get(12322).c_str()[0];
+ CGUIDialog::OnMessage(message);
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ m_bConfirmed = false;
+ m_bCanceled = false;
+ }
+ break;
+ }
+ return CGUIDialogBoxBase::OnMessage(message);
+}
+
+// \brief Show gamepad keypad and replace aTextString with user input.
+// \param aTextString String to preload into the keyboard accumulator. Overwritten with user input if return=true.
+// \param dlgHeading String shown on dialog title. Converts to localized string if contains a positive integer.
+// \param bHideUserInput Masks user input as asterisks if set as true. Currently not yet implemented.
+// \return true if successful display and user input. false if unsuccessful display, no user input, or canceled editing.
+bool CGUIDialogGamepad::ShowAndGetInput(std::string& aTextString, const std::string &dlgHeading, bool bHideUserInput)
+{
+ // Prompt user for input
+ std::string strUserInput;
+ if (ShowAndVerifyInput(strUserInput, dlgHeading, aTextString, "", "", true, bHideUserInput))
+ {
+ // user entry was blank
+ return false;
+ }
+
+ if (strUserInput.empty())
+ // user canceled out
+ return false;
+
+
+ // We should have a string to return
+ aTextString = strUserInput;
+ return true;
+}
+
+// \brief Show gamepad keypad twice to get and confirm a user-entered password string.
+// \param strNewPassword String to preload into the keyboard accumulator. Overwritten with user input if return=true.
+// \return true if successful display and user input entry/re-entry. false if unsuccessful display, no user input, or canceled editing.
+bool CGUIDialogGamepad::ShowAndVerifyNewPassword(std::string& strNewPassword)
+{
+ // Prompt user for password input
+ std::string strUserInput;
+ if (ShowAndVerifyInput(strUserInput, "12340", "12330", "12331", "", true, true))
+ {
+ //! @todo Show error to user saying the password entry was blank
+ HELPERS::ShowOKDialogText(CVariant{12357}, CVariant{12358}); // Password is empty/blank
+ return false;
+ }
+
+ if (strUserInput.empty())
+ // user canceled out
+ return false;
+
+ // Prompt again for password input, this time sending previous input as the password to verify
+ if (!ShowAndVerifyInput(strUserInput, "12341", "12330", "12331", "", false, true))
+ {
+ //! @todo Show error to user saying the password re-entry failed
+ HELPERS::ShowOKDialogText(CVariant{12357}, CVariant{12344}); // Password do not match
+ return false;
+ }
+
+ // password entry and re-entry succeeded
+ strNewPassword = strUserInput;
+ return true;
+}
+
+// \brief Show gamepad keypad and verify user input against strPassword.
+// \param strPassword Value to compare against user input.
+// \param dlgHeading String shown on dialog title. Converts to localized string if contains a positive integer.
+// \param iRetries If greater than 0, shows "Incorrect password, %d retries left" on dialog line 2, else line 2 is blank.
+// \return 0 if successful display and user input. 1 if unsuccessful input. -1 if no user input or canceled editing.
+int CGUIDialogGamepad::ShowAndVerifyPassword(std::string& strPassword, const std::string& dlgHeading, int iRetries)
+{
+ std::string strLine2;
+ if (0 < iRetries)
+ {
+ // Show a string telling user they have iRetries retries left
+ strLine2 = StringUtils::Format("{} {} {}", g_localizeStrings.Get(12342), iRetries,
+ g_localizeStrings.Get(12343));
+ }
+
+ // make a copy of strPassword to prevent from overwriting it later
+ std::string strPassTemp = strPassword;
+ if (ShowAndVerifyInput(strPassTemp, dlgHeading, g_localizeStrings.Get(12330), g_localizeStrings.Get(12331), strLine2, true, true))
+ {
+ // user entered correct password
+ return 0;
+ }
+
+ if (strPassTemp.empty())
+ // user canceled out
+ return -1;
+
+ // user must have entered an incorrect password
+ return 1;
+}
+
+// \brief Show gamepad keypad and verify user input against strToVerify.
+// \param strToVerify Value to compare against user input.
+// \param dlgHeading String shown on dialog title. Converts to localized string if contains a positive integer.
+// \param dlgLine0 String shown on dialog line 0. Converts to localized string if contains a positive integer.
+// \param dlgLine1 String shown on dialog line 1. Converts to localized string if contains a positive integer.
+// \param dlgLine2 String shown on dialog line 2. Converts to localized string if contains a positive integer.
+// \param bGetUserInput If set as true and return=true, strToVerify is overwritten with user input string.
+// \param bHideInputChars Masks user input as asterisks if set as true. Currently not yet implemented.
+// \return true if successful display and user input. false if unsuccessful display, no user input, or canceled editing.
+bool CGUIDialogGamepad::ShowAndVerifyInput(std::string& strToVerify, const std::string& dlgHeading,
+ const std::string& dlgLine0, const std::string& dlgLine1,
+ const std::string& dlgLine2, bool bGetUserInput, bool bHideInputChars)
+{
+ // Prompt user for password input
+ CGUIDialogGamepad *pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogGamepad>(WINDOW_DIALOG_GAMEPAD);
+ pDialog->m_strPassword = strToVerify;
+ pDialog->m_bUserInputCleanup = !bGetUserInput;
+ pDialog->m_bHideInputChars = bHideInputChars;
+
+ // HACK: This won't work if the label specified is actually a positive numeric value, but that's very unlikely
+ if (!StringUtils::IsNaturalNumber(dlgHeading))
+ pDialog->SetHeading(CVariant{dlgHeading});
+ else
+ pDialog->SetHeading(CVariant{atoi(dlgHeading.c_str())});
+
+ if (!StringUtils::IsNaturalNumber(dlgLine0))
+ pDialog->SetLine(0, CVariant{dlgLine0});
+ else
+ pDialog->SetLine(0, CVariant{atoi(dlgLine0.c_str())});
+
+ if (!StringUtils::IsNaturalNumber(dlgLine1))
+ pDialog->SetLine(1, CVariant{dlgLine1});
+ else
+ pDialog->SetLine(1, CVariant{atoi(dlgLine1.c_str())});
+
+ if (!StringUtils::IsNaturalNumber(dlgLine2))
+ pDialog->SetLine(2, CVariant{dlgLine2});
+ else
+ pDialog->SetLine(2, CVariant{atoi(dlgLine2.c_str())});
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetAudioManager().Enable(false); // don't do sounds during pwd input
+
+ pDialog->Open();
+
+ if (gui)
+ gui->GetAudioManager().Enable(true);
+
+ if (bGetUserInput && !pDialog->IsCanceled())
+ {
+ strToVerify = CDigest::Calculate(CDigest::Type::MD5, pDialog->m_strUserInput);
+ pDialog->m_strUserInput = "";
+ }
+
+ if (!pDialog->IsConfirmed() || pDialog->IsCanceled())
+ {
+ // user canceled out or entered an incorrect password
+ return false;
+ }
+
+ // user entered correct password
+ return true;
+}
+
+bool CGUIDialogGamepad::IsCanceled() const
+{
+ return m_bCanceled;
+}
+
diff --git a/xbmc/dialogs/GUIDialogGamepad.h b/xbmc/dialogs/GUIDialogGamepad.h
new file mode 100644
index 0000000..db520ac
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogGamepad.h
@@ -0,0 +1,35 @@
+/*
+ * 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 "GUIDialogBoxBase.h"
+
+class CGUIDialogGamepad :
+ public CGUIDialogBoxBase
+{
+public:
+ CGUIDialogGamepad(void);
+ ~CGUIDialogGamepad(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool IsCanceled() const;
+ std::string m_strUserInput;
+ std::string m_strPassword;
+ int m_iRetries;
+ bool m_bUserInputCleanup;
+ bool m_bHideInputChars;
+ static bool ShowAndGetInput(std::string& aTextString, const std::string& dlgHeading, bool bHideUserInput);
+ static bool ShowAndVerifyNewPassword(std::string& strNewPassword);
+ static int ShowAndVerifyPassword(std::string& strPassword, const std::string& dlgHeading, int iRetries);
+ static bool ShowAndVerifyInput(std::string& strPassword, const std::string& dlgHeading, const std::string& dlgLine0, const std::string& dlgLine1, const std::string& dlgLine2, bool bGetUserInput, bool bHideInputChars);
+protected:
+ bool OnAction(const CAction &action) override;
+ void OnInitWindow() override;
+ bool m_bCanceled;
+ char m_cHideInputChar;
+};
diff --git a/xbmc/dialogs/GUIDialogKaiToast.cpp b/xbmc/dialogs/GUIDialogKaiToast.cpp
new file mode 100644
index 0000000..2f8ca00
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogKaiToast.cpp
@@ -0,0 +1,186 @@
+/*
+ * 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 "GUIDialogKaiToast.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIFadeLabelControl.h"
+#include "guilib/GUIMessage.h"
+#include "peripherals/Peripherals.h"
+#include "utils/TimeUtils.h"
+
+#include <mutex>
+
+#define POPUP_ICON 400
+#define POPUP_CAPTION_TEXT 401
+#define POPUP_NOTIFICATION_BUTTON 402
+
+CGUIDialogKaiToast::TOASTQUEUE CGUIDialogKaiToast::m_notifications;
+CCriticalSection CGUIDialogKaiToast::m_critical;
+
+CGUIDialogKaiToast::CGUIDialogKaiToast(void)
+ : CGUIDialog(WINDOW_DIALOG_KAI_TOAST, "DialogNotification.xml", DialogModalityType::MODELESS)
+{
+ m_loadType = LOAD_ON_GUI_INIT;
+ m_timer = 0;
+ m_toastDisplayTime = 0;
+ m_toastMessageTime = 0;
+}
+
+CGUIDialogKaiToast::~CGUIDialogKaiToast(void) = default;
+
+bool CGUIDialogKaiToast::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CGUIDialog::OnMessage(message);
+ ResetTimer();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ }
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogKaiToast::QueueNotification(eMessageType eType, const std::string& aCaption, const std::string& aDescription, unsigned int displayTime /*= TOAST_DISPLAY_TIME*/, bool withSound /*= true*/, unsigned int messageTime /*= TOAST_MESSAGE_TIME*/)
+{
+ AddToQueue("", eType, aCaption, aDescription, displayTime, withSound, messageTime);
+}
+
+void CGUIDialogKaiToast::QueueNotification(const std::string& aCaption, const std::string& aDescription)
+{
+ QueueNotification("", aCaption, aDescription);
+}
+
+void CGUIDialogKaiToast::QueueNotification(const std::string& aImageFile, const std::string& aCaption, const std::string& aDescription, unsigned int displayTime /*= TOAST_DISPLAY_TIME*/, bool withSound /*= true*/, unsigned int messageTime /*= TOAST_MESSAGE_TIME*/)
+{
+ AddToQueue(aImageFile, Default, aCaption, aDescription, displayTime, withSound, messageTime);
+}
+
+void CGUIDialogKaiToast::AddToQueue(const std::string& aImageFile, const eMessageType eType, const std::string& aCaption, const std::string& aDescription, unsigned int displayTime /*= TOAST_DISPLAY_TIME*/, bool withSound /*= true*/, unsigned int messageTime /*= TOAST_MESSAGE_TIME*/)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ if (!m_notifications.empty())
+ {
+ const auto& last = m_notifications.back();
+ if (last.eType == eType && last.imagefile == aImageFile && last.caption == aCaption &&
+ last.description == aDescription)
+ return; // avoid duplicates in a row.
+ }
+
+ Notification toast;
+ toast.eType = eType;
+ toast.imagefile = aImageFile;
+ toast.caption = aCaption;
+ toast.description = aDescription;
+ toast.displayTime = displayTime > TOAST_MESSAGE_TIME + 500 ? displayTime : TOAST_MESSAGE_TIME + 500;
+ toast.messageTime = messageTime;
+ toast.withSound = withSound;
+
+ m_notifications.push(toast);
+}
+
+bool CGUIDialogKaiToast::DoWork()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ if (!m_notifications.empty() &&
+ CTimeUtils::GetFrameTime() - m_timer > m_toastMessageTime)
+ {
+ // if we have a fade label control for the text to display, ensure the whole text was shown
+ // (scrolled to the end) before we move on to the next message
+ const CGUIFadeLabelControl* notificationText =
+ dynamic_cast<const CGUIFadeLabelControl*>(GetControl(POPUP_NOTIFICATION_BUTTON));
+ if (notificationText && !notificationText->AllLabelsShown())
+ return false;
+
+ Notification toast = m_notifications.front();
+ m_notifications.pop();
+ lock.unlock();
+
+ m_toastDisplayTime = toast.displayTime;
+ m_toastMessageTime = toast.messageTime;
+
+ std::unique_lock<CCriticalSection> lock2(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ if(!Initialize())
+ return false;
+
+ SET_CONTROL_LABEL(POPUP_CAPTION_TEXT, toast.caption);
+
+ SET_CONTROL_LABEL(POPUP_NOTIFICATION_BUTTON, toast.description);
+
+ // set the appropriate icon
+ {
+ std::string icon = toast.imagefile;
+ if (icon.empty())
+ {
+ if (toast.eType == Warning)
+ icon = "DefaultIconWarning.png";
+ else if (toast.eType == Error)
+ icon = "DefaultIconError.png";
+ else
+ icon = "DefaultIconInfo.png";
+ }
+ SET_CONTROL_FILENAME(POPUP_ICON, icon);
+ }
+
+ // Play the window specific init sound for each notification queued
+ SetSound(toast.withSound);
+
+ // Activate haptics for this notification
+ CServiceBroker::GetPeripherals().OnUserNotification();
+
+ ResetTimer();
+ return true;
+ }
+
+ return false;
+}
+
+
+void CGUIDialogKaiToast::ResetTimer()
+{
+ m_timer = CTimeUtils::GetFrameTime();
+}
+
+void CGUIDialogKaiToast::FrameMove()
+{
+ // Fading does not count as display time
+ if (IsAnimating(ANIM_TYPE_WINDOW_OPEN))
+ ResetTimer();
+
+ // now check if we should exit
+ if (CTimeUtils::GetFrameTime() - m_timer > m_toastDisplayTime)
+ {
+ bool bClose = true;
+
+ // if we have a fade label control for the text to display, ensure the whole text was shown
+ // (scrolled to the end) before we're closing the toast dialog
+ const CGUIFadeLabelControl* notificationText =
+ dynamic_cast<const CGUIFadeLabelControl*>(GetControl(POPUP_NOTIFICATION_BUTTON));
+ if (notificationText)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ bClose = notificationText->AllLabelsShown() && m_notifications.empty();
+ }
+
+ if (bClose)
+ Close();
+ }
+
+ CGUIDialog::FrameMove();
+}
diff --git a/xbmc/dialogs/GUIDialogKaiToast.h b/xbmc/dialogs/GUIDialogKaiToast.h
new file mode 100644
index 0000000..78f2919
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogKaiToast.h
@@ -0,0 +1,58 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+#include <queue>
+
+#define TOAST_DISPLAY_TIME 5000L // default 5 seconds
+#define TOAST_MESSAGE_TIME 1000L // minimal message time 1 second
+
+class CGUIDialogKaiToast: public CGUIDialog
+{
+public:
+ CGUIDialogKaiToast(void);
+ ~CGUIDialogKaiToast(void) override;
+
+ enum eMessageType { Default = 0, Info, Warning, Error };
+
+ struct Notification
+ {
+ std::string caption;
+ std::string description;
+ std::string imagefile;
+ eMessageType eType;
+ unsigned int displayTime;
+ unsigned int messageTime;
+ bool withSound;
+ };
+
+ typedef std::queue<Notification> TOASTQUEUE;
+
+ static void QueueNotification(eMessageType eType, const std::string& aCaption, const std::string& aDescription, unsigned int displayTime = TOAST_DISPLAY_TIME, bool withSound = true, unsigned int messageTime = TOAST_MESSAGE_TIME);
+ static void QueueNotification(const std::string& aCaption, const std::string& aDescription);
+ static void QueueNotification(const std::string& aImageFile, const std::string& aCaption, const std::string& aDescription, unsigned int displayTime = TOAST_DISPLAY_TIME, bool withSound = true, unsigned int messageTime = TOAST_MESSAGE_TIME);
+ bool DoWork();
+
+ bool OnMessage(CGUIMessage& message) override;
+ void FrameMove() override;
+ void ResetTimer();
+
+protected:
+ static void AddToQueue(const std::string& aImageFile, const eMessageType eType, const std::string& aCaption, const std::string& aDescription, unsigned int displayTime, bool withSound, unsigned int messageTime);
+
+ unsigned int m_timer;
+
+ unsigned int m_toastDisplayTime;
+ unsigned int m_toastMessageTime;
+
+ static TOASTQUEUE m_notifications;
+ static CCriticalSection m_critical;
+};
diff --git a/xbmc/dialogs/GUIDialogKeyboardGeneric.cpp b/xbmc/dialogs/GUIDialogKeyboardGeneric.cpp
new file mode 100644
index 0000000..5eeed8f
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogKeyboardGeneric.cpp
@@ -0,0 +1,843 @@
+/*
+ * 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 "GUIDialogKeyboardGeneric.h"
+
+#include "GUIDialogNumeric.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUILabelControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/InputCodingTable.h"
+#include "input/Key.h"
+#include "input/KeyboardLayoutManager.h"
+#include "input/XBMC_vkeys.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "speech/ISpeechRecognition.h"
+#include "speech/ISpeechRecognitionListener.h"
+#include "speech/SpeechRecognitionErrors.h"
+#include "utils/CharsetConverter.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+
+using namespace KODI::MESSAGING;
+
+#define BUTTON_ID_OFFSET 100
+#define BUTTONS_PER_ROW 20
+#define BUTTONS_MAX_ROWS 4
+
+#define CTL_BUTTON_DONE 300
+#define CTL_BUTTON_CANCEL 301
+#define CTL_BUTTON_SHIFT 302
+#define CTL_BUTTON_CAPS 303
+#define CTL_BUTTON_SYMBOLS 304
+#define CTL_BUTTON_LEFT 305
+#define CTL_BUTTON_RIGHT 306
+#define CTL_BUTTON_IP_ADDRESS 307
+#define CTL_BUTTON_CLEAR 308
+#define CTL_BUTTON_LAYOUT 309
+#define CTL_BUTTON_REVEAL 310
+#define CTL_LABEL_HEADING 311
+#define CTL_EDIT 312
+#define CTL_LABEL_HZCODE 313
+#define CTL_LABEL_HZLIST 314
+
+#define CTL_BUTTON_BACKSPACE 8
+#define CTL_BUTTON_SPACE 32
+
+#define SEARCH_DELAY 1000
+
+class CSpeechRecognitionListener : public speech::ISpeechRecognitionListener
+{
+public:
+ CSpeechRecognitionListener(int dialogId) : m_dialogId(dialogId) {}
+
+ void OnReadyForSpeech() override
+ {
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info,
+ g_localizeStrings.Get(39177), // Speech to text
+ g_localizeStrings.Get(39179)); // Listening...
+ }
+
+ void OnError(int recognitionError) override
+ {
+ uint32_t msgId = 0;
+ switch (recognitionError)
+ {
+ case speech::RecognitionError::SERVICE_NOT_AVAILABLE:
+ msgId = 39178; // Speech recognition service not available
+ break;
+ case speech::RecognitionError::NO_MATCH:
+ msgId = 39180; // No recognition result matched
+ break;
+ case speech::RecognitionError::INSUFFICIENT_PERMISSIONS:
+ msgId = 39185; // Insufficient permissions for speech recognition
+ break;
+ default:
+ msgId = 39181; // Speech recognition error
+ break;
+ }
+
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(39177), // Speech to text
+ g_localizeStrings.Get(msgId));
+ }
+
+ void OnResults(const std::vector<std::string>& results) override
+ {
+ if (!results.empty())
+ {
+ CGUIMessage msg(GUI_MSG_SET_TEXT, m_dialogId, CTL_EDIT);
+ msg.SetLabel(results.front());
+
+ // dispatch to GUI thread
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, m_dialogId);
+ }
+ }
+
+private:
+ const int m_dialogId{0};
+};
+
+CGUIDialogKeyboardGeneric::CGUIDialogKeyboardGeneric()
+: CGUIDialog(WINDOW_DIALOG_KEYBOARD, "DialogKeyboard.xml")
+, CGUIKeyboard()
+, m_pCharCallback(NULL)
+{
+ m_bIsConfirmed = false;
+ m_bShift = false;
+ m_hiddenInput = false;
+ m_keyType = LOWER;
+ m_currentLayout = 0;
+ m_loadType = KEEP_IN_MEMORY;
+ m_isKeyboardNavigationMode = false;
+ m_previouslyFocusedButton = 0;
+ m_pos = 0;
+ m_listwidth = 600;
+}
+
+void CGUIDialogKeyboardGeneric::OnWindowLoaded()
+{
+ CGUIEditControl *edit = static_cast<CGUIEditControl*>(GetControl(CTL_EDIT));
+ if (edit)
+ {
+ // add control CTL_LABEL_HZCODE and CTL_LABEL_HZLIST if not exist
+ CGUIControlGroup *ParentControl = static_cast<CGUIControlGroup*>(edit->GetParentControl());
+ CLabelInfo labelInfo = edit->GetLabelInfo();
+ float px = edit->GetXPosition();
+ float py = edit->GetYPosition();
+ float pw = edit->GetWidth();
+ float ph = edit->GetHeight();
+
+ CGUILabelControl* control = static_cast<CGUILabelControl*>(GetControl(CTL_LABEL_HZCODE));
+ if (!control)
+ {
+ control = new CGUILabelControl(GetID(), CTL_LABEL_HZCODE, px, py + ph, 90, 30, labelInfo, false, false);
+ ParentControl->AddControl(control);
+ }
+
+ control = static_cast<CGUILabelControl*>(GetControl(CTL_LABEL_HZLIST));
+ if (!control)
+ {
+ labelInfo.align = XBFONT_CENTER_Y;
+ control = new CGUILabelControl(GetID(), CTL_LABEL_HZLIST, px + 95, py + ph, pw - 95, 30, labelInfo, false, false);
+ ParentControl->AddControl(control);
+ }
+ }
+
+ CGUIDialog::OnWindowLoaded();
+}
+
+void CGUIDialogKeyboardGeneric::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ m_bIsConfirmed = false;
+ m_isKeyboardNavigationMode = false;
+
+ // fill in the keyboard layouts
+ m_currentLayout = 0;
+ m_layouts.clear();
+ const KeyboardLayouts& keyboardLayouts = CServiceBroker::GetKeyboardLayoutManager()->GetLayouts();
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::vector<CVariant> layoutNames = settings->GetList(CSettings::SETTING_LOCALE_KEYBOARDLAYOUTS);
+ std::string activeLayout = settings->GetString(CSettings::SETTING_LOCALE_ACTIVEKEYBOARDLAYOUT);
+
+ for (const auto& layoutName : layoutNames)
+ {
+ const auto keyboardLayout = keyboardLayouts.find(layoutName.asString());
+ if (keyboardLayout != keyboardLayouts.end())
+ {
+ m_layouts.emplace_back(keyboardLayout->second);
+ if (layoutName.asString() == activeLayout)
+ m_currentLayout = m_layouts.size() - 1;
+ }
+ }
+
+ // set alphabetic (capitals)
+ UpdateButtons();
+
+ // set heading
+ if (!m_strHeading.empty())
+ {
+ SET_CONTROL_LABEL(CTL_LABEL_HEADING, m_strHeading);
+ SET_CONTROL_VISIBLE(CTL_LABEL_HEADING);
+ }
+ else
+ {
+ SET_CONTROL_HIDDEN(CTL_LABEL_HEADING);
+ }
+ // set type
+ {
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CTL_EDIT, m_hiddenInput ? CGUIEditControl::INPUT_TYPE_PASSWORD : CGUIEditControl::INPUT_TYPE_TEXT);
+ OnMessage(msg);
+ }
+ if (m_hiddenInput)
+ {
+ SET_CONTROL_VISIBLE(CTL_BUTTON_REVEAL);
+ SET_CONTROL_LABEL(CTL_BUTTON_REVEAL, g_localizeStrings.Get(12308));
+ }
+ else
+ SET_CONTROL_HIDDEN(CTL_BUTTON_REVEAL);
+
+ SetEditText(m_text);
+
+ // get HZLIST label options
+ CGUILabelControl* pEdit = static_cast<CGUILabelControl*>(GetControl(CTL_LABEL_HZLIST));
+ CLabelInfo labelInfo = pEdit->GetLabelInfo();
+ m_listfont = labelInfo.font;
+ m_listwidth = pEdit->GetWidth();
+ m_hzcode.clear();
+ m_words.clear();
+ SET_CONTROL_LABEL(CTL_LABEL_HZCODE, "");
+ SET_CONTROL_LABEL(CTL_LABEL_HZLIST, "");
+
+ CVariant data;
+ data["title"] = m_strHeading;
+ data["type"] = !m_hiddenInput ? "keyboard" : "password";
+ data["value"] = GetText();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Input, "OnInputRequested", data);
+}
+
+bool CGUIDialogKeyboardGeneric::OnAction(const CAction &action)
+{
+ int actionId = action.GetID();
+ bool handled = true;
+ if (actionId == (KEY_VKEY | XBMCVK_BACK))
+ Backspace();
+ else if (actionId == ACTION_ENTER ||
+ (actionId == ACTION_SELECT_ITEM && (m_isKeyboardNavigationMode || GetFocusedControlID() == CTL_EDIT)))
+ OnOK();
+ else if (actionId == ACTION_SHIFT)
+ OnShift();
+ else if (actionId == ACTION_SYMBOLS)
+ OnSymbols();
+ // don't handle move left/right and select in the edit control
+ else if (!m_isKeyboardNavigationMode &&
+ (actionId == ACTION_MOVE_LEFT ||
+ actionId == ACTION_MOVE_RIGHT ||
+ actionId == ACTION_SELECT_ITEM))
+ handled = false;
+ else if (actionId == ACTION_VOICE_RECOGNIZE)
+ OnVoiceRecognition();
+ else
+ {
+ std::wstring wch = L"";
+ wch.insert(wch.begin(), action.GetUnicode());
+ std::string ch;
+ g_charsetConverter.wToUTF8(wch, ch);
+ handled = CodingCharacter(ch);
+ if (!handled)
+ {
+ // send action to edit control
+ CGUIControl *edit = GetControl(CTL_EDIT);
+ if (edit)
+ handled = edit->OnAction(action);
+ if (!handled && actionId >= KEY_VKEY && actionId < KEY_UNICODE)
+ {
+ unsigned char b = actionId & 0xFF;
+ if (b == XBMCVK_TAB)
+ {
+ // Toggle left/right key mode
+ m_isKeyboardNavigationMode = !m_isKeyboardNavigationMode;
+ if (m_isKeyboardNavigationMode)
+ {
+ m_previouslyFocusedButton = GetFocusedControlID();
+ SET_CONTROL_FOCUS(edit->GetID(), 0);
+ }
+ else
+ SET_CONTROL_FOCUS(m_previouslyFocusedButton, 0);
+ handled = true;
+ }
+ }
+ }
+ }
+
+ if (!handled) // unhandled by us - let's see if the baseclass wants it
+ handled = CGUIDialog::OnAction(action);
+
+ return handled;
+}
+
+bool CGUIDialogKeyboardGeneric::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+
+ switch (iControl)
+ {
+ case CTL_BUTTON_DONE:
+ OnOK();
+ break;
+ case CTL_BUTTON_CANCEL:
+ Close();
+ break;
+ case CTL_BUTTON_SHIFT:
+ OnShift();
+ break;
+ case CTL_BUTTON_CAPS:
+ if (m_keyType == LOWER)
+ m_keyType = CAPS;
+ else if (m_keyType == CAPS)
+ m_keyType = LOWER;
+ UpdateButtons();
+ break;
+ case CTL_BUTTON_LAYOUT:
+ OnLayout();
+ break;
+ case CTL_BUTTON_REVEAL:
+ OnReveal();
+ break;
+ case CTL_BUTTON_SYMBOLS:
+ OnSymbols();
+ break;
+ case CTL_BUTTON_LEFT:
+ MoveCursor( -1);
+ break;
+ case CTL_BUTTON_RIGHT:
+ MoveCursor(1);
+ break;
+ case CTL_BUTTON_IP_ADDRESS:
+ OnIPAddress();
+ break;
+ case CTL_BUTTON_CLEAR:
+ SetEditText("");
+ break;
+ case CTL_EDIT:
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CTL_EDIT);
+ OnMessage(msg);
+ // update callback I guess?
+ if (m_pCharCallback)
+ { // we did _something_, so make sure our search message filter is reset
+ m_pCharCallback(this, msg.GetLabel());
+ }
+ m_text = msg.GetLabel();
+ return true;
+ }
+ default:
+ OnClickButton(iControl);
+ break;
+ }
+ }
+ break;
+
+ case GUI_MSG_SET_TEXT:
+ {
+ // the edit control only handles these messages if it is either focused
+ // or its specific control ID is set in the message. As neither is the
+ // case here (focus is on one of the keyboard buttons) we have to force
+ // the control ID of the message to the control ID of the edit control
+ // (unfortunately we have to create a whole copy of the message object for that)
+ CGUIMessage messageCopy(message.GetMessage(), message.GetSenderId(), CTL_EDIT, message.GetParam1(), message.GetParam2(), message.GetItem());
+ messageCopy.SetLabel(message.GetLabel());
+
+ // ensure this goes to the edit control
+ CGUIControl *edit = GetControl(CTL_EDIT);
+ if (edit)
+ edit->OnMessage(messageCopy);
+
+ // close the dialog if requested
+ if (message.GetMessage() == GUI_MSG_SET_TEXT && message.GetParam1() > 0)
+ OnOK();
+ return true;
+ }
+ case GUI_MSG_CODINGTABLE_LOOKUP_COMPLETED:
+ {
+ const std::string& code = message.GetStringParam();
+ if (code == m_hzcode)
+ {
+ int response = message.GetParam1();
+ auto words = m_codingtable->GetResponse(response);
+ m_words.insert(m_words.end(), words.begin(), words.end());
+ ShowWordList(0);
+ }
+ }
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogKeyboardGeneric::SetEditText(const std::string &text)
+{
+ CGUIMessage msg(GUI_MSG_SET_TEXT, GetID(), CTL_EDIT);
+ msg.SetLabel(text);
+ OnMessage(msg);
+}
+
+void CGUIDialogKeyboardGeneric::SetText(const std::string& text)
+{
+ m_text = text;
+}
+
+const std::string &CGUIDialogKeyboardGeneric::GetText() const
+{
+ return m_text;
+}
+
+void CGUIDialogKeyboardGeneric::Character(const std::string &ch)
+{
+ if (ch.empty()) return;
+ if (!CodingCharacter(ch))
+ NormalCharacter(ch);
+}
+
+void CGUIDialogKeyboardGeneric::NormalCharacter(const std::string &ch)
+{
+ // send text to edit control
+ CGUIControl *edit = GetControl(CTL_EDIT);
+ if (edit)
+ {
+ CAction action(ACTION_INPUT_TEXT);
+ action.SetText(ch);
+ edit->OnAction(action);
+ }
+}
+
+void CGUIDialogKeyboardGeneric::Backspace()
+{
+ if (m_codingtable && m_hzcode.length() > 0)
+ {
+ std::wstring tmp;
+ g_charsetConverter.utf8ToW(m_hzcode, tmp);
+ tmp.erase(tmp.length() - 1, 1);
+ g_charsetConverter.wToUTF8(tmp, m_hzcode);
+
+ switch (m_codingtable->GetType())
+ {
+ case IInputCodingTable::TYPE_WORD_LIST:
+ SetControlLabel(CTL_LABEL_HZCODE, m_hzcode);
+ ChangeWordList(0);
+ break;
+
+ case IInputCodingTable::TYPE_CONVERT_STRING:
+ SetEditText(m_codingtable->ConvertString(m_hzcode));
+ break;
+ }
+ }
+ else
+ {
+ // send action to edit control
+ CGUIControl *edit = GetControl(CTL_EDIT);
+ if (edit)
+ edit->OnAction(CAction(ACTION_BACKSPACE));
+
+ if (m_codingtable && m_codingtable->GetType() == IInputCodingTable::TYPE_CONVERT_STRING)
+ m_codingtable->SetTextPrev(GetText());
+ }
+}
+
+void CGUIDialogKeyboardGeneric::OnClickButton(int iButtonControl)
+{
+ if (iButtonControl == CTL_BUTTON_BACKSPACE)
+ {
+ Backspace();
+ }
+ else if (iButtonControl == CTL_BUTTON_SPACE)
+ {
+ Character(" ");
+ }
+ else
+ {
+ const CGUIControl* pButton = GetControl(iButtonControl);
+ // Do not register input for buttons with id >= 500
+ if (pButton && iButtonControl < 500)
+ {
+ Character(pButton->GetDescription());
+ // reset the shift keys
+ if (m_bShift) OnShift();
+ }
+ }
+}
+
+void CGUIDialogKeyboardGeneric::UpdateButtons()
+{
+ SET_CONTROL_SELECTED(GetID(), CTL_BUTTON_SHIFT, m_bShift);
+ SET_CONTROL_SELECTED(GetID(), CTL_BUTTON_CAPS, m_keyType == CAPS);
+ SET_CONTROL_SELECTED(GetID(), CTL_BUTTON_SYMBOLS, m_keyType == SYMBOLS);
+
+ if (m_currentLayout >= m_layouts.size())
+ m_currentLayout = 0;
+ CKeyboardLayout layout = m_layouts.empty() ? CKeyboardLayout() : m_layouts[m_currentLayout];
+ m_codingtable = layout.GetCodingTable();
+ if (m_codingtable && !m_codingtable->IsInitialized())
+ m_codingtable->Initialize();
+
+ bool bShowWordList = false;
+ if (m_codingtable)
+ {
+ switch (m_codingtable->GetType())
+ {
+ case IInputCodingTable::TYPE_WORD_LIST:
+ bShowWordList = true;
+ break;
+
+ case IInputCodingTable::TYPE_CONVERT_STRING:
+ m_codingtable->SetTextPrev(GetText());
+ m_hzcode.clear();
+ break;
+ }
+ }
+
+ if (bShowWordList)
+ {
+ SET_CONTROL_VISIBLE(CTL_LABEL_HZCODE);
+ SET_CONTROL_VISIBLE(CTL_LABEL_HZLIST);
+ }
+ else
+ {
+ SET_CONTROL_HIDDEN(CTL_LABEL_HZCODE);
+ SET_CONTROL_HIDDEN(CTL_LABEL_HZLIST);
+ }
+ SET_CONTROL_LABEL(CTL_BUTTON_LAYOUT, layout.GetName());
+
+ unsigned int modifiers = CKeyboardLayout::ModifierKeyNone;
+ if ((m_keyType == CAPS && !m_bShift) || (m_keyType == LOWER && m_bShift))
+ modifiers |= CKeyboardLayout::ModifierKeyShift;
+ if (m_keyType == SYMBOLS)
+ {
+ modifiers |= CKeyboardLayout::ModifierKeySymbol;
+ if (m_bShift)
+ modifiers |= CKeyboardLayout::ModifierKeyShift;
+ }
+
+ for (unsigned int row = 0; row < BUTTONS_MAX_ROWS; row++)
+ {
+ for (unsigned int column = 0; column < BUTTONS_PER_ROW; column++)
+ {
+ int buttonID = (row * BUTTONS_PER_ROW) + column + BUTTON_ID_OFFSET;
+ std::string label = layout.GetCharAt(row, column, modifiers);
+ SetControlLabel(buttonID, label);
+ if (!label.empty())
+ SET_CONTROL_VISIBLE(buttonID);
+ else
+ SET_CONTROL_HIDDEN(buttonID);
+ }
+ }
+}
+
+void CGUIDialogKeyboardGeneric::OnDeinitWindow(int nextWindowID)
+{
+ for (auto& layout : m_layouts)
+ {
+ auto codingTable = layout.GetCodingTable();
+ if (codingTable && codingTable->IsInitialized())
+ codingTable->Deinitialize();
+ }
+ // call base class
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+ // reset the heading (we don't always have this)
+ m_strHeading = "";
+
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Input, "OnInputFinished");
+}
+
+void CGUIDialogKeyboardGeneric::MoveCursor(int iAmount)
+{
+ if (m_codingtable && m_words.size())
+ ChangeWordList(iAmount);
+ else
+ {
+ CGUIControl *edit = GetControl(CTL_EDIT);
+ if (edit)
+ edit->OnAction(CAction(iAmount < 0 ? ACTION_CURSOR_LEFT : ACTION_CURSOR_RIGHT));
+ }
+}
+
+void CGUIDialogKeyboardGeneric::OnLayout()
+{
+ m_currentLayout++;
+ if (m_currentLayout >= m_layouts.size())
+ m_currentLayout = 0;
+ CKeyboardLayout layout = m_layouts.empty() ? CKeyboardLayout() : m_layouts[m_currentLayout];
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_LOCALE_ACTIVEKEYBOARDLAYOUT, layout.GetName());
+ UpdateButtons();
+}
+
+void CGUIDialogKeyboardGeneric::OnSymbols()
+{
+ if (m_keyType == SYMBOLS)
+ m_keyType = LOWER;
+ else
+ m_keyType = SYMBOLS;
+ UpdateButtons();
+}
+
+void CGUIDialogKeyboardGeneric::OnReveal()
+{
+ m_hiddenInput = !m_hiddenInput;
+ SET_CONTROL_LABEL(CTL_BUTTON_REVEAL, g_localizeStrings.Get(m_hiddenInput ? 12308 : 12309));
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CTL_EDIT,
+ m_hiddenInput ? CGUIEditControl::INPUT_TYPE_PASSWORD
+ : CGUIEditControl::INPUT_TYPE_TEXT);
+ OnMessage(msg);
+}
+
+void CGUIDialogKeyboardGeneric::OnShift()
+{
+ m_bShift = !m_bShift;
+ UpdateButtons();
+}
+
+void CGUIDialogKeyboardGeneric::OnIPAddress()
+{
+ // find any IP address in the current string if there is any
+ // We match to #.#.#.#
+ std::string text = GetText();
+ std::string ip;
+ CRegExp reg;
+ reg.RegComp("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+");
+ int start = reg.RegFind(text.c_str());
+ int length = 0;
+ if (start > -1)
+ {
+ length = reg.GetSubLength(0);
+ ip = text.substr(start, length);
+ }
+ else
+ start = text.size();
+ if (CGUIDialogNumeric::ShowAndGetIPAddress(ip, g_localizeStrings.Get(14068)))
+ SetEditText(text.substr(0, start) + ip.c_str() + text.substr(start + length));
+}
+
+void CGUIDialogKeyboardGeneric::OnVoiceRecognition()
+{
+ const auto speechRecognition = CServiceBroker::GetSpeechRecognition();
+ if (speechRecognition)
+ {
+ if (!m_speechRecognitionListener)
+ m_speechRecognitionListener = std::make_shared<CSpeechRecognitionListener>(GetID());
+
+ speechRecognition->StartSpeechRecognition(m_speechRecognitionListener);
+ }
+ else
+ {
+ CLog::LogF(LOGWARNING, "No voice recognition implementation available.");
+ }
+}
+
+void CGUIDialogKeyboardGeneric::SetControlLabel(int id, const std::string &label)
+{ // find all controls with this id, and set all their labels
+ CGUIMessage message(GUI_MSG_LABEL_SET, GetID(), id);
+ message.SetLabel(label);
+ for (unsigned int i = 0; i < m_children.size(); i++)
+ {
+ if (m_children[i]->GetID() == id || m_children[i]->IsGroup())
+ m_children[i]->OnMessage(message);
+ }
+}
+
+void CGUIDialogKeyboardGeneric::OnOK()
+{
+ m_bIsConfirmed = true;
+ Close();
+}
+
+void CGUIDialogKeyboardGeneric::SetHeading(const std::string &heading)
+{
+ m_strHeading = heading;
+}
+
+int CGUIDialogKeyboardGeneric::GetWindowId() const
+{
+ return GetID();
+}
+
+void CGUIDialogKeyboardGeneric::Cancel()
+{
+ m_bIsConfirmed = false;
+ Close();
+}
+
+bool CGUIDialogKeyboardGeneric::ShowAndGetInput(char_callback_t pCallback, const std::string &initialString, std::string &typedString, const std::string &heading, bool bHiddenInput)
+{
+ CGUIDialogKeyboardGeneric *pKeyboard = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogKeyboardGeneric>(WINDOW_DIALOG_KEYBOARD);
+
+ if (!pKeyboard)
+ return false;
+
+ m_pCharCallback = pCallback;
+ // setup keyboard
+ pKeyboard->Initialize();
+ pKeyboard->SetHeading(heading);
+ pKeyboard->SetHiddenInput(bHiddenInput);
+ pKeyboard->SetText(initialString);
+ pKeyboard->Open();
+ pKeyboard->Close();
+
+ // If have text - update this.
+ if (pKeyboard->IsConfirmed())
+ {
+ typedString = pKeyboard->GetText();
+ return true;
+ }
+ else return false;
+}
+
+float CGUIDialogKeyboardGeneric::GetStringWidth(const std::wstring & utf16)
+{
+ vecText utf32;
+
+ utf32.resize(utf16.size());
+ for (unsigned int i = 0; i < utf16.size(); i++)
+ utf32[i] = utf16[i];
+
+ return m_listfont->GetTextWidth(utf32);
+}
+
+void CGUIDialogKeyboardGeneric::ChangeWordList(int direct)
+{
+ if (direct == 0)
+ {
+ m_pos = 0;
+ m_words.clear();
+ m_codingtable->GetWordListPage(m_hzcode, true);
+ }
+ else
+ {
+ ShowWordList(direct);
+ if (direct > 0 && m_pos + m_num == static_cast<int>(m_words.size()))
+ m_codingtable->GetWordListPage(m_hzcode, false);
+ }
+}
+
+void CGUIDialogKeyboardGeneric::ShowWordList(int direct)
+{
+ std::unique_lock<CCriticalSection> lock(m_CS);
+ std::wstring hzlist = L"";
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetScalingResolution(m_coordsRes, true);
+ float width = m_listfont->GetCharWidth(L'<') + m_listfont->GetCharWidth(L'>');
+ float spacewidth = m_listfont->GetCharWidth(L' ');
+ float numwidth = m_listfont->GetCharWidth(L'1') + m_listfont->GetCharWidth(L'.');
+ int i;
+
+ if (direct >= 0)
+ {
+ if (direct > 0)
+ m_pos += m_num;
+ if (m_pos > static_cast<int>(m_words.size()) - 1)
+ m_pos = 0;
+ for (i = 0; m_pos + i < static_cast<int>(m_words.size()); i++)
+ {
+ if ((i > 0 && width + GetStringWidth(m_words[m_pos + i]) + numwidth > m_listwidth) || i > 9)
+ break;
+ hzlist.insert(hzlist.length(), 1, (wchar_t)(i + 48));
+ hzlist.insert(hzlist.length(), 1, L'.');
+ hzlist.append(m_words[m_pos + i]);
+ hzlist.insert(hzlist.length(), 1, L' ');
+ width += GetStringWidth(m_words[m_pos + i]) + numwidth + spacewidth;
+ }
+ m_num = i;
+ }
+ else
+ {
+ if (m_pos == 0)
+ return;
+ for (i = 1; i <= 10; i++)
+ {
+ if (m_pos - i < 0 || (i > 1 && width + GetStringWidth(m_words[m_pos - i]) + numwidth > m_listwidth))
+ break;
+ width += GetStringWidth(m_words[m_pos - i]) + numwidth + spacewidth;
+ }
+ m_num = --i;
+ m_pos -= m_num;
+ for (i = 0; i < m_num; i++)
+ {
+ hzlist.insert(hzlist.length(), 1, (wchar_t)(i + 48));
+ hzlist.insert(hzlist.length(), 1, L'.');
+ hzlist.append(m_words[m_pos + i]);
+ hzlist.insert(hzlist.length(), 1, L' ');
+ }
+ }
+ hzlist.erase(hzlist.find_last_not_of(L' ') + 1);
+ if (m_pos > 0)
+ hzlist.insert(0, 1, L'<');
+ if (m_pos + m_num < static_cast<int>(m_words.size()))
+ hzlist.insert(hzlist.length(), 1, L'>');
+ std::string utf8String;
+ g_charsetConverter.wToUTF8(hzlist, utf8String);
+ SET_CONTROL_LABEL(CTL_LABEL_HZLIST, utf8String);
+}
+
+bool CGUIDialogKeyboardGeneric::CodingCharacter(const std::string &ch)
+{
+ if (!m_codingtable)
+ return false;
+
+ switch (m_codingtable->GetType())
+ {
+ case IInputCodingTable::TYPE_CONVERT_STRING:
+ if (!ch.empty() && ch[0] != 0)
+ {
+ m_hzcode += ch;
+ SetEditText(m_codingtable->ConvertString(m_hzcode));
+ return true;
+ }
+ break;
+
+ case IInputCodingTable::TYPE_WORD_LIST:
+ if (m_codingtable->GetCodeChars().find(ch) != std::string::npos)
+ {
+ m_hzcode += ch;
+ SetControlLabel(CTL_LABEL_HZCODE, m_hzcode);
+ ChangeWordList(0);
+ return true;
+ }
+ if (ch[0] >= '0' && ch[0] <= '9')
+ {
+ int i = m_pos + (int)ch[0] - 48;
+ if (i < (m_pos + m_num))
+ {
+ m_hzcode = "";
+ SetControlLabel(CTL_LABEL_HZCODE, m_hzcode);
+ std::string utf8String;
+ g_charsetConverter.wToUTF8(m_words[i], utf8String);
+ NormalCharacter(utf8String);
+ }
+ return true;
+ }
+ break;
+ }
+
+ return false;
+}
diff --git a/xbmc/dialogs/GUIDialogKeyboardGeneric.h b/xbmc/dialogs/GUIDialogKeyboardGeneric.h
new file mode 100644
index 0000000..c88d056
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogKeyboardGeneric.h
@@ -0,0 +1,95 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "guilib/GUIKeyboard.h"
+#include "input/KeyboardLayout.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CGUIFont;
+
+class CSpeechRecognitionListener;
+
+enum KEYBOARD {CAPS, LOWER, SYMBOLS};
+
+class CGUIDialogKeyboardGeneric : public CGUIDialog, public CGUIKeyboard
+{
+ public:
+ CGUIDialogKeyboardGeneric();
+
+ //CGUIKeyboard Interface
+ bool ShowAndGetInput(char_callback_t pCallback, const std::string &initialString, std::string &typedString, const std::string &heading, bool bHiddenInput) override;
+ void Cancel() override;
+ int GetWindowId() const override;
+
+ void SetHeading(const std::string& heading);
+ void SetText(const std::string& text);
+ const std::string &GetText() const;
+ bool IsConfirmed() { return m_bIsConfirmed; }
+ void SetHiddenInput(bool hiddenInput) { m_hiddenInput = hiddenInput; }
+ bool IsInputHidden() const { return m_hiddenInput; }
+
+ protected:
+ void OnWindowLoaded() override;
+ void OnInitWindow() override;
+ bool OnAction(const CAction &action) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void OnDeinitWindow(int nextWindowID) override;
+ void SetControlLabel(int id, const std::string &label);
+ void OnShift();
+ void MoveCursor(int iAmount);
+ void OnLayout();
+ void OnReveal();
+ void OnSymbols();
+ void OnIPAddress();
+ void OnVoiceRecognition();
+ void OnOK();
+
+ private:
+ void OnClickButton(int iButtonControl);
+ void UpdateButtons();
+ void Character(const std::string &ch);
+ void Backspace();
+ void SetEditText(const std::string& text);
+ float GetStringWidth(const std::wstring& utf16);
+ void ChangeWordList(int direct); // direct: 0 - first page, 1 - next page, -1 - prev page
+ void ShowWordList(int which); // which: 0 - current page, 1 - next page, -1 -prev page
+ bool CodingCharacter(const std::string &ch);
+ void NormalCharacter(const std::string &ch);
+
+ bool m_bIsConfirmed;
+ KEYBOARD m_keyType;
+ bool m_bShift;
+ bool m_hiddenInput;
+ bool m_isKeyboardNavigationMode;
+ int m_previouslyFocusedButton;
+
+ std::vector<CKeyboardLayout> m_layouts;
+ unsigned int m_currentLayout;
+
+ std::string m_strHeading;
+ std::string m_text; ///< current text
+
+ IInputCodingTablePtr m_codingtable;
+ std::vector<std::wstring> m_words;
+ std::string m_hzcode;
+ int m_pos;
+ int m_num = 0;
+ float m_listwidth;
+ CGUIFont *m_listfont = nullptr;
+ CCriticalSection m_CS;
+
+ char_callback_t m_pCharCallback;
+
+ std::shared_ptr<CSpeechRecognitionListener> m_speechRecognitionListener;
+};
diff --git a/xbmc/dialogs/GUIDialogKeyboardTouch.cpp b/xbmc/dialogs/GUIDialogKeyboardTouch.cpp
new file mode 100644
index 0000000..e9511ce
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogKeyboardTouch.cpp
@@ -0,0 +1,86 @@
+/*
+ * 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 "GUIDialogKeyboardTouch.h"
+#if defined(TARGET_DARWIN_EMBEDDED)
+#include "platform/darwin/ios-common/DarwinEmbedKeyboard.h"
+#endif
+
+CGUIDialogKeyboardTouch::CGUIDialogKeyboardTouch()
+: CGUIDialog(WINDOW_DIALOG_KEYBOARD_TOUCH, "")
+, CGUIKeyboard()
+, CThread("keyboard")
+, m_pCharCallback(NULL)
+{
+}
+
+bool CGUIDialogKeyboardTouch::ShowAndGetInput(char_callback_t pCallback, const std::string &initialString, std::string &typedString, const std::string &heading, bool bHiddenInput)
+{
+#if defined(TARGET_DARWIN_EMBEDDED)
+ m_keyboard.reset(new CDarwinEmbedKeyboard());
+#endif
+
+ if (!m_keyboard)
+ return false;
+
+ m_pCharCallback = pCallback;
+ m_initialString = initialString;
+ m_typedString = typedString;
+ m_heading = heading;
+ m_bHiddenInput = bHiddenInput;
+
+ m_confirmed = false;
+ Initialize();
+ Open();
+
+ m_keyboard.reset();
+
+ if (m_confirmed)
+ {
+ typedString = m_typedString;
+ return true;
+ }
+
+ return false;
+}
+
+bool CGUIDialogKeyboardTouch::SetTextToKeyboard(const std::string &text, bool closeKeyboard)
+{
+ if (m_keyboard)
+ return m_keyboard->SetTextToKeyboard(text, closeKeyboard);
+
+ return false;
+}
+
+void CGUIDialogKeyboardTouch::Cancel()
+{
+ if (m_keyboard)
+ m_keyboard->Cancel();
+}
+
+int CGUIDialogKeyboardTouch::GetWindowId() const
+{
+ return GetID();
+}
+
+void CGUIDialogKeyboardTouch::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+ m_windowLoaded = true;
+ m_active = true;
+ Create();
+}
+
+void CGUIDialogKeyboardTouch::Process()
+{
+ if (m_keyboard)
+ {
+ m_confirmed = m_keyboard->ShowAndGetInput(m_pCharCallback, m_initialString, m_typedString, m_heading, m_bHiddenInput);
+ }
+ Close();
+} \ No newline at end of file
diff --git a/xbmc/dialogs/GUIDialogKeyboardTouch.h b/xbmc/dialogs/GUIDialogKeyboardTouch.h
new file mode 100644
index 0000000..e7ad433
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogKeyboardTouch.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "guilib/GUIKeyboard.h"
+
+#include <atomic>
+#include <memory>
+
+class CGUIDialogKeyboardTouch : public CGUIDialog, public CGUIKeyboard, public CThread
+{
+public:
+ CGUIDialogKeyboardTouch();
+ bool ShowAndGetInput(char_callback_t pCallback, const std::string &initialString, std::string &typedString, const std::string &heading, bool bHiddenInput) override;
+ bool SetTextToKeyboard(const std::string &text, bool closeKeyboard = false) override;
+ void Cancel() override;
+ int GetWindowId() const override;
+
+protected:
+ void OnInitWindow() override;
+ using CGUIControlGroup::Process;
+ void Process() override;
+
+ char_callback_t m_pCharCallback;
+ std::string m_initialString;
+ std::string m_typedString;
+ std::string m_heading;
+ bool m_bHiddenInput;
+
+ std::unique_ptr<CGUIKeyboard> m_keyboard;
+ std::atomic_bool m_active;
+ bool m_confirmed;
+};
diff --git a/xbmc/dialogs/GUIDialogMediaFilter.cpp b/xbmc/dialogs/GUIDialogMediaFilter.cpp
new file mode 100644
index 0000000..4985627
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogMediaFilter.cpp
@@ -0,0 +1,937 @@
+/*
+ * 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 "GUIDialogMediaFilter.h"
+
+#include "DbUrl.h"
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicDbUrl.h"
+#include "playlists/SmartPlayList.h"
+#include "settings/SettingUtils.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoDbUrl.h"
+
+#define CONTROL_HEADING 2
+
+#define CONTROL_OKAY_BUTTON 28
+#define CONTROL_CANCEL_BUTTON 29
+#define CONTROL_CLEAR_BUTTON 30
+
+#define CHECK_ALL -1
+#define CHECK_NO 0
+#define CHECK_YES 1
+#define CHECK_LABEL_ALL 593
+#define CHECK_LABEL_NO 106
+#define CHECK_LABEL_YES 107
+
+static const CGUIDialogMediaFilter::Filter filterList[] = {
+ { "movies", FieldTitle, 556, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "movies", FieldRating, 563, SettingType::Number, "range", "number", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "movies", FieldUserRating, 38018, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ //{ "movies", FieldTime, 180, SettingType::Integer, "range", "time", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "movies", FieldInProgress, 575, SettingType::Integer, "toggle", "", CDatabaseQueryRule::OPERATOR_FALSE },
+ { "movies", FieldYear, 562, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "movies", FieldTag, 20459, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "movies", FieldGenre, 515, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "movies", FieldActor, 20337, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "movies", FieldDirector, 20339, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "movies", FieldStudio, 572, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+
+ { "tvshows", FieldTitle, 556, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ //{ "tvshows", FieldTvShowStatus, 126, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "tvshows", FieldRating, 563, SettingType::Number, "range", "number", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "tvshows", FieldUserRating, 38018, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "tvshows", FieldInProgress, 575, SettingType::Integer, "toggle", "", CDatabaseQueryRule::OPERATOR_FALSE },
+ { "tvshows", FieldYear, 562, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "tvshows", FieldTag, 20459, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "tvshows", FieldGenre, 515, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "tvshows", FieldActor, 20337, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "tvshows", FieldDirector, 20339, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "tvshows", FieldStudio, 572, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+
+ { "episodes", FieldTitle, 556, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "episodes", FieldRating, 563, SettingType::Number, "range", "number", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "episodes", FieldUserRating, 38018, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "episodes", FieldAirDate, 20416, SettingType::Integer, "range", "date", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "episodes", FieldInProgress, 575, SettingType::Integer, "toggle", "", CDatabaseQueryRule::OPERATOR_FALSE },
+ { "episodes", FieldActor, 20337, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "episodes", FieldDirector, 20339, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+
+ { "musicvideos", FieldTitle, 556, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "musicvideos", FieldRating, 563, SettingType::Number, "range", "number", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "musicvideos", FieldUserRating, 38018, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "musicvideos", FieldArtist, 557, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "musicvideos", FieldAlbum, 558, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ //{ "musicvideos", FieldTime, 180, SettingType::Integer, "range", "time", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "musicvideos", FieldYear, 562, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "musicvideos", FieldTag, 20459, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "musicvideos", FieldGenre, 515, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "musicvideos", FieldDirector, 20339, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "musicvideos", FieldStudio, 572, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+
+ { "artists", FieldArtist, 557, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "artists", FieldSource, 39030, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "artists", FieldGenre, 515, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "artists", FieldMoods, 175, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "artists", FieldStyles, 176, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "artists", FieldInstruments, 21892, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "artists", FieldArtistType, 564, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "artists", FieldGender, 39025, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "artists", FieldDisambiguation, 39026, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "artists", FieldBiography, 21887, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "artists", FieldBorn, 21893, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "artists", FieldBandFormed, 21894, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "artists", FieldDisbanded, 21896, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "artists", FieldDied, 21897, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+
+ { "albums", FieldAlbum, 556, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+// { "albums", FieldArtist, 557, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "albums", FieldDiscTitle, 38076, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "albums", FieldAlbumArtist, 566, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "albums", FieldSource, 39030, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "albums", FieldRating, 563, SettingType::Number, "range", "number", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "albums", FieldUserRating, 38018, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "albums", FieldAlbumType, 564, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "albums", FieldYear, 562, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "albums", FieldGenre, 515, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "albums", FieldMusicLabel, 21899, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "albums", FieldCompilation, 204, SettingType::Boolean, "toggle", "", CDatabaseQueryRule::OPERATOR_FALSE },
+ { "albums", FieldIsBoxset, 38074, SettingType::Boolean, "toggle", "", CDatabaseQueryRule::OPERATOR_FALSE },
+ { "albums", FieldOrigYear, 38078, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+
+ { "songs", FieldTitle, 556, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "songs", FieldAlbum, 558, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "songs", FieldDiscTitle, 38076, SettingType::String, "edit", "string", CDatabaseQueryRule::OPERATOR_CONTAINS },
+ { "songs", FieldArtist, 557, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "songs", FieldTime, 180, SettingType::Integer, "range", "time", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "songs", FieldRating, 563, SettingType::Number, "range", "number", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "songs", FieldUserRating, 38018, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "songs", FieldYear, 562, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "songs", FieldGenre, 515, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS },
+ { "songs", FieldPlaycount, 567, SettingType::Integer, "range", "integer", CDatabaseQueryRule::OPERATOR_BETWEEN },
+ { "songs", FieldSource, 39030, SettingType::List, "list", "string", CDatabaseQueryRule::OPERATOR_EQUALS }
+};
+
+CGUIDialogMediaFilter::CGUIDialogMediaFilter()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_MEDIA_FILTER, "DialogSettings.xml"),
+ m_dbUrl(NULL),
+ m_filter(NULL)
+{ }
+
+CGUIDialogMediaFilter::~CGUIDialogMediaFilter()
+{
+ Reset();
+}
+
+bool CGUIDialogMediaFilter::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ if (message.GetSenderId()== CONTROL_CLEAR_BUTTON)
+ {
+ m_filter->Reset();
+ m_filter->SetType(m_mediaType);
+
+ for (auto& filter : m_filters)
+ {
+ filter.second.rule = nullptr;
+ filter.second.setting->Reset();
+ }
+
+ TriggerFilter();
+ return true;
+ }
+ break;
+ }
+
+ case GUI_MSG_REFRESH_LIST:
+ {
+ TriggerFilter();
+ UpdateControls();
+ break;
+ }
+
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ Reset();
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return CGUIDialogSettingsManualBase::OnMessage(message);
+}
+
+void CGUIDialogMediaFilter::ShowAndEditMediaFilter(const std::string &path, CSmartPlaylist &filter)
+{
+ CGUIDialogMediaFilter *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogMediaFilter>(WINDOW_DIALOG_MEDIA_FILTER);
+ if (dialog == NULL)
+ return;
+
+ // initialize and show the dialog
+ dialog->Initialize();
+ dialog->m_filter = &filter;
+
+ // must be called after setting the filter/smartplaylist
+ if (!dialog->SetPath(path))
+ return;
+
+ dialog->Open();
+}
+
+void CGUIDialogMediaFilter::OnWindowLoaded()
+{
+ CGUIDialogSettingsManualBase::OnWindowLoaded();
+
+ // we don't need the cancel button so let's hide it
+ SET_CONTROL_HIDDEN(CONTROL_CANCEL_BUTTON);
+}
+
+void CGUIDialogMediaFilter::OnInitWindow()
+{
+ CGUIDialogSettingsManualBase::OnInitWindow();
+
+ UpdateControls();
+}
+
+void CGUIDialogMediaFilter::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ std::map<std::string, Filter>::iterator it = m_filters.find(setting->GetId());
+ if (it == m_filters.end())
+ return;
+
+ bool remove = false;
+ Filter& filter = it->second;
+
+ if (filter.controlType == "edit")
+ {
+ std::string value = setting->ToString();
+ if (!value.empty())
+ {
+ if (filter.rule == NULL)
+ filter.rule = AddRule(filter.field, filter.ruleOperator);
+ filter.rule->m_parameter.clear();
+ filter.rule->m_parameter.push_back(value);
+ }
+ else
+ remove = true;
+ }
+ else if (filter.controlType == "toggle")
+ {
+ int choice = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ if (choice > CHECK_ALL)
+ {
+ CDatabaseQueryRule::SEARCH_OPERATOR ruleOperator = choice == CHECK_YES ? CDatabaseQueryRule::OPERATOR_TRUE : CDatabaseQueryRule::OPERATOR_FALSE;
+ if (filter.rule == NULL)
+ filter.rule = AddRule(filter.field, ruleOperator);
+ else
+ filter.rule->m_operator = ruleOperator;
+ }
+ else
+ remove = true;
+ }
+ else if (filter.controlType == "list")
+ {
+ std::vector<CVariant> values = CSettingUtils::GetList(std::static_pointer_cast<const CSettingList>(setting));
+ if (!values.empty())
+ {
+ if (filter.rule == NULL)
+ filter.rule = AddRule(filter.field, filter.ruleOperator);
+
+ filter.rule->m_parameter.clear();
+ for (const auto& itValue : values)
+ filter.rule->m_parameter.push_back(itValue.asString());
+ }
+ else
+ remove = true;
+ }
+ else if (filter.controlType == "range")
+ {
+ const std::shared_ptr<const CSettingList> settingList = std::static_pointer_cast<const CSettingList>(setting);
+ std::vector<CVariant> values = CSettingUtils::GetList(settingList);
+ if (values.size() != 2)
+ return;
+
+ std::string strValueLower, strValueUpper;
+
+ SettingConstPtr definition = settingList->GetDefinition();
+ if (definition->GetType() == SettingType::Integer)
+ {
+ const std::shared_ptr<const CSettingInt> definitionInt = std::static_pointer_cast<const CSettingInt>(definition);
+ int valueLower = static_cast<int>(values.at(0).asInteger());
+ int valueUpper = static_cast<int>(values.at(1).asInteger());
+
+ if (valueLower > definitionInt->GetMinimum() ||
+ valueUpper < definitionInt->GetMaximum())
+ {
+ if (filter.controlFormat == "date")
+ {
+ strValueLower = CDateTime(static_cast<time_t>(valueLower)).GetAsDBDate();
+ strValueUpper = CDateTime(static_cast<time_t>(valueUpper)).GetAsDBDate();
+ }
+ else
+ {
+ strValueLower = values.at(0).asString();
+ strValueUpper = values.at(1).asString();
+ }
+ }
+ }
+ else if (definition->GetType() == SettingType::Number)
+ {
+ const std::shared_ptr<const CSettingNumber> definitionNumber = std::static_pointer_cast<const CSettingNumber>(definition);
+ float valueLower = values.at(0).asFloat();
+ float valueUpper = values.at(1).asFloat();
+
+ if (static_cast<double>(valueLower) > definitionNumber->GetMinimum() ||
+ static_cast<double>(valueUpper) < definitionNumber->GetMaximum())
+ {
+ strValueLower = values.at(0).asString();
+ strValueUpper = values.at(1).asString();
+ }
+ }
+ else
+ return;
+
+ if (!strValueLower.empty() && !strValueUpper.empty())
+ {
+ // prepare the filter rule
+ if (filter.rule == NULL)
+ filter.rule = AddRule(filter.field, filter.ruleOperator);
+ filter.rule->m_parameter.clear();
+
+ filter.rule->m_parameter.push_back(strValueLower);
+ filter.rule->m_parameter.push_back(strValueUpper);
+ }
+ else
+ remove = true;
+ }
+ else
+ return;
+
+ // we need to remove the existing rule for the title
+ if (remove && filter.rule != NULL)
+ {
+ DeleteRule(filter.field);
+ filter.rule = NULL;
+ }
+
+ CGUIMessage msg(GUI_MSG_REFRESH_LIST, GetID(), 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, WINDOW_DIALOG_MEDIA_FILTER);
+}
+
+void CGUIDialogMediaFilter::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ // set the heading label based on the media type
+ uint32_t localizedMediaId = 0;
+ if (m_mediaType == "movies")
+ localizedMediaId = 20342;
+ else if (m_mediaType == "tvshows")
+ localizedMediaId = 20343;
+ else if (m_mediaType == "episodes")
+ localizedMediaId = 20360;
+ else if (m_mediaType == "musicvideos")
+ localizedMediaId = 20389;
+ else if (m_mediaType == "artists")
+ localizedMediaId = 133;
+ else if (m_mediaType == "albums")
+ localizedMediaId = 132;
+ else if (m_mediaType == "songs")
+ localizedMediaId = 134;
+
+ // set the heading
+ SET_CONTROL_LABEL(CONTROL_HEADING, StringUtils::Format(g_localizeStrings.Get(1275),
+ g_localizeStrings.Get(localizedMediaId)));
+
+ SET_CONTROL_LABEL(CONTROL_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_CLEAR_BUTTON, 192);
+}
+
+void CGUIDialogMediaFilter::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ if (m_filter == NULL)
+ return;
+
+ Reset(true);
+
+ int handledRules = 0;
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("filter", -1);
+ if (category == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogMediaFilter: unable to setup filters");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogMediaFilter: unable to setup filters");
+ return;
+ }
+
+ for (const Filter& f : filterList)
+ {
+ if (f.mediaType != m_mediaType)
+ continue;
+
+ Filter filter = f;
+
+ // check the smartplaylist if it contains a matching rule
+ for (auto& rule : m_filter->m_ruleCombination.m_rules)
+ {
+ if (rule->m_field == filter.field)
+ {
+ filter.rule = static_cast<CSmartPlaylistRule*>(rule.get());
+ handledRules++;
+ break;
+ }
+ }
+
+ std::string settingId = StringUtils::Format("filter.{}.{}", filter.mediaType, filter.field);
+ if (filter.controlType == "edit")
+ {
+ CVariant data;
+ if (filter.rule != NULL && filter.rule->m_parameter.size() == 1)
+ data = filter.rule->m_parameter.at(0);
+
+ if (filter.settingType == SettingType::String)
+ filter.setting = AddEdit(group, settingId, filter.label, SettingLevel::Basic, data.asString(), true, false, filter.label, true);
+ else if (filter.settingType == SettingType::Integer)
+ filter.setting = AddEdit(group, settingId, filter.label, SettingLevel::Basic, static_cast<int>(data.asInteger()), 0, 1, 0, false, static_cast<int>(filter.label), true);
+ else if (filter.settingType == SettingType::Number)
+ filter.setting = AddEdit(group, settingId, filter.label, SettingLevel::Basic, data.asFloat(), 0.0f, 1.0f, 0.0f, false, filter.label, true);
+ }
+ else if (filter.controlType == "toggle")
+ {
+ int value = CHECK_ALL;
+ if (filter.rule != NULL)
+ value = filter.rule->m_operator == CDatabaseQueryRule::OPERATOR_TRUE ? CHECK_YES : CHECK_NO;
+
+ TranslatableIntegerSettingOptions entries;
+ entries.push_back(TranslatableIntegerSettingOption(CHECK_LABEL_ALL, CHECK_ALL));
+ entries.push_back(TranslatableIntegerSettingOption(CHECK_LABEL_NO, CHECK_NO));
+ entries.push_back(TranslatableIntegerSettingOption(CHECK_LABEL_YES, CHECK_YES));
+
+ filter.setting = AddSpinner(group, settingId, filter.label, SettingLevel::Basic, value, entries, true);
+ }
+ else if (filter.controlType == "list")
+ {
+ std::vector<std::string> values;
+ if (filter.rule != NULL && !filter.rule->m_parameter.empty())
+ {
+ values = StringUtils::Split(filter.rule->GetParameter(), DATABASEQUERY_RULE_VALUE_SEPARATOR);
+ if (values.size() == 1 && values.at(0).empty())
+ values.erase(values.begin());
+ }
+
+ filter.setting = AddList(group, settingId, filter.label, SettingLevel::Basic, values, GetStringListOptions, filter.label);
+ }
+ else if (filter.controlType == "range")
+ {
+ CVariant valueLower, valueUpper;
+ if (filter.rule != NULL)
+ {
+ if (filter.rule->m_parameter.size() == 2)
+ {
+ valueLower = filter.rule->m_parameter.at(0);
+ valueUpper = filter.rule->m_parameter.at(1);
+ }
+ else
+ {
+ DeleteRule(filter.field);
+ filter.rule = NULL;
+ }
+ }
+
+ if (filter.settingType == SettingType::Integer)
+ {
+ int min = 0;
+ int interval = 0;
+ int max = 0;
+ GetRange(filter, min, interval, max);
+
+ // don't create the filter if there's no real range
+ if (min == max)
+ continue;
+
+ int iValueLower = valueLower.isNull() ? min : static_cast<int>(valueLower.asInteger());
+ int iValueUpper = valueUpper.isNull() ? max : static_cast<int>(valueUpper.asInteger());
+
+ if (filter.controlFormat == "integer")
+ filter.setting = AddRange(group, settingId, filter.label, SettingLevel::Basic, iValueLower, iValueUpper, min, interval, max, -1, 21469, true);
+ else if (filter.controlFormat == "percentage")
+ filter.setting = AddPercentageRange(group, settingId, filter.label, SettingLevel::Basic, iValueLower, iValueUpper, -1, 1, 21469, true);
+ else if (filter.controlFormat == "date")
+ filter.setting = AddDateRange(group, settingId, filter.label, SettingLevel::Basic, iValueLower, iValueUpper, min, interval, max, -1, 21469, true);
+ else if (filter.controlFormat == "time")
+ filter.setting = AddTimeRange(group, settingId, filter.label, SettingLevel::Basic, iValueLower, iValueUpper, min, interval, max, -1, 21469, true);
+ }
+ else if (filter.settingType == SettingType::Number)
+ {
+ float min = 0;
+ float interval = 0;
+ float max = 0;
+ GetRange(filter, min, interval, max);
+
+ // don't create the filter if there's no real range
+ if (min == max)
+ continue;
+
+ float fValueLower = valueLower.isNull() ? min : valueLower.asFloat();
+ float fValueUpper = valueUpper.isNull() ? max : valueUpper.asFloat();
+
+ filter.setting = AddRange(group, settingId, filter.label, SettingLevel::Basic, fValueLower, fValueUpper, min, interval, max, -1, 21469, true);
+ }
+ }
+ else
+ {
+ if (filter.rule != NULL)
+ handledRules--;
+
+ CLog::Log(LOGWARNING,
+ "CGUIDialogMediaFilter: filter {} of media type {} with unknown control type '{}'",
+ filter.field, filter.mediaType, filter.controlType);
+ continue;
+ }
+
+ if (filter.setting == NULL)
+ {
+ if (filter.rule != NULL)
+ handledRules--;
+
+ CLog::Log(LOGWARNING,
+ "CGUIDialogMediaFilter: failed to create filter {} of media type {} with control "
+ "type '{}'",
+ filter.field, filter.mediaType, filter.controlType);
+ continue;
+ }
+
+ m_filters.insert(make_pair(settingId, filter));
+ }
+
+ // make sure that no change in capacity size is needed when adding new rules
+ // which would copy around the rules and our pointers in the Filter struct
+ // wouldn't work anymore
+ m_filter->m_ruleCombination.m_rules.reserve(m_filters.size() + (m_filter->m_ruleCombination.m_rules.size() - handledRules));
+}
+
+bool CGUIDialogMediaFilter::SetPath(const std::string &path)
+{
+ if (path.empty() || m_filter == NULL)
+ {
+ CLog::Log(LOGWARNING, "CGUIDialogMediaFilter::SetPath({}): invalid path or filter", path);
+ return false;
+ }
+
+ delete m_dbUrl;
+ bool video = false;
+ if (path.find("videodb://") == 0)
+ {
+ m_dbUrl = new CVideoDbUrl();
+ video = true;
+ }
+ else if (path.find("musicdb://") == 0)
+ m_dbUrl = new CMusicDbUrl();
+ else
+ {
+ CLog::Log(
+ LOGWARNING,
+ "CGUIDialogMediaFilter::SetPath({}): invalid path (neither videodb:// nor musicdb://)",
+ path);
+ return false;
+ }
+
+ if (!m_dbUrl->FromString(path) ||
+ (video && m_dbUrl->GetType() != "movies" && m_dbUrl->GetType() != "tvshows" && m_dbUrl->GetType() != "episodes" && m_dbUrl->GetType() != "musicvideos") ||
+ (!video && m_dbUrl->GetType() != "artists" && m_dbUrl->GetType() != "albums" && m_dbUrl->GetType() != "songs"))
+ {
+ CLog::Log(LOGWARNING, "CGUIDialogMediaFilter::SetPath({}): invalid media type", path);
+ return false;
+ }
+
+ // remove "filter" option
+ if (m_dbUrl->HasOption("filter"))
+ m_dbUrl->RemoveOption("filter");
+
+ if (video)
+ m_mediaType = ((CVideoDbUrl*)m_dbUrl)->GetItemType();
+ else
+ m_mediaType = m_dbUrl->GetType();
+
+ m_filter->SetType(m_mediaType);
+ return true;
+}
+
+void CGUIDialogMediaFilter::UpdateControls()
+{
+ for (const auto& itFilter : m_filters)
+ {
+ if (itFilter.second.controlType != "list")
+ continue;
+
+ std::vector<std::string> items;
+ int size = GetItems(itFilter.second, items, true);
+
+ std::string label = g_localizeStrings.Get(itFilter.second.label);
+ BaseSettingControlPtr control = GetSettingControl(itFilter.second.setting->GetId());
+ if (control == NULL)
+ continue;
+
+ if (size <= 0 ||
+ (size == 1 && itFilter.second.field != FieldSet && itFilter.second.field != FieldTag))
+ CONTROL_DISABLE(control->GetID());
+ else
+ {
+ CONTROL_ENABLE(control->GetID());
+ label = StringUtils::Format(g_localizeStrings.Get(21470), label, size);
+ }
+ SET_CONTROL_LABEL(control->GetID(), label);
+ }
+}
+
+void CGUIDialogMediaFilter::TriggerFilter() const
+{
+ if (m_filter == NULL)
+ return;
+
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, GetID(), 0, GUI_MSG_FILTER_ITEMS, 10); // 10 for advanced
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+}
+
+void CGUIDialogMediaFilter::Reset(bool filtersOnly /* = false */)
+{
+ if (!filtersOnly)
+ {
+ delete m_dbUrl;
+ m_dbUrl = NULL;
+ }
+
+ m_filters.clear();
+}
+
+int CGUIDialogMediaFilter::GetItems(const Filter &filter, std::vector<std::string> &items, bool countOnly /* = false */)
+{
+ CFileItemList selectItems;
+
+ // remove the rule for the field of the filter we want to retrieve items for
+ CSmartPlaylist tmpFilter = *m_filter;
+ for (CDatabaseQueryRules::iterator rule = tmpFilter.m_ruleCombination.m_rules.begin();
+ rule != tmpFilter.m_ruleCombination.m_rules.end(); ++rule)
+ {
+ if ((*rule)->m_field == filter.field)
+ {
+ tmpFilter.m_ruleCombination.m_rules.erase(rule);
+ break;
+ }
+ }
+
+ if (m_mediaType == "movies" || m_mediaType == "tvshows" || m_mediaType == "episodes" || m_mediaType == "musicvideos")
+ {
+ CVideoDatabase videodb;
+ if (!videodb.Open())
+ return -1;
+
+ std::set<std::string> playlists;
+ CDatabase::Filter dbfilter;
+ dbfilter.where = tmpFilter.GetWhereClause(videodb, playlists);
+
+ VideoDbContentType type = VideoDbContentType::MOVIES;
+ if (m_mediaType == "tvshows")
+ type = VideoDbContentType::TVSHOWS;
+ else if (m_mediaType == "episodes")
+ type = VideoDbContentType::EPISODES;
+ else if (m_mediaType == "musicvideos")
+ type = VideoDbContentType::MUSICVIDEOS;
+
+ if (filter.field == FieldGenre)
+ videodb.GetGenresNav(m_dbUrl->ToString(), selectItems, type, dbfilter, countOnly);
+ else if (filter.field == FieldActor || filter.field == FieldArtist)
+ videodb.GetActorsNav(m_dbUrl->ToString(), selectItems, type, dbfilter, countOnly);
+ else if (filter.field == FieldDirector)
+ videodb.GetDirectorsNav(m_dbUrl->ToString(), selectItems, type, dbfilter, countOnly);
+ else if (filter.field == FieldStudio)
+ videodb.GetStudiosNav(m_dbUrl->ToString(), selectItems, type, dbfilter, countOnly);
+ else if (filter.field == FieldAlbum)
+ videodb.GetMusicVideoAlbumsNav(m_dbUrl->ToString(), selectItems, -1, dbfilter, countOnly);
+ else if (filter.field == FieldTag)
+ videodb.GetTagsNav(m_dbUrl->ToString(), selectItems, type, dbfilter, countOnly);
+ }
+ else if (m_mediaType == "artists" || m_mediaType == "albums" || m_mediaType == "songs")
+ {
+ CMusicDatabase musicdb;
+ if (!musicdb.Open())
+ return -1;
+
+ std::set<std::string> playlists;
+ CDatabase::Filter dbfilter;
+ dbfilter.where = tmpFilter.GetWhereClause(musicdb, playlists);
+
+ if (filter.field == FieldGenre)
+ musicdb.GetGenresNav(m_dbUrl->ToString(), selectItems, dbfilter, countOnly);
+ else if (filter.field == FieldArtist || filter.field == FieldAlbumArtist)
+ musicdb.GetArtistsNav(m_dbUrl->ToString(), selectItems, m_mediaType == "albums", -1, -1, -1, dbfilter, SortDescription(), countOnly);
+ else if (filter.field == FieldAlbum)
+ musicdb.GetAlbumsNav(m_dbUrl->ToString(), selectItems, -1, -1, dbfilter, SortDescription(), countOnly);
+ else if (filter.field == FieldAlbumType)
+ musicdb.GetAlbumTypesNav(m_dbUrl->ToString(), selectItems, dbfilter, countOnly);
+ else if (filter.field == FieldMusicLabel)
+ musicdb.GetMusicLabelsNav(m_dbUrl->ToString(), selectItems, dbfilter, countOnly);
+ if (filter.field == FieldSource)
+ musicdb.GetSourcesNav(m_dbUrl->ToString(), selectItems, dbfilter, countOnly);
+ }
+
+ int size = selectItems.Size();
+ if (size <= 0)
+ return 0;
+
+ if (countOnly)
+ {
+ if (size == 1 && selectItems.Get(0)->HasProperty("total"))
+ return (int)selectItems.Get(0)->GetProperty("total").asInteger();
+ return 0;
+ }
+
+ // sort the items
+ selectItems.Sort(SortByLabel, SortOrderAscending);
+
+ for (int index = 0; index < size; ++index)
+ items.push_back(selectItems.Get(index)->GetLabel());
+
+ return items.size();
+}
+
+CSmartPlaylistRule* CGUIDialogMediaFilter::AddRule(Field field, CDatabaseQueryRule::SEARCH_OPERATOR ruleOperator /* = CDatabaseQueryRule::OPERATOR_CONTAINS */)
+{
+ CSmartPlaylistRule rule;
+ rule.m_field = field;
+ rule.m_operator = ruleOperator;
+
+ m_filter->m_ruleCombination.AddRule(rule);
+ return (CSmartPlaylistRule *)m_filter->m_ruleCombination.m_rules.back().get();
+}
+
+void CGUIDialogMediaFilter::DeleteRule(Field field)
+{
+ for (CDatabaseQueryRules::iterator rule = m_filter->m_ruleCombination.m_rules.begin();
+ rule != m_filter->m_ruleCombination.m_rules.end(); ++rule)
+ {
+ if ((*rule)->m_field == field)
+ {
+ m_filter->m_ruleCombination.m_rules.erase(rule);
+ break;
+ }
+ }
+}
+
+void CGUIDialogMediaFilter::GetStringListOptions(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ if (setting == NULL || data == NULL)
+ return;
+
+ CGUIDialogMediaFilter *mediaFilter = static_cast<CGUIDialogMediaFilter*>(data);
+
+ std::map<std::string, Filter>::const_iterator itFilter = mediaFilter->m_filters.find(setting->GetId());
+ if (itFilter == mediaFilter->m_filters.end())
+ return;
+
+ std::vector<std::string> items;
+ if (mediaFilter->GetItems(itFilter->second, items, false) <= 0)
+ return;
+
+ for (const auto& item : items)
+ list.emplace_back(item, item);
+}
+
+void CGUIDialogMediaFilter::GetRange(const Filter &filter, int &min, int &interval, int &max)
+{
+ if (filter.field == FieldUserRating &&
+ (m_mediaType == "movies" || m_mediaType == "tvshows" || m_mediaType == "episodes"|| m_mediaType == "musicvideos" || m_mediaType == "albums" || m_mediaType == "songs"))
+ {
+ min = 0;
+ interval = 1;
+ max = 10;
+ }
+ else if (filter.field == FieldYear)
+ {
+ min = 0;
+ interval = 1;
+ max = 0;
+
+ if (m_mediaType == "movies" || m_mediaType == "tvshows" || m_mediaType == "musicvideos")
+ {
+ std::string table;
+ std::string year;
+ if (m_mediaType == "movies")
+ {
+ table = "movie_view";
+ year = DatabaseUtils::GetField(FieldYear, MediaTypeMovie, DatabaseQueryPartWhere);
+ }
+ else if (m_mediaType == "tvshows")
+ {
+ table = "tvshow_view";
+ year = StringUtils::Format(
+ "strftime(\"%%Y\", {})",
+ DatabaseUtils::GetField(FieldYear, MediaTypeTvShow, DatabaseQueryPartWhere));
+ }
+ else if (m_mediaType == "musicvideos")
+ {
+ table = "musicvideo_view";
+ year = DatabaseUtils::GetField(FieldYear, MediaTypeMusicVideo, DatabaseQueryPartWhere);
+ }
+
+ CDatabase::Filter filter;
+ filter.where = year + " > 0";
+ GetMinMax(table, year, min, max, filter);
+ }
+ else if (m_mediaType == "albums" || m_mediaType == "songs")
+ {
+ std::string table;
+ if (m_mediaType == "albums")
+ table = "albumview";
+ else if (m_mediaType == "songs")
+ table = "songview";
+ else
+ return;
+
+ CDatabase::Filter filter;
+ filter.where = DatabaseUtils::GetField(FieldYear, CMediaTypes::FromString(m_mediaType), DatabaseQueryPartWhere) + " > 0";
+ GetMinMax(table, DatabaseUtils::GetField(FieldYear, CMediaTypes::FromString(m_mediaType), DatabaseQueryPartSelect), min, max, filter);
+ }
+ }
+ else if (filter.field == FieldAirDate)
+ {
+ min = 0;
+ interval = 1;
+ max = 0;
+
+ if (m_mediaType == "episodes")
+ {
+ std::string field = StringUtils::Format("CAST(strftime(\"%%s\", c{:02}) AS INTEGER)",
+ VIDEODB_ID_EPISODE_AIRED);
+
+ GetMinMax("episode_view", field, min, max);
+ interval = 60 * 60 * 24 * 7; // 1 week
+ }
+ }
+ else if (filter.field == FieldTime)
+ {
+ min = 0;
+ interval = 10;
+ max = 0;
+
+ if (m_mediaType == "songs")
+ GetMinMax("songview", "iDuration", min, max);
+ }
+ else if (filter.field == FieldPlaycount)
+ {
+ min = 0;
+ interval = 1;
+ max = 0;
+
+ if (m_mediaType == "songs")
+ GetMinMax("songview", "iTimesPlayed", min, max);
+ }
+}
+
+void CGUIDialogMediaFilter::GetRange(const Filter &filter, float &min, float &interval, float &max)
+{
+ if (filter.field == FieldRating &&
+ (m_mediaType == "movies" || m_mediaType == "tvshows" || m_mediaType == "episodes" || m_mediaType == "musicvideos" || m_mediaType == "albums" || m_mediaType == "songs"))
+ {
+ min = 0.0f;
+ interval = 0.1f;
+ max = 10.0f;
+ }
+}
+
+bool CGUIDialogMediaFilter::GetMinMax(const std::string &table, const std::string &field, int &min, int &max, const CDatabase::Filter &filter /* = CDatabase::Filter() */)
+{
+ if (table.empty() || field.empty())
+ return false;
+
+ CDatabase *db = NULL;
+ CDbUrl *dbUrl = NULL;
+ if (m_mediaType == "movies" || m_mediaType == "tvshows" || m_mediaType == "episodes" || m_mediaType == "musicvideos")
+ {
+ CVideoDatabase *videodb = new CVideoDatabase();
+ if (!videodb->Open())
+ {
+ delete videodb;
+ return false;
+ }
+
+ db = videodb;
+ dbUrl = new CVideoDbUrl();
+ }
+ else if (m_mediaType == "artists" || m_mediaType == "albums" || m_mediaType == "songs")
+ {
+ CMusicDatabase *musicdb = new CMusicDatabase();
+ if (!musicdb->Open())
+ {
+ delete musicdb;
+ return false;
+ }
+
+ db = musicdb;
+ dbUrl = new CMusicDbUrl();
+ }
+
+ if (db == NULL || !db->IsOpen() || dbUrl == NULL)
+ {
+ delete db;
+ delete dbUrl;
+ return false;
+ }
+
+ CDatabase::Filter extFilter = filter;
+ std::string strSQLExtra;
+ if (!db->BuildSQL(m_dbUrl->ToString(), strSQLExtra, extFilter, strSQLExtra, *dbUrl))
+ {
+ delete db;
+ delete dbUrl;
+ return false;
+ }
+
+ std::string strSQL = "SELECT %s FROM %s ";
+
+ min = static_cast<int>(strtol(db->GetSingleValue(db->PrepareSQL(strSQL, ("MIN(" + field + ")").c_str(), table.c_str()) + strSQLExtra).c_str(), NULL, 0));
+ max = static_cast<int>(strtol(db->GetSingleValue(db->PrepareSQL(strSQL, ("MAX(" + field + ")").c_str(), table.c_str()) + strSQLExtra).c_str(), NULL, 0));
+
+ db->Close();
+ delete db;
+ delete dbUrl;
+
+ return true;
+}
diff --git a/xbmc/dialogs/GUIDialogMediaFilter.h b/xbmc/dialogs/GUIDialogMediaFilter.h
new file mode 100644
index 0000000..893f1c2
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogMediaFilter.h
@@ -0,0 +1,92 @@
+/*
+ * 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 "dbwrappers/Database.h"
+#include "dbwrappers/DatabaseQuery.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+#include "settings/lib/SettingType.h"
+#include "utils/DatabaseUtils.h"
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CDbUrl;
+class CSetting;
+class CSmartPlaylist;
+class CSmartPlaylistRule;
+struct StringSettingOption;
+
+class CGUIDialogMediaFilter : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogMediaFilter();
+ ~CGUIDialogMediaFilter() override;
+
+ // specializations of CGUIControl
+ bool OnMessage(CGUIMessage &message) override;
+
+ static void ShowAndEditMediaFilter(const std::string &path, CSmartPlaylist &filter);
+
+ struct Filter
+ {
+ std::string mediaType;
+ Field field;
+ uint32_t label;
+ SettingType settingType;
+ std::string controlType;
+ std::string controlFormat;
+ CDatabaseQueryRule::SEARCH_OPERATOR ruleOperator;
+ std::shared_ptr<CSetting> setting = nullptr;
+ CSmartPlaylistRule* rule = nullptr;
+ void* data = nullptr;
+ };
+
+protected:
+ // specializations of CGUIWindow
+ void OnWindowLoaded() override;
+ void OnInitWindow() override;
+
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override { return true; }
+ std::chrono::milliseconds GetDelayMs() const override { return std::chrono::milliseconds(500); }
+
+ // specialization of CGUIDialogSettingsManualBase
+ void SetupView() override;
+ void InitializeSettings() override;
+
+ bool SetPath(const std::string &path);
+ void UpdateControls();
+ void TriggerFilter() const;
+ void Reset(bool filtersOnly = false);
+
+ int GetItems(const Filter &filter, std::vector<std::string> &items, bool countOnly = false);
+ void GetRange(const Filter &filter, int &min, int &interval, int &max);
+ void GetRange(const Filter &filter, float &min, float &interval, float &max);
+ bool GetMinMax(const std::string &table, const std::string &field, int &min, int &max, const CDatabase::Filter &filter = CDatabase::Filter());
+
+ CSmartPlaylistRule* AddRule(Field field, CDatabaseQueryRule::SEARCH_OPERATOR ruleOperator = CDatabaseQueryRule::OPERATOR_CONTAINS);
+ void DeleteRule(Field field);
+
+ static void GetStringListOptions(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+
+ CDbUrl* m_dbUrl;
+ std::string m_mediaType;
+ CSmartPlaylist *m_filter;
+ std::map<std::string, Filter> m_filters;
+};
diff --git a/xbmc/dialogs/GUIDialogMediaSource.cpp b/xbmc/dialogs/GUIDialogMediaSource.cpp
new file mode 100644
index 0000000..13b13e5
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogMediaSource.cpp
@@ -0,0 +1,620 @@
+/*
+ * 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 "GUIDialogMediaSource.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "GUIDialogFileBrowser.h"
+#include "video/windows/GUIWindowVideoBase.h"
+#include "music/windows/GUIWindowMusicBase.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/Key.h"
+#include "Util.h"
+#include "utils/URIUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "filesystem/Directory.h"
+#include "filesystem/PVRDirectory.h"
+#include "GUIDialogYesNo.h"
+#include "FileItem.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "guilib/LocalizeStrings.h"
+#include "PasswordManager.h"
+#include "URL.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+
+#if defined(TARGET_ANDROID)
+#include "utils/FileUtils.h"
+
+#include "platform/android/activity/XBMCApp.h"
+#endif
+
+#ifdef TARGET_WINDOWS_STORE
+#include "platform/win10/filesystem/WinLibraryDirectory.h"
+#endif
+
+using namespace XFILE;
+
+#define CONTROL_HEADING 2
+#define CONTROL_PATH 10
+#define CONTROL_PATH_BROWSE 11
+#define CONTROL_NAME 12
+#define CONTROL_PATH_ADD 13
+#define CONTROL_PATH_REMOVE 14
+#define CONTROL_OK 18
+#define CONTROL_CANCEL 19
+#define CONTROL_CONTENT 20
+
+CGUIDialogMediaSource::CGUIDialogMediaSource(void)
+ : CGUIDialog(WINDOW_DIALOG_MEDIA_SOURCE, "DialogMediaSource.xml")
+{
+ m_paths = new CFileItemList;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogMediaSource::~CGUIDialogMediaSource()
+{
+ delete m_paths;
+}
+
+bool CGUIDialogMediaSource::OnBack(int actionID)
+{
+ m_confirmed = false;
+ return CGUIDialog::OnBack(actionID);
+}
+
+bool CGUIDialogMediaSource::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ int iAction = message.GetParam1();
+ if (iControl == CONTROL_PATH && (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK))
+ OnPath(GetSelectedItem());
+ else if (iControl == CONTROL_PATH_BROWSE)
+ OnPathBrowse(GetSelectedItem());
+ else if (iControl == CONTROL_PATH_ADD)
+ OnPathAdd();
+ else if (iControl == CONTROL_PATH_REMOVE)
+ OnPathRemove(GetSelectedItem());
+ else if (iControl == CONTROL_NAME)
+ {
+ OnEditChanged(iControl, m_name);
+ UpdateButtons();
+ }
+ else if (iControl == CONTROL_OK)
+ OnOK();
+ else if (iControl == CONTROL_CANCEL)
+ OnCancel();
+ else
+ break;
+ return true;
+ }
+ break;
+ case GUI_MSG_WINDOW_INIT:
+ {
+ UpdateButtons();
+ }
+ break;
+ case GUI_MSG_SETFOCUS:
+ if (message.GetControlId() == CONTROL_PATH_BROWSE ||
+ message.GetControlId() == CONTROL_PATH_ADD ||
+ message.GetControlId() == CONTROL_PATH_REMOVE)
+ {
+ HighlightItem(GetSelectedItem());
+ }
+ else
+ HighlightItem(-1);
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+// \brief Show CGUIDialogMediaSource dialog and prompt for a new media source.
+// \return True if the media source is added, false otherwise.
+bool CGUIDialogMediaSource::ShowAndAddMediaSource(const std::string &type)
+{
+ CGUIDialogMediaSource *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogMediaSource>(WINDOW_DIALOG_MEDIA_SOURCE);
+ if (!dialog) return false;
+ dialog->Initialize();
+ dialog->SetShare(CMediaSource());
+ dialog->SetTypeOfMedia(type);
+ dialog->Open();
+ bool confirmed(dialog->IsConfirmed());
+ if (confirmed)
+ {
+ // Add this media source
+ // Get unique source name
+ std::string strName = dialog->GetUniqueMediaSourceName();
+
+ CMediaSource share;
+ share.FromNameAndPaths(type, strName, dialog->GetPaths());
+ if (dialog->m_paths->Size() > 0)
+ share.m_strThumbnailImage = dialog->m_paths->Get(0)->GetArt("thumb");
+ CMediaSourceSettings::GetInstance().AddShare(type, share);
+ OnMediaSourceChanged(type, "", share);
+ }
+ dialog->m_paths->Clear();
+ return confirmed;
+}
+
+bool CGUIDialogMediaSource::ShowAndEditMediaSource(const std::string &type, const std::string&share)
+{
+ VECSOURCES* pShares = CMediaSourceSettings::GetInstance().GetSources(type);
+ if (pShares)
+ {
+ for (unsigned int i = 0;i<pShares->size();++i)
+ {
+ if (StringUtils::EqualsNoCase((*pShares)[i].strName, share))
+ return ShowAndEditMediaSource(type, (*pShares)[i]);
+ }
+ }
+ return false;
+}
+
+bool CGUIDialogMediaSource::ShowAndEditMediaSource(const std::string &type, const CMediaSource &share)
+{
+ std::string strOldName = share.strName;
+ CGUIDialogMediaSource *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogMediaSource>(WINDOW_DIALOG_MEDIA_SOURCE);
+ if (!dialog) return false;
+ dialog->Initialize();
+ dialog->SetShare(share);
+ dialog->SetTypeOfMedia(type, true);
+ dialog->Open();
+ bool confirmed(dialog->IsConfirmed());
+ if (confirmed)
+ {
+ // Update media source
+ // Get unique new source name when changed
+ std::string strName(dialog->m_name);
+ if (!StringUtils::EqualsNoCase(dialog->m_name, strOldName))
+ strName = dialog->GetUniqueMediaSourceName();
+
+ CMediaSource newShare;
+ newShare.FromNameAndPaths(type, strName, dialog->GetPaths());
+ CMediaSourceSettings::GetInstance().UpdateShare(type, strOldName, newShare);
+
+ OnMediaSourceChanged(type, strOldName, newShare);
+ }
+ dialog->m_paths->Clear();
+ return confirmed;
+}
+
+std::string CGUIDialogMediaSource::GetUniqueMediaSourceName()
+{
+ // Get unique source name for this media type
+ unsigned int i, j = 2;
+ bool bConfirmed = false;
+ VECSOURCES* pShares = CMediaSourceSettings::GetInstance().GetSources(m_type);
+ std::string strName = m_name;
+ while (!bConfirmed)
+ {
+ for (i = 0; i<pShares->size(); ++i)
+ {
+ if (StringUtils::EqualsNoCase((*pShares)[i].strName, strName))
+ break;
+ }
+ if (i < pShares->size())
+ // found a match - try next
+ strName = StringUtils::Format("{} ({})", m_name, j++);
+ else
+ bConfirmed = true;
+ }
+ return strName;
+}
+
+void CGUIDialogMediaSource::OnMediaSourceChanged(const std::string& type, const std::string& oldName, const CMediaSource& share)
+{
+ // Processing once media source added/edited - library scraping and scanning
+ if (!StringUtils::StartsWithNoCase(share.strPath, "rss://") &&
+ !StringUtils::StartsWithNoCase(share.strPath, "rsss://") &&
+ !StringUtils::StartsWithNoCase(share.strPath, "upnp://"))
+ {
+ if (type == "video" && !URIUtils::IsLiveTV(share.strPath))
+ // Assign content to a path, refresh scraper information optionally start a scan
+ CGUIWindowVideoBase::OnAssignContent(share.strPath);
+ else if (type == "music")
+ CGUIWindowMusicBase::OnAssignContent(oldName, share);
+ }
+}
+
+void CGUIDialogMediaSource::OnPathBrowse(int item)
+{
+ if (item < 0 || item >= m_paths->Size()) return;
+ // Browse is called. Open the filebrowser dialog.
+ // Ignore current path is best at this stage??
+ std::string path = m_paths->Get(item)->GetPath();
+ bool allowNetworkShares(m_type != "programs");
+ VECSOURCES extraShares;
+
+ if (m_name != CUtil::GetTitleFromPath(path))
+ m_bNameChanged = true;
+ path.clear();
+
+ if (m_type == "music")
+ {
+ CMediaSource share1;
+#if defined(TARGET_ANDROID)
+ // add the default android music directory
+ std::string path;
+ if (CXBMCApp::GetExternalStorage(path, "music") && !path.empty() && CDirectory::Exists(path))
+ {
+ share1.strPath = path;
+ share1.strName = g_localizeStrings.Get(20240);
+ share1.m_ignore = true;
+ extraShares.push_back(share1);
+ }
+#endif
+
+#if defined(TARGET_WINDOWS_STORE)
+ // add the default UWP music directory
+ std::string path;
+ if (XFILE::CWinLibraryDirectory::GetStoragePath(m_type, path) && !path.empty() && CDirectory::Exists(path))
+ {
+ share1.strPath = path;
+ share1.strName = g_localizeStrings.Get(20245);
+ share1.m_ignore = true;
+ extraShares.push_back(share1);
+ }
+#endif
+
+ // add the music playlist location
+ share1.strPath = "special://musicplaylists/";
+ share1.strName = g_localizeStrings.Get(20011);
+ share1.m_ignore = true;
+ extraShares.push_back(share1);
+
+ // add the recordings dir as needed
+ if (CPVRDirectory::HasRadioRecordings())
+ {
+ share1.strPath = PVR::CPVRRecordingsPath::PATH_ACTIVE_RADIO_RECORDINGS;
+ share1.strName = g_localizeStrings.Get(19017); // Recordings
+ extraShares.push_back(share1);
+ }
+ if (CPVRDirectory::HasDeletedRadioRecordings())
+ {
+ share1.strPath = PVR::CPVRRecordingsPath::PATH_DELETED_RADIO_RECORDINGS;
+ share1.strName = g_localizeStrings.Get(19184); // Deleted recordings
+ extraShares.push_back(share1);
+ }
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_AUDIOCDS_RECORDINGPATH) != "")
+ {
+ share1.strPath = "special://recordings/";
+ share1.strName = g_localizeStrings.Get(21883);
+ extraShares.push_back(share1);
+ }
+ }
+ else if (m_type == "video")
+ {
+ CMediaSource share1;
+#if defined(TARGET_ANDROID)
+ // add the default android video directory
+ std::string path;
+ if (CXBMCApp::GetExternalStorage(path, "videos") && !path.empty() && CFileUtils::Exists(path))
+ {
+ share1.strPath = path;
+ share1.strName = g_localizeStrings.Get(20241);
+ share1.m_ignore = true;
+ extraShares.push_back(share1);
+ }
+#endif
+#if defined(TARGET_WINDOWS_STORE)
+ // add the default UWP music directory
+ std::string path;
+ if (XFILE::CWinLibraryDirectory::GetStoragePath(m_type, path) && !path.empty() && CDirectory::Exists(path))
+ {
+ share1.strPath = path;
+ share1.strName = g_localizeStrings.Get(20246);
+ share1.m_ignore = true;
+ extraShares.push_back(share1);
+ }
+#endif
+
+ // add the video playlist location
+ share1.m_ignore = true;
+ share1.strPath = "special://videoplaylists/";
+ share1.strName = g_localizeStrings.Get(20012);
+ extraShares.push_back(share1);
+
+ // add the recordings dir as needed
+ if (CPVRDirectory::HasTVRecordings())
+ {
+ share1.strPath = PVR::CPVRRecordingsPath::PATH_ACTIVE_TV_RECORDINGS;
+ share1.strName = g_localizeStrings.Get(19017); // Recordings
+ extraShares.push_back(share1);
+ }
+ if (CPVRDirectory::HasDeletedTVRecordings())
+ {
+ share1.strPath = PVR::CPVRRecordingsPath::PATH_DELETED_TV_RECORDINGS;
+ share1.strName = g_localizeStrings.Get(19184); // Deleted recordings
+ extraShares.push_back(share1);
+ }
+ }
+ else if (m_type == "pictures")
+ {
+ CMediaSource share1;
+#if defined(TARGET_ANDROID)
+ // add the default android music directory
+ std::string path;
+ if (CXBMCApp::GetExternalStorage(path, "pictures") && !path.empty() && CFileUtils::Exists(path))
+ {
+ share1.strPath = path;
+ share1.strName = g_localizeStrings.Get(20242);
+ share1.m_ignore = true;
+ extraShares.push_back(share1);
+ }
+
+ path.clear();
+ if (CXBMCApp::GetExternalStorage(path, "photos") && !path.empty() && CFileUtils::Exists(path))
+ {
+ share1.strPath = path;
+ share1.strName = g_localizeStrings.Get(20243);
+ share1.m_ignore = true;
+ extraShares.push_back(share1);
+ }
+#endif
+#if defined(TARGET_WINDOWS_STORE)
+ // add the default UWP music directory
+ std::string path;
+ if (XFILE::CWinLibraryDirectory::GetStoragePath(m_type, path) && !path.empty() && CDirectory::Exists(path))
+ {
+ share1.strPath = path;
+ share1.strName = g_localizeStrings.Get(20247);
+ share1.m_ignore = true;
+ extraShares.push_back(share1);
+ }
+ path.clear();
+ if (XFILE::CWinLibraryDirectory::GetStoragePath("photos", path) && !path.empty() && CDirectory::Exists(path))
+ {
+ share1.strPath = path;
+ share1.strName = g_localizeStrings.Get(20248);
+ share1.m_ignore = true;
+ extraShares.push_back(share1);
+ }
+#endif
+
+ share1.m_ignore = true;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_DEBUG_SCREENSHOTPATH) != "")
+ {
+ share1.strPath = "special://screenshots/";
+ share1.strName = g_localizeStrings.Get(20008);
+ extraShares.push_back(share1);
+ }
+ }
+ else if (m_type == "games")
+ {
+ // nothing to add
+ }
+ else if (m_type == "programs")
+ {
+ // nothing to add
+ }
+ if (CGUIDialogFileBrowser::ShowAndGetSource(path, allowNetworkShares, extraShares.size() == 0 ? NULL : &extraShares))
+ {
+ if (item < m_paths->Size()) // if the skin does funky things, m_paths may have been cleared
+ m_paths->Get(item)->SetPath(path);
+ if (!m_bNameChanged || m_name.empty())
+ {
+ CURL url(path);
+ m_name = url.GetWithoutUserDetails();
+ URIUtils::RemoveSlashAtEnd(m_name);
+ m_name = CUtil::GetTitleFromPath(m_name);
+ }
+ UpdateButtons();
+ }
+}
+
+void CGUIDialogMediaSource::OnPath(int item)
+{
+ if (item < 0 || item >= m_paths->Size()) return;
+
+ std::string path(m_paths->Get(item)->GetPath());
+ if (m_name != CUtil::GetTitleFromPath(path))
+ m_bNameChanged = true;
+
+ CGUIKeyboardFactory::ShowAndGetInput(path, CVariant{ g_localizeStrings.Get(1021) }, false);
+ m_paths->Get(item)->SetPath(path);
+
+ if (!m_bNameChanged || m_name.empty())
+ {
+ CURL url(m_paths->Get(item)->GetPath());
+ m_name = url.GetWithoutUserDetails();
+ URIUtils::RemoveSlashAtEnd(m_name);
+ m_name = CUtil::GetTitleFromPath(m_name);
+ }
+ UpdateButtons();
+}
+
+void CGUIDialogMediaSource::OnOK()
+{
+ // Verify the paths by doing a GetDirectory.
+ CFileItemList items;
+
+ // Create temp media source to encode path urls as multipath
+ // Name of actual source may need to be made unique when saved in sources
+ CMediaSource share;
+ share.FromNameAndPaths(m_type, m_name, GetPaths());
+
+ if (StringUtils::StartsWithNoCase(share.strPath, "plugin://") ||
+ CDirectory::GetDirectory(share.strPath, items, "", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_ALLOW_PROMPT) ||
+ CGUIDialogYesNo::ShowAndGetInput(CVariant{ 1001 }, CVariant{ 1025 }))
+ {
+ m_confirmed = true;
+ Close();
+ }
+}
+
+void CGUIDialogMediaSource::OnCancel()
+{
+ m_confirmed = false;
+ Close();
+}
+
+void CGUIDialogMediaSource::UpdateButtons()
+{
+ if (!m_paths->Size()) // sanity
+ return;
+
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_OK, !m_paths->Get(0)->GetPath().empty() && !m_name.empty());
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_PATH_ADD, !m_paths->Get(0)->GetPath().empty());
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_PATH_REMOVE, m_paths->Size() > 1);
+ // name
+ SET_CONTROL_LABEL2(CONTROL_NAME, m_name);
+ SendMessage(GUI_MSG_SET_TYPE, CONTROL_NAME, 0, 1022);
+
+ int currentItem = GetSelectedItem();
+ SendMessage(GUI_MSG_LABEL_RESET, CONTROL_PATH);
+ for (int i = 0; i < m_paths->Size(); i++)
+ {
+ CFileItemPtr item = m_paths->Get(i);
+ std::string path;
+ CURL url(item->GetPath());
+ path = url.GetWithoutUserDetails();
+ if (path.empty()) path = "<" + g_localizeStrings.Get(231) + ">"; // <None>
+ item->SetLabel(path);
+ }
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_PATH, 0, 0, m_paths);
+ OnMessage(msg);
+ SendMessage(GUI_MSG_ITEM_SELECT, CONTROL_PATH, currentItem);
+
+ SET_CONTROL_HIDDEN(CONTROL_CONTENT);
+}
+
+void CGUIDialogMediaSource::SetShare(const CMediaSource &share)
+{
+ m_paths->Clear();
+ for (unsigned int i = 0; i < share.vecPaths.size(); i++)
+ {
+ CFileItemPtr item(new CFileItem(share.vecPaths[i], true));
+ m_paths->Add(item);
+ }
+ if (0 == share.vecPaths.size())
+ {
+ CFileItemPtr item(new CFileItem("", true));
+ m_paths->Add(item);
+ }
+ m_name = share.strName;
+ UpdateButtons();
+}
+
+void CGUIDialogMediaSource::SetTypeOfMedia(const std::string &type, bool editNotAdd)
+{
+ m_type = type;
+ std::string heading;
+ if (editNotAdd)
+ {
+ if (type == "video")
+ heading = g_localizeStrings.Get(10053);
+ else if (type == "music")
+ heading = g_localizeStrings.Get(10054);
+ else if (type == "pictures")
+ heading = g_localizeStrings.Get(10055);
+ else if (type == "games")
+ heading = g_localizeStrings.Get(35252); // "Edit game source"
+ else if (type == "programs")
+ heading = g_localizeStrings.Get(10056);
+ else
+ heading = g_localizeStrings.Get(10057);
+ }
+ else
+ {
+ if (type == "video")
+ heading = g_localizeStrings.Get(10048);
+ else if (type == "music")
+ heading = g_localizeStrings.Get(10049);
+ else if (type == "pictures")
+ heading = g_localizeStrings.Get(13006);
+ else if (type == "games")
+ heading = g_localizeStrings.Get(35251); // "Add game source"
+ else if (type == "programs")
+ heading = g_localizeStrings.Get(10051);
+ else
+ heading = g_localizeStrings.Get(10052);
+ }
+ SET_CONTROL_LABEL(CONTROL_HEADING, heading);
+}
+
+int CGUIDialogMediaSource::GetSelectedItem()
+{
+ CGUIMessage message(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_PATH);
+ OnMessage(message);
+ int value = message.GetParam1();
+ if (value < 0 || value >= m_paths->Size()) return 0;
+ return value;
+}
+
+void CGUIDialogMediaSource::HighlightItem(int item)
+{
+ for (int i = 0; i < m_paths->Size(); i++)
+ m_paths->Get(i)->Select(false);
+ if (item >= 0 && item < m_paths->Size())
+ m_paths->Get(item)->Select(true);
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), CONTROL_PATH, item);
+ OnMessage(msg);
+}
+
+void CGUIDialogMediaSource::OnPathRemove(int item)
+{
+ m_paths->Remove(item);
+ UpdateButtons();
+ if (item >= m_paths->Size())
+ HighlightItem(m_paths->Size() - 1);
+ else
+ HighlightItem(item);
+ if (m_paths->Size() <= 1)
+ {
+ SET_CONTROL_FOCUS(CONTROL_PATH_ADD, 0);
+ }
+}
+
+void CGUIDialogMediaSource::OnPathAdd()
+{
+ // add a new item and select it as well
+ CFileItemPtr item(new CFileItem("", true));
+ m_paths->Add(item);
+ UpdateButtons();
+ HighlightItem(m_paths->Size() - 1);
+}
+
+std::vector<std::string> CGUIDialogMediaSource::GetPaths() const
+{
+ std::vector<std::string> paths;
+ for (int i = 0; i < m_paths->Size(); i++)
+ {
+ if (!m_paths->Get(i)->GetPath().empty())
+ { // strip off the user and password for supported paths (anything that the password manager can auth)
+ // and add the user/pass to the password manager - note, we haven't confirmed that it works
+ // at this point, but if it doesn't, the user will get prompted anyway in implementation.
+ CURL url(m_paths->Get(i)->GetPath());
+ if (CPasswordManager::GetInstance().IsURLSupported(url) && !url.GetUserName().empty())
+ {
+ CPasswordManager::GetInstance().SaveAuthenticatedURL(url);
+ url.SetPassword("");
+ url.SetUserName("");
+ url.SetDomain("");
+ }
+ paths.push_back(url.Get());
+ }
+ }
+ return paths;
+}
+
+void CGUIDialogMediaSource::OnDeinitWindow(int nextWindowID)
+{
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ // clear paths container
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_PATH, 0);
+ OnMessage(msg);
+}
diff --git a/xbmc/dialogs/GUIDialogMediaSource.h b/xbmc/dialogs/GUIDialogMediaSource.h
new file mode 100644
index 0000000..e858e13
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogMediaSource.h
@@ -0,0 +1,56 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+#include <string>
+#include <vector>
+
+class CFileItemList;
+class CMediaSource;
+
+class CGUIDialogMediaSource :
+ public CGUIDialog
+{
+public:
+ CGUIDialogMediaSource(void);
+ ~CGUIDialogMediaSource(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void OnDeinitWindow(int nextWindowID) override;
+ bool OnBack(int actionID) override;
+ static bool ShowAndAddMediaSource(const std::string &type);
+ static bool ShowAndEditMediaSource(const std::string &type, const CMediaSource &share);
+ static bool ShowAndEditMediaSource(const std::string &type, const std::string &share);
+
+ bool IsConfirmed() const { return m_confirmed; }
+
+ void SetShare(const CMediaSource &share);
+ void SetTypeOfMedia(const std::string &type, bool editNotAdd = false);
+protected:
+ void OnPathBrowse(int item);
+ void OnPath(int item);
+ void OnPathAdd();
+ void OnPathRemove(int item);
+ void OnOK();
+ void OnCancel();
+ void UpdateButtons();
+ int GetSelectedItem();
+ void HighlightItem(int item);
+ std::string GetUniqueMediaSourceName();
+ static void OnMediaSourceChanged(const std::string& type, const std::string& oldName, const CMediaSource& share);
+
+ std::vector<std::string> GetPaths() const;
+
+ std::string m_type;
+ std::string m_name;
+ CFileItemList* m_paths;
+ bool m_confirmed = false;
+ bool m_bNameChanged = false;
+};
diff --git a/xbmc/dialogs/GUIDialogNumeric.cpp b/xbmc/dialogs/GUIDialogNumeric.cpp
new file mode 100644
index 0000000..182cf6a
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogNumeric.cpp
@@ -0,0 +1,916 @@
+/*
+ * 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 "GUIDialogNumeric.h"
+
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUILabelControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "input/XBMC_vkeys.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/Digest.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <cassert>
+
+#define CONTROL_HEADING_LABEL 1
+#define CONTROL_INPUT_LABEL 4
+#define CONTROL_NUM0 10
+#define CONTROL_NUM9 19
+#define CONTROL_PREVIOUS 20
+#define CONTROL_ENTER 21
+#define CONTROL_NEXT 22
+#define CONTROL_BACKSPACE 23
+
+using namespace KODI::MESSAGING;
+using KODI::UTILITY::CDigest;
+
+CGUIDialogNumeric::CGUIDialogNumeric(void)
+ : CGUIDialog(WINDOW_DIALOG_NUMERIC, "DialogNumeric.xml"),
+ m_bConfirmed{false},
+ m_bCanceled{false},
+ m_mode{INPUT_PASSWORD},
+ m_block{},
+ m_lastblock{},
+ m_dirty{false}
+{
+ memset(&m_datetime, 0, sizeof(KODI::TIME::SystemTime));
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogNumeric::~CGUIDialogNumeric(void) = default;
+
+void CGUIDialogNumeric::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ CVariant data;
+ switch (m_mode)
+ {
+ case INPUT_TIME:
+ data["type"] = "time";
+ break;
+ case INPUT_DATE:
+ data["type"] = "date";
+ break;
+ case INPUT_IP_ADDRESS:
+ data["type"] = "ip";
+ break;
+ case INPUT_PASSWORD:
+ data["type"] = "numericpassword";
+ break;
+ case INPUT_NUMBER:
+ data["type"] = "number";
+ break;
+ case INPUT_TIME_SECONDS:
+ data["type"] = "seconds";
+ break;
+ default:
+ data["type"] = "keyboard";
+ break;
+ }
+
+ const CGUIControl *control = GetControl(CONTROL_HEADING_LABEL);
+ if (control != nullptr)
+ data["title"] = control->GetDescription();
+
+ data["value"] = GetOutputString();
+
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Input, "OnInputRequested", data);
+}
+
+void CGUIDialogNumeric::OnDeinitWindow(int nextWindowID)
+{
+ // call base class
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Input, "OnInputFinished");
+}
+
+bool CGUIDialogNumeric::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_NEXT_ITEM)
+ OnNext();
+ else if (action.GetID() == ACTION_PREV_ITEM)
+ OnPrevious();
+ else if (action.GetID() == ACTION_BACKSPACE)
+ OnBackSpace();
+ else if (action.GetID() == ACTION_ENTER)
+ OnOK();
+ else if (action.GetID() >= REMOTE_0 && action.GetID() <= REMOTE_9)
+ OnNumber(action.GetID() - REMOTE_0);
+ else if (action.GetID() >= KEY_VKEY && action.GetID() < KEY_UNICODE)
+ {
+ // input from the keyboard (vkey, not ascii)
+ uint8_t b = action.GetID() & 0xFF;
+ if (b == XBMCVK_LEFT)
+ OnPrevious();
+ else if (b == XBMCVK_RIGHT)
+ OnNext();
+ else if (b == XBMCVK_RETURN || b == XBMCVK_NUMPADENTER)
+ OnOK();
+ else if (b == XBMCVK_BACK)
+ OnBackSpace();
+ else if (b == XBMCVK_ESCAPE)
+ OnCancel();
+ }
+ else if (action.GetID() == KEY_UNICODE)
+ { // input from the keyboard
+ if (action.GetUnicode() == 10 || action.GetUnicode() == 13)
+ OnOK(); // enter
+ else if (action.GetUnicode() == 8)
+ OnBackSpace(); // backspace
+ else if (action.GetUnicode() == 27)
+ OnCancel(); // escape
+ else if (action.GetUnicode() == 46)
+ OnNext(); // '.'
+ else if (action.GetUnicode() >= 48 && action.GetUnicode() < 58) // number
+ OnNumber(action.GetUnicode() - 48);
+ }
+ else
+ return CGUIDialog::OnAction(action);
+
+ return true;
+}
+
+bool CGUIDialogNumeric::OnBack(int actionID)
+{
+ OnCancel();
+ return true;
+}
+
+bool CGUIDialogNumeric::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_bConfirmed = false;
+ m_bCanceled = false;
+ m_dirty = false;
+ return CGUIDialog::OnMessage(message);
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ m_bConfirmed = false;
+ m_bCanceled = false;
+ if (CONTROL_NUM0 <= iControl && iControl <= CONTROL_NUM9) // User numeric entry via dialog button UI
+ {
+ OnNumber(iControl - 10);
+ return true;
+ }
+ else if (iControl == CONTROL_PREVIOUS)
+ {
+ OnPrevious();
+ return true;
+ }
+ else if (iControl == CONTROL_NEXT)
+ {
+ OnNext();
+ return true;
+ }
+ else if (iControl == CONTROL_BACKSPACE)
+ {
+ OnBackSpace();
+ return true;
+ }
+ else if (iControl == CONTROL_ENTER)
+ {
+ OnOK();
+ return true;
+ }
+ }
+ break;
+
+ case GUI_MSG_SET_TEXT:
+ SetMode(m_mode, message.GetLabel());
+
+ // close the dialog if requested
+ if (message.GetParam1() > 0)
+ OnOK();
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogNumeric::OnBackSpace()
+{
+ if (!m_dirty && m_block)
+ {
+ --m_block;
+ return;
+ }
+ if (m_mode == INPUT_NUMBER || m_mode == INPUT_PASSWORD)
+ { // just go back one character
+ if (!m_number.empty())
+ m_number.erase(m_number.length() - 1);
+ }
+ else if (m_mode == INPUT_IP_ADDRESS)
+ {
+ if (m_ip[m_block])
+ m_ip[m_block] /= 10;
+ else if (m_block)
+ {
+ --m_block;
+ m_dirty = false;
+ }
+ }
+ else if (m_mode == INPUT_TIME)
+ {
+ if (m_block == 0)
+ m_datetime.hour /= 10;
+ else if (m_datetime.minute)
+ m_datetime.minute /= 10;
+ else
+ {
+ m_block = 0;
+ m_dirty = false;
+ }
+ }
+ else if (m_mode == INPUT_TIME_SECONDS)
+ {
+ if (m_block == 0)
+ m_datetime.hour /= 10;
+ else if (m_block == 1)
+ {
+ if (m_datetime.minute)
+ m_datetime.minute /= 10;
+ else
+ {
+ m_block = 0;
+ m_dirty = false;
+ }
+ }
+ else if (m_datetime.second)
+ m_datetime.minute /= 10;
+ else
+ {
+ m_block = 0;
+ m_dirty = false;
+ }
+ }
+ else if (m_mode == INPUT_DATE)
+ {
+ if (m_block == 0)
+ m_datetime.day /= 10;
+ else if (m_block == 1)
+ {
+ if (m_datetime.month)
+ m_datetime.month /= 10;
+ else
+ {
+ m_block = 0;
+ m_dirty = false;
+ }
+ }
+ else if (m_datetime.year) // m_block == 2
+ m_datetime.year /= 10;
+ else
+ {
+ m_block = 1;
+ m_dirty = false;
+ }
+ }
+}
+
+void CGUIDialogNumeric::OnPrevious()
+{
+ if (m_block)
+ m_block--;
+ m_dirty = false;
+}
+
+void CGUIDialogNumeric::OnNext()
+{
+ if (m_mode == INPUT_IP_ADDRESS && m_block==0 && m_ip[0]==0)
+ return;
+
+ if (m_block < m_lastblock)
+ m_block++;
+ m_dirty = false;
+ if (m_mode == INPUT_DATE)
+ VerifyDate(m_block == 2);
+}
+
+void CGUIDialogNumeric::FrameMove()
+{
+ std::string strLabel;
+ unsigned int start = 0;
+ unsigned int end = 0;
+ if (m_mode == INPUT_PASSWORD)
+ strLabel.assign(m_number.length(), '*');
+ else if (m_mode == INPUT_NUMBER)
+ strLabel = m_number;
+ else if (m_mode == INPUT_TIME)
+ { // format up the time
+ strLabel = StringUtils::Format("{:2}:{:02}", m_datetime.hour, m_datetime.minute);
+ start = m_block * 3;
+ end = m_block * 3 + 2;
+ }
+ else if (m_mode == INPUT_TIME_SECONDS)
+ { // format up the time
+ strLabel = StringUtils::Format("{:2}:{:02}:{:02}", m_datetime.hour, m_datetime.minute,
+ m_datetime.second);
+ start = m_block * 3;
+ end = m_block * 3 + 2;
+ }
+ else if (m_mode == INPUT_DATE)
+ { // format up the date
+ strLabel =
+ StringUtils::Format("{:2}/{:2}/{:4}", m_datetime.day, m_datetime.month, m_datetime.year);
+ start = m_block * 3;
+ end = m_block * 3 + 2;
+ if (m_block == 2)
+ end = m_block * 3 + 4;
+ }
+ else if (m_mode == INPUT_IP_ADDRESS)
+ { // format up the date
+ strLabel = StringUtils::Format("{:3}.{:3}.{:3}.{:3}", m_ip[0], m_ip[1], m_ip[2], m_ip[3]);
+ start = m_block * 4;
+ end = m_block * 4 + 3;
+ }
+ CGUILabelControl *pLabel = dynamic_cast<CGUILabelControl *>(GetControl(CONTROL_INPUT_LABEL));
+ if (pLabel)
+ {
+ pLabel->SetLabel(strLabel);
+ pLabel->SetHighlight(start, end);
+ }
+ CGUIDialog::FrameMove();
+}
+
+void CGUIDialogNumeric::OnNumber(uint32_t num)
+{
+ ResetAutoClose();
+
+ switch (m_mode)
+ {
+ case INPUT_NUMBER:
+ case INPUT_PASSWORD:
+ m_number += num + '0';
+ break;
+ case INPUT_TIME:
+ HandleInputTime(num);
+ break;
+ case INPUT_TIME_SECONDS:
+ HandleInputSeconds(num);
+ break;
+ case INPUT_DATE:
+ HandleInputDate(num);
+ break;
+ case INPUT_IP_ADDRESS:
+ HandleInputIP(num);
+ break;
+ }
+}
+
+void CGUIDialogNumeric::SetMode(INPUT_MODE mode, const KODI::TIME::SystemTime& initial)
+{
+ m_mode = mode;
+ m_block = 0;
+ m_lastblock = 0;
+ if (m_mode == INPUT_TIME || m_mode == INPUT_TIME_SECONDS || m_mode == INPUT_DATE)
+ {
+ m_datetime = initial;
+ m_lastblock = (m_mode != INPUT_TIME) ? 2 : 1;
+ }
+}
+
+void CGUIDialogNumeric::SetMode(INPUT_MODE mode, const std::string &initial)
+{
+ m_mode = mode;
+ m_block = 0;
+ m_lastblock = 0;
+ if (m_mode == INPUT_TIME || m_mode == INPUT_TIME_SECONDS || m_mode == INPUT_DATE)
+ {
+ CDateTime dateTime;
+ if (m_mode == INPUT_TIME || m_mode == INPUT_TIME_SECONDS)
+ {
+ // check if we have a pure number
+ if (initial.find_first_not_of("0123456789") == std::string::npos)
+ {
+ long seconds = strtol(initial.c_str(), nullptr, 10);
+ dateTime = seconds;
+ }
+ else
+ {
+ std::string tmp = initial;
+ // if we are handling seconds and if the string only contains
+ // "mm:ss" we need to add dummy "hh:" to get "hh:mm:ss"
+ if (m_mode == INPUT_TIME_SECONDS && tmp.length() <= 5)
+ tmp = "00:" + tmp;
+ dateTime.SetFromDBTime(tmp);
+ }
+ }
+ else if (m_mode == INPUT_DATE)
+ {
+ std::string tmp = initial;
+ StringUtils::Replace(tmp, '/', '.');
+ dateTime.SetFromDBDate(tmp);
+ }
+
+ if (!dateTime.IsValid())
+ return;
+
+ dateTime.GetAsSystemTime(m_datetime);
+ m_lastblock = (m_mode == INPUT_DATE) ? 2 : 1;
+ }
+ else if (m_mode == INPUT_IP_ADDRESS)
+ {
+ m_lastblock = 3;
+ auto blocks = StringUtils::Split(initial, '.');
+ if (blocks.size() != 4)
+ return;
+
+ for (size_t i = 0; i < blocks.size(); ++i)
+ {
+ if (blocks[i].length() > 3)
+ return;
+
+ m_ip[i] = static_cast<uint8_t>(atoi(blocks[i].c_str()));
+ }
+ }
+ else if (m_mode == INPUT_NUMBER || m_mode == INPUT_PASSWORD)
+ m_number = initial;
+}
+
+KODI::TIME::SystemTime CGUIDialogNumeric::GetOutput() const
+{
+ assert(m_mode == INPUT_TIME || m_mode == INPUT_TIME_SECONDS || m_mode == INPUT_DATE);
+ return m_datetime;
+}
+
+std::string CGUIDialogNumeric::GetOutputString() const
+{
+ switch (m_mode)
+ {
+ case INPUT_DATE:
+ return StringUtils::Format("{:02}/{:02}/{:04}", m_datetime.day, m_datetime.month,
+ m_datetime.year);
+ case INPUT_TIME:
+ return StringUtils::Format("{}:{:02}", m_datetime.hour, m_datetime.minute);
+ case INPUT_TIME_SECONDS:
+ return StringUtils::Format("{}:{:02}:{:02}", m_datetime.hour, m_datetime.minute,
+ m_datetime.second);
+ case INPUT_IP_ADDRESS:
+ return StringUtils::Format("{}.{}.{}.{}", m_ip[0], m_ip[1], m_ip[2], m_ip[3]);
+ case INPUT_NUMBER:
+ case INPUT_PASSWORD:
+ return m_number;
+ }
+
+ //should never get here
+ return std::string();
+}
+
+bool CGUIDialogNumeric::ShowAndGetSeconds(std::string &timeString, const std::string &heading)
+{
+ CGUIDialogNumeric *pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogNumeric>(WINDOW_DIALOG_NUMERIC);
+ if (!pDialog) return false;
+ int seconds = StringUtils::TimeStringToSeconds(timeString);
+ KODI::TIME::SystemTime time = {};
+ time.hour = seconds / 3600;
+ time.minute = (seconds - time.hour * 3600) / 60;
+ time.second = seconds - time.hour * 3600 - time.minute * 60;
+ pDialog->SetMode(INPUT_TIME_SECONDS, time);
+ pDialog->SetHeading(heading);
+ pDialog->Open();
+ if (!pDialog->IsConfirmed() || pDialog->IsCanceled())
+ return false;
+ time = pDialog->GetOutput();
+ seconds = time.hour * 3600 + time.minute * 60 + time.second;
+ timeString = StringUtils::SecondsToTimeString(seconds);
+ return true;
+}
+
+bool CGUIDialogNumeric::ShowAndGetTime(KODI::TIME::SystemTime& time, const std::string& heading)
+{
+ CGUIDialogNumeric *pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogNumeric>(WINDOW_DIALOG_NUMERIC);
+ if (!pDialog) return false;
+ pDialog->SetMode(INPUT_TIME, time);
+ pDialog->SetHeading(heading);
+ pDialog->Open();
+ if (!pDialog->IsConfirmed() || pDialog->IsCanceled())
+ return false;
+ time = pDialog->GetOutput();
+ return true;
+}
+
+bool CGUIDialogNumeric::ShowAndGetDate(KODI::TIME::SystemTime& date, const std::string& heading)
+{
+ CGUIDialogNumeric *pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogNumeric>(WINDOW_DIALOG_NUMERIC);
+ if (!pDialog) return false;
+ pDialog->SetMode(INPUT_DATE, date);
+ pDialog->SetHeading(heading);
+ pDialog->Open();
+ if (!pDialog->IsConfirmed() || pDialog->IsCanceled())
+ return false;
+ date = pDialog->GetOutput();
+ return true;
+}
+
+bool CGUIDialogNumeric::ShowAndGetIPAddress(std::string &IPAddress, const std::string &heading)
+{
+ CGUIDialogNumeric *pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogNumeric>(WINDOW_DIALOG_NUMERIC);
+ if (!pDialog) return false;
+ pDialog->SetMode(INPUT_IP_ADDRESS, IPAddress);
+ pDialog->SetHeading(heading);
+ pDialog->Open();
+ if (!pDialog->IsConfirmed() || pDialog->IsCanceled())
+ return false;
+ IPAddress = pDialog->GetOutputString();
+ return true;
+}
+
+bool CGUIDialogNumeric::ShowAndGetNumber(std::string& strInput, const std::string &strHeading, unsigned int iAutoCloseTimeoutMs /* = 0 */, bool bSetHidden /* = false */)
+{
+ // Prompt user for password input
+ CGUIDialogNumeric *pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogNumeric>(WINDOW_DIALOG_NUMERIC);
+ pDialog->SetHeading( strHeading );
+
+ if (bSetHidden)
+ pDialog->SetMode(INPUT_PASSWORD, strInput);
+ else
+ pDialog->SetMode(INPUT_NUMBER, strInput);
+ if (iAutoCloseTimeoutMs)
+ pDialog->SetAutoClose(iAutoCloseTimeoutMs);
+
+ pDialog->Open();
+
+ if (!pDialog->IsAutoClosed() && (!pDialog->IsConfirmed() || pDialog->IsCanceled()))
+ return false;
+ strInput = pDialog->GetOutputString();
+ return true;
+}
+
+// \brief Show numeric keypad twice to get and confirm a user-entered password string.
+// \param strNewPassword String to preload into the keyboard accumulator. Overwritten with user input if return=true.
+// \return true if successful display and user input entry/re-entry. false if unsuccessful display, no user input, or canceled editing.
+bool CGUIDialogNumeric::ShowAndVerifyNewPassword(std::string& strNewPassword)
+{
+ // Prompt user for password input
+ std::string strUserInput;
+ InputVerificationResult ret = ShowAndVerifyInput(strUserInput, g_localizeStrings.Get(12340), false);
+ if (ret != InputVerificationResult::SUCCESS)
+ {
+ if (ret == InputVerificationResult::FAILED)
+ {
+ // Show error to user saying the password entry was blank
+ HELPERS::ShowOKDialogText(CVariant{12357}, CVariant{12358}); // Password is empty/blank
+ }
+ return false;
+ }
+
+ if (strUserInput.empty())
+ // user canceled out
+ return false;
+
+ // Prompt again for password input, this time sending previous input as the password to verify
+ ret = ShowAndVerifyInput(strUserInput, g_localizeStrings.Get(12341), true);
+ if (ret != InputVerificationResult::SUCCESS)
+ {
+ if (ret == InputVerificationResult::FAILED)
+ {
+ // Show error to user saying the password re-entry failed
+ HELPERS::ShowOKDialogText(CVariant{12357}, CVariant{12344}); // Password do not match
+ }
+ return false;
+ }
+
+ // password entry and re-entry succeeded
+ strNewPassword = strUserInput;
+ return true;
+}
+
+// \brief Show numeric keypad and verify user input against strPassword.
+// \param strPassword Value to compare against user input.
+// \param strHeading String shown on dialog title. Converts to localized string if contains a positive integer.
+// \param iRetries If greater than 0, shows "Incorrect password, %d retries left" on dialog line 2, else line 2 is blank.
+// \return 0 if successful display and user input. 1 if unsuccessful input. -1 if no user input or canceled editing.
+int CGUIDialogNumeric::ShowAndVerifyPassword(std::string& strPassword, const std::string& strHeading, int iRetries)
+{
+ std::string strTempHeading = strHeading;
+ if (iRetries > 0)
+ {
+ // Show a string telling user they have iRetries retries left
+ strTempHeading = StringUtils::Format("{}. {} {} {}", strHeading, g_localizeStrings.Get(12342),
+ iRetries, g_localizeStrings.Get(12343));
+ }
+
+ // make a copy of strPassword to prevent from overwriting it later
+ std::string strPassTemp = strPassword;
+ InputVerificationResult ret = ShowAndVerifyInput(strPassTemp, strTempHeading, true);
+ if (ret == InputVerificationResult::SUCCESS)
+ return 0; // user entered correct password
+
+ if (ret == InputVerificationResult::CANCELED)
+ return -1; // user canceled out
+
+ return 1; // user must have entered an incorrect password
+}
+
+// \brief Show numeric keypad and verify user input against strToVerify.
+// \param strToVerify Value to compare against user input.
+// \param dlgHeading String shown on dialog title.
+// \param bVerifyInput If set as true we verify the users input versus strToVerify.
+// \return the result of the check (success, failed, or canceled by user).
+InputVerificationResult CGUIDialogNumeric::ShowAndVerifyInput(std::string& strToVerify, const std::string& dlgHeading, bool bVerifyInput)
+{
+ // Prompt user for password input
+ CGUIDialogNumeric *pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogNumeric>(WINDOW_DIALOG_NUMERIC);
+ pDialog->SetHeading(dlgHeading);
+
+ std::string strInput;
+ if (!bVerifyInput)
+ strInput = strToVerify;
+
+ pDialog->SetMode(INPUT_PASSWORD, strInput);
+ pDialog->Open();
+
+ strInput = pDialog->GetOutputString();
+
+ if (!pDialog->IsConfirmed() || pDialog->IsCanceled())
+ {
+ // user canceled out
+ strToVerify = "";
+ return InputVerificationResult::CANCELED;
+ }
+
+ const std::string md5pword2 = CDigest::Calculate(CDigest::Type::MD5, strInput);
+
+ if (!bVerifyInput)
+ {
+ strToVerify = md5pword2;
+ return InputVerificationResult::SUCCESS;
+ }
+
+ return StringUtils::EqualsNoCase(strToVerify, md5pword2) ? InputVerificationResult::SUCCESS : InputVerificationResult::FAILED;
+}
+
+bool CGUIDialogNumeric::IsConfirmed() const
+{
+ return m_bConfirmed;
+}
+
+bool CGUIDialogNumeric::IsCanceled() const
+{
+ return m_bCanceled;
+}
+
+void CGUIDialogNumeric::SetHeading(const std::string& strHeading)
+{
+ Initialize();
+ CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), CONTROL_HEADING_LABEL);
+ msg.SetLabel(strHeading);
+ OnMessage(msg);
+}
+
+void CGUIDialogNumeric::VerifyDate(bool checkYear)
+{
+ if (m_datetime.day == 0)
+ m_datetime.day = 1;
+ if (m_datetime.month == 0)
+ m_datetime.month = 1;
+ // check for number of days in the month
+ if (m_datetime.day == 31)
+ {
+ if (m_datetime.month == 4 || m_datetime.month == 6 || m_datetime.month == 9 ||
+ m_datetime.month == 11)
+ m_datetime.day = 30;
+ }
+ if (m_datetime.month == 2 && m_datetime.day > 28)
+ {
+ m_datetime.day = 29; // max in february.
+ if (checkYear)
+ {
+ // leap years occur when the year is divisible by 4 but not by 100, or the year is divisible by 400
+ // thus they don't occur, if the year has a remainder when divided by 4, or when the year is divisible by 100 but not by 400
+ if ((m_datetime.year % 4) || (!(m_datetime.year % 100) && (m_datetime.year % 400)))
+ m_datetime.day = 28;
+ }
+ }
+}
+
+void CGUIDialogNumeric::OnOK()
+{
+ m_bConfirmed = true;
+ m_bCanceled = false;
+ Close();
+}
+
+void CGUIDialogNumeric::OnCancel()
+{
+ m_bConfirmed = false;
+ m_bCanceled = true;
+ Close();
+}
+
+void CGUIDialogNumeric::HandleInputIP(uint32_t num)
+{
+ if (m_dirty && ((m_ip[m_block] < 25) || (m_ip[m_block] == 25 && num < 6) || !(m_block == 0 && num == 0)))
+ {
+ m_ip[m_block] *= 10;
+ m_ip[m_block] += num;
+ }
+ else
+ m_ip[m_block] = num;
+
+ if (m_ip[m_block] > 25 || (m_ip[m_block] == 0 && num == 0))
+ {
+ ++m_block;
+ if (m_block > 3)
+ m_block = 0;
+ m_dirty = false;
+ }
+ else
+ m_dirty = true;
+}
+
+void CGUIDialogNumeric::HandleInputDate(uint32_t num)
+{
+ if (m_block == 0) // day of month
+ {
+ if (m_dirty && (m_datetime.day < 3 || num < 2))
+ {
+ m_datetime.day *= 10;
+ m_datetime.day += num;
+ }
+ else
+ m_datetime.day = num;
+
+ if (m_datetime.day > 3)
+ {
+ m_block = 1; // move to months
+ m_dirty = false;
+ }
+ else
+ m_dirty = true;
+ }
+ else if (m_block == 1) // months
+ {
+ if (m_dirty && num < 3)
+ {
+ m_datetime.month *= 10;
+ m_datetime.month += num;
+ }
+ else
+ m_datetime.month = num;
+
+ if (m_datetime.month > 1)
+ {
+ VerifyDate(false);
+ m_block = 2; // move to year
+ m_dirty = false;
+ }
+ else
+ m_dirty = true;
+ }
+ else // year
+ {
+ if (m_dirty && m_datetime.year < 1000) // have taken input
+ {
+ m_datetime.year *= 10;
+ m_datetime.year += num;
+ }
+ else
+ m_datetime.year = num;
+
+ if (m_datetime.year > 1000)
+ {
+ VerifyDate(true);
+ m_block = 0; // move to day of month
+ m_dirty = false;
+ }
+ else
+ m_dirty = true;
+ }
+}
+
+void CGUIDialogNumeric::HandleInputSeconds(uint32_t num)
+{
+ if (m_block == 0) // hour
+ {
+ if (m_dirty) // have input the first digit
+ {
+ m_datetime.hour *= 10;
+ m_datetime.hour += num;
+ m_block = 1; // move to minutes - allows up to 99 hours
+ m_dirty = false;
+ }
+ else // this is the first digit
+ {
+ m_datetime.hour = num;
+ m_dirty = true;
+ }
+ }
+ else if (m_block == 1) // minute
+ {
+ if (m_dirty) // have input the first digit
+ {
+ m_datetime.minute *= 10;
+ m_datetime.minute += num;
+ m_block = 2; // move to seconds - allows up to 99 minutes
+ m_dirty = false;
+ }
+ else // this is the first digit
+ {
+ m_datetime.minute = num;
+ if (num > 5)
+ {
+ m_block = 2; // move to seconds
+ m_dirty = false;
+ }
+ else
+ m_dirty = true;
+ }
+ }
+ else // seconds
+ {
+ if (m_dirty) // have input the first digit
+ {
+ m_datetime.second *= 10;
+ m_datetime.second += num;
+ m_block = 0; // move to hours
+ m_dirty = false;
+ }
+ else // this is the first digit
+ {
+ m_datetime.second = num;
+ if (num > 5)
+ {
+ m_block = 0; // move to hours
+ m_dirty = false;
+ }
+ else
+ m_dirty = true;
+ }
+ }
+}
+
+void CGUIDialogNumeric::HandleInputTime(uint32_t num)
+{
+ if (m_block == 0) // hour
+ {
+ if (m_dirty) // have input the first digit
+ {
+ if (m_datetime.hour < 2 || num < 4)
+ {
+ m_datetime.hour *= 10;
+ m_datetime.hour += num;
+ }
+ else
+ m_datetime.hour = num;
+
+ m_block = 1; // move to minutes
+ m_dirty = false;
+ }
+ else // this is the first digit
+ {
+ m_datetime.hour = num;
+
+ if (num > 2)
+ {
+ m_block = 1; // move to minutes
+ m_dirty = false;
+ }
+ else
+ m_dirty = true;
+ }
+ }
+ else // minute
+ {
+ if (m_dirty) // have input the first digit
+ {
+ m_datetime.minute *= 10;
+ m_datetime.minute += num;
+ m_block = 0; // move to hours
+ m_dirty = false;
+ }
+ else // this is the first digit
+ {
+ m_datetime.minute = num;
+
+ if (num > 5)
+ {
+ m_block = 0; // move to hours
+ m_dirty = false;
+ }
+ else
+ m_dirty = true;
+ }
+ }
+}
+
diff --git a/xbmc/dialogs/GUIDialogNumeric.h b/xbmc/dialogs/GUIDialogNumeric.h
new file mode 100644
index 0000000..2932053
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogNumeric.h
@@ -0,0 +1,82 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "utils/XTimeUtils.h"
+
+#include <cstdint>
+
+enum class InputVerificationResult
+{
+ CANCELED,
+ FAILED,
+ SUCCESS
+};
+
+class CGUIDialogNumeric :
+ public CGUIDialog
+{
+public:
+ enum INPUT_MODE { INPUT_TIME = 1, INPUT_DATE, INPUT_IP_ADDRESS, INPUT_PASSWORD, INPUT_NUMBER, INPUT_TIME_SECONDS };
+ CGUIDialogNumeric(void);
+ ~CGUIDialogNumeric(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnBack(int actionID) override;
+ void FrameMove() override;
+
+ bool IsConfirmed() const;
+ bool IsCanceled() const;
+ bool IsInputHidden() const { return m_mode == INPUT_PASSWORD; }
+
+ static bool ShowAndVerifyNewPassword(std::string& strNewPassword);
+ static int ShowAndVerifyPassword(std::string& strPassword, const std::string& strHeading, int iRetries);
+ static InputVerificationResult ShowAndVerifyInput(std::string& strPassword, const std::string& strHeading, bool bGetUserInput);
+
+ void SetHeading(const std::string &strHeading);
+ void SetMode(INPUT_MODE mode, const KODI::TIME::SystemTime& initial);
+ void SetMode(INPUT_MODE mode, const std::string &initial);
+ KODI::TIME::SystemTime GetOutput() const;
+ std::string GetOutputString() const;
+
+ static bool ShowAndGetTime(KODI::TIME::SystemTime& time, const std::string& heading);
+ static bool ShowAndGetDate(KODI::TIME::SystemTime& date, const std::string& heading);
+ static bool ShowAndGetIPAddress(std::string &IPAddress, const std::string &heading);
+ static bool ShowAndGetNumber(std::string& strInput, const std::string &strHeading, unsigned int iAutoCloseTimeoutMs = 0, bool bSetHidden = false);
+ static bool ShowAndGetSeconds(std::string& timeString, const std::string &heading);
+
+protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ void OnNumber(uint32_t num);
+ void VerifyDate(bool checkYear);
+ void OnNext();
+ void OnPrevious();
+ void OnBackSpace();
+ void OnOK();
+ void OnCancel();
+
+ void HandleInputIP(uint32_t num);
+ void HandleInputDate(uint32_t num);
+ void HandleInputSeconds(uint32_t num);
+ void HandleInputTime(uint32_t num);
+
+ bool m_bConfirmed;
+ bool m_bCanceled;
+
+ INPUT_MODE m_mode; // the current input mode
+ KODI::TIME::SystemTime m_datetime; // for time and date modes
+ uint8_t m_ip[4]; // for ip address mode
+ uint32_t m_block; // for time, date, and IP methods.
+ uint32_t m_lastblock;
+ bool m_dirty; // true if the current block has been changed.
+ std::string m_number; ///< for number or password input
+};
diff --git a/xbmc/dialogs/GUIDialogOK.cpp b/xbmc/dialogs/GUIDialogOK.cpp
new file mode 100644
index 0000000..af626dc
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogOK.cpp
@@ -0,0 +1,99 @@
+/*
+ * 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 "GUIDialogOK.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/Variant.h"
+
+CGUIDialogOK::CGUIDialogOK(void)
+ : CGUIDialogBoxBase(WINDOW_DIALOG_OK, "DialogConfirm.xml")
+{
+}
+
+CGUIDialogOK::~CGUIDialogOK(void) = default;
+
+bool CGUIDialogOK::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_CLICKED)
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_YES_BUTTON)
+ {
+ m_bConfirmed = true;
+ Close();
+ return true;
+ }
+ }
+ return CGUIDialogBoxBase::OnMessage(message);
+}
+
+// \brief Show CGUIDialogOK dialog, then wait for user to dismiss it.
+bool CGUIDialogOK::ShowAndGetInput(const CVariant& heading, const CVariant& text)
+{
+ CGUIDialogOK *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogOK>(WINDOW_DIALOG_OK);
+ if (!dialog)
+ return false;
+ dialog->SetHeading(heading);
+ dialog->SetText(text);
+ dialog->Open();
+ return dialog->IsConfirmed();
+}
+
+// \brief Show CGUIDialogOK dialog, then wait for user to dismiss it.
+bool CGUIDialogOK::ShowAndGetInput(const CVariant& heading,
+ const CVariant& line0,
+ const CVariant& line1,
+ const CVariant& line2)
+{
+ CGUIDialogOK *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogOK>(WINDOW_DIALOG_OK);
+ if (!dialog)
+ return false;
+ dialog->SetHeading(heading);
+ dialog->SetLine(0, line0);
+ dialog->SetLine(1, line1);
+ dialog->SetLine(2, line2);
+ dialog->Open();
+ return dialog->IsConfirmed();
+}
+
+bool CGUIDialogOK::ShowAndGetInput(const HELPERS::DialogOKMessage & options)
+{
+ if (!options.heading.isNull())
+ SetHeading(options.heading);
+ if (!options.text.isNull())
+ SetText(options.text);
+
+ for (size_t i = 0; i < 3; ++i)
+ {
+ if (!options.lines[i].isNull())
+ SetLine(i, options.lines[i]);
+ }
+ Open();
+ return IsConfirmed();
+}
+
+void CGUIDialogOK::OnInitWindow()
+{
+ SET_CONTROL_HIDDEN(CONTROL_NO_BUTTON);
+ SET_CONTROL_HIDDEN(CONTROL_CUSTOM_BUTTON);
+ SET_CONTROL_HIDDEN(CONTROL_PROGRESS_BAR);
+ SET_CONTROL_FOCUS(CONTROL_YES_BUTTON, 0);
+
+ CGUIDialogBoxBase::OnInitWindow();
+}
+
+int CGUIDialogOK::GetDefaultLabelID(int controlId) const
+{
+ if (controlId == CONTROL_YES_BUTTON)
+ return 186;
+ return CGUIDialogBoxBase::GetDefaultLabelID(controlId);
+}
diff --git a/xbmc/dialogs/GUIDialogOK.h b/xbmc/dialogs/GUIDialogOK.h
new file mode 100644
index 0000000..94678e3
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogOK.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "GUIDialogBoxBase.h"
+#include "messaging/helpers/DialogOKHelper.h"
+
+class CGUIMessage;
+class CVariant;
+
+using namespace KODI::MESSAGING;
+
+class CGUIDialogOK :
+ public CGUIDialogBoxBase
+{
+public:
+ CGUIDialogOK(void);
+ ~CGUIDialogOK(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ static bool ShowAndGetInput(const CVariant& heading, const CVariant& text);
+ static bool ShowAndGetInput(const CVariant& heading,
+ const CVariant& line0,
+ const CVariant& line1,
+ const CVariant& line2);
+ /*!
+ \brief Open a OK dialog and wait for input
+
+ \param[in] options a struct of type DialogOKMessage containing
+ the options to set for this dialog.
+
+ \sa KODI::MESSAGING::HELPERS::DialogOKMessage
+ */
+ bool ShowAndGetInput(const HELPERS::DialogOKMessage& options);
+protected:
+ void OnInitWindow() override;
+ int GetDefaultLabelID(int controlId) const override;
+};
diff --git a/xbmc/dialogs/GUIDialogPlayEject.cpp b/xbmc/dialogs/GUIDialogPlayEject.cpp
new file mode 100644
index 0000000..14ee2bc
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogPlayEject.cpp
@@ -0,0 +1,101 @@
+/*
+ * 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 "GUIDialogPlayEject.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "storage/MediaManager.h"
+#include "utils/Variant.h"
+
+#include <utility>
+
+#define ID_BUTTON_PLAY 11
+#define ID_BUTTON_EJECT 10
+
+CGUIDialogPlayEject::CGUIDialogPlayEject()
+ : CGUIDialogYesNo(WINDOW_DIALOG_PLAY_EJECT)
+{
+}
+
+CGUIDialogPlayEject::~CGUIDialogPlayEject() = default;
+
+bool CGUIDialogPlayEject::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_CLICKED)
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == ID_BUTTON_PLAY)
+ {
+ if (CServiceBroker::GetMediaManager().IsDiscInDrive())
+ {
+ m_bConfirmed = true;
+ Close();
+ }
+
+ return true;
+ }
+ if (iControl == ID_BUTTON_EJECT)
+ {
+ CServiceBroker::GetMediaManager().ToggleTray();
+ return true;
+ }
+ }
+
+ return CGUIDialogYesNo::OnMessage(message);
+}
+
+void CGUIDialogPlayEject::FrameMove()
+{
+ CONTROL_ENABLE_ON_CONDITION(ID_BUTTON_PLAY, CServiceBroker::GetMediaManager().IsDiscInDrive());
+
+ CGUIDialogYesNo::FrameMove();
+}
+
+void CGUIDialogPlayEject::OnInitWindow()
+{
+ if (CServiceBroker::GetMediaManager().IsDiscInDrive())
+ {
+ m_defaultControl = ID_BUTTON_PLAY;
+ }
+ else
+ {
+ CONTROL_DISABLE(ID_BUTTON_PLAY);
+ m_defaultControl = ID_BUTTON_EJECT;
+ }
+
+ CGUIDialogYesNo::OnInitWindow();
+}
+
+bool CGUIDialogPlayEject::ShowAndGetInput(const std::string& strLine1,
+ const std::string& strLine2,
+ unsigned int uiAutoCloseTime /* = 0 */)
+{
+
+ // Create the dialog
+ CGUIDialogPlayEject * pDialog = (CGUIDialogPlayEject *)CServiceBroker::GetGUI()->GetWindowManager().
+ GetWindow(WINDOW_DIALOG_PLAY_EJECT);
+ if (!pDialog)
+ return false;
+
+ // Setup dialog parameters
+ pDialog->SetHeading(CVariant{219});
+ pDialog->SetLine(0, CVariant{429});
+ pDialog->SetLine(1, CVariant{strLine1});
+ pDialog->SetLine(2, CVariant{strLine2});
+ pDialog->SetChoice(ID_BUTTON_PLAY - 10, 208);
+ pDialog->SetChoice(ID_BUTTON_EJECT - 10, 13391);
+ if (uiAutoCloseTime)
+ pDialog->SetAutoClose(uiAutoCloseTime);
+
+ // Display the dialog
+ pDialog->Open();
+
+ return pDialog->IsConfirmed();
+}
diff --git a/xbmc/dialogs/GUIDialogPlayEject.h b/xbmc/dialogs/GUIDialogPlayEject.h
new file mode 100644
index 0000000..8354b53
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogPlayEject.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "GUIDialogYesNo.h"
+
+class CGUIDialogPlayEject : public CGUIDialogYesNo
+{
+public:
+ CGUIDialogPlayEject();
+ ~CGUIDialogPlayEject() override;
+ bool OnMessage(CGUIMessage& message) override;
+ void FrameMove() override;
+
+ static bool ShowAndGetInput(const std::string& strLine1,
+ const std::string& strLine2,
+ unsigned int uiAutoCloseTime = 0);
+
+protected:
+ void OnInitWindow() override;
+};
diff --git a/xbmc/dialogs/GUIDialogPlayerControls.cpp b/xbmc/dialogs/GUIDialogPlayerControls.cpp
new file mode 100644
index 0000000..e137eb1
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogPlayerControls.cpp
@@ -0,0 +1,19 @@
+/*
+ * 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 "GUIDialogPlayerControls.h"
+
+
+CGUIDialogPlayerControls::CGUIDialogPlayerControls(void)
+ : CGUIDialog(WINDOW_DIALOG_PLAYER_CONTROLS, "PlayerControls.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogPlayerControls::~CGUIDialogPlayerControls(void) = default;
+
diff --git a/xbmc/dialogs/GUIDialogPlayerControls.h b/xbmc/dialogs/GUIDialogPlayerControls.h
new file mode 100644
index 0000000..e2bb1a4
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogPlayerControls.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIDialogPlayerControls :
+ public CGUIDialog
+{
+public:
+ CGUIDialogPlayerControls(void);
+ ~CGUIDialogPlayerControls(void) override;
+};
diff --git a/xbmc/dialogs/GUIDialogPlayerProcessInfo.cpp b/xbmc/dialogs/GUIDialogPlayerProcessInfo.cpp
new file mode 100644
index 0000000..8276311
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogPlayerProcessInfo.cpp
@@ -0,0 +1,29 @@
+/*
+ * 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 "GUIDialogPlayerProcessInfo.h"
+
+#include "input/Key.h"
+
+CGUIDialogPlayerProcessInfo::CGUIDialogPlayerProcessInfo(void)
+ : CGUIDialog(WINDOW_DIALOG_PLAYER_PROCESS_INFO, "DialogPlayerProcessInfo.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogPlayerProcessInfo::~CGUIDialogPlayerProcessInfo(void) = default;
+
+bool CGUIDialogPlayerProcessInfo::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_PLAYER_PROCESS_INFO)
+ {
+ Close();
+ return true;
+ }
+ return CGUIDialog::OnAction(action);
+}
diff --git a/xbmc/dialogs/GUIDialogPlayerProcessInfo.h b/xbmc/dialogs/GUIDialogPlayerProcessInfo.h
new file mode 100644
index 0000000..7413770
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogPlayerProcessInfo.h
@@ -0,0 +1,20 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIDialogPlayerProcessInfo : public CGUIDialog
+{
+public:
+ CGUIDialogPlayerProcessInfo(void);
+ ~CGUIDialogPlayerProcessInfo(void) override;
+
+ bool OnAction(const CAction &action) override;
+};
diff --git a/xbmc/dialogs/GUIDialogProgress.cpp b/xbmc/dialogs/GUIDialogProgress.cpp
new file mode 100644
index 0000000..21acb0a
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogProgress.cpp
@@ -0,0 +1,292 @@
+/*
+ * 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 "GUIDialogProgress.h"
+
+#include "guilib/GUIProgressControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+CGUIDialogProgress::CGUIDialogProgress(void)
+ : CGUIDialogBoxBase(WINDOW_DIALOG_PROGRESS, "DialogConfirm.xml")
+{
+ Reset();
+}
+
+CGUIDialogProgress::~CGUIDialogProgress(void) = default;
+
+void CGUIDialogProgress::Reset()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_iCurrent = 0;
+ m_iMax = 0;
+ m_percentage = 0;
+ m_showProgress = true;
+ m_bCanCancel = true;
+ m_iChoice = CHOICE_NONE;
+ m_supportedChoices = {};
+
+ SetInvalid();
+}
+
+void CGUIDialogProgress::SetCanCancel(bool bCanCancel)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_bCanCancel = bCanCancel;
+ SetInvalid();
+}
+
+void CGUIDialogProgress::ShowChoice(int iChoice, const CVariant& label)
+{
+ if (iChoice >= 0 && iChoice < DIALOG_MAX_CHOICES)
+ {
+ m_supportedChoices[iChoice] = true;
+ SetChoice(iChoice, label);
+ SetInvalid();
+ }
+}
+
+int CGUIDialogProgress::GetChoice() const
+{
+ return m_iChoice;
+}
+
+void CGUIDialogProgress::Open(const std::string &param /* = "" */)
+{
+ CLog::Log(LOGDEBUG, "DialogProgress::Open called {}", m_active ? "(already running)!" : "");
+
+ {
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ ShowProgressBar(true);
+ }
+
+ CGUIDialog::Open(false, param);
+
+ while (m_active && IsAnimating(ANIM_TYPE_WINDOW_OPEN))
+ {
+ Progress();
+ // we should have rendered at least once by now - if we haven't, then
+ // we must be running from fullscreen video or similar where the
+ // calling thread handles rendering (ie not main app thread) but
+ // is waiting on this routine before rendering begins
+ if (!HasProcessed())
+ break;
+ }
+}
+
+void CGUIDialogProgress::Progress()
+{
+ if (m_active)
+ {
+ ProcessRenderLoop();
+ }
+}
+
+bool CGUIDialogProgress::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+
+ case GUI_MSG_WINDOW_DEINIT:
+ Reset();
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl >= CONTROL_CHOICES_START && iControl < (CONTROL_CHOICES_START + DIALOG_MAX_CHOICES))
+ {
+ // special handling for choice 0 mapped to cancel button
+ if (m_bCanCancel && !m_supportedChoices[0] && (iControl == CONTROL_CHOICES_START))
+ {
+ if (m_iChoice != CHOICE_CANCELED)
+ {
+ std::string strHeading = m_strHeading;
+ strHeading.append(" : ");
+ strHeading.append(g_localizeStrings.Get(16024));
+ CGUIDialogBoxBase::SetHeading(CVariant{strHeading});
+ m_iChoice = CHOICE_CANCELED;
+ }
+ }
+ else
+ {
+ m_iChoice = iControl - CONTROL_CHOICES_START;
+ }
+ return true;
+ }
+ }
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogProgress::OnBack(int actionID)
+{
+ if (m_bCanCancel)
+ m_iChoice = CHOICE_CANCELED;
+ else
+ m_iChoice = CHOICE_NONE;
+
+ return true;
+}
+
+void CGUIDialogProgress::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+ CGUIControl *control = GetControl(CONTROL_PROGRESS_BAR);
+ if (control && control->GetControlType() == CGUIControl::GUICONTROL_PROGRESS)
+ {
+ // make sure we have the appropriate info set
+ CGUIProgressControl *progress = static_cast<CGUIProgressControl*>(control);
+ if (!progress->GetInfo())
+ progress->SetInfo(SYSTEM_PROGRESS_BAR);
+ }
+}
+
+void CGUIDialogProgress::SetPercentage(int iPercentage)
+{
+ if (iPercentage < 0) iPercentage = 0;
+ if (iPercentage > 100) iPercentage = 100;
+
+ if (iPercentage != m_percentage)
+ MarkDirtyRegion();
+
+ m_percentage = iPercentage;
+}
+
+void CGUIDialogProgress::SetProgressMax(int iMax)
+{
+ m_iMax=iMax;
+ m_iCurrent=0;
+}
+
+void CGUIDialogProgress::SetProgressAdvance(int nSteps/*=1*/)
+{
+ m_iCurrent+=nSteps;
+
+ if (m_iCurrent>m_iMax)
+ m_iCurrent=0;
+
+ if (m_iMax > 0)
+ SetPercentage((m_iCurrent*100)/m_iMax);
+}
+
+bool CGUIDialogProgress::Abort()
+{
+ return m_active ? IsCanceled() : false;
+}
+
+void CGUIDialogProgress::ShowProgressBar(bool bOnOff)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_showProgress = bOnOff;
+ SetInvalid();
+}
+
+bool CGUIDialogProgress::Wait(int progresstime /*= 10*/)
+{
+ CEvent m_done;
+ while (!m_done.Wait(std::chrono::milliseconds(progresstime)) && m_active && !IsCanceled())
+ Progress();
+
+ return !IsCanceled();
+}
+
+bool CGUIDialogProgress::WaitOnEvent(CEvent& event)
+{
+ while (!event.Wait(1ms))
+ {
+ if (IsCanceled())
+ return false;
+
+ Progress();
+ }
+
+ return !IsCanceled();
+}
+
+void CGUIDialogProgress::UpdateControls()
+{
+ // take a copy to save holding the lock for too long
+ bool bShowProgress;
+ bool bShowCancel;
+ std::array<bool, DIALOG_MAX_CHOICES> choices;
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ bShowProgress = m_showProgress;
+ bShowCancel = m_bCanCancel;
+ choices = m_supportedChoices;
+ }
+
+ if (bShowProgress)
+ SET_CONTROL_VISIBLE(CONTROL_PROGRESS_BAR);
+ else
+ SET_CONTROL_HIDDEN(CONTROL_PROGRESS_BAR);
+
+ bool bAllHidden = true;
+ for (int i = 0; i < DIALOG_MAX_CHOICES; ++i)
+ {
+ if (choices[i])
+ {
+ bAllHidden = false;
+ SET_CONTROL_VISIBLE(CONTROL_CHOICES_START + i);
+ }
+ else
+ SET_CONTROL_HIDDEN(CONTROL_CHOICES_START + i);
+ }
+
+ // special handling for choice 0 mapped to cancel button
+ if (bShowCancel && bAllHidden)
+ SET_CONTROL_VISIBLE(CONTROL_CHOICES_START);
+}
+
+void CGUIDialogProgress::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_bInvalidated)
+ UpdateControls();
+
+ CGUIDialogBoxBase::Process(currentTime, dirtyregions);
+}
+
+void CGUIDialogProgress::OnInitWindow()
+{
+ UpdateControls();
+
+ bool bNoFocus = true;
+ for (int i = 0; i < DIALOG_MAX_CHOICES; ++i)
+ {
+ if (m_supportedChoices[i])
+ {
+ bNoFocus = false;
+ SET_CONTROL_FOCUS(CONTROL_CHOICES_START + i, 0);
+ break;
+ }
+ }
+
+ // special handling for choice 0 mapped to cancel button
+ if (m_bCanCancel && bNoFocus)
+ SET_CONTROL_FOCUS(CONTROL_CHOICES_START,0 );
+
+ CGUIDialogBoxBase::OnInitWindow();
+}
+
+int CGUIDialogProgress::GetDefaultLabelID(int controlId) const
+{
+ // special handling for choice 0 mapped to cancel button
+ if (m_bCanCancel && !m_supportedChoices[0] && (controlId == CONTROL_CHOICES_START))
+ return 222; // Cancel
+
+ return CGUIDialogBoxBase::GetDefaultLabelID(controlId);
+}
diff --git a/xbmc/dialogs/GUIDialogProgress.h b/xbmc/dialogs/GUIDialogProgress.h
new file mode 100644
index 0000000..ba84269
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogProgress.h
@@ -0,0 +1,80 @@
+/*
+ * 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 "GUIDialogBoxBase.h"
+#include "IProgressCallback.h"
+
+#include <array>
+
+class CGUIDialogProgress :
+ public CGUIDialogBoxBase, public IProgressCallback
+{
+public:
+ CGUIDialogProgress(void);
+ ~CGUIDialogProgress(void) override;
+
+ void Reset();
+ void Open(const std::string &param = "");
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnBack(int actionID) override;
+ void OnWindowLoaded() override;
+ void Progress();
+ bool IsCanceled() const { return m_iChoice == CHOICE_CANCELED; }
+ void SetPercentage(int iPercentage);
+ int GetPercentage() const { return m_percentage; }
+ void ShowProgressBar(bool bOnOff);
+
+ void ShowChoice(int iChoice, const CVariant& label);
+
+ static constexpr int CHOICE_NONE = -2;
+ static constexpr int CHOICE_CANCELED = -1;
+ int GetChoice() const;
+
+ /*! \brief Wait for the progress dialog to be closed or canceled, while regularly
+ rendering to allow for pointer movement or progress to be shown. Used when showing
+ the progress of a process that is taking place on a separate thread and may be
+ reporting progress infrequently.
+ \param progresstime the time in ms to wait between rendering the dialog (defaults to 10ms)
+ \return true if the dialog is closed, false if the user cancels early.
+ */
+ bool Wait(int progresstime = 10);
+
+ /*! \brief Wait on an event or for the progress dialog to be canceled, while
+ regularly rendering to allow for pointer movement or progress to be shown.
+ \param event the CEvent to wait on.
+ \return true if the event completed, false if cancelled.
+ */
+ bool WaitOnEvent(CEvent& event);
+
+ // Implements IProgressCallback
+ void SetProgressMax(int iMax) override;
+ void SetProgressAdvance(int nSteps=1) override;
+ bool Abort() override;
+
+ void SetCanCancel(bool bCanCancel);
+
+protected:
+ void OnInitWindow() override;
+ int GetDefaultLabelID(int controlId) const override;
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+
+ bool m_bCanCancel;
+
+ int m_iCurrent;
+ int m_iMax;
+ int m_percentage;
+ bool m_showProgress;
+
+ std::array<bool, DIALOG_MAX_CHOICES> m_supportedChoices = {};
+ int m_iChoice = CHOICE_NONE;
+
+private:
+ void UpdateControls();
+};
diff --git a/xbmc/dialogs/GUIDialogSeekBar.cpp b/xbmc/dialogs/GUIDialogSeekBar.cpp
new file mode 100644
index 0000000..d2f462d
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSeekBar.cpp
@@ -0,0 +1,130 @@
+/*
+ * 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 "GUIDialogSeekBar.h"
+
+#include "GUIInfoManager.h"
+#include "SeekHandler.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+
+#include <cmath>
+
+#define POPUP_SEEK_PROGRESS 401
+#define POPUP_SEEK_EPG_EVENT_PROGRESS 402
+#define POPUP_SEEK_TIMESHIFT_PROGRESS 403
+
+CGUIDialogSeekBar::CGUIDialogSeekBar(void)
+ : CGUIDialog(WINDOW_DIALOG_SEEK_BAR, "DialogSeekBar.xml", DialogModalityType::MODELESS)
+{
+ m_loadType = LOAD_ON_GUI_INIT; // the application class handles our resources
+}
+
+CGUIDialogSeekBar::~CGUIDialogSeekBar(void) = default;
+
+bool CGUIDialogSeekBar::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ case GUI_MSG_WINDOW_DEINIT:
+ return CGUIDialog::OnMessage(message);
+ case GUI_MSG_ITEM_SELECT:
+ if (message.GetSenderId() == GetID() &&
+ (message.GetControlId() == POPUP_SEEK_PROGRESS ||
+ message.GetControlId() == POPUP_SEEK_EPG_EVENT_PROGRESS ||
+ message.GetControlId() == POPUP_SEEK_TIMESHIFT_PROGRESS))
+ return CGUIDialog::OnMessage(message);
+ break;
+ case GUI_MSG_REFRESH_TIMER:
+ return CGUIDialog::OnMessage(message);
+ }
+ return false; // don't process anything other than what we need!
+}
+
+void CGUIDialogSeekBar::FrameMove()
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->HasPlayer())
+ {
+ Close(true);
+ return;
+ }
+
+ int progress = GetProgress();
+ if (progress != m_lastProgress)
+ CONTROL_SELECT_ITEM(POPUP_SEEK_PROGRESS, m_lastProgress = progress);
+
+ int epgEventProgress = GetEpgEventProgress();
+ if (epgEventProgress != m_lastEpgEventProgress)
+ CONTROL_SELECT_ITEM(POPUP_SEEK_EPG_EVENT_PROGRESS, m_lastEpgEventProgress = epgEventProgress);
+
+ int timeshiftProgress = GetTimeshiftProgress();
+ if (timeshiftProgress != m_lastTimeshiftProgress)
+ CONTROL_SELECT_ITEM(POPUP_SEEK_TIMESHIFT_PROGRESS, m_lastTimeshiftProgress = timeshiftProgress);
+
+ CGUIDialog::FrameMove();
+}
+
+int CGUIDialogSeekBar::GetProgress() const
+{
+ const CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+
+ int progress = 0;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->GetSeekHandler().GetSeekSize() != 0)
+ infoMgr.GetInt(progress, PLAYER_SEEKBAR, INFO::DEFAULT_CONTEXT);
+ else
+ infoMgr.GetInt(progress, PLAYER_PROGRESS, INFO::DEFAULT_CONTEXT);
+
+ return progress;
+}
+
+int CGUIDialogSeekBar::GetEpgEventProgress() const
+{
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+
+ int progress = 0;
+ infoMgr.GetInt(progress, PVR_EPG_EVENT_PROGRESS, INFO::DEFAULT_CONTEXT);
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ int seekSize = appPlayer->GetSeekHandler().GetSeekSize();
+ if (seekSize != 0)
+ {
+ int total = 0;
+ infoMgr.GetInt(total, PVR_EPG_EVENT_DURATION, INFO::DEFAULT_CONTEXT);
+
+ float totalTime = static_cast<float>(total);
+ if (totalTime == 0.0f)
+ return 0;
+
+ float percentPerSecond = 100.0f / totalTime;
+ float percent = progress + percentPerSecond * seekSize;
+ percent = std::max(0.0f, std::min(percent, 100.0f));
+ return std::lrintf(percent);
+ }
+
+ return progress;
+}
+
+int CGUIDialogSeekBar::GetTimeshiftProgress() const
+{
+ const CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+
+ int progress = 0;
+ infoMgr.GetInt(progress, PVR_TIMESHIFT_SEEKBAR, INFO::DEFAULT_CONTEXT);
+
+ return progress;
+}
diff --git a/xbmc/dialogs/GUIDialogSeekBar.h b/xbmc/dialogs/GUIDialogSeekBar.h
new file mode 100644
index 0000000..314bf27
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSeekBar.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIDialogSeekBar : public CGUIDialog
+{
+public:
+ CGUIDialogSeekBar(void);
+ ~CGUIDialogSeekBar(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void FrameMove() override;
+private:
+ int GetProgress() const;
+ int GetEpgEventProgress() const;
+ int GetTimeshiftProgress() const;
+
+ int m_lastProgress = 0;
+ int m_lastEpgEventProgress = 0;
+ int m_lastTimeshiftProgress = 0;
+};
diff --git a/xbmc/dialogs/GUIDialogSelect.cpp b/xbmc/dialogs/GUIDialogSelect.cpp
new file mode 100644
index 0000000..0f125b2
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSelect.cpp
@@ -0,0 +1,405 @@
+/*
+ * 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 "GUIDialogSelect.h"
+
+#include "FileItem.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "utils/StringUtils.h"
+
+#define CONTROL_HEADING 1
+#define CONTROL_NUMBER_OF_ITEMS 2
+#define CONTROL_SIMPLE_LIST 3
+#define CONTROL_DETAILED_LIST 6
+#define CONTROL_EXTRA_BUTTON 5
+#define CONTROL_EXTRA_BUTTON2 8
+#define CONTROL_CANCEL_BUTTON 7
+
+CGUIDialogSelect::CGUIDialogSelect() : CGUIDialogSelect(WINDOW_DIALOG_SELECT) {}
+
+CGUIDialogSelect::CGUIDialogSelect(int windowId)
+ : CGUIDialogBoxBase(windowId, "DialogSelect.xml"),
+ m_vecList(std::make_unique<CFileItemList>()),
+ m_bButtonEnabled(false),
+ m_bButton2Enabled(false),
+ m_bButtonPressed(false),
+ m_bButton2Pressed(false),
+ m_useDetails(false),
+ m_multiSelection(false)
+{
+ m_bConfirmed = false;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogSelect::~CGUIDialogSelect(void) = default;
+
+bool CGUIDialogSelect::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ CGUIDialogBoxBase::OnMessage(message);
+
+ m_bButtonEnabled = false;
+ m_bButton2Enabled = false;
+ m_useDetails = false;
+ m_multiSelection = false;
+
+ // construct selected items list
+ m_selectedItems.clear();
+ m_selectedItem = nullptr;
+ for (int i = 0 ; i < m_vecList->Size() ; i++)
+ {
+ CFileItemPtr item = m_vecList->Get(i);
+ if (item->IsSelected())
+ {
+ m_selectedItems.push_back(i);
+ if (!m_selectedItem)
+ m_selectedItem = item;
+ }
+ }
+ m_vecList->Clear();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_bButtonPressed = false;
+ m_bButton2Pressed = false;
+ m_bConfirmed = false;
+ CGUIDialogBoxBase::OnMessage(message);
+ return true;
+ }
+ break;
+
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (m_viewControl.HasControl(CONTROL_SIMPLE_LIST))
+ {
+ int iAction = message.GetParam1();
+ if (ACTION_SELECT_ITEM == iAction || ACTION_MOUSE_LEFT_CLICK == iAction)
+ {
+ int iSelected = m_viewControl.GetSelectedItem();
+ if (iSelected >= 0 && iSelected < m_vecList->Size())
+ {
+ CFileItemPtr item(m_vecList->Get(iSelected));
+ if (m_multiSelection)
+ item->Select(!item->IsSelected());
+ else
+ {
+ for (int i = 0 ; i < m_vecList->Size() ; i++)
+ m_vecList->Get(i)->Select(false);
+ item->Select(true);
+ OnSelect(iSelected);
+ }
+ }
+ }
+ }
+ if (iControl == CONTROL_EXTRA_BUTTON2)
+ {
+ m_bButton2Pressed = true;
+ if (m_multiSelection)
+ m_bConfirmed = true;
+ Close();
+ }
+ if (iControl == CONTROL_EXTRA_BUTTON)
+ {
+ m_selectedItem = nullptr;
+ m_bButtonPressed = true;
+ if (m_multiSelection)
+ m_bConfirmed = true;
+ Close();
+ }
+ else if (iControl == CONTROL_CANCEL_BUTTON)
+ {
+ m_selectedItem = nullptr;
+ m_vecList->Clear();
+ m_selectedItems.clear();
+ m_bConfirmed = false;
+ Close();
+ }
+ }
+ break;
+ case GUI_MSG_SETFOCUS:
+ {
+ if (m_viewControl.HasControl(message.GetControlId()))
+ {
+ if (m_vecList->IsEmpty())
+ {
+ if (m_bButtonEnabled)
+ SET_CONTROL_FOCUS(CONTROL_EXTRA_BUTTON, 0);
+ else
+ SET_CONTROL_FOCUS(CONTROL_CANCEL_BUTTON, 0);
+ return true;
+ }
+ if (m_viewControl.GetCurrentControl() != message.GetControlId())
+ {
+ m_viewControl.SetFocused();
+ return true;
+ }
+ }
+ }
+ break;
+ }
+
+ return CGUIDialogBoxBase::OnMessage(message);
+}
+
+void CGUIDialogSelect::OnSelect(int idx)
+{
+ m_bConfirmed = true;
+ Close();
+}
+
+bool CGUIDialogSelect::OnBack(int actionID)
+{
+ m_selectedItem = nullptr;
+ m_vecList->Clear();
+ m_selectedItems.clear();
+ m_bConfirmed = false;
+ return CGUIDialogBoxBase::OnBack(actionID);
+}
+
+void CGUIDialogSelect::Reset()
+{
+ m_bButtonEnabled = false;
+ m_bButtonPressed = false;
+ m_bButton2Enabled = false;
+ m_bButton2Pressed = false;
+
+ m_useDetails = false;
+ m_multiSelection = false;
+ m_focusToButton = false;
+ m_selectedItem = nullptr;
+ m_vecList->Clear();
+ m_selectedItems.clear();
+}
+
+int CGUIDialogSelect::Add(const std::string& strLabel)
+{
+ CFileItemPtr pItem(new CFileItem(strLabel));
+ m_vecList->Add(pItem);
+ return m_vecList->Size() - 1;
+}
+
+int CGUIDialogSelect::Add(const CFileItem& item)
+{
+ m_vecList->Add(CFileItemPtr(new CFileItem(item)));
+ return m_vecList->Size() - 1;
+}
+
+void CGUIDialogSelect::SetItems(const CFileItemList& pList)
+{
+ // need to make internal copy of list to be sure dialog is owner of it
+ m_vecList->Clear();
+ m_vecList->Copy(pList);
+
+ m_viewControl.SetItems(*m_vecList);
+}
+
+int CGUIDialogSelect::GetSelectedItem() const
+{
+ return m_selectedItems.size() > 0 ? m_selectedItems[0] : -1;
+}
+
+const CFileItemPtr CGUIDialogSelect::GetSelectedFileItem() const
+{
+ if (m_selectedItem)
+ return m_selectedItem;
+ return CFileItemPtr(new CFileItem);
+}
+
+const std::vector<int>& CGUIDialogSelect::GetSelectedItems() const
+{
+ return m_selectedItems;
+}
+
+void CGUIDialogSelect::EnableButton(bool enable, int label)
+{
+ m_bButtonEnabled = enable;
+ m_buttonLabel = g_localizeStrings.Get(label);
+}
+
+void CGUIDialogSelect::EnableButton(bool enable, const std::string& label)
+{
+ m_bButtonEnabled = enable;
+ m_buttonLabel = label;
+}
+
+void CGUIDialogSelect::EnableButton2(bool enable, int label)
+{
+ m_bButton2Enabled = enable;
+ m_button2Label = g_localizeStrings.Get(label);
+}
+
+void CGUIDialogSelect::EnableButton2(bool enable, const std::string& label)
+{
+ m_bButton2Enabled = enable;
+ m_button2Label = label;
+}
+
+bool CGUIDialogSelect::IsButtonPressed()
+{
+ return m_bButtonPressed;
+}
+
+bool CGUIDialogSelect::IsButton2Pressed()
+{
+ return m_bButton2Pressed;
+}
+
+void CGUIDialogSelect::Sort(bool bSortOrder /*=true*/)
+{
+ m_vecList->Sort(SortByLabel, bSortOrder ? SortOrderAscending : SortOrderDescending);
+}
+
+void CGUIDialogSelect::SetSelected(int iSelected)
+{
+ if (iSelected < 0 || iSelected >= m_vecList->Size() ||
+ m_vecList->Get(iSelected).get() == NULL)
+ return;
+
+ // only set m_iSelected if there is no multi-select
+ // or if it doesn't have a valid value yet
+ // or if the current value is bigger than the new one
+ // so that we always focus the item nearest to the beginning of the list
+ if (!m_multiSelection || !m_selectedItem ||
+ (!m_selectedItems.empty() && m_selectedItems.back() > iSelected))
+ m_selectedItem = m_vecList->Get(iSelected);
+ m_vecList->Get(iSelected)->Select(true);
+ m_selectedItems.push_back(iSelected);
+}
+
+void CGUIDialogSelect::SetSelected(const std::string &strSelectedLabel)
+{
+ for (int index = 0; index < m_vecList->Size(); index++)
+ {
+ if (strSelectedLabel == m_vecList->Get(index)->GetLabel())
+ {
+ SetSelected(index);
+ return;
+ }
+ }
+}
+
+void CGUIDialogSelect::SetSelected(const std::vector<int>& selectedIndexes)
+{
+ for (auto i : selectedIndexes)
+ SetSelected(i);
+}
+
+void CGUIDialogSelect::SetSelected(const std::vector<std::string> &selectedLabels)
+{
+ for (const auto& label : selectedLabels)
+ SetSelected(label);
+}
+
+void CGUIDialogSelect::SetUseDetails(bool useDetails)
+{
+ m_useDetails = useDetails;
+}
+
+void CGUIDialogSelect::SetMultiSelection(bool multiSelection)
+{
+ m_multiSelection = multiSelection;
+}
+
+void CGUIDialogSelect::SetButtonFocus(bool buttonFocus)
+{
+ m_focusToButton = buttonFocus;
+}
+
+CGUIControl *CGUIDialogSelect::GetFirstFocusableControl(int id)
+{
+ if (m_viewControl.HasControl(id))
+ id = m_viewControl.GetCurrentControl();
+ return CGUIDialogBoxBase::GetFirstFocusableControl(id);
+}
+
+void CGUIDialogSelect::OnWindowLoaded()
+{
+ CGUIDialogBoxBase::OnWindowLoaded();
+ m_viewControl.Reset();
+ m_viewControl.SetParentWindow(GetID());
+ m_viewControl.AddView(GetControl(CONTROL_SIMPLE_LIST));
+ m_viewControl.AddView(GetControl(CONTROL_DETAILED_LIST));
+}
+
+void CGUIDialogSelect::OnInitWindow()
+{
+ m_viewControl.SetItems(*m_vecList);
+ m_selectedItems.clear();
+ for(int i = 0 ; i < m_vecList->Size(); i++)
+ {
+ auto item = m_vecList->Get(i);
+ if (item->IsSelected())
+ {
+ m_selectedItems.push_back(i);
+ if (m_selectedItem == nullptr)
+ m_selectedItem = item;
+ }
+ }
+ m_viewControl.SetCurrentView(m_useDetails ? CONTROL_DETAILED_LIST : CONTROL_SIMPLE_LIST);
+
+ SET_CONTROL_LABEL(CONTROL_NUMBER_OF_ITEMS,
+ StringUtils::Format("{} {}", m_vecList->Size(), g_localizeStrings.Get(127)));
+
+ if (m_multiSelection)
+ EnableButton(true, 186);
+
+ if (m_bButtonEnabled)
+ {
+ SET_CONTROL_LABEL(CONTROL_EXTRA_BUTTON, m_buttonLabel);
+ SET_CONTROL_VISIBLE(CONTROL_EXTRA_BUTTON);
+ }
+ else
+ SET_CONTROL_HIDDEN(CONTROL_EXTRA_BUTTON);
+
+ if (m_bButton2Enabled)
+ {
+ SET_CONTROL_LABEL(CONTROL_EXTRA_BUTTON2, m_button2Label);
+ SET_CONTROL_VISIBLE(CONTROL_EXTRA_BUTTON2);
+ }
+ else
+ SET_CONTROL_HIDDEN(CONTROL_EXTRA_BUTTON2);
+
+ SET_CONTROL_LABEL(CONTROL_CANCEL_BUTTON, g_localizeStrings.Get(222));
+
+ CGUIDialogBoxBase::OnInitWindow();
+
+ // focus one of the buttons if explicitly requested
+ // ATTENTION: this must be done after calling CGUIDialogBoxBase::OnInitWindow()
+ if (m_focusToButton)
+ {
+ if (m_bButtonEnabled)
+ SET_CONTROL_FOCUS(CONTROL_EXTRA_BUTTON, 0);
+ else
+ SET_CONTROL_FOCUS(CONTROL_CANCEL_BUTTON, 0);
+ }
+
+ // if nothing is selected, select first item
+ m_viewControl.SetSelectedItem(std::max(GetSelectedItem(), 0));
+}
+
+void CGUIDialogSelect::OnDeinitWindow(int nextWindowID)
+{
+ m_viewControl.Clear();
+ CGUIDialogBoxBase::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIDialogSelect::OnWindowUnload()
+{
+ CGUIDialogBoxBase::OnWindowUnload();
+ m_viewControl.Reset();
+}
diff --git a/xbmc/dialogs/GUIDialogSelect.h b/xbmc/dialogs/GUIDialogSelect.h
new file mode 100644
index 0000000..7edb493
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSelect.h
@@ -0,0 +1,76 @@
+/*
+ * 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 "GUIDialogBoxBase.h"
+#include "view/GUIViewControl.h"
+
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CFileItemList;
+
+class CGUIDialogSelect : public CGUIDialogBoxBase
+{
+public:
+ CGUIDialogSelect();
+ ~CGUIDialogSelect(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnBack(int actionID) override;
+
+ void Reset();
+ int Add(const std::string& strLabel);
+ int Add(const CFileItem& item);
+ void SetItems(const CFileItemList& items);
+ const CFileItemPtr GetSelectedFileItem() const;
+ int GetSelectedItem() const;
+ const std::vector<int>& GetSelectedItems() const;
+ void EnableButton(bool enable, int label);
+ void EnableButton(bool enable, const std::string& label);
+ void EnableButton2(bool enable, int label);
+ void EnableButton2(bool enable, const std::string& label);
+ bool IsButtonPressed();
+ bool IsButton2Pressed();
+ void Sort(bool bSortOrder = true);
+ void SetSelected(int iSelected);
+ void SetSelected(const std::string &strSelectedLabel);
+ void SetSelected(const std::vector<int>& selectedIndexes);
+ void SetSelected(const std::vector<std::string> &selectedLabels);
+ void SetUseDetails(bool useDetails);
+ void SetMultiSelection(bool multiSelection);
+ void SetButtonFocus(bool buttonFocus);
+
+protected:
+ explicit CGUIDialogSelect(int windowid);
+ CGUIControl *GetFirstFocusableControl(int id) override;
+ void OnWindowLoaded() override;
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ void OnWindowUnload() override;
+
+ virtual void OnSelect(int idx);
+
+ CFileItemPtr m_selectedItem;
+ std::unique_ptr<CFileItemList> m_vecList;
+ CGUIViewControl m_viewControl;
+
+private:
+ bool m_bButtonEnabled;
+ bool m_bButton2Enabled;
+ bool m_bButtonPressed;
+ bool m_bButton2Pressed;
+ std::string m_buttonLabel;
+ std::string m_button2Label;
+ bool m_useDetails;
+ bool m_multiSelection;
+ bool m_focusToButton{};
+
+ std::vector<int> m_selectedItems;
+};
diff --git a/xbmc/dialogs/GUIDialogSimpleMenu.cpp b/xbmc/dialogs/GUIDialogSimpleMenu.cpp
new file mode 100644
index 0000000..0d4d11a
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSimpleMenu.cpp
@@ -0,0 +1,154 @@
+/*
+ * 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 "GUIDialogSimpleMenu.h"
+
+#include "FileItem.h"
+#include "GUIDialogSelect.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "settings/DiscSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/IRunnable.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+namespace
+{
+class CGetDirectoryItems : public IRunnable
+{
+public:
+ CGetDirectoryItems(const std::string &path, CFileItemList &items, const XFILE::CDirectory::CHints &hints)
+ : m_path(path), m_items(items), m_hints(hints)
+ {
+ }
+ void Run() override
+ {
+ m_result = XFILE::CDirectory::GetDirectory(m_path, m_items, m_hints);
+ }
+ bool m_result;
+protected:
+ std::string m_path;
+ CFileItemList &m_items;
+ XFILE::CDirectory::CHints m_hints;
+};
+}
+
+
+bool CGUIDialogSimpleMenu::ShowPlaySelection(CFileItem& item)
+{
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_DISC_PLAYBACK) != BD_PLAYBACK_SIMPLE_MENU)
+ return true;
+
+ if (item.IsBDFile())
+ {
+ std::string root = URIUtils::GetParentPath(item.GetDynPath());
+ URIUtils::RemoveSlashAtEnd(root);
+ if (URIUtils::GetFileName(root) == "BDMV")
+ {
+ CURL url("bluray://");
+ url.SetHostName(URIUtils::GetParentPath(root));
+ url.SetFileName("root");
+ return ShowPlaySelection(item, url.Get());
+ }
+ }
+
+ if (item.IsDiscImage())
+ {
+ CURL url2("udf://");
+ url2.SetHostName(item.GetDynPath());
+ url2.SetFileName("BDMV/index.bdmv");
+ if (CFileUtils::Exists(url2.Get()))
+ {
+ url2.SetFileName("");
+
+ CURL url("bluray://");
+ url.SetHostName(url2.Get());
+ url.SetFileName("root");
+ return ShowPlaySelection(item, url.Get());
+ }
+ }
+ return true;
+}
+
+bool CGUIDialogSimpleMenu::ShowPlaySelection(CFileItem& item, const std::string& directory)
+{
+
+ CFileItemList items;
+
+ if (!GetDirectoryItems(directory, items, XFILE::CDirectory::CHints()))
+ {
+ CLog::Log(LOGERROR,
+ "CGUIWindowVideoBase::ShowPlaySelection - Failed to get play directory for {}",
+ directory);
+ return true;
+ }
+
+ if (items.IsEmpty())
+ {
+ CLog::Log(LOGERROR, "CGUIWindowVideoBase::ShowPlaySelection - Failed to get any items {}",
+ directory);
+ return true;
+ }
+
+ CGUIDialogSelect* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ while (true)
+ {
+ dialog->Reset();
+ dialog->SetHeading(CVariant{25006}); // Select playback item
+ dialog->SetItems(items);
+ dialog->SetUseDetails(true);
+ dialog->Open();
+
+ CFileItemPtr item_new = dialog->GetSelectedFileItem();
+ if (!item_new || dialog->GetSelectedItem() < 0)
+ {
+ CLog::Log(LOGDEBUG, "CGUIWindowVideoBase::ShowPlaySelection - User aborted {}", directory);
+ break;
+ }
+
+ if (item_new->m_bIsFolder == false)
+ {
+ std::string original_path = item.GetDynPath();
+ item.SetDynPath(item_new->GetDynPath());
+ item.SetProperty("get_stream_details_from_player", true);
+ item.SetProperty("original_listitem_url", original_path);
+ return true;
+ }
+
+ items.Clear();
+ if (!GetDirectoryItems(item_new->GetDynPath(), items, XFILE::CDirectory::CHints()) || items.IsEmpty())
+ {
+ CLog::Log(LOGERROR, "CGUIWindowVideoBase::ShowPlaySelection - Failed to get any items {}",
+ item_new->GetPath());
+ break;
+ }
+ }
+
+ return false;
+}
+
+bool CGUIDialogSimpleMenu::GetDirectoryItems(const std::string &path, CFileItemList &items,
+ const XFILE::CDirectory::CHints &hints)
+{
+ CGetDirectoryItems getItems(path, items, hints);
+ if (!CGUIDialogBusy::Wait(&getItems, 100, true))
+ {
+ return false;
+ }
+ return getItems.m_result;
+}
diff --git a/xbmc/dialogs/GUIDialogSimpleMenu.h b/xbmc/dialogs/GUIDialogSimpleMenu.h
new file mode 100644
index 0000000..ed476a4
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSimpleMenu.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "filesystem/Directory.h"
+
+#include <string>
+
+class CFileItem;
+class CFileItemList;
+
+class CGUIDialogSimpleMenu
+{
+public:
+
+ /*! \brief Show dialog allowing selection of wanted playback item */
+ static bool ShowPlaySelection(CFileItem& item);
+ static bool ShowPlaySelection(CFileItem& item, const std::string& directory);
+
+protected:
+ static bool GetDirectoryItems(const std::string &path, CFileItemList &items, const XFILE::CDirectory::CHints &hints);
+};
diff --git a/xbmc/dialogs/GUIDialogSlider.cpp b/xbmc/dialogs/GUIDialogSlider.cpp
new file mode 100644
index 0000000..5d9fb1d
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSlider.cpp
@@ -0,0 +1,125 @@
+/*
+ * 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 "GUIDialogSlider.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUISliderControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+
+#define CONTROL_HEADING 10
+#define CONTROL_SLIDER 11
+#define CONTROL_LABEL 12
+
+CGUIDialogSlider::CGUIDialogSlider(void)
+ : CGUIDialog(WINDOW_DIALOG_SLIDER, "DialogSlider.xml")
+{
+ m_callback = NULL;
+ m_callbackData = NULL;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogSlider::~CGUIDialogSlider(void) = default;
+
+bool CGUIDialogSlider::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_SELECT_ITEM)
+ {
+ Close();
+ return true;
+ }
+ return CGUIDialog::OnAction(action);
+}
+
+bool CGUIDialogSlider::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_CLICKED:
+ if (message.GetSenderId() == CONTROL_SLIDER)
+ {
+ CGUISliderControl *slider = dynamic_cast<CGUISliderControl *>(GetControl(CONTROL_SLIDER));
+ if (slider && m_callback)
+ {
+ m_callback->OnSliderChange(m_callbackData, slider);
+ SET_CONTROL_LABEL(CONTROL_LABEL, slider->GetDescription());
+ }
+ }
+ break;
+ case GUI_MSG_WINDOW_DEINIT:
+ m_callback = NULL;
+ m_callbackData = NULL;
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogSlider::SetSlider(const std::string &label, float value, float min, float delta, float max, ISliderCallback *callback, void *callbackData)
+{
+ SET_CONTROL_LABEL(CONTROL_HEADING, label);
+ CGUISliderControl *slider = dynamic_cast<CGUISliderControl *>(GetControl(CONTROL_SLIDER));
+ m_callback = callback;
+ m_callbackData = callbackData;
+ if (slider)
+ {
+ slider->SetType(SLIDER_CONTROL_TYPE_FLOAT);
+ slider->SetFloatRange(min, max);
+ slider->SetFloatInterval(delta);
+ slider->SetFloatValue(value);
+ if (m_callback)
+ {
+ m_callback->OnSliderChange(m_callbackData, slider);
+ SET_CONTROL_LABEL(CONTROL_LABEL, slider->GetDescription());
+ }
+ }
+}
+
+void CGUIDialogSlider::OnWindowLoaded()
+{
+ // ensure our callbacks are NULL, incase we were loaded via some non-standard means
+ m_callback = NULL;
+ m_callbackData = NULL;
+ CGUIDialog::OnWindowLoaded();
+}
+
+void CGUIDialogSlider::SetModalityType(DialogModalityType type)
+{
+ m_modalityType = type;
+}
+
+void CGUIDialogSlider::ShowAndGetInput(const std::string &label, float value, float min, float delta, float max, ISliderCallback *callback, void *callbackData)
+{
+ // grab the slider dialog
+ CGUIDialogSlider *slider = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSlider>(WINDOW_DIALOG_SLIDER);
+ if (!slider)
+ return;
+
+ // set the label and value
+ slider->Initialize();
+ slider->SetSlider(label, value, min, delta, max, callback, callbackData);
+ slider->SetModalityType(DialogModalityType::MODAL);
+ slider->Open();
+}
+
+void CGUIDialogSlider::Display(int label, float value, float min, float delta, float max, ISliderCallback *callback)
+{
+ // grab the slider dialog
+ CGUIDialogSlider *slider = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSlider>(WINDOW_DIALOG_SLIDER);
+ if (!slider)
+ return;
+
+ // set the label and value
+ slider->Initialize();
+ slider->SetAutoClose(1000);
+ slider->SetSlider(g_localizeStrings.Get(label), value, min, delta, max, callback, NULL);
+ slider->SetModalityType(DialogModalityType::MODELESS);
+ slider->Open();
+}
diff --git a/xbmc/dialogs/GUIDialogSlider.h b/xbmc/dialogs/GUIDialogSlider.h
new file mode 100644
index 0000000..5d09a90
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSlider.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 "guilib/GUIDialog.h"
+#include "guilib/ISliderCallback.h"
+
+class CGUIDialogSlider : public CGUIDialog
+{
+public:
+ CGUIDialogSlider();
+ ~CGUIDialogSlider(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+
+ void SetModalityType(DialogModalityType type);
+
+ /*! \brief Show the slider dialog and wait for the user to change the value
+ Shows the slider until the user is happy with the adjusted value. Calls back with each change to the callback function
+ allowing changes to take place immediately.
+ \param label description of what is being changed by the slider
+ \param value start value of the slider
+ \param min minimal value the slider may take
+ \param delta amount the slider advances for a single click
+ \param max maximal value the slider may take
+ \param callback callback class that implements ISliderCallback::OnSliderChange
+ \param callbackData pointer to callback-specific data (defaults to NULL)
+ \sa ISliderCallback, Display
+ */
+ static void ShowAndGetInput(const std::string &label, float value, float min, float delta, float max, ISliderCallback *callback, void *callbackData = NULL);
+
+ /*! \brief Show the slider dialog as a response to user input
+ Shows the slider with the given values for a short period of time, used for UI feedback of a set user action.
+ This function is asynchronous.
+ \param label id of the description label for the slider
+ \param value start value of the slider
+ \param min minimal value the slider may take
+ \param delta amount the slider advances for a single click
+ \param max maximal value the slider may take
+ \param callback callback class that implements ISliderCallback::OnSliderChange
+ \sa ISliderCallback, ShowAndGetInput
+ */
+ static void Display(int label, float value, float min, float delta, float max, ISliderCallback *callback);
+protected:
+ void SetSlider(const std::string &label, float value, float min, float delta, float max, ISliderCallback *callback, void *callbackData);
+ void OnWindowLoaded() override;
+
+ ISliderCallback *m_callback;
+ void *m_callbackData;
+};
+
diff --git a/xbmc/dialogs/GUIDialogSmartPlaylistEditor.cpp b/xbmc/dialogs/GUIDialogSmartPlaylistEditor.cpp
new file mode 100644
index 0000000..49d6398
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSmartPlaylistEditor.cpp
@@ -0,0 +1,648 @@
+/*
+ * 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 "GUIDialogSmartPlaylistEditor.h"
+
+#include "FileItem.h"
+#include "GUIDialogContextMenu.h"
+#include "GUIDialogSelect.h"
+#include "GUIDialogSmartPlaylistRule.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "filesystem/File.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "profiles/ProfileManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+#include <utility>
+
+#define CONTROL_HEADING 2
+#define CONTROL_RULE_LIST 10
+#define CONTROL_NAME 12
+#define CONTROL_RULE_ADD 13
+#define CONTROL_RULE_REMOVE 14
+#define CONTROL_RULE_EDIT 15
+#define CONTROL_MATCH 16
+#define CONTROL_LIMIT 17
+#define CONTROL_ORDER_FIELD 18
+#define CONTROL_ORDER_DIRECTION 19
+#define CONTROL_GROUP_BY 23
+#define CONTROL_GROUP_MIXED 24
+
+#define CONTROL_OK 20
+#define CONTROL_CANCEL 21
+#define CONTROL_TYPE 22
+
+typedef struct
+{
+ CGUIDialogSmartPlaylistEditor::PLAYLIST_TYPE type;
+ char string[13];
+ int localizedString;
+} translateType;
+
+static const translateType types[] = { { CGUIDialogSmartPlaylistEditor::TYPE_SONGS, "songs", 134 },
+ { CGUIDialogSmartPlaylistEditor::TYPE_ALBUMS, "albums", 132 },
+ { CGUIDialogSmartPlaylistEditor::TYPE_ARTISTS, "artists", 133 },
+ { CGUIDialogSmartPlaylistEditor::TYPE_MIXED, "mixed", 20395 },
+ { CGUIDialogSmartPlaylistEditor::TYPE_MUSICVIDEOS, "musicvideos", 20389 },
+ { CGUIDialogSmartPlaylistEditor::TYPE_MOVIES, "movies", 20342 },
+ { CGUIDialogSmartPlaylistEditor::TYPE_TVSHOWS, "tvshows", 20343 },
+ { CGUIDialogSmartPlaylistEditor::TYPE_EPISODES, "episodes", 20360 }
+ };
+
+CGUIDialogSmartPlaylistEditor::CGUIDialogSmartPlaylistEditor(void)
+ : CGUIDialog(WINDOW_DIALOG_SMART_PLAYLIST_EDITOR, "SmartPlaylistEditor.xml")
+{
+ m_cancelled = false;
+ m_ruleLabels = new CFileItemList;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogSmartPlaylistEditor::~CGUIDialogSmartPlaylistEditor()
+{
+ delete m_ruleLabels;
+}
+
+bool CGUIDialogSmartPlaylistEditor::OnBack(int actionID)
+{
+ m_cancelled = true;
+ return CGUIDialog::OnBack(actionID);
+}
+
+bool CGUIDialogSmartPlaylistEditor::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ int iAction = message.GetParam1();
+ if (iControl == CONTROL_RULE_LIST && (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK))
+ OnRuleList(GetSelectedItem());
+ else if (iControl == CONTROL_RULE_ADD)
+ OnRuleAdd();
+ else if (iControl == CONTROL_RULE_EDIT)
+ OnRuleList(GetSelectedItem());
+ else if (iControl == CONTROL_RULE_REMOVE)
+ OnRuleRemove(GetSelectedItem());
+ else if (iControl == CONTROL_NAME)
+ OnName();
+ else if (iControl == CONTROL_OK)
+ OnOK();
+ else if (iControl == CONTROL_CANCEL)
+ OnCancel();
+ else if (iControl == CONTROL_MATCH)
+ OnMatch();
+ else if (iControl == CONTROL_LIMIT)
+ OnLimit();
+ else if (iControl == CONTROL_ORDER_FIELD)
+ OnOrder();
+ else if (iControl == CONTROL_ORDER_DIRECTION)
+ OnOrderDirection();
+ else if (iControl == CONTROL_TYPE)
+ OnType();
+ else if (iControl == CONTROL_GROUP_BY)
+ OnGroupBy();
+ else if (iControl == CONTROL_GROUP_MIXED)
+ OnGroupMixed();
+ else if (iControl == CONTROL_RULE_LIST && (iAction == ACTION_CONTEXT_MENU || iAction == ACTION_MOUSE_RIGHT_CLICK))
+ OnPopupMenu(GetSelectedItem());
+ else
+ return CGUIDialog::OnMessage(message);
+ return true;
+ }
+ break;
+ case GUI_MSG_FOCUSED:
+ if (message.GetControlId() == CONTROL_RULE_REMOVE ||
+ message.GetControlId() == CONTROL_RULE_EDIT)
+ HighlightItem(GetSelectedItem());
+ else
+ {
+ if (message.GetControlId() == CONTROL_RULE_LIST)
+ UpdateRuleControlButtons();
+
+ HighlightItem(-1);
+ }
+ break;
+ case GUI_MSG_WINDOW_INIT:
+ {
+ const std::string& startupList = message.GetStringParam(0);
+ if (!startupList.empty())
+ {
+ int party = 0;
+ if (URIUtils::PathEquals(startupList, CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetUserDataItem("PartyMode.xsp")))
+ party = 1;
+ else if (URIUtils::PathEquals(startupList, CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetUserDataItem("PartyMode-Video.xsp")))
+ party = 2;
+
+ if ((party && !XFILE::CFile::Exists(startupList)) ||
+ m_playlist.Load(startupList))
+ {
+ m_path = startupList;
+
+ if (party == 1)
+ m_mode = "partymusic";
+ else if (party == 2)
+ m_mode = "partyvideo";
+ else
+ {
+ PLAYLIST_TYPE type = ConvertType(m_playlist.GetType());
+ if (type == TYPE_SONGS || type == TYPE_ALBUMS || type == TYPE_ARTISTS)
+ m_mode = "music";
+ else
+ m_mode = "video";
+ }
+ }
+ else
+ return false;
+ }
+ }
+ break;
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ m_playlist.Reset();
+ }
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogSmartPlaylistEditor::OnPopupMenu(int item)
+{
+ if (item < 0 || static_cast<size_t>(item) >= m_playlist.m_ruleCombination.m_rules.size())
+ return;
+ // highlight the item
+ m_ruleLabels->Get(item)->Select(true);
+
+ CContextButtons choices;
+ choices.Add(1, 15015);
+
+ int button = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+
+ // unhighlight the item
+ m_ruleLabels->Get(item)->Select(false);
+
+ if (button == 1)
+ OnRuleRemove(item);
+}
+
+void CGUIDialogSmartPlaylistEditor::OnRuleList(int item)
+{
+ if (item < 0 || item > static_cast<int>(m_playlist.m_ruleCombination.m_rules.size()))
+ return;
+ if (item == static_cast<int>(m_playlist.m_ruleCombination.m_rules.size()))
+ OnRuleAdd();
+ else
+ {
+ CSmartPlaylistRule rule = *std::static_pointer_cast<CSmartPlaylistRule>(m_playlist.m_ruleCombination.m_rules[item]);
+ if (CGUIDialogSmartPlaylistRule::EditRule(rule, m_playlist.GetType()))
+ *m_playlist.m_ruleCombination.m_rules[item] = rule;
+ }
+ UpdateButtons();
+}
+
+void CGUIDialogSmartPlaylistEditor::OnOK()
+{
+ std::string systemPlaylistsPath = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH);
+ // save our playlist
+ if (m_path.empty())
+ {
+ std::string filename(CUtil::MakeLegalFileName(m_playlist.m_playlistName));
+ std::string path;
+ if (CGUIKeyboardFactory::ShowAndGetInput(filename, CVariant{g_localizeStrings.Get(16013)}, false))
+ {
+ path = URIUtils::AddFileToFolder(systemPlaylistsPath, m_playlist.GetSaveLocation(),
+ CUtil::MakeLegalFileName(filename));
+ }
+ else
+ return;
+ if (!URIUtils::HasExtension(path, ".xsp"))
+ path += ".xsp";
+
+ // should we check whether we should overwrite?
+ m_path = path;
+ }
+ else
+ {
+ // check if we need to actually change the save location for this playlist
+ // this occurs if the user switches from music video <> songs <> mixed
+ if (StringUtils::StartsWith(m_path, systemPlaylistsPath))
+ {
+ std::string filename = URIUtils::GetFileName(m_path);
+ std::string strFolder = m_path.substr(systemPlaylistsPath.size(), m_path.size() - filename.size() - systemPlaylistsPath.size() - 1);
+ if (strFolder != m_playlist.GetSaveLocation())
+ { // move to the correct folder
+ XFILE::CFile::Delete(m_path);
+ m_path = URIUtils::AddFileToFolder(systemPlaylistsPath, m_playlist.GetSaveLocation(), filename);
+ }
+ }
+ }
+
+ m_playlist.Save(m_path);
+
+ m_cancelled = false;
+ Close();
+}
+
+void CGUIDialogSmartPlaylistEditor::OnCancel()
+{
+ m_cancelled = true;
+ Close();
+}
+
+void CGUIDialogSmartPlaylistEditor::OnMatch()
+{
+ // toggle between AND and OR setting
+ if (m_playlist.m_ruleCombination.GetType() == CSmartPlaylistRuleCombination::CombinationOr)
+ m_playlist.m_ruleCombination.SetType(CSmartPlaylistRuleCombination::CombinationAnd);
+ else
+ m_playlist.m_ruleCombination.SetType(CSmartPlaylistRuleCombination::CombinationOr);
+ UpdateButtons();
+}
+
+void CGUIDialogSmartPlaylistEditor::OnName()
+{
+ std::string name = m_playlist.m_playlistName;
+ if (CGUIKeyboardFactory::ShowAndGetInput(name, CVariant{16012}, false))
+ {
+ m_playlist.m_playlistName = name;
+ UpdateButtons();
+ }
+}
+
+void CGUIDialogSmartPlaylistEditor::OnLimit()
+{
+ std::vector<int> limits = {0, 10, 25, 50, 100, 250, 500, 1000};
+ CGUIDialogSelect* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ dialog->Reset();
+ int selected = -1;
+ for (auto limit = limits.begin(); limit != limits.end(); limit++)
+ {
+ if (*limit == static_cast<int>(m_playlist.m_limit))
+ selected = std::distance(limits.begin(), limit);
+ if (*limit == 0)
+ dialog->Add(g_localizeStrings.Get(21428));
+ else
+ dialog->Add(StringUtils::Format(g_localizeStrings.Get(21436), *limit));
+ }
+ dialog->SetHeading(CVariant{ 21427 });
+ dialog->SetSelected(selected);
+ dialog->Open();
+ int newSelected = dialog->GetSelectedItem();
+ if (!dialog->IsConfirmed() || newSelected < 0 || limits[newSelected] == static_cast<int>(m_playlist.m_limit))
+ return;
+ m_playlist.m_limit = limits[newSelected];
+ UpdateButtons();
+}
+
+void CGUIDialogSmartPlaylistEditor::OnType()
+{
+ std::vector<PLAYLIST_TYPE> allowedTypes = GetAllowedTypes(m_mode);
+ CGUIDialogSelect* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ dialog->Reset();
+ for (auto allowedType: allowedTypes)
+ dialog->Add(GetLocalizedType(allowedType));
+ dialog->SetHeading(CVariant{ 564 });
+ dialog->SetSelected(GetLocalizedType(ConvertType(m_playlist.GetType())));
+ dialog->Open();
+ int newSelected = dialog->GetSelectedItem();
+ if (!dialog->IsConfirmed() || newSelected < 0 || allowedTypes[newSelected] == ConvertType(m_playlist.GetType()))
+ return;
+
+ m_playlist.SetType(ConvertType(allowedTypes[newSelected]));
+
+ // Remove any invalid grouping left over when changing the type
+ Field currentGroup = CSmartPlaylistRule::TranslateGroup(m_playlist.GetGroup().c_str());
+ if (currentGroup != FieldNone && currentGroup != FieldUnknown)
+ {
+ std::vector<Field> groups = CSmartPlaylistRule::GetGroups(m_playlist.GetType());
+ if (std::find(groups.begin(), groups.end(), currentGroup) == groups.end())
+ m_playlist.SetGroup(CSmartPlaylistRule::TranslateGroup(FieldUnknown));
+ }
+
+ UpdateButtons();
+}
+
+void CGUIDialogSmartPlaylistEditor::OnOrder()
+{
+ std::vector<SortBy> orders = CSmartPlaylistRule::GetOrders(m_playlist.GetType());
+ CGUIDialogSelect* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ dialog->Reset();
+ for (auto order: orders)
+ dialog->Add(g_localizeStrings.Get(SortUtils::GetSortLabel(order)));
+ dialog->SetHeading(CVariant{ 21429 });
+ dialog->SetSelected(g_localizeStrings.Get(SortUtils::GetSortLabel(m_playlist.m_orderField)));
+ dialog->Open();
+ int newSelected = dialog->GetSelectedItem();
+ if (!dialog->IsConfirmed() || newSelected < 0 || orders[newSelected] == m_playlist.m_orderField)
+ return;
+ m_playlist.m_orderField = orders[newSelected];
+ UpdateButtons();
+}
+
+void CGUIDialogSmartPlaylistEditor::OnOrderDirection()
+{
+ if (m_playlist.m_orderDirection == SortOrderDescending)
+ m_playlist.m_orderDirection = SortOrderAscending;
+ else
+ m_playlist.m_orderDirection = SortOrderDescending;
+ UpdateButtons();
+}
+
+void CGUIDialogSmartPlaylistEditor::OnGroupBy()
+{
+ std::vector<Field> groups = CSmartPlaylistRule::GetGroups(m_playlist.GetType());
+ Field currentGroup = CSmartPlaylistRule::TranslateGroup(m_playlist.GetGroup().c_str());
+ CGUIDialogSelect* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ dialog->Reset();
+ for (auto group : groups)
+ dialog->Add(CSmartPlaylistRule::GetLocalizedGroup(group));
+ dialog->SetHeading(CVariant{ 21458 });
+ dialog->SetSelected(CSmartPlaylistRule::GetLocalizedGroup(currentGroup));
+ dialog->Open();
+ int newSelected = dialog->GetSelectedItem();
+ // check if selection has changed
+ if (!dialog->IsConfirmed() || newSelected < 0 || groups[newSelected] == currentGroup)
+ return;
+ m_playlist.SetGroup(CSmartPlaylistRule::TranslateGroup(groups[newSelected]));
+
+ if (m_playlist.IsGroupMixed() && !CSmartPlaylistRule::CanGroupMix(currentGroup))
+ m_playlist.SetGroupMixed(false);
+
+ UpdateButtons();
+}
+
+void CGUIDialogSmartPlaylistEditor::OnGroupMixed()
+{
+ m_playlist.SetGroupMixed(!m_playlist.IsGroupMixed());
+ UpdateButtons();
+}
+
+void CGUIDialogSmartPlaylistEditor::UpdateButtons()
+{
+ CONTROL_ENABLE(CONTROL_OK); // always enabled since we can have no rules -> match everything (as we do with default partymode playlists)
+
+ if (m_mode == "partyvideo" || m_mode == "partymusic")
+ {
+ SET_CONTROL_LABEL2(CONTROL_NAME, g_localizeStrings.Get(16035));
+ CONTROL_DISABLE(CONTROL_NAME);
+ }
+ else
+ SET_CONTROL_LABEL2(CONTROL_NAME, m_playlist.m_playlistName);
+
+ UpdateRuleControlButtons();
+
+ if (m_playlist.m_ruleCombination.GetType() == CSmartPlaylistRuleCombination::CombinationOr)
+ SET_CONTROL_LABEL2(CONTROL_MATCH, g_localizeStrings.Get(21426)); // one or more of the rules
+ else
+ SET_CONTROL_LABEL2(CONTROL_MATCH, g_localizeStrings.Get(21425)); // all of the rules
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_MATCH, m_playlist.m_ruleCombination.m_rules.size() > 1);
+ if (m_playlist.m_limit == 0)
+ SET_CONTROL_LABEL2(CONTROL_LIMIT, g_localizeStrings.Get(21428)); // no limit
+ else
+ SET_CONTROL_LABEL2(CONTROL_LIMIT,
+ StringUtils::Format(g_localizeStrings.Get(21436), m_playlist.m_limit));
+ int currentItem = GetSelectedItem();
+ CGUIMessage msgReset(GUI_MSG_LABEL_RESET, GetID(), CONTROL_RULE_LIST);
+ OnMessage(msgReset);
+ m_ruleLabels->Clear();
+ for (const auto& rule: m_playlist.m_ruleCombination.m_rules)
+ {
+ CFileItemPtr item(new CFileItem("", false));
+ item->SetLabel(std::static_pointer_cast<CSmartPlaylistRule>(rule)->GetLocalizedRule());
+ m_ruleLabels->Add(item);
+ }
+ CFileItemPtr item(new CFileItem("", false));
+ item->SetLabel(g_localizeStrings.Get(21423));
+ m_ruleLabels->Add(item);
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_RULE_LIST, 0, 0, m_ruleLabels);
+ OnMessage(msg);
+ SendMessage(GUI_MSG_ITEM_SELECT, GetID(), CONTROL_RULE_LIST, currentItem);
+
+ if (m_playlist.m_orderDirection != SortOrderDescending)
+ {
+ SET_CONTROL_LABEL2(CONTROL_ORDER_DIRECTION, g_localizeStrings.Get(21430));
+ }
+ else
+ {
+ SET_CONTROL_LABEL2(CONTROL_ORDER_DIRECTION, g_localizeStrings.Get(21431));
+ }
+
+ SET_CONTROL_LABEL2(CONTROL_ORDER_FIELD, g_localizeStrings.Get(SortUtils::GetSortLabel(m_playlist.m_orderField)));
+ SET_CONTROL_LABEL2(CONTROL_TYPE, GetLocalizedType(ConvertType(m_playlist.GetType())));
+
+ // setup groups
+ std::vector<Field> groups = CSmartPlaylistRule::GetGroups(m_playlist.GetType());
+ Field currentGroup = CSmartPlaylistRule::TranslateGroup(m_playlist.GetGroup().c_str());
+ SET_CONTROL_LABEL2(CONTROL_GROUP_BY, CSmartPlaylistRule::GetLocalizedGroup(currentGroup));
+ if (m_playlist.IsGroupMixed())
+ CONTROL_SELECT(CONTROL_GROUP_MIXED);
+ else
+ CONTROL_DESELECT(CONTROL_GROUP_MIXED);
+
+ // disable the group controls if there's no group
+ // or only one group which can't be mixed
+ if (groups.empty() ||
+ (groups.size() == 1 && !CSmartPlaylistRule::CanGroupMix(groups[0])))
+ {
+ CONTROL_DISABLE(CONTROL_GROUP_BY);
+ CONTROL_DISABLE(CONTROL_GROUP_MIXED);
+ }
+ else
+ {
+ CONTROL_ENABLE(CONTROL_GROUP_BY);
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_GROUP_MIXED, CSmartPlaylistRule::CanGroupMix(currentGroup));
+ }
+}
+
+void CGUIDialogSmartPlaylistEditor::UpdateRuleControlButtons()
+{
+ int iSize = m_playlist.m_ruleCombination.m_rules.size();
+ int iItem = GetSelectedItem();
+ // only enable the remove control if ...
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_RULE_REMOVE,
+ iSize > 0 && // there is at least one item
+ iItem >= 0 && iItem < iSize && // and a valid item is selected
+ m_playlist.m_ruleCombination.m_rules[iItem]->m_field != FieldNone); // and it is not be empty
+}
+
+void CGUIDialogSmartPlaylistEditor::OnInitWindow()
+{
+ m_cancelled = false;
+
+ std::vector<PLAYLIST_TYPE> allowedTypes = GetAllowedTypes(m_mode);
+ // check if our playlist type is allowed
+ PLAYLIST_TYPE type = ConvertType(m_playlist.GetType());
+ bool allowed = false;
+ for (auto allowedType: allowedTypes)
+ {
+ if (type == allowedType)
+ allowed = true;
+ }
+ if (!allowed && allowedTypes.size())
+ m_playlist.SetType(ConvertType(allowedTypes[0]));
+
+ UpdateButtons();
+
+ SET_CONTROL_LABEL(CONTROL_HEADING, 21432);
+
+ CGUIDialog::OnInitWindow();
+}
+
+void CGUIDialogSmartPlaylistEditor::OnDeinitWindow(int nextWindowID)
+{
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+ SendMessage(GUI_MSG_LABEL_RESET, CONTROL_RULE_LIST);
+ SendMessage(GUI_MSG_LABEL_RESET, CONTROL_TYPE);
+ m_ruleLabels->Clear();
+}
+
+CGUIDialogSmartPlaylistEditor::PLAYLIST_TYPE CGUIDialogSmartPlaylistEditor::ConvertType(const std::string &type)
+{
+ for (const translateType& t : types)
+ if (type == t.string)
+ return t.type;
+ assert(false);
+ return TYPE_SONGS;
+}
+
+std::string CGUIDialogSmartPlaylistEditor::GetLocalizedType(PLAYLIST_TYPE type)
+{
+ for (const translateType& t : types)
+ if (t.type == type)
+ return g_localizeStrings.Get(t.localizedString);
+ assert(false);
+ return "";
+}
+
+std::string CGUIDialogSmartPlaylistEditor::ConvertType(PLAYLIST_TYPE type)
+{
+ for (const translateType& t : types)
+ if (t.type == type)
+ return t.string;
+ assert(false);
+ return "songs";
+}
+
+int CGUIDialogSmartPlaylistEditor::GetSelectedItem()
+{
+ CGUIMessage message(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_RULE_LIST);
+ OnMessage(message);
+ return message.GetParam1();
+}
+
+void CGUIDialogSmartPlaylistEditor::HighlightItem(int item)
+{
+ for (int i = 0; i < m_ruleLabels->Size(); i++)
+ (*m_ruleLabels)[i]->Select(false);
+ if (item >= 0 && item < m_ruleLabels->Size())
+ (*m_ruleLabels)[item]->Select(true);
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), CONTROL_RULE_LIST, item);
+ OnMessage(msg);
+}
+
+std::vector<CGUIDialogSmartPlaylistEditor::PLAYLIST_TYPE> CGUIDialogSmartPlaylistEditor::GetAllowedTypes(const std::string& mode)
+{
+ std::vector<PLAYLIST_TYPE> allowedTypes;
+ if (mode == "partymusic")
+ {
+ allowedTypes.push_back(TYPE_SONGS);
+ allowedTypes.push_back(TYPE_MIXED);
+ }
+ else if (mode == "partyvideo")
+ {
+ allowedTypes.push_back(TYPE_MUSICVIDEOS);
+ allowedTypes.push_back(TYPE_MIXED);
+ }
+ else if (mode == "music")
+ { // music types + mixed
+ allowedTypes.push_back(TYPE_SONGS);
+ allowedTypes.push_back(TYPE_ALBUMS);
+ allowedTypes.push_back(TYPE_ARTISTS);
+ allowedTypes.push_back(TYPE_MIXED);
+ }
+ else if (mode == "video")
+ { // general category for videos
+ allowedTypes.push_back(TYPE_MOVIES);
+ allowedTypes.push_back(TYPE_TVSHOWS);
+ allowedTypes.push_back(TYPE_EPISODES);
+ allowedTypes.push_back(TYPE_MUSICVIDEOS);
+ allowedTypes.push_back(TYPE_MIXED);
+ }
+ return allowedTypes;
+}
+
+void CGUIDialogSmartPlaylistEditor::OnRuleRemove(int item)
+{
+ if (item < 0 || item >= (int)m_playlist.m_ruleCombination.m_rules.size()) return;
+ m_playlist.m_ruleCombination.m_rules.erase(m_playlist.m_ruleCombination.m_rules.begin() + item);
+
+ UpdateButtons();
+ if (item >= m_ruleLabels->Size())
+ HighlightItem(m_ruleLabels->Size() - 1);
+ else
+ HighlightItem(item);
+}
+
+void CGUIDialogSmartPlaylistEditor::OnRuleAdd()
+{
+ CSmartPlaylistRule rule;
+ if (CGUIDialogSmartPlaylistRule::EditRule(rule,m_playlist.GetType()))
+ m_playlist.m_ruleCombination.AddRule(rule);
+ UpdateButtons();
+}
+
+bool CGUIDialogSmartPlaylistEditor::NewPlaylist(const std::string &type)
+{
+ CGUIDialogSmartPlaylistEditor *editor = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSmartPlaylistEditor>(WINDOW_DIALOG_SMART_PLAYLIST_EDITOR);
+ if (!editor) return false;
+
+ editor->m_path = "";
+ editor->m_playlist = CSmartPlaylist();
+ editor->m_mode = type;
+ editor->Initialize();
+ editor->Open();
+ return !editor->m_cancelled;
+}
+
+bool CGUIDialogSmartPlaylistEditor::EditPlaylist(const std::string &path, const std::string &type)
+{
+ CGUIDialogSmartPlaylistEditor *editor = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSmartPlaylistEditor>(WINDOW_DIALOG_SMART_PLAYLIST_EDITOR);
+ if (!editor) return false;
+
+ editor->m_mode = type;
+ if (URIUtils::PathEquals(path, CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetUserDataItem("PartyMode.xsp")))
+ editor->m_mode = "partymusic";
+ if (URIUtils::PathEquals(path, CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetUserDataItem("PartyMode-Video.xsp")))
+ editor->m_mode = "partyvideo";
+
+ CSmartPlaylist playlist;
+ bool loaded(playlist.Load(path));
+ if (!loaded)
+ { // failed to load
+ if (!StringUtils::StartsWithNoCase(editor->m_mode, "party"))
+ return false; // only edit normal playlists that exist
+ // party mode playlists can be edited even if they don't exist
+ playlist.SetType(editor->m_mode == "partymusic" ? "songs" : "musicvideos");
+ }
+
+ editor->m_playlist = playlist;
+ editor->m_path = path;
+ editor->Initialize();
+ editor->Open();
+ return !editor->m_cancelled;
+}
diff --git a/xbmc/dialogs/GUIDialogSmartPlaylistEditor.h b/xbmc/dialogs/GUIDialogSmartPlaylistEditor.h
new file mode 100644
index 0000000..c754f91
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSmartPlaylistEditor.h
@@ -0,0 +1,64 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "playlists/SmartPlayList.h"
+
+class CFileItemList;
+
+class CGUIDialogSmartPlaylistEditor :
+ public CGUIDialog
+{
+public:
+ enum PLAYLIST_TYPE { TYPE_SONGS = 1, TYPE_ALBUMS, TYPE_ARTISTS, TYPE_MIXED, TYPE_MUSICVIDEOS, TYPE_MOVIES, TYPE_TVSHOWS, TYPE_EPISODES };
+
+ CGUIDialogSmartPlaylistEditor(void);
+ ~CGUIDialogSmartPlaylistEditor(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnBack(int actionID) override;
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ static bool EditPlaylist(const std::string &path, const std::string &type = "");
+ static bool NewPlaylist(const std::string &type);
+
+protected:
+ void OnRuleList(int item);
+ void OnRuleAdd();
+ void OnRuleRemove(int item);
+ void OnMatch();
+ void OnLimit();
+ void OnName();
+ void OnType();
+ void OnOrder();
+ void OnOrderDirection();
+ void OnGroupBy();
+ void OnGroupMixed();
+ void OnOK();
+ void OnCancel();
+ void OnPopupMenu(int item);
+ void UpdateButtons();
+ void UpdateRuleControlButtons();
+ int GetSelectedItem();
+ void HighlightItem(int item);
+ std::vector<PLAYLIST_TYPE> GetAllowedTypes(const std::string& mode);
+ PLAYLIST_TYPE ConvertType(const std::string &type);
+ std::string ConvertType(PLAYLIST_TYPE type);
+ std::string GetLocalizedType(PLAYLIST_TYPE type);
+
+ CSmartPlaylist m_playlist;
+
+ // our list of rules for display purposes
+ CFileItemList* m_ruleLabels;
+
+ std::string m_path;
+ bool m_cancelled;
+ std::string m_mode; // mode we're in (partymode etc.)
+};
diff --git a/xbmc/dialogs/GUIDialogSmartPlaylistRule.cpp b/xbmc/dialogs/GUIDialogSmartPlaylistRule.cpp
new file mode 100644
index 0000000..671219b
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSmartPlaylistRule.cpp
@@ -0,0 +1,575 @@
+/*
+ * 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 "GUIDialogSmartPlaylistRule.h"
+
+#include "FileItem.h"
+#include "GUIDialogFileBrowser.h"
+#include "GUIDialogSelect.h"
+#include "ServiceBroker.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/MusicDatabase.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/LabelFormatter.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "video/VideoDatabase.h"
+
+#include <utility>
+
+#define CONTROL_FIELD 15
+#define CONTROL_OPERATOR 16
+#define CONTROL_VALUE 17
+#define CONTROL_OK 18
+#define CONTROL_CANCEL 19
+#define CONTROL_BROWSE 20
+
+CGUIDialogSmartPlaylistRule::CGUIDialogSmartPlaylistRule(void)
+ : CGUIDialog(WINDOW_DIALOG_SMART_PLAYLIST_RULE, "SmartPlaylistRule.xml")
+{
+ m_cancelled = false;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogSmartPlaylistRule::~CGUIDialogSmartPlaylistRule() = default;
+
+bool CGUIDialogSmartPlaylistRule::OnBack(int actionID)
+{
+ m_cancelled = true;
+ return CGUIDialog::OnBack(actionID);
+}
+
+bool CGUIDialogSmartPlaylistRule::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_OK)
+ OnOK();
+ else if (iControl == CONTROL_CANCEL)
+ OnCancel();
+ else if (iControl == CONTROL_VALUE)
+ {
+ std::string parameter;
+ OnEditChanged(iControl, parameter);
+ m_rule.SetParameter(parameter);
+ }
+ else if (iControl == CONTROL_OPERATOR)
+ OnOperator();
+ else if (iControl == CONTROL_FIELD)
+ OnField();
+ else if (iControl == CONTROL_BROWSE)
+ OnBrowse();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_VALIDITY_CHANGED:
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_OK, message.GetParam1());
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogSmartPlaylistRule::OnOK()
+{
+ m_cancelled = false;
+ Close();
+}
+
+void CGUIDialogSmartPlaylistRule::OnBrowse()
+{
+ CFileItemList items;
+ CMusicDatabase database;
+ database.Open();
+ CVideoDatabase videodatabase;
+ videodatabase.Open();
+
+ std::string basePath;
+ if (CSmartPlaylist::IsMusicType(m_type))
+ basePath = "musicdb://";
+ else
+ basePath = "videodb://";
+
+ VideoDbContentType type = VideoDbContentType::MOVIES;
+ if (m_type == "movies")
+ basePath += "movies/";
+ else if (m_type == "tvshows")
+ {
+ type = VideoDbContentType::TVSHOWS;
+ basePath += "tvshows/";
+ }
+ else if (m_type == "musicvideos")
+ {
+ type = VideoDbContentType::MUSICVIDEOS;
+ basePath += "musicvideos/";
+ }
+ else if (m_type == "episodes")
+ {
+ if (m_rule.m_field == FieldGenre || m_rule.m_field == FieldYear ||
+ m_rule.m_field == FieldStudio)
+ type = VideoDbContentType::TVSHOWS;
+ else
+ type = VideoDbContentType::EPISODES;
+ basePath += "tvshows/";
+ }
+
+ int iLabel = 0;
+ if (m_rule.m_field == FieldGenre)
+ {
+ if (m_type == "tvshows" ||
+ m_type == "episodes" ||
+ m_type == "movies")
+ videodatabase.GetGenresNav(basePath + "genres/", items, type);
+ else if (m_type == "songs" ||
+ m_type == "albums" ||
+ m_type == "artists" ||
+ m_type == "mixed")
+ database.GetGenresNav("musicdb://genres/",items);
+ if (m_type == "musicvideos" ||
+ m_type == "mixed")
+ {
+ CFileItemList items2;
+ videodatabase.GetGenresNav("videodb://musicvideos/genres/", items2,
+ VideoDbContentType::MUSICVIDEOS);
+ items.Append(items2);
+ }
+ iLabel = 515;
+ }
+ else if (m_rule.m_field == FieldSource)
+ {
+ if (m_type == "songs" ||
+ m_type == "albums" ||
+ m_type == "artists" ||
+ m_type == "mixed")
+ {
+ database.GetSourcesNav("musicdb://sources/", items);
+ iLabel = 39030;
+ }
+ }
+ else if (m_rule.m_field == FieldRole)
+ {
+ if (m_type == "artists" || m_type == "mixed")
+ {
+ database.GetRolesNav("musicdb://songs/", items);
+ iLabel = 38033;
+ }
+ }
+ else if (m_rule.m_field == FieldCountry)
+ {
+ videodatabase.GetCountriesNav(basePath, items, type);
+ iLabel = 574;
+ }
+ else if (m_rule.m_field == FieldArtist || m_rule.m_field == FieldAlbumArtist)
+ {
+ if (CSmartPlaylist::IsMusicType(m_type))
+ database.GetArtistsNav("musicdb://artists/", items, m_rule.m_field == FieldAlbumArtist, -1);
+ if (m_type == "musicvideos" ||
+ m_type == "mixed")
+ {
+ CFileItemList items2;
+ videodatabase.GetMusicVideoArtistsByName("", items2);
+ items.Append(items2);
+ }
+ iLabel = 557;
+ }
+ else if (m_rule.m_field == FieldAlbum)
+ {
+ if (CSmartPlaylist::IsMusicType(m_type))
+ database.GetAlbumsNav("musicdb://albums/", items);
+ if (m_type == "musicvideos" ||
+ m_type == "mixed")
+ {
+ CFileItemList items2;
+ videodatabase.GetMusicVideoAlbumsByName("", items2);
+ items.Append(items2);
+ }
+ iLabel = 558;
+ }
+ else if (m_rule.m_field == FieldActor)
+ {
+ videodatabase.GetActorsNav(basePath + "actors/",items,type);
+ iLabel = 20337;
+ }
+ else if (m_rule.m_field == FieldYear)
+ {
+ if (CSmartPlaylist::IsMusicType(m_type))
+ database.GetYearsNav("musicdb://years/", items);
+ if (CSmartPlaylist::IsVideoType(m_type))
+ {
+ CFileItemList items2;
+ videodatabase.GetYearsNav(basePath + "years/", items2, type);
+ items.Append(items2);
+ }
+ iLabel = 562;
+ }
+ else if (m_rule.m_field == FieldOrigYear)
+ {
+ database.GetYearsNav("musicdb://originalyears/", items);
+ iLabel = 38078;
+ }
+ else if (m_rule.m_field == FieldDirector)
+ {
+ videodatabase.GetDirectorsNav(basePath + "directors/", items, type);
+ iLabel = 20339;
+ }
+ else if (m_rule.m_field == FieldStudio)
+ {
+ videodatabase.GetStudiosNav(basePath + "studios/", items, type);
+ iLabel = 572;
+ }
+ else if (m_rule.m_field == FieldWriter)
+ {
+ videodatabase.GetWritersNav(basePath, items, type);
+ iLabel = 20417;
+ }
+ else if (m_rule.m_field == FieldTvShowTitle ||
+ (m_type == "tvshows" && m_rule.m_field == FieldTitle))
+ {
+ videodatabase.GetTvShowsNav(basePath + "titles/", items);
+ iLabel = 20343;
+ }
+ else if (m_rule.m_field == FieldTitle)
+ {
+ if (m_type == "songs" || m_type == "mixed")
+ {
+ database.GetSongsNav("musicdb://songs/", items, -1, -1, -1);
+ iLabel = 134;
+ }
+ if (m_type == "movies")
+ {
+ videodatabase.GetMoviesNav(basePath + "titles/", items);
+ iLabel = 20342;
+ }
+ if (m_type == "episodes")
+ {
+ videodatabase.GetEpisodesNav(basePath + "titles/-1/-1/", items);
+ // we need to replace the db label (<season>x<episode> <title>) with the title only
+ CLabelFormatter format("%T", "");
+ for (int i = 0; i < items.Size(); i++)
+ format.FormatLabel(items[i].get());
+ iLabel = 20360;
+ }
+ if (m_type == "musicvideos" || m_type == "mixed")
+ {
+ videodatabase.GetMusicVideosNav(basePath + "titles/", items);
+ iLabel = 20389;
+ }
+ }
+ else if (m_rule.m_field == FieldPlaylist || m_rule.m_field == FieldVirtualFolder)
+ {
+ // use filebrowser to grab another smart playlist
+
+ // Note: This can cause infinite loops (playlist that refers to the same playlist) but I don't
+ // think there's any decent way to deal with this, as the infinite loop may be an arbitrary
+ // number of playlists deep, eg playlist1 -> playlist2 -> playlist3 ... -> playlistn -> playlist1
+ if (CSmartPlaylist::IsVideoType(m_type))
+ XFILE::CDirectory::GetDirectory("special://videoplaylists/", items, ".xsp", XFILE::DIR_FLAG_NO_FILE_DIRS);
+ if (CSmartPlaylist::IsMusicType(m_type))
+ {
+ CFileItemList items2;
+ XFILE::CDirectory::GetDirectory("special://musicplaylists/", items2, ".xsp", XFILE::DIR_FLAG_NO_FILE_DIRS);
+ items.Append(items2);
+ }
+
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ CSmartPlaylist playlist;
+ // don't list unloadable smartplaylists or any referenceable smartplaylists
+ // which do not match the type of the current smartplaylist
+ if (!playlist.Load(item->GetPath()) ||
+ (m_rule.m_field == FieldPlaylist &&
+ (!CSmartPlaylist::CheckTypeCompatibility(m_type, playlist.GetType()) ||
+ (!playlist.GetGroup().empty() || playlist.IsGroupMixed()))))
+ {
+ items.Remove(i);
+ i -= 1;
+ continue;
+ }
+
+ if (!playlist.GetName().empty())
+ item->SetLabel(playlist.GetName());
+ }
+ iLabel = 559;
+ }
+ else if (m_rule.m_field == FieldPath)
+ {
+ VECSOURCES sources;
+ if (m_type == "songs" || m_type == "mixed")
+ sources = *CMediaSourceSettings::GetInstance().GetSources("music");
+ if (CSmartPlaylist::IsVideoType(m_type))
+ {
+ VECSOURCES sources2 = *CMediaSourceSettings::GetInstance().GetSources("video");
+ sources.insert(sources.end(),sources2.begin(),sources2.end());
+ }
+ CServiceBroker::GetMediaManager().GetLocalDrives(sources);
+
+ std::string path = m_rule.GetParameter();
+ CGUIDialogFileBrowser::ShowAndGetDirectory(sources, g_localizeStrings.Get(657), path, false);
+ if (!m_rule.m_parameter.empty())
+ m_rule.m_parameter.clear();
+ if (!path.empty())
+ m_rule.m_parameter.emplace_back(std::move(path));
+
+ UpdateButtons();
+ return;
+ }
+ else if (m_rule.m_field == FieldSet)
+ {
+ videodatabase.GetSetsNav("videodb://movies/sets/", items, VideoDbContentType::MOVIES);
+ iLabel = 20434;
+ }
+ else if (m_rule.m_field == FieldTag)
+ {
+ VideoDbContentType type = VideoDbContentType::MOVIES;
+ if (m_type == "tvshows" ||
+ m_type == "episodes")
+ type = VideoDbContentType::TVSHOWS;
+ else if (m_type == "musicvideos")
+ type = VideoDbContentType::MUSICVIDEOS;
+ else if (m_type != "movies")
+ return;
+
+ videodatabase.GetTagsNav(basePath + "tags/", items, type);
+ iLabel = 20459;
+ }
+ else
+ { //! @todo Add browseability in here.
+ assert(false);
+ }
+
+ // sort the items
+ items.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+
+ CGUIDialogSelect* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ pDialog->Reset();
+ pDialog->SetItems(items);
+ std::string strHeading =
+ StringUtils::Format(g_localizeStrings.Get(13401), g_localizeStrings.Get(iLabel));
+ pDialog->SetHeading(CVariant{std::move(strHeading)});
+ pDialog->SetMultiSelection(m_rule.m_field != FieldPlaylist && m_rule.m_field != FieldVirtualFolder);
+
+ if (!m_rule.m_parameter.empty())
+ pDialog->SetSelected(m_rule.m_parameter);
+
+ pDialog->Open();
+ if (pDialog->IsConfirmed())
+ {
+ m_rule.m_parameter.clear();
+ for (int i : pDialog->GetSelectedItems())
+ m_rule.m_parameter.push_back(items.Get(i)->GetLabel());
+
+ UpdateButtons();
+ }
+ pDialog->Reset();
+}
+
+std::pair<std::string, int> OperatorLabel(CDatabaseQueryRule::SEARCH_OPERATOR op)
+{
+ return std::make_pair(CSmartPlaylistRule::GetLocalizedOperator(op), op);
+}
+
+std::vector<std::pair<std::string, int>> CGUIDialogSmartPlaylistRule::GetValidOperators(const CSmartPlaylistRule& rule)
+{
+ std::vector< std::pair<std::string, int> > labels;
+ switch (rule.GetFieldType(rule.m_field))
+ {
+ case CDatabaseQueryRule::TEXT_FIELD:
+ // text fields - add the usual comparisons
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_EQUALS));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_DOES_NOT_EQUAL));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_CONTAINS));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_DOES_NOT_CONTAIN));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_STARTS_WITH));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_ENDS_WITH));
+ break;
+
+ case CDatabaseQueryRule::REAL_FIELD:
+ case CDatabaseQueryRule::NUMERIC_FIELD:
+ case CDatabaseQueryRule::SECONDS_FIELD:
+ // numerical fields - less than greater than
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_EQUALS));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_DOES_NOT_EQUAL));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_GREATER_THAN));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_LESS_THAN));
+ break;
+
+ case CDatabaseQueryRule::DATE_FIELD:
+ // date field
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_AFTER));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_BEFORE));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_IN_THE_LAST));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_NOT_IN_THE_LAST));
+ break;
+
+ case CDatabaseQueryRule::PLAYLIST_FIELD:
+ CONTROL_ENABLE(CONTROL_BROWSE);
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_EQUALS));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_DOES_NOT_EQUAL));
+ break;
+
+ case CDatabaseQueryRule::BOOLEAN_FIELD:
+ CONTROL_DISABLE(CONTROL_VALUE);
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_TRUE));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_FALSE));
+ break;
+
+ case CDatabaseQueryRule::TEXTIN_FIELD:
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_EQUALS));
+ labels.push_back(OperatorLabel(CDatabaseQueryRule::OPERATOR_DOES_NOT_EQUAL));
+ break;
+ }
+ return labels;
+}
+
+void CGUIDialogSmartPlaylistRule::OnCancel()
+{
+ m_cancelled = true;
+ Close();
+}
+
+void CGUIDialogSmartPlaylistRule::OnField()
+{
+ const auto fields = CSmartPlaylistRule::GetFields(m_type);
+ CGUIDialogSelect* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ dialog->Reset();
+ dialog->SetHeading(CVariant{20427});
+ int selected = -1;
+ for (auto field = fields.begin(); field != fields.end(); field++)
+ {
+ dialog->Add(CSmartPlaylistRule::GetLocalizedField(*field));
+ if (*field == m_rule.m_field)
+ selected = std::distance(fields.begin(), field);
+ }
+ if (selected > -1)
+ dialog->SetSelected(selected);
+ dialog->Open();
+ int newSelected = dialog->GetSelectedItem();
+ // check if selection has changed
+ if (!dialog->IsConfirmed() || newSelected < 0 || newSelected == selected)
+ return;
+
+ m_rule.m_field = fields[newSelected];
+ // check if operator is still valid. if not, reset to first valid one
+ std::vector< std::pair<std::string, int> > validOperators = GetValidOperators(m_rule);
+ bool isValid = false;
+ for (auto op : validOperators)
+ if (std::get<0>(op) == std::get<0>(OperatorLabel(m_rule.m_operator)))
+ isValid = true;
+ if (!isValid)
+ m_rule.m_operator = (CDatabaseQueryRule::SEARCH_OPERATOR)std::get<1>(validOperators[0]);
+
+ m_rule.SetParameter("");
+ UpdateButtons();
+}
+
+void CGUIDialogSmartPlaylistRule::OnOperator()
+{
+ const auto labels = GetValidOperators(m_rule);
+ CGUIDialogSelect* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ dialog->Reset();
+ dialog->SetHeading(CVariant{ 16023 });
+ for (auto label : labels)
+ dialog->Add(std::get<0>(label));
+ dialog->SetSelected(CSmartPlaylistRule::GetLocalizedOperator(m_rule.m_operator));
+ dialog->Open();
+ int newSelected = dialog->GetSelectedItem();
+ // check if selection has changed
+ if (!dialog->IsConfirmed() || newSelected < 0)
+ return;
+
+ m_rule.m_operator = (CDatabaseQueryRule::SEARCH_OPERATOR)std::get<1>(labels[newSelected]);
+ UpdateButtons();
+}
+
+void CGUIDialogSmartPlaylistRule::UpdateButtons()
+{
+ if (m_rule.m_field == 0)
+ m_rule.m_field = CSmartPlaylistRule::GetFields(m_type)[0];
+ SET_CONTROL_LABEL(CONTROL_FIELD, CSmartPlaylistRule::GetLocalizedField(m_rule.m_field));
+
+ CONTROL_ENABLE(CONTROL_VALUE);
+ if (CSmartPlaylistRule::IsFieldBrowseable(m_rule.m_field))
+ CONTROL_ENABLE(CONTROL_BROWSE);
+ else
+ CONTROL_DISABLE(CONTROL_BROWSE);
+ SET_CONTROL_LABEL(CONTROL_OPERATOR, std::get<0>(OperatorLabel(m_rule.m_operator)));
+
+ // update label2 appropriately
+ SET_CONTROL_LABEL2(CONTROL_VALUE, m_rule.GetParameter());
+ CGUIEditControl::INPUT_TYPE type = CGUIEditControl::INPUT_TYPE_TEXT;
+ CDatabaseQueryRule::FIELD_TYPE fieldType = m_rule.GetFieldType(m_rule.m_field);
+ switch (fieldType)
+ {
+ case CDatabaseQueryRule::TEXT_FIELD:
+ case CDatabaseQueryRule::PLAYLIST_FIELD:
+ case CDatabaseQueryRule::TEXTIN_FIELD:
+ case CDatabaseQueryRule::REAL_FIELD:
+ case CDatabaseQueryRule::NUMERIC_FIELD:
+ type = CGUIEditControl::INPUT_TYPE_TEXT;
+ break;
+ case CDatabaseQueryRule::DATE_FIELD:
+ if (m_rule.m_operator == CDatabaseQueryRule::OPERATOR_IN_THE_LAST ||
+ m_rule.m_operator == CDatabaseQueryRule::OPERATOR_NOT_IN_THE_LAST)
+ type = CGUIEditControl::INPUT_TYPE_TEXT;
+ else
+ type = CGUIEditControl::INPUT_TYPE_DATE;
+ break;
+ case CDatabaseQueryRule::SECONDS_FIELD:
+ type = CGUIEditControl::INPUT_TYPE_SECONDS;
+ break;
+ case CDatabaseQueryRule::BOOLEAN_FIELD:
+ type = CGUIEditControl::INPUT_TYPE_NUMBER;
+ break;
+ }
+ SendMessage(GUI_MSG_SET_TYPE, CONTROL_VALUE, type, 21420);
+}
+
+void CGUIDialogSmartPlaylistRule::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ UpdateButtons();
+
+ CGUIEditControl *editControl = dynamic_cast<CGUIEditControl*>(GetControl(CONTROL_VALUE));
+ if (editControl != NULL)
+ editControl->SetInputValidation(CSmartPlaylistRule::Validate, &m_rule);
+}
+
+void CGUIDialogSmartPlaylistRule::OnDeinitWindow(int nextWindowID)
+{
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ // reset field spincontrolex
+ SendMessage(GUI_MSG_LABEL_RESET, CONTROL_FIELD);
+ // reset operator spincontrolex
+ SendMessage(GUI_MSG_LABEL_RESET, CONTROL_OPERATOR);
+}
+
+bool CGUIDialogSmartPlaylistRule::EditRule(CSmartPlaylistRule &rule, const std::string& type)
+{
+ CGUIDialogSmartPlaylistRule *editor = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSmartPlaylistRule>(WINDOW_DIALOG_SMART_PLAYLIST_RULE);
+ if (!editor) return false;
+
+ editor->m_rule = rule;
+ editor->m_type = type;
+ editor->Open();
+ rule = editor->m_rule;
+ return !editor->m_cancelled;
+}
+
diff --git a/xbmc/dialogs/GUIDialogSmartPlaylistRule.h b/xbmc/dialogs/GUIDialogSmartPlaylistRule.h
new file mode 100644
index 0000000..23ca11d
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSmartPlaylistRule.h
@@ -0,0 +1,38 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "playlists/SmartPlayList.h"
+
+class CGUIDialogSmartPlaylistRule :
+ public CGUIDialog
+{
+public:
+ CGUIDialogSmartPlaylistRule(void);
+ ~CGUIDialogSmartPlaylistRule(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnBack(int actionID) override;
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ static bool EditRule(CSmartPlaylistRule &rule, const std::string& type="songs");
+
+protected:
+ void OnField();
+ void OnOperator();
+ void OnOK();
+ void OnCancel();
+ void UpdateButtons();
+ void OnBrowse();
+ std::vector< std::pair<std::string, int> > GetValidOperators(const CSmartPlaylistRule& rule);
+ CSmartPlaylistRule m_rule;
+ bool m_cancelled;
+ std::string m_type;
+};
diff --git a/xbmc/dialogs/GUIDialogSubMenu.cpp b/xbmc/dialogs/GUIDialogSubMenu.cpp
new file mode 100644
index 0000000..ab1cdb2
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSubMenu.cpp
@@ -0,0 +1,30 @@
+/*
+ * 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 "GUIDialogSubMenu.h"
+
+#include "guilib/GUIMessage.h"
+
+CGUIDialogSubMenu::CGUIDialogSubMenu(int id, const std::string &xmlFile)
+ : CGUIDialog(id, xmlFile.c_str())
+{
+}
+
+CGUIDialogSubMenu::~CGUIDialogSubMenu(void) = default;
+
+bool CGUIDialogSubMenu::OnMessage(CGUIMessage &message)
+{
+ if (message.GetMessage() == GUI_MSG_CLICKED)
+ {
+ // someone has been clicked - deinit...
+ CGUIDialog::OnMessage(message);
+ Close();
+ return true;
+ }
+ return CGUIDialog::OnMessage(message);
+}
diff --git a/xbmc/dialogs/GUIDialogSubMenu.h b/xbmc/dialogs/GUIDialogSubMenu.h
new file mode 100644
index 0000000..eaf77d2
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogSubMenu.h
@@ -0,0 +1,20 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIDialogSubMenu :
+ public CGUIDialog
+{
+public:
+ CGUIDialogSubMenu(int id = WINDOW_DIALOG_SUB_MENU, const std::string &xmlFile = "DialogSubMenu.xml");
+ ~CGUIDialogSubMenu(void) override;
+ bool OnMessage(CGUIMessage &message) override;
+};
diff --git a/xbmc/dialogs/GUIDialogTextViewer.cpp b/xbmc/dialogs/GUIDialogTextViewer.cpp
new file mode 100644
index 0000000..363f102
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogTextViewer.cpp
@@ -0,0 +1,132 @@
+/*
+ * 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 "GUIDialogTextViewer.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+#define CONTROL_HEADING 1
+#define CONTROL_TEXTAREA 5
+
+CGUIDialogTextViewer::CGUIDialogTextViewer(void)
+ : CGUIDialog(WINDOW_DIALOG_TEXT_VIEWER, "DialogTextViewer.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogTextViewer::~CGUIDialogTextViewer(void) = default;
+
+bool CGUIDialogTextViewer::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_TOGGLE_FONT)
+ {
+ UseMonoFont(!m_mono);
+ return true;
+ }
+
+ return CGUIDialog::OnAction(action);
+}
+
+bool CGUIDialogTextViewer::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CGUIDialog::OnMessage(message);
+ SetHeading();
+ SetText();
+ UseMonoFont(m_mono);
+ return true;
+ }
+ break;
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (message.GetParam1() == GUI_MSG_UPDATE)
+ {
+ SetText();
+ SetHeading();
+ return true;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogTextViewer::SetText()
+{
+ CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), CONTROL_TEXTAREA);
+ msg.SetLabel(m_strText);
+ OnMessage(msg);
+}
+
+void CGUIDialogTextViewer::SetHeading()
+{
+ CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), CONTROL_HEADING);
+ msg.SetLabel(m_strHeading);
+ OnMessage(msg);
+}
+
+void CGUIDialogTextViewer::UseMonoFont(bool use)
+{
+ m_mono = use;
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_TEXTAREA, use ? 1 : 0);
+ OnMessage(msg);
+}
+
+void CGUIDialogTextViewer::OnDeinitWindow(int nextWindowID)
+{
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ // reset text area
+ CGUIMessage msgReset(GUI_MSG_LABEL_RESET, GetID(), CONTROL_TEXTAREA);
+ OnMessage(msgReset);
+
+ // reset heading
+ SET_CONTROL_LABEL(CONTROL_HEADING, "");
+}
+
+void CGUIDialogTextViewer::ShowForFile(const std::string& path, bool useMonoFont)
+{
+ CFile file;
+ if (file.Open(path))
+ {
+ std::string data;
+ try
+ {
+ data.resize(file.GetLength()+1);
+ file.Read(&data[0], file.GetLength());
+ CGUIDialogTextViewer* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogTextViewer>(WINDOW_DIALOG_TEXT_VIEWER);
+ pDialog->SetHeading(URIUtils::GetFileName(path));
+ pDialog->SetText(data);
+ pDialog->UseMonoFont(useMonoFont);
+ pDialog->Open();
+ }
+ catch(const std::bad_alloc&)
+ {
+ CLog::Log(LOGERROR, "Not enough memory to load text file {}", path);
+ }
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "Exception while trying to view text file {}", path);
+ }
+ }
+}
diff --git a/xbmc/dialogs/GUIDialogTextViewer.h b/xbmc/dialogs/GUIDialogTextViewer.h
new file mode 100644
index 0000000..1e7bacd
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogTextViewer.h
@@ -0,0 +1,39 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIDialogTextViewer :
+ public CGUIDialog
+{
+public:
+ CGUIDialogTextViewer(void);
+ ~CGUIDialogTextViewer(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void SetText(const std::string& strText) { m_strText = strText; }
+ void SetHeading(const std::string& strHeading) { m_strHeading = strHeading; }
+ void UseMonoFont(bool use);
+
+ //! \brief Load a file into memory and show in dialog.
+ //! \param path Path to file
+ //! \param useMonoFont True to use monospace font
+ static void ShowForFile(const std::string& path, bool useMonoFont);
+protected:
+ void OnDeinitWindow(int nextWindowID) override;
+ bool OnAction(const CAction &action) override;
+
+ std::string m_strText;
+ std::string m_strHeading;
+ bool m_mono = false;
+
+ void SetText();
+ void SetHeading();
+};
+
diff --git a/xbmc/dialogs/GUIDialogVolumeBar.cpp b/xbmc/dialogs/GUIDialogVolumeBar.cpp
new file mode 100644
index 0000000..a4d903f
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogVolumeBar.cpp
@@ -0,0 +1,88 @@
+/*
+ * 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 "GUIDialogVolumeBar.h"
+
+#include "IGUIVolumeBarCallback.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "guilib/GUIMessage.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+
+#include <mutex>
+
+#define VOLUME_BAR_DISPLAY_TIME 1000L
+
+CGUIDialogVolumeBar::CGUIDialogVolumeBar(void)
+ : CGUIDialog(WINDOW_DIALOG_VOLUME_BAR, "DialogVolumeBar.xml", DialogModalityType::MODELESS)
+{
+ m_loadType = LOAD_ON_GUI_INIT;
+}
+
+CGUIDialogVolumeBar::~CGUIDialogVolumeBar(void) = default;
+
+bool CGUIDialogVolumeBar::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_VOLUME_UP || action.GetID() == ACTION_VOLUME_DOWN || action.GetID() == ACTION_VOLUME_SET || action.GetID() == ACTION_MUTE)
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ if (appVolume->IsMuted() ||
+ appVolume->GetVolumeRatio() <= CApplicationVolumeHandling::VOLUME_MINIMUM)
+ { // cancel the timer, dialog needs to stay visible
+ CancelAutoClose();
+ }
+ else
+ { // reset the timer, as we've changed the volume level
+ SetAutoClose(VOLUME_BAR_DISPLAY_TIME);
+ }
+ MarkDirtyRegion();
+ return true;
+ }
+ return CGUIDialog::OnAction(action);
+}
+
+bool CGUIDialogVolumeBar::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ case GUI_MSG_WINDOW_DEINIT:
+ return CGUIDialog::OnMessage(message);
+ }
+ return false; // don't process anything other than what we need!
+}
+
+void CGUIDialogVolumeBar::RegisterCallback(IGUIVolumeBarCallback *callback)
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ m_callbacks.insert(callback);
+}
+
+void CGUIDialogVolumeBar::UnregisterCallback(IGUIVolumeBarCallback *callback)
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ m_callbacks.erase(callback);
+}
+
+bool CGUIDialogVolumeBar::IsVolumeBarEnabled() const
+{
+ std::unique_lock<CCriticalSection> lock(m_callbackMutex);
+
+ // Hide volume bar if any callbacks are shown
+ for (const auto &callback : m_callbacks)
+ {
+ if (callback->IsShown())
+ return false;
+ }
+
+ return true;
+}
diff --git a/xbmc/dialogs/GUIDialogVolumeBar.h b/xbmc/dialogs/GUIDialogVolumeBar.h
new file mode 100644
index 0000000..e8413d0
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogVolumeBar.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "threads/CriticalSection.h"
+
+#include <set>
+
+class IGUIVolumeBarCallback;
+
+class CGUIDialogVolumeBar : public CGUIDialog
+{
+public:
+ CGUIDialogVolumeBar(void);
+ ~CGUIDialogVolumeBar(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+
+ // Volume bar interface
+ void RegisterCallback(IGUIVolumeBarCallback *callback);
+ void UnregisterCallback(IGUIVolumeBarCallback *callback);
+ bool IsVolumeBarEnabled() const;
+
+private:
+ std::set<IGUIVolumeBarCallback*> m_callbacks;
+ mutable CCriticalSection m_callbackMutex;
+};
diff --git a/xbmc/dialogs/GUIDialogYesNo.cpp b/xbmc/dialogs/GUIDialogYesNo.cpp
new file mode 100644
index 0000000..56709c3
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogYesNo.cpp
@@ -0,0 +1,243 @@
+/*
+ * 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 "GUIDialogYesNo.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/Key.h"
+#include "messaging/helpers/DialogHelper.h"
+
+CGUIDialogYesNo::CGUIDialogYesNo(int overrideId /* = -1 */)
+ : CGUIDialogBoxBase(overrideId == -1 ? WINDOW_DIALOG_YES_NO : overrideId, "DialogConfirm.xml")
+{
+ Reset();
+}
+
+CGUIDialogYesNo::~CGUIDialogYesNo() = default;
+
+bool CGUIDialogYesNo::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ int iAction = message.GetParam1();
+ if (true || ACTION_SELECT_ITEM == iAction)
+ {
+ if (iControl == CONTROL_NO_BUTTON)
+ {
+ m_bConfirmed = false;
+ Close();
+ return true;
+ }
+ if (iControl == CONTROL_YES_BUTTON)
+ {
+ m_bConfirmed = true;
+ Close();
+ return true;
+ }
+ if (iControl == CONTROL_CUSTOM_BUTTON)
+ {
+ m_bConfirmed = false;
+ m_bCustom = true;
+ Close();
+ return true;
+ }
+ }
+ }
+ break;
+ }
+ return CGUIDialogBoxBase::OnMessage(message);
+}
+
+bool CGUIDialogYesNo::OnBack(int actionID)
+{
+ m_bCanceled = true;
+ m_bConfirmed = false;
+ m_bCustom = false;
+ return CGUIDialogBoxBase::OnBack(actionID);
+}
+
+void CGUIDialogYesNo::OnInitWindow()
+{
+ if (!m_strChoices[2].empty())
+ SET_CONTROL_VISIBLE(CONTROL_CUSTOM_BUTTON);
+ else
+ SET_CONTROL_HIDDEN(CONTROL_CUSTOM_BUTTON);
+ SET_CONTROL_HIDDEN(CONTROL_PROGRESS_BAR);
+ SET_CONTROL_FOCUS(m_defaultButtonId, 0);
+
+ CGUIDialogBoxBase::OnInitWindow();
+}
+
+bool CGUIDialogYesNo::ShowAndGetInput(const CVariant& heading,
+ const CVariant& line0,
+ const CVariant& line1,
+ const CVariant& line2,
+ bool& bCanceled)
+{
+ return ShowAndGetInput(heading, line0, line1, line2, bCanceled, "", "", NO_TIMEOUT);
+}
+
+bool CGUIDialogYesNo::ShowAndGetInput(const CVariant& heading,
+ const CVariant& line0,
+ const CVariant& line1,
+ const CVariant& line2,
+ const CVariant& noLabel /* = "" */,
+ const CVariant& yesLabel /* = "" */)
+{
+ bool bDummy(false);
+ return ShowAndGetInput(heading, line0, line1, line2, bDummy, noLabel, yesLabel, NO_TIMEOUT);
+}
+
+bool CGUIDialogYesNo::ShowAndGetInput(const CVariant& heading,
+ const CVariant& line0,
+ const CVariant& line1,
+ const CVariant& line2,
+ bool& bCanceled,
+ const CVariant& noLabel,
+ const CVariant& yesLabel,
+ unsigned int autoCloseTime)
+{
+ CGUIDialogYesNo *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (!dialog)
+ return false;
+
+ dialog->SetHeading(heading);
+ dialog->SetLine(0, line0);
+ dialog->SetLine(1, line1);
+ dialog->SetLine(2, line2);
+ if (autoCloseTime)
+ dialog->SetAutoClose(autoCloseTime);
+ dialog->SetChoice(0, !noLabel.empty() ? noLabel : 106);
+ dialog->SetChoice(1, !yesLabel.empty() ? yesLabel : 107);
+ dialog->SetChoice(2, "");
+ dialog->m_bCanceled = false;
+ dialog->m_defaultButtonId = CONTROL_NO_BUTTON;
+ dialog->Open();
+
+ bCanceled = dialog->m_bCanceled;
+ return (dialog->IsConfirmed()) ? true : false;
+}
+
+bool CGUIDialogYesNo::ShowAndGetInput(const CVariant& heading, const CVariant& text)
+{
+ bool bDummy(false);
+ return ShowAndGetInput(heading, text, "", "", bDummy);
+}
+
+bool CGUIDialogYesNo::ShowAndGetInput(const CVariant& heading,
+ const CVariant& text,
+ bool& bCanceled,
+ const CVariant& noLabel /* = "" */,
+ const CVariant& yesLabel /* = "" */,
+ unsigned int autoCloseTime,
+ int defaultButtonId /* = CONTROL_NO_BUTTON */)
+{
+ int result =
+ ShowAndGetInput(heading, text, noLabel, yesLabel, "", autoCloseTime, defaultButtonId);
+
+ bCanceled = result == -1;
+ return result == 1;
+}
+
+void CGUIDialogYesNo::Reset()
+{
+ m_bConfirmed = false;
+ m_bCanceled = false;
+ m_bCustom = false;
+ m_bAutoClosed = false;
+ m_defaultButtonId = CONTROL_NO_BUTTON;
+}
+
+int CGUIDialogYesNo::GetResult() const
+{
+ if (m_bCanceled)
+ return -1;
+ else if (m_bCustom)
+ return 2;
+ else if (IsConfirmed())
+ return 1;
+ else
+ return 0;
+}
+
+int CGUIDialogYesNo::ShowAndGetInput(const CVariant& heading,
+ const CVariant& text,
+ const CVariant& noLabel,
+ const CVariant& yesLabel,
+ const CVariant& customLabel,
+ unsigned int autoCloseTime,
+ int defaultButtonId /* = CONTROL_NO_BUTTON */)
+{
+ CGUIDialogYesNo *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (!dialog)
+ return false;
+
+ dialog->SetHeading(heading);
+ dialog->SetText(text);
+ if (autoCloseTime > 0)
+ dialog->SetAutoClose(autoCloseTime);
+ dialog->m_bCanceled = false;
+ dialog->m_bCustom = false;
+ dialog->m_defaultButtonId = defaultButtonId;
+ dialog->SetChoice(0, !noLabel.empty() ? noLabel : 106);
+ dialog->SetChoice(1, !yesLabel.empty() ? yesLabel : 107);
+ dialog->SetChoice(2, customLabel); // Button only visible when label is not empty
+ dialog->Open();
+
+ return dialog->GetResult();
+}
+
+int CGUIDialogYesNo::ShowAndGetInput(const KODI::MESSAGING::HELPERS::DialogYesNoMessage& options)
+{
+ //Set default yes/no labels, these might be overwritten further down if specified
+ //by the caller
+ SetChoice(0, 106);
+ SetChoice(1, 107);
+ SetChoice(2, "");
+ if (!options.heading.isNull())
+ SetHeading(options.heading);
+ if (!options.text.isNull())
+ SetText(options.text);
+ if (!options.noLabel.isNull())
+ SetChoice(0, options.noLabel);
+ if (!options.yesLabel.isNull())
+ SetChoice(1, options.yesLabel);
+ if (!options.customLabel.isNull())
+ SetChoice(2, options.customLabel);
+ if (options.autoclose > 0)
+ SetAutoClose(options.autoclose);
+ m_bCanceled = false;
+ m_bCustom = false;
+ m_defaultButtonId = CONTROL_NO_BUTTON;
+
+ for (size_t i = 0; i < 3; ++i)
+ {
+ if (!options.lines[i].isNull())
+ SetLine(i, options.lines[i]);
+ }
+
+ Open();
+
+ return GetResult();
+}
+
+int CGUIDialogYesNo::GetDefaultLabelID(int controlId) const
+{
+ if (controlId == CONTROL_NO_BUTTON)
+ return 106;
+ else if (controlId == CONTROL_YES_BUTTON)
+ return 107;
+ else if (controlId == CONTROL_CUSTOM_BUTTON)
+ return -1;
+ return CGUIDialogBoxBase::GetDefaultLabelID(controlId);
+}
diff --git a/xbmc/dialogs/GUIDialogYesNo.h b/xbmc/dialogs/GUIDialogYesNo.h
new file mode 100644
index 0000000..e8baac2
--- /dev/null
+++ b/xbmc/dialogs/GUIDialogYesNo.h
@@ -0,0 +1,153 @@
+/*
+ * 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 "GUIDialogBoxBase.h"
+#include "utils/Variant.h"
+
+namespace KODI
+{
+ namespace MESSAGING
+ {
+ namespace HELPERS
+ {
+ struct DialogYesNoMessage;
+ }
+ }
+}
+
+class CGUIDialogYesNo :
+ public CGUIDialogBoxBase
+{
+public:
+ explicit CGUIDialogYesNo(int overrideId = -1);
+ ~CGUIDialogYesNo(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnBack(int actionID) override;
+
+ void Reset();
+ int GetResult() const;
+
+ enum TimeOut
+ {
+ NO_TIMEOUT = 0
+ };
+
+ /*! \brief Show a yes-no dialog, then wait for user to dismiss it.
+ \param heading Localized label id or string for the heading of the dialog
+ \param line0 Localized label id or string for line 1 of the dialog message
+ \param line1 Localized label id or string for line 2 of the dialog message
+ \param line2 Localized label id or string for line 3 of the dialog message
+ \param bCanceled Holds true if the dialog was canceled otherwise false
+ \return true if user selects Yes, otherwise false if user selects No.
+ */
+ static bool ShowAndGetInput(const CVariant& heading,
+ const CVariant& line0,
+ const CVariant& line1,
+ const CVariant& line2,
+ bool& bCanceled);
+
+ /*! \brief Show a yes-no dialog, then wait for user to dismiss it.
+ \param heading Localized label id or string for the heading of the dialog
+ \param line0 Localized label id or string for line 1 of the dialog message
+ \param line1 Localized label id or string for line 2 of the dialog message
+ \param line2 Localized label id or string for line 3 of the dialog message
+ \param iNoLabel Localized label id or string for the no button
+ \param iYesLabel Localized label id or string for the yes button
+ \return true if user selects Yes, otherwise false if user selects No.
+ */
+ static bool ShowAndGetInput(const CVariant& heading,
+ const CVariant& line0,
+ const CVariant& line1,
+ const CVariant& line2,
+ const CVariant& noLabel = "",
+ const CVariant& yesLabel = "");
+
+ /*! \brief Show a yes-no dialog, then wait for user to dismiss it.
+ \param heading Localized label id or string for the heading of the dialog
+ \param line0 Localized label id or string for line 1 of the dialog message
+ \param line1 Localized label id or string for line 2 of the dialog message
+ \param line2 Localized label id or string for line 3 of the dialog message
+ \param bCanceled Holds true if the dialog was canceled otherwise false
+ \param iNoLabel Localized label id or string for the no button
+ \param iYesLabel Localized label id or string for the yes button
+ \param autoCloseTime Time in ms before the dialog becomes automatically closed
+ \return true if user selects Yes, otherwise false if user selects No.
+ */
+ static bool ShowAndGetInput(const CVariant& heading,
+ const CVariant& line0,
+ const CVariant& line1,
+ const CVariant& line2,
+ bool& bCanceled,
+ const CVariant& noLabel,
+ const CVariant& yesLabel,
+ unsigned int autoCloseTime);
+
+ /*! \brief Show a yes-no dialog, then wait for user to dismiss it.
+ \param heading Localized label id or string for the heading of the dialog
+ \param text Localized label id or string for the dialog message
+ \return true if user selects Yes, otherwise false if user selects No.
+ */
+ static bool ShowAndGetInput(const CVariant& heading, const CVariant& text);
+
+ /*! \brief Show a yes-no dialog, then wait for user to dismiss it.
+ \param heading Localized label id or string for the heading of the dialog
+ \param text Localized label id or string for the dialog message
+ \param bCanceled Holds true if the dialog was canceled otherwise false
+ \param iNoLabel Localized label id or string for the no button
+ \param iYesLabel Localized label id or string for the yes button
+ \param autoCloseTime Time in ms before the dialog becomes automatically closed
+ \param defaultButtonId Specifies the default focused button
+ \return true if user selects Yes, otherwise false if user selects No.
+ */
+ static bool ShowAndGetInput(const CVariant& heading,
+ const CVariant& text,
+ bool& bCanceled,
+ const CVariant& noLabel,
+ const CVariant& yesLabel,
+ unsigned int autoCloseTime,
+ int defaultButtonId = CONTROL_NO_BUTTON);
+
+ /*! \brief Show a yes-no dialog with 3rd custom button, then wait for user to dismiss it.
+ \param heading Localized label id or string for the heading of the dialog
+ \param text Localized label id or string for the dialog message
+ \param noLabel Localized label id or string for the no button
+ \param yesLabel Localized label id or string for the yes button
+ \param customLabel Localized label id or string for the custom button
+ \param autoCloseTime Time in ms before the dialog becomes automatically closed
+ \param defaultButtonId Specifies the default focused button
+ \return -1 for cancelled, 0 for No, 1 for Yes and 2 for custom button
+ */
+ static int ShowAndGetInput(const CVariant& heading,
+ const CVariant& text,
+ const CVariant& noLabel,
+ const CVariant& yesLabel,
+ const CVariant& customLabel,
+ unsigned int autoCloseTime,
+ int defaultButtonId = CONTROL_NO_BUTTON);
+
+ /*!
+ \brief Open a Yes/No dialog and wait for input
+
+ \param[in] options a struct of type DialogYesNoMessage containing
+ the options to set for this dialog.
+
+ \returns -1 for cancelled, 0 for No and 1 for Yes
+ \sa KODI::MESSAGING::HELPERS::DialogYesNoMessage
+ */
+ int ShowAndGetInput(const KODI::MESSAGING::HELPERS::DialogYesNoMessage& options);
+
+protected:
+ void OnInitWindow() override;
+ int GetDefaultLabelID(int controlId) const override;
+
+ bool m_bCanceled;
+ bool m_bCustom;
+ int m_defaultButtonId;
+};
diff --git a/xbmc/dialogs/IGUIVolumeBarCallback.h b/xbmc/dialogs/IGUIVolumeBarCallback.h
new file mode 100644
index 0000000..5e4012a
--- /dev/null
+++ b/xbmc/dialogs/IGUIVolumeBarCallback.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017-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
+
+/*!
+ * \brief Interface to expose properties to the volume bar dialog
+ */
+class IGUIVolumeBarCallback
+{
+public:
+ virtual ~IGUIVolumeBarCallback() = default;
+
+ /*!
+ * \brief Return true if the callback is active in the GUI
+ *
+ * If a registered callback is shown in the GUI, the volume bar is disabled
+ * until no more callbacks are shown.
+ *
+ * \return True if the callback is active in the GUI, false otherwise
+ */
+ virtual bool IsShown() const = 0;
+};
diff --git a/xbmc/events/AddonEvent.cpp b/xbmc/events/AddonEvent.cpp
new file mode 100644
index 0000000..459c37d
--- /dev/null
+++ b/xbmc/events/AddonEvent.cpp
@@ -0,0 +1,48 @@
+/*
+ * 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 "AddonEvent.h"
+
+CAddonEvent::CAddonEvent(const ADDON::AddonPtr& addon, const CVariant& description)
+ : CUniqueEvent(addon->Name(), description, addon->Icon()), m_addon(addon)
+{ }
+
+CAddonEvent::CAddonEvent(const ADDON::AddonPtr& addon,
+ const CVariant& description,
+ const CVariant& details)
+ : CUniqueEvent(addon->Name(), description, addon->Icon(), details), m_addon(addon)
+{ }
+
+CAddonEvent::CAddonEvent(const ADDON::AddonPtr& addon,
+ const CVariant& description,
+ const CVariant& details,
+ const CVariant& executionLabel)
+ : CUniqueEvent(addon->Name(), description, addon->Icon(), details, executionLabel), m_addon(addon)
+{ }
+
+CAddonEvent::CAddonEvent(const ADDON::AddonPtr& addon,
+ EventLevel level,
+ const CVariant& description)
+ : CUniqueEvent(addon->Name(), description, addon->Icon(), level), m_addon(addon)
+{ }
+
+CAddonEvent::CAddonEvent(const ADDON::AddonPtr& addon,
+ EventLevel level,
+ const CVariant& description,
+ const CVariant& details)
+ : CUniqueEvent(addon->Name(), description, addon->Icon(), details, level), m_addon(addon)
+{ }
+
+CAddonEvent::CAddonEvent(const ADDON::AddonPtr& addon,
+ EventLevel level,
+ const CVariant& description,
+ const CVariant& details,
+ const CVariant& executionLabel)
+ : CUniqueEvent(addon->Name(), description, addon->Icon(), details, executionLabel, level),
+ m_addon(addon)
+{ }
diff --git a/xbmc/events/AddonEvent.h b/xbmc/events/AddonEvent.h
new file mode 100644
index 0000000..84154f1
--- /dev/null
+++ b/xbmc/events/AddonEvent.h
@@ -0,0 +1,39 @@
+/*
+ * 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 "addons/IAddon.h"
+#include "events/UniqueEvent.h"
+
+class CAddonEvent : public CUniqueEvent
+{
+public:
+ CAddonEvent(const ADDON::AddonPtr& addon, const CVariant& description);
+ CAddonEvent(const ADDON::AddonPtr& addon, const CVariant& description, const CVariant& details);
+ CAddonEvent(const ADDON::AddonPtr& addon,
+ const CVariant& description,
+ const CVariant& details,
+ const CVariant& executionLabel);
+ CAddonEvent(const ADDON::AddonPtr& addon, EventLevel level, const CVariant& description);
+ CAddonEvent(const ADDON::AddonPtr& addon,
+ EventLevel level,
+ const CVariant& description,
+ const CVariant& details);
+ CAddonEvent(const ADDON::AddonPtr& addon,
+ EventLevel level,
+ const CVariant& description,
+ const CVariant& details,
+ const CVariant& executionLabel);
+ ~CAddonEvent() override = default;
+
+ const char* GetType() const override { return "AddonEvent"; }
+
+protected:
+ ADDON::AddonPtr m_addon;
+};
diff --git a/xbmc/events/AddonManagementEvent.cpp b/xbmc/events/AddonManagementEvent.cpp
new file mode 100644
index 0000000..6d97915
--- /dev/null
+++ b/xbmc/events/AddonManagementEvent.cpp
@@ -0,0 +1,74 @@
+/*
+ * 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 "AddonManagementEvent.h"
+
+#include "addons/gui/GUIDialogAddonInfo.h"
+#include "filesystem/AddonsDirectory.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/URIUtils.h"
+
+CAddonManagementEvent::CAddonManagementEvent(const ADDON::AddonPtr& addon,
+ const CVariant& description)
+ : CAddonEvent(addon, description)
+{ }
+
+CAddonManagementEvent::CAddonManagementEvent(const ADDON::AddonPtr& addon,
+ const CVariant& description,
+ const CVariant& details)
+ : CAddonEvent(addon, description, details)
+{ }
+
+CAddonManagementEvent::CAddonManagementEvent(const ADDON::AddonPtr& addon,
+ const CVariant& description,
+ const CVariant& details,
+ const CVariant& executionLabel)
+ : CAddonEvent(addon, description, details, executionLabel)
+{ }
+
+CAddonManagementEvent::CAddonManagementEvent(const ADDON::AddonPtr& addon,
+ EventLevel level,
+ const CVariant& description)
+ : CAddonEvent(addon, level, description)
+{ }
+
+CAddonManagementEvent::CAddonManagementEvent(const ADDON::AddonPtr& addon,
+ EventLevel level,
+ const CVariant& description,
+ const CVariant& details)
+ : CAddonEvent(addon, level, description, details)
+{ }
+
+CAddonManagementEvent::CAddonManagementEvent(const ADDON::AddonPtr& addon,
+ EventLevel level,
+ const CVariant& description,
+ const CVariant& details,
+ const CVariant& executionLabel)
+ : CAddonEvent(addon, level, description, details, executionLabel)
+{ }
+
+std::string CAddonManagementEvent::GetExecutionLabel() const
+{
+ std::string executionLabel = CAddonEvent::GetExecutionLabel();
+ if (!executionLabel.empty())
+ return executionLabel;
+
+ return g_localizeStrings.Get(24139);
+}
+
+bool CAddonManagementEvent::Execute() const
+{
+ if (!CanExecute())
+ return false;
+
+ CFileItemPtr addonItem = XFILE::CAddonsDirectory::FileItemFromAddon(m_addon, URIUtils::AddFileToFolder("addons://", m_addon->ID()));
+ if (addonItem == nullptr)
+ return false;
+
+ return CGUIDialogAddonInfo::ShowForItem(addonItem);
+}
diff --git a/xbmc/events/AddonManagementEvent.h b/xbmc/events/AddonManagementEvent.h
new file mode 100644
index 0000000..f6f0744
--- /dev/null
+++ b/xbmc/events/AddonManagementEvent.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 "events/AddonEvent.h"
+
+class CAddonManagementEvent : public CAddonEvent
+{
+public:
+ CAddonManagementEvent(const ADDON::AddonPtr& addon, const CVariant& description);
+ CAddonManagementEvent(const ADDON::AddonPtr& addon,
+ const CVariant& description,
+ const CVariant& details);
+ CAddonManagementEvent(const ADDON::AddonPtr& addon,
+ const CVariant& description,
+ const CVariant& details,
+ const CVariant& executionLabel);
+ CAddonManagementEvent(const ADDON::AddonPtr& addon,
+ EventLevel level,
+ const CVariant& description);
+ CAddonManagementEvent(const ADDON::AddonPtr& addon,
+ EventLevel level,
+ const CVariant& description,
+ const CVariant& details);
+ CAddonManagementEvent(const ADDON::AddonPtr& addon,
+ EventLevel level,
+ const CVariant& description,
+ const CVariant& details,
+ const CVariant& executionLabel);
+ ~CAddonManagementEvent() override = default;
+
+ const char* GetType() const override { return "AddonManagementEvent"; }
+ std::string GetExecutionLabel() const override;
+
+ bool CanExecute() const override { return m_addon != NULL; }
+ bool Execute() const override;
+};
diff --git a/xbmc/events/BaseEvent.cpp b/xbmc/events/BaseEvent.cpp
new file mode 100644
index 0000000..d90207d
--- /dev/null
+++ b/xbmc/events/BaseEvent.cpp
@@ -0,0 +1,107 @@
+/*
+ * 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 "BaseEvent.h"
+
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+
+#include <chrono>
+#include <inttypes.h>
+
+CBaseEvent::CBaseEvent(const std::string& identifier, const CVariant& label, const CVariant& description, EventLevel level /* = EventLevel::Information */)
+ : m_level(level),
+ m_identifier(identifier),
+ m_icon(),
+ m_label(label),
+ m_description(description),
+ m_details(),
+ m_executionLabel(),
+ m_timestamp(GetInternalTimestamp()),
+ m_dateTime(CDateTime::GetCurrentDateTime())
+{ }
+
+CBaseEvent::CBaseEvent(const std::string& identifier, const CVariant& label, const CVariant& description, const std::string& icon, EventLevel level /* = EventLevel::Information */)
+ : m_level(level),
+ m_identifier(identifier),
+ m_icon(icon),
+ m_label(label),
+ m_description(description),
+ m_details(),
+ m_executionLabel(),
+ m_timestamp(GetInternalTimestamp()),
+ m_dateTime(CDateTime::GetCurrentDateTime())
+{ }
+
+CBaseEvent::CBaseEvent(const std::string& identifier, const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, EventLevel level /* = EventLevel::Information */)
+ : m_level(level),
+ m_identifier(identifier),
+ m_icon(icon),
+ m_label(label),
+ m_description(description),
+ m_details(details),
+ m_executionLabel(),
+ m_timestamp(GetInternalTimestamp()),
+ m_dateTime(CDateTime::GetCurrentDateTime())
+{ }
+
+CBaseEvent::CBaseEvent(const std::string& identifier, const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, const CVariant& executionLabel, EventLevel level /* = EventLevel::Information */)
+ : m_level(level),
+ m_identifier(identifier),
+ m_icon(icon),
+ m_label(label),
+ m_description(description),
+ m_details(details),
+ m_executionLabel(executionLabel),
+ m_timestamp(GetInternalTimestamp()),
+ m_dateTime(CDateTime::GetCurrentDateTime())
+{ }
+
+uint64_t CBaseEvent::GetInternalTimestamp()
+{
+ return std::chrono::time_point_cast<std::chrono::microseconds>(std::chrono::steady_clock::now()).time_since_epoch().count();
+}
+
+std::string CBaseEvent::GetLabel() const
+{
+ return VariantToLocalizedString(m_label);
+}
+
+std::string CBaseEvent::GetDescription() const
+{
+ return VariantToLocalizedString(m_description);
+}
+
+std::string CBaseEvent::GetDetails() const
+{
+ return VariantToLocalizedString(m_details);
+}
+
+std::string CBaseEvent::GetExecutionLabel() const
+{
+ return VariantToLocalizedString(m_executionLabel);
+}
+
+std::string CBaseEvent::VariantToLocalizedString(const CVariant& variant)
+{
+ if (variant.isString())
+ return variant.asString();
+
+ if (variant.isInteger() && variant.asInteger() > 0)
+ return g_localizeStrings.Get(static_cast<uint32_t>(variant.asInteger()));
+ if (variant.isUnsignedInteger() && variant.asUnsignedInteger() > 0)
+ return g_localizeStrings.Get(static_cast<uint32_t>(variant.asUnsignedInteger()));
+
+ return "";
+}
+
+void CBaseEvent::ToSortable(SortItem& sortable, Field field) const
+{
+ if (field == FieldDate)
+ sortable[FieldDate] = StringUtils::Format("{:020}", m_timestamp);
+}
diff --git a/xbmc/events/BaseEvent.h b/xbmc/events/BaseEvent.h
new file mode 100644
index 0000000..80919b9
--- /dev/null
+++ b/xbmc/events/BaseEvent.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "XBDateTime.h"
+#include "events/IEvent.h"
+#include "utils/Variant.h"
+
+class CBaseEvent : public IEvent
+{
+public:
+ ~CBaseEvent() override = default;
+
+ std::string GetIdentifier() const override { return m_identifier; }
+ EventLevel GetLevel() const override { return m_level; }
+ std::string GetLabel() const override;
+ std::string GetIcon() const override { return m_icon; }
+ std::string GetDescription() const override;
+ std::string GetDetails() const override;
+ std::string GetExecutionLabel() const override;
+ CDateTime GetDateTime() const override { return m_dateTime; }
+
+ bool CanExecute() const override { return !GetExecutionLabel().empty(); }
+
+ void ToSortable(SortItem& sortable, Field field) const override;
+
+protected:
+ CBaseEvent(const std::string& identifier, const CVariant& label, const CVariant& description, EventLevel level = EventLevel::Information);
+ CBaseEvent(const std::string& identifier, const CVariant& label, const CVariant& description, const std::string& icon, EventLevel level = EventLevel::Information);
+ CBaseEvent(const std::string& identifier, const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, EventLevel level = EventLevel::Information);
+ CBaseEvent(const std::string& identifier, const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, const CVariant& executionLabel, EventLevel level = EventLevel::Information);
+
+ EventLevel m_level;
+ std::string m_identifier;
+ std::string m_icon;
+ CVariant m_label;
+ CVariant m_description;
+ CVariant m_details;
+ CVariant m_executionLabel;
+
+private:
+ static std::string VariantToLocalizedString(const CVariant& variant);
+ static uint64_t GetInternalTimestamp();
+
+ uint64_t m_timestamp; // high res internal time stamp
+ CDateTime m_dateTime; // user interface time stamp
+};
diff --git a/xbmc/events/CMakeLists.txt b/xbmc/events/CMakeLists.txt
new file mode 100644
index 0000000..1a490e0
--- /dev/null
+++ b/xbmc/events/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(SOURCES AddonEvent.cpp
+ AddonManagementEvent.cpp
+ BaseEvent.cpp
+ EventLog.cpp
+ EventLogManager.cpp
+ MediaLibraryEvent.cpp)
+
+set(HEADERS AddonEvent.h
+ AddonManagementEvent.h
+ BaseEvent.h
+ EventLog.h
+ EventLogManager.h
+ IEvent.h
+ MediaLibraryEvent.h
+ NotificationEvent.h
+ UniqueEvent.h)
+
+core_add_library(events)
diff --git a/xbmc/events/EventLog.cpp b/xbmc/events/EventLog.cpp
new file mode 100644
index 0000000..89eff93
--- /dev/null
+++ b/xbmc/events/EventLog.cpp
@@ -0,0 +1,230 @@
+/*
+ * 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 "EventLog.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "filesystem/EventsDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "profiles/ProfileManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+#include <mutex>
+#include <utility>
+
+std::string CEventLog::EventLevelToString(EventLevel level)
+{
+ switch (level)
+ {
+ case EventLevel::Basic:
+ return "basic";
+
+ case EventLevel::Warning:
+ return "warning";
+
+ case EventLevel::Error:
+ return "error";
+
+ case EventLevel::Information:
+ default:
+ break;
+ }
+
+ return "information";
+}
+
+EventLevel CEventLog::EventLevelFromString(const std::string& level)
+{
+ if (level == "basic")
+ return EventLevel::Basic;
+ if (level == "warning")
+ return EventLevel::Warning;
+ if (level == "error")
+ return EventLevel::Error;
+
+ return EventLevel::Information;
+}
+
+Events CEventLog::Get() const
+{
+ return m_events;
+}
+
+Events CEventLog::Get(EventLevel level, bool includeHigherLevels /* = false */) const
+{
+ Events events;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ for (const auto& eventPtr : m_events)
+ {
+ if (eventPtr->GetLevel() == level ||
+ (includeHigherLevels && eventPtr->GetLevel() > level))
+ events.push_back(eventPtr);
+ }
+
+ return events;
+}
+
+EventPtr CEventLog::Get(const std::string& eventPtrIdentifier) const
+{
+ if (eventPtrIdentifier.empty())
+ return EventPtr();
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ const auto& eventPtr = m_eventsMap.find(eventPtrIdentifier);
+ if (eventPtr == m_eventsMap.end())
+ return EventPtr();
+
+ return eventPtr->second;
+}
+
+void CEventLog::Add(const EventPtr& eventPtr)
+{
+ if (eventPtr == nullptr || eventPtr->GetIdentifier().empty() ||
+ !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_EVENTLOG_ENABLED) ||
+ (eventPtr->GetLevel() == EventLevel::Information && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_EVENTLOG_ENABLED_NOTIFICATIONS)))
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (m_eventsMap.find(eventPtr->GetIdentifier()) != m_eventsMap.end())
+ return;
+
+ // store the event
+ m_events.push_back(eventPtr);
+ m_eventsMap.insert(std::make_pair(eventPtr->GetIdentifier(), eventPtr));
+
+ SendMessage(eventPtr, GUI_MSG_EVENT_ADDED);
+}
+
+void CEventLog::Add(const EventPtr& eventPtr, bool withNotification, bool withSound /* = true */)
+{
+ if (!withNotification)
+ Add(eventPtr);
+ else
+ AddWithNotification(eventPtr, withSound);
+}
+
+void CEventLog::AddWithNotification(const EventPtr& eventPtr,
+ unsigned int displayTime /* = NOTIFICATION_DISPLAY_TIME */,
+ unsigned int messageTime /* = NOTIFICATION_MESSAGE_TIME */,
+ bool withSound /* = true */)
+{
+ if (eventPtr == nullptr)
+ return;
+
+ Add(eventPtr);
+
+ // queue the eventPtr as a kai toast notification
+ if (!eventPtr->GetIcon().empty())
+ CGUIDialogKaiToast::QueueNotification(eventPtr->GetIcon(), eventPtr->GetLabel(), eventPtr->GetDescription(), displayTime, withSound, messageTime);
+ else
+ {
+ CGUIDialogKaiToast::eMessageType type = CGUIDialogKaiToast::Info;
+ if (eventPtr->GetLevel() == EventLevel::Warning)
+ type = CGUIDialogKaiToast::Warning;
+ else if (eventPtr->GetLevel() == EventLevel::Error)
+ type = CGUIDialogKaiToast::Error;
+
+ CGUIDialogKaiToast::QueueNotification(type, eventPtr->GetLabel(), eventPtr->GetDescription(), displayTime, withSound, messageTime);
+ }
+}
+
+void CEventLog::AddWithNotification(const EventPtr& eventPtr, bool withSound)
+{
+ AddWithNotification(eventPtr, NOTIFICATION_DISPLAY_TIME, NOTIFICATION_MESSAGE_TIME, withSound);
+}
+
+void CEventLog::Remove(const EventPtr& eventPtr)
+{
+ if (eventPtr == nullptr)
+ return;
+
+ Remove(eventPtr->GetIdentifier());
+}
+
+void CEventLog::Remove(const std::string& eventPtrIdentifier)
+{
+ if (eventPtrIdentifier.empty())
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ const auto& itEvent = m_eventsMap.find(eventPtrIdentifier);
+ if (itEvent == m_eventsMap.end())
+ return;
+
+ EventPtr eventPtr = itEvent->second;
+ m_eventsMap.erase(itEvent);
+ m_events.erase(std::remove(m_events.begin(), m_events.end(), eventPtr), m_events.end());
+
+ SendMessage(eventPtr, GUI_MSG_EVENT_REMOVED);
+}
+
+void CEventLog::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_events.clear();
+ m_eventsMap.clear();
+}
+
+void CEventLog::Clear(EventLevel level, bool includeHigherLevels /* = false */)
+{
+ EventsList eventsCopy = m_events;
+ for (const auto& eventPtr : eventsCopy)
+ {
+
+ if (eventPtr->GetLevel() == level ||
+ (includeHigherLevels && eventPtr->GetLevel() > level))
+ Remove(eventPtr);
+ }
+}
+
+bool CEventLog::Execute(const std::string& eventPtrIdentifier)
+{
+ if (eventPtrIdentifier.empty())
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ const auto& itEvent = m_eventsMap.find(eventPtrIdentifier);
+ if (itEvent == m_eventsMap.end())
+ return false;
+
+ return itEvent->second->Execute();
+}
+
+void CEventLog::ShowFullEventLog(EventLevel level /* = EventLevel::Basic */, bool includeHigherLevels /* = true */)
+{
+ // put together the path
+ std::string path = "events://";
+ if (level != EventLevel::Basic || !includeHigherLevels)
+ {
+ // add the level to the path
+ path += EventLevelToString(level);
+ // add whether to include higher levels or not to the path
+ if (includeHigherLevels)
+ path += "+";
+ }
+
+ // activate the full eventPtr log window
+ std::vector<std::string> params;
+ params.push_back(path);
+ params.emplace_back("return");
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_EVENT_LOG, params);
+}
+
+void CEventLog::SendMessage(const EventPtr& eventPtr, int message)
+{
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, message, 0, XFILE::CEventsDirectory::EventToFileItem(eventPtr));
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
diff --git a/xbmc/events/EventLog.h b/xbmc/events/EventLog.h
new file mode 100644
index 0000000..035e448
--- /dev/null
+++ b/xbmc/events/EventLog.h
@@ -0,0 +1,62 @@
+/*
+ * 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 "events/IEvent.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+#define NOTIFICATION_DISPLAY_TIME 5000
+#define NOTIFICATION_MESSAGE_TIME 1000
+
+typedef std::vector<EventPtr> Events;
+
+class CEventLog
+{
+public:
+ CEventLog() = default;
+ CEventLog(const CEventLog&) = delete;
+ CEventLog& operator=(CEventLog const&) = delete;
+ ~CEventLog() = default;
+
+ Events Get() const;
+ Events Get(EventLevel level, bool includeHigherLevels = false) const;
+ EventPtr Get(const std::string& eventIdentifier) const;
+
+ void Add(const EventPtr& event);
+ void Add(const EventPtr& event, bool withNotification, bool withSound = true);
+ void AddWithNotification(const EventPtr& event,
+ unsigned int displayTime = NOTIFICATION_DISPLAY_TIME,
+ unsigned int messageTime = NOTIFICATION_MESSAGE_TIME,
+ bool withSound = true);
+ void AddWithNotification(const EventPtr& event, bool withSound);
+ void Remove(const EventPtr& event);
+ void Remove(const std::string& eventIdentifier);
+ void Clear();
+ void Clear(EventLevel level, bool includeHigherLevels = false);
+
+ bool Execute(const std::string& eventIdentifier);
+
+ static std::string EventLevelToString(EventLevel level);
+ static EventLevel EventLevelFromString(const std::string& level);
+
+ void ShowFullEventLog(EventLevel level = EventLevel::Basic, bool includeHigherLevels = true);
+
+private:
+ void SendMessage(const EventPtr& event, int message);
+
+ typedef std::vector<EventPtr> EventsList;
+ typedef std::map<std::string, EventPtr> EventsMap;
+ EventsList m_events;
+ EventsMap m_eventsMap;
+ mutable CCriticalSection m_critical;
+};
diff --git a/xbmc/events/EventLogManager.cpp b/xbmc/events/EventLogManager.cpp
new file mode 100644
index 0000000..d56b88a
--- /dev/null
+++ b/xbmc/events/EventLogManager.cpp
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 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 "EventLogManager.h"
+
+#include "EventLog.h"
+
+#include <mutex>
+#include <utility>
+
+CEventLog& CEventLogManager::GetEventLog(unsigned int profileId)
+{
+ std::unique_lock<CCriticalSection> lock(m_eventMutex);
+
+ auto eventLog = m_eventLogs.find(profileId);
+ if (eventLog == m_eventLogs.end())
+ {
+ m_eventLogs.insert(std::make_pair(profileId, std::unique_ptr<CEventLog>(new CEventLog)));
+ eventLog = m_eventLogs.find(profileId);
+ }
+
+ return *eventLog->second;
+}
diff --git a/xbmc/events/EventLogManager.h b/xbmc/events/EventLogManager.h
new file mode 100644
index 0000000..033dbc0
--- /dev/null
+++ b/xbmc/events/EventLogManager.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 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 <map>
+#include <memory>
+
+class CEventLog;
+
+class CEventLogManager
+{
+public:
+ CEventLog &GetEventLog(unsigned int profileId);
+
+private:
+ std::map<unsigned int, std::unique_ptr<CEventLog>> m_eventLogs;
+ CCriticalSection m_eventMutex;
+};
diff --git a/xbmc/events/IEvent.h b/xbmc/events/IEvent.h
new file mode 100644
index 0000000..4ff8c02
--- /dev/null
+++ b/xbmc/events/IEvent.h
@@ -0,0 +1,47 @@
+/*
+ * 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 "utils/ISortable.h"
+
+#include <memory>
+#include <string>
+
+class CDateTime;
+
+enum class EventLevel
+{
+ Basic = 0,
+ Information = 1,
+ Warning = 2,
+ Error = 3,
+};
+
+class IEvent : public ISortable
+{
+public:
+ virtual ~IEvent() = default;
+
+ virtual const char* GetType() const = 0;
+ virtual std::string GetIdentifier() const = 0;
+ virtual EventLevel GetLevel() const = 0;
+ virtual std::string GetLabel() const = 0;
+ virtual std::string GetIcon() const = 0;
+ virtual std::string GetDescription() const = 0;
+ virtual std::string GetDetails() const = 0;
+ virtual std::string GetExecutionLabel() const = 0;
+ virtual CDateTime GetDateTime() const = 0;
+
+ virtual bool CanExecute() const = 0;
+ virtual bool Execute() const = 0;
+
+ void ToSortable(SortItem& sortable, Field field) const override = 0;
+};
+
+typedef std::shared_ptr<const IEvent> EventPtr;
diff --git a/xbmc/events/MediaLibraryEvent.cpp b/xbmc/events/MediaLibraryEvent.cpp
new file mode 100644
index 0000000..c079f5d
--- /dev/null
+++ b/xbmc/events/MediaLibraryEvent.cpp
@@ -0,0 +1,116 @@
+/*
+ * 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 "MediaLibraryEvent.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "utils/URIUtils.h"
+
+CMediaLibraryEvent::CMediaLibraryEvent(const MediaType& mediaType, const std::string& mediaPath, const CVariant& label, const CVariant& description, EventLevel level /* = EventLevel::Information */)
+ : CUniqueEvent(label, description, level),
+ m_mediaType(mediaType),
+ m_mediaPath(mediaPath)
+{ }
+
+CMediaLibraryEvent::CMediaLibraryEvent(const MediaType& mediaType, const std::string& mediaPath, const CVariant& label, const CVariant& description, const std::string& icon, EventLevel level /* = EventLevel::Information */)
+ : CUniqueEvent(label, description, icon, level),
+ m_mediaType(mediaType),
+ m_mediaPath(mediaPath)
+{ }
+
+CMediaLibraryEvent::CMediaLibraryEvent(const MediaType& mediaType, const std::string& mediaPath, const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, EventLevel level /* = EventLevel::Information */)
+ : CUniqueEvent(label, description, icon, details, level),
+ m_mediaType(mediaType),
+ m_mediaPath(mediaPath)
+{ }
+
+CMediaLibraryEvent::CMediaLibraryEvent(const MediaType& mediaType, const std::string& mediaPath, const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, const CVariant& executionLabel, EventLevel level /* = EventLevel::Information */)
+ : CUniqueEvent(label, description, icon, details, executionLabel, level),
+ m_mediaType(mediaType),
+ m_mediaPath(mediaPath)
+{ }
+
+std::string CMediaLibraryEvent::GetExecutionLabel() const
+{
+ std::string executionLabel = CUniqueEvent::GetExecutionLabel();
+ if (!executionLabel.empty())
+ return executionLabel;
+
+ return g_localizeStrings.Get(24140);
+}
+
+bool CMediaLibraryEvent::Execute() const
+{
+ if (!CanExecute())
+ return false;
+
+ int windowId = -1;
+ std::string path = m_mediaPath;
+ if (m_mediaType == MediaTypeVideo || m_mediaType == MediaTypeMovie || m_mediaType == MediaTypeVideoCollection ||
+ m_mediaType == MediaTypeTvShow || m_mediaType == MediaTypeSeason || m_mediaType == MediaTypeEpisode ||
+ m_mediaType == MediaTypeMusicVideo)
+ {
+ if (path.empty())
+ {
+ if (m_mediaType == MediaTypeVideo)
+ path = "sources://video/";
+ else if (m_mediaType == MediaTypeMovie)
+ path = "videodb://movies/titles/";
+ else if (m_mediaType == MediaTypeVideoCollection)
+ path = "videodb://movies/sets/";
+ else if (m_mediaType == MediaTypeMusicVideo)
+ path = "videodb://musicvideos/titles/";
+ else if (m_mediaType == MediaTypeTvShow || m_mediaType == MediaTypeSeason || m_mediaType == MediaTypeEpisode)
+ path = "videodb://tvshows/titles/";
+ }
+ else
+ {
+ //! @todo remove the filename for now as CGUIMediaWindow::GetDirectory() can't handle it
+ if (m_mediaType == MediaTypeMovie || m_mediaType == MediaTypeMusicVideo || m_mediaType == MediaTypeEpisode)
+ path = URIUtils::GetDirectory(path);
+ }
+
+ windowId = WINDOW_VIDEO_NAV;
+ }
+ else if (m_mediaType == MediaTypeMusic || m_mediaType == MediaTypeArtist ||
+ m_mediaType == MediaTypeAlbum || m_mediaType == MediaTypeSong)
+ {
+ if (path.empty())
+ {
+ if (m_mediaType == MediaTypeMusic)
+ path = "sources://music/";
+ else if (m_mediaType == MediaTypeArtist)
+ path = "musicdb://artists/";
+ else if (m_mediaType == MediaTypeAlbum)
+ path = "musicdb://albums/";
+ else if (m_mediaType == MediaTypeSong)
+ path = "musicdb://songs/";
+ }
+ else
+ {
+ //! @todo remove the filename for now as CGUIMediaWindow::GetDirectory() can't handle it
+ if (m_mediaType == MediaTypeSong)
+ path = URIUtils::GetDirectory(path);
+ }
+
+ windowId = WINDOW_MUSIC_NAV;
+ }
+
+ if (windowId < 0)
+ return false;
+
+ std::vector<std::string> params;
+ params.push_back(path);
+ params.emplace_back("return");
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowId, params);
+ return true;
+}
diff --git a/xbmc/events/MediaLibraryEvent.h b/xbmc/events/MediaLibraryEvent.h
new file mode 100644
index 0000000..effd4ac
--- /dev/null
+++ b/xbmc/events/MediaLibraryEvent.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "events/UniqueEvent.h"
+#include "media/MediaType.h"
+
+class CMediaLibraryEvent : public CUniqueEvent
+{
+public:
+ CMediaLibraryEvent(const MediaType& mediaType, const std::string& mediaPath, const CVariant& label, const CVariant& description, EventLevel level = EventLevel::Information);
+ CMediaLibraryEvent(const MediaType& mediaType, const std::string& mediaPath, const CVariant& label, const CVariant& description, const std::string& icon, EventLevel level = EventLevel::Information);
+ CMediaLibraryEvent(const MediaType& mediaType, const std::string& mediaPath, const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, EventLevel level = EventLevel::Information);
+ CMediaLibraryEvent(const MediaType& mediaType, const std::string& mediaPath, const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, const CVariant& executionLabel, EventLevel level = EventLevel::Information);
+ ~CMediaLibraryEvent() override = default;
+
+ const char* GetType() const override { return "MediaLibraryEvent"; }
+ std::string GetExecutionLabel() const override;
+
+ bool CanExecute() const override { return !m_mediaType.empty(); }
+ bool Execute() const override;
+
+protected:
+ MediaType m_mediaType;
+ std::string m_mediaPath;
+};
diff --git a/xbmc/events/NotificationEvent.h b/xbmc/events/NotificationEvent.h
new file mode 100644
index 0000000..b4be9b7
--- /dev/null
+++ b/xbmc/events/NotificationEvent.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "events/UniqueEvent.h"
+
+class CNotificationEvent : public CUniqueEvent
+{
+public:
+ CNotificationEvent(const CVariant& label, const CVariant& description, EventLevel level = EventLevel::Information)
+ : CUniqueEvent(label, description, level)
+ { }
+ CNotificationEvent(const CVariant& label, const CVariant& description, const std::string& icon, EventLevel level = EventLevel::Information)
+ : CUniqueEvent(label, description, icon, level)
+ { }
+ CNotificationEvent(const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, EventLevel level = EventLevel::Information)
+ : CUniqueEvent(label, description, icon, details, level)
+ { }
+ CNotificationEvent(const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, const CVariant& executionLabel, EventLevel level = EventLevel::Information)
+ : CUniqueEvent(label, description, icon, details, executionLabel, level)
+ { }
+ ~CNotificationEvent() override = default;
+
+ const char* GetType() const override { return "NotificationEvent"; }
+
+ bool CanExecute() const override { return false; }
+ bool Execute() const override { return true; }
+};
diff --git a/xbmc/events/UniqueEvent.h b/xbmc/events/UniqueEvent.h
new file mode 100644
index 0000000..4a03fc3
--- /dev/null
+++ b/xbmc/events/UniqueEvent.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "events/BaseEvent.h"
+#include "utils/StringUtils.h"
+
+class CUniqueEvent : public CBaseEvent
+{
+public:
+ ~CUniqueEvent() override = default;
+
+protected:
+ CUniqueEvent(const CVariant& label, const CVariant& description, EventLevel level = EventLevel::Information)
+ : CBaseEvent(StringUtils::CreateUUID(), label, description, level)
+ { }
+ CUniqueEvent(const CVariant& label, const CVariant& description, const std::string& icon, EventLevel level = EventLevel::Information)
+ : CBaseEvent(StringUtils::CreateUUID(), label, description, icon, level)
+ { }
+ CUniqueEvent(const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, EventLevel level = EventLevel::Information)
+ : CBaseEvent(StringUtils::CreateUUID(), label, description, icon, details, level)
+ { }
+ CUniqueEvent(const CVariant& label, const CVariant& description, const std::string& icon, const CVariant& details, const CVariant& executionLabel, EventLevel level = EventLevel::Information)
+ : CBaseEvent(StringUtils::CreateUUID(), label, description, icon, details, executionLabel, level)
+ { }
+};
diff --git a/xbmc/events/windows/CMakeLists.txt b/xbmc/events/windows/CMakeLists.txt
new file mode 100644
index 0000000..fcd0b94
--- /dev/null
+++ b/xbmc/events/windows/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES GUIViewStateEventLog.cpp
+ GUIWindowEventLog.cpp)
+
+set(HEADERS GUIViewStateEventLog.h
+ GUIWindowEventLog.h)
+
+core_add_library(events_windows)
diff --git a/xbmc/events/windows/GUIViewStateEventLog.cpp b/xbmc/events/windows/GUIViewStateEventLog.cpp
new file mode 100644
index 0000000..1f6dd66
--- /dev/null
+++ b/xbmc/events/windows/GUIViewStateEventLog.cpp
@@ -0,0 +1,35 @@
+/*
+ * 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 "GUIViewStateEventLog.h"
+
+#include "FileItem.h"
+#include "guilib/WindowIDs.h"
+#include "view/ViewState.h"
+#include "windowing/GraphicContext.h"
+
+CGUIViewStateEventLog::CGUIViewStateEventLog(const CFileItemList& items) : CGUIViewState(items)
+{
+ AddSortMethod(SortByDate, 552, LABEL_MASKS("%L", "%d", "%L", "%d")); // Label, Date | Label, Date
+
+ SetSortMethod(SortByDate);
+ SetViewAsControl(DEFAULT_VIEW_AUTO);
+
+ SetSortOrder(SortOrderDescending);
+ LoadViewState(items.GetPath(), WINDOW_EVENT_LOG);
+}
+
+void CGUIViewStateEventLog::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_EVENT_LOG);
+}
+
+std::string CGUIViewStateEventLog::GetExtensions()
+{
+ return "";
+}
diff --git a/xbmc/events/windows/GUIViewStateEventLog.h b/xbmc/events/windows/GUIViewStateEventLog.h
new file mode 100644
index 0000000..d7436ed
--- /dev/null
+++ b/xbmc/events/windows/GUIViewStateEventLog.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "view/GUIViewState.h"
+
+class CGUIViewStateEventLog : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateEventLog(const CFileItemList& items);
+ ~CGUIViewStateEventLog() override = default;
+
+ // specializations of CGUIViewState
+ bool HideExtensions() override { return true; }
+ bool HideParentDirItems() override { return true; }
+ bool DisableAddSourceButtons() override { return true; }
+
+protected:
+ // specializations of CGUIViewState
+ void SaveViewState() override;
+ std::string GetExtensions() override;
+};
+
diff --git a/xbmc/events/windows/GUIWindowEventLog.cpp b/xbmc/events/windows/GUIWindowEventLog.cpp
new file mode 100644
index 0000000..01ec4bb
--- /dev/null
+++ b/xbmc/events/windows/GUIWindowEventLog.cpp
@@ -0,0 +1,307 @@
+/*
+ * 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 "GUIWindowEventLog.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "events/EventLog.h"
+#include "filesystem/EventsDirectory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "input/Key.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "view/ViewStateSettings.h"
+
+#define CONTROL_BUTTON_CLEAR 20
+#define CONTROL_BUTTON_LEVEL 21
+#define CONTROL_BUTTON_LEVEL_ONLY 22
+
+CGUIWindowEventLog::CGUIWindowEventLog()
+ : CGUIMediaWindow(WINDOW_EVENT_LOG, "EventLog.xml")
+{ }
+
+CGUIWindowEventLog::~CGUIWindowEventLog() = default;
+
+bool CGUIWindowEventLog::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+
+ // check if we should clear all items
+ if (iControl == CONTROL_BUTTON_CLEAR)
+ {
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Clear(CViewStateSettings::GetInstance().GetEventLevel(),
+ CViewStateSettings::GetInstance().ShowHigherEventLevels());
+
+ // refresh the list
+ Refresh(true);
+ return true;
+ }
+
+ // check if we should change the level
+ if (iControl == CONTROL_BUTTON_LEVEL)
+ {
+ // update the event level
+ CViewStateSettings::GetInstance().CycleEventLevel();
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ // update the listing
+ Refresh();
+ return true;
+ }
+
+ // check if we should change the level
+ if (iControl == CONTROL_BUTTON_LEVEL_ONLY)
+ {
+ // update whether to show higher event levels
+ CViewStateSettings::GetInstance().ToggleShowHigherEventLevels();
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ // update the listing
+ Refresh();
+ return true;
+ }
+
+ // check if the user interacted with one of the events
+ if (m_viewControl.HasControl(iControl))
+ {
+ // get selected item
+ int itemIndex = m_viewControl.GetSelectedItem();
+ if (itemIndex < 0 || itemIndex >= m_vecItems->Size())
+ break;
+
+ CFileItemPtr item = m_vecItems->Get(itemIndex);
+ int actionId = message.GetParam1();
+
+ if (actionId == ACTION_DELETE_ITEM)
+ return OnDelete(item);
+ }
+
+ break;
+ }
+
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ CFileItemPtr item = std::dynamic_pointer_cast<CFileItem>(message.GetItem());
+ if (item == nullptr)
+ break;
+
+ switch (message.GetParam1())
+ {
+ case GUI_MSG_EVENT_ADDED:
+ OnEventAdded(item);
+ return true;
+
+ case GUI_MSG_EVENT_REMOVED:
+ OnEventRemoved(item);
+ return true;
+
+ default:
+ break;
+ }
+ }
+
+ default:
+ break;
+ }
+
+ return CGUIMediaWindow::OnMessage(message);
+}
+
+bool CGUIWindowEventLog::OnSelect(int item)
+{
+ if (item < 0 || item >= m_vecItems->Size())
+ return false;
+
+ return OnSelect(m_vecItems->Get(item));
+}
+
+void CGUIWindowEventLog::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ if (itemNumber < 0 && itemNumber >= m_vecItems->Size())
+ return;
+
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+ if (item == nullptr)
+ return;
+
+ std::string eventIdentifier = item->GetProperty(PROPERTY_EVENT_IDENTIFIER).asString();
+ if (eventIdentifier.empty())
+ return;
+
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (!eventLog)
+ return;
+
+ EventPtr eventPtr = eventLog->Get(eventIdentifier);
+ if (eventPtr == nullptr)
+ return;
+
+ buttons.Add(CONTEXT_BUTTON_DELETE, g_localizeStrings.Get(1210));
+}
+
+bool CGUIWindowEventLog::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ if (itemNumber < 0 && itemNumber >= m_vecItems->Size())
+ return false;
+
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+ if (item == nullptr)
+ return false;
+
+ switch (button)
+ {
+ case CONTEXT_BUTTON_DELETE:
+ return OnDelete(item);
+
+ default:
+ break;
+ }
+
+ return CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+void CGUIWindowEventLog::UpdateButtons()
+{
+ // only enable the "clear" button if there is something to clear
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BUTTON_CLEAR, m_vecItems->GetObjectCount() > 0);
+
+ EventLevel eventLevel = CViewStateSettings::GetInstance().GetEventLevel();
+ // set the label of the "level" button
+ SET_CONTROL_LABEL(CONTROL_BUTTON_LEVEL,
+ StringUtils::Format(g_localizeStrings.Get(14119),
+ g_localizeStrings.Get(14115 + (int)eventLevel)));
+
+ // set the label, value and enabled state of the "level only" button
+ SET_CONTROL_LABEL(CONTROL_BUTTON_LEVEL_ONLY, 14120);
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BUTTON_LEVEL_ONLY, CViewStateSettings::GetInstance().ShowHigherEventLevels());
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BUTTON_LEVEL_ONLY, eventLevel < EventLevel::Error);
+
+ CGUIMediaWindow::UpdateButtons();
+}
+
+bool CGUIWindowEventLog::GetDirectory(const std::string &strDirectory, CFileItemList &items)
+{
+ bool result = CGUIMediaWindow::GetDirectory(strDirectory, items);
+
+ EventLevel currentLevel = CViewStateSettings::GetInstance().GetEventLevel();
+ bool showHigherLevels = CViewStateSettings::GetInstance().ShowHigherEventLevels();
+
+ CFileItemList filteredItems(items.GetPath());
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items.Get(i);
+ if (item->IsParentFolder())
+ {
+ filteredItems.Add(item);
+ continue;
+ }
+
+ if (!item->HasProperty(PROPERTY_EVENT_LEVEL))
+ continue;
+
+ EventLevel level = CEventLog::EventLevelFromString(item->GetProperty(PROPERTY_EVENT_LEVEL).asString());
+ if (level == currentLevel ||
+ (level > currentLevel && showHigherLevels))
+ filteredItems.Add(item);
+ }
+
+ items.ClearItems();
+ items.Append(filteredItems);
+
+ return result;
+}
+
+bool CGUIWindowEventLog::OnSelect(const CFileItemPtr& item)
+{
+ if (item == nullptr)
+ return false;
+
+ OnExecute(item);
+ return true;
+}
+
+bool CGUIWindowEventLog::OnDelete(const CFileItemPtr& item)
+{
+ if (item == nullptr)
+ return false;
+
+ std::string eventIdentifier = item->GetProperty(PROPERTY_EVENT_IDENTIFIER).asString();
+ if (eventIdentifier.empty())
+ return false;
+
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (!eventLog)
+ return false;
+
+ eventLog->Remove(eventIdentifier);
+ return true;
+}
+
+bool CGUIWindowEventLog::OnExecute(const CFileItemPtr& item)
+{
+ if (item == nullptr)
+ return false;
+
+ std::string eventIdentifier = item->GetProperty(PROPERTY_EVENT_IDENTIFIER).asString();
+ if (eventIdentifier.empty())
+ return false;
+
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (!eventLog)
+ return false;
+
+ const EventPtr eventPtr = eventLog->Get(eventIdentifier);
+ if (eventPtr == nullptr)
+ return false;
+
+ if (!eventPtr->CanExecute())
+ return true;
+
+ return eventPtr->Execute();
+}
+
+void CGUIWindowEventLog::OnEventAdded(const CFileItemPtr& item)
+{
+ if (!IsActive())
+ return;
+
+ Refresh(true);
+}
+
+void CGUIWindowEventLog::OnEventRemoved(const CFileItemPtr& item)
+{
+ if (!IsActive())
+ return;
+
+ int selectedItemIndex = -1;
+ if (item != nullptr)
+ {
+ selectedItemIndex = m_viewControl.GetSelectedItem();
+ // only update the selected item index when the deleted item is focused
+ if (m_vecItems->Get(selectedItemIndex)->GetProperty(PROPERTY_EVENT_IDENTIFIER).asString() != item->GetProperty(PROPERTY_EVENT_IDENTIFIER).asString())
+ selectedItemIndex = -1;
+ }
+
+ Refresh(true);
+
+ // update the selected item
+ if (selectedItemIndex >= 0)
+ m_viewControl.SetSelectedItem(selectedItemIndex);
+}
diff --git a/xbmc/events/windows/GUIWindowEventLog.h b/xbmc/events/windows/GUIWindowEventLog.h
new file mode 100644
index 0000000..d3cb22a
--- /dev/null
+++ b/xbmc/events/windows/GUIWindowEventLog.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "windows/GUIMediaWindow.h"
+
+class CGUIWindowEventLog : public CGUIMediaWindow
+{
+public:
+ CGUIWindowEventLog();
+ ~CGUIWindowEventLog() override;
+
+ // specialization of CGUIControl
+ bool OnMessage(CGUIMessage& message) override;
+
+protected:
+ // specialization of CGUIMediaWindow
+ bool OnSelect(int item) override;
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ void UpdateButtons() override;
+ bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override;
+ std::string GetRootPath() const override { return "events://"; }
+
+ bool OnSelect(const CFileItemPtr& item);
+ bool OnDelete(const CFileItemPtr& item);
+ bool OnExecute(const CFileItemPtr& item);
+
+ void OnEventAdded(const CFileItemPtr& item);
+ void OnEventRemoved(const CFileItemPtr& item);
+};
diff --git a/xbmc/favourites/CMakeLists.txt b/xbmc/favourites/CMakeLists.txt
new file mode 100644
index 0000000..beee8bd
--- /dev/null
+++ b/xbmc/favourites/CMakeLists.txt
@@ -0,0 +1,17 @@
+set(SOURCES ContextMenus.cpp
+ GUIDialogFavourites.cpp
+ GUIViewStateFavourites.cpp
+ GUIWindowFavourites.cpp
+ FavouritesService.cpp
+ FavouritesURL.cpp
+ FavouritesUtils.cpp)
+
+set(HEADERS ContextMenus.h
+ GUIDialogFavourites.h
+ GUIViewStateFavourites.h
+ GUIWindowFavourites.h
+ FavouritesService.h
+ FavouritesURL.h
+ FavouritesUtils.h)
+
+core_add_library(favourites)
diff --git a/xbmc/favourites/ContextMenus.cpp b/xbmc/favourites/ContextMenus.cpp
new file mode 100644
index 0000000..821841f
--- /dev/null
+++ b/xbmc/favourites/ContextMenus.cpp
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenus.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "favourites/FavouritesService.h"
+#include "favourites/FavouritesUtils.h"
+#include "utils/URIUtils.h"
+
+namespace CONTEXTMENU
+{
+ bool CFavouriteContextMenuAction::IsVisible(const CFileItem& item) const
+ {
+ return URIUtils::IsProtocol(item.GetPath(), "favourites");
+ }
+
+ bool CFavouriteContextMenuAction::Execute(const std::shared_ptr<CFileItem>& item) const
+ {
+ CFileItemList items;
+ CServiceBroker::GetFavouritesService().GetAll(items);
+ for (const auto& favourite : items)
+ {
+ if (favourite->GetPath() == item->GetPath())
+ {
+ if (DoExecute(items, favourite))
+ return CServiceBroker::GetFavouritesService().Save(items);
+ }
+ }
+ return false;
+ }
+
+ bool CMoveUpFavourite::DoExecute(CFileItemList& items,
+ const std::shared_ptr<CFileItem>& item) const
+ {
+ return FAVOURITES_UTILS::MoveItem(items, item, -1);
+ }
+
+ bool CMoveUpFavourite::IsVisible(const CFileItem& item) const
+ {
+ return CFavouriteContextMenuAction::IsVisible(item) &&
+ FAVOURITES_UTILS::ShouldEnableMoveItems();
+ }
+
+ bool CMoveDownFavourite::DoExecute(CFileItemList& items,
+ const std::shared_ptr<CFileItem>& item) const
+ {
+ return FAVOURITES_UTILS::MoveItem(items, item, +1);
+ }
+
+ bool CMoveDownFavourite::IsVisible(const CFileItem& item) const
+ {
+ return CFavouriteContextMenuAction::IsVisible(item) &&
+ FAVOURITES_UTILS::ShouldEnableMoveItems();
+ }
+
+ bool CRemoveFavourite::DoExecute(CFileItemList& items,
+ const std::shared_ptr<CFileItem>& item) const
+ {
+ return FAVOURITES_UTILS::RemoveItem(items, item);
+ }
+
+ bool CRenameFavourite::DoExecute(CFileItemList&, const std::shared_ptr<CFileItem>& item) const
+ {
+ return FAVOURITES_UTILS::ChooseAndSetNewName(*item);
+ }
+
+ bool CChooseThumbnailForFavourite::DoExecute(CFileItemList&,
+ const std::shared_ptr<CFileItem>& item) const
+ {
+ return FAVOURITES_UTILS::ChooseAndSetNewThumbnail(*item);
+ }
+
+} // namespace CONTEXTMENU
diff --git a/xbmc/favourites/ContextMenus.h b/xbmc/favourites/ContextMenus.h
new file mode 100644
index 0000000..fe1edc8
--- /dev/null
+++ b/xbmc/favourites/ContextMenus.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenuItem.h"
+
+#include <memory>
+
+class CFileItemList;
+
+namespace CONTEXTMENU
+{
+
+class CFavouriteContextMenuAction : public CStaticContextMenuAction
+{
+public:
+ explicit CFavouriteContextMenuAction(uint32_t label) : CStaticContextMenuAction(label) {}
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+
+protected:
+ ~CFavouriteContextMenuAction() override = default;
+ virtual bool DoExecute(CFileItemList& items, const std::shared_ptr<CFileItem>& item) const = 0;
+};
+
+class CMoveUpFavourite : public CFavouriteContextMenuAction
+{
+public:
+ CMoveUpFavourite() : CFavouriteContextMenuAction(13332) {} // Move up
+ bool IsVisible(const CFileItem& item) const override;
+
+protected:
+ bool DoExecute(CFileItemList& items, const std::shared_ptr<CFileItem>& item) const override;
+};
+
+class CMoveDownFavourite : public CFavouriteContextMenuAction
+{
+public:
+ CMoveDownFavourite() : CFavouriteContextMenuAction(13333) {} // Move down
+ bool IsVisible(const CFileItem& item) const override;
+
+protected:
+ bool DoExecute(CFileItemList& items, const std::shared_ptr<CFileItem>& item) const override;
+};
+
+class CRemoveFavourite : public CFavouriteContextMenuAction
+{
+public:
+ CRemoveFavourite() : CFavouriteContextMenuAction(15015) {} // Remove
+protected:
+ bool DoExecute(CFileItemList& items, const std::shared_ptr<CFileItem>& item) const override;
+};
+
+class CRenameFavourite : public CFavouriteContextMenuAction
+{
+public:
+ CRenameFavourite() : CFavouriteContextMenuAction(118) {} // Rename
+protected:
+ bool DoExecute(CFileItemList& items, const std::shared_ptr<CFileItem>& item) const override;
+};
+
+class CChooseThumbnailForFavourite : public CFavouriteContextMenuAction
+{
+public:
+ CChooseThumbnailForFavourite() : CFavouriteContextMenuAction(20019) {} // Choose thumbnail
+protected:
+ bool DoExecute(CFileItemList& items, const std::shared_ptr<CFileItem>& item) const override;
+};
+
+}
diff --git a/xbmc/favourites/FavouritesService.cpp b/xbmc/favourites/FavouritesService.cpp
new file mode 100644
index 0000000..8659445
--- /dev/null
+++ b/xbmc/favourites/FavouritesService.cpp
@@ -0,0 +1,273 @@
+/*
+ * 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 "FavouritesService.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "favourites/FavouritesURL.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "utils/ContentUtils.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+namespace
+{
+bool IsMediasourceOfFavItemUnlocked(const std::shared_ptr<CFileItem>& item)
+{
+ if (!item)
+ {
+ CLog::Log(LOGERROR, "{}: No item passed (nullptr).", __func__);
+ return true;
+ }
+
+ if (!item->IsFavourite())
+ {
+ CLog::Log(LOGERROR, "{}: Wrong item passed (not a favourite).", __func__);
+ return true;
+ }
+
+ const auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ {
+ CLog::Log(LOGERROR, "{}: returned nullptr.", __func__);
+ return true;
+ }
+
+ const auto profileManager = settingsComponent->GetProfileManager();
+ if (!profileManager)
+ {
+ CLog::Log(LOGERROR, "{}: returned nullptr.", __func__);
+ return true;
+ }
+
+ const CFavouritesURL url(item->GetPath());
+ if (!url.IsValid())
+ {
+ CLog::Log(LOGERROR, "{}: Invalid exec string (syntax error).", __func__);
+ return true;
+ }
+
+ const CFavouritesURL::Action action = url.GetAction();
+
+ if (action != CFavouritesURL::Action::PLAY_MEDIA &&
+ action != CFavouritesURL::Action::SHOW_PICTURE)
+ return true;
+
+ const CFileItem itemToCheck(url.GetTarget(), url.IsDir());
+
+ if (action == CFavouritesURL::Action::PLAY_MEDIA)
+ {
+ if (itemToCheck.IsVideo())
+ {
+ if (!profileManager->GetCurrentProfile().videoLocked())
+ return g_passwordManager.IsMediaFileUnlocked("video", itemToCheck.GetPath());
+
+ return false;
+ }
+ else if (itemToCheck.IsAudio())
+ {
+ if (!profileManager->GetCurrentProfile().musicLocked())
+ return g_passwordManager.IsMediaFileUnlocked("music", itemToCheck.GetPath());
+
+ return false;
+ }
+ }
+ else if (action == CFavouritesURL::Action::SHOW_PICTURE && itemToCheck.IsPicture())
+ {
+ if (!profileManager->GetCurrentProfile().picturesLocked())
+ return g_passwordManager.IsMediaFileUnlocked("pictures", itemToCheck.GetPath());
+
+ return false;
+ }
+
+ return true;
+}
+
+bool LoadFromFile(const std::string& strPath, CFileItemList& items)
+{
+ CXBMCTinyXML doc;
+ if (!doc.LoadFile(strPath))
+ {
+ CLog::Log(LOGERROR, "Unable to load {} (row {} column {})", strPath, doc.Row(), doc.Column());
+ return false;
+ }
+ TiXmlElement *root = doc.RootElement();
+ if (!root || strcmp(root->Value(), "favourites"))
+ {
+ CLog::Log(LOGERROR, "Favourites.xml doesn't contain the <favourites> root element");
+ return false;
+ }
+
+ TiXmlElement *favourite = root->FirstChildElement("favourite");
+ while (favourite)
+ {
+ // format:
+ // <favourite name="Cool Video" thumb="foo.jpg">PlayMedia(c:\videos\cool_video.avi)</favourite>
+ // <favourite name="My Album" thumb="bar.tbn">ActivateWindow(MyMusic,c:\music\my album)</favourite>
+ // <favourite name="Apple Movie Trailers" thumb="path_to_thumb.png">RunScript(special://xbmc/scripts/apple movie trailers/default.py)</favourite>
+ const char *name = favourite->Attribute("name");
+ const char *thumb = favourite->Attribute("thumb");
+ if (name && favourite->FirstChild())
+ {
+ const std::string favURL(
+ CFavouritesURL(CExecString(favourite->FirstChild()->Value())).GetURL());
+ if (!items.Contains(favURL))
+ {
+ const CFileItemPtr item(std::make_shared<CFileItem>(name));
+ item->SetPath(favURL);
+ if (thumb)
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ }
+ }
+ favourite = favourite->NextSiblingElement("favourite");
+ }
+ return true;
+}
+} // unnamed namespace
+
+CFavouritesService::CFavouritesService(std::string userDataFolder) : m_favourites("favourites://")
+{
+ ReInit(std::move(userDataFolder));
+}
+
+void CFavouritesService::ReInit(std::string userDataFolder)
+{
+ m_userDataFolder = std::move(userDataFolder);
+ m_favourites.Clear();
+ m_favourites.SetContent("favourites");
+
+ std::string favourites = "special://xbmc/system/favourites.xml";
+ if (CFileUtils::Exists(favourites))
+ LoadFromFile(favourites, m_favourites);
+ else
+ CLog::Log(LOGDEBUG, "CFavourites::Load - no system favourites found, skipping");
+
+ favourites = URIUtils::AddFileToFolder(m_userDataFolder, "favourites.xml");
+ if (CFileUtils::Exists(favourites))
+ LoadFromFile(favourites, m_favourites);
+ else
+ CLog::Log(LOGDEBUG, "CFavourites::Load - no userdata favourites found, skipping");
+}
+
+bool CFavouritesService::Persist()
+{
+ CXBMCTinyXML doc;
+ TiXmlElement xmlRootElement("favourites");
+ TiXmlNode *rootNode = doc.InsertEndChild(xmlRootElement);
+ if (!rootNode)
+ return false;
+
+ for (const auto& item : m_favourites)
+ {
+ TiXmlElement favNode("favourite");
+ favNode.SetAttribute("name", item->GetLabel().c_str());
+ if (item->HasArt("thumb"))
+ favNode.SetAttribute("thumb", item->GetArt("thumb").c_str());
+
+ TiXmlText execute(CFavouritesURL(item->GetPath()).GetExecString());
+ favNode.InsertEndChild(execute);
+ rootNode->InsertEndChild(favNode);
+ }
+
+ auto path = URIUtils::AddFileToFolder(m_userDataFolder, "favourites.xml");
+ return doc.SaveFile(path);
+}
+
+bool CFavouritesService::Save(const CFileItemList& items)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ m_favourites.Clear();
+ m_favourites.Copy(items);
+ Persist();
+ }
+ OnUpdated();
+ return true;
+}
+
+void CFavouritesService::OnUpdated()
+{
+ m_events.Publish(FavouritesUpdated{});
+}
+
+std::string CFavouritesService::GetFavouritesUrl(const CFileItem& item, int contextWindow) const
+{
+ return CFavouritesURL(item, contextWindow).GetURL();
+}
+
+bool CFavouritesService::AddOrRemove(const CFileItem& item, int contextWindow)
+{
+ auto favUrl = GetFavouritesUrl(item, contextWindow);
+ {
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ CFileItemPtr match = m_favourites.Get(favUrl);
+ if (match)
+ { // remove the item
+ m_favourites.Remove(match.get());
+ }
+ else
+ { // create our new favourite item
+ const CFileItemPtr favourite(std::make_shared<CFileItem>(item.GetLabel()));
+ if (item.GetLabel().empty())
+ favourite->SetLabel(CUtil::GetTitleFromPath(item.GetPath(), item.m_bIsFolder));
+ favourite->SetArt("thumb", ContentUtils::GetPreferredArtImage(item));
+ favourite->SetPath(favUrl);
+ m_favourites.Add(favourite);
+ }
+ Persist();
+ }
+ OnUpdated();
+ return true;
+}
+
+bool CFavouritesService::IsFavourited(const CFileItem& item, int contextWindow) const
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ return m_favourites.Contains(GetFavouritesUrl(item, contextWindow));
+}
+
+void CFavouritesService::GetAll(CFileItemList& items) const
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ items.Clear();
+ if (g_passwordManager.IsMasterLockUnlocked(false)) // don't prompt
+ {
+ items.Copy(m_favourites, true); // copy items
+ }
+ else
+ {
+ for (const auto& fav : m_favourites)
+ {
+ if (IsMediasourceOfFavItemUnlocked(fav))
+ items.Add(fav);
+ }
+ }
+
+ int index = 0;
+ for (const auto& item : items)
+ {
+ const CFavouritesURL favURL(item->GetPath());
+ item->SetProperty("favourite.action", favURL.GetActionLabel());
+ item->SetProperty("favourite.provider", favURL.GetProviderLabel());
+ item->SetProperty("favourite.index", index++);
+ }
+}
+
+void CFavouritesService::RefreshFavourites()
+{
+ m_events.Publish(FavouritesUpdated{});
+}
diff --git a/xbmc/favourites/FavouritesService.h b/xbmc/favourites/FavouritesService.h
new file mode 100644
index 0000000..5a16459
--- /dev/null
+++ b/xbmc/favourites/FavouritesService.h
@@ -0,0 +1,56 @@
+/*
+ * 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 "FileItem.h"
+#include "threads/CriticalSection.h"
+#include "utils/EventStream.h"
+
+#include <string>
+#include <vector>
+
+class CFavouritesService
+{
+public:
+ explicit CFavouritesService(std::string userDataFolder);
+ virtual ~CFavouritesService() = default;
+
+ /** For profiles*/
+ void ReInit(std::string userDataFolder);
+
+ bool IsFavourited(const CFileItem& item, int contextWindow) const;
+ void GetAll(CFileItemList& items) const;
+ bool AddOrRemove(const CFileItem& item, int contextWindow);
+ bool Save(const CFileItemList& items);
+
+ /*! \brief Refresh favourites for directory providers, e.g. the GUI needs to be updated
+ */
+ void RefreshFavourites();
+
+ struct FavouritesUpdated { };
+
+ CEventStream<FavouritesUpdated>& Events() { return m_events; }
+
+private:
+ CFavouritesService() = delete;
+ CFavouritesService(const CFavouritesService&) = delete;
+ CFavouritesService& operator=(const CFavouritesService&) = delete;
+ CFavouritesService(CFavouritesService&&) = delete;
+ CFavouritesService& operator=(CFavouritesService&&) = delete;
+
+ void OnUpdated();
+ bool Persist();
+ std::string GetFavouritesUrl(const CFileItem &item, int contextWindow) const;
+
+ std::string m_userDataFolder;
+ CFileItemList m_favourites;
+ CEventSource<FavouritesUpdated> m_events;
+ mutable CCriticalSection m_criticalSection;
+};
+
diff --git a/xbmc/favourites/FavouritesURL.cpp b/xbmc/favourites/FavouritesURL.cpp
new file mode 100644
index 0000000..a277256
--- /dev/null
+++ b/xbmc/favourites/FavouritesURL.cpp
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2022 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 "FavouritesURL.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddon.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/WindowTranslator.h"
+#include "utils/ExecString.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <vector>
+
+namespace
+{
+std::string GetActionString(CFavouritesURL::Action action)
+{
+ switch (action)
+ {
+ case CFavouritesURL::Action::ACTIVATE_WINDOW:
+ return "activatewindow";
+ case CFavouritesURL::Action::PLAY_MEDIA:
+ return "playmedia";
+ case CFavouritesURL::Action::SHOW_PICTURE:
+ return "showpicture";
+ case CFavouritesURL::Action::RUN_SCRIPT:
+ return "runscript";
+ case CFavouritesURL::Action::RUN_ADDON:
+ return "runaddon";
+ case CFavouritesURL::Action::START_ANDROID_ACTIVITY:
+ return "startandroidactivity";
+ default:
+ return {};
+ }
+}
+
+CFavouritesURL::Action GetActionId(const std::string& actionString)
+{
+ if (actionString == "activatewindow")
+ return CFavouritesURL::Action::ACTIVATE_WINDOW;
+ else if (actionString == "playmedia")
+ return CFavouritesURL::Action::PLAY_MEDIA;
+ else if (actionString == "showpicture")
+ return CFavouritesURL::Action::SHOW_PICTURE;
+ else if (actionString == "runscript")
+ return CFavouritesURL::Action::RUN_SCRIPT;
+ else if (actionString == "runaddon")
+ return CFavouritesURL::Action::RUN_ADDON;
+ else if (actionString == "startandroidactivity")
+ return CFavouritesURL::Action::START_ANDROID_ACTIVITY;
+ else
+ return CFavouritesURL::Action::UNKNOWN;
+}
+} // namespace
+
+CFavouritesURL::CFavouritesURL(const std::string& favouritesURL)
+{
+ const CURL url(favouritesURL);
+ if (url.GetProtocol() != "favourites")
+ {
+ CLog::LogF(LOGERROR, "Invalid protocol");
+ return;
+ }
+
+ m_exec = CExecString(CURL::Decode(url.GetHostName()));
+ if (m_exec.IsValid())
+ m_valid = Parse(GetActionId(m_exec.GetFunction()), m_exec.GetParams());
+}
+
+CFavouritesURL::CFavouritesURL(const CExecString& execString) : m_exec(execString)
+{
+ if (m_exec.IsValid())
+ m_valid = Parse(GetActionId(m_exec.GetFunction()), m_exec.GetParams());
+}
+
+CFavouritesURL::CFavouritesURL(Action action, const std::vector<std::string>& params)
+ : m_exec(GetActionString(action), params)
+{
+ if (m_exec.IsValid())
+ m_valid = Parse(action, params);
+}
+
+CFavouritesURL::CFavouritesURL(const CFileItem& item, int contextWindow)
+ : m_exec(item, std::to_string(contextWindow))
+{
+ if (m_exec.IsValid())
+ m_valid = Parse(GetActionId(m_exec.GetFunction()), m_exec.GetParams());
+}
+
+bool CFavouritesURL::Parse(CFavouritesURL::Action action, const std::vector<std::string>& params)
+{
+ m_action = action;
+
+ bool pathIsAddonID = false;
+
+ switch (action)
+ {
+ case Action::ACTIVATE_WINDOW:
+ if (params.empty())
+ {
+ CLog::LogF(LOGERROR, "Missing parameter");
+ return false;
+ }
+
+ // mandatory: window name/id
+ m_windowId = CWindowTranslator::TranslateWindow(params[0]);
+
+ if (params.size() > 1)
+ {
+ // optional: target url/path
+ m_target = StringUtils::DeParamify(params[1]);
+ }
+ m_actionLabel =
+ StringUtils::Format(g_localizeStrings.Get(15220), // Show content in '<windowname>'
+ g_localizeStrings.Get(m_windowId));
+ break;
+ case Action::PLAY_MEDIA:
+ if (params.empty())
+ {
+ CLog::LogF(LOGERROR, "Missing parameter");
+ return false;
+ }
+ m_target = StringUtils::DeParamify(params[0]);
+ m_actionLabel = g_localizeStrings.Get(15218); // Play media
+ break;
+ case Action::SHOW_PICTURE:
+ if (params.empty())
+ {
+ CLog::LogF(LOGERROR, "Missing parameter");
+ return false;
+ }
+ m_target = StringUtils::DeParamify(params[0]);
+ m_actionLabel = g_localizeStrings.Get(15219); // Show picture
+ break;
+ case Action::RUN_SCRIPT:
+ if (params.empty())
+ {
+ CLog::LogF(LOGERROR, "Missing parameter");
+ return false;
+ }
+ m_target = StringUtils::DeParamify(params[0]);
+ m_actionLabel = g_localizeStrings.Get(15221); // Execute script
+ pathIsAddonID = true;
+ break;
+ case Action::RUN_ADDON:
+ if (params.empty())
+ {
+ CLog::LogF(LOGERROR, "Missing parameter");
+ return false;
+ }
+ m_target = StringUtils::DeParamify(params[0]);
+ m_actionLabel = g_localizeStrings.Get(15223); // Execute add-on
+ pathIsAddonID = true;
+ break;
+ case Action::START_ANDROID_ACTIVITY:
+ if (params.empty())
+ {
+ CLog::LogF(LOGERROR, "Missing parameter");
+ return false;
+ }
+ m_target = StringUtils::DeParamify(params[0]);
+ m_actionLabel = g_localizeStrings.Get(15222); // Execute Android app
+ break;
+ default:
+ if (params.empty())
+ {
+ CLog::LogF(LOGERROR, "Missing parameter");
+ return false;
+ }
+ m_action = CFavouritesURL::Action::UNKNOWN;
+ m_target = StringUtils::DeParamify(params[0]);
+ m_actionLabel = g_localizeStrings.Get(15224); // Other / Unknown
+ break;
+ }
+
+ m_path = StringUtils::Format("favourites://{}", CURL::Encode(GetExecString()));
+ m_isDir = URIUtils::HasSlashAtEnd(m_target, true);
+
+ if (pathIsAddonID || URIUtils::IsPlugin(m_target))
+ {
+ // get the add-on name
+ const std::string plugin = pathIsAddonID ? m_target : CURL(m_target).GetHostName();
+
+ ADDON::AddonPtr addon;
+ CServiceBroker::GetAddonMgr().GetAddon(plugin, addon, ADDON::OnlyEnabled::CHOICE_NO);
+ if (addon)
+ m_providerLabel = addon->Name();
+ }
+ if (m_providerLabel.empty())
+ m_providerLabel = CSysInfo::GetAppName();
+
+ return true;
+}
diff --git a/xbmc/favourites/FavouritesURL.h b/xbmc/favourites/FavouritesURL.h
new file mode 100644
index 0000000..7261484
--- /dev/null
+++ b/xbmc/favourites/FavouritesURL.h
@@ -0,0 +1,66 @@
+/*
+ * 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 "utils/ExecString.h"
+
+#include <string>
+#include <vector>
+
+class CFileItem;
+
+class CFavouritesURL
+{
+public:
+ enum class Action
+ {
+ UNKNOWN,
+ ACTIVATE_WINDOW,
+ PLAY_MEDIA,
+ SHOW_PICTURE,
+ RUN_SCRIPT,
+ RUN_ADDON,
+ START_ANDROID_ACTIVITY,
+ };
+
+ explicit CFavouritesURL(const std::string& favouritesURL);
+ explicit CFavouritesURL(const CExecString& execString);
+ CFavouritesURL(Action action, const std::vector<std::string>& params);
+ CFavouritesURL(const CFileItem& item, int contextWindow);
+
+ virtual ~CFavouritesURL() = default;
+
+ std::string GetURL() const { return m_path; }
+
+ bool IsValid() const { return m_valid && m_exec.IsValid(); }
+
+ bool IsDir() const { return m_isDir; }
+
+ std::string GetExecString() const { return m_exec.GetExecString(); }
+ Action GetAction() const { return m_action; }
+ std::vector<std::string> GetParams() const { return m_exec.GetParams(); }
+ std::string GetTarget() const { return m_target; }
+ int GetWindowID() const { return m_windowId; }
+ std::string GetActionLabel() const { return m_actionLabel; }
+ std::string GetProviderLabel() const { return m_providerLabel; }
+
+private:
+ bool Parse(CFavouritesURL::Action action, const std::vector<std::string>& params);
+
+ CExecString m_exec;
+
+ bool m_valid{false};
+ std::string m_path;
+ Action m_action{Action::UNKNOWN};
+ std::string m_target;
+ int m_windowId{-1};
+ bool m_isDir{false};
+ std::string m_actionLabel;
+ std::string m_providerLabel;
+};
diff --git a/xbmc/favourites/FavouritesUtils.cpp b/xbmc/favourites/FavouritesUtils.cpp
new file mode 100644
index 0000000..9a055c5
--- /dev/null
+++ b/xbmc/favourites/FavouritesUtils.cpp
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023 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 "FavouritesUtils.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "favourites/GUIWindowFavourites.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "storage/MediaManager.h"
+#include "utils/Variant.h"
+#include "view/GUIViewState.h"
+
+#include <string>
+
+namespace FAVOURITES_UTILS
+{
+bool ChooseAndSetNewName(CFileItem& item)
+{
+ std::string label = item.GetLabel();
+ if (CGUIKeyboardFactory::ShowAndGetInput(label, CVariant{g_localizeStrings.Get(16008)},
+ false)) // Enter new title
+ {
+ item.SetLabel(label);
+ return true;
+ }
+ return false;
+}
+
+bool ChooseAndSetNewThumbnail(CFileItem& item)
+{
+ CFileItemList prefilledItems;
+ if (item.HasArt("thumb"))
+ {
+ const auto current = std::make_shared<CFileItem>("thumb://Current", false);
+ current->SetArt("thumb", item.GetArt("thumb"));
+ current->SetLabel(g_localizeStrings.Get(20016)); // Current thumb
+ prefilledItems.Add(current);
+ }
+
+ const auto none = std::make_shared<CFileItem>("thumb://None", false);
+ none->SetArt("icon", item.GetArt("icon"));
+ none->SetLabel(g_localizeStrings.Get(20018)); // No thumb
+ prefilledItems.Add(none);
+
+ std::string thumb;
+ VECSOURCES sources;
+ CServiceBroker::GetMediaManager().GetLocalDrives(sources);
+ if (CGUIDialogFileBrowser::ShowAndGetImage(prefilledItems, sources, g_localizeStrings.Get(1030),
+ thumb)) // Browse for image
+ {
+ item.SetArt("thumb", thumb);
+ return true;
+ }
+ return false;
+}
+
+bool MoveItem(CFileItemList& items, const std::shared_ptr<CFileItem>& item, int amount)
+{
+ if (items.Size() < 2 || amount == 0)
+ return false;
+
+ int itemPos = -1;
+ for (const auto& i : items)
+ {
+ itemPos++;
+
+ if (i->GetPath() == item->GetPath())
+ break;
+ }
+
+ if (itemPos < 0 || itemPos >= items.Size())
+ return false;
+
+ int nextItem = (itemPos + amount) % items.Size();
+ if (nextItem < 0)
+ {
+ const auto itemToAdd(item);
+ items.Remove(itemPos);
+ items.Add(itemToAdd);
+ }
+ else if (nextItem == 0)
+ {
+ const auto itemToAdd(item);
+ items.Remove(itemPos);
+ items.AddFront(itemToAdd, 0);
+ }
+ else
+ {
+ items.Swap(itemPos, nextItem);
+ }
+ return true;
+}
+
+bool RemoveItem(CFileItemList& items, const std::shared_ptr<CFileItem>& item)
+{
+ int iBefore = items.Size();
+ items.Remove(item.get());
+ return items.Size() == iBefore - 1;
+}
+
+bool ShouldEnableMoveItems()
+{
+ auto& mgr = CServiceBroker::GetGUI()->GetWindowManager();
+ CGUIWindowFavourites* window = mgr.GetWindow<CGUIWindowFavourites>(WINDOW_FAVOURITES);
+ if (window && window->IsActive())
+ {
+ const CGUIViewState* state = window->GetViewState();
+ if (state && state->GetSortMethod().sortBy != SortByUserPreference)
+ return false; // in favs window, allow move only if current sort method is by user preference
+ }
+ return true;
+}
+
+} // namespace FAVOURITES_UTILS
diff --git a/xbmc/favourites/FavouritesUtils.h b/xbmc/favourites/FavouritesUtils.h
new file mode 100644
index 0000000..0388b4d
--- /dev/null
+++ b/xbmc/favourites/FavouritesUtils.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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 <memory>
+
+class CFileItem;
+class CFileItemList;
+
+namespace FAVOURITES_UTILS
+{
+bool ChooseAndSetNewName(CFileItem& item);
+bool ChooseAndSetNewThumbnail(CFileItem& item);
+bool MoveItem(CFileItemList& items, const std::shared_ptr<CFileItem>& item, int amount);
+bool RemoveItem(CFileItemList& items, const std::shared_ptr<CFileItem>& item);
+bool ShouldEnableMoveItems();
+
+} // namespace FAVOURITES_UTILS
diff --git a/xbmc/favourites/GUIDialogFavourites.cpp b/xbmc/favourites/GUIDialogFavourites.cpp
new file mode 100644
index 0000000..7af6d29
--- /dev/null
+++ b/xbmc/favourites/GUIDialogFavourites.cpp
@@ -0,0 +1,213 @@
+/*
+ * 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 "GUIDialogFavourites.h"
+
+#include "ContextMenuManager.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "favourites/FavouritesURL.h"
+#include "favourites/FavouritesUtils.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/ActionIDs.h"
+
+#define FAVOURITES_LIST 450
+
+CGUIDialogFavourites::CGUIDialogFavourites() :
+ CGUIDialog(WINDOW_DIALOG_FAVOURITES, "DialogFavourites.xml"),
+ m_favouritesService(CServiceBroker::GetFavouritesService())
+{
+ m_favourites = new CFileItemList;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogFavourites::~CGUIDialogFavourites(void)
+{
+ delete m_favourites;
+}
+
+bool CGUIDialogFavourites::OnMessage(CGUIMessage &message)
+{
+ if (message.GetMessage() == GUI_MSG_CLICKED)
+ {
+ if (message.GetSenderId() == FAVOURITES_LIST)
+ {
+ int item = GetSelectedItem();
+ int action = message.GetParam1();
+ if (action == ACTION_SELECT_ITEM || action == ACTION_MOUSE_LEFT_CLICK)
+ OnClick(item);
+ else if (action == ACTION_MOVE_ITEM_UP)
+ OnMoveItem(item, -1);
+ else if (action == ACTION_MOVE_ITEM_DOWN)
+ OnMoveItem(item, 1);
+ else if (action == ACTION_CONTEXT_MENU || action == ACTION_MOUSE_RIGHT_CLICK)
+ OnPopupMenu(item);
+ else if (action == ACTION_DELETE_ITEM)
+ OnDelete(item);
+ else
+ return false;
+ return true;
+ }
+ }
+ else if (message.GetMessage() == GUI_MSG_WINDOW_DEINIT)
+ {
+ CGUIDialog::OnMessage(message);
+ // clear our favourites
+ CGUIMessage message(GUI_MSG_LABEL_RESET, GetID(), FAVOURITES_LIST);
+ OnMessage(message);
+ m_favourites->Clear();
+ return true;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogFavourites::OnInitWindow()
+{
+ m_favouritesService.GetAll(*m_favourites);
+ UpdateList();
+ CGUIWindow::OnInitWindow();
+}
+
+int CGUIDialogFavourites::GetSelectedItem()
+{
+ CGUIMessage message(GUI_MSG_ITEM_SELECTED, GetID(), FAVOURITES_LIST);
+ OnMessage(message);
+ return message.GetParam1();
+}
+
+void CGUIDialogFavourites::OnClick(int item)
+{
+ if (item < 0 || item >= m_favourites->Size())
+ return;
+
+ CGUIMessage message(GUI_MSG_EXECUTE, 0, GetID());
+ message.SetStringParam(CFavouritesURL(*(*m_favourites)[item], GetID()).GetExecString());
+
+ Close();
+
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+}
+
+void CGUIDialogFavourites::OnPopupMenu(int item)
+{
+ if (item < 0 || item >= m_favourites->Size())
+ return;
+
+ // highlight the item
+ (*m_favourites)[item]->Select(true);
+
+ CContextButtons choices;
+ if (m_favourites->Size() > 1)
+ {
+ choices.Add(1, 13332); // Move up
+ choices.Add(2, 13333); // Move down
+ }
+ choices.Add(3, 20019); // Choose thumbnail
+ choices.Add(4, 118); // Rename
+ choices.Add(5, 15015); // Remove
+
+ CFileItemPtr itemPtr = m_favourites->Get(item);
+
+ //temporary workaround until the context menu ids are removed
+ const int addonItemOffset = 10000;
+
+ auto addonItems = CServiceBroker::GetContextMenuManager().GetAddonItems(*itemPtr);
+
+ for (size_t i = 0; i < addonItems.size(); ++i)
+ choices.Add(addonItemOffset + i, addonItems[i]->GetLabel(*itemPtr));
+
+ int button = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+
+ // unhighlight the item
+ (*m_favourites)[item]->Select(false);
+
+ if (button == 1)
+ OnMoveItem(item, -1);
+ else if (button == 2)
+ OnMoveItem(item, +1);
+ else if (button == 3)
+ OnSetThumb(item);
+ else if (button == 4)
+ OnRename(item);
+ else if (button == 5)
+ OnDelete(item);
+ else if (button >= addonItemOffset)
+ CONTEXTMENU::LoopFrom(*addonItems.at(button - addonItemOffset), itemPtr);
+}
+
+void CGUIDialogFavourites::OnMoveItem(int item, int amount)
+{
+ if (item < 0 || item >= m_favourites->Size() || m_favourites->Size() <= 1 || 0 == amount) return;
+
+ int nextItem = (item + amount) % m_favourites->Size();
+ if (nextItem < 0) nextItem += m_favourites->Size();
+
+ m_favourites->Swap(item, nextItem);
+ m_favouritesService.Save(*m_favourites);
+
+ CGUIMessage message(GUI_MSG_ITEM_SELECT, GetID(), FAVOURITES_LIST, nextItem);
+ OnMessage(message);
+
+ UpdateList();
+}
+
+void CGUIDialogFavourites::OnDelete(int item)
+{
+ if (item < 0 || item >= m_favourites->Size())
+ return;
+ m_favourites->Remove(item);
+ m_favouritesService.Save(*m_favourites);
+
+ CGUIMessage message(GUI_MSG_ITEM_SELECT, GetID(), FAVOURITES_LIST, item < m_favourites->Size() ? item : item - 1);
+ OnMessage(message);
+
+ UpdateList();
+}
+
+void CGUIDialogFavourites::OnRename(int item)
+{
+ if (item < 0 || item >= m_favourites->Size())
+ return;
+
+ if (FAVOURITES_UTILS::ChooseAndSetNewName(*(*m_favourites)[item]))
+ {
+ m_favouritesService.Save(*m_favourites);
+ UpdateList();
+ }
+}
+
+void CGUIDialogFavourites::OnSetThumb(int item)
+{
+ if (item < 0 || item >= m_favourites->Size())
+ return;
+
+ if (FAVOURITES_UTILS::ChooseAndSetNewThumbnail(*(*m_favourites)[item]))
+ {
+ m_favouritesService.Save(*m_favourites);
+ UpdateList();
+ }
+}
+
+void CGUIDialogFavourites::UpdateList()
+{
+ int currentItem = GetSelectedItem();
+ CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), FAVOURITES_LIST, currentItem >= 0 ? currentItem : 0, 0, m_favourites);
+ OnMessage(message);
+}
+
+CFileItemPtr CGUIDialogFavourites::GetCurrentListItem(int offset)
+{
+ int currentItem = GetSelectedItem();
+ if (currentItem < 0 || !m_favourites->Size()) return CFileItemPtr();
+
+ int item = (currentItem + offset) % m_favourites->Size();
+ if (item < 0) item += m_favourites->Size();
+ return (*m_favourites)[item];
+}
diff --git a/xbmc/favourites/GUIDialogFavourites.h b/xbmc/favourites/GUIDialogFavourites.h
new file mode 100644
index 0000000..72d26bd
--- /dev/null
+++ b/xbmc/favourites/GUIDialogFavourites.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "favourites/FavouritesService.h"
+#include "guilib/GUIDialog.h"
+
+class CFileItem;
+class CFileItemList;
+
+class CGUIDialogFavourites :
+ public CGUIDialog
+{
+public:
+ CGUIDialogFavourites(void);
+ ~CGUIDialogFavourites(void) override;
+ bool OnMessage(CGUIMessage &message) override;
+ void OnInitWindow() override;
+
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+
+ bool HasListItems() const override { return true; }
+
+protected:
+ int GetSelectedItem();
+ void OnClick(int item);
+ void OnPopupMenu(int item);
+ void OnMoveItem(int item, int amount);
+ void OnDelete(int item);
+ void OnRename(int item);
+ void OnSetThumb(int item);
+ void UpdateList();
+
+private:
+ CFileItemList* m_favourites;
+ CFavouritesService& m_favouritesService;
+};
diff --git a/xbmc/favourites/GUIViewStateFavourites.cpp b/xbmc/favourites/GUIViewStateFavourites.cpp
new file mode 100644
index 0000000..7bb5ff1
--- /dev/null
+++ b/xbmc/favourites/GUIViewStateFavourites.cpp
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 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 "GUIViewStateFavourites.h"
+
+#include "FileItem.h"
+#include "guilib/WindowIDs.h"
+
+CGUIViewStateFavourites::CGUIViewStateFavourites(const CFileItemList& items) : CGUIViewState(items)
+{
+ AddSortMethod(SortByUserPreference, 19349,
+ LABEL_MASKS("%L", "", "%L", "")); // Label, empty | Label, empty
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS("%L", "", "%L", "")); // Label, empty | Label, empty
+
+ SetSortMethod(SortByUserPreference);
+
+ LoadViewState(items.GetPath(), WINDOW_FAVOURITES);
+}
+
+void CGUIViewStateFavourites::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_FAVOURITES);
+}
diff --git a/xbmc/favourites/GUIViewStateFavourites.h b/xbmc/favourites/GUIViewStateFavourites.h
new file mode 100644
index 0000000..aad4444
--- /dev/null
+++ b/xbmc/favourites/GUIViewStateFavourites.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 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 "view/GUIViewState.h"
+
+class CFileItemList;
+
+class CGUIViewStateFavourites : public CGUIViewState
+{
+public:
+ CGUIViewStateFavourites(const CFileItemList& items);
+ ~CGUIViewStateFavourites() override = default;
+
+protected:
+ void SaveViewState() override;
+ bool HideParentDirItems() override { return true; };
+};
diff --git a/xbmc/favourites/GUIWindowFavourites.cpp b/xbmc/favourites/GUIWindowFavourites.cpp
new file mode 100644
index 0000000..0d43b59
--- /dev/null
+++ b/xbmc/favourites/GUIWindowFavourites.cpp
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2022 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 "GUIWindowFavourites.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "favourites/FavouritesURL.h"
+#include "favourites/FavouritesUtils.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/PlayerUtils.h"
+#include "utils/StringUtils.h"
+
+CGUIWindowFavourites::CGUIWindowFavourites()
+ : CGUIMediaWindow(WINDOW_FAVOURITES, "MyFavourites.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+ CServiceBroker::GetFavouritesService().Events().Subscribe(
+ this, &CGUIWindowFavourites::OnFavouritesEvent);
+}
+
+CGUIWindowFavourites::~CGUIWindowFavourites()
+{
+ CServiceBroker::GetFavouritesService().Events().Unsubscribe(this);
+}
+
+void CGUIWindowFavourites::OnFavouritesEvent(const CFavouritesService::FavouritesUpdated& event)
+{
+ CGUIMessage m(GUI_MSG_REFRESH_LIST, GetID(), 0, 0);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+}
+
+namespace
+{
+bool ExecuteAction(const std::string& execute)
+{
+ if (!execute.empty())
+ {
+ CGUIMessage message(GUI_MSG_EXECUTE, 0, 0);
+ message.SetStringParam(execute);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+ return true;
+ }
+ return false;
+}
+} // namespace
+
+bool CGUIWindowFavourites::OnSelect(int item)
+{
+ if (item < 0 || item >= m_vecItems->Size())
+ return false;
+
+ return ExecuteAction(CFavouritesURL(*(*m_vecItems)[item], GetID()).GetExecString());
+}
+
+bool CGUIWindowFavourites::OnAction(const CAction& action)
+{
+ const int selectedItem = m_viewControl.GetSelectedItem();
+ if (selectedItem < 0 || selectedItem >= m_vecItems->Size())
+ return false;
+
+ if (action.GetID() == ACTION_PLAYER_PLAY)
+ {
+ const CFavouritesURL favURL((*m_vecItems)[selectedItem]->GetPath());
+ if (!favURL.IsValid())
+ return false;
+
+ // If action is playmedia, just play it
+ if (favURL.GetAction() == CFavouritesURL::Action::PLAY_MEDIA)
+ return ExecuteAction(favURL.GetExecString());
+
+ // Resolve and check the target
+ const auto item = std::make_shared<CFileItem>(favURL.GetTarget(), favURL.IsDir());
+ if (CPlayerUtils::IsItemPlayable(*item))
+ {
+ CFavouritesURL target(*item, {});
+ if (target.GetAction() == CFavouritesURL::Action::PLAY_MEDIA)
+ {
+ return ExecuteAction(target.GetExecString());
+ }
+ else
+ {
+ // build and execute a playmedia execute string
+ target = CFavouritesURL(CFavouritesURL::Action::PLAY_MEDIA,
+ {StringUtils::Paramify(item->GetPath())});
+ return ExecuteAction(target.GetExecString());
+ }
+ }
+ return false;
+ }
+ else if (action.GetID() == ACTION_MOVE_ITEM_UP)
+ {
+ if (FAVOURITES_UTILS::ShouldEnableMoveItems())
+ return MoveItem(selectedItem, -1);
+ }
+ else if (action.GetID() == ACTION_MOVE_ITEM_DOWN)
+ {
+ if (FAVOURITES_UTILS::ShouldEnableMoveItems())
+ return MoveItem(selectedItem, +1);
+ }
+ else if (action.GetID() == ACTION_DELETE_ITEM)
+ {
+ return RemoveItem(selectedItem);
+ }
+
+ return CGUIMediaWindow::OnAction(action);
+}
+
+bool CGUIWindowFavourites::OnMessage(CGUIMessage& message)
+{
+ bool ret = false;
+
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_REFRESH_LIST:
+ {
+ const int size = m_vecItems->Size();
+ int selected = m_viewControl.GetSelectedItem();
+ if (m_vecItems->Size() > 0 && selected == size - 1)
+ --selected; // remove of last item, select the new last item after refresh
+
+ Refresh(true);
+
+ if (m_vecItems->Size() < size)
+ {
+ // item removed. select item after the removed item
+ m_viewControl.SetSelectedItem(selected);
+ }
+
+ ret = true;
+ break;
+ }
+ }
+
+ return ret || CGUIMediaWindow::OnMessage(message);
+}
+
+bool CGUIWindowFavourites::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ std::string directory = strDirectory;
+ if (directory.empty())
+ directory = "favourites://";
+
+ return CGUIMediaWindow::Update(directory, updateFilterPath);
+}
+
+bool CGUIWindowFavourites::MoveItem(int item, int amount)
+{
+ if (item < 0 || item >= m_vecItems->Size() || m_vecItems->Size() < 2 || amount == 0)
+ return false;
+
+ if (FAVOURITES_UTILS::MoveItem(*m_vecItems, (*m_vecItems)[item], amount) &&
+ CServiceBroker::GetFavouritesService().Save(*m_vecItems))
+ {
+ int selected = item + amount;
+ if (selected >= m_vecItems->Size())
+ selected = 0;
+ else if (selected < 0)
+ selected = m_vecItems->Size() - 1;
+
+ m_viewControl.SetSelectedItem(selected);
+ return true;
+ }
+
+ return false;
+}
+
+bool CGUIWindowFavourites::RemoveItem(int item)
+{
+ if (item < 0 || item >= m_vecItems->Size())
+ return false;
+
+ if (FAVOURITES_UTILS::RemoveItem(*m_vecItems, (*m_vecItems)[item]) &&
+ CServiceBroker::GetFavouritesService().Save(*m_vecItems))
+ return true;
+
+ return false;
+}
diff --git a/xbmc/favourites/GUIWindowFavourites.h b/xbmc/favourites/GUIWindowFavourites.h
new file mode 100644
index 0000000..ec4ea32
--- /dev/null
+++ b/xbmc/favourites/GUIWindowFavourites.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 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 "favourites/FavouritesService.h"
+#include "windows/GUIMediaWindow.h"
+
+class CGUIWindowFavourites : public CGUIMediaWindow
+{
+public:
+ CGUIWindowFavourites();
+ ~CGUIWindowFavourites() override;
+
+protected:
+ std::string GetRootPath() const override { return "favourites://"; }
+
+ bool OnSelect(int item) override;
+ bool OnAction(const CAction& action) override;
+ bool OnMessage(CGUIMessage& message) override;
+
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+
+private:
+ void OnFavouritesEvent(const CFavouritesService::FavouritesUpdated& event);
+ bool MoveItem(int item, int amount);
+ bool RemoveItem(int item);
+};
diff --git a/xbmc/filesystem/AddonsDirectory.cpp b/xbmc/filesystem/AddonsDirectory.cpp
new file mode 100644
index 0000000..1439ae0
--- /dev/null
+++ b/xbmc/filesystem/AddonsDirectory.cpp
@@ -0,0 +1,953 @@
+/*
+ * 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 "AddonsDirectory.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/AddonDatabase.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonRepos.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/PluginSource.h"
+#include "addons/RepositoryUpdater.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "games/GameUtils.h"
+#include "games/addons/GameClient.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/TextureManager.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <algorithm>
+#include <array>
+#include <functional>
+#include <set>
+
+using namespace KODI;
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+
+namespace XFILE
+{
+
+CAddonsDirectory::CAddonsDirectory(void) = default;
+
+CAddonsDirectory::~CAddonsDirectory(void) = default;
+
+const auto CATEGORY_INFO_PROVIDERS = "category.infoproviders";
+const auto CATEGORY_LOOK_AND_FEEL = "category.lookandfeel";
+const auto CATEGORY_GAME_ADDONS = "category.gameaddons";
+const auto CATEGORY_EMULATORS = "category.emulators";
+const auto CATEGORY_STANDALONE_GAMES = "category.standalonegames";
+const auto CATEGORY_GAME_PROVIDERS = "category.gameproviders";
+const auto CATEGORY_GAME_RESOURCES = "category.gameresources";
+const auto CATEGORY_GAME_SUPPORT_ADDONS = "category.gamesupport";
+
+const std::set<AddonType> infoProviderTypes = {
+ AddonType::SCRAPER_ALBUMS, AddonType::SCRAPER_ARTISTS, AddonType::SCRAPER_MOVIES,
+ AddonType::SCRAPER_MUSICVIDEOS, AddonType::SCRAPER_TVSHOWS,
+};
+
+const std::set<AddonType> lookAndFeelTypes = {
+ AddonType::SKIN,
+ AddonType::SCREENSAVER,
+ AddonType::RESOURCE_IMAGES,
+ AddonType::RESOURCE_LANGUAGE,
+ AddonType::RESOURCE_UISOUNDS,
+ AddonType::RESOURCE_FONT,
+ AddonType::VISUALIZATION,
+};
+
+const std::set<AddonType> gameTypes = {
+ AddonType::GAME_CONTROLLER,
+ AddonType::GAMEDLL,
+ AddonType::GAME,
+ AddonType::RESOURCE_GAMES,
+};
+
+static bool IsInfoProviderType(AddonType type)
+{
+ return infoProviderTypes.find(type) != infoProviderTypes.end();
+}
+
+static bool IsInfoProviderTypeAddon(const AddonPtr& addon)
+{
+ return IsInfoProviderType(addon->Type());
+}
+
+static bool IsLookAndFeelType(AddonType type)
+{
+ return lookAndFeelTypes.find(type) != lookAndFeelTypes.end();
+}
+
+static bool IsLookAndFeelTypeAddon(const AddonPtr& addon)
+{
+ return IsLookAndFeelType(addon->Type());
+}
+
+static bool IsGameType(AddonType type)
+{
+ return gameTypes.find(type) != gameTypes.end();
+}
+
+static bool IsStandaloneGame(const AddonPtr& addon)
+{
+ return GAME::CGameUtils::IsStandaloneGame(addon);
+}
+
+static bool IsEmulator(const AddonPtr& addon)
+{
+ return addon->Type() == AddonType::GAMEDLL &&
+ std::static_pointer_cast<GAME::CGameClient>(addon)->SupportsPath();
+}
+
+static bool IsGameProvider(const AddonPtr& addon)
+{
+ return addon->Type() == AddonType::PLUGIN && addon->HasType(AddonType::GAME);
+}
+
+static bool IsGameResource(const AddonPtr& addon)
+{
+ return addon->Type() == AddonType::RESOURCE_GAMES;
+}
+
+static bool IsGameSupportAddon(const AddonPtr& addon)
+{
+ return addon->Type() == AddonType::GAMEDLL &&
+ !std::static_pointer_cast<GAME::CGameClient>(addon)->SupportsPath() &&
+ !std::static_pointer_cast<GAME::CGameClient>(addon)->SupportsStandalone();
+}
+
+static bool IsGameAddon(const AddonPtr& addon)
+{
+ return IsGameType(addon->Type()) ||
+ IsStandaloneGame(addon) ||
+ IsGameProvider(addon) ||
+ IsGameResource(addon) ||
+ IsGameSupportAddon(addon);
+}
+
+static bool IsUserInstalled(const AddonPtr& addon)
+{
+ return !CAddonType::IsDependencyType(addon->MainType());
+}
+
+// Creates categories from addon types, if we have any addons with that type.
+static void GenerateTypeListing(const CURL& path,
+ const std::set<AddonType>& types,
+ const VECADDONS& addons,
+ CFileItemList& items)
+{
+ for (const auto& type : types)
+ {
+ for (const auto& addon : addons)
+ {
+ if (addon->HasType(type))
+ {
+ CFileItemPtr item(new CFileItem(CAddonInfo::TranslateType(type, true)));
+ CURL itemPath = path;
+ itemPath.SetFileName(CAddonInfo::TranslateType(type, false));
+ item->SetPath(itemPath.Get());
+ item->m_bIsFolder = true;
+ std::string thumb = CAddonInfo::TranslateIconType(type);
+ if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb))
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ break;
+ }
+ }
+ }
+}
+
+// Creates categories for game add-ons, if we have any game add-ons
+static void GenerateGameListing(const CURL& path, const VECADDONS& addons, CFileItemList& items)
+{
+ // Game controllers
+ for (const auto& addon : addons)
+ {
+ if (addon->Type() == AddonType::GAME_CONTROLLER)
+ {
+ CFileItemPtr item(new CFileItem(CAddonInfo::TranslateType(AddonType::GAME_CONTROLLER, true)));
+ CURL itemPath = path;
+ itemPath.SetFileName(CAddonInfo::TranslateType(AddonType::GAME_CONTROLLER, false));
+ item->SetPath(itemPath.Get());
+ item->m_bIsFolder = true;
+ std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAME_CONTROLLER);
+ if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb))
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ break;
+ }
+ }
+ // Emulators
+ for (const auto& addon : addons)
+ {
+ if (IsEmulator(addon))
+ {
+ CFileItemPtr item(new CFileItem(g_localizeStrings.Get(35207))); // Emulators
+ CURL itemPath = path;
+ itemPath.SetFileName(CATEGORY_EMULATORS);
+ item->SetPath(itemPath.Get());
+ item->m_bIsFolder = true;
+ std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAMEDLL);
+ if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb))
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ break;
+ }
+ }
+ // Standalone games
+ for (const auto& addon : addons)
+ {
+ if (IsStandaloneGame(addon))
+ {
+ CFileItemPtr item(new CFileItem(g_localizeStrings.Get(35208))); // Standalone games
+ CURL itemPath = path;
+ itemPath.SetFileName(CATEGORY_STANDALONE_GAMES);
+ item->SetPath(itemPath.Get());
+ item->m_bIsFolder = true;
+ std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAMEDLL);
+ if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb))
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ break;
+ }
+ }
+ // Game providers
+ for (const auto& addon : addons)
+ {
+ if (IsGameProvider(addon))
+ {
+ CFileItemPtr item(new CFileItem(g_localizeStrings.Get(35220))); // Game providers
+ CURL itemPath = path;
+ itemPath.SetFileName(CATEGORY_GAME_PROVIDERS);
+ item->SetPath(itemPath.Get());
+ item->m_bIsFolder = true;
+ std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAMEDLL);
+ if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb))
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ break;
+ }
+ }
+ // Game resources
+ for (const auto& addon : addons)
+ {
+ if (IsGameResource(addon))
+ {
+ CFileItemPtr item(new CFileItem(g_localizeStrings.Get(35209))); // Game resources
+ CURL itemPath = path;
+ itemPath.SetFileName(CATEGORY_GAME_RESOURCES);
+ item->SetPath(itemPath.Get());
+ item->m_bIsFolder = true;
+ std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAMEDLL);
+ if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb))
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ break;
+ }
+ }
+ // Game support add-ons
+ for (const auto& addon : addons)
+ {
+ if (IsGameSupportAddon(addon))
+ {
+ CFileItemPtr item(new CFileItem(g_localizeStrings.Get(35216))); // Support add-ons
+ CURL itemPath = path;
+ itemPath.SetFileName(CATEGORY_GAME_SUPPORT_ADDONS);
+ item->SetPath(itemPath.Get());
+ item->m_bIsFolder = true;
+ std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAMEDLL);
+ if (!thumb.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb))
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ break;
+ }
+ }
+}
+
+//Creates the top-level category list
+static void GenerateMainCategoryListing(const CURL& path, const VECADDONS& addons,
+ CFileItemList& items)
+{
+ if (std::any_of(addons.begin(), addons.end(), IsInfoProviderTypeAddon))
+ {
+ CFileItemPtr item(new CFileItem(g_localizeStrings.Get(24993)));
+ item->SetPath(URIUtils::AddFileToFolder(path.Get(), CATEGORY_INFO_PROVIDERS));
+ item->m_bIsFolder = true;
+ const std::string thumb = "DefaultAddonInfoProvider.png";
+ if (CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb))
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ }
+ if (std::any_of(addons.begin(), addons.end(), IsLookAndFeelTypeAddon))
+ {
+ CFileItemPtr item(new CFileItem(g_localizeStrings.Get(24997)));
+ item->SetPath(URIUtils::AddFileToFolder(path.Get(), CATEGORY_LOOK_AND_FEEL));
+ item->m_bIsFolder = true;
+ const std::string thumb = "DefaultAddonLookAndFeel.png";
+ if (CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb))
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ }
+ if (std::any_of(addons.begin(), addons.end(), IsGameAddon))
+ {
+ CFileItemPtr item(new CFileItem(CAddonInfo::TranslateType(AddonType::GAME, true)));
+ item->SetPath(URIUtils::AddFileToFolder(path.Get(), CATEGORY_GAME_ADDONS));
+ item->m_bIsFolder = true;
+ const std::string thumb = CAddonInfo::TranslateIconType(AddonType::GAME);
+ if (CServiceBroker::GetGUI()->GetTextureManager().HasTexture(thumb))
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ }
+
+ std::set<AddonType> uncategorized;
+ for (unsigned int i = static_cast<unsigned int>(AddonType::UNKNOWN) + 1;
+ i < static_cast<unsigned int>(AddonType::MAX_TYPES) - 1; ++i)
+ {
+ const AddonType type = static_cast<AddonType>(i);
+ /*
+ * Check and prevent insert for this cases:
+ * - By a provider, look and feel, dependency and game becomes given to
+ * subdirectory to control the types
+ * - By ADDON_SCRIPT and ADDON_PLUGIN, them contains one of the possible
+ * subtypes (audio, video, app or/and game) and not needed to show
+ * together in a Script or Plugin list
+ */
+ if (!IsInfoProviderType(type) && !IsLookAndFeelType(type) &&
+ !CAddonType::IsDependencyType(type) && !IsGameType(type) && type != AddonType::SCRIPT &&
+ type != AddonType::PLUGIN)
+ uncategorized.insert(type);
+ }
+ GenerateTypeListing(path, uncategorized, addons, items);
+}
+
+//Creates sub-categories or addon list for a category
+static void GenerateCategoryListing(const CURL& path, VECADDONS& addons,
+ CFileItemList& items)
+{
+ const std::string& category = path.GetFileName();
+ if (category == CATEGORY_INFO_PROVIDERS)
+ {
+ items.SetProperty("addoncategory", g_localizeStrings.Get(24993));
+ items.SetLabel(g_localizeStrings.Get(24993));
+ GenerateTypeListing(path, infoProviderTypes, addons, items);
+ }
+ else if (category == CATEGORY_LOOK_AND_FEEL)
+ {
+ items.SetProperty("addoncategory", g_localizeStrings.Get(24997));
+ items.SetLabel(g_localizeStrings.Get(24997));
+ GenerateTypeListing(path, lookAndFeelTypes, addons, items);
+ }
+ else if (category == CATEGORY_GAME_ADDONS)
+ {
+ items.SetProperty("addoncategory", CAddonInfo::TranslateType(AddonType::GAME, true));
+ items.SetLabel(CAddonInfo::TranslateType(AddonType::GAME, true));
+ GenerateGameListing(path, addons, items);
+ }
+ else if (category == CATEGORY_EMULATORS)
+ {
+ items.SetProperty("addoncategory", g_localizeStrings.Get(35207)); // Emulators
+ addons.erase(std::remove_if(addons.begin(), addons.end(),
+ [](const AddonPtr& addon){ return !IsEmulator(addon); }), addons.end());
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(35207)); // Emulators
+ }
+ else if (category == CATEGORY_STANDALONE_GAMES)
+ {
+ items.SetProperty("addoncategory", g_localizeStrings.Get(35208)); // Standalone games
+ addons.erase(std::remove_if(addons.begin(), addons.end(),
+ [](const AddonPtr& addon){ return !IsStandaloneGame(addon); }), addons.end());
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(35208)); // Standalone games
+ }
+ else if (category == CATEGORY_GAME_PROVIDERS)
+ {
+ items.SetProperty("addoncategory", g_localizeStrings.Get(35220)); // Game providers
+ addons.erase(std::remove_if(addons.begin(), addons.end(),
+ [](const AddonPtr& addon){ return !IsGameProvider(addon); }), addons.end());
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(35220)); // Game providers
+ }
+ else if (category == CATEGORY_GAME_RESOURCES)
+ {
+ items.SetProperty("addoncategory", g_localizeStrings.Get(35209)); // Game resources
+ addons.erase(std::remove_if(addons.begin(), addons.end(),
+ [](const AddonPtr& addon){ return !IsGameResource(addon); }), addons.end());
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(35209)); // Game resources
+ }
+ else if (category == CATEGORY_GAME_SUPPORT_ADDONS)
+ {
+ items.SetProperty("addoncategory", g_localizeStrings.Get(35216)); // Support add-ons
+ addons.erase(std::remove_if(addons.begin(), addons.end(),
+ [](const AddonPtr& addon) { return !IsGameSupportAddon(addon); }), addons.end());
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(35216)); // Support add-ons
+ }
+ else
+ { // fallback to addon type
+ AddonType type = CAddonInfo::TranslateType(category);
+ items.SetProperty("addoncategory", CAddonInfo::TranslateType(type, true));
+ addons.erase(std::remove_if(addons.begin(), addons.end(),
+ [type](const AddonPtr& addon) { return !addon->HasType(type); }),
+ addons.end());
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, CAddonInfo::TranslateType(type, true));
+ }
+}
+
+bool CAddonsDirectory::GetSearchResults(const CURL& path, CFileItemList &items)
+{
+ std::string search(path.GetFileName());
+ if (search.empty() && !GetKeyboardInput(16017, search))
+ return false;
+
+ CAddonDatabase database;
+ database.Open();
+
+ VECADDONS addons;
+ database.Search(search, addons);
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(283));
+ CURL searchPath(path);
+ searchPath.SetFileName(search);
+ items.SetPath(searchPath.Get());
+ return true;
+}
+
+static void UserInstalledAddons(const CURL& path, CFileItemList &items)
+{
+ items.ClearItems();
+ items.SetLabel(g_localizeStrings.Get(24998));
+
+ VECADDONS addons;
+ CServiceBroker::GetAddonMgr().GetInstalledAddons(addons);
+ addons.erase(std::remove_if(addons.begin(), addons.end(),
+ [](const AddonPtr& addon) { return !IsUserInstalled(addon); }), addons.end());
+
+ if (addons.empty())
+ return;
+
+ const std::string& category = path.GetFileName();
+ if (category.empty())
+ {
+ GenerateMainCategoryListing(path, addons, items);
+
+ //"All" node
+ CFileItemPtr item(new CFileItem());
+ item->m_bIsFolder = true;
+ CURL itemPath = path;
+ itemPath.SetFileName("all");
+ item->SetPath(itemPath.Get());
+ item->SetLabel(g_localizeStrings.Get(593));
+ item->SetSpecialSort(SortSpecialOnTop);
+ items.Add(item);
+ }
+ else if (category == "all")
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(24998));
+ else
+ GenerateCategoryListing(path, addons, items);
+}
+
+static void DependencyAddons(const CURL& path, CFileItemList &items)
+{
+ VECADDONS all;
+ CServiceBroker::GetAddonMgr().GetInstalledAddons(all);
+
+ VECADDONS deps;
+ std::copy_if(all.begin(), all.end(), std::back_inserter(deps),
+ [&](const AddonPtr& _){ return !IsUserInstalled(_); });
+
+ CAddonsDirectory::GenerateAddonListing(path, deps, items, g_localizeStrings.Get(24996));
+
+ //Set orphaned status
+ std::set<std::string> orphaned;
+ for (const auto& addon : deps)
+ {
+ if (CServiceBroker::GetAddonMgr().IsOrphaned(addon, all))
+ orphaned.insert(addon->ID());
+ }
+
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ if (orphaned.find(items[i]->GetProperty("Addon.ID").asString()) != orphaned.end())
+ {
+ items[i]->SetProperty("Addon.Status", g_localizeStrings.Get(24995));
+ items[i]->SetProperty("Addon.Orphaned", true);
+ }
+ }
+}
+
+static void OutdatedAddons(const CURL& path, CFileItemList &items)
+{
+ VECADDONS addons = CServiceBroker::GetAddonMgr().GetAvailableUpdates();
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(24043));
+
+ if (!items.IsEmpty())
+ {
+ if (CAddonSystemSettings::GetInstance().GetAddonAutoUpdateMode() == AUTO_UPDATES_ON)
+ {
+ const CFileItemPtr itemUpdateAllowed(
+ std::make_shared<CFileItem>("addons://update_allowed/", false));
+ itemUpdateAllowed->SetLabel(g_localizeStrings.Get(24137));
+ itemUpdateAllowed->SetSpecialSort(SortSpecialOnTop);
+ items.Add(itemUpdateAllowed);
+ }
+
+ const CFileItemPtr itemUpdateAll(std::make_shared<CFileItem>("addons://update_all/", false));
+ itemUpdateAll->SetLabel(g_localizeStrings.Get(24122));
+ itemUpdateAll->SetSpecialSort(SortSpecialOnTop);
+ items.Add(itemUpdateAll);
+ }
+}
+
+static void RunningAddons(const CURL& path, CFileItemList &items)
+{
+ VECADDONS addons;
+ CServiceBroker::GetAddonMgr().GetAddons(addons, AddonType::SERVICE);
+
+ addons.erase(std::remove_if(addons.begin(), addons.end(),
+ [](const AddonPtr& addon){ return !CScriptInvocationManager::GetInstance().IsRunning(addon->LibPath()); }), addons.end());
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(24994));
+}
+
+static bool Browse(const CURL& path, CFileItemList &items)
+{
+ const std::string& repoId = path.GetHostName();
+
+ VECADDONS addons;
+ items.SetPath(path.Get());
+ if (repoId == "all")
+ {
+ CAddonRepos addonRepos;
+ if (!addonRepos.IsValid())
+ return false;
+
+ // get all latest addon versions by repo
+ addonRepos.GetLatestAddonVersionsFromAllRepos(addons);
+
+ items.SetProperty("reponame", g_localizeStrings.Get(24087));
+ items.SetLabel(g_localizeStrings.Get(24087));
+ }
+ else
+ {
+ AddonPtr repoAddon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(repoId, repoAddon, AddonType::REPOSITORY,
+ OnlyEnabled::CHOICE_YES))
+ {
+ return false;
+ }
+
+ CAddonRepos addonRepos(repoAddon);
+ if (!addonRepos.IsValid())
+ return false;
+
+ // get all addons from the single repository
+ addonRepos.GetLatestAddonVersions(addons);
+
+ items.SetProperty("reponame", repoAddon->Name());
+ items.SetLabel(repoAddon->Name());
+ }
+
+ const std::string& category = path.GetFileName();
+ if (category.empty())
+ GenerateMainCategoryListing(path, addons, items);
+ else
+ GenerateCategoryListing(path, addons, items);
+ return true;
+}
+
+static bool GetRecentlyUpdatedAddons(VECADDONS& addons)
+{
+ if (!CServiceBroker::GetAddonMgr().GetInstalledAddons(addons))
+ return false;
+
+ auto limit = CDateTime::GetCurrentDateTime() - CDateTimeSpan(14, 0, 0, 0);
+ auto isOld = [limit](const AddonPtr& addon){ return addon->LastUpdated() < limit; };
+ addons.erase(std::remove_if(addons.begin(), addons.end(), isOld), addons.end());
+ return true;
+}
+
+static bool HasRecentlyUpdatedAddons()
+{
+ VECADDONS addons;
+ return GetRecentlyUpdatedAddons(addons) && !addons.empty();
+}
+
+static bool Repos(const CURL& path, CFileItemList &items)
+{
+ items.SetLabel(g_localizeStrings.Get(24033));
+
+ VECADDONS addons;
+ CServiceBroker::GetAddonMgr().GetAddons(addons, AddonType::REPOSITORY);
+ if (addons.empty())
+ return true;
+ else if (addons.size() == 1)
+ return Browse(CURL("addons://" + addons[0]->ID()), items);
+ CFileItemPtr item(new CFileItem("addons://all/", true));
+ item->SetLabel(g_localizeStrings.Get(24087));
+ item->SetSpecialSort(SortSpecialOnTop);
+ items.Add(item);
+ for (const auto& repo : addons)
+ {
+ CFileItemPtr item = CAddonsDirectory::FileItemFromAddon(repo, "addons://" + repo->ID(), true);
+ items.Add(item);
+ }
+ items.SetContent("addons");
+ return true;
+}
+
+static void RootDirectory(CFileItemList& items)
+{
+ items.SetLabel(g_localizeStrings.Get(10040));
+ {
+ CFileItemPtr item(new CFileItem("addons://user/", true));
+ item->SetLabel(g_localizeStrings.Get(24998));
+ item->SetArt("icon", "DefaultAddonsInstalled.png");
+ items.Add(item);
+ }
+ if (CServiceBroker::GetAddonMgr().HasAvailableUpdates())
+ {
+ CFileItemPtr item(new CFileItem("addons://outdated/", true));
+ item->SetLabel(g_localizeStrings.Get(24043));
+ item->SetArt("icon", "DefaultAddonsUpdates.png");
+ items.Add(item);
+ }
+ if (CAddonInstaller::GetInstance().IsDownloading())
+ {
+ CFileItemPtr item(new CFileItem("addons://downloading/", true));
+ item->SetLabel(g_localizeStrings.Get(24067));
+ item->SetArt("icon", "DefaultNetwork.png");
+ items.Add(item);
+ }
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_ADDONS_AUTOUPDATES) == ADDON::AUTO_UPDATES_ON
+ && HasRecentlyUpdatedAddons())
+ {
+ CFileItemPtr item(new CFileItem("addons://recently_updated/", true));
+ item->SetLabel(g_localizeStrings.Get(24004));
+ item->SetArt("icon", "DefaultAddonsRecentlyUpdated.png");
+ items.Add(item);
+ }
+ if (CServiceBroker::GetAddonMgr().HasAddons(AddonType::REPOSITORY))
+ {
+ CFileItemPtr item(new CFileItem("addons://repos/", true));
+ item->SetLabel(g_localizeStrings.Get(24033));
+ item->SetArt("icon", "DefaultAddonsRepo.png");
+ items.Add(item);
+ }
+ {
+ CFileItemPtr item(new CFileItem("addons://install/", false));
+ item->SetLabel(g_localizeStrings.Get(24041));
+ item->SetArt("icon", "DefaultAddonsZip.png");
+ items.Add(item);
+ }
+ {
+ CFileItemPtr item(new CFileItem("addons://search/", true));
+ item->SetLabel(g_localizeStrings.Get(137));
+ item->SetArt("icon", "DefaultAddonsSearch.png");
+ items.Add(item);
+ }
+}
+
+bool CAddonsDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ std::string tmp(url.Get());
+ URIUtils::RemoveSlashAtEnd(tmp);
+ CURL path(tmp);
+ const std::string& endpoint = path.GetHostName();
+ items.ClearItems();
+ items.ClearProperties();
+ items.SetCacheToDisc(CFileItemList::CACHE_NEVER);
+ items.SetPath(path.Get());
+
+ if (endpoint.empty())
+ {
+ RootDirectory(items);
+ return true;
+ }
+ else if (endpoint == "user")
+ {
+ UserInstalledAddons(path, items);
+ return true;
+ }
+ else if (endpoint == "dependencies")
+ {
+ DependencyAddons(path, items);
+ return true;
+ }
+ // PVR hardcodes this view so keep for compatibility
+ else if (endpoint == "disabled")
+ {
+ VECADDONS addons;
+ AddonType type;
+
+ if (path.GetFileName() == "kodi.pvrclient")
+ type = AddonType::PVRDLL;
+ else if (path.GetFileName() == "kodi.vfs")
+ type = AddonType::VFS;
+ else
+ type = AddonType::UNKNOWN;
+
+ if (type != AddonType::UNKNOWN &&
+ CServiceBroker::GetAddonMgr().GetInstalledAddons(addons, type))
+ {
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, CAddonInfo::TranslateType(type, true));
+ return true;
+ }
+ return false;
+ }
+ else if (endpoint == "outdated")
+ {
+ OutdatedAddons(path, items);
+ return true;
+ }
+ else if (endpoint == "running")
+ {
+ RunningAddons(path, items);
+ return true;
+ }
+ else if (endpoint == "repos")
+ {
+ return Repos(path, items);
+ }
+ else if (endpoint == "sources")
+ {
+ return GetScriptsAndPlugins(path.GetFileName(), items);
+ }
+ else if (endpoint == "search")
+ {
+ return GetSearchResults(path, items);
+ }
+ else if (endpoint == "recently_updated")
+ {
+ VECADDONS addons;
+ if (!GetRecentlyUpdatedAddons(addons))
+ return false;
+
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(24004));
+ return true;
+ }
+ else if (endpoint == "downloading")
+ {
+ VECADDONS addons;
+ CAddonInstaller::GetInstance().GetInstallList(addons);
+ CAddonsDirectory::GenerateAddonListing(path, addons, items, g_localizeStrings.Get(24067));
+ return true;
+ }
+ else if (endpoint == "more")
+ {
+ const std::string& type = path.GetFileName();
+ if (type == "video" || type == "audio" || type == "image" || type == "executable")
+ return Browse(CURL("addons://all/xbmc.addon." + type), items);
+ else if (type == "game")
+ return Browse(CURL("addons://all/category.gameaddons"), items);
+ return false;
+ }
+ else
+ {
+ return Browse(path, items);
+ }
+}
+
+bool CAddonsDirectory::IsRepoDirectory(const CURL& url)
+{
+ if (url.GetHostName().empty() || !url.IsProtocol("addons"))
+ return false;
+
+ AddonPtr tmp;
+ return url.GetHostName() == "repos" || url.GetHostName() == "all" ||
+ url.GetHostName() == "search" ||
+ CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), tmp, AddonType::REPOSITORY,
+ OnlyEnabled::CHOICE_YES);
+}
+
+void CAddonsDirectory::GenerateAddonListing(const CURL& path,
+ const VECADDONS& addons,
+ CFileItemList& items,
+ const std::string& label)
+{
+ std::map<std::string, AddonWithUpdate> addonsWithUpdate =
+ CServiceBroker::GetAddonMgr().GetAddonsWithAvailableUpdate();
+
+ items.ClearItems();
+ items.SetContent("addons");
+ items.SetLabel(label);
+ for (const auto& addon : addons)
+ {
+ CURL itemPath = path;
+ itemPath.SetFileName(addon->ID());
+ CFileItemPtr pItem = FileItemFromAddon(addon, itemPath.Get(), false);
+
+ bool installed = CServiceBroker::GetAddonMgr().IsAddonInstalled(addon->ID(), addon->Origin(),
+ addon->Version());
+ bool disabled = CServiceBroker::GetAddonMgr().IsAddonDisabled(addon->ID());
+
+ std::function<bool(bool)> CheckOutdatedOrUpdate = [&](bool checkOutdated) -> bool {
+ auto mapEntry = addonsWithUpdate.find(addon->ID());
+ if (mapEntry != addonsWithUpdate.end())
+ {
+ const std::shared_ptr<IAddon>& checkedObject =
+ checkOutdated ? mapEntry->second.m_installed : mapEntry->second.m_update;
+
+ return (checkedObject->Origin() == addon->Origin() &&
+ checkedObject->Version() == addon->Version());
+ }
+ return false;
+ };
+
+ bool isUpdate = CheckOutdatedOrUpdate(false); // check if it's an available update
+ bool hasUpdate = CheckOutdatedOrUpdate(true); // check if it's an outdated addon
+
+ std::string validUpdateVersion;
+ std::string validUpdateOrigin;
+ if (hasUpdate)
+ {
+ auto mapEntry = addonsWithUpdate.find(addon->ID());
+ validUpdateVersion = mapEntry->second.m_update->Version().asString();
+ validUpdateOrigin = mapEntry->second.m_update->Origin();
+ }
+
+ bool fromOfficialRepo = CAddonRepos::IsFromOfficialRepo(addon, CheckAddonPath::CHOICE_NO);
+
+ pItem->SetProperty("Addon.IsInstalled", installed);
+ pItem->SetProperty("Addon.IsEnabled", installed && !disabled);
+ pItem->SetProperty("Addon.HasUpdate", hasUpdate);
+ pItem->SetProperty("Addon.IsUpdate", isUpdate);
+ pItem->SetProperty("Addon.ValidUpdateVersion", validUpdateVersion);
+ pItem->SetProperty("Addon.ValidUpdateOrigin", validUpdateOrigin);
+ pItem->SetProperty("Addon.IsFromOfficialRepo", fromOfficialRepo);
+ pItem->SetProperty("Addon.IsBinary", addon->IsBinary());
+
+ if (installed)
+ pItem->SetProperty("Addon.Status", g_localizeStrings.Get(305));
+ if (disabled)
+ pItem->SetProperty("Addon.Status", g_localizeStrings.Get(24023));
+ if (hasUpdate)
+ pItem->SetProperty("Addon.Status", g_localizeStrings.Get(24068));
+ else if (addon->LifecycleState() == AddonLifecycleState::BROKEN)
+ pItem->SetProperty("Addon.Status", g_localizeStrings.Get(24098));
+ else if (addon->LifecycleState() == AddonLifecycleState::DEPRECATED)
+ pItem->SetProperty("Addon.Status", g_localizeStrings.Get(24170));
+
+ items.Add(pItem);
+ }
+}
+
+CFileItemPtr CAddonsDirectory::FileItemFromAddon(const AddonPtr &addon,
+ const std::string& path, bool folder)
+{
+ if (!addon)
+ return CFileItemPtr();
+
+ CFileItemPtr item(new CFileItem(addon));
+ item->m_bIsFolder = folder;
+ item->SetPath(path);
+
+ std::string strLabel(addon->Name());
+ if (CURL(path).GetHostName() == "search")
+ strLabel = StringUtils::Format("{} - {}", CAddonInfo::TranslateType(addon->Type(), true),
+ addon->Name());
+ item->SetLabel(strLabel);
+ item->SetArt(addon->Art());
+ item->SetArt("thumb", addon->Icon());
+ item->SetArt("icon", "DefaultAddon.png");
+
+ //! @todo fix hacks that depends on these
+ item->SetProperty("Addon.ID", addon->ID());
+ item->SetProperty("Addon.Name", addon->Name());
+ item->SetCanQueue(false);
+ const auto it = addon->ExtraInfo().find("language");
+ if (it != addon->ExtraInfo().end())
+ item->SetProperty("Addon.Language", it->second);
+
+ return item;
+}
+
+bool CAddonsDirectory::GetScriptsAndPlugins(const std::string &content, VECADDONS &addons)
+{
+ CPluginSource::Content type = CPluginSource::Translate(content);
+ if (type == CPluginSource::UNKNOWN)
+ return false;
+
+ VECADDONS tempAddons;
+ CServiceBroker::GetAddonMgr().GetAddons(tempAddons, AddonType::PLUGIN);
+ for (unsigned i=0; i<tempAddons.size(); i++)
+ {
+ const auto plugin = std::dynamic_pointer_cast<CPluginSource>(tempAddons[i]);
+ if (plugin && plugin->Provides(type))
+ addons.push_back(tempAddons[i]);
+ }
+ tempAddons.clear();
+ CServiceBroker::GetAddonMgr().GetAddons(tempAddons, AddonType::SCRIPT);
+ for (unsigned i=0; i<tempAddons.size(); i++)
+ {
+ const auto plugin = std::dynamic_pointer_cast<CPluginSource>(tempAddons[i]);
+ if (plugin && plugin->Provides(type))
+ addons.push_back(tempAddons[i]);
+ }
+ tempAddons.clear();
+
+ if (type == CPluginSource::GAME)
+ {
+ CServiceBroker::GetAddonMgr().GetAddons(tempAddons, AddonType::GAMEDLL);
+ for (auto& addon : tempAddons)
+ {
+ if (IsStandaloneGame(addon))
+ addons.push_back(addon);
+ }
+ }
+
+ return true;
+}
+
+bool CAddonsDirectory::GetScriptsAndPlugins(const std::string &content, CFileItemList &items)
+{
+ VECADDONS addons;
+ if (!GetScriptsAndPlugins(content, addons))
+ return false;
+
+ for (AddonPtr& addon : addons)
+ {
+ const bool bIsFolder = (addon->Type() == AddonType::PLUGIN);
+
+ std::string path;
+ if (addon->HasType(AddonType::PLUGIN))
+ {
+ path = "plugin://" + addon->ID();
+ const auto plugin = std::dynamic_pointer_cast<CPluginSource>(addon);
+ if (plugin && plugin->ProvidesSeveral())
+ {
+ CURL url(path);
+ std::string opt = StringUtils::Format("?content_type={}", content);
+ url.SetOptions(opt);
+ path = url.Get();
+ }
+ }
+ else if (addon->HasType(AddonType::SCRIPT))
+ {
+ path = "script://" + addon->ID();
+ }
+ else if (addon->HasType(AddonType::GAMEDLL))
+ {
+ // Kodi fails to launch games with empty path from home screen
+ path = "game://" + addon->ID();
+ }
+
+ items.Add(FileItemFromAddon(addon, path, bIsFolder));
+ }
+
+ items.SetContent("addons");
+ items.SetLabel(g_localizeStrings.Get(24001)); // Add-ons
+
+ return true;
+}
+
+}
+
diff --git a/xbmc/filesystem/AddonsDirectory.h b/xbmc/filesystem/AddonsDirectory.h
new file mode 100644
index 0000000..0ff0c11
--- /dev/null
+++ b/xbmc/filesystem/AddonsDirectory.h
@@ -0,0 +1,72 @@
+/*
+ * 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 "IDirectory.h"
+
+#include <memory>
+#include <vector>
+
+class CFileItem;
+class CFileItemList;
+class CURL;
+typedef std::shared_ptr<CFileItem> CFileItemPtr;
+
+namespace ADDON
+{
+class IAddon;
+using VECADDONS = std::vector<std::shared_ptr<IAddon>>;
+} // namespace ADDON
+
+namespace XFILE
+{
+
+ /*!
+ \ingroup windows
+ \brief Get access to shares and it's directories.
+ */
+ class CAddonsDirectory : public IDirectory
+ {
+ public:
+ CAddonsDirectory(void);
+ ~CAddonsDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool Create(const CURL& url) override { return true; }
+ bool Exists(const CURL& url) override { return true; }
+ bool AllowAll() const override { return true; }
+
+ /*! \brief Fetch script and plugin addons of a given content type
+ \param content the content type to fetch
+ \param addons the list of addons to fill with scripts and plugin content
+ \return true if content is valid, false if it's invalid.
+ */
+ static bool GetScriptsAndPlugins(const std::string &content, ADDON::VECADDONS &addons);
+
+ /*! \brief Fetch scripts and plugins of a given content type
+ \param content the content type to fetch
+ \param items the list to fill with scripts and content
+ \return true if more than one item is found, false otherwise.
+ */
+ static bool GetScriptsAndPlugins(const std::string &content, CFileItemList &items);
+
+ static void GenerateAddonListing(const CURL& path,
+ const ADDON::VECADDONS& addons,
+ CFileItemList& items,
+ const std::string& label);
+ static CFileItemPtr FileItemFromAddon(const std::shared_ptr<ADDON::IAddon>& addon,
+ const std::string& path,
+ bool folder = false);
+
+ /*! \brief Returns true if `path` is a path or subpath of the repository directory, otherwise false */
+ static bool IsRepoDirectory(const CURL& path);
+
+ private:
+ bool GetSearchResults(const CURL& path, CFileItemList &items);
+ };
+}
diff --git a/xbmc/filesystem/AudioBookFileDirectory.cpp b/xbmc/filesystem/AudioBookFileDirectory.cpp
new file mode 100644
index 0000000..04d1c14
--- /dev/null
+++ b/xbmc/filesystem/AudioBookFileDirectory.cpp
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2014 Arne Morten Kvarving
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "AudioBookFileDirectory.h"
+
+#include "FileItem.h"
+#include "TextureDatabase.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/File.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/tags/MusicInfoTag.h"
+#include "utils/StringUtils.h"
+
+using namespace XFILE;
+
+static int cfile_file_read(void *h, uint8_t* buf, int size)
+{
+ CFile* pFile = static_cast<CFile*>(h);
+ return pFile->Read(buf, size);
+}
+
+static int64_t cfile_file_seek(void *h, int64_t pos, int whence)
+{
+ CFile* pFile = static_cast<CFile*>(h);
+ if(whence == AVSEEK_SIZE)
+ return pFile->GetLength();
+ else
+ return pFile->Seek(pos, whence & ~AVSEEK_FORCE);
+}
+
+CAudioBookFileDirectory::~CAudioBookFileDirectory(void)
+{
+ if (m_fctx)
+ avformat_close_input(&m_fctx);
+ if (m_ioctx)
+ {
+ av_free(m_ioctx->buffer);
+ av_free(m_ioctx);
+ }
+}
+
+bool CAudioBookFileDirectory::GetDirectory(const CURL& url,
+ CFileItemList &items)
+{
+ if (!m_fctx && !ContainsFiles(url))
+ return true;
+
+ std::string title;
+ std::string author;
+ std::string album;
+
+ AVDictionaryEntry* tag=nullptr;
+ while ((tag = av_dict_get(m_fctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX)))
+ {
+ if (StringUtils::CompareNoCase(tag->key, "title") == 0)
+ title = tag->value;
+ else if (StringUtils::CompareNoCase(tag->key, "album") == 0)
+ album = tag->value;
+ else if (StringUtils::CompareNoCase(tag->key, "artist") == 0)
+ author = tag->value;
+ }
+
+ std::string thumb;
+ if (m_fctx->nb_chapters > 1)
+ thumb = CTextureUtils::GetWrappedImageURL(url.Get(), "music");
+
+ for (size_t i=0;i<m_fctx->nb_chapters;++i)
+ {
+ tag=nullptr;
+ std::string chaptitle = StringUtils::Format(g_localizeStrings.Get(25010), i + 1);
+ std::string chapauthor;
+ std::string chapalbum;
+ while ((tag=av_dict_get(m_fctx->chapters[i]->metadata, "", tag, AV_DICT_IGNORE_SUFFIX)))
+ {
+ if (StringUtils::CompareNoCase(tag->key, "title") == 0)
+ chaptitle = tag->value;
+ else if (StringUtils::CompareNoCase(tag->key, "artist") == 0)
+ chapauthor = tag->value;
+ else if (StringUtils::CompareNoCase(tag->key, "album") == 0)
+ chapalbum = tag->value;
+ }
+ CFileItemPtr item(new CFileItem(url.Get(),false));
+ item->GetMusicInfoTag()->SetTrackNumber(i+1);
+ item->GetMusicInfoTag()->SetLoaded(true);
+ item->GetMusicInfoTag()->SetTitle(chaptitle);
+ if (album.empty())
+ item->GetMusicInfoTag()->SetAlbum(title);
+ else if (chapalbum.empty())
+ item->GetMusicInfoTag()->SetAlbum(album);
+ else
+ item->GetMusicInfoTag()->SetAlbum(chapalbum);
+ if (chapauthor.empty())
+ item->GetMusicInfoTag()->SetArtist(author);
+ else
+ item->GetMusicInfoTag()->SetArtist(chapauthor);
+
+ item->SetLabel(StringUtils::Format("{0:02}. {1} - {2}", i + 1,
+ item->GetMusicInfoTag()->GetAlbum(),
+ item->GetMusicInfoTag()->GetTitle()));
+ item->SetStartOffset(CUtil::ConvertSecsToMilliSecs(m_fctx->chapters[i]->start *
+ av_q2d(m_fctx->chapters[i]->time_base)));
+ item->SetEndOffset(m_fctx->chapters[i]->end * av_q2d(m_fctx->chapters[i]->time_base));
+ int compare = m_fctx->duration / (AV_TIME_BASE);
+ if (item->GetEndOffset() < 0 || item->GetEndOffset() > compare)
+ {
+ if (i < m_fctx->nb_chapters-1)
+ item->SetEndOffset(m_fctx->chapters[i + 1]->start *
+ av_q2d(m_fctx->chapters[i + 1]->time_base));
+ else
+ item->SetEndOffset(compare);
+ }
+ item->SetEndOffset(CUtil::ConvertSecsToMilliSecs(item->GetEndOffset()));
+ item->GetMusicInfoTag()->SetDuration(
+ CUtil::ConvertMilliSecsToSecsInt(item->GetEndOffset() - item->GetStartOffset()));
+ item->SetProperty("item_start", item->GetStartOffset());
+ if (!thumb.empty())
+ item->SetArt("thumb", thumb);
+ items.Add(item);
+ }
+
+ return true;
+}
+
+bool CAudioBookFileDirectory::Exists(const CURL& url)
+{
+ return CFile::Exists(url) && ContainsFiles(url);
+}
+
+bool CAudioBookFileDirectory::ContainsFiles(const CURL& url)
+{
+ CFile file;
+ if (!file.Open(url))
+ return false;
+
+ uint8_t* buffer = (uint8_t*)av_malloc(32768);
+ m_ioctx = avio_alloc_context(buffer, 32768, 0, &file, cfile_file_read,
+ nullptr, cfile_file_seek);
+
+ m_fctx = avformat_alloc_context();
+ m_fctx->pb = m_ioctx;
+
+ if (file.IoControl(IOCTRL_SEEK_POSSIBLE, nullptr) == 0)
+ m_ioctx->seekable = 0;
+
+ m_ioctx->max_packet_size = 32768;
+
+ AVInputFormat* iformat=nullptr;
+ av_probe_input_buffer(m_ioctx, &iformat, url.Get().c_str(), nullptr, 0, 0);
+
+ bool contains = false;
+ if (avformat_open_input(&m_fctx, url.Get().c_str(), iformat, nullptr) < 0)
+ {
+ if (m_fctx)
+ avformat_close_input(&m_fctx);
+ av_free(m_ioctx->buffer);
+ av_free(m_ioctx);
+ return false;
+ }
+
+ contains = m_fctx->nb_chapters > 1;
+
+ return contains;
+}
diff --git a/xbmc/filesystem/AudioBookFileDirectory.h b/xbmc/filesystem/AudioBookFileDirectory.h
new file mode 100644
index 0000000..ccc01cd
--- /dev/null
+++ b/xbmc/filesystem/AudioBookFileDirectory.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2014 Arne Morten Kvarving
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "IFileDirectory.h"
+extern "C" {
+#include <libavformat/avformat.h>
+}
+
+namespace XFILE
+{
+ class CAudioBookFileDirectory : public IFileDirectory
+ {
+ public:
+ ~CAudioBookFileDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool Exists(const CURL& url) override;
+ bool ContainsFiles(const CURL& url) override;
+ bool IsAllowed(const CURL& url) const override { return true; }
+
+ protected:
+ AVIOContext* m_ioctx = nullptr;
+ AVFormatContext* m_fctx = nullptr;
+ };
+}
diff --git a/xbmc/filesystem/BlurayCallback.cpp b/xbmc/filesystem/BlurayCallback.cpp
new file mode 100644
index 0000000..fbf9e7c
--- /dev/null
+++ b/xbmc/filesystem/BlurayCallback.cpp
@@ -0,0 +1,159 @@
+/*
+ * 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 "BlurayCallback.h"
+
+#include "FileItem.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+struct SDirState
+{
+ CFileItemList list;
+ int curr = 0;
+};
+
+void CBlurayCallback::bluray_logger(const char* msg)
+{
+ CLog::Log(LOGDEBUG, "CBlurayCallback::Logger - {}", msg);
+}
+
+void CBlurayCallback::dir_close(BD_DIR_H *dir)
+{
+ if (dir)
+ {
+ CLog::Log(LOGDEBUG, "CBlurayCallback - Closed dir ({})", fmt::ptr(dir));
+ delete static_cast<SDirState*>(dir->internal);
+ delete dir;
+ }
+}
+
+BD_DIR_H* CBlurayCallback::dir_open(void *handle, const char* rel_path)
+{
+ std::string strRelPath(rel_path);
+ std::string* strBasePath = reinterpret_cast<std::string*>(handle);
+ if (!strBasePath)
+ {
+ CLog::Log(LOGDEBUG, "CBlurayCallback - Error opening dir, null handle!");
+ return nullptr;
+ }
+
+ std::string strDirname = URIUtils::AddFileToFolder(*strBasePath, strRelPath);
+ if (URIUtils::HasSlashAtEnd(strDirname))
+ URIUtils::RemoveSlashAtEnd(strDirname);
+
+ CLog::Log(LOGDEBUG, "CBlurayCallback - Opening dir {}", CURL::GetRedacted(strDirname));
+
+ SDirState *st = new SDirState();
+ if (!CDirectory::GetDirectory(strDirname, st->list, "", DIR_FLAG_DEFAULTS))
+ {
+ if (!CFile::Exists(strDirname))
+ CLog::Log(LOGDEBUG, "CBlurayCallback - Error opening dir! ({})",
+ CURL::GetRedacted(strDirname));
+ delete st;
+ return nullptr;
+ }
+
+ BD_DIR_H *dir = new BD_DIR_H;
+ dir->close = dir_close;
+ dir->read = dir_read;
+ dir->internal = (void*)st;
+
+ return dir;
+}
+
+int CBlurayCallback::dir_read(BD_DIR_H *dir, BD_DIRENT *entry)
+{
+ SDirState* state = static_cast<SDirState*>(dir->internal);
+
+ if (state->curr >= state->list.Size())
+ return 1;
+
+ strncpy(entry->d_name, state->list[state->curr]->GetLabel().c_str(), sizeof(entry->d_name));
+ entry->d_name[sizeof(entry->d_name) - 1] = 0;
+ state->curr++;
+
+ return 0;
+}
+
+void CBlurayCallback::file_close(BD_FILE_H *file)
+{
+ if (file)
+ {
+ delete static_cast<CFile*>(file->internal);
+ delete file;
+ }
+}
+
+int CBlurayCallback::file_eof(BD_FILE_H *file)
+{
+ if (static_cast<CFile*>(file->internal)->GetPosition() == static_cast<CFile*>(file->internal)->GetLength())
+ return 1;
+ else
+ return 0;
+}
+
+BD_FILE_H * CBlurayCallback::file_open(void *handle, const char *rel_path)
+{
+ std::string strRelPath(rel_path);
+ std::string* strBasePath = reinterpret_cast<std::string*>(handle);
+ if (!strBasePath)
+ {
+ CLog::Log(LOGDEBUG, "CBlurayCallback - Error opening dir, null handle!");
+ return nullptr;
+ }
+
+ std::string strFilename = URIUtils::AddFileToFolder(*strBasePath, strRelPath);
+
+ BD_FILE_H *file = new BD_FILE_H;
+
+ file->close = file_close;
+ file->seek = file_seek;
+ file->read = file_read;
+ file->write = file_write;
+ file->tell = file_tell;
+ file->eof = file_eof;
+
+ CFile* fp = new CFile();
+ if (fp->Open(strFilename))
+ {
+ file->internal = (void*)fp;
+ return file;
+ }
+
+ CLog::Log(LOGDEBUG, "CBlurayCallback - Error opening file! ({})", CURL::GetRedacted(strFilename));
+
+ delete fp;
+ delete file;
+
+ return nullptr;
+}
+
+int64_t CBlurayCallback::file_seek(BD_FILE_H *file, int64_t offset, int32_t origin)
+{
+ return static_cast<CFile*>(file->internal)->Seek(offset, origin);
+}
+
+int64_t CBlurayCallback::file_tell(BD_FILE_H *file)
+{
+ return static_cast<CFile*>(file->internal)->GetPosition();
+}
+
+int64_t CBlurayCallback::file_read(BD_FILE_H *file, uint8_t *buf, int64_t size)
+{
+ return static_cast<int64_t>(static_cast<CFile*>(file->internal)->Read(buf, static_cast<size_t>(size)));
+}
+
+int64_t CBlurayCallback::file_write(BD_FILE_H *file, const uint8_t *buf, int64_t size)
+{
+ return static_cast<int64_t>(static_cast<CFile*>(file->internal)->Write(buf, static_cast<size_t>(size)));
+}
diff --git a/xbmc/filesystem/BlurayCallback.h b/xbmc/filesystem/BlurayCallback.h
new file mode 100644
index 0000000..181b056
--- /dev/null
+++ b/xbmc/filesystem/BlurayCallback.h
@@ -0,0 +1,31 @@
+/*
+ * 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 <libbluray/filesystem.h>
+
+class CBlurayCallback
+{
+public:
+ static void bluray_logger(const char* msg);
+ static void dir_close(BD_DIR_H* dir);
+ static BD_DIR_H* dir_open(void* handle, const char* rel_path);
+ static int dir_read(BD_DIR_H* dir, BD_DIRENT* entry);
+ static void file_close(BD_FILE_H* file);
+ static int file_eof(BD_FILE_H* file);
+ static BD_FILE_H* file_open(void* handle, const char* rel_path);
+ static int64_t file_read(BD_FILE_H* file, uint8_t* buf, int64_t size);
+ static int64_t file_seek(BD_FILE_H* file, int64_t offset, int32_t origin);
+ static int64_t file_tell(BD_FILE_H* file);
+ static int64_t file_write(BD_FILE_H* file, const uint8_t* buf, int64_t size);
+
+private:
+ CBlurayCallback() = default;
+ ~CBlurayCallback() = default;
+};
diff --git a/xbmc/filesystem/BlurayDirectory.cpp b/xbmc/filesystem/BlurayDirectory.cpp
new file mode 100644
index 0000000..dbfb41b
--- /dev/null
+++ b/xbmc/filesystem/BlurayDirectory.cpp
@@ -0,0 +1,344 @@
+/*
+ * 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 "BlurayDirectory.h"
+
+#include "File.h"
+#include "FileItem.h"
+#include "LangInfo.h"
+#include "URL.h"
+#include "filesystem/BlurayCallback.h"
+#include "filesystem/Directory.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <array>
+#include <cassert>
+#include <climits>
+#include <stdlib.h>
+#include <string>
+
+#include <libbluray/bluray-version.h>
+#include <libbluray/bluray.h>
+#include <libbluray/filesystem.h>
+#include <libbluray/log_control.h>
+
+namespace XFILE
+{
+
+#define MAIN_TITLE_LENGTH_PERCENT 70 /** Minimum length of main titles, based on longest title */
+
+CBlurayDirectory::~CBlurayDirectory()
+{
+ Dispose();
+}
+
+void CBlurayDirectory::Dispose()
+{
+ if(m_bd)
+ {
+ bd_close(m_bd);
+ m_bd = nullptr;
+ }
+}
+
+std::string CBlurayDirectory::GetBlurayTitle()
+{
+ return GetDiscInfoString(DiscInfo::TITLE);
+}
+
+std::string CBlurayDirectory::GetBlurayID()
+{
+ return GetDiscInfoString(DiscInfo::ID);
+}
+
+std::string CBlurayDirectory::GetDiscInfoString(DiscInfo info)
+{
+ switch (info)
+ {
+ case XFILE::CBlurayDirectory::DiscInfo::TITLE:
+ {
+ if (!m_blurayInitialized)
+ return "";
+ const BLURAY_DISC_INFO* disc_info = bd_get_disc_info(m_bd);
+ if (!disc_info || !disc_info->bluray_detected)
+ return "";
+
+ std::string title = "";
+
+#if (BLURAY_VERSION > BLURAY_VERSION_CODE(1,0,0))
+ title = disc_info->disc_name ? disc_info->disc_name : "";
+#endif
+
+ return title;
+ }
+ case XFILE::CBlurayDirectory::DiscInfo::ID:
+ {
+ if (!m_blurayInitialized)
+ return "";
+
+ const BLURAY_DISC_INFO* disc_info = bd_get_disc_info(m_bd);
+ if (!disc_info || !disc_info->bluray_detected)
+ return "";
+
+ std::string id = "";
+
+#if (BLURAY_VERSION > BLURAY_VERSION_CODE(1,0,0))
+ id = disc_info->udf_volume_id ? disc_info->udf_volume_id : "";
+
+ if (id.empty())
+ {
+ id = HexToString(disc_info->disc_id, 20);
+ }
+#endif
+
+ return id;
+ }
+ default:
+ break;
+ }
+
+ return "";
+}
+
+std::shared_ptr<CFileItem> CBlurayDirectory::GetTitle(const BLURAY_TITLE_INFO* title,
+ const std::string& label)
+{
+ std::string buf;
+ std::string chap;
+ CFileItemPtr item(new CFileItem("", false));
+ CURL path(m_url);
+ buf = StringUtils::Format("BDMV/PLAYLIST/{:05}.mpls", title->playlist);
+ path.SetFileName(buf);
+ item->SetPath(path.Get());
+ int duration = (int)(title->duration / 90000);
+ item->GetVideoInfoTag()->SetDuration(duration);
+ item->GetVideoInfoTag()->m_iTrack = title->playlist;
+ buf = StringUtils::Format(label, title->playlist);
+ item->m_strTitle = buf;
+ item->SetLabel(buf);
+ chap = StringUtils::Format(g_localizeStrings.Get(25007), title->chapter_count,
+ StringUtils::SecondsToTimeString(duration));
+ item->SetLabel2(chap);
+ item->m_dwSize = 0;
+ item->SetArt("icon", "DefaultVideo.png");
+ for(unsigned int i = 0; i < title->clip_count; ++i)
+ item->m_dwSize += title->clips[i].pkt_count * 192;
+
+ return item;
+}
+
+void CBlurayDirectory::GetTitles(bool main, CFileItemList &items)
+{
+ std::vector<BLURAY_TITLE_INFO*> titleList;
+ uint64_t minDuration = 0;
+
+ // Searching for a user provided list of playlists.
+ if (main)
+ titleList = GetUserPlaylists();
+
+ if (!main || titleList.empty())
+ {
+ uint32_t numTitles = bd_get_titles(m_bd, TITLES_RELEVANT, 0);
+
+ for (uint32_t i = 0; i < numTitles; i++)
+ {
+ BLURAY_TITLE_INFO* t = bd_get_title_info(m_bd, i, 0);
+
+ if (!t)
+ {
+ CLog::Log(LOGDEBUG, "CBlurayDirectory - unable to get title {}", i);
+ continue;
+ }
+
+ if (main && t->duration > minDuration)
+ minDuration = t->duration;
+
+ titleList.emplace_back(t);
+ }
+ }
+
+ minDuration = minDuration * MAIN_TITLE_LENGTH_PERCENT / 100;
+
+ for (auto& title : titleList)
+ {
+ if (title->duration < minDuration)
+ continue;
+
+ items.Add(GetTitle(title, main ? g_localizeStrings.Get(25004) /* Main Title */ : g_localizeStrings.Get(25005) /* Title */));
+ bd_free_title_info(title);
+ }
+}
+
+void CBlurayDirectory::GetRoot(CFileItemList &items)
+{
+ GetTitles(true, items);
+
+ CURL path(m_url);
+ CFileItemPtr item;
+
+ path.SetFileName(URIUtils::AddFileToFolder(m_url.GetFileName(), "titles"));
+ item.reset(new CFileItem());
+ item->SetPath(path.Get());
+ item->m_bIsFolder = true;
+ item->SetLabel(g_localizeStrings.Get(25002) /* All titles */);
+ item->SetArt("icon", "DefaultVideoPlaylists.png");
+ items.Add(item);
+
+ const BLURAY_DISC_INFO* disc_info = bd_get_disc_info(m_bd);
+ if (disc_info && disc_info->no_menu_support)
+ {
+ CLog::Log(LOGDEBUG, "CBlurayDirectory::GetRoot - no menu support, skipping menu entry");
+ return;
+ }
+
+ path.SetFileName("menu");
+ item.reset(new CFileItem());
+ item->SetPath(path.Get());
+ item->m_bIsFolder = false;
+ item->SetLabel(g_localizeStrings.Get(25003) /* Menus */);
+ item->SetArt("icon", "DefaultProgram.png");
+ items.Add(item);
+}
+
+bool CBlurayDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ Dispose();
+ m_url = url;
+ std::string root = m_url.GetHostName();
+ std::string file = m_url.GetFileName();
+ URIUtils::RemoveSlashAtEnd(file);
+ URIUtils::RemoveSlashAtEnd(root);
+
+ if (!InitializeBluray(root))
+ return false;
+
+ if(file == "root")
+ GetRoot(items);
+ else if(file == "root/titles")
+ GetTitles(false, items);
+ else
+ {
+ CURL url2 = GetUnderlyingCURL(url);
+ CDirectory::CHints hints;
+ hints.flags = m_flags;
+ if (!CDirectory::GetDirectory(url2, items, hints))
+ return false;
+ }
+
+ items.AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS("%L", "%D", "%L", "")); // FileName, Duration | Foldername, empty
+ items.AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I")); // FileName, Size | Foldername, Size
+
+ return true;
+}
+
+CURL CBlurayDirectory::GetUnderlyingCURL(const CURL& url)
+{
+ assert(url.IsProtocol("bluray"));
+ std::string host = url.GetHostName();
+ const std::string& filename = url.GetFileName();
+ return CURL(host.append(filename));
+}
+
+bool CBlurayDirectory::InitializeBluray(const std::string &root)
+{
+ bd_set_debug_handler(CBlurayCallback::bluray_logger);
+ bd_set_debug_mask(DBG_CRIT | DBG_BLURAY | DBG_NAV);
+
+ m_bd = bd_init();
+
+ if (!m_bd)
+ {
+ CLog::Log(LOGERROR, "CBlurayDirectory::InitializeBluray - failed to initialize libbluray");
+ return false;
+ }
+
+ std::string langCode;
+ g_LangCodeExpander.ConvertToISO6392T(g_langInfo.GetDVDMenuLanguage(), langCode);
+ bd_set_player_setting_str(m_bd, BLURAY_PLAYER_SETTING_MENU_LANG, langCode.c_str());
+
+ if (!bd_open_files(m_bd, const_cast<std::string*>(&root), CBlurayCallback::dir_open, CBlurayCallback::file_open))
+ {
+ CLog::Log(LOGERROR, "CBlurayDirectory::InitializeBluray - failed to open {}",
+ CURL::GetRedacted(root));
+ return false;
+ }
+ m_blurayInitialized = true;
+
+ return true;
+}
+
+std::string CBlurayDirectory::HexToString(const uint8_t *buf, int count)
+{
+ std::array<char, 42> tmp;
+
+ for (int i = 0; i < count; i++)
+ {
+ sprintf(tmp.data() + (i * 2), "%02x", buf[i]);
+ }
+
+ return std::string(std::begin(tmp), std::end(tmp));
+}
+
+std::vector<BLURAY_TITLE_INFO*> CBlurayDirectory::GetUserPlaylists()
+{
+ std::string root = m_url.GetHostName();
+ std::string discInfPath = URIUtils::AddFileToFolder(root, "disc.inf");
+ std::vector<BLURAY_TITLE_INFO*> userTitles;
+ CFile file;
+ char buffer[1025];
+
+ if (file.Open(discInfPath))
+ {
+ CLog::Log(LOGDEBUG, "CBlurayDirectory::GetTitles - disc.inf found");
+
+ CRegExp pl(true);
+ if (!pl.RegComp("(\\d+)"))
+ {
+ file.Close();
+ return userTitles;
+ }
+
+ uint8_t maxLines = 100;
+ while ((maxLines > 0) && file.ReadString(buffer, 1024))
+ {
+ maxLines--;
+ if (StringUtils::StartsWithNoCase(buffer, "playlists"))
+ {
+ int pos = 0;
+ while ((pos = pl.RegFind(buffer, static_cast<unsigned int>(pos))) >= 0)
+ {
+ std::string playlist = pl.GetMatch(0);
+ uint32_t len = static_cast<uint32_t>(playlist.length());
+
+ if (len <= 5)
+ {
+ unsigned long int plNum = strtoul(playlist.c_str(), nullptr, 10);
+
+ BLURAY_TITLE_INFO* t = bd_get_playlist_info(m_bd, static_cast<uint32_t>(plNum), 0);
+ if (t)
+ userTitles.emplace_back(t);
+ }
+
+ if (static_cast<int64_t>(pos) + static_cast<int64_t>(len) > INT_MAX)
+ break;
+ else
+ pos += len;
+ }
+ }
+ }
+ file.Close();
+ }
+ return userTitles;
+}
+
+} /* namespace XFILE */
diff --git a/xbmc/filesystem/BlurayDirectory.h b/xbmc/filesystem/BlurayDirectory.h
new file mode 100644
index 0000000..f4b2be6
--- /dev/null
+++ b/xbmc/filesystem/BlurayDirectory.h
@@ -0,0 +1,56 @@
+/*
+ * 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 "IDirectory.h"
+#include "URL.h"
+
+#include <memory>
+
+class CFileItem;
+class CFileItemList;
+
+typedef struct bluray BLURAY;
+typedef struct bd_title_info BLURAY_TITLE_INFO;
+
+namespace XFILE
+{
+
+class CBlurayDirectory : public IDirectory
+{
+public:
+ CBlurayDirectory() = default;
+ ~CBlurayDirectory() override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+
+ bool InitializeBluray(const std::string &root);
+ std::string GetBlurayTitle();
+ std::string GetBlurayID();
+
+private:
+ enum class DiscInfo
+ {
+ TITLE,
+ ID
+ };
+
+ void Dispose();
+ std::string GetDiscInfoString(DiscInfo info);
+ void GetRoot (CFileItemList &items);
+ void GetTitles(bool main, CFileItemList &items);
+ std::vector<BLURAY_TITLE_INFO*> GetUserPlaylists();
+ std::shared_ptr<CFileItem> GetTitle(const BLURAY_TITLE_INFO* title, const std::string& label);
+ CURL GetUnderlyingCURL(const CURL& url);
+ std::string HexToString(const uint8_t * buf, int count);
+ CURL m_url;
+ BLURAY* m_bd = nullptr;
+ bool m_blurayInitialized = false;
+};
+
+}
diff --git a/xbmc/filesystem/BlurayFile.cpp b/xbmc/filesystem/BlurayFile.cpp
new file mode 100644
index 0000000..34d2732
--- /dev/null
+++ b/xbmc/filesystem/BlurayFile.cpp
@@ -0,0 +1,35 @@
+/*
+ * 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 "BlurayFile.h"
+
+#include "URL.h"
+
+#include <assert.h>
+
+namespace XFILE
+{
+
+ CBlurayFile::CBlurayFile(void)
+ : COverrideFile(false)
+ { }
+
+ CBlurayFile::~CBlurayFile(void) = default;
+
+ std::string CBlurayFile::TranslatePath(const CURL& url)
+ {
+ assert(url.IsProtocol("bluray"));
+
+ std::string host = url.GetHostName();
+ const std::string& filename = url.GetFileName();
+ if (host.empty() || filename.empty())
+ return "";
+
+ return host.append(filename);
+ }
+} /* namespace XFILE */
diff --git a/xbmc/filesystem/BlurayFile.h b/xbmc/filesystem/BlurayFile.h
new file mode 100644
index 0000000..1d43936
--- /dev/null
+++ b/xbmc/filesystem/BlurayFile.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "filesystem/OverrideFile.h"
+
+namespace XFILE
+{
+
+ class CBlurayFile : public COverrideFile
+ {
+ public:
+ CBlurayFile();
+ ~CBlurayFile() override;
+
+ protected:
+ std::string TranslatePath(const CURL& url) override;
+ };
+}
diff --git a/xbmc/filesystem/CDDADirectory.cpp b/xbmc/filesystem/CDDADirectory.cpp
new file mode 100644
index 0000000..9b6beaa
--- /dev/null
+++ b/xbmc/filesystem/CDDADirectory.cpp
@@ -0,0 +1,71 @@
+/*
+ * 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 "CDDADirectory.h"
+
+#include "File.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "music/MusicDatabase.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+
+using namespace XFILE;
+using namespace MEDIA_DETECT;
+
+CCDDADirectory::CCDDADirectory(void) = default;
+
+CCDDADirectory::~CCDDADirectory(void) = default;
+
+
+bool CCDDADirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ // Reads the tracks from an audio cd
+ std::string strPath = url.Get();
+
+ if (!CServiceBroker::GetMediaManager().IsDiscInDrive(strPath))
+ return false;
+
+ // Get information for the inserted disc
+ CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo(strPath);
+ if (pCdInfo == NULL)
+ return false;
+
+ // Preload CDDB info
+ CMusicDatabase musicdatabase;
+ musicdatabase.LookupCDDBInfo();
+
+ // If the disc has no tracks, we are finished here.
+ int nTracks = pCdInfo->GetTrackCount();
+ if (nTracks <= 0)
+ return false;
+
+ // Generate fileitems
+ for (int i = 1;i <= nTracks;++i)
+ {
+ // Skip Datatracks for display,
+ // but needed to query cddb
+ if (!pCdInfo->IsAudio(i))
+ continue;
+
+ // Format standard cdda item label
+ std::string strLabel = StringUtils::Format("Track {:02}", i);
+
+ CFileItemPtr pItem(new CFileItem(strLabel));
+ pItem->m_bIsFolder = false;
+ std::string path = StringUtils::Format("cdda://local/{:02}.cdda", i);
+ pItem->SetPath(path);
+
+ struct __stat64 s64;
+ if (CFile::Stat(pItem->GetPath(), &s64) == 0)
+ pItem->m_dwSize = s64.st_size;
+
+ items.Add(pItem);
+ }
+ return true;
+}
diff --git a/xbmc/filesystem/CDDADirectory.h b/xbmc/filesystem/CDDADirectory.h
new file mode 100644
index 0000000..12e5d42
--- /dev/null
+++ b/xbmc/filesystem/CDDADirectory.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "IDirectory.h"
+
+namespace XFILE
+{
+
+class CCDDADirectory :
+ public IDirectory
+{
+public:
+ CCDDADirectory(void);
+ ~CCDDADirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+};
+}
diff --git a/xbmc/filesystem/CDDAFile.cpp b/xbmc/filesystem/CDDAFile.cpp
new file mode 100644
index 0000000..cd98b08
--- /dev/null
+++ b/xbmc/filesystem/CDDAFile.cpp
@@ -0,0 +1,227 @@
+/*
+ * 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 "CDDAFile.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "storage/MediaManager.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+#include <sys/stat.h>
+
+using namespace MEDIA_DETECT;
+using namespace XFILE;
+
+CFileCDDA::CFileCDDA(void)
+{
+ m_pCdIo = NULL;
+ m_cdio = CLibcdio::GetInstance();
+ m_iSectorCount = 52;
+}
+
+CFileCDDA::~CFileCDDA(void)
+{
+ Close();
+}
+
+bool CFileCDDA::Open(const CURL& url)
+{
+ std::string strURL = url.GetWithoutFilename();
+
+ if (!CServiceBroker::GetMediaManager().IsDiscInDrive(strURL) || !IsValidFile(url))
+ return false;
+
+ // Open the dvd drive
+#ifdef TARGET_POSIX
+ m_pCdIo = m_cdio->cdio_open(CServiceBroker::GetMediaManager().TranslateDevicePath(strURL).c_str(),
+ DRIVER_UNKNOWN);
+#elif defined(TARGET_WINDOWS)
+ m_pCdIo = m_cdio->cdio_open_win32(
+ CServiceBroker::GetMediaManager().TranslateDevicePath(strURL, true).c_str());
+#endif
+ if (!m_pCdIo)
+ {
+ CLog::Log(LOGERROR, "file cdda: Opening the dvd drive failed");
+ return false;
+ }
+
+ int iTrack = GetTrackNum(url);
+
+ m_lsnStart = m_cdio->cdio_get_track_lsn(m_pCdIo, iTrack);
+ m_lsnEnd = m_cdio->cdio_get_track_last_lsn(m_pCdIo, iTrack);
+ m_lsnCurrent = m_lsnStart;
+
+ if (m_lsnStart == CDIO_INVALID_LSN || m_lsnEnd == CDIO_INVALID_LSN)
+ {
+ m_cdio->cdio_destroy(m_pCdIo);
+ m_pCdIo = NULL;
+ return false;
+ }
+
+ return true;
+}
+
+bool CFileCDDA::Exists(const CURL& url)
+{
+ if (!IsValidFile(url))
+ return false;
+
+ int iTrack = GetTrackNum(url);
+
+ if (!Open(url))
+ return false;
+
+ int iLastTrack = m_cdio->cdio_get_last_track_num(m_pCdIo);
+ if (iLastTrack == CDIO_INVALID_TRACK)
+ return false;
+
+ return (iTrack > 0 && iTrack <= iLastTrack);
+}
+
+int CFileCDDA::Stat(const CURL& url, struct __stat64* buffer)
+{
+ if (Open(url) && buffer)
+ {
+ *buffer = {};
+ buffer->st_size = GetLength();
+ buffer->st_mode = _S_IFREG;
+ Close();
+ return 0;
+ }
+ errno = ENOENT;
+ return -1;
+}
+
+ssize_t CFileCDDA::Read(void* lpBuf, size_t uiBufSize)
+{
+ if (!m_pCdIo || !CServiceBroker::GetMediaManager().IsDiscInDrive())
+ return -1;
+
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+ // limit number of sectors that fits in buffer by m_iSectorCount
+ int iSectorCount = std::min((int)uiBufSize / CDIO_CD_FRAMESIZE_RAW, m_iSectorCount);
+
+ if (iSectorCount <= 0)
+ return -1;
+
+ // Are there enough sectors left to read
+ if (m_lsnCurrent + iSectorCount > m_lsnEnd)
+ iSectorCount = m_lsnEnd - m_lsnCurrent;
+
+ // The loop tries to solve read error problem by lowering number of sectors to read (iSectorCount).
+ // When problem is solved the proper number of sectors is stored in m_iSectorCount
+ int big_iSectorCount = iSectorCount;
+ while (iSectorCount > 0)
+ {
+ int iret = m_cdio->cdio_read_audio_sectors(m_pCdIo, lpBuf, m_lsnCurrent, iSectorCount);
+
+ if (iret == DRIVER_OP_SUCCESS)
+ {
+ // If lower iSectorCount solved the problem limit it's value
+ if (iSectorCount < big_iSectorCount)
+ {
+ m_iSectorCount = iSectorCount;
+ }
+ break;
+ }
+
+ // iSectorCount is low so it cannot solve read problem
+ if (iSectorCount <= 10)
+ {
+ CLog::Log(LOGERROR,
+ "file cdda: Reading {} sectors of audio data starting at lsn {} failed with error "
+ "code {}",
+ iSectorCount, m_lsnCurrent, iret);
+ return -1;
+ }
+
+ iSectorCount = 10;
+ }
+ m_lsnCurrent += iSectorCount;
+
+ return iSectorCount*CDIO_CD_FRAMESIZE_RAW;
+}
+
+int64_t CFileCDDA::Seek(int64_t iFilePosition, int iWhence /*=SEEK_SET*/)
+{
+ if (!m_pCdIo)
+ return -1;
+
+ lsn_t lsnPosition = (int)iFilePosition / CDIO_CD_FRAMESIZE_RAW;
+
+ switch (iWhence)
+ {
+ case SEEK_SET:
+ // cur = pos
+ m_lsnCurrent = m_lsnStart + lsnPosition;
+ break;
+ case SEEK_CUR:
+ // cur += pos
+ m_lsnCurrent += lsnPosition;
+ break;
+ case SEEK_END:
+ // end += pos
+ m_lsnCurrent = m_lsnEnd + lsnPosition;
+ break;
+ default:
+ return -1;
+ }
+
+ return ((int64_t)(m_lsnCurrent -m_lsnStart)*CDIO_CD_FRAMESIZE_RAW);
+}
+
+void CFileCDDA::Close()
+{
+ if (m_pCdIo)
+ {
+ m_cdio->cdio_destroy(m_pCdIo);
+ m_pCdIo = NULL;
+ }
+}
+
+int64_t CFileCDDA::GetPosition()
+{
+ if (!m_pCdIo)
+ return 0;
+
+ return ((int64_t)(m_lsnCurrent -m_lsnStart)*CDIO_CD_FRAMESIZE_RAW);
+}
+
+int64_t CFileCDDA::GetLength()
+{
+ if (!m_pCdIo)
+ return 0;
+
+ return ((int64_t)(m_lsnEnd -m_lsnStart)*CDIO_CD_FRAMESIZE_RAW);
+}
+
+bool CFileCDDA::IsValidFile(const CURL& url)
+{
+ // Only .cdda files are supported
+ return URIUtils::HasExtension(url.Get(), ".cdda");
+}
+
+int CFileCDDA::GetTrackNum(const CURL& url)
+{
+ std::string strFileName = url.Get();
+
+ // get track number from "cdda://local/01.cdda"
+ return atoi(strFileName.substr(13, strFileName.size() - 13 - 5).c_str());
+}
+
+#define SECTOR_COUNT 52 // max. sectors that can be read at once
+int CFileCDDA::GetChunkSize()
+{
+ return SECTOR_COUNT*CDIO_CD_FRAMESIZE_RAW;
+}
diff --git a/xbmc/filesystem/CDDAFile.h b/xbmc/filesystem/CDDAFile.h
new file mode 100644
index 0000000..654e0f9
--- /dev/null
+++ b/xbmc/filesystem/CDDAFile.h
@@ -0,0 +1,44 @@
+/*
+ * 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 "IFile.h"
+#include "storage/cdioSupport.h"
+
+namespace XFILE
+{
+class CFileCDDA : public IFile
+{
+public:
+ CFileCDDA(void);
+ ~CFileCDDA(void) override;
+ bool Open(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+
+ ssize_t Read(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;
+
+protected:
+ bool IsValidFile(const CURL& url);
+ int GetTrackNum(const CURL& url);
+
+protected:
+ CdIo_t* m_pCdIo;
+ lsn_t m_lsnStart = CDIO_INVALID_LSN; // Start of m_iTrack in logical sector number
+ lsn_t m_lsnCurrent = CDIO_INVALID_LSN; // Position inside the track in logical sector number
+ lsn_t m_lsnEnd = CDIO_INVALID_LSN; // End of m_iTrack in logical sector number
+ int m_iSectorCount; // max number of sectors to read at once
+ std::shared_ptr<MEDIA_DETECT::CLibcdio> m_cdio;
+};
+}
diff --git a/xbmc/filesystem/CMakeLists.txt b/xbmc/filesystem/CMakeLists.txt
new file mode 100644
index 0000000..8bbaa44
--- /dev/null
+++ b/xbmc/filesystem/CMakeLists.txt
@@ -0,0 +1,177 @@
+set(SOURCES AddonsDirectory.cpp
+ AudioBookFileDirectory.cpp
+ CacheStrategy.cpp
+ CircularCache.cpp
+ CurlFile.cpp
+ DAVCommon.cpp
+ DAVDirectory.cpp
+ DAVFile.cpp
+ DirectoryCache.cpp
+ Directory.cpp
+ DirectoryFactory.cpp
+ DirectoryHistory.cpp
+ DllLibCurl.cpp
+ EventsDirectory.cpp
+ FavouritesDirectory.cpp
+ FileCache.cpp
+ File.cpp
+ FileDirectoryFactory.cpp
+ FileFactory.cpp
+ FTPDirectory.cpp
+ FTPParse.cpp
+ HTTPDirectory.cpp
+ IDirectory.cpp
+ IFile.cpp
+ ImageFile.cpp
+ LibraryDirectory.cpp
+ MultiPathDirectory.cpp
+ MultiPathFile.cpp
+ MusicDatabaseDirectory.cpp
+ MusicDatabaseFile.cpp
+ MusicFileDirectory.cpp
+ MusicSearchDirectory.cpp
+ OverrideDirectory.cpp
+ OverrideFile.cpp
+ PipeFile.cpp
+ PipesManager.cpp
+ PlaylistDirectory.cpp
+ PlaylistFileDirectory.cpp
+ PluginDirectory.cpp
+ PluginFile.cpp
+ PVRDirectory.cpp
+ ResourceDirectory.cpp
+ ResourceFile.cpp
+ RSSDirectory.cpp
+ ShoutcastFile.cpp
+ SmartPlaylistDirectory.cpp
+ SourcesDirectory.cpp
+ SpecialProtocol.cpp
+ SpecialProtocolDirectory.cpp
+ SpecialProtocolFile.cpp
+ StackDirectory.cpp
+ VideoDatabaseDirectory.cpp
+ VideoDatabaseFile.cpp
+ VirtualDirectory.cpp
+ XbtDirectory.cpp
+ XbtFile.cpp
+ XbtManager.cpp
+ ZeroconfDirectory.cpp
+ ZipDirectory.cpp
+ ZipFile.cpp
+ ZipManager.cpp)
+
+set(HEADERS AddonsDirectory.h
+ CacheStrategy.h
+ CircularCache.h
+ CurlFile.h
+ DAVCommon.h
+ DAVDirectory.h
+ DAVFile.h
+ Directorization.h
+ Directory.h
+ DirectoryCache.h
+ DirectoryFactory.h
+ DirectoryHistory.h
+ DllLibCurl.h
+ EventsDirectory.h
+ FTPDirectory.h
+ FTPParse.h
+ FavouritesDirectory.h
+ File.h
+ FileCache.h
+ FileDirectoryFactory.h
+ FileFactory.h
+ HTTPDirectory.h
+ IDirectory.h
+ IFile.h
+ IFileDirectory.h
+ IFileTypes.h
+ ImageFile.h
+ LibraryDirectory.h
+ MultiPathDirectory.h
+ MultiPathFile.h
+ MusicDatabaseDirectory.h
+ MusicDatabaseFile.h
+ MusicFileDirectory.h
+ MusicSearchDirectory.h
+ OverrideDirectory.h
+ OverrideFile.h
+ PVRDirectory.h
+ PipeFile.h
+ PipesManager.h
+ PlaylistDirectory.h
+ PlaylistFileDirectory.h
+ PluginDirectory.h
+ PluginFile.h
+ RSSDirectory.h
+ ResourceDirectory.h
+ ResourceFile.h
+ ShoutcastFile.h
+ SmartPlaylistDirectory.h
+ SourcesDirectory.h
+ SpecialProtocol.h
+ SpecialProtocolDirectory.h
+ SpecialProtocolFile.h
+ StackDirectory.h
+ VideoDatabaseDirectory.h
+ VideoDatabaseFile.h
+ VirtualDirectory.h
+ XbtDirectory.h
+ XbtFile.h
+ XbtManager.h
+ ZeroconfDirectory.h
+ ZipDirectory.h
+ ZipFile.h
+ ZipManager.h)
+
+if(ISO9660PP_FOUND)
+ list(APPEND SOURCES ISO9660Directory.cpp
+ ISO9660File.cpp)
+ list(APPEND HEADERS ISO9660Directory.h
+ ISO9660File.h)
+endif()
+
+if(UDFREAD_FOUND)
+ list(APPEND SOURCES UDFBlockInput.cpp
+ UDFDirectory.cpp
+ UDFFile.cpp)
+ list(APPEND HEADERS UDFBlockInput.h
+ UDFDirectory.h
+ UDFFile.h)
+endif()
+
+if(BLURAY_FOUND)
+ list(APPEND SOURCES BlurayCallback.cpp
+ BlurayDirectory.cpp
+ BlurayFile.cpp)
+ list(APPEND HEADERS BlurayCallback.h
+ BlurayDirectory.h
+ BlurayFile.h)
+endif()
+
+if(ENABLE_OPTICAL)
+ list(APPEND SOURCES CDDADirectory.cpp
+ CDDAFile.cpp)
+ list(APPEND HEADERS CDDADirectory.h
+ CDDAFile.h)
+endif()
+
+if(NFS_FOUND)
+ list(APPEND SOURCES NFSDirectory.cpp
+ NFSFile.cpp)
+ list(APPEND HEADERS NFSDirectory.h
+ NFSFile.h)
+endif()
+
+if(ENABLE_UPNP)
+ list(APPEND SOURCES NptXbmcFile.cpp
+ UPnPDirectory.cpp
+ UPnPFile.cpp)
+ list(APPEND HEADERS UPnPDirectory.h
+ UPnPFile.h)
+endif()
+
+core_add_library(filesystem)
+if(ENABLE_STATIC_LIBS AND ENABLE_UPNP)
+ target_link_libraries(filesystem PRIVATE upnp)
+endif()
diff --git a/xbmc/filesystem/CacheStrategy.cpp b/xbmc/filesystem/CacheStrategy.cpp
new file mode 100644
index 0000000..7aaeec9
--- /dev/null
+++ b/xbmc/filesystem/CacheStrategy.cpp
@@ -0,0 +1,447 @@
+/*
+ * 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 "threads/SystemClock.h"
+#include "CacheStrategy.h"
+#include "IFile.h"
+#ifdef TARGET_POSIX
+#include "PlatformDefs.h"
+#include "platform/posix/ConvUtils.h"
+#endif
+#include "Util.h"
+#include "utils/log.h"
+#include "SpecialProtocol.h"
+#include "URL.h"
+#if defined(TARGET_POSIX)
+#include "platform/posix/filesystem/PosixFile.h"
+#define CacheLocalFile CPosixFile
+#elif defined(TARGET_WINDOWS)
+#include "platform/win32/filesystem/Win32File.h"
+#define CacheLocalFile CWin32File
+#endif // TARGET_WINDOWS
+
+#include <cassert>
+#include <algorithm>
+
+using namespace XFILE;
+
+using namespace std::chrono_literals;
+
+CCacheStrategy::~CCacheStrategy() = default;
+
+void CCacheStrategy::EndOfInput() {
+ m_bEndOfInput = true;
+}
+
+bool CCacheStrategy::IsEndOfInput()
+{
+ return m_bEndOfInput;
+}
+
+void CCacheStrategy::ClearEndOfInput()
+{
+ m_bEndOfInput = false;
+}
+
+CSimpleFileCache::CSimpleFileCache()
+ : m_cacheFileRead(new CacheLocalFile())
+ , m_cacheFileWrite(new CacheLocalFile())
+ , m_hDataAvailEvent(NULL)
+{
+}
+
+CSimpleFileCache::~CSimpleFileCache()
+{
+ Close();
+ delete m_cacheFileRead;
+ delete m_cacheFileWrite;
+}
+
+int CSimpleFileCache::Open()
+{
+ Close();
+
+ m_hDataAvailEvent = new CEvent;
+
+ m_filename = CSpecialProtocol::TranslatePath(
+ CUtil::GetNextFilename("special://temp/filecache{:03}.cache", 999));
+ if (m_filename.empty())
+ {
+ CLog::Log(LOGERROR, "CSimpleFileCache::{} - Unable to generate a new filename", __FUNCTION__);
+ Close();
+ return CACHE_RC_ERROR;
+ }
+
+ CURL fileURL(m_filename);
+
+ if (!m_cacheFileWrite->OpenForWrite(fileURL, false))
+ {
+ CLog::Log(LOGERROR, "CSimpleFileCache::{} - Failed to create file \"{}\" for writing",
+ __FUNCTION__, m_filename);
+ Close();
+ return CACHE_RC_ERROR;
+ }
+
+ if (!m_cacheFileRead->Open(fileURL))
+ {
+ CLog::Log(LOGERROR, "CSimpleFileCache::{} - Failed to open file \"{}\" for reading",
+ __FUNCTION__, m_filename);
+ Close();
+ return CACHE_RC_ERROR;
+ }
+
+ return CACHE_RC_OK;
+}
+
+void CSimpleFileCache::Close()
+{
+ if (m_hDataAvailEvent)
+ delete m_hDataAvailEvent;
+
+ m_hDataAvailEvent = NULL;
+
+ m_cacheFileWrite->Close();
+ m_cacheFileRead->Close();
+
+ if (!m_filename.empty() && !m_cacheFileRead->Delete(CURL(m_filename)))
+ CLog::Log(LOGWARNING, "SimpleFileCache::{} - Failed to delete cache file \"{}\"", __FUNCTION__,
+ m_filename);
+
+ m_filename.clear();
+}
+
+size_t CSimpleFileCache::GetMaxWriteSize(const size_t& iRequestSize)
+{
+ return iRequestSize; // Can always write since it's on disk
+}
+
+int CSimpleFileCache::WriteToCache(const char *pBuffer, size_t iSize)
+{
+ size_t written = 0;
+ while (iSize > 0)
+ {
+ const ssize_t lastWritten =
+ m_cacheFileWrite->Write(pBuffer, std::min(iSize, static_cast<size_t>(SSIZE_MAX)));
+ if (lastWritten <= 0)
+ {
+ CLog::Log(LOGERROR, "SimpleFileCache::{} - <{}> Failed to write to cache", __FUNCTION__,
+ m_filename);
+ return CACHE_RC_ERROR;
+ }
+ m_nWritePosition += lastWritten;
+ iSize -= lastWritten;
+ written += lastWritten;
+ }
+
+ // when reader waits for data it will wait on the event.
+ m_hDataAvailEvent->Set();
+
+ return written;
+}
+
+int64_t CSimpleFileCache::GetAvailableRead()
+{
+ return m_nWritePosition - m_nReadPosition;
+}
+
+int CSimpleFileCache::ReadFromCache(char *pBuffer, size_t iMaxSize)
+{
+ int64_t iAvailable = GetAvailableRead();
+ if ( iAvailable <= 0 )
+ return m_bEndOfInput ? 0 : CACHE_RC_WOULD_BLOCK;
+
+ size_t toRead = std::min(iMaxSize, static_cast<size_t>(iAvailable));
+
+ size_t readBytes = 0;
+ while (toRead > 0)
+ {
+ const ssize_t lastRead =
+ m_cacheFileRead->Read(pBuffer, std::min(toRead, static_cast<size_t>(SSIZE_MAX)));
+
+ if (lastRead == 0)
+ break;
+ if (lastRead < 0)
+ {
+ CLog::Log(LOGERROR, "CSimpleFileCache::{} - <{}> Failed to read from cache", __FUNCTION__,
+ m_filename);
+ return CACHE_RC_ERROR;
+ }
+ m_nReadPosition += lastRead;
+ toRead -= lastRead;
+ readBytes += lastRead;
+ }
+
+ if (readBytes > 0)
+ m_space.Set();
+
+ return readBytes;
+}
+
+int64_t CSimpleFileCache::WaitForData(uint32_t iMinAvail, std::chrono::milliseconds timeout)
+{
+ if (timeout == 0ms || IsEndOfInput())
+ return GetAvailableRead();
+
+ XbmcThreads::EndTime<> endTime{timeout};
+ while (!IsEndOfInput())
+ {
+ int64_t iAvail = GetAvailableRead();
+ if (iAvail >= iMinAvail)
+ return iAvail;
+
+ if (!m_hDataAvailEvent->Wait(endTime.GetTimeLeft()))
+ return CACHE_RC_TIMEOUT;
+ }
+ return GetAvailableRead();
+}
+
+int64_t CSimpleFileCache::Seek(int64_t iFilePosition)
+{
+ int64_t iTarget = iFilePosition - m_nStartPosition;
+
+ if (iTarget < 0)
+ {
+ CLog::Log(LOGDEBUG, "CSimpleFileCache::{} - <{}> Request seek to {} before start of cache",
+ __FUNCTION__, iFilePosition, m_filename);
+ return CACHE_RC_ERROR;
+ }
+
+ int64_t nDiff = iTarget - m_nWritePosition;
+ if (nDiff > 500000)
+ {
+ CLog::Log(LOGDEBUG,
+ "CSimpleFileCache::{} - <{}> Requested position {} is beyond cached data ({})",
+ __FUNCTION__, m_filename, iFilePosition, m_nWritePosition);
+ return CACHE_RC_ERROR;
+ }
+
+ if (nDiff > 0 &&
+ WaitForData(static_cast<uint32_t>(iTarget - m_nReadPosition), 5s) == CACHE_RC_TIMEOUT)
+ {
+ CLog::Log(LOGDEBUG, "CSimpleFileCache::{} - <{}> Wait for position {} failed. Ended up at {}",
+ __FUNCTION__, m_filename, iFilePosition, m_nWritePosition);
+ return CACHE_RC_ERROR;
+ }
+
+ m_nReadPosition = m_cacheFileRead->Seek(iTarget, SEEK_SET);
+ if (m_nReadPosition != iTarget)
+ {
+ CLog::Log(LOGERROR, "CSimpleFileCache::{} - <{}> Can't seek cache file for position {}",
+ __FUNCTION__, iFilePosition, m_filename);
+ return CACHE_RC_ERROR;
+ }
+
+ m_space.Set();
+
+ return iFilePosition;
+}
+
+bool CSimpleFileCache::Reset(int64_t iSourcePosition)
+{
+ if (IsCachedPosition(iSourcePosition))
+ {
+ m_nReadPosition = m_cacheFileRead->Seek(iSourcePosition - m_nStartPosition, SEEK_SET);
+ return false;
+ }
+
+ m_nStartPosition = iSourcePosition;
+ m_nWritePosition = m_cacheFileWrite->Seek(0, SEEK_SET);
+ m_nReadPosition = m_cacheFileRead->Seek(0, SEEK_SET);
+ return true;
+}
+
+void CSimpleFileCache::EndOfInput()
+{
+ CCacheStrategy::EndOfInput();
+ m_hDataAvailEvent->Set();
+}
+
+int64_t CSimpleFileCache::CachedDataEndPosIfSeekTo(int64_t iFilePosition)
+{
+ if (iFilePosition >= m_nStartPosition && iFilePosition <= m_nStartPosition + m_nWritePosition)
+ return m_nStartPosition + m_nWritePosition;
+ return iFilePosition;
+}
+
+int64_t CSimpleFileCache::CachedDataStartPos()
+{
+ return m_nStartPosition;
+}
+
+int64_t CSimpleFileCache::CachedDataEndPos()
+{
+ return m_nStartPosition + m_nWritePosition;
+}
+
+bool CSimpleFileCache::IsCachedPosition(int64_t iFilePosition)
+{
+ return iFilePosition >= m_nStartPosition && iFilePosition <= m_nStartPosition + m_nWritePosition;
+}
+
+CCacheStrategy *CSimpleFileCache::CreateNew()
+{
+ return new CSimpleFileCache();
+}
+
+
+CDoubleCache::CDoubleCache(CCacheStrategy *impl)
+{
+ assert(NULL != impl);
+ m_pCache = impl;
+ m_pCacheOld = NULL;
+}
+
+CDoubleCache::~CDoubleCache()
+{
+ delete m_pCache;
+ delete m_pCacheOld;
+}
+
+int CDoubleCache::Open()
+{
+ return m_pCache->Open();
+}
+
+void CDoubleCache::Close()
+{
+ m_pCache->Close();
+ if (m_pCacheOld)
+ {
+ delete m_pCacheOld;
+ m_pCacheOld = NULL;
+ }
+}
+
+size_t CDoubleCache::GetMaxWriteSize(const size_t& iRequestSize)
+{
+ return m_pCache->GetMaxWriteSize(iRequestSize); // NOTE: Check the active cache only
+}
+
+int CDoubleCache::WriteToCache(const char *pBuffer, size_t iSize)
+{
+ return m_pCache->WriteToCache(pBuffer, iSize);
+}
+
+int CDoubleCache::ReadFromCache(char *pBuffer, size_t iMaxSize)
+{
+ return m_pCache->ReadFromCache(pBuffer, iMaxSize);
+}
+
+int64_t CDoubleCache::WaitForData(uint32_t iMinAvail, std::chrono::milliseconds timeout)
+{
+ return m_pCache->WaitForData(iMinAvail, timeout);
+}
+
+int64_t CDoubleCache::Seek(int64_t iFilePosition)
+{
+ /* Check whether position is NOT in our current cache but IS in our old cache.
+ * This is faster/more efficient than having to possibly wait for data in the
+ * Seek() call below
+ */
+ if (!m_pCache->IsCachedPosition(iFilePosition) &&
+ m_pCacheOld && m_pCacheOld->IsCachedPosition(iFilePosition))
+ {
+ // Return error to trigger a seek event which will swap the caches:
+ return CACHE_RC_ERROR;
+ }
+
+ return m_pCache->Seek(iFilePosition); // Normal seek
+}
+
+bool CDoubleCache::Reset(int64_t iSourcePosition)
+{
+ /* Check if we should (not) swap the caches. Note that when both caches have the
+ * requested position, we prefer the cache that has the most forward data
+ */
+ if (m_pCache->IsCachedPosition(iSourcePosition) &&
+ (!m_pCacheOld || !m_pCacheOld->IsCachedPosition(iSourcePosition) ||
+ m_pCache->CachedDataEndPos() >= m_pCacheOld->CachedDataEndPos()))
+ {
+ // No swap: Just use current cache
+ return m_pCache->Reset(iSourcePosition);
+ }
+
+ // Need to swap caches
+ CCacheStrategy* pCacheTmp;
+ if (!m_pCacheOld)
+ {
+ pCacheTmp = m_pCache->CreateNew();
+ if (pCacheTmp->Open() != CACHE_RC_OK)
+ {
+ delete pCacheTmp;
+ return m_pCache->Reset(iSourcePosition);
+ }
+ }
+ else
+ {
+ pCacheTmp = m_pCacheOld;
+ }
+
+ // Perform actual swap:
+ m_pCacheOld = m_pCache;
+ m_pCache = pCacheTmp;
+
+ // If new active cache still doesn't have this position, log it
+ if (!m_pCache->IsCachedPosition(iSourcePosition))
+ {
+ CLog::Log(LOGDEBUG, "CDoubleCache::{} - ({}) Cache miss for {} with new={}-{} and old={}-{}",
+ __FUNCTION__, fmt::ptr(this), iSourcePosition, m_pCache->CachedDataStartPos(),
+ m_pCache->CachedDataEndPos(), m_pCacheOld->CachedDataStartPos(),
+ m_pCacheOld->CachedDataEndPos());
+ }
+
+ return m_pCache->Reset(iSourcePosition);
+}
+
+void CDoubleCache::EndOfInput()
+{
+ m_pCache->EndOfInput();
+}
+
+bool CDoubleCache::IsEndOfInput()
+{
+ return m_pCache->IsEndOfInput();
+}
+
+void CDoubleCache::ClearEndOfInput()
+{
+ m_pCache->ClearEndOfInput();
+}
+
+int64_t CDoubleCache::CachedDataStartPos()
+{
+ return m_pCache->CachedDataStartPos();
+}
+
+int64_t CDoubleCache::CachedDataEndPos()
+{
+ return m_pCache->CachedDataEndPos();
+}
+
+int64_t CDoubleCache::CachedDataEndPosIfSeekTo(int64_t iFilePosition)
+{
+ /* Return the position on source we would end up after a cache-seek(/reset)
+ * Note that we select the cache that has the most forward data already cached
+ * for this position
+ */
+ int64_t ret = m_pCache->CachedDataEndPosIfSeekTo(iFilePosition);
+ if (m_pCacheOld)
+ return std::max(ret, m_pCacheOld->CachedDataEndPosIfSeekTo(iFilePosition));
+ return ret;
+}
+
+bool CDoubleCache::IsCachedPosition(int64_t iFilePosition)
+{
+ return m_pCache->IsCachedPosition(iFilePosition) || (m_pCacheOld && m_pCacheOld->IsCachedPosition(iFilePosition));
+}
+
+CCacheStrategy *CDoubleCache::CreateNew()
+{
+ return new CDoubleCache(m_pCache->CreateNew());
+}
+
diff --git a/xbmc/filesystem/CacheStrategy.h b/xbmc/filesystem/CacheStrategy.h
new file mode 100644
index 0000000..76c66bb
--- /dev/null
+++ b/xbmc/filesystem/CacheStrategy.h
@@ -0,0 +1,133 @@
+/*
+ * 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 "threads/Event.h"
+
+#include <stdint.h>
+#include <string>
+
+namespace XFILE {
+
+#define CACHE_RC_OK 0
+#define CACHE_RC_ERROR -1
+#define CACHE_RC_WOULD_BLOCK -2
+#define CACHE_RC_TIMEOUT -3
+
+class IFile; // forward declaration
+
+class CCacheStrategy{
+public:
+ virtual ~CCacheStrategy();
+
+ virtual int Open() = 0;
+ virtual void Close() = 0;
+
+ virtual size_t GetMaxWriteSize(const size_t& iRequestSize) = 0;
+ virtual int WriteToCache(const char *pBuffer, size_t iSize) = 0;
+ virtual int ReadFromCache(char *pBuffer, size_t iMaxSize) = 0;
+ virtual int64_t WaitForData(uint32_t iMinAvail, std::chrono::milliseconds timeout) = 0;
+
+ virtual int64_t Seek(int64_t iFilePosition) = 0;
+
+ /*!
+ \brief Reset cache position
+ \param iSourcePosition position to reset to
+ \return Whether a full reset was performed, or not (e.g. only cache swap)
+ \sa CCacheStrategy
+ */
+ virtual bool Reset(int64_t iSourcePosition) = 0;
+
+ virtual void EndOfInput(); // mark the end of the input stream so that Read will know when to return EOF
+ virtual bool IsEndOfInput();
+ virtual void ClearEndOfInput();
+
+ virtual int64_t CachedDataEndPosIfSeekTo(int64_t iFilePosition) = 0;
+ virtual int64_t CachedDataStartPos() = 0;
+ virtual int64_t CachedDataEndPos() = 0;
+ virtual bool IsCachedPosition(int64_t iFilePosition) = 0;
+
+ virtual CCacheStrategy *CreateNew() = 0;
+
+ CEvent m_space;
+protected:
+ bool m_bEndOfInput = false;
+};
+
+/**
+*/
+class CSimpleFileCache : public CCacheStrategy {
+public:
+ CSimpleFileCache();
+ ~CSimpleFileCache() override;
+
+ int Open() override;
+ void Close() override;
+
+ size_t GetMaxWriteSize(const size_t& iRequestSize) override;
+ int WriteToCache(const char *pBuffer, size_t iSize) override;
+ int ReadFromCache(char *pBuffer, size_t iMaxSize) override;
+ int64_t WaitForData(uint32_t iMinAvail, std::chrono::milliseconds timeout) override;
+
+ int64_t Seek(int64_t iFilePosition) override;
+ bool Reset(int64_t iSourcePosition) override;
+ void EndOfInput() override;
+
+ int64_t CachedDataEndPosIfSeekTo(int64_t iFilePosition) override;
+ int64_t CachedDataStartPos() override;
+ int64_t CachedDataEndPos() override;
+ bool IsCachedPosition(int64_t iFilePosition) override;
+
+ CCacheStrategy *CreateNew() override;
+
+ int64_t GetAvailableRead();
+
+protected:
+ std::string m_filename;
+ IFile* m_cacheFileRead;
+ IFile* m_cacheFileWrite;
+ CEvent* m_hDataAvailEvent;
+ volatile int64_t m_nStartPosition = 0;
+ volatile int64_t m_nWritePosition = 0;
+ volatile int64_t m_nReadPosition = 0;
+};
+
+class CDoubleCache : public CCacheStrategy{
+public:
+ explicit CDoubleCache(CCacheStrategy *impl);
+ ~CDoubleCache() override;
+
+ int Open() override;
+ void Close() override;
+
+ size_t GetMaxWriteSize(const size_t& iRequestSize) override;
+ int WriteToCache(const char *pBuffer, size_t iSize) override;
+ int ReadFromCache(char *pBuffer, size_t iMaxSize) override;
+ int64_t WaitForData(uint32_t iMinAvail, std::chrono::milliseconds timeout) override;
+
+ int64_t Seek(int64_t iFilePosition) override;
+ bool Reset(int64_t iSourcePosition) override;
+ void EndOfInput() override;
+ bool IsEndOfInput() override;
+ void ClearEndOfInput() override;
+
+ int64_t CachedDataEndPosIfSeekTo(int64_t iFilePosition) override;
+ int64_t CachedDataStartPos() override;
+ int64_t CachedDataEndPos() override;
+ bool IsCachedPosition(int64_t iFilePosition) override;
+
+ CCacheStrategy *CreateNew() override;
+
+protected:
+ CCacheStrategy *m_pCache;
+ CCacheStrategy *m_pCacheOld;
+};
+
+}
+
diff --git a/xbmc/filesystem/CircularCache.cpp b/xbmc/filesystem/CircularCache.cpp
new file mode 100644
index 0000000..443736a
--- /dev/null
+++ b/xbmc/filesystem/CircularCache.cpp
@@ -0,0 +1,281 @@
+/*
+ * 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 "CircularCache.h"
+
+#include "threads/SystemClock.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+#include <string.h>
+
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+CCircularCache::CCircularCache(size_t front, size_t back)
+ : CCacheStrategy()
+ , m_beg(0)
+ , m_end(0)
+ , m_cur(0)
+ , m_buf(NULL)
+ , m_size(front + back)
+ , m_size_back(back)
+#ifdef TARGET_WINDOWS
+ , m_handle(NULL)
+#endif
+{
+}
+
+CCircularCache::~CCircularCache()
+{
+ Close();
+}
+
+int CCircularCache::Open()
+{
+#ifdef TARGET_WINDOWS
+ m_handle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, m_size, NULL);
+ if(m_handle == NULL)
+ return CACHE_RC_ERROR;
+ m_buf = (uint8_t*)MapViewOfFile(m_handle, FILE_MAP_ALL_ACCESS, 0, 0, 0);
+#else
+ m_buf = new uint8_t[m_size];
+#endif
+ if (m_buf == NULL)
+ return CACHE_RC_ERROR;
+ m_beg = 0;
+ m_end = 0;
+ m_cur = 0;
+ return CACHE_RC_OK;
+}
+
+void CCircularCache::Close()
+{
+#ifdef TARGET_WINDOWS
+ if (m_buf != NULL)
+ UnmapViewOfFile(m_buf);
+ if (m_handle != NULL)
+ CloseHandle(m_handle);
+ m_handle = NULL;
+#else
+ delete[] m_buf;
+#endif
+ m_buf = NULL;
+}
+
+size_t CCircularCache::GetMaxWriteSize(const size_t& iRequestSize)
+{
+ std::unique_lock<CCriticalSection> lock(m_sync);
+
+ size_t back = (size_t)(m_cur - m_beg); // Backbuffer size
+ size_t front = (size_t)(m_end - m_cur); // Frontbuffer size
+ size_t limit = m_size - std::min(back, m_size_back) - front;
+
+ // Never return more than limit and size requested by caller
+ return std::min(iRequestSize, limit);
+}
+
+/**
+ * Function will write to m_buf at m_end % m_size location
+ * it will write at maximum m_size, but it will only write
+ * as much it can without wrapping around in the buffer
+ *
+ * It will always leave m_size_back of the backbuffer intact
+ * but if the back buffer is less than that, that space is
+ * usable to write.
+ *
+ * If back buffer is filled to an larger extent than
+ * m_size_back, it will allow it to be overwritten
+ * until only m_size_back data remains.
+ *
+ * The following always apply:
+ * * m_end <= m_cur <= m_end
+ * * m_end - m_beg <= m_size
+ *
+ * Multiple calls may be needed to fill buffer completely.
+ */
+int CCircularCache::WriteToCache(const char *buf, size_t len)
+{
+ std::unique_lock<CCriticalSection> lock(m_sync);
+
+ // where are we in the buffer
+ size_t pos = m_end % m_size;
+ size_t back = (size_t)(m_cur - m_beg);
+ size_t front = (size_t)(m_end - m_cur);
+
+ size_t limit = m_size - std::min(back, m_size_back) - front;
+ size_t wrap = m_size - pos;
+
+ // limit by max forward size
+ if(len > limit)
+ len = limit;
+
+ // limit to wrap point
+ if(len > wrap)
+ len = wrap;
+
+ if(len == 0)
+ return 0;
+
+ if (m_buf == NULL)
+ return 0;
+
+ // write the data
+ memcpy(m_buf + pos, buf, len);
+ m_end += len;
+
+ // drop history that was overwritten
+ if(m_end - m_beg > (int64_t)m_size)
+ m_beg = m_end - m_size;
+
+ m_written.Set();
+
+ return len;
+}
+
+/**
+ * Reads data from cache. Will only read up till
+ * the buffer wrap point. So multiple calls
+ * may be needed to empty the whole cache
+ */
+int CCircularCache::ReadFromCache(char *buf, size_t len)
+{
+ std::unique_lock<CCriticalSection> lock(m_sync);
+
+ size_t pos = m_cur % m_size;
+ size_t front = (size_t)(m_end - m_cur);
+ size_t avail = std::min(m_size - pos, front);
+
+ if(avail == 0)
+ {
+ if(IsEndOfInput())
+ return 0;
+ else
+ return CACHE_RC_WOULD_BLOCK;
+ }
+
+ if(len > avail)
+ len = avail;
+
+ if(len == 0)
+ return 0;
+
+ if (m_buf == NULL)
+ return 0;
+
+ memcpy(buf, m_buf + pos, len);
+ m_cur += len;
+
+ m_space.Set();
+
+ return len;
+}
+
+/* Wait "millis" milliseconds for "minimum" amount of data to come in.
+ * Note that caller needs to make sure there's sufficient space in the forward
+ * buffer for "minimum" bytes else we may block the full timeout time
+ */
+int64_t CCircularCache::WaitForData(uint32_t minimum, std::chrono::milliseconds timeout)
+{
+ std::unique_lock<CCriticalSection> lock(m_sync);
+ int64_t avail = m_end - m_cur;
+
+ if (timeout == 0ms || IsEndOfInput())
+ return avail;
+
+ if(minimum > m_size - m_size_back)
+ minimum = m_size - m_size_back;
+
+ XbmcThreads::EndTime<> endtime{timeout};
+ while (!IsEndOfInput() && avail < minimum && !endtime.IsTimePast() )
+ {
+ lock.unlock();
+ m_written.Wait(50ms); // may miss the deadline. shouldn't be a problem.
+ lock.lock();
+ avail = m_end - m_cur;
+ }
+
+ return avail;
+}
+
+int64_t CCircularCache::Seek(int64_t pos)
+{
+ std::unique_lock<CCriticalSection> lock(m_sync);
+
+ // if seek is a bit over what we have, try to wait a few seconds for the data to be available.
+ // we try to avoid a (heavy) seek on the source
+ if (pos >= m_end && pos < m_end + 100000)
+ {
+ /* Make everything in the cache (back & forward) back-cache, to make sure
+ * there's sufficient forward space. Increasing it with only 100000 may not be
+ * sufficient due to variable filesystem chunksize
+ */
+ m_cur = m_end;
+
+ lock.unlock();
+ WaitForData((size_t)(pos - m_cur), 5s);
+ lock.lock();
+
+ if (pos < m_beg || pos > m_end)
+ CLog::Log(LOGDEBUG,
+ "CCircularCache::{} - ({}) Wait for data failed for pos {}, ended up at {}",
+ __FUNCTION__, fmt::ptr(this), pos, m_cur);
+ }
+
+ if (pos >= m_beg && pos <= m_end)
+ {
+ m_cur = pos;
+ return pos;
+ }
+
+ return CACHE_RC_ERROR;
+}
+
+bool CCircularCache::Reset(int64_t pos)
+{
+ std::unique_lock<CCriticalSection> lock(m_sync);
+ if (IsCachedPosition(pos))
+ {
+ m_cur = pos;
+ return false;
+ }
+ m_end = pos;
+ m_beg = pos;
+ m_cur = pos;
+
+ return true;
+}
+
+int64_t CCircularCache::CachedDataEndPosIfSeekTo(int64_t iFilePosition)
+{
+ if (IsCachedPosition(iFilePosition))
+ return m_end;
+ return iFilePosition;
+}
+
+int64_t CCircularCache::CachedDataStartPos()
+{
+ return m_beg;
+}
+
+int64_t CCircularCache::CachedDataEndPos()
+{
+ return m_end;
+}
+
+bool CCircularCache::IsCachedPosition(int64_t iFilePosition)
+{
+ return iFilePosition >= m_beg && iFilePosition <= m_end;
+}
+
+CCacheStrategy *CCircularCache::CreateNew()
+{
+ return new CCircularCache(m_size - m_size_back, m_size_back);
+}
+
diff --git a/xbmc/filesystem/CircularCache.h b/xbmc/filesystem/CircularCache.h
new file mode 100644
index 0000000..21d3e6b
--- /dev/null
+++ b/xbmc/filesystem/CircularCache.h
@@ -0,0 +1,55 @@
+/*
+ * 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 "CacheStrategy.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+
+namespace XFILE {
+
+class CCircularCache : public CCacheStrategy
+{
+public:
+ CCircularCache(size_t front, size_t back);
+ ~CCircularCache() override;
+
+ int Open() override;
+ void Close() override;
+
+ size_t GetMaxWriteSize(const size_t& iRequestSize) override;
+ int WriteToCache(const char *buf, size_t len) override;
+ int ReadFromCache(char *buf, size_t len) override;
+ int64_t WaitForData(uint32_t minimum, std::chrono::milliseconds timeout) override;
+
+ int64_t Seek(int64_t pos) override;
+ bool Reset(int64_t pos) override;
+
+ int64_t CachedDataEndPosIfSeekTo(int64_t iFilePosition) override;
+ int64_t CachedDataStartPos() override;
+ int64_t CachedDataEndPos() override;
+ bool IsCachedPosition(int64_t iFilePosition) override;
+
+ CCacheStrategy *CreateNew() override;
+protected:
+ int64_t m_beg; /**< index in file (not buffer) of beginning of valid data */
+ int64_t m_end; /**< index in file (not buffer) of end of valid data */
+ int64_t m_cur; /**< current reading index in file */
+ uint8_t *m_buf; /**< buffer holding data */
+ size_t m_size; /**< size of data buffer used (m_buf) */
+ size_t m_size_back; /**< guaranteed size of back buffer (actual size can be smaller, or larger if front buffer doesn't need it) */
+ CCriticalSection m_sync;
+ CEvent m_written;
+#ifdef TARGET_WINDOWS
+ HANDLE m_handle;
+#endif
+};
+
+} // namespace XFILE
+
diff --git a/xbmc/filesystem/CurlFile.cpp b/xbmc/filesystem/CurlFile.cpp
new file mode 100644
index 0000000..11a5272
--- /dev/null
+++ b/xbmc/filesystem/CurlFile.cpp
@@ -0,0 +1,2141 @@
+/*
+ * 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 "CurlFile.h"
+
+#include "File.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "filesystem/SpecialProtocol.h"
+#include "network/DNSNameCache.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SystemClock.h"
+#include "utils/Base64.h"
+#include "utils/XTimeUtils.h"
+
+#include <algorithm>
+#include <cassert>
+#include <climits>
+#include <vector>
+
+#ifdef TARGET_POSIX
+#include <errno.h>
+#include <inttypes.h>
+#include "platform/posix/ConvUtils.h"
+#endif
+
+#include "DllLibCurl.h"
+#include "ShoutcastFile.h"
+#include "utils/CharsetConverter.h"
+#include "utils/log.h"
+#include "utils/StringUtils.h"
+
+using namespace XFILE;
+using namespace XCURL;
+
+using namespace std::chrono_literals;
+
+#define FITS_INT(a) (((a) <= INT_MAX) && ((a) >= INT_MIN))
+
+curl_proxytype proxyType2CUrlProxyType[] = {
+ CURLPROXY_HTTP, CURLPROXY_SOCKS4, CURLPROXY_SOCKS4A,
+ CURLPROXY_SOCKS5, CURLPROXY_SOCKS5_HOSTNAME, CURLPROXY_HTTPS,
+};
+
+#define FILLBUFFER_OK 0
+#define FILLBUFFER_NO_DATA 1
+#define FILLBUFFER_FAIL 2
+
+// curl calls this routine to debug
+extern "C" int debug_callback(CURL_HANDLE *handle, curl_infotype info, char *output, size_t size, void *data)
+{
+ if (info == CURLINFO_DATA_IN || info == CURLINFO_DATA_OUT)
+ return 0;
+
+ if (!CServiceBroker::GetLogging().CanLogComponent(LOGCURL))
+ return 0;
+
+ std::string strLine;
+ strLine.append(output, size);
+ std::vector<std::string> vecLines;
+ StringUtils::Tokenize(strLine, vecLines, "\r\n");
+ std::vector<std::string>::const_iterator it = vecLines.begin();
+
+ const char *infotype;
+ switch(info)
+ {
+ case CURLINFO_TEXT : infotype = "TEXT: "; break;
+ case CURLINFO_HEADER_IN : infotype = "HEADER_IN: "; break;
+ case CURLINFO_HEADER_OUT : infotype = "HEADER_OUT: "; break;
+ case CURLINFO_SSL_DATA_IN : infotype = "SSL_DATA_IN: "; break;
+ case CURLINFO_SSL_DATA_OUT : infotype = "SSL_DATA_OUT: "; break;
+ case CURLINFO_END : infotype = "END: "; break;
+ default : infotype = ""; break;
+ }
+
+ while (it != vecLines.end())
+ {
+ CLog::Log(LOGDEBUG, "Curl::Debug - {}{}", infotype, (*it));
+ ++it;
+ }
+ return 0;
+}
+
+/* curl calls this routine to get more data */
+extern "C" size_t write_callback(char *buffer,
+ size_t size,
+ size_t nitems,
+ void *userp)
+{
+ if(userp == NULL) return 0;
+
+ CCurlFile::CReadState *state = (CCurlFile::CReadState *)userp;
+ return state->WriteCallback(buffer, size, nitems);
+}
+
+extern "C" size_t read_callback(char *buffer,
+ size_t size,
+ size_t nitems,
+ void *userp)
+{
+ if(userp == NULL) return 0;
+
+ CCurlFile::CReadState *state = (CCurlFile::CReadState *)userp;
+ return state->ReadCallback(buffer, size, nitems);
+}
+
+extern "C" size_t header_callback(void *ptr, size_t size, size_t nmemb, void *stream)
+{
+ CCurlFile::CReadState *state = (CCurlFile::CReadState *)stream;
+ return state->HeaderCallback(ptr, size, nmemb);
+}
+
+/* used only by CCurlFile::Stat to bail out of unwanted transfers */
+extern "C" int transfer_abort_callback(void *clientp,
+ curl_off_t dltotal,
+ curl_off_t dlnow,
+ curl_off_t ultotal,
+ curl_off_t ulnow)
+{
+ if(dlnow > 0)
+ return 1;
+ else
+ return 0;
+}
+
+/* fix for silly behavior of realloc */
+static inline void* realloc_simple(void *ptr, size_t size)
+{
+ void *ptr2 = realloc(ptr, size);
+ if(ptr && !ptr2 && size > 0)
+ {
+ free(ptr);
+ return NULL;
+ }
+ else
+ return ptr2;
+}
+
+static constexpr int CURL_OFF = 0L;
+static constexpr int CURL_ON = 1L;
+
+size_t CCurlFile::CReadState::HeaderCallback(void *ptr, size_t size, size_t nmemb)
+{
+ std::string inString;
+ // libcurl doc says that this info is not always \0 terminated
+ const char* strBuf = (const char*)ptr;
+ const size_t iSize = size * nmemb;
+ if (strBuf[iSize - 1] == 0)
+ inString.assign(strBuf, iSize - 1); // skip last char if it's zero
+ else
+ inString.append(strBuf, iSize);
+
+ m_httpheader.Parse(inString);
+
+ return iSize;
+}
+
+size_t CCurlFile::CReadState::ReadCallback(char *buffer, size_t size, size_t nitems)
+{
+ if (m_fileSize == 0)
+ return 0;
+
+ if (m_filePos >= m_fileSize)
+ {
+ m_isPaused = true;
+ return CURL_READFUNC_PAUSE;
+ }
+
+ int64_t retSize = std::min(m_fileSize - m_filePos, int64_t(nitems * size));
+ memcpy(buffer, m_readBuffer + m_filePos, retSize);
+ m_filePos += retSize;
+
+ return retSize;
+}
+
+size_t CCurlFile::CReadState::WriteCallback(char *buffer, size_t size, size_t nitems)
+{
+ unsigned int amount = size * nitems;
+ if (m_overflowSize)
+ {
+ // we have our overflow buffer - first get rid of as much as we can
+ unsigned int maxWriteable = std::min(m_buffer.getMaxWriteSize(), m_overflowSize);
+ if (maxWriteable)
+ {
+ if (!m_buffer.WriteData(m_overflowBuffer, maxWriteable))
+ {
+ CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Unable to write to buffer",
+ __FUNCTION__, fmt::ptr(this));
+ return 0;
+ }
+
+ if (maxWriteable < m_overflowSize)
+ {
+ // still have some more - copy it down
+ memmove(m_overflowBuffer, m_overflowBuffer + maxWriteable, m_overflowSize - maxWriteable);
+ }
+ m_overflowSize -= maxWriteable;
+
+ // Shrink memory:
+ m_overflowBuffer = (char*)realloc_simple(m_overflowBuffer, m_overflowSize);
+ }
+ }
+ // ok, now copy the data into our ring buffer
+ unsigned int maxWriteable = std::min(m_buffer.getMaxWriteSize(), amount);
+ if (maxWriteable)
+ {
+ if (!m_buffer.WriteData(buffer, maxWriteable))
+ {
+ CLog::Log(LOGERROR,
+ "CCurlFile::CReadState::{} - ({}) Unable to write to buffer with {} bytes",
+ __FUNCTION__, fmt::ptr(this), maxWriteable);
+ return 0;
+ }
+ else
+ {
+ amount -= maxWriteable;
+ buffer += maxWriteable;
+ }
+ }
+ if (amount)
+ {
+ //! @todo Limit max. amount of the overflowbuffer
+ m_overflowBuffer = (char*)realloc_simple(m_overflowBuffer, amount + m_overflowSize);
+ if(m_overflowBuffer == NULL)
+ {
+ CLog::Log(LOGWARNING,
+ "CCurlFile::CReadState::{} - ({}) Failed to grow overflow buffer from {} bytes to "
+ "{} bytes",
+ __FUNCTION__, fmt::ptr(this), m_overflowSize, amount + m_overflowSize);
+ return 0;
+ }
+ memcpy(m_overflowBuffer + m_overflowSize, buffer, amount);
+ m_overflowSize += amount;
+ }
+ return size * nitems;
+}
+
+CCurlFile::CReadState::CReadState()
+{
+ m_easyHandle = NULL;
+ m_multiHandle = NULL;
+ m_overflowBuffer = NULL;
+ m_overflowSize = 0;
+ m_stillRunning = 0;
+ m_filePos = 0;
+ m_fileSize = 0;
+ m_bufferSize = 0;
+ m_cancelled = false;
+ m_bFirstLoop = true;
+ m_sendRange = true;
+ m_bLastError = false;
+ m_readBuffer = 0;
+ m_isPaused = false;
+ m_bRetry = true;
+ m_curlHeaderList = NULL;
+ m_curlAliasList = NULL;
+}
+
+CCurlFile::CReadState::~CReadState()
+{
+ Disconnect();
+
+ if(m_easyHandle)
+ g_curlInterface.easy_release(&m_easyHandle, &m_multiHandle);
+}
+
+bool CCurlFile::CReadState::Seek(int64_t pos)
+{
+ if(pos == m_filePos)
+ return true;
+
+ if(FITS_INT(pos - m_filePos) && m_buffer.SkipBytes((int)(pos - m_filePos)))
+ {
+ m_filePos = pos;
+ return true;
+ }
+
+ if(pos > m_filePos && pos < m_filePos + m_bufferSize)
+ {
+ int len = m_buffer.getMaxReadSize();
+ m_filePos += len;
+ m_buffer.SkipBytes(len);
+ if (FillBuffer(m_bufferSize) != FILLBUFFER_OK)
+ {
+ if(!m_buffer.SkipBytes(-len))
+ CLog::Log(LOGERROR,
+ "CCurlFile::CReadState::{} - ({}) Failed to restore position after failed fill",
+ __FUNCTION__, fmt::ptr(this));
+ else
+ m_filePos -= len;
+ return false;
+ }
+
+ if(!FITS_INT(pos - m_filePos) || !m_buffer.SkipBytes((int)(pos - m_filePos)))
+ {
+ CLog::Log(
+ LOGERROR,
+ "CCurlFile::CReadState::{} - ({}) Failed to skip to position after having filled buffer",
+ __FUNCTION__, fmt::ptr(this));
+ if(!m_buffer.SkipBytes(-len))
+ CLog::Log(LOGERROR,
+ "CCurlFile::CReadState::{} - ({}) Failed to restore position after failed seek",
+ __FUNCTION__, fmt::ptr(this));
+ else
+ m_filePos -= len;
+ return false;
+ }
+ m_filePos = pos;
+ return true;
+ }
+ return false;
+}
+
+void CCurlFile::CReadState::SetResume(void)
+{
+ /*
+ * Explicitly set RANGE header when filepos=0 as some http servers require us to always send the range
+ * request header. If we don't the server may provide different content causing seeking to fail.
+ * This only affects HTTP-like items, for FTP it's a null operation.
+ */
+ if (m_sendRange && m_filePos == 0)
+ g_curlInterface.easy_setopt(m_easyHandle, CURLOPT_RANGE, "0-");
+ else
+ {
+ g_curlInterface.easy_setopt(m_easyHandle, CURLOPT_RANGE, NULL);
+ m_sendRange = false;
+ }
+
+ g_curlInterface.easy_setopt(m_easyHandle, CURLOPT_RESUME_FROM_LARGE, m_filePos);
+}
+
+long CCurlFile::CReadState::Connect(unsigned int size)
+{
+ if (m_filePos != 0)
+ CLog::Log(LOGDEBUG, "CurlFile::CReadState::{} - ({}) Resume from position {}", __FUNCTION__,
+ fmt::ptr(this), m_filePos);
+
+ SetResume();
+ g_curlInterface.multi_add_handle(m_multiHandle, m_easyHandle);
+
+ m_bufferSize = size;
+ m_buffer.Destroy();
+ m_buffer.Create(size * 3);
+ m_httpheader.Clear();
+
+ // read some data in to try and obtain the length
+ // maybe there's a better way to get this info??
+ m_stillRunning = 1;
+
+ // (Try to) fill buffer
+ if (FillBuffer(1) != FILLBUFFER_OK)
+ {
+ // Check response code
+ long response;
+ if (CURLE_OK == g_curlInterface.easy_getinfo(m_easyHandle, CURLINFO_RESPONSE_CODE, &response))
+ return response;
+ else
+ return -1;
+ }
+
+ double length;
+ if (CURLE_OK == g_curlInterface.easy_getinfo(m_easyHandle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &length))
+ {
+ if (length < 0)
+ length = 0.0;
+ m_fileSize = m_filePos + (int64_t)length;
+ }
+
+ long response;
+ if (CURLE_OK == g_curlInterface.easy_getinfo(m_easyHandle, CURLINFO_RESPONSE_CODE, &response))
+ return response;
+
+ return -1;
+}
+
+void CCurlFile::CReadState::Disconnect()
+{
+ if(m_multiHandle && m_easyHandle)
+ g_curlInterface.multi_remove_handle(m_multiHandle, m_easyHandle);
+
+ m_buffer.Clear();
+ free(m_overflowBuffer);
+ m_overflowBuffer = NULL;
+ m_overflowSize = 0;
+ m_filePos = 0;
+ m_fileSize = 0;
+ m_bufferSize = 0;
+ m_readBuffer = 0;
+
+ /* cleanup */
+ if( m_curlHeaderList )
+ g_curlInterface.slist_free_all(m_curlHeaderList);
+ m_curlHeaderList = NULL;
+
+ if( m_curlAliasList )
+ g_curlInterface.slist_free_all(m_curlAliasList);
+ m_curlAliasList = NULL;
+}
+
+
+CCurlFile::~CCurlFile()
+{
+ Close();
+ delete m_state;
+ delete m_oldState;
+}
+
+CCurlFile::CCurlFile()
+ : m_overflowBuffer(NULL)
+{
+ m_opened = false;
+ m_forWrite = false;
+ m_inError = false;
+ m_multisession = true;
+ m_seekable = true;
+ m_connecttimeout = 0;
+ m_redirectlimit = 5;
+ m_lowspeedtime = 0;
+ m_ftppasvip = false;
+ m_bufferSize = 32768;
+ m_postdataset = false;
+ m_state = new CReadState();
+ m_oldState = NULL;
+ m_skipshout = false;
+ m_httpresponse = -1;
+ m_acceptCharset = "UTF-8,*;q=0.8"; /* prefer UTF-8 if available */
+ m_allowRetry = true;
+ m_acceptencoding = "all"; /* Accept all supported encoding by default */
+}
+
+//Has to be called before Open()
+void CCurlFile::SetBufferSize(unsigned int size)
+{
+ m_bufferSize = size;
+}
+
+void CCurlFile::Close()
+{
+ if (m_opened && m_forWrite && !m_inError)
+ Write(NULL, 0);
+
+ m_state->Disconnect();
+ delete m_oldState;
+ m_oldState = NULL;
+
+ m_url.clear();
+ m_referer.clear();
+ m_cookie.clear();
+
+ m_opened = false;
+ m_forWrite = false;
+ m_inError = false;
+
+ if (m_dnsCacheList)
+ g_curlInterface.slist_free_all(m_dnsCacheList);
+ m_dnsCacheList = nullptr;
+}
+
+void CCurlFile::SetCommonOptions(CReadState* state, bool failOnError /* = true */)
+{
+ CURL_HANDLE* h = state->m_easyHandle;
+
+ g_curlInterface.easy_reset(h);
+
+ g_curlInterface.easy_setopt(h, CURLOPT_DEBUGFUNCTION, debug_callback);
+
+ if( CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel >= LOG_LEVEL_DEBUG )
+ g_curlInterface.easy_setopt(h, CURLOPT_VERBOSE, CURL_ON);
+ else
+ g_curlInterface.easy_setopt(h, CURLOPT_VERBOSE, CURL_OFF);
+
+ g_curlInterface.easy_setopt(h, CURLOPT_WRITEDATA, state);
+ g_curlInterface.easy_setopt(h, CURLOPT_WRITEFUNCTION, write_callback);
+
+ g_curlInterface.easy_setopt(h, CURLOPT_READDATA, state);
+ g_curlInterface.easy_setopt(h, CURLOPT_READFUNCTION, read_callback);
+
+ // use DNS cache
+ g_curlInterface.easy_setopt(h, CURLOPT_RESOLVE, m_dnsCacheList);
+
+ // make sure headers are separated from the data stream
+ g_curlInterface.easy_setopt(h, CURLOPT_WRITEHEADER, state);
+ g_curlInterface.easy_setopt(h, CURLOPT_HEADERFUNCTION, header_callback);
+ g_curlInterface.easy_setopt(h, CURLOPT_HEADER, CURL_OFF);
+
+ g_curlInterface.easy_setopt(h, CURLOPT_FTP_USE_EPSV, 0); // turn off epsv
+
+ // Allow us to follow redirects
+ g_curlInterface.easy_setopt(h, CURLOPT_FOLLOWLOCATION, m_redirectlimit != 0);
+ g_curlInterface.easy_setopt(h, CURLOPT_MAXREDIRS, m_redirectlimit);
+
+ // Enable cookie engine for current handle
+ g_curlInterface.easy_setopt(h, CURLOPT_COOKIEFILE, "");
+
+ // Set custom cookie if requested
+ if (!m_cookie.empty())
+ g_curlInterface.easy_setopt(h, CURLOPT_COOKIE, m_cookie.c_str());
+
+ g_curlInterface.easy_setopt(h, CURLOPT_COOKIELIST, "FLUSH");
+
+ // When using multiple threads you should set the CURLOPT_NOSIGNAL option to
+ // TRUE for all handles. Everything will work fine except that timeouts are not
+ // honored during the DNS lookup - which you can work around by building libcurl
+ // with c-ares support. c-ares is a library that provides asynchronous name
+ // resolves. Unfortunately, c-ares does not yet support IPv6.
+ g_curlInterface.easy_setopt(h, CURLOPT_NOSIGNAL, CURL_ON);
+
+ if (failOnError)
+ {
+ // not interested in failed requests
+ g_curlInterface.easy_setopt(h, CURLOPT_FAILONERROR, 1);
+ }
+
+ // enable support for icecast / shoutcast streams
+ if ( NULL == state->m_curlAliasList )
+ // m_curlAliasList is used only by this one place, but SetCommonOptions can
+ // be called multiple times, only append to list if it's empty.
+ state->m_curlAliasList = g_curlInterface.slist_append(state->m_curlAliasList, "ICY 200 OK");
+ g_curlInterface.easy_setopt(h, CURLOPT_HTTP200ALIASES, state->m_curlAliasList);
+
+ if (!m_verifyPeer)
+ g_curlInterface.easy_setopt(h, CURLOPT_SSL_VERIFYPEER, 0);
+
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_URL, m_url.c_str());
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TRANSFERTEXT, CURL_OFF);
+
+ // setup POST data if it is set (and it may be empty)
+ if (m_postdataset)
+ {
+ g_curlInterface.easy_setopt(h, CURLOPT_POST, 1 );
+ g_curlInterface.easy_setopt(h, CURLOPT_POSTFIELDSIZE, m_postdata.length());
+ g_curlInterface.easy_setopt(h, CURLOPT_POSTFIELDS, m_postdata.c_str());
+ }
+
+ // setup Referer header if needed
+ if (!m_referer.empty())
+ g_curlInterface.easy_setopt(h, CURLOPT_REFERER, m_referer.c_str());
+ else
+ {
+ g_curlInterface.easy_setopt(h, CURLOPT_REFERER, NULL);
+ // Do not send referer header on redirects (same behaviour as ffmpeg and browsers)
+ g_curlInterface.easy_setopt(h, CURLOPT_AUTOREFERER, CURL_OFF);
+ }
+
+ // setup any requested authentication
+ if( !m_ftpauth.empty() )
+ {
+ g_curlInterface.easy_setopt(h, CURLOPT_FTP_SSL, CURLFTPSSL_TRY);
+ if( m_ftpauth == "any" )
+ g_curlInterface.easy_setopt(h, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_DEFAULT);
+ else if( m_ftpauth == "ssl" )
+ g_curlInterface.easy_setopt(h, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_SSL);
+ else if( m_ftpauth == "tls" )
+ g_curlInterface.easy_setopt(h, CURLOPT_FTPSSLAUTH, CURLFTPAUTH_TLS);
+ }
+
+ // setup requested http authentication method
+ bool bAuthSet = false;
+ if(!m_httpauth.empty())
+ {
+ bAuthSet = true;
+ if( m_httpauth == "any" )
+ g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
+ else if( m_httpauth == "anysafe" )
+ g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_ANYSAFE);
+ else if( m_httpauth == "digest" )
+ g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
+ else if( m_httpauth == "ntlm" )
+ g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_NTLM);
+ else
+ bAuthSet = false;
+ }
+
+ // set username and password for current handle
+ if (!m_username.empty())
+ {
+ g_curlInterface.easy_setopt(h, CURLOPT_USERNAME, m_username.c_str());
+ if (!m_password.empty())
+ g_curlInterface.easy_setopt(h, CURLOPT_PASSWORD, m_password.c_str());
+
+ if (!bAuthSet)
+ g_curlInterface.easy_setopt(h, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
+ }
+
+ // allow passive mode for ftp
+ if( m_ftpport.length() > 0 )
+ g_curlInterface.easy_setopt(h, CURLOPT_FTPPORT, m_ftpport.c_str());
+ else
+ g_curlInterface.easy_setopt(h, CURLOPT_FTPPORT, NULL);
+
+ // allow curl to not use the ip address in the returned pasv response
+ if( m_ftppasvip )
+ g_curlInterface.easy_setopt(h, CURLOPT_FTP_SKIP_PASV_IP, 0);
+ else
+ g_curlInterface.easy_setopt(h, CURLOPT_FTP_SKIP_PASV_IP, 1);
+
+ // setup Accept-Encoding if requested
+ if (m_acceptencoding.length() > 0)
+ g_curlInterface.easy_setopt(h, CURLOPT_ACCEPT_ENCODING, m_acceptencoding == "all" ? "" : m_acceptencoding.c_str());
+
+ if (!m_acceptCharset.empty())
+ SetRequestHeader("Accept-Charset", m_acceptCharset);
+
+ if (m_userAgent.length() > 0)
+ g_curlInterface.easy_setopt(h, CURLOPT_USERAGENT, m_userAgent.c_str());
+ else /* set some default agent as shoutcast doesn't return proper stuff otherwise */
+ g_curlInterface.easy_setopt(h, CURLOPT_USERAGENT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_userAgent.c_str());
+
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlDisableIPV6)
+ g_curlInterface.easy_setopt(h, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+
+ if (!m_proxyhost.empty())
+ {
+ g_curlInterface.easy_setopt(h, CURLOPT_PROXYTYPE, proxyType2CUrlProxyType[m_proxytype]);
+
+ const std::string hostport = m_proxyhost + StringUtils::Format(":{}", m_proxyport);
+ g_curlInterface.easy_setopt(h, CURLOPT_PROXY, hostport.c_str());
+
+ std::string userpass;
+
+ if (!m_proxyuser.empty() && !m_proxypassword.empty())
+ userpass = CURL::Encode(m_proxyuser) + ":" + CURL::Encode(m_proxypassword);
+
+ if (!userpass.empty())
+ g_curlInterface.easy_setopt(h, CURLOPT_PROXYUSERPWD, userpass.c_str());
+ }
+ if (m_customrequest.length() > 0)
+ g_curlInterface.easy_setopt(h, CURLOPT_CUSTOMREQUEST, m_customrequest.c_str());
+
+ if (m_connecttimeout == 0)
+ m_connecttimeout = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout;
+
+ // set our timeouts, we abort connection after m_timeout, and reads after no data for m_timeout seconds
+ g_curlInterface.easy_setopt(h, CURLOPT_CONNECTTIMEOUT, m_connecttimeout);
+
+ // We abort in case we transfer less than 1byte/second
+ g_curlInterface.easy_setopt(h, CURLOPT_LOW_SPEED_LIMIT, 1);
+
+ if (m_lowspeedtime == 0)
+ m_lowspeedtime = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curllowspeedtime;
+
+ // Set the lowspeed time very low as it seems Curl takes much longer to detect a lowspeed condition
+ g_curlInterface.easy_setopt(h, CURLOPT_LOW_SPEED_TIME, m_lowspeedtime);
+
+ // enable tcp keepalive
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlKeepAliveInterval > 0)
+ {
+ g_curlInterface.easy_setopt(h, CURLOPT_TCP_KEEPALIVE, 1L);
+ g_curlInterface.easy_setopt(
+ h, CURLOPT_TCP_KEEPIDLE,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlKeepAliveInterval / 2);
+ g_curlInterface.easy_setopt(
+ h, CURLOPT_TCP_KEEPINTVL,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlKeepAliveInterval);
+ }
+
+ // Setup allowed TLS/SSL ciphers. New versions of cURL may deprecate things that are still in use.
+ if (!m_cipherlist.empty())
+ g_curlInterface.easy_setopt(h, CURLOPT_SSL_CIPHER_LIST, m_cipherlist.c_str());
+
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlDisableHTTP2)
+ g_curlInterface.easy_setopt(h, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+ else
+ // enable HTTP2 support. default: CURL_HTTP_VERSION_1_1. Curl >= 7.62.0 defaults to CURL_HTTP_VERSION_2TLS
+ g_curlInterface.easy_setopt(h, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
+
+ // set CA bundle file
+ std::string caCert = CSpecialProtocol::TranslatePath(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_caTrustFile);
+#ifdef TARGET_WINDOWS_STORE
+ // UWP Curl - Setting CURLOPT_CAINFO with a valid cacert file path is required for UWP
+ g_curlInterface.easy_setopt(h, CURLOPT_CAINFO, "system\\certs\\cacert.pem");
+#endif
+ if (!caCert.empty() && XFILE::CFile::Exists(caCert))
+ g_curlInterface.easy_setopt(h, CURLOPT_CAINFO, caCert.c_str());
+}
+
+void CCurlFile::SetRequestHeaders(CReadState* state)
+{
+ if(state->m_curlHeaderList)
+ {
+ g_curlInterface.slist_free_all(state->m_curlHeaderList);
+ state->m_curlHeaderList = NULL;
+ }
+
+ for (const auto& it : m_requestheaders)
+ {
+ std::string buffer = it.first + ": " + it.second;
+ state->m_curlHeaderList = g_curlInterface.slist_append(state->m_curlHeaderList, buffer.c_str());
+ }
+
+ // add user defined headers
+ if (state->m_easyHandle)
+ g_curlInterface.easy_setopt(state->m_easyHandle, CURLOPT_HTTPHEADER, state->m_curlHeaderList);
+}
+
+void CCurlFile::SetCorrectHeaders(CReadState* state)
+{
+ CHttpHeader& h = state->m_httpheader;
+ /* workaround for shoutcast server which doesn't set content type on standard mp3 */
+ if( h.GetMimeType().empty() )
+ {
+ if( !h.GetValue("icy-notice1").empty()
+ || !h.GetValue("icy-name").empty()
+ || !h.GetValue("icy-br").empty() )
+ h.AddParam("Content-Type", "audio/mpeg");
+ }
+
+ /* hack for google video */
+ if (StringUtils::EqualsNoCase(h.GetMimeType(),"text/html")
+ && !h.GetValue("Content-Disposition").empty() )
+ {
+ std::string strValue = h.GetValue("Content-Disposition");
+ if (strValue.find("filename=") != std::string::npos &&
+ strValue.find(".flv") != std::string::npos)
+ h.AddParam("Content-Type", "video/flv");
+ }
+}
+
+void CCurlFile::ParseAndCorrectUrl(CURL &url2)
+{
+ std::string strProtocol = url2.GetTranslatedProtocol();
+ url2.SetProtocol(strProtocol);
+
+ // lookup host in DNS cache
+ std::string resolvedHost;
+ if (CDNSNameCache::GetCached(url2.GetHostName(), resolvedHost))
+ {
+ struct curl_slist* tempCache;
+ int entryPort = url2.GetPort();
+
+ if (entryPort == 0)
+ {
+ if (strProtocol == "http")
+ entryPort = 80;
+ else if (strProtocol == "https")
+ entryPort = 443;
+ else if (strProtocol == "ftp")
+ entryPort = 21;
+ else if (strProtocol == "ftps")
+ entryPort = 990;
+ }
+
+ std::string entryString =
+ url2.GetHostName() + ":" + std::to_string(entryPort) + ":" + resolvedHost;
+ tempCache = g_curlInterface.slist_append(m_dnsCacheList, entryString.c_str());
+
+ if (tempCache)
+ m_dnsCacheList = tempCache;
+ }
+
+ if( url2.IsProtocol("ftp")
+ || url2.IsProtocol("ftps") )
+ {
+ // we was using url options for urls, keep the old code work and warning
+ if (!url2.GetOptions().empty())
+ {
+ CLog::Log(LOGWARNING,
+ "CCurlFile::{} - <{}> FTP url option is deprecated, please switch to use protocol "
+ "option (change "
+ "'?' to '|')",
+ __FUNCTION__, url2.GetRedacted());
+ url2.SetProtocolOptions(url2.GetOptions().substr(1));
+ /* ftp has no options */
+ url2.SetOptions("");
+ }
+
+ /* this is ugly, depending on where we get */
+ /* the link from, it may or may not be */
+ /* url encoded. if handed from ftpdirectory */
+ /* it won't be so let's handle that case */
+
+ std::string filename(url2.GetFileName());
+ std::vector<std::string> array;
+
+ // if server sent us the filename in non-utf8, we need send back with same encoding.
+ if (url2.GetProtocolOption("utf8") == "0")
+ g_charsetConverter.utf8ToStringCharset(filename);
+
+ //! @todo create a tokenizer that doesn't skip empty's
+ StringUtils::Tokenize(filename, array, "/");
+ filename.clear();
+ for (std::vector<std::string>::iterator it = array.begin(); it != array.end(); ++it)
+ {
+ if(it != array.begin())
+ filename += "/";
+
+ filename += CURL::Encode(*it);
+ }
+
+ /* make sure we keep slashes */
+ if(StringUtils::EndsWith(url2.GetFileName(), "/"))
+ filename += "/";
+
+ url2.SetFileName(filename);
+
+ m_ftpauth.clear();
+ if (url2.HasProtocolOption("auth"))
+ {
+ m_ftpauth = url2.GetProtocolOption("auth");
+ StringUtils::ToLower(m_ftpauth);
+ if(m_ftpauth.empty())
+ m_ftpauth = "any";
+ }
+ m_ftpport = "";
+ if (url2.HasProtocolOption("active"))
+ {
+ m_ftpport = url2.GetProtocolOption("active");
+ if(m_ftpport.empty())
+ m_ftpport = "-";
+ }
+ if (url2.HasProtocolOption("verifypeer"))
+ {
+ if (url2.GetProtocolOption("verifypeer") == "false")
+ m_verifyPeer = false;
+ }
+ m_ftppasvip = url2.HasProtocolOption("pasvip") && url2.GetProtocolOption("pasvip") != "0";
+ }
+ else if(url2.IsProtocol("http") ||
+ url2.IsProtocol("https"))
+ {
+ std::shared_ptr<CSettings> s = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (!s)
+ return;
+
+ if (!url2.IsLocalHost() &&
+ m_proxyhost.empty() &&
+ s->GetBool(CSettings::SETTING_NETWORK_USEHTTPPROXY) &&
+ !s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYSERVER).empty() &&
+ s->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYPORT) > 0)
+ {
+ m_proxytype = (ProxyType)s->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYTYPE);
+ m_proxyhost = s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYSERVER);
+ m_proxyport = s->GetInt(CSettings::SETTING_NETWORK_HTTPPROXYPORT);
+ m_proxyuser = s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYUSERNAME);
+ m_proxypassword = s->GetString(CSettings::SETTING_NETWORK_HTTPPROXYPASSWORD);
+ CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Using proxy {}, type {}", url2.GetRedacted(),
+ m_proxyhost, proxyType2CUrlProxyType[m_proxytype]);
+ }
+
+ // get username and password
+ m_username = url2.GetUserName();
+ m_password = url2.GetPassWord();
+
+ // handle any protocol options
+ std::map<std::string, std::string> options;
+ url2.GetProtocolOptions(options);
+ if (!options.empty())
+ {
+ // set xbmc headers
+ for (const auto& it : options)
+ {
+ std::string name = it.first;
+ StringUtils::ToLower(name);
+ const std::string& value = it.second;
+
+ if (name == "auth")
+ {
+ m_httpauth = value;
+ StringUtils::ToLower(m_httpauth);
+ if(m_httpauth.empty())
+ m_httpauth = "any";
+ }
+ else if (name == "referer")
+ SetReferer(value);
+ else if (name == "user-agent")
+ SetUserAgent(value);
+ else if (name == "cookie")
+ SetCookie(value);
+ else if (name == "acceptencoding" || name == "encoding")
+ SetAcceptEncoding(value);
+ else if (name == "noshout" && value == "true")
+ m_skipshout = true;
+ else if (name == "seekable" && value == "0")
+ m_seekable = false;
+ else if (name == "accept-charset")
+ SetAcceptCharset(value);
+ else if (name == "sslcipherlist")
+ m_cipherlist = value;
+ else if (name == "connection-timeout")
+ m_connecttimeout = strtol(value.c_str(), NULL, 10);
+ else if (name == "failonerror")
+ m_failOnError = value == "true";
+ else if (name == "redirect-limit")
+ m_redirectlimit = strtol(value.c_str(), NULL, 10);
+ else if (name == "postdata")
+ {
+ m_postdata = Base64::Decode(value);
+ m_postdataset = true;
+ }
+ else if (name == "active-remote")// needed for DACP!
+ {
+ SetRequestHeader(it.first, value);
+ }
+ else if (name == "customrequest")
+ {
+ SetCustomRequest(value);
+ }
+ else if (name == "verifypeer")
+ {
+ if (value == "false")
+ m_verifyPeer = false;
+ }
+ else
+ {
+ if (name.length() > 0 && name[0] == '!')
+ {
+ SetRequestHeader(it.first.substr(1), value);
+ CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Adding custom header option '{}: ***********'",
+ url2.GetRedacted(), it.first.substr(1));
+ }
+ else
+ {
+ SetRequestHeader(it.first, value);
+ if (name == "authorization")
+ CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Adding custom header option '{}: ***********'",
+ url2.GetRedacted(), it.first);
+ else
+ CLog::LogFC(LOGDEBUG, LOGCURL, "<{}> Adding custom header option '{}: {}'",
+ url2.GetRedacted(), it.first, value);
+ }
+ }
+ }
+ }
+ }
+
+ // Unset the protocol options to have an url without protocol options
+ url2.SetProtocolOptions("");
+
+ if (m_username.length() > 0 && m_password.length() > 0)
+ m_url = url2.GetWithoutUserDetails();
+ else
+ m_url = url2.Get();
+}
+
+bool CCurlFile::Post(const std::string& strURL, const std::string& strPostData, std::string& strHTML)
+{
+ m_postdata = strPostData;
+ m_postdataset = true;
+ return Service(strURL, strHTML);
+}
+
+bool CCurlFile::Get(const std::string& strURL, std::string& strHTML)
+{
+ m_postdata = "";
+ m_postdataset = false;
+ return Service(strURL, strHTML);
+}
+
+bool CCurlFile::Service(const std::string& strURL, std::string& strHTML)
+{
+ const CURL pathToUrl(strURL);
+ if (Open(pathToUrl))
+ {
+ if (ReadData(strHTML))
+ {
+ Close();
+ return true;
+ }
+ }
+ Close();
+ return false;
+}
+
+bool CCurlFile::ReadData(std::string& strHTML)
+{
+ int size_read = 0;
+ strHTML = "";
+ char buffer[16384];
+ while( (size_read = Read(buffer, sizeof(buffer)-1) ) > 0 )
+ {
+ buffer[size_read] = 0;
+ strHTML.append(buffer, size_read);
+ }
+ if (m_state->m_cancelled)
+ return false;
+ return true;
+}
+
+bool CCurlFile::Download(const std::string& strURL, const std::string& strFileName, unsigned int* pdwSize)
+{
+ CLog::Log(LOGINFO, "CCurlFile::{} - {}->{}", __FUNCTION__, CURL::GetRedacted(strURL),
+ strFileName);
+
+ std::string strData;
+ if (!Get(strURL, strData))
+ return false;
+
+ XFILE::CFile file;
+ if (!file.OpenForWrite(strFileName, true))
+ {
+ CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Unable to open file for write: {} ({})", __FUNCTION__,
+ CURL::GetRedacted(strURL), strFileName, GetLastError());
+ return false;
+ }
+ ssize_t written = 0;
+ if (!strData.empty())
+ written = file.Write(strData.c_str(), strData.size());
+
+ if (pdwSize != NULL)
+ *pdwSize = written > 0 ? written : 0;
+
+ return written == static_cast<ssize_t>(strData.size());
+}
+
+// Detect whether we are "online" or not! Very simple and dirty!
+bool CCurlFile::IsInternet()
+{
+ CURL url("http://www.msftconnecttest.com/connecttest.txt");
+ bool found = Exists(url);
+ if (!found)
+ {
+ // fallback
+ Close();
+ url.Parse("http://www.w3.org/");
+ found = Exists(url);
+ }
+ Close();
+
+ return found;
+}
+
+void CCurlFile::Cancel()
+{
+ m_state->m_cancelled = true;
+ while (m_opened)
+ KODI::TIME::Sleep(1ms);
+}
+
+void CCurlFile::Reset()
+{
+ m_state->m_cancelled = false;
+}
+
+void CCurlFile::SetProxy(const std::string &type, const std::string &host,
+ uint16_t port, const std::string &user, const std::string &password)
+{
+ m_proxytype = CCurlFile::PROXY_HTTP;
+ if (type == "http")
+ m_proxytype = CCurlFile::PROXY_HTTP;
+ else if (type == "https")
+ m_proxytype = CCurlFile::PROXY_HTTPS;
+ else if (type == "socks4")
+ m_proxytype = CCurlFile::PROXY_SOCKS4;
+ else if (type == "socks4a")
+ m_proxytype = CCurlFile::PROXY_SOCKS4A;
+ else if (type == "socks5")
+ m_proxytype = CCurlFile::PROXY_SOCKS5;
+ else if (type == "socks5-remote")
+ m_proxytype = CCurlFile::PROXY_SOCKS5_REMOTE;
+ else
+ CLog::Log(LOGERROR, "CCurFile::{} - <{}> Invalid proxy type \"{}\"", __FUNCTION__,
+ CURL::GetRedacted(m_url), type);
+ m_proxyhost = host;
+ m_proxyport = port;
+ m_proxyuser = user;
+ m_proxypassword = password;
+}
+
+bool CCurlFile::Open(const CURL& url)
+{
+ m_opened = true;
+ m_seekable = true;
+
+ CURL url2(url);
+ ParseAndCorrectUrl(url2);
+
+ std::string redactPath = CURL::GetRedacted(m_url);
+ CLog::Log(LOGDEBUG, "CurlFile::{} - <{}>", __FUNCTION__, redactPath);
+
+ assert(!(!m_state->m_easyHandle ^ !m_state->m_multiHandle));
+ if( m_state->m_easyHandle == NULL )
+ g_curlInterface.easy_acquire(url2.GetProtocol().c_str(),
+ url2.GetHostName().c_str(),
+ &m_state->m_easyHandle,
+ &m_state->m_multiHandle);
+
+ // setup common curl options
+ SetCommonOptions(m_state,
+ m_failOnError && !CServiceBroker::GetLogging().CanLogComponent(LOGCURL));
+ SetRequestHeaders(m_state);
+ m_state->m_sendRange = m_seekable;
+ m_state->m_bRetry = m_allowRetry;
+
+ m_httpresponse = m_state->Connect(m_bufferSize);
+
+ if (m_httpresponse <= 0 || (m_failOnError && m_httpresponse >= 400))
+ {
+ std::string error;
+ if (m_httpresponse >= 400 && CServiceBroker::GetLogging().CanLogComponent(LOGCURL))
+ {
+ error.resize(4096);
+ ReadString(&error[0], 4095);
+ }
+
+ CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed with code {}:\n{}", __FUNCTION__,
+ redactPath, m_httpresponse, error);
+
+ return false;
+ }
+
+ SetCorrectHeaders(m_state);
+
+ // since we can't know the stream size up front if we're gzipped/deflated
+ // flag the stream with an unknown file size rather than the compressed
+ // file size.
+ if (!m_state->m_httpheader.GetValue("Content-Encoding").empty() && !StringUtils::EqualsNoCase(m_state->m_httpheader.GetValue("Content-Encoding"), "identity"))
+ m_state->m_fileSize = 0;
+
+ // check if this stream is a shoutcast stream. sometimes checking the protocol line is not enough so examine other headers as well.
+ // shoutcast streams should be handled by FileShoutcast.
+ if ((m_state->m_httpheader.GetProtoLine().substr(0, 3) == "ICY" || !m_state->m_httpheader.GetValue("icy-notice1").empty()
+ || !m_state->m_httpheader.GetValue("icy-name").empty()
+ || !m_state->m_httpheader.GetValue("icy-br").empty()) && !m_skipshout)
+ {
+ CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> File is a shoutcast stream. Re-opening", __FUNCTION__,
+ redactPath);
+ throw new CRedirectException(new CShoutcastFile);
+ }
+
+ m_multisession = false;
+ if(url2.IsProtocol("http") || url2.IsProtocol("https"))
+ {
+ m_multisession = true;
+ if(m_state->m_httpheader.GetValue("Server").find("Portable SDK for UPnP devices") != std::string::npos)
+ {
+ CLog::Log(LOGWARNING,
+ "CCurlFile::{} - <{}> Disabling multi session due to broken libupnp server",
+ __FUNCTION__, redactPath);
+ m_multisession = false;
+ }
+ }
+
+ if(StringUtils::EqualsNoCase(m_state->m_httpheader.GetValue("Transfer-Encoding"), "chunked"))
+ m_state->m_fileSize = 0;
+
+ if(m_state->m_fileSize <= 0)
+ m_seekable = false;
+ if (m_seekable)
+ {
+ if(url2.IsProtocol("http")
+ || url2.IsProtocol("https"))
+ {
+ // if server says explicitly it can't seek, respect that
+ if(StringUtils::EqualsNoCase(m_state->m_httpheader.GetValue("Accept-Ranges"),"none"))
+ m_seekable = false;
+ }
+ }
+
+ std::string efurl = GetInfoString(CURLINFO_EFFECTIVE_URL);
+ if (!efurl.empty())
+ {
+ if (m_url != efurl)
+ {
+ std::string redactEfpath = CURL::GetRedacted(efurl);
+ CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> Effective URL is {}", __FUNCTION__, redactPath,
+ redactEfpath);
+ }
+ m_url = efurl;
+ }
+
+ return true;
+}
+
+bool CCurlFile::OpenForWrite(const CURL& url, bool bOverWrite)
+{
+ if(m_opened)
+ return false;
+
+ if (Exists(url) && !bOverWrite)
+ return false;
+
+ CURL url2(url);
+ ParseAndCorrectUrl(url2);
+
+ CLog::Log(LOGDEBUG, "CCurlFile::{} - Opening {}", __FUNCTION__, CURL::GetRedacted(m_url));
+
+ assert(m_state->m_easyHandle == NULL);
+ g_curlInterface.easy_acquire(url2.GetProtocol().c_str(),
+ url2.GetHostName().c_str(),
+ &m_state->m_easyHandle,
+ &m_state->m_multiHandle);
+
+ // setup common curl options
+ SetCommonOptions(m_state);
+ SetRequestHeaders(m_state);
+
+ std::string efurl = GetInfoString(CURLINFO_EFFECTIVE_URL);
+ if (!efurl.empty())
+ m_url = efurl;
+
+ m_opened = true;
+ m_forWrite = true;
+ m_inError = false;
+ m_writeOffset = 0;
+
+ assert(m_state->m_multiHandle);
+
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_UPLOAD, 1);
+
+ g_curlInterface.multi_add_handle(m_state->m_multiHandle, m_state->m_easyHandle);
+
+ m_state->SetReadBuffer(NULL, 0);
+
+ return true;
+}
+
+ssize_t CCurlFile::Write(const void* lpBuf, size_t uiBufSize)
+{
+ if (!(m_opened && m_forWrite) || m_inError)
+ return -1;
+
+ assert(m_state->m_multiHandle);
+
+ m_state->SetReadBuffer(lpBuf, uiBufSize);
+ m_state->m_isPaused = false;
+ g_curlInterface.easy_pause(m_state->m_easyHandle, CURLPAUSE_CONT);
+
+ CURLMcode result = CURLM_OK;
+
+ m_stillRunning = 1;
+ while (m_stillRunning && !m_state->m_isPaused)
+ {
+ while ((result = g_curlInterface.multi_perform(m_state->m_multiHandle, &m_stillRunning)) == CURLM_CALL_MULTI_PERFORM);
+
+ if (!m_stillRunning)
+ break;
+
+ if (result != CURLM_OK)
+ {
+ long code;
+ if(g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK )
+ CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Unable to write curl resource with code {}",
+ __FUNCTION__, CURL::GetRedacted(m_url), code);
+ m_inError = true;
+ return -1;
+ }
+ }
+
+ m_writeOffset += m_state->m_filePos;
+ return m_state->m_filePos;
+}
+
+bool CCurlFile::CReadState::ReadString(char *szLine, int iLineLength)
+{
+ unsigned int want = (unsigned int)iLineLength;
+
+ if((m_fileSize == 0 || m_filePos < m_fileSize) && FillBuffer(want) != FILLBUFFER_OK)
+ return false;
+
+ // ensure only available data is considered
+ want = std::min(m_buffer.getMaxReadSize(), want);
+
+ /* check if we finished prematurely */
+ if (!m_stillRunning && (m_fileSize == 0 || m_filePos != m_fileSize) && !want)
+ {
+ if (m_fileSize != 0)
+ CLog::Log(
+ LOGWARNING,
+ "CCurlFile::{} - ({}) Transfer ended before entire file was retrieved pos {}, size {}",
+ __FUNCTION__, fmt::ptr(this), m_filePos, m_fileSize);
+
+ return false;
+ }
+
+ char* pLine = szLine;
+ do
+ {
+ if (!m_buffer.ReadData(pLine, 1))
+ break;
+
+ pLine++;
+ } while (((pLine - 1)[0] != '\n') && ((unsigned int)(pLine - szLine) < want));
+ pLine[0] = 0;
+ m_filePos += (pLine - szLine);
+ return (pLine - szLine) > 0;
+}
+
+bool CCurlFile::ReOpen(const CURL& url)
+{
+ Close();
+ return Open(url);
+}
+
+bool CCurlFile::Exists(const CURL& url)
+{
+ // if file is already running, get info from it
+ if( m_opened )
+ {
+ CLog::Log(LOGWARNING, "CCurlFile::{} - <{}> Exist called on open file", __FUNCTION__,
+ url.GetRedacted());
+ return true;
+ }
+
+ CURL url2(url);
+ ParseAndCorrectUrl(url2);
+
+ assert(m_state->m_easyHandle == NULL);
+ g_curlInterface.easy_acquire(url2.GetProtocol().c_str(),
+ url2.GetHostName().c_str(),
+ &m_state->m_easyHandle, NULL);
+
+ SetCommonOptions(m_state);
+ SetRequestHeaders(m_state);
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TIMEOUT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout);
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOBODY, 1);
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_WRITEDATA, NULL); /* will cause write failure*/
+
+ if(url2.IsProtocol("ftp") || url2.IsProtocol("ftps"))
+ {
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME, 1);
+ // nocwd is less standard, will return empty list for non-existed remote dir on some ftp server, avoid it.
+ if (StringUtils::EndsWith(url2.GetFileName(), "/"))
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD);
+ else
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_NOCWD);
+ }
+
+ CURLcode result = g_curlInterface.easy_perform(m_state->m_easyHandle);
+
+ if (result == CURLE_WRITE_ERROR || result == CURLE_OK)
+ {
+ g_curlInterface.easy_release(&m_state->m_easyHandle, NULL);
+ return true;
+ }
+
+ if (result == CURLE_HTTP_RETURNED_ERROR)
+ {
+ long code;
+ if(g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK && code != 404 )
+ {
+ if (code == 405)
+ {
+ // If we get a Method Not Allowed response, retry with a GET Request
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOBODY, 0);
+
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME, 1);
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_XFERINFOFUNCTION, transfer_abort_callback);
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOPROGRESS, 0);
+
+ curl_slist *list = NULL;
+ list = g_curlInterface.slist_append(list, "Range: bytes=0-1"); /* try to only request 1 byte */
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_HTTPHEADER, list);
+
+ CURLcode result = g_curlInterface.easy_perform(m_state->m_easyHandle);
+ g_curlInterface.slist_free_all(list);
+
+ if (result == CURLE_WRITE_ERROR || result == CURLE_OK)
+ {
+ g_curlInterface.easy_release(&m_state->m_easyHandle, NULL);
+ return true;
+ }
+
+ if (result == CURLE_HTTP_RETURNED_ERROR)
+ {
+ if (g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK && code != 404 )
+ CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: HTTP returned error {}", __FUNCTION__,
+ url.GetRedacted(), code);
+ }
+ }
+ else
+ CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: HTTP returned error {}", __FUNCTION__,
+ url.GetRedacted(), code);
+ }
+ }
+ else if (result != CURLE_REMOTE_FILE_NOT_FOUND && result != CURLE_FTP_COULDNT_RETR_FILE)
+ {
+ CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: {}({})", __FUNCTION__, url.GetRedacted(),
+ g_curlInterface.easy_strerror(result), result);
+ }
+
+ errno = ENOENT;
+ g_curlInterface.easy_release(&m_state->m_easyHandle, NULL);
+ return false;
+}
+
+int64_t CCurlFile::Seek(int64_t iFilePosition, int iWhence)
+{
+ int64_t nextPos = m_state->m_filePos;
+
+ if(!m_seekable)
+ return -1;
+
+ switch(iWhence)
+ {
+ case SEEK_SET:
+ nextPos = iFilePosition;
+ break;
+ case SEEK_CUR:
+ nextPos += iFilePosition;
+ break;
+ case SEEK_END:
+ if (m_state->m_fileSize)
+ nextPos = m_state->m_fileSize + iFilePosition;
+ else
+ return -1;
+ break;
+ default:
+ return -1;
+ }
+
+ // We can't seek beyond EOF
+ if (m_state->m_fileSize && nextPos > m_state->m_fileSize) return -1;
+
+ if(m_state->Seek(nextPos))
+ return nextPos;
+
+ if (m_multisession)
+ {
+ if (!m_oldState)
+ {
+ CURL url(m_url);
+ m_oldState = m_state;
+ m_state = new CReadState();
+ m_state->m_fileSize = m_oldState->m_fileSize;
+ g_curlInterface.easy_acquire(url.GetProtocol().c_str(),
+ url.GetHostName().c_str(),
+ &m_state->m_easyHandle,
+ &m_state->m_multiHandle );
+ }
+ else
+ {
+ CReadState *tmp;
+ tmp = m_state;
+ m_state = m_oldState;
+ m_oldState = tmp;
+
+ if (m_state->Seek(nextPos))
+ return nextPos;
+
+ m_state->Disconnect();
+ }
+ }
+ else
+ m_state->Disconnect();
+
+ // re-setup common curl options
+ SetCommonOptions(m_state);
+
+ /* caller might have changed some headers (needed for daap)*/
+ //! @todo daap is gone. is this needed for something else?
+ SetRequestHeaders(m_state);
+
+ m_state->m_filePos = nextPos;
+ m_state->m_sendRange = true;
+ m_state->m_bRetry = m_allowRetry;
+
+ long response = m_state->Connect(m_bufferSize);
+ if(response < 0 && (m_state->m_fileSize == 0 || m_state->m_fileSize != m_state->m_filePos))
+ {
+ if(m_multisession)
+ {
+ if (m_oldState)
+ {
+ delete m_state;
+ m_state = m_oldState;
+ m_oldState = NULL;
+ }
+ // Retry without multisession
+ m_multisession = false;
+ return Seek(iFilePosition, iWhence);
+ }
+ else
+ {
+ m_seekable = false;
+ return -1;
+ }
+ }
+
+ SetCorrectHeaders(m_state);
+
+ return m_state->m_filePos;
+}
+
+int64_t CCurlFile::GetLength()
+{
+ if (!m_opened) return 0;
+ return m_state->m_fileSize;
+}
+
+int64_t CCurlFile::GetPosition()
+{
+ if (!m_opened) return 0;
+ return m_state->m_filePos;
+}
+
+int CCurlFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ // if file is already running, get info from it
+ if( m_opened )
+ {
+ CLog::Log(LOGWARNING, "CCurlFile::{} - <{}> Stat called on open file", __FUNCTION__,
+ url.GetRedacted());
+ if (buffer)
+ {
+ *buffer = {};
+ buffer->st_size = GetLength();
+ buffer->st_mode = _S_IFREG;
+ }
+ return 0;
+ }
+
+ CURL url2(url);
+ ParseAndCorrectUrl(url2);
+
+ assert(m_state->m_easyHandle == NULL);
+ g_curlInterface.easy_acquire(url2.GetProtocol().c_str(),
+ url2.GetHostName().c_str(),
+ &m_state->m_easyHandle, NULL);
+
+ SetCommonOptions(m_state);
+ SetRequestHeaders(m_state);
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TIMEOUT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout);
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOBODY, 1);
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME , 1);
+
+ if(url2.IsProtocol("ftp"))
+ {
+ // nocwd is less standard, will return empty list for non-existed remote dir on some ftp server, avoid it.
+ if (StringUtils::EndsWith(url2.GetFileName(), "/"))
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_SINGLECWD);
+ else
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_NOCWD);
+ }
+
+ CURLcode result = g_curlInterface.easy_perform(m_state->m_easyHandle);
+
+ if(result == CURLE_HTTP_RETURNED_ERROR)
+ {
+ long code;
+ if(g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_RESPONSE_CODE, &code) == CURLE_OK && code == 404 )
+ {
+ g_curlInterface.easy_release(&m_state->m_easyHandle, NULL);
+ errno = ENOENT;
+ return -1;
+ }
+ }
+
+ if(result == CURLE_GOT_NOTHING
+ || result == CURLE_HTTP_RETURNED_ERROR
+ || result == CURLE_RECV_ERROR /* some silly shoutcast servers */ )
+ {
+ /* some http servers and shoutcast servers don't give us any data on a head request */
+ /* request normal and just bail out via progress meter callback after we received data */
+ /* somehow curl doesn't reset CURLOPT_NOBODY properly so reset everything */
+ SetCommonOptions(m_state);
+ SetRequestHeaders(m_state);
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_TIMEOUT, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlconnecttimeout);
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_FILETIME, 1);
+#if LIBCURL_VERSION_NUM >= 0x072000 // 0.7.32
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_XFERINFOFUNCTION, transfer_abort_callback);
+#else
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_PROGRESSFUNCTION, transfer_abort_callback);
+#endif
+ g_curlInterface.easy_setopt(m_state->m_easyHandle, CURLOPT_NOPROGRESS, 0);
+
+ result = g_curlInterface.easy_perform(m_state->m_easyHandle);
+
+ }
+
+ if( result != CURLE_ABORTED_BY_CALLBACK && result != CURLE_OK )
+ {
+ g_curlInterface.easy_release(&m_state->m_easyHandle, NULL);
+ errno = ENOENT;
+ CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Failed: {}({})", __FUNCTION__, url.GetRedacted(),
+ g_curlInterface.easy_strerror(result), result);
+ return -1;
+ }
+
+ double length;
+ result = g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &length);
+ if (result != CURLE_OK || length < 0.0)
+ {
+ if (url.IsProtocol("ftp"))
+ {
+ g_curlInterface.easy_release(&m_state->m_easyHandle, NULL);
+ CLog::Log(LOGINFO, "CCurlFile::{} - <{}> Content length failed: {}({})", __FUNCTION__,
+ url.GetRedacted(), g_curlInterface.easy_strerror(result), result);
+ errno = ENOENT;
+ return -1;
+ }
+ else
+ length = 0.0;
+ }
+
+ SetCorrectHeaders(m_state);
+
+ if (buffer)
+ {
+ *buffer = {};
+ buffer->st_size = static_cast<int64_t>(length);
+
+ // Note: CURLINFO_CONTENT_TYPE returns the last received content-type response header value.
+ // In case there is authentication required there might be multiple requests involved and if
+ // the last request which actually returns the data does not return a content-type header, but
+ // one of the preceding requests, CURLINFO_CONTENT_TYPE returns not the content type of the
+ // actual resource requested! m_state contains only the values of the last request, which is
+ // what we want here.
+ const std::string mimeType = m_state->m_httpheader.GetMimeType();
+ if (mimeType.find("text/html") != std::string::npos) // consider html files directories
+ buffer->st_mode = _S_IFDIR;
+ else
+ buffer->st_mode = _S_IFREG;
+
+ long filetime;
+ result = g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_FILETIME, &filetime);
+ if (result != CURLE_OK)
+ {
+ CLog::Log(LOGINFO, "CCurlFile::{} - <{}> Filetime failed: {}({})", __FUNCTION__,
+ url.GetRedacted(), g_curlInterface.easy_strerror(result), result);
+ }
+ else
+ {
+ if (filetime != -1)
+ buffer->st_mtime = filetime;
+ }
+ }
+ g_curlInterface.easy_release(&m_state->m_easyHandle, NULL);
+ return 0;
+}
+
+ssize_t CCurlFile::CReadState::Read(void* lpBuf, size_t uiBufSize)
+{
+ /* only request 1 byte, for truncated reads (only if not eof) */
+ if (m_fileSize == 0 || m_filePos < m_fileSize)
+ {
+ int8_t result = FillBuffer(1);
+ if (result == FILLBUFFER_FAIL)
+ return -1; // Fatal error
+
+ if (result == FILLBUFFER_NO_DATA)
+ return 0;
+ }
+
+ /* ensure only available data is considered */
+ unsigned int want = std::min<unsigned int>(m_buffer.getMaxReadSize(), uiBufSize);
+
+ /* xfer data to caller */
+ if (m_buffer.ReadData((char *)lpBuf, want))
+ {
+ m_filePos += want;
+ return want;
+ }
+
+ /* check if we finished prematurely */
+ if (!m_stillRunning && (m_fileSize == 0 || m_filePos != m_fileSize))
+ {
+ CLog::Log(LOGWARNING,
+ "CCurlFile::CReadState::{} - ({}) Transfer ended before entire file was retrieved "
+ "pos {}, size {}",
+ __FUNCTION__, fmt::ptr(this), m_filePos, m_fileSize);
+ return -1;
+ }
+
+ return 0;
+}
+
+/* use to attempt to fill the read buffer up to requested number of bytes */
+int8_t CCurlFile::CReadState::FillBuffer(unsigned int want)
+{
+ int retry = 0;
+ fd_set fdread;
+ fd_set fdwrite;
+ fd_set fdexcep;
+
+ // only attempt to fill buffer if transactions still running and buffer
+ // doesn't exceed required size already
+ while (m_buffer.getMaxReadSize() < want && m_buffer.getMaxWriteSize() > 0 )
+ {
+ if (m_cancelled)
+ return FILLBUFFER_NO_DATA;
+
+ /* if there is data in overflow buffer, try to use that first */
+ if (m_overflowSize)
+ {
+ unsigned amount = std::min(m_buffer.getMaxWriteSize(), m_overflowSize);
+ m_buffer.WriteData(m_overflowBuffer, amount);
+
+ if (amount < m_overflowSize)
+ memmove(m_overflowBuffer, m_overflowBuffer + amount, m_overflowSize - amount);
+
+ m_overflowSize -= amount;
+ // Shrink memory:
+ m_overflowBuffer = (char*)realloc_simple(m_overflowBuffer, m_overflowSize);
+ continue;
+ }
+
+ CURLMcode result = g_curlInterface.multi_perform(m_multiHandle, &m_stillRunning);
+ if (!m_stillRunning)
+ {
+ if (result == CURLM_OK)
+ {
+ /* if we still have stuff in buffer, we are fine */
+ if (m_buffer.getMaxReadSize())
+ return FILLBUFFER_OK;
+
+ // check for errors
+ int msgs;
+ CURLMsg* msg;
+ bool bRetryNow = true;
+ bool bError = false;
+ while ((msg = g_curlInterface.multi_info_read(m_multiHandle, &msgs)))
+ {
+ if (msg->msg == CURLMSG_DONE)
+ {
+ if (msg->data.result == CURLE_OK)
+ return FILLBUFFER_OK;
+
+ long httpCode = 0;
+ if (msg->data.result == CURLE_HTTP_RETURNED_ERROR)
+ {
+ g_curlInterface.easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &httpCode);
+
+ // Don't log 404 not-found errors to prevent log-spam
+ if (httpCode != 404)
+ CLog::Log(LOGERROR,
+ "CCurlFile::CReadState::{} - ({}) Failed: HTTP returned code {}",
+ __FUNCTION__, fmt::ptr(this), httpCode);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Failed: {}({})", __FUNCTION__,
+ fmt::ptr(this), g_curlInterface.easy_strerror(msg->data.result),
+ msg->data.result);
+ }
+
+ if ( (msg->data.result == CURLE_OPERATION_TIMEDOUT ||
+ msg->data.result == CURLE_PARTIAL_FILE ||
+ msg->data.result == CURLE_COULDNT_CONNECT ||
+ msg->data.result == CURLE_RECV_ERROR) &&
+ !m_bFirstLoop)
+ {
+ bRetryNow = false; // Leave it to caller whether the operation is retried
+ bError = true;
+ }
+ else if ( (msg->data.result == CURLE_HTTP_RANGE_ERROR ||
+ httpCode == 416 /* = Requested Range Not Satisfiable */ ||
+ httpCode == 406 /* = Not Acceptable (fixes issues with non compliant HDHomerun servers */) &&
+ m_bFirstLoop &&
+ m_filePos == 0 &&
+ m_sendRange)
+ {
+ // If server returns a (possible) range error, disable range and retry (handled below)
+ bRetryNow = true;
+ bError = true;
+ m_sendRange = false;
+ }
+ else
+ {
+ // For all other errors, abort the operation
+ return FILLBUFFER_FAIL;
+ }
+ }
+ }
+
+ // Check for an actual error, if not, just return no-data
+ if (!bError && !m_bLastError)
+ return FILLBUFFER_NO_DATA;
+
+ // Close handle
+ if (m_multiHandle && m_easyHandle)
+ g_curlInterface.multi_remove_handle(m_multiHandle, m_easyHandle);
+
+ // Reset all the stuff like we would in Disconnect()
+ m_buffer.Clear();
+ free(m_overflowBuffer);
+ m_overflowBuffer = NULL;
+ m_overflowSize = 0;
+ m_bLastError = true; // Flag error for the next run
+
+ // Retry immediately or leave it up to the caller?
+ if ((m_bRetry && retry < CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_curlretries) || (bRetryNow && retry == 0))
+ {
+ retry++;
+
+ // Connect + seek to current position (again)
+ SetResume();
+ g_curlInterface.multi_add_handle(m_multiHandle, m_easyHandle);
+
+ CLog::Log(LOGWARNING, "CCurlFile::CReadState::{} - ({}) Reconnect, (re)try {}",
+ __FUNCTION__, fmt::ptr(this), retry);
+
+ // Return to the beginning of the loop:
+ continue;
+ }
+
+ return FILLBUFFER_NO_DATA; // We failed but flag no data to caller, so it can retry the operation
+ }
+ return FILLBUFFER_FAIL;
+ }
+
+ // We've finished out first loop
+ if(m_bFirstLoop && m_buffer.getMaxReadSize() > 0)
+ m_bFirstLoop = false;
+
+ // No error this run
+ m_bLastError = false;
+
+ switch (result)
+ {
+ case CURLM_OK:
+ {
+ int maxfd = -1;
+ FD_ZERO(&fdread);
+ FD_ZERO(&fdwrite);
+ FD_ZERO(&fdexcep);
+
+ // get file descriptors from the transfers
+ g_curlInterface.multi_fdset(m_multiHandle, &fdread, &fdwrite, &fdexcep, &maxfd);
+
+ long timeout = 0;
+ if (CURLM_OK != g_curlInterface.multi_timeout(m_multiHandle, &timeout) || timeout == -1 || timeout < 200)
+ timeout = 200;
+
+ XbmcThreads::EndTime<> endTime{std::chrono::milliseconds(timeout)};
+ int rc;
+
+ do
+ {
+ /* On success the value of maxfd is guaranteed to be >= -1. We call
+ * select(maxfd + 1, ...); specially in case of (maxfd == -1) there are
+ * no fds ready yet so we call select(0, ...) --or Sleep() on Windows--
+ * to sleep 100ms, which is the minimum suggested value in the
+ * curl_multi_fdset() doc.
+ */
+ if (maxfd == -1)
+ {
+#ifdef TARGET_WINDOWS
+ /* Windows does not support using select() for sleeping without a dummy
+ * socket. Instead use Windows' Sleep() and sleep for 100ms which is the
+ * minimum suggested value in the curl_multi_fdset() doc.
+ */
+ KODI::TIME::Sleep(100ms);
+ rc = 0;
+#else
+ /* Portable sleep for platforms other than Windows. */
+ struct timeval wait = { 0, 100 * 1000 }; /* 100ms */
+ rc = select(0, NULL, NULL, NULL, &wait);
+#endif
+ }
+ else
+ {
+ unsigned int time_left = endTime.GetTimeLeft().count();
+ struct timeval wait = { (int)time_left / 1000, ((int)time_left % 1000) * 1000 };
+ rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &wait);
+ }
+#ifdef TARGET_WINDOWS
+ } while(rc == SOCKET_ERROR && WSAGetLastError() == WSAEINTR);
+#else
+ } while(rc == SOCKET_ERROR && errno == EINTR);
+#endif
+
+ if(rc == SOCKET_ERROR)
+ {
+#ifdef TARGET_WINDOWS
+ char buf[256];
+ strerror_s(buf, 256, WSAGetLastError());
+ CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Failed with socket error:{}",
+ __FUNCTION__, fmt::ptr(this), buf);
+#else
+ char const * str = strerror(errno);
+ CLog::Log(LOGERROR, "CCurlFile::CReadState::{} - ({}) Failed with socket error:{}",
+ __FUNCTION__, fmt::ptr(this), str);
+#endif
+
+ return FILLBUFFER_FAIL;
+ }
+ }
+ break;
+ case CURLM_CALL_MULTI_PERFORM:
+ {
+ // we don't keep calling here as that can easily overwrite our buffer which we want to avoid
+ // docs says we should call it soon after, but as long as we are reading data somewhere
+ // this aught to be soon enough. should stay in socket otherwise
+ continue;
+ }
+ break;
+ default:
+ {
+ CLog::Log(LOGERROR,
+ "CCurlFile::CReadState::{} - ({}) Multi perform failed with code {}, aborting",
+ __FUNCTION__, fmt::ptr(this), result);
+ return FILLBUFFER_FAIL;
+ }
+ break;
+ }
+ }
+ return FILLBUFFER_OK;
+}
+
+void CCurlFile::CReadState::SetReadBuffer(const void* lpBuf, int64_t uiBufSize)
+{
+ m_readBuffer = const_cast<char*>((const char*)lpBuf);
+ m_fileSize = uiBufSize;
+ m_filePos = 0;
+}
+
+void CCurlFile::ClearRequestHeaders()
+{
+ m_requestheaders.clear();
+}
+
+void CCurlFile::SetRequestHeader(const std::string& header, const std::string& value)
+{
+ m_requestheaders[header] = value;
+}
+
+void CCurlFile::SetRequestHeader(const std::string& header, long value)
+{
+ m_requestheaders[header] = std::to_string(value);
+}
+
+std::string CCurlFile::GetURL(void)
+{
+ return m_url;
+}
+
+std::string CCurlFile::GetRedirectURL()
+{
+ return GetInfoString(CURLINFO_REDIRECT_URL);
+}
+
+std::string CCurlFile::GetInfoString(int infoType)
+{
+ char* info{};
+ CURLcode result = g_curlInterface.easy_getinfo(m_state->m_easyHandle, static_cast<CURLINFO> (infoType), &info);
+ if (result != CURLE_OK)
+ {
+ CLog::Log(LOGERROR,
+ "CCurlFile::{} - <{}> Info string request for type {} failed with result code {}",
+ __FUNCTION__, CURL::GetRedacted(m_url), infoType, result);
+ return "";
+ }
+ return (info ? info : "");
+}
+
+/* STATIC FUNCTIONS */
+bool CCurlFile::GetHttpHeader(const CURL &url, CHttpHeader &headers)
+{
+ try
+ {
+ CCurlFile file;
+ if(file.Stat(url, NULL) == 0)
+ {
+ headers = file.GetHttpHeader();
+ return true;
+ }
+ return false;
+ }
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Exception thrown while trying to retrieve header",
+ __FUNCTION__, url.GetRedacted());
+ return false;
+ }
+}
+
+bool CCurlFile::GetMimeType(const CURL &url, std::string &content, const std::string &useragent)
+{
+ CCurlFile file;
+ if (!useragent.empty())
+ file.SetUserAgent(useragent);
+
+ struct __stat64 buffer;
+ std::string redactUrl = url.GetRedacted();
+ if( file.Stat(url, &buffer) == 0 )
+ {
+ if (buffer.st_mode == _S_IFDIR)
+ content = "x-directory/normal";
+ else
+ content = file.GetProperty(XFILE::FILE_PROPERTY_MIME_TYPE);
+ CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> {}", __FUNCTION__, redactUrl, content);
+ return true;
+ }
+ CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> failed", __FUNCTION__, redactUrl);
+ content.clear();
+ return false;
+}
+
+bool CCurlFile::GetContentType(const CURL &url, std::string &content, const std::string &useragent)
+{
+ CCurlFile file;
+ if (!useragent.empty())
+ file.SetUserAgent(useragent);
+
+ struct __stat64 buffer;
+ std::string redactUrl = url.GetRedacted();
+ if (file.Stat(url, &buffer) == 0)
+ {
+ if (buffer.st_mode == _S_IFDIR)
+ content = "x-directory/normal";
+ else
+ content = file.GetProperty(XFILE::FILE_PROPERTY_CONTENT_TYPE, "");
+ CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> {}", __FUNCTION__, redactUrl, content);
+ return true;
+ }
+ CLog::Log(LOGDEBUG, "CCurlFile::{} - <{}> -> failed", __FUNCTION__, redactUrl);
+ content.clear();
+ return false;
+}
+
+bool CCurlFile::GetCookies(const CURL &url, std::string &cookies)
+{
+ std::string cookiesStr;
+ curl_slist* curlCookies;
+ CURL_HANDLE* easyHandle;
+ CURLM* multiHandle;
+
+ // get the cookies list
+ g_curlInterface.easy_acquire(url.GetProtocol().c_str(),
+ url.GetHostName().c_str(),
+ &easyHandle, &multiHandle);
+ if (CURLE_OK == g_curlInterface.easy_getinfo(easyHandle, CURLINFO_COOKIELIST, &curlCookies))
+ {
+ // iterate over each cookie and format it into an RFC 2109 formatted Set-Cookie string
+ curl_slist* curlCookieIter = curlCookies;
+ while(curlCookieIter)
+ {
+ // tokenize the CURL cookie string
+ std::vector<std::string> valuesVec;
+ StringUtils::Tokenize(curlCookieIter->data, valuesVec, "\t");
+
+ // ensure the length is valid
+ if (valuesVec.size() < 7)
+ {
+ CLog::Log(LOGERROR, "CCurlFile::{} - <{}> Invalid cookie: '{}'", __FUNCTION__,
+ url.GetRedacted(), curlCookieIter->data);
+ curlCookieIter = curlCookieIter->next;
+ continue;
+ }
+
+ // create a http-header formatted cookie string
+ std::string cookieStr = valuesVec[5] + "=" + valuesVec[6] +
+ "; path=" + valuesVec[2] +
+ "; domain=" + valuesVec[0];
+
+ // append this cookie to the string containing all cookies
+ if (!cookiesStr.empty())
+ cookiesStr += "\n";
+ cookiesStr += cookieStr;
+
+ // move on to the next cookie
+ curlCookieIter = curlCookieIter->next;
+ }
+
+ // free the curl cookies
+ g_curlInterface.slist_free_all(curlCookies);
+
+ // release our handles
+ g_curlInterface.easy_release(&easyHandle, &multiHandle);
+
+ // if we have a non-empty cookie string, return it
+ if (!cookiesStr.empty())
+ {
+ cookies = cookiesStr;
+ return true;
+ }
+ }
+
+ // no cookies to return
+ return false;
+}
+
+int CCurlFile::IoControl(EIoControl request, void* param)
+{
+ if (request == IOCTRL_SEEK_POSSIBLE)
+ return m_seekable ? 1 : 0;
+
+ if (request == IOCTRL_SET_RETRY)
+ {
+ m_allowRetry = *(bool*) param;
+ return 0;
+ }
+
+ return -1;
+}
+
+const std::string CCurlFile::GetProperty(XFILE::FileProperty type, const std::string &name) const
+{
+ switch (type)
+ {
+ case FILE_PROPERTY_RESPONSE_PROTOCOL:
+ return m_state->m_httpheader.GetProtoLine();
+ case FILE_PROPERTY_RESPONSE_HEADER:
+ return m_state->m_httpheader.GetValue(name);
+ case FILE_PROPERTY_CONTENT_TYPE:
+ return m_state->m_httpheader.GetValue("content-type");
+ case FILE_PROPERTY_CONTENT_CHARSET:
+ return m_state->m_httpheader.GetCharset();
+ case FILE_PROPERTY_MIME_TYPE:
+ return m_state->m_httpheader.GetMimeType();
+ case FILE_PROPERTY_EFFECTIVE_URL:
+ {
+ char *url = nullptr;
+ g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_EFFECTIVE_URL, &url);
+ return url ? url : "";
+ }
+ default:
+ return "";
+ }
+}
+
+const std::vector<std::string> CCurlFile::GetPropertyValues(XFILE::FileProperty type, const std::string &name) const
+{
+ if (type == FILE_PROPERTY_RESPONSE_HEADER)
+ {
+ return m_state->m_httpheader.GetValues(name);
+ }
+ std::vector<std::string> values;
+ std::string value = GetProperty(type, name);
+ if (!value.empty())
+ {
+ values.emplace_back(value);
+ }
+ return values;
+}
+
+double CCurlFile::GetDownloadSpeed()
+{
+#if LIBCURL_VERSION_NUM >= 0x073a00 // 0.7.58.0
+ double speed = 0.0;
+ if (g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_SPEED_DOWNLOAD, &speed) == CURLE_OK)
+ return speed;
+#else
+ double time = 0.0, size = 0.0;
+ if (g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_TOTAL_TIME, &time) == CURLE_OK
+ && g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_SIZE_DOWNLOAD, &size) == CURLE_OK
+ && time > 0.0)
+ {
+ return size / time;
+ }
+#endif
+ return 0.0;
+}
diff --git a/xbmc/filesystem/CurlFile.h b/xbmc/filesystem/CurlFile.h
new file mode 100644
index 0000000..88f2923
--- /dev/null
+++ b/xbmc/filesystem/CurlFile.h
@@ -0,0 +1,202 @@
+/*
+ * 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 "IFile.h"
+#include "utils/HttpHeader.h"
+#include "utils/RingBuffer.h"
+
+#include <map>
+#include <string>
+
+typedef void CURL_HANDLE;
+typedef void CURLM;
+struct curl_slist;
+
+namespace XFILE
+{
+ class CCurlFile : public IFile
+ {
+ private:
+ typedef enum
+ {
+ PROXY_HTTP = 0,
+ PROXY_SOCKS4,
+ PROXY_SOCKS4A,
+ PROXY_SOCKS5,
+ PROXY_SOCKS5_REMOTE,
+ PROXY_HTTPS,
+ } ProxyType;
+
+ public:
+ CCurlFile();
+ ~CCurlFile() override;
+ bool Open(const CURL& url) override;
+ bool OpenForWrite(const CURL& url, bool bOverWrite = false) override;
+ bool ReOpen(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ int64_t Seek(int64_t iFilePosition, int iWhence=SEEK_SET) override;
+ int64_t GetPosition() override;
+ int64_t GetLength() override;
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+ void Close() override;
+ bool ReadString(char *szLine, int iLineLength) override { return m_state->ReadString(szLine, iLineLength); }
+ ssize_t Read(void* lpBuf, size_t uiBufSize) override { return m_state->Read(lpBuf, uiBufSize); }
+ ssize_t Write(const void* lpBuf, size_t uiBufSize) override;
+ const std::string GetProperty(XFILE::FileProperty type, const std::string &name = "") const override;
+ const std::vector<std::string> GetPropertyValues(XFILE::FileProperty type, const std::string &name = "") const override;
+ int IoControl(EIoControl request, void* param) override;
+ double GetDownloadSpeed() override;
+
+ bool Post(const std::string& strURL, const std::string& strPostData, std::string& strHTML);
+ bool Get(const std::string& strURL, std::string& strHTML);
+ bool ReadData(std::string& strHTML);
+ bool Download(const std::string& strURL, const std::string& strFileName, unsigned int* pdwSize = NULL);
+ bool IsInternet();
+ void Cancel();
+ void Reset();
+ void SetUserAgent(const std::string& sUserAgent) { m_userAgent = sUserAgent; }
+ void SetProxy(const std::string &type, const std::string &host, uint16_t port,
+ const std::string &user, const std::string &password);
+ void SetCustomRequest(const std::string &request) { m_customrequest = request; }
+ void SetAcceptEncoding(const std::string& encoding) { m_acceptencoding = encoding; }
+ void SetAcceptCharset(const std::string& charset) { m_acceptCharset = charset; }
+ void SetTimeout(int connecttimeout) { m_connecttimeout = connecttimeout; }
+ void SetLowSpeedTime(int lowspeedtime) { m_lowspeedtime = lowspeedtime; }
+ void SetPostData(const std::string& postdata) { m_postdata = postdata; }
+ void SetReferer(const std::string& referer) { m_referer = referer; }
+ void SetCookie(const std::string& cookie) { m_cookie = cookie; }
+ void SetMimeType(const std::string& mimetype) { SetRequestHeader("Content-Type", mimetype); }
+ void SetRequestHeader(const std::string& header, const std::string& value);
+ void SetRequestHeader(const std::string& header, long value);
+
+ void ClearRequestHeaders();
+ void SetBufferSize(unsigned int size);
+
+ const CHttpHeader& GetHttpHeader() const { return m_state->m_httpheader; }
+ std::string GetURL(void);
+ std::string GetRedirectURL();
+
+ /* static function that will get content type of a file */
+ static bool GetHttpHeader(const CURL &url, CHttpHeader &headers);
+ static bool GetMimeType(const CURL &url, std::string &content, const std::string &useragent="");
+ static bool GetContentType(const CURL &url, std::string &content, const std::string &useragent = "");
+
+ /* static function that will get cookies stored by CURL in RFC 2109 format */
+ static bool GetCookies(const CURL &url, std::string &cookies);
+
+ class CReadState
+ {
+ public:
+ CReadState();
+ ~CReadState();
+ CURL_HANDLE* m_easyHandle;
+ CURLM* m_multiHandle;
+
+ CRingBuffer m_buffer; // our ringhold buffer
+ unsigned int m_bufferSize;
+
+ char* m_overflowBuffer; // in the rare case we would overflow the above buffer
+ unsigned int m_overflowSize; // size of the overflow buffer
+ int m_stillRunning; // Is background url fetch still in progress
+ bool m_cancelled;
+ int64_t m_fileSize;
+ int64_t m_filePos;
+ bool m_bFirstLoop;
+ bool m_isPaused;
+ bool m_sendRange;
+ bool m_bLastError;
+ bool m_bRetry;
+
+ char* m_readBuffer;
+
+ /* returned http header */
+ CHttpHeader m_httpheader;
+ bool IsHeaderDone(void) { return m_httpheader.IsHeaderDone(); }
+
+ curl_slist* m_curlHeaderList;
+ curl_slist* m_curlAliasList;
+
+ size_t ReadCallback(char *buffer, size_t size, size_t nitems);
+ size_t WriteCallback(char *buffer, size_t size, size_t nitems);
+ size_t HeaderCallback(void *ptr, size_t size, size_t nmemb);
+
+ bool Seek(int64_t pos);
+ ssize_t Read(void* lpBuf, size_t uiBufSize);
+ bool ReadString(char *szLine, int iLineLength);
+ int8_t FillBuffer(unsigned int want);
+ void SetReadBuffer(const void* lpBuf, int64_t uiBufSize);
+
+ void SetResume(void);
+ long Connect(unsigned int size);
+ void Disconnect();
+ };
+
+ protected:
+ void ParseAndCorrectUrl(CURL &url);
+ void SetCommonOptions(CReadState* state, bool failOnError = true);
+ void SetRequestHeaders(CReadState* state);
+ void SetCorrectHeaders(CReadState* state);
+ bool Service(const std::string& strURL, std::string& strHTML);
+ std::string GetInfoString(int infoType);
+
+ protected:
+ CReadState* m_state;
+ CReadState* m_oldState;
+ unsigned int m_bufferSize;
+ int64_t m_writeOffset = 0;
+
+ std::string m_url;
+ std::string m_userAgent;
+ ProxyType m_proxytype = PROXY_HTTP;
+ std::string m_proxyhost;
+ uint16_t m_proxyport = 3128;
+ std::string m_proxyuser;
+ std::string m_proxypassword;
+ std::string m_customrequest;
+ std::string m_acceptencoding;
+ std::string m_acceptCharset;
+ std::string m_ftpauth;
+ std::string m_ftpport;
+ std::string m_binary;
+ std::string m_postdata;
+ std::string m_referer;
+ std::string m_cookie;
+ std::string m_username;
+ std::string m_password;
+ std::string m_httpauth;
+ std::string m_cipherlist;
+ bool m_ftppasvip;
+ int m_connecttimeout;
+ int m_redirectlimit;
+ int m_lowspeedtime;
+ bool m_opened;
+ bool m_forWrite;
+ bool m_inError;
+ bool m_seekable;
+ bool m_multisession;
+ bool m_skipshout;
+ bool m_postdataset;
+ bool m_allowRetry;
+ bool m_verifyPeer = true;
+ bool m_failOnError = true;
+ curl_slist* m_dnsCacheList = nullptr;
+
+ CRingBuffer m_buffer; // our ringhold buffer
+ char* m_overflowBuffer; // in the rare case we would overflow the above buffer
+ unsigned int m_overflowSize = 0; // size of the overflow buffer
+
+ int m_stillRunning; // Is background url fetch still in progress?
+
+ typedef std::map<std::string, std::string> MAPHTTPHEADERS;
+ MAPHTTPHEADERS m_requestheaders;
+
+ long m_httpresponse;
+ };
+}
diff --git a/xbmc/filesystem/DAVCommon.cpp b/xbmc/filesystem/DAVCommon.cpp
new file mode 100644
index 0000000..f200609
--- /dev/null
+++ b/xbmc/filesystem/DAVCommon.cpp
@@ -0,0 +1,73 @@
+/*
+ * 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 "DAVCommon.h"
+
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+/*
+ * Return true if pElement value is equal value without namespace.
+ *
+ * if pElement is <DAV:foo> and value is foo then ValueWithoutNamespace is true
+ */
+bool CDAVCommon::ValueWithoutNamespace(const TiXmlNode *pNode, const std::string& value)
+{
+ const TiXmlElement *pElement;
+
+ if (!pNode)
+ {
+ return false;
+ }
+
+ pElement = pNode->ToElement();
+
+ if (!pElement)
+ {
+ return false;
+ }
+
+ std::vector<std::string> tag = StringUtils::Split(pElement->ValueStr(), ":", 2);
+
+ if (tag.size() == 1 && tag[0] == value)
+ {
+ return true;
+ }
+ else if (tag.size() == 2 && tag[1] == value)
+ {
+ return true;
+ }
+ else if (tag.size() > 2)
+ {
+ CLog::Log(LOGERROR, "{} - Splitting {} failed, size(): {}, value: {}", __FUNCTION__,
+ pElement->Value(), (unsigned long int)tag.size(), value);
+ }
+
+ return false;
+}
+
+/*
+ * Search for <status> and return its content
+ */
+std::string CDAVCommon::GetStatusTag(const TiXmlElement *pElement)
+{
+ const TiXmlElement *pChild;
+
+ for (pChild = pElement->FirstChildElement(); pChild != 0; pChild = pChild->NextSiblingElement())
+ {
+ if (ValueWithoutNamespace(pChild, "status"))
+ {
+ return pChild->NoChildren() ? "" : pChild->FirstChild()->ValueStr();
+ }
+ }
+
+ return "";
+}
+
diff --git a/xbmc/filesystem/DAVCommon.h b/xbmc/filesystem/DAVCommon.h
new file mode 100644
index 0000000..483de3b
--- /dev/null
+++ b/xbmc/filesystem/DAVCommon.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "utils/XBMCTinyXML.h"
+
+namespace XFILE
+{
+ class CDAVCommon
+ {
+ public:
+ static bool ValueWithoutNamespace(const TiXmlNode *pNode, const std::string& value);
+ static std::string GetStatusTag(const TiXmlElement *pElement);
+ };
+}
diff --git a/xbmc/filesystem/DAVDirectory.cpp b/xbmc/filesystem/DAVDirectory.cpp
new file mode 100644
index 0000000..48d4865
--- /dev/null
+++ b/xbmc/filesystem/DAVDirectory.cpp
@@ -0,0 +1,230 @@
+/*
+ * 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 "DAVDirectory.h"
+
+#include "CurlFile.h"
+#include "DAVCommon.h"
+#include "DAVFile.h"
+#include "FileItem.h"
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+CDAVDirectory::CDAVDirectory(void) = default;
+CDAVDirectory::~CDAVDirectory(void) = default;
+
+/*
+ * Parses a <response>
+ *
+ * <!ELEMENT response (href, ((href*, status)|(propstat+)), responsedescription?) >
+ * <!ELEMENT propstat (prop, status, responsedescription?) >
+ *
+ */
+void CDAVDirectory::ParseResponse(const TiXmlElement *pElement, CFileItem &item)
+{
+ const TiXmlElement *pResponseChild;
+ const TiXmlNode *pPropstatChild;
+ const TiXmlElement *pPropChild;
+
+ /* Iterate response children elements */
+ for (pResponseChild = pElement->FirstChildElement(); pResponseChild != 0; pResponseChild = pResponseChild->NextSiblingElement())
+ {
+ if (CDAVCommon::ValueWithoutNamespace(pResponseChild, "href") && !pResponseChild->NoChildren())
+ {
+ std::string path(pResponseChild->FirstChild()->ValueStr());
+ URIUtils::RemoveSlashAtEnd(path);
+ item.SetPath(path);
+ }
+ else
+ if (CDAVCommon::ValueWithoutNamespace(pResponseChild, "propstat"))
+ {
+ if (CDAVCommon::GetStatusTag(pResponseChild->ToElement()).find("200 OK") != std::string::npos)
+ {
+ /* Iterate propstat children elements */
+ for (pPropstatChild = pResponseChild->FirstChild(); pPropstatChild != 0; pPropstatChild = pPropstatChild->NextSibling())
+ {
+ if (CDAVCommon::ValueWithoutNamespace(pPropstatChild, "prop"))
+ {
+ /* Iterate all properties available */
+ for (pPropChild = pPropstatChild->FirstChildElement(); pPropChild != 0; pPropChild = pPropChild->NextSiblingElement())
+ {
+ if (CDAVCommon::ValueWithoutNamespace(pPropChild, "getcontentlength") && !pPropChild->NoChildren())
+ {
+ item.m_dwSize = strtoll(pPropChild->FirstChild()->Value(), NULL, 10);
+ }
+ else
+ if (CDAVCommon::ValueWithoutNamespace(pPropChild, "getlastmodified") && !pPropChild->NoChildren())
+ {
+ struct tm timeDate = {};
+ strptime(pPropChild->FirstChild()->Value(), "%a, %d %b %Y %T", &timeDate);
+ item.m_dateTime = mktime(&timeDate);
+ }
+ else
+ if (CDAVCommon::ValueWithoutNamespace(pPropChild, "displayname") && !pPropChild->NoChildren())
+ {
+ item.SetLabel(CURL::Decode(pPropChild->FirstChild()->ValueStr()));
+ }
+ else
+ if (!item.m_dateTime.IsValid() && CDAVCommon::ValueWithoutNamespace(pPropChild, "creationdate") && !pPropChild->NoChildren())
+ {
+ struct tm timeDate = {};
+ strptime(pPropChild->FirstChild()->Value(), "%Y-%m-%dT%T", &timeDate);
+ item.m_dateTime = mktime(&timeDate);
+ }
+ else
+ if (CDAVCommon::ValueWithoutNamespace(pPropChild, "resourcetype"))
+ {
+ if (CDAVCommon::ValueWithoutNamespace(pPropChild->FirstChild(), "collection"))
+ {
+ item.m_bIsFolder = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+bool CDAVDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ CCurlFile dav;
+ std::string strRequest = "PROPFIND";
+
+ dav.SetCustomRequest(strRequest);
+ dav.SetMimeType("text/xml; charset=\"utf-8\"");
+ dav.SetRequestHeader("depth", 1);
+ dav.SetPostData(
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
+ " <D:propfind xmlns:D=\"DAV:\">"
+ " <D:prop>"
+ " <D:resourcetype/>"
+ " <D:getcontentlength/>"
+ " <D:getlastmodified/>"
+ " <D:creationdate/>"
+ " <D:displayname/>"
+ " </D:prop>"
+ " </D:propfind>");
+
+ if (!dav.Open(url))
+ {
+ CLog::Log(LOGERROR, "{} - Unable to get dav directory ({})", __FUNCTION__, url.GetRedacted());
+ return false;
+ }
+
+ std::string strResponse;
+ dav.ReadData(strResponse);
+
+ std::string fileCharset(dav.GetProperty(XFILE::FILE_PROPERTY_CONTENT_CHARSET));
+ CXBMCTinyXML davResponse;
+ davResponse.Parse(strResponse, fileCharset);
+
+ if (!davResponse.Parse(strResponse))
+ {
+ CLog::Log(LOGERROR, "{} - Unable to process dav directory ({})", __FUNCTION__,
+ url.GetRedacted());
+ dav.Close();
+ return false;
+ }
+
+ TiXmlNode *pChild;
+ // Iterate over all responses
+ for (pChild = davResponse.RootElement()->FirstChild(); pChild != 0; pChild = pChild->NextSibling())
+ {
+ if (CDAVCommon::ValueWithoutNamespace(pChild, "response"))
+ {
+ CFileItem item;
+ ParseResponse(pChild->ToElement(), item);
+ const CURL& url2(url);
+ CURL url3(item.GetPath());
+
+ std::string itemPath(URIUtils::AddFileToFolder(url2.GetWithoutFilename(), url3.GetFileName()));
+
+ if (item.GetLabel().empty())
+ {
+ std::string name(itemPath);
+ URIUtils::RemoveSlashAtEnd(name);
+ item.SetLabel(CURL::Decode(URIUtils::GetFileName(name)));
+ }
+
+ if (item.m_bIsFolder)
+ URIUtils::AddSlashAtEnd(itemPath);
+
+ // Add back protocol options
+ if (!url2.GetProtocolOptions().empty())
+ itemPath += "|" + url2.GetProtocolOptions();
+ item.SetPath(itemPath);
+
+ if (!item.IsURL(url))
+ {
+ CFileItemPtr pItem(new CFileItem(item));
+ items.Add(pItem);
+ }
+ }
+ }
+
+ dav.Close();
+
+ return true;
+}
+
+bool CDAVDirectory::Create(const CURL& url)
+{
+ CDAVFile dav;
+ std::string strRequest = "MKCOL";
+
+ dav.SetCustomRequest(strRequest);
+
+ if (!dav.Execute(url))
+ {
+ CLog::Log(LOGERROR, "{} - Unable to create dav directory ({}) - {}", __FUNCTION__,
+ url.GetRedacted(), dav.GetLastResponseCode());
+ return false;
+ }
+
+ dav.Close();
+
+ return true;
+}
+
+bool CDAVDirectory::Exists(const CURL& url)
+{
+ CCurlFile dav;
+
+ // Set the PROPFIND custom request else we may not find folders, depending
+ // on the server's configuration
+ std::string strRequest = "PROPFIND";
+ dav.SetCustomRequest(strRequest);
+ dav.SetRequestHeader("depth", 0);
+
+ return dav.Exists(url);
+}
+
+bool CDAVDirectory::Remove(const CURL& url)
+{
+ CDAVFile dav;
+ std::string strRequest = "DELETE";
+
+ dav.SetCustomRequest(strRequest);
+
+ if (!dav.Execute(url))
+ {
+ CLog::Log(LOGERROR, "{} - Unable to delete dav directory ({}) - {}", __FUNCTION__,
+ url.GetRedacted(), dav.GetLastResponseCode());
+ return false;
+ }
+
+ dav.Close();
+
+ return true;
+}
diff --git a/xbmc/filesystem/DAVDirectory.h b/xbmc/filesystem/DAVDirectory.h
new file mode 100644
index 0000000..d442dc3
--- /dev/null
+++ b/xbmc/filesystem/DAVDirectory.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "IDirectory.h"
+
+class CFileItem;
+class CFileItemList;
+class TiXmlElement;
+
+namespace XFILE
+{
+ class CDAVDirectory : public IDirectory
+ {
+ public:
+ CDAVDirectory(void);
+ ~CDAVDirectory(void) override;
+ 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;
+ DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ONCE; }
+
+ private:
+ void ParseResponse(const TiXmlElement *pElement, CFileItem &item);
+ };
+}
diff --git a/xbmc/filesystem/DAVFile.cpp b/xbmc/filesystem/DAVFile.cpp
new file mode 100644
index 0000000..a0ff2fd
--- /dev/null
+++ b/xbmc/filesystem/DAVFile.cpp
@@ -0,0 +1,145 @@
+/*
+ * 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 "DAVFile.h"
+
+#include "DAVCommon.h"
+#include "DllLibCurl.h"
+#include "URL.h"
+#include "utils/RegExp.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+using namespace XCURL;
+
+CDAVFile::CDAVFile(void)
+ : CCurlFile()
+{
+}
+
+CDAVFile::~CDAVFile(void) = default;
+
+bool CDAVFile::Execute(const CURL& url)
+{
+ CURL url2(url);
+ ParseAndCorrectUrl(url2);
+
+ CLog::Log(LOGDEBUG, "CDAVFile::Execute({}) {}", fmt::ptr(this), m_url);
+
+ assert(!(!m_state->m_easyHandle ^ !m_state->m_multiHandle));
+ if( m_state->m_easyHandle == NULL )
+ g_curlInterface.easy_acquire(url2.GetProtocol().c_str(),
+ url2.GetHostName().c_str(),
+ &m_state->m_easyHandle,
+ &m_state->m_multiHandle);
+
+ // setup common curl options
+ SetCommonOptions(m_state);
+ SetRequestHeaders(m_state);
+
+ m_lastResponseCode = m_state->Connect(m_bufferSize);
+ if (m_lastResponseCode < 0 || m_lastResponseCode >= 400)
+ return false;
+
+ char* efurl;
+ if (CURLE_OK == g_curlInterface.easy_getinfo(m_state->m_easyHandle, CURLINFO_EFFECTIVE_URL,&efurl) && efurl)
+ m_url = efurl;
+
+ if (m_lastResponseCode == 207)
+ {
+ std::string strResponse;
+ ReadData(strResponse);
+
+ CXBMCTinyXML davResponse;
+ davResponse.Parse(strResponse);
+
+ if (!davResponse.Parse(strResponse))
+ {
+ CLog::Log(LOGERROR, "CDAVFile::Execute - Unable to process dav response ({})",
+ CURL(m_url).GetRedacted());
+ Close();
+ return false;
+ }
+
+ TiXmlNode *pChild;
+ // Iterate over all responses
+ for (pChild = davResponse.RootElement()->FirstChild(); pChild != 0; pChild = pChild->NextSibling())
+ {
+ if (CDAVCommon::ValueWithoutNamespace(pChild, "response"))
+ {
+ std::string sRetCode = CDAVCommon::GetStatusTag(pChild->ToElement());
+ CRegExp rxCode;
+ rxCode.RegComp("HTTP/.+\\s(\\d+)\\s.*");
+ if (rxCode.RegFind(sRetCode) >= 0)
+ {
+ if (rxCode.GetSubCount())
+ {
+ m_lastResponseCode = atoi(rxCode.GetMatch(1).c_str());
+ if (m_lastResponseCode < 0 || m_lastResponseCode >= 400)
+ return false;
+ }
+ }
+
+ }
+ }
+ }
+
+ return true;
+}
+
+bool CDAVFile::Delete(const CURL& url)
+{
+ if (m_opened)
+ return false;
+
+ CDAVFile dav;
+ std::string strRequest = "DELETE";
+
+ dav.SetCustomRequest(strRequest);
+
+ CLog::Log(LOGDEBUG, "CDAVFile::Delete - Execute DELETE ({})", url.GetRedacted());
+ if (!dav.Execute(url))
+ {
+ CLog::Log(LOGERROR, "CDAVFile::Delete - Unable to delete dav resource ({})", url.GetRedacted());
+ return false;
+ }
+
+ dav.Close();
+
+ return true;
+}
+
+bool CDAVFile::Rename(const CURL& url, const CURL& urlnew)
+{
+ if (m_opened)
+ return false;
+
+ CDAVFile dav;
+
+ CURL url2(urlnew);
+ std::string strProtocol = url2.GetTranslatedProtocol();
+ url2.SetProtocol(strProtocol);
+
+ std::string strRequest = "MOVE";
+ dav.SetCustomRequest(strRequest);
+ dav.SetRequestHeader("Destination", url2.GetWithoutUserDetails());
+
+ CLog::Log(LOGDEBUG, "CDAVFile::Rename - Execute MOVE ({} -> {})", url.GetRedacted(),
+ url2.GetRedacted());
+ if (!dav.Execute(url))
+ {
+ CLog::Log(LOGERROR, "CDAVFile::Rename - Unable to rename dav resource ({} -> {})",
+ url.GetRedacted(), url2.GetRedacted());
+ return false;
+ }
+
+ dav.Close();
+
+ return true;
+}
diff --git a/xbmc/filesystem/DAVFile.h b/xbmc/filesystem/DAVFile.h
new file mode 100644
index 0000000..2103a45
--- /dev/null
+++ b/xbmc/filesystem/DAVFile.h
@@ -0,0 +1,31 @@
+/*
+ * 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 "CurlFile.h"
+
+namespace XFILE
+{
+ class CDAVFile : public CCurlFile
+ {
+ public:
+ CDAVFile(void);
+ ~CDAVFile(void) override;
+
+ virtual bool Execute(const CURL& url);
+
+ bool Delete(const CURL& url) override;
+ bool Rename(const CURL& url, const CURL& urlnew) override;
+
+ virtual int GetLastResponseCode() { return m_lastResponseCode; }
+
+ private:
+ int m_lastResponseCode = 0;
+ };
+}
diff --git a/xbmc/filesystem/Directorization.h b/xbmc/filesystem/Directorization.h
new file mode 100644
index 0000000..f413635
--- /dev/null
+++ b/xbmc/filesystem/Directorization.h
@@ -0,0 +1,139 @@
+/*
+ * 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 "FileItem.h"
+#include "URL.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace XFILE
+{
+ /**
+ * \brief Method definition to convert an entry to a CFileItemPtr.
+ *
+ * \param entry The entry to convert to a CFileItemPtr
+ * \param label The label of the entry
+ * \param path The path of the entry
+ * \param isFolder Whether the entry is a folder or not
+ * \return The CFileItemPtr object created from the given entry and data.
+ */
+ template<class TEntry>
+ using DirectorizeEntryToFileItemFunction = CFileItemPtr(*)(const TEntry& entry, const std::string& label, const std::string& path, bool isFolder);
+
+ template<class TEntry>
+ using DirectorizeEntry = std::pair<std::string, TEntry>;
+ template<class TEntry>
+ using DirectorizeEntries = std::vector<DirectorizeEntry<TEntry>>;
+
+ /**
+ * \brief Analyzes the given entry list from the given URL and turns them into files and directories on one directory hierarchy.
+ *
+ * \param url URL of the directory hierarchy to build
+ * \param entries Entries to analyze and turn into files and directories
+ * \param converter Converter function to convert an entry into a CFileItemPtr
+ * \param items Resulting item list
+ */
+ template<class TEntry>
+ static void Directorize(const CURL& url, const DirectorizeEntries<TEntry>& entries, DirectorizeEntryToFileItemFunction<TEntry> converter, CFileItemList& items)
+ {
+ if (url.Get().empty() || entries.empty())
+ return;
+
+ const std::string& options = url.GetOptions();
+ const std::string& filePath = url.GetFileName();
+
+ CURL baseUrl(url);
+ baseUrl.SetOptions(""); // delete options to have a clean path to add stuff too
+ baseUrl.SetFileName(""); // delete filename too as our names later will contain it
+
+ std::string basePath = baseUrl.Get();
+ URIUtils::AddSlashAtEnd(basePath);
+
+ std::vector<std::string> filePathTokens;
+ if (!filePath.empty())
+ StringUtils::Tokenize(filePath, filePathTokens, "/");
+
+ bool fastLookup = items.GetFastLookup();
+ items.SetFastLookup(true);
+ for (const auto& entry : entries)
+ {
+ std::string entryPath = entry.first;
+ std::string entryFileName = entryPath;
+ StringUtils::Replace(entryFileName, '\\', '/');
+
+ // skip the requested entry
+ if (entryFileName == filePath)
+ continue;
+
+ // Disregard Apple Resource Fork data
+ std::size_t found = entryPath.find("__MACOSX");
+ if (found != std::string::npos)
+ continue;
+
+ std::vector<std::string> pathTokens;
+ StringUtils::Tokenize(entryFileName, pathTokens, "/");
+
+ // ignore any entries in lower directory hierarchies
+ if (pathTokens.size() < filePathTokens.size() + 1)
+ continue;
+
+ // ignore any entries in different directory hierarchies
+ bool ignoreItem = false;
+ entryFileName.clear();
+ for (auto filePathToken = filePathTokens.begin(); filePathToken != filePathTokens.end(); ++filePathToken)
+ {
+ if (*filePathToken != pathTokens[std::distance(filePathTokens.begin(), filePathToken)])
+ {
+ ignoreItem = true;
+ break;
+ }
+ entryFileName = URIUtils::AddFileToFolder(entryFileName, *filePathToken);
+ }
+ if (ignoreItem)
+ continue;
+
+ entryFileName = URIUtils::AddFileToFolder(entryFileName, pathTokens[filePathTokens.size()]);
+ char c = entryPath[entryFileName.size()];
+ if (c == '/' || c == '\\')
+ URIUtils::AddSlashAtEnd(entryFileName);
+
+ std::string itemPath = URIUtils::AddFileToFolder(basePath, entryFileName) + options;
+ bool isFolder = false;
+ if (URIUtils::HasSlashAtEnd(entryFileName)) // this is a directory
+ {
+ // check if the directory has already been added
+ if (items.Contains(itemPath)) // already added
+ continue;
+
+ isFolder = true;
+ URIUtils::AddSlashAtEnd(itemPath);
+ }
+
+ // determine the entry's filename
+ std::string label = pathTokens[filePathTokens.size()];
+ g_charsetConverter.unknownToUTF8(label);
+
+ // convert the entry into a CFileItem
+ CFileItemPtr item = converter(entry.second, label, itemPath, isFolder);
+ item->SetPath(itemPath);
+ item->m_bIsFolder = isFolder;
+ if (isFolder)
+ item->m_dwSize = 0;
+
+ items.Add(item);
+ }
+ items.SetFastLookup(fastLookup);
+ }
+}
diff --git a/xbmc/filesystem/Directory.cpp b/xbmc/filesystem/Directory.cpp
new file mode 100644
index 0000000..5436fd9
--- /dev/null
+++ b/xbmc/filesystem/Directory.cpp
@@ -0,0 +1,446 @@
+/*
+ * 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 "Directory.h"
+
+#include "DirectoryCache.h"
+#include "DirectoryFactory.h"
+#include "FileDirectoryFactory.h"
+#include "FileItem.h"
+#include "PasswordManager.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "commons/Exception.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Job.h"
+#include "utils/JobManager.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+#define TIME_TO_BUSY_DIALOG 500
+
+class CGetDirectory
+{
+private:
+
+ struct CResult
+ {
+ CResult(const CURL& dir, const CURL& listDir) : m_event(true), m_dir(dir), m_listDir(listDir), m_result(false) {}
+ CEvent m_event;
+ CFileItemList m_list;
+ CURL m_dir;
+ CURL m_listDir;
+ bool m_result;
+ };
+
+ struct CGetJob
+ : CJob
+ {
+ CGetJob(std::shared_ptr<IDirectory>& imp
+ , std::shared_ptr<CResult>& result)
+ : m_result(result)
+ , m_imp(imp)
+ {}
+ public:
+ bool DoWork() override
+ {
+ m_result->m_list.SetURL(m_result->m_listDir);
+ m_result->m_result = m_imp->GetDirectory(m_result->m_dir, m_result->m_list);
+ m_result->m_event.Set();
+ return m_result->m_result;
+ }
+
+ std::shared_ptr<CResult> m_result;
+ std::shared_ptr<IDirectory> m_imp;
+ };
+
+public:
+
+ CGetDirectory(std::shared_ptr<IDirectory>& imp, const CURL& dir, const CURL& listDir)
+ : m_result(new CResult(dir, listDir))
+ {
+ m_id = CServiceBroker::GetJobManager()->AddJob(new CGetJob(imp, m_result), nullptr,
+ CJob::PRIORITY_HIGH);
+ if (m_id == 0)
+ {
+ CGetJob job(imp, m_result);
+ job.DoWork();
+ }
+ }
+ ~CGetDirectory() { CServiceBroker::GetJobManager()->CancelJob(m_id); }
+
+ CEvent& GetEvent()
+ {
+ return m_result->m_event;
+ }
+
+ bool Wait(unsigned int timeout)
+ {
+ return m_result->m_event.Wait(std::chrono::milliseconds(timeout));
+ }
+
+ bool GetDirectory(CFileItemList& list)
+ {
+ /* if it was not finished or failed, return failure */
+ if (!m_result->m_event.Wait(0ms) || !m_result->m_result)
+ {
+ list.Clear();
+ return false;
+ }
+
+ list.Copy(m_result->m_list);
+ return true;
+ }
+ std::shared_ptr<CResult> m_result;
+ unsigned int m_id;
+};
+
+
+CDirectory::CDirectory() = default;
+
+CDirectory::~CDirectory() = default;
+
+bool CDirectory::GetDirectory(const std::string& strPath, CFileItemList &items, const std::string &strMask, int flags)
+{
+ CHints hints;
+ hints.flags = flags;
+ hints.mask = strMask;
+ const CURL pathToUrl(strPath);
+ return GetDirectory(pathToUrl, items, hints);
+}
+
+bool CDirectory::GetDirectory(const std::string& strPath,
+ const std::shared_ptr<IDirectory>& pDirectory,
+ CFileItemList& items,
+ const std::string& strMask,
+ int flags)
+{
+ CHints hints;
+ hints.flags = flags;
+ hints.mask = strMask;
+ const CURL pathToUrl(strPath);
+ return GetDirectory(pathToUrl, pDirectory, items, hints);
+}
+
+bool CDirectory::GetDirectory(const std::string& strPath, CFileItemList &items, const CHints &hints)
+{
+ const CURL pathToUrl(strPath);
+ return GetDirectory(pathToUrl, items, hints);
+}
+
+bool CDirectory::GetDirectory(const CURL& url, CFileItemList &items, const std::string &strMask, int flags)
+{
+ CHints hints;
+ hints.flags = flags;
+ hints.mask = strMask;
+ return GetDirectory(url, items, hints);
+}
+
+bool CDirectory::GetDirectory(const CURL& url, CFileItemList &items, const CHints &hints)
+{
+ CURL realURL = URIUtils::SubstitutePath(url);
+ std::shared_ptr<IDirectory> pDirectory(CDirectoryFactory::Create(realURL));
+ return CDirectory::GetDirectory(url, pDirectory, items, hints);
+}
+
+bool CDirectory::GetDirectory(const CURL& url,
+ const std::shared_ptr<IDirectory>& pDirectory,
+ CFileItemList& items,
+ const CHints& hints)
+{
+ try
+ {
+ CURL realURL = URIUtils::SubstitutePath(url);
+ if (!pDirectory)
+ return false;
+
+ // check our cache for this path
+ if (g_directoryCache.GetDirectory(realURL.Get(), items, (hints.flags & DIR_FLAG_READ_CACHE) == DIR_FLAG_READ_CACHE))
+ items.SetURL(url);
+ else
+ {
+ // need to clear the cache (in case the directory fetch fails)
+ // and (re)fetch the folder
+ if (!(hints.flags & DIR_FLAG_BYPASS_CACHE))
+ g_directoryCache.ClearDirectory(realURL.Get());
+
+ pDirectory->SetFlags(hints.flags);
+
+ bool result = false;
+ CURL authUrl = realURL;
+
+ while (!result)
+ {
+ const std::string pathToUrl(url.Get());
+
+ // don't change auth if it's set explicitly
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrl);
+
+ items.SetURL(url);
+ result = pDirectory->GetDirectory(authUrl, items);
+
+ if (!result)
+ {
+ // @TODO ProcessRequirements() can bring up the keyboard input dialog
+ // filesystem must not depend on GUI
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread() &&
+ pDirectory->ProcessRequirements())
+ {
+ authUrl.SetDomain("");
+ authUrl.SetUserName("");
+ authUrl.SetPassword("");
+ continue;
+ }
+
+ CLog::Log(LOGERROR, "{} - Error getting {}", __FUNCTION__, url.GetRedacted());
+ return false;
+ }
+ }
+
+ // hide credentials if necessary
+ if (CPasswordManager::GetInstance().IsURLSupported(realURL))
+ {
+ bool hide = false;
+ // for explicitly credentials
+ if (!realURL.GetUserName().empty())
+ {
+ // credentials was changed i.e. were stored in the password
+ // manager, in this case we can hide them from an item URL,
+ // otherwise we have to keep credentials in an item URL
+ if ( realURL.GetUserName() != authUrl.GetUserName()
+ || realURL.GetPassWord() != authUrl.GetPassWord()
+ || realURL.GetDomain() != authUrl.GetDomain())
+ {
+ hide = true;
+ }
+ }
+ else
+ {
+ // hide credentials in any other cases
+ hide = true;
+ }
+
+ if (hide)
+ {
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr item = items[i];
+ CURL itemUrl = item->GetURL();
+ itemUrl.SetDomain("");
+ itemUrl.SetUserName("");
+ itemUrl.SetPassword("");
+ item->SetPath(itemUrl.Get());
+ }
+ }
+ }
+
+ // cache the directory, if necessary
+ if (!(hints.flags & DIR_FLAG_BYPASS_CACHE))
+ g_directoryCache.SetDirectory(realURL.Get(), items, pDirectory->GetCacheType(url));
+ }
+
+ // now filter for allowed files
+ if (!pDirectory->AllowAll())
+ {
+ pDirectory->SetMask(hints.mask);
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr item = items[i];
+ if (!item->m_bIsFolder && !pDirectory->IsAllowed(item->GetURL()))
+ {
+ items.Remove(i);
+ i--; // don't confuse loop
+ }
+ }
+ }
+ // filter hidden files
+ //! @todo we shouldn't be checking the gui setting here, callers should use getHidden instead
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWHIDDEN) && !(hints.flags & DIR_FLAG_GET_HIDDEN))
+ {
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ if (items[i]->GetProperty("file:hidden").asBoolean())
+ {
+ items.Remove(i);
+ i--; // don't confuse loop
+ }
+ }
+ }
+
+ // Should any of the files we read be treated as a directory?
+ // Disable for database folders, as they already contain the extracted items
+ if (!(hints.flags & DIR_FLAG_NO_FILE_DIRS) && !items.IsMusicDb() && !items.IsVideoDb() && !items.IsSmartPlayList())
+ FilterFileDirectories(items, hints.mask);
+
+ // Correct items for path substitution
+ const std::string pathToUrl(url.Get());
+ const std::string pathToUrl2(realURL.Get());
+ if (pathToUrl != pathToUrl2)
+ {
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr item = items[i];
+ item->SetPath(URIUtils::SubstitutePath(item->GetPath(), true));
+ }
+ }
+
+ return true;
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ CLog::Log(LOGERROR, "{} - Error getting {}", __FUNCTION__, url.GetRedacted());
+ return false;
+}
+
+bool CDirectory::Create(const std::string& strPath)
+{
+ const CURL pathToUrl(strPath);
+ return Create(pathToUrl);
+}
+
+bool CDirectory::Create(const CURL& url)
+{
+ try
+ {
+ CURL realURL = URIUtils::SubstitutePath(url);
+
+ if (CPasswordManager::GetInstance().IsURLSupported(realURL) && realURL.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(realURL);
+
+ std::unique_ptr<IDirectory> pDirectory(CDirectoryFactory::Create(realURL));
+ if (pDirectory)
+ if(pDirectory->Create(realURL))
+ return true;
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ CLog::Log(LOGERROR, "{} - Error creating {}", __FUNCTION__, url.GetRedacted());
+ return false;
+}
+
+bool CDirectory::Exists(const std::string& strPath, bool bUseCache /* = true */)
+{
+ const CURL pathToUrl(strPath);
+ return Exists(pathToUrl, bUseCache);
+}
+
+bool CDirectory::Exists(const CURL& url, bool bUseCache /* = true */)
+{
+ try
+ {
+ CURL realURL = URIUtils::SubstitutePath(url);
+ if (bUseCache)
+ {
+ bool bPathInCache;
+ std::string realPath(realURL.Get());
+ URIUtils::AddSlashAtEnd(realPath);
+ if (g_directoryCache.FileExists(realPath, bPathInCache))
+ return true;
+ if (bPathInCache)
+ return false;
+ }
+
+ if (CPasswordManager::GetInstance().IsURLSupported(realURL) && realURL.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(realURL);
+
+ std::unique_ptr<IDirectory> pDirectory(CDirectoryFactory::Create(realURL));
+ if (pDirectory)
+ return pDirectory->Exists(realURL);
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ CLog::Log(LOGERROR, "{} - Error checking for {}", __FUNCTION__, url.GetRedacted());
+ return false;
+}
+
+bool CDirectory::Remove(const std::string& strPath)
+{
+ const CURL pathToUrl(strPath);
+ return Remove(pathToUrl);
+}
+
+bool CDirectory::RemoveRecursive(const std::string& strPath)
+{
+ return RemoveRecursive(CURL{ strPath });
+}
+
+bool CDirectory::Remove(const CURL& url)
+{
+ try
+ {
+ CURL realURL = URIUtils::SubstitutePath(url);
+ CURL authUrl = realURL;
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrl);
+
+ std::unique_ptr<IDirectory> pDirectory(CDirectoryFactory::Create(realURL));
+ if (pDirectory)
+ if(pDirectory->Remove(authUrl))
+ {
+ g_directoryCache.ClearFile(realURL.Get());
+ return true;
+ }
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ CLog::Log(LOGERROR, "{} - Error removing {}", __FUNCTION__, url.GetRedacted());
+ return false;
+}
+
+bool CDirectory::RemoveRecursive(const CURL& url)
+{
+ try
+ {
+ CURL realURL = URIUtils::SubstitutePath(url);
+ CURL authUrl = realURL;
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrl);
+
+ std::unique_ptr<IDirectory> pDirectory(CDirectoryFactory::Create(realURL));
+ if (pDirectory)
+ if(pDirectory->RemoveRecursive(authUrl))
+ {
+ g_directoryCache.ClearFile(realURL.Get());
+ return true;
+ }
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ CLog::Log(LOGERROR, "{} - Error removing {}", __FUNCTION__, url.GetRedacted());
+ return false;
+}
+
+void CDirectory::FilterFileDirectories(CFileItemList &items, const std::string &mask,
+ bool expandImages)
+{
+ for (int i=0; i< items.Size(); ++i)
+ {
+ CFileItemPtr pItem=items[i];
+ auto mode = expandImages && pItem->IsDiscImage() ? EFILEFOLDER_TYPE_ONBROWSE : EFILEFOLDER_TYPE_ALWAYS;
+ if (!pItem->m_bIsFolder && pItem->IsFileFolder(mode))
+ {
+ std::unique_ptr<IFileDirectory> pDirectory(CFileDirectoryFactory::Create(pItem->GetURL(),pItem.get(),mask));
+ if (pDirectory)
+ pItem->m_bIsFolder = true;
+ else
+ if (pItem->m_bIsFolder)
+ {
+ items.Remove(i);
+ i--; // don't confuse loop
+ }
+ }
+ }
+}
diff --git a/xbmc/filesystem/Directory.h b/xbmc/filesystem/Directory.h
new file mode 100644
index 0000000..0af92f3
--- /dev/null
+++ b/xbmc/filesystem/Directory.h
@@ -0,0 +1,82 @@
+/*
+ * 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 "IDirectory.h"
+
+#include <memory>
+#include <string>
+
+namespace XFILE
+{
+/*!
+ \ingroup filesystem
+ \brief Wrappers for \e IDirectory
+ */
+class CDirectory
+{
+public:
+ CDirectory(void);
+ virtual ~CDirectory(void);
+
+ class CHints
+ {
+ public:
+ std::string mask;
+ int flags = DIR_FLAG_DEFAULTS;
+ };
+
+ static bool GetDirectory(const CURL& url
+ , CFileItemList &items
+ , const std::string &strMask
+ , int flags);
+
+ static bool GetDirectory(const CURL& url,
+ const std::shared_ptr<IDirectory>& pDirectory,
+ CFileItemList& items,
+ const CHints& hints);
+
+ static bool GetDirectory(const CURL& url
+ , CFileItemList &items
+ , const CHints &hints);
+
+ static bool Create(const CURL& url);
+ static bool Exists(const CURL& url, bool bUseCache = true);
+ static bool Remove(const CURL& url);
+ static bool RemoveRecursive(const CURL& url);
+
+ static bool GetDirectory(const std::string& strPath
+ , CFileItemList &items
+ , const std::string &strMask
+ , int flags);
+
+ static bool GetDirectory(const std::string& strPath,
+ const std::shared_ptr<IDirectory>& pDirectory,
+ CFileItemList& items,
+ const std::string& strMask,
+ int flags);
+
+ static bool GetDirectory(const std::string& strPath
+ , CFileItemList &items
+ , const CHints &hints);
+
+ static bool Create(const std::string& strPath);
+ static bool Exists(const std::string& strPath, bool bUseCache = true);
+ static bool Remove(const std::string& strPath);
+ static bool RemoveRecursive(const std::string& strPath);
+
+ /*! \brief Filter files that act like directories from the list, replacing them with their directory counterparts
+ \param items The item list to filter
+ \param mask The mask to apply when filtering files
+ \param expandImages True to include disc images in file directory expansion
+ */
+ static void FilterFileDirectories(CFileItemList &items, const std::string &mask,
+ bool expandImages=false);
+};
+}
diff --git a/xbmc/filesystem/DirectoryCache.cpp b/xbmc/filesystem/DirectoryCache.cpp
new file mode 100644
index 0000000..d46f11b
--- /dev/null
+++ b/xbmc/filesystem/DirectoryCache.cpp
@@ -0,0 +1,266 @@
+/*
+ * 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 "DirectoryCache.h"
+
+#include "Directory.h"
+#include "FileItem.h"
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <climits>
+#include <mutex>
+
+// Maximum number of directories to keep in our cache
+#define MAX_CACHED_DIRS 50
+
+using namespace XFILE;
+
+CDirectoryCache::CDir::CDir(DIR_CACHE_TYPE cacheType)
+{
+ m_cacheType = cacheType;
+ m_lastAccess = 0;
+ m_Items = std::make_unique<CFileItemList>();
+ m_Items->SetIgnoreURLOptions(true);
+ m_Items->SetFastLookup(true);
+}
+
+CDirectoryCache::CDir::~CDir() = default;
+
+void CDirectoryCache::CDir::SetLastAccess(unsigned int &accessCounter)
+{
+ m_lastAccess = accessCounter++;
+}
+
+CDirectoryCache::CDirectoryCache(void)
+{
+ m_accessCounter = 0;
+#ifdef _DEBUG
+ m_cacheHits = 0;
+ m_cacheMisses = 0;
+#endif
+}
+
+CDirectoryCache::~CDirectoryCache(void) = default;
+
+bool CDirectoryCache::GetDirectory(const std::string& strPath, CFileItemList &items, bool retrieveAll)
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+
+ // Get rid of any URL options, else the compare may be wrong
+ std::string storedPath = CURL(strPath).GetWithoutOptions();
+ URIUtils::RemoveSlashAtEnd(storedPath);
+
+ auto i = m_cache.find(storedPath);
+ if (i != m_cache.end())
+ {
+ CDir& dir = i->second;
+ if (dir.m_cacheType == XFILE::DIR_CACHE_ALWAYS ||
+ (dir.m_cacheType == XFILE::DIR_CACHE_ONCE && retrieveAll))
+ {
+ items.Copy(*dir.m_Items);
+ dir.SetLastAccess(m_accessCounter);
+#ifdef _DEBUG
+ m_cacheHits+=items.Size();
+#endif
+ return true;
+ }
+ }
+ return false;
+}
+
+void CDirectoryCache::SetDirectory(const std::string& strPath, const CFileItemList &items, DIR_CACHE_TYPE cacheType)
+{
+ if (cacheType == DIR_CACHE_NEVER)
+ return; // nothing to do
+
+ // caches the given directory using a copy of the items, rather than the items
+ // themselves. The reason we do this is because there is often some further
+ // processing on the items (stacking, transparent rars/zips for instance) that
+ // alters the URL of the items. If we shared the pointers, we'd have problems
+ // as the URLs in the cache would have changed, so things such as
+ // CDirectoryCache::FileExists() would fail for files that really do exist (just their
+ // URL's have been altered). This is called from CFile::Exists() which causes
+ // all sorts of hassles.
+ // IDEALLY, any further processing on the item would actually create a new item
+ // instead of altering it, but we can't really enforce that in an easy way, so
+ // this is the best solution for now.
+ std::unique_lock<CCriticalSection> lock(m_cs);
+
+ // Get rid of any URL options, else the compare may be wrong
+ std::string storedPath = CURL(strPath).GetWithoutOptions();
+ URIUtils::RemoveSlashAtEnd(storedPath);
+
+ ClearDirectory(storedPath);
+
+ CheckIfFull();
+
+ CDir dir(cacheType);
+ dir.m_Items->Copy(items);
+ dir.SetLastAccess(m_accessCounter);
+ m_cache.emplace(std::make_pair(storedPath, std::move(dir)));
+}
+
+void CDirectoryCache::ClearFile(const std::string& strFile)
+{
+ // Get rid of any URL options, else the compare may be wrong
+ std::string strFile2 = CURL(strFile).GetWithoutOptions();
+ URIUtils::RemoveSlashAtEnd(strFile2);
+
+ ClearDirectory(URIUtils::GetDirectory(strFile2));
+}
+
+void CDirectoryCache::ClearDirectory(const std::string& strPath)
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+
+ // Get rid of any URL options, else the compare may be wrong
+ std::string storedPath = CURL(strPath).GetWithoutOptions();
+ URIUtils::RemoveSlashAtEnd(storedPath);
+
+ m_cache.erase(storedPath);
+}
+
+void CDirectoryCache::ClearSubPaths(const std::string& strPath)
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+
+ // Get rid of any URL options, else the compare may be wrong
+ std::string storedPath = CURL(strPath).GetWithoutOptions();
+
+ auto i = m_cache.begin();
+ while (i != m_cache.end())
+ {
+ if (URIUtils::PathHasParent(i->first, storedPath))
+ m_cache.erase(i++);
+ else
+ i++;
+ }
+}
+
+void CDirectoryCache::AddFile(const std::string& strFile)
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+
+ // Get rid of any URL options, else the compare may be wrong
+ std::string strPath = URIUtils::GetDirectory(CURL(strFile).GetWithoutOptions());
+ URIUtils::RemoveSlashAtEnd(strPath);
+
+ auto i = m_cache.find(strPath);
+ if (i != m_cache.end())
+ {
+ CDir& dir = i->second;
+ CFileItemPtr item(new CFileItem(strFile, false));
+ dir.m_Items->Add(item);
+ dir.SetLastAccess(m_accessCounter);
+ }
+}
+
+bool CDirectoryCache::FileExists(const std::string& strFile, bool& bInCache)
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+ bInCache = false;
+
+ // Get rid of any URL options, else the compare may be wrong
+ std::string strPath = CURL(strFile).GetWithoutOptions();
+ URIUtils::RemoveSlashAtEnd(strPath);
+ std::string storedPath = URIUtils::GetDirectory(strPath);
+ URIUtils::RemoveSlashAtEnd(storedPath);
+
+ auto i = m_cache.find(storedPath);
+ if (i != m_cache.end())
+ {
+ bInCache = true;
+ CDir& dir = i->second;
+ dir.SetLastAccess(m_accessCounter);
+#ifdef _DEBUG
+ m_cacheHits++;
+#endif
+ return (URIUtils::PathEquals(strPath, storedPath) || dir.m_Items->Contains(strFile));
+ }
+#ifdef _DEBUG
+ m_cacheMisses++;
+#endif
+ return false;
+}
+
+void CDirectoryCache::Clear()
+{
+ // this routine clears everything
+ std::unique_lock<CCriticalSection> lock(m_cs);
+ m_cache.clear();
+}
+
+void CDirectoryCache::InitCache(const std::set<std::string>& dirs)
+{
+ for (const std::string& strDir : dirs)
+ {
+ CFileItemList items;
+ CDirectory::GetDirectory(strDir, items, "", DIR_FLAG_NO_FILE_DIRS);
+ items.Clear();
+ }
+}
+
+void CDirectoryCache::ClearCache(std::set<std::string>& dirs)
+{
+ auto i = m_cache.begin();
+ while (i != m_cache.end())
+ {
+ if (dirs.find(i->first) != dirs.end())
+ m_cache.erase(i++);
+ else
+ i++;
+ }
+}
+
+void CDirectoryCache::CheckIfFull()
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+
+ // find the last accessed folder, and remove if the number of cached folders is too many
+ auto lastAccessed = m_cache.end();
+ unsigned int numCached = 0;
+ for (auto i = m_cache.begin(); i != m_cache.end(); i++)
+ {
+ // ensure dirs that are always cached aren't cleared
+ if (i->second.m_cacheType != DIR_CACHE_ALWAYS)
+ {
+ if (lastAccessed == m_cache.end() ||
+ i->second.GetLastAccess() < lastAccessed->second.GetLastAccess())
+ lastAccessed = i;
+ numCached++;
+ }
+ }
+ if (lastAccessed != m_cache.end() && numCached >= MAX_CACHED_DIRS)
+ m_cache.erase(lastAccessed);
+}
+
+#ifdef _DEBUG
+void CDirectoryCache::PrintStats() const
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+ CLog::Log(LOGDEBUG, "{} - total of {} cache hits, and {} cache misses", __FUNCTION__, m_cacheHits,
+ m_cacheMisses);
+ // run through and find the oldest and the number of items cached
+ unsigned int oldest = UINT_MAX;
+ unsigned int numItems = 0;
+ unsigned int numDirs = 0;
+ for (auto i = m_cache.begin(); i != m_cache.end(); i++)
+ {
+ const CDir& dir = i->second;
+ oldest = std::min(oldest, dir.GetLastAccess());
+ numItems += dir.m_Items->Size();
+ numDirs++;
+ }
+ CLog::Log(LOGDEBUG, "{} - {} folders cached, with {} items total. Oldest is {}, current is {}",
+ __FUNCTION__, numDirs, numItems, oldest, m_accessCounter);
+}
+#endif
diff --git a/xbmc/filesystem/DirectoryCache.h b/xbmc/filesystem/DirectoryCache.h
new file mode 100644
index 0000000..2058c78
--- /dev/null
+++ b/xbmc/filesystem/DirectoryCache.h
@@ -0,0 +1,73 @@
+/*
+ * 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 "IDirectory.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+#include <set>
+
+class CFileItem;
+
+namespace XFILE
+{
+ class CDirectoryCache
+ {
+ class CDir
+ {
+ public:
+ explicit CDir(DIR_CACHE_TYPE cacheType);
+ CDir(CDir&& dir) = default;
+ CDir& operator=(CDir&& dir) = default;
+ virtual ~CDir();
+
+ void SetLastAccess(unsigned int &accessCounter);
+ unsigned int GetLastAccess() const { return m_lastAccess; }
+
+ std::unique_ptr<CFileItemList> m_Items;
+ DIR_CACHE_TYPE m_cacheType;
+ private:
+ CDir(const CDir&) = delete;
+ CDir& operator=(const CDir&) = delete;
+ unsigned int m_lastAccess;
+ };
+ public:
+ CDirectoryCache(void);
+ virtual ~CDirectoryCache(void);
+ bool GetDirectory(const std::string& strPath, CFileItemList &items, bool retrieveAll = false);
+ void SetDirectory(const std::string& strPath, const CFileItemList &items, DIR_CACHE_TYPE cacheType);
+ void ClearDirectory(const std::string& strPath);
+ void ClearFile(const std::string& strFile);
+ void ClearSubPaths(const std::string& strPath);
+ void Clear();
+ void AddFile(const std::string& strFile);
+ bool FileExists(const std::string& strPath, bool& bInCache);
+#ifdef _DEBUG
+ void PrintStats() const;
+#endif
+ protected:
+ void InitCache(const std::set<std::string>& dirs);
+ void ClearCache(std::set<std::string>& dirs);
+ void CheckIfFull();
+
+ std::map<std::string, CDir> m_cache;
+
+ mutable CCriticalSection m_cs;
+
+ unsigned int m_accessCounter;
+
+#ifdef _DEBUG
+ unsigned int m_cacheHits;
+ unsigned int m_cacheMisses;
+#endif
+ };
+}
+extern XFILE::CDirectoryCache g_directoryCache;
diff --git a/xbmc/filesystem/DirectoryFactory.cpp b/xbmc/filesystem/DirectoryFactory.cpp
new file mode 100644
index 0000000..a3016f6
--- /dev/null
+++ b/xbmc/filesystem/DirectoryFactory.cpp
@@ -0,0 +1,197 @@
+/*
+ * 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 <stdlib.h>
+#include "network/Network.h"
+#include "DirectoryFactory.h"
+#include "SpecialProtocolDirectory.h"
+#include "MultiPathDirectory.h"
+#include "StackDirectory.h"
+#include "FileDirectoryFactory.h"
+#include "PlaylistDirectory.h"
+#include "MusicDatabaseDirectory.h"
+#include "MusicSearchDirectory.h"
+#include "VideoDatabaseDirectory.h"
+#include "FavouritesDirectory.h"
+#include "LibraryDirectory.h"
+#include "EventsDirectory.h"
+#include "AddonsDirectory.h"
+#include "SourcesDirectory.h"
+#include "FTPDirectory.h"
+#include "HTTPDirectory.h"
+#include "DAVDirectory.h"
+#if defined(HAS_UDFREAD)
+#include "UDFDirectory.h"
+#endif
+#include "utils/log.h"
+#include "network/WakeOnAccess.h"
+
+#ifdef TARGET_POSIX
+#include "platform/posix/filesystem/PosixDirectory.h"
+#elif defined(TARGET_WINDOWS)
+#include "platform/win32/filesystem/Win32Directory.h"
+#ifdef TARGET_WINDOWS_STORE
+#include "platform/win10/filesystem/WinLibraryDirectory.h"
+#endif
+#endif
+#ifdef HAS_FILESYSTEM_SMB
+#ifdef TARGET_WINDOWS
+#include "platform/win32/filesystem/Win32SMBDirectory.h"
+#else
+#include "platform/posix/filesystem/SMBDirectory.h"
+#endif
+#endif
+#include "CDDADirectory.h"
+#include "PluginDirectory.h"
+#if defined(HAS_ISO9660PP)
+#include "ISO9660Directory.h"
+#endif
+#ifdef HAS_UPNP
+#include "UPnPDirectory.h"
+#endif
+#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"
+#include "FileItem.h"
+#include "URL.h"
+#include "RSSDirectory.h"
+#ifdef HAS_ZEROCONF
+#include "ZeroconfDirectory.h"
+#endif
+#ifdef HAS_FILESYSTEM_NFS
+#include "NFSDirectory.h"
+#endif
+#ifdef HAVE_LIBBLURAY
+#include "BlurayDirectory.h"
+#endif
+#if defined(TARGET_ANDROID)
+#include "platform/android/filesystem/AndroidAppDirectory.h"
+#endif
+#include "ResourceDirectory.h"
+#include "ServiceBroker.h"
+#include "addons/VFSEntry.h"
+#include "utils/StringUtils.h"
+
+using namespace ADDON;
+
+using namespace XFILE;
+
+/*!
+ \brief Create a IDirectory object of the share type specified in \e strPath .
+ \param strPath Specifies the share type to access, can be a share or share with path.
+ \return IDirectory object to access the directories on the share.
+ \sa IDirectory
+ */
+IDirectory* CDirectoryFactory::Create(const CURL& url)
+{
+ if (!CWakeOnAccess::GetInstance().WakeUpHost(url))
+ return NULL;
+
+ CFileItem item(url.Get(), true);
+ IFileDirectory* pDir = CFileDirectoryFactory::Create(url, &item);
+ if (pDir)
+ return pDir;
+
+ if (!url.GetProtocol().empty() && CServiceBroker::IsAddonInterfaceUp())
+ {
+ for (const auto& vfsAddon : CServiceBroker::GetVFSAddonCache().GetAddonInstances())
+ {
+ auto prots = StringUtils::Split(vfsAddon->GetProtocols(), "|");
+
+ if (vfsAddon->HasDirectories() && std::find(prots.begin(), prots.end(), url.GetProtocol()) != prots.end())
+ return new CVFSEntryIDirectoryWrapper(vfsAddon);
+ }
+ }
+
+#ifdef TARGET_POSIX
+ 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
+#error Local directory access is not implemented for this platform
+#endif
+ if (url.IsProtocol("special")) return new CSpecialProtocolDirectory();
+ if (url.IsProtocol("sources")) return new CSourcesDirectory();
+ if (url.IsProtocol("addons")) return new CAddonsDirectory();
+#if defined(HAS_DVD_DRIVE)
+ if (url.IsProtocol("cdda")) return new CCDDADirectory();
+#endif
+#if defined(HAS_ISO9660PP)
+ if (url.IsProtocol("iso9660")) return new CISO9660Directory();
+#endif
+#if defined(HAS_UDFREAD)
+ if (url.IsProtocol("udf")) return new CUDFDirectory();
+#endif
+ if (url.IsProtocol("plugin")) return new CPluginDirectory();
+#if defined(TARGET_ANDROID)
+ if (url.IsProtocol("apk")) return new CAPKDirectory();
+#endif
+ if (url.IsProtocol("zip")) return new CZipDirectory();
+ if (url.IsProtocol("xbt")) return new CXbtDirectory();
+ if (url.IsProtocol("multipath")) return new CMultiPathDirectory();
+ if (url.IsProtocol("stack")) return new CStackDirectory();
+ if (url.IsProtocol("playlistmusic")) return new CPlaylistDirectory();
+ if (url.IsProtocol("playlistvideo")) return new CPlaylistDirectory();
+ if (url.IsProtocol("musicdb")) return new CMusicDatabaseDirectory();
+ if (url.IsProtocol("musicsearch")) return new CMusicSearchDirectory();
+ if (url.IsProtocol("videodb")) return new CVideoDatabaseDirectory();
+ if (url.IsProtocol("library")) return new CLibraryDirectory();
+ if (url.IsProtocol("favourites")) return new CFavouritesDirectory();
+#if defined(TARGET_ANDROID)
+ if (url.IsProtocol("androidapp")) return new CAndroidAppDirectory();
+#endif
+#ifdef HAVE_LIBBLURAY
+ if (url.IsProtocol("bluray")) return new CBlurayDirectory();
+#endif
+ if (url.IsProtocol("resource")) return new CResourceDirectory();
+ if (url.IsProtocol("events")) return new CEventsDirectory();
+#ifdef TARGET_WINDOWS_STORE
+ if (CWinLibraryDirectory::IsValid(url)) return new CWinLibraryDirectory();
+#endif
+
+ if (url.IsProtocol("ftp") || url.IsProtocol("ftps")) return new CFTPDirectory();
+ if (url.IsProtocol("http") || url.IsProtocol("https")) return new CHTTPDirectory();
+ if (url.IsProtocol("dav") || url.IsProtocol("davs")) return new CDAVDirectory();
+#ifdef HAS_FILESYSTEM_SMB
+#ifdef TARGET_WINDOWS
+ if (url.IsProtocol("smb")) return new CWin32SMBDirectory();
+#else
+ if (url.IsProtocol("smb")) return new CSMBDirectory();
+#endif
+#endif
+#ifdef HAS_UPNP
+ if (url.IsProtocol("upnp")) return new CUPnPDirectory();
+#endif
+ if (url.IsProtocol("rss") || url.IsProtocol("rsss")) return new CRSSDirectory();
+#ifdef HAS_ZEROCONF
+ if (url.IsProtocol("zeroconf")) return new CZeroconfDirectory();
+#endif
+#ifdef HAS_FILESYSTEM_NFS
+ if (url.IsProtocol("nfs")) return new CNFSDirectory();
+#endif
+
+ if (url.IsProtocol("pvr"))
+ return new CPVRDirectory();
+
+ CLog::Log(LOGWARNING, "{} - unsupported protocol({}) in {}", __FUNCTION__, url.GetProtocol(),
+ url.GetRedacted());
+ return NULL;
+}
+
diff --git a/xbmc/filesystem/DirectoryFactory.h b/xbmc/filesystem/DirectoryFactory.h
new file mode 100644
index 0000000..71d5f6f
--- /dev/null
+++ b/xbmc/filesystem/DirectoryFactory.h
@@ -0,0 +1,39 @@
+/*
+ * 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 "IDirectory.h"
+
+namespace XFILE
+{
+/*!
+ \ingroup filesystem
+ \brief Get access to a directory of a file system.
+
+ The Factory can be used to create a directory object
+ for every file system accessable. \n
+ \n
+ Example:
+
+ \verbatim
+ std::string strShare="iso9660://";
+
+ IDirectory* pDir=CDirectoryFactory::Create(strShare);
+ \endverbatim
+ The \e pDir pointer can be used to access a directory and retrieve it's content.
+
+ When different types of shares have to be accessed use CVirtualDirectory.
+ \sa IDirectory
+ */
+class CDirectoryFactory
+{
+public:
+ static IDirectory* Create(const CURL& url);
+};
+}
diff --git a/xbmc/filesystem/DirectoryHistory.cpp b/xbmc/filesystem/DirectoryHistory.cpp
new file mode 100644
index 0000000..aecf285
--- /dev/null
+++ b/xbmc/filesystem/DirectoryHistory.cpp
@@ -0,0 +1,156 @@
+/*
+ * 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 "DirectoryHistory.h"
+
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+const std::string& CDirectoryHistory::CPathHistoryItem::GetPath(bool filter /* = false */) const
+{
+ if (filter && !m_strFilterPath.empty())
+ return m_strFilterPath;
+
+ return m_strPath;
+}
+
+CDirectoryHistory::~CDirectoryHistory()
+{
+ m_vecHistory.clear();
+ m_vecPathHistory.clear();
+}
+
+void CDirectoryHistory::RemoveSelectedItem(const std::string& strDirectory)
+{
+ HistoryMap::iterator iter = m_vecHistory.find(preparePath(strDirectory));
+ if (iter != m_vecHistory.end())
+ m_vecHistory.erase(iter);
+}
+
+void CDirectoryHistory::SetSelectedItem(const std::string& strSelectedItem, const std::string& strDirectory)
+{
+ if (strSelectedItem.empty())
+ return;
+
+ std::string strDir = preparePath(strDirectory);
+ std::string strItem = preparePath(strSelectedItem, false);
+
+ HistoryMap::iterator iter = m_vecHistory.find(strDir);
+ if (iter != m_vecHistory.end())
+ {
+ iter->second.m_strItem = strItem;
+ return;
+ }
+
+ CHistoryItem item;
+ item.m_strItem = strItem;
+ item.m_strDirectory = strDir;
+ m_vecHistory[strDir] = item;
+}
+
+const std::string& CDirectoryHistory::GetSelectedItem(const std::string& strDirectory) const
+{
+ HistoryMap::const_iterator iter = m_vecHistory.find(preparePath(strDirectory));
+ if (iter != m_vecHistory.end())
+ return iter->second.m_strItem;
+
+ return StringUtils::Empty;
+}
+
+void CDirectoryHistory::AddPath(const std::string& strPath, const std::string &strFilterPath /* = "" */)
+{
+ if (!m_vecPathHistory.empty() && m_vecPathHistory.back().m_strPath == strPath)
+ {
+ if (!strFilterPath.empty())
+ m_vecPathHistory.back().m_strFilterPath = strFilterPath;
+ return;
+ }
+
+ CPathHistoryItem item;
+ item.m_strPath = strPath;
+ item.m_strFilterPath = strFilterPath;
+ m_vecPathHistory.push_back(item);
+}
+
+void CDirectoryHistory::AddPathFront(const std::string& strPath, const std::string &strFilterPath /* = "" */)
+{
+ CPathHistoryItem item;
+ item.m_strPath = strPath;
+ item.m_strFilterPath = strFilterPath;
+ m_vecPathHistory.insert(m_vecPathHistory.begin(), item);
+}
+
+std::string CDirectoryHistory::GetParentPath(bool filter /* = false */)
+{
+ if (m_vecPathHistory.empty())
+ return "";
+
+ return m_vecPathHistory.back().GetPath(filter);
+}
+
+bool CDirectoryHistory::IsInHistory(const std::string &path) const
+{
+ std::string slashEnded(path);
+ URIUtils::AddSlashAtEnd(slashEnded);
+ for (std::vector<CPathHistoryItem>::const_iterator i = m_vecPathHistory.begin(); i != m_vecPathHistory.end(); ++i)
+ {
+ std::string testPath(i->GetPath());
+ URIUtils::AddSlashAtEnd(testPath);
+ if (slashEnded == testPath)
+ return true;
+ }
+ return false;
+}
+
+std::string CDirectoryHistory::RemoveParentPath(bool filter /* = false */)
+{
+ if (m_vecPathHistory.empty())
+ return "";
+
+ std::string strParent = GetParentPath(filter);
+ m_vecPathHistory.pop_back();
+ return strParent;
+}
+
+void CDirectoryHistory::ClearPathHistory()
+{
+ m_vecPathHistory.clear();
+}
+
+bool CDirectoryHistory::IsMusicSearchUrl(CPathHistoryItem &i)
+{
+ return StringUtils::StartsWith(i.GetPath(), "musicsearch://");
+}
+
+void CDirectoryHistory::ClearSearchHistory()
+{
+ m_vecPathHistory.erase(remove_if(m_vecPathHistory.begin(), m_vecPathHistory.end(), IsMusicSearchUrl), m_vecPathHistory.end());
+}
+
+void CDirectoryHistory::DumpPathHistory()
+{
+ // debug log
+ CLog::Log(LOGDEBUG,"Current m_vecPathHistory:");
+ for (int i = 0; i < (int)m_vecPathHistory.size(); ++i)
+ CLog::Log(LOGDEBUG, " {:02}.[{}; {}]", i, m_vecPathHistory[i].m_strPath,
+ m_vecPathHistory[i].m_strFilterPath);
+}
+
+std::string CDirectoryHistory::preparePath(const std::string &strDirectory, bool tolower /* = true */)
+{
+ std::string strDir = strDirectory;
+ if (tolower)
+ StringUtils::ToLower(strDir);
+
+ URIUtils::RemoveSlashAtEnd(strDir);
+
+ return strDir;
+}
diff --git a/xbmc/filesystem/DirectoryHistory.h b/xbmc/filesystem/DirectoryHistory.h
new file mode 100644
index 0000000..49f1873
--- /dev/null
+++ b/xbmc/filesystem/DirectoryHistory.h
@@ -0,0 +1,67 @@
+/*
+ * 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 <map>
+#include <string>
+#include <vector>
+
+class CDirectoryHistory
+{
+public:
+ class CHistoryItem
+ {
+ public:
+ CHistoryItem() = default;
+ virtual ~CHistoryItem() = default;
+ std::string m_strItem;
+ std::string m_strDirectory;
+ };
+
+ class CPathHistoryItem
+ {
+ public:
+ CPathHistoryItem() = default;
+ virtual ~CPathHistoryItem() = default;
+
+ const std::string& GetPath(bool filter = false) const;
+
+ std::string m_strPath;
+ std::string m_strFilterPath;
+ };
+
+ CDirectoryHistory() = default;
+ virtual ~CDirectoryHistory();
+
+ void SetSelectedItem(const std::string& strSelectedItem, const std::string& strDirectory);
+ const std::string& GetSelectedItem(const std::string& strDirectory) const;
+ void RemoveSelectedItem(const std::string& strDirectory);
+
+ void AddPath(const std::string& strPath, const std::string &m_strFilterPath = "");
+ void AddPathFront(const std::string& strPath, const std::string &m_strFilterPath = "");
+ std::string GetParentPath(bool filter = false);
+ std::string RemoveParentPath(bool filter = false);
+ void ClearPathHistory();
+ void ClearSearchHistory();
+ void DumpPathHistory();
+
+ /*! \brief Returns whether a path is in the history.
+ \param path to test
+ \return true if the path is in the history, false otherwise.
+ */
+ bool IsInHistory(const std::string &path) const;
+
+private:
+ static std::string preparePath(const std::string &strDirectory, bool tolower = true);
+
+ typedef std::map<std::string, CHistoryItem> HistoryMap;
+ HistoryMap m_vecHistory;
+ std::vector<CPathHistoryItem> m_vecPathHistory; ///< History of traversed directories
+ static bool IsMusicSearchUrl(CPathHistoryItem &i);
+};
diff --git a/xbmc/filesystem/DllLibCurl.cpp b/xbmc/filesystem/DllLibCurl.cpp
new file mode 100644
index 0000000..8367e9d
--- /dev/null
+++ b/xbmc/filesystem/DllLibCurl.cpp
@@ -0,0 +1,306 @@
+/*
+ * 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 "DllLibCurl.h"
+
+#include "threads/SystemClock.h"
+#include "utils/log.h"
+
+#include <assert.h>
+#include <mutex>
+
+namespace XCURL
+{
+CURLcode DllLibCurl::global_init(long flags)
+{
+ return curl_global_init(flags);
+}
+
+void DllLibCurl::global_cleanup()
+{
+ curl_global_cleanup();
+}
+
+CURL_HANDLE* DllLibCurl::easy_init()
+{
+ return curl_easy_init();
+}
+
+CURLcode DllLibCurl::easy_perform(CURL_HANDLE* handle)
+{
+ return curl_easy_perform(handle);
+}
+
+CURLcode DllLibCurl::easy_pause(CURL_HANDLE* handle, int bitmask)
+{
+ return curl_easy_pause(handle, bitmask);
+}
+
+void DllLibCurl::easy_reset(CURL_HANDLE* handle)
+{
+ curl_easy_reset(handle);
+}
+
+void DllLibCurl::easy_cleanup(CURL_HANDLE* handle)
+{
+ curl_easy_cleanup(handle);
+}
+
+CURL_HANDLE* DllLibCurl::easy_duphandle(CURL_HANDLE* handle)
+{
+ return curl_easy_duphandle(handle);
+}
+
+CURLM* DllLibCurl::multi_init()
+{
+ return curl_multi_init();
+}
+
+CURLMcode DllLibCurl::multi_add_handle(CURLM* multi_handle, CURL_HANDLE* easy_handle)
+{
+ return curl_multi_add_handle(multi_handle, easy_handle);
+}
+
+CURLMcode DllLibCurl::multi_perform(CURLM* multi_handle, int* running_handles)
+{
+ return curl_multi_perform(multi_handle, running_handles);
+}
+
+CURLMcode DllLibCurl::multi_remove_handle(CURLM* multi_handle, CURL_HANDLE* easy_handle)
+{
+ return curl_multi_remove_handle(multi_handle, easy_handle);
+}
+
+CURLMcode DllLibCurl::multi_fdset(
+ CURLM* multi_handle, fd_set* read_fd_set, fd_set* write_fd_set, fd_set* exc_fd_set, int* max_fd)
+{
+ return curl_multi_fdset(multi_handle, read_fd_set, write_fd_set, exc_fd_set, max_fd);
+}
+
+CURLMcode DllLibCurl::multi_timeout(CURLM* multi_handle, long* timeout)
+{
+ return curl_multi_timeout(multi_handle, timeout);
+}
+
+CURLMsg* DllLibCurl::multi_info_read(CURLM* multi_handle, int* msgs_in_queue)
+{
+ return curl_multi_info_read(multi_handle, msgs_in_queue);
+}
+
+CURLMcode DllLibCurl::multi_cleanup(CURLM* handle)
+{
+ return curl_multi_cleanup(handle);
+}
+
+curl_slist* DllLibCurl::slist_append(curl_slist* list, const char* to_append)
+{
+ return curl_slist_append(list, to_append);
+}
+
+void DllLibCurl::slist_free_all(curl_slist* list)
+{
+ curl_slist_free_all(list);
+}
+
+const char* DllLibCurl::easy_strerror(CURLcode code)
+{
+ return curl_easy_strerror(code);
+}
+
+DllLibCurlGlobal::DllLibCurlGlobal()
+{
+ /* we handle this ourself */
+ if (curl_global_init(CURL_GLOBAL_ALL))
+ {
+ CLog::Log(LOGERROR, "Error initializing libcurl");
+ }
+}
+
+DllLibCurlGlobal::~DllLibCurlGlobal()
+{
+ // close libcurl
+ curl_global_cleanup();
+}
+
+void DllLibCurlGlobal::CheckIdle()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ /* 20 seconds idle time before closing handle */
+ const unsigned int idletime = 30000;
+
+ VEC_CURLSESSIONS::iterator it = m_sessions.begin();
+ while (it != m_sessions.end())
+ {
+ auto now = std::chrono::steady_clock::now();
+ auto duration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - it->m_idletimestamp);
+
+ if (!it->m_busy && duration.count() > idletime)
+ {
+ CLog::Log(LOGDEBUG, "{} - Closing session to {}://{} (easy={}, multi={})", __FUNCTION__,
+ it->m_protocol, it->m_hostname, fmt::ptr(it->m_easy), fmt::ptr(it->m_multi));
+
+ if (it->m_multi && it->m_easy)
+ multi_remove_handle(it->m_multi, it->m_easy);
+ if (it->m_easy)
+ easy_cleanup(it->m_easy);
+ if (it->m_multi)
+ multi_cleanup(it->m_multi);
+
+ it = m_sessions.erase(it);
+ continue;
+ }
+ ++it;
+ }
+}
+
+void DllLibCurlGlobal::easy_acquire(const char* protocol,
+ const char* hostname,
+ CURL_HANDLE** easy_handle,
+ CURLM** multi_handle)
+{
+ assert(easy_handle != NULL);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (auto& it : m_sessions)
+ {
+ if (!it.m_busy)
+ {
+ /* allow reuse of requester is trying to connect to same host */
+ /* curl will take care of any differences in username/password */
+ if (it.m_protocol.compare(protocol) == 0 && it.m_hostname.compare(hostname) == 0)
+ {
+ it.m_busy = true;
+ if (easy_handle)
+ {
+ if (!it.m_easy)
+ it.m_easy = easy_init();
+
+ *easy_handle = it.m_easy;
+ }
+
+ if (multi_handle)
+ {
+ if (!it.m_multi)
+ it.m_multi = multi_init();
+
+ *multi_handle = it.m_multi;
+ }
+
+ return;
+ }
+ }
+ }
+
+ SSession session = {};
+ session.m_busy = true;
+ session.m_protocol = protocol;
+ session.m_hostname = hostname;
+
+ if (easy_handle)
+ {
+ session.m_easy = easy_init();
+ *easy_handle = session.m_easy;
+ }
+
+ if (multi_handle)
+ {
+ session.m_multi = multi_init();
+ *multi_handle = session.m_multi;
+ }
+
+ m_sessions.push_back(session);
+
+ CLog::Log(LOGDEBUG, "{} - Created session to {}://{}", __FUNCTION__, protocol, hostname);
+}
+
+void DllLibCurlGlobal::easy_release(CURL_HANDLE** easy_handle, CURLM** multi_handle)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CURL_HANDLE* easy = NULL;
+ CURLM* multi = NULL;
+
+ if (easy_handle)
+ {
+ easy = *easy_handle;
+ *easy_handle = NULL;
+ }
+
+ if (multi_handle)
+ {
+ multi = *multi_handle;
+ *multi_handle = NULL;
+ }
+
+ for (auto& it : m_sessions)
+ {
+ if (it.m_easy == easy && (multi == nullptr || it.m_multi == multi))
+ {
+ /* reset session so next caller doesn't reuse options, only connections */
+ /* will reset verbose too so it won't print that it closed connections on cleanup*/
+ easy_reset(easy);
+ it.m_busy = false;
+ it.m_idletimestamp = std::chrono::steady_clock::now();
+ return;
+ }
+ }
+}
+
+CURL_HANDLE* DllLibCurlGlobal::easy_duphandle(CURL_HANDLE* easy_handle)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& it : m_sessions)
+ {
+ if (it.m_easy == easy_handle)
+ {
+ SSession session = it;
+ session.m_easy = DllLibCurl::easy_duphandle(easy_handle);
+ m_sessions.push_back(session);
+ return session.m_easy;
+ }
+ }
+ return DllLibCurl::easy_duphandle(easy_handle);
+}
+
+void DllLibCurlGlobal::easy_duplicate(CURL_HANDLE* easy,
+ const CURLM* multi,
+ CURL_HANDLE** easy_out,
+ CURLM** multi_out)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (easy_out && easy)
+ *easy_out = DllLibCurl::easy_duphandle(easy);
+
+ if (multi_out && multi)
+ *multi_out = DllLibCurl::multi_init();
+
+ for (const auto& it : m_sessions)
+ {
+ if (it.m_easy == easy)
+ {
+ SSession session = it;
+ if (easy_out && easy)
+ session.m_easy = *easy_out;
+ else
+ session.m_easy = NULL;
+
+ if (multi_out && multi)
+ session.m_multi = *multi_out;
+ else
+ session.m_multi = NULL;
+
+ m_sessions.push_back(session);
+ return;
+ }
+ }
+}
+} // namespace XCURL
diff --git a/xbmc/filesystem/DllLibCurl.h b/xbmc/filesystem/DllLibCurl.h
new file mode 100644
index 0000000..760f28d
--- /dev/null
+++ b/xbmc/filesystem/DllLibCurl.h
@@ -0,0 +1,106 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+#include <stdio.h>
+#include <string>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <type_traits>
+#include <vector>
+
+#define CURL CURL_HANDLE
+#include <curl/curl.h>
+#undef CURL
+
+namespace XCURL
+{
+
+class DllLibCurl
+{
+public:
+ virtual ~DllLibCurl() = default;
+
+ CURLcode global_init(long flags);
+ void global_cleanup();
+ CURL_HANDLE* easy_init();
+ template<typename... Args>
+ CURLcode easy_setopt(CURL_HANDLE* handle, CURLoption option, Args... args)
+ {
+ return curl_easy_setopt(handle, option, std::forward<Args>(args)...);
+ }
+ CURLcode easy_perform(CURL_HANDLE* handle);
+ CURLcode easy_pause(CURL_HANDLE* handle, int bitmask);
+ void easy_reset(CURL_HANDLE* handle);
+ template<typename... Args>
+ CURLcode easy_getinfo(CURL_HANDLE* curl, CURLINFO info, Args... args)
+ {
+ return curl_easy_getinfo(curl, info, std::forward<Args>(args)...);
+ }
+ void easy_cleanup(CURL_HANDLE* handle);
+ virtual CURL_HANDLE* easy_duphandle(CURL_HANDLE* handle);
+ CURLM* multi_init(void);
+ CURLMcode multi_add_handle(CURLM* multi_handle, CURL_HANDLE* easy_handle);
+ CURLMcode multi_perform(CURLM* multi_handle, int* running_handles);
+ CURLMcode multi_remove_handle(CURLM* multi_handle, CURL_HANDLE* easy_handle);
+ CURLMcode multi_fdset(CURLM* multi_handle,
+ fd_set* read_fd_set,
+ fd_set* write_fd_set,
+ fd_set* exc_fd_set,
+ int* max_fd);
+ CURLMcode multi_timeout(CURLM* multi_handle, long* timeout);
+ CURLMsg* multi_info_read(CURLM* multi_handle, int* msgs_in_queue);
+ CURLMcode multi_cleanup(CURLM* handle);
+ curl_slist* slist_append(curl_slist* list, const char* to_append);
+ void slist_free_all(curl_slist* list);
+ const char* easy_strerror(CURLcode code);
+};
+
+class DllLibCurlGlobal : public DllLibCurl
+{
+public:
+ DllLibCurlGlobal();
+ ~DllLibCurlGlobal();
+ /* extend interface with buffered functions */
+ void easy_acquire(const char* protocol,
+ const char* hostname,
+ CURL_HANDLE** easy_handle,
+ CURLM** multi_handle);
+ void easy_release(CURL_HANDLE** easy_handle, CURLM** multi_handle);
+ void easy_duplicate(CURL_HANDLE* easy,
+ const CURLM* multi,
+ CURL_HANDLE** easy_out,
+ CURLM** multi_out);
+ CURL_HANDLE* easy_duphandle(CURL_HANDLE* easy_handle) override;
+ void CheckIdle();
+
+ /* overloaded load and unload with reference counter */
+
+ /* structure holding a session info */
+ typedef struct SSession
+ {
+ std::chrono::time_point<std::chrono::steady_clock>
+ m_idletimestamp; // timestamp of when this object when idle
+ std::string m_protocol;
+ std::string m_hostname;
+ bool m_busy;
+ CURL_HANDLE* m_easy;
+ CURLM* m_multi;
+ } SSession;
+
+ typedef std::vector<SSession> VEC_CURLSESSIONS;
+
+ VEC_CURLSESSIONS m_sessions;
+ CCriticalSection m_critSection;
+};
+} // namespace XCURL
+
+extern XCURL::DllLibCurlGlobal g_curlInterface;
diff --git a/xbmc/filesystem/EventsDirectory.cpp b/xbmc/filesystem/EventsDirectory.cpp
new file mode 100644
index 0000000..d533475
--- /dev/null
+++ b/xbmc/filesystem/EventsDirectory.cpp
@@ -0,0 +1,69 @@
+/*
+ * 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 "EventsDirectory.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "events/EventLog.h"
+#include "utils/StringUtils.h"
+
+using namespace XFILE;
+
+bool CEventsDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ items.ClearProperties();
+ items.SetContent("events");
+
+ auto log = CServiceBroker::GetEventLog();
+ Events events;
+
+ std::string hostname = url.GetHostName();
+ if (hostname.empty())
+ events = log->Get();
+ else
+ {
+ bool includeHigherLevels = false;
+ // check if we should only retrieve events from a specific level or
+ // also from all higher levels
+ if (StringUtils::EndsWith(hostname, "+"))
+ {
+ includeHigherLevels = true;
+
+ // remove the "+" from the end of the hostname
+ hostname = hostname.substr(0, hostname.size() - 1);
+ }
+
+ EventLevel level = CEventLog::EventLevelFromString(hostname);
+
+ // get the events of the specified level(s)
+ events = log->Get(level, includeHigherLevels);
+ }
+
+ for (const auto& eventItem : events)
+ items.Add(EventToFileItem(eventItem));
+
+ return true;
+}
+
+std::shared_ptr<CFileItem> CEventsDirectory::EventToFileItem(
+ const std::shared_ptr<const IEvent>& eventItem)
+{
+ if (!eventItem)
+ return CFileItemPtr();
+
+ CFileItemPtr item(new CFileItem(eventItem));
+
+ item->SetProperty(PROPERTY_EVENT_IDENTIFIER, eventItem->GetIdentifier());
+ item->SetProperty(PROPERTY_EVENT_LEVEL, CEventLog::EventLevelToString(eventItem->GetLevel()));
+ item->SetProperty(PROPERTY_EVENT_DESCRIPTION, eventItem->GetDescription());
+
+ return item;
+}
diff --git a/xbmc/filesystem/EventsDirectory.h b/xbmc/filesystem/EventsDirectory.h
new file mode 100644
index 0000000..adb26c3
--- /dev/null
+++ b/xbmc/filesystem/EventsDirectory.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "filesystem/IDirectory.h"
+
+#include <memory>
+
+class CFileItem;
+class CFileItemList;
+class IEvent;
+
+#define PROPERTY_EVENT_IDENTIFIER "Event.ID"
+#define PROPERTY_EVENT_LEVEL "Event.Level"
+#define PROPERTY_EVENT_DESCRIPTION "Event.Description"
+
+namespace XFILE
+{
+ class CEventsDirectory : public IDirectory
+ {
+ public:
+ CEventsDirectory() = default;
+ ~CEventsDirectory() override = default;
+
+ // implementations of IDirectory
+ bool GetDirectory(const CURL& url, CFileItemList& items) override;
+ bool Create(const CURL& url) override { return true; }
+ bool Exists(const CURL& url) override { return true; }
+ bool AllowAll() const override { return true; }
+
+ static std::shared_ptr<CFileItem> EventToFileItem(
+ const std::shared_ptr<const IEvent>& activity);
+ };
+}
diff --git a/xbmc/filesystem/FTPDirectory.cpp b/xbmc/filesystem/FTPDirectory.cpp
new file mode 100644
index 0000000..46d3d7d
--- /dev/null
+++ b/xbmc/filesystem/FTPDirectory.cpp
@@ -0,0 +1,113 @@
+/*
+ * 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 "FTPDirectory.h"
+
+#include "CurlFile.h"
+#include "FTPParse.h"
+#include "FileItem.h"
+#include "URL.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <climits>
+
+using namespace XFILE;
+
+CFTPDirectory::CFTPDirectory(void) = default;
+CFTPDirectory::~CFTPDirectory(void) = default;
+
+bool CFTPDirectory::GetDirectory(const CURL& url2, CFileItemList &items)
+{
+ CCurlFile reader;
+
+ CURL url(url2);
+
+ std::string path = url.GetFileName();
+ if( !path.empty() && !StringUtils::EndsWith(path, "/") )
+ {
+ path += "/";
+ url.SetFileName(path);
+ }
+
+ if (!reader.Open(url))
+ return false;
+
+ bool serverNotUseUTF8 = url.GetProtocolOption("utf8") == "0";
+
+ char buffer[MAX_PATH + 1024];
+ while( reader.ReadString(buffer, sizeof(buffer)) )
+ {
+ std::string strBuffer = buffer;
+
+ StringUtils::RemoveCRLF(strBuffer);
+
+ CFTPParse parse;
+ if (parse.FTPParse(strBuffer))
+ {
+ if( parse.getName().length() == 0 )
+ continue;
+
+ if( parse.getFlagtrycwd() == 0 && parse.getFlagtryretr() == 0 )
+ continue;
+
+ /* buffer name */
+ std::string name;
+ name.assign(parse.getName());
+
+ if( name == ".." || name == "." )
+ continue;
+
+ // server returned filename could in utf8 or non-utf8 encoding
+ // we need utf8, so convert it to utf8 anyway
+ g_charsetConverter.unknownToUTF8(name);
+
+ // convert got empty result, ignore it
+ if (name.empty())
+ continue;
+
+ if (serverNotUseUTF8 || name != parse.getName())
+ // non-utf8 name path, tag it with protocol option.
+ // then we can talk to server with the same encoding in CurlFile according to this tag.
+ url.SetProtocolOption("utf8", "0");
+ else
+ url.RemoveProtocolOption("utf8");
+
+ CFileItemPtr pItem(new CFileItem(name));
+
+ pItem->m_bIsFolder = parse.getFlagtrycwd() != 0;
+ std::string filePath = path + name;
+ if (pItem->m_bIsFolder)
+ URIUtils::AddSlashAtEnd(filePath);
+
+ /* qualify the url with host and all */
+ url.SetFileName(filePath);
+ pItem->SetPath(url.Get());
+
+ pItem->m_dwSize = parse.getSize();
+ pItem->m_dateTime=parse.getTime();
+
+ items.Add(pItem);
+ }
+ }
+
+ return true;
+}
+
+bool CFTPDirectory::Exists(const CURL& url)
+{
+ // make sure ftp dir ends with slash,
+ // curl need to known it's a dir to check ftp directory existence.
+ std::string file = url.Get();
+ URIUtils::AddSlashAtEnd(file);
+
+ CCurlFile ftp;
+ CURL url2(file);
+ return ftp.Exists(url2);
+}
diff --git a/xbmc/filesystem/FTPDirectory.h b/xbmc/filesystem/FTPDirectory.h
new file mode 100644
index 0000000..0a0c6db
--- /dev/null
+++ b/xbmc/filesystem/FTPDirectory.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "IDirectory.h"
+
+namespace XFILE
+{
+ class CFTPDirectory : public IDirectory
+ {
+ public:
+ CFTPDirectory(void);
+ ~CFTPDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool Exists(const CURL& url) override;
+ private:
+ };
+}
+
diff --git a/xbmc/filesystem/FTPParse.cpp b/xbmc/filesystem/FTPParse.cpp
new file mode 100644
index 0000000..fb5e035
--- /dev/null
+++ b/xbmc/filesystem/FTPParse.cpp
@@ -0,0 +1,504 @@
+/*
+ * 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 "FTPParse.h"
+
+#include <cmath>
+
+#include <pcrecpp.h>
+
+CFTPParse::CFTPParse()
+{
+ m_flagtrycwd = 0;
+ m_flagtryretr = 0;
+ m_size = 0;
+}
+
+std::string CFTPParse::getName()
+{
+ return m_name;
+}
+
+int CFTPParse::getFlagtrycwd()
+{
+ return m_flagtrycwd;
+}
+
+int CFTPParse::getFlagtryretr()
+{
+ return m_flagtryretr;
+}
+
+uint64_t CFTPParse::getSize()
+{
+ return m_size;
+}
+
+time_t CFTPParse::getTime()
+{
+ return m_time;
+}
+
+void CFTPParse::setTime(const std::string& str)
+{
+ /* Variables used to capture patterns via the regexes */
+ std::string month;
+ std::string day;
+ std::string year;
+ std::string hour;
+ std::string minute;
+ std::string second;
+ std::string am_or_pm;
+
+ /* time struct used to set the time_t variable */
+ struct tm time_struct = {};
+
+ /* Regex to read Unix, NetWare and NetPresenz time format */
+ pcrecpp::RE unix_re("^([A-Za-z]{3})" // month
+ "\\s+(\\d{1,2})" // day of month
+ "\\s+([:\\d]{4,5})$" // time of day or year
+ );
+
+ /* Regex to read MultiNet time format */
+ pcrecpp::RE multinet_re("^(\\d{1,2})" // day of month
+ "-([A-Za-z]{3})" // month
+ "-(\\d{4})" // year
+ "\\s+(\\d{2})" // hour
+ ":(\\d{2})" // minute
+ "(:(\\d{2}))?$" // second
+ );
+
+ /* Regex to read MSDOS time format */
+ pcrecpp::RE msdos_re("^(\\d{2})" // month
+ "-(\\d{2})" // day of month
+ "-(\\d{2})" // year
+ "\\s+(\\d{2})" // hour
+ ":(\\d{2})" // minute
+ "([AP]M)$" // AM or PM
+ );
+
+ if (unix_re.FullMatch(str, &month, &day, &year))
+ {
+ /* set the month */
+ if (pcrecpp::RE("jan",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 0;
+ else if (pcrecpp::RE("feb",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 1;
+ else if (pcrecpp::RE("mar",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 2;
+ else if (pcrecpp::RE("apr",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 3;
+ else if (pcrecpp::RE("may",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 4;
+ else if (pcrecpp::RE("jun",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 5;
+ else if (pcrecpp::RE("jul",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 6;
+ else if (pcrecpp::RE("aug",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 7;
+ else if (pcrecpp::RE("sep",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 8;
+ else if (pcrecpp::RE("oct",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 9;
+ else if (pcrecpp::RE("nov",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 10;
+ else if (pcrecpp::RE("dec",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 11;
+
+ /* set the day of the month */
+ time_struct.tm_mday = atoi(day.c_str());
+
+ time_t t = time(NULL);
+ struct tm *current_time;
+#ifdef LOCALTIME_R
+ struct tm result = {};
+ current_time = localtime_r(&t, &result);
+#else
+ current_time = localtime(&t);
+#endif
+ if (pcrecpp::RE("(\\d{2}):(\\d{2})").FullMatch(year, &hour, &minute))
+ {
+ /* set the hour and minute */
+ time_struct.tm_hour = atoi(hour.c_str());
+ time_struct.tm_min = atoi(minute.c_str());
+
+ /* set the year */
+ if ((current_time->tm_mon - time_struct.tm_mon < 0) ||
+ ((current_time->tm_mon - time_struct.tm_mon == 0) &&
+ (current_time->tm_mday - time_struct.tm_mday < 0)))
+ time_struct.tm_year = current_time->tm_year - 1;
+ else
+ time_struct.tm_year = current_time->tm_year;
+ }
+ else
+ {
+ /* set the year */
+ time_struct.tm_year = atoi(year.c_str()) - 1900;
+ }
+
+ /* set the day of the week */
+ time_struct.tm_wday = getDayOfWeek(time_struct.tm_mon + 1,
+ time_struct.tm_mday,
+ time_struct.tm_year + 1900);
+ }
+ else if (multinet_re.FullMatch(str, &day, &month, &year,
+ &hour, &minute, (void*)NULL, &second))
+ {
+ /* set the month */
+ if (pcrecpp::RE("jan",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 0;
+ else if (pcrecpp::RE("feb",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 1;
+ else if (pcrecpp::RE("mar",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 2;
+ else if (pcrecpp::RE("apr",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 3;
+ else if (pcrecpp::RE("may",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 4;
+ else if (pcrecpp::RE("jun",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 5;
+ else if (pcrecpp::RE("jul",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 6;
+ else if (pcrecpp::RE("aug",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 7;
+ else if (pcrecpp::RE("sep",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 8;
+ else if (pcrecpp::RE("oct",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 9;
+ else if (pcrecpp::RE("nov",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 10;
+ else if (pcrecpp::RE("dec",
+ pcrecpp::RE_Options().set_caseless(true)).FullMatch(month))
+ time_struct.tm_mon = 11;
+
+ /* set the day of the month and year */
+ time_struct.tm_mday = atoi(day.c_str());
+ time_struct.tm_year = atoi(year.c_str()) - 1900;
+
+ /* set the hour and minute */
+ time_struct.tm_hour = atoi(hour.c_str());
+ time_struct.tm_min = atoi(minute.c_str());
+
+ /* set the second if given*/
+ if (second.length() > 0)
+ time_struct.tm_sec = atoi(second.c_str());
+
+ /* set the day of the week */
+ time_struct.tm_wday = getDayOfWeek(time_struct.tm_mon + 1,
+ time_struct.tm_mday,
+ time_struct.tm_year + 1900);
+ }
+ else if (msdos_re.FullMatch(str, &month, &day,
+ &year, &hour, &minute, &am_or_pm))
+ {
+ /* set the month and the day of the month */
+ time_struct.tm_mon = atoi(month.c_str()) - 1;
+ time_struct.tm_mday = atoi(day.c_str());
+
+ /* set the year */
+ time_struct.tm_year = atoi(year.c_str());
+ if (time_struct.tm_year < 70)
+ time_struct.tm_year += 100;
+
+ /* set the hour */
+ time_struct.tm_hour = atoi(hour.c_str());
+ if (time_struct.tm_hour == 12)
+ time_struct.tm_hour -= 12;
+ if (pcrecpp::RE("PM").FullMatch(am_or_pm))
+ time_struct.tm_hour += 12;
+
+ /* set the minute */
+ time_struct.tm_min = atoi(minute.c_str());
+
+ /* set the day of the week */
+ time_struct.tm_wday = getDayOfWeek(time_struct.tm_mon + 1,
+ time_struct.tm_mday,
+ time_struct.tm_year + 1900);
+ }
+
+ /* now set m_time */
+ m_time = mktime(&time_struct);
+}
+
+int CFTPParse::getDayOfWeek(int month, int date, int year)
+{
+ /* Here we use the Doomsday rule to calculate the day of the week */
+
+ /* First determine the anchor day */
+ int anchor;
+ if (year >= 1900 && year < 2000)
+ anchor = 3;
+ else if (year >= 2000 && year < 2100)
+ anchor = 2;
+ else if (year >= 2100 && year < 2200)
+ anchor = 0;
+ else if (year >= 2200 && year < 2300)
+ anchor = 5;
+ else // must have been given an invalid year :-/
+ return -1;
+
+ /* Now determine the doomsday */
+ int y = year % 100;
+ int dday =
+ ((y/12 + (y % 12) + ((y % 12)/4)) % 7) + anchor;
+
+ /* Determine if the given year is a leap year */
+ int leap_year = 0;
+ if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
+ leap_year = 1;
+
+ /* Now select a doomsday for the given month */
+ int mdday = 1;
+ if (month == 1)
+ {
+ if (leap_year)
+ mdday = 4;
+ else
+ mdday = 3;
+ }
+ if (month == 2)
+ {
+ if (leap_year)
+ mdday = 1;
+ else
+ mdday = 7;
+ }
+ if (month == 3)
+ mdday = 7;
+ if (month == 4)
+ mdday = 4;
+ if (month == 5)
+ mdday = 9;
+ if (month == 6)
+ mdday = 6;
+ if (month == 7)
+ mdday = 11;
+ if (month == 8)
+ mdday = 8;
+ if (month == 9)
+ mdday = 5;
+ if (month == 10)
+ mdday = 10;
+ if (month == 11)
+ mdday = 9;
+ if (month == 12)
+ mdday = 12;
+
+ /* Now calculate the day of the week
+ * Sunday = 0, Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4,
+ * Friday = 5, Saturday = 6.
+ */
+ int day_of_week = ((date - 1) % 7) - ((mdday - 1) % 7) + dday;
+ if (day_of_week >= 7)
+ day_of_week -= 7;
+
+ return day_of_week;
+}
+
+int CFTPParse::FTPParse(const std::string& str)
+{
+ /* Various variable to capture patterns via the regexes */
+ std::string permissions;
+ std::string link_count;
+ std::string owner;
+ std::string group;
+ std::string size;
+ std::string date;
+ std::string name;
+ std::string type;
+ std::string stuff;
+ std::string facts;
+ std::string version;
+ std::string file_id;
+
+ /* Regex for standard Unix listing formats */
+ pcrecpp::RE unix_re("^([-bcdlps])" // type
+ "([-rwxXsStT]{9})" // permissions
+ "\\s+(\\d+)" // hard link count
+ "\\s+(\\w+)" // owner
+ "\\s+(\\w+)" // group
+ "\\s+(\\d+)" // size
+ "\\s+([A-Za-z]{3}\\s+\\d{1,2}\\s+[:\\d]{4,5})" // modification date
+ "\\s+(.+)$" // name
+ );
+
+ /* Regex for NetWare listing formats */
+ /* See http://www.novell.com/documentation/oes/ftp_enu/data/a3ep22p.html#fbhbaijf */
+ pcrecpp::RE netware_re("^([-d])" // type
+ "\\s+(\\[[-SRWCIEMFA]{8}\\])" // rights
+ "\\s+(\\w+)" // owner
+ "\\s+(\\d+)" // size
+ "\\s+([A-Za-z]{3}\\s+\\d{1,2}\\s+[:\\d]{4,5})" // time
+ "\\s+(.+)$" // name
+ );
+
+ /* Regex for NetPresenz */
+ /* See http://files.stairways.com/other/ftp-list-specs-info.txt */
+ /* Here we will capture permissions and size if given */
+ pcrecpp::RE netpresenz_re("^([-dl])" // type
+ "([-rwx]{9}|)" // permissions
+ "\\s+(.*)" // stuff
+ "\\s+(\\d+|)" // size
+ "\\s+([A-Za-z]{3}\\s+\\d{1,2}\\s+[:\\d]{4,5})" // modification date
+ "\\s+(.+)$" // name
+ );
+
+ /* Regex for EPLF */
+ /* See http://cr.yp.to/ftp/list/eplf.html */
+ /* SAVE: "(/,|r,|s\\d+,|m\\d+,|i[\\d!#@$%^&*()]+(\\.[\\d!#@$%^&*()]+|),)+" */
+ pcrecpp::RE eplf_re("^\\+" // initial "plus" sign
+ "([^\\s]+)" // facts
+ "\\s(.+)$" // name
+ );
+
+ /* Regex for MultiNet */
+ /* Best documentation found was
+ * http://www-sld.slac.stanford.edu/SLDWWW/workbook/vms_files.html */
+ pcrecpp::RE multinet_re("^([^;]+)" // name
+ ";(\\d+)" // version
+ "\\s+([\\d/]+)" // file id
+ "\\s+(\\d{1,2}-[A-Za-z]{3}-\\d{4}\\s+\\d{2}:\\d{2}(:\\d{2})?)" // date
+ "\\s+\\[([^\\]]+)\\]" // owner,group
+ "\\s+\\(([^\\)]+)\\)$" // permissions
+ );
+
+ /* Regex for MSDOS */
+ pcrecpp::RE msdos_re("^(\\d{2}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}[AP]M)" // date
+ "\\s+(<DIR>|[\\d]+)" // dir or size
+ "\\s+(.+)$" // name
+ );
+
+ if (unix_re.FullMatch(str, &type, &permissions, &link_count, &owner, &group, &size, &date, &name))
+ {
+ m_name = name;
+ m_size = (uint64_t)strtod(size.c_str(), NULL);
+ if (pcrecpp::RE("d").FullMatch(type))
+ m_flagtrycwd = 1;
+ if (pcrecpp::RE("-").FullMatch(type))
+ m_flagtryretr = 1;
+ if (pcrecpp::RE("l").FullMatch(type))
+ {
+ m_flagtrycwd = m_flagtryretr = 1;
+ // handle symlink
+ size_t found = m_name.find(" -> ");
+ if (found != std::string::npos)
+ m_name = m_name.substr(0, found);
+ }
+ setTime(date);
+
+ return 1;
+ }
+ if (netware_re.FullMatch(str, &type, &permissions, &owner, &size, &date, &name))
+ {
+ m_name = name;
+ m_size = (uint64_t)strtod(size.c_str(), NULL);
+ if (pcrecpp::RE("d").FullMatch(type))
+ m_flagtrycwd = 1;
+ if (pcrecpp::RE("-").FullMatch(type))
+ m_flagtryretr = 1;
+ setTime(date);
+
+ return 1;
+ }
+ if (netpresenz_re.FullMatch(str, &type, &permissions, &stuff, &size, &date, &name))
+ {
+ m_name = name;
+ m_size = (uint64_t)strtod(size.c_str(), NULL);
+ if (pcrecpp::RE("d").FullMatch(type))
+ m_flagtrycwd = 1;
+ if (pcrecpp::RE("-").FullMatch(type))
+ m_flagtryretr = 1;
+ if (pcrecpp::RE("l").FullMatch(type))
+ {
+ m_flagtrycwd = m_flagtryretr = 1;
+ // handle symlink
+ size_t found = m_name.find(" -> ");
+ if (found != std::string::npos)
+ m_name = m_name.substr(0, found);
+ }
+ setTime(date);
+
+ return 1;
+ }
+ if (eplf_re.FullMatch(str, &facts, &name))
+ {
+ /* Get the type, size, and date from the facts */
+ pcrecpp::RE("(\\+|,)(r|/),").PartialMatch(facts, (void*)NULL, &type);
+ pcrecpp::RE("(\\+|,)s(\\d+),").PartialMatch(facts, (void*)NULL, &size);
+ pcrecpp::RE("(\\+|,)m(\\d+),").PartialMatch(facts, (void*)NULL, &date);
+
+ m_name = name;
+ m_size = (uint64_t)strtod(size.c_str(), NULL);
+ if (pcrecpp::RE("/").FullMatch(type))
+ m_flagtrycwd = 1;
+ if (pcrecpp::RE("r").FullMatch(type))
+ m_flagtryretr = 1;
+ /* eplf stores the date in time_t format already */
+ m_time = atoi(date.c_str());
+
+ return 1;
+ }
+ if (multinet_re.FullMatch(str, &name, &version, &file_id, &date, (void*)NULL, &owner, &permissions))
+ {
+ if (pcrecpp::RE("\\.DIR$").PartialMatch(name))
+ {
+ name.resize(name.size() - 4);
+ m_flagtrycwd = 1;
+ }
+ else
+ m_flagtryretr = 1;
+ m_name = name;
+ setTime(date);
+ /* Multinet doesn't provide a size */
+ m_size = 0;
+
+ return 1;
+ }
+ if (msdos_re.FullMatch(str, &date, &size, &name))
+ {
+ m_name = name;
+ if (pcrecpp::RE("<DIR>").FullMatch(size))
+ {
+ m_flagtrycwd = 1;
+ m_size = 0;
+ }
+ else
+ {
+ m_flagtryretr = 1;
+ m_size = (uint64_t)strtod(size.c_str(), NULL);
+ }
+ setTime(date);
+
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/xbmc/filesystem/FTPParse.h b/xbmc/filesystem/FTPParse.h
new file mode 100644
index 0000000..5f56981
--- /dev/null
+++ b/xbmc/filesystem/FTPParse.h
@@ -0,0 +1,33 @@
+/*
+ * 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 <ctime>
+#include <stdint.h>
+#include <string>
+
+class CFTPParse
+{
+public:
+ CFTPParse();
+ int FTPParse(const std::string& str);
+ std::string getName();
+ int getFlagtrycwd();
+ int getFlagtryretr();
+ uint64_t getSize();
+ time_t getTime();
+private:
+ std::string m_name; // not necessarily 0-terminated
+ int m_flagtrycwd; // 0 if cwd is definitely pointless, 1 otherwise
+ int m_flagtryretr; // 0 if retr is definitely pointless, 1 otherwise
+ uint64_t m_size; // number of octets
+ time_t m_time = 0; // modification time
+ void setTime(const std::string& str); // Method used to set m_time from a string
+ int getDayOfWeek(int month, int date, int year); // Method to get day of week
+};
diff --git a/xbmc/filesystem/FavouritesDirectory.cpp b/xbmc/filesystem/FavouritesDirectory.cpp
new file mode 100644
index 0000000..fe46874
--- /dev/null
+++ b/xbmc/filesystem/FavouritesDirectory.cpp
@@ -0,0 +1,43 @@
+/*
+ * 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 "FavouritesDirectory.h"
+
+#include "Directory.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "favourites/FavouritesService.h"
+#include "profiles/ProfileManager.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+
+namespace XFILE
+{
+
+bool CFavouritesDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ items.Clear();
+ CServiceBroker::GetFavouritesService().GetAll(items);
+ return true;
+}
+
+bool CFavouritesDirectory::Exists(const CURL& url)
+{
+ if (url.IsProtocol("favourites"))
+ {
+ if (CFileUtils::Exists("special://xbmc/system/favourites.xml"))
+ return true;
+
+ const std::string favouritesXml = URIUtils::AddFileToFolder(m_profileManager->GetProfileUserDataFolder(), "favourites.xml");
+
+ return CFileUtils::Exists(favouritesXml);
+ }
+
+ return XFILE::CDirectory::Exists(url);
+}
+} // namespace XFILE
diff --git a/xbmc/filesystem/FavouritesDirectory.h b/xbmc/filesystem/FavouritesDirectory.h
new file mode 100644
index 0000000..fe7d5b6
--- /dev/null
+++ b/xbmc/filesystem/FavouritesDirectory.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "IDirectory.h"
+
+class CFileItemList;
+class CFileItem;
+
+namespace XFILE
+{
+
+ class CFavouritesDirectory : public IDirectory
+ {
+ public:
+ CFavouritesDirectory() = default;
+
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool Exists(const CURL& url) override;
+ };
+
+}
diff --git a/xbmc/filesystem/File.cpp b/xbmc/filesystem/File.cpp
new file mode 100644
index 0000000..9e8a266
--- /dev/null
+++ b/xbmc/filesystem/File.cpp
@@ -0,0 +1,1224 @@
+/*
+ * Copyright (c) 2002 Frodo
+ * Portions Copyright (c) by the authors of ffmpeg and xvid
+ * Copyright (C) 2002-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 "File.h"
+
+#include "Directory.h"
+#include "DirectoryCache.h"
+#include "FileCache.h"
+#include "FileFactory.h"
+#include "IFile.h"
+#include "PasswordManager.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "commons/Exception.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/BitstreamStats.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+//////////////////////////////////////////////////////////////////////
+// Construction/Destruction
+//////////////////////////////////////////////////////////////////////
+#ifndef __GNUC__
+#pragma warning (disable:4244)
+#endif
+
+//*********************************************************************************************
+CFile::CFile() = default;
+
+//*********************************************************************************************
+CFile::~CFile()
+{
+ Close();
+}
+
+//*********************************************************************************************
+
+bool CFile::Copy(const std::string& strFileName, const std::string& strDest, XFILE::IFileCallback* pCallback, void* pContext)
+{
+ const CURL pathToUrl(strFileName);
+ const CURL pathToUrlDest(strDest);
+ return Copy(pathToUrl, pathToUrlDest, pCallback, pContext);
+}
+
+bool CFile::Copy(const CURL& url2, const CURL& dest, XFILE::IFileCallback* pCallback, void* pContext)
+{
+ CFile file;
+
+ const std::string pathToUrl(dest.Get());
+ if (pathToUrl.empty())
+ return false;
+
+ // special case for zips - ignore caching
+ CURL url(url2);
+ if (StringUtils::StartsWith(url.Get(), "zip://") || URIUtils::IsInAPK(url.Get()))
+ url.SetOptions("?cache=no");
+ if (file.Open(url.Get(), READ_TRUNCATED | READ_CHUNKED))
+ {
+
+ CFile newFile;
+ if (URIUtils::IsHD(pathToUrl)) // create possible missing dirs
+ {
+ std::vector<std::string> tokens;
+ std::string strDirectory = URIUtils::GetDirectory(pathToUrl);
+ URIUtils::RemoveSlashAtEnd(strDirectory); // for the test below
+ if (!(strDirectory.size() == 2 && strDirectory[1] == ':'))
+ {
+ CURL url(strDirectory);
+ std::string pathsep;
+#ifndef TARGET_POSIX
+ pathsep = "\\";
+#else
+ pathsep = "/";
+#endif
+ // Try to use the recursive creation first, if it fails
+ // it might not be implemented for that subsystem so let's
+ // fall back to the old method in that case
+ if (!CDirectory::Create(url))
+ {
+ StringUtils::Tokenize(url.GetFileName(), tokens, pathsep);
+ std::string strCurrPath;
+ // Handle special
+ if (!url.GetProtocol().empty())
+ {
+ pathsep = "/";
+ strCurrPath += url.GetProtocol() + "://";
+ } // If the directory has a / at the beginning, don't forget it
+ else if (strDirectory[0] == pathsep[0])
+ strCurrPath += pathsep;
+
+ for (const std::string& iter : tokens)
+ {
+ strCurrPath += iter + pathsep;
+ CDirectory::Create(strCurrPath);
+ }
+ }
+ }
+ }
+ if (CFile::Exists(dest))
+ CFile::Delete(dest);
+ if (!newFile.OpenForWrite(dest, true)) // overwrite always
+ {
+ file.Close();
+ return false;
+ }
+
+ int iBufferSize = DetermineChunkSize(file.GetChunkSize(), 128 * 1024);
+
+ std::vector<char> buffer(iBufferSize);
+ ssize_t iRead, iWrite;
+
+ unsigned long long llFileSize = file.GetLength();
+ unsigned long long llPos = 0;
+
+ CStopWatch timer;
+ timer.StartZero();
+ float start = 0.0f;
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ while (true)
+ {
+ appPower->ResetScreenSaver();
+
+ iRead = file.Read(buffer.data(), buffer.size());
+ if (iRead == 0) break;
+ else if (iRead < 0)
+ {
+ CLog::Log(LOGERROR, "{} - Failed read from file {}", __FUNCTION__, url.GetRedacted());
+ llFileSize = (uint64_t)-1;
+ break;
+ }
+
+ /* write data and make sure we managed to write it all */
+ iWrite = 0;
+ while(iWrite < iRead)
+ {
+ ssize_t iWrite2 = newFile.Write(buffer.data() + iWrite, iRead - iWrite);
+ if(iWrite2 <=0)
+ break;
+ iWrite+=iWrite2;
+ }
+
+ if (iWrite != iRead)
+ {
+ CLog::Log(LOGERROR, "{} - Failed write to file {}", __FUNCTION__, dest.GetRedacted());
+ llFileSize = (uint64_t)-1;
+ break;
+ }
+
+ llPos += iRead;
+
+ // calculate the current and average speeds
+ float end = timer.GetElapsedSeconds();
+
+ if (pCallback && end - start > 0.5f && end)
+ {
+ start = end;
+
+ float averageSpeed = llPos / end;
+ int ipercent = 0;
+ if(llFileSize)
+ ipercent = 100 * llPos / llFileSize;
+
+ if(!pCallback->OnFileCallback(pContext, ipercent, averageSpeed))
+ {
+ CLog::Log(LOGERROR, "{} - User aborted copy", __FUNCTION__);
+ llFileSize = (uint64_t)-1;
+ break;
+ }
+ }
+ }
+
+ /* close both files */
+ newFile.Close();
+ file.Close();
+
+ /* verify that we managed to completed the file */
+ if (llFileSize && llPos != llFileSize)
+ {
+ CFile::Delete(dest);
+ return false;
+ }
+ return true;
+ }
+ return false;
+}
+
+//*********************************************************************************************
+
+bool CFile::CURLCreate(const std::string &url)
+{
+ m_curl.Parse(url);
+ return true;
+}
+
+bool CFile::CURLAddOption(XFILE::CURLOPTIONTYPE type, const char* name, const char * value)
+{
+ switch (type){
+ case XFILE::CURL_OPTION_CREDENTIALS:
+ {
+ m_curl.SetUserName(name);
+ m_curl.SetPassword(value);
+ break;
+ }
+ case XFILE::CURL_OPTION_PROTOCOL:
+ case XFILE::CURL_OPTION_HEADER:
+ {
+ m_curl.SetProtocolOption(name, value);
+ break;
+ }
+ case XFILE::CURL_OPTION_OPTION:
+ {
+ m_curl.SetOption(name, value);
+ break;
+ }
+ default:
+ return false;
+ }
+ return true;
+}
+
+bool CFile::CURLOpen(unsigned int flags)
+{
+ return Open(m_curl, flags);
+}
+
+bool CFile::Open(const std::string& strFileName, const unsigned int flags)
+{
+ const CURL pathToUrl(strFileName);
+ return Open(pathToUrl, flags);
+}
+
+bool CFile::Open(const CURL& file, const unsigned int flags)
+{
+ if (m_pFile)
+ {
+ if ((flags & READ_REOPEN) == 0)
+ {
+ CLog::Log(LOGERROR, "File::Open - already open: {}", file.GetRedacted());
+ return false;
+ }
+ else
+ {
+ return m_pFile->ReOpen(URIUtils::SubstitutePath(file));
+ }
+ }
+
+ m_flags = flags;
+ try
+ {
+ bool bPathInCache;
+
+ CURL url(URIUtils::SubstitutePath(file)), url2(url);
+
+ if (url2.IsProtocol("apk") || url2.IsProtocol("zip") )
+ url2.SetOptions("");
+
+ if (!g_directoryCache.FileExists(url2.Get(), bPathInCache) )
+ {
+ if (bPathInCache)
+ return false;
+ }
+
+ /*
+ * There are 5 buffer modes available (configurable in as.xml)
+ * 0) Buffer all internet filesystems (like 2 but additionally also ftp, webdav, etc.)
+ * 1) Buffer all filesystems (including local)
+ * 2) Only buffer true internet filesystems (streams) (http, etc.)
+ * 3) No buffer
+ * 4) Buffer all remote (non-local) filesystems
+ */
+ if (!(m_flags & READ_NO_CACHE))
+ {
+ const std::string pathToUrl(url.Get());
+ if (URIUtils::IsDVD(pathToUrl) || URIUtils::IsBluray(pathToUrl) ||
+ (m_flags & READ_AUDIO_VIDEO))
+ {
+ const unsigned int iCacheBufferMode =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheBufferMode;
+ if ((iCacheBufferMode == CACHE_BUFFER_MODE_INTERNET &&
+ URIUtils::IsInternetStream(pathToUrl, true)) ||
+ (iCacheBufferMode == CACHE_BUFFER_MODE_TRUE_INTERNET &&
+ URIUtils::IsInternetStream(pathToUrl, false)) ||
+ (iCacheBufferMode == CACHE_BUFFER_MODE_NETWORK &&
+ URIUtils::IsNetworkFilesystem(pathToUrl)) ||
+ (iCacheBufferMode == CACHE_BUFFER_MODE_ALL &&
+ (URIUtils::IsNetworkFilesystem(pathToUrl) || URIUtils::IsHD(pathToUrl))))
+ {
+ m_flags |= READ_CACHED;
+ }
+ }
+
+ if (m_flags & READ_CACHED)
+ {
+ m_pFile = std::make_unique<CFileCache>(m_flags);
+
+ if (!m_pFile)
+ return false;
+
+ return m_pFile->Open(url);
+ }
+ }
+
+ m_pFile.reset(CFileFactory::CreateLoader(url));
+
+ if (!m_pFile)
+ return false;
+
+ CURL authUrl(url);
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrl);
+
+ try
+ {
+ if (!m_pFile->Open(authUrl))
+ return false;
+ }
+ catch (CRedirectException *pRedirectEx)
+ {
+ // the file implementation decided this item should use a different implementation.
+ // the exception will contain the new implementation.
+ CLog::Log(LOGDEBUG, "File::Open - redirecting implementation for {}", file.GetRedacted());
+ if (pRedirectEx && pRedirectEx->m_pNewFileImp)
+ {
+ std::unique_ptr<CURL> pNewUrl(pRedirectEx->m_pNewUrl);
+ m_pFile.reset(pRedirectEx->m_pNewFileImp);
+ delete pRedirectEx;
+
+ if (pNewUrl)
+ {
+ CURL newAuthUrl(*pNewUrl);
+ if (CPasswordManager::GetInstance().IsURLSupported(newAuthUrl) && newAuthUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(newAuthUrl);
+
+ if (!m_pFile->Open(newAuthUrl))
+ return false;
+ }
+ else
+ {
+ if (!m_pFile->Open(authUrl))
+ return false;
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "File::Open - unknown exception when opening {}", file.GetRedacted());
+ return false;
+ }
+
+ if (m_pFile->GetChunkSize() && !(m_flags & READ_CHUNKED))
+ {
+ m_pBuffer = std::make_unique<CFileStreamBuffer>(0);
+ m_pBuffer->Attach(m_pFile.get());
+ }
+
+ if (m_flags & READ_BITRATE)
+ {
+ m_bitStreamStats = std::make_unique<BitstreamStats>();
+ m_bitStreamStats->Start();
+ }
+
+ return true;
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ CLog::Log(LOGERROR, "{} - Error opening {}", __FUNCTION__, file.GetRedacted());
+ return false;
+}
+
+bool CFile::OpenForWrite(const std::string& strFileName, bool bOverWrite)
+{
+ const CURL pathToUrl(strFileName);
+ return OpenForWrite(pathToUrl, bOverWrite);
+}
+
+bool CFile::OpenForWrite(const CURL& file, bool bOverWrite)
+{
+ try
+ {
+ CURL url = URIUtils::SubstitutePath(file);
+ CURL authUrl = url;
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrl);
+
+ m_pFile.reset(CFileFactory::CreateLoader(url));
+
+ if (m_pFile && m_pFile->OpenForWrite(authUrl, bOverWrite))
+ {
+ // add this file to our directory cache (if it's stored)
+ g_directoryCache.AddFile(url.Get());
+ return true;
+ }
+ return false;
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "{} - Unhandled exception opening {}", __FUNCTION__, file.GetRedacted());
+ }
+ CLog::Log(LOGERROR, "{} - Error opening {}", __FUNCTION__, file.GetRedacted());
+ return false;
+}
+
+int CFile::DetermineChunkSize(const int srcChunkSize, const int reqChunkSize)
+{
+ // Determine cache chunk size: if source chunk size is bigger than 1
+ // use source chunk size else use requested chunk size
+ return (srcChunkSize > 1 ? srcChunkSize : reqChunkSize);
+}
+
+bool CFile::Exists(const std::string& strFileName, bool bUseCache /* = true */)
+{
+ const CURL pathToUrl(strFileName);
+ return Exists(pathToUrl, bUseCache);
+}
+
+bool CFile::Exists(const CURL& file, bool bUseCache /* = true */)
+{
+ CURL url(URIUtils::SubstitutePath(file));
+ CURL authUrl = url;
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrl);
+
+ try
+ {
+ if (bUseCache)
+ {
+ bool bPathInCache;
+ if (g_directoryCache.FileExists(url.Get(), bPathInCache))
+ return true;
+ if (bPathInCache)
+ return false;
+ }
+
+ std::unique_ptr<IFile> pFile(CFileFactory::CreateLoader(url));
+ if (!pFile)
+ return false;
+
+ return pFile->Exists(authUrl);
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (CRedirectException *pRedirectEx)
+ {
+ // the file implementation decided this item should use a different implementation.
+ // the exception will contain the new implementation and optional a redirected URL.
+ CLog::Log(LOGDEBUG, "File::Exists - redirecting implementation for {}", file.GetRedacted());
+ if (pRedirectEx && pRedirectEx->m_pNewFileImp)
+ {
+ std::unique_ptr<IFile> pImp(pRedirectEx->m_pNewFileImp);
+ std::unique_ptr<CURL> pNewUrl(pRedirectEx->m_pNewUrl);
+ delete pRedirectEx;
+
+ if (pImp)
+ {
+ if (pNewUrl)
+ {
+ if (bUseCache)
+ {
+ bool bPathInCache;
+ if (g_directoryCache.FileExists(pNewUrl->Get(), bPathInCache))
+ return true;
+ if (bPathInCache)
+ return false;
+ }
+ CURL newAuthUrl = *pNewUrl;
+ if (CPasswordManager::GetInstance().IsURLSupported(newAuthUrl) && newAuthUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(newAuthUrl);
+
+ return pImp->Exists(newAuthUrl);
+ }
+ else
+ {
+ return pImp->Exists(authUrl);
+ }
+ }
+ }
+ }
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ CLog::Log(LOGERROR, "{} - Error checking for {}", __FUNCTION__, file.GetRedacted());
+ return false;
+}
+
+int CFile::Stat(struct __stat64 *buffer)
+{
+ if (!buffer)
+ return -1;
+
+ if (!m_pFile)
+ {
+ *buffer = {};
+ errno = ENOENT;
+ return -1;
+ }
+
+ return m_pFile->Stat(buffer);
+}
+
+int CFile::Stat(const std::string& strFileName, struct __stat64* buffer)
+{
+ const CURL pathToUrl(strFileName);
+ return Stat(pathToUrl, buffer);
+}
+
+int CFile::Stat(const CURL& file, struct __stat64* buffer)
+{
+ if (!buffer)
+ return -1;
+
+ CURL url(URIUtils::SubstitutePath(file));
+ CURL authUrl = url;
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrl);
+
+ try
+ {
+ std::unique_ptr<IFile> pFile(CFileFactory::CreateLoader(url));
+ if (!pFile)
+ return -1;
+ return pFile->Stat(authUrl, buffer);
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (CRedirectException *pRedirectEx)
+ {
+ // the file implementation decided this item should use a different implementation.
+ // the exception will contain the new implementation and optional a redirected URL.
+ CLog::Log(LOGDEBUG, "File::Stat - redirecting implementation for {}", file.GetRedacted());
+ if (pRedirectEx && pRedirectEx->m_pNewFileImp)
+ {
+ std::unique_ptr<IFile> pImp(pRedirectEx->m_pNewFileImp);
+ std::unique_ptr<CURL> pNewUrl(pRedirectEx->m_pNewUrl);
+ delete pRedirectEx;
+
+ if (pNewUrl)
+ {
+ if (pImp)
+ {
+ CURL newAuthUrl = *pNewUrl;
+ if (CPasswordManager::GetInstance().IsURLSupported(newAuthUrl) && newAuthUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(newAuthUrl);
+
+ if (!pImp->Stat(newAuthUrl, buffer))
+ {
+ return 0;
+ }
+ }
+ }
+ else
+ {
+ if (pImp.get() && !pImp->Stat(authUrl, buffer))
+ {
+ return 0;
+ }
+ }
+ }
+ }
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ CLog::Log(LOGERROR, "{} - Error statting {}", __FUNCTION__, file.GetRedacted());
+ return -1;
+}
+
+ssize_t CFile::Read(void *lpBuf, size_t uiBufSize)
+{
+ if (!m_pFile)
+ return -1;
+ if (lpBuf == NULL && uiBufSize != 0)
+ return -1;
+
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+ if (uiBufSize == 0)
+ {
+ // "test" read with zero size
+ // some VFSs don't handle correctly null buffer pointer
+ // provide valid buffer pointer for them
+ char dummy;
+ return m_pFile->Read(&dummy, 0);
+ }
+
+ if(m_pBuffer)
+ {
+ if(m_flags & READ_TRUNCATED)
+ {
+ const ssize_t nBytes = m_pBuffer->sgetn(
+ (char *)lpBuf, std::min<std::streamsize>((std::streamsize)uiBufSize,
+ m_pBuffer->in_avail()));
+ if (m_bitStreamStats && nBytes>0)
+ m_bitStreamStats->AddSampleBytes(nBytes);
+ return nBytes;
+ }
+ else
+ {
+ const ssize_t nBytes = m_pBuffer->sgetn((char*)lpBuf, uiBufSize);
+ if (m_bitStreamStats && nBytes>0)
+ m_bitStreamStats->AddSampleBytes(nBytes);
+ return nBytes;
+ }
+ }
+
+ try
+ {
+ if(m_flags & READ_TRUNCATED)
+ {
+ const ssize_t nBytes = m_pFile->Read(lpBuf, uiBufSize);
+ if (m_bitStreamStats && nBytes>0)
+ m_bitStreamStats->AddSampleBytes(nBytes);
+ return nBytes;
+ }
+ else
+ {
+ ssize_t done = 0;
+ while((uiBufSize-done) > 0)
+ {
+ const ssize_t curr = m_pFile->Read((char*)lpBuf+done, uiBufSize-done);
+ if (curr <= 0)
+ {
+ if (curr < 0 && done == 0)
+ return -1;
+
+ break;
+ }
+ done+=curr;
+ }
+ if (m_bitStreamStats && done > 0)
+ m_bitStreamStats->AddSampleBytes(done);
+ return done;
+ }
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__);
+ return -1;
+ }
+ return 0;
+}
+
+//*********************************************************************************************
+void CFile::Close()
+{
+ try
+ {
+ if (m_pFile)
+ m_pFile->Close();
+
+ m_pBuffer.reset();
+ m_pFile.reset();
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+}
+
+void CFile::Flush()
+{
+ try
+ {
+ if (m_pFile)
+ m_pFile->Flush();
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+}
+
+//*********************************************************************************************
+int64_t CFile::Seek(int64_t iFilePosition, int iWhence)
+{
+ if (!m_pFile)
+ return -1;
+
+ if (m_pBuffer)
+ {
+ if(iWhence == SEEK_CUR)
+ return m_pBuffer->pubseekoff(iFilePosition, std::ios_base::cur);
+ else if(iWhence == SEEK_END)
+ return m_pBuffer->pubseekoff(iFilePosition, std::ios_base::end);
+ else if(iWhence == SEEK_SET)
+ return m_pBuffer->pubseekoff(iFilePosition, std::ios_base::beg);
+ }
+
+ try
+ {
+ return m_pFile->Seek(iFilePosition, iWhence);
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ return -1;
+}
+
+//*********************************************************************************************
+int CFile::Truncate(int64_t iSize)
+{
+ if (!m_pFile)
+ return -1;
+
+ try
+ {
+ return m_pFile->Truncate(iSize);
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ return -1;
+}
+
+//*********************************************************************************************
+int64_t CFile::GetLength()
+{
+ try
+ {
+ if (m_pFile)
+ return m_pFile->GetLength();
+ return 0;
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ return 0;
+}
+
+//*********************************************************************************************
+int64_t CFile::GetPosition() const
+{
+ if (!m_pFile)
+ return -1;
+
+ if (m_pBuffer)
+ return m_pBuffer->pubseekoff(0, std::ios_base::cur);
+
+ try
+ {
+ return m_pFile->GetPosition();
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ return -1;
+}
+
+
+//*********************************************************************************************
+bool CFile::ReadString(char *szLine, int iLineLength)
+{
+ if (!m_pFile || !szLine)
+ return false;
+
+ if (m_pBuffer)
+ {
+ typedef CFileStreamBuffer::traits_type traits;
+ CFileStreamBuffer::int_type aByte = m_pBuffer->sgetc();
+
+ if(aByte == traits::eof())
+ return false;
+
+ while(iLineLength>0)
+ {
+ aByte = m_pBuffer->sbumpc();
+
+ if(aByte == traits::eof())
+ break;
+
+ if(aByte == traits::to_int_type('\n'))
+ {
+ if(m_pBuffer->sgetc() == traits::to_int_type('\r'))
+ m_pBuffer->sbumpc();
+ break;
+ }
+
+ if(aByte == traits::to_int_type('\r'))
+ {
+ if(m_pBuffer->sgetc() == traits::to_int_type('\n'))
+ m_pBuffer->sbumpc();
+ break;
+ }
+
+ *szLine = traits::to_char_type(aByte);
+ szLine++;
+ iLineLength--;
+ }
+
+ // if we have no space for terminating character we failed
+ if(iLineLength==0)
+ return false;
+
+ *szLine = 0;
+
+ return true;
+ }
+
+ try
+ {
+ return m_pFile->ReadString(szLine, iLineLength);
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ return false;
+}
+
+ssize_t CFile::Write(const void* lpBuf, size_t uiBufSize)
+{
+ if (!m_pFile)
+ return -1;
+ if (lpBuf == NULL && uiBufSize != 0)
+ return -1;
+
+ try
+ {
+ if (uiBufSize == 0 && lpBuf == NULL)
+ { // "test" write with zero size
+ // some VFSs don't handle correctly null buffer pointer
+ // provide valid buffer pointer for them
+ const char dummyBuf = 0;
+ return m_pFile->Write(&dummyBuf, 0);
+ }
+
+ return m_pFile->Write(lpBuf, uiBufSize);
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ return -1;
+}
+
+bool CFile::Delete(const std::string& strFileName)
+{
+ const CURL pathToUrl(strFileName);
+ return Delete(pathToUrl);
+}
+
+bool CFile::Delete(const CURL& file)
+{
+ try
+ {
+ CURL url(URIUtils::SubstitutePath(file));
+ CURL authUrl = url;
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrl);
+
+ std::unique_ptr<IFile> pFile(CFileFactory::CreateLoader(url));
+ if (!pFile)
+ return false;
+
+ if(pFile->Delete(authUrl))
+ {
+ g_directoryCache.ClearFile(url.Get());
+ return true;
+ }
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception", __FUNCTION__); }
+ if (Exists(file))
+ CLog::Log(LOGERROR, "{} - Error deleting file {}", __FUNCTION__, file.GetRedacted());
+ return false;
+}
+
+bool CFile::Rename(const std::string& strFileName, const std::string& strNewFileName)
+{
+ const CURL pathToUrl(strFileName);
+ const CURL pathToUrlNew(strNewFileName);
+ return Rename(pathToUrl, pathToUrlNew);
+}
+
+bool CFile::Rename(const CURL& file, const CURL& newFile)
+{
+ try
+ {
+ CURL url(URIUtils::SubstitutePath(file));
+ CURL urlnew(URIUtils::SubstitutePath(newFile));
+
+ CURL authUrl = url;
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrl);
+ CURL authUrlNew = urlnew;
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrlNew) && authUrlNew.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrlNew);
+
+ std::unique_ptr<IFile> pFile(CFileFactory::CreateLoader(url));
+ if (!pFile)
+ return false;
+
+ if(pFile->Rename(authUrl, authUrlNew))
+ {
+ g_directoryCache.ClearFile(url.Get());
+ g_directoryCache.AddFile(urlnew.Get());
+ return true;
+ }
+ }
+ XBMCCOMMONS_HANDLE_UNCHECKED
+ catch (...) { CLog::Log(LOGERROR, "{} - Unhandled exception ", __FUNCTION__); }
+ CLog::Log(LOGERROR, "{} - Error renaming file {}", __FUNCTION__, file.GetRedacted());
+ return false;
+}
+
+bool CFile::SetHidden(const std::string& fileName, bool hidden)
+{
+ const CURL pathToUrl(fileName);
+ return SetHidden(pathToUrl, hidden);
+}
+
+bool CFile::SetHidden(const CURL& file, bool hidden)
+{
+ try
+ {
+ CURL url(URIUtils::SubstitutePath(file));
+ CURL authUrl = url;
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrl);
+
+ std::unique_ptr<IFile> pFile(CFileFactory::CreateLoader(url));
+ if (!pFile)
+ return false;
+
+ return pFile->SetHidden(authUrl, hidden);
+ }
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "{}({}) - Unhandled exception", __FUNCTION__, file.GetRedacted());
+ }
+ return false;
+}
+
+int CFile::IoControl(EIoControl request, void* param)
+{
+ int result = -1;
+ if (!m_pFile)
+ return -1;
+ result = m_pFile->IoControl(request, param);
+
+ if(result == -1 && request == IOCTRL_SEEK_POSSIBLE)
+ {
+ if(m_pFile->GetLength() >= 0 && m_pFile->Seek(0, SEEK_CUR) >= 0)
+ return 1;
+ else
+ return 0;
+ }
+
+ return result;
+}
+
+int CFile::GetChunkSize()
+{
+ if (m_pFile)
+ return m_pFile->GetChunkSize();
+ return 0;
+}
+
+const std::string CFile::GetProperty(XFILE::FileProperty type, const std::string &name) const
+{
+ if (!m_pFile)
+ return "";
+ return m_pFile->GetProperty(type, name);
+}
+
+const std::vector<std::string> CFile::GetPropertyValues(XFILE::FileProperty type, const std::string &name) const
+{
+ if (!m_pFile)
+ {
+ return std::vector<std::string>();
+ }
+ return m_pFile->GetPropertyValues(type, name);
+}
+
+ssize_t CFile::LoadFile(const std::string& filename, std::vector<uint8_t>& outputBuffer)
+{
+ const CURL pathToUrl(filename);
+ return LoadFile(pathToUrl, outputBuffer);
+}
+
+ssize_t CFile::LoadFile(const CURL& file, std::vector<uint8_t>& outputBuffer)
+{
+ static const size_t max_file_size = 0x7FFFFFFF;
+ static const size_t min_chunk_size = 64 * 1024U;
+ static const size_t max_chunk_size = 2048 * 1024U;
+
+ outputBuffer.clear();
+
+ if (!Open(file, READ_TRUNCATED))
+ return 0;
+
+ /*
+ GetLength() will typically return values that fall into three cases:
+ 1. The real filesize. This is the typical case.
+ 2. Zero. This is the case for some http:// streams for example.
+ 3. Some value smaller than the real filesize. This is the case for an expanding file.
+
+ In order to handle all three cases, we read the file in chunks, relying on Read()
+ returning 0 at EOF. To minimize (re)allocation of the buffer, the chunksize in
+ cases 1 and 3 is set to one byte larger than the value returned by GetLength().
+ The chunksize in case 2 is set to the lowest value larger than min_chunk_size aligned
+ to GetChunkSize().
+
+ We fill the buffer entirely before reallocation. Thus, reallocation never occurs in case 1
+ as the buffer is larger than the file, so we hit EOF before we hit the end of buffer.
+
+ To minimize reallocation, we double the chunksize each read while chunksize is lower
+ than max_chunk_size.
+ */
+ int64_t filesize = GetLength();
+ if (filesize > (int64_t)max_file_size)
+ return 0; /* file is too large for this function */
+
+ size_t chunksize = (filesize > 0) ? static_cast<size_t>(filesize + 1)
+ : static_cast<size_t>(DetermineChunkSize(GetChunkSize(),
+ min_chunk_size));
+ size_t total_read = 0;
+ while (true)
+ {
+ if (total_read == outputBuffer.size())
+ { // (re)alloc
+ if (outputBuffer.size() + chunksize > max_file_size)
+ {
+ outputBuffer.clear();
+ return -1;
+ }
+ outputBuffer.resize(outputBuffer.size() + chunksize);
+ if (chunksize < max_chunk_size)
+ chunksize *= 2;
+ }
+ ssize_t read = Read(outputBuffer.data() + total_read, outputBuffer.size() - total_read);
+ if (read < 0)
+ {
+ outputBuffer.clear();
+ return -1;
+ }
+ total_read += read;
+ if (!read)
+ break;
+ }
+
+ outputBuffer.resize(total_read);
+
+ return total_read;
+}
+
+double CFile::GetDownloadSpeed()
+{
+ if (m_pFile)
+ return m_pFile->GetDownloadSpeed();
+ return 0.0;
+}
+
+//*********************************************************************************************
+//*************** Stream IO for CFile objects *************************************************
+//*********************************************************************************************
+CFileStreamBuffer::~CFileStreamBuffer()
+{
+ sync();
+ Detach();
+}
+
+CFileStreamBuffer::CFileStreamBuffer(int backsize)
+ : std::streambuf()
+ , m_file(NULL)
+ , m_buffer(NULL)
+ , m_backsize(backsize)
+{
+}
+
+void CFileStreamBuffer::Attach(IFile *file)
+{
+ m_file = file;
+
+ m_frontsize = CFile::DetermineChunkSize(m_file->GetChunkSize(), 64 * 1024);
+
+ m_buffer = new char[m_frontsize+m_backsize];
+ setg(0,0,0);
+ setp(0,0);
+}
+
+void CFileStreamBuffer::Detach()
+{
+ setg(0,0,0);
+ setp(0,0);
+ delete[] m_buffer;
+ m_buffer = NULL;
+}
+
+CFileStreamBuffer::int_type CFileStreamBuffer::underflow()
+{
+ if(gptr() < egptr())
+ return traits_type::to_int_type(*gptr());
+
+ if(!m_file)
+ return traits_type::eof();
+
+ size_t backsize = 0;
+ if(m_backsize)
+ {
+ backsize = (size_t)std::min<ptrdiff_t>((ptrdiff_t)m_backsize, egptr()-eback());
+ memmove(m_buffer, egptr()-backsize, backsize);
+ }
+
+ ssize_t size = m_file->Read(m_buffer+backsize, m_frontsize);
+
+ if (size == 0)
+ return traits_type::eof();
+ else if (size < 0)
+ {
+ CLog::LogF(LOGWARNING, "Error reading file - assuming eof");
+ return traits_type::eof();
+ }
+
+ setg(m_buffer, m_buffer+backsize, m_buffer+backsize+size);
+ return traits_type::to_int_type(*gptr());
+}
+
+CFileStreamBuffer::pos_type CFileStreamBuffer::seekoff(
+ off_type offset,
+ std::ios_base::seekdir way,
+ std::ios_base::openmode mode)
+{
+ // calculate relative offset
+ off_type aheadbytes = (egptr() - gptr());
+ off_type pos = m_file->GetPosition() - aheadbytes;
+ off_type offset2;
+ if(way == std::ios_base::cur)
+ offset2 = offset;
+ else if(way == std::ios_base::beg)
+ offset2 = offset - pos;
+ else if(way == std::ios_base::end)
+ offset2 = offset + m_file->GetLength() - pos;
+ else
+ return std::streampos(-1);
+
+ // a non seek shouldn't modify our buffer
+ if(offset2 == 0)
+ return pos;
+
+ // try to seek within buffer
+ if(gptr()+offset2 >= eback() && gptr()+offset2 < egptr())
+ {
+ gbump(offset2);
+ return pos + offset2;
+ }
+
+ // reset our buffer pointer, will
+ // start buffering on next read
+ setg(0,0,0);
+ setp(0,0);
+
+ int64_t position = -1;
+ if(way == std::ios_base::cur)
+ position = m_file->Seek(offset - aheadbytes, SEEK_CUR);
+ else if(way == std::ios_base::end)
+ position = m_file->Seek(offset, SEEK_END);
+ else
+ position = m_file->Seek(offset, SEEK_SET);
+
+ if(position<0)
+ return std::streampos(-1);
+
+ return position;
+}
+
+CFileStreamBuffer::pos_type CFileStreamBuffer::seekpos(
+ pos_type pos,
+ std::ios_base::openmode mode)
+{
+ return seekoff(pos, std::ios_base::beg, mode);
+}
+
+std::streamsize CFileStreamBuffer::showmanyc()
+{
+ underflow();
+ return egptr() - gptr();
+}
+
+CFileStream::CFileStream(int backsize /*= 0*/) : std::istream(&m_buffer), m_buffer(backsize)
+{
+}
+
+CFileStream::~CFileStream()
+{
+ Close();
+}
+
+
+bool CFileStream::Open(const CURL& filename)
+{
+ Close();
+
+ CURL url(URIUtils::SubstitutePath(filename));
+ m_file.reset(CFileFactory::CreateLoader(url));
+
+ CURL authUrl = url;
+ if (CPasswordManager::GetInstance().IsURLSupported(authUrl) && authUrl.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(authUrl);
+
+ if(m_file && m_file->Open(authUrl))
+ {
+ m_buffer.Attach(m_file.get());
+ return true;
+ }
+
+ setstate(failbit);
+ return false;
+}
+
+int64_t CFileStream::GetLength()
+{
+ return m_file->GetLength();
+}
+
+void CFileStream::Close()
+{
+ if(!m_file)
+ return;
+
+ m_buffer.Detach();
+}
+
+bool CFileStream::Open(const std::string& filename)
+{
+ const CURL pathToUrl(filename);
+ return Open(pathToUrl);
+}
diff --git a/xbmc/filesystem/File.h b/xbmc/filesystem/File.h
new file mode 100644
index 0000000..a5f991a
--- /dev/null
+++ b/xbmc/filesystem/File.h
@@ -0,0 +1,206 @@
+/*
+ * Copyright (c) 2002 Frodo
+ * Portions Copyright (c) by the authors of ffmpeg and xvid
+ * Copyright (C) 2002-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
+
+// File.h: interface for the CFile class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "IFileTypes.h"
+#include "URL.h"
+
+#include <iostream>
+#include <memory>
+#include <stdio.h>
+#include <string>
+#include <vector>
+
+#include "PlatformDefs.h"
+
+class BitstreamStats;
+
+namespace XFILE
+{
+
+class IFile;
+
+class CFileStreamBuffer;
+
+class CFile
+{
+public:
+ CFile();
+ ~CFile();
+
+ bool CURLCreate(const std::string &url);
+ bool CURLAddOption(XFILE::CURLOPTIONTYPE type, const char* name, const char * value);
+ bool CURLOpen(unsigned int flags);
+
+ /**
+ * Attempt to open an IFile instance.
+ * @param file reference to CCurl file description
+ * @param flags see IFileTypes.h
+ * @return true on success, false otherwise
+ *
+ * Remarks: Open can only be called once. Calling
+ * Open() on an already opened file will fail
+ * except if flag READ_REOPEN is set and the underlying
+ * file has an implementation of ReOpen().
+ */
+ bool Open(const CURL& file, const unsigned int flags = 0);
+ bool Open(const std::string& strFileName, const unsigned int flags = 0);
+
+ bool OpenForWrite(const CURL& file, bool bOverWrite = false);
+ bool OpenForWrite(const std::string& strFileName, bool bOverWrite = false);
+
+ ssize_t LoadFile(const CURL& file, std::vector<uint8_t>& outputBuffer);
+
+ /**
+ * Attempt to read bufSize bytes from currently opened file into buffer bufPtr.
+ * @param bufPtr pointer to buffer
+ * @param bufSize size of the buffer
+ * @return number of successfully read bytes if any bytes were read and stored in
+ * buffer, zero if no bytes are available to read (end of file was reached)
+ * or undetectable error occur, -1 in case of any explicit error
+ */
+ ssize_t Read(void* bufPtr, size_t bufSize);
+ bool ReadString(char *szLine, int iLineLength);
+ /**
+ * Attempt to write bufSize bytes from buffer bufPtr into currently opened file.
+ * @param bufPtr pointer to buffer
+ * @param bufSize size of the buffer
+ * @return number of successfully written bytes if any bytes were written,
+ * zero if no bytes were written and no detectable error occur,
+ * -1 in case of any explicit error
+ */
+ ssize_t Write(const void* bufPtr, size_t bufSize);
+ void Flush();
+ int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET);
+ int Truncate(int64_t iSize);
+ int64_t GetPosition() const;
+ int64_t GetLength();
+ void Close();
+ int GetChunkSize();
+ const std::string GetProperty(XFILE::FileProperty type, const std::string &name = "") const;
+ const std::vector<std::string> GetPropertyValues(XFILE::FileProperty type, const std::string &name = "") const;
+ ssize_t LoadFile(const std::string& filename, std::vector<uint8_t>& outputBuffer);
+
+ static int DetermineChunkSize(const int srcChunkSize, const int reqChunkSize);
+
+ const std::unique_ptr<BitstreamStats>& GetBitstreamStats() const { return m_bitStreamStats; }
+
+ int IoControl(EIoControl request, void* param);
+
+ IFile* GetImplementation() const { return m_pFile.get(); }
+
+ // CURL interface
+ static bool Exists(const CURL& file, bool bUseCache = true);
+ static bool Delete(const CURL& file);
+ /**
+ * Fills struct __stat64 with information about file specified by filename
+ * For st_mode function will set correctly _S_IFDIR (directory) flag and may set
+ * _S_IREAD (read permission), _S_IWRITE (write permission) flags if such
+ * information is available. Function may set st_size (file size), st_atime,
+ * st_mtime, st_ctime (access, modification, creation times).
+ * Any other flags and members of __stat64 that didn't updated with actual file
+ * information will be set to zero (st_nlink can be set ether to 1 or zero).
+ * @param file specifies requested file
+ * @param buffer pointer to __stat64 buffer to receive information about file
+ * @return zero of success, -1 otherwise.
+ */
+ static int Stat(const CURL& file, struct __stat64* buffer);
+ static bool Rename(const CURL& file, const CURL& urlNew);
+ static bool Copy(const CURL& file, const CURL& dest, XFILE::IFileCallback* pCallback = NULL, void* pContext = NULL);
+ static bool SetHidden(const CURL& file, bool hidden);
+
+ // string interface
+ static bool Exists(const std::string& strFileName, bool bUseCache = true);
+ /**
+ * Fills struct __stat64 with information about file specified by filename
+ * For st_mode function will set correctly _S_IFDIR (directory) flag and may set
+ * _S_IREAD (read permission), _S_IWRITE (write permission) flags if such
+ * information is available. Function may set st_size (file size), st_atime,
+ * st_mtime, st_ctime (access, modification, creation times).
+ * Any other flags and members of __stat64 that didn't updated with actual file
+ * information will be set to zero (st_nlink can be set ether to 1 or zero).
+ * @param strFileName specifies requested file
+ * @param buffer pointer to __stat64 buffer to receive information about file
+ * @return zero of success, -1 otherwise.
+ */
+ static int Stat(const std::string& strFileName, struct __stat64* buffer);
+ /**
+ * Fills struct __stat64 with information about currently open file
+ * For st_mode function will set correctly _S_IFDIR (directory) flag and may set
+ * _S_IREAD (read permission), _S_IWRITE (write permission) flags if such
+ * information is available. Function may set st_size (file size), st_atime,
+ * st_mtime, st_ctime (access, modification, creation times).
+ * Any other flags and members of __stat64 that didn't updated with actual file
+ * information will be set to zero (st_nlink can be set ether to 1 or zero).
+ * @param buffer pointer to __stat64 buffer to receive information about file
+ * @return zero of success, -1 otherwise.
+ */
+ int Stat(struct __stat64 *buffer);
+ static bool Delete(const std::string& strFileName);
+ static bool Rename(const std::string& strFileName, const std::string& strNewFileName);
+ static bool Copy(const std::string& strFileName, const std::string& strDest, XFILE::IFileCallback* pCallback = NULL, void* pContext = NULL);
+ static bool SetHidden(const std::string& fileName, bool hidden);
+ double GetDownloadSpeed();
+
+private:
+ unsigned int m_flags = 0;
+ CURL m_curl;
+ std::unique_ptr<IFile> m_pFile;
+ std::unique_ptr<CFileStreamBuffer> m_pBuffer;
+ std::unique_ptr<BitstreamStats> m_bitStreamStats;
+};
+
+// streambuf for file io, only supports buffered input currently
+class CFileStreamBuffer
+ : public std::streambuf
+{
+public:
+ ~CFileStreamBuffer() override;
+ explicit CFileStreamBuffer(int backsize = 0);
+
+ void Attach(IFile *file);
+ void Detach();
+
+private:
+ int_type underflow() override;
+ std::streamsize showmanyc() override;
+ pos_type seekoff(off_type, std::ios_base::seekdir,std::ios_base::openmode = std::ios_base::in | std::ios_base::out) override;
+ pos_type seekpos(pos_type, std::ios_base::openmode = std::ios_base::in | std::ios_base::out) override;
+
+ IFile* m_file;
+ char* m_buffer;
+ int m_backsize;
+ int m_frontsize = 0;
+};
+
+// very basic file input stream
+class CFileStream
+ : public std::istream
+{
+public:
+ explicit CFileStream(int backsize = 0);
+ ~CFileStream() override;
+
+ bool Open(const std::string& filename);
+ bool Open(const CURL& filename);
+ void Close();
+
+ int64_t GetLength();
+private:
+ CFileStreamBuffer m_buffer;
+ std::unique_ptr<IFile> m_file;
+};
+
+}
diff --git a/xbmc/filesystem/FileCache.cpp b/xbmc/filesystem/FileCache.cpp
new file mode 100644
index 0000000..4f2e07b
--- /dev/null
+++ b/xbmc/filesystem/FileCache.cpp
@@ -0,0 +1,630 @@
+/*
+ * 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 "FileCache.h"
+
+#include "CircularCache.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/Thread.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#if !defined(TARGET_WINDOWS)
+#include "platform/posix/ConvUtils.h"
+#endif
+
+#include <algorithm>
+#include <cassert>
+#include <chrono>
+#include <inttypes.h>
+#include <memory>
+
+#ifdef TARGET_POSIX
+#include "platform/posix/ConvUtils.h"
+#endif
+
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+class CWriteRate
+{
+public:
+ CWriteRate()
+ {
+ m_stamp = std::chrono::steady_clock::now();
+ m_pos = 0;
+ m_size = 0;
+ m_time = std::chrono::milliseconds(0);
+ }
+
+ void Reset(int64_t pos, bool bResetAll = true)
+ {
+ m_stamp = std::chrono::steady_clock::now();
+ m_pos = pos;
+
+ if (bResetAll)
+ {
+ m_size = 0;
+ m_time = std::chrono::milliseconds(0);
+ }
+ }
+
+ uint32_t Rate(int64_t pos, uint32_t time_bias = 0)
+ {
+ auto ts = std::chrono::steady_clock::now();
+
+ m_size += (pos - m_pos);
+ m_time += std::chrono::duration_cast<std::chrono::milliseconds>(ts - m_stamp);
+ m_pos = pos;
+ m_stamp = ts;
+
+ if (m_time == std::chrono::milliseconds(0))
+ return 0;
+
+ return static_cast<uint32_t>(1000 * (m_size / (m_time.count() + time_bias)));
+ }
+
+private:
+ std::chrono::time_point<std::chrono::steady_clock> m_stamp;
+ int64_t m_pos;
+ std::chrono::milliseconds m_time;
+ int64_t m_size;
+};
+
+
+CFileCache::CFileCache(const unsigned int flags)
+ : CThread("FileCache"),
+ m_seekPossible(0),
+ m_nSeekResult(0),
+ m_seekPos(0),
+ m_readPos(0),
+ m_writePos(0),
+ m_chunkSize(0),
+ m_writeRate(0),
+ m_writeRateActual(0),
+ m_writeRateLowSpeed(0),
+ m_forwardCacheSize(0),
+ m_bFilling(false),
+ m_fileSize(0),
+ m_flags(flags)
+{
+}
+
+CFileCache::~CFileCache()
+{
+ Close();
+}
+
+IFile *CFileCache::GetFileImp()
+{
+ return m_source.GetImplementation();
+}
+
+bool CFileCache::Open(const CURL& url)
+{
+ Close();
+
+ std::unique_lock<CCriticalSection> lock(m_sync);
+
+ m_sourcePath = url.GetRedacted();
+
+ CLog::Log(LOGDEBUG, "CFileCache::{} - <{}> opening", __FUNCTION__, m_sourcePath);
+
+ // opening the source file.
+ if (!m_source.Open(url.Get(), READ_NO_CACHE | READ_TRUNCATED | READ_CHUNKED))
+ {
+ CLog::Log(LOGERROR, "CFileCache::{} - <{}> failed to open", __FUNCTION__, m_sourcePath);
+ Close();
+ return false;
+ }
+
+ m_source.IoControl(IOCTRL_SET_CACHE, this);
+
+ bool retry = false;
+ m_source.IoControl(IOCTRL_SET_RETRY, &retry); // We already handle retrying ourselves
+
+ // check if source can seek
+ m_seekPossible = m_source.IoControl(IOCTRL_SEEK_POSSIBLE, NULL);
+
+ // Determine the best chunk size we can use
+ m_chunkSize = CFile::DetermineChunkSize(
+ m_source.GetChunkSize(),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheChunkSize);
+ CLog::Log(LOGDEBUG,
+ "CFileCache::{} - <{}> source chunk size is {}, setting cache chunk size to {}",
+ __FUNCTION__, m_sourcePath, m_source.GetChunkSize(), m_chunkSize);
+
+ m_fileSize = m_source.GetLength();
+
+ if (!m_pCache)
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheMemSize == 0)
+ {
+ // Use cache on disk
+ m_pCache = std::unique_ptr<CSimpleFileCache>(new CSimpleFileCache()); // C++14 - Replace with std::make_unique
+ m_forwardCacheSize = 0;
+ }
+ else
+ {
+ size_t cacheSize;
+ if (m_fileSize > 0 && m_fileSize < CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheMemSize && !(m_flags & READ_AUDIO_VIDEO))
+ {
+ // Cap cache size by filesize, but not for audio/video files as those may grow.
+ // We don't need to take into account READ_MULTI_STREAM here as that's only used for audio/video
+ cacheSize = m_fileSize;
+
+ // Cap chunk size by cache size
+ if (m_chunkSize > cacheSize)
+ m_chunkSize = cacheSize;
+ }
+ else
+ {
+ cacheSize = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheMemSize;
+
+ // NOTE: READ_MULTI_STREAM is only used with READ_AUDIO_VIDEO
+ if (m_flags & READ_MULTI_STREAM)
+ {
+ // READ_MULTI_STREAM requires double buffering, so use half the amount of memory for each buffer
+ cacheSize /= 2;
+ }
+
+ // Make sure cache can at least hold 2 chunks
+ if (cacheSize < m_chunkSize * 2)
+ cacheSize = m_chunkSize * 2;
+ }
+
+ if (m_flags & READ_MULTI_STREAM)
+ CLog::Log(LOGDEBUG, "CFileCache::{} - <{}> using double memory cache each sized {} bytes",
+ __FUNCTION__, m_sourcePath, cacheSize);
+ else
+ CLog::Log(LOGDEBUG, "CFileCache::{} - <{}> using single memory cache sized {} bytes",
+ __FUNCTION__, m_sourcePath, cacheSize);
+
+ const size_t back = cacheSize / 4;
+ const size_t front = cacheSize - back;
+
+ m_pCache = std::unique_ptr<CCircularCache>(new CCircularCache(front, back)); // C++14 - Replace with std::make_unique
+ m_forwardCacheSize = front;
+ }
+
+ if (m_flags & READ_MULTI_STREAM)
+ {
+ // If READ_MULTI_STREAM flag is set: Double buffering is required
+ m_pCache = std::unique_ptr<CDoubleCache>(new CDoubleCache(m_pCache.release())); // C++14 - Replace with std::make_unique
+ }
+ }
+
+ // open cache strategy
+ if (!m_pCache || m_pCache->Open() != CACHE_RC_OK)
+ {
+ CLog::Log(LOGERROR, "CFileCache::{} - <{}> failed to open cache", __FUNCTION__, m_sourcePath);
+ Close();
+ return false;
+ }
+
+ m_readPos = 0;
+ m_writePos = 0;
+ m_writeRate = 1024 * 1024;
+ m_writeRateActual = 0;
+ m_writeRateLowSpeed = 0;
+ m_bFilling = true;
+ m_seekEvent.Reset();
+ m_seekEnded.Reset();
+
+ CThread::Create(false);
+
+ return true;
+}
+
+void CFileCache::Process()
+{
+ if (!m_pCache)
+ {
+ CLog::Log(LOGERROR, "CFileCache::{} - <{}> sanity failed. no cache strategy", __FUNCTION__,
+ m_sourcePath);
+ return;
+ }
+
+ // create our read buffer
+ std::unique_ptr<char[]> buffer(new char[m_chunkSize]);
+ if (buffer == nullptr)
+ {
+ CLog::Log(LOGERROR, "CFileCache::{} - <{}> failed to allocate read buffer", __FUNCTION__,
+ m_sourcePath);
+ return;
+ }
+
+ CWriteRate limiter;
+ CWriteRate average;
+
+ while (!m_bStop)
+ {
+ // Update filesize
+ m_fileSize = m_source.GetLength();
+
+ // check for seek events
+ if (m_seekEvent.Wait(0ms))
+ {
+ m_seekEvent.Reset();
+ const int64_t cacheMaxPos = m_pCache->CachedDataEndPosIfSeekTo(m_seekPos);
+ const bool cacheReachEOF = (cacheMaxPos == m_fileSize);
+
+ bool sourceSeekFailed = false;
+ if (!cacheReachEOF)
+ {
+ m_nSeekResult = m_source.Seek(cacheMaxPos, SEEK_SET);
+ if (m_nSeekResult != cacheMaxPos)
+ {
+ CLog::Log(LOGERROR, "CFileCache::{} - <{}> error {} seeking. Seek returned {}",
+ __FUNCTION__, m_sourcePath, GetLastError(), m_nSeekResult);
+ m_seekPossible = m_source.IoControl(IOCTRL_SEEK_POSSIBLE, NULL);
+ sourceSeekFailed = true;
+ }
+ }
+
+ if (!sourceSeekFailed)
+ {
+ const bool bCompleteReset = m_pCache->Reset(m_seekPos);
+ m_readPos = m_seekPos;
+ m_writePos = m_pCache->CachedDataEndPos();
+ assert(m_writePos == cacheMaxPos);
+ average.Reset(m_writePos, bCompleteReset); // Can only recalculate new average from scratch after a full reset (empty cache)
+ limiter.Reset(m_writePos);
+ m_nSeekResult = m_seekPos;
+ if (bCompleteReset)
+ {
+ CLog::Log(LOGDEBUG,
+ "CFileCache::{} - <{}> cache completely reset for seek to position {}",
+ __FUNCTION__, m_sourcePath, m_seekPos);
+ m_bFilling = true;
+ m_writeRateLowSpeed = 0;
+ }
+ }
+
+ m_seekEnded.Set();
+ }
+
+ while (m_writeRate)
+ {
+ if (m_writePos - m_readPos < m_writeRate * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheReadFactor)
+ {
+ limiter.Reset(m_writePos);
+ break;
+ }
+
+ if (limiter.Rate(m_writePos) < m_writeRate * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheReadFactor)
+ break;
+
+ if (m_seekEvent.Wait(100ms))
+ {
+ if (!m_bStop)
+ m_seekEvent.Set();
+ break;
+ }
+ }
+
+ const int64_t maxWrite = m_pCache->GetMaxWriteSize(m_chunkSize);
+ int64_t maxSourceRead = m_chunkSize;
+ // Cap source read size by space available between current write position and EOF
+ if (m_fileSize != 0)
+ maxSourceRead = std::min(maxSourceRead, m_fileSize - m_writePos);
+
+ /* Only read from source if there's enough write space in the cache
+ * else we may keep disposing data and seeking back on (slow) source
+ */
+ if (maxWrite < maxSourceRead)
+ {
+ // Wait until sufficient cache write space is available
+ m_pCache->m_space.Wait(5ms);
+ continue;
+ }
+
+ ssize_t iRead = 0;
+ if (maxSourceRead > 0)
+ iRead = m_source.Read(buffer.get(), maxSourceRead);
+ if (iRead <= 0)
+ {
+ // Check for actual EOF and retry as long as we still have data in our cache
+ if (m_writePos < m_fileSize && m_pCache->WaitForData(0, 0ms) > 0)
+ {
+ CLog::Log(LOGWARNING, "CFileCache::{} - <{}> source read returned {}! Will retry",
+ __FUNCTION__, m_sourcePath, iRead);
+
+ // Wait a bit:
+ if (m_seekEvent.Wait(2000ms))
+ {
+ if (!m_bStop)
+ m_seekEvent.Set(); // hack so that later we realize seek is needed
+ }
+
+ // and retry:
+ continue; // while (!m_bStop)
+ }
+ else
+ {
+ if (iRead < 0)
+ CLog::Log(LOGERROR,
+ "{} - <{}> source read failed with {}!", __FUNCTION__, m_sourcePath, iRead);
+ else if (m_fileSize == 0)
+ CLog::Log(LOGDEBUG,
+ "CFileCache::{} - <{}> source read didn't return any data! Hit eof(?)",
+ __FUNCTION__, m_sourcePath);
+ else if (m_writePos < m_fileSize)
+ CLog::Log(LOGERROR,
+ "CFileCache::{} - <{}> source read didn't return any data before eof!",
+ __FUNCTION__, m_sourcePath);
+ else
+ CLog::Log(LOGDEBUG, "CFileCache::{} - <{}> source read hit eof", __FUNCTION__,
+ m_sourcePath);
+
+ m_pCache->EndOfInput();
+
+ // The thread event will now also cause the wait of an event to return a false.
+ if (AbortableWait(m_seekEvent) == WAIT_SIGNALED)
+ {
+ m_pCache->ClearEndOfInput();
+ if (!m_bStop)
+ m_seekEvent.Set(); // hack so that later we realize seek is needed
+ }
+ else
+ break; // while (!m_bStop)
+ }
+ }
+
+ int iTotalWrite = 0;
+ while (!m_bStop && (iTotalWrite < iRead))
+ {
+ int iWrite = 0;
+ iWrite = m_pCache->WriteToCache(buffer.get() + iTotalWrite, iRead - iTotalWrite);
+
+ // write should always work. all handling of buffering and errors should be
+ // done inside the cache strategy. only if unrecoverable error happened, WriteToCache would return error and we break.
+ if (iWrite < 0)
+ {
+ CLog::Log(LOGERROR, "CFileCache::{} - <{}> error writing to cache", __FUNCTION__,
+ m_sourcePath);
+ m_bStop = true;
+ break;
+ }
+ else if (iWrite == 0)
+ {
+ m_pCache->m_space.Wait(5ms);
+ }
+
+ iTotalWrite += iWrite;
+
+ // check if seek was asked. otherwise if cache is full we'll freeze.
+ if (m_seekEvent.Wait(0ms))
+ {
+ if (!m_bStop)
+ m_seekEvent.Set(); // make sure we get the seek event later.
+ break;
+ }
+ }
+
+ m_writePos += iTotalWrite;
+
+ // under estimate write rate by a second, to
+ // avoid uncertainty at start of caching
+ m_writeRateActual = average.Rate(m_writePos, 1000);
+
+ /* NOTE: We can only reliably test for low speed condition, when the cache is *really*
+ * filling. This is because as soon as it's full the average-
+ * rate will become approximately the current-rate which can flag false
+ * low read-rate conditions.
+ */
+ if (m_bFilling && m_forwardCacheSize != 0)
+ {
+ const int64_t forward = m_pCache->WaitForData(0, 0ms);
+ if (forward + m_chunkSize >= m_forwardCacheSize)
+ {
+ if (m_writeRateActual < m_writeRate)
+ m_writeRateLowSpeed = m_writeRateActual;
+
+ m_bFilling = false;
+ }
+ }
+ }
+}
+
+void CFileCache::OnExit()
+{
+ m_bStop = true;
+
+ // make sure cache is set to mark end of file (read may be waiting).
+ if (m_pCache)
+ m_pCache->EndOfInput();
+
+ // just in case someone's waiting...
+ m_seekEnded.Set();
+}
+
+bool CFileCache::Exists(const CURL& url)
+{
+ return CFile::Exists(url.Get());
+}
+
+int CFileCache::Stat(const CURL& url, struct __stat64* buffer)
+{
+ return CFile::Stat(url.Get(), buffer);
+}
+
+ssize_t CFileCache::Read(void* lpBuf, size_t uiBufSize)
+{
+ std::unique_lock<CCriticalSection> lock(m_sync);
+ if (!m_pCache)
+ {
+ CLog::Log(LOGERROR, "CFileCache::{} - <{}> sanity failed. no cache strategy!", __FUNCTION__,
+ m_sourcePath);
+ return -1;
+ }
+ int64_t iRc;
+
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+retry:
+ // attempt to read
+ iRc = m_pCache->ReadFromCache((char *)lpBuf, uiBufSize);
+ if (iRc > 0)
+ {
+ m_readPos += iRc;
+ return (int)iRc;
+ }
+
+ if (iRc == CACHE_RC_WOULD_BLOCK)
+ {
+ // just wait for some data to show up
+ iRc = m_pCache->WaitForData(1, 10s);
+ if (iRc > 0)
+ goto retry;
+ }
+
+ if (iRc == CACHE_RC_TIMEOUT)
+ {
+ CLog::Log(LOGWARNING, "CFileCache::{} - <{}> timeout waiting for data", __FUNCTION__,
+ m_sourcePath);
+ return -1;
+ }
+
+ if (iRc == 0)
+ return 0;
+
+ // unknown error code
+ CLog::Log(LOGERROR, "CFileCache::{} - <{}> cache strategy returned unknown error code {}",
+ __FUNCTION__, m_sourcePath, (int)iRc);
+ return -1;
+}
+
+int64_t CFileCache::Seek(int64_t iFilePosition, int iWhence)
+{
+ std::unique_lock<CCriticalSection> lock(m_sync);
+
+ if (!m_pCache)
+ {
+ CLog::Log(LOGERROR, "CFileCache::{} - <{}> sanity failed. no cache strategy!", __FUNCTION__,
+ m_sourcePath);
+ return -1;
+ }
+
+ int64_t iCurPos = m_readPos;
+ int64_t iTarget = iFilePosition;
+ if (iWhence == SEEK_END)
+ iTarget = m_fileSize + iTarget;
+ else if (iWhence == SEEK_CUR)
+ iTarget = iCurPos + iTarget;
+ else if (iWhence != SEEK_SET)
+ return -1;
+
+ if (iTarget == m_readPos)
+ return m_readPos;
+
+ if ((m_nSeekResult = m_pCache->Seek(iTarget)) != iTarget)
+ {
+ if (m_seekPossible == 0)
+ return m_nSeekResult;
+
+ // Never request closer to end than one chunk. Speeds up tag reading
+ m_seekPos = std::min(iTarget, std::max((int64_t)0, m_fileSize - m_chunkSize));
+
+ m_seekEvent.Set();
+ while (!m_seekEnded.Wait(100ms))
+ {
+ // SeekEnded will never be set if FileCache thread is not running
+ if (!CThread::IsRunning())
+ return -1;
+ }
+
+ /* wait for any remaining data */
+ if(m_seekPos < iTarget)
+ {
+ CLog::Log(LOGDEBUG, "CFileCache::{} - <{}> waiting for position {}", __FUNCTION__,
+ m_sourcePath, iTarget);
+ if (m_pCache->WaitForData(static_cast<uint32_t>(iTarget - m_seekPos), 10s) <
+ iTarget - m_seekPos)
+ {
+ CLog::Log(LOGWARNING, "CFileCache::{} - <{}> failed to get remaining data", __FUNCTION__,
+ m_sourcePath);
+ return -1;
+ }
+ m_pCache->Seek(iTarget);
+ }
+ m_readPos = iTarget;
+ m_seekEvent.Reset();
+ }
+ else
+ m_readPos = iTarget;
+
+ return iTarget;
+}
+
+void CFileCache::Close()
+{
+ StopThread();
+
+ std::unique_lock<CCriticalSection> lock(m_sync);
+ if (m_pCache)
+ m_pCache->Close();
+
+ m_source.Close();
+}
+
+int64_t CFileCache::GetPosition()
+{
+ return m_readPos;
+}
+
+int64_t CFileCache::GetLength()
+{
+ return m_fileSize;
+}
+
+void CFileCache::StopThread(bool bWait /*= true*/)
+{
+ m_bStop = true;
+ //Process could be waiting for seekEvent
+ m_seekEvent.Set();
+ CThread::StopThread(bWait);
+}
+
+const std::string CFileCache::GetProperty(XFILE::FileProperty type, const std::string &name) const
+{
+ if (!m_source.GetImplementation())
+ return IFile::GetProperty(type, name);
+
+ return m_source.GetImplementation()->GetProperty(type, name);
+}
+
+int CFileCache::IoControl(EIoControl request, void* param)
+{
+ if (request == IOCTRL_CACHE_STATUS)
+ {
+ SCacheStatus* status = (SCacheStatus*)param;
+ status->forward = m_pCache->WaitForData(0, 0ms);
+ status->maxrate = m_writeRate;
+ status->currate = m_writeRateActual;
+ status->lowrate = m_writeRateLowSpeed;
+ m_writeRateLowSpeed = 0; // Reset low speed condition
+ return 0;
+ }
+
+ if (request == IOCTRL_CACHE_SETRATE)
+ {
+ m_writeRate = *static_cast<uint32_t*>(param);
+ return 0;
+ }
+
+ if (request == IOCTRL_SEEK_POSSIBLE)
+ return m_seekPossible;
+
+ return -1;
+}
diff --git a/xbmc/filesystem/FileCache.h b/xbmc/filesystem/FileCache.h
new file mode 100644
index 0000000..df7c839
--- /dev/null
+++ b/xbmc/filesystem/FileCache.h
@@ -0,0 +1,79 @@
+/*
+ * 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 "CacheStrategy.h"
+#include "File.h"
+#include "IFile.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <atomic>
+#include <memory>
+
+namespace XFILE
+{
+
+ class CFileCache : public IFile, public CThread
+ {
+ public:
+ explicit CFileCache(const unsigned int flags);
+ ~CFileCache() override;
+
+ // CThread methods
+ void Process() override;
+ void OnExit() override;
+ void StopThread(bool bWait = true) override;
+
+ // IFIle methods
+ bool Open(const CURL& url) override;
+ void Close() override;
+ bool Exists(const CURL& url) override;
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+
+ ssize_t Read(void* lpBuf, size_t uiBufSize) override;
+
+ int64_t Seek(int64_t iFilePosition, int iWhence) override;
+ int64_t GetPosition() override;
+ int64_t GetLength() override;
+
+ int IoControl(EIoControl request, void* param) override;
+
+ IFile *GetFileImp();
+
+ const std::string GetProperty(XFILE::FileProperty type, const std::string &name = "") const override;
+
+ const std::vector<std::string> GetPropertyValues(XFILE::FileProperty type, const std::string& name = "") const override
+ {
+ return std::vector<std::string>();
+ }
+
+ private:
+ std::unique_ptr<CCacheStrategy> m_pCache;
+ int m_seekPossible;
+ CFile m_source;
+ std::string m_sourcePath;
+ CEvent m_seekEvent;
+ CEvent m_seekEnded;
+ int64_t m_nSeekResult;
+ int64_t m_seekPos;
+ int64_t m_readPos;
+ int64_t m_writePos;
+ unsigned m_chunkSize;
+ uint32_t m_writeRate;
+ uint32_t m_writeRateActual;
+ uint32_t m_writeRateLowSpeed;
+ int64_t m_forwardCacheSize;
+ bool m_bFilling;
+ std::atomic<int64_t> m_fileSize;
+ unsigned int m_flags;
+ CCriticalSection m_sync;
+ };
+
+}
diff --git a/xbmc/filesystem/FileDirectoryFactory.cpp b/xbmc/filesystem/FileDirectoryFactory.cpp
new file mode 100644
index 0000000..a4e0f3f
--- /dev/null
+++ b/xbmc/filesystem/FileDirectoryFactory.cpp
@@ -0,0 +1,250 @@
+/*
+ * 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 "FileDirectoryFactory.h"
+
+#if defined(HAS_ISO9660PP)
+#include "ISO9660Directory.h"
+#endif
+#if defined(HAS_UDFREAD)
+#include "UDFDirectory.h"
+#endif
+#include "RSSDirectory.h"
+#include "UDFDirectory.h"
+#include "utils/URIUtils.h"
+#if defined(TARGET_ANDROID)
+#include "platform/android/filesystem/APKDirectory.h"
+#endif
+#include "AudioBookFileDirectory.h"
+#include "Directory.h"
+#include "FileItem.h"
+#include "PlaylistFileDirectory.h"
+#include "ServiceBroker.h"
+#include "SmartPlaylistDirectory.h"
+#include "URL.h"
+#include "XbtDirectory.h"
+#include "ZipDirectory.h"
+#include "addons/AudioDecoder.h"
+#include "addons/ExtsMimeSupportList.h"
+#include "addons/VFSEntry.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "playlists/PlayListFactory.h"
+#include "playlists/SmartPlayList.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+using namespace ADDON;
+using namespace KODI::ADDONS;
+using namespace XFILE;
+using namespace PLAYLIST;
+
+CFileDirectoryFactory::CFileDirectoryFactory(void) = default;
+
+CFileDirectoryFactory::~CFileDirectoryFactory(void) = default;
+
+// return NULL + set pItem->m_bIsFolder to remove it completely from list.
+IFileDirectory* CFileDirectoryFactory::Create(const CURL& url, CFileItem* pItem, const std::string& strMask)
+{
+ if (url.IsProtocol("stack")) // disqualify stack as we need to work with each of the parts instead
+ return NULL;
+
+ /**
+ * Check available binary addons which can contain files with underlaid
+ * folders / files.
+ * Currently in vfs and audiodecoder addons.
+ *
+ * @note The file extensions are absolutely necessary for these in order to
+ * identify the associated add-on.
+ */
+ /**@{*/
+
+ // Get file extensions to find addon related to it.
+ std::string strExtension = URIUtils::GetExtension(url);
+ StringUtils::ToLower(strExtension);
+
+ if (!strExtension.empty() && CServiceBroker::IsAddonInterfaceUp())
+ {
+ /*!
+ * Scan here about audiodecoder addons.
+ *
+ * @note: Do not check audio decoder files that are already open, they cannot
+ * contain any further sub-folders.
+ */
+ if (!StringUtils::EndsWith(strExtension, KODI_ADDON_AUDIODECODER_TRACK_EXT))
+ {
+ auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetExtensionSupportedAddonInfos(
+ strExtension, CExtsMimeSupportList::FilterSelect::hasTracks);
+ for (const auto& addonInfo : addonInfos)
+ {
+ std::unique_ptr<CAudioDecoder> result = std::make_unique<CAudioDecoder>(addonInfo.second);
+ if (!result->CreateDecoder() || !result->ContainsFiles(url))
+ {
+ CLog::Log(LOGINFO,
+ "CFileDirectoryFactory::{}: Addon '{}' support extension '{}' but creation "
+ "failed (seems not supported), trying other addons and Kodi",
+ __func__, addonInfo.second->ID(), strExtension);
+ continue;
+ }
+ return result.release();
+ }
+ }
+
+ /*!
+ * Scan here about VFS addons.
+ */
+ for (const auto& vfsAddon : CServiceBroker::GetVFSAddonCache().GetAddonInstances())
+ {
+ if (vfsAddon->HasFileDirectories())
+ {
+ auto exts = StringUtils::Split(vfsAddon->GetExtensions(), "|");
+ if (std::find(exts.begin(), exts.end(), strExtension) != exts.end())
+ {
+ CVFSEntryIFileDirectoryWrapper* wrap = new CVFSEntryIFileDirectoryWrapper(vfsAddon);
+ if (wrap->ContainsFiles(url))
+ {
+ if (wrap->m_items.Size() == 1)
+ {
+ // one STORED file - collapse it down
+ *pItem = *wrap->m_items[0];
+ }
+ else
+ {
+ // compressed or more than one file -> create a dir
+ pItem->SetPath(wrap->m_items.GetPath());
+ }
+
+ // Check for folder, if yes return also wrap.
+ // Needed to fix for e.g. RAR files with only one file inside
+ pItem->m_bIsFolder = URIUtils::HasSlashAtEnd(pItem->GetPath());
+ if (pItem->m_bIsFolder)
+ return wrap;
+ }
+ else
+ {
+ pItem->m_bIsFolder = true;
+ }
+
+ delete wrap;
+ return nullptr;
+ }
+ }
+ }
+ }
+ /**@}*/
+
+ if (pItem->IsRSS())
+ return new CRSSDirectory();
+
+
+ if (pItem->IsDiscImage())
+ {
+#if defined(HAS_ISO9660PP)
+ CISO9660Directory* iso = new CISO9660Directory();
+ if (iso->Exists(pItem->GetURL()))
+ return iso;
+
+ delete iso;
+#endif
+
+#if defined(HAS_UDFREAD)
+ return new CUDFDirectory();
+#endif
+
+ return nullptr;
+ }
+
+#if defined(TARGET_ANDROID)
+ if (url.IsFileType("apk"))
+ {
+ CURL zipURL = URIUtils::CreateArchivePath("apk", url);
+
+ CFileItemList items;
+ CDirectory::GetDirectory(zipURL, items, strMask, DIR_FLAG_DEFAULTS);
+ if (items.Size() == 0) // no files
+ pItem->m_bIsFolder = true;
+ else if (items.Size() == 1 && items[0]->m_idepth == 0 && !items[0]->m_bIsFolder)
+ {
+ // one STORED file - collapse it down
+ *pItem = *items[0];
+ }
+ else
+ { // compressed or more than one file -> create a apk dir
+ pItem->SetURL(zipURL);
+ return new CAPKDirectory;
+ }
+ return NULL;
+ }
+#endif
+ if (url.IsFileType("zip"))
+ {
+ CURL zipURL = URIUtils::CreateArchivePath("zip", url);
+
+ CFileItemList items;
+ CDirectory::GetDirectory(zipURL, items, strMask, DIR_FLAG_DEFAULTS);
+ if (items.Size() == 0) // no files
+ pItem->m_bIsFolder = true;
+ else if (items.Size() == 1 && items[0]->m_idepth == 0 && !items[0]->m_bIsFolder)
+ {
+ // one STORED file - collapse it down
+ *pItem = *items[0];
+ }
+ else
+ { // compressed or more than one file -> create a zip dir
+ pItem->SetURL(zipURL);
+ return new CZipDirectory;
+ }
+ return NULL;
+ }
+ if (url.IsFileType("xbt"))
+ {
+ CURL xbtUrl = URIUtils::CreateArchivePath("xbt", url);
+ pItem->SetURL(xbtUrl);
+
+ return new CXbtDirectory();
+ }
+ if (url.IsFileType("xsp"))
+ { // XBMC Smart playlist - just XML renamed to XSP
+ // read the name of the playlist in
+ CSmartPlaylist playlist;
+ if (playlist.OpenAndReadName(url))
+ {
+ pItem->SetLabel(playlist.GetName());
+ pItem->SetLabelPreformatted(true);
+ }
+ IFileDirectory* pDir=new CSmartPlaylistDirectory;
+ return pDir; // treat as directory
+ }
+ if (CPlayListFactory::IsPlaylist(url))
+ { // Playlist file
+ // currently we only return the directory if it contains
+ // more than one file. Reason is that .pls and .m3u may be used
+ // for links to http streams etc.
+ IFileDirectory *pDir = new CPlaylistFileDirectory();
+ CFileItemList items;
+ if (pDir->GetDirectory(url, items))
+ {
+ if (items.Size() > 1)
+ return pDir;
+ }
+ delete pDir;
+ return NULL;
+ }
+
+ if (pItem->IsAudioBook())
+ {
+ if (!pItem->HasMusicInfoTag() || pItem->GetEndOffset() <= 0)
+ {
+ std::unique_ptr<CAudioBookFileDirectory> pDir(new CAudioBookFileDirectory);
+ if (pDir->ContainsFiles(url))
+ return pDir.release();
+ }
+ return NULL;
+ }
+ return NULL;
+}
+
diff --git a/xbmc/filesystem/FileDirectoryFactory.h b/xbmc/filesystem/FileDirectoryFactory.h
new file mode 100644
index 0000000..b5b4575
--- /dev/null
+++ b/xbmc/filesystem/FileDirectoryFactory.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "IFileDirectory.h"
+
+#include <string>
+
+class CFileItem;
+
+namespace XFILE
+{
+class CFileDirectoryFactory
+{
+public:
+ CFileDirectoryFactory(void);
+ virtual ~CFileDirectoryFactory(void);
+ static IFileDirectory* Create(const CURL& url, CFileItem* pItem, const std::string& strMask="");
+};
+}
diff --git a/xbmc/filesystem/FileFactory.cpp b/xbmc/filesystem/FileFactory.cpp
new file mode 100644
index 0000000..b248255
--- /dev/null
+++ b/xbmc/filesystem/FileFactory.cpp
@@ -0,0 +1,180 @@
+/*
+ * 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 "network/Network.h"
+#include "FileFactory.h"
+#ifdef TARGET_POSIX
+#include "platform/posix/filesystem/PosixFile.h"
+#elif defined(TARGET_WINDOWS)
+#include "platform/win32/filesystem/Win32File.h"
+#ifdef TARGET_WINDOWS_STORE
+#include "platform/win10/filesystem/WinLibraryFile.h"
+#endif
+#endif // TARGET_WINDOWS
+#include "CurlFile.h"
+#include "DAVFile.h"
+#include "ShoutcastFile.h"
+#ifdef HAS_FILESYSTEM_SMB
+#ifdef TARGET_WINDOWS
+#include "platform/win32/filesystem/Win32SMBFile.h"
+#else
+#include "platform/posix/filesystem/SMBFile.h"
+#endif
+#endif
+#include "CDDAFile.h"
+#if defined(HAS_ISO9660PP)
+#include "ISO9660File.h"
+#endif
+#if defined(TARGET_ANDROID)
+#include "platform/android/filesystem/APKFile.h"
+#endif
+#include "XbtFile.h"
+#include "ZipFile.h"
+#ifdef HAS_FILESYSTEM_NFS
+#include "NFSFile.h"
+#endif
+#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
+#ifdef HAVE_LIBBLURAY
+#include "BlurayFile.h"
+#endif
+#include "PipeFile.h"
+#include "MusicDatabaseFile.h"
+#include "VideoDatabaseFile.h"
+#include "PluginFile.h"
+#include "SpecialProtocolFile.h"
+#include "MultiPathFile.h"
+#if defined(HAS_UDFREAD)
+#include "UDFFile.h"
+#endif
+#include "ImageFile.h"
+#include "ResourceFile.h"
+#include "URL.h"
+#include "utils/log.h"
+#include "network/WakeOnAccess.h"
+#include "utils/StringUtils.h"
+#include "ServiceBroker.h"
+#include "addons/VFSEntry.h"
+
+using namespace ADDON;
+using namespace XFILE;
+
+CFileFactory::CFileFactory() = default;
+
+CFileFactory::~CFileFactory() = default;
+
+IFile* CFileFactory::CreateLoader(const std::string& strFileName)
+{
+ CURL url(strFileName);
+ return CreateLoader(url);
+}
+
+IFile* CFileFactory::CreateLoader(const CURL& url)
+{
+ if (!CWakeOnAccess::GetInstance().WakeUpHost(url))
+ return NULL;
+
+ if (!url.GetProtocol().empty() && CServiceBroker::IsAddonInterfaceUp())
+ {
+ for (const auto& vfsAddon : CServiceBroker::GetVFSAddonCache().GetAddonInstances())
+ {
+ auto prots = StringUtils::Split(vfsAddon->GetProtocols(), "|");
+
+ if (vfsAddon->HasFiles() && std::find(prots.begin(), prots.end(), url.GetProtocol()) != prots.end())
+ return new CVFSEntryIFileWrapper(vfsAddon);
+ }
+ }
+
+#if defined(TARGET_ANDROID)
+ if (url.IsProtocol("apk")) return new CAPKFile();
+#endif
+ if (url.IsProtocol("zip")) return new CZipFile();
+ else if (url.IsProtocol("xbt")) return new CXbtFile();
+ else if (url.IsProtocol("musicdb")) return new CMusicDatabaseFile();
+ else if (url.IsProtocol("videodb")) return new CVideoDatabaseFile();
+ else if (url.IsProtocol("plugin")) return new CPluginFile();
+ else if (url.IsProtocol("library")) return nullptr;
+ else if (url.IsProtocol("pvr")) return nullptr;
+ else if (url.IsProtocol("special")) return new CSpecialProtocolFile();
+ 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())
+ {
+#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())
+ {
+#ifdef TARGET_WINDOWS_STORE
+ if (CWinLibraryFile::IsInAccessList(url))
+ return new CWinLibraryFile();
+#endif
+ return new CWin32File();
+ }
+#endif // TARGET_WINDOWS
+#if defined(HAS_DVD_DRIVE)
+ else if (url.IsProtocol("cdda")) return new CFileCDDA();
+#endif
+#if defined(HAS_ISO9660PP)
+ else if (url.IsProtocol("iso9660"))
+ return new CISO9660File();
+#endif
+#if defined(HAS_UDFREAD)
+ else if(url.IsProtocol("udf"))
+ return new CUDFFile();
+#endif
+#if defined(TARGET_ANDROID)
+ else if (url.IsProtocol("androidapp")) return new CFileAndroidApp();
+#endif
+ else if (url.IsProtocol("pipe")) return new CPipeFile();
+#ifdef HAVE_LIBBLURAY
+ else if (url.IsProtocol("bluray")) return new CBlurayFile();
+#endif
+ else if (url.IsProtocol("resource")) return new CResourceFile();
+#ifdef TARGET_WINDOWS_STORE
+ else if (CWinLibraryFile::IsValid(url)) return new CWinLibraryFile();
+#endif
+
+ if (url.IsProtocol("ftp")
+ || url.IsProtocol("ftps")
+ || url.IsProtocol("rss")
+ || url.IsProtocol("rsss")
+ || url.IsProtocol("http")
+ || url.IsProtocol("https")) return new CCurlFile();
+ else if (url.IsProtocol("dav") || url.IsProtocol("davs")) return new CDAVFile();
+ else if (url.IsProtocol("shout") || url.IsProtocol("shouts")) return new CShoutcastFile();
+#ifdef HAS_FILESYSTEM_SMB
+#ifdef TARGET_WINDOWS
+ else if (url.IsProtocol("smb")) return new CWin32SMBFile();
+#else
+ else if (url.IsProtocol("smb")) return new CSMBFile();
+#endif
+#endif
+#ifdef HAS_FILESYSTEM_NFS
+ else if (url.IsProtocol("nfs")) return new CNFSFile();
+#endif
+#ifdef HAS_UPNP
+ else if (url.IsProtocol("upnp")) return new CUPnPFile();
+#endif
+
+ CLog::Log(LOGWARNING, "{} - unsupported protocol({}) in {}", __FUNCTION__, url.GetProtocol(),
+ url.GetRedacted());
+ return NULL;
+}
diff --git a/xbmc/filesystem/FileFactory.h b/xbmc/filesystem/FileFactory.h
new file mode 100644
index 0000000..ec0fd10
--- /dev/null
+++ b/xbmc/filesystem/FileFactory.h
@@ -0,0 +1,29 @@
+/*
+ * 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
+
+// FileFactory1.h: interface for the CFileFactory class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "IFile.h"
+
+#include <string>
+
+namespace XFILE
+{
+class CFileFactory
+{
+public:
+ CFileFactory();
+ virtual ~CFileFactory();
+ static IFile* CreateLoader(const std::string& strFileName);
+ static IFile* CreateLoader(const CURL& url);
+};
+}
diff --git a/xbmc/filesystem/HTTPDirectory.cpp b/xbmc/filesystem/HTTPDirectory.cpp
new file mode 100644
index 0000000..0097b7f
--- /dev/null
+++ b/xbmc/filesystem/HTTPDirectory.cpp
@@ -0,0 +1,318 @@
+/*
+ * 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 "HTTPDirectory.h"
+
+#include "CurlFile.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CharsetConverter.h"
+#include "utils/HTMLUtil.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <climits>
+
+using namespace XFILE;
+
+CHTTPDirectory::CHTTPDirectory(void) = default;
+CHTTPDirectory::~CHTTPDirectory(void) = default;
+
+bool CHTTPDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ CCurlFile http;
+
+ const std::string& strBasePath = url.GetFileName();
+
+ if(!http.Open(url))
+ {
+ CLog::Log(LOGERROR, "{} - Unable to get http directory ({})", __FUNCTION__, url.GetRedacted());
+ return false;
+ }
+
+ CRegExp reItem(true); // HTML is case-insensitive
+ reItem.RegComp("<a href=\"([^\"]*)\"[^>]*>\\s*(.*?)\\s*</a>(.+?)(?=<a|</tr|$)");
+
+ CRegExp reDateTimeHtml(true);
+ reDateTimeHtml.RegComp(
+ "<td align=\"right\">([0-9]{2})-([A-Z]{3})-([0-9]{4}) ([0-9]{2}):([0-9]{2}) +</td>");
+
+ CRegExp reDateTimeLighttp(true);
+ reDateTimeLighttp.RegComp(
+ "<td class=\"m\">([0-9]{4})-([A-Z]{3})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})</td>");
+
+ CRegExp reDateTimeNginx(true);
+ reDateTimeNginx.RegComp("([0-9]{2})-([A-Z]{3})-([0-9]{4}) ([0-9]{2}):([0-9]{2})");
+
+ CRegExp reDateTimeNginxFancy(true);
+ reDateTimeNginxFancy.RegComp(
+ "<td class=\"date\">([0-9]{4})-([A-Z]{3})-([0-9]{2}) ([0-9]{2}):([0-9]{2})</td>");
+
+ CRegExp reDateTimeApacheNewFormat(true);
+ reDateTimeApacheNewFormat.RegComp(
+ "<td align=\"right\">([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}) +</td>");
+
+ CRegExp reDateTime(true);
+ reDateTime.RegComp("([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2})");
+
+ CRegExp reSizeHtml(true);
+ reSizeHtml.RegComp("> *([0-9.]+) *(B|K|M|G| )(iB)?</td>");
+
+ CRegExp reSize(true);
+ reSize.RegComp(" +([0-9]+)(B|K|M|G)?(?=\\s|<|$)");
+
+ /* read response from server into string buffer */
+ std::string strBuffer;
+ if (http.ReadData(strBuffer) && strBuffer.length() > 0)
+ {
+ /* if Content-Length is found and its not text/html, URL is pointing to file so don't treat URL as HTTPDirectory */
+ if (!http.GetHttpHeader().GetValue("Content-Length").empty() &&
+ !StringUtils::StartsWithNoCase(http.GetHttpHeader().GetValue("Content-type"), "text/html"))
+ {
+ return false;
+ }
+
+ std::string fileCharset(http.GetProperty(XFILE::FILE_PROPERTY_CONTENT_CHARSET));
+ if (!fileCharset.empty() && fileCharset != "UTF-8")
+ {
+ std::string converted;
+ if (g_charsetConverter.ToUtf8(fileCharset, strBuffer, converted) && !converted.empty())
+ strBuffer = converted;
+ }
+
+ unsigned int bufferOffset = 0;
+ while (bufferOffset < strBuffer.length())
+ {
+ int matchOffset = reItem.RegFind(strBuffer.c_str(), bufferOffset);
+ if (matchOffset < 0)
+ break;
+
+ bufferOffset = matchOffset + reItem.GetSubLength(0);
+
+ std::string strLink = reItem.GetMatch(1);
+ std::string strName = reItem.GetMatch(2);
+ std::string strMetadata = reItem.GetMatch(3);
+ StringUtils::Trim(strMetadata);
+
+ if(strLink[0] == '/')
+ strLink = strLink.substr(1);
+
+ std::string strNameTemp = StringUtils::Trim(strName);
+
+ std::wstring wName, wLink, wConverted;
+ if (fileCharset.empty())
+ g_charsetConverter.unknownToUTF8(strNameTemp);
+ g_charsetConverter.utf8ToW(strNameTemp, wName, false);
+ HTML::CHTMLUtil::ConvertHTMLToW(wName, wConverted);
+ g_charsetConverter.wToUTF8(wConverted, strNameTemp);
+ URIUtils::RemoveSlashAtEnd(strNameTemp);
+
+ std::string strLinkBase = strLink;
+ std::string strLinkOptions;
+
+ // split link with url options
+ size_t pos = strLinkBase.find('?');
+ if (pos != std::string::npos)
+ {
+ strLinkOptions = strLinkBase.substr(pos);
+ strLinkBase.erase(pos);
+ }
+
+ // strip url fragment from the link
+ pos = strLinkBase.find('#');
+ if (pos != std::string::npos)
+ {
+ strLinkBase.erase(pos);
+ }
+
+ // Convert any HTTP character entities (e.g.: "&amp;") to percentage encoding
+ // (e.g.: "%xx") as some web servers (Apache) put these in HTTP Directory Indexes
+ // this is also needed as CURL objects interpret them incorrectly due to the ;
+ // also being allowed as URL option separator
+ if (fileCharset.empty())
+ g_charsetConverter.unknownToUTF8(strLinkBase);
+ g_charsetConverter.utf8ToW(strLinkBase, wLink, false);
+ HTML::CHTMLUtil::ConvertHTMLToW(wLink, wConverted);
+ g_charsetConverter.wToUTF8(wConverted, strLinkBase);
+
+ // encoding + and ; to URL encode if it is not already encoded by http server used on the remote server (example: Apache)
+ // more characters may be added here when required when required by certain http servers
+ pos = strLinkBase.find_first_of("+;");
+ while (pos != std::string::npos)
+ {
+ std::stringstream convert;
+ convert << '%' << std::hex << int(strLinkBase.at(pos));
+ strLinkBase.replace(pos, 1, convert.str());
+ pos = strLinkBase.find_first_of("+;");
+ }
+
+ std::string strLinkTemp = strLinkBase;
+
+ URIUtils::RemoveSlashAtEnd(strLinkTemp);
+ strLinkTemp = CURL::Decode(strLinkTemp);
+
+ if (StringUtils::EndsWith(strNameTemp, "..>") &&
+ StringUtils::StartsWith(strLinkTemp, strNameTemp.substr(0, strNameTemp.length() - 3)))
+ strName = strNameTemp = strLinkTemp;
+
+ /* Per RFC 1808 § 5.3, relative paths containing a colon ":" should be either prefixed with
+ * "./" or escaped (as "%3A"). This handles the prefix case, the escaping should be handled by
+ * the CURL::Decode above
+ * - https://tools.ietf.org/html/rfc1808#section-5.3
+ */
+ auto NameMatchesLink([](const std::string& name, const std::string& link) -> bool
+ {
+ return (name == link) ||
+ ((std::string::npos != name.find(':')) && (std::string{"./"}.append(name) == link));
+ });
+
+ // we detect http directory items by its display name and its stripped link
+ // if same, we consider it as a valid item.
+ if (strLinkTemp != ".." && strLinkTemp != "" && NameMatchesLink(strNameTemp, strLinkTemp))
+ {
+ CFileItemPtr pItem(new CFileItem(strNameTemp));
+ pItem->SetProperty("IsHTTPDirectory", true);
+ CURL url2(url);
+
+ url2.SetFileName(strBasePath + strLinkBase);
+ url2.SetOptions(strLinkOptions);
+ pItem->SetURL(url2);
+
+ if(URIUtils::HasSlashAtEnd(pItem->GetPath(), true))
+ pItem->m_bIsFolder = true;
+
+ std::string day, month, year, hour, minute;
+ int monthNum = 0;
+
+ if (reDateTimeHtml.RegFind(strMetadata.c_str()) >= 0)
+ {
+ day = reDateTimeHtml.GetMatch(1);
+ month = reDateTimeHtml.GetMatch(2);
+ year = reDateTimeHtml.GetMatch(3);
+ hour = reDateTimeHtml.GetMatch(4);
+ minute = reDateTimeHtml.GetMatch(5);
+ }
+ else if (reDateTimeNginxFancy.RegFind(strMetadata.c_str()) >= 0)
+ {
+ day = reDateTimeNginxFancy.GetMatch(3);
+ month = reDateTimeNginxFancy.GetMatch(2);
+ year = reDateTimeNginxFancy.GetMatch(1);
+ hour = reDateTimeNginxFancy.GetMatch(4);
+ minute = reDateTimeNginxFancy.GetMatch(5);
+ }
+ else if (reDateTimeNginx.RegFind(strMetadata.c_str()) >= 0)
+ {
+ day = reDateTimeNginx.GetMatch(1);
+ month = reDateTimeNginx.GetMatch(2);
+ year = reDateTimeNginx.GetMatch(3);
+ hour = reDateTimeNginx.GetMatch(4);
+ minute = reDateTimeNginx.GetMatch(5);
+ }
+ else if (reDateTimeLighttp.RegFind(strMetadata.c_str()) >= 0)
+ {
+ day = reDateTimeLighttp.GetMatch(3);
+ month = reDateTimeLighttp.GetMatch(2);
+ year = reDateTimeLighttp.GetMatch(1);
+ hour = reDateTimeLighttp.GetMatch(4);
+ minute = reDateTimeLighttp.GetMatch(5);
+ }
+ else if (reDateTimeApacheNewFormat.RegFind(strMetadata.c_str()) >= 0)
+ {
+ day = reDateTimeApacheNewFormat.GetMatch(3);
+ monthNum = atoi(reDateTimeApacheNewFormat.GetMatch(2).c_str());
+ year = reDateTimeApacheNewFormat.GetMatch(1);
+ hour = reDateTimeApacheNewFormat.GetMatch(4);
+ minute = reDateTimeApacheNewFormat.GetMatch(5);
+ }
+ else if (reDateTime.RegFind(strMetadata.c_str()) >= 0)
+ {
+ day = reDateTime.GetMatch(3);
+ monthNum = atoi(reDateTime.GetMatch(2).c_str());
+ year = reDateTime.GetMatch(1);
+ hour = reDateTime.GetMatch(4);
+ minute = reDateTime.GetMatch(5);
+ }
+
+ if (month.length() > 0)
+ monthNum = CDateTime::MonthStringToMonthNum(month);
+
+ if (day.length() > 0 && monthNum > 0 && year.length() > 0)
+ {
+ pItem->m_dateTime = CDateTime(atoi(year.c_str()), monthNum, atoi(day.c_str()), atoi(hour.c_str()), atoi(minute.c_str()), 0);
+ }
+
+ if (!pItem->m_bIsFolder)
+ {
+ if (reSizeHtml.RegFind(strMetadata.c_str()) >= 0)
+ {
+ double Size = atof(reSizeHtml.GetMatch(1).c_str());
+ std::string strUnit(reSizeHtml.GetMatch(2));
+
+ if (strUnit == "K")
+ Size = Size * 1024;
+ else if (strUnit == "M")
+ Size = Size * 1024 * 1024;
+ else if (strUnit == "G")
+ Size = Size * 1024 * 1024 * 1024;
+
+ pItem->m_dwSize = (int64_t)Size;
+ }
+ else if (reSize.RegFind(strMetadata.c_str()) >= 0)
+ {
+ double Size = atof(reSize.GetMatch(1).c_str());
+ std::string strUnit(reSize.GetMatch(2));
+
+ if (strUnit == "K")
+ Size = Size * 1024;
+ else if (strUnit == "M")
+ Size = Size * 1024 * 1024;
+ else if (strUnit == "G")
+ Size = Size * 1024 * 1024 * 1024;
+
+ pItem->m_dwSize = (int64_t)Size;
+ }
+ else
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bHTTPDirectoryStatFilesize) // As a fallback get the size by stat-ing the file (slow)
+ {
+ CCurlFile file;
+ file.Open(url);
+ pItem->m_dwSize=file.GetLength();
+ file.Close();
+ }
+ }
+ items.Add(pItem);
+ }
+ }
+ }
+ http.Close();
+
+ items.SetProperty("IsHTTPDirectory", true);
+
+ return true;
+}
+
+bool CHTTPDirectory::Exists(const CURL &url)
+{
+ CCurlFile http;
+ struct __stat64 buffer;
+
+ if( http.Stat(url, &buffer) != 0 )
+ {
+ return false;
+ }
+
+ if (buffer.st_mode == _S_IFDIR)
+ return true;
+
+ return false;
+}
diff --git a/xbmc/filesystem/HTTPDirectory.h b/xbmc/filesystem/HTTPDirectory.h
new file mode 100644
index 0000000..02a27d9
--- /dev/null
+++ b/xbmc/filesystem/HTTPDirectory.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "IDirectory.h"
+
+namespace XFILE
+{
+ class CHTTPDirectory : public IDirectory
+ {
+ public:
+ CHTTPDirectory(void);
+ ~CHTTPDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool Exists(const CURL& url) override;
+ DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ONCE; }
+
+ private:
+ };
+}
diff --git a/xbmc/filesystem/IDirectory.cpp b/xbmc/filesystem/IDirectory.cpp
new file mode 100644
index 0000000..b2ad59a
--- /dev/null
+++ b/xbmc/filesystem/IDirectory.cpp
@@ -0,0 +1,177 @@
+/*
+ * 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 "IDirectory.h"
+
+#include "PasswordManager.h"
+#include "URL.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace KODI::MESSAGING;
+using namespace XFILE;
+
+const CProfileManager *IDirectory::m_profileManager = nullptr;
+
+void IDirectory::RegisterProfileManager(const CProfileManager &profileManager)
+{
+ m_profileManager = &profileManager;
+}
+
+void IDirectory::UnregisterProfileManager()
+{
+ m_profileManager = nullptr;
+}
+
+IDirectory::IDirectory()
+{
+ m_flags = DIR_FLAG_DEFAULTS;
+}
+
+IDirectory::~IDirectory(void) = default;
+
+/*!
+ \brief Test if file have an allowed extension, as specified with SetMask()
+ \param strFile File to test
+ \return \e true if file is allowed
+ \note If extension is ".ifo", filename format must be "vide_ts.ifo" or
+ "vts_##_0.ifo". If extension is ".dat", filename format must be
+ "AVSEQ##(#).DAT", "ITEM###(#).DAT" or "MUSIC##(#).DAT".
+ */
+bool IDirectory::IsAllowed(const CURL& url) const
+{
+ if (m_strFileMask.empty())
+ return true;
+
+ // Check if strFile have an allowed extension
+ if (!URIUtils::HasExtension(url, m_strFileMask))
+ return false;
+
+ // We should ignore all non dvd/vcd related ifo and dat files.
+ if (URIUtils::HasExtension(url, ".ifo"))
+ {
+ std::string fileName = URIUtils::GetFileName(url);
+
+ // Allow filenames of the form video_ts.ifo or vts_##_0.ifo
+
+ return StringUtils::EqualsNoCase(fileName, "video_ts.ifo") ||
+ (fileName.length() == 12 &&
+ StringUtils::StartsWithNoCase(fileName, "vts_") &&
+ StringUtils::EndsWithNoCase(fileName, "_0.ifo"));
+ }
+
+ if (URIUtils::HasExtension(url, ".dat"))
+ {
+ std::string fileName = URIUtils::GetFileName(url);
+ std::string folder = URIUtils::GetDirectory(fileName);
+ URIUtils::RemoveSlashAtEnd(folder);
+ folder = URIUtils::GetFileName(folder);
+ if (StringUtils::EqualsNoCase(folder, "vcd") ||
+ StringUtils::EqualsNoCase(folder, "mpegav") ||
+ StringUtils::EqualsNoCase(folder, "cdda"))
+ {
+ // Allow filenames of the form AVSEQ##(#).DAT, ITEM###(#).DAT
+ // and MUSIC##(#).DAT
+ return (fileName.length() == 11 || fileName.length() == 12) &&
+ (StringUtils::StartsWithNoCase(fileName, "AVSEQ") ||
+ StringUtils::StartsWithNoCase(fileName, "MUSIC") ||
+ StringUtils::StartsWithNoCase(fileName, "ITEM"));
+ }
+ }
+ return true;
+}
+
+/*!
+ \brief Set a mask of extensions for the files in the directory.
+ \param strMask Mask of file extensions that are allowed.
+
+ The mask has to look like the following: \n
+ \verbatim
+ .m4a|.flac|.aac|
+ \endverbatim
+ So only *.m4a, *.flac, *.aac files will be retrieved with GetDirectory().
+ */
+void IDirectory::SetMask(const std::string& strMask)
+{
+ m_strFileMask = strMask;
+ // ensure it's completed with a | so that filtering is easy.
+ StringUtils::ToLower(m_strFileMask);
+ if (m_strFileMask.size() && m_strFileMask[m_strFileMask.size() - 1] != '|')
+ m_strFileMask += '|';
+}
+
+/*!
+ \brief Set the flags for this directory handler.
+ \param flags - \sa XFILE::DIR_FLAG for a description.
+ */
+void IDirectory::SetFlags(int flags)
+{
+ m_flags = flags;
+}
+
+bool IDirectory::ProcessRequirements()
+{
+ std::string type = m_requirements["type"].asString();
+ if (type == "keyboard")
+ {
+ std::string input;
+ if (CGUIKeyboardFactory::ShowAndGetInput(input, m_requirements["heading"], false, m_requirements["hidden"].asBoolean()))
+ {
+ m_requirements["input"] = input;
+ return true;
+ }
+ }
+ else if (type == "authenticate")
+ {
+ CURL url(m_requirements["url"].asString());
+ if (CPasswordManager::GetInstance().PromptToAuthenticateURL(url))
+ {
+ m_requirements.clear();
+ return true;
+ }
+ }
+ else if (type == "error")
+ {
+ HELPERS::ShowOKDialogLines(CVariant{m_requirements["heading"]}, CVariant{m_requirements["line1"]}, CVariant{m_requirements["line2"]}, CVariant{m_requirements["line3"]});
+ }
+ m_requirements.clear();
+ return false;
+}
+
+bool IDirectory::GetKeyboardInput(const CVariant &heading, std::string &input, bool hiddenInput)
+{
+ if (!m_requirements["input"].asString().empty())
+ {
+ input = m_requirements["input"].asString();
+ return true;
+ }
+ m_requirements.clear();
+ m_requirements["type"] = "keyboard";
+ m_requirements["heading"] = heading;
+ m_requirements["hidden"] = hiddenInput;
+ return false;
+}
+
+void IDirectory::SetErrorDialog(const CVariant &heading, const CVariant &line1, const CVariant &line2, const CVariant &line3)
+{
+ m_requirements.clear();
+ m_requirements["type"] = "error";
+ m_requirements["heading"] = heading;
+ m_requirements["line1"] = line1;
+ m_requirements["line2"] = line2;
+ m_requirements["line3"] = line3;
+}
+
+void IDirectory::RequireAuthentication(const CURL &url)
+{
+ m_requirements.clear();
+ m_requirements["type"] = "authenticate";
+ m_requirements["url"] = url.Get();
+}
diff --git a/xbmc/filesystem/IDirectory.h b/xbmc/filesystem/IDirectory.h
new file mode 100644
index 0000000..5667dc3
--- /dev/null
+++ b/xbmc/filesystem/IDirectory.h
@@ -0,0 +1,175 @@
+/*
+ * 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 "utils/Variant.h"
+
+#include <string>
+
+class CFileItemList;
+class CProfileManager;
+class CURL;
+
+namespace XFILE
+{
+ enum DIR_CACHE_TYPE
+ {
+ DIR_CACHE_NEVER = 0, ///< Never cache this directory to memory
+ DIR_CACHE_ONCE, ///< Cache this directory to memory for each fetch (so that FileExists() checks are fast)
+ DIR_CACHE_ALWAYS ///< Always cache this directory to memory, so that each additional fetch of this folder will utilize the cache (until it's cleared)
+ };
+
+ /*! \brief Available directory flags
+ The defaults are to allow file directories, no prompting, retrieve file information, hide hidden files, and utilise the directory cache
+ based on the implementation's wishes.
+ */
+ enum DIR_FLAG
+ {
+ DIR_FLAG_DEFAULTS = 0,
+ DIR_FLAG_NO_FILE_DIRS = (2 << 0), ///< Don't convert files (zip, rar etc.) to directories
+ DIR_FLAG_ALLOW_PROMPT = (2 << 1), ///< Allow prompting for further info (passwords etc.)
+ DIR_FLAG_NO_FILE_INFO = (2 << 2), ///< Don't read additional file info (stat for example)
+ DIR_FLAG_GET_HIDDEN = (2 << 3), ///< Get hidden files
+ DIR_FLAG_READ_CACHE = (2 << 4), ///< Force reading from the directory cache (if available)
+ DIR_FLAG_BYPASS_CACHE = (2 << 5) ///< Completely bypass the directory cache (no reading, no writing)
+ };
+/*!
+ \ingroup filesystem
+ \brief Interface to the directory on a file system.
+
+ This Interface is retrieved from CDirectoryFactory and can be used to
+ access the directories on a filesystem.
+ \sa CDirectoryFactory
+ */
+class IDirectory
+{
+public:
+ static void RegisterProfileManager(const CProfileManager &profileManager);
+ static void UnregisterProfileManager();
+
+ IDirectory();
+ virtual ~IDirectory(void);
+ /*!
+ \brief Get the \e items of the directory \e strPath.
+ \param url Directory to read.
+ \param items Retrieves the directory entries.
+ \return Returns \e true, if successful.
+ \sa CDirectoryFactory
+ */
+ virtual bool GetDirectory(const CURL& url, CFileItemList &items) = 0;
+ /*!
+ \brief Retrieve the progress of the current directory fetch (if possible).
+ \return the progress as a float in the range 0..100.
+ \sa GetDirectory, CancelDirectory
+ */
+ virtual float GetProgress() const { return 0.0f; }
+ /*!
+ \brief Cancel the current directory fetch (if possible).
+ \sa GetDirectory
+ */
+ virtual void CancelDirectory() {}
+ /*!
+ \brief Create the directory
+ \param url Directory to create.
+ \return Returns \e true, if directory is created or if it already exists
+ \sa CDirectoryFactory
+ */
+ virtual bool Create(const CURL& url) { return false; }
+ /*!
+ \brief Check for directory existence
+ \param url Directory to check.
+ \return Returns \e true, if directory exists
+ \sa CDirectoryFactory
+ */
+ virtual bool Exists(const CURL& url) { return false; }
+ /*!
+ \brief Removes the directory
+ \param url Directory to remove.
+ \return Returns \e false if not successful
+ */
+ virtual bool Remove(const CURL& url) { return false; }
+
+ /*!
+ \brief Recursively removes the directory
+ \param url Directory to remove.
+ \return Returns \e false if not successful
+ */
+ virtual bool RemoveRecursive(const CURL& url) { return false; }
+
+ /*!
+ \brief Whether this file should be listed
+ \param url File to test.
+ \return Returns \e true if the file should be listed
+ */
+ virtual bool IsAllowed(const CURL& url) const;
+
+ /*! \brief Whether to allow all files/folders to be listed.
+ \return Returns \e true if all files/folder should be listed.
+ */
+ virtual bool AllowAll() const { return false; }
+
+ /*!
+ \brief How this directory should be cached
+ \param url Directory at hand.
+ \return Returns the cache type.
+ */
+ virtual DIR_CACHE_TYPE GetCacheType(const CURL& url) const { return DIR_CACHE_ONCE; }
+
+ void SetMask(const std::string& strMask);
+ void SetFlags(int flags);
+
+ /*! \brief Process additional requirements before the directory fetch is performed.
+ Some directory fetches may require authentication, keyboard input etc. The IDirectory subclass
+ should call GetKeyboardInput, SetErrorDialog or RequireAuthentication and then return false
+ from the GetDirectory method. CDirectory will then prompt for input from the user, before
+ re-calling the GetDirectory method.
+ \sa GetKeyboardInput, SetErrorDialog, RequireAuthentication
+ */
+ bool ProcessRequirements();
+
+protected:
+ /*! \brief Prompt the user for some keyboard input
+ Call this method from the GetDirectory method to retrieve additional input from the user.
+ If this function returns false then no input has been received, and the GetDirectory call
+ should return false.
+ \param heading an integer or string heading for the keyboard dialog
+ \param input [out] the returned input (if available).
+ \return true if keyboard input has been received. False if it hasn't.
+ \sa ProcessRequirements
+ */
+ bool GetKeyboardInput(const CVariant &heading, std::string &input, bool hiddenInput = false);
+
+ /*! \brief Show an error dialog on failure of GetDirectory call
+ Call this method from the GetDirectory method to set an error message to be shown to the user
+ \param heading an integer or string heading for the error dialog.
+ \param line1 the first line to be displayed (integer or string).
+ \param line2 the first line to be displayed (integer or string).
+ \param line3 the first line to be displayed (integer or string).
+ \sa ProcessRequirements
+ */
+ void SetErrorDialog(const CVariant &heading, const CVariant &line1, const CVariant &line2 = 0, const CVariant &line3 = 0);
+
+ /*! \brief Prompt the user for authentication of a URL.
+ Call this method from the GetDirectory method when authentication is required from the user, before returning
+ false from the GetDirectory call. The user will be prompted for authentication, and GetDirectory will be
+ re-called.
+ \param url the URL to authenticate.
+ \sa ProcessRequirements
+ */
+ void RequireAuthentication(const CURL& url);
+
+ static const CProfileManager *m_profileManager;
+
+ std::string m_strFileMask; ///< Holds the file mask specified by SetMask()
+
+ int m_flags; ///< Directory flags - see DIR_FLAG
+
+ CVariant m_requirements;
+};
+}
diff --git a/xbmc/filesystem/IFile.cpp b/xbmc/filesystem/IFile.cpp
new file mode 100644
index 0000000..eddffea
--- /dev/null
+++ b/xbmc/filesystem/IFile.cpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2002 Frodo
+ * Portions Copyright (c) by the authors of ffmpeg and xvid
+ * Copyright (C) 2002-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 "IFile.h"
+
+#include "URL.h"
+
+#include <cstring>
+#include <errno.h>
+
+using namespace XFILE;
+
+//////////////////////////////////////////////////////////////////////
+// Construction/Destruction
+//////////////////////////////////////////////////////////////////////
+
+IFile::IFile() = default;
+
+IFile::~IFile() = default;
+
+int IFile::Stat(struct __stat64* buffer)
+{
+ if (buffer)
+ *buffer = {};
+
+ errno = ENOENT;
+ return -1;
+}
+bool IFile::ReadString(char *szLine, int iLineLength)
+{
+ if(Seek(0, SEEK_CUR) < 0) return false;
+
+ int64_t iFilePos = GetPosition();
+ int iBytesRead = Read( (unsigned char*)szLine, iLineLength - 1);
+ if (iBytesRead <= 0)
+ return false;
+
+ szLine[iBytesRead] = 0;
+
+ for (int i = 0; i < iBytesRead; i++)
+ {
+ if ('\n' == szLine[i])
+ {
+ if ('\r' == szLine[i + 1])
+ {
+ szLine[i + 1] = 0;
+ Seek(iFilePos + i + 2, SEEK_SET);
+ }
+ else
+ {
+ // end of line
+ szLine[i + 1] = 0;
+ Seek(iFilePos + i + 1, SEEK_SET);
+ }
+ break;
+ }
+ else if ('\r' == szLine[i])
+ {
+ if ('\n' == szLine[i + 1])
+ {
+ szLine[i + 1] = 0;
+ Seek(iFilePos + i + 2, SEEK_SET);
+ }
+ else
+ {
+ // end of line
+ szLine[i + 1] = 0;
+ Seek(iFilePos + i + 1, SEEK_SET);
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+CRedirectException::CRedirectException() :
+ m_pNewFileImp(NULL), m_pNewUrl(NULL)
+{
+}
+
+CRedirectException::CRedirectException(IFile *pNewFileImp, CURL *pNewUrl) :
+ m_pNewFileImp(pNewFileImp), m_pNewUrl(pNewUrl)
+{
+}
diff --git a/xbmc/filesystem/IFile.h b/xbmc/filesystem/IFile.h
new file mode 100644
index 0000000..d76d6ed
--- /dev/null
+++ b/xbmc/filesystem/IFile.h
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2002 Frodo
+ * Portions Copyright (c) by the authors of ffmpeg and xvid
+ * Copyright (C) 2002-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
+
+// IFile.h: interface for the IFile class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "PlatformDefs.h" // for __stat64, ssize_t
+
+#include <stdio.h>
+#include <stdint.h>
+#include <sys/stat.h>
+#include <string>
+#include <vector>
+
+#if !defined(SIZE_MAX) || !defined(SSIZE_MAX)
+#include <limits.h>
+#ifndef SIZE_MAX
+#define SIZE_MAX UINTPTR_MAX
+#endif // ! SIZE_MAX
+#ifndef SSIZE_MAX
+#define SSIZE_MAX INTPTR_MAX
+#endif // ! SSIZE_MAX
+#endif // ! SIZE_MAX || ! SSIZE_MAX
+
+#include "IFileTypes.h"
+
+class CURL;
+
+namespace XFILE
+{
+
+class IFile
+{
+public:
+ IFile();
+ virtual ~IFile();
+
+ virtual bool Open(const CURL& url) = 0;
+ virtual bool OpenForWrite(const CURL& url, bool bOverWrite = false) { return false; }
+ virtual bool ReOpen(const CURL& url) { return false; }
+ virtual bool Exists(const CURL& url) = 0;
+ /**
+ * Fills struct __stat64 with information about file specified by url.
+ * For st_mode function will set correctly _S_IFDIR (directory) flag and may set
+ * _S_IREAD (read permission), _S_IWRITE (write permission) flags if such
+ * information is available. Function may set st_size (file size), st_atime,
+ * st_mtime, st_ctime (access, modification, creation times).
+ * Any other flags and members of __stat64 that didn't updated with actual file
+ * information will be set to zero (st_nlink can be set ether to 1 or zero).
+ * @param url specifies requested file
+ * @param buffer pointer to __stat64 buffer to receive information about file
+ * @return zero of success, -1 otherwise.
+ */
+ virtual int Stat(const CURL& url, struct __stat64* buffer) = 0;
+ /**
+ * Fills struct __stat64 with information about currently open file
+ * For st_mode function will set correctly _S_IFDIR (directory) flag and may set
+ * _S_IREAD (read permission), _S_IWRITE (write permission) flags if such
+ * information is available. Function may set st_size (file size), st_atime,
+ * st_mtime, st_ctime (access, modification, creation times).
+ * Any other flags and members of __stat64 that didn't updated with actual file
+ * information will be set to zero (st_nlink can be set ether to 1 or zero).
+ * @param buffer pointer to __stat64 buffer to receive information about file
+ * @return zero of success, -1 otherwise.
+ */
+ virtual int Stat(struct __stat64* buffer);
+ /**
+ * Attempt to read bufSize bytes from currently opened file into buffer bufPtr.
+ * @param bufPtr pointer to buffer
+ * @param bufSize size of the buffer
+ * @return number of successfully read bytes if any bytes were read and stored in
+ * buffer, zero if no bytes are available to read (end of file was reached)
+ * or undetectable error occur, -1 in case of any explicit error
+ */
+ virtual ssize_t Read(void* bufPtr, size_t bufSize) = 0;
+ /**
+ * Attempt to write bufSize bytes from buffer bufPtr into currently opened file.
+ * @param bufPtr pointer to buffer
+ * @param bufSize size of the buffer
+ * @return number of successfully written bytes if any bytes were written,
+ * zero if no bytes were written and no detectable error occur,
+ * -1 in case of any explicit error
+ */
+ virtual ssize_t Write(const void* bufPtr, size_t bufSize) { return -1;}
+ virtual bool ReadString(char *szLine, int iLineLength);
+ virtual int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) = 0;
+ virtual void Close() = 0;
+ virtual int64_t GetPosition() = 0;
+ virtual int64_t GetLength() = 0;
+ virtual void Flush() { }
+ virtual int Truncate(int64_t size) { return -1; }
+
+ /* Returns the minimum size that can be read from input stream. *
+ * For example cdrom access where access could be sector based. *
+ * This will cause file system to buffer read requests, to *
+ * to meet the requirement of CFile. *
+ * It can also be used to indicate a file system is non buffered *
+ * but accepts any read size, have it return the value 1 */
+ virtual int GetChunkSize() {return 0;}
+ virtual double GetDownloadSpeed() { return 0.0; }
+
+ virtual bool Delete(const CURL& url) { return false; }
+ virtual bool Rename(const CURL& url, const CURL& urlnew) { return false; }
+ virtual bool SetHidden(const CURL& url, bool hidden) { return false; }
+
+ virtual int IoControl(EIoControl request, void* param) { return -1; }
+
+ virtual const std::string GetProperty(XFILE::FileProperty type, const std::string &name = "") const
+ {
+ return type == XFILE::FILE_PROPERTY_CONTENT_TYPE ? "application/octet-stream" : "";
+ };
+
+ virtual const std::vector<std::string> GetPropertyValues(XFILE::FileProperty type, const std::string &name = "") const
+ {
+ std::vector<std::string> values;
+ std::string value = GetProperty(type, name);
+ if (!value.empty())
+ {
+ values.emplace_back(value);
+ }
+ return values;
+ }
+};
+
+class CRedirectException
+{
+public:
+ IFile *m_pNewFileImp;
+ CURL *m_pNewUrl;
+
+ CRedirectException();
+
+ CRedirectException(IFile *pNewFileImp, CURL *pNewUrl=NULL);
+};
+
+}
diff --git a/xbmc/filesystem/IFileDirectory.h b/xbmc/filesystem/IFileDirectory.h
new file mode 100644
index 0000000..357ed2e
--- /dev/null
+++ b/xbmc/filesystem/IFileDirectory.h
@@ -0,0 +1,22 @@
+/*
+ * 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 "IDirectory.h"
+
+namespace XFILE
+{
+class IFileDirectory : public IDirectory
+{
+public:
+ ~IFileDirectory(void) override = default;
+ virtual bool ContainsFiles(const CURL& url)=0;
+};
+
+}
diff --git a/xbmc/filesystem/IFileTypes.h b/xbmc/filesystem/IFileTypes.h
new file mode 100644
index 0000000..524d871
--- /dev/null
+++ b/xbmc/filesystem/IFileTypes.h
@@ -0,0 +1,108 @@
+/*
+ * 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 <stdint.h>
+
+namespace XFILE
+{
+
+/* indicate that caller can handle truncated reads, where function returns before entire buffer has been filled */
+ static const unsigned int READ_TRUNCATED = 0x01;
+
+/* indicate that that caller support read in the minimum defined chunk size, this disables internal cache then */
+ static const unsigned int READ_CHUNKED = 0x02;
+
+/* use cache to access this file */
+ static const unsigned int READ_CACHED = 0x04;
+
+/* open without caching. regardless to file type. */
+ static const unsigned int READ_NO_CACHE = 0x08;
+
+/* calculate bitrate for file while reading */
+ static const unsigned int READ_BITRATE = 0x10;
+
+/* indicate to the caller we will seek between multiple streams in the file frequently */
+ static const unsigned int READ_MULTI_STREAM = 0x20;
+
+/* indicate to the caller file is audio and/or video (and e.g. may grow) */
+ static const unsigned int READ_AUDIO_VIDEO = 0x40;
+
+/* indicate that caller will do write operations before reading */
+ static const unsigned int READ_AFTER_WRITE = 0x80;
+
+/* indicate that caller want to reopen a file if its already open */
+ static const unsigned int READ_REOPEN = 0x100;
+
+struct SNativeIoControl
+{
+ unsigned long int request;
+ void* param;
+};
+
+struct SCacheStatus
+{
+ uint64_t forward; /**< number of bytes cached forward of current position */
+ uint32_t maxrate; /**< maximum allowed read(fill) rate (bytes/second) */
+ uint32_t currate; /**< average read rate (bytes/second) since last position change */
+ uint32_t lowrate; /**< low speed read rate (bytes/second) (if any, else 0) */
+};
+
+typedef enum {
+ IOCTRL_NATIVE = 1, /**< SNativeIoControl structure, containing what should be passed to native ioctrl */
+ IOCTRL_SEEK_POSSIBLE = 2, /**< return 0 if known not to work, 1 if it should work */
+ IOCTRL_CACHE_STATUS = 3, /**< SCacheStatus structure */
+ IOCTRL_CACHE_SETRATE = 4, /**< unsigned int with speed limit for caching in bytes per second */
+ IOCTRL_SET_CACHE = 8, /**< CFileCache */
+ IOCTRL_SET_RETRY = 16, /**< Enable/disable retry within the protocol handler (if supported) */
+} EIoControl;
+
+enum CURLOPTIONTYPE
+{
+ CURL_OPTION_OPTION, /**< Set a general option */
+ CURL_OPTION_PROTOCOL, /**< Set a protocol option (see below) */
+ CURL_OPTION_CREDENTIALS,/**< Set User and password */
+ CURL_OPTION_HEADER /**< Add a Header */
+};
+
+/**
+ * The following names for CURL_OPTION_PROTOCOL are possible:
+ *
+ * accept-charset: Set the "accept-charset" header
+ * acceptencoding or encoding: Set the "accept-encoding" header
+ * active-remote: Set the "active-remote" header
+ * auth: Set the authentication method. Possible values: any, anysafe, digest, ntlm
+ * connection-timeout: Set the connection timeout in seconds
+ * cookie: Set the "cookie" header
+ * customrequest: Set a custom HTTP request like DELETE
+ * noshout: Set to true if kodi detects a stream as shoutcast by mistake.
+ * postdata: Set the post body (value needs to be base64 encoded). (Implicitly sets the request to POST)
+ * referer: Set the "referer" header
+ * user-agent: Set the "user-agent" header
+ * seekable: Set the stream seekable. 1: enable, 0: disable
+ * sslcipherlist: Set list of accepted SSL ciphers.
+ */
+
+enum FileProperty
+{
+ FILE_PROPERTY_RESPONSE_PROTOCOL, /**< Get response protocol line */
+ FILE_PROPERTY_RESPONSE_HEADER, /**< Get response Header value */
+ FILE_PROPERTY_CONTENT_TYPE, /**< Get file content-type */
+ FILE_PROPERTY_CONTENT_CHARSET, /**< Get file content charset */
+ FILE_PROPERTY_MIME_TYPE, /**< Get file mime type */
+ FILE_PROPERTY_EFFECTIVE_URL /**< Get effective URL for redirected streams */
+};
+
+class IFileCallback
+{
+public:
+ virtual bool OnFileCallback(void* pContext, int ipercent, float avgSpeed) = 0;
+ virtual ~IFileCallback() = default;
+};
+}
diff --git a/xbmc/filesystem/ISO9660Directory.cpp b/xbmc/filesystem/ISO9660Directory.cpp
new file mode 100644
index 0000000..894db53
--- /dev/null
+++ b/xbmc/filesystem/ISO9660Directory.cpp
@@ -0,0 +1,81 @@
+/*
+ * 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 "ISO9660Directory.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "utils/URIUtils.h"
+
+#include <cdio++/iso9660.hpp>
+
+using namespace XFILE;
+
+bool CISO9660Directory::GetDirectory(const CURL& url, CFileItemList& items)
+{
+ CURL url2(url);
+ if (!url2.IsProtocol("iso9660"))
+ {
+ url2.Reset();
+ url2.SetProtocol("iso9660");
+ url2.SetHostName(url.Get());
+ }
+
+ std::string strRoot(url2.Get());
+ std::string strSub(url2.GetFileName());
+
+ URIUtils::AddSlashAtEnd(strRoot);
+ URIUtils::AddSlashAtEnd(strSub);
+
+ std::unique_ptr<ISO9660::IFS> iso(new ISO9660::IFS);
+
+ if (!iso->open(url2.GetHostName().c_str()))
+ return false;
+
+ std::vector<ISO9660::Stat*> isoFiles;
+
+ if (iso->readdir(strSub.c_str(), isoFiles))
+ {
+ for (const auto file : isoFiles)
+ {
+ std::string filename(file->p_stat->filename);
+
+ if (file->p_stat->type == 2)
+ {
+ if (filename != "." && filename != "..")
+ {
+ CFileItemPtr pItem(new CFileItem(filename));
+ std::string strDir(strRoot + filename);
+ URIUtils::AddSlashAtEnd(strDir);
+ pItem->SetPath(strDir);
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+ }
+ }
+ else
+ {
+ CFileItemPtr pItem(new CFileItem(filename));
+ pItem->SetPath(strRoot + filename);
+ pItem->m_bIsFolder = false;
+ pItem->m_dwSize = file->p_stat->size;
+ items.Add(pItem);
+ }
+ }
+
+ isoFiles.clear();
+ return true;
+ }
+
+ return false;
+}
+
+bool CISO9660Directory::Exists(const CURL& url)
+{
+ CFileItemList items;
+ return GetDirectory(url, items);
+}
diff --git a/xbmc/filesystem/ISO9660Directory.h b/xbmc/filesystem/ISO9660Directory.h
new file mode 100644
index 0000000..6c624ff
--- /dev/null
+++ b/xbmc/filesystem/ISO9660Directory.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "IFileDirectory.h"
+
+namespace XFILE
+{
+
+class CISO9660Directory : public IFileDirectory
+{
+public:
+ CISO9660Directory() = default;
+ ~CISO9660Directory() = default;
+ bool GetDirectory(const CURL& url, CFileItemList& items) override;
+ bool Exists(const CURL& url) override;
+ bool ContainsFiles(const CURL& url) override { return true; }
+};
+}
diff --git a/xbmc/filesystem/ISO9660File.cpp b/xbmc/filesystem/ISO9660File.cpp
new file mode 100644
index 0000000..83de0cc
--- /dev/null
+++ b/xbmc/filesystem/ISO9660File.cpp
@@ -0,0 +1,127 @@
+/*
+ * 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 "ISO9660File.h"
+
+#include "URL.h"
+
+#include <cmath>
+
+using namespace XFILE;
+
+CISO9660File::CISO9660File() : m_iso(new ISO9660::IFS())
+{
+}
+
+bool CISO9660File::Open(const CURL& url)
+{
+ if (m_iso && m_stat)
+ return true;
+
+ if (!m_iso->open(url.GetHostName().c_str()))
+ return false;
+
+ m_stat.reset(m_iso->stat(url.GetFileName().c_str()));
+
+ if (!m_stat)
+ return false;
+
+ if (!m_stat->p_stat)
+ return false;
+
+ m_start = m_stat->p_stat->lsn;
+ m_current = 0;
+
+ return true;
+}
+
+int CISO9660File::Stat(const CURL& url, struct __stat64* buffer)
+{
+ if (!m_iso)
+ return -1;
+
+ if (!m_stat)
+ return -1;
+
+ if (!m_stat->p_stat)
+ return -1;
+
+ if (!buffer)
+ return -1;
+
+ *buffer = {};
+ buffer->st_size = m_stat->p_stat->size;
+
+ switch (m_stat->p_stat->type)
+ {
+ case 2:
+ buffer->st_mode = S_IFDIR;
+ break;
+ case 1:
+ default:
+ buffer->st_mode = S_IFREG;
+ break;
+ }
+
+ return 0;
+}
+
+ssize_t CISO9660File::Read(void* buffer, size_t size)
+{
+ const int maxSize = std::min(size, static_cast<size_t>(GetLength()));
+ const int blocks = std::ceil(maxSize / ISO_BLOCKSIZE);
+
+ if (m_current > std::ceil(GetLength() / ISO_BLOCKSIZE))
+ return -1;
+
+ auto read = m_iso->seek_read(buffer, m_start + m_current, blocks);
+
+ m_current += blocks;
+
+ return read;
+}
+
+int64_t CISO9660File::Seek(int64_t filePosition, int whence)
+{
+ int block = std::floor(filePosition / ISO_BLOCKSIZE);
+
+ switch (whence)
+ {
+ case SEEK_SET:
+ m_current = block;
+ break;
+ case SEEK_CUR:
+ m_current += block;
+ break;
+ case SEEK_END:
+ m_current = std::ceil(GetLength() / ISO_BLOCKSIZE) + block;
+ break;
+ }
+
+ return m_current * ISO_BLOCKSIZE;
+}
+
+int64_t CISO9660File::GetLength()
+{
+ return m_stat->p_stat->size;
+}
+
+int64_t CISO9660File::GetPosition()
+{
+ return m_current * ISO_BLOCKSIZE;
+}
+
+bool CISO9660File::Exists(const CURL& url)
+{
+ return Open(url);
+}
+
+int CISO9660File::GetChunkSize()
+{
+ return ISO_BLOCKSIZE;
+}
diff --git a/xbmc/filesystem/ISO9660File.h b/xbmc/filesystem/ISO9660File.h
new file mode 100644
index 0000000..5754605
--- /dev/null
+++ b/xbmc/filesystem/ISO9660File.h
@@ -0,0 +1,49 @@
+/*
+ * 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 "IFile.h"
+
+#include <memory>
+
+#include <cdio++/iso9660.hpp>
+
+namespace XFILE
+{
+
+class CISO9660File : public IFile
+{
+public:
+ CISO9660File();
+ ~CISO9660File() override = default;
+
+ bool Open(const CURL& url) override;
+ void Close() override {}
+
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+
+ ssize_t Read(void* buffer, size_t size) override;
+ int64_t Seek(int64_t filePosition, int whence) override;
+
+ int64_t GetLength() override;
+ int64_t GetPosition() override;
+
+ bool Exists(const CURL& url) override;
+
+ int GetChunkSize() override;
+
+private:
+ std::unique_ptr<ISO9660::IFS> m_iso;
+ std::unique_ptr<ISO9660::Stat> m_stat;
+
+ int32_t m_start;
+ int32_t m_current;
+};
+
+} // namespace XFILE
diff --git a/xbmc/filesystem/ImageFile.cpp b/xbmc/filesystem/ImageFile.cpp
new file mode 100644
index 0000000..bb1e23a
--- /dev/null
+++ b/xbmc/filesystem/ImageFile.cpp
@@ -0,0 +1,106 @@
+/*
+ * 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 "ImageFile.h"
+
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "URL.h"
+
+using namespace XFILE;
+
+CImageFile::CImageFile(void) = default;
+
+CImageFile::~CImageFile(void)
+{
+ Close();
+}
+
+bool CImageFile::Open(const CURL& url)
+{
+ std::string file = url.Get();
+ bool needsRecaching = false;
+ std::string cachedFile =
+ CServiceBroker::GetTextureCache()->CheckCachedImage(file, needsRecaching);
+ if (cachedFile.empty())
+ { // not in the cache, so cache it
+ cachedFile = CServiceBroker::GetTextureCache()->CacheImage(file);
+ }
+ if (!cachedFile.empty())
+ { // in the cache, return what we have
+ if (m_file.Open(cachedFile))
+ return true;
+ }
+ return false;
+}
+
+bool CImageFile::Exists(const CURL& url)
+{
+ bool needsRecaching = false;
+ std::string cachedFile =
+ CServiceBroker::GetTextureCache()->CheckCachedImage(url.Get(), needsRecaching);
+ if (!cachedFile.empty())
+ {
+ if (CFile::Exists(cachedFile, false))
+ return true;
+ else
+ // Remove from cache so it gets cached again on next Open()
+ CServiceBroker::GetTextureCache()->ClearCachedImage(url.Get());
+ }
+
+ // need to check if the original can be cached on demand and that the file exists
+ if (!CTextureCache::CanCacheImageURL(url))
+ return false;
+
+ return CFile::Exists(url.GetHostName());
+}
+
+int CImageFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ bool needsRecaching = false;
+ std::string cachedFile =
+ CServiceBroker::GetTextureCache()->CheckCachedImage(url.Get(), needsRecaching);
+ if (!cachedFile.empty())
+ return CFile::Stat(cachedFile, buffer);
+
+ /*
+ Doesn't exist in the cache yet. We have 3 options here:
+ 1. Cache the file and do the Stat() on the cached file.
+ 2. Do the Stat() on the original file.
+ 3. Return -1;
+ Only 1 will return valid results, at the cost of being time consuming. ATM we do 3 under
+ the theory that the only user of this is the webinterface currently, where Stat() is not
+ required.
+ */
+ return -1;
+}
+
+ssize_t CImageFile::Read(void* lpBuf, size_t uiBufSize)
+{
+ return m_file.Read(lpBuf, uiBufSize);
+}
+
+int64_t CImageFile::Seek(int64_t iFilePosition, int iWhence /*=SEEK_SET*/)
+{
+ return m_file.Seek(iFilePosition, iWhence);
+}
+
+void CImageFile::Close()
+{
+ m_file.Close();
+}
+
+int64_t CImageFile::GetPosition()
+{
+ return m_file.GetPosition();
+}
+
+int64_t CImageFile::GetLength()
+{
+ return m_file.GetLength();
+}
diff --git a/xbmc/filesystem/ImageFile.h b/xbmc/filesystem/ImageFile.h
new file mode 100644
index 0000000..ebf6d7b
--- /dev/null
+++ b/xbmc/filesystem/ImageFile.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "File.h"
+#include "IFile.h"
+
+namespace XFILE
+{
+ class CImageFile: public IFile
+ {
+ public:
+ CImageFile();
+ ~CImageFile() override;
+ bool Open(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+
+ ssize_t Read(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;
+
+ protected:
+ CFile m_file;
+ };
+}
diff --git a/xbmc/filesystem/LibraryDirectory.cpp b/xbmc/filesystem/LibraryDirectory.cpp
new file mode 100644
index 0000000..8f81495
--- /dev/null
+++ b/xbmc/filesystem/LibraryDirectory.cpp
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2011-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 "LibraryDirectory.h"
+
+#include "Directory.h"
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "SmartPlaylistDirectory.h"
+#include "URL.h"
+#include "guilib/GUIControlFactory.h" // for label parsing
+#include "guilib/TextureManager.h"
+#include "playlists/SmartPlayList.h"
+#include "profiles/ProfileManager.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+CLibraryDirectory::CLibraryDirectory(void) = default;
+
+CLibraryDirectory::~CLibraryDirectory(void) = default;
+
+bool CLibraryDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ std::string libNode = GetNode(url);
+ if (libNode.empty())
+ return false;
+
+ if (URIUtils::HasExtension(libNode, ".xml"))
+ { // a filter or folder node
+ TiXmlElement *node = LoadXML(libNode);
+ if (node)
+ {
+ std::string type = XMLUtils::GetAttribute(node, "type");
+ if (type == "filter")
+ {
+ CSmartPlaylist playlist;
+ std::string type, label;
+ XMLUtils::GetString(node, "content", type);
+ if (type.empty())
+ {
+ CLog::Log(LOGERROR, "<content> tag must not be empty for type=\"filter\" node '{}'",
+ libNode);
+ return false;
+ }
+ if (XMLUtils::GetString(node, "label", label))
+ label = CGUIControlFactory::FilterLabel(label);
+ playlist.SetType(type);
+ playlist.SetName(label);
+ if (playlist.LoadFromXML(node) &&
+ CSmartPlaylistDirectory::GetDirectory(playlist, items))
+ {
+ items.SetProperty("library.filter", "true");
+ items.SetPath(items.GetProperty("path.db").asString());
+ return true;
+ }
+ }
+ else if (type == "folder")
+ {
+ std::string label;
+ if (XMLUtils::GetString(node, "label", label))
+ label = CGUIControlFactory::FilterLabel(label);
+ items.SetLabel(label);
+ std::string path;
+ XMLUtils::GetPath(node, "path", path);
+ if (!path.empty())
+ {
+ URIUtils::AddSlashAtEnd(path);
+ return CDirectory::GetDirectory(path, items, m_strFileMask, m_flags);
+ }
+ }
+ }
+ return false;
+ }
+
+ // just a plain node - read the folder for XML nodes and other folders
+ CFileItemList nodes;
+ if (!CDirectory::GetDirectory(libNode, nodes, ".xml", DIR_FLAG_NO_FILE_DIRS))
+ return false;
+
+ // iterate over our nodes
+ std::string basePath = url.Get();
+ for (int i = 0; i < nodes.Size(); i++)
+ {
+ const TiXmlElement *node = NULL;
+ std::string xml = nodes[i]->GetPath();
+ if (nodes[i]->m_bIsFolder)
+ node = LoadXML(URIUtils::AddFileToFolder(xml, "index.xml"));
+ else
+ {
+ node = LoadXML(xml);
+ if (node && URIUtils::GetFileName(xml) == "index.xml")
+ { // set the label on our items
+ std::string label;
+ if (XMLUtils::GetString(node, "label", label))
+ label = CGUIControlFactory::FilterLabel(label);
+ items.SetLabel(label);
+ continue;
+ }
+ }
+ if (node)
+ {
+ std::string label, icon;
+ if (XMLUtils::GetString(node, "label", label))
+ label = CGUIControlFactory::FilterLabel(label);
+ XMLUtils::GetString(node, "icon", icon);
+ int order = 0;
+ node->Attribute("order", &order);
+
+ // create item
+ URIUtils::RemoveSlashAtEnd(xml);
+ std::string folder = URIUtils::GetFileName(xml);
+ CFileItemPtr item(new CFileItem(URIUtils::AddFileToFolder(basePath, folder), true));
+
+ item->SetLabel(label);
+ if (!icon.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(icon))
+ item->SetArt("icon", icon);
+ item->m_iprogramCount = order;
+ items.Add(item);
+ }
+ }
+ items.Sort(SortByPlaylistOrder, SortOrderAscending);
+ return true;
+}
+
+TiXmlElement *CLibraryDirectory::LoadXML(const std::string &xmlFile)
+{
+ if (!CFileUtils::Exists(xmlFile))
+ return nullptr;
+
+ if (!m_doc.LoadFile(xmlFile))
+ return nullptr;
+
+ TiXmlElement *xml = m_doc.RootElement();
+ if (!xml || xml->ValueStr() != "node")
+ return nullptr;
+
+ // check the condition
+ std::string condition = XMLUtils::GetAttribute(xml, "visible");
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (condition.empty() ||
+ (gui && gui->GetInfoManager().EvaluateBool(condition, INFO::DEFAULT_CONTEXT)))
+ return xml;
+
+ return nullptr;
+}
+
+bool CLibraryDirectory::Exists(const CURL& url)
+{
+ return !GetNode(url).empty();
+}
+
+std::string CLibraryDirectory::GetNode(const CURL& url)
+{
+ std::string libDir = URIUtils::AddFileToFolder(m_profileManager->GetLibraryFolder(), url.GetHostName() + "/");
+ if (!CDirectory::Exists(libDir))
+ libDir = URIUtils::AddFileToFolder("special://xbmc/system/library/", url.GetHostName() + "/");
+
+ libDir = URIUtils::AddFileToFolder(libDir, url.GetFileName());
+
+ // is this a virtual node (aka actual folder on disk?)
+ if (CDirectory::Exists(libDir))
+ return libDir;
+
+ // maybe it's an XML node?
+ std::string xmlNode = libDir;
+ URIUtils::RemoveSlashAtEnd(xmlNode);
+
+ if (CFileUtils::Exists(xmlNode))
+ return xmlNode;
+
+ return "";
+}
diff --git a/xbmc/filesystem/LibraryDirectory.h b/xbmc/filesystem/LibraryDirectory.h
new file mode 100644
index 0000000..af76dd4
--- /dev/null
+++ b/xbmc/filesystem/LibraryDirectory.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011-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 "IDirectory.h"
+#include "utils/XBMCTinyXML.h"
+
+namespace XFILE
+{
+ class CLibraryDirectory : public IDirectory
+ {
+ public:
+ CLibraryDirectory();
+ ~CLibraryDirectory() override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool Exists(const CURL& url) override;
+ bool AllowAll() const override { return true; }
+ private:
+ /*! \brief parse the given path and return the node corresponding to this path
+ \param path the library:// path to parse
+ \return path to the XML file or directory corresponding to this path
+ */
+ std::string GetNode(const CURL& path);
+
+ /*! \brief load the XML file and return a pointer to the <node> root element.
+ Checks visible attribute and only returns non-NULL for valid nodes that should be visible.
+ \param xmlFile the XML file to load and parse
+ \return the TiXmlElement pointer to the node, if it should be visible.
+ */
+ TiXmlElement *LoadXML(const std::string &xmlFile);
+
+ CXBMCTinyXML m_doc;
+ };
+}
diff --git a/xbmc/filesystem/MultiPathDirectory.cpp b/xbmc/filesystem/MultiPathDirectory.cpp
new file mode 100644
index 0000000..4cdcfcc
--- /dev/null
+++ b/xbmc/filesystem/MultiPathDirectory.cpp
@@ -0,0 +1,309 @@
+/*
+ * 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 "MultiPathDirectory.h"
+
+#include "Directory.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "threads/SystemClock.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+using namespace std::chrono_literals;
+
+//
+// multipath://{path1}/{path2}/{path3}/.../{path-N}
+//
+// unlike the older virtualpath:// protocol, sub-folders are combined together into a new
+// multipath:// style url.
+//
+
+CMultiPathDirectory::CMultiPathDirectory() = default;
+
+CMultiPathDirectory::~CMultiPathDirectory() = default;
+
+bool CMultiPathDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ CLog::Log(LOGDEBUG, "CMultiPathDirectory::GetDirectory({})", url.GetRedacted());
+
+ std::vector<std::string> vecPaths;
+ if (!GetPaths(url, vecPaths))
+ return false;
+
+ XbmcThreads::EndTime<> progressTime(3000ms); // 3 seconds before showing progress bar
+ CGUIDialogProgress* dlgProgress = NULL;
+
+ unsigned int iFailures = 0;
+ for (unsigned int i = 0; i < vecPaths.size(); ++i)
+ {
+ // show the progress dialog if we have passed our time limit
+ if (progressTime.IsTimePast() && !dlgProgress)
+ {
+ dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (dlgProgress)
+ {
+ dlgProgress->SetHeading(CVariant{15310});
+ dlgProgress->SetLine(0, CVariant{15311});
+ dlgProgress->SetLine(1, CVariant{""});
+ dlgProgress->SetLine(2, CVariant{""});
+ dlgProgress->Open();
+ dlgProgress->ShowProgressBar(true);
+ dlgProgress->SetProgressMax((int)vecPaths.size()*2);
+ dlgProgress->Progress();
+ }
+ }
+ if (dlgProgress)
+ {
+ CURL url(vecPaths[i]);
+ dlgProgress->SetLine(1, CVariant{url.GetWithoutUserDetails()});
+ dlgProgress->SetProgressAdvance();
+ dlgProgress->Progress();
+ }
+
+ CFileItemList tempItems;
+ CLog::Log(LOGDEBUG, "Getting Directory ({})", CURL::GetRedacted(vecPaths[i]));
+ if (CDirectory::GetDirectory(vecPaths[i], tempItems, m_strFileMask, m_flags))
+ items.Append(tempItems);
+ else
+ {
+ CLog::Log(LOGERROR, "Error Getting Directory ({})", CURL::GetRedacted(vecPaths[i]));
+ iFailures++;
+ }
+
+ if (dlgProgress)
+ {
+ dlgProgress->SetProgressAdvance();
+ dlgProgress->Progress();
+ }
+ }
+
+ if (dlgProgress)
+ dlgProgress->Close();
+
+ if (iFailures == vecPaths.size())
+ return false;
+
+ // merge like-named folders into a sub multipath:// style url
+ MergeItems(items);
+
+ return true;
+}
+
+bool CMultiPathDirectory::Exists(const CURL& url)
+{
+ CLog::Log(LOGDEBUG, "Testing Existence ({})", url.GetRedacted());
+
+ std::vector<std::string> vecPaths;
+ if (!GetPaths(url, vecPaths))
+ return false;
+
+ for (unsigned int i = 0; i < vecPaths.size(); ++i)
+ {
+ CLog::Log(LOGDEBUG, "Testing Existence ({})", CURL::GetRedacted(vecPaths[i]));
+ if (CDirectory::Exists(vecPaths[i]))
+ return true;
+ }
+ return false;
+}
+
+bool CMultiPathDirectory::Remove(const CURL& url)
+{
+ std::vector<std::string> vecPaths;
+ if (!GetPaths(url, vecPaths))
+ return false;
+
+ bool success = false;
+ for (unsigned int i = 0; i < vecPaths.size(); ++i)
+ {
+ if (CDirectory::Remove(vecPaths[i]))
+ success = true;
+ }
+ return success;
+}
+
+std::string CMultiPathDirectory::GetFirstPath(const std::string &strPath)
+{
+ size_t pos = strPath.find('/', 12);
+ if (pos != std::string::npos)
+ return CURL::Decode(strPath.substr(12, pos - 12));
+ return "";
+}
+
+bool CMultiPathDirectory::GetPaths(const CURL& url, std::vector<std::string>& vecPaths)
+{
+ const std::string pathToUrl(url.Get());
+ return GetPaths(pathToUrl, vecPaths);
+}
+
+bool CMultiPathDirectory::GetPaths(const std::string& path, std::vector<std::string>& paths)
+{
+ paths.clear();
+
+ // remove multipath:// from path and any trailing / (so that the last path doesn't get any more than it originally had)
+ std::string path1 = path.substr(12);
+ path1.erase(path1.find_last_not_of('/')+1);
+
+ // split on "/"
+ std::vector<std::string> temp = StringUtils::Split(path1, '/');
+ if (temp.empty())
+ return false;
+
+ // URL decode each item
+ paths.resize(temp.size());
+ std::transform(temp.begin(), temp.end(), paths.begin(), CURL::Decode);
+ return true;
+}
+
+bool CMultiPathDirectory::HasPath(const std::string& strPath, const std::string& strPathToFind)
+{
+ // remove multipath:// from path and any trailing / (so that the last path doesn't get any more than it originally had)
+ std::string strPath1 = strPath.substr(12);
+ URIUtils::RemoveSlashAtEnd(strPath1);
+
+ // split on "/"
+ std::vector<std::string> vecTemp = StringUtils::Split(strPath1, '/');
+ if (vecTemp.empty())
+ return false;
+
+ // check each item
+ for (unsigned int i = 0; i < vecTemp.size(); i++)
+ {
+ if (CURL::Decode(vecTemp[i]) == strPathToFind)
+ return true;
+ }
+ return false;
+}
+
+std::string CMultiPathDirectory::ConstructMultiPath(const CFileItemList& items, const std::vector<int> &stack)
+{
+ // we replace all instances of comma's with double comma's, then separate
+ // the paths using " , "
+ //CLog::Log(LOGDEBUG, "Building multipath");
+ std::string newPath = "multipath://";
+ //CLog::Log(LOGDEBUG, "-- adding path: {}", strPath);
+ for (unsigned int i = 0; i < stack.size(); ++i)
+ AddToMultiPath(newPath, items[stack[i]]->GetPath());
+
+ //CLog::Log(LOGDEBUG, "Final path: {}", newPath);
+ return newPath;
+}
+
+void CMultiPathDirectory::AddToMultiPath(std::string& strMultiPath, const std::string& strPath)
+{
+ URIUtils::AddSlashAtEnd(strMultiPath);
+ //CLog::Log(LOGDEBUG, "-- adding path: {}", strPath);
+ strMultiPath += CURL::Encode(strPath);
+ strMultiPath += "/";
+}
+
+std::string CMultiPathDirectory::ConstructMultiPath(const std::vector<std::string> &vecPaths)
+{
+ // we replace all instances of comma's with double comma's, then separate
+ // the paths using " , "
+ //CLog::Log(LOGDEBUG, "Building multipath");
+ std::string newPath = "multipath://";
+ //CLog::Log(LOGDEBUG, "-- adding path: {}", strPath);
+ for (std::vector<std::string>::const_iterator path = vecPaths.begin(); path != vecPaths.end(); ++path)
+ AddToMultiPath(newPath, *path);
+ //CLog::Log(LOGDEBUG, "Final path: {}", newPath);
+ return newPath;
+}
+
+std::string CMultiPathDirectory::ConstructMultiPath(const std::set<std::string> &setPaths)
+{
+ std::string newPath = "multipath://";
+ for (const std::string& path : setPaths)
+ AddToMultiPath(newPath, path);
+
+ return newPath;
+}
+
+void CMultiPathDirectory::MergeItems(CFileItemList &items)
+{
+ CLog::Log(LOGDEBUG, "CMultiPathDirectory::MergeItems, items = {}", items.Size());
+ auto start = std::chrono::steady_clock::now();
+ if (items.Size() == 0)
+ return;
+ // sort items by label
+ // folders are before files in this sort method
+ items.Sort(SortByLabel, SortOrderAscending);
+ int i = 0;
+
+ // if first item in the sorted list is a file, just abort
+ if (!items.Get(i)->m_bIsFolder)
+ return;
+
+ while (i + 1 < items.Size())
+ {
+ // there are no more folders left, so exit the loop
+ CFileItemPtr pItem1 = items.Get(i);
+ if (!pItem1->m_bIsFolder)
+ break;
+
+ std::vector<int> stack;
+ stack.push_back(i);
+ CLog::Log(LOGDEBUG, "Testing path: [{:03}] {}", i, CURL::GetRedacted(pItem1->GetPath()));
+
+ int j = i + 1;
+ do
+ {
+ CFileItemPtr pItem2 = items.Get(j);
+ if (pItem2->GetLabel() != pItem1->GetLabel())
+ break;
+
+ // ignore any filefolders which may coincidently have
+ // the same label as a true folder
+ if (!pItem2->IsFileFolder())
+ {
+ stack.push_back(j);
+ CLog::Log(LOGDEBUG, " Adding path: [{:03}] {}", j, CURL::GetRedacted(pItem2->GetPath()));
+ }
+ j++;
+ }
+ while (j < items.Size());
+
+ // do we have anything to combine?
+ if (stack.size() > 1)
+ {
+ // we have a multipath so remove the items and add the new item
+ std::string newPath = ConstructMultiPath(items, stack);
+ for (unsigned int k = stack.size() - 1; k > 0; --k)
+ items.Remove(stack[k]);
+ pItem1->SetPath(newPath);
+ CLog::Log(LOGDEBUG, " New path: {}", CURL::GetRedacted(pItem1->GetPath()));
+ }
+
+ i++;
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "CMultiPathDirectory::MergeItems, items = {}, took {} ms", items.Size(),
+ duration.count());
+}
+
+bool CMultiPathDirectory::SupportsWriteFileOperations(const std::string &strPath)
+{
+ std::vector<std::string> paths;
+ GetPaths(strPath, paths);
+ for (unsigned int i = 0; i < paths.size(); ++i)
+ if (CUtil::SupportsWriteFileOperations(paths[i]))
+ return true;
+ return false;
+}
diff --git a/xbmc/filesystem/MultiPathDirectory.h b/xbmc/filesystem/MultiPathDirectory.h
new file mode 100644
index 0000000..7f92217
--- /dev/null
+++ b/xbmc/filesystem/MultiPathDirectory.h
@@ -0,0 +1,42 @@
+/*
+ * 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 "IDirectory.h"
+
+#include <set>
+#include <string>
+#include <vector>
+
+namespace XFILE
+{
+class CMultiPathDirectory :
+ public IDirectory
+{
+public:
+ CMultiPathDirectory(void);
+ ~CMultiPathDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool Exists(const CURL& url) override;
+ bool Remove(const CURL& url) override;
+
+ static std::string GetFirstPath(const std::string &strPath);
+ static bool SupportsWriteFileOperations(const std::string &strPath);
+ static bool GetPaths(const CURL& url, std::vector<std::string>& vecPaths);
+ static bool GetPaths(const std::string& path, std::vector<std::string>& paths);
+ static bool HasPath(const std::string& strPath, const std::string& strPathToFind);
+ static std::string ConstructMultiPath(const std::vector<std::string> &vecPaths);
+ static std::string ConstructMultiPath(const std::set<std::string> &setPaths);
+
+private:
+ void MergeItems(CFileItemList &items);
+ static void AddToMultiPath(std::string& strMultiPath, const std::string& strPath);
+ std::string ConstructMultiPath(const CFileItemList& items, const std::vector<int> &stack);
+};
+}
diff --git a/xbmc/filesystem/MultiPathFile.cpp b/xbmc/filesystem/MultiPathFile.cpp
new file mode 100644
index 0000000..1f1bf96
--- /dev/null
+++ b/xbmc/filesystem/MultiPathFile.cpp
@@ -0,0 +1,84 @@
+/*
+ * 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 "MultiPathFile.h"
+
+#include "MultiPathDirectory.h"
+#include "URL.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE;
+
+CMultiPathFile::CMultiPathFile(void)
+ : COverrideFile(false)
+{ }
+
+CMultiPathFile::~CMultiPathFile(void) = default;
+
+bool CMultiPathFile::Open(const CURL& url)
+{
+ // grab the filename off the url
+ std::string path, fileName;
+ URIUtils::Split(url.Get(), path, fileName);
+ std::vector<std::string> vecPaths;
+ if (!CMultiPathDirectory::GetPaths(path, vecPaths))
+ return false;
+
+ for (unsigned int i = 0; i < vecPaths.size(); i++)
+ {
+ std::string filePath = vecPaths[i];
+ filePath = URIUtils::AddFileToFolder(filePath, fileName);
+ if (m_file.Open(filePath))
+ return true;
+ }
+ return false;
+}
+
+bool CMultiPathFile::Exists(const CURL& url)
+{
+ // grab the filename off the url
+ std::string path, fileName;
+ URIUtils::Split(url.Get(), path, fileName);
+ std::vector<std::string> vecPaths;
+ if (!CMultiPathDirectory::GetPaths(path, vecPaths))
+ return false;
+
+ for (unsigned int i = 0; i < vecPaths.size(); i++)
+ {
+ std::string filePath = vecPaths[i];
+ filePath = URIUtils::AddFileToFolder(filePath, fileName);
+ if (CFile::Exists(filePath))
+ return true;
+ }
+ return false;
+}
+
+int CMultiPathFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ // grab the filename off the url
+ std::string path, fileName;
+ URIUtils::Split(url.Get(), path, fileName);
+ std::vector<std::string> vecPaths;
+ if (!CMultiPathDirectory::GetPaths(path, vecPaths))
+ return false;
+
+ for (unsigned int i = 0; i < vecPaths.size(); i++)
+ {
+ std::string filePath = vecPaths[i];
+ filePath = URIUtils::AddFileToFolder(filePath, fileName);
+ int ret = CFile::Stat(filePath, buffer);
+ if (ret == 0)
+ return ret;
+ }
+ return -1;
+}
+
+std::string CMultiPathFile::TranslatePath(const CURL& url)
+{
+ return url.Get();
+}
diff --git a/xbmc/filesystem/MultiPathFile.h b/xbmc/filesystem/MultiPathFile.h
new file mode 100644
index 0000000..edff559
--- /dev/null
+++ b/xbmc/filesystem/MultiPathFile.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "filesystem/OverrideFile.h"
+
+namespace XFILE
+{
+ class CMultiPathFile : public COverrideFile
+ {
+ public:
+ CMultiPathFile(void);
+ ~CMultiPathFile(void) override;
+ bool Open(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+
+ protected:
+ std::string TranslatePath(const CURL &url) override;
+ };
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory.cpp b/xbmc/filesystem/MusicDatabaseDirectory.cpp
new file mode 100644
index 0000000..f998d55
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory.cpp
@@ -0,0 +1,344 @@
+/*
+ * 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 "MusicDatabaseDirectory.h"
+
+#include "FileItem.h"
+#include "MusicDatabaseDirectory/QueryParams.h"
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/TextureManager.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicDbUrl.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Crc32.h"
+#include "utils/LegacyPathTranslation.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE;
+using namespace MUSICDATABASEDIRECTORY;
+
+CMusicDatabaseDirectory::CMusicDatabaseDirectory(void) = default;
+
+CMusicDatabaseDirectory::~CMusicDatabaseDirectory(void) = default;
+
+bool CMusicDatabaseDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ std::string path = CLegacyPathTranslation::TranslateMusicDbPath(url);
+
+ // Adjust path to control navigation from albums to discs or directly to songs
+ CQueryParams params;
+ NODE_TYPE type;
+ NODE_TYPE childtype;
+ GetDirectoryNodeInfo(path, type, childtype, params);
+ if (childtype == NODE_TYPE_DISC)
+ {
+ bool bFlatten = false;
+ if (params.GetAlbumId() < 0)
+ bFlatten = true; // Showing *all albums next always songs
+ else
+ {
+ // Option to show discs for ordinary albums (not just boxed sets)
+ bFlatten = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_SHOWDISCS);
+ CMusicDatabase musicdatabase;
+ if (musicdatabase.Open())
+ {
+ if (bFlatten) // Check for boxed set
+ bFlatten = !musicdatabase.IsAlbumBoxset(params.GetAlbumId());
+ if (!bFlatten)
+ { // Check we will get more than 1 disc when path filter options applied
+ int iDiscTotal = musicdatabase.GetDiscsCount(path);
+ bFlatten = iDiscTotal <= 1;
+ }
+ }
+ musicdatabase.Close();
+ }
+ if (bFlatten)
+ { // Skip discs level and go directly to songs
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(path))
+ return false;
+ musicUrl.AppendPath("-2/"); // Flattened so adjust list label etc.
+ path = musicUrl.ToString();
+ }
+ }
+
+ items.SetPath(path);
+ items.m_dwSize = -1; // No size
+
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+
+ if (!pNode)
+ return false;
+
+ bool bResult = pNode->GetChilds(items);
+ for (int i=0;i<items.Size();++i)
+ {
+ CFileItemPtr item = items[i];
+ if (item->m_bIsFolder && !item->HasArt("icon") && !item->HasArt("thumb"))
+ {
+ std::string strImage = GetIcon(item->GetPath());
+ if (!strImage.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(strImage))
+ item->SetArt("icon", strImage);
+ }
+ }
+ if (items.GetLabel().empty())
+ items.SetLabel(pNode->GetLocalizedName());
+
+ return bResult;
+}
+
+NODE_TYPE CMusicDatabaseDirectory::GetDirectoryChildType(const std::string& strPath)
+{
+ std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strPath);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+
+ if (!pNode)
+ return NODE_TYPE_NONE;
+
+ return pNode->GetChildType();
+}
+
+NODE_TYPE CMusicDatabaseDirectory::GetDirectoryType(const std::string& strPath)
+{
+ std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strPath);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+
+ if (!pNode)
+ return NODE_TYPE_NONE;
+
+ return pNode->GetType();
+}
+
+NODE_TYPE CMusicDatabaseDirectory::GetDirectoryParentType(const std::string& strPath)
+{
+ std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strPath);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+
+ if (!pNode)
+ return NODE_TYPE_NONE;
+
+ CDirectoryNode* pParentNode=pNode->GetParent();
+
+ if (!pParentNode)
+ return NODE_TYPE_NONE;
+
+ return pParentNode->GetChildType();
+}
+
+bool CMusicDatabaseDirectory::GetDirectoryNodeInfo(const std::string& strPath,
+ MUSICDATABASEDIRECTORY::NODE_TYPE& type,
+ MUSICDATABASEDIRECTORY::NODE_TYPE& childtype,
+ MUSICDATABASEDIRECTORY::CQueryParams& params)
+{
+ std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strPath);
+ if (!CDirectoryNode::GetNodeInfo(path, type, childtype, params))
+ return false;
+
+ return true;
+}
+
+bool CMusicDatabaseDirectory::IsArtistDir(const std::string& strDirectory)
+{
+ NODE_TYPE node=GetDirectoryType(strDirectory);
+ return (node==NODE_TYPE_ARTIST);
+}
+
+void CMusicDatabaseDirectory::ClearDirectoryCache(const std::string& strDirectory)
+{
+ std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strDirectory);
+ URIUtils::RemoveSlashAtEnd(path);
+
+ uint32_t crc = Crc32::ComputeFromLowerCase(path);
+
+ std::string strFileName = StringUtils::Format("special://temp/archive_cache/{:08x}.fi", crc);
+ CFile::Delete(strFileName);
+}
+
+bool CMusicDatabaseDirectory::IsAllItem(const std::string& strDirectory)
+{
+ //Last query parameter, ignoring any appended options, is -1 or -2
+ CURL url(strDirectory);
+ if (StringUtils::EndsWith(url.GetWithoutOptions(), "/-1/") || // any albumid
+ StringUtils::EndsWith(url.GetWithoutOptions(), "/-1/-2/")) // any albumid + flattened
+ return true;
+ return false;
+}
+
+bool CMusicDatabaseDirectory::GetLabel(const std::string& strDirectory, std::string& strLabel)
+{
+ strLabel = "";
+
+ std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strDirectory);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+ if (!pNode)
+ return false;
+
+ // first see if there's any filter criteria
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(path, params);
+
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ // get genre
+ if (params.GetGenreId() >= 0)
+ strLabel += musicdatabase.GetGenreById(params.GetGenreId());
+
+ // get artist
+ if (params.GetArtistId() >= 0)
+ {
+ if (!strLabel.empty())
+ strLabel += " / ";
+ strLabel += musicdatabase.GetArtistById(params.GetArtistId());
+ }
+
+ // get album
+ if (params.GetAlbumId() >= 0)
+ {
+ if (!strLabel.empty())
+ strLabel += " / ";
+ strLabel += musicdatabase.GetAlbumById(params.GetAlbumId());
+ }
+
+ if (strLabel.empty())
+ {
+ switch (pNode->GetChildType())
+ {
+ case NODE_TYPE_TOP100:
+ strLabel = g_localizeStrings.Get(271); // Top 100
+ break;
+ case NODE_TYPE_GENRE:
+ strLabel = g_localizeStrings.Get(135); // Genres
+ break;
+ case NODE_TYPE_SOURCE:
+ strLabel = g_localizeStrings.Get(39030); // Sources
+ break;
+ case NODE_TYPE_ROLE:
+ strLabel = g_localizeStrings.Get(38033); // Roles
+ break;
+ case NODE_TYPE_ARTIST:
+ strLabel = g_localizeStrings.Get(133); // Artists
+ break;
+ case NODE_TYPE_ALBUM:
+ strLabel = g_localizeStrings.Get(132); // Albums
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS:
+ strLabel = g_localizeStrings.Get(359); // Recently Added Albums
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS:
+ strLabel = g_localizeStrings.Get(517); // Recently Played Albums
+ break;
+ case NODE_TYPE_ALBUM_TOP100:
+ case NODE_TYPE_ALBUM_TOP100_SONGS:
+ strLabel = g_localizeStrings.Get(10505); // Top 100 Albums
+ break;
+ case NODE_TYPE_SINGLES:
+ strLabel = g_localizeStrings.Get(1050); // Singles
+ break;
+ case NODE_TYPE_SONG:
+ strLabel = g_localizeStrings.Get(134); // Songs
+ break;
+ case NODE_TYPE_SONG_TOP100:
+ strLabel = g_localizeStrings.Get(10504); // Top 100 Songs
+ break;
+ case NODE_TYPE_YEAR:
+ strLabel = g_localizeStrings.Get(652); // Years
+ break;
+ case NODE_TYPE_OVERVIEW:
+ strLabel = "";
+ break;
+ default:
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool CMusicDatabaseDirectory::ContainsSongs(const std::string &path)
+{
+ MUSICDATABASEDIRECTORY::NODE_TYPE type = GetDirectoryChildType(path);
+ if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_SONG) return true;
+ if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_SINGLES) return true;
+ if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS) return true;
+ if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS) return true;
+ if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_TOP100_SONGS) return true;
+ if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_SONG_TOP100) return true;
+ if (type == MUSICDATABASEDIRECTORY::NODE_TYPE_DISC) return true;
+ return false;
+}
+
+bool CMusicDatabaseDirectory::Exists(const CURL& url)
+{
+ std::string path = CLegacyPathTranslation::TranslateMusicDbPath(url);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+
+ if (!pNode)
+ return false;
+
+ if (pNode->GetChildType() == MUSICDATABASEDIRECTORY::NODE_TYPE_NONE)
+ return false;
+
+ return true;
+}
+
+bool CMusicDatabaseDirectory::CanCache(const std::string& strPath)
+{
+ std::string path = CLegacyPathTranslation::TranslateMusicDbPath(strPath);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+ if (!pNode)
+ return false;
+ return pNode->CanCache();
+}
+
+std::string CMusicDatabaseDirectory::GetIcon(const std::string &strDirectory)
+{
+ switch (GetDirectoryChildType(strDirectory))
+ {
+ case NODE_TYPE_ARTIST:
+ return "DefaultMusicArtists.png";
+ case NODE_TYPE_GENRE:
+ return "DefaultMusicGenres.png";
+ case NODE_TYPE_SOURCE:
+ return "DefaultMusicSources.png";
+ case NODE_TYPE_ROLE:
+ return "DefaultMusicRoles.png";
+ case NODE_TYPE_TOP100:
+ return "DefaultMusicTop100.png";
+ case NODE_TYPE_ALBUM:
+ return "DefaultMusicAlbums.png";
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS:
+ return "DefaultMusicRecentlyAdded.png";
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS:
+ return "DefaultMusicRecentlyPlayed.png";
+ case NODE_TYPE_SINGLES:
+ case NODE_TYPE_SONG:
+ return "DefaultMusicSongs.png";
+ case NODE_TYPE_ALBUM_TOP100:
+ case NODE_TYPE_ALBUM_TOP100_SONGS:
+ return "DefaultMusicTop100Albums.png";
+ case NODE_TYPE_SONG_TOP100:
+ return "DefaultMusicTop100Songs.png";
+ case NODE_TYPE_YEAR:
+ return "DefaultMusicYears.png";
+ default:
+ break;
+ }
+
+ return "";
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory.h b/xbmc/filesystem/MusicDatabaseDirectory.h
new file mode 100644
index 0000000..dc52464
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "IDirectory.h"
+#include "MusicDatabaseDirectory/DirectoryNode.h"
+#include "MusicDatabaseDirectory/QueryParams.h"
+
+namespace XFILE
+{
+ class CMusicDatabaseDirectory : public IDirectory
+ {
+ public:
+ CMusicDatabaseDirectory(void);
+ ~CMusicDatabaseDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool AllowAll() const override { return true; }
+ bool Exists(const CURL& url) override;
+ static MUSICDATABASEDIRECTORY::NODE_TYPE GetDirectoryChildType(const std::string& strPath);
+ static MUSICDATABASEDIRECTORY::NODE_TYPE GetDirectoryType(const std::string& strPath);
+ static MUSICDATABASEDIRECTORY::NODE_TYPE GetDirectoryParentType(const std::string& strPath);
+ static bool GetDirectoryNodeInfo(const std::string& strPath, MUSICDATABASEDIRECTORY::NODE_TYPE& type, MUSICDATABASEDIRECTORY::NODE_TYPE& childtype, MUSICDATABASEDIRECTORY::CQueryParams& params);
+ bool IsArtistDir(const std::string& strDirectory);
+ void ClearDirectoryCache(const std::string& strDirectory);
+ static bool IsAllItem(const std::string& strDirectory);
+ static bool GetLabel(const std::string& strDirectory, std::string& strLabel);
+ bool ContainsSongs(const std::string &path);
+ static bool CanCache(const std::string& strPath);
+ static std::string GetIcon(const std::string& strDirectory);
+ };
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/CMakeLists.txt b/xbmc/filesystem/MusicDatabaseDirectory/CMakeLists.txt
new file mode 100644
index 0000000..6eafcba
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/CMakeLists.txt
@@ -0,0 +1,39 @@
+set(SOURCES DirectoryNodeAlbum.cpp
+ DirectoryNodeAlbumRecentlyAdded.cpp
+ DirectoryNodeAlbumRecentlyAddedSong.cpp
+ DirectoryNodeAlbumRecentlyPlayed.cpp
+ DirectoryNodeAlbumRecentlyPlayedSong.cpp
+ DirectoryNodeAlbumTop100.cpp
+ DirectoryNodeAlbumTop100Song.cpp
+ DirectoryNodeArtist.cpp
+ DirectoryNodeDiscs.cpp
+ DirectoryNode.cpp
+ DirectoryNodeGrouped.cpp
+ DirectoryNodeOverview.cpp
+ DirectoryNodeRoot.cpp
+ DirectoryNodeSingles.cpp
+ DirectoryNodeSong.cpp
+ DirectoryNodeSongTop100.cpp
+ DirectoryNodeTop100.cpp
+ QueryParams.cpp)
+
+set(HEADERS DirectoryNode.h
+ DirectoryNodeAlbum.h
+ DirectoryNodeAlbumRecentlyAdded.h
+ DirectoryNodeAlbumRecentlyAddedSong.h
+ DirectoryNodeAlbumRecentlyPlayed.h
+ DirectoryNodeAlbumRecentlyPlayedSong.h
+ DirectoryNodeAlbumTop100.h
+ DirectoryNodeAlbumTop100Song.h
+ DirectoryNodeArtist.h
+ DirectoryNodeDiscs.h
+ DirectoryNodeGrouped.h
+ DirectoryNodeOverview.h
+ DirectoryNodeRoot.h
+ DirectoryNodeSingles.h
+ DirectoryNodeSong.h
+ DirectoryNodeSongTop100.h
+ DirectoryNodeTop100.h
+ QueryParams.h)
+
+core_add_library(musicdatabasedirectory)
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.cpp
new file mode 100644
index 0000000..00e1bce
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.cpp
@@ -0,0 +1,284 @@
+/*
+ * 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 "DirectoryNode.h"
+
+#include "DirectoryNodeAlbum.h"
+#include "DirectoryNodeAlbumRecentlyAdded.h"
+#include "DirectoryNodeAlbumRecentlyAddedSong.h"
+#include "DirectoryNodeAlbumRecentlyPlayed.h"
+#include "DirectoryNodeAlbumRecentlyPlayedSong.h"
+#include "DirectoryNodeAlbumTop100.h"
+#include "DirectoryNodeAlbumTop100Song.h"
+#include "DirectoryNodeArtist.h"
+#include "DirectoryNodeDiscs.h"
+#include "DirectoryNodeGrouped.h"
+#include "DirectoryNodeOverview.h"
+#include "DirectoryNodeRoot.h"
+#include "DirectoryNodeSingles.h"
+#include "DirectoryNodeSong.h"
+#include "DirectoryNodeSongTop100.h"
+#include "DirectoryNodeTop100.h"
+#include "FileItem.h"
+#include "QueryParams.h"
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+// Constructor is protected use ParseURL()
+CDirectoryNode::CDirectoryNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent)
+{
+ m_Type=Type;
+ m_strName=strName;
+ m_pParent=pParent;
+}
+
+CDirectoryNode::~CDirectoryNode()
+{
+ delete m_pParent;
+}
+
+// Parses a given path and returns the current node of the path
+CDirectoryNode* CDirectoryNode::ParseURL(const std::string& strPath)
+{
+ CURL url(strPath);
+
+ std::string strDirectory=url.GetFileName();
+ URIUtils::RemoveSlashAtEnd(strDirectory);
+
+ std::vector<std::string> Path = StringUtils::Split(strDirectory, '/');
+ Path.insert(Path.begin(), "");
+
+ CDirectoryNode* pNode = nullptr;
+ CDirectoryNode* pParent = nullptr;
+ NODE_TYPE NodeType = NODE_TYPE_ROOT;
+
+ for (int i=0; i < static_cast<int>(Path.size()); ++i)
+ {
+ pNode = CreateNode(NodeType, Path[i], pParent);
+ NodeType = pNode ? pNode->GetChildType() : NODE_TYPE_NONE;
+ pParent = pNode;
+ }
+
+ // Add all the additional URL options to the last node
+ if (pNode)
+ pNode->AddOptions(url.GetOptions());
+
+ return pNode;
+}
+
+// returns the database ids of the path,
+void CDirectoryNode::GetDatabaseInfo(const std::string& strPath, CQueryParams& params)
+{
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(strPath));
+
+ if (!pNode)
+ return;
+
+ pNode->CollectQueryParams(params);
+}
+
+bool CDirectoryNode::GetNodeInfo(const std::string& strPath,
+ NODE_TYPE& type,
+ NODE_TYPE& childtype,
+ CQueryParams& params)
+{
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(strPath));
+ if (!pNode)
+ return false;
+
+ type = pNode->GetType();
+ childtype = pNode->GetChildType();
+ pNode->CollectQueryParams(params);
+
+ return true;
+}
+
+// Create a node object
+CDirectoryNode* CDirectoryNode::CreateNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent)
+{
+ switch (Type)
+ {
+ case NODE_TYPE_ROOT:
+ return new CDirectoryNodeRoot(strName, pParent);
+ case NODE_TYPE_OVERVIEW:
+ return new CDirectoryNodeOverview(strName, pParent);
+ case NODE_TYPE_GENRE:
+ case NODE_TYPE_SOURCE:
+ case NODE_TYPE_ROLE:
+ case NODE_TYPE_YEAR:
+ return new CDirectoryNodeGrouped(Type, strName, pParent);
+ case NODE_TYPE_DISC:
+ return new CDirectoryNodeDiscs(strName, pParent);
+ case NODE_TYPE_ARTIST:
+ return new CDirectoryNodeArtist(strName, pParent);
+ case NODE_TYPE_ALBUM:
+ return new CDirectoryNodeAlbum(strName, pParent);
+ case NODE_TYPE_SONG:
+ return new CDirectoryNodeSong(strName, pParent);
+ case NODE_TYPE_SINGLES:
+ return new CDirectoryNodeSingles(strName, pParent);
+ case NODE_TYPE_TOP100:
+ return new CDirectoryNodeTop100(strName, pParent);
+ case NODE_TYPE_ALBUM_TOP100:
+ return new CDirectoryNodeAlbumTop100(strName, pParent);
+ case NODE_TYPE_ALBUM_TOP100_SONGS:
+ return new CDirectoryNodeAlbumTop100Song(strName, pParent);
+ case NODE_TYPE_SONG_TOP100:
+ return new CDirectoryNodeSongTop100(strName, pParent);
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ return new CDirectoryNodeAlbumRecentlyAdded(strName, pParent);
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS:
+ return new CDirectoryNodeAlbumRecentlyAddedSong(strName, pParent);
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ return new CDirectoryNodeAlbumRecentlyPlayed(strName, pParent);
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS:
+ return new CDirectoryNodeAlbumRecentlyPlayedSong(strName, pParent);
+ default:
+ break;
+ }
+
+ return nullptr;
+}
+
+// Current node name
+const std::string& CDirectoryNode::GetName() const
+{
+ return m_strName;
+}
+
+int CDirectoryNode::GetID() const
+{
+ return atoi(m_strName.c_str());
+}
+
+std::string CDirectoryNode::GetLocalizedName() const
+{
+ return "";
+}
+
+// Current node type
+NODE_TYPE CDirectoryNode::GetType() const
+{
+ return m_Type;
+}
+
+// Return the parent directory node or NULL, if there is no
+CDirectoryNode* CDirectoryNode::GetParent() const
+{
+ return m_pParent;
+}
+
+void CDirectoryNode::RemoveParent()
+{
+ m_pParent = nullptr;
+}
+
+// should be overloaded by a derived class
+// to get the content of a node. Will be called
+// by GetChilds() of a parent node
+bool CDirectoryNode::GetContent(CFileItemList& items) const
+{
+ return false;
+}
+
+// Creates a musicdb url
+std::string CDirectoryNode::BuildPath() const
+{
+ std::vector<std::string> array;
+
+ if (!m_strName.empty())
+ array.insert(array.begin(), m_strName);
+
+ CDirectoryNode* pParent=m_pParent;
+ while (pParent != nullptr)
+ {
+ const std::string& strNodeName=pParent->GetName();
+ if (!strNodeName.empty())
+ array.insert(array.begin(), strNodeName);
+
+ pParent=pParent->GetParent();
+ }
+
+ std::string strPath="musicdb://";
+ for (int i = 0; i < static_cast<int>(array.size()); ++i)
+ strPath+=array[i]+"/";
+
+ std::string options = m_options.GetOptionsString();
+ if (!options.empty())
+ strPath += "?" + options;
+
+ return strPath;
+}
+
+void CDirectoryNode::AddOptions(const std::string &options)
+{
+ if (options.empty())
+ return;
+
+ m_options.AddOptions(options);
+}
+
+// Collects Query params from this and all parent nodes. If a NODE_TYPE can
+// be used as a database parameter, it will be added to the
+// params object.
+void CDirectoryNode::CollectQueryParams(CQueryParams& params) const
+{
+ params.SetQueryParam(m_Type, m_strName);
+
+ CDirectoryNode* pParent=m_pParent;
+ while (pParent != nullptr)
+ {
+ params.SetQueryParam(pParent->GetType(), pParent->GetName());
+ pParent=pParent->GetParent();
+ }
+}
+
+// Should be overloaded by a derived class.
+// Returns the NODE_TYPE of the child nodes.
+NODE_TYPE CDirectoryNode::GetChildType() const
+{
+ return NODE_TYPE_NONE;
+}
+
+// Get the child fileitems of this node
+bool CDirectoryNode::GetChilds(CFileItemList& items)
+{
+ if (CanCache() && items.Load())
+ return true;
+
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::CreateNode(GetChildType(), "", this));
+
+ bool bSuccess=false;
+ if (pNode)
+ {
+ pNode->m_options = m_options;
+ bSuccess=pNode->GetContent(items);
+ if (bSuccess)
+ {
+ if (CanCache())
+ items.SetCacheToDisc(CFileItemList::CACHE_ALWAYS);
+ }
+ else
+ items.Clear();
+
+ pNode->RemoveParent();
+ }
+
+ return bSuccess;
+}
+
+
+bool CDirectoryNode::CanCache() const
+{
+ // JM: No need to cache these views, as caching is added in the mediawindow baseclass for anything that takes
+ // longer than a second
+ return false;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.h
new file mode 100644
index 0000000..63d11d1
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNode.h
@@ -0,0 +1,92 @@
+/*
+ * 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 "utils/UrlOptions.h"
+
+class CFileItemList;
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CQueryParams;
+
+ typedef enum _NODE_TYPE
+ {
+ NODE_TYPE_NONE=0,
+ NODE_TYPE_ROOT,
+ NODE_TYPE_OVERVIEW,
+ NODE_TYPE_TOP100,
+ NODE_TYPE_ROLE,
+ NODE_TYPE_SOURCE,
+ NODE_TYPE_GENRE,
+ NODE_TYPE_ARTIST,
+ NODE_TYPE_ALBUM,
+ NODE_TYPE_ALBUM_RECENTLY_ADDED,
+ NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS,
+ NODE_TYPE_ALBUM_RECENTLY_PLAYED,
+ NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS,
+ NODE_TYPE_ALBUM_TOP100,
+ NODE_TYPE_ALBUM_TOP100_SONGS,
+ NODE_TYPE_SONG,
+ NODE_TYPE_SONG_TOP100,
+ NODE_TYPE_YEAR,
+ NODE_TYPE_SINGLES,
+ NODE_TYPE_DISC,
+ } NODE_TYPE;
+
+ typedef struct {
+ NODE_TYPE node;
+ std::string id;
+ int label;
+ } Node;
+
+ class CDirectoryNode
+ {
+ public:
+ static CDirectoryNode* ParseURL(const std::string& strPath);
+ static void GetDatabaseInfo(const std::string& strPath, CQueryParams& params);
+ static bool GetNodeInfo(const std::string& strPath, NODE_TYPE& type, NODE_TYPE& childtype, CQueryParams& params);
+ virtual ~CDirectoryNode();
+
+ NODE_TYPE GetType() const;
+
+ bool GetChilds(CFileItemList& items);
+ virtual NODE_TYPE GetChildType() const;
+ virtual std::string GetLocalizedName() const;
+
+ CDirectoryNode* GetParent() const;
+ virtual bool CanCache() const;
+
+ std::string BuildPath() const;
+
+ protected:
+ CDirectoryNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent);
+ static CDirectoryNode* CreateNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent);
+
+ void AddOptions(const std::string &options);
+ void CollectQueryParams(CQueryParams& params) const;
+
+ const std::string& GetName() const;
+ int GetID() const;
+ void RemoveParent();
+
+ virtual bool GetContent(CFileItemList& items) const;
+
+ private:
+ NODE_TYPE m_Type;
+ std::string m_strName;
+ CDirectoryNode* m_pParent;
+ CUrlOptions m_options;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.cpp
new file mode 100644
index 0000000..f60a025
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.cpp
@@ -0,0 +1,52 @@
+/*
+ * 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 "DirectoryNodeAlbum.h"
+
+#include "QueryParams.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/MusicDatabase.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeAlbum::CDirectoryNodeAlbum(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_ALBUM, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeAlbum::GetChildType() const
+{
+ return NODE_TYPE_DISC;
+}
+
+std::string CDirectoryNodeAlbum::GetLocalizedName() const
+{
+ if (GetID() == -1)
+ return g_localizeStrings.Get(15102); // All Albums
+ CMusicDatabase db;
+ if (db.Open())
+ return db.GetAlbumById(GetID());
+ return "";
+}
+
+bool CDirectoryNodeAlbum::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ bool bSuccess=musicdatabase.GetAlbumsNav(BuildPath(), items, params.GetGenreId(), params.GetArtistId());
+
+ musicdatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.h
new file mode 100644
index 0000000..60fb233
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbum.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeAlbum : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeAlbum(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.cpp
new file mode 100644
index 0000000..f274b59
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.cpp
@@ -0,0 +1,65 @@
+/*
+ * 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 "DirectoryNodeAlbumRecentlyAdded.h"
+
+#include "FileItem.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/MusicDatabase.h"
+#include "utils/StringUtils.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeAlbumRecentlyAdded::CDirectoryNodeAlbumRecentlyAdded(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_ALBUM_RECENTLY_ADDED, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeAlbumRecentlyAdded::GetChildType() const
+{
+ if (GetName()=="-1")
+ return NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS;
+
+ return NODE_TYPE_DISC;
+}
+
+std::string CDirectoryNodeAlbumRecentlyAdded::GetLocalizedName() const
+{
+ if (GetID() == -1)
+ return g_localizeStrings.Get(15102); // All Albums
+ CMusicDatabase db;
+ if (db.Open())
+ return db.GetAlbumById(GetID());
+ return "";
+}
+
+bool CDirectoryNodeAlbumRecentlyAdded::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ VECALBUMS albums;
+ if (!musicdatabase.GetRecentlyAddedAlbums(albums))
+ {
+ musicdatabase.Close();
+ return false;
+ }
+
+ for (int i=0; i<(int)albums.size(); ++i)
+ {
+ CAlbum& album=albums[i];
+ std::string strDir = StringUtils::Format("{}{}/", BuildPath(), album.idAlbum);
+ CFileItemPtr pItem(new CFileItem(strDir, album));
+ items.Add(pItem);
+ }
+
+ musicdatabase.Close();
+ return true;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.h
new file mode 100644
index 0000000..a1b4dc1
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAdded.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeAlbumRecentlyAdded : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeAlbumRecentlyAdded(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.cpp
new file mode 100644
index 0000000..8c8d786
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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 "DirectoryNodeAlbumRecentlyAddedSong.h"
+
+#include "music/MusicDatabase.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeAlbumRecentlyAddedSong::CDirectoryNodeAlbumRecentlyAddedSong(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeAlbumRecentlyAddedSong::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ std::string strBaseDir=BuildPath();
+ bool bSuccess=musicdatabase.GetRecentlyAddedAlbumSongs(strBaseDir, items);
+
+ musicdatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.h
new file mode 100644
index 0000000..9cc46c1
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyAddedSong.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeAlbumRecentlyAddedSong : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeAlbumRecentlyAddedSong(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& items) const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.cpp
new file mode 100644
index 0000000..77940c2
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.cpp
@@ -0,0 +1,65 @@
+/*
+ * 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 "DirectoryNodeAlbumRecentlyPlayed.h"
+
+#include "FileItem.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/MusicDatabase.h"
+#include "utils/StringUtils.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeAlbumRecentlyPlayed::CDirectoryNodeAlbumRecentlyPlayed(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_ALBUM_RECENTLY_PLAYED, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeAlbumRecentlyPlayed::GetChildType() const
+{
+ if (GetName()=="-1")
+ return NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS;
+
+ return NODE_TYPE_DISC;
+}
+
+std::string CDirectoryNodeAlbumRecentlyPlayed::GetLocalizedName() const
+{
+ if (GetID() == -1)
+ return g_localizeStrings.Get(15102); // All Albums
+ CMusicDatabase db;
+ if (db.Open())
+ return db.GetAlbumById(GetID());
+ return "";
+}
+
+bool CDirectoryNodeAlbumRecentlyPlayed::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ VECALBUMS albums;
+ if (!musicdatabase.GetRecentlyPlayedAlbums(albums))
+ {
+ musicdatabase.Close();
+ return false;
+ }
+
+ for (int i=0; i<(int)albums.size(); ++i)
+ {
+ CAlbum& album=albums[i];
+ std::string strDir = StringUtils::Format("{}{}/", BuildPath(), album.idAlbum);
+ CFileItemPtr pItem(new CFileItem(strDir, album));
+ items.Add(pItem);
+ }
+
+ musicdatabase.Close();
+ return true;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.h
new file mode 100644
index 0000000..4691c89
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayed.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeAlbumRecentlyPlayed : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeAlbumRecentlyPlayed(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.cpp
new file mode 100644
index 0000000..7a01af0
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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 "DirectoryNodeAlbumRecentlyPlayedSong.h"
+
+#include "music/MusicDatabase.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeAlbumRecentlyPlayedSong::CDirectoryNodeAlbumRecentlyPlayedSong(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeAlbumRecentlyPlayedSong::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ std::string strBaseDir=BuildPath();
+ bool bSuccess=musicdatabase.GetRecentlyPlayedAlbumSongs(strBaseDir, items);
+
+ musicdatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.h
new file mode 100644
index 0000000..3ffab08
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumRecentlyPlayedSong.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeAlbumRecentlyPlayedSong : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeAlbumRecentlyPlayedSong(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& items) const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.cpp
new file mode 100644
index 0000000..88ffaff
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.cpp
@@ -0,0 +1,63 @@
+/*
+ * 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 "DirectoryNodeAlbumTop100.h"
+
+#include "FileItem.h"
+#include "music/MusicDatabase.h"
+#include "utils/StringUtils.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeAlbumTop100::CDirectoryNodeAlbumTop100(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_ALBUM_TOP100, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeAlbumTop100::GetChildType() const
+{
+ if (GetName()=="-1")
+ return NODE_TYPE_ALBUM_TOP100_SONGS;
+
+ return NODE_TYPE_SONG;
+}
+
+std::string CDirectoryNodeAlbumTop100::GetLocalizedName() const
+{
+ CMusicDatabase db;
+ if (db.Open())
+ return db.GetAlbumById(GetID());
+ return "";
+}
+
+bool CDirectoryNodeAlbumTop100::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ VECALBUMS albums;
+ if (!musicdatabase.GetTop100Albums(albums))
+ {
+ musicdatabase.Close();
+ return false;
+ }
+
+ for (int i=0; i<(int)albums.size(); ++i)
+ {
+ CAlbum& album=albums[i];
+ std::string strDir = StringUtils::Format("{}{}/", BuildPath(), album.idAlbum);
+ CFileItemPtr pItem(new CFileItem(strDir, album));
+ items.Add(pItem);
+ }
+
+ musicdatabase.Close();
+
+ return true;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.h
new file mode 100644
index 0000000..de117bd
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeAlbumTop100 : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeAlbumTop100(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.cpp
new file mode 100644
index 0000000..109af60
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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 "DirectoryNodeAlbumTop100Song.h"
+
+#include "music/MusicDatabase.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeAlbumTop100Song::CDirectoryNodeAlbumTop100Song(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_ALBUM_TOP100_SONGS, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeAlbumTop100Song::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ std::string strBaseDir=BuildPath();
+ bool bSuccess=musicdatabase.GetTop100AlbumSongs(strBaseDir, items);
+
+ musicdatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.h
new file mode 100644
index 0000000..18351d7
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeAlbumTop100Song.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeAlbumTop100Song : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeAlbumTop100Song(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& items) const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.cpp
new file mode 100644
index 0000000..b087fa2
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.cpp
@@ -0,0 +1,55 @@
+/*
+ * 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 "DirectoryNodeArtist.h"
+
+#include "QueryParams.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/MusicDatabase.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeArtist::CDirectoryNodeArtist(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_ARTIST, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeArtist::GetChildType() const
+{
+ return NODE_TYPE_ALBUM;
+}
+
+std::string CDirectoryNodeArtist::GetLocalizedName() const
+{
+ if (GetID() == -1)
+ return g_localizeStrings.Get(15103); // All Artists
+ CMusicDatabase db;
+ if (db.Open())
+ return db.GetArtistById(GetID());
+ return "";
+}
+
+bool CDirectoryNodeArtist::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ bool bSuccess = musicdatabase.GetArtistsNav(BuildPath(), items, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS), params.GetGenreId());
+
+ musicdatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.h
new file mode 100644
index 0000000..59b8c73
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeArtist.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeArtist : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeArtist(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.cpp
new file mode 100644
index 0000000..42cf3bb
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.cpp
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2005-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.
+ */
+
+#include "DirectoryNodeDiscs.h"
+
+#include "QueryParams.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/MusicDatabase.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeDiscs::CDirectoryNodeDiscs(const std::string& strName,
+ CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_DISC, strName, pParent)
+{
+}
+
+NODE_TYPE CDirectoryNodeDiscs::GetChildType() const
+{
+ return NODE_TYPE_SONG;
+}
+
+std::string CDirectoryNodeDiscs::GetLocalizedName() const
+{
+ CQueryParams params;
+ CollectQueryParams(params);
+ std::string title;
+ CMusicDatabase db;
+ if (db.Open())
+ title = db.GetAlbumDiscTitle(params.GetAlbumId(), params.GetDisc());
+ db.Close();
+ if (title.empty())
+ title = g_localizeStrings.Get(15102); // All Albums
+
+ return title;
+}
+
+bool CDirectoryNodeDiscs::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ bool bSuccess = musicdatabase.GetDiscsNav(BuildPath(), items, params.GetAlbumId());
+
+ musicdatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.h
new file mode 100644
index 0000000..5cf4f6a
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeDiscs.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2005-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 "DirectoryNode.h"
+
+namespace XFILE
+{
+namespace MUSICDATABASEDIRECTORY
+{
+class CDirectoryNodeDiscs : public CDirectoryNode
+{
+public:
+ CDirectoryNodeDiscs(const std::string& strName, CDirectoryNode* pParent);
+
+protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+};
+} // namespace MUSICDATABASEDIRECTORY
+} // namespace XFILE
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.cpp
new file mode 100644
index 0000000..65875f7
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.cpp
@@ -0,0 +1,61 @@
+/*
+ * 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 "DirectoryNodeGrouped.h"
+
+#include "music/MusicDatabase.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeGrouped::CDirectoryNodeGrouped(NODE_TYPE type, const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(type, strName, pParent)
+{ }
+
+NODE_TYPE CDirectoryNodeGrouped::GetChildType() const
+{
+ if (GetType() == NODE_TYPE_YEAR)
+ return NODE_TYPE_ALBUM;
+
+ return NODE_TYPE_ARTIST;
+}
+
+std::string CDirectoryNodeGrouped::GetLocalizedName() const
+{
+ CMusicDatabase db;
+ if (db.Open())
+ return db.GetItemById(GetContentType(), GetID());
+ return "";
+}
+
+bool CDirectoryNodeGrouped::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ return musicdatabase.GetItems(BuildPath(), GetContentType(), items);
+}
+
+std::string CDirectoryNodeGrouped::GetContentType() const
+{
+ switch (GetType())
+ {
+ case NODE_TYPE_GENRE:
+ return "genres";
+ case NODE_TYPE_SOURCE:
+ return "sources";
+ case NODE_TYPE_ROLE:
+ return "roles";
+ case NODE_TYPE_YEAR:
+ return "years";
+ default:
+ break;
+ }
+
+ return "";
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.h
new file mode 100644
index 0000000..38e7fd9
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeGrouped.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeGrouped : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeGrouped(NODE_TYPE type, const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+
+ private:
+ std::string GetContentType() const;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.cpp
new file mode 100644
index 0000000..fefeb6d
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.cpp
@@ -0,0 +1,87 @@
+/*
+ * 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 "DirectoryNodeOverview.h"
+
+#include "FileItem.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/MusicDatabase.h"
+#include "utils/StringUtils.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ Node OverviewChildren[] = {
+ { NODE_TYPE_GENRE, "genres", 135 },
+ { NODE_TYPE_ARTIST, "artists", 133 },
+ { NODE_TYPE_ALBUM, "albums", 132 },
+ { NODE_TYPE_SINGLES, "singles", 1050 },
+ { NODE_TYPE_SONG, "songs", 134 },
+ { NODE_TYPE_YEAR, "years", 652 },
+ { NODE_TYPE_TOP100, "top100", 271 },
+ { NODE_TYPE_ALBUM_RECENTLY_ADDED, "recentlyaddedalbums", 359 },
+ { NODE_TYPE_ALBUM_RECENTLY_PLAYED, "recentlyplayedalbums", 517 },
+ { NODE_TYPE_ALBUM, "compilations", 521 },
+ { NODE_TYPE_ROLE, "roles", 38033 },
+ { NODE_TYPE_SOURCE, "sources", 39031 },
+ { NODE_TYPE_DISC, "discs", 14087 },
+ { NODE_TYPE_YEAR, "originalyears", 38078 },
+ };
+ };
+};
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeOverview::CDirectoryNodeOverview(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_OVERVIEW, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeOverview::GetChildType() const
+{
+ for (const Node& node : OverviewChildren)
+ if (GetName() == node.id)
+ return node.node;
+ return NODE_TYPE_NONE;
+}
+
+std::string CDirectoryNodeOverview::GetLocalizedName() const
+{
+ for (const Node& node : OverviewChildren)
+ if (GetName() == node.id)
+ return g_localizeStrings.Get(node.label);
+ return "";
+}
+
+bool CDirectoryNodeOverview::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicDatabase;
+ musicDatabase.Open();
+
+ bool hasSingles = (musicDatabase.GetSinglesCount() > 0);
+ bool hasCompilations = (musicDatabase.GetCompilationAlbumsCount() > 0);
+
+ for (unsigned int i = 0; i < sizeof(OverviewChildren) / sizeof(Node); ++i)
+ {
+ if (i == 3 && !hasSingles)
+ continue;
+ if (i == 9 && !hasCompilations)
+ continue;
+
+ CFileItemPtr pItem(new CFileItem(g_localizeStrings.Get(OverviewChildren[i].label)));
+ std::string strDir = StringUtils::Format("{}/", OverviewChildren[i].id);
+ pItem->SetPath(BuildPath() + strDir);
+ pItem->m_bIsFolder = true;
+ pItem->SetCanQueue(false);
+ items.Add(pItem);
+ }
+
+ return true;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.h
new file mode 100644
index 0000000..057a9e8
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeOverview.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeOverview : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeOverview(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.cpp
new file mode 100644
index 0000000..31cc761
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.cpp
@@ -0,0 +1,22 @@
+/*
+ * 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 "DirectoryNodeRoot.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeRoot::CDirectoryNodeRoot(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_ROOT, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeRoot::GetChildType() const
+{
+ return NODE_TYPE_OVERVIEW;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.h
new file mode 100644
index 0000000..c4128a3
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeRoot.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeRoot : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeRoot(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.cpp
new file mode 100644
index 0000000..8deae27
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.cpp
@@ -0,0 +1,32 @@
+/*
+ * 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 "DirectoryNodeSingles.h"
+
+#include "music/MusicDatabase.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeSingles::CDirectoryNodeSingles(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_SINGLES, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeSingles::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ bool bSuccess = musicdatabase.GetSongsFullByWhere(BuildPath(), CDatabase::Filter(), items, SortDescription(), true);
+
+ musicdatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.h
new file mode 100644
index 0000000..cc297b7
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSingles.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeSingles : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeSingles(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& items) const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.cpp
new file mode 100644
index 0000000..4c43f90
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.cpp
@@ -0,0 +1,37 @@
+/*
+ * 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 "DirectoryNodeSong.h"
+
+#include "QueryParams.h"
+#include "music/MusicDatabase.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeSong::CDirectoryNodeSong(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_SONG, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeSong::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ std::string strBaseDir=BuildPath();
+ bool bSuccess=musicdatabase.GetSongsNav(strBaseDir, items, params.GetGenreId(), params.GetArtistId(), params.GetAlbumId());
+
+ musicdatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.h
new file mode 100644
index 0000000..668014b
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSong.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeSong : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeSong(const std::string& strEntryName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& items) const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.cpp
new file mode 100644
index 0000000..a03a8ff
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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 "DirectoryNodeSongTop100.h"
+
+#include "music/MusicDatabase.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CDirectoryNodeSongTop100::CDirectoryNodeSongTop100(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_SONG_TOP100, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeSongTop100::GetContent(CFileItemList& items) const
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ std::string strBaseDir=BuildPath();
+ bool bSuccess=musicdatabase.GetTop100(strBaseDir, items);
+
+ musicdatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.h
new file mode 100644
index 0000000..ca34a72
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeSongTop100.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeSongTop100 : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeSongTop100(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& items) const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.cpp b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.cpp
new file mode 100644
index 0000000..fff9228
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.cpp
@@ -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.
+ */
+
+#include "DirectoryNodeTop100.h"
+
+#include "FileItem.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+Node Top100Children[] = {
+ { NODE_TYPE_SONG_TOP100, "songs", 10504 },
+ { NODE_TYPE_ALBUM_TOP100, "albums", 10505 },
+ };
+
+CDirectoryNodeTop100::CDirectoryNodeTop100(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_TOP100, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeTop100::GetChildType() const
+{
+ for (const Node& node : Top100Children)
+ if (GetName() == node.id)
+ return node.node;
+
+ return NODE_TYPE_NONE;
+}
+
+std::string CDirectoryNodeTop100::GetLocalizedName() const
+{
+ for (const Node& node : Top100Children)
+ if (GetName() == node.id)
+ return g_localizeStrings.Get(node.label);
+ return "";
+}
+
+bool CDirectoryNodeTop100::GetContent(CFileItemList& items) const
+{
+ for (const Node& node : Top100Children)
+ {
+ CFileItemPtr pItem(new CFileItem(g_localizeStrings.Get(node.label)));
+ std::string strDir = StringUtils::Format("{}/", node.id);
+ pItem->SetPath(BuildPath() + strDir);
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+ }
+
+ return true;
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.h b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.h
new file mode 100644
index 0000000..bf5068f
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/DirectoryNodeTop100.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CDirectoryNodeTop100 : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeTop100(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.cpp b/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.cpp
new file mode 100644
index 0000000..3353631
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.cpp
@@ -0,0 +1,58 @@
+/*
+ * 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 "QueryParams.h"
+
+#include <stdlib.h>
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CQueryParams::CQueryParams()
+{
+ m_idArtist=-1;
+ m_idAlbum=-1;
+ m_idGenre=-1;
+ m_idSong=-1;
+ m_year=-1;
+ m_disc = -1;
+}
+
+void CQueryParams::SetQueryParam(NODE_TYPE NodeType, const std::string& strNodeName)
+{
+ int idDb = atoi(strNodeName.c_str());
+
+ switch (NodeType)
+ {
+ case NODE_TYPE_GENRE:
+ m_idGenre=idDb;
+ break;
+ case NODE_TYPE_YEAR:
+ m_year=idDb;
+ break;
+ case NODE_TYPE_ARTIST:
+ m_idArtist=idDb;
+ break;
+ case NODE_TYPE_DISC:
+ m_disc = idDb;
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ case NODE_TYPE_ALBUM_TOP100:
+ case NODE_TYPE_ALBUM:
+ m_idAlbum=idDb;
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS:
+ case NODE_TYPE_ALBUM_TOP100_SONGS:
+ case NODE_TYPE_SONG:
+ case NODE_TYPE_SONG_TOP100:
+ m_idSong=idDb;
+ default:
+ break;
+ }
+}
diff --git a/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.h b/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.h
new file mode 100644
index 0000000..abfbdea
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseDirectory/QueryParams.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace MUSICDATABASEDIRECTORY
+ {
+ class CQueryParams
+ {
+ public:
+ CQueryParams();
+ int GetArtistId() { return m_idArtist; }
+ int GetAlbumId() { return m_idAlbum; }
+ int GetGenreId() { return m_idGenre; }
+ int GetSongId() { return m_idSong; }
+ int GetYear() { return m_year; }
+ int GetDisc() { return m_disc; }
+
+ protected:
+ void SetQueryParam(NODE_TYPE NodeType, const std::string& strNodeName);
+
+ friend class CDirectoryNode;
+ private:
+ int m_idArtist;
+ int m_idAlbum;
+ int m_idGenre;
+ int m_idSong;
+ int m_year;
+ int m_disc;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/MusicDatabaseFile.cpp b/xbmc/filesystem/MusicDatabaseFile.cpp
new file mode 100644
index 0000000..0e41ad4
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseFile.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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 "MusicDatabaseFile.h"
+
+#include "URL.h"
+#include "music/MusicDatabase.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE;
+
+CMusicDatabaseFile::CMusicDatabaseFile(void) = default;
+
+CMusicDatabaseFile::~CMusicDatabaseFile(void)
+{
+ Close();
+}
+
+std::string CMusicDatabaseFile::TranslateUrl(const CURL& url)
+{
+ CMusicDatabase musicDatabase;
+ if (!musicDatabase.Open())
+ return "";
+
+ std::string strFileName=URIUtils::GetFileName(url.Get());
+ std::string strExtension = URIUtils::GetExtension(strFileName);
+ URIUtils::RemoveExtension(strFileName);
+
+ if (!StringUtils::IsNaturalNumber(strFileName))
+ return "";
+
+ int idSong = atoi(strFileName.c_str());
+
+ CSong song;
+ if (!musicDatabase.GetSong(idSong, song))
+ return "";
+
+ StringUtils::ToLower(strExtension);
+ if (!URIUtils::HasExtension(song.strFileName, strExtension))
+ return "";
+
+ return song.strFileName;
+}
+
+bool CMusicDatabaseFile::Open(const CURL& url)
+{
+ return m_file.Open(TranslateUrl(url));
+}
+
+bool CMusicDatabaseFile::Exists(const CURL& url)
+{
+ return !TranslateUrl(url).empty();
+}
+
+int CMusicDatabaseFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ return m_file.Stat(TranslateUrl(url), buffer);
+}
+
+ssize_t CMusicDatabaseFile::Read(void* lpBuf, size_t uiBufSize)
+{
+ return m_file.Read(lpBuf, uiBufSize);
+}
+
+int64_t CMusicDatabaseFile::Seek(int64_t iFilePosition, int iWhence /*=SEEK_SET*/)
+{
+ return m_file.Seek(iFilePosition, iWhence);
+}
+
+void CMusicDatabaseFile::Close()
+{
+ m_file.Close();
+}
+
+int64_t CMusicDatabaseFile::GetPosition()
+{
+ return m_file.GetPosition();
+}
+
+int64_t CMusicDatabaseFile::GetLength()
+{
+ return m_file.GetLength();
+}
+
diff --git a/xbmc/filesystem/MusicDatabaseFile.h b/xbmc/filesystem/MusicDatabaseFile.h
new file mode 100644
index 0000000..1e259f0
--- /dev/null
+++ b/xbmc/filesystem/MusicDatabaseFile.h
@@ -0,0 +1,35 @@
+/*
+ * 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 "File.h"
+#include "IFile.h"
+
+namespace XFILE
+{
+class CMusicDatabaseFile : public IFile
+{
+public:
+ CMusicDatabaseFile(void);
+ ~CMusicDatabaseFile(void) override;
+ bool Open(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+
+ ssize_t Read(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;
+
+ static std::string TranslateUrl(const CURL& url);
+protected:
+ CFile m_file;
+};
+}
diff --git a/xbmc/filesystem/MusicFileDirectory.cpp b/xbmc/filesystem/MusicFileDirectory.cpp
new file mode 100644
index 0000000..b3c7749
--- /dev/null
+++ b/xbmc/filesystem/MusicFileDirectory.cpp
@@ -0,0 +1,79 @@
+/*
+ * 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 "MusicFileDirectory.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+
+CMusicFileDirectory::CMusicFileDirectory(void) = default;
+
+CMusicFileDirectory::~CMusicFileDirectory(void) = default;
+
+bool CMusicFileDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ std::string strPath=url.Get();
+
+ std::string strFileName;
+ strFileName = URIUtils::GetFileName(strPath);
+ URIUtils::RemoveExtension(strFileName);
+
+ int iStreams = GetTrackCount(strPath);
+
+ URIUtils::AddSlashAtEnd(strPath);
+
+ for (int i=0; i<iStreams; ++i)
+ {
+ std::string strLabel =
+ StringUtils::Format("{} - {} {:02}", strFileName, g_localizeStrings.Get(554), i + 1);
+ CFileItemPtr pItem(new CFileItem(strLabel));
+ strLabel = StringUtils::Format("{}{}-{}.{}", strPath, strFileName, i + 1, m_strExt);
+ pItem->SetPath(strLabel);
+
+ /*
+ * Try fist to load tag about related stream track. If them fails or not
+ * available, take base tag for all streams (in this case the item names
+ * are all the same).
+ */
+ MUSIC_INFO::CMusicInfoTag tag;
+ if (Load(strLabel, tag, nullptr))
+ *pItem->GetMusicInfoTag() = tag;
+ else if (m_tag.Loaded())
+ *pItem->GetMusicInfoTag() = m_tag;
+
+ /*
+ * Check track number not set and take stream entry number about.
+ * NOTE: Audio decoder addons can also give a own track number.
+ */
+ if (pItem->GetMusicInfoTag()->GetTrackNumber() == 0)
+ pItem->GetMusicInfoTag()->SetTrackNumber(i+1);
+ items.Add(pItem);
+ }
+
+ return true;
+}
+
+bool CMusicFileDirectory::Exists(const CURL& url)
+{
+ return true;
+}
+
+bool CMusicFileDirectory::ContainsFiles(const CURL &url)
+{
+ const std::string pathToUrl(url.Get());
+ if (GetTrackCount(pathToUrl) > 1)
+ return true;
+
+ return false;
+}
diff --git a/xbmc/filesystem/MusicFileDirectory.h b/xbmc/filesystem/MusicFileDirectory.h
new file mode 100644
index 0000000..283e40e
--- /dev/null
+++ b/xbmc/filesystem/MusicFileDirectory.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "IFileDirectory.h"
+#include "music/tags/MusicInfoTag.h"
+
+namespace XFILE
+{
+ class CMusicFileDirectory : public IFileDirectory
+ {
+ public:
+ CMusicFileDirectory(void);
+ ~CMusicFileDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool Exists(const CURL& url) override;
+ bool ContainsFiles(const CURL& url) override;
+ bool AllowAll() const override { return true; }
+ protected:
+ virtual bool Load(const std::string& strFileName,
+ MUSIC_INFO::CMusicInfoTag& tag,
+ EmbeddedArt* art = nullptr) { return false; }
+ virtual int GetTrackCount(const std::string& strPath) = 0;
+ std::string m_strExt;
+ MUSIC_INFO::CMusicInfoTag m_tag;
+ };
+}
diff --git a/xbmc/filesystem/MusicSearchDirectory.cpp b/xbmc/filesystem/MusicSearchDirectory.cpp
new file mode 100644
index 0000000..1d1868a
--- /dev/null
+++ b/xbmc/filesystem/MusicSearchDirectory.cpp
@@ -0,0 +1,52 @@
+/*
+ * 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 "MusicSearchDirectory.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/MusicDatabase.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+CMusicSearchDirectory::CMusicSearchDirectory(void) = default;
+
+CMusicSearchDirectory::~CMusicSearchDirectory(void) = default;
+
+bool CMusicSearchDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ // break up our path
+ // format is: musicsearch://<url encoded search string>
+ const std::string& search(url.GetHostName());
+
+ if (search.empty())
+ return false;
+
+ // and retrieve the search details
+ items.SetURL(url);
+ auto start = std::chrono::steady_clock::now();
+ CMusicDatabase db;
+ db.Open();
+ db.Search(search, items);
+ db.Close();
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{} ({}) took {} ms", __FUNCTION__, url.GetRedacted(), duration.count());
+
+ items.SetLabel(g_localizeStrings.Get(137)); // Search
+ return true;
+}
+
+bool CMusicSearchDirectory::Exists(const CURL& url)
+{
+ return true;
+}
diff --git a/xbmc/filesystem/MusicSearchDirectory.h b/xbmc/filesystem/MusicSearchDirectory.h
new file mode 100644
index 0000000..67e51f3
--- /dev/null
+++ b/xbmc/filesystem/MusicSearchDirectory.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "IDirectory.h"
+
+namespace XFILE
+{
+ class CMusicSearchDirectory : public IDirectory
+ {
+ public:
+ CMusicSearchDirectory(void);
+ ~CMusicSearchDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool Exists(const CURL& url) override;
+ bool AllowAll() const override { return true; }
+ };
+}
diff --git a/xbmc/filesystem/NFSDirectory.cpp b/xbmc/filesystem/NFSDirectory.cpp
new file mode 100644
index 0000000..7feba53
--- /dev/null
+++ b/xbmc/filesystem/NFSDirectory.cpp
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2011-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.
+ */
+
+#ifdef TARGET_WINDOWS
+#include <mutex>
+
+#include <sys\stat.h>
+#endif
+
+#include "FileItem.h"
+#include "NFSDirectory.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#ifdef TARGET_WINDOWS
+#include <sys\stat.h>
+#endif
+
+using namespace XFILE;
+#include <limits.h>
+#include <nfsc/libnfs.h>
+#include <nfsc/libnfs-raw-nfs.h>
+
+#if defined(TARGET_WINDOWS)
+#define S_IFLNK 0120000
+#define S_ISBLK(m) (0)
+#define S_ISSOCK(m) (0)
+#define S_ISLNK(m) ((m & S_IFLNK) != 0)
+#define S_ISCHR(m) ((m & _S_IFCHR) != 0)
+#define S_ISDIR(m) ((m & _S_IFDIR) != 0)
+#define S_ISFIFO(m) ((m & _S_IFIFO) != 0)
+#define S_ISREG(m) ((m & _S_IFREG) != 0)
+#endif
+
+CNFSDirectory::CNFSDirectory(void)
+{
+ gNfsConnection.AddActiveConnection();
+}
+
+CNFSDirectory::~CNFSDirectory(void)
+{
+ gNfsConnection.AddIdleConnection();
+}
+
+bool CNFSDirectory::GetDirectoryFromExportList(const std::string& strPath, CFileItemList &items)
+{
+ CURL url(strPath);
+ std::string nonConstStrPath(strPath);
+ std::list<std::string> exportList=gNfsConnection.GetExportList(url);
+
+ for (const std::string& it : exportList)
+ {
+ const std::string& currentExport(it);
+ URIUtils::RemoveSlashAtEnd(nonConstStrPath);
+
+ CFileItemPtr pItem(new CFileItem(currentExport));
+ std::string path(nonConstStrPath + currentExport);
+ URIUtils::AddSlashAtEnd(path);
+ pItem->SetPath(path);
+ pItem->m_dateTime = 0;
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+ }
+
+ return exportList.empty() ? false : true;
+}
+
+bool CNFSDirectory::GetServerList(CFileItemList &items)
+{
+ struct nfs_server_list *srvrs;
+ struct nfs_server_list *srv;
+ bool ret = false;
+
+ srvrs = nfs_find_local_servers();
+
+ for (srv=srvrs; srv; srv = srv->next)
+ {
+ std::string currentExport(srv->addr);
+
+ CFileItemPtr pItem(new CFileItem(currentExport));
+ std::string path("nfs://" + currentExport);
+ URIUtils::AddSlashAtEnd(path);
+ pItem->m_dateTime=0;
+
+ pItem->SetPath(path);
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+ ret = true; //added at least one entry
+ }
+ free_nfs_srvr_list(srvrs);
+
+ return ret;
+}
+
+bool CNFSDirectory::ResolveSymlink( const std::string &dirName, struct nfsdirent *dirent, CURL &resolvedUrl)
+{
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ int ret = 0;
+ bool retVal = true;
+ std::string fullpath = dirName;
+ char resolvedLink[MAX_PATH];
+
+ URIUtils::AddSlashAtEnd(fullpath);
+ fullpath.append(dirent->name);
+
+ resolvedUrl.Reset();
+ resolvedUrl.SetPort(2049);
+ resolvedUrl.SetProtocol("nfs");
+ resolvedUrl.SetHostName(gNfsConnection.GetConnectedIp());
+
+ ret = nfs_readlink(gNfsConnection.GetNfsContext(), fullpath.c_str(), resolvedLink, MAX_PATH);
+
+ if(ret == 0)
+ {
+ nfs_stat_64 tmpBuffer = {};
+ fullpath = dirName;
+ URIUtils::AddSlashAtEnd(fullpath);
+ fullpath.append(resolvedLink);
+
+ //special case - if link target is absolute it could be even another export
+ //intervolume symlinks baby ...
+ if(resolvedLink[0] == '/')
+ {
+ //use the special stat function for using an extra context
+ //because we are inside of a dir traversal
+ //and just can't change the global nfs context here
+ //without destroying something...
+ fullpath = resolvedLink;
+ resolvedUrl.SetFileName(fullpath);
+ ret = gNfsConnection.stat(resolvedUrl, &tmpBuffer);
+ }
+ else
+ {
+ ret = nfs_stat64(gNfsConnection.GetNfsContext(), fullpath.c_str(), &tmpBuffer);
+ resolvedUrl.SetFileName(gNfsConnection.GetConnectedExport() + fullpath);
+ }
+
+ if (ret != 0)
+ {
+ CLog::Log(LOGERROR, "NFS: Failed to stat({}) on link resolve {}", fullpath,
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ retVal = false;
+ }
+ else
+ {
+ dirent->inode = tmpBuffer.nfs_ino;
+ dirent->mode = tmpBuffer.nfs_mode;
+ dirent->size = tmpBuffer.nfs_size;
+ dirent->atime.tv_sec = tmpBuffer.nfs_atime;
+ dirent->mtime.tv_sec = tmpBuffer.nfs_mtime;
+ dirent->ctime.tv_sec = tmpBuffer.nfs_ctime;
+
+ //map stat mode to nf3type
+ if (S_ISBLK(tmpBuffer.nfs_mode))
+ {
+ dirent->type = NF3BLK;
+ }
+ else if (S_ISCHR(tmpBuffer.nfs_mode))
+ {
+ dirent->type = NF3CHR;
+ }
+ else if (S_ISDIR(tmpBuffer.nfs_mode))
+ {
+ dirent->type = NF3DIR;
+ }
+ else if (S_ISFIFO(tmpBuffer.nfs_mode))
+ {
+ dirent->type = NF3FIFO;
+ }
+ else if (S_ISREG(tmpBuffer.nfs_mode))
+ {
+ dirent->type = NF3REG;
+ }
+ else if (S_ISLNK(tmpBuffer.nfs_mode))
+ {
+ dirent->type = NF3LNK;
+ }
+ else if (S_ISSOCK(tmpBuffer.nfs_mode))
+ {
+ dirent->type = NF3SOCK;
+ }
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Failed to readlink({}) {}", fullpath,
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ retVal = false;
+ }
+ return retVal;
+}
+
+bool CNFSDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ // We accept nfs://server/path[/file]]]]
+ int ret = 0;
+ KODI::TIME::FileTime fileTime, localTime;
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string strDirName="";
+ std::string myStrPath(url.Get());
+ URIUtils::AddSlashAtEnd(myStrPath); //be sure the dir ends with a slash
+
+ if(!gNfsConnection.Connect(url,strDirName))
+ {
+ //connect has failed - so try to get the exported filesystems if no path is given to the url
+ if(url.GetShareName().empty())
+ {
+ if(url.GetHostName().empty())
+ {
+ return GetServerList(items);
+ }
+ else
+ {
+ return GetDirectoryFromExportList(myStrPath, items);
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ struct nfsdir *nfsdir = NULL;
+ struct nfsdirent *nfsdirent = NULL;
+
+ ret = nfs_opendir(gNfsConnection.GetNfsContext(), strDirName.c_str(), &nfsdir);
+
+ if(ret != 0)
+ {
+ CLog::Log(LOGERROR, "Failed to open({}) {}", strDirName,
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ return false;
+ }
+ lock.unlock();
+
+ while((nfsdirent = nfs_readdir(gNfsConnection.GetNfsContext(), nfsdir)) != NULL)
+ {
+ struct nfsdirent tmpDirent = *nfsdirent;
+ std::string strName = tmpDirent.name;
+ std::string path(myStrPath + strName);
+ int64_t iSize = 0;
+ bool bIsDir = false;
+ int64_t lTimeDate = 0;
+
+ //reslove symlinks
+ if(tmpDirent.type == NF3LNK)
+ {
+ CURL linkUrl;
+ //resolve symlink changes tmpDirent and strName
+ if(!ResolveSymlink(strDirName,&tmpDirent,linkUrl))
+ {
+ continue;
+ }
+
+ path = linkUrl.Get();
+ }
+
+ iSize = tmpDirent.size;
+ bIsDir = tmpDirent.type == NF3DIR;
+ lTimeDate = tmpDirent.mtime.tv_sec;
+
+ if (!StringUtils::EqualsNoCase(strName,".") && !StringUtils::EqualsNoCase(strName,"..")
+ && !StringUtils::EqualsNoCase(strName,"lost+found"))
+ {
+ if(lTimeDate == 0) // if modification date is missing, use create date
+ {
+ lTimeDate = tmpDirent.ctime.tv_sec;
+ }
+
+ long long ll = lTimeDate & 0xffffffff;
+ ll *= 10000000ll;
+ ll += 116444736000000000ll;
+ fileTime.lowDateTime = (DWORD)(ll & 0xffffffff);
+ fileTime.highDateTime = (DWORD)(ll >> 32);
+ KODI::TIME::FileTimeToLocalFileTime(&fileTime, &localTime);
+
+ CFileItemPtr pItem(new CFileItem(tmpDirent.name));
+ pItem->m_dateTime=localTime;
+ pItem->m_dwSize = iSize;
+
+ if (bIsDir)
+ {
+ URIUtils::AddSlashAtEnd(path);
+ pItem->m_bIsFolder = true;
+ }
+ else
+ {
+ pItem->m_bIsFolder = false;
+ }
+
+ if (strName[0] == '.')
+ {
+ pItem->SetProperty("file:hidden", true);
+ }
+ pItem->SetPath(path);
+ items.Add(pItem);
+ }
+ }
+
+ lock.lock();
+ nfs_closedir(gNfsConnection.GetNfsContext(), nfsdir);//close the dir
+ lock.unlock();
+ return true;
+}
+
+bool CNFSDirectory::Create(const CURL& url2)
+{
+ int ret = 0;
+ bool success=true;
+
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string folderName(url2.Get());
+ URIUtils::RemoveSlashAtEnd(folderName);//mkdir fails if a slash is at the end!!!
+ CURL url(folderName);
+ folderName = "";
+
+ if(!gNfsConnection.Connect(url,folderName))
+ return false;
+
+ ret = nfs_mkdir(gNfsConnection.GetNfsContext(), folderName.c_str());
+
+ success = (ret == 0 || -EEXIST == ret);
+ if(!success)
+ CLog::Log(LOGERROR, "NFS: Failed to create({}) {}", folderName,
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ return success;
+}
+
+bool CNFSDirectory::Remove(const CURL& url2)
+{
+ int ret = 0;
+
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string folderName(url2.Get());
+ URIUtils::RemoveSlashAtEnd(folderName);//rmdir fails if a slash is at the end!!!
+ CURL url(folderName);
+ folderName = "";
+
+ if(!gNfsConnection.Connect(url,folderName))
+ return false;
+
+ ret = nfs_rmdir(gNfsConnection.GetNfsContext(), folderName.c_str());
+
+ if(ret != 0 && errno != ENOENT)
+ {
+ CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__,
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ return false;
+ }
+ return true;
+}
+
+bool CNFSDirectory::Exists(const CURL& url2)
+{
+ int ret = 0;
+
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string folderName(url2.Get());
+ URIUtils::RemoveSlashAtEnd(folderName);//remove slash at end or URIUtils::GetFileName won't return what we want...
+ CURL url(folderName);
+ folderName = "";
+
+ if(!gNfsConnection.Connect(url,folderName))
+ return false;
+
+ nfs_stat_64 info;
+ ret = nfs_stat64(gNfsConnection.GetNfsContext(), folderName.c_str(), &info);
+
+ if (ret != 0)
+ {
+ return false;
+ }
+ return S_ISDIR(info.nfs_mode) ? true : false;
+}
diff --git a/xbmc/filesystem/NFSDirectory.h b/xbmc/filesystem/NFSDirectory.h
new file mode 100644
index 0000000..684ad8c
--- /dev/null
+++ b/xbmc/filesystem/NFSDirectory.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2011-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 "IDirectory.h"
+#include "NFSFile.h"
+
+struct nfsdirent;
+
+namespace XFILE
+{
+ class CNFSDirectory : public IDirectory
+ {
+ public:
+ CNFSDirectory(void);
+ ~CNFSDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ONCE; }
+ bool Create(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ bool Remove(const CURL& url) override;
+ private:
+ bool GetServerList(CFileItemList &items);
+ bool GetDirectoryFromExportList(const std::string& strPath, CFileItemList &items);
+ bool ResolveSymlink( const std::string &dirName, struct nfsdirent *dirent, CURL &resolvedUrl);
+ };
+}
+
diff --git a/xbmc/filesystem/NFSFile.cpp b/xbmc/filesystem/NFSFile.cpp
new file mode 100644
index 0000000..5e0c148
--- /dev/null
+++ b/xbmc/filesystem/NFSFile.cpp
@@ -0,0 +1,962 @@
+/*
+ * Copyright (C) 2011-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.
+ */
+
+// FileNFS.cpp: implementation of the CNFSFile class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "NFSFile.h"
+
+#include "ServiceBroker.h"
+#include "network/DNSNameCache.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <inttypes.h>
+#include <mutex>
+
+#include <nfsc/libnfs-raw-mount.h>
+#include <nfsc/libnfs.h>
+
+#ifdef TARGET_WINDOWS
+#include <fcntl.h>
+#include <sys\stat.h>
+#endif
+
+#if defined(TARGET_WINDOWS)
+#define S_IRGRP 0
+#define S_IROTH 0
+#define S_IWUSR _S_IWRITE
+#define S_IRUSR _S_IREAD
+#endif
+
+using namespace XFILE;
+
+using namespace std::chrono_literals;
+
+namespace
+{
+// Default "lease_time" on most Linux NFSv4 servers are 90s.
+// See: https://linux-nfs.org/wiki/index.php/NFS_lock_recovery_notes
+// Keep alive interval should be always less than lease_time to avoid client session expires
+
+constexpr auto CONTEXT_TIMEOUT = 60s; // 2/3 parts of lease_time
+constexpr auto KEEP_ALIVE_TIMEOUT = 45s; // half of lease_time
+constexpr auto IDLE_TIMEOUT = 30s; // close fast unused contexts when no active connections
+
+constexpr int NFS4ERR_EXPIRED = -11; // client session expired due idle time greater than lease_time
+
+constexpr auto SETTING_NFS_VERSION = "nfs.version";
+} // unnamed namespace
+
+CNfsConnection::CNfsConnection()
+ : m_pNfsContext(NULL),
+ m_exportPath(""),
+ m_hostName(""),
+ m_resolvedHostName(""),
+ m_IdleTimeout(std::chrono::steady_clock::now() + IDLE_TIMEOUT)
+{
+}
+
+CNfsConnection::~CNfsConnection()
+{
+ Deinit();
+}
+
+void CNfsConnection::resolveHost(const CURL& url)
+{
+ // resolve if hostname has changed
+ CDNSNameCache::Lookup(url.GetHostName(), m_resolvedHostName);
+}
+
+std::list<std::string> CNfsConnection::GetExportList(const CURL& url)
+{
+ std::list<std::string> retList;
+
+ struct exportnode *exportlist, *tmp;
+#ifdef HAS_NFS_MOUNT_GETEXPORTS_TIMEOUT
+ exportlist = mount_getexports_timeout(
+ m_resolvedHostName.c_str(),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_nfsTimeout * 1000);
+#else
+ exportlist = mount_getexports(m_resolvedHostName.c_str());
+#endif
+
+ for (tmp = exportlist; tmp != NULL; tmp = tmp->ex_next)
+ {
+ std::string exportStr = std::string(tmp->ex_dir);
+
+ retList.push_back(exportStr);
+ }
+
+ mount_free_export_list(exportlist);
+ retList.sort();
+ retList.reverse();
+
+ return retList;
+}
+
+void CNfsConnection::clearMembers()
+{
+ // NOTE - DON'T CLEAR m_exportList HERE!
+ // splitUrlIntoExportAndPath checks for m_exportList.empty()
+ // and would query the server in an excessive unwanted fashion
+ // also don't clear m_KeepAliveTimeouts here because we
+ // would loose any "paused" file handles during export change
+ m_exportPath.clear();
+ m_hostName.clear();
+ m_writeChunkSize = 0;
+ m_readChunkSize = 0;
+ m_pNfsContext = NULL;
+}
+
+void CNfsConnection::destroyOpenContexts()
+{
+ std::unique_lock<CCriticalSection> lock(openContextLock);
+ for (auto& it : m_openContextMap)
+ {
+ nfs_destroy_context(it.second.pContext);
+ }
+ m_openContextMap.clear();
+}
+
+void CNfsConnection::destroyContext(const std::string &exportName)
+{
+ std::unique_lock<CCriticalSection> lock(openContextLock);
+ tOpenContextMap::iterator it = m_openContextMap.find(exportName.c_str());
+ if (it != m_openContextMap.end())
+ {
+ nfs_destroy_context(it->second.pContext);
+ m_openContextMap.erase(it);
+ }
+}
+
+struct nfs_context *CNfsConnection::getContextFromMap(const std::string &exportname, bool forceCacheHit/* = false*/)
+{
+ struct nfs_context *pRet = NULL;
+ std::unique_lock<CCriticalSection> lock(openContextLock);
+
+ tOpenContextMap::iterator it = m_openContextMap.find(exportname.c_str());
+ if (it != m_openContextMap.end())
+ {
+ //check if context has timed out already
+ auto now = std::chrono::steady_clock::now();
+ auto duration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - it->second.lastAccessedTime);
+ if (duration < CONTEXT_TIMEOUT || forceCacheHit)
+ {
+ //its not timedout yet or caller wants the cached entry regardless of timeout
+ //refresh access time of that
+ //context and return it
+ if (!forceCacheHit) // only log it if this isn't the resetkeepalive on each read ;)
+ CLog::Log(LOGDEBUG, "NFS: Refreshing context for {}, old: {}, new: {}", exportname,
+ it->second.lastAccessedTime.time_since_epoch().count(),
+ now.time_since_epoch().count());
+ it->second.lastAccessedTime = now;
+ pRet = it->second.pContext;
+ }
+ else
+ {
+ //context is timed out
+ //destroy it and return NULL
+ CLog::Log(LOGDEBUG, "NFS: Old context timed out - destroying it");
+ nfs_destroy_context(it->second.pContext);
+ m_openContextMap.erase(it);
+ }
+ }
+ return pRet;
+}
+
+CNfsConnection::ContextStatus CNfsConnection::getContextForExport(const std::string& exportname)
+{
+ CNfsConnection::ContextStatus ret = CNfsConnection::ContextStatus::INVALID;
+
+ clearMembers();
+
+ m_pNfsContext = getContextFromMap(exportname);
+
+ if(!m_pNfsContext)
+ {
+ CLog::Log(LOGDEBUG, "NFS: Context for {} not open - get a new context.", exportname);
+ m_pNfsContext = nfs_init_context();
+
+ if(!m_pNfsContext)
+ {
+ CLog::Log(LOGERROR,"NFS: Error initcontext in getContextForExport.");
+ }
+ else
+ {
+ struct contextTimeout tmp;
+ std::unique_lock<CCriticalSection> lock(openContextLock);
+ setOptions(m_pNfsContext);
+ tmp.pContext = m_pNfsContext;
+ tmp.lastAccessedTime = std::chrono::steady_clock::now();
+ m_openContextMap[exportname] = tmp; //add context to list of all contexts
+ ret = CNfsConnection::ContextStatus::NEW;
+ }
+ }
+ else
+ {
+ ret = CNfsConnection::ContextStatus::CACHED;
+ CLog::Log(LOGDEBUG,"NFS: Using cached context.");
+ }
+ m_lastAccessedTime = std::chrono::steady_clock::now();
+
+ return ret;
+}
+
+bool CNfsConnection::splitUrlIntoExportAndPath(const CURL& url, std::string &exportPath, std::string &relativePath)
+{
+ //refresh exportlist if empty or hostname change
+ if(m_exportList.empty() || !StringUtils::EqualsNoCase(url.GetHostName(), m_hostName))
+ {
+ const auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return false;
+
+ const auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return false;
+
+ const int nfsVersion = settings->GetInt(SETTING_NFS_VERSION);
+
+ if (nfsVersion == 4)
+ m_exportList = {"/"};
+ else
+ m_exportList = GetExportList(url);
+ }
+
+ return splitUrlIntoExportAndPath(url, exportPath, relativePath, m_exportList);
+}
+
+bool CNfsConnection::splitUrlIntoExportAndPath(const CURL& url,std::string &exportPath, std::string &relativePath, std::list<std::string> &exportList)
+{
+ bool ret = false;
+
+ if(!exportList.empty())
+ {
+ relativePath = "";
+ exportPath = "";
+
+ std::string path = url.GetFileName();
+
+ //GetFileName returns path without leading "/"
+ //but we need it because the export paths start with "/"
+ //and path.Find(*it) wouldn't work else
+ if(path[0] != '/')
+ {
+ path = "/" + path;
+ }
+
+ for (const std::string& it : exportList)
+ {
+ //if path starts with the current export path
+ if (URIUtils::PathHasParent(path, it))
+ {
+ /* It's possible that PathHasParent() may not find the correct match first/
+ * As an example, if /path/ & and /path/sub/ are exported, but
+ * the user specifies the path /path/subdir/ (from /path/ export).
+ * If the path is longer than the exportpath, make sure / is next.
+ */
+ if ((path.length() > it.length()) && (path[it.length()] != '/') && it != "/")
+ continue;
+ exportPath = it;
+ //handle special case where root is exported
+ //in that case we don't want to strip off to
+ //much from the path
+ if( exportPath == path )
+ relativePath = "//";
+ else if( exportPath == "/" )
+ relativePath = "//" + path.substr(exportPath.length());
+ else
+ relativePath = "//" + path.substr(exportPath.length()+1);
+ ret = true;
+ break;
+ }
+ }
+ }
+ return ret;
+}
+
+bool CNfsConnection::Connect(const CURL& url, std::string &relativePath)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ int nfsRet = 0;
+ std::string exportPath;
+
+ resolveHost(url);
+ bool ret = splitUrlIntoExportAndPath(url, exportPath, relativePath);
+
+ auto now = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastAccessedTime);
+
+ if ((ret && (exportPath != m_exportPath || url.GetHostName() != m_hostName)) ||
+ duration > CONTEXT_TIMEOUT)
+ {
+ CNfsConnection::ContextStatus contextRet = getContextForExport(url.GetHostName() + exportPath);
+
+ // we need a new context because sharename or hostname has changed
+ if (contextRet == CNfsConnection::ContextStatus::INVALID)
+ {
+ return false;
+ }
+
+ // new context was created - we need to mount it
+ if (contextRet == CNfsConnection::ContextStatus::NEW)
+ {
+ //we connect to the directory of the path. This will be the "root" path of this connection then.
+ //So all fileoperations are relative to this mountpoint...
+ nfsRet = nfs_mount(m_pNfsContext, m_resolvedHostName.c_str(), exportPath.c_str());
+
+ if(nfsRet != 0)
+ {
+ CLog::Log(LOGERROR, "NFS: Failed to mount nfs share: {} ({})", exportPath,
+ nfs_get_error(m_pNfsContext));
+ destroyContext(url.GetHostName() + exportPath);
+ return false;
+ }
+ CLog::Log(LOGDEBUG, "NFS: Connected to server {} and export {}", url.GetHostName(),
+ exportPath);
+ }
+ m_exportPath = exportPath;
+ m_hostName = url.GetHostName();
+
+ // read chunksize only works after mount
+ m_readChunkSize = nfs_get_readmax(m_pNfsContext);
+ m_writeChunkSize = nfs_get_writemax(m_pNfsContext);
+
+ if (m_readChunkSize == 0)
+ {
+ CLog::Log(LOGDEBUG, "NFS Server did not return max read chunksize - Using 128K default");
+ m_readChunkSize = 128 * 1024; // 128K
+ }
+ if (m_writeChunkSize == 0)
+ {
+ CLog::Log(LOGDEBUG, "NFS Server did not return max write chunksize - Using 128K default");
+ m_writeChunkSize = 128 * 1024; // 128K
+ }
+
+ if (contextRet == CNfsConnection::ContextStatus::NEW)
+ {
+ CLog::Log(LOGDEBUG, "NFS: chunks: r/w {}/{}", (int)m_readChunkSize, (int)m_writeChunkSize);
+ }
+ }
+ return ret;
+}
+
+void CNfsConnection::Deinit()
+{
+ if(m_pNfsContext)
+ {
+ destroyOpenContexts();
+ m_pNfsContext = NULL;
+ }
+ clearMembers();
+ // clear any keep alive timeouts on deinit
+ m_KeepAliveTimeouts.clear();
+}
+
+/* This is called from CApplication::ProcessSlow() and is used to tell if nfs have been idle for too long */
+void CNfsConnection::CheckIfIdle()
+{
+ /* We check if there are open connections. This is done without a lock to not halt the mainthread. It should be thread safe as
+ worst case scenario is that m_OpenConnections could read 0 and then changed to 1 if this happens it will enter the if which will lead to another check, which is locked. */
+ if (m_OpenConnections == 0 && m_pNfsContext != NULL)
+ { /* I've set the the maximum IDLE time to be 1 min and 30 sec. */
+ std::unique_lock<CCriticalSection> lock(*this);
+ if (m_OpenConnections == 0 /* check again - when locked */)
+ {
+ const auto now = std::chrono::steady_clock::now();
+
+ if (m_IdleTimeout < now)
+ {
+ CLog::Log(LOGINFO, "NFS is idle. Closing the remaining connections.");
+ gNfsConnection.Deinit();
+ }
+ }
+ }
+
+ if( m_pNfsContext != NULL )
+ {
+ std::unique_lock<CCriticalSection> lock(keepAliveLock);
+
+ const auto now = std::chrono::steady_clock::now();
+
+ //handle keep alive on opened files
+ for (auto& it : m_KeepAliveTimeouts)
+ {
+ if (it.second.refreshTime < now)
+ {
+ keepAlive(it.second.exportPath, it.first);
+ //reset timeout
+ resetKeepAlive(it.second.exportPath, it.first);
+ }
+ }
+ }
+}
+
+//remove file handle from keep alive list on file close
+void CNfsConnection::removeFromKeepAliveList(struct nfsfh *_pFileHandle)
+{
+ std::unique_lock<CCriticalSection> lock(keepAliveLock);
+ m_KeepAliveTimeouts.erase(_pFileHandle);
+}
+
+//reset timeouts on read
+void CNfsConnection::resetKeepAlive(const std::string& _exportPath, struct nfsfh* _pFileHandle)
+{
+ std::unique_lock<CCriticalSection> lock(keepAliveLock);
+ //refresh last access time of the context aswell
+ struct nfs_context *pContext = getContextFromMap(_exportPath, true);
+
+ // if we keep alive using m_pNfsContext we need to mark
+ // its last access time too here
+ if (m_pNfsContext == pContext)
+ {
+ m_lastAccessedTime = std::chrono::steady_clock::now();
+ }
+
+ //adds new keys - refreshes existing ones
+ m_KeepAliveTimeouts[_pFileHandle].exportPath = _exportPath;
+ m_KeepAliveTimeouts[_pFileHandle].refreshTime = m_lastAccessedTime + KEEP_ALIVE_TIMEOUT;
+}
+
+//keep alive the filehandles nfs connection
+//by blindly doing a read 32bytes - seek back to where
+//we were before
+void CNfsConnection::keepAlive(const std::string& _exportPath, struct nfsfh* _pFileHandle)
+{
+ uint64_t offset = 0;
+ char buffer[32];
+ // this also refreshes the last accessed time for the context
+ // true forces a cachehit regardless the context is timedout
+ // on this call we are sure its not timedout even if the last accessed
+ // time suggests it.
+ struct nfs_context *pContext = getContextFromMap(_exportPath, true);
+
+ if (!pContext)// this should normally never happen - paranoia
+ pContext = m_pNfsContext;
+
+ CLog::LogF(LOGDEBUG, "sending keep alive after {}s.",
+ std::chrono::duration_cast<std::chrono::seconds>(KEEP_ALIVE_TIMEOUT).count());
+
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ nfs_lseek(pContext, _pFileHandle, 0, SEEK_CUR, &offset);
+
+ int bytes = nfs_read(pContext, _pFileHandle, 32, buffer);
+ if (bytes < 0)
+ {
+ CLog::LogF(LOGERROR, "nfs_read - Error ({}, {})", bytes, nfs_get_error(pContext));
+ return;
+ }
+
+ nfs_lseek(pContext, _pFileHandle, offset, SEEK_SET, &offset);
+}
+
+int CNfsConnection::stat(const CURL& url, nfs_stat_64* statbuff)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ int nfsRet = 0;
+ std::string exportPath;
+ std::string relativePath;
+ struct nfs_context *pTmpContext = NULL;
+
+ resolveHost(url);
+
+ if(splitUrlIntoExportAndPath(url, exportPath, relativePath))
+ {
+ pTmpContext = nfs_init_context();
+
+ if(pTmpContext)
+ {
+ setOptions(pTmpContext);
+ //we connect to the directory of the path. This will be the "root" path of this connection then.
+ //So all fileoperations are relative to this mountpoint...
+ nfsRet = nfs_mount(pTmpContext, m_resolvedHostName.c_str(), exportPath.c_str());
+
+ if(nfsRet == 0)
+ {
+ nfsRet = nfs_stat64(pTmpContext, relativePath.c_str(), statbuff);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "NFS: Failed to mount nfs share: {} ({})", exportPath,
+ nfs_get_error(m_pNfsContext));
+ }
+
+ nfs_destroy_context(pTmpContext);
+ CLog::Log(LOGDEBUG, "NFS: Connected to server {} and export {} in tmpContext",
+ url.GetHostName(), exportPath);
+ }
+ }
+ return nfsRet;
+}
+
+/* The following two function is used to keep track on how many Opened files/directories there are.
+needed for unloading the dylib*/
+void CNfsConnection::AddActiveConnection()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ m_OpenConnections++;
+}
+
+void CNfsConnection::AddIdleConnection()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ m_OpenConnections--;
+ /* If we close a file we reset the idle timer so that we don't have any weird behaviours if a user
+ leaves the movie paused for a long while and then press stop */
+ const auto now = std::chrono::steady_clock::now();
+ m_IdleTimeout = now + IDLE_TIMEOUT;
+}
+
+
+void CNfsConnection::setOptions(struct nfs_context* context)
+{
+ const auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ const auto advancedSettings = settingsComponent->GetAdvancedSettings();
+ if (!advancedSettings)
+ return;
+
+#ifdef HAS_NFS_SET_TIMEOUT
+ uint32_t timeout = advancedSettings->m_nfsTimeout;
+ nfs_set_timeout(context, timeout > 0 ? timeout * 1000 : -1);
+#endif
+ int retries = advancedSettings->m_nfsRetries;
+ nfs_set_autoreconnect(context, retries);
+
+ const auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return;
+
+ const int nfsVersion = settings->GetInt(SETTING_NFS_VERSION);
+
+ int ret = nfs_set_version(context, nfsVersion);
+ if (ret != 0)
+ {
+ CLog::Log(LOGERROR, "NFS: Failed to set nfs version: {} ({})", nfsVersion,
+ nfs_get_error(context));
+ return;
+ }
+
+ CLog::Log(LOGDEBUG, "NFS: version: {}", nfsVersion);
+}
+
+CNfsConnection gNfsConnection;
+
+CNFSFile::CNFSFile()
+: m_pFileHandle(NULL)
+, m_pNfsContext(NULL)
+{
+ gNfsConnection.AddActiveConnection();
+}
+
+CNFSFile::~CNFSFile()
+{
+ Close();
+ gNfsConnection.AddIdleConnection();
+}
+
+int64_t CNFSFile::GetPosition()
+{
+ int ret = 0;
+ uint64_t offset = 0;
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+
+ if (gNfsConnection.GetNfsContext() == NULL || m_pFileHandle == NULL) return 0;
+
+ ret = nfs_lseek(gNfsConnection.GetNfsContext(), m_pFileHandle, 0, SEEK_CUR, &offset);
+
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "NFS: Failed to lseek({})", nfs_get_error(gNfsConnection.GetNfsContext()));
+ }
+ return offset;
+}
+
+int64_t CNFSFile::GetLength()
+{
+ if (m_pFileHandle == NULL) return 0;
+ return m_fileSize;
+}
+
+bool CNFSFile::Open(const CURL& url)
+{
+ Close();
+ // we can't open files like nfs://file.f or nfs://server/file.f
+ // if a file matches the if below return false, it can't exist on a nfs share.
+ if (!IsValidFile(url.GetFileName()))
+ {
+ CLog::Log(LOGINFO, "NFS: Bad URL : '{}'", url.GetFileName());
+ return false;
+ }
+
+ std::string filename;
+
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+
+ if (!gNfsConnection.Connect(url, filename))
+ return false;
+
+ m_pNfsContext = gNfsConnection.GetNfsContext();
+ m_exportPath = gNfsConnection.GetContextMapId();
+
+ int ret = nfs_open(m_pNfsContext, filename.c_str(), O_RDONLY, &m_pFileHandle);
+
+ if (ret == NFS4ERR_EXPIRED) // client session expired due no activity/keep alive
+ {
+ CLog::Log(LOGERROR,
+ "CNFSFile::Open: Unable to open file - trying again with a new context: error: '{}'",
+ nfs_get_error(m_pNfsContext));
+
+ gNfsConnection.Deinit();
+ m_pNfsContext = gNfsConnection.GetNfsContext();
+ m_exportPath = gNfsConnection.GetContextMapId();
+ ret = nfs_open(m_pNfsContext, filename.c_str(), O_RDONLY, &m_pFileHandle);
+ }
+
+ if (ret != 0)
+ {
+ CLog::Log(LOGERROR, "CNFSFile::Open: Unable to open file: '{}' error: '{}'", url.GetFileName(),
+ nfs_get_error(m_pNfsContext));
+
+ m_pNfsContext = nullptr;
+ m_exportPath.clear();
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "CNFSFile::Open - opened {}", url.GetFileName());
+ m_url=url;
+
+ struct __stat64 tmpBuffer;
+
+ if( Stat(&tmpBuffer) )
+ {
+ m_url.Reset();
+ Close();
+ return false;
+ }
+
+ m_fileSize = tmpBuffer.st_size;//cache the size of this file
+ // We've successfully opened the file!
+ return true;
+}
+
+bool CNFSFile::Exists(const CURL& url)
+{
+ return Stat(url,NULL) == 0;
+}
+
+int CNFSFile::Stat(struct __stat64* buffer)
+{
+ return Stat(m_url,buffer);
+}
+
+
+int CNFSFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ int ret = 0;
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string filename;
+
+ if(!gNfsConnection.Connect(url,filename))
+ return -1;
+
+ nfs_stat_64 tmpBuffer = {};
+
+ ret = nfs_stat64(gNfsConnection.GetNfsContext(), filename.c_str(), &tmpBuffer);
+
+ //if buffer == NULL we where called from Exists - in that case don't spam the log with errors
+ if (ret != 0 && buffer != NULL)
+ {
+ CLog::Log(LOGERROR, "NFS: Failed to stat({}) {}", url.GetFileName(),
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ ret = -1;
+ }
+ else
+ {
+ if (buffer)
+ {
+ *buffer = {};
+ buffer->st_dev = tmpBuffer.nfs_dev;
+ buffer->st_ino = tmpBuffer.nfs_ino;
+ buffer->st_mode = tmpBuffer.nfs_mode;
+ buffer->st_nlink = tmpBuffer.nfs_nlink;
+ buffer->st_uid = tmpBuffer.nfs_uid;
+ buffer->st_gid = tmpBuffer.nfs_gid;
+ buffer->st_rdev = tmpBuffer.nfs_rdev;
+ buffer->st_size = tmpBuffer.nfs_size;
+ buffer->st_atime = tmpBuffer.nfs_atime;
+ buffer->st_mtime = tmpBuffer.nfs_mtime;
+ buffer->st_ctime = tmpBuffer.nfs_ctime;
+ }
+ }
+ return ret;
+}
+
+ssize_t CNFSFile::Read(void *lpBuf, size_t uiBufSize)
+{
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+ ssize_t numberOfBytesRead = 0;
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+
+ if (m_pFileHandle == NULL || m_pNfsContext == NULL )
+ return -1;
+
+ numberOfBytesRead = nfs_read(m_pNfsContext, m_pFileHandle, uiBufSize, (char *)lpBuf);
+
+ lock.unlock(); //no need to keep the connection lock after that
+
+ gNfsConnection.resetKeepAlive(m_exportPath, m_pFileHandle);//triggers keep alive timer reset for this filehandle
+
+ //something went wrong ...
+ if (numberOfBytesRead < 0)
+ CLog::Log(LOGERROR, "{} - Error( {}, {} )", __FUNCTION__, (int64_t)numberOfBytesRead,
+ nfs_get_error(m_pNfsContext));
+
+ return numberOfBytesRead;
+}
+
+int64_t CNFSFile::Seek(int64_t iFilePosition, int iWhence)
+{
+ int ret = 0;
+ uint64_t offset = 0;
+
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ if (m_pFileHandle == NULL || m_pNfsContext == NULL) return -1;
+
+
+ ret = nfs_lseek(m_pNfsContext, m_pFileHandle, iFilePosition, iWhence, &offset);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "{} - Error( seekpos: {}, whence: {}, fsize: {}, {})", __FUNCTION__,
+ iFilePosition, iWhence, m_fileSize, nfs_get_error(m_pNfsContext));
+ return -1;
+ }
+ return (int64_t)offset;
+}
+
+int CNFSFile::Truncate(int64_t iSize)
+{
+ int ret = 0;
+
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ if (m_pFileHandle == NULL || m_pNfsContext == NULL) return -1;
+
+
+ ret = nfs_ftruncate(m_pNfsContext, m_pFileHandle, iSize);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "{} - Error( ftruncate: {}, fsize: {}, {})", __FUNCTION__, iSize,
+ m_fileSize, nfs_get_error(m_pNfsContext));
+ return -1;
+ }
+ return ret;
+}
+
+void CNFSFile::Close()
+{
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+
+ if (m_pFileHandle != NULL && m_pNfsContext != NULL)
+ {
+ int ret = 0;
+ CLog::Log(LOGDEBUG, "CNFSFile::Close closing file {}", m_url.GetFileName());
+ // remove it from keep alive list before closing
+ // so keep alive code doesn't process it anymore
+ gNfsConnection.removeFromKeepAliveList(m_pFileHandle);
+ ret = nfs_close(m_pNfsContext, m_pFileHandle);
+
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "Failed to close({}) - {}", m_url.GetFileName(),
+ nfs_get_error(m_pNfsContext));
+ }
+ m_pFileHandle = NULL;
+ m_pNfsContext = NULL;
+ m_fileSize = 0;
+ m_exportPath.clear();
+ }
+}
+
+//this was a bitch!
+//for nfs write to work we have to write chunked
+//otherwise this could crash on big files
+ssize_t CNFSFile::Write(const void* lpBuf, size_t uiBufSize)
+{
+ size_t numberOfBytesWritten = 0;
+ int writtenBytes = 0;
+ size_t leftBytes = uiBufSize;
+ //clamp max write chunksize to 32kb - fixme - this might be superfluous with future libnfs versions
+ size_t chunkSize = gNfsConnection.GetMaxWriteChunkSize() > 32768 ? 32768 : (size_t)gNfsConnection.GetMaxWriteChunkSize();
+
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+
+ if (m_pFileHandle == NULL || m_pNfsContext == NULL) return -1;
+
+ //write as long as some bytes are left to be written
+ while( leftBytes )
+ {
+ //the last chunk could be smalle than chunksize
+ if(leftBytes < chunkSize)
+ {
+ chunkSize = leftBytes;//write last chunk with correct size
+ }
+ //write chunk
+ //! @bug libnfs < 2.0.0 isn't const correct
+ writtenBytes = nfs_write(m_pNfsContext,
+ m_pFileHandle,
+ chunkSize,
+ const_cast<char*>((const char *)lpBuf) + numberOfBytesWritten);
+ //decrease left bytes
+ leftBytes-= writtenBytes;
+ //increase overall written bytes
+ numberOfBytesWritten += writtenBytes;
+
+ //danger - something went wrong
+ if (writtenBytes < 0)
+ {
+ CLog::Log(LOGERROR, "Failed to pwrite({}) {}", m_url.GetFileName(),
+ nfs_get_error(m_pNfsContext));
+ if (numberOfBytesWritten == 0)
+ return -1;
+
+ break;
+ }
+ }
+ //return total number of written bytes
+ return numberOfBytesWritten;
+}
+
+bool CNFSFile::Delete(const CURL& url)
+{
+ int ret = 0;
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string filename;
+
+ if(!gNfsConnection.Connect(url, filename))
+ return false;
+
+
+ ret = nfs_unlink(gNfsConnection.GetNfsContext(), filename.c_str());
+
+ if(ret != 0)
+ {
+ CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__,
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ }
+ return (ret == 0);
+}
+
+bool CNFSFile::Rename(const CURL& url, const CURL& urlnew)
+{
+ int ret = 0;
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string strFile;
+
+ if(!gNfsConnection.Connect(url,strFile))
+ return false;
+
+ std::string strFileNew;
+ std::string strDummy;
+ gNfsConnection.splitUrlIntoExportAndPath(urlnew, strDummy, strFileNew);
+
+ ret = nfs_rename(gNfsConnection.GetNfsContext() , strFile.c_str(), strFileNew.c_str());
+
+ if(ret != 0)
+ {
+ CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__,
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ }
+ return (ret == 0);
+}
+
+bool CNFSFile::OpenForWrite(const CURL& url, bool bOverWrite)
+{
+ int ret = 0;
+ // we can't open files like nfs://file.f or nfs://server/file.f
+ // if a file matches the if below return false, it can't exist on a nfs share.
+ if (!IsValidFile(url.GetFileName())) return false;
+
+ Close();
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string filename;
+
+ if(!gNfsConnection.Connect(url,filename))
+ return false;
+
+ m_pNfsContext = gNfsConnection.GetNfsContext();
+ m_exportPath = gNfsConnection.GetContextMapId();
+
+ if (bOverWrite)
+ {
+ CLog::Log(LOGWARNING, "FileNFS::OpenForWrite() called with overwriting enabled! - {}",
+ filename);
+ //create file with proper permissions
+ ret = nfs_creat(m_pNfsContext, filename.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, &m_pFileHandle);
+ //if file was created the file handle isn't valid ... so close it and open later
+ if(ret == 0)
+ {
+ nfs_close(m_pNfsContext,m_pFileHandle);
+ m_pFileHandle = NULL;
+ }
+ }
+
+ ret = nfs_open(m_pNfsContext, filename.c_str(), O_RDWR, &m_pFileHandle);
+
+ if (ret || m_pFileHandle == NULL)
+ {
+ // write error to logfile
+ CLog::Log(LOGERROR, "CNFSFile::Open: Unable to open file : '{}' error : '{}'", filename,
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ m_pNfsContext = NULL;
+ m_exportPath.clear();
+ return false;
+ }
+ m_url=url;
+
+ struct __stat64 tmpBuffer = {};
+
+ //only stat if file was not created
+ if(!bOverWrite)
+ {
+ if(Stat(&tmpBuffer))
+ {
+ m_url.Reset();
+ Close();
+ return false;
+ }
+ m_fileSize = tmpBuffer.st_size;//cache filesize of this file
+ }
+ else//file was created - filesize is zero
+ {
+ m_fileSize = 0;
+ }
+
+ // We've successfully opened the file!
+ return true;
+}
+
+bool CNFSFile::IsValidFile(const std::string& strFileName)
+{
+ if (strFileName.find('/') == std::string::npos || /* doesn't have sharename */
+ StringUtils::EndsWith(strFileName, "/.") || /* not current folder */
+ StringUtils::EndsWith(strFileName, "/..")) /* not parent folder */
+ return false;
+ return true;
+}
diff --git a/xbmc/filesystem/NFSFile.h b/xbmc/filesystem/NFSFile.h
new file mode 100644
index 0000000..9e9dde9
--- /dev/null
+++ b/xbmc/filesystem/NFSFile.h
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2011-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
+
+// FileNFS.h: interface for the CNFSFile class.
+
+#include "IFile.h"
+#include "URL.h"
+#include "threads/CriticalSection.h"
+
+#include <chrono>
+#include <list>
+#include <map>
+
+struct nfs_stat_64;
+
+class CNfsConnection : public CCriticalSection
+{
+public:
+ struct keepAliveStruct
+ {
+ std::string exportPath;
+ std::chrono::time_point<std::chrono::steady_clock> refreshTime;
+ };
+ typedef std::map<struct nfsfh *, struct keepAliveStruct> tFileKeepAliveMap;
+
+ struct contextTimeout
+ {
+ struct nfs_context *pContext;
+ std::chrono::time_point<std::chrono::steady_clock> lastAccessedTime;
+ };
+
+ typedef std::map<std::string, struct contextTimeout> tOpenContextMap;
+
+ CNfsConnection();
+ ~CNfsConnection();
+ bool Connect(const CURL &url, std::string &relativePath);
+ struct nfs_context *GetNfsContext() {return m_pNfsContext;}
+ uint64_t GetMaxReadChunkSize() {return m_readChunkSize;}
+ uint64_t GetMaxWriteChunkSize() {return m_writeChunkSize;}
+ std::list<std::string> GetExportList(const CURL &url);
+ //this functions splits the url into the exportpath (feed to mount) and the rest of the path
+ //relative to the mounted export
+ bool splitUrlIntoExportAndPath(const CURL& url, std::string &exportPath, std::string &relativePath, std::list<std::string> &exportList);
+ bool splitUrlIntoExportAndPath(const CURL& url, std::string &exportPath, std::string &relativePath);
+
+ //special stat which uses its own context
+ //needed for getting intervolume symlinks to work
+ int stat(const CURL& url, nfs_stat_64* statbuff);
+
+ void AddActiveConnection();
+ void AddIdleConnection();
+ void CheckIfIdle();
+ void Deinit();
+ //adds the filehandle to the keep alive list or resets
+ //the timeout for this filehandle if already in list
+ void resetKeepAlive(const std::string& _exportPath, struct nfsfh* _pFileHandle);
+ //removes file handle from keep alive list
+ void removeFromKeepAliveList(struct nfsfh *_pFileHandle);
+
+ const std::string& GetConnectedIp() const {return m_resolvedHostName;}
+ const std::string& GetConnectedExport() const {return m_exportPath;}
+ const std::string GetContextMapId() const {return m_hostName + m_exportPath;}
+
+private:
+ enum class ContextStatus
+ {
+ INVALID,
+ NEW,
+ CACHED
+ };
+
+ struct nfs_context *m_pNfsContext;//current nfs context
+ std::string m_exportPath;//current connected export path
+ std::string m_hostName;//current connected host
+ std::string m_resolvedHostName;//current connected host - as ip
+ uint64_t m_readChunkSize = 0;//current read chunksize of connected server
+ uint64_t m_writeChunkSize = 0;//current write chunksize of connected server
+ int m_OpenConnections = 0; //number of open connections
+ std::chrono::time_point<std::chrono::steady_clock> m_IdleTimeout;
+ tFileKeepAliveMap m_KeepAliveTimeouts;//mapping filehandles to its idle timeout
+ tOpenContextMap m_openContextMap;//unique map for tracking all open contexts
+ std::chrono::time_point<std::chrono::steady_clock>
+ m_lastAccessedTime; //last access time for m_pNfsContext
+ std::list<std::string> m_exportList;//list of exported paths of current connected servers
+ CCriticalSection keepAliveLock;
+ CCriticalSection openContextLock;
+
+ void clearMembers();
+ struct nfs_context *getContextFromMap(const std::string &exportname, bool forceCacheHit = false);
+
+ // get context for given export and add to open contexts map - sets m_pNfsContext (may return an already mounted cached context)
+ ContextStatus getContextForExport(const std::string& exportname);
+ void destroyOpenContexts();
+ void destroyContext(const std::string &exportName);
+ void resolveHost(const CURL &url);//resolve hostname by dnslookup
+ void keepAlive(const std::string& _exportPath, struct nfsfh* _pFileHandle);
+ static void setOptions(struct nfs_context* context);
+};
+
+extern CNfsConnection gNfsConnection;
+
+namespace XFILE
+{
+ class CNFSFile : public IFile
+ {
+ public:
+ CNFSFile();
+ ~CNFSFile() override;
+ void Close() override;
+ int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override;
+ ssize_t Read(void* lpBuf, size_t uiBufSize) override;
+ 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;
+ int64_t GetLength() override;
+ int64_t GetPosition() override;
+ ssize_t Write(const void* lpBuf, size_t uiBufSize) override;
+ int Truncate(int64_t iSize) override;
+
+ //implement iocontrol for seek_possible for preventing the stat in File class for
+ //getting this info ...
+ int IoControl(EIoControl request, void* param) override
+ {
+ return request == IOCTRL_SEEK_POSSIBLE ? 1 : -1;
+ }
+ int GetChunkSize() override {return static_cast<int>(gNfsConnection.GetMaxReadChunkSize());}
+
+ bool OpenForWrite(const CURL& url, bool bOverWrite = false) override;
+ bool Delete(const CURL& url) override;
+ bool Rename(const CURL& url, const CURL& urlnew) override;
+ protected:
+ CURL m_url;
+ bool IsValidFile(const std::string& strFileName);
+ int64_t m_fileSize = 0;
+ struct nfsfh *m_pFileHandle;
+ struct nfs_context *m_pNfsContext;//current nfs context
+ std::string m_exportPath;
+ };
+}
+
diff --git a/xbmc/filesystem/NptXbmcFile.cpp b/xbmc/filesystem/NptXbmcFile.cpp
new file mode 100644
index 0000000..bcbdea1
--- /dev/null
+++ b/xbmc/filesystem/NptXbmcFile.cpp
@@ -0,0 +1,534 @@
+/*
+ * Neptune - Files :: XBMC Implementation
+ *
+ * Copyright (c) 2002-2008, Axiomatic Systems, LLC.
+ * All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ * See LICENSES/README.md for more information.
+ */
+
+/*----------------------------------------------------------------------
+| includes
++---------------------------------------------------------------------*/
+#include "File.h"
+#include "FileFactory.h"
+#include "PasswordManager.h"
+#include "URL.h"
+#include "utils/URIUtils.h"
+
+#include <limits>
+
+#include <Neptune/Source/Core/NptDebug.h>
+#include <Neptune/Source/Core/NptFile.h>
+#include <Neptune/Source/Core/NptStrings.h>
+#include <Neptune/Source/Core/NptUtils.h>
+
+#ifdef TARGET_WINDOWS
+#define S_IWUSR _S_IWRITE
+#define S_ISDIR(m) ((m & _S_IFDIR) != 0)
+#define S_ISREG(m) ((m & _S_IFREG) != 0)
+#endif
+
+using namespace XFILE;
+
+typedef NPT_Reference<IFile> NPT_XbmcFileReference;
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFileStream
++---------------------------------------------------------------------*/
+class NPT_XbmcFileStream
+{
+public:
+ // constructors and destructor
+ explicit NPT_XbmcFileStream(const NPT_XbmcFileReference& file) : m_FileReference(file) {}
+
+ // NPT_FileInterface methods
+ NPT_Result Seek(NPT_Position offset);
+ NPT_Result Tell(NPT_Position& offset);
+ NPT_Result Flush();
+
+protected:
+ // constructors and destructors
+ virtual ~NPT_XbmcFileStream() = default;
+
+ // members
+ NPT_XbmcFileReference m_FileReference;
+};
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFileStream::Seek
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_XbmcFileStream::Seek(NPT_Position offset)
+{
+ int64_t result;
+
+ result = m_FileReference->Seek(offset, SEEK_SET) ;
+ if (result >= 0) {
+ return NPT_SUCCESS;
+ } else {
+ return NPT_FAILURE;
+ }
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFileStream::Tell
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_XbmcFileStream::Tell(NPT_Position& offset)
+{
+ int64_t result = m_FileReference->GetPosition();
+ if (result >= 0) {
+ offset = (NPT_Position)result;
+ return NPT_SUCCESS;
+ } else {
+ return NPT_FAILURE;
+ }
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFileStream::Flush
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_XbmcFileStream::Flush()
+{
+ m_FileReference->Flush();
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFileInputStream
++---------------------------------------------------------------------*/
+class NPT_XbmcFileInputStream : public NPT_InputStream,
+ private NPT_XbmcFileStream
+
+{
+public:
+ // constructors and destructor
+ explicit NPT_XbmcFileInputStream(NPT_XbmcFileReference& file) :
+ NPT_XbmcFileStream(file) {}
+
+ // NPT_InputStream methods
+ NPT_Result Read(void* buffer,
+ NPT_Size bytes_to_read,
+ NPT_Size* bytes_read) override;
+ NPT_Result Seek(NPT_Position offset) override {
+ return NPT_XbmcFileStream::Seek(offset);
+ }
+ NPT_Result Tell(NPT_Position& offset) override {
+ return NPT_XbmcFileStream::Tell(offset);
+ }
+ NPT_Result GetSize(NPT_LargeSize& size) override;
+ NPT_Result GetAvailable(NPT_LargeSize& available) override;
+};
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFileInputStream::Read
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_XbmcFileInputStream::Read(void* buffer,
+ NPT_Size bytes_to_read,
+ NPT_Size* bytes_read)
+{
+ unsigned int nb_read;
+
+ // check the parameters
+ if (buffer == NULL) {
+ return NPT_ERROR_INVALID_PARAMETERS;
+ }
+
+ // read from the file
+ nb_read = m_FileReference->Read(buffer, bytes_to_read);
+ if (nb_read > 0) {
+ if (bytes_read) *bytes_read = (NPT_Size)nb_read;
+ return NPT_SUCCESS;
+ } else {
+ if (bytes_read) *bytes_read = 0;
+ return NPT_ERROR_EOS;
+ //} else { // currently no way to indicate failure
+ // if (bytes_read) *bytes_read = 0;
+ // return NPT_ERROR_READ_FAILED;
+ }
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFileInputStream::GetSize
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_XbmcFileInputStream::GetSize(NPT_LargeSize& size)
+{
+ size = m_FileReference->GetLength();
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFileInputStream::GetAvailable
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_XbmcFileInputStream::GetAvailable(NPT_LargeSize& available)
+{
+ int64_t offset = m_FileReference->GetPosition();
+ NPT_LargeSize size = 0;
+
+ if (NPT_SUCCEEDED(GetSize(size)) && offset >= 0 && (NPT_LargeSize)offset <= size) {
+ available = size - offset;
+ return NPT_SUCCESS;
+ } else {
+ available = 0;
+ return NPT_FAILURE;
+ }
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFileOutputStream
++---------------------------------------------------------------------*/
+class NPT_XbmcFileOutputStream : public NPT_OutputStream,
+ private NPT_XbmcFileStream
+{
+public:
+ // constructors and destructor
+ explicit NPT_XbmcFileOutputStream(NPT_XbmcFileReference& file) :
+ NPT_XbmcFileStream(file) {}
+
+ // NPT_OutputStream methods
+ NPT_Result Write(const void* buffer,
+ NPT_Size bytes_to_write,
+ NPT_Size* bytes_written) override;
+ NPT_Result Seek(NPT_Position offset) override {
+ return NPT_XbmcFileStream::Seek(offset);
+ }
+ NPT_Result Tell(NPT_Position& offset) override {
+ return NPT_XbmcFileStream::Tell(offset);
+ }
+ NPT_Result Flush() override {
+ return NPT_XbmcFileStream::Flush();
+ }
+};
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFileOutputStream::Write
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_XbmcFileOutputStream::Write(const void* buffer,
+ NPT_Size bytes_to_write,
+ NPT_Size* bytes_written)
+{
+ int nb_written;
+ nb_written = m_FileReference->Write(buffer, bytes_to_write);
+
+ if (nb_written > 0) {
+ if (bytes_written) *bytes_written = (NPT_Size)nb_written;
+ return NPT_SUCCESS;
+ } else {
+ if (bytes_written) *bytes_written = 0;
+ return NPT_ERROR_WRITE_FAILED;
+ }
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFile
++---------------------------------------------------------------------*/
+class NPT_XbmcFile: public NPT_FileInterface
+{
+public:
+ // constructors and destructor
+ explicit NPT_XbmcFile(NPT_File& delegator);
+ ~NPT_XbmcFile() override;
+
+ // NPT_FileInterface methods
+ NPT_Result Open(OpenMode mode) override;
+ NPT_Result Close() override;
+ NPT_Result GetInputStream(NPT_InputStreamReference& stream) override;
+ NPT_Result GetOutputStream(NPT_OutputStreamReference& stream) override;
+
+private:
+ // members
+ NPT_File& m_Delegator;
+ OpenMode m_Mode;
+ NPT_XbmcFileReference m_FileReference;
+};
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFile::NPT_XbmcFile
++---------------------------------------------------------------------*/
+NPT_XbmcFile::NPT_XbmcFile(NPT_File& delegator) :
+ m_Delegator(delegator),
+ m_Mode(0)
+{
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFile::~NPT_XbmcFile
++---------------------------------------------------------------------*/
+NPT_XbmcFile::~NPT_XbmcFile()
+{
+ Close();
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFile::Open
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_XbmcFile::Open(NPT_File::OpenMode mode)
+{
+ NPT_XbmcFileReference file;
+
+ // check if we're already open
+ if (!m_FileReference.IsNull()) {
+ return NPT_ERROR_FILE_ALREADY_OPEN;
+ }
+
+ // store the mode
+ m_Mode = mode;
+
+ // check for special names
+ const char* name = (const char*)m_Delegator.GetPath();
+ if (NPT_StringsEqual(name, NPT_FILE_STANDARD_INPUT)) {
+ return NPT_ERROR_FILE_NOT_READABLE;
+ } else if (NPT_StringsEqual(name, NPT_FILE_STANDARD_OUTPUT)) {
+ return NPT_ERROR_FILE_NOT_WRITABLE;
+ } else if (NPT_StringsEqual(name, NPT_FILE_STANDARD_ERROR)) {
+ return NPT_ERROR_FILE_NOT_WRITABLE;
+ } else {
+
+ file = CFileFactory::CreateLoader(name);
+ if (file.IsNull()) {
+ return NPT_ERROR_NO_SUCH_FILE;
+ }
+
+ bool result;
+ CURL url(URIUtils::SubstitutePath(name));
+
+ if (CPasswordManager::GetInstance().IsURLSupported(url) && url.GetUserName().empty())
+ CPasswordManager::GetInstance().AuthenticateURL(url);
+
+ // compute mode
+ if (mode & NPT_FILE_OPEN_MODE_WRITE)
+ result = file->OpenForWrite(url, (mode & NPT_FILE_OPEN_MODE_TRUNCATE) ? true : false);
+ else
+ result = file->Open(url);
+
+ if (!result) return NPT_ERROR_NO_SUCH_FILE;
+ }
+
+ // store reference
+ m_FileReference = file;
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFile::Close
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_XbmcFile::Close()
+{
+ // release the file reference
+ m_FileReference = NULL;
+
+ // reset the mode
+ m_Mode = 0;
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFile::GetInputStream
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_XbmcFile::GetInputStream(NPT_InputStreamReference& stream)
+{
+ // default value
+ stream = NULL;
+
+ // check that the file is open
+ if (m_FileReference.IsNull()) return NPT_ERROR_FILE_NOT_OPEN;
+
+ // check that the mode is compatible
+ if (!(m_Mode & NPT_FILE_OPEN_MODE_READ)) {
+ return NPT_ERROR_FILE_NOT_READABLE;
+ }
+
+ // create a stream
+ stream = new NPT_XbmcFileInputStream(m_FileReference);
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| NPT_XbmcFile::GetOutputStream
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_XbmcFile::GetOutputStream(NPT_OutputStreamReference& stream)
+{
+ // default value
+ stream = NULL;
+
+ // check that the file is open
+ if (m_FileReference.IsNull()) return NPT_ERROR_FILE_NOT_OPEN;
+
+ // check that the mode is compatible
+ if (!(m_Mode & NPT_FILE_OPEN_MODE_WRITE)) {
+ return NPT_ERROR_FILE_NOT_WRITABLE;
+ }
+
+ // create a stream
+ stream = new NPT_XbmcFileOutputStream(m_FileReference);
+
+ return NPT_SUCCESS;
+}
+
+static NPT_Result
+MapErrno(int err) {
+ switch (err) {
+ case EACCES: return NPT_ERROR_PERMISSION_DENIED;
+ case EPERM: return NPT_ERROR_PERMISSION_DENIED;
+ case ENOENT: return NPT_ERROR_NO_SUCH_FILE;
+ case ENAMETOOLONG: return NPT_ERROR_INVALID_PARAMETERS;
+ case EBUSY: return NPT_ERROR_FILE_BUSY;
+ case EROFS: return NPT_ERROR_FILE_NOT_WRITABLE;
+ case ENOTDIR: return NPT_ERROR_FILE_NOT_DIRECTORY;
+ case EEXIST: return NPT_ERROR_FILE_ALREADY_EXISTS;
+ case ENOSPC: return NPT_ERROR_FILE_NOT_ENOUGH_SPACE;
+ case ENOTEMPTY: return NPT_ERROR_DIRECTORY_NOT_EMPTY;
+ default: return NPT_ERROR_ERRNO(err);
+ }
+}
+/*----------------------------------------------------------------------
+| NPT_FilePath::Separator
++---------------------------------------------------------------------*/
+const char* const NPT_FilePath::Separator = "/";
+
+/*----------------------------------------------------------------------
+| NPT_File::NPT_File
++---------------------------------------------------------------------*/
+NPT_File::NPT_File(const char* path) : m_Path(path)
+{
+ m_Delegate = new NPT_XbmcFile(*this);
+}
+
+/*----------------------------------------------------------------------
+| NPT_File::operator=
++---------------------------------------------------------------------*/
+NPT_File&
+NPT_File::operator=(const NPT_File& file)
+{
+ if (this != &file) {
+ delete m_Delegate;
+ m_Path = file.m_Path;
+ m_Delegate = new NPT_XbmcFile(*this);
+ }
+ return *this;
+}
+
+/*----------------------------------------------------------------------
+| NPT_File::GetRoots
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_File::GetRoots(NPT_List<NPT_String>& roots)
+{
+ return NPT_FAILURE;
+}
+
+/*----------------------------------------------------------------------
+| NPT_File::CreateDir
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_File::CreateDir(const char* path)
+{
+ return NPT_ERROR_PERMISSION_DENIED;
+}
+
+/*----------------------------------------------------------------------
+| NPT_File::RemoveFile
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_File::RemoveFile(const char* path)
+{
+ return NPT_ERROR_PERMISSION_DENIED;
+}
+
+/*----------------------------------------------------------------------
+| NPT_File::RemoveDir
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_File::RemoveDir(const char* path)
+{
+ return NPT_ERROR_PERMISSION_DENIED;
+}
+
+/*----------------------------------------------------------------------
+| NPT_File::Rename
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_File::Rename(const char* from_path, const char* to_path)
+{
+ return NPT_ERROR_PERMISSION_DENIED;
+}
+
+/*----------------------------------------------------------------------
+| NPT_File::ListDir
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_File::ListDir(const char* path,
+ NPT_List<NPT_String>& entries,
+ NPT_Ordinal start /* = 0 */,
+ NPT_Cardinal max /* = 0 */)
+{
+ return NPT_FAILURE;
+}
+
+/*----------------------------------------------------------------------
+| NPT_File::GetWorkingDir
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_File::GetWorkingDir(NPT_String& path)
+{
+ return NPT_FAILURE;
+}
+
+/*----------------------------------------------------------------------
+| NPT_File::GetInfo
++---------------------------------------------------------------------*/
+NPT_Result
+NPT_File::GetInfo(const char* path, NPT_FileInfo* info)
+{
+ struct __stat64 stat_buffer = {};
+ int result;
+
+ if (!info)
+ return NPT_FAILURE;
+
+ *info = NPT_FileInfo();
+
+ result = CFile::Stat(path, &stat_buffer);
+ if (result != 0)
+ return MapErrno(errno);
+ if (info)
+ {
+ info->m_Size = stat_buffer.st_size;
+ if (S_ISREG(stat_buffer.st_mode))
+ {
+ info->m_Type = NPT_FileInfo::FILE_TYPE_REGULAR;
+ }
+ else if (S_ISDIR(stat_buffer.st_mode))
+ {
+ info->m_Type = NPT_FileInfo::FILE_TYPE_DIRECTORY;
+ }
+ else
+ {
+ info->m_Type = NPT_FileInfo::FILE_TYPE_OTHER;
+ }
+ info->m_AttributesMask &= NPT_FILE_ATTRIBUTE_READ_ONLY;
+ if ((stat_buffer.st_mode & S_IWUSR) == 0)
+ {
+ info->m_Attributes &= NPT_FILE_ATTRIBUTE_READ_ONLY;
+ }
+ info->m_CreationTime.SetSeconds(0);
+ info->m_ModificationTime.SetSeconds(stat_buffer.st_mtime);
+ }
+
+ return NPT_SUCCESS;
+}
+
diff --git a/xbmc/filesystem/OverrideDirectory.cpp b/xbmc/filesystem/OverrideDirectory.cpp
new file mode 100644
index 0000000..0e24247
--- /dev/null
+++ b/xbmc/filesystem/OverrideDirectory.cpp
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2014-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 "OverrideDirectory.h"
+
+#include "URL.h"
+#include "filesystem/Directory.h"
+
+using namespace XFILE;
+
+
+COverrideDirectory::COverrideDirectory() = default;
+
+
+COverrideDirectory::~COverrideDirectory() = default;
+
+bool COverrideDirectory::Create(const CURL& url)
+{
+ std::string translatedPath = TranslatePath(url);
+
+ return CDirectory::Create(translatedPath.c_str());
+}
+
+bool COverrideDirectory::Remove(const CURL& url)
+{
+ std::string translatedPath = TranslatePath(url);
+
+ return CDirectory::Remove(translatedPath.c_str());
+}
+
+bool COverrideDirectory::Exists(const CURL& url)
+{
+ std::string translatedPath = TranslatePath(url);
+
+ return CDirectory::Exists(translatedPath.c_str());
+}
diff --git a/xbmc/filesystem/OverrideDirectory.h b/xbmc/filesystem/OverrideDirectory.h
new file mode 100644
index 0000000..0c6a7ab
--- /dev/null
+++ b/xbmc/filesystem/OverrideDirectory.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2014-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 "filesystem/IDirectory.h"
+
+namespace XFILE
+{
+class COverrideDirectory : public IDirectory
+{
+public:
+ COverrideDirectory();
+ ~COverrideDirectory() override;
+
+ bool Create(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ bool Remove(const CURL& url) override;
+
+protected:
+ virtual std::string TranslatePath(const CURL &url) = 0;
+};
+}
diff --git a/xbmc/filesystem/OverrideFile.cpp b/xbmc/filesystem/OverrideFile.cpp
new file mode 100644
index 0000000..73fb43e
--- /dev/null
+++ b/xbmc/filesystem/OverrideFile.cpp
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2014-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 "OverrideFile.h"
+
+#include "URL.h"
+
+#include <sys/stat.h>
+
+using namespace XFILE;
+
+
+COverrideFile::COverrideFile(bool writable)
+ : m_writable(writable)
+{ }
+
+
+COverrideFile::~COverrideFile()
+{
+ Close();
+}
+
+bool COverrideFile::Open(const CURL& url)
+{
+ std::string strFileName = TranslatePath(url);
+
+ return m_file.Open(strFileName);
+}
+
+bool COverrideFile::OpenForWrite(const CURL& url, bool bOverWrite /* = false */)
+{
+ if (!m_writable)
+ return false;
+
+ std::string strFileName = TranslatePath(url);
+
+ return m_file.OpenForWrite(strFileName, bOverWrite);
+}
+
+bool COverrideFile::Delete(const CURL& url)
+{
+ if (!m_writable)
+ return false;
+
+ std::string strFileName = TranslatePath(url);
+
+ return m_file.Delete(strFileName);
+}
+
+bool COverrideFile::Exists(const CURL& url)
+{
+ std::string strFileName = TranslatePath(url);
+
+ return m_file.Exists(strFileName);
+}
+
+int COverrideFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ std::string strFileName = TranslatePath(url);
+
+ return m_file.Stat(strFileName, buffer);
+}
+
+bool COverrideFile::Rename(const CURL& url, const CURL& urlnew)
+{
+ if (!m_writable)
+ return false;
+
+ std::string strFileName = TranslatePath(url);
+ std::string strFileName2 = TranslatePath(urlnew);
+
+ return m_file.Rename(strFileName, strFileName2);
+}
+
+int COverrideFile::Stat(struct __stat64* buffer)
+{
+ return m_file.Stat(buffer);
+}
+
+ssize_t COverrideFile::Read(void* lpBuf, size_t uiBufSize)
+{
+ return m_file.Read(lpBuf, uiBufSize);
+}
+
+ssize_t COverrideFile::Write(const void* lpBuf, size_t uiBufSize)
+{
+ if (!m_writable)
+ return -1;
+
+ return m_file.Write(lpBuf, uiBufSize);
+}
+
+int64_t COverrideFile::Seek(int64_t iFilePosition, int iWhence /*=SEEK_SET*/)
+{
+ return m_file.Seek(iFilePosition, iWhence);
+}
+
+void COverrideFile::Close()
+{
+ m_file.Close();
+}
+
+int64_t COverrideFile::GetPosition()
+{
+ return m_file.GetPosition();
+}
+
+int64_t COverrideFile::GetLength()
+{
+ return m_file.GetLength();
+}
diff --git a/xbmc/filesystem/OverrideFile.h b/xbmc/filesystem/OverrideFile.h
new file mode 100644
index 0000000..974b473
--- /dev/null
+++ b/xbmc/filesystem/OverrideFile.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014-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 "filesystem/File.h"
+#include "filesystem/IFile.h"
+
+namespace XFILE
+{
+class COverrideFile : public IFile
+{
+public:
+ explicit COverrideFile(bool writable);
+ ~COverrideFile() override;
+
+ 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;
+
+protected:
+ virtual std::string TranslatePath(const CURL &url) = 0;
+
+ CFile m_file;
+ bool m_writable;
+};
+}
diff --git a/xbmc/filesystem/PVRDirectory.cpp b/xbmc/filesystem/PVRDirectory.cpp
new file mode 100644
index 0000000..915d3f7
--- /dev/null
+++ b/xbmc/filesystem/PVRDirectory.cpp
@@ -0,0 +1,56 @@
+/*
+ * 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 "PVRDirectory.h"
+
+#include "pvr/filesystem/PVRGUIDirectory.h"
+
+using namespace XFILE;
+using namespace PVR;
+
+CPVRDirectory::CPVRDirectory() = default;
+
+CPVRDirectory::~CPVRDirectory() = default;
+
+bool CPVRDirectory::Exists(const CURL& url)
+{
+ const CPVRGUIDirectory dir(url);
+ return dir.Exists();
+}
+
+bool CPVRDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ const CPVRGUIDirectory dir(url);
+ return dir.GetDirectory(items);
+}
+
+bool CPVRDirectory::SupportsWriteFileOperations(const std::string& strPath)
+{
+ const CPVRGUIDirectory dir(strPath);
+ return dir.SupportsWriteFileOperations();
+}
+
+bool CPVRDirectory::HasTVRecordings()
+{
+ return CPVRGUIDirectory::HasTVRecordings();
+}
+
+bool CPVRDirectory::HasDeletedTVRecordings()
+{
+ return CPVRGUIDirectory::HasDeletedTVRecordings();
+}
+
+bool CPVRDirectory::HasRadioRecordings()
+{
+ return CPVRGUIDirectory::HasRadioRecordings();
+}
+
+bool CPVRDirectory::HasDeletedRadioRecordings()
+{
+ return CPVRGUIDirectory::HasDeletedRadioRecordings();
+}
diff --git a/xbmc/filesystem/PVRDirectory.h b/xbmc/filesystem/PVRDirectory.h
new file mode 100644
index 0000000..8745fb1
--- /dev/null
+++ b/xbmc/filesystem/PVRDirectory.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 "IDirectory.h"
+
+namespace XFILE {
+
+class CPVRDirectory
+ : public IDirectory
+{
+public:
+ CPVRDirectory();
+ ~CPVRDirectory() override;
+
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool AllowAll() const override { return true; }
+ DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_NEVER; }
+ bool Exists(const CURL& url) override;
+
+ static bool SupportsWriteFileOperations(const std::string& strPath);
+
+ static bool HasTVRecordings();
+ static bool HasDeletedTVRecordings();
+ static bool HasRadioRecordings();
+ static bool HasDeletedRadioRecordings();
+};
+
+}
diff --git a/xbmc/filesystem/PipeFile.cpp b/xbmc/filesystem/PipeFile.cpp
new file mode 100644
index 0000000..f732843
--- /dev/null
+++ b/xbmc/filesystem/PipeFile.cpp
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2011-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 "PipeFile.h"
+
+#include "PipesManager.h"
+#include "URL.h"
+
+#include <mutex>
+
+using namespace XFILE;
+
+CPipeFile::CPipeFile() : m_pipe(NULL)
+{
+}
+
+CPipeFile::~CPipeFile()
+{
+ Close();
+}
+
+int64_t CPipeFile::GetPosition()
+{
+ return m_pos;
+}
+
+int64_t CPipeFile::GetLength()
+{
+ return m_length;
+}
+
+void CPipeFile::SetLength(int64_t len)
+{
+ m_length = len;
+}
+
+bool CPipeFile::Open(const CURL& url)
+{
+ std::string name = url.Get();
+ m_pipe = PipesManager::GetInstance().OpenPipe(name);
+ if (m_pipe)
+ m_pipe->AddListener(this);
+ return (m_pipe != NULL);
+}
+
+bool CPipeFile::Exists(const CURL& url)
+{
+ std::string name = url.Get();
+ return PipesManager::GetInstance().Exists(name);
+}
+
+int CPipeFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ return -1;
+}
+
+int CPipeFile::Stat(struct __stat64* buffer)
+{
+ if (!buffer)
+ return -1;
+
+ *buffer = {};
+ buffer->st_size = m_length;
+ return 0;
+}
+
+ssize_t CPipeFile::Read(void* lpBuf, size_t uiBufSize)
+{
+ if (!m_pipe)
+ return -1;
+
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+ return m_pipe->Read((char *)lpBuf,(int)uiBufSize);
+}
+
+ssize_t CPipeFile::Write(const void* lpBuf, size_t uiBufSize)
+{
+ if (!m_pipe)
+ return -1;
+
+ // m_pipe->Write return bool. either all was written or not.
+ return m_pipe->Write((const char *)lpBuf,uiBufSize) ? uiBufSize : -1;
+}
+
+void CPipeFile::SetEof()
+{
+ if (!m_pipe)
+ return ;
+ m_pipe->SetEof();
+}
+
+bool CPipeFile::IsEof()
+{
+ if (!m_pipe)
+ return true;
+ return m_pipe->IsEof();
+}
+
+bool CPipeFile::IsEmpty()
+{
+ if (!m_pipe)
+ return true;
+ return m_pipe->IsEmpty();
+}
+
+int64_t CPipeFile::Seek(int64_t iFilePosition, int iWhence)
+{
+ return -1;
+}
+
+void CPipeFile::Close()
+{
+ if (m_pipe)
+ {
+ m_pipe->RemoveListener(this);
+ PipesManager::GetInstance().ClosePipe(m_pipe);
+ }
+ m_pipe = NULL;
+}
+
+bool CPipeFile::IsClosed()
+{
+ return (m_pipe == NULL);
+}
+
+void CPipeFile::Flush()
+{
+ if (m_pipe)
+ m_pipe->Flush();
+}
+
+bool CPipeFile::OpenForWrite(const CURL& url, bool bOverWrite)
+{
+ std::string name = url.Get();
+
+ m_pipe = PipesManager::GetInstance().CreatePipe(name);
+ if (m_pipe)
+ m_pipe->AddListener(this);
+ return (m_pipe != NULL);
+}
+
+bool CPipeFile::Delete(const CURL& url)
+{
+ return false;
+}
+
+bool CPipeFile::Rename(const CURL& url, const CURL& urlnew)
+{
+ return false;
+}
+
+int CPipeFile::IoControl(EIoControl, void* param)
+{
+ return -1;
+}
+
+std::string CPipeFile::GetName() const
+{
+ if (!m_pipe)
+ return "";
+ return m_pipe->GetName();
+}
+
+void CPipeFile::OnPipeOverFlow()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ for (size_t l=0; l<m_listeners.size(); l++)
+ m_listeners[l]->OnPipeOverFlow();
+}
+
+int64_t CPipeFile::GetAvailableRead()
+{
+ return m_pipe->GetAvailableRead();
+}
+
+void CPipeFile::OnPipeUnderFlow()
+{
+ for (size_t l=0; l<m_listeners.size(); l++)
+ m_listeners[l]->OnPipeUnderFlow();
+}
+
+void CPipeFile::AddListener(IPipeListener *l)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ for (size_t i=0; i<m_listeners.size(); i++)
+ {
+ if (m_listeners[i] == l)
+ return;
+ }
+ m_listeners.push_back(l);
+}
+
+void CPipeFile::RemoveListener(IPipeListener *l)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ std::vector<XFILE::IPipeListener *>::iterator i = m_listeners.begin();
+ while(i != m_listeners.end())
+ {
+ if ( (*i) == l)
+ i = m_listeners.erase(i);
+ else
+ ++i;
+ }
+}
+
+void CPipeFile::SetOpenThreshold(int threshold)
+{
+ m_pipe->SetOpenThreshold(threshold);
+}
+
diff --git a/xbmc/filesystem/PipeFile.h b/xbmc/filesystem/PipeFile.h
new file mode 100644
index 0000000..507006b
--- /dev/null
+++ b/xbmc/filesystem/PipeFile.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2002 Frodo
+ * Portions Copyright (c) by the authors of ffmpeg and xvid
+ * Copyright (C) 2002-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
+
+// FilePipe.h: interface for the CPipeFile class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "IFile.h"
+#include "PipesManager.h"
+#include "threads/CriticalSection.h"
+
+#include <string>
+#include <vector>
+
+namespace XFILE
+{
+
+class CPipeFile : public IFile, public IPipeListener
+{
+public:
+ CPipeFile();
+ ~CPipeFile() override;
+ int64_t GetPosition() override;
+ int64_t GetLength() override;
+ virtual void SetLength(int64_t len);
+ 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;
+ 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;
+ void Flush() override;
+ virtual int64_t GetAvailableRead();
+
+ bool OpenForWrite(const CURL& url, bool bOverWrite = false) override;
+
+ bool Delete(const CURL& url) override;
+ bool Rename(const CURL& url, const CURL& urlnew) override;
+ int IoControl(EIoControl request, void* param) override;
+
+ std::string GetName() const;
+
+ void OnPipeOverFlow() override;
+ void OnPipeUnderFlow() override;
+
+ void AddListener(IPipeListener *l);
+ void RemoveListener(IPipeListener *l);
+
+ void SetEof();
+ bool IsEof();
+ bool IsEmpty();
+ bool IsClosed();
+
+ void SetOpenThreshold(int threshold);
+
+protected:
+ int64_t m_pos = 0;
+ int64_t m_length = -1;
+
+ XFILE::Pipe *m_pipe;
+
+ CCriticalSection m_lock;
+ std::vector<XFILE::IPipeListener *> m_listeners;
+};
+
+}
diff --git a/xbmc/filesystem/PipesManager.cpp b/xbmc/filesystem/PipesManager.cpp
new file mode 100644
index 0000000..58b9099
--- /dev/null
+++ b/xbmc/filesystem/PipesManager.cpp
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2011-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 "PipesManager.h"
+
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+#include <mutex>
+
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+Pipe::Pipe(const std::string &name, int nMaxSize)
+{
+ m_buffer.Create(nMaxSize);
+ m_nRefCount = 1;
+ m_readEvent.Reset();
+ m_writeEvent.Set();
+ m_strPipeName = name;
+ m_bOpen = true;
+ m_bEof = false;
+ m_nOpenThreshold = PIPE_DEFAULT_MAX_SIZE / 2;
+ m_bReadyForRead = true; // open threshold disabled atm
+}
+
+Pipe::~Pipe() = default;
+
+void Pipe::SetOpenThreshold(int threshold)
+{
+ m_nOpenThreshold = threshold;
+}
+
+const std::string &Pipe::GetName()
+{
+ return m_strPipeName;
+}
+
+void Pipe::AddRef()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ m_nRefCount++;
+}
+
+void Pipe::DecRef()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ m_nRefCount--;
+}
+
+int Pipe::RefCount()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ return m_nRefCount;
+}
+
+void Pipe::SetEof()
+{
+ m_bEof = true;
+}
+
+bool Pipe::IsEof()
+{
+ return m_bEof;
+}
+
+bool Pipe::IsEmpty()
+{
+ return (m_buffer.getMaxReadSize() == 0);
+}
+
+void Pipe::Flush()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ if (!m_bOpen || !m_bReadyForRead || m_bEof)
+ {
+ return;
+ }
+ m_buffer.Clear();
+ CheckStatus();
+}
+
+int Pipe::Read(char *buf, int nMaxSize, int nWaitMillis)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+
+ if (!m_bOpen)
+ {
+ return -1;
+ }
+
+ while (!m_bReadyForRead && !m_bEof)
+ m_readEvent.Wait(100ms);
+
+ int nResult = 0;
+ if (!IsEmpty())
+ {
+ int nToRead = std::min(static_cast<int>(m_buffer.getMaxReadSize()), nMaxSize);
+ m_buffer.ReadData(buf, nToRead);
+ nResult = nToRead;
+ }
+ else if (m_bEof)
+ {
+ nResult = 0;
+ }
+ else
+ {
+ // we're leaving the guard - add ref to make sure we are not getting erased.
+ // at the moment we leave m_listeners unprotected which might be a problem in future
+ // but as long as we only have 1 listener attaching at startup and detaching on close we're fine
+ AddRef();
+ lock.unlock();
+
+ bool bHasData = false;
+ auto nMillisLeft = std::chrono::milliseconds(nWaitMillis);
+ if (nMillisLeft < 0ms)
+ nMillisLeft = 300000ms; // arbitrary. 5 min.
+
+ do
+ {
+ for (size_t l=0; l<m_listeners.size(); l++)
+ m_listeners[l]->OnPipeUnderFlow();
+
+ bHasData = m_readEvent.Wait(std::min(200ms, nMillisLeft));
+ nMillisLeft -= 200ms;
+ } while (!bHasData && nMillisLeft > 0ms && !m_bEof);
+
+ lock.lock();
+ DecRef();
+
+ if (!m_bOpen)
+ return -1;
+
+ if (bHasData)
+ {
+ int nToRead = std::min(static_cast<int>(m_buffer.getMaxReadSize()), nMaxSize);
+ m_buffer.ReadData(buf, nToRead);
+ nResult = nToRead;
+ }
+ }
+
+ CheckStatus();
+
+ return nResult;
+}
+
+bool Pipe::Write(const char *buf, int nSize, int nWaitMillis)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ if (!m_bOpen)
+ return false;
+ bool bOk = false;
+ int writeSize = m_buffer.getMaxWriteSize();
+ if (writeSize > nSize)
+ {
+ m_buffer.WriteData(buf, nSize);
+ bOk = true;
+ }
+ else
+ {
+ while ( (int)m_buffer.getMaxWriteSize() < nSize && m_bOpen )
+ {
+ lock.unlock();
+ for (size_t l=0; l<m_listeners.size(); l++)
+ m_listeners[l]->OnPipeOverFlow();
+
+ bool bClear = nWaitMillis < 0 ? m_writeEvent.Wait()
+ : m_writeEvent.Wait(std::chrono::milliseconds(nWaitMillis));
+ lock.lock();
+ if (bClear && (int)m_buffer.getMaxWriteSize() >= nSize)
+ {
+ m_buffer.WriteData(buf, nSize);
+ bOk = true;
+ break;
+ }
+
+ // FIXME: is this right? Shouldn't we see if the time limit has been reached?
+ if (nWaitMillis > 0)
+ break;
+ }
+ }
+
+ CheckStatus();
+
+ return bOk && m_bOpen;
+}
+
+void Pipe::CheckStatus()
+{
+ if (m_bEof)
+ {
+ m_writeEvent.Set();
+ m_readEvent.Set();
+ return;
+ }
+
+ if (m_buffer.getMaxWriteSize() == 0)
+ m_writeEvent.Reset();
+ else
+ m_writeEvent.Set();
+
+ if (m_buffer.getMaxReadSize() == 0)
+ m_readEvent.Reset();
+ else
+ {
+ if (!m_bReadyForRead && (int)m_buffer.getMaxReadSize() >= m_nOpenThreshold)
+ m_bReadyForRead = true;
+ m_readEvent.Set();
+ }
+}
+
+void Pipe::Close()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ m_bOpen = false;
+ m_readEvent.Set();
+ m_writeEvent.Set();
+}
+
+void Pipe::AddListener(IPipeListener *l)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ for (size_t i=0; i<m_listeners.size(); i++)
+ {
+ if (m_listeners[i] == l)
+ return;
+ }
+ m_listeners.push_back(l);
+}
+
+void Pipe::RemoveListener(IPipeListener *l)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ std::vector<XFILE::IPipeListener *>::iterator i = m_listeners.begin();
+ while(i != m_listeners.end())
+ {
+ if ( (*i) == l)
+ i = m_listeners.erase(i);
+ else
+ ++i;
+ }
+}
+
+int Pipe::GetAvailableRead()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ return m_buffer.getMaxReadSize();
+}
+
+PipesManager::~PipesManager() = default;
+
+PipesManager &PipesManager::GetInstance()
+{
+ static PipesManager instance;
+ return instance;
+}
+
+std::string PipesManager::GetUniquePipeName()
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ return StringUtils::Format("pipe://{}/", m_nGenIdHelper++);
+}
+
+XFILE::Pipe *PipesManager::CreatePipe(const std::string &name, int nMaxPipeSize)
+{
+ std::string pName = name;
+ if (pName.empty())
+ pName = GetUniquePipeName();
+
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ if (m_pipes.find(pName) != m_pipes.end())
+ return NULL;
+
+ XFILE::Pipe *p = new XFILE::Pipe(pName, nMaxPipeSize);
+ m_pipes[pName] = p;
+ return p;
+}
+
+XFILE::Pipe *PipesManager::OpenPipe(const std::string &name)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ if (m_pipes.find(name) == m_pipes.end())
+ return NULL;
+ m_pipes[name]->AddRef();
+ return m_pipes[name];
+}
+
+void PipesManager::ClosePipe(XFILE::Pipe *pipe)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ if (!pipe)
+ return ;
+
+ pipe->DecRef();
+ if (pipe->RefCount() == 0)
+ {
+ pipe->Close();
+ m_pipes.erase(pipe->GetName());
+ delete pipe;
+ }
+}
+
+bool PipesManager::Exists(const std::string &name)
+{
+ std::unique_lock<CCriticalSection> lock(m_lock);
+ return (m_pipes.find(name) != m_pipes.end());
+}
+
diff --git a/xbmc/filesystem/PipesManager.h b/xbmc/filesystem/PipesManager.h
new file mode 100644
index 0000000..100f7d8
--- /dev/null
+++ b/xbmc/filesystem/PipesManager.h
@@ -0,0 +1,120 @@
+/*
+ * Many concepts and protocol are taken from
+ * the Boxee project. http://www.boxee.tv
+ *
+ * Copyright (C) 2011-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 "utils/RingBuffer.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+#define PIPE_DEFAULT_MAX_SIZE (6 * 1024 * 1024)
+
+namespace XFILE
+{
+
+class IPipeListener
+{
+public:
+ virtual ~IPipeListener() = default;
+ virtual void OnPipeOverFlow() = 0;
+ virtual void OnPipeUnderFlow() = 0;
+};
+
+class Pipe
+ {
+ public:
+ Pipe(const std::string &name, int nMaxSize = PIPE_DEFAULT_MAX_SIZE );
+ virtual ~Pipe();
+ const std::string &GetName();
+
+ void AddRef();
+ void DecRef(); // a pipe does NOT delete itself with ref-count 0.
+ int RefCount();
+
+ bool IsEmpty();
+
+ /**
+ * Read into the buffer from the Pipe the num of bytes asked for
+ * blocking forever (which happens to be 5 minutes in this case).
+ *
+ * In the case where nWaitMillis is provided block for that number
+ * of milliseconds instead.
+ */
+ int Read(char *buf, int nMaxSize, int nWaitMillis = -1);
+
+ /**
+ * Write into the Pipe from the buffer the num of bytes asked for
+ * blocking forever.
+ *
+ * In the case where nWaitMillis is provided block for that number
+ * of milliseconds instead.
+ */
+ bool Write(const char *buf, int nSize, int nWaitMillis = -1);
+
+ void Flush();
+
+ void CheckStatus();
+ void Close();
+
+ void AddListener(IPipeListener *l);
+ void RemoveListener(IPipeListener *l);
+
+ void SetEof();
+ bool IsEof();
+
+ int GetAvailableRead();
+ void SetOpenThreshold(int threshold);
+
+ protected:
+
+ bool m_bOpen;
+ bool m_bReadyForRead;
+
+ bool m_bEof;
+ CRingBuffer m_buffer;
+ std::string m_strPipeName;
+ int m_nRefCount;
+ int m_nOpenThreshold;
+
+ CEvent m_readEvent;
+ CEvent m_writeEvent;
+
+ std::vector<XFILE::IPipeListener *> m_listeners;
+
+ CCriticalSection m_lock;
+ };
+
+
+class PipesManager
+{
+public:
+ virtual ~PipesManager();
+ static PipesManager &GetInstance();
+
+ std::string GetUniquePipeName();
+ XFILE::Pipe *CreatePipe(const std::string &name="", int nMaxPipeSize = PIPE_DEFAULT_MAX_SIZE);
+ XFILE::Pipe *OpenPipe(const std::string &name);
+ void ClosePipe(XFILE::Pipe *pipe);
+ bool Exists(const std::string &name);
+
+protected:
+ int m_nGenIdHelper = 1;
+ std::map<std::string, XFILE::Pipe *> m_pipes;
+
+ CCriticalSection m_lock;
+};
+
+}
+
diff --git a/xbmc/filesystem/PlaylistDirectory.cpp b/xbmc/filesystem/PlaylistDirectory.cpp
new file mode 100644
index 0000000..8be88dc
--- /dev/null
+++ b/xbmc/filesystem/PlaylistDirectory.cpp
@@ -0,0 +1,47 @@
+/*
+ * 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 "PlaylistDirectory.h"
+
+#include "FileItem.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "playlists/PlayList.h"
+
+using namespace XFILE;
+
+CPlaylistDirectory::CPlaylistDirectory() = default;
+
+CPlaylistDirectory::~CPlaylistDirectory() = default;
+
+bool CPlaylistDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ PLAYLIST::Id playlistId = PLAYLIST::TYPE_NONE;
+ if (url.IsProtocol("playlistmusic"))
+ playlistId = PLAYLIST::TYPE_MUSIC;
+ else if (url.IsProtocol("playlistvideo"))
+ playlistId = PLAYLIST::TYPE_VIDEO;
+
+ if (playlistId == PLAYLIST::TYPE_NONE)
+ return false;
+
+ const PLAYLIST::CPlayList& playlist = CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId);
+ items.Reserve(playlist.size());
+
+ for (int i = 0; i < playlist.size(); ++i)
+ {
+ CFileItemPtr item = playlist[i];
+ item->SetProperty("playlistposition", i);
+ item->SetProperty("playlisttype", playlistId);
+ //item->m_iprogramCount = i; // the programCount is set as items are added!
+ items.Add(item);
+ }
+
+ return true;
+}
diff --git a/xbmc/filesystem/PlaylistDirectory.h b/xbmc/filesystem/PlaylistDirectory.h
new file mode 100644
index 0000000..416c0ea
--- /dev/null
+++ b/xbmc/filesystem/PlaylistDirectory.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "IDirectory.h"
+
+namespace XFILE
+{
+ class CPlaylistDirectory : public IDirectory
+ {
+ public:
+ CPlaylistDirectory(void);
+ ~CPlaylistDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool AllowAll() const override { return true; }
+ };
+}
diff --git a/xbmc/filesystem/PlaylistFileDirectory.cpp b/xbmc/filesystem/PlaylistFileDirectory.cpp
new file mode 100644
index 0000000..2bdb243
--- /dev/null
+++ b/xbmc/filesystem/PlaylistFileDirectory.cpp
@@ -0,0 +1,67 @@
+/*
+ * 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 "PlaylistFileDirectory.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "filesystem/File.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+
+using namespace PLAYLIST;
+
+namespace XFILE
+{
+ CPlaylistFileDirectory::CPlaylistFileDirectory() = default;
+
+ CPlaylistFileDirectory::~CPlaylistFileDirectory() = default;
+
+ bool CPlaylistFileDirectory::GetDirectory(const CURL& url, CFileItemList& items)
+ {
+ const std::string pathToUrl = url.Get();
+ std::unique_ptr<CPlayList> pPlayList (CPlayListFactory::Create(pathToUrl));
+ if (nullptr != pPlayList)
+ {
+ // load it
+ if (!pPlayList->Load(pathToUrl))
+ return false; //hmmm unable to load playlist?
+
+ CPlayList playlist = *pPlayList;
+ // convert playlist items to songs
+ for (int i = 0; i < playlist.size(); ++i)
+ {
+ CFileItemPtr item = playlist[i];
+ item->m_iprogramCount = i; // hack for playlist order
+ items.Add(item);
+ }
+ }
+ return true;
+ }
+
+ bool CPlaylistFileDirectory::ContainsFiles(const CURL& url)
+ {
+ const std::string pathToUrl = url.Get();
+ std::unique_ptr<CPlayList> pPlayList (CPlayListFactory::Create(pathToUrl));
+ if (nullptr != pPlayList)
+ {
+ // load it
+ if (!pPlayList->Load(pathToUrl))
+ return false; //hmmm unable to load playlist?
+
+ return (pPlayList->size() > 1);
+ }
+ return false;
+ }
+
+ bool CPlaylistFileDirectory::Remove(const CURL& url)
+ {
+ return XFILE::CFile::Delete(url);
+ }
+}
+
diff --git a/xbmc/filesystem/PlaylistFileDirectory.h b/xbmc/filesystem/PlaylistFileDirectory.h
new file mode 100644
index 0000000..9f956ce
--- /dev/null
+++ b/xbmc/filesystem/PlaylistFileDirectory.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "IFileDirectory.h"
+
+namespace XFILE
+{
+ class CPlaylistFileDirectory : public IFileDirectory
+ {
+ public:
+ CPlaylistFileDirectory();
+ ~CPlaylistFileDirectory() override;
+ bool GetDirectory(const CURL& url, CFileItemList& items) override;
+ bool ContainsFiles(const CURL& url) override;
+ bool Remove(const CURL& url) override;
+ bool AllowAll() const override { return true; }
+ };
+}
diff --git a/xbmc/filesystem/PluginDirectory.cpp b/xbmc/filesystem/PluginDirectory.cpp
new file mode 100644
index 0000000..8273886
--- /dev/null
+++ b/xbmc/filesystem/PluginDirectory.cpp
@@ -0,0 +1,548 @@
+/*
+ * 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 "PluginDirectory.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddon.h"
+#include "addons/PluginSource.h"
+#include "addons/addoninfo/AddonType.h"
+#include "interfaces/generic/RunningScriptObserver.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <mutex>
+
+using namespace XFILE;
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+
+namespace
+{
+const unsigned int maxPluginResolutions = 5;
+
+/*!
+ \brief Get the plugin path from a CFileItem.
+
+ \param item CFileItem where to get the path.
+ \return The plugin path if found otherwise an empty string.
+*/
+std::string GetOriginalPluginPath(const CFileItem& item)
+{
+ std::string currentPath = item.GetPath();
+ if (URIUtils::IsPlugin(currentPath))
+ return currentPath;
+
+ currentPath = item.GetDynPath();
+ if (URIUtils::IsPlugin(currentPath))
+ return currentPath;
+
+ return std::string();
+}
+} // unnamed namespace
+
+CPluginDirectory::CPluginDirectory()
+ : m_listItems(new CFileItemList), m_fileResult(new CFileItem), m_cancelled(false)
+
+{
+}
+
+CPluginDirectory::~CPluginDirectory(void)
+{
+}
+
+bool CPluginDirectory::StartScript(const std::string& strPath, bool resume)
+{
+ CURL url(strPath);
+
+ ADDON::AddonPtr addon;
+ // try the plugin type first, and if not found, try an unknown type
+ if (!CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), addon, AddonType::PLUGIN,
+ OnlyEnabled::CHOICE_YES) &&
+ !CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), addon, AddonType::UNKNOWN,
+ OnlyEnabled::CHOICE_YES) &&
+ !CAddonInstaller::GetInstance().InstallModal(url.GetHostName(), addon,
+ InstallModalPrompt::CHOICE_YES))
+ {
+ CLog::Log(LOGERROR, "Unable to find plugin {}", url.GetHostName());
+ return false;
+ }
+
+ // clear out our status variables
+ m_fileResult->Reset();
+ m_listItems->Clear();
+ m_listItems->SetPath(strPath);
+ m_listItems->SetLabel(addon->Name());
+ m_cancelled = false;
+ m_success = false;
+ m_totalItems = 0;
+
+ // run the script
+ return RunScript(this, addon, strPath, resume);
+}
+
+bool CPluginDirectory::GetResolvedPluginResult(CFileItem& resultItem)
+{
+ std::string lastResolvedPath;
+ if (resultItem.HasProperty("ForceResolvePlugin") &&
+ resultItem.GetProperty("ForceResolvePlugin").asBoolean())
+ {
+ // ensures that a plugin have the callback to resolve the paths in any case
+ // also when the same items in the playlist are played more times
+ lastResolvedPath = GetOriginalPluginPath(resultItem);
+ }
+ else
+ {
+ lastResolvedPath = resultItem.GetDynPath();
+ }
+
+ if (!lastResolvedPath.empty())
+ {
+ // we try to resolve recursively up to n. (maxPluginResolutions) nested plugin paths
+ // to avoid deadlocks (plugin:// paths can resolve to plugin:// paths)
+ for (unsigned int i = 0; URIUtils::IsPlugin(lastResolvedPath) && i < maxPluginResolutions; ++i)
+ {
+ bool resume = resultItem.GetStartOffset() == STARTOFFSET_RESUME;
+
+ // we modify the item so that it becomes a real URL
+ if (!XFILE::CPluginDirectory::GetPluginResult(lastResolvedPath, resultItem, resume) ||
+ resultItem.GetDynPath() ==
+ resultItem.GetPath()) // GetPluginResult resolved to an empty path
+ {
+ return false;
+ }
+ lastResolvedPath = resultItem.GetDynPath();
+ }
+ // if after the maximum allowed resolution attempts the item is still a plugin just return, it isn't playable
+ if (URIUtils::IsPlugin(resultItem.GetDynPath()))
+ return false;
+ }
+
+ return true;
+}
+
+bool CPluginDirectory::GetPluginResult(const std::string& strPath, CFileItem &resultItem, bool resume)
+{
+ CURL url(strPath);
+ CPluginDirectory newDir;
+
+ bool success = newDir.StartScript(strPath, resume);
+
+ if (success)
+ { // update the play path and metadata, saving the old one as needed
+ if (!resultItem.HasProperty("original_listitem_url"))
+ resultItem.SetProperty("original_listitem_url", resultItem.GetPath());
+ resultItem.SetDynPath(newDir.m_fileResult->GetPath());
+ resultItem.SetMimeType(newDir.m_fileResult->GetMimeType());
+ resultItem.SetContentLookup(newDir.m_fileResult->ContentLookup());
+
+ if (resultItem.HasProperty("OverrideInfotag") &&
+ resultItem.GetProperty("OverrideInfotag").asBoolean())
+ resultItem.UpdateInfo(*newDir.m_fileResult);
+ else
+ resultItem.MergeInfo(*newDir.m_fileResult);
+
+ if (newDir.m_fileResult->HasVideoInfoTag() && newDir.m_fileResult->GetVideoInfoTag()->GetResumePoint().IsSet())
+ resultItem.SetStartOffset(
+ STARTOFFSET_RESUME); // resume point set in the resume item, so force resume
+ }
+
+ return success;
+}
+
+bool CPluginDirectory::AddItem(int handle, const CFileItem *item, int totalItems)
+{
+ std::unique_lock<CCriticalSection> lock(GetScriptsLock());
+ CPluginDirectory* dir = GetScriptFromHandle(handle);
+ if (!dir)
+ return false;
+
+ CFileItemPtr pItem(new CFileItem(*item));
+ dir->m_listItems->Add(pItem);
+ dir->m_totalItems = totalItems;
+
+ return !dir->m_cancelled;
+}
+
+bool CPluginDirectory::AddItems(int handle, const CFileItemList *items, int totalItems)
+{
+ std::unique_lock<CCriticalSection> lock(GetScriptsLock());
+ CPluginDirectory* dir = GetScriptFromHandle(handle);
+ if (!dir)
+ return false;
+
+ CFileItemList pItemList;
+ pItemList.Copy(*items);
+ dir->m_listItems->Append(pItemList);
+ dir->m_totalItems = totalItems;
+
+ return !dir->m_cancelled;
+}
+
+void CPluginDirectory::EndOfDirectory(int handle, bool success, bool replaceListing, bool cacheToDisc)
+{
+ std::unique_lock<CCriticalSection> lock(GetScriptsLock());
+ CPluginDirectory* dir = GetScriptFromHandle(handle);
+ if (!dir)
+ return;
+
+ // set cache to disc
+ dir->m_listItems->SetCacheToDisc(cacheToDisc ? CFileItemList::CACHE_IF_SLOW : CFileItemList::CACHE_NEVER);
+
+ dir->m_success = success;
+ dir->m_listItems->SetReplaceListing(replaceListing);
+
+ if (!dir->m_listItems->HasSortDetails())
+ dir->m_listItems->AddSortMethod(SortByNone, 552, LABEL_MASKS("%L", "%D"));
+
+ // set the event to mark that we're done
+ dir->SetDone();
+}
+
+void CPluginDirectory::AddSortMethod(int handle, SORT_METHOD sortMethod, const std::string &labelMask, const std::string &label2Mask)
+{
+ std::unique_lock<CCriticalSection> lock(GetScriptsLock());
+ CPluginDirectory* dir = GetScriptFromHandle(handle);
+ if (!dir)
+ return;
+
+ //! @todo Add all sort methods and fix which labels go on the right or left
+ switch(sortMethod)
+ {
+ case SORT_METHOD_LABEL:
+ case SORT_METHOD_LABEL_IGNORE_THE:
+ {
+ dir->m_listItems->AddSortMethod(SortByLabel, 551, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ break;
+ }
+ case SORT_METHOD_TITLE:
+ case SORT_METHOD_TITLE_IGNORE_THE:
+ {
+ dir->m_listItems->AddSortMethod(SortByTitle, 556, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ break;
+ }
+ case SORT_METHOD_ARTIST:
+ case SORT_METHOD_ARTIST_IGNORE_THE:
+ {
+ dir->m_listItems->AddSortMethod(SortByArtist, 557, LABEL_MASKS(labelMask, "%A", labelMask, "%A"), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ break;
+ }
+ case SORT_METHOD_ALBUM:
+ case SORT_METHOD_ALBUM_IGNORE_THE:
+ {
+ dir->m_listItems->AddSortMethod(SortByAlbum, 558, LABEL_MASKS(labelMask, "%B", labelMask, "%B"), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ break;
+ }
+ case SORT_METHOD_DATE:
+ {
+ dir->m_listItems->AddSortMethod(SortByDate, 552, LABEL_MASKS(labelMask, "%J", labelMask, "%J"));
+ break;
+ }
+ case SORT_METHOD_BITRATE:
+ {
+ dir->m_listItems->AddSortMethod(SortByBitrate, 623, LABEL_MASKS(labelMask, "%X", labelMask, "%X"));
+ break;
+ }
+ case SORT_METHOD_SIZE:
+ {
+ dir->m_listItems->AddSortMethod(SortBySize, 553, LABEL_MASKS(labelMask, "%I", labelMask, "%I"));
+ break;
+ }
+ case SORT_METHOD_FILE:
+ {
+ dir->m_listItems->AddSortMethod(SortByFile, 561, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask));
+ break;
+ }
+ case SORT_METHOD_TRACKNUM:
+ {
+ dir->m_listItems->AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask));
+ break;
+ }
+ case SORT_METHOD_DURATION:
+ case SORT_METHOD_VIDEO_RUNTIME:
+ {
+ dir->m_listItems->AddSortMethod(SortByTime, 180, LABEL_MASKS(labelMask, "%D", labelMask, "%D"));
+ break;
+ }
+ case SORT_METHOD_VIDEO_RATING:
+ case SORT_METHOD_SONG_RATING:
+ {
+ dir->m_listItems->AddSortMethod(SortByRating, 563, LABEL_MASKS(labelMask, "%R", labelMask, "%R"));
+ break;
+ }
+ case SORT_METHOD_YEAR:
+ {
+ dir->m_listItems->AddSortMethod(SortByYear, 562, LABEL_MASKS(labelMask, "%Y", labelMask, "%Y"));
+ break;
+ }
+ case SORT_METHOD_GENRE:
+ {
+ dir->m_listItems->AddSortMethod(SortByGenre, 515, LABEL_MASKS(labelMask, "%G", labelMask, "%G"));
+ break;
+ }
+ case SORT_METHOD_COUNTRY:
+ {
+ dir->m_listItems->AddSortMethod(SortByCountry, 574, LABEL_MASKS(labelMask, "%G", labelMask, "%G"));
+ break;
+ }
+ case SORT_METHOD_VIDEO_TITLE:
+ {
+ dir->m_listItems->AddSortMethod(SortByTitle, 369, LABEL_MASKS(labelMask, "%M", labelMask, "%M"));
+ break;
+ }
+ case SORT_METHOD_VIDEO_SORT_TITLE:
+ case SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE:
+ {
+ dir->m_listItems->AddSortMethod(SortBySortTitle, 556, LABEL_MASKS(labelMask, "%M", labelMask, "%M"), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ break;
+ }
+ case SORT_METHOD_VIDEO_ORIGINAL_TITLE:
+ case SORT_METHOD_VIDEO_ORIGINAL_TITLE_IGNORE_THE:
+ {
+ dir->m_listItems->AddSortMethod(
+ SortByOriginalTitle, 20376, LABEL_MASKS(labelMask, "%M", labelMask, "%M"),
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)
+ ? SortAttributeIgnoreArticle
+ : SortAttributeNone);
+ break;
+ }
+ case SORT_METHOD_MPAA_RATING:
+ {
+ dir->m_listItems->AddSortMethod(SortByMPAA, 20074, LABEL_MASKS(labelMask, "%O", labelMask, "%O"));
+ break;
+ }
+ case SORT_METHOD_STUDIO:
+ case SORT_METHOD_STUDIO_IGNORE_THE:
+ {
+ dir->m_listItems->AddSortMethod(SortByStudio, 572, LABEL_MASKS(labelMask, "%U", labelMask, "%U"), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ break;
+ }
+ case SORT_METHOD_PROGRAM_COUNT:
+ {
+ dir->m_listItems->AddSortMethod(SortByProgramCount, 567, LABEL_MASKS(labelMask, "%C", labelMask, "%C"));
+ break;
+ }
+ case SORT_METHOD_UNSORTED:
+ {
+ dir->m_listItems->AddSortMethod(SortByNone, 571, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask));
+ break;
+ }
+ case SORT_METHOD_NONE:
+ {
+ dir->m_listItems->AddSortMethod(SortByNone, 552, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask));
+ break;
+ }
+ case SORT_METHOD_DRIVE_TYPE:
+ {
+ dir->m_listItems->AddSortMethod(SortByDriveType, 564, LABEL_MASKS()); // Preformatted
+ break;
+ }
+ case SORT_METHOD_PLAYLIST_ORDER:
+ {
+ std::string strTrack=CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ dir->m_listItems->AddSortMethod(SortByPlaylistOrder, 559, LABEL_MASKS(strTrack, "%D"));
+ break;
+ }
+ case SORT_METHOD_EPISODE:
+ {
+ dir->m_listItems->AddSortMethod(SortByEpisodeNumber, 20359, LABEL_MASKS(labelMask, "%R", labelMask, "%R"));
+ break;
+ }
+ case SORT_METHOD_PRODUCTIONCODE:
+ {
+ //dir->m_listItems.AddSortMethod(SORT_METHOD_PRODUCTIONCODE,20368,LABEL_MASKS("%E. %T","%P", "%E. %T","%P"));
+ dir->m_listItems->AddSortMethod(SortByProductionCode, 20368, LABEL_MASKS(labelMask, "%P", labelMask, "%P"));
+ break;
+ }
+ case SORT_METHOD_LISTENERS:
+ {
+ dir->m_listItems->AddSortMethod(SortByListeners, 20455, LABEL_MASKS(labelMask, "%W"));
+ break;
+ }
+ case SORT_METHOD_DATEADDED:
+ {
+ dir->m_listItems->AddSortMethod(SortByDateAdded, 570, LABEL_MASKS(labelMask, "%a"));
+ break;
+ }
+ case SORT_METHOD_FULLPATH:
+ {
+ dir->m_listItems->AddSortMethod(SortByPath, 573, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask));
+ break;
+ }
+ case SORT_METHOD_LABEL_IGNORE_FOLDERS:
+ {
+ dir->m_listItems->AddSortMethod(SortByLabel, SortAttributeIgnoreFolders, 551, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask));
+ break;
+ }
+ case SORT_METHOD_LASTPLAYED:
+ {
+ dir->m_listItems->AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS(labelMask, "%G"));
+ break;
+ }
+ case SORT_METHOD_PLAYCOUNT:
+ {
+ dir->m_listItems->AddSortMethod(SortByPlaycount, 567, LABEL_MASKS(labelMask, "%V", labelMask, "%V"));
+ break;
+ }
+ case SORT_METHOD_CHANNEL:
+ {
+ dir->m_listItems->AddSortMethod(SortByChannel, 19029, LABEL_MASKS(labelMask, label2Mask, labelMask, label2Mask));
+ break;
+ }
+
+ default:
+ break;
+ }
+}
+
+bool CPluginDirectory::GetDirectory(const CURL& url, CFileItemList& items)
+{
+ const std::string pathToUrl(url.Get());
+ bool success = StartScript(pathToUrl, false);
+
+ // append the items to the list
+ items.Assign(*m_listItems, true); // true to keep the current items
+ m_listItems->Clear();
+ return success;
+}
+
+bool CPluginDirectory::RunScriptWithParams(const std::string& strPath, bool resume)
+{
+ CURL url(strPath);
+ if (url.GetHostName().empty()) // called with no script - should never happen
+ return false;
+
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), addon, AddonType::PLUGIN,
+ OnlyEnabled::CHOICE_YES) &&
+ !CAddonInstaller::GetInstance().InstallModal(url.GetHostName(), addon,
+ InstallModalPrompt::CHOICE_YES))
+ {
+ CLog::Log(LOGERROR, "Unable to find plugin {}", url.GetHostName());
+ return false;
+ }
+
+ return ExecuteScript(addon, strPath, resume) >= 0;
+}
+
+void CPluginDirectory::SetResolvedUrl(int handle, bool success, const CFileItem *resultItem)
+{
+ std::unique_lock<CCriticalSection> lock(GetScriptsLock());
+ CPluginDirectory* dir = GetScriptFromHandle(handle);
+ if (!dir)
+ return;
+
+ dir->m_success = success;
+ *dir->m_fileResult = *resultItem;
+
+ // set the event to mark that we're done
+ dir->SetDone();
+}
+
+std::string CPluginDirectory::GetSetting(int handle, const std::string &strID)
+{
+ std::unique_lock<CCriticalSection> lock(GetScriptsLock());
+ CPluginDirectory* dir = GetScriptFromHandle(handle);
+ if (dir && dir->GetAddon())
+ return dir->GetAddon()->GetSetting(strID);
+ else
+ return "";
+}
+
+void CPluginDirectory::SetSetting(int handle, const std::string &strID, const std::string &value)
+{
+ std::unique_lock<CCriticalSection> lock(GetScriptsLock());
+ CPluginDirectory* dir = GetScriptFromHandle(handle);
+ if (dir && dir->GetAddon())
+ dir->GetAddon()->UpdateSetting(strID, value);
+}
+
+void CPluginDirectory::SetContent(int handle, const std::string &strContent)
+{
+ std::unique_lock<CCriticalSection> lock(GetScriptsLock());
+ CPluginDirectory* dir = GetScriptFromHandle(handle);
+ if (dir)
+ dir->m_listItems->SetContent(strContent);
+}
+
+void CPluginDirectory::SetProperty(int handle, const std::string &strProperty, const std::string &strValue)
+{
+ std::unique_lock<CCriticalSection> lock(GetScriptsLock());
+ CPluginDirectory* dir = GetScriptFromHandle(handle);
+ if (!dir)
+ return;
+ if (strProperty == "fanart_image")
+ dir->m_listItems->SetArt("fanart", strValue);
+ else
+ dir->m_listItems->SetProperty(strProperty, strValue);
+}
+
+void CPluginDirectory::CancelDirectory()
+{
+ m_cancelled = true;
+}
+
+float CPluginDirectory::GetProgress() const
+{
+ if (m_totalItems > 0)
+ return (m_listItems->Size() * 100.0f) / m_totalItems;
+ return 0.0f;
+}
+
+bool CPluginDirectory::IsMediaLibraryScanningAllowed(const std::string& content, const std::string& strPath)
+{
+ if (content.empty())
+ return false;
+
+ CURL url(strPath);
+ if (url.GetHostName().empty())
+ return false;
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), addon, AddonType::PLUGIN,
+ OnlyEnabled::CHOICE_YES))
+ {
+ CLog::Log(LOGERROR, "Unable to find plugin {}", url.GetHostName());
+ return false;
+ }
+ CPluginSource* plugin = dynamic_cast<CPluginSource*>(addon.get());
+ if (!plugin)
+ return false;
+
+ auto& paths = plugin->MediaLibraryScanPaths();
+ if (paths.empty())
+ return false;
+ auto it = paths.find(content);
+ if (it == paths.end())
+ return false;
+ const std::string& path = url.GetFileName();
+ for (const auto& p : it->second)
+ if (p.empty() || p == "/" || URIUtils::PathHasParent(path, p))
+ return true;
+ return false;
+}
+
+bool CPluginDirectory::CheckExists(const std::string& content, const std::string& strPath)
+{
+ if (!IsMediaLibraryScanningAllowed(content, strPath))
+ return false;
+ // call the plugin at specified path with option "kodi_action=check_exists"
+ // url exists if the plugin returns any fileitem with setResolvedUrl
+ CURL url(strPath);
+ url.SetOption("kodi_action", "check_exists");
+ CFileItem item;
+ return CPluginDirectory::GetPluginResult(url.Get(), item, false);
+}
diff --git a/xbmc/filesystem/PluginDirectory.h b/xbmc/filesystem/PluginDirectory.h
new file mode 100644
index 0000000..ebcfd13
--- /dev/null
+++ b/xbmc/filesystem/PluginDirectory.h
@@ -0,0 +1,89 @@
+/*
+ * 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 "IDirectory.h"
+#include "SortFileItem.h"
+#include "interfaces/generic/RunningScriptsHandler.h"
+#include "threads/Event.h"
+
+#include <atomic>
+#include <memory>
+#include <string>
+
+class CURL;
+class CFileItem;
+class CFileItemList;
+
+namespace XFILE
+{
+
+class CPluginDirectory : public IDirectory, public CRunningScriptsHandler<CPluginDirectory>
+{
+public:
+ CPluginDirectory();
+ ~CPluginDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList& items) override;
+ bool AllowAll() const override { return true; }
+ bool Exists(const CURL& url) override { return true; }
+ float GetProgress() const override;
+ void CancelDirectory() override;
+ static bool RunScriptWithParams(const std::string& strPath, bool resume);
+
+ /*! \brief Get a reproducible CFileItem by trying to recursively resolve the plugin paths
+ up to a maximum allowed limit. If no plugin paths exist it will be ignored.
+ \param resultItem the CFileItem with plugin paths to be resolved.
+ \return false if the plugin path cannot be resolved, true otherwise.
+ */
+ static bool GetResolvedPluginResult(CFileItem& resultItem);
+ static bool GetPluginResult(const std::string& strPath, CFileItem &resultItem, bool resume);
+
+ /*! \brief Check whether a plugin supports media library scanning.
+ \param content content type - movies, tvshows, musicvideos.
+ \param strPath full plugin url.
+ \return true if scanning at specified url is allowed, false otherwise.
+ */
+ static bool IsMediaLibraryScanningAllowed(const std::string& content, const std::string& strPath);
+
+ /*! \brief Check whether a plugin url exists by calling the plugin and checking result.
+ Applies only to plugins that support media library scanning.
+ \param content content type - movies, tvshows, musicvideos.
+ \param strPath full plugin url.
+ \return true if the plugin supports scanning and specified url exists, false otherwise.
+ */
+ static bool CheckExists(const std::string& content, const std::string& strPath);
+
+ // callbacks from python
+ static bool AddItem(int handle, const CFileItem *item, int totalItems);
+ static bool AddItems(int handle, const CFileItemList *items, int totalItems);
+ static void EndOfDirectory(int handle, bool success, bool replaceListing, bool cacheToDisc);
+ static void AddSortMethod(int handle, SORT_METHOD sortMethod, const std::string &labelMask, const std::string &label2Mask);
+ static std::string GetSetting(int handle, const std::string &key);
+ static void SetSetting(int handle, const std::string &key, const std::string &value);
+ static void SetContent(int handle, const std::string &strContent);
+ static void SetProperty(int handle, const std::string &strProperty, const std::string &strValue);
+ static void SetResolvedUrl(int handle, bool success, const CFileItem* resultItem);
+ static void SetLabel2(int handle, const std::string& ident);
+
+protected:
+ // implementations of CRunningScriptsHandler / CScriptRunner
+ bool IsSuccessful() const override { return m_success; }
+ bool IsCancelled() const override { return m_cancelled; }
+
+private:
+ bool StartScript(const std::string& strPath, bool resume);
+
+ std::unique_ptr<CFileItemList> m_listItems;
+ std::unique_ptr<CFileItem> m_fileResult;
+
+ std::atomic<bool> m_cancelled;
+ bool m_success = false; // set by script in EndOfDirectory
+ int m_totalItems = 0; // set by script in AddDirectoryItem
+};
+}
diff --git a/xbmc/filesystem/PluginFile.cpp b/xbmc/filesystem/PluginFile.cpp
new file mode 100644
index 0000000..63bf827
--- /dev/null
+++ b/xbmc/filesystem/PluginFile.cpp
@@ -0,0 +1,59 @@
+/*
+ * 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 "PluginFile.h"
+
+#include "URL.h"
+
+using namespace XFILE;
+
+CPluginFile::CPluginFile(void) : COverrideFile(false)
+{
+}
+
+CPluginFile::~CPluginFile(void) = default;
+
+bool CPluginFile::Open(const CURL& url)
+{
+ return false;
+}
+
+bool CPluginFile::Exists(const CURL& url)
+{
+ return true;
+}
+
+int CPluginFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ return -1;
+}
+
+int CPluginFile::Stat(struct __stat64* buffer)
+{
+ return -1;
+}
+
+bool CPluginFile::OpenForWrite(const CURL& url, bool bOverWrite)
+{
+ return false;
+}
+
+bool CPluginFile::Delete(const CURL& url)
+{
+ return false;
+}
+
+bool CPluginFile::Rename(const CURL& url, const CURL& urlnew)
+{
+ return false;
+}
+
+std::string CPluginFile::TranslatePath(const CURL& url)
+{
+ return url.Get();
+}
diff --git a/xbmc/filesystem/PluginFile.h b/xbmc/filesystem/PluginFile.h
new file mode 100644
index 0000000..2044fd9
--- /dev/null
+++ b/xbmc/filesystem/PluginFile.h
@@ -0,0 +1,31 @@
+/*
+ * 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 "filesystem/OverrideFile.h"
+
+namespace XFILE
+{
+class CPluginFile : public COverrideFile
+{
+public:
+ CPluginFile(void);
+ ~CPluginFile(void) override;
+ 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;
+
+protected:
+ std::string TranslatePath(const CURL& url) override;
+};
+} // namespace XFILE
diff --git a/xbmc/filesystem/RSSDirectory.cpp b/xbmc/filesystem/RSSDirectory.cpp
new file mode 100644
index 0000000..a176381
--- /dev/null
+++ b/xbmc/filesystem/RSSDirectory.cpp
@@ -0,0 +1,623 @@
+/*
+ * 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 "RSSDirectory.h"
+
+#include "CurlFile.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/HTMLUtil.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <climits>
+#include <mutex>
+#include <utility>
+
+using namespace XFILE;
+using namespace MUSIC_INFO;
+
+namespace {
+
+ struct SResource
+ {
+ std::string tag;
+ std::string path;
+ std::string mime;
+ std::string lang;
+ int width = 0;
+ int height = 0;
+ int bitrate = 0;
+ int duration = 0;
+ int64_t size = 0;
+ };
+
+ typedef std::vector<SResource> SResources;
+
+}
+
+std::map<std::string,CDateTime> CRSSDirectory::m_cache;
+CCriticalSection CRSSDirectory::m_section;
+
+CRSSDirectory::CRSSDirectory() = default;
+
+CRSSDirectory::~CRSSDirectory() = default;
+
+bool CRSSDirectory::ContainsFiles(const CURL& url)
+{
+ CFileItemList items;
+ if(!GetDirectory(url, items))
+ return false;
+
+ return items.Size() > 0;
+}
+
+static bool IsPathToMedia(const std::string& strPath )
+{
+ return URIUtils::HasExtension(strPath,
+ CServiceBroker::GetFileExtensionProvider().GetVideoExtensions() + '|' +
+ CServiceBroker::GetFileExtensionProvider().GetMusicExtensions() + '|' +
+ CServiceBroker::GetFileExtensionProvider().GetPictureExtensions());
+}
+
+static bool IsPathToThumbnail(const std::string& strPath )
+{
+ // Currently just check if this is an image, maybe we will add some
+ // other checks later
+ return URIUtils::HasExtension(strPath,
+ CServiceBroker::GetFileExtensionProvider().GetPictureExtensions());
+}
+
+static time_t ParseDate(const std::string & strDate)
+{
+ struct tm pubDate = {};
+ //! @todo Handle time zone
+ strptime(strDate.c_str(), "%a, %d %b %Y %H:%M:%S", &pubDate);
+ // Check the difference between the time of last check and time of the item
+ return mktime(&pubDate);
+}
+static void ParseItem(CFileItem* item, SResources& resources, TiXmlElement* root, const std::string& path);
+
+static std::string GetValue(TiXmlElement *element)
+{
+ if (element && !element->NoChildren())
+ return element->FirstChild()->ValueStr();
+ return "";
+}
+
+static void ParseItemMRSS(CFileItem* item, SResources& resources, TiXmlElement* item_child, const std::string& name, const std::string& xmlns, const std::string& path)
+{
+ CVideoInfoTag* vtag = item->GetVideoInfoTag();
+ std::string text = GetValue(item_child);
+
+ if(name == "content")
+ {
+ SResource res;
+ res.tag = "media:content";
+ res.mime = XMLUtils::GetAttribute(item_child, "type");
+ res.path = XMLUtils::GetAttribute(item_child, "url");
+ item_child->Attribute("width", &res.width);
+ item_child->Attribute("height", &res.height);
+ item_child->Attribute("bitrate", &res.bitrate);
+ item_child->Attribute("duration", &res.duration);
+ if(item_child->Attribute("fileSize"))
+ res.size = std::atoll(item_child->Attribute("fileSize"));
+
+ resources.push_back(res);
+ ParseItem(item, resources, item_child, path);
+ }
+ else if(name == "group")
+ {
+ ParseItem(item, resources, item_child, path);
+ }
+ else if(name == "thumbnail")
+ {
+ if(!item_child->NoChildren() && IsPathToThumbnail(item_child->FirstChild()->ValueStr()))
+ item->SetArt("thumb", item_child->FirstChild()->ValueStr());
+ else
+ {
+ const char * url = item_child->Attribute("url");
+ if(url && IsPathToThumbnail(url))
+ item->SetArt("thumb", url);
+ }
+ }
+ else if (name == "title")
+ {
+ if(text.empty())
+ return;
+
+ if(text.length() > item->m_strTitle.length())
+ item->m_strTitle = text;
+ }
+ else if(name == "description")
+ {
+ if(text.empty())
+ return;
+
+ std::string description = text;
+ if(XMLUtils::GetAttribute(item_child, "type") == "html")
+ HTML::CHTMLUtil::RemoveTags(description);
+ item->SetProperty("description", description);
+ }
+ else if(name == "category")
+ {
+ if(text.empty())
+ return;
+
+ std::string scheme = XMLUtils::GetAttribute(item_child, "scheme");
+
+ /* okey this is silly, boxee what did you think?? */
+ if (scheme == "urn:boxee:genre")
+ vtag->m_genre.push_back(text);
+ else if(scheme == "urn:boxee:title-type")
+ {
+ if (text == "tv")
+ item->SetProperty("boxee:istvshow", true);
+ else if(text == "movie")
+ item->SetProperty("boxee:ismovie", true);
+ }
+ else if(scheme == "urn:boxee:episode")
+ vtag->m_iEpisode = atoi(text.c_str());
+ else if(scheme == "urn:boxee:season")
+ vtag->m_iSeason = atoi(text.c_str());
+ else if(scheme == "urn:boxee:show-title")
+ vtag->m_strShowTitle = text.c_str();
+ else if(scheme == "urn:boxee:view-count")
+ vtag->SetPlayCount(atoi(text.c_str()));
+ else if(scheme == "urn:boxee:source")
+ item->SetProperty("boxee:provider_source", text);
+ else
+ vtag->m_genre = StringUtils::Split(text, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+ else if(name == "rating")
+ {
+ std::string scheme = XMLUtils::GetAttribute(item_child, "scheme");
+ if(scheme == "urn:user")
+ vtag->SetRating((float)atof(text.c_str()));
+ else
+ vtag->m_strMPAARating = text;
+ }
+ else if(name == "credit")
+ {
+ std::string role = XMLUtils::GetAttribute(item_child, "role");
+ if (role == "director")
+ vtag->m_director.push_back(text);
+ else if(role == "author"
+ || role == "writer")
+ vtag->m_writingCredits.push_back(text);
+ else if(role == "actor")
+ {
+ SActorInfo actor;
+ actor.strName = text;
+ vtag->m_cast.push_back(actor);
+ }
+ }
+ else if(name == "copyright")
+ vtag->m_studio = StringUtils::Split(text, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ else if(name == "keywords")
+ item->SetProperty("keywords", text);
+
+}
+
+static void ParseItemItunes(CFileItem* item, SResources& resources, TiXmlElement* item_child, const std::string& name, const std::string& xmlns, const std::string& path)
+{
+ CVideoInfoTag* vtag = item->GetVideoInfoTag();
+ std::string text = GetValue(item_child);
+
+ if(name == "image")
+ {
+ const char * url = item_child->Attribute("href");
+ if(url)
+ item->SetArt("thumb", url);
+ else
+ item->SetArt("thumb", text);
+ }
+ else if(name == "summary")
+ vtag->m_strPlot = text;
+ else if(name == "subtitle")
+ vtag->m_strPlotOutline = text;
+ else if(name == "author")
+ vtag->m_writingCredits.push_back(text);
+ else if(name == "duration")
+ vtag->SetDuration(StringUtils::TimeStringToSeconds(text));
+ else if(name == "keywords")
+ item->SetProperty("keywords", text);
+}
+
+static void ParseItemRSS(CFileItem* item, SResources& resources, TiXmlElement* item_child, const std::string& name, const std::string& xmlns, const std::string& path)
+{
+ std::string text = GetValue(item_child);
+ if (name == "title")
+ {
+ if(text.length() > item->m_strTitle.length())
+ item->m_strTitle = text;
+ }
+ else if (name == "pubDate")
+ {
+ CDateTime pubDate(ParseDate(text));
+ item->m_dateTime = pubDate;
+ }
+ else if (name == "link")
+ {
+ SResource res;
+ res.tag = "rss:link";
+ res.path = text;
+ resources.push_back(res);
+ }
+ else if(name == "enclosure")
+ {
+ const char * len = item_child->Attribute("length");
+
+ SResource res;
+ res.tag = "rss:enclosure";
+ res.path = XMLUtils::GetAttribute(item_child, "url");
+ res.mime = XMLUtils::GetAttribute(item_child, "type");
+ if(len)
+ res.size = std::atoll(len);
+
+ resources.push_back(res);
+ }
+ else if(name == "description")
+ {
+ std::string description = text;
+ HTML::CHTMLUtil::RemoveTags(description);
+ item->SetProperty("description", description);
+ }
+ else if(name == "guid")
+ {
+ if(IsPathToMedia(text))
+ {
+ SResource res;
+ res.tag = "rss:guid";
+ res.path = text;
+ resources.push_back(res);
+ }
+ }
+}
+
+static void ParseItemVoddler(CFileItem* item, SResources& resources, TiXmlElement* element, const std::string& name, const std::string& xmlns, const std::string& path)
+{
+ CVideoInfoTag* vtag = item->GetVideoInfoTag();
+ std::string text = GetValue(element);
+
+ if(name == "trailer")
+ {
+ vtag->m_strTrailer = text;
+
+ SResource res;
+ res.tag = "voddler:trailer";
+ res.mime = XMLUtils::GetAttribute(element, "type");
+ res.path = text;
+ resources.push_back(res);
+ }
+ else if(name == "year")
+ vtag->SetYear(atoi(text.c_str()));
+ else if(name == "rating")
+ vtag->SetRating((float)atof(text.c_str()));
+ else if(name == "tagline")
+ vtag->m_strTagLine = text;
+ else if(name == "posterwall")
+ {
+ const char* url = element->Attribute("url");
+ if(url)
+ item->SetArt("fanart", url);
+ else if(IsPathToThumbnail(text))
+ item->SetArt("fanart", text);
+ }
+}
+
+static void ParseItemBoxee(CFileItem* item, SResources& resources, TiXmlElement* element, const std::string& name, const std::string& xmlns, const std::string& path)
+{
+ CVideoInfoTag* vtag = item->GetVideoInfoTag();
+ std::string text = GetValue(element);
+
+ if (name == "image")
+ item->SetArt("thumb", text);
+ else if(name == "user_agent")
+ item->SetProperty("boxee:user_agent", text);
+ else if(name == "content_type")
+ item->SetMimeType(text);
+ else if(name == "runtime")
+ vtag->SetDuration(StringUtils::TimeStringToSeconds(text));
+ else if(name == "episode")
+ vtag->m_iEpisode = atoi(text.c_str());
+ else if(name == "season")
+ vtag->m_iSeason = atoi(text.c_str());
+ else if(name == "view-count")
+ vtag->SetPlayCount(atoi(text.c_str()));
+ else if(name == "tv-show-title")
+ vtag->m_strShowTitle = text;
+ else if(name == "release-date")
+ item->SetProperty("boxee:releasedate", text);
+}
+
+static void ParseItemZink(CFileItem* item, SResources& resources, TiXmlElement* element, const std::string& name, const std::string& xmlns, const std::string& path)
+{
+ CVideoInfoTag* vtag = item->GetVideoInfoTag();
+ std::string text = GetValue(element);
+ if (name == "episode")
+ vtag->m_iEpisode = atoi(text.c_str());
+ else if(name == "season")
+ vtag->m_iSeason = atoi(text.c_str());
+ else if(name == "views")
+ vtag->SetPlayCount(atoi(text.c_str()));
+ else if(name == "airdate")
+ vtag->m_firstAired.SetFromDateString(text);
+ else if(name == "userrating")
+ vtag->SetRating((float)atof(text.c_str()));
+ else if(name == "duration")
+ vtag->SetDuration(atoi(text.c_str()));
+ else if(name == "durationstr")
+ vtag->SetDuration(StringUtils::TimeStringToSeconds(text));
+}
+
+static void ParseItemSVT(CFileItem* item, SResources& resources, TiXmlElement* element, const std::string& name, const std::string& xmlns, const std::string& path)
+{
+ std::string text = GetValue(element);
+ if (name == "xmllink")
+ {
+ SResource res;
+ res.tag = "svtplay:xmllink";
+ res.path = text;
+ res.mime = "application/rss+xml";
+ resources.push_back(res);
+ }
+ else if (name == "broadcasts")
+ {
+ CURL url(path);
+ if(StringUtils::StartsWith(url.GetFileName(), "v1/"))
+ {
+ SResource res;
+ res.tag = "svtplay:broadcasts";
+ res.path = url.GetWithoutFilename() + "v1/video/list/" + text;
+ res.mime = "application/rss+xml";
+ resources.push_back(res);
+ }
+ }
+}
+
+static void ParseItem(CFileItem* item, SResources& resources, TiXmlElement* root, const std::string& path)
+{
+ for (TiXmlElement* child = root->FirstChildElement(); child; child = child->NextSiblingElement())
+ {
+ std::string name = child->Value();
+ std::string xmlns;
+ size_t pos = name.find(':');
+ if(pos != std::string::npos)
+ {
+ xmlns = name.substr(0, pos);
+ name.erase(0, pos+1);
+ }
+
+ if (xmlns == "media")
+ ParseItemMRSS (item, resources, child, name, xmlns, path);
+ else if (xmlns == "itunes")
+ ParseItemItunes (item, resources, child, name, xmlns, path);
+ else if (xmlns == "voddler")
+ ParseItemVoddler(item, resources, child, name, xmlns, path);
+ else if (xmlns == "boxee")
+ ParseItemBoxee (item, resources, child, name, xmlns, path);
+ else if (xmlns == "zn")
+ ParseItemZink (item, resources, child, name, xmlns, path);
+ else if (xmlns == "svtplay")
+ ParseItemSVT (item, resources, child, name, xmlns, path);
+ else
+ ParseItemRSS (item, resources, child, name, xmlns, path);
+ }
+}
+
+static bool FindMime(const SResources& resources, const std::string& mime)
+{
+ for (const auto& it : resources)
+ {
+ if (StringUtils::StartsWithNoCase(it.mime, mime))
+ return true;
+ }
+ return false;
+}
+
+static void ParseItem(CFileItem* item, TiXmlElement* root, const std::string& path)
+{
+ SResources resources;
+ ParseItem(item, resources, root, path);
+
+ const char* prio[] = { "media:content", "voddler:trailer", "rss:enclosure", "svtplay:broadcasts", "svtplay:xmllink", "rss:link", "rss:guid", NULL };
+
+ std::string mime;
+ if (FindMime(resources, "video/"))
+ mime = "video/";
+ else if(FindMime(resources, "audio/"))
+ mime = "audio/";
+ else if(FindMime(resources, "application/rss"))
+ mime = "application/rss";
+ else if(FindMime(resources, "image/"))
+ mime = "image/";
+
+ int maxrate = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_NETWORK_BANDWIDTH);
+ if(maxrate == 0)
+ maxrate = INT_MAX;
+
+ SResources::iterator best = resources.end();
+ for(const char** type = prio; *type && best == resources.end(); type++)
+ {
+ for (SResources::iterator it = resources.begin(); it != resources.end(); ++it)
+ {
+ if(!StringUtils::StartsWith(it->mime, mime))
+ continue;
+
+ if(it->tag == *type)
+ {
+ if(best == resources.end())
+ {
+ best = it;
+ continue;
+ }
+
+ if(it->bitrate == best->bitrate)
+ {
+ if(it->width*it->height > best->width*best->height)
+ best = it;
+ continue;
+ }
+
+ if(it->bitrate > maxrate)
+ {
+ if(it->bitrate < best->bitrate)
+ best = it;
+ continue;
+ }
+
+ if(it->bitrate > best->bitrate)
+ {
+ best = it;
+ continue;
+ }
+ }
+ }
+ }
+
+ if(best != resources.end())
+ {
+ item->SetMimeType(best->mime);
+ item->SetPath(best->path);
+ item->m_dwSize = best->size;
+
+ if(best->duration)
+ item->SetProperty("duration", StringUtils::SecondsToTimeString(best->duration));
+
+ /* handling of mimetypes fo directories are sub optimal at best */
+ if(best->mime == "application/rss+xml" && StringUtils::StartsWithNoCase(item->GetPath(), "http://"))
+ item->SetPath("rss://" + item->GetPath().substr(7));
+
+ if(best->mime == "application/rss+xml" && StringUtils::StartsWithNoCase(item->GetPath(), "https://"))
+ item->SetPath("rsss://" + item->GetPath().substr(8));
+
+ if(StringUtils::StartsWithNoCase(item->GetPath(), "rss://")
+ || StringUtils::StartsWithNoCase(item->GetPath(), "rsss://"))
+ item->m_bIsFolder = true;
+ else
+ item->m_bIsFolder = false;
+ }
+
+ if(!item->m_strTitle.empty())
+ item->SetLabel(item->m_strTitle);
+
+ if(item->HasVideoInfoTag())
+ {
+ CVideoInfoTag* vtag = item->GetVideoInfoTag();
+
+ if(item->HasProperty("duration") && !vtag->GetDuration())
+ vtag->SetDuration(StringUtils::TimeStringToSeconds(item->GetProperty("duration").asString()));
+
+ if(item->HasProperty("description") && vtag->m_strPlot.empty())
+ vtag->m_strPlot = item->GetProperty("description").asString();
+
+ if(vtag->m_strPlotOutline.empty() && !vtag->m_strPlot.empty())
+ {
+ size_t pos = vtag->m_strPlot.find('\n');
+ if(pos != std::string::npos)
+ vtag->m_strPlotOutline = vtag->m_strPlot.substr(0, pos);
+ else
+ vtag->m_strPlotOutline = vtag->m_strPlot;
+ }
+
+ if(!vtag->GetDuration())
+ item->SetLabel2(StringUtils::SecondsToTimeString(vtag->GetDuration()));
+ }
+}
+
+bool CRSSDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ const std::string pathToUrl(url.Get());
+ std::string strPath(pathToUrl);
+ URIUtils::RemoveSlashAtEnd(strPath);
+ std::map<std::string,CDateTime>::iterator it;
+ items.SetPath(strPath);
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if ((it=m_cache.find(strPath)) != m_cache.end())
+ {
+ if (it->second > CDateTime::GetCurrentDateTime() &&
+ items.Load())
+ return true;
+ m_cache.erase(it);
+ }
+ lock.unlock();
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(strPath))
+ {
+ CLog::Log(LOGERROR, "failed to load xml from <{}>. error: <{}>", strPath, xmlDoc.ErrorId());
+ return false;
+ }
+ if (xmlDoc.Error())
+ {
+ CLog::Log(LOGERROR, "error parsing xml doc from <{}>. error: <{}>", strPath, xmlDoc.ErrorId());
+ return false;
+ }
+
+ TiXmlElement* rssXmlNode = xmlDoc.RootElement();
+
+ if (!rssXmlNode)
+ return false;
+
+ TiXmlHandle docHandle( &xmlDoc );
+ TiXmlElement* channelXmlNode = docHandle.FirstChild( "rss" ).FirstChild( "channel" ).Element();
+ if (channelXmlNode)
+ ParseItem(&items, channelXmlNode, pathToUrl);
+ else
+ return false;
+
+ TiXmlElement* child = NULL;
+ for (child = channelXmlNode->FirstChildElement("item"); child; child = child->NextSiblingElement())
+ {
+ // Create new item,
+ CFileItemPtr item(new CFileItem());
+ ParseItem(item.get(), child, pathToUrl);
+
+ item->SetProperty("isrss", "1");
+ // Use channel image if item doesn't have one
+ if (!item->HasArt("thumb") && items.HasArt("thumb"))
+ item->SetArt("thumb", items.GetArt("thumb"));
+
+ if (!item->GetPath().empty())
+ items.Add(item);
+ }
+
+ items.AddSortMethod(SortByNone , 231, LABEL_MASKS("%L", "%D", "%L", "")); // FileName, Duration | Foldername, empty
+ items.AddSortMethod(SortByLabel , 551, LABEL_MASKS("%L", "%D", "%L", "")); // FileName, Duration | Foldername, empty
+ items.AddSortMethod(SortBySize , 553, LABEL_MASKS("%L", "%I", "%L", "%I")); // FileName, Size | Foldername, Size
+ items.AddSortMethod(SortByDate , 552, LABEL_MASKS("%L", "%J", "%L", "%J")); // FileName, Date | Foldername, Date
+
+ CDateTime time = CDateTime::GetCurrentDateTime();
+ int mins = 60;
+ TiXmlElement* ttl = docHandle.FirstChild("rss").FirstChild("ttl").Element();
+ if (ttl)
+ mins = strtol(ttl->FirstChild()->Value(),NULL,10);
+ time += CDateTimeSpan(0,0,mins,0);
+ items.SetPath(strPath);
+ items.Save();
+ std::unique_lock<CCriticalSection> lock2(m_section);
+ m_cache.insert(make_pair(strPath,time));
+
+ return true;
+}
+
+bool CRSSDirectory::Exists(const CURL& url)
+{
+ CCurlFile rss;
+ return rss.Exists(url);
+}
diff --git a/xbmc/filesystem/RSSDirectory.h b/xbmc/filesystem/RSSDirectory.h
new file mode 100644
index 0000000..b2e3d5f
--- /dev/null
+++ b/xbmc/filesystem/RSSDirectory.h
@@ -0,0 +1,39 @@
+/*
+ * 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 "IFileDirectory.h"
+#include "XBDateTime.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <string>
+
+class CFileItemList;
+
+namespace XFILE
+{
+ class CRSSDirectory : public IFileDirectory
+ {
+ public:
+ CRSSDirectory();
+ ~CRSSDirectory() override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool Exists(const CURL& url) override;
+ bool AllowAll() const override { return true; }
+ bool ContainsFiles(const CURL& url) override;
+ DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ONCE; }
+
+ protected:
+ // key is path, value is cache invalidation date
+ static std::map<std::string,CDateTime> m_cache;
+ static CCriticalSection m_section;
+ };
+}
+
diff --git a/xbmc/filesystem/ResourceDirectory.cpp b/xbmc/filesystem/ResourceDirectory.cpp
new file mode 100644
index 0000000..c3623d0
--- /dev/null
+++ b/xbmc/filesystem/ResourceDirectory.cpp
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2014-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 "ResourceDirectory.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "filesystem/Directory.h"
+#include "filesystem/ResourceFile.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE;
+
+CResourceDirectory::CResourceDirectory() = default;
+
+CResourceDirectory::~CResourceDirectory() = default;
+
+bool CResourceDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ const std::string pathToUrl(url.Get());
+ std::string translatedPath;
+ if (!CResourceFile::TranslatePath(url, translatedPath))
+ return false;
+
+ if (CDirectory::GetDirectory(translatedPath, items, m_strFileMask, m_flags | DIR_FLAG_GET_HIDDEN))
+ { // replace our paths as necessary
+ items.SetURL(url);
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ if (URIUtils::PathHasParent(item->GetPath(), translatedPath))
+ item->SetPath(URIUtils::AddFileToFolder(pathToUrl, item->GetPath().substr(translatedPath.size())));
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+std::string CResourceDirectory::TranslatePath(const CURL &url)
+{
+ std::string translatedPath;
+ if (!CResourceFile::TranslatePath(url, translatedPath))
+ return "";
+
+ return translatedPath;
+}
diff --git a/xbmc/filesystem/ResourceDirectory.h b/xbmc/filesystem/ResourceDirectory.h
new file mode 100644
index 0000000..f487852
--- /dev/null
+++ b/xbmc/filesystem/ResourceDirectory.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014-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 "filesystem/OverrideDirectory.h"
+
+namespace XFILE
+{
+ class CResourceDirectory : public COverrideDirectory
+ {
+ public:
+ CResourceDirectory();
+ ~CResourceDirectory() override;
+
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+
+ protected:
+ std::string TranslatePath(const CURL &url) override;
+ };
+}
diff --git a/xbmc/filesystem/ResourceFile.cpp b/xbmc/filesystem/ResourceFile.cpp
new file mode 100644
index 0000000..81a9982
--- /dev/null
+++ b/xbmc/filesystem/ResourceFile.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2014-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 "ResourceFile.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/Resource.h"
+
+using namespace ADDON;
+using namespace XFILE;
+
+CResourceFile::CResourceFile()
+ : COverrideFile(false)
+{ }
+
+CResourceFile::~CResourceFile() = default;
+
+bool CResourceFile::TranslatePath(const std::string &path, std::string &translatedPath)
+{
+ return TranslatePath(CURL(path), translatedPath);
+}
+
+bool CResourceFile::TranslatePath(const CURL &url, std::string &translatedPath)
+{
+ translatedPath = url.Get();
+
+ // only handle resource:// paths
+ if (!url.IsProtocol("resource"))
+ return false;
+
+ // the share name represents an identifier that can be mapped to an addon ID
+ const std::string& addonId = url.GetShareName();
+ std::string filePath;
+ if (url.GetFileName().length() > addonId.length())
+ filePath = url.GetFileName().substr(addonId.size() + 1);
+
+ if (addonId.empty())
+ return false;
+
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(addonId, addon, OnlyEnabled::CHOICE_YES) ||
+ addon == NULL)
+ return false;
+
+ std::shared_ptr<CResource> resource = std::dynamic_pointer_cast<ADDON::CResource>(addon);
+ if (resource == NULL)
+ return false;
+
+ if (!resource->IsAllowed(filePath))
+ return false;
+
+ translatedPath = CUtil::ValidatePath(resource->GetFullPath(filePath));
+ return true;
+}
+
+std::string CResourceFile::TranslatePath(const CURL &url)
+{
+ std::string translatedPath;
+ if (!TranslatePath(url, translatedPath))
+ return "";
+
+ return translatedPath;
+}
diff --git a/xbmc/filesystem/ResourceFile.h b/xbmc/filesystem/ResourceFile.h
new file mode 100644
index 0000000..da721b5
--- /dev/null
+++ b/xbmc/filesystem/ResourceFile.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2014-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 "filesystem/OverrideFile.h"
+
+namespace XFILE
+{
+class CResourceFile : public COverrideFile
+{
+public:
+ CResourceFile();
+ ~CResourceFile() override;
+
+ static bool TranslatePath(const std::string &path, std::string &translatedPath);
+ static bool TranslatePath(const CURL &url, std::string &translatedPath);
+
+protected:
+ std::string TranslatePath(const CURL &url) override;
+};
+}
diff --git a/xbmc/filesystem/ShoutcastFile.cpp b/xbmc/filesystem/ShoutcastFile.cpp
new file mode 100644
index 0000000..7488ee7
--- /dev/null
+++ b/xbmc/filesystem/ShoutcastFile.cpp
@@ -0,0 +1,362 @@
+/*
+ * 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.
+ */
+
+
+// FileShoutcast.cpp: implementation of the CShoutcastFile class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "ShoutcastFile.h"
+
+#include "FileCache.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "filesystem/CurlFile.h"
+#include "messaging/ApplicationMessenger.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SingleLock.h"
+#include "utils/CharsetConverter.h"
+#include "utils/HTMLUtil.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/UrlOptions.h"
+
+#include <climits>
+#include <mutex>
+
+using namespace XFILE;
+using namespace MUSIC_INFO;
+using namespace std::chrono_literals;
+
+CShoutcastFile::CShoutcastFile() :
+ IFile(), CThread("ShoutcastFile")
+{
+ m_discarded = 0;
+ m_currint = 0;
+ m_buffer = NULL;
+ m_cacheReader = NULL;
+ m_metaint = 0;
+}
+
+CShoutcastFile::~CShoutcastFile()
+{
+ Close();
+}
+
+int64_t CShoutcastFile::GetPosition()
+{
+ return m_file.GetPosition()-m_discarded;
+}
+
+int64_t CShoutcastFile::GetLength()
+{
+ return 0;
+}
+
+std::string CShoutcastFile::DecodeToUTF8(const std::string& str)
+{
+ std::string ret = str;
+
+ if (m_fileCharset.empty())
+ g_charsetConverter.unknownToUTF8(ret);
+ else
+ g_charsetConverter.ToUtf8(m_fileCharset, str, ret);
+
+ std::wstring wBuffer, wConverted;
+ g_charsetConverter.utf8ToW(ret, wBuffer, false);
+ HTML::CHTMLUtil::ConvertHTMLToW(wBuffer, wConverted);
+ g_charsetConverter.wToUTF8(wConverted, ret);
+
+ return ret;
+}
+
+bool CShoutcastFile::Open(const CURL& url)
+{
+ CURL url2(url);
+ url2.SetProtocolOptions(url2.GetProtocolOptions()+"&noshout=true&Icy-MetaData=1");
+ if (url.GetProtocol() == "shouts")
+ url2.SetProtocol("https");
+ else if (url.GetProtocol() == "shout")
+ url2.SetProtocol("http");
+
+ std::string icyTitle;
+ std::string icyGenre;
+
+ bool result = m_file.Open(url2);
+ if (result)
+ {
+ m_fileCharset = m_file.GetProperty(XFILE::FILE_PROPERTY_CONTENT_CHARSET);
+
+ icyTitle = m_file.GetHttpHeader().GetValue("icy-name");
+ if (icyTitle.empty())
+ icyTitle = m_file.GetHttpHeader().GetValue("ice-name"); // icecast
+ if (icyTitle == "This is my server name") // Handle badly set up servers
+ icyTitle.clear();
+
+ icyTitle = DecodeToUTF8(icyTitle);
+
+ icyGenre = m_file.GetHttpHeader().GetValue("icy-genre");
+ if (icyGenre.empty())
+ icyGenre = m_file.GetHttpHeader().GetValue("ice-genre"); // icecast
+
+ icyGenre = DecodeToUTF8(icyGenre);
+ }
+ m_metaint = atoi(m_file.GetHttpHeader().GetValue("icy-metaint").c_str());
+ if (!m_metaint)
+ m_metaint = -1;
+
+ m_buffer = new char[16*255];
+
+ if (result)
+ {
+ std::unique_lock<CCriticalSection> lock(m_tagSection);
+
+ m_masterTag.reset(new CMusicInfoTag());
+ m_masterTag->SetStationName(icyTitle);
+ m_masterTag->SetGenre(icyGenre);
+ m_masterTag->SetLoaded(true);
+
+ m_tags.push({1, m_masterTag});
+ m_tagChange.Set();
+ }
+
+ return result;
+}
+
+ssize_t CShoutcastFile::Read(void* lpBuf, size_t uiBufSize)
+{
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+ if (m_currint >= m_metaint && m_metaint > 0)
+ {
+ unsigned char header;
+ m_file.Read(&header,1);
+ ReadTruncated(m_buffer, header*16);
+ ExtractTagInfo(m_buffer);
+ m_discarded += header*16+1;
+ m_currint = 0;
+ }
+
+ ssize_t toRead;
+ if (m_metaint > 0)
+ toRead = std::min<size_t>(uiBufSize,m_metaint-m_currint);
+ else
+ toRead = std::min<size_t>(uiBufSize,16*255);
+ toRead = m_file.Read(lpBuf,toRead);
+ if (toRead > 0)
+ m_currint += toRead;
+ return toRead;
+}
+
+int64_t CShoutcastFile::Seek(int64_t iFilePosition, int iWhence)
+{
+ return -1;
+}
+
+void CShoutcastFile::Close()
+{
+ StopThread();
+ delete[] m_buffer;
+ m_buffer = NULL;
+ m_file.Close();
+ m_title.clear();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_tagSection);
+ while (!m_tags.empty())
+ m_tags.pop();
+ m_masterTag.reset();
+ m_tagChange.Set();
+ }
+}
+
+bool CShoutcastFile::ExtractTagInfo(const char* buf)
+{
+ std::string strBuffer = DecodeToUTF8(buf);
+
+ bool result = false;
+
+ CRegExp reTitle(true);
+ reTitle.RegComp("StreamTitle=\'(.*?)\';");
+
+ if (reTitle.RegFind(strBuffer.c_str()) != -1)
+ {
+ const std::string newtitle = reTitle.GetMatch(1);
+
+ result = (m_title != newtitle);
+ if (result) // track has changed
+ {
+ m_title = newtitle;
+
+ std::string title;
+ std::string artistInfo;
+ std::string coverURL;
+
+ CRegExp reURL(true);
+ reURL.RegComp("StreamUrl=\'(.*?)\';");
+ bool haveStreamUrlData =
+ (reURL.RegFind(strBuffer.c_str()) != -1) && !reURL.GetMatch(1).empty();
+
+ if (haveStreamUrlData) // track has changed and extra metadata might be available
+ {
+ const std::string streamUrlData = reURL.GetMatch(1);
+ if (StringUtils::StartsWithNoCase(streamUrlData, "http://") ||
+ StringUtils::StartsWithNoCase(streamUrlData, "https://"))
+ {
+ // Bauer Media Radio listenapi null event to erase current data
+ if (!StringUtils::EndsWithNoCase(streamUrlData, "eventdata/-1"))
+ {
+ const CURL dataURL(streamUrlData);
+ XFILE::CCurlFile http;
+ std::string extData;
+
+ if (http.Get(dataURL.Get(), extData))
+ {
+ const std::string contentType = http.GetHttpHeader().GetMimeType();
+ if (StringUtils::EqualsNoCase(contentType, "application/json"))
+ {
+ CVariant json;
+ if (CJSONVariantParser::Parse(extData, json))
+ {
+ // Check for Bauer Media Radio listenapi meta data.
+ // Example: StreamUrl='https://listenapi.bauerradio.com/api9/eventdata/58431417'
+ artistInfo = json["eventSongArtist"].asString();
+ title = json["eventSongTitle"].asString();
+ coverURL = json["eventImageUrl"].asString();
+ }
+ }
+ }
+ }
+ }
+ else if (StringUtils::StartsWithNoCase(streamUrlData, "&"))
+ {
+ // Check for SAM Cast meta data.
+ // Example: StreamUrl='&artist=RECLAM&title=BOLORDURAN%2017&album=&duration=17894&songtype=S&overlay=no&buycd=&website=&picture='
+
+ CUrlOptions urlOptions(streamUrlData);
+ const CUrlOptions::UrlOptions& options = urlOptions.GetOptions();
+
+ auto it = options.find("artist");
+ if (it != options.end())
+ artistInfo = (*it).second.asString();
+
+ it = options.find("title");
+ if (it != options.end())
+ title = (*it).second.asString();
+
+ it = options.find("picture");
+ if (it != options.end())
+ {
+ coverURL = (*it).second.asString();
+ if (!coverURL.empty())
+ {
+ // Check value being a URL (not just a file name)
+ const CURL url(coverURL);
+ if (url.GetProtocol().empty())
+ coverURL.clear();
+ }
+ }
+ }
+ }
+
+ if (artistInfo.empty() || title.empty())
+ {
+ // Most stations supply StreamTitle in format "artist - songtitle"
+ const std::vector<std::string> tokens = StringUtils::Split(newtitle, " - ");
+ if (tokens.size() == 2)
+ {
+ if (artistInfo.empty())
+ artistInfo = tokens[0];
+
+ if (title.empty())
+ title = tokens[1];
+ }
+ else
+ {
+ if (title.empty())
+ {
+ // Do not display Bauer Media Radio SteamTitle values to mark start/stop of ad breaks.
+ if (!StringUtils::StartsWithNoCase(newtitle, "START ADBREAK ") &&
+ !StringUtils::StartsWithNoCase(newtitle, "STOP ADBREAK "))
+ title = newtitle;
+ }
+ }
+ }
+
+ if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bShoutcastArt)
+ coverURL.clear();
+
+ std::unique_lock<CCriticalSection> lock(m_tagSection);
+
+ const std::shared_ptr<CMusicInfoTag> tag = std::make_shared<CMusicInfoTag>(*m_masterTag);
+ tag->SetArtist(artistInfo);
+ tag->SetTitle(title);
+ tag->SetStationArt(coverURL);
+
+ m_tags.push({m_file.GetPosition(), tag});
+ m_tagChange.Set();
+ }
+ }
+
+ return result;
+}
+
+void CShoutcastFile::ReadTruncated(char* buf2, int size)
+{
+ char* buf = buf2;
+ while (size > 0)
+ {
+ int read = m_file.Read(buf,size);
+ size -= read;
+ buf += read;
+ }
+}
+
+int CShoutcastFile::IoControl(EIoControl control, void* payload)
+{
+ if (control == IOCTRL_SET_CACHE && m_cacheReader == nullptr)
+ {
+ std::unique_lock<CCriticalSection> lock(m_tagSection);
+ m_cacheReader = static_cast<CFileCache*>(payload);
+ Create();
+ }
+
+ return IFile::IoControl(control, payload);
+}
+
+void CShoutcastFile::Process()
+{
+ while (!m_bStop)
+ {
+ if (m_tagChange.Wait(500ms))
+ {
+ std::unique_lock<CCriticalSection> lock(m_tagSection);
+ while (!m_bStop && !m_tags.empty())
+ {
+ const TagInfo& front = m_tags.front();
+ if (m_cacheReader->GetPosition() < front.first) // tagpos
+ {
+ CSingleExit ex(m_tagSection);
+ CThread::Sleep(20ms);
+ }
+ else
+ {
+ CFileItem* item = new CFileItem(*front.second); // will be deleted by msg receiver
+ m_tags.pop();
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_CURRENT_ITEM, 1, -1,
+ static_cast<void*>(item));
+ }
+ }
+ }
+ }
+}
diff --git a/xbmc/filesystem/ShoutcastFile.h b/xbmc/filesystem/ShoutcastFile.h
new file mode 100644
index 0000000..6b34361
--- /dev/null
+++ b/xbmc/filesystem/ShoutcastFile.h
@@ -0,0 +1,76 @@
+/*
+ * 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
+
+// FileShoutcast.h: interface for the CShoutcastFile class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "CurlFile.h"
+#include "IFile.h"
+#include "threads/Thread.h"
+
+#include <memory>
+#include <queue>
+#include <utility>
+
+namespace MUSIC_INFO
+{
+class CMusicInfoTag;
+}
+
+namespace XFILE
+{
+
+class CFileCache;
+
+class CShoutcastFile : public IFile, public CThread
+{
+public:
+ CShoutcastFile();
+ ~CShoutcastFile() override;
+ int64_t GetPosition() override;
+ int64_t GetLength() override;
+ bool Open(const CURL& url) override;
+ bool Exists(const CURL& url) override { return true; }
+ int Stat(const CURL& url, struct __stat64* buffer) override
+ {
+ errno = ENOENT;
+ return -1;
+ }
+ ssize_t Read(void* lpBuf, size_t uiBufSize) override;
+ int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override;
+ void Close() override;
+ int IoControl(EIoControl request, void* param) override;
+
+ void Process() override;
+protected:
+ bool ExtractTagInfo(const char* buf);
+ void ReadTruncated(char* buf2, int size);
+
+private:
+ std::string DecodeToUTF8(const std::string& str);
+
+ CCurlFile m_file;
+ std::string m_fileCharset;
+ int m_metaint;
+ int m_discarded; // data used for tags
+ int m_currint;
+ char* m_buffer; // buffer used for tags
+ std::string m_title;
+
+ CFileCache* m_cacheReader;
+ CEvent m_tagChange;
+ CCriticalSection m_tagSection;
+ using TagInfo = std::pair<int64_t, std::shared_ptr<MUSIC_INFO::CMusicInfoTag>>;
+ std::queue<TagInfo> m_tags; // tagpos, tag
+ std::shared_ptr<MUSIC_INFO::CMusicInfoTag> m_masterTag;
+};
+}
+
diff --git a/xbmc/filesystem/SmartPlaylistDirectory.cpp b/xbmc/filesystem/SmartPlaylistDirectory.cpp
new file mode 100644
index 0000000..9e05599
--- /dev/null
+++ b/xbmc/filesystem/SmartPlaylistDirectory.cpp
@@ -0,0 +1,359 @@
+/*
+ * 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 "SmartPlaylistDirectory.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/FileDirectoryFactory.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicDbUrl.h"
+#include "playlists/PlayListTypes.h"
+#include "playlists/SmartPlayList.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoDbUrl.h"
+
+#include <math.h>
+
+#define PROPERTY_PATH_DB "path.db"
+#define PROPERTY_SORT_ORDER "sort.order"
+#define PROPERTY_SORT_ASCENDING "sort.ascending"
+#define PROPERTY_GROUP_BY "group.by"
+#define PROPERTY_GROUP_MIXED "group.mixed"
+
+namespace XFILE
+{
+ CSmartPlaylistDirectory::CSmartPlaylistDirectory() = default;
+
+ CSmartPlaylistDirectory::~CSmartPlaylistDirectory() = default;
+
+ bool CSmartPlaylistDirectory::GetDirectory(const CURL& url, CFileItemList& items)
+ {
+ // Load in the SmartPlaylist and get the WHERE query
+ CSmartPlaylist playlist;
+ if (!playlist.Load(url))
+ return false;
+ bool result = GetDirectory(playlist, items);
+ if (result)
+ items.SetProperty("library.smartplaylist", true);
+
+ return result;
+ }
+
+ bool CSmartPlaylistDirectory::GetDirectory(const CSmartPlaylist &playlist, CFileItemList& items, const std::string &strBaseDir /* = "" */, bool filter /* = false */)
+ {
+ bool success = false, success2 = false;
+ std::vector<std::string> virtualFolders;
+
+ SortDescription sorting;
+ if (playlist.GetLimit() > 0)
+ sorting.limitEnd = playlist.GetLimit();
+ sorting.sortBy = playlist.GetOrder();
+ sorting.sortOrder = playlist.GetOrderAscending() ? SortOrderAscending : SortOrderDescending;
+ sorting.sortAttributes = playlist.GetOrderAttributes();
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sorting.sortAttributes = (SortAttribute)(sorting.sortAttributes | SortAttributeIgnoreArticle);
+ if (playlist.IsMusicType() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sorting.sortAttributes =
+ static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
+ items.SetSortIgnoreFolders((sorting.sortAttributes & SortAttributeIgnoreFolders) ==
+ SortAttributeIgnoreFolders);
+
+ std::string option = !filter ? "xsp" : "filter";
+ std::string group = playlist.GetGroup();
+ bool isGrouped = !group.empty() && !StringUtils::EqualsNoCase(group, "none") && !playlist.IsGroupMixed();
+ // Hint for playlist files like STRM
+ PLAYLIST::Id playlistTypeHint = PLAYLIST::TYPE_NONE;
+
+ // get all virtual folders and add them to the item list
+ playlist.GetVirtualFolders(virtualFolders);
+ for (const std::string& virtualFolder : virtualFolders)
+ {
+ CFileItemPtr pItem = CFileItemPtr(new CFileItem(virtualFolder, true));
+ IFileDirectory *dir = CFileDirectoryFactory::Create(pItem->GetURL(), pItem.get());
+
+ if (dir != NULL)
+ {
+ pItem->SetSpecialSort(SortSpecialOnTop);
+ items.Add(pItem);
+ delete dir;
+ }
+ }
+
+ if (playlist.GetType() == "movies" ||
+ playlist.GetType() == "tvshows" ||
+ playlist.GetType() == "episodes")
+ {
+ playlistTypeHint = PLAYLIST::TYPE_VIDEO;
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ MediaType mediaType = CMediaTypes::FromString(playlist.GetType());
+
+ std::string baseDir = strBaseDir;
+ if (strBaseDir.empty())
+ {
+ if (mediaType == MediaTypeTvShow || mediaType == MediaTypeEpisode)
+ baseDir = "videodb://tvshows/";
+ else if (mediaType == MediaTypeMovie)
+ baseDir = "videodb://movies/";
+ else
+ return false;
+
+ if (!isGrouped)
+ baseDir += "titles";
+ else
+ baseDir += group;
+ URIUtils::AddSlashAtEnd(baseDir);
+
+ if (mediaType == MediaTypeEpisode)
+ baseDir += "-1/-1/";
+ }
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(baseDir))
+ return false;
+
+ // store the smartplaylist as JSON in the URL as well
+ std::string xsp;
+ if (!playlist.IsEmpty(filter))
+ {
+ if (!playlist.SaveAsJson(xsp, !filter))
+ return false;
+ }
+
+ if (!xsp.empty())
+ videoUrl.AddOption(option, xsp);
+ else
+ videoUrl.RemoveOption(option);
+
+ CDatabase::Filter dbfilter;
+ success = db.GetItems(videoUrl.ToString(), items, dbfilter, sorting);
+ db.Close();
+
+ // if we retrieve a list of episodes and we didn't receive
+ // a pre-defined base path, we need to fix it
+ if (strBaseDir.empty() && mediaType == MediaTypeEpisode && !isGrouped)
+ videoUrl.AppendPath("-1/-1/");
+ items.SetProperty(PROPERTY_PATH_DB, videoUrl.ToString());
+ }
+ }
+ else if (playlist.IsMusicType() || playlist.GetType().empty())
+ {
+ playlistTypeHint = PLAYLIST::TYPE_MUSIC;
+ CMusicDatabase db;
+ if (db.Open())
+ {
+ CSmartPlaylist plist(playlist);
+ if (playlist.GetType() == "mixed" || playlist.GetType().empty())
+ plist.SetType("songs");
+
+ MediaType mediaType = CMediaTypes::FromString(plist.GetType());
+
+ std::string baseDir = strBaseDir;
+ if (strBaseDir.empty())
+ {
+ baseDir = "musicdb://";
+ if (!isGrouped)
+ {
+ if (mediaType == MediaTypeArtist)
+ baseDir += "artists";
+ else if (mediaType == MediaTypeAlbum)
+ baseDir += "albums";
+ else if (mediaType == MediaTypeSong)
+ baseDir += "songs";
+ else
+ return false;
+ }
+ else
+ baseDir += group;
+
+ URIUtils::AddSlashAtEnd(baseDir);
+ }
+
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(baseDir))
+ return false;
+
+ // store the smartplaylist as JSON in the URL as well
+ std::string xsp;
+ if (!plist.IsEmpty(filter))
+ {
+ if (!plist.SaveAsJson(xsp, !filter))
+ return false;
+ }
+
+ if (!xsp.empty())
+ musicUrl.AddOption(option, xsp);
+ else
+ musicUrl.RemoveOption(option);
+
+ CDatabase::Filter dbfilter;
+ success = db.GetItems(musicUrl.ToString(), items, dbfilter, sorting);
+ db.Close();
+
+ items.SetProperty(PROPERTY_PATH_DB, musicUrl.ToString());
+ }
+ }
+
+ if (playlist.GetType() == "musicvideos" || playlist.GetType() == "mixed")
+ {
+ playlistTypeHint = PLAYLIST::TYPE_VIDEO;
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ CSmartPlaylist mvidPlaylist(playlist);
+ if (playlist.GetType() == "mixed")
+ mvidPlaylist.SetType("musicvideos");
+
+ std::string baseDir = strBaseDir;
+ if (baseDir.empty())
+ {
+ baseDir = "videodb://musicvideos/";
+
+ if (!isGrouped)
+ baseDir += "titles";
+ else
+ baseDir += group;
+ URIUtils::AddSlashAtEnd(baseDir);
+ }
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(baseDir))
+ return false;
+
+ // adjust the group in case we're retrieving a grouped playlist
+ // based on artists. This is needed because the video library
+ // is using the actorslink table for artists.
+ if (isGrouped && group == "artists")
+ {
+ group = "actors";
+ mvidPlaylist.SetGroup(group);
+ }
+
+ // store the smartplaylist as JSON in the URL as well
+ std::string xsp;
+ if (!mvidPlaylist.IsEmpty(filter))
+ {
+ if (!mvidPlaylist.SaveAsJson(xsp, !filter))
+ return false;
+ }
+
+ if (!xsp.empty())
+ videoUrl.AddOption(option, xsp);
+ else
+ videoUrl.RemoveOption(option);
+
+ CFileItemList items2;
+ CDatabase::Filter dbfilter;
+ success2 = db.GetItems(videoUrl.ToString(), items2, dbfilter, sorting);
+
+ db.Close();
+ if (items.Size() <= 0)
+ items.SetPath(videoUrl.ToString());
+
+ items.Append(items2);
+ if (items2.Size())
+ {
+ if (items.Size() > items2.Size())
+ items.SetContent("mixed");
+ else
+ items.SetContent("musicvideos");
+ }
+ items.SetProperty(PROPERTY_PATH_DB, videoUrl.ToString());
+ }
+ }
+
+ items.SetLabel(playlist.GetName());
+ if (isGrouped)
+ items.SetContent(group);
+ else
+ items.SetContent(playlist.GetType());
+
+ items.SetProperty(PROPERTY_SORT_ORDER, (int)playlist.GetOrder());
+ items.SetProperty(PROPERTY_SORT_ASCENDING, playlist.GetOrderDirection() == SortOrderAscending);
+ if (!group.empty())
+ {
+ items.SetProperty(PROPERTY_GROUP_BY, group);
+ items.SetProperty(PROPERTY_GROUP_MIXED, playlist.IsGroupMixed());
+ }
+
+ // sort grouped list by label
+ if (items.Size() > 1 && !group.empty())
+ items.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+
+ // go through and set the playlist order
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ item->m_iprogramCount = i; // hack for playlist order
+ item->SetProperty("playlist_type_hint", playlistTypeHint);
+ }
+
+ if (playlist.GetType() == "mixed")
+ return success || success2;
+ else if (playlist.GetType() == "musicvideos")
+ return success2;
+ else
+ return success;
+ }
+
+ bool CSmartPlaylistDirectory::ContainsFiles(const CURL& url)
+ {
+ // smart playlists always have files??
+ return true;
+ }
+
+ std::string CSmartPlaylistDirectory::GetPlaylistByName(const std::string& name, const std::string& playlistType)
+ {
+ CFileItemList list;
+ bool filesExist = false;
+ if (CSmartPlaylist::IsMusicType(playlistType))
+ filesExist = CDirectory::GetDirectory("special://musicplaylists/", list, ".xsp", DIR_FLAG_DEFAULTS);
+ else // all others are video
+ filesExist = CDirectory::GetDirectory("special://videoplaylists/", list, ".xsp", DIR_FLAG_DEFAULTS);
+ if (filesExist)
+ {
+ for (int i = 0; i < list.Size(); i++)
+ {
+ CFileItemPtr item = list[i];
+ CSmartPlaylist playlist;
+ if (playlist.OpenAndReadName(item->GetURL()))
+ {
+ if (StringUtils::EqualsNoCase(playlist.GetName(), name))
+ return item->GetPath();
+ }
+ }
+ for (int i = 0; i < list.Size(); i++)
+ { // check based on filename
+ CFileItemPtr item = list[i];
+ if (URIUtils::GetFileName(item->GetPath()) == name)
+ { // found :)
+ return item->GetPath();
+ }
+ }
+ }
+ return "";
+ }
+
+ bool CSmartPlaylistDirectory::Remove(const CURL& url)
+ {
+ return XFILE::CFile::Delete(url);
+ }
+}
+
+
+
diff --git a/xbmc/filesystem/SmartPlaylistDirectory.h b/xbmc/filesystem/SmartPlaylistDirectory.h
new file mode 100644
index 0000000..145c262
--- /dev/null
+++ b/xbmc/filesystem/SmartPlaylistDirectory.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "IFileDirectory.h"
+
+#include <string>
+
+class CSmartPlaylist;
+
+namespace XFILE
+{
+ class CSmartPlaylistDirectory : public IFileDirectory
+ {
+ public:
+ CSmartPlaylistDirectory();
+ ~CSmartPlaylistDirectory() override;
+ bool GetDirectory(const CURL& url, CFileItemList& items) override;
+ bool AllowAll() const override { return true; }
+ bool ContainsFiles(const CURL& url) override;
+ bool Remove(const CURL& url) override;
+
+ static bool GetDirectory(const CSmartPlaylist &playlist, CFileItemList& items, const std::string &strBaseDir = "", bool filter = false);
+
+ static std::string GetPlaylistByName(const std::string& name, const std::string& playlistType);
+ };
+}
diff --git a/xbmc/filesystem/SourcesDirectory.cpp b/xbmc/filesystem/SourcesDirectory.cpp
new file mode 100644
index 0000000..b83ee2e
--- /dev/null
+++ b/xbmc/filesystem/SourcesDirectory.cpp
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2005-2020 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 "SourcesDirectory.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "guilib/TextureManager.h"
+#include "media/MediaLockState.h"
+#include "profiles/ProfileManager.h"
+#include "settings/MediaSourceSettings.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE;
+
+CSourcesDirectory::CSourcesDirectory(void) = default;
+
+CSourcesDirectory::~CSourcesDirectory(void) = default;
+
+bool CSourcesDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ // break up our path
+ // format is: sources://<type>/
+ std::string type(url.GetFileName());
+ URIUtils::RemoveSlashAtEnd(type);
+
+ VECSOURCES sources;
+ VECSOURCES *sourcesFromType = CMediaSourceSettings::GetInstance().GetSources(type);
+ if (!sourcesFromType)
+ return false;
+
+ sources = *sourcesFromType;
+ CServiceBroker::GetMediaManager().GetRemovableDrives(sources);
+
+ return GetDirectory(sources, items);
+}
+
+bool CSourcesDirectory::GetDirectory(const VECSOURCES &sources, CFileItemList &items)
+{
+ for (unsigned int i = 0; i < sources.size(); ++i)
+ {
+ const CMediaSource& share = sources[i];
+ CFileItemPtr pItem(new CFileItem(share));
+ if (URIUtils::IsProtocol(pItem->GetPath(), "musicsearch"))
+ pItem->SetCanQueue(false);
+
+ std::string strIcon;
+ // We have the real DVD-ROM, set icon on disktype
+ if (share.m_iDriveType == CMediaSource::SOURCE_TYPE_DVD && share.m_strThumbnailImage.empty())
+ {
+ CUtil::GetDVDDriveIcon( pItem->GetPath(), strIcon );
+ // CDetectDVDMedia::SetNewDVDShareUrl() caches disc thumb as special://temp/dvdicon.tbn
+ std::string strThumb = "special://temp/dvdicon.tbn";
+ if (CFileUtils::Exists(strThumb))
+ pItem->SetArt("thumb", strThumb);
+ }
+ else if (URIUtils::IsProtocol(pItem->GetPath(), "addons"))
+ strIcon = "DefaultHardDisk.png";
+ else if ( pItem->IsPath("special://musicplaylists/")
+ || pItem->IsPath("special://videoplaylists/"))
+ strIcon = "DefaultPlaylist.png";
+ else if ( pItem->IsVideoDb()
+ || pItem->IsMusicDb()
+ || pItem->IsPlugin()
+ || pItem->IsPath("musicsearch://"))
+ strIcon = "DefaultFolder.png";
+ else if (pItem->IsRemote())
+ strIcon = "DefaultNetwork.png";
+ else if (pItem->IsISO9660())
+ strIcon = "DefaultDVDRom.png";
+ else if (pItem->IsDVD())
+ strIcon = "DefaultDVDFull.png";
+ else if (pItem->IsBluray())
+ strIcon = "DefaultBluray.png";
+ else if (pItem->IsCDDA())
+ strIcon = "DefaultCDDA.png";
+ else if (pItem->IsRemovable() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture("DefaultRemovableDisk.png"))
+ strIcon = "DefaultRemovableDisk.png";
+ else
+ strIcon = "DefaultHardDisk.png";
+
+ pItem->SetArt("icon", strIcon);
+ if (share.m_iHasLock == LOCK_STATE_LOCKED &&
+ m_profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE)
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_LOCKED);
+ else
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_NONE);
+
+ items.Add(pItem);
+ }
+ return true;
+}
+
+bool CSourcesDirectory::Exists(const CURL& url)
+{
+ return true;
+}
diff --git a/xbmc/filesystem/SourcesDirectory.h b/xbmc/filesystem/SourcesDirectory.h
new file mode 100644
index 0000000..84df8f0
--- /dev/null
+++ b/xbmc/filesystem/SourcesDirectory.h
@@ -0,0 +1,30 @@
+/*
+ * 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 "IDirectory.h"
+
+#include <vector>
+
+class CMediaSource;
+typedef std::vector<CMediaSource> VECSOURCES;
+
+namespace XFILE
+{
+ class CSourcesDirectory : public IDirectory
+ {
+ public:
+ CSourcesDirectory(void);
+ ~CSourcesDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool GetDirectory(const VECSOURCES &sources, CFileItemList &items);
+ bool Exists(const CURL& url) override;
+ bool AllowAll() const override { return true; }
+ };
+}
diff --git a/xbmc/filesystem/SpecialProtocol.cpp b/xbmc/filesystem/SpecialProtocol.cpp
new file mode 100644
index 0000000..1828773
--- /dev/null
+++ b/xbmc/filesystem/SpecialProtocol.cpp
@@ -0,0 +1,304 @@
+/*
+ * 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 "SpecialProtocol.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <cassert>
+
+#include "PlatformDefs.h"
+#ifdef TARGET_POSIX
+#include <dirent.h>
+#include "utils/StringUtils.h"
+#endif
+
+const CProfileManager *CSpecialProtocol::m_profileManager = nullptr;
+
+void CSpecialProtocol::RegisterProfileManager(const CProfileManager &profileManager)
+{
+ m_profileManager = &profileManager;
+}
+
+void CSpecialProtocol::UnregisterProfileManager()
+{
+ m_profileManager = nullptr;
+}
+
+void CSpecialProtocol::SetProfilePath(const std::string &dir)
+{
+ SetPath("profile", dir);
+ CLog::Log(LOGINFO, "special://profile/ is mapped to: {}", GetPath("profile"));
+}
+
+void CSpecialProtocol::SetXBMCPath(const std::string &dir)
+{
+ SetPath("xbmc", dir);
+}
+
+void CSpecialProtocol::SetXBMCBinPath(const std::string &dir)
+{
+ SetPath("xbmcbin", dir);
+}
+
+void CSpecialProtocol::SetXBMCBinAddonPath(const std::string &dir)
+{
+ SetPath("xbmcbinaddons", dir);
+}
+
+void CSpecialProtocol::SetXBMCAltBinAddonPath(const std::string &dir)
+{
+ SetPath("xbmcaltbinaddons", dir);
+}
+
+void CSpecialProtocol::SetXBMCFrameworksPath(const std::string &dir)
+{
+ SetPath("frameworks", dir);
+}
+
+void CSpecialProtocol::SetHomePath(const std::string &dir)
+{
+ SetPath("home", dir);
+}
+
+void CSpecialProtocol::SetUserHomePath(const std::string &dir)
+{
+ SetPath("userhome", dir);
+}
+
+void CSpecialProtocol::SetEnvHomePath(const std::string &dir)
+{
+ SetPath("envhome", dir);
+}
+
+void CSpecialProtocol::SetMasterProfilePath(const std::string &dir)
+{
+ SetPath("masterprofile", dir);
+}
+
+void CSpecialProtocol::SetTempPath(const std::string &dir)
+{
+ SetPath("temp", dir);
+}
+
+void CSpecialProtocol::SetLogPath(const std::string &dir)
+{
+ SetPath("logpath", dir);
+}
+
+bool CSpecialProtocol::ComparePath(const std::string &path1, const std::string &path2)
+{
+ return TranslatePath(path1) == TranslatePath(path2);
+}
+
+std::string CSpecialProtocol::TranslatePath(const std::string &path)
+{
+ CURL url(path);
+ // check for special-protocol, if not, return
+ if (!url.IsProtocol("special"))
+ {
+ return path;
+ }
+ return TranslatePath(url);
+}
+
+std::string CSpecialProtocol::TranslatePath(const CURL &url)
+{
+ // check for special-protocol, if not, return
+ if (!url.IsProtocol("special"))
+ {
+#if defined(TARGET_POSIX) && defined(_DEBUG)
+ std::string path(url.Get());
+ if (path.length() >= 2 && path[1] == ':')
+ {
+ CLog::Log(LOGWARNING, "Trying to access old style dir: {}", path);
+ // printf("Trying to access old style dir: %s\n", path.c_str());
+ }
+#endif
+
+ return url.Get();
+ }
+
+ const std::string& FullFileName = url.GetFileName();
+
+ std::string translatedPath;
+ std::string FileName;
+ std::string RootDir;
+
+ // Split up into the special://root and the rest of the filename
+ size_t pos = FullFileName.find('/');
+ if (pos != std::string::npos && pos > 1)
+ {
+ RootDir = FullFileName.substr(0, pos);
+
+ if (pos < FullFileName.size())
+ FileName = FullFileName.substr(pos + 1);
+ }
+ else
+ RootDir = FullFileName;
+
+ if (RootDir == "subtitles")
+ translatedPath = URIUtils::AddFileToFolder(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_CUSTOMPATH), FileName);
+ else if (RootDir == "userdata" && m_profileManager)
+ translatedPath = URIUtils::AddFileToFolder(m_profileManager->GetUserDataFolder(), FileName);
+ else if (RootDir == "database" && m_profileManager)
+ translatedPath = URIUtils::AddFileToFolder(m_profileManager->GetDatabaseFolder(), FileName);
+ else if (RootDir == "thumbnails" && m_profileManager)
+ translatedPath = URIUtils::AddFileToFolder(m_profileManager->GetThumbnailsFolder(), FileName);
+ else if (RootDir == "recordings" || RootDir == "cdrips")
+ translatedPath = URIUtils::AddFileToFolder(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_AUDIOCDS_RECORDINGPATH), FileName);
+ else if (RootDir == "screenshots")
+ translatedPath = URIUtils::AddFileToFolder(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_DEBUG_SCREENSHOTPATH), FileName);
+ else if (RootDir == "musicartistsinfo")
+ translatedPath = URIUtils::AddFileToFolder(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER), FileName);
+ else if (RootDir == "musicplaylists")
+ translatedPath = URIUtils::AddFileToFolder(CUtil::MusicPlaylistsLocation(), FileName);
+ else if (RootDir == "videoplaylists")
+ translatedPath = URIUtils::AddFileToFolder(CUtil::VideoPlaylistsLocation(), FileName);
+ else if (RootDir == "skin")
+ {
+ auto winSystem = CServiceBroker::GetWinSystem();
+ // windowing may not have been initialized yet
+ if (winSystem)
+ translatedPath = URIUtils::AddFileToFolder(winSystem->GetGfxContext().GetMediaDir(), FileName);
+ }
+ // from here on, we have our "real" special paths
+ else if (RootDir == "xbmc" ||
+ RootDir == "xbmcbin" ||
+ RootDir == "xbmcbinaddons" ||
+ RootDir == "xbmcaltbinaddons" ||
+ RootDir == "home" ||
+ RootDir == "envhome" ||
+ RootDir == "userhome" ||
+ RootDir == "temp" ||
+ RootDir == "profile" ||
+ RootDir == "masterprofile" ||
+ RootDir == "frameworks" ||
+ RootDir == "logpath")
+ {
+ std::string basePath = GetPath(RootDir);
+ if (!basePath.empty())
+ translatedPath = URIUtils::AddFileToFolder(basePath, FileName);
+ else
+ translatedPath.clear();
+ }
+
+ // check if we need to recurse in
+ if (URIUtils::IsSpecial(translatedPath))
+ { // we need to recurse in, as there may be multiple translations required
+ return TranslatePath(translatedPath);
+ }
+
+ // Validate the final path, just in case
+ return CUtil::ValidatePath(translatedPath);
+}
+
+std::string CSpecialProtocol::TranslatePathConvertCase(const std::string& path)
+{
+ std::string translatedPath = TranslatePath(path);
+
+#ifdef TARGET_POSIX
+ if (translatedPath.find("://") != std::string::npos)
+ return translatedPath;
+
+ // If the file exists with the requested name, simply return it
+ struct stat stat_buf;
+ if (stat(translatedPath.c_str(), &stat_buf) == 0)
+ return translatedPath;
+
+ std::string result;
+ std::vector<std::string> tokens;
+ StringUtils::Tokenize(translatedPath, tokens, "/");
+ std::string file;
+ DIR* dir;
+ struct dirent* de;
+
+ for (unsigned int i = 0; i < tokens.size(); i++)
+ {
+ file = result + "/";
+ file += tokens[i];
+ if (stat(file.c_str(), &stat_buf) == 0)
+ {
+ result += "/" + tokens[i];
+ }
+ else
+ {
+ dir = opendir(result.c_str());
+ if (dir)
+ {
+ while ((de = readdir(dir)) != NULL)
+ {
+ // check if there's a file with same name but different case
+ if (StringUtils::CompareNoCase(de->d_name, tokens[i]) == 0)
+ {
+ result += "/";
+ result += de->d_name;
+ break;
+ }
+ }
+
+ // if we did not find any file that somewhat matches, just
+ // fallback but we know it's not gonna be a good ending
+ if (de == NULL)
+ result += "/" + tokens[i];
+
+ closedir(dir);
+ }
+ else
+ { // this is just fallback, we won't succeed anyway...
+ result += "/" + tokens[i];
+ }
+ }
+ }
+
+ return result;
+#else
+ return translatedPath;
+#endif
+}
+
+void CSpecialProtocol::LogPaths()
+{
+ CLog::Log(LOGINFO, "special://xbmc/ is mapped to: {}", GetPath("xbmc"));
+ CLog::Log(LOGINFO, "special://xbmcbin/ is mapped to: {}", GetPath("xbmcbin"));
+ CLog::Log(LOGINFO, "special://xbmcbinaddons/ is mapped to: {}", GetPath("xbmcbinaddons"));
+ CLog::Log(LOGINFO, "special://masterprofile/ is mapped to: {}", GetPath("masterprofile"));
+#if defined(TARGET_POSIX)
+ CLog::Log(LOGINFO, "special://envhome/ is mapped to: {}", GetPath("envhome"));
+#endif
+ CLog::Log(LOGINFO, "special://home/ is mapped to: {}", GetPath("home"));
+ CLog::Log(LOGINFO, "special://temp/ is mapped to: {}", GetPath("temp"));
+ CLog::Log(LOGINFO, "special://logpath/ is mapped to: {}", GetPath("logpath"));
+ //CLog::Log(LOGINFO, "special://userhome/ is mapped to: {}", GetPath("userhome"));
+ if (!CUtil::GetFrameworksPath().empty())
+ CLog::Log(LOGINFO, "special://frameworks/ is mapped to: {}", GetPath("frameworks"));
+}
+
+// private routines, to ensure we only set/get an appropriate path
+void CSpecialProtocol::SetPath(const std::string &key, const std::string &path)
+{
+ m_pathMap[key] = path;
+}
+
+std::string CSpecialProtocol::GetPath(const std::string &key)
+{
+ std::map<std::string, std::string>::iterator it = m_pathMap.find(key);
+ if (it != m_pathMap.end())
+ return it->second;
+ assert(false);
+ return "";
+}
diff --git a/xbmc/filesystem/SpecialProtocol.h b/xbmc/filesystem/SpecialProtocol.h
new file mode 100644
index 0000000..2d0cec8
--- /dev/null
+++ b/xbmc/filesystem/SpecialProtocol.h
@@ -0,0 +1,85 @@
+/*
+ * 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 <map>
+#include <string>
+
+class CProfileManager;
+
+// static class for path translation from our special:// URLs.
+
+/* paths are as follows:
+
+ special://xbmc/ - the main XBMC folder (i.e. where the app resides).
+ special://home/ - a writeable version of the main XBMC folder
+ Linux: ~/.kodi/
+ OS X: ~/Library/Application Support/Kodi/
+ Win32: ~/Application Data/XBMC/
+ special://envhome/ - on posix systems this will be equal to the $HOME
+ special://userhome/ - a writable version of the user home directory
+ Linux, OS X: ~/.kodi
+ Win32: home directory of user
+ special://masterprofile/ - the master users userdata folder - usually special://home/userdata
+ Linux: ~/.kodi/userdata/
+ OS X: ~/Library/Application Support/Kodi/UserData/
+ Win32: ~/Application Data/XBMC/UserData/
+ special://profile/ - the current users userdata folder - usually special://masterprofile/profiles/<current_profile>
+ Linux: ~/.kodi/userdata/profiles/<current_profile>
+ OS X: ~/Library/Application Support/Kodi/UserData/profiles/<current_profile>
+ Win32: ~/Application Data/XBMC/UserData/profiles/<current_profile>
+
+ special://temp/ - the temporary directory.
+ Linux: ~/.kodi/temp
+ OS X: ~/
+ Win32: ~/Application Data/XBMC/cache
+*/
+class CURL;
+class CSpecialProtocol
+{
+public:
+ static void RegisterProfileManager(const CProfileManager &profileManager);
+ static void UnregisterProfileManager();
+
+ static void SetProfilePath(const std::string &path);
+ static void SetXBMCPath(const std::string &path);
+ static void SetXBMCBinPath(const std::string &path);
+ static void SetXBMCBinAddonPath(const std::string &path);
+ static void SetXBMCAltBinAddonPath(const std::string &path);
+ static void SetXBMCFrameworksPath(const std::string &path);
+ static void SetHomePath(const std::string &path);
+ static void SetUserHomePath(const std::string &path);
+ static void SetEnvHomePath(const std::string &path);
+ static void SetMasterProfilePath(const std::string &path);
+ static void SetTempPath(const std::string &path);
+ static void SetLogPath(const std::string &dir);
+
+ static bool ComparePath(const std::string &path1, const std::string &path2);
+ static void LogPaths();
+
+ static std::string TranslatePath(const std::string &path);
+ static std::string TranslatePath(const CURL &url);
+ static std::string TranslatePathConvertCase(const std::string& path);
+
+private:
+ static const CProfileManager *m_profileManager;
+
+ static void SetPath(const std::string &key, const std::string &path);
+ static std::string GetPath(const std::string &key);
+
+ static std::map<std::string, std::string> m_pathMap;
+};
+
+#ifdef TARGET_WINDOWS
+#define PATH_SEPARATOR_CHAR '\\'
+#define PATH_SEPARATOR_STRING "\\"
+#else
+#define PATH_SEPARATOR_CHAR '/'
+#define PATH_SEPARATOR_STRING "/"
+#endif
diff --git a/xbmc/filesystem/SpecialProtocolDirectory.cpp b/xbmc/filesystem/SpecialProtocolDirectory.cpp
new file mode 100644
index 0000000..e4a970e
--- /dev/null
+++ b/xbmc/filesystem/SpecialProtocolDirectory.cpp
@@ -0,0 +1,44 @@
+/*
+ * 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 "SpecialProtocolDirectory.h"
+
+#include "Directory.h"
+#include "FileItem.h"
+#include "SpecialProtocol.h"
+#include "URL.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE;
+
+CSpecialProtocolDirectory::CSpecialProtocolDirectory(void) = default;
+
+CSpecialProtocolDirectory::~CSpecialProtocolDirectory(void) = default;
+
+bool CSpecialProtocolDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ const std::string pathToUrl(url.Get());
+ std::string translatedPath = CSpecialProtocol::TranslatePath(url);
+ if (CDirectory::GetDirectory(translatedPath, items, m_strFileMask, m_flags | DIR_FLAG_GET_HIDDEN))
+ { // replace our paths as necessary
+ items.SetURL(url);
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ if (URIUtils::PathHasParent(item->GetPath(), translatedPath))
+ item->SetPath(URIUtils::AddFileToFolder(pathToUrl, item->GetPath().substr(translatedPath.size())));
+ }
+ return true;
+ }
+ return false;
+}
+
+std::string CSpecialProtocolDirectory::TranslatePath(const CURL &url)
+{
+ return CSpecialProtocol::TranslatePath(url);
+}
diff --git a/xbmc/filesystem/SpecialProtocolDirectory.h b/xbmc/filesystem/SpecialProtocolDirectory.h
new file mode 100644
index 0000000..97b3613
--- /dev/null
+++ b/xbmc/filesystem/SpecialProtocolDirectory.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "filesystem/OverrideDirectory.h"
+
+namespace XFILE
+{
+ class CSpecialProtocolDirectory : public COverrideDirectory
+ {
+ public:
+ CSpecialProtocolDirectory(void);
+ ~CSpecialProtocolDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+
+ protected:
+ std::string TranslatePath(const CURL &url) override;
+ };
+}
diff --git a/xbmc/filesystem/SpecialProtocolFile.cpp b/xbmc/filesystem/SpecialProtocolFile.cpp
new file mode 100644
index 0000000..9ef367d
--- /dev/null
+++ b/xbmc/filesystem/SpecialProtocolFile.cpp
@@ -0,0 +1,25 @@
+/*
+ * 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 "SpecialProtocolFile.h"
+
+#include "URL.h"
+#include "filesystem/SpecialProtocol.h"
+
+using namespace XFILE;
+
+CSpecialProtocolFile::CSpecialProtocolFile(void)
+ : COverrideFile(true)
+{ }
+
+CSpecialProtocolFile::~CSpecialProtocolFile(void) = default;
+
+std::string CSpecialProtocolFile::TranslatePath(const CURL& url)
+{
+ return CSpecialProtocol::TranslatePath(url);
+}
diff --git a/xbmc/filesystem/SpecialProtocolFile.h b/xbmc/filesystem/SpecialProtocolFile.h
new file mode 100644
index 0000000..58fd649
--- /dev/null
+++ b/xbmc/filesystem/SpecialProtocolFile.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "filesystem/OverrideFile.h"
+
+namespace XFILE
+{
+class CSpecialProtocolFile : public COverrideFile
+{
+public:
+ CSpecialProtocolFile(void);
+ ~CSpecialProtocolFile(void) override;
+
+protected:
+ std::string TranslatePath(const CURL& url) override;
+};
+}
diff --git a/xbmc/filesystem/StackDirectory.cpp b/xbmc/filesystem/StackDirectory.cpp
new file mode 100644
index 0000000..a752a94
--- /dev/null
+++ b/xbmc/filesystem/StackDirectory.cpp
@@ -0,0 +1,232 @@
+/*
+ * 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 "StackDirectory.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <stdlib.h>
+
+namespace XFILE
+{
+ CStackDirectory::CStackDirectory() = default;
+
+ CStackDirectory::~CStackDirectory() = default;
+
+ bool CStackDirectory::GetDirectory(const CURL& url, CFileItemList& items)
+ {
+ items.Clear();
+ std::vector<std::string> files;
+ const std::string pathToUrl(url.Get());
+ if (!GetPaths(pathToUrl, files))
+ return false; // error in path
+
+ for (const std::string& i : files)
+ {
+ CFileItemPtr item(new CFileItem(i));
+ item->SetPath(i);
+ item->m_bIsFolder = false;
+ items.Add(item);
+ }
+ return true;
+ }
+
+ std::string CStackDirectory::GetStackedTitlePath(const std::string &strPath)
+ {
+ // Load up our REs
+ VECCREGEXP RegExps;
+ CRegExp tempRE(true, CRegExp::autoUtf8);
+ const std::vector<std::string>& strRegExps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoStackRegExps;
+ std::vector<std::string>::const_iterator itRegExp = strRegExps.begin();
+ while (itRegExp != strRegExps.end())
+ {
+ (void)tempRE.RegComp(*itRegExp);
+ if (tempRE.GetCaptureTotal() == 4)
+ RegExps.push_back(tempRE);
+ else
+ CLog::Log(LOGERROR, "Invalid video stack RE ({}). Must have exactly 4 captures.",
+ itRegExp->c_str());
+ ++itRegExp;
+ }
+ return GetStackedTitlePath(strPath, RegExps);
+ }
+
+ std::string CStackDirectory::GetStackedTitlePath(const std::string &strPath, VECCREGEXP& RegExps)
+ {
+ CStackDirectory stack;
+ CFileItemList files;
+ std::string strStackTitlePath,
+ strCommonDir = URIUtils::GetParentPath(strPath);
+
+ const CURL pathToUrl(strPath);
+ stack.GetDirectory(pathToUrl, files);
+
+ if (files.Size() > 1)
+ {
+ std::string strStackTitle;
+
+ std::string File1 = URIUtils::GetFileName(files[0]->GetPath());
+ std::string File2 = URIUtils::GetFileName(files[1]->GetPath());
+ // Check if source path uses URL encoding
+ if (URIUtils::HasEncodedFilename(CURL(strCommonDir)))
+ {
+ File1 = CURL::Decode(File1);
+ File2 = CURL::Decode(File2);
+ }
+
+ std::vector<CRegExp>::iterator itRegExp = RegExps.begin();
+ int offset = 0;
+
+ while (itRegExp != RegExps.end())
+ {
+ if (itRegExp->RegFind(File1, offset) != -1)
+ {
+ std::string Title1 = itRegExp->GetMatch(1),
+ Volume1 = itRegExp->GetMatch(2),
+ Ignore1 = itRegExp->GetMatch(3),
+ Extension1 = itRegExp->GetMatch(4);
+ if (offset)
+ Title1 = File1.substr(0, itRegExp->GetSubStart(2));
+ if (itRegExp->RegFind(File2, offset) != -1)
+ {
+ std::string Title2 = itRegExp->GetMatch(1),
+ Volume2 = itRegExp->GetMatch(2),
+ Ignore2 = itRegExp->GetMatch(3),
+ Extension2 = itRegExp->GetMatch(4);
+ if (offset)
+ Title2 = File2.substr(0, itRegExp->GetSubStart(2));
+ if (StringUtils::EqualsNoCase(Title1, Title2))
+ {
+ if (!StringUtils::EqualsNoCase(Volume1, Volume2))
+ {
+ if (StringUtils::EqualsNoCase(Ignore1, Ignore2) &&
+ StringUtils::EqualsNoCase(Extension1, Extension2))
+ {
+ // got it
+ strStackTitle = Title1 + Ignore1 + Extension1;
+ // Check if source path uses URL encoding
+ if (URIUtils::HasEncodedFilename(CURL(strCommonDir)))
+ strStackTitle = CURL::Encode(strStackTitle);
+
+ itRegExp = RegExps.end();
+ break;
+ }
+ else // Invalid stack
+ break;
+ }
+ else // Early match, retry with offset
+ {
+ offset = itRegExp->GetSubStart(3);
+ continue;
+ }
+ }
+ }
+ }
+ offset = 0;
+ ++itRegExp;
+ }
+ if (!strCommonDir.empty() && !strStackTitle.empty())
+ strStackTitlePath = strCommonDir + strStackTitle;
+ }
+
+ return strStackTitlePath;
+ }
+
+ std::string CStackDirectory::GetFirstStackedFile(const std::string &strPath)
+ {
+ // the stacked files are always in volume order, so just get up to the first filename
+ // occurrence of " , "
+ std::string file, folder;
+ size_t pos = strPath.find(" , ");
+ if (pos != std::string::npos)
+ URIUtils::Split(strPath.substr(0, pos), folder, file);
+ else
+ URIUtils::Split(strPath, folder, file); // single filed stacks - should really not happen
+
+ // remove "stack://" from the folder
+ folder = folder.substr(8);
+ StringUtils::Replace(file, ",,", ",");
+
+ return URIUtils::AddFileToFolder(folder, file);
+ }
+
+ bool CStackDirectory::GetPaths(const std::string& strPath, std::vector<std::string>& vecPaths)
+ {
+ // format is:
+ // stack://file1 , file2 , file3 , file4
+ // filenames with commas are double escaped (ie replaced with ,,), thus the " , " separator used.
+ std::string path = strPath;
+ // remove stack:// from the beginning
+ path = path.substr(8);
+
+ vecPaths = StringUtils::Split(path, " , ");
+ if (vecPaths.empty())
+ return false;
+
+ // because " , " is used as a separator any "," in the real paths are double escaped
+ for (std::string& itPath : vecPaths)
+ StringUtils::Replace(itPath, ",,", ",");
+
+ return true;
+ }
+
+ std::string CStackDirectory::ConstructStackPath(const CFileItemList &items, const std::vector<int> &stack)
+ {
+ // no checks on the range of stack here.
+ // we replace all instances of comma's with double comma's, then separate
+ // the files using " , ".
+ std::string stackedPath = "stack://";
+ std::string folder, file;
+ URIUtils::Split(items[stack[0]]->GetPath(), folder, file);
+ stackedPath += folder;
+ // double escape any occurrence of commas
+ StringUtils::Replace(file, ",", ",,");
+ stackedPath += file;
+ for (unsigned int i = 1; i < stack.size(); ++i)
+ {
+ stackedPath += " , ";
+ file = items[stack[i]]->GetPath();
+
+ // double escape any occurrence of commas
+ StringUtils::Replace(file, ",", ",,");
+ stackedPath += file;
+ }
+ return stackedPath;
+ }
+
+ bool CStackDirectory::ConstructStackPath(const std::vector<std::string> &paths, std::string& stackedPath)
+ {
+ if (paths.size() < 2)
+ return false;
+ stackedPath = "stack://";
+ std::string folder, file;
+ URIUtils::Split(paths[0], folder, file);
+ stackedPath += folder;
+ // double escape any occurrence of commas
+ StringUtils::Replace(file, ",", ",,");
+ stackedPath += file;
+ for (unsigned int i = 1; i < paths.size(); ++i)
+ {
+ stackedPath += " , ";
+ file = paths[i];
+
+ // double escape any occurrence of commas
+ StringUtils::Replace(file, ",", ",,");
+ stackedPath += file;
+ }
+ return true;
+ }
+}
+
diff --git a/xbmc/filesystem/StackDirectory.h b/xbmc/filesystem/StackDirectory.h
new file mode 100644
index 0000000..5f61fe3
--- /dev/null
+++ b/xbmc/filesystem/StackDirectory.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "IDirectory.h"
+#include "utils/RegExp.h"
+
+#include <string>
+#include <vector>
+
+namespace XFILE
+{
+ class CStackDirectory : public IDirectory
+ {
+ public:
+ CStackDirectory();
+ ~CStackDirectory() override;
+ bool GetDirectory(const CURL& url, CFileItemList& items) override;
+ bool AllowAll() const override { return true; }
+ static std::string GetStackedTitlePath(const std::string &strPath);
+ static std::string GetStackedTitlePath(const std::string &strPath, VECCREGEXP& RegExps);
+ static std::string GetFirstStackedFile(const std::string &strPath);
+ static bool GetPaths(const std::string& strPath, std::vector<std::string>& vecPaths);
+ static std::string ConstructStackPath(const CFileItemList& items, const std::vector<int> &stack);
+ static bool ConstructStackPath(const std::vector<std::string> &paths, std::string &stackedPath);
+ };
+}
diff --git a/xbmc/filesystem/UDFBlockInput.cpp b/xbmc/filesystem/UDFBlockInput.cpp
new file mode 100644
index 0000000..b4eb265
--- /dev/null
+++ b/xbmc/filesystem/UDFBlockInput.cpp
@@ -0,0 +1,73 @@
+/*
+ * 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 "UDFBlockInput.h"
+
+#include "filesystem/File.h"
+
+#include <mutex>
+
+#include <udfread/udfread.h>
+
+int CUDFBlockInput::Close(udfread_block_input* bi)
+{
+ auto m_bi = reinterpret_cast<UDF_BI*>(bi);
+
+ m_bi->fp->Close();
+
+ return 0;
+}
+
+uint32_t CUDFBlockInput::Size(udfread_block_input* bi)
+{
+ auto m_bi = reinterpret_cast<UDF_BI*>(bi);
+
+ return static_cast<uint32_t>(m_bi->fp->GetLength() / UDF_BLOCK_SIZE);
+}
+
+int CUDFBlockInput::Read(
+ udfread_block_input* bi, uint32_t lba, void* buf, uint32_t blocks, int flags)
+{
+ auto m_bi = reinterpret_cast<UDF_BI*>(bi);
+ std::unique_lock<CCriticalSection> lock(m_bi->lock);
+
+ int64_t pos = static_cast<int64_t>(lba) * UDF_BLOCK_SIZE;
+
+ if (m_bi->fp->Seek(pos, SEEK_SET) != pos)
+ return -1;
+
+ ssize_t size = blocks * UDF_BLOCK_SIZE;
+ ssize_t read = m_bi->fp->Read(buf, size);
+ if (read > 0)
+ return static_cast<int>(read / UDF_BLOCK_SIZE);
+
+ return static_cast<int>(read);
+}
+
+udfread_block_input* CUDFBlockInput::GetBlockInput(const std::string& file)
+{
+ auto fp = std::make_shared<XFILE::CFile>();
+
+ if (fp->Open(file))
+ {
+ m_bi = std::make_unique<UDF_BI>();
+ if (m_bi)
+ {
+ m_bi->fp = fp;
+ m_bi->bi.close = CUDFBlockInput::Close;
+ m_bi->bi.read = CUDFBlockInput::Read;
+ m_bi->bi.size = CUDFBlockInput::Size;
+
+ return &m_bi->bi;
+ }
+
+ fp->Close();
+ }
+
+ return nullptr;
+}
diff --git a/xbmc/filesystem/UDFBlockInput.h b/xbmc/filesystem/UDFBlockInput.h
new file mode 100644
index 0000000..e7cfc88
--- /dev/null
+++ b/xbmc/filesystem/UDFBlockInput.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+#include <memory>
+
+#include <udfread/blockinput.h>
+
+namespace XFILE
+{
+class CFile;
+}
+
+class CUDFBlockInput
+{
+public:
+ CUDFBlockInput() = default;
+ ~CUDFBlockInput() = default;
+
+ udfread_block_input* GetBlockInput(const std::string& file);
+
+private:
+ static int Close(udfread_block_input* bi);
+ static uint32_t Size(udfread_block_input* bi);
+ static int Read(udfread_block_input* bi, uint32_t lba, void* buf, uint32_t nblocks, int flags);
+
+ struct UDF_BI
+ {
+ struct udfread_block_input bi;
+ std::shared_ptr<XFILE::CFile> fp{nullptr};
+ CCriticalSection lock;
+ };
+
+ std::unique_ptr<UDF_BI> m_bi{nullptr};
+};
diff --git a/xbmc/filesystem/UDFDirectory.cpp b/xbmc/filesystem/UDFDirectory.cpp
new file mode 100644
index 0000000..236b281
--- /dev/null
+++ b/xbmc/filesystem/UDFDirectory.cpp
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2010 Team Boxee
+ * http://www.boxee.tv
+ *
+ * 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 "UDFDirectory.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/UDFBlockInput.h"
+#include "utils/URIUtils.h"
+
+#include <udfread/udfread.h>
+
+using namespace XFILE;
+
+bool CUDFDirectory::GetDirectory(const CURL& url, CFileItemList& items)
+{
+ CURL url2(url);
+ if (!url2.IsProtocol("udf"))
+ {
+ url2.Reset();
+ url2.SetProtocol("udf");
+ url2.SetHostName(url.Get());
+ }
+
+ std::string strRoot(url2.Get());
+ std::string strSub(url2.GetFileName());
+
+ URIUtils::AddSlashAtEnd(strRoot);
+ URIUtils::AddSlashAtEnd(strSub);
+
+ auto udf = udfread_init();
+
+ if (!udf)
+ return false;
+
+ CUDFBlockInput udfbi;
+
+ auto bi = udfbi.GetBlockInput(url2.GetHostName());
+
+ if (udfread_open_input(udf, bi) < 0)
+ {
+ udfread_close(udf);
+ return false;
+ }
+
+ auto path = udfread_opendir(udf, strSub.c_str());
+ if (!path)
+ {
+ udfread_close(udf);
+ return false;
+ }
+
+ struct udfread_dirent dirent;
+
+ while (udfread_readdir(path, &dirent))
+ {
+ if (dirent.d_type == UDF_DT_DIR)
+ {
+ std::string filename = dirent.d_name;
+ if (filename != "." && filename != "..")
+ {
+ CFileItemPtr pItem(new CFileItem(filename));
+ std::string strDir(strRoot + filename);
+ URIUtils::AddSlashAtEnd(strDir);
+ pItem->SetPath(strDir);
+ pItem->m_bIsFolder = true;
+
+ items.Add(pItem);
+ }
+ }
+ else
+ {
+ std::string filename = dirent.d_name;
+ std::string filenameWithPath{strSub + filename};
+ auto file = udfread_file_open(udf, filenameWithPath.c_str());
+ if (!file)
+ continue;
+
+ CFileItemPtr pItem(new CFileItem(filename));
+ pItem->SetPath(strRoot + filename);
+ pItem->m_bIsFolder = false;
+ pItem->m_dwSize = udfread_file_size(file);
+ items.Add(pItem);
+
+ udfread_file_close(file);
+ }
+ }
+
+ udfread_closedir(path);
+ udfread_close(udf);
+
+ return true;
+}
+
+bool CUDFDirectory::Exists(const CURL& url)
+{
+ CFileItemList items;
+ return GetDirectory(url, items);
+}
diff --git a/xbmc/filesystem/UDFDirectory.h b/xbmc/filesystem/UDFDirectory.h
new file mode 100644
index 0000000..ea4487a
--- /dev/null
+++ b/xbmc/filesystem/UDFDirectory.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 Team Boxee
+ * http://www.boxee.tv
+ *
+ * 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 "IFileDirectory.h"
+
+namespace XFILE
+{
+
+class CUDFDirectory : public IFileDirectory
+{
+public:
+ CUDFDirectory() = default;
+ ~CUDFDirectory() = default;
+ bool GetDirectory(const CURL& url, CFileItemList& items) override;
+ bool Exists(const CURL& url) override;
+ bool ContainsFiles(const CURL& url) override { return true; }
+};
+}
+
diff --git a/xbmc/filesystem/UDFFile.cpp b/xbmc/filesystem/UDFFile.cpp
new file mode 100644
index 0000000..f4c5d5b
--- /dev/null
+++ b/xbmc/filesystem/UDFFile.cpp
@@ -0,0 +1,105 @@
+/*
+ * 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 "UDFFile.h"
+
+#include "URL.h"
+
+#include <udfread/udfread.h>
+
+using namespace XFILE;
+
+CUDFFile::CUDFFile() : m_bi{std::make_unique<CUDFBlockInput>()}
+{
+}
+
+bool CUDFFile::Open(const CURL& url)
+{
+ if (m_udf && m_file)
+ return true;
+
+ m_udf = udfread_init();
+
+ if (!m_udf)
+ return false;
+
+ auto bi = m_bi->GetBlockInput(url.GetHostName());
+
+ if (!bi)
+ {
+ udfread_close(m_udf);
+ return false;
+ }
+
+ if (udfread_open_input(m_udf, bi) < 0)
+ {
+ bi->close(bi);
+ udfread_close(m_udf);
+ return false;
+ }
+
+ m_file = udfread_file_open(m_udf, url.GetFileName().c_str());
+ if (!m_file)
+ {
+ Close();
+ return false;
+ }
+
+ return true;
+}
+
+void CUDFFile::Close()
+{
+ if (m_file)
+ {
+ udfread_file_close(m_file);
+ m_file = nullptr;
+ }
+
+ if (m_udf)
+ {
+ udfread_close(m_udf);
+ m_udf = nullptr;
+ }
+}
+
+int CUDFFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ if (!m_udf || !m_file || !buffer)
+ return -1;
+
+ *buffer = {};
+ buffer->st_size = GetLength();
+
+ return 0;
+}
+
+ssize_t CUDFFile::Read(void* buffer, size_t size)
+{
+ return udfread_file_read(m_file, buffer, size);
+}
+
+int64_t CUDFFile::Seek(int64_t filePosition, int whence)
+{
+ return udfread_file_seek(m_file, filePosition, whence);
+}
+
+int64_t CUDFFile::GetLength()
+{
+ return udfread_file_size(m_file);
+}
+
+int64_t CUDFFile::GetPosition()
+{
+ return udfread_file_tell(m_file);
+}
+
+bool CUDFFile::Exists(const CURL& url)
+{
+ return Open(url);
+}
diff --git a/xbmc/filesystem/UDFFile.h b/xbmc/filesystem/UDFFile.h
new file mode 100644
index 0000000..1617a1f
--- /dev/null
+++ b/xbmc/filesystem/UDFFile.h
@@ -0,0 +1,48 @@
+/*
+ * 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 "IFile.h"
+#include "filesystem/UDFBlockInput.h"
+
+#include <memory>
+
+class udfread;
+typedef struct udfread_file UDFFILE;
+
+namespace XFILE
+{
+
+class CUDFFile : public IFile
+{
+public:
+ CUDFFile();
+ ~CUDFFile() override = default;
+
+ bool Open(const CURL& url) override;
+ void Close() override;
+
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+
+ ssize_t Read(void* buffer, size_t size) override;
+ int64_t Seek(int64_t filePosition, int whence) override;
+
+ int64_t GetLength() override;
+ int64_t GetPosition() override;
+
+ bool Exists(const CURL& url) override;
+
+private:
+ std::unique_ptr<CUDFBlockInput> m_bi{nullptr};
+
+ udfread* m_udf{nullptr};
+ UDFFILE* m_file{nullptr};
+};
+
+} // namespace XFILE
diff --git a/xbmc/filesystem/UPnPDirectory.cpp b/xbmc/filesystem/UPnPDirectory.cpp
new file mode 100644
index 0000000..b93b6bf
--- /dev/null
+++ b/xbmc/filesystem/UPnPDirectory.cpp
@@ -0,0 +1,358 @@
+/*
+ * UPnP Support for XBMC
+ * Copyright (c) 2006 c0diq (Sylvain Rebaud)
+ * Portions Copyright (c) by the authors of libPlatinum
+ * http://www.plutinosoft.com/blog/category/platinum/
+ *
+ * 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 "UPnPDirectory.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "network/upnp/UPnP.h"
+#include "network/upnp/UPnPInternal.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <Platinum/Source/Devices/MediaServer/PltSyncMediaBrowser.h>
+#include <Platinum/Source/Platinum/Platinum.h>
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+using namespace UPNP;
+
+namespace XFILE
+{
+
+static std::string GetContentMapping(NPT_String& objectClass)
+{
+ struct SClassMapping
+ {
+ const char* ObjectClass;
+ const char* Content;
+ };
+ static const SClassMapping mapping[] = {
+ { "object.item.videoItem.videoBroadcast" , "episodes" }
+ , { "object.item.videoItem.musicVideoClip" , "musicvideos" }
+ , { "object.item.videoItem" , "movies" }
+ , { "object.item.audioItem.musicTrack" , "songs" }
+ , { "object.item.audioItem" , "songs" }
+ , { "object.item.imageItem.photo" , "photos" }
+ , { "object.item.imageItem" , "photos" }
+ , { "object.container.album.videoAlbum.videoBroadcastShow" , "tvshows" }
+ , { "object.container.album.videoAlbum.videoBroadcastSeason", "seasons" }
+ , { "object.container.album.musicAlbum" , "albums" }
+ , { "object.container.album.photoAlbum" , "photos" }
+ , { "object.container.album" , "albums" }
+ , { "object.container.person" , "artists" }
+ , { NULL , NULL }
+ };
+ for(const SClassMapping* map = mapping; map->ObjectClass; map++)
+ {
+ if(objectClass.StartsWith(map->ObjectClass, true))
+ {
+ return map->Content;
+ break;
+ }
+ }
+ return "unknown";
+}
+
+static bool FindDeviceWait(CUPnP* upnp, const char* uuid, PLT_DeviceDataReference& device)
+{
+ bool client_started = upnp->IsClientStarted();
+ upnp->StartClient();
+
+ // look for device in our list
+ // (and wait for it to respond for 5 secs if we're just starting upnp client)
+ NPT_TimeStamp watchdog;
+ NPT_System::GetCurrentTimeStamp(watchdog);
+ watchdog += 5.0;
+
+ for (;;) {
+ if (NPT_SUCCEEDED(upnp->m_MediaBrowser->FindServer(uuid, device)) && !device.IsNull())
+ break;
+
+ // fail right away if device not found and upnp client was already running
+ if (client_started)
+ return false;
+
+ // otherwise check if we've waited long enough without success
+ NPT_TimeStamp now;
+ NPT_System::GetCurrentTimeStamp(now);
+ if (now > watchdog)
+ return false;
+
+ // sleep a bit and try again
+ NPT_System::Sleep(NPT_TimeInterval((double)1));
+ }
+
+ return !device.IsNull();
+}
+
+/*----------------------------------------------------------------------
+| CUPnPDirectory::GetFriendlyName
++---------------------------------------------------------------------*/
+std::string CUPnPDirectory::GetFriendlyName(const CURL& url)
+{
+ NPT_String path = url.Get().c_str();
+ if (!path.EndsWith("/")) path += "/";
+
+ if (path.Left(7).Compare("upnp://", true) != 0) {
+ return {};
+ } else if (path.Compare("upnp://", true) == 0) {
+ return "UPnP Media Servers (Auto-Discover)";
+ }
+
+ // look for nextslash
+ int next_slash = path.Find('/', 7);
+ if (next_slash == -1)
+ return {};
+
+ NPT_String uuid = path.SubString(7, next_slash-7);
+ NPT_String object_id = path.SubString(next_slash+1, path.GetLength()-next_slash-2);
+
+ // look for device
+ PLT_DeviceDataReference device;
+ if(!FindDeviceWait(CUPnP::GetInstance(), uuid, device))
+ return {};
+
+ return device->GetFriendlyName().GetChars();
+}
+
+/*----------------------------------------------------------------------
+| CUPnPDirectory::GetDirectory
++---------------------------------------------------------------------*/
+bool CUPnPDirectory::GetResource(const CURL& path, CFileItem &item)
+{
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ return false;
+
+ if(!path.IsProtocol("upnp"))
+ return false;
+
+ CUPnP* upnp = CUPnP::GetInstance();
+ if(!upnp)
+ return false;
+
+ const std::string& uuid = path.GetHostName();
+ std::string object = path.GetFileName();
+ StringUtils::TrimRight(object, "/");
+ object = CURL::Decode(object);
+
+ PLT_DeviceDataReference device;
+ if(!FindDeviceWait(upnp, uuid.c_str(), device)) {
+ CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - unable to find uuid {}", uuid);
+ return false;
+ }
+
+ PLT_MediaObjectListReference list;
+ if (NPT_FAILED(upnp->m_MediaBrowser->BrowseSync(device, object.c_str(), list, true))) {
+ CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - unable to find object {}", object);
+ return false;
+ }
+
+ if (list.IsNull() || !list->GetItemCount()) {
+ CLog::Log(LOGERROR, "CUPnPDirectory::GetResource - no items returned for object {}", object);
+ return false;
+ }
+
+ PLT_MediaObjectList::Iterator entry = list->GetFirstItem();
+ if (entry == 0)
+ return false;
+
+ return UPNP::GetResource(*entry, item);
+}
+
+
+/*----------------------------------------------------------------------
+| CUPnPDirectory::GetDirectory
++---------------------------------------------------------------------*/
+bool
+CUPnPDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ return false;
+
+ CUPnP* upnp = CUPnP::GetInstance();
+
+ /* upnp should never be cached, it has internal cache */
+ items.SetCacheToDisc(CFileItemList::CACHE_NEVER);
+
+ // We accept upnp://devuuid/[item_id/]
+ NPT_String path = url.Get().c_str();
+ if (!path.StartsWith("upnp://", true)) {
+ return false;
+ }
+
+ if (path.Compare("upnp://", true) == 0) {
+ upnp->StartClient();
+
+ // root -> get list of devices
+ const NPT_Lock<PLT_DeviceDataReferenceList>& devices = upnp->m_MediaBrowser->GetMediaServers();
+ NPT_List<PLT_DeviceDataReference>::Iterator device = devices.GetFirstItem();
+ while (device) {
+ NPT_String name = (*device)->GetFriendlyName();
+ NPT_String uuid = (*device)->GetUUID();
+
+ CFileItemPtr pItem(new CFileItem((const char*)name));
+ pItem->SetPath(std::string((const char*) "upnp://" + uuid + "/"));
+ pItem->m_bIsFolder = true;
+ pItem->SetArt("thumb", (const char*)(*device)->GetIconUrl("image/png"));
+
+ items.Add(pItem);
+
+ ++device;
+ }
+ } else {
+ if (!path.EndsWith("/")) path += "/";
+
+ // look for nextslash
+ int next_slash = path.Find('/', 7);
+
+ NPT_String uuid = (next_slash==-1)?path.SubString(7):path.SubString(7, next_slash-7);
+ NPT_String object_id = (next_slash == -1) ? NPT_String("") : path.SubString(next_slash + 1);
+ object_id.TrimRight("/");
+ if (object_id.GetLength()) {
+ object_id = CURL::Decode((char*)object_id).c_str();
+ }
+
+ // try to find the device with wait on startup
+ PLT_DeviceDataReference device;
+ if (!FindDeviceWait(upnp, uuid, device))
+ goto failure;
+
+ // issue a browse request with object_id
+ // if object_id is empty use "0" for root
+ object_id = object_id.IsEmpty() ? NPT_String("0") : object_id;
+
+ // remember a count of object classes
+ std::map<NPT_String, int> classes;
+
+ // just a guess as to what types of files we want
+ bool video = true;
+ bool audio = true;
+ bool image = true;
+ StringUtils::TrimLeft(m_strFileMask, "/");
+ if (!m_strFileMask.empty()) {
+ video = m_strFileMask.find(".wmv") != std::string::npos;
+ audio = m_strFileMask.find(".wma") != std::string::npos;
+ image = m_strFileMask.find(".jpg") != std::string::npos;
+ }
+
+ // special case for Windows Media Connect and WMP11 when looking for root
+ // We can target which root subfolder we want based on directory mask
+ if (object_id == "0" && ((device->GetFriendlyName().Find("Windows Media Connect", 0, true) >= 0) ||
+ (device->m_ModelName == "Windows Media Player Sharing"))) {
+
+ // look for a specific type to differentiate which folder we want
+ if (audio && !video && !image) {
+ // music
+ object_id = "1";
+ } else if (!audio && video && !image) {
+ // video
+ object_id = "2";
+ } else if (!audio && !video && image) {
+ // pictures
+ object_id = "3";
+ }
+ }
+
+#ifdef DISABLE_SPECIALCASE
+ // same thing but special case for XBMC
+ if (object_id == "0" && ((device->m_ModelName.Find("XBMC", 0, true) >= 0) ||
+ (device->m_ModelName.Find("Xbox Media Center", 0, true) >= 0))) {
+ // look for a specific type to differentiate which folder we want
+ if (audio && !video && !image) {
+ // music
+ object_id = "virtualpath://upnpmusic";
+ } else if (!audio && video && !image) {
+ // video
+ object_id = "virtualpath://upnpvideo";
+ } else if (!audio && !video && image) {
+ // pictures
+ object_id = "virtualpath://upnppictures";
+ }
+ }
+#endif
+
+ // if error, return now, the device could have gone away
+ // this will make us go back to the sources list
+ PLT_MediaObjectListReference list;
+ NPT_Result res = upnp->m_MediaBrowser->BrowseSync(device, object_id, list);
+ if (NPT_FAILED(res)) goto failure;
+
+ // empty list is ok
+ if (list.IsNull()) goto cleanup;
+
+ PLT_MediaObjectList::Iterator entry = list->GetFirstItem();
+ while (entry) {
+ // disregard items with wrong class/type
+ if( (!video && (*entry)->m_ObjectClass.type.CompareN("object.item.videoitem", 21,true) == 0)
+ || (!audio && (*entry)->m_ObjectClass.type.CompareN("object.item.audioitem", 21,true) == 0)
+ || (!image && (*entry)->m_ObjectClass.type.CompareN("object.item.imageitem", 21,true) == 0) )
+ {
+ ++entry;
+ continue;
+ }
+
+ // keep count of classes
+ classes[(*entry)->m_ObjectClass.type]++;
+ CFileItemPtr pItem = BuildObject(*entry, UPnPClient);
+ if(!pItem) {
+ ++entry;
+ continue;
+ }
+
+ std::string id;
+ if ((*entry)->m_ReferenceID.IsEmpty())
+ id = (const char*) (*entry)->m_ObjectID;
+ else
+ id = (const char*) (*entry)->m_ReferenceID;
+
+ id = CURL::Encode(id);
+ URIUtils::AddSlashAtEnd(id);
+ pItem->SetPath(std::string((const char*) "upnp://" + uuid + "/" + id.c_str()));
+
+ items.Add(pItem);
+
+ ++entry;
+ }
+
+ NPT_String max_string = "";
+ int max_count = 0;
+ for (auto& it : classes)
+ {
+ if (it.second > max_count)
+ {
+ max_string = it.first;
+ max_count = it.second;
+ }
+ }
+ std::string content = GetContentMapping(max_string);
+ items.SetContent(content);
+ if (content == "unknown")
+ {
+ items.AddSortMethod(SortByNone, 571, LABEL_MASKS("%L", "%I", "%L", ""));
+ items.AddSortMethod(SortByLabel, SortAttributeIgnoreFolders, 551, LABEL_MASKS("%L", "%I", "%L", ""));
+ items.AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I"));
+ items.AddSortMethod(SortByDate, 552, LABEL_MASKS("%L", "%J", "%L", "%J"));
+ }
+ }
+
+cleanup:
+ return true;
+
+failure:
+ return false;
+}
+}
diff --git a/xbmc/filesystem/UPnPDirectory.h b/xbmc/filesystem/UPnPDirectory.h
new file mode 100644
index 0000000..e49407e
--- /dev/null
+++ b/xbmc/filesystem/UPnPDirectory.h
@@ -0,0 +1,37 @@
+/*
+ * UPnP Support for XBMC
+ * Copyright (c) 2006 c0diq (Sylvain Rebaud)
+ * Portions Copyright (c) by the authors of libPlatinum
+ * http://www.plutinosoft.com/blog/category/platinum/
+ *
+ * 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 "IDirectory.h"
+
+class CFileItem;
+class CURL;
+
+namespace XFILE
+{
+class CUPnPDirectory : public IDirectory
+{
+public:
+ CUPnPDirectory(void) = default;
+ ~CUPnPDirectory(void) override = default;
+
+ // IDirectory methods
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool AllowAll() const override { return true; }
+
+ // class methods
+ static std::string GetFriendlyName(const CURL& url);
+ static bool GetResource(const CURL &path, CFileItem& item);
+};
+}
diff --git a/xbmc/filesystem/UPnPFile.cpp b/xbmc/filesystem/UPnPFile.cpp
new file mode 100644
index 0000000..5605170
--- /dev/null
+++ b/xbmc/filesystem/UPnPFile.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011-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 "UPnPFile.h"
+
+#include "FileFactory.h"
+#include "FileItem.h"
+#include "UPnPDirectory.h"
+#include "URL.h"
+
+using namespace XFILE;
+
+CUPnPFile::CUPnPFile() = default;
+
+CUPnPFile::~CUPnPFile() = default;
+
+bool CUPnPFile::Open(const CURL& url)
+{
+ CFileItem item_new;
+ if (CUPnPDirectory::GetResource(url, item_new))
+ {
+ //CLog::Log(LOGDEBUG,"FileUPnP - file redirect to {}.", item_new.GetPath());
+ IFile *pNewImp = CFileFactory::CreateLoader(item_new.GetPath());
+ CURL *pNewUrl = new CURL(item_new.GetPath());
+ if (pNewImp)
+ {
+ throw new CRedirectException(pNewImp, pNewUrl);
+ }
+ delete pNewUrl;
+ }
+ return false;
+}
+
+int CUPnPFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ CFileItem item_new;
+ if (CUPnPDirectory::GetResource(url, item_new))
+ {
+ //CLog::Log(LOGDEBUG,"FileUPnP - file redirect to {}.", item_new.GetPath());
+ IFile *pNewImp = CFileFactory::CreateLoader(item_new.GetPath());
+ CURL *pNewUrl = new CURL(item_new.GetPath());
+ if (pNewImp)
+ {
+ throw new CRedirectException(pNewImp, pNewUrl);
+ }
+ delete pNewUrl;
+ }
+ return -1;
+}
+
+bool CUPnPFile::Exists(const CURL& url)
+{
+ CFileItem item_new;
+ if (CUPnPDirectory::GetResource(url, item_new))
+ {
+ //CLog::Log(LOGDEBUG,"FileUPnP - file redirect to {}.", item_new.GetPath());
+ IFile *pNewImp = CFileFactory::CreateLoader(item_new.GetPath());
+ CURL *pNewUrl = new CURL(item_new.GetPath());
+ if (pNewImp)
+ {
+ throw new CRedirectException(pNewImp, pNewUrl);
+ }
+ delete pNewUrl;
+ }
+ return false;
+}
diff --git a/xbmc/filesystem/UPnPFile.h b/xbmc/filesystem/UPnPFile.h
new file mode 100644
index 0000000..b56dcb1
--- /dev/null
+++ b/xbmc/filesystem/UPnPFile.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2011-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 "IFile.h"
+
+namespace XFILE
+{
+ class CUPnPFile : public IFile
+ {
+ public:
+ CUPnPFile();
+ ~CUPnPFile() override;
+ bool Open(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+
+ ssize_t Read(void* lpBuf, size_t uiBufSize) override {return -1;}
+ int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override {return -1;}
+ void Close() override{}
+ int64_t GetPosition() override {return -1;}
+ int64_t GetLength() override {return -1;}
+ };
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory.cpp b/xbmc/filesystem/VideoDatabaseDirectory.cpp
new file mode 100644
index 0000000..6a2cb6a
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory.cpp
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2016-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 "VideoDatabaseDirectory.h"
+
+#include "File.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "VideoDatabaseDirectory/QueryParams.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/TextureManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Crc32.h"
+#include "utils/LegacyPathTranslation.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "video/VideoDatabase.h"
+
+using namespace XFILE;
+using namespace VIDEODATABASEDIRECTORY;
+
+CVideoDatabaseDirectory::CVideoDatabaseDirectory(void) = default;
+
+CVideoDatabaseDirectory::~CVideoDatabaseDirectory(void) = default;
+
+namespace
+{
+std::string GetChildContentType(const std::unique_ptr<CDirectoryNode>& node)
+{
+ switch (node->GetChildType())
+ {
+ case NODE_TYPE_EPISODES:
+ case NODE_TYPE_RECENTLY_ADDED_EPISODES:
+ return "episodes";
+ case NODE_TYPE_SEASONS:
+ return "seasons";
+ case NODE_TYPE_TITLE_MOVIES:
+ case NODE_TYPE_RECENTLY_ADDED_MOVIES:
+ return "movies";
+ case NODE_TYPE_TITLE_TVSHOWS:
+ case NODE_TYPE_INPROGRESS_TVSHOWS:
+ return "tvshows";
+ case NODE_TYPE_TITLE_MUSICVIDEOS:
+ case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS:
+ return "musicvideos";
+ case NODE_TYPE_GENRE:
+ return "genres";
+ case NODE_TYPE_COUNTRY:
+ return "countries";
+ case NODE_TYPE_ACTOR:
+ {
+ CQueryParams params;
+ node->CollectQueryParams(params);
+ if (static_cast<VideoDbContentType>(params.GetContentType()) ==
+ VideoDbContentType::MUSICVIDEOS)
+ return "artists";
+
+ return "actors";
+ }
+ case NODE_TYPE_DIRECTOR:
+ return "directors";
+ case NODE_TYPE_STUDIO:
+ return "studios";
+ case NODE_TYPE_YEAR:
+ return "years";
+ case NODE_TYPE_MUSICVIDEOS_ALBUM:
+ return "albums";
+ case NODE_TYPE_SETS:
+ return "sets";
+ case NODE_TYPE_TAGS:
+ return "tags";
+ default:
+ break;
+ }
+ return {};
+}
+
+} // unnamed namespace
+
+bool CVideoDatabaseDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ std::string path = CLegacyPathTranslation::TranslateVideoDbPath(url);
+ items.SetPath(path);
+ items.m_dwSize = -1; // No size
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+
+ if (!pNode)
+ return false;
+
+ bool bResult = pNode->GetChilds(items);
+ for (int i=0;i<items.Size();++i)
+ {
+ CFileItemPtr item = items[i];
+ if (item->m_bIsFolder && !item->HasArt("icon") && !item->HasArt("thumb"))
+ {
+ std::string strImage = GetIcon(item->GetPath());
+ if (!strImage.empty() && CServiceBroker::GetGUI()->GetTextureManager().HasTexture(strImage))
+ item->SetArt("icon", strImage);
+ }
+ if (item->GetVideoInfoTag())
+ {
+ item->SetDynPath(item->GetVideoInfoTag()->GetPath());
+ }
+ }
+ if (items.HasProperty("customtitle"))
+ items.SetLabel(items.GetProperty("customtitle").asString());
+ else
+ items.SetLabel(pNode->GetLocalizedName());
+
+ items.SetContent(GetChildContentType(pNode));
+
+ return bResult;
+}
+
+NODE_TYPE CVideoDatabaseDirectory::GetDirectoryChildType(const std::string& strPath)
+{
+ std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strPath);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+
+ if (!pNode)
+ return NODE_TYPE_NONE;
+
+ return pNode->GetChildType();
+}
+
+NODE_TYPE CVideoDatabaseDirectory::GetDirectoryType(const std::string& strPath)
+{
+ std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strPath);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+
+ if (!pNode)
+ return NODE_TYPE_NONE;
+
+ return pNode->GetType();
+}
+
+NODE_TYPE CVideoDatabaseDirectory::GetDirectoryParentType(const std::string& strPath)
+{
+ std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strPath);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+
+ if (!pNode)
+ return NODE_TYPE_NONE;
+
+ CDirectoryNode* pParentNode=pNode->GetParent();
+
+ if (!pParentNode)
+ return NODE_TYPE_NONE;
+
+ return pParentNode->GetChildType();
+}
+
+bool CVideoDatabaseDirectory::GetQueryParams(const std::string& strPath, CQueryParams& params)
+{
+ std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strPath);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+
+ if (!pNode)
+ return false;
+
+ CDirectoryNode::GetDatabaseInfo(strPath,params);
+ return true;
+}
+
+void CVideoDatabaseDirectory::ClearDirectoryCache(const std::string& strDirectory)
+{
+ std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strDirectory);
+ URIUtils::RemoveSlashAtEnd(path);
+
+ uint32_t crc = Crc32::ComputeFromLowerCase(path);
+
+ std::string strFileName = StringUtils::Format("special://temp/archive_cache/{:08x}.fi", crc);
+ CFile::Delete(strFileName);
+}
+
+bool CVideoDatabaseDirectory::IsAllItem(const std::string& strDirectory)
+{
+ if (StringUtils::EndsWith(strDirectory, "/-1/"))
+ return true;
+ return false;
+}
+
+bool CVideoDatabaseDirectory::GetLabel(const std::string& strDirectory, std::string& strLabel)
+{
+ strLabel = "";
+
+ std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strDirectory);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+ if (!pNode || path.empty())
+ return false;
+
+ // first see if there's any filter criteria
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(path, params);
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ // get genre
+ if (params.GetGenreId() != -1)
+ strLabel += videodatabase.GetGenreById(params.GetGenreId());
+
+ // get country
+ if (params.GetCountryId() != -1)
+ strLabel += videodatabase.GetCountryById(params.GetCountryId());
+
+ // get set
+ if (params.GetSetId() != -1)
+ strLabel += videodatabase.GetSetById(params.GetSetId());
+
+ // get tag
+ if (params.GetTagId() != -1)
+ strLabel += videodatabase.GetTagById(params.GetTagId());
+
+ // get year
+ if (params.GetYear() != -1)
+ {
+ std::string strTemp = std::to_string(params.GetYear());
+ if (!strLabel.empty())
+ strLabel += " / ";
+ strLabel += strTemp;
+ }
+
+ if (strLabel.empty())
+ {
+ switch (pNode->GetChildType())
+ {
+ case NODE_TYPE_TITLE_MOVIES:
+ case NODE_TYPE_TITLE_TVSHOWS:
+ case NODE_TYPE_TITLE_MUSICVIDEOS:
+ strLabel = g_localizeStrings.Get(369); break;
+ case NODE_TYPE_ACTOR: // Actor
+ strLabel = g_localizeStrings.Get(344); break;
+ case NODE_TYPE_GENRE: // Genres
+ strLabel = g_localizeStrings.Get(135); break;
+ case NODE_TYPE_COUNTRY: // Countries
+ strLabel = g_localizeStrings.Get(20451); break;
+ case NODE_TYPE_YEAR: // Year
+ strLabel = g_localizeStrings.Get(562); break;
+ case NODE_TYPE_DIRECTOR: // Director
+ strLabel = g_localizeStrings.Get(20348); break;
+ case NODE_TYPE_SETS: // Sets
+ strLabel = g_localizeStrings.Get(20434); break;
+ case NODE_TYPE_TAGS: // Tags
+ strLabel = g_localizeStrings.Get(20459); break;
+ case NODE_TYPE_MOVIES_OVERVIEW: // Movies
+ strLabel = g_localizeStrings.Get(342); break;
+ case NODE_TYPE_TVSHOWS_OVERVIEW: // TV Shows
+ strLabel = g_localizeStrings.Get(20343); break;
+ case NODE_TYPE_RECENTLY_ADDED_MOVIES: // Recently Added Movies
+ strLabel = g_localizeStrings.Get(20386); break;
+ case NODE_TYPE_RECENTLY_ADDED_EPISODES: // Recently Added Episodes
+ strLabel = g_localizeStrings.Get(20387); break;
+ case NODE_TYPE_STUDIO: // Studios
+ strLabel = g_localizeStrings.Get(20388); break;
+ case NODE_TYPE_MUSICVIDEOS_OVERVIEW: // Music Videos
+ strLabel = g_localizeStrings.Get(20389); break;
+ case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS: // Recently Added Music Videos
+ strLabel = g_localizeStrings.Get(20390); break;
+ case NODE_TYPE_SEASONS: // Seasons
+ strLabel = g_localizeStrings.Get(33054); break;
+ case NODE_TYPE_EPISODES: // Episodes
+ strLabel = g_localizeStrings.Get(20360); break;
+ case NODE_TYPE_INPROGRESS_TVSHOWS: // InProgress TvShows
+ strLabel = g_localizeStrings.Get(626); break;
+ default:
+ return false;
+ }
+ }
+
+ return true;
+}
+
+std::string CVideoDatabaseDirectory::GetIcon(const std::string &strDirectory)
+{
+ std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strDirectory);
+ switch (GetDirectoryChildType(path))
+ {
+ case NODE_TYPE_TITLE_MOVIES:
+ if (URIUtils::PathEquals(path, "videodb://movies/titles/"))
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN))
+ return "DefaultMovies.png";
+ return "DefaultMovieTitle.png";
+ }
+ return "";
+ case NODE_TYPE_TITLE_TVSHOWS:
+ if (URIUtils::PathEquals(path, "videodb://tvshows/titles/"))
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN))
+ return "DefaultTVShows.png";
+ return "DefaultTVShowTitle.png";
+ }
+ return "";
+ case NODE_TYPE_TITLE_MUSICVIDEOS:
+ if (URIUtils::PathEquals(path, "videodb://musicvideos/titles/"))
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN))
+ return "DefaultMusicVideos.png";
+ return "DefaultMusicVideoTitle.png";
+ }
+ return "";
+ case NODE_TYPE_ACTOR: // Actor
+ return "DefaultActor.png";
+ case NODE_TYPE_GENRE: // Genres
+ return "DefaultGenre.png";
+ case NODE_TYPE_COUNTRY: // Countries
+ return "DefaultCountry.png";
+ case NODE_TYPE_SETS: // Sets
+ return "DefaultSets.png";
+ case NODE_TYPE_TAGS: // Tags
+ return "DefaultTags.png";
+ case NODE_TYPE_YEAR: // Year
+ return "DefaultYear.png";
+ case NODE_TYPE_DIRECTOR: // Director
+ return "DefaultDirector.png";
+ case NODE_TYPE_MOVIES_OVERVIEW: // Movies
+ return "DefaultMovies.png";
+ case NODE_TYPE_TVSHOWS_OVERVIEW: // TV Shows
+ return "DefaultTVShows.png";
+ case NODE_TYPE_RECENTLY_ADDED_MOVIES: // Recently Added Movies
+ return "DefaultRecentlyAddedMovies.png";
+ case NODE_TYPE_RECENTLY_ADDED_EPISODES: // Recently Added Episodes
+ return "DefaultRecentlyAddedEpisodes.png";
+ case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS: // Recently Added Episodes
+ return "DefaultRecentlyAddedMusicVideos.png";
+ case NODE_TYPE_INPROGRESS_TVSHOWS: // InProgress TvShows
+ return "DefaultInProgressShows.png";
+ case NODE_TYPE_STUDIO: // Studios
+ return "DefaultStudios.png";
+ case NODE_TYPE_MUSICVIDEOS_OVERVIEW: // Music Videos
+ return "DefaultMusicVideos.png";
+ case NODE_TYPE_MUSICVIDEOS_ALBUM: // Music Videos - Albums
+ return "DefaultMusicAlbums.png";
+ default:
+ break;
+ }
+
+ return "";
+}
+
+bool CVideoDatabaseDirectory::ContainsMovies(const std::string &path)
+{
+ VIDEODATABASEDIRECTORY::NODE_TYPE type = GetDirectoryChildType(path);
+ if (type == VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MOVIES || type == VIDEODATABASEDIRECTORY::NODE_TYPE_EPISODES || type == VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MUSICVIDEOS) return true;
+ return false;
+}
+
+bool CVideoDatabaseDirectory::Exists(const CURL& url)
+{
+ std::string path = CLegacyPathTranslation::TranslateVideoDbPath(url);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+
+ if (!pNode)
+ return false;
+
+ if (pNode->GetChildType() == VIDEODATABASEDIRECTORY::NODE_TYPE_NONE)
+ return false;
+
+ return true;
+}
+
+bool CVideoDatabaseDirectory::CanCache(const std::string& strPath)
+{
+ std::string path = CLegacyPathTranslation::TranslateVideoDbPath(strPath);
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(path));
+ if (!pNode)
+ return false;
+ return pNode->CanCache();
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory.h b/xbmc/filesystem/VideoDatabaseDirectory.h
new file mode 100644
index 0000000..9603b7d
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory.h
@@ -0,0 +1,36 @@
+/*
+ * 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 "IDirectory.h"
+#include "VideoDatabaseDirectory/DirectoryNode.h"
+#include "VideoDatabaseDirectory/QueryParams.h"
+
+namespace XFILE
+{
+ class CVideoDatabaseDirectory : public IDirectory
+ {
+ public:
+ CVideoDatabaseDirectory(void);
+ ~CVideoDatabaseDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ bool Exists(const CURL& url) override;
+ bool AllowAll() const override { return true; }
+ static VIDEODATABASEDIRECTORY::NODE_TYPE GetDirectoryChildType(const std::string& strPath);
+ static VIDEODATABASEDIRECTORY::NODE_TYPE GetDirectoryType(const std::string& strPath);
+ static VIDEODATABASEDIRECTORY::NODE_TYPE GetDirectoryParentType(const std::string& strPath);
+ static bool GetQueryParams(const std::string& strPath, VIDEODATABASEDIRECTORY::CQueryParams& params);
+ void ClearDirectoryCache(const std::string& strDirectory);
+ static bool IsAllItem(const std::string& strDirectory);
+ static bool GetLabel(const std::string& strDirectory, std::string& strLabel);
+ static std::string GetIcon(const std::string& strDirectory);
+ bool ContainsMovies(const std::string &path);
+ static bool CanCache(const std::string &path);
+ };
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/CMakeLists.txt b/xbmc/filesystem/VideoDatabaseDirectory/CMakeLists.txt
new file mode 100644
index 0000000..149fa5c
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/CMakeLists.txt
@@ -0,0 +1,37 @@
+set(SOURCES DirectoryNode.cpp
+ DirectoryNodeEpisodes.cpp
+ DirectoryNodeGrouped.cpp
+ DirectoryNodeInProgressTvShows.cpp
+ DirectoryNodeMoviesOverview.cpp
+ DirectoryNodeMusicVideosOverview.cpp
+ DirectoryNodeOverview.cpp
+ DirectoryNodeRecentlyAddedEpisodes.cpp
+ DirectoryNodeRecentlyAddedMovies.cpp
+ DirectoryNodeRecentlyAddedMusicVideos.cpp
+ DirectoryNodeRoot.cpp
+ DirectoryNodeSeasons.cpp
+ DirectoryNodeTitleMovies.cpp
+ DirectoryNodeTitleMusicVideos.cpp
+ DirectoryNodeTitleTvShows.cpp
+ DirectoryNodeTvShowsOverview.cpp
+ QueryParams.cpp)
+
+set(HEADERS DirectoryNode.h
+ DirectoryNodeEpisodes.h
+ DirectoryNodeGrouped.h
+ DirectoryNodeInProgressTvShows.h
+ DirectoryNodeMoviesOverview.h
+ DirectoryNodeMusicVideosOverview.h
+ DirectoryNodeOverview.h
+ DirectoryNodeRecentlyAddedEpisodes.h
+ DirectoryNodeRecentlyAddedMovies.h
+ DirectoryNodeRecentlyAddedMusicVideos.h
+ DirectoryNodeRoot.h
+ DirectoryNodeSeasons.h
+ DirectoryNodeTitleMovies.h
+ DirectoryNodeTitleMusicVideos.h
+ DirectoryNodeTitleTvShows.h
+ DirectoryNodeTvShowsOverview.h
+ QueryParams.h)
+
+core_add_library(videodatabasedirectory)
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.cpp
new file mode 100644
index 0000000..7f83cff
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.cpp
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2016-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 "DirectoryNode.h"
+
+#include "DirectoryNodeEpisodes.h"
+#include "DirectoryNodeGrouped.h"
+#include "DirectoryNodeInProgressTvShows.h"
+#include "DirectoryNodeMoviesOverview.h"
+#include "DirectoryNodeMusicVideosOverview.h"
+#include "DirectoryNodeOverview.h"
+#include "DirectoryNodeRecentlyAddedEpisodes.h"
+#include "DirectoryNodeRecentlyAddedMovies.h"
+#include "DirectoryNodeRecentlyAddedMusicVideos.h"
+#include "DirectoryNodeRoot.h"
+#include "DirectoryNodeSeasons.h"
+#include "DirectoryNodeTitleMovies.h"
+#include "DirectoryNodeTitleMusicVideos.h"
+#include "DirectoryNodeTitleTvShows.h"
+#include "DirectoryNodeTvShowsOverview.h"
+#include "FileItem.h"
+#include "QueryParams.h"
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+// Constructor is protected use ParseURL()
+CDirectoryNode::CDirectoryNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent)
+{
+ m_Type = Type;
+ m_strName = strName;
+ m_pParent = pParent;
+}
+
+CDirectoryNode::~CDirectoryNode()
+{
+ delete m_pParent, m_pParent = nullptr;
+}
+
+// Parses a given path and returns the current node of the path
+CDirectoryNode* CDirectoryNode::ParseURL(const std::string& strPath)
+{
+ CURL url(strPath);
+
+ std::string strDirectory = url.GetFileName();
+ URIUtils::RemoveSlashAtEnd(strDirectory);
+
+ std::vector<std::string> Path = StringUtils::Tokenize(strDirectory, '/');
+ // we always have a root node, it is special and has a path of ""
+ Path.insert(Path.begin(), "");
+
+ CDirectoryNode *pNode = nullptr;
+ CDirectoryNode *pParent = nullptr;
+ NODE_TYPE NodeType = NODE_TYPE_ROOT;
+ // loop down the dir path, creating a node with a parent.
+ // if we hit a child type of NODE_TYPE_NONE, then we are done.
+ for (size_t i = 0; i < Path.size() && NodeType != NODE_TYPE_NONE; ++i)
+ {
+ pNode = CDirectoryNode::CreateNode(NodeType, Path[i], pParent);
+ NodeType = pNode ? pNode->GetChildType() : NODE_TYPE_NONE;
+ pParent = pNode;
+ }
+
+ // Add all the additional URL options to the last node
+ if (pNode)
+ pNode->AddOptions(url.GetOptions());
+
+ return pNode;
+}
+
+// returns the database ids of the path,
+void CDirectoryNode::GetDatabaseInfo(const std::string& strPath, CQueryParams& params)
+{
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::ParseURL(strPath));
+
+ if (!pNode)
+ return;
+
+ pNode->CollectQueryParams(params);
+}
+
+// Create a node object
+CDirectoryNode* CDirectoryNode::CreateNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent)
+{
+ switch (Type)
+ {
+ case NODE_TYPE_ROOT:
+ return new CDirectoryNodeRoot(strName, pParent);
+ case NODE_TYPE_OVERVIEW:
+ return new CDirectoryNodeOverview(strName, pParent);
+ case NODE_TYPE_GENRE:
+ case NODE_TYPE_COUNTRY:
+ case NODE_TYPE_SETS:
+ case NODE_TYPE_TAGS:
+ case NODE_TYPE_YEAR:
+ case NODE_TYPE_ACTOR:
+ case NODE_TYPE_DIRECTOR:
+ case NODE_TYPE_STUDIO:
+ case NODE_TYPE_MUSICVIDEOS_ALBUM:
+ return new CDirectoryNodeGrouped(Type, strName, pParent);
+ case NODE_TYPE_TITLE_MOVIES:
+ return new CDirectoryNodeTitleMovies(strName, pParent);
+ case NODE_TYPE_TITLE_TVSHOWS:
+ return new CDirectoryNodeTitleTvShows(strName, pParent);
+ case NODE_TYPE_MOVIES_OVERVIEW:
+ return new CDirectoryNodeMoviesOverview(strName, pParent);
+ case NODE_TYPE_TVSHOWS_OVERVIEW:
+ return new CDirectoryNodeTvShowsOverview(strName, pParent);
+ case NODE_TYPE_SEASONS:
+ return new CDirectoryNodeSeasons(strName, pParent);
+ case NODE_TYPE_EPISODES:
+ return new CDirectoryNodeEpisodes(strName, pParent);
+ case NODE_TYPE_RECENTLY_ADDED_MOVIES:
+ return new CDirectoryNodeRecentlyAddedMovies(strName,pParent);
+ case NODE_TYPE_RECENTLY_ADDED_EPISODES:
+ return new CDirectoryNodeRecentlyAddedEpisodes(strName,pParent);
+ case NODE_TYPE_MUSICVIDEOS_OVERVIEW:
+ return new CDirectoryNodeMusicVideosOverview(strName,pParent);
+ case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS:
+ return new CDirectoryNodeRecentlyAddedMusicVideos(strName,pParent);
+ case NODE_TYPE_INPROGRESS_TVSHOWS:
+ return new CDirectoryNodeInProgressTvShows(strName,pParent);
+ case NODE_TYPE_TITLE_MUSICVIDEOS:
+ return new CDirectoryNodeTitleMusicVideos(strName,pParent);
+ default:
+ break;
+ }
+
+ return nullptr;
+}
+
+// Current node name
+const std::string& CDirectoryNode::GetName() const
+{
+ return m_strName;
+}
+
+int CDirectoryNode::GetID() const
+{
+ return atoi(m_strName.c_str());
+}
+
+std::string CDirectoryNode::GetLocalizedName() const
+{
+ return "";
+}
+
+// Current node type
+NODE_TYPE CDirectoryNode::GetType() const
+{
+ return m_Type;
+}
+
+// Return the parent directory node or NULL, if there is no
+CDirectoryNode* CDirectoryNode::GetParent() const
+{
+ return m_pParent;
+}
+
+void CDirectoryNode::RemoveParent()
+{
+ m_pParent = nullptr;
+}
+
+// should be overloaded by a derived class
+// to get the content of a node. Will be called
+// by GetChilds() of a parent node
+bool CDirectoryNode::GetContent(CFileItemList& items) const
+{
+ return false;
+}
+
+// Creates a videodb url
+std::string CDirectoryNode::BuildPath() const
+{
+ std::vector<std::string> array;
+
+ if (!m_strName.empty())
+ array.insert(array.begin(), m_strName);
+
+ CDirectoryNode* pParent=m_pParent;
+ while (pParent != nullptr)
+ {
+ const std::string& strNodeName=pParent->GetName();
+ if (!strNodeName.empty())
+ array.insert(array.begin(), strNodeName);
+
+ pParent = pParent->GetParent();
+ }
+
+ std::string strPath="videodb://";
+ for (int i = 0; i < static_cast<int>(array.size()); ++i)
+ strPath += array[i]+"/";
+
+ std::string options = m_options.GetOptionsString();
+ if (!options.empty())
+ strPath += "?" + options;
+
+ return strPath;
+}
+
+void CDirectoryNode::AddOptions(const std::string &options)
+{
+ if (options.empty())
+ return;
+
+ m_options.AddOptions(options);
+}
+
+// Collects Query params from this and all parent nodes. If a NODE_TYPE can
+// be used as a database parameter, it will be added to the
+// params object.
+void CDirectoryNode::CollectQueryParams(CQueryParams& params) const
+{
+ params.SetQueryParam(m_Type, m_strName);
+
+ CDirectoryNode* pParent=m_pParent;
+ while (pParent != nullptr)
+ {
+ params.SetQueryParam(pParent->GetType(), pParent->GetName());
+ pParent = pParent->GetParent();
+ }
+}
+
+// Should be overloaded by a derived class.
+// Returns the NODE_TYPE of the child nodes.
+NODE_TYPE CDirectoryNode::GetChildType() const
+{
+ return NODE_TYPE_NONE;
+}
+
+// Get the child fileitems of this node
+bool CDirectoryNode::GetChilds(CFileItemList& items)
+{
+ if (CanCache() && items.Load())
+ return true;
+
+ std::unique_ptr<CDirectoryNode> pNode(CDirectoryNode::CreateNode(GetChildType(), "", this));
+
+ bool bSuccess=false;
+ if (pNode)
+ {
+ pNode->m_options = m_options;
+ bSuccess = pNode->GetContent(items);
+ if (bSuccess)
+ {
+ if (CanCache())
+ items.SetCacheToDisc(CFileItemList::CACHE_ALWAYS);
+ }
+ else
+ items.Clear();
+
+ pNode->RemoveParent();
+ }
+
+ return bSuccess;
+}
+
+bool CDirectoryNode::CanCache() const
+{
+ // no caching is required - the list is cached in CGUIMediaWindow::GetDirectory
+ // if it was slow to fetch anyway.
+ return false;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.h
new file mode 100644
index 0000000..7ab27c5
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNode.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016-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 "utils/UrlOptions.h"
+
+#include <string>
+
+class CFileItemList;
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CQueryParams;
+
+ typedef enum _NODE_TYPE
+ {
+ NODE_TYPE_NONE=0,
+ NODE_TYPE_MOVIES_OVERVIEW,
+ NODE_TYPE_TVSHOWS_OVERVIEW,
+ NODE_TYPE_GENRE,
+ NODE_TYPE_ACTOR,
+ NODE_TYPE_ROOT,
+ NODE_TYPE_OVERVIEW,
+ NODE_TYPE_TITLE_MOVIES,
+ NODE_TYPE_YEAR,
+ NODE_TYPE_DIRECTOR,
+ NODE_TYPE_TITLE_TVSHOWS,
+ NODE_TYPE_SEASONS,
+ NODE_TYPE_EPISODES,
+ NODE_TYPE_RECENTLY_ADDED_MOVIES,
+ NODE_TYPE_RECENTLY_ADDED_EPISODES,
+ NODE_TYPE_STUDIO,
+ NODE_TYPE_MUSICVIDEOS_OVERVIEW,
+ NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS,
+ NODE_TYPE_TITLE_MUSICVIDEOS,
+ NODE_TYPE_MUSICVIDEOS_ALBUM,
+ NODE_TYPE_SETS,
+ NODE_TYPE_COUNTRY,
+ NODE_TYPE_TAGS,
+ NODE_TYPE_INPROGRESS_TVSHOWS
+ } NODE_TYPE;
+
+ typedef struct {
+ NODE_TYPE node;
+ std::string id;
+ int label;
+ } Node;
+
+ class CDirectoryNode
+ {
+ public:
+ static CDirectoryNode* ParseURL(const std::string& strPath);
+ static void GetDatabaseInfo(const std::string& strPath, CQueryParams& params);
+ virtual ~CDirectoryNode();
+
+ NODE_TYPE GetType() const;
+
+ bool GetChilds(CFileItemList& items);
+ virtual NODE_TYPE GetChildType() const;
+ virtual std::string GetLocalizedName() const;
+ void CollectQueryParams(CQueryParams& params) const;
+
+ CDirectoryNode* GetParent() const;
+
+ std::string BuildPath() const;
+
+ virtual bool CanCache() const;
+ protected:
+ CDirectoryNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent);
+ static CDirectoryNode* CreateNode(NODE_TYPE Type, const std::string& strName, CDirectoryNode* pParent);
+
+ void AddOptions(const std::string& options);
+
+ const std::string& GetName() const;
+ int GetID() const;
+ void RemoveParent();
+
+ virtual bool GetContent(CFileItemList& items) const;
+
+
+ private:
+ NODE_TYPE m_Type;
+ std::string m_strName;
+ CDirectoryNode* m_pParent;
+ CUrlOptions m_options;
+ };
+ }
+}
+
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.cpp
new file mode 100644
index 0000000..9f629ba
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.cpp
@@ -0,0 +1,52 @@
+/*
+ * 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 "DirectoryNodeEpisodes.h"
+
+#include "FileItem.h"
+#include "QueryParams.h"
+#include "video/VideoDatabase.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CDirectoryNodeEpisodes::CDirectoryNodeEpisodes(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_EPISODES, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeEpisodes::GetContent(CFileItemList& items) const
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ int season = (int)params.GetSeason();
+ if (season == -2)
+ season = -1;
+
+ int details = items.HasProperty("set_videodb_details")
+ ? items.GetProperty("set_videodb_details").asInteger32()
+ : VideoDbDetailsNone;
+
+ bool bSuccess = videodatabase.GetEpisodesNav(
+ BuildPath(), items, params.GetGenreId(), params.GetYear(), params.GetActorId(),
+ params.GetDirectorId(), params.GetTvShowId(), season, SortDescription(), details);
+
+ videodatabase.Close();
+
+ return bSuccess;
+}
+
+NODE_TYPE CDirectoryNodeEpisodes::GetChildType() const
+{
+ return NODE_TYPE_EPISODES;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.h
new file mode 100644
index 0000000..e5a640a
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeEpisodes.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeEpisodes : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeEpisodes(const std::string& strEntryName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& items) const override;
+ NODE_TYPE GetChildType() const override;
+ };
+ }
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.cpp
new file mode 100644
index 0000000..73ec115
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.cpp
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2016-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 "DirectoryNodeGrouped.h"
+
+#include "QueryParams.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoDbUrl.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CDirectoryNodeGrouped::CDirectoryNodeGrouped(NODE_TYPE type, const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(type, strName, pParent)
+{ }
+
+NODE_TYPE CDirectoryNodeGrouped::GetChildType() const
+{
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ VideoDbContentType type = static_cast<VideoDbContentType>(params.GetContentType());
+ if (type == VideoDbContentType::MOVIES)
+ return NODE_TYPE_TITLE_MOVIES;
+ if (type == VideoDbContentType::MUSICVIDEOS)
+ {
+ if (GetType() == NODE_TYPE_ACTOR)
+ return NODE_TYPE_MUSICVIDEOS_ALBUM;
+ else
+ return NODE_TYPE_TITLE_MUSICVIDEOS;
+ }
+
+ return NODE_TYPE_TITLE_TVSHOWS;
+}
+
+std::string CDirectoryNodeGrouped::GetLocalizedName() const
+{
+ CVideoDatabase db;
+ if (db.Open())
+ return db.GetItemById(GetContentType(), GetID());
+
+ return "";
+}
+
+bool CDirectoryNodeGrouped::GetContent(CFileItemList& items) const
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ std::string itemType = GetContentType(params);
+ if (itemType.empty())
+ return false;
+
+ // make sure to translate all IDs in the path into URL parameters
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(BuildPath()))
+ return false;
+
+ return videodatabase.GetItems(videoUrl.ToString(),
+ static_cast<VideoDbContentType>(params.GetContentType()), itemType,
+ items);
+}
+
+std::string CDirectoryNodeGrouped::GetContentType() const
+{
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ return GetContentType(params);
+}
+
+std::string CDirectoryNodeGrouped::GetContentType(const CQueryParams &params) const
+{
+ switch (GetType())
+ {
+ case NODE_TYPE_GENRE:
+ return "genres";
+ case NODE_TYPE_COUNTRY:
+ return "countries";
+ case NODE_TYPE_SETS:
+ return "sets";
+ case NODE_TYPE_TAGS:
+ return "tags";
+ case NODE_TYPE_YEAR:
+ return "years";
+ case NODE_TYPE_ACTOR:
+ if (static_cast<VideoDbContentType>(params.GetContentType()) ==
+ VideoDbContentType::MUSICVIDEOS)
+ return "artists";
+ else
+ return "actors";
+ case NODE_TYPE_DIRECTOR:
+ return "directors";
+ case NODE_TYPE_STUDIO:
+ return "studios";
+ case NODE_TYPE_MUSICVIDEOS_ALBUM:
+ return "albums";
+
+ case NODE_TYPE_EPISODES:
+ case NODE_TYPE_MOVIES_OVERVIEW:
+ case NODE_TYPE_MUSICVIDEOS_OVERVIEW:
+ case NODE_TYPE_NONE:
+ case NODE_TYPE_OVERVIEW:
+ case NODE_TYPE_RECENTLY_ADDED_EPISODES:
+ case NODE_TYPE_RECENTLY_ADDED_MOVIES:
+ case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS:
+ case NODE_TYPE_INPROGRESS_TVSHOWS:
+ case NODE_TYPE_ROOT:
+ case NODE_TYPE_SEASONS:
+ case NODE_TYPE_TITLE_MOVIES:
+ case NODE_TYPE_TITLE_MUSICVIDEOS:
+ case NODE_TYPE_TITLE_TVSHOWS:
+ case NODE_TYPE_TVSHOWS_OVERVIEW:
+ default:
+ break;
+ }
+
+ return "";
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.h
new file mode 100644
index 0000000..63a5929
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeGrouped.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeGrouped : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeGrouped(NODE_TYPE type, const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+
+ private:
+ std::string GetContentType() const;
+ std::string GetContentType(const CQueryParams &params) const;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.cpp
new file mode 100644
index 0000000..044f8b3
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016-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 "DirectoryNodeInProgressTvShows.h"
+
+#include "FileItem.h"
+#include "video/VideoDatabase.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CDirectoryNodeInProgressTvShows::CDirectoryNodeInProgressTvShows(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_INPROGRESS_TVSHOWS, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeInProgressTvShows::GetChildType() const
+{
+ return NODE_TYPE_SEASONS;
+}
+
+std::string CDirectoryNodeInProgressTvShows::GetLocalizedName() const
+{
+ CVideoDatabase db;
+ if (db.Open())
+ return db.GetTvShowTitleById(GetID());
+ return "";
+}
+
+bool CDirectoryNodeInProgressTvShows::GetContent(CFileItemList& items) const
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ int details = items.HasProperty("set_videodb_details")
+ ? items.GetProperty("set_videodb_details").asInteger32()
+ : VideoDbDetailsNone;
+ bool bSuccess = videodatabase.GetInProgressTvShowsNav(BuildPath(), items, 0, details);
+
+ videodatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.h
new file mode 100644
index 0000000..987a166
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeInProgressTvShows.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016-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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeInProgressTvShows : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeInProgressTvShows(const std::string& strEntryName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.cpp
new file mode 100644
index 0000000..20ef46e
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.cpp
@@ -0,0 +1,80 @@
+/*
+ * 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 "DirectoryNodeMoviesOverview.h"
+
+#include "FileItem.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoDbUrl.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+Node MovieChildren[] = {
+ { NODE_TYPE_GENRE, "genres", 135 },
+ { NODE_TYPE_TITLE_MOVIES, "titles", 10024 },
+ { NODE_TYPE_YEAR, "years", 652 },
+ { NODE_TYPE_ACTOR, "actors", 344 },
+ { NODE_TYPE_DIRECTOR, "directors", 20348 },
+ { NODE_TYPE_STUDIO, "studios", 20388 },
+ { NODE_TYPE_SETS, "sets", 20434 },
+ { NODE_TYPE_COUNTRY, "countries", 20451 },
+ { NODE_TYPE_TAGS, "tags", 20459 }
+ };
+
+CDirectoryNodeMoviesOverview::CDirectoryNodeMoviesOverview(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_MOVIES_OVERVIEW, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeMoviesOverview::GetChildType() const
+{
+ for (const Node& node : MovieChildren)
+ if (GetName() == node.id)
+ return node.node;
+
+ return NODE_TYPE_NONE;
+}
+
+std::string CDirectoryNodeMoviesOverview::GetLocalizedName() const
+{
+ for (const Node& node : MovieChildren)
+ if (GetName() == node.id)
+ return g_localizeStrings.Get(node.label);
+ return "";
+}
+
+bool CDirectoryNodeMoviesOverview::GetContent(CFileItemList& items) const
+{
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(BuildPath()))
+ return false;
+
+ for (unsigned int i = 0; i < sizeof(MovieChildren) / sizeof(Node); ++i)
+ {
+ if (i == 6)
+ {
+ CVideoDatabase db;
+ if (db.Open() && !db.HasSets())
+ continue;
+ }
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string strDir = StringUtils::Format("{}/", MovieChildren[i].id);
+ itemUrl.AppendPath(strDir);
+
+ CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), true));
+ pItem->SetLabel(g_localizeStrings.Get(MovieChildren[i].label));
+ pItem->SetCanQueue(false);
+ items.Add(pItem);
+ }
+
+ return true;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.h
new file mode 100644
index 0000000..0f32e28
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMoviesOverview.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeMoviesOverview : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeMoviesOverview(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.cpp
new file mode 100644
index 0000000..b71d8f1
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.cpp
@@ -0,0 +1,75 @@
+/*
+ * 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 "DirectoryNodeMusicVideosOverview.h"
+
+#include "FileItem.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+#include "video/VideoDbUrl.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+Node MusicVideoChildren[] = {
+ { NODE_TYPE_GENRE, "genres", 135 },
+ { NODE_TYPE_TITLE_MUSICVIDEOS, "titles", 10024 },
+ { NODE_TYPE_YEAR, "years", 652 },
+ { NODE_TYPE_ACTOR, "artists", 133 },
+ { NODE_TYPE_MUSICVIDEOS_ALBUM, "albums", 132 },
+ { NODE_TYPE_DIRECTOR, "directors", 20348 },
+ { NODE_TYPE_STUDIO, "studios", 20388 },
+ { NODE_TYPE_TAGS, "tags", 20459 }
+ };
+
+CDirectoryNodeMusicVideosOverview::CDirectoryNodeMusicVideosOverview(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_MUSICVIDEOS_OVERVIEW, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeMusicVideosOverview::GetChildType() const
+{
+ for (const Node& node : MusicVideoChildren)
+ if (GetName() == node.id)
+ return node.node;
+
+ return NODE_TYPE_NONE;
+}
+
+std::string CDirectoryNodeMusicVideosOverview::GetLocalizedName() const
+{
+ for (const Node& node : MusicVideoChildren)
+ if (GetName() == node.id)
+ return g_localizeStrings.Get(node.label);
+ return "";
+}
+
+bool CDirectoryNodeMusicVideosOverview::GetContent(CFileItemList& items) const
+{
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(BuildPath()))
+ return false;
+
+ for (const Node& node : MusicVideoChildren)
+ {
+ CFileItemPtr pItem(new CFileItem(g_localizeStrings.Get(node.label)));
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string strDir = StringUtils::Format("{}/", node.id);
+ itemUrl.AppendPath(strDir);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ pItem->SetCanQueue(false);
+ pItem->SetSpecialSort(SortSpecialOnTop);
+ items.Add(pItem);
+ }
+
+ return true;
+}
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.h
new file mode 100644
index 0000000..4a79592
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeMusicVideosOverview.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeMusicVideosOverview : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeMusicVideosOverview(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.cpp
new file mode 100644
index 0000000..b52ae24
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.cpp
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2016-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 "DirectoryNodeOverview.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "video/VideoDatabase.h"
+
+#include <utility>
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+Node OverviewChildren[] = {
+ { NODE_TYPE_MOVIES_OVERVIEW, "movies", 342 },
+ { NODE_TYPE_TVSHOWS_OVERVIEW, "tvshows", 20343 },
+ { NODE_TYPE_MUSICVIDEOS_OVERVIEW, "musicvideos", 20389 },
+ { NODE_TYPE_RECENTLY_ADDED_MOVIES, "recentlyaddedmovies", 20386 },
+ { NODE_TYPE_RECENTLY_ADDED_EPISODES, "recentlyaddedepisodes", 20387 },
+ { NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS, "recentlyaddedmusicvideos", 20390 },
+ { NODE_TYPE_INPROGRESS_TVSHOWS, "inprogresstvshows", 626 },
+ };
+
+CDirectoryNodeOverview::CDirectoryNodeOverview(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_OVERVIEW, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeOverview::GetChildType() const
+{
+ for (const Node& node : OverviewChildren)
+ if (GetName() == node.id)
+ return node.node;
+
+ return NODE_TYPE_NONE;
+}
+
+std::string CDirectoryNodeOverview::GetLocalizedName() const
+{
+ for (const Node& node : OverviewChildren)
+ if (GetName() == node.id)
+ return g_localizeStrings.Get(node.label);
+ return "";
+}
+
+bool CDirectoryNodeOverview::GetContent(CFileItemList& items) const
+{
+ CVideoDatabase database;
+ database.Open();
+ bool hasMovies = database.HasContent(VideoDbContentType::MOVIES);
+ bool hasTvShows = database.HasContent(VideoDbContentType::TVSHOWS);
+ bool hasMusicVideos = database.HasContent(VideoDbContentType::MUSICVIDEOS);
+ std::vector<std::pair<const char*, int> > vec;
+ if (hasMovies)
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN))
+ vec.emplace_back("movies/titles", 342);
+ else
+ vec.emplace_back("movies", 342); // Movies
+ }
+ if (hasTvShows)
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN))
+ vec.emplace_back("tvshows/titles", 20343);
+ else
+ vec.emplace_back("tvshows", 20343); // TV Shows
+ }
+ if (hasMusicVideos)
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN))
+ vec.emplace_back("musicvideos/titles", 20389);
+ else
+ vec.emplace_back("musicvideos", 20389); // Music Videos
+ }
+ {
+ if (hasMovies)
+ vec.emplace_back("recentlyaddedmovies", 20386); // Recently Added Movies
+ if (hasTvShows)
+ {
+ vec.emplace_back("recentlyaddedepisodes", 20387); // Recently Added Episodes
+ vec.emplace_back("inprogresstvshows", 626); // InProgress TvShows
+ }
+ if (hasMusicVideos)
+ vec.emplace_back("recentlyaddedmusicvideos", 20390); // Recently Added Music Videos
+ }
+ std::string path = BuildPath();
+ for (unsigned int i = 0; i < vec.size(); ++i)
+ {
+ CFileItemPtr pItem(new CFileItem(path + vec[i].first + "/", true));
+ pItem->SetLabel(g_localizeStrings.Get(vec[i].second));
+ pItem->SetLabelPreformatted(true);
+ pItem->SetCanQueue(false);
+ items.Add(pItem);
+ }
+
+ return true;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.h
new file mode 100644
index 0000000..5ad498b
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeOverview.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeOverview : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeOverview(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.cpp
new file mode 100644
index 0000000..fe50dad
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.cpp
@@ -0,0 +1,36 @@
+/*
+ * 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 "DirectoryNodeRecentlyAddedEpisodes.h"
+
+#include "FileItem.h"
+#include "video/VideoDatabase.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CDirectoryNodeRecentlyAddedEpisodes::CDirectoryNodeRecentlyAddedEpisodes(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_RECENTLY_ADDED_EPISODES, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeRecentlyAddedEpisodes::GetContent(CFileItemList& items) const
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ int details = items.HasProperty("set_videodb_details")
+ ? items.GetProperty("set_videodb_details").asInteger32()
+ : VideoDbDetailsNone;
+ bool bSuccess = videodatabase.GetRecentlyAddedEpisodesNav(BuildPath(), items, 0, details);
+
+ videodatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.h
new file mode 100644
index 0000000..2cb1e6c
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedEpisodes.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeRecentlyAddedEpisodes : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeRecentlyAddedEpisodes(const std::string& strEntryName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& items) const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.cpp
new file mode 100644
index 0000000..202d6f7
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.cpp
@@ -0,0 +1,36 @@
+/*
+ * 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 "DirectoryNodeRecentlyAddedMovies.h"
+
+#include "FileItem.h"
+#include "video/VideoDatabase.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CDirectoryNodeRecentlyAddedMovies::CDirectoryNodeRecentlyAddedMovies(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_RECENTLY_ADDED_MOVIES, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeRecentlyAddedMovies::GetContent(CFileItemList& items) const
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ int details = items.HasProperty("set_videodb_details")
+ ? items.GetProperty("set_videodb_details").asInteger32()
+ : VideoDbDetailsNone;
+ bool bSuccess = videodatabase.GetRecentlyAddedMoviesNav(BuildPath(), items, 0, details);
+
+ videodatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.h
new file mode 100644
index 0000000..318e0fc
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMovies.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeRecentlyAddedMovies : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeRecentlyAddedMovies(const std::string& strEntryName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& items) const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.cpp
new file mode 100644
index 0000000..d8ba5ef
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.cpp
@@ -0,0 +1,37 @@
+/*
+ * 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 "DirectoryNodeRecentlyAddedMusicVideos.h"
+
+#include "FileItem.h"
+#include "video/VideoDatabase.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CDirectoryNodeRecentlyAddedMusicVideos::CDirectoryNodeRecentlyAddedMusicVideos(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeRecentlyAddedMusicVideos::GetContent(CFileItemList& items) const
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ int details = items.HasProperty("set_videodb_details")
+ ? items.GetProperty("set_videodb_details").asInteger32()
+ : VideoDbDetailsNone;
+ bool bSuccess = videodatabase.GetRecentlyAddedMusicVideosNav(BuildPath(), items, 0, details);
+
+ videodatabase.Close();
+
+ return bSuccess;
+}
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.h
new file mode 100644
index 0000000..bbc9711
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRecentlyAddedMusicVideos.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeRecentlyAddedMusicVideos : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeRecentlyAddedMusicVideos(const std::string& strEntryName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& items) const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.cpp
new file mode 100644
index 0000000..6e1b69c
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.cpp
@@ -0,0 +1,22 @@
+/*
+ * 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 "DirectoryNodeRoot.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CDirectoryNodeRoot::CDirectoryNodeRoot(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_ROOT, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeRoot::GetChildType() const
+{
+ return NODE_TYPE_OVERVIEW;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.h
new file mode 100644
index 0000000..99f8e6e
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeRoot.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeRoot : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeRoot(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.cpp
new file mode 100644
index 0000000..25b8b09
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.cpp
@@ -0,0 +1,81 @@
+/*
+ * 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 "DirectoryNodeSeasons.h"
+
+#include "FileItem.h"
+#include "QueryParams.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+#include "video/VideoDatabase.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CDirectoryNodeSeasons::CDirectoryNodeSeasons(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_SEASONS, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeSeasons::GetChildType() const
+{
+ return NODE_TYPE_EPISODES;
+}
+
+std::string CDirectoryNodeSeasons::GetLocalizedName() const
+{
+ switch (GetID())
+ {
+ case 0:
+ return g_localizeStrings.Get(20381); // Specials
+ case -1:
+ return g_localizeStrings.Get(20366); // All Seasons
+ case -2:
+ {
+ CDirectoryNode* pParent = GetParent();
+ if (pParent)
+ return pParent->GetLocalizedName();
+ return "";
+ }
+ default:
+ return GetSeasonTitle();
+ }
+}
+
+std::string CDirectoryNodeSeasons::GetSeasonTitle() const
+{
+ std::string season;
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ season = db.GetTvShowNamedSeasonById(params.GetTvShowId(), params.GetSeason());
+ }
+ if (season.empty())
+ season = StringUtils::Format(g_localizeStrings.Get(20358), GetID()); // Season <n>
+
+ return season;
+}
+
+bool CDirectoryNodeSeasons::GetContent(CFileItemList& items) const
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ bool bSuccess=videodatabase.GetSeasonsNav(BuildPath(), items, params.GetActorId(), params.GetDirectorId(), params.GetGenreId(), params.GetYear(), params.GetTvShowId());
+
+ videodatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.h
new file mode 100644
index 0000000..d5dd3a1
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeSeasons.h
@@ -0,0 +1,36 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeSeasons : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeSeasons(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+
+ private:
+ /*!
+ * \brief Get the title of choosen season.
+ * \return The season title.
+ */
+ std::string GetSeasonTitle() const;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.cpp
new file mode 100644
index 0000000..45a83ce
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.cpp
@@ -0,0 +1,44 @@
+/*
+ * 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 "DirectoryNodeTitleMovies.h"
+
+#include "FileItem.h"
+#include "QueryParams.h"
+#include "video/VideoDatabase.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CDirectoryNodeTitleMovies::CDirectoryNodeTitleMovies(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_TITLE_MOVIES, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeTitleMovies::GetContent(CFileItemList& items) const
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ int details = items.HasProperty("set_videodb_details")
+ ? items.GetProperty("set_videodb_details").asInteger32()
+ : VideoDbDetailsNone;
+
+ bool bSuccess = videodatabase.GetMoviesNav(
+ BuildPath(), items, params.GetGenreId(), params.GetYear(), params.GetActorId(),
+ params.GetDirectorId(), params.GetStudioId(), params.GetCountryId(), params.GetSetId(),
+ params.GetTagId(), SortDescription(), details);
+
+ videodatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.h
new file mode 100644
index 0000000..7166ac8
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMovies.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeTitleMovies : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeTitleMovies(const std::string& strEntryName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& items) const override;
+ };
+ }
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.cpp
new file mode 100644
index 0000000..95fe300
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.cpp
@@ -0,0 +1,43 @@
+/*
+ * 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 "DirectoryNodeTitleMusicVideos.h"
+
+#include "FileItem.h"
+#include "QueryParams.h"
+#include "video/VideoDatabase.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CDirectoryNodeTitleMusicVideos::CDirectoryNodeTitleMusicVideos(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_TITLE_MUSICVIDEOS, strName, pParent)
+{
+
+}
+
+bool CDirectoryNodeTitleMusicVideos::GetContent(CFileItemList& items) const
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ int details = items.HasProperty("set_videodb_details")
+ ? items.GetProperty("set_videodb_details").asInteger32()
+ : VideoDbDetailsNone;
+ bool bSuccess = videodatabase.GetMusicVideosNav(
+ BuildPath(), items, params.GetGenreId(), params.GetYear(), params.GetActorId(),
+ params.GetDirectorId(), params.GetStudioId(), params.GetAlbumId(), params.GetTagId(),
+ SortDescription(), details);
+
+ videodatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.h
new file mode 100644
index 0000000..88c54fe
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleMusicVideos.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeTitleMusicVideos : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeTitleMusicVideos(const std::string& strEntryName, CDirectoryNode* pParent);
+ protected:
+ bool GetContent(CFileItemList& item) const override;
+ };
+ }
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.cpp
new file mode 100644
index 0000000..b1a247c
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.cpp
@@ -0,0 +1,56 @@
+/*
+ * 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 "DirectoryNodeTitleTvShows.h"
+
+#include "FileItem.h"
+#include "QueryParams.h"
+#include "video/VideoDatabase.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CDirectoryNodeTitleTvShows::CDirectoryNodeTitleTvShows(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_TITLE_TVSHOWS, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeTitleTvShows::GetChildType() const
+{
+ return NODE_TYPE_SEASONS;
+}
+
+std::string CDirectoryNodeTitleTvShows::GetLocalizedName() const
+{
+ CVideoDatabase db;
+ if (db.Open())
+ return db.GetTvShowTitleById(GetID());
+ return "";
+}
+
+bool CDirectoryNodeTitleTvShows::GetContent(CFileItemList& items) const
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ CQueryParams params;
+ CollectQueryParams(params);
+
+ int details = items.HasProperty("set_videodb_details")
+ ? items.GetProperty("set_videodb_details").asInteger32()
+ : VideoDbDetailsNone;
+
+ bool bSuccess = videodatabase.GetTvShowsNav(
+ BuildPath(), items, params.GetGenreId(), params.GetYear(), params.GetActorId(),
+ params.GetDirectorId(), params.GetStudioId(), params.GetTagId(), SortDescription(), details);
+
+ videodatabase.Close();
+
+ return bSuccess;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.h
new file mode 100644
index 0000000..697d572
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTitleTvShows.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeTitleTvShows : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeTitleTvShows(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.cpp b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.cpp
new file mode 100644
index 0000000..07e5ff4
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.cpp
@@ -0,0 +1,74 @@
+/*
+ * 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 "DirectoryNodeTvShowsOverview.h"
+
+#include "FileItem.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+#include "video/VideoDbUrl.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+Node TvShowChildren[] = {
+ { NODE_TYPE_GENRE, "genres", 135 },
+ { NODE_TYPE_TITLE_TVSHOWS, "titles", 10024 },
+ { NODE_TYPE_YEAR, "years", 652 },
+ { NODE_TYPE_ACTOR, "actors", 344 },
+ { NODE_TYPE_STUDIO, "studios", 20388 },
+ { NODE_TYPE_TAGS, "tags", 20459 }
+ };
+
+CDirectoryNodeTvShowsOverview::CDirectoryNodeTvShowsOverview(const std::string& strName, CDirectoryNode* pParent)
+ : CDirectoryNode(NODE_TYPE_TVSHOWS_OVERVIEW, strName, pParent)
+{
+
+}
+
+NODE_TYPE CDirectoryNodeTvShowsOverview::GetChildType() const
+{
+ if (GetName()=="0")
+ return NODE_TYPE_EPISODES;
+
+ for (const Node& node : TvShowChildren)
+ if (GetName() == node.id)
+ return node.node;
+
+ return NODE_TYPE_NONE;
+}
+
+std::string CDirectoryNodeTvShowsOverview::GetLocalizedName() const
+{
+ for (const Node& node : TvShowChildren)
+ if (GetName() == node.id)
+ return g_localizeStrings.Get(node.label);
+ return "";
+}
+
+bool CDirectoryNodeTvShowsOverview::GetContent(CFileItemList& items) const
+{
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(BuildPath()))
+ return false;
+
+ for (const Node& node : TvShowChildren)
+ {
+ CFileItemPtr pItem(new CFileItem(g_localizeStrings.Get(node.label)));
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string strDir = StringUtils::Format("{}/", node.id);
+ itemUrl.AppendPath(strDir);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ pItem->SetCanQueue(false);
+ items.Add(pItem);
+ }
+
+ return true;
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.h b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.h
new file mode 100644
index 0000000..4394e1f
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/DirectoryNodeTvShowsOverview.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CDirectoryNodeTvShowsOverview : public CDirectoryNode
+ {
+ public:
+ CDirectoryNodeTvShowsOverview(const std::string& strName, CDirectoryNode* pParent);
+ protected:
+ NODE_TYPE GetChildType() const override;
+ bool GetContent(CFileItemList& items) const override;
+ std::string GetLocalizedName() const override;
+ };
+ }
+}
+
+
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.cpp b/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.cpp
new file mode 100644
index 0000000..d4c78ff
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.cpp
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2016-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 "QueryParams.h"
+
+#include "video/VideoDatabase.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+CQueryParams::CQueryParams()
+{
+ m_idMovie = -1;
+ m_idGenre = -1;
+ m_idCountry = -1;
+ m_idYear = -1;
+ m_idActor = -1;
+ m_idDirector = -1;
+ m_idContent = static_cast<long>(VideoDbContentType::UNKNOWN);
+ m_idShow = -1;
+ m_idSeason = -1;
+ m_idEpisode = -1;
+ m_idStudio = -1;
+ m_idMVideo = -1;
+ m_idAlbum = -1;
+ m_idSet = -1;
+ m_idTag = -1;
+}
+
+void CQueryParams::SetQueryParam(NODE_TYPE NodeType, const std::string& strNodeName)
+{
+ long idDb=atol(strNodeName.c_str());
+
+ switch (NodeType)
+ {
+ case NODE_TYPE_OVERVIEW:
+ if (strNodeName == "tvshows")
+ m_idContent = static_cast<long>(VideoDbContentType::TVSHOWS);
+ else if (strNodeName == "musicvideos")
+ m_idContent = static_cast<long>(VideoDbContentType::MUSICVIDEOS);
+ else
+ m_idContent = static_cast<long>(VideoDbContentType::MOVIES);
+ break;
+ case NODE_TYPE_GENRE:
+ m_idGenre = idDb;
+ break;
+ case NODE_TYPE_COUNTRY:
+ m_idCountry = idDb;
+ break;
+ case NODE_TYPE_YEAR:
+ m_idYear = idDb;
+ break;
+ case NODE_TYPE_ACTOR:
+ m_idActor = idDb;
+ break;
+ case NODE_TYPE_DIRECTOR:
+ m_idDirector = idDb;
+ break;
+ case NODE_TYPE_TITLE_MOVIES:
+ case NODE_TYPE_RECENTLY_ADDED_MOVIES:
+ m_idMovie = idDb;
+ break;
+ case NODE_TYPE_TITLE_TVSHOWS:
+ case NODE_TYPE_INPROGRESS_TVSHOWS:
+ m_idShow = idDb;
+ break;
+ case NODE_TYPE_SEASONS:
+ m_idSeason = idDb;
+ break;
+ case NODE_TYPE_EPISODES:
+ case NODE_TYPE_RECENTLY_ADDED_EPISODES:
+ m_idEpisode = idDb;
+ break;
+ case NODE_TYPE_STUDIO:
+ m_idStudio = idDb;
+ break;
+ case NODE_TYPE_TITLE_MUSICVIDEOS:
+ case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS:
+ m_idMVideo = idDb;
+ break;
+ case NODE_TYPE_MUSICVIDEOS_ALBUM:
+ m_idAlbum = idDb;
+ break;
+ case NODE_TYPE_SETS:
+ m_idSet = idDb;
+ break;
+ case NODE_TYPE_TAGS:
+ m_idTag = idDb;
+ break;
+ default:
+ break;
+ }
+}
diff --git a/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.h b/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.h
new file mode 100644
index 0000000..f9d7e45
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseDirectory/QueryParams.h
@@ -0,0 +1,59 @@
+/*
+ * 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 "DirectoryNode.h"
+
+namespace XFILE
+{
+ namespace VIDEODATABASEDIRECTORY
+ {
+ class CQueryParams
+ {
+ public:
+ CQueryParams();
+ long GetContentType() const { return m_idContent; }
+ long GetMovieId() const { return m_idMovie; }
+ long GetYear() const { return m_idYear; }
+ long GetGenreId() const { return m_idGenre; }
+ long GetCountryId() const { return m_idCountry; }
+ long GetActorId() const { return m_idActor; }
+ long GetAlbumId() const { return m_idAlbum; }
+ long GetDirectorId() const { return m_idDirector; }
+ long GetTvShowId() const { return m_idShow; }
+ long GetSeason() const { return m_idSeason; }
+ long GetEpisodeId() const { return m_idEpisode; }
+ long GetStudioId() const { return m_idStudio; }
+ long GetMVideoId() const { return m_idMVideo; }
+ long GetSetId() const { return m_idSet; }
+ long GetTagId() const { return m_idTag; }
+
+ protected:
+ void SetQueryParam(NODE_TYPE NodeType, const std::string& strNodeName);
+
+ friend class CDirectoryNode;
+ private:
+ long m_idContent;
+ long m_idMovie;
+ long m_idGenre;
+ long m_idCountry;
+ long m_idYear;
+ long m_idActor;
+ long m_idDirector;
+ long m_idShow;
+ long m_idSeason;
+ long m_idEpisode;
+ long m_idStudio;
+ long m_idMVideo;
+ long m_idAlbum;
+ long m_idSet;
+ long m_idTag;
+ };
+ }
+}
diff --git a/xbmc/filesystem/VideoDatabaseFile.cpp b/xbmc/filesystem/VideoDatabaseFile.cpp
new file mode 100644
index 0000000..036eeba
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseFile.cpp
@@ -0,0 +1,99 @@
+/*
+ * 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 "VideoDatabaseFile.h"
+
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoInfoTag.h"
+
+using namespace XFILE;
+
+CVideoDatabaseFile::CVideoDatabaseFile(void)
+ : COverrideFile(true)
+{ }
+
+CVideoDatabaseFile::~CVideoDatabaseFile(void) = default;
+
+CVideoInfoTag CVideoDatabaseFile::GetVideoTag(const CURL& url)
+{
+ CVideoInfoTag tag;
+
+ std::string strFileName = URIUtils::GetFileName(url.Get());
+ if (strFileName.empty())
+ return tag;
+
+ URIUtils::RemoveExtension(strFileName);
+ if (!StringUtils::IsNaturalNumber(strFileName))
+ return tag;
+ long idDb = atol(strFileName.c_str());
+
+ VideoDbContentType type = GetType(url);
+ if (type == VideoDbContentType::UNKNOWN)
+ return tag;
+
+ CVideoDatabase videoDatabase;
+ if (!videoDatabase.Open())
+ return tag;
+
+ tag = videoDatabase.GetDetailsByTypeAndId(type, idDb);
+
+ return tag;
+}
+
+VideoDbContentType CVideoDatabaseFile::GetType(const CURL& url)
+{
+ std::string strPath = URIUtils::GetDirectory(url.Get());
+ if (strPath.empty())
+ return VideoDbContentType::UNKNOWN;
+
+ std::vector<std::string> pathElem = StringUtils::Split(strPath, "/");
+ if (pathElem.size() == 0)
+ return VideoDbContentType::UNKNOWN;
+
+ std::string itemType = pathElem.at(2);
+ VideoDbContentType type;
+ if (itemType == "movies" || itemType == "recentlyaddedmovies")
+ type = VideoDbContentType::MOVIES;
+ else if (itemType == "episodes" || itemType == "recentlyaddedepisodes" || itemType == "inprogresstvshows" || itemType == "tvshows")
+ type = VideoDbContentType::EPISODES;
+ else if (itemType == "musicvideos" || itemType == "recentlyaddedmusicvideos")
+ type = VideoDbContentType::MUSICVIDEOS;
+ else
+ type = VideoDbContentType::UNKNOWN;
+
+ return type;
+}
+
+
+std::string CVideoDatabaseFile::TranslatePath(const CURL& url)
+{
+ std::string strFileName = URIUtils::GetFileName(url.Get());
+ if (strFileName.empty())
+ return "";
+
+ URIUtils::RemoveExtension(strFileName);
+ if (!StringUtils::IsNaturalNumber(strFileName))
+ return "";
+ long idDb = atol(strFileName.c_str());
+
+ VideoDbContentType type = GetType(url);
+ if (type == VideoDbContentType::UNKNOWN)
+ return "";
+
+ CVideoDatabase videoDatabase;
+ if (!videoDatabase.Open())
+ return "";
+
+ std::string realFilename;
+ videoDatabase.GetFilePathById(idDb, realFilename, type);
+
+ return realFilename;
+}
diff --git a/xbmc/filesystem/VideoDatabaseFile.h b/xbmc/filesystem/VideoDatabaseFile.h
new file mode 100644
index 0000000..d5488f9
--- /dev/null
+++ b/xbmc/filesystem/VideoDatabaseFile.h
@@ -0,0 +1,31 @@
+/*
+ * 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 "filesystem/OverrideFile.h"
+
+enum class VideoDbContentType;
+class CVideoInfoTag;
+class CURL;
+
+namespace XFILE
+{
+class CVideoDatabaseFile : public COverrideFile
+{
+public:
+ CVideoDatabaseFile(void);
+ ~CVideoDatabaseFile(void) override;
+
+ static CVideoInfoTag GetVideoTag(const CURL& url);
+
+protected:
+ std::string TranslatePath(const CURL& url) override;
+ static VideoDbContentType GetType(const CURL& url);
+};
+}
diff --git a/xbmc/filesystem/VirtualDirectory.cpp b/xbmc/filesystem/VirtualDirectory.cpp
new file mode 100644
index 0000000..7134eba
--- /dev/null
+++ b/xbmc/filesystem/VirtualDirectory.cpp
@@ -0,0 +1,204 @@
+/*
+ * 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 "VirtualDirectory.h"
+
+#include "Directory.h"
+#include "DirectoryFactory.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "SourcesDirectory.h"
+#include "URL.h"
+#include "Util.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE;
+
+namespace XFILE
+{
+
+CVirtualDirectory::CVirtualDirectory(void)
+{
+ m_flags = DIR_FLAG_ALLOW_PROMPT;
+ m_allowNonLocalSources = true;
+}
+
+CVirtualDirectory::~CVirtualDirectory(void) = default;
+
+/*!
+ \brief Add shares to the virtual directory
+ \param VECSOURCES Shares to add
+ \sa CMediaSource, VECSOURCES
+ */
+void CVirtualDirectory::SetSources(const VECSOURCES& vecSources)
+{
+ m_vecSources = vecSources;
+}
+
+/*!
+ \brief Retrieve the shares or the content of a directory.
+ \param strPath Specifies the path of the directory to retrieve or pass an empty string to get the shares.
+ \param items Content of the directory.
+ \return Returns \e true, if directory access is successful.
+ \note If \e strPath is an empty string, the share \e items have thumbnails and icons set, else the thumbnails
+ and icons have to be set manually.
+ */
+
+bool CVirtualDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ return GetDirectory(url, items, true, false);
+}
+
+bool CVirtualDirectory::GetDirectory(const CURL& url, CFileItemList &items, bool bUseFileDirectories, bool keepImpl)
+{
+ std::string strPath = url.Get();
+ int flags = m_flags;
+ if (!bUseFileDirectories)
+ flags |= DIR_FLAG_NO_FILE_DIRS;
+ if (!strPath.empty() && strPath != "files://")
+ {
+ CURL realURL = URIUtils::SubstitutePath(url);
+ if (!m_pDir)
+ m_pDir.reset(CDirectoryFactory::Create(realURL));
+ bool ret = CDirectory::GetDirectory(strPath, m_pDir, items, m_strFileMask, flags);
+ if (!keepImpl)
+ m_pDir.reset();
+ return ret;
+ }
+
+ // if strPath is blank, clear the list (to avoid parent items showing up)
+ if (strPath.empty())
+ items.Clear();
+
+ // return the root listing
+ items.SetPath(strPath);
+
+ // grab our shares
+ VECSOURCES shares;
+ GetSources(shares);
+ CSourcesDirectory dir;
+ return dir.GetDirectory(shares, items);
+}
+
+void CVirtualDirectory::CancelDirectory()
+{
+ if (m_pDir)
+ m_pDir->CancelDirectory();
+}
+
+/*!
+ \brief Is the share \e strPath in the virtual directory.
+ \param strPath Share to test
+ \return Returns \e true, if share is in the virtual directory.
+ \note The parameter \e strPath can not be a share with directory. Eg. "iso9660://dir" will return \e false.
+ It must be "iso9660://".
+ */
+bool CVirtualDirectory::IsSource(const std::string& strPath, VECSOURCES *sources, std::string *name) const
+{
+ std::string strPathCpy = strPath;
+ StringUtils::TrimRight(strPathCpy, "/\\");
+
+ // just to make sure there's no mixed slashing in share/default defines
+ // ie. f:/video and f:\video was not be recognised as the same directory,
+ // resulting in navigation to a lower directory then the share.
+ if(URIUtils::IsDOSPath(strPathCpy))
+ StringUtils::Replace(strPathCpy, '/', '\\');
+
+ VECSOURCES shares;
+ if (sources)
+ shares = *sources;
+ else
+ GetSources(shares);
+ for (int i = 0; i < (int)shares.size(); ++i)
+ {
+ const CMediaSource& share = shares.at(i);
+ std::string strShare = share.strPath;
+ StringUtils::TrimRight(strShare, "/\\");
+ if(URIUtils::IsDOSPath(strShare))
+ StringUtils::Replace(strShare, '/', '\\');
+ if (strShare == strPathCpy)
+ {
+ if (name)
+ *name = share.strName;
+ return true;
+ }
+ }
+ return false;
+}
+
+/*!
+ \brief Is the share \e path in the virtual directory.
+ \param path Share to test
+ \return Returns \e true, if share is in the virtual directory.
+ \note The parameter \e path CAN be a share with directory. Eg. "iso9660://dir" will
+ return the same as "iso9660://".
+ */
+bool CVirtualDirectory::IsInSource(const std::string &path) const
+{
+ bool isSourceName;
+ VECSOURCES shares;
+ GetSources(shares);
+ int iShare = CUtil::GetMatchingSource(path, shares, isSourceName);
+ if (URIUtils::IsOnDVD(path))
+ { // check to see if our share path is still available and of the same type, as it changes during autodetect
+ // and GetMatchingSource() is too naive at it's matching
+ for (unsigned int i = 0; i < shares.size(); i++)
+ {
+ CMediaSource &share = shares[i];
+ if (URIUtils::IsOnDVD(share.strPath) &&
+ URIUtils::PathHasParent(path, share.strPath))
+ return true;
+ }
+ return false;
+ }
+ //! @todo May need to handle other special cases that GetMatchingSource() fails on
+ return (iShare > -1);
+}
+
+void CVirtualDirectory::GetSources(VECSOURCES &shares) const
+{
+ shares = m_vecSources;
+ // add our plug n play shares
+
+ if (m_allowNonLocalSources)
+ CServiceBroker::GetMediaManager().GetRemovableDrives(shares);
+
+#ifdef HAS_DVD_DRIVE
+ // and update our dvd share
+ for (unsigned int i = 0; i < shares.size(); ++i)
+ {
+ CMediaSource& share = shares[i];
+ if (share.m_iDriveType == CMediaSource::SOURCE_TYPE_DVD)
+ {
+ if (CServiceBroker::GetMediaManager().IsAudio(share.strPath))
+ {
+ share.strStatus = "Audio-CD";
+ share.strPath = "cdda://local/";
+ share.strDiskUniqueId = "";
+ }
+ else
+ {
+ share.strStatus = CServiceBroker::GetMediaManager().GetDiskLabel(share.strPath);
+ share.strDiskUniqueId = CServiceBroker::GetMediaManager().GetDiskUniqueId(share.strPath);
+ if (!share.strPath.length()) // unmounted CD
+ {
+ if (CServiceBroker::GetMediaManager().GetDiscPath() == "iso9660://")
+ share.strPath = "iso9660://";
+ else
+ // share is unmounted and not iso9660, discard it
+ shares.erase(shares.begin() + i--);
+ }
+ }
+ }
+ }
+#endif
+}
+}
+
diff --git a/xbmc/filesystem/VirtualDirectory.h b/xbmc/filesystem/VirtualDirectory.h
new file mode 100644
index 0000000..5a46353
--- /dev/null
+++ b/xbmc/filesystem/VirtualDirectory.h
@@ -0,0 +1,65 @@
+/*
+ * 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 "IDirectory.h"
+#include "MediaSource.h"
+
+#include <memory>
+#include <string>
+
+namespace XFILE
+{
+
+ /*!
+ \ingroup windows
+ \brief Get access to shares and it's directories.
+ */
+ class CVirtualDirectory : public IDirectory
+ {
+ public:
+ CVirtualDirectory(void);
+ ~CVirtualDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ void CancelDirectory() override;
+ bool GetDirectory(const CURL& url, CFileItemList &items, bool bUseFileDirectories, bool keepImpl);
+ void SetSources(const VECSOURCES& vecSources);
+ inline unsigned int GetNumberOfSources()
+ {
+ return static_cast<uint32_t>(m_vecSources.size());
+ }
+
+ bool IsSource(const std::string& strPath, VECSOURCES *sources = NULL, std::string *name = NULL) const;
+ bool IsInSource(const std::string& strPath) const;
+
+ inline const CMediaSource& operator [](const int index) const
+ {
+ return m_vecSources[index];
+ }
+
+ inline CMediaSource& operator[](const int index)
+ {
+ return m_vecSources[index];
+ }
+
+ void GetSources(VECSOURCES &sources) const;
+
+ void AllowNonLocalSources(bool allow) { m_allowNonLocalSources = allow; }
+
+ std::shared_ptr<IDirectory> GetDirImpl() { return m_pDir; }
+ void ReleaseDirImpl() { m_pDir.reset(); }
+
+ protected:
+ void CacheThumbs(CFileItemList &items);
+
+ VECSOURCES m_vecSources;
+ bool m_allowNonLocalSources;
+ std::shared_ptr<IDirectory> m_pDir;
+ };
+}
diff --git a/xbmc/filesystem/XbtDirectory.cpp b/xbmc/filesystem/XbtDirectory.cpp
new file mode 100644
index 0000000..5c7d395
--- /dev/null
+++ b/xbmc/filesystem/XbtDirectory.cpp
@@ -0,0 +1,67 @@
+/*
+ * 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 "XbtDirectory.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "filesystem/Directorization.h"
+#include "filesystem/XbtManager.h"
+#include "guilib/XBTF.h"
+
+#include <stdio.h>
+
+namespace XFILE
+{
+
+static CFileItemPtr XBTFFileToFileItem(const CXBTFFile& entry, const std::string& label, const std::string& path, bool isFolder)
+{
+ CFileItemPtr item(new CFileItem(label));
+ if (!isFolder)
+ item->m_dwSize = static_cast<int64_t>(entry.GetUnpackedSize());
+
+ return item;
+}
+
+CXbtDirectory::CXbtDirectory() = default;
+
+CXbtDirectory::~CXbtDirectory() = default;
+
+bool CXbtDirectory::GetDirectory(const CURL& urlOrig, CFileItemList& items)
+{
+ CURL urlXbt(urlOrig);
+
+ // if this isn't a proper xbt:// path, assume it's the path to a xbt file
+ if (!urlOrig.IsProtocol("xbt"))
+ urlXbt = URIUtils::CreateArchivePath("xbt", urlOrig);
+
+ CURL url(urlXbt);
+ url.SetOptions(""); // delete options to have a clean path to add stuff too
+ url.SetFileName(""); // delete filename too as our names later will contain it
+
+ std::vector<CXBTFFile> files;
+ if (!CXbtManager::GetInstance().GetFiles(url, files))
+ return false;
+
+ // prepare the files for directorization
+ DirectorizeEntries<CXBTFFile> entries;
+ entries.reserve(files.size());
+ for (const auto& file : files)
+ entries.push_back(DirectorizeEntry<CXBTFFile>(file.GetPath(), file));
+
+ Directorize(urlXbt, entries, XBTFFileToFileItem, items);
+
+ return true;
+}
+
+bool CXbtDirectory::ContainsFiles(const CURL& url)
+{
+ return CXbtManager::GetInstance().HasFiles(url);
+}
+
+}
diff --git a/xbmc/filesystem/XbtDirectory.h b/xbmc/filesystem/XbtDirectory.h
new file mode 100644
index 0000000..feb4540
--- /dev/null
+++ b/xbmc/filesystem/XbtDirectory.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "IFileDirectory.h"
+
+#include <map>
+#include <string>
+
+class CXBTFFile;
+
+namespace XFILE
+{
+class CXbtDirectory : public IFileDirectory
+{
+public:
+ CXbtDirectory();
+ ~CXbtDirectory() override;
+
+ // specialization of IDirectory
+ DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ALWAYS; }
+ bool GetDirectory(const CURL& url, CFileItemList& items) override;
+
+ // specialization of IFileDirectory
+ bool ContainsFiles(const CURL& url) override;
+};
+}
diff --git a/xbmc/filesystem/XbtFile.cpp b/xbmc/filesystem/XbtFile.cpp
new file mode 100644
index 0000000..92586b7
--- /dev/null
+++ b/xbmc/filesystem/XbtFile.cpp
@@ -0,0 +1,368 @@
+/*
+ * 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 "XbtFile.h"
+
+#include "URL.h"
+#include "filesystem/File.h"
+#include "filesystem/XbtManager.h"
+#include "guilib/TextureBundleXBT.h"
+#include "guilib/XBTFReader.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+#include <climits>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <utility>
+
+namespace XFILE
+{
+
+CXbtFile::CXbtFile()
+ : m_url(),
+ m_xbtfReader(nullptr),
+ m_xbtfFile(),
+ m_frameStartPositions(),
+ m_unpackedFrames()
+{ }
+
+CXbtFile::~CXbtFile()
+{
+ Close();
+}
+
+bool CXbtFile::Open(const CURL& url)
+{
+ if (m_open)
+ return false;
+
+ CURL xbtUrl(url);
+ xbtUrl.SetOptions("");
+
+ if (!GetReaderAndFile(url, m_xbtfReader, m_xbtfFile))
+ return false;
+
+ m_url = url;
+ m_open = true;
+
+ uint64_t frameStartPosition = 0;
+ const auto& frames = m_xbtfFile.GetFrames();
+ for (const auto& frame : frames)
+ {
+ m_frameStartPositions.push_back(frameStartPosition);
+
+ frameStartPosition += frame.GetUnpackedSize();
+ }
+
+ m_frameIndex = 0;
+ m_positionWithinFrame = 0;
+ m_positionTotal = 0;
+
+ m_unpackedFrames.resize(frames.size());
+
+ return true;
+}
+
+void CXbtFile::Close()
+{
+ m_unpackedFrames.clear();
+ m_frameIndex = 0;
+ m_positionWithinFrame = 0;
+ m_positionTotal = 0;
+ m_frameStartPositions.clear();
+ m_open = false;
+}
+
+bool CXbtFile::Exists(const CURL& url)
+{
+ CXBTFFile dummy;
+ return GetFile(url, dummy);
+}
+
+int64_t CXbtFile::GetPosition()
+{
+ if (!m_open)
+ return -1;
+
+ return m_positionTotal;
+}
+
+int64_t CXbtFile::GetLength()
+{
+ if (!m_open)
+ return -1;
+
+ return static_cast<int>(m_xbtfFile.GetUnpackedSize());
+}
+
+int CXbtFile::Stat(struct __stat64 *buffer)
+{
+ if (!m_open)
+ return -1;
+
+ return Stat(m_url, buffer);
+}
+
+int CXbtFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ if (!buffer)
+ return -1;
+
+ *buffer = {};
+
+ // check if the file exists
+ CXBTFReaderPtr reader;
+ CXBTFFile file;
+ if (!GetReaderAndFile(url, reader, file))
+ {
+ // check if the URL points to the XBT file itself
+ if (!url.GetFileName().empty() || !CFile::Exists(url.GetHostName()))
+ return -1;
+
+ // stat the XBT file itself
+ if (XFILE::CFile::Stat(url.GetHostName(), buffer) != 0)
+ return -1;
+
+ buffer->st_mode = _S_IFDIR;
+ return 0;
+ }
+
+ // stat the XBT file itself
+ if (XFILE::CFile::Stat(url.GetHostName(), buffer) != 0)
+ return -1;
+
+ buffer->st_size = file.GetUnpackedSize();
+
+ return 0;
+}
+
+ssize_t CXbtFile::Read(void* lpBuf, size_t uiBufSize)
+{
+ if (lpBuf == nullptr || !m_open)
+ return -1;
+
+ // nothing to read
+ if (m_xbtfFile.GetFrames().empty() || m_positionTotal >= GetLength())
+ return 0;
+
+ // we can't read more than is left
+ if (static_cast<int64_t>(uiBufSize) > GetLength() - m_positionTotal)
+ uiBufSize = static_cast<ssize_t>(GetLength() - m_positionTotal);
+
+ // we can't read more than we can signal with the return value
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+ const auto& frames = m_xbtfFile.GetFrames();
+
+ size_t remaining = uiBufSize;
+ while (remaining > 0)
+ {
+ const CXBTFFrame& frame = frames[m_frameIndex];
+
+ // check if we have already unpacked the current frame
+ if (m_unpackedFrames[m_frameIndex].empty())
+ {
+ // unpack the data from the current frame
+ std::vector<uint8_t> unpackedFrame =
+ CTextureBundleXBT::UnpackFrame(*m_xbtfReader.get(), frame);
+ if (unpackedFrame.empty())
+ {
+ Close();
+ return -1;
+ }
+
+ m_unpackedFrames[m_frameIndex] = std::move(unpackedFrame);
+ }
+
+ // determine how many bytes we need to copy from the current frame
+ uint64_t remainingBytesInFrame = frame.GetUnpackedSize() - m_positionWithinFrame;
+ size_t bytesToCopy = remaining;
+ if (remainingBytesInFrame <= SIZE_MAX)
+ bytesToCopy = std::min(remaining, static_cast<size_t>(remainingBytesInFrame));
+
+ // copy the data
+ memcpy(lpBuf, m_unpackedFrames[m_frameIndex].data() + m_positionWithinFrame, bytesToCopy);
+ m_positionWithinFrame += bytesToCopy;
+ m_positionTotal += bytesToCopy;
+ remaining -= bytesToCopy;
+
+ // check if we need to go to the next frame and there is a next frame
+ if (m_positionWithinFrame >= frame.GetUnpackedSize() && m_frameIndex < frames.size() - 1)
+ {
+ m_positionWithinFrame = 0;
+ m_frameIndex += 1;
+ }
+ }
+
+ return uiBufSize;
+}
+
+int64_t CXbtFile::Seek(int64_t iFilePosition, int iWhence)
+{
+ if (!m_open)
+ return -1;
+
+ int64_t newPosition = m_positionTotal;
+ switch (iWhence)
+ {
+ case SEEK_SET:
+ newPosition = iFilePosition;
+ break;
+
+ case SEEK_CUR:
+ newPosition += iFilePosition;
+ break;
+
+ case SEEK_END:
+ newPosition = GetLength() + iFilePosition;
+ break;
+
+ // unsupported seek mode
+ default:
+ return -1;
+ }
+
+ // can't seek before the beginning or after the end of the file
+ if (newPosition < 0 || newPosition >= GetLength())
+ return -1;
+
+ // seeking backwards doesn't require additional work
+ if (newPosition <= m_positionTotal)
+ {
+ m_positionTotal = newPosition;
+ return m_positionTotal;
+ }
+
+ // when seeking forward we need to unpack all frames we seek past
+ const auto& frames = m_xbtfFile.GetFrames();
+ while (m_positionTotal < newPosition)
+ {
+ const CXBTFFrame& frame = frames[m_frameIndex];
+
+ // check if we have already unpacked the current frame
+ if (m_unpackedFrames[m_frameIndex].empty())
+ {
+ // unpack the data from the current frame
+ std::vector<uint8_t> unpackedFrame =
+ CTextureBundleXBT::UnpackFrame(*m_xbtfReader.get(), frame);
+ if (unpackedFrame.empty())
+ {
+ Close();
+ return -1;
+ }
+
+ m_unpackedFrames[m_frameIndex] = std::move(unpackedFrame);
+ }
+
+ int64_t remainingBytesToSeek = newPosition - m_positionTotal;
+ // check if the new position is within the current frame
+ uint64_t remainingBytesInFrame = frame.GetUnpackedSize() - m_positionWithinFrame;
+ if (static_cast<uint64_t>(remainingBytesToSeek) < remainingBytesInFrame)
+ {
+ m_positionWithinFrame += remainingBytesToSeek;
+ break;
+ }
+
+ // otherwise move to the end of the frame
+ m_positionTotal += remainingBytesInFrame;
+ m_positionWithinFrame += remainingBytesInFrame;
+
+ // and go to the next frame if there is a next frame
+ if (m_frameIndex < frames.size() - 1)
+ {
+ m_positionWithinFrame = 0;
+ m_frameIndex += 1;
+ }
+ }
+
+ m_positionTotal = newPosition;
+ return m_positionTotal;
+}
+
+uint32_t CXbtFile::GetImageWidth() const
+{
+ CXBTFFrame frame;
+ if (!GetFirstFrame(frame))
+ return false;
+
+ return frame.GetWidth();
+}
+
+uint32_t CXbtFile::GetImageHeight() const
+{
+ CXBTFFrame frame;
+ if (!GetFirstFrame(frame))
+ return false;
+
+ return frame.GetHeight();
+}
+
+uint32_t CXbtFile::GetImageFormat() const
+{
+ CXBTFFrame frame;
+ if (!GetFirstFrame(frame))
+ return false;
+
+ return frame.GetFormat();
+}
+
+bool CXbtFile::HasImageAlpha() const
+{
+ CXBTFFrame frame;
+ if (!GetFirstFrame(frame))
+ return false;
+
+ return frame.HasAlpha();
+}
+
+bool CXbtFile::GetFirstFrame(CXBTFFrame& frame) const
+{
+ if (!m_open)
+ return false;
+
+ const auto& frames = m_xbtfFile.GetFrames();
+ if (frames.empty())
+ return false;
+
+ frame = frames.at(0);
+ return true;
+}
+
+bool CXbtFile::GetReader(const CURL& url, CXBTFReaderPtr& reader)
+{
+ CURL xbtUrl(url);
+ xbtUrl.SetOptions("");
+
+ return CXbtManager::GetInstance().GetReader(xbtUrl, reader);
+}
+
+bool CXbtFile::GetReaderAndFile(const CURL& url, CXBTFReaderPtr& reader, CXBTFFile& file)
+{
+ if (!GetReader(url, reader))
+ return false;
+
+ CURL xbtUrl(url);
+ xbtUrl.SetOptions("");
+
+ // CXBTFReader stores all filenames in lower case
+ std::string fileName = xbtUrl.GetFileName();
+ StringUtils::ToLower(fileName);
+
+ return reader->Get(fileName, file);
+}
+
+bool CXbtFile::GetFile(const CURL& url, CXBTFFile& file)
+{
+ CXBTFReaderPtr reader;
+ return GetReaderAndFile(url, reader, file);
+}
+
+}
diff --git a/xbmc/filesystem/XbtFile.h b/xbmc/filesystem/XbtFile.h
new file mode 100644
index 0000000..fad28a5
--- /dev/null
+++ b/xbmc/filesystem/XbtFile.h
@@ -0,0 +1,67 @@
+/*
+ * 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 "IFile.h"
+#include "URL.h"
+#include "guilib/XBTF.h"
+#include "guilib/XBTFReader.h"
+
+#include <cstdint>
+#include <cstdio>
+#include <vector>
+
+#include "PlatformDefs.h"
+
+namespace XFILE
+{
+class CXbtFile : public IFile
+{
+public:
+ CXbtFile();
+ ~CXbtFile() override;
+
+ bool Open(const CURL& url) override;
+ void Close() override;
+ bool Exists(const CURL& url) override;
+
+ int64_t GetPosition() override;
+ int64_t GetLength() override;
+
+ int Stat(struct __stat64* buffer) override;
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+
+ ssize_t Read(void* lpBuf, size_t uiBufSize) override;
+ int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override;
+
+ uint32_t GetImageWidth() const;
+ uint32_t GetImageHeight() const;
+ uint32_t GetImageFormat() const;
+ bool HasImageAlpha() const;
+
+private:
+ bool GetFirstFrame(CXBTFFrame& frame) const;
+
+ static bool GetReader(const CURL& url, CXBTFReaderPtr& reader);
+ static bool GetReaderAndFile(const CURL& url, CXBTFReaderPtr& reader, CXBTFFile& file);
+ static bool GetFile(const CURL& url, CXBTFFile& file);
+
+ CURL m_url;
+ bool m_open = false;
+ CXBTFReaderPtr m_xbtfReader;
+ CXBTFFile m_xbtfFile;
+
+ std::vector<uint64_t> m_frameStartPositions;
+ size_t m_frameIndex = 0;
+ uint64_t m_positionWithinFrame = 0;
+ int64_t m_positionTotal = 0;
+
+ std::vector<std::vector<uint8_t>> m_unpackedFrames;
+};
+}
diff --git a/xbmc/filesystem/XbtManager.cpp b/xbmc/filesystem/XbtManager.cpp
new file mode 100644
index 0000000..bb091f1
--- /dev/null
+++ b/xbmc/filesystem/XbtManager.cpp
@@ -0,0 +1,127 @@
+/*
+ * 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 "XbtManager.h"
+
+#include "URL.h"
+#include "guilib/XBTF.h"
+#include "guilib/XBTFReader.h"
+
+#include <utility>
+
+namespace XFILE
+{
+
+CXbtManager::CXbtManager() = default;
+
+CXbtManager::~CXbtManager() = default;
+
+CXbtManager& CXbtManager::GetInstance()
+{
+ static CXbtManager xbtManager;
+ return xbtManager;
+}
+
+bool CXbtManager::HasFiles(const CURL& path) const
+{
+ return ProcessFile(path) != m_readers.end();
+}
+
+bool CXbtManager::GetFiles(const CURL& path, std::vector<CXBTFFile>& files) const
+{
+ const auto& reader = ProcessFile(path);
+ if (reader == m_readers.end())
+ return false;
+
+ files = reader->second.reader->GetFiles();
+ return true;
+}
+
+bool CXbtManager::GetReader(const CURL& path, CXBTFReaderPtr& reader) const
+{
+ const auto& it = ProcessFile(path);
+ if (it == m_readers.end())
+ return false;
+
+ reader = it->second.reader;
+ return true;
+}
+
+void CXbtManager::Release(const CURL& path)
+{
+ const auto& it = GetReader(path);
+ if (it == m_readers.end())
+ return;
+
+ RemoveReader(it);
+}
+
+CXbtManager::XBTFReaders::iterator CXbtManager::GetReader(const CURL& path) const
+{
+ return GetReader(NormalizePath(path));
+}
+
+CXbtManager::XBTFReaders::iterator CXbtManager::GetReader(const std::string& path) const
+{
+ if (path.empty())
+ return m_readers.end();
+
+ return m_readers.find(path);
+}
+
+void CXbtManager::RemoveReader(XBTFReaders::iterator readerIterator) const
+{
+ if (readerIterator == m_readers.end())
+ return;
+
+ // close the reader
+ readerIterator->second.reader->Close();
+
+ // and remove it from the map
+ m_readers.erase(readerIterator);
+}
+
+CXbtManager::XBTFReaders::const_iterator CXbtManager::ProcessFile(const CURL& path) const
+{
+ std::string filePath = NormalizePath(path);
+
+ // check if the file is known
+ auto it = GetReader(filePath);
+ if (it != m_readers.end())
+ {
+ // check if the XBT file has been modified
+ if (it->second.reader->GetLastModificationTimestamp() <= it->second.lastModification)
+ return it;
+
+ // the XBT file has been modified so close and remove it from the cache
+ // it will be re-opened by the following logic
+ RemoveReader(it);
+ }
+
+ // try to read the file
+ CXBTFReaderPtr reader(new CXBTFReader());
+ if (!reader->Open(filePath))
+ return m_readers.end();
+
+ XBTFReader xbtfReader = {
+ reader,
+ reader->GetLastModificationTimestamp()
+ };
+ std::pair<XBTFReaders::iterator, bool> result = m_readers.insert(std::make_pair(filePath, xbtfReader));
+ return result.first;
+}
+
+std::string CXbtManager::NormalizePath(const CURL& path)
+{
+ if (path.IsProtocol("xbt"))
+ return path.GetHostName();
+
+ return path.Get();
+}
+
+}
diff --git a/xbmc/filesystem/XbtManager.h b/xbmc/filesystem/XbtManager.h
new file mode 100644
index 0000000..1b46eba
--- /dev/null
+++ b/xbmc/filesystem/XbtManager.h
@@ -0,0 +1,58 @@
+/*
+ * 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/XBTFReader.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CURL;
+class CXBTFFile;
+
+namespace XFILE
+{
+class CXbtManager
+{
+public:
+ ~CXbtManager();
+
+ static CXbtManager& GetInstance();
+
+ bool HasFiles(const CURL& path) const;
+ bool GetFiles(const CURL& path, std::vector<CXBTFFile>& files) const;
+
+ bool GetReader(const CURL& path, CXBTFReaderPtr& reader) const;
+
+ void Release(const CURL& path);
+
+private:
+ CXbtManager();
+ CXbtManager(const CXbtManager&) = delete;
+ CXbtManager& operator=(const CXbtManager&) = delete;
+
+ struct XBTFReader
+ {
+ CXBTFReaderPtr reader;
+ time_t lastModification;
+ };
+ using XBTFReaders = std::map<std::string, XBTFReader>;
+
+ XBTFReaders::iterator GetReader(const CURL& path) const;
+ XBTFReaders::iterator GetReader(const std::string& path) const;
+ void RemoveReader(XBTFReaders::iterator readerIterator) const;
+ XBTFReaders::const_iterator ProcessFile(const CURL& path) const;
+
+ static std::string NormalizePath(const CURL& path);
+
+ mutable XBTFReaders m_readers;
+};
+}
diff --git a/xbmc/filesystem/ZeroconfDirectory.cpp b/xbmc/filesystem/ZeroconfDirectory.cpp
new file mode 100644
index 0000000..f34b6b2
--- /dev/null
+++ b/xbmc/filesystem/ZeroconfDirectory.cpp
@@ -0,0 +1,232 @@
+/*
+ * 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 "ZeroconfDirectory.h"
+
+#include "Directory.h"
+#include "FileItem.h"
+#include "URL.h"
+#include "network/ZeroconfBrowser.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <cassert>
+#include <stdexcept>
+
+using namespace XFILE;
+
+CZeroconfDirectory::CZeroconfDirectory()
+{
+ CZeroconfBrowser::GetInstance()->Start();
+}
+
+CZeroconfDirectory::~CZeroconfDirectory() = default;
+
+namespace
+{
+ std::string GetHumanReadableProtocol(std::string const& fcr_service_type)
+ {
+ if(fcr_service_type == "_smb._tcp.")
+ return "SAMBA";
+ else if(fcr_service_type == "_ftp._tcp.")
+ return "FTP";
+ else if(fcr_service_type == "_webdav._tcp.")
+ return "WebDAV";
+ else if(fcr_service_type == "_nfs._tcp.")
+ return "NFS";
+ else if(fcr_service_type == "_sftp-ssh._tcp.")
+ return "SFTP";
+ //fallback, just return the received type
+ return fcr_service_type;
+ }
+ bool GetXBMCProtocol(std::string const& fcr_service_type, std::string& fr_protocol)
+ {
+ if(fcr_service_type == "_smb._tcp.")
+ fr_protocol = "smb";
+ else if(fcr_service_type == "_ftp._tcp.")
+ fr_protocol = "ftp";
+ else if(fcr_service_type == "_webdav._tcp.")
+ fr_protocol = "dav";
+ else if(fcr_service_type == "_nfs._tcp.")
+ fr_protocol = "nfs";
+ else if(fcr_service_type == "_sftp-ssh._tcp.")
+ fr_protocol = "sftp";
+ else
+ return false;
+ return true;
+ }
+}
+
+bool GetDirectoryFromTxtRecords(const CZeroconfBrowser::ZeroconfService& zeroconf_service, CURL& url, CFileItemList &items)
+{
+ bool ret = false;
+
+ //get the txt-records from this service
+ CZeroconfBrowser::ZeroconfService::tTxtRecordMap txtRecords=zeroconf_service.GetTxtRecords();
+
+ //if we have some records
+ if(!txtRecords.empty())
+ {
+ std::string path;
+ std::string username;
+ std::string password;
+
+ //search for a path key entry
+ CZeroconfBrowser::ZeroconfService::tTxtRecordMap::iterator it = txtRecords.find(TXT_RECORD_PATH_KEY);
+
+ //if we found the key - be sure there is a value there
+ if( it != txtRecords.end() && !it->second.empty() )
+ {
+ //from now on we treat the value as a path - everything else would mean
+ //a misconfigured zeroconf server.
+ path=it->second;
+ }
+
+ //search for a username key entry
+ it = txtRecords.find(TXT_RECORD_USERNAME_KEY);
+
+ //if we found the key - be sure there is a value there
+ if( it != txtRecords.end() && !it->second.empty() )
+ {
+ username=it->second;
+ url.SetUserName(username);
+ }
+
+ //search for a password key entry
+ it = txtRecords.find(TXT_RECORD_PASSWORD_KEY);
+
+ //if we found the key - be sure there is a value there
+ if( it != txtRecords.end() && !it->second.empty() )
+ {
+ password=it->second;
+ url.SetPassword(password);
+ }
+
+ //if we got a path - add a item - else at least we maybe have set username and password to theurl
+ if( !path.empty())
+ {
+ CFileItemPtr item(new CFileItem("", true));
+ std::string urlStr(url.Get());
+ //if path has a leading slash (sure it should have one)
+ if( path.at(0) == '/' )
+ {
+ URIUtils::RemoveSlashAtEnd(urlStr);//we don't need the slash at and of url then
+ }
+ else//path doesn't start with slash -
+ {//this is some kind of misconfiguration - we fix it by adding a slash to the url
+ URIUtils::AddSlashAtEnd(urlStr);
+ }
+
+ //add slash at end of path since it has to be a folder
+ URIUtils::AddSlashAtEnd(path);
+ //this is the full path including remote stuff (e.x. nfs://ip/path
+ item->SetPath(urlStr + path);
+ //remove the slash at the end of the path or GetFileName will not give the last dir
+ URIUtils::RemoveSlashAtEnd(path);
+ //set the label to the last directory in path
+ if( !URIUtils::GetFileName(path).empty() )
+ item->SetLabel(URIUtils::GetFileName(path));
+ else
+ item->SetLabel("/");
+
+ item->SetLabelPreformatted(true);
+ //just set the default folder icon
+ item->FillInDefaultIcon();
+ item->m_bIsShareOrDrive=true;
+ items.Add(item);
+ ret = true;
+ }
+ }
+ return ret;
+}
+
+bool CZeroconfDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ assert(url.IsProtocol("zeroconf"));
+ std::string strPath = url.Get();
+ std::string path = strPath.substr(11, strPath.length());
+ URIUtils::RemoveSlashAtEnd(path);
+ if(path.empty())
+ {
+ std::vector<CZeroconfBrowser::ZeroconfService> found_services = CZeroconfBrowser::GetInstance()->GetFoundServices();
+ for (auto& it : found_services)
+ {
+ //only use discovered services we can connect to through directory
+ std::string tmp;
+ if (GetXBMCProtocol(it.GetType(), tmp))
+ {
+ CFileItemPtr item(new CFileItem("", true));
+ CURL url;
+ url.SetProtocol("zeroconf");
+ std::string service_path(CURL::Encode(CZeroconfBrowser::ZeroconfService::toPath(it)));
+ url.SetFileName(service_path);
+ item->SetPath(url.Get());
+
+ //now do the formatting
+ std::string protocol = GetHumanReadableProtocol(it.GetType());
+ item->SetLabel(it.GetName() + " (" + protocol + ")");
+ item->SetLabelPreformatted(true);
+ //just set the default folder icon
+ item->FillInDefaultIcon();
+ items.Add(item);
+ }
+ }
+ return true;
+ }
+ else
+ {
+ //decode the path first
+ std::string decoded(CURL::Decode(path));
+ try
+ {
+ CZeroconfBrowser::ZeroconfService zeroconf_service = CZeroconfBrowser::ZeroconfService::fromPath(decoded);
+
+ if(!CZeroconfBrowser::GetInstance()->ResolveService(zeroconf_service))
+ {
+ CLog::Log(LOGINFO,
+ "CZeroconfDirectory::GetDirectory service ( {} ) could not be resolved in time",
+ zeroconf_service.GetName());
+ return false;
+ }
+ else
+ {
+ assert(!zeroconf_service.GetIP().empty());
+ CURL service;
+ service.SetPort(zeroconf_service.GetPort());
+ service.SetHostName(zeroconf_service.GetIP());
+ //do protocol conversion (_smb._tcp -> smb)
+ //! @todo try automatic conversion -> remove leading '_' and '._tcp'?
+ std::string protocol;
+ if(!GetXBMCProtocol(zeroconf_service.GetType(), protocol))
+ {
+ CLog::Log(LOGERROR,
+ "CZeroconfDirectory::GetDirectory Unknown service type ({}), skipping; ",
+ zeroconf_service.GetType());
+ return false;
+ }
+
+ service.SetProtocol(protocol);
+
+ //first try to show the txt-record defined path if any
+ if(GetDirectoryFromTxtRecords(zeroconf_service, service, items))
+ {
+ return true;
+ }
+ else//no txt record path - so let the CDirectory handler show the folders
+ {
+ return CDirectory::GetDirectory(service.Get(), items, "", DIR_FLAG_ALLOW_PROMPT);
+ }
+ }
+ } catch (std::runtime_error& e) {
+ CLog::Log(LOGERROR,
+ "CZeroconfDirectory::GetDirectory failed getting directory: '{}'. Error: '{}'",
+ decoded, e.what());
+ return false;
+ }
+ }
+}
diff --git a/xbmc/filesystem/ZeroconfDirectory.h b/xbmc/filesystem/ZeroconfDirectory.h
new file mode 100644
index 0000000..fddc2ee
--- /dev/null
+++ b/xbmc/filesystem/ZeroconfDirectory.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "IDirectory.h"
+//txt-records as of http://www.dns-sd.org/ServiceTypes.html
+#define TXT_RECORD_PATH_KEY "path"
+#define TXT_RECORD_USERNAME_KEY "u"
+#define TXT_RECORD_PASSWORD_KEY "p"
+
+namespace XFILE
+{
+ class CZeroconfDirectory : public IDirectory
+ {
+ public:
+ CZeroconfDirectory(void);
+ ~CZeroconfDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_NEVER; }
+ };
+}
+
diff --git a/xbmc/filesystem/ZipDirectory.cpp b/xbmc/filesystem/ZipDirectory.cpp
new file mode 100644
index 0000000..bfe43fb
--- /dev/null
+++ b/xbmc/filesystem/ZipDirectory.cpp
@@ -0,0 +1,79 @@
+/*
+ * 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 "ZipDirectory.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "ZipManager.h"
+#include "filesystem/Directorization.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <vector>
+
+namespace XFILE
+{
+
+ static CFileItemPtr ZipEntryToFileItem(const SZipEntry& entry, const std::string& label, const std::string& path, bool isFolder)
+ {
+ CFileItemPtr item(new CFileItem(label));
+ if (!isFolder)
+ {
+ item->m_dwSize = entry.usize;
+ item->m_idepth = entry.method;
+ }
+
+ return item;
+ }
+
+ CZipDirectory::CZipDirectory() = default;
+
+ CZipDirectory::~CZipDirectory() = default;
+
+ bool CZipDirectory::GetDirectory(const CURL& urlOrig, CFileItemList& items)
+ {
+ CURL urlZip(urlOrig);
+
+ /* if this isn't a proper archive path, assume it's the path to a archive file */
+ if (!urlOrig.IsProtocol("zip"))
+ urlZip = URIUtils::CreateArchivePath("zip", urlOrig);
+
+ std::vector<SZipEntry> zipEntries;
+ if (!g_ZipManager.GetZipList(urlZip, zipEntries))
+ return false;
+
+ // prepare the ZIP entries for directorization
+ DirectorizeEntries<SZipEntry> entries;
+ entries.reserve(zipEntries.size());
+ for (const auto& zipEntry : zipEntries)
+ entries.push_back(DirectorizeEntry<SZipEntry>(zipEntry.name, zipEntry));
+
+ // directorize the ZIP entries into files and directories
+ Directorize(urlZip, entries, ZipEntryToFileItem, items);
+
+ return true;
+ }
+
+ bool CZipDirectory::ContainsFiles(const CURL& url)
+ {
+ std::vector<SZipEntry> items;
+ g_ZipManager.GetZipList(url, items);
+ if (items.size())
+ {
+ if (items.size() > 1)
+ return true;
+
+ return false;
+ }
+
+ return false;
+ }
+}
+
diff --git a/xbmc/filesystem/ZipDirectory.h b/xbmc/filesystem/ZipDirectory.h
new file mode 100644
index 0000000..bd09c34
--- /dev/null
+++ b/xbmc/filesystem/ZipDirectory.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "IFileDirectory.h"
+
+namespace XFILE
+{
+ class CZipDirectory : public IFileDirectory
+ {
+ public:
+ CZipDirectory();
+ ~CZipDirectory() override;
+ bool GetDirectory(const CURL& url, CFileItemList& items) override;
+ bool ContainsFiles(const CURL& url) override;
+ DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ALWAYS; }
+ };
+}
diff --git a/xbmc/filesystem/ZipFile.cpp b/xbmc/filesystem/ZipFile.cpp
new file mode 100644
index 0000000..eb3d953
--- /dev/null
+++ b/xbmc/filesystem/ZipFile.cpp
@@ -0,0 +1,531 @@
+/*
+ * 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 "ZipFile.h"
+
+#include "URL.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <sys/stat.h>
+
+#define ZIP_CACHE_LIMIT 4*1024*1024
+
+using namespace XFILE;
+
+CZipFile::CZipFile()
+{
+ m_szStringBuffer = NULL;
+ m_szStartOfStringBuffer = NULL;
+ m_iDataInStringBuffer = 0;
+ m_bCached = false;
+ m_iRead = -1;
+}
+
+CZipFile::~CZipFile()
+{
+ delete[] m_szStringBuffer;
+ Close();
+}
+
+bool CZipFile::Open(const CURL&url)
+{
+ const std::string& strOpts = url.GetOptions();
+ CURL url2(url);
+ url2.SetOptions("");
+ if (!g_ZipManager.GetZipEntry(url2,mZipItem))
+ return false;
+
+ if ((mZipItem.flags & 64) == 64)
+ {
+ CLog::Log(LOGERROR,"FileZip: encrypted file, not supported!");
+ return false;
+ }
+
+ if ((mZipItem.method != 8) && (mZipItem.method != 0))
+ {
+ CLog::Log(LOGERROR,"FileZip: unsupported compression method!");
+ return false;
+ }
+
+ if (mZipItem.method != 0 && mZipItem.usize > ZIP_CACHE_LIMIT && strOpts != "?cache=no")
+ {
+ if (!CFile::Exists("special://temp/" + URIUtils::GetFileName(url2)))
+ {
+ url2.SetOptions("?cache=no");
+ const CURL pathToUrl("special://temp/" + URIUtils::GetFileName(url2));
+ if (!CFile::Copy(url2, pathToUrl))
+ return false;
+ }
+ m_bCached = true;
+ return mFile.Open("special://temp/" + URIUtils::GetFileName(url2));
+ }
+
+ if (!mFile.Open(url.GetHostName())) // this is the zip-file, always open binary
+ {
+ CLog::Log(LOGERROR, "FileZip: unable to open zip file {}!", url.GetHostName());
+ return false;
+ }
+ mFile.Seek(mZipItem.offset,SEEK_SET);
+ return InitDecompress();
+}
+
+bool CZipFile::InitDecompress()
+{
+ m_iRead = 1;
+ m_iFilePos = 0;
+ m_iZipFilePos = 0;
+ m_iAvailBuffer = 0;
+ m_bFlush = false;
+ m_ZStream.zalloc = Z_NULL;
+ m_ZStream.zfree = Z_NULL;
+ m_ZStream.opaque = Z_NULL;
+ if( mZipItem.method != 0 )
+ {
+ if (inflateInit2(&m_ZStream,-MAX_WBITS) != Z_OK)
+ {
+ CLog::Log(LOGERROR,"FileZip: error initializing zlib!");
+ return false;
+ }
+ }
+ m_ZStream.next_in = (Bytef*)m_szBuffer;
+ m_ZStream.avail_in = 0;
+ m_ZStream.total_out = 0;
+
+ return true;
+}
+
+int64_t CZipFile::GetLength()
+{
+ return mZipItem.usize;
+}
+
+int64_t CZipFile::GetPosition()
+{
+ if (m_bCached)
+ return mFile.GetPosition();
+
+ return m_iFilePos;
+}
+
+int64_t CZipFile::Seek(int64_t iFilePosition, int iWhence)
+{
+ if (m_bCached)
+ return mFile.Seek(iFilePosition,iWhence);
+ if (mZipItem.method == 0) // this is easy
+ {
+ int64_t iResult;
+ switch (iWhence)
+ {
+ case SEEK_SET:
+ if (iFilePosition > mZipItem.usize)
+ return -1;
+ m_iFilePos = iFilePosition;
+ m_iZipFilePos = m_iFilePos;
+ iResult = mFile.Seek(iFilePosition+mZipItem.offset,SEEK_SET)-mZipItem.offset;
+ return iResult;
+ break;
+
+ case SEEK_CUR:
+ if (m_iFilePos+iFilePosition > mZipItem.usize)
+ return -1;
+ m_iFilePos += iFilePosition;
+ m_iZipFilePos = m_iFilePos;
+ iResult = mFile.Seek(iFilePosition,SEEK_CUR)-mZipItem.offset;
+ return iResult;
+ break;
+
+ case SEEK_END:
+ if (iFilePosition > mZipItem.usize)
+ return -1;
+ m_iFilePos = mZipItem.usize+iFilePosition;
+ m_iZipFilePos = m_iFilePos;
+ iResult = mFile.Seek(mZipItem.offset+mZipItem.usize+iFilePosition,SEEK_SET)-mZipItem.offset;
+ return iResult;
+ break;
+ default:
+ return -1;
+
+ }
+ }
+ // here goes the stupid part..
+ if (mZipItem.method == 8)
+ {
+ static const int blockSize = 128 * 1024;
+ std::vector<char> buf(blockSize);
+ switch (iWhence)
+ {
+ case SEEK_SET:
+ if (iFilePosition == m_iFilePos)
+ return m_iFilePos; // mp3reader does this lots-of-times
+ if (iFilePosition > mZipItem.usize || iFilePosition < 0)
+ return -1;
+ // read until position in 128k blocks.. only way to do it due to format.
+ // can't start in the middle of data since then we'd have no clue where
+ // we are in uncompressed data..
+ if (iFilePosition < m_iFilePos)
+ {
+ m_iFilePos = 0;
+ m_iZipFilePos = 0;
+ inflateEnd(&m_ZStream);
+ inflateInit2(&m_ZStream,-MAX_WBITS); // simply restart zlib
+ mFile.Seek(mZipItem.offset,SEEK_SET);
+ m_ZStream.next_in = (Bytef*)m_szBuffer;
+ m_ZStream.avail_in = 0;
+ m_ZStream.total_out = 0;
+ while (m_iFilePos < iFilePosition)
+ {
+ ssize_t iToRead = (iFilePosition - m_iFilePos) > blockSize ? blockSize : iFilePosition - m_iFilePos;
+ if (Read(buf.data(), iToRead) != iToRead)
+ return -1;
+ }
+ return m_iFilePos;
+ }
+ else // seek forward
+ return Seek(iFilePosition-m_iFilePos,SEEK_CUR);
+ break;
+
+ case SEEK_CUR:
+ if (iFilePosition < 0)
+ return Seek(m_iFilePos+iFilePosition,SEEK_SET); // can't rewind stream
+ // read until requested position, drop data
+ if (m_iFilePos+iFilePosition > mZipItem.usize)
+ return -1;
+ iFilePosition += m_iFilePos;
+ while (m_iFilePos < iFilePosition)
+ {
+ ssize_t iToRead = (iFilePosition - m_iFilePos)>blockSize ? blockSize : iFilePosition - m_iFilePos;
+ if (Read(buf.data(), iToRead) != iToRead)
+ return -1;
+ }
+ return m_iFilePos;
+ break;
+
+ case SEEK_END:
+ // now this is a nasty bastard, possibly takes lotsoftime
+ // uncompress, minding m_ZStream.total_out
+
+ while(static_cast<ssize_t>(m_ZStream.total_out) < mZipItem.usize+iFilePosition)
+ {
+ ssize_t iToRead = (mZipItem.usize + iFilePosition - m_ZStream.total_out > blockSize) ? blockSize : mZipItem.usize + iFilePosition - m_ZStream.total_out;
+ if (Read(buf.data(), iToRead) != iToRead)
+ return -1;
+ }
+ return m_iFilePos;
+ break;
+ default:
+ return -1;
+ }
+ }
+ return -1;
+}
+
+bool CZipFile::Exists(const CURL& url)
+{
+ SZipEntry item;
+ if (g_ZipManager.GetZipEntry(url,item))
+ return true;
+ return false;
+}
+
+int CZipFile::Stat(struct __stat64 *buffer)
+{
+ int ret;
+ struct tm tm = {};
+
+ ret = mFile.Stat(buffer);
+ tm.tm_sec = (mZipItem.mod_time & 0x1F) << 1;
+ tm.tm_min = (mZipItem.mod_time & 0x7E0) >> 5;
+ tm.tm_hour = (mZipItem.mod_time & 0xF800) >> 11;
+ tm.tm_mday = (mZipItem.mod_date & 0x1F);
+ tm.tm_mon = (mZipItem.mod_date & 0x1E0) >> 5;
+ tm.tm_year = (mZipItem.mod_date & 0xFE00) >> 9;
+ buffer->st_atime = buffer->st_ctime = buffer->st_mtime = mktime(&tm);
+
+ buffer->st_size = mZipItem.usize;
+ buffer->st_dev = (buffer->st_dev << 16) ^ (buffer->st_ino << 16);
+ buffer->st_ino ^= mZipItem.crc32;
+ return ret;
+}
+
+int CZipFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ if (!buffer)
+ return -1;
+
+ if (!g_ZipManager.GetZipEntry(url, mZipItem))
+ {
+ if (url.GetFileName().empty() && CFile::Exists(url.GetHostName()))
+ { // when accessing the zip "root" recognize it as a directory
+ *buffer = {};
+ buffer->st_mode = _S_IFDIR;
+ return 0;
+ }
+ else
+ return -1;
+ }
+
+ *buffer = {};
+ buffer->st_gid = 0;
+ buffer->st_atime = buffer->st_ctime = mZipItem.mod_time;
+ buffer->st_size = mZipItem.usize;
+ return 0;
+}
+
+ssize_t CZipFile::Read(void* lpBuf, size_t uiBufSize)
+{
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+ if (m_bCached)
+ return mFile.Read(lpBuf,uiBufSize);
+
+ // flush what might be left in the string buffer
+ if (m_iDataInStringBuffer > 0)
+ {
+ size_t iMax = uiBufSize>m_iDataInStringBuffer?m_iDataInStringBuffer:uiBufSize;
+ memcpy(lpBuf,m_szStartOfStringBuffer,iMax);
+ uiBufSize -= iMax;
+ m_iDataInStringBuffer -= iMax;
+ }
+ if (mZipItem.method == 8) // deflated
+ {
+ uLong iDecompressed = 0;
+ uLong prevOut = m_ZStream.total_out;
+ while ((iDecompressed < uiBufSize) && ((m_iZipFilePos < mZipItem.csize) || (m_bFlush)))
+ {
+ m_ZStream.next_out = (Bytef*)(lpBuf)+iDecompressed;
+ m_ZStream.avail_out = static_cast<uInt>(uiBufSize-iDecompressed);
+ if (m_bFlush) // need to flush buffer !
+ {
+ int iMessage = inflate(&m_ZStream,Z_SYNC_FLUSH);
+ m_bFlush = ((iMessage == Z_OK) && (m_ZStream.avail_out == 0))?true:false;
+ if (!m_ZStream.avail_out) // flush filled buffer, get out of here
+ {
+ iDecompressed = m_ZStream.total_out-prevOut;
+ break;
+ }
+ }
+
+ if (!m_ZStream.avail_in)
+ {
+ if (!FillBuffer()) // eof!
+ {
+ iDecompressed = m_ZStream.total_out-prevOut;
+ break;
+ }
+ }
+
+ int iMessage = inflate(&m_ZStream,Z_SYNC_FLUSH);
+ if (iMessage < 0)
+ {
+ Close();
+ return -1; // READ ERROR
+ }
+
+ m_bFlush = ((iMessage == Z_OK) && (m_ZStream.avail_out == 0))?true:false; // more info in input buffer
+
+ iDecompressed = m_ZStream.total_out-prevOut;
+ }
+ m_iFilePos += iDecompressed;
+ return static_cast<unsigned int>(iDecompressed);
+ }
+ else if (mZipItem.method == 0) // uncompressed. just read from file, but mind our boundaries.
+ {
+ if (uiBufSize+m_iFilePos > mZipItem.csize)
+ uiBufSize = mZipItem.csize-m_iFilePos;
+
+ if (uiBufSize == 0)
+ return 0; // we are past eof, this shouldn't happen but test anyway
+
+ ssize_t iResult = mFile.Read(lpBuf,uiBufSize);
+ if (iResult < 0)
+ return -1;
+ m_iZipFilePos += iResult;
+ m_iFilePos += iResult;
+ return iResult;
+ }
+ else
+ return -1; // shouldn't happen. compression method checked in open
+}
+
+void CZipFile::Close()
+{
+ if (mZipItem.method == 8 && !m_bCached && m_iRead != -1)
+ inflateEnd(&m_ZStream);
+
+ mFile.Close();
+}
+
+bool CZipFile::FillBuffer()
+{
+ ssize_t sToRead = 65535;
+ if (m_iZipFilePos+65535 > mZipItem.csize)
+ sToRead = mZipItem.csize-m_iZipFilePos;
+
+ if (sToRead <= 0)
+ return false; // eof!
+
+ if (mFile.Read(m_szBuffer,sToRead) != sToRead)
+ return false;
+ m_ZStream.avail_in = static_cast<unsigned int>(sToRead);
+ m_ZStream.next_in = reinterpret_cast<Byte*>(m_szBuffer);
+ m_iZipFilePos += sToRead;
+ return true;
+}
+
+void CZipFile::DestroyBuffer(void* lpBuffer, int iBufSize)
+{
+ if (!m_bFlush)
+ return;
+ int iMessage = Z_OK;
+ while ((iMessage == Z_OK) && (m_ZStream.avail_out == 0))
+ {
+ m_ZStream.next_out = (Bytef*)lpBuffer;
+ m_ZStream.avail_out = iBufSize;
+ iMessage = inflate(&m_ZStream,Z_SYNC_FLUSH);
+ }
+ m_bFlush = false;
+}
+
+int CZipFile::UnpackFromMemory(std::string& strDest, const std::string& strInput, bool isGZ)
+{
+ unsigned int iPos=0;
+ int iResult=0;
+ while( iPos+LHDR_SIZE < strInput.size() || isGZ)
+ {
+ if (!isGZ)
+ {
+ CZipManager::readHeader(strInput.data()+iPos,mZipItem);
+ if (mZipItem.header == ZIP_DATA_RECORD_HEADER)
+ {
+ // this header concerns a file we already processed, so we can just skip it
+ iPos += DREC_SIZE;
+ continue;
+ }
+ if (mZipItem.header != ZIP_LOCAL_HEADER)
+ return iResult;
+ if( (mZipItem.flags & 8) == 8 )
+ {
+ // if an extended local header (=data record header) is present,
+ // the following fields are 0 in the local header and we need to read
+ // them from the extended local header
+
+ // search for the extended local header
+ unsigned int i = iPos + LHDR_SIZE + mZipItem.flength + mZipItem.elength;
+ while (1)
+ {
+ if (i + DREC_SIZE > strInput.size())
+ {
+ CLog::Log(LOGERROR, "FileZip: extended local header expected, but not present!");
+ return iResult;
+ }
+ if ((strInput[i] == 0x50) && (strInput[i + 1] == 0x4b) &&
+ (strInput[i + 2] == 0x07) && (strInput[i + 3] == 0x08))
+ break; // header found
+ i++;
+ }
+ // ZIP is little endian:
+ mZipItem.crc32 = static_cast<uint8_t>(strInput[i + 4]) |
+ static_cast<uint8_t>(strInput[i + 5]) << 8 |
+ static_cast<uint8_t>(strInput[i + 6]) << 16 |
+ static_cast<uint8_t>(strInput[i + 7]) << 24;
+ mZipItem.csize = static_cast<uint8_t>(strInput[i + 8]) |
+ static_cast<uint8_t>(strInput[i + 9]) << 8 |
+ static_cast<uint8_t>(strInput[i + 10]) << 16 |
+ static_cast<uint8_t>(strInput[i + 11]) << 24;
+ mZipItem.usize = static_cast<uint8_t>(strInput[i + 12]) |
+ static_cast<uint8_t>(strInput[i + 13]) << 8 |
+ static_cast<uint8_t>(strInput[i + 14]) << 16 |
+ static_cast<uint8_t>(strInput[i + 15]) << 24;
+ }
+ }
+ if (!InitDecompress())
+ return iResult;
+ // we have a file - fill the buffer
+ char* temp;
+ ssize_t toRead=0;
+ if (isGZ)
+ {
+ m_ZStream.avail_in = static_cast<unsigned int>(strInput.size());
+ m_ZStream.next_in = const_cast<Bytef*>((const Bytef*)strInput.data());
+ temp = new char[8192];
+ toRead = 8191;
+ }
+ else
+ {
+ m_ZStream.avail_in = mZipItem.csize;
+ m_ZStream.next_in = const_cast<Bytef*>((const Bytef*)strInput.data())+iPos+LHDR_SIZE+mZipItem.flength+mZipItem.elength;
+ // init m_zipitem
+ strDest.reserve(mZipItem.usize);
+ temp = new char[mZipItem.usize+1];
+ toRead = mZipItem.usize;
+ }
+ int iCurrResult;
+ while((iCurrResult = static_cast<int>(Read(temp, toRead))) > 0)
+ {
+ strDest.append(temp,temp+iCurrResult);
+ iResult += iCurrResult;
+ }
+ Close();
+ delete[] temp;
+ iPos += LHDR_SIZE+mZipItem.flength+mZipItem.elength+mZipItem.csize;
+ if (isGZ)
+ break;
+ }
+
+ return iResult;
+}
+
+bool CZipFile::DecompressGzip(const std::string& in, std::string& out)
+{
+ const int windowBits = MAX_WBITS + 16;
+
+ z_stream strm;
+ strm.zalloc = Z_NULL;
+ strm.zfree = Z_NULL;
+ strm.opaque = Z_NULL;
+
+ int err = inflateInit2(&strm, windowBits);
+ if (err != Z_OK)
+ {
+ CLog::Log(LOGERROR, "FileZip: zlib error {}", err);
+ return false;
+ }
+
+ const int bufferSize = 16384;
+ unsigned char buffer[bufferSize];
+
+ strm.avail_in = static_cast<unsigned int>(in.size());
+ strm.next_in = reinterpret_cast<unsigned char*>(const_cast<char*>(in.c_str()));
+
+ do
+ {
+ strm.avail_out = bufferSize;
+ strm.next_out = buffer;
+ int err = inflate(&strm, Z_NO_FLUSH);
+ switch (err)
+ {
+ case Z_NEED_DICT:
+ err = Z_DATA_ERROR;
+ [[fallthrough]];
+ case Z_DATA_ERROR:
+ case Z_MEM_ERROR:
+ case Z_STREAM_ERROR:
+ CLog::Log(LOGERROR, "FileZip: failed to decompress. zlib error {}", err);
+ inflateEnd(&strm);
+ return false;
+ }
+ int read = bufferSize - strm.avail_out;
+ out.append((char*)buffer, read);
+ }
+ while (strm.avail_out == 0);
+
+ inflateEnd(&strm);
+ return true;
+}
diff --git a/xbmc/filesystem/ZipFile.h b/xbmc/filesystem/ZipFile.h
new file mode 100644
index 0000000..1b09e5c
--- /dev/null
+++ b/xbmc/filesystem/ZipFile.h
@@ -0,0 +1,61 @@
+/*
+ * 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 "File.h"
+#include "IFile.h"
+#include "ZipManager.h"
+
+#include <zlib.h>
+
+namespace XFILE
+{
+ class CZipFile : public IFile
+ {
+ public:
+ CZipFile();
+ ~CZipFile() override;
+
+ int64_t GetPosition() override;
+ int64_t GetLength() override;
+ bool Open(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ int Stat(struct __stat64* buffer) override;
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+ ssize_t Read(void* lpBuf, size_t uiBufSize) override;
+ //virtual bool ReadString(char *szLine, int iLineLength);
+ int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override;
+ void Close() override;
+
+ //NOTE: gzip doesn't work. use DecompressGzip() instead
+ int UnpackFromMemory(std::string& strDest, const std::string& strInput, bool isGZ=false);
+
+ /*! Decompress gzip encoded buffer in-memory */
+ static bool DecompressGzip(const std::string& in, std::string& out);
+
+ private:
+ bool InitDecompress();
+ bool FillBuffer();
+ void DestroyBuffer(void* lpBuffer, int iBufSize);
+ CFile mFile;
+ SZipEntry mZipItem;
+ int64_t m_iFilePos = 0; // position in _uncompressed_ data read
+ int64_t m_iZipFilePos = 0; // position in _compressed_ data
+ int m_iAvailBuffer = 0;
+ z_stream m_ZStream;
+ char m_szBuffer[65535]; // 64k buffer for compressed data
+ char* m_szStringBuffer;
+ char* m_szStartOfStringBuffer; // never allocated!
+ size_t m_iDataInStringBuffer;
+ int m_iRead;
+ bool m_bFlush = false;
+ bool m_bCached;
+ };
+}
+
diff --git a/xbmc/filesystem/ZipManager.cpp b/xbmc/filesystem/ZipManager.cpp
new file mode 100644
index 0000000..710cfbc
--- /dev/null
+++ b/xbmc/filesystem/ZipManager.cpp
@@ -0,0 +1,320 @@
+/*
+ * 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 "ZipManager.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "File.h"
+#include "URL.h"
+#if defined(TARGET_POSIX)
+#include "PlatformDefs.h"
+#endif
+#include "utils/CharsetConverter.h"
+#include "utils/EndianSwap.h"
+#include "utils/log.h"
+#include "utils/RegExp.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE;
+
+static const size_t ZC_FLAG_EFS = 1 << 11; // general purpose bit 11 - zip holds utf-8 filenames
+
+CZipManager::CZipManager() = default;
+
+CZipManager::~CZipManager() = default;
+
+bool CZipManager::GetZipList(const CURL& url, std::vector<SZipEntry>& items)
+{
+ struct __stat64 m_StatData = {};
+
+ std::string strFile = url.GetHostName();
+
+ if (CFile::Stat(strFile,&m_StatData))
+ {
+ CLog::Log(LOGDEBUG, "CZipManager::GetZipList: failed to stat file {}", url.GetRedacted());
+ return false;
+ }
+
+ std::map<std::string, std::vector<SZipEntry> >::iterator it = mZipMap.find(strFile);
+ if (it != mZipMap.end()) // already listed, just return it if not changed, else release and reread
+ {
+ std::map<std::string,int64_t>::iterator it2=mZipDate.find(strFile);
+
+ if (m_StatData.st_mtime == it2->second)
+ {
+ items = it->second;
+ return true;
+ }
+ mZipMap.erase(it);
+ mZipDate.erase(it2);
+ }
+
+ CFile mFile;
+ if (!mFile.Open(strFile))
+ {
+ CLog::Log(LOGDEBUG, "ZipManager: unable to open file {}!", strFile);
+ return false;
+ }
+
+ unsigned int hdr;
+ if (mFile.Read(&hdr, 4)!=4 || (Endian_SwapLE32(hdr) != ZIP_LOCAL_HEADER &&
+ Endian_SwapLE32(hdr) != ZIP_DATA_RECORD_HEADER &&
+ Endian_SwapLE32(hdr) != ZIP_SPLIT_ARCHIVE_HEADER))
+ {
+ CLog::Log(LOGDEBUG,"ZipManager: not a zip file!");
+ mFile.Close();
+ return false;
+ }
+
+ if (Endian_SwapLE32(hdr) == ZIP_SPLIT_ARCHIVE_HEADER)
+ CLog::LogF(LOGWARNING, "ZIP split archive header found. Trying to process as a single archive..");
+
+ // push date for update detection
+ mZipDate.insert(make_pair(strFile,m_StatData.st_mtime));
+
+
+ // Look for end of central directory record
+ // Zipfile comment may be up to 65535 bytes
+ // End of central directory record is 22 bytes (ECDREC_SIZE)
+ // -> need to check the last 65557 bytes
+ int64_t fileSize = mFile.GetLength();
+ // Don't need to look in the last 18 bytes (ECDREC_SIZE-4)
+ // But as we need to do overlapping between blocks (3 bytes),
+ // we start the search at ECDREC_SIZE-1 from the end of file
+ if (fileSize < ECDREC_SIZE - 1)
+ {
+ CLog::Log(LOGERROR, "ZipManager: Invalid zip file length: {}", fileSize);
+ return false;
+ }
+ int searchSize = (int) std::min(static_cast<int64_t>(65557), fileSize-ECDREC_SIZE+1);
+ int blockSize = (int) std::min(1024, searchSize);
+ int nbBlock = searchSize / blockSize;
+ int extraBlockSize = searchSize % blockSize;
+ // Signature is on 4 bytes
+ // It could be between 2 blocks, so we need to read 3 extra bytes
+ std::vector<char> buffer(blockSize + 3);
+ bool found = false;
+
+ // Loop through blocks starting at the end of the file (minus ECDREC_SIZE-1)
+ for (int nb=1; !found && (nb <= nbBlock); nb++)
+ {
+ mFile.Seek(fileSize-ECDREC_SIZE+1-(blockSize*nb),SEEK_SET);
+ if (mFile.Read(buffer.data(), blockSize + 3) != blockSize + 3)
+ return false;
+ for (int i=blockSize-1; !found && (i >= 0); i--)
+ {
+ if (Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer.data() + i)) == ZIP_END_CENTRAL_HEADER)
+ {
+ // Set current position to start of end of central directory
+ mFile.Seek(fileSize-ECDREC_SIZE+1-(blockSize*nb)+i,SEEK_SET);
+ found = true;
+ }
+ }
+ }
+
+ // If not found, look in the last block left...
+ if ( !found && (extraBlockSize > 0) )
+ {
+ mFile.Seek(fileSize-ECDREC_SIZE+1-searchSize,SEEK_SET);
+ if (mFile.Read(buffer.data(), extraBlockSize + 3) != extraBlockSize + 3)
+ return false;
+ for (int i=extraBlockSize-1; !found && (i >= 0); i--)
+ {
+ if (Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer.data() + i)) == ZIP_END_CENTRAL_HEADER)
+ {
+ // Set current position to start of end of central directory
+ mFile.Seek(fileSize-ECDREC_SIZE+1-searchSize+i,SEEK_SET);
+ found = true;
+ }
+ }
+ }
+
+ buffer.clear();
+
+ if ( !found )
+ {
+ CLog::Log(LOGDEBUG, "ZipManager: broken file {}!", strFile);
+ mFile.Close();
+ return false;
+ }
+
+ unsigned int cdirOffset, cdirSize;
+ // Get size of the central directory
+ mFile.Seek(12,SEEK_CUR);
+ if (mFile.Read(&cdirSize, 4) != 4)
+ return false;
+ cdirSize = Endian_SwapLE32(cdirSize);
+ // Get Offset of start of central directory with respect to the starting disk number
+ if (mFile.Read(&cdirOffset, 4) != 4)
+ return false;
+ cdirOffset = Endian_SwapLE32(cdirOffset);
+
+ // Go to the start of central directory
+ mFile.Seek(cdirOffset,SEEK_SET);
+
+ CRegExp pathTraversal;
+ pathTraversal.RegComp(PATH_TRAVERSAL);
+
+ char temp[CHDR_SIZE];
+ while (mFile.GetPosition() < cdirOffset + cdirSize)
+ {
+ SZipEntry ze;
+ if (mFile.Read(temp, CHDR_SIZE) != CHDR_SIZE)
+ return false;
+ readCHeader(temp, ze);
+ if (ze.header != ZIP_CENTRAL_HEADER)
+ {
+ CLog::Log(LOGDEBUG, "ZipManager: broken file {}!", strFile);
+ mFile.Close();
+ return false;
+ }
+
+ // Get the filename just after the central file header
+ std::vector<char> bufName(ze.flength);
+ if (mFile.Read(bufName.data(), ze.flength) != ze.flength)
+ return false;
+ std::string strName(bufName.data(), bufName.size());
+ bufName.clear();
+ if ((ze.flags & ZC_FLAG_EFS) == 0)
+ {
+ std::string tmp(strName);
+ g_charsetConverter.ToUtf8("CP437", tmp, strName);
+ }
+ memset(ze.name, 0, 255);
+ strncpy(ze.name, strName.c_str(), strName.size() > 254 ? 254 : strName.size());
+
+ // Jump after central file header extra field and file comment
+ mFile.Seek(ze.eclength + ze.clength,SEEK_CUR);
+
+ if (pathTraversal.RegFind(strName) < 0)
+ items.push_back(ze);
+ }
+
+ /* go through list and figure out file header lengths */
+ for (auto& ze : items)
+ {
+ // Go to the local file header to get the extra field length
+ // !! local header extra field length != central file header extra field length !!
+ mFile.Seek(ze.lhdrOffset+28,SEEK_SET);
+ if (mFile.Read(&(ze.elength), 2) != 2)
+ return false;
+ ze.elength = Endian_SwapLE16(ze.elength);
+
+ // Compressed data offset = local header offset + size of local header + filename length + local file header extra field length
+ ze.offset = ze.lhdrOffset + LHDR_SIZE + ze.flength + ze.elength;
+
+ }
+
+ mZipMap.insert(make_pair(strFile,items));
+ mFile.Close();
+ return true;
+}
+
+bool CZipManager::GetZipEntry(const CURL& url, SZipEntry& item)
+{
+ const std::string& strFile = url.GetHostName();
+
+ std::map<std::string, std::vector<SZipEntry> >::iterator it = mZipMap.find(strFile);
+ std::vector<SZipEntry> items;
+ if (it == mZipMap.end()) // we need to list the zip
+ {
+ GetZipList(url,items);
+ }
+ else
+ {
+ items = it->second;
+ }
+
+ const std::string& strFileName = url.GetFileName();
+ for (const auto& it2 : items)
+ {
+ if (std::string(it2.name) == strFileName)
+ {
+ item = it2;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CZipManager::ExtractArchive(const std::string& strArchive, const std::string& strPath)
+{
+ const CURL pathToUrl(strArchive);
+ return ExtractArchive(pathToUrl, strPath);
+}
+
+bool CZipManager::ExtractArchive(const CURL& archive, const std::string& strPath)
+{
+ std::vector<SZipEntry> entry;
+ CURL url = URIUtils::CreateArchivePath("zip", archive);
+ GetZipList(url, entry);
+ for (const auto& it : entry)
+ {
+ if (it.name[strlen(it.name) - 1] == '/') // skip dirs
+ continue;
+ std::string strFilePath(it.name);
+
+ CURL zipPath = URIUtils::CreateArchivePath("zip", archive, strFilePath);
+ const CURL pathToUrl(strPath + strFilePath);
+ if (!CFile::Copy(zipPath, pathToUrl))
+ return false;
+ }
+ return true;
+}
+
+// Read local file header
+void CZipManager::readHeader(const char* buffer, SZipEntry& info)
+{
+ info.header = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer));
+ info.version = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 4));
+ info.flags = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 6));
+ info.method = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 8));
+ info.mod_time = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 10));
+ info.mod_date = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 12));
+ info.crc32 = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 14));
+ info.csize = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 18));
+ info.usize = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 22));
+ info.flength = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 26));
+ info.elength = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 28));
+}
+
+// Read central file header (from central directory)
+void CZipManager::readCHeader(const char* buffer, SZipEntry& info)
+{
+ info.header = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer));
+ // Skip version made by
+ info.version = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 6));
+ info.flags = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 8));
+ info.method = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 10));
+ info.mod_time = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 12));
+ info.mod_date = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 14));
+ info.crc32 = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 16));
+ info.csize = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 20));
+ info.usize = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 24));
+ info.flength = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 28));
+ info.eclength = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 30));
+ info.clength = Endian_SwapLE16(ReadUnaligned<uint16_t>(buffer + 32));
+ // Skip disk number start, internal/external file attributes
+ info.lhdrOffset = Endian_SwapLE32(ReadUnaligned<uint32_t>(buffer + 42));
+}
+
+void CZipManager::release(const std::string& strPath)
+{
+ CURL url(strPath);
+ std::map<std::string, std::vector<SZipEntry> >::iterator it= mZipMap.find(url.GetHostName());
+ if (it != mZipMap.end())
+ {
+ std::map<std::string,int64_t>::iterator it2=mZipDate.find(url.GetHostName());
+ mZipMap.erase(it);
+ mZipDate.erase(it2);
+ }
+}
+
+
diff --git a/xbmc/filesystem/ZipManager.h b/xbmc/filesystem/ZipManager.h
new file mode 100644
index 0000000..3fba27f
--- /dev/null
+++ b/xbmc/filesystem/ZipManager.h
@@ -0,0 +1,82 @@
+/*
+ * 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
+
+// See http://www.pkware.com/documents/casestudies/APPNOTE.TXT
+#define ZIP_LOCAL_HEADER 0x04034b50
+#define ZIP_DATA_RECORD_HEADER 0x08074b50
+#define ZIP_CENTRAL_HEADER 0x02014b50
+#define ZIP_END_CENTRAL_HEADER 0x06054b50
+#define ZIP_SPLIT_ARCHIVE_HEADER 0x30304b50
+#define LHDR_SIZE 30
+#define DREC_SIZE 16
+#define CHDR_SIZE 46
+#define ECDREC_SIZE 22
+
+#include <cstring>
+#include <map>
+#include <string>
+#include <vector>
+
+class CURL;
+
+static const std::string PATH_TRAVERSAL(R"_((^|\/|\\)\.{2}($|\/|\\))_");
+
+struct SZipEntry {
+ unsigned int header = 0;
+ unsigned short version = 0;
+ unsigned short flags = 0;
+ unsigned short method = 0;
+ unsigned short mod_time = 0;
+ unsigned short mod_date = 0;
+ unsigned int crc32 = 0;
+ unsigned int csize = 0; // compressed size
+ unsigned int usize = 0; // uncompressed size
+ unsigned short flength = 0; // filename length
+ unsigned short elength = 0; // extra field length (local file header)
+ unsigned short eclength = 0; // extra field length (central file header)
+ unsigned short clength = 0; // file comment length (central file header)
+ unsigned int lhdrOffset = 0; // Relative offset of local header
+ int64_t offset = 0; // offset in file to compressed data
+ char name[255];
+
+ SZipEntry()
+ {
+ name[0] = '\0';
+ }
+};
+
+class CZipManager
+{
+public:
+ CZipManager();
+ ~CZipManager();
+
+ bool GetZipList(const CURL& url, std::vector<SZipEntry>& items);
+ bool GetZipEntry(const CURL& url, SZipEntry& item);
+ bool ExtractArchive(const std::string& strArchive, const std::string& strPath);
+ bool ExtractArchive(const CURL& archive, const std::string& strPath);
+ void release(const std::string& strPath); // release resources used by list zip
+ static void readHeader(const char* buffer, SZipEntry& info);
+ static void readCHeader(const char* buffer, SZipEntry& info);
+private:
+ std::map<std::string,std::vector<SZipEntry> > mZipMap;
+ std::map<std::string,int64_t> mZipDate;
+
+ template<typename T>
+ static T ReadUnaligned(const void* mem)
+ {
+ T var;
+ std::memcpy(&var, mem, sizeof(T));
+ return var;
+ }
+};
+
+extern CZipManager g_ZipManager;
+
diff --git a/xbmc/filesystem/test/CMakeLists.txt b/xbmc/filesystem/test/CMakeLists.txt
new file mode 100644
index 0000000..9572459
--- /dev/null
+++ b/xbmc/filesystem/test/CMakeLists.txt
@@ -0,0 +1,15 @@
+set(SOURCES TestDirectory.cpp
+ TestFile.cpp
+ TestFileFactory.cpp
+ TestZipFile.cpp
+ TestZipManager.cpp)
+
+if(MICROHTTPD_FOUND)
+ list(APPEND SOURCES TestHTTPDirectory.cpp)
+endif()
+
+if(NFS_FOUND)
+ list(APPEND SOURCES TestNfsFile.cpp)
+endif()
+
+core_add_test_library(filesystem_test)
diff --git a/xbmc/filesystem/test/TestDirectory.cpp b/xbmc/filesystem/test/TestDirectory.cpp
new file mode 100644
index 0000000..e99408c
--- /dev/null
+++ b/xbmc/filesystem/test/TestDirectory.cpp
@@ -0,0 +1,56 @@
+/*
+ * 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 "FileItem.h"
+#include "filesystem/Directory.h"
+#include "filesystem/IDirectory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "test/TestUtils.h"
+#include "utils/URIUtils.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestDirectory, General)
+{
+ std::string tmppath1, tmppath2, tmppath3;
+ CFileItemList items;
+ CFileItemPtr itemptr;
+ tmppath1 = CSpecialProtocol::TranslatePath("special://temp/");
+ tmppath1 = URIUtils::AddFileToFolder(tmppath1, "TestDirectory");
+ tmppath2 = tmppath1;
+ tmppath2 = URIUtils::AddFileToFolder(tmppath2, "subdir");
+ EXPECT_TRUE(XFILE::CDirectory::Create(tmppath1));
+ EXPECT_TRUE(XFILE::CDirectory::Exists(tmppath1));
+ EXPECT_FALSE(XFILE::CDirectory::Exists(tmppath2));
+ EXPECT_TRUE(XFILE::CDirectory::Create(tmppath2));
+ EXPECT_TRUE(XFILE::CDirectory::Exists(tmppath2));
+ EXPECT_TRUE(XFILE::CDirectory::GetDirectory(tmppath1, items, "", XFILE::DIR_FLAG_DEFAULTS));
+ XFILE::CDirectory::FilterFileDirectories(items, "");
+ tmppath3 = tmppath2;
+ URIUtils::AddSlashAtEnd(tmppath3);
+ itemptr = items[0];
+ EXPECT_STREQ(tmppath3.c_str(), itemptr->GetPath().c_str());
+ EXPECT_TRUE(XFILE::CDirectory::Remove(tmppath2));
+ EXPECT_FALSE(XFILE::CDirectory::Exists(tmppath2));
+ EXPECT_TRUE(XFILE::CDirectory::Exists(tmppath1));
+ EXPECT_TRUE(XFILE::CDirectory::Remove(tmppath1));
+ EXPECT_FALSE(XFILE::CDirectory::Exists(tmppath1));
+}
+
+TEST(TestDirectory, CreateRecursive)
+{
+ auto path1 = URIUtils::AddFileToFolder(
+ CSpecialProtocol::TranslatePath("special://temp/"),
+ "level1");
+ auto path2 = URIUtils::AddFileToFolder(path1,
+ "level2",
+ "level3");
+
+ EXPECT_TRUE(XFILE::CDirectory::Create(path2));
+ EXPECT_TRUE(XFILE::CDirectory::RemoveRecursive(path1));
+}
diff --git a/xbmc/filesystem/test/TestFile.cpp b/xbmc/filesystem/test/TestFile.cpp
new file mode 100644
index 0000000..abefe46
--- /dev/null
+++ b/xbmc/filesystem/test/TestFile.cpp
@@ -0,0 +1,212 @@
+/*
+ * 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 "filesystem/File.h"
+#include "test/TestUtils.h"
+
+#include <errno.h>
+#include <string>
+
+#include <gtest/gtest.h>
+
+TEST(TestFile, Read)
+{
+ const std::string newLine = CXBMCTestUtils::Instance().getNewLineCharacters();
+ const size_t size = 1616;
+ const size_t lines = 25;
+ size_t addPerLine = newLine.length() - 1;
+ size_t realSize = size + lines * addPerLine;
+
+ const std::string firstBuf = "About" + newLine + "-----" + newLine + "XBMC is ";
+ const std::string secondBuf = "an award-winning fre";
+ const std::string thirdBuf = "ent hub for digital ";
+ const std::string fourthBuf = "rs, XBMC is a non-pr";
+ const std::string fifthBuf = "multimedia jukebox." + newLine;
+
+ XFILE::CFile file;
+ char buf[23] = {};
+
+ size_t currentPos;
+ ASSERT_TRUE(file.Open(
+ XBMC_REF_FILE_PATH("/xbmc/filesystem/test/reffile.txt")));
+ EXPECT_EQ(0, file.GetPosition());
+ EXPECT_EQ(realSize, file.GetLength());
+ EXPECT_EQ(firstBuf.length(), static_cast<size_t>(file.Read(buf, firstBuf.length())));
+ file.Flush();
+ currentPos = firstBuf.length();
+ EXPECT_EQ(currentPos, file.GetPosition());
+ EXPECT_EQ(0, memcmp(firstBuf.c_str(), buf, firstBuf.length()));
+ EXPECT_EQ(secondBuf.length(), static_cast<size_t>(file.Read(buf, secondBuf.length())));
+ currentPos += secondBuf.length();
+ EXPECT_EQ(currentPos, file.GetPosition());
+ EXPECT_EQ(0, memcmp(secondBuf.c_str(), buf, secondBuf.length()));
+ currentPos = 100 + addPerLine * 3;
+ EXPECT_EQ(currentPos, file.Seek(currentPos));
+ EXPECT_EQ(currentPos, file.GetPosition());
+ EXPECT_EQ(thirdBuf.length(), static_cast<size_t>(file.Read(buf, thirdBuf.length())));
+ file.Flush();
+ currentPos += thirdBuf.length();
+ EXPECT_EQ(currentPos, file.GetPosition());
+ EXPECT_EQ(0, memcmp(thirdBuf.c_str(), buf, thirdBuf.length()));
+ currentPos += 100 + addPerLine * 1;
+ EXPECT_EQ(currentPos, file.Seek(100 + addPerLine * 1, SEEK_CUR));
+ EXPECT_EQ(currentPos, file.GetPosition());
+ EXPECT_EQ(fourthBuf.length(), static_cast<size_t>(file.Read(buf, fourthBuf.length())));
+ file.Flush();
+ currentPos += fourthBuf.length();
+ EXPECT_EQ(currentPos, file.GetPosition());
+ EXPECT_EQ(0, memcmp(fourthBuf.c_str(), buf, fourthBuf.length()));
+ currentPos = realSize - fifthBuf.length();
+ EXPECT_EQ(currentPos, file.Seek(-(int64_t)fifthBuf.length(), SEEK_END));
+ EXPECT_EQ(currentPos, file.GetPosition());
+ EXPECT_EQ(fifthBuf.length(), static_cast<size_t>(file.Read(buf, fifthBuf.length())));
+ file.Flush();
+ currentPos += fifthBuf.length();
+ EXPECT_EQ(currentPos, file.GetPosition());
+ EXPECT_EQ(0, memcmp(fifthBuf.c_str(), buf, fifthBuf.length()));
+ currentPos += 100;
+ EXPECT_EQ(currentPos, file.Seek(100, SEEK_CUR));
+ EXPECT_EQ(currentPos, file.GetPosition());
+ currentPos = 0;
+ EXPECT_EQ(currentPos, file.Seek(currentPos, SEEK_SET));
+ EXPECT_EQ(firstBuf.length(), static_cast<size_t>(file.Read(buf, firstBuf.length())));
+ file.Flush();
+ currentPos += firstBuf.length();
+ EXPECT_EQ(currentPos, file.GetPosition());
+ EXPECT_EQ(0, memcmp(firstBuf.c_str(), buf, firstBuf.length()));
+ EXPECT_EQ(0, file.Seek(0, SEEK_SET));
+ EXPECT_EQ(-1, file.Seek(-100, SEEK_SET));
+ file.Close();
+}
+
+TEST(TestFile, Write)
+{
+ XFILE::CFile *file;
+ const char str[] = "TestFile.Write test string\n";
+ char buf[30] = {};
+
+ ASSERT_NE(nullptr, file = XBMC_CREATETEMPFILE(""));
+ file->Close();
+ ASSERT_TRUE(file->OpenForWrite(XBMC_TEMPFILEPATH(file), true));
+ EXPECT_EQ((int)sizeof(str), file->Write(str, sizeof(str)));
+ file->Flush();
+ EXPECT_EQ((int64_t)sizeof(str), file->GetPosition());
+ file->Close();
+ ASSERT_TRUE(file->Open(XBMC_TEMPFILEPATH(file)));
+ EXPECT_EQ(0, file->GetPosition());
+ EXPECT_EQ((int64_t)sizeof(str), file->Seek(0, SEEK_END));
+ EXPECT_EQ(0, file->Seek(0, SEEK_SET));
+ EXPECT_EQ((int64_t)sizeof(str), file->GetLength());
+ EXPECT_EQ(sizeof(str), static_cast<size_t>(file->Read(buf, sizeof(buf))));
+ file->Flush();
+ EXPECT_EQ((int64_t)sizeof(str), file->GetPosition());
+ EXPECT_EQ(0, memcmp(str, buf, sizeof(str)));
+ file->Close();
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(file));
+}
+
+TEST(TestFile, Exists)
+{
+ XFILE::CFile *file;
+
+ ASSERT_NE(nullptr, file = XBMC_CREATETEMPFILE(""));
+ file->Close();
+ EXPECT_TRUE(XFILE::CFile::Exists(XBMC_TEMPFILEPATH(file)));
+ EXPECT_FALSE(XFILE::CFile::Exists(""));
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(file));
+}
+
+TEST(TestFile, Stat)
+{
+ XFILE::CFile *file;
+ struct __stat64 buffer;
+
+ ASSERT_NE(nullptr, file = XBMC_CREATETEMPFILE(""));
+ EXPECT_EQ(0, file->Stat(&buffer));
+ file->Close();
+ EXPECT_NE(0U, buffer.st_mode | _S_IFREG);
+ EXPECT_EQ(-1, XFILE::CFile::Stat("", &buffer));
+ EXPECT_EQ(ENOENT, errno);
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(file));
+}
+
+TEST(TestFile, Delete)
+{
+ XFILE::CFile *file;
+ std::string path;
+
+ ASSERT_NE(nullptr, file = XBMC_CREATETEMPFILE(""));
+ file->Close();
+ path = XBMC_TEMPFILEPATH(file);
+ EXPECT_TRUE(XFILE::CFile::Exists(path));
+ EXPECT_TRUE(XFILE::CFile::Delete(path));
+ EXPECT_FALSE(XFILE::CFile::Exists(path));
+ EXPECT_FALSE(XBMC_DELETETEMPFILE(file));
+}
+
+TEST(TestFile, Rename)
+{
+ XFILE::CFile *file1, *file2;
+ std::string path1, path2;
+
+ ASSERT_NE(nullptr, file1 = XBMC_CREATETEMPFILE(""));
+ file1->Close();
+ path1 = XBMC_TEMPFILEPATH(file1);
+ ASSERT_NE(nullptr, file2 = XBMC_CREATETEMPFILE(""));
+ file2->Close();
+ path2 = XBMC_TEMPFILEPATH(file2);
+ EXPECT_TRUE(XFILE::CFile::Delete(path1));
+ EXPECT_FALSE(XFILE::CFile::Exists(path1));
+ EXPECT_TRUE(XFILE::CFile::Exists(path2));
+ EXPECT_TRUE(XFILE::CFile::Rename(path2, path1));
+ EXPECT_TRUE(XFILE::CFile::Exists(path1));
+ EXPECT_FALSE(XFILE::CFile::Exists(path2));
+ EXPECT_TRUE(XFILE::CFile::Delete(path1));
+ EXPECT_FALSE(XBMC_DELETETEMPFILE(file1));
+ EXPECT_FALSE(XBMC_DELETETEMPFILE(file2));
+}
+
+TEST(TestFile, Copy)
+{
+ XFILE::CFile *file1, *file2;
+ std::string path1, path2;
+
+ ASSERT_NE(nullptr, file1 = XBMC_CREATETEMPFILE(""));
+ file1->Close();
+ path1 = XBMC_TEMPFILEPATH(file1);
+ ASSERT_NE(nullptr, file2 = XBMC_CREATETEMPFILE(""));
+ file2->Close();
+ path2 = XBMC_TEMPFILEPATH(file2);
+ EXPECT_TRUE(XFILE::CFile::Delete(path1));
+ EXPECT_FALSE(XFILE::CFile::Exists(path1));
+ EXPECT_TRUE(XFILE::CFile::Exists(path2));
+ EXPECT_TRUE(XFILE::CFile::Copy(path2, path1));
+ EXPECT_TRUE(XFILE::CFile::Exists(path1));
+ EXPECT_TRUE(XFILE::CFile::Exists(path2));
+ EXPECT_TRUE(XFILE::CFile::Delete(path1));
+ EXPECT_TRUE(XFILE::CFile::Delete(path2));
+ EXPECT_FALSE(XBMC_DELETETEMPFILE(file1));
+ EXPECT_FALSE(XBMC_DELETETEMPFILE(file2));
+}
+
+TEST(TestFile, SetHidden)
+{
+ XFILE::CFile *file;
+
+ ASSERT_NE(nullptr, file = XBMC_CREATETEMPFILE(""));
+ file->Close();
+ EXPECT_TRUE(XFILE::CFile::Exists(XBMC_TEMPFILEPATH(file)));
+ bool result = XFILE::CFile::SetHidden(XBMC_TEMPFILEPATH(file), true);
+#ifdef TARGET_WINDOWS
+ EXPECT_TRUE(result);
+#else
+ EXPECT_FALSE(result);
+#endif
+ EXPECT_TRUE(XFILE::CFile::Exists(XBMC_TEMPFILEPATH(file)));
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(file));
+}
diff --git a/xbmc/filesystem/test/TestFileFactory.cpp b/xbmc/filesystem/test/TestFileFactory.cpp
new file mode 100644
index 0000000..6129592
--- /dev/null
+++ b/xbmc/filesystem/test/TestFileFactory.cpp
@@ -0,0 +1,161 @@
+/*
+ * 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 "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "test/TestUtils.h"
+#include "utils/StringUtils.h"
+
+#include <gtest/gtest.h>
+
+class TestFileFactory : public testing::Test
+{
+protected:
+ TestFileFactory()
+ {
+ std::vector<std::string> advancedsettings =
+ CXBMCTestUtils::Instance().getAdvancedSettingsFiles();
+ std::vector<std::string> guisettings =
+ CXBMCTestUtils::Instance().getGUISettingsFiles();
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ for (const auto& it : guisettings)
+ settings->Load(it);
+
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+ for (const auto& it : advancedsettings)
+ advancedSettings->ParseSettingsFile(it);
+
+ settings->SetLoaded();
+ }
+
+ ~TestFileFactory() override
+ {
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Unload();
+ }
+};
+
+/* The tests for XFILE::CFileFactory are tested indirectly through
+ * XFILE::CFile. Since most parts of the VFS require some form of
+ * network connection, the settings and VFS URLs must be given as
+ * arguments in the main testsuite program.
+ */
+TEST_F(TestFileFactory, Read)
+{
+ XFILE::CFile file;
+ std::string str;
+ ssize_t size, i;
+ unsigned char buf[16];
+ int64_t count = 0;
+
+ std::vector<std::string> urls =
+ CXBMCTestUtils::Instance().getTestFileFactoryReadUrls();
+
+ for (const auto& url : urls)
+ {
+ std::cout << "Testing URL: " << url << std::endl;
+ ASSERT_TRUE(file.Open(url));
+ std::cout << "file.GetLength(): " <<
+ testing::PrintToString(file.GetLength()) << std::endl;
+ std::cout << "file.Seek(file.GetLength() / 2, SEEK_CUR) return value: " <<
+ testing::PrintToString(file.Seek(file.GetLength() / 2, SEEK_CUR)) << std::endl;
+ std::cout << "file.Seek(0, SEEK_END) return value: " <<
+ testing::PrintToString(file.Seek(0, SEEK_END)) << std::endl;
+ std::cout << "file.Seek(0, SEEK_SET) return value: " <<
+ testing::PrintToString(file.Seek(0, SEEK_SET)) << std::endl;
+ std::cout << "File contents:" << std::endl;
+ while ((size = file.Read(buf, sizeof(buf))) > 0)
+ {
+ str = StringUtils::Format(" {:08X}", count);
+ std::cout << str << " ";
+ count += size;
+ for (i = 0; i < size; i++)
+ {
+ str = StringUtils::Format("{:02X} ", buf[i]);
+ std::cout << str;
+ }
+ while (i++ < static_cast<ssize_t> (sizeof(buf)))
+ std::cout << " ";
+ std::cout << " [";
+ for (i = 0; i < size; i++)
+ {
+ if (buf[i] >= ' ' && buf[i] <= '~')
+ std::cout << buf[i];
+ else
+ std::cout << ".";
+ }
+ std::cout << "]" << std::endl;
+ }
+ file.Close();
+ }
+}
+
+TEST_F(TestFileFactory, Write)
+{
+ XFILE::CFile file, inputfile;
+ std::string str;
+ size_t size, i;
+ unsigned char buf[16];
+ int64_t count = 0;
+
+ str = CXBMCTestUtils::Instance().getTestFileFactoryWriteInputFile();
+ ASSERT_TRUE(inputfile.Open(str));
+
+ std::vector<std::string> urls =
+ CXBMCTestUtils::Instance().getTestFileFactoryWriteUrls();
+
+ for (const auto& url : urls)
+ {
+ std::cout << "Testing URL: " << url << std::endl;
+ std::cout << "Writing...";
+ ASSERT_TRUE(file.OpenForWrite(url, true));
+ while ((size = inputfile.Read(buf, sizeof(buf))) > 0)
+ {
+ EXPECT_GE(file.Write(buf, size), 0);
+ }
+ file.Close();
+ std::cout << "done." << std::endl;
+ std::cout << "Reading..." << std::endl;
+ ASSERT_TRUE(file.Open(url));
+ EXPECT_EQ(inputfile.GetLength(), file.GetLength());
+ std::cout << "file.Seek(file.GetLength() / 2, SEEK_CUR) return value: " <<
+ testing::PrintToString(file.Seek(file.GetLength() / 2, SEEK_CUR)) << std::endl;
+ std::cout << "file.Seek(0, SEEK_END) return value: " <<
+ testing::PrintToString(file.Seek(0, SEEK_END)) << std::endl;
+ std::cout << "file.Seek(0, SEEK_SET) return value: " <<
+ testing::PrintToString(file.Seek(0, SEEK_SET)) << std::endl;
+ std::cout << "File contents:\n";
+ while ((size = file.Read(buf, sizeof(buf))) > 0)
+ {
+ str = StringUtils::Format(" {:08X}", count);
+ std::cout << str << " ";
+ count += size;
+ for (i = 0; i < size; i++)
+ {
+ str = StringUtils::Format("{:02X} ", buf[i]);
+ std::cout << str;
+ }
+ while (i++ < sizeof(buf))
+ std::cout << " ";
+ std::cout << " [";
+ for (i = 0; i < size; i++)
+ {
+ if (buf[i] >= ' ' && buf[i] <= '~')
+ std::cout << buf[i];
+ else
+ std::cout << ".";
+ }
+ std::cout << "]" << std::endl;
+ }
+ file.Close();
+ }
+ inputfile.Close();
+}
diff --git a/xbmc/filesystem/test/TestHTTPDirectory.cpp b/xbmc/filesystem/test/TestHTTPDirectory.cpp
new file mode 100644
index 0000000..7736307
--- /dev/null
+++ b/xbmc/filesystem/test/TestHTTPDirectory.cpp
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2015-2020 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 "FileItem.h"
+#include "URL.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/HTTPDirectory.h"
+#include "network/WebServer.h"
+#include "network/httprequesthandler/HTTPVfsHandler.h"
+#include "settings/MediaSourceSettings.h"
+#include "test/TestUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XTimeUtils.h"
+
+#include <random>
+#include <stdlib.h>
+
+#include <gtest/gtest.h>
+
+using namespace XFILE;
+
+#define WEBSERVER_HOST "localhost"
+
+#define SOURCE_PATH "xbmc/filesystem/test/data/httpdirectory/"
+
+#define TEST_FILE_APACHE_DEFAULT "apache-default.html"
+#define TEST_FILE_APACHE_FANCY "apache-fancy.html"
+#define TEST_FILE_APACHE_HTML "apache-html.html"
+#define TEST_FILE_BASIC "basic.html"
+#define TEST_FILE_BASIC_MULTILINE "basic-multiline.html"
+#define TEST_FILE_LIGHTTP_DEFAULT "lighttp-default.html"
+#define TEST_FILE_NGINX_DEFAULT "nginx-default.html"
+#define TEST_FILE_NGINX_FANCYINDEX "nginx-fancyindex.html"
+
+#define SAMPLE_ITEM_COUNT 6
+
+#define SAMPLE_ITEM_1_LABEL "folder1"
+#define SAMPLE_ITEM_2_LABEL "folder2"
+#define SAMPLE_ITEM_3_LABEL "sample3: the sampling.mpg"
+#define SAMPLE_ITEM_4_LABEL "sample & samplability 4.mpg"
+#define SAMPLE_ITEM_5_LABEL "sample5.mpg"
+#define SAMPLE_ITEM_6_LABEL "sample6.mpg"
+
+#define SAMPLE_ITEM_1_SIZE 0
+#define SAMPLE_ITEM_2_SIZE 0
+#define SAMPLE_ITEM_3_SIZE 123
+#define SAMPLE_ITEM_4_SIZE 125952 // 123K
+#define SAMPLE_ITEM_5_SIZE 128974848 // 123M
+#define SAMPLE_ITEM_6_SIZE 132070244352 // 123G
+
+// HTTPDirectory ignores the seconds component of parsed date/times
+#define SAMPLE_ITEM_1_DATETIME "2019-01-01 01:01:00"
+#define SAMPLE_ITEM_2_DATETIME "2019-02-02 02:02:00"
+#define SAMPLE_ITEM_3_DATETIME "2019-03-03 03:03:00"
+#define SAMPLE_ITEM_4_DATETIME "2019-04-04 04:04:00"
+#define SAMPLE_ITEM_5_DATETIME "2019-05-05 05:05:00"
+#define SAMPLE_ITEM_6_DATETIME "2019-06-06 06:06:00"
+
+class TestHTTPDirectory : public testing::Test
+{
+protected:
+ TestHTTPDirectory() : m_sourcePath(XBMC_REF_FILE_PATH(SOURCE_PATH))
+ {
+ std::random_device rd;
+ std::mt19937 mt(rd());
+ std::uniform_int_distribution<uint16_t> dist(49152, 65535);
+ m_webServerPort = dist(mt);
+
+ m_baseUrl = StringUtils::Format("http://" WEBSERVER_HOST ":{}", m_webServerPort);
+ }
+
+ ~TestHTTPDirectory() override = default;
+
+protected:
+ void SetUp() override
+ {
+ SetupMediaSources();
+
+ m_webServer.Start(m_webServerPort, "", "");
+ m_webServer.RegisterRequestHandler(&m_vfsHandler);
+ }
+
+ void TearDown() override
+ {
+ if (m_webServer.IsStarted())
+ m_webServer.Stop();
+
+ m_webServer.UnregisterRequestHandler(&m_vfsHandler);
+
+ TearDownMediaSources();
+ }
+
+ void SetupMediaSources()
+ {
+ CMediaSource source;
+ source.strName = "WebServer Share";
+ source.strPath = m_sourcePath;
+ source.vecPaths.push_back(m_sourcePath);
+ source.m_allowSharing = true;
+ source.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ source.m_iLockMode = LOCK_MODE_EVERYONE;
+ source.m_ignore = true;
+
+ CMediaSourceSettings::GetInstance().AddShare("videos", source);
+ }
+
+ void TearDownMediaSources() { CMediaSourceSettings::GetInstance().Clear(); }
+
+ std::string GetUrl(const std::string& path)
+ {
+ if (path.empty())
+ return m_baseUrl;
+
+ return URIUtils::AddFileToFolder(m_baseUrl, path);
+ }
+
+ std::string GetUrlOfTestFile(const std::string& testFile)
+ {
+ if (testFile.empty())
+ return "";
+
+ std::string path = URIUtils::AddFileToFolder(m_sourcePath, testFile);
+ path = CURL::Encode(path);
+ path = URIUtils::AddFileToFolder("vfs", path);
+
+ return GetUrl(path);
+ }
+
+ void CheckFileItemTypes(CFileItemList const& items)
+ {
+ ASSERT_EQ(items.GetObjectCount(), SAMPLE_ITEM_COUNT);
+
+ // folders
+ ASSERT_TRUE(items[0]->m_bIsFolder);
+ ASSERT_TRUE(items[1]->m_bIsFolder);
+
+ // files
+ ASSERT_FALSE(items[2]->m_bIsFolder);
+ ASSERT_FALSE(items[3]->m_bIsFolder);
+ ASSERT_FALSE(items[4]->m_bIsFolder);
+ ASSERT_FALSE(items[5]->m_bIsFolder);
+ }
+
+ void CheckFileItemLabels(CFileItemList const& items)
+ {
+ ASSERT_EQ(items.GetObjectCount(), SAMPLE_ITEM_COUNT);
+
+ ASSERT_STREQ(items[0]->GetLabel().c_str(), SAMPLE_ITEM_1_LABEL);
+ ASSERT_STREQ(items[1]->GetLabel().c_str(), SAMPLE_ITEM_2_LABEL);
+ ASSERT_STREQ(items[2]->GetLabel().c_str(), SAMPLE_ITEM_3_LABEL);
+ ASSERT_STREQ(items[3]->GetLabel().c_str(), SAMPLE_ITEM_4_LABEL);
+ ASSERT_STREQ(items[4]->GetLabel().c_str(), SAMPLE_ITEM_5_LABEL);
+ ASSERT_STREQ(items[5]->GetLabel().c_str(), SAMPLE_ITEM_6_LABEL);
+ }
+
+ void CheckFileItemDateTimes(CFileItemList const& items)
+ {
+ ASSERT_EQ(items.GetObjectCount(), SAMPLE_ITEM_COUNT);
+
+ ASSERT_STREQ(items[0]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_1_DATETIME);
+ ASSERT_STREQ(items[1]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_2_DATETIME);
+ ASSERT_STREQ(items[2]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_3_DATETIME);
+ ASSERT_STREQ(items[3]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_4_DATETIME);
+ ASSERT_STREQ(items[4]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_5_DATETIME);
+ ASSERT_STREQ(items[5]->m_dateTime.GetAsDBDateTime().c_str(), SAMPLE_ITEM_6_DATETIME);
+ }
+
+ void CheckFileItemSizes(CFileItemList const& items)
+ {
+ ASSERT_EQ(items.GetObjectCount(), SAMPLE_ITEM_COUNT);
+
+ // folders
+ ASSERT_EQ(items[0]->m_dwSize, SAMPLE_ITEM_1_SIZE);
+ ASSERT_EQ(items[1]->m_dwSize, SAMPLE_ITEM_2_SIZE);
+
+ // files - due to K/M/G conversions provided by some formats, allow for
+ // non-zero values that are less than or equal to the expected file size
+ ASSERT_NE(items[2]->m_dwSize, 0);
+ ASSERT_LE(items[2]->m_dwSize, SAMPLE_ITEM_3_SIZE);
+ ASSERT_NE(items[3]->m_dwSize, 0);
+ ASSERT_LE(items[3]->m_dwSize, SAMPLE_ITEM_4_SIZE);
+ ASSERT_NE(items[4]->m_dwSize, 0);
+ ASSERT_LE(items[4]->m_dwSize, SAMPLE_ITEM_5_SIZE);
+ ASSERT_NE(items[5]->m_dwSize, 0);
+ ASSERT_LE(items[5]->m_dwSize, SAMPLE_ITEM_6_SIZE);
+ }
+
+ void CheckFileItems(CFileItemList const& items)
+ {
+ CheckFileItemTypes(items);
+ CheckFileItemLabels(items);
+ }
+
+ void CheckFileItemsAndMetadata(CFileItemList const& items)
+ {
+ CheckFileItems(items);
+ CheckFileItemDateTimes(items);
+ CheckFileItemSizes(items);
+ }
+
+ CWebServer m_webServer;
+ uint16_t m_webServerPort;
+ std::string m_baseUrl;
+ std::string const m_sourcePath;
+ CHTTPVfsHandler m_vfsHandler;
+ CHTTPDirectory m_httpDirectory;
+};
+
+TEST_F(TestHTTPDirectory, IsStarted)
+{
+ ASSERT_TRUE(m_webServer.IsStarted());
+}
+
+TEST_F(TestHTTPDirectory, ApacheDefaultIndex)
+{
+ CFileItemList items;
+
+ ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_DEFAULT))));
+ ASSERT_TRUE(
+ m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_DEFAULT)), items));
+
+ CheckFileItems(items);
+}
+
+TEST_F(TestHTTPDirectory, ApacheFancyIndex)
+{
+ CFileItemList items;
+
+ ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_FANCY))));
+ ASSERT_TRUE(m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_FANCY)), items));
+
+ CheckFileItemsAndMetadata(items);
+}
+
+TEST_F(TestHTTPDirectory, ApacheHtmlIndex)
+{
+ CFileItemList items;
+
+ ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_HTML))));
+ ASSERT_TRUE(m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_APACHE_HTML)), items));
+
+ CheckFileItemsAndMetadata(items);
+}
+
+TEST_F(TestHTTPDirectory, BasicIndex)
+{
+ CFileItemList items;
+
+ ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_BASIC))));
+ ASSERT_TRUE(m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_BASIC)), items));
+
+ CheckFileItems(items);
+}
+
+TEST_F(TestHTTPDirectory, BasicMultilineIndex)
+{
+ CFileItemList items;
+
+ ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_BASIC_MULTILINE))));
+ ASSERT_TRUE(
+ m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_BASIC_MULTILINE)), items));
+
+ CheckFileItems(items);
+}
+
+TEST_F(TestHTTPDirectory, LighttpDefaultIndex)
+{
+ CFileItemList items;
+
+ ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_LIGHTTP_DEFAULT))));
+ ASSERT_TRUE(
+ m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_LIGHTTP_DEFAULT)), items));
+
+ CheckFileItemsAndMetadata(items);
+}
+
+TEST_F(TestHTTPDirectory, NginxDefaultIndex)
+{
+ CFileItemList items;
+
+ ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_NGINX_DEFAULT))));
+ ASSERT_TRUE(m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_NGINX_DEFAULT)), items));
+
+ CheckFileItemsAndMetadata(items);
+}
+
+TEST_F(TestHTTPDirectory, NginxFancyIndex)
+{
+ CFileItemList items;
+
+ ASSERT_TRUE(m_httpDirectory.Exists(CURL(GetUrlOfTestFile(TEST_FILE_NGINX_FANCYINDEX))));
+ ASSERT_TRUE(m_httpDirectory.GetDirectory(CURL(GetUrlOfTestFile(TEST_FILE_NGINX_FANCYINDEX)), items));
+
+ CheckFileItemsAndMetadata(items);
+}
diff --git a/xbmc/filesystem/test/TestNfsFile.cpp b/xbmc/filesystem/test/TestNfsFile.cpp
new file mode 100644
index 0000000..69b1203
--- /dev/null
+++ b/xbmc/filesystem/test/TestNfsFile.cpp
@@ -0,0 +1,84 @@
+/*
+ * 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 "URL.h"
+#include "filesystem/NFSFile.h"
+#include "test/TestUtils.h"
+
+#include <errno.h>
+#include <string>
+
+#include <gtest/gtest.h>
+
+using ::testing::Test;
+using ::testing::WithParamInterface;
+using ::testing::ValuesIn;
+
+struct SplitPath
+{
+ std::string url;
+ std::string exportPath;
+ std::string relativePath;
+ bool expectedResultExport;
+ bool expectedResultPath;
+} g_TestData[] = {
+ {"nfs://192.168.0.1:2049/srv/test/tvmedia/foo.txt", "/srv/test", "//tvmedia/foo.txt", true, true},
+ {"nfs://192.168.0.1/srv/test/tv/media/foo.txt", "/srv/test/tv", "//media/foo.txt", true, true},
+ {"nfs://192.168.0.1:2049/srv/test/tvmedia", "/srv/test", "//tvmedia", true, true},
+ {"nfs://192.168.0.1:2049/srv/test/tvmedia/", "/srv/test", "//tvmedia/", true, true},
+ {"nfs://192.168.0.1:2049/srv/test/tv/media", "/srv/test/tv", "//media", true, true},
+ {"nfs://192.168.0.1:2049/srv/test/tv/media/", "/srv/test/tv", "//media/", true, true},
+ {"nfs://192.168.0.1:2049/srv/test/tv", "/srv/test/tv", "//", true, true},
+ {"nfs://192.168.0.1:2049/srv/test/", "/srv/test", "//", true, true},
+ {"nfs://192.168.0.1:2049/", "/", "//", true, true},
+ {"nfs://192.168.0.1:2049/notexported/foo.txt", "/", "//notexported/foo.txt", true, true},
+
+ {"nfs://192.168.0.1:2049/notexported/foo.txt", "/notexported", "//foo.txt", false, false},
+ };
+
+class TestNfs : public Test,
+ public WithParamInterface<SplitPath>
+{
+};
+
+class ExportList
+{
+ public:
+ std::list<std::string> data;
+
+ ExportList()
+ {
+ data.emplace_back("/srv/test");
+ data.emplace_back("/srv/test/tv");
+ data.emplace_back("/");
+ data.sort();
+ data.reverse();
+ }
+};
+
+static ExportList exportList;
+
+TEST_P(TestNfs, splitUrlIntoExportAndPath)
+{
+ CURL url(GetParam().url);
+ std::string exportPath;
+ std::string relativePath;
+ gNfsConnection.splitUrlIntoExportAndPath(url, exportPath, relativePath, exportList.data);
+
+ if (GetParam().expectedResultExport)
+ EXPECT_STREQ(GetParam().exportPath.c_str(), exportPath.c_str());
+ else
+ EXPECT_STRNE(GetParam().exportPath.c_str(), exportPath.c_str());
+
+ if (GetParam().expectedResultPath)
+ EXPECT_STREQ(GetParam().relativePath.c_str(), relativePath.c_str());
+ else
+ EXPECT_STRNE(GetParam().relativePath.c_str(), relativePath.c_str());
+}
+
+INSTANTIATE_TEST_SUITE_P(NfsFile, TestNfs, ValuesIn(g_TestData));
diff --git a/xbmc/filesystem/test/TestZipFile.cpp b/xbmc/filesystem/test/TestZipFile.cpp
new file mode 100644
index 0000000..3ff518b
--- /dev/null
+++ b/xbmc/filesystem/test/TestZipFile.cpp
@@ -0,0 +1,211 @@
+/*
+ * 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 "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/ZipFile.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "test/TestUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <errno.h>
+
+#include <gtest/gtest.h>
+
+class TestZipFile : public testing::Test
+{
+protected:
+ TestZipFile() = default;
+
+ ~TestZipFile() override
+ {
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Unload();
+ }
+};
+
+TEST_F(TestZipFile, Read)
+{
+ XFILE::CFile file;
+ char buf[20] = {};
+ std::string reffile, strpathinzip;
+ CFileItemList itemlist;
+
+ reffile = XBMC_REF_FILE_PATH("xbmc/filesystem/test/reffile.txt.zip");
+ CURL zipUrl = URIUtils::CreateArchivePath("zip", CURL(reffile), "");
+ ASSERT_TRUE(XFILE::CDirectory::GetDirectory(zipUrl, itemlist, "",
+ XFILE::DIR_FLAG_NO_FILE_DIRS));
+ EXPECT_GT(itemlist.Size(), 0);
+ EXPECT_FALSE(itemlist[0]->GetPath().empty());
+ strpathinzip = itemlist[0]->GetPath();
+ ASSERT_TRUE(file.Open(strpathinzip));
+ EXPECT_EQ(0, file.GetPosition());
+ EXPECT_EQ(1616, file.GetLength());
+ EXPECT_EQ(sizeof(buf), static_cast<size_t>(file.Read(buf, sizeof(buf))));
+ file.Flush();
+ EXPECT_EQ(20, file.GetPosition());
+ EXPECT_TRUE(!memcmp("About\n-----\nXBMC is ", buf, sizeof(buf) - 1));
+ EXPECT_TRUE(file.ReadString(buf, sizeof(buf)));
+ EXPECT_EQ(39, file.GetPosition());
+ EXPECT_STREQ("an award-winning fr", buf);
+ EXPECT_EQ(100, file.Seek(100));
+ EXPECT_EQ(100, file.GetPosition());
+ EXPECT_EQ(sizeof(buf), static_cast<size_t>(file.Read(buf, sizeof(buf))));
+ file.Flush();
+ EXPECT_EQ(120, file.GetPosition());
+ EXPECT_TRUE(!memcmp("ent hub for digital ", buf, sizeof(buf) - 1));
+ EXPECT_EQ(220, file.Seek(100, SEEK_CUR));
+ EXPECT_EQ(220, file.GetPosition());
+ EXPECT_EQ(sizeof(buf), static_cast<size_t>(file.Read(buf, sizeof(buf))));
+ file.Flush();
+ EXPECT_EQ(240, file.GetPosition());
+ EXPECT_TRUE(!memcmp("rs, XBMC is a non-pr", buf, sizeof(buf) - 1));
+ EXPECT_EQ(1596, file.Seek(-(int64_t)sizeof(buf), SEEK_END));
+ EXPECT_EQ(1596, file.GetPosition());
+ EXPECT_EQ(sizeof(buf), static_cast<size_t>(file.Read(buf, sizeof(buf))));
+ file.Flush();
+ EXPECT_EQ(1616, file.GetPosition());
+ EXPECT_TRUE(!memcmp("multimedia jukebox.\n", buf, sizeof(buf) - 1));
+ EXPECT_EQ(-1, file.Seek(100, SEEK_CUR));
+ EXPECT_EQ(1616, file.GetPosition());
+ EXPECT_EQ(0, file.Seek(0, SEEK_SET));
+ EXPECT_EQ(sizeof(buf), static_cast<size_t>(file.Read(buf, sizeof(buf))));
+ file.Flush();
+ EXPECT_EQ(20, file.GetPosition());
+ EXPECT_TRUE(!memcmp("About\n-----\nXBMC is ", buf, sizeof(buf) - 1));
+ EXPECT_EQ(0, file.Seek(0, SEEK_SET));
+ EXPECT_EQ(-1, file.Seek(-100, SEEK_SET));
+ file.Close();
+}
+
+TEST_F(TestZipFile, Exists)
+{
+ std::string reffile, strpathinzip;
+ CFileItemList itemlist;
+
+ reffile = XBMC_REF_FILE_PATH("xbmc/filesystem/test/reffile.txt.zip");
+ CURL zipUrl = URIUtils::CreateArchivePath("zip", CURL(reffile), "");
+ ASSERT_TRUE(XFILE::CDirectory::GetDirectory(zipUrl, itemlist, "",
+ XFILE::DIR_FLAG_NO_FILE_DIRS));
+ strpathinzip = itemlist[0]->GetPath();
+
+ EXPECT_TRUE(XFILE::CFile::Exists(strpathinzip));
+}
+
+TEST_F(TestZipFile, Stat)
+{
+ struct __stat64 buffer;
+ std::string reffile, strpathinzip;
+ CFileItemList itemlist;
+
+ reffile = XBMC_REF_FILE_PATH("xbmc/filesystem/test/reffile.txt.zip");
+ CURL zipUrl = URIUtils::CreateArchivePath("zip", CURL(reffile), "");
+ ASSERT_TRUE(XFILE::CDirectory::GetDirectory(zipUrl, itemlist, "",
+ XFILE::DIR_FLAG_NO_FILE_DIRS));
+ strpathinzip = itemlist[0]->GetPath();
+
+ EXPECT_EQ(0, XFILE::CFile::Stat(strpathinzip, &buffer));
+ EXPECT_TRUE(buffer.st_mode | _S_IFREG);
+}
+
+/* Test case to test for graceful handling of corrupted input.
+ * NOTE: The test case is considered a "success" as long as the corrupted
+ * file was successfully generated and the test case runs without a segfault.
+ */
+TEST_F(TestZipFile, CorruptedFile)
+{
+ XFILE::CFile *file;
+ char buf[16] = {};
+ std::string reffilepath, strpathinzip, str;
+ CFileItemList itemlist;
+ ssize_t size, i;
+ int64_t count = 0;
+
+ reffilepath = XBMC_REF_FILE_PATH("xbmc/filesystem/test/reffile.txt.zip");
+ ASSERT_TRUE((file = XBMC_CREATECORRUPTEDFILE(reffilepath, ".zip")) != NULL);
+ std::cout << "Reference file generated at '" << XBMC_TEMPFILEPATH(file) << "'" << std::endl;
+
+ CURL zipUrl = URIUtils::CreateArchivePath("zip", CURL(reffilepath), "");
+ if (!XFILE::CDirectory::GetDirectory(zipUrl, itemlist, "",
+ XFILE::DIR_FLAG_NO_FILE_DIRS))
+ {
+ XBMC_DELETETEMPFILE(file);
+ SUCCEED();
+ return;
+ }
+ if (itemlist.IsEmpty())
+ {
+ XBMC_DELETETEMPFILE(file);
+ SUCCEED();
+ return;
+ }
+ strpathinzip = itemlist[0]->GetPath();
+
+ if (!file->Open(strpathinzip))
+ {
+ XBMC_DELETETEMPFILE(file);
+ SUCCEED();
+ return;
+ }
+ std::cout << "file->GetLength(): " <<
+ testing::PrintToString(file->GetLength()) << std::endl;
+ std::cout << "file->Seek(file->GetLength() / 2, SEEK_CUR) return value: " <<
+ testing::PrintToString(file->Seek(file->GetLength() / 2, SEEK_CUR)) << std::endl;
+ std::cout << "file->Seek(0, SEEK_END) return value: " <<
+ testing::PrintToString(file->Seek(0, SEEK_END)) << std::endl;
+ std::cout << "file->Seek(0, SEEK_SET) return value: " <<
+ testing::PrintToString(file->Seek(0, SEEK_SET)) << std::endl;
+ std::cout << "File contents:" << std::endl;
+ while ((size = file->Read(buf, sizeof(buf))) > 0)
+ {
+ str = StringUtils::Format(" {:08X}", count);
+ std::cout << str << " ";
+ count += size;
+ for (i = 0; i < size; i++)
+ {
+ str = StringUtils::Format("{:02X} ", buf[i]);
+ std::cout << str;
+ }
+ while (i++ < static_cast<ssize_t> (sizeof(buf)))
+ std::cout << " ";
+ std::cout << " [";
+ for (i = 0; i < size; i++)
+ {
+ if (buf[i] >= ' ' && buf[i] <= '~')
+ std::cout << buf[i];
+ else
+ std::cout << ".";
+ }
+ std::cout << "]" << std::endl;
+ }
+ file->Close();
+ XBMC_DELETETEMPFILE(file);
+}
+
+TEST_F(TestZipFile, ExtendedLocalHeader)
+{
+ XFILE::CFile file;
+ ssize_t readlen;
+ char zipdata[20000]; // size of zip file is 15352 Bytes
+
+ ASSERT_TRUE(file.Open(XBMC_REF_FILE_PATH("xbmc/filesystem/test/extendedlocalheader.zip")));
+ readlen = file.Read(zipdata, sizeof(zipdata));
+ EXPECT_TRUE(readlen);
+
+ XFILE::CZipFile zipfile;
+ std::string strBuffer;
+
+ int iSize = zipfile.UnpackFromMemory(strBuffer, std::string(zipdata, readlen), false);
+ EXPECT_EQ(152774, iSize); // sum of uncompressed size of all files in zip
+ EXPECT_TRUE(strBuffer.substr(0, 6) == "<Data>");
+ file.Close();
+}
diff --git a/xbmc/filesystem/test/TestZipManager.cpp b/xbmc/filesystem/test/TestZipManager.cpp
new file mode 100644
index 0000000..ca669c5
--- /dev/null
+++ b/xbmc/filesystem/test/TestZipManager.cpp
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017-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 "filesystem/ZipManager.h"
+#include "utils/RegExp.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestZipManager, PathTraversal)
+{
+ CRegExp pathTraversal;
+ pathTraversal.RegComp(PATH_TRAVERSAL);
+
+ ASSERT_TRUE(pathTraversal.RegFind("..") >= 0);
+ ASSERT_TRUE(pathTraversal.RegFind("../test.txt") >= 0);
+ ASSERT_TRUE(pathTraversal.RegFind("..\\test.txt") >= 0);
+ ASSERT_TRUE(pathTraversal.RegFind("test/../test.txt") >= 0);
+ ASSERT_TRUE(pathTraversal.RegFind("test\\../test.txt") >= 0);
+ ASSERT_TRUE(pathTraversal.RegFind("test\\..\\test.txt") >= 0);
+
+ ASSERT_FALSE(pathTraversal.RegFind("...") >= 0);
+ ASSERT_FALSE(pathTraversal.RegFind("..test.txt") >= 0);
+ ASSERT_FALSE(pathTraversal.RegFind("test.txt..") >= 0);
+ ASSERT_FALSE(pathTraversal.RegFind("test..test.txt") >= 0);
+}
diff --git a/xbmc/filesystem/test/data/httpdirectory/apache-default.html b/xbmc/filesystem/test/data/httpdirectory/apache-default.html
new file mode 100644
index 0000000..e29ff27
--- /dev/null
+++ b/xbmc/filesystem/test/data/httpdirectory/apache-default.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+ <head>
+ <title>Index of /</title>
+ </head>
+ <body>
+<h1>Index of /</h1>
+<ul><li><a href="folder1/"> folder1/</a></li>
+<li><a href="folder2/"> folder2/</a></li>
+<li><a href="./sample3:%20the%20sampling.mpg"> sample3: the sampling.mpg</a></li>
+<li><a href="sample%20&amp;%20samplability%204.mpg"> sample & samplability 4.mpg</a></li>
+<li><a href="sample5.mpg"> sample5.mpg</a></li>
+<li><a href="sample6.mpg"> sample6.mpg</a></li>
+</ul>
+</body></html> \ No newline at end of file
diff --git a/xbmc/filesystem/test/data/httpdirectory/apache-fancy.html b/xbmc/filesystem/test/data/httpdirectory/apache-fancy.html
new file mode 100644
index 0000000..a45d52a
--- /dev/null
+++ b/xbmc/filesystem/test/data/httpdirectory/apache-fancy.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+ <head>
+ <title>Index of /</title>
+ </head>
+ <body>
+<h1>Index of /</h1>
+<pre> <a href="?C=N;O=D;F=1">Name</a> <a href="?C=M;O=A;F=1">Last modified</a> <a href="?C=S;O=A;F=1">Size</a> <a href="?C=D;O=A;F=1">Description</a><hr> <a href="folder1/">folder1/</a> 2019-01-01 01:01 -
+ <a href="folder2/">folder2/</a> 2019-02-02 02:02 -
+ <a href="./sample3:%20the%20sampling.mpg">sample3: the sampling.mpg</a> 2019-03-03 03:03 123
+ <a href="sample%20&amp;%20samplability%204.mpg">sample & samplability 4.mpg</a> 2019-04-04 04:04 123K
+ <a href="sample5.mpg">sample5.mpg</a> 2019-05-05 05:05 123M
+ <a href="sample6.mpg">sample6.mpg</a> 2019-06-06 06:06 123G
+<hr></pre>
+</body></html>
diff --git a/xbmc/filesystem/test/data/httpdirectory/apache-html.html b/xbmc/filesystem/test/data/httpdirectory/apache-html.html
new file mode 100644
index 0000000..8e69ab4
--- /dev/null
+++ b/xbmc/filesystem/test/data/httpdirectory/apache-html.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+ <head>
+ <title>Index of /</title>
+ </head>
+ <body>
+<h1>Index of /</h1>
+ <table>
+ <tr><th valign="top">&nbsp;</th><th><a href="?C=N;O=D;F=2">Name</a></th><th><a href="?C=M;O=A;F=2">Last modified</a></th><th><a href="?C=S;O=A;F=2">Size</a></th><th><a href="?C=D;O=A;F=2">Description</a></th></tr>
+ <tr><th colspan="5"><hr></th></tr>
+<tr><td valign="top">&nbsp;</td><td><a href="folder1/">folder1/</a> </td><td align="right">2019-01-01 01:01 </td><td align="right"> - </td><td>&nbsp;</td></tr>
+<tr><td valign="top">&nbsp;</td><td><a href="folder2/">folder2/</a> </td><td align="right">2019-02-02 02:02 </td><td align="right"> - </td><td>&nbsp;</td></tr>
+<tr><td valign="top">&nbsp;</td><td><a href="./sample3:%20the%20sampling.mpg">sample3: the sampling.mpg</a> </td><td align="right">2019-03-03 03:03 </td><td align="right">123 </td><td>&nbsp;</td></tr>
+<tr><td valign="top">&nbsp;</td><td><a href="sample%20&amp;%20samplability%204.mpg">sample & samplability 4.mpg</a> </td><td align="right">2019-04-04 04:04 </td><td align="right">123K</td><td>&nbsp;</td></tr>
+<tr><td valign="top">&nbsp;</td><td><a href="sample5.mpg">sample5.mpg</a> </td><td align="right">2019-05-05 05:05 </td><td align="right">123M</td><td>&nbsp;</td></tr>
+<tr><td valign="top">&nbsp;</td><td><a href="sample6.mpg">sample6.mpg</a> </td><td align="right">2019-06-06 06:06 </td><td align="right">123G</td><td>&nbsp;</td></tr>
+ <tr><th colspan="5"><hr></th></tr>
+</table>
+</body></html>
diff --git a/xbmc/filesystem/test/data/httpdirectory/basic-multiline.html b/xbmc/filesystem/test/data/httpdirectory/basic-multiline.html
new file mode 100644
index 0000000..707a1f0
--- /dev/null
+++ b/xbmc/filesystem/test/data/httpdirectory/basic-multiline.html
@@ -0,0 +1,16 @@
+<html>
+ <head>
+ <title>Directory Listing</title>
+ </head>
+ <body>
+ <a href="folder1/">folder1/</a>
+ <a href="folder2/">
+ folder2/
+ </a>
+ <a href="./sample3:%20the%20sampling.mpg">sample3: the sampling.mpg</a>
+ <a href="sample%20&%20samplability%204.mpg">sample & samplability 4.mpg</a> <a href="sample5.mpg">sample5.mpg</a>
+ <a href="sample6.mpg">
+ sample6.mpg
+ </a>
+ </body>
+</html> \ No newline at end of file
diff --git a/xbmc/filesystem/test/data/httpdirectory/basic.html b/xbmc/filesystem/test/data/httpdirectory/basic.html
new file mode 100644
index 0000000..ce98a10
--- /dev/null
+++ b/xbmc/filesystem/test/data/httpdirectory/basic.html
@@ -0,0 +1,13 @@
+<html>
+ <head>
+ <title>Directory Listing</title>
+ </head>
+ <body>
+ <a href="folder1/">folder1/</a>
+ <a href="folder2/">folder2/</a>
+ <a href="./sample3:%20the%20sampling.mpg">sample3: the sampling.mpg</a>
+ <a href="sample%20&%20samplability%204.mpg">sample & samplability 4.mpg</a>
+ <a href="sample5.mpg">sample5.mpg</a>
+ <a href="sample6.mpg">sample6.mpg</a>
+ </body>
+</html> \ No newline at end of file
diff --git a/xbmc/filesystem/test/data/httpdirectory/lighttp-default.html b/xbmc/filesystem/test/data/httpdirectory/lighttp-default.html
new file mode 100644
index 0000000..505f477
--- /dev/null
+++ b/xbmc/filesystem/test/data/httpdirectory/lighttp-default.html
@@ -0,0 +1,211 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Index of /</title>
+<style type="text/css">
+a, a:active {text-decoration: none; color: blue;}
+a:visited {color: #48468F;}
+a:hover, a:focus {text-decoration: underline; color: red;}
+body {background-color: #F5F5F5;}
+h2 {margin-bottom: 12px;}
+table {margin-left: 12px;}
+th, td { font: 90% monospace; text-align: left;}
+th { font-weight: bold; padding-right: 14px; padding-bottom: 3px;}
+td {padding-right: 14px;}
+td.s, th.s {text-align: right;}
+div.list { background-color: white; border-top: 1px solid #646464; border-bottom: 1px solid #646464; padding-top: 10px; padding-bottom: 14px;}
+div.foot { font: 90% monospace; color: #787878; padding-top: 4px;}
+</style>
+</head>
+<body>
+<h2>Index of /</h2>
+<div class="list">
+<table summary="Directory Listing" cellpadding="0" cellspacing="0">
+<thead><tr><th class="n">Name</th><th class="m">Last Modified</th><th class="s">Size</th><th class="t">Type</th></tr></thead>
+<tbody>
+<tr class="d"><td class="n"><a href="folder1/">folder1</a>/</td><td class="m">2019-Jan-01 01:01:01</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr class="d"><td class="n"><a href="folder2/">folder2</a>/</td><td class="m">2019-Feb-02 02:02:02</td><td class="s">- &nbsp;</td><td class="t">Directory</td></tr>
+<tr><td class="n"><a href="sample3%3a%20the%20sampling.mpg">sample3: the sampling.mpg</a></td><td class="m">2019-Mar-03 03:03:03</td><td class="s">0.1K</td><td class="t">video/mpeg</td></tr>
+<tr><td class="n"><a href="sample%20%26%20samplability%204.mpg">sample &#x26; samplability 4.mpg</a></td><td class="m">2019-Apr-04 04:04:04</td><td class="s">123.0K</td><td class="t">video/mpeg</td></tr>
+<tr><td class="n"><a href="sample5.mpg">sample5.mpg</a></td><td class="m">2019-May-05 05:05:05</td><td class="s">123.0M</td><td class="t">video/mpeg</td></tr>
+<tr><td class="n"><a href="sample6.mpg">sample6.mpg</a></td><td class="m">2019-Jun-06 06:06:06</td><td class="s">123.0G</td><td class="t">video/mpeg</td></tr>
+</tbody>
+</table>
+</div>
+<div class="foot">lighttpd/1.4.49</div>
+
+<script type="text/javascript">
+// <!--
+
+var click_column;
+var name_column = 0;
+var date_column = 1;
+var size_column = 2;
+var type_column = 3;
+var prev_span = null;
+
+if (typeof(String.prototype.localeCompare) === 'undefined') {
+ String.prototype.localeCompare = function(str, locale, options) {
+ return ((this == str) ? 0 : ((this > str) ? 1 : -1));
+ };
+}
+
+if (typeof(String.prototype.toLocaleUpperCase) === 'undefined') {
+ String.prototype.toLocaleUpperCase = function() {
+ return this.toUpperCase();
+ };
+}
+
+function get_inner_text(el) {
+ if((typeof el == 'string')||(typeof el == 'undefined'))
+ return el;
+ if(el.innerText)
+ return el.innerText;
+ else {
+ var str = "";
+ var cs = el.childNodes;
+ var l = cs.length;
+ for (i=0;i<l;i++) {
+ if (cs[i].nodeType==1) str += get_inner_text(cs[i]);
+ else if (cs[i].nodeType==3) str += cs[i].nodeValue;
+ }
+ }
+ return str;
+}
+
+function isdigit(c) {
+ return (c >= '0' && c <= '9');
+}
+
+function unit_multiplier(unit) {
+ return (unit=='K') ? 1000
+ : (unit=='M') ? 1000000
+ : (unit=='G') ? 1000000000
+ : (unit=='T') ? 1000000000000
+ : (unit=='P') ? 1000000000000000
+ : (unit=='E') ? 1000000000000000000 : 1;
+}
+
+var li_date_regex=/(\d{4})-(\w{3})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/;
+
+var li_mon = ['Jan','Feb','Mar','Apr','May','Jun',
+ 'Jul','Aug','Sep','Oct','Nov','Dec'];
+
+function li_mon_num(mon) {
+ var i; for (i = 0; i < 12 && mon != li_mon[i]; ++i); return i;
+}
+
+function li_date_cmp(s1, s2) {
+ var dp1 = li_date_regex.exec(s1)
+ var dp2 = li_date_regex.exec(s2)
+ for (var i = 1; i < 7; ++i) {
+ var cmp = (2 != i)
+ ? parseInt(dp1[i]) - parseInt(dp2[i])
+ : li_mon_num(dp1[2]) - li_mon_num(dp2[2]);
+ if (0 != cmp) return cmp;
+ }
+ return 0;
+}
+
+function sortfn_then_by_name(a,b,sort_column) {
+ if (sort_column == name_column || sort_column == type_column) {
+ var ad = (a.cells[type_column].innerHTML === 'Directory');
+ var bd = (b.cells[type_column].innerHTML === 'Directory');
+ if (ad != bd) return (ad ? -1 : 1);
+ }
+ var at = get_inner_text(a.cells[sort_column]);
+ var bt = get_inner_text(b.cells[sort_column]);
+ var cmp;
+ if (sort_column == name_column) {
+ if (at == '..') return -1;
+ if (bt == '..') return 1;
+ }
+ if (a.cells[sort_column].className == 'int') {
+ cmp = parseInt(at)-parseInt(bt);
+ } else if (sort_column == date_column) {
+ var ad = isdigit(at.substr(0,1));
+ var bd = isdigit(bt.substr(0,1));
+ if (ad != bd) return (!ad ? -1 : 1);
+ cmp = li_date_cmp(at,bt);
+ } else if (sort_column == size_column) {
+ var ai = parseInt(at, 10) * unit_multiplier(at.substr(-1,1));
+ var bi = parseInt(bt, 10) * unit_multiplier(bt.substr(-1,1));
+ if (at.substr(0,1) == '-') ai = -1;
+ if (bt.substr(0,1) == '-') bi = -1;
+ cmp = ai - bi;
+ } else {
+ cmp = at.toLocaleUpperCase().localeCompare(bt.toLocaleUpperCase());
+ if (0 != cmp) return cmp;
+ cmp = at.localeCompare(bt);
+ }
+ if (0 != cmp || sort_column == name_column) return cmp;
+ return sortfn_then_by_name(a,b,name_column);
+}
+
+function sortfn(a,b) {
+ return sortfn_then_by_name(a,b,click_column);
+}
+
+function resort(lnk) {
+ var span = lnk.childNodes[1];
+ var table = lnk.parentNode.parentNode.parentNode.parentNode;
+ var rows = new Array();
+ for (j=1;j<table.rows.length;j++)
+ rows[j-1] = table.rows[j];
+ click_column = lnk.parentNode.cellIndex;
+ rows.sort(sortfn);
+
+ if (prev_span != null) prev_span.innerHTML = '';
+ if (span.getAttribute('sortdir')=='down') {
+ span.innerHTML = '&uarr;';
+ span.setAttribute('sortdir','up');
+ rows.reverse();
+ } else {
+ span.innerHTML = '&darr;';
+ span.setAttribute('sortdir','down');
+ }
+ for (i=0;i<rows.length;i++)
+ table.tBodies[0].appendChild(rows[i]);
+ prev_span = span;
+}
+
+function init_sort(init_sort_column, ascending) {
+ var tables = document.getElementsByTagName("table");
+ for (var i = 0; i < tables.length; i++) {
+ var table = tables[i];
+ //var c = table.getAttribute("class")
+ //if (-1 != c.split(" ").indexOf("sort")) {
+ var row = table.rows[0].cells;
+ for (var j = 0; j < row.length; j++) {
+ var n = row[j];
+ if (n.childNodes.length == 1 && n.childNodes[0].nodeType == 3) {
+ var link = document.createElement("a");
+ var title = n.childNodes[0].nodeValue.replace(/:$/, "");
+ link.appendChild(document.createTextNode(title));
+ link.setAttribute("href", "#");
+ link.setAttribute("class", "sortheader");
+ link.setAttribute("onclick", "resort(this);return false;");
+ var arrow = document.createElement("span");
+ arrow.setAttribute("class", "sortarrow");
+ arrow.appendChild(document.createTextNode(":"));
+ link.appendChild(arrow)
+ n.replaceChild(link, n.firstChild);
+ }
+ }
+ var lnk = row[init_sort_column].firstChild;
+ if (ascending) {
+ var span = lnk.childNodes[1];
+ span.setAttribute('sortdir','down');
+ }
+ resort(lnk);
+ //}
+ }
+}
+
+init_sort(0, 0);
+
+// -->
+</script>
+
+</body>
+</html>
diff --git a/xbmc/filesystem/test/data/httpdirectory/nginx-default.html b/xbmc/filesystem/test/data/httpdirectory/nginx-default.html
new file mode 100644
index 0000000..77bcd75
--- /dev/null
+++ b/xbmc/filesystem/test/data/httpdirectory/nginx-default.html
@@ -0,0 +1,11 @@
+<html>
+<head><title>Index of /</title></head>
+<body>
+<h1>Index of /</h1><hr><pre><a href="folder1/">folder1/</a> 01-Jan-2019 01:01 -
+<a href="folder2/">folder2/</a> 02-Feb-2019 02:02 -
+<a href="sample3%3A%20the%20sampling.mpg">sample3: the sampling.mpg</a> 03-Mar-2019 03:03 123
+<a href="sample%20%26%20samplability%204.mpg">sample &#x26; samplability 4.mpg</a> 04-Apr-2019 04:04 125952
+<a href="sample5.mpg">sample5.mpg</a> 05-May-2019 05:05 128974848
+<a href="sample6.mpg">sample6.mpg</a> 06-Jun-2019 06:06 132070244352
+</pre><hr></body>
+</html>
diff --git a/xbmc/filesystem/test/data/httpdirectory/nginx-fancyindex.html b/xbmc/filesystem/test/data/httpdirectory/nginx-fancyindex.html
new file mode 100644
index 0000000..d9772df
--- /dev/null
+++ b/xbmc/filesystem/test/data/httpdirectory/nginx-fancyindex.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
+ <meta name="viewport" content="width=device-width"/>
+ <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=">
+ <title>Files...</title>
+</head>
+<body>
+<div class="box box-breadcrumbs">
+ <div class="box-header">
+ <div class="box-header-content">
+ <div id="breadcrumbs" class="breadcrumbs">
+ <a href="#"></a>
+ </div>
+ </div>
+ </div>
+ <div class="box-content clearfix">
+ <h1>Index of:
+/</h1>
+<table id="list"><thead><tr><th style="width:55%"><a href="?C=N&amp;O=A">File Name</a>&nbsp;<a href="?C=N&amp;O=D">&nbsp;&darr;&nbsp;</a></th><th style="width:20%"><a href="?C=S&amp;O=A">File Size</a>&nbsp;<a href="?C=S&amp;O=D">&nbsp;&darr;&nbsp;</a></th><th style="width:25%"><a href="?C=M&amp;O=A">Date</a>&nbsp;<a href="?C=M&amp;O=D">&nbsp;&darr;&nbsp;</a></th></tr></thead>
+<tbody><tr><td class="link"><a href="../">Parent directory/</a></td><td class="size">-</td><td class="date">-</td></tr>
+<tr><td class="link"><a href="folder1/" title="folder1">folder1/</a></td><td class="size">-</td><td class="date">2019-Jan-01 01:01</td></tr>
+<tr><td class="link"><a href="folder2/" title="folder2">folder2/</a></td><td class="size">-</td><td class="date">2019-Feb-02 02:02</td></tr>
+<tr><td class="link"><a href="sample3%3A%20the%20sampling.mpg" title="sample3: the sampling.mpg">sample3: the sampling.mpg</a></td><td class="size">123 B</td><td class="date">2019-Mar-03 03:03</td></tr>
+<tr><td class="link"><a href="sample%20%26%20samplability%204.mpg" title="sample &#x26; samplability 4.mpg">sample &#x26; samplability 4.mpg</a></td><td class="size">123.0 KiB</td><td class="date">2019-Apr-04 04:04</td></tr>
+<tr><td class="link"><a href="sample5.mpg" title="sample5.mpg">sample5.mpg</a></td><td class="size">123.0 MiB</td><td class="date">2019-May-05 05:05</td></tr>
+<tr><td class="link"><a href="sample6.mpg" title="sample6.mpg">sample6.mpg</a></td><td class="size">123.0 GiB</td><td class="date">2019-Jun-06 06:06</td></tr>
+</tbody></table>
+</div>
+</div>
+</body>
+</html>
diff --git a/xbmc/filesystem/test/extendedlocalheader.zip b/xbmc/filesystem/test/extendedlocalheader.zip
new file mode 100644
index 0000000..b30d92e
--- /dev/null
+++ b/xbmc/filesystem/test/extendedlocalheader.zip
Binary files differ
diff --git a/xbmc/filesystem/test/refRARnormal.rar b/xbmc/filesystem/test/refRARnormal.rar
new file mode 100644
index 0000000..58fe71d
--- /dev/null
+++ b/xbmc/filesystem/test/refRARnormal.rar
Binary files differ
diff --git a/xbmc/filesystem/test/refRARstored.rar b/xbmc/filesystem/test/refRARstored.rar
new file mode 100644
index 0000000..1500027
--- /dev/null
+++ b/xbmc/filesystem/test/refRARstored.rar
Binary files differ
diff --git a/xbmc/filesystem/test/reffile.txt b/xbmc/filesystem/test/reffile.txt
new file mode 100644
index 0000000..7a5e510
--- /dev/null
+++ b/xbmc/filesystem/test/reffile.txt
@@ -0,0 +1,25 @@
+About
+-----
+XBMC is an award-winning free and open source (GPL) software media player and
+entertainment hub for digital media. XBMC is available for multiple platforms.
+Created in 2003 by a group of like minded programmers, XBMC is a non-profit
+project run and developed by volunteers located around the world. More than 50
+software developers have contributed to XBMC, and 100-plus translators have
+worked to expand its reach, making it available in more than 30 languages.
+
+While XBMC functions very well as a standard media player application for your
+computer, it has been designed to be the perfect companion for your HTPC.
+Supporting an almost endless range of remote controls, and combined with its
+beautiful interface and powerful skinning engine, XBMC feels very natural to
+use from the couch and is the ideal solution for your home theater.
+
+Currently XBMC can be used to play almost all popular audio and video formats
+around. It was designed for network playback, so you can stream your multimedia
+from anywhere in the house or directly from the internet using practically any
+protocol available. Use your media as-is: XBMC can play CDs and DVDs directly
+from the disk or image file, almost all popular archive formats from your hard
+drive, and even files inside ZIP and RAR archives. It will even scan all of
+your media and automatically create a personalized library complete with box
+covers, descriptions, and fanart. There are playlist and slideshow functions, a
+weather forecast feature and many audio visualizations. Once installed, your
+computer will become a fully functional multimedia jukebox.
diff --git a/xbmc/filesystem/test/reffile.txt.rar b/xbmc/filesystem/test/reffile.txt.rar
new file mode 100644
index 0000000..20841b8
--- /dev/null
+++ b/xbmc/filesystem/test/reffile.txt.rar
Binary files differ
diff --git a/xbmc/filesystem/test/reffile.txt.zip b/xbmc/filesystem/test/reffile.txt.zip
new file mode 100644
index 0000000..dd0572f
--- /dev/null
+++ b/xbmc/filesystem/test/reffile.txt.zip
Binary files differ
diff --git a/xbmc/games/CMakeLists.txt b/xbmc/games/CMakeLists.txt
new file mode 100644
index 0000000..70b26f3
--- /dev/null
+++ b/xbmc/games/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(SOURCES GameServices.cpp
+ GameSettings.cpp
+ GameUtils.cpp)
+
+set(HEADERS GameServices.h
+ GameSettings.h
+ GameTypes.h
+ GameUtils.h)
+
+core_add_library(games)
diff --git a/xbmc/games/GameServices.cpp b/xbmc/games/GameServices.cpp
new file mode 100644
index 0000000..099c9a9
--- /dev/null
+++ b/xbmc/games/GameServices.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017-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 "GameServices.h"
+
+#include "controllers/Controller.h"
+#include "controllers/ControllerManager.h"
+#include "games/GameSettings.h"
+#include "games/agents/GameAgentManager.h"
+#include "profiles/ProfileManager.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGameServices::CGameServices(CControllerManager& controllerManager,
+ RETRO::CGUIGameRenderManager& renderManager,
+ PERIPHERALS::CPeripherals& peripheralManager,
+ const CProfileManager& profileManager,
+ CInputManager& inputManager)
+ : m_controllerManager(controllerManager),
+ m_gameRenderManager(renderManager),
+ m_profileManager(profileManager),
+ m_gameSettings(new CGameSettings()),
+ m_gameAgentManager(new CGameAgentManager(peripheralManager, inputManager))
+{
+}
+
+CGameServices::~CGameServices() = default;
+
+ControllerPtr CGameServices::GetController(const std::string& controllerId)
+{
+ return m_controllerManager.GetController(controllerId);
+}
+
+ControllerPtr CGameServices::GetDefaultController()
+{
+ return m_controllerManager.GetDefaultController();
+}
+
+ControllerPtr CGameServices::GetDefaultKeyboard()
+{
+ return m_controllerManager.GetDefaultKeyboard();
+}
+
+ControllerPtr CGameServices::GetDefaultMouse()
+{
+ return m_controllerManager.GetDefaultMouse();
+}
+
+ControllerVector CGameServices::GetControllers()
+{
+ return m_controllerManager.GetControllers();
+}
+
+std::string CGameServices::GetSavestatesFolder() const
+{
+ return m_profileManager.GetSavestatesFolder();
+}
diff --git a/xbmc/games/GameServices.h b/xbmc/games/GameServices.h
new file mode 100644
index 0000000..c736feb
--- /dev/null
+++ b/xbmc/games/GameServices.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017-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 "controllers/ControllerTypes.h"
+
+#include <memory>
+#include <string>
+
+class CInputManager;
+class CProfileManager;
+
+namespace PERIPHERALS
+{
+class CPeripherals;
+}
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGUIGameRenderManager;
+}
+
+namespace GAME
+{
+class CGameAgentManager;
+class CControllerManager;
+class CGameSettings;
+
+class CGameServices
+{
+public:
+ CGameServices(CControllerManager& controllerManager,
+ RETRO::CGUIGameRenderManager& renderManager,
+ PERIPHERALS::CPeripherals& peripheralManager,
+ const CProfileManager& profileManager,
+ CInputManager& inputManager);
+ ~CGameServices();
+
+ ControllerPtr GetController(const std::string& controllerId);
+ ControllerPtr GetDefaultController();
+ ControllerPtr GetDefaultKeyboard();
+ ControllerPtr GetDefaultMouse();
+ ControllerVector GetControllers();
+
+ std::string GetSavestatesFolder() const;
+
+ CGameSettings& GameSettings() { return *m_gameSettings; }
+
+ RETRO::CGUIGameRenderManager& GameRenderManager() { return m_gameRenderManager; }
+
+ CGameAgentManager& GameAgentManager() { return *m_gameAgentManager; }
+
+private:
+ // Construction parameters
+ CControllerManager& m_controllerManager;
+ RETRO::CGUIGameRenderManager& m_gameRenderManager;
+ const CProfileManager& m_profileManager;
+
+ // Game services
+ std::unique_ptr<CGameSettings> m_gameSettings;
+ std::unique_ptr<CGameAgentManager> m_gameAgentManager;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/GameSettings.cpp b/xbmc/games/GameSettings.cpp
new file mode 100644
index 0000000..acbccdc
--- /dev/null
+++ b/xbmc/games/GameSettings.cpp
@@ -0,0 +1,242 @@
+/*
+ * 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 "GameSettings.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+#include "filesystem/File.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <vector>
+
+using namespace KODI;
+using namespace GAME;
+
+namespace
+{
+const std::string SETTING_GAMES_ENABLE = "gamesgeneral.enable";
+const std::string SETTING_GAMES_SHOW_OSD_HELP = "gamesgeneral.showosdhelp";
+const std::string SETTING_GAMES_ENABLEAUTOSAVE = "gamesgeneral.enableautosave";
+const std::string SETTING_GAMES_ENABLEREWIND = "gamesgeneral.enablerewind";
+const std::string SETTING_GAMES_REWINDTIME = "gamesgeneral.rewindtime";
+const std::string SETTING_GAMES_ACHIEVEMENTS_USERNAME = "gamesachievements.username";
+const std::string SETTING_GAMES_ACHIEVEMENTS_PASSWORD = "gamesachievements.password";
+const std::string SETTING_GAMES_ACHIEVEMENTS_TOKEN = "gamesachievements.token";
+const std::string SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN = "gamesachievements.loggedin";
+
+constexpr auto LOGIN_TO_RETRO_ACHIEVEMENTS_URL_TEMPLATE =
+ "http://retroachievements.org/dorequest.php?r=login&u={}&p={}";
+constexpr auto GET_PATCH_DATA_URL_TEMPLATE =
+ "http://retroachievements.org/dorequest.php?r=patch&u={}&t={}&g=0";
+constexpr auto SUCCESS = "Success";
+constexpr auto TOKEN = "Token";
+} // namespace
+
+CGameSettings::CGameSettings()
+{
+ m_settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ m_settings->RegisterCallback(this, {SETTING_GAMES_ENABLEREWIND, SETTING_GAMES_REWINDTIME,
+ SETTING_GAMES_ACHIEVEMENTS_USERNAME,
+ SETTING_GAMES_ACHIEVEMENTS_PASSWORD,
+ SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN});
+}
+
+CGameSettings::~CGameSettings()
+{
+ m_settings->UnregisterCallback(this);
+}
+
+bool CGameSettings::GamesEnabled()
+{
+ return m_settings->GetBool(SETTING_GAMES_ENABLE);
+}
+
+bool CGameSettings::ShowOSDHelp()
+{
+ return m_settings->GetBool(SETTING_GAMES_SHOW_OSD_HELP);
+}
+
+void CGameSettings::SetShowOSDHelp(bool bShow)
+{
+ if (m_settings->GetBool(SETTING_GAMES_SHOW_OSD_HELP) != bShow)
+ {
+ m_settings->SetBool(SETTING_GAMES_SHOW_OSD_HELP, bShow);
+
+ //! @todo Asynchronous save
+ m_settings->Save();
+ }
+}
+
+void CGameSettings::ToggleGames()
+{
+ m_settings->ToggleBool(SETTING_GAMES_ENABLE);
+}
+
+bool CGameSettings::AutosaveEnabled()
+{
+ return m_settings->GetBool(SETTING_GAMES_ENABLEAUTOSAVE);
+}
+
+bool CGameSettings::RewindEnabled()
+{
+ return m_settings->GetBool(SETTING_GAMES_ENABLEREWIND);
+}
+
+unsigned int CGameSettings::MaxRewindTimeSec()
+{
+ int rewindTimeSec = m_settings->GetInt(SETTING_GAMES_REWINDTIME);
+
+ return static_cast<unsigned int>(std::max(rewindTimeSec, 0));
+}
+
+std::string CGameSettings::GetRAUsername() const
+{
+ return m_settings->GetString(SETTING_GAMES_ACHIEVEMENTS_USERNAME);
+}
+
+std::string CGameSettings::GetRAToken() const
+{
+ return m_settings->GetString(SETTING_GAMES_ACHIEVEMENTS_TOKEN);
+}
+
+void CGameSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+
+ if (settingId == SETTING_GAMES_ENABLEREWIND || settingId == SETTING_GAMES_REWINDTIME)
+ {
+ SetChanged();
+ NotifyObservers(ObservableMessageSettingsChanged);
+ }
+ else if (settingId == SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN &&
+ std::dynamic_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ const std::string username = m_settings->GetString(SETTING_GAMES_ACHIEVEMENTS_USERNAME);
+ const std::string password = m_settings->GetString(SETTING_GAMES_ACHIEVEMENTS_PASSWORD);
+ std::string token = m_settings->GetString(SETTING_GAMES_ACHIEVEMENTS_TOKEN);
+
+ token = LoginToRA(username, password, std::move(token));
+
+ m_settings->SetString(SETTING_GAMES_ACHIEVEMENTS_TOKEN, token);
+
+ if (!token.empty())
+ {
+ m_settings->SetBool(SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN, true);
+ }
+ else
+ {
+ if (settingId == SETTING_GAMES_ACHIEVEMENTS_PASSWORD)
+ m_settings->SetString(SETTING_GAMES_ACHIEVEMENTS_PASSWORD, "");
+ m_settings->SetBool(SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN, false);
+ }
+
+ m_settings->Save();
+ }
+ else if (settingId == SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN &&
+ !std::dynamic_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ m_settings->SetString(SETTING_GAMES_ACHIEVEMENTS_TOKEN, "");
+ m_settings->Save();
+ }
+ else if (settingId == SETTING_GAMES_ACHIEVEMENTS_USERNAME ||
+ settingId == SETTING_GAMES_ACHIEVEMENTS_PASSWORD)
+ {
+ m_settings->SetBool(SETTING_GAMES_ACHIEVEMENTS_LOGGED_IN, false);
+ m_settings->SetString(SETTING_GAMES_ACHIEVEMENTS_TOKEN, "");
+ m_settings->Save();
+ }
+}
+
+std::string CGameSettings::LoginToRA(const std::string& username,
+ const std::string& password,
+ std::string token) const
+{
+ if (username.empty() || password.empty())
+ return token;
+
+ XFILE::CFile request;
+ const CURL loginUrl(
+ StringUtils::Format(LOGIN_TO_RETRO_ACHIEVEMENTS_URL_TEMPLATE, username, password));
+
+ std::vector<uint8_t> response;
+ if (request.LoadFile(loginUrl, response) > 0)
+ {
+ std::string strResponse(response.begin(), response.end());
+ CVariant data(CVariant::VariantTypeObject);
+ if (CJSONVariantParser::Parse(strResponse, data))
+ {
+ if (data[SUCCESS].asBoolean())
+ {
+ token = data[TOKEN].asString();
+ if (!IsAccountVerified(username, token))
+ {
+ token.clear();
+ // "RetroAchievements", "Your account is not verified, please check your emails to complete your sign up"
+ CServiceBroker::GetEventLog()->AddWithNotification(
+ EventPtr(new CNotificationEvent(35264, 35270, EventLevel::Error)));
+ }
+ }
+ else
+ {
+ token.clear();
+
+ // "RetroAchievements", "Incorrect User/Password!"
+ CServiceBroker::GetEventLog()->AddWithNotification(
+ EventPtr(new CNotificationEvent(35264, 35265, EventLevel::Error)));
+ }
+ }
+ else
+ {
+ // "RetroAchievements", "Invalid response from server"
+ CServiceBroker::GetEventLog()->AddWithNotification(
+ EventPtr(new CNotificationEvent(35264, 35267, EventLevel::Error)));
+
+ CLog::Log(LOGERROR, "Invalid server response: {}", strResponse);
+ }
+ }
+ else
+ {
+ // "RetroAchievements", "Failed to contact server"
+ CServiceBroker::GetEventLog()->AddWithNotification(
+ EventPtr(new CNotificationEvent(35264, 35266, EventLevel::Error)));
+ }
+ return token;
+}
+
+bool CGameSettings::IsAccountVerified(const std::string& username, const std::string& token) const
+{
+ XFILE::CFile request;
+ const CURL getPatchFileUrl(StringUtils::Format(GET_PATCH_DATA_URL_TEMPLATE, username, token));
+ std::vector<uint8_t> response;
+ if (request.LoadFile(getPatchFileUrl, response) > 0)
+ {
+ std::string strResponse(response.begin(), response.end());
+ CVariant data(CVariant::VariantTypeObject);
+
+ if (CJSONVariantParser::Parse(strResponse, data))
+ {
+ return data[SUCCESS].asBoolean();
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/games/GameSettings.h b/xbmc/games/GameSettings.h
new file mode 100644
index 0000000..63ee67b
--- /dev/null
+++ b/xbmc/games/GameSettings.h
@@ -0,0 +1,55 @@
+/*
+ * 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 "settings/lib/ISettingCallback.h"
+#include "utils/Observer.h"
+
+#include <string>
+
+class CSetting;
+class CSettings;
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CGameSettings : public ISettingCallback, public Observable
+{
+public:
+ CGameSettings();
+ ~CGameSettings() override;
+
+ // General settings
+ bool GamesEnabled();
+ bool ShowOSDHelp();
+ void SetShowOSDHelp(bool bShow);
+ void ToggleGames();
+ bool AutosaveEnabled();
+ bool RewindEnabled();
+ unsigned int MaxRewindTimeSec();
+ std::string GetRAUsername() const;
+ std::string GetRAToken() const;
+
+ // Inherited from ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+private:
+ std::string LoginToRA(const std::string& username,
+ const std::string& password,
+ std::string token) const;
+ bool IsAccountVerified(const std::string& username, const std::string& token) const;
+
+ // Construction parameters
+ std::shared_ptr<CSettings> m_settings;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/GameTypes.h b/xbmc/games/GameTypes.h
new file mode 100644
index 0000000..3be3195
--- /dev/null
+++ b/xbmc/games/GameTypes.h
@@ -0,0 +1,32 @@
+/*
+ * 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 <memory>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CGameClient;
+using GameClientPtr = std::shared_ptr<CGameClient>;
+using GameClientVector = std::vector<GameClientPtr>;
+
+class CGameClientPort;
+using GameClientPortPtr = std::unique_ptr<CGameClientPort>;
+using GameClientPortVec = std::vector<GameClientPortPtr>;
+
+class CGameClientDevice;
+using GameClientDevicePtr = std::unique_ptr<CGameClientDevice>;
+using GameClientDeviceVec = std::vector<GameClientDevicePtr>;
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/GameUtils.cpp b/xbmc/games/GameUtils.cpp
new file mode 100644
index 0000000..5eba00b
--- /dev/null
+++ b/xbmc/games/GameUtils.cpp
@@ -0,0 +1,314 @@
+/*
+ * 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 "GameUtils.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/Addon.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/BinaryAddonCache.h"
+#include "addons/addoninfo/AddonType.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
+#include "filesystem/SpecialProtocol.h"
+#include "games/addons/GameClient.h"
+#include "games/dialogs/GUIDialogSelectGameClient.h"
+#include "games/dialogs/GUIDialogSelectSavestate.h"
+#include "games/tags/GameInfoTag.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+bool CGameUtils::FillInGameClient(CFileItem& item, std::string& savestatePath)
+{
+ using namespace ADDON;
+
+ if (item.GetGameInfoTag()->GetGameClient().empty())
+ {
+ // If the fileitem is an add-on, fall back to that
+ if (item.HasAddonInfo() && item.GetAddonInfo()->Type() == AddonType::GAMEDLL)
+ {
+ item.GetGameInfoTag()->SetGameClient(item.GetAddonInfo()->ID());
+ }
+ else
+ {
+ if (!CGUIDialogSelectSavestate::ShowAndGetSavestate(item.GetPath(), savestatePath))
+ return false;
+
+ if (!savestatePath.empty())
+ {
+ RETRO::CSavestateDatabase db;
+ std::unique_ptr<RETRO::ISavestate> save = RETRO::CSavestateDatabase::AllocateSavestate();
+ db.GetSavestate(savestatePath, *save);
+ item.GetGameInfoTag()->SetGameClient(save->GameClientID());
+ }
+ else
+ {
+ // No game client specified, need to ask the user
+ GameClientVector candidates;
+ GameClientVector installable;
+ bool bHasVfsGameClient;
+ GetGameClients(item, candidates, installable, bHasVfsGameClient);
+
+ if (candidates.empty() && installable.empty())
+ {
+ // if: "This game can only be played directly from a hard drive or partition. Compressed files must be extracted."
+ // else: "This game isn't compatible with any available emulators."
+ int errorTextId = bHasVfsGameClient ? 35214 : 35212;
+
+ // "Failed to play game"
+ KODI::MESSAGING::HELPERS::ShowOKDialogText(CVariant{35210}, CVariant{errorTextId});
+ }
+ else if (candidates.size() == 1 && installable.empty())
+ {
+ // Only 1 option, avoid prompting the user
+ item.GetGameInfoTag()->SetGameClient(candidates[0]->ID());
+ }
+ else
+ {
+ std::string gameClient = CGUIDialogSelectGameClient::ShowAndGetGameClient(
+ item.GetPath(), candidates, installable);
+
+ if (!gameClient.empty())
+ item.GetGameInfoTag()->SetGameClient(gameClient);
+ }
+ }
+ }
+ }
+
+ const std::string gameClient = item.GetGameInfoTag()->GetGameClient();
+ if (gameClient.empty())
+ return false;
+
+ if (Install(gameClient))
+ {
+ // If the addon is disabled we need to enable it
+ if (!Enable(gameClient))
+ {
+ CLog::Log(LOGDEBUG, "Failed to enable game client {}", gameClient);
+ item.GetGameInfoTag()->SetGameClient("");
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Failed to install game client: {}", gameClient);
+ item.GetGameInfoTag()->SetGameClient("");
+ }
+
+ return !item.GetGameInfoTag()->GetGameClient().empty();
+}
+
+void CGameUtils::GetGameClients(const CFileItem& file,
+ GameClientVector& candidates,
+ GameClientVector& installable,
+ bool& bHasVfsGameClient)
+{
+ using namespace ADDON;
+
+ bHasVfsGameClient = false;
+
+ // Try to resolve path to a local file, as not all game clients support VFS
+ CURL translatedUrl(CSpecialProtocol::TranslatePath(file.GetPath()));
+
+ // Get local candidates
+ VECADDONS localAddons;
+ CBinaryAddonCache& addonCache = CServiceBroker::GetBinaryAddonCache();
+ addonCache.GetAddons(localAddons, AddonType::GAMEDLL);
+
+ bool bVfs = false;
+ GetGameClients(localAddons, translatedUrl, candidates, bVfs);
+ bHasVfsGameClient |= bVfs;
+
+ // Get remote candidates
+ VECADDONS remoteAddons;
+ if (CServiceBroker::GetAddonMgr().GetInstallableAddons(remoteAddons, AddonType::GAMEDLL))
+ {
+ GetGameClients(remoteAddons, translatedUrl, installable, bVfs);
+ bHasVfsGameClient |= bVfs;
+ }
+
+ // Sort by name
+ //! @todo Move to presentation code
+ auto SortByName = [](const GameClientPtr& lhs, const GameClientPtr& rhs) {
+ std::string lhsName = lhs->Name();
+ std::string rhsName = rhs->Name();
+
+ StringUtils::ToLower(lhsName);
+ StringUtils::ToLower(rhsName);
+
+ return lhsName < rhsName;
+ };
+
+ std::sort(candidates.begin(), candidates.end(), SortByName);
+ std::sort(installable.begin(), installable.end(), SortByName);
+}
+
+void CGameUtils::GetGameClients(const ADDON::VECADDONS& addons,
+ const CURL& translatedUrl,
+ GameClientVector& candidates,
+ bool& bHasVfsGameClient)
+{
+ bHasVfsGameClient = false;
+
+ const std::string extension = URIUtils::GetExtension(translatedUrl.Get());
+
+ const bool bIsLocalFile =
+ (translatedUrl.GetProtocol() == "file" || translatedUrl.GetProtocol().empty());
+
+ for (auto& addon : addons)
+ {
+ GameClientPtr gameClient = std::static_pointer_cast<CGameClient>(addon);
+
+ // Filter by extension
+ if (!gameClient->IsExtensionValid(extension))
+ continue;
+
+ // Filter by VFS
+ if (!bIsLocalFile && !gameClient->SupportsVFS())
+ {
+ bHasVfsGameClient = true;
+ continue;
+ }
+
+ candidates.push_back(gameClient);
+ }
+}
+
+bool CGameUtils::HasGameExtension(const std::string& path)
+{
+ using namespace ADDON;
+
+ // Get filename from CURL so that top-level zip directories will become
+ // normal paths:
+ //
+ // zip://%2Fpath_to_zip_file.zip/ -> /path_to_zip_file.zip
+ //
+ std::string filename = CURL(path).GetFileNameWithoutPath();
+
+ // Get the file extension
+ std::string extension = URIUtils::GetExtension(filename);
+ if (extension.empty())
+ return false;
+
+ StringUtils::ToLower(extension);
+
+ // Look for a game client that supports this extension
+ VECADDONS gameClients;
+ CBinaryAddonCache& addonCache = CServiceBroker::GetBinaryAddonCache();
+ addonCache.GetInstalledAddons(gameClients, AddonType::GAMEDLL);
+ for (auto& gameClient : gameClients)
+ {
+ GameClientPtr gc(std::static_pointer_cast<CGameClient>(gameClient));
+ if (gc->IsExtensionValid(extension))
+ return true;
+ }
+
+ // Check remote add-ons
+ gameClients.clear();
+ if (CServiceBroker::GetAddonMgr().GetInstallableAddons(gameClients, AddonType::GAMEDLL))
+ {
+ for (auto& gameClient : gameClients)
+ {
+ GameClientPtr gc(std::static_pointer_cast<CGameClient>(gameClient));
+ if (gc->IsExtensionValid(extension))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+std::set<std::string> CGameUtils::GetGameExtensions()
+{
+ using namespace ADDON;
+
+ std::set<std::string> extensions;
+
+ VECADDONS gameClients;
+ CBinaryAddonCache& addonCache = CServiceBroker::GetBinaryAddonCache();
+ addonCache.GetAddons(gameClients, AddonType::GAMEDLL);
+ for (auto& gameClient : gameClients)
+ {
+ GameClientPtr gc(std::static_pointer_cast<CGameClient>(gameClient));
+ extensions.insert(gc->GetExtensions().begin(), gc->GetExtensions().end());
+ }
+
+ // Check remote add-ons
+ gameClients.clear();
+ if (CServiceBroker::GetAddonMgr().GetInstallableAddons(gameClients, AddonType::GAMEDLL))
+ {
+ for (auto& gameClient : gameClients)
+ {
+ GameClientPtr gc(std::static_pointer_cast<CGameClient>(gameClient));
+ extensions.insert(gc->GetExtensions().begin(), gc->GetExtensions().end());
+ }
+ }
+
+ return extensions;
+}
+
+bool CGameUtils::IsStandaloneGame(const ADDON::AddonPtr& addon)
+{
+ using namespace ADDON;
+
+ switch (addon->Type())
+ {
+ case AddonType::GAMEDLL:
+ {
+ return std::static_pointer_cast<GAME::CGameClient>(addon)->SupportsStandalone();
+ }
+ case AddonType::SCRIPT:
+ {
+ return addon->HasType(AddonType::GAME);
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool CGameUtils::Install(const std::string& gameClient)
+{
+ // If the addon isn't installed we need to install it
+ bool installed = CServiceBroker::GetAddonMgr().IsAddonInstalled(gameClient);
+ if (!installed)
+ {
+ ADDON::AddonPtr installedAddon;
+ installed = ADDON::CAddonInstaller::GetInstance().InstallModal(
+ gameClient, installedAddon, ADDON::InstallModalPrompt::CHOICE_NO);
+ if (!installed)
+ {
+ CLog::Log(LOGERROR, "Game utils: Failed to install {}", gameClient);
+ // "Error"
+ // "Failed to install add-on."
+ MESSAGING::HELPERS::ShowOKDialogText(CVariant{257}, CVariant{35256});
+ }
+ }
+
+ return installed;
+}
+
+bool CGameUtils::Enable(const std::string& gameClient)
+{
+ bool bSuccess = true;
+
+ if (CServiceBroker::GetAddonMgr().IsAddonDisabled(gameClient))
+ bSuccess = CServiceBroker::GetAddonMgr().EnableAddon(gameClient);
+
+ return bSuccess;
+}
diff --git a/xbmc/games/GameUtils.h b/xbmc/games/GameUtils.h
new file mode 100644
index 0000000..b49985c
--- /dev/null
+++ b/xbmc/games/GameUtils.h
@@ -0,0 +1,105 @@
+/*
+ * 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 "GameTypes.h"
+
+#include <set>
+#include <string>
+
+class CFileItem;
+class CURL;
+
+namespace ADDON
+{
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+using VECADDONS = std::vector<AddonPtr>;
+} // namespace ADDON
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \ingroup games
+ * \brief Game related utilities.
+ */
+class CGameUtils
+{
+public:
+ /*!
+ * \brief Set the game client property, prompt the user for a savestate if there are any
+ * (savestates store the information of which game client created it).
+ * If there are no savestates or the user wants a new savestate, prompt the user
+ * for a game client.
+ *
+ * \param item The item with or without a game client in its info tag
+ * \param savestatePath Output. The path to the savestate selected. Empty if new savestate was
+ * selected
+ *
+ * \return True if the item has a valid game client ID in its info tag
+ */
+ static bool FillInGameClient(CFileItem& item, std::string& savestatePath);
+
+ /*!
+ * \brief Check if the file extension is supported by an add-on in
+ * a local or remote repository
+ *
+ * \param path The path of the game file
+ *
+ * \return true if the path's extension is supported by a known game client
+ */
+ static bool HasGameExtension(const std::string& path);
+
+ /*!
+ * \brief Get all game extensions
+ */
+ static std::set<std::string> GetGameExtensions();
+
+ /*!
+ * \brief Check if game script or game add-on can be launched directly
+ *
+ * \return true if the add-on can be launched, false otherwise
+ */
+ static bool IsStandaloneGame(const ADDON::AddonPtr& addon);
+
+private:
+ static void GetGameClients(const CFileItem& file,
+ GameClientVector& candidates,
+ GameClientVector& installable,
+ bool& bHasVfsGameClient);
+ static void GetGameClients(const ADDON::VECADDONS& addons,
+ const CURL& translatedUrl,
+ GameClientVector& candidates,
+ bool& bHasVfsGameClient);
+
+ /*!
+ * \brief Install the specified game client
+ *
+ * If the game client is not installed, a model dialog is shown installing
+ * the game client. If the installation fails, an error dialog is shown.
+ *
+ * \param gameClient The game client to install
+ *
+ * \return True if the game client is installed, false otherwise
+ */
+ static bool Install(const std::string& gameClient);
+
+ /*!
+ * \brief Enable the specified game client
+ *
+ * \param gameClient the game client to enable
+ *
+ * \return True if the game client is enabled, false otherwise
+ */
+ static bool Enable(const std::string& gameClient);
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/CMakeLists.txt b/xbmc/games/addons/CMakeLists.txt
new file mode 100644
index 0000000..42ec18d
--- /dev/null
+++ b/xbmc/games/addons/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES GameClient.cpp
+ GameClientInGameSaves.cpp
+ GameClientProperties.cpp
+ GameClientSubsystem.cpp
+ GameClientTranslator.cpp)
+
+set(HEADERS GameClient.h
+ GameClientCallbacks.h
+ GameClientInGameSaves.h
+ GameClientProperties.h
+ GameClientSubsystem.h
+ GameClientTranslator.h)
+
+core_add_library(gameaddons)
diff --git a/xbmc/games/addons/GameClient.cpp b/xbmc/games/addons/GameClient.cpp
new file mode 100644
index 0000000..b3e7e01
--- /dev/null
+++ b/xbmc/games/addons/GameClient.cpp
@@ -0,0 +1,730 @@
+/*
+ * 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 "GameClient.h"
+
+#include "FileItem.h"
+#include "GameClientCallbacks.h"
+#include "GameClientInGameSaves.h"
+#include "GameClientProperties.h"
+#include "GameClientTranslator.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/AddonManager.h"
+#include "addons/BinaryAddonCache.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "games/GameServices.h"
+#include "games/addons/cheevos/GameClientCheevos.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/addons/streams/GameClientStreams.h"
+#include "games/addons/streams/IGameClientStream.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cstring>
+#include <iterator>
+#include <mutex>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+using namespace KODI::MESSAGING;
+
+#define EXTENSION_SEPARATOR "|"
+#define EXTENSION_WILDCARD "*"
+
+#define GAME_PROPERTY_EXTENSIONS "extensions"
+#define GAME_PROPERTY_SUPPORTS_VFS "supports_vfs"
+#define GAME_PROPERTY_SUPPORTS_STANDALONE "supports_standalone"
+
+// --- NormalizeExtension ------------------------------------------------------
+
+namespace
+{
+/*
+ * \brief Convert to lower case and canonicalize with a leading "."
+ */
+std::string NormalizeExtension(const std::string& strExtension)
+{
+ std::string ext = strExtension;
+
+ if (!ext.empty() && ext != EXTENSION_WILDCARD)
+ {
+ StringUtils::ToLower(ext);
+
+ if (ext[0] != '.')
+ ext.insert(0, ".");
+ }
+
+ return ext;
+}
+} // namespace
+
+// --- CGameClient -------------------------------------------------------------
+
+CGameClient::CGameClient(const ADDON::AddonInfoPtr& addonInfo)
+ : CAddonDll(addonInfo, ADDON::AddonType::GAMEDLL),
+ m_subsystems(CGameClientSubsystem::CreateSubsystems(*this, *m_ifc.game, m_critSection)),
+ m_bSupportsAllExtensions(false),
+ m_bIsPlaying(false),
+ m_serializeSize(0),
+ m_region(GAME_REGION_UNKNOWN)
+{
+ using namespace ADDON;
+
+ std::vector<std::string> extensions = StringUtils::Split(
+ Type(AddonType::GAMEDLL)->GetValue(GAME_PROPERTY_EXTENSIONS).asString(), EXTENSION_SEPARATOR);
+ std::transform(extensions.begin(), extensions.end(),
+ std::inserter(m_extensions, m_extensions.begin()), NormalizeExtension);
+
+ // Check for wildcard extension
+ if (m_extensions.find(EXTENSION_WILDCARD) != m_extensions.end())
+ {
+ m_bSupportsAllExtensions = true;
+ m_extensions.clear();
+ }
+
+ m_bSupportsVFS =
+ addonInfo->Type(AddonType::GAMEDLL)->GetValue(GAME_PROPERTY_SUPPORTS_VFS).asBoolean();
+ m_bSupportsStandalone =
+ addonInfo->Type(AddonType::GAMEDLL)->GetValue(GAME_PROPERTY_SUPPORTS_STANDALONE).asBoolean();
+
+ std::tie(m_emulatorName, m_platforms) = ParseLibretroName(Name());
+}
+
+CGameClient::~CGameClient(void)
+{
+ CloseFile();
+ CGameClientSubsystem::DestroySubsystems(m_subsystems);
+}
+
+std::string CGameClient::LibPath() const
+{
+ // If the game client requires a proxy, load its DLL instead
+ if (m_ifc.game->props->proxy_dll_count > 0)
+ return GetDllPath(m_ifc.game->props->proxy_dll_paths[0]);
+
+ return CAddonDll::LibPath();
+}
+
+ADDON::AddonPtr CGameClient::GetRunningInstance() const
+{
+ using namespace ADDON;
+
+ CBinaryAddonCache& addonCache = CServiceBroker::GetBinaryAddonCache();
+ return addonCache.GetAddonInstance(ID(), Type());
+}
+
+bool CGameClient::SupportsPath() const
+{
+ return !m_extensions.empty() || m_bSupportsAllExtensions;
+}
+
+bool CGameClient::IsExtensionValid(const std::string& strExtension) const
+{
+ if (strExtension.empty())
+ return false;
+
+ if (SupportsAllExtensions())
+ return true;
+
+ return m_extensions.find(NormalizeExtension(strExtension)) != m_extensions.end();
+}
+
+bool CGameClient::Initialize(void)
+{
+ using namespace XFILE;
+
+ // Ensure user profile directory exists for add-on
+ if (!CDirectory::Exists(Profile()))
+ CDirectory::Create(Profile());
+
+ // Ensure directory exists for savestates
+ const CGameServices& gameServices = CServiceBroker::GetGameServices();
+ std::string savestatesDir = URIUtils::AddFileToFolder(gameServices.GetSavestatesFolder(), ID());
+ if (!CDirectory::Exists(savestatesDir))
+ CDirectory::Create(savestatesDir);
+
+ if (!AddonProperties().InitializeProperties())
+ return false;
+
+ m_ifc.game->toKodi->kodiInstance = this;
+ m_ifc.game->toKodi->CloseGame = cb_close_game;
+ m_ifc.game->toKodi->OpenStream = cb_open_stream;
+ m_ifc.game->toKodi->GetStreamBuffer = cb_get_stream_buffer;
+ m_ifc.game->toKodi->AddStreamData = cb_add_stream_data;
+ m_ifc.game->toKodi->ReleaseStreamBuffer = cb_release_stream_buffer;
+ m_ifc.game->toKodi->CloseStream = cb_close_stream;
+ m_ifc.game->toKodi->HwGetProcAddress = cb_hw_get_proc_address;
+ m_ifc.game->toKodi->InputEvent = cb_input_event;
+
+ memset(m_ifc.game->toAddon, 0, sizeof(KodiToAddonFuncTable_Game));
+
+ if (CreateInstance(&m_ifc) == ADDON_STATUS_OK)
+ {
+ Input().Initialize();
+ LogAddonProperties();
+ return true;
+ }
+
+ return false;
+}
+
+void CGameClient::Unload()
+{
+ Input().Deinitialize();
+
+ DestroyInstance(&m_ifc);
+}
+
+bool CGameClient::OpenFile(const CFileItem& file,
+ RETRO::IStreamManager& streamManager,
+ IGameInputCallback* input)
+{
+ // Check if we should open in standalone mode
+ if (file.GetPath().empty())
+ return false;
+
+ // Some cores "succeed" to load the file even if it doesn't exist
+ if (!CFileUtils::Exists(file.GetPath()))
+ {
+
+ // Failed to play game
+ // The required files can't be found.
+ HELPERS::ShowOKDialogText(CVariant{35210}, CVariant{g_localizeStrings.Get(35219)});
+ return false;
+ }
+
+ // Resolve special:// URLs
+ CURL translatedUrl(CSpecialProtocol::TranslatePath(file.GetPath()));
+
+ // Remove file:// from URLs if add-on doesn't support VFS
+ if (!m_bSupportsVFS)
+ {
+ if (translatedUrl.GetProtocol() == "file")
+ translatedUrl.SetProtocol("");
+ }
+
+ std::string path = translatedUrl.Get();
+ CLog::Log(LOGDEBUG, "GameClient: Loading {}", CURL::GetRedacted(path));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!Initialized())
+ return false;
+
+ CloseFile();
+
+ GAME_ERROR error = GAME_ERROR_FAILED;
+
+ // Loading the game might require the stream subsystem to be initialized
+ Streams().Initialize(streamManager);
+
+ try
+ {
+ LogError(error = m_ifc.game->toAddon->LoadGame(m_ifc.game, path.c_str()), "LoadGame()");
+ }
+ catch (...)
+ {
+ LogException("LoadGame()");
+ }
+
+ if (error != GAME_ERROR_NO_ERROR)
+ {
+ NotifyError(error);
+ Streams().Deinitialize();
+ return false;
+ }
+
+ if (!InitializeGameplay(file.GetPath(), streamManager, input))
+ {
+ Streams().Deinitialize();
+ return false;
+ }
+
+ return true;
+}
+
+bool CGameClient::OpenStandalone(RETRO::IStreamManager& streamManager, IGameInputCallback* input)
+{
+ CLog::Log(LOGDEBUG, "GameClient: Loading {} in standalone mode", ID());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!Initialized())
+ return false;
+
+ CloseFile();
+
+ GAME_ERROR error = GAME_ERROR_FAILED;
+
+ // Loading the game might require the stream subsystem to be initialized
+ Streams().Initialize(streamManager);
+
+ try
+ {
+ LogError(error = m_ifc.game->toAddon->LoadStandalone(m_ifc.game), "LoadStandalone()");
+ }
+ catch (...)
+ {
+ LogException("LoadStandalone()");
+ }
+
+ if (error != GAME_ERROR_NO_ERROR)
+ {
+ NotifyError(error);
+ Streams().Deinitialize();
+ return false;
+ }
+
+ if (!InitializeGameplay("", streamManager, input))
+ {
+ Streams().Deinitialize();
+ return false;
+ }
+
+ return true;
+}
+
+bool CGameClient::InitializeGameplay(const std::string& gamePath,
+ RETRO::IStreamManager& streamManager,
+ IGameInputCallback* input)
+{
+ if (LoadGameInfo())
+ {
+ Input().Start(input);
+
+ m_bIsPlaying = true;
+ m_gamePath = gamePath;
+ m_input = input;
+
+ m_inGameSaves.reset(new CGameClientInGameSaves(this, m_ifc.game));
+ m_inGameSaves->Load();
+
+ return true;
+ }
+
+ return false;
+}
+
+bool CGameClient::LoadGameInfo()
+{
+ bool bRequiresGameLoop;
+ try
+ {
+ bRequiresGameLoop = m_ifc.game->toAddon->RequiresGameLoop(m_ifc.game);
+ }
+ catch (...)
+ {
+ LogException("RequiresGameLoop()");
+ return false;
+ }
+
+ // Get information about system timings
+ // Can be called only after retro_load_game()
+ game_system_timing timingInfo = {};
+
+ bool bSuccess = false;
+ try
+ {
+ bSuccess =
+ LogError(m_ifc.game->toAddon->GetGameTiming(m_ifc.game, &timingInfo), "GetGameTiming()");
+ }
+ catch (...)
+ {
+ LogException("GetGameTiming()");
+ }
+
+ if (!bSuccess)
+ {
+ CLog::Log(LOGERROR, "GameClient: Failed to get timing info");
+ return false;
+ }
+
+ GAME_REGION region;
+ try
+ {
+ region = m_ifc.game->toAddon->GetRegion(m_ifc.game);
+ }
+ catch (...)
+ {
+ LogException("GetRegion()");
+ return false;
+ }
+
+ size_t serializeSize;
+ try
+ {
+ serializeSize = m_ifc.game->toAddon->SerializeSize(m_ifc.game);
+ }
+ catch (...)
+ {
+ LogException("SerializeSize()");
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "GAME: ---------------------------------------");
+ CLog::Log(LOGINFO, "GAME: Game loop: {}", bRequiresGameLoop ? "true" : "false");
+ CLog::Log(LOGINFO, "GAME: FPS: {:f}", timingInfo.fps);
+ CLog::Log(LOGINFO, "GAME: Sample Rate: {:f}", timingInfo.sample_rate);
+ CLog::Log(LOGINFO, "GAME: Region: {}", CGameClientTranslator::TranslateRegion(region));
+ CLog::Log(LOGINFO, "GAME: Savestate size: {}", serializeSize);
+ CLog::Log(LOGINFO, "GAME: ---------------------------------------");
+
+ m_bRequiresGameLoop = bRequiresGameLoop;
+ m_serializeSize = serializeSize;
+ m_framerate = timingInfo.fps;
+ m_samplerate = timingInfo.sample_rate;
+ m_region = region;
+
+ return true;
+}
+
+void CGameClient::NotifyError(GAME_ERROR error)
+{
+ std::string missingResource;
+
+ if (error == GAME_ERROR_RESTRICTED)
+ missingResource = GetMissingResource();
+
+ if (!missingResource.empty())
+ {
+ // Failed to play game
+ // This game requires the following add-on: %s
+ HELPERS::ShowOKDialogText(CVariant{35210}, CVariant{StringUtils::Format(
+ g_localizeStrings.Get(35211), missingResource)});
+ }
+ else
+ {
+ // Failed to play game
+ // The emulator "%s" had an internal error.
+ HELPERS::ShowOKDialogText(CVariant{35210},
+ CVariant{StringUtils::Format(g_localizeStrings.Get(35213), Name())});
+ }
+}
+
+std::string CGameClient::GetMissingResource()
+{
+ using namespace ADDON;
+
+ std::string strAddonId;
+
+ const auto& dependencies = GetDependencies();
+ for (auto it = dependencies.begin(); it != dependencies.end(); ++it)
+ {
+ const std::string& strDependencyId = it->id;
+ if (StringUtils::StartsWith(strDependencyId, "resource.games"))
+ {
+ AddonPtr addon;
+ const bool bInstalled =
+ CServiceBroker::GetAddonMgr().GetAddon(strDependencyId, addon, OnlyEnabled::CHOICE_YES);
+ if (!bInstalled)
+ {
+ strAddonId = strDependencyId;
+ break;
+ }
+ }
+ }
+
+ return strAddonId;
+}
+
+void CGameClient::Reset()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsPlaying)
+ {
+ try
+ {
+ LogError(m_ifc.game->toAddon->Reset(m_ifc.game), "Reset()");
+ }
+ catch (...)
+ {
+ LogException("Reset()");
+ }
+ }
+}
+
+void CGameClient::CloseFile()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsPlaying)
+ {
+ m_inGameSaves->Save();
+ m_inGameSaves.reset();
+
+ m_bIsPlaying = false;
+ m_gamePath.clear();
+ m_serializeSize = 0;
+ m_input = nullptr;
+
+ Input().Stop();
+
+ try
+ {
+ LogError(m_ifc.game->toAddon->UnloadGame(m_ifc.game), "UnloadGame()");
+ }
+ catch (...)
+ {
+ LogException("UnloadGame()");
+ }
+
+ Streams().Deinitialize();
+ }
+}
+
+void CGameClient::RunFrame()
+{
+ IGameInputCallback* input;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ input = m_input;
+ }
+
+ if (input)
+ input->PollInput();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsPlaying)
+ {
+ try
+ {
+ LogError(m_ifc.game->toAddon->RunFrame(m_ifc.game), "RunFrame()");
+ }
+ catch (...)
+ {
+ LogException("RunFrame()");
+ }
+ }
+}
+
+bool CGameClient::Serialize(uint8_t* data, size_t size)
+{
+ if (data == nullptr || size == 0)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bool bSuccess = false;
+ if (m_bIsPlaying)
+ {
+ try
+ {
+ bSuccess = LogError(m_ifc.game->toAddon->Serialize(m_ifc.game, data, size), "Serialize()");
+ }
+ catch (...)
+ {
+ LogException("Serialize()");
+ }
+ }
+
+ return bSuccess;
+}
+
+bool CGameClient::Deserialize(const uint8_t* data, size_t size)
+{
+ if (data == nullptr || size == 0)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bool bSuccess = false;
+ if (m_bIsPlaying)
+ {
+ try
+ {
+ bSuccess =
+ LogError(m_ifc.game->toAddon->Deserialize(m_ifc.game, data, size), "Deserialize()");
+ }
+ catch (...)
+ {
+ LogException("Deserialize()");
+ }
+ }
+
+ return bSuccess;
+}
+
+void CGameClient::LogAddonProperties(void) const
+{
+ CLog::Log(LOGINFO, "GAME: ------------------------------------");
+ CLog::Log(LOGINFO, "GAME: Loaded DLL for {}", ID());
+ CLog::Log(LOGINFO, "GAME: Client: {}", Name());
+ CLog::Log(LOGINFO, "GAME: Version: {}", Version().asString());
+ CLog::Log(LOGINFO, "GAME: Valid extensions: {}", StringUtils::Join(m_extensions, " "));
+ CLog::Log(LOGINFO, "GAME: Supports VFS: {}", m_bSupportsVFS);
+ CLog::Log(LOGINFO, "GAME: Supports standalone: {}", m_bSupportsStandalone);
+ CLog::Log(LOGINFO, "GAME: ------------------------------------");
+}
+
+bool CGameClient::LogError(GAME_ERROR error, const char* strMethod) const
+{
+ if (error != GAME_ERROR_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "GAME - {} - addon '{}' returned an error: {}", strMethod, ID(),
+ CGameClientTranslator::ToString(error));
+ return false;
+ }
+ return true;
+}
+
+void CGameClient::LogException(const char* strFunctionName) const
+{
+ CLog::Log(LOGERROR, "GAME: exception caught while trying to call '{}' on add-on {}",
+ strFunctionName, ID());
+ CLog::Log(LOGERROR, "Please contact the developer of this add-on: {}", Author());
+}
+
+void CGameClient::cb_close_game(KODI_HANDLE kodiInstance)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(ACTION_STOP)));
+}
+
+KODI_GAME_STREAM_HANDLE CGameClient::cb_open_stream(KODI_HANDLE kodiInstance,
+ const game_stream_properties* properties)
+{
+ if (properties == nullptr)
+ return nullptr;
+
+ CGameClient* gameClient = static_cast<CGameClient*>(kodiInstance);
+ if (gameClient == nullptr)
+ return nullptr;
+
+ return gameClient->Streams().OpenStream(*properties);
+}
+
+bool CGameClient::cb_get_stream_buffer(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ unsigned int width,
+ unsigned int height,
+ game_stream_buffer* buffer)
+{
+ if (buffer == nullptr)
+ return false;
+
+ IGameClientStream* gameClientStream = static_cast<IGameClientStream*>(stream);
+ if (gameClientStream == nullptr)
+ return false;
+
+ return gameClientStream->GetBuffer(width, height, *buffer);
+}
+
+void CGameClient::cb_add_stream_data(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ const game_stream_packet* packet)
+{
+ if (packet == nullptr)
+ return;
+
+ IGameClientStream* gameClientStream = static_cast<IGameClientStream*>(stream);
+ if (gameClientStream == nullptr)
+ return;
+
+ gameClientStream->AddData(*packet);
+}
+
+void CGameClient::cb_release_stream_buffer(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ game_stream_buffer* buffer)
+{
+ if (buffer == nullptr)
+ return;
+
+ IGameClientStream* gameClientStream = static_cast<IGameClientStream*>(stream);
+ if (gameClientStream == nullptr)
+ return;
+
+ gameClientStream->ReleaseBuffer(*buffer);
+}
+
+void CGameClient::cb_close_stream(KODI_HANDLE kodiInstance, KODI_GAME_STREAM_HANDLE stream)
+{
+ CGameClient* gameClient = static_cast<CGameClient*>(kodiInstance);
+ if (gameClient == nullptr)
+ return;
+
+ IGameClientStream* gameClientStream = static_cast<IGameClientStream*>(stream);
+ if (gameClientStream == nullptr)
+ return;
+
+ gameClient->Streams().CloseStream(gameClientStream);
+}
+
+game_proc_address_t CGameClient::cb_hw_get_proc_address(KODI_HANDLE kodiInstance, const char* sym)
+{
+ CGameClient* gameClient = static_cast<CGameClient*>(kodiInstance);
+ if (!gameClient)
+ return nullptr;
+
+ //! @todo
+ return nullptr;
+}
+
+bool CGameClient::cb_input_event(KODI_HANDLE kodiInstance, const game_input_event* event)
+{
+ CGameClient* gameClient = static_cast<CGameClient*>(kodiInstance);
+ if (!gameClient)
+ return false;
+
+ if (event == nullptr)
+ return false;
+
+ return gameClient->Input().ReceiveInputEvent(*event);
+}
+
+std::pair<std::string, std::string> CGameClient::ParseLibretroName(const std::string& addonName)
+{
+ std::string emulatorName;
+ std::string platforms;
+
+ // libretro has a de-facto standard for naming their cores. If the
+ // core emulates one or more platforms, then the format is:
+ //
+ // "Platforms (Emulator name)"
+ //
+ // Otherwise, the format is just the name with no platforms:
+ //
+ // "Emulator name"
+ //
+ // The has been observed for all 130 cores we package.
+ //
+ size_t beginPos = addonName.find('(');
+ size_t endPos = addonName.find(')');
+
+ if (beginPos != std::string::npos && endPos != std::string::npos && beginPos < endPos)
+ {
+ emulatorName = addonName.substr(beginPos + 1, endPos - beginPos - 1);
+ platforms = addonName.substr(0, beginPos);
+ StringUtils::TrimRight(platforms);
+ }
+ else
+ {
+ emulatorName = addonName;
+ platforms.clear();
+ }
+
+ return std::make_pair(emulatorName, platforms);
+}
diff --git a/xbmc/games/addons/GameClient.h b/xbmc/games/addons/GameClient.h
new file mode 100644
index 0000000..12a0b61
--- /dev/null
+++ b/xbmc/games/addons/GameClient.h
@@ -0,0 +1,264 @@
+/*
+ * 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 "GameClientSubsystem.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "threads/CriticalSection.h"
+
+#include <atomic>
+#include <memory>
+#include <set>
+#include <stdint.h>
+#include <string>
+#include <utility>
+
+class CFileItem;
+
+namespace KODI
+{
+namespace RETRO
+{
+class IStreamManager;
+}
+
+namespace GAME
+{
+
+class CGameClientCheevos;
+class CGameClientInGameSaves;
+class CGameClientInput;
+class CGameClientProperties;
+class IGameInputCallback;
+
+/*!
+ * \ingroup games
+ * \brief Helper class to have "C" struct created before other parts becomes his pointer.
+ */
+class CGameClientStruct
+{
+public:
+ CGameClientStruct()
+ {
+ // Create "C" interface structures, used as own parts to prevent API problems on update
+ KODI_ADDON_INSTANCE_INFO* info = new KODI_ADDON_INSTANCE_INFO();
+ info->id = "";
+ info->version = kodi::addon::GetTypeVersion(ADDON_INSTANCE_GAME);
+ info->type = ADDON_INSTANCE_GAME;
+ info->kodi = this;
+ info->parent = nullptr;
+ info->first_instance = true;
+ info->functions = new KODI_ADDON_INSTANCE_FUNC_CB();
+ m_ifc.info = info;
+ m_ifc.functions = new KODI_ADDON_INSTANCE_FUNC();
+
+ m_ifc.game = new AddonInstance_Game;
+ m_ifc.game->props = new AddonProps_Game();
+ m_ifc.game->toKodi = new AddonToKodiFuncTable_Game();
+ m_ifc.game->toAddon = new KodiToAddonFuncTable_Game();
+ }
+
+ ~CGameClientStruct()
+ {
+ delete m_ifc.functions;
+ if (m_ifc.info)
+ delete m_ifc.info->functions;
+ delete m_ifc.info;
+ if (m_ifc.game)
+ {
+ delete m_ifc.game->toAddon;
+ delete m_ifc.game->toKodi;
+ delete m_ifc.game->props;
+ delete m_ifc.game;
+ }
+ }
+
+ KODI_ADDON_INSTANCE_STRUCT m_ifc;
+};
+
+/*!
+ * \ingroup games
+ * \brief Interface between Kodi and Game add-ons.
+ *
+ * The game add-on system is extremely large. To make the code more manageable,
+ * a subsystem pattern has been put in place. This pattern takes functionality
+ * that would normally be placed in this class, and puts it in another class
+ * (a "subsystem").
+ *
+ * The architecture is relatively simple. Subsystems are placed in a flat
+ * struct and accessed by calling the subsystem name. For example,
+ * historically, OpenJoystick() was a member of this class. Now, the function
+ * is called like Input().OpenJoystick().
+ *
+ * Although this pattern adds a layer of complexity, it enforces modularity and
+ * separation of concerns by making it very clear when one subsystem becomes
+ * dependent on another. Subsystems are all given access to each other by the
+ * calling mechanism. However, calling a subsystem creates a dependency on it,
+ * and an engineering decision must be made to justify the dependency.
+ *
+ * CONTRIBUTING
+ *
+ * If you wish to contribute, a beneficial task would be to refactor anything
+ * in this class into a new subsystem:
+ *
+ * Using line count as a heuristic, the subsystem pattern has shrunk the .cpp
+ * from 1,200 lines to just over 600. Reducing this further is the challenge.
+ * You must now choose whether to accept.
+ */
+class CGameClient : public ADDON::CAddonDll, private CGameClientStruct
+{
+public:
+ explicit CGameClient(const ADDON::AddonInfoPtr& addonInfo);
+
+ ~CGameClient() override;
+
+ // Game subsystems (const)
+ const CGameClientCheevos& Cheevos() const { return *m_subsystems.Cheevos; }
+ const CGameClientInput& Input() const { return *m_subsystems.Input; }
+ const CGameClientProperties& AddonProperties() const { return *m_subsystems.AddonProperties; }
+ const CGameClientStreams& Streams() const { return *m_subsystems.Streams; }
+
+ // Game subsystems (mutable)
+ CGameClientCheevos& Cheevos() { return *m_subsystems.Cheevos; }
+ CGameClientInput& Input() { return *m_subsystems.Input; }
+ CGameClientProperties& AddonProperties() { return *m_subsystems.AddonProperties; }
+ CGameClientStreams& Streams() { return *m_subsystems.Streams; }
+
+ // Implementation of IAddon via CAddonDll
+ std::string LibPath() const override;
+ ADDON::AddonPtr GetRunningInstance() const override;
+
+ // Query properties of the game client
+ bool SupportsStandalone() const { return m_bSupportsStandalone; }
+ bool SupportsPath() const;
+ bool SupportsVFS() const { return m_bSupportsVFS; }
+ const std::set<std::string>& GetExtensions() const { return m_extensions; }
+ bool SupportsAllExtensions() const { return m_bSupportsAllExtensions; }
+ bool IsExtensionValid(const std::string& strExtension) const;
+ const std::string& GetEmulatorName() const { return m_emulatorName; }
+ const std::string& GetPlatforms() const { return m_platforms; }
+
+ // Start/stop gameplay
+ bool Initialize(void);
+ void Unload();
+ bool OpenFile(const CFileItem& file,
+ RETRO::IStreamManager& streamManager,
+ IGameInputCallback* input);
+ bool OpenStandalone(RETRO::IStreamManager& streamManager, IGameInputCallback* input);
+ void Reset();
+ void CloseFile();
+ const std::string& GetGamePath() const { return m_gamePath; }
+
+ // Playback control
+ bool RequiresGameLoop() const { return m_bRequiresGameLoop; }
+ bool IsPlaying() const { return m_bIsPlaying; }
+ size_t GetSerializeSize() const { return m_serializeSize; }
+ double GetFrameRate() const { return m_framerate; }
+ double GetSampleRate() const { return m_samplerate; }
+ void RunFrame();
+
+ // Access memory
+ size_t SerializeSize() const { return m_serializeSize; }
+ bool Serialize(uint8_t* data, size_t size);
+ bool Deserialize(const uint8_t* data, size_t size);
+
+ /*!
+ * @brief To get the interface table used between addon and kodi
+ * @todo This function becomes removed after old callback library system
+ * is removed.
+ */
+ AddonInstance_Game* GetInstanceInterface() { return m_ifc.game; }
+
+ // Helper functions
+ bool LogError(GAME_ERROR error, const char* strMethod) const;
+ void LogException(const char* strFunctionName) const;
+
+private:
+ // Private gameplay functions
+ bool InitializeGameplay(const std::string& gamePath,
+ RETRO::IStreamManager& streamManager,
+ IGameInputCallback* input);
+ bool LoadGameInfo();
+ void NotifyError(GAME_ERROR error);
+ std::string GetMissingResource();
+
+ // Helper functions
+ void LogAddonProperties(void) const;
+
+ /*!
+ * @brief Callback functions from addon to kodi
+ */
+ //@{
+ static void cb_close_game(KODI_HANDLE kodiInstance);
+ static KODI_GAME_STREAM_HANDLE cb_open_stream(KODI_HANDLE kodiInstance,
+ const game_stream_properties* properties);
+ static bool cb_get_stream_buffer(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ unsigned int width,
+ unsigned int height,
+ game_stream_buffer* buffer);
+ static void cb_add_stream_data(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ const game_stream_packet* packet);
+ static void cb_release_stream_buffer(KODI_HANDLE kodiInstance,
+ KODI_GAME_STREAM_HANDLE stream,
+ game_stream_buffer* buffer);
+ static void cb_close_stream(KODI_HANDLE kodiInstance, KODI_GAME_STREAM_HANDLE stream);
+ static game_proc_address_t cb_hw_get_proc_address(KODI_HANDLE kodiInstance, const char* sym);
+ static bool cb_input_event(KODI_HANDLE kodiInstance, const game_input_event* event);
+ //@}
+
+ /*!
+ * \brief Parse the name of a libretro game add-on into its emulator name
+ * and platforms
+ *
+ * \param addonName The name of the add-on, e.g. "Nintendo - SNES / SFC (Snes9x 2002)"
+ *
+ * \return A tuple of two strings:
+ * - first: the emulator name, e.g. "Snes9x 2002"
+ * - second: the platforms, e.g. "Nintendo - SNES / SFC"
+ *
+ * For cores that don't emulate a platform, such as 2048 with the add-on name
+ * "2048", then the emulator name will be the add-on name and platforms
+ * will be empty, e.g.:
+ * - first: "2048"
+ * - second: ""
+ */
+ static std::pair<std::string, std::string> ParseLibretroName(const std::string& addonName);
+
+ // Game subsystems
+ GameClientSubsystems m_subsystems;
+
+ // Game API xml parameters
+ bool m_bSupportsVFS;
+ bool m_bSupportsStandalone;
+ std::set<std::string> m_extensions;
+ bool m_bSupportsAllExtensions;
+ std::string m_emulatorName;
+ std::string m_platforms;
+
+ // Properties of the current playing file
+ std::atomic_bool m_bIsPlaying; // True between OpenFile() and CloseFile()
+ std::string m_gamePath;
+ bool m_bRequiresGameLoop = false;
+ size_t m_serializeSize;
+ IGameInputCallback* m_input = nullptr; // The input callback passed to OpenFile()
+ double m_framerate = 0.0; // Video frame rate (fps)
+ double m_samplerate = 0.0; // Audio sample rate (Hz)
+ GAME_REGION m_region; // Region of the loaded game
+
+ // In-game saves
+ std::unique_ptr<CGameClientInGameSaves> m_inGameSaves;
+
+ CCriticalSection m_critSection;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/GameClientCallbacks.h b/xbmc/games/addons/GameClientCallbacks.h
new file mode 100644
index 0000000..f4bbe2d
--- /dev/null
+++ b/xbmc/games/addons/GameClientCallbacks.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016-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
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief Input callbacks
+ *
+ * @todo Remove this file when Game API is updated for input polling
+ */
+class IGameInputCallback
+{
+public:
+ virtual ~IGameInputCallback() = default;
+
+ /*!
+ * \brief Return true if the input source accepts input
+ *
+ * \return True if input should be processed, false otherwise
+ */
+ virtual bool AcceptsInput() const = 0;
+
+ /*!
+ * \brief Poll the input source for input
+ */
+ virtual void PollInput() = 0;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/GameClientInGameSaves.cpp b/xbmc/games/addons/GameClientInGameSaves.cpp
new file mode 100644
index 0000000..802ebb7
--- /dev/null
+++ b/xbmc/games/addons/GameClientInGameSaves.cpp
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2016-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 "GameClientInGameSaves.h"
+
+#include "GameClient.h"
+#include "GameClientTranslator.h"
+#include "ServiceBroker.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "games/GameServices.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <assert.h>
+
+using namespace KODI;
+using namespace GAME;
+
+#define INGAME_SAVES_DIRECTORY "InGameSaves"
+#define INGAME_SAVES_EXTENSION_SAVE_RAM ".sav"
+#define INGAME_SAVES_EXTENSION_RTC ".rtc"
+
+CGameClientInGameSaves::CGameClientInGameSaves(CGameClient* addon,
+ const AddonInstance_Game* dllStruct)
+ : m_gameClient(addon), m_dllStruct(dllStruct)
+{
+ assert(m_gameClient != nullptr);
+ assert(m_dllStruct != nullptr);
+}
+
+void CGameClientInGameSaves::Load()
+{
+ Load(GAME_MEMORY_SAVE_RAM);
+ Load(GAME_MEMORY_RTC);
+}
+
+void CGameClientInGameSaves::Save()
+{
+ Save(GAME_MEMORY_SAVE_RAM);
+ Save(GAME_MEMORY_RTC);
+}
+
+std::string CGameClientInGameSaves::GetPath(GAME_MEMORY memoryType)
+{
+ const CGameServices& gameServices = CServiceBroker::GetGameServices();
+ std::string path =
+ URIUtils::AddFileToFolder(gameServices.GetSavestatesFolder(), INGAME_SAVES_DIRECTORY);
+ if (!XFILE::CDirectory::Exists(path))
+ XFILE::CDirectory::Create(path);
+
+ // Append save game filename
+ std::string gamePath = URIUtils::GetFileName(m_gameClient->GetGamePath());
+ path = URIUtils::AddFileToFolder(path, gamePath.empty() ? m_gameClient->ID() : gamePath);
+
+ // Append file extension
+ switch (memoryType)
+ {
+ case GAME_MEMORY_SAVE_RAM:
+ return path + INGAME_SAVES_EXTENSION_SAVE_RAM;
+ case GAME_MEMORY_RTC:
+ return path + INGAME_SAVES_EXTENSION_RTC;
+ default:
+ break;
+ }
+ return std::string();
+}
+
+void CGameClientInGameSaves::Load(GAME_MEMORY memoryType)
+{
+ uint8_t* gameMemory = nullptr;
+ size_t size = 0;
+
+ try
+ {
+ m_dllStruct->toAddon->GetMemory(m_dllStruct, memoryType, &gameMemory, &size);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "GAME: {}: Exception caught in GetMemory()", m_gameClient->ID());
+ }
+
+ const std::string path = GetPath(memoryType);
+ if (size > 0 && XFILE::CFile::Exists(path))
+ {
+ XFILE::CFile file;
+ if (file.Open(path))
+ {
+ ssize_t read = file.Read(gameMemory, size);
+ if (read == static_cast<ssize_t>(size))
+ {
+ CLog::Log(LOGINFO, "GAME: In-game saves ({}) loaded from {}",
+ CGameClientTranslator::ToString(memoryType), path);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "GAME: Failed to read in-game saves ({}): {}/{} bytes read",
+ CGameClientTranslator::ToString(memoryType), read, size);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "GAME: Unable to open in-game saves ({}) from file {}",
+ CGameClientTranslator::ToString(memoryType), path);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "GAME: No in-game saves ({}) to load",
+ CGameClientTranslator::ToString(memoryType));
+ }
+}
+
+void CGameClientInGameSaves::Save(GAME_MEMORY memoryType)
+{
+ uint8_t* gameMemory = nullptr;
+ size_t size = 0;
+
+ try
+ {
+ m_dllStruct->toAddon->GetMemory(m_dllStruct, memoryType, &gameMemory, &size);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "GAME: {}: Exception caught in GetMemory()", m_gameClient->ID());
+ }
+
+ if (size > 0)
+ {
+ const std::string path = GetPath(memoryType);
+ XFILE::CFile file;
+ if (file.OpenForWrite(path, true))
+ {
+ ssize_t written = 0;
+ written = file.Write(gameMemory, size);
+ file.Close();
+ if (written == static_cast<ssize_t>(size))
+ {
+ CLog::Log(LOGINFO, "GAME: In-game saves ({}) written to {}",
+ CGameClientTranslator::ToString(memoryType), path);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "GAME: Failed to write in-game saves ({}): {}/{} bytes written",
+ CGameClientTranslator::ToString(memoryType), written, size);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "GAME: Unable to open in-game saves ({}) from file {}",
+ CGameClientTranslator::ToString(memoryType), path);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "GAME: No in-game saves ({}) to save",
+ CGameClientTranslator::ToString(memoryType));
+ }
+}
diff --git a/xbmc/games/addons/GameClientInGameSaves.h b/xbmc/games/addons/GameClientInGameSaves.h
new file mode 100644
index 0000000..a75c155
--- /dev/null
+++ b/xbmc/games/addons/GameClientInGameSaves.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016-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 "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+
+#include <string>
+
+struct GameClient;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClient;
+
+/*!
+ * \brief This class implements in-game saves.
+ *
+ * \details Some games do not implement state persistence on their own, but rely on the frontend for
+ * saving their current memory state to disk. This is mostly the case for emulators for SRAM
+ * (battery backed up ram on cartridges) or memory cards.
+ *
+ * Differences to save states:
+ * - Works only for supported games (e.g. emulated games with SRAM support)
+ * - Often works emulator independent (and can be used to start a game with one emulator and
+ * continue with another)
+ * - Visible in-game (e.g. in-game save game selection menus)
+ */
+class CGameClientInGameSaves
+{
+public:
+ /*!
+ * \brief Constructor.
+ * \param addon The game client implementation.
+ * \param dllStruct The emulator or game for which the in-game saves are processed.
+ */
+ CGameClientInGameSaves(CGameClient* addon, const AddonInstance_Game* dllStruct);
+
+ /*!
+ * \brief Load in-game data.
+ */
+ void Load();
+
+ /*!
+ * \brief Save in-game data.
+ */
+ void Save();
+
+private:
+ std::string GetPath(GAME_MEMORY memoryType);
+
+ void Load(GAME_MEMORY memoryType);
+ void Save(GAME_MEMORY memoryType);
+
+ const CGameClient* const m_gameClient;
+ const AddonInstance_Game* const m_dllStruct;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/GameClientProperties.cpp b/xbmc/games/addons/GameClientProperties.cpp
new file mode 100644
index 0000000..4f7273a
--- /dev/null
+++ b/xbmc/games/addons/GameClientProperties.cpp
@@ -0,0 +1,292 @@
+/*
+ * 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 "GameClientProperties.h"
+
+#include "FileItem.h"
+#include "GameClient.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/GameResource.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <cstring>
+
+using namespace KODI;
+using namespace ADDON;
+using namespace GAME;
+using namespace XFILE;
+
+#define GAME_CLIENT_RESOURCES_DIRECTORY "resources"
+
+CGameClientProperties::CGameClientProperties(const CGameClient& parent, AddonProps_Game& props)
+ : m_parent(parent), m_properties(props)
+{
+}
+
+void CGameClientProperties::ReleaseResources(void)
+{
+ for (auto& it : m_proxyDllPaths)
+ delete[] it;
+ m_proxyDllPaths.clear();
+
+ for (auto& it : m_resourceDirectories)
+ delete[] it;
+ m_resourceDirectories.clear();
+
+ for (auto& it : m_extensions)
+ delete[] it;
+ m_extensions.clear();
+}
+
+bool CGameClientProperties::InitializeProperties(void)
+{
+ ReleaseResources();
+
+ ADDON::VECADDONS addons;
+ if (!GetProxyAddons(addons))
+ return false;
+
+ m_properties.game_client_dll_path = GetLibraryPath();
+ m_properties.proxy_dll_paths = GetProxyDllPaths(addons);
+ m_properties.proxy_dll_count = GetProxyDllCount();
+ m_properties.resource_directories = GetResourceDirectories();
+ m_properties.resource_directory_count = GetResourceDirectoryCount();
+ m_properties.profile_directory = GetProfileDirectory();
+ m_properties.supports_vfs = m_parent.SupportsVFS();
+ m_properties.extensions = GetExtensions();
+ m_properties.extension_count = GetExtensionCount();
+
+ return true;
+}
+
+const char* CGameClientProperties::GetLibraryPath(void)
+{
+ if (m_strLibraryPath.empty())
+ {
+ // Get the parent add-on's real path
+ std::string strLibPath = m_parent.CAddonDll::LibPath();
+ m_strLibraryPath = CSpecialProtocol::TranslatePath(strLibPath);
+ URIUtils::RemoveSlashAtEnd(m_strLibraryPath);
+ }
+ return m_strLibraryPath.c_str();
+}
+
+const char** CGameClientProperties::GetProxyDllPaths(const ADDON::VECADDONS& addons)
+{
+ if (m_proxyDllPaths.empty())
+ {
+ for (const auto& addon : addons)
+ AddProxyDll(std::static_pointer_cast<CGameClient>(addon));
+ }
+
+ if (!m_proxyDllPaths.empty())
+ return const_cast<const char**>(m_proxyDllPaths.data());
+
+ return nullptr;
+}
+
+unsigned int CGameClientProperties::GetProxyDllCount(void) const
+{
+ return static_cast<unsigned int>(m_proxyDllPaths.size());
+}
+
+const char** CGameClientProperties::GetResourceDirectories(void)
+{
+ if (m_resourceDirectories.empty())
+ {
+ // Add all other game resources
+ const auto& dependencies = m_parent.GetDependencies();
+ for (auto it = dependencies.begin(); it != dependencies.end(); ++it)
+ {
+ const std::string& strAddonId = it->id;
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(strAddonId, addon, AddonType::RESOURCE_GAMES,
+ OnlyEnabled::CHOICE_YES))
+ {
+ std::shared_ptr<CGameResource> resource = std::static_pointer_cast<CGameResource>(addon);
+
+ std::string resourcePath = resource->GetFullPath("");
+ URIUtils::RemoveSlashAtEnd(resourcePath);
+
+ char* resourceDir = new char[resourcePath.length() + 1];
+ std::strcpy(resourceDir, resourcePath.c_str());
+ m_resourceDirectories.push_back(resourceDir);
+ }
+ }
+
+ // Add resource directories for profile and path
+ std::string addonProfile = CSpecialProtocol::TranslatePath(m_parent.Profile());
+ std::string addonPath = m_parent.Path();
+
+ addonProfile = URIUtils::AddFileToFolder(addonProfile, GAME_CLIENT_RESOURCES_DIRECTORY);
+ addonPath = URIUtils::AddFileToFolder(addonPath, GAME_CLIENT_RESOURCES_DIRECTORY);
+
+ if (!CDirectory::Exists(addonProfile))
+ {
+ CLog::Log(LOGDEBUG, "Creating resource directory: {}", addonProfile);
+ CDirectory::Create(addonProfile);
+ }
+
+ // Only add user profile directory if non-empty
+ CFileItemList items;
+ if (CDirectory::GetDirectory(addonProfile, items, "", DIR_FLAG_DEFAULTS))
+ {
+ if (!items.IsEmpty())
+ {
+ char* addonProfileDir = new char[addonProfile.length() + 1];
+ std::strcpy(addonProfileDir, addonProfile.c_str());
+ m_resourceDirectories.push_back(addonProfileDir);
+ }
+ }
+
+ char* addonPathDir = new char[addonPath.length() + 1];
+ std::strcpy(addonPathDir, addonPath.c_str());
+ m_resourceDirectories.push_back(addonPathDir);
+ }
+
+ if (!m_resourceDirectories.empty())
+ return const_cast<const char**>(m_resourceDirectories.data());
+
+ return nullptr;
+}
+
+unsigned int CGameClientProperties::GetResourceDirectoryCount(void) const
+{
+ return static_cast<unsigned int>(m_resourceDirectories.size());
+}
+
+const char* CGameClientProperties::GetProfileDirectory(void)
+{
+ if (m_strProfileDirectory.empty())
+ {
+ m_strProfileDirectory = CSpecialProtocol::TranslatePath(m_parent.Profile());
+ URIUtils::RemoveSlashAtEnd(m_strProfileDirectory);
+ }
+
+ return m_strProfileDirectory.c_str();
+}
+
+const char** CGameClientProperties::GetExtensions(void)
+{
+ for (auto& extension : m_parent.GetExtensions())
+ {
+ char* ext = new char[extension.length() + 1];
+ std::strcpy(ext, extension.c_str());
+ m_extensions.push_back(ext);
+ }
+
+ return !m_extensions.empty() ? const_cast<const char**>(m_extensions.data()) : nullptr;
+}
+
+unsigned int CGameClientProperties::GetExtensionCount(void) const
+{
+ return static_cast<unsigned int>(m_extensions.size());
+}
+
+bool CGameClientProperties::GetProxyAddons(ADDON::VECADDONS& addons)
+{
+ ADDON::VECADDONS ret;
+ std::vector<std::string> missingDependencies; // ID or name of missing dependencies
+
+ for (const auto& dependency : m_parent.GetDependencies())
+ {
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(dependency.id, addon, OnlyEnabled::CHOICE_NO))
+ {
+ // If add-on is disabled, ask the user to enable it
+ if (CServiceBroker::GetAddonMgr().IsAddonDisabled(dependency.id))
+ {
+ // "Failed to play game"
+ // "This game depends on a disabled add-on. Would you like to enable it?"
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{35210}, CVariant{35215}))
+ {
+ if (!CServiceBroker::GetAddonMgr().EnableAddon(dependency.id))
+ {
+ CLog::Log(LOGERROR, "Failed to enable add-on {}", dependency.id);
+ missingDependencies.emplace_back(addon->Name());
+ addon.reset();
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "User chose to not enable add-on {}", dependency.id);
+ missingDependencies.emplace_back(addon->Name());
+ addon.reset();
+ }
+ }
+
+ if (addon && addon->Type() == AddonType::GAMEDLL)
+ ret.emplace_back(std::move(addon));
+ }
+ else
+ {
+ if (dependency.optional)
+ {
+ CLog::Log(LOGDEBUG, "Missing optional dependency {}", dependency.id);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Missing mandatory dependency {}", dependency.id);
+ missingDependencies.emplace_back(dependency.id);
+ }
+ }
+ }
+
+ if (!missingDependencies.empty())
+ {
+ std::string strDependencies = StringUtils::Join(missingDependencies, ", ");
+ std::string dialogText = StringUtils::Format(g_localizeStrings.Get(35223), strDependencies);
+
+ // "Failed to play game"
+ // "Add-on is incompatible due to unmet dependencies."
+ // ""
+ // "Missing: {0:s}"
+ MESSAGING::HELPERS::ShowOKDialogLines(CVariant{35210}, CVariant{24104}, CVariant{""},
+ CVariant{dialogText});
+
+ return false;
+ }
+
+ addons = std::move(ret);
+ return true;
+}
+
+void CGameClientProperties::AddProxyDll(const GameClientPtr& gameClient)
+{
+ // Get the add-on's real path
+ std::string strLibPath = gameClient->CAddon::LibPath();
+
+ // Ignore add-on if it is already added
+ if (!HasProxyDll(strLibPath))
+ {
+ char* libPath = new char[strLibPath.length() + 1];
+ std::strcpy(libPath, strLibPath.c_str());
+ m_proxyDllPaths.push_back(libPath);
+ }
+}
+
+bool CGameClientProperties::HasProxyDll(const std::string& strLibPath) const
+{
+ for (const auto& it : m_proxyDllPaths)
+ {
+ if (strLibPath == it)
+ return true;
+ }
+ return false;
+}
diff --git a/xbmc/games/addons/GameClientProperties.h b/xbmc/games/addons/GameClientProperties.h
new file mode 100644
index 0000000..5cb1c6a
--- /dev/null
+++ b/xbmc/games/addons/GameClientProperties.h
@@ -0,0 +1,93 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/GameTypes.h"
+
+#include <string>
+#include <vector>
+
+struct AddonProps_Game;
+
+namespace ADDON
+{
+class IAddon;
+using AddonPtr = std::shared_ptr<IAddon>;
+using VECADDONS = std::vector<AddonPtr>;
+} // namespace ADDON
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CGameClient;
+
+/**
+ * \ingroup games
+ * \brief C++ wrapper for properties to pass to the DLL
+ *
+ * Game client properties declared in addon-instance/Game.h.
+ */
+class CGameClientProperties
+{
+public:
+ CGameClientProperties(const CGameClient& parent, AddonProps_Game& props);
+ ~CGameClientProperties(void) { ReleaseResources(); }
+
+ bool InitializeProperties(void);
+
+private:
+ // Release mutable resources
+ void ReleaseResources(void);
+
+ // Equal to parent's real library path
+ const char* GetLibraryPath(void);
+
+ // List of proxy DLLs needed to load the game client
+ const char** GetProxyDllPaths(const ADDON::VECADDONS& addons);
+
+ // Number of proxy DLLs needed to load the game client
+ unsigned int GetProxyDllCount(void) const;
+
+ // Paths to game resources
+ const char** GetResourceDirectories(void);
+
+ // Number of resource directories
+ unsigned int GetResourceDirectoryCount(void) const;
+
+ // Equal to special://profile/addon_data/<parent's id>
+ const char* GetProfileDirectory(void);
+
+ // List of extensions from addon.xml
+ const char** GetExtensions(void);
+
+ // Number of extensions
+ unsigned int GetExtensionCount(void) const;
+
+ // Helper functions
+ bool GetProxyAddons(ADDON::VECADDONS& addons);
+ void AddProxyDll(const GameClientPtr& gameClient);
+ bool HasProxyDll(const std::string& strLibPath) const;
+
+ // Construction parameters
+ const CGameClient& m_parent;
+ AddonProps_Game& m_properties;
+
+ // Buffers to hold the strings
+ std::string m_strLibraryPath;
+ std::vector<char*> m_proxyDllPaths;
+ std::vector<char*> m_resourceDirectories;
+ std::string m_strProfileDirectory;
+ std::vector<char*> m_extensions;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/GameClientSubsystem.cpp b/xbmc/games/addons/GameClientSubsystem.cpp
new file mode 100644
index 0000000..599125f
--- /dev/null
+++ b/xbmc/games/addons/GameClientSubsystem.cpp
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017-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 "GameClientSubsystem.h"
+
+#include "GameClient.h"
+#include "GameClientProperties.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/addons/cheevos/GameClientCheevos.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/addons/streams/GameClientStreams.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientSubsystem::CGameClientSubsystem(CGameClient& gameClient,
+ AddonInstance_Game& addonStruct,
+ CCriticalSection& clientAccess)
+ : m_gameClient(gameClient), m_struct(addonStruct), m_clientAccess(clientAccess)
+{
+}
+
+CGameClientSubsystem::~CGameClientSubsystem() = default;
+
+GameClientSubsystems CGameClientSubsystem::CreateSubsystems(CGameClient& gameClient,
+ AddonInstance_Game& gameStruct,
+ CCriticalSection& clientAccess)
+{
+ GameClientSubsystems subsystems = {};
+
+ subsystems.Cheevos = std::make_unique<CGameClientCheevos>(gameClient, gameStruct);
+ subsystems.Input.reset(new CGameClientInput(gameClient, gameStruct, clientAccess));
+ subsystems.AddonProperties.reset(new CGameClientProperties(gameClient, *gameStruct.props));
+ subsystems.Streams.reset(new CGameClientStreams(gameClient));
+
+ return subsystems;
+}
+
+void CGameClientSubsystem::DestroySubsystems(GameClientSubsystems& subsystems)
+{
+ subsystems.Cheevos.reset();
+ subsystems.Input.reset();
+ subsystems.AddonProperties.reset();
+ subsystems.Streams.reset();
+}
+
+CGameClientCheevos& CGameClientSubsystem::Cheevos() const
+{
+ return m_gameClient.Cheevos();
+}
+
+CGameClientInput& CGameClientSubsystem::Input() const
+{
+ return m_gameClient.Input();
+}
+
+CGameClientProperties& CGameClientSubsystem::AddonProperties() const
+{
+ return m_gameClient.AddonProperties();
+}
+
+CGameClientStreams& CGameClientSubsystem::Streams() const
+{
+ return m_gameClient.Streams();
+}
diff --git a/xbmc/games/addons/GameClientSubsystem.h b/xbmc/games/addons/GameClientSubsystem.h
new file mode 100644
index 0000000..4711011
--- /dev/null
+++ b/xbmc/games/addons/GameClientSubsystem.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017-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 <memory>
+
+struct AddonInstance_Game;
+class CCriticalSection;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClient;
+class CGameClientCheevos;
+class CGameClientInput;
+class CGameClientProperties;
+class CGameClientStreams;
+
+struct GameClientSubsystems
+{
+ std::unique_ptr<CGameClientCheevos> Cheevos;
+ std::unique_ptr<CGameClientInput> Input;
+ std::unique_ptr<CGameClientProperties> AddonProperties;
+ std::unique_ptr<CGameClientStreams> Streams;
+};
+
+/*!
+ * \brief Base class for game client subsystems
+ */
+class CGameClientSubsystem
+{
+protected:
+ CGameClientSubsystem(CGameClient& gameClient,
+ AddonInstance_Game& addonStruct,
+ CCriticalSection& clientAccess);
+
+ virtual ~CGameClientSubsystem();
+
+public:
+ /*!
+ * \brief Create a struct with the allocated subsystems
+ *
+ * \param gameClient The owner of the subsystems
+ * \param gameStruct The game client's add-on function table
+ * \param clientAccess Mutex guarding client function access
+ *
+ * \return A fully-allocated GameClientSubsystems struct
+ */
+ static GameClientSubsystems CreateSubsystems(CGameClient& gameClient,
+ AddonInstance_Game& gameStruct,
+ CCriticalSection& clientAccess);
+
+ /*!
+ * \brief Deallocate subsystems
+ *
+ * \param subsystems The subsystems created by CreateSubsystems()
+ */
+ static void DestroySubsystems(GameClientSubsystems& subsystems);
+
+protected:
+ // Subsystems
+ CGameClientCheevos& Cheevos() const;
+ CGameClientInput& Input() const;
+ CGameClientProperties& AddonProperties() const;
+ CGameClientStreams& Streams() const;
+
+ // Construction parameters
+ CGameClient& m_gameClient;
+ AddonInstance_Game& m_struct;
+ CCriticalSection& m_clientAccess;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/GameClientTranslator.cpp b/xbmc/games/addons/GameClientTranslator.cpp
new file mode 100644
index 0000000..c48ba4a
--- /dev/null
+++ b/xbmc/games/addons/GameClientTranslator.cpp
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2016-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 "GameClientTranslator.h"
+
+using namespace KODI;
+using namespace GAME;
+
+const char* CGameClientTranslator::ToString(GAME_ERROR error)
+{
+ switch (error)
+ {
+ case GAME_ERROR_NO_ERROR:
+ return "no error";
+ case GAME_ERROR_NOT_IMPLEMENTED:
+ return "not implemented";
+ case GAME_ERROR_REJECTED:
+ return "rejected by the client";
+ case GAME_ERROR_INVALID_PARAMETERS:
+ return "invalid parameters for this method";
+ case GAME_ERROR_FAILED:
+ return "the command failed";
+ case GAME_ERROR_NOT_LOADED:
+ return "no game is loaded";
+ case GAME_ERROR_RESTRICTED:
+ return "the required resources are restricted";
+ default:
+ break;
+ }
+ return "unknown error";
+}
+
+const char* CGameClientTranslator::ToString(GAME_MEMORY memory)
+{
+ switch (memory)
+ {
+ case GAME_MEMORY_SAVE_RAM:
+ return "save ram";
+ case GAME_MEMORY_RTC:
+ return "rtc";
+ case GAME_MEMORY_SYSTEM_RAM:
+ return "system ram";
+ case GAME_MEMORY_VIDEO_RAM:
+ return "video ram";
+ case GAME_MEMORY_SNES_BSX_RAM:
+ return "snes bsx ram";
+ case GAME_MEMORY_SNES_SUFAMI_TURBO_A_RAM:
+ return "snes sufami turbo a ram";
+ case GAME_MEMORY_SNES_SUFAMI_TURBO_B_RAM:
+ return "snes sufami turbo b ram";
+ case GAME_MEMORY_SNES_GAME_BOY_RAM:
+ return "snes game boy ram";
+ case GAME_MEMORY_SNES_GAME_BOY_RTC:
+ return "snes game boy rtc";
+ default:
+ break;
+ }
+ return "unknown memory";
+}
+
+bool CGameClientTranslator::TranslateStreamType(GAME_STREAM_TYPE gameType,
+ RETRO::StreamType& retroType)
+{
+ switch (gameType)
+ {
+ case GAME_STREAM_AUDIO:
+ retroType = RETRO::StreamType::AUDIO;
+ return true;
+ case GAME_STREAM_VIDEO:
+ retroType = RETRO::StreamType::VIDEO;
+ return true;
+ case GAME_STREAM_SW_FRAMEBUFFER:
+ retroType = RETRO::StreamType::SW_BUFFER;
+ return true;
+ case GAME_STREAM_HW_FRAMEBUFFER:
+ retroType = RETRO::StreamType::HW_BUFFER;
+ return true;
+ default:
+ break;
+ }
+ return false;
+}
+
+AVPixelFormat CGameClientTranslator::TranslatePixelFormat(GAME_PIXEL_FORMAT format)
+{
+ switch (format)
+ {
+ case GAME_PIXEL_FORMAT_0RGB8888:
+ return AV_PIX_FMT_0RGB32;
+ case GAME_PIXEL_FORMAT_RGB565:
+ return AV_PIX_FMT_RGB565;
+ case GAME_PIXEL_FORMAT_0RGB1555:
+ return AV_PIX_FMT_RGB555;
+ default:
+ break;
+ }
+ return AV_PIX_FMT_NONE;
+}
+
+GAME_PIXEL_FORMAT CGameClientTranslator::TranslatePixelFormat(AVPixelFormat format)
+{
+ switch (format)
+ {
+ case AV_PIX_FMT_0RGB32:
+ return GAME_PIXEL_FORMAT_0RGB8888;
+ case AV_PIX_FMT_RGB565:
+ return GAME_PIXEL_FORMAT_RGB565;
+ case AV_PIX_FMT_RGB555:
+ return GAME_PIXEL_FORMAT_0RGB1555;
+ default:
+ break;
+ }
+ return GAME_PIXEL_FORMAT_UNKNOWN;
+}
+
+RETRO::PCMFormat CGameClientTranslator::TranslatePCMFormat(GAME_PCM_FORMAT format)
+{
+ switch (format)
+ {
+ case GAME_PCM_FORMAT_S16NE:
+ return RETRO::PCMFormat::FMT_S16NE;
+ default:
+ break;
+ }
+ return RETRO::PCMFormat::FMT_UNKNOWN;
+}
+
+RETRO::AudioChannel CGameClientTranslator::TranslateAudioChannel(GAME_AUDIO_CHANNEL channel)
+{
+ switch (channel)
+ {
+ case GAME_CH_FL:
+ return RETRO::AudioChannel::CH_FL;
+ case GAME_CH_FR:
+ return RETRO::AudioChannel::CH_FR;
+ case GAME_CH_FC:
+ return RETRO::AudioChannel::CH_FC;
+ case GAME_CH_LFE:
+ return RETRO::AudioChannel::CH_LFE;
+ case GAME_CH_BL:
+ return RETRO::AudioChannel::CH_BL;
+ case GAME_CH_BR:
+ return RETRO::AudioChannel::CH_BR;
+ case GAME_CH_FLOC:
+ return RETRO::AudioChannel::CH_FLOC;
+ case GAME_CH_FROC:
+ return RETRO::AudioChannel::CH_FROC;
+ case GAME_CH_BC:
+ return RETRO::AudioChannel::CH_BC;
+ case GAME_CH_SL:
+ return RETRO::AudioChannel::CH_SL;
+ case GAME_CH_SR:
+ return RETRO::AudioChannel::CH_SR;
+ case GAME_CH_TFL:
+ return RETRO::AudioChannel::CH_TFL;
+ case GAME_CH_TFR:
+ return RETRO::AudioChannel::CH_TFR;
+ case GAME_CH_TFC:
+ return RETRO::AudioChannel::CH_TFC;
+ case GAME_CH_TC:
+ return RETRO::AudioChannel::CH_TC;
+ case GAME_CH_TBL:
+ return RETRO::AudioChannel::CH_TBL;
+ case GAME_CH_TBR:
+ return RETRO::AudioChannel::CH_TBR;
+ case GAME_CH_TBC:
+ return RETRO::AudioChannel::CH_TBC;
+ case GAME_CH_BLOC:
+ return RETRO::AudioChannel::CH_BLOC;
+ case GAME_CH_BROC:
+ return RETRO::AudioChannel::CH_BROC;
+ default:
+ break;
+ }
+ return RETRO::AudioChannel::CH_NULL;
+}
+
+RETRO::VideoRotation CGameClientTranslator::TranslateRotation(GAME_VIDEO_ROTATION rotation)
+{
+ switch (rotation)
+ {
+ case GAME_VIDEO_ROTATION_90_CCW:
+ return RETRO::VideoRotation::ROTATION_90_CCW;
+ case GAME_VIDEO_ROTATION_180_CCW:
+ return RETRO::VideoRotation::ROTATION_180_CCW;
+ case GAME_VIDEO_ROTATION_270_CCW:
+ return RETRO::VideoRotation::ROTATION_270_CCW;
+ default:
+ break;
+ }
+ return RETRO::VideoRotation::ROTATION_0;
+}
+
+GAME_KEY_MOD CGameClientTranslator::GetModifiers(KEYBOARD::Modifier modifier)
+{
+ using namespace KEYBOARD;
+
+ unsigned int mods = GAME_KEY_MOD_NONE;
+
+ if (modifier & Modifier::MODIFIER_CTRL)
+ mods |= GAME_KEY_MOD_CTRL;
+ if (modifier & Modifier::MODIFIER_SHIFT)
+ mods |= GAME_KEY_MOD_SHIFT;
+ if (modifier & Modifier::MODIFIER_ALT)
+ mods |= GAME_KEY_MOD_ALT;
+ if (modifier & Modifier::MODIFIER_RALT)
+ mods |= GAME_KEY_MOD_ALT;
+ if (modifier & Modifier::MODIFIER_META)
+ mods |= GAME_KEY_MOD_META;
+ if (modifier & Modifier::MODIFIER_SUPER)
+ mods |= GAME_KEY_MOD_SUPER;
+ if (modifier & Modifier::MODIFIER_NUMLOCK)
+ mods |= GAME_KEY_MOD_NUMLOCK;
+ if (modifier & Modifier::MODIFIER_CAPSLOCK)
+ mods |= GAME_KEY_MOD_CAPSLOCK;
+ if (modifier & Modifier::MODIFIER_SCROLLLOCK)
+ mods |= GAME_KEY_MOD_SCROLLOCK;
+
+ return static_cast<GAME_KEY_MOD>(mods);
+}
+
+const char* CGameClientTranslator::TranslateRegion(GAME_REGION region)
+{
+ switch (region)
+ {
+ case GAME_REGION_NTSC:
+ return "NTSC";
+ case GAME_REGION_PAL:
+ return "PAL";
+ default:
+ break;
+ }
+ return "Unknown";
+}
+
+PORT_TYPE CGameClientTranslator::TranslatePortType(GAME_PORT_TYPE portType)
+{
+ switch (portType)
+ {
+ case GAME_PORT_KEYBOARD:
+ return PORT_TYPE::KEYBOARD;
+ case GAME_PORT_MOUSE:
+ return PORT_TYPE::MOUSE;
+ case GAME_PORT_CONTROLLER:
+ return PORT_TYPE::CONTROLLER;
+ default:
+ break;
+ }
+
+ return PORT_TYPE::UNKNOWN;
+}
diff --git a/xbmc/games/addons/GameClientTranslator.h b/xbmc/games/addons/GameClientTranslator.h
new file mode 100644
index 0000000..6b6b448
--- /dev/null
+++ b/xbmc/games/addons/GameClientTranslator.h
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2016-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 "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "cores/RetroPlayer/streams/RetroPlayerStreamTypes.h"
+#include "games/controllers/ControllerTypes.h"
+#include "input/keyboard/KeyboardTypes.h"
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \ingroup games
+ * \brief Translates data types from Game API to the corresponding format in Kodi.
+ *
+ * This class is stateless.
+ */
+class CGameClientTranslator
+{
+ CGameClientTranslator() = delete;
+
+public:
+ /*!
+ * \brief Translates game errors to string representation (e.g. for logging).
+ * \param error The error to translate.
+ * \return Translated error.
+ */
+ static const char* ToString(GAME_ERROR error);
+
+ /*!
+ * \brief Translates game memory types to string representation (e.g. for logging).
+ * \param memory The memory type to translate.
+ * \return Translated memory type.
+ */
+ static const char* ToString(GAME_MEMORY error);
+
+ /*!
+ * \brief Translate stream type (Game API to RetroPlayer).
+ * \param gameType The stream type to translate.
+ * \param[out] retroType The translated stream type.
+ * \return True if the Game API type was translated to a valid RetroPlayer type
+ */
+ static bool TranslateStreamType(GAME_STREAM_TYPE gameType, RETRO::StreamType& retroType);
+
+ /*!
+ * \brief Translate pixel format (Game API to RetroPlayer/FFMPEG).
+ * \param format The pixel format to translate.
+ * \return Translated pixel format.
+ */
+ static AVPixelFormat TranslatePixelFormat(GAME_PIXEL_FORMAT format);
+
+ /*!
+ * \brief Translate pixel format (RetroPlayer/FFMPEG to Game API).
+ * \param format The pixel format to translate.
+ * \return Translated pixel format.
+ */
+ static GAME_PIXEL_FORMAT TranslatePixelFormat(AVPixelFormat format);
+
+ /*!
+ * \brief Translate audio PCM format (Game API to RetroPlayer).
+ * \param format The audio PCM format to translate.
+ * \return Translated audio PCM format.
+ */
+ static RETRO::PCMFormat TranslatePCMFormat(GAME_PCM_FORMAT format);
+
+ /*!
+ * \brief Translate audio channels (Game API to RetroPlayer).
+ * \param format The audio channels to translate.
+ * \return Translated audio channels.
+ */
+ static RETRO::AudioChannel TranslateAudioChannel(GAME_AUDIO_CHANNEL channel);
+
+ /*!
+ * \brief Translate video rotation (Game API to RetroPlayer).
+ * \param rotation The video rotation to translate.
+ * \return Translated video rotation.
+ */
+ static RETRO::VideoRotation TranslateRotation(GAME_VIDEO_ROTATION rotation);
+
+ /*!
+ * \brief Translate key modifiers (Kodi to Game API).
+ * \param modifiers The key modifiers to translate (e.g. Shift, Ctrl).
+ * \return Translated key modifiers.
+ */
+ static GAME_KEY_MOD GetModifiers(KEYBOARD::Modifier modifier);
+
+ /*!
+ * \brief Translate region to string representation (e.g. for logging).
+ * \param error The region to translate (e.g. PAL, NTSC).
+ * \return Translated region.
+ */
+ static const char* TranslateRegion(GAME_REGION region);
+
+ /*!
+ * \brief Translate port type (Game API to Kodi)
+ * \param portType The port type to translate
+ * \return Translated port type
+ */
+ static PORT_TYPE TranslatePortType(GAME_PORT_TYPE portType);
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/cheevos/CMakeLists.txt b/xbmc/games/addons/cheevos/CMakeLists.txt
new file mode 100644
index 0000000..826fc04
--- /dev/null
+++ b/xbmc/games/addons/cheevos/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES GameClientCheevos.cpp)
+
+set(HEADERS GameClientCheevos.h)
+
+core_add_library(gamecheevos)
diff --git a/xbmc/games/addons/cheevos/GameClientCheevos.cpp b/xbmc/games/addons/cheevos/GameClientCheevos.cpp
new file mode 100644
index 0000000..e9d457b
--- /dev/null
+++ b/xbmc/games/addons/cheevos/GameClientCheevos.cpp
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2020-2021 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 "GameClientCheevos.h"
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/game.h"
+#include "cores/RetroPlayer/cheevos/RConsoleIDs.h"
+#include "games/addons/GameClient.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientCheevos::CGameClientCheevos(CGameClient& gameClient, AddonInstance_Game& addonStruct)
+ : m_gameClient(gameClient), m_struct(addonStruct)
+{
+}
+
+bool CGameClientCheevos::RCGenerateHashFromFile(std::string& hash,
+ RETRO::RConsoleID consoleID,
+ const std::string& filePath)
+{
+ char* _hash = nullptr;
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(
+ error = m_struct.toAddon->RCGenerateHashFromFile(
+ &m_struct, &_hash, static_cast<unsigned int>(consoleID), filePath.c_str()),
+ "RCGenerateHashFromFile()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCGetGameIDUrl()");
+ }
+
+ if (_hash)
+ {
+ hash = _hash;
+ m_struct.toAddon->FreeString(&m_struct, _hash);
+ }
+
+ return error == GAME_ERROR_NO_ERROR;
+}
+
+bool CGameClientCheevos::RCGetGameIDUrl(std::string& url, const std::string& hash)
+{
+ char* _url = nullptr;
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCGetGameIDUrl(&m_struct, &_url, hash.c_str()),
+ "RCGetGameIDUrl()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCGetGameIDUrl()");
+ }
+
+ if (_url)
+ {
+ url = _url;
+ m_struct.toAddon->FreeString(&m_struct, _url);
+ }
+
+ return error == GAME_ERROR_NO_ERROR;
+}
+
+bool CGameClientCheevos::RCGetPatchFileUrl(std::string& url,
+ const std::string& username,
+ const std::string& token,
+ unsigned int gameID)
+{
+ char* _url = nullptr;
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCGetPatchFileUrl(
+ &m_struct, &_url, username.c_str(), token.c_str(), gameID),
+ "RCGetPatchFileUrl()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCGetPatchFileUrl()");
+ }
+
+ if (_url)
+ {
+ url = _url;
+ m_struct.toAddon->FreeString(&m_struct, _url);
+ }
+
+ return error == GAME_ERROR_NO_ERROR;
+}
+
+bool CGameClientCheevos::RCPostRichPresenceUrl(std::string& url,
+ std::string& postData,
+ const std::string& username,
+ const std::string& token,
+ unsigned gameID,
+ const std::string& richPresence)
+{
+ char* _url = nullptr;
+ char* _postData = nullptr;
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCPostRichPresenceUrl(
+ &m_struct, &_url, &_postData, username.c_str(), token.c_str(), gameID,
+ richPresence.c_str()),
+ "RCPostRichPresenceUrl()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCPostRichPresenceUrl()");
+ }
+
+ if (_url)
+ {
+ url = _url;
+ m_struct.toAddon->FreeString(&m_struct, _url);
+ }
+ if (_postData)
+ {
+ postData = _postData;
+ m_struct.toAddon->FreeString(&m_struct, _postData);
+ }
+
+ return error == GAME_ERROR_NO_ERROR;
+}
+
+void CGameClientCheevos::RCEnableRichPresence(const std::string& script)
+{
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCEnableRichPresence(&m_struct, script.c_str()),
+ "RCEnableRichPresence()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCEnableRichPresence()");
+ }
+}
+
+void CGameClientCheevos::RCGetRichPresenceEvaluation(std::string& evaluation,
+ RETRO::RConsoleID consoleID)
+{
+ char* _evaluation = nullptr;
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCGetRichPresenceEvaluation(
+ &m_struct, &_evaluation, static_cast<unsigned int>(consoleID)),
+ "RCGetRichPresenceEvaluation()");
+
+ if (_evaluation)
+ {
+ evaluation = _evaluation;
+ m_struct.toAddon->FreeString(&m_struct, _evaluation);
+ }
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCGetRichPresenceEvaluation()");
+ }
+}
+
+void CGameClientCheevos::RCResetRuntime()
+{
+ GAME_ERROR error = GAME_ERROR_NO_ERROR;
+
+ try
+ {
+ m_gameClient.LogError(error = m_struct.toAddon->RCResetRuntime(&m_struct), "RCResetRuntime()");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("RCResetRuntime()");
+ }
+}
diff --git a/xbmc/games/addons/cheevos/GameClientCheevos.h b/xbmc/games/addons/cheevos/GameClientCheevos.h
new file mode 100644
index 0000000..2be8c7a
--- /dev/null
+++ b/xbmc/games/addons/cheevos/GameClientCheevos.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020-2021 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 <stddef.h> /* size_t */
+#include <string>
+
+struct AddonInstance_Game;
+
+namespace KODI
+{
+namespace RETRO
+{
+enum class RConsoleID;
+}
+
+namespace GAME
+{
+
+class CGameClient;
+
+class CGameClientCheevos
+{
+public:
+ CGameClientCheevos(CGameClient& gameClient, AddonInstance_Game& addonStruct);
+
+ bool RCGenerateHashFromFile(std::string& hash,
+ RETRO::RConsoleID consoleID,
+ const std::string& filePath);
+ bool RCGetGameIDUrl(std::string& url, const std::string& hash);
+ bool RCGetPatchFileUrl(std::string& url,
+ const std::string& username,
+ const std::string& token,
+ unsigned int gameID);
+ bool RCPostRichPresenceUrl(std::string& url,
+ std::string& postData,
+ const std::string& username,
+ const std::string& token,
+ unsigned gameID,
+ const std::string& richPresence);
+ void RCEnableRichPresence(const std::string& script);
+ void RCGetRichPresenceEvaluation(std::string& evaluation, RETRO::RConsoleID consoleID);
+ // When the game is reset, the runtime should also be reset
+ void RCResetRuntime();
+
+private:
+ CGameClient& m_gameClient;
+ AddonInstance_Game& m_struct;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/CMakeLists.txt b/xbmc/games/addons/input/CMakeLists.txt
new file mode 100644
index 0000000..69cc342
--- /dev/null
+++ b/xbmc/games/addons/input/CMakeLists.txt
@@ -0,0 +1,23 @@
+set(SOURCES GameClientController.cpp
+ GameClientDevice.cpp
+ GameClientHardware.cpp
+ GameClientInput.cpp
+ GameClientJoystick.cpp
+ GameClientKeyboard.cpp
+ GameClientMouse.cpp
+ GameClientPort.cpp
+ GameClientTopology.cpp
+)
+
+set(HEADERS GameClientController.h
+ GameClientDevice.h
+ GameClientHardware.h
+ GameClientInput.h
+ GameClientJoystick.h
+ GameClientKeyboard.h
+ GameClientMouse.h
+ GameClientPort.h
+ GameClientTopology.h
+)
+
+core_add_library(gameinput)
diff --git a/xbmc/games/addons/input/GameClientController.cpp b/xbmc/games/addons/input/GameClientController.cpp
new file mode 100644
index 0000000..435e277
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientController.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2018 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 this Program; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "GameClientController.h"
+
+#include "GameClientInput.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "games/controllers/input/PhysicalTopology.h"
+
+#include <algorithm>
+#include <vector>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientController::CGameClientController(CGameClientInput& input, ControllerPtr controller)
+ : m_input(input), m_controller(std::move(controller)), m_controllerId(m_controller->ID())
+{
+ // Generate arrays of features
+ for (const CPhysicalFeature& feature : m_controller->Features())
+ {
+ // Skip feature if not supported by the game client
+ if (!m_input.HasFeature(m_controller->ID(), feature.Name()))
+ continue;
+
+ // Add feature to array of the appropriate type
+ switch (feature.Type())
+ {
+ case FEATURE_TYPE::SCALAR:
+ {
+ switch (feature.InputType())
+ {
+ case JOYSTICK::INPUT_TYPE::DIGITAL:
+ m_digitalButtons.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case JOYSTICK::INPUT_TYPE::ANALOG:
+ m_analogButtons.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case FEATURE_TYPE::ANALOG_STICK:
+ m_analogSticks.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case FEATURE_TYPE::ACCELEROMETER:
+ m_accelerometers.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case FEATURE_TYPE::KEY:
+ m_keys.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case FEATURE_TYPE::RELPOINTER:
+ m_relPointers.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case FEATURE_TYPE::ABSPOINTER:
+ m_absPointers.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ case FEATURE_TYPE::MOTOR:
+ m_motors.emplace_back(const_cast<char*>(feature.Name().c_str()));
+ break;
+ default:
+ break;
+ }
+ }
+
+ //! @todo Sort vectors
+}
+
+game_controller_layout CGameClientController::TranslateController() const
+{
+ game_controller_layout controllerStruct{};
+
+ controllerStruct.controller_id = const_cast<char*>(m_controllerId.c_str());
+ controllerStruct.provides_input = m_controller->Layout().Topology().ProvidesInput();
+
+ if (!m_digitalButtons.empty())
+ {
+ controllerStruct.digital_buttons = const_cast<char**>(m_digitalButtons.data());
+ controllerStruct.digital_button_count = static_cast<unsigned int>(m_digitalButtons.size());
+ }
+ if (!m_analogButtons.empty())
+ {
+ controllerStruct.analog_buttons = const_cast<char**>(m_analogButtons.data());
+ controllerStruct.analog_button_count = static_cast<unsigned int>(m_analogButtons.size());
+ }
+ if (!m_analogSticks.empty())
+ {
+ controllerStruct.analog_sticks = const_cast<char**>(m_analogSticks.data());
+ controllerStruct.analog_stick_count = static_cast<unsigned int>(m_analogSticks.size());
+ }
+ if (!m_accelerometers.empty())
+ {
+ controllerStruct.accelerometers = const_cast<char**>(m_accelerometers.data());
+ controllerStruct.accelerometer_count = static_cast<unsigned int>(m_accelerometers.size());
+ }
+ if (!m_keys.empty())
+ {
+ controllerStruct.keys = const_cast<char**>(m_keys.data());
+ controllerStruct.key_count = static_cast<unsigned int>(m_keys.size());
+ }
+ if (!m_relPointers.empty())
+ {
+ controllerStruct.rel_pointers = const_cast<char**>(m_relPointers.data());
+ controllerStruct.rel_pointer_count = static_cast<unsigned int>(m_relPointers.size());
+ }
+ if (!m_absPointers.empty())
+ {
+ controllerStruct.abs_pointers = const_cast<char**>(m_absPointers.data());
+ controllerStruct.abs_pointer_count = static_cast<unsigned int>(m_absPointers.size());
+ }
+ if (!m_motors.empty())
+ {
+ controllerStruct.motors = const_cast<char**>(m_motors.data());
+ controllerStruct.motor_count = static_cast<unsigned int>(m_motors.size());
+ }
+
+ return controllerStruct;
+}
diff --git a/xbmc/games/addons/input/GameClientController.h b/xbmc/games/addons/input/GameClientController.h
new file mode 100644
index 0000000..5445868
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientController.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 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 this Program; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#pragma once
+
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/controllers/ControllerTypes.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClientInput;
+
+/*!
+ * \brief A container for the layout of a controller connected to a game
+ * client input port
+ */
+class CGameClientController
+{
+public:
+ /*!
+ * \brief Construct a controller layout
+ *
+ * \brief controller The controller add-on
+ */
+ CGameClientController(CGameClientInput& input, ControllerPtr controller);
+
+ /*!
+ * \brief Get a controller layout for the Game API
+ */
+ game_controller_layout TranslateController() const;
+
+private:
+ // Construction parameters
+ CGameClientInput& m_input;
+ const ControllerPtr m_controller;
+
+ // Buffer parameters
+ std::string m_controllerId;
+ std::vector<char*> m_digitalButtons;
+ std::vector<char*> m_analogButtons;
+ std::vector<char*> m_analogSticks;
+ std::vector<char*> m_accelerometers;
+ std::vector<char*> m_keys;
+ std::vector<char*> m_relPointers;
+ std::vector<char*> m_absPointers;
+ std::vector<char*> m_motors;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientDevice.cpp b/xbmc/games/addons/input/GameClientDevice.cpp
new file mode 100644
index 0000000..589fb66
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientDevice.cpp
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017-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 "GameClientDevice.h"
+
+#include "GameClientPort.h"
+#include "ServiceBroker.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/GameServices.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientDevice::CGameClientDevice(const game_input_device& device)
+ : m_controller(GetController(device.controller_id))
+{
+ if (m_controller && device.available_ports != nullptr)
+ {
+ // Look for matching ports. We enumerate in physical order because logical
+ // order can change per emulator.
+ for (const auto& physicalPort : m_controller->Topology().Ports())
+ {
+ for (unsigned int i = 0; i < device.port_count; i++)
+ {
+ const auto& logicalPort = device.available_ports[i];
+ if (logicalPort.port_id != nullptr && logicalPort.port_id == physicalPort.ID())
+ {
+ // Handle matching ports
+ AddPort(logicalPort, physicalPort);
+ break;
+ }
+ }
+ }
+ }
+}
+
+CGameClientDevice::CGameClientDevice(const ControllerPtr& controller) : m_controller(controller)
+{
+}
+
+CGameClientDevice::~CGameClientDevice() = default;
+
+void CGameClientDevice::AddPort(const game_input_port& logicalPort,
+ const CPhysicalPort& physicalPort)
+{
+ std::unique_ptr<CGameClientPort> port(new CGameClientPort(logicalPort, physicalPort));
+ m_ports.emplace_back(std::move(port));
+}
+
+ControllerPtr CGameClientDevice::GetController(const char* controllerId)
+{
+ ControllerPtr controller;
+
+ if (controllerId != nullptr)
+ {
+ controller = CServiceBroker::GetGameServices().GetController(controllerId);
+ if (!controller)
+ CLog::Log(LOGERROR, "Invalid controller ID: {}", controllerId);
+ }
+
+ return controller;
+}
diff --git a/xbmc/games/addons/input/GameClientDevice.h b/xbmc/games/addons/input/GameClientDevice.h
new file mode 100644
index 0000000..403d76c
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientDevice.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017-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 "games/GameTypes.h"
+#include "games/controllers/ControllerTypes.h"
+
+#include <string>
+
+struct game_input_device;
+struct game_input_port;
+
+namespace KODI
+{
+namespace GAME
+{
+class CPhysicalPort;
+
+/*!
+ * \ingroup games
+ * \brief Represents a device connected to a port
+ */
+class CGameClientDevice
+{
+public:
+ /*!
+ * \brief Construct a device
+ *
+ * \param device The device Game API struct
+ */
+ CGameClientDevice(const game_input_device& device);
+
+ /*!
+ * \brief Construct a device from a controller add-on
+ *
+ * \param controller The controller add-on
+ */
+ CGameClientDevice(const ControllerPtr& controller);
+
+ /*!
+ * \brief Destructor
+ */
+ ~CGameClientDevice();
+
+ /*!
+ * \brief The controller profile
+ */
+ const ControllerPtr& Controller() const { return m_controller; }
+
+ /*!
+ * \brief The ports on this device
+ */
+ const GameClientPortVec& Ports() const { return m_ports; }
+
+private:
+ /*!
+ * \brief Add a controller port
+ *
+ * \param logicalPort The logical port Game API struct
+ * \param physicalPort The physical port definition
+ */
+ void AddPort(const game_input_port& logicalPort, const CPhysicalPort& physicalPort);
+
+ // Helper function
+ static ControllerPtr GetController(const char* controllerId);
+
+ ControllerPtr m_controller;
+ GameClientPortVec m_ports;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientHardware.cpp b/xbmc/games/addons/input/GameClientHardware.cpp
new file mode 100644
index 0000000..bacff90
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientHardware.cpp
@@ -0,0 +1,25 @@
+/*
+ * 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 "GameClientHardware.h"
+
+#include "games/addons/GameClient.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientHardware::CGameClientHardware(CGameClient& gameClient) : m_gameClient(gameClient)
+{
+}
+
+void CGameClientHardware::OnResetButton()
+{
+ CLog::Log(LOGDEBUG, "{}: Sending hardware reset", m_gameClient.ID());
+ m_gameClient.Reset();
+}
diff --git a/xbmc/games/addons/input/GameClientHardware.h b/xbmc/games/addons/input/GameClientHardware.h
new file mode 100644
index 0000000..1ffc96b
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientHardware.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017-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 "input/hardware/IHardwareInput.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClient;
+
+/*!
+ * \ingroup games
+ * \brief Handles events for hardware such as reset buttons
+ */
+class CGameClientHardware : public HARDWARE::IHardwareInput
+{
+public:
+ /*!
+ * \brief Constructor
+ *
+ * \param gameClient The game client implementation
+ */
+ explicit CGameClientHardware(CGameClient& gameClient);
+
+ ~CGameClientHardware() override = default;
+
+ // Implementation of IHardwareInput
+ void OnResetButton() override;
+
+private:
+ // Construction parameter
+ CGameClient& m_gameClient;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientInput.cpp b/xbmc/games/addons/input/GameClientInput.cpp
new file mode 100644
index 0000000..5805cbf
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientInput.cpp
@@ -0,0 +1,697 @@
+/*
+ * Copyright (C) 2017-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 "GameClientInput.h"
+
+#include "GameClientController.h"
+#include "GameClientHardware.h"
+#include "GameClientJoystick.h"
+#include "GameClientKeyboard.h"
+#include "GameClientMouse.h"
+#include "GameClientPort.h"
+#include "GameClientTopology.h"
+#include "ServiceBroker.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/GameServices.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/GameClientCallbacks.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "games/controllers/types/ControllerHub.h"
+#include "games/controllers/types/ControllerNode.h"
+#include "games/controllers/types/ControllerTree.h"
+#include "games/ports/input/PortManager.h"
+#include "games/ports/types/PortNode.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "peripherals/EventLockHandle.h"
+#include "peripherals/Peripherals.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientInput::CGameClientInput(CGameClient& gameClient,
+ AddonInstance_Game& addonStruct,
+ CCriticalSection& clientAccess)
+ : CGameClientSubsystem(gameClient, addonStruct, clientAccess),
+ m_topology(new CGameClientTopology),
+ m_portManager(std::make_unique<CPortManager>())
+{
+}
+
+CGameClientInput::~CGameClientInput()
+{
+ Deinitialize();
+}
+
+void CGameClientInput::Initialize()
+{
+ LoadTopology();
+
+ // Send controller layouts to game client
+ SetControllerLayouts(m_topology->GetControllerTree().GetControllers());
+
+ // Reset ports to default state (first accepted controller is connected)
+ ActivateControllers(m_topology->GetControllerTree());
+
+ // Initialize the port manager
+ m_portManager->Initialize(m_gameClient.Profile());
+ m_portManager->SetControllerTree(m_topology->GetControllerTree());
+ m_portManager->LoadXML();
+}
+
+void CGameClientInput::Start(IGameInputCallback* input)
+{
+ m_inputCallback = input;
+
+ // Connect/disconnect active controllers
+ for (const CPortNode& port : GetActiveControllerTree().GetPorts())
+ {
+ if (port.IsConnected())
+ {
+ const ControllerPtr& activeController = port.GetActiveController().GetController();
+ if (activeController)
+ ConnectController(port.GetAddress(), activeController);
+ }
+ else
+ DisconnectController(port.GetAddress());
+ }
+
+ // Ensure hardware is open to receive events
+ m_hardware.reset(new CGameClientHardware(m_gameClient));
+
+ // Notify observers of the initial port configuration
+ NotifyObservers(ObservableMessageGamePortsChanged);
+}
+
+void CGameClientInput::Deinitialize()
+{
+ Stop();
+
+ m_topology->Clear();
+ m_controllerLayouts.clear();
+ m_portManager->Clear();
+}
+
+void CGameClientInput::Stop()
+{
+ m_hardware.reset();
+
+ CloseMouse();
+
+ CloseKeyboard();
+
+ PERIPHERALS::EventLockHandlePtr inputHandlingLock;
+ CloseJoysticks(inputHandlingLock);
+
+ // If a port was closed, then this blocks until all peripheral input has
+ // been handled
+ inputHandlingLock.reset();
+
+ m_inputCallback = nullptr;
+}
+
+bool CGameClientInput::HasFeature(const std::string& controllerId,
+ const std::string& featureName) const
+{
+ bool bHasFeature = false;
+
+ try
+ {
+ bHasFeature =
+ m_struct.toAddon->HasFeature(&m_struct, controllerId.c_str(), featureName.c_str());
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "GAME: {}: exception caught in HasFeature()", m_gameClient.ID());
+
+ // Fail gracefully
+ bHasFeature = true;
+ }
+
+ return bHasFeature;
+}
+
+bool CGameClientInput::AcceptsInput() const
+{
+ if (m_inputCallback != nullptr)
+ return m_inputCallback->AcceptsInput();
+
+ return false;
+}
+
+bool CGameClientInput::InputEvent(const game_input_event& event)
+{
+ bool bHandled = false;
+
+ try
+ {
+ bHandled = m_struct.toAddon->InputEvent(&m_struct, &event);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "GAME: {}: exception caught in InputEvent()", m_gameClient.ID());
+ }
+
+ return bHandled;
+}
+
+void CGameClientInput::LoadTopology()
+{
+ game_input_topology* topologyStruct = nullptr;
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ topologyStruct = m_struct.toAddon->GetTopology(&m_struct);
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("GetTopology()");
+ }
+ }
+
+ GameClientPortVec hardwarePorts;
+ int playerLimit = -1;
+
+ if (topologyStruct != nullptr)
+ {
+ //! @todo Guard against infinite loops provided by the game client
+
+ game_input_port* ports = topologyStruct->ports;
+ if (ports != nullptr)
+ {
+ for (unsigned int i = 0; i < topologyStruct->port_count; i++)
+ hardwarePorts.emplace_back(new CGameClientPort(ports[i]));
+ }
+
+ playerLimit = topologyStruct->player_limit;
+
+ try
+ {
+ m_struct.toAddon->FreeTopology(&m_struct, topologyStruct);
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("FreeTopology()");
+ }
+ }
+
+ // If no topology is available, create a default one with a single port that
+ // accepts all controllers imported by addon.xml
+ if (hardwarePorts.empty())
+ hardwarePorts.emplace_back(new CGameClientPort(GetControllers(m_gameClient)));
+
+ m_topology.reset(new CGameClientTopology(std::move(hardwarePorts), playerLimit));
+}
+
+void CGameClientInput::ActivateControllers(CControllerHub& hub)
+{
+ for (auto& port : hub.GetPorts())
+ {
+ if (port.GetCompatibleControllers().empty())
+ continue;
+
+ port.SetConnected(true);
+ port.SetActiveController(0);
+ for (auto& controller : port.GetCompatibleControllers())
+ ActivateControllers(controller.GetHub());
+ }
+}
+
+void CGameClientInput::SetControllerLayouts(const ControllerVector& controllers)
+{
+ if (controllers.empty())
+ return;
+
+ for (const auto& controller : controllers)
+ {
+ const std::string controllerId = controller->ID();
+ if (m_controllerLayouts.find(controllerId) == m_controllerLayouts.end())
+ m_controllerLayouts[controllerId].reset(new CGameClientController(*this, controller));
+ }
+
+ std::vector<game_controller_layout> controllerStructs;
+ for (const auto& it : m_controllerLayouts)
+ controllerStructs.emplace_back(it.second->TranslateController());
+
+ try
+ {
+ m_struct.toAddon->SetControllerLayouts(&m_struct, controllerStructs.data(),
+ static_cast<unsigned int>(controllerStructs.size()));
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("SetControllerLayouts()");
+ }
+}
+
+const CControllerTree& CGameClientInput::GetDefaultControllerTree() const
+{
+ return m_topology->GetControllerTree();
+}
+
+const CControllerTree& CGameClientInput::GetActiveControllerTree() const
+{
+ return m_portManager->GetControllerTree();
+}
+
+bool CGameClientInput::SupportsKeyboard() const
+{
+ const CControllerTree& controllers = GetDefaultControllerTree();
+
+ auto it =
+ std::find_if(controllers.GetPorts().begin(), controllers.GetPorts().end(),
+ [](const CPortNode& port) { return port.GetPortType() == PORT_TYPE::KEYBOARD; });
+
+ return it != controllers.GetPorts().end() && !it->GetCompatibleControllers().empty();
+}
+
+bool CGameClientInput::SupportsMouse() const
+{
+ const CControllerTree& controllers = GetDefaultControllerTree();
+
+ auto it =
+ std::find_if(controllers.GetPorts().begin(), controllers.GetPorts().end(),
+ [](const CPortNode& port) { return port.GetPortType() == PORT_TYPE::MOUSE; });
+
+ return it != controllers.GetPorts().end() && !it->GetCompatibleControllers().empty();
+}
+
+int CGameClientInput::GetPlayerLimit() const
+{
+ return m_topology->GetPlayerLimit();
+}
+
+bool CGameClientInput::ConnectController(const std::string& portAddress,
+ const ControllerPtr& controller)
+{
+ // Validate parameters
+ if (portAddress.empty() || !controller)
+ return false;
+
+ const CControllerTree& controllerTree = GetDefaultControllerTree();
+
+ // Validate controller
+ const CPortNode& port = controllerTree.GetPort(portAddress);
+ if (!port.IsControllerAccepted(portAddress, controller->ID()))
+ {
+ CLog::Log(LOGERROR, "Failed to open port: Invalid controller \"{}\" on port \"{}\"",
+ controller->ID(), portAddress);
+ return false;
+ }
+
+ const CPortNode& currentPort = GetActiveControllerTree().GetPort(portAddress);
+
+ // Close current ports if any are open
+ PERIPHERALS::EventLockHandlePtr inputHandlingLock;
+ CloseJoysticks(currentPort, inputHandlingLock);
+ inputHandlingLock.reset();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (!m_gameClient.Initialized())
+ return false;
+
+ try
+ {
+ if (!m_struct.toAddon->ConnectController(&m_struct, true, portAddress.c_str(),
+ controller->ID().c_str()))
+ {
+ return false;
+ }
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("ConnectController()");
+ return false;
+ }
+ }
+
+ // Update port state
+ m_portManager->ConnectController(portAddress, true, controller->ID());
+ SetChanged();
+
+ // Update agent input
+ if (controller->Layout().Topology().ProvidesInput())
+ OpenJoystick(portAddress, controller);
+
+ bool bSuccess = true;
+
+ // If port is a multitap, we need to activate its children
+ const CPortNode& updatedPort = GetActiveControllerTree().GetPort(portAddress);
+ const PortVec& childPorts = updatedPort.GetActiveController().GetHub().GetPorts();
+ for (const CPortNode& childPort : childPorts)
+ {
+ const ControllerPtr& childController = childPort.GetActiveController().GetController();
+ if (childController)
+ bSuccess &= ConnectController(childPort.GetAddress(), childController);
+ }
+
+ return bSuccess;
+}
+
+bool CGameClientInput::DisconnectController(const std::string& portAddress)
+{
+ PERIPHERALS::EventLockHandlePtr inputHandlingLock;
+
+ // If port is a multitap, we need to deactivate its children
+ const CPortNode& currentPort = GetActiveControllerTree().GetPort(portAddress);
+ CloseJoysticks(currentPort, inputHandlingLock);
+
+ // If a port was closed, then destroying the lock will block until all
+ // peripheral input handling is complete to avoid invalidating the port's
+ // input handler
+ inputHandlingLock.reset();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (!m_gameClient.Initialized())
+ return false;
+
+ try
+ {
+ if (!m_struct.toAddon->ConnectController(&m_struct, false, portAddress.c_str(), ""))
+ return false;
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("ConnectController()");
+ return false;
+ }
+ }
+
+ // Update port state
+ m_portManager->ConnectController(portAddress, false);
+ SetChanged();
+
+ // Update agent input
+ CloseJoystick(portAddress, inputHandlingLock);
+ inputHandlingLock.reset();
+
+ return true;
+}
+
+void CGameClientInput::SavePorts()
+{
+ // Save port state
+ m_portManager->SaveXMLAsync();
+
+ // Let the observers know that ports have changed
+ NotifyObservers(ObservableMessageGamePortsChanged);
+}
+
+void CGameClientInput::ResetPorts()
+{
+ const CControllerTree& controllerTree = GetDefaultControllerTree();
+ for (const CPortNode& port : controllerTree.GetPorts())
+ ConnectController(port.GetAddress(), port.GetActiveController().GetController());
+}
+
+bool CGameClientInput::HasAgent() const
+{
+ if (!m_joysticks.empty())
+ return true;
+
+ if (m_keyboard)
+ return true;
+
+ if (m_mouse)
+ return true;
+
+ return false;
+}
+
+bool CGameClientInput::OpenKeyboard(const ControllerPtr& controller,
+ const PERIPHERALS::PeripheralPtr& keyboard)
+{
+ using namespace JOYSTICK;
+
+ if (!controller)
+ {
+ CLog::Log(LOGERROR, "Failed to open keyboard, no controller given");
+ return false;
+ }
+
+ if (!keyboard)
+ return false;
+
+ bool bSuccess = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ bSuccess = m_struct.toAddon->EnableKeyboard(&m_struct, true, controller->ID().c_str());
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableKeyboard()");
+ }
+ }
+ }
+
+ if (bSuccess)
+ {
+ m_keyboard =
+ std::make_unique<CGameClientKeyboard>(m_gameClient, controller->ID(), keyboard.get());
+ m_keyboard->SetSource(keyboard);
+ return true;
+ }
+
+ return false;
+}
+
+bool CGameClientInput::IsKeyboardOpen() const
+{
+ return static_cast<bool>(m_keyboard);
+}
+
+void CGameClientInput::CloseKeyboard()
+{
+ if (m_keyboard)
+ {
+ m_keyboard.reset();
+
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ m_struct.toAddon->EnableKeyboard(&m_struct, false, "");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableKeyboard()");
+ }
+ }
+ }
+}
+
+bool CGameClientInput::OpenMouse(const ControllerPtr& controller,
+ const PERIPHERALS::PeripheralPtr& mouse)
+{
+ using namespace JOYSTICK;
+
+ if (!controller)
+ {
+ CLog::Log(LOGERROR, "Failed to open mouse, no controller given");
+ return false;
+ }
+
+ if (!mouse)
+ return false;
+
+ bool bSuccess = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ bSuccess = m_struct.toAddon->EnableMouse(&m_struct, true, controller->ID().c_str());
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableMouse()");
+ }
+ }
+ }
+
+ if (bSuccess)
+ {
+ m_mouse = std::make_unique<CGameClientMouse>(m_gameClient, controller->ID(), mouse.get());
+ m_mouse->SetSource(mouse);
+ return true;
+ }
+
+ return false;
+}
+
+bool CGameClientInput::IsMouseOpen() const
+{
+ return static_cast<bool>(m_mouse);
+}
+
+void CGameClientInput::CloseMouse()
+{
+ if (m_mouse)
+ {
+ m_mouse.reset();
+
+ std::unique_lock<CCriticalSection> lock(m_clientAccess);
+
+ if (m_gameClient.Initialized())
+ {
+ try
+ {
+ m_struct.toAddon->EnableMouse(&m_struct, false, "");
+ }
+ catch (...)
+ {
+ m_gameClient.LogException("EnableMouse()");
+ }
+ }
+ }
+}
+
+bool CGameClientInput::OpenJoystick(const std::string& portAddress, const ControllerPtr& controller)
+{
+ using namespace JOYSTICK;
+
+ if (!controller)
+ {
+ CLog::Log(LOGERROR, "Failed to open port \"{}\", no controller given", portAddress);
+ return false;
+ }
+
+ if (m_joysticks.find(portAddress) != m_joysticks.end())
+ {
+ CLog::Log(LOGERROR, "Failed to open port \"{}\", already open", portAddress);
+ return false;
+ }
+
+ m_joysticks[portAddress].reset(new CGameClientJoystick(m_gameClient, portAddress, controller));
+
+ return true;
+}
+
+void CGameClientInput::CloseJoysticks(PERIPHERALS::EventLockHandlePtr& inputHandlingLock)
+{
+ std::vector<std::string> portAddresses;
+ for (const auto& it : m_joysticks)
+ portAddresses.emplace_back(it.first);
+
+ for (const std::string& portAddress : portAddresses)
+ CloseJoystick(portAddress, inputHandlingLock);
+}
+
+void CGameClientInput::CloseJoysticks(const CPortNode& port,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock)
+{
+ const PortVec& childPorts = port.GetActiveController().GetHub().GetPorts();
+ for (const CPortNode& childPort : childPorts)
+ CloseJoysticks(childPort, inputHandlingLock);
+
+ CloseJoystick(port.GetAddress(), inputHandlingLock);
+}
+
+void CGameClientInput::CloseJoystick(const std::string& portAddress,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock)
+{
+ auto it = m_joysticks.find(portAddress);
+ if (it != m_joysticks.end())
+ {
+ if (!inputHandlingLock)
+ {
+ // An input handler is being destroyed. Disable input until the lock is
+ // released. Note: acquiring the lock blocks until all peripheral input
+ // has been handled.
+ inputHandlingLock = CServiceBroker::GetPeripherals().RegisterEventLock();
+ }
+
+ m_joysticks.erase(it);
+ }
+}
+
+void CGameClientInput::HardwareReset()
+{
+ if (m_hardware)
+ m_hardware->OnResetButton();
+}
+
+bool CGameClientInput::ReceiveInputEvent(const game_input_event& event)
+{
+ bool bHandled = false;
+
+ switch (event.type)
+ {
+ case GAME_INPUT_EVENT_MOTOR:
+ if (event.port_address != nullptr && event.feature_name != nullptr)
+ bHandled = SetRumble(event.port_address, event.feature_name, event.motor.magnitude);
+ break;
+ default:
+ break;
+ }
+
+ return bHandled;
+}
+
+bool CGameClientInput::SetRumble(const std::string& portAddress,
+ const std::string& feature,
+ float magnitude)
+{
+ bool bHandled = false;
+
+ auto it = m_joysticks.find(portAddress);
+ if (it != m_joysticks.end())
+ bHandled = it->second->SetRumble(feature, magnitude);
+
+ return bHandled;
+}
+
+ControllerVector CGameClientInput::GetControllers(const CGameClient& gameClient)
+{
+ using namespace ADDON;
+
+ ControllerVector controllers;
+
+ CGameServices& gameServices = CServiceBroker::GetGameServices();
+
+ const auto& dependencies = gameClient.GetDependencies();
+ for (auto it = dependencies.begin(); it != dependencies.end(); ++it)
+ {
+ ControllerPtr controller = gameServices.GetController(it->id);
+ if (controller)
+ controllers.push_back(controller);
+ }
+
+ if (controllers.empty())
+ {
+ // Use the default controller
+ ControllerPtr controller = gameServices.GetDefaultController();
+ if (controller)
+ controllers.push_back(controller);
+ }
+
+ return controllers;
+}
diff --git a/xbmc/games/addons/input/GameClientInput.h b/xbmc/games/addons/input/GameClientInput.h
new file mode 100644
index 0000000..be7e71c
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientInput.h
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2017-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 "games/addons/GameClientSubsystem.h"
+#include "games/controllers/ControllerTypes.h"
+#include "games/controllers/types/ControllerTree.h"
+#include "peripherals/PeripheralTypes.h"
+#include "utils/Observer.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+class CCriticalSection;
+struct game_input_event;
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IInputProvider;
+}
+
+namespace GAME
+{
+class CGameClient;
+class CGameClientController;
+class CGameClientHardware;
+class CGameClientJoystick;
+class CGameClientKeyboard;
+class CGameClientMouse;
+class CGameClientTopology;
+class CPortManager;
+class IGameInputCallback;
+
+class CGameClientInput : protected CGameClientSubsystem, public Observable
+{
+public:
+ //! @todo de-duplicate
+ using PortAddress = std::string;
+ using JoystickMap = std::map<PortAddress, std::shared_ptr<CGameClientJoystick>>;
+
+ CGameClientInput(CGameClient& gameClient,
+ AddonInstance_Game& addonStruct,
+ CCriticalSection& clientAccess);
+ ~CGameClientInput() override;
+
+ void Initialize();
+ void Deinitialize();
+
+ void Start(IGameInputCallback* input);
+ void Stop();
+
+ // Input functions
+ bool HasFeature(const std::string& controllerId, const std::string& featureName) const;
+ bool AcceptsInput() const;
+ bool InputEvent(const game_input_event& event);
+
+ // Topology functions
+ const CControllerTree& GetDefaultControllerTree() const;
+ const CControllerTree& GetActiveControllerTree() const;
+ bool SupportsKeyboard() const;
+ bool SupportsMouse() const;
+ int GetPlayerLimit() const;
+ bool ConnectController(const std::string& portAddress, const ControllerPtr& controller);
+ bool DisconnectController(const std::string& portAddress);
+ void SavePorts();
+ void ResetPorts();
+
+ // Joystick functions
+ const JoystickMap& GetJoystickMap() const { return m_joysticks; }
+ void CloseJoysticks(PERIPHERALS::EventLockHandlePtr& inputHandlingLock);
+
+ // Keyboard functions
+ bool OpenKeyboard(const ControllerPtr& controller, const PERIPHERALS::PeripheralPtr& keyboard);
+ bool IsKeyboardOpen() const;
+ void CloseKeyboard();
+
+ // Mouse functions
+ bool OpenMouse(const ControllerPtr& controller, const PERIPHERALS::PeripheralPtr& mouse);
+ bool IsMouseOpen() const;
+ void CloseMouse();
+
+ // Agent functions
+ bool HasAgent() const;
+
+ // Hardware input functions
+ void HardwareReset();
+
+ // Input callbacks
+ bool ReceiveInputEvent(const game_input_event& eventStruct);
+
+private:
+ // Private input helpers
+ void LoadTopology();
+ void SetControllerLayouts(const ControllerVector& controllers);
+ bool OpenJoystick(const std::string& portAddress, const ControllerPtr& controller);
+ void CloseJoysticks(const CPortNode& port, PERIPHERALS::EventLockHandlePtr& inputHandlingLock);
+ void CloseJoystick(const std::string& portAddress,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock);
+
+ // Private callback helpers
+ bool SetRumble(const std::string& portAddress, const std::string& feature, float magnitude);
+
+ // Helper functions
+ static ControllerVector GetControllers(const CGameClient& gameClient);
+ static void ActivateControllers(CControllerHub& hub);
+
+ // Input properties
+ IGameInputCallback* m_inputCallback = nullptr;
+ std::unique_ptr<CGameClientTopology> m_topology;
+ using ControllerLayoutMap = std::map<std::string, std::unique_ptr<CGameClientController>>;
+ ControllerLayoutMap m_controllerLayouts;
+
+ /*!
+ * \brief Map of port address to joystick handler
+ *
+ * The port address is a string that identifies the adress of the port.
+ *
+ * The joystick handler connects to joystick input of the game client.
+ *
+ * This property is always populated with the default joystick configuration
+ * (i.e. all ports are connected to the first controller they accept).
+ */
+ JoystickMap m_joysticks;
+
+ // TODO: Guard with a mutex
+ std::unique_ptr<CPortManager> m_portManager;
+
+ /*!
+ * \brief Keyboard handler
+ *
+ * This connects to the keyboard input of the game client.
+ */
+ std::unique_ptr<CGameClientKeyboard> m_keyboard;
+
+ /*!
+ * \brief Mouse handler
+ *
+ * This connects to the mouse input of the game client.
+ */
+ std::unique_ptr<CGameClientMouse> m_mouse;
+
+ /*!
+ * \brief Hardware input handler
+ *
+ * This connects to input from game console hardware belonging to the game
+ * client.
+ */
+ std::unique_ptr<CGameClientHardware> m_hardware;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientJoystick.cpp b/xbmc/games/addons/input/GameClientJoystick.cpp
new file mode 100644
index 0000000..d4f083b
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientJoystick.cpp
@@ -0,0 +1,205 @@
+/*
+ * 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 "GameClientJoystick.h"
+
+#include "GameClientInput.h"
+#include "GameClientTopology.h"
+#include "games/addons/GameClient.h"
+#include "games/controllers/Controller.h"
+#include "games/ports/input/PortInput.h"
+#include "input/joysticks/interfaces/IInputReceiver.h"
+#include "peripherals/devices/Peripheral.h"
+#include "utils/log.h"
+
+#include <assert.h>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientJoystick::CGameClientJoystick(CGameClient& gameClient,
+ const std::string& portAddress,
+ const ControllerPtr& controller)
+ : m_gameClient(gameClient),
+ m_portAddress(portAddress),
+ m_controller(controller),
+ m_portInput(new CPortInput(this))
+{
+ assert(m_controller.get() != NULL);
+}
+
+CGameClientJoystick::~CGameClientJoystick() = default;
+
+void CGameClientJoystick::RegisterInput(JOYSTICK::IInputProvider* inputProvider)
+{
+ m_portInput->RegisterInput(inputProvider);
+}
+
+void CGameClientJoystick::UnregisterInput(JOYSTICK::IInputProvider* inputProvider)
+{
+ m_portInput->UnregisterInput(inputProvider);
+}
+
+std::string CGameClientJoystick::ControllerID(void) const
+{
+ return m_controller->ID();
+}
+
+bool CGameClientJoystick::HasFeature(const std::string& feature) const
+{
+ return m_gameClient.Input().HasFeature(m_controller->ID(), feature);
+}
+
+bool CGameClientJoystick::AcceptsInput(const std::string& feature) const
+{
+ return m_gameClient.Input().AcceptsInput();
+}
+
+bool CGameClientJoystick::OnButtonPress(const std::string& feature, bool bPressed)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_DIGITAL_BUTTON;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.digital_button.pressed = bPressed;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientJoystick::OnButtonMotion(const std::string& feature,
+ float magnitude,
+ unsigned int motionTimeMs)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_ANALOG_BUTTON;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.analog_button.magnitude = magnitude;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientJoystick::OnAnalogStickMotion(const std::string& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_ANALOG_STICK;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.analog_stick.x = x;
+ event.analog_stick.y = y;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientJoystick::OnAccelerometerMotion(const std::string& feature,
+ float x,
+ float y,
+ float z)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_ACCELEROMETER;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.accelerometer.x = x;
+ event.accelerometer.y = y;
+ event.accelerometer.z = z;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientJoystick::OnWheelMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_AXIS;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.axis.position = position;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientJoystick::OnThrottleMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ game_input_event event;
+
+ std::string controllerId = m_controller->ID();
+
+ event.type = GAME_INPUT_EVENT_AXIS;
+ event.controller_id = controllerId.c_str();
+ event.port_type = GAME_PORT_CONTROLLER;
+ event.port_address = m_portAddress.c_str();
+ event.feature_name = feature.c_str();
+ event.axis.position = position;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+std::string CGameClientJoystick::GetControllerAddress() const
+{
+ return CGameClientTopology::MakeAddress(m_portAddress, m_controller->ID());
+}
+
+std::string CGameClientJoystick::GetSourceLocation() const
+{
+ if (m_sourcePeripheral)
+ return m_sourcePeripheral->Location();
+
+ return "";
+}
+
+void CGameClientJoystick::SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral)
+{
+ m_sourcePeripheral = std::move(sourcePeripheral);
+}
+
+void CGameClientJoystick::ClearSource()
+{
+ m_sourcePeripheral.reset();
+}
+
+bool CGameClientJoystick::SetRumble(const std::string& feature, float magnitude)
+{
+ bool bHandled = false;
+
+ if (InputReceiver())
+ bHandled = InputReceiver()->SetRumbleState(feature, magnitude);
+
+ return bHandled;
+}
diff --git a/xbmc/games/addons/input/GameClientJoystick.h b/xbmc/games/addons/input/GameClientJoystick.h
new file mode 100644
index 0000000..cfb1709
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientJoystick.h
@@ -0,0 +1,101 @@
+/*
+ * 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 "games/controllers/ControllerTypes.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+#include "peripherals/PeripheralTypes.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IInputProvider;
+}
+
+namespace GAME
+{
+class CGameClient;
+class CPortInput;
+
+/*!
+ * \ingroup games
+ * \brief Handles game controller events for games.
+ *
+ * Listens to game controller events and forwards them to the games (as game_input_event).
+ */
+class CGameClientJoystick : public JOYSTICK::IInputHandler
+{
+public:
+ /*!
+ * \brief Constructor.
+ * \param addon The game client implementation.
+ * \param port The port this game controller is associated with.
+ * \param controller The game controller which is used (for controller mapping).
+ * \param dllStruct The emulator or game to which the events are sent.
+ */
+ CGameClientJoystick(CGameClient& addon,
+ const std::string& portAddress,
+ const ControllerPtr& controller);
+
+ ~CGameClientJoystick() override;
+
+ void RegisterInput(JOYSTICK::IInputProvider* inputProvider);
+ void UnregisterInput(JOYSTICK::IInputProvider* inputProvider);
+
+ // Implementation of IInputHandler
+ std::string ControllerID() const override;
+ bool HasFeature(const std::string& feature) const override;
+ bool AcceptsInput(const std::string& feature) const override;
+ bool OnButtonPress(const std::string& feature, bool bPressed) override;
+ void OnButtonHold(const std::string& feature, unsigned int holdTimeMs) override {}
+ bool OnButtonMotion(const std::string& feature,
+ float magnitude,
+ unsigned int motionTimeMs) override;
+ bool OnAnalogStickMotion(const std::string& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs) override;
+ bool OnAccelerometerMotion(const std::string& feature, float x, float y, float z) override;
+ bool OnWheelMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ bool OnThrottleMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ void OnInputFrame() override {}
+
+ // Input accessors
+ const std::string& GetPortAddress() const { return m_portAddress; }
+ const ControllerPtr& GetController() const { return m_controller; }
+ std::string GetControllerAddress() const;
+ const PERIPHERALS::PeripheralPtr& GetSource() const { return m_sourcePeripheral; }
+ std::string GetSourceLocation() const;
+
+ // Input mutators
+ void SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral);
+ void ClearSource();
+
+ // Input handlers
+ bool SetRumble(const std::string& feature, float magnitude);
+
+private:
+ // Construction parameters
+ CGameClient& m_gameClient;
+ const std::string m_portAddress;
+ const ControllerPtr m_controller;
+
+ // Input parameters
+ std::unique_ptr<CPortInput> m_portInput;
+ PERIPHERALS::PeripheralPtr m_sourcePeripheral;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientKeyboard.cpp b/xbmc/games/addons/input/GameClientKeyboard.cpp
new file mode 100644
index 0000000..e6f48c9
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientKeyboard.cpp
@@ -0,0 +1,101 @@
+/*
+ * 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 "GameClientKeyboard.h"
+
+#include "GameClientInput.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/GameClientTranslator.h"
+#include "input/keyboard/interfaces/IKeyboardInputProvider.h"
+#include "utils/log.h"
+
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+#define BUTTON_INDEX_MASK 0x01ff
+
+CGameClientKeyboard::CGameClientKeyboard(CGameClient& gameClient,
+ std::string controllerId,
+ KEYBOARD::IKeyboardInputProvider* inputProvider)
+ : m_gameClient(gameClient),
+ m_controllerId(std::move(controllerId)),
+ m_inputProvider(inputProvider)
+{
+ m_inputProvider->RegisterKeyboardHandler(this, false);
+}
+
+CGameClientKeyboard::~CGameClientKeyboard()
+{
+ m_inputProvider->UnregisterKeyboardHandler(this);
+}
+
+std::string CGameClientKeyboard::ControllerID() const
+{
+ return m_controllerId;
+}
+
+bool CGameClientKeyboard::HasKey(const KEYBOARD::KeyName& key) const
+{
+ return m_gameClient.Input().HasFeature(ControllerID(), key);
+}
+
+bool CGameClientKeyboard::OnKeyPress(const KEYBOARD::KeyName& key,
+ KEYBOARD::Modifier mod,
+ uint32_t unicode)
+{
+ // Only allow activated input in fullscreen game
+ if (!m_gameClient.Input().AcceptsInput())
+ {
+ CLog::Log(LOGDEBUG, "GAME: key press ignored, not in fullscreen game");
+ return false;
+ }
+
+ game_input_event event;
+
+ event.type = GAME_INPUT_EVENT_KEY;
+ event.controller_id = m_controllerId.c_str();
+ event.port_type = GAME_PORT_KEYBOARD;
+ event.port_address = ""; // Not used
+ event.feature_name = key.c_str();
+ event.key.pressed = true;
+ event.key.unicode = unicode;
+ event.key.modifiers = CGameClientTranslator::GetModifiers(mod);
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+void CGameClientKeyboard::OnKeyRelease(const KEYBOARD::KeyName& key,
+ KEYBOARD::Modifier mod,
+ uint32_t unicode)
+{
+ game_input_event event;
+
+ event.type = GAME_INPUT_EVENT_KEY;
+ event.controller_id = m_controllerId.c_str();
+ event.port_type = GAME_PORT_KEYBOARD;
+ event.port_address = ""; // Not used
+ event.feature_name = key.c_str();
+ event.key.pressed = false;
+ event.key.unicode = unicode;
+ event.key.modifiers = CGameClientTranslator::GetModifiers(mod);
+
+ m_gameClient.Input().InputEvent(event);
+}
+
+void CGameClientKeyboard::SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral)
+{
+ m_sourcePeripheral = std::move(sourcePeripheral);
+}
+
+void CGameClientKeyboard::ClearSource()
+{
+ m_sourcePeripheral.reset();
+}
diff --git a/xbmc/games/addons/input/GameClientKeyboard.h b/xbmc/games/addons/input/GameClientKeyboard.h
new file mode 100644
index 0000000..a6103f2
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientKeyboard.h
@@ -0,0 +1,76 @@
+/*
+ * 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 "input/keyboard/interfaces/IKeyboardInputHandler.h"
+#include "peripherals/PeripheralTypes.h"
+
+namespace KODI
+{
+namespace KEYBOARD
+{
+class IKeyboardInputProvider;
+}
+
+namespace GAME
+{
+class CGameClient;
+
+/*!
+ * \ingroup games
+ * \brief Handles keyboard events for games.
+ *
+ * Listens to keyboard events and forwards them to the games (as game_input_event).
+ */
+class CGameClientKeyboard : public KEYBOARD::IKeyboardInputHandler
+{
+public:
+ /*!
+ * \brief Constructor registers for keyboard events at CInputManager.
+ * \param gameClient The game client implementation.
+ * \param controllerId The controller profile used for input
+ * \param dllStruct The emulator or game to which the events are sent.
+ * \param inputProvider The interface providing us with keyboard input.
+ */
+ CGameClientKeyboard(CGameClient& gameClient,
+ std::string controllerId,
+ KEYBOARD::IKeyboardInputProvider* inputProvider);
+
+ /*!
+ * \brief Destructor unregisters from keyboard events from CInputManager.
+ */
+ ~CGameClientKeyboard() override;
+
+ // implementation of IKeyboardInputHandler
+ std::string ControllerID() const override;
+ bool HasKey(const KEYBOARD::KeyName& key) const override;
+ bool OnKeyPress(const KEYBOARD::KeyName& key, KEYBOARD::Modifier mod, uint32_t unicode) override;
+ void OnKeyRelease(const KEYBOARD::KeyName& key,
+ KEYBOARD::Modifier mod,
+ uint32_t unicode) override;
+
+ // Input accessors
+ const std::string& GetControllerID() const { return m_controllerId; }
+ const PERIPHERALS::PeripheralPtr& GetSource() const { return m_sourcePeripheral; }
+
+ // Input mutators
+ void SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral);
+ void ClearSource();
+
+private:
+ // Construction parameters
+ CGameClient& m_gameClient;
+ const std::string m_controllerId;
+ KEYBOARD::IKeyboardInputProvider* const m_inputProvider;
+
+ // Input parameters
+ PERIPHERALS::PeripheralPtr m_sourcePeripheral;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientMouse.cpp b/xbmc/games/addons/input/GameClientMouse.cpp
new file mode 100644
index 0000000..5cc9676
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientMouse.cpp
@@ -0,0 +1,106 @@
+/*
+ * 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 "GameClientMouse.h"
+
+#include "GameClientInput.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/addons/GameClient.h"
+#include "input/mouse/interfaces/IMouseInputProvider.h"
+
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientMouse::CGameClientMouse(CGameClient& gameClient,
+ std::string controllerId,
+ MOUSE::IMouseInputProvider* inputProvider)
+ : m_gameClient(gameClient),
+ m_controllerId(std::move(controllerId)),
+ m_inputProvider(inputProvider)
+{
+ inputProvider->RegisterMouseHandler(this, false);
+}
+
+CGameClientMouse::~CGameClientMouse()
+{
+ m_inputProvider->UnregisterMouseHandler(this);
+}
+
+std::string CGameClientMouse::ControllerID(void) const
+{
+ return m_controllerId;
+}
+
+bool CGameClientMouse::OnMotion(const std::string& relpointer, int dx, int dy)
+{
+ // Only allow activated input in fullscreen game
+ if (!m_gameClient.Input().AcceptsInput())
+ {
+ return false;
+ }
+
+ const std::string controllerId = ControllerID();
+
+ game_input_event event;
+
+ event.type = GAME_INPUT_EVENT_RELATIVE_POINTER;
+ event.controller_id = m_controllerId.c_str();
+ event.port_type = GAME_PORT_MOUSE;
+ event.port_address = ""; // Not used
+ event.feature_name = relpointer.c_str();
+ event.rel_pointer.x = dx;
+ event.rel_pointer.y = dy;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+bool CGameClientMouse::OnButtonPress(const std::string& button)
+{
+ // Only allow activated input in fullscreen game
+ if (!m_gameClient.Input().AcceptsInput())
+ {
+ return false;
+ }
+
+ game_input_event event;
+
+ event.type = GAME_INPUT_EVENT_DIGITAL_BUTTON;
+ event.controller_id = m_controllerId.c_str();
+ event.port_type = GAME_PORT_MOUSE;
+ event.port_address = ""; // Not used
+ event.feature_name = button.c_str();
+ event.digital_button.pressed = true;
+
+ return m_gameClient.Input().InputEvent(event);
+}
+
+void CGameClientMouse::OnButtonRelease(const std::string& button)
+{
+ game_input_event event;
+
+ event.type = GAME_INPUT_EVENT_DIGITAL_BUTTON;
+ event.controller_id = m_controllerId.c_str();
+ event.port_type = GAME_PORT_MOUSE;
+ event.port_address = ""; // Not used
+ event.feature_name = button.c_str();
+ event.digital_button.pressed = false;
+
+ m_gameClient.Input().InputEvent(event);
+}
+
+void CGameClientMouse::SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral)
+{
+ m_sourcePeripheral = std::move(sourcePeripheral);
+}
+
+void CGameClientMouse::ClearSource()
+{
+ m_sourcePeripheral.reset();
+}
diff --git a/xbmc/games/addons/input/GameClientMouse.h b/xbmc/games/addons/input/GameClientMouse.h
new file mode 100644
index 0000000..4da592f
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientMouse.h
@@ -0,0 +1,74 @@
+/*
+ * 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 "input/mouse/interfaces/IMouseInputHandler.h"
+#include "peripherals/PeripheralTypes.h"
+
+namespace KODI
+{
+namespace MOUSE
+{
+class IMouseInputProvider;
+}
+
+namespace GAME
+{
+class CGameClient;
+
+/*!
+ * \ingroup games
+ * \brief Handles mouse events for games.
+ *
+ * Listens to mouse events and forwards them to the games (as game_input_event).
+ */
+class CGameClientMouse : public MOUSE::IMouseInputHandler
+{
+public:
+ /*!
+ * \brief Constructor registers for mouse events at CInputManager.
+ * \param gameClient The game client implementation.
+ * \param controllerId The controller profile used for input
+ * \param dllStruct The emulator or game to which the events are sent.
+ * \param inputProvider The interface providing us with mouse input.
+ */
+ CGameClientMouse(CGameClient& gameClient,
+ std::string controllerId,
+ MOUSE::IMouseInputProvider* inputProvider);
+
+ /*!
+ * \brief Destructor unregisters from mouse events from CInputManager.
+ */
+ ~CGameClientMouse() override;
+
+ // implementation of IMouseInputHandler
+ std::string ControllerID() const override;
+ bool OnMotion(const std::string& relpointer, int dx, int dy) override;
+ bool OnButtonPress(const std::string& button) override;
+ void OnButtonRelease(const std::string& button) override;
+
+ // Input accessors
+ const std::string& GetControllerID() const { return m_controllerId; }
+ const PERIPHERALS::PeripheralPtr& GetSource() const { return m_sourcePeripheral; }
+
+ // Input mutators
+ void SetSource(PERIPHERALS::PeripheralPtr sourcePeripheral);
+ void ClearSource();
+
+private:
+ // Construction parameters
+ CGameClient& m_gameClient;
+ const std::string m_controllerId;
+ MOUSE::IMouseInputProvider* const m_inputProvider;
+
+ // Input parameters
+ PERIPHERALS::PeripheralPtr m_sourcePeripheral;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientPort.cpp b/xbmc/games/addons/input/GameClientPort.cpp
new file mode 100644
index 0000000..c582fff
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientPort.cpp
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017-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 "GameClientPort.h"
+
+#include "GameClientDevice.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "games/addons/GameClientTranslator.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientPort::CGameClientPort(const game_input_port& port)
+ : m_type(CGameClientTranslator::TranslatePortType(port.type)),
+ m_portId(port.port_id ? port.port_id : ""),
+ m_forceConnected(port.force_connected)
+{
+ if (port.accepted_devices != nullptr)
+ {
+ for (unsigned int i = 0; i < port.device_count; i++)
+ {
+ std::unique_ptr<CGameClientDevice> device(new CGameClientDevice(port.accepted_devices[i]));
+
+ if (device->Controller() != CController::EmptyPtr)
+ m_acceptedDevices.emplace_back(std::move(device));
+ }
+ }
+}
+
+CGameClientPort::CGameClientPort(const ControllerVector& controllers)
+ : m_type(PORT_TYPE::CONTROLLER), m_portId(DEFAULT_PORT_ID)
+{
+ for (const auto& controller : controllers)
+ m_acceptedDevices.emplace_back(new CGameClientDevice(controller));
+}
+
+CGameClientPort::CGameClientPort(const game_input_port& logicalPort,
+ const CPhysicalPort& physicalPort)
+ : m_type(PORT_TYPE::CONTROLLER),
+ m_portId(physicalPort.ID()),
+ m_forceConnected(logicalPort.force_connected)
+{
+ if (logicalPort.accepted_devices != nullptr)
+ {
+ for (unsigned int i = 0; i < logicalPort.device_count; i++)
+ {
+ // Ensure device is physically compatible
+ const game_input_device& deviceStruct = logicalPort.accepted_devices[i];
+ std::string controllerId = deviceStruct.controller_id ? deviceStruct.controller_id : "";
+
+ if (physicalPort.IsCompatible(controllerId))
+ {
+ std::unique_ptr<CGameClientDevice> device(new CGameClientDevice(deviceStruct));
+
+ if (device->Controller() != CController::EmptyPtr)
+ m_acceptedDevices.emplace_back(std::move(device));
+ }
+ }
+ }
+}
+
+CGameClientPort::~CGameClientPort() = default;
diff --git a/xbmc/games/addons/input/GameClientPort.h b/xbmc/games/addons/input/GameClientPort.h
new file mode 100644
index 0000000..80fccef
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientPort.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2017-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 "games/GameTypes.h"
+#include "games/controllers/ControllerTypes.h"
+
+#include <string>
+
+struct game_input_device;
+struct game_input_port;
+
+namespace KODI
+{
+namespace GAME
+{
+class CPhysicalPort;
+
+/*!
+ * \ingroup games
+ * \brief Represents a port that devices can connect to
+ */
+class CGameClientPort
+{
+public:
+ /*!
+ * \brief Construct a hardware port
+ *
+ * \param port The hardware port Game API struct
+ */
+ CGameClientPort(const game_input_port& port);
+
+ /*!
+ * \brief Construct a hardware port that accepts the given controllers
+ *
+ * \param controllers List of accepted controller profiles
+ *
+ * The port is given the ID specified by DEFAULT_PORT_ID.
+ */
+ CGameClientPort(const ControllerVector& controllers);
+
+ /*!
+ * \brief Construct a controller port
+ *
+ * \param logicalPort The logical port Game API struct
+ * \param physicalPort The physical port definition
+ *
+ * The physical port is defined by the controller profile. This definition
+ * specifies which controllers the port is physically compatible with.
+ *
+ * The logical port is defined by the emulator's input topology. This
+ * definition specifies which controllers the emulator's logic can handle.
+ *
+ * Obviously, the controllers specified by the logical port must be a subset
+ * of the controllers supported by the physical port.
+ */
+ CGameClientPort(const game_input_port& logicalPort, const CPhysicalPort& physicalPort);
+
+ /*!
+ * \brief Destructor
+ */
+ ~CGameClientPort();
+
+ /*!
+ * \brief Get the port type
+ *
+ * The port type identifies if this port is for a keyboard, mouse, or
+ * controller.
+ */
+ PORT_TYPE PortType() const { return m_type; }
+
+ /*!
+ * \brief Get the ID of the port
+ *
+ * The ID is used when creating a toplogical address for the port.
+ */
+ const std::string& ID() const { return m_portId; }
+
+ /*!
+ * \brief True if a controller must be connected, preventing the disconnected
+ * option from being shown to the user
+ */
+ bool ForceConnected() const { return m_forceConnected; }
+
+ /*!
+ * \brief Get the list of devices accepted by this port
+ */
+ const GameClientDeviceVec& Devices() const { return m_acceptedDevices; }
+
+private:
+ PORT_TYPE m_type;
+ std::string m_portId;
+ bool m_forceConnected{false};
+ GameClientDeviceVec m_acceptedDevices;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/input/GameClientTopology.cpp b/xbmc/games/addons/input/GameClientTopology.cpp
new file mode 100644
index 0000000..e1e9757
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientTopology.cpp
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017-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 "GameClientTopology.h"
+
+#include "GameClientDevice.h"
+#include "GameClientPort.h"
+#include "games/controllers/Controller.h"
+
+#include <sstream>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+#define CONTROLLER_ADDRESS_SEPARATOR "/"
+
+CGameClientTopology::CGameClientTopology(GameClientPortVec ports, int playerLimit)
+ : m_ports(std::move(ports)), m_playerLimit(playerLimit), m_controllers(GetControllerTree(m_ports))
+{
+}
+
+void CGameClientTopology::Clear()
+{
+ m_ports.clear();
+ m_controllers.Clear();
+}
+
+CControllerTree CGameClientTopology::GetControllerTree(const GameClientPortVec& ports)
+{
+ CControllerTree tree;
+
+ PortVec controllerPorts;
+ for (const GameClientPortPtr& port : ports)
+ {
+ CPortNode portNode = GetPortNode(port, "");
+ controllerPorts.emplace_back(std::move(portNode));
+ }
+
+ tree.SetPorts(std::move(controllerPorts));
+
+ return tree;
+}
+
+CPortNode CGameClientTopology::GetPortNode(const GameClientPortPtr& port,
+ const std::string& controllerAddress)
+{
+ CPortNode portNode;
+
+ std::string portAddress = MakeAddress(controllerAddress, port->ID());
+
+ portNode.SetConnected(false);
+ portNode.SetPortType(port->PortType());
+ portNode.SetPortID(port->ID());
+ portNode.SetAddress(portAddress);
+ portNode.SetForceConnected(port->ForceConnected());
+
+ ControllerNodeVec nodes;
+ for (const GameClientDevicePtr& device : port->Devices())
+ {
+ CControllerNode controllerNode = GetControllerNode(device, portAddress);
+ nodes.emplace_back(std::move(controllerNode));
+ }
+ portNode.SetCompatibleControllers(std::move(nodes));
+
+ return portNode;
+}
+
+CControllerNode CGameClientTopology::GetControllerNode(const GameClientDevicePtr& device,
+ const std::string& portAddress)
+{
+ CControllerNode controllerNode;
+
+ const std::string controllerAddress = MakeAddress(portAddress, device->Controller()->ID());
+
+ controllerNode.SetController(device->Controller());
+ controllerNode.SetPortAddress(portAddress);
+ controllerNode.SetControllerAddress(controllerAddress);
+
+ PortVec ports;
+ for (const GameClientPortPtr& port : device->Ports())
+ {
+ CPortNode portNode = GetPortNode(port, controllerAddress);
+ ports.emplace_back(std::move(portNode));
+ }
+
+ CControllerHub controllerHub;
+ controllerHub.SetPorts(std::move(ports));
+ controllerNode.SetHub(std::move(controllerHub));
+
+ return controllerNode;
+}
+
+std::string CGameClientTopology::MakeAddress(const std::string& baseAddress,
+ const std::string& nodeId)
+{
+ std::ostringstream address;
+
+ if (!baseAddress.empty())
+ address << baseAddress;
+
+ address << CONTROLLER_ADDRESS_SEPARATOR << nodeId;
+
+ return address.str();
+}
diff --git a/xbmc/games/addons/input/GameClientTopology.h b/xbmc/games/addons/input/GameClientTopology.h
new file mode 100644
index 0000000..10b40f1
--- /dev/null
+++ b/xbmc/games/addons/input/GameClientTopology.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017-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 "games/GameTypes.h"
+#include "games/controllers/types/ControllerTree.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClientTopology
+{
+public:
+ CGameClientTopology() = default;
+ CGameClientTopology(GameClientPortVec ports, int playerLimit);
+
+ void Clear();
+
+ int GetPlayerLimit() const { return m_playerLimit; }
+
+ const CControllerTree& GetControllerTree() const { return m_controllers; }
+ CControllerTree& GetControllerTree() { return m_controllers; }
+
+ // Utility function
+ static std::string MakeAddress(const std::string& baseAddress, const std::string& nodeId);
+
+private:
+ static CControllerTree GetControllerTree(const GameClientPortVec& ports);
+ static CPortNode GetPortNode(const GameClientPortPtr& port, const std::string& controllerAddress);
+ static CControllerNode GetControllerNode(const GameClientDevicePtr& device,
+ const std::string& portAddress);
+
+ // Game API parameters
+ GameClientPortVec m_ports;
+ int m_playerLimit = -1;
+
+ // Controller parameters
+ CControllerTree m_controllers;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/streams/CMakeLists.txt b/xbmc/games/addons/streams/CMakeLists.txt
new file mode 100644
index 0000000..f8de0e6
--- /dev/null
+++ b/xbmc/games/addons/streams/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES GameClientStreamAudio.cpp
+ GameClientStreams.cpp
+ GameClientStreamSwFramebuffer.cpp
+ GameClientStreamVideo.cpp
+)
+
+set(HEADERS GameClientStreamAudio.h
+ GameClientStreams.h
+ GameClientStreamSwFramebuffer.h
+ GameClientStreamVideo.h
+ IGameClientStream.h
+)
+
+core_add_library(game_addon_streams)
diff --git a/xbmc/games/addons/streams/GameClientStreamAudio.cpp b/xbmc/games/addons/streams/GameClientStreamAudio.cpp
new file mode 100644
index 0000000..a38f054
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamAudio.cpp
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 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 "GameClientStreamAudio.h"
+
+#include "cores/RetroPlayer/streams/RetroPlayerAudio.h"
+#include "games/addons/GameClientTranslator.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientStreamAudio::CGameClientStreamAudio(double sampleRate) : m_sampleRate(sampleRate)
+{
+}
+
+bool CGameClientStreamAudio::OpenStream(RETRO::IRetroPlayerStream* stream,
+ const game_stream_properties& properties)
+{
+ RETRO::CRetroPlayerAudio* audioStream = dynamic_cast<RETRO::CRetroPlayerAudio*>(stream);
+ if (audioStream == nullptr)
+ {
+ CLog::Log(LOGERROR, "GAME: RetroPlayer stream is not an audio stream");
+ return false;
+ }
+
+ std::unique_ptr<RETRO::AudioStreamProperties> audioProperties(
+ TranslateProperties(properties.audio, m_sampleRate));
+ if (audioProperties)
+ {
+ if (audioStream->OpenStream(static_cast<const RETRO::StreamProperties&>(*audioProperties)))
+ m_stream = stream;
+ }
+
+ return m_stream != nullptr;
+}
+
+void CGameClientStreamAudio::CloseStream()
+{
+ if (m_stream != nullptr)
+ {
+ m_stream->CloseStream();
+ m_stream = nullptr;
+ }
+}
+
+void CGameClientStreamAudio::AddData(const game_stream_packet& packet)
+{
+ if (packet.type != GAME_STREAM_AUDIO)
+ return;
+
+ if (m_stream != nullptr)
+ {
+ const game_stream_audio_packet& audio = packet.audio;
+
+ RETRO::AudioStreamPacket audioPacket{audio.data, audio.size};
+
+ m_stream->AddStreamData(static_cast<RETRO::StreamPacket&>(audioPacket));
+ }
+}
+
+RETRO::AudioStreamProperties* CGameClientStreamAudio::TranslateProperties(
+ const game_stream_audio_properties& properties, double sampleRate)
+{
+ const RETRO::PCMFormat pcmFormat = CGameClientTranslator::TranslatePCMFormat(properties.format);
+ if (pcmFormat == RETRO::PCMFormat::FMT_UNKNOWN)
+ {
+ CLog::Log(LOGERROR, "GAME: Unknown PCM format: {}", static_cast<int>(properties.format));
+ return nullptr;
+ }
+
+ RETRO::AudioChannelMap channelMap = {{RETRO::AudioChannel::CH_NULL}};
+ unsigned int i = 0;
+ if (properties.channel_map != nullptr)
+ {
+ for (const GAME_AUDIO_CHANNEL* channelPtr = properties.channel_map; *channelPtr != GAME_CH_NULL;
+ channelPtr++)
+ {
+ RETRO::AudioChannel channel = CGameClientTranslator::TranslateAudioChannel(*channelPtr);
+ if (channel == RETRO::AudioChannel::CH_NULL)
+ {
+ CLog::Log(LOGERROR, "GAME: Unknown channel ID: {}", static_cast<int>(*channelPtr));
+ return nullptr;
+ }
+
+ channelMap[i++] = channel;
+ if (i + 1 >= channelMap.size())
+ break;
+ }
+ }
+ channelMap[i] = RETRO::AudioChannel::CH_NULL;
+
+ if (channelMap[0] == RETRO::AudioChannel::CH_NULL)
+ {
+ CLog::Log(LOGERROR, "GAME: Empty channel layout");
+ return nullptr;
+ }
+
+ return new RETRO::AudioStreamProperties{pcmFormat, sampleRate, channelMap};
+}
diff --git a/xbmc/games/addons/streams/GameClientStreamAudio.h b/xbmc/games/addons/streams/GameClientStreamAudio.h
new file mode 100644
index 0000000..d48b0a6
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamAudio.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 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 "IGameClientStream.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+
+#include <vector>
+
+namespace KODI
+{
+namespace RETRO
+{
+class IRetroPlayerStream;
+struct AudioStreamProperties;
+} // namespace RETRO
+
+namespace GAME
+{
+
+class CGameClientStreamAudio : public IGameClientStream
+{
+public:
+ CGameClientStreamAudio(double sampleRate);
+ ~CGameClientStreamAudio() override { CloseStream(); }
+
+ // Implementation of IGameClientStream
+ bool OpenStream(RETRO::IRetroPlayerStream* stream,
+ const game_stream_properties& properties) override;
+ void CloseStream() override;
+ void AddData(const game_stream_packet& packet) override;
+
+private:
+ // Utility functions
+ static RETRO::AudioStreamProperties* TranslateProperties(
+ const game_stream_audio_properties& properties, double sampleRate);
+
+ // Construction parameters
+ const double m_sampleRate;
+
+ // Stream parameters
+ RETRO::IRetroPlayerStream* m_stream = nullptr;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.cpp b/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.cpp
new file mode 100644
index 0000000..4d1e8af
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 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 "GameClientStreamSwFramebuffer.h"
+
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "cores/RetroPlayer/streams/RetroPlayerVideo.h"
+#include "games/addons/GameClientTranslator.h"
+
+using namespace KODI;
+using namespace GAME;
+
+bool CGameClientStreamSwFramebuffer::GetBuffer(unsigned int width,
+ unsigned int height,
+ game_stream_buffer& buffer)
+{
+ if (m_stream != nullptr)
+ {
+ RETRO::VideoStreamBuffer streamBuffer;
+ if (m_stream->GetStreamBuffer(width, height, static_cast<RETRO::StreamBuffer&>(streamBuffer)))
+ {
+ buffer.type = GAME_STREAM_SW_FRAMEBUFFER;
+
+ game_stream_sw_framebuffer_buffer& framebuffer = buffer.sw_framebuffer;
+
+ framebuffer.format = CGameClientTranslator::TranslatePixelFormat(streamBuffer.pixfmt);
+ framebuffer.data = streamBuffer.data;
+ framebuffer.size = streamBuffer.size;
+
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.h b/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.h
new file mode 100644
index 0000000..2b17dac
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamSwFramebuffer.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 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 "GameClientStreamVideo.h"
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CGameClientStreamSwFramebuffer : public CGameClientStreamVideo
+{
+public:
+ CGameClientStreamSwFramebuffer() = default;
+ ~CGameClientStreamSwFramebuffer() override = default;
+
+ // Implementation of IGameClientStream via CGameClientStreamVideo
+ bool GetBuffer(unsigned int width, unsigned int height, game_stream_buffer& buffer) override;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/streams/GameClientStreamVideo.cpp b/xbmc/games/addons/streams/GameClientStreamVideo.cpp
new file mode 100644
index 0000000..d110b28
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamVideo.cpp
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 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 "GameClientStreamVideo.h"
+
+#include "cores/RetroPlayer/streams/RetroPlayerVideo.h"
+#include "games/addons/GameClientTranslator.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+bool CGameClientStreamVideo::OpenStream(RETRO::IRetroPlayerStream* stream,
+ const game_stream_properties& properties)
+{
+ RETRO::CRetroPlayerVideo* videoStream = dynamic_cast<RETRO::CRetroPlayerVideo*>(stream);
+ if (videoStream == nullptr)
+ {
+ CLog::Log(LOGERROR, "GAME: RetroPlayer stream is not a video stream");
+ return false;
+ }
+
+ std::unique_ptr<RETRO::VideoStreamProperties> videoProperties(
+ TranslateProperties(properties.video));
+ if (videoProperties)
+ {
+ if (videoStream->OpenStream(static_cast<const RETRO::StreamProperties&>(*videoProperties)))
+ m_stream = stream;
+ }
+
+ return m_stream != nullptr;
+}
+
+void CGameClientStreamVideo::CloseStream()
+{
+ if (m_stream != nullptr)
+ {
+ m_stream->CloseStream();
+ m_stream = nullptr;
+ }
+}
+
+void CGameClientStreamVideo::AddData(const game_stream_packet& packet)
+{
+ if (packet.type != GAME_STREAM_VIDEO && packet.type != GAME_STREAM_SW_FRAMEBUFFER)
+ return;
+
+ if (m_stream != nullptr)
+ {
+ const game_stream_video_packet& video = packet.video;
+
+ RETRO::VideoRotation rotation = CGameClientTranslator::TranslateRotation(video.rotation);
+
+ RETRO::VideoStreamPacket videoPacket{
+ video.width, video.height, rotation, video.data, video.size,
+ };
+
+ m_stream->AddStreamData(static_cast<const RETRO::StreamPacket&>(videoPacket));
+ }
+}
+
+RETRO::VideoStreamProperties* CGameClientStreamVideo::TranslateProperties(
+ const game_stream_video_properties& properties)
+{
+ const AVPixelFormat pixelFormat = CGameClientTranslator::TranslatePixelFormat(properties.format);
+ if (pixelFormat == AV_PIX_FMT_NONE)
+ {
+ CLog::Log(LOGERROR, "GAME: Unknown pixel format: {}", properties.format);
+ return nullptr;
+ }
+
+ const unsigned int nominalWidth = properties.nominal_width;
+ const unsigned int nominalHeight = properties.nominal_height;
+ if (nominalWidth == 0 || nominalHeight == 0)
+ {
+ CLog::Log(LOGERROR, "GAME: Invalid nominal dimensions: {}x{}", nominalWidth, nominalHeight);
+ return nullptr;
+ }
+
+ const unsigned int maxWidth = properties.max_width;
+ const unsigned int maxHeight = properties.max_height;
+ if (maxWidth == 0 || maxHeight == 0)
+ {
+ CLog::Log(LOGERROR, "GAME: Invalid max dimensions: {}x{}", maxWidth, maxHeight);
+ return nullptr;
+ }
+
+ if (nominalWidth > maxWidth || nominalHeight > maxHeight)
+ CLog::Log(LOGERROR, "GAME: Nominal dimensions ({}x{}) bigger than max dimensions ({}x{})",
+ nominalWidth, nominalHeight, maxWidth, maxHeight);
+
+ float pixelAspectRatio;
+
+ // Game API: If aspect_ratio is <= 0.0, an aspect ratio of
+ // (nominal_width / nominal_height) is assumed
+ if (properties.aspect_ratio <= 0.0f)
+ pixelAspectRatio = 1.0f;
+ else
+ pixelAspectRatio = properties.aspect_ratio * nominalHeight / nominalWidth;
+
+ return new RETRO::VideoStreamProperties{pixelFormat, nominalWidth, nominalHeight,
+ maxWidth, maxHeight, pixelAspectRatio};
+}
diff --git a/xbmc/games/addons/streams/GameClientStreamVideo.h b/xbmc/games/addons/streams/GameClientStreamVideo.h
new file mode 100644
index 0000000..4c90c2c
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreamVideo.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 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 "IGameClientStream.h"
+
+struct game_stream_video_properties;
+
+namespace KODI
+{
+namespace RETRO
+{
+class IRetroPlayerStream;
+struct VideoStreamProperties;
+} // namespace RETRO
+
+namespace GAME
+{
+
+class CGameClientStreamVideo : public IGameClientStream
+{
+public:
+ CGameClientStreamVideo() = default;
+ ~CGameClientStreamVideo() override { CloseStream(); }
+
+ // Implementation of IGameClientStream
+ bool OpenStream(RETRO::IRetroPlayerStream* stream,
+ const game_stream_properties& properties) override;
+ void CloseStream() override;
+ void AddData(const game_stream_packet& packet) override;
+
+protected:
+ // Stream parameters
+ RETRO::IRetroPlayerStream* m_stream = nullptr;
+
+private:
+ // Utility functions
+ static RETRO::VideoStreamProperties* TranslateProperties(
+ const game_stream_video_properties& properties);
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/streams/GameClientStreams.cpp b/xbmc/games/addons/streams/GameClientStreams.cpp
new file mode 100644
index 0000000..7d005ea
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreams.cpp
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 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 "GameClientStreams.h"
+
+#include "GameClientStreamAudio.h"
+#include "GameClientStreamSwFramebuffer.h"
+#include "GameClientStreamVideo.h"
+#include "cores/RetroPlayer/streams/IRetroPlayerStream.h"
+#include "cores/RetroPlayer/streams/IStreamManager.h"
+#include "cores/RetroPlayer/streams/RetroPlayerStreamTypes.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/GameClientTranslator.h"
+#include "utils/log.h"
+
+#include <memory>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameClientStreams::CGameClientStreams(CGameClient& gameClient) : m_gameClient(gameClient)
+{
+}
+
+void CGameClientStreams::Initialize(RETRO::IStreamManager& streamManager)
+{
+ m_streamManager = &streamManager;
+}
+
+void CGameClientStreams::Deinitialize()
+{
+ m_streamManager = nullptr;
+}
+
+IGameClientStream* CGameClientStreams::OpenStream(const game_stream_properties& properties)
+{
+ if (m_streamManager == nullptr)
+ return nullptr;
+
+ RETRO::StreamType retroStreamType;
+ if (!CGameClientTranslator::TranslateStreamType(properties.type, retroStreamType))
+ {
+ CLog::Log(LOGERROR, "GAME: Invalid stream type: {}", static_cast<int>(properties.type));
+ return nullptr;
+ }
+
+ std::unique_ptr<IGameClientStream> gameStream = CreateStream(properties.type);
+ if (!gameStream)
+ {
+ CLog::Log(LOGERROR, "GAME: No stream implementation for type: {}",
+ static_cast<int>(properties.type));
+ return nullptr;
+ }
+
+ RETRO::StreamPtr retroStream = m_streamManager->CreateStream(retroStreamType);
+ if (!retroStream)
+ {
+ CLog::Log(LOGERROR, "GAME: Invalid RetroPlayer stream type: {}",
+ static_cast<int>(retroStreamType));
+ return nullptr;
+ }
+
+ if (!gameStream->OpenStream(retroStream.get(), properties))
+ {
+ CLog::Log(LOGERROR, "GAME: Failed to open audio stream");
+ return nullptr;
+ }
+
+ m_streams[gameStream.get()] = std::move(retroStream);
+
+ return gameStream.release();
+}
+
+void CGameClientStreams::CloseStream(IGameClientStream* stream)
+{
+ if (stream != nullptr)
+ {
+ std::unique_ptr<IGameClientStream> streamHolder(stream);
+ streamHolder->CloseStream();
+
+ m_streamManager->CloseStream(std::move(m_streams[stream]));
+ m_streams.erase(stream);
+ }
+}
+
+std::unique_ptr<IGameClientStream> CGameClientStreams::CreateStream(
+ GAME_STREAM_TYPE streamType) const
+{
+ std::unique_ptr<IGameClientStream> gameStream;
+
+ switch (streamType)
+ {
+ case GAME_STREAM_AUDIO:
+ {
+ gameStream.reset(new CGameClientStreamAudio(m_gameClient.GetSampleRate()));
+ break;
+ }
+ case GAME_STREAM_VIDEO:
+ {
+ gameStream.reset(new CGameClientStreamVideo);
+ break;
+ }
+ case GAME_STREAM_SW_FRAMEBUFFER:
+ {
+ gameStream.reset(new CGameClientStreamSwFramebuffer);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return gameStream;
+}
diff --git a/xbmc/games/addons/streams/GameClientStreams.h b/xbmc/games/addons/streams/GameClientStreams.h
new file mode 100644
index 0000000..6f3f639
--- /dev/null
+++ b/xbmc/games/addons/streams/GameClientStreams.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 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 "addons/kodi-dev-kit/include/kodi/addon-instance/Game.h"
+#include "cores/RetroPlayer/streams/RetroPlayerStreamTypes.h"
+
+#include <map>
+
+namespace KODI
+{
+namespace RETRO
+{
+class IStreamManager;
+}
+
+namespace GAME
+{
+
+class CGameClient;
+class IGameClientStream;
+
+class CGameClientStreams
+{
+public:
+ CGameClientStreams(CGameClient& gameClient);
+
+ void Initialize(RETRO::IStreamManager& streamManager);
+ void Deinitialize();
+
+ IGameClientStream* OpenStream(const game_stream_properties& properties);
+ void CloseStream(IGameClientStream* stream);
+
+private:
+ // Utility functions
+ std::unique_ptr<IGameClientStream> CreateStream(GAME_STREAM_TYPE streamType) const;
+
+ // Construction parameters
+ CGameClient& m_gameClient;
+
+ // Initialization parameters
+ RETRO::IStreamManager* m_streamManager = nullptr;
+
+ // Stream parameters
+ std::map<IGameClientStream*, RETRO::StreamPtr> m_streams;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/addons/streams/IGameClientStream.h b/xbmc/games/addons/streams/IGameClientStream.h
new file mode 100644
index 0000000..c2374bc
--- /dev/null
+++ b/xbmc/games/addons/streams/IGameClientStream.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 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
+
+struct game_stream_buffer;
+struct game_stream_packet;
+struct game_stream_properties;
+
+namespace KODI
+{
+namespace RETRO
+{
+class IRetroPlayerStream;
+}
+
+namespace GAME
+{
+
+class IGameClientStream
+{
+public:
+ virtual ~IGameClientStream() = default;
+
+ /*!
+ * \brief Open the stream
+ *
+ * \param stream The RetroPlayer resource to take ownership of
+ *
+ * \return True if the stream was opened, false otherwise
+ */
+ virtual bool OpenStream(RETRO::IRetroPlayerStream* stream,
+ const game_stream_properties& properties) = 0;
+
+ /*!
+ * \brief Release the RetroPlayer stream resource
+ */
+ virtual void CloseStream() = 0;
+
+ /*!
+ * \brief Get a buffer for zero-copy stream data
+ *
+ * \param width The framebuffer width, or 0 for no width specified
+ * \param height The framebuffer height, or 0 for no height specified
+ * \param[out] buffer The buffer, or unmodified if false is returned
+ *
+ * If this returns true, buffer must be freed using ReleaseBuffer().
+ *
+ * \return True if buffer was set, false otherwise
+ */
+ virtual bool GetBuffer(unsigned int width, unsigned int height, game_stream_buffer& buffer)
+ {
+ return false;
+ }
+
+ /*!
+ * \brief Free an allocated buffer
+ *
+ * \param buffer The buffer returned from GetBuffer()
+ */
+ virtual void ReleaseBuffer(game_stream_buffer& buffer) {}
+
+ /*!
+ * \brief Add a data packet to a stream
+ *
+ * \param packet The data packet
+ */
+ virtual void AddData(const game_stream_packet& packet) = 0;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/agents/CMakeLists.txt b/xbmc/games/agents/CMakeLists.txt
new file mode 100644
index 0000000..e66ee2f
--- /dev/null
+++ b/xbmc/games/agents/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES GameAgentManager.cpp
+)
+
+set(HEADERS GameAgentManager.h
+)
+
+core_add_library(games_agents)
diff --git a/xbmc/games/agents/GameAgentManager.cpp b/xbmc/games/agents/GameAgentManager.cpp
new file mode 100644
index 0000000..5b72fbf
--- /dev/null
+++ b/xbmc/games/agents/GameAgentManager.cpp
@@ -0,0 +1,547 @@
+/*
+ * Copyright (C) 2017-2022 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 "GameAgentManager.h"
+
+#include "ServiceBroker.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/addons/input/GameClientJoystick.h"
+#include "input/InputManager.h"
+#include "peripherals/EventLockHandle.h"
+#include "peripherals/Peripherals.h"
+#include "peripherals/devices/Peripheral.h"
+#include "peripherals/devices/PeripheralJoystick.h"
+#include "utils/log.h"
+
+#include <array>
+
+using namespace KODI;
+using namespace GAME;
+
+CGameAgentManager::CGameAgentManager(PERIPHERALS::CPeripherals& peripheralManager,
+ CInputManager& inputManager)
+ : m_peripheralManager(peripheralManager), m_inputManager(inputManager)
+{
+ // Register callbacks
+ m_peripheralManager.RegisterObserver(this);
+ m_inputManager.RegisterKeyboardDriverHandler(this);
+ m_inputManager.RegisterMouseDriverHandler(this);
+}
+
+CGameAgentManager::~CGameAgentManager()
+{
+ // Unregister callbacks in reverse order
+ m_inputManager.UnregisterMouseDriverHandler(this);
+ m_inputManager.UnregisterKeyboardDriverHandler(this);
+ m_peripheralManager.UnregisterObserver(this);
+}
+
+void CGameAgentManager::Start(GameClientPtr gameClient)
+{
+ // Initialize state
+ m_gameClient = std::move(gameClient);
+
+ // Register callbacks
+ if (m_gameClient)
+ m_gameClient->Input().RegisterObserver(this);
+}
+
+void CGameAgentManager::Stop()
+{
+ // Unregister callbacks in reverse order
+ if (m_gameClient)
+ m_gameClient->Input().UnregisterObserver(this);
+
+ // Close open joysticks
+ {
+ PERIPHERALS::EventLockHandlePtr inputHandlingLock;
+
+ for (const auto& [inputProvider, joystick] : m_portMap)
+ {
+ if (!inputHandlingLock)
+ inputHandlingLock = CServiceBroker::GetPeripherals().RegisterEventLock();
+
+ joystick->UnregisterInput(inputProvider);
+
+ SetChanged(true);
+ }
+
+ m_portMap.clear();
+ m_peripheralMap.clear();
+ m_disconnectedPeripherals.clear();
+ }
+
+ // Notify observers if anything changed
+ NotifyObservers(ObservableMessageGameAgentsChanged);
+
+ // Reset state
+ m_gameClient.reset();
+}
+
+void CGameAgentManager::Refresh()
+{
+ if (m_gameClient)
+ {
+ // Open keyboard
+ ProcessKeyboard();
+
+ // Open mouse
+ ProcessMouse();
+
+ // Open/close joysticks
+ PERIPHERALS::EventLockHandlePtr inputHandlingLock;
+ ProcessJoysticks(inputHandlingLock);
+ }
+
+ // Notify observers if anything changed
+ NotifyObservers(ObservableMessageGameAgentsChanged);
+}
+
+void CGameAgentManager::Notify(const Observable& obs, const ObservableMessage msg)
+{
+ switch (msg)
+ {
+ case ObservableMessageGamePortsChanged:
+ case ObservableMessagePeripheralsChanged:
+ {
+ Refresh();
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+bool CGameAgentManager::OnKeyPress(const CKey& key)
+{
+ m_bHasKeyboard = true;
+ return false;
+}
+
+bool CGameAgentManager::OnPosition(int x, int y)
+{
+ m_bHasMouse = true;
+ return false;
+}
+
+bool CGameAgentManager::OnButtonPress(MOUSE::BUTTON_ID button)
+{
+ m_bHasMouse = true;
+ return false;
+}
+
+void CGameAgentManager::ProcessJoysticks(PERIPHERALS::EventLockHandlePtr& inputHandlingLock)
+{
+ // Get system joysticks.
+ //
+ // It's important to hold these shared pointers for the function scope
+ // because we call into the input handlers in m_portMap.
+ //
+ // The input handlers are upcasted from their peripheral object, so the
+ // peripheral object must persist through this function.
+ //
+ PERIPHERALS::PeripheralVector joysticks;
+ m_peripheralManager.GetPeripheralsWithFeature(joysticks, PERIPHERALS::FEATURE_JOYSTICK);
+
+ // Remove "virtual" Android joysticks
+ //
+ // The heuristic used to identify these is to check if the device name is all
+ // lowercase letters and dashes (and contains at least one dash). The
+ // following virtual devices have been observed:
+ //
+ // shield-ask-remote
+ // sunxi-ir-uinput
+ // virtual-search
+ //
+ // Additionally, we specifically allow the following devices:
+ //
+ // virtual-remote
+ //
+ joysticks.erase(
+ std::remove_if(joysticks.begin(), joysticks.end(),
+ [](const PERIPHERALS::PeripheralPtr& joystick)
+ {
+ const std::string& joystickName = joystick->DeviceName();
+
+ // Skip joysticks in the allowlist
+ static const std::array<std::string, 1> peripheralAllowlist = {
+ "virtual-remote",
+ };
+ if (std::find_if(peripheralAllowlist.begin(), peripheralAllowlist.end(),
+ [&joystickName](const std::string& allowedJoystick) {
+ return allowedJoystick == joystickName;
+ }) != peripheralAllowlist.end())
+ {
+ return false;
+ }
+
+ // Require at least one dash
+ if (std::find_if(joystickName.begin(), joystickName.end(),
+ [](char c) { return c == '-'; }) == joystickName.end())
+ {
+ return false;
+ }
+
+ // Require all lowercase letters or dashes
+ if (std::find_if(joystickName.begin(), joystickName.end(),
+ [](char c)
+ {
+ const bool isLowercase = ('a' <= c && c <= 'z');
+ const bool isDash = (c == '-');
+ return !(isLowercase || isDash);
+ }) != joystickName.end())
+ {
+ return false;
+ }
+
+ // Joystick matches the pattern, remove it
+ return true;
+ }),
+ joysticks.end());
+
+ // Update expired joysticks
+ UpdateExpiredJoysticks(joysticks, inputHandlingLock);
+
+ // Perform the port mapping
+ PortMap newPortMap =
+ MapJoysticks(joysticks, m_gameClient->Input().GetJoystickMap(), m_currentPorts,
+ m_currentPeripherals, m_gameClient->Input().GetPlayerLimit());
+
+ // Update connected joysticks
+ std::set<PERIPHERALS::PeripheralPtr> disconnectedPeripherals;
+ UpdateConnectedJoysticks(joysticks, newPortMap, inputHandlingLock, disconnectedPeripherals);
+
+ // Rebuild peripheral map
+ PeripheralMap peripheralMap;
+ for (const auto& [inputProvider, joystick] : m_portMap)
+ peripheralMap[joystick->GetControllerAddress()] = joystick->GetSource();
+
+ // Log peripheral map if there were any changes
+ if (peripheralMap != m_peripheralMap || disconnectedPeripherals != m_disconnectedPeripherals)
+ {
+ m_peripheralMap = std::move(peripheralMap);
+ m_disconnectedPeripherals = std::move(disconnectedPeripherals);
+ LogPeripheralMap(m_peripheralMap, m_disconnectedPeripherals);
+ }
+}
+
+void CGameAgentManager::ProcessKeyboard()
+{
+ if (m_bHasKeyboard && m_gameClient->Input().SupportsKeyboard() &&
+ !m_gameClient->Input().IsKeyboardOpen())
+ {
+ PERIPHERALS::PeripheralVector keyboards;
+ CServiceBroker::GetPeripherals().GetPeripheralsWithFeature(keyboards,
+ PERIPHERALS::FEATURE_KEYBOARD);
+ if (!keyboards.empty())
+ {
+ const CControllerTree& controllers = m_gameClient->Input().GetActiveControllerTree();
+
+ auto it = std::find_if(
+ controllers.GetPorts().begin(), controllers.GetPorts().end(),
+ [](const CPortNode& port) { return port.GetPortType() == PORT_TYPE::KEYBOARD; });
+
+ PERIPHERALS::PeripheralPtr keyboard = std::move(keyboards.at(0));
+ m_gameClient->Input().OpenKeyboard(it->GetActiveController().GetController(), keyboard);
+
+ SetChanged(true);
+ }
+ }
+}
+
+void CGameAgentManager::ProcessMouse()
+{
+ if (m_bHasMouse && m_gameClient->Input().SupportsMouse() && !m_gameClient->Input().IsMouseOpen())
+ {
+ PERIPHERALS::PeripheralVector mice;
+ CServiceBroker::GetPeripherals().GetPeripheralsWithFeature(mice, PERIPHERALS::FEATURE_MOUSE);
+ if (!mice.empty())
+ {
+ const CControllerTree& controllers = m_gameClient->Input().GetActiveControllerTree();
+
+ auto it = std::find_if(
+ controllers.GetPorts().begin(), controllers.GetPorts().end(),
+ [](const CPortNode& port) { return port.GetPortType() == PORT_TYPE::MOUSE; });
+
+ PERIPHERALS::PeripheralPtr mouse = std::move(mice.at(0));
+ m_gameClient->Input().OpenMouse(it->GetActiveController().GetController(), mouse);
+
+ SetChanged(true);
+ }
+ }
+}
+
+void CGameAgentManager::UpdateExpiredJoysticks(const PERIPHERALS::PeripheralVector& joysticks,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock)
+{
+ // Make a copy - expired joysticks are removed from m_portMap
+ PortMap portMapCopy = m_portMap;
+
+ for (const auto& [inputProvider, joystick] : portMapCopy)
+ {
+ // Structured binding cannot be captured, so make a copy
+ JOYSTICK::IInputProvider* const inputProviderCopy = inputProvider;
+
+ // Search peripheral vector for input provider
+ auto it2 = std::find_if(joysticks.begin(), joysticks.end(),
+ [inputProviderCopy](const PERIPHERALS::PeripheralPtr& joystick) {
+ // Upcast peripheral to input interface
+ JOYSTICK::IInputProvider* peripheralInput = joystick.get();
+
+ // Compare
+ return inputProviderCopy == peripheralInput;
+ });
+
+ // If peripheral wasn't found, then it was disconnected
+ const bool bDisconnected = (it2 == joysticks.end());
+
+ // Erase joystick from port map if its peripheral becomes disconnected
+ if (bDisconnected)
+ {
+ // Must use nullptr because peripheral has likely fallen out of scope,
+ // destroying the object
+ joystick->UnregisterInput(nullptr);
+
+ if (!inputHandlingLock)
+ inputHandlingLock = CServiceBroker::GetPeripherals().RegisterEventLock();
+ m_portMap.erase(inputProvider);
+
+ SetChanged(true);
+ }
+ }
+}
+
+void CGameAgentManager::UpdateConnectedJoysticks(
+ const PERIPHERALS::PeripheralVector& joysticks,
+ const PortMap& newPortMap,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock,
+ std::set<PERIPHERALS::PeripheralPtr>& disconnectedPeripherals)
+{
+ for (auto& peripheralJoystick : joysticks)
+ {
+ // Upcast peripheral to input interface
+ JOYSTICK::IInputProvider* inputProvider = peripheralJoystick.get();
+
+ // Get connection states
+ auto itConnectedPort = newPortMap.find(inputProvider);
+ auto itDisconnectedPort = m_portMap.find(inputProvider);
+
+ // Get possibly connected joystick
+ std::shared_ptr<CGameClientJoystick> newJoystick;
+ if (itConnectedPort != newPortMap.end())
+ newJoystick = itConnectedPort->second;
+
+ // Get possibly disconnected joystick
+ std::shared_ptr<CGameClientJoystick> oldJoystick;
+ if (itDisconnectedPort != m_portMap.end())
+ oldJoystick = itDisconnectedPort->second;
+
+ // Check for a change in joysticks
+ if (oldJoystick != newJoystick)
+ {
+ // Unregister old input handler
+ if (oldJoystick != nullptr)
+ {
+ oldJoystick->UnregisterInput(inputProvider);
+
+ if (!inputHandlingLock)
+ inputHandlingLock = CServiceBroker::GetPeripherals().RegisterEventLock();
+ m_portMap.erase(itDisconnectedPort);
+
+ SetChanged(true);
+ }
+
+ // Register new handler
+ if (newJoystick != nullptr)
+ {
+ newJoystick->RegisterInput(inputProvider);
+
+ m_portMap[inputProvider] = std::move(newJoystick);
+
+ SetChanged(true);
+ }
+ }
+ }
+
+ // Record disconnected peripherals
+ for (const auto& peripheral : joysticks)
+ {
+ // Upcast peripheral to input interface
+ JOYSTICK::IInputProvider* inputProvider = peripheral.get();
+
+ // Check if peripheral is disconnected
+ if (m_portMap.find(inputProvider) == m_portMap.end())
+ disconnectedPeripherals.emplace(peripheral);
+ }
+}
+
+CGameAgentManager::PortMap CGameAgentManager::MapJoysticks(
+ const PERIPHERALS::PeripheralVector& peripheralJoysticks,
+ const JoystickMap& gameClientjoysticks,
+ CurrentPortMap& currentPorts,
+ CurrentPeripheralMap& currentPeripherals,
+ int playerLimit)
+{
+ PortMap result;
+
+ // First, create a map of the current ports to attempt to preserve
+ // player numbers
+ for (const auto& [portAddress, joystick] : gameClientjoysticks)
+ {
+ std::string sourceLocation = joystick->GetSourceLocation();
+ if (!sourceLocation.empty())
+ currentPorts[portAddress] = std::move(sourceLocation);
+ }
+
+ // Allow reverse lookups by peripheral location
+ for (const auto& [portAddress, sourceLocation] : currentPorts)
+ currentPeripherals[sourceLocation] = portAddress;
+
+ // Next, create a list of joystick peripherals sorted by order of last
+ // button press. Joysticks without a current port are assigned in this
+ // order.
+ PERIPHERALS::PeripheralVector availableJoysticks = peripheralJoysticks;
+ std::sort(availableJoysticks.begin(), availableJoysticks.end(),
+ [](const PERIPHERALS::PeripheralPtr& lhs, const PERIPHERALS::PeripheralPtr& rhs) {
+ if (lhs->LastActive().IsValid() && !rhs->LastActive().IsValid())
+ return true;
+ if (!lhs->LastActive().IsValid() && rhs->LastActive().IsValid())
+ return false;
+
+ return lhs->LastActive() > rhs->LastActive();
+ });
+
+ // Loop through the active ports and assign joysticks
+ unsigned int iJoystick = 0;
+ for (const auto& [portAddress, gameClientJoystick] : gameClientjoysticks)
+ {
+ const unsigned int joystickCount = ++iJoystick;
+
+ // Check if we're out of joystick peripherals or over the topology limit
+ if (availableJoysticks.empty() ||
+ (playerLimit >= 0 && static_cast<int>(joystickCount) > playerLimit))
+ {
+ gameClientJoystick->ClearSource();
+ continue;
+ }
+
+ PERIPHERALS::PeripheralVector::iterator itJoystick = availableJoysticks.end();
+
+ // Attempt to preserve player numbers
+ auto itCurrentPort = currentPorts.find(portAddress);
+ if (itCurrentPort != currentPorts.end())
+ {
+ const PeripheralLocation& currentPeripheral = itCurrentPort->second;
+
+ // Find peripheral with matching source location
+ itJoystick = std::find_if(availableJoysticks.begin(), availableJoysticks.end(),
+ [&currentPeripheral](const PERIPHERALS::PeripheralPtr& joystick) {
+ return joystick->Location() == currentPeripheral;
+ });
+ }
+
+ if (itJoystick == availableJoysticks.end())
+ {
+ // Get the next most recently active joystick that doesn't have a current port
+ itJoystick = std::find_if(
+ availableJoysticks.begin(), availableJoysticks.end(),
+ [&currentPeripherals, &gameClientjoysticks](const PERIPHERALS::PeripheralPtr& joystick) {
+ const PeripheralLocation& joystickLocation = joystick->Location();
+
+ // If joystick doesn't have a current port, use it
+ auto itPeripheral = currentPeripherals.find(joystickLocation);
+ if (itPeripheral == currentPeripherals.end())
+ return true;
+
+ // Get the address of the last port this joystick was connected to
+ const PortAddress& portAddress = itPeripheral->second;
+
+ // If port is disconnected, use this joystick
+ if (gameClientjoysticks.find(portAddress) == gameClientjoysticks.end())
+ return true;
+
+ return false;
+ });
+ }
+
+ // If found, assign the port and remove from the lists
+ if (itJoystick != availableJoysticks.end())
+ {
+ // Dereference iterator
+ PERIPHERALS::PeripheralPtr peripheralJoystick = *itJoystick;
+
+ // Map joystick
+ MapJoystick(std::move(peripheralJoystick), gameClientJoystick, result);
+
+ // Remove from availableJoysticks list
+ availableJoysticks.erase(itJoystick);
+ }
+ else
+ {
+ // No joystick found, clear the port
+ gameClientJoystick->ClearSource();
+ }
+ }
+
+ return result;
+}
+
+void CGameAgentManager::MapJoystick(PERIPHERALS::PeripheralPtr peripheralJoystick,
+ std::shared_ptr<CGameClientJoystick> gameClientJoystick,
+ PortMap& result)
+{
+ // Upcast peripheral joystick to input provider
+ JOYSTICK::IInputProvider* inputProvider = peripheralJoystick.get();
+
+ // Update game joystick's source peripheral
+ gameClientJoystick->SetSource(std::move(peripheralJoystick));
+
+ // Map input provider to input handler
+ result[inputProvider] = std::move(gameClientJoystick);
+}
+
+void CGameAgentManager::LogPeripheralMap(
+ const PeripheralMap& peripheralMap,
+ const std::set<PERIPHERALS::PeripheralPtr>& disconnectedPeripherals)
+{
+ CLog::Log(LOGDEBUG, "===== Peripheral Map =====");
+
+ unsigned int line = 0;
+
+ if (!peripheralMap.empty())
+ {
+ for (const auto& [controllerAddress, peripheral] : peripheralMap)
+ {
+ if (line != 0)
+ CLog::Log(LOGDEBUG, "");
+ CLog::Log(LOGDEBUG, "{}:", controllerAddress);
+ CLog::Log(LOGDEBUG, " {} [{}]", peripheral->Location(), peripheral->DeviceName());
+
+ ++line;
+ }
+ }
+
+ if (!disconnectedPeripherals.empty())
+ {
+ if (line != 0)
+ CLog::Log(LOGDEBUG, "");
+ CLog::Log(LOGDEBUG, "Disconnected:");
+
+ // Sort by peripheral location
+ std::map<std::string, std::string> disconnectedPeripheralMap;
+ for (const auto& peripheral : disconnectedPeripherals)
+ disconnectedPeripheralMap[peripheral->Location()] = peripheral->DeviceName();
+
+ // Log location and device name for disconnected peripherals
+ for (const auto& [location, deviceName] : disconnectedPeripheralMap)
+ CLog::Log(LOGDEBUG, " {} [{}]", location, deviceName);
+ }
+
+ CLog::Log(LOGDEBUG, "==========================");
+}
diff --git a/xbmc/games/agents/GameAgentManager.h b/xbmc/games/agents/GameAgentManager.h
new file mode 100644
index 0000000..901a492
--- /dev/null
+++ b/xbmc/games/agents/GameAgentManager.h
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2017-2022 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 "games/GameTypes.h"
+#include "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+#include "input/mouse/interfaces/IMouseDriverHandler.h"
+#include "peripherals/PeripheralTypes.h"
+#include "utils/Observer.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+
+class CInputManager;
+
+namespace PERIPHERALS
+{
+class CPeripheral;
+class CPeripherals;
+} // namespace PERIPHERALS
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IInputProvider;
+}
+
+namespace GAME
+{
+class CGameAgent;
+class CGameClient;
+class CGameClientJoystick;
+
+/*!
+ * \brief Class to manage game-playing agents for a running game client
+ *
+ * Currently, port mapping is controller-based and does not take into account
+ * the human belonging to the controller. In the future, humans and possibly
+ * bots will be managed here.
+ *
+ * To map ports to controllers, a list of controllers is retrieved in
+ * ProcessJoysticks(). After expired controllers are removed, the port mapping
+ * occurs in the static function MapJoysticks(). The strategy is to simply
+ * sort controllers by heuristics and greedily assign to game ports.
+ */
+class CGameAgentManager : public Observable,
+ public Observer,
+ KEYBOARD::IKeyboardDriverHandler,
+ MOUSE::IMouseDriverHandler
+{
+public:
+ CGameAgentManager(PERIPHERALS::CPeripherals& peripheralManager, CInputManager& inputManager);
+
+ virtual ~CGameAgentManager();
+
+ // Lifecycle functions
+ void Start(GameClientPtr gameClient);
+ void Stop();
+ void Refresh();
+
+ // Implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override;
+
+ // Implementation of IKeyboardDriverHandler
+ bool OnKeyPress(const CKey& key) override;
+ void OnKeyRelease(const CKey& key) override {}
+
+ // Implementation of IMouseDriverHandler
+ bool OnPosition(int x, int y) override;
+ bool OnButtonPress(MOUSE::BUTTON_ID button) override;
+ void OnButtonRelease(MOUSE::BUTTON_ID button) override {}
+
+private:
+ //! @todo De-duplicate these types
+ using PortAddress = std::string;
+ using JoystickMap = std::map<PortAddress, std::shared_ptr<CGameClientJoystick>>;
+ using PortMap = std::map<JOYSTICK::IInputProvider*, std::shared_ptr<CGameClientJoystick>>;
+
+ using PeripheralLocation = std::string;
+ using CurrentPortMap = std::map<PortAddress, PeripheralLocation>;
+ using CurrentPeripheralMap = std::map<PeripheralLocation, PortAddress>;
+
+ using ControllerAddress = std::string;
+ using PeripheralMap = std::map<ControllerAddress, PERIPHERALS::PeripheralPtr>;
+
+ // Internal interface
+ void ProcessJoysticks(PERIPHERALS::EventLockHandlePtr& inputHandlingLock);
+ void ProcessKeyboard();
+ void ProcessMouse();
+
+ // Internal helpers
+ void UpdateExpiredJoysticks(const PERIPHERALS::PeripheralVector& joysticks,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock);
+ void UpdateConnectedJoysticks(const PERIPHERALS::PeripheralVector& joysticks,
+ const PortMap& newPortMap,
+ PERIPHERALS::EventLockHandlePtr& inputHandlingLock,
+ std::set<PERIPHERALS::PeripheralPtr>& disconnectedJoysticks);
+
+ // Static functionals
+ static PortMap MapJoysticks(const PERIPHERALS::PeripheralVector& peripheralJoysticks,
+ const JoystickMap& gameClientjoysticks,
+ CurrentPortMap& currentPorts,
+ CurrentPeripheralMap& currentPeripherals,
+ int playerLimit);
+ static void MapJoystick(PERIPHERALS::PeripheralPtr peripheralJoystick,
+ std::shared_ptr<CGameClientJoystick> gameClientJoystick,
+ PortMap& result);
+ static void LogPeripheralMap(const PeripheralMap& peripheralMap,
+ const std::set<PERIPHERALS::PeripheralPtr>& disconnectedPeripherals);
+
+ // Construction parameters
+ PERIPHERALS::CPeripherals& m_peripheralManager;
+ CInputManager& m_inputManager;
+
+ // State parameters
+ GameClientPtr m_gameClient;
+ bool m_bHasKeyboard = false;
+ bool m_bHasMouse = false;
+
+ /*!
+ * \brief Map of input provider to joystick handler
+ *
+ * The input provider is a handle to agent input.
+ *
+ * The joystick handler connects to joystick input of the game client.
+ *
+ * This property remembers which joysticks are actually being controlled by
+ * agents.
+ *
+ * Not exposed to the game.
+ */
+ PortMap m_portMap;
+
+ /*!
+ * \brief Map of the current ports to their peripheral
+ *
+ * This allows attempt to preserve player numbers.
+ */
+ CurrentPortMap m_currentPorts;
+
+ /*!
+ * \brief Map of the current peripherals to their port
+ *
+ * This allows attempt to preserve player numbers.
+ */
+ CurrentPeripheralMap m_currentPeripherals;
+
+ /*!
+ * Map of controller address to source peripheral
+ *
+ * Source peripherals are not exposed to the game.
+ */
+ PeripheralMap m_peripheralMap;
+
+ /*!
+ * Collection of disconnected joysticks
+ *
+ * Source peripherals are not exposed to the game.
+ */
+ std::set<PERIPHERALS::PeripheralPtr> m_disconnectedPeripherals;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/CMakeLists.txt b/xbmc/games/controllers/CMakeLists.txt
new file mode 100644
index 0000000..b54f5c0
--- /dev/null
+++ b/xbmc/games/controllers/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(SOURCES Controller.cpp
+ ControllerLayout.cpp
+ ControllerManager.cpp
+ ControllerTranslator.cpp
+ DefaultController.cpp
+)
+
+set(HEADERS Controller.h
+ ControllerDefinitions.h
+ ControllerIDs.h
+ ControllerLayout.h
+ ControllerManager.h
+ ControllerTranslator.h
+ ControllerTypes.h
+ DefaultController.h
+)
+
+core_add_library(games_controller)
diff --git a/xbmc/games/controllers/Controller.cpp b/xbmc/games/controllers/Controller.cpp
new file mode 100644
index 0000000..e51af4e
--- /dev/null
+++ b/xbmc/games/controllers/Controller.cpp
@@ -0,0 +1,157 @@
+/*
+ * 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 "Controller.h"
+
+#include "ControllerDefinitions.h"
+#include "ControllerLayout.h"
+#include "URL.h"
+#include "addons/addoninfo/AddonType.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+// --- FeatureTypeEqual --------------------------------------------------------
+
+struct FeatureTypeEqual
+{
+ FeatureTypeEqual(FEATURE_TYPE type, JOYSTICK::INPUT_TYPE inputType)
+ : type(type), inputType(inputType)
+ {
+ }
+
+ bool operator()(const CPhysicalFeature& feature) const
+ {
+ if (type == FEATURE_TYPE::UNKNOWN)
+ return true; // Match all feature types
+
+ if (type == FEATURE_TYPE::SCALAR && feature.Type() == FEATURE_TYPE::SCALAR)
+ {
+ if (inputType == JOYSTICK::INPUT_TYPE::UNKNOWN)
+ return true; // Match all input types
+
+ return inputType == feature.InputType();
+ }
+
+ return type == feature.Type();
+ }
+
+ const FEATURE_TYPE type;
+ const JOYSTICK::INPUT_TYPE inputType;
+};
+
+// --- CController -------------------------------------------------------------
+
+const ControllerPtr CController::EmptyPtr;
+
+CController::CController(const ADDON::AddonInfoPtr& addonInfo)
+ : CAddon(addonInfo, ADDON::AddonType::GAME_CONTROLLER), m_layout(new CControllerLayout)
+{
+}
+
+CController::~CController() = default;
+
+const CPhysicalFeature& CController::GetFeature(const std::string& name) const
+{
+ auto it =
+ std::find_if(m_features.begin(), m_features.end(),
+ [&name](const CPhysicalFeature& feature) { return name == feature.Name(); });
+
+ if (it != m_features.end())
+ return *it;
+
+ static const CPhysicalFeature invalid{};
+ return invalid;
+}
+
+unsigned int CController::FeatureCount(
+ FEATURE_TYPE type /* = FEATURE_TYPE::UNKNOWN */,
+ JOYSTICK::INPUT_TYPE inputType /* = JOYSTICK::INPUT_TYPE::UNKNOWN */) const
+{
+ auto featureCount =
+ std::count_if(m_features.begin(), m_features.end(), FeatureTypeEqual(type, inputType));
+ return static_cast<unsigned int>(featureCount);
+}
+
+void CController::GetFeatures(std::vector<std::string>& features,
+ FEATURE_TYPE type /* = FEATURE_TYPE::UNKNOWN */) const
+{
+ for (const CPhysicalFeature& feature : m_features)
+ {
+ if (type == FEATURE_TYPE::UNKNOWN || type == feature.Type())
+ features.push_back(feature.Name());
+ }
+}
+
+JOYSTICK::FEATURE_TYPE CController::FeatureType(const std::string& feature) const
+{
+ for (auto it = m_features.begin(); it != m_features.end(); ++it)
+ {
+ if (feature == it->Name())
+ return it->Type();
+ }
+ return JOYSTICK::FEATURE_TYPE::UNKNOWN;
+}
+
+JOYSTICK::INPUT_TYPE CController::GetInputType(const std::string& feature) const
+{
+ for (auto it = m_features.begin(); it != m_features.end(); ++it)
+ {
+ if (feature == it->Name())
+ return it->InputType();
+ }
+ return JOYSTICK::INPUT_TYPE::UNKNOWN;
+}
+
+bool CController::LoadLayout(void)
+{
+ if (!m_bLoaded)
+ {
+ std::string strLayoutXmlPath = LibPath();
+
+ CLog::Log(LOGINFO, "Loading controller layout: {}", CURL::GetRedacted(strLayoutXmlPath));
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(strLayoutXmlPath))
+ {
+ CLog::Log(LOGDEBUG, "Unable to load file: {} at line {}", xmlDoc.ErrorDesc(),
+ xmlDoc.ErrorRow());
+ return false;
+ }
+
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (!pRootElement || pRootElement->NoChildren() || pRootElement->ValueStr() != LAYOUT_XML_ROOT)
+ {
+ CLog::Log(LOGERROR, "Can't find root <{}> tag", LAYOUT_XML_ROOT);
+ return false;
+ }
+
+ m_layout->Deserialize(pRootElement, this, m_features);
+ if (m_layout->IsValid(true))
+ {
+ m_bLoaded = true;
+ }
+ else
+ {
+ m_layout->Reset();
+ }
+ }
+
+ return m_bLoaded;
+}
+
+const CPhysicalTopology& CController::Topology() const
+{
+ return m_layout->Topology();
+}
diff --git a/xbmc/games/controllers/Controller.h b/xbmc/games/controllers/Controller.h
new file mode 100644
index 0000000..2dd7c18
--- /dev/null
+++ b/xbmc/games/controllers/Controller.h
@@ -0,0 +1,120 @@
+/*
+ * 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 "ControllerTypes.h"
+#include "addons/Addon.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "input/joysticks/JoystickTypes.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerLayout;
+class CPhysicalTopology;
+
+using JOYSTICK::FEATURE_TYPE;
+
+class CController : public ADDON::CAddon
+{
+public:
+ explicit CController(const ADDON::AddonInfoPtr& addonInfo);
+
+ ~CController() override;
+
+ static const ControllerPtr EmptyPtr;
+
+ /*!
+ * \brief Get all controller features
+ *
+ * \return The features
+ */
+ const std::vector<CPhysicalFeature>& Features(void) const { return m_features; }
+
+ /*!
+ * \brief Get a feature by its name
+ *
+ * \param name The feature name
+ *
+ * \return The feature, or a feature of type FEATURE_TYPE::UNKNOWN if the name is invalid
+ */
+ const CPhysicalFeature& GetFeature(const std::string& name) const;
+
+ /*!
+ * \brief Get the count of controller features matching the specified types
+ *
+ * \param type The feature type, or FEATURE_TYPE::UNKNOWN to match all feature types
+ * \param inputType The input type, or INPUT_TYPE::UNKNOWN to match all input types
+ *
+ * \return The feature count
+ */
+ unsigned int FeatureCount(FEATURE_TYPE type = FEATURE_TYPE::UNKNOWN,
+ JOYSTICK::INPUT_TYPE inputType = JOYSTICK::INPUT_TYPE::UNKNOWN) const;
+
+ /*!
+ * \brief Get the features matching the specified type
+ *
+ * \param type The feature type, or FEATURE_TYPE::UNKNOWN to get all features
+ */
+ void GetFeatures(std::vector<std::string>& features,
+ FEATURE_TYPE type = FEATURE_TYPE::UNKNOWN) const;
+
+ /*!
+ * \brief Get the type of the specified feature
+ *
+ * \param feature The feature name to look up
+ *
+ * \return The feature type, or FEATURE_TYPE::UNKNOWN if an invalid feature was specified
+ */
+ FEATURE_TYPE FeatureType(const std::string& feature) const;
+
+ /*!
+ * \brief Get the input type of the specified feature
+ *
+ * \param feature The feature name to look up
+ *
+ * \return The input type of the feature, or INPUT_TYPE::UNKNOWN if unknown
+ */
+ JOYSTICK::INPUT_TYPE GetInputType(const std::string& feature) const;
+
+ /*!
+ * \brief Load the controller layout
+ *
+ * \return true if the layout is loaded or was already loaded, false otherwise
+ */
+ bool LoadLayout(void);
+
+ /*!
+ * \brief Get the controller layout
+ */
+ const CControllerLayout& Layout(void) const { return *m_layout; }
+
+ /*!
+ * \brief Get the controller's physical topology
+ *
+ * This defines how controllers physically connect to each other.
+ *
+ * \return The physical topology of the controller
+ */
+ const CPhysicalTopology& Topology() const;
+
+private:
+ std::unique_ptr<CControllerLayout> m_layout;
+ std::vector<CPhysicalFeature> m_features;
+ bool m_bLoaded = false;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/ControllerDefinitions.h b/xbmc/games/controllers/ControllerDefinitions.h
new file mode 100644
index 0000000..517eb50
--- /dev/null
+++ b/xbmc/games/controllers/ControllerDefinitions.h
@@ -0,0 +1,55 @@
+/*
+ * 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
+
+// XML definitions
+#define LAYOUT_XML_ROOT "layout"
+#define LAYOUT_XML_ELM_CATEGORY "category"
+#define LAYOUT_XML_ELM_BUTTON "button"
+#define LAYOUT_XML_ELM_ANALOG_STICK "analogstick"
+#define LAYOUT_XML_ELM_ACCELEROMETER "accelerometer"
+#define LAYOUT_XML_ELM_MOTOR "motor"
+#define LAYOUT_XML_ELM_RELPOINTER "relpointer"
+#define LAYOUT_XML_ELM_ABSPOINTER "abspointer"
+#define LAYOUT_XML_ELM_WHEEL "wheel"
+#define LAYOUT_XML_ELM_THROTTLE "throttle"
+#define LAYOUT_XML_ELM_KEY "key"
+#define LAYOUT_XML_ELM_TOPOLOGY "physicaltopology"
+#define LAYOUT_XML_ELM_PORT "port"
+#define LAYOUT_XML_ELM_ACCEPTS "accepts"
+#define LAYOUT_XML_ATTR_LAYOUT_LABEL "label"
+#define LAYOUT_XML_ATTR_LAYOUT_ICON "icon"
+#define LAYOUT_XML_ATTR_LAYOUT_IMAGE "image"
+#define LAYOUT_XML_ATTR_CATEGORY_NAME "name"
+#define LAYOUT_XML_ATTR_CATEGORY_LABEL "label"
+#define LAYOUT_XML_ATTR_FEATURE_NAME "name"
+#define LAYOUT_XML_ATTR_FEATURE_LABEL "label"
+#define LAYOUT_XML_ATTR_INPUT_TYPE "type"
+#define LAYOUT_XML_ATTR_KEY_SYMBOL "symbol"
+#define LAYOUT_XML_ATTR_PROVIDES_INPUT "providesinput"
+#define LAYOUT_XML_ATTR_PORT_ID "id"
+#define LAYOUT_XML_ATTR_CONTROLLER "controller"
+
+// Controller definitions
+#define FEATURE_CATEGORY_FACE "face"
+#define FEATURE_CATEGORY_SHOULDER "shoulder"
+#define FEATURE_CATEGORY_TRIGGER "triggers"
+#define FEATURE_CATEGORY_ANALOG_STICK "analogsticks"
+#define FEATURE_CATEGORY_ACCELEROMETER "accelerometer"
+#define FEATURE_CATEGORY_HAPTICS "haptics"
+#define FEATURE_CATEGORY_MOUSE_BUTTON "mouse"
+#define FEATURE_CATEGORY_POINTER "pointer"
+#define FEATURE_CATEGORY_LIGHTGUN "lightgun"
+#define FEATURE_CATEGORY_OFFSCREEN "offscreen"
+#define FEATURE_CATEGORY_KEY "keys"
+#define FEATURE_CATEGORY_KEYPAD "keypad"
+#define FEATURE_CATEGORY_HARDWARE "hardware"
+#define FEATURE_CATEGORY_WHEEL "wheel"
+#define FEATURE_CATEGORY_JOYSTICK "joysticks"
+#define FEATURE_CATEGORY_PADDLE "paddles"
diff --git a/xbmc/games/controllers/ControllerIDs.h b/xbmc/games/controllers/ControllerIDs.h
new file mode 100644
index 0000000..a9254e0
--- /dev/null
+++ b/xbmc/games/controllers/ControllerIDs.h
@@ -0,0 +1,15 @@
+/*
+ * Copyright (C) 2017-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
+
+// Default controller IDs
+#define DEFAULT_CONTROLLER_ID "game.controller.default"
+#define DEFAULT_KEYBOARD_ID "game.controller.keyboard"
+#define DEFAULT_MOUSE_ID "game.controller.mouse"
+#define DEFAULT_REMOTE_ID "game.controller.remote"
diff --git a/xbmc/games/controllers/ControllerLayout.cpp b/xbmc/games/controllers/ControllerLayout.cpp
new file mode 100644
index 0000000..85816c1
--- /dev/null
+++ b/xbmc/games/controllers/ControllerLayout.cpp
@@ -0,0 +1,159 @@
+/*
+ * 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 "ControllerLayout.h"
+
+#include "Controller.h"
+#include "ControllerDefinitions.h"
+#include "ControllerTranslator.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/URIUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <sstream>
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerLayout::CControllerLayout() : m_topology(new CPhysicalTopology)
+{
+}
+
+CControllerLayout::CControllerLayout(const CControllerLayout& other)
+ : m_controller(other.m_controller),
+ m_labelId(other.m_labelId),
+ m_icon(other.m_icon),
+ m_strImage(other.m_strImage),
+ m_topology(new CPhysicalTopology(*other.m_topology))
+{
+}
+
+CControllerLayout::~CControllerLayout() = default;
+
+void CControllerLayout::Reset(void)
+{
+ m_controller = nullptr;
+ m_labelId = -1;
+ m_icon.clear();
+ m_strImage.clear();
+ m_topology->Reset();
+}
+
+bool CControllerLayout::IsValid(bool bLog) const
+{
+ if (m_labelId < 0)
+ {
+ if (bLog)
+ CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", LAYOUT_XML_ROOT,
+ LAYOUT_XML_ATTR_LAYOUT_LABEL);
+ return false;
+ }
+
+ if (m_strImage.empty())
+ {
+ if (bLog)
+ CLog::Log(LOGDEBUG, "<{}> tag has no \"{}\" attribute", LAYOUT_XML_ROOT,
+ LAYOUT_XML_ATTR_LAYOUT_IMAGE);
+ return false;
+ }
+
+ return true;
+}
+
+std::string CControllerLayout::Label(void) const
+{
+ std::string label;
+
+ if (m_labelId >= 0 && m_controller != nullptr)
+ label = g_localizeStrings.GetAddonString(m_controller->ID(), m_labelId);
+
+ return label;
+}
+
+std::string CControllerLayout::ImagePath(void) const
+{
+ std::string path;
+
+ if (!m_strImage.empty() && m_controller != nullptr)
+ return URIUtils::AddFileToFolder(URIUtils::GetDirectory(m_controller->LibPath()), m_strImage);
+
+ return path;
+}
+
+void CControllerLayout::Deserialize(const TiXmlElement* pElement,
+ const CController* controller,
+ std::vector<CPhysicalFeature>& features)
+{
+ if (pElement == nullptr || controller == nullptr)
+ return;
+
+ // Controller (used for string lookup and path translation)
+ m_controller = controller;
+
+ // Label
+ std::string strLabel = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_LAYOUT_LABEL);
+ if (!strLabel.empty())
+ std::istringstream(strLabel) >> m_labelId;
+
+ // Icon
+ std::string icon = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_LAYOUT_ICON);
+ if (!icon.empty())
+ m_icon = icon;
+
+ // Fallback icon, use add-on icon
+ if (m_icon.empty())
+ m_icon = controller->Icon();
+
+ // Image
+ std::string image = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_LAYOUT_IMAGE);
+ if (!image.empty())
+ m_strImage = image;
+
+ for (const TiXmlElement* pChild = pElement->FirstChildElement(); pChild != nullptr;
+ pChild = pChild->NextSiblingElement())
+ {
+ if (pChild->ValueStr() == LAYOUT_XML_ELM_CATEGORY)
+ {
+ // Category
+ std::string strCategory = XMLUtils::GetAttribute(pChild, LAYOUT_XML_ATTR_CATEGORY_NAME);
+ JOYSTICK::FEATURE_CATEGORY category =
+ CControllerTranslator::TranslateFeatureCategory(strCategory);
+
+ // Category label
+ int categoryLabelId = -1;
+
+ std::string strCategoryLabelId =
+ XMLUtils::GetAttribute(pChild, LAYOUT_XML_ATTR_CATEGORY_LABEL);
+ if (!strCategoryLabelId.empty())
+ std::istringstream(strCategoryLabelId) >> categoryLabelId;
+
+ // Features
+ for (const TiXmlElement* pFeature = pChild->FirstChildElement(); pFeature != nullptr;
+ pFeature = pFeature->NextSiblingElement())
+ {
+ CPhysicalFeature feature;
+
+ if (feature.Deserialize(pFeature, controller, category, categoryLabelId))
+ features.push_back(feature);
+ }
+ }
+ else if (pChild->ValueStr() == LAYOUT_XML_ELM_TOPOLOGY)
+ {
+ // Topology
+ CPhysicalTopology topology;
+ if (topology.Deserialize(pChild))
+ *m_topology = std::move(topology);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Ignoring <{}> tag", pChild->ValueStr());
+ }
+ }
+}
diff --git a/xbmc/games/controllers/ControllerLayout.h b/xbmc/games/controllers/ControllerLayout.h
new file mode 100644
index 0000000..1589837
--- /dev/null
+++ b/xbmc/games/controllers/ControllerLayout.h
@@ -0,0 +1,92 @@
+/*
+ * 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 <memory>
+#include <string>
+#include <vector>
+
+class TiXmlElement;
+
+namespace KODI
+{
+namespace GAME
+{
+class CController;
+class CPhysicalFeature;
+class CPhysicalTopology;
+
+class CControllerLayout
+{
+public:
+ CControllerLayout();
+ CControllerLayout(const CControllerLayout& other);
+ ~CControllerLayout();
+
+ void Reset(void);
+
+ int LabelID(void) const { return m_labelId; }
+ const std::string& Icon(void) const { return m_icon; }
+ const std::string& Image(void) const { return m_strImage; }
+
+ /*!
+ * \brief Ensures the layout was deserialized correctly, and optionally logs if not
+ *
+ * \param bLog If true, output the cause of invalidness to the log
+ *
+ * \return True if the layout is valid and can be used in the GUI, false otherwise
+ */
+ bool IsValid(bool bLog) const;
+
+ /*!
+ * \brief Get the label of the primary layout used when mapping the controller
+ *
+ * \return The label, or empty if unknown
+ */
+ std::string Label(void) const;
+
+ /*!
+ * \brief Get the image path of the primary layout used when mapping the controller
+ *
+ * \return The image path, or empty if unknown
+ */
+ std::string ImagePath(void) const;
+
+ /*!
+ * \brief Get the physical topology of this controller
+ *
+ * The topology of a controller defines its ports and which controllers can
+ * physically be connected to them. Also, the topology defines if the
+ * controller can provide player input, which is false in the case of hubs.
+ *
+ * \return The physical topology of the controller
+ */
+ const CPhysicalTopology& Topology(void) const { return *m_topology; }
+
+ /*!
+ * \brief Deserialize the specified XML element
+ *
+ * \param pLayoutElement The XML element
+ * \param controller The controller, used to obtain read-only properties
+ * \param features The deserialized features, if any
+ */
+ void Deserialize(const TiXmlElement* pLayoutElement,
+ const CController* controller,
+ std::vector<CPhysicalFeature>& features);
+
+private:
+ const CController* m_controller = nullptr;
+ int m_labelId = -1;
+ std::string m_icon;
+ std::string m_strImage;
+ std::unique_ptr<CPhysicalTopology> m_topology;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/ControllerManager.cpp b/xbmc/games/controllers/ControllerManager.cpp
new file mode 100644
index 0000000..707733b
--- /dev/null
+++ b/xbmc/games/controllers/ControllerManager.cpp
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017-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 "ControllerManager.h"
+
+#include "Controller.h"
+#include "ControllerIDs.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerManager::CControllerManager(ADDON::CAddonMgr& addonManager)
+ : m_addonManager(addonManager)
+{
+ m_addonManager.Events().Subscribe(this, &CControllerManager::OnEvent);
+}
+
+CControllerManager::~CControllerManager()
+{
+ m_addonManager.Events().Unsubscribe(this);
+}
+
+ControllerPtr CControllerManager::GetController(const std::string& controllerId)
+{
+ using namespace ADDON;
+
+ std::lock_guard<CCriticalSection> lock(m_mutex);
+
+ ControllerPtr& cachedController = m_cache[controllerId];
+
+ if (!cachedController && m_failedControllers.find(controllerId) == m_failedControllers.end())
+ {
+ AddonPtr addon;
+ if (m_addonManager.GetAddon(controllerId, addon, AddonType::GAME_CONTROLLER,
+ OnlyEnabled::CHOICE_NO))
+ cachedController = LoadController(addon);
+ }
+
+ return cachedController;
+}
+
+ControllerPtr CControllerManager::GetDefaultController()
+{
+ return GetController(DEFAULT_CONTROLLER_ID);
+}
+
+ControllerPtr CControllerManager::GetDefaultKeyboard()
+{
+ return GetController(DEFAULT_KEYBOARD_ID);
+}
+
+ControllerPtr CControllerManager::GetDefaultMouse()
+{
+ return GetController(DEFAULT_MOUSE_ID);
+}
+
+ControllerVector CControllerManager::GetControllers()
+{
+ using namespace ADDON;
+
+ ControllerVector controllers;
+
+ std::lock_guard<CCriticalSection> lock(m_mutex);
+
+ VECADDONS addons;
+ if (m_addonManager.GetInstalledAddons(addons, AddonType::GAME_CONTROLLER))
+ {
+ for (auto& addon : addons)
+ {
+ ControllerPtr& cachedController = m_cache[addon->ID()];
+ if (!cachedController && m_failedControllers.find(addon->ID()) == m_failedControllers.end())
+ cachedController = LoadController(addon);
+
+ if (cachedController)
+ controllers.emplace_back(cachedController);
+ }
+ }
+
+ return controllers;
+}
+
+void CControllerManager::OnEvent(const ADDON::AddonEvent& event)
+{
+ if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) || // Also called on install
+ typeid(event) == typeid(ADDON::AddonEvents::ReInstalled))
+ {
+ std::lock_guard<CCriticalSection> lock(m_mutex);
+
+ const std::string& addonId = event.addonId;
+
+ // Clear caches for add-on
+ auto it = m_cache.find(addonId);
+ if (it != m_cache.end())
+ m_cache.erase(it);
+
+ auto it2 = m_failedControllers.find(addonId);
+ if (it2 != m_failedControllers.end())
+ m_failedControllers.erase(it2);
+ }
+}
+
+ControllerPtr CControllerManager::LoadController(const ADDON::AddonPtr& addon)
+{
+ ControllerPtr controller = std::static_pointer_cast<CController>(addon);
+ if (!controller->LoadLayout())
+ {
+ m_failedControllers.insert(addon->ID());
+ controller.reset();
+ }
+
+ return controller;
+}
diff --git a/xbmc/games/controllers/ControllerManager.h b/xbmc/games/controllers/ControllerManager.h
new file mode 100644
index 0000000..25ffe0f
--- /dev/null
+++ b/xbmc/games/controllers/ControllerManager.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017-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 "ControllerTypes.h"
+#include "addons/IAddon.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <set>
+#include <string>
+
+namespace ADDON
+{
+struct AddonEvent;
+} // namespace ADDON
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerManager
+{
+public:
+ CControllerManager(ADDON::CAddonMgr& addonManager);
+ ~CControllerManager();
+
+ /*!
+ * \brief Get a controller
+ *
+ * A cache is used to avoid reloading controllers each time they are
+ * requested.
+ *
+ * \param controllerId The controller's ID
+ *
+ * \return The controller, or empty if the controller isn't installed or
+ * can't be loaded
+ */
+ ControllerPtr GetController(const std::string& controllerId);
+
+ /*!
+ * \brief Get the default controller
+ *
+ * \return The default controller, or empty if the controller failed to load
+ */
+ ControllerPtr GetDefaultController();
+
+ /*!
+ * \brief Get the default keyboard
+ *
+ * \return The keyboard controller, or empty if the controller failed to load
+ */
+ ControllerPtr GetDefaultKeyboard();
+
+ /*!
+ * \brief Get the default mouse
+ *
+ * \return The mouse controller, or empty if the controller failed to load
+ */
+ ControllerPtr GetDefaultMouse();
+
+ /*!
+ * \brief Get installed controllers
+ *
+ * \return The installed controllers that loaded successfully
+ */
+ ControllerVector GetControllers();
+
+private:
+ // Add-on event handler
+ void OnEvent(const ADDON::AddonEvent& event);
+
+ // Utility functions
+ ControllerPtr LoadController(const ADDON::AddonPtr& addon);
+
+ // Construction parameters
+ ADDON::CAddonMgr& m_addonManager;
+
+ // Controller state
+ std::map<std::string, ControllerPtr> m_cache;
+ std::set<std::string> m_failedControllers; // Controllers that failed to load
+
+ // Synchronization parameters
+ CCriticalSection m_mutex;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/ControllerTranslator.cpp b/xbmc/games/controllers/ControllerTranslator.cpp
new file mode 100644
index 0000000..df8641b
--- /dev/null
+++ b/xbmc/games/controllers/ControllerTranslator.cpp
@@ -0,0 +1,744 @@
+/*
+ * 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 "ControllerTranslator.h"
+
+#include "ControllerDefinitions.h"
+
+using namespace KODI;
+using namespace GAME;
+using namespace JOYSTICK;
+
+const char* CControllerTranslator::TranslateFeatureType(FEATURE_TYPE type)
+{
+ switch (type)
+ {
+ case FEATURE_TYPE::SCALAR:
+ return LAYOUT_XML_ELM_BUTTON;
+ case FEATURE_TYPE::ANALOG_STICK:
+ return LAYOUT_XML_ELM_ANALOG_STICK;
+ case FEATURE_TYPE::ACCELEROMETER:
+ return LAYOUT_XML_ELM_ACCELEROMETER;
+ case FEATURE_TYPE::MOTOR:
+ return LAYOUT_XML_ELM_MOTOR;
+ case FEATURE_TYPE::RELPOINTER:
+ return LAYOUT_XML_ELM_RELPOINTER;
+ case FEATURE_TYPE::ABSPOINTER:
+ return LAYOUT_XML_ELM_ABSPOINTER;
+ case FEATURE_TYPE::WHEEL:
+ return LAYOUT_XML_ELM_WHEEL;
+ case FEATURE_TYPE::THROTTLE:
+ return LAYOUT_XML_ELM_THROTTLE;
+ case FEATURE_TYPE::KEY:
+ return LAYOUT_XML_ELM_KEY;
+ default:
+ break;
+ }
+ return "";
+}
+
+FEATURE_TYPE CControllerTranslator::TranslateFeatureType(const std::string& strType)
+{
+ if (strType == LAYOUT_XML_ELM_BUTTON)
+ return FEATURE_TYPE::SCALAR;
+ if (strType == LAYOUT_XML_ELM_ANALOG_STICK)
+ return FEATURE_TYPE::ANALOG_STICK;
+ if (strType == LAYOUT_XML_ELM_ACCELEROMETER)
+ return FEATURE_TYPE::ACCELEROMETER;
+ if (strType == LAYOUT_XML_ELM_MOTOR)
+ return FEATURE_TYPE::MOTOR;
+ if (strType == LAYOUT_XML_ELM_RELPOINTER)
+ return FEATURE_TYPE::RELPOINTER;
+ if (strType == LAYOUT_XML_ELM_ABSPOINTER)
+ return FEATURE_TYPE::ABSPOINTER;
+ if (strType == LAYOUT_XML_ELM_WHEEL)
+ return FEATURE_TYPE::WHEEL;
+ if (strType == LAYOUT_XML_ELM_THROTTLE)
+ return FEATURE_TYPE::THROTTLE;
+ if (strType == LAYOUT_XML_ELM_KEY)
+ return FEATURE_TYPE::KEY;
+
+ return FEATURE_TYPE::UNKNOWN;
+}
+
+const char* CControllerTranslator::TranslateFeatureCategory(FEATURE_CATEGORY category)
+{
+ switch (category)
+ {
+ case FEATURE_CATEGORY::FACE:
+ return FEATURE_CATEGORY_FACE;
+ case FEATURE_CATEGORY::SHOULDER:
+ return FEATURE_CATEGORY_SHOULDER;
+ case FEATURE_CATEGORY::TRIGGER:
+ return FEATURE_CATEGORY_TRIGGER;
+ case FEATURE_CATEGORY::ANALOG_STICK:
+ return FEATURE_CATEGORY_ANALOG_STICK;
+ case FEATURE_CATEGORY::ACCELEROMETER:
+ return FEATURE_CATEGORY_ACCELEROMETER;
+ case FEATURE_CATEGORY::HAPTICS:
+ return FEATURE_CATEGORY_HAPTICS;
+ case FEATURE_CATEGORY::MOUSE_BUTTON:
+ return FEATURE_CATEGORY_MOUSE_BUTTON;
+ case FEATURE_CATEGORY::POINTER:
+ return FEATURE_CATEGORY_POINTER;
+ case FEATURE_CATEGORY::LIGHTGUN:
+ return FEATURE_CATEGORY_LIGHTGUN;
+ case FEATURE_CATEGORY::OFFSCREEN:
+ return FEATURE_CATEGORY_OFFSCREEN;
+ case FEATURE_CATEGORY::KEY:
+ return FEATURE_CATEGORY_KEY;
+ case FEATURE_CATEGORY::KEYPAD:
+ return FEATURE_CATEGORY_KEYPAD;
+ case FEATURE_CATEGORY::HARDWARE:
+ return FEATURE_CATEGORY_HARDWARE;
+ case FEATURE_CATEGORY::WHEEL:
+ return FEATURE_CATEGORY_WHEEL;
+ case FEATURE_CATEGORY::JOYSTICK:
+ return FEATURE_CATEGORY_JOYSTICK;
+ case FEATURE_CATEGORY::PADDLE:
+ return FEATURE_CATEGORY_PADDLE;
+ default:
+ break;
+ }
+ return "";
+}
+
+FEATURE_CATEGORY CControllerTranslator::TranslateFeatureCategory(const std::string& strCategory)
+{
+ if (strCategory == FEATURE_CATEGORY_FACE)
+ return FEATURE_CATEGORY::FACE;
+ if (strCategory == FEATURE_CATEGORY_SHOULDER)
+ return FEATURE_CATEGORY::SHOULDER;
+ if (strCategory == FEATURE_CATEGORY_TRIGGER)
+ return FEATURE_CATEGORY::TRIGGER;
+ if (strCategory == FEATURE_CATEGORY_ANALOG_STICK)
+ return FEATURE_CATEGORY::ANALOG_STICK;
+ if (strCategory == FEATURE_CATEGORY_ACCELEROMETER)
+ return FEATURE_CATEGORY::ACCELEROMETER;
+ if (strCategory == FEATURE_CATEGORY_HAPTICS)
+ return FEATURE_CATEGORY::HAPTICS;
+ if (strCategory == FEATURE_CATEGORY_MOUSE_BUTTON)
+ return FEATURE_CATEGORY::MOUSE_BUTTON;
+ if (strCategory == FEATURE_CATEGORY_POINTER)
+ return FEATURE_CATEGORY::POINTER;
+ if (strCategory == FEATURE_CATEGORY_LIGHTGUN)
+ return FEATURE_CATEGORY::LIGHTGUN;
+ if (strCategory == FEATURE_CATEGORY_OFFSCREEN)
+ return FEATURE_CATEGORY::OFFSCREEN;
+ if (strCategory == FEATURE_CATEGORY_KEY)
+ return FEATURE_CATEGORY::KEY;
+ if (strCategory == FEATURE_CATEGORY_KEYPAD)
+ return FEATURE_CATEGORY::KEYPAD;
+ if (strCategory == FEATURE_CATEGORY_HARDWARE)
+ return FEATURE_CATEGORY::HARDWARE;
+ if (strCategory == FEATURE_CATEGORY_WHEEL)
+ return FEATURE_CATEGORY::WHEEL;
+ if (strCategory == FEATURE_CATEGORY_JOYSTICK)
+ return FEATURE_CATEGORY::JOYSTICK;
+ if (strCategory == FEATURE_CATEGORY_PADDLE)
+ return FEATURE_CATEGORY::PADDLE;
+
+ return FEATURE_CATEGORY::UNKNOWN;
+}
+
+const char* CControllerTranslator::TranslateInputType(INPUT_TYPE type)
+{
+ switch (type)
+ {
+ case INPUT_TYPE::DIGITAL:
+ return "digital";
+ case INPUT_TYPE::ANALOG:
+ return "analog";
+ default:
+ break;
+ }
+ return "";
+}
+
+INPUT_TYPE CControllerTranslator::TranslateInputType(const std::string& strType)
+{
+ if (strType == "digital")
+ return INPUT_TYPE::DIGITAL;
+ if (strType == "analog")
+ return INPUT_TYPE::ANALOG;
+
+ return INPUT_TYPE::UNKNOWN;
+}
+
+KEYBOARD::KeySymbol CControllerTranslator::TranslateKeysym(const std::string& symbol)
+{
+ if (symbol == "backspace")
+ return XBMCK_BACKSPACE;
+ if (symbol == "tab")
+ return XBMCK_TAB;
+ if (symbol == "clear")
+ return XBMCK_CLEAR;
+ if (symbol == "enter")
+ return XBMCK_RETURN;
+ if (symbol == "pause")
+ return XBMCK_PAUSE;
+ if (symbol == "escape")
+ return XBMCK_ESCAPE;
+ if (symbol == "space")
+ return XBMCK_SPACE;
+ if (symbol == "exclaim")
+ return XBMCK_EXCLAIM;
+ if (symbol == "doublequote")
+ return XBMCK_QUOTEDBL;
+ if (symbol == "hash")
+ return XBMCK_HASH;
+ if (symbol == "dollar")
+ return XBMCK_DOLLAR;
+ if (symbol == "ampersand")
+ return XBMCK_AMPERSAND;
+ if (symbol == "quote")
+ return XBMCK_QUOTE;
+ if (symbol == "leftparen")
+ return XBMCK_LEFTPAREN;
+ if (symbol == "rightparen")
+ return XBMCK_RIGHTPAREN;
+ if (symbol == "asterisk")
+ return XBMCK_ASTERISK;
+ if (symbol == "plus")
+ return XBMCK_PLUS;
+ if (symbol == "comma")
+ return XBMCK_COMMA;
+ if (symbol == "minus")
+ return XBMCK_MINUS;
+ if (symbol == "period")
+ return XBMCK_PERIOD;
+ if (symbol == "slash")
+ return XBMCK_SLASH;
+ if (symbol == "0")
+ return XBMCK_0;
+ if (symbol == "1")
+ return XBMCK_1;
+ if (symbol == "2")
+ return XBMCK_2;
+ if (symbol == "3")
+ return XBMCK_3;
+ if (symbol == "4")
+ return XBMCK_4;
+ if (symbol == "5")
+ return XBMCK_5;
+ if (symbol == "6")
+ return XBMCK_6;
+ if (symbol == "7")
+ return XBMCK_7;
+ if (symbol == "8")
+ return XBMCK_8;
+ if (symbol == "9")
+ return XBMCK_9;
+ if (symbol == "colon")
+ return XBMCK_COLON;
+ if (symbol == "semicolon")
+ return XBMCK_SEMICOLON;
+ if (symbol == "less")
+ return XBMCK_LESS;
+ if (symbol == "equals")
+ return XBMCK_EQUALS;
+ if (symbol == "greater")
+ return XBMCK_GREATER;
+ if (symbol == "question")
+ return XBMCK_QUESTION;
+ if (symbol == "at")
+ return XBMCK_AT;
+ if (symbol == "leftbracket")
+ return XBMCK_LEFTBRACKET;
+ if (symbol == "backslash")
+ return XBMCK_BACKSLASH;
+ if (symbol == "rightbracket")
+ return XBMCK_RIGHTBRACKET;
+ if (symbol == "caret")
+ return XBMCK_CARET;
+ if (symbol == "underscore")
+ return XBMCK_UNDERSCORE;
+ if (symbol == "grave")
+ return XBMCK_BACKQUOTE;
+ if (symbol == "a")
+ return XBMCK_a;
+ if (symbol == "b")
+ return XBMCK_b;
+ if (symbol == "c")
+ return XBMCK_c;
+ if (symbol == "d")
+ return XBMCK_d;
+ if (symbol == "e")
+ return XBMCK_e;
+ if (symbol == "f")
+ return XBMCK_f;
+ if (symbol == "g")
+ return XBMCK_g;
+ if (symbol == "h")
+ return XBMCK_h;
+ if (symbol == "i")
+ return XBMCK_i;
+ if (symbol == "j")
+ return XBMCK_j;
+ if (symbol == "k")
+ return XBMCK_k;
+ if (symbol == "l")
+ return XBMCK_l;
+ if (symbol == "m")
+ return XBMCK_m;
+ if (symbol == "n")
+ return XBMCK_n;
+ if (symbol == "o")
+ return XBMCK_o;
+ if (symbol == "p")
+ return XBMCK_p;
+ if (symbol == "q")
+ return XBMCK_q;
+ if (symbol == "r")
+ return XBMCK_r;
+ if (symbol == "s")
+ return XBMCK_s;
+ if (symbol == "t")
+ return XBMCK_t;
+ if (symbol == "u")
+ return XBMCK_u;
+ if (symbol == "v")
+ return XBMCK_v;
+ if (symbol == "w")
+ return XBMCK_w;
+ if (symbol == "x")
+ return XBMCK_x;
+ if (symbol == "y")
+ return XBMCK_y;
+ if (symbol == "z")
+ return XBMCK_z;
+ if (symbol == "leftbrace")
+ return XBMCK_LEFTBRACE;
+ if (symbol == "bar")
+ return XBMCK_PIPE;
+ if (symbol == "rightbrace")
+ return XBMCK_RIGHTBRACE;
+ if (symbol == "tilde")
+ return XBMCK_TILDE;
+ if (symbol == "delete")
+ return XBMCK_DELETE;
+ if (symbol == "kp0")
+ return XBMCK_KP0;
+ if (symbol == "kp1")
+ return XBMCK_KP1;
+ if (symbol == "kp2")
+ return XBMCK_KP2;
+ if (symbol == "kp3")
+ return XBMCK_KP3;
+ if (symbol == "kp4")
+ return XBMCK_KP4;
+ if (symbol == "kp5")
+ return XBMCK_KP5;
+ if (symbol == "kp6")
+ return XBMCK_KP6;
+ if (symbol == "kp7")
+ return XBMCK_KP7;
+ if (symbol == "kp8")
+ return XBMCK_KP8;
+ if (symbol == "kp9")
+ return XBMCK_KP9;
+ if (symbol == "kpperiod")
+ return XBMCK_KP_PERIOD;
+ if (symbol == "kpdivide")
+ return XBMCK_KP_DIVIDE;
+ if (symbol == "kpmultiply")
+ return XBMCK_KP_MULTIPLY;
+ if (symbol == "kpminus")
+ return XBMCK_KP_MINUS;
+ if (symbol == "kpplus")
+ return XBMCK_KP_PLUS;
+ if (symbol == "kpenter")
+ return XBMCK_KP_ENTER;
+ if (symbol == "kpequals")
+ return XBMCK_KP_EQUALS;
+ if (symbol == "up")
+ return XBMCK_UP;
+ if (symbol == "down")
+ return XBMCK_DOWN;
+ if (symbol == "right")
+ return XBMCK_RIGHT;
+ if (symbol == "left")
+ return XBMCK_LEFT;
+ if (symbol == "insert")
+ return XBMCK_INSERT;
+ if (symbol == "home")
+ return XBMCK_HOME;
+ if (symbol == "end")
+ return XBMCK_END;
+ if (symbol == "pageup")
+ return XBMCK_PAGEUP;
+ if (symbol == "pagedown")
+ return XBMCK_PAGEDOWN;
+ if (symbol == "f1")
+ return XBMCK_F1;
+ if (symbol == "f2")
+ return XBMCK_F2;
+ if (symbol == "f3")
+ return XBMCK_F3;
+ if (symbol == "f4")
+ return XBMCK_F4;
+ if (symbol == "f5")
+ return XBMCK_F5;
+ if (symbol == "f6")
+ return XBMCK_F6;
+ if (symbol == "f7")
+ return XBMCK_F7;
+ if (symbol == "f8")
+ return XBMCK_F8;
+ if (symbol == "f9")
+ return XBMCK_F9;
+ if (symbol == "f10")
+ return XBMCK_F10;
+ if (symbol == "f11")
+ return XBMCK_F11;
+ if (symbol == "f12")
+ return XBMCK_F12;
+ if (symbol == "f13")
+ return XBMCK_F13;
+ if (symbol == "f14")
+ return XBMCK_F14;
+ if (symbol == "f15")
+ return XBMCK_F15;
+ if (symbol == "numlock")
+ return XBMCK_NUMLOCK;
+ if (symbol == "capslock")
+ return XBMCK_CAPSLOCK;
+ if (symbol == "scrolllock")
+ return XBMCK_SCROLLOCK;
+ if (symbol == "leftshift")
+ return XBMCK_LSHIFT;
+ if (symbol == "rightshift")
+ return XBMCK_RSHIFT;
+ if (symbol == "leftctrl")
+ return XBMCK_LCTRL;
+ if (symbol == "rightctrl")
+ return XBMCK_RCTRL;
+ if (symbol == "leftalt")
+ return XBMCK_LALT;
+ if (symbol == "rightalt")
+ return XBMCK_RALT;
+ if (symbol == "leftmeta")
+ return XBMCK_LMETA;
+ if (symbol == "rightmeta")
+ return XBMCK_RMETA;
+ if (symbol == "leftsuper")
+ return XBMCK_LSUPER;
+ if (symbol == "rightsuper")
+ return XBMCK_RSUPER;
+ if (symbol == "mode")
+ return XBMCK_MODE;
+ if (symbol == "compose")
+ return XBMCK_COMPOSE;
+ if (symbol == "help")
+ return XBMCK_HELP;
+ if (symbol == "printscreen")
+ return XBMCK_PRINT;
+ if (symbol == "sysreq")
+ return XBMCK_SYSREQ;
+ if (symbol == "break")
+ return XBMCK_BREAK;
+ if (symbol == "menu")
+ return XBMCK_MENU;
+ if (symbol == "power")
+ return XBMCK_POWER;
+ if (symbol == "euro")
+ return XBMCK_EURO;
+ if (symbol == "undo")
+ return XBMCK_UNDO;
+
+ return XBMCK_UNKNOWN;
+}
+
+const char* CControllerTranslator::TranslateKeycode(KEYBOARD::KeySymbol keycode)
+{
+ switch (keycode)
+ {
+ case XBMCK_BACKSPACE:
+ return "backspace";
+ case XBMCK_TAB:
+ return "tab";
+ case XBMCK_CLEAR:
+ return "clear";
+ case XBMCK_RETURN:
+ return "enter";
+ case XBMCK_PAUSE:
+ return "pause";
+ case XBMCK_ESCAPE:
+ return "escape";
+ case XBMCK_SPACE:
+ return "space";
+ case XBMCK_EXCLAIM:
+ return "exclaim";
+ case XBMCK_QUOTEDBL:
+ return "doublequote";
+ case XBMCK_HASH:
+ return "hash";
+ case XBMCK_DOLLAR:
+ return "dollar";
+ case XBMCK_AMPERSAND:
+ return "ampersand";
+ case XBMCK_QUOTE:
+ return "quote";
+ case XBMCK_LEFTPAREN:
+ return "leftparen";
+ case XBMCK_RIGHTPAREN:
+ return "rightparen";
+ case XBMCK_ASTERISK:
+ return "asterisk";
+ case XBMCK_PLUS:
+ return "plus";
+ case XBMCK_COMMA:
+ return "comma";
+ case XBMCK_MINUS:
+ return "minus";
+ case XBMCK_PERIOD:
+ return "period";
+ case XBMCK_SLASH:
+ return "slash";
+ case XBMCK_0:
+ return "0";
+ case XBMCK_1:
+ return "1";
+ case XBMCK_2:
+ return "2";
+ case XBMCK_3:
+ return "3";
+ case XBMCK_4:
+ return "4";
+ case XBMCK_5:
+ return "5";
+ case XBMCK_6:
+ return "6";
+ case XBMCK_7:
+ return "7";
+ case XBMCK_8:
+ return "8";
+ case XBMCK_9:
+ return "9";
+ case XBMCK_COLON:
+ return "colon";
+ case XBMCK_SEMICOLON:
+ return "semicolon";
+ case XBMCK_LESS:
+ return "less";
+ case XBMCK_EQUALS:
+ return "equals";
+ case XBMCK_GREATER:
+ return "greater";
+ case XBMCK_QUESTION:
+ return "question";
+ case XBMCK_AT:
+ return "at";
+ case XBMCK_LEFTBRACKET:
+ return "leftbracket";
+ case XBMCK_BACKSLASH:
+ return "backslash";
+ case XBMCK_RIGHTBRACKET:
+ return "rightbracket";
+ case XBMCK_CARET:
+ return "caret";
+ case XBMCK_UNDERSCORE:
+ return "underscore";
+ case XBMCK_BACKQUOTE:
+ return "grave";
+ case XBMCK_a:
+ return "a";
+ case XBMCK_b:
+ return "b";
+ case XBMCK_c:
+ return "c";
+ case XBMCK_d:
+ return "d";
+ case XBMCK_e:
+ return "e";
+ case XBMCK_f:
+ return "f";
+ case XBMCK_g:
+ return "g";
+ case XBMCK_h:
+ return "h";
+ case XBMCK_i:
+ return "i";
+ case XBMCK_j:
+ return "j";
+ case XBMCK_k:
+ return "k";
+ case XBMCK_l:
+ return "l";
+ case XBMCK_m:
+ return "m";
+ case XBMCK_n:
+ return "n";
+ case XBMCK_o:
+ return "o";
+ case XBMCK_p:
+ return "p";
+ case XBMCK_q:
+ return "q";
+ case XBMCK_r:
+ return "r";
+ case XBMCK_s:
+ return "s";
+ case XBMCK_t:
+ return "t";
+ case XBMCK_u:
+ return "u";
+ case XBMCK_v:
+ return "v";
+ case XBMCK_w:
+ return "w";
+ case XBMCK_x:
+ return "x";
+ case XBMCK_y:
+ return "y";
+ case XBMCK_z:
+ return "z";
+ case XBMCK_LEFTBRACE:
+ return "leftbrace";
+ case XBMCK_PIPE:
+ return "bar";
+ case XBMCK_RIGHTBRACE:
+ return "rightbrace";
+ case XBMCK_TILDE:
+ return "tilde";
+ case XBMCK_DELETE:
+ return "delete";
+ case XBMCK_KP0:
+ return "kp0";
+ case XBMCK_KP1:
+ return "kp1";
+ case XBMCK_KP2:
+ return "kp2";
+ case XBMCK_KP3:
+ return "kp3";
+ case XBMCK_KP4:
+ return "kp4";
+ case XBMCK_KP5:
+ return "kp5";
+ case XBMCK_KP6:
+ return "kp6";
+ case XBMCK_KP7:
+ return "kp7";
+ case XBMCK_KP8:
+ return "kp8";
+ case XBMCK_KP9:
+ return "kp9";
+ case XBMCK_KP_PERIOD:
+ return "kpperiod";
+ case XBMCK_KP_DIVIDE:
+ return "kpdivide";
+ case XBMCK_KP_MULTIPLY:
+ return "kpmultiply";
+ case XBMCK_KP_MINUS:
+ return "kpminus";
+ case XBMCK_KP_PLUS:
+ return "kpplus";
+ case XBMCK_KP_ENTER:
+ return "kpenter";
+ case XBMCK_KP_EQUALS:
+ return "kpequals";
+ case XBMCK_UP:
+ return "up";
+ case XBMCK_DOWN:
+ return "down";
+ case XBMCK_RIGHT:
+ return "right";
+ case XBMCK_LEFT:
+ return "left";
+ case XBMCK_INSERT:
+ return "insert";
+ case XBMCK_HOME:
+ return "home";
+ case XBMCK_END:
+ return "end";
+ case XBMCK_PAGEUP:
+ return "pageup";
+ case XBMCK_PAGEDOWN:
+ return "pagedown";
+ case XBMCK_F1:
+ return "f1";
+ case XBMCK_F2:
+ return "f2";
+ case XBMCK_F3:
+ return "f3";
+ case XBMCK_F4:
+ return "f4";
+ case XBMCK_F5:
+ return "f5";
+ case XBMCK_F6:
+ return "f6";
+ case XBMCK_F7:
+ return "f7";
+ case XBMCK_F8:
+ return "f8";
+ case XBMCK_F9:
+ return "f9";
+ case XBMCK_F10:
+ return "f10";
+ case XBMCK_F11:
+ return "f11";
+ case XBMCK_F12:
+ return "f12";
+ case XBMCK_F13:
+ return "f13";
+ case XBMCK_F14:
+ return "f14";
+ case XBMCK_F15:
+ return "f15";
+ case XBMCK_NUMLOCK:
+ return "numlock";
+ case XBMCK_CAPSLOCK:
+ return "capslock";
+ case XBMCK_SCROLLOCK:
+ return "scrolllock";
+ case XBMCK_LSHIFT:
+ return "leftshift";
+ case XBMCK_RSHIFT:
+ return "rightshift";
+ case XBMCK_LCTRL:
+ return "leftctrl";
+ case XBMCK_RCTRL:
+ return "rightctrl";
+ case XBMCK_LALT:
+ return "leftalt";
+ case XBMCK_RALT:
+ return "rightalt";
+ case XBMCK_LMETA:
+ return "leftmeta";
+ case XBMCK_RMETA:
+ return "rightmeta";
+ case XBMCK_LSUPER:
+ return "leftsuper";
+ case XBMCK_RSUPER:
+ return "rightsuper";
+ case XBMCK_MODE:
+ return "mode";
+ case XBMCK_COMPOSE:
+ return "compose";
+ case XBMCK_HELP:
+ return "help";
+ case XBMCK_PRINT:
+ return "printscreen";
+ case XBMCK_SYSREQ:
+ return "sysreq";
+ case XBMCK_BREAK:
+ return "break";
+ case XBMCK_MENU:
+ return "menu";
+ case XBMCK_POWER:
+ return "power";
+ case XBMCK_EURO:
+ return "euro";
+ case XBMCK_UNDO:
+ return "undo";
+ default:
+ break;
+ }
+
+ return "";
+}
diff --git a/xbmc/games/controllers/ControllerTranslator.h b/xbmc/games/controllers/ControllerTranslator.h
new file mode 100644
index 0000000..0bf4bf1
--- /dev/null
+++ b/xbmc/games/controllers/ControllerTranslator.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "input/joysticks/JoystickTypes.h"
+#include "input/keyboard/KeyboardTypes.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CControllerTranslator
+{
+public:
+ static const char* TranslateFeatureType(JOYSTICK::FEATURE_TYPE type);
+ static JOYSTICK::FEATURE_TYPE TranslateFeatureType(const std::string& strType);
+
+ static const char* TranslateFeatureCategory(JOYSTICK::FEATURE_CATEGORY category);
+ static JOYSTICK::FEATURE_CATEGORY TranslateFeatureCategory(const std::string& strCategory);
+
+ static const char* TranslateInputType(JOYSTICK::INPUT_TYPE type);
+ static JOYSTICK::INPUT_TYPE TranslateInputType(const std::string& strType);
+
+ /*!
+ * \brief Translate a keyboard symbol to a Kodi key code
+ *
+ * \param symbol The key's symbol, defined in the kodi-game-controllers project
+ *
+ * \return The layout-independent keycode associated with the key
+ */
+ static KEYBOARD::KeySymbol TranslateKeysym(const std::string& symbol);
+
+ /*!
+ * \brief Translate a Kodi key code to a keyboard symbol
+ *
+ * \param keycode The Kodi key code
+ *
+ * \return The key's symbol, or an empty string if no symbol is defined for the keycode
+ */
+ static const char* TranslateKeycode(KEYBOARD::KeySymbol keycode);
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/ControllerTypes.h b/xbmc/games/controllers/ControllerTypes.h
new file mode 100644
index 0000000..838a07d
--- /dev/null
+++ b/xbmc/games/controllers/ControllerTypes.h
@@ -0,0 +1,33 @@
+/*
+ * 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 <memory>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CController;
+using ControllerPtr = std::shared_ptr<CController>;
+using ControllerVector = std::vector<ControllerPtr>;
+
+/*!
+ * \brief Type of input provided by a hardware or controller port
+ */
+enum class PORT_TYPE
+{
+ UNKNOWN,
+ KEYBOARD,
+ MOUSE,
+ CONTROLLER,
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/DefaultController.cpp b/xbmc/games/controllers/DefaultController.cpp
new file mode 100644
index 0000000..58bb464
--- /dev/null
+++ b/xbmc/games/controllers/DefaultController.cpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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 "DefaultController.h"
+
+using namespace KODI;
+using namespace GAME;
+
+const char* CDefaultController::FEATURE_A = "a";
+const char* CDefaultController::FEATURE_B = "b";
+const char* CDefaultController::FEATURE_X = "x";
+const char* CDefaultController::FEATURE_Y = "y";
+const char* CDefaultController::FEATURE_START = "start";
+const char* CDefaultController::FEATURE_BACK = "back";
+const char* CDefaultController::FEATURE_GUIDE = "guide";
+const char* CDefaultController::FEATURE_UP = "up";
+const char* CDefaultController::FEATURE_RIGHT = "right";
+const char* CDefaultController::FEATURE_DOWN = "down";
+const char* CDefaultController::FEATURE_LEFT = "left";
+const char* CDefaultController::FEATURE_LEFT_THUMB = "leftthumb";
+const char* CDefaultController::FEATURE_RIGHT_THUMB = "rightthumb";
+const char* CDefaultController::FEATURE_LEFT_BUMPER = "leftbumper";
+const char* CDefaultController::FEATURE_RIGHT_BUMPER = "rightbumper";
+const char* CDefaultController::FEATURE_LEFT_TRIGGER = "lefttrigger";
+const char* CDefaultController::FEATURE_RIGHT_TRIGGER = "righttrigger";
+const char* CDefaultController::FEATURE_LEFT_STICK = "leftstick";
+const char* CDefaultController::FEATURE_RIGHT_STICK = "rightstick";
+const char* CDefaultController::FEATURE_LEFT_MOTOR = "leftmotor";
+const char* CDefaultController::FEATURE_RIGHT_MOTOR = "rightmotor";
diff --git a/xbmc/games/controllers/DefaultController.h b/xbmc/games/controllers/DefaultController.h
new file mode 100644
index 0000000..f82ce49
--- /dev/null
+++ b/xbmc/games/controllers/DefaultController.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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
+
+namespace KODI
+{
+namespace GAME
+{
+class CDefaultController
+{
+public:
+ // Face buttons
+ static const char* FEATURE_A;
+ static const char* FEATURE_B;
+ static const char* FEATURE_X;
+ static const char* FEATURE_Y;
+ static const char* FEATURE_START;
+ static const char* FEATURE_BACK;
+ static const char* FEATURE_GUIDE;
+ static const char* FEATURE_UP;
+ static const char* FEATURE_RIGHT;
+ static const char* FEATURE_DOWN;
+ static const char* FEATURE_LEFT;
+ static const char* FEATURE_LEFT_THUMB;
+ static const char* FEATURE_RIGHT_THUMB;
+
+ // Shoulder buttons
+ static const char* FEATURE_LEFT_BUMPER;
+ static const char* FEATURE_RIGHT_BUMPER;
+
+ // Triggers
+ static const char* FEATURE_LEFT_TRIGGER;
+ static const char* FEATURE_RIGHT_TRIGGER;
+
+ // Analog sticks
+ static const char* FEATURE_LEFT_STICK;
+ static const char* FEATURE_RIGHT_STICK;
+
+ // Haptics
+ static const char* FEATURE_LEFT_MOTOR;
+ static const char* FEATURE_RIGHT_MOTOR;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/dialogs/CMakeLists.txt b/xbmc/games/controllers/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..e40e60e
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/CMakeLists.txt
@@ -0,0 +1,15 @@
+set(SOURCES ControllerInstaller.cpp
+ ControllerSelect.cpp
+ GUIDialogAxisDetection.cpp
+ GUIDialogButtonCapture.cpp
+ GUIDialogIgnoreInput.cpp
+)
+
+set(HEADERS ControllerInstaller.h
+ ControllerSelect.h
+ GUIDialogAxisDetection.h
+ GUIDialogButtonCapture.h
+ GUIDialogIgnoreInput.h
+)
+
+core_add_library(games_controller_dialogs)
diff --git a/xbmc/games/controllers/dialogs/ControllerInstaller.cpp b/xbmc/games/controllers/dialogs/ControllerInstaller.cpp
new file mode 100644
index 0000000..9b0fe6f
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/ControllerInstaller.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 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 "ControllerInstaller.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerInstaller::CControllerInstaller() : CThread("ControllerInstaller")
+{
+}
+
+void CControllerInstaller::Process()
+{
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui == nullptr)
+ return;
+
+ CGUIWindowManager& windowManager = gui->GetWindowManager();
+
+ auto pSelectDialog = windowManager.GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (pSelectDialog == nullptr)
+ return;
+
+ auto pProgressDialog = windowManager.GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (pProgressDialog == nullptr)
+ return;
+
+ ADDON::VECADDONS installableAddons;
+ CServiceBroker::GetAddonMgr().GetInstallableAddons(installableAddons,
+ ADDON::AddonType::GAME_CONTROLLER);
+ if (installableAddons.empty())
+ {
+ // "Controller profiles"
+ // "All available controller profiles are installed."
+ MESSAGING::HELPERS::ShowOKDialogText(CVariant{35050}, CVariant{35062});
+ return;
+ }
+
+ CLog::Log(LOGDEBUG, "Controller installer: Found {} controller add-ons",
+ installableAddons.size());
+
+ CFileItemList items;
+ for (const auto& addon : installableAddons)
+ {
+ CFileItemPtr item(new CFileItem(addon->Name()));
+ item->SetArt("icon", addon->Icon());
+ items.Add(std::move(item));
+ }
+
+ pSelectDialog->Reset();
+ pSelectDialog->SetHeading(39020); // "The following additional add-ons will be installed"
+ pSelectDialog->SetUseDetails(true);
+ pSelectDialog->EnableButton(true, 186); // "OK""
+ for (const auto& it : items)
+ pSelectDialog->Add(*it);
+ pSelectDialog->Open();
+
+ if (!pSelectDialog->IsButtonPressed())
+ {
+ CLog::Log(LOGDEBUG, "Controller installer: User cancelled installation dialog");
+ return;
+ }
+
+ CLog::Log(LOGDEBUG, "Controller installer: Installing {} controller add-ons",
+ installableAddons.size());
+
+ pProgressDialog->SetHeading(CVariant{24086}); // "Installing add-on..."
+ pProgressDialog->SetLine(0, CVariant{""});
+ pProgressDialog->SetLine(1, CVariant{""});
+ pProgressDialog->SetLine(2, CVariant{""});
+
+ pProgressDialog->Open();
+
+ unsigned int installedCount = 0;
+ while (installedCount < installableAddons.size())
+ {
+ const auto& addon = installableAddons[installedCount];
+
+ // Set dialog text
+ const std::string& progressTemplate = g_localizeStrings.Get(24057); // "Installing {0:s}..."
+ const std::string progressText = StringUtils::Format(progressTemplate, addon->Name());
+ pProgressDialog->SetLine(0, CVariant{progressText});
+
+ // Set dialog percentage
+ const unsigned int percentage =
+ 100 * (installedCount + 1) / static_cast<unsigned int>(installableAddons.size());
+ pProgressDialog->SetPercentage(percentage);
+
+ if (!ADDON::CAddonInstaller::GetInstance().InstallOrUpdate(
+ addon->ID(), ADDON::BackgroundJob::CHOICE_NO, ADDON::ModalJob::CHOICE_NO))
+ {
+ CLog::Log(LOGERROR, "Controller installer: Failed to install {}", addon->ID());
+ // "Error"
+ // "Failed to install add-on."
+ MESSAGING::HELPERS::ShowOKDialogText(257, 35256);
+ break;
+ }
+
+ if (pProgressDialog->IsCanceled())
+ {
+ CLog::Log(LOGDEBUG, "Controller installer: User cancelled add-on installation");
+ break;
+ }
+
+ if (windowManager.GetActiveWindowOrDialog() != WINDOW_DIALOG_PROGRESS)
+ {
+ CLog::Log(LOGDEBUG, "Controller installer: Progress dialog is hidden, cancelling");
+ break;
+ }
+
+ installedCount++;
+ }
+
+ CLog::Log(LOGDEBUG, "Controller window: Installed {} controller add-ons", installedCount);
+ pProgressDialog->Close();
+}
diff --git a/xbmc/games/controllers/dialogs/ControllerInstaller.h b/xbmc/games/controllers/dialogs/ControllerInstaller.h
new file mode 100644
index 0000000..9863e27
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/ControllerInstaller.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 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/Thread.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerInstaller : public CThread
+{
+public:
+ CControllerInstaller();
+ ~CControllerInstaller() override = default;
+
+protected:
+ // implementation of CThread
+ void Process() override;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/dialogs/ControllerSelect.cpp b/xbmc/games/controllers/dialogs/ControllerSelect.cpp
new file mode 100644
index 0000000..2ef9941
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/ControllerSelect.cpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2021 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 "ControllerSelect.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "games/controllers/ControllerTypes.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerSelect::CControllerSelect() : CThread("ControllerSelect")
+{
+}
+
+CControllerSelect::~CControllerSelect() = default;
+
+void CControllerSelect::Initialize(ControllerVector controllers,
+ ControllerPtr defaultController,
+ bool showDisconnect,
+ const std::function<void(ControllerPtr)>& callback)
+{
+ // Validate parameters
+ if (!callback)
+ return;
+
+ // Stop thread and reset state
+ Deinitialize();
+
+ // Initialize state
+ m_controllers = std::move(controllers);
+ m_defaultController = std::move(defaultController);
+ m_showDisconnect = showDisconnect;
+ m_callback = callback;
+
+ // Create thread
+ Create(false);
+}
+
+void CControllerSelect::Deinitialize()
+{
+ // Stop thread
+ StopThread(true);
+
+ // Reset state
+ m_controllers.clear();
+ m_defaultController.reset();
+ m_showDisconnect = true;
+ m_callback = nullptr;
+}
+
+void CControllerSelect::Process()
+{
+ // Select first controller by default
+ unsigned int initialSelected = 0;
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui == nullptr)
+ return;
+
+ CGUIWindowManager& windowManager = gui->GetWindowManager();
+
+ auto pSelectDialog = windowManager.GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (pSelectDialog == nullptr)
+ return;
+
+ CLog::Log(LOGDEBUG, "Controller select: Showing dialog for {} controllers", m_controllers.size());
+
+ CFileItemList items;
+ for (const ControllerPtr& controller : m_controllers)
+ {
+ CFileItemPtr item(new CFileItem(controller->Layout().Label()));
+ item->SetArt("icon", controller->Layout().ImagePath());
+ items.Add(std::move(item));
+
+ // Check if a specified controller should be selected by default
+ if (m_defaultController && m_defaultController->ID() == controller->ID())
+ initialSelected = items.Size() - 1;
+ }
+
+ if (m_showDisconnect)
+ {
+ // Add a button to disconnect the port
+ CFileItemPtr item(new CFileItem(g_localizeStrings.Get(13298))); // "Disconnected"
+ item->SetArt("icon", "DefaultAddonNone.png");
+ items.Add(std::move(item));
+
+ // Check if the disconnect button should be selected by default
+ if (!m_defaultController)
+ initialSelected = items.Size() - 1;
+ }
+
+ pSelectDialog->Reset();
+ pSelectDialog->SetHeading(CVariant{35113}); // "Select a Controller"
+ pSelectDialog->SetUseDetails(true);
+ pSelectDialog->EnableButton(false, 186); // "OK""
+ pSelectDialog->SetButtonFocus(false);
+ for (const auto& it : items)
+ pSelectDialog->Add(*it);
+ pSelectDialog->SetSelected(static_cast<int>(initialSelected));
+ pSelectDialog->Open();
+
+ // If the thread was stopped, exit early
+ if (m_bStop)
+ return;
+
+ if (pSelectDialog->IsConfirmed())
+ {
+ ControllerPtr resultController;
+
+ const int selected = pSelectDialog->GetSelectedItem();
+ if (0 <= selected && selected < static_cast<int>(m_controllers.size()))
+ resultController = m_controllers.at(selected);
+
+ // Fire a callback with the result
+ m_callback(resultController);
+ }
+}
diff --git a/xbmc/games/controllers/dialogs/ControllerSelect.h b/xbmc/games/controllers/dialogs/ControllerSelect.h
new file mode 100644
index 0000000..83a1b41
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/ControllerSelect.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 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 "games/controllers/ControllerTypes.h"
+#include "threads/Thread.h"
+
+#include <functional>
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerSelect : public CThread
+{
+public:
+ CControllerSelect();
+ ~CControllerSelect() override;
+
+ void Initialize(ControllerVector controllers,
+ ControllerPtr defaultController,
+ bool showDisconnect,
+ const std::function<void(ControllerPtr)>& callback);
+ void Deinitialize();
+
+protected:
+ // Implementation of CThread
+ void Process() override;
+
+private:
+ // State parameters
+ ControllerVector m_controllers;
+ ControllerPtr m_defaultController;
+ bool m_showDisconnect = true;
+ std::function<void(ControllerPtr)> m_callback;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp
new file mode 100644
index 0000000..2052a4c
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.cpp
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017-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 "GUIDialogAxisDetection.h"
+
+#include "guilib/LocalizeStrings.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/JoystickTranslator.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+std::string CGUIDialogAxisDetection::GetDialogText()
+{
+ // "Press all analog buttons now to detect them:[CR][CR]%s"
+ const std::string& dialogText = g_localizeStrings.Get(35020);
+
+ std::vector<std::string> primitives;
+
+ for (const auto& axisEntry : m_detectedAxes)
+ {
+ JOYSTICK::CDriverPrimitive axis(axisEntry.second, 0, JOYSTICK::SEMIAXIS_DIRECTION::POSITIVE, 1);
+ primitives.emplace_back(JOYSTICK::CJoystickTranslator::GetPrimitiveName(axis));
+ }
+
+ return StringUtils::Format(dialogText, StringUtils::Join(primitives, " | "));
+}
+
+std::string CGUIDialogAxisDetection::GetDialogHeader()
+{
+ return g_localizeStrings.Get(35058); // "Controller Configuration"
+}
+
+bool CGUIDialogAxisDetection::MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive)
+{
+ if (primitive.Type() == JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS)
+ AddAxis(buttonMap->Location(), primitive.Index());
+
+ return true;
+}
+
+bool CGUIDialogAxisDetection::AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const
+{
+ switch (type)
+ {
+ case JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS:
+ return true;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+void CGUIDialogAxisDetection::OnLateAxis(const JOYSTICK::IButtonMap* buttonMap,
+ unsigned int axisIndex)
+{
+ AddAxis(buttonMap->Location(), axisIndex);
+}
+
+void CGUIDialogAxisDetection::AddAxis(const std::string& deviceLocation, unsigned int axisIndex)
+{
+ auto it = std::find_if(m_detectedAxes.begin(), m_detectedAxes.end(),
+ [&deviceLocation, axisIndex](const AxisEntry& axis) {
+ return axis.first == deviceLocation && axis.second == axisIndex;
+ });
+
+ if (it == m_detectedAxes.end())
+ {
+ m_detectedAxes.emplace_back(std::make_pair(deviceLocation, axisIndex));
+ m_captureEvent.Set();
+ }
+}
diff --git a/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h
new file mode 100644
index 0000000..4ca0d27
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogAxisDetection.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017-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 "GUIDialogButtonCapture.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIDialogAxisDetection : public CGUIDialogButtonCapture
+{
+public:
+ CGUIDialogAxisDetection() = default;
+
+ ~CGUIDialogAxisDetection() override = default;
+
+ // specialization of IButtonMapper via CGUIDialogButtonCapture
+ bool AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const override;
+ void OnLateAxis(const JOYSTICK::IButtonMap* buttonMap, unsigned int axisIndex) override;
+
+protected:
+ // implementation of CGUIDialogButtonCapture
+ std::string GetDialogText() override;
+ std::string GetDialogHeader() override;
+ bool MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive) override;
+ void OnClose(bool bAccepted) override {}
+
+private:
+ void AddAxis(const std::string& deviceLocation, unsigned int axisIndex);
+
+ // Axis types
+ using DeviceName = std::string;
+ using AxisIndex = unsigned int;
+ using AxisEntry = std::pair<DeviceName, AxisIndex>;
+ using AxisVector = std::vector<AxisEntry>;
+
+ // Axis detection
+ AxisVector m_detectedAxes;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.cpp b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.cpp
new file mode 100644
index 0000000..9bf8dbc
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2016-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 "GUIDialogButtonCapture.h"
+
+#include "ServiceBroker.h"
+#include "games/controllers/ControllerIDs.h"
+#include "input/IKeymap.h"
+#include "input/actions/ActionIDs.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "peripherals/Peripherals.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace KODI;
+using namespace GAME;
+using namespace KODI::MESSAGING;
+
+CGUIDialogButtonCapture::CGUIDialogButtonCapture() : CThread("ButtonCaptureDlg")
+{
+}
+
+std::string CGUIDialogButtonCapture::ControllerID(void) const
+{
+ return DEFAULT_CONTROLLER_ID;
+}
+
+void CGUIDialogButtonCapture::Show()
+{
+ if (!IsRunning())
+ {
+ InstallHooks();
+
+ Create();
+
+ bool bAccepted =
+ HELPERS::ShowOKDialogText(CVariant{GetDialogHeader()}, CVariant{GetDialogText()});
+
+ StopThread(false);
+
+ m_captureEvent.Set();
+
+ OnClose(bAccepted);
+
+ RemoveHooks();
+ }
+}
+
+void CGUIDialogButtonCapture::Process()
+{
+ while (!m_bStop)
+ {
+ m_captureEvent.Wait();
+
+ if (m_bStop)
+ break;
+
+ //! @todo Move to rendering thread when there is a rendering thread
+ HELPERS::UpdateOKDialogText(CVariant{35013}, CVariant{GetDialogText()});
+ }
+}
+
+bool CGUIDialogButtonCapture::MapPrimitive(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive)
+{
+ if (m_bStop)
+ return false;
+
+ // First check to see if driver primitive closes the dialog
+ if (keymap && keymap->ControllerID() == buttonMap->ControllerID())
+ {
+ std::string feature;
+ if (buttonMap->GetFeature(primitive, feature))
+ {
+ const auto& actions =
+ keymap->GetActions(JOYSTICK::CJoystickUtils::MakeKeyName(feature)).actions;
+ if (!actions.empty())
+ {
+ switch (actions.begin()->actionId)
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_NAV_BACK:
+ case ACTION_PREVIOUS_MENU:
+ return false;
+ default:
+ break;
+ }
+ }
+ }
+ }
+
+ return MapPrimitiveInternal(buttonMap, keymap, primitive);
+}
+
+void CGUIDialogButtonCapture::InstallHooks(void)
+{
+ CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this);
+ CServiceBroker::GetPeripherals().RegisterObserver(this);
+}
+
+void CGUIDialogButtonCapture::RemoveHooks(void)
+{
+ CServiceBroker::GetPeripherals().UnregisterObserver(this);
+ CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this);
+}
+
+void CGUIDialogButtonCapture::Notify(const Observable& obs, const ObservableMessage msg)
+{
+ switch (msg)
+ {
+ case ObservableMessagePeripheralsChanged:
+ {
+ CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this);
+ CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this);
+ break;
+ }
+ default:
+ break;
+ }
+}
diff --git a/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h
new file mode 100644
index 0000000..97795c6
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogButtonCapture.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016-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 "input/joysticks/interfaces/IButtonMapper.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "utils/Observer.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIDialogButtonCapture : public JOYSTICK::IButtonMapper, public Observer, protected CThread
+{
+public:
+ CGUIDialogButtonCapture();
+
+ ~CGUIDialogButtonCapture() override = default;
+
+ // implementation of IButtonMapper
+ std::string ControllerID() const override;
+ bool NeedsCooldown() const override { return false; }
+ bool MapPrimitive(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive) override;
+ void OnEventFrame(const JOYSTICK::IButtonMap* buttonMap, bool bMotion) override {}
+ void OnLateAxis(const JOYSTICK::IButtonMap* buttonMap, unsigned int axisIndex) override {}
+
+ // implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override;
+
+ /*!
+ * \brief Show the dialog
+ */
+ void Show();
+
+protected:
+ // implementation of CThread
+ void Process() override;
+
+ virtual std::string GetDialogText() = 0;
+ virtual std::string GetDialogHeader() = 0;
+ virtual bool MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive) = 0;
+ virtual void OnClose(bool bAccepted) = 0;
+
+ CEvent m_captureEvent;
+
+private:
+ void InstallHooks();
+ void RemoveHooks();
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp
new file mode 100644
index 0000000..b5f28d6
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.cpp
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2017-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 "GUIDialogIgnoreInput.h"
+
+#include "guilib/LocalizeStrings.h"
+#include "input/joysticks/JoystickTranslator.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IButtonMapCallback.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace KODI;
+using namespace GAME;
+
+bool CGUIDialogIgnoreInput::AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const
+{
+ switch (type)
+ {
+ case JOYSTICK::PRIMITIVE_TYPE::BUTTON:
+ case JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS:
+ return true;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+std::string CGUIDialogIgnoreInput::GetDialogText()
+{
+ // "Some controllers have buttons and axes that interfere with mapping. Press
+ // these now to disable them:[CR]%s"
+ const std::string& dialogText = g_localizeStrings.Get(35014);
+
+ std::vector<std::string> primitives;
+
+ std::transform(m_capturedPrimitives.begin(), m_capturedPrimitives.end(),
+ std::back_inserter(primitives), [](const JOYSTICK::CDriverPrimitive& primitive) {
+ return JOYSTICK::CJoystickTranslator::GetPrimitiveName(primitive);
+ });
+
+ return StringUtils::Format(dialogText, StringUtils::Join(primitives, " | "));
+}
+
+std::string CGUIDialogIgnoreInput::GetDialogHeader()
+{
+
+ return g_localizeStrings.Get(35019); // "Ignore input"
+}
+
+bool CGUIDialogIgnoreInput::MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive)
+{
+ // Check if we have already started capturing primitives for a device
+ const bool bHasDevice = !m_location.empty();
+
+ // If a primitive comes from a different device, ignore it
+ if (bHasDevice && m_location != buttonMap->Location())
+ {
+ CLog::Log(LOGDEBUG, "{}: ignoring input from device {}", buttonMap->ControllerID(),
+ buttonMap->Location());
+ return false;
+ }
+
+ if (!bHasDevice)
+ {
+ CLog::Log(LOGDEBUG, "{}: capturing input for device {}", buttonMap->ControllerID(),
+ buttonMap->Location());
+ m_location = buttonMap->Location();
+ }
+
+ if (AddPrimitive(primitive))
+ {
+ buttonMap->SetIgnoredPrimitives(m_capturedPrimitives);
+ m_captureEvent.Set();
+ }
+
+ return true;
+}
+
+void CGUIDialogIgnoreInput::OnClose(bool bAccepted)
+{
+ for (auto& callback : ButtonMapCallbacks())
+ {
+ if (bAccepted)
+ {
+ // See documentation of IButtonMapCallback::ResetIgnoredPrimitives()
+ // for why this call is needed
+ if (m_location.empty())
+ callback.second->ResetIgnoredPrimitives();
+
+ if (m_location.empty() || m_location == callback.first)
+ callback.second->SaveButtonMap();
+ }
+ else
+ callback.second->RevertButtonMap();
+ }
+}
+
+bool CGUIDialogIgnoreInput::AddPrimitive(const JOYSTICK::CDriverPrimitive& primitive)
+{
+ bool bValid = false;
+
+ if (primitive.Type() == JOYSTICK::PRIMITIVE_TYPE::BUTTON ||
+ primitive.Type() == JOYSTICK::PRIMITIVE_TYPE::SEMIAXIS)
+ {
+ auto PrimitiveMatch = [&primitive](const JOYSTICK::CDriverPrimitive& other) {
+ return primitive.Type() == other.Type() && primitive.Index() == other.Index();
+ };
+
+ bValid = std::find_if(m_capturedPrimitives.begin(), m_capturedPrimitives.end(),
+ PrimitiveMatch) == m_capturedPrimitives.end();
+ }
+
+ if (bValid)
+ {
+ m_capturedPrimitives.emplace_back(primitive);
+ return true;
+ }
+
+ return false;
+}
diff --git a/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h
new file mode 100644
index 0000000..e1d3922
--- /dev/null
+++ b/xbmc/games/controllers/dialogs/GUIDialogIgnoreInput.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017-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 "GUIDialogButtonCapture.h"
+#include "input/joysticks/DriverPrimitive.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIDialogIgnoreInput : public CGUIDialogButtonCapture
+{
+public:
+ CGUIDialogIgnoreInput() = default;
+
+ ~CGUIDialogIgnoreInput() override = default;
+
+ // specialization of IButtonMapper via CGUIDialogButtonCapture
+ bool AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const override;
+
+protected:
+ // implementation of CGUIDialogButtonCapture
+ std::string GetDialogText() override;
+ std::string GetDialogHeader() override;
+ bool MapPrimitiveInternal(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive) override;
+ void OnClose(bool bAccepted) override;
+
+private:
+ bool AddPrimitive(const JOYSTICK::CDriverPrimitive& primitive);
+
+ std::string m_location;
+ std::vector<JOYSTICK::CDriverPrimitive> m_capturedPrimitives;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/CMakeLists.txt b/xbmc/games/controllers/guicontrols/CMakeLists.txt
new file mode 100644
index 0000000..e6babcc
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/CMakeLists.txt
@@ -0,0 +1,28 @@
+set(SOURCES GUICardinalFeatureButton.cpp
+ GUIControllerButton.cpp
+ GUIFeatureButton.cpp
+ GUIFeatureControls.cpp
+ GUIFeatureFactory.cpp
+ GUIFeatureTranslator.cpp
+ GUIGameController.cpp
+ GUIScalarFeatureButton.cpp
+ GUISelectKeyButton.cpp
+ GUIThrottleButton.cpp
+ GUIWheelButton.cpp
+)
+
+set(HEADERS GUICardinalFeatureButton.h
+ GUIControllerButton.h
+ GUIControlTypes.h
+ GUIFeatureButton.h
+ GUIFeatureControls.h
+ GUIFeatureFactory.h
+ GUIFeatureTranslator.h
+ GUIGameController.h
+ GUIScalarFeatureButton.h
+ GUISelectKeyButton.h
+ GUIThrottleButton.h
+ GUIWheelButton.h
+)
+
+core_add_library(games_controller_guicontrols)
diff --git a/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp
new file mode 100644
index 0000000..3c8732c
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.cpp
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016-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 "GUICardinalFeatureButton.h"
+
+#include "guilib/LocalizeStrings.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUICardinalFeatureButton::CGUICardinalFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+ : CGUIFeatureButton(buttonTemplate, wizard, feature, index)
+{
+ Reset();
+}
+
+bool CGUICardinalFeatureButton::PromptForInput(CEvent& waitEvent)
+{
+ using namespace JOYSTICK;
+
+ bool bInterrupted = false;
+
+ // Get the prompt for the current analog stick direction
+ std::string strPrompt;
+ std::string strWarn;
+ switch (m_state)
+ {
+ case STATE::CARDINAL_DIRECTION_UP:
+ strPrompt = g_localizeStrings.Get(35092); // "Move %s up"
+ strWarn = g_localizeStrings.Get(35093); // "Move %s up (%d)"
+ break;
+ case STATE::CARDINAL_DIRECTION_RIGHT:
+ strPrompt = g_localizeStrings.Get(35096); // "Move %s right"
+ strWarn = g_localizeStrings.Get(35097); // "Move %s right (%d)"
+ break;
+ case STATE::CARDINAL_DIRECTION_DOWN:
+ strPrompt = g_localizeStrings.Get(35094); // "Move %s down"
+ strWarn = g_localizeStrings.Get(35095); // "Move %s down (%d)"
+ break;
+ case STATE::CARDINAL_DIRECTION_LEFT:
+ strPrompt = g_localizeStrings.Get(35098); // "Move %s left"
+ strWarn = g_localizeStrings.Get(35099); // "Move %s left (%d)"
+ break;
+ default:
+ break;
+ }
+
+ if (!strPrompt.empty())
+ {
+ bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent);
+
+ if (!bInterrupted)
+ m_state = STATE::FINISHED; // Not interrupted, must have timed out
+ else
+ m_state = GetNextState(m_state); // Interrupted by input, proceed
+ }
+
+ return bInterrupted;
+}
+
+bool CGUICardinalFeatureButton::IsFinished(void) const
+{
+ return m_state >= STATE::FINISHED;
+}
+
+KODI::INPUT::CARDINAL_DIRECTION CGUICardinalFeatureButton::GetCardinalDirection(void) const
+{
+ using namespace INPUT;
+
+ switch (m_state)
+ {
+ case STATE::CARDINAL_DIRECTION_UP:
+ return CARDINAL_DIRECTION::UP;
+ case STATE::CARDINAL_DIRECTION_RIGHT:
+ return CARDINAL_DIRECTION::RIGHT;
+ case STATE::CARDINAL_DIRECTION_DOWN:
+ return CARDINAL_DIRECTION::DOWN;
+ case STATE::CARDINAL_DIRECTION_LEFT:
+ return CARDINAL_DIRECTION::LEFT;
+ default:
+ break;
+ }
+
+ return CARDINAL_DIRECTION::NONE;
+}
+
+void CGUICardinalFeatureButton::Reset(void)
+{
+ m_state = STATE::CARDINAL_DIRECTION_UP;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h
new file mode 100644
index 0000000..48eb0e8
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUICardinalFeatureButton.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureButton.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUICardinalFeatureButton : public CGUIFeatureButton
+{
+public:
+ CGUICardinalFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+
+ ~CGUICardinalFeatureButton() override = default;
+
+ // implementation of IFeatureButton
+ bool PromptForInput(CEvent& waitEvent) override;
+ bool IsFinished() const override;
+ INPUT::CARDINAL_DIRECTION GetCardinalDirection() const override;
+ void Reset() override;
+
+private:
+ enum class STATE
+ {
+ CARDINAL_DIRECTION_UP,
+ CARDINAL_DIRECTION_RIGHT,
+ CARDINAL_DIRECTION_DOWN,
+ CARDINAL_DIRECTION_LEFT,
+ FINISHED,
+ };
+
+ STATE m_state;
+};
+
+using CGUIAnalogStickButton = CGUICardinalFeatureButton;
+using CGUIRelativePointerButton = CGUICardinalFeatureButton;
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIControlTypes.h b/xbmc/games/controllers/guicontrols/GUIControlTypes.h
new file mode 100644
index 0000000..bf91b6e
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIControlTypes.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017-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
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief Types of button controls that can populate the feature list
+ */
+enum class BUTTON_TYPE
+{
+ UNKNOWN,
+ BUTTON,
+ ANALOG_STICK,
+ RELATIVE_POINTER,
+ WHEEL,
+ THROTTLE,
+ SELECT_KEY,
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIControllerButton.cpp b/xbmc/games/controllers/guicontrols/GUIControllerButton.cpp
new file mode 100644
index 0000000..c8156ce
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIControllerButton.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014-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 "GUIControllerButton.h"
+
+#include "games/controllers/windows/GUIControllerDefines.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIControllerButton::CGUIControllerButton(const CGUIButtonControl& buttonControl,
+ const std::string& label,
+ unsigned int index)
+ : CGUIButtonControl(buttonControl)
+{
+ // Initialize CGUIButtonControl
+ SetLabel(label);
+ SetID(CONTROL_CONTROLLER_BUTTONS_START + index);
+ SetVisible(true);
+ AllocResources();
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIControllerButton.h b/xbmc/games/controllers/guicontrols/GUIControllerButton.h
new file mode 100644
index 0000000..ebdc924
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIControllerButton.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016-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/GUIButtonControl.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIControllerButton : public CGUIButtonControl
+{
+public:
+ CGUIControllerButton(const CGUIButtonControl& buttonControl,
+ const std::string& label,
+ unsigned int index);
+
+ ~CGUIControllerButton() override = default;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp
new file mode 100644
index 0000000..0f1bfd6
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureButton.cpp
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureButton.h"
+
+#include "ServiceBroker.h"
+#include "games/controllers/windows/GUIControllerDefines.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "threads/Event.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI;
+using namespace GAME;
+using namespace std::chrono_literals;
+
+CGUIFeatureButton::CGUIFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+ : CGUIButtonControl(buttonTemplate), m_feature(feature), m_wizard(wizard)
+{
+ // Initialize CGUIButtonControl
+ SetLabel(m_feature.Label());
+ SetID(CONTROL_FEATURE_BUTTONS_START + index);
+ SetVisible(true);
+ AllocResources();
+}
+
+void CGUIFeatureButton::OnUnFocus(void)
+{
+ CGUIButtonControl::OnUnFocus();
+ m_wizard->OnUnfocus(this);
+}
+
+bool CGUIFeatureButton::DoPrompt(const std::string& strPrompt,
+ const std::string& strWarn,
+ const std::string& strFeature,
+ CEvent& waitEvent)
+{
+ bool bInterrupted = false;
+
+ if (!HasFocus())
+ {
+ CGUIMessage msgFocus(GUI_MSG_SETFOCUS, GetID(), GetID());
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msgFocus, WINDOW_INVALID, false);
+ }
+
+ CGUIMessage msgLabel(GUI_MSG_LABEL_SET, GetID(), GetID());
+
+ for (unsigned int i = 0; i < COUNTDOWN_DURATION_SEC; i++)
+ {
+ const unsigned int secondsElapsed = i;
+ const unsigned int secondsRemaining = COUNTDOWN_DURATION_SEC - i;
+
+ const bool bWarn = secondsElapsed >= WAIT_TO_WARN_SEC;
+
+ std::string strLabel;
+
+ if (bWarn)
+ strLabel = StringUtils::Format(strWarn, strFeature, secondsRemaining);
+ else
+ strLabel = StringUtils::Format(strPrompt, strFeature, secondsRemaining);
+
+ msgLabel.SetLabel(strLabel);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msgLabel, WINDOW_INVALID, false);
+
+ waitEvent.Reset();
+ bInterrupted = waitEvent.Wait(1000ms); // Wait 1 second
+
+ if (bInterrupted)
+ break;
+ }
+
+ // Reset label
+ msgLabel.SetLabel(m_feature.Label());
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msgLabel, WINDOW_INVALID, false);
+
+ return bInterrupted;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureButton.h b/xbmc/games/controllers/guicontrols/GUIFeatureButton.h
new file mode 100644
index 0000000..b69f521
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureButton.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016-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 "games/controllers/input/PhysicalFeature.h"
+#include "games/controllers/windows/IConfigurationWindow.h"
+#include "guilib/GUIButtonControl.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIFeatureButton : public CGUIButtonControl, public IFeatureButton
+{
+public:
+ CGUIFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+
+ ~CGUIFeatureButton() override = default;
+
+ // implementation of CGUIControl via CGUIButtonControl
+ void OnUnFocus() override;
+
+ // partial implementation of IFeatureButton
+ const CPhysicalFeature& Feature() const override { return m_feature; }
+ INPUT::CARDINAL_DIRECTION GetCardinalDirection() const override
+ {
+ return INPUT::CARDINAL_DIRECTION::NONE;
+ }
+ JOYSTICK::WHEEL_DIRECTION GetWheelDirection() const override
+ {
+ return JOYSTICK::WHEEL_DIRECTION::NONE;
+ }
+ JOYSTICK::THROTTLE_DIRECTION GetThrottleDirection() const override
+ {
+ return JOYSTICK::THROTTLE_DIRECTION::NONE;
+ }
+
+protected:
+ bool DoPrompt(const std::string& strPrompt,
+ const std::string& strWarn,
+ const std::string& strFeature,
+ CEvent& waitEvent);
+
+ // FSM helper
+ template<typename T>
+ T GetNextState(T state)
+ {
+ return static_cast<T>(static_cast<int>(state) + 1);
+ }
+
+ const CPhysicalFeature m_feature;
+
+private:
+ IConfigurationWizard* const m_wizard;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp
new file mode 100644
index 0000000..63d4757
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureControls.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureControls.h"
+
+#include "games/controllers/windows/GUIControllerDefines.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIFeatureGroupTitle::CGUIFeatureGroupTitle(const CGUILabelControl& groupTitleTemplate,
+ const std::string& groupName,
+ unsigned int buttonIndex)
+ : CGUILabelControl(groupTitleTemplate)
+{
+ // Initialize CGUILabelControl
+ SetLabel(groupName);
+ SetID(CONTROL_FEATURE_GROUPS_START + buttonIndex);
+ SetVisible(true);
+ AllocResources();
+}
+
+CGUIFeatureSeparator::CGUIFeatureSeparator(const CGUIImage& separatorTemplate,
+ unsigned int buttonIndex)
+ : CGUIImage(separatorTemplate)
+{
+ // Initialize CGUIImage
+ SetID(CONTROL_FEATURE_SEPARATORS_START + buttonIndex);
+ SetVisible(true);
+ AllocResources();
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureControls.h b/xbmc/games/controllers/guicontrols/GUIFeatureControls.h
new file mode 100644
index 0000000..c72a11a
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureControls.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016-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/GUIImage.h"
+#include "guilib/GUILabelControl.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIFeatureGroupTitle : public CGUILabelControl
+{
+public:
+ CGUIFeatureGroupTitle(const CGUILabelControl& groupTitleTemplate,
+ const std::string& groupName,
+ unsigned int buttonIndex);
+
+ ~CGUIFeatureGroupTitle() override = default;
+};
+
+class CGUIFeatureSeparator : public CGUIImage
+{
+public:
+ CGUIFeatureSeparator(const CGUIImage& separatorTemplate, unsigned int buttonIndex);
+
+ ~CGUIFeatureSeparator() override = default;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp
new file mode 100644
index 0000000..2cce033
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.cpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017-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 "GUIFeatureFactory.h"
+
+#include "GUICardinalFeatureButton.h"
+#include "GUIScalarFeatureButton.h"
+#include "GUISelectKeyButton.h"
+#include "GUIThrottleButton.h"
+#include "GUIWheelButton.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIButtonControl* CGUIFeatureFactory::CreateButton(BUTTON_TYPE type,
+ const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+{
+ switch (type)
+ {
+ case BUTTON_TYPE::BUTTON:
+ return new CGUIScalarFeatureButton(buttonTemplate, wizard, feature, index);
+
+ case BUTTON_TYPE::ANALOG_STICK:
+ return new CGUIAnalogStickButton(buttonTemplate, wizard, feature, index);
+
+ case BUTTON_TYPE::WHEEL:
+ return new CGUIWheelButton(buttonTemplate, wizard, feature, index);
+
+ case BUTTON_TYPE::THROTTLE:
+ return new CGUIThrottleButton(buttonTemplate, wizard, feature, index);
+
+ case BUTTON_TYPE::SELECT_KEY:
+ return new CGUISelectKeyButton(buttonTemplate, wizard, index);
+
+ case BUTTON_TYPE::RELATIVE_POINTER:
+ return new CGUIRelativePointerButton(buttonTemplate, wizard, feature, index);
+
+ default:
+ break;
+ }
+
+ return nullptr;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureFactory.h b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.h
new file mode 100644
index 0000000..e35070d
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureFactory.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017-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 "GUIControlTypes.h"
+
+class CGUIButtonControl;
+
+namespace KODI
+{
+namespace GAME
+{
+class CPhysicalFeature;
+class IConfigurationWizard;
+
+class CGUIFeatureFactory
+{
+public:
+ /*!
+ * \brief Create a button of the specified type
+ * \param type The type of button control being created
+ * \return A button control, or nullptr if type is invalid
+ */
+ static CGUIButtonControl* CreateButton(BUTTON_TYPE type,
+ const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp
new file mode 100644
index 0000000..ab941c5
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017-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 "GUIFeatureTranslator.h"
+
+using namespace KODI;
+using namespace GAME;
+
+BUTTON_TYPE CGUIFeatureTranslator::GetButtonType(JOYSTICK::FEATURE_TYPE featureType)
+{
+ switch (featureType)
+ {
+ case JOYSTICK::FEATURE_TYPE::SCALAR:
+ case JOYSTICK::FEATURE_TYPE::KEY:
+ return BUTTON_TYPE::BUTTON;
+
+ case JOYSTICK::FEATURE_TYPE::ANALOG_STICK:
+ return BUTTON_TYPE::ANALOG_STICK;
+
+ case JOYSTICK::FEATURE_TYPE::RELPOINTER:
+ return BUTTON_TYPE::RELATIVE_POINTER;
+
+ case JOYSTICK::FEATURE_TYPE::WHEEL:
+ return BUTTON_TYPE::WHEEL;
+
+ case JOYSTICK::FEATURE_TYPE::THROTTLE:
+ return BUTTON_TYPE::THROTTLE;
+
+ default:
+ break;
+ }
+
+ return BUTTON_TYPE::UNKNOWN;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h
new file mode 100644
index 0000000..8e28d16
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIFeatureTranslator.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017-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 "GUIControlTypes.h"
+#include "input/joysticks/JoystickTypes.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIFeatureTranslator
+{
+public:
+ /*!
+ * \brief Get the type of button control used to map the feature
+ */
+ static BUTTON_TYPE GetButtonType(JOYSTICK::FEATURE_TYPE featureType);
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIGameController.cpp b/xbmc/games/controllers/guicontrols/GUIGameController.cpp
new file mode 100644
index 0000000..8990cf2
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIGameController.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014-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 "GUIGameController.h"
+
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIGameController::CGUIGameController(
+ int parentID, int controlID, float posX, float posY, float width, float height)
+ : CGUIImage(parentID, controlID, posX, posY, width, height, CTextureInfo())
+{
+ // Initialize CGUIControl
+ ControlType = GUICONTROL_GAMECONTROLLER;
+}
+
+CGUIGameController::CGUIGameController(const CGUIGameController& from) : CGUIImage(from)
+{
+ // Initialize CGUIControl
+ ControlType = GUICONTROL_GAMECONTROLLER;
+}
+
+CGUIGameController* CGUIGameController::Clone(void) const
+{
+ return new CGUIGameController(*this);
+}
+
+void CGUIGameController::Render(void)
+{
+ CGUIImage::Render();
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (m_currentController)
+ {
+ //! @todo Render pressed buttons
+ }
+}
+
+void CGUIGameController::ActivateController(const ControllerPtr& controller)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (controller && controller != m_currentController)
+ {
+ m_currentController = controller;
+
+ lock.unlock();
+
+ //! @todo Sometimes this fails on window init
+ SetFileName(m_currentController->Layout().ImagePath());
+ }
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIGameController.h b/xbmc/games/controllers/guicontrols/GUIGameController.h
new file mode 100644
index 0000000..d62819a
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIGameController.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014-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 "games/controllers/ControllerTypes.h"
+#include "guilib/GUIImage.h"
+#include "threads/CriticalSection.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIGameController : public CGUIImage
+{
+public:
+ CGUIGameController(
+ int parentID, int controlID, float posX, float posY, float width, float height);
+ CGUIGameController(const CGUIGameController& from);
+
+ ~CGUIGameController() override = default;
+
+ // implementation of CGUIControl via CGUIImage
+ CGUIGameController* Clone() const override;
+ void Render() override;
+
+ void ActivateController(const ControllerPtr& controller);
+
+private:
+ ControllerPtr m_currentController;
+ CCriticalSection m_mutex;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.cpp b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.cpp
new file mode 100644
index 0000000..5358aea
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016-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 "GUIScalarFeatureButton.h"
+
+#include "guilib/LocalizeStrings.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIScalarFeatureButton::CGUIScalarFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+ : CGUIFeatureButton(buttonTemplate, wizard, feature, index)
+{
+ Reset();
+}
+
+bool CGUIScalarFeatureButton::PromptForInput(CEvent& waitEvent)
+{
+ bool bInterrupted = false;
+
+ switch (m_state)
+ {
+ case STATE::NEED_INPUT:
+ {
+ const std::string& strPrompt = g_localizeStrings.Get(35090); // "Press %s"
+ const std::string& strWarn = g_localizeStrings.Get(35091); // "Press %s (%d)"
+
+ bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent);
+
+ m_state = GetNextState(m_state);
+
+ break;
+ }
+ default:
+ break;
+ }
+
+ return bInterrupted;
+}
+
+bool CGUIScalarFeatureButton::IsFinished(void) const
+{
+ return m_state >= STATE::FINISHED;
+}
+
+void CGUIScalarFeatureButton::Reset(void)
+{
+ m_state = STATE::NEED_INPUT;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h
new file mode 100644
index 0000000..6505684
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIScalarFeatureButton.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureButton.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIScalarFeatureButton : public CGUIFeatureButton
+{
+public:
+ CGUIScalarFeatureButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+
+ ~CGUIScalarFeatureButton() override = default;
+
+ // implementation of IFeatureButton
+ bool PromptForInput(CEvent& waitEvent) override;
+ bool IsFinished() const override;
+ void Reset() override;
+
+private:
+ enum class STATE
+ {
+ NEED_INPUT,
+ FINISHED,
+ };
+
+ STATE m_state;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp
new file mode 100644
index 0000000..ea4a656
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.cpp
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017-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 "GUISelectKeyButton.h"
+
+#include "guilib/LocalizeStrings.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUISelectKeyButton::CGUISelectKeyButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ unsigned int index)
+ : CGUIFeatureButton(buttonTemplate, wizard, GetFeature(), index)
+{
+}
+
+const CPhysicalFeature& CGUISelectKeyButton::Feature(void) const
+{
+ if (m_state == STATE::NEED_INPUT)
+ return m_selectedKey;
+
+ return CGUIFeatureButton::Feature();
+}
+
+bool CGUISelectKeyButton::PromptForInput(CEvent& waitEvent)
+{
+ bool bInterrupted = false;
+
+ switch (m_state)
+ {
+ case STATE::NEED_KEY:
+ {
+ const std::string& strPrompt = g_localizeStrings.Get(35169); // "Press a key"
+ const std::string& strWarn = g_localizeStrings.Get(35170); // "Press a key ({1:d})"
+
+ bInterrupted = DoPrompt(strPrompt, strWarn, "", waitEvent);
+
+ m_state = GetNextState(m_state);
+
+ break;
+ }
+ case STATE::NEED_INPUT:
+ {
+ const std::string& strPrompt = g_localizeStrings.Get(35090); // "Press {0:s}"
+ const std::string& strWarn = g_localizeStrings.Get(35091); // "Press {0:s} ({1:d})"
+
+ bInterrupted = DoPrompt(strPrompt, strWarn, m_selectedKey.Label(), waitEvent);
+
+ m_state = GetNextState(m_state);
+
+ break;
+ }
+ default:
+ break;
+ }
+
+ return bInterrupted;
+}
+
+bool CGUISelectKeyButton::IsFinished(void) const
+{
+ return m_state >= STATE::FINISHED;
+}
+
+void CGUISelectKeyButton::SetKey(const CPhysicalFeature& key)
+{
+ m_selectedKey = key;
+}
+
+void CGUISelectKeyButton::Reset(void)
+{
+ m_state = STATE::NEED_KEY;
+ m_selectedKey.Reset();
+}
+
+CPhysicalFeature CGUISelectKeyButton::GetFeature()
+{
+ return CPhysicalFeature(35168); // "Select key"
+}
diff --git a/xbmc/games/controllers/guicontrols/GUISelectKeyButton.h b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.h
new file mode 100644
index 0000000..e96f78b
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUISelectKeyButton.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017-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 "GUIFeatureButton.h"
+#include "games/controllers/input/PhysicalFeature.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUISelectKeyButton : public CGUIFeatureButton
+{
+public:
+ CGUISelectKeyButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ unsigned int index);
+
+ ~CGUISelectKeyButton() override = default;
+
+ // implementation of IFeatureButton
+ const CPhysicalFeature& Feature(void) const override;
+ bool AllowWizard() const override { return false; }
+ bool PromptForInput(CEvent& waitEvent) override;
+ bool IsFinished() const override;
+ bool NeedsKey() const override { return m_state == STATE::NEED_KEY; }
+ void SetKey(const CPhysicalFeature& key) override;
+ void Reset() override;
+
+private:
+ static CPhysicalFeature GetFeature();
+
+ enum class STATE
+ {
+ NEED_KEY,
+ NEED_INPUT,
+ FINISHED,
+ };
+
+ STATE m_state = STATE::NEED_KEY;
+
+ CPhysicalFeature m_selectedKey;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp b/xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp
new file mode 100644
index 0000000..69f32a1
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIThrottleButton.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016-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 "GUIThrottleButton.h"
+
+#include "guilib/LocalizeStrings.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIThrottleButton::CGUIThrottleButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+ : CGUIFeatureButton(buttonTemplate, wizard, feature, index)
+{
+ Reset();
+}
+
+bool CGUIThrottleButton::PromptForInput(CEvent& waitEvent)
+{
+ using namespace JOYSTICK;
+
+ bool bInterrupted = false;
+
+ // Get the prompt for the current analog stick direction
+ std::string strPrompt;
+ std::string strWarn;
+ switch (m_state)
+ {
+ case STATE::THROTTLE_UP:
+ strPrompt = g_localizeStrings.Get(35092); // "Move %s up"
+ strWarn = g_localizeStrings.Get(35093); // "Move %s up (%d)"
+ break;
+ case STATE::THROTTLE_DOWN:
+ strPrompt = g_localizeStrings.Get(35094); // "Move %s down"
+ strWarn = g_localizeStrings.Get(35095); // "Move %s down (%d)"
+ break;
+ default:
+ break;
+ }
+
+ if (!strPrompt.empty())
+ {
+ bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent);
+
+ if (!bInterrupted)
+ m_state = STATE::FINISHED; // Not interrupted, must have timed out
+ else
+ m_state = GetNextState(m_state); // Interrupted by input, proceed
+ }
+
+ return bInterrupted;
+}
+
+bool CGUIThrottleButton::IsFinished(void) const
+{
+ return m_state >= STATE::FINISHED;
+}
+
+JOYSTICK::THROTTLE_DIRECTION CGUIThrottleButton::GetThrottleDirection(void) const
+{
+ using namespace JOYSTICK;
+
+ switch (m_state)
+ {
+ case STATE::THROTTLE_UP:
+ return THROTTLE_DIRECTION::UP;
+ case STATE::THROTTLE_DOWN:
+ return THROTTLE_DIRECTION::DOWN;
+ default:
+ break;
+ }
+
+ return THROTTLE_DIRECTION::NONE;
+}
+
+void CGUIThrottleButton::Reset(void)
+{
+ m_state = STATE::THROTTLE_UP;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIThrottleButton.h b/xbmc/games/controllers/guicontrols/GUIThrottleButton.h
new file mode 100644
index 0000000..72b7ee3
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIThrottleButton.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureButton.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIThrottleButton : public CGUIFeatureButton
+{
+public:
+ CGUIThrottleButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+
+ ~CGUIThrottleButton() override = default;
+
+ // implementation of IFeatureButton
+ bool PromptForInput(CEvent& waitEvent) override;
+ bool IsFinished() const override;
+ JOYSTICK::THROTTLE_DIRECTION GetThrottleDirection() const override;
+ void Reset() override;
+
+private:
+ enum class STATE
+ {
+ THROTTLE_UP,
+ THROTTLE_DOWN,
+ FINISHED,
+ };
+
+ STATE m_state;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/guicontrols/GUIWheelButton.cpp b/xbmc/games/controllers/guicontrols/GUIWheelButton.cpp
new file mode 100644
index 0000000..63c3819
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIWheelButton.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016-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 "GUIWheelButton.h"
+
+#include "guilib/LocalizeStrings.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIWheelButton::CGUIWheelButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index)
+ : CGUIFeatureButton(buttonTemplate, wizard, feature, index)
+{
+ Reset();
+}
+
+bool CGUIWheelButton::PromptForInput(CEvent& waitEvent)
+{
+ using namespace JOYSTICK;
+
+ bool bInterrupted = false;
+
+ // Get the prompt for the current analog stick direction
+ std::string strPrompt;
+ std::string strWarn;
+ switch (m_state)
+ {
+ case STATE::WHEEL_LEFT:
+ strPrompt = g_localizeStrings.Get(35098); // "Move %s left"
+ strWarn = g_localizeStrings.Get(35099); // "Move %s left (%d)"
+ break;
+ case STATE::WHEEL_RIGHT:
+ strPrompt = g_localizeStrings.Get(35096); // "Move %s right"
+ strWarn = g_localizeStrings.Get(35097); // "Move %s right (%d)"
+ break;
+ default:
+ break;
+ }
+
+ if (!strPrompt.empty())
+ {
+ bInterrupted = DoPrompt(strPrompt, strWarn, m_feature.Label(), waitEvent);
+
+ if (!bInterrupted)
+ m_state = STATE::FINISHED; // Not interrupted, must have timed out
+ else
+ m_state = GetNextState(m_state); // Interrupted by input, proceed
+ }
+
+ return bInterrupted;
+}
+
+bool CGUIWheelButton::IsFinished(void) const
+{
+ return m_state >= STATE::FINISHED;
+}
+
+JOYSTICK::WHEEL_DIRECTION CGUIWheelButton::GetWheelDirection(void) const
+{
+ using namespace JOYSTICK;
+
+ switch (m_state)
+ {
+ case STATE::WHEEL_LEFT:
+ return WHEEL_DIRECTION::LEFT;
+ case STATE::WHEEL_RIGHT:
+ return WHEEL_DIRECTION::RIGHT;
+ default:
+ break;
+ }
+
+ return WHEEL_DIRECTION::NONE;
+}
+
+void CGUIWheelButton::Reset(void)
+{
+ m_state = STATE::WHEEL_LEFT;
+}
diff --git a/xbmc/games/controllers/guicontrols/GUIWheelButton.h b/xbmc/games/controllers/guicontrols/GUIWheelButton.h
new file mode 100644
index 0000000..937703a
--- /dev/null
+++ b/xbmc/games/controllers/guicontrols/GUIWheelButton.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016-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 "GUIFeatureButton.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIWheelButton : public CGUIFeatureButton
+{
+public:
+ CGUIWheelButton(const CGUIButtonControl& buttonTemplate,
+ IConfigurationWizard* wizard,
+ const CPhysicalFeature& feature,
+ unsigned int index);
+
+ ~CGUIWheelButton() override = default;
+
+ // implementation of IFeatureButton
+ bool PromptForInput(CEvent& waitEvent) override;
+ bool IsFinished() const override;
+ JOYSTICK::WHEEL_DIRECTION GetWheelDirection() const override;
+ void Reset() override;
+
+private:
+ enum class STATE
+ {
+ WHEEL_LEFT,
+ WHEEL_RIGHT,
+ FINISHED,
+ };
+
+ STATE m_state;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/input/CMakeLists.txt b/xbmc/games/controllers/input/CMakeLists.txt
new file mode 100644
index 0000000..511fa37
--- /dev/null
+++ b/xbmc/games/controllers/input/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES InputSink.cpp
+ PhysicalFeature.cpp
+ PhysicalTopology.cpp
+)
+
+set(HEADERS InputSink.h
+ PhysicalFeature.h
+ PhysicalTopology.h
+)
+
+core_add_library(games_controller_input)
diff --git a/xbmc/games/controllers/input/InputSink.cpp b/xbmc/games/controllers/input/InputSink.cpp
new file mode 100644
index 0000000..a48b4b9
--- /dev/null
+++ b/xbmc/games/controllers/input/InputSink.cpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017-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 "InputSink.h"
+
+#include "games/controllers/ControllerIDs.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CInputSink::CInputSink(JOYSTICK::IInputHandler* gameInput) : m_gameInput(gameInput)
+{
+}
+
+std::string CInputSink::ControllerID(void) const
+{
+ return DEFAULT_CONTROLLER_ID;
+}
+
+bool CInputSink::AcceptsInput(const std::string& feature) const
+{
+ return m_gameInput->AcceptsInput(feature);
+}
+
+bool CInputSink::OnButtonPress(const std::string& feature, bool bPressed)
+{
+ return true;
+}
+
+bool CInputSink::OnButtonMotion(const std::string& feature,
+ float magnitude,
+ unsigned int motionTimeMs)
+{
+ return true;
+}
+
+bool CInputSink::OnAnalogStickMotion(const std::string& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs)
+{
+ return true;
+}
+
+bool CInputSink::OnAccelerometerMotion(const std::string& feature, float x, float y, float z)
+{
+ return true;
+}
+
+bool CInputSink::OnWheelMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ return true;
+}
+
+bool CInputSink::OnThrottleMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ return true;
+}
diff --git a/xbmc/games/controllers/input/InputSink.h b/xbmc/games/controllers/input/InputSink.h
new file mode 100644
index 0000000..5dc331f
--- /dev/null
+++ b/xbmc/games/controllers/input/InputSink.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017-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 "input/joysticks/interfaces/IInputHandler.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameClient;
+
+class CInputSink : public JOYSTICK::IInputHandler
+{
+public:
+ explicit CInputSink(JOYSTICK::IInputHandler* gameInput);
+
+ ~CInputSink() override = default;
+
+ // Implementation of IInputHandler
+ std::string ControllerID() const override;
+ bool HasFeature(const std::string& feature) const override { return true; }
+ bool AcceptsInput(const std::string& feature) const override;
+ bool OnButtonPress(const std::string& feature, bool bPressed) override;
+ void OnButtonHold(const std::string& feature, unsigned int holdTimeMs) override {}
+ bool OnButtonMotion(const std::string& feature,
+ float magnitude,
+ unsigned int motionTimeMs) override;
+ bool OnAnalogStickMotion(const std::string& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs) override;
+ bool OnAccelerometerMotion(const std::string& feature, float x, float y, float z) override;
+ bool OnWheelMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ bool OnThrottleMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ void OnInputFrame() override {}
+
+private:
+ // Construction parameters
+ JOYSTICK::IInputHandler* m_gameInput;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/input/PhysicalFeature.cpp b/xbmc/games/controllers/input/PhysicalFeature.cpp
new file mode 100644
index 0000000..24e0c41
--- /dev/null
+++ b/xbmc/games/controllers/input/PhysicalFeature.cpp
@@ -0,0 +1,162 @@
+/*
+ * 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 "PhysicalFeature.h"
+
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerDefinitions.h"
+#include "games/controllers/ControllerTranslator.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <sstream>
+
+using namespace KODI;
+using namespace GAME;
+using namespace JOYSTICK;
+
+CPhysicalFeature::CPhysicalFeature(int labelId)
+{
+ Reset();
+ m_labelId = labelId;
+}
+
+void CPhysicalFeature::Reset(void)
+{
+ *this = CPhysicalFeature();
+}
+
+CPhysicalFeature& CPhysicalFeature::operator=(const CPhysicalFeature& rhs)
+{
+ if (this != &rhs)
+ {
+ m_controller = rhs.m_controller;
+ m_type = rhs.m_type;
+ m_category = rhs.m_category;
+ m_categoryLabelId = rhs.m_categoryLabelId;
+ m_strName = rhs.m_strName;
+ m_labelId = rhs.m_labelId;
+ m_inputType = rhs.m_inputType;
+ m_keycode = rhs.m_keycode;
+ }
+ return *this;
+}
+
+std::string CPhysicalFeature::CategoryLabel() const
+{
+ std::string categoryLabel;
+
+ if (m_categoryLabelId >= 0 && m_controller != nullptr)
+ categoryLabel = g_localizeStrings.GetAddonString(m_controller->ID(), m_categoryLabelId);
+
+ if (categoryLabel.empty())
+ categoryLabel = g_localizeStrings.Get(m_categoryLabelId);
+
+ return categoryLabel;
+}
+
+std::string CPhysicalFeature::Label() const
+{
+ std::string label;
+
+ if (m_labelId >= 0 && m_controller != nullptr)
+ label = g_localizeStrings.GetAddonString(m_controller->ID(), m_labelId);
+
+ if (label.empty())
+ label = g_localizeStrings.Get(m_labelId);
+
+ return label;
+}
+
+bool CPhysicalFeature::Deserialize(const TiXmlElement* pElement,
+ const CController* controller,
+ FEATURE_CATEGORY category,
+ int categoryLabelId)
+{
+ Reset();
+
+ if (!pElement)
+ return false;
+
+ std::string strType(pElement->Value());
+
+ // Type
+ m_type = CControllerTranslator::TranslateFeatureType(strType);
+ if (m_type == FEATURE_TYPE::UNKNOWN)
+ {
+ CLog::Log(LOGDEBUG, "Invalid feature: <{}> ", pElement->Value());
+ return false;
+ }
+
+ // Cagegory was obtained from parent XML node
+ m_category = category;
+ m_categoryLabelId = categoryLabelId;
+
+ // Name
+ m_strName = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_FEATURE_NAME);
+ if (m_strName.empty())
+ {
+ CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_FEATURE_NAME);
+ return false;
+ }
+
+ // Label ID
+ std::string strLabel = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_FEATURE_LABEL);
+ if (strLabel.empty())
+ CLog::Log(LOGDEBUG, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_FEATURE_LABEL);
+ else
+ std::istringstream(strLabel) >> m_labelId;
+
+ // Input type
+ if (m_type == FEATURE_TYPE::SCALAR)
+ {
+ std::string strInputType = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_INPUT_TYPE);
+ if (strInputType.empty())
+ {
+ CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_INPUT_TYPE);
+ return false;
+ }
+ else
+ {
+ m_inputType = CControllerTranslator::TranslateInputType(strInputType);
+ if (m_inputType == INPUT_TYPE::UNKNOWN)
+ {
+ CLog::Log(LOGERROR, "<{}> tag - attribute \"{}\" is invalid: \"{}\"", strType,
+ LAYOUT_XML_ATTR_INPUT_TYPE, strInputType);
+ return false;
+ }
+ }
+ }
+
+ // Keycode
+ if (m_type == FEATURE_TYPE::KEY)
+ {
+ std::string strSymbol = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_KEY_SYMBOL);
+ if (strSymbol.empty())
+ {
+ CLog::Log(LOGERROR, "<{}> tag has no \"{}\" attribute", strType, LAYOUT_XML_ATTR_KEY_SYMBOL);
+ return false;
+ }
+ else
+ {
+ m_keycode = CControllerTranslator::TranslateKeysym(strSymbol);
+ if (m_keycode == XBMCK_UNKNOWN)
+ {
+ CLog::Log(LOGERROR, "<{}> tag - attribute \"{}\" is invalid: \"{}\"", strType,
+ LAYOUT_XML_ATTR_KEY_SYMBOL, strSymbol);
+ return false;
+ }
+ }
+ }
+
+ // Save controller for string translation
+ m_controller = controller;
+
+ return true;
+}
diff --git a/xbmc/games/controllers/input/PhysicalFeature.h b/xbmc/games/controllers/input/PhysicalFeature.h
new file mode 100644
index 0000000..6319a63
--- /dev/null
+++ b/xbmc/games/controllers/input/PhysicalFeature.h
@@ -0,0 +1,65 @@
+/*
+ * 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 "games/controllers/ControllerTypes.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "input/keyboard/KeyboardTypes.h"
+
+#include <string>
+
+class TiXmlElement;
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CPhysicalFeature
+{
+public:
+ CPhysicalFeature() = default;
+ CPhysicalFeature(int labelId);
+ CPhysicalFeature(const CPhysicalFeature& other) { *this = other; }
+
+ void Reset(void);
+
+ CPhysicalFeature& operator=(const CPhysicalFeature& rhs);
+
+ JOYSTICK::FEATURE_TYPE Type(void) const { return m_type; }
+ JOYSTICK::FEATURE_CATEGORY Category(void) const { return m_category; }
+ const std::string& Name(void) const { return m_strName; }
+
+ // GUI properties
+ std::string Label(void) const;
+ int LabelID(void) const { return m_labelId; }
+ std::string CategoryLabel(void) const;
+
+ // Input properties
+ JOYSTICK::INPUT_TYPE InputType(void) const { return m_inputType; }
+ KEYBOARD::KeySymbol Keycode() const { return m_keycode; }
+
+ bool Deserialize(const TiXmlElement* pElement,
+ const CController* controller,
+ JOYSTICK::FEATURE_CATEGORY category,
+ int categoryLabelId);
+
+private:
+ const CController* m_controller = nullptr; // Used for translating addon-specific labels
+ JOYSTICK::FEATURE_TYPE m_type = JOYSTICK::FEATURE_TYPE::UNKNOWN;
+ JOYSTICK::FEATURE_CATEGORY m_category = JOYSTICK::FEATURE_CATEGORY::UNKNOWN;
+ int m_categoryLabelId = -1;
+ std::string m_strName;
+ int m_labelId = -1;
+ JOYSTICK::INPUT_TYPE m_inputType = JOYSTICK::INPUT_TYPE::UNKNOWN;
+ KEYBOARD::KeySymbol m_keycode = XBMCK_UNKNOWN;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/input/PhysicalTopology.cpp b/xbmc/games/controllers/input/PhysicalTopology.cpp
new file mode 100644
index 0000000..2fecb5a
--- /dev/null
+++ b/xbmc/games/controllers/input/PhysicalTopology.cpp
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017-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 "PhysicalTopology.h"
+
+#include "games/controllers/ControllerDefinitions.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CPhysicalTopology::CPhysicalTopology(bool bProvidesInput, std::vector<CPhysicalPort> ports)
+ : m_bProvidesInput(bProvidesInput), m_ports(std::move(ports))
+{
+}
+
+void CPhysicalTopology::Reset()
+{
+ CPhysicalTopology defaultTopology;
+ *this = std::move(defaultTopology);
+}
+
+bool CPhysicalTopology::Deserialize(const TiXmlElement* pElement)
+{
+ Reset();
+
+ if (pElement == nullptr)
+ return false;
+
+ m_bProvidesInput = (XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_PROVIDES_INPUT) != "false");
+
+ for (const TiXmlElement* pChild = pElement->FirstChildElement(); pChild != nullptr;
+ pChild = pChild->NextSiblingElement())
+ {
+ if (pChild->ValueStr() == LAYOUT_XML_ELM_PORT)
+ {
+ CPhysicalPort port;
+ if (port.Deserialize(pChild))
+ m_ports.emplace_back(std::move(port));
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Unknown physical topology tag: <{}>", pChild->ValueStr());
+ }
+ }
+
+ return true;
+}
diff --git a/xbmc/games/controllers/input/PhysicalTopology.h b/xbmc/games/controllers/input/PhysicalTopology.h
new file mode 100644
index 0000000..c194de8
--- /dev/null
+++ b/xbmc/games/controllers/input/PhysicalTopology.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017-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 "games/ports/input/PhysicalPort.h"
+
+#include <vector>
+
+class TiXmlElement;
+
+namespace KODI
+{
+namespace GAME
+{
+
+/*!
+ * \brief Represents the physical topology of controller add-ons
+ *
+ * The physical topology of a controller defines how many ports it has and
+ * whether it can provide player input (hubs like the Super Multitap don't
+ * provide input).
+ */
+class CPhysicalTopology
+{
+public:
+ CPhysicalTopology() = default;
+ CPhysicalTopology(bool bProvidesInput, std::vector<CPhysicalPort> ports);
+
+ void Reset();
+
+ /*!
+ * \brief Check if the controller can provide player input
+ *
+ * This allows hubs to specify that they provide no input
+ *
+ * \return True if the controller can provide player input, false otherwise
+ */
+ bool ProvidesInput() const { return m_bProvidesInput; }
+
+ /*!
+ * \brief Get a list of ports provided by this controller
+ *
+ * \return The ports
+ */
+ const std::vector<CPhysicalPort>& Ports() const { return m_ports; }
+
+ bool Deserialize(const TiXmlElement* pElement);
+
+private:
+ bool m_bProvidesInput = true;
+ std::vector<CPhysicalPort> m_ports;
+};
+
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/types/CMakeLists.txt b/xbmc/games/controllers/types/CMakeLists.txt
new file mode 100644
index 0000000..503d254
--- /dev/null
+++ b/xbmc/games/controllers/types/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(SOURCES ControllerGrid.cpp
+ ControllerHub.cpp
+ ControllerNode.cpp
+)
+
+set(HEADERS ControllerGrid.h
+ ControllerHub.h
+ ControllerNode.h
+ ControllerTree.h
+)
+
+core_add_library(games_controller_types)
diff --git a/xbmc/games/controllers/types/ControllerGrid.cpp b/xbmc/games/controllers/types/ControllerGrid.cpp
new file mode 100644
index 0000000..f718d5a
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerGrid.cpp
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2017-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 "ControllerGrid.h"
+
+#include "games/controllers/Controller.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerGrid::~CControllerGrid() = default;
+
+void CControllerGrid::SetControllerTree(const CControllerTree& controllerTree)
+{
+ // Clear the result
+ m_grid.clear();
+
+ m_height = AddPorts(controllerTree.GetPorts(), m_grid);
+ SetHeight(m_height, m_grid);
+}
+
+ControllerVector CControllerGrid::GetControllers(unsigned int playerIndex) const
+{
+ ControllerVector controllers;
+
+ if (playerIndex < m_grid.size())
+ {
+ for (const auto& controllerVertex : m_grid[playerIndex].vertices)
+ {
+ if (controllerVertex.controller)
+ controllers.emplace_back(controllerVertex.controller);
+ }
+ }
+
+ return controllers;
+}
+
+unsigned int CControllerGrid::AddPorts(const PortVec& ports, ControllerGrid& grid)
+{
+ unsigned int height = 0;
+
+ auto itKeyboard = std::find_if(ports.begin(), ports.end(), [](const CPortNode& port) {
+ return port.GetPortType() == PORT_TYPE::KEYBOARD;
+ });
+
+ auto itMouse = std::find_if(ports.begin(), ports.end(), [](const CPortNode& port) {
+ return port.GetPortType() == PORT_TYPE::MOUSE;
+ });
+
+ auto itController = std::find_if(ports.begin(), ports.end(), [](const CPortNode& port) {
+ return port.GetPortType() == PORT_TYPE::CONTROLLER;
+ });
+
+ // Keyboard and mouse are not allowed to have ports because they might
+ // overlap with controllers
+ if (itKeyboard != ports.end() && itKeyboard->GetActiveController().GetHub().HasPorts())
+ {
+ CLog::Log(LOGERROR, "Found keyboard with controller ports, skipping");
+ itKeyboard = ports.end();
+ }
+ if (itMouse != ports.end() && itMouse->GetActiveController().GetHub().HasPorts())
+ {
+ CLog::Log(LOGERROR, "Found mouse with controller ports, skipping");
+ itMouse = ports.end();
+ }
+
+ if (itController != ports.end())
+ {
+ // Add controller ports
+ bool bFirstPlayer = true;
+ for (const CPortNode& port : ports)
+ {
+ ControllerColumn column;
+
+ if (port.GetPortType() == PORT_TYPE::CONTROLLER)
+ {
+ // Add controller
+ height =
+ std::max(height, AddController(port, static_cast<unsigned int>(column.vertices.size()),
+ column.vertices, grid));
+
+ if (bFirstPlayer)
+ {
+ bFirstPlayer = false;
+
+ // Keyboard and mouse are added below the first controller
+ if (itKeyboard != ports.end())
+ height =
+ std::max(height, AddController(*itKeyboard,
+ static_cast<unsigned int>(column.vertices.size()),
+ column.vertices, grid));
+ if (itMouse != ports.end())
+ height = std::max(
+ height, AddController(*itMouse, static_cast<unsigned int>(column.vertices.size()),
+ column.vertices, grid));
+ }
+ }
+
+ if (!column.vertices.empty())
+ grid.emplace_back(std::move(column));
+ }
+ }
+ else
+ {
+ // No controllers, add keyboard and mouse
+ ControllerColumn column;
+
+ if (itKeyboard != ports.end())
+ height = std::max(height, AddController(*itKeyboard,
+ static_cast<unsigned int>(column.vertices.size()),
+ column.vertices, grid));
+ if (itMouse != ports.end())
+ height = std::max(height,
+ AddController(*itMouse, static_cast<unsigned int>(column.vertices.size()),
+ column.vertices, grid));
+
+ if (!column.vertices.empty())
+ grid.emplace_back(std::move(column));
+ }
+
+ return height;
+}
+
+unsigned int CControllerGrid::AddController(const CPortNode& port,
+ unsigned int height,
+ std::vector<ControllerVertex>& column,
+ ControllerGrid& grid)
+{
+ // Add spacers
+ while (column.size() < height)
+ AddInvisible(column);
+
+ const CControllerNode& activeController = port.GetActiveController();
+
+ // Add vertex
+ ControllerVertex vertex;
+ vertex.bVisible = true;
+ vertex.bConnected = port.IsConnected();
+ vertex.portType = port.GetPortType();
+ vertex.controller = activeController.GetController();
+ vertex.address = activeController.GetControllerAddress();
+ for (const CControllerNode& node : port.GetCompatibleControllers())
+ vertex.compatible.emplace_back(node.GetController());
+ column.emplace_back(std::move(vertex));
+
+ height++;
+
+ // Process ports
+ const PortVec& ports = activeController.GetHub().GetPorts();
+ if (!ports.empty())
+ {
+ switch (GetDirection(activeController))
+ {
+ case GRID_DIRECTION::RIGHT:
+ {
+ height = std::max(height, AddHub(ports, height - 1, false, grid));
+ break;
+ }
+ case GRID_DIRECTION::DOWN:
+ {
+ const unsigned int row = height;
+
+ // Add the first controller to the column
+ const CPortNode firstController = ports.at(0);
+ height = std::max(height, AddController(firstController, row, column, grid));
+
+ // Add the remaining controllers on the same row
+ height = std::max(height, AddHub(ports, row, true, grid));
+
+ break;
+ }
+ }
+ }
+
+ return height;
+}
+
+unsigned int CControllerGrid::AddHub(const PortVec& ports,
+ unsigned int height,
+ bool bSkipFirst,
+ ControllerGrid& grid)
+{
+ const unsigned int row = height;
+
+ unsigned int port = 0;
+ for (const auto& controllerPort : ports)
+ {
+ // If controller has no player, it has already added the hub's first controller
+ if (bSkipFirst && port == 0)
+ continue;
+
+ // Add a column for this controller
+ grid.emplace_back();
+ ControllerColumn& column = grid.back();
+
+ height = std::max(height, AddController(controllerPort, row, column.vertices, grid));
+
+ port++;
+ }
+
+ return height;
+}
+
+void CControllerGrid::AddInvisible(std::vector<ControllerVertex>& column)
+{
+ ControllerVertex vertex;
+ vertex.bVisible = false;
+ column.emplace_back(std::move(vertex));
+}
+
+void CControllerGrid::SetHeight(unsigned int height, ControllerGrid& grid)
+{
+ for (auto& column : grid)
+ {
+ while (static_cast<unsigned int>(column.vertices.size()) < height)
+ AddInvisible(column.vertices);
+ }
+}
+
+CControllerGrid::GRID_DIRECTION CControllerGrid::GetDirection(const CControllerNode& node)
+{
+ // Hub controllers are added horizontally, one per row.
+ //
+ // If the current controller offers a player spot, the row starts to the
+ // right at the same height as the controller.
+ //
+ // Otherwise, to row starts below the current controller in the same
+ // column.
+ if (node.ProvidesInput())
+ return GRID_DIRECTION::RIGHT;
+ else
+ return GRID_DIRECTION::DOWN;
+}
diff --git a/xbmc/games/controllers/types/ControllerGrid.h b/xbmc/games/controllers/types/ControllerGrid.h
new file mode 100644
index 0000000..44ec1d3
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerGrid.h
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2017-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 "ControllerTree.h"
+#include "games/controllers/ControllerTypes.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief Vertex in the grid of controllers
+ */
+struct ControllerVertex
+{
+ bool bVisible = true;
+ bool bConnected = false;
+ ControllerPtr controller; // Mandatory if connected
+ PORT_TYPE portType = PORT_TYPE::UNKNOWN; // Optional
+ std::string address; // Optional
+ ControllerVector compatible; // Compatible controllers
+};
+
+/*!
+ * \brief Column of controllers in the grid
+ */
+struct ControllerColumn
+{
+ std::vector<ControllerVertex> vertices;
+};
+
+/*!
+ * \brief Collection of controllers in a grid layout
+ */
+using ControllerGrid = std::vector<ControllerColumn>;
+
+/*!
+ * \brief Class to encapsulate grid operations
+ */
+class CControllerGrid
+{
+public:
+ CControllerGrid() = default;
+ CControllerGrid(const CControllerGrid& other) = default;
+ ~CControllerGrid();
+
+ /*!
+ * \brief Create a grid from a controller tree
+ */
+ void SetControllerTree(const CControllerTree& controllerTree);
+
+ /*!
+ * \brief Get the width of the controller grid
+ */
+ unsigned int GetWidth() const { return static_cast<unsigned int>(m_grid.size()); }
+
+ /*!
+ * \brief Get the height (deepest controller) of the controller grid
+ *
+ * The height is cached when the controller grid is created to avoid
+ * iterating the grid
+ */
+ unsigned int GetHeight() const { return m_height; }
+
+ /*!
+ * \brief Access the controller grid
+ */
+ const ControllerGrid& GetGrid() const { return m_grid; }
+
+ /*!
+ * \brief Get the controllers in use by the specified player
+ *
+ * \param playerIndex The column in the grid to get controllers from
+ */
+ ControllerVector GetControllers(unsigned int playerIndex) const;
+
+private:
+ /*!
+ * \brief Directions of vertex traversal
+ */
+ enum class GRID_DIRECTION
+ {
+ RIGHT,
+ DOWN,
+ };
+
+ /*!
+ * \brief Add ports to the grid
+ *
+ * \param ports The ports on a console or controller
+ * \param[out] grid The controller grid being created
+ *
+ * \return The height of the grid determined by the maximum column height
+ */
+ static unsigned int AddPorts(const PortVec& ports, ControllerGrid& grid);
+
+ /*!
+ * \brief Draw a controller to the column at the specified height
+ *
+ * \param port The controller's port node
+ * \param height The height to draw the controller at
+ * \param column[in/out] The column to draw to
+ * \param grid[in/out] The grid to add additional columns to
+ *
+ * \return The height of the grid
+ */
+ static unsigned int AddController(const CPortNode& port,
+ unsigned int height,
+ std::vector<ControllerVertex>& column,
+ ControllerGrid& grid);
+
+ /*!
+ * \brief Draw a series of controllers to the grid at the specified height
+ *
+ * \param ports The ports of the controllers to draw
+ * \param height The height to start drawing the controllers at
+ * \param bSkipFirst True if the first controller has already been drawn to
+ * a column, false to start drawing at the first controller
+ * \param grid[in/out] The grid to add columns to
+ *
+ * \return The height of the grid
+ */
+ static unsigned int AddHub(const PortVec& ports,
+ unsigned int height,
+ bool bSkipFirst,
+ ControllerGrid& grid);
+
+ /*!
+ * \brief Draw an invisible vertex to the column
+ *
+ * \param[in/out] column The column in a controller grid
+ */
+ static void AddInvisible(std::vector<ControllerVertex>& column);
+
+ /*!
+ * \brief Fill all columns with invisible vertices until the specified height
+ *
+ * \param height The height to make all columns
+ * \param[in/out] grid The grid to update
+ */
+ static void SetHeight(unsigned int height, ControllerGrid& grid);
+
+ /*!
+ * \brief Get the direction of traversal for the next vertex
+ *
+ * \param node The node in the controller tree being visited
+ *
+ * \return The direction of the next vertex, or GRID_DIRECTION::UNKNOWN if
+ * unknown
+ */
+ static GRID_DIRECTION GetDirection(const CControllerNode& node);
+
+ ControllerGrid m_grid;
+ unsigned int m_height = 0;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/types/ControllerHub.cpp b/xbmc/games/controllers/types/ControllerHub.cpp
new file mode 100644
index 0000000..5fd0ece
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerHub.cpp
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2017-2021 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 "ControllerHub.h"
+
+#include "ControllerTree.h"
+#include "games/controllers/Controller.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerHub::~CControllerHub() = default;
+
+CControllerHub& CControllerHub::operator=(const CControllerHub& rhs)
+{
+ if (this != &rhs)
+ {
+ m_ports = rhs.m_ports;
+ }
+
+ return *this;
+}
+
+CControllerHub& CControllerHub::operator=(CControllerHub&& rhs) noexcept
+{
+ if (this != &rhs)
+ {
+ m_ports = std::move(rhs.m_ports);
+ }
+
+ return *this;
+}
+
+void CControllerHub::Clear()
+{
+ m_ports.clear();
+}
+
+void CControllerHub::SetPorts(PortVec ports)
+{
+ m_ports = std::move(ports);
+}
+
+bool CControllerHub::IsControllerAccepted(const std::string& controllerId) const
+{
+ return std::any_of(m_ports.begin(), m_ports.end(), [controllerId](const CPortNode& port) {
+ return port.IsControllerAccepted(controllerId);
+ });
+}
+
+bool CControllerHub::IsControllerAccepted(const std::string& portAddress,
+ const std::string& controllerId) const
+{
+ return std::any_of(m_ports.begin(), m_ports.end(),
+ [portAddress, controllerId](const CPortNode& port) {
+ return port.IsControllerAccepted(portAddress, controllerId);
+ });
+}
+
+ControllerVector CControllerHub::GetControllers() const
+{
+ ControllerVector controllers;
+ GetControllers(controllers);
+ return controllers;
+}
+
+void CControllerHub::GetControllers(ControllerVector& controllers) const
+{
+ for (const CPortNode& port : m_ports)
+ {
+ for (const CControllerNode& node : port.GetCompatibleControllers())
+ node.GetControllers(controllers);
+ }
+}
+
+const CPortNode& CControllerHub::GetPort(const std::string& address) const
+{
+ return GetPortInternal(m_ports, address);
+}
+
+const CPortNode& CControllerHub::GetPortInternal(const PortVec& ports, const std::string& address)
+{
+ for (const CPortNode& port : ports)
+ {
+ // Base case
+ if (port.GetAddress() == address)
+ return port;
+
+ // Check children
+ for (const CControllerNode& controller : port.GetCompatibleControllers())
+ {
+ const CPortNode& port = GetPortInternal(controller.GetHub().GetPorts(), address);
+ if (port.GetAddress() == address)
+ return port;
+ }
+ }
+
+ // Not found
+ static const CPortNode empty{};
+ return empty;
+}
diff --git a/xbmc/games/controllers/types/ControllerHub.h b/xbmc/games/controllers/types/ControllerHub.h
new file mode 100644
index 0000000..fc48a81
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerHub.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017-2021 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 "games/controllers/ControllerTypes.h"
+#include "games/ports/types/PortNode.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief A branch in the controller tree
+ */
+class CControllerHub
+{
+public:
+ CControllerHub() = default;
+ CControllerHub(const CControllerHub& other) { *this = other; }
+ CControllerHub(CControllerHub&& other) = default;
+ CControllerHub& operator=(const CControllerHub& rhs);
+ CControllerHub& operator=(CControllerHub&& rhs) noexcept;
+ ~CControllerHub();
+
+ void Clear();
+
+ bool HasPorts() const { return !m_ports.empty(); }
+ PortVec& GetPorts() { return m_ports; }
+ const PortVec& GetPorts() const { return m_ports; }
+ void SetPorts(PortVec ports);
+
+ bool IsControllerAccepted(const std::string& controllerId) const;
+ bool IsControllerAccepted(const std::string& portAddress, const std::string& controllerId) const;
+ ControllerVector GetControllers() const;
+ void GetControllers(ControllerVector& controllers) const;
+
+ const CPortNode& GetPort(const std::string& address) const;
+
+private:
+ static const CPortNode& GetPortInternal(const PortVec& ports, const std::string& address);
+
+ PortVec m_ports;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/types/ControllerNode.cpp b/xbmc/games/controllers/types/ControllerNode.cpp
new file mode 100644
index 0000000..4bfda73
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerNode.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2017-2021 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 "ControllerNode.h"
+
+#include "ControllerHub.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/input/PhysicalTopology.h"
+#include "games/ports/types/PortNode.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CControllerNode::CControllerNode() : m_hub(new CControllerHub)
+{
+}
+
+CControllerNode::~CControllerNode() = default;
+
+CControllerNode& CControllerNode::operator=(const CControllerNode& rhs)
+{
+ if (this != &rhs)
+ {
+ m_controller = rhs.m_controller;
+ m_portAddress = rhs.m_portAddress;
+ m_controllerAddress = rhs.m_controllerAddress;
+ m_hub.reset(new CControllerHub(*rhs.m_hub));
+ }
+
+ return *this;
+}
+
+CControllerNode& CControllerNode::operator=(CControllerNode&& rhs) noexcept
+{
+ if (this != &rhs)
+ {
+ m_controller = std::move(rhs.m_controller);
+ m_portAddress = std::move(rhs.m_portAddress);
+ m_controllerAddress = std::move(rhs.m_controllerAddress);
+ m_hub = std::move(rhs.m_hub);
+ }
+
+ return *this;
+}
+
+void CControllerNode::Clear()
+{
+ m_controller.reset();
+ m_portAddress.clear();
+ m_controllerAddress.clear();
+ m_hub.reset(new CControllerHub);
+}
+
+void CControllerNode::SetController(ControllerPtr controller)
+{
+ m_controller = std::move(controller);
+}
+
+void CControllerNode::GetControllers(ControllerVector& controllers) const
+{
+ if (m_controller)
+ {
+ const ControllerPtr& myController = m_controller;
+
+ auto it = std::find_if(controllers.begin(), controllers.end(),
+ [&myController](const ControllerPtr& controller) {
+ return myController->ID() == controller->ID();
+ });
+
+ if (it == controllers.end())
+ controllers.emplace_back(m_controller);
+ }
+
+ m_hub->GetControllers(controllers);
+}
+
+void CControllerNode::SetPortAddress(std::string portAddress)
+{
+ m_portAddress = std::move(portAddress);
+}
+
+void CControllerNode::SetControllerAddress(std::string controllerAddress)
+{
+ m_controllerAddress = std::move(controllerAddress);
+}
+
+void CControllerNode::SetHub(CControllerHub hub)
+{
+ m_hub.reset(new CControllerHub(std::move(hub)));
+}
+
+bool CControllerNode::IsControllerAccepted(const std::string& controllerId) const
+{
+ bool bAccepted = false;
+
+ for (const auto& port : m_hub->GetPorts())
+ {
+ if (port.IsControllerAccepted(controllerId))
+ {
+ bAccepted = true;
+ break;
+ }
+ }
+
+ return bAccepted;
+}
+
+bool CControllerNode::IsControllerAccepted(const std::string& portAddress,
+ const std::string& controllerId) const
+{
+ bool bAccepted = false;
+
+ for (const auto& port : m_hub->GetPorts())
+ {
+ if (port.IsControllerAccepted(portAddress, controllerId))
+ {
+ bAccepted = true;
+ break;
+ }
+ }
+
+ return bAccepted;
+}
+
+bool CControllerNode::ProvidesInput() const
+{
+ return m_controller && m_controller->Topology().ProvidesInput();
+}
diff --git a/xbmc/games/controllers/types/ControllerNode.h b/xbmc/games/controllers/types/ControllerNode.h
new file mode 100644
index 0000000..d4d86e5
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerNode.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017-2021 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 "games/controllers/ControllerTypes.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerHub;
+
+/*!
+ * \brief Node in the controller tree
+ *
+ * The node identifies the controller profile, and optionally the available
+ * controller ports.
+ */
+class CControllerNode
+{
+public:
+ CControllerNode();
+ CControllerNode(const CControllerNode& other) { *this = other; }
+ CControllerNode(CControllerNode&& other) = default;
+ CControllerNode& operator=(const CControllerNode& rhs);
+ CControllerNode& operator=(CControllerNode&& rhs) noexcept;
+ ~CControllerNode();
+
+ void Clear();
+
+ /*!
+ * \brief Controller profile of this code
+ *
+ * \return Controller profile, or empty if this node is invalid
+ *
+ * \sa IsValid()
+ */
+ const ControllerPtr& GetController() const { return m_controller; }
+ void SetController(ControllerPtr controller);
+
+ void GetControllers(ControllerVector& controllers) const;
+
+ /*!
+ * \brief Address given to the controller's port by the implementation
+ */
+ const std::string& GetPortAddress() const { return m_portAddress; }
+ void SetPortAddress(std::string portAddress);
+
+ /*!
+ * \brief Address given to the controller node by the implementation
+ */
+ const std::string& GetControllerAddress() const { return m_controllerAddress; }
+ void SetControllerAddress(std::string controllerAddress);
+
+ /*!
+ * \brief Collection of ports on this controller
+ *
+ * \return A hub with controller ports, or an empty hub if this controller
+ * has no available ports
+ */
+ const CControllerHub& GetHub() const { return *m_hub; }
+ CControllerHub& GetHub() { return *m_hub; }
+ void SetHub(CControllerHub hub);
+
+ /*!
+ * \brief Check if this node has a valid controller profile
+ */
+ bool IsValid() const { return static_cast<bool>(m_controller); }
+
+ /*!
+ * \brief Check to see if a controller is compatible with a controller port
+ *
+ * \param controllerId The ID of the controller
+ *
+ * \return True if the controller is compatible with a port, false otherwise
+ */
+ bool IsControllerAccepted(const std::string& controllerId) const;
+
+ /*!
+ * \brief Check to see if a controller is compatible with a controller port
+ *
+ * \param portAddress The port address
+ * \param controllerId The ID of the controller
+ *
+ * \return True if the controller is compatible with a port, false otherwise
+ */
+ bool IsControllerAccepted(const std::string& portAddress, const std::string& controllerId) const;
+
+ /*!
+ * \brief Check if this node provides input
+ */
+ bool ProvidesInput() const;
+
+private:
+ ControllerPtr m_controller;
+
+ // Address of the port this controller is connected to
+ std::string m_portAddress;
+
+ // Address of this controller: m_portAddress + "/" + m_controller->ID()
+ std::string m_controllerAddress;
+
+ std::unique_ptr<CControllerHub> m_hub;
+};
+
+using ControllerNodeVec = std::vector<CControllerNode>;
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/types/ControllerTree.h b/xbmc/games/controllers/types/ControllerTree.h
new file mode 100644
index 0000000..47e42e9
--- /dev/null
+++ b/xbmc/games/controllers/types/ControllerTree.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017-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
+
+//
+// Note: Hierarchy of headers is:
+//
+// - ControllerTree.h (this file)
+// - ControllerHub.h
+// - PortNode.h
+// - ControllerNode.h
+//
+#include "ControllerHub.h"
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief Collection of ports on a console
+ */
+using CControllerTree = CControllerHub;
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/windows/CMakeLists.txt b/xbmc/games/controllers/windows/CMakeLists.txt
new file mode 100644
index 0000000..72c8154
--- /dev/null
+++ b/xbmc/games/controllers/windows/CMakeLists.txt
@@ -0,0 +1,15 @@
+set(SOURCES GUIConfigurationWizard.cpp
+ GUIControllerList.cpp
+ GUIControllerWindow.cpp
+ GUIFeatureList.cpp
+)
+
+set(HEADERS GUIConfigurationWizard.h
+ GUIControllerDefines.h
+ GUIControllerList.h
+ GUIControllerWindow.h
+ GUIFeatureList.h
+ IConfigurationWindow.h
+)
+
+core_add_library(games_controller_windows)
diff --git a/xbmc/games/controllers/windows/GUIConfigurationWizard.cpp b/xbmc/games/controllers/windows/GUIConfigurationWizard.cpp
new file mode 100644
index 0000000..c2ac2f5
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIConfigurationWizard.cpp
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2014-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 "GUIConfigurationWizard.h"
+
+#include "ServiceBroker.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/dialogs/GUIDialogAxisDetection.h"
+#include "games/controllers/guicontrols/GUIFeatureButton.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "input/IKeymap.h"
+#include "input/InputManager.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IButtonMapCallback.h"
+#include "input/keyboard/KeymapActionMap.h"
+#include "peripherals/Peripherals.h"
+#include "threads/SingleLock.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace GAME;
+using namespace std::chrono_literals;
+
+namespace
+{
+
+#define ESC_KEY_CODE 27
+#define SKIPPING_DETECTION_MS 200
+
+// Duration to wait for axes to neutralize after mapping is finished
+constexpr auto POST_MAPPING_WAIT_TIME_MS = 5000ms;
+
+} // namespace
+
+CGUIConfigurationWizard::CGUIConfigurationWizard()
+ : CThread("GUIConfigurationWizard"), m_actionMap(new KEYBOARD::CKeymapActionMap)
+{
+ InitializeState();
+}
+
+CGUIConfigurationWizard::~CGUIConfigurationWizard(void) = default;
+
+void CGUIConfigurationWizard::InitializeState(void)
+{
+ m_currentButton = nullptr;
+ m_cardinalDirection = INPUT::CARDINAL_DIRECTION::NONE;
+ m_wheelDirection = JOYSTICK::WHEEL_DIRECTION::NONE;
+ m_throttleDirection = JOYSTICK::THROTTLE_DIRECTION::NONE;
+ m_history.clear();
+ m_lateAxisDetected = false;
+ m_location.clear();
+}
+
+void CGUIConfigurationWizard::Run(const std::string& strControllerId,
+ const std::vector<IFeatureButton*>& buttons)
+{
+ Abort();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+
+ // Set Run() parameters
+ m_strControllerId = strControllerId;
+ m_buttons = buttons;
+
+ // Reset synchronization variables
+ m_inputEvent.Reset();
+ m_motionlessEvent.Reset();
+ m_bInMotion.clear();
+
+ // Initialize state variables
+ InitializeState();
+ }
+
+ Create();
+}
+
+void CGUIConfigurationWizard::OnUnfocus(IFeatureButton* button)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+
+ if (button == m_currentButton)
+ Abort(false);
+}
+
+bool CGUIConfigurationWizard::Abort(bool bWait /* = true */)
+{
+ bool bWasRunning = !m_bStop;
+
+ StopThread(false);
+
+ m_inputEvent.Set();
+ m_motionlessEvent.Set();
+
+ if (bWait)
+ StopThread(true);
+
+ return bWasRunning;
+}
+
+void CGUIConfigurationWizard::RegisterKey(const CPhysicalFeature& key)
+{
+ if (key.Keycode() != XBMCK_UNKNOWN)
+ m_keyMap[key.Keycode()] = key;
+}
+
+void CGUIConfigurationWizard::UnregisterKeys()
+{
+ m_keyMap.clear();
+}
+
+void CGUIConfigurationWizard::Process(void)
+{
+ CLog::Log(LOGDEBUG, "Starting configuration wizard");
+
+ InstallHooks();
+
+ bool bLateAxisDetected = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+ for (IFeatureButton* button : m_buttons)
+ {
+ // Allow other threads to access the button we're using
+ m_currentButton = button;
+
+ while (!button->IsFinished())
+ {
+ // Allow other threads to access which direction the prompt is on
+ m_cardinalDirection = button->GetCardinalDirection();
+ m_wheelDirection = button->GetWheelDirection();
+ m_throttleDirection = button->GetThrottleDirection();
+
+ // Wait for input
+ {
+ using namespace JOYSTICK;
+
+ CSingleExit exit(m_stateMutex);
+
+ if (button->Feature().Type() == FEATURE_TYPE::UNKNOWN)
+ CLog::Log(LOGDEBUG, "{}: Waiting for input", m_strControllerId);
+ else
+ CLog::Log(LOGDEBUG, "{}: Waiting for input for feature \"{}\"", m_strControllerId,
+ button->Feature().Name());
+
+ if (!button->PromptForInput(m_inputEvent))
+ Abort(false);
+ }
+
+ if (m_bStop)
+ break;
+ }
+
+ button->Reset();
+
+ if (m_bStop)
+ break;
+ }
+
+ bLateAxisDetected = m_lateAxisDetected;
+
+ // Finished mapping
+ InitializeState();
+ }
+
+ for (auto callback : ButtonMapCallbacks())
+ callback.second->SaveButtonMap();
+
+ if (bLateAxisDetected)
+ {
+ CGUIDialogAxisDetection dialog;
+ dialog.Show();
+ }
+ else
+ {
+ // Wait for motion to stop to avoid sending analog actions for the button
+ // that is pressed immediately after button mapping finishes.
+ bool bInMotion;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_motionMutex);
+ bInMotion = !m_bInMotion.empty();
+ }
+
+ if (bInMotion)
+ {
+ CLog::Log(LOGDEBUG, "Configuration wizard: waiting {}ms for axes to neutralize",
+ POST_MAPPING_WAIT_TIME_MS.count());
+ m_motionlessEvent.Wait(POST_MAPPING_WAIT_TIME_MS);
+ }
+ }
+
+ RemoveHooks();
+
+ CLog::Log(LOGDEBUG, "Configuration wizard ended");
+}
+
+bool CGUIConfigurationWizard::MapPrimitive(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive)
+{
+ using namespace INPUT;
+ using namespace JOYSTICK;
+
+ bool bHandled = false;
+
+ // Abort if another controller cancels the prompt
+ if (IsMapping() && !IsMapping(buttonMap->Location()))
+ {
+ //! @todo This only succeeds for game.controller.default; no actions are
+ // currently defined for other controllers
+ if (keymap)
+ {
+ std::string feature;
+ if (buttonMap->GetFeature(primitive, feature))
+ {
+ const auto& actions = keymap->GetActions(CJoystickUtils::MakeKeyName(feature)).actions;
+ if (!actions.empty())
+ {
+ //! @todo Handle multiple actions mapped to the same key
+ OnAction(actions.begin()->actionId);
+ }
+ }
+ }
+
+ // Discard input
+ bHandled = true;
+ }
+ else if (m_history.find(primitive) != m_history.end())
+ {
+ // Primitive has already been mapped this round, ignore it
+ bHandled = true;
+ }
+ else if (buttonMap->IsIgnored(primitive))
+ {
+ bHandled = true;
+ }
+ else
+ {
+ // Get the current state of the thread
+ IFeatureButton* currentButton;
+ CARDINAL_DIRECTION cardinalDirection;
+ WHEEL_DIRECTION wheelDirection;
+ THROTTLE_DIRECTION throttleDirection;
+ {
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+ currentButton = m_currentButton;
+ cardinalDirection = m_cardinalDirection;
+ wheelDirection = m_wheelDirection;
+ throttleDirection = m_throttleDirection;
+ }
+
+ if (currentButton)
+ {
+ // Check if we were expecting a keyboard key
+ if (currentButton->NeedsKey())
+ {
+ if (primitive.Type() == PRIMITIVE_TYPE::KEY)
+ {
+ auto it = m_keyMap.find(primitive.Keycode());
+ if (it != m_keyMap.end())
+ {
+ const CPhysicalFeature& key = it->second;
+ currentButton->SetKey(key);
+ m_inputEvent.Set();
+ }
+ }
+ else
+ {
+ //! @todo Check if primitive is a cancel or motion action
+ }
+ bHandled = true;
+ }
+ else
+ {
+ const CPhysicalFeature& feature = currentButton->Feature();
+
+ if (primitive.Type() == PRIMITIVE_TYPE::RELATIVE_POINTER &&
+ feature.Type() != FEATURE_TYPE::RELPOINTER)
+ {
+ // Don't allow relative pointers to map to other features
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{}: mapping feature \"{}\" for device {} to \"{}\"",
+ m_strControllerId, feature.Name(), buttonMap->Location(), primitive.ToString());
+
+ switch (feature.Type())
+ {
+ case FEATURE_TYPE::SCALAR:
+ {
+ buttonMap->AddScalar(feature.Name(), primitive);
+ bHandled = true;
+ break;
+ }
+ case FEATURE_TYPE::ANALOG_STICK:
+ {
+ buttonMap->AddAnalogStick(feature.Name(), cardinalDirection, primitive);
+ bHandled = true;
+ break;
+ }
+ case FEATURE_TYPE::RELPOINTER:
+ {
+ buttonMap->AddRelativePointer(feature.Name(), cardinalDirection, primitive);
+ bHandled = true;
+ break;
+ }
+ case FEATURE_TYPE::WHEEL:
+ {
+ buttonMap->AddWheel(feature.Name(), wheelDirection, primitive);
+ bHandled = true;
+ break;
+ }
+ case FEATURE_TYPE::THROTTLE:
+ {
+ buttonMap->AddThrottle(feature.Name(), throttleDirection, primitive);
+ bHandled = true;
+ break;
+ }
+ case FEATURE_TYPE::KEY:
+ {
+ buttonMap->AddKey(feature.Name(), primitive);
+ bHandled = true;
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ if (bHandled)
+ {
+ m_history.insert(primitive);
+
+ // Don't record motion for relative pointers
+ if (primitive.Type() != PRIMITIVE_TYPE::RELATIVE_POINTER)
+ OnMotion(buttonMap);
+
+ m_inputEvent.Set();
+
+ if (m_location.empty())
+ {
+ m_location = buttonMap->Location();
+ m_bIsKeyboard = (primitive.Type() == PRIMITIVE_TYPE::KEY);
+ }
+ }
+ }
+ }
+ }
+
+ return bHandled;
+}
+
+void CGUIConfigurationWizard::OnEventFrame(const JOYSTICK::IButtonMap* buttonMap, bool bMotion)
+{
+ std::unique_lock<CCriticalSection> lock(m_motionMutex);
+
+ if (m_bInMotion.find(buttonMap) != m_bInMotion.end() && !bMotion)
+ OnMotionless(buttonMap);
+}
+
+void CGUIConfigurationWizard::OnLateAxis(const JOYSTICK::IButtonMap* buttonMap,
+ unsigned int axisIndex)
+{
+ std::unique_lock<CCriticalSection> lock(m_stateMutex);
+
+ m_lateAxisDetected = true;
+ Abort(false);
+}
+
+void CGUIConfigurationWizard::OnMotion(const JOYSTICK::IButtonMap* buttonMap)
+{
+ std::unique_lock<CCriticalSection> lock(m_motionMutex);
+
+ m_motionlessEvent.Reset();
+ m_bInMotion.insert(buttonMap);
+}
+
+void CGUIConfigurationWizard::OnMotionless(const JOYSTICK::IButtonMap* buttonMap)
+{
+ m_bInMotion.erase(buttonMap);
+ if (m_bInMotion.empty())
+ m_motionlessEvent.Set();
+}
+
+bool CGUIConfigurationWizard::OnKeyPress(const CKey& key)
+{
+ bool bHandled = false;
+
+ if (!m_bStop)
+ {
+ // Only allow key to abort the prompt if we know for sure that we're mapping
+ // a controller
+ const bool bIsMappingController = (IsMapping() && !m_bIsKeyboard);
+
+ if (bIsMappingController)
+ {
+ bHandled = OnAction(m_actionMap->GetActionID(key));
+ }
+ else
+ {
+ // Allow key press to fall through to the button mapper
+ }
+ }
+
+ return bHandled;
+}
+
+bool CGUIConfigurationWizard::OnAction(unsigned int actionId)
+{
+ bool bHandled = false;
+
+ switch (actionId)
+ {
+ case ACTION_MOVE_LEFT:
+ case ACTION_MOVE_RIGHT:
+ case ACTION_MOVE_UP:
+ case ACTION_MOVE_DOWN:
+ case ACTION_PAGE_UP:
+ case ACTION_PAGE_DOWN:
+ // Abort and allow motion
+ Abort(false);
+ bHandled = false;
+ break;
+
+ case ACTION_PARENT_DIR:
+ case ACTION_PREVIOUS_MENU:
+ case ACTION_STOP:
+ case ACTION_NAV_BACK:
+ // Abort and prevent action
+ Abort(false);
+ bHandled = true;
+ break;
+
+ default:
+ // Absorb keypress
+ bHandled = true;
+ break;
+ }
+
+ return bHandled;
+}
+
+bool CGUIConfigurationWizard::IsMapping() const
+{
+ return !m_location.empty();
+}
+
+bool CGUIConfigurationWizard::IsMapping(const std::string& location) const
+{
+ return m_location == location;
+}
+
+void CGUIConfigurationWizard::InstallHooks(void)
+{
+ // Install button mapper with lowest priority
+ CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this);
+
+ // Install hook to reattach button mapper when peripherals change
+ CServiceBroker::GetPeripherals().RegisterObserver(this);
+
+ // Install hook to cancel the button mapper
+ CServiceBroker::GetInputManager().RegisterKeyboardDriverHandler(this);
+}
+
+void CGUIConfigurationWizard::RemoveHooks(void)
+{
+ CServiceBroker::GetInputManager().UnregisterKeyboardDriverHandler(this);
+ CServiceBroker::GetPeripherals().UnregisterObserver(this);
+ CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this);
+}
+
+void CGUIConfigurationWizard::Notify(const Observable& obs, const ObservableMessage msg)
+{
+ switch (msg)
+ {
+ case ObservableMessagePeripheralsChanged:
+ {
+ CServiceBroker::GetPeripherals().UnregisterJoystickButtonMapper(this);
+ CServiceBroker::GetPeripherals().RegisterJoystickButtonMapper(this);
+ break;
+ }
+ default:
+ break;
+ }
+}
diff --git a/xbmc/games/controllers/windows/GUIConfigurationWizard.h b/xbmc/games/controllers/windows/GUIConfigurationWizard.h
new file mode 100644
index 0000000..b69badf
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIConfigurationWizard.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2014-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 "IConfigurationWindow.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "input/XBMC_keysym.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/interfaces/IButtonMapper.h"
+#include "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "utils/Observer.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace KEYBOARD
+{
+class IActionMap;
+}
+
+namespace GAME
+{
+class CGUIConfigurationWizard : public IConfigurationWizard,
+ public JOYSTICK::IButtonMapper,
+ public KEYBOARD::IKeyboardDriverHandler,
+ public Observer,
+ protected CThread
+{
+public:
+ CGUIConfigurationWizard();
+
+ ~CGUIConfigurationWizard() override;
+
+ // implementation of IConfigurationWizard
+ void Run(const std::string& strControllerId,
+ const std::vector<IFeatureButton*>& buttons) override;
+ void OnUnfocus(IFeatureButton* button) override;
+ bool Abort(bool bWait = true) override;
+ void RegisterKey(const CPhysicalFeature& key) override;
+ void UnregisterKeys() override;
+
+ // implementation of IButtonMapper
+ std::string ControllerID() const override { return m_strControllerId; }
+ bool NeedsCooldown() const override { return true; }
+ bool AcceptsPrimitive(JOYSTICK::PRIMITIVE_TYPE type) const override { return true; }
+ bool MapPrimitive(JOYSTICK::IButtonMap* buttonMap,
+ IKeymap* keymap,
+ const JOYSTICK::CDriverPrimitive& primitive) override;
+ void OnEventFrame(const JOYSTICK::IButtonMap* buttonMap, bool bMotion) override;
+ void OnLateAxis(const JOYSTICK::IButtonMap* buttonMap, unsigned int axisIndex) override;
+
+ // implementation of IKeyboardDriverHandler
+ bool OnKeyPress(const CKey& key) override;
+ void OnKeyRelease(const CKey& key) override {}
+
+ // implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override;
+
+protected:
+ // implementation of CThread
+ void Process() override;
+
+private:
+ void InitializeState(void);
+
+ bool IsMapping() const;
+ bool IsMapping(const std::string& location) const;
+
+ void InstallHooks(void);
+ void RemoveHooks(void);
+
+ void OnMotion(const JOYSTICK::IButtonMap* buttonMap);
+ void OnMotionless(const JOYSTICK::IButtonMap* buttonMap);
+
+ bool OnAction(unsigned int actionId);
+
+ // Run() parameters
+ std::string m_strControllerId;
+ std::vector<IFeatureButton*> m_buttons;
+
+ // State variables and mutex
+ IFeatureButton* m_currentButton;
+ INPUT::CARDINAL_DIRECTION m_cardinalDirection;
+ JOYSTICK::WHEEL_DIRECTION m_wheelDirection;
+ JOYSTICK::THROTTLE_DIRECTION m_throttleDirection;
+ std::set<JOYSTICK::CDriverPrimitive> m_history; // History to avoid repeated features
+ bool m_lateAxisDetected; // Set to true if an axis is detected during button mapping
+ std::string m_location; // Peripheral location of device that we're mapping
+ bool m_bIsKeyboard = false; // True if we're mapping keyboard keys
+ CCriticalSection m_stateMutex;
+
+ // Synchronization events
+ CEvent m_inputEvent;
+ CEvent m_motionlessEvent;
+ CCriticalSection m_motionMutex;
+ std::set<const JOYSTICK::IButtonMap*> m_bInMotion;
+
+ // Keyboard handling
+ std::unique_ptr<KEYBOARD::IActionMap> m_actionMap;
+ std::map<XBMCKey, CPhysicalFeature> m_keyMap; // Keycode -> feature
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/windows/GUIControllerDefines.h b/xbmc/games/controllers/windows/GUIControllerDefines.h
new file mode 100644
index 0000000..64d2142
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIControllerDefines.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2014-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
+
+// Duration to wait for input from the user
+#define COUNTDOWN_DURATION_SEC 6
+
+// Warn the user that time is running out after this duration
+#define WAIT_TO_WARN_SEC 2
+
+// GUI Control IDs
+#define CONTROL_CONTROLLER_LIST 3
+#define CONTROL_FEATURE_LIST 5
+#define CONTROL_FEATURE_BUTTON_TEMPLATE 7
+#define CONTROL_FEATURE_GROUP_TITLE 8
+#define CONTROL_FEATURE_SEPARATOR 9
+#define CONTROL_CONTROLLER_BUTTON_TEMPLATE 10
+#define CONTROL_GAME_CONTROLLER 31
+#define CONTROL_CONTROLLER_DESCRIPTION 32
+
+// GUI button IDs
+#define CONTROL_HELP_BUTTON 17
+#define CONTROL_CLOSE_BUTTON 18
+#define CONTROL_RESET_BUTTON 19
+#define CONTROL_GET_MORE 20
+#define CONTROL_FIX_SKIPPING 21
+#define CONTROL_GET_ALL 22
+
+#define MAX_CONTROLLER_COUNT 100 // large enough
+#define MAX_FEATURE_COUNT 200 // large enough
+
+#define CONTROL_CONTROLLER_BUTTONS_START 100
+#define CONTROL_CONTROLLER_BUTTONS_END (CONTROL_CONTROLLER_BUTTONS_START + MAX_CONTROLLER_COUNT)
+#define CONTROL_FEATURE_BUTTONS_START CONTROL_CONTROLLER_BUTTONS_END
+#define CONTROL_FEATURE_BUTTONS_END (CONTROL_FEATURE_BUTTONS_START + MAX_FEATURE_COUNT)
+#define CONTROL_FEATURE_GROUPS_START CONTROL_FEATURE_BUTTONS_END
+#define CONTROL_FEATURE_GROUPS_END (CONTROL_FEATURE_GROUPS_START + MAX_FEATURE_COUNT)
+#define CONTROL_FEATURE_SEPARATORS_START CONTROL_FEATURE_GROUPS_END
+#define CONTROL_FEATURE_SEPARATORS_END (CONTROL_FEATURE_SEPARATORS_START + MAX_FEATURE_COUNT)
diff --git a/xbmc/games/controllers/windows/GUIControllerList.cpp b/xbmc/games/controllers/windows/GUIControllerList.cpp
new file mode 100644
index 0000000..02f8d37
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIControllerList.cpp
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2014-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 "GUIControllerList.h"
+
+#include "GUIControllerDefines.h"
+#include "GUIControllerWindow.h"
+#include "GUIFeatureList.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "games/GameServices.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerIDs.h"
+#include "games/controllers/ControllerLayout.h"
+#include "games/controllers/guicontrols/GUIControllerButton.h"
+#include "games/controllers/guicontrols/GUIGameController.h"
+#include "games/controllers/types/ControllerTree.h"
+#include "guilib/GUIButtonControl.h"
+#include "guilib/GUIControlGroupList.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindow.h"
+#include "messaging/ApplicationMessenger.h"
+#include "peripherals/Peripherals.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+#include <assert.h>
+#include <iterator>
+
+using namespace KODI;
+using namespace ADDON;
+using namespace GAME;
+
+CGUIControllerList::CGUIControllerList(CGUIWindow* window,
+ IFeatureList* featureList,
+ GameClientPtr gameClient)
+ : m_guiWindow(window),
+ m_featureList(featureList),
+ m_controllerList(nullptr),
+ m_controllerButton(nullptr),
+ m_focusedController(-1), // Initially unfocused
+ m_gameClient(std::move(gameClient))
+{
+ assert(m_featureList != nullptr);
+}
+
+bool CGUIControllerList::Initialize(void)
+{
+ m_controllerList =
+ dynamic_cast<CGUIControlGroupList*>(m_guiWindow->GetControl(CONTROL_CONTROLLER_LIST));
+ m_controllerButton =
+ dynamic_cast<CGUIButtonControl*>(m_guiWindow->GetControl(CONTROL_CONTROLLER_BUTTON_TEMPLATE));
+
+ if (m_controllerButton)
+ m_controllerButton->SetVisible(false);
+
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CGUIControllerList::OnEvent);
+ Refresh("");
+
+ return m_controllerList != nullptr && m_controllerButton != nullptr;
+}
+
+void CGUIControllerList::Deinitialize(void)
+{
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+
+ CleanupButtons();
+
+ m_controllerList = nullptr;
+ m_controllerButton = nullptr;
+}
+
+bool CGUIControllerList::Refresh(const std::string& controllerId)
+{
+ // Focus specified controller after refresh
+ std::string focusController = controllerId;
+
+ if (focusController.empty() && m_focusedController >= 0)
+ {
+ // If controller ID wasn't provided, focus current controller
+ focusController = m_controllers[m_focusedController]->ID();
+ }
+
+ if (!RefreshControllers())
+ return false;
+
+ CleanupButtons();
+
+ if (m_controllerList)
+ {
+ unsigned int buttonId = 0;
+ for (const auto& controller : m_controllers)
+ {
+ CGUIButtonControl* pButton =
+ new CGUIControllerButton(*m_controllerButton, controller->Layout().Label(), buttonId++);
+ m_controllerList->AddControl(pButton);
+
+ if (!focusController.empty() && controller->ID() == focusController)
+ {
+ CGUIMessage msg(GUI_MSG_SETFOCUS, m_guiWindow->GetID(), pButton->GetID());
+ m_guiWindow->OnMessage(msg);
+ }
+
+ // Just in case
+ if (buttonId >= MAX_CONTROLLER_COUNT)
+ break;
+ }
+ }
+
+ return true;
+}
+
+void CGUIControllerList::OnFocus(unsigned int controllerIndex)
+{
+ if (controllerIndex < m_controllers.size())
+ {
+ m_focusedController = controllerIndex;
+
+ const ControllerPtr& controller = m_controllers[controllerIndex];
+ m_featureList->Load(controller);
+
+ //! @todo Activate controller for all game controller controls
+ CGUIGameController* pController =
+ dynamic_cast<CGUIGameController*>(m_guiWindow->GetControl(CONTROL_GAME_CONTROLLER));
+ if (pController)
+ pController->ActivateController(controller);
+
+ // Update controller description
+ CGUIMessage msg(GUI_MSG_LABEL_SET, m_guiWindow->GetID(), CONTROL_CONTROLLER_DESCRIPTION);
+ msg.SetLabel(controller->Description());
+ m_guiWindow->OnMessage(msg);
+ }
+}
+
+void CGUIControllerList::OnSelect(unsigned int controllerIndex)
+{
+ m_featureList->OnSelect(0);
+}
+
+void CGUIControllerList::ResetController(void)
+{
+ if (0 <= m_focusedController && m_focusedController < (int)m_controllers.size())
+ {
+ const std::string strControllerId = m_controllers[m_focusedController]->ID();
+
+ //! @todo Choose peripheral
+ // For now, ask the user if they would like to reset all peripherals
+ // "Reset controller profile"
+ // "Would you like to reset this controller profile for all devices?"
+ if (!CGUIDialogYesNo::ShowAndGetInput(35060, 35061))
+ return;
+
+ CServiceBroker::GetPeripherals().ResetButtonMaps(strControllerId);
+ }
+}
+
+void CGUIControllerList::OnEvent(const ADDON::AddonEvent& event)
+{
+ if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) || // also called on install,
+ typeid(event) == typeid(ADDON::AddonEvents::Disabled) || // not called on uninstall
+ typeid(event) == typeid(ADDON::AddonEvents::ReInstalled) ||
+ typeid(event) == typeid(ADDON::AddonEvents::UnInstalled))
+ {
+ CGUIMessage msg(GUI_MSG_REFRESH_LIST, m_guiWindow->GetID(), CONTROL_CONTROLLER_LIST);
+
+ // Focus installed add-on
+ if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) ||
+ typeid(event) == typeid(ADDON::AddonEvents::ReInstalled))
+ msg.SetStringParam(event.addonId);
+
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, m_guiWindow->GetID());
+ }
+}
+
+bool CGUIControllerList::RefreshControllers(void)
+{
+ // Get current controllers
+ CGameServices& gameServices = CServiceBroker::GetGameServices();
+ ControllerVector newControllers = gameServices.GetControllers();
+
+ // Filter by current game add-on
+ if (m_gameClient)
+ {
+ const CControllerTree& controllers = m_gameClient->Input().GetDefaultControllerTree();
+
+ auto ControllerNotAccepted = [&controllers](const ControllerPtr& controller) {
+ return !controllers.IsControllerAccepted(controller->ID());
+ };
+
+ if (!std::all_of(newControllers.begin(), newControllers.end(), ControllerNotAccepted))
+ newControllers.erase(
+ std::remove_if(newControllers.begin(), newControllers.end(), ControllerNotAccepted),
+ newControllers.end());
+ }
+
+ // Check for changes
+ std::set<std::string> oldControllerIds;
+ std::set<std::string> newControllerIds;
+
+ auto GetControllerID = [](const ControllerPtr& controller) { return controller->ID(); };
+
+ std::transform(m_controllers.begin(), m_controllers.end(),
+ std::inserter(oldControllerIds, oldControllerIds.begin()), GetControllerID);
+ std::transform(newControllers.begin(), newControllers.end(),
+ std::inserter(newControllerIds, newControllerIds.begin()), GetControllerID);
+
+ const bool bChanged = (oldControllerIds != newControllerIds);
+ if (bChanged)
+ {
+ m_controllers = std::move(newControllers);
+
+ // Sort add-ons, with default controller first
+ std::sort(m_controllers.begin(), m_controllers.end(),
+ [](const ControllerPtr& i, const ControllerPtr& j) {
+ if (i->ID() == DEFAULT_CONTROLLER_ID && j->ID() != DEFAULT_CONTROLLER_ID)
+ return true;
+ if (i->ID() != DEFAULT_CONTROLLER_ID && j->ID() == DEFAULT_CONTROLLER_ID)
+ return false;
+
+ return StringUtils::CompareNoCase(i->Layout().Label(), j->Layout().Label()) < 0;
+ });
+ }
+
+ return bChanged;
+}
+
+void CGUIControllerList::CleanupButtons(void)
+{
+ if (m_controllerList)
+ m_controllerList->ClearAll();
+}
diff --git a/xbmc/games/controllers/windows/GUIControllerList.h b/xbmc/games/controllers/windows/GUIControllerList.h
new file mode 100644
index 0000000..8292889
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIControllerList.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2014-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 "IConfigurationWindow.h"
+#include "addons/AddonEvents.h"
+#include "games/GameTypes.h"
+#include "games/controllers/ControllerTypes.h"
+
+#include <set>
+#include <string>
+
+class CGUIButtonControl;
+class CGUIControlGroupList;
+class CGUIWindow;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIControllerWindow;
+
+class CGUIControllerList : public IControllerList
+{
+public:
+ CGUIControllerList(CGUIWindow* window, IFeatureList* featureList, GameClientPtr gameClient);
+ ~CGUIControllerList() override { Deinitialize(); }
+
+ // implementation of IControllerList
+ bool Initialize() override;
+ void Deinitialize() override;
+ bool Refresh(const std::string& controllerId) override;
+ void OnFocus(unsigned int controllerIndex) override;
+ void OnSelect(unsigned int controllerIndex) override;
+ int GetFocusedController() const override { return m_focusedController; }
+ void ResetController() override;
+
+private:
+ bool RefreshControllers(void);
+
+ void CleanupButtons(void);
+ void OnEvent(const ADDON::AddonEvent& event);
+
+ // GUI stuff
+ CGUIWindow* const m_guiWindow;
+ IFeatureList* const m_featureList;
+ CGUIControlGroupList* m_controllerList;
+ CGUIButtonControl* m_controllerButton;
+
+ // Game stuff
+ ControllerVector m_controllers;
+ int m_focusedController;
+ GameClientPtr m_gameClient;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/windows/GUIControllerWindow.cpp b/xbmc/games/controllers/windows/GUIControllerWindow.cpp
new file mode 100644
index 0000000..6b2d2ac
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIControllerWindow.cpp
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2014-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 "GUIControllerWindow.h"
+
+#include "GUIControllerDefines.h"
+#include "GUIControllerList.h"
+#include "GUIFeatureList.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h"
+#include "games/addons/GameClient.h"
+#include "games/controllers/dialogs/ControllerInstaller.h"
+#include "games/controllers/dialogs/GUIDialogIgnoreInput.h"
+#include "guilib/GUIButtonControl.h"
+#include "guilib/GUIControl.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+
+// To enable button mapping support
+#include "peripherals/Peripherals.h"
+
+using namespace KODI;
+using namespace GAME;
+using namespace KODI::MESSAGING;
+
+CGUIControllerWindow::CGUIControllerWindow(void)
+ : CGUIDialog(WINDOW_DIALOG_GAME_CONTROLLERS, "DialogGameControllers.xml"),
+ m_installer(new CControllerInstaller)
+{
+ // initialize CGUIWindow
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIControllerWindow::~CGUIControllerWindow(void)
+{
+ delete m_controllerList;
+ delete m_featureList;
+}
+
+void CGUIControllerWindow::DoProcess(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ /*
+ * Apply the faded focus texture to the current controller when unfocused
+ */
+
+ CGUIControl* control = nullptr; // The controller button
+ bool bAlphaFaded = false; // True if the controller button has been focused and faded this frame
+
+ if (m_controllerList && m_controllerList->GetFocusedController() >= 0)
+ {
+ control = GetFirstFocusableControl(CONTROL_CONTROLLER_BUTTONS_START +
+ m_controllerList->GetFocusedController());
+ if (control && !control->HasFocus())
+ {
+ if (control->GetControlType() == CGUIControl::GUICONTROL_BUTTON)
+ {
+ control->SetFocus(true);
+ static_cast<CGUIButtonControl*>(control)->SetAlpha(0x80);
+ bAlphaFaded = true;
+ }
+ }
+ }
+
+ CGUIDialog::DoProcess(currentTime, dirtyregions);
+
+ if (control && bAlphaFaded)
+ {
+ control->SetFocus(false);
+ if (control->GetControlType() == CGUIControl::GUICONTROL_BUTTON)
+ static_cast<CGUIButtonControl*>(control)->SetAlpha(0xFF);
+ }
+}
+
+bool CGUIControllerWindow::OnMessage(CGUIMessage& message)
+{
+ // Set to true to block the call to the super class
+ bool bHandled = false;
+
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int controlId = message.GetSenderId();
+
+ if (controlId == CONTROL_CLOSE_BUTTON)
+ {
+ Close();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_GET_MORE)
+ {
+ GetMoreControllers();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_GET_ALL)
+ {
+ GetAllControllers();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_RESET_BUTTON)
+ {
+ ResetController();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_HELP_BUTTON)
+ {
+ ShowHelp();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_FIX_SKIPPING)
+ {
+ ShowButtonCaptureDialog();
+ }
+ else if (CONTROL_CONTROLLER_BUTTONS_START <= controlId &&
+ controlId < CONTROL_CONTROLLER_BUTTONS_END)
+ {
+ OnControllerSelected(controlId - CONTROL_CONTROLLER_BUTTONS_START);
+ bHandled = true;
+ }
+ else if (CONTROL_FEATURE_BUTTONS_START <= controlId &&
+ controlId < CONTROL_FEATURE_BUTTONS_END)
+ {
+ OnFeatureSelected(controlId - CONTROL_FEATURE_BUTTONS_START);
+ bHandled = true;
+ }
+ break;
+ }
+ case GUI_MSG_FOCUSED:
+ {
+ int controlId = message.GetControlId();
+
+ if (CONTROL_CONTROLLER_BUTTONS_START <= controlId &&
+ controlId < CONTROL_CONTROLLER_BUTTONS_END)
+ {
+ OnControllerFocused(controlId - CONTROL_CONTROLLER_BUTTONS_START);
+ }
+ else if (CONTROL_FEATURE_BUTTONS_START <= controlId &&
+ controlId < CONTROL_FEATURE_BUTTONS_END)
+ {
+ OnFeatureFocused(controlId - CONTROL_FEATURE_BUTTONS_START);
+ }
+ break;
+ }
+ case GUI_MSG_SETFOCUS:
+ {
+ int controlId = message.GetControlId();
+
+ if (CONTROL_CONTROLLER_BUTTONS_START <= controlId &&
+ controlId < CONTROL_CONTROLLER_BUTTONS_END)
+ {
+ OnControllerFocused(controlId - CONTROL_CONTROLLER_BUTTONS_START);
+ }
+ else if (CONTROL_FEATURE_BUTTONS_START <= controlId &&
+ controlId < CONTROL_FEATURE_BUTTONS_END)
+ {
+ OnFeatureFocused(controlId - CONTROL_FEATURE_BUTTONS_START);
+ }
+ break;
+ }
+ case GUI_MSG_REFRESH_LIST:
+ {
+ int controlId = message.GetControlId();
+
+ if (controlId == CONTROL_CONTROLLER_LIST)
+ {
+ const std::string controllerId = message.GetStringParam();
+ if (m_controllerList && m_controllerList->Refresh(controllerId))
+ {
+ CGUIDialog::OnMessage(message);
+ bHandled = true;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (!bHandled)
+ bHandled = CGUIDialog::OnMessage(message);
+
+ return bHandled;
+}
+
+void CGUIControllerWindow::OnEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event)
+{
+ UpdateButtons();
+}
+
+void CGUIControllerWindow::OnEvent(const ADDON::AddonEvent& event)
+{
+ using namespace ADDON;
+
+ if (typeid(event) == typeid(AddonEvents::Enabled) || // also called on install,
+ typeid(event) == typeid(AddonEvents::Disabled) || // not called on uninstall
+ typeid(event) == typeid(AddonEvents::UnInstalled) ||
+ typeid(event) == typeid(AddonEvents::ReInstalled))
+ {
+ if (CServiceBroker::GetAddonMgr().HasType(event.addonId, AddonType::GAME_CONTROLLER))
+ {
+ UpdateButtons();
+ }
+ }
+}
+
+void CGUIControllerWindow::OnInitWindow(void)
+{
+ // Get active game add-on
+ GameClientPtr gameClient;
+ {
+ auto gameSettingsHandle = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+ if (gameSettingsHandle)
+ {
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(gameSettingsHandle->GameClientID(), addon,
+ ADDON::AddonType::GAMEDLL,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ gameClient = std::static_pointer_cast<CGameClient>(addon);
+ }
+ }
+ m_gameClient = std::move(gameClient);
+
+ CGUIDialog::OnInitWindow();
+
+ if (!m_featureList)
+ {
+ m_featureList = new CGUIFeatureList(this, m_gameClient);
+ if (!m_featureList->Initialize())
+ {
+ delete m_featureList;
+ m_featureList = nullptr;
+ }
+ }
+
+ if (!m_controllerList && m_featureList)
+ {
+ m_controllerList = new CGUIControllerList(this, m_featureList, m_gameClient);
+ if (!m_controllerList->Initialize())
+ {
+ delete m_controllerList;
+ m_controllerList = nullptr;
+ }
+ }
+
+ // Focus the first controller so that the feature list is loaded properly
+ CGUIMessage msgFocus(GUI_MSG_SETFOCUS, GetID(), CONTROL_CONTROLLER_BUTTONS_START);
+ OnMessage(msgFocus);
+
+ // Enable button mapping support
+ CServiceBroker::GetPeripherals().EnableButtonMapping();
+
+ UpdateButtons();
+
+ // subscribe to events
+ CServiceBroker::GetRepositoryUpdater().Events().Subscribe(this, &CGUIControllerWindow::OnEvent);
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CGUIControllerWindow::OnEvent);
+}
+
+void CGUIControllerWindow::OnDeinitWindow(int nextWindowID)
+{
+ CServiceBroker::GetRepositoryUpdater().Events().Unsubscribe(this);
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+
+ if (m_controllerList)
+ {
+ m_controllerList->Deinitialize();
+ delete m_controllerList;
+ m_controllerList = nullptr;
+ }
+
+ if (m_featureList)
+ {
+ m_featureList->Deinitialize();
+ delete m_featureList;
+ m_featureList = nullptr;
+ }
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ m_gameClient.reset();
+}
+
+void CGUIControllerWindow::OnControllerFocused(unsigned int controllerIndex)
+{
+ if (m_controllerList)
+ m_controllerList->OnFocus(controllerIndex);
+}
+
+void CGUIControllerWindow::OnControllerSelected(unsigned int controllerIndex)
+{
+ if (m_controllerList)
+ m_controllerList->OnSelect(controllerIndex);
+}
+
+void CGUIControllerWindow::OnFeatureFocused(unsigned int buttonIndex)
+{
+ if (m_featureList)
+ m_featureList->OnFocus(buttonIndex);
+}
+
+void CGUIControllerWindow::OnFeatureSelected(unsigned int buttonIndex)
+{
+ if (m_featureList)
+ m_featureList->OnSelect(buttonIndex);
+}
+
+void CGUIControllerWindow::UpdateButtons(void)
+{
+ using namespace ADDON;
+
+ VECADDONS addons;
+ if (m_gameClient)
+ {
+ SET_CONTROL_HIDDEN(CONTROL_GET_MORE);
+ SET_CONTROL_HIDDEN(CONTROL_GET_ALL);
+ }
+ else
+ {
+ const bool bEnable = CServiceBroker::GetAddonMgr().GetInstallableAddons(
+ addons, ADDON::AddonType::GAME_CONTROLLER) &&
+ !addons.empty();
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_GET_MORE, bEnable);
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_GET_ALL, bEnable);
+ }
+}
+
+void CGUIControllerWindow::GetMoreControllers(void)
+{
+ std::string strAddonId;
+ if (CGUIWindowAddonBrowser::SelectAddonID(ADDON::AddonType::GAME_CONTROLLER, strAddonId, false,
+ true, false, true, false) < 0)
+ {
+ // "Controller profiles"
+ // "All available controller profiles are installed."
+ HELPERS::ShowOKDialogText(CVariant{35050}, CVariant{35062});
+ return;
+ }
+}
+
+void CGUIControllerWindow::GetAllControllers()
+{
+ if (m_installer->IsRunning())
+ return;
+
+ m_installer->Create(false);
+}
+
+void CGUIControllerWindow::ResetController(void)
+{
+ if (m_controllerList)
+ m_controllerList->ResetController();
+}
+
+void CGUIControllerWindow::ShowHelp(void)
+{
+ // "Help"
+ // <help text>
+ HELPERS::ShowOKDialogText(CVariant{10043}, CVariant{35055});
+}
+
+void CGUIControllerWindow::ShowButtonCaptureDialog(void)
+{
+ CGUIDialogIgnoreInput dialog;
+ dialog.Show();
+}
diff --git a/xbmc/games/controllers/windows/GUIControllerWindow.h b/xbmc/games/controllers/windows/GUIControllerWindow.h
new file mode 100644
index 0000000..137fd56
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIControllerWindow.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2014-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 "addons/RepositoryUpdater.h"
+#include "games/GameTypes.h"
+#include "guilib/GUIDialog.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerInstaller;
+class IControllerList;
+class IFeatureList;
+
+class CGUIControllerWindow : public CGUIDialog
+{
+public:
+ CGUIControllerWindow(void);
+ ~CGUIControllerWindow() override;
+
+ // implementation of CGUIControl via CGUIDialog
+ void DoProcess(unsigned int currentTime, CDirtyRegionList& dirtyregions) override;
+ bool OnMessage(CGUIMessage& message) override;
+
+protected:
+ // implementation of CGUIWindow via CGUIDialog
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+private:
+ void OnControllerFocused(unsigned int controllerIndex);
+ void OnControllerSelected(unsigned int controllerIndex);
+ void OnFeatureFocused(unsigned int featureIndex);
+ void OnFeatureSelected(unsigned int featureIndex);
+ void UpdateButtons(void);
+
+ // Callbacks for events
+ void OnEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event);
+ void OnEvent(const ADDON::AddonEvent& event);
+
+ // Action for the available button
+ void GetMoreControllers(void);
+ void GetAllControllers();
+ void ResetController(void);
+ void ShowHelp(void);
+ void ShowButtonCaptureDialog(void);
+
+ IControllerList* m_controllerList = nullptr;
+ IFeatureList* m_featureList = nullptr;
+
+ // Game parameters
+ GameClientPtr m_gameClient;
+
+ // Controller parameters
+ std::unique_ptr<CControllerInstaller> m_installer;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/windows/GUIFeatureList.cpp b/xbmc/games/controllers/windows/GUIFeatureList.cpp
new file mode 100644
index 0000000..d43c16d
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIFeatureList.cpp
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2014-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 "GUIFeatureList.h"
+
+#include "GUIConfigurationWizard.h"
+#include "GUIControllerDefines.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/guicontrols/GUIFeatureButton.h"
+#include "games/controllers/guicontrols/GUIFeatureControls.h"
+#include "games/controllers/guicontrols/GUIFeatureFactory.h"
+#include "games/controllers/guicontrols/GUIFeatureTranslator.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "guilib/GUIButtonControl.h"
+#include "guilib/GUIControlGroupList.h"
+#include "guilib/GUIImage.h"
+#include "guilib/GUILabelControl.h"
+#include "guilib/GUIWindow.h"
+#include "guilib/LocalizeStrings.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIFeatureList::CGUIFeatureList(CGUIWindow* window, GameClientPtr gameClient)
+ : m_window(window),
+ m_guiList(nullptr),
+ m_guiButtonTemplate(nullptr),
+ m_guiGroupTitle(nullptr),
+ m_guiFeatureSeparator(nullptr),
+ m_gameClient(std::move(gameClient)),
+ m_wizard(new CGUIConfigurationWizard)
+{
+}
+
+CGUIFeatureList::~CGUIFeatureList(void)
+{
+ Deinitialize();
+ delete m_wizard;
+}
+
+bool CGUIFeatureList::Initialize(void)
+{
+ m_guiList = dynamic_cast<CGUIControlGroupList*>(m_window->GetControl(CONTROL_FEATURE_LIST));
+ m_guiButtonTemplate =
+ dynamic_cast<CGUIButtonControl*>(m_window->GetControl(CONTROL_FEATURE_BUTTON_TEMPLATE));
+ m_guiGroupTitle =
+ dynamic_cast<CGUILabelControl*>(m_window->GetControl(CONTROL_FEATURE_GROUP_TITLE));
+ m_guiFeatureSeparator = dynamic_cast<CGUIImage*>(m_window->GetControl(CONTROL_FEATURE_SEPARATOR));
+
+ if (m_guiButtonTemplate)
+ m_guiButtonTemplate->SetVisible(false);
+
+ if (m_guiGroupTitle)
+ m_guiGroupTitle->SetVisible(false);
+
+ if (m_guiFeatureSeparator)
+ m_guiFeatureSeparator->SetVisible(false);
+
+ return m_guiList != nullptr && m_guiButtonTemplate != nullptr;
+}
+
+void CGUIFeatureList::Deinitialize(void)
+{
+ CleanupButtons();
+
+ m_guiList = nullptr;
+ m_guiButtonTemplate = nullptr;
+ m_guiGroupTitle = nullptr;
+ m_guiFeatureSeparator = nullptr;
+}
+
+void CGUIFeatureList::Load(const ControllerPtr& controller)
+{
+ if (m_controller && m_controller->ID() == controller->ID())
+ return; // Already loaded
+
+ CleanupButtons();
+
+ // Set new controller
+ m_controller = controller;
+
+ // Get features
+ const std::vector<CPhysicalFeature>& features = controller->Features();
+
+ // Split into groups
+ auto featureGroups = GetFeatureGroups(features);
+
+ // Create controls
+ m_buttonCount = 0;
+ for (auto itGroup = featureGroups.begin(); itGroup != featureGroups.end(); ++itGroup)
+ {
+ const std::string& groupName = itGroup->groupName;
+ const bool bIsVirtualKey = itGroup->bIsVirtualKey;
+
+ std::vector<CGUIButtonControl*> buttons;
+
+ // Create buttons
+ if (bIsVirtualKey)
+ {
+ CGUIButtonControl* button = GetSelectKeyButton(itGroup->features, m_buttonCount);
+ if (button != nullptr)
+ buttons.push_back(button);
+ }
+ else
+ {
+ buttons = GetButtons(itGroup->features, m_buttonCount);
+ }
+
+ // Just in case
+ if (m_buttonCount + buttons.size() >= MAX_FEATURE_COUNT)
+ break;
+
+ // Add a separator if the group list isn't empty
+ if (m_guiFeatureSeparator && m_guiList->GetTotalSize() > 0)
+ {
+ CGUIFeatureSeparator* pSeparator =
+ new CGUIFeatureSeparator(*m_guiFeatureSeparator, m_buttonCount);
+ m_guiList->AddControl(pSeparator);
+ }
+
+ // Add the group title
+ if (m_guiGroupTitle && !groupName.empty())
+ {
+ CGUIFeatureGroupTitle* pGroupTitle =
+ new CGUIFeatureGroupTitle(*m_guiGroupTitle, groupName, m_buttonCount);
+ m_guiList->AddControl(pGroupTitle);
+ }
+
+ // Add the buttons
+ for (CGUIButtonControl* pButton : buttons)
+ m_guiList->AddControl(pButton);
+
+ m_buttonCount += static_cast<unsigned int>(buttons.size());
+ }
+}
+
+void CGUIFeatureList::OnSelect(unsigned int buttonIndex)
+{
+ // Generate list of buttons for the wizard
+ std::vector<IFeatureButton*> buttons;
+ for (; buttonIndex < m_buttonCount; buttonIndex++)
+ {
+ IFeatureButton* control = GetButtonControl(buttonIndex);
+ if (control == nullptr)
+ continue;
+
+ if (control->AllowWizard())
+ buttons.push_back(control);
+ else
+ {
+ // Only map this button if it's the only one
+ if (buttons.empty())
+ buttons.push_back(control);
+ break;
+ }
+ }
+
+ m_wizard->Run(m_controller->ID(), buttons);
+}
+
+IFeatureButton* CGUIFeatureList::GetButtonControl(unsigned int buttonIndex)
+{
+ CGUIControl* control = m_guiList->GetControl(CONTROL_FEATURE_BUTTONS_START + buttonIndex);
+
+ return static_cast<IFeatureButton*>(dynamic_cast<CGUIFeatureButton*>(control));
+}
+
+void CGUIFeatureList::CleanupButtons(void)
+{
+ m_buttonCount = 0;
+
+ m_wizard->Abort(true);
+ m_wizard->UnregisterKeys();
+
+ if (m_guiList)
+ m_guiList->ClearAll();
+}
+
+std::vector<CGUIFeatureList::FeatureGroup> CGUIFeatureList::GetFeatureGroups(
+ const std::vector<CPhysicalFeature>& features) const
+{
+ std::vector<FeatureGroup> groups;
+
+ // Get group names
+ std::vector<std::string> groupNames;
+ for (const CPhysicalFeature& feature : features)
+ {
+ // Skip features not supported by the game client
+ if (m_gameClient)
+ {
+ if (!m_gameClient->Input().HasFeature(m_controller->ID(), feature.Name()))
+ continue;
+ }
+
+ bool bAdded = false;
+
+ if (!groups.empty())
+ {
+ FeatureGroup& previousGroup = *groups.rbegin();
+ if (feature.CategoryLabel() == previousGroup.groupName)
+ {
+ // Add feature to previous group
+ previousGroup.features.emplace_back(feature);
+ bAdded = true;
+
+ // If feature is a key, add it to the preceding virtual group as well
+ if (feature.Category() == JOYSTICK::FEATURE_CATEGORY::KEY && groups.size() >= 2)
+ {
+ FeatureGroup& virtualGroup = *(groups.rbegin() + 1);
+ if (virtualGroup.bIsVirtualKey)
+ virtualGroup.features.emplace_back(feature);
+ }
+ }
+ }
+
+ if (!bAdded)
+ {
+ // If feature is a key, create a virtual group that allows the user to
+ // select which key to map
+ if (feature.Category() == JOYSTICK::FEATURE_CATEGORY::KEY)
+ {
+ FeatureGroup virtualGroup;
+ virtualGroup.groupName = g_localizeStrings.Get(35166); // "All keys"
+ virtualGroup.bIsVirtualKey = true;
+ virtualGroup.features.emplace_back(feature);
+ groups.emplace_back(std::move(virtualGroup));
+ }
+
+ // Create new group and add feature
+ FeatureGroup group;
+ group.groupName = feature.CategoryLabel();
+ group.features.emplace_back(feature);
+ groups.emplace_back(std::move(group));
+ }
+ }
+
+ // If there are no features, add an empty group
+ if (groups.empty())
+ {
+ FeatureGroup group;
+ group.groupName = g_localizeStrings.Get(35022); // "Nothing to map"
+ groups.emplace_back(std::move(group));
+ }
+
+ return groups;
+}
+
+bool CGUIFeatureList::HasButton(JOYSTICK::FEATURE_TYPE type) const
+{
+ return CGUIFeatureTranslator::GetButtonType(type) != BUTTON_TYPE::UNKNOWN;
+}
+
+std::vector<CGUIButtonControl*> CGUIFeatureList::GetButtons(
+ const std::vector<CPhysicalFeature>& features, unsigned int startIndex)
+{
+ std::vector<CGUIButtonControl*> buttons;
+
+ // Create buttons
+ unsigned int buttonIndex = startIndex;
+ for (const CPhysicalFeature& feature : features)
+ {
+ BUTTON_TYPE buttonType = CGUIFeatureTranslator::GetButtonType(feature.Type());
+
+ CGUIButtonControl* pButton = CGUIFeatureFactory::CreateButton(buttonType, *m_guiButtonTemplate,
+ m_wizard, feature, buttonIndex);
+
+ // If successful, add button to result
+ if (pButton != nullptr)
+ {
+ buttons.push_back(pButton);
+ buttonIndex++;
+ }
+ }
+
+ return buttons;
+}
+
+CGUIButtonControl* CGUIFeatureList::GetSelectKeyButton(
+ const std::vector<CPhysicalFeature>& features, unsigned int buttonIndex)
+{
+ // Expose keycodes to the wizard
+ for (const CPhysicalFeature& feature : features)
+ {
+ if (feature.Type() == JOYSTICK::FEATURE_TYPE::KEY)
+ m_wizard->RegisterKey(feature);
+ }
+
+ return CGUIFeatureFactory::CreateButton(BUTTON_TYPE::SELECT_KEY, *m_guiButtonTemplate, m_wizard,
+ CPhysicalFeature(), buttonIndex);
+}
diff --git a/xbmc/games/controllers/windows/GUIFeatureList.h b/xbmc/games/controllers/windows/GUIFeatureList.h
new file mode 100644
index 0000000..c7df54f
--- /dev/null
+++ b/xbmc/games/controllers/windows/GUIFeatureList.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014-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 "IConfigurationWindow.h"
+#include "games/GameTypes.h"
+#include "games/controllers/ControllerTypes.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "input/joysticks/JoystickTypes.h"
+
+class CGUIButtonControl;
+class CGUIControlGroupList;
+class CGUIImage;
+class CGUILabelControl;
+class CGUIWindow;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIFeatureList : public IFeatureList
+{
+public:
+ CGUIFeatureList(CGUIWindow* window, GameClientPtr gameClient);
+ ~CGUIFeatureList() override;
+
+ // implementation of IFeatureList
+ bool Initialize() override;
+ void Deinitialize() override;
+ bool HasButton(JOYSTICK::FEATURE_TYPE type) const override;
+ void Load(const ControllerPtr& controller) override;
+ void OnFocus(unsigned int buttonIndex) override {}
+ void OnSelect(unsigned int buttonIndex) override;
+
+private:
+ IFeatureButton* GetButtonControl(unsigned int buttonIndex);
+
+ void CleanupButtons(void);
+
+ // Helper functions
+ struct FeatureGroup
+ {
+ std::string groupName;
+ std::vector<CPhysicalFeature> features;
+ /*!
+ * True if this group is a button that allows the user to map a key of
+ * their choosing.
+ */
+ bool bIsVirtualKey = false;
+ };
+ std::vector<FeatureGroup> GetFeatureGroups(const std::vector<CPhysicalFeature>& features) const;
+ std::vector<CGUIButtonControl*> GetButtons(const std::vector<CPhysicalFeature>& features,
+ unsigned int startIndex);
+ CGUIButtonControl* GetSelectKeyButton(const std::vector<CPhysicalFeature>& features,
+ unsigned int buttonIndex);
+
+ // GUI stuff
+ CGUIWindow* const m_window;
+ unsigned int m_buttonCount = 0;
+ CGUIControlGroupList* m_guiList;
+ CGUIButtonControl* m_guiButtonTemplate;
+ CGUILabelControl* m_guiGroupTitle;
+ CGUIImage* m_guiFeatureSeparator;
+
+ // Game window stuff
+ GameClientPtr m_gameClient;
+ ControllerPtr m_controller;
+ IConfigurationWizard* m_wizard;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/controllers/windows/IConfigurationWindow.h b/xbmc/games/controllers/windows/IConfigurationWindow.h
new file mode 100644
index 0000000..017adf0
--- /dev/null
+++ b/xbmc/games/controllers/windows/IConfigurationWindow.h
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2014-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 "games/controllers/ControllerTypes.h"
+#include "input/InputTypes.h"
+#include "input/joysticks/JoystickTypes.h"
+
+#include <string>
+#include <vector>
+
+class CEvent;
+
+/*!
+ * \brief Controller configuration window
+ *
+ * The configuration window presents a list of controllers. Also on the screen
+ * is a list of features belonging to that controller.
+ *
+ * The configuration utility reacts to several events:
+ *
+ * 1) When a controller is focused, the feature list is populated with the
+ * controller's features.
+ *
+ * 2) When a feature is selected, the user is prompted for controller input.
+ * This initiates a "wizard" that walks the user through the subsequent
+ * features.
+ *
+ * 3) When the wizard's active feature loses focus, the wizard is cancelled
+ * and the prompt for input ends.
+ */
+
+namespace KODI
+{
+namespace GAME
+{
+class CPhysicalFeature;
+
+/*!
+ * \brief A list populated by installed controllers
+ */
+class IControllerList
+{
+public:
+ virtual ~IControllerList() = default;
+
+ /*!
+ * \brief Initialize the resource
+ * \return true if the resource is initialized and can be used
+ * false if the resource failed to initialize and must not be used
+ */
+ virtual bool Initialize(void) = 0;
+
+ /*!
+ * \brief Deinitialize the resource
+ */
+ virtual void Deinitialize(void) = 0;
+
+ /*!
+ * \brief Refresh the contents of the list
+ * \param controllerId The controller to focus, or empty to leave focus unchanged
+ * \return True if the list was changed
+ */
+ virtual bool Refresh(const std::string& controllerId) = 0;
+
+ /*
+ * \brief The specified controller has been focused
+ * \param controllerIndex The index of the controller being focused
+ */
+ virtual void OnFocus(unsigned int controllerIndex) = 0;
+
+ /*!
+ * \brief The specified controller has been selected
+ * \param controllerIndex The index of the controller being selected
+ */
+ virtual void OnSelect(unsigned int controllerIndex) = 0;
+
+ /*!
+ * \brief Get the index of the focused controller
+ * \return The index of the focused controller, or -1 if no controller has been focused yet
+ */
+ virtual int GetFocusedController() const = 0;
+
+ /*!
+ * \brief Reset the focused controller
+ */
+ virtual void ResetController(void) = 0;
+};
+
+/*!
+ * \brief A list populated by the controller's features
+ */
+class IFeatureList
+{
+public:
+ virtual ~IFeatureList() = default;
+
+ /*!
+ * \brief Initialize the resource
+ * \return true if the resource is initialized and can be used
+ * false if the resource failed to initialize and must not be used
+ */
+ virtual bool Initialize(void) = 0;
+
+ /*!
+ * \brief Deinitialize the resource
+ * \remark This must be called if Initialize() returned true
+ */
+ virtual void Deinitialize(void) = 0;
+
+ /*!
+ * \brief Check if the feature type has any buttons in the GUI
+ * \param The type of the feature being added to the GUI
+ * \return True if the type is support, false otherwise
+ */
+ virtual bool HasButton(JOYSTICK::FEATURE_TYPE type) const = 0;
+
+ /*!
+ * \brief Load the features for the specified controller
+ * \param controller The controller to load
+ */
+ virtual void Load(const ControllerPtr& controller) = 0;
+
+ /*!
+ * \brief Focus has been set to the specified GUI button
+ * \param buttonIndex The index of the button being focused
+ */
+ virtual void OnFocus(unsigned int buttonIndex) = 0;
+
+ /*!
+ * \brief The specified GUI button has been selected
+ * \param buttonIndex The index of the button being selected
+ */
+ virtual void OnSelect(unsigned int buttonIndex) = 0;
+};
+
+/*!
+ * \brief A GUI button in a feature list
+ */
+class IFeatureButton
+{
+public:
+ virtual ~IFeatureButton() = default;
+
+ /*!
+ * \brief Get the feature represented by this button
+ */
+ virtual const CPhysicalFeature& Feature(void) const = 0;
+
+ /*!
+ * \brief Allow the wizard to include this feature in a list of buttons
+ * to map
+ */
+ virtual bool AllowWizard() const { return true; }
+
+ /*!
+ * \brief Prompt the user for a single input element
+ * \param waitEvent The event to block on while prompting for input
+ * \return true if input was received (event fired), false if the prompt timed out
+ *
+ * After the button has finished prompting the user for all the input
+ * elements it requires, this will return false until Reset() is called.
+ */
+ virtual bool PromptForInput(CEvent& waitEvent) = 0;
+
+ /*!
+ * \brief Check if the button supports further calls to PromptForInput()
+ * \return true if the button requires no more input elements from the user
+ */
+ virtual bool IsFinished(void) const = 0;
+
+ /*!
+ * \brief Get the direction of the next analog stick or relative pointer
+ * prompt
+ * \return The next direction to be prompted, or UNKNOWN if this isn't a
+ * cardinal feature or the prompt is finished
+ */
+ virtual INPUT::CARDINAL_DIRECTION GetCardinalDirection(void) const = 0;
+
+ /*!
+ * \brief Get the direction of the next wheel prompt
+ * \return The next direction to be prompted, or UNKNOWN if this isn't a
+ * wheel or the prompt is finished
+ */
+ virtual JOYSTICK::WHEEL_DIRECTION GetWheelDirection(void) const = 0;
+
+ /*!
+ * \brief Get the direction of the next throttle prompt
+ * \return The next direction to be prompted, or UNKNOWN if this isn't a
+ * throttle or the prompt is finished
+ */
+ virtual JOYSTICK::THROTTLE_DIRECTION GetThrottleDirection(void) const = 0;
+
+ /*!
+ * \brief True if the button is waiting for a key press
+ */
+ virtual bool NeedsKey() const { return false; }
+
+ /*!
+ * \brief Set the pressed key that the user will be prompted to map
+ *
+ * \param key The key that was pressed
+ */
+ virtual void SetKey(const CPhysicalFeature& key) {}
+
+ /*!
+ * \brief Reset button after prompting for input has finished
+ */
+ virtual void Reset(void) = 0;
+};
+
+/*!
+ * \brief A wizard to direct user input
+ */
+class IConfigurationWizard
+{
+public:
+ virtual ~IConfigurationWizard() = default;
+
+ /*!
+ * \brief Start the wizard for the specified buttons
+ * \param controllerId The controller ID being mapped
+ * \param buttons The buttons to map
+ */
+ virtual void Run(const std::string& strControllerId,
+ const std::vector<IFeatureButton*>& buttons) = 0;
+
+ /*!
+ * \brief Callback for feature losing focus
+ * \param button The feature button losing focus
+ */
+ virtual void OnUnfocus(IFeatureButton* button) = 0;
+
+ /*!
+ * \brief Abort a running wizard
+ * \param bWait True if the call should block until the wizard is fully aborted
+ * \return true if aborted, or false if the wizard wasn't running
+ */
+ virtual bool Abort(bool bWait = true) = 0;
+
+ /*!
+ * \brief Register a key by its keycode
+ * \param key A key with a valid keycode
+ *
+ * This should be called before Run(). It allows the user to choose a key
+ * to map instead of scrolling through a long list.
+ */
+ virtual void RegisterKey(const CPhysicalFeature& key) = 0;
+
+ /*!
+ * \brief Unregister all registered keys
+ */
+ virtual void UnregisterKeys() = 0;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/CMakeLists.txt b/xbmc/games/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..d6c9a16
--- /dev/null
+++ b/xbmc/games/dialogs/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(SOURCES GUIDialogSelectGameClient.cpp
+ GUIDialogSelectSavestate.cpp
+)
+
+set(HEADERS DialogGameDefines.h
+ GUIDialogSelectGameClient.h
+ GUIDialogSelectSavestate.h
+)
+
+core_add_library(gamedialogs)
diff --git a/xbmc/games/dialogs/DialogGameDefines.h b/xbmc/games/dialogs/DialogGameDefines.h
new file mode 100644
index 0000000..60dc99d
--- /dev/null
+++ b/xbmc/games/dialogs/DialogGameDefines.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020-2021 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
+
+// Name of list item property for savestate captions
+constexpr auto SAVESTATE_LABEL = "savestate.label";
+constexpr auto SAVESTATE_CAPTION = "savestate.caption";
+constexpr auto SAVESTATE_GAME_CLIENT = "savestate.gameclient";
+
+// Control IDs for game dialogs
+constexpr unsigned int CONTROL_VIDEO_HEADING = 10810;
+constexpr unsigned int CONTROL_VIDEO_THUMBS = 10811;
+constexpr unsigned int CONTROL_VIDEO_DESCRIPTION = 10812;
+constexpr unsigned int CONTROL_SAVES_HEADING = 10820;
+constexpr unsigned int CONTROL_SAVES_DETAILED_LIST = 3; // Select dialog defaults to this control ID
+constexpr unsigned int CONTROL_SAVES_DESCRIPTION = 10822;
+constexpr unsigned int CONTROL_SAVES_EMULATOR_NAME = 10823;
+constexpr unsigned int CONTROL_SAVES_EMULATOR_ICON = 10824;
+constexpr unsigned int CONTROL_SAVES_NEW_BUTTON = 10825;
+constexpr unsigned int CONTROL_SAVES_CANCEL_BUTTON = 10826;
+constexpr unsigned int CONTROL_NUMBER_OF_ITEMS = 10827;
diff --git a/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp b/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp
new file mode 100644
index 0000000..e17bc27
--- /dev/null
+++ b/xbmc/games/dialogs/GUIDialogSelectGameClient.cpp
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016-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 "GUIDialogSelectGameClient.h"
+
+#include "FileItem.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "filesystem/AddonsDirectory.h"
+#include "games/addons/GameClient.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace KODI::MESSAGING;
+using namespace GAME;
+
+std::string CGUIDialogSelectGameClient::ShowAndGetGameClient(const std::string& gamePath,
+ const GameClientVector& candidates,
+ const GameClientVector& installable)
+{
+ std::string gameClient;
+
+ LogGameClients(candidates, installable);
+
+ std::string extension = URIUtils::GetExtension(gamePath);
+
+ // "Select emulator for {0:s}"
+ CGUIDialogSelect* dialog =
+ GetDialog(StringUtils::Format(g_localizeStrings.Get(35258), extension));
+ if (dialog != nullptr)
+ {
+ // Turn the addons into items
+ CFileItemList items;
+ CFileItemList installableItems;
+ for (const auto& candidate : candidates)
+ {
+ CFileItemPtr item(XFILE::CAddonsDirectory::FileItemFromAddon(candidate, candidate->ID()));
+ item->SetLabel2(g_localizeStrings.Get(35257)); // "Installed"
+ items.Add(std::move(item));
+ }
+ for (const auto& addon : installable)
+ {
+ CFileItemPtr item(XFILE::CAddonsDirectory::FileItemFromAddon(addon, addon->ID()));
+ installableItems.Add(std::move(item));
+ }
+ items.Sort(SortByLabel, SortOrderAscending);
+ installableItems.Sort(SortByLabel, SortOrderAscending);
+
+ items.Append(installableItems);
+
+ dialog->SetItems(items);
+
+ dialog->Open();
+
+ // If the "Get More" button has been pressed, show a list of installable addons
+ if (dialog->IsConfirmed())
+ {
+ int selectedIndex = dialog->GetSelectedItem();
+
+ if (0 <= selectedIndex && selectedIndex < items.Size())
+ {
+ gameClient = items[selectedIndex]->GetPath();
+
+ CLog::Log(LOGDEBUG, "Select game client dialog: User selected emulator {}", gameClient);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Select game client dialog: User selected invalid emulator {}",
+ selectedIndex);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Select game client dialog: User cancelled game client installation");
+ }
+ }
+
+ return gameClient;
+}
+
+CGUIDialogSelect* CGUIDialogSelectGameClient::GetDialog(const std::string& title)
+{
+ CGUIDialogSelect* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (dialog != nullptr)
+ {
+ dialog->Reset();
+ dialog->SetHeading(CVariant{title});
+ dialog->SetUseDetails(true);
+ }
+
+ return dialog;
+}
+
+void CGUIDialogSelectGameClient::LogGameClients(const GameClientVector& candidates,
+ const GameClientVector& installable)
+{
+ CLog::Log(LOGDEBUG, "Select game client dialog: Found {} candidates",
+ static_cast<unsigned int>(candidates.size()));
+ for (const auto& gameClient : candidates)
+ CLog::Log(LOGDEBUG, "Adding {} as a candidate", gameClient->ID());
+
+ if (!installable.empty())
+ {
+ CLog::Log(LOGDEBUG, "Select game client dialog: Found {} installable clients",
+ static_cast<unsigned int>(installable.size()));
+ for (const auto& gameClient : installable)
+ CLog::Log(LOGDEBUG, "Adding {} as an installable client", gameClient->ID());
+ }
+}
diff --git a/xbmc/games/dialogs/GUIDialogSelectGameClient.h b/xbmc/games/dialogs/GUIDialogSelectGameClient.h
new file mode 100644
index 0000000..c492481
--- /dev/null
+++ b/xbmc/games/dialogs/GUIDialogSelectGameClient.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016-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 "games/GameTypes.h"
+
+#include <string>
+
+class CGUIDialogSelect;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIDialogSelectGameClient
+{
+public:
+ /*!
+ * \brief Show a series of dialogs that results in a game client being
+ * selected
+ *
+ * \param gamePath The path of the file being played
+ * \param candidates A list of installed candidates that the user can
+ * select from
+ * \param installable A list of installable candidates that the user can
+ * select from
+ *
+ * \return The ID of the selected game client, or empty if no game client
+ * was selected
+ */
+ static std::string ShowAndGetGameClient(const std::string& gamePath,
+ const GameClientVector& candidates,
+ const GameClientVector& installable);
+
+private:
+ /*!
+ * \brief Get an initialized select dialog
+ *
+ * \param title The title of the select dialog
+ *
+ * \return A select dialog with its properties initialized, or nullptr if
+ * the dialog isn't found
+ */
+ static CGUIDialogSelect* GetDialog(const std::string& title);
+
+ /*!
+ * \brief Log the candidates and installable game clients
+ *
+ * Other than logging, this has no side effects.
+ */
+ static void LogGameClients(const GameClientVector& candidates,
+ const GameClientVector& installable);
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/GUIDialogSelectSavestate.cpp b/xbmc/games/dialogs/GUIDialogSelectSavestate.cpp
new file mode 100644
index 0000000..d56b1f4
--- /dev/null
+++ b/xbmc/games/dialogs/GUIDialogSelectSavestate.cpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020-2021 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 "GUIDialogSelectSavestate.h"
+
+#include "ServiceBroker.h"
+#include "games/dialogs/osd/DialogGameSaves.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+
+bool CGUIDialogSelectSavestate::ShowAndGetSavestate(const std::string& gamePath,
+ std::string& savestatePath)
+{
+ savestatePath = "";
+
+ // Can't ask the user if there's no dialog
+ CDialogGameSaves* dialog = GetDialog();
+ if (dialog == nullptr)
+ return true;
+
+ if (!dialog->Open(gamePath))
+ return true;
+
+ if (dialog->IsConfirmed())
+ {
+ savestatePath = dialog->GetSelectedItemPath();
+ return true;
+ }
+ else if (dialog->IsNewPressed())
+ {
+ CLog::Log(LOGDEBUG, "Select savestate dialog: New savestate selected");
+ return true;
+ }
+
+ // User canceled the dialog
+ return false;
+}
+
+CDialogGameSaves* CGUIDialogSelectSavestate::GetDialog()
+{
+ CDialogGameSaves* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CDialogGameSaves>(
+ WINDOW_DIALOG_GAME_SAVES);
+
+ if (dialog != nullptr)
+ dialog->Reset();
+
+ return dialog;
+}
diff --git a/xbmc/games/dialogs/GUIDialogSelectSavestate.h b/xbmc/games/dialogs/GUIDialogSelectSavestate.h
new file mode 100644
index 0000000..e554142
--- /dev/null
+++ b/xbmc/games/dialogs/GUIDialogSelectSavestate.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020-2021 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 <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameSaves;
+
+class CGUIDialogSelectSavestate
+{
+public:
+ static bool ShowAndGetSavestate(const std::string& gamePath, std::string& savestatePath);
+
+private:
+ static CDialogGameSaves* GetDialog();
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/CMakeLists.txt b/xbmc/games/dialogs/osd/CMakeLists.txt
new file mode 100644
index 0000000..3f7f5bd
--- /dev/null
+++ b/xbmc/games/dialogs/osd/CMakeLists.txt
@@ -0,0 +1,25 @@
+set(SOURCES DialogGameAdvancedSettings.cpp
+ DialogGameOSD.cpp
+ DialogGameOSDHelp.cpp
+ DialogGameSaves.cpp
+ DialogGameStretchMode.cpp
+ DialogGameVideoFilter.cpp
+ DialogGameVideoRotation.cpp
+ DialogGameVideoSelect.cpp
+ DialogGameVolume.cpp
+ DialogInGameSaves.cpp
+)
+
+set(HEADERS DialogGameAdvancedSettings.h
+ DialogGameOSD.h
+ DialogGameOSDHelp.h
+ DialogGameSaves.h
+ DialogGameStretchMode.h
+ DialogGameVideoFilter.h
+ DialogGameVideoRotation.h
+ DialogGameVideoSelect.h
+ DialogGameVolume.h
+ DialogInGameSaves.h
+)
+
+core_add_library(gameosddialogs)
diff --git a/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.cpp b/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.cpp
new file mode 100644
index 0000000..83b7a3f
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.cpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 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 "DialogGameAdvancedSettings.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/WindowIDs.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CDialogGameAdvancedSettings::CDialogGameAdvancedSettings()
+ : CGUIDialog(WINDOW_DIALOG_GAME_ADVANCED_SETTINGS, "")
+{
+}
+
+bool CDialogGameAdvancedSettings::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ auto gameSettingsHandle = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+ if (gameSettingsHandle)
+ {
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(gameSettingsHandle->GameClientID(), addon,
+ ADDON::AddonType::GAMEDLL,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ gameSettingsHandle.reset();
+ CGUIDialogAddonSettings::ShowForAddon(addon);
+ }
+ }
+
+ return false;
+ }
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.h b/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.h
new file mode 100644
index 0000000..00b669b
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameAdvancedSettings.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 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/GUIDialog.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameAdvancedSettings : public CGUIDialog
+{
+public:
+ CDialogGameAdvancedSettings();
+ ~CDialogGameAdvancedSettings() override = default;
+
+ // implementation of CGUIControl via CGUIDialog
+ bool OnMessage(CGUIMessage& message) override;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameOSD.cpp b/xbmc/games/dialogs/osd/DialogGameOSD.cpp
new file mode 100644
index 0000000..3a0eb52
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameOSD.cpp
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017-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 "DialogGameOSD.h"
+
+#include "DialogGameOSDHelp.h"
+#include "ServiceBroker.h"
+#include "games/GameServices.h"
+#include "games/GameSettings.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CDialogGameOSD::CDialogGameOSD()
+ : CGUIDialog(WINDOW_DIALOG_GAME_OSD, "GameOSD.xml"), m_helpDialog(new CDialogGameOSDHelp(*this))
+{
+ // Initialize CGUIWindow
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+bool CDialogGameOSD::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_PARENT_DIR:
+ case ACTION_PREVIOUS_MENU:
+ case ACTION_NAV_BACK:
+ case ACTION_SHOW_OSD:
+ case ACTION_PLAYER_PLAY:
+ {
+ // Disable OSD help if visible
+ if (m_helpDialog->IsVisible() && CServiceBroker::IsServiceManagerUp())
+ {
+ GAME::CGameSettings& gameSettings = CServiceBroker::GetGameServices().GameSettings();
+ if (gameSettings.ShowOSDHelp())
+ {
+ gameSettings.SetShowOSDHelp(false);
+ return true;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnAction(action);
+}
+
+void CDialogGameOSD::OnInitWindow()
+{
+ // Init parent class
+ CGUIDialog::OnInitWindow();
+
+ // Init help dialog
+ m_helpDialog->OnInitWindow();
+}
+
+void CDialogGameOSD::OnDeinitWindow(int nextWindowID)
+{
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ if (CServiceBroker::IsServiceManagerUp())
+ {
+ GAME::CGameSettings& gameSettings = CServiceBroker::GetGameServices().GameSettings();
+ gameSettings.SetShowOSDHelp(false);
+ }
+}
+
+bool CDialogGameOSD::PlayInBackground(int dialogId)
+{
+ return dialogId == WINDOW_DIALOG_GAME_VOLUME;
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameOSD.h b/xbmc/games/dialogs/osd/DialogGameOSD.h
new file mode 100644
index 0000000..3a8127b
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameOSD.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017-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/GUIDialog.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameOSDHelp;
+
+class CDialogGameOSD : public CGUIDialog
+{
+public:
+ CDialogGameOSD();
+
+ ~CDialogGameOSD() override = default;
+
+ // Implementation of CGUIControl via CGUIDialog
+ bool OnAction(const CAction& action) override;
+
+ // Implementation of CGUIWindow via CGUIDialog
+ void OnDeinitWindow(int nextWindowID) override;
+
+ /*!
+ * \brief Decide if the game should play behind the given dialog
+ *
+ * If true, the game should be played at regular speed.
+ *
+ * \param dialog The current dialog
+ *
+ * \return True if the game should be played at regular speed behind the
+ * dialog, false otherwise
+ */
+ static bool PlayInBackground(int dialogId);
+
+protected:
+ // Implementation of CGUIWindow via CGUIDialog
+ void OnInitWindow() override;
+
+private:
+ std::unique_ptr<CDialogGameOSDHelp> m_helpDialog;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameOSDHelp.cpp b/xbmc/games/dialogs/osd/DialogGameOSDHelp.cpp
new file mode 100644
index 0000000..3152172
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameOSDHelp.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 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 "DialogGameOSDHelp.h"
+
+#include "DialogGameOSD.h"
+#include "ServiceBroker.h"
+#include "games/GameServices.h"
+#include "games/controllers/guicontrols/GUIGameController.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI;
+using namespace GAME;
+
+const int CDialogGameOSDHelp::CONTROL_ID_HELP_TEXT = 1101;
+const int CDialogGameOSDHelp::CONTROL_ID_GAME_CONTROLLER = 1102;
+
+CDialogGameOSDHelp::CDialogGameOSDHelp(CDialogGameOSD& dialog) : m_dialog(dialog)
+{
+}
+
+void CDialogGameOSDHelp::OnInitWindow()
+{
+ // Set help text
+ //! @todo Define Select + X combo elsewhere
+ // "Press {0:s} to open the menu."
+ std::string helpText = StringUtils::Format(g_localizeStrings.Get(35235), "Select + X");
+
+ CGUIMessage msg(GUI_MSG_LABEL_SET, WINDOW_DIALOG_GAME_OSD, CONTROL_ID_HELP_TEXT);
+ msg.SetLabel(helpText);
+ m_dialog.OnMessage(msg);
+
+ // Set controller
+ if (CServiceBroker::IsServiceManagerUp())
+ {
+ CGameServices& gameServices = CServiceBroker::GetGameServices();
+
+ //! @todo Define SNES controller elsewhere
+ ControllerPtr controller = gameServices.GetController("game.controller.snes");
+ if (controller)
+ {
+ //! @todo Activate controller for all game controller controls
+ CGUIGameController* guiController =
+ dynamic_cast<CGUIGameController*>(m_dialog.GetControl(CONTROL_ID_GAME_CONTROLLER));
+ if (guiController != nullptr)
+ guiController->ActivateController(controller);
+ }
+ }
+}
+
+bool CDialogGameOSDHelp::IsVisible()
+{
+ return IsVisible(CONTROL_ID_HELP_TEXT) || IsVisible(CONTROL_ID_GAME_CONTROLLER);
+}
+
+bool CDialogGameOSDHelp::IsVisible(int windowId)
+{
+ CGUIControl* control = m_dialog.GetControl(windowId);
+ if (control != nullptr)
+ return control->IsVisible();
+
+ return false;
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameOSDHelp.h b/xbmc/games/dialogs/osd/DialogGameOSDHelp.h
new file mode 100644
index 0000000..6153075
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameOSDHelp.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 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
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameOSD;
+
+class CDialogGameOSDHelp
+{
+public:
+ CDialogGameOSDHelp(CDialogGameOSD& dialog);
+
+ // Initialize help controls
+ void OnInitWindow();
+
+ // Check if any help controls are visible
+ bool IsVisible();
+
+private:
+ // Utility functions
+ bool IsVisible(int windowId);
+
+ // Construction parameters
+ CDialogGameOSD& m_dialog;
+
+ // Help control IDs
+ static const int CONTROL_ID_HELP_TEXT;
+ static const int CONTROL_ID_GAME_CONTROLLER;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameSaves.cpp b/xbmc/games/dialogs/osd/DialogGameSaves.cpp
new file mode 100644
index 0000000..90e7785
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameSaves.cpp
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2020-2021 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 "DialogGameSaves.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogOK.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "games/addons/GameClient.h"
+#include "games/dialogs/DialogGameDefines.h"
+#include "guilib/GUIBaseContainer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "input/Key.h"
+#include "utils/FileUtils.h"
+#include "utils/Variant.h"
+#include "view/GUIViewControl.h"
+#include "view/ViewState.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CDialogGameSaves::CDialogGameSaves()
+ : CGUIDialog(WINDOW_DIALOG_GAME_SAVES, "DialogSelect.xml"),
+ m_viewControl(std::make_unique<CGUIViewControl>()),
+ m_vecList(std::make_unique<CFileItemList>())
+{
+}
+
+bool CDialogGameSaves::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ const int actionId = message.GetParam1();
+
+ switch (actionId)
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ int selectedId = m_viewControl->GetSelectedItem();
+ if (0 <= selectedId && selectedId < m_vecList->Size())
+ {
+ CFileItemPtr item = m_vecList->Get(selectedId);
+ if (item)
+ {
+ for (int i = 0; i < m_vecList->Size(); i++)
+ m_vecList->Get(i)->Select(false);
+
+ item->Select(true);
+
+ OnSelect(*item);
+
+ return true;
+ }
+ }
+ break;
+ }
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ {
+ int selectedItem = m_viewControl->GetSelectedItem();
+ if (selectedItem >= 0 && selectedItem < m_vecList->Size())
+ {
+ CFileItemPtr item = m_vecList->Get(selectedItem);
+ if (item)
+ {
+ OnContextMenu(*item);
+ return true;
+ }
+ }
+ break;
+ }
+ case ACTION_RENAME_ITEM:
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ int selectedItem = m_viewControl->GetSelectedItem();
+ if (selectedItem >= 0 && selectedItem < m_vecList->Size())
+ {
+ CFileItemPtr item = m_vecList->Get(selectedItem);
+ if (item)
+ {
+ OnRename(*item);
+ return true;
+ }
+ }
+ }
+ break;
+ }
+ case ACTION_DELETE_ITEM:
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ int selectedItem = m_viewControl->GetSelectedItem();
+ if (selectedItem >= 0 && selectedItem < m_vecList->Size())
+ {
+ CFileItemPtr item = m_vecList->Get(selectedItem);
+ if (item)
+ {
+ OnDelete(*item);
+ return true;
+ }
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ const int controlId = message.GetSenderId();
+ switch (controlId)
+ {
+ case CONTROL_SAVES_NEW_BUTTON:
+ {
+ m_bNewPressed = true;
+ Close();
+ break;
+ }
+ case CONTROL_SAVES_CANCEL_BUTTON:
+ {
+ m_selectedItem.reset();
+ m_vecList->Clear();
+ m_bConfirmed = false;
+ Close();
+ break;
+ }
+ default:
+ break;
+ }
+
+ break;
+ }
+
+ case GUI_MSG_SETFOCUS:
+ {
+ const int controlId = message.GetControlId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (m_vecList->IsEmpty())
+ {
+ SET_CONTROL_FOCUS(CONTROL_SAVES_NEW_BUTTON, 0);
+ return true;
+ }
+
+ if (m_viewControl->GetCurrentControl() != controlId)
+ {
+ m_viewControl->SetFocused();
+ return true;
+ }
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CDialogGameSaves::FrameMove()
+{
+ CGUIControl* itemContainer = GetControl(CONTROL_SAVES_DETAILED_LIST);
+ if (itemContainer != nullptr)
+ {
+ if (itemContainer->HasFocus())
+ {
+ int selectedItem = m_viewControl->GetSelectedItem();
+ if (selectedItem >= 0 && selectedItem < m_vecList->Size())
+ {
+ CFileItemPtr item = m_vecList->Get(selectedItem);
+ if (item)
+ OnFocus(*item);
+ }
+ }
+ else
+ {
+ OnFocusLost();
+ }
+ }
+
+ CGUIDialog::FrameMove();
+}
+
+void CDialogGameSaves::OnInitWindow()
+{
+ m_viewControl->SetItems(*m_vecList);
+ m_viewControl->SetCurrentView(CONTROL_SAVES_DETAILED_LIST);
+
+ CGUIDialog::OnInitWindow();
+
+ // Select the first item
+ m_viewControl->SetSelectedItem(0);
+
+ // There's a race condition where the item's focus sends the update message
+ // before the window is fully initialized, so explicitly set the info now.
+ if (!m_vecList->IsEmpty())
+ {
+ CFileItemPtr item = m_vecList->Get(0);
+ if (item)
+ {
+ const std::string gameClientId = item->GetProperty(SAVESTATE_GAME_CLIENT).asString();
+ if (!gameClientId.empty())
+ {
+ std::string emulatorName;
+ std::string emulatorIcon;
+
+ using namespace ADDON;
+
+ AddonPtr addon;
+ CAddonMgr& addonManager = CServiceBroker::GetAddonMgr();
+ if (addonManager.GetAddon(m_currentGameClient, addon, OnlyEnabled::CHOICE_NO))
+ {
+ std::shared_ptr<CGameClient> gameClient = std::dynamic_pointer_cast<CGameClient>(addon);
+ if (gameClient)
+ {
+ m_currentGameClient = gameClient->ID();
+
+ emulatorName = gameClient->GetEmulatorName();
+ emulatorIcon = gameClient->Icon();
+ }
+ }
+
+ if (!emulatorName.empty())
+ {
+ CGUIMessage message(GUI_MSG_LABEL_SET, GetID(), CONTROL_SAVES_EMULATOR_NAME);
+ message.SetLabel(emulatorName);
+ OnMessage(message);
+ }
+ if (!emulatorIcon.empty())
+ {
+ CGUIMessage message(GUI_MSG_SET_FILENAME, GetID(), CONTROL_SAVES_EMULATOR_ICON);
+ message.SetLabel(emulatorIcon);
+ OnMessage(message);
+ }
+ }
+
+ const std::string caption = item->GetProperty(SAVESTATE_CAPTION).asString();
+ if (!caption.empty())
+ {
+ m_currentCaption = caption;
+
+ CGUIMessage message(GUI_MSG_LABEL_SET, GetID(), CONTROL_SAVES_DESCRIPTION);
+ message.SetLabel(m_currentCaption);
+ OnMessage(message);
+ }
+ }
+ }
+}
+
+void CDialogGameSaves::OnDeinitWindow(int nextWindowID)
+{
+ m_viewControl->Clear();
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ // Get selected item
+ for (int i = 0; i < m_vecList->Size(); ++i)
+ {
+ CFileItemPtr item = m_vecList->Get(i);
+ if (item->IsSelected())
+ {
+ m_selectedItem = item;
+ break;
+ }
+ }
+
+ m_vecList->Clear();
+}
+
+void CDialogGameSaves::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+
+ m_viewControl->Reset();
+ m_viewControl->SetParentWindow(GetID());
+ m_viewControl->AddView(GetControl(CONTROL_SAVES_DETAILED_LIST));
+}
+
+void CDialogGameSaves::OnWindowUnload()
+{
+ CGUIDialog::OnWindowUnload();
+ m_viewControl->Reset();
+}
+
+void CDialogGameSaves::Reset()
+{
+ m_bConfirmed = false;
+ m_bNewPressed = false;
+
+ m_vecList->Clear();
+ m_selectedItem.reset();
+}
+
+bool CDialogGameSaves::Open(const std::string& gamePath)
+{
+ CFileItemList items;
+
+ RETRO::CSavestateDatabase db;
+ if (!db.GetSavestatesNav(items, gamePath))
+ return false;
+
+ if (items.IsEmpty())
+ return false;
+
+ items.Sort(SortByDate, SortOrderDescending);
+
+ SetItems(items);
+
+ CGUIDialog::Open();
+
+ return true;
+}
+
+std::string CDialogGameSaves::GetSelectedItemPath()
+{
+ if (m_selectedItem)
+ return m_selectedItem->GetPath();
+
+ return "";
+}
+
+void CDialogGameSaves::SetItems(const CFileItemList& itemList)
+{
+ m_vecList->Clear();
+
+ // Need to make internal copy of list to be sure dialog is owner of it
+ m_vecList->Copy(itemList);
+
+ m_viewControl->SetItems(*m_vecList);
+}
+
+void CDialogGameSaves::OnSelect(const CFileItem& item)
+{
+ m_bConfirmed = true;
+ Close();
+}
+
+void CDialogGameSaves::OnFocus(const CFileItem& item)
+{
+ const std::string caption = item.GetProperty(SAVESTATE_CAPTION).asString();
+ const std::string gameClientId = item.GetProperty(SAVESTATE_GAME_CLIENT).asString();
+
+ HandleCaption(caption);
+ HandleGameClient(gameClientId);
+}
+
+void CDialogGameSaves::OnFocusLost()
+{
+ HandleCaption("");
+ HandleGameClient("");
+}
+
+void CDialogGameSaves::OnContextMenu(CFileItem& item)
+{
+ CContextButtons buttons;
+
+ buttons.Add(0, 118); // "Rename"
+ buttons.Add(1, 117); // "Delete"
+
+ const int index = CGUIDialogContextMenu::Show(buttons);
+
+ if (index == 0)
+ OnRename(item);
+ else if (index == 1)
+ OnDelete(item);
+}
+
+void CDialogGameSaves::OnRename(CFileItem& item)
+{
+ const std::string& savestatePath = item.GetPath();
+
+ // Get savestate properties
+ RETRO::CSavestateDatabase db;
+ std::unique_ptr<RETRO::ISavestate> savestate = RETRO::CSavestateDatabase::AllocateSavestate();
+ db.GetSavestate(savestatePath, *savestate);
+
+ std::string label(savestate->Label());
+
+ // "Enter new filename"
+ if (CGUIKeyboardFactory::ShowAndGetInput(label, CVariant{g_localizeStrings.Get(16013)}, true) &&
+ label != savestate->Label())
+ {
+ std::unique_ptr<RETRO::ISavestate> newSavestate = db.RenameSavestate(savestatePath, label);
+ if (newSavestate)
+ {
+ RETRO::CSavestateDatabase::GetSavestateItem(*newSavestate, savestatePath, item);
+
+ // Refresh thumbnails
+ m_viewControl->SetItems(*m_vecList);
+ }
+ else
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+ }
+}
+
+void CDialogGameSaves::OnDelete(CFileItem& item)
+{
+ // "Confirm delete"
+ // "Would you like to delete the selected file(s)?[CR]Warning - this action can't be undone!"
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{122}, CVariant{125}))
+ {
+ const std::string& savestatePath = item.GetPath();
+
+ RETRO::CSavestateDatabase db;
+ if (db.DeleteSavestate(savestatePath))
+ {
+ m_vecList->Remove(&item);
+
+ // Refresh thumbnails
+ m_viewControl->SetItems(*m_vecList);
+ }
+ else
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+ }
+}
+
+void CDialogGameSaves::HandleCaption(const std::string& caption)
+{
+ if (caption != m_currentCaption)
+ {
+ m_currentCaption = caption;
+
+ // Update the GUI label
+ CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), CONTROL_SAVES_DESCRIPTION);
+ msg.SetLabel(m_currentCaption);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, GetID());
+ }
+}
+
+void CDialogGameSaves::HandleGameClient(const std::string& gameClientId)
+{
+ if (gameClientId == m_currentGameClient)
+ return;
+
+ m_currentGameClient = gameClientId;
+ if (m_currentGameClient.empty())
+ return;
+
+ // Get game client properties
+ std::shared_ptr<CGameClient> gameClient;
+ std::string emulatorName;
+ std::string iconPath;
+
+ using namespace ADDON;
+
+ AddonPtr addon;
+ CAddonMgr& addonManager = CServiceBroker::GetAddonMgr();
+ if (addonManager.GetAddon(m_currentGameClient, addon, OnlyEnabled::CHOICE_NO))
+ gameClient = std::dynamic_pointer_cast<CGameClient>(addon);
+
+ if (gameClient)
+ {
+ emulatorName = gameClient->GetEmulatorName();
+ iconPath = gameClient->Icon();
+ }
+
+ // Update the GUI elements
+ if (!emulatorName.empty())
+ {
+ CGUIMessage message(GUI_MSG_LABEL_SET, GetID(), CONTROL_SAVES_EMULATOR_NAME);
+ message.SetLabel(emulatorName);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message, GetID());
+ }
+ if (!iconPath.empty())
+ {
+ CGUIMessage message(GUI_MSG_SET_FILENAME, GetID(), CONTROL_SAVES_EMULATOR_ICON);
+ message.SetLabel(iconPath);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message, GetID());
+ }
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameSaves.h b/xbmc/games/dialogs/osd/DialogGameSaves.h
new file mode 100644
index 0000000..3605ee6
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameSaves.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020-2021 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/GUIDialog.h"
+
+#include <memory>
+#include <string>
+
+class CFileItem;
+class CFileItemList;
+class CGUIMessage;
+class CGUIViewControl;
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameSaves : public CGUIDialog
+{
+public:
+ CDialogGameSaves();
+ ~CDialogGameSaves() override = default;
+
+ // implementation of CGUIControl via CGUIDialog
+ bool OnMessage(CGUIMessage& message) override;
+
+ // implementation of CGUIWindow via CGUIDialog
+ void FrameMove() override;
+
+ // Player interface
+ void Reset();
+ bool Open(const std::string& gamePath);
+ bool IsConfirmed() const { return m_bConfirmed; }
+ bool IsNewPressed() const { return m_bNewPressed; }
+ std::string GetSelectedItemPath();
+
+protected:
+ // implementation of CGUIWIndow via CGUIDialog
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+
+private:
+ using CGUIControl::OnFocus;
+
+ /*!
+ * \breif Called when opening to set the item list
+ */
+ void SetItems(const CFileItemList& itemList);
+
+ /*!
+ * \brief Called when an item has been selected
+ */
+ void OnSelect(const CFileItem& item);
+
+ /*!
+ * \brief Called every frame with the item being focused
+ */
+ void OnFocus(const CFileItem& item);
+
+ /*!
+ * \brief Called every frame if no item is focused
+ */
+ void OnFocusLost();
+
+ /*!
+ * \brief Called when a context menu is opened for an item
+ */
+ void OnContextMenu(CFileItem& item);
+
+ /*!
+ * \brief Called when "Rename" is selected from the context menu
+ */
+ void OnRename(CFileItem& item);
+
+ /*!
+ * \brief Called when "Delete" is selected from the context menu
+ */
+ void OnDelete(CFileItem& item);
+
+ /*!
+ * \brief Called every frame with the caption to set
+ */
+ void HandleCaption(const std::string& caption);
+
+ /*!
+ * \brief Called every frame with the game client to set
+ */
+ void HandleGameClient(const std::string& gameClientId);
+
+ // Dialog parameters
+ std::unique_ptr<CGUIViewControl> m_viewControl;
+ std::unique_ptr<CFileItemList> m_vecList;
+ std::shared_ptr<CFileItem> m_selectedItem;
+
+ // Player parameters
+ bool m_bConfirmed{false};
+ bool m_bNewPressed{false};
+
+ // State parameters
+ std::string m_currentCaption;
+ std::string m_currentGameClient;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameStretchMode.cpp b/xbmc/games/dialogs/osd/DialogGameStretchMode.cpp
new file mode 100644
index 0000000..98c1e07
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameStretchMode.cpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2017-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 "DialogGameStretchMode.h"
+
+#include "FileItem.h"
+#include "cores/RetroPlayer/RetroPlayerUtils.h"
+#include "cores/RetroPlayer/guibridge/GUIGameVideoHandle.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "settings/GameSettings.h"
+#include "settings/MediaSettings.h"
+#include "utils/Variant.h"
+
+using namespace KODI;
+using namespace GAME;
+
+const std::vector<CDialogGameStretchMode::StretchModeProperties>
+ CDialogGameStretchMode::m_allStretchModes = {
+ {630, RETRO::STRETCHMODE::Normal},
+ // { 631, RETRO::STRETCHMODE::Zoom }, //! @todo RetroArch allows trimming some outer
+ // pixels
+ {632, RETRO::STRETCHMODE::Stretch4x3},
+ {35232, RETRO::STRETCHMODE::Fullscreen},
+ {635, RETRO::STRETCHMODE::Original},
+};
+
+CDialogGameStretchMode::CDialogGameStretchMode()
+ : CDialogGameVideoSelect(WINDOW_DIALOG_GAME_STRETCH_MODE)
+{
+}
+
+std::string CDialogGameStretchMode::GetHeading()
+{
+ return g_localizeStrings.Get(35233); // "Stretch mode"
+}
+
+void CDialogGameStretchMode::PreInit()
+{
+ m_stretchModes.clear();
+
+ for (const auto& stretchMode : m_allStretchModes)
+ {
+ bool bSupported = false;
+
+ switch (stretchMode.stretchMode)
+ {
+ case RETRO::STRETCHMODE::Normal:
+ case RETRO::STRETCHMODE::Original:
+ bSupported = true;
+ break;
+
+ case RETRO::STRETCHMODE::Stretch4x3:
+ case RETRO::STRETCHMODE::Fullscreen:
+ if (m_gameVideoHandle)
+ {
+ bSupported = m_gameVideoHandle->SupportsRenderFeature(RETRO::RENDERFEATURE::STRETCH) ||
+ m_gameVideoHandle->SupportsRenderFeature(RETRO::RENDERFEATURE::PIXEL_RATIO);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (bSupported)
+ m_stretchModes.emplace_back(stretchMode);
+ }
+}
+
+void CDialogGameStretchMode::GetItems(CFileItemList& items)
+{
+ for (const auto& stretchMode : m_stretchModes)
+ {
+ CFileItemPtr item = std::make_shared<CFileItem>(g_localizeStrings.Get(stretchMode.stringIndex));
+
+ const std::string stretchModeId =
+ RETRO::CRetroPlayerUtils::StretchModeToIdentifier(stretchMode.stretchMode);
+ if (!stretchModeId.empty())
+ item->SetProperty("game.stretchmode", CVariant{stretchModeId});
+ items.Add(std::move(item));
+ }
+}
+
+void CDialogGameStretchMode::OnItemFocus(unsigned int index)
+{
+ if (index < m_stretchModes.size())
+ {
+ const RETRO::STRETCHMODE stretchMode = m_stretchModes[index].stretchMode;
+
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+ if (gameSettings.StretchMode() != stretchMode)
+ {
+ gameSettings.SetStretchMode(stretchMode);
+ gameSettings.NotifyObservers(ObservableMessageSettingsChanged);
+ }
+ }
+}
+
+unsigned int CDialogGameStretchMode::GetFocusedItem() const
+{
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+
+ for (unsigned int i = 0; i < m_stretchModes.size(); i++)
+ {
+ const RETRO::STRETCHMODE stretchMode = m_stretchModes[i].stretchMode;
+ if (stretchMode == gameSettings.StretchMode())
+ return i;
+ }
+
+ return 0;
+}
+
+void CDialogGameStretchMode::PostExit()
+{
+ m_stretchModes.clear();
+}
+
+bool CDialogGameStretchMode::OnClickAction()
+{
+ Close();
+ return true;
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameStretchMode.h b/xbmc/games/dialogs/osd/DialogGameStretchMode.h
new file mode 100644
index 0000000..1e05061
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameStretchMode.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017-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 "DialogGameVideoSelect.h"
+#include "cores/GameSettings.h"
+
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameStretchMode : public CDialogGameVideoSelect
+{
+public:
+ CDialogGameStretchMode();
+ ~CDialogGameStretchMode() override = default;
+
+protected:
+ // implementation of CDialogGameVideoSelect
+ std::string GetHeading() override;
+ void PreInit() override;
+ void GetItems(CFileItemList& items) override;
+ void OnItemFocus(unsigned int index) override;
+ unsigned int GetFocusedItem() const override;
+ void PostExit() override;
+ bool OnClickAction() override;
+
+private:
+ struct StretchModeProperties
+ {
+ int stringIndex;
+ RETRO::STRETCHMODE stretchMode;
+ };
+
+ std::vector<StretchModeProperties> m_stretchModes;
+
+ /*!
+ * \brief The list of all the stretch modes along with their properties
+ */
+ static const std::vector<StretchModeProperties> m_allStretchModes;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoFilter.cpp b/xbmc/games/dialogs/osd/DialogGameVideoFilter.cpp
new file mode 100644
index 0000000..a5bba0b
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoFilter.cpp
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2017-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 "DialogGameVideoFilter.h"
+
+#include "cores/RetroPlayer/guibridge/GUIGameVideoHandle.h"
+#include "cores/RetroPlayer/rendering/RenderVideoSettings.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "settings/GameSettings.h"
+#include "settings/MediaSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+using namespace KODI;
+using namespace GAME;
+
+namespace
+{
+struct ScalingMethodProperties
+{
+ int nameIndex;
+ int categoryIndex;
+ int descriptionIndex;
+ RETRO::SCALINGMETHOD scalingMethod;
+};
+
+const std::vector<ScalingMethodProperties> scalingMethods = {
+ {16301, 16296, 16298, RETRO::SCALINGMETHOD::NEAREST},
+ {16302, 16297, 16299, RETRO::SCALINGMETHOD::LINEAR},
+};
+} // namespace
+
+CDialogGameVideoFilter::CDialogGameVideoFilter()
+ : CDialogGameVideoSelect(WINDOW_DIALOG_GAME_VIDEO_FILTER)
+{
+}
+
+std::string CDialogGameVideoFilter::GetHeading()
+{
+ return g_localizeStrings.Get(35225); // "Video filter"
+}
+
+void CDialogGameVideoFilter::PreInit()
+{
+ m_items.Clear();
+
+ InitVideoFilters();
+
+ if (m_items.Size() == 0)
+ {
+ CFileItemPtr item = std::make_shared<CFileItem>(g_localizeStrings.Get(231)); // "None"
+ m_items.Add(std::move(item));
+ }
+
+ m_bHasDescription = false;
+}
+
+void CDialogGameVideoFilter::InitVideoFilters()
+{
+ if (m_gameVideoHandle)
+ {
+ for (const auto& scalingMethodProps : scalingMethods)
+ {
+ if (m_gameVideoHandle->SupportsScalingMethod(scalingMethodProps.scalingMethod))
+ {
+ RETRO::CRenderVideoSettings videoSettings;
+ videoSettings.SetScalingMethod(scalingMethodProps.scalingMethod);
+
+ CFileItemPtr item =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(scalingMethodProps.nameIndex));
+ item->SetLabel2(g_localizeStrings.Get(scalingMethodProps.categoryIndex));
+ item->SetProperty("game.videofilter", CVariant{videoSettings.GetVideoFilter()});
+ item->SetProperty("game.videofilterdescription",
+ CVariant{g_localizeStrings.Get(scalingMethodProps.descriptionIndex)});
+ m_items.Add(std::move(item));
+ }
+ }
+ }
+}
+
+void CDialogGameVideoFilter::GetItems(CFileItemList& items)
+{
+ for (const auto& item : m_items)
+ items.Add(item);
+}
+
+void CDialogGameVideoFilter::OnItemFocus(unsigned int index)
+{
+ if (static_cast<int>(index) < m_items.Size())
+ {
+ CFileItemPtr item = m_items[index];
+
+ std::string videoFilter;
+ std::string description;
+ GetProperties(*item, videoFilter, description);
+
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+
+ if (gameSettings.VideoFilter() != videoFilter)
+ {
+ gameSettings.SetVideoFilter(videoFilter);
+ gameSettings.NotifyObservers(ObservableMessageSettingsChanged);
+
+ OnDescriptionChange(description);
+ m_bHasDescription = true;
+ }
+ else if (!m_bHasDescription)
+ {
+ OnDescriptionChange(description);
+ m_bHasDescription = true;
+ }
+ }
+}
+
+unsigned int CDialogGameVideoFilter::GetFocusedItem() const
+{
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+
+ for (int i = 0; i < m_items.Size(); i++)
+ {
+ std::string videoFilter;
+ std::string description;
+ GetProperties(*m_items[i], videoFilter, description);
+
+ if (videoFilter == gameSettings.VideoFilter())
+ {
+ return i;
+ }
+ }
+
+ return 0;
+}
+
+void CDialogGameVideoFilter::PostExit()
+{
+ m_items.Clear();
+}
+
+bool CDialogGameVideoFilter::OnClickAction()
+{
+ Close();
+ return true;
+}
+
+void CDialogGameVideoFilter::GetProperties(const CFileItem& item,
+ std::string& videoFilter,
+ std::string& description)
+{
+ videoFilter = item.GetProperty("game.videofilter").asString();
+ description = item.GetProperty("game.videofilterdescription").asString();
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoFilter.h b/xbmc/games/dialogs/osd/DialogGameVideoFilter.h
new file mode 100644
index 0000000..7ec8c76
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoFilter.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017-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 "DialogGameVideoSelect.h"
+#include "FileItem.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameVideoFilter : public CDialogGameVideoSelect
+{
+public:
+ CDialogGameVideoFilter();
+ ~CDialogGameVideoFilter() override = default;
+
+protected:
+ // implementation of CDialogGameVideoSelect
+ std::string GetHeading() override;
+ void PreInit() override;
+ void GetItems(CFileItemList& items) override;
+ void OnItemFocus(unsigned int index) override;
+ unsigned int GetFocusedItem() const override;
+ void PostExit() override;
+ bool OnClickAction() override;
+
+private:
+ void InitVideoFilters();
+
+ static void GetProperties(const CFileItem& item,
+ std::string& videoFilter,
+ std::string& description);
+
+ CFileItemList m_items;
+
+ //! \brief Set to true when a description has first been set
+ bool m_bHasDescription = false;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoRotation.cpp b/xbmc/games/dialogs/osd/DialogGameVideoRotation.cpp
new file mode 100644
index 0000000..7037a6b
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoRotation.cpp
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 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 "DialogGameVideoRotation.h"
+
+#include "FileItem.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "settings/GameSettings.h"
+#include "settings/MediaSettings.h"
+#include "utils/Variant.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CDialogGameVideoRotation::CDialogGameVideoRotation()
+ : CDialogGameVideoSelect(WINDOW_DIALOG_GAME_VIDEO_ROTATION)
+{
+}
+
+std::string CDialogGameVideoRotation::GetHeading()
+{
+ return g_localizeStrings.Get(35227); // "Rotation"
+}
+
+void CDialogGameVideoRotation::PreInit()
+{
+ m_rotations.clear();
+
+ // Present the user with clockwise rotation
+ m_rotations.push_back(0);
+ m_rotations.push_back(270);
+ m_rotations.push_back(180);
+ m_rotations.push_back(90);
+}
+
+void CDialogGameVideoRotation::GetItems(CFileItemList& items)
+{
+ for (unsigned int rotation : m_rotations)
+ {
+ CFileItemPtr item = std::make_shared<CFileItem>(GetRotationLabel(rotation));
+ item->SetProperty("game.videorotation", CVariant{rotation});
+ items.Add(std::move(item));
+ }
+}
+
+void CDialogGameVideoRotation::OnItemFocus(unsigned int index)
+{
+ if (index < m_rotations.size())
+ {
+ const unsigned int rotationDegCCW = m_rotations[index];
+
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+ if (gameSettings.RotationDegCCW() != rotationDegCCW)
+ {
+ gameSettings.SetRotationDegCCW(rotationDegCCW);
+ gameSettings.NotifyObservers(ObservableMessageSettingsChanged);
+ }
+ }
+}
+
+unsigned int CDialogGameVideoRotation::GetFocusedItem() const
+{
+ CGameSettings& gameSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+
+ for (unsigned int i = 0; i < m_rotations.size(); i++)
+ {
+ const unsigned int rotationDegCCW = m_rotations[i];
+ if (rotationDegCCW == gameSettings.RotationDegCCW())
+ return i;
+ }
+
+ return 0;
+}
+
+void CDialogGameVideoRotation::PostExit()
+{
+ m_rotations.clear();
+}
+
+bool CDialogGameVideoRotation::OnClickAction()
+{
+ Close();
+ return true;
+}
+
+std::string CDialogGameVideoRotation::GetRotationLabel(unsigned int rotationDegCCW)
+{
+ switch (rotationDegCCW)
+ {
+ case 0:
+ return g_localizeStrings.Get(35228); // 0
+ case 90:
+ return g_localizeStrings.Get(35231); // 270
+ case 180:
+ return g_localizeStrings.Get(35230); // 180
+ case 270:
+ return g_localizeStrings.Get(35229); // 90
+ default:
+ break;
+ }
+
+ return "";
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoRotation.h b/xbmc/games/dialogs/osd/DialogGameVideoRotation.h
new file mode 100644
index 0000000..ef454db
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoRotation.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 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 "DialogGameVideoSelect.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogGameVideoRotation : public CDialogGameVideoSelect
+{
+public:
+ CDialogGameVideoRotation();
+ ~CDialogGameVideoRotation() override = default;
+
+protected:
+ // implementation of CDialogGameVideoSelect
+ std::string GetHeading() override;
+ void PreInit() override;
+ void GetItems(CFileItemList& items) override;
+ void OnItemFocus(unsigned int index) override;
+ unsigned int GetFocusedItem() const override;
+ void PostExit() override;
+ bool OnClickAction() override;
+
+private:
+ // Helper functions
+ static std::string GetRotationLabel(unsigned int rotationDegCCW);
+
+ // Dialog parameters
+ std::vector<unsigned int> m_rotations; // Degrees counter-clockwise
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoSelect.cpp b/xbmc/games/dialogs/osd/DialogGameVideoSelect.cpp
new file mode 100644
index 0000000..a69b26f
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoSelect.cpp
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2017-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 "DialogGameVideoSelect.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIGameVideoHandle.h"
+#include "games/dialogs/DialogGameDefines.h"
+#include "guilib/GUIBaseContainer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/ActionIDs.h"
+#include "settings/GameSettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "view/GUIViewControl.h"
+#include "view/ViewState.h"
+#include "windowing/GraphicContext.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CDialogGameVideoSelect::CDialogGameVideoSelect(int windowId)
+ : CGUIDialog(windowId, "DialogSelect.xml"),
+ m_viewControl(new CGUIViewControl),
+ m_vecItems(new CFileItemList)
+{
+ // Initialize CGUIWindow
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CDialogGameVideoSelect::~CDialogGameVideoSelect() = default;
+
+bool CDialogGameVideoSelect::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ RegisterDialog();
+
+ // Don't init this dialog if we aren't playing a game
+ if (!m_gameVideoHandle || !m_gameVideoHandle->IsPlayingGame())
+ return false;
+
+ break;
+ }
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ UnregisterDialog();
+
+ break;
+ }
+ case GUI_MSG_SETFOCUS:
+ {
+ const int controlId = message.GetControlId();
+ if (m_viewControl->HasControl(controlId) && m_viewControl->GetCurrentControl() != controlId)
+ {
+ m_viewControl->SetFocused();
+ return true;
+ }
+ break;
+ }
+ case GUI_MSG_CLICKED:
+ {
+ const int actionId = message.GetParam1();
+ if (actionId == ACTION_SELECT_ITEM || actionId == ACTION_MOUSE_LEFT_CLICK)
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (OnClickAction())
+ return true;
+ }
+ }
+ else if (actionId == ACTION_CONTEXT_MENU || actionId == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (OnMenuAction())
+ return true;
+ }
+ }
+ else if (actionId == ACTION_CREATE_BOOKMARK)
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (OnOverwriteAction())
+ return true;
+ }
+ }
+ else if (actionId == ACTION_RENAME_ITEM)
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (OnRenameAction())
+ return true;
+ }
+ }
+ else if (actionId == ACTION_DELETE_ITEM)
+ {
+ const int controlId = message.GetSenderId();
+ if (m_viewControl->HasControl(controlId))
+ {
+ if (OnDeleteAction())
+ return true;
+ }
+ }
+
+ break;
+ }
+ case GUI_MSG_REFRESH_LIST:
+ {
+ RefreshList();
+ break;
+ }
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CDialogGameVideoSelect::FrameMove()
+{
+ CGUIBaseContainer* thumbs = dynamic_cast<CGUIBaseContainer*>(GetControl(CONTROL_VIDEO_THUMBS));
+ if (thumbs != nullptr)
+ OnItemFocus(thumbs->GetSelectedItem());
+
+ CGUIDialog::FrameMove();
+}
+
+void CDialogGameVideoSelect::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+
+ m_viewControl->SetParentWindow(GetID());
+ m_viewControl->AddView(GetControl(CONTROL_VIDEO_THUMBS));
+}
+
+void CDialogGameVideoSelect::OnWindowUnload()
+{
+ m_viewControl->Reset();
+
+ CGUIDialog::OnWindowUnload();
+}
+
+void CDialogGameVideoSelect::OnInitWindow()
+{
+ PreInit();
+
+ CGUIDialog::OnInitWindow();
+
+ Update();
+
+ CGUIMessage msg(GUI_MSG_SETFOCUS, GetID(), CONTROL_VIDEO_THUMBS);
+ OnMessage(msg);
+
+ std::string heading = GetHeading();
+ SET_CONTROL_LABEL(CONTROL_VIDEO_HEADING, heading);
+
+ FrameMove();
+}
+
+void CDialogGameVideoSelect::OnDeinitWindow(int nextWindowID)
+{
+ Clear();
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+
+ PostExit();
+
+ SaveSettings();
+}
+
+void CDialogGameVideoSelect::Update()
+{
+ //! @todo
+ // Lock our display, as this window is rendered from the player thread
+ // CServiceBroker::GetWinSystem()->GetGfxContext().Lock();
+
+ m_viewControl->SetCurrentView(DEFAULT_VIEW_ICONS);
+
+ // Empty the list ready for population
+ Clear();
+
+ RefreshList();
+
+ // CServiceBroker::GetWinSystem()->GetGfxContext().Unlock();
+}
+
+void CDialogGameVideoSelect::Clear()
+{
+ m_viewControl->Clear();
+ m_vecItems->Clear();
+}
+
+void CDialogGameVideoSelect::RefreshList()
+{
+ m_vecItems->Clear();
+
+ GetItems(*m_vecItems);
+
+ m_viewControl->SetItems(*m_vecItems);
+
+ auto focusedIndex = GetFocusedItem();
+ m_viewControl->SetSelectedItem(focusedIndex);
+ OnItemFocus(focusedIndex);
+
+ // Refresh the panel container
+ CGUIMessage message(GUI_MSG_REFRESH_THUMBS, GetID(), CONTROL_VIDEO_THUMBS);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message, GetID());
+}
+
+void CDialogGameVideoSelect::SaveSettings()
+{
+ CGameSettings& defaultSettings = CMediaSettings::GetInstance().GetDefaultGameSettings();
+ CGameSettings& currentSettings = CMediaSettings::GetInstance().GetCurrentGameSettings();
+
+ if (defaultSettings != currentSettings)
+ {
+ defaultSettings = currentSettings;
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ }
+}
+
+void CDialogGameVideoSelect::OnDescriptionChange(const std::string& description)
+{
+ CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), CONTROL_VIDEO_THUMBS);
+ msg.SetLabel(description);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, GetID());
+}
+
+void CDialogGameVideoSelect::RegisterDialog()
+{
+ m_gameVideoHandle = CServiceBroker::GetGameRenderManager().RegisterDialog(*this);
+}
+
+void CDialogGameVideoSelect::UnregisterDialog()
+{
+ m_gameVideoHandle.reset();
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameVideoSelect.h b/xbmc/games/dialogs/osd/DialogGameVideoSelect.h
new file mode 100644
index 0000000..c14c94e
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVideoSelect.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017-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/GUIDialog.h"
+
+#include <memory>
+
+class CFileItemList;
+class CGUIViewControl;
+
+namespace KODI
+{
+namespace RETRO
+{
+class CGUIGameVideoHandle;
+}
+
+namespace GAME
+{
+class CDialogGameVideoSelect : public CGUIDialog
+{
+public:
+ ~CDialogGameVideoSelect() override;
+
+ // implementation of CGUIControl via CGUIDialog
+ bool OnMessage(CGUIMessage& message) override;
+
+ // implementation of CGUIWindow via CGUIDialog
+ void FrameMove() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+protected:
+ CDialogGameVideoSelect(int windowId);
+
+ // implementation of CGUIWindow via CGUIDialog
+ void OnWindowUnload() override;
+ void OnWindowLoaded() override;
+ void OnInitWindow() override;
+
+ // Video select interface
+ virtual std::string GetHeading() = 0;
+ virtual void PreInit() = 0;
+ virtual void GetItems(CFileItemList& items) = 0;
+ virtual void OnItemFocus(unsigned int index) = 0;
+ virtual unsigned int GetFocusedItem() const = 0;
+ virtual void PostExit() = 0;
+ // override this to do something when an item is selected
+ virtual bool OnClickAction() { return false; }
+ // override this to do something when an item's context menu is opened
+ virtual bool OnMenuAction() { return false; }
+ // override this to do something when an item is overwritten with a new savestate
+ virtual bool OnOverwriteAction() { return false; }
+ // override this to do something when an item is renamed
+ virtual bool OnRenameAction() { return false; }
+ // override this to do something when an item is deleted
+ virtual bool OnDeleteAction() { return false; }
+
+ // GUI functions
+ void RefreshList();
+ void OnDescriptionChange(const std::string& description);
+
+ std::shared_ptr<RETRO::CGUIGameVideoHandle> m_gameVideoHandle;
+
+private:
+ void Update();
+ void Clear();
+
+ void SaveSettings();
+
+ void RegisterDialog();
+ void UnregisterDialog();
+
+ std::unique_ptr<CGUIViewControl> m_viewControl;
+ std::unique_ptr<CFileItemList> m_vecItems;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogGameVolume.cpp b/xbmc/games/dialogs/osd/DialogGameVolume.cpp
new file mode 100644
index 0000000..8467269
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVolume.cpp
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2017-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 "DialogGameVolume.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "dialogs/GUIDialogVolumeBar.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIDialog.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUISliderControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "utils/Variant.h"
+
+#include <cmath>
+
+using namespace KODI;
+using namespace GAME;
+
+#define CONTROL_LABEL 12 //! @todo Remove me
+
+CDialogGameVolume::CDialogGameVolume()
+{
+ // Initialize CGUIWindow
+ SetID(WINDOW_DIALOG_GAME_VOLUME);
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+bool CDialogGameVolume::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_STATE_CHANGED:
+ {
+ int controlId = message.GetControlId();
+ if (controlId == GetID())
+ {
+ OnStateChanged();
+ return true;
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+
+ return CGUIDialogSlider::OnMessage(message);
+}
+
+void CDialogGameVolume::OnInitWindow()
+{
+ m_volumePercent = m_oldVolumePercent = GetVolumePercent();
+
+ CGUIDialogSlider::OnInitWindow();
+
+ // Set slider parameters
+ SetModalityType(DialogModalityType::MODAL);
+ SetSlider(GetLabel(), GetVolumePercent(), VOLUME_MIN, VOLUME_DELTA, VOLUME_MAX, this, nullptr);
+
+ SET_CONTROL_HIDDEN(CONTROL_LABEL);
+
+ CGUIDialogVolumeBar* dialogVolumeBar = dynamic_cast<CGUIDialogVolumeBar*>(
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_DIALOG_VOLUME_BAR));
+ if (dialogVolumeBar != nullptr)
+ dialogVolumeBar->RegisterCallback(this);
+
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+void CDialogGameVolume::OnDeinitWindow(int nextWindowID)
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+
+ CGUIDialogVolumeBar* dialogVolumeBar = dynamic_cast<CGUIDialogVolumeBar*>(
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_DIALOG_VOLUME_BAR));
+ if (dialogVolumeBar != nullptr)
+ dialogVolumeBar->UnregisterCallback(this);
+
+ CGUIDialogSlider::OnDeinitWindow(nextWindowID);
+}
+
+void CDialogGameVolume::OnSliderChange(void* data, CGUISliderControl* slider)
+{
+ const float volumePercent = slider->GetFloatValue();
+
+ if (std::fabs(volumePercent - m_volumePercent) > 0.1f)
+ {
+ m_volumePercent = volumePercent;
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume(volumePercent, true);
+ }
+}
+
+bool CDialogGameVolume::IsShown() const
+{
+ return m_active;
+}
+
+void CDialogGameVolume::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (flag == ANNOUNCEMENT::Application && message == "OnVolumeChanged")
+ {
+ const float volumePercent = static_cast<float>(data["volume"].asDouble());
+
+ if (std::fabs(volumePercent - m_volumePercent) > 0.1f)
+ {
+ m_volumePercent = volumePercent;
+
+ CGUIMessage msg(GUI_MSG_STATE_CHANGED, GetID(), GetID());
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+ }
+}
+
+void CDialogGameVolume::OnStateChanged()
+{
+ if (m_volumePercent != m_oldVolumePercent)
+ {
+ m_oldVolumePercent = m_volumePercent;
+ SetSlider(GetLabel(), m_volumePercent, VOLUME_MIN, VOLUME_DELTA, VOLUME_MAX, this, nullptr);
+ }
+}
+
+float CDialogGameVolume::GetVolumePercent() const
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ return appVolume->GetVolumePercent();
+}
+
+std::string CDialogGameVolume::GetLabel()
+{
+ return g_localizeStrings.Get(13376); // "Volume"
+}
diff --git a/xbmc/games/dialogs/osd/DialogGameVolume.h b/xbmc/games/dialogs/osd/DialogGameVolume.h
new file mode 100644
index 0000000..a32339d
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogGameVolume.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017-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 "dialogs/GUIDialogSlider.h"
+#include "dialogs/IGUIVolumeBarCallback.h"
+#include "guilib/ISliderCallback.h"
+#include "interfaces/IAnnouncer.h"
+
+#include <set>
+
+namespace KODI
+{
+
+namespace GAME
+{
+class CDialogGameVolume : public CGUIDialogSlider, // GUI interface
+ public ISliderCallback, // GUI callback
+ public IGUIVolumeBarCallback, // Volume bar dialog callback
+ public ANNOUNCEMENT::IAnnouncer // Application callback
+{
+public:
+ CDialogGameVolume();
+ ~CDialogGameVolume() override = default;
+
+ // implementation of CGUIControl via CGUIDialogSlider
+ bool OnMessage(CGUIMessage& message) override;
+
+ // implementation of CGUIWindow via CGUIDialogSlider
+ void OnDeinitWindow(int nextWindowID) override;
+
+ // implementation of ISliderCallback
+ void OnSliderChange(void* data, CGUISliderControl* slider) override;
+
+ // implementation of IGUIVolumeBarCallback
+ bool IsShown() const override;
+
+ // implementation of IAnnouncer
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+protected:
+ // implementation of CGUIWindow via CGUIDialogSlider
+ void OnInitWindow() override;
+
+private:
+ /*!
+ * \brief Call when state change message is received
+ */
+ void OnStateChanged();
+
+ /*!
+ * \brief Get the volume of the first callback
+ *
+ * \return The volume, as a fraction of maximum volume
+ */
+ float GetVolumePercent() const;
+
+ /*!
+ * \brief Get the volume bar label
+ */
+ static std::string GetLabel();
+
+ // Volume parameters
+ const float VOLUME_MIN = 0.0f;
+ const float VOLUME_DELTA = 10.0f;
+ const float VOLUME_MAX = 100.0f;
+ float m_volumePercent = 100.0f;
+ float m_oldVolumePercent = 100.0f;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/dialogs/osd/DialogInGameSaves.cpp b/xbmc/games/dialogs/osd/DialogInGameSaves.cpp
new file mode 100644
index 0000000..9f66fd5
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogInGameSaves.cpp
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2020-2021 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 "DialogInGameSaves.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "XBDateTime.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h"
+#include "cores/RetroPlayer/guicontrols/GUIGameControl.h"
+#include "cores/RetroPlayer/playback/IPlayback.h"
+#include "cores/RetroPlayer/savestates/ISavestate.h"
+#include "cores/RetroPlayer/savestates/SavestateDatabase.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogOK.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "games/dialogs/DialogGameDefines.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "settings/GameSettings.h"
+#include "settings/MediaSettings.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace GAME;
+using namespace RETRO;
+
+namespace
+{
+CFileItemPtr CreateNewSaveItem()
+{
+ CFileItemPtr item = std::make_shared<CFileItem>(g_localizeStrings.Get(15314)); // "Save"
+
+ // A nonexistent path ensures a gamewindow control won't render any pixels
+ item->SetPath(NO_PIXEL_DATA);
+ item->SetArt("icon", "DefaultAddSource.png");
+ item->SetProperty(SAVESTATE_CAPTION,
+ g_localizeStrings.Get(15315)); // "Save progress to a new save file"
+
+ return item;
+}
+} // namespace
+
+CDialogInGameSaves::CDialogInGameSaves()
+ : CDialogGameVideoSelect(WINDOW_DIALOG_IN_GAME_SAVES), m_newSaveItem(CreateNewSaveItem())
+{
+}
+
+bool CDialogInGameSaves::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_REFRESH_THUMBS:
+ {
+ if (message.GetControlId() == GetID())
+ {
+ const std::string& itemPath = message.GetStringParam();
+ CGUIListItemPtr itemInfo = message.GetItem();
+
+ if (!itemPath.empty())
+ {
+ OnItemRefresh(itemPath, std::move(itemInfo));
+ }
+ else
+ {
+ InitSavedGames();
+ RefreshList();
+ }
+
+ return true;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return CDialogGameVideoSelect::OnMessage(message);
+}
+
+std::string CDialogInGameSaves::GetHeading()
+{
+ return g_localizeStrings.Get(35249); // "Save / Load"
+}
+
+void CDialogInGameSaves::PreInit()
+{
+ InitSavedGames();
+}
+
+void CDialogInGameSaves::InitSavedGames()
+{
+ m_savestateItems.Clear();
+
+ auto gameSettings = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+
+ CSavestateDatabase db;
+ db.GetSavestatesNav(m_savestateItems, gameSettings->GetPlayingGame(),
+ gameSettings->GameClientID());
+
+ m_savestateItems.Sort(SortByDate, SortOrderDescending);
+}
+
+void CDialogInGameSaves::GetItems(CFileItemList& items)
+{
+ items.Add(m_newSaveItem);
+ std::for_each(m_savestateItems.cbegin(), m_savestateItems.cend(),
+ [&items](const auto& item) { items.Add(item); });
+}
+
+void CDialogInGameSaves::OnItemFocus(unsigned int index)
+{
+ if (static_cast<int>(index) < 1 + m_savestateItems.Size())
+ m_focusedItemIndex = index;
+}
+
+unsigned int CDialogInGameSaves::GetFocusedItem() const
+{
+ return m_focusedControl;
+}
+
+void CDialogInGameSaves::OnItemRefresh(const std::string& itemPath, CGUIListItemPtr itemInfo)
+{
+ // Turn the message params into a savestate item
+ CFileItemPtr item = TranslateMessageItem(itemPath, std::move(itemInfo));
+ if (item)
+ {
+ // Look up existing savestate by path
+ auto it =
+ std::find_if(m_savestateItems.cbegin(), m_savestateItems.cend(),
+ [&itemPath](const CFileItemPtr& item) { return item->GetPath() == itemPath; });
+
+ // Update savestate or add a new one
+ if (it != m_savestateItems.cend())
+ **it = std::move(*item);
+ else
+ m_savestateItems.AddFront(std::move(item), 0);
+
+ RefreshList();
+ }
+}
+
+void CDialogInGameSaves::PostExit()
+{
+ m_savestateItems.Clear();
+}
+
+bool CDialogInGameSaves::OnClickAction()
+{
+ if (static_cast<int>(m_focusedItemIndex) < 1 + m_savestateItems.Size())
+ {
+ if (m_focusedItemIndex <= 0)
+ {
+ OnNewSave();
+ return true;
+ }
+ else
+ {
+ CFileItemPtr focusedItem = m_savestateItems[m_focusedItemIndex - 1];
+ if (focusedItem)
+ {
+ OnLoad(*focusedItem);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool CDialogInGameSaves::OnMenuAction()
+{
+ // Start at index 1 to account for leading "Save" item
+ if (1 <= m_focusedItemIndex && static_cast<int>(m_focusedItemIndex) < 1 + m_savestateItems.Size())
+ {
+ CFileItemPtr focusedItem = m_savestateItems[m_focusedItemIndex - 1];
+ if (focusedItem)
+ {
+ CContextButtons buttons;
+
+ buttons.Add(0, 13206); // "Overwrite"
+ buttons.Add(1, 118); // "Rename"
+ buttons.Add(2, 117); // "Delete"
+
+ const int index = CGUIDialogContextMenu::Show(buttons);
+
+ if (index == 0)
+ OnOverwrite(*focusedItem);
+ if (index == 1)
+ OnRename(*focusedItem);
+ else if (index == 2)
+ OnDelete(*focusedItem);
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CDialogInGameSaves::OnOverwriteAction()
+{
+ // Start at index 1 to account for leading "Save" item
+ if (1 <= m_focusedItemIndex && static_cast<int>(m_focusedItemIndex) < 1 + m_savestateItems.Size())
+ {
+ CFileItemPtr focusedItem = m_savestateItems[m_focusedItemIndex - 1];
+ if (focusedItem)
+ {
+ OnOverwrite(*focusedItem);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CDialogInGameSaves::OnRenameAction()
+{
+ // Start at index 1 to account for leading "Save" item
+ if (1 <= m_focusedItemIndex && static_cast<int>(m_focusedItemIndex) < 1 + m_savestateItems.Size())
+ {
+ CFileItemPtr focusedItem = m_savestateItems[m_focusedItemIndex - 1];
+ if (focusedItem)
+ {
+ OnRename(*focusedItem);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CDialogInGameSaves::OnDeleteAction()
+{
+ // Start at index 1 to account for leading "Save" item
+ if (1 <= m_focusedItemIndex && static_cast<int>(m_focusedItemIndex) < 1 + m_savestateItems.Size())
+ {
+ CFileItemPtr focusedItem = m_savestateItems[m_focusedItemIndex - 1];
+ if (focusedItem)
+ {
+ OnDelete(*focusedItem);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CDialogInGameSaves::OnNewSave()
+{
+ auto gameSettings = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+
+ const std::string savestatePath = gameSettings->CreateSavestate(false);
+ if (savestatePath.empty())
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ return;
+ }
+
+ // Create a simulated savestate to update the GUI faster. We will be notified
+ // of the real savestate info via OnMessage() when the savestate creation
+ // completes.
+ auto savestate = RETRO::CSavestateDatabase::AllocateSavestate();
+
+ savestate->SetType(SAVE_TYPE::MANUAL);
+ savestate->SetCreated(CDateTime::GetUTCDateTime());
+
+ savestate->Finalize();
+
+ CFileItemPtr item = std::make_shared<CFileItem>();
+ CSavestateDatabase::GetSavestateItem(*savestate, savestatePath, *item);
+
+ m_savestateItems.AddFront(std::move(item), 0);
+
+ RefreshList();
+}
+
+void CDialogInGameSaves::OnLoad(CFileItem& focusedItem)
+{
+ auto gameSettings = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+
+ // Load savestate
+ if (gameSettings->LoadSavestate(focusedItem.GetPath()))
+ {
+ // Close OSD on successful load
+ gameSettings->CloseOSD();
+ }
+ else
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+}
+
+void CDialogInGameSaves::OnOverwrite(CFileItem& focusedItem)
+{
+ std::string savestatePath = focusedItem.GetPath();
+ if (savestatePath.empty())
+ return;
+
+ auto gameSettings = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+
+ // Update savestate
+ if (gameSettings->UpdateSavestate(savestatePath))
+ {
+ // Create a simulated savestate to update the GUI faster. We will be
+ // notified of the real savestate info via OnMessage() when the
+ // overwriting completes.
+ auto savestate = RETRO::CSavestateDatabase::AllocateSavestate();
+
+ savestate->SetType(SAVE_TYPE::MANUAL);
+ savestate->SetLabel(focusedItem.GetProperty(SAVESTATE_LABEL).asString());
+ savestate->SetCaption(focusedItem.GetProperty(SAVESTATE_CAPTION).asString());
+ savestate->SetCreated(CDateTime::GetUTCDateTime());
+
+ savestate->Finalize();
+
+ CSavestateDatabase::GetSavestateItem(*savestate, savestatePath, focusedItem);
+
+ RefreshList();
+ }
+ else
+ {
+ // Log an error and notify the user
+ CLog::Log(LOGERROR, "Failed to overwrite savestate at {}", CURL::GetRedacted(savestatePath));
+
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+}
+
+void CDialogInGameSaves::OnRename(CFileItem& focusedItem)
+{
+ const std::string& savestatePath = focusedItem.GetPath();
+ if (savestatePath.empty())
+ return;
+
+ RETRO::CSavestateDatabase db;
+
+ std::string label;
+
+ std::unique_ptr<RETRO::ISavestate> savestate = RETRO::CSavestateDatabase::AllocateSavestate();
+ if (db.GetSavestate(savestatePath, *savestate))
+ label = savestate->Label();
+
+ // "Enter new filename"
+ if (CGUIKeyboardFactory::ShowAndGetInput(label, CVariant{g_localizeStrings.Get(16013)}, true) &&
+ label != savestate->Label())
+ {
+ std::unique_ptr<RETRO::ISavestate> newSavestate = db.RenameSavestate(savestatePath, label);
+ if (newSavestate)
+ {
+ RETRO::CSavestateDatabase::GetSavestateItem(*newSavestate, savestatePath, focusedItem);
+
+ RefreshList();
+ }
+ else
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+ }
+}
+
+void CDialogInGameSaves::OnDelete(CFileItem& focusedItem)
+{
+ // "Confirm delete"
+ // "Would you like to delete the selected file(s)?[CR]Warning - this action can't be undone!"
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{122}, CVariant{125}))
+ {
+ RETRO::CSavestateDatabase db;
+ if (db.DeleteSavestate(focusedItem.GetPath()))
+ {
+ m_savestateItems.Remove(&focusedItem);
+
+ RefreshList();
+
+ auto gameSettings = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+ gameSettings->FreeSavestateResources(focusedItem.GetPath());
+ }
+ else
+ {
+ // "Error"
+ // "An unknown error has occurred."
+ CGUIDialogOK::ShowAndGetInput(257, 24071);
+ }
+ }
+}
+
+CFileItemPtr CDialogInGameSaves::TranslateMessageItem(const std::string& messagePath,
+ CGUIListItemPtr messageItem)
+{
+ CFileItemPtr item;
+
+ if (messageItem && messageItem->IsFileItem())
+ item = std::static_pointer_cast<CFileItem>(messageItem);
+ else if (messageItem)
+ item = std::make_shared<CFileItem>(*messageItem);
+ else if (!messagePath.empty())
+ {
+ item = std::make_shared<CFileItem>();
+
+ // Load savestate if no item info was given
+ auto savestate = RETRO::CSavestateDatabase::AllocateSavestate();
+ RETRO::CSavestateDatabase db;
+ if (db.GetSavestate(messagePath, *savestate))
+ RETRO::CSavestateDatabase::GetSavestateItem(*savestate, messagePath, *item);
+ else
+ item.reset();
+ }
+
+ return item;
+}
diff --git a/xbmc/games/dialogs/osd/DialogInGameSaves.h b/xbmc/games/dialogs/osd/DialogInGameSaves.h
new file mode 100644
index 0000000..6f98f94
--- /dev/null
+++ b/xbmc/games/dialogs/osd/DialogInGameSaves.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020-2021 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 "DialogGameVideoSelect.h"
+#include "FileItem.h"
+#include "guilib/GUIListItem.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CDialogInGameSaves : public CDialogGameVideoSelect
+{
+public:
+ CDialogInGameSaves();
+ ~CDialogInGameSaves() override = default;
+
+ // implementation of CGUIControl via CDialogGameVideoSelect
+ bool OnMessage(CGUIMessage& message) override;
+
+protected:
+ // implementation of CDialogGameVideoSelect
+ std::string GetHeading() override;
+ void PreInit() override;
+ void GetItems(CFileItemList& items) override;
+ void OnItemFocus(unsigned int index) override;
+ unsigned int GetFocusedItem() const override;
+ void PostExit() override;
+ bool OnClickAction() override;
+ bool OnMenuAction() override;
+ bool OnOverwriteAction() override;
+ bool OnRenameAction() override;
+ bool OnDeleteAction() override;
+
+ void OnNewSave();
+ void OnLoad(CFileItem& focusedItem);
+ void OnOverwrite(CFileItem& focusedItem);
+ void OnRename(CFileItem& focusedItem);
+ void OnDelete(CFileItem& focusedItem);
+
+private:
+ void InitSavedGames();
+ void OnItemRefresh(const std::string& itemPath, CGUIListItemPtr itemInfo);
+
+ /*!
+ * \brief Translates the GUI list item received in a GUI message into a
+ * CFileItem with savestate properties
+ *
+ * When a savestate is overwritten, we optimistically populate the GUI list
+ * with a simulated savestate for immediate user feedback. Later (about a
+ * quarter second) a message arrives with the real savestate info.
+ *
+ * \param messagePath The savestate path, pass as the message's string param
+ * \param messageItem The savestate info, if known, or empty if unknown
+ *
+ * If messageItem is empty, the savestate will be loaded from disk, which
+ * is potentially expensive.
+ *
+ * \return A savestate item for the GUI, or empty if no savestate information
+ * can be obtained
+ */
+ static CFileItemPtr TranslateMessageItem(const std::string& messagePath,
+ CGUIListItemPtr messageItem);
+
+ CFileItemList m_savestateItems;
+ const CFileItemPtr m_newSaveItem;
+ unsigned int m_focusedItemIndex = false;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/input/CMakeLists.txt b/xbmc/games/ports/input/CMakeLists.txt
new file mode 100644
index 0000000..5b2b27f
--- /dev/null
+++ b/xbmc/games/ports/input/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES PhysicalPort.cpp
+ PortInput.cpp
+ PortManager.cpp
+)
+
+set(HEADERS PhysicalPort.h
+ PortInput.h
+ PortManager.h
+)
+
+core_add_library(games_ports_input)
diff --git a/xbmc/games/ports/input/PhysicalPort.cpp b/xbmc/games/ports/input/PhysicalPort.cpp
new file mode 100644
index 0000000..9c53c76
--- /dev/null
+++ b/xbmc/games/ports/input/PhysicalPort.cpp
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017-2021 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 "PhysicalPort.h"
+
+#include "games/controllers/ControllerDefinitions.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CPhysicalPort::CPhysicalPort(std::string portId, std::vector<std::string> accepts)
+ : m_portId(std::move(portId)), m_accepts(std::move(accepts))
+{
+}
+
+void CPhysicalPort::Reset()
+{
+ CPhysicalPort defaultPort;
+ *this = std::move(defaultPort);
+}
+
+bool CPhysicalPort::IsCompatible(const std::string& controllerId) const
+{
+ return std::find(m_accepts.begin(), m_accepts.end(), controllerId) != m_accepts.end();
+}
+
+bool CPhysicalPort::Deserialize(const TiXmlElement* pElement)
+{
+ if (pElement == nullptr)
+ return false;
+
+ Reset();
+
+ m_portId = XMLUtils::GetAttribute(pElement, LAYOUT_XML_ATTR_PORT_ID);
+
+ for (const TiXmlElement* pChild = pElement->FirstChildElement(); pChild != nullptr;
+ pChild = pChild->NextSiblingElement())
+ {
+ if (pChild->ValueStr() == LAYOUT_XML_ELM_ACCEPTS)
+ {
+ std::string controller = XMLUtils::GetAttribute(pChild, LAYOUT_XML_ATTR_CONTROLLER);
+
+ if (!controller.empty())
+ m_accepts.emplace_back(std::move(controller));
+ else
+ CLog::Log(LOGWARNING, "<{}> tag is missing \"{}\" attribute", LAYOUT_XML_ELM_ACCEPTS,
+ LAYOUT_XML_ATTR_CONTROLLER);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Unknown physical topology port tag: <{}>", pChild->ValueStr());
+ }
+ }
+
+ return true;
+}
diff --git a/xbmc/games/ports/input/PhysicalPort.h b/xbmc/games/ports/input/PhysicalPort.h
new file mode 100644
index 0000000..83fe3a5
--- /dev/null
+++ b/xbmc/games/ports/input/PhysicalPort.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2017-2021 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 <string>
+#include <vector>
+
+class TiXmlElement;
+
+namespace KODI
+{
+namespace GAME
+{
+
+class CPhysicalPort
+{
+public:
+ CPhysicalPort() = default;
+
+ /*!
+ * \brief Create a controller port
+ *
+ * \param portId The port's ID
+ * \param accepts A list of controller IDs that this port accepts
+ */
+ CPhysicalPort(std::string portId, std::vector<std::string> accepts);
+
+ void Reset();
+
+ /*!
+ * \brief Get the ID of the port
+ *
+ * \return The port's ID, e.g. "1", as a string
+ */
+ const std::string& ID() const { return m_portId; }
+
+ /*!
+ * \brief Get the controllers that can connect to this port
+ *
+ * \return A list of controllers that are physically compatible with this port
+ */
+ const std::vector<std::string>& Accepts() const { return m_accepts; }
+
+ /*!
+ * \brief Check if the controller is compatible with this port
+ *
+ * \return True if the controller is accepted, false otherwise
+ */
+ bool IsCompatible(const std::string& controllerId) const;
+
+ bool Deserialize(const TiXmlElement* pElement);
+
+private:
+ std::string m_portId;
+ std::vector<std::string> m_accepts;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/input/PortInput.cpp b/xbmc/games/ports/input/PortInput.cpp
new file mode 100644
index 0000000..af1652b
--- /dev/null
+++ b/xbmc/games/ports/input/PortInput.cpp
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2017-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 "PortInput.h"
+
+#include "games/addons/GameClient.h"
+#include "games/controllers/input/InputSink.h"
+#include "guilib/WindowIDs.h"
+#include "input/joysticks/keymaps/KeymapHandling.h"
+#include "peripherals/devices/Peripheral.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CPortInput::CPortInput(JOYSTICK::IInputHandler* gameInput)
+ : m_gameInput(gameInput), m_inputSink(new CInputSink(gameInput))
+{
+}
+
+CPortInput::~CPortInput() = default;
+
+void CPortInput::RegisterInput(JOYSTICK::IInputProvider* provider)
+{
+ // Give input sink the lowest priority by registering it before the other
+ // input handlers
+ provider->RegisterInputHandler(m_inputSink.get(), false);
+
+ // Register input handler
+ provider->RegisterInputHandler(this, false);
+
+ // Register GUI input
+ m_appInput.reset(new JOYSTICK::CKeymapHandling(provider, false, this));
+}
+
+void CPortInput::UnregisterInput(JOYSTICK::IInputProvider* provider)
+{
+ // Unregister in reverse order
+ if (provider == nullptr && m_appInput)
+ m_appInput->UnregisterInputProvider();
+ m_appInput.reset();
+
+ if (provider != nullptr)
+ {
+ provider->UnregisterInputHandler(this);
+ provider->UnregisterInputHandler(m_inputSink.get());
+ }
+}
+
+std::string CPortInput::ControllerID() const
+{
+ return m_gameInput->ControllerID();
+}
+
+bool CPortInput::AcceptsInput(const std::string& feature) const
+{
+ return m_gameInput->AcceptsInput(feature);
+}
+
+bool CPortInput::OnButtonPress(const std::string& feature, bool bPressed)
+{
+ if (bPressed && !m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnButtonPress(feature, bPressed);
+}
+
+void CPortInput::OnButtonHold(const std::string& feature, unsigned int holdTimeMs)
+{
+ m_gameInput->OnButtonHold(feature, holdTimeMs);
+}
+
+bool CPortInput::OnButtonMotion(const std::string& feature,
+ float magnitude,
+ unsigned int motionTimeMs)
+{
+ if (magnitude > 0.0f && !m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnButtonMotion(feature, magnitude, motionTimeMs);
+}
+
+bool CPortInput::OnAnalogStickMotion(const std::string& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs)
+{
+ if ((x != 0.0f || y != 0.0f) && !m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnAnalogStickMotion(feature, x, y, motionTimeMs);
+}
+
+bool CPortInput::OnAccelerometerMotion(const std::string& feature, float x, float y, float z)
+{
+ if (!m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnAccelerometerMotion(feature, x, y, z);
+}
+
+bool CPortInput::OnWheelMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ if ((position != 0.0f) && !m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnWheelMotion(feature, position, motionTimeMs);
+}
+
+bool CPortInput::OnThrottleMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ if ((position != 0.0f) && !m_gameInput->AcceptsInput(feature))
+ return false;
+
+ return m_gameInput->OnThrottleMotion(feature, position, motionTimeMs);
+}
+
+int CPortInput::GetWindowID() const
+{
+ return WINDOW_FULLSCREEN_GAME;
+}
diff --git a/xbmc/games/ports/input/PortInput.h b/xbmc/games/ports/input/PortInput.h
new file mode 100644
index 0000000..d3c6524
--- /dev/null
+++ b/xbmc/games/ports/input/PortInput.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017-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 "input/KeymapEnvironment.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CKeymapHandling;
+class IInputProvider;
+} // namespace JOYSTICK
+
+namespace GAME
+{
+class CPortInput : public JOYSTICK::IInputHandler, public IKeymapEnvironment
+{
+public:
+ CPortInput(JOYSTICK::IInputHandler* gameInput);
+ ~CPortInput() override;
+
+ void RegisterInput(JOYSTICK::IInputProvider* provider);
+ void UnregisterInput(JOYSTICK::IInputProvider* provider);
+
+ JOYSTICK::IInputHandler* InputHandler() { return m_gameInput; }
+
+ // Implementation of IInputHandler
+ std::string ControllerID() const override;
+ bool HasFeature(const std::string& feature) const override { return true; }
+ bool AcceptsInput(const std::string& feature) const override;
+ bool OnButtonPress(const std::string& feature, bool bPressed) override;
+ void OnButtonHold(const std::string& feature, unsigned int holdTimeMs) override;
+ bool OnButtonMotion(const std::string& feature,
+ float magnitude,
+ unsigned int motionTimeMs) override;
+ bool OnAnalogStickMotion(const std::string& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs) override;
+ bool OnAccelerometerMotion(const std::string& feature, float x, float y, float z) override;
+ bool OnWheelMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ bool OnThrottleMotion(const std::string& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ void OnInputFrame() override {}
+
+ // Implementation of IKeymapEnvironment
+ int GetWindowID() const override;
+ void SetWindowID(int windowId) override {}
+ int GetFallthrough(int windowId) const override { return -1; }
+ bool UseGlobalFallthrough() const override { return false; }
+ bool UseEasterEgg() const override { return false; }
+
+private:
+ // Construction parameters
+ JOYSTICK::IInputHandler* const m_gameInput;
+
+ // Handles input to Kodi
+ std::unique_ptr<JOYSTICK::CKeymapHandling> m_appInput;
+
+ // Prevents input falling through to Kodi when not handled by the game
+ std::unique_ptr<JOYSTICK::IInputHandler> m_inputSink;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/input/PortManager.cpp b/xbmc/games/ports/input/PortManager.cpp
new file mode 100644
index 0000000..a04177a
--- /dev/null
+++ b/xbmc/games/ports/input/PortManager.cpp
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2021 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 "PortManager.h"
+
+#include "URL.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/types/ControllerHub.h"
+#include "games/controllers/types/ControllerNode.h"
+#include "games/ports/types/PortNode.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+namespace
+{
+constexpr const char* PORT_XML_FILE = "ports.xml";
+constexpr const char* XML_ROOT_PORTS = "ports";
+constexpr const char* XML_ELM_PORT = "port";
+constexpr const char* XML_ELM_CONTROLLER = "controller";
+constexpr const char* XML_ATTR_PORT_ID = "id";
+constexpr const char* XML_ATTR_PORT_ADDRESS = "address";
+constexpr const char* XML_ATTR_PORT_CONNECTED = "connected";
+constexpr const char* XML_ATTR_PORT_CONTROLLER = "controller";
+constexpr const char* XML_ATTR_CONTROLLER_ID = "id";
+} // namespace
+
+CPortManager::CPortManager() = default;
+
+CPortManager::~CPortManager() = default;
+
+void CPortManager::Initialize(const std::string& profilePath)
+{
+ m_xmlPath = URIUtils::AddFileToFolder(profilePath, PORT_XML_FILE);
+}
+
+void CPortManager::Deinitialize()
+{
+ // Wait for save tasks
+ for (std::future<void>& task : m_saveFutures)
+ task.wait();
+ m_saveFutures.clear();
+
+ m_controllerTree.Clear();
+ m_xmlPath.clear();
+}
+
+void CPortManager::SetControllerTree(const CControllerTree& controllerTree)
+{
+ m_controllerTree = controllerTree;
+}
+
+void CPortManager::LoadXML()
+{
+ if (!CFileUtils::Exists(m_xmlPath))
+ {
+ CLog::Log(LOGDEBUG, "Can't load port config, file doesn't exist: {}", m_xmlPath);
+ return;
+ }
+
+ CLog::Log(LOGINFO, "Loading port layout: {}", CURL::GetRedacted(m_xmlPath));
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(m_xmlPath))
+ {
+ CLog::Log(LOGDEBUG, "Unable to load file: {} at line {}", xmlDoc.ErrorDesc(),
+ xmlDoc.ErrorRow());
+ return;
+ }
+
+ const TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (pRootElement == nullptr || pRootElement->NoChildren() ||
+ pRootElement->ValueStr() != XML_ROOT_PORTS)
+ {
+ CLog::Log(LOGERROR, "Can't find root <{}> tag", XML_ROOT_PORTS);
+ return;
+ }
+
+ DeserializePorts(pRootElement, m_controllerTree.GetPorts());
+}
+
+void CPortManager::SaveXMLAsync()
+{
+ PortVec ports = m_controllerTree.GetPorts();
+
+ // Prune any finished save tasks
+ m_saveFutures.erase(std::remove_if(m_saveFutures.begin(), m_saveFutures.end(),
+ [](std::future<void>& task) {
+ return task.wait_for(std::chrono::seconds(0)) ==
+ std::future_status::ready;
+ }),
+ m_saveFutures.end());
+
+ // Save async
+ std::future<void> task = std::async(std::launch::async, [this, ports = std::move(ports)]() {
+ CXBMCTinyXML doc;
+ TiXmlElement node(XML_ROOT_PORTS);
+
+ SerializePorts(node, ports);
+
+ doc.InsertEndChild(node);
+
+ std::lock_guard<std::mutex> lock(m_saveMutex);
+ doc.SaveFile(m_xmlPath);
+ });
+
+ m_saveFutures.emplace_back(std::move(task));
+}
+
+void CPortManager::Clear()
+{
+ m_xmlPath.clear();
+ m_controllerTree.Clear();
+}
+
+void CPortManager::ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId /* = "" */)
+{
+ ConnectController(portAddress, connected, controllerId, m_controllerTree.GetPorts());
+}
+
+bool CPortManager::ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ PortVec& ports)
+{
+ for (CPortNode& port : ports)
+ {
+ if (ConnectController(portAddress, connected, controllerId, port))
+ return true;
+ }
+
+ return false;
+}
+
+bool CPortManager::ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ CPortNode& port)
+{
+ // Base case
+ if (port.GetAddress() == portAddress)
+ {
+ port.SetConnected(connected);
+ if (!controllerId.empty())
+ port.SetActiveController(controllerId);
+ return true;
+ }
+
+ // Check children
+ return ConnectController(portAddress, connected, controllerId, port.GetCompatibleControllers());
+}
+
+bool CPortManager::ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ ControllerNodeVec& controllers)
+{
+ for (CControllerNode& controller : controllers)
+ {
+ if (ConnectController(portAddress, connected, controllerId, controller))
+ return true;
+ }
+
+ return false;
+}
+
+bool CPortManager::ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ CControllerNode& controller)
+{
+ for (CPortNode& childPort : controller.GetHub().GetPorts())
+ {
+ if (ConnectController(portAddress, connected, controllerId, childPort))
+ return true;
+ }
+
+ return false;
+}
+
+void CPortManager::DeserializePorts(const TiXmlElement* pElement, PortVec& ports)
+{
+ for (const TiXmlElement* pPort = pElement->FirstChildElement(); pPort != nullptr;
+ pPort = pPort->NextSiblingElement())
+ {
+ if (pPort->ValueStr() != XML_ELM_PORT)
+ {
+ CLog::Log(LOGDEBUG, "Inside <{}> tag: Ignoring <{}> tag", pElement->ValueStr(),
+ pPort->ValueStr());
+ continue;
+ }
+
+ std::string portId = XMLUtils::GetAttribute(pPort, XML_ATTR_PORT_ID);
+
+ auto it = std::find_if(ports.begin(), ports.end(),
+ [&portId](const CPortNode& port) { return port.GetPortID() == portId; });
+ if (it != ports.end())
+ {
+ CPortNode& port = *it;
+
+ DeserializePort(pPort, port);
+ }
+ }
+}
+
+void CPortManager::DeserializePort(const TiXmlElement* pPort, CPortNode& port)
+{
+ // Connected
+ bool connected = (XMLUtils::GetAttribute(pPort, XML_ATTR_PORT_CONNECTED) == "true");
+ port.SetConnected(connected);
+
+ // Controller
+ const std::string activeControllerId = XMLUtils::GetAttribute(pPort, XML_ATTR_PORT_CONTROLLER);
+ if (!port.SetActiveController(activeControllerId))
+ port.SetConnected(false);
+
+ DeserializeControllers(pPort, port.GetCompatibleControllers());
+}
+
+void CPortManager::DeserializeControllers(const TiXmlElement* pPort, ControllerNodeVec& controllers)
+{
+ for (const TiXmlElement* pController = pPort->FirstChildElement(); pController != nullptr;
+ pController = pController->NextSiblingElement())
+ {
+ if (pController->ValueStr() != XML_ELM_CONTROLLER)
+ {
+ CLog::Log(LOGDEBUG, "Inside <{}> tag: Ignoring <{}> tag", pPort->ValueStr(),
+ pController->ValueStr());
+ continue;
+ }
+
+ std::string controllerId = XMLUtils::GetAttribute(pController, XML_ATTR_CONTROLLER_ID);
+
+ auto it = std::find_if(controllers.begin(), controllers.end(),
+ [&controllerId](const CControllerNode& controller) {
+ return controller.GetController()->ID() == controllerId;
+ });
+ if (it != controllers.end())
+ {
+ CControllerNode& controller = *it;
+
+ DeserializeController(pController, controller);
+ }
+ }
+}
+
+void CPortManager::DeserializeController(const TiXmlElement* pController,
+ CControllerNode& controller)
+{
+ // Child ports
+ DeserializePorts(pController, controller.GetHub().GetPorts());
+}
+
+void CPortManager::SerializePorts(TiXmlElement& node, const PortVec& ports)
+{
+ for (const CPortNode& port : ports)
+ {
+ TiXmlElement portNode(XML_ELM_PORT);
+
+ SerializePort(portNode, port);
+
+ node.InsertEndChild(portNode);
+ }
+}
+
+void CPortManager::SerializePort(TiXmlElement& portNode, const CPortNode& port)
+{
+ // Port ID
+ portNode.SetAttribute(XML_ATTR_PORT_ID, port.GetPortID());
+
+ // Port address
+ portNode.SetAttribute(XML_ATTR_PORT_ADDRESS, port.GetAddress());
+
+ // Connected state
+ portNode.SetAttribute(XML_ATTR_PORT_CONNECTED, port.IsConnected() ? "true" : "false");
+
+ // Active controller
+ if (port.GetActiveController().GetController())
+ {
+ const std::string controllerId = port.GetActiveController().GetController()->ID();
+ portNode.SetAttribute(XML_ATTR_PORT_CONTROLLER, controllerId);
+ }
+
+ // All compatible controllers
+ SerializeControllers(portNode, port.GetCompatibleControllers());
+}
+
+void CPortManager::SerializeControllers(TiXmlElement& portNode,
+ const ControllerNodeVec& controllers)
+{
+ for (const CControllerNode& controller : controllers)
+ {
+ // Skip controller if it has no state
+ if (!HasState(controller))
+ continue;
+
+ TiXmlElement controllerNode(XML_ELM_CONTROLLER);
+
+ SerializeController(controllerNode, controller);
+
+ portNode.InsertEndChild(controllerNode);
+ }
+}
+
+void CPortManager::SerializeController(TiXmlElement& controllerNode,
+ const CControllerNode& controller)
+{
+ // Controller ID
+ if (controller.GetController())
+ controllerNode.SetAttribute(XML_ATTR_CONTROLLER_ID, controller.GetController()->ID());
+
+ // Ports
+ SerializePorts(controllerNode, controller.GetHub().GetPorts());
+}
+
+bool CPortManager::HasState(const CPortNode& port)
+{
+ // Ports have state (is connected / active controller)
+ return true;
+}
+
+bool CPortManager::HasState(const CControllerNode& controller)
+{
+ // Check controller ports
+ for (const CPortNode& port : controller.GetHub().GetPorts())
+ {
+ if (HasState(port))
+ return true;
+ }
+
+ // Controller itself has no state
+ return false;
+}
diff --git a/xbmc/games/ports/input/PortManager.h b/xbmc/games/ports/input/PortManager.h
new file mode 100644
index 0000000..a02ad35
--- /dev/null
+++ b/xbmc/games/ports/input/PortManager.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2021 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 "games/controllers/types/ControllerTree.h"
+
+#include <future>
+#include <mutex>
+#include <string>
+
+class TiXmlElement;
+
+namespace KODI
+{
+namespace GAME
+{
+class CPortManager
+{
+public:
+ CPortManager();
+ ~CPortManager();
+
+ void Initialize(const std::string& profilePath);
+ void Deinitialize();
+
+ void SetControllerTree(const CControllerTree& controllerTree);
+ void LoadXML();
+ void SaveXMLAsync();
+ void Clear();
+
+ void ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId = "");
+
+ const CControllerTree& GetControllerTree() const { return m_controllerTree; }
+
+private:
+ static void DeserializePorts(const TiXmlElement* pElement, PortVec& ports);
+ static void DeserializePort(const TiXmlElement* pElement, CPortNode& port);
+ static void DeserializeControllers(const TiXmlElement* pElement, ControllerNodeVec& controllers);
+ static void DeserializeController(const TiXmlElement* pElement, CControllerNode& controller);
+
+ static void SerializePorts(TiXmlElement& node, const PortVec& ports);
+ static void SerializePort(TiXmlElement& portNode, const CPortNode& port);
+ static void SerializeControllers(TiXmlElement& portNode, const ControllerNodeVec& controllers);
+ static void SerializeController(TiXmlElement& controllerNode, const CControllerNode& controller);
+
+ static bool ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ PortVec& ports);
+ static bool ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ CPortNode& port);
+ static bool ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ ControllerNodeVec& controllers);
+ static bool ConnectController(const std::string& portAddress,
+ bool connected,
+ const std::string& controllerId,
+ CControllerNode& controller);
+
+ static bool HasState(const CPortNode& port);
+ static bool HasState(const CControllerNode& controller);
+
+ CControllerTree m_controllerTree;
+ std::string m_xmlPath;
+
+ std::vector<std::future<void>> m_saveFutures;
+ std::mutex m_saveMutex;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/types/CMakeLists.txt b/xbmc/games/ports/types/CMakeLists.txt
new file mode 100644
index 0000000..735df42
--- /dev/null
+++ b/xbmc/games/ports/types/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES PortNode.cpp
+)
+
+set(HEADERS PortNode.h
+)
+
+core_add_library(games_ports_types)
diff --git a/xbmc/games/ports/types/PortNode.cpp b/xbmc/games/ports/types/PortNode.cpp
new file mode 100644
index 0000000..7caf6eb
--- /dev/null
+++ b/xbmc/games/ports/types/PortNode.cpp
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2017-2021 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 "PortNode.h"
+
+#include "games/controllers/Controller.h"
+#include "games/controllers/types/ControllerHub.h"
+#include "games/ports/input/PhysicalPort.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+using namespace GAME;
+
+CPortNode::~CPortNode() = default;
+
+CPortNode& CPortNode::operator=(const CPortNode& rhs)
+{
+ if (this != &rhs)
+ {
+ m_bConnected = rhs.m_bConnected;
+ m_active = rhs.m_active;
+ m_portType = rhs.m_portType;
+ m_portId = rhs.m_portId;
+ m_address = rhs.m_address;
+ m_forceConnected = rhs.m_forceConnected;
+ m_controllers = rhs.m_controllers;
+ }
+
+ return *this;
+}
+
+CPortNode& CPortNode::operator=(CPortNode&& rhs) noexcept
+{
+ if (this != &rhs)
+ {
+ m_bConnected = rhs.m_bConnected;
+ m_active = rhs.m_active;
+ m_portType = rhs.m_portType;
+ m_portId = std::move(rhs.m_portId);
+ m_address = std::move(rhs.m_address);
+ m_forceConnected = rhs.m_forceConnected;
+ m_controllers = std::move(rhs.m_controllers);
+ }
+
+ return *this;
+}
+
+const CControllerNode& CPortNode::GetActiveController() const
+{
+ if (m_bConnected && m_active < m_controllers.size())
+ return m_controllers[m_active];
+
+ static const CControllerNode invalid{};
+ return invalid;
+}
+
+CControllerNode& CPortNode::GetActiveController()
+{
+ if (m_bConnected && m_active < m_controllers.size())
+ return m_controllers[m_active];
+
+ static CControllerNode invalid;
+ invalid.Clear();
+ return invalid;
+}
+
+bool CPortNode::SetActiveController(const std::string& controllerId)
+{
+ for (size_t i = 0; i < m_controllers.size(); ++i)
+ {
+ const ControllerPtr& controller = m_controllers.at(i).GetController();
+ if (controller && controller->ID() == controllerId)
+ {
+ m_active = i;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CPortNode::SetPortID(std::string portId)
+{
+ m_portId = std::move(portId);
+}
+
+void CPortNode::SetAddress(std::string address)
+{
+ m_address = std::move(address);
+}
+
+void CPortNode::SetCompatibleControllers(ControllerNodeVec controllers)
+{
+ m_controllers = std::move(controllers);
+}
+
+bool CPortNode::IsControllerAccepted(const std::string& controllerId) const
+{
+ // Base case
+ CPhysicalPort port;
+ GetPort(port);
+ if (port.IsCompatible(controllerId))
+ return true;
+
+ // Visit nodes
+ return std::any_of(m_controllers.begin(), m_controllers.end(),
+ [controllerId](const CControllerNode& node) {
+ return node.IsControllerAccepted(controllerId);
+ });
+}
+
+bool CPortNode::IsControllerAccepted(const std::string& portAddress,
+ const std::string& controllerId) const
+{
+ bool bAccepted = false;
+
+ if (m_address == portAddress)
+ {
+ // Base case
+ CPhysicalPort port;
+ GetPort(port);
+ if (port.IsCompatible(controllerId))
+ bAccepted = true;
+ }
+ else
+ {
+ // Visit nodes
+ if (std::any_of(m_controllers.begin(), m_controllers.end(),
+ [portAddress, controllerId](const CControllerNode& node) {
+ return node.IsControllerAccepted(portAddress, controllerId);
+ }))
+ {
+ bAccepted = true;
+ }
+ }
+
+ return bAccepted;
+}
+
+void CPortNode::GetPort(CPhysicalPort& port) const
+{
+ std::vector<std::string> accepts;
+ for (const CControllerNode& node : m_controllers)
+ {
+ if (node.GetController())
+ accepts.emplace_back(node.GetController()->ID());
+ }
+
+ port = CPhysicalPort(m_portId, std::move(accepts));
+}
diff --git a/xbmc/games/ports/types/PortNode.h b/xbmc/games/ports/types/PortNode.h
new file mode 100644
index 0000000..660a7bc
--- /dev/null
+++ b/xbmc/games/ports/types/PortNode.h
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017-2021 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 "games/controllers/ControllerTypes.h"
+#include "games/controllers/types/ControllerNode.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace GAME
+{
+class CPhysicalPort;
+
+/*!
+ * \brief Collection of nodes that can be connected to this port
+ */
+class CPortNode
+{
+public:
+ CPortNode() = default;
+ CPortNode(const CPortNode& other) { *this = other; }
+ CPortNode(CPortNode&& other) = default;
+ CPortNode& operator=(const CPortNode& rhs);
+ CPortNode& operator=(CPortNode&& rhs) noexcept;
+ ~CPortNode();
+
+ /*!
+ * \brief Connection state of the port
+ *
+ * \return True if a controller is connected, false otherwise
+ */
+ bool IsConnected() const { return m_bConnected; }
+ void SetConnected(bool bConnected) { m_bConnected = bConnected; }
+
+ /*!
+ * \brief The controller that is active on this port
+ *
+ * \return The active controller, or invalid if port is disconnected
+ */
+ const CControllerNode& GetActiveController() const;
+ CControllerNode& GetActiveController();
+ void SetActiveController(unsigned int controllerIndex) { m_active = controllerIndex; }
+ bool SetActiveController(const std::string& controllerId);
+
+ /*!
+ * \brief The port type
+ *
+ * \return The port type, if known
+ */
+ PORT_TYPE GetPortType() const { return m_portType; }
+ void SetPortType(PORT_TYPE type) { m_portType = type; }
+
+ /*!
+ * \brief The hardware or controller port ID
+ *
+ * \return The port ID of the hardware port or controller port, or empty if
+ * the port is only identified by its type
+ */
+ const std::string& GetPortID() const { return m_portId; }
+ void SetPortID(std::string portId);
+
+ /*!
+ * \brief Address given to the node by the implementation
+ */
+ const std::string& GetAddress() const { return m_address; }
+ void SetAddress(std::string address);
+
+ /*!
+ * \brief If true, prevents a disconnection option from being shown for this
+ * port
+ */
+ bool IsForceConnected() const { return m_forceConnected; }
+ void SetForceConnected(bool forceConnected) { m_forceConnected = forceConnected; }
+
+ /*!
+ * \brief Return the controller profiles that are compatible with this port
+ *
+ * \return The controller profiles, or empty if this port doesn't support
+ * any controller profiles
+ */
+ const ControllerNodeVec& GetCompatibleControllers() const { return m_controllers; }
+ ControllerNodeVec& GetCompatibleControllers() { return m_controllers; }
+ void SetCompatibleControllers(ControllerNodeVec controllers);
+
+ /*!
+ * \brief Check to see if a controller is compatible with this tree
+ *
+ * \param controllerId The ID of the controller
+ *
+ * \return True if the controller is compatible with the tree, false otherwise
+ */
+ bool IsControllerAccepted(const std::string& controllerId) const;
+
+ /*!
+ * \brief Check to see if a controller is compatible with this tree
+ *
+ * \param portAddress The port address
+ * \param controllerId The ID of the controller
+ *
+ * \return True if the controller is compatible with the tree, false otherwise
+ */
+ bool IsControllerAccepted(const std::string& portAddress, const std::string& controllerId) const;
+
+private:
+ void GetPort(CPhysicalPort& port) const;
+
+ bool m_bConnected = false;
+ unsigned int m_active = 0;
+ PORT_TYPE m_portType = PORT_TYPE::UNKNOWN;
+ std::string m_portId;
+ std::string m_address;
+ bool m_forceConnected{true};
+ ControllerNodeVec m_controllers;
+};
+
+/*!
+ * \brief Collection of port nodes
+ */
+using PortVec = std::vector<CPortNode>;
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/windows/CMakeLists.txt b/xbmc/games/ports/windows/CMakeLists.txt
new file mode 100644
index 0000000..4cf07ea
--- /dev/null
+++ b/xbmc/games/ports/windows/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES GUIPortList.cpp
+ GUIPortWindow.cpp
+)
+
+set(HEADERS GUIPortDefines.h
+ GUIPortList.h
+ GUIPortWindow.h
+ IPortList.h
+)
+
+core_add_library(games_ports_windows)
diff --git a/xbmc/games/ports/windows/GUIPortDefines.h b/xbmc/games/ports/windows/GUIPortDefines.h
new file mode 100644
index 0000000..c9c255b
--- /dev/null
+++ b/xbmc/games/ports/windows/GUIPortDefines.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2021 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
+
+// Dialog title
+#define CONTROL_PORT_DIALOG_LABEL 2
+
+// GUI control IDs
+#define CONTROL_PORT_LIST 3
+
+// GUI button IDs
+#define CONTROL_CLOSE_BUTTON 18
+#define CONTROL_RESET_BUTTON 19
+
+// Skin XML file
+#define PORT_DIALOG_XML "DialogGameControllers.xml"
diff --git a/xbmc/games/ports/windows/GUIPortList.cpp b/xbmc/games/ports/windows/GUIPortList.cpp
new file mode 100644
index 0000000..a25c94b
--- /dev/null
+++ b/xbmc/games/ports/windows/GUIPortList.cpp
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2021 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 "GUIPortList.h"
+
+#include "FileItem.h"
+#include "GUIPortDefines.h"
+#include "GUIPortWindow.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "games/GameServices.h"
+#include "games/addons/GameClient.h"
+#include "games/addons/input/GameClientInput.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "games/controllers/types/ControllerHub.h"
+#include "games/controllers/types/ControllerTree.h"
+#include "games/ports/types/PortNode.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindow.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "view/GUIViewControl.h"
+#include "view/ViewState.h"
+
+using namespace KODI;
+using namespace ADDON;
+using namespace GAME;
+
+CGUIPortList::CGUIPortList(CGUIWindow& window)
+ : m_guiWindow(window),
+ m_viewControl(std::make_unique<CGUIViewControl>()),
+ m_vecItems(std::make_unique<CFileItemList>())
+{
+}
+
+CGUIPortList::~CGUIPortList()
+{
+ Deinitialize();
+}
+
+void CGUIPortList::OnWindowLoaded()
+{
+ m_viewControl->Reset();
+ m_viewControl->SetParentWindow(m_guiWindow.GetID());
+ m_viewControl->AddView(m_guiWindow.GetControl(CONTROL_PORT_LIST));
+}
+
+void CGUIPortList::OnWindowUnload()
+{
+ m_viewControl->Reset();
+}
+
+bool CGUIPortList::Initialize(GameClientPtr gameClient)
+{
+ // Validate parameters
+ if (!gameClient)
+ return false;
+
+ // Initialize state
+ m_gameClient = std::move(gameClient);
+ m_viewControl->SetCurrentView(DEFAULT_VIEW_LIST);
+
+ // Initialize GUI
+ Refresh();
+
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CGUIPortList::OnEvent);
+
+ return true;
+}
+
+void CGUIPortList::Deinitialize()
+{
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+
+ // Deinitialize GUI
+ CleanupItems();
+
+ // Reset state
+ m_gameClient.reset();
+}
+
+bool CGUIPortList::HasControl(int controlId)
+{
+ return m_viewControl->HasControl(controlId);
+}
+
+int CGUIPortList::GetCurrentControl()
+{
+ return m_viewControl->GetCurrentControl();
+}
+
+void CGUIPortList::Refresh()
+{
+ // Send a synchronous message to clear the view control
+ m_viewControl->Clear();
+
+ CleanupItems();
+
+ if (m_gameClient)
+ {
+ unsigned int itemIndex = 0;
+ for (const CPortNode& port : m_gameClient->Input().GetActiveControllerTree().GetPorts())
+ AddItems(port, itemIndex, GetLabel(port));
+
+ m_viewControl->SetItems(*m_vecItems);
+
+ // Try to restore focus to the previously focused port
+ if (!m_focusedPort.empty() && m_addressToItem.find(m_focusedPort) != m_addressToItem.end())
+ {
+ const unsigned int itemIndex = m_addressToItem[m_focusedPort];
+ m_viewControl->SetSelectedItem(itemIndex);
+ OnItemFocus(itemIndex);
+ }
+ }
+}
+
+void CGUIPortList::FrameMove()
+{
+ const int itemIndex = m_viewControl->GetSelectedItem();
+ if (itemIndex != m_currentItem)
+ {
+ m_currentItem = itemIndex;
+ if (itemIndex >= 0)
+ OnItemFocus(static_cast<unsigned int>(itemIndex));
+ }
+}
+
+void CGUIPortList::SetFocused()
+{
+ m_viewControl->SetFocused();
+}
+
+bool CGUIPortList::OnSelect()
+{
+ const int itemIndex = m_viewControl->GetSelectedItem();
+ if (itemIndex >= 0)
+ {
+ OnItemSelect(static_cast<unsigned int>(itemIndex));
+ return true;
+ }
+
+ return false;
+}
+
+void CGUIPortList::ResetPorts()
+{
+ if (m_gameClient)
+ {
+ // Update the game client
+ m_gameClient->Input().ResetPorts();
+ m_gameClient->Input().SavePorts();
+
+ // Refresh the GUI
+ CGUIMessage msg(GUI_MSG_REFRESH_LIST, m_guiWindow.GetID(), CONTROL_PORT_LIST);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, m_guiWindow.GetID());
+ }
+}
+
+void CGUIPortList::OnEvent(const ADDON::AddonEvent& event)
+{
+ if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) || // Also called on install
+ typeid(event) == typeid(ADDON::AddonEvents::Disabled) || // Not called on uninstall
+ typeid(event) == typeid(ADDON::AddonEvents::ReInstalled) ||
+ typeid(event) == typeid(ADDON::AddonEvents::UnInstalled))
+ {
+ CGUIMessage msg(GUI_MSG_REFRESH_LIST, m_guiWindow.GetID(), CONTROL_PORT_LIST);
+ msg.SetStringParam(event.addonId);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, m_guiWindow.GetID());
+ }
+}
+
+bool CGUIPortList::AddItems(const CPortNode& port,
+ unsigned int& itemId,
+ const std::string& itemLabel)
+{
+ // Validate parameters
+ if (itemLabel.empty())
+ return false;
+
+ // Record the port address so that we can decode item indexes later
+ m_itemToAddress[itemId] = port.GetAddress();
+ m_addressToItem[port.GetAddress()] = itemId;
+
+ if (port.IsConnected())
+ {
+ const CControllerNode& controllerNode = port.GetActiveController();
+ const ControllerPtr& controller = controllerNode.GetController();
+
+ // Create the list item
+ CFileItemPtr item = std::make_shared<CFileItem>(itemLabel);
+ item->SetLabel2(controller->Layout().Label());
+ item->SetPath(port.GetAddress());
+ item->SetArt("icon", controller->Layout().ImagePath());
+ m_vecItems->Add(std::move(item));
+ ++itemId;
+
+ // Handle items for child ports
+ const PortVec& ports = controllerNode.GetHub().GetPorts();
+ for (const CPortNode& childPort : ports)
+ {
+ std::ostringstream childItemLabel;
+ childItemLabel << " - ";
+ childItemLabel << controller->Layout().Label();
+ childItemLabel << " - ";
+ childItemLabel << GetLabel(childPort);
+
+ if (!AddItems(childPort, itemId, childItemLabel.str()))
+ return false;
+ }
+ }
+ else
+ {
+ // Create the list item
+ CFileItemPtr item = std::make_shared<CFileItem>(itemLabel);
+ item->SetLabel2(g_localizeStrings.Get(13298)); // "Disconnected"
+ item->SetPath(port.GetAddress());
+ item->SetArt("icon", "DefaultAddonNone.png");
+ m_vecItems->Add(std::move(item));
+ ++itemId;
+ }
+
+ return true;
+}
+
+void CGUIPortList::CleanupItems()
+{
+ m_vecItems->Clear();
+ m_itemToAddress.clear();
+ m_addressToItem.clear();
+}
+
+void CGUIPortList::OnItemFocus(unsigned int itemIndex)
+{
+ m_focusedPort = m_itemToAddress[itemIndex];
+}
+
+void CGUIPortList::OnItemSelect(unsigned int itemIndex)
+{
+ if (m_gameClient)
+ {
+ const auto it = m_itemToAddress.find(itemIndex);
+ if (it == m_itemToAddress.end())
+ return;
+
+ const std::string& portAddress = it->second;
+ if (portAddress.empty())
+ return;
+
+ const CPortNode& port = m_gameClient->Input().GetActiveControllerTree().GetPort(portAddress);
+
+ ControllerVector controllers;
+ for (const CControllerNode& controllerNode : port.GetCompatibleControllers())
+ controllers.emplace_back(controllerNode.GetController());
+
+ // Get current controller to give initial focus
+ ControllerPtr controller = port.GetActiveController().GetController();
+
+ auto callback = [this, &port](const ControllerPtr& controller) {
+ OnControllerSelected(port, controller);
+ };
+
+ const bool showDisconnect = !port.IsForceConnected();
+ m_controllerSelectDialog.Initialize(std::move(controllers), std::move(controller),
+ showDisconnect, callback);
+ }
+}
+
+void CGUIPortList::OnControllerSelected(const CPortNode& port, const ControllerPtr& controller)
+{
+ if (m_gameClient)
+ {
+ // Translate parameter
+ const bool bConnected = static_cast<bool>(controller);
+
+ // Update the game client
+ const bool bSuccess =
+ bConnected ? m_gameClient->Input().ConnectController(port.GetAddress(), controller)
+ : m_gameClient->Input().DisconnectController(port.GetAddress());
+
+ if (bSuccess)
+ {
+ m_gameClient->Input().SavePorts();
+ }
+ else
+ {
+ // "Failed to change controller"
+ // "The emulator "%s" had an internal error."
+ MESSAGING::HELPERS::ShowOKDialogText(
+ CVariant{35114},
+ CVariant{StringUtils::Format(g_localizeStrings.Get(35213), m_gameClient->Name())});
+ }
+
+ // Send a GUI message to reload the port list
+ CGUIMessage msg(GUI_MSG_REFRESH_LIST, m_guiWindow.GetID(), CONTROL_PORT_LIST);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, m_guiWindow.GetID());
+ }
+}
+
+std::string CGUIPortList::GetLabel(const CPortNode& port)
+{
+ const PORT_TYPE portType = port.GetPortType();
+ switch (portType)
+ {
+ case PORT_TYPE::KEYBOARD:
+ {
+ // "Keyboard"
+ return g_localizeStrings.Get(35150);
+ }
+ case PORT_TYPE::MOUSE:
+ {
+ // "Mouse"
+ return g_localizeStrings.Get(35171);
+ }
+ case PORT_TYPE::CONTROLLER:
+ {
+ const std::string& portId = port.GetPortID();
+ if (portId.empty())
+ {
+ CLog::Log(LOGERROR, "Controller port with address \"{}\" doesn't have a port ID",
+ port.GetAddress());
+ }
+ else
+ {
+ // "Port {0:s}"
+ const std::string& portString = g_localizeStrings.Get(35112);
+ return StringUtils::Format(portString, portId);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return "";
+}
diff --git a/xbmc/games/ports/windows/GUIPortList.h b/xbmc/games/ports/windows/GUIPortList.h
new file mode 100644
index 0000000..3fed6b7
--- /dev/null
+++ b/xbmc/games/ports/windows/GUIPortList.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 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 "IPortList.h"
+#include "addons/AddonEvents.h"
+#include "games/GameTypes.h"
+#include "games/controllers/ControllerTypes.h"
+#include "games/controllers/dialogs/ControllerSelect.h"
+#include "games/controllers/types/ControllerTree.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+class CFileItemList;
+class CGUIViewControl;
+class CGUIWindow;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIPortList : public IPortList
+{
+public:
+ CGUIPortList(CGUIWindow& window);
+ ~CGUIPortList() override;
+
+ // Implementation of IPortList
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ bool Initialize(GameClientPtr gameClient) override;
+ void Deinitialize() override;
+ bool HasControl(int controlId) override;
+ int GetCurrentControl() override;
+ void Refresh() override;
+ void FrameMove() override;
+ void SetFocused() override;
+ bool OnSelect() override;
+ void ResetPorts() override;
+
+private:
+ // Add-on API
+ void OnEvent(const ADDON::AddonEvent& event);
+
+ bool AddItems(const CPortNode& port, unsigned int& itemId, const std::string& itemLabel);
+ void CleanupItems();
+ void OnItemFocus(unsigned int itemIndex);
+ void OnItemSelect(unsigned int itemIndex);
+
+ // Controller selection callback
+ void OnControllerSelected(const CPortNode& port, const ControllerPtr& controller);
+
+ static std::string GetLabel(const CPortNode& port);
+
+ // Construction parameters
+ CGUIWindow& m_guiWindow;
+
+ // GUI parameters
+ CControllerSelect m_controllerSelectDialog;
+ std::string m_focusedPort; // Address of focused port
+ int m_currentItem{-1}; // Index of the selected item, or -1 if no item is selected
+ std::unique_ptr<CGUIViewControl> m_viewControl;
+ std::unique_ptr<CFileItemList> m_vecItems;
+
+ // Game parameters
+ GameClientPtr m_gameClient;
+ std::map<unsigned int, std::string> m_itemToAddress; // item index -> port address
+ std::map<std::string, unsigned int> m_addressToItem; // port address -> item index
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/windows/GUIPortWindow.cpp b/xbmc/games/ports/windows/GUIPortWindow.cpp
new file mode 100644
index 0000000..e95927a
--- /dev/null
+++ b/xbmc/games/ports/windows/GUIPortWindow.cpp
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2021 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 "GUIPortWindow.h"
+
+#include "GUIPortDefines.h"
+#include "GUIPortList.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonType.h"
+#include "cores/RetroPlayer/guibridge/GUIGameRenderManager.h"
+#include "cores/RetroPlayer/guibridge/GUIGameSettingsHandle.h"
+#include "games/addons/GameClient.h"
+#include "guilib/GUIButtonControl.h"
+#include "guilib/GUIControl.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/ActionIDs.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIPortWindow::CGUIPortWindow()
+ : CGUIDialog(WINDOW_DIALOG_GAME_PORTS, PORT_DIALOG_XML),
+ m_portList(std::make_unique<CGUIPortList>(*this))
+{
+ // Initialize CGUIWindow
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIPortWindow::~CGUIPortWindow() = default;
+
+bool CGUIPortWindow::OnMessage(CGUIMessage& message)
+{
+ // Set to true to block the call to the super class
+ bool bHandled = false;
+
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_SETFOCUS:
+ {
+ const int controlId = message.GetControlId();
+ if (m_portList->HasControl(controlId) && m_portList->GetCurrentControl() != controlId)
+ {
+ FocusPortList();
+ bHandled = true;
+ }
+ break;
+ }
+ case GUI_MSG_CLICKED:
+ {
+ const int controlId = message.GetSenderId();
+
+ if (controlId == CONTROL_CLOSE_BUTTON)
+ {
+ CloseDialog();
+ bHandled = true;
+ }
+ else if (controlId == CONTROL_RESET_BUTTON)
+ {
+ ResetPorts();
+ bHandled = true;
+ }
+ else if (m_portList->HasControl(controlId))
+ {
+ const int actionId = message.GetParam1();
+ if (actionId == ACTION_SELECT_ITEM || actionId == ACTION_MOUSE_LEFT_CLICK)
+ {
+ OnClickAction();
+ bHandled = true;
+ }
+ }
+ break;
+ }
+ case GUI_MSG_REFRESH_LIST:
+ {
+ UpdatePortList();
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (!bHandled)
+ bHandled = CGUIDialog::OnMessage(message);
+
+ return bHandled;
+}
+
+void CGUIPortWindow::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+
+ m_portList->OnWindowLoaded();
+}
+
+void CGUIPortWindow::OnWindowUnload()
+{
+ m_portList->OnWindowUnload();
+
+ CGUIDialog::OnWindowUnload();
+}
+
+void CGUIPortWindow::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ // Get active game add-on
+ GameClientPtr gameClient;
+ {
+ auto gameSettingsHandle = CServiceBroker::GetGameRenderManager().RegisterGameSettingsDialog();
+ if (gameSettingsHandle)
+ {
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(gameSettingsHandle->GameClientID(), addon,
+ ADDON::AddonType::GAMEDLL,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ gameClient = std::static_pointer_cast<CGameClient>(addon);
+ }
+ }
+ m_gameClient = std::move(gameClient);
+
+ // Set the heading
+ // "Port Setup - {game client name}"
+ SET_CONTROL_LABEL(CONTROL_PORT_DIALOG_LABEL,
+ StringUtils::Format("$LOCALIZE[35111] - {}", m_gameClient->Name()));
+
+ m_portList->Initialize(m_gameClient);
+
+ UpdatePortList();
+
+ // Focus the port list
+ CGUIMessage msgFocus(GUI_MSG_SETFOCUS, GetID(), CONTROL_PORT_LIST);
+ OnMessage(msgFocus);
+}
+
+void CGUIPortWindow::OnDeinitWindow(int nextWindowID)
+{
+ m_portList->Deinitialize();
+
+ m_gameClient.reset();
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIPortWindow::FrameMove()
+{
+ CGUIDialog::FrameMove();
+
+ m_portList->FrameMove();
+}
+
+void CGUIPortWindow::UpdatePortList()
+{
+ m_portList->Refresh();
+}
+
+void CGUIPortWindow::FocusPortList()
+{
+ m_portList->SetFocused();
+}
+
+bool CGUIPortWindow::OnClickAction()
+{
+ return m_portList->OnSelect();
+}
+
+void CGUIPortWindow::ResetPorts()
+{
+ m_portList->ResetPorts();
+}
+
+void CGUIPortWindow::CloseDialog()
+{
+ Close();
+}
diff --git a/xbmc/games/ports/windows/GUIPortWindow.h b/xbmc/games/ports/windows/GUIPortWindow.h
new file mode 100644
index 0000000..3b98708
--- /dev/null
+++ b/xbmc/games/ports/windows/GUIPortWindow.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 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 "games/GameTypes.h"
+#include "guilib/GUIDialog.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GAME
+{
+class IPortList;
+
+class CGUIPortWindow : public CGUIDialog
+{
+public:
+ CGUIPortWindow();
+ ~CGUIPortWindow() override;
+
+ // Implementation of CGUIControl via CGUIDialog
+ bool OnMessage(CGUIMessage& message) override;
+
+protected:
+ // Implementation of CGUIWindow via CGUIDialog
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ void FrameMove() override;
+
+private:
+ // Actions for port list
+ void UpdatePortList();
+ void FocusPortList();
+ bool OnClickAction();
+
+ // Actions for the available buttons
+ void ResetPorts();
+ void CloseDialog();
+
+ // GUI parameters
+ std::unique_ptr<IPortList> m_portList;
+
+ // Game parameters
+ GameClientPtr m_gameClient;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/ports/windows/IPortList.h b/xbmc/games/ports/windows/IPortList.h
new file mode 100644
index 0000000..e330c6e
--- /dev/null
+++ b/xbmc/games/ports/windows/IPortList.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2021 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 "games/GameTypes.h"
+
+/*!
+ * \brief Controller port setup window
+ *
+ * The port setup window presents a list of ports and their attached
+ * controllers.
+ *
+ * The label2 of each port is the currently-connected controller. The user
+ * selects from all controllers that the port accepts (as given by the
+ * game-addon's topology.xml file).
+ *
+ * The controller topology is stored as a generic tree. Here we apply game logic
+ * to simplify controller selection.
+ */
+
+namespace KODI
+{
+namespace GAME
+{
+/*!
+ * \brief A list populated by controller ports
+ */
+class IPortList
+{
+public:
+ virtual ~IPortList() = default;
+
+ /*!
+ * \brief Callback when the GUI window is loaded
+ */
+ virtual void OnWindowLoaded() = 0;
+
+ /*!
+ * \brief Callback when the GUI window is unloaded
+ */
+ virtual void OnWindowUnload() = 0;
+
+ /*!
+ * \brief Initialize resources
+ *
+ * \param gameClient The game client providing the ports
+ *
+ * \return True if the resource is initialized and can be used, false if the
+ * resource failed to initialize and must not be used
+ */
+ virtual bool Initialize(GameClientPtr gameClient) = 0;
+
+ /*!
+ * \brief Deinitialize resources
+ */
+ virtual void Deinitialize() = 0;
+
+ /*!
+ * \brief Query if a control with the given ID belongs to this list
+ */
+ virtual bool HasControl(int controlId) = 0;
+
+ /*!
+ * \brief Query the ID of the current control in this list
+ *
+ * \return The control ID, or -1 if no control is currently active
+ */
+ virtual int GetCurrentControl() = 0;
+
+ /*!
+ * \brief Refresh the contents of the list
+ */
+ virtual void Refresh() = 0;
+
+ /*!
+ * \brief Callback when a frame is rendered by the GUI
+ */
+ virtual void FrameMove() = 0;
+
+ /*!
+ * \brief The port list has been focused in the GUI
+ */
+ virtual void SetFocused() = 0;
+
+ /*!
+ * \brief The port list has been selected
+ *
+ * \brief True if a control was active, false of all controls were inactive
+ */
+ virtual bool OnSelect() = 0;
+
+ /*!
+ * \brief Reset the ports to their game add-on's default configuration
+ */
+ virtual void ResetPorts() = 0;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/tags/CMakeLists.txt b/xbmc/games/tags/CMakeLists.txt
new file mode 100644
index 0000000..d43cdbb
--- /dev/null
+++ b/xbmc/games/tags/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES GameInfoTag.cpp)
+
+set(HEADERS GameInfoTag.h)
+
+core_add_library(gametags)
diff --git a/xbmc/games/tags/GameInfoTag.cpp b/xbmc/games/tags/GameInfoTag.cpp
new file mode 100644
index 0000000..8a02c2c
--- /dev/null
+++ b/xbmc/games/tags/GameInfoTag.cpp
@@ -0,0 +1,158 @@
+/*
+ * 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 "GameInfoTag.h"
+
+#include "utils/Archive.h"
+#include "utils/Variant.h"
+
+#include <string>
+
+using namespace KODI;
+using namespace GAME;
+
+void CGameInfoTag::Reset()
+{
+ m_bLoaded = false;
+ m_strURL.clear();
+ m_strTitle.clear();
+ m_strPlatform.clear();
+ m_genres.clear();
+ m_strDeveloper.clear();
+ m_strOverview.clear();
+ m_year = 0;
+ m_strID.clear();
+ m_strRegion.clear();
+ m_strPublisher.clear();
+ m_strFormat.clear();
+ m_strCartridgeType.clear();
+ m_strGameClient.clear();
+}
+
+CGameInfoTag& CGameInfoTag::operator=(const CGameInfoTag& tag)
+{
+ if (this != &tag)
+ {
+ m_bLoaded = tag.m_bLoaded;
+ m_strURL = tag.m_strURL;
+ m_strTitle = tag.m_strTitle;
+ m_strPlatform = tag.m_strPlatform;
+ m_genres = tag.m_genres;
+ m_strDeveloper = tag.m_strDeveloper;
+ m_strOverview = tag.m_strOverview;
+ m_year = tag.m_year;
+ m_strID = tag.m_strID;
+ m_strRegion = tag.m_strRegion;
+ m_strPublisher = tag.m_strPublisher;
+ m_strFormat = tag.m_strFormat;
+ m_strCartridgeType = tag.m_strCartridgeType;
+ m_strGameClient = tag.m_strGameClient;
+ }
+ return *this;
+}
+
+bool CGameInfoTag::operator==(const CGameInfoTag& tag) const
+{
+ if (this != &tag)
+ {
+ if (m_bLoaded != tag.m_bLoaded)
+ return false;
+
+ if (m_bLoaded)
+ {
+ if (m_strURL != tag.m_strURL)
+ return false;
+ if (m_strTitle != tag.m_strTitle)
+ return false;
+ if (m_strPlatform != tag.m_strPlatform)
+ return false;
+ if (m_genres != tag.m_genres)
+ return false;
+ if (m_strDeveloper != tag.m_strDeveloper)
+ return false;
+ if (m_strOverview != tag.m_strOverview)
+ return false;
+ if (m_year != tag.m_year)
+ return false;
+ if (m_strID != tag.m_strID)
+ return false;
+ if (m_strRegion != tag.m_strRegion)
+ return false;
+ if (m_strPublisher != tag.m_strPublisher)
+ return false;
+ if (m_strFormat != tag.m_strFormat)
+ return false;
+ if (m_strCartridgeType != tag.m_strCartridgeType)
+ return false;
+ if (m_strGameClient != tag.m_strGameClient)
+ return false;
+ }
+ }
+ return true;
+}
+
+void CGameInfoTag::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_bLoaded;
+ ar << m_strURL;
+ ar << m_strTitle;
+ ar << m_strPlatform;
+ ar << m_genres;
+ ar << m_strDeveloper;
+ ar << m_strOverview;
+ ar << m_year;
+ ar << m_strID;
+ ar << m_strRegion;
+ ar << m_strPublisher;
+ ar << m_strFormat;
+ ar << m_strCartridgeType;
+ ar << m_strGameClient;
+ }
+ else
+ {
+ ar >> m_bLoaded;
+ ar >> m_strURL;
+ ar >> m_strTitle;
+ ar >> m_strPlatform;
+ ar >> m_genres;
+ ar >> m_strDeveloper;
+ ar >> m_strOverview;
+ ar >> m_year;
+ ar >> m_strID;
+ ar >> m_strRegion;
+ ar >> m_strPublisher;
+ ar >> m_strFormat;
+ ar >> m_strCartridgeType;
+ ar >> m_strGameClient;
+ }
+}
+
+void CGameInfoTag::Serialize(CVariant& value) const
+{
+ value["loaded"] = m_bLoaded;
+ value["url"] = m_strURL;
+ value["name"] = m_strTitle;
+ value["platform"] = m_strPlatform;
+ value["genres"] = m_genres;
+ value["developer"] = m_strDeveloper;
+ value["overview"] = m_strOverview;
+ value["year"] = m_year;
+ value["id"] = m_strID;
+ value["region"] = m_strRegion;
+ value["publisher"] = m_strPublisher;
+ value["format"] = m_strFormat;
+ value["cartridgetype"] = m_strCartridgeType;
+ value["gameclient"] = m_strGameClient;
+}
+
+void CGameInfoTag::ToSortable(SortItem& sortable, Field field) const
+{
+ // No database entries for games (...yet)
+}
diff --git a/xbmc/games/tags/GameInfoTag.h b/xbmc/games/tags/GameInfoTag.h
new file mode 100644
index 0000000..00287ad
--- /dev/null
+++ b/xbmc/games/tags/GameInfoTag.h
@@ -0,0 +1,112 @@
+/*
+ * 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 "utils/IArchivable.h"
+#include "utils/ISerializable.h"
+#include "utils/ISortable.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameInfoTag : public IArchivable, public ISerializable, public ISortable
+{
+public:
+ CGameInfoTag() { Reset(); }
+ CGameInfoTag(const CGameInfoTag& tag) { *this = tag; }
+ CGameInfoTag& operator=(const CGameInfoTag& tag);
+ virtual ~CGameInfoTag() = default;
+ void Reset();
+
+ bool operator==(const CGameInfoTag& tag) const;
+ bool operator!=(const CGameInfoTag& tag) const { return !(*this == tag); }
+
+ bool IsLoaded() const { return m_bLoaded; }
+ void SetLoaded(bool bOnOff = true) { m_bLoaded = bOnOff; }
+
+ // File path
+ const std::string& GetURL() const { return m_strURL; }
+ void SetURL(const std::string& strURL) { m_strURL = strURL; }
+
+ // Title
+ const std::string& GetTitle() const { return m_strTitle; }
+ void SetTitle(const std::string& strTitle) { m_strTitle = strTitle; }
+
+ // Platform
+ const std::string& GetPlatform() const { return m_strPlatform; }
+ void SetPlatform(const std::string& strPlatform) { m_strPlatform = strPlatform; }
+
+ // Genres
+ const std::vector<std::string>& GetGenres() const { return m_genres; }
+ void SetGenres(const std::vector<std::string>& genres) { m_genres = genres; }
+
+ // Developer
+ const std::string& GetDeveloper() const { return m_strDeveloper; }
+ void SetDeveloper(const std::string& strDeveloper) { m_strDeveloper = strDeveloper; }
+
+ // Overview
+ const std::string& GetOverview() const { return m_strOverview; }
+ void SetOverview(const std::string& strOverview) { m_strOverview = strOverview; }
+
+ // Year
+ unsigned int GetYear() const { return m_year; }
+ void SetYear(unsigned int year) { m_year = year; }
+
+ // Game Code (ID)
+ const std::string& GetID() const { return m_strID; }
+ void SetID(const std::string& strID) { m_strID = strID; }
+
+ // Region
+ const std::string& GetRegion() const { return m_strRegion; }
+ void SetRegion(const std::string& strRegion) { m_strRegion = strRegion; }
+
+ // Publisher / Licensee
+ const std::string& GetPublisher() const { return m_strPublisher; }
+ void SetPublisher(const std::string& strPublisher) { m_strPublisher = strPublisher; }
+
+ // Format (PAL/NTSC)
+ const std::string& GetFormat() const { return m_strFormat; }
+ void SetFormat(const std::string& strFormat) { m_strFormat = strFormat; }
+
+ // Cartridge Type, e.g. "ROM+MBC5+RAM+BATT" or "CD"
+ const std::string& GetCartridgeType() const { return m_strCartridgeType; }
+ void SetCartridgeType(const std::string& strCartridgeType)
+ {
+ m_strCartridgeType = strCartridgeType;
+ }
+
+ // Game client add-on ID
+ const std::string& GetGameClient() const { return m_strGameClient; }
+ void SetGameClient(const std::string& strGameClient) { m_strGameClient = strGameClient; }
+
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& value) const override;
+ void ToSortable(SortItem& sortable, Field field) const override;
+
+private:
+ bool m_bLoaded;
+ std::string m_strURL;
+ std::string m_strTitle;
+ std::string m_strPlatform;
+ std::vector<std::string> m_genres;
+ std::string m_strDeveloper;
+ std::string m_strOverview;
+ unsigned int m_year;
+ std::string m_strID;
+ std::string m_strRegion;
+ std::string m_strPublisher;
+ std::string m_strFormat;
+ std::string m_strCartridgeType;
+ std::string m_strGameClient;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/windows/CMakeLists.txt b/xbmc/games/windows/CMakeLists.txt
new file mode 100644
index 0000000..314608c
--- /dev/null
+++ b/xbmc/games/windows/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES GUIViewStateWindowGames.cpp
+ GUIWindowGames.cpp)
+
+set(HEADERS GUIViewStateWindowGames.h
+ GUIWindowGames.h)
+
+core_add_library(gameswindows)
diff --git a/xbmc/games/windows/GUIViewStateWindowGames.cpp b/xbmc/games/windows/GUIViewStateWindowGames.cpp
new file mode 100644
index 0000000..ffb9bf0
--- /dev/null
+++ b/xbmc/games/windows/GUIViewStateWindowGames.cpp
@@ -0,0 +1,91 @@
+/*
+ * 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 "GUIViewStateWindowGames.h"
+
+#include "FileItem.h"
+#include "games/GameUtils.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "settings/MediaSourceSettings.h"
+#include "utils/StringUtils.h"
+#include "view/ViewState.h"
+#include "view/ViewStateSettings.h"
+#include "windowing/GraphicContext.h" // include before ViewState.h
+
+#include <assert.h>
+#include <set>
+
+using namespace KODI;
+using namespace GAME;
+
+CGUIViewStateWindowGames::CGUIViewStateWindowGames(const CFileItemList& items)
+ : CGUIViewState(items)
+{
+ if (items.IsVirtualDirectoryRoot())
+ {
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS());
+ AddSortMethod(SortByDriveType, 564, LABEL_MASKS());
+ SetSortMethod(SortByLabel);
+ SetSortOrder(SortOrderAscending);
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+ }
+ else
+ {
+ AddSortMethod(SortByFile, 561,
+ LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Label, empty
+ AddSortMethod(SortBySize, 553,
+ LABEL_MASKS("%L", "%I", "%L", "%I")); // Filename, Size | Label, Size
+
+ const CViewState* viewState = CViewStateSettings::GetInstance().Get("games");
+ if (viewState)
+ {
+ SetSortMethod(viewState->m_sortDescription);
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ }
+
+ LoadViewState(items.GetPath(), WINDOW_GAMES);
+}
+
+std::string CGUIViewStateWindowGames::GetLockType()
+{
+ return "games";
+}
+
+std::string CGUIViewStateWindowGames::GetExtensions()
+{
+ using namespace ADDON;
+
+ std::set<std::string> exts = CGameUtils::GetGameExtensions();
+
+ // Ensure .zip appears
+ exts.insert(".zip");
+
+ return StringUtils::Join(exts, "|");
+}
+
+VECSOURCES& CGUIViewStateWindowGames::GetSources()
+{
+ VECSOURCES* pGameSources = CMediaSourceSettings::GetInstance().GetSources("games");
+
+ // Guard against source type not existing
+ if (pGameSources == nullptr)
+ {
+ static VECSOURCES empty;
+ return empty;
+ }
+
+ return *pGameSources;
+}
+
+void CGUIViewStateWindowGames::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_GAMES, CViewStateSettings::GetInstance().Get("games"));
+}
diff --git a/xbmc/games/windows/GUIViewStateWindowGames.h b/xbmc/games/windows/GUIViewStateWindowGames.h
new file mode 100644
index 0000000..90ad821
--- /dev/null
+++ b/xbmc/games/windows/GUIViewStateWindowGames.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "view/GUIViewState.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIViewStateWindowGames : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateWindowGames(const CFileItemList& items);
+
+ ~CGUIViewStateWindowGames() override = default;
+
+ // implementation of CGUIViewState
+ std::string GetLockType() override;
+ std::string GetExtensions() override;
+ VECSOURCES& GetSources() override;
+
+protected:
+ // implementation of CGUIViewState
+ void SaveViewState() override;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/games/windows/GUIWindowGames.cpp b/xbmc/games/windows/GUIWindowGames.cpp
new file mode 100644
index 0000000..a74186d
--- /dev/null
+++ b/xbmc/games/windows/GUIWindowGames.cpp
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2012-2020 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 "GUIWindowGames.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/gui/GUIDialogAddonInfo.h"
+#include "application/Application.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogMediaSource.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/ActionIDs.h"
+#include "media/MediaLockState.h"
+#include "playlists/PlayListTypes.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <algorithm>
+
+using namespace KODI;
+using namespace GAME;
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+
+CGUIWindowGames::CGUIWindowGames() : CGUIMediaWindow(WINDOW_GAMES, "MyGames.xml")
+{
+}
+
+bool CGUIWindowGames::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_rootDir.AllowNonLocalSources(true); //! @todo
+
+ // Is this the first time the window is opened?
+ if (m_vecItems->GetPath() == "?" && message.GetStringParam().empty())
+ message.SetStringParam(CMediaSourceSettings::GetInstance().GetDefaultSource("games"));
+
+ //! @todo
+ m_dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
+ WINDOW_DIALOG_PROGRESS);
+
+ break;
+ }
+ case GUI_MSG_CLICKED:
+ {
+ if (OnClickMsg(message.GetSenderId(), message.GetParam1()))
+ return true;
+ break;
+ }
+ default:
+ break;
+ }
+ return CGUIMediaWindow::OnMessage(message);
+}
+
+bool CGUIWindowGames::OnClickMsg(int controlId, int actionId)
+{
+ if (!m_viewControl.HasControl(controlId)) // list/thumb control
+ return false;
+
+ const int iItem = m_viewControl.GetSelectedItem();
+
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+ if (!pItem)
+ return false;
+
+ switch (actionId)
+ {
+ case ACTION_DELETE_ITEM:
+ {
+ // Is delete allowed?
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_ALLOWFILEDELETION))
+ {
+ OnDeleteItem(iItem);
+ return true;
+ }
+ break;
+ }
+ case ACTION_PLAYER_PLAY:
+ {
+ if (OnClick(iItem))
+ return true;
+ break;
+ }
+ case ACTION_SHOW_INFO:
+ {
+ if (!m_vecItems->IsPlugin())
+ {
+ if (pItem->HasAddonInfo())
+ {
+ CGUIDialogAddonInfo::ShowForItem(pItem);
+ return true;
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
+
+void CGUIWindowGames::SetupShares()
+{
+ CGUIMediaWindow::SetupShares();
+
+ // Don't convert zip files to directories. Otherwise, the files will be
+ // opened and scanned for games with a valid extension. If none are found,
+ // the .zip won't be shown.
+ //
+ // This is a problem for MAME roms, because the files inside the .zip don't
+ // have standard extensions.
+ //
+ m_rootDir.SetFlags(XFILE::DIR_FLAG_NO_FILE_DIRS);
+}
+
+bool CGUIWindowGames::OnClick(int iItem, const std::string& player /* = "" */)
+{
+ CFileItemPtr item = m_vecItems->Get(iItem);
+ if (item)
+ {
+ // Compensate for DIR_FLAG_NO_FILE_DIRS flag
+ if (URIUtils::IsArchive(item->GetPath()))
+ {
+ bool bIsGame = false;
+
+ // If zip file contains no games, assume it is a game
+ CFileItemList items;
+ if (m_rootDir.GetDirectory(CURL(item->GetPath()), items))
+ {
+ if (items.Size() == 0)
+ bIsGame = true;
+ }
+
+ if (!bIsGame)
+ item->m_bIsFolder = true;
+ }
+
+ if (!item->m_bIsFolder)
+ {
+ PlayGame(*item);
+ return true;
+ }
+ }
+
+ return CGUIMediaWindow::OnClick(iItem, player);
+}
+
+void CGUIWindowGames::GetContextButtons(int itemNumber, CContextButtons& buttons)
+{
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+
+ if (item && !item->GetProperty("pluginreplacecontextitems").asBoolean())
+ {
+ if (m_vecItems->IsVirtualDirectoryRoot() || m_vecItems->IsSourcesPath())
+ {
+ // Context buttons for a sources path, like "Add Source", "Remove Source", etc.
+ CGUIDialogContextMenu::GetContextButtons("games", item, buttons);
+ }
+ else
+ {
+ if (item->IsGame())
+ {
+ buttons.Add(CONTEXT_BUTTON_PLAY_ITEM, 208); // Play
+ }
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_ALLOWFILEDELETION) &&
+ !item->IsReadOnly())
+ {
+ buttons.Add(CONTEXT_BUTTON_DELETE, 117);
+ buttons.Add(CONTEXT_BUTTON_RENAME, 118);
+ }
+ }
+ }
+
+ CGUIMediaWindow::GetContextButtons(itemNumber, buttons);
+}
+
+bool CGUIWindowGames::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+ if (item)
+ {
+ if (m_vecItems->IsVirtualDirectoryRoot() || m_vecItems->IsSourcesPath())
+ {
+ if (CGUIDialogContextMenu::OnContextButton("games", item, button))
+ {
+ Update(m_vecItems->GetPath());
+ return true;
+ }
+ }
+ switch (button)
+ {
+ case CONTEXT_BUTTON_PLAY_ITEM:
+ PlayGame(*item);
+ return true;
+ case CONTEXT_BUTTON_INFO:
+ CGUIDialogAddonInfo::ShowForItem(item);
+ return true;
+ case CONTEXT_BUTTON_DELETE:
+ OnDeleteItem(itemNumber);
+ return true;
+ case CONTEXT_BUTTON_RENAME:
+ OnRenameItem(itemNumber);
+ return true;
+ default:
+ break;
+ }
+ }
+ return CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowGames::OnAddMediaSource()
+{
+ return CGUIDialogMediaSource::ShowAndAddMediaSource("games");
+}
+
+bool CGUIWindowGames::GetDirectory(const std::string& strDirectory, CFileItemList& items)
+{
+ if (!CGUIMediaWindow::GetDirectory(strDirectory, items))
+ return false;
+
+ // Set label
+ std::string label;
+ if (items.GetLabel().empty())
+ {
+ std::string source;
+ if (m_rootDir.IsSource(items.GetPath(), CMediaSourceSettings::GetInstance().GetSources("games"),
+ &source))
+ label = std::move(source);
+ }
+
+ if (!label.empty())
+ items.SetLabel(label);
+
+ // Set content
+ std::string content;
+ if (items.GetContent().empty())
+ {
+ if (!items.IsVirtualDirectoryRoot() && // Don't set content for root directory
+ !items.IsPlugin()) // Don't set content for plugins
+ {
+ content = "games";
+ }
+ }
+
+ if (!content.empty())
+ items.SetContent(content);
+
+ // Ensure a game info tag is created so that files are recognized as games
+ for (const CFileItemPtr& item : items)
+ {
+ if (!item->m_bIsFolder)
+ item->GetGameInfoTag();
+ }
+
+ return true;
+}
+
+std::string CGUIWindowGames::GetStartFolder(const std::string& dir)
+{
+ // From CGUIWindowPictures::GetStartFolder()
+
+ if (StringUtils::EqualsNoCase(dir, "plugins") || StringUtils::EqualsNoCase(dir, "addons"))
+ {
+ return "addons://sources/game/";
+ }
+
+ SetupShares();
+ VECSOURCES shares;
+ m_rootDir.GetSources(shares);
+ bool bIsSourceName = false;
+ int iIndex = CUtil::GetMatchingSource(dir, shares, bIsSourceName);
+ if (iIndex >= 0)
+ {
+ if (iIndex < static_cast<int>(shares.size()) && shares[iIndex].m_iHasLock == LOCK_STATE_LOCKED)
+ {
+ CFileItem item(shares[iIndex]);
+ if (!g_passwordManager.IsItemUnlocked(&item, "games"))
+ return "";
+ }
+ if (bIsSourceName)
+ return shares[iIndex].strPath;
+ return dir;
+ }
+ return CGUIMediaWindow::GetStartFolder(dir);
+}
+
+void CGUIWindowGames::OnItemInfo(int itemNumber)
+{
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+ if (!item)
+ return;
+
+ if (!m_vecItems->IsPlugin())
+ {
+ if (item->IsPlugin() || item->IsScript())
+ CGUIDialogAddonInfo::ShowForItem(item);
+ }
+
+ //! @todo
+ /*
+ CGUIDialogGameInfo* gameInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogGameInfo>(WINDOW_DIALOG_PICTURE_INFO);
+ if (gameInfo)
+ {
+ gameInfo->SetGame(item);
+ gameInfo->Open();
+ }
+ */
+}
+
+bool CGUIWindowGames::PlayGame(const CFileItem& item)
+{
+ CFileItem itemCopy(item);
+ return g_application.PlayMedia(itemCopy, "", PLAYLIST::TYPE_NONE);
+}
diff --git a/xbmc/games/windows/GUIWindowGames.h b/xbmc/games/windows/GUIWindowGames.h
new file mode 100644
index 0000000..2fbabfa
--- /dev/null
+++ b/xbmc/games/windows/GUIWindowGames.h
@@ -0,0 +1,45 @@
+/*
+ * 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 "windows/GUIMediaWindow.h"
+
+class CGUIDialogProgress;
+
+namespace KODI
+{
+namespace GAME
+{
+class CGUIWindowGames : public CGUIMediaWindow
+{
+public:
+ CGUIWindowGames();
+ ~CGUIWindowGames() override = default;
+
+ // implementation of CGUIControl via CGUIMediaWindow
+ bool OnMessage(CGUIMessage& message) override;
+
+protected:
+ // implementation of CGUIMediaWindow
+ void SetupShares() override;
+ bool OnClick(int iItem, const std::string& player = "") override;
+ void GetContextButtons(int itemNumber, CContextButtons& buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool OnAddMediaSource() override;
+ bool GetDirectory(const std::string& strDirectory, CFileItemList& items) override;
+ std::string GetStartFolder(const std::string& dir) override;
+
+ bool OnClickMsg(int controlId, int actionId);
+ void OnItemInfo(int itemNumber);
+ bool PlayGame(const CFileItem& item);
+
+ CGUIDialogProgress* m_dlgProgress = nullptr;
+};
+} // namespace GAME
+} // namespace KODI
diff --git a/xbmc/guilib/CMakeLists.txt b/xbmc/guilib/CMakeLists.txt
new file mode 100644
index 0000000..d96ce09
--- /dev/null
+++ b/xbmc/guilib/CMakeLists.txt
@@ -0,0 +1,233 @@
+set(SOURCES DDSImage.cpp
+ DirtyRegionSolvers.cpp
+ DirtyRegionTracker.cpp
+ FFmpegImage.cpp
+ GUIAction.cpp
+ GUIAudioManager.cpp
+ GUIBaseContainer.cpp
+ GUIBorderedImage.cpp
+ GUIButtonControl.cpp
+ GUIColorButtonControl.cpp
+ GUIColorManager.cpp
+ GUIComponent.cpp
+ GUIControl.cpp
+ GUIControlFactory.cpp
+ GUIControlGroup.cpp
+ GUIControlGroupList.cpp
+ GUIControlLookup.cpp
+ GUIControlProfiler.cpp
+ GUIDialog.cpp
+ GUIEditControl.cpp
+ GUIFadeLabelControl.cpp
+ GUIFixedListContainer.cpp
+ GUIFont.cpp
+ GUIFontCache.cpp
+ GUIFontManager.cpp
+ GUIFontTTF.cpp
+ GUIImage.cpp
+ GUIIncludes.cpp
+ GUIKeyboardFactory.cpp
+ GUILabelControl.cpp
+ GUILabel.cpp
+ GUIListContainer.cpp
+ GUIListGroup.cpp
+ GUIListItem.cpp
+ GUIListItemLayout.cpp
+ GUIListLabel.cpp
+ GUIMessage.cpp
+ GUIMoverControl.cpp
+ GUIMultiImage.cpp
+ GUIPanelContainer.cpp
+ GUIProgressControl.cpp
+ GUIRadioButtonControl.cpp
+ GUIRangesControl.cpp
+ GUIRenderingControl.cpp
+ GUIResizeControl.cpp
+ GUIRSSControl.cpp
+ GUIScrollBarControl.cpp
+ GUISettingsSliderControl.cpp
+ GUISliderControl.cpp
+ GUISpinControl.cpp
+ GUISpinControlEx.cpp
+ GUIStaticItem.cpp
+ GUITextBox.cpp
+ GUITextLayout.cpp
+ GUITexture.cpp
+ GUIToggleButtonControl.cpp
+ GUIVideoControl.cpp
+ GUIVisualisationControl.cpp
+ GUIWindow.cpp
+ GUIWindowManager.cpp
+ GUIWrappingListContainer.cpp
+ imagefactory.cpp
+ IWindowManagerCallback.cpp
+ LocalizeStrings.cpp
+ StereoscopicsManager.cpp
+ TextureBundle.cpp
+ TextureBundleXBT.cpp
+ Texture.cpp
+ TextureManager.cpp
+ VisibleEffect.cpp
+ XBTF.cpp
+ XBTFReader.cpp)
+
+set(HEADERS DDSImage.h
+ DirtyRegion.h
+ DirtyRegionSolvers.h
+ DirtyRegionTracker.h
+ DispResource.h
+ FFmpegImage.h
+ gui3d.h
+ GUIAction.h
+ GUIAudioManager.h
+ GUIBaseContainer.h
+ GUIBorderedImage.h
+ GUIButtonControl.h
+ GUIColorButtonControl.h
+ GUIColorManager.h
+ GUIComponent.h
+ GUIControl.h
+ GUIControlFactory.h
+ GUIControlGroup.h
+ GUIControlGroupList.h
+ GUIControlProfiler.h
+ GUIControlLookup.h
+ GUIDialog.h
+ GUIEditControl.h
+ GUIFadeLabelControl.h
+ GUIFixedListContainer.h
+ GUIFont.h
+ GUIFontCache.h
+ GUIFontManager.h
+ GUIFontTTF.h
+ GUIImage.h
+ GUIIncludes.h
+ GUIKeyboard.h
+ GUIKeyboardFactory.h
+ GUILabel.h
+ GUILabelControl.h
+ GUIListContainer.h
+ GUIListGroup.h
+ GUIListItem.h
+ GUIListItemLayout.h
+ GUIListLabel.h
+ GUIMessage.h
+ GUIMoverControl.h
+ GUIMultiImage.h
+ GUIPanelContainer.h
+ GUIProgressControl.h
+ GUIRadioButtonControl.h
+ GUIRangesControl.h
+ GUIRenderingControl.h
+ GUIResizeControl.h
+ GUIRSSControl.h
+ GUIScrollBarControl.h
+ GUISettingsSliderControl.h
+ GUISliderControl.h
+ GUISpinControl.h
+ GUISpinControlEx.h
+ GUIStaticItem.h
+ GUITextBox.h
+ GUITextLayout.h
+ GUITexture.h
+ GUIToggleButtonControl.h
+ GUIVideoControl.h
+ GUIVisualisationControl.h
+ GUIWindow.h
+ GUIWindowManager.h
+ GUIWrappingListContainer.h
+ IAudioDeviceChangedCallback.h
+ IDirtyRegionSolver.h
+ IGUIContainer.h
+ iimage.h
+ imagefactory.h
+ IMsgTargetCallback.h
+ IRenderingCallback.h
+ ISliderCallback.h
+ IWindowManagerCallback.h
+ LocalizeStrings.h
+ StereoscopicsManager.h
+ Texture.h
+ TextureBundle.h
+ TextureBundleXBT.h
+ TextureManager.h
+ Tween.h
+ VisibleEffect.h
+ WindowIDs.h
+ XBTF.h
+ XBTFReader.h)
+
+if(OPENGL_FOUND OR OPENGLES_FOUND)
+ list(APPEND SOURCES GUIFontTTFGL.cpp
+ Shader.cpp
+ TextureGL.cpp)
+ list(APPEND HEADERS GUIFontTTFGL.h
+ Shader.h
+ TextureGL.h)
+
+ if(OPENGL_FOUND)
+ list(APPEND SOURCES GUITextureGL.cpp)
+ list(APPEND HEADERS GUITextureGL.h)
+ endif()
+
+ if(OPENGLES_FOUND)
+ list(APPEND SOURCES GUITextureGLES.cpp)
+ list(APPEND HEADERS GUITextureGLES.h)
+ endif()
+
+endif()
+
+if(CORE_SYSTEM_NAME STREQUAL windows OR CORE_SYSTEM_NAME STREQUAL windowsstore)
+ list(APPEND SOURCES D3DResource.cpp
+ DirectXGraphics.cpp
+ GUIFontTTFDX.cpp
+ GUIShaderDX.cpp
+ GUITextureD3D.cpp
+ TextureDX.cpp)
+ list(APPEND HEADERS D3DResource.h
+ DirectXGraphics.h
+ GUIFontTTFDX.h
+ GUIShaderDX.h
+ GUITextureD3D.h
+ TextureDX.h)
+endif()
+
+core_add_library(guilib)
+
+if(CORE_SYSTEM_NAME STREQUAL windows OR CORE_SYSTEM_NAME STREQUAL windowsstore)
+ set(SHADERS_VERTEX guishader_vert.hlsl)
+ set(SHADERS_PIXEL guishader_checkerboard_right.hlsl
+ guishader_checkerboard_left.hlsl
+ guishader_default.hlsl
+ guishader_fonts.hlsl
+ guishader_interlaced_right.hlsl
+ guishader_interlaced_left.hlsl
+ guishader_multi_texture_blend.hlsl
+ guishader_texture.hlsl
+ guishader_texture_noblend.hlsl)
+ foreach(shader ${SHADERS_VERTEX})
+ get_filename_component(file ${shader} NAME_WE)
+ add_custom_command(OUTPUT ${file}.h
+ COMMAND ${FXC} /Fh ${file}.h /E VS /T vs_4_0_level_9_1 /Vn ${file} /Qstrip_reflect
+ ${CMAKE_SOURCE_DIR}/system/shaders/${shader}
+ DEPENDS ${CMAKE_SOURCE_DIR}/system/shaders/${shader}
+ COMMENT "FX compile vertex shader ${shader}"
+ VERBATIM)
+ list(APPEND SHADERS ${file}.h)
+ endforeach()
+ foreach(shader ${SHADERS_PIXEL})
+ get_filename_component(file ${shader} NAME_WE)
+ add_custom_command(OUTPUT ${file}.h
+ COMMAND ${FXC} /Fh ${file}.h /E PS /T ps_4_0_level_9_1 /Vn ${file} /Qstrip_reflect
+ ${CMAKE_SOURCE_DIR}/system/shaders/${shader}
+ DEPENDS ${CMAKE_SOURCE_DIR}/system/shaders/${shader}
+ COMMENT "FX compile pixel shader ${shader}"
+ VERBATIM)
+ list(APPEND SHADERS ${file}.h)
+ endforeach()
+
+ add_custom_target(generate_shaders ALL DEPENDS ${SHADERS})
+ set_target_properties(generate_shaders PROPERTIES FOLDER "Build Utilities")
+ target_include_directories(${CORE_LIBRARY} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+ add_dependencies(${CORE_LIBRARY} generate_shaders)
+endif()
diff --git a/xbmc/guilib/D3DResource.cpp b/xbmc/guilib/D3DResource.cpp
new file mode 100644
index 0000000..0f1dc89
--- /dev/null
+++ b/xbmc/guilib/D3DResource.cpp
@@ -0,0 +1,1244 @@
+/*
+ * 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 "D3DResource.h"
+
+#include "GUIShaderDX.h"
+#include "filesystem/File.h"
+#include "rendering/dx/DeviceResources.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/log.h"
+
+#include <d3dcompiler.h>
+
+using namespace DirectX;
+using namespace Microsoft::WRL;
+
+#ifdef TARGET_WINDOWS_DESKTOP
+#pragma comment(lib, "d3dcompiler.lib")
+#endif
+
+size_t CD3DHelper::BitsPerPixel(DXGI_FORMAT fmt)
+{
+ switch (fmt)
+ {
+ case DXGI_FORMAT_R32G32B32A32_TYPELESS:
+ case DXGI_FORMAT_R32G32B32A32_FLOAT:
+ case DXGI_FORMAT_R32G32B32A32_UINT:
+ case DXGI_FORMAT_R32G32B32A32_SINT:
+ return 128;
+
+ case DXGI_FORMAT_R32G32B32_TYPELESS:
+ case DXGI_FORMAT_R32G32B32_FLOAT:
+ case DXGI_FORMAT_R32G32B32_UINT:
+ case DXGI_FORMAT_R32G32B32_SINT:
+ return 96;
+
+ case DXGI_FORMAT_R16G16B16A16_TYPELESS:
+ case DXGI_FORMAT_R16G16B16A16_FLOAT:
+ case DXGI_FORMAT_R16G16B16A16_UNORM:
+ case DXGI_FORMAT_R16G16B16A16_UINT:
+ case DXGI_FORMAT_R16G16B16A16_SNORM:
+ case DXGI_FORMAT_R16G16B16A16_SINT:
+ case DXGI_FORMAT_R32G32_TYPELESS:
+ case DXGI_FORMAT_R32G32_FLOAT:
+ case DXGI_FORMAT_R32G32_UINT:
+ case DXGI_FORMAT_R32G32_SINT:
+ case DXGI_FORMAT_R32G8X24_TYPELESS:
+ case DXGI_FORMAT_D32_FLOAT_S8X24_UINT:
+ case DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS:
+ case DXGI_FORMAT_X32_TYPELESS_G8X24_UINT:
+ return 64;
+
+ case DXGI_FORMAT_R10G10B10A2_TYPELESS:
+ case DXGI_FORMAT_R10G10B10A2_UNORM:
+ case DXGI_FORMAT_R10G10B10A2_UINT:
+ case DXGI_FORMAT_R11G11B10_FLOAT:
+ case DXGI_FORMAT_R8G8B8A8_TYPELESS:
+ case DXGI_FORMAT_R8G8B8A8_UNORM:
+ case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
+ case DXGI_FORMAT_R8G8B8A8_UINT:
+ case DXGI_FORMAT_R8G8B8A8_SNORM:
+ case DXGI_FORMAT_R8G8B8A8_SINT:
+ case DXGI_FORMAT_R16G16_TYPELESS:
+ case DXGI_FORMAT_R16G16_FLOAT:
+ case DXGI_FORMAT_R16G16_UNORM:
+ case DXGI_FORMAT_R16G16_UINT:
+ case DXGI_FORMAT_R16G16_SNORM:
+ case DXGI_FORMAT_R16G16_SINT:
+ case DXGI_FORMAT_R32_TYPELESS:
+ case DXGI_FORMAT_D32_FLOAT:
+ case DXGI_FORMAT_R32_FLOAT:
+ case DXGI_FORMAT_R32_UINT:
+ case DXGI_FORMAT_R32_SINT:
+ case DXGI_FORMAT_R24G8_TYPELESS:
+ case DXGI_FORMAT_D24_UNORM_S8_UINT:
+ case DXGI_FORMAT_R24_UNORM_X8_TYPELESS:
+ case DXGI_FORMAT_X24_TYPELESS_G8_UINT:
+ case DXGI_FORMAT_R9G9B9E5_SHAREDEXP:
+ case DXGI_FORMAT_R8G8_B8G8_UNORM:
+ case DXGI_FORMAT_G8R8_G8B8_UNORM:
+ case DXGI_FORMAT_B8G8R8A8_UNORM:
+ case DXGI_FORMAT_B8G8R8X8_UNORM:
+ case DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM:
+ case DXGI_FORMAT_B8G8R8A8_TYPELESS:
+ case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
+ case DXGI_FORMAT_B8G8R8X8_TYPELESS:
+ case DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
+ return 32;
+
+ case DXGI_FORMAT_R8G8_TYPELESS:
+ case DXGI_FORMAT_R8G8_UNORM:
+ case DXGI_FORMAT_R8G8_UINT:
+ case DXGI_FORMAT_R8G8_SNORM:
+ case DXGI_FORMAT_R8G8_SINT:
+ case DXGI_FORMAT_R16_TYPELESS:
+ case DXGI_FORMAT_R16_FLOAT:
+ case DXGI_FORMAT_D16_UNORM:
+ case DXGI_FORMAT_R16_UNORM:
+ case DXGI_FORMAT_R16_UINT:
+ case DXGI_FORMAT_R16_SNORM:
+ case DXGI_FORMAT_R16_SINT:
+ case DXGI_FORMAT_B5G6R5_UNORM:
+ case DXGI_FORMAT_B5G5R5A1_UNORM:
+ case DXGI_FORMAT_B4G4R4A4_UNORM:
+ return 16;
+
+ case DXGI_FORMAT_R8_TYPELESS:
+ case DXGI_FORMAT_R8_UNORM:
+ case DXGI_FORMAT_R8_UINT:
+ case DXGI_FORMAT_R8_SNORM:
+ case DXGI_FORMAT_R8_SINT:
+ case DXGI_FORMAT_A8_UNORM:
+ return 8;
+
+ case DXGI_FORMAT_R1_UNORM:
+ return 1;
+
+ case DXGI_FORMAT_BC1_TYPELESS:
+ case DXGI_FORMAT_BC1_UNORM:
+ case DXGI_FORMAT_BC1_UNORM_SRGB:
+ case DXGI_FORMAT_BC4_TYPELESS:
+ case DXGI_FORMAT_BC4_UNORM:
+ case DXGI_FORMAT_BC4_SNORM:
+ return 4;
+
+ case DXGI_FORMAT_BC2_TYPELESS:
+ case DXGI_FORMAT_BC2_UNORM:
+ case DXGI_FORMAT_BC2_UNORM_SRGB:
+ case DXGI_FORMAT_BC3_TYPELESS:
+ case DXGI_FORMAT_BC3_UNORM:
+ case DXGI_FORMAT_BC3_UNORM_SRGB:
+ case DXGI_FORMAT_BC5_TYPELESS:
+ case DXGI_FORMAT_BC5_UNORM:
+ case DXGI_FORMAT_BC5_SNORM:
+ case DXGI_FORMAT_BC6H_TYPELESS:
+ case DXGI_FORMAT_BC6H_UF16:
+ case DXGI_FORMAT_BC6H_SF16:
+ case DXGI_FORMAT_BC7_TYPELESS:
+ case DXGI_FORMAT_BC7_UNORM:
+ case DXGI_FORMAT_BC7_UNORM_SRGB:
+ return 8;
+
+ default:
+ return 0;
+ }
+}
+
+void ID3DResource::Register()
+{
+ if (!m_bRegistered)
+ DX::Windowing()->Register(this);
+ m_bRegistered = true;
+}
+
+void ID3DResource::Unregister()
+{
+ if (m_bRegistered)
+ DX::Windowing()->Unregister(this);
+ m_bRegistered = false;
+}
+
+CD3DTexture::CD3DTexture()
+{
+ m_width = 0;
+ m_height = 0;
+ m_mipLevels = 0;
+ m_usage = D3D11_USAGE_DEFAULT;
+ m_format = DXGI_FORMAT_B8G8R8A8_UNORM;
+ m_texture = nullptr;
+ m_renderTargets[0] = nullptr;
+ m_renderTargets[1] = nullptr;
+ m_data = nullptr;
+ m_pitch = 0;
+ m_bindFlags = 0;
+ m_cpuFlags = 0;
+ m_viewIdx = 0;
+ m_views.clear();
+}
+
+CD3DTexture::~CD3DTexture()
+{
+ Release();
+ delete[] m_data;
+}
+
+bool CD3DTexture::Create(UINT width, UINT height, UINT mipLevels, D3D11_USAGE usage, DXGI_FORMAT format, const void* pixels /* nullptr */, unsigned int srcPitch /* 0 */)
+{
+ m_width = width;
+ m_height = height;
+ m_mipLevels = mipLevels;
+ // create the texture
+ Release();
+
+ if (format == DXGI_FORMAT_UNKNOWN)
+ format = DXGI_FORMAT_B8G8R8A8_UNORM; // DXGI_FORMAT_UNKNOWN
+
+ if (!DX::Windowing()->IsFormatSupport(format, D3D11_FORMAT_SUPPORT_TEXTURE2D))
+ {
+ CLog::LogF(LOGERROR, "unsupported texture format {}", format);
+ return false;
+ }
+
+ m_cpuFlags = 0;
+ if (usage == D3D11_USAGE_DYNAMIC || usage == D3D11_USAGE_STAGING)
+ {
+ m_cpuFlags |= D3D11_CPU_ACCESS_WRITE;
+ if (usage == D3D11_USAGE_STAGING)
+ m_cpuFlags |= D3D11_CPU_ACCESS_READ;
+ }
+
+ m_format = format;
+ m_usage = usage;
+
+ m_bindFlags = 0; // D3D11_BIND_SHADER_RESOURCE;
+ if (D3D11_USAGE_DEFAULT == usage && DX::Windowing()->IsFormatSupport(format, D3D11_FORMAT_SUPPORT_RENDER_TARGET))
+ m_bindFlags |= D3D11_BIND_RENDER_TARGET;
+ if ( D3D11_USAGE_STAGING != m_usage )
+ {
+ if (DX::Windowing()->IsFormatSupport(format, D3D11_FORMAT_SUPPORT_SHADER_LOAD)
+ || DX::Windowing()->IsFormatSupport(format, D3D11_FORMAT_SUPPORT_SHADER_SAMPLE))
+ {
+ m_bindFlags |= D3D11_BIND_SHADER_RESOURCE;
+ }
+ if (DX::Windowing()->IsFormatSupport(format, D3D11_FORMAT_SUPPORT_DECODER_OUTPUT))
+ {
+ m_bindFlags |= D3D11_BIND_DECODER;
+ }
+ }
+
+ if (!CreateInternal(pixels, srcPitch))
+ {
+ CLog::LogF(LOGERROR, "failed to create texture.");
+ return false;
+ }
+
+ Register();
+
+ return true;
+}
+
+bool CD3DTexture::CreateInternal(const void* pixels /* nullptr */, unsigned int srcPitch /* 0 */)
+{
+ ComPtr<ID3D11Device> pD3DDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ ComPtr<ID3D11DeviceContext> pD3D11Context = DX::DeviceResources::Get()->GetD3DContext();
+
+ UINT miscFlags = 0;
+ bool autogenmm = false;
+ if (m_mipLevels == 0 && DX::Windowing()->IsFormatSupport(m_format, D3D11_FORMAT_SUPPORT_MIP_AUTOGEN))
+ {
+ autogenmm = pixels != nullptr;
+ miscFlags |= D3D11_RESOURCE_MISC_GENERATE_MIPS;
+ }
+ else
+ m_mipLevels = 1;
+
+ CD3D11_TEXTURE2D_DESC textureDesc(m_format, m_width, m_height, 1, m_mipLevels, m_bindFlags, m_usage, m_cpuFlags, 1, 0, miscFlags);
+ D3D11_SUBRESOURCE_DATA initData = {};
+ initData.pSysMem = pixels;
+ initData.SysMemPitch = srcPitch ? srcPitch : CD3DHelper::BitsPerPixel(m_format) * m_width / 8;
+ initData.SysMemSlicePitch = 0;
+
+ HRESULT hr = pD3DDevice->CreateTexture2D(&textureDesc, (!autogenmm && pixels) ? &initData : nullptr, m_texture.ReleaseAndGetAddressOf());
+ if (SUCCEEDED(hr) && autogenmm)
+ {
+ pD3D11Context->UpdateSubresource(m_texture.Get(), 0, nullptr, pixels,
+ (srcPitch ? srcPitch : CD3DHelper::BitsPerPixel(m_format) * m_width / 8), 0);
+ }
+
+ if (autogenmm)
+ GenerateMipmaps();
+
+ return SUCCEEDED(hr);
+}
+
+ID3D11ShaderResourceView* CD3DTexture::GetShaderResource(DXGI_FORMAT format /* = DXGI_FORMAT_UNKNOWN */)
+{
+ if (!m_texture)
+ return nullptr;
+
+ if (format == DXGI_FORMAT_UNKNOWN)
+ format = m_format;
+
+ if (!DX::Windowing()->IsFormatSupport(format, D3D11_FORMAT_SUPPORT_SHADER_SAMPLE)
+ && !DX::Windowing()->IsFormatSupport(format, D3D11_FORMAT_SUPPORT_SHADER_LOAD))
+ return nullptr;
+
+ if (!m_views[format])
+ {
+ ComPtr<ID3D11ShaderResourceView> view;
+ CD3D11_SHADER_RESOURCE_VIEW_DESC cSRVDesc(D3D11_SRV_DIMENSION_TEXTURE2D, format);
+ HRESULT hr = DX::DeviceResources::Get()->GetD3DDevice()->CreateShaderResourceView(m_texture.Get(), &cSRVDesc, view.GetAddressOf());
+ if (SUCCEEDED(hr))
+ m_views.insert_or_assign(format, view);
+ else
+ CLog::LogF(LOGWARNING, "cannot create texture view.");
+ }
+
+ return m_views[format].Get();
+}
+
+ID3D11ShaderResourceView** CD3DTexture::GetAddressOfSRV(DXGI_FORMAT format)
+{
+ if (format == DXGI_FORMAT_UNKNOWN)
+ format = m_format;
+
+ if (!m_views[format])
+ GetShaderResource(format);
+
+ return m_views[format].GetAddressOf();
+}
+
+ID3D11RenderTargetView* CD3DTexture::GetRenderTarget()
+{
+ return GetRenderTargetInternal(m_viewIdx);
+}
+
+ID3D11RenderTargetView** CD3DTexture::GetAddressOfRTV()
+{
+ if (!m_renderTargets[m_viewIdx])
+ GetRenderTargetInternal(m_viewIdx);
+ return m_renderTargets[m_viewIdx].GetAddressOf();
+}
+
+void CD3DTexture::Release()
+{
+ Unregister();
+
+ m_views.clear();
+ m_renderTargets[0] = nullptr;
+ m_renderTargets[1] = nullptr;
+ m_texture = nullptr;
+}
+
+bool CD3DTexture::GetDesc(D3D11_TEXTURE2D_DESC *desc) const
+{
+ if (m_texture)
+ {
+ m_texture->GetDesc(desc);
+ return true;
+ }
+ return false;
+}
+
+bool CD3DTexture::LockRect(UINT subresource, D3D11_MAPPED_SUBRESOURCE *res, D3D11_MAP mapType) const
+{
+ if (m_texture)
+ {
+ if (m_usage == D3D11_USAGE_DEFAULT)
+ return false;
+ if ((mapType == D3D11_MAP_READ || mapType == D3D11_MAP_READ_WRITE) && m_usage == D3D11_USAGE_DYNAMIC)
+ return false;
+
+ return (S_OK == DX::DeviceResources::Get()->GetImmediateContext()->Map(m_texture.Get(), subresource, mapType, 0, res));
+ }
+ return false;
+}
+
+bool CD3DTexture::UnlockRect(UINT subresource) const
+{
+ if (m_texture)
+ {
+ DX::DeviceResources::Get()->GetImmediateContext()->Unmap(m_texture.Get(), subresource);
+ return true;
+ }
+ return false;
+}
+
+void CD3DTexture::SaveTexture()
+{
+ if (m_texture)
+ {
+ delete[] m_data;
+ m_data = nullptr;
+
+ ID3D11DeviceContext* pContext = DX::DeviceResources::Get()->GetImmediateContext();
+
+ D3D11_TEXTURE2D_DESC textureDesc;
+ m_texture->GetDesc(&textureDesc);
+
+ ComPtr<ID3D11Texture2D> texture = nullptr;
+ if (textureDesc.Usage != D3D11_USAGE_STAGING || 0 == (textureDesc.CPUAccessFlags & D3D11_CPU_ACCESS_READ))
+ {
+ // create texture which can be readed by CPU - D3D11_USAGE_STAGING
+ CD3D11_TEXTURE2D_DESC stagingDesc(textureDesc);
+ stagingDesc.Usage = D3D11_USAGE_STAGING;
+ stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ stagingDesc.BindFlags = 0;
+
+ if (FAILED(DX::DeviceResources::Get()->GetD3DDevice()->CreateTexture2D(&stagingDesc, NULL, &texture)))
+ return;
+
+ // copy contents to new texture
+ pContext->CopyResource(texture.Get(), m_texture.Get());
+ }
+ else
+ texture = m_texture;
+
+ // read data from texture
+ D3D11_MAPPED_SUBRESOURCE res;
+ if (SUCCEEDED(pContext->Map(texture.Get(), 0, D3D11_MAP_READ, 0, &res)))
+ {
+ m_pitch = res.RowPitch;
+ unsigned int memUsage = GetMemoryUsage(res.RowPitch);
+ m_data = new unsigned char[memUsage];
+ memcpy(m_data, res.pData, memUsage);
+ pContext->Unmap(texture.Get(), 0);
+ }
+ else
+ CLog::LogF(LOGERROR, "Failed to store resource.");
+ }
+}
+
+void CD3DTexture::OnDestroyDevice(bool fatal)
+{
+ if (!fatal)
+ SaveTexture();
+ m_views.clear();
+ m_renderTargets[0] = nullptr;
+ m_renderTargets[1] = nullptr;
+ m_texture = nullptr;
+}
+
+void CD3DTexture::RestoreTexture()
+{
+ // yay, we're back - make a new copy of the texture
+ if (!m_texture && m_data)
+ {
+ if (!CreateInternal(m_data, m_pitch))
+ {
+ CLog::LogF(LOGERROR, "failed restore texture");
+ }
+
+ delete[] m_data;
+ m_data = NULL;
+ m_pitch = 0;
+ }
+}
+
+void CD3DTexture::OnCreateDevice()
+{
+ RestoreTexture();
+}
+
+ID3D11RenderTargetView* CD3DTexture::GetRenderTargetInternal(unsigned idx)
+{
+ if (idx > 1)
+ return nullptr;
+
+ if (!m_texture)
+ return nullptr;
+
+ if (!DX::Windowing()->IsFormatSupport(m_format, D3D11_FORMAT_SUPPORT_RENDER_TARGET))
+ return nullptr;
+
+ if (!m_renderTargets[idx])
+ {
+ CD3D11_RENDER_TARGET_VIEW_DESC cRTVDesc(D3D11_RTV_DIMENSION_TEXTURE2DARRAY, DXGI_FORMAT_UNKNOWN, 0, idx, 1);
+ if (FAILED(DX::DeviceResources::Get()->GetD3DDevice()->CreateRenderTargetView(m_texture.Get(), &cRTVDesc, m_renderTargets[idx].ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGWARNING, "cannot create texture view.");
+ }
+ }
+
+ return m_renderTargets[idx].Get();
+}
+
+unsigned int CD3DTexture::GetMemoryUsage(unsigned int pitch) const
+{
+ switch (m_format)
+ {
+ case DXGI_FORMAT_BC1_UNORM:
+ case DXGI_FORMAT_BC2_UNORM:
+ case DXGI_FORMAT_BC3_UNORM:
+ return pitch * m_height / 4;
+ default:
+ return pitch * m_height;
+ }
+}
+
+void CD3DTexture::GenerateMipmaps()
+{
+ if (m_mipLevels == 0)
+ {
+ ID3D11ShaderResourceView* pSRView = GetShaderResource();
+ if (pSRView != nullptr)
+ DX::DeviceResources::Get()->GetD3DContext()->GenerateMips(pSRView);
+ }
+}
+
+// static methods
+void CD3DTexture::DrawQuad(const CPoint points[4],
+ UTILS::COLOR::Color color,
+ CD3DTexture* texture,
+ const CRect* texCoords,
+ SHADER_METHOD options)
+{
+ unsigned numViews = 0;
+ ID3D11ShaderResourceView* views = nullptr;
+
+ if (texture)
+ {
+ numViews = 1;
+ views = texture->GetShaderResource();
+ }
+
+ DrawQuad(points, color, numViews, &views, texCoords, options);
+}
+
+void CD3DTexture::DrawQuad(const CRect& rect,
+ UTILS::COLOR::Color color,
+ CD3DTexture* texture,
+ const CRect* texCoords,
+ SHADER_METHOD options)
+{
+ CPoint points[] =
+ {
+ { rect.x1, rect.y1 },
+ { rect.x2, rect.y1 },
+ { rect.x2, rect.y2 },
+ { rect.x1, rect.y2 },
+ };
+ DrawQuad(points, color, texture, texCoords, options);
+}
+
+void CD3DTexture::DrawQuad(const CPoint points[4],
+ UTILS::COLOR::Color color,
+ unsigned numViews,
+ ID3D11ShaderResourceView** view,
+ const CRect* texCoords,
+ SHADER_METHOD options)
+{
+ XMFLOAT4 xcolor;
+ CD3DHelper::XMStoreColor(&xcolor, color);
+ CRect coords = texCoords ? *texCoords : CRect(0.0f, 0.0f, 1.0f, 1.0f);
+
+ Vertex verts[4] = {
+ { XMFLOAT3(points[0].x, points[0].y, 0), xcolor, XMFLOAT2(coords.x1, coords.y1), XMFLOAT2(0.0f, 0.0f) },
+ { XMFLOAT3(points[1].x, points[1].y, 0), xcolor, XMFLOAT2(coords.x2, coords.y1), XMFLOAT2(0.0f, 0.0f) },
+ { XMFLOAT3(points[2].x, points[2].y, 0), xcolor, XMFLOAT2(coords.x2, coords.y2), XMFLOAT2(0.0f, 0.0f) },
+ { XMFLOAT3(points[3].x, points[3].y, 0), xcolor, XMFLOAT2(coords.x1, coords.y2), XMFLOAT2(0.0f, 0.0f) },
+ };
+
+ CGUIShaderDX* pGUIShader = DX::Windowing()->GetGUIShader();
+
+ pGUIShader->Begin(view && numViews > 0 ? options : SHADER_METHOD_RENDER_DEFAULT);
+ if (view && numViews > 0)
+ pGUIShader->SetShaderViews(numViews, view);
+ pGUIShader->DrawQuad(verts[0], verts[1], verts[2], verts[3]);
+}
+
+void CD3DTexture::DrawQuad(const CRect& rect,
+ UTILS::COLOR::Color color,
+ unsigned numViews,
+ ID3D11ShaderResourceView** view,
+ const CRect* texCoords,
+ SHADER_METHOD options)
+{
+ CPoint points[] =
+ {
+ { rect.x1, rect.y1 },
+ { rect.x2, rect.y1 },
+ { rect.x2, rect.y2 },
+ { rect.x1, rect.y2 },
+ };
+ DrawQuad(points, color, numViews, view, texCoords, options);
+}
+
+CD3DEffect::CD3DEffect()
+{
+ m_effect = nullptr;
+ m_techniquie = nullptr;
+ m_currentPass = nullptr;
+}
+
+CD3DEffect::~CD3DEffect()
+{
+ Release();
+}
+
+bool CD3DEffect::Create(const std::string &effectString, DefinesMap* defines)
+{
+ Release();
+ m_effectString = effectString;
+ m_defines.clear();
+ if (defines != nullptr)
+ m_defines = *defines; //FIXME: is this a copy of all members?
+ if (CreateEffect())
+ {
+ Register();
+ return true;
+ }
+ return false;
+}
+
+void CD3DEffect::Release()
+{
+ Unregister();
+ OnDestroyDevice(false);
+}
+
+void CD3DEffect::OnDestroyDevice(bool fatal)
+{
+ m_effect = nullptr;
+ m_techniquie = nullptr;
+ m_currentPass = nullptr;
+}
+
+void CD3DEffect::OnCreateDevice()
+{
+ CreateEffect();
+}
+
+HRESULT CD3DEffect::Open(D3D_INCLUDE_TYPE IncludeType, LPCSTR pFileName, LPCVOID pParentData, LPCVOID* ppData, UINT* pBytes)
+{
+ XFILE::CFile includeFile;
+
+ std::string fileName("special://xbmc/system/shaders/");
+ fileName.append(pFileName);
+
+ if (!includeFile.Open(fileName))
+ {
+ CLog::LogF(LOGERROR, "Could not open 3DLUT file: {}", fileName);
+ return E_FAIL;
+ }
+
+ int64_t length = includeFile.GetLength();
+ void *pData = malloc(length);
+ if (includeFile.Read(pData, length) != length)
+ {
+ free(pData);
+ return E_FAIL;
+ }
+ *ppData = pData;
+ *pBytes = length;
+
+ return S_OK;
+}
+
+HRESULT CD3DEffect::Close(LPCVOID pData)
+{
+ free((void*)pData);
+ return S_OK;
+}
+
+bool CD3DEffect::SetFloatArray(LPCSTR handle, const float* val, unsigned int count)
+{
+ if (m_effect)
+ {
+ return S_OK == m_effect->GetVariableByName(handle)->SetRawValue(val, 0, sizeof(float) * count);
+ }
+ return false;
+}
+
+bool CD3DEffect::SetMatrix(LPCSTR handle, const float* mat)
+{
+ if (m_effect)
+ {
+ return S_OK == m_effect->GetVariableByName(handle)->AsMatrix()->SetMatrix(mat);
+ }
+ return false;
+}
+
+bool CD3DEffect::SetTechnique(LPCSTR handle)
+{
+ if (m_effect)
+ {
+ m_techniquie = m_effect->GetTechniqueByName(handle);
+ if (!m_techniquie->IsValid())
+ m_techniquie = nullptr;
+
+ return nullptr != m_techniquie;
+ }
+ return false;
+}
+
+bool CD3DEffect::SetTexture(LPCSTR handle, CD3DTexture &texture)
+{
+ if (m_effect)
+ {
+ ID3DX11EffectShaderResourceVariable* var = m_effect->GetVariableByName(handle)->AsShaderResource();
+ if (var->IsValid())
+ return SUCCEEDED(var->SetResource(texture.GetShaderResource()));
+ }
+ return false;
+}
+
+bool CD3DEffect::SetResources(LPCSTR handle, ID3D11ShaderResourceView** ppSRViews, size_t count)
+{
+ if (m_effect)
+ {
+ ID3DX11EffectShaderResourceVariable* var = m_effect->GetVariableByName(handle)->AsShaderResource();
+ if (var->IsValid())
+ return SUCCEEDED(var->SetResourceArray(ppSRViews, 0, count));
+ }
+ return false;
+}
+
+bool CD3DEffect::SetConstantBuffer(LPCSTR handle, ID3D11Buffer *buffer)
+{
+ if (m_effect)
+ {
+ ID3DX11EffectConstantBuffer* effectbuffer = m_effect->GetConstantBufferByName(handle);
+ if (effectbuffer->IsValid())
+ return (S_OK == effectbuffer->SetConstantBuffer(buffer));
+ }
+ return false;
+}
+
+bool CD3DEffect::SetScalar(LPCSTR handle, float value)
+{
+ if (m_effect)
+ {
+ ID3DX11EffectScalarVariable* scalar = m_effect->GetVariableByName(handle)->AsScalar();
+ if (scalar->IsValid())
+ return (S_OK == scalar->SetFloat(value));
+ }
+
+ return false;
+}
+
+bool CD3DEffect::Begin(UINT *passes, DWORD flags)
+{
+ if (m_effect && m_techniquie)
+ {
+ D3DX11_TECHNIQUE_DESC desc = {};
+ HRESULT hr = m_techniquie->GetDesc(&desc);
+ *passes = desc.Passes;
+ return S_OK == hr;
+ }
+ return false;
+}
+
+bool CD3DEffect::BeginPass(UINT pass)
+{
+ if (m_effect && m_techniquie)
+ {
+ m_currentPass = m_techniquie->GetPassByIndex(pass);
+ if (!m_currentPass || !m_currentPass->IsValid())
+ {
+ m_currentPass = nullptr;
+ return false;
+ }
+ return (S_OK == m_currentPass->Apply(0, DX::DeviceResources::Get()->GetD3DContext()));
+ }
+ return false;
+}
+
+bool CD3DEffect::EndPass()
+{
+ if (m_effect && m_currentPass)
+ {
+ m_currentPass = nullptr;
+ return true;
+ }
+ return false;
+}
+
+bool CD3DEffect::End()
+{
+ if (m_effect && m_techniquie)
+ {
+ m_techniquie = nullptr;
+ return true;
+ }
+ return false;
+}
+
+bool CD3DEffect::CreateEffect()
+{
+ HRESULT hr;
+ ID3DBlob* pError = nullptr;
+
+ std::vector<D3D_SHADER_MACRO> definemacros;
+
+ for (const auto& it : m_defines)
+ {
+ D3D_SHADER_MACRO m;
+ m.Name = it.first.c_str();
+ if (it.second.empty())
+ m.Definition = nullptr;
+ else
+ m.Definition = it.second.c_str();
+ definemacros.push_back( m );
+ }
+
+ definemacros.push_back(D3D_SHADER_MACRO());
+ definemacros.back().Name = nullptr;
+ definemacros.back().Definition = nullptr;
+
+ UINT dwShaderFlags = 0;
+
+#ifdef _DEBUG
+ //dwShaderFlags |= D3DCOMPILE_DEBUG;
+ // Disable optimizations to further improve shader debugging
+ //dwShaderFlags |= D3DCOMPILE_SKIP_OPTIMIZATION;
+#endif
+
+ hr = D3DX11CompileEffectFromMemory(m_effectString.c_str(), m_effectString.length(), "", &definemacros[0], this,
+ dwShaderFlags, 0, DX::DeviceResources::Get()->GetD3DDevice(), m_effect.ReleaseAndGetAddressOf(), &pError);
+
+ if(hr == S_OK)
+ return true;
+ else if(pError)
+ {
+ std::string error;
+ error.assign((const char*)pError->GetBufferPointer(), pError->GetBufferSize());
+ CLog::Log(LOGERROR, "CD3DEffect::CreateEffect(): {}", error);
+ }
+ else
+ CLog::Log(LOGERROR, "CD3DEffect::CreateEffect(): call to D3DXCreateEffect() failed with {}",
+ hr);
+ return false;
+}
+
+CD3DBuffer::CD3DBuffer()
+{
+ m_length = 0;
+ m_stride = 0;
+ m_usage = D3D11_USAGE_DEFAULT;
+ m_format = DXGI_FORMAT_UNKNOWN;
+ m_buffer = nullptr;
+ m_data = nullptr;
+}
+
+CD3DBuffer::~CD3DBuffer()
+{
+ Release();
+ delete[] m_data;
+}
+
+bool CD3DBuffer::Create(D3D11_BIND_FLAG type, UINT count, UINT stride, DXGI_FORMAT format, D3D11_USAGE usage, const void* data)
+{
+ m_type = type;
+ m_stride = stride ? stride : CD3DHelper::BitsPerPixel(format);
+ m_format = format;
+ m_length = count * m_stride;
+ m_usage = usage;
+ m_cpuFlags = 0;
+
+ if (m_usage == D3D11_USAGE_DYNAMIC || m_usage == D3D11_USAGE_STAGING)
+ {
+ m_cpuFlags |= D3D11_CPU_ACCESS_WRITE;
+ if (m_usage == D3D11_USAGE_STAGING)
+ m_cpuFlags |= D3D11_CPU_ACCESS_READ;
+ }
+
+ Release();
+
+ // create the vertex buffer
+ if (CreateBuffer(data))
+ {
+ Register();
+ return true;
+ }
+ return false;
+}
+
+void CD3DBuffer::Release()
+{
+ Unregister();
+ m_buffer = nullptr;
+}
+
+bool CD3DBuffer::Map(void **data)
+{
+ if (m_buffer)
+ {
+ D3D11_MAPPED_SUBRESOURCE resource;
+ if (SUCCEEDED(DX::DeviceResources::Get()->GetD3DContext()->Map(m_buffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &resource)))
+ {
+ *data = resource.pData;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CD3DBuffer::Unmap()
+{
+ if (m_buffer)
+ {
+ DX::DeviceResources::Get()->GetD3DContext()->Unmap(m_buffer.Get(), 0);
+ return true;
+ }
+ return false;
+}
+
+void CD3DBuffer::OnDestroyDevice(bool fatal)
+{
+ if (fatal)
+ {
+ m_buffer = nullptr;
+ return;
+ }
+
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetImmediateContext();
+
+ if (!pDevice || !pContext || !m_buffer)
+ return;
+
+ D3D11_BUFFER_DESC srcDesc;
+ m_buffer->GetDesc(&srcDesc);
+
+ ComPtr<ID3D11Buffer> buffer;
+ if (srcDesc.Usage != D3D11_USAGE_STAGING || 0 == (srcDesc.CPUAccessFlags & D3D11_CPU_ACCESS_READ))
+ {
+ CD3D11_BUFFER_DESC trgDesc(srcDesc);
+ trgDesc.Usage = D3D11_USAGE_STAGING;
+ trgDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ trgDesc.BindFlags = 0;
+
+ if (SUCCEEDED(pDevice->CreateBuffer(&trgDesc, NULL, &buffer)))
+ pContext->CopyResource(buffer.Get(), m_buffer.Get());
+ }
+ else
+ buffer = m_buffer;
+
+ if (buffer != nullptr)
+ {
+ D3D11_MAPPED_SUBRESOURCE res;
+ if (SUCCEEDED(pContext->Map(buffer.Get(), 0, D3D11_MAP_READ, 0, &res)))
+ {
+ m_data = new unsigned char[srcDesc.ByteWidth];
+ memcpy(m_data, res.pData, srcDesc.ByteWidth);
+ pContext->Unmap(buffer.Get(), 0);
+ }
+ }
+ m_buffer = nullptr;
+}
+
+void CD3DBuffer::OnCreateDevice()
+{
+ // yay, we're back - make a new copy of the vertices
+ if (!m_buffer && m_data)
+ {
+ CreateBuffer(m_data);
+ delete[] m_data;
+ m_data = nullptr;
+ }
+}
+
+bool CD3DBuffer::CreateBuffer(const void* pData)
+{
+ CD3D11_BUFFER_DESC bDesc(m_length, m_type, m_usage, m_cpuFlags);
+ D3D11_SUBRESOURCE_DATA initData;
+ initData.pSysMem = pData;
+ return (S_OK == DX::DeviceResources::Get()->GetD3DDevice()->CreateBuffer(&bDesc, (pData ? &initData : nullptr), m_buffer.ReleaseAndGetAddressOf()));
+}
+
+/****************************************************/
+/* D3D Vertex Shader Class */
+/****************************************************/
+CD3DVertexShader::CD3DVertexShader()
+{
+ m_VS = nullptr;
+ m_vertexLayout = nullptr;
+ m_VSBuffer = nullptr;
+ m_inputLayout = nullptr;
+ m_vertexLayoutSize = 0;
+ m_inited = false;
+}
+
+CD3DVertexShader::~CD3DVertexShader()
+{
+ Release();
+}
+
+void CD3DVertexShader::Release()
+{
+ Unregister();
+ ReleaseShader();
+ m_VSBuffer = nullptr;
+ if (m_vertexLayout)
+ {
+ delete[] m_vertexLayout;
+ m_vertexLayout = nullptr;
+ }
+}
+
+void CD3DVertexShader::ReleaseShader()
+{
+ UnbindShader();
+
+ m_VS = nullptr;
+ m_inputLayout = nullptr;
+ m_inited = false;
+}
+
+bool CD3DVertexShader::Create(const std::wstring& vertexFile, D3D11_INPUT_ELEMENT_DESC* vertexLayout, unsigned int vertexLayoutSize)
+{
+ ReleaseShader();
+
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+
+ if (!pDevice)
+ return false;
+
+ if (FAILED(D3DReadFileToBlob(vertexFile.c_str(), m_VSBuffer.ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGERROR, "failed to load the vertex shader.");
+ return false;
+ }
+
+ if (vertexLayout && vertexLayoutSize)
+ {
+ m_vertexLayoutSize = vertexLayoutSize;
+ m_vertexLayout = new D3D11_INPUT_ELEMENT_DESC[vertexLayoutSize];
+ for (unsigned int i = 0; i < vertexLayoutSize; ++i)
+ m_vertexLayout[i] = vertexLayout[i];
+ }
+ else
+ return false;
+
+ m_inited = CreateInternal();
+
+ if (m_inited)
+ Register();
+
+ return m_inited;
+}
+
+bool CD3DVertexShader::Create(const void* code, size_t codeLength, D3D11_INPUT_ELEMENT_DESC* vertexLayout, unsigned int vertexLayoutSize)
+{
+ ReleaseShader();
+
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+
+ if (!pDevice)
+ return false;
+
+ // trick to load bytecode into ID3DBlob
+ if (FAILED(D3DStripShader(code, codeLength, D3DCOMPILER_STRIP_REFLECTION_DATA, m_VSBuffer.ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGERROR, "failed to load the vertex shader.");
+ return false;
+ }
+
+ if (vertexLayout && vertexLayoutSize)
+ {
+ m_vertexLayoutSize = vertexLayoutSize;
+ m_vertexLayout = new D3D11_INPUT_ELEMENT_DESC[vertexLayoutSize];
+ for (unsigned int i = 0; i < vertexLayoutSize; ++i)
+ m_vertexLayout[i] = vertexLayout[i];
+ }
+ else
+ return false;
+
+ m_inited = CreateInternal();
+
+ if (m_inited)
+ Register();
+
+ return m_inited;
+}
+
+bool CD3DVertexShader::CreateInternal()
+{
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+
+ CLog::LogF(LOGDEBUG, "creating vertex shader.");
+
+ // Create the vertex shader
+ if (FAILED(pDevice->CreateVertexShader(m_VSBuffer->GetBufferPointer(), m_VSBuffer->GetBufferSize(), nullptr, m_VS.ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGERROR, "failed to Create the vertex shader.");
+ m_VSBuffer = nullptr;
+ return false;
+ }
+
+ CLog::LogF(LOGDEBUG, "creating input layout.");
+
+ if (FAILED(pDevice->CreateInputLayout(m_vertexLayout, m_vertexLayoutSize, m_VSBuffer->GetBufferPointer(), m_VSBuffer->GetBufferSize(), m_inputLayout.ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGERROR, "failed to create the input layout.");
+ return false;
+ }
+
+ return true;
+}
+
+void CD3DVertexShader::BindShader()
+{
+ if (!m_inited)
+ return;
+
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext();
+ if (!pContext)
+ return;
+
+ pContext->IASetInputLayout(m_inputLayout.Get());
+ pContext->VSSetShader(m_VS.Get(), nullptr, 0);
+}
+
+void CD3DVertexShader::UnbindShader()
+{
+ if (!m_inited)
+ return;
+
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext();
+ pContext->IASetInputLayout(nullptr);
+ pContext->VSSetShader(nullptr, nullptr, 0);
+}
+
+void CD3DVertexShader::OnCreateDevice()
+{
+ if (m_VSBuffer && !m_VS)
+ m_inited = CreateInternal();
+}
+
+void CD3DVertexShader::OnDestroyDevice(bool fatal)
+{
+ ReleaseShader();
+}
+
+/****************************************************/
+/* D3D Pixel Shader Class */
+/****************************************************/
+CD3DPixelShader::CD3DPixelShader()
+{
+ m_PS = nullptr;
+ m_PSBuffer = nullptr;
+ m_inited = false;
+}
+
+CD3DPixelShader::~CD3DPixelShader()
+{
+ Release();
+}
+
+void CD3DPixelShader::Release()
+{
+ Unregister();
+ ReleaseShader();
+ m_PSBuffer = nullptr;
+}
+
+void CD3DPixelShader::ReleaseShader()
+{
+ m_inited = false;
+ m_PS = nullptr;
+}
+
+bool CD3DPixelShader::Create(const std::wstring& wstrFile)
+{
+ ReleaseShader();
+
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+
+ if (!pDevice)
+ return false;
+
+ if (FAILED(D3DReadFileToBlob(wstrFile.c_str(), m_PSBuffer.ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGERROR, "Failed to load the vertex shader.");
+ return false;
+ }
+
+ m_inited = CreateInternal();
+
+ if (m_inited)
+ Register();
+
+ return m_inited;
+}
+
+bool CD3DPixelShader::Create(const void* code, size_t codeLength)
+{
+ ReleaseShader();
+
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+
+ if (!pDevice)
+ return false;
+
+ // trick to load bytecode into ID3DBlob
+ if (FAILED(D3DStripShader(code, codeLength, D3DCOMPILER_STRIP_REFLECTION_DATA, m_PSBuffer.ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGERROR, "Failed to load the vertex shader.");
+ return false;
+ }
+
+ m_inited = CreateInternal();
+
+ if (m_inited)
+ Register();
+
+ return m_inited;
+}
+
+bool CD3DPixelShader::CreateInternal()
+{
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+
+ CLog::LogF(LOGDEBUG, "Create the pixel shader.");
+
+ // Create the vertex shader
+ if (FAILED(pDevice->CreatePixelShader(m_PSBuffer->GetBufferPointer(), m_PSBuffer->GetBufferSize(), nullptr, m_PS.ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGERROR, "Failed to Create the pixel shader.");
+ m_PSBuffer = nullptr;
+ return false;
+ }
+
+ return true;
+}
+
+void CD3DPixelShader::BindShader()
+{
+ if (!m_inited)
+ return;
+
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext();
+ if (!pContext)
+ return;
+
+ pContext->PSSetShader(m_PS.Get(), nullptr, 0);
+}
+
+void CD3DPixelShader::UnbindShader()
+{
+ if (!m_inited)
+ return;
+
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext();
+ pContext->IASetInputLayout(nullptr);
+ pContext->VSSetShader(nullptr, nullptr, 0);
+}
+
+void CD3DPixelShader::OnCreateDevice()
+{
+ if (m_PSBuffer && !m_PS)
+ m_inited = CreateInternal();
+}
+
+void CD3DPixelShader::OnDestroyDevice(bool fatal)
+{
+ ReleaseShader();
+}
diff --git a/xbmc/guilib/D3DResource.h b/xbmc/guilib/D3DResource.h
new file mode 100644
index 0000000..e7f6517
--- /dev/null
+++ b/xbmc/guilib/D3DResource.h
@@ -0,0 +1,293 @@
+/*
+ * 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 "GUIColorManager.h"
+#include "utils/ColorUtils.h"
+#include "utils/Geometry.h"
+
+#include <map>
+
+#include <DirectXMath.h>
+#include <d3dx11effect.h>
+#include <wrl/client.h>
+
+#define KODI_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT 4
+
+typedef enum SHADER_METHOD {
+ SHADER_METHOD_RENDER_DEFAULT,
+ SHADER_METHOD_RENDER_TEXTURE_NOBLEND,
+ SHADER_METHOD_RENDER_FONT,
+ SHADER_METHOD_RENDER_TEXTURE_BLEND,
+ SHADER_METHOD_RENDER_MULTI_TEXTURE_BLEND,
+ SHADER_METHOD_RENDER_STEREO_INTERLACED_LEFT,
+ SHADER_METHOD_RENDER_STEREO_INTERLACED_RIGHT,
+ SHADER_METHOD_RENDER_STEREO_CHECKERBOARD_LEFT,
+ SHADER_METHOD_RENDER_STEREO_CHECKERBOARD_RIGHT,
+ SHADER_METHOD_RENDER_COUNT
+} _SHADER_METHOD;
+
+class ID3DResource
+{
+public:
+ virtual ~ID3DResource() {}
+
+ virtual void OnDestroyDevice(bool fatal)=0;
+ virtual void OnCreateDevice()=0;
+
+protected:
+ void Register();
+ void Unregister();
+
+ bool m_bRegistered = false;
+};
+
+class CD3DHelper
+{
+public:
+ static inline void XMStoreColor(float* floats, DWORD dword)
+ {
+ floats[0] = float((dword >> 16) & 0xFF) * (1.0f / 255.0f); // r
+ floats[1] = float((dword >> 8) & 0xFF) * (1.0f / 255.0f); // g
+ floats[2] = float((dword >> 0) & 0xFF) * (1.0f / 255.0f); // b
+ floats[3] = float((dword >> 24) & 0xFF) * (1.0f / 255.0f); // a
+ }
+
+ static inline void XMStoreColor(DirectX::XMFLOAT4* floats, DWORD dword)
+ {
+ XMStoreColor(reinterpret_cast<float*>(floats), dword);
+ }
+
+ static inline void XMStoreColor(float* floats, unsigned char a, unsigned char r, unsigned char g, unsigned char b)
+ {
+ floats[0] = r * (1.0f / 255.0f);
+ floats[1] = g * (1.0f / 255.0f);
+ floats[2] = b * (1.0f / 255.0f);
+ floats[3] = a * (1.0f / 255.0f);
+ }
+
+ static inline void XMStoreColor(DirectX::XMFLOAT4* floats, unsigned char a, unsigned char r, unsigned char g, unsigned char b)
+ {
+ XMStoreColor(reinterpret_cast<float*>(floats), a, r, g, b);
+ }
+
+ // helper function to properly "clear" shader resources
+ static inline void PSClearShaderResources(ID3D11DeviceContext* pContext)
+ {
+ ID3D11ShaderResourceView* shader_resource_views[KODI_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT] = {};
+ pContext->PSSetShaderResources(0, ARRAYSIZE(shader_resource_views), shader_resource_views);
+ }
+
+ static size_t BitsPerPixel(DXGI_FORMAT fmt);
+};
+
+class CD3DTexture : public ID3DResource
+{
+public:
+ CD3DTexture();
+ virtual ~CD3DTexture();
+
+ bool Create(UINT width, UINT height, UINT mipLevels, D3D11_USAGE usage, DXGI_FORMAT format, const void* pInitData = nullptr, unsigned int srcPitch = 0);
+
+ void Release();
+ bool GetDesc(D3D11_TEXTURE2D_DESC *desc) const;
+ bool LockRect(UINT subresource, D3D11_MAPPED_SUBRESOURCE *res, D3D11_MAP mapType) const;
+ bool UnlockRect(UINT subresource) const;
+
+ // Accessors
+ ID3D11Texture2D* Get() const { return m_texture.Get(); }
+ ID3D11ShaderResourceView* GetShaderResource(DXGI_FORMAT format = DXGI_FORMAT_UNKNOWN);
+ ID3D11ShaderResourceView** GetAddressOfSRV(DXGI_FORMAT format = DXGI_FORMAT_UNKNOWN);
+ ID3D11RenderTargetView* GetRenderTarget();
+ ID3D11RenderTargetView** GetAddressOfRTV();
+ UINT GetWidth() const { return m_width; }
+ UINT GetHeight() const { return m_height; }
+ DXGI_FORMAT GetFormat() const { return m_format; }
+ void GenerateMipmaps();
+
+ // static methods
+ static void DrawQuad(const CPoint points[4],
+ UTILS::COLOR::Color color,
+ CD3DTexture* texture,
+ const CRect* texCoords,
+ SHADER_METHOD options = SHADER_METHOD_RENDER_TEXTURE_BLEND);
+
+ static void DrawQuad(const CPoint points[4],
+ UTILS::COLOR::Color color,
+ unsigned numViews,
+ ID3D11ShaderResourceView** view,
+ const CRect* texCoords,
+ SHADER_METHOD options = SHADER_METHOD_RENDER_TEXTURE_BLEND);
+
+ static void DrawQuad(const CRect& coords,
+ UTILS::COLOR::Color color,
+ CD3DTexture* texture,
+ const CRect* texCoords,
+ SHADER_METHOD options = SHADER_METHOD_RENDER_TEXTURE_BLEND);
+
+ static void DrawQuad(const CRect& coords,
+ UTILS::COLOR::Color color,
+ unsigned numViews,
+ ID3D11ShaderResourceView** view,
+ const CRect* texCoords,
+ SHADER_METHOD options = SHADER_METHOD_RENDER_TEXTURE_BLEND);
+
+ void OnDestroyDevice(bool fatal) override;
+ void OnCreateDevice() override;
+
+protected:
+ ID3D11RenderTargetView* GetRenderTargetInternal(unsigned idx = 0);
+ unsigned int GetMemoryUsage(unsigned int pitch) const;
+ bool CreateInternal(const void* pInitData = nullptr, unsigned int srcPitch = 0);
+
+ void SaveTexture();
+ void RestoreTexture();
+
+ // saved data
+ BYTE* m_data;
+ // creation parameters
+ UINT m_width;
+ UINT m_height;
+ UINT m_mipLevels;
+ UINT m_pitch;
+ UINT m_bindFlags;
+ UINT m_cpuFlags;
+ UINT m_viewIdx;
+ D3D11_USAGE m_usage;
+ DXGI_FORMAT m_format;
+
+ // created texture
+ Microsoft::WRL::ComPtr<ID3D11Texture2D> m_texture;
+ Microsoft::WRL::ComPtr<ID3D11RenderTargetView> m_renderTargets[2];
+ // store views in different formats
+ std::map<DXGI_FORMAT, Microsoft::WRL::ComPtr<ID3D11ShaderResourceView>> m_views;
+};
+
+typedef std::map<std::string, std::string> DefinesMap;
+
+class CD3DEffect : public ID3DResource, public ID3DInclude
+{
+public:
+ CD3DEffect();
+ virtual ~CD3DEffect();
+ bool Create(const std::string &effectString, DefinesMap* defines);
+ void Release();
+ bool SetFloatArray(LPCSTR handle, const float* val, unsigned int count);
+ bool SetMatrix(LPCSTR handle, const float* mat);
+ bool SetTechnique(LPCSTR handle);
+ bool SetTexture(LPCSTR handle, CD3DTexture &texture);
+ bool SetResources(LPCSTR handle, ID3D11ShaderResourceView** ppSRViews, size_t count);
+ bool SetConstantBuffer(LPCSTR handle, ID3D11Buffer *buffer);
+ bool SetScalar(LPCSTR handle, float value);
+ bool Begin(UINT *passes, DWORD flags);
+ bool BeginPass(UINT pass);
+ bool EndPass();
+ bool End();
+
+ ID3DX11Effect* Get() const { return m_effect.Get(); }
+
+ void OnDestroyDevice(bool fatal) override;
+ void OnCreateDevice() override;
+
+ // ID3DInclude interface
+ __declspec(nothrow) HRESULT __stdcall Open(D3D_INCLUDE_TYPE IncludeType, LPCSTR pFileName, LPCVOID pParentData, LPCVOID *ppData, UINT *pBytes) override;
+ __declspec(nothrow) HRESULT __stdcall Close(LPCVOID pData) override;
+
+private:
+ bool CreateEffect();
+
+ std::string m_effectString;
+ DefinesMap m_defines;
+ Microsoft::WRL::ComPtr<ID3DX11Effect> m_effect;
+ Microsoft::WRL::ComPtr<ID3DX11EffectTechnique> m_techniquie;
+ Microsoft::WRL::ComPtr<ID3DX11EffectPass> m_currentPass;
+};
+
+class CD3DBuffer : public ID3DResource
+{
+public:
+ CD3DBuffer();
+ virtual ~CD3DBuffer();
+ bool Create(D3D11_BIND_FLAG type, UINT count, UINT stride, DXGI_FORMAT format, D3D11_USAGE usage, const void* initData = nullptr);
+ bool Map(void** resource);
+ bool Unmap();
+ void Release();
+ unsigned int GetStride() { return m_stride; }
+ DXGI_FORMAT GetFormat() { return m_format; }
+ ID3D11Buffer* Get() const { return m_buffer.Get(); }
+
+ void OnDestroyDevice(bool fatal) override;
+ void OnCreateDevice() override;
+
+private:
+ bool CreateBuffer(const void *pData);
+
+ // saved data
+ BYTE* m_data;
+ UINT m_length;
+ UINT m_stride;
+ UINT m_cpuFlags;
+ DXGI_FORMAT m_format;
+ D3D11_USAGE m_usage;
+ D3D11_BIND_FLAG m_type;
+ Microsoft::WRL::ComPtr<ID3D11Buffer> m_buffer;
+};
+
+class CD3DVertexShader : public ID3DResource
+{
+public:
+ CD3DVertexShader();
+ ~CD3DVertexShader();
+
+ bool Create(const std::wstring& vertexFile, D3D11_INPUT_ELEMENT_DESC* vertexLayout, unsigned int vertexLayoutSize);
+ bool Create(const void* code, size_t codeLength, D3D11_INPUT_ELEMENT_DESC* vertexLayout, unsigned int vertexLayoutSize);
+ void ReleaseShader();
+ void BindShader();
+ void UnbindShader();
+ void Release();
+ bool IsInited() { return m_inited; }
+
+ void OnDestroyDevice(bool fatal) override;
+ void OnCreateDevice() override;
+
+private:
+ bool CreateInternal();
+
+ bool m_inited;
+ unsigned int m_vertexLayoutSize;
+ D3D11_INPUT_ELEMENT_DESC* m_vertexLayout;
+ Microsoft::WRL::ComPtr<ID3DBlob> m_VSBuffer;
+ Microsoft::WRL::ComPtr<ID3D11VertexShader> m_VS;
+ Microsoft::WRL::ComPtr<ID3D11InputLayout> m_inputLayout;
+};
+
+class CD3DPixelShader : public ID3DResource
+{
+public:
+ CD3DPixelShader();
+ ~CD3DPixelShader();
+
+ bool Create(const std::wstring& wstrFile);
+ bool Create(const void* code, size_t codeLength);
+ void ReleaseShader();
+ void BindShader();
+ void UnbindShader();
+ void Release();
+ bool IsInited() { return m_inited; }
+
+ void OnDestroyDevice(bool fatal) override;
+ void OnCreateDevice() override;
+
+private:
+ bool CreateInternal();
+
+ bool m_inited;
+ Microsoft::WRL::ComPtr<ID3DBlob> m_PSBuffer;
+ Microsoft::WRL::ComPtr<ID3D11PixelShader> m_PS;
+};
diff --git a/xbmc/guilib/DDSImage.cpp b/xbmc/guilib/DDSImage.cpp
new file mode 100644
index 0000000..124352d
--- /dev/null
+++ b/xbmc/guilib/DDSImage.cpp
@@ -0,0 +1,148 @@
+/*
+ * 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 "DDSImage.h"
+
+#include "XBTF.h"
+#include "filesystem/File.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <string.h>
+using namespace XFILE;
+
+CDDSImage::CDDSImage()
+{
+ m_data = NULL;
+ memset(&m_desc, 0, sizeof(m_desc));
+}
+
+CDDSImage::CDDSImage(unsigned int width, unsigned int height, unsigned int format)
+{
+ m_data = NULL;
+ Allocate(width, height, format);
+}
+
+CDDSImage::~CDDSImage()
+{
+ delete[] m_data;
+}
+
+unsigned int CDDSImage::GetWidth() const
+{
+ return m_desc.width;
+}
+
+unsigned int CDDSImage::GetHeight() const
+{
+ return m_desc.height;
+}
+
+unsigned int CDDSImage::GetFormat() const
+{
+ if (m_desc.pixelFormat.flags & DDPF_RGB)
+ return 0; // Not supported
+ if (m_desc.pixelFormat.flags & DDPF_FOURCC)
+ {
+ if (strncmp((const char *)&m_desc.pixelFormat.fourcc, "DXT1", 4) == 0)
+ return XB_FMT_DXT1;
+ if (strncmp((const char *)&m_desc.pixelFormat.fourcc, "DXT3", 4) == 0)
+ return XB_FMT_DXT3;
+ if (strncmp((const char *)&m_desc.pixelFormat.fourcc, "DXT5", 4) == 0)
+ return XB_FMT_DXT5;
+ if (strncmp((const char *)&m_desc.pixelFormat.fourcc, "ARGB", 4) == 0)
+ return XB_FMT_A8R8G8B8;
+ }
+ return 0;
+}
+
+unsigned int CDDSImage::GetSize() const
+{
+ return m_desc.linearSize;
+}
+
+unsigned char *CDDSImage::GetData() const
+{
+ return m_data;
+}
+
+bool CDDSImage::ReadFile(const std::string &inputFile)
+{
+ // open the file
+ CFile file;
+ if (!file.Open(inputFile))
+ return false;
+
+ // read the header
+ uint32_t magic;
+ if (file.Read(&magic, 4) != 4)
+ return false;
+ if (file.Read(&m_desc, sizeof(m_desc)) != sizeof(m_desc))
+ return false;
+ if (!GetFormat())
+ return false; // not supported
+
+ // allocate our data
+ m_data = new unsigned char[m_desc.linearSize];
+ if (!m_data)
+ return false;
+
+ // and read it in
+ if (file.Read(m_data, m_desc.linearSize) != static_cast<ssize_t>(m_desc.linearSize))
+ return false;
+
+ file.Close();
+ return true;
+}
+
+unsigned int CDDSImage::GetStorageRequirements(unsigned int width, unsigned int height, unsigned int format)
+{
+ switch (format)
+ {
+ case XB_FMT_DXT1:
+ return ((width + 3) / 4) * ((height + 3) / 4) * 8;
+ case XB_FMT_DXT3:
+ case XB_FMT_DXT5:
+ return ((width + 3) / 4) * ((height + 3) / 4) * 16;
+ case XB_FMT_A8R8G8B8:
+ default:
+ return width * height * 4;
+ }
+}
+
+void CDDSImage::Allocate(unsigned int width, unsigned int height, unsigned int format)
+{
+ memset(&m_desc, 0, sizeof(m_desc));
+ m_desc.size = sizeof(m_desc);
+ m_desc.flags = ddsd_caps | ddsd_pixelformat | ddsd_width | ddsd_height | ddsd_linearsize;
+ m_desc.height = height;
+ m_desc.width = width;
+ m_desc.linearSize = GetStorageRequirements(width, height, format);
+ m_desc.pixelFormat.size = sizeof(m_desc.pixelFormat);
+ m_desc.pixelFormat.flags = ddpf_fourcc;
+ memcpy(&m_desc.pixelFormat.fourcc, GetFourCC(format), 4);
+ m_desc.caps.flags1 = ddscaps_texture;
+ delete[] m_data;
+ m_data = new unsigned char[m_desc.linearSize];
+}
+
+const char *CDDSImage::GetFourCC(unsigned int format)
+{
+ switch (format)
+ {
+ case XB_FMT_DXT1:
+ return "DXT1";
+ case XB_FMT_DXT3:
+ return "DXT3";
+ case XB_FMT_DXT5:
+ return "DXT5";
+ case XB_FMT_A8R8G8B8:
+ default:
+ return "ARGB";
+ }
+}
diff --git a/xbmc/guilib/DDSImage.h b/xbmc/guilib/DDSImage.h
new file mode 100644
index 0000000..8c8bca2
--- /dev/null
+++ b/xbmc/guilib/DDSImage.h
@@ -0,0 +1,102 @@
+/*
+ * 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 <stdint.h>
+#include <string>
+
+class CDDSImage
+{
+public:
+ CDDSImage();
+ CDDSImage(unsigned int width, unsigned int height, unsigned int format);
+ ~CDDSImage();
+
+ unsigned int GetWidth() const;
+ unsigned int GetHeight() const;
+ unsigned int GetFormat() const;
+ unsigned int GetSize() const;
+ unsigned char *GetData() const;
+
+ bool ReadFile(const std::string &file);
+
+private:
+ void Allocate(unsigned int width, unsigned int height, unsigned int format);
+ static const char *GetFourCC(unsigned int format);
+
+ static unsigned int GetStorageRequirements(unsigned int width, unsigned int height, unsigned int format);
+ enum {
+ ddsd_caps = 0x00000001,
+ ddsd_height = 0x00000002,
+ ddsd_width = 0x00000004,
+ ddsd_pitch = 0x00000008,
+ ddsd_pixelformat = 0x00001000,
+ ddsd_mipmapcount = 0x00020000,
+ ddsd_linearsize = 0x00080000,
+ ddsd_depth = 0x00800000
+ };
+
+ enum {
+ ddpf_alphapixels = 0x00000001,
+ ddpf_fourcc = 0x00000004,
+ ddpf_rgb = 0x00000040
+ };
+
+ enum {
+ ddscaps_complex = 0x00000008,
+ ddscaps_texture = 0x00001000,
+ ddscaps_mipmap = 0x00400000
+ };
+
+ #pragma pack(push, 2)
+ typedef struct
+ {
+ uint32_t size;
+ uint32_t flags;
+ uint32_t fourcc;
+ uint32_t rgbBitCount;
+ uint32_t rBitMask;
+ uint32_t gBitMask;
+ uint32_t bBitMask;
+ uint32_t aBitMask;
+ } ddpixelformat;
+
+#define DDPF_ALPHAPIXELS 0x00000001
+#define DDPF_ALPHA 0x00000002
+#define DDPF_FOURCC 0x00000004
+#define DDPF_RGB 0x00000040
+#define DDPF_YUV 0x00000200
+#define DDPF_LUMINANCE 0x00020000
+
+ typedef struct
+ {
+ uint32_t flags1;
+ uint32_t flags2;
+ uint32_t reserved[2];
+ } ddcaps2;
+
+ typedef struct
+ {
+ uint32_t size;
+ uint32_t flags;
+ uint32_t height;
+ uint32_t width;
+ uint32_t linearSize;
+ uint32_t depth;
+ uint32_t mipmapcount;
+ uint32_t reserved[11];
+ ddpixelformat pixelFormat;
+ ddcaps2 caps;
+ uint32_t reserved2;
+ } ddsurfacedesc2;
+ #pragma pack(pop)
+
+ ddsurfacedesc2 m_desc;
+ unsigned char *m_data;
+};
diff --git a/xbmc/guilib/DirectXGraphics.cpp b/xbmc/guilib/DirectXGraphics.cpp
new file mode 100644
index 0000000..5bab895
--- /dev/null
+++ b/xbmc/guilib/DirectXGraphics.cpp
@@ -0,0 +1,374 @@
+/*
+ * 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 "DirectXGraphics.h"
+
+#include "Texture.h"
+#include "XBTF.h"
+
+LPVOID XPhysicalAlloc(SIZE_T s, DWORD ulPhysicalAddress, DWORD ulAlignment, DWORD flProtect)
+{
+ return malloc(s);
+}
+
+void XPhysicalFree(LPVOID lpAddress)
+{
+ free(lpAddress);
+}
+
+DWORD GetD3DFormat(XB_D3DFORMAT format)
+{
+#ifndef MAKEFOURCC
+#define MAKEFOURCC(ch0, ch1, ch2, ch3) \
+ ((DWORD)(BYTE)(ch0) | ((DWORD)(BYTE)(ch1) << 8) | \
+ ((DWORD)(BYTE)(ch2) << 16) | ((DWORD)(BYTE)(ch3) << 24 ))
+#endif
+ switch (format)
+ {
+ case XB_D3DFMT_A8R8G8B8:
+ case XB_D3DFMT_LIN_A8R8G8B8:
+ case XB_D3DFMT_P8:
+ return 21;
+ case XB_D3DFMT_DXT1:
+ return MAKEFOURCC('D', 'X', 'T', '1');
+ case XB_D3DFMT_DXT2:
+ return MAKEFOURCC('D', 'X', 'T', '2');
+ case XB_D3DFMT_DXT4:
+ return MAKEFOURCC('D', 'X', 'T', '4');
+ default:
+ return 0;
+ }
+}
+
+DWORD BytesPerPixelFromFormat(XB_D3DFORMAT format)
+{
+ switch (format)
+ {
+ case XB_D3DFMT_A8R8G8B8:
+ case XB_D3DFMT_LIN_A8R8G8B8:
+ case XB_D3DFMT_DXT4:
+ return 4;
+ case XB_D3DFMT_P8:
+ case XB_D3DFMT_DXT1:
+ case XB_D3DFMT_DXT2:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+bool IsPalettedFormat(XB_D3DFORMAT format)
+{
+ if (format == XB_D3DFMT_P8)
+ return true;
+ return false;
+}
+
+void ParseTextureHeader(D3DTexture *tex, XB_D3DFORMAT &fmt, DWORD &width, DWORD &height, DWORD &pitch, DWORD &offset)
+{
+ fmt = (XB_D3DFORMAT)((tex->Format & 0xff00) >> 8);
+ offset = tex->Data;
+ if (tex->Size)
+ {
+ width = (tex->Size & 0x00000fff) + 1;
+ height = ((tex->Size & 0x00fff000) >> 12) + 1;
+ pitch = (((tex->Size & 0xff000000) >> 24) + 1) << 6;
+ }
+ else
+ {
+ width = 1 << ((tex->Format & 0x00f00000) >> 20);
+ height = 1 << ((tex->Format & 0x0f000000) >> 24);
+ pitch = width * BytesPerPixelFromFormat(fmt);
+ }
+}
+
+bool IsSwizzledFormat(XB_D3DFORMAT format)
+{
+ switch (format)
+ {
+ case XB_D3DFMT_A8R8G8B8:
+ case XB_D3DFMT_P8:
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Unswizzle.
+// Format is:
+
+// 00 01 04 05
+// 02 03 06 07
+// 08 09 12 13
+// 10 11 14 15 ...
+
+// Currently only works for 32bit and 8bit textures, with power of 2 width and height
+void Unswizzle(const void *src, unsigned int depth, unsigned int width, unsigned int height, void *dest)
+{
+ if (height == 0 || width == 0)
+ return;
+
+ for (UINT y = 0; y < height; y++)
+ {
+ UINT sy = 0;
+ if (y < width)
+ {
+ for (int bit = 0; bit < 16; bit++)
+ sy |= ((y >> bit) & 1) << (2*bit);
+ sy <<= 1; // y counts twice
+ }
+ else
+ {
+ UINT y_mask = y % width;
+ for (int bit = 0; bit < 16; bit++)
+ sy |= ((y_mask >> bit) & 1) << (2*bit);
+ sy <<= 1; // y counts twice
+ sy += (y / width) * width * width;
+ }
+ BYTE *d = (BYTE *)dest + y * width * depth;
+ for (UINT x = 0; x < width; x++)
+ {
+ UINT sx = 0;
+ if (x < height * 2)
+ {
+ for (int bit = 0; bit < 16; bit++)
+ sx |= ((x >> bit) & 1) << (2*bit);
+ }
+ else
+ {
+ int x_mask = x % (2*height);
+ for (int bit = 0; bit < 16; bit++)
+ sx |= ((x_mask >> bit) & 1) << (2*bit);
+ sx += (x / (2 * height)) * 2 * height * height;
+ }
+ BYTE *s = (BYTE *)src + (sx + sy)*depth;
+ for (unsigned int i = 0; i < depth; ++i)
+ *d++ = *s++;
+ }
+ }
+}
+
+void DXT1toARGB(const void *src, void *dest, unsigned int destWidth)
+{
+ const BYTE *b = (const BYTE *)src;
+ // colour is in R5G6B5 format, convert to R8G8B8
+ DWORD colour[4];
+ BYTE red[4];
+ BYTE green[4];
+ BYTE blue[4];
+ for (int i = 0; i < 2; i++)
+ {
+ red[i] = b[2*i+1] & 0xf8;
+ green[i] = ((b[2*i+1] & 0x7) << 5) | ((b[2*i] & 0xe0) >> 3);
+ blue[i] = (b[2*i] & 0x1f) << 3;
+ colour[i] = (red[i] << 16) | (green[i] << 8) | blue[i];
+ }
+ if (colour[0] > colour[1])
+ {
+ red[2] = (2 * red[0] + red[1] + 1) / 3;
+ green[2] = (2 * green[0] + green[1] + 1) / 3;
+ blue[2] = (2 * blue[0] + blue[1] + 1) / 3;
+ red[3] = (red[0] + 2 * red[1] + 1) / 3;
+ green[3] = (green[0] + 2 * green[1] + 1) / 3;
+ blue[3] = (blue[0] + 2 * blue[1] + 1) / 3;
+ for (int i = 0; i < 4; i++)
+ colour[i] = (red[i] << 16) | (green[i] << 8) | blue[i] | 0xFF000000;
+ }
+ else
+ {
+ red[2] = (red[0] + red[1]) / 2;
+ green[2] = (green[0] + green[1]) / 2;
+ blue[2] = (blue[0] + blue[1]) / 2;
+ for (int i = 0; i < 3; i++)
+ colour[i] = (red[i] << 16) | (green[i] << 8) | blue[i] | 0xFF000000;
+ colour[3] = 0; // transparent
+ }
+ // ok, now grab the bits
+ for (int y = 0; y < 4; y++)
+ {
+ DWORD *d = (DWORD *)dest + destWidth * y;
+ *d++ = colour[(b[4 + y] & 0x03)];
+ *d++ = colour[(b[4 + y] & 0x0c) >> 2];
+ *d++ = colour[(b[4 + y] & 0x30) >> 4];
+ *d++ = colour[(b[4 + y] & 0xc0) >> 6];
+ }
+}
+
+void DXT4toARGB(const void *src, void *dest, unsigned int destWidth)
+{
+ const BYTE *b = (const BYTE *)src;
+ BYTE alpha[8];
+ alpha[0] = b[0];
+ alpha[1] = b[1];
+ if (alpha[0] > alpha[1])
+ {
+ alpha[2] = (6 * alpha[0] + 1 * alpha[1]+ 3) / 7;
+ alpha[3] = (5 * alpha[0] + 2 * alpha[1] + 3) / 7; // bit code 011
+ alpha[4] = (4 * alpha[0] + 3 * alpha[1] + 3) / 7; // bit code 100
+ alpha[5] = (3 * alpha[0] + 4 * alpha[1] + 3) / 7; // bit code 101
+ alpha[6] = (2 * alpha[0] + 5 * alpha[1] + 3) / 7; // bit code 110
+ alpha[7] = (1 * alpha[0] + 6 * alpha[1] + 3) / 7; // bit code 111
+ }
+ else
+ {
+ alpha[2] = (4 * alpha[0] + 1 * alpha[1] + 2) / 5; // Bit code 010
+ alpha[3] = (3 * alpha[0] + 2 * alpha[1] + 2) / 5; // Bit code 011
+ alpha[4] = (2 * alpha[0] + 3 * alpha[1] + 2) / 5; // Bit code 100
+ alpha[5] = (1 * alpha[0] + 4 * alpha[1] + 2) / 5; // Bit code 101
+ alpha[6] = 0; // Bit code 110
+ alpha[7] = 255; // Bit code 111
+ }
+ // ok, now grab the bits
+ BYTE a[4][4];
+ a[0][0] = alpha[(b[2] & 0xe0) >> 5];
+ a[0][1] = alpha[(b[2] & 0x1c) >> 2];
+ a[0][2] = alpha[((b[2] & 0x03) << 1) | ((b[3] & 0x80) >> 7)];
+ a[0][3] = alpha[(b[3] & 0x70) >> 4];
+ a[1][0] = alpha[(b[3] & 0x0e) >> 1];
+ a[1][1] = alpha[((b[3] & 0x01) << 2) | ((b[4] & 0xc0) >> 6)];
+ a[1][2] = alpha[(b[4] & 0x38) >> 3];
+ a[1][3] = alpha[(b[4] & 0x07)];
+ a[2][0] = alpha[(b[5] & 0xe0) >> 5];
+ a[2][1] = alpha[(b[5] & 0x1c) >> 2];
+ a[2][2] = alpha[((b[5] & 0x03) << 1) | ((b[6] & 0x80) >> 7)];
+ a[2][3] = alpha[(b[6] & 0x70) >> 4];
+ a[3][0] = alpha[(b[6] & 0x0e) >> 1];
+ a[3][1] = alpha[((b[6] & 0x01) << 2) | ((b[7] & 0xc0) >> 6)];
+ a[3][2] = alpha[(b[7] & 0x38) >> 3];
+ a[3][3] = alpha[(b[7] & 0x07)];
+
+ b = (BYTE *)src + 8;
+ // colour is in R5G6B5 format, convert to R8G8B8
+ DWORD colour[4];
+ BYTE red[4];
+ BYTE green[4];
+ BYTE blue[4];
+ for (int i = 0; i < 2; i++)
+ {
+ red[i] = b[2*i+1] & 0xf8;
+ green[i] = ((b[2*i+1] & 0x7) << 5) | ((b[2*i] & 0xe0) >> 3);
+ blue[i] = (b[2*i] & 0x1f) << 3;
+ }
+ red[2] = (2 * red[0] + red[1] + 1) / 3;
+ green[2] = (2 * green[0] + green[1] + 1) / 3;
+ blue[2] = (2 * blue[0] + blue[1] + 1) / 3;
+ red[3] = (red[0] + 2 * red[1] + 1) / 3;
+ green[3] = (green[0] + 2 * green[1] + 1) / 3;
+ blue[3] = (blue[0] + 2 * blue[1] + 1) / 3;
+ for (int i = 0; i < 4; i++)
+ colour[i] = (red[i] << 16) | (green[i] << 8) | blue[i];
+ // and assign them to our texture
+ for (int y = 0; y < 4; y++)
+ {
+ DWORD *d = (DWORD *)dest + destWidth * y;
+ *d++ = colour[(b[4 + y] & 0x03)] | (a[y][0] << 24);
+ *d++ = colour[(b[4 + y] & 0x0e) >> 2] | (a[y][1] << 24);
+ *d++ = colour[(b[4 + y] & 0x30) >> 4] | (a[y][2] << 24);
+ *d++ = colour[(b[4 + y] & 0xe0) >> 6] | (a[y][3] << 24);
+ }
+
+}
+
+void ConvertDXT1(const void *src, unsigned int width, unsigned int height, void *dest)
+{
+ for (unsigned int y = 0; y < height; y += 4)
+ {
+ for (unsigned int x = 0; x < width; x += 4)
+ {
+ const BYTE *s = (const BYTE *)src + y * width / 2 + x * 2;
+ DWORD *d = (DWORD *)dest + y * width + x;
+ DXT1toARGB(s, d, width);
+ }
+ }
+}
+
+void ConvertDXT4(const void *src, unsigned int width, unsigned int height, void *dest)
+{
+ // [4 4 4 4][4 4 4 4]
+ //
+ //
+ //
+ for (unsigned int y = 0; y < height; y += 4)
+ {
+ for (unsigned int x = 0; x < width; x += 4)
+ {
+ const BYTE *s = (const BYTE *)src + y * width + x * 4;
+ DWORD *d = (DWORD *)dest + y * width + x;
+ DXT4toARGB(s, d, width);
+ }
+ }
+}
+
+void GetTextureFromData(D3DTexture* pTex, void* texData, std::unique_ptr<CTexture>* ppTexture)
+{
+ XB_D3DFORMAT fmt;
+ DWORD width, height, pitch, offset;
+ ParseTextureHeader(pTex, fmt, width, height, pitch, offset);
+
+ *ppTexture = CTexture::CreateTexture(width, height, XB_FMT_A8R8G8B8);
+
+ if (*ppTexture)
+ {
+ BYTE *texDataStart = (BYTE *)texData;
+ COLOR *color = (COLOR *)texData;
+ texDataStart += offset;
+/* DXMERGE - We should really support DXT1,DXT2 and DXT4 in both renderers
+ Perhaps we should extend CTexture::Update() to support a bunch of different texture types
+ Rather than assuming linear 32bits
+ We could just override, as at least then all the loading code from various texture formats
+ will be in one place
+
+ BYTE *dstPixels = (BYTE *)lr.pBits;
+ DWORD destPitch = lr.Pitch;
+ if (fmt == XB_D3DFMT_DXT1) // Not sure if these are 100% correct, but they seem to work :P
+ {
+ pitch /= 2;
+ destPitch /= 4;
+ }
+ else if (fmt == XB_D3DFMT_DXT2)
+ {
+ destPitch /= 4;
+ }
+ else if (fmt == XB_D3DFMT_DXT4)
+ {
+ pitch /= 4;
+ destPitch /= 4;
+ }
+*/
+ if (fmt == XB_D3DFMT_DXT1)
+ {
+ pitch = width * 4;
+ BYTE *decoded = new BYTE[pitch * height];
+ ConvertDXT1(texDataStart, width, height, decoded);
+ texDataStart = decoded;
+ }
+ else if (fmt == XB_D3DFMT_DXT2 || fmt == XB_D3DFMT_DXT4)
+ {
+ pitch = width * 4;
+ BYTE *decoded = new BYTE[pitch * height];
+ ConvertDXT4(texDataStart, width, height, decoded);
+ texDataStart = decoded;
+ }
+ if (IsSwizzledFormat(fmt))
+ { // first we unswizzle
+ BYTE *unswizzled = new BYTE[pitch * height];
+ Unswizzle(texDataStart, BytesPerPixelFromFormat(fmt), width, height, unswizzled);
+ texDataStart = unswizzled;
+ }
+
+ if (IsPalettedFormat(fmt))
+ (*ppTexture)->LoadPaletted(width, height, pitch, XB_FMT_A8R8G8B8, texDataStart, color);
+ else
+ (*ppTexture)->LoadFromMemory(width, height, pitch, XB_FMT_A8R8G8B8, true, texDataStart);
+
+ if (IsSwizzledFormat(fmt) || fmt == XB_D3DFMT_DXT1 || fmt == XB_D3DFMT_DXT2 || fmt == XB_D3DFMT_DXT4)
+ {
+ delete[] texDataStart;
+ }
+ }
+}
diff --git a/xbmc/guilib/DirectXGraphics.h b/xbmc/guilib/DirectXGraphics.h
new file mode 100644
index 0000000..01159da
--- /dev/null
+++ b/xbmc/guilib/DirectXGraphics.h
@@ -0,0 +1,159 @@
+/*
+ * 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
+
+class CTexture;
+
+#include "gui3d.h"
+
+#include <memory>
+
+LPVOID XPhysicalAlloc(SIZE_T s, DWORD ulPhysicalAddress, DWORD ulAlignment, DWORD flProtect);
+void XPhysicalFree(LPVOID lpAddress);
+
+// XPR header
+struct XPR_HEADER
+{
+ DWORD dwMagic;
+ DWORD dwTotalSize;
+ DWORD dwHeaderSize;
+};
+#define XPR_MAGIC_VALUE (('0' << 24) | ('R' << 16) | ('P' << 8) | 'X')
+
+typedef enum _XB_D3DFORMAT
+{
+ XB_D3DFMT_UNKNOWN = 0xFFFFFFFF,
+
+ /* Swizzled formats */
+
+ XB_D3DFMT_A8R8G8B8 = 0x00000006,
+ XB_D3DFMT_X8R8G8B8 = 0x00000007,
+ XB_D3DFMT_R5G6B5 = 0x00000005,
+ XB_D3DFMT_R6G5B5 = 0x00000027,
+ XB_D3DFMT_X1R5G5B5 = 0x00000003,
+ XB_D3DFMT_A1R5G5B5 = 0x00000002,
+ XB_D3DFMT_A4R4G4B4 = 0x00000004,
+ XB_D3DFMT_A8 = 0x00000019,
+ XB_D3DFMT_A8B8G8R8 = 0x0000003A,
+ XB_D3DFMT_B8G8R8A8 = 0x0000003B,
+ XB_D3DFMT_R4G4B4A4 = 0x00000039,
+ XB_D3DFMT_R5G5B5A1 = 0x00000038,
+ XB_D3DFMT_R8G8B8A8 = 0x0000003C,
+ XB_D3DFMT_R8B8 = 0x00000029,
+ XB_D3DFMT_G8B8 = 0x00000028,
+
+ XB_D3DFMT_P8 = 0x0000000B,
+
+ XB_D3DFMT_L8 = 0x00000000,
+ XB_D3DFMT_A8L8 = 0x0000001A,
+ XB_D3DFMT_AL8 = 0x00000001,
+ XB_D3DFMT_L16 = 0x00000032,
+
+ XB_D3DFMT_V8U8 = 0x00000028,
+ XB_D3DFMT_L6V5U5 = 0x00000027,
+ XB_D3DFMT_X8L8V8U8 = 0x00000007,
+ XB_D3DFMT_Q8W8V8U8 = 0x0000003A,
+ XB_D3DFMT_V16U16 = 0x00000033,
+
+ XB_D3DFMT_D16_LOCKABLE = 0x0000002C,
+ XB_D3DFMT_D16 = 0x0000002C,
+ XB_D3DFMT_D24S8 = 0x0000002A,
+ XB_D3DFMT_F16 = 0x0000002D,
+ XB_D3DFMT_F24S8 = 0x0000002B,
+
+ /* YUV formats */
+
+ XB_D3DFMT_YUY2 = 0x00000024,
+ XB_D3DFMT_UYVY = 0x00000025,
+
+ /* Compressed formats */
+
+ XB_D3DFMT_DXT1 = 0x0000000C,
+ XB_D3DFMT_DXT2 = 0x0000000E,
+ XB_D3DFMT_DXT3 = 0x0000000E,
+ XB_D3DFMT_DXT4 = 0x0000000F,
+ XB_D3DFMT_DXT5 = 0x0000000F,
+
+ /* Linear formats */
+
+ XB_D3DFMT_LIN_A1R5G5B5 = 0x00000010,
+ XB_D3DFMT_LIN_A4R4G4B4 = 0x0000001D,
+ XB_D3DFMT_LIN_A8 = 0x0000001F,
+ XB_D3DFMT_LIN_A8B8G8R8 = 0x0000003F,
+ XB_D3DFMT_LIN_A8R8G8B8 = 0x00000012,
+ XB_D3DFMT_LIN_B8G8R8A8 = 0x00000040,
+ XB_D3DFMT_LIN_G8B8 = 0x00000017,
+ XB_D3DFMT_LIN_R4G4B4A4 = 0x0000003E,
+ XB_D3DFMT_LIN_R5G5B5A1 = 0x0000003D,
+ XB_D3DFMT_LIN_R5G6B5 = 0x00000011,
+ XB_D3DFMT_LIN_R6G5B5 = 0x00000037,
+ XB_D3DFMT_LIN_R8B8 = 0x00000016,
+ XB_D3DFMT_LIN_R8G8B8A8 = 0x00000041,
+ XB_D3DFMT_LIN_X1R5G5B5 = 0x0000001C,
+ XB_D3DFMT_LIN_X8R8G8B8 = 0x0000001E,
+
+ XB_D3DFMT_LIN_A8L8 = 0x00000020,
+ XB_D3DFMT_LIN_AL8 = 0x0000001B,
+ XB_D3DFMT_LIN_L16 = 0x00000035,
+ XB_D3DFMT_LIN_L8 = 0x00000013,
+
+ XB_D3DFMT_LIN_V16U16 = 0x00000036,
+ XB_D3DFMT_LIN_V8U8 = 0x00000017,
+ XB_D3DFMT_LIN_L6V5U5 = 0x00000037,
+ XB_D3DFMT_LIN_X8L8V8U8 = 0x0000001E,
+ XB_D3DFMT_LIN_Q8W8V8U8 = 0x00000012,
+
+ XB_D3DFMT_LIN_D24S8 = 0x0000002E,
+ XB_D3DFMT_LIN_F24S8 = 0x0000002F,
+ XB_D3DFMT_LIN_D16 = 0x00000030,
+ XB_D3DFMT_LIN_F16 = 0x00000031,
+
+ XB_D3DFMT_VERTEXDATA = 100,
+ XB_D3DFMT_INDEX16 = 101,
+
+ XB_D3DFMT_FORCE_DWORD =0x7fffffff
+} XB_D3DFORMAT;
+
+DWORD GetD3DFormat(XB_D3DFORMAT format);
+DWORD BytesPerPixelFromFormat(XB_D3DFORMAT format);
+bool IsPalettedFormat(XB_D3DFORMAT format);
+void ParseTextureHeader(D3DTexture *tex, XB_D3DFORMAT &fmt, DWORD &width, DWORD &height, DWORD &pitch, DWORD &offset);
+bool IsSwizzledFormat(XB_D3DFORMAT format);
+
+#ifndef TARGET_POSIX
+typedef unsigned __int8 uint8_t;
+typedef __int16 int16_t;
+#endif
+
+#pragma pack(push, 2)
+typedef struct {
+ uint8_t id[2]; // offset
+ uint32_t filesize; // 2
+ uint32_t reserved; // 6
+ uint32_t headersize; // 10
+ uint32_t infoSize; // 14
+ uint32_t width; // 18
+ uint32_t height; // 22
+ uint16_t biPlanes; // 26
+ uint16_t bits; // 28
+ uint32_t biCompression; // 30
+ uint32_t biSizeImage; // 34
+ uint32_t biXPelsPerMeter; // 38
+ uint32_t biYPelsPerMeter; // 42
+ uint32_t biClrUsed; // 46
+ uint32_t biClrImportant; // 50
+} BMPHEAD;
+#pragma pack(pop)
+
+void Unswizzle(const void *src, unsigned int depth, unsigned int width, unsigned int height, void *dest);
+void DXT1toARGB(const void *src, void *dest, unsigned int destWidth);
+void DXT4toARGB(const void *src, void *dest, unsigned int destWidth);
+void ConvertDXT1(const void *src, unsigned int width, unsigned int height, void *dest);
+void ConvertDXT4(const void *src, unsigned int width, unsigned int height, void *dest);
+void GetTextureFromData(D3DTexture* pTex, void* texData, std::unique_ptr<CTexture>* ppTexture);
diff --git a/xbmc/guilib/DirtyRegion.h b/xbmc/guilib/DirtyRegion.h
new file mode 100644
index 0000000..51af101
--- /dev/null
+++ b/xbmc/guilib/DirtyRegion.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "utils/Geometry.h"
+
+#include <vector>
+
+class CDirtyRegion : public CRect
+{
+public:
+ explicit CDirtyRegion(const CRect &rect) : CRect(rect) { m_age = 0; }
+ CDirtyRegion(float left, float top, float right, float bottom) : CRect(left, top, right, bottom) { m_age = 0; }
+ CDirtyRegion() : CRect() { m_age = 0; }
+
+ int UpdateAge() { return ++m_age; }
+private:
+ int m_age;
+};
+
+typedef std::vector<CDirtyRegion> CDirtyRegionList;
diff --git a/xbmc/guilib/DirtyRegionSolvers.cpp b/xbmc/guilib/DirtyRegionSolvers.cpp
new file mode 100644
index 0000000..58c32a1
--- /dev/null
+++ b/xbmc/guilib/DirtyRegionSolvers.cpp
@@ -0,0 +1,73 @@
+/*
+ * 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 "DirtyRegionSolvers.h"
+
+#include "windowing/GraphicContext.h"
+
+#include <stdio.h>
+
+void CUnionDirtyRegionSolver::Solve(const CDirtyRegionList &input, CDirtyRegionList &output)
+{
+ CDirtyRegion unifiedRegion;
+ for (unsigned int i = 0; i < input.size(); i++)
+ unifiedRegion.Union(input[i]);
+
+ if (!unifiedRegion.IsEmpty())
+ output.push_back(unifiedRegion);
+}
+
+void CFillViewportAlwaysRegionSolver::Solve(const CDirtyRegionList &input, CDirtyRegionList &output)
+{
+ CDirtyRegion unifiedRegion(CServiceBroker::GetWinSystem()->GetGfxContext().GetViewWindow());
+ output.push_back(unifiedRegion);
+}
+
+void CFillViewportOnChangeRegionSolver::Solve(const CDirtyRegionList &input, CDirtyRegionList &output)
+{
+ if (!input.empty())
+ output.assign(1,CDirtyRegion(CServiceBroker::GetWinSystem()->GetGfxContext().GetViewWindow()));
+}
+
+CGreedyDirtyRegionSolver::CGreedyDirtyRegionSolver()
+{
+ m_costNewRegion = 10.0f;
+ m_costPerArea = 0.01f;
+}
+
+void CGreedyDirtyRegionSolver::Solve(const CDirtyRegionList &input, CDirtyRegionList &output)
+{
+ for (unsigned int i = 0; i < input.size(); i++)
+ {
+ CDirtyRegion possibleUnionRegion;
+ int possibleUnionNbr = -1;
+ float possibleUnionCost = 100000.0f;
+
+ CDirtyRegion currentRegion = input[i];
+ for (unsigned int j = 0; j < output.size(); j++)
+ {
+ CDirtyRegion temporaryUnion = output[j];
+ temporaryUnion.Union(currentRegion);
+ float temporaryCost = m_costPerArea * (temporaryUnion.Area() - output[j].Area());
+ if (temporaryCost < possibleUnionCost)
+ {
+ //! @todo if the temporaryCost is 0 then we could skip checking the other regions since there exist no better solution
+ possibleUnionRegion = temporaryUnion;
+ possibleUnionNbr = j;
+ possibleUnionCost = temporaryCost;
+ }
+ }
+
+ float newRegionTotalCost = m_costPerArea * currentRegion.Area() + m_costNewRegion;
+
+ if (possibleUnionNbr >= 0 && possibleUnionCost < newRegionTotalCost)
+ output[possibleUnionNbr] = possibleUnionRegion;
+ else
+ output.push_back(currentRegion);
+ }
+}
diff --git a/xbmc/guilib/DirtyRegionSolvers.h b/xbmc/guilib/DirtyRegionSolvers.h
new file mode 100644
index 0000000..ace6264
--- /dev/null
+++ b/xbmc/guilib/DirtyRegionSolvers.h
@@ -0,0 +1,39 @@
+/*
+ * 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 "IDirtyRegionSolver.h"
+
+class CUnionDirtyRegionSolver : public IDirtyRegionSolver
+{
+public:
+ void Solve(const CDirtyRegionList &input, CDirtyRegionList &output) override;
+};
+
+class CFillViewportAlwaysRegionSolver : public IDirtyRegionSolver
+{
+public:
+ void Solve(const CDirtyRegionList &input, CDirtyRegionList &output) override;
+};
+
+class CFillViewportOnChangeRegionSolver : public IDirtyRegionSolver
+{
+public:
+ void Solve(const CDirtyRegionList &input, CDirtyRegionList &output) override;
+};
+
+class CGreedyDirtyRegionSolver : public IDirtyRegionSolver
+{
+public:
+ CGreedyDirtyRegionSolver();
+ void Solve(const CDirtyRegionList &input, CDirtyRegionList &output) override;
+private:
+ float m_costNewRegion;
+ float m_costPerArea;
+};
diff --git a/xbmc/guilib/DirtyRegionTracker.cpp b/xbmc/guilib/DirtyRegionTracker.cpp
new file mode 100644
index 0000000..398d455
--- /dev/null
+++ b/xbmc/guilib/DirtyRegionTracker.cpp
@@ -0,0 +1,88 @@
+/*
+ * 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 "DirtyRegionTracker.h"
+
+#include "DirtyRegionSolvers.h"
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+
+#include <stdio.h>
+
+CDirtyRegionTracker::CDirtyRegionTracker(int buffering)
+{
+ m_buffering = buffering;
+ m_solver = NULL;
+}
+
+CDirtyRegionTracker::~CDirtyRegionTracker()
+{
+ delete m_solver;
+}
+
+void CDirtyRegionTracker::SelectAlgorithm()
+{
+ delete m_solver;
+
+ switch (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_guiAlgorithmDirtyRegions)
+ {
+ case DIRTYREGION_SOLVER_FILL_VIEWPORT_ON_CHANGE:
+ CLog::Log(LOGDEBUG, "guilib: Fill viewport on change for solving rendering passes");
+ m_solver = new CFillViewportOnChangeRegionSolver();
+ break;
+ case DIRTYREGION_SOLVER_COST_REDUCTION:
+ CLog::Log(LOGDEBUG, "guilib: Cost reduction as algorithm for solving rendering passes");
+ m_solver = new CGreedyDirtyRegionSolver();
+ break;
+ case DIRTYREGION_SOLVER_UNION:
+ m_solver = new CUnionDirtyRegionSolver();
+ CLog::Log(LOGDEBUG, "guilib: Union as algorithm for solving rendering passes");
+ break;
+ case DIRTYREGION_SOLVER_FILL_VIEWPORT_ALWAYS:
+ default:
+ CLog::Log(LOGDEBUG, "guilib: Fill viewport always for solving rendering passes");
+ m_solver = new CFillViewportAlwaysRegionSolver();
+ break;
+ }
+}
+
+void CDirtyRegionTracker::MarkDirtyRegion(const CDirtyRegion &region)
+{
+ if (!region.IsEmpty())
+ m_markedRegions.push_back(region);
+}
+
+const CDirtyRegionList &CDirtyRegionTracker::GetMarkedRegions() const
+{
+ return m_markedRegions;
+}
+
+CDirtyRegionList CDirtyRegionTracker::GetDirtyRegions()
+{
+ CDirtyRegionList output;
+
+ if (m_solver)
+ m_solver->Solve(m_markedRegions, output);
+
+ return output;
+}
+
+void CDirtyRegionTracker::CleanMarkedRegions()
+{
+ int buffering = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_guiVisualizeDirtyRegions ? 20 : m_buffering;
+ int i = m_markedRegions.size() - 1;
+ while (i >= 0)
+ {
+ if (m_markedRegions[i].UpdateAge() >= buffering)
+ m_markedRegions.erase(m_markedRegions.begin() + i);
+
+ i--;
+ }
+}
diff --git a/xbmc/guilib/DirtyRegionTracker.h b/xbmc/guilib/DirtyRegionTracker.h
new file mode 100644
index 0000000..697cb64
--- /dev/null
+++ b/xbmc/guilib/DirtyRegionTracker.h
@@ -0,0 +1,35 @@
+/*
+ * 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 "IDirtyRegionSolver.h"
+
+#if defined(TARGET_DARWIN_EMBEDDED)
+#define DEFAULT_BUFFERING 4
+#else
+#define DEFAULT_BUFFERING 3
+#endif
+
+class CDirtyRegionTracker
+{
+public:
+ explicit CDirtyRegionTracker(int buffering = DEFAULT_BUFFERING);
+ ~CDirtyRegionTracker();
+ void SelectAlgorithm();
+ void MarkDirtyRegion(const CDirtyRegion &region);
+
+ const CDirtyRegionList &GetMarkedRegions() const;
+ CDirtyRegionList GetDirtyRegions();
+ void CleanMarkedRegions();
+
+private:
+ CDirtyRegionList m_markedRegions;
+ int m_buffering;
+ IDirtyRegionSolver *m_solver;
+};
diff --git a/xbmc/guilib/DispResource.h b/xbmc/guilib/DispResource.h
new file mode 100644
index 0000000..e22bb21
--- /dev/null
+++ b/xbmc/guilib/DispResource.h
@@ -0,0 +1,32 @@
+/*
+ * 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
+
+// interface for registering into windowing
+// to get notified about display events
+// interface only, does not control lifetime of the object
+class IDispResource
+{
+public:
+ virtual ~IDispResource() = default;
+
+ virtual void OnLostDisplay() {}
+ virtual void OnResetDisplay() {}
+ virtual void OnAppFocusChange(bool focus) {}
+};
+
+// interface used by clients to register into render loop
+// interface only, does not control lifetime of the object
+class IRenderLoop
+{
+public:
+ virtual ~IRenderLoop() = default;
+
+ virtual void FrameMove() = 0;
+};
diff --git a/xbmc/guilib/FFmpegImage.cpp b/xbmc/guilib/FFmpegImage.cpp
new file mode 100644
index 0000000..fa25527
--- /dev/null
+++ b/xbmc/guilib/FFmpegImage.cpp
@@ -0,0 +1,754 @@
+/*
+ * 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 "FFmpegImage.h"
+
+#include "cores/FFmpeg.h"
+#include "guilib/Texture.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+extern "C"
+{
+#include <libavformat/avformat.h>
+#include <libavutil/imgutils.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/avutil.h>
+#include <libswscale/swscale.h>
+#include <libavutil/pixdesc.h>
+}
+
+Frame::Frame(const Frame& src) :
+ m_delay(src.m_delay),
+ m_imageSize(src.m_imageSize),
+ m_height(src.m_height),
+ m_width(src.m_width)
+{
+ if (src.m_pImage)
+ {
+ m_pImage = (unsigned char*) av_malloc(m_imageSize);
+ memcpy(m_pImage, src.m_pImage, m_imageSize);
+ }
+}
+
+Frame::~Frame()
+{
+ av_free(m_pImage);
+ m_pImage = nullptr;
+}
+
+struct ThumbDataManagement
+{
+ uint8_t* intermediateBuffer = nullptr; // gets av_alloced
+ AVFrame* frame_input = nullptr;
+ AVFrame* frame_temporary = nullptr;
+ SwsContext* sws = nullptr;
+ AVCodecContext* avOutctx = nullptr;
+ AVCodec* codec = nullptr;
+ ~ThumbDataManagement()
+ {
+ av_free(intermediateBuffer);
+ intermediateBuffer = nullptr;
+ av_frame_free(&frame_input);
+ frame_input = nullptr;
+ av_frame_free(&frame_temporary);
+ frame_temporary = nullptr;
+ avcodec_free_context(&avOutctx);
+ avOutctx = nullptr;
+ sws_freeContext(sws);
+ sws = nullptr;
+ }
+};
+
+// valid positions are including 0 (start of buffer)
+// and bufferSize -1 last data point
+static inline size_t Clamp(int64_t newPosition, size_t bufferSize)
+{
+ return std::min(std::max((int64_t) 0, newPosition), (int64_t) (bufferSize -1));
+}
+
+static int mem_file_read(void *h, uint8_t* buf, int size)
+{
+ if (size < 0)
+ return -1;
+
+ MemBuffer* mbuf = static_cast<MemBuffer*>(h);
+ int64_t unread = mbuf->size - mbuf->pos;
+ if (unread <= 0)
+ return AVERROR_EOF;
+
+ size_t tocopy = std::min((size_t)size, (size_t)unread);
+ memcpy(buf, mbuf->data + mbuf->pos, tocopy);
+ mbuf->pos += tocopy;
+ return static_cast<int>(tocopy);
+}
+
+static int64_t mem_file_seek(void *h, int64_t pos, int whence)
+{
+ MemBuffer* mbuf = static_cast<MemBuffer*>(h);
+ if (whence == AVSEEK_SIZE)
+ return mbuf->size;
+
+ // we want to ignore the AVSEEK_FORCE flag and therefore mask it away
+ whence &= ~AVSEEK_FORCE;
+
+ if (whence == SEEK_SET)
+ {
+ mbuf->pos = Clamp(pos, mbuf->size);
+ }
+ else if (whence == SEEK_CUR)
+ {
+ mbuf->pos = Clamp(((int64_t)mbuf->pos) + pos, mbuf->size);
+ }
+ else
+ CLog::LogF(LOGERROR, "Unknown seek mode: {}", whence);
+
+ return mbuf->pos;
+}
+
+CFFmpegImage::CFFmpegImage(const std::string& strMimeType) : m_strMimeType(strMimeType)
+{
+ m_hasAlpha = false;
+ m_pFrame = nullptr;
+ m_outputBuffer = nullptr;
+}
+
+CFFmpegImage::~CFFmpegImage()
+{
+ av_frame_free(&m_pFrame);
+ // someone could have forgotten to call us
+ CleanupLocalOutputBuffer();
+ if (m_fctx)
+ {
+ avcodec_free_context(&m_codec_ctx);
+ avformat_close_input(&m_fctx);
+ }
+ if (m_ioctx)
+ FreeIOCtx(&m_ioctx);
+
+ m_buf.data = nullptr;
+ m_buf.pos = 0;
+ m_buf.size = 0;
+}
+
+bool CFFmpegImage::LoadImageFromMemory(unsigned char* buffer, unsigned int bufSize,
+ unsigned int width, unsigned int height)
+{
+
+ if (!Initialize(buffer, bufSize))
+ {
+ //log
+ return false;
+ }
+
+ av_frame_free(&m_pFrame);
+ m_pFrame = ExtractFrame();
+
+ return !(m_pFrame == nullptr);
+}
+
+bool CFFmpegImage::Initialize(unsigned char* buffer, size_t bufSize)
+{
+ int bufferSize = 4096;
+ uint8_t* fbuffer = (uint8_t*)av_malloc(bufferSize + AV_INPUT_BUFFER_PADDING_SIZE);
+ if (!fbuffer)
+ {
+ CLog::LogF(LOGERROR, "Could not allocate buffer");
+ return false;
+ }
+ m_buf.data = buffer;
+ m_buf.size = bufSize;
+ m_buf.pos = 0;
+
+ m_ioctx = avio_alloc_context(fbuffer, bufferSize, 0, &m_buf,
+ mem_file_read, NULL, mem_file_seek);
+
+ if (!m_ioctx)
+ {
+ av_free(fbuffer);
+ CLog::LogF(LOGERROR, "Could not allocate AVIOContext");
+ return false;
+ }
+
+ // signal to ffmepg this is not streaming protocol
+ m_ioctx->max_packet_size = bufferSize;
+
+ m_fctx = avformat_alloc_context();
+ if (!m_fctx)
+ {
+ FreeIOCtx(&m_ioctx);
+ CLog::LogF(LOGERROR, "Could not allocate AVFormatContext");
+ return false;
+ }
+
+ m_fctx->pb = m_ioctx;
+
+ // Some clients have pngs saved as jpeg or ask us for png but are jpeg
+ // mythv throws all mimetypes away and asks us with application/octet-stream
+ // this is poor man's fallback to at least identify png / jpeg
+ bool is_jpeg = (bufSize > 2 && buffer[0] == 0xFF && buffer[1] == 0xD8 && buffer[2] == 0xFF);
+ bool is_png = (bufSize > 3 && buffer[1] == 'P' && buffer[2] == 'N' && buffer[3] == 'G');
+ bool is_tiff = (bufSize > 2 && buffer[0] == 'I' && buffer[1] == 'I' && buffer[2] == '*');
+
+ AVInputFormat* inp = nullptr;
+ if (is_jpeg)
+ inp = av_find_input_format("image2");
+ else if (m_strMimeType == "image/apng")
+ inp = av_find_input_format("apng");
+ else if (is_png)
+ inp = av_find_input_format("png_pipe");
+ else if (is_tiff)
+ inp = av_find_input_format("tiff_pipe");
+ else if (m_strMimeType == "image/jp2")
+ inp = av_find_input_format("j2k_pipe");
+ else if (m_strMimeType == "image/webp")
+ inp = av_find_input_format("webp_pipe");
+ // brute force parse if above check already failed
+ else if (m_strMimeType == "image/jpeg" || m_strMimeType == "image/jpg")
+ inp = av_find_input_format("image2");
+ else if (m_strMimeType == "image/png")
+ inp = av_find_input_format("png_pipe");
+ else if (m_strMimeType == "image/tiff")
+ inp = av_find_input_format("tiff_pipe");
+ else if (m_strMimeType == "image/gif")
+ inp = av_find_input_format("gif");
+
+ if (avformat_open_input(&m_fctx, NULL, inp, NULL) < 0)
+ {
+ CLog::Log(LOGERROR, "Could not find suitable input format: {}", m_strMimeType);
+ avformat_close_input(&m_fctx);
+ FreeIOCtx(&m_ioctx);
+ return false;
+ }
+
+ if (m_fctx->nb_streams <= 0)
+ {
+ avformat_close_input(&m_fctx);
+ FreeIOCtx(&m_ioctx);
+ return false;
+ }
+ AVCodecParameters* codec_params = m_fctx->streams[0]->codecpar;
+ AVCodec* codec = avcodec_find_decoder(codec_params->codec_id);
+ m_codec_ctx = avcodec_alloc_context3(codec);
+ if (!m_codec_ctx)
+ {
+ avformat_close_input(&m_fctx);
+ FreeIOCtx(&m_ioctx);
+ return false;
+ }
+
+ if (avcodec_parameters_to_context(m_codec_ctx, codec_params) < 0)
+ {
+ avformat_close_input(&m_fctx);
+ avcodec_free_context(&m_codec_ctx);
+ FreeIOCtx(&m_ioctx);
+ return false;
+ }
+
+ if (avcodec_open2(m_codec_ctx, codec, NULL) < 0)
+ {
+ avformat_close_input(&m_fctx);
+ avcodec_free_context(&m_codec_ctx);
+ FreeIOCtx(&m_ioctx);
+ return false;
+ }
+
+ return true;
+}
+
+AVFrame* CFFmpegImage::ExtractFrame()
+{
+ if (!m_fctx || !m_fctx->streams[0])
+ {
+ CLog::LogF(LOGERROR, "No valid format context or stream");
+ return nullptr;
+ }
+
+ AVPacket pkt;
+ AVFrame* frame = av_frame_alloc();
+ int frame_decoded = 0;
+ int ret = 0;
+ ret = av_read_frame(m_fctx, &pkt);
+ if (ret < 0)
+ {
+ CLog::Log(LOGDEBUG, "Error [{}] while reading frame: {}", ret, strerror(AVERROR(ret)));
+ av_frame_free(&frame);
+ av_packet_unref(&pkt);
+ return nullptr;
+ }
+
+ ret = DecodeFFmpegFrame(m_codec_ctx, frame, &frame_decoded, &pkt);
+ if (ret < 0 || frame_decoded == 0 || !frame)
+ {
+ CLog::Log(LOGDEBUG, "Error [{}] while decoding frame: {}", ret, strerror(AVERROR(ret)));
+ av_frame_free(&frame);
+ av_packet_unref(&pkt);
+ return nullptr;
+ }
+ //we need milliseconds
+ frame->pkt_duration = av_rescale_q(frame->pkt_duration, m_fctx->streams[0]->time_base, AVRational{ 1, 1000 });
+ m_height = frame->height;
+ m_width = frame->width;
+ m_originalWidth = m_width;
+ m_originalHeight = m_height;
+
+ const AVPixFmtDescriptor* pixDescriptor = av_pix_fmt_desc_get(static_cast<AVPixelFormat>(frame->format));
+ if (pixDescriptor && ((pixDescriptor->flags & (AV_PIX_FMT_FLAG_ALPHA | AV_PIX_FMT_FLAG_PAL)) != 0))
+ m_hasAlpha = true;
+
+ AVDictionary* dic = frame->metadata;
+ AVDictionaryEntry* entry = NULL;
+ if (dic)
+ {
+ entry = av_dict_get(dic, "Orientation", NULL, AV_DICT_MATCH_CASE);
+ if (entry && entry->value)
+ {
+ int orientation = atoi(entry->value);
+ // only values between including 0 and including 8
+ // http://sylvana.net/jpegcrop/exif_orientation.html
+ if (orientation >= 0 && orientation <= 8)
+ m_orientation = (unsigned int)orientation;
+ }
+ }
+ av_packet_unref(&pkt);
+
+ return frame;
+}
+
+AVPixelFormat CFFmpegImage::ConvertFormats(AVFrame* frame)
+{
+ switch (frame->format) {
+ case AV_PIX_FMT_YUVJ420P:
+ return AV_PIX_FMT_YUV420P;
+ break;
+ case AV_PIX_FMT_YUVJ422P:
+ return AV_PIX_FMT_YUV422P;
+ break;
+ case AV_PIX_FMT_YUVJ444P:
+ return AV_PIX_FMT_YUV444P;
+ break;
+ case AV_PIX_FMT_YUVJ440P:
+ return AV_PIX_FMT_YUV440P;
+ default:
+ return static_cast<AVPixelFormat>(frame->format);
+ break;
+ }
+}
+
+void CFFmpegImage::FreeIOCtx(AVIOContext** ioctx)
+{
+ av_freep(&((*ioctx)->buffer));
+ av_freep(ioctx);
+}
+
+bool CFFmpegImage::Decode(unsigned char * const pixels, unsigned int width, unsigned int height,
+ unsigned int pitch, unsigned int format)
+{
+ if (m_width == 0 || m_height == 0 || format != XB_FMT_A8R8G8B8)
+ return false;
+
+ if (pixels == nullptr)
+ {
+ CLog::Log(LOGERROR, "{} - No valid buffer pointer (nullptr) passed", __FUNCTION__);
+ return false;
+ }
+
+ if (!m_pFrame || !m_pFrame->data[0])
+ {
+ CLog::LogF(LOGERROR, "AVFrame member not allocated");
+ return false;
+ }
+
+ return DecodeFrame(m_pFrame, width, height, pitch, pixels);
+}
+
+int CFFmpegImage::EncodeFFmpegFrame(AVCodecContext *avctx, AVPacket *pkt, int *got_packet, AVFrame *frame)
+{
+ int ret;
+
+ *got_packet = 0;
+
+ ret = avcodec_send_frame(avctx, frame);
+ if (ret < 0)
+ return ret;
+
+ ret = avcodec_receive_packet(avctx, pkt);
+ if (!ret)
+ *got_packet = 1;
+
+ if (ret == AVERROR(EAGAIN))
+ return 0;
+
+ return ret;
+}
+
+int CFFmpegImage::DecodeFFmpegFrame(AVCodecContext *avctx, AVFrame *frame, int *got_frame, AVPacket *pkt)
+{
+ int ret;
+
+ *got_frame = 0;
+
+ if (pkt)
+ {
+ ret = avcodec_send_packet(avctx, pkt);
+ // In particular, we don't expect AVERROR(EAGAIN), because we read all
+ // decoded frames with avcodec_receive_frame() until done.
+ if (ret < 0)
+ return ret;
+ }
+
+ ret = avcodec_receive_frame(avctx, frame);
+ if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF)
+ return ret;
+ if (ret >= 0) // static code analysers would complain
+ *got_frame = 1;
+
+ return 0;
+}
+
+bool CFFmpegImage::DecodeFrame(AVFrame* frame, unsigned int width, unsigned int height, unsigned int pitch, unsigned char * const pixels)
+{
+ if (pixels == nullptr)
+ {
+ CLog::Log(LOGERROR, "{} - No valid buffer pointer (nullptr) passed", __FUNCTION__);
+ return false;
+ }
+
+ AVFrame* pictureRGB = av_frame_alloc();
+ if (!pictureRGB)
+ {
+ CLog::LogF(LOGERROR, "AVFrame could not be allocated");
+ return false;
+ }
+
+ // we align on 16 as the input provided by the Texture also aligns the buffer size to 16
+ int size = av_image_fill_arrays(pictureRGB->data, pictureRGB->linesize, NULL, AV_PIX_FMT_RGB32, width, height, 16);
+ if (size < 0)
+ {
+ CLog::LogF(LOGERROR, "Could not allocate AVFrame member with {} x {} pixels", width, height);
+ av_frame_free(&pictureRGB);
+ return false;
+ }
+
+ bool needsCopy = false;
+ int pixelsSize = pitch * height;
+ bool aligned = (((uintptr_t)(const void *)(pixels)) % (32) == 0);
+ if (!aligned)
+ CLog::Log(LOGDEBUG, "Alignment of external buffer is not suitable for ffmpeg intrinsics - please fix your malloc");
+
+ if (aligned && size == pixelsSize && (int)pitch == pictureRGB->linesize[0])
+ {
+ // We can use the pixels buffer directly
+ pictureRGB->data[0] = pixels;
+ }
+ else
+ {
+ // We need an extra buffer and copy it manually afterwards
+ pictureRGB->format = AV_PIX_FMT_RGB32;
+ pictureRGB->width = width;
+ pictureRGB->height = height;
+ // we copy the data manually later so give a chance to intrinsics (e.g. mmx, neon)
+ if (av_frame_get_buffer(pictureRGB, 32) < 0)
+ {
+ CLog::LogF(LOGERROR, "Could not allocate temp buffer of size {} bytes", size);
+ av_frame_free(&pictureRGB);
+ return false;
+ }
+ needsCopy = true;
+ }
+
+ // Especially jpeg formats are full range this we need to take care here
+ // Input Formats like RGBA are handled correctly automatically
+ AVColorRange range = frame->color_range;
+ AVPixelFormat pixFormat = ConvertFormats(frame);
+
+ // assumption quadratic maximums e.g. 2048x2048
+ float ratio = m_width / (float)m_height;
+ unsigned int nHeight = m_originalHeight;
+ unsigned int nWidth = m_originalWidth;
+ if (nHeight > height)
+ {
+ nHeight = height;
+ nWidth = (unsigned int)(nHeight * ratio + 0.5f);
+ }
+ if (nWidth > width)
+ {
+ nWidth = width;
+ nHeight = (unsigned int)(nWidth / ratio + 0.5f);
+ }
+
+ struct SwsContext* context = sws_getContext(m_originalWidth, m_originalHeight, pixFormat,
+ nWidth, nHeight, AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL);
+
+ if (range == AVCOL_RANGE_JPEG)
+ {
+ int* inv_table = nullptr;
+ int* table = nullptr;
+ int srcRange, dstRange, brightness, contrast, saturation;
+ sws_getColorspaceDetails(context, &inv_table, &srcRange, &table, &dstRange, &brightness, &contrast, &saturation);
+ srcRange = 1;
+ sws_setColorspaceDetails(context, inv_table, srcRange, table, dstRange, brightness, contrast, saturation);
+ }
+
+ sws_scale(context, frame->data, frame->linesize, 0, m_originalHeight,
+ pictureRGB->data, pictureRGB->linesize);
+ sws_freeContext(context);
+
+ if (needsCopy)
+ {
+ int minPitch = std::min((int)pitch, pictureRGB->linesize[0]);
+ if (minPitch < 0)
+ {
+ CLog::LogF(LOGERROR, "negative pitch or height");
+ av_frame_free(&pictureRGB);
+ return false;
+ }
+ const unsigned char *src = pictureRGB->data[0];
+ unsigned char* dst = pixels;
+
+ for (unsigned int y = 0; y < nHeight; y++)
+ {
+ memcpy(dst, src, minPitch);
+ src += pictureRGB->linesize[0];
+ dst += pitch;
+ }
+ av_frame_free(&pictureRGB);
+ }
+ else
+ {
+ // we only lended the data so don't get it deleted
+ pictureRGB->data[0] = nullptr;
+ av_frame_free(&pictureRGB);
+ }
+
+ // update width and height original dimensions are kept
+ m_height = nHeight;
+ m_width = nWidth;
+
+ return true;
+}
+
+bool CFFmpegImage::CreateThumbnailFromSurface(unsigned char* bufferin, unsigned int width,
+ unsigned int height, unsigned int format,
+ unsigned int pitch,
+ const std::string& destFile,
+ unsigned char* &bufferout,
+ unsigned int &bufferoutSize)
+{
+ // It seems XB_FMT_A8R8G8B8 mean RGBA and not ARGB
+ if (format != XB_FMT_A8R8G8B8)
+ {
+ CLog::Log(LOGERROR, "Supplied format: {} is not supported.", format);
+ return false;
+ }
+
+ bool jpg_output = false;
+ if (m_strMimeType == "image/jpeg" || m_strMimeType == "image/jpg")
+ jpg_output = true;
+ else if (m_strMimeType == "image/png")
+ jpg_output = false;
+ else
+ {
+ CLog::Log(LOGERROR, "Output Format is not supported: {} is not supported.", destFile);
+ return false;
+ }
+
+ ThumbDataManagement tdm;
+
+ tdm.codec = avcodec_find_encoder(jpg_output ? AV_CODEC_ID_MJPEG : AV_CODEC_ID_PNG);
+ if (!tdm.codec)
+ {
+ CLog::Log(LOGERROR, "You are missing a working encoder for format: {}",
+ jpg_output ? "JPEG" : "PNG");
+ return false;
+ }
+
+ tdm.avOutctx = avcodec_alloc_context3(tdm.codec);
+ if (!tdm.avOutctx)
+ {
+ CLog::Log(LOGERROR, "Could not allocate context for thumbnail: {}", destFile);
+ return false;
+ }
+
+ tdm.avOutctx->height = height;
+ tdm.avOutctx->width = width;
+ tdm.avOutctx->time_base.num = 1;
+ tdm.avOutctx->time_base.den = 1;
+ tdm.avOutctx->pix_fmt = jpg_output ? AV_PIX_FMT_YUVJ420P : AV_PIX_FMT_RGBA;
+ tdm.avOutctx->flags = AV_CODEC_FLAG_QSCALE;
+ tdm.avOutctx->mb_lmin = tdm.avOutctx->qmin * FF_QP2LAMBDA;
+ tdm.avOutctx->mb_lmax = tdm.avOutctx->qmax * FF_QP2LAMBDA;
+ unsigned int quality =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageQualityJpeg;
+ tdm.avOutctx->global_quality =
+ jpg_output ? quality * FF_QP2LAMBDA : tdm.avOutctx->qmin * FF_QP2LAMBDA;
+
+ unsigned int internalBufOutSize = 0;
+
+ int size = av_image_get_buffer_size(tdm.avOutctx->pix_fmt, tdm.avOutctx->width, tdm.avOutctx->height, 16);
+ if (size < 0)
+ {
+ CLog::Log(LOGERROR, "Could not compute picture size for thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ return false;
+ }
+ internalBufOutSize = (unsigned int) size;
+
+ tdm.intermediateBuffer = (uint8_t*) av_malloc(internalBufOutSize);
+ if (!tdm.intermediateBuffer)
+ {
+ CLog::Log(LOGERROR, "Could not allocate memory for thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ return false;
+ }
+
+ if (avcodec_open2(tdm.avOutctx, tdm.codec, NULL) < 0)
+ {
+ CLog::Log(LOGERROR, "Could not open avcodec context thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ return false;
+ }
+
+ tdm.frame_input = av_frame_alloc();
+ if (!tdm.frame_input)
+ {
+ CLog::Log(LOGERROR, "Could not allocate frame for thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ return false;
+ }
+
+ // convert the RGB32 frame to AV_PIX_FMT_YUV420P - we use this later on as AV_PIX_FMT_YUVJ420P
+ tdm.frame_temporary = av_frame_alloc();
+ if (!tdm.frame_temporary)
+ {
+ CLog::Log(LOGERROR, "Could not allocate frame for thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ return false;
+ }
+
+ if (av_image_fill_arrays(tdm.frame_temporary->data, tdm.frame_temporary->linesize, tdm.intermediateBuffer, jpg_output ? AV_PIX_FMT_YUV420P : AV_PIX_FMT_RGBA, width, height, 16) < 0)
+ {
+ CLog::Log(LOGERROR, "Could not fill picture for thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ return false;
+ }
+
+ uint8_t* src[] = { bufferin, NULL, NULL, NULL };
+ int srcStride[] = { (int) pitch, 0, 0, 0};
+
+ //input size == output size which means only pix_fmt conversion
+ tdm.sws = sws_getContext(width, height, AV_PIX_FMT_RGB32, width, height, jpg_output ? AV_PIX_FMT_YUV420P : AV_PIX_FMT_RGBA, 0, 0, 0, 0);
+ if (!tdm.sws)
+ {
+ CLog::Log(LOGERROR, "Could not setup scaling context for thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ return false;
+ }
+
+ // Setup jpeg range for sws
+ if (jpg_output)
+ {
+ int* inv_table = nullptr;
+ int* table = nullptr;
+ int srcRange, dstRange, brightness, contrast, saturation;
+
+ if (sws_getColorspaceDetails(tdm.sws, &inv_table, &srcRange, &table, &dstRange, &brightness, &contrast, &saturation) < 0)
+ {
+ CLog::Log(LOGERROR, "SWS_SCALE failed to get ColorSpaceDetails for thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ return false;
+ }
+ dstRange = 1; // jpeg full range yuv420p output
+ srcRange = 0; // full range RGB32 input
+ if (sws_setColorspaceDetails(tdm.sws, inv_table, srcRange, table, dstRange, brightness, contrast, saturation) < 0)
+ {
+ CLog::Log(LOGERROR, "SWS_SCALE failed to set ColorSpace Details for thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ return false;
+ }
+ }
+
+ if (sws_scale(tdm.sws, src, srcStride, 0, height, tdm.frame_temporary->data, tdm.frame_temporary->linesize) < 0)
+ {
+ CLog::Log(LOGERROR, "SWS_SCALE failed for thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ return false;
+ }
+ tdm.frame_input->pts = 1;
+ tdm.frame_input->quality = tdm.avOutctx->global_quality;
+ tdm.frame_input->data[0] = tdm.frame_temporary->data[0];
+ tdm.frame_input->data[1] = tdm.frame_temporary->data[1];
+ tdm.frame_input->data[2] = tdm.frame_temporary->data[2];
+ tdm.frame_input->height = height;
+ tdm.frame_input->width = width;
+ tdm.frame_input->linesize[0] = tdm.frame_temporary->linesize[0];
+ tdm.frame_input->linesize[1] = tdm.frame_temporary->linesize[1];
+ tdm.frame_input->linesize[2] = tdm.frame_temporary->linesize[2];
+ // this is deprecated but mjpeg is not yet transitioned
+ tdm.frame_input->format = jpg_output ? AV_PIX_FMT_YUVJ420P : AV_PIX_FMT_RGBA;
+
+ int got_package = 0;
+ AVPacket* avpkt;
+ avpkt = av_packet_alloc();
+
+ int ret = EncodeFFmpegFrame(tdm.avOutctx, avpkt, &got_package, tdm.frame_input);
+
+ if ((ret < 0) || (got_package == 0))
+ {
+ CLog::Log(LOGERROR, "Could not encode thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ av_packet_free(&avpkt);
+ return false;
+ }
+
+ bufferoutSize = avpkt->size;
+ m_outputBuffer = (uint8_t*) av_malloc(bufferoutSize);
+ if (!m_outputBuffer)
+ {
+ CLog::Log(LOGERROR, "Could not generate allocate memory for thumbnail: {}", destFile);
+ CleanupLocalOutputBuffer();
+ av_packet_free(&avpkt);
+ return false;
+ }
+ // update buffer ptr for caller
+ bufferout = m_outputBuffer;
+
+ // copy avpkt data into outputbuffer
+ memcpy(m_outputBuffer, avpkt->data, avpkt->size);
+ av_packet_free(&avpkt);
+
+ return true;
+}
+
+void CFFmpegImage::ReleaseThumbnailBuffer()
+{
+ CleanupLocalOutputBuffer();
+}
+
+void CFFmpegImage::CleanupLocalOutputBuffer()
+{
+ av_free(m_outputBuffer);
+ m_outputBuffer = nullptr;
+}
+
+std::shared_ptr<Frame> CFFmpegImage::ReadFrame()
+{
+ AVFrame* avframe = ExtractFrame();
+ if (avframe == nullptr)
+ return nullptr;
+ std::shared_ptr<Frame> frame(new Frame());
+ frame->m_delay = (unsigned int)avframe->pkt_duration;
+ frame->m_pitch = avframe->width * 4;
+ frame->m_pImage = (unsigned char*) av_malloc(avframe->height * frame->m_pitch);
+ DecodeFrame(avframe, avframe->width, avframe->height, frame->m_pitch, frame->m_pImage);
+ av_frame_free(&avframe);
+ return frame;
+}
diff --git a/xbmc/guilib/FFmpegImage.h b/xbmc/guilib/FFmpegImage.h
new file mode 100644
index 0000000..0f7cee3
--- /dev/null
+++ b/xbmc/guilib/FFmpegImage.h
@@ -0,0 +1,95 @@
+/*
+ * 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 "iimage.h"
+#include <memory>
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+class Frame
+{
+public:
+ friend class CFFmpegImage;
+
+ Frame() = default;
+ virtual ~Frame();
+
+ int GetPitch() const { return m_pitch; }
+
+ unsigned char* m_pImage = nullptr;
+ unsigned int m_delay = 0;
+
+private:
+ Frame(const Frame& src);
+
+ int m_pitch = 0;
+ unsigned int m_imageSize = 0;
+ unsigned int m_height = 0;
+ unsigned int m_width = 0;
+};
+
+
+struct MemBuffer
+{
+ uint8_t* data = nullptr;
+ size_t size = 0;
+ size_t pos = 0;
+};
+
+struct AVFrame;
+struct AVIOContext;
+struct AVFormatContext;
+struct AVCodecContext;
+struct AVPacket;
+
+class CFFmpegImage : public IImage
+{
+public:
+ explicit CFFmpegImage(const std::string& strMimeType);
+ ~CFFmpegImage() override;
+
+ bool LoadImageFromMemory(unsigned char* buffer, unsigned int bufSize,
+ unsigned int width, unsigned int height) override;
+ bool Decode(unsigned char * const pixels, unsigned int width, unsigned int height,
+ unsigned int pitch, unsigned int format) override;
+ bool CreateThumbnailFromSurface(unsigned char* bufferin, unsigned int width,
+ unsigned int height, unsigned int format,
+ unsigned int pitch, const std::string& destFile,
+ unsigned char* &bufferout,
+ unsigned int &bufferoutSize) override;
+ void ReleaseThumbnailBuffer() override;
+
+ bool Initialize(unsigned char* buffer, size_t bufSize);
+
+ std::shared_ptr<Frame> ReadFrame();
+
+private:
+ static void FreeIOCtx(AVIOContext** ioctx);
+ AVFrame* ExtractFrame();
+ bool DecodeFrame(AVFrame* m_pFrame, unsigned int width, unsigned int height, unsigned int pitch, unsigned char * const pixels);
+ static int EncodeFFmpegFrame(AVCodecContext *avctx, AVPacket *pkt, int *got_packet, AVFrame *frame);
+ static int DecodeFFmpegFrame(AVCodecContext *avctx, AVFrame *frame, int *got_frame, AVPacket *pkt);
+ static AVPixelFormat ConvertFormats(AVFrame* frame);
+ std::string m_strMimeType;
+ void CleanupLocalOutputBuffer();
+
+
+ MemBuffer m_buf;
+
+ AVIOContext* m_ioctx = nullptr;
+ AVFormatContext* m_fctx = nullptr;
+ AVCodecContext* m_codec_ctx = nullptr;
+
+ AVFrame* m_pFrame;
+ uint8_t* m_outputBuffer;
+};
diff --git a/xbmc/guilib/GUIAction.cpp b/xbmc/guilib/GUIAction.cpp
new file mode 100644
index 0000000..030fdc3
--- /dev/null
+++ b/xbmc/guilib/GUIAction.cpp
@@ -0,0 +1,153 @@
+/*
+ * 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 "GUIAction.h"
+
+#include "GUIComponent.h"
+#include "GUIInfoManager.h"
+#include "GUIWindowManager.h"
+#include "ServiceBroker.h"
+#include "utils/StringUtils.h"
+
+namespace
+{
+constexpr int DEFAULT_CONTROL_ID = 0;
+}
+
+CGUIAction::CExecutableAction::CExecutableAction(const std::string& action) : m_action{action}
+{
+}
+
+CGUIAction::CExecutableAction::CExecutableAction(const std::string& condition,
+ const std::string& action)
+ : m_condition{condition}, m_action{action}
+{
+}
+
+std::string CGUIAction::CExecutableAction::GetCondition() const
+{
+ return m_condition;
+}
+
+std::string CGUIAction::CExecutableAction::GetAction() const
+{
+ return m_action;
+}
+
+bool CGUIAction::CExecutableAction::HasCondition() const
+{
+ return !m_condition.empty();
+}
+
+void CGUIAction::CExecutableAction::SetAction(const std::string& action)
+{
+ m_action = action;
+}
+
+CGUIAction::CGUIAction(int controlID)
+{
+ SetNavigation(controlID);
+}
+
+bool CGUIAction::ExecuteActions() const
+{
+ return ExecuteActions(DEFAULT_CONTROL_ID, DEFAULT_CONTROL_ID);
+}
+
+bool CGUIAction::ExecuteActions(int controlID, int parentID, const CGUIListItemPtr &item /* = NULL */) const
+{
+ if (m_actions.empty())
+ return false;
+
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ // take a copy of actions that satisfy our conditions
+ std::vector<std::string> actions;
+ for (const auto &i : m_actions)
+ {
+ if (!i.HasCondition() || infoMgr.EvaluateBool(i.GetCondition(), 0, item))
+ {
+ if (!StringUtils::IsInteger(i.GetAction()))
+ actions.emplace_back(i.GetAction());
+ }
+ }
+ // execute them
+ bool retval = false;
+ for (const auto &i : actions)
+ {
+ CGUIMessage msg(GUI_MSG_EXECUTE, controlID, parentID, 0, 0, item);
+ msg.SetStringParam(i);
+ if (m_sendThreadMessages)
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ else
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ retval = true;
+ }
+ return retval;
+}
+
+int CGUIAction::GetNavigation() const
+{
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ for (const auto &i : m_actions)
+ {
+ if (StringUtils::IsInteger(i.GetAction()))
+ {
+ if (!i.HasCondition() || infoMgr.EvaluateBool(i.GetCondition(), INFO::DEFAULT_CONTEXT))
+ return std::stoi(i.GetAction());
+ }
+ }
+ return 0;
+}
+
+void CGUIAction::SetNavigation(int id)
+{
+ if (id == 0)
+ return;
+
+ std::string strId = std::to_string(id);
+ for (auto &i : m_actions)
+ {
+ if (StringUtils::IsInteger(i.GetAction()) && !i.HasCondition())
+ {
+ i.SetAction(strId);
+ return;
+ }
+ }
+ m_actions.emplace_back(std::move(strId));
+}
+
+bool CGUIAction::HasActionsMeetingCondition() const
+{
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ for (const auto &i : m_actions)
+ {
+ if (!i.HasCondition() || infoMgr.EvaluateBool(i.GetCondition(), INFO::DEFAULT_CONTEXT))
+ return true;
+ }
+ return false;
+}
+
+bool CGUIAction::HasAnyActions() const
+{
+ return m_actions.size() > 0;
+}
+
+void CGUIAction::Append(const CExecutableAction& action)
+{
+ m_actions.emplace_back(action);
+}
+
+void CGUIAction::EnableSendThreadMessageMode()
+{
+ m_sendThreadMessages = true;
+}
+
+void CGUIAction::Reset()
+{
+ m_actions.clear();
+}
diff --git a/xbmc/guilib/GUIAction.h b/xbmc/guilib/GUIAction.h
new file mode 100644
index 0000000..1c881c5
--- /dev/null
+++ b/xbmc/guilib/GUIAction.h
@@ -0,0 +1,119 @@
+/*
+ * 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 <memory>
+#include <string>
+#include <vector>
+
+class CGUIControl;
+class CGUIListItem; typedef std::shared_ptr<CGUIListItem> CGUIListItemPtr;
+
+/**
+ * Class containing vector of condition->(action/navigation route) and handling its execution.
+ */
+class CGUIAction
+{
+public:
+ /**
+ * Class which defines an executable action
+ */
+ class CExecutableAction
+ {
+ public:
+ /**
+ * Executable action constructor (without conditional execution)
+ * @param action - The action to be executed
+ */
+ explicit CExecutableAction(const std::string& action);
+ /**
+ * Executable action constructor (providing the condition and the action)
+ * @param condition - The condition that dictates the action execution
+ * @param action - The actual action
+ */
+ CExecutableAction(const std::string& condition, const std::string& action);
+
+ /**
+ * Get the condition of this executable action (may be empty)
+ * @return condition - The condition that dictates the action execution (or an empty string)
+ */
+ std::string GetCondition() const;
+
+ /**
+ * Checks if the executable action has any condition
+ * @return true if the executable action has any condition, else false
+ */
+ bool HasCondition() const;
+
+ /**
+ * Get the action string of this executable action
+ * @return action - The action string
+ */
+ std::string GetAction() const;
+
+ /**
+ * Sets/Replaces the action string of this executable action
+ * @param action - The action string
+ */
+ void SetAction(const std::string& action);
+
+ private:
+ /**
+ * Executable action default constructor
+ */
+ CExecutableAction() = delete;
+ /* The condition that dictates the action execution */
+ std::string m_condition;
+ /* The actual action */
+ std::string m_action;
+ };
+
+ CGUIAction() = default;
+ explicit CGUIAction(int controlID);
+ /**
+ * Execute actions without specifying any target control or parent control. Action will use the default focused control.
+ */
+ bool ExecuteActions() const;
+ /**
+ * Execute actions (no navigation paths); if action is paired with condition - evaluate condition first
+ */
+ bool ExecuteActions(int controlID, int parentID, const CGUIListItemPtr& item = nullptr) const;
+ /**
+ * Check if there is any action that meet its condition
+ */
+ bool HasActionsMeetingCondition() const;
+ /**
+ * Check if there is any action
+ */
+ bool HasAnyActions() const;
+ /**
+ * Get navigation route that meet its conditions first
+ */
+ int GetNavigation() const;
+ /**
+ * Set navigation route
+ */
+ void SetNavigation(int id);
+ /**
+ * Configure CGUIAction to send threaded messages
+ */
+ void EnableSendThreadMessageMode();
+ /**
+ * Add an executable action to the CGUIAction container
+ */
+ void Append(const CExecutableAction& action);
+ /**
+ * Prune any executable actions stored in the CGUIAction
+ */
+ void Reset();
+
+private:
+ std::vector<CExecutableAction> m_actions;
+ bool m_sendThreadMessages = false;
+};
diff --git a/xbmc/guilib/GUIAudioManager.cpp b/xbmc/guilib/GUIAudioManager.cpp
new file mode 100644
index 0000000..7f461c4
--- /dev/null
+++ b/xbmc/guilib/GUIAudioManager.cpp
@@ -0,0 +1,399 @@
+/*
+ * 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 "GUIAudioManager.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/Skin.h"
+#include "addons/addoninfo/AddonType.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "filesystem/Directory.h"
+#include "input/Key.h"
+#include "input/WindowTranslator.h"
+#include "input/actions/ActionIDs.h"
+#include "input/actions/ActionTranslator.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+CGUIAudioManager::CGUIAudioManager()
+ : m_settings(CServiceBroker::GetSettingsComponent()->GetSettings())
+{
+ m_bEnabled = false;
+
+ m_settings->RegisterCallback(this, {CSettings::SETTING_LOOKANDFEEL_SOUNDSKIN,
+ CSettings::SETTING_AUDIOOUTPUT_GUISOUNDVOLUME});
+}
+
+CGUIAudioManager::~CGUIAudioManager()
+{
+ m_settings->UnregisterCallback(this);
+}
+
+void CGUIAudioManager::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_LOOKANDFEEL_SOUNDSKIN)
+ {
+ Enable(true);
+ Load();
+ }
+}
+
+bool CGUIAudioManager::OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode)
+{
+ if (setting == NULL)
+ return false;
+
+ if (setting->GetId() == CSettings::SETTING_LOOKANDFEEL_SOUNDSKIN)
+ {
+ //Migrate the old settings
+ if (std::static_pointer_cast<CSettingString>(setting)->GetValue() == "SKINDEFAULT")
+ std::static_pointer_cast<CSettingString>(setting)->Reset();
+ else if (std::static_pointer_cast<CSettingString>(setting)->GetValue() == "OFF")
+ std::static_pointer_cast<CSettingString>(setting)->SetValue("");
+ }
+ if (setting->GetId() == CSettings::SETTING_AUDIOOUTPUT_GUISOUNDVOLUME)
+ {
+ int vol = m_settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_GUISOUNDVOLUME);
+ SetVolume(0.01f * vol);
+ }
+ return true;
+}
+
+
+void CGUIAudioManager::Initialize()
+{
+}
+
+void CGUIAudioManager::DeInitialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+ UnLoad();
+}
+
+void CGUIAudioManager::Stop()
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+ for (const auto& windowSound : m_windowSoundMap)
+ {
+ if (windowSound.second.initSound)
+ windowSound.second.initSound->Stop();
+ if (windowSound.second.deInitSound)
+ windowSound.second.deInitSound->Stop();
+ }
+
+ for (const auto& pythonSound : m_pythonSounds)
+ {
+ pythonSound.second->Stop();
+ }
+}
+
+// \brief Play a sound associated with a CAction
+void CGUIAudioManager::PlayActionSound(const CAction& action)
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+
+ // it's not possible to play gui sounds when passthrough is active
+ if (!m_bEnabled)
+ return;
+
+ const auto it = m_actionSoundMap.find(action.GetID());
+ if (it == m_actionSoundMap.end())
+ return;
+
+ if (it->second)
+ {
+ int vol = m_settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_GUISOUNDVOLUME);
+ it->second->SetVolume(0.01f * vol);
+ it->second->Play();
+ }
+}
+
+// \brief Play a sound associated with a window and its event
+// Events: SOUND_INIT, SOUND_DEINIT
+void CGUIAudioManager::PlayWindowSound(int id, WINDOW_SOUND event)
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+
+ // it's not possible to play gui sounds when passthrough is active
+ if (!m_bEnabled)
+ return;
+
+ const auto it = m_windowSoundMap.find(id);
+ if (it==m_windowSoundMap.end())
+ return;
+
+ std::shared_ptr<IAESound> sound;
+ switch (event)
+ {
+ case SOUND_INIT:
+ sound = it->second.initSound;
+ break;
+ case SOUND_DEINIT:
+ sound = it->second.deInitSound;
+ break;
+ }
+
+ if (!sound)
+ return;
+
+ int vol = m_settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_GUISOUNDVOLUME);
+ sound->SetVolume(0.01f * vol);
+ sound->Play();
+}
+
+// \brief Play a sound given by filename
+void CGUIAudioManager::PlayPythonSound(const std::string& strFileName, bool useCached /*= true*/)
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+
+ // it's not possible to play gui sounds when passthrough is active
+ if (!m_bEnabled)
+ return;
+
+ // If we already loaded the sound, just play it
+ const auto itsb = m_pythonSounds.find(strFileName);
+ if (itsb != m_pythonSounds.end())
+ {
+ const auto& sound = itsb->second;
+ if (useCached)
+ {
+ sound->Play();
+ return;
+ }
+ else
+ {
+ m_pythonSounds.erase(itsb);
+ }
+ }
+
+ auto sound = LoadSound(strFileName);
+ if (!sound)
+ return;
+
+ int vol = m_settings->GetInt(CSettings::SETTING_AUDIOOUTPUT_GUISOUNDVOLUME);
+ sound->SetVolume(0.01f * vol);
+ sound->Play();
+ m_pythonSounds.emplace(strFileName, std::move(sound));
+}
+
+void CGUIAudioManager::UnLoad()
+{
+ m_windowSoundMap.clear();
+ m_pythonSounds.clear();
+ m_actionSoundMap.clear();
+ m_soundCache.clear();
+}
+
+
+std::string GetSoundSkinPath()
+{
+ auto setting = std::static_pointer_cast<CSettingString>(CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(CSettings::SETTING_LOOKANDFEEL_SOUNDSKIN));
+ auto value = setting->GetValue();
+ if (value.empty())
+ return "";
+
+ ADDON::AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(value, addon, ADDON::AddonType::RESOURCE_UISOUNDS,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ CLog::Log(LOGINFO, "Unknown sounds addon '{}'. Setting default sounds.", value);
+ setting->Reset();
+ }
+ return URIUtils::AddFileToFolder("resource://", setting->GetValue());
+}
+
+
+// \brief Load the config file (sounds.xml) for nav sounds
+bool CGUIAudioManager::Load()
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+ UnLoad();
+
+ m_strMediaDir = GetSoundSkinPath();
+ if (m_strMediaDir.empty())
+ return true;
+
+ Enable(true);
+ std::string strSoundsXml = URIUtils::AddFileToFolder(m_strMediaDir, "sounds.xml");
+
+ // Load our xml file
+ CXBMCTinyXML xmlDoc;
+
+ CLog::Log(LOGINFO, "Loading {}", strSoundsXml);
+
+ // Load the config file
+ if (!xmlDoc.LoadFile(strSoundsXml))
+ {
+ CLog::Log(LOGINFO, "{}, Line {}\n{}", strSoundsXml, xmlDoc.ErrorRow(), xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement* pRoot = xmlDoc.RootElement();
+ std::string strValue = pRoot->Value();
+ if ( strValue != "sounds")
+ {
+ CLog::Log(LOGINFO, "{} Doesn't contain <sounds>", strSoundsXml);
+ return false;
+ }
+
+ // Load sounds for actions
+ TiXmlElement* pActions = pRoot->FirstChildElement("actions");
+ if (pActions)
+ {
+ TiXmlNode* pAction = pActions->FirstChild("action");
+
+ while (pAction)
+ {
+ TiXmlNode* pIdNode = pAction->FirstChild("name");
+ unsigned int id = ACTION_NONE; // action identity
+ if (pIdNode && pIdNode->FirstChild())
+ {
+ CActionTranslator::TranslateString(pIdNode->FirstChild()->Value(), id);
+ }
+
+ TiXmlNode* pFileNode = pAction->FirstChild("file");
+ std::string strFile;
+ if (pFileNode && pFileNode->FirstChild())
+ strFile += pFileNode->FirstChild()->Value();
+
+ if (id != ACTION_NONE && !strFile.empty())
+ {
+ std::string filename = URIUtils::AddFileToFolder(m_strMediaDir, strFile);
+ auto sound = LoadSound(filename);
+ if (sound)
+ m_actionSoundMap.emplace(id, std::move(sound));
+ }
+
+ pAction = pAction->NextSibling();
+ }
+ }
+
+ // Load window specific sounds
+ TiXmlElement* pWindows = pRoot->FirstChildElement("windows");
+ if (pWindows)
+ {
+ TiXmlNode* pWindow = pWindows->FirstChild("window");
+
+ while (pWindow)
+ {
+ int id = 0;
+
+ TiXmlNode* pIdNode = pWindow->FirstChild("name");
+ if (pIdNode)
+ {
+ if (pIdNode->FirstChild())
+ id = CWindowTranslator::TranslateWindow(pIdNode->FirstChild()->Value());
+ }
+
+ CWindowSounds sounds;
+ sounds.initSound = LoadWindowSound(pWindow, "activate" );
+ sounds.deInitSound = LoadWindowSound(pWindow, "deactivate");
+
+ if (id > 0)
+ m_windowSoundMap.insert(std::pair<int, CWindowSounds>(id, sounds));
+
+ pWindow = pWindow->NextSibling();
+ }
+ }
+
+ return true;
+}
+
+std::shared_ptr<IAESound> CGUIAudioManager::LoadSound(const std::string& filename)
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+ const auto it = m_soundCache.find(filename);
+ if (it != m_soundCache.end())
+ {
+ auto sound = it->second.lock();
+ if (sound)
+ return sound;
+ else
+ m_soundCache.erase(it); // cleanup orphaned cache entry
+ }
+
+ IAE *ae = CServiceBroker::GetActiveAE();
+ if (!ae)
+ return nullptr;
+
+ std::shared_ptr<IAESound> sound(ae->MakeSound(filename));
+ if (!sound)
+ return nullptr;
+
+ m_soundCache[filename] = sound;
+
+ return sound;
+}
+
+// \brief Load a window node of the config file (sounds.xml)
+std::shared_ptr<IAESound> CGUIAudioManager::LoadWindowSound(TiXmlNode* pWindowNode,
+ const std::string& strIdentifier)
+{
+ if (!pWindowNode)
+ return NULL;
+
+ TiXmlNode* pFileNode = pWindowNode->FirstChild(strIdentifier);
+ if (pFileNode && pFileNode->FirstChild())
+ return LoadSound(URIUtils::AddFileToFolder(m_strMediaDir, pFileNode->FirstChild()->Value()));
+
+ return NULL;
+}
+
+// \brief Enable/Disable nav sounds
+void CGUIAudioManager::Enable(bool bEnable)
+{
+ // always deinit audio when we don't want gui sounds
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SOUNDSKIN).empty())
+ bEnable = false;
+
+ std::unique_lock<CCriticalSection> lock(m_cs);
+ m_bEnabled = bEnable;
+}
+
+// \brief Sets the volume of all playing sounds
+void CGUIAudioManager::SetVolume(float level)
+{
+ std::unique_lock<CCriticalSection> lock(m_cs);
+
+ {
+ for (const auto& actionSound : m_actionSoundMap)
+ {
+ if (actionSound.second)
+ actionSound.second->SetVolume(level);
+ }
+ }
+
+ for (const auto& windowSound : m_windowSoundMap)
+ {
+ if (windowSound.second.initSound)
+ windowSound.second.initSound->SetVolume(level);
+ if (windowSound.second.deInitSound)
+ windowSound.second.deInitSound->SetVolume(level);
+ }
+
+ {
+ for (const auto& pythonSound : m_pythonSounds)
+ {
+ if (pythonSound.second)
+ pythonSound.second->SetVolume(level);
+ }
+ }
+}
diff --git a/xbmc/guilib/GUIAudioManager.h b/xbmc/guilib/GUIAudioManager.h
new file mode 100644
index 0000000..893cbdc
--- /dev/null
+++ b/xbmc/guilib/GUIAudioManager.h
@@ -0,0 +1,89 @@
+/*
+ * 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 "GUIComponent.h"
+#include "cores/AudioEngine/Interfaces/AESound.h"
+#include "settings/lib/ISettingCallback.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+// forward definitions
+class CAction;
+class CSettings;
+class TiXmlNode;
+class IAESound;
+
+enum WINDOW_SOUND { SOUND_INIT = 0, SOUND_DEINIT };
+
+class CGUIAudioManager : public ISettingCallback
+{
+ class CWindowSounds
+ {
+ public:
+ std::shared_ptr<IAESound> initSound;
+ std::shared_ptr<IAESound> deInitSound;
+ };
+
+ struct IAESoundDeleter
+ {
+ void operator()(IAESound* s);
+ };
+
+public:
+ CGUIAudioManager();
+ ~CGUIAudioManager() override;
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ bool OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode) override;
+
+ void Initialize();
+ void DeInitialize();
+
+ bool Load();
+ void UnLoad();
+
+
+ void PlayActionSound(const CAction& action);
+ void PlayWindowSound(int id, WINDOW_SOUND event);
+ void PlayPythonSound(const std::string& strFileName, bool useCached = true);
+
+ void Enable(bool bEnable);
+ void SetVolume(float level);
+ void Stop();
+
+private:
+ // Construction parameters
+ std::shared_ptr<CSettings> m_settings;
+
+ typedef std::map<const std::string, std::weak_ptr<IAESound>> soundCache;
+ typedef std::map<int, std::shared_ptr<IAESound>> actionSoundMap;
+ typedef std::map<int, CWindowSounds> windowSoundMap;
+ typedef std::map<const std::string, std::shared_ptr<IAESound>> pythonSoundsMap;
+
+ soundCache m_soundCache;
+ actionSoundMap m_actionSoundMap;
+ windowSoundMap m_windowSoundMap;
+ pythonSoundsMap m_pythonSounds;
+
+ std::string m_strMediaDir;
+ bool m_bEnabled;
+
+ CCriticalSection m_cs;
+
+ std::shared_ptr<IAESound> LoadSound(const std::string& filename);
+ std::shared_ptr<IAESound> LoadWindowSound(TiXmlNode* pWindowNode,
+ const std::string& strIdentifier);
+};
+
diff --git a/xbmc/guilib/GUIBaseContainer.cpp b/xbmc/guilib/GUIBaseContainer.cpp
new file mode 100644
index 0000000..f8089da
--- /dev/null
+++ b/xbmc/guilib/GUIBaseContainer.cpp
@@ -0,0 +1,1510 @@
+/*
+ * 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 "GUIBaseContainer.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIListItemLayout.h"
+#include "GUIMessage.h"
+#include "ServiceBroker.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/Key.h"
+#include "listproviders/IListProvider.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CharsetConverter.h"
+#include "utils/MathUtils.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#define HOLD_TIME_START 100
+#define HOLD_TIME_END 3000
+#define SCROLLING_GAP 200U
+#define SCROLLING_THRESHOLD 300U
+
+CGUIBaseContainer::CGUIBaseContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems)
+ : IGUIContainer(parentID, controlID, posX, posY, width, height)
+ , m_scroller(scroller)
+{
+ m_cursor = 0;
+ m_offset = 0;
+ m_lastHoldTime = 0;
+ m_itemsPerPage = 10;
+ m_pageControl = 0;
+ m_orientation = orientation;
+ m_analogScrollCount = 0;
+ m_wasReset = false;
+ m_layout = NULL;
+ m_focusedLayout = NULL;
+ m_cacheItems = preloadItems;
+ m_scrollItemsPerFrame = 0.0f;
+ m_type = VIEW_TYPE_NONE;
+ m_autoScrollMoveTime = 0;
+ m_autoScrollDelayTime = 0;
+ m_autoScrollIsReversed = false;
+ m_lastRenderTime = 0;
+}
+
+CGUIBaseContainer::CGUIBaseContainer(const CGUIBaseContainer& other)
+ : IGUIContainer(other),
+ m_renderOffset(other.m_renderOffset),
+ m_analogScrollCount(other.m_analogScrollCount),
+ m_lastHoldTime(other.m_lastHoldTime),
+ m_orientation(other.m_orientation),
+ m_itemsPerPage(other.m_itemsPerPage),
+ m_pageControl(other.m_pageControl),
+ m_layoutCondition(other.m_layoutCondition),
+ m_focusedLayoutCondition(other.m_focusedLayoutCondition),
+ m_scroller(other.m_scroller),
+ m_listProvider(other.m_listProvider ? other.m_listProvider->Clone() : nullptr),
+ m_wasReset(other.m_wasReset),
+ m_letterOffsets(other.m_letterOffsets),
+ m_autoScrollCondition(other.m_autoScrollCondition),
+ m_autoScrollMoveTime(other.m_autoScrollMoveTime),
+ m_autoScrollDelayTime(other.m_autoScrollDelayTime),
+ m_autoScrollIsReversed(other.m_autoScrollIsReversed),
+ m_lastRenderTime(other.m_lastRenderTime),
+ m_cursor(other.m_cursor),
+ m_offset(other.m_offset),
+ m_cacheItems(other.m_cacheItems),
+ m_scrollTimer(other.m_scrollTimer),
+ m_lastScrollStartTimer(other.m_lastScrollStartTimer),
+ m_pageChangeTimer(other.m_pageChangeTimer),
+ m_clickActions(other.m_clickActions),
+ m_focusActions(other.m_focusActions),
+ m_unfocusActions(other.m_unfocusActions),
+ m_matchTimer(other.m_matchTimer),
+ m_match(other.m_match),
+ m_scrollItemsPerFrame(other.m_scrollItemsPerFrame),
+ m_gestureActive(other.m_gestureActive),
+ m_waitForScrollEnd(other.m_waitForScrollEnd),
+ m_lastScrollValue(other.m_lastScrollValue)
+{
+ // Initialize CGUIControl
+ m_bInvalidated = true;
+
+ for (const auto& item : other.m_items)
+ m_items.emplace_back(std::make_shared<CGUIListItem>(*item));
+
+ for (const auto& layout : other.m_layouts)
+ m_layouts.emplace_back(layout, this);
+
+ for (const auto& focusedLayout : other.m_focusedLayouts)
+ m_focusedLayouts.emplace_back(focusedLayout, this);
+}
+
+CGUIBaseContainer::~CGUIBaseContainer(void)
+{
+ // release the container from items
+ for (const auto& item : m_items)
+ item->FreeMemory();
+}
+
+void CGUIBaseContainer::DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ CGUIControl::DoProcess(currentTime, dirtyregions);
+
+ if (m_pageChangeTimer.IsRunning() && m_pageChangeTimer.GetElapsedMilliseconds() > 200)
+ m_pageChangeTimer.Stop();
+ m_wasReset = false;
+
+ // if not visible, we reset the autoscroll timer
+ if (!IsVisible() && m_autoScrollMoveTime)
+ {
+ ResetAutoScrolling();
+ }
+}
+
+void CGUIBaseContainer::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ // update our auto-scrolling as necessary
+ UpdateAutoScrolling(currentTime);
+
+ if (!m_waitForScrollEnd && !m_gestureActive)
+ ValidateOffset();
+
+ if (m_bInvalidated)
+ UpdateLayout();
+
+ if (!m_layout || !m_focusedLayout) return;
+
+ UpdateScrollOffset(currentTime);
+
+ if (m_scroller.IsScrolling())
+ MarkDirtyRegion();
+
+ int offset = (int)floorf(m_scroller.GetValue() / m_layout->Size(m_orientation));
+
+ int cacheBefore, cacheAfter;
+ GetCacheOffsets(cacheBefore, cacheAfter);
+
+ // Free memory not used on screen
+ if ((int)m_items.size() > m_itemsPerPage + cacheBefore + cacheAfter)
+ FreeMemory(CorrectOffset(offset - cacheBefore, 0), CorrectOffset(offset + m_itemsPerPage + 1 + cacheAfter, 0));
+
+ CPoint origin = CPoint(m_posX, m_posY) + m_renderOffset;
+ float pos = (m_orientation == VERTICAL) ? origin.y : origin.x;
+ float end = (m_orientation == VERTICAL) ? m_posY + m_height : m_posX + m_width;
+
+ // we offset our draw position to take into account scrolling and whether or not our focused
+ // item is offscreen "above" the list.
+ float drawOffset = (offset - cacheBefore) * m_layout->Size(m_orientation) - m_scroller.GetValue();
+ if (GetOffset() + GetCursor() < offset)
+ drawOffset += m_focusedLayout->Size(m_orientation) - m_layout->Size(m_orientation);
+ pos += drawOffset;
+ end += cacheAfter * m_layout->Size(m_orientation);
+
+ int current = offset - cacheBefore;
+ while (pos < end && m_items.size())
+ {
+ int itemNo = CorrectOffset(current, 0);
+ if (itemNo >= (int)m_items.size())
+ break;
+ bool focused = (current == GetOffset() + GetCursor());
+ if (itemNo >= 0)
+ {
+ CGUIListItemPtr item = m_items[itemNo];
+ item->SetCurrentItem(itemNo + 1);
+
+ // render our item
+ if (m_orientation == VERTICAL)
+ ProcessItem(origin.x, pos, item, focused, currentTime, dirtyregions);
+ else
+ ProcessItem(pos, origin.y, item, focused, currentTime, dirtyregions);
+ }
+ // increment our position
+ pos += focused ? m_focusedLayout->Size(m_orientation) : m_layout->Size(m_orientation);
+ current++;
+ }
+
+ // when we are scrolling up, offset will become lower (integer division, see offset calc)
+ // to have same behaviour when scrolling down, we need to set page control to offset+1
+ UpdatePageControl(offset + (m_scroller.IsScrollingDown() ? 1 : 0));
+
+ m_lastRenderTime = currentTime;
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIBaseContainer::ProcessItem(float posX, float posY, CGUIListItemPtr& item, bool focused, unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (!m_focusedLayout || !m_layout) return;
+
+ // set the origin
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(posX, posY);
+
+ if (m_bInvalidated)
+ item->SetInvalid();
+ if (focused)
+ {
+ if (!item->GetFocusedLayout())
+ {
+ item->SetFocusedLayout(std::make_unique<CGUIListItemLayout>(*m_focusedLayout, this));
+ }
+ if (item->GetFocusedLayout())
+ {
+ if (item != m_lastItem || !HasFocus())
+ {
+ item->GetFocusedLayout()->SetFocusedItem(0);
+ }
+ if (item != m_lastItem && HasFocus())
+ {
+ item->GetFocusedLayout()->ResetAnimation(ANIM_TYPE_UNFOCUS);
+ unsigned int subItem = 1;
+ if (m_lastItem && m_lastItem->GetFocusedLayout())
+ subItem = m_lastItem->GetFocusedLayout()->GetFocusedItem();
+ item->GetFocusedLayout()->SetFocusedItem(subItem ? subItem : 1);
+ }
+ item->GetFocusedLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions);
+ }
+ m_lastItem = item;
+ }
+ else
+ {
+ if (item->GetFocusedLayout())
+ item->GetFocusedLayout()->SetFocusedItem(0); // focus is not set
+ if (!item->GetLayout())
+ {
+ CGUIListItemLayoutPtr layout = std::make_unique<CGUIListItemLayout>(*m_layout, this);
+ item->SetLayout(std::move(layout));
+ }
+ if (item->GetFocusedLayout())
+ item->GetFocusedLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions);
+ if (item->GetLayout())
+ item->GetLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions);
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
+}
+
+void CGUIBaseContainer::Render()
+{
+ if (!m_layout || !m_focusedLayout) return;
+
+ int offset = (int)floorf(m_scroller.GetValue() / m_layout->Size(m_orientation));
+
+ int cacheBefore, cacheAfter;
+ GetCacheOffsets(cacheBefore, cacheAfter);
+
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height))
+ {
+ CPoint origin = CPoint(m_posX, m_posY) + m_renderOffset;
+ float pos = (m_orientation == VERTICAL) ? origin.y : origin.x;
+ float end = (m_orientation == VERTICAL) ? m_posY + m_height : m_posX + m_width;
+
+ // we offset our draw position to take into account scrolling and whether or not our focused
+ // item is offscreen "above" the list.
+ float drawOffset = (offset - cacheBefore) * m_layout->Size(m_orientation) - m_scroller.GetValue();
+ if (GetOffset() + GetCursor() < offset)
+ drawOffset += m_focusedLayout->Size(m_orientation) - m_layout->Size(m_orientation);
+ pos += drawOffset;
+ end += cacheAfter * m_layout->Size(m_orientation);
+
+ float focusedPos = 0;
+ CGUIListItemPtr focusedItem;
+ int current = offset - cacheBefore;
+ while (pos < end && m_items.size())
+ {
+ int itemNo = CorrectOffset(current, 0);
+ if (itemNo >= (int)m_items.size())
+ break;
+ bool focused = (current == GetOffset() + GetCursor());
+ if (itemNo >= 0)
+ {
+ CGUIListItemPtr item = m_items[itemNo];
+ // render our item
+ if (focused)
+ {
+ focusedPos = pos;
+ focusedItem = item;
+ }
+ else
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(origin.x, pos, item.get(), false);
+ else
+ RenderItem(pos, origin.y, item.get(), false);
+ }
+ }
+ // increment our position
+ pos += focused ? m_focusedLayout->Size(m_orientation) : m_layout->Size(m_orientation);
+ current++;
+ }
+ // render focused item last so it can overlap other items
+ if (focusedItem)
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(origin.x, focusedPos, focusedItem.get(), true);
+ else
+ RenderItem(focusedPos, origin.y, focusedItem.get(), true);
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+
+ CGUIControl::Render();
+}
+
+
+void CGUIBaseContainer::RenderItem(float posX, float posY, CGUIListItem *item, bool focused)
+{
+ if (!m_focusedLayout || !m_layout) return;
+
+ // set the origin
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(posX, posY);
+
+ if (focused)
+ {
+ if (item->GetFocusedLayout())
+ item->GetFocusedLayout()->Render(item, m_parentID);
+ }
+ else
+ {
+ if (item->GetFocusedLayout() && item->GetFocusedLayout()->IsAnimating(ANIM_TYPE_UNFOCUS))
+ item->GetFocusedLayout()->Render(item, m_parentID);
+ else if (item->GetLayout())
+ item->GetLayout()->Render(item, m_parentID);
+ }
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
+}
+
+bool CGUIBaseContainer::OnAction(const CAction &action)
+{
+ if (action.GetID() == KEY_UNICODE)
+ {
+ std::string letter;
+ g_charsetConverter.wToUTF8({action.GetUnicode()}, letter);
+ OnJumpLetter(letter);
+ return true;
+ }
+ // stop the timer on any other action
+ m_matchTimer.Stop();
+
+ switch (action.GetID())
+ {
+ case ACTION_MOVE_LEFT:
+ case ACTION_MOVE_RIGHT:
+ case ACTION_MOVE_DOWN:
+ case ACTION_MOVE_UP:
+ case ACTION_NAV_BACK:
+ case ACTION_PREVIOUS_MENU:
+ {
+ if (!HasFocus()) return false;
+
+ if (action.GetHoldTime() > HOLD_TIME_START &&
+ ((m_orientation == VERTICAL && (action.GetID() == ACTION_MOVE_UP || action.GetID() == ACTION_MOVE_DOWN)) ||
+ (m_orientation == HORIZONTAL && (action.GetID() == ACTION_MOVE_LEFT || action.GetID() == ACTION_MOVE_RIGHT))))
+ { // action is held down - repeat a number of times
+ float speed = std::min(1.0f, (float)(action.GetHoldTime() - HOLD_TIME_START) / (HOLD_TIME_END - HOLD_TIME_START));
+ unsigned int frameDuration = std::min(CTimeUtils::GetFrameTime() - m_lastHoldTime, 50u); // max 20fps
+
+ // maximal scroll rate is at least 30 items per second, and at most (item_rows/7) items per second
+ // i.e. timed to take 7 seconds to traverse the list at full speed.
+ // minimal scroll rate is at least 10 items per second
+ float maxSpeed = std::max(frameDuration * 0.001f * 30, frameDuration * 0.001f * GetRows() / 7);
+ float minSpeed = frameDuration * 0.001f * 10;
+ m_scrollItemsPerFrame += std::max(minSpeed, speed*maxSpeed); // accelerate to max speed
+ m_lastHoldTime = CTimeUtils::GetFrameTime();
+
+ if(m_scrollItemsPerFrame < 1.0f)//not enough hold time accumulated for one step
+ return true;
+
+ while (m_scrollItemsPerFrame >= 1)
+ {
+ if (action.GetID() == ACTION_MOVE_LEFT || action.GetID() == ACTION_MOVE_UP)
+ MoveUp(false);
+ else
+ MoveDown(false);
+ m_scrollItemsPerFrame--;
+ }
+ return true;
+ }
+ else
+ {
+ //if HOLD_TIME_START is reached we need
+ //a sane initial value for calculating m_scrollItemsPerPage
+ m_lastHoldTime = CTimeUtils::GetFrameTime();
+ m_scrollItemsPerFrame = 0.0f;
+ return CGUIControl::OnAction(action);
+ }
+ }
+ case ACTION_CONTEXT_MENU:
+ if (OnContextMenu())
+ return true;
+ break;
+ case ACTION_SHOW_INFO:
+ if (m_listProvider)
+ {
+ const int selected = GetSelectedItem();
+ if (selected >= 0 && selected < static_cast<int>(m_items.size()))
+ {
+ if (m_listProvider->OnInfo(m_items[selected]))
+ return true;
+ }
+ }
+ if (OnInfo())
+ return true;
+ else if (action.GetID())
+ return OnClick(action.GetID());
+
+ return false;
+
+ case ACTION_PLAYER_PLAY:
+ if (m_listProvider)
+ {
+ const int selected = GetSelectedItem();
+ if (selected >= 0 && selected < static_cast<int>(m_items.size()))
+ {
+ if (m_listProvider->OnPlay(m_items[selected]))
+ return true;
+ }
+ }
+ break;
+
+ case ACTION_FIRST_PAGE:
+ SelectItem(0);
+ return true;
+
+ case ACTION_LAST_PAGE:
+ if (m_items.size())
+ SelectItem(m_items.size() - 1);
+ return true;
+
+ case ACTION_NEXT_LETTER:
+ OnNextLetter();
+ return true;
+ case ACTION_PREV_LETTER:
+ OnPrevLetter();
+ return true;
+ case ACTION_JUMP_SMS2:
+ case ACTION_JUMP_SMS3:
+ case ACTION_JUMP_SMS4:
+ case ACTION_JUMP_SMS5:
+ case ACTION_JUMP_SMS6:
+ case ACTION_JUMP_SMS7:
+ case ACTION_JUMP_SMS8:
+ case ACTION_JUMP_SMS9:
+ OnJumpSMS(action.GetID() - ACTION_JUMP_SMS2 + 2);
+ return true;
+
+ default:
+ break;
+ }
+ return action.GetID() && OnClick(action.GetID());
+}
+
+bool CGUIBaseContainer::OnMessage(CGUIMessage& message)
+{
+ if (message.GetControlId() == GetID() )
+ {
+ if (!m_listProvider)
+ {
+ if (message.GetMessage() == GUI_MSG_LABEL_BIND && message.GetPointer())
+ { // bind our items
+ Reset();
+ CFileItemList *items = static_cast<CFileItemList*>(message.GetPointer());
+ for (int i = 0; i < items->Size(); i++)
+ m_items.push_back(items->Get(i));
+ UpdateLayout(true); // true to refresh all items
+ UpdateScrollByLetter();
+ SelectItem(message.GetParam1());
+ return true;
+ }
+ else if (message.GetMessage() == GUI_MSG_LABEL_RESET)
+ {
+ Reset();
+ SetPageControlRange();
+ return true;
+ }
+ }
+ if (message.GetMessage() == GUI_MSG_ITEM_SELECT)
+ {
+ SelectItem(message.GetParam1());
+ return true;
+ }
+ else if (message.GetMessage() == GUI_MSG_SETFOCUS)
+ {
+ if (message.GetParam1()) // subfocus item is specified, so set the offset appropriately
+ {
+ int offset = GetOffset();
+ if (message.GetParam2() && message.GetParam2() == 1)
+ offset = 0;
+ int item = std::min(offset + message.GetParam1() - 1, (int)m_items.size() - 1);
+ SelectItem(item);
+ }
+ }
+ else if (message.GetMessage() == GUI_MSG_ITEM_SELECTED)
+ {
+ message.SetParam1(GetSelectedItem());
+ return true;
+ }
+ else if (message.GetMessage() == GUI_MSG_PAGE_CHANGE)
+ {
+ if (message.GetSenderId() == m_pageControl && IsVisible())
+ { // update our page if we're visible - not much point otherwise
+ if (message.GetParam1() != GetOffset())
+ m_pageChangeTimer.StartZero();
+ ScrollToOffset(message.GetParam1());
+ return true;
+ }
+ }
+ else if (message.GetMessage() == GUI_MSG_REFRESH_LIST)
+ { // update our list contents
+ for (unsigned int i = 0; i < m_items.size(); ++i)
+ m_items[i]->SetInvalid();
+ }
+ else if (message.GetMessage() == GUI_MSG_REFRESH_THUMBS)
+ {
+ if (m_listProvider)
+ m_listProvider->FreeResources(true);
+ }
+ else if (message.GetMessage() == GUI_MSG_MOVE_OFFSET)
+ {
+ int count = message.GetParam1();
+ while (count < 0)
+ {
+ MoveUp(true);
+ count++;
+ }
+ while (count > 0)
+ {
+ MoveDown(true);
+ count--;
+ }
+ return true;
+ }
+ }
+ return CGUIControl::OnMessage(message);
+}
+
+void CGUIBaseContainer::OnUp()
+{
+ CGUIAction action = GetAction(ACTION_MOVE_UP);
+ bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
+ if (m_orientation == VERTICAL && MoveUp(wrapAround))
+ return;
+ // with horizontal lists it doesn't make much sense to have multiselect labels
+ CGUIControl::OnUp();
+}
+
+void CGUIBaseContainer::OnDown()
+{
+ CGUIAction action = GetAction(ACTION_MOVE_DOWN);
+ bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
+ if (m_orientation == VERTICAL && MoveDown(wrapAround))
+ return;
+ // with horizontal lists it doesn't make much sense to have multiselect labels
+ CGUIControl::OnDown();
+}
+
+void CGUIBaseContainer::OnLeft()
+{
+ CGUIAction action = GetAction(ACTION_MOVE_LEFT);
+ bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
+ if (m_orientation == HORIZONTAL && MoveUp(wrapAround))
+ return;
+ else if (m_orientation == VERTICAL)
+ {
+ CGUIListItemLayout *focusedLayout = GetFocusedLayout();
+ if (focusedLayout && focusedLayout->MoveLeft())
+ return;
+ }
+ CGUIControl::OnLeft();
+}
+
+void CGUIBaseContainer::OnRight()
+{
+ CGUIAction action = GetAction(ACTION_MOVE_RIGHT);
+ bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
+ if (m_orientation == HORIZONTAL && MoveDown(wrapAround))
+ return;
+ else if (m_orientation == VERTICAL)
+ {
+ CGUIListItemLayout *focusedLayout = GetFocusedLayout();
+ if (focusedLayout && focusedLayout->MoveRight())
+ return;
+ }
+ CGUIControl::OnRight();
+}
+
+void CGUIBaseContainer::OnNextLetter()
+{
+ int offset = CorrectOffset(GetOffset(), GetCursor());
+ for (unsigned int i = 0; i < m_letterOffsets.size(); i++)
+ {
+ if (m_letterOffsets[i].first > offset)
+ {
+ SelectItem(m_letterOffsets[i].first);
+ return;
+ }
+ }
+}
+
+void CGUIBaseContainer::OnPrevLetter()
+{
+ int offset = CorrectOffset(GetOffset(), GetCursor());
+ if (!m_letterOffsets.size())
+ return;
+ for (int i = (int)m_letterOffsets.size() - 1; i >= 0; i--)
+ {
+ if (m_letterOffsets[i].first < offset)
+ {
+ SelectItem(m_letterOffsets[i].first);
+ return;
+ }
+ }
+}
+
+void CGUIBaseContainer::OnJumpLetter(const std::string& letter, bool skip /*=false*/)
+{
+ if (m_matchTimer.GetElapsedMilliseconds() < letter_match_timeout)
+ m_match += letter;
+ else
+ m_match = letter;
+
+ m_matchTimer.StartZero();
+
+ // we can't jump through letters if we have none
+ if (0 == m_letterOffsets.size())
+ return;
+
+ // find the current letter we're focused on
+ unsigned int offset = CorrectOffset(GetOffset(), GetCursor());
+ unsigned int i = (offset + ((skip) ? 1 : 0)) % m_items.size();
+ do
+ {
+ CGUIListItemPtr item = m_items[i];
+ std::string label = item->GetLabel();
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ label = SortUtils::RemoveArticles(label);
+ if (0 == StringUtils::CompareNoCase(label, m_match, m_match.size()))
+ {
+ SelectItem(i);
+ return;
+ }
+ i = (i+1) % m_items.size();
+ } while (i != offset);
+
+ // no match found - repeat with a single letter
+ std::wstring wmatch;
+ g_charsetConverter.utf8ToW(m_match, wmatch);
+ if (wmatch.length() > 1)
+ {
+ m_match.clear();
+ OnJumpLetter(letter, true);
+ }
+}
+
+void CGUIBaseContainer::OnJumpSMS(int letter)
+{
+ static const char letterMap[8][6] = { "ABC2", "DEF3", "GHI4", "JKL5", "MNO6", "PQRS7", "TUV8", "WXYZ9" };
+
+ // only 2..9 supported
+ if (letter < 2 || letter > 9 || !m_letterOffsets.size())
+ return;
+
+ const std::string letters = letterMap[letter - 2];
+ // find where we currently are
+ int offset = CorrectOffset(GetOffset(), GetCursor());
+ unsigned int currentLetter = 0;
+ while (currentLetter + 1 < m_letterOffsets.size() && m_letterOffsets[currentLetter + 1].first <= offset)
+ currentLetter++;
+
+ // now switch to the next letter
+ std::string current = m_letterOffsets[currentLetter].second;
+ size_t startPos = (letters.find(current) + 1) % letters.size();
+ // now jump to letters[startPos], or another one in the same range if possible
+ size_t pos = startPos;
+ while (true)
+ {
+ // check if we can jump to this letter
+ for (size_t i = 0; i < m_letterOffsets.size(); i++)
+ {
+ if (m_letterOffsets[i].second == letters.substr(pos, 1))
+ {
+ SelectItem(m_letterOffsets[i].first);
+ return;
+ }
+ }
+ pos = (pos + 1) % letters.size();
+ if (pos == startPos)
+ return;
+ }
+}
+
+bool CGUIBaseContainer::MoveUp(bool wrapAround)
+{
+ return true;
+}
+
+bool CGUIBaseContainer::MoveDown(bool wrapAround)
+{
+ return true;
+}
+
+// scrolls the said amount
+void CGUIBaseContainer::Scroll(int amount)
+{
+ ResetAutoScrolling();
+ ScrollToOffset(GetOffset() + amount);
+}
+
+int CGUIBaseContainer::GetSelectedItem() const
+{
+ return CorrectOffset(GetOffset(), GetCursor());
+}
+
+CGUIListItemPtr CGUIBaseContainer::GetListItem(int offset, unsigned int flag) const
+{
+ if (!m_items.size() || !m_layout)
+ return CGUIListItemPtr();
+ int item = GetSelectedItem() + offset;
+ if (flag & INFOFLAG_LISTITEM_POSITION) // use offset from the first item displayed, taking into account scrolling
+ item = CorrectOffset((int)(m_scroller.GetValue() / m_layout->Size(m_orientation)), offset);
+
+ if (flag & INFOFLAG_LISTITEM_ABSOLUTE) // use offset from the first item
+ item = CorrectOffset(0, offset);
+
+ if (flag & INFOFLAG_LISTITEM_WRAP)
+ {
+ item %= ((int)m_items.size());
+ if (item < 0) item += m_items.size();
+ return m_items[item];
+ }
+ else
+ {
+ if (item >= 0 && item < (int)m_items.size())
+ return m_items[item];
+ }
+ return CGUIListItemPtr();
+}
+
+CGUIListItemLayout *CGUIBaseContainer::GetFocusedLayout() const
+{
+ CGUIListItemPtr item = GetListItem(0);
+ if (item.get()) return item->GetFocusedLayout();
+ return NULL;
+}
+
+bool CGUIBaseContainer::OnMouseOver(const CPoint &point)
+{
+ // select the item under the pointer
+ if (!m_waitForScrollEnd)
+ SelectItemFromPoint(point - CPoint(m_posX, m_posY));
+ return CGUIControl::OnMouseOver(point);
+}
+
+EVENT_RESULT CGUIBaseContainer::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_LEFT_CLICK ||
+ event.m_id == ACTION_MOUSE_DOUBLE_CLICK ||
+ event.m_id == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ // Cancel touch
+ m_waitForScrollEnd = false;
+ int select = GetSelectedItem();
+ if (SelectItemFromPoint(point - CPoint(m_posX, m_posY)))
+ {
+ if (event.m_id != ACTION_MOUSE_RIGHT_CLICK || select == GetSelectedItem())
+ OnClick(event.m_id);
+ return EVENT_RESULT_HANDLED;
+ }
+ }
+ else if (event.m_id == ACTION_MOUSE_WHEEL_UP)
+ {
+ Scroll(-1);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_MOUSE_WHEEL_DOWN)
+ {
+ Scroll(1);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_NOTIFY)
+ {
+ m_waitForScrollEnd = true;
+ m_lastScrollValue = m_scroller.GetValue();
+ return (m_orientation == HORIZONTAL) ? EVENT_RESULT_PAN_HORIZONTAL : EVENT_RESULT_PAN_VERTICAL;
+ }
+ else if (event.m_id == ACTION_GESTURE_BEGIN)
+ { // grab exclusive access
+ m_gestureActive = true;
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_PAN)
+ { // do the drag and validate our offset (corrects for end of scroll)
+ m_scroller.SetValue(m_scroller.GetValue() - ((m_orientation == HORIZONTAL) ? event.m_offsetX : event.m_offsetY));
+ float size = (m_layout) ? m_layout->Size(m_orientation) : 10.0f;
+ int offset = MathUtils::round_int(static_cast<double>(m_scroller.GetValue() / size));
+ m_lastScrollStartTimer.Stop();
+ m_scrollTimer.Start();
+ const int absCursor = CorrectOffset(GetOffset(), GetCursor());
+ SetOffset(offset);
+ ValidateOffset();
+ // Notify Application if Inertial scrolling reaches lists end
+ if (m_waitForScrollEnd)
+ {
+ if (fabs(m_scroller.GetValue() - m_lastScrollValue) < 0.001f)
+ {
+ m_waitForScrollEnd = false;
+ return EVENT_RESULT_UNHANDLED;
+ }
+ else
+ m_lastScrollValue = m_scroller.GetValue();
+ }
+ else
+ {
+ CGUIBaseContainer::SetCursor(absCursor - CorrectOffset(GetOffset(), 0));
+ }
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_END || event.m_id == ACTION_GESTURE_ABORT)
+ { // release exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
+ SendWindowMessage(msg);
+ m_scrollTimer.Stop();
+ // and compute the nearest offset from this and scroll there
+ float size = (m_layout) ? m_layout->Size(m_orientation) : 10.0f;
+ float offset = m_scroller.GetValue() / size;
+ int toOffset = MathUtils::round_int(static_cast<double>(offset));
+ if (toOffset < offset)
+ SetOffset(toOffset+1);
+ else
+ SetOffset(toOffset-1);
+ ScrollToOffset(toOffset);
+ ValidateOffset();
+ SetCursor(GetCursor());
+ SetFocus(true);
+ m_waitForScrollEnd = false;
+ m_gestureActive = false;
+ return EVENT_RESULT_HANDLED;
+ }
+ return EVENT_RESULT_UNHANDLED;
+}
+
+bool CGUIBaseContainer::OnClick(int actionID)
+{
+ int subItem = 0;
+ if (actionID == ACTION_SELECT_ITEM || actionID == ACTION_MOUSE_LEFT_CLICK)
+ {
+ if (m_listProvider)
+ { // "select" action
+ int selected = GetSelectedItem();
+ if (selected >= 0 && selected < static_cast<int>(m_items.size()))
+ {
+ if (m_clickActions.HasActionsMeetingCondition())
+ m_clickActions.ExecuteActions(0, GetParentID(), m_items[selected]);
+ else
+ m_listProvider->OnClick(m_items[selected]);
+ }
+ return true;
+ }
+ // grab the currently focused subitem (if applicable)
+ CGUIListItemLayout *focusedLayout = GetFocusedLayout();
+ if (focusedLayout)
+ subItem = focusedLayout->GetFocusedItem();
+ }
+ else if (actionID == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ if (OnContextMenu())
+ return true;
+ }
+ // Don't know what to do, so send to our parent window.
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID(), actionID, subItem);
+ return SendWindowMessage(msg);
+}
+
+bool CGUIBaseContainer::OnContextMenu()
+{
+ if (m_listProvider)
+ {
+ int selected = GetSelectedItem();
+ if (selected >= 0 && selected < static_cast<int>(m_items.size()))
+ {
+ m_listProvider->OnContextMenu(m_items[selected]);
+ return true;
+ }
+ }
+ return false;
+}
+
+std::string CGUIBaseContainer::GetDescription() const
+{
+ std::string strLabel;
+ int item = GetSelectedItem();
+ if (item >= 0 && item < (int)m_items.size())
+ {
+ CGUIListItemPtr pItem = m_items[item];
+ if (pItem->m_bIsFolder)
+ strLabel = StringUtils::Format("[{}]", pItem->GetLabel());
+ else
+ strLabel = pItem->GetLabel();
+ }
+ return strLabel;
+}
+
+void CGUIBaseContainer::SetFocus(bool bOnOff)
+{
+ if (bOnOff != HasFocus())
+ {
+ SetInvalid();
+ m_lastItem.reset();
+ }
+ CGUIControl::SetFocus(bOnOff);
+}
+
+void CGUIBaseContainer::SaveStates(std::vector<CControlState> &states)
+{
+ if (!m_listProvider || !m_listProvider->AlwaysFocusDefaultItem())
+ states.emplace_back(GetID(), GetSelectedItem());
+}
+
+void CGUIBaseContainer::SetPageControl(int id)
+{
+ m_pageControl = id;
+}
+
+bool CGUIBaseContainer::GetOffsetRange(int &minOffset, int &maxOffset) const
+{
+ minOffset = 0;
+ maxOffset = GetRows() - m_itemsPerPage;
+ return true;
+}
+
+void CGUIBaseContainer::ValidateOffset()
+{
+}
+
+void CGUIBaseContainer::AllocResources()
+{
+ CGUIControl::AllocResources();
+ CalculateLayout();
+ if (m_listProvider)
+ {
+ UpdateListProvider(true);
+ }
+}
+
+void CGUIBaseContainer::FreeResources(bool immediately)
+{
+ CGUIControl::FreeResources(immediately);
+ if (m_listProvider)
+ {
+ if (immediately)
+ {
+ Reset();
+ m_listProvider->Reset();
+ }
+ }
+ m_scroller.Stop();
+}
+
+void CGUIBaseContainer::UpdateLayout(bool updateAllItems)
+{
+ if (updateAllItems)
+ { // free memory of items
+ for (iItems it = m_items.begin(); it != m_items.end(); ++it)
+ (*it)->FreeMemory();
+ }
+ // and recalculate the layout
+ CalculateLayout();
+ SetPageControlRange();
+ MarkDirtyRegion();
+}
+
+void CGUIBaseContainer::SetPageControlRange()
+{
+ if (m_pageControl)
+ {
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), m_pageControl, m_itemsPerPage, GetRows());
+ SendWindowMessage(msg);
+ }
+}
+
+void CGUIBaseContainer::UpdatePageControl(int offset)
+{
+ if (m_pageControl)
+ { // tell our pagecontrol (scrollbar or whatever) to update (offset it by our cursor position)
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), m_pageControl, offset);
+ SendWindowMessage(msg);
+ }
+}
+
+void CGUIBaseContainer::UpdateVisibility(const CGUIListItem *item)
+{
+ CGUIControl::UpdateVisibility(item);
+
+ if (!IsVisible() && !CGUIControl::CanFocus())
+ return; // no need to update the content if we're not visible and we can't focus
+
+ // update layouts in case of condition changed
+ if ((m_layout && m_layout->CheckCondition() != m_layoutCondition) ||
+ (m_focusedLayout && m_focusedLayout->CheckCondition() != m_focusedLayoutCondition))
+ {
+ if (m_layout)
+ m_layoutCondition = m_layout->CheckCondition();
+ if (m_focusedLayout)
+ m_focusedLayoutCondition = m_focusedLayout->CheckCondition();
+
+ int itemIndex = GetSelectedItem();
+ UpdateLayout(true); // true to refresh all items
+ SelectItem(itemIndex);
+ }
+
+ UpdateListProvider();
+}
+
+void CGUIBaseContainer::UpdateListProvider(bool forceRefresh /* = false */)
+{
+ if (m_listProvider)
+ {
+ if (m_listProvider->Update(forceRefresh))
+ {
+ // save the current item
+ int currentItem = GetSelectedItem();
+ CGUIListItem *current = (currentItem >= 0 && currentItem < (int)m_items.size()) ? m_items[currentItem].get() : NULL;
+ const std::string prevSelectedPath((current && current->IsFileItem()) ? static_cast<CFileItem *>(current)->GetPath() : "");
+
+ Reset();
+ m_listProvider->Fetch(m_items);
+ SetPageControlRange();
+ // update the newly selected item
+ bool found = false;
+
+ // first, try to re-identify selected item by comparing item pointers, though it is not guaranteed that item instances got not recreated on update.
+ for (int i = 0; i < (int)m_items.size(); i++)
+ {
+ if (m_items[i].get() == current)
+ {
+ found = true;
+ if (i != currentItem)
+ {
+ SelectItem(i);
+ break;
+ }
+ }
+ }
+ if (!found && !prevSelectedPath.empty())
+ {
+ // as fallback, try to re-identify selected item by comparing item paths.
+ for (int i = 0; i < static_cast<int>(m_items.size()); i++)
+ {
+ const CGUIListItemPtr c(m_items[i]);
+ if (c->IsFileItem())
+ {
+ const std::string &selectedPath = static_cast<CFileItem *>(c.get())->GetPath();
+ if (selectedPath == prevSelectedPath)
+ {
+ found = true;
+ if (i != currentItem)
+ {
+ SelectItem(i);
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (!found && currentItem >= (int)m_items.size())
+ SelectItem(m_items.size()-1);
+ SetInvalid();
+ }
+ // always update the scroll by letter, as the list provider may have altered labels
+ // while not actually changing the list items.
+ UpdateScrollByLetter();
+ }
+}
+
+void CGUIBaseContainer::CalculateLayout()
+{
+ CGUIListItemLayout *oldFocusedLayout = m_focusedLayout;
+ CGUIListItemLayout *oldLayout = m_layout;
+ GetCurrentLayouts();
+
+ // calculate the number of items to display
+ if (!m_focusedLayout || !m_layout)
+ return;
+
+ if (oldLayout == m_layout && oldFocusedLayout == m_focusedLayout)
+ return; // nothing has changed, so don't update stuff
+
+ m_itemsPerPage = std::max((int)((Size() - m_focusedLayout->Size(m_orientation)) / m_layout->Size(m_orientation)) + 1, 1);
+
+ // ensure that the scroll offset is a multiple of our size
+ m_scroller.SetValue(GetOffset() * m_layout->Size(m_orientation));
+}
+
+void CGUIBaseContainer::UpdateScrollByLetter()
+{
+ m_letterOffsets.clear();
+
+ // for scrolling by letter we have an offset table into our vector.
+ std::string currentMatch;
+ for (unsigned int i = 0; i < m_items.size(); i++)
+ {
+ CGUIListItemPtr item = m_items[i];
+ // The letter offset jumping is only for ASCII characters at present, and
+ // our checks are all done in uppercase
+ std::string nextLetter;
+ std::wstring character = item->GetSortLabel().substr(0, 1);
+ StringUtils::ToUpper(character);
+ g_charsetConverter.wToUTF8(character, nextLetter);
+ if (currentMatch != nextLetter)
+ {
+ currentMatch = nextLetter;
+ m_letterOffsets.emplace_back(static_cast<int>(i), currentMatch);
+ }
+ }
+}
+
+unsigned int CGUIBaseContainer::GetRows() const
+{
+ return m_items.size();
+}
+
+inline float CGUIBaseContainer::Size() const
+{
+ return (m_orientation == HORIZONTAL) ? m_width : m_height;
+}
+
+int CGUIBaseContainer::ScrollCorrectionRange() const
+{
+ int range = m_itemsPerPage / 4;
+ if (range <= 0) range = 1;
+ return range;
+}
+
+void CGUIBaseContainer::ScrollToOffset(int offset)
+{
+ int minOffset, maxOffset;
+ if(GetOffsetRange(minOffset, maxOffset))
+ offset = std::max(minOffset, std::min(offset, maxOffset));
+ float size = (m_layout) ? m_layout->Size(m_orientation) : 10.0f;
+ int range = ScrollCorrectionRange();
+ if (offset * size < m_scroller.GetValue() && m_scroller.GetValue() - offset * size > size * range)
+ { // scrolling up, and we're jumping more than 0.5 of a screen
+ m_scroller.SetValue((offset + range) * size);
+ }
+ if (offset * size > m_scroller.GetValue() && offset * size - m_scroller.GetValue() > size * range)
+ { // scrolling down, and we're jumping more than 0.5 of a screen
+ m_scroller.SetValue((offset - range) * size);
+ }
+ m_scroller.ScrollTo(offset * size);
+ m_lastScrollStartTimer.StartZero();
+ if (!m_wasReset)
+ {
+ SetContainerMoving(offset - GetOffset());
+ if (m_scroller.IsScrolling())
+ m_scrollTimer.Start();
+ else
+ m_scrollTimer.Stop();
+ }
+ else
+ {
+ m_scrollTimer.Stop();
+ m_scroller.Update(~0U);
+ }
+ SetOffset(offset);
+}
+
+void CGUIBaseContainer::SetAutoScrolling(const TiXmlNode *node)
+{
+ if (!node) return;
+ const TiXmlElement *scroll = node->FirstChildElement("autoscroll");
+ if (scroll)
+ {
+ scroll->Attribute("time", &m_autoScrollMoveTime);
+ if (scroll->Attribute("reverse"))
+ m_autoScrollIsReversed = true;
+ if (scroll->FirstChild())
+ m_autoScrollCondition = CServiceBroker::GetGUI()->GetInfoManager().Register(scroll->FirstChild()->ValueStr(), GetParentID());
+ }
+}
+
+void CGUIBaseContainer::ResetAutoScrolling()
+{
+ m_autoScrollDelayTime = 0;
+}
+
+void CGUIBaseContainer::UpdateAutoScrolling(unsigned int currentTime)
+{
+ if (m_autoScrollCondition && m_autoScrollCondition->Get(INFO::DEFAULT_CONTEXT))
+ {
+ if (m_lastRenderTime)
+ m_autoScrollDelayTime += currentTime - m_lastRenderTime;
+ if (m_autoScrollDelayTime > (unsigned int)m_autoScrollMoveTime && !m_scroller.IsScrolling())
+ { // delay is finished - start moving
+ m_autoScrollDelayTime = 0;
+ // Move up or down whether reversed moving is true or false
+ m_autoScrollIsReversed ? MoveUp(true) : MoveDown(true);
+ }
+ }
+ else
+ ResetAutoScrolling();
+}
+
+void CGUIBaseContainer::SetContainerMoving(int direction)
+{
+ if (direction)
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetGUIControlsInfoProvider().SetContainerMoving(GetID(), direction > 0, m_scroller.IsScrolling());
+}
+
+void CGUIBaseContainer::UpdateScrollOffset(unsigned int currentTime)
+{
+ if (m_scroller.Update(currentTime))
+ MarkDirtyRegion();
+ else if (m_lastScrollStartTimer.IsRunning() && m_lastScrollStartTimer.GetElapsedMilliseconds() >= SCROLLING_GAP)
+ {
+ m_scrollTimer.Stop();
+ m_lastScrollStartTimer.Stop();
+ SetCursor(GetCursor());
+ }
+}
+
+int CGUIBaseContainer::CorrectOffset(int offset, int cursor) const
+{
+ return offset + cursor;
+}
+
+void CGUIBaseContainer::Reset()
+{
+ m_wasReset = true;
+ m_items.clear();
+ m_lastItem.reset();
+ ResetAutoScrolling();
+}
+
+void CGUIBaseContainer::LoadLayout(TiXmlElement *layout)
+{
+ TiXmlElement *itemElement = layout->FirstChildElement("itemlayout");
+ while (itemElement)
+ { // we have a new item layout
+ m_layouts.emplace_back();
+ m_layouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("itemlayout");
+ m_layouts.back().SetParentControl(this);
+ }
+ itemElement = layout->FirstChildElement("focusedlayout");
+ while (itemElement)
+ { // we have a new item layout
+ m_focusedLayouts.emplace_back();
+ m_focusedLayouts.back().LoadLayout(itemElement, GetParentID(), true, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("focusedlayout");
+ m_focusedLayouts.back().SetParentControl(this);
+ }
+}
+
+void CGUIBaseContainer::LoadListProvider(TiXmlElement *content, int defaultItem, bool defaultAlways)
+{
+ m_listProvider = IListProvider::Create(content, GetParentID());
+ if (m_listProvider)
+ m_listProvider->SetDefaultItem(defaultItem, defaultAlways);
+}
+
+void CGUIBaseContainer::SetListProvider(std::unique_ptr<IListProvider> provider)
+{
+ m_listProvider = std::move(provider);
+ UpdateListProvider(true);
+}
+
+void CGUIBaseContainer::SetRenderOffset(const CPoint &offset)
+{
+ m_renderOffset = offset;
+}
+
+void CGUIBaseContainer::FreeMemory(int keepStart, int keepEnd)
+{
+ if (keepStart < keepEnd)
+ { // remove before keepStart and after keepEnd
+ for (int i = 0; i < keepStart && i < (int)m_items.size(); ++i)
+ m_items[i]->FreeMemory();
+ for (int i = std::max(keepEnd + 1, 0); i < (int)m_items.size(); ++i)
+ m_items[i]->FreeMemory();
+ }
+ else
+ { // wrapping
+ for (int i = std::max(keepEnd + 1, 0); i < keepStart && i < (int)m_items.size(); ++i)
+ m_items[i]->FreeMemory();
+ }
+}
+
+bool CGUIBaseContainer::InsideLayout(const CGUIListItemLayout *layout, const CPoint &point) const
+{
+ if (!layout) return false;
+ if ((m_orientation == VERTICAL && (layout->Size(HORIZONTAL) > 1) && point.x > layout->Size(HORIZONTAL)) ||
+ (m_orientation == HORIZONTAL && (layout->Size(VERTICAL) > 1)&& point.y > layout->Size(VERTICAL)))
+ return false;
+ return true;
+}
+
+#ifdef _DEBUG
+void CGUIBaseContainer::DumpTextureUse()
+{
+ CLog::Log(LOGDEBUG, "{} for container {}", __FUNCTION__, GetID());
+ for (unsigned int i = 0; i < m_items.size(); ++i)
+ {
+ CGUIListItemPtr item = m_items[i];
+ if (item->GetFocusedLayout()) item->GetFocusedLayout()->DumpTextureUse();
+ if (item->GetLayout()) item->GetLayout()->DumpTextureUse();
+ }
+}
+#endif
+
+bool CGUIBaseContainer::GetCondition(int condition, int data) const
+{
+ switch (condition)
+ {
+ case CONTAINER_ROW:
+ return (m_orientation == VERTICAL) ? (GetCursor() == data) : true;
+ case CONTAINER_COLUMN:
+ return (m_orientation == HORIZONTAL) ? (GetCursor() == data) : true;
+ case CONTAINER_POSITION:
+ return (GetCursor() == data);
+ case CONTAINER_HAS_NEXT:
+ return (HasNextPage());
+ case CONTAINER_HAS_PREVIOUS:
+ return (HasPreviousPage());
+ case CONTAINER_HAS_PARENT_ITEM:
+ return (m_items.size() && m_items[0]->IsFileItem() && (std::static_pointer_cast<CFileItem>(m_items[0]))->IsParentFolder());
+ case CONTAINER_SUBITEM:
+ {
+ CGUIListItemLayout *layout = GetFocusedLayout();
+ return layout ? (layout->GetFocusedItem() == (unsigned int)data) : false;
+ }
+ case CONTAINER_SCROLLING:
+ return ((m_scrollTimer.IsRunning() && m_scrollTimer.GetElapsedMilliseconds() > std::max(m_scroller.GetDuration(), SCROLLING_THRESHOLD)) || m_pageChangeTimer.IsRunning());
+ case CONTAINER_ISUPDATING:
+ return (m_listProvider) ? m_listProvider->IsUpdating() : false;
+ default:
+ return false;
+ }
+}
+
+void CGUIBaseContainer::GetCurrentLayouts()
+{
+ m_layout = NULL;
+ for (auto &layout : m_layouts)
+ {
+ if (layout.CheckCondition())
+ {
+ m_layout = &layout;
+ break;
+ }
+ }
+ if (!m_layout && !m_layouts.empty())
+ m_layout = &m_layouts.front(); // failsafe
+
+ m_focusedLayout = NULL;
+ for (auto &layout : m_focusedLayouts)
+ {
+ if (layout.CheckCondition())
+ {
+ m_focusedLayout = &layout;
+ break;
+ }
+ }
+ if (!m_focusedLayout && !m_focusedLayouts.empty())
+ m_focusedLayout = &m_focusedLayouts.front(); // failsafe
+}
+
+bool CGUIBaseContainer::HasNextPage() const
+{
+ return false;
+}
+
+bool CGUIBaseContainer::HasPreviousPage() const
+{
+ return false;
+}
+
+std::string CGUIBaseContainer::GetLabel(int info) const
+{
+ std::string label;
+ switch (info)
+ {
+ case CONTAINER_NUM_PAGES:
+ label = std::to_string((GetRows() + m_itemsPerPage - 1) / m_itemsPerPage);
+ break;
+ case CONTAINER_CURRENT_PAGE:
+ label = std::to_string(GetCurrentPage());
+ break;
+ case CONTAINER_POSITION:
+ label = std::to_string(GetCursor());
+ break;
+ case CONTAINER_CURRENT_ITEM:
+ {
+ if (m_items.size() && m_items[0]->IsFileItem() && (std::static_pointer_cast<CFileItem>(m_items[0]))->IsParentFolder())
+ label = std::to_string(GetSelectedItem());
+ else
+ label = std::to_string(GetSelectedItem() + 1);
+ }
+ break;
+ case CONTAINER_NUM_ALL_ITEMS:
+ case CONTAINER_NUM_ITEMS:
+ {
+ unsigned int numItems = GetNumItems();
+ if (info == CONTAINER_NUM_ITEMS && numItems && m_items[0]->IsFileItem() && (std::static_pointer_cast<CFileItem>(m_items[0]))->IsParentFolder())
+ label = std::to_string(numItems - 1);
+ else
+ label = std::to_string(numItems);
+ }
+ break;
+ case CONTAINER_NUM_NONFOLDER_ITEMS:
+ {
+ int numItems = 0;
+ for (const auto& item : m_items)
+ {
+ if (!item->m_bIsFolder)
+ numItems++;
+ }
+ label = std::to_string(numItems);
+ }
+ break;
+ default:
+ break;
+ }
+ return label;
+}
+
+int CGUIBaseContainer::GetCurrentPage() const
+{
+ if (GetOffset() + m_itemsPerPage >= (int)GetRows()) // last page
+ return (GetRows() + m_itemsPerPage - 1) / m_itemsPerPage;
+ return GetOffset() / m_itemsPerPage + 1;
+}
+
+void CGUIBaseContainer::GetCacheOffsets(int &cacheBefore, int &cacheAfter) const
+{
+ if (m_scroller.IsScrollingDown())
+ {
+ cacheBefore = 0;
+ cacheAfter = m_cacheItems;
+ }
+ else if (m_scroller.IsScrollingUp())
+ {
+ cacheBefore = m_cacheItems;
+ cacheAfter = 0;
+ }
+ else
+ {
+ cacheBefore = m_cacheItems / 2;
+ cacheAfter = m_cacheItems / 2;
+ }
+}
+
+void CGUIBaseContainer::SetCursor(int cursor)
+{
+ if (m_cursor != cursor)
+ MarkDirtyRegion();
+ m_cursor = cursor;
+}
+
+void CGUIBaseContainer::SetOffset(int offset)
+{
+ if (m_offset != offset)
+ MarkDirtyRegion();
+ m_offset = offset;
+}
+
+bool CGUIBaseContainer::CanFocus() const
+{
+ if (CGUIControl::CanFocus())
+ {
+ /*
+ We allow focus if we have items available or if we have a list provider
+ that's in the process of updating.
+ */
+ return !m_items.empty() || (m_listProvider && m_listProvider->IsUpdating());
+ }
+ return false;
+}
+
+void CGUIBaseContainer::OnFocus()
+{
+ if (m_listProvider && m_listProvider->AlwaysFocusDefaultItem())
+ SelectItem(m_listProvider->GetDefaultItem());
+
+ if (m_focusActions.HasAnyActions())
+ m_focusActions.ExecuteActions(GetID(), GetParentID());
+
+ CGUIControl::OnFocus();
+}
+
+void CGUIBaseContainer::OnUnFocus()
+{
+ if (m_unfocusActions.HasAnyActions())
+ m_unfocusActions.ExecuteActions(GetID(), GetParentID());
+
+ CGUIControl::OnUnFocus();
+}
diff --git a/xbmc/guilib/GUIBaseContainer.h b/xbmc/guilib/GUIBaseContainer.h
new file mode 100644
index 0000000..2ca35f0
--- /dev/null
+++ b/xbmc/guilib/GUIBaseContainer.h
@@ -0,0 +1,238 @@
+/*
+ * 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
+
+/*!
+\file GUIListContainer.h
+\brief
+*/
+
+#include "GUIAction.h"
+#include "IGUIContainer.h"
+#include "utils/Stopwatch.h"
+
+#include <list>
+#include <memory>
+#include <utility>
+#include <vector>
+
+/*!
+ \ingroup controls
+ \brief
+ */
+
+class IListProvider;
+class TiXmlNode;
+class CGUIListItemLayout;
+
+class CGUIBaseContainer : public IGUIContainer
+{
+public:
+ CGUIBaseContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems);
+ explicit CGUIBaseContainer(const CGUIBaseContainer& other);
+ ~CGUIBaseContainer(void) override;
+
+ bool OnAction(const CAction &action) override;
+ void OnDown() override;
+ void OnUp() override;
+ void OnLeft() override;
+ void OnRight() override;
+ bool OnMouseOver(const CPoint &point) override;
+ bool CanFocus() const override;
+ bool OnMessage(CGUIMessage& message) override;
+ void SetFocus(bool bOnOff) override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void UpdateVisibility(const CGUIListItem *item = NULL) override;
+
+ virtual unsigned int GetRows() const;
+
+ virtual bool HasNextPage() const;
+ virtual bool HasPreviousPage() const;
+
+ void SetPageControl(int id);
+
+ std::string GetDescription() const override;
+ void SaveStates(std::vector<CControlState> &states) override;
+ virtual int GetSelectedItem() const;
+
+ void DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+
+ void LoadLayout(TiXmlElement *layout);
+ void LoadListProvider(TiXmlElement *content, int defaultItem, bool defaultAlways);
+
+ CGUIListItemPtr GetListItem(int offset, unsigned int flag = 0) const override;
+
+ bool GetCondition(int condition, int data) const override;
+ std::string GetLabel(int info) const override;
+
+ /*! \brief Set the list provider for this container (for python).
+ \param provider the list provider to use for this container.
+ */
+ void SetListProvider(std::unique_ptr<IListProvider> provider);
+
+ /*! \brief Set the offset of the first item in the container from the container's position
+ Useful for lists/panels where the focused item may be larger than the non-focused items and thus
+ normally cut off from the clipping window defined by the container's position + size.
+ \param offset CPoint holding the offset in skin coordinates.
+ */
+ void SetRenderOffset(const CPoint &offset);
+
+ void SetClickActions(const CGUIAction& clickActions) { m_clickActions = clickActions; }
+ void SetFocusActions(const CGUIAction& focusActions) { m_focusActions = focusActions; }
+ void SetUnFocusActions(const CGUIAction& unfocusActions) { m_unfocusActions = unfocusActions; }
+
+ void SetAutoScrolling(const TiXmlNode *node);
+ void ResetAutoScrolling();
+ void UpdateAutoScrolling(unsigned int currentTime);
+
+#ifdef _DEBUG
+ void DumpTextureUse() override;
+#endif
+protected:
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ bool OnClick(int actionID);
+
+ virtual void ProcessItem(float posX, float posY, CGUIListItemPtr& item, bool focused, unsigned int currentTime, CDirtyRegionList &dirtyregions);
+
+ void Render() override;
+ virtual void RenderItem(float posX, float posY, CGUIListItem *item, bool focused);
+ virtual void Scroll(int amount);
+ virtual bool MoveDown(bool wrapAround);
+ virtual bool MoveUp(bool wrapAround);
+ virtual bool GetOffsetRange(int &minOffset, int &maxOffset) const;
+ virtual void ValidateOffset();
+ virtual int CorrectOffset(int offset, int cursor) const;
+ virtual void UpdateLayout(bool refreshAllItems = false);
+ virtual void SetPageControlRange();
+ virtual void UpdatePageControl(int offset);
+ virtual void CalculateLayout();
+ virtual void SelectItem(int item) {}
+ virtual bool SelectItemFromPoint(const CPoint& point) { return false; }
+ virtual int GetCursorFromPoint(const CPoint& point, CPoint* itemPoint = NULL) const { return -1; }
+ virtual void Reset();
+ virtual size_t GetNumItems() const { return m_items.size(); }
+ virtual int GetCurrentPage() const;
+ bool InsideLayout(const CGUIListItemLayout *layout, const CPoint &point) const;
+ void OnFocus() override;
+ void OnUnFocus() override;
+ void UpdateListProvider(bool forceRefresh = false);
+
+ int ScrollCorrectionRange() const;
+ inline float Size() const;
+ void FreeMemory(int keepStart, int keepEnd);
+ void GetCurrentLayouts();
+ CGUIListItemLayout *GetFocusedLayout() const;
+
+ CPoint m_renderOffset; ///< \brief render offset of the first item in the list \sa SetRenderOffset
+
+ float m_analogScrollCount;
+ unsigned int m_lastHoldTime;
+
+ ORIENTATION m_orientation;
+ int m_itemsPerPage;
+
+ std::vector< CGUIListItemPtr > m_items;
+ typedef std::vector<CGUIListItemPtr> ::iterator iItems;
+ CGUIListItemPtr m_lastItem;
+
+ int m_pageControl;
+
+ std::list<CGUIListItemLayout> m_layouts;
+ std::list<CGUIListItemLayout> m_focusedLayouts;
+
+ CGUIListItemLayout* m_layout{nullptr};
+ CGUIListItemLayout* m_focusedLayout{nullptr};
+ bool m_layoutCondition = false;
+ bool m_focusedLayoutCondition = false;
+
+ void ScrollToOffset(int offset);
+ void SetContainerMoving(int direction);
+ void UpdateScrollOffset(unsigned int currentTime);
+
+ CScroller m_scroller;
+
+ std::unique_ptr<IListProvider> m_listProvider;
+
+ bool m_wasReset; // true if we've received a Reset message until we've rendered once. Allows
+ // us to make sure we don't tell the infomanager that we've been moving when
+ // the "movement" was simply due to the list being repopulated (thus cursor position
+ // changing around)
+
+ void UpdateScrollByLetter();
+ void GetCacheOffsets(int &cacheBefore, int &cacheAfter) const;
+ int GetCacheCount() const { return m_cacheItems; }
+ bool ScrollingDown() const { return m_scroller.IsScrollingDown(); }
+ bool ScrollingUp() const { return m_scroller.IsScrollingUp(); }
+ void OnNextLetter();
+ void OnPrevLetter();
+ void OnJumpLetter(const std::string& letter, bool skip = false);
+ void OnJumpSMS(int letter);
+ std::vector< std::pair<int, std::string> > m_letterOffsets;
+
+ /*! \brief Set the cursor position
+ Should be used by all base classes rather than directly setting it, as
+ this also marks the control as dirty (if needed)
+ */
+ virtual void SetCursor(int cursor);
+ inline int GetCursor() const { return m_cursor; }
+
+ /*! \brief Set the container offset
+ Should be used by all base classes rather than directly setting it, as
+ this also marks the control as dirty (if needed)
+ */
+ void SetOffset(int offset);
+ /*! \brief Returns the index of the first visible row
+ returns the first row. This may be outside of the range of available items. Use GetItemOffset() to retrieve the first visible item in the list.
+ \sa GetItemOffset
+ */
+ inline int GetOffset() const { return m_offset; }
+ /*! \brief Returns the index of the first visible item
+ returns the first visible item. This will always be in the range of available items. Use GetOffset() to retrieve the first visible row in the list.
+ \sa GetOffset
+ */
+ inline int GetItemOffset() const { return CorrectOffset(GetOffset(), 0); }
+
+ // autoscrolling
+ INFO::InfoPtr m_autoScrollCondition;
+ int m_autoScrollMoveTime; // time between to moves
+ unsigned int m_autoScrollDelayTime; // current offset into the delay
+ bool m_autoScrollIsReversed; // scroll backwards
+
+ unsigned int m_lastRenderTime;
+
+private:
+ bool OnContextMenu();
+
+ int m_cursor;
+ int m_offset;
+ int m_cacheItems;
+ CStopWatch m_scrollTimer;
+ CStopWatch m_lastScrollStartTimer;
+ CStopWatch m_pageChangeTimer;
+
+ CGUIAction m_clickActions;
+ CGUIAction m_focusActions;
+ CGUIAction m_unfocusActions;
+
+ // letter match searching
+ CStopWatch m_matchTimer;
+ std::string m_match;
+ float m_scrollItemsPerFrame;
+ static const int letter_match_timeout = 1000;
+
+ bool m_gestureActive = false;
+
+ // early inertial scroll cancellation
+ bool m_waitForScrollEnd = false;
+ float m_lastScrollValue = 0.0f;
+};
+
+
diff --git a/xbmc/guilib/GUIBorderedImage.cpp b/xbmc/guilib/GUIBorderedImage.cpp
new file mode 100644
index 0000000..7c48ddb
--- /dev/null
+++ b/xbmc/guilib/GUIBorderedImage.cpp
@@ -0,0 +1,86 @@
+/*
+ * 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 "GUIBorderedImage.h"
+
+CGUIBorderedImage::CGUIBorderedImage(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& texture,
+ const CTextureInfo& borderTexture,
+ const CRect& borderSize)
+ : CGUIImage(parentID,
+ controlID,
+ posX + borderSize.x1,
+ posY + borderSize.y1,
+ width - borderSize.x1 - borderSize.x2,
+ height - borderSize.y1 - borderSize.y2,
+ texture),
+ m_borderImage(CGUITexture::CreateTexture(posX, posY, width, height, borderTexture)),
+ m_borderSize(borderSize)
+{
+ ControlType = GUICONTROL_BORDEREDIMAGE;
+}
+
+CGUIBorderedImage::CGUIBorderedImage(const CGUIBorderedImage& right)
+ : CGUIImage(right), m_borderImage(right.m_borderImage->Clone()), m_borderSize(right.m_borderSize)
+{
+ ControlType = GUICONTROL_BORDEREDIMAGE;
+}
+
+void CGUIBorderedImage::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ CGUIImage::Process(currentTime, dirtyregions);
+ if (!m_borderImage->GetFileName().empty() && m_texture->ReadyToRender())
+ {
+ CRect rect = CRect(m_texture->GetXPosition(), m_texture->GetYPosition(),
+ m_texture->GetXPosition() + m_texture->GetWidth(),
+ m_texture->GetYPosition() + m_texture->GetHeight());
+ rect.Intersect(m_texture->GetRenderRect());
+ m_borderImage->SetPosition(rect.x1 - m_borderSize.x1, rect.y1 - m_borderSize.y1);
+ m_borderImage->SetWidth(rect.Width() + m_borderSize.x1 + m_borderSize.x2);
+ m_borderImage->SetHeight(rect.Height() + m_borderSize.y1 + m_borderSize.y2);
+ m_borderImage->SetDiffuseColor(m_diffuseColor);
+ if (m_borderImage->Process(currentTime))
+ MarkDirtyRegion();
+ }
+}
+
+void CGUIBorderedImage::Render()
+{
+ if (!m_borderImage->GetFileName().empty() && m_texture->ReadyToRender())
+ m_borderImage->Render();
+ CGUIImage::Render();
+}
+
+CRect CGUIBorderedImage::CalcRenderRegion() const
+{
+ // have to union the image as well as fading images may still exist that are bigger than our current border image
+ return CGUIImage::CalcRenderRegion().Union(m_borderImage->GetRenderRect());
+}
+
+void CGUIBorderedImage::AllocResources()
+{
+ m_borderImage->AllocResources();
+ CGUIImage::AllocResources();
+}
+
+void CGUIBorderedImage::FreeResources(bool immediately)
+{
+ m_borderImage->FreeResources(immediately);
+ CGUIImage::FreeResources(immediately);
+}
+
+void CGUIBorderedImage::DynamicResourceAlloc(bool bOnOff)
+{
+ m_borderImage->DynamicResourceAlloc(bOnOff);
+ CGUIImage::DynamicResourceAlloc(bOnOff);
+}
diff --git a/xbmc/guilib/GUIBorderedImage.h b/xbmc/guilib/GUIBorderedImage.h
new file mode 100644
index 0000000..cbe2955
--- /dev/null
+++ b/xbmc/guilib/GUIBorderedImage.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "GUIControl.h"
+#include "GUIImage.h"
+#include "TextureManager.h"
+
+class CGUIBorderedImage : public CGUIImage
+{
+public:
+ CGUIBorderedImage(int parentID, int controlID, float posX, float posY, float width, float height, const CTextureInfo& texture, const CTextureInfo& borderTexture, const CRect &borderSize);
+ ~CGUIBorderedImage(void) override = default;
+ CGUIBorderedImage* Clone() const override { return new CGUIBorderedImage(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+
+ CRect CalcRenderRegion() const override;
+
+protected:
+ std::unique_ptr<CGUITexture> m_borderImage;
+ CRect m_borderSize;
+
+private:
+ CGUIBorderedImage(const CGUIBorderedImage& right);
+};
+
diff --git a/xbmc/guilib/GUIButtonControl.cpp b/xbmc/guilib/GUIButtonControl.cpp
new file mode 100644
index 0000000..0e7f757
--- /dev/null
+++ b/xbmc/guilib/GUIButtonControl.cpp
@@ -0,0 +1,409 @@
+/*
+ * 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 "GUIButtonControl.h"
+
+#include "GUIFontManager.h"
+#include "input/Key.h"
+
+CGUIButtonControl::CGUIButtonControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& textureFocus,
+ const CTextureInfo& textureNoFocus,
+ const CLabelInfo& labelInfo,
+ bool wrapMultiline)
+ : CGUIControl(parentID, controlID, posX, posY, width, height),
+ m_imgFocus(CGUITexture::CreateTexture(posX, posY, width, height, textureFocus)),
+ m_imgNoFocus(CGUITexture::CreateTexture(posX, posY, width, height, textureNoFocus)),
+ m_label(posX,
+ posY,
+ width,
+ height,
+ labelInfo,
+ wrapMultiline ? CGUILabel::OVER_FLOW_WRAP : CGUILabel::OVER_FLOW_TRUNCATE),
+ m_label2(posX, posY, width, height, labelInfo)
+{
+ m_bSelected = false;
+ m_alpha = 255;
+ m_focusCounter = 0;
+ m_minWidth = 0;
+ m_maxWidth = width;
+ ControlType = GUICONTROL_BUTTON;
+}
+
+CGUIButtonControl::CGUIButtonControl(const CGUIButtonControl& control)
+ : CGUIControl(control),
+ m_imgFocus(control.m_imgFocus->Clone()),
+ m_imgNoFocus(control.m_imgNoFocus->Clone()),
+ m_focusCounter(control.m_focusCounter),
+ m_alpha(control.m_alpha),
+ m_minWidth(control.m_minWidth),
+ m_maxWidth(control.m_maxWidth),
+ m_info(control.m_info),
+ m_info2(control.m_info2),
+ m_label(control.m_label),
+ m_label2(control.m_label2),
+ m_clickActions(control.m_clickActions),
+ m_focusActions(control.m_focusActions),
+ m_unfocusActions(control.m_unfocusActions),
+ m_bSelected(control.m_bSelected)
+{
+}
+
+void CGUIButtonControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ ProcessText(currentTime);
+ if (m_bInvalidated)
+ {
+ m_imgFocus->SetWidth(GetWidth());
+ m_imgFocus->SetHeight(m_height);
+
+ m_imgNoFocus->SetWidth(GetWidth());
+ m_imgNoFocus->SetHeight(m_height);
+ }
+
+ if (HasFocus())
+ {
+ unsigned int alphaChannel = m_alpha;
+ if (m_pulseOnSelect)
+ {
+ unsigned int alphaCounter = m_focusCounter + 2;
+ if ((alphaCounter % 128) >= 64)
+ alphaChannel = alphaCounter % 64;
+ else
+ alphaChannel = 63 - (alphaCounter % 64);
+
+ alphaChannel += 192;
+ alphaChannel = (unsigned int)((float)m_alpha * (float)alphaChannel / 255.0f);
+ }
+ if (m_imgFocus->SetAlpha((unsigned char)alphaChannel))
+ MarkDirtyRegion();
+
+ m_imgFocus->SetVisible(true);
+ m_imgNoFocus->SetVisible(false);
+ m_focusCounter++;
+ }
+ else
+ {
+ m_imgFocus->SetVisible(false);
+ m_imgNoFocus->SetVisible(true);
+ }
+
+ m_imgFocus->Process(currentTime);
+ m_imgNoFocus->Process(currentTime);
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIButtonControl::Render()
+{
+ m_imgFocus->Render();
+ m_imgNoFocus->Render();
+
+ RenderText();
+ CGUIControl::Render();
+}
+
+void CGUIButtonControl::RenderText()
+{
+ m_label.Render();
+ m_label2.Render();
+}
+
+CGUILabel::COLOR CGUIButtonControl::GetTextColor() const
+{
+ if (IsDisabled())
+ return CGUILabel::COLOR_DISABLED;
+ if (HasFocus())
+ return CGUILabel::COLOR_FOCUSED;
+ return CGUILabel::COLOR_TEXT;
+}
+
+#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))
+float CGUIButtonControl::GetWidth() const
+{
+ if (m_minWidth && m_minWidth != m_width)
+ {
+ float txtWidth = m_label.GetTextWidth() + 2 * m_label.GetLabelInfo().offsetX;
+ if (m_label2.GetTextWidth())
+ {
+ static const float min_space = 10;
+ txtWidth += m_label2.GetTextWidth() + 2 * m_label2.GetLabelInfo().offsetX + min_space;
+ }
+ float maxWidth = m_maxWidth ? m_maxWidth : txtWidth;
+ return CLAMP(txtWidth, m_minWidth, maxWidth);
+ }
+ return m_width;
+}
+
+void CGUIButtonControl::SetMinWidth(float minWidth)
+{
+ if (m_minWidth != minWidth)
+ MarkDirtyRegion();
+
+ m_minWidth = minWidth;
+}
+
+void CGUIButtonControl::ProcessText(unsigned int currentTime)
+{
+ CRect labelRenderRect = m_label.GetRenderRect();
+ CRect label2RenderRect = m_label2.GetRenderRect();
+
+ float renderWidth = GetWidth();
+ float renderTextWidth = renderWidth;
+ if (m_labelMaxWidth > 0 && m_labelMaxWidth < renderWidth)
+ renderTextWidth = m_labelMaxWidth;
+
+ bool changed = m_label.SetMaxRect(m_posX, m_posY, renderTextWidth, m_height);
+ changed |= m_label.SetText(m_info.GetLabel(m_parentID));
+ changed |= m_label.SetScrolling(HasFocus());
+ changed |= m_label2.SetMaxRect(m_posX, m_posY, renderTextWidth, m_height);
+ changed |= m_label2.SetText(m_info2.GetLabel(m_parentID));
+
+ // text changed - images need resizing
+ if (m_minWidth && (m_label.GetRenderRect() != labelRenderRect))
+ SetInvalid();
+
+ // auto-width - adjust hitrect
+ if (m_minWidth && m_width != renderWidth)
+ {
+ CRect rect{m_posX, m_posY, m_posX + renderWidth, m_posY + m_height};
+ SetHitRect(rect, m_hitColor);
+ }
+
+ // render the second label if it exists
+ if (!m_info2.GetLabel(m_parentID).empty())
+ {
+ changed |= m_label2.SetAlign(XBFONT_RIGHT | (m_label.GetLabelInfo().align & XBFONT_CENTER_Y) | XBFONT_TRUNCATED);
+ changed |= m_label2.SetScrolling(HasFocus());
+
+ // If overlapping was corrected - compare render rects to determine
+ // if they changed since last frame.
+ if (CGUILabel::CheckAndCorrectOverlap(m_label, m_label2))
+ changed |= (m_label.GetRenderRect() != labelRenderRect ||
+ m_label2.GetRenderRect() != label2RenderRect);
+
+ changed |= m_label2.SetColor(GetTextColor());
+ changed |= m_label2.Process(currentTime);
+ }
+ changed |= m_label.SetColor(GetTextColor());
+ changed |= m_label.Process(currentTime);
+ if (changed)
+ MarkDirtyRegion();
+}
+
+bool CGUIButtonControl::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_SELECT_ITEM)
+ {
+ OnClick();
+ return true;
+ }
+ return CGUIControl::OnAction(action);
+}
+
+bool CGUIButtonControl::OnMessage(CGUIMessage& message)
+{
+ if (message.GetControlId() == GetID())
+ {
+ if (message.GetMessage() == GUI_MSG_LABEL_SET)
+ {
+ SetLabel(message.GetLabel());
+ return true;
+ }
+ if (message.GetMessage() == GUI_MSG_LABEL2_SET)
+ {
+ SetLabel2(message.GetLabel());
+ return true;
+ }
+ if (message.GetMessage() == GUI_MSG_IS_SELECTED)
+ {
+ message.SetParam1(m_bSelected ? 1 : 0);
+ return true;
+ }
+ if (message.GetMessage() == GUI_MSG_SET_SELECTED)
+ {
+ if (!m_bSelected)
+ SetInvalid();
+ m_bSelected = true;
+ return true;
+ }
+ if (message.GetMessage() == GUI_MSG_SET_DESELECTED)
+ {
+ if (m_bSelected)
+ SetInvalid();
+ m_bSelected = false;
+ return true;
+ }
+ }
+
+ return CGUIControl::OnMessage(message);
+}
+
+void CGUIButtonControl::AllocResources()
+{
+ CGUIControl::AllocResources();
+ m_focusCounter = 0;
+ m_imgFocus->AllocResources();
+ m_imgNoFocus->AllocResources();
+ if (!m_width)
+ m_width = m_imgFocus->GetWidth();
+ if (!m_height)
+ m_height = m_imgFocus->GetHeight();
+}
+
+void CGUIButtonControl::FreeResources(bool immediately)
+{
+ CGUIControl::FreeResources(immediately);
+ m_imgFocus->FreeResources(immediately);
+ m_imgNoFocus->FreeResources(immediately);
+}
+
+void CGUIButtonControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+ m_imgFocus->DynamicResourceAlloc(bOnOff);
+ m_imgNoFocus->DynamicResourceAlloc(bOnOff);
+}
+
+void CGUIButtonControl::SetInvalid()
+{
+ CGUIControl::SetInvalid();
+ m_label.SetInvalid();
+ m_label2.SetInvalid();
+ m_imgFocus->SetInvalid();
+ m_imgNoFocus->SetInvalid();
+}
+
+void CGUIButtonControl::SetLabel(const std::string &label)
+{ // NOTE: No fallback for buttons at this point
+ if (m_info.GetLabel(GetParentID(), false) != label)
+ {
+ m_info.SetLabel(label, "", GetParentID());
+ SetInvalid();
+ }
+}
+
+void CGUIButtonControl::SetLabel2(const std::string &label2)
+{ // NOTE: No fallback for buttons at this point
+ if (m_info2.GetLabel(GetParentID(), false) != label2)
+ {
+ m_info2.SetLabel(label2, "", GetParentID());
+ SetInvalid();
+ }
+}
+
+void CGUIButtonControl::SetPosition(float posX, float posY)
+{
+ CGUIControl::SetPosition(posX, posY);
+ m_imgFocus->SetPosition(posX, posY);
+ m_imgNoFocus->SetPosition(posX, posY);
+}
+
+void CGUIButtonControl::SetAlpha(unsigned char alpha)
+{
+ if (m_alpha != alpha)
+ MarkDirtyRegion();
+ m_alpha = alpha;
+}
+
+bool CGUIButtonControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_label.UpdateColors();
+ changed |= m_label2.UpdateColors();
+ changed |= m_imgFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgNoFocus->SetDiffuseColor(m_diffuseColor);
+
+ return changed;
+}
+
+CRect CGUIButtonControl::CalcRenderRegion() const
+{
+ CRect buttonRect = CGUIControl::CalcRenderRegion();
+ CRect textRect = m_label.GetRenderRect();
+ buttonRect.Union(textRect);
+ return buttonRect;
+}
+
+EVENT_RESULT CGUIButtonControl::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_LEFT_CLICK)
+ {
+ OnAction(CAction(ACTION_SELECT_ITEM));
+ return EVENT_RESULT_HANDLED;
+ }
+ return EVENT_RESULT_UNHANDLED;
+}
+
+std::string CGUIButtonControl::GetDescription() const
+{
+ std::string strLabel(m_info.GetLabel(m_parentID));
+ return strLabel;
+}
+
+std::string CGUIButtonControl::GetLabel2() const
+{
+ std::string strLabel(m_info2.GetLabel(m_parentID));
+ return strLabel;
+}
+
+void CGUIButtonControl::PythonSetLabel(const std::string& strFont,
+ const std::string& strText,
+ UTILS::COLOR::Color textColor,
+ UTILS::COLOR::Color shadowColor,
+ UTILS::COLOR::Color focusedColor)
+{
+ m_label.GetLabelInfo().font = g_fontManager.GetFont(strFont);
+ m_label.GetLabelInfo().textColor = textColor;
+ m_label.GetLabelInfo().focusedColor = focusedColor;
+ m_label.GetLabelInfo().shadowColor = shadowColor;
+ SetLabel(strText);
+}
+
+void CGUIButtonControl::PythonSetDisabledColor(UTILS::COLOR::Color disabledColor)
+{
+ m_label.GetLabelInfo().disabledColor = disabledColor;
+}
+
+void CGUIButtonControl::OnClick()
+{
+ // Save values, as the click message may deactivate the window
+ int controlID = GetID();
+ int parentID = GetParentID();
+ CGUIAction clickActions = m_clickActions;
+
+ // button selected, send a message
+ CGUIMessage msg(GUI_MSG_CLICKED, controlID, parentID, 0);
+ SendWindowMessage(msg);
+
+ clickActions.ExecuteActions(controlID, parentID);
+}
+
+void CGUIButtonControl::OnFocus()
+{
+ m_focusActions.ExecuteActions(GetID(), GetParentID());
+}
+
+void CGUIButtonControl::OnUnFocus()
+{
+ m_unfocusActions.ExecuteActions(GetID(), GetParentID());
+}
+
+void CGUIButtonControl::SetSelected(bool bSelected)
+{
+ if (m_bSelected != bSelected)
+ {
+ m_bSelected = bSelected;
+ SetInvalid();
+ }
+}
diff --git a/xbmc/guilib/GUIButtonControl.dox b/xbmc/guilib/GUIButtonControl.dox
new file mode 100644
index 0000000..bbcf631
--- /dev/null
+++ b/xbmc/guilib/GUIButtonControl.dox
@@ -0,0 +1,85 @@
+/*!
+
+\page skin_Button_control Button control
+\brief **A standard push button control.**
+
+\tableofcontents
+
+The button control is used for creating push buttons in Kodi. You can
+choose the position, size, and look of the button, as well as choosing what
+action(s) should be performed when pushed.
+
+--------------------------------------------------------------------------------
+\section skin_Button_control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="button" id="1">
+ <description>My first button control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+ <colordiffuse>FFFFFFFF</colordiffuse>
+ <texturefocus colordiffuse="FFFFAAFF">myfocustexture.png</texturefocus>
+ <texturenofocus colordiffuse="FFFFAAFF">mynormaltexture.png</texturenofocus>
+ <label>29</label>
+ <wrapmultiline>true</wrapmultiline>
+ <font>font12</font>
+ <textcolor>FFFFFFFF</textcolor>
+ <focusedcolor>FFFFFFFF</focusedcolor>
+ <disabledcolor>80FFFFFF</disabledcolor>
+ <invalidcolor>FFFFFFFF</invalidcolor>
+ <align></align>
+ <aligny></aligny>
+ <textoffsetx></textoffsetx>
+ <textoffsety></textoffsety>
+ <pulseonselect></pulseonselect>
+ <onclick>ActivateWindow(MyVideos)</onclick>
+ <onfocus>-</onfocus>
+ <onunfocus>-</onunfocus>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+</control>
+~~~~~~~~~~~~~
+
+--------------------------------------------------------------------------------
+\section skin_Button_control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is lower case only. This is
+important, as xml tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| texturefocus | Specifies the image file which should be displayed when the button has focus. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| texturenofocus| Specifies the image file which should be displayed when the button does not have focus.
+| label | The label used on the button. It can be a link into <b>`strings.po`</b>, or an actual text label.
+| font | Font used for the button label. From fonts.xml.
+| textcolor | Color used for displaying the button label. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| focusedcolor | Color used for the button label when the button has in focus. In **AARRGGBB** hex format or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| disabledcolor | Color used for the button label if the button is disabled. In **AARRGGBB** hex format or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| invalidcolor | Color used for the button if the user entered some invalid value. In **AARRGGBB** hex format or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| shadowcolor | Specifies the color of the drop shadow on the text, in **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| angle | The angle the text should be rendered at, in degrees. A value of 0 is horizontal.
+| align | Label horizontal alignment on the button. Defaults to left, can also be center or right.
+| aligny | Label vertical alignment on the button. Defaults to top, can also be center.
+| textoffsetx | Amount to offset the label from the left (or right) edge of the button when using left or right alignment.
+| textoffsety | Amount to offset the label from the top edge of the button when using top alignment.
+| textwidth | Will truncate any text that's too long.
+| onclick | Specifies the action to perform when the button is pressed. Should be a built in function. [See here for more information](http://kodi.wiki/view/Built-in_functions_available_to_FTP,_Webserver,_skins,_keymap_and_to_python). You may have more than one <b>`<onclick>`</b> tag, and they'll be executed in sequence.
+| onfocus | Specifies the action to perform when the button is focused. Should be a built in function. The action is performed after any focus animations have completed. [See here for more information](http://kodi.wiki/view/Built-in_functions_available_to_FTP,_Webserver,_skins,_keymap_and_to_python).
+| onunfocus | Specifies the action to perform when the button loses focus. Should be a built in function.
+| wrapmultiline | Will wrap the label across multiple lines if the label exceeds the control width.
+
+
+--------------------------------------------------------------------------------
+\section skin_Button_control_sect3 See also
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIButtonControl.h b/xbmc/guilib/GUIButtonControl.h
new file mode 100644
index 0000000..2de7695
--- /dev/null
+++ b/xbmc/guilib/GUIButtonControl.h
@@ -0,0 +1,112 @@
+/*
+ * 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
+
+/*!
+\file GUIButtonControl.h
+\brief
+*/
+
+#include "GUIAction.h"
+#include "GUIControl.h"
+#include "GUILabel.h"
+#include "GUITexture.h"
+#include "guilib/guiinfo/GUIInfoLabel.h"
+#include "utils/ColorUtils.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIButtonControl : public CGUIControl
+{
+public:
+ CGUIButtonControl(int parentID, int controlID,
+ float posX, float posY, float width, float height,
+ const CTextureInfo& textureFocus, const CTextureInfo& textureNoFocus,
+ const CLabelInfo &label, bool wrapMultiline = false);
+
+ CGUIButtonControl(const CGUIButtonControl& control);
+
+ ~CGUIButtonControl() override = default;
+ CGUIButtonControl* Clone() const override { return new CGUIButtonControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnAction(const CAction &action) override ;
+ bool OnMessage(CGUIMessage& message) override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ void SetPosition(float posX, float posY) override;
+ virtual void SetLabel(const std::string & aLabel);
+ virtual void SetLabel2(const std::string & aLabel2);
+ void SetClickActions(const CGUIAction& clickActions) { m_clickActions = clickActions; }
+ const CGUIAction& GetClickActions() const { return m_clickActions; }
+ void SetFocusActions(const CGUIAction& focusActions) { m_focusActions = focusActions; }
+ void SetUnFocusActions(const CGUIAction& unfocusActions) { m_unfocusActions = unfocusActions; }
+ const CLabelInfo& GetLabelInfo() const { return m_label.GetLabelInfo(); }
+ virtual std::string GetLabel() const { return GetDescription(); }
+ virtual std::string GetLabel2() const;
+ void SetSelected(bool bSelected);
+ std::string GetDescription() const override;
+ float GetWidth() const override;
+ virtual void SetMinWidth(float minWidth);
+ void SetAlpha(unsigned char alpha);
+
+ void PythonSetLabel(const std::string& strFont,
+ const std::string& strText,
+ UTILS::COLOR::Color textColor,
+ UTILS::COLOR::Color shadowColor,
+ UTILS::COLOR::Color focusedColor);
+ void PythonSetDisabledColor(UTILS::COLOR::Color disabledColor);
+
+ virtual void OnClick();
+ bool HasClickActions() const { return m_clickActions.HasActionsMeetingCondition(); }
+
+ bool UpdateColors(const CGUIListItem* item) override;
+
+ CRect CalcRenderRegion() const override;
+
+protected:
+ friend class CGUISpinControlEx;
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ void OnFocus() override;
+ void OnUnFocus() override;
+ virtual void ProcessText(unsigned int currentTime);
+ virtual void RenderText();
+ virtual CGUILabel::COLOR GetTextColor() const;
+
+ /*!
+ * \brief Set the maximum width for the left label
+ */
+ void SetMaxWidth(float labelMaxWidth) { m_labelMaxWidth = labelMaxWidth; }
+
+ std::unique_ptr<CGUITexture> m_imgFocus;
+ std::unique_ptr<CGUITexture> m_imgNoFocus;
+ unsigned int m_focusCounter;
+ unsigned char m_alpha;
+
+ float m_minWidth;
+ float m_maxWidth;
+ float m_labelMaxWidth{0};
+
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_info;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_info2;
+ CGUILabel m_label;
+ CGUILabel m_label2;
+
+ CGUIAction m_clickActions;
+ CGUIAction m_focusActions;
+ CGUIAction m_unfocusActions;
+
+ bool m_bSelected;
+};
+
diff --git a/xbmc/guilib/GUIColorButtonControl.cpp b/xbmc/guilib/GUIColorButtonControl.cpp
new file mode 100644
index 0000000..67597d7
--- /dev/null
+++ b/xbmc/guilib/GUIColorButtonControl.cpp
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2005-2021 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 "GUIColorButtonControl.h"
+
+#include "GUIInfoManager.h"
+#include "LocalizeStrings.h"
+#include "input/Key.h"
+#include "utils/ColorUtils.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI;
+using namespace GUILIB;
+
+CGUIColorButtonControl::CGUIColorButtonControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& textureFocus,
+ const CTextureInfo& textureNoFocus,
+ const CLabelInfo& labelInfo,
+ const CTextureInfo& colorMask,
+ const CTextureInfo& colorDisabledMask)
+ : CGUIButtonControl(
+ parentID, controlID, posX, posY, width, height, textureFocus, textureNoFocus, labelInfo),
+ m_imgColorMask(CGUITexture::CreateTexture(posX, posY, 16, 16, colorMask)),
+ m_imgColorDisabledMask(CGUITexture::CreateTexture(posX, posY, 16, 16, colorDisabledMask)),
+ m_labelInfo(posX, posY, width, height, labelInfo, CGUILabel::OVER_FLOW_SCROLL)
+{
+ m_colorPosX = 0;
+ m_colorPosY = 0;
+ m_imgColorMask->SetAspectRatio(CAspectRatio::AR_KEEP);
+ m_imgColorDisabledMask->SetAspectRatio(CAspectRatio::AR_KEEP);
+ m_imgBoxColor = GUIINFO::CGUIInfoColor(UTILS::COLOR::NONE);
+ ControlType = GUICONTROL_COLORBUTTON;
+ // offsetX is like a left/right padding, "hex" label does not require high values
+ m_labelInfo.GetLabelInfo().offsetX = 2;
+}
+
+CGUIColorButtonControl::CGUIColorButtonControl(const CGUIColorButtonControl& control)
+ : CGUIButtonControl(control),
+ m_imgColorMask(control.m_imgColorMask->Clone()),
+ m_imgColorDisabledMask(control.m_imgColorDisabledMask->Clone()),
+ m_colorPosX(control.m_colorPosX),
+ m_colorPosY(control.m_colorPosY),
+ m_labelInfo(control.m_labelInfo)
+{
+}
+
+void CGUIColorButtonControl::Render()
+{
+ CGUIButtonControl::Render();
+ RenderInfoText();
+ if (IsDisabled())
+ m_imgColorDisabledMask->Render();
+ else
+ m_imgColorMask->Render();
+}
+
+void CGUIColorButtonControl::Process(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ ProcessInfoText(currentTime);
+ m_imgColorMask->Process(currentTime);
+ m_imgColorDisabledMask->Process(currentTime);
+ CGUIButtonControl::Process(currentTime, dirtyregions);
+}
+
+bool CGUIColorButtonControl::OnAction(const CAction& action)
+{
+ return CGUIButtonControl::OnAction(action);
+}
+
+bool CGUIColorButtonControl::OnMessage(CGUIMessage& message)
+{
+ return CGUIButtonControl::OnMessage(message);
+}
+
+void CGUIColorButtonControl::AllocResources()
+{
+ CGUIButtonControl::AllocResources();
+ m_imgColorMask->AllocResources();
+ m_imgColorDisabledMask->AllocResources();
+ SetPosition(m_posX, m_posY);
+}
+
+void CGUIColorButtonControl::FreeResources(bool immediately)
+{
+ CGUIButtonControl::FreeResources(immediately);
+ m_imgColorMask->FreeResources(immediately);
+ m_imgColorDisabledMask->FreeResources(immediately);
+}
+
+void CGUIColorButtonControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+ m_imgColorMask->DynamicResourceAlloc(bOnOff);
+ m_imgColorDisabledMask->DynamicResourceAlloc(bOnOff);
+}
+
+void CGUIColorButtonControl::SetInvalid()
+{
+ CGUIButtonControl::SetInvalid();
+ m_imgColorMask->SetInvalid();
+ m_imgColorDisabledMask->SetInvalid();
+ m_labelInfo.SetInvalid();
+}
+
+void CGUIColorButtonControl::SetPosition(float posX, float posY)
+{
+ CGUIButtonControl::SetPosition(posX, posY);
+ float colorPosX =
+ m_colorPosX ? m_posX + m_colorPosX : (m_posX + m_width) - m_imgColorMask->GetWidth();
+ float colorPosY =
+ m_colorPosY ? m_posY + m_colorPosY : m_posY + (m_height - m_imgColorMask->GetHeight()) / 2;
+ m_imgColorMask->SetPosition(colorPosX, colorPosY);
+ m_imgColorDisabledMask->SetPosition(colorPosX, colorPosY);
+}
+
+void CGUIColorButtonControl::SetColorDimensions(float posX, float posY, float width, float height)
+{
+ m_colorPosX = posX;
+ m_colorPosY = posY;
+ if (width)
+ {
+ m_imgColorMask->SetWidth(width);
+ m_imgColorDisabledMask->SetWidth(width);
+ }
+ if (height)
+ {
+ m_imgColorMask->SetHeight(height);
+ m_imgColorDisabledMask->SetHeight(height);
+ }
+ SetPosition(GetXPosition(), GetYPosition());
+}
+
+void CGUIColorButtonControl::SetWidth(float width)
+{
+ CGUIButtonControl::SetWidth(width);
+ SetPosition(GetXPosition(), GetYPosition());
+}
+
+void CGUIColorButtonControl::SetHeight(float height)
+{
+ CGUIButtonControl::SetHeight(height);
+ SetPosition(GetXPosition(), GetYPosition());
+}
+
+std::string CGUIColorButtonControl::GetDescription() const
+{
+ return CGUIButtonControl::GetDescription();
+}
+
+void CGUIColorButtonControl::SetImageBoxColor(GUIINFO::CGUIInfoColor color)
+{
+ m_imgBoxColor = color;
+}
+
+void CGUIColorButtonControl::SetImageBoxColor(const std::string& hexColor)
+{
+ if (hexColor.empty())
+ m_imgBoxColor = GUIINFO::CGUIInfoColor(UTILS::COLOR::NONE);
+ else
+ m_imgBoxColor = GUIINFO::CGUIInfoColor(UTILS::COLOR::ConvertHexToColor(hexColor));
+}
+
+bool CGUIColorButtonControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIButtonControl::UpdateColors(nullptr);
+ changed |= m_imgBoxColor.Update();
+ changed |= m_imgColorMask->SetDiffuseColor(m_imgBoxColor, item);
+ changed |= m_imgColorDisabledMask->SetDiffuseColor(m_diffuseColor);
+ changed |= m_labelInfo.UpdateColors();
+ return changed;
+}
+
+void CGUIColorButtonControl::RenderInfoText()
+{
+ m_labelInfo.Render();
+}
+
+void CGUIColorButtonControl::ProcessInfoText(unsigned int currentTime)
+{
+ CRect labelRenderRect = m_labelInfo.GetRenderRect();
+ bool changed = m_labelInfo.SetText(
+ StringUtils::Format("#{:08X}", static_cast<UTILS::COLOR::Color>(m_imgBoxColor)));
+ // Set Label X position based on image mask control position
+ float textWidth = m_labelInfo.GetTextWidth() + 2 * m_labelInfo.GetLabelInfo().offsetX;
+ float textPosX = m_imgColorMask->GetXPosition() - textWidth;
+ changed = m_labelInfo.SetMaxRect(textPosX, m_posY, textWidth, m_height);
+ // Limit the width for the left label to avoid text overlapping
+ SetMaxWidth(textPosX);
+
+ // text changed - images need resizing
+ if (m_minWidth && (m_labelInfo.GetRenderRect() != labelRenderRect))
+ SetInvalid();
+
+ changed |= m_labelInfo.SetColor(GetTextColor());
+ changed |= m_labelInfo.Process(currentTime);
+ if (changed)
+ MarkDirtyRegion();
+}
+
+CGUILabel::COLOR CGUIColorButtonControl::GetTextColor() const
+{
+ if (IsDisabled())
+ return CGUILabel::COLOR_DISABLED;
+ if (HasFocus())
+ return CGUILabel::COLOR_FOCUSED;
+ return CGUILabel::COLOR_TEXT;
+}
diff --git a/xbmc/guilib/GUIColorButtonControl.dox b/xbmc/guilib/GUIColorButtonControl.dox
new file mode 100644
index 0000000..7705a02
--- /dev/null
+++ b/xbmc/guilib/GUIColorButtonControl.dox
@@ -0,0 +1,90 @@
+/*!
+
+\page Color_button_control Color button control
+\brief **A color button control (as used for color settings).**
+
+\tableofcontents
+
+The color button control is used for creating push buttons in Kodi with a
+box for color preview. You can choose the position, size, and look of
+the button, as well as choosing what action(s) should be performed when pushed.
+
+--------------------------------------------------------------------------------
+\section Color_button_control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="colorbutton" id="2">
+ <description>My first colorbutton control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+ <colordiffuse>FFFFFFFF</colordiffuse>
+ <texturecolormask>mycolormask.png</texturecolormask>
+ <texturecolordisabledmask>mycolormask.png</texturecolordisabledmask>
+ <colorbox>FF0533ff</colorbox>
+ <onclick>ActivateWindow(MyVideos)</onclick>
+ <label>29</label>
+ <font>font12</font>
+ <textcolor>FFFFFFFF</textcolor>
+ <focusedcolor>FFFFFFFF</focusedcolor>
+ <disabledcolor>80FFFFFF</disabledcolor>
+ <align>left</align>
+ <aligny>center</aligny>
+ <pulseonselect>false</pulseonselect>
+ <onfocus>-</onfocus>
+ <onunfocus>-</onunfocus>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Color_button_control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is lower case only. This is
+important, as xml tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| texturefocus | Specifies the image file which should be displayed when the button has focus. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| texturenofocus| Specifies the image file which should be displayed when the button does not have focus.
+| label | The label used on the button. It can be a link into <b>`strings.po`</b>, or an actual text label.
+| font | Font used for the button label. From fonts.xml.
+| textcolor | Color used for displaying the button label. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| focusedcolor | Color used for the button label when the button has in focus. In **AARRGGBB** hex format or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| disabledcolor | Color used for the button label if the button is disabled. In **AARRGGBB** hex format or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| invalidcolor | Color used for the button if the user entered some invalid value. In **AARRGGBB** hex format or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| shadowcolor | Specifies the color of the drop shadow on the text, in **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| angle | The angle the text should be rendered at, in degrees. A value of 0 is horizontal.
+| align | Label horizontal alignment on the button. Defaults to left, can also be center or right.
+| aligny | Label vertical alignment on the button. Defaults to top, can also be center.
+| textoffsetx | Amount to offset the label from the left (or right) edge of the button when using left or right alignment.
+| textoffsety | Amount to offset the label from the top edge of the button when using top alignment.
+| textwidth | Will truncate any text that's too long.
+| onclick | Specifies the action to perform when the button is pressed. Should be a built in function. [See here for more information](http://kodi.wiki/view/Built-in_functions_available_to_FTP,_Webserver,_skins,_keymap_and_to_python). You may have more than one <b>`<onclick>`</b> tag, and they'll be executed in sequence.
+| onfocus | Specifies the action to perform when the button is focused. Should be a built in function. The action is performed after any focus animations have completed. [See here for more information](http://kodi.wiki/view/Built-in_functions_available_to_FTP,_Webserver,_skins,_keymap_and_to_python).
+| onunfocus | Specifies the action to perform when the button loses focus. Should be a built in function.
+| wrapmultiline | Will wrap the label across multiple lines if the label exceeds the control width.
+| colorbox | Specifies the color of the preview color box, in **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| colorposx | X offset of the preview color box
+| colorposy | Y offset of the preview color box
+| colorwidth | Width in Pixels of the preview color box
+| colorheight | Height in Pixels of the preview color box
+| texturecolormask | Specifies the image mask which should be displayed to see the color preview. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| texturecolordisabledmask | Specifies the image mask which should be displayed when the control is disabled. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+
+
+--------------------------------------------------------------------------------
+\section Color_button_control_sect3 See also
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIColorButtonControl.h b/xbmc/guilib/GUIColorButtonControl.h
new file mode 100644
index 0000000..828898c
--- /dev/null
+++ b/xbmc/guilib/GUIColorButtonControl.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2005-2021 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
+
+/*!
+\file CGUIColorButtonControl.h
+\brief
+*/
+#include "GUIButtonControl.h"
+#include "guilib/GUILabel.h"
+#include "guilib/guiinfo/GUIInfoColor.h"
+#include "utils/ColorUtils.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIColorButtonControl : public CGUIButtonControl
+{
+public:
+ CGUIColorButtonControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& textureFocus,
+ const CTextureInfo& textureNoFocus,
+ const CLabelInfo& labelInfo,
+ const CTextureInfo& colorMask,
+ const CTextureInfo& colorDisabledMask);
+
+ ~CGUIColorButtonControl() override = default;
+ CGUIColorButtonControl* Clone() const override { return new CGUIColorButtonControl(*this); }
+ CGUIColorButtonControl(const CGUIColorButtonControl& control);
+
+ void Process(unsigned int currentTime, CDirtyRegionList& dirtyregions) override;
+ void Render() override;
+ bool OnAction(const CAction& action) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ void SetPosition(float posX, float posY) override;
+ void SetWidth(float width) override;
+ void SetHeight(float height) override;
+ std::string GetDescription() const override;
+ void SetColorDimensions(float posX, float posY, float width, float height);
+ bool IsSelected() const { return m_bSelected; }
+ void SetImageBoxColor(const std::string& hexColor);
+ void SetImageBoxColor(KODI::GUILIB::GUIINFO::CGUIInfoColor color);
+
+protected:
+ bool UpdateColors(const CGUIListItem* item) override;
+ void ProcessInfoText(unsigned int currentTime);
+ void RenderInfoText();
+ CGUILabel::COLOR GetTextColor() const override;
+ std::unique_ptr<CGUITexture> m_imgColorMask;
+ std::unique_ptr<CGUITexture> m_imgColorDisabledMask;
+ float m_colorPosX;
+ float m_colorPosY;
+ KODI::GUILIB::GUIINFO::CGUIInfoColor m_imgBoxColor;
+ CGUILabel m_labelInfo;
+};
diff --git a/xbmc/guilib/GUIColorManager.cpp b/xbmc/guilib/GUIColorManager.cpp
new file mode 100644
index 0000000..b3ff2db
--- /dev/null
+++ b/xbmc/guilib/GUIColorManager.cpp
@@ -0,0 +1,144 @@
+/*
+ * 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 "GUIColorManager.h"
+
+#include "addons/Skin.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/ColorUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+CGUIColorManager::CGUIColorManager(void) = default;
+
+CGUIColorManager::~CGUIColorManager(void)
+{
+ Clear();
+}
+
+void CGUIColorManager::Clear()
+{
+ m_colors.clear();
+}
+
+// load the color file in
+void CGUIColorManager::Load(const std::string &colorFile)
+{
+ Clear();
+
+ // load the global color map if it exists
+ CXBMCTinyXML xmlDoc;
+ if (xmlDoc.LoadFile(CSpecialProtocol::TranslatePathConvertCase("special://xbmc/system/colors.xml")))
+ LoadXML(xmlDoc);
+
+ // first load the default color map if it exists
+ std::string path = URIUtils::AddFileToFolder(g_SkinInfo->Path(), "colors", "defaults.xml");
+
+ if (xmlDoc.LoadFile(CSpecialProtocol::TranslatePathConvertCase(path)))
+ LoadXML(xmlDoc);
+
+ // now the color map requested
+ if (StringUtils::EqualsNoCase(colorFile, "SKINDEFAULT"))
+ return; // nothing to do
+
+ path = URIUtils::AddFileToFolder(g_SkinInfo->Path(), "colors", colorFile);
+ if (!URIUtils::HasExtension(path))
+ path += ".xml";
+ CLog::Log(LOGINFO, "Loading colors from {}", path);
+
+ if (xmlDoc.LoadFile(path))
+ LoadXML(xmlDoc);
+}
+
+bool CGUIColorManager::LoadXML(CXBMCTinyXML &xmlDoc)
+{
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+
+ std::string strValue = pRootElement->Value();
+ if (strValue != std::string("colors"))
+ {
+ CLog::Log(LOGERROR, "color file doesn't start with <colors>");
+ return false;
+ }
+
+ const TiXmlElement *color = pRootElement->FirstChildElement("color");
+
+ while (color)
+ {
+ if (color->FirstChild() && color->Attribute("name"))
+ {
+ UTILS::COLOR::Color value = 0xffffffff;
+ sscanf(color->FirstChild()->Value(), "%x", (unsigned int*) &value);
+ std::string name = color->Attribute("name");
+ const auto it = m_colors.find(name);
+ if (it != m_colors.end())
+ (*it).second = value;
+ else
+ m_colors.insert(make_pair(name, value));
+ }
+ color = color->NextSiblingElement("color");
+ }
+ return true;
+}
+
+// lookup a color and return it's hex value
+UTILS::COLOR::Color CGUIColorManager::GetColor(const std::string& color) const
+{
+ // look in our color map
+ std::string trimmed(color);
+ StringUtils::TrimLeft(trimmed, "= ");
+ const auto it = m_colors.find(trimmed);
+ if (it != m_colors.end())
+ return (*it).second;
+
+ // try converting hex directly
+ UTILS::COLOR::Color value = 0;
+ sscanf(trimmed.c_str(), "%x", &value);
+ return value;
+}
+
+bool CGUIColorManager::LoadColorsListFromXML(
+ const std::string& filePath,
+ std::vector<std::pair<std::string, UTILS::COLOR::ColorInfo>>& colors,
+ bool sortColors)
+{
+ CLog::Log(LOGDEBUG, "Loading colors from file {}", filePath);
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(filePath))
+ {
+ CLog::Log(LOGERROR, "{} - Failed to load colors from file {}", __FUNCTION__, filePath);
+ return false;
+ }
+
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ std::string strValue = pRootElement->Value();
+ if (strValue != std::string("colors"))
+ {
+ CLog::Log(LOGERROR, "{} - Color file doesn't start with <colors>", __FUNCTION__);
+ return false;
+ }
+
+ const TiXmlElement* xmlColor = pRootElement->FirstChildElement("color");
+ while (xmlColor)
+ {
+ if (xmlColor->FirstChild() && xmlColor->Attribute("name"))
+ {
+ colors.emplace_back(
+ std::make_pair(xmlColor->Attribute("name"),
+ UTILS::COLOR::MakeColorInfo(xmlColor->FirstChild()->Value())));
+ }
+ xmlColor = xmlColor->NextSiblingElement("color");
+ }
+
+ if (sortColors)
+ std::sort(colors.begin(), colors.end(), UTILS::COLOR::comparePairColorInfo);
+
+ return true;
+}
diff --git a/xbmc/guilib/GUIColorManager.h b/xbmc/guilib/GUIColorManager.h
new file mode 100644
index 0000000..ac33c6d
--- /dev/null
+++ b/xbmc/guilib/GUIColorManager.h
@@ -0,0 +1,56 @@
+/*
+ * 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
+
+/*!
+\file GUIColorManager.h
+\brief
+*/
+
+/*!
+ \ingroup textures
+ \brief
+ */
+
+#include "utils/ColorUtils.h"
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CXBMCTinyXML;
+
+class CGUIColorManager
+{
+public:
+ CGUIColorManager(void);
+ virtual ~CGUIColorManager(void);
+
+ void Load(const std::string &colorFile);
+
+ UTILS::COLOR::Color GetColor(const std::string& color) const;
+
+ void Clear();
+
+ /*! \brief Load a colors list from a XML file
+ \param filePath The path to the XML file
+ \param colors The vector to populate
+ \param sortColors if true the colors will be sorted in a hue scale
+ \return true if success, otherwise false
+ */
+ bool LoadColorsListFromXML(const std::string& filePath,
+ std::vector<std::pair<std::string, UTILS::COLOR::ColorInfo>>& colors,
+ bool sortColors);
+
+protected:
+ bool LoadXML(CXBMCTinyXML& xmlDoc);
+
+ std::map<std::string, UTILS::COLOR::Color> m_colors;
+};
diff --git a/xbmc/guilib/GUIComponent.cpp b/xbmc/guilib/GUIComponent.cpp
new file mode 100644
index 0000000..e143e75
--- /dev/null
+++ b/xbmc/guilib/GUIComponent.cpp
@@ -0,0 +1,103 @@
+/*
+ * 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 "GUIComponent.h"
+
+#include "GUIAudioManager.h"
+#include "GUIColorManager.h"
+#include "GUIInfoManager.h"
+#include "GUILargeTextureManager.h"
+#include "GUIWindowManager.h"
+#include "ServiceBroker.h"
+#include "StereoscopicsManager.h"
+#include "TextureManager.h"
+#include "URL.h"
+#include "dialogs/GUIDialogYesNo.h"
+
+CGUIComponent::CGUIComponent()
+{
+ m_pWindowManager.reset(new CGUIWindowManager());
+ m_pTextureManager.reset(new CGUITextureManager());
+ m_pLargeTextureManager.reset(new CGUILargeTextureManager());
+ m_stereoscopicsManager.reset(new CStereoscopicsManager());
+ m_guiInfoManager.reset(new CGUIInfoManager());
+ m_guiColorManager.reset(new CGUIColorManager());
+ m_guiAudioManager.reset(new CGUIAudioManager());
+}
+
+CGUIComponent::~CGUIComponent()
+{
+ Deinit();
+}
+
+void CGUIComponent::Init()
+{
+ m_pWindowManager->Initialize();
+ m_stereoscopicsManager->Initialize();
+ m_guiInfoManager->Initialize();
+
+ CServiceBroker::RegisterGUI(this);
+}
+
+void CGUIComponent::Deinit()
+{
+ CServiceBroker::UnregisterGUI();
+
+ m_pWindowManager->DeInitialize();
+}
+
+CGUIWindowManager& CGUIComponent::GetWindowManager()
+{
+ return *m_pWindowManager;
+}
+
+CGUITextureManager& CGUIComponent::GetTextureManager()
+{
+ return *m_pTextureManager;
+}
+
+CGUILargeTextureManager& CGUIComponent::GetLargeTextureManager()
+{
+ return *m_pLargeTextureManager;
+}
+
+CStereoscopicsManager &CGUIComponent::GetStereoscopicsManager()
+{
+ return *m_stereoscopicsManager;
+}
+
+CGUIInfoManager &CGUIComponent::GetInfoManager()
+{
+ return *m_guiInfoManager;
+}
+
+CGUIColorManager &CGUIComponent::GetColorManager()
+{
+ return *m_guiColorManager;
+}
+
+CGUIAudioManager &CGUIComponent::GetAudioManager()
+{
+ return *m_guiAudioManager;
+}
+
+bool CGUIComponent::ConfirmDelete(const std::string& path)
+{
+ CGUIDialogYesNo* pDialog = GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (pDialog)
+ {
+ pDialog->SetHeading(CVariant{122});
+ pDialog->SetLine(0, CVariant{125});
+ pDialog->SetLine(1, CVariant{CURL(path).GetWithoutUserDetails()});
+ pDialog->SetLine(2, CVariant{""});
+ pDialog->Open();
+ if (pDialog->IsConfirmed())
+ return true;
+ }
+ return false;
+}
diff --git a/xbmc/guilib/GUIComponent.h b/xbmc/guilib/GUIComponent.h
new file mode 100644
index 0000000..97ada48
--- /dev/null
+++ b/xbmc/guilib/GUIComponent.h
@@ -0,0 +1,49 @@
+/*
+ * 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 <memory>
+#include <string>
+
+class CGUIWindowManager;
+class CGUITextureManager;
+class CGUILargeTextureManager;
+class CStereoscopicsManager;
+class CGUIInfoManager;
+class CGUIColorManager;
+class CGUIAudioManager;
+
+class CGUIComponent
+{
+public:
+ CGUIComponent();
+ virtual ~CGUIComponent();
+ void Init();
+ void Deinit();
+
+ CGUIWindowManager& GetWindowManager();
+ CGUITextureManager& GetTextureManager();
+ CGUILargeTextureManager& GetLargeTextureManager();
+ CStereoscopicsManager &GetStereoscopicsManager();
+ CGUIInfoManager &GetInfoManager();
+ CGUIColorManager &GetColorManager();
+ CGUIAudioManager &GetAudioManager();
+
+ bool ConfirmDelete(const std::string& path);
+
+protected:
+ // members are pointers in order to avoid includes
+ std::unique_ptr<CGUIWindowManager> m_pWindowManager;
+ std::unique_ptr<CGUITextureManager> m_pTextureManager;
+ std::unique_ptr<CGUILargeTextureManager> m_pLargeTextureManager;
+ std::unique_ptr<CStereoscopicsManager> m_stereoscopicsManager;
+ std::unique_ptr<CGUIInfoManager> m_guiInfoManager;
+ std::unique_ptr<CGUIColorManager> m_guiColorManager;
+ std::unique_ptr<CGUIAudioManager> m_guiAudioManager;
+};
diff --git a/xbmc/guilib/GUIControl.cpp b/xbmc/guilib/GUIControl.cpp
new file mode 100644
index 0000000..bd40475
--- /dev/null
+++ b/xbmc/guilib/GUIControl.cpp
@@ -0,0 +1,979 @@
+/*
+ * 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 "GUIControl.h"
+
+#include "GUIAction.h"
+#include "GUIComponent.h"
+#include "GUIControlProfiler.h"
+#include "GUIInfoManager.h"
+#include "GUIMessage.h"
+#include "GUITexture.h"
+#include "GUIWindowManager.h"
+#include "ServiceBroker.h"
+#include "input/InputManager.h"
+#include "input/Key.h"
+#include "input/mouse/MouseStat.h"
+#include "utils/log.h"
+
+using namespace KODI::GUILIB;
+
+CGUIControl::CGUIControl()
+{
+ m_hasProcessed = false;
+ m_bHasFocus = false;
+ m_controlID = 0;
+ m_parentID = 0;
+ m_visible = VISIBLE;
+ m_visibleFromSkinCondition = true;
+ m_forceHidden = false;
+ m_enabled = true;
+ m_posX = 0;
+ m_posY = 0;
+ m_width = 0;
+ m_height = 0;
+ ControlType = GUICONTROL_UNKNOWN;
+ m_bInvalidated = true;
+ m_bAllocated=false;
+ m_parentControl = NULL;
+ m_hasCamera = false;
+ m_pushedUpdates = false;
+ m_pulseOnSelect = false;
+ m_controlDirtyState = DIRTY_STATE_CONTROL;
+ m_stereo = 0.0f;
+ m_controlStats = nullptr;
+}
+
+CGUIControl::CGUIControl(int parentID, int controlID, float posX, float posY, float width, float height)
+: m_hitRect(posX, posY, posX + width, posY + height),
+ m_diffuseColor(0xffffffff)
+{
+ m_posX = posX;
+ m_posY = posY;
+ m_width = width;
+ m_height = height;
+ m_bHasFocus = false;
+ m_controlID = controlID;
+ m_parentID = parentID;
+ m_visible = VISIBLE;
+ m_visibleFromSkinCondition = true;
+ m_forceHidden = false;
+ m_enabled = true;
+ ControlType = GUICONTROL_UNKNOWN;
+ m_bInvalidated = true;
+ m_bAllocated=false;
+ m_hasProcessed = false;
+ m_parentControl = NULL;
+ m_hasCamera = false;
+ m_pushedUpdates = false;
+ m_pulseOnSelect = false;
+ m_controlDirtyState = DIRTY_STATE_CONTROL;
+ m_stereo = 0.0f;
+ m_controlStats = nullptr;
+}
+
+CGUIControl::CGUIControl(const CGUIControl &) = default;
+
+CGUIControl::~CGUIControl(void) = default;
+
+void CGUIControl::AllocResources()
+{
+ m_hasProcessed = false;
+ m_bInvalidated = true;
+ m_bAllocated=true;
+}
+
+void CGUIControl::FreeResources(bool immediately)
+{
+ if (m_bAllocated)
+ {
+ // Reset our animation states - not conditional anims though.
+ // I'm not sure if this is needed for most cases anyway. I believe it's only here
+ // because some windows aren't loaded on demand
+ for (unsigned int i = 0; i < m_animations.size(); i++)
+ {
+ CAnimation &anim = m_animations[i];
+ if (anim.GetType() != ANIM_TYPE_CONDITIONAL)
+ anim.ResetAnimation();
+ }
+ m_bAllocated=false;
+ }
+ m_hasProcessed = false;
+}
+
+void CGUIControl::DynamicResourceAlloc(bool bOnOff)
+{
+
+}
+
+// the main processing routine.
+// 1. animate and set animation transform
+// 2. if visible, process
+// 3. reset the animation transform
+void CGUIControl::DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ CRect dirtyRegion = m_renderRegion;
+
+ bool changed = (m_controlDirtyState & DIRTY_STATE_CONTROL) != 0 || (m_bInvalidated && IsVisible());
+ m_controlDirtyState = 0;
+
+ if (Animate(currentTime))
+ MarkDirtyRegion();
+
+ // if the control changed culling state from true to false, mark it
+ const bool culled = m_transform.alpha <= 0.01f;
+ if (m_isCulled != culled)
+ {
+ m_isCulled = false;
+ MarkDirtyRegion();
+ }
+ m_isCulled = culled;
+
+ if (IsVisible())
+ {
+ m_cachedTransform = CServiceBroker::GetWinSystem()->GetGfxContext().AddTransform(m_transform);
+ if (m_hasCamera)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetCameraPosition(m_camera);
+
+ Process(currentTime, dirtyregions);
+ m_bInvalidated = false;
+
+ if (dirtyRegion != m_renderRegion)
+ {
+ dirtyRegion.Union(m_renderRegion);
+ changed = true;
+ }
+
+ if (m_hasCamera)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreCameraPosition();
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+ }
+
+ UpdateControlStats();
+
+ changed |= (m_controlDirtyState & DIRTY_STATE_CONTROL) != 0;
+
+ if (changed)
+ {
+ dirtyregions.emplace_back(dirtyRegion);
+ }
+}
+
+void CGUIControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ // update our render region
+ m_renderRegion = CServiceBroker::GetWinSystem()->GetGfxContext().GenerateAABB(CalcRenderRegion());
+ m_hasProcessed = true;
+}
+
+// the main render routine.
+// 1. set the animation transform
+// 2. if visible, paint
+// 3. reset the animation transform
+void CGUIControl::DoRender()
+{
+ if (IsVisible() && !m_isCulled)
+ {
+ bool hasStereo =
+ m_stereo != 0.0f &&
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() !=
+ RENDER_STEREO_MODE_MONO &&
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() != RENDER_STEREO_MODE_OFF;
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetTransform(m_cachedTransform);
+ if (m_hasCamera)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetCameraPosition(m_camera);
+ if (hasStereo)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetStereoFactor(m_stereo);
+
+ GUIPROFILER_RENDER_BEGIN(this);
+
+ if (m_hitColor != 0xffffffff)
+ {
+ UTILS::COLOR::Color color =
+ CServiceBroker::GetWinSystem()->GetGfxContext().MergeAlpha(m_hitColor);
+ CGUITexture::DrawQuad(CServiceBroker::GetWinSystem()->GetGfxContext().GenerateAABB(m_hitRect), color);
+ }
+
+ Render();
+
+ GUIPROFILER_RENDER_END(this);
+
+ if (hasStereo)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreStereoFactor();
+ if (m_hasCamera)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreCameraPosition();
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+ }
+}
+
+bool CGUIControl::OnAction(const CAction &action)
+{
+ if (HasFocus())
+ {
+ switch (action.GetID())
+ {
+ case ACTION_MOVE_DOWN:
+ OnDown();
+ return true;
+
+ case ACTION_MOVE_UP:
+ OnUp();
+ return true;
+
+ case ACTION_MOVE_LEFT:
+ OnLeft();
+ return true;
+
+ case ACTION_MOVE_RIGHT:
+ OnRight();
+ return true;
+
+ case ACTION_SHOW_INFO:
+ return OnInfo();
+
+ case ACTION_NAV_BACK:
+ return OnBack();
+
+ case ACTION_NEXT_CONTROL:
+ OnNextControl();
+ return true;
+
+ case ACTION_PREV_CONTROL:
+ OnPrevControl();
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIControl::Navigate(int direction) const
+{
+ if (HasFocus())
+ {
+ CGUIMessage msg(GUI_MSG_MOVE, GetParentID(), GetID(), direction);
+ return SendWindowMessage(msg);
+ }
+ return false;
+}
+
+// Movement controls (derived classes can override)
+void CGUIControl::OnUp()
+{
+ Navigate(ACTION_MOVE_UP);
+}
+
+void CGUIControl::OnDown()
+{
+ Navigate(ACTION_MOVE_DOWN);
+}
+
+void CGUIControl::OnLeft()
+{
+ Navigate(ACTION_MOVE_LEFT);
+}
+
+void CGUIControl::OnRight()
+{
+ Navigate(ACTION_MOVE_RIGHT);
+}
+
+bool CGUIControl::OnBack()
+{
+ return Navigate(ACTION_NAV_BACK);
+}
+
+bool CGUIControl::OnInfo()
+{
+ CGUIAction action = GetAction(ACTION_SHOW_INFO);
+ if (action.HasAnyActions())
+ return action.ExecuteActions(GetID(), GetParentID());
+ return false;
+}
+
+void CGUIControl::OnNextControl()
+{
+ Navigate(ACTION_NEXT_CONTROL);
+}
+
+void CGUIControl::OnPrevControl()
+{
+ Navigate(ACTION_PREV_CONTROL);
+}
+
+bool CGUIControl::SendWindowMessage(CGUIMessage &message) const
+{
+ CGUIWindow *pWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(GetParentID());
+ if (pWindow)
+ return pWindow->OnMessage(message);
+ return CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+}
+
+int CGUIControl::GetID(void) const
+{
+ return m_controlID;
+}
+
+
+int CGUIControl::GetParentID(void) const
+{
+ return m_parentID;
+}
+
+bool CGUIControl::HasFocus(void) const
+{
+ return m_bHasFocus;
+}
+
+void CGUIControl::SetFocus(bool focus)
+{
+ if (m_bHasFocus && !focus)
+ QueueAnimation(ANIM_TYPE_UNFOCUS);
+ else if (!m_bHasFocus && focus)
+ QueueAnimation(ANIM_TYPE_FOCUS);
+ m_bHasFocus = focus;
+}
+
+bool CGUIControl::OnMessage(CGUIMessage& message)
+{
+ if ( message.GetControlId() == GetID() )
+ {
+ switch (message.GetMessage() )
+ {
+ case GUI_MSG_SETFOCUS:
+ // if control is disabled then move 2 the next control
+ if ( !CanFocus() )
+ {
+ CLog::Log(LOGERROR,
+ "Control {} in window {} has been asked to focus, "
+ "but it can't",
+ GetID(), GetParentID());
+ return false;
+ }
+ SetFocus(true);
+ {
+ // inform our parent window that this has happened
+ CGUIMessage message(GUI_MSG_FOCUSED, GetParentID(), GetID());
+ if (m_parentControl)
+ m_parentControl->OnMessage(message);
+ }
+ return true;
+ break;
+
+ case GUI_MSG_LOSTFOCUS:
+ {
+ SetFocus(false);
+ // and tell our parent so it can unfocus
+ if (m_parentControl)
+ m_parentControl->OnMessage(message);
+ return true;
+ }
+ break;
+
+ case GUI_MSG_VISIBLE:
+ SetVisible(true, true);
+ return true;
+ break;
+
+ case GUI_MSG_HIDDEN:
+ SetVisible(false);
+ return true;
+
+ // Note that the skin <enable> tag will override these messages
+ case GUI_MSG_ENABLED:
+ SetEnabled(true);
+ return true;
+
+ case GUI_MSG_DISABLED:
+ SetEnabled(false);
+ return true;
+
+ case GUI_MSG_WINDOW_RESIZE:
+ // invalidate controls to get them to recalculate sizing information
+ SetInvalid();
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIControl::CanFocus() const
+{
+ if (!IsVisible() && !m_allowHiddenFocus) return false;
+ if (IsDisabled()) return false;
+ return true;
+}
+
+bool CGUIControl::IsVisible() const
+{
+ if (m_forceHidden)
+ return false;
+ return m_visible == VISIBLE;
+}
+
+bool CGUIControl::IsDisabled() const
+{
+ return !m_enabled;
+}
+
+void CGUIControl::SetEnabled(bool bEnable)
+{
+ if (bEnable != m_enabled)
+ {
+ m_enabled = bEnable;
+ SetInvalid();
+ }
+}
+
+void CGUIControl::SetEnableCondition(const std::string &expression)
+{
+ if (expression == "true")
+ m_enabled = true;
+ else if (expression == "false")
+ m_enabled = false;
+ else
+ m_enableCondition = CServiceBroker::GetGUI()->GetInfoManager().Register(expression, GetParentID());
+}
+
+void CGUIControl::SetPosition(float posX, float posY)
+{
+ if ((m_posX != posX) || (m_posY != posY))
+ {
+ MarkDirtyRegion();
+
+ m_hitRect += CPoint(posX - m_posX, posY - m_posY);
+ m_posX = posX;
+ m_posY = posY;
+
+ SetInvalid();
+ }
+}
+
+bool CGUIControl::SetColorDiffuse(const GUIINFO::CGUIInfoColor &color)
+{
+ bool changed = m_diffuseColor != color;
+ m_diffuseColor = color;
+ return changed;
+}
+
+float CGUIControl::GetXPosition() const
+{
+ return m_posX;
+}
+
+float CGUIControl::GetYPosition() const
+{
+ return m_posY;
+}
+
+float CGUIControl::GetWidth() const
+{
+ return m_width;
+}
+
+float CGUIControl::GetHeight() const
+{
+ return m_height;
+}
+
+void CGUIControl::MarkDirtyRegion(const unsigned int dirtyState)
+{
+ // if the control is culled, bail
+ if (dirtyState == DIRTY_STATE_CONTROL && m_isCulled)
+ return;
+ if (!m_controlDirtyState && m_parentControl)
+ m_parentControl->MarkDirtyRegion(DIRTY_STATE_CHILD);
+
+ m_controlDirtyState |= dirtyState;
+}
+
+CRect CGUIControl::CalcRenderRegion() const
+{
+ CPoint tl(GetXPosition(), GetYPosition());
+ CPoint br(tl.x + GetWidth(), tl.y + GetHeight());
+
+ return CRect(tl.x, tl.y, br.x, br.y);
+}
+
+void CGUIControl::SetActions(const ActionMap &actions)
+{
+ m_actions = actions;
+}
+
+void CGUIControl::SetAction(int actionID, const CGUIAction &action, bool replace /*= true*/)
+{
+ ActionMap::iterator i = m_actions.find(actionID);
+ if (i == m_actions.end() || !i->second.HasAnyActions() || replace)
+ m_actions[actionID] = action;
+}
+
+void CGUIControl::SetWidth(float width)
+{
+ if (m_width != width)
+ {
+ MarkDirtyRegion();
+ m_width = width;
+ m_hitRect.x2 = m_hitRect.x1 + width;
+ SetInvalid();
+ }
+}
+
+void CGUIControl::SetHeight(float height)
+{
+ if (m_height != height)
+ {
+ MarkDirtyRegion();
+ m_height = height;
+ m_hitRect.y2 = m_hitRect.y1 + height;
+ SetInvalid();
+ }
+}
+
+void CGUIControl::SetVisible(bool bVisible, bool setVisState)
+{
+ if (bVisible && setVisState)
+ { //! @todo currently we only update m_visible from GUI_MSG_VISIBLE (SET_CONTROL_VISIBLE)
+ //! otherwise we just set m_forceHidden
+ GUIVISIBLE visible;
+ if (m_visibleCondition)
+ visible = m_visibleCondition->Get(INFO::DEFAULT_CONTEXT) ? VISIBLE : HIDDEN;
+ else
+ visible = VISIBLE;
+ if (visible != m_visible)
+ {
+ m_visible = visible;
+ SetInvalid();
+ }
+ }
+ if (m_forceHidden == bVisible)
+ {
+ m_forceHidden = !bVisible;
+ SetInvalid();
+ if (m_forceHidden)
+ MarkDirtyRegion();
+ }
+ if (m_forceHidden)
+ { // reset any visible animations that are in process
+ if (IsAnimating(ANIM_TYPE_VISIBLE))
+ {
+ // CLog::Log(LOGDEBUG, "Resetting visible animation on control {} (we are {})", m_controlID, m_visible ? "visible" : "hidden");
+ CAnimation *visibleAnim = GetAnimation(ANIM_TYPE_VISIBLE);
+ if (visibleAnim) visibleAnim->ResetAnimation();
+ }
+ }
+}
+
+bool CGUIControl::HitTest(const CPoint &point) const
+{
+ return m_hitRect.PtInRect(point);
+}
+
+EVENT_RESULT CGUIControl::SendMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ CPoint childPoint(point);
+ m_transform.InverseTransformPosition(childPoint.x, childPoint.y);
+ if (!CanFocusFromPoint(childPoint))
+ return EVENT_RESULT_UNHANDLED;
+
+ bool handled = event.m_id != ACTION_MOUSE_MOVE || OnMouseOver(childPoint);
+ EVENT_RESULT ret = OnMouseEvent(childPoint, event);
+ if (ret)
+ return ret;
+ return (handled && (event.m_id == ACTION_MOUSE_MOVE)) ? EVENT_RESULT_HANDLED : EVENT_RESULT_UNHANDLED;
+}
+
+// override this function to implement custom mouse behaviour
+bool CGUIControl::OnMouseOver(const CPoint &point)
+{
+ if (CServiceBroker::GetInputManager().GetMouseState() != MOUSE_STATE_DRAG)
+ CServiceBroker::GetInputManager().SetMouseState(MOUSE_STATE_FOCUS);
+ if (!CanFocus()) return false;
+ if (!HasFocus())
+ {
+ CGUIMessage msg(GUI_MSG_SETFOCUS, GetParentID(), GetID());
+ OnMessage(msg);
+ }
+ return true;
+}
+
+void CGUIControl::UpdateVisibility(const CGUIListItem *item)
+{
+ if (m_visibleCondition)
+ {
+ bool bWasVisible = m_visibleFromSkinCondition;
+ m_visibleFromSkinCondition = m_visibleCondition->Get(INFO::DEFAULT_CONTEXT, item);
+ if (!bWasVisible && m_visibleFromSkinCondition)
+ { // automatic change of visibility - queue the in effect
+ // CLog::Log(LOGDEBUG, "Visibility changed to visible for control id {}", m_controlID);
+ QueueAnimation(ANIM_TYPE_VISIBLE);
+ }
+ else if (bWasVisible && !m_visibleFromSkinCondition)
+ { // automatic change of visibility - do the out effect
+ // CLog::Log(LOGDEBUG, "Visibility changed to hidden for control id {}", m_controlID);
+ QueueAnimation(ANIM_TYPE_HIDDEN);
+ }
+ }
+ // check for conditional animations
+ for (unsigned int i = 0; i < m_animations.size(); i++)
+ {
+ CAnimation &anim = m_animations[i];
+ if (anim.GetType() == ANIM_TYPE_CONDITIONAL)
+ anim.UpdateCondition(item);
+ }
+ // and check for conditional enabling - note this overrides SetEnabled() from the code currently
+ // this may need to be reviewed at a later date
+ bool enabled = m_enabled;
+ if (m_enableCondition)
+ m_enabled = m_enableCondition->Get(INFO::DEFAULT_CONTEXT, item);
+
+ if (m_enabled != enabled)
+ MarkDirtyRegion();
+
+ m_allowHiddenFocus.Update(INFO::DEFAULT_CONTEXT, item);
+ if (UpdateColors(item))
+ MarkDirtyRegion();
+ // and finally, update our control information (if not pushed)
+ if (!m_pushedUpdates)
+ UpdateInfo(item);
+}
+
+bool CGUIControl::UpdateColors(const CGUIListItem* item)
+{
+ return m_diffuseColor.Update(item);
+}
+
+void CGUIControl::SetInitialVisibility()
+{
+ if (m_visibleCondition)
+ {
+ m_visibleFromSkinCondition = m_visibleCondition->Get(INFO::DEFAULT_CONTEXT);
+ m_visible = m_visibleFromSkinCondition ? VISIBLE : HIDDEN;
+ // CLog::Log(LOGDEBUG, "Set initial visibility for control {}: {}", m_controlID, m_visible == VISIBLE ? "visible" : "hidden");
+ }
+ else if (m_visible == DELAYED)
+ m_visible = VISIBLE;
+ // and handle animation conditions as well
+ for (unsigned int i = 0; i < m_animations.size(); i++)
+ {
+ CAnimation &anim = m_animations[i];
+ if (anim.GetType() == ANIM_TYPE_CONDITIONAL)
+ anim.SetInitialCondition();
+ }
+ // and check for conditional enabling - note this overrides SetEnabled() from the code currently
+ // this may need to be reviewed at a later date
+ if (m_enableCondition)
+ m_enabled = m_enableCondition->Get(INFO::DEFAULT_CONTEXT);
+ m_allowHiddenFocus.Update(INFO::DEFAULT_CONTEXT);
+ UpdateColors(nullptr);
+
+ MarkDirtyRegion();
+}
+
+void CGUIControl::SetVisibleCondition(const std::string &expression, const std::string &allowHiddenFocus)
+{
+ if (expression == "true")
+ m_visible = VISIBLE;
+ else if (expression == "false")
+ m_visible = HIDDEN;
+ else // register with the infomanager for updates
+ m_visibleCondition = CServiceBroker::GetGUI()->GetInfoManager().Register(expression, GetParentID());
+ m_allowHiddenFocus.Parse(allowHiddenFocus, GetParentID());
+}
+
+void CGUIControl::SetAnimations(const std::vector<CAnimation> &animations)
+{
+ m_animations = animations;
+ MarkDirtyRegion();
+}
+
+void CGUIControl::ResetAnimation(ANIMATION_TYPE type)
+{
+ MarkDirtyRegion();
+
+ for (unsigned int i = 0; i < m_animations.size(); i++)
+ {
+ if (m_animations[i].GetType() == type)
+ m_animations[i].ResetAnimation();
+ }
+}
+
+void CGUIControl::ResetAnimations()
+{
+ MarkDirtyRegion();
+
+ for (unsigned int i = 0; i < m_animations.size(); i++)
+ m_animations[i].ResetAnimation();
+
+ MarkDirtyRegion();
+}
+
+bool CGUIControl::CheckAnimation(ANIMATION_TYPE animType)
+{
+ // rule out the animations we shouldn't perform
+ if (!IsVisible() || !HasProcessed())
+ { // hidden or never processed - don't allow exit or entry animations for this control
+ if (animType == ANIM_TYPE_WINDOW_CLOSE)
+ { // could be animating a (delayed) window open anim, so reset it
+ ResetAnimation(ANIM_TYPE_WINDOW_OPEN);
+ return false;
+ }
+ }
+ if (!IsVisible())
+ { // hidden - only allow hidden anims if we're animating a visible anim
+ if (animType == ANIM_TYPE_HIDDEN && !IsAnimating(ANIM_TYPE_VISIBLE))
+ {
+ // update states to force it hidden
+ UpdateStates(animType, ANIM_PROCESS_NORMAL, ANIM_STATE_APPLIED);
+ return false;
+ }
+ if (animType == ANIM_TYPE_WINDOW_OPEN)
+ return false;
+ }
+ return true;
+}
+
+void CGUIControl::QueueAnimation(ANIMATION_TYPE animType)
+{
+ if (!CheckAnimation(animType))
+ return;
+
+ MarkDirtyRegion();
+
+ CAnimation *reverseAnim = GetAnimation((ANIMATION_TYPE)-animType, false);
+ CAnimation *forwardAnim = GetAnimation(animType);
+ // we first check whether the reverse animation is in progress (and reverse it)
+ // then we check for the normal animation, and queue it
+ if (reverseAnim && reverseAnim->IsReversible() && (reverseAnim->GetState() == ANIM_STATE_IN_PROCESS || reverseAnim->GetState() == ANIM_STATE_DELAYED))
+ {
+ reverseAnim->QueueAnimation(ANIM_PROCESS_REVERSE);
+ if (forwardAnim) forwardAnim->ResetAnimation();
+ }
+ else if (forwardAnim)
+ {
+ forwardAnim->QueueAnimation(ANIM_PROCESS_NORMAL);
+ if (reverseAnim) reverseAnim->ResetAnimation();
+ }
+ else
+ { // hidden and visible animations delay the change of state. If there is no animations
+ // to perform, then we should just change the state straightaway
+ if (reverseAnim) reverseAnim->ResetAnimation();
+ UpdateStates(animType, ANIM_PROCESS_NORMAL, ANIM_STATE_APPLIED);
+ }
+}
+
+CAnimation *CGUIControl::GetAnimation(ANIMATION_TYPE type, bool checkConditions /* = true */)
+{
+ for (unsigned int i = 0; i < m_animations.size(); i++)
+ {
+ CAnimation &anim = m_animations[i];
+ if (anim.GetType() == type)
+ {
+ if (!checkConditions || anim.CheckCondition())
+ return &anim;
+ }
+ }
+ return NULL;
+}
+
+bool CGUIControl::HasAnimation(ANIMATION_TYPE type)
+{
+ return (NULL != GetAnimation(type, true));
+}
+
+void CGUIControl::UpdateStates(ANIMATION_TYPE type, ANIMATION_PROCESS currentProcess, ANIMATION_STATE currentState)
+{
+ // Make sure control is hidden or visible at the appropriate times
+ // while processing a visible or hidden animation it needs to be visible,
+ // but when finished a hidden operation it needs to be hidden
+ if (type == ANIM_TYPE_VISIBLE)
+ {
+ if (currentProcess == ANIM_PROCESS_REVERSE)
+ {
+ if (currentState == ANIM_STATE_APPLIED)
+ m_visible = HIDDEN;
+ }
+ else if (currentProcess == ANIM_PROCESS_NORMAL)
+ {
+ if (currentState == ANIM_STATE_DELAYED)
+ m_visible = DELAYED;
+ else
+ m_visible = m_visibleFromSkinCondition ? VISIBLE : HIDDEN;
+ }
+ }
+ else if (type == ANIM_TYPE_HIDDEN)
+ {
+ if (currentProcess == ANIM_PROCESS_NORMAL) // a hide animation
+ {
+ if (currentState == ANIM_STATE_APPLIED)
+ m_visible = HIDDEN; // finished
+ else
+ m_visible = VISIBLE; // have to be visible until we are finished
+ }
+ else if (currentProcess == ANIM_PROCESS_REVERSE) // a visible animation
+ { // no delay involved here - just make sure it's visible
+ m_visible = m_visibleFromSkinCondition ? VISIBLE : HIDDEN;
+ }
+ }
+ else if (type == ANIM_TYPE_WINDOW_OPEN)
+ {
+ if (currentProcess == ANIM_PROCESS_NORMAL)
+ {
+ if (currentState == ANIM_STATE_DELAYED)
+ m_visible = DELAYED; // delayed
+ else
+ m_visible = m_visibleFromSkinCondition ? VISIBLE : HIDDEN;
+ }
+ }
+ else if (type == ANIM_TYPE_FOCUS)
+ {
+ // call the focus function if we have finished a focus animation
+ // (buttons can "click" on focus)
+ if (currentProcess == ANIM_PROCESS_NORMAL && currentState == ANIM_STATE_APPLIED)
+ OnFocus();
+ }
+ else if (type == ANIM_TYPE_UNFOCUS)
+ {
+ // call the unfocus function if we have finished a focus animation
+ // (buttons can "click" on focus)
+ if (currentProcess == ANIM_PROCESS_NORMAL && currentState == ANIM_STATE_APPLIED)
+ OnUnFocus();
+ }
+}
+
+bool CGUIControl::Animate(unsigned int currentTime)
+{
+ // check visible state outside the loop, as it could change
+ GUIVISIBLE visible = m_visible;
+
+ m_transform.Reset();
+ bool changed = false;
+
+ CPoint center(GetXPosition() + GetWidth() * 0.5f, GetYPosition() + GetHeight() * 0.5f);
+ for (unsigned int i = 0; i < m_animations.size(); i++)
+ {
+ CAnimation &anim = m_animations[i];
+ anim.Animate(currentTime, HasProcessed() || visible == DELAYED);
+ // Update the control states (such as visibility)
+ UpdateStates(anim.GetType(), anim.GetProcess(), anim.GetState());
+ // and render the animation effect
+ changed |= (anim.GetProcess() != ANIM_PROCESS_NONE);
+ anim.RenderAnimation(m_transform, center);
+
+ // debug stuff
+ //if (anim.GetProcess() != ANIM_PROCESS_NONE && IsVisible())
+ //{
+ // CLog::Log(LOGDEBUG, "Animating control {}", m_controlID);
+ //}
+ }
+
+ return changed;
+}
+
+bool CGUIControl::IsAnimating(ANIMATION_TYPE animType)
+{
+ for (unsigned int i = 0; i < m_animations.size(); i++)
+ {
+ CAnimation &anim = m_animations[i];
+ if (anim.GetType() == animType)
+ {
+ if (anim.GetQueuedProcess() == ANIM_PROCESS_NORMAL)
+ return true;
+ if (anim.GetProcess() == ANIM_PROCESS_NORMAL)
+ return true;
+ }
+ else if (anim.GetType() == -animType)
+ {
+ if (anim.GetQueuedProcess() == ANIM_PROCESS_REVERSE)
+ return true;
+ if (anim.GetProcess() == ANIM_PROCESS_REVERSE)
+ return true;
+ }
+ }
+ return false;
+}
+
+CGUIAction CGUIControl::GetAction(int actionID) const
+{
+ ActionMap::const_iterator i = m_actions.find(actionID);
+ if (i != m_actions.end())
+ return i->second;
+ return CGUIAction();
+}
+
+bool CGUIControl::CanFocusFromPoint(const CPoint &point) const
+{
+ return CanFocus() && HitTest(point);
+}
+
+void CGUIControl::UnfocusFromPoint(const CPoint &point)
+{
+ if (HasFocus())
+ {
+ CPoint controlPoint(point);
+ m_transform.InverseTransformPosition(controlPoint.x, controlPoint.y);
+ if (!HitTest(controlPoint))
+ {
+ SetFocus(false);
+
+ // and tell our parent so it can unfocus
+ if (m_parentControl)
+ {
+ CGUIMessage msgLostFocus(GUI_MSG_LOSTFOCUS, GetID(), GetID());
+ m_parentControl->OnMessage(msgLostFocus);
+ }
+ }
+ }
+}
+
+void CGUIControl::SaveStates(std::vector<CControlState> &states)
+{
+ // empty for now - do nothing with the majority of controls
+}
+
+CGUIControl *CGUIControl::GetControl(int iControl, std::vector<CGUIControl*> *idCollector)
+{
+ return (iControl == m_controlID) ? this : nullptr;
+}
+
+
+void CGUIControl::UpdateControlStats()
+{
+ if (m_controlStats)
+ {
+ ++m_controlStats->nCountTotal;
+ if (IsVisible() && IsVisibleFromSkin())
+ ++m_controlStats->nCountVisible;
+ }
+}
+
+void CGUIControl::SetHitRect(const CRect& rect, const UTILS::COLOR::Color& color)
+{
+ m_hitRect = rect;
+ m_hitColor = color;
+}
+
+void CGUIControl::SetCamera(const CPoint &camera)
+{
+ m_camera = camera;
+ m_hasCamera = true;
+}
+
+CPoint CGUIControl::GetRenderPosition() const
+{
+ float z = 0;
+ CPoint point(GetPosition());
+ m_transform.TransformPosition(point.x, point.y, z);
+ if (m_parentControl)
+ point += m_parentControl->GetRenderPosition();
+ return point;
+}
+
+void CGUIControl::SetStereoFactor(const float &factor)
+{
+ m_stereo = factor;
+}
diff --git a/xbmc/guilib/GUIControl.h b/xbmc/guilib/GUIControl.h
new file mode 100644
index 0000000..49fadaa
--- /dev/null
+++ b/xbmc/guilib/GUIControl.h
@@ -0,0 +1,383 @@
+/*
+ * 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
+
+/*!
+\file GUIControl.h
+\brief
+*/
+
+#include "DirtyRegion.h"
+#include "VisibleEffect.h" // needed for the CAnimation members
+#include "guiinfo/GUIInfoBool.h"
+#include "guiinfo/GUIInfoColor.h" // needed for CGUIInfoColor to handle infolabel'ed colors
+#include "utils/ColorUtils.h"
+#include "windowing/GraphicContext.h" // needed by any rendering operation (all controls)
+
+#include <vector>
+
+class CGUIListItem; // forward
+class CAction;
+class CMouseEvent;
+class CGUIMessage;
+class CGUIAction;
+
+enum ORIENTATION { HORIZONTAL = 0, VERTICAL };
+
+class CControlState
+{
+public:
+ CControlState(int id, int data)
+ {
+ m_id = id;
+ m_data = data;
+ }
+ int m_id;
+ int m_data;
+};
+
+struct GUICONTROLSTATS
+{
+ unsigned int nCountTotal;
+ unsigned int nCountVisible;
+
+ void Reset()
+ {
+ nCountTotal = nCountVisible = 0;
+ };
+};
+
+/*!
+ \brief Results of OnMouseEvent()
+ Any value not equal to EVENT_RESULT_UNHANDLED indicates that the event was handled.
+ */
+enum EVENT_RESULT { EVENT_RESULT_UNHANDLED = 0x00,
+ EVENT_RESULT_HANDLED = 0x01,
+ EVENT_RESULT_PAN_HORIZONTAL = 0x02,
+ EVENT_RESULT_PAN_VERTICAL = 0x04,
+ EVENT_RESULT_PAN_VERTICAL_WITHOUT_INERTIA = 0x08,
+ EVENT_RESULT_PAN_HORIZONTAL_WITHOUT_INERTIA = 0x10,
+ EVENT_RESULT_ROTATE = 0x20,
+ EVENT_RESULT_ZOOM = 0x40,
+ EVENT_RESULT_SWIPE = 0x80
+};
+
+/*!
+ \ingroup controls
+ \brief Base class for controls
+ */
+class CGUIControl
+{
+public:
+ CGUIControl();
+ CGUIControl(int parentID, int controlID, float posX, float posY, float width, float height);
+ CGUIControl(const CGUIControl &);
+ virtual ~CGUIControl(void);
+ virtual CGUIControl *Clone() const=0;
+
+ virtual void DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions);
+ virtual void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions);
+ virtual void DoRender();
+ virtual void Render() {}
+ // Called after the actual rendering is completed to trigger additional
+ // non GUI rendering operations
+ virtual void RenderEx() {}
+
+ /*! \brief Returns whether or not we have processed */
+ bool HasProcessed() const { return m_hasProcessed; }
+
+ // OnAction() is called by our window when we are the focused control.
+ // We should process any control-specific actions in the derived classes,
+ // and return true if we have taken care of the action. Returning false
+ // indicates that the message may be handed down to the window or application
+ // levels. This base class implementation handles basic movement, and should
+ // be called from the derived classes when the action has not been handled.
+ // Return true to indicate that the action has been dealt with.
+ virtual bool OnAction(const CAction &action);
+
+ // Common actions to make the code easier to read (no ugly switch statements in derived controls)
+ virtual void OnUp();
+ virtual void OnDown();
+ virtual void OnLeft();
+ virtual void OnRight();
+ virtual bool OnBack();
+ virtual bool OnInfo();
+ virtual void OnNextControl();
+ virtual void OnPrevControl();
+ virtual void OnFocus() {}
+ virtual void OnUnFocus() {}
+
+ /*! \brief React to a mouse event
+
+ Mouse events are sent from the window to all controls, and each control can react based on the event
+ and location of the event.
+
+ \param point the location in transformed skin coordinates from the upper left corner of the parent control.
+ \param event the mouse event to perform
+ \return EVENT_RESULT corresponding to whether the control handles this event
+ \sa HitTest, CanFocusFromPoint, CMouseEvent, EVENT_RESULT
+ */
+ virtual EVENT_RESULT SendMouseEvent(const CPoint &point, const CMouseEvent &event);
+
+ /*! \brief Perform a mouse action
+
+ Mouse actions are sent from the window to all controls, and each control can react based on the event
+ and location of the actions.
+
+ \param point the location in transformed skin coordinates from the upper left corner of the parent control.
+ \param event the mouse event to perform
+ \return EVENT_RESULT corresponding to whether the control handles this event
+ \sa SendMouseEvent, HitTest, CanFocusFromPoint, CMouseEvent
+ */
+ virtual EVENT_RESULT OnMouseEvent(const CPoint& point, const CMouseEvent& event)
+ {
+ return EVENT_RESULT_UNHANDLED;
+ }
+
+ /*! \brief Unfocus the control if the given point on screen is not within it's boundary
+ \param point the location in transformed skin coordinates from the upper left corner of the parent control.
+ \sa CanFocusFromPoint
+ */
+ virtual void UnfocusFromPoint(const CPoint &point);
+
+ /*! \brief Used to test whether the point is inside a control.
+ \param point location to test
+ \return true if the point is inside the bounds of this control.
+ \sa SetHitRect
+ */
+ virtual bool HitTest(const CPoint &point) const;
+
+ virtual bool OnMessage(CGUIMessage& message);
+ virtual int GetID(void) const;
+ virtual void SetID(int id) { m_controlID = id; }
+ int GetParentID() const;
+ virtual bool HasFocus() const;
+ virtual void AllocResources();
+ virtual void FreeResources(bool immediately = false);
+ virtual void DynamicResourceAlloc(bool bOnOff);
+ virtual bool IsDynamicallyAllocated() { return false; }
+ virtual bool CanFocus() const;
+ virtual bool IsVisible() const;
+ bool IsVisibleFromSkin() const { return m_visibleFromSkinCondition; }
+ virtual bool IsDisabled() const;
+ virtual void SetPosition(float posX, float posY);
+ virtual void SetHitRect(const CRect& rect, const UTILS::COLOR::Color& color);
+ virtual void SetCamera(const CPoint &camera);
+ virtual void SetStereoFactor(const float &factor);
+ bool SetColorDiffuse(const KODI::GUILIB::GUIINFO::CGUIInfoColor &color);
+ CPoint GetRenderPosition() const;
+ virtual float GetXPosition() const;
+ virtual float GetYPosition() const;
+ virtual float GetWidth() const;
+ virtual float GetHeight() const;
+
+ void MarkDirtyRegion(const unsigned int dirtyState = DIRTY_STATE_CONTROL);
+ bool IsControlDirty() const { return m_controlDirtyState != 0; }
+
+ /*! \brief return the render region in screen coordinates of this control
+ */
+ const CRect& GetRenderRegion() const { return m_renderRegion; }
+ /*! \brief calculate the render region in parentcontrol coordinates of this control
+ Called during process to update m_renderRegion
+ */
+ virtual CRect CalcRenderRegion() const;
+
+ /*! \brief Set actions to perform on navigation
+ \param actions ActionMap of actions
+ \sa SetNavigationAction
+ */
+ typedef std::map<int, CGUIAction> ActionMap;
+ void SetActions(const ActionMap &actions);
+
+ /*! \brief Set actions to perform on navigation
+ Navigations are set if replace is true or if there is no previously set action
+ \param actionID id of the navigation action
+ \param action CGUIAction to set
+ \param replace Actions are set only if replace is true or there is no previously set action. Defaults to true
+ \sa SetNavigationActions
+ */
+ void SetAction(int actionID, const CGUIAction &action, bool replace = true);
+
+ /*! \brief Get an action the control can be perform.
+ \param actionID The actionID to retrieve.
+ */
+ CGUIAction GetAction(int actionID) const;
+
+ /*! \brief Start navigating in given direction.
+ */
+ bool Navigate(int direction) const;
+ virtual void SetFocus(bool focus);
+ virtual void SetWidth(float width);
+ virtual void SetHeight(float height);
+ virtual void SetVisible(bool bVisible, bool setVisState = false);
+ void SetVisibleCondition(const std::string &expression, const std::string &allowHiddenFocus = "");
+ bool HasVisibleCondition() const { return m_visibleCondition != NULL; }
+ void SetEnableCondition(const std::string &expression);
+ virtual void UpdateVisibility(const CGUIListItem *item);
+ virtual void SetInitialVisibility();
+ virtual void SetEnabled(bool bEnable);
+ virtual void SetInvalid() { m_bInvalidated = true; }
+ virtual void SetPulseOnSelect(bool pulse) { m_pulseOnSelect = pulse; }
+ virtual std::string GetDescription() const { return ""; }
+ virtual std::string GetDescriptionByIndex(int index) const { return ""; }
+
+ void SetAnimations(const std::vector<CAnimation> &animations);
+ const std::vector<CAnimation>& GetAnimations() const { return m_animations; }
+
+ virtual void QueueAnimation(ANIMATION_TYPE anim);
+ virtual bool IsAnimating(ANIMATION_TYPE anim);
+ virtual bool HasAnimation(ANIMATION_TYPE anim);
+ CAnimation *GetAnimation(ANIMATION_TYPE type, bool checkConditions = true);
+ virtual void ResetAnimation(ANIMATION_TYPE type);
+ virtual void ResetAnimations();
+
+ // push information updates
+ virtual void UpdateInfo(const CGUIListItem* item = NULL) {}
+ virtual void SetPushUpdates(bool pushUpdates) { m_pushedUpdates = pushUpdates; }
+
+ virtual bool IsGroup() const { return false; }
+ virtual bool IsContainer() const { return false; }
+ virtual bool GetCondition(int condition, int data) const { return false; }
+
+ void SetParentControl(CGUIControl* control) { m_parentControl = control; }
+ CGUIControl* GetParentControl(void) const { return m_parentControl; }
+ virtual void SaveStates(std::vector<CControlState> &states);
+ virtual CGUIControl *GetControl(int id, std::vector<CGUIControl*> *idCollector = nullptr);
+
+
+ void SetControlStats(GUICONTROLSTATS* controlStats) { m_controlStats = controlStats; }
+ virtual void UpdateControlStats();
+
+ enum GUICONTROLTYPES
+ {
+ GUICONTROL_UNKNOWN,
+
+ // Keep sorted
+ GUICONTAINER_EPGGRID,
+ GUICONTAINER_FIXEDLIST,
+ GUICONTAINER_LIST,
+ GUICONTAINER_PANEL,
+ GUICONTAINER_WRAPLIST,
+ GUICONTROL_BORDEREDIMAGE,
+ GUICONTROL_BUTTON,
+ GUICONTROL_COLORBUTTON,
+ GUICONTROL_EDIT,
+ GUICONTROL_FADELABEL,
+ GUICONTROL_GAME,
+ GUICONTROL_GAMECONTROLLER,
+ GUICONTROL_GROUP,
+ GUICONTROL_GROUPLIST,
+ GUICONTROL_IMAGE,
+ GUICONTROL_LABEL,
+ GUICONTROL_LISTGROUP,
+ GUICONTROL_LISTLABEL,
+ GUICONTROL_MOVER,
+ GUICONTROL_MULTI_IMAGE,
+ GUICONTROL_PROGRESS,
+ GUICONTROL_RADIO,
+ GUICONTROL_RANGES,
+ GUICONTROL_RENDERADDON,
+ GUICONTROL_RESIZE,
+ GUICONTROL_RSS,
+ GUICONTROL_SCROLLBAR,
+ GUICONTROL_SETTINGS_SLIDER,
+ GUICONTROL_SLIDER,
+ GUICONTROL_SPIN,
+ GUICONTROL_SPINEX,
+ GUICONTROL_TEXTBOX,
+ GUICONTROL_TOGGLEBUTTON,
+ GUICONTROL_VIDEO,
+ GUICONTROL_VISUALISATION,
+ };
+ GUICONTROLTYPES GetControlType() const { return ControlType; }
+
+ enum GUIVISIBLE { HIDDEN = 0, DELAYED, VISIBLE };
+
+ enum GUISCROLLVALUE { FOCUS = 0, NEVER, ALWAYS };
+
+#ifdef _DEBUG
+ virtual void DumpTextureUse() {}
+#endif
+protected:
+ /*!
+ \brief Return the coordinates of the top left of the control, in the control's parent coordinates
+ \return The top left coordinates of the control
+ */
+ virtual CPoint GetPosition() const { return CPoint(GetXPosition(), GetYPosition()); }
+
+ /*! \brief Called when the mouse is over the control.
+ Default implementation selects the control.
+ \param point location of the mouse in transformed skin coordinates
+ \return true if handled, false otherwise.
+ */
+ virtual bool OnMouseOver(const CPoint &point);
+
+ /*! \brief Test whether we can focus a control from a point on screen
+ \param point the location in vanilla skin coordinates from the upper left corner of the parent control.
+ \return true if the control can be focused from this location
+ \sa UnfocusFromPoint, HitRect
+ */
+ virtual bool CanFocusFromPoint(const CPoint &point) const;
+
+ virtual bool UpdateColors(const CGUIListItem* item);
+ virtual bool Animate(unsigned int currentTime);
+ virtual bool CheckAnimation(ANIMATION_TYPE animType);
+ void UpdateStates(ANIMATION_TYPE type, ANIMATION_PROCESS currentProcess, ANIMATION_STATE currentState);
+ bool SendWindowMessage(CGUIMessage &message) const;
+
+ // navigation and actions
+ ActionMap m_actions;
+
+ float m_posX;
+ float m_posY;
+ float m_height;
+ float m_width;
+ CRect m_hitRect;
+ UTILS::COLOR::Color m_hitColor = 0xffffffff;
+ KODI::GUILIB::GUIINFO::CGUIInfoColor m_diffuseColor;
+ int m_controlID;
+ int m_parentID;
+ bool m_bHasFocus;
+ bool m_bInvalidated;
+ bool m_bAllocated;
+ bool m_pulseOnSelect;
+ GUICONTROLTYPES ControlType;
+ GUICONTROLSTATS *m_controlStats;
+
+ CGUIControl *m_parentControl; // our parent control if we're part of a group
+
+ // visibility condition/state
+ INFO::InfoPtr m_visibleCondition;
+ GUIVISIBLE m_visible;
+ bool m_visibleFromSkinCondition;
+ bool m_forceHidden; // set from the code when a hidden operation is given - overrides m_visible
+ KODI::GUILIB::GUIINFO::CGUIInfoBool m_allowHiddenFocus;
+ bool m_hasProcessed;
+ // enable/disable state
+ INFO::InfoPtr m_enableCondition;
+ bool m_enabled;
+
+ bool m_pushedUpdates;
+
+ // animation effects
+ std::vector<CAnimation> m_animations;
+ CPoint m_camera;
+ bool m_hasCamera;
+ float m_stereo;
+ TransformMatrix m_transform;
+ TransformMatrix m_cachedTransform; // Contains the absolute transform the control
+ bool m_isCulled{true};
+
+ static const unsigned int DIRTY_STATE_CONTROL = 1; //This control is dirty
+ static const unsigned int DIRTY_STATE_CHILD = 2; //One / more children are dirty
+
+ unsigned int m_controlDirtyState;
+ CRect m_renderRegion; // In screen coordinates
+};
+
diff --git a/xbmc/guilib/GUIControlFactory.cpp b/xbmc/guilib/GUIControlFactory.cpp
new file mode 100644
index 0000000..cf10978
--- /dev/null
+++ b/xbmc/guilib/GUIControlFactory.cpp
@@ -0,0 +1,1588 @@
+/*
+ * 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 "GUIControlFactory.h"
+
+#include "GUIAction.h"
+#include "GUIBorderedImage.h"
+#include "GUIButtonControl.h"
+#include "GUIColorButtonControl.h"
+#include "GUIColorManager.h"
+#include "GUIControlGroup.h"
+#include "GUIControlGroupList.h"
+#include "GUIEditControl.h"
+#include "GUIFadeLabelControl.h"
+#include "GUIFixedListContainer.h"
+#include "GUIFontManager.h"
+#include "GUIImage.h"
+#include "GUIInfoManager.h"
+#include "GUILabelControl.h"
+#include "GUIListContainer.h"
+#include "GUIListGroup.h"
+#include "GUIListLabel.h"
+#include "GUIMoverControl.h"
+#include "GUIMultiImage.h"
+#include "GUIPanelContainer.h"
+#include "GUIProgressControl.h"
+#include "GUIRSSControl.h"
+#include "GUIRadioButtonControl.h"
+#include "GUIRangesControl.h"
+#include "GUIRenderingControl.h"
+#include "GUIResizeControl.h"
+#include "GUIScrollBarControl.h"
+#include "GUISettingsSliderControl.h"
+#include "GUISliderControl.h"
+#include "GUISpinControl.h"
+#include "GUISpinControlEx.h"
+#include "GUITextBox.h"
+#include "GUIToggleButtonControl.h"
+#include "GUIVideoControl.h"
+#include "GUIVisualisationControl.h"
+#include "GUIWrappingListContainer.h"
+#include "LocalizeStrings.h"
+#include "addons/Skin.h"
+#include "cores/RetroPlayer/guicontrols/GUIGameControl.h"
+#include "games/controllers/guicontrols/GUIGameController.h"
+#include "input/Key.h"
+#include "pvr/guilib/GUIEPGGridContainer.h"
+#include "utils/CharsetConverter.h"
+#include "utils/RssManager.h"
+#include "utils/StringUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace KODI::GUILIB;
+using namespace PVR;
+
+typedef struct
+{
+ const char* name;
+ CGUIControl::GUICONTROLTYPES type;
+} ControlMapping;
+
+static const ControlMapping controls[] = {
+ {"button", CGUIControl::GUICONTROL_BUTTON},
+ {"colorbutton", CGUIControl::GUICONTROL_COLORBUTTON},
+ {"edit", CGUIControl::GUICONTROL_EDIT},
+ {"epggrid", CGUIControl::GUICONTAINER_EPGGRID},
+ {"fadelabel", CGUIControl::GUICONTROL_FADELABEL},
+ {"fixedlist", CGUIControl::GUICONTAINER_FIXEDLIST},
+ {"gamecontroller", CGUIControl::GUICONTROL_GAMECONTROLLER},
+ {"gamewindow", CGUIControl::GUICONTROL_GAME},
+ {"group", CGUIControl::GUICONTROL_GROUP},
+ {"group", CGUIControl::GUICONTROL_LISTGROUP},
+ {"grouplist", CGUIControl::GUICONTROL_GROUPLIST},
+ {"image", CGUIControl::GUICONTROL_IMAGE},
+ {"image", CGUIControl::GUICONTROL_BORDEREDIMAGE},
+ {"label", CGUIControl::GUICONTROL_LABEL},
+ {"label", CGUIControl::GUICONTROL_LISTLABEL},
+ {"list", CGUIControl::GUICONTAINER_LIST},
+ {"mover", CGUIControl::GUICONTROL_MOVER},
+ {"multiimage", CGUIControl::GUICONTROL_MULTI_IMAGE},
+ {"panel", CGUIControl::GUICONTAINER_PANEL},
+ {"progress", CGUIControl::GUICONTROL_PROGRESS},
+ {"radiobutton", CGUIControl::GUICONTROL_RADIO},
+ {"ranges", CGUIControl::GUICONTROL_RANGES},
+ {"renderaddon", CGUIControl::GUICONTROL_RENDERADDON},
+ {"resize", CGUIControl::GUICONTROL_RESIZE},
+ {"rss", CGUIControl::GUICONTROL_RSS},
+ {"scrollbar", CGUIControl::GUICONTROL_SCROLLBAR},
+ {"slider", CGUIControl::GUICONTROL_SLIDER},
+ {"sliderex", CGUIControl::GUICONTROL_SETTINGS_SLIDER},
+ {"spincontrol", CGUIControl::GUICONTROL_SPIN},
+ {"spincontrolex", CGUIControl::GUICONTROL_SPINEX},
+ {"textbox", CGUIControl::GUICONTROL_TEXTBOX},
+ {"togglebutton", CGUIControl::GUICONTROL_TOGGLEBUTTON},
+ {"videowindow", CGUIControl::GUICONTROL_VIDEO},
+ {"visualisation", CGUIControl::GUICONTROL_VISUALISATION},
+ {"wraplist", CGUIControl::GUICONTAINER_WRAPLIST},
+};
+
+CGUIControl::GUICONTROLTYPES CGUIControlFactory::TranslateControlType(const std::string &type)
+{
+ for (const ControlMapping& control : controls)
+ if (StringUtils::EqualsNoCase(type, control.name))
+ return control.type;
+ return CGUIControl::GUICONTROL_UNKNOWN;
+}
+
+std::string CGUIControlFactory::TranslateControlType(CGUIControl::GUICONTROLTYPES type)
+{
+ for (const ControlMapping& control : controls)
+ if (type == control.type)
+ return control.name;
+ return "";
+}
+
+CGUIControlFactory::CGUIControlFactory(void) = default;
+
+CGUIControlFactory::~CGUIControlFactory(void) = default;
+
+bool CGUIControlFactory::GetIntRange(const TiXmlNode* pRootNode, const char* strTag, int& iMinValue, int& iMaxValue, int& iIntervalValue)
+{
+ const TiXmlNode* pNode = pRootNode->FirstChild(strTag);
+ if (!pNode || !pNode->FirstChild()) return false;
+ iMinValue = atoi(pNode->FirstChild()->Value());
+ const char* maxValue = strchr(pNode->FirstChild()->Value(), ',');
+ if (maxValue)
+ {
+ maxValue++;
+ iMaxValue = atoi(maxValue);
+
+ const char* intervalValue = strchr(maxValue, ',');
+ if (intervalValue)
+ {
+ intervalValue++;
+ iIntervalValue = atoi(intervalValue);
+ }
+ }
+
+ return true;
+}
+
+bool CGUIControlFactory::GetFloatRange(const TiXmlNode* pRootNode, const char* strTag, float& fMinValue, float& fMaxValue, float& fIntervalValue)
+{
+ const TiXmlNode* pNode = pRootNode->FirstChild(strTag);
+ if (!pNode || !pNode->FirstChild()) return false;
+ fMinValue = (float)atof(pNode->FirstChild()->Value());
+ const char* maxValue = strchr(pNode->FirstChild()->Value(), ',');
+ if (maxValue)
+ {
+ maxValue++;
+ fMaxValue = (float)atof(maxValue);
+
+ const char* intervalValue = strchr(maxValue, ',');
+ if (intervalValue)
+ {
+ intervalValue++;
+ fIntervalValue = (float)atof(intervalValue);
+ }
+ }
+
+ return true;
+}
+
+float CGUIControlFactory::ParsePosition(const char* pos, const float parentSize)
+{
+ char* end = NULL;
+ float value = pos ? (float)strtod(pos, &end) : 0;
+ if (end)
+ {
+ if (*end == 'r')
+ value = parentSize - value;
+ else if (*end == '%')
+ value = value * parentSize / 100.0f;
+ }
+ return value;
+}
+
+bool CGUIControlFactory::GetPosition(const TiXmlNode *node, const char* strTag, const float parentSize, float& value)
+{
+ const TiXmlElement* pNode = node->FirstChildElement(strTag);
+ if (!pNode || !pNode->FirstChild()) return false;
+
+ value = ParsePosition(pNode->FirstChild()->Value(), parentSize);
+ return true;
+}
+
+bool CGUIControlFactory::GetDimension(const TiXmlNode *pRootNode, const char* strTag, const float parentSize, float &value, float &min)
+{
+ const TiXmlElement* pNode = pRootNode->FirstChildElement(strTag);
+ if (!pNode || !pNode->FirstChild()) return false;
+ if (0 == StringUtils::CompareNoCase("auto", pNode->FirstChild()->Value(), 4))
+ { // auto-width - at least min must be set
+ value = ParsePosition(pNode->Attribute("max"), parentSize);
+ min = ParsePosition(pNode->Attribute("min"), parentSize);
+ if (!min) min = 1;
+ return true;
+ }
+ value = ParsePosition(pNode->FirstChild()->Value(), parentSize);
+ return true;
+}
+
+bool CGUIControlFactory::GetDimensions(const TiXmlNode *node, const char *leftTag, const char *rightTag, const char *centerLeftTag,
+ const char *centerRightTag, const char *widthTag, const float parentSize, float &left,
+ float &width, float &min_width)
+{
+ float center = 0, right = 0;
+
+ // read from the XML
+ bool hasLeft = GetPosition(node, leftTag, parentSize, left);
+ bool hasCenter = GetPosition(node, centerLeftTag, parentSize, center);
+ if (!hasCenter && GetPosition(node, centerRightTag, parentSize, center))
+ {
+ center = parentSize - center;
+ hasCenter = true;
+ }
+ bool hasRight = false;
+ if (GetPosition(node, rightTag, parentSize, right))
+ {
+ right = parentSize - right;
+ hasRight = true;
+ }
+ bool hasWidth = GetDimension(node, widthTag, parentSize, width, min_width);
+
+ if (!hasLeft)
+ { // figure out position
+ if (hasCenter) // no left specified
+ {
+ if (hasWidth)
+ {
+ left = center - width/2;
+ hasLeft = true;
+ }
+ else
+ {
+ if (hasRight)
+ {
+ width = (right - center) * 2;
+ left = right - width;
+ hasLeft = true;
+ }
+ }
+ }
+ else if (hasRight) // no left or centre
+ {
+ if (hasWidth)
+ {
+ left = right - width;
+ hasLeft = true;
+ }
+ }
+ }
+ if (!hasWidth)
+ {
+ if (hasRight)
+ {
+ width = std::max(0.0f, right - left); // if left=0, this fills to size of parent
+ hasLeft = true;
+ hasWidth = true;
+ }
+ else if (hasCenter)
+ {
+ if (hasLeft)
+ {
+ width = std::max(0.0f, (center - left) * 2);
+ hasWidth = true;
+ }
+ else if (center > 0 && center < parentSize)
+ { // centre given, so fill to edge of parent
+ width = std::max(0.0f, std::min(parentSize - center, center) * 2);
+ left = center - width/2;
+ hasLeft = true;
+ hasWidth = true;
+ }
+ }
+ else if (hasLeft) // neither right nor center specified
+ {
+ width = std::max(0.0f, parentSize - left); // if left=0, this fills to parent
+ hasWidth = true;
+ }
+ }
+ return hasLeft && hasWidth;
+}
+
+bool CGUIControlFactory::GetAspectRatio(const TiXmlNode* pRootNode, const char* strTag, CAspectRatio &aspect)
+{
+ std::string ratio;
+ const TiXmlElement *node = pRootNode->FirstChildElement(strTag);
+ if (!node || !node->FirstChild())
+ return false;
+
+ ratio = node->FirstChild()->Value();
+ if (StringUtils::EqualsNoCase(ratio, "keep")) aspect.ratio = CAspectRatio::AR_KEEP;
+ else if (StringUtils::EqualsNoCase(ratio, "scale")) aspect.ratio = CAspectRatio::AR_SCALE;
+ else if (StringUtils::EqualsNoCase(ratio, "center")) aspect.ratio = CAspectRatio::AR_CENTER;
+ else if (StringUtils::EqualsNoCase(ratio, "stretch")) aspect.ratio = CAspectRatio::AR_STRETCH;
+
+ const char *attribute = node->Attribute("align");
+ if (attribute)
+ {
+ std::string align(attribute);
+ if (StringUtils::EqualsNoCase(align, "center")) aspect.align = ASPECT_ALIGN_CENTER | (aspect.align & ASPECT_ALIGNY_MASK);
+ else if (StringUtils::EqualsNoCase(align, "right")) aspect.align = ASPECT_ALIGN_RIGHT | (aspect.align & ASPECT_ALIGNY_MASK);
+ else if (StringUtils::EqualsNoCase(align, "left")) aspect.align = ASPECT_ALIGN_LEFT | (aspect.align & ASPECT_ALIGNY_MASK);
+ }
+ attribute = node->Attribute("aligny");
+ if (attribute)
+ {
+ std::string align(attribute);
+ if (StringUtils::EqualsNoCase(align, "center")) aspect.align = ASPECT_ALIGNY_CENTER | (aspect.align & ASPECT_ALIGN_MASK);
+ else if (StringUtils::EqualsNoCase(align, "bottom")) aspect.align = ASPECT_ALIGNY_BOTTOM | (aspect.align & ASPECT_ALIGN_MASK);
+ else if (StringUtils::EqualsNoCase(align, "top")) aspect.align = ASPECT_ALIGNY_TOP | (aspect.align & ASPECT_ALIGN_MASK);
+ }
+ attribute = node->Attribute("scalediffuse");
+ if (attribute)
+ {
+ std::string scale(attribute);
+ if (StringUtils::EqualsNoCase(scale, "true") || StringUtils::EqualsNoCase(scale, "yes"))
+ aspect.scaleDiffuse = true;
+ else
+ aspect.scaleDiffuse = false;
+ }
+ return true;
+}
+
+bool CGUIControlFactory::GetInfoTexture(const TiXmlNode* pRootNode, const char* strTag, CTextureInfo &image, GUIINFO::CGUIInfoLabel &info, int parentID)
+{
+ GetTexture(pRootNode, strTag, image);
+ image.filename = "";
+ GetInfoLabel(pRootNode, strTag, info, parentID);
+ return true;
+}
+
+bool CGUIControlFactory::GetTexture(const TiXmlNode* pRootNode, const char* strTag, CTextureInfo &image)
+{
+ const TiXmlElement* pNode = pRootNode->FirstChildElement(strTag);
+ if (!pNode) return false;
+ const char *border = pNode->Attribute("border");
+ if (border)
+ {
+ GetRectFromString(border, image.border);
+ const char* borderinfill = pNode->Attribute("infill");
+ image.m_infill = (!borderinfill || !StringUtils::EqualsNoCase(borderinfill, "false"));
+ }
+ image.orientation = 0;
+ const char *flipX = pNode->Attribute("flipx");
+ if (flipX && StringUtils::CompareNoCase(flipX, "true") == 0)
+ image.orientation = 1;
+ const char *flipY = pNode->Attribute("flipy");
+ if (flipY && StringUtils::CompareNoCase(flipY, "true") == 0)
+ image.orientation = 3 - image.orientation; // either 3 or 2
+ image.diffuse = XMLUtils::GetAttribute(pNode, "diffuse");
+ image.diffuseColor.Parse(XMLUtils::GetAttribute(pNode, "colordiffuse"), 0);
+ const char *background = pNode->Attribute("background");
+ if (background && StringUtils::CompareNoCase(background, "true", 4) == 0)
+ image.useLarge = true;
+ image.filename = pNode->FirstChild() ? pNode->FirstChild()->Value() : "";
+ return true;
+}
+
+void CGUIControlFactory::GetRectFromString(const std::string &string, CRect &rect)
+{
+ // format is rect="left[,top,right,bottom]"
+ std::vector<std::string> strRect = StringUtils::Split(string, ',');
+ if (strRect.size() == 1)
+ {
+ rect.x1 = (float)atof(strRect[0].c_str());
+ rect.y1 = rect.x1;
+ rect.x2 = rect.x1;
+ rect.y2 = rect.x1;
+ }
+ else if (strRect.size() == 4)
+ {
+ rect.x1 = (float)atof(strRect[0].c_str());
+ rect.y1 = (float)atof(strRect[1].c_str());
+ rect.x2 = (float)atof(strRect[2].c_str());
+ rect.y2 = (float)atof(strRect[3].c_str());
+ }
+}
+
+bool CGUIControlFactory::GetAlignment(const TiXmlNode* pRootNode, const char* strTag, uint32_t& alignment)
+{
+ const TiXmlNode* pNode = pRootNode->FirstChild(strTag);
+ if (!pNode || !pNode->FirstChild()) return false;
+
+ std::string strAlign = pNode->FirstChild()->Value();
+ if (strAlign == "right" || strAlign == "bottom") alignment = XBFONT_RIGHT;
+ else if (strAlign == "center") alignment = XBFONT_CENTER_X;
+ else if (strAlign == "justify") alignment = XBFONT_JUSTIFIED;
+ else alignment = XBFONT_LEFT;
+ return true;
+}
+
+bool CGUIControlFactory::GetAlignmentY(const TiXmlNode* pRootNode, const char* strTag, uint32_t& alignment)
+{
+ const TiXmlNode* pNode = pRootNode->FirstChild(strTag );
+ if (!pNode || !pNode->FirstChild())
+ {
+ return false;
+ }
+
+ std::string strAlign = pNode->FirstChild()->Value();
+
+ alignment = 0;
+ if (strAlign == "center")
+ {
+ alignment = XBFONT_CENTER_Y;
+ }
+
+ return true;
+}
+
+bool CGUIControlFactory::GetConditionalVisibility(const TiXmlNode* control, std::string &condition, std::string &allowHiddenFocus)
+{
+ const TiXmlElement* node = control->FirstChildElement("visible");
+ if (!node) return false;
+ std::vector<std::string> conditions;
+ while (node)
+ {
+ const char *hidden = node->Attribute("allowhiddenfocus");
+ if (hidden)
+ allowHiddenFocus = hidden;
+ // add to our condition string
+ if (!node->NoChildren())
+ conditions.emplace_back(node->FirstChild()->Value());
+ node = node->NextSiblingElement("visible");
+ }
+ if (!conditions.size())
+ return false;
+ if (conditions.size() == 1)
+ condition = conditions[0];
+ else
+ { // multiple conditions should be anded together
+ condition = "[";
+ for (unsigned int i = 0; i < conditions.size() - 1; i++)
+ condition += conditions[i] + "] + [";
+ condition += conditions[conditions.size() - 1] + "]";
+ }
+ return true;
+}
+
+bool CGUIControlFactory::GetConditionalVisibility(const TiXmlNode *control, std::string &condition)
+{
+ std::string allowHiddenFocus;
+ return GetConditionalVisibility(control, condition, allowHiddenFocus);
+}
+
+bool CGUIControlFactory::GetAnimations(TiXmlNode *control, const CRect &rect, int context, std::vector<CAnimation> &animations)
+{
+ TiXmlElement* node = control->FirstChildElement("animation");
+ bool ret = false;
+ if (node)
+ animations.clear();
+ while (node)
+ {
+ ret = true;
+ if (node->FirstChild())
+ {
+ CAnimation anim;
+ anim.Create(node, rect, context);
+ animations.push_back(anim);
+ if (StringUtils::CompareNoCase(node->FirstChild()->Value(), "VisibleChange") == 0)
+ { // add the hidden one as well
+ TiXmlElement hidden(*node);
+ hidden.FirstChild()->SetValue("hidden");
+ const char *start = hidden.Attribute("start");
+ const char *end = hidden.Attribute("end");
+ if (start && end)
+ {
+ std::string temp = end;
+ hidden.SetAttribute("end", start);
+ hidden.SetAttribute("start", temp.c_str());
+ }
+ else if (start)
+ hidden.SetAttribute("end", start);
+ else if (end)
+ hidden.SetAttribute("start", end);
+ CAnimation anim2;
+ anim2.Create(&hidden, rect, context);
+ animations.push_back(anim2);
+ }
+ }
+ node = node->NextSiblingElement("animation");
+ }
+ return ret;
+}
+
+bool CGUIControlFactory::GetActions(const TiXmlNode* pRootNode,
+ const char* strTag,
+ CGUIAction& actions)
+{
+ actions.Reset();
+ const TiXmlElement* pElement = pRootNode->FirstChildElement(strTag);
+ while (pElement)
+ {
+ if (pElement->FirstChild())
+ {
+ actions.Append(
+ {XMLUtils::GetAttribute(pElement, "condition"), pElement->FirstChild()->Value()});
+ }
+ pElement = pElement->NextSiblingElement(strTag);
+ }
+ return actions.HasAnyActions();
+}
+
+bool CGUIControlFactory::GetHitRect(const TiXmlNode *control, CRect &rect, const CRect &parentRect)
+{
+ const TiXmlElement* node = control->FirstChildElement("hitrect");
+ if (node)
+ {
+ rect.x1 = ParsePosition(node->Attribute("x"), parentRect.Width());
+ rect.y1 = ParsePosition(node->Attribute("y"), parentRect.Height());
+ if (node->Attribute("w"))
+ rect.x2 = (float)atof(node->Attribute("w")) + rect.x1;
+ else if (node->Attribute("right"))
+ rect.x2 = std::min(ParsePosition(node->Attribute("right"), parentRect.Width()), rect.x1);
+ if (node->Attribute("h"))
+ rect.y2 = (float)atof(node->Attribute("h")) + rect.y1;
+ else if (node->Attribute("bottom"))
+ rect.y2 = std::min(ParsePosition(node->Attribute("bottom"), parentRect.Height()), rect.y1);
+ return true;
+ }
+ return false;
+}
+
+bool CGUIControlFactory::GetScroller(const TiXmlNode *control, const std::string &scrollerTag, CScroller& scroller)
+{
+ const TiXmlElement* node = control->FirstChildElement(scrollerTag);
+ if (node)
+ {
+ unsigned int scrollTime;
+ if (XMLUtils::GetUInt(control, scrollerTag.c_str(), scrollTime))
+ {
+ scroller = CScroller(scrollTime, CAnimEffect::GetTweener(node));
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIControlFactory::GetColor(const TiXmlNode* control,
+ const char* strTag,
+ UTILS::COLOR::Color& value)
+{
+ const TiXmlElement* node = control->FirstChildElement(strTag);
+ if (node && node->FirstChild())
+ {
+ value = CServiceBroker::GetGUI()->GetColorManager().GetColor(node->FirstChild()->Value());
+ return true;
+ }
+ return false;
+}
+
+bool CGUIControlFactory::GetInfoColor(const TiXmlNode *control, const char *strTag, GUIINFO::CGUIInfoColor &value,int parentID)
+{
+ const TiXmlElement* node = control->FirstChildElement(strTag);
+ if (node && node->FirstChild())
+ {
+ value.Parse(node->FirstChild()->ValueStr(), parentID);
+ return true;
+ }
+ return false;
+}
+
+void CGUIControlFactory::GetInfoLabel(const TiXmlNode *pControlNode, const std::string &labelTag, GUIINFO::CGUIInfoLabel &infoLabel, int parentID)
+{
+ std::vector<GUIINFO::CGUIInfoLabel> labels;
+ GetInfoLabels(pControlNode, labelTag, labels, parentID);
+ if (labels.size())
+ infoLabel = labels[0];
+}
+
+bool CGUIControlFactory::GetInfoLabelFromElement(const TiXmlElement *element, GUIINFO::CGUIInfoLabel &infoLabel, int parentID)
+{
+ if (!element || !element->FirstChild())
+ return false;
+
+ std::string label = element->FirstChild()->Value();
+ if (label.empty())
+ return false;
+
+ std::string fallback = XMLUtils::GetAttribute(element, "fallback");
+ if (StringUtils::IsNaturalNumber(label))
+ label = g_localizeStrings.Get(atoi(label.c_str()));
+ if (StringUtils::IsNaturalNumber(fallback))
+ fallback = g_localizeStrings.Get(atoi(fallback.c_str()));
+ else
+ g_charsetConverter.unknownToUTF8(fallback);
+ infoLabel.SetLabel(label, fallback, parentID);
+ return true;
+}
+
+void CGUIControlFactory::GetInfoLabels(const TiXmlNode *pControlNode, const std::string &labelTag, std::vector<GUIINFO::CGUIInfoLabel> &infoLabels, int parentID)
+{
+ // we can have the following infolabels:
+ // 1. <number>1234</number> -> direct number
+ // 2. <label>number</label> -> lookup in localizestrings
+ // 3. <label fallback="blah">$LOCALIZE(blah) $INFO(blah)</label> -> infolabel with given fallback
+ // 4. <info>ListItem.Album</info> (uses <label> as fallback)
+ int labelNumber = 0;
+ if (XMLUtils::GetInt(pControlNode, "number", labelNumber))
+ {
+ std::string label = std::to_string(labelNumber);
+ infoLabels.emplace_back(label);
+ return; // done
+ }
+ const TiXmlElement *labelNode = pControlNode->FirstChildElement(labelTag);
+ while (labelNode)
+ {
+ GUIINFO::CGUIInfoLabel label;
+ if (GetInfoLabelFromElement(labelNode, label, parentID))
+ infoLabels.push_back(label);
+ labelNode = labelNode->NextSiblingElement(labelTag);
+ }
+ const TiXmlNode *infoNode = pControlNode->FirstChild("info");
+ if (infoNode)
+ { // <info> nodes override <label>'s (backward compatibility)
+ std::string fallback;
+ if (infoLabels.size())
+ fallback = infoLabels[0].GetLabel(0);
+ infoLabels.clear();
+ while (infoNode)
+ {
+ if (infoNode->FirstChild())
+ {
+ std::string info = StringUtils::Format("$INFO[{}]", infoNode->FirstChild()->Value());
+ infoLabels.emplace_back(info, fallback, parentID);
+ }
+ infoNode = infoNode->NextSibling("info");
+ }
+ }
+}
+
+// Convert a string to a GUI label, by translating/parsing the label for localisable strings
+std::string CGUIControlFactory::FilterLabel(const std::string &label)
+{
+ std::string viewLabel = label;
+ if (StringUtils::IsNaturalNumber(viewLabel))
+ viewLabel = g_localizeStrings.Get(atoi(label.c_str()));
+ else
+ g_charsetConverter.unknownToUTF8(viewLabel);
+ return viewLabel;
+}
+
+bool CGUIControlFactory::GetString(const TiXmlNode* pRootNode, const char *strTag, std::string &text)
+{
+ if (!XMLUtils::GetString(pRootNode, strTag, text))
+ return false;
+ if (StringUtils::IsNaturalNumber(text))
+ text = g_localizeStrings.Get(atoi(text.c_str()));
+ return true;
+}
+
+std::string CGUIControlFactory::GetType(const TiXmlElement *pControlNode)
+{
+ std::string type = XMLUtils::GetAttribute(pControlNode, "type");
+ if (type.empty()) // backward compatibility - not desired
+ XMLUtils::GetString(pControlNode, "type", type);
+ return type;
+}
+
+bool CGUIControlFactory::GetMovingSpeedConfig(const TiXmlNode* pRootNode,
+ const char* strTag,
+ UTILS::MOVING_SPEED::MapEventConfig& movingSpeedCfg)
+{
+ const TiXmlElement* msNode = pRootNode->FirstChildElement(strTag);
+ if (!msNode)
+ return false;
+
+ float globalAccel{StringUtils::ToFloat(XMLUtils::GetAttribute(msNode, "acceleration"))};
+ float globalMaxVel{StringUtils::ToFloat(XMLUtils::GetAttribute(msNode, "maxvelocity"))};
+ uint32_t globalResetTimeout{
+ StringUtils::ToUint32(XMLUtils::GetAttribute(msNode, "resettimeout"))};
+ float globalDelta{StringUtils::ToFloat(XMLUtils::GetAttribute(msNode, "delta"))};
+
+ const TiXmlElement* configElement{msNode->FirstChildElement("eventconfig")};
+ while (configElement)
+ {
+ const char* eventType = configElement->Attribute("type");
+ if (!eventType)
+ {
+ CLog::LogF(LOGERROR, "Failed to parse XML \"eventconfig\" tag missing \"type\" attribute");
+ continue;
+ }
+
+ const char* accelerationStr{configElement->Attribute("acceleration")};
+ float acceleration = accelerationStr ? StringUtils::ToFloat(accelerationStr) : globalAccel;
+
+ const char* maxVelocityStr{configElement->Attribute("maxvelocity")};
+ float maxVelocity = maxVelocityStr ? StringUtils::ToFloat(maxVelocityStr) : globalMaxVel;
+
+ const char* resetTimeoutStr{configElement->Attribute("resettimeout")};
+ uint32_t resetTimeout =
+ resetTimeoutStr ? StringUtils::ToUint32(resetTimeoutStr) : globalResetTimeout;
+
+ const char* deltaStr{configElement->Attribute("delta")};
+ float delta = deltaStr ? StringUtils::ToFloat(deltaStr) : globalDelta;
+
+ UTILS::MOVING_SPEED::EventCfg eventCfg{acceleration, maxVelocity, resetTimeout, delta};
+ movingSpeedCfg.emplace(UTILS::MOVING_SPEED::ParseEventType(eventType), eventCfg);
+
+ configElement = configElement->NextSiblingElement("eventconfig");
+ }
+ return true;
+}
+
+CGUIControl* CGUIControlFactory::Create(int parentID, const CRect &rect, TiXmlElement* pControlNode, bool insideContainer)
+{
+ // get the control type
+ std::string strType = GetType(pControlNode);
+ CGUIControl::GUICONTROLTYPES type = TranslateControlType(strType);
+
+ int id = 0;
+ float posX = 0, posY = 0;
+ float width = 0, height = 0;
+ float minHeight = 0, minWidth = 0;
+
+ CGUIControl::ActionMap actions;
+
+ int pageControl = 0;
+ GUIINFO::CGUIInfoColor colorDiffuse(0xFFFFFFFF);
+ GUIINFO::CGUIInfoColor colorBox(0xFF000000);
+ int defaultControl = 0;
+ bool defaultAlways = false;
+ std::string strTmp;
+ int singleInfo = 0;
+ int singleInfo2 = 0;
+ std::string strLabel;
+ int iUrlSet=0;
+ std::string toggleSelect;
+
+ float spinWidth = 16;
+ float spinHeight = 16;
+ float spinPosX = 0, spinPosY = 0;
+ std::string strSubType;
+ int iType = SPIN_CONTROL_TYPE_TEXT;
+ int iMin = 0;
+ int iMax = 100;
+ int iInterval = 1;
+ float fMin = 0.0f;
+ float fMax = 1.0f;
+ float fInterval = 0.1f;
+ bool bReverse = true;
+ bool bReveal = false;
+ CTextureInfo textureBackground, textureLeft, textureRight, textureMid, textureOverlay;
+ CTextureInfo textureNib, textureNibFocus, textureBar, textureBarFocus;
+ CTextureInfo textureUp, textureDown;
+ CTextureInfo textureUpFocus, textureDownFocus;
+ CTextureInfo textureUpDisabled, textureDownDisabled;
+ CTextureInfo texture, borderTexture;
+ GUIINFO::CGUIInfoLabel textureFile;
+ CTextureInfo textureFocus, textureNoFocus;
+ CTextureInfo textureAltFocus, textureAltNoFocus;
+ CTextureInfo textureRadioOnFocus, textureRadioOnNoFocus;
+ CTextureInfo textureRadioOffFocus, textureRadioOffNoFocus;
+ CTextureInfo textureRadioOnDisabled, textureRadioOffDisabled;
+ CTextureInfo textureProgressIndicator;
+ CTextureInfo textureColorMask, textureColorDisabledMask;
+
+ GUIINFO::CGUIInfoLabel texturePath;
+ CRect borderSize;
+
+ float sliderWidth = 150, sliderHeight = 16;
+ CPoint offset;
+
+ bool bHasPath = false;
+ CGUIAction clickActions;
+ CGUIAction altclickActions;
+ CGUIAction focusActions;
+ CGUIAction unfocusActions;
+ CGUIAction textChangeActions;
+ std::string strTitle = "";
+ std::string strRSSTags = "";
+
+ float buttonGap = 5;
+ int iMovementRange = 0;
+ CAspectRatio aspect;
+ std::string allowHiddenFocus;
+ std::string enableCondition;
+
+ std::vector<CAnimation> animations;
+
+ CGUIControl::GUISCROLLVALUE scrollValue = CGUIControl::FOCUS;
+ bool bPulse = true;
+ unsigned int timePerImage = 0;
+ unsigned int fadeTime = 0;
+ unsigned int timeToPauseAtEnd = 0;
+ bool randomized = false;
+ bool loop = true;
+ bool wrapMultiLine = false;
+ ORIENTATION orientation = VERTICAL;
+ bool showOnePage = true;
+ bool scrollOut = true;
+ int preloadItems = 0;
+
+ CLabelInfo labelInfo, labelInfoMono;
+
+ GUIINFO::CGUIInfoColor hitColor(0xFFFFFFFF);
+ GUIINFO::CGUIInfoColor textColor3;
+ GUIINFO::CGUIInfoColor headlineColor;
+
+ float radioWidth = 0;
+ float radioHeight = 0;
+ float radioPosX = 0;
+ float radioPosY = 0;
+
+ float colorWidth = 0;
+ float colorHeight = 0;
+ float colorPosX = 0;
+ float colorPosY = 0;
+
+ std::string altLabel;
+ std::string strLabel2;
+ std::string action;
+
+ int focusPosition = 0;
+ int scrollTime = 200;
+ int timeBlocks = 36;
+ int rulerUnit = 12;
+ bool useControlCoords = false;
+ bool renderFocusedLast = false;
+
+ CRect hitRect;
+ CPoint camera;
+ float stereo = 0.f;
+ bool hasCamera = false;
+ bool resetOnLabelChange = true;
+ bool bPassword = false;
+ std::string visibleCondition;
+
+ UTILS::MOVING_SPEED::MapEventConfig movingSpeedCfg;
+
+ /////////////////////////////////////////////////////////////////////////////
+ // Read control properties from XML
+ //
+
+ if (!pControlNode->Attribute("id", &id))
+ XMLUtils::GetInt(pControlNode, "id", id); // backward compatibility - not desired
+ //! @todo Perhaps we should check here whether id is valid for focusable controls
+ //! such as buttons etc. For labels/fadelabels/images it does not matter
+
+ GetAlignment(pControlNode, "align", labelInfo.align);
+ if (!GetDimensions(pControlNode, "left", "right", "centerleft", "centerright", "width", rect.Width(), posX, width, minWidth))
+ { // didn't get 2 dimensions, so test for old <posx> as well
+ if (GetPosition(pControlNode, "posx", rect.Width(), posX))
+ { // <posx> available, so use it along with any hacks we used to support
+ if (!insideContainer &&
+ type == CGUIControl::GUICONTROL_LABEL &&
+ (labelInfo.align & XBFONT_RIGHT))
+ posX -= width;
+ }
+ if (!width) // no width specified, so compute from parent
+ width = std::max(rect.Width() - posX, 0.0f);
+ }
+ if (!GetDimensions(pControlNode, "top", "bottom", "centertop", "centerbottom", "height", rect.Height(), posY, height, minHeight))
+ {
+ GetPosition(pControlNode, "posy", rect.Height(), posY);
+ if (!height)
+ height = std::max(rect.Height() - posY, 0.0f);
+ }
+
+ XMLUtils::GetFloat(pControlNode, "offsetx", offset.x);
+ XMLUtils::GetFloat(pControlNode, "offsety", offset.y);
+
+ hitRect.SetRect(posX, posY, posX + width, posY + height);
+ GetHitRect(pControlNode, hitRect, rect);
+
+ GetInfoColor(pControlNode, "hitrectcolor", hitColor, parentID);
+
+ GetActions(pControlNode, "onup", actions[ACTION_MOVE_UP]);
+ GetActions(pControlNode, "ondown", actions[ACTION_MOVE_DOWN]);
+ GetActions(pControlNode, "onleft", actions[ACTION_MOVE_LEFT]);
+ GetActions(pControlNode, "onright", actions[ACTION_MOVE_RIGHT]);
+ GetActions(pControlNode, "onnext", actions[ACTION_NEXT_CONTROL]);
+ GetActions(pControlNode, "onprev", actions[ACTION_PREV_CONTROL]);
+ GetActions(pControlNode, "onback", actions[ACTION_NAV_BACK]);
+ GetActions(pControlNode, "oninfo", actions[ACTION_SHOW_INFO]);
+
+ if (XMLUtils::GetInt(pControlNode, "defaultcontrol", defaultControl))
+ {
+ const char *always = pControlNode->FirstChildElement("defaultcontrol")->Attribute("always");
+ if (always && StringUtils::CompareNoCase(always, "true", 4) == 0)
+ defaultAlways = true;
+ }
+ XMLUtils::GetInt(pControlNode, "pagecontrol", pageControl);
+
+ GetInfoColor(pControlNode, "colordiffuse", colorDiffuse, parentID);
+ GetInfoColor(pControlNode, "colorbox", colorBox, parentID);
+
+ GetConditionalVisibility(pControlNode, visibleCondition, allowHiddenFocus);
+ XMLUtils::GetString(pControlNode, "enable", enableCondition);
+
+ CRect animRect(posX, posY, posX + width, posY + height);
+ GetAnimations(pControlNode, animRect, parentID, animations);
+
+ GetInfoColor(pControlNode, "textcolor", labelInfo.textColor, parentID);
+ GetInfoColor(pControlNode, "focusedcolor", labelInfo.focusedColor, parentID);
+ GetInfoColor(pControlNode, "disabledcolor", labelInfo.disabledColor, parentID);
+ GetInfoColor(pControlNode, "shadowcolor", labelInfo.shadowColor, parentID);
+ GetInfoColor(pControlNode, "selectedcolor", labelInfo.selectedColor, parentID);
+ GetInfoColor(pControlNode, "invalidcolor", labelInfo.invalidColor, parentID);
+ XMLUtils::GetFloat(pControlNode, "textoffsetx", labelInfo.offsetX);
+ XMLUtils::GetFloat(pControlNode, "textoffsety", labelInfo.offsetY);
+ int angle = 0; // use the negative angle to compensate for our vertically flipped cartesian plane
+ if (XMLUtils::GetInt(pControlNode, "angle", angle)) labelInfo.angle = (float)-angle;
+ std::string strFont, strMonoFont;
+ if (XMLUtils::GetString(pControlNode, "font", strFont))
+ labelInfo.font = g_fontManager.GetFont(strFont);
+ XMLUtils::GetString(pControlNode, "monofont", strMonoFont);
+ uint32_t alignY = 0;
+ if (GetAlignmentY(pControlNode, "aligny", alignY))
+ labelInfo.align |= alignY;
+ if (XMLUtils::GetFloat(pControlNode, "textwidth", labelInfo.width))
+ labelInfo.align |= XBFONT_TRUNCATED;
+
+ GetActions(pControlNode, "onclick", clickActions);
+ GetActions(pControlNode, "ontextchange", textChangeActions);
+ GetActions(pControlNode, "onfocus", focusActions);
+ GetActions(pControlNode, "onunfocus", unfocusActions);
+ focusActions.EnableSendThreadMessageMode();
+ unfocusActions.EnableSendThreadMessageMode();
+ GetActions(pControlNode, "altclick", altclickActions);
+
+ std::string infoString;
+ if (XMLUtils::GetString(pControlNode, "info", infoString))
+ singleInfo = CServiceBroker::GetGUI()->GetInfoManager().TranslateString(infoString);
+ if (XMLUtils::GetString(pControlNode, "info2", infoString))
+ singleInfo2 = CServiceBroker::GetGUI()->GetInfoManager().TranslateString(infoString);
+
+ GetTexture(pControlNode, "texturefocus", textureFocus);
+ GetTexture(pControlNode, "texturenofocus", textureNoFocus);
+ GetTexture(pControlNode, "alttexturefocus", textureAltFocus);
+ GetTexture(pControlNode, "alttexturenofocus", textureAltNoFocus);
+
+ XMLUtils::GetString(pControlNode, "usealttexture", toggleSelect);
+ XMLUtils::GetString(pControlNode, "selected", toggleSelect);
+
+ XMLUtils::GetBoolean(pControlNode, "haspath", bHasPath);
+
+ GetTexture(pControlNode, "textureup", textureUp);
+ GetTexture(pControlNode, "texturedown", textureDown);
+ GetTexture(pControlNode, "textureupfocus", textureUpFocus);
+ GetTexture(pControlNode, "texturedownfocus", textureDownFocus);
+ GetTexture(pControlNode, "textureupdisabled", textureUpDisabled);
+ GetTexture(pControlNode, "texturedowndisabled", textureDownDisabled);
+
+ XMLUtils::GetFloat(pControlNode, "spinwidth", spinWidth);
+ XMLUtils::GetFloat(pControlNode, "spinheight", spinHeight);
+ XMLUtils::GetFloat(pControlNode, "spinposx", spinPosX);
+ XMLUtils::GetFloat(pControlNode, "spinposy", spinPosY);
+
+ XMLUtils::GetFloat(pControlNode, "sliderwidth", sliderWidth);
+ XMLUtils::GetFloat(pControlNode, "sliderheight", sliderHeight);
+ if (!GetTexture(pControlNode, "textureradioonfocus", textureRadioOnFocus) || !GetTexture(pControlNode, "textureradioonnofocus", textureRadioOnNoFocus))
+ {
+ GetTexture(pControlNode, "textureradiofocus", textureRadioOnFocus); // backward compatibility
+ GetTexture(pControlNode, "textureradioon", textureRadioOnFocus);
+ textureRadioOnNoFocus = textureRadioOnFocus;
+ }
+ if (!GetTexture(pControlNode, "textureradioofffocus", textureRadioOffFocus) || !GetTexture(pControlNode, "textureradiooffnofocus", textureRadioOffNoFocus))
+ {
+ GetTexture(pControlNode, "textureradionofocus", textureRadioOffFocus); // backward compatibility
+ GetTexture(pControlNode, "textureradiooff", textureRadioOffFocus);
+ textureRadioOffNoFocus = textureRadioOffFocus;
+ }
+ GetTexture(pControlNode, "textureradioondisabled", textureRadioOnDisabled);
+ GetTexture(pControlNode, "textureradiooffdisabled", textureRadioOffDisabled);
+ GetTexture(pControlNode, "texturesliderbackground", textureBackground);
+ GetTexture(pControlNode, "texturesliderbar", textureBar);
+ GetTexture(pControlNode, "texturesliderbarfocus", textureBarFocus);
+ GetTexture(pControlNode, "textureslidernib", textureNib);
+ GetTexture(pControlNode, "textureslidernibfocus", textureNibFocus);
+
+ GetTexture(pControlNode, "texturecolormask", textureColorMask);
+ GetTexture(pControlNode, "texturecolordisabledmask", textureColorDisabledMask);
+
+ XMLUtils::GetString(pControlNode, "title", strTitle);
+ XMLUtils::GetString(pControlNode, "tagset", strRSSTags);
+ GetInfoColor(pControlNode, "headlinecolor", headlineColor, parentID);
+ GetInfoColor(pControlNode, "titlecolor", textColor3, parentID);
+
+ if (XMLUtils::GetString(pControlNode, "subtype", strSubType))
+ {
+ StringUtils::ToLower(strSubType);
+
+ if ( strSubType == "int")
+ iType = SPIN_CONTROL_TYPE_INT;
+ else if ( strSubType == "page")
+ iType = SPIN_CONTROL_TYPE_PAGE;
+ else if ( strSubType == "float")
+ iType = SPIN_CONTROL_TYPE_FLOAT;
+ else
+ iType = SPIN_CONTROL_TYPE_TEXT;
+ }
+
+ if (!GetIntRange(pControlNode, "range", iMin, iMax, iInterval))
+ {
+ GetFloatRange(pControlNode, "range", fMin, fMax, fInterval);
+ }
+
+ XMLUtils::GetBoolean(pControlNode, "reverse", bReverse);
+ XMLUtils::GetBoolean(pControlNode, "reveal", bReveal);
+
+ GetTexture(pControlNode, "texturebg", textureBackground);
+ GetTexture(pControlNode, "lefttexture", textureLeft);
+ GetTexture(pControlNode, "midtexture", textureMid);
+ GetTexture(pControlNode, "righttexture", textureRight);
+ GetTexture(pControlNode, "overlaytexture", textureOverlay);
+
+ // the <texture> tag can be overridden by the <info> tag
+ GetInfoTexture(pControlNode, "texture", texture, textureFile, parentID);
+
+ GetTexture(pControlNode, "bordertexture", borderTexture);
+
+ // fade label can have a whole bunch, but most just have one
+ std::vector<GUIINFO::CGUIInfoLabel> infoLabels;
+ GetInfoLabels(pControlNode, "label", infoLabels, parentID);
+
+ GetString(pControlNode, "label", strLabel);
+ GetString(pControlNode, "altlabel", altLabel);
+ GetString(pControlNode, "label2", strLabel2);
+
+ XMLUtils::GetBoolean(pControlNode, "wrapmultiline", wrapMultiLine);
+ XMLUtils::GetInt(pControlNode,"urlset",iUrlSet);
+
+ if ( XMLUtils::GetString(pControlNode, "orientation", strTmp) )
+ {
+ StringUtils::ToLower(strTmp);
+ if (strTmp == "horizontal")
+ orientation = HORIZONTAL;
+ }
+ XMLUtils::GetFloat(pControlNode, "itemgap", buttonGap);
+ XMLUtils::GetInt(pControlNode, "movement", iMovementRange);
+ GetAspectRatio(pControlNode, "aspectratio", aspect);
+
+ bool alwaysScroll;
+ if (XMLUtils::GetBoolean(pControlNode, "scroll", alwaysScroll))
+ scrollValue = alwaysScroll ? CGUIControl::ALWAYS : CGUIControl::NEVER;
+
+ XMLUtils::GetBoolean(pControlNode,"pulseonselect", bPulse);
+ XMLUtils::GetInt(pControlNode, "timeblocks", timeBlocks);
+ XMLUtils::GetInt(pControlNode, "rulerunit", rulerUnit);
+ GetTexture(pControlNode, "progresstexture", textureProgressIndicator);
+
+ GetInfoTexture(pControlNode, "imagepath", texture, texturePath, parentID);
+
+ XMLUtils::GetUInt(pControlNode,"timeperimage", timePerImage);
+ XMLUtils::GetUInt(pControlNode,"fadetime", fadeTime);
+ XMLUtils::GetUInt(pControlNode,"pauseatend", timeToPauseAtEnd);
+ XMLUtils::GetBoolean(pControlNode, "randomize", randomized);
+ XMLUtils::GetBoolean(pControlNode, "loop", loop);
+ XMLUtils::GetBoolean(pControlNode, "scrollout", scrollOut);
+
+ XMLUtils::GetFloat(pControlNode, "radiowidth", radioWidth);
+ XMLUtils::GetFloat(pControlNode, "radioheight", radioHeight);
+ XMLUtils::GetFloat(pControlNode, "radioposx", radioPosX);
+ XMLUtils::GetFloat(pControlNode, "radioposy", radioPosY);
+
+ XMLUtils::GetFloat(pControlNode, "colorwidth", colorWidth);
+ XMLUtils::GetFloat(pControlNode, "colorheight", colorHeight);
+ XMLUtils::GetFloat(pControlNode, "colorposx", colorPosX);
+ XMLUtils::GetFloat(pControlNode, "colorposy", colorPosY);
+
+ std::string borderStr;
+ if (XMLUtils::GetString(pControlNode, "bordersize", borderStr))
+ GetRectFromString(borderStr, borderSize);
+
+ XMLUtils::GetBoolean(pControlNode, "showonepage", showOnePage);
+ XMLUtils::GetInt(pControlNode, "focusposition", focusPosition);
+ XMLUtils::GetInt(pControlNode, "scrolltime", scrollTime);
+ XMLUtils::GetInt(pControlNode, "preloaditems", preloadItems, 0, 2);
+
+ XMLUtils::GetBoolean(pControlNode, "usecontrolcoords", useControlCoords);
+ XMLUtils::GetBoolean(pControlNode, "renderfocusedlast", renderFocusedLast);
+ XMLUtils::GetBoolean(pControlNode, "resetonlabelchange", resetOnLabelChange);
+
+ XMLUtils::GetBoolean(pControlNode, "password", bPassword);
+
+ // view type
+ VIEW_TYPE viewType = VIEW_TYPE_NONE;
+ std::string viewLabel;
+ if (type == CGUIControl::GUICONTAINER_PANEL)
+ {
+ viewType = VIEW_TYPE_ICON;
+ viewLabel = g_localizeStrings.Get(536);
+ }
+ else if (type == CGUIControl::GUICONTAINER_LIST)
+ {
+ viewType = VIEW_TYPE_LIST;
+ viewLabel = g_localizeStrings.Get(535);
+ }
+ else
+ {
+ viewType = VIEW_TYPE_WRAP;
+ viewLabel = g_localizeStrings.Get(541);
+ }
+ TiXmlElement *itemElement = pControlNode->FirstChildElement("viewtype");
+ if (itemElement && itemElement->FirstChild())
+ {
+ std::string type = itemElement->FirstChild()->Value();
+ if (type == "list")
+ viewType = VIEW_TYPE_LIST;
+ else if (type == "icon")
+ viewType = VIEW_TYPE_ICON;
+ else if (type == "biglist")
+ viewType = VIEW_TYPE_BIG_LIST;
+ else if (type == "bigicon")
+ viewType = VIEW_TYPE_BIG_ICON;
+ else if (type == "wide")
+ viewType = VIEW_TYPE_WIDE;
+ else if (type == "bigwide")
+ viewType = VIEW_TYPE_BIG_WIDE;
+ else if (type == "wrap")
+ viewType = VIEW_TYPE_WRAP;
+ else if (type == "bigwrap")
+ viewType = VIEW_TYPE_BIG_WRAP;
+ else if (type == "info")
+ viewType = VIEW_TYPE_INFO;
+ else if (type == "biginfo")
+ viewType = VIEW_TYPE_BIG_INFO;
+ const char *label = itemElement->Attribute("label");
+ if (label)
+ viewLabel = GUIINFO::CGUIInfoLabel::GetLabel(FilterLabel(label), INFO::DEFAULT_CONTEXT);
+ }
+
+ TiXmlElement *cam = pControlNode->FirstChildElement("camera");
+ if (cam)
+ {
+ hasCamera = true;
+ camera.x = ParsePosition(cam->Attribute("x"), width);
+ camera.y = ParsePosition(cam->Attribute("y"), height);
+ }
+
+ if (XMLUtils::GetFloat(pControlNode, "depth", stereo))
+ stereo = std::max(-1.f, std::min(1.f, stereo));
+
+ XMLUtils::GetInt(pControlNode, "scrollspeed", labelInfo.scrollSpeed);
+
+ GetString(pControlNode, "scrollsuffix", labelInfo.scrollSuffix);
+
+ XMLUtils::GetString(pControlNode, "action", action);
+
+ GetMovingSpeedConfig(pControlNode, "movingspeed", movingSpeedCfg);
+
+ /////////////////////////////////////////////////////////////////////////////
+ // Instantiate a new control using the properties gathered above
+ //
+
+ CGUIControl *control = NULL;
+ switch (type)
+ {
+ case CGUIControl::GUICONTROL_GROUP:
+ {
+ if (insideContainer)
+ {
+ control = new CGUIListGroup(parentID, id, posX, posY, width, height);
+ }
+ else
+ {
+ control = new CGUIControlGroup(
+ parentID, id, posX, posY, width, height);
+ static_cast<CGUIControlGroup*>(control)->SetDefaultControl(defaultControl, defaultAlways);
+ static_cast<CGUIControlGroup*>(control)->SetRenderFocusedLast(renderFocusedLast);
+ }
+ }
+ break;
+ case CGUIControl::GUICONTROL_GROUPLIST:
+ {
+ CScroller scroller;
+ GetScroller(pControlNode, "scrolltime", scroller);
+
+ control = new CGUIControlGroupList(
+ parentID, id, posX, posY, width, height, buttonGap, pageControl, orientation, useControlCoords, labelInfo.align, scroller);
+ static_cast<CGUIControlGroup*>(control)->SetDefaultControl(defaultControl, defaultAlways);
+ static_cast<CGUIControlGroup*>(control)->SetRenderFocusedLast(renderFocusedLast);
+ static_cast<CGUIControlGroupList*>(control)->SetMinSize(minWidth, minHeight);
+ }
+ break;
+ case CGUIControl::GUICONTROL_LABEL:
+ {
+ static const GUIINFO::CGUIInfoLabel empty;
+ const GUIINFO::CGUIInfoLabel& content = !infoLabels.empty() ? infoLabels[0] : empty;
+ if (insideContainer)
+ { // inside lists we use CGUIListLabel
+ control = new CGUIListLabel(parentID, id, posX, posY, width, height, labelInfo, content, scrollValue);
+ }
+ else
+ {
+ control = new CGUILabelControl(
+ parentID, id, posX, posY, width, height,
+ labelInfo, wrapMultiLine, bHasPath);
+ static_cast<CGUILabelControl*>(control)->SetInfo(content);
+ static_cast<CGUILabelControl*>(control)->SetWidthControl(minWidth, (scrollValue == CGUIControl::ALWAYS));
+ }
+ }
+ break;
+ case CGUIControl::GUICONTROL_EDIT:
+ {
+ control = new CGUIEditControl(
+ parentID, id, posX, posY, width, height, textureFocus, textureNoFocus,
+ labelInfo, strLabel);
+
+ GUIINFO::CGUIInfoLabel hint_text;
+ GetInfoLabel(pControlNode, "hinttext", hint_text, parentID);
+ static_cast<CGUIEditControl*>(control)->SetHint(hint_text);
+
+ if (bPassword)
+ static_cast<CGUIEditControl*>(control)->SetInputType(CGUIEditControl::INPUT_TYPE_PASSWORD, 0);
+ static_cast<CGUIEditControl*>(control)->SetTextChangeActions(textChangeActions);
+ }
+ break;
+ case CGUIControl::GUICONTROL_VIDEO:
+ {
+ control = new CGUIVideoControl(
+ parentID, id, posX, posY, width, height);
+ }
+ break;
+ case CGUIControl::GUICONTROL_GAME:
+ {
+ control = new RETRO::CGUIGameControl(parentID, id, posX, posY, width, height);
+
+ GUIINFO::CGUIInfoLabel videoFilter;
+ GetInfoLabel(pControlNode, "videofilter", videoFilter, parentID);
+ static_cast<RETRO::CGUIGameControl*>(control)->SetVideoFilter(videoFilter);
+
+ GUIINFO::CGUIInfoLabel stretchMode;
+ GetInfoLabel(pControlNode, "stretchmode", stretchMode, parentID);
+ static_cast<RETRO::CGUIGameControl*>(control)->SetStretchMode(stretchMode);
+
+ GUIINFO::CGUIInfoLabel rotation;
+ GetInfoLabel(pControlNode, "rotation", rotation, parentID);
+ static_cast<RETRO::CGUIGameControl*>(control)->SetRotation(rotation);
+
+ GUIINFO::CGUIInfoLabel pixels;
+ GetInfoLabel(pControlNode, "pixels", pixels, parentID);
+ static_cast<RETRO::CGUIGameControl*>(control)->SetPixels(pixels);
+ }
+ break;
+ case CGUIControl::GUICONTROL_FADELABEL:
+ {
+ control = new CGUIFadeLabelControl(
+ parentID, id, posX, posY, width, height,
+ labelInfo, scrollOut, timeToPauseAtEnd, resetOnLabelChange, randomized);
+
+ static_cast<CGUIFadeLabelControl*>(control)->SetInfo(infoLabels);
+
+ // check whether or not a scroll tag was specified.
+ if (scrollValue != CGUIControl::FOCUS)
+ static_cast<CGUIFadeLabelControl*>(control)->SetScrolling(scrollValue == CGUIControl::ALWAYS);
+ }
+ break;
+ case CGUIControl::GUICONTROL_RSS:
+ {
+ control = new CGUIRSSControl(
+ parentID, id, posX, posY, width, height,
+ labelInfo, textColor3, headlineColor, strRSSTags);
+ RssUrls::const_iterator iter = CRssManager::GetInstance().GetUrls().find(iUrlSet);
+ if (iter != CRssManager::GetInstance().GetUrls().end())
+ static_cast<CGUIRSSControl*>(control)->SetUrlSet(iUrlSet);
+ }
+ break;
+ case CGUIControl::GUICONTROL_BUTTON:
+ {
+ control = new CGUIButtonControl(
+ parentID, id, posX, posY, width, height,
+ textureFocus, textureNoFocus,
+ labelInfo, wrapMultiLine);
+
+ CGUIButtonControl* bcontrol = static_cast<CGUIButtonControl*>(control);
+ bcontrol->SetLabel(strLabel);
+ bcontrol->SetLabel2(strLabel2);
+ bcontrol->SetMinWidth(minWidth);
+ bcontrol->SetClickActions(clickActions);
+ bcontrol->SetFocusActions(focusActions);
+ bcontrol->SetUnFocusActions(unfocusActions);
+ }
+ break;
+ case CGUIControl::GUICONTROL_TOGGLEBUTTON:
+ {
+ control = new CGUIToggleButtonControl(
+ parentID, id, posX, posY, width, height,
+ textureFocus, textureNoFocus,
+ textureAltFocus, textureAltNoFocus,
+ labelInfo, wrapMultiLine);
+
+ CGUIToggleButtonControl* tcontrol = static_cast<CGUIToggleButtonControl*>(control);
+ tcontrol->SetLabel(strLabel);
+ tcontrol->SetAltLabel(altLabel);
+ tcontrol->SetMinWidth(minWidth);
+ tcontrol->SetClickActions(clickActions);
+ tcontrol->SetAltClickActions(altclickActions);
+ tcontrol->SetFocusActions(focusActions);
+ tcontrol->SetUnFocusActions(unfocusActions);
+ tcontrol->SetToggleSelect(toggleSelect);
+ }
+ break;
+ case CGUIControl::GUICONTROL_RADIO:
+ {
+ control = new CGUIRadioButtonControl(
+ parentID, id, posX, posY, width, height,
+ textureFocus, textureNoFocus,
+ labelInfo,
+ textureRadioOnFocus, textureRadioOnNoFocus, textureRadioOffFocus, textureRadioOffNoFocus, textureRadioOnDisabled, textureRadioOffDisabled);
+
+ CGUIRadioButtonControl* rcontrol = static_cast<CGUIRadioButtonControl*>(control);
+ rcontrol->SetLabel(strLabel);
+ rcontrol->SetLabel2(strLabel2);
+ rcontrol->SetRadioDimensions(radioPosX, radioPosY, radioWidth, radioHeight);
+ rcontrol->SetToggleSelect(toggleSelect);
+ rcontrol->SetClickActions(clickActions);
+ rcontrol->SetFocusActions(focusActions);
+ rcontrol->SetUnFocusActions(unfocusActions);
+ }
+ break;
+ case CGUIControl::GUICONTROL_SPIN:
+ {
+ control = new CGUISpinControl(
+ parentID, id, posX, posY, width, height,
+ textureUp, textureDown, textureUpFocus, textureDownFocus,
+ textureUpDisabled, textureDownDisabled,
+ labelInfo, iType);
+
+ CGUISpinControl* scontrol = static_cast<CGUISpinControl*>(control);
+ scontrol->SetReverse(bReverse);
+
+ if (iType == SPIN_CONTROL_TYPE_INT)
+ {
+ scontrol->SetRange(iMin, iMax);
+ }
+ else if (iType == SPIN_CONTROL_TYPE_PAGE)
+ {
+ scontrol->SetRange(iMin, iMax);
+ scontrol->SetShowRange(true);
+ scontrol->SetReverse(false);
+ scontrol->SetShowOnePage(showOnePage);
+ }
+ else if (iType == SPIN_CONTROL_TYPE_FLOAT)
+ {
+ scontrol->SetFloatRange(fMin, fMax);
+ scontrol->SetFloatInterval(fInterval);
+ }
+ }
+ break;
+ case CGUIControl::GUICONTROL_SLIDER:
+ {
+ control = new CGUISliderControl(
+ parentID, id, posX, posY, width, height,
+ textureBar, textureNib, textureNibFocus, SLIDER_CONTROL_TYPE_PERCENTAGE, orientation);
+
+ static_cast<CGUISliderControl*>(control)->SetInfo(singleInfo);
+ static_cast<CGUISliderControl*>(control)->SetAction(action);
+ }
+ break;
+ case CGUIControl::GUICONTROL_SETTINGS_SLIDER:
+ {
+ control = new CGUISettingsSliderControl(
+ parentID, id, posX, posY, width, height, sliderWidth, sliderHeight, textureFocus, textureNoFocus,
+ textureBar, textureNib, textureNibFocus, labelInfo, SLIDER_CONTROL_TYPE_PERCENTAGE);
+
+ static_cast<CGUISettingsSliderControl*>(control)->SetText(strLabel);
+ static_cast<CGUISettingsSliderControl*>(control)->SetInfo(singleInfo);
+ }
+ break;
+ case CGUIControl::GUICONTROL_SCROLLBAR:
+ {
+ control = new GUIScrollBarControl(
+ parentID, id, posX, posY, width, height,
+ textureBackground, textureBar, textureBarFocus, textureNib, textureNibFocus, orientation, showOnePage);
+ }
+ break;
+ case CGUIControl::GUICONTROL_PROGRESS:
+ {
+ control = new CGUIProgressControl(
+ parentID, id, posX, posY, width, height,
+ textureBackground, textureLeft, textureMid, textureRight,
+ textureOverlay, bReveal);
+
+ static_cast<CGUIProgressControl*>(control)->SetInfo(singleInfo, singleInfo2);
+ }
+ break;
+ case CGUIControl::GUICONTROL_RANGES:
+ {
+ control = new CGUIRangesControl(
+ parentID, id, posX, posY, width, height,
+ textureBackground, textureLeft, textureMid, textureRight,
+ textureOverlay, singleInfo);
+ }
+ break;
+ case CGUIControl::GUICONTROL_IMAGE:
+ {
+ // use a bordered texture if we have <bordersize> or <bordertexture> specified.
+ if (borderTexture.filename.empty() && borderStr.empty())
+ control = new CGUIImage(
+ parentID, id, posX, posY, width, height, texture);
+ else
+ control = new CGUIBorderedImage(
+ parentID, id, posX, posY, width, height, texture, borderTexture, borderSize);
+ CGUIImage* icontrol = static_cast<CGUIImage*>(control);
+ icontrol->SetInfo(textureFile);
+ icontrol->SetAspectRatio(aspect);
+ icontrol->SetCrossFade(fadeTime);
+ }
+ break;
+ case CGUIControl::GUICONTROL_MULTI_IMAGE:
+ {
+ control = new CGUIMultiImage(
+ parentID, id, posX, posY, width, height, texture, timePerImage, fadeTime, randomized, loop, timeToPauseAtEnd);
+ static_cast<CGUIMultiImage*>(control)->SetInfo(texturePath);
+ static_cast<CGUIMultiImage*>(control)->SetAspectRatio(aspect);
+ }
+ break;
+ case CGUIControl::GUICONTAINER_LIST:
+ {
+ CScroller scroller;
+ GetScroller(pControlNode, "scrolltime", scroller);
+
+ control = new CGUIListContainer(parentID, id, posX, posY, width, height, orientation, scroller, preloadItems);
+ CGUIListContainer* lcontrol = static_cast<CGUIListContainer*>(control);
+ lcontrol->LoadLayout(pControlNode);
+ lcontrol->LoadListProvider(pControlNode, defaultControl, defaultAlways);
+ lcontrol->SetType(viewType, viewLabel);
+ lcontrol->SetPageControl(pageControl);
+ lcontrol->SetRenderOffset(offset);
+ lcontrol->SetAutoScrolling(pControlNode);
+ lcontrol->SetClickActions(clickActions);
+ lcontrol->SetFocusActions(focusActions);
+ lcontrol->SetUnFocusActions(unfocusActions);
+ }
+ break;
+ case CGUIControl::GUICONTAINER_WRAPLIST:
+ {
+ CScroller scroller;
+ GetScroller(pControlNode, "scrolltime", scroller);
+
+ control = new CGUIWrappingListContainer(parentID, id, posX, posY, width, height, orientation, scroller, preloadItems, focusPosition);
+ CGUIWrappingListContainer* wcontrol = static_cast<CGUIWrappingListContainer*>(control);
+ wcontrol->LoadLayout(pControlNode);
+ wcontrol->LoadListProvider(pControlNode, defaultControl, defaultAlways);
+ wcontrol->SetType(viewType, viewLabel);
+ wcontrol->SetPageControl(pageControl);
+ wcontrol->SetRenderOffset(offset);
+ wcontrol->SetAutoScrolling(pControlNode);
+ wcontrol->SetClickActions(clickActions);
+ wcontrol->SetFocusActions(focusActions);
+ wcontrol->SetUnFocusActions(unfocusActions);
+ }
+ break;
+ case CGUIControl::GUICONTAINER_EPGGRID:
+ {
+ CGUIEPGGridContainer *epgGridContainer = new CGUIEPGGridContainer(parentID, id, posX, posY, width, height, orientation, scrollTime, preloadItems, timeBlocks, rulerUnit, textureProgressIndicator);
+ control = epgGridContainer;
+ epgGridContainer->LoadLayout(pControlNode);
+ epgGridContainer->SetRenderOffset(offset);
+ epgGridContainer->SetType(viewType, viewLabel);
+ epgGridContainer->SetPageControl(pageControl);
+ }
+ break;
+ case CGUIControl::GUICONTAINER_FIXEDLIST:
+ {
+ CScroller scroller;
+ GetScroller(pControlNode, "scrolltime", scroller);
+
+ control = new CGUIFixedListContainer(parentID, id, posX, posY, width, height, orientation, scroller, preloadItems, focusPosition, iMovementRange);
+ CGUIFixedListContainer* fcontrol = static_cast<CGUIFixedListContainer*>(control);
+ fcontrol->LoadLayout(pControlNode);
+ fcontrol->LoadListProvider(pControlNode, defaultControl, defaultAlways);
+ fcontrol->SetType(viewType, viewLabel);
+ fcontrol->SetPageControl(pageControl);
+ fcontrol->SetRenderOffset(offset);
+ fcontrol->SetAutoScrolling(pControlNode);
+ fcontrol->SetClickActions(clickActions);
+ fcontrol->SetFocusActions(focusActions);
+ fcontrol->SetUnFocusActions(unfocusActions);
+ }
+ break;
+ case CGUIControl::GUICONTAINER_PANEL:
+ {
+ CScroller scroller;
+ GetScroller(pControlNode, "scrolltime", scroller);
+
+ control = new CGUIPanelContainer(parentID, id, posX, posY, width, height, orientation, scroller, preloadItems);
+ CGUIPanelContainer* pcontrol = static_cast<CGUIPanelContainer*>(control);
+ pcontrol->LoadLayout(pControlNode);
+ pcontrol->LoadListProvider(pControlNode, defaultControl, defaultAlways);
+ pcontrol->SetType(viewType, viewLabel);
+ pcontrol->SetPageControl(pageControl);
+ pcontrol->SetRenderOffset(offset);
+ pcontrol->SetAutoScrolling(pControlNode);
+ pcontrol->SetClickActions(clickActions);
+ pcontrol->SetFocusActions(focusActions);
+ pcontrol->SetUnFocusActions(unfocusActions);
+ }
+ break;
+ case CGUIControl::GUICONTROL_TEXTBOX:
+ {
+ if (!strMonoFont.empty())
+ {
+ labelInfoMono = labelInfo;
+ labelInfoMono.font = g_fontManager.GetFont(strMonoFont);
+ }
+ control = new CGUITextBox(
+ parentID, id, posX, posY, width, height,
+ labelInfo, scrollTime, strMonoFont.empty() ? nullptr : &labelInfoMono);
+
+ CGUITextBox* tcontrol = static_cast<CGUITextBox*>(control);
+
+ tcontrol->SetPageControl(pageControl);
+ if (infoLabels.size())
+ tcontrol->SetInfo(infoLabels[0]);
+ tcontrol->SetAutoScrolling(pControlNode);
+ tcontrol->SetMinHeight(minHeight);
+ }
+ break;
+ case CGUIControl::GUICONTROL_MOVER:
+ {
+ control = new CGUIMoverControl(parentID, id, posX, posY, width, height, textureFocus,
+ textureNoFocus, movingSpeedCfg);
+ }
+ break;
+ case CGUIControl::GUICONTROL_RESIZE:
+ {
+ control = new CGUIResizeControl(parentID, id, posX, posY, width, height, textureFocus,
+ textureNoFocus, movingSpeedCfg);
+ }
+ break;
+ case CGUIControl::GUICONTROL_SPINEX:
+ {
+ control = new CGUISpinControlEx(
+ parentID, id, posX, posY, width, height, spinWidth, spinHeight,
+ labelInfo, textureFocus, textureNoFocus, textureUp, textureDown, textureUpFocus, textureDownFocus,
+ textureUpDisabled, textureDownDisabled, labelInfo, iType);
+
+ CGUISpinControlEx* scontrol = static_cast<CGUISpinControlEx*>(control);
+ scontrol->SetSpinPosition(spinPosX);
+ scontrol->SetText(strLabel);
+ scontrol->SetReverse(bReverse);
+ }
+ break;
+ case CGUIControl::GUICONTROL_VISUALISATION:
+ control = new CGUIVisualisationControl(parentID, id, posX, posY, width, height);
+ break;
+ case CGUIControl::GUICONTROL_RENDERADDON:
+ control = new CGUIRenderingControl(parentID, id, posX, posY, width, height);
+ break;
+ case CGUIControl::GUICONTROL_GAMECONTROLLER:
+ control = new GAME::CGUIGameController(parentID, id, posX, posY, width, height);
+ break;
+ case CGUIControl::GUICONTROL_COLORBUTTON:
+ {
+ control = new CGUIColorButtonControl(parentID, id, posX, posY, width, height, textureFocus,
+ textureNoFocus, labelInfo, textureColorMask,
+ textureColorDisabledMask);
+
+ CGUIColorButtonControl* rcontrol = static_cast<CGUIColorButtonControl*>(control);
+ rcontrol->SetLabel(strLabel);
+ rcontrol->SetImageBoxColor(colorBox);
+ rcontrol->SetColorDimensions(colorPosX, colorPosY, colorWidth, colorHeight);
+ rcontrol->SetClickActions(clickActions);
+ rcontrol->SetFocusActions(focusActions);
+ rcontrol->SetUnFocusActions(unfocusActions);
+ }
+ break;
+ default:
+ break;
+ }
+
+ // things that apply to all controls
+ if (control)
+ {
+ control->SetHitRect(hitRect, hitColor);
+ control->SetVisibleCondition(visibleCondition, allowHiddenFocus);
+ control->SetEnableCondition(enableCondition);
+ control->SetAnimations(animations);
+ control->SetColorDiffuse(colorDiffuse);
+ control->SetActions(actions);
+ control->SetPulseOnSelect(bPulse);
+ if (hasCamera)
+ control->SetCamera(camera);
+ control->SetStereoFactor(stereo);
+ }
+ return control;
+}
diff --git a/xbmc/guilib/GUIControlFactory.h b/xbmc/guilib/GUIControlFactory.h
new file mode 100644
index 0000000..9cdf56c
--- /dev/null
+++ b/xbmc/guilib/GUIControlFactory.h
@@ -0,0 +1,160 @@
+/*
+ * 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
+
+/*!
+\file GUIControlFactory.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "utils/ColorUtils.h"
+#include "utils/MovingSpeed.h"
+
+#include <string>
+#include <vector>
+
+class CTextureInfo; // forward
+class CAspectRatio;
+class TiXmlNode;
+class CGUIAction;
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+ class CGUIInfoLabel;
+}
+}
+}
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIControlFactory
+{
+public:
+ CGUIControlFactory(void);
+ virtual ~CGUIControlFactory(void);
+ CGUIControl* Create(int parentID, const CRect &rect, TiXmlElement* pControlNode, bool insideContainer = false);
+
+ /*! \brief translate from control name to control type
+ \param type name of the control
+ \return type of control
+ */
+ static CGUIControl::GUICONTROLTYPES TranslateControlType(const std::string &type);
+
+ /*! \brief translate from control type to control name
+ \param type type of the control
+ \return name of control
+ */
+ static std::string TranslateControlType(CGUIControl::GUICONTROLTYPES type);
+
+ static bool GetAspectRatio(const TiXmlNode* pRootNode, const char* strTag, CAspectRatio &aspectRatio);
+ static bool GetInfoTexture(const TiXmlNode* pRootNode, const char* strTag, CTextureInfo &image, KODI::GUILIB::GUIINFO::CGUIInfoLabel &info, int parentID);
+ static bool GetTexture(const TiXmlNode* pRootNode, const char* strTag, CTextureInfo &image);
+ static bool GetAlignment(const TiXmlNode* pRootNode, const char* strTag, uint32_t& dwAlignment);
+ static bool GetAlignmentY(const TiXmlNode* pRootNode, const char* strTag, uint32_t& dwAlignment);
+ static bool GetAnimations(TiXmlNode *control, const CRect &rect, int context, std::vector<CAnimation> &animation);
+
+ /*! \brief Create an info label from an XML element
+ Processes XML elements of the form
+ <xmltag fallback="fallback_value">info_value</xmltag>
+ where info_value may use $INFO[], $LOCALIZE[], $NUMBER[] etc.
+ If either the fallback_value or info_value are natural numbers they are interpreted
+ as ids for lookup in strings.po. The fallback attribute is optional.
+ \param element XML element to process
+ \param infoLabel Returned infoLabel
+ \param parentID The parent id
+ \return true if a valid info label was read, false otherwise
+ */
+
+ /*! \brief Parse a position string
+ Handles strings of the form
+ ### number of pixels
+ ###r number of pixels measured from the right
+ ###% percentage of parent size
+ \param pos the string to parse.
+ \param parentSize the size of the parent.
+ \sa GetPosition
+ */
+ static float ParsePosition(const char* pos, const float parentSize);
+
+ static bool GetInfoLabelFromElement(const TiXmlElement *element, KODI::GUILIB::GUIINFO::CGUIInfoLabel &infoLabel, int parentID);
+ static void GetInfoLabel(const TiXmlNode *pControlNode, const std::string &labelTag, KODI::GUILIB::GUIINFO::CGUIInfoLabel &infoLabel, int parentID);
+ static void GetInfoLabels(const TiXmlNode *pControlNode, const std::string &labelTag, std::vector<KODI::GUILIB::GUIINFO::CGUIInfoLabel> &infoLabels, int parentID);
+ static bool GetColor(const TiXmlNode* pRootNode, const char* strTag, UTILS::COLOR::Color& value);
+ static bool GetInfoColor(const TiXmlNode* pRootNode, const char* strTag, KODI::GUILIB::GUIINFO::CGUIInfoColor &value, int parentID);
+ static std::string FilterLabel(const std::string &label);
+ static bool GetConditionalVisibility(const TiXmlNode* control, std::string &condition);
+ static bool GetActions(const TiXmlNode* pRootNode, const char* strTag, CGUIAction& actions);
+ static void GetRectFromString(const std::string &string, CRect &rect);
+ static bool GetHitRect(const TiXmlNode* pRootNode, CRect &rect, const CRect &parentRect);
+ static bool GetScroller(const TiXmlNode *pControlNode, const std::string &scrollerTag, CScroller& scroller);
+private:
+ static std::string GetType(const TiXmlElement *pControlNode);
+ static bool GetMovingSpeedConfig(const TiXmlNode* pRootNode,
+ const char* strTag,
+ UTILS::MOVING_SPEED::MapEventConfig& movingSpeedCfg);
+ static bool GetConditionalVisibility(const TiXmlNode* control, std::string &condition, std::string &allowHiddenFocus);
+ bool GetString(const TiXmlNode* pRootNode, const char* strTag, std::string& strString);
+ static bool GetFloatRange(const TiXmlNode* pRootNode, const char* strTag, float& iMinValue, float& iMaxValue, float& iIntervalValue);
+ static bool GetIntRange(const TiXmlNode* pRootNode, const char* strTag, int& iMinValue, int& iMaxValue, int& iIntervalValue);
+
+ /*! \brief Get the value of a position tag from XML
+ Handles both absolute and relative values.
+ \param node the <control> XML node.
+ \param tag the XML node to parse.
+ \param parentSize the size of the parent, for relative positioning.
+ \param value [out] the returned value.
+ \sa ParsePosition, GetDimension, GetDimensions.
+ */
+ static bool GetPosition(const TiXmlNode *node, const char* tag, const float parentSize, float& value);
+
+ /*! \brief grab a dimension out of the XML
+
+ Supports plain reading of a number (or constant) and, in addition allows "auto" as the value
+ for the dimension, whereby value is set to the max attribute (if it exists) and min is set the min
+ attribute (if it exists) or 1. Auto values are thus detected by min != 0.
+
+ \param node the <control> XML node to read
+ \param strTag tag within node to read
+ \param parentSize the size of the parent for relative sizing.
+ \param value value to set, or maximum value if using auto
+ \param min minimum value - set != 0 if auto is used.
+ \return true if we found and read the tag.
+ \sa GetPosition, GetDimensions, ParsePosition.
+ */
+ static bool GetDimension(const TiXmlNode *node, const char* strTag, const float parentSize, float &value, float &min);
+
+ /*! \brief Retrieve the dimensions for a control.
+
+ Handles positioning based on at least 2 of left/right/center/width.
+
+ \param node the <control> node describing the control.
+ \param leftTag the tag that holds the left field.
+ \param rightTag the tag that holds the right field.
+ \param centerLeftTag the tag that holds the center left field.
+ \param centerRightTag the tag that holds the center right field.
+ \param widthTag the tag holding the width.
+ \param parentSize the size of the parent, for relative sizing.
+ \param pos [out] the discovered position.
+ \param width [out] the discovered width.
+ \param min_width [out] the discovered minimum width.
+ \return true if we can successfully derive the position and size, false otherwise.
+ \sa GetDimension, GetPosition, ParsePosition.
+ */
+ static bool GetDimensions(const TiXmlNode *node, const char *leftTag, const char *rightTag, const char *centerLeftTag,
+ const char *centerRightTag, const char *widthTag, const float parentSize, float &left,
+ float &width, float &min_width);
+};
+
diff --git a/xbmc/guilib/GUIControlGroup.cpp b/xbmc/guilib/GUIControlGroup.cpp
new file mode 100644
index 0000000..f737f9f
--- /dev/null
+++ b/xbmc/guilib/GUIControlGroup.cpp
@@ -0,0 +1,537 @@
+/*
+ * 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 "GUIControlGroup.h"
+
+#include "GUIMessage.h"
+
+#include <cassert>
+#include <utility>
+
+CGUIControlGroup::CGUIControlGroup()
+{
+ m_defaultControl = 0;
+ m_defaultAlways = false;
+ m_focusedControl = 0;
+ m_renderFocusedLast = false;
+ ControlType = GUICONTROL_GROUP;
+}
+
+CGUIControlGroup::CGUIControlGroup(int parentID, int controlID, float posX, float posY, float width, float height)
+: CGUIControlLookup(parentID, controlID, posX, posY, width, height)
+{
+ m_defaultControl = 0;
+ m_defaultAlways = false;
+ m_focusedControl = 0;
+ m_renderFocusedLast = false;
+ ControlType = GUICONTROL_GROUP;
+}
+
+CGUIControlGroup::CGUIControlGroup(const CGUIControlGroup &from)
+: CGUIControlLookup(from)
+{
+ m_defaultControl = from.m_defaultControl;
+ m_defaultAlways = from.m_defaultAlways;
+ m_renderFocusedLast = from.m_renderFocusedLast;
+
+ // run through and add our controls
+ for (auto *i : from.m_children)
+ AddControl(i->Clone());
+
+ // defaults
+ m_focusedControl = 0;
+ ControlType = GUICONTROL_GROUP;
+}
+
+CGUIControlGroup::~CGUIControlGroup(void)
+{
+ ClearAll();
+}
+
+void CGUIControlGroup::AllocResources()
+{
+ CGUIControl::AllocResources();
+ for (auto *control : m_children)
+ {
+ if (!control->IsDynamicallyAllocated())
+ control->AllocResources();
+ }
+}
+
+void CGUIControlGroup::FreeResources(bool immediately)
+{
+ CGUIControl::FreeResources(immediately);
+ for (auto *control : m_children)
+ {
+ control->FreeResources(immediately);
+ }
+}
+
+void CGUIControlGroup::DynamicResourceAlloc(bool bOnOff)
+{
+ for (auto *control : m_children)
+ {
+ control->DynamicResourceAlloc(bOnOff);
+ }
+}
+
+void CGUIControlGroup::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ CPoint pos(GetPosition());
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(pos.x, pos.y);
+
+ CRect rect;
+ for (auto *control : m_children)
+ {
+ control->UpdateVisibility(nullptr);
+ unsigned int oldDirty = dirtyregions.size();
+ control->DoProcess(currentTime, dirtyregions);
+ if (control->IsVisible() || (oldDirty != dirtyregions.size())) // visible or dirty (was visible?)
+ rect.Union(control->GetRenderRegion());
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
+ CGUIControl::Process(currentTime, dirtyregions);
+ m_renderRegion = rect;
+}
+
+void CGUIControlGroup::Render()
+{
+ CPoint pos(GetPosition());
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(pos.x, pos.y);
+ CGUIControl *focusedControl = NULL;
+ for (auto *control : m_children)
+ {
+ if (m_renderFocusedLast && control->HasFocus())
+ focusedControl = control;
+ else
+ control->DoRender();
+ }
+ if (focusedControl)
+ focusedControl->DoRender();
+ CGUIControl::Render();
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
+}
+
+void CGUIControlGroup::RenderEx()
+{
+ for (auto *control : m_children)
+ control->RenderEx();
+ CGUIControl::RenderEx();
+}
+
+bool CGUIControlGroup::OnAction(const CAction &action)
+{
+ assert(false); // unimplemented
+ return false;
+}
+
+bool CGUIControlGroup::HasFocus() const
+{
+ for (auto *control : m_children)
+ {
+ if (control->HasFocus())
+ return true;
+ }
+ return false;
+}
+
+bool CGUIControlGroup::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage() )
+ {
+ case GUI_MSG_ITEM_SELECT:
+ {
+ if (message.GetControlId() == GetID())
+ {
+ m_focusedControl = message.GetParam1();
+ return true;
+ }
+ break;
+ }
+ case GUI_MSG_ITEM_SELECTED:
+ {
+ if (message.GetControlId() == GetID())
+ {
+ message.SetParam1(m_focusedControl);
+ return true;
+ }
+ break;
+ }
+ case GUI_MSG_FOCUSED:
+ { // a control has been focused
+ m_focusedControl = message.GetControlId();
+ SetFocus(true);
+ // tell our parent thatwe have focus
+ if (m_parentControl)
+ m_parentControl->OnMessage(message);
+ return true;
+ }
+ case GUI_MSG_SETFOCUS:
+ {
+ // first try our last focused control...
+ if (!m_defaultAlways && m_focusedControl)
+ {
+ CGUIControl *control = GetFirstFocusableControl(m_focusedControl);
+ if (control)
+ {
+ CGUIMessage msg(GUI_MSG_SETFOCUS, GetParentID(), control->GetID());
+ return control->OnMessage(msg);
+ }
+ }
+ // ok, no previously focused control, try the default control first
+ if (m_defaultControl)
+ {
+ CGUIControl *control = GetFirstFocusableControl(m_defaultControl);
+ if (control)
+ {
+ CGUIMessage msg(GUI_MSG_SETFOCUS, GetParentID(), control->GetID());
+ return control->OnMessage(msg);
+ }
+ }
+ // no success with the default control, so just find one to focus
+ CGUIControl *control = GetFirstFocusableControl(0);
+ if (control)
+ {
+ CGUIMessage msg(GUI_MSG_SETFOCUS, GetParentID(), control->GetID());
+ return control->OnMessage(msg);
+ }
+ // unsuccessful
+ return false;
+ break;
+ }
+ case GUI_MSG_LOSTFOCUS:
+ {
+ // set all subcontrols unfocused
+ for (auto *control : m_children)
+ control->SetFocus(false);
+ if (!GetControl(message.GetParam1()))
+ { // we don't have the new id, so unfocus
+ SetFocus(false);
+ if (m_parentControl)
+ m_parentControl->OnMessage(message);
+ }
+ return true;
+ }
+ break;
+ case GUI_MSG_PAGE_CHANGE:
+ case GUI_MSG_REFRESH_THUMBS:
+ case GUI_MSG_REFRESH_LIST:
+ case GUI_MSG_WINDOW_RESIZE:
+ { // send to all child controls (make sure the target is the control id)
+ for (auto *control : m_children)
+ {
+ CGUIMessage msg(message.GetMessage(), message.GetSenderId(), control->GetID(), message.GetParam1());
+ control->OnMessage(msg);
+ }
+ return true;
+ }
+ break;
+ case GUI_MSG_REFRESH_TIMER:
+ if (!IsVisible() || !IsVisibleFromSkin())
+ return true;
+ break;
+ }
+ bool handled(false);
+ //not intended for any specific control, send to all childs and our base handler.
+ if (message.GetControlId() == 0)
+ {
+ for (auto *control : m_children)
+ {
+ handled |= control->OnMessage(message);
+ }
+ return CGUIControl::OnMessage(message) || handled;
+ }
+ // if it's intended for us, then so be it
+ if (message.GetControlId() == GetID())
+ return CGUIControl::OnMessage(message);
+
+ return SendControlMessage(message);
+}
+
+bool CGUIControlGroup::SendControlMessage(CGUIMessage &message)
+{
+ IDCollector collector(m_idCollector);
+
+ CGUIControl *ctrl(GetControl(message.GetControlId(), collector.m_collector));
+ // see if a child matches, and send to the child control if so
+ if (ctrl && ctrl->OnMessage(message))
+ return true;
+
+ // Unhandled - send to all matching invisible controls as well
+ bool handled(false);
+ for (auto *control : *collector.m_collector)
+ if (control->OnMessage(message))
+ handled = true;
+
+ return handled;
+}
+
+bool CGUIControlGroup::CanFocus() const
+{
+ if (!CGUIControl::CanFocus()) return false;
+ // see if we have any children that can be focused
+ for (auto *control : m_children)
+ {
+ if (control->CanFocus())
+ return true;
+ }
+ return false;
+}
+
+void CGUIControlGroup::SetInitialVisibility()
+{
+ CGUIControl::SetInitialVisibility();
+ for (auto *control : m_children)
+ control->SetInitialVisibility();
+}
+
+void CGUIControlGroup::QueueAnimation(ANIMATION_TYPE animType)
+{
+ CGUIControl::QueueAnimation(animType);
+ // send window level animations to our children as well
+ if (animType == ANIM_TYPE_WINDOW_OPEN || animType == ANIM_TYPE_WINDOW_CLOSE)
+ {
+ for (auto *control : m_children)
+ control->QueueAnimation(animType);
+ }
+}
+
+void CGUIControlGroup::ResetAnimation(ANIMATION_TYPE animType)
+{
+ CGUIControl::ResetAnimation(animType);
+ // send window level animations to our children as well
+ if (animType == ANIM_TYPE_WINDOW_OPEN || animType == ANIM_TYPE_WINDOW_CLOSE)
+ {
+ for (auto *control : m_children)
+ control->ResetAnimation(animType);
+ }
+}
+
+void CGUIControlGroup::ResetAnimations()
+{ // resets all animations, regardless of condition
+ CGUIControl::ResetAnimations();
+ for (auto *control : m_children)
+ control->ResetAnimations();
+}
+
+bool CGUIControlGroup::IsAnimating(ANIMATION_TYPE animType)
+{
+ if (CGUIControl::IsAnimating(animType))
+ return true;
+
+ if (IsVisible())
+ {
+ for (auto *control : m_children)
+ {
+ if (control->IsAnimating(animType))
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIControlGroup::HasAnimation(ANIMATION_TYPE animType)
+{
+ if (CGUIControl::HasAnimation(animType))
+ return true;
+
+ if (IsVisible())
+ {
+ for (auto *control : m_children)
+ {
+ if (control->HasAnimation(animType))
+ return true;
+ }
+ }
+ return false;
+}
+
+EVENT_RESULT CGUIControlGroup::SendMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ // transform our position into child coordinates
+ CPoint childPoint(point);
+ m_transform.InverseTransformPosition(childPoint.x, childPoint.y);
+
+ if (CGUIControl::CanFocus())
+ {
+ CPoint pos(GetPosition());
+ // run through our controls in reverse order (so that last rendered is checked first)
+ for (rControls i = m_children.rbegin(); i != m_children.rend(); ++i)
+ {
+ CGUIControl *child = *i;
+ EVENT_RESULT ret = child->SendMouseEvent(childPoint - pos, event);
+ if (ret)
+ { // we've handled the action, and/or have focused an item
+ return ret;
+ }
+ }
+ // none of our children want the event, but we may want it.
+ EVENT_RESULT ret;
+ if (HitTest(childPoint) && (ret = OnMouseEvent(childPoint, event)))
+ return ret;
+ }
+ m_focusedControl = 0;
+ return EVENT_RESULT_UNHANDLED;
+}
+
+void CGUIControlGroup::UnfocusFromPoint(const CPoint &point)
+{
+ CPoint controlCoords(point);
+ m_transform.InverseTransformPosition(controlCoords.x, controlCoords.y);
+ controlCoords -= GetPosition();
+ for (auto *child : m_children)
+ {
+ child->UnfocusFromPoint(controlCoords);
+ }
+ CGUIControl::UnfocusFromPoint(point);
+}
+
+int CGUIControlGroup::GetFocusedControlID() const
+{
+ if (m_focusedControl) return m_focusedControl;
+ CGUIControl *control = GetFocusedControl();
+ if (control) return control->GetID();
+ return 0;
+}
+
+CGUIControl *CGUIControlGroup::GetFocusedControl() const
+{
+ // try lookup first
+ if (m_focusedControl)
+ {
+ // we may have multiple controls with same id - we pick first that has focus
+ std::pair<LookupMap::const_iterator, LookupMap::const_iterator> range = GetLookupControls(m_focusedControl);
+
+ for (LookupMap::const_iterator i = range.first; i != range.second; ++i)
+ {
+ if (i->second->HasFocus())
+ return i->second;
+ }
+ }
+
+ // if lookup didn't find focused control, iterate m_children to find it
+ for (auto *control : m_children)
+ {
+ // Avoid calling HasFocus() on control group as it will (possibly) recursively
+ // traverse entire group tree just to check if there is focused control.
+ // We are recursively traversing it here so no point in doing it twice.
+ CGUIControlGroup *groupControl(dynamic_cast<CGUIControlGroup*>(control));
+ if (groupControl)
+ {
+ CGUIControl* focusedControl = groupControl->GetFocusedControl();
+ if (focusedControl)
+ return focusedControl;
+ }
+ else if (control->HasFocus())
+ return control;
+ }
+ return NULL;
+}
+
+// in the case of id == 0, we don't match id
+CGUIControl *CGUIControlGroup::GetFirstFocusableControl(int id)
+{
+ if (!CanFocus()) return NULL;
+ if (id && id == GetID()) return this; // we're focusable and they want us
+ for (auto *pControl : m_children)
+ {
+ CGUIControlGroup *group(dynamic_cast<CGUIControlGroup*>(pControl));
+ if (group)
+ {
+ CGUIControl *control = group->GetFirstFocusableControl(id);
+ if (control) return control;
+ }
+ if ((!id || pControl->GetID() == id) && pControl->CanFocus())
+ return pControl;
+ }
+ return NULL;
+}
+
+void CGUIControlGroup::AddControl(CGUIControl *control, int position /* = -1*/)
+{
+ if (!control) return;
+ if (position < 0 || position > (int)m_children.size())
+ position = (int)m_children.size();
+ m_children.insert(m_children.begin() + position, control);
+ control->SetParentControl(this);
+ control->SetControlStats(m_controlStats);
+ control->SetPushUpdates(m_pushedUpdates);
+ AddLookup(control);
+ SetInvalid();
+}
+
+bool CGUIControlGroup::InsertControl(CGUIControl *control, const CGUIControl *insertPoint)
+{
+ // find our position
+ for (unsigned int i = 0; i < m_children.size(); i++)
+ {
+ CGUIControl *child = m_children[i];
+ CGUIControlGroup *group(dynamic_cast<CGUIControlGroup*>(child));
+ if (group && group->InsertControl(control, insertPoint))
+ return true;
+ else if (child == insertPoint)
+ {
+ AddControl(control, i);
+ return true;
+ }
+ }
+ return false;
+}
+
+void CGUIControlGroup::SaveStates(std::vector<CControlState> &states)
+{
+ // save our state, and that of our children
+ states.emplace_back(GetID(), m_focusedControl);
+ for (auto *control : m_children)
+ control->SaveStates(states);
+}
+
+// Note: This routine doesn't delete the control. It just removes it from the control list
+bool CGUIControlGroup::RemoveControl(const CGUIControl *control)
+{
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *child = *it;
+ CGUIControlGroup *group(dynamic_cast<CGUIControlGroup*>(child));
+ if (group && group->RemoveControl(control))
+ return true;
+ if (control == child)
+ {
+ m_children.erase(it);
+ RemoveLookup(child);
+ SetInvalid();
+ return true;
+ }
+ }
+ return false;
+}
+
+void CGUIControlGroup::ClearAll()
+{
+ // first remove from the lookup table
+ RemoveLookup();
+
+ // and delete all our children
+ for (auto *control : m_children)
+ {
+ delete control;
+ }
+ m_focusedControl = 0;
+ m_children.clear();
+ ClearLookup();
+ SetInvalid();
+}
+
+#ifdef _DEBUG
+void CGUIControlGroup::DumpTextureUse()
+{
+ for (auto *control : m_children)
+ control->DumpTextureUse();
+}
+#endif
diff --git a/xbmc/guilib/GUIControlGroup.dox b/xbmc/guilib/GUIControlGroup.dox
new file mode 100644
index 0000000..66ac520
--- /dev/null
+++ b/xbmc/guilib/GUIControlGroup.dox
@@ -0,0 +1,108 @@
+/*!
+
+\page Group_Control Group Control
+\brief **Used to group controls together.**
+
+\tableofcontents
+
+The group control is one of the most important controls. It allows you to group
+controls together, applying attributes to all of them at once. It also remembers
+the last navigated button in the group, so you can set the <b>`<onup>`</b> of a control
+to a group of controls to have it always go back to the one you were at before.
+It also allows you to position controls more accurately relative to each other,
+as any controls within a group take their coordinates from the group's top left
+corner (or from elsewhere if you use the "r" attribute). You can have as many
+groups as you like within the skin, and groups within groups are handled with
+no issues.
+
+
+--------------------------------------------------------------------------------
+\section Group_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="group" id="17">
+ <description>My first group control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>30</height>
+ <defaultcontrol>2</defaultcontrol>
+ <visible>true</visible>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+ ... more controls go here ...
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Group_Control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|------------------:|:--------------------------------------------------------------|
+| defaultcontrol | Specifies the default control that will be focused within the group when the group receives focus. Note that the group remembers it's previously focused item and will return to it.
+
+
+--------------------------------------------------------------------------------
+\section Group_Control_sect3 Notes on positioning of controls within groups
+
+All controls within a group take their positions relative to the group's placement.
+Thus, the group always requires its <b>`<posx>`</b>, <b>`<posy>`</b>,
+<b>`<width>`</b>, and <b>`<height>`</b> attributes to be defined.
+As this can be a pain to remember, anything that you don't specify will be
+inherited from it's parent group (or the main window).
+
+By way of example, consider the first group within a PAL full screen window
+(720x576), and suppose we have
+
+~~~~~~~~~~~~~
+<control type="group" id="15">
+ <posx>30</posx>
+ <posy>70</posy>
+ <width>400</width>
+ ... more controls go here ...
+</control>
+~~~~~~~~~~~~~
+
+so that the <b>`<height>`</b> hasn't been defined. Then Kodi will
+automatically set the <b>`<height>`</b> equal to 506 by inheriting this
+from the window's height of 576, less the <b>`<posy>`</b> amount.
+
+You can align controls within a group to the right edge of the group, by
+using the <b>"r"</b> modifier to the <b>`<posx>`</b> and <b>`<posy>`</b> fields
+
+~~~~~~~~~~~~~
+<control type="group" id="20">
+ <control type="button" id=2>
+ <posx>180r</posx>
+ <width>180</width>
+ </control>
+ <control type="button" id=3>
+ <posx>180r</posx>
+ <width>180</width>
+ </control>
+ <control type="button" id=4>
+ <posx>180r</posx>
+ <width>180</width>
+ </control>
+</control>
+~~~~~~~~~~~~~
+
+All the buttons have width 180, and are aligned 180 pixels from the right edge of the group they're within.
+
+
+--------------------------------------------------------------------------------
+\section Group_Control_sect4 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIControlGroup.h b/xbmc/guilib/GUIControlGroup.h
new file mode 100644
index 0000000..5f4a761
--- /dev/null
+++ b/xbmc/guilib/GUIControlGroup.h
@@ -0,0 +1,124 @@
+/*
+ * 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
+
+/*!
+\file GUIControlGroup.h
+\brief
+*/
+
+#include "GUIControlLookup.h"
+
+#include <vector>
+
+/*!
+ \ingroup controls
+ \brief group of controls, useful for remembering last control + animating/hiding together
+ */
+class CGUIControlGroup : public CGUIControlLookup
+{
+public:
+ CGUIControlGroup();
+ CGUIControlGroup(int parentID, int controlID, float posX, float posY, float width, float height);
+ explicit CGUIControlGroup(const CGUIControlGroup& from);
+ ~CGUIControlGroup(void) override;
+ CGUIControlGroup* Clone() const override { return new CGUIControlGroup(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ void RenderEx() override;
+ bool OnAction(const CAction &action) override;
+ bool OnMessage(CGUIMessage& message) override;
+ virtual bool SendControlMessage(CGUIMessage& message);
+ bool HasFocus() const override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ bool CanFocus() const override;
+
+ EVENT_RESULT SendMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ void UnfocusFromPoint(const CPoint &point) override;
+
+ void SetInitialVisibility() override;
+
+ bool IsAnimating(ANIMATION_TYPE anim) override;
+ bool HasAnimation(ANIMATION_TYPE anim) override;
+ void QueueAnimation(ANIMATION_TYPE anim) override;
+ void ResetAnimation(ANIMATION_TYPE anim) override;
+ void ResetAnimations() override;
+
+ int GetFocusedControlID() const;
+ CGUIControl *GetFocusedControl() const;
+ virtual CGUIControl *GetFirstFocusableControl(int id);
+
+ virtual void AddControl(CGUIControl *control, int position = -1);
+ bool InsertControl(CGUIControl *control, const CGUIControl *insertPoint);
+ virtual bool RemoveControl(const CGUIControl *control);
+ virtual void ClearAll();
+ void SetDefaultControl(int id, bool always)
+ {
+ m_defaultControl = id;
+ m_defaultAlways = always;
+ }
+ void SetRenderFocusedLast(bool renderLast) { m_renderFocusedLast = renderLast; }
+
+ void SaveStates(std::vector<CControlState> &states) override;
+
+ bool IsGroup() const override { return true; }
+
+#ifdef _DEBUG
+ void DumpTextureUse() override;
+#endif
+protected:
+ // sub controls
+ std::vector<CGUIControl *> m_children;
+
+ typedef std::vector<CGUIControl *>::iterator iControls;
+ typedef std::vector<CGUIControl *>::const_iterator ciControls;
+ typedef std::vector<CGUIControl *>::reverse_iterator rControls;
+ typedef std::vector<CGUIControl *>::const_reverse_iterator crControls;
+
+ int m_defaultControl;
+ bool m_defaultAlways;
+ int m_focusedControl;
+ bool m_renderFocusedLast;
+private:
+ typedef std::vector< std::vector<CGUIControl *> * > COLLECTORTYPE;
+
+ struct IDCollectorList
+ {
+ ~IDCollectorList()
+ {
+ for (auto item : m_items)
+ delete item;
+ }
+
+ std::vector<CGUIControl *> *Get() {
+ if (++m_stackDepth > m_items.size())
+ m_items.push_back(new std::vector<CGUIControl *>());
+ return m_items[m_stackDepth - 1];
+ }
+
+ void Release() { --m_stackDepth; }
+
+ COLLECTORTYPE m_items;
+ size_t m_stackDepth = 0;
+ }m_idCollector;
+
+ struct IDCollector
+ {
+ explicit IDCollector(IDCollectorList& list) : m_list(list), m_collector(list.Get()) {}
+
+ ~IDCollector() { m_list.Release(); }
+
+ IDCollectorList &m_list;
+ std::vector<CGUIControl *> *m_collector;
+ };
+};
+
diff --git a/xbmc/guilib/GUIControlGroupList.cpp b/xbmc/guilib/GUIControlGroupList.cpp
new file mode 100644
index 0000000..5252d67
--- /dev/null
+++ b/xbmc/guilib/GUIControlGroupList.cpp
@@ -0,0 +1,601 @@
+/*
+ * 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 "GUIControlGroupList.h"
+
+#include "GUIAction.h"
+#include "GUIControlProfiler.h"
+#include "GUIFont.h" // for XBFONT_* definitions
+#include "GUIMessage.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/Key.h"
+#include "utils/StringUtils.h"
+
+CGUIControlGroupList::CGUIControlGroupList(int parentID, int controlID, float posX, float posY, float width, float height, float itemGap, int pageControl, ORIENTATION orientation, bool useControlPositions, uint32_t alignment, const CScroller& scroller)
+: CGUIControlGroup(parentID, controlID, posX, posY, width, height)
+, m_scroller(scroller)
+{
+ m_itemGap = itemGap;
+ m_pageControl = pageControl;
+ m_focusedPosition = 0;
+ m_totalSize = 0;
+ m_orientation = orientation;
+ m_alignment = alignment;
+ m_lastScrollerValue = -1;
+ m_useControlPositions = useControlPositions;
+ ControlType = GUICONTROL_GROUPLIST;
+ m_minSize = 0;
+}
+
+CGUIControlGroupList::~CGUIControlGroupList(void) = default;
+
+void CGUIControlGroupList::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_scroller.Update(currentTime))
+ MarkDirtyRegion();
+
+ // first we update visibility of all our items, to ensure our size and
+ // alignment computations are correct.
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *control = *it;
+ GUIPROFILER_VISIBILITY_BEGIN(control);
+ control->UpdateVisibility(nullptr);
+ GUIPROFILER_VISIBILITY_END(control);
+ }
+
+ // visibility status of some of the list items may have changed. Thus, the group list size
+ // may now be different and the scroller needs to be updated
+ int previousTotalSize = m_totalSize;
+ ValidateOffset(); // m_totalSize is updated here
+ bool sizeChanged = previousTotalSize != m_totalSize;
+
+ if (m_pageControl && (m_lastScrollerValue != m_scroller.GetValue() || sizeChanged))
+ {
+ CGUIMessage message(GUI_MSG_LABEL_RESET, GetParentID(), m_pageControl, (int)Size(), (int)m_totalSize);
+ SendWindowMessage(message);
+ CGUIMessage message2(GUI_MSG_ITEM_SELECT, GetParentID(), m_pageControl, (int)m_scroller.GetValue());
+ SendWindowMessage(message2);
+ m_lastScrollerValue = static_cast<int>(m_scroller.GetValue());
+ }
+ // we run through the controls, rendering as we go
+ int index = 0;
+ float pos = GetAlignOffset();
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ // note we render all controls, even if they're offscreen, as then they'll be updated
+ // with respect to animations
+ CGUIControl *control = *it;
+ if (m_orientation == VERTICAL)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + pos - m_scroller.GetValue());
+ else
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + pos - m_scroller.GetValue(), m_posY);
+ control->DoProcess(currentTime, dirtyregions);
+
+ if (control->IsVisible())
+ {
+ if (IsControlOnScreen(pos, control))
+ {
+ if (control->HasFocus())
+ m_focusedPosition = index;
+ index++;
+ }
+
+ pos += Size(control) + m_itemGap;
+ }
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
+ }
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIControlGroupList::Render()
+{
+ // we run through the controls, rendering as we go
+ bool render(CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height));
+ float pos = GetAlignOffset();
+ float focusedPos = 0;
+ CGUIControl *focusedControl = NULL;
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ // note we render all controls, even if they're offscreen, as then they'll be updated
+ // with respect to animations
+ CGUIControl *control = *it;
+ if (m_renderFocusedLast && control->HasFocus())
+ {
+ focusedControl = control;
+ focusedPos = pos;
+ }
+ else
+ {
+ if (m_orientation == VERTICAL)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + pos - m_scroller.GetValue());
+ else
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + pos - m_scroller.GetValue(), m_posY);
+ control->DoRender();
+ }
+ if (control->IsVisible())
+ pos += Size(control) + m_itemGap;
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
+ }
+ if (focusedControl)
+ {
+ if (m_orientation == VERTICAL)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + focusedPos - m_scroller.GetValue());
+ else
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + focusedPos - m_scroller.GetValue(), m_posY);
+ focusedControl->DoRender();
+ }
+ if (render) CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ CGUIControl::Render();
+}
+
+bool CGUIControlGroupList::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage() )
+ {
+ case GUI_MSG_FOCUSED:
+ { // a control has been focused
+ // scroll if we need to and update our page control
+ ValidateOffset();
+ float offset = 0;
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *control = *it;
+ if (!control->IsVisible())
+ continue;
+ if (control->GetControl(message.GetControlId()))
+ {
+ // find out whether this is the first or last control
+ if (IsFirstFocusableControl(control))
+ ScrollTo(0);
+ else if (IsLastFocusableControl(control))
+ ScrollTo(m_totalSize - Size());
+ else if (offset < m_scroller.GetValue())
+ ScrollTo(offset);
+ else if (offset + Size(control) > m_scroller.GetValue() + Size())
+ ScrollTo(offset + Size(control) - Size());
+ break;
+ }
+ offset += Size(control) + m_itemGap;
+ }
+ }
+ break;
+ case GUI_MSG_SETFOCUS:
+ {
+ // we've been asked to focus. We focus the last control if it's on this page,
+ // else we'll focus the first focusable control from our offset (after verifying it)
+ ValidateOffset();
+ // now check the focusControl's offset
+ float offset = 0;
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *control = *it;
+ if (!control->IsVisible())
+ continue;
+ if (control->GetControl(m_focusedControl))
+ {
+ if (IsControlOnScreen(offset, control))
+ return CGUIControlGroup::OnMessage(message);
+ break;
+ }
+ offset += Size(control) + m_itemGap;
+ }
+ // find the first control on this page
+ offset = 0;
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *control = *it;
+ if (!control->IsVisible())
+ continue;
+ if (control->CanFocus() && IsControlOnScreen(offset, control))
+ {
+ m_focusedControl = control->GetID();
+ break;
+ }
+ offset += Size(control) + m_itemGap;
+ }
+ }
+ break;
+ case GUI_MSG_PAGE_CHANGE:
+ {
+ if (message.GetSenderId() == m_pageControl)
+ { // it's from our page control
+ ScrollTo((float)message.GetParam1());
+ return true;
+ }
+ }
+ break;
+ }
+ return CGUIControlGroup::OnMessage(message);
+}
+
+void CGUIControlGroupList::ValidateOffset()
+{
+ // calculate item gap. this needs to be done
+ // before fetching the total size
+ CalculateItemGap();
+ // calculate how many items we have on this page
+ m_totalSize = GetTotalSize();
+ // check our m_offset range
+ if (m_scroller.GetValue() > m_totalSize - Size())
+ m_scroller.SetValue(m_totalSize - Size());
+ if (m_scroller.GetValue() < 0) m_scroller.SetValue(0);
+}
+
+void CGUIControlGroupList::AddControl(CGUIControl *control, int position /*= -1*/)
+{
+ // NOTE: We override control navigation here, but we don't override the <onleft> etc. builtins
+ // if specified.
+ if (position < 0 || position > (int)m_children.size()) // add at the end
+ position = (int)m_children.size();
+
+ if (control)
+ { // set the navigation of items so that they form a list
+ CGUIAction beforeAction = GetAction((m_orientation == VERTICAL) ? ACTION_MOVE_UP : ACTION_MOVE_LEFT);
+ CGUIAction afterAction = GetAction((m_orientation == VERTICAL) ? ACTION_MOVE_DOWN : ACTION_MOVE_RIGHT);
+ if (m_children.size())
+ {
+ // we're inserting at the given position, so grab the items above and below and alter
+ // their navigation accordingly
+ CGUIControl *before = NULL;
+ CGUIControl *after = NULL;
+ if (position == 0)
+ { // inserting at the beginning
+ after = m_children[0];
+ if (!afterAction.HasActionsMeetingCondition() || afterAction.GetNavigation() == GetID()) // we're wrapping around bottom->top, so we have to update the last item
+ before = m_children[m_children.size() - 1];
+ if (!beforeAction.HasActionsMeetingCondition() || beforeAction.GetNavigation() == GetID()) // we're wrapping around top->bottom
+ beforeAction = CGUIAction(m_children[m_children.size() - 1]->GetID());
+ afterAction = CGUIAction(after->GetID());
+ }
+ else if (position == (int)m_children.size())
+ { // inserting at the end
+ before = m_children[m_children.size() - 1];
+ if (!beforeAction.HasActionsMeetingCondition() || beforeAction.GetNavigation() == GetID()) // we're wrapping around top->bottom, so we have to update the first item
+ after = m_children[0];
+ if (!afterAction.HasActionsMeetingCondition() || afterAction.GetNavigation() == GetID()) // we're wrapping around bottom->top
+ afterAction = CGUIAction(m_children[0]->GetID());
+ beforeAction = CGUIAction(before->GetID());
+ }
+ else
+ { // inserting somewhere in the middle
+ before = m_children[position - 1];
+ after = m_children[position];
+ beforeAction = CGUIAction(before->GetID());
+ afterAction = CGUIAction(after->GetID());
+ }
+ if (m_orientation == VERTICAL)
+ {
+ if (before) // update the DOWN action to point to us
+ before->SetAction(ACTION_MOVE_DOWN, CGUIAction(control->GetID()));
+ if (after) // update the UP action to point to us
+ after->SetAction(ACTION_MOVE_UP, CGUIAction(control->GetID()));
+ }
+ else
+ {
+ if (before) // update the RIGHT action to point to us
+ before->SetAction(ACTION_MOVE_RIGHT, CGUIAction(control->GetID()));
+ if (after) // update the LEFT action to point to us
+ after->SetAction(ACTION_MOVE_LEFT, CGUIAction(control->GetID()));
+ }
+ }
+ // now the control's nav
+ // set navigation path on orientation axis
+ // and try to apply other nav actions from grouplist
+ // don't override them if child have already defined actions
+ if (m_orientation == VERTICAL)
+ {
+ control->SetAction(ACTION_MOVE_UP, beforeAction);
+ control->SetAction(ACTION_MOVE_DOWN, afterAction);
+ control->SetAction(ACTION_MOVE_LEFT, GetAction(ACTION_MOVE_LEFT), false);
+ control->SetAction(ACTION_MOVE_RIGHT, GetAction(ACTION_MOVE_RIGHT), false);
+ }
+ else
+ {
+ control->SetAction(ACTION_MOVE_LEFT, beforeAction);
+ control->SetAction(ACTION_MOVE_RIGHT, afterAction);
+ control->SetAction(ACTION_MOVE_UP, GetAction(ACTION_MOVE_UP), false);
+ control->SetAction(ACTION_MOVE_DOWN, GetAction(ACTION_MOVE_DOWN), false);
+ }
+ control->SetAction(ACTION_NAV_BACK, GetAction(ACTION_NAV_BACK), false);
+
+ if (!m_useControlPositions)
+ control->SetPosition(0,0);
+ CGUIControlGroup::AddControl(control, position);
+ m_totalSize = GetTotalSize();
+ }
+}
+
+void CGUIControlGroupList::ClearAll()
+{
+ m_totalSize = 0;
+ CGUIControlGroup::ClearAll();
+ m_scroller.SetValue(0);
+}
+
+#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))
+
+float CGUIControlGroupList::GetWidth() const
+{
+ if (m_orientation == HORIZONTAL)
+ return CLAMP(m_totalSize, m_minSize, m_width);
+ return CGUIControlGroup::GetWidth();
+}
+
+float CGUIControlGroupList::GetHeight() const
+{
+ if (m_orientation == VERTICAL)
+ return CLAMP(m_totalSize, m_minSize, m_height);
+ return CGUIControlGroup::GetHeight();
+}
+
+void CGUIControlGroupList::SetMinSize(float minWidth, float minHeight)
+{
+ if (m_orientation == VERTICAL)
+ m_minSize = minHeight;
+ else
+ m_minSize = minWidth;
+}
+
+float CGUIControlGroupList::Size(const CGUIControl *control) const
+{
+ return (m_orientation == VERTICAL) ? control->GetYPosition() + control->GetHeight() : control->GetXPosition() + control->GetWidth();
+}
+
+inline float CGUIControlGroupList::Size() const
+{
+ return (m_orientation == VERTICAL) ? m_height : m_width;
+}
+
+void CGUIControlGroupList::SetInvalid()
+{
+ CGUIControl::SetInvalid();
+ // Force a message to the scrollbar
+ m_lastScrollerValue = -1;
+}
+
+void CGUIControlGroupList::ScrollTo(float offset)
+{
+ m_scroller.ScrollTo(offset);
+ if (m_scroller.IsScrolling())
+ SetInvalid();
+ MarkDirtyRegion();
+}
+
+EVENT_RESULT CGUIControlGroupList::SendMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ // transform our position into child coordinates
+ CPoint childPoint(point);
+ m_transform.InverseTransformPosition(childPoint.x, childPoint.y);
+ if (CGUIControl::CanFocus())
+ {
+ float pos = 0;
+ float alignOffset = GetAlignOffset();
+ for (ciControls i = m_children.begin(); i != m_children.end(); ++i)
+ {
+ CGUIControl *child = *i;
+ if (child->IsVisible())
+ {
+ if (IsControlOnScreen(pos, child))
+ { // we're on screen
+ float offsetX = m_orientation == VERTICAL ? m_posX : m_posX + alignOffset + pos - m_scroller.GetValue();
+ float offsetY = m_orientation == VERTICAL ? m_posY + alignOffset + pos - m_scroller.GetValue() : m_posY;
+ EVENT_RESULT ret = child->SendMouseEvent(childPoint - CPoint(offsetX, offsetY), event);
+ if (ret)
+ { // we've handled the action, and/or have focused an item
+ return ret;
+ }
+ }
+ pos += Size(child) + m_itemGap;
+ }
+ }
+ // none of our children want the event, but we may want it.
+ EVENT_RESULT ret;
+ if (HitTest(childPoint) && (ret = OnMouseEvent(childPoint, event)))
+ return ret;
+ }
+ m_focusedControl = 0;
+ return EVENT_RESULT_UNHANDLED;
+}
+
+void CGUIControlGroupList::UnfocusFromPoint(const CPoint &point)
+{
+ float pos = 0;
+ CPoint controlCoords(point);
+ m_transform.InverseTransformPosition(controlCoords.x, controlCoords.y);
+ float alignOffset = GetAlignOffset();
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *child = *it;
+ if (child->IsVisible())
+ {
+ if (IsControlOnScreen(pos, child))
+ { // we're on screen
+ CPoint offset = (m_orientation == VERTICAL) ? CPoint(m_posX, m_posY + alignOffset + pos - m_scroller.GetValue()) : CPoint(m_posX + alignOffset + pos - m_scroller.GetValue(), m_posY);
+ child->UnfocusFromPoint(controlCoords - offset);
+ }
+ pos += Size(child) + m_itemGap;
+ }
+ }
+ CGUIControl::UnfocusFromPoint(point);
+}
+
+bool CGUIControlGroupList::GetCondition(int condition, int data) const
+{
+ switch (condition)
+ {
+ case CONTAINER_HAS_NEXT:
+ return (m_totalSize >= Size() && m_scroller.GetValue() < m_totalSize - Size());
+ case CONTAINER_HAS_PREVIOUS:
+ return (m_scroller.GetValue() > 0);
+ case CONTAINER_POSITION:
+ return (m_focusedPosition == data);
+ default:
+ return false;
+ }
+}
+
+std::string CGUIControlGroupList::GetLabel(int info) const
+{
+ switch (info)
+ {
+ case CONTAINER_CURRENT_ITEM:
+ return std::to_string(GetSelectedItem());
+ case CONTAINER_NUM_ITEMS:
+ return std::to_string(GetNumItems());
+ case CONTAINER_POSITION:
+ return std::to_string(m_focusedPosition);
+ default:
+ break;
+ }
+ return "";
+}
+
+int CGUIControlGroupList::GetNumItems() const
+{
+ return std::count_if(m_children.begin(), m_children.end(), [&](const CGUIControl *child) {
+ return (child->IsVisible() && child->CanFocus());
+ });
+}
+
+int CGUIControlGroupList::GetSelectedItem() const
+{
+ int index = 1;
+ for (const auto& child : m_children)
+ {
+ if (child->IsVisible() && child->CanFocus())
+ {
+ if (child->HasFocus())
+ return index;
+ index++;
+ }
+ }
+ return -1;
+}
+
+bool CGUIControlGroupList::IsControlOnScreen(float pos, const CGUIControl *control) const
+{
+ return (pos >= m_scroller.GetValue() && pos + Size(control) <= m_scroller.GetValue() + Size());
+}
+
+bool CGUIControlGroupList::IsFirstFocusableControl(const CGUIControl *control) const
+{
+ for (ciControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *child = *it;
+ if (child->IsVisible() && child->CanFocus())
+ { // found first focusable
+ return child == control;
+ }
+ }
+ return false;
+}
+
+bool CGUIControlGroupList::IsLastFocusableControl(const CGUIControl *control) const
+{
+ for (crControls it = m_children.rbegin(); it != m_children.rend(); ++it)
+ {
+ CGUIControl *child = *it;
+ if (child->IsVisible() && child->CanFocus())
+ { // found first focusable
+ return child == control;
+ }
+ }
+ return false;
+}
+
+void CGUIControlGroupList::CalculateItemGap()
+{
+ if (m_alignment & XBFONT_JUSTIFIED)
+ {
+ int itemsCount = 0;
+ float itemsSize = 0;
+ for (const auto& child : m_children)
+ {
+ if (child->IsVisible())
+ {
+ itemsSize += Size(child);
+ itemsCount++;
+ }
+ }
+
+ if (itemsCount > 0)
+ m_itemGap = (Size() - itemsSize) / itemsCount;
+ }
+}
+
+float CGUIControlGroupList::GetAlignOffset() const
+{
+ if (m_totalSize < Size())
+ {
+ if (m_alignment & XBFONT_RIGHT)
+ return Size() - m_totalSize;
+ if (m_alignment & (XBFONT_CENTER_X | XBFONT_JUSTIFIED))
+ return (Size() - m_totalSize)*0.5f;
+ }
+ return 0.0f;
+}
+
+EVENT_RESULT CGUIControlGroupList::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_WHEEL_UP || event.m_id == ACTION_MOUSE_WHEEL_DOWN)
+ {
+ // find the current control and move to the next or previous
+ float offset = 0;
+ for (ciControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *control = *it;
+ if (!control->IsVisible()) continue;
+ float nextOffset = offset + Size(control) + m_itemGap;
+ if (event.m_id == ACTION_MOUSE_WHEEL_DOWN && nextOffset > m_scroller.GetValue() && m_scroller.GetValue() < m_totalSize - Size()) // past our current offset
+ {
+ ScrollTo(nextOffset);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_MOUSE_WHEEL_UP && nextOffset >= m_scroller.GetValue() && m_scroller.GetValue() > 0) // at least at our current offset
+ {
+ ScrollTo(offset);
+ return EVENT_RESULT_HANDLED;
+ }
+ offset = nextOffset;
+ }
+ }
+ else if (event.m_id == ACTION_GESTURE_BEGIN)
+ { // grab exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_END || event.m_id == ACTION_GESTURE_ABORT)
+ { // release exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
+ SendWindowMessage(msg);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_PAN)
+ { // do the drag and validate our offset (corrects for end of scroll)
+ m_scroller.SetValue(CLAMP(m_scroller.GetValue() - ((m_orientation == HORIZONTAL) ? event.m_offsetX : event.m_offsetY), 0, m_totalSize - Size()));
+ SetInvalid();
+ return EVENT_RESULT_HANDLED;
+ }
+
+ return EVENT_RESULT_UNHANDLED;
+}
+
+float CGUIControlGroupList::GetTotalSize() const
+{
+ float totalSize = 0;
+ for (ciControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *control = *it;
+ if (!control->IsVisible()) continue;
+ totalSize += Size(control) + m_itemGap;
+ }
+ if (totalSize > 0) totalSize -= m_itemGap;
+ return totalSize;
+}
diff --git a/xbmc/guilib/GUIControlGroupList.h b/xbmc/guilib/GUIControlGroupList.h
new file mode 100644
index 0000000..e50cf86
--- /dev/null
+++ b/xbmc/guilib/GUIControlGroupList.h
@@ -0,0 +1,84 @@
+/*
+ * 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
+
+/*!
+\file GUIControlGroupList.h
+\brief
+*/
+
+#include "GUIControlGroup.h"
+
+/*!
+ \ingroup controls
+ \brief list of controls that is scrollable
+ */
+class CGUIControlGroupList : public CGUIControlGroup
+{
+public:
+ CGUIControlGroupList(int parentID, int controlID, float posX, float posY, float width, float height, float itemGap, int pageControl, ORIENTATION orientation, bool useControlPositions, uint32_t alignment, const CScroller& scroller);
+ ~CGUIControlGroupList(void) override;
+ CGUIControlGroupList* Clone() const override { return new CGUIControlGroupList(*this); }
+
+ float GetWidth() const override;
+ float GetHeight() const override;
+ virtual float Size() const;
+ void SetInvalid() override;
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnMessage(CGUIMessage& message) override;
+
+ EVENT_RESULT SendMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ void UnfocusFromPoint(const CPoint &point) override;
+
+ void AddControl(CGUIControl *control, int position = -1) override;
+ void ClearAll() override;
+
+ virtual std::string GetLabel(int info) const;
+ bool GetCondition(int condition, int data) const override;
+ /**
+ * Calculate total size of child controls area (including gaps between controls)
+ */
+ float GetTotalSize() const;
+ ORIENTATION GetOrientation() const { return m_orientation; }
+
+ // based on grouplist orientation pick one value as minSize;
+ void SetMinSize(float minWidth, float minHeight);
+protected:
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ bool IsControlOnScreen(float pos, const CGUIControl* control) const;
+ bool IsFirstFocusableControl(const CGUIControl *control) const;
+ bool IsLastFocusableControl(const CGUIControl *control) const;
+ void ValidateOffset();
+ void CalculateItemGap();
+ inline float Size(const CGUIControl *control) const;
+ void ScrollTo(float offset);
+ float GetAlignOffset() const;
+
+ int GetNumItems() const;
+ int GetSelectedItem() const;
+
+ float m_itemGap;
+ int m_pageControl;
+ int m_focusedPosition;
+
+ float m_totalSize;
+
+ CScroller m_scroller;
+ int m_lastScrollerValue;
+
+ bool m_useControlPositions;
+ ORIENTATION m_orientation;
+ uint32_t m_alignment;
+
+ // for autosizing
+ float m_minSize;
+};
+
diff --git a/xbmc/guilib/GUIControlLookup.cpp b/xbmc/guilib/GUIControlLookup.cpp
new file mode 100644
index 0000000..8c572bb
--- /dev/null
+++ b/xbmc/guilib/GUIControlLookup.cpp
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2017-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 "GUIControlLookup.h"
+
+CGUIControlLookup::CGUIControlLookup(const CGUIControlLookup& from) : CGUIControl(from)
+{
+}
+
+CGUIControl *CGUIControlLookup::GetControl(int iControl, std::vector<CGUIControl*> *idCollector)
+{
+ if (idCollector)
+ idCollector->clear();
+
+ CGUIControl* pPotential(nullptr);
+
+ LookupMap::const_iterator first = m_lookup.find(iControl);
+ if (first != m_lookup.end())
+ {
+ LookupMap::const_iterator last = m_lookup.upper_bound(iControl);
+ for (LookupMap::const_iterator i = first; i != last; ++i)
+ {
+ CGUIControl *control = i->second;
+ if (control->IsVisible())
+ return control;
+ else if (idCollector)
+ idCollector->push_back(control);
+ else if (!pPotential)
+ pPotential = control;
+ }
+ }
+ return pPotential;
+}
+
+bool CGUIControlLookup::IsValidControl(const CGUIControl *control) const
+{
+ if (control->GetID())
+ {
+ for (const auto &i : m_lookup)
+ {
+ if (control == i.second)
+ return true;
+ }
+ }
+ return false;
+}
+
+void CGUIControlLookup::AddLookup(CGUIControl *control)
+{
+ CGUIControlLookup *lookupControl(dynamic_cast<CGUIControlLookup*>(control));
+
+ if (lookupControl)
+ { // first add all the subitems of this group (if they exist)
+ const LookupMap &map(lookupControl->GetLookup());
+ for (const auto &i : map)
+ m_lookup.insert(m_lookup.upper_bound(i.first), std::make_pair(i.first, i.second));
+ }
+ if (control->GetID())
+ m_lookup.insert(m_lookup.upper_bound(control->GetID()), std::make_pair(control->GetID(), control));
+ // ensure that our size is what it should be
+ if (m_parentControl && (lookupControl = dynamic_cast<CGUIControlLookup*>(m_parentControl)))
+ lookupControl->AddLookup(control);
+}
+
+void CGUIControlLookup::RemoveLookup(CGUIControl *control)
+{
+ CGUIControlLookup *lookupControl(dynamic_cast<CGUIControlLookup*>(control));
+ if (lookupControl)
+ { // remove the group's lookup
+ const LookupMap &map(lookupControl->GetLookup());
+ for (const auto &i : map)
+ { // remove this control
+ for (LookupMap::iterator it = m_lookup.begin(); it != m_lookup.end(); ++it)
+ {
+ if (i.second == it->second)
+ {
+ m_lookup.erase(it);
+ break;
+ }
+ }
+ }
+ }
+ // remove the actual control
+ if (control->GetID())
+ {
+ for (LookupMap::iterator it = m_lookup.begin(); it != m_lookup.end(); ++it)
+ {
+ if (control == it->second)
+ {
+ m_lookup.erase(it);
+ break;
+ }
+ }
+ }
+ if (m_parentControl && (lookupControl = dynamic_cast<CGUIControlLookup*>(m_parentControl)))
+ lookupControl->RemoveLookup(control);
+}
+
+void CGUIControlLookup::RemoveLookup()
+{
+ CGUIControlLookup *lookupControl;
+ if (m_parentControl && (lookupControl = dynamic_cast<CGUIControlLookup*>(m_parentControl)))
+ {
+ const LookupMap map(m_lookup);
+ for (const auto &i : map)
+ lookupControl->RemoveLookup(i.second);
+ }
+}
diff --git a/xbmc/guilib/GUIControlLookup.h b/xbmc/guilib/GUIControlLookup.h
new file mode 100644
index 0000000..01175e4
--- /dev/null
+++ b/xbmc/guilib/GUIControlLookup.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017-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 "GUIControl.h"
+
+class CGUIControlLookup : public CGUIControl
+{
+public:
+ CGUIControlLookup() = default;
+ CGUIControlLookup(int parentID, int controlID, float posX, float posY, float width, float height)
+ : CGUIControl(parentID, controlID, posX, posY, width, height) {}
+ explicit CGUIControlLookup(const CGUIControlLookup& from);
+ ~CGUIControlLookup(void) override = default;
+
+ CGUIControl *GetControl(int id, std::vector<CGUIControl*> *idCollector = nullptr) override;
+protected:
+ typedef std::multimap<int, CGUIControl *> LookupMap;
+
+ /*!
+ \brief Check whether a given control is valid
+ Runs through controls and returns whether this control is valid. Only functional
+ for controls with non-zero id.
+ \param control to check
+ \return true if the control is valid, false otherwise.
+ */
+ bool IsValidControl(const CGUIControl *control) const;
+ std::pair<LookupMap::const_iterator, LookupMap::const_iterator> GetLookupControls(int controlId) const
+ {
+ return m_lookup.equal_range(controlId);
+ };
+
+ // fast lookup by id
+ void AddLookup(CGUIControl *control);
+ void RemoveLookup(CGUIControl *control);
+ void RemoveLookup();
+ const LookupMap &GetLookup() const { return m_lookup; }
+ void ClearLookup() { m_lookup.clear(); }
+private:
+ LookupMap m_lookup;
+};
diff --git a/xbmc/guilib/GUIControlProfiler.cpp b/xbmc/guilib/GUIControlProfiler.cpp
new file mode 100644
index 0000000..a3605a2
--- /dev/null
+++ b/xbmc/guilib/GUIControlProfiler.cpp
@@ -0,0 +1,337 @@
+/*
+ * 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 "GUIControlProfiler.h"
+
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/XBMCTinyXML.h"
+
+bool CGUIControlProfiler::m_bIsRunning = false;
+
+CGUIControlProfilerItem::CGUIControlProfilerItem(CGUIControlProfiler *pProfiler, CGUIControlProfilerItem *pParent, CGUIControl *pControl)
+: m_pProfiler(pProfiler), m_pParent(pParent), m_pControl(pControl), m_visTime(0), m_renderTime(0), m_i64VisStart(0), m_i64RenderStart(0)
+{
+ if (m_pControl)
+ {
+ m_controlID = m_pControl->GetID();
+ m_ControlType = m_pControl->GetControlType();
+ m_strDescription = m_pControl->GetDescription();
+ }
+ else
+ {
+ m_controlID = 0;
+ m_ControlType = CGUIControl::GUICONTROL_UNKNOWN;
+ }
+}
+
+CGUIControlProfilerItem::~CGUIControlProfilerItem(void)
+{
+ Reset(NULL);
+}
+
+void CGUIControlProfilerItem::Reset(CGUIControlProfiler *pProfiler)
+{
+ m_controlID = 0;
+ m_ControlType = CGUIControl::GUICONTROL_UNKNOWN;
+ m_pControl = NULL;
+
+ m_visTime = 0;
+ m_renderTime = 0;
+ const unsigned int dwSize = m_vecChildren.size();
+ for (unsigned int i=0; i<dwSize; ++i)
+ delete m_vecChildren[i];
+ m_vecChildren.clear();
+
+ m_pProfiler = pProfiler;
+}
+
+void CGUIControlProfilerItem::BeginVisibility(void)
+{
+ m_i64VisStart = CurrentHostCounter();
+}
+
+void CGUIControlProfilerItem::EndVisibility(void)
+{
+ m_visTime += (unsigned int)(m_pProfiler->m_fPerfScale * (CurrentHostCounter() - m_i64VisStart));
+}
+
+void CGUIControlProfilerItem::BeginRender(void)
+{
+ m_i64RenderStart = CurrentHostCounter();
+}
+
+void CGUIControlProfilerItem::EndRender(void)
+{
+ m_renderTime += (unsigned int)(m_pProfiler->m_fPerfScale * (CurrentHostCounter() - m_i64RenderStart));
+}
+
+void CGUIControlProfilerItem::SaveToXML(TiXmlElement *parent)
+{
+ TiXmlElement *xmlControl = new TiXmlElement("control");
+ parent->LinkEndChild(xmlControl);
+
+ const char *lpszType = NULL;
+ switch (m_ControlType)
+ {
+ case CGUIControl::GUICONTROL_BUTTON:
+ lpszType = "button"; break;
+ case CGUIControl::GUICONTROL_FADELABEL:
+ lpszType = "fadelabel"; break;
+ case CGUIControl::GUICONTROL_IMAGE:
+ case CGUIControl::GUICONTROL_BORDEREDIMAGE:
+ lpszType = "image"; break;
+ case CGUIControl::GUICONTROL_LABEL:
+ lpszType = "label"; break;
+ case CGUIControl::GUICONTROL_LISTGROUP:
+ lpszType = "group"; break;
+ case CGUIControl::GUICONTROL_PROGRESS:
+ lpszType = "progress"; break;
+ case CGUIControl::GUICONTROL_RADIO:
+ lpszType = "radiobutton"; break;
+ case CGUIControl::GUICONTROL_RSS:
+ lpszType = "rss"; break;
+ case CGUIControl::GUICONTROL_SLIDER:
+ lpszType = "slider"; break;
+ case CGUIControl::GUICONTROL_SETTINGS_SLIDER:
+ lpszType = "sliderex"; break;
+ case CGUIControl::GUICONTROL_SPIN:
+ lpszType = "spincontrol"; break;
+ case CGUIControl::GUICONTROL_SPINEX:
+ lpszType = "spincontrolex"; break;
+ case CGUIControl::GUICONTROL_TEXTBOX:
+ lpszType = "textbox"; break;
+ case CGUIControl::GUICONTROL_TOGGLEBUTTON:
+ lpszType = "togglebutton"; break;
+ case CGUIControl::GUICONTROL_VIDEO:
+ lpszType = "videowindow"; break;
+ case CGUIControl::GUICONTROL_MOVER:
+ lpszType = "mover"; break;
+ case CGUIControl::GUICONTROL_RESIZE:
+ lpszType = "resize"; break;
+ case CGUIControl::GUICONTROL_EDIT:
+ lpszType = "edit"; break;
+ case CGUIControl::GUICONTROL_VISUALISATION:
+ lpszType = "visualisation"; break;
+ case CGUIControl::GUICONTROL_MULTI_IMAGE:
+ lpszType = "multiimage"; break;
+ case CGUIControl::GUICONTROL_GROUP:
+ lpszType = "group"; break;
+ case CGUIControl::GUICONTROL_GROUPLIST:
+ lpszType = "grouplist"; break;
+ case CGUIControl::GUICONTROL_SCROLLBAR:
+ lpszType = "scrollbar"; break;
+ case CGUIControl::GUICONTROL_LISTLABEL:
+ lpszType = "label"; break;
+ case CGUIControl::GUICONTAINER_LIST:
+ lpszType = "list"; break;
+ case CGUIControl::GUICONTAINER_WRAPLIST:
+ lpszType = "wraplist"; break;
+ case CGUIControl::GUICONTAINER_FIXEDLIST:
+ lpszType = "fixedlist"; break;
+ case CGUIControl::GUICONTAINER_PANEL:
+ lpszType = "panel"; break;
+ case CGUIControl::GUICONTROL_COLORBUTTON:
+ lpszType = "colorbutton";
+ break;
+ //case CGUIControl::GUICONTROL_UNKNOWN:
+ default:
+ break;
+ }
+
+ if (lpszType)
+ xmlControl->SetAttribute("type", lpszType);
+ if (m_controlID != 0)
+ {
+ std::string str = std::to_string(m_controlID);
+ xmlControl->SetAttribute("id", str.c_str());
+ }
+
+ float pct = (float)GetTotalTime() / (float)m_pProfiler->GetTotalTime();
+ if (pct > 0.01f)
+ {
+ std::string str = StringUtils::Format("{:.0f}", pct * 100.0f);
+ xmlControl->SetAttribute("percent", str.c_str());
+ }
+
+ if (!m_strDescription.empty())
+ {
+ TiXmlElement *elem = new TiXmlElement("description");
+ xmlControl->LinkEndChild(elem);
+ TiXmlText *text = new TiXmlText(m_strDescription.c_str());
+ elem->LinkEndChild(text);
+ }
+
+ // Note time is stored in 1/100 milliseconds but reported in ms
+ unsigned int vis = m_visTime / 100;
+ unsigned int rend = m_renderTime / 100;
+ if (vis || rend)
+ {
+ std::string val;
+ TiXmlElement *elem = new TiXmlElement("rendertime");
+ xmlControl->LinkEndChild(elem);
+ val = std::to_string(rend);
+ TiXmlText *text = new TiXmlText(val.c_str());
+ elem->LinkEndChild(text);
+
+ elem = new TiXmlElement("visibletime");
+ xmlControl->LinkEndChild(elem);
+ val = std::to_string(vis);
+ text = new TiXmlText(val.c_str());
+ elem->LinkEndChild(text);
+ }
+
+ if (m_vecChildren.size())
+ {
+ TiXmlElement *xmlChilds = new TiXmlElement("children");
+ xmlControl->LinkEndChild(xmlChilds);
+ const unsigned int dwSize = m_vecChildren.size();
+ for (unsigned int i=0; i<dwSize; ++i)
+ m_vecChildren[i]->SaveToXML(xmlChilds);
+ }
+}
+
+CGUIControlProfilerItem *CGUIControlProfilerItem::AddControl(CGUIControl *pControl)
+{
+ m_vecChildren.push_back(new CGUIControlProfilerItem(m_pProfiler, this, pControl));
+ return m_vecChildren.back();
+}
+
+CGUIControlProfilerItem *CGUIControlProfilerItem::FindOrAddControl(CGUIControl *pControl, bool recurse)
+{
+ const unsigned int dwSize = m_vecChildren.size();
+ for (unsigned int i=0; i<dwSize; ++i)
+ {
+ CGUIControlProfilerItem *p = m_vecChildren[i];
+ if (p->m_pControl == pControl)
+ return p;
+ if (recurse && (p = p->FindOrAddControl(pControl, true)))
+ return p;
+ }
+
+ if (pControl->GetParentControl() == m_pControl)
+ return AddControl(pControl);
+
+ return NULL;
+}
+
+CGUIControlProfiler::CGUIControlProfiler(void)
+: m_ItemHead(NULL, NULL, NULL), m_pLastItem(NULL)
+// m_bIsRunning(false), no isRunning because it is static
+{
+ m_fPerfScale = 100000.0f / CurrentHostFrequency();
+}
+
+CGUIControlProfiler &CGUIControlProfiler::Instance(void)
+{
+ static CGUIControlProfiler _instance;
+ return _instance;
+}
+
+bool CGUIControlProfiler::IsRunning(void)
+{
+ return m_bIsRunning;
+}
+
+void CGUIControlProfiler::Start(void)
+{
+ m_iFrameCount = 0;
+ m_bIsRunning = true;
+ m_pLastItem = NULL;
+ m_ItemHead.Reset(this);
+}
+
+void CGUIControlProfiler::BeginVisibility(CGUIControl *pControl)
+{
+ CGUIControlProfilerItem *item = FindOrAddControl(pControl);
+ item->BeginVisibility();
+}
+
+void CGUIControlProfiler::EndVisibility(CGUIControl *pControl)
+{
+ CGUIControlProfilerItem *item = FindOrAddControl(pControl);
+ item->EndVisibility();
+}
+
+void CGUIControlProfiler::BeginRender(CGUIControl *pControl)
+{
+ CGUIControlProfilerItem *item = FindOrAddControl(pControl);
+ item->BeginRender();
+}
+
+void CGUIControlProfiler::EndRender(CGUIControl *pControl)
+{
+ CGUIControlProfilerItem *item = FindOrAddControl(pControl);
+ item->EndRender();
+}
+
+CGUIControlProfilerItem *CGUIControlProfiler::FindOrAddControl(CGUIControl *pControl)
+{
+ if (m_pLastItem)
+ {
+ // Typically calls come in pairs so the last control we found is probably
+ // the one we want again next time
+ if (m_pLastItem->m_pControl == pControl)
+ return m_pLastItem;
+ // If that control is not a match, usually the one we want is the next
+ // sibling of that control, or the parent of that control so check
+ // the parent first as it is more convenient
+ m_pLastItem = m_pLastItem->m_pParent;
+ if (m_pLastItem && m_pLastItem->m_pControl == pControl)
+ return m_pLastItem;
+ // continued from above, this searches the original control's siblings
+ if (m_pLastItem)
+ m_pLastItem = m_pLastItem->FindOrAddControl(pControl, false);
+ if (m_pLastItem)
+ return m_pLastItem;
+ }
+
+ m_pLastItem = m_ItemHead.FindOrAddControl(pControl, true);
+ if (!m_pLastItem)
+ m_pLastItem = m_ItemHead.AddControl(pControl);
+
+ return m_pLastItem;
+}
+
+void CGUIControlProfiler::EndFrame(void)
+{
+ m_iFrameCount++;
+ if (m_iFrameCount >= m_iMaxFrameCount)
+ {
+ const unsigned int dwSize = m_ItemHead.m_vecChildren.size();
+ for (unsigned int i=0; i<dwSize; ++i)
+ {
+ CGUIControlProfilerItem *p = m_ItemHead.m_vecChildren[i];
+ m_ItemHead.m_visTime += p->m_visTime;
+ m_ItemHead.m_renderTime += p->m_renderTime;
+ }
+
+ m_bIsRunning = false;
+ if (SaveResults())
+ m_ItemHead.Reset(this);
+ }
+}
+
+bool CGUIControlProfiler::SaveResults(void)
+{
+ if (m_strOutputFile.empty())
+ return false;
+
+ CXBMCTinyXML doc;
+ TiXmlDeclaration decl("1.0", "", "yes");
+ doc.InsertEndChild(decl);
+
+ TiXmlElement *root = new TiXmlElement("guicontrolprofiler");
+ std::string str = std::to_string(m_iFrameCount);
+ root->SetAttribute("framecount", str.c_str());
+ root->SetAttribute("timeunit", "ms");
+ doc.LinkEndChild(root);
+
+ m_ItemHead.SaveToXML(root);
+ return doc.SaveFile(m_strOutputFile);
+}
diff --git a/xbmc/guilib/GUIControlProfiler.h b/xbmc/guilib/GUIControlProfiler.h
new file mode 100644
index 0000000..006493a
--- /dev/null
+++ b/xbmc/guilib/GUIControlProfiler.h
@@ -0,0 +1,88 @@
+/*
+ * 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 "GUIControl.h"
+
+#include <vector>
+
+class CGUIControlProfiler;
+class TiXmlElement;
+
+class CGUIControlProfilerItem
+{
+public:
+ CGUIControlProfiler *m_pProfiler;
+ CGUIControlProfilerItem * m_pParent;
+ CGUIControl *m_pControl;
+ std::vector<CGUIControlProfilerItem *> m_vecChildren;
+ std::string m_strDescription;
+ int m_controlID;
+ CGUIControl::GUICONTROLTYPES m_ControlType;
+ unsigned int m_visTime;
+ unsigned int m_renderTime;
+ int64_t m_i64VisStart;
+ int64_t m_i64RenderStart;
+
+ CGUIControlProfilerItem(CGUIControlProfiler *pProfiler, CGUIControlProfilerItem *pParent, CGUIControl *pControl);
+ ~CGUIControlProfilerItem(void);
+
+ void Reset(CGUIControlProfiler *pProfiler);
+ void BeginVisibility(void);
+ void EndVisibility(void);
+ void BeginRender(void);
+ void EndRender(void);
+ void SaveToXML(TiXmlElement *parent);
+ unsigned int GetTotalTime(void) const { return m_visTime + m_renderTime; }
+
+ CGUIControlProfilerItem *AddControl(CGUIControl *pControl);
+ CGUIControlProfilerItem *FindOrAddControl(CGUIControl *pControl, bool recurse);
+};
+
+class CGUIControlProfiler
+{
+public:
+ static CGUIControlProfiler &Instance(void);
+ static bool IsRunning(void);
+
+ void Start(void);
+ void EndFrame(void);
+ void BeginVisibility(CGUIControl *pControl);
+ void EndVisibility(CGUIControl *pControl);
+ void BeginRender(CGUIControl *pControl);
+ void EndRender(CGUIControl *pControl);
+ int GetMaxFrameCount(void) const { return m_iMaxFrameCount; }
+ void SetMaxFrameCount(int iMaxFrameCount) { m_iMaxFrameCount = iMaxFrameCount; }
+ void SetOutputFile(const std::string& strOutputFile) { m_strOutputFile = strOutputFile; }
+ const std::string& GetOutputFile(void) const { return m_strOutputFile; }
+ bool SaveResults(void);
+ unsigned int GetTotalTime(void) const { return m_ItemHead.GetTotalTime(); }
+
+ float m_fPerfScale;
+private:
+ CGUIControlProfiler(void);
+ ~CGUIControlProfiler(void) = default;
+ CGUIControlProfiler(const CGUIControlProfiler &that) = delete;
+ CGUIControlProfiler &operator=(const CGUIControlProfiler &that) = delete;
+
+ CGUIControlProfilerItem m_ItemHead;
+ CGUIControlProfilerItem *m_pLastItem;
+ CGUIControlProfilerItem *FindOrAddControl(CGUIControl *pControl);
+
+ static bool m_bIsRunning;
+ std::string m_strOutputFile;
+ int m_iMaxFrameCount = 200;
+ int m_iFrameCount = 0;
+};
+
+#define GUIPROFILER_VISIBILITY_BEGIN(x) { if (CGUIControlProfiler::IsRunning()) CGUIControlProfiler::Instance().BeginVisibility(x); }
+#define GUIPROFILER_VISIBILITY_END(x) { if (CGUIControlProfiler::IsRunning()) CGUIControlProfiler::Instance().EndVisibility(x); }
+#define GUIPROFILER_RENDER_BEGIN(x) { if (CGUIControlProfiler::IsRunning()) CGUIControlProfiler::Instance().BeginRender(x); }
+#define GUIPROFILER_RENDER_END(x) { if (CGUIControlProfiler::IsRunning()) CGUIControlProfiler::Instance().EndRender(x); }
+
diff --git a/xbmc/guilib/GUIDialog.cpp b/xbmc/guilib/GUIDialog.cpp
new file mode 100644
index 0000000..a3d0e02
--- /dev/null
+++ b/xbmc/guilib/GUIDialog.cpp
@@ -0,0 +1,239 @@
+/*
+ * 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 "GUIDialog.h"
+
+#include "GUIComponent.h"
+#include "GUIControlFactory.h"
+#include "GUILabelControl.h"
+#include "GUIWindowManager.h"
+#include "ServiceBroker.h"
+#include "input/Key.h"
+#include "messaging/ApplicationMessenger.h"
+#include "threads/SingleLock.h"
+#include "utils/TimeUtils.h"
+
+CGUIDialog::CGUIDialog(int id, const std::string &xmlFile, DialogModalityType modalityType /* = DialogModalityType::MODAL */)
+ : CGUIWindow(id, xmlFile)
+{
+ m_modalityType = modalityType;
+ m_wasRunning = false;
+ m_renderOrder = RENDER_ORDER_DIALOG;
+ m_autoClosing = false;
+ m_showStartTime = 0;
+ m_showDuration = 0;
+ m_enableSound = true;
+ m_bAutoClosed = false;
+}
+
+CGUIDialog::~CGUIDialog(void) = default;
+
+bool CGUIDialog::Load(TiXmlElement* pRootElement)
+{
+ return CGUIWindow::Load(pRootElement);
+}
+
+void CGUIDialog::OnWindowLoaded()
+{
+ CGUIWindow::OnWindowLoaded();
+
+ // Clip labels to extents
+ if (m_children.size())
+ {
+ CGUIControl* pBase = m_children[0];
+
+ for (iControls p = m_children.begin() + 1; p != m_children.end(); ++p)
+ {
+ if ((*p)->GetControlType() == CGUIControl::GUICONTROL_LABEL)
+ {
+ CGUILabelControl* pLabel = (CGUILabelControl*)(*p);
+
+ if (!pLabel->GetWidth())
+ {
+ float spacing = (pLabel->GetXPosition() - pBase->GetXPosition()) * 2;
+ pLabel->SetWidth(pBase->GetWidth() - spacing);
+ }
+ }
+ }
+ }
+}
+
+bool CGUIDialog::OnAction(const CAction &action)
+{
+ // keyboard or controller movement should prevent autoclosing
+ if (!action.IsMouse() && m_autoClosing)
+ SetAutoClose(m_showDuration);
+
+ return CGUIWindow::OnAction(action);
+}
+
+bool CGUIDialog::OnBack(int actionID)
+{
+ Close();
+ return true;
+}
+
+bool CGUIDialog::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ CGUIWindow::OnMessage(message);
+ return true;
+ }
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CGUIWindow::OnMessage(message);
+ m_showStartTime = 0;
+ return true;
+ }
+ }
+
+ return CGUIWindow::OnMessage(message);
+}
+
+void CGUIDialog::OnDeinitWindow(int nextWindowID)
+{
+ if (m_active)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().RemoveDialog(GetID());
+ m_autoClosing = false;
+ }
+ CGUIWindow::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIDialog::DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ UpdateVisibility();
+
+ // if we were running but now we're not, mark us dirty
+ if (!m_active && m_wasRunning)
+ dirtyregions.push_back(CDirtyRegion(m_renderRegion));
+
+ if (m_active)
+ CGUIWindow::DoProcess(currentTime, dirtyregions);
+
+ m_wasRunning = m_active;
+}
+
+void CGUIDialog::UpdateVisibility()
+{
+ if (m_visibleCondition)
+ {
+ if (m_visibleCondition->Get(INFO::DEFAULT_CONTEXT))
+ Open();
+ else
+ Close();
+ }
+
+ if (m_autoClosing)
+ { // check if our timer is running
+ if (!m_showStartTime)
+ {
+ if (HasProcessed()) // start timer
+ m_showStartTime = CTimeUtils::GetFrameTime();
+ }
+ else
+ {
+ if (m_showStartTime + m_showDuration < CTimeUtils::GetFrameTime() && !m_closing)
+ {
+ m_bAutoClosed = true;
+ Close();
+ }
+ }
+ }
+}
+
+void CGUIDialog::Open_Internal(bool bProcessRenderLoop, const std::string &param /* = "" */)
+{
+ if (!CServiceBroker::GetGUI()->GetWindowManager().Initialized() ||
+ (m_active && !m_closing && !IsAnimating(ANIM_TYPE_WINDOW_CLOSE)))
+ return;
+
+ // set running before it's added to the window manager, else the auto-show code
+ // could show it as well if we are in a different thread from the main rendering
+ // thread (this should really be handled via a thread message though IMO)
+ m_active = true;
+ m_closing = false;
+ CServiceBroker::GetGUI()->GetWindowManager().RegisterDialog(this);
+
+ // active this window
+ CGUIMessage msg(GUI_MSG_WINDOW_INIT, 0, 0);
+ msg.SetStringParam(param);
+ OnMessage(msg);
+
+ // process render loop
+ if (bProcessRenderLoop)
+ {
+ if (!m_windowLoaded)
+ Close(true);
+
+ while (m_active)
+ {
+ if (!CServiceBroker::GetGUI()->GetWindowManager().ProcessRenderLoop(false))
+ break;
+ }
+ }
+}
+
+void CGUIDialog::Open(const std::string &param /* = "" */)
+{
+ CGUIDialog::Open(m_modalityType != DialogModalityType::MODELESS, param);
+}
+
+
+void CGUIDialog::Open(bool bProcessRenderLoop, const std::string& param /* = "" */)
+{
+ if (!CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ // make sure graphics lock is not held
+ CSingleExit leaveIt(CServiceBroker::GetWinSystem()->GetGfxContext());
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_DIALOG_OPEN, -1, bProcessRenderLoop,
+ static_cast<void*>(this), param);
+ }
+ else
+ Open_Internal(bProcessRenderLoop, param);
+}
+
+void CGUIDialog::Render()
+{
+ if (!m_active)
+ return;
+
+ CGUIWindow::Render();
+}
+
+void CGUIDialog::SetDefaults()
+{
+ CGUIWindow::SetDefaults();
+ m_renderOrder = RENDER_ORDER_DIALOG;
+}
+
+void CGUIDialog::SetAutoClose(unsigned int timeoutMs)
+{
+ m_autoClosing = true;
+ m_showDuration = timeoutMs;
+ ResetAutoClose();
+}
+
+void CGUIDialog::ResetAutoClose(void)
+{
+ if (m_autoClosing && m_active)
+ m_showStartTime = CTimeUtils::GetFrameTime();
+}
+
+void CGUIDialog::CancelAutoClose(void)
+{
+ m_autoClosing = false;
+}
+
+void CGUIDialog::ProcessRenderLoop(bool renderOnly)
+{
+ CServiceBroker::GetGUI()->GetWindowManager().ProcessRenderLoop(renderOnly);
+}
diff --git a/xbmc/guilib/GUIDialog.h b/xbmc/guilib/GUIDialog.h
new file mode 100644
index 0000000..7300c1f
--- /dev/null
+++ b/xbmc/guilib/GUIDialog.h
@@ -0,0 +1,83 @@
+/*
+ * 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
+
+/*!
+\file GUIDialog.h
+\brief
+*/
+
+#include "GUIWindow.h"
+#include "WindowIDs.h"
+
+#ifdef TARGET_WINDOWS_STORE
+#pragma pack(push, 8)
+#endif
+enum class DialogModalityType
+{
+ MODELESS,
+ MODAL
+};
+#ifdef TARGET_WINDOWS_STORE
+#pragma pack(pop)
+#endif
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+class CGUIDialog :
+ public CGUIWindow
+{
+public:
+ CGUIDialog(int id, const std::string &xmlFile, DialogModalityType modalityType = DialogModalityType::MODAL);
+ ~CGUIDialog(void) override;
+
+ bool OnAction(const CAction &action) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+
+ void Open(const std::string &param = "");
+ void Open(bool bProcessRenderLoop, const std::string& param = "");
+
+ bool OnBack(int actionID) override;
+
+ bool IsDialogRunning() const override { return m_active; }
+ bool IsDialog() const override { return true; }
+ bool IsModalDialog() const override { return m_modalityType == DialogModalityType::MODAL; }
+ virtual DialogModalityType GetModalityType() const { return m_modalityType; }
+
+ void SetAutoClose(unsigned int timeoutMs);
+ void ResetAutoClose(void);
+ void CancelAutoClose(void);
+ bool IsAutoClosed(void) const { return m_bAutoClosed; }
+ void SetSound(bool OnOff) { m_enableSound = OnOff; }
+ bool IsSoundEnabled() const override { return m_enableSound; }
+
+protected:
+ bool Load(TiXmlElement *pRootElement) override;
+ void SetDefaults() override;
+ void OnWindowLoaded() override;
+ using CGUIWindow::UpdateVisibility;
+ virtual void UpdateVisibility();
+
+ virtual void Open_Internal(bool bProcessRenderLoop, const std::string &param = "");
+ void OnDeinitWindow(int nextWindowID) override;
+
+ void ProcessRenderLoop(bool renderOnly = false);
+
+ bool m_wasRunning; ///< \brief true if we were running during the last DoProcess()
+ bool m_autoClosing;
+ bool m_enableSound;
+ unsigned int m_showStartTime;
+ unsigned int m_showDuration;
+ bool m_bAutoClosed;
+ DialogModalityType m_modalityType;
+};
diff --git a/xbmc/guilib/GUIEditControl.cpp b/xbmc/guilib/GUIEditControl.cpp
new file mode 100644
index 0000000..325a703
--- /dev/null
+++ b/xbmc/guilib/GUIEditControl.cpp
@@ -0,0 +1,743 @@
+/*
+ * 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 "GUIEditControl.h"
+
+#include "GUIFont.h"
+#include "GUIKeyboardFactory.h"
+#include "GUIUserMessages.h"
+#include "GUIWindowManager.h"
+#include "LocalizeStrings.h"
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "input/Key.h"
+#include "input/XBMC_vkeys.h"
+#include "utils/CharsetConverter.h"
+#include "utils/ColorUtils.h"
+#include "utils/Digest.h"
+#include "utils/Variant.h"
+#include "windowing/WinSystem.h"
+
+using namespace KODI::GUILIB;
+
+using KODI::UTILITY::CDigest;
+
+const char* CGUIEditControl::smsLetters[10] = { " !@#$%^&*()[]{}<>/\\|0", ".,;:\'\"-+_=?`~1", "abc2ABC", "def3DEF", "ghi4GHI", "jkl5JKL", "mno6MNO", "pqrs7PQRS", "tuv8TUV", "wxyz9WXYZ" };
+const unsigned int CGUIEditControl::smsDelay = 1000;
+
+#ifdef TARGET_WINDOWS
+extern HWND g_hWnd;
+#endif
+
+CGUIEditControl::CGUIEditControl(int parentID, int controlID, float posX, float posY,
+ float width, float height, const CTextureInfo &textureFocus, const CTextureInfo &textureNoFocus,
+ const CLabelInfo& labelInfo, const std::string &text)
+ : CGUIButtonControl(parentID, controlID, posX, posY, width, height, textureFocus, textureNoFocus, labelInfo)
+{
+ DefaultConstructor();
+ SetLabel(text);
+}
+
+void CGUIEditControl::DefaultConstructor()
+{
+ ControlType = GUICONTROL_EDIT;
+ m_textOffset = 0;
+ m_textWidth = GetWidth();
+ m_cursorPos = 0;
+ m_cursorBlink = 0;
+ m_inputHeading = g_localizeStrings.Get(16028);
+ m_inputType = INPUT_TYPE_TEXT;
+ m_smsLastKey = 0;
+ m_smsKeyIndex = 0;
+ m_label.SetAlign(m_label.GetLabelInfo().align & XBFONT_CENTER_Y); // left align
+ m_label2.GetLabelInfo().offsetX = 0;
+ m_isMD5 = false;
+ m_invalidInput = false;
+ m_inputValidator = NULL;
+ m_inputValidatorData = NULL;
+ m_editLength = 0;
+ m_editOffset = 0;
+}
+
+CGUIEditControl::CGUIEditControl(const CGUIButtonControl &button)
+ : CGUIButtonControl(button)
+{
+ DefaultConstructor();
+}
+
+CGUIEditControl::~CGUIEditControl(void) = default;
+
+bool CGUIEditControl::OnMessage(CGUIMessage &message)
+{
+ if (message.GetMessage() == GUI_MSG_SET_TYPE)
+ {
+ SetInputType((INPUT_TYPE)message.GetParam1(), message.GetParam2());
+ return true;
+ }
+ else if (message.GetMessage() == GUI_MSG_ITEM_SELECTED)
+ {
+ message.SetLabel(GetLabel2());
+ return true;
+ }
+ else if (message.GetMessage() == GUI_MSG_SET_TEXT &&
+ ((message.GetControlId() <= 0 && HasFocus()) || (message.GetControlId() == GetID())))
+ {
+ SetLabel2(message.GetLabel());
+ UpdateText();
+ }
+ return CGUIButtonControl::OnMessage(message);
+}
+
+bool CGUIEditControl::OnAction(const CAction &action)
+{
+ ValidateCursor();
+
+ if (m_inputType != INPUT_TYPE_READONLY)
+ {
+ if (action.GetID() == ACTION_BACKSPACE)
+ {
+ // backspace
+ if (m_cursorPos)
+ {
+ if (!ClearMD5())
+ m_text2.erase(--m_cursorPos, 1);
+ UpdateText();
+ }
+ return true;
+ }
+ else if (action.GetID() == ACTION_MOVE_LEFT ||
+ action.GetID() == ACTION_CURSOR_LEFT)
+ {
+ if (m_cursorPos > 0)
+ {
+ m_cursorPos--;
+ UpdateText(false);
+ return true;
+ }
+ }
+ else if (action.GetID() == ACTION_MOVE_RIGHT ||
+ action.GetID() == ACTION_CURSOR_RIGHT)
+ {
+ if (m_cursorPos < m_text2.size())
+ {
+ m_cursorPos++;
+ UpdateText(false);
+ return true;
+ }
+ }
+ else if (action.GetID() == ACTION_PASTE)
+ {
+ ClearMD5();
+ OnPasteClipboard();
+ return true;
+ }
+ else if (action.GetID() >= KEY_VKEY && action.GetID() < KEY_UNICODE && m_edit.empty())
+ {
+ // input from the keyboard (vkey, not ascii)
+ unsigned char b = action.GetID() & 0xFF;
+ if (b == XBMCVK_HOME)
+ {
+ m_cursorPos = 0;
+ UpdateText(false);
+ return true;
+ }
+ else if (b == XBMCVK_END)
+ {
+ m_cursorPos = m_text2.length();
+ UpdateText(false);
+ return true;
+ }
+ if (b == XBMCVK_LEFT && m_cursorPos > 0)
+ {
+ m_cursorPos--;
+ UpdateText(false);
+ return true;
+ }
+ if (b == XBMCVK_RIGHT && m_cursorPos < m_text2.length())
+ {
+ m_cursorPos++;
+ UpdateText(false);
+ return true;
+ }
+ if (b == XBMCVK_DELETE)
+ {
+ if (m_cursorPos < m_text2.length())
+ {
+ if (!ClearMD5())
+ m_text2.erase(m_cursorPos, 1);
+ UpdateText();
+ return true;
+ }
+ }
+ if (b == XBMCVK_BACK)
+ {
+ if (m_cursorPos > 0)
+ {
+ if (!ClearMD5())
+ m_text2.erase(--m_cursorPos, 1);
+ UpdateText();
+ }
+ return true;
+ }
+ else if (b == XBMCVK_RETURN || b == XBMCVK_NUMPADENTER)
+ {
+ // enter - send click message, but otherwise ignore
+ SEND_CLICK_MESSAGE(GetID(), GetParentID(), 1);
+ return true;
+ }
+ else if (b == XBMCVK_ESCAPE)
+ { // escape - fallthrough to default action
+ return CGUIButtonControl::OnAction(action);
+ }
+ }
+ else if (action.GetID() == KEY_UNICODE)
+ {
+ // input from the keyboard
+ int ch = action.GetUnicode();
+ // ignore non-printing characters
+ if ( !((0 <= ch && ch < 0x8) || (0xE <= ch && ch < 0x1B) || (0x1C <= ch && ch < 0x20)) )
+ {
+ switch (ch)
+ {
+ case 9: // tab, ignore
+ case 11: // Non-printing character, ignore
+ case 12: // Non-printing character, ignore
+ break;
+ case 10:
+ case 13:
+ {
+ // enter - send click message, but otherwise ignore
+ SEND_CLICK_MESSAGE(GetID(), GetParentID(), 1);
+ return true;
+ }
+ case 27:
+ { // escape - fallthrough to default action
+ return CGUIButtonControl::OnAction(action);
+ }
+ case 8:
+ {
+ // backspace
+ if (m_cursorPos)
+ {
+ if (!ClearMD5())
+ m_text2.erase(--m_cursorPos, 1);
+ }
+ break;
+ }
+ case 127:
+ { // delete
+ if (m_cursorPos < m_text2.length())
+ {
+ if (!ClearMD5())
+ m_text2.erase(m_cursorPos, 1);
+ }
+ break;
+ }
+ default:
+ {
+ ClearMD5();
+ m_edit.clear();
+ m_text2.insert(m_text2.begin() + m_cursorPos++, action.GetUnicode());
+ break;
+ }
+ }
+ UpdateText();
+ return true;
+ }
+ }
+ else if (action.GetID() >= REMOTE_0 && action.GetID() <= REMOTE_9)
+ { // input from the remote
+ ClearMD5();
+ m_edit.clear();
+ OnSMSCharacter(action.GetID() - REMOTE_0);
+ return true;
+ }
+ else if (action.GetID() == ACTION_INPUT_TEXT)
+ {
+ m_edit.clear();
+ std::wstring str;
+ g_charsetConverter.utf8ToW(action.GetText(), str, false);
+ m_text2.insert(m_cursorPos, str);
+ m_cursorPos += str.size();
+ UpdateText();
+ return true;
+ }
+ }
+ return CGUIButtonControl::OnAction(action);
+}
+
+void CGUIEditControl::OnClick()
+{
+ // we received a click - it's not from the keyboard, so pop up the virtual keyboard, unless
+ // that is where we reside!
+ if (GetParentID() == WINDOW_DIALOG_KEYBOARD)
+ return;
+
+ std::string utf8;
+ g_charsetConverter.wToUTF8(m_text2, utf8);
+ bool textChanged = false;
+ switch (m_inputType)
+ {
+ case INPUT_TYPE_READONLY:
+ textChanged = false;
+ break;
+ case INPUT_TYPE_NUMBER:
+ textChanged = CGUIDialogNumeric::ShowAndGetNumber(utf8, m_inputHeading);
+ break;
+ case INPUT_TYPE_SECONDS:
+ textChanged = CGUIDialogNumeric::ShowAndGetSeconds(utf8, g_localizeStrings.Get(21420));
+ break;
+ case INPUT_TYPE_TIME:
+ {
+ CDateTime dateTime;
+ dateTime.SetFromDBTime(utf8);
+ KODI::TIME::SystemTime time;
+ dateTime.GetAsSystemTime(time);
+ if (CGUIDialogNumeric::ShowAndGetTime(time, !m_inputHeading.empty() ? m_inputHeading : g_localizeStrings.Get(21420)))
+ {
+ dateTime = CDateTime(time);
+ utf8 = dateTime.GetAsLocalizedTime("", false);
+ textChanged = true;
+ }
+ break;
+ }
+ case INPUT_TYPE_DATE:
+ {
+ CDateTime dateTime;
+ dateTime.SetFromDBDate(utf8);
+ if (dateTime < CDateTime(2000,1, 1, 0, 0, 0))
+ dateTime = CDateTime(2000, 1, 1, 0, 0, 0);
+ KODI::TIME::SystemTime date;
+ dateTime.GetAsSystemTime(date);
+ if (CGUIDialogNumeric::ShowAndGetDate(date, !m_inputHeading.empty() ? m_inputHeading : g_localizeStrings.Get(21420)))
+ {
+ dateTime = CDateTime(date);
+ utf8 = dateTime.GetAsDBDate();
+ textChanged = true;
+ }
+ break;
+ }
+ case INPUT_TYPE_IPADDRESS:
+ textChanged = CGUIDialogNumeric::ShowAndGetIPAddress(utf8, m_inputHeading);
+ break;
+ case INPUT_TYPE_SEARCH:
+ textChanged = CGUIKeyboardFactory::ShowAndGetFilter(utf8, true);
+ break;
+ case INPUT_TYPE_FILTER:
+ textChanged = CGUIKeyboardFactory::ShowAndGetFilter(utf8, false);
+ break;
+ case INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW:
+ textChanged = CGUIDialogNumeric::ShowAndVerifyNewPassword(utf8);
+ break;
+ case INPUT_TYPE_PASSWORD_MD5:
+ utf8 = ""; //! @todo Ideally we'd send this to the keyboard and tell the keyboard we have this type of input
+ // fallthrough
+ [[fallthrough]];
+ case INPUT_TYPE_TEXT:
+ default:
+ textChanged = CGUIKeyboardFactory::ShowAndGetInput(utf8, m_inputHeading, true, m_inputType == INPUT_TYPE_PASSWORD || m_inputType == INPUT_TYPE_PASSWORD_MD5);
+ break;
+ }
+ if (textChanged)
+ {
+ ClearMD5();
+ m_edit.clear();
+ g_charsetConverter.utf8ToW(utf8, m_text2, false);
+ m_cursorPos = m_text2.size();
+ UpdateText();
+ m_cursorPos = m_text2.size();
+ }
+}
+
+void CGUIEditControl::UpdateText(bool sendUpdate)
+{
+ m_smsTimer.Stop();
+ if (sendUpdate)
+ {
+ ValidateInput();
+
+ SEND_CLICK_MESSAGE(GetID(), GetParentID(), 0);
+
+ m_textChangeActions.ExecuteActions(GetID(), GetParentID());
+ }
+ SetInvalid();
+}
+
+void CGUIEditControl::SetInputType(CGUIEditControl::INPUT_TYPE type, const CVariant& heading)
+{
+ m_inputType = type;
+ if (heading.isString())
+ m_inputHeading = heading.asString();
+ else if (heading.isInteger() && heading.asInteger())
+ m_inputHeading = g_localizeStrings.Get(static_cast<uint32_t>(heading.asInteger()));
+ //! @todo Verify the current input string?
+}
+
+void CGUIEditControl::RecalcLabelPosition()
+{
+ // ensure that our cursor is within our width
+ ValidateCursor();
+
+ std::wstring text = GetDisplayedText();
+ m_textWidth = m_label.CalcTextWidth(text + L'|');
+ float beforeCursorWidth = m_label.CalcTextWidth(text.substr(0, m_cursorPos));
+ float afterCursorWidth = m_label.CalcTextWidth(text.substr(0, m_cursorPos) + L'|');
+ float leftTextWidth = m_label.GetRenderRect().Width();
+ float maxTextWidth = m_label.GetMaxWidth();
+ if (leftTextWidth > 0)
+ maxTextWidth -= leftTextWidth + spaceWidth;
+
+ // if skinner forgot to set height :p
+ if (m_height == 0 && m_label.GetLabelInfo().font)
+ m_height = m_label.GetLabelInfo().font->GetTextHeight(1);
+
+ if (m_textWidth > maxTextWidth)
+ { // we render taking up the full width, so make sure our cursor position is
+ // within the render window
+ if (m_textOffset + afterCursorWidth > maxTextWidth)
+ {
+ // move the position to the left (outside of the viewport)
+ m_textOffset = maxTextWidth - afterCursorWidth;
+ }
+ else if (m_textOffset + beforeCursorWidth < 0) // offscreen to the left
+ {
+ // otherwise use original position
+ m_textOffset = -beforeCursorWidth;
+ }
+ else if (m_textOffset + m_textWidth < maxTextWidth)
+ { // we have more text than we're allowed, but we aren't filling all the space
+ m_textOffset = maxTextWidth - m_textWidth;
+ }
+ }
+ else
+ m_textOffset = 0;
+}
+
+void CGUIEditControl::ProcessText(unsigned int currentTime)
+{
+ if (m_smsTimer.IsRunning() && m_smsTimer.GetElapsedMilliseconds() > smsDelay)
+ UpdateText();
+
+ if (m_bInvalidated)
+ {
+ m_label.SetMaxRect(m_posX, m_posY, m_width, m_height);
+ m_label.SetText(m_info.GetLabel(GetParentID()));
+ RecalcLabelPosition();
+ }
+
+ bool changed = false;
+
+ m_clipRect.x1 = m_label.GetRenderRect().x1;
+ m_clipRect.x2 = m_clipRect.x1 + m_label.GetMaxWidth();
+ m_clipRect.y1 = m_posY;
+ m_clipRect.y2 = m_posY + m_height;
+
+ // start by rendering the normal text
+ float leftTextWidth = m_label.GetRenderRect().Width();
+ if (leftTextWidth > 0)
+ {
+ // render the text on the left
+ changed |= m_label.SetColor(GetTextColor());
+ changed |= m_label.Process(currentTime);
+
+ m_clipRect.x1 += leftTextWidth + spaceWidth;
+ }
+
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_clipRect.x1, m_clipRect.y1, m_clipRect.Width(), m_clipRect.Height()))
+ {
+ uint32_t align = m_label.GetLabelInfo().align & XBFONT_CENTER_Y; // start aligned left
+ if (m_label2.GetTextWidth() < m_clipRect.Width())
+ { // align text as our text fits
+ if (leftTextWidth > 0)
+ { // right align as we have 2 labels
+ align |= XBFONT_RIGHT;
+ }
+ else
+ { // align by whatever the skinner requests
+ align |= (m_label2.GetLabelInfo().align & 3);
+ }
+ }
+ changed |= m_label2.SetMaxRect(m_clipRect.x1 + m_textOffset, m_posY, m_clipRect.Width() - m_textOffset, m_height);
+
+ std::wstring text = GetDisplayedText();
+ std::string hint = m_hintInfo.GetLabel(GetParentID());
+
+ if (!HasFocus() && text.empty() && !hint.empty())
+ {
+ changed |= m_label2.SetText(hint);
+ }
+ else if ((HasFocus() || GetParentID() == WINDOW_DIALOG_KEYBOARD) &&
+ m_inputType != INPUT_TYPE_READONLY)
+ {
+ changed |= SetStyledText(text);
+ }
+ else
+ changed |= m_label2.SetTextW(text);
+
+ changed |= m_label2.SetAlign(align);
+ changed |= m_label2.SetColor(GetTextColor());
+ changed |= m_label2.SetOverflow(CGUILabel::OVER_FLOW_CLIP);
+ changed |= m_label2.Process(currentTime);
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+ if (changed)
+ MarkDirtyRegion();
+}
+
+void CGUIEditControl::RenderText()
+{
+ m_label.Render();
+
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_clipRect.x1, m_clipRect.y1, m_clipRect.Width(), m_clipRect.Height()))
+ {
+ m_label2.Render();
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+}
+
+CGUILabel::COLOR CGUIEditControl::GetTextColor() const
+{
+ CGUILabel::COLOR color = CGUIButtonControl::GetTextColor();
+ if (color != CGUILabel::COLOR_DISABLED && HasInvalidInput())
+ return CGUILabel::COLOR_INVALID;
+
+ return color;
+}
+
+void CGUIEditControl::SetHint(const GUIINFO::CGUIInfoLabel& hint)
+{
+ m_hintInfo = hint;
+}
+
+std::wstring CGUIEditControl::GetDisplayedText() const
+{
+ std::wstring text(m_text2);
+ if (m_inputType == INPUT_TYPE_PASSWORD || m_inputType == INPUT_TYPE_PASSWORD_MD5 || m_inputType == INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW)
+ {
+ text.clear();
+ if (m_smsTimer.IsRunning())
+ { // using the remove to input, so display the last key input
+ text.append(m_cursorPos - 1, L'*');
+ text.append(1, m_text2[m_cursorPos - 1]);
+ text.append(m_text2.size() - m_cursorPos, L'*');
+ }
+ else
+ text.append(m_text2.size(), L'*');
+ }
+ else if (!m_edit.empty())
+ text.insert(m_editOffset, m_edit);
+ return text;
+}
+
+bool CGUIEditControl::SetStyledText(const std::wstring &text)
+{
+ vecText styled;
+ styled.reserve(text.size() + 1);
+
+ std::vector<UTILS::COLOR::Color> colors;
+ colors.push_back(m_label.GetLabelInfo().textColor);
+ colors.push_back(m_label.GetLabelInfo().disabledColor);
+ UTILS::COLOR::Color select = m_label.GetLabelInfo().selectedColor;
+ if (!select)
+ select = 0xFFFF0000;
+ colors.push_back(select);
+ colors.push_back(0x00FFFFFF);
+
+ unsigned int startHighlight = m_cursorPos;
+ unsigned int endHighlight = m_cursorPos + m_edit.size();
+ unsigned int startSelection = m_cursorPos + m_editOffset;
+ unsigned int endSelection = m_cursorPos + m_editOffset + m_editLength;
+
+ CGUIFont* font = m_label2.GetLabelInfo().font;
+ uint32_t style = (font ? font->GetStyle() : (FONT_STYLE_NORMAL & FONT_STYLE_MASK)) << 24;
+
+ for (unsigned int i = 0; i < text.size(); i++)
+ {
+ uint32_t ch = text[i] | style;
+ if (m_editLength > 0 && startSelection <= i && i < endSelection)
+ ch |= (2 << 16); // highlight the letters we're playing with
+ else if (!m_edit.empty() && (i < startHighlight || i >= endHighlight))
+ ch |= (1 << 16); // dim the bits we're not editing
+ styled.push_back(ch);
+ }
+
+ // show the cursor
+ uint32_t ch = L'|' | style;
+ if ((++m_cursorBlink % 64) > 32)
+ ch |= (3 << 16);
+ styled.insert(styled.begin() + m_cursorPos, ch);
+
+ return m_label2.SetStyledText(styled, colors);
+}
+
+void CGUIEditControl::ValidateCursor()
+{
+ if (m_cursorPos > m_text2.size())
+ m_cursorPos = m_text2.size();
+}
+
+void CGUIEditControl::SetLabel(const std::string &text)
+{
+ CGUIButtonControl::SetLabel(text);
+ SetInvalid();
+}
+
+void CGUIEditControl::SetLabel2(const std::string &text)
+{
+ m_edit.clear();
+ std::wstring newText;
+ g_charsetConverter.utf8ToW(text, newText, false);
+ if (newText != m_text2)
+ {
+ m_isMD5 = (m_inputType == INPUT_TYPE_PASSWORD_MD5 || m_inputType == INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW);
+ m_text2 = newText;
+ m_cursorPos = m_text2.size();
+ ValidateInput();
+ SetInvalid();
+ }
+}
+
+std::string CGUIEditControl::GetLabel2() const
+{
+ std::string text;
+ g_charsetConverter.wToUTF8(m_text2, text);
+ if (m_inputType == INPUT_TYPE_PASSWORD_MD5 && !m_isMD5)
+ return CDigest::Calculate(CDigest::Type::MD5, text);
+ return text;
+}
+
+bool CGUIEditControl::ClearMD5()
+{
+ if (!(m_inputType == INPUT_TYPE_PASSWORD_MD5 || m_inputType == INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW) || !m_isMD5)
+ return false;
+
+ m_text2.clear();
+ m_cursorPos = 0;
+ if (m_inputType != INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW)
+ m_isMD5 = false;
+ return true;
+}
+
+unsigned int CGUIEditControl::GetCursorPosition() const
+{
+ return m_cursorPos;
+}
+
+void CGUIEditControl::SetCursorPosition(unsigned int iPosition)
+{
+ m_cursorPos = iPosition;
+}
+
+void CGUIEditControl::OnSMSCharacter(unsigned int key)
+{
+ assert(key < 10);
+ if (m_smsTimer.IsRunning())
+ {
+ // we're already entering an SMS character
+ if (key != m_smsLastKey || m_smsTimer.GetElapsedMilliseconds() > smsDelay)
+ { // a different key was clicked than last time, or we have timed out
+ m_smsLastKey = key;
+ m_smsKeyIndex = 0;
+ }
+ else
+ { // same key as last time within the appropriate time period
+ m_smsKeyIndex++;
+ if (m_cursorPos)
+ m_text2.erase(--m_cursorPos, 1);
+ }
+ }
+ else
+ { // key is pressed for the first time
+ m_smsLastKey = key;
+ m_smsKeyIndex = 0;
+ }
+
+ m_smsKeyIndex = m_smsKeyIndex % strlen(smsLetters[key]);
+
+ m_text2.insert(m_text2.begin() + m_cursorPos++, smsLetters[key][m_smsKeyIndex]);
+ UpdateText();
+ m_smsTimer.StartZero();
+}
+
+void CGUIEditControl::OnPasteClipboard()
+{
+ std::wstring unicode_text;
+ std::string utf8_text;
+
+ // Get text from the clipboard
+ utf8_text = CServiceBroker::GetWinSystem()->GetClipboardText();
+ g_charsetConverter.utf8ToW(utf8_text, unicode_text, false);
+
+ // Insert the pasted text at the current cursor position.
+ if (unicode_text.length() > 0)
+ {
+ std::wstring left_end = m_text2.substr(0, m_cursorPos);
+ std::wstring right_end = m_text2.substr(m_cursorPos);
+
+ m_text2 = left_end;
+ m_text2.append(unicode_text);
+ m_text2.append(right_end);
+ m_cursorPos += unicode_text.length();
+ UpdateText();
+ }
+}
+
+void CGUIEditControl::SetInputValidation(StringValidation::Validator inputValidator, void *data /* = NULL */)
+{
+ if (m_inputValidator == inputValidator)
+ return;
+
+ m_inputValidator = inputValidator;
+ m_inputValidatorData = data;
+ // the input validator has changed, so re-validate the current data
+ ValidateInput();
+}
+
+bool CGUIEditControl::ValidateInput(const std::wstring &data) const
+{
+ if (m_inputValidator == NULL)
+ return true;
+
+ return m_inputValidator(GetLabel2(), m_inputValidatorData != NULL ? m_inputValidatorData : const_cast<void*>((const void*)this));
+}
+
+void CGUIEditControl::ValidateInput()
+{
+ // validate the input
+ bool invalid = !ValidateInput(m_text2);
+ // nothing to do if still valid/invalid
+ if (invalid != m_invalidInput)
+ {
+ // the validity state has changed so we need to update the control
+ m_invalidInput = invalid;
+
+ // let the window/dialog know that the validity has changed
+ CGUIMessage msg(GUI_MSG_VALIDITY_CHANGED, GetID(), GetID(), m_invalidInput ? 0 : 1);
+ SendWindowMessage(msg);
+
+ SetInvalid();
+ }
+}
+
+void CGUIEditControl::SetFocus(bool focus)
+{
+ m_smsTimer.Stop();
+ CGUIControl::SetFocus(focus);
+ SetInvalid();
+}
+
+std::string CGUIEditControl::GetDescriptionByIndex(int index) const
+{
+ if (index == 0)
+ return GetDescription();
+ else if(index == 1)
+ return GetLabel2();
+
+ return "";
+}
diff --git a/xbmc/guilib/GUIEditControl.dox b/xbmc/guilib/GUIEditControl.dox
new file mode 100644
index 0000000..9cad4d0
--- /dev/null
+++ b/xbmc/guilib/GUIEditControl.dox
@@ -0,0 +1,60 @@
+/*!
+
+\page skin_Edit_control Edit control
+\brief **Used as an input control for the osd keyboard and other input fields.**
+
+\tableofcontents
+
+The edit control allows a user to input text in Kodi. You can choose the font,
+size, colour, location and header of the text to be displayed.
+
+--------------------------------------------------------------------------------
+\section skin_Edit_control_sect1 Example
+
+~~~~~~~~~~~~~
+ <control type="edit" id="1">
+ <description>My First edit control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <visible>true</visible>
+ <aligny>center</aligny>
+ <label>Search</label>
+ <hinttext>Enter search string</hinttext>
+ <font>font14</font>
+ <textoffsetx>10</textoffsetx>
+ <textcolor>FFB2D4F5</textcolor>
+ <disabledcolor>FF000000</disabledcolor>
+ <invalidcolor>FFFFFFFF</invalidcolor>
+ <texturefocus>button-focus.png</texturefocus>
+ <texturenofocus>button-nofocus.png</texturenofocus>
+ <pulseonselect>no</pulseonselect>
+ </control>
+~~~~~~~~~~~~~
+
+--------------------------------------------------------------------------------
+\section skin_Edit_control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| aligny | Can be top or center. Aligns the text within its given control <b>`<height>`</b>. Defaults to top
+| label | Specifies the header text which should be shown. You should specify an entry from the **strings.po** here (either the Kodi strings.po or your skin's strings.po file), however you may also hardcode a piece of text also if you wish, though of course it will not be localized. You can use the full [label formatting syntax](http://kodi.wiki/view/Label_Formatting) and [you may also specify more than one piece of information here by using the $INFO and $LOCALIZE formats.strings.po](http://kodi.wiki/view/Label_Parsing))
+| hinttext | Specifies the text which should be displayed in the edit label control, until the user enters some text. It can be used to provide a clue as to what a user should enter in this control.
+| font | Specifies the font to use from the **font.xml** file.
+| textcolor | Specifies the color the text should be, in hex **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| textwidth | Will truncate any text that's too long.
+
+
+--------------------------------------------------------------------------------
+\section skin_Edit_control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIEditControl.h b/xbmc/guilib/GUIEditControl.h
new file mode 100644
index 0000000..9f44f8c
--- /dev/null
+++ b/xbmc/guilib/GUIEditControl.h
@@ -0,0 +1,131 @@
+/*
+ * 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
+
+/*!
+\file GUIEditControl.h
+\brief
+*/
+
+#include "GUIButtonControl.h"
+#include "utils/Stopwatch.h"
+#include "utils/StringValidation.h"
+#include "utils/Variant.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+
+class CGUIEditControl : public CGUIButtonControl
+{
+public:
+ enum INPUT_TYPE {
+ INPUT_TYPE_READONLY = -1,
+ INPUT_TYPE_TEXT = 0,
+ INPUT_TYPE_NUMBER,
+ INPUT_TYPE_SECONDS,
+ INPUT_TYPE_TIME,
+ INPUT_TYPE_DATE,
+ INPUT_TYPE_IPADDRESS,
+ INPUT_TYPE_PASSWORD,
+ INPUT_TYPE_PASSWORD_MD5,
+ INPUT_TYPE_SEARCH,
+ INPUT_TYPE_FILTER,
+ INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW
+ };
+
+ CGUIEditControl(int parentID, int controlID, float posX, float posY,
+ float width, float height, const CTextureInfo &textureFocus, const CTextureInfo &textureNoFocus,
+ const CLabelInfo& labelInfo, const std::string &text);
+ explicit CGUIEditControl(const CGUIButtonControl &button);
+ ~CGUIEditControl(void) override;
+ CGUIEditControl* Clone() const override { return new CGUIEditControl(*this); }
+
+ bool OnMessage(CGUIMessage &message) override;
+ bool OnAction(const CAction &action) override;
+ void OnClick() override;
+
+ void SetLabel(const std::string &text) override;
+ void SetLabel2(const std::string &text) override;
+ void SetHint(const KODI::GUILIB::GUIINFO::CGUIInfoLabel& hint);
+
+ std::string GetLabel2() const override;
+
+ unsigned int GetCursorPosition() const;
+ void SetCursorPosition(unsigned int iPosition);
+
+ void SetInputType(INPUT_TYPE type, const CVariant& heading);
+
+ void SetTextChangeActions(const CGUIAction& textChangeActions)
+ {
+ m_textChangeActions = textChangeActions;
+ }
+
+ bool HasTextChangeActions() const { return m_textChangeActions.HasActionsMeetingCondition(); }
+
+ virtual bool HasInvalidInput() const { return m_invalidInput; }
+ virtual void SetInputValidation(StringValidation::Validator inputValidator, void *data = NULL);
+
+protected:
+ void SetFocus(bool focus) override;
+ void ProcessText(unsigned int currentTime) override;
+ void RenderText() override;
+ CGUILabel::COLOR GetTextColor() const override;
+ std::wstring GetDisplayedText() const;
+ std::string GetDescriptionByIndex(int index) const override;
+ bool SetStyledText(const std::wstring &text);
+ void RecalcLabelPosition();
+ void ValidateCursor();
+ void UpdateText(bool sendUpdate = true);
+ void OnPasteClipboard();
+ void OnSMSCharacter(unsigned int key);
+ void DefaultConstructor();
+
+ virtual bool ValidateInput(const std::wstring &data) const;
+ void ValidateInput();
+
+ /*! \brief Clear out the current text input if it's an MD5 password.
+ \return true if the password is cleared, false otherwise.
+ */
+ bool ClearMD5();
+
+ std::wstring m_text2;
+ std::string m_text;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_hintInfo;
+ float m_textOffset;
+ float m_textWidth;
+ CRect m_clipRect; ///< clipping rect for the second label
+
+ static const int spaceWidth = 5;
+
+ unsigned int m_cursorPos;
+ unsigned int m_cursorBlink;
+
+ std::string m_inputHeading;
+ INPUT_TYPE m_inputType;
+ bool m_isMD5;
+
+ CGUIAction m_textChangeActions;
+
+ bool m_invalidInput;
+ StringValidation::Validator m_inputValidator;
+ void *m_inputValidatorData;
+
+ unsigned int m_smsKeyIndex;
+ unsigned int m_smsLastKey;
+ CStopWatch m_smsTimer;
+
+ std::wstring m_edit;
+ int m_editOffset;
+ int m_editLength;
+
+ static const char* smsLetters[10];
+ static const unsigned int smsDelay;
+};
diff --git a/xbmc/guilib/GUIFadeLabelControl.cpp b/xbmc/guilib/GUIFadeLabelControl.cpp
new file mode 100644
index 0000000..5855f3c
--- /dev/null
+++ b/xbmc/guilib/GUIFadeLabelControl.cpp
@@ -0,0 +1,267 @@
+/*
+ * 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 "GUIFadeLabelControl.h"
+
+#include "GUIMessage.h"
+#include "utils/Random.h"
+
+using namespace KODI::GUILIB;
+
+CGUIFadeLabelControl::CGUIFadeLabelControl(int parentID, int controlID, float posX, float posY, float width, float height, const CLabelInfo& labelInfo, bool scrollOut, unsigned int timeToDelayAtEnd, bool resetOnLabelChange, bool randomized)
+ : CGUIControl(parentID, controlID, posX, posY, width, height), m_label(labelInfo), m_scrollInfo(50, labelInfo.offsetX, labelInfo.scrollSpeed)
+ , m_textLayout(labelInfo.font, false)
+ , m_fadeAnim(CAnimation::CreateFader(100, 0, timeToDelayAtEnd, 200))
+{
+ m_currentLabel = 0;
+ ControlType = GUICONTROL_FADELABEL;
+ m_scrollOut = scrollOut;
+ m_fadeAnim.ApplyAnimation();
+ m_lastLabel = -1;
+ m_scrollSpeed = labelInfo.scrollSpeed; // save it for later
+ m_resetOnLabelChange = resetOnLabelChange;
+ m_shortText = true;
+ m_scroll = true;
+ m_randomized = randomized;
+}
+
+CGUIFadeLabelControl::CGUIFadeLabelControl(const CGUIFadeLabelControl &from)
+: CGUIControl(from), m_infoLabels(from.m_infoLabels), m_label(from.m_label), m_scrollInfo(from.m_scrollInfo), m_textLayout(from.m_textLayout),
+ m_fadeAnim(from.m_fadeAnim)
+{
+ m_scrollOut = from.m_scrollOut;
+ m_scrollSpeed = from.m_scrollSpeed;
+ m_resetOnLabelChange = from.m_resetOnLabelChange;
+
+ m_fadeAnim.ApplyAnimation();
+ m_currentLabel = 0;
+ m_lastLabel = -1;
+ ControlType = GUICONTROL_FADELABEL;
+ m_shortText = from.m_shortText;
+ m_scroll = from.m_scroll;
+ m_randomized = from.m_randomized;
+ m_allLabelsShown = from.m_allLabelsShown;
+}
+
+CGUIFadeLabelControl::~CGUIFadeLabelControl(void) = default;
+
+void CGUIFadeLabelControl::SetInfo(const std::vector<GUIINFO::CGUIInfoLabel> &infoLabels)
+{
+ m_lastLabel = -1;
+ m_infoLabels = infoLabels;
+ m_allLabelsShown = m_infoLabels.empty();
+ if (m_randomized)
+ KODI::UTILS::RandomShuffle(m_infoLabels.begin(), m_infoLabels.end());
+}
+
+void CGUIFadeLabelControl::AddLabel(const std::string &label)
+{
+ m_infoLabels.emplace_back(label, "", GetParentID());
+ m_allLabelsShown = false;
+}
+
+void CGUIFadeLabelControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_infoLabels.empty() || !m_label.font)
+ {
+ CGUIControl::Process(currentTime, dirtyregions);
+ return;
+ }
+
+ if (m_currentLabel >= m_infoLabels.size() )
+ m_currentLabel = 0;
+
+ if (m_textLayout.Update(GetLabel()))
+ { // changed label - update our suffix based on length of available text
+ float width, height;
+ m_textLayout.GetTextExtent(width, height);
+ float spaceWidth = m_label.font->GetCharWidth(L' ');
+ unsigned int numSpaces = (unsigned int)(m_width / spaceWidth) + 1;
+ if (width < m_width) // append spaces for scrolling
+ numSpaces += (unsigned int)((m_width - width) / spaceWidth) + 1;
+ m_shortText = (width + m_label.offsetX) < m_width;
+ m_scrollInfo.m_suffix.assign(numSpaces, ' ');
+ if (m_resetOnLabelChange)
+ {
+ m_scrollInfo.Reset();
+ m_fadeAnim.ResetAnimation();
+ }
+ MarkDirtyRegion();
+ }
+
+ if (m_shortText && m_infoLabels.size() == 1)
+ m_allLabelsShown = true;
+
+ if (m_currentLabel != m_lastLabel)
+ { // new label - reset scrolling
+ m_scrollInfo.Reset();
+ m_fadeAnim.QueueAnimation(ANIM_PROCESS_REVERSE);
+ m_lastLabel = m_currentLabel;
+ MarkDirtyRegion();
+ }
+
+ if (m_infoLabels.size() > 1 || !m_shortText)
+ { // have scrolling text
+ bool moveToNextLabel = false;
+ if (!m_scrollOut)
+ {
+ if (m_scrollInfo.m_pixelPos + m_width > m_scrollInfo.m_textWidth)
+ {
+ if (m_fadeAnim.GetProcess() != ANIM_PROCESS_NORMAL)
+ m_fadeAnim.QueueAnimation(ANIM_PROCESS_NORMAL);
+ moveToNextLabel = true;
+ }
+ }
+ else if (m_scrollInfo.m_pixelPos > m_scrollInfo.m_textWidth)
+ moveToNextLabel = true;
+
+ if (m_scrollInfo.m_pixelSpeed || m_fadeAnim.GetState() == ANIM_STATE_IN_PROCESS)
+ MarkDirtyRegion();
+
+ // apply the fading animation
+ TransformMatrix matrix;
+ m_fadeAnim.Animate(currentTime, true);
+ m_fadeAnim.RenderAnimation(matrix);
+ m_fadeMatrix = CServiceBroker::GetWinSystem()->GetGfxContext().AddTransform(matrix);
+
+ if (m_fadeAnim.GetState() == ANIM_STATE_APPLIED)
+ m_fadeAnim.ResetAnimation();
+
+ m_scrollInfo.SetSpeed((m_fadeAnim.GetProcess() == ANIM_PROCESS_NONE) ? m_scrollSpeed : 0);
+
+ if (moveToNextLabel)
+ { // increment the label and reset scrolling
+ if (m_fadeAnim.GetProcess() != ANIM_PROCESS_NORMAL)
+ {
+ if (++m_currentLabel >= m_infoLabels.size())
+ {
+ m_currentLabel = 0;
+ m_allLabelsShown = true;
+ }
+ m_scrollInfo.Reset();
+ m_fadeAnim.QueueAnimation(ANIM_PROCESS_REVERSE);
+ }
+ }
+
+ if (m_scroll)
+ {
+ m_textLayout.UpdateScrollinfo(m_scrollInfo);
+ MarkDirtyRegion();
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+ }
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+bool CGUIFadeLabelControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_label.UpdateColors();
+
+ return changed;
+}
+
+void CGUIFadeLabelControl::Render()
+{
+ if (!m_label.font)
+ { // nothing to render
+ CGUIControl::Render();
+ return ;
+ }
+
+ float posY = m_posY;
+ if (m_label.align & XBFONT_CENTER_Y)
+ posY += m_height * 0.5f;
+ if (m_infoLabels.size() == 1 && m_shortText)
+ { // single label set and no scrolling required - just display
+ float posX = m_posX + m_label.offsetX;
+ if (m_label.align & XBFONT_CENTER_X)
+ posX = m_posX + m_width * 0.5f;
+ else if (m_label.align & XBFONT_RIGHT)
+ posX = m_posX + m_width;
+ m_textLayout.Render(posX, posY, m_label.angle, m_label.textColor, m_label.shadowColor, m_label.align, m_width - m_label.offsetX);
+ CGUIControl::Render();
+ return;
+ }
+
+ // render the scrolling text
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetTransform(m_fadeMatrix);
+ if (!m_scroll || (!m_scrollOut && m_shortText))
+ {
+ float posX = m_posX + m_label.offsetX;
+ if (m_label.align & XBFONT_CENTER_X)
+ posX = m_posX + m_width * 0.5f;
+ else if (m_label.align & XBFONT_RIGHT)
+ posX = m_posX + m_width;
+ m_textLayout.Render(posX, posY, 0, m_label.textColor, m_label.shadowColor, m_label.align, m_width);
+ }
+ else
+ m_textLayout.RenderScrolling(m_posX, posY, 0, m_label.textColor, m_label.shadowColor, (m_label.align & ~3), m_width, m_scrollInfo);
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+ CGUIControl::Render();
+}
+
+
+bool CGUIFadeLabelControl::CanFocus() const
+{
+ return false;
+}
+
+
+bool CGUIFadeLabelControl::OnMessage(CGUIMessage& message)
+{
+ if ( message.GetControlId() == GetID() )
+ {
+ if (message.GetMessage() == GUI_MSG_LABEL_ADD)
+ {
+ AddLabel(message.GetLabel());
+ return true;
+ }
+ if (message.GetMessage() == GUI_MSG_LABEL_RESET)
+ {
+ m_lastLabel = -1;
+ m_infoLabels.clear();
+ m_allLabelsShown = true;
+ m_scrollInfo.Reset();
+ return true;
+ }
+ if (message.GetMessage() == GUI_MSG_LABEL_SET)
+ {
+ m_lastLabel = -1;
+ m_infoLabels.clear();
+ m_allLabelsShown = true;
+ m_scrollInfo.Reset();
+ AddLabel(message.GetLabel());
+ return true;
+ }
+ }
+ return CGUIControl::OnMessage(message);
+}
+
+std::string CGUIFadeLabelControl::GetDescription() const
+{
+ return (m_currentLabel < m_infoLabels.size()) ? m_infoLabels[m_currentLabel].GetLabel(m_parentID) : "";
+}
+
+std::string CGUIFadeLabelControl::GetLabel()
+{
+ if (m_currentLabel > m_infoLabels.size())
+ m_currentLabel = 0;
+
+ unsigned int numTries = 0;
+ std::string label(m_infoLabels[m_currentLabel].GetLabel(m_parentID));
+ while (label.empty() && ++numTries < m_infoLabels.size())
+ {
+ if (++m_currentLabel >= m_infoLabels.size())
+ m_currentLabel = 0;
+ label = m_infoLabels[m_currentLabel].GetLabel(m_parentID);
+ }
+ return label;
+}
diff --git a/xbmc/guilib/GUIFadeLabelControl.dox b/xbmc/guilib/GUIFadeLabelControl.dox
new file mode 100644
index 0000000..a64ab30
--- /dev/null
+++ b/xbmc/guilib/GUIFadeLabelControl.dox
@@ -0,0 +1,72 @@
+/*!
+
+\page Fade_Label_Control Fade label control
+\brief **Used to show multiple pieces of text in the same position, by fading from one to the other.**
+
+\tableofcontents
+
+The fade label control is used for displaying multiple pieces of text in the
+same space in Kodi. You can choose the font, size, colour, location and contents
+of the text to be displayed. The first piece of information to display fades in
+over 50 frames, then scrolls off to the left. Once it is finished scrolling off
+screen, the second piece of information fades in and the process repeats. A fade
+label control is not supported in a list container.
+
+--------------------------------------------------------------------------------
+\section Fade_Label_Control_sect1 Example
+
+~~~~~~~~~~~~~
+ <control type="fadelabel" id="1">
+ <description>My First fadelabel</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <visible>true</visible>
+ <scrollout>true</scrollout>
+ <pauseatend>200</pauseatend>
+ <label>6</label>
+ <info>MusicPlayer.Genre</info>
+ <info>MusicPlayer.Artist</info>
+ <info>MusicPlayer.Album</info>
+ <info>MusicPlayer.Year</info>
+ <font>font14</font>
+ <textcolor>FFB2D4F5</textcolor>
+ <textoffsetx>20</textoffsetx>
+ <scroll>true</scroll>
+ <randomize>true</randomize>
+ </control>
+~~~~~~~~~~~~~
+
+--------------------------------------------------------------------------------
+\section Fade_Label_Control_sect2 Tag descriptions
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|--------------------:|:--------------------------------------------------------------|
+| label | Specifies the text which should be drawn. You should specify an entry from the strings.po here, however you may also specify a piece of text yourself if you wish, though of course it will not be localisable. [You may also specify more than one piece of information here by using the $INFO and $LOCALIZE formats](http://kodi.wiki/view/Label_Parsing).
+| info | Specifies the information that should be presented. Kodi will auto-fill in this info in place of the <b>`<label>`</b>. [See here for more information](http://kodi.wiki/view/InfoLabels).
+| font | Specifies the font to use from the font.xml file.
+| textcolor | Specified the color the text should be, in hex **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| textoffsetx | Specify the offset from the left edge that the text should be rendered at when static (not scrolling). The scrolling text will still scroll using the full <b>`<width>`</b> of the control.
+| shadowcolor | Specifies the color of the drop shadow on the text, in **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| angle | Specifies the angle at which the text should be drawn, measured counter clockwise from the horizontal.
+| scrollout | If set to False the fadelabel will only scroll until the last char is to the right side of the width of the fadelabel instead of all the way out to the left.
+| pauseatend | Specifies the time that the text will wait until it fades away before it scrolls again or moves to the next item.
+| resetonlabelchange | If set to false the fadelabel will not reset the scrolling offset when the label's content changes. Useful if you have things such as the play time (in seconds) inside a fadelabel. Defaults to true.
+| scrollspeed | Scroll speed of text in pixels per second. Defaults to 60.
+| scroll | If set to false, the labels won't scroll. Defaults to true.
+| randomize | If set to true, the labels will be displayed in a random order. Defaults to false.
+
+
+--------------------------------------------------------------------------------
+\section Fade_Label_Control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIFadeLabelControl.h b/xbmc/guilib/GUIFadeLabelControl.h
new file mode 100644
index 0000000..9f5f7ab
--- /dev/null
+++ b/xbmc/guilib/GUIFadeLabelControl.h
@@ -0,0 +1,78 @@
+/*
+ * 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
+
+/*!
+\file GUIFadeLabelControl.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUILabel.h"
+#include "guilib/guiinfo/GUIInfoLabel.h"
+
+#include <vector>
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIFadeLabelControl : public CGUIControl
+{
+public:
+ CGUIFadeLabelControl(int parentID, int controlID, float posX, float posY, float width, float height, const CLabelInfo& labelInfo, bool scrollOut, unsigned int timeToDelayAtEnd, bool resetOnLabelChange, bool randomized);
+ CGUIFadeLabelControl(const CGUIFadeLabelControl &from);
+ ~CGUIFadeLabelControl(void) override;
+ CGUIFadeLabelControl* Clone() const override { return new CGUIFadeLabelControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool CanFocus() const override;
+ bool OnMessage(CGUIMessage& message) override;
+
+ void SetInfo(const std::vector<KODI::GUILIB::GUIINFO::CGUIInfoLabel> &vecInfo);
+ void SetScrolling(bool scroll) { m_scroll = scroll; }
+
+ bool AllLabelsShown() const { return m_allLabelsShown; }
+
+protected:
+ bool UpdateColors(const CGUIListItem* item) override;
+ std::string GetDescription() const override;
+ void AddLabel(const std::string &label);
+
+ /*! \brief retrieve the current label for display
+
+ The fadelabel has multiple labels which it cycles through. This routine retrieves the current label.
+ It first checks the current label and if non-empty returns it. Otherwise it will iterate through all labels
+ until it has a non-empty label to return.
+
+ \return the label that should be displayed. If empty, there is no label available.
+ */
+ std::string GetLabel();
+
+ std::vector<KODI::GUILIB::GUIINFO::CGUIInfoLabel> m_infoLabels;
+ unsigned int m_currentLabel;
+ unsigned int m_lastLabel;
+
+ CLabelInfo m_label;
+
+ bool m_scroll; // true if we scroll the text
+ bool m_scrollOut; // true if we scroll the text all the way to the left before fading in the next label
+ bool m_shortText; // true if the text we have is shorter than the width of the control
+
+ CScrollInfo m_scrollInfo;
+ CGUITextLayout m_textLayout;
+ CAnimation m_fadeAnim;
+ TransformMatrix m_fadeMatrix;
+ unsigned int m_scrollSpeed;
+ bool m_resetOnLabelChange;
+ bool m_randomized;
+ bool m_allLabelsShown = true;
+};
+
diff --git a/xbmc/guilib/GUIFixedListContainer.cpp b/xbmc/guilib/GUIFixedListContainer.cpp
new file mode 100644
index 0000000..0295736
--- /dev/null
+++ b/xbmc/guilib/GUIFixedListContainer.cpp
@@ -0,0 +1,308 @@
+/*
+ * 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 "GUIFixedListContainer.h"
+
+#include "GUIListItemLayout.h"
+#include "input/Key.h"
+
+CGUIFixedListContainer::CGUIFixedListContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems, int fixedPosition, int cursorRange)
+ : CGUIBaseContainer(parentID, controlID, posX, posY, width, height, orientation, scroller, preloadItems)
+{
+ ControlType = GUICONTAINER_FIXEDLIST;
+ m_type = VIEW_TYPE_LIST;
+ m_fixedCursor = fixedPosition;
+ m_cursorRange = std::max(0, cursorRange);
+ SetCursor(m_fixedCursor);
+}
+
+CGUIFixedListContainer::~CGUIFixedListContainer(void) = default;
+
+bool CGUIFixedListContainer::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_PAGE_UP:
+ {
+ Scroll(-m_itemsPerPage);
+ return true;
+ }
+ break;
+ case ACTION_PAGE_DOWN:
+ {
+ Scroll(m_itemsPerPage);
+ return true;
+ }
+ break;
+ // smooth scrolling (for analog controls)
+ case ACTION_SCROLL_UP:
+ {
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+ Scroll(-1);
+ }
+ return handled;
+ }
+ break;
+ case ACTION_SCROLL_DOWN:
+ {
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+ Scroll(1);
+ }
+ return handled;
+ }
+ break;
+ }
+ return CGUIBaseContainer::OnAction(action);
+}
+
+bool CGUIFixedListContainer::MoveUp(bool wrapAround)
+{
+ int item = GetSelectedItem();
+ if (item > 0)
+ SelectItem(item - 1);
+ else if (wrapAround)
+ {
+ SelectItem((int)m_items.size() - 1);
+ SetContainerMoving(-1);
+ }
+ else
+ return false;
+ return true;
+}
+
+bool CGUIFixedListContainer::MoveDown(bool wrapAround)
+{
+ int item = GetSelectedItem();
+ if (item < (int)m_items.size() - 1)
+ SelectItem(item + 1);
+ else if (wrapAround)
+ { // move first item in list
+ SelectItem(0);
+ SetContainerMoving(1);
+ }
+ else
+ return false;
+ return true;
+}
+
+void CGUIFixedListContainer::Scroll(int amount)
+{
+ // increase or decrease the offset within [-minCursor, m_items.size() - maxCursor]
+ int minCursor, maxCursor;
+ GetCursorRange(minCursor, maxCursor);
+ const int nextCursor = GetCursor() + amount;
+ int offset = GetOffset() + amount;
+ if (offset < -minCursor)
+ {
+ offset = -minCursor;
+ SetCursor(nextCursor < minCursor ? minCursor : nextCursor);
+ }
+ if (offset > (int)m_items.size() - 1 - maxCursor)
+ {
+ offset = m_items.size() - 1 - maxCursor;
+ SetCursor(nextCursor > maxCursor ? maxCursor : nextCursor);
+ }
+ ScrollToOffset(offset);
+}
+
+bool CGUIFixedListContainer::GetOffsetRange(int &minOffset, int &maxOffset) const
+{
+ GetCursorRange(minOffset, maxOffset);
+ minOffset = -minOffset;
+ maxOffset = m_items.size() - maxOffset - 1;
+ return true;
+}
+
+void CGUIFixedListContainer::ValidateOffset()
+{
+ if (!m_layout) return;
+ // ensure our fixed cursor position is valid
+ if (m_fixedCursor >= m_itemsPerPage)
+ m_fixedCursor = m_itemsPerPage - 1;
+ if (m_fixedCursor < 0)
+ m_fixedCursor = 0;
+ // compute our minimum and maximum cursor positions
+ int minCursor, maxCursor;
+ GetCursorRange(minCursor, maxCursor);
+ // assure our cursor is between these limits
+ SetCursor(std::max(GetCursor(), minCursor));
+ SetCursor(std::min(GetCursor(), maxCursor));
+ int minOffset, maxOffset;
+ GetOffsetRange(minOffset, maxOffset);
+ // and finally ensure our offset is valid
+ // don't validate offset if we are scrolling in case the tween image exceed <0, 1> range
+ if (GetOffset() > maxOffset || (!m_scroller.IsScrolling() && m_scroller.GetValue() > maxOffset * m_layout->Size(m_orientation)))
+ {
+ SetOffset(std::max(-minCursor, maxOffset));
+ m_scroller.SetValue(GetOffset() * m_layout->Size(m_orientation));
+ }
+ if (GetOffset() < minOffset || (!m_scroller.IsScrolling() && m_scroller.GetValue() < minOffset * m_layout->Size(m_orientation)))
+ {
+ SetOffset(minOffset);
+ m_scroller.SetValue(GetOffset() * m_layout->Size(m_orientation));
+ }
+}
+
+int CGUIFixedListContainer::GetCursorFromPoint(const CPoint &point, CPoint *itemPoint) const
+{
+ if (!m_focusedLayout || !m_layout)
+ return -1;
+ int minCursor, maxCursor;
+ GetCursorRange(minCursor, maxCursor);
+ // see if the point is either side of our focus range
+ float start = (minCursor + 0.2f) * m_layout->Size(m_orientation);
+ float end = (maxCursor - 0.2f) * m_layout->Size(m_orientation) + m_focusedLayout->Size(m_orientation);
+ float pos = (m_orientation == VERTICAL) ? point.y : point.x;
+ if (pos >= start && pos <= end)
+ { // select the appropriate item
+ pos -= minCursor * m_layout->Size(m_orientation);
+ for (int row = minCursor; row <= maxCursor; row++)
+ {
+ const CGUIListItemLayout *layout = (row == GetCursor()) ? m_focusedLayout : m_layout;
+ if (pos < layout->Size(m_orientation))
+ {
+ if (!InsideLayout(layout, point))
+ return -1;
+ return row;
+ }
+ pos -= layout->Size(m_orientation);
+ }
+ }
+ return -1;
+}
+
+bool CGUIFixedListContainer::SelectItemFromPoint(const CPoint &point)
+{
+ if (!m_focusedLayout || !m_layout)
+ return false;
+
+ MarkDirtyRegion();
+
+ const float mouse_scroll_speed = 0.25f;
+ const float mouse_max_amount = 1.5f;
+ float sizeOfItem = m_layout->Size(m_orientation);
+ int minCursor, maxCursor;
+ GetCursorRange(minCursor, maxCursor);
+ // see if the point is either side of our focus range
+ float start = (minCursor + 0.2f) * sizeOfItem;
+ float end = (maxCursor - 0.2f) * sizeOfItem + m_focusedLayout->Size(m_orientation);
+ float pos = (m_orientation == VERTICAL) ? point.y : point.x;
+ if (pos < start && GetOffset() > -minCursor)
+ { // scroll backward
+ if (!InsideLayout(m_layout, point))
+ return false;
+ float amount = std::min((start - pos) / sizeOfItem, mouse_max_amount);
+ m_analogScrollCount += amount * amount * mouse_scroll_speed;
+ if (m_analogScrollCount > 1)
+ {
+ ScrollToOffset(GetOffset() - 1);
+ m_analogScrollCount = 0;
+ }
+ return true;
+ }
+ else if (pos > end && GetOffset() + maxCursor < (int)m_items.size() - 1)
+ {
+ if (!InsideLayout(m_layout, point))
+ return false;
+ // scroll forward
+ float amount = std::min((pos - end) / sizeOfItem, mouse_max_amount);
+ m_analogScrollCount += amount * amount * mouse_scroll_speed;
+ if (m_analogScrollCount > 1)
+ {
+ ScrollToOffset(GetOffset() + 1);
+ m_analogScrollCount = 0;
+ }
+ return true;
+ }
+ else
+ { // select the appropriate item
+ int cursor = GetCursorFromPoint(point);
+ if (cursor < 0)
+ return false;
+ // calling SelectItem() here will focus the item and scroll, which isn't really what we're after
+ SetCursor(cursor);
+ return true;
+ }
+}
+
+void CGUIFixedListContainer::SelectItem(int item)
+{
+ // Check that GetOffset() is valid
+ ValidateOffset();
+ // only select an item if it's in a valid range
+ if (item >= 0 && item < (int)m_items.size())
+ {
+ // Select the item requested - we first set the cursor position
+ // which may be different at either end of the list, then the offset
+ int minCursor, maxCursor;
+ GetCursorRange(minCursor, maxCursor);
+
+ int cursor;
+ if ((int)m_items.size() - 1 - item <= maxCursor - m_fixedCursor)
+ cursor = std::max(m_fixedCursor, maxCursor + item - (int)m_items.size() + 1);
+ else if (item <= m_fixedCursor - minCursor)
+ cursor = std::min(m_fixedCursor, minCursor + item);
+ else
+ cursor = m_fixedCursor;
+ if (cursor != GetCursor())
+ SetContainerMoving(cursor - GetCursor());
+ SetCursor(cursor);
+ ScrollToOffset(item - GetCursor());
+ MarkDirtyRegion();
+ }
+}
+
+bool CGUIFixedListContainer::HasPreviousPage() const
+{
+ return (GetOffset() > 0);
+}
+
+bool CGUIFixedListContainer::HasNextPage() const
+{
+ return (GetOffset() < (int)m_items.size() - m_itemsPerPage && (int)m_items.size() >= m_itemsPerPage);
+}
+
+int CGUIFixedListContainer::GetCurrentPage() const
+{
+ int offset = CorrectOffset(GetOffset(), GetCursor());
+ if (offset + m_itemsPerPage - GetCursor() >= (int)GetRows()) // last page
+ return (GetRows() + m_itemsPerPage - 1) / m_itemsPerPage;
+ return offset / m_itemsPerPage + 1;
+}
+
+void CGUIFixedListContainer::GetCursorRange(int &minCursor, int &maxCursor) const
+{
+ minCursor = std::max(m_fixedCursor - m_cursorRange, 0);
+ maxCursor = std::min(m_fixedCursor + m_cursorRange, m_itemsPerPage);
+
+ if (!m_items.size())
+ {
+ minCursor = m_fixedCursor;
+ maxCursor = m_fixedCursor;
+ return;
+ }
+
+ while (maxCursor - minCursor > (int)m_items.size() - 1)
+ {
+ if (maxCursor - m_fixedCursor > m_fixedCursor - minCursor)
+ maxCursor--;
+ else
+ minCursor++;
+ }
+}
+
diff --git a/xbmc/guilib/GUIFixedListContainer.dox b/xbmc/guilib/GUIFixedListContainer.dox
new file mode 100644
index 0000000..b9e7726
--- /dev/null
+++ b/xbmc/guilib/GUIFixedListContainer.dox
@@ -0,0 +1,142 @@
+/*!
+
+\page Fixed_List_Container Fixed List Container
+\brief <b>Used for a list of items with a fixed focus. Same as the
+\ref Wrap_List_Container "Wrap List Container" except it doesn't wrap.</b>
+
+\tableofcontents
+
+The fixed list container is one of several containers used to display items from
+file lists in various ways. The fixed list container is the same as the
+[List Container](http://kodi.wiki/view/List_Container), with one exception: the
+focused item is fixed. Thus, moving up or down scrolls the items, not the focused
+position. As with all container controls, the layout of the items within the
+control is very flexible.
+
+~~~~~~~~~~~~~
+ <control type="fixedlist" id="50">
+ <description>My first fixed list container</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+ <viewtype label="3D list">list</viewtype>
+ <orientation>vertical</orientation>
+ <pagecontrol>25</pagecontrol>
+ <scrolltime tween="sine" easing="out">200</scrolltime>
+ <autoscroll>true</autoscroll>
+ <focusposition>4</focusposition>
+ <movement>6</movement>
+ <itemlayout width="250" height="29">
+ <control type="image">
+ <posx>5</posx>
+ <posy>3</posy>
+ <width>22</width>
+ <height>22</height>
+ <info>ListItem.Icon</info>
+ </control>
+ <control type="label">
+ <posx>30</posx>
+ <posy>3</posy>
+ <width>430</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <align>left</align>
+ <info>ListItem.Label</info>
+ </control>
+ <control type="label">
+ <posx>475</posx>
+ <posy>3</posy>
+ <width>300</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <textcolor>grey</textcolor>
+ <align>right</align>
+ <info>ListItem.Label2</info>
+ </control>
+ </itemlayout>
+ <focusedlayout height="29" width="250">
+ <control type="image">
+ <width>485</width>
+ <height>29</height>
+ <posx>0</posx>
+ <posy>0</posy>
+ <visible>Control.HasFocus(50)</visible>
+ <texture>list-focus.png</texture>
+ </control>
+ <control type="image">
+ <posx>5</posx>
+ <posy>3</posy>
+ <width>22</width>
+ <height>22</height>
+ <info>ListItem.Icon</info>
+ </control>
+ <control type="label">
+ <posx>30</posx>
+ <posy>3</posy>
+ <width>430</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <align>left</align>
+ <info>ListItem.Label</info>
+ </control>
+ <control type="label">
+ <posx>475</posx>
+ <posy>3</posy>
+ <width>300</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <textcolor>grey</textcolor>
+ <align>right</align>
+ <info>ListItem.Label2</info>
+ </control>
+ </focusedlayout>
+ </control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Fixed_List_Container_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is lower case only. This is
+important, as xml tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| viewtype | The type of view. Choices are list, icon, wide, wrap, biglist, bigicon, bigwide, bigwrap, info and biginfo. The label attribute indicates the label that will be used in the "View As" control within the GUI. It is localizable via **strings.po**. *viewtype* has no effect on the view itself. It is used by kodi when switching skin to automatically select a view with a similar layout. Skinners should try to set viewtype to describe the layout as best as possible.
+| orientation | The orientation of the list. Defaults to vertical.
+| pagecontrol | Used to set the <b>`<id>`</b> of the page control used to control this list.
+| scrolltime | The time (in ms) to scroll from one item to another. By default, this is 200ms. The list will scroll smoothly from one item to another as needed. Set it to zero to disable the smooth scrolling. The scroll movement can be further adjusted by selecting one of the available [tween](http://kodi.wiki/view/Tweeners) methods.
+| focusposition | Specifies the focus position (from 0 -> number of displayable items - 1). The focused position doesn't change - instead, the items move up or down (or left and right) as focus changes.
+| movement | This will make the focused position scroll again at the end of the list. (ie. <b>`<movement>6</movement>`</b> if there are only 6 items below the focus, the focus moves down to the bottom)
+| itemlayout | Specifies the layout of items in the list. Requires the height attribute set in a vertical list, and the width attribute set for a horizontal list. The <b>`<itemlayout>`</b> then contains as many label and image controls as required. [See here for more information](http://kodi.wiki/view/Container_Item_Layout).
+| focusedlayout | Specifies the layout of items in the list that have focus. Requires the height attribute set in a vertical list, and the width attribute set for a horizontal list. The <b>`<focusedlayout>`</b> then contains as many label and image controls as required. [See here for more information](http://kodi.wiki/view/Container_Item_Layout).
+| content | Used to set the item content that this list will contain. Allows the skinner to setup a list anywhere they want with a static set of content, as a useful alternative to the grouplist control. [See here for more information](http://kodi.wiki/view/Static_List_Content).
+| preloaditems | Used in association with the background image loader. [See here for more information](http://kodi.wiki/view/Background_Image_Loader).
+| autoscroll | Used to make the container scroll automatically
+
+
+--------------------------------------------------------------------------------
+\section Fixed_List_Container_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+
+*/
diff --git a/xbmc/guilib/GUIFixedListContainer.h b/xbmc/guilib/GUIFixedListContainer.h
new file mode 100644
index 0000000..b75ef11
--- /dev/null
+++ b/xbmc/guilib/GUIFixedListContainer.h
@@ -0,0 +1,60 @@
+/*
+ * 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
+
+/*!
+\file GUIFixedListContainer.h
+\brief
+*/
+
+#include "GUIBaseContainer.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIFixedListContainer : public CGUIBaseContainer
+{
+public:
+ CGUIFixedListContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems, int fixedPosition, int cursorRange);
+ ~CGUIFixedListContainer(void) override;
+ CGUIFixedListContainer* Clone() const override { return new CGUIFixedListContainer(*this); }
+
+ bool OnAction(const CAction &action) override;
+
+protected:
+ void Scroll(int amount) override;
+ bool MoveDown(bool wrapAround) override;
+ bool MoveUp(bool wrapAround) override;
+ bool GetOffsetRange(int &minOffset, int &maxOffset) const override;
+ void ValidateOffset() override;
+ bool SelectItemFromPoint(const CPoint &point) override;
+ int GetCursorFromPoint(const CPoint &point, CPoint *itemPoint = NULL) const override;
+ void SelectItem(int item) override;
+ bool HasNextPage() const override;
+ bool HasPreviousPage() const override;
+ int GetCurrentPage() const override;
+
+private:
+ /*!
+ \brief Get the minimal and maximal cursor positions in the list
+
+ As the fixed list cursor position may vary at the ends of the list based
+ on the cursor range, this function computes the minimal and maximal positions
+ based on the number of items in the list
+ \param minCursor the minimal cursor position
+ \param maxCursor the maximal cursor position
+ \sa m_fixedCursor, m_cursorRange
+ */
+ void GetCursorRange(int &minCursor, int &maxCursor) const;
+
+ int m_fixedCursor; ///< default position the skinner wishes to use for the focused item
+ int m_cursorRange; ///< range that the focused item can vary when at the ends of the list
+};
+
diff --git a/xbmc/guilib/GUIFont.cpp b/xbmc/guilib/GUIFont.cpp
new file mode 100644
index 0000000..e39a5b0
--- /dev/null
+++ b/xbmc/guilib/GUIFont.cpp
@@ -0,0 +1,353 @@
+/*
+ * 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 "GUIFont.h"
+
+#include "GUIFontTTF.h"
+#include "utils/CharsetConverter.h"
+#include "utils/MathUtils.h"
+#include "utils/TimeUtils.h"
+#include "windowing/GraphicContext.h"
+
+#include <mutex>
+
+#define ROUND(x) (float)(MathUtils::round_int(x))
+
+CScrollInfo::CScrollInfo(unsigned int wait /* = 50 */,
+ float pos /* = 0 */,
+ int speed /* = defaultSpeed */,
+ const std::string& scrollSuffix /* = " | " */)
+ : m_initialWait(wait), m_initialPos(pos)
+
+{
+ SetSpeed(speed ? speed : defaultSpeed);
+ std::wstring wsuffix;
+ g_charsetConverter.utf8ToW(scrollSuffix, wsuffix);
+ m_suffix.clear();
+ m_suffix.reserve(wsuffix.size());
+ m_suffix = vecText(wsuffix.begin(), wsuffix.end());
+ Reset();
+}
+
+float CScrollInfo::GetPixelsPerFrame()
+{
+ static const float alphaEMA = 0.05f;
+
+ if (0 == m_pixelSpeed)
+ return 0; // not scrolling
+
+ unsigned int currentTime = CTimeUtils::GetFrameTime();
+ float delta =
+ m_lastFrameTime ? static_cast<float>(currentTime - m_lastFrameTime) : m_averageFrameTime;
+ if (delta > 100)
+ delta = 100; // assume a minimum of 10 fps
+ m_lastFrameTime = currentTime;
+ // do an exponential moving average of the frame time
+ if (delta)
+ m_averageFrameTime = m_averageFrameTime + (delta - m_averageFrameTime) * alphaEMA;
+ // and multiply by pixel speed (per ms) to get number of pixels to move this frame
+ return m_pixelSpeed * m_averageFrameTime;
+}
+
+CGUIFont::CGUIFont(const std::string& strFontName,
+ uint32_t style,
+ UTILS::COLOR::Color textColor,
+ UTILS::COLOR::Color shadowColor,
+ float lineSpacing,
+ float origHeight,
+ CGUIFontTTF* font)
+ : m_strFontName(strFontName)
+{
+ m_style = style & FONT_STYLE_MASK;
+ m_textColor = textColor;
+ m_shadowColor = shadowColor;
+ m_lineSpacing = lineSpacing;
+ m_origHeight = origHeight;
+ m_font = font;
+
+ if (m_font)
+ m_font->AddReference();
+}
+
+CGUIFont::~CGUIFont()
+{
+ if (m_font)
+ m_font->RemoveReference();
+}
+
+std::string& CGUIFont::GetFontName()
+{
+ return m_strFontName;
+}
+
+void CGUIFont::DrawText(float x,
+ float y,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ UTILS::COLOR::Color shadowColor,
+ const vecText& text,
+ uint32_t alignment,
+ float maxPixelWidth)
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!m_font || !winSystem)
+ return;
+
+ CGraphicContext& context = winSystem->GetGfxContext();
+
+ bool clip = maxPixelWidth > 0;
+ if (clip && ClippedRegionIsEmpty(context, x, y, maxPixelWidth, alignment))
+ return;
+
+ maxPixelWidth = ROUND(static_cast<double>(maxPixelWidth / context.GetGUIScaleX()));
+ std::vector<UTILS::COLOR::Color> renderColors;
+ renderColors.reserve(colors.size());
+ for (const auto& color : colors)
+ renderColors.emplace_back(context.MergeColor(color ? color : m_textColor));
+ if (!shadowColor)
+ shadowColor = m_shadowColor;
+ if (shadowColor)
+ {
+ shadowColor = context.MergeColor(shadowColor);
+ std::vector<UTILS::COLOR::Color> shadowColors;
+ shadowColors.reserve(renderColors.size());
+ for (const auto& renderColor : renderColors)
+ shadowColors.emplace_back((renderColor & 0xff000000) != 0 ? shadowColor : 0);
+ m_font->DrawTextInternal(context, x + 1, y + 1, shadowColors, text, alignment, maxPixelWidth,
+ false);
+ }
+ m_font->DrawTextInternal(context, x, y, renderColors, text, alignment, maxPixelWidth, false);
+
+ if (clip)
+ context.RestoreClipRegion();
+}
+
+bool CGUIFont::UpdateScrollInfo(const vecText& text, CScrollInfo& scrollInfo)
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!winSystem)
+ return false;
+
+ // draw at our scroll position
+ // we handle the scrolling as follows:
+ // We scroll on a per-pixel basis (eschewing the use of character indices
+ // which were also in use previously). The complete string, including suffix,
+ // is plotted to achieve the desired effect - normally just the one time, but
+ // if there is a wrap point within the viewport then it will be plotted twice.
+ // If the string is smaller than the viewport, then it may be plotted even
+ // more times than that.
+ //
+ if (scrollInfo.m_waitTime)
+ {
+ scrollInfo.m_waitTime--;
+ return true;
+ }
+
+ if (text.empty())
+ return false;
+
+ CScrollInfo old(scrollInfo);
+
+ // move along by the appropriate scroll amount
+ float scrollAmount =
+ fabs(scrollInfo.GetPixelsPerFrame() * winSystem->GetGfxContext().GetGUIScaleX());
+
+ if (!scrollInfo.m_widthValid)
+ {
+ /* Calculate the pixel width of the complete string */
+ scrollInfo.m_textWidth = GetTextWidth(text);
+ scrollInfo.m_totalWidth = scrollInfo.m_textWidth + GetTextWidth(scrollInfo.m_suffix);
+ scrollInfo.m_widthValid = true;
+ }
+ scrollInfo.m_pixelPos += scrollAmount;
+ assert(scrollInfo.m_totalWidth != 0);
+ while (scrollInfo.m_pixelPos >= scrollInfo.m_totalWidth)
+ scrollInfo.m_pixelPos -= scrollInfo.m_totalWidth;
+
+ if (scrollInfo.m_pixelPos < old.m_pixelPos)
+ ++scrollInfo.m_loopCount;
+
+ if (scrollInfo.m_pixelPos != old.m_pixelPos)
+ return true;
+
+ return false;
+}
+
+void CGUIFont::DrawScrollingText(float x,
+ float y,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ UTILS::COLOR::Color shadowColor,
+ const vecText& text,
+ uint32_t alignment,
+ float maxWidth,
+ const CScrollInfo& scrollInfo)
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!m_font || !winSystem)
+ return;
+
+ CGraphicContext& context = winSystem->GetGfxContext();
+
+ if (!shadowColor)
+ shadowColor = m_shadowColor;
+
+ if (!text.size() || ClippedRegionIsEmpty(context, x, y, maxWidth, alignment))
+ return; // nothing to render
+
+ if (!scrollInfo.m_widthValid)
+ {
+ /* Calculate the pixel width of the complete string */
+ scrollInfo.m_textWidth = GetTextWidth(text);
+ scrollInfo.m_totalWidth = scrollInfo.m_textWidth + GetTextWidth(scrollInfo.m_suffix);
+ scrollInfo.m_widthValid = true;
+ }
+
+ assert(scrollInfo.m_totalWidth != 0);
+
+ float textPixelWidth =
+ ROUND(static_cast<double>(scrollInfo.m_textWidth / context.GetGUIScaleX()));
+ float suffixPixelWidth = ROUND(static_cast<double>(
+ (scrollInfo.m_totalWidth - scrollInfo.m_textWidth) / context.GetGUIScaleX()));
+
+ float offset;
+ if (scrollInfo.m_pixelSpeed >= 0)
+ offset = scrollInfo.m_pixelPos;
+ else
+ offset = scrollInfo.m_totalWidth - scrollInfo.m_pixelPos;
+
+ std::vector<UTILS::COLOR::Color> renderColors;
+ renderColors.reserve(colors.size());
+ for (const auto& color : colors)
+ renderColors.emplace_back(context.MergeColor(color ? color : m_textColor));
+
+ bool scroll = !scrollInfo.m_waitTime && scrollInfo.m_pixelSpeed;
+ if (shadowColor)
+ {
+ shadowColor = context.MergeColor(shadowColor);
+ std::vector<UTILS::COLOR::Color> shadowColors;
+ shadowColors.reserve(renderColors.size());
+ for (const auto& renderColor : renderColors)
+ shadowColors.emplace_back((renderColor & 0xff000000) != 0 ? shadowColor : 0);
+ for (float dx = -offset; dx < maxWidth; dx += scrollInfo.m_totalWidth)
+ {
+ m_font->DrawTextInternal(context, x + dx + 1, y + 1, shadowColors, text, alignment,
+ textPixelWidth, scroll);
+ m_font->DrawTextInternal(context, x + dx + scrollInfo.m_textWidth + 1, y + 1, shadowColors,
+ scrollInfo.m_suffix, alignment, suffixPixelWidth, scroll);
+ }
+ }
+ for (float dx = -offset; dx < maxWidth; dx += scrollInfo.m_totalWidth)
+ {
+ m_font->DrawTextInternal(context, x + dx, y, renderColors, text, alignment, textPixelWidth,
+ scroll);
+ m_font->DrawTextInternal(context, x + dx + scrollInfo.m_textWidth, y, renderColors,
+ scrollInfo.m_suffix, alignment, suffixPixelWidth, scroll);
+ }
+
+ context.RestoreClipRegion();
+}
+
+bool CGUIFont::ClippedRegionIsEmpty(
+ CGraphicContext& context, float x, float y, float width, uint32_t alignment) const
+{
+ if (alignment & XBFONT_CENTER_X)
+ x -= width * 0.5f;
+ else if (alignment & XBFONT_RIGHT)
+ x -= width;
+ if (alignment & XBFONT_CENTER_Y)
+ y -= m_font->GetLineHeight(m_lineSpacing);
+
+ return !context.SetClipRegion(x, y, width, m_font->GetTextHeight(1, 2) * context.GetGUIScaleY());
+}
+
+float CGUIFont::GetTextWidth(const vecText& text)
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!m_font || !winSystem)
+ return 0;
+
+ CGraphicContext& context = winSystem->GetGfxContext();
+
+ std::unique_lock<CCriticalSection> lock(context);
+ return m_font->GetTextWidthInternal(text) * context.GetGUIScaleX();
+}
+
+float CGUIFont::GetCharWidth(character_t ch)
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!m_font || !winSystem)
+ return 0;
+
+ CGraphicContext& context = winSystem->GetGfxContext();
+
+ std::unique_lock<CCriticalSection> lock(context);
+ return m_font->GetCharWidthInternal(ch) * context.GetGUIScaleX();
+}
+
+float CGUIFont::GetTextHeight(int numLines) const
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!m_font || !winSystem)
+ return 0;
+
+ return m_font->GetTextHeight(m_lineSpacing, numLines) * winSystem->GetGfxContext().GetGUIScaleY();
+}
+
+float CGUIFont::GetTextBaseLine() const
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!m_font || !winSystem)
+ return 0;
+
+ return m_font->GetTextBaseLine() * winSystem->GetGfxContext().GetGUIScaleY();
+}
+
+float CGUIFont::GetLineHeight() const
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!m_font || !winSystem)
+ return 0;
+
+ return m_font->GetLineHeight(m_lineSpacing) * winSystem->GetGfxContext().GetGUIScaleY();
+}
+
+float CGUIFont::GetScaleFactor() const
+{
+ if (!m_font)
+ return 1.0f;
+
+ return m_font->GetFontHeight() / m_origHeight;
+}
+
+void CGUIFont::Begin()
+{
+ if (!m_font)
+ return;
+
+ m_font->Begin();
+}
+
+void CGUIFont::End()
+{
+ if (!m_font)
+ return;
+
+ m_font->End();
+}
+
+void CGUIFont::SetFont(CGUIFontTTF* font)
+{
+ if (m_font == font)
+ return; // no need to update the font if we already have it
+
+ if (m_font)
+ m_font->RemoveReference();
+
+ m_font = font;
+ if (m_font)
+ m_font->AddReference();
+}
diff --git a/xbmc/guilib/GUIFont.h b/xbmc/guilib/GUIFont.h
new file mode 100644
index 0000000..672a395
--- /dev/null
+++ b/xbmc/guilib/GUIFont.h
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2003-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
+
+/*!
+\file GUIFont.h
+\brief
+*/
+
+#include "utils/ColorUtils.h"
+
+#include <assert.h>
+#include <math.h>
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+typedef uint32_t character_t;
+typedef std::vector<character_t> vecText;
+
+class CGUIFontTTF;
+class CGraphicContext;
+
+///
+/// \defgroup kodi_gui_font_alignment Font alignment flags
+/// \ingroup python_xbmcgui_control_radiobutton
+/// @{
+/// @brief Flags for alignment
+///
+/// Flags are used as bits to have several together, e.g. `XBFONT_LEFT | XBFONT_CENTER_Y`
+///
+constexpr int XBFONT_LEFT = 0; ///< Align X left
+constexpr int XBFONT_RIGHT = (1 << 0); ///< Align X right
+constexpr int XBFONT_CENTER_X = (1 << 1); ///< Align X center
+constexpr int XBFONT_CENTER_Y = (1 << 2); ///< Align Y center
+constexpr int XBFONT_TRUNCATED = (1 << 3); ///< Truncated text
+constexpr int XBFONT_JUSTIFIED = (1 << 4); ///< Justify text
+/// @}
+
+// flags for font style. lower 16 bits are the unicode code
+// points, 16-24 are color bits and 24-32 are style bits
+constexpr int FONT_STYLE_NORMAL = 0;
+constexpr int FONT_STYLE_BOLD = (1 << 0);
+constexpr int FONT_STYLE_ITALICS = (1 << 1);
+constexpr int FONT_STYLE_LIGHT = (1 << 2);
+constexpr int FONT_STYLE_UPPERCASE = (1 << 3);
+constexpr int FONT_STYLE_LOWERCASE = (1 << 4);
+constexpr int FONT_STYLE_CAPITALIZE = (1 << 5);
+constexpr int FONT_STYLE_MASK = 0xFF;
+constexpr int FONT_STYLES_COUNT = 7;
+
+class CScrollInfo
+{
+public:
+ CScrollInfo(unsigned int wait = 50,
+ float pos = 0,
+ int speed = defaultSpeed,
+ const std::string& scrollSuffix = " | ");
+
+ void SetSpeed(int speed) { m_pixelSpeed = speed * 0.001f; }
+ void Reset()
+ {
+ m_waitTime = m_initialWait;
+ // pixelPos is where we start the current letter, so is measured
+ // to the left of the text rendering's left edge. Thus, a negative
+ // value will mean the text starts to the right
+ m_pixelPos = -m_initialPos;
+ // privates:
+ m_averageFrameTime = 1000.f / fabs((float)defaultSpeed);
+ m_lastFrameTime = 0;
+ m_textWidth = 0;
+ m_totalWidth = 0;
+ m_widthValid = false;
+ m_loopCount = 0;
+ }
+ float GetPixelsPerFrame();
+
+ float m_pixelPos;
+ float m_pixelSpeed;
+ unsigned int m_waitTime;
+ unsigned int m_initialWait;
+ float m_initialPos;
+ vecText m_suffix;
+ mutable float m_textWidth;
+ mutable float m_totalWidth;
+ mutable bool m_widthValid;
+
+ unsigned int m_loopCount;
+
+ static const int defaultSpeed = 60;
+
+private:
+ float m_averageFrameTime;
+ uint32_t m_lastFrameTime;
+};
+
+/*!
+ \ingroup textures
+ \brief
+ */
+class CGUIFont
+{
+public:
+ CGUIFont(const std::string& strFontName,
+ uint32_t style,
+ UTILS::COLOR::Color textColor,
+ UTILS::COLOR::Color shadowColor,
+ float lineSpacing,
+ float origHeight,
+ CGUIFontTTF* font);
+ virtual ~CGUIFont();
+
+ std::string& GetFontName();
+
+ void DrawText(float x,
+ float y,
+ UTILS::COLOR::Color color,
+ UTILS::COLOR::Color shadowColor,
+ const vecText& text,
+ uint32_t alignment,
+ float maxPixelWidth)
+ {
+ std::vector<UTILS::COLOR::Color> colors;
+ colors.push_back(color);
+ DrawText(x, y, colors, shadowColor, text, alignment, maxPixelWidth);
+ };
+
+ void DrawText(float x,
+ float y,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ UTILS::COLOR::Color shadowColor,
+ const vecText& text,
+ uint32_t alignment,
+ float maxPixelWidth);
+
+ void DrawScrollingText(float x,
+ float y,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ UTILS::COLOR::Color shadowColor,
+ const vecText& text,
+ uint32_t alignment,
+ float maxPixelWidth,
+ const CScrollInfo& scrollInfo);
+
+ bool UpdateScrollInfo(const vecText& text, CScrollInfo& scrollInfo);
+
+ float GetTextWidth(const vecText& text);
+ float GetCharWidth(character_t ch);
+ float GetTextHeight(int numLines) const;
+ float GetTextBaseLine() const;
+ float GetLineHeight() const;
+
+ //! get font scale factor (rendered height / original height)
+ float GetScaleFactor() const;
+
+ void Begin();
+ void End();
+
+ uint32_t GetStyle() const { return m_style; }
+
+ CGUIFontTTF* GetFont() const { return m_font; }
+
+ void SetFont(CGUIFontTTF* font);
+
+protected:
+ std::string m_strFontName;
+ uint32_t m_style;
+ UTILS::COLOR::Color m_shadowColor;
+ UTILS::COLOR::Color m_textColor;
+ float m_lineSpacing;
+ float m_origHeight;
+ CGUIFontTTF* m_font; // the font object has the size information
+
+private:
+ bool ClippedRegionIsEmpty(
+ CGraphicContext& context, float x, float y, float width, uint32_t alignment) const;
+};
diff --git a/xbmc/guilib/GUIFontCache.cpp b/xbmc/guilib/GUIFontCache.cpp
new file mode 100644
index 0000000..70f17ac
--- /dev/null
+++ b/xbmc/guilib/GUIFontCache.cpp
@@ -0,0 +1,260 @@
+/*
+ * 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 "GUIFontTTF.h"
+#include "windowing/GraphicContext.h"
+
+#include <stdint.h>
+#include <vector>
+
+using namespace std::chrono_literals;
+
+namespace
+{
+constexpr auto FONT_CACHE_TIME_LIMIT = 1000ms;
+}
+
+template<class Position, class Value>
+class CGUIFontCacheImpl
+{
+ struct EntryList
+ {
+ using HashMap = std::multimap<size_t, std::unique_ptr<CGUIFontCacheEntry<Position, Value>>>;
+ using HashIter = typename HashMap::iterator;
+ using AgeMap = std::multimap<std::chrono::steady_clock::time_point, HashIter>;
+
+ ~EntryList() { Flush(); }
+
+ HashIter Insert(size_t hash, std::unique_ptr<CGUIFontCacheEntry<Position, Value>> v)
+ {
+ auto r(hashMap.insert(typename HashMap::value_type(hash, std::move(v))));
+ if (r->second)
+ ageMap.insert(typename AgeMap::value_type(r->second->m_lastUsed, r));
+
+ return r;
+ }
+ void Flush()
+ {
+ ageMap.clear();
+ hashMap.clear();
+ }
+ typename HashMap::iterator FindKey(CGUIFontCacheKey<Position> key)
+ {
+ CGUIFontCacheHash<Position> hashGen;
+ CGUIFontCacheKeysMatch<Position> keyMatch;
+ auto range = hashMap.equal_range(hashGen(key));
+ for (auto ret = range.first; ret != range.second; ++ret)
+ {
+ if (keyMatch(ret->second->m_key, key))
+ {
+ return ret;
+ }
+ }
+
+ return hashMap.end();
+ }
+ void UpdateAge(HashIter it, std::chrono::steady_clock::time_point now)
+ {
+ auto range = ageMap.equal_range(it->second->m_lastUsed);
+ for (auto ageit = range.first; ageit != range.second; ++ageit)
+ {
+ if (ageit->second == it)
+ {
+ ageMap.erase(ageit);
+ ageMap.insert(typename AgeMap::value_type(now, it));
+ it->second->m_lastUsed = now;
+ return;
+ }
+ }
+ }
+
+ HashMap hashMap;
+ AgeMap ageMap;
+ };
+
+ EntryList m_list;
+ CGUIFontCache<Position, Value>* m_parent;
+
+public:
+ explicit CGUIFontCacheImpl(CGUIFontCache<Position, Value>* parent) : m_parent(parent) {}
+ Value& Lookup(const CGraphicContext& context,
+ Position& pos,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ const vecText& text,
+ uint32_t alignment,
+ float maxPixelWidth,
+ bool scrolling,
+ std::chrono::steady_clock::time_point now,
+ bool& dirtyCache);
+ void Flush();
+};
+
+template<class Position, class Value>
+CGUIFontCacheEntry<Position, Value>::~CGUIFontCacheEntry()
+{
+ delete &m_key.m_colors;
+ delete &m_key.m_text;
+ m_value.clear();
+}
+
+template<class Position, class Value>
+void CGUIFontCacheEntry<Position, Value>::Assign(const CGUIFontCacheKey<Position>& key,
+ std::chrono::steady_clock::time_point now)
+{
+ m_key.m_pos = key.m_pos;
+ m_key.m_colors.assign(key.m_colors.begin(), key.m_colors.end());
+ m_key.m_text.assign(key.m_text.begin(), key.m_text.end());
+ m_key.m_alignment = key.m_alignment;
+ m_key.m_maxPixelWidth = key.m_maxPixelWidth;
+ m_key.m_scrolling = key.m_scrolling;
+ m_matrix = key.m_matrix;
+ m_key.m_scaleX = key.m_scaleX;
+ m_key.m_scaleY = key.m_scaleY;
+ m_lastUsed = now;
+ m_value.clear();
+}
+
+template<class Position, class Value>
+CGUIFontCache<Position, Value>::CGUIFontCache(CGUIFontTTF& font)
+ : m_impl(std::make_unique<CGUIFontCacheImpl<Position, Value>>(this)), m_font(font)
+{
+}
+
+template<class Position, class Value>
+CGUIFontCache<Position, Value>::~CGUIFontCache() = default;
+
+template<class Position, class Value>
+Value& CGUIFontCache<Position, Value>::Lookup(const CGraphicContext& context,
+ Position& pos,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ const vecText& text,
+ uint32_t alignment,
+ float maxPixelWidth,
+ bool scrolling,
+ std::chrono::steady_clock::time_point now,
+ bool& dirtyCache)
+{
+ if (!m_impl)
+ m_impl = std::make_unique<CGUIFontCacheImpl<Position, Value>>(this);
+
+ return m_impl->Lookup(context, pos, colors, text, alignment, maxPixelWidth, scrolling, now,
+ dirtyCache);
+}
+
+template<class Position, class Value>
+Value& CGUIFontCacheImpl<Position, Value>::Lookup(const CGraphicContext& context,
+ Position& pos,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ const vecText& text,
+ uint32_t alignment,
+ float maxPixelWidth,
+ bool scrolling,
+ std::chrono::steady_clock::time_point now,
+ bool& dirtyCache)
+{
+ const CGUIFontCacheKey<Position> key(pos, const_cast<std::vector<UTILS::COLOR::Color>&>(colors),
+ const_cast<vecText&>(text), alignment, maxPixelWidth,
+ scrolling, context.GetGUIMatrix(), context.GetGUIScaleX(),
+ context.GetGUIScaleY());
+
+ auto i = m_list.FindKey(key);
+ if (i == m_list.hashMap.end())
+ {
+ // Cache miss
+ dirtyCache = true;
+ std::unique_ptr<CGUIFontCacheEntry<Position, Value>> entry;
+
+ if (!m_list.ageMap.empty())
+ {
+ const auto duration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - m_list.ageMap.begin()->first);
+ if (duration > FONT_CACHE_TIME_LIMIT)
+ {
+ entry = std::move(m_list.ageMap.begin()->second->second);
+ m_list.hashMap.erase(m_list.ageMap.begin()->second);
+ m_list.ageMap.erase(m_list.ageMap.begin());
+ }
+ }
+
+ // add new entry
+ CGUIFontCacheHash<Position> hashgen;
+ if (!entry)
+ entry = std::make_unique<CGUIFontCacheEntry<Position, Value>>(*m_parent, key, now);
+ else
+ entry->Assign(key, now);
+ return m_list.Insert(hashgen(key), std::move(entry))->second->m_value;
+ }
+ else
+ {
+ // Cache hit
+ // Update the translation arguments so that they hold the offset to apply
+ // to the cached values (but only in the dynamic case)
+ pos.UpdateWithOffsets(i->second->m_key.m_pos, scrolling);
+
+ // Update time in entry and move to the back of the list
+ m_list.UpdateAge(i, now);
+
+ dirtyCache = false;
+
+ return i->second->m_value;
+ }
+}
+
+template<class Position, class Value>
+void CGUIFontCache<Position, Value>::Flush()
+{
+ m_impl->Flush();
+}
+
+template<class Position, class Value>
+void CGUIFontCacheImpl<Position, Value>::Flush()
+{
+ m_list.Flush();
+}
+
+template CGUIFontCache<CGUIFontCacheStaticPosition, CGUIFontCacheStaticValue>::CGUIFontCache(
+ CGUIFontTTF& font);
+template CGUIFontCache<CGUIFontCacheStaticPosition, CGUIFontCacheStaticValue>::~CGUIFontCache();
+template CGUIFontCacheEntry<CGUIFontCacheStaticPosition,
+ CGUIFontCacheStaticValue>::~CGUIFontCacheEntry();
+template CGUIFontCacheStaticValue& CGUIFontCache<
+ CGUIFontCacheStaticPosition,
+ CGUIFontCacheStaticValue>::Lookup(const CGraphicContext& context,
+ CGUIFontCacheStaticPosition&,
+ const std::vector<UTILS::COLOR::Color>&,
+ const vecText&,
+ uint32_t,
+ float,
+ bool,
+ std::chrono::steady_clock::time_point,
+ bool&);
+template void CGUIFontCache<CGUIFontCacheStaticPosition, CGUIFontCacheStaticValue>::Flush();
+
+template CGUIFontCache<CGUIFontCacheDynamicPosition, CGUIFontCacheDynamicValue>::CGUIFontCache(
+ CGUIFontTTF& font);
+template CGUIFontCache<CGUIFontCacheDynamicPosition, CGUIFontCacheDynamicValue>::~CGUIFontCache();
+template CGUIFontCacheEntry<CGUIFontCacheDynamicPosition,
+ CGUIFontCacheDynamicValue>::~CGUIFontCacheEntry();
+template CGUIFontCacheDynamicValue& CGUIFontCache<
+ CGUIFontCacheDynamicPosition,
+ CGUIFontCacheDynamicValue>::Lookup(const CGraphicContext& context,
+ CGUIFontCacheDynamicPosition&,
+ const std::vector<UTILS::COLOR::Color>&,
+ const vecText&,
+ uint32_t,
+ float,
+ bool,
+ std::chrono::steady_clock::time_point,
+ bool&);
+template void CGUIFontCache<CGUIFontCacheDynamicPosition, CGUIFontCacheDynamicValue>::Flush();
+
+void CVertexBuffer::clear()
+{
+ if (m_font)
+ m_font->DestroyVertexBuffer(*this);
+}
diff --git a/xbmc/guilib/GUIFontCache.h b/xbmc/guilib/GUIFontCache.h
new file mode 100644
index 0000000..8a967fa
--- /dev/null
+++ b/xbmc/guilib/GUIFontCache.h
@@ -0,0 +1,286 @@
+/*
+ * 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
+
+/*!
+\file GUIFontCache.h
+\brief
+*/
+
+#include "utils/ColorUtils.h"
+#include "utils/TransformMatrix.h"
+
+#include <algorithm>
+#include <cassert>
+#include <chrono>
+#include <cstddef>
+#include <cstring>
+#include <memory>
+#include <stdint.h>
+#include <vector>
+
+constexpr float FONT_CACHE_DIST_LIMIT = 0.01f;
+
+class CGraphicContext;
+
+template<class Position, class Value>
+class CGUIFontCache;
+class CGUIFontTTF;
+
+template<class Position, class Value>
+class CGUIFontCacheImpl;
+
+template<class Position>
+struct CGUIFontCacheKey
+{
+ Position m_pos;
+ std::vector<UTILS::COLOR::Color>& m_colors;
+ vecText& m_text;
+ uint32_t m_alignment;
+ float m_maxPixelWidth;
+ bool m_scrolling;
+ const TransformMatrix& m_matrix;
+ float m_scaleX;
+ float m_scaleY;
+
+ CGUIFontCacheKey(Position pos,
+ std::vector<UTILS::COLOR::Color>& colors,
+ vecText& text,
+ uint32_t alignment,
+ float maxPixelWidth,
+ bool scrolling,
+ const TransformMatrix& matrix,
+ float scaleX,
+ float scaleY)
+ : m_pos(pos),
+ m_colors(colors),
+ m_text(text),
+ m_alignment(alignment),
+ m_maxPixelWidth(maxPixelWidth),
+ m_scrolling(scrolling),
+ m_matrix(matrix),
+ m_scaleX(scaleX),
+ m_scaleY(scaleY)
+ {
+ }
+};
+
+template<class Position, class Value>
+struct CGUIFontCacheEntry
+{
+ const CGUIFontCache<Position, Value>& m_cache;
+ CGUIFontCacheKey<Position> m_key;
+ TransformMatrix m_matrix;
+ std::chrono::steady_clock::time_point m_lastUsed;
+ Value m_value;
+
+ CGUIFontCacheEntry(const CGUIFontCache<Position, Value>& cache,
+ const CGUIFontCacheKey<Position>& key,
+ std::chrono::steady_clock::time_point now)
+ : m_cache(cache),
+ m_key(key.m_pos,
+ *new std::vector<UTILS::COLOR::Color>,
+ *new vecText,
+ key.m_alignment,
+ key.m_maxPixelWidth,
+ key.m_scrolling,
+ m_matrix,
+ key.m_scaleX,
+ key.m_scaleY),
+ m_lastUsed(now)
+ {
+ m_key.m_colors.assign(key.m_colors.begin(), key.m_colors.end());
+ m_key.m_text.assign(key.m_text.begin(), key.m_text.end());
+ m_matrix = key.m_matrix;
+ }
+
+ ~CGUIFontCacheEntry();
+
+ void Assign(const CGUIFontCacheKey<Position>& key, std::chrono::steady_clock::time_point now);
+};
+
+template<class Position>
+struct CGUIFontCacheHash
+{
+ size_t operator()(const CGUIFontCacheKey<Position>& key) const
+ {
+ /* Not much effort has gone into choosing this hash function */
+ size_t hash = 0, i;
+ for (i = 0; i < 3 && i < key.m_text.size(); ++i)
+ hash += key.m_text[i];
+ if (key.m_colors.size())
+ hash += key.m_colors[0];
+ hash += static_cast<size_t>(MatrixHashContribution(key)); // horrible
+ return hash;
+ }
+};
+
+template<class Position>
+struct CGUIFontCacheKeysMatch
+{
+ bool operator()(const CGUIFontCacheKey<Position>& a, const CGUIFontCacheKey<Position>& b) const
+ {
+ // clang-format off
+ return a.m_text == b.m_text &&
+ a.m_colors == b.m_colors &&
+ a.m_alignment == b.m_alignment &&
+ a.m_scrolling == b.m_scrolling &&
+ a.m_maxPixelWidth == b.m_maxPixelWidth &&
+ Match(a.m_pos, a.m_matrix, b.m_pos, b.m_matrix, a.m_scrolling) &&
+ a.m_scaleX == b.m_scaleX &&
+ a.m_scaleY == b.m_scaleY;
+ // clang-format on
+ }
+};
+
+
+template<class Position, class Value>
+class CGUIFontCache
+{
+ std::unique_ptr<CGUIFontCacheImpl<Position, Value>> m_impl;
+
+ CGUIFontCache(const CGUIFontCache<Position, Value>&) = delete;
+ const CGUIFontCache<Position, Value>& operator=(const CGUIFontCache<Position, Value>&) = delete;
+
+public:
+ const CGUIFontTTF& m_font;
+
+ explicit CGUIFontCache(CGUIFontTTF& font);
+
+ ~CGUIFontCache();
+
+ Value& Lookup(const CGraphicContext& context,
+ Position& pos,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ const vecText& text,
+ uint32_t alignment,
+ float maxPixelWidth,
+ bool scrolling,
+ std::chrono::steady_clock::time_point now,
+ bool& dirtyCache);
+ void Flush();
+};
+
+struct CGUIFontCacheStaticPosition
+{
+ float m_x;
+ float m_y;
+ CGUIFontCacheStaticPosition(float x, float y) : m_x(x), m_y(y) {}
+ void UpdateWithOffsets(const CGUIFontCacheStaticPosition& cached, bool scrolling) {}
+};
+
+struct CGUIFontCacheStaticValue : public std::shared_ptr<std::vector<SVertex>>
+{
+ void clear()
+ {
+ if (*this)
+ (*this)->clear();
+ }
+};
+
+inline bool Match(const CGUIFontCacheStaticPosition& a,
+ const TransformMatrix& a_m,
+ const CGUIFontCacheStaticPosition& b,
+ const TransformMatrix& b_m,
+ bool scrolling)
+{
+ return a.m_x == b.m_x && a.m_y == b.m_y && a_m == b_m;
+}
+
+inline float MatrixHashContribution(const CGUIFontCacheKey<CGUIFontCacheStaticPosition>& a)
+{
+ /* Ensure horizontally translated versions end up in different buckets */
+ return a.m_matrix.m[0][3];
+}
+
+struct CGUIFontCacheDynamicPosition
+{
+ float m_x;
+ float m_y;
+ float m_z;
+ CGUIFontCacheDynamicPosition() = default;
+ CGUIFontCacheDynamicPosition(float x, float y, float z) : m_x(x), m_y(y), m_z(z) {}
+ void UpdateWithOffsets(const CGUIFontCacheDynamicPosition& cached, bool scrolling)
+ {
+ if (scrolling)
+ m_x = m_x - cached.m_x;
+ else
+ m_x = floorf(m_x - cached.m_x + FONT_CACHE_DIST_LIMIT);
+ m_y = floorf(m_y - cached.m_y + FONT_CACHE_DIST_LIMIT);
+ m_z = floorf(m_z - cached.m_z + FONT_CACHE_DIST_LIMIT);
+ }
+};
+
+struct CVertexBuffer
+{
+#if defined(HAS_GL) || defined(HAS_GLES)
+ typedef unsigned int BufferHandleType;
+#define BUFFER_HANDLE_INIT 0
+#elif defined(HAS_DX)
+ typedef void* BufferHandleType;
+#define BUFFER_HANDLE_INIT nullptr
+#endif
+ BufferHandleType bufferHandle = BUFFER_HANDLE_INIT; // this is really a GLuint
+ size_t size = 0;
+ CVertexBuffer() : m_font(nullptr) {}
+ CVertexBuffer(BufferHandleType bufferHandle, size_t size, const CGUIFontTTF* font)
+ : bufferHandle(bufferHandle), size(size), m_font(font)
+ {
+ }
+ CVertexBuffer(const CVertexBuffer& other)
+ : bufferHandle(other.bufferHandle), size(other.size), m_font(other.m_font)
+ {
+ /* In practice, the copy constructor is only called before a vertex buffer
+ * has been attached. If this should ever change, we'll need another support
+ * function in GUIFontTTFGL/DX to duplicate a buffer, given its handle. */
+ assert(other.bufferHandle == 0);
+ }
+ CVertexBuffer& operator=(CVertexBuffer& other)
+ {
+ /* This is used with move-assignment semantics for initialising the object in the font cache */
+ assert(bufferHandle == 0);
+ bufferHandle = other.bufferHandle;
+ other.bufferHandle = 0;
+ size = other.size;
+ m_font = other.m_font;
+ return *this;
+ }
+ void clear();
+
+private:
+ const CGUIFontTTF* m_font;
+};
+
+typedef CVertexBuffer CGUIFontCacheDynamicValue;
+
+inline bool Match(const CGUIFontCacheDynamicPosition& a,
+ const TransformMatrix& a_m,
+ const CGUIFontCacheDynamicPosition& b,
+ const TransformMatrix& b_m,
+ bool scrolling)
+{
+ float diffX = a.m_x - b.m_x + FONT_CACHE_DIST_LIMIT;
+ float diffY = a.m_y - b.m_y + FONT_CACHE_DIST_LIMIT;
+ float diffZ = a.m_z - b.m_z + FONT_CACHE_DIST_LIMIT;
+ // clang-format off
+ return (scrolling ||
+ diffX - floorf(diffX) < 2 * FONT_CACHE_DIST_LIMIT) &&
+ diffY - floorf(diffY) < 2 * FONT_CACHE_DIST_LIMIT &&
+ diffZ - floorf(diffZ) < 2 * FONT_CACHE_DIST_LIMIT &&
+ a_m.m[0][0] == b_m.m[0][0] &&
+ a_m.m[1][1] == b_m.m[1][1] &&
+ a_m.m[2][2] == b_m.m[2][2];
+ // clang-format on
+ // We already know the first 3 columns of both matrices are diagonal, so no need to check the other elements
+}
+
+inline float MatrixHashContribution(const CGUIFontCacheKey<CGUIFontCacheDynamicPosition>& a)
+{
+ return 0;
+}
diff --git a/xbmc/guilib/GUIFontManager.cpp b/xbmc/guilib/GUIFontManager.cpp
new file mode 100644
index 0000000..605b9c8
--- /dev/null
+++ b/xbmc/guilib/GUIFontManager.cpp
@@ -0,0 +1,671 @@
+/*
+ * 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 "GUIFontManager.h"
+
+#include "GUIComponent.h"
+#include "GUIFontTTF.h"
+#include "GUIWindowManager.h"
+#include "addons/AddonManager.h"
+#include "addons/FontResource.h"
+#include "addons/Skin.h"
+#include "addons/addoninfo/AddonType.h"
+#include "windowing/GraphicContext.h"
+
+#include <mutex>
+#if defined(HAS_GLES) || defined(HAS_GL)
+#include "GUIFontTTFGL.h"
+#endif
+#include "FileItem.h"
+#include "GUIControlFactory.h"
+#include "GUIFont.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "filesystem/Directory.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "utils/FileUtils.h"
+#include "utils/FontUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <set>
+
+#ifdef TARGET_POSIX
+#include "filesystem/SpecialProtocol.h"
+#endif
+
+using namespace ADDON;
+
+namespace
+{
+constexpr const char* XML_FONTCACHE_FILENAME = "fontcache.xml";
+
+bool LoadXMLData(const std::string& filepath, CXBMCTinyXML& xmlDoc)
+{
+ if (!CFileUtils::Exists(filepath))
+ {
+ CLog::LogF(LOGDEBUG, "Couldn't load '{}' the file don't exists", filepath);
+ return false;
+ }
+ if (!xmlDoc.LoadFile(filepath))
+ {
+ CLog::LogF(LOGERROR, "Couldn't load '{}'", filepath);
+ return false;
+ }
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (!pRootElement || pRootElement->ValueStr() != "fonts")
+ {
+ CLog::LogF(LOGERROR, "Couldn't load '{}' XML content doesn't start with <fonts>", filepath);
+ return false;
+ }
+ return true;
+}
+} // unnamed namespace
+
+
+GUIFontManager::GUIFontManager() = default;
+
+GUIFontManager::~GUIFontManager()
+{
+ Clear();
+}
+
+void GUIFontManager::RescaleFontSizeAndAspect(CGraphicContext& context,
+ float* size,
+ float* aspect,
+ const RESOLUTION_INFO& sourceRes,
+ bool preserveAspect)
+{
+ // get the UI scaling constants so that we can scale our font sizes correctly
+ // as fonts aren't scaled at render time (due to aliasing) we must scale
+ // the size of the fonts before they are drawn to bitmaps
+ float scaleX, scaleY;
+ context.GetGUIScaling(sourceRes, scaleX, scaleY);
+
+ if (preserveAspect)
+ {
+ // font always displayed in the aspect specified by the aspect parameter
+ *aspect /= context.GetResInfo().fPixelRatio;
+ }
+ else
+ {
+ // font stretched like the rest of the UI, aspect parameter being the original aspect
+
+ // adjust aspect ratio
+ *aspect *= sourceRes.fPixelRatio;
+
+ *aspect *= scaleY / scaleX;
+ }
+
+ *size /= scaleY;
+}
+
+static bool CheckFont(std::string& strPath, const std::string& newPath, const std::string& filename)
+{
+ if (!CFileUtils::Exists(strPath))
+ {
+ strPath = URIUtils::AddFileToFolder(newPath, filename);
+#ifdef TARGET_POSIX
+ strPath = CSpecialProtocol::TranslatePathConvertCase(strPath);
+#endif
+ return false;
+ }
+
+ return true;
+}
+
+CGUIFont* GUIFontManager::LoadTTF(const std::string& strFontName,
+ const std::string& strFilename,
+ UTILS::COLOR::Color textColor,
+ UTILS::COLOR::Color shadowColor,
+ const int iSize,
+ const int iStyle,
+ bool border,
+ float lineSpacing,
+ float aspect,
+ const RESOLUTION_INFO* sourceRes,
+ bool preserveAspect)
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!winSystem)
+ {
+ CLog::Log(LOGFATAL,
+ "GUIFontManager::{}: Something tries to call function without an available GUI "
+ "window system",
+ __func__);
+ return nullptr;
+ }
+
+ CGraphicContext& context = winSystem->GetGfxContext();
+
+ float originalAspect = aspect;
+
+ //check if font already exists
+ CGUIFont* pFont = GetFont(strFontName, false);
+ if (pFont)
+ return pFont;
+
+ if (!sourceRes) // no source res specified, so assume the skin res
+ sourceRes = &m_skinResolution;
+
+ float newSize = static_cast<float>(iSize);
+ RescaleFontSizeAndAspect(context, &newSize, &aspect, *sourceRes, preserveAspect);
+
+ // First try to load the font from the skin
+ std::string strPath;
+ if (!CURL::IsFullPath(strFilename))
+ {
+ strPath = URIUtils::AddFileToFolder(context.GetMediaDir(), "fonts", strFilename);
+ }
+ else
+ strPath = strFilename;
+
+#ifdef TARGET_POSIX
+ strPath = CSpecialProtocol::TranslatePathConvertCase(strPath);
+#endif
+
+ // Check if the file exists, otherwise try loading it from the global media dir
+ std::string file = URIUtils::GetFileName(strFilename);
+ if (!CheckFont(strPath, "special://home/media/Fonts", file) &&
+ !CheckFont(strPath, "special://xbmc/media/Fonts", file))
+ {
+ VECADDONS addons;
+ CServiceBroker::GetAddonMgr().GetAddons(addons, AddonType::RESOURCE_FONT);
+ for (auto& it : addons)
+ {
+ std::shared_ptr<CFontResource> font(std::static_pointer_cast<CFontResource>(it));
+ if (font->GetFont(file, strPath))
+ break;
+ }
+ }
+
+ // check if we already have this font file loaded (font object could differ only by color or style)
+ const std::string fontIdent =
+ StringUtils::Format("{}_{:f}_{:f}{}", strFilename, newSize, aspect, border ? "_border" : "");
+
+ CGUIFontTTF* pFontFile = GetFontFile(fontIdent);
+ if (!pFontFile)
+ {
+ pFontFile = CGUIFontTTF::CreateGUIFontTTF(fontIdent);
+ bool bFontLoaded = pFontFile->Load(strPath, newSize, aspect, 1.0f, border);
+
+ if (!bFontLoaded)
+ {
+ delete pFontFile;
+
+ // font could not be loaded - try Arial.ttf, which we distribute
+ if (strFilename != "arial.ttf")
+ {
+ CLog::Log(LOGERROR, "GUIFontManager::{}: Couldn't load font name: {}({}), trying arial.ttf",
+ __func__, strFontName, strFilename);
+ return LoadTTF(strFontName, "arial.ttf", textColor, shadowColor, iSize, iStyle, border,
+ lineSpacing, originalAspect);
+ }
+ CLog::Log(LOGERROR, "GUIFontManager::{}: Couldn't load font name:{} file:{}", __func__,
+ strFontName, strPath);
+
+ return nullptr;
+ }
+
+ m_vecFontFiles.emplace_back(pFontFile);
+ }
+
+ // font file is loaded, create our CGUIFont
+ CGUIFont* pNewFont = new CGUIFont(strFontName, iStyle, textColor, shadowColor, lineSpacing,
+ static_cast<float>(iSize), pFontFile);
+ m_vecFonts.emplace_back(pNewFont);
+
+ // Store the original TTF font info in case we need to reload it in a different resolution
+ OrigFontInfo fontInfo;
+ fontInfo.size = iSize;
+ fontInfo.aspect = originalAspect;
+ fontInfo.fontFilePath = strPath;
+ fontInfo.fileName = strFilename;
+ fontInfo.sourceRes = *sourceRes;
+ fontInfo.preserveAspect = preserveAspect;
+ fontInfo.border = border;
+ m_vecFontInfo.emplace_back(fontInfo);
+
+ return pNewFont;
+}
+
+bool GUIFontManager::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() != GUI_MSG_NOTIFY_ALL)
+ return false;
+
+ if (message.GetParam1() == GUI_MSG_RENDERER_LOST)
+ {
+ m_canReload = false;
+ return true;
+ }
+
+ if (message.GetParam1() == GUI_MSG_RENDERER_RESET)
+ { // our device has been reset - we have to reload our ttf fonts, and send
+ // a message to controls that we have done so
+ ReloadTTFFonts();
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0,
+ GUI_MSG_WINDOW_RESIZE);
+ m_canReload = true;
+ return true;
+ }
+
+ if (message.GetParam1() == GUI_MSG_WINDOW_RESIZE)
+ { // we need to reload our fonts
+ if (m_canReload)
+ {
+ ReloadTTFFonts();
+ // no need to send a resize message, as this message will do the rounds
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void GUIFontManager::ReloadTTFFonts(void)
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (m_vecFonts.empty() || !winSystem)
+ return; // we haven't even loaded fonts in yet
+
+ for (size_t i = 0; i < m_vecFonts.size(); ++i)
+ {
+ const auto& font = m_vecFonts[i];
+ OrigFontInfo fontInfo = m_vecFontInfo[i];
+
+ float aspect = fontInfo.aspect;
+ float newSize = static_cast<float>(fontInfo.size);
+ std::string& strPath = fontInfo.fontFilePath;
+ std::string& strFilename = fontInfo.fileName;
+
+ RescaleFontSizeAndAspect(winSystem->GetGfxContext(), &newSize, &aspect, fontInfo.sourceRes,
+ fontInfo.preserveAspect);
+
+ const std::string fontIdent = StringUtils::Format("{}_{:f}_{:f}{}", strFilename, newSize,
+ aspect, fontInfo.border ? "_border" : "");
+ CGUIFontTTF* pFontFile = GetFontFile(fontIdent);
+ if (!pFontFile)
+ {
+ pFontFile = CGUIFontTTF::CreateGUIFontTTF(fontIdent);
+ if (!pFontFile || !pFontFile->Load(strPath, newSize, aspect, 1.0f, fontInfo.border))
+ {
+ delete pFontFile;
+ // font could not be loaded
+ CLog::Log(LOGERROR, "GUIFontManager::{}: Couldn't re-load font file: '{}'", __func__,
+ strPath);
+ return;
+ }
+
+ m_vecFontFiles.emplace_back(pFontFile);
+ }
+
+ font->SetFont(pFontFile);
+ }
+}
+
+void GUIFontManager::Unload(const std::string& strFontName)
+{
+ for (auto iFont = m_vecFonts.begin(); iFont != m_vecFonts.end(); ++iFont)
+ {
+ if (StringUtils::EqualsNoCase((*iFont)->GetFontName(), strFontName))
+ {
+ m_vecFonts.erase(iFont);
+ return;
+ }
+ }
+}
+
+void GUIFontManager::FreeFontFile(CGUIFontTTF* pFont)
+{
+ for (auto it = m_vecFontFiles.begin(); it != m_vecFontFiles.end(); ++it)
+ {
+ if (pFont == it->get())
+ {
+ m_vecFontFiles.erase(it);
+ return;
+ }
+ }
+}
+
+CGUIFontTTF* GUIFontManager::GetFontFile(const std::string& fontIdent)
+{
+ for (const auto& it : m_vecFontFiles)
+ {
+ if (StringUtils::EqualsNoCase(it->GetFontIdent(), fontIdent))
+ return it.get();
+ }
+
+ return nullptr;
+}
+
+CGUIFont* GUIFontManager::GetFont(const std::string& strFontName, bool fallback /*= true*/)
+{
+ for (const auto& it : m_vecFonts)
+ {
+ CGUIFont* pFont = it.get();
+ if (StringUtils::EqualsNoCase(pFont->GetFontName(), strFontName))
+ return pFont;
+ }
+
+ // fall back to "font13" if we have none
+ if (fallback && !strFontName.empty() && !StringUtils::EqualsNoCase(strFontName, "font13"))
+ return GetFont("font13");
+
+ return nullptr;
+}
+
+CGUIFont* GUIFontManager::GetDefaultFont(bool border)
+{
+ // first find "font13" or "__defaultborder__"
+ size_t font13index = m_vecFonts.size();
+ CGUIFont* font13border = nullptr;
+ for (size_t i = 0; i < m_vecFonts.size(); i++)
+ {
+ CGUIFont* font = m_vecFonts[i].get();
+ if (font->GetFontName() == "font13")
+ font13index = i;
+ else if (font->GetFontName() == "__defaultborder__")
+ font13border = font;
+ }
+ // no "font13" means no default font is found - use the first font found.
+ if (font13index == m_vecFonts.size())
+ {
+ if (m_vecFonts.empty())
+ return nullptr;
+
+ font13index = 0;
+ }
+
+ if (border)
+ {
+ if (!font13border)
+ { // create it
+ const auto& font13 = m_vecFonts[font13index];
+ OrigFontInfo fontInfo = m_vecFontInfo[font13index];
+ font13border = LoadTTF("__defaultborder__", fontInfo.fileName, UTILS::COLOR::BLACK, 0,
+ fontInfo.size, font13->GetStyle(), true, 1.0f, fontInfo.aspect,
+ &fontInfo.sourceRes, fontInfo.preserveAspect);
+ }
+ return font13border;
+ }
+
+ return m_vecFonts[font13index].get();
+}
+
+void GUIFontManager::Clear()
+{
+ m_vecFonts.clear();
+ m_vecFontFiles.clear();
+ m_vecFontInfo.clear();
+
+#if defined(HAS_GLES) || defined(HAS_GL)
+ CGUIFontTTFGL::DestroyStaticVertexBuffers();
+#endif
+}
+
+void GUIFontManager::LoadFonts(const std::string& fontSet)
+{
+ // Get the file to load fonts from:
+ const std::string filePath = g_SkinInfo->GetSkinPath("Font.xml", &m_skinResolution);
+ CLog::LogF(LOGINFO, "Loading fonts from '{}'", filePath);
+
+ CXBMCTinyXML xmlDoc;
+ if (!LoadXMLData(filePath, xmlDoc))
+ return;
+
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ // Resolve includes in Font.xml
+ g_SkinInfo->ResolveIncludes(pRootElement);
+ // take note of the first font available in case we can't load the one specified
+ std::string firstFont;
+ const TiXmlElement* pChild = pRootElement->FirstChildElement("fontset");
+ while (pChild)
+ {
+ const char* idAttr = pChild->Attribute("id");
+ if (idAttr)
+ {
+ if (firstFont.empty())
+ firstFont = idAttr;
+
+ if (StringUtils::EqualsNoCase(fontSet, idAttr))
+ {
+ LoadFonts(pChild->FirstChild("font"));
+ return;
+ }
+ }
+ pChild = pChild->NextSiblingElement("fontset");
+ }
+
+ // no fontset was loaded, try the first
+ if (!firstFont.empty())
+ {
+ CLog::Log(LOGWARNING,
+ "GUIFontManager::{}: File doesn't have <fontset> with name '{}', defaulting to first "
+ "fontset",
+ __func__, fontSet);
+ LoadFonts(firstFont);
+ }
+ else
+ CLog::LogF(LOGERROR, "File '{}' doesn't have a valid <fontset>", filePath);
+}
+
+void GUIFontManager::LoadFonts(const TiXmlNode* fontNode)
+{
+ while (fontNode)
+ {
+ std::string fontName;
+ std::string fileName;
+ int iSize = 20;
+ float aspect = 1.0f;
+ float lineSpacing = 1.0f;
+ UTILS::COLOR::Color shadowColor = 0;
+ UTILS::COLOR::Color textColor = 0;
+ int iStyle = FONT_STYLE_NORMAL;
+
+ XMLUtils::GetString(fontNode, "name", fontName);
+ XMLUtils::GetInt(fontNode, "size", iSize);
+ XMLUtils::GetFloat(fontNode, "linespacing", lineSpacing);
+ XMLUtils::GetFloat(fontNode, "aspect", aspect);
+ CGUIControlFactory::GetColor(fontNode, "shadow", shadowColor);
+ CGUIControlFactory::GetColor(fontNode, "color", textColor);
+ XMLUtils::GetString(fontNode, "filename", fileName);
+ GetStyle(fontNode, iStyle);
+
+ if (!fontName.empty() && URIUtils::HasExtension(fileName, ".ttf"))
+ {
+ //! @todo Why do we tolower() this shit?
+ std::string strFontFileName = fileName;
+ StringUtils::ToLower(strFontFileName);
+ LoadTTF(fontName, strFontFileName, textColor, shadowColor, iSize, iStyle, false, lineSpacing,
+ aspect);
+ }
+ fontNode = fontNode->NextSibling("font");
+ }
+}
+
+void GUIFontManager::GetStyle(const TiXmlNode* fontNode, int& iStyle)
+{
+ std::string style;
+ iStyle = FONT_STYLE_NORMAL;
+ if (XMLUtils::GetString(fontNode, "style", style))
+ {
+ std::vector<std::string> styles = StringUtils::Tokenize(style, " ");
+ for (const std::string& i : styles)
+ {
+ if (i == "bold")
+ iStyle |= FONT_STYLE_BOLD;
+ else if (i == "italics")
+ iStyle |= FONT_STYLE_ITALICS;
+ else if (i == "bolditalics") // backward compatibility
+ iStyle |= (FONT_STYLE_BOLD | FONT_STYLE_ITALICS);
+ else if (i == "uppercase")
+ iStyle |= FONT_STYLE_UPPERCASE;
+ else if (i == "lowercase")
+ iStyle |= FONT_STYLE_LOWERCASE;
+ else if (i == "capitalize")
+ iStyle |= FONT_STYLE_CAPITALIZE;
+ else if (i == "lighten")
+ iStyle |= FONT_STYLE_LIGHT;
+ }
+ }
+}
+
+void GUIFontManager::SettingOptionsFontsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ CFileItemList itemsRoot;
+ CFileItemList items;
+
+ // Find font files
+ XFILE::CDirectory::GetDirectory(UTILS::FONT::FONTPATH::SYSTEM, itemsRoot,
+ UTILS::FONT::SUPPORTED_EXTENSIONS_MASK,
+ XFILE::DIR_FLAG_NO_FILE_DIRS | XFILE::DIR_FLAG_NO_FILE_INFO);
+ XFILE::CDirectory::GetDirectory(UTILS::FONT::FONTPATH::USER, items,
+ UTILS::FONT::SUPPORTED_EXTENSIONS_MASK,
+ XFILE::DIR_FLAG_NO_FILE_DIRS | XFILE::DIR_FLAG_NO_FILE_INFO);
+
+ for (auto itItem = itemsRoot.rbegin(); itItem != itemsRoot.rend(); ++itItem)
+ items.AddFront(*itItem, 0);
+
+ for (const auto& item : items)
+ {
+ if (item->m_bIsFolder)
+ continue;
+
+ list.emplace_back(item->GetLabel(), item->GetLabel());
+ }
+}
+
+void GUIFontManager::Initialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ LoadUserFonts();
+}
+
+void GUIFontManager::LoadUserFonts()
+{
+ if (!XFILE::CDirectory::Exists(UTILS::FONT::FONTPATH::USER))
+ return;
+
+ CLog::LogF(LOGDEBUG, "Updating user fonts cache...");
+ CXBMCTinyXML xmlDoc;
+ std::string userFontCacheFilepath =
+ URIUtils::AddFileToFolder(UTILS::FONT::FONTPATH::USER, XML_FONTCACHE_FILENAME);
+ if (LoadXMLData(userFontCacheFilepath, xmlDoc))
+ {
+ // Load in cache the fonts metadata previously stored in the XML
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (pRootElement)
+ {
+ const TiXmlNode* fontNode = pRootElement->FirstChild("font");
+ while (fontNode)
+ {
+ std::string filename;
+ std::string familyName;
+ XMLUtils::GetString(fontNode, "filename", filename);
+ XMLUtils::GetString(fontNode, "familyname", familyName);
+ m_userFontsCache.emplace_back(filename, familyName);
+ fontNode = fontNode->NextSibling("font");
+ }
+ }
+ }
+
+ bool isCacheChanged{false};
+ size_t previousCacheSize = m_userFontsCache.size();
+ CFileItemList dirItems;
+ // Get the current files list from user fonts folder
+ XFILE::CDirectory::GetDirectory(UTILS::FONT::FONTPATH::USER, dirItems,
+ UTILS::FONT::SUPPORTED_EXTENSIONS_MASK,
+ XFILE::DIR_FLAG_NO_FILE_DIRS | XFILE::DIR_FLAG_NO_FILE_INFO);
+ dirItems.SetFastLookup(true);
+
+ // Remove files that no longer exist from cache
+ auto it = m_userFontsCache.begin();
+ while (it != m_userFontsCache.end())
+ {
+ const std::string filePath = UTILS::FONT::FONTPATH::USER + (*it).m_filename;
+ if (!dirItems.Contains(filePath))
+ {
+ it = m_userFontsCache.erase(it);
+ }
+ else
+ {
+ auto item = dirItems.Get(filePath);
+ dirItems.Remove(item.get());
+ ++it;
+ }
+ }
+ isCacheChanged = previousCacheSize != m_userFontsCache.size();
+ previousCacheSize = m_userFontsCache.size();
+
+ // Add new files in cache and generate the metadata
+ //!@todo FIXME: this "for" loop should be replaced with the more performant
+ //! parallel execution std::for_each(std::execution::par, ...
+ //! to halving loading times of fonts list, maybe with C++17 with appropriate
+ //! fix to include parallel execution or future C++20
+ for (auto& item : dirItems)
+ {
+ std::string filepath = item->GetPath();
+ if (item->m_bIsFolder)
+ continue;
+
+ std::string familyName = UTILS::FONT::GetFontFamily(filepath);
+ if (!familyName.empty())
+ {
+ m_userFontsCache.emplace_back(item->GetLabel(), familyName);
+ }
+ }
+ isCacheChanged = isCacheChanged || previousCacheSize != m_userFontsCache.size();
+
+ // If the cache is changed save an updated XML cache file
+ if (isCacheChanged)
+ {
+ CXBMCTinyXML xmlDoc;
+ TiXmlDeclaration decl("1.0", "UTF-8", "yes");
+ xmlDoc.InsertEndChild(decl);
+ TiXmlElement xmlMainElement("fonts");
+ TiXmlNode* fontsNode = xmlDoc.InsertEndChild(xmlMainElement);
+ if (fontsNode)
+ {
+ for (auto& fontMetadata : m_userFontsCache)
+ {
+ TiXmlElement fontElement("font");
+ TiXmlNode* fontNode = fontsNode->InsertEndChild(fontElement);
+ XMLUtils::SetString(fontNode, "filename", fontMetadata.m_filename);
+ XMLUtils::SetString(fontNode, "familyname", fontMetadata.m_familyName);
+ }
+ if (!xmlDoc.SaveFile(userFontCacheFilepath))
+ CLog::LogF(LOGERROR, "Failed to save fonts cache file '{}'", userFontCacheFilepath);
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Failed to create XML 'fonts' node");
+ }
+ }
+ CLog::LogF(LOGDEBUG, "Updating user fonts cache... DONE");
+}
+
+std::vector<std::string> GUIFontManager::GetUserFontsFamilyNames()
+{
+ // We ensure to have unique font family names and sorted alphabetically
+ // Duplicated family names can happens for example when a font have each style
+ // on different files
+ std::set<std::string, sortstringbyname> familyNames;
+ for (auto& fontMetadata : m_userFontsCache)
+ {
+ familyNames.insert(fontMetadata.m_familyName);
+ }
+ return std::vector<std::string>(familyNames.begin(), familyNames.end());
+}
diff --git a/xbmc/guilib/GUIFontManager.h b/xbmc/guilib/GUIFontManager.h
new file mode 100644
index 0000000..5923517
--- /dev/null
+++ b/xbmc/guilib/GUIFontManager.h
@@ -0,0 +1,140 @@
+/*
+ * 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
+
+/*!
+\file GUIFontManager.h
+\brief
+*/
+
+#include "IMsgTargetCallback.h"
+#include "threads/CriticalSection.h"
+#include "threads/SingleLock.h"
+#include "utils/ColorUtils.h"
+#include "utils/GlobalsHandling.h"
+#include "windowing/GraphicContext.h"
+
+#include <utility>
+#include <vector>
+
+// Forward
+class CGUIFont;
+class CGUIFontTTF;
+class CXBMCTinyXML;
+class TiXmlNode;
+class CSetting;
+struct StringSettingOption;
+
+struct OrigFontInfo
+{
+ int size;
+ float aspect;
+ std::string fontFilePath;
+ std::string fileName;
+ RESOLUTION_INFO sourceRes;
+ bool preserveAspect;
+ bool border;
+};
+
+struct FontMetadata
+{
+ FontMetadata(const std::string& filename, const std::string& familyName)
+ : m_filename{filename}, m_familyName{familyName}
+ {
+ }
+
+ std::string m_filename;
+ std::string m_familyName;
+};
+
+/*!
+ \ingroup textures
+ \brief
+ */
+class GUIFontManager : public IMsgTargetCallback
+{
+public:
+ GUIFontManager();
+ ~GUIFontManager() override;
+
+ /*!
+ * \brief Initialize the font manager.
+ * Checks that fonts cache are up to date, otherwise update it.
+ */
+ void Initialize();
+
+ bool IsUpdating() const { return m_critSection.IsLocked(); }
+
+ bool OnMessage(CGUIMessage& message) override;
+
+ void Unload(const std::string& strFontName);
+ void LoadFonts(const std::string& fontSet);
+ CGUIFont* LoadTTF(const std::string& strFontName,
+ const std::string& strFilename,
+ UTILS::COLOR::Color textColor,
+ UTILS::COLOR::Color shadowColor,
+ const int iSize,
+ const int iStyle,
+ bool border = false,
+ float lineSpacing = 1.0f,
+ float aspect = 1.0f,
+ const RESOLUTION_INFO* res = nullptr,
+ bool preserveAspect = false);
+ CGUIFont* GetFont(const std::string& strFontName, bool fallback = true);
+
+ /*! \brief return a default font
+ \param border whether the font should be a font with an outline
+ \return the font. `nullptr` if no default font can be found.
+ */
+ CGUIFont* GetDefaultFont(bool border = false);
+
+ void Clear();
+ void FreeFontFile(CGUIFontTTF* pFont);
+
+ static void SettingOptionsFontsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+
+ /*!
+ * \brief Get the list of user fonts as family names from cache
+ * \return The list of available fonts family names
+ */
+ std::vector<std::string> GetUserFontsFamilyNames();
+
+protected:
+ void ReloadTTFFonts();
+ static void RescaleFontSizeAndAspect(CGraphicContext& context,
+ float* size,
+ float* aspect,
+ const RESOLUTION_INFO& sourceRes,
+ bool preserveAspect);
+ void LoadFonts(const TiXmlNode* fontNode);
+ CGUIFontTTF* GetFontFile(const std::string& fontIdent);
+ static void GetStyle(const TiXmlNode* fontNode, int& iStyle);
+
+ std::vector<std::unique_ptr<CGUIFont>> m_vecFonts;
+ std::vector<std::unique_ptr<CGUIFontTTF>> m_vecFontFiles;
+ std::vector<OrigFontInfo> m_vecFontInfo;
+ RESOLUTION_INFO m_skinResolution;
+ bool m_canReload{true};
+
+private:
+ void LoadUserFonts();
+
+ mutable CCriticalSection m_critSection;
+ std::vector<FontMetadata> m_userFontsCache;
+};
+
+/*!
+ \ingroup textures
+ \brief
+ */
+XBMC_GLOBAL_REF(GUIFontManager, g_fontManager);
+#define g_fontManager XBMC_GLOBAL_USE(GUIFontManager)
diff --git a/xbmc/guilib/GUIFontTTF.cpp b/xbmc/guilib/GUIFontTTF.cpp
new file mode 100644
index 0000000..6b38fb0
--- /dev/null
+++ b/xbmc/guilib/GUIFontTTF.cpp
@@ -0,0 +1,1166 @@
+/*
+ * 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 "GUIFontTTF.h"
+
+#include "GUIFontManager.h"
+#include "ServiceBroker.h"
+#include "Texture.h"
+#include "URL.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "rendering/RenderSystem.h"
+#include "threads/SystemClock.h"
+#include "utils/MathUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include <math.h>
+#include <memory>
+#include <queue>
+#include <utility>
+
+// stuff for freetype
+#include <ft2build.h>
+#include <harfbuzz/hb-ft.h>
+#if defined(HAS_GL) || defined(HAS_GLES)
+#include "utils/GLUtils.h"
+
+#include "system_gl.h"
+#endif
+
+#if defined(HAS_DX)
+#include "guilib/D3DResource.h"
+#endif
+
+#ifdef TARGET_WINDOWS_STORE
+#define generic GenericFromFreeTypeLibrary
+#endif
+
+#include FT_FREETYPE_H
+#include FT_GLYPH_H
+#include FT_OUTLINE_H
+#include FT_STROKER_H
+
+namespace
+{
+constexpr int VERTEX_PER_GLYPH = 4; // number of vertex for each glyph
+constexpr int CHARS_PER_TEXTURE_LINE = 20; // number characters to cache per texture line
+constexpr int MAX_TRANSLATED_VERTEX = 32; // max number of structs CTranslatedVertices expect to use
+constexpr int MAX_GLYPHS_PER_TEXT_LINE = 1024; // max number of glyphs per text line expect to use
+constexpr unsigned int SPACING_BETWEEN_CHARACTERS_IN_TEXTURE = 1;
+constexpr int CHAR_CHUNK = 64; // 64 chars allocated at a time (2048 bytes)
+constexpr int GLYPH_STRENGTH_BOLD = 24;
+constexpr int GLYPH_STRENGTH_LIGHT = -48;
+constexpr int TAB_SPACE_LENGTH = 4;
+} /* namespace */
+
+class CFreeTypeLibrary
+{
+public:
+ CFreeTypeLibrary() = default;
+ virtual ~CFreeTypeLibrary()
+ {
+ if (m_library)
+ FT_Done_FreeType(m_library);
+ }
+
+ FT_Face GetFont(const std::string& filename,
+ float size,
+ float aspect,
+ std::vector<uint8_t>& memoryBuf)
+ {
+ // don't have it yet - create it
+ if (!m_library)
+ FT_Init_FreeType(&m_library);
+ if (!m_library)
+ {
+ CLog::LogF(LOGERROR, "Unable to initialize freetype library");
+ return nullptr;
+ }
+
+ FT_Face face;
+
+ // ok, now load the font face
+ CURL realFile(CSpecialProtocol::TranslatePath(filename));
+ if (realFile.GetFileName().empty())
+ return nullptr;
+
+ memoryBuf.clear();
+#ifndef TARGET_WINDOWS
+ if (!realFile.GetProtocol().empty())
+#endif // ! TARGET_WINDOWS
+ {
+ // load file into memory if it is not on local drive
+ // in case of win32: always load file into memory as filename is in UTF-8,
+ // but freetype expect filename in ANSI encoding
+ XFILE::CFile f;
+ if (f.LoadFile(realFile, memoryBuf) <= 0)
+ return nullptr;
+
+ if (FT_New_Memory_Face(m_library, reinterpret_cast<const FT_Byte*>(memoryBuf.data()),
+ memoryBuf.size(), 0, &face) != 0)
+ return nullptr;
+ }
+#ifndef TARGET_WINDOWS
+ else if (FT_New_Face(m_library, realFile.GetFileName().c_str(), 0, &face))
+ return nullptr;
+#endif // ! TARGET_WINDOWS
+
+ unsigned int ydpi = 72; // 72 points to the inch is the freetype default
+ unsigned int xdpi =
+ static_cast<unsigned int>(MathUtils::round_int(static_cast<double>(ydpi * aspect)));
+
+ // we set our screen res currently to 96dpi in both directions (windows default)
+ // we cache our characters (for rendering speed) so it's probably
+ // not a good idea to allow free scaling of fonts - rather, just
+ // scaling to pixel ratio on screen perhaps?
+ if (FT_Set_Char_Size(face, 0, static_cast<int>(size * 64 + 0.5f), xdpi, ydpi))
+ {
+ FT_Done_Face(face);
+ return nullptr;
+ }
+
+ return face;
+ };
+
+ FT_Stroker GetStroker()
+ {
+ if (!m_library)
+ return nullptr;
+
+ FT_Stroker stroker;
+ if (FT_Stroker_New(m_library, &stroker))
+ return nullptr;
+
+ return stroker;
+ };
+
+ static void ReleaseFont(FT_Face face)
+ {
+ assert(face);
+ FT_Done_Face(face);
+ };
+
+ static void ReleaseStroker(FT_Stroker stroker)
+ {
+ assert(stroker);
+ FT_Stroker_Done(stroker);
+ }
+
+private:
+ FT_Library m_library{nullptr};
+};
+
+XBMC_GLOBAL_REF(CFreeTypeLibrary, g_freeTypeLibrary); // our freetype library
+#define g_freeTypeLibrary XBMC_GLOBAL_USE(CFreeTypeLibrary)
+
+CGUIFontTTF::CGUIFontTTF(const std::string& fontIdent)
+ : m_fontIdent(fontIdent),
+ m_staticCache(*this),
+ m_dynamicCache(*this),
+ m_renderSystem(CServiceBroker::GetRenderSystem())
+{
+}
+
+CGUIFontTTF::~CGUIFontTTF(void)
+{
+ Clear();
+}
+
+void CGUIFontTTF::AddReference()
+{
+ m_referenceCount++;
+}
+
+void CGUIFontTTF::RemoveReference()
+{
+ // delete this object when it's reference count hits zero
+ m_referenceCount--;
+ if (!m_referenceCount)
+ g_fontManager.FreeFontFile(this);
+}
+
+
+void CGUIFontTTF::ClearCharacterCache()
+{
+ m_texture.reset();
+
+ DeleteHardwareTexture();
+
+ m_texture = nullptr;
+ m_char.clear();
+ m_char.reserve(CHAR_CHUNK);
+ memset(m_charquick, 0, sizeof(m_charquick));
+ // set the posX and posY so that our texture will be created on first character write.
+ m_posX = m_textureWidth;
+ m_posY = -static_cast<int>(GetTextureLineHeight());
+ m_textureHeight = 0;
+}
+
+void CGUIFontTTF::Clear()
+{
+ m_texture.reset();
+ m_texture = nullptr;
+ memset(m_charquick, 0, sizeof(m_charquick));
+ m_posX = 0;
+ m_posY = 0;
+ m_nestedBeginCount = 0;
+
+ if (m_hbFont)
+ hb_font_destroy(m_hbFont);
+ m_hbFont = nullptr;
+ if (m_face)
+ g_freeTypeLibrary.ReleaseFont(m_face);
+ m_face = nullptr;
+ if (m_stroker)
+ g_freeTypeLibrary.ReleaseStroker(m_stroker);
+ m_stroker = nullptr;
+
+ m_vertexTrans.clear();
+ m_vertex.clear();
+
+ m_fontFileInMemory.clear();
+}
+
+bool CGUIFontTTF::Load(
+ const std::string& strFilename, float height, float aspect, float lineSpacing, bool border)
+{
+ // we now know that this object is unique - only the GUIFont objects are non-unique, so no need
+ // for reference tracking these fonts
+ m_face = g_freeTypeLibrary.GetFont(strFilename, height, aspect, m_fontFileInMemory);
+ if (!m_face)
+ return false;
+
+ m_hbFont = hb_ft_font_create(m_face, 0);
+ if (!m_hbFont)
+ return false;
+ /*
+ the values used are described below
+
+ XBMC coords Freetype coords
+
+ 0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ bbox.yMax, ascender
+ A \
+ A A |
+ A A |
+ AAAAA pppp cellAscender
+ A A p p |
+ A A p p |
+ m_cellBaseLine _ _A_ _A_ pppp_ _ _ _ _/_ _ _ _ _ 0, base line.
+ p \
+ p cellDescender
+ m_cellHeight _ _ _ _ _ p _ _ _ _ _ _/_ _ _ _ _ bbox.yMin, descender
+
+ */
+ int cellDescender = std::min<int>(m_face->bbox.yMin, m_face->descender);
+ int cellAscender = std::max<int>(m_face->bbox.yMax, m_face->ascender);
+
+ if (border)
+ {
+ /*
+ add on the strength of any border - the non-bordered font needs
+ aligning with the bordered font by utilising GetTextBaseLine()
+ */
+ FT_Pos strength = FT_MulFix(m_face->units_per_EM, m_face->size->metrics.y_scale) / 12;
+ if (strength < 128)
+ strength = 128;
+
+ cellDescender -= strength;
+ cellAscender += strength;
+
+ m_stroker = g_freeTypeLibrary.GetStroker();
+ if (m_stroker)
+ FT_Stroker_Set(m_stroker, strength, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
+ }
+
+ // scale to pixel sizing, rounding so that maximal extent is obtained
+ float scaler = height / m_face->units_per_EM;
+ cellDescender =
+ MathUtils::round_int(cellDescender * static_cast<double>(scaler) - 0.5); // round down
+ cellAscender = MathUtils::round_int(cellAscender * static_cast<double>(scaler) + 0.5); // round up
+
+ m_cellBaseLine = cellAscender;
+ m_cellHeight = cellAscender - cellDescender;
+
+ m_height = height;
+
+ m_texture.reset();
+ m_texture = nullptr;
+
+ m_textureHeight = 0;
+ m_textureWidth = ((m_cellHeight * CHARS_PER_TEXTURE_LINE) & ~63) + 64;
+
+ m_textureWidth = CTexture::PadPow2(m_textureWidth);
+
+ if (m_textureWidth > m_renderSystem->GetMaxTextureSize())
+ m_textureWidth = m_renderSystem->GetMaxTextureSize();
+ m_textureScaleX = 1.0f / m_textureWidth;
+
+ // set the posX and posY so that our texture will be created on first character write.
+ m_posX = m_textureWidth;
+ m_posY = -static_cast<int>(GetTextureLineHeight());
+
+ return true;
+}
+
+void CGUIFontTTF::Begin()
+{
+ if (m_nestedBeginCount == 0 && m_texture && FirstBegin())
+ {
+ m_vertexTrans.clear();
+ m_vertex.clear();
+ }
+ // Keep track of the nested begin/end calls.
+ m_nestedBeginCount++;
+}
+
+void CGUIFontTTF::End()
+{
+ if (m_nestedBeginCount == 0)
+ return;
+
+ if (--m_nestedBeginCount > 0)
+ return;
+
+ LastEnd();
+}
+
+void CGUIFontTTF::DrawTextInternal(CGraphicContext& context,
+ float x,
+ float y,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ const vecText& text,
+ uint32_t alignment,
+ float maxPixelWidth,
+ bool scrolling)
+{
+ if (text.empty())
+ {
+ return;
+ }
+
+ Begin();
+ uint32_t rawAlignment = alignment;
+ bool dirtyCache(false);
+ const bool hardwareClipping = m_renderSystem->ScissorsCanEffectClipping();
+ CGUIFontCacheStaticPosition staticPos(x, y);
+ CGUIFontCacheDynamicPosition dynamicPos;
+ if (hardwareClipping)
+ {
+ dynamicPos =
+ CGUIFontCacheDynamicPosition(context.ScaleFinalXCoord(x, y), context.ScaleFinalYCoord(x, y),
+ context.ScaleFinalZCoord(x, y));
+ }
+ CVertexBuffer unusedVertexBuffer;
+ CVertexBuffer& vertexBuffer =
+ hardwareClipping
+ ? m_dynamicCache.Lookup(context, dynamicPos, colors, text, alignment, maxPixelWidth,
+ scrolling, std::chrono::steady_clock::now(), dirtyCache)
+ : unusedVertexBuffer;
+ std::shared_ptr<std::vector<SVertex>> tempVertices = std::make_shared<std::vector<SVertex>>();
+ std::shared_ptr<std::vector<SVertex>>& vertices =
+ hardwareClipping ? tempVertices
+ : static_cast<std::shared_ptr<std::vector<SVertex>>&>(m_staticCache.Lookup(
+ context, staticPos, colors, text, alignment, maxPixelWidth, scrolling,
+ std::chrono::steady_clock::now(), dirtyCache));
+
+ // reserves vertex vector capacity, only the ones that are going to be used
+ if (hardwareClipping)
+ {
+ if (m_vertexTrans.capacity() == 0)
+ m_vertexTrans.reserve(MAX_TRANSLATED_VERTEX);
+ }
+ else
+ {
+ if (m_vertex.capacity() == 0)
+ m_vertex.reserve(VERTEX_PER_GLYPH * MAX_GLYPHS_PER_TEXT_LINE);
+ }
+
+ if (dirtyCache)
+ {
+ const std::vector<Glyph> glyphs = GetHarfBuzzShapedGlyphs(text);
+ // save the origin, which is scaled separately
+ m_originX = x;
+ m_originY = y;
+
+ // cache the ellipses width
+ if (!m_ellipseCached)
+ {
+ m_ellipseCached = true;
+ Character* ellipse = GetCharacter(L'.', 0);
+ if (ellipse)
+ m_ellipsesWidth = ellipse->m_advance;
+ }
+
+ // Check if we will really need to truncate or justify the text
+ if (alignment & XBFONT_TRUNCATED)
+ {
+ if (maxPixelWidth <= 0.0f || GetTextWidthInternal(text, glyphs) <= maxPixelWidth)
+ alignment &= ~XBFONT_TRUNCATED;
+ }
+ else if (alignment & XBFONT_JUSTIFIED)
+ {
+ if (maxPixelWidth <= 0.0f)
+ alignment &= ~XBFONT_JUSTIFIED;
+ }
+
+ // calculate sizing information
+ float startX = 0;
+ float startY = (alignment & XBFONT_CENTER_Y) ? -0.5f * m_cellHeight : 0; // vertical centering
+
+ if (alignment & (XBFONT_RIGHT | XBFONT_CENTER_X))
+ {
+ // Get the extent of this line
+ float w = GetTextWidthInternal(text, glyphs);
+
+ if (alignment & XBFONT_TRUNCATED && w > maxPixelWidth + 0.5f) // + 0.5f due to rounding issues
+ w = maxPixelWidth;
+
+ if (alignment & XBFONT_CENTER_X)
+ w *= 0.5f;
+ // Offset this line's starting position
+ startX -= w;
+ }
+
+ float spacePerSpaceCharacter = 0; // for justification effects
+ if (alignment & XBFONT_JUSTIFIED)
+ {
+ // first compute the size of the text to render in both characters and pixels
+ unsigned int numSpaces = 0;
+ float linePixels = 0;
+ for (const auto& glyph : glyphs)
+ {
+ Character* ch = GetCharacter(text[glyph.m_glyphInfo.cluster], glyph.m_glyphInfo.codepoint);
+ if (ch)
+ {
+ if ((text[glyph.m_glyphInfo.cluster] & 0xffff) == L' ')
+ numSpaces += 1;
+ linePixels += ch->m_advance;
+ }
+ }
+ if (numSpaces > 0)
+ spacePerSpaceCharacter = (maxPixelWidth - linePixels) / numSpaces;
+ }
+
+ float cursorX = 0; // current position along the line
+ float offsetX = 0;
+ float offsetY = 0;
+
+ // Collect all the Character info in a first pass, in case any of them
+ // are not currently cached and cause the texture to be enlarged, which
+ // would invalidate the texture coordinates.
+ std::queue<Character> characters;
+ if (alignment & XBFONT_TRUNCATED)
+ GetCharacter(L'.', 0);
+ for (const auto& glyph : glyphs)
+ {
+ Character* ch = GetCharacter(text[glyph.m_glyphInfo.cluster], glyph.m_glyphInfo.codepoint);
+ if (!ch)
+ {
+ Character null = {};
+ characters.push(null);
+ continue;
+ }
+ characters.push(*ch);
+
+ if (maxPixelWidth > 0 &&
+ cursorX + ((alignment & XBFONT_TRUNCATED) ? ch->m_advance + 3 * m_ellipsesWidth : 0) >
+ maxPixelWidth)
+ break;
+ cursorX += ch->m_advance;
+ }
+
+ // Reserve vector space: 4 vertex for each glyph
+ tempVertices->reserve(VERTEX_PER_GLYPH * glyphs.size());
+ cursorX = 0;
+
+ for (const auto& glyph : glyphs)
+ {
+ // If starting text on a new line, determine justification effects
+ // Get the current letter in the CStdString
+ UTILS::COLOR::Color color = (text[glyph.m_glyphInfo.cluster] & 0xff0000) >> 16;
+ if (color >= colors.size())
+ color = 0;
+ color = colors[color];
+
+ // grab the next character
+ Character* ch = &characters.front();
+
+ if ((text[glyph.m_glyphInfo.cluster] & 0xffff) == static_cast<character_t>('\t'))
+ {
+ const float tabwidth = GetTabSpaceLength();
+ const float a = cursorX / tabwidth;
+ cursorX += tabwidth - ((a - floorf(a)) * tabwidth);
+ characters.pop();
+ continue;
+ }
+
+ if (alignment & XBFONT_TRUNCATED)
+ {
+ // Check if we will be exceeded the max allowed width
+ if (cursorX + ch->m_advance + 3 * m_ellipsesWidth > maxPixelWidth)
+ {
+ // Yup. Let's draw the ellipses, then bail
+ // Perhaps we should really bail to the next line in this case??
+ Character* period = GetCharacter(L'.', 0);
+ if (!period)
+ break;
+
+ for (int i = 0; i < 3; i++)
+ {
+ RenderCharacter(context, startX + cursorX, startY, period, color, !scrolling,
+ *tempVertices);
+ cursorX += period->m_advance;
+ }
+ break;
+ }
+ }
+ else if (maxPixelWidth > 0 && cursorX > maxPixelWidth)
+ break; // exceeded max allowed width - stop rendering
+
+ offsetX = static_cast<float>(
+ MathUtils::round_int(static_cast<double>(glyph.m_glyphPosition.x_offset) / 64));
+ offsetY = static_cast<float>(
+ MathUtils::round_int(static_cast<double>(glyph.m_glyphPosition.y_offset) / 64));
+ RenderCharacter(context, startX + cursorX + offsetX, startY - offsetY, ch, color, !scrolling,
+ *tempVertices);
+ if (alignment & XBFONT_JUSTIFIED)
+ {
+ if ((text[glyph.m_glyphInfo.cluster] & 0xffff) == L' ')
+ cursorX += ch->m_advance + spacePerSpaceCharacter;
+ else
+ cursorX += ch->m_advance;
+ }
+ else
+ cursorX += ch->m_advance;
+ characters.pop();
+ }
+ if (hardwareClipping)
+ {
+ CVertexBuffer& vertexBuffer =
+ m_dynamicCache.Lookup(context, dynamicPos, colors, text, rawAlignment, maxPixelWidth,
+ scrolling, std::chrono::steady_clock::now(), dirtyCache);
+ CVertexBuffer newVertexBuffer = CreateVertexBuffer(*tempVertices);
+ vertexBuffer = newVertexBuffer;
+ m_vertexTrans.emplace_back(.0f, .0f, .0f, &vertexBuffer, context.GetClipRegion());
+ }
+ else
+ {
+ m_staticCache.Lookup(context, staticPos, colors, text, rawAlignment, maxPixelWidth, scrolling,
+ std::chrono::steady_clock::now(), dirtyCache) =
+ *static_cast<CGUIFontCacheStaticValue*>(&tempVertices);
+ /* Append the new vertices to the set collected since the first Begin() call */
+ m_vertex.insert(m_vertex.end(), tempVertices->begin(), tempVertices->end());
+ }
+ }
+ else
+ {
+ if (hardwareClipping)
+ m_vertexTrans.emplace_back(dynamicPos.m_x, dynamicPos.m_y, dynamicPos.m_z, &vertexBuffer,
+ context.GetClipRegion());
+ else
+ /* Append the vertices from the cache to the set collected since the first Begin() call */
+ m_vertex.insert(m_vertex.end(), vertices->begin(), vertices->end());
+ }
+
+ End();
+}
+
+
+float CGUIFontTTF::GetTextWidthInternal(const vecText& text)
+{
+ const std::vector<Glyph> glyphs = GetHarfBuzzShapedGlyphs(text);
+ return GetTextWidthInternal(text, glyphs);
+}
+
+// this routine assumes a single line (i.e. it was called from GUITextLayout)
+float CGUIFontTTF::GetTextWidthInternal(const vecText& text, const std::vector<Glyph>& glyphs)
+{
+ float width = 0;
+ for (auto it = glyphs.begin(); it != glyphs.end(); it++)
+ {
+ const character_t ch = text[(*it).m_glyphInfo.cluster];
+ Character* c = GetCharacter(ch, (*it).m_glyphInfo.codepoint);
+ if (c)
+ {
+ // If last character in line, we want to add render width
+ // and not advance distance - this makes sure that italic text isn't
+ // choped on the end (as render width is larger than advance then).
+ if (std::next(it) == glyphs.end())
+ width += std::max(c->m_right - c->m_left + c->m_offsetX, c->m_advance);
+ else if ((ch & 0xffff) == static_cast<character_t>('\t'))
+ width += GetTabSpaceLength();
+ else
+ width += c->m_advance;
+ }
+ }
+
+ return width;
+}
+
+float CGUIFontTTF::GetCharWidthInternal(character_t ch)
+{
+ Character* c = GetCharacter(ch, 0);
+ if (c)
+ {
+ if ((ch & 0xffff) == static_cast<character_t>('\t'))
+ return GetTabSpaceLength();
+ else
+ return c->m_advance;
+ }
+
+ return 0;
+}
+
+float CGUIFontTTF::GetTextHeight(float lineSpacing, int numLines) const
+{
+ return static_cast<float>(numLines - 1) * GetLineHeight(lineSpacing) + m_cellHeight;
+}
+
+float CGUIFontTTF::GetLineHeight(float lineSpacing) const
+{
+ if (!m_face)
+ return 0.0f;
+
+ return lineSpacing * m_face->size->metrics.height / 64.0f;
+}
+
+unsigned int CGUIFontTTF::GetTextureLineHeight() const
+{
+ return m_cellHeight + SPACING_BETWEEN_CHARACTERS_IN_TEXTURE;
+}
+
+unsigned int CGUIFontTTF::GetMaxFontHeight() const
+{
+ return m_maxFontHeight + SPACING_BETWEEN_CHARACTERS_IN_TEXTURE;
+}
+
+std::vector<CGUIFontTTF::Glyph> CGUIFontTTF::GetHarfBuzzShapedGlyphs(const vecText& text)
+{
+ std::vector<Glyph> glyphs;
+ if (text.empty())
+ {
+ return glyphs;
+ }
+
+ std::vector<hb_script_t> scripts;
+ std::vector<RunInfo> runs;
+ hb_unicode_funcs_t* ufuncs = hb_unicode_funcs_get_default();
+ hb_script_t lastScript;
+ int lastScriptIndex = -1;
+ int lastSetIndex = -1;
+
+ for (const auto& character : text)
+ {
+ scripts.emplace_back(hb_unicode_script(ufuncs, static_cast<wchar_t>(0xffff & character)));
+ }
+
+ // HB_SCRIPT_COMMON or HB_SCRIPT_INHERITED should be replaced with previous script
+ for (size_t i = 0; i < scripts.size(); ++i)
+ {
+ if (scripts[i] == HB_SCRIPT_COMMON || scripts[i] == HB_SCRIPT_INHERITED)
+ {
+ if (lastScriptIndex != -1)
+ {
+ scripts[i] = lastScript;
+ lastSetIndex = i;
+ }
+ }
+ else
+ {
+ for (size_t j = lastSetIndex + 1; j < i; ++j)
+ scripts[j] = scripts[i];
+ lastScript = scripts[i];
+ lastScriptIndex = i;
+ lastSetIndex = i;
+ }
+ }
+
+ lastScript = scripts[0];
+ int lastRunStart = 0;
+
+ for (unsigned int i = 0; i <= static_cast<unsigned int>(scripts.size()); ++i)
+ {
+ if (i == scripts.size() || scripts[i] != lastScript)
+ {
+ RunInfo run{};
+ run.m_startOffset = lastRunStart;
+ run.m_endOffset = i;
+ run.m_script = lastScript;
+ runs.emplace_back(run);
+
+ if (i < scripts.size())
+ {
+ lastScript = scripts[i];
+ lastRunStart = i;
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ for (auto& run : runs)
+ {
+ run.m_buffer = hb_buffer_create();
+ hb_buffer_set_direction(run.m_buffer, static_cast<hb_direction_t>(HB_DIRECTION_LTR));
+ hb_buffer_set_script(run.m_buffer, run.m_script);
+
+ for (unsigned int j = run.m_startOffset; j < run.m_endOffset; j++)
+ {
+ hb_buffer_add(run.m_buffer, static_cast<wchar_t>(0xffff & text[j]), j);
+ }
+
+ hb_buffer_set_content_type(run.m_buffer, HB_BUFFER_CONTENT_TYPE_UNICODE);
+ hb_shape(m_hbFont, run.m_buffer, nullptr, 0);
+ unsigned int glyphCount;
+ run.m_glyphInfos = hb_buffer_get_glyph_infos(run.m_buffer, &glyphCount);
+ run.m_glyphPositions = hb_buffer_get_glyph_positions(run.m_buffer, &glyphCount);
+
+ for (size_t k = 0; k < glyphCount; k++)
+ {
+ glyphs.emplace_back(run.m_glyphInfos[k], run.m_glyphPositions[k]);
+ }
+
+ hb_buffer_destroy(run.m_buffer);
+ }
+
+ return glyphs;
+}
+
+CGUIFontTTF::Character* CGUIFontTTF::GetCharacter(character_t chr, FT_UInt glyphIndex)
+{
+ const wchar_t letter = static_cast<wchar_t>(chr & 0xffff);
+
+ // ignore linebreaks
+ if (letter == L'\r')
+ return nullptr;
+
+ const character_t style = (chr & 0x7000000) >> 24; // style = 0 - 6
+
+ if (!glyphIndex)
+ glyphIndex = FT_Get_Char_Index(m_face, letter);
+
+ // quick access to the most frequently used glyphs
+ if (glyphIndex < MAX_GLYPH_IDX)
+ {
+ character_t ch = (style << 12) | glyphIndex; // 2^12 = 4096
+
+ if (ch < LOOKUPTABLE_SIZE && m_charquick[ch])
+ return m_charquick[ch];
+ }
+
+ // letters are stored based on style and glyph
+ character_t ch = (style << 16) | glyphIndex;
+
+ // perform binary search on sorted array by m_glyphAndStyle and
+ // if not found obtains position to insert the new m_char to keep sorted
+ int low = 0;
+ int high = m_char.size() - 1;
+ while (low <= high)
+ {
+ int mid = (low + high) >> 1;
+ if (ch > m_char[mid].m_glyphAndStyle)
+ low = mid + 1;
+ else if (ch < m_char[mid].m_glyphAndStyle)
+ high = mid - 1;
+ else
+ return &m_char[mid];
+ }
+ // if we get to here, then low is where we should insert the new character
+
+ int startIndex = low;
+
+ // increase the size of the buffer if we need it
+ if (m_char.size() == m_char.capacity())
+ {
+ m_char.reserve(m_char.capacity() + CHAR_CHUNK);
+ startIndex = 0;
+ }
+
+ // render the character to our texture
+ // must End() as we can't render text to our texture during a Begin(), End() block
+ unsigned int nestedBeginCount = m_nestedBeginCount;
+ m_nestedBeginCount = 1;
+ if (nestedBeginCount)
+ End();
+
+ m_char.emplace(m_char.begin() + low);
+ if (!CacheCharacter(glyphIndex, style, m_char.data() + low))
+ { // unable to cache character - try clearing them all out and starting over
+ CLog::LogF(LOGDEBUG, "Unable to cache character. Clearing character cache of {} characters",
+ m_char.size());
+ ClearCharacterCache();
+ low = 0;
+ startIndex = 0;
+ m_char.emplace(m_char.begin());
+ if (!CacheCharacter(glyphIndex, style, m_char.data()))
+ {
+ CLog::LogF(LOGERROR, "Unable to cache character (out of memory?)");
+ if (nestedBeginCount)
+ Begin();
+ m_nestedBeginCount = nestedBeginCount;
+ return nullptr;
+ }
+ }
+
+ if (nestedBeginCount)
+ Begin();
+ m_nestedBeginCount = nestedBeginCount;
+
+ // update the lookup table with only the m_char addresses that have changed
+ for (size_t i = startIndex; i < m_char.size(); ++i)
+ {
+ if (m_char[i].m_glyphIndex < MAX_GLYPH_IDX)
+ {
+ // >> 16 is style (0-6), then 16 - 12 (>> 4) is equivalent to style * 4096
+ character_t ch = ((m_char[i].m_glyphAndStyle & 0xffff0000) >> 4) | m_char[i].m_glyphIndex;
+
+ if (ch < LOOKUPTABLE_SIZE)
+ m_charquick[ch] = m_char.data() + i;
+ }
+ }
+
+ return m_char.data() + low;
+}
+
+bool CGUIFontTTF::CacheCharacter(FT_UInt glyphIndex, uint32_t style, Character* ch)
+{
+ FT_Glyph glyph = nullptr;
+ if (FT_Load_Glyph(m_face, glyphIndex, FT_LOAD_TARGET_LIGHT))
+ {
+ CLog::LogF(LOGDEBUG, "Failed to load glyph {:x}", glyphIndex);
+ return false;
+ }
+
+ // make bold if applicable
+ if (style & FONT_STYLE_BOLD)
+ SetGlyphStrength(m_face->glyph, GLYPH_STRENGTH_BOLD);
+ // and italics if applicable
+ if (style & FONT_STYLE_ITALICS)
+ ObliqueGlyph(m_face->glyph);
+ // and light if applicable
+ if (style & FONT_STYLE_LIGHT)
+ SetGlyphStrength(m_face->glyph, GLYPH_STRENGTH_LIGHT);
+ // grab the glyph
+ if (FT_Get_Glyph(m_face->glyph, &glyph))
+ {
+ CLog::LogF(LOGDEBUG, "Failed to get glyph {:x}", glyphIndex);
+ return false;
+ }
+ if (m_stroker)
+ FT_Glyph_StrokeBorder(&glyph, m_stroker, 0, 1);
+ // render the glyph
+ if (FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, nullptr, 1))
+ {
+ CLog::LogF(LOGDEBUG, "Failed to render glyph {:x} to a bitmap", glyphIndex);
+ return false;
+ }
+
+ FT_BitmapGlyph bitGlyph = (FT_BitmapGlyph)glyph;
+ FT_Bitmap bitmap = bitGlyph->bitmap;
+ bool isEmptyGlyph = (bitmap.width == 0 || bitmap.rows == 0);
+
+ if (!isEmptyGlyph)
+ {
+ if (bitGlyph->left < 0)
+ m_posX += -bitGlyph->left;
+
+ // check we have enough room for the character.
+ // cast-fest is here to avoid warnings due to freeetype version differences (signedness of width).
+ if (static_cast<int>(m_posX + bitGlyph->left + bitmap.width +
+ SPACING_BETWEEN_CHARACTERS_IN_TEXTURE) > static_cast<int>(m_textureWidth))
+ { // no space - gotta drop to the next line (which means creating a new texture and copying it across)
+ m_posX = 1;
+ m_posY += GetTextureLineHeight();
+ if (bitGlyph->left < 0)
+ m_posX += -bitGlyph->left;
+
+ if (m_posY + GetTextureLineHeight() >= m_textureHeight)
+ {
+ // create the new larger texture
+ unsigned int newHeight = m_posY + GetTextureLineHeight();
+ // check for max height
+ if (newHeight > m_renderSystem->GetMaxTextureSize())
+ {
+ CLog::LogF(LOGDEBUG, "New cache texture is too large ({} > {} pixels long)", newHeight,
+ m_renderSystem->GetMaxTextureSize());
+ FT_Done_Glyph(glyph);
+ return false;
+ }
+
+ std::unique_ptr<CTexture> newTexture = ReallocTexture(newHeight);
+ if (!newTexture)
+ {
+ FT_Done_Glyph(glyph);
+ CLog::LogF(LOGDEBUG, "Failed to allocate new texture of height {}", newHeight);
+ return false;
+ }
+ m_texture = std::move(newTexture);
+ }
+ m_posY = GetMaxFontHeight();
+ }
+
+ if (!m_texture)
+ {
+ FT_Done_Glyph(glyph);
+ CLog::LogF(LOGDEBUG, "no texture to cache character to");
+ return false;
+ }
+ }
+
+ // set the character in our table
+ ch->m_glyphAndStyle = (style << 16) | glyphIndex;
+ ch->m_glyphIndex = glyphIndex;
+ ch->m_offsetX = static_cast<short>(bitGlyph->left);
+ ch->m_offsetY = static_cast<short>(m_cellBaseLine - bitGlyph->top);
+ ch->m_left = isEmptyGlyph ? 0.0f : (static_cast<float>(m_posX));
+ ch->m_top = isEmptyGlyph ? 0.0f : (static_cast<float>(m_posY));
+ ch->m_right = ch->m_left + bitmap.width;
+ ch->m_bottom = ch->m_top + bitmap.rows;
+ ch->m_advance =
+ static_cast<float>(MathUtils::round_int(static_cast<double>(m_face->glyph->advance.x) / 64));
+
+ // we need only render if we actually have some pixels
+ if (!isEmptyGlyph)
+ {
+ // ensure our rect will stay inside the texture (it *should* but we need to be certain)
+ unsigned int x1 = std::max(m_posX, 0);
+ unsigned int y1 = std::max(m_posY, 0);
+ unsigned int x2 = std::min(x1 + bitmap.width, m_textureWidth);
+ unsigned int y2 = std::min(y1 + bitmap.rows, m_textureHeight);
+ m_maxFontHeight = std::max(m_maxFontHeight, y2);
+ CopyCharToTexture(bitGlyph, x1, y1, x2, y2);
+
+ m_posX += SPACING_BETWEEN_CHARACTERS_IN_TEXTURE +
+ static_cast<unsigned short>(ch->m_right - ch->m_left);
+ }
+
+ // free the glyph
+ FT_Done_Glyph(glyph);
+
+ return true;
+}
+
+void CGUIFontTTF::RenderCharacter(CGraphicContext& context,
+ float posX,
+ float posY,
+ const Character* ch,
+ UTILS::COLOR::Color color,
+ bool roundX,
+ std::vector<SVertex>& vertices)
+{
+ // actual image width isn't same as the character width as that is
+ // just baseline width and height should include the descent
+ const float width = ch->m_right - ch->m_left;
+ const float height = ch->m_bottom - ch->m_top;
+
+ // return early if nothing to render
+ if (width == 0 || height == 0)
+ return;
+
+ // posX and posY are relative to our origin, and the textcell is offset
+ // from our (posX, posY). Plus, these are unscaled quantities compared to the underlying GUI resolution
+ CRect vertex((posX + ch->m_offsetX) * context.GetGUIScaleX(),
+ (posY + ch->m_offsetY) * context.GetGUIScaleY(),
+ (posX + ch->m_offsetX + width) * context.GetGUIScaleX(),
+ (posY + ch->m_offsetY + height) * context.GetGUIScaleY());
+ vertex += CPoint(m_originX, m_originY);
+ CRect texture(ch->m_left, ch->m_top, ch->m_right, ch->m_bottom);
+ if (!m_renderSystem->ScissorsCanEffectClipping())
+ context.ClipRect(vertex, texture);
+
+ // transform our positions - note, no scaling due to GUI calibration/resolution occurs
+ float x[VERTEX_PER_GLYPH] = {context.ScaleFinalXCoord(vertex.x1, vertex.y1),
+ context.ScaleFinalXCoord(vertex.x2, vertex.y1),
+ context.ScaleFinalXCoord(vertex.x2, vertex.y2),
+ context.ScaleFinalXCoord(vertex.x1, vertex.y2)};
+
+ if (roundX)
+ {
+ // We only round the "left" side of the character, and then use the direction of rounding to
+ // move the "right" side of the character. This ensures that a constant width is kept when rendering
+ // the same letter at the same size at different places of the screen, avoiding the problem
+ // of the "left" side rounding one way while the "right" side rounds the other way, thus getting
+ // altering the width of thin characters substantially. This only really works for positive
+ // coordinates (due to the direction of truncation for negatives) but this is the only case that
+ // really interests us anyway.
+ float rx0 = static_cast<float>(MathUtils::round_int(static_cast<double>(x[0])));
+ float rx3 = static_cast<float>(MathUtils::round_int(static_cast<double>(x[3])));
+ x[1] = static_cast<float>(MathUtils::truncate_int(static_cast<double>(x[1])));
+ x[2] = static_cast<float>(MathUtils::truncate_int(static_cast<double>(x[2])));
+ if (x[0] > 0.0f && rx0 > x[0])
+ x[1] += 1;
+ else if (x[0] < 0.0f && rx0 < x[0])
+ x[1] -= 1;
+ if (x[3] > 0.0f && rx3 > x[3])
+ x[2] += 1;
+ else if (x[3] < 0.0f && rx3 < x[3])
+ x[2] -= 1;
+ x[0] = rx0;
+ x[3] = rx3;
+ }
+
+ const float y[VERTEX_PER_GLYPH] = {
+ static_cast<float>(MathUtils::round_int(
+ static_cast<double>(context.ScaleFinalYCoord(vertex.x1, vertex.y1)))),
+ static_cast<float>(MathUtils::round_int(
+ static_cast<double>(context.ScaleFinalYCoord(vertex.x2, vertex.y1)))),
+ static_cast<float>(MathUtils::round_int(
+ static_cast<double>(context.ScaleFinalYCoord(vertex.x2, vertex.y2)))),
+ static_cast<float>(MathUtils::round_int(
+ static_cast<double>(context.ScaleFinalYCoord(vertex.x1, vertex.y2))))};
+
+ const float z[VERTEX_PER_GLYPH] = {
+ static_cast<float>(MathUtils::round_int(
+ static_cast<double>(context.ScaleFinalZCoord(vertex.x1, vertex.y1)))),
+ static_cast<float>(MathUtils::round_int(
+ static_cast<double>(context.ScaleFinalZCoord(vertex.x2, vertex.y1)))),
+ static_cast<float>(MathUtils::round_int(
+ static_cast<double>(context.ScaleFinalZCoord(vertex.x2, vertex.y2)))),
+ static_cast<float>(MathUtils::round_int(
+ static_cast<double>(context.ScaleFinalZCoord(vertex.x1, vertex.y2))))};
+
+ // tex coords converted to 0..1 range
+ const float tl = texture.x1 * m_textureScaleX;
+ const float tr = texture.x2 * m_textureScaleX;
+ const float tt = texture.y1 * m_textureScaleY;
+ const float tb = texture.y2 * m_textureScaleY;
+
+ vertices.resize(vertices.size() + VERTEX_PER_GLYPH);
+ SVertex* v = &vertices[vertices.size() - VERTEX_PER_GLYPH];
+ m_color = color;
+
+#if !defined(HAS_DX)
+ uint8_t r = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color);
+ uint8_t g = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color);
+ uint8_t b = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color);
+ uint8_t a = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color);
+#endif
+
+ for (int i = 0; i < VERTEX_PER_GLYPH; i++)
+ {
+#ifdef HAS_DX
+ CD3DHelper::XMStoreColor(&v[i].col, color);
+#else
+ v[i].r = r;
+ v[i].g = g;
+ v[i].b = b;
+ v[i].a = a;
+#endif
+ }
+
+#if defined(HAS_DX)
+ for (int i = 0; i < VERTEX_PER_GLYPH; i++)
+ {
+ v[i].x = x[i];
+ v[i].y = y[i];
+ v[i].z = z[i];
+ }
+
+ v[0].u = tl;
+ v[0].v = tt;
+
+ v[1].u = tr;
+ v[1].v = tt;
+
+ v[2].u = tr;
+ v[2].v = tb;
+
+ v[3].u = tl;
+ v[3].v = tb;
+#else
+ // GL / GLES uses triangle strips, not quads, so have to rearrange the vertex order
+ v[0].u = tl;
+ v[0].v = tt;
+ v[0].x = x[0];
+ v[0].y = y[0];
+ v[0].z = z[0];
+
+ v[1].u = tl;
+ v[1].v = tb;
+ v[1].x = x[3];
+ v[1].y = y[3];
+ v[1].z = z[3];
+
+ v[2].u = tr;
+ v[2].v = tt;
+ v[2].x = x[1];
+ v[2].y = y[1];
+ v[2].z = z[1];
+
+ v[3].u = tr;
+ v[3].v = tb;
+ v[3].x = x[2];
+ v[3].y = y[2];
+ v[3].z = z[2];
+#endif
+}
+
+// Oblique code - original taken from freetype2 (ftsynth.c)
+void CGUIFontTTF::ObliqueGlyph(FT_GlyphSlot slot)
+{
+ /* only oblique outline glyphs */
+ if (slot->format != FT_GLYPH_FORMAT_OUTLINE)
+ return;
+
+ /* we don't touch the advance width */
+
+ /* For italic, simply apply a shear transform, with an angle */
+ /* of about 12 degrees. */
+
+ FT_Matrix transform;
+ transform.xx = 0x10000L;
+ transform.yx = 0x00000L;
+
+ transform.xy = 0x06000L;
+ transform.yy = 0x10000L;
+
+ FT_Outline_Transform(&slot->outline, &transform);
+}
+
+// Embolden code - original taken from freetype2 (ftsynth.c)
+void CGUIFontTTF::SetGlyphStrength(FT_GlyphSlot slot, int glyphStrength)
+{
+ if (slot->format != FT_GLYPH_FORMAT_OUTLINE)
+ return;
+
+ /* some reasonable strength */
+ FT_Pos strength = FT_MulFix(m_face->units_per_EM, m_face->size->metrics.y_scale) / glyphStrength;
+
+ FT_BBox bbox_before, bbox_after;
+ FT_Outline_Get_CBox(&slot->outline, &bbox_before);
+ FT_Outline_Embolden(&slot->outline, strength); // ignore error
+ FT_Outline_Get_CBox(&slot->outline, &bbox_after);
+
+ FT_Pos dx = bbox_after.xMax - bbox_before.xMax;
+ FT_Pos dy = bbox_after.yMax - bbox_before.yMax;
+
+ if (slot->advance.x)
+ slot->advance.x += dx;
+
+ if (slot->advance.y)
+ slot->advance.y += dy;
+
+ slot->metrics.width += dx;
+ slot->metrics.height += dy;
+ slot->metrics.horiBearingY += dy;
+ slot->metrics.horiAdvance += dx;
+ slot->metrics.vertBearingX -= dx / 2;
+ slot->metrics.vertBearingY += dy;
+ slot->metrics.vertAdvance += dy;
+}
+
+float CGUIFontTTF::GetTabSpaceLength()
+{
+ const Character* c = GetCharacter(static_cast<character_t>('X'), 0);
+ return c ? c->m_advance * TAB_SPACE_LENGTH : 28.0f * TAB_SPACE_LENGTH;
+}
diff --git a/xbmc/guilib/GUIFontTTF.h b/xbmc/guilib/GUIFontTTF.h
new file mode 100644
index 0000000..2ed2703
--- /dev/null
+++ b/xbmc/guilib/GUIFontTTF.h
@@ -0,0 +1,279 @@
+/*
+ * 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 "GUIFont.h"
+#include "utils/ColorUtils.h"
+#include "utils/Geometry.h"
+
+#include <memory>
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+#include <ft2build.h>
+#include FT_FREETYPE_H
+#include <harfbuzz/hb.h>
+
+#ifdef HAS_DX
+#include <DirectXMath.h>
+#include <DirectXPackedVector.h>
+
+using namespace DirectX;
+using namespace DirectX::PackedVector;
+#endif
+
+class CGraphicContext;
+class CTexture;
+class CRenderSystemBase;
+
+struct FT_FaceRec_;
+struct FT_LibraryRec_;
+struct FT_GlyphSlotRec_;
+struct FT_BitmapGlyphRec_;
+struct FT_StrokerRec_;
+
+typedef struct FT_FaceRec_* FT_Face;
+typedef struct FT_LibraryRec_* FT_Library;
+typedef struct FT_GlyphSlotRec_* FT_GlyphSlot;
+typedef struct FT_BitmapGlyphRec_* FT_BitmapGlyph;
+typedef struct FT_StrokerRec_* FT_Stroker;
+
+typedef uint32_t character_t;
+typedef std::vector<character_t> vecText;
+
+/*!
+ \ingroup textures
+ \brief
+ */
+
+#ifdef HAS_DX
+struct SVertex
+{
+ float x, y, z;
+ XMFLOAT4 col;
+ float u, v;
+ float u2, v2;
+};
+#else
+struct SVertex
+{
+ float x, y, z;
+ unsigned char r, g, b, a;
+ float u, v;
+};
+#endif
+
+#include "GUIFontCache.h"
+
+
+class CGUIFontTTF
+{
+ // use lookup table for the first 4096 glyphs (almost any letter or symbol) to
+ // speed up GUI rendering and decrease CPU usage and less memory reallocations
+ static constexpr int MAX_GLYPH_IDX = 4096;
+ static constexpr size_t LOOKUPTABLE_SIZE = MAX_GLYPH_IDX * FONT_STYLES_COUNT;
+
+ friend class CGUIFont;
+
+public:
+ virtual ~CGUIFontTTF();
+
+ static CGUIFontTTF* CreateGUIFontTTF(const std::string& fontIdent);
+
+ void Clear();
+
+ bool Load(const std::string& strFilename,
+ float height = 20.0f,
+ float aspect = 1.0f,
+ float lineSpacing = 1.0f,
+ bool border = false);
+
+ void Begin();
+ void End();
+ /* The next two should only be called if we've declared we can do hardware clipping */
+ virtual CVertexBuffer CreateVertexBuffer(const std::vector<SVertex>& vertices) const
+ {
+ assert(false);
+ return CVertexBuffer();
+ }
+ virtual void DestroyVertexBuffer(CVertexBuffer& bufferHandle) const {}
+
+ const std::string& GetFontIdent() const { return m_fontIdent; }
+
+protected:
+ explicit CGUIFontTTF(const std::string& fontIdent);
+
+ struct Glyph
+ {
+ hb_glyph_info_t m_glyphInfo{};
+ hb_glyph_position_t m_glyphPosition{};
+
+ Glyph(const hb_glyph_info_t& glyphInfo, const hb_glyph_position_t& glyphPosition)
+ : m_glyphInfo(glyphInfo), m_glyphPosition(glyphPosition)
+ {
+ }
+ };
+
+ struct Character
+ {
+ short m_offsetX;
+ short m_offsetY;
+ float m_left;
+ float m_top;
+ float m_right;
+ float m_bottom;
+ float m_advance;
+ FT_UInt m_glyphIndex;
+ character_t m_glyphAndStyle;
+ };
+
+ struct RunInfo
+ {
+ unsigned int m_startOffset;
+ unsigned int m_endOffset;
+ hb_buffer_t* m_buffer;
+ hb_script_t m_script;
+ hb_glyph_info_t* m_glyphInfos;
+ hb_glyph_position_t* m_glyphPositions;
+ };
+
+ void AddReference();
+ void RemoveReference();
+
+ std::vector<Glyph> GetHarfBuzzShapedGlyphs(const vecText& text);
+
+ float GetTextWidthInternal(const vecText& text);
+ float GetTextWidthInternal(const vecText& text, const std::vector<Glyph>& glyph);
+ float GetCharWidthInternal(character_t ch);
+ float GetTextHeight(float lineSpacing, int numLines) const;
+ float GetTextBaseLine() const { return static_cast<float>(m_cellBaseLine); }
+ float GetLineHeight(float lineSpacing) const;
+ float GetFontHeight() const { return m_height; }
+
+ void DrawTextInternal(CGraphicContext& context,
+ float x,
+ float y,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ const vecText& text,
+ uint32_t alignment,
+ float maxPixelWidth,
+ bool scrolling);
+
+ float m_height{0.0f};
+
+ // Stuff for pre-rendering for speed
+ Character* GetCharacter(character_t letter, FT_UInt glyphIndex);
+ bool CacheCharacter(FT_UInt glyphIndex, uint32_t style, Character* ch);
+ void RenderCharacter(CGraphicContext& context,
+ float posX,
+ float posY,
+ const Character* ch,
+ UTILS::COLOR::Color color,
+ bool roundX,
+ std::vector<SVertex>& vertices);
+ void ClearCharacterCache();
+
+ virtual std::unique_ptr<CTexture> ReallocTexture(unsigned int& newHeight) = 0;
+ virtual bool CopyCharToTexture(FT_BitmapGlyph bitGlyph,
+ unsigned int x1,
+ unsigned int y1,
+ unsigned int x2,
+ unsigned int y2) = 0;
+ virtual void DeleteHardwareTexture() = 0;
+
+ // modifying glyphs
+ void SetGlyphStrength(FT_GlyphSlot slot, int glyphStrength);
+ static void ObliqueGlyph(FT_GlyphSlot slot);
+
+ std::unique_ptr<CTexture>
+ m_texture; // texture that holds our rendered characters (8bit alpha only)
+
+ unsigned int m_textureWidth{0}; // width of our texture
+ unsigned int m_textureHeight{0}; // height of our texture
+ int m_posX{0}; // current position in the texture
+ int m_posY{0};
+
+ /*! \brief the height of each line in the texture.
+ Accounts for spacing between lines to avoid characters overlapping.
+ */
+ unsigned int GetTextureLineHeight() const;
+ unsigned int GetMaxFontHeight() const;
+
+ UTILS::COLOR::Color m_color{UTILS::COLOR::NONE};
+
+ std::vector<Character> m_char; // our characters
+
+ // room for the first MAX_GLYPH_IDX glyphs in 7 styles
+ Character* m_charquick[LOOKUPTABLE_SIZE]{nullptr};
+
+ bool m_ellipseCached{false};
+ float m_ellipsesWidth{0.0f}; // this is used every character (width of '.')
+
+ unsigned int m_cellBaseLine{0};
+ unsigned int m_cellHeight{0};
+ unsigned int m_maxFontHeight{0};
+
+ unsigned int m_nestedBeginCount{0}; // speedups
+
+ // freetype stuff
+ FT_Face m_face{nullptr};
+ FT_Stroker m_stroker{nullptr};
+
+ hb_font_t* m_hbFont{nullptr};
+
+ float m_originX{0.0f};
+ float m_originY{0.0f};
+
+ unsigned int m_nTexture{0};
+
+ struct CTranslatedVertices
+ {
+ float m_translateX;
+ float m_translateY;
+ float m_translateZ;
+ const CVertexBuffer* m_vertexBuffer;
+ CRect m_clip;
+ CTranslatedVertices(float translateX,
+ float translateY,
+ float translateZ,
+ const CVertexBuffer* vertexBuffer,
+ const CRect& clip)
+ : m_translateX(translateX),
+ m_translateY(translateY),
+ m_translateZ(translateZ),
+ m_vertexBuffer(vertexBuffer),
+ m_clip(clip)
+ {
+ }
+ };
+ std::vector<CTranslatedVertices> m_vertexTrans;
+ std::vector<SVertex> m_vertex;
+
+ float m_textureScaleX{0.0f};
+ float m_textureScaleY{0.0f};
+
+ const std::string m_fontIdent;
+ std::vector<uint8_t>
+ m_fontFileInMemory; // used only in some cases, see CFreeTypeLibrary::GetFont()
+
+ CGUIFontCache<CGUIFontCacheStaticPosition, CGUIFontCacheStaticValue> m_staticCache;
+ CGUIFontCache<CGUIFontCacheDynamicPosition, CGUIFontCacheDynamicValue> m_dynamicCache;
+
+ CRenderSystemBase* m_renderSystem;
+
+private:
+ float GetTabSpaceLength();
+
+ virtual bool FirstBegin() = 0;
+ virtual void LastEnd() = 0;
+ CGUIFontTTF(const CGUIFontTTF&) = delete;
+ CGUIFontTTF& operator=(const CGUIFontTTF&) = delete;
+ int m_referenceCount{0};
+};
diff --git a/xbmc/guilib/GUIFontTTFDX.cpp b/xbmc/guilib/GUIFontTTFDX.cpp
new file mode 100644
index 0000000..0f4a6b0
--- /dev/null
+++ b/xbmc/guilib/GUIFontTTFDX.cpp
@@ -0,0 +1,372 @@
+/*
+ * 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 "GUIFontTTFDX.h"
+
+#include "GUIFontManager.h"
+#include "GUIShaderDX.h"
+#include "TextureDX.h"
+#include "rendering/dx/DeviceResources.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/log.h"
+
+// stuff for freetype
+#include <ft2build.h>
+
+using namespace Microsoft::WRL;
+
+#ifdef TARGET_WINDOWS_STORE
+#define generic GenericFromFreeTypeLibrary
+#endif
+
+#include FT_FREETYPE_H
+#include FT_GLYPH_H
+
+namespace
+{
+constexpr size_t ELEMENT_ARRAY_MAX_CHAR_INDEX = 2000;
+} /* namespace */
+
+CGUIFontTTF* CGUIFontTTF::CreateGUIFontTTF(const std::string& fontIdent)
+{
+ return new CGUIFontTTFDX(fontIdent);
+}
+
+CGUIFontTTFDX::CGUIFontTTFDX(const std::string& fontIdent) : CGUIFontTTF(fontIdent)
+{
+ DX::Windowing()->Register(this);
+}
+
+CGUIFontTTFDX::~CGUIFontTTFDX(void)
+{
+ DX::Windowing()->Unregister(this);
+
+ m_vertexBuffer = nullptr;
+ m_staticIndexBuffer = nullptr;
+ if (!m_buffers.empty())
+ {
+ std::for_each(m_buffers.begin(), m_buffers.end(), [](CD3DBuffer* buf) {
+ if (buf)
+ delete buf;
+ });
+ }
+ m_buffers.clear();
+ m_staticIndexBufferCreated = false;
+ m_vertexWidth = 0;
+}
+
+bool CGUIFontTTFDX::FirstBegin()
+{
+ if (!DX::DeviceResources::Get()->GetD3DContext())
+ return false;
+
+ CGUIShaderDX* pGUIShader = DX::Windowing()->GetGUIShader();
+ pGUIShader->Begin(SHADER_METHOD_RENDER_FONT);
+
+ return true;
+}
+
+void CGUIFontTTFDX::LastEnd()
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext();
+ if (!pContext || !winSystem)
+ return;
+
+ typedef CGUIFontTTF::CTranslatedVertices trans;
+ bool transIsEmpty = std::all_of(m_vertexTrans.begin(), m_vertexTrans.end(),
+ [](trans& _) { return _.m_vertexBuffer->size <= 0; });
+ // no chars to render
+ if (m_vertex.empty() && transIsEmpty)
+ return;
+
+ CreateStaticIndexBuffer();
+
+ unsigned int offset = 0;
+ unsigned int stride = sizeof(SVertex);
+
+ CGUIShaderDX* pGUIShader = DX::Windowing()->GetGUIShader();
+ // Set font texture as shader resource
+ pGUIShader->SetShaderViews(1, m_speedupTexture->GetAddressOfSRV());
+ // Enable alpha blend
+ DX::Windowing()->SetAlphaBlendEnable(true);
+ // Set our static index buffer
+ pContext->IASetIndexBuffer(m_staticIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);
+ // Set the type of primitive that should be rendered from this vertex buffer, in this case triangles.
+ pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+
+ if (!m_vertex.empty())
+ {
+ // Deal with vertices that had to use software clipping
+ if (!UpdateDynamicVertexBuffer(&m_vertex[0], m_vertex.size()))
+ return;
+
+ // Set the dynamic vertex buffer to active in the input assembler
+ pContext->IASetVertexBuffers(0, 1, m_vertexBuffer.GetAddressOf(), &stride, &offset);
+
+ // Do the actual drawing operation, split into groups of characters no
+ // larger than the pre-determined size of the element array
+ size_t size = m_vertex.size() / 4;
+ for (size_t character = 0; size > character; character += ELEMENT_ARRAY_MAX_CHAR_INDEX)
+ {
+ size_t count = size - character;
+ count = std::min<size_t>(count, ELEMENT_ARRAY_MAX_CHAR_INDEX);
+
+ // 6 indices and 4 vertices per character
+ pGUIShader->DrawIndexed(count * 6, 0, character * 4);
+ }
+ }
+
+ if (!transIsEmpty)
+ {
+ // Deal with the vertices that can be hardware clipped and therefore translated
+
+ // Store current GPU transform
+ XMMATRIX view = pGUIShader->GetView();
+ // Store current scissor
+ CGraphicContext& context = winSystem->GetGfxContext();
+ CRect scissor = context.StereoCorrection(context.GetScissors());
+
+ for (size_t i = 0; i < m_vertexTrans.size(); i++)
+ {
+ // ignore empty buffers
+ if (m_vertexTrans[i].m_vertexBuffer->size == 0)
+ continue;
+
+ // Apply the clip rectangle
+ CRect clip = DX::Windowing()->ClipRectToScissorRect(m_vertexTrans[i].m_clip);
+ // Intersect with current scissors
+ clip.Intersect(scissor);
+
+ // skip empty clip, a little improvement to not render invisible text
+ if (clip.IsEmpty())
+ continue;
+
+ DX::Windowing()->SetScissors(clip);
+
+ // Apply the translation to the model view matrix
+ XMMATRIX translation =
+ XMMatrixTranslation(m_vertexTrans[i].m_translateX, m_vertexTrans[i].m_translateY,
+ m_vertexTrans[i].m_translateZ);
+ pGUIShader->SetView(XMMatrixMultiply(translation, view));
+
+ CD3DBuffer* vbuffer =
+ reinterpret_cast<CD3DBuffer*>(m_vertexTrans[i].m_vertexBuffer->bufferHandle);
+ // Set the static vertex buffer to active in the input assembler
+ ID3D11Buffer* buffers[1] = {vbuffer->Get()};
+ pContext->IASetVertexBuffers(0, 1, buffers, &stride, &offset);
+
+ // Do the actual drawing operation, split into groups of characters no
+ // larger than the pre-determined size of the element array
+ for (size_t character = 0; m_vertexTrans[i].m_vertexBuffer->size > character;
+ character += ELEMENT_ARRAY_MAX_CHAR_INDEX)
+ {
+ size_t count = m_vertexTrans[i].m_vertexBuffer->size - character;
+ count = std::min<size_t>(count, ELEMENT_ARRAY_MAX_CHAR_INDEX);
+
+ // 6 indices and 4 vertices per character
+ pGUIShader->DrawIndexed(count * 6, 0, character * 4);
+ }
+ }
+
+ // restore scissor
+ DX::Windowing()->SetScissors(scissor);
+
+ // Restore the original transform
+ pGUIShader->SetView(view);
+ }
+
+ pGUIShader->RestoreBuffers();
+}
+
+CVertexBuffer CGUIFontTTFDX::CreateVertexBuffer(const std::vector<SVertex>& vertices) const
+{
+ CD3DBuffer* buffer = nullptr;
+ // do not create empty buffers, leave buffer as nullptr, it will be ignored on drawing stage
+ if (!vertices.empty())
+ {
+ buffer = new CD3DBuffer();
+ if (!buffer->Create(D3D11_BIND_VERTEX_BUFFER, vertices.size(), sizeof(SVertex),
+ DXGI_FORMAT_UNKNOWN, D3D11_USAGE_IMMUTABLE, &vertices[0]))
+ CLog::LogF(LOGERROR, "Failed to create vertex buffer.");
+ else
+ AddReference((CGUIFontTTFDX*)this, buffer);
+ }
+
+ return CVertexBuffer(reinterpret_cast<void*>(buffer), vertices.size() / 4, this);
+}
+
+void CGUIFontTTFDX::AddReference(CGUIFontTTFDX* font, CD3DBuffer* pBuffer)
+{
+ font->m_buffers.emplace_back(pBuffer);
+}
+
+void CGUIFontTTFDX::DestroyVertexBuffer(CVertexBuffer& buffer) const
+{
+ if (nullptr != buffer.bufferHandle)
+ {
+ CD3DBuffer* vbuffer = reinterpret_cast<CD3DBuffer*>(buffer.bufferHandle);
+ ClearReference((CGUIFontTTFDX*)this, vbuffer);
+ if (vbuffer)
+ delete vbuffer;
+ buffer.bufferHandle = 0;
+ }
+}
+
+void CGUIFontTTFDX::ClearReference(CGUIFontTTFDX* font, CD3DBuffer* pBuffer)
+{
+ std::list<CD3DBuffer*>::iterator it =
+ std::find(font->m_buffers.begin(), font->m_buffers.end(), pBuffer);
+ if (it != font->m_buffers.end())
+ font->m_buffers.erase(it);
+}
+
+std::unique_ptr<CTexture> CGUIFontTTFDX::ReallocTexture(unsigned int& newHeight)
+{
+ assert(newHeight != 0);
+ assert(m_textureWidth != 0);
+ if (m_textureHeight == 0)
+ {
+ m_texture.reset();
+ m_speedupTexture.reset();
+ }
+ m_staticCache.Flush();
+ m_dynamicCache.Flush();
+
+ std::unique_ptr<CDXTexture> pNewTexture =
+ std::make_unique<CDXTexture>(m_textureWidth, newHeight, XB_FMT_A8);
+ std::unique_ptr<CD3DTexture> newSpeedupTexture = std::make_unique<CD3DTexture>();
+ if (!newSpeedupTexture->Create(m_textureWidth, newHeight, 1, D3D11_USAGE_DEFAULT,
+ DXGI_FORMAT_R8_UNORM))
+ {
+ return nullptr;
+ }
+
+ // There might be data to copy from the previous texture
+ if (newSpeedupTexture && m_speedupTexture)
+ {
+ CD3D11_BOX rect(0, 0, 0, m_textureWidth, m_textureHeight, 1);
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetImmediateContext();
+ pContext->CopySubresourceRegion(newSpeedupTexture->Get(), 0, 0, 0, 0, m_speedupTexture->Get(),
+ 0, &rect);
+ }
+
+ m_texture.reset();
+
+ m_textureHeight = newHeight;
+ m_textureScaleY = 1.0f / m_textureHeight;
+ m_speedupTexture = std::move(newSpeedupTexture);
+
+ return pNewTexture;
+}
+
+bool CGUIFontTTFDX::CopyCharToTexture(
+ FT_BitmapGlyph bitGlyph, unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2)
+{
+ FT_Bitmap bitmap = bitGlyph->bitmap;
+
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetImmediateContext();
+ if (m_speedupTexture && m_speedupTexture->Get() && pContext && bitmap.buffer)
+ {
+ CD3D11_BOX dstBox(x1, y1, 0, x2, y2, 1);
+ pContext->UpdateSubresource(m_speedupTexture->Get(), 0, &dstBox, bitmap.buffer, bitmap.pitch,
+ 0);
+ return true;
+ }
+
+ return false;
+}
+
+void CGUIFontTTFDX::DeleteHardwareTexture()
+{
+}
+
+bool CGUIFontTTFDX::UpdateDynamicVertexBuffer(const SVertex* pSysMem, unsigned int vertex_count)
+{
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext();
+
+ if (!pDevice || !pContext)
+ return false;
+
+ unsigned width = sizeof(SVertex) * vertex_count;
+ if (width > m_vertexWidth) // create or re-create
+ {
+ CD3D11_BUFFER_DESC bufferDesc(width, D3D11_BIND_VERTEX_BUFFER, D3D11_USAGE_DYNAMIC,
+ D3D11_CPU_ACCESS_WRITE);
+ D3D11_SUBRESOURCE_DATA initData = {};
+ initData.pSysMem = pSysMem;
+
+ if (FAILED(
+ pDevice->CreateBuffer(&bufferDesc, &initData, m_vertexBuffer.ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGERROR, "Failed to create the vertex buffer.");
+ return false;
+ }
+
+ m_vertexWidth = width;
+ }
+ else
+ {
+ D3D11_MAPPED_SUBRESOURCE resource;
+ if (FAILED(pContext->Map(m_vertexBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &resource)))
+ {
+ CLog::LogF(LOGERROR, "Failed to update the vertex buffer.");
+ return false;
+ }
+
+ memcpy(resource.pData, pSysMem, width);
+ pContext->Unmap(m_vertexBuffer.Get(), 0);
+ }
+
+ return true;
+}
+
+void CGUIFontTTFDX::CreateStaticIndexBuffer(void)
+{
+ if (m_staticIndexBufferCreated)
+ return;
+
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+ if (!pDevice)
+ return;
+
+ uint16_t index[ELEMENT_ARRAY_MAX_CHAR_INDEX][6];
+ for (size_t i = 0; i < ELEMENT_ARRAY_MAX_CHAR_INDEX; i++)
+ {
+ index[i][0] = 4 * i;
+ index[i][1] = 4 * i + 1;
+ index[i][2] = 4 * i + 2;
+ index[i][3] = 4 * i + 2;
+ index[i][4] = 4 * i + 3;
+ index[i][5] = 4 * i + 0;
+ }
+
+ CD3D11_BUFFER_DESC desc(sizeof(index), D3D11_BIND_INDEX_BUFFER, D3D11_USAGE_IMMUTABLE);
+ D3D11_SUBRESOURCE_DATA initData = {};
+ initData.pSysMem = index;
+
+ if (SUCCEEDED(
+ pDevice->CreateBuffer(&desc, &initData, m_staticIndexBuffer.ReleaseAndGetAddressOf())))
+ m_staticIndexBufferCreated = true;
+}
+
+bool CGUIFontTTFDX::m_staticIndexBufferCreated = false;
+ComPtr<ID3D11Buffer> CGUIFontTTFDX::m_staticIndexBuffer = nullptr;
+
+void CGUIFontTTFDX::OnDestroyDevice(bool fatal)
+{
+ m_staticIndexBufferCreated = false;
+ m_vertexWidth = 0;
+ m_staticIndexBuffer = nullptr;
+ m_vertexBuffer = nullptr;
+}
+
+void CGUIFontTTFDX::OnCreateDevice(void)
+{
+}
diff --git a/xbmc/guilib/GUIFontTTFDX.h b/xbmc/guilib/GUIFontTTFDX.h
new file mode 100644
index 0000000..ab44ce2
--- /dev/null
+++ b/xbmc/guilib/GUIFontTTFDX.h
@@ -0,0 +1,67 @@
+/*
+ * 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
+
+/*!
+\file GUIFont.h
+\brief
+*/
+
+#include "D3DResource.h"
+#include "GUIFontTTF.h"
+
+#include <list>
+#include <memory>
+#include <vector>
+
+#include <wrl/client.h>
+
+/*!
+ \ingroup textures
+ \brief
+ */
+class CGUIFontTTFDX : public CGUIFontTTF, public ID3DResource
+{
+public:
+ explicit CGUIFontTTFDX(const std::string& fontIdent);
+ virtual ~CGUIFontTTFDX(void);
+
+ bool FirstBegin() override;
+ void LastEnd() override;
+ CVertexBuffer CreateVertexBuffer(const std::vector<SVertex>& vertices) const override;
+ void DestroyVertexBuffer(CVertexBuffer& bufferHandle) const override;
+
+ void OnDestroyDevice(bool fatal) override;
+ void OnCreateDevice() override;
+
+ static void CreateStaticIndexBuffer(void);
+ static void DestroyStaticIndexBuffer(void);
+
+protected:
+ std::unique_ptr<CTexture> ReallocTexture(unsigned int& newHeight) override;
+ bool CopyCharToTexture(FT_BitmapGlyph bitGlyph,
+ unsigned int x1,
+ unsigned int y1,
+ unsigned int x2,
+ unsigned int y2) override;
+ void DeleteHardwareTexture() override;
+
+private:
+ bool UpdateDynamicVertexBuffer(const SVertex* pSysMem, unsigned int count);
+ static void AddReference(CGUIFontTTFDX* font, CD3DBuffer* pBuffer);
+ static void ClearReference(CGUIFontTTFDX* font, CD3DBuffer* pBuffer);
+
+ unsigned m_vertexWidth{0};
+ std::unique_ptr<CD3DTexture> m_speedupTexture; // extra texture to speed up reallocations
+ Microsoft::WRL::ComPtr<ID3D11Buffer> m_vertexBuffer;
+ std::list<CD3DBuffer*> m_buffers;
+
+ static bool m_staticIndexBufferCreated;
+ static Microsoft::WRL::ComPtr<ID3D11Buffer> m_staticIndexBuffer;
+};
diff --git a/xbmc/guilib/GUIFontTTFGL.cpp b/xbmc/guilib/GUIFontTTFGL.cpp
new file mode 100644
index 0000000..e6dbc03
--- /dev/null
+++ b/xbmc/guilib/GUIFontTTFGL.cpp
@@ -0,0 +1,494 @@
+/*
+ * 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 "GUIFontTTFGL.h"
+
+#include "GUIFont.h"
+#include "GUIFontManager.h"
+#include "ServiceBroker.h"
+#include "Texture.h"
+#include "TextureManager.h"
+#include "gui3d.h"
+#include "utils/GLUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#ifdef HAS_GL
+#include "rendering/gl/RenderSystemGL.h"
+#elif HAS_GLES
+#include "rendering/gles/RenderSystemGLES.h"
+#endif
+#include "rendering/MatrixGL.h"
+
+#include <cassert>
+#include <memory>
+
+// stuff for freetype
+#include <ft2build.h>
+#include FT_FREETYPE_H
+#include FT_GLYPH_H
+#include FT_OUTLINE_H
+
+namespace
+{
+constexpr size_t ELEMENT_ARRAY_MAX_CHAR_INDEX = 1000;
+} /* namespace */
+
+CGUIFontTTF* CGUIFontTTF::CreateGUIFontTTF(const std::string& fontIdent)
+{
+ return new CGUIFontTTFGL(fontIdent);
+}
+
+CGUIFontTTFGL::CGUIFontTTFGL(const std::string& fontIdent) : CGUIFontTTF(fontIdent)
+{
+}
+
+CGUIFontTTFGL::~CGUIFontTTFGL(void)
+{
+ // It's important that all the CGUIFontCacheEntry objects are
+ // destructed before the CGUIFontTTFGL goes out of scope, because
+ // our virtual methods won't be accessible after this point
+ m_dynamicCache.Flush();
+ DeleteHardwareTexture();
+}
+
+bool CGUIFontTTFGL::FirstBegin()
+{
+#if defined(HAS_GL)
+ GLenum pixformat = GL_RED;
+ GLenum internalFormat;
+ unsigned int major, minor;
+ CRenderSystemGL* renderSystem = dynamic_cast<CRenderSystemGL*>(CServiceBroker::GetRenderSystem());
+ renderSystem->GetRenderVersion(major, minor);
+ if (major >= 3)
+ internalFormat = GL_R8;
+ else
+ internalFormat = GL_LUMINANCE;
+ renderSystem->EnableShader(ShaderMethodGL::SM_FONTS);
+#else
+ CRenderSystemGLES* renderSystem =
+ dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_FONTS);
+ GLenum pixformat = GL_ALPHA; // deprecated
+ GLenum internalFormat = GL_ALPHA;
+#endif
+
+ if (m_textureStatus == TEXTURE_REALLOCATED)
+ {
+ if (glIsTexture(m_nTexture))
+ CServiceBroker::GetGUI()->GetTextureManager().ReleaseHwTexture(m_nTexture);
+ m_textureStatus = TEXTURE_VOID;
+ }
+
+ if (m_textureStatus == TEXTURE_VOID)
+ {
+ // Have OpenGL generate a texture object handle for us
+ glGenTextures(1, static_cast<GLuint*>(&m_nTexture));
+
+ // Bind the texture object
+ glBindTexture(GL_TEXTURE_2D, m_nTexture);
+
+ // Set the texture's stretching properties
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ // Set the texture image -- THIS WORKS, so the pixels must be wrong.
+ glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, m_texture->GetWidth(), m_texture->GetHeight(), 0,
+ pixformat, GL_UNSIGNED_BYTE, 0);
+
+ VerifyGLState();
+ m_textureStatus = TEXTURE_UPDATED;
+ }
+
+ if (m_textureStatus == TEXTURE_UPDATED)
+ {
+ // Copies one more line in case we have to sample from there
+ m_updateY2 = std::min(m_updateY2 + 1, m_texture->GetHeight());
+
+ glBindTexture(GL_TEXTURE_2D, m_nTexture);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, m_updateY1, m_texture->GetWidth(), m_updateY2 - m_updateY1,
+ pixformat, GL_UNSIGNED_BYTE,
+ m_texture->GetPixels() + m_updateY1 * m_texture->GetPitch());
+
+ m_updateY1 = m_updateY2 = 0;
+ m_textureStatus = TEXTURE_READY;
+ }
+
+ // Turn Blending On
+ glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_ONE);
+ glEnable(GL_BLEND);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, m_nTexture);
+
+ return true;
+}
+
+void CGUIFontTTFGL::LastEnd()
+{
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!winSystem)
+ return;
+
+#ifdef HAS_GL
+ CRenderSystemGL* renderSystem = dynamic_cast<CRenderSystemGL*>(CServiceBroker::GetRenderSystem());
+
+ GLint posLoc = renderSystem->ShaderGetPos();
+ GLint colLoc = renderSystem->ShaderGetCol();
+ GLint tex0Loc = renderSystem->ShaderGetCoord0();
+ GLint modelLoc = renderSystem->ShaderGetModel();
+
+ CreateStaticVertexBuffers();
+
+ // Enable the attributes used by this shader
+ glEnableVertexAttribArray(posLoc);
+ glEnableVertexAttribArray(colLoc);
+ glEnableVertexAttribArray(tex0Loc);
+
+ if (!m_vertex.empty())
+ {
+
+ // Deal with vertices that had to use software clipping
+ std::vector<SVertex> vecVertices(6 * (m_vertex.size() / 4));
+ SVertex* vertices = &vecVertices[0];
+ for (size_t i = 0; i < m_vertex.size(); i += 4)
+ {
+ *vertices++ = m_vertex[i];
+ *vertices++ = m_vertex[i + 1];
+ *vertices++ = m_vertex[i + 2];
+
+ *vertices++ = m_vertex[i + 1];
+ *vertices++ = m_vertex[i + 3];
+ *vertices++ = m_vertex[i + 2];
+ }
+ vertices = &vecVertices[0];
+
+ GLuint VertexVBO;
+
+ glGenBuffers(1, &VertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, VertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(SVertex) * vecVertices.size(), &vecVertices[0],
+ GL_STATIC_DRAW);
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, GL_FALSE, sizeof(SVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(SVertex, x)));
+ glVertexAttribPointer(colLoc, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(SVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(SVertex, r)));
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, GL_FALSE, sizeof(SVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(SVertex, u)));
+
+ glDrawArrays(GL_TRIANGLES, 0, vecVertices.size());
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &VertexVBO);
+ }
+
+#else
+ // GLES 2.0 version.
+ CRenderSystemGLES* renderSystem =
+ dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+
+ GLint posLoc = renderSystem->GUIShaderGetPos();
+ GLint colLoc = renderSystem->GUIShaderGetCol();
+ GLint tex0Loc = renderSystem->GUIShaderGetCoord0();
+ GLint modelLoc = renderSystem->GUIShaderGetModel();
+
+
+ CreateStaticVertexBuffers();
+
+ // Enable the attributes used by this shader
+ glEnableVertexAttribArray(posLoc);
+ glEnableVertexAttribArray(colLoc);
+ glEnableVertexAttribArray(tex0Loc);
+
+ if (!m_vertex.empty())
+ {
+ // Deal with vertices that had to use software clipping
+ std::vector<SVertex> vecVertices(6 * (m_vertex.size() / 4));
+ SVertex* vertices = &vecVertices[0];
+
+ for (size_t i = 0; i < m_vertex.size(); i += 4)
+ {
+ *vertices++ = m_vertex[i];
+ *vertices++ = m_vertex[i + 1];
+ *vertices++ = m_vertex[i + 2];
+
+ *vertices++ = m_vertex[i + 1];
+ *vertices++ = m_vertex[i + 3];
+ *vertices++ = m_vertex[i + 2];
+ }
+
+ vertices = &vecVertices[0];
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, GL_FALSE, sizeof(SVertex),
+ reinterpret_cast<char*>(vertices) + offsetof(SVertex, x));
+ glVertexAttribPointer(colLoc, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(SVertex),
+ reinterpret_cast<char*>(vertices) + offsetof(SVertex, r));
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, GL_FALSE, sizeof(SVertex),
+ reinterpret_cast<char*>(vertices) + offsetof(SVertex, u));
+
+ glDrawArrays(GL_TRIANGLES, 0, vecVertices.size());
+ }
+#endif
+
+ if (!m_vertexTrans.empty())
+ {
+ // Deal with the vertices that can be hardware clipped and therefore translated
+
+ // Bind our pre-calculated array to GL_ELEMENT_ARRAY_BUFFER
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_elementArrayHandle);
+ // Store current scissor
+ CGraphicContext& context = winSystem->GetGfxContext();
+ CRect scissor = context.StereoCorrection(context.GetScissors());
+
+ for (size_t i = 0; i < m_vertexTrans.size(); i++)
+ {
+ if (m_vertexTrans[i].m_vertexBuffer->bufferHandle == 0)
+ {
+ continue;
+ }
+
+ // Apply the clip rectangle
+ CRect clip = renderSystem->ClipRectToScissorRect(m_vertexTrans[i].m_clip);
+ if (!clip.IsEmpty())
+ {
+ // intersect with current scissor
+ clip.Intersect(scissor);
+ // skip empty clip
+ if (clip.IsEmpty())
+ continue;
+ renderSystem->SetScissors(clip);
+ }
+
+ // Apply the translation to the currently active (top-of-stack) model view matrix
+ glMatrixModview.Push();
+ glMatrixModview.Get().Translatef(m_vertexTrans[i].m_translateX, m_vertexTrans[i].m_translateY,
+ m_vertexTrans[i].m_translateZ);
+ glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glMatrixModview.Get());
+
+ // Bind the buffer to the OpenGL context's GL_ARRAY_BUFFER binding point
+ glBindBuffer(GL_ARRAY_BUFFER, m_vertexTrans[i].m_vertexBuffer->bufferHandle);
+
+ // Do the actual drawing operation, split into groups of characters no
+ // larger than the pre-determined size of the element array
+ for (size_t character = 0; m_vertexTrans[i].m_vertexBuffer->size > character;
+ character += ELEMENT_ARRAY_MAX_CHAR_INDEX)
+ {
+ size_t count = m_vertexTrans[i].m_vertexBuffer->size - character;
+ count = std::min<size_t>(count, ELEMENT_ARRAY_MAX_CHAR_INDEX);
+
+ // Set up the offsets of the various vertex attributes within the buffer
+ // object bound to GL_ARRAY_BUFFER
+ glVertexAttribPointer(
+ posLoc, 3, GL_FLOAT, GL_FALSE, sizeof(SVertex),
+ reinterpret_cast<GLvoid*>(character * sizeof(SVertex) * 4 + offsetof(SVertex, x)));
+ glVertexAttribPointer(
+ colLoc, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(SVertex),
+ reinterpret_cast<GLvoid*>(character * sizeof(SVertex) * 4 + offsetof(SVertex, r)));
+ glVertexAttribPointer(
+ tex0Loc, 2, GL_FLOAT, GL_FALSE, sizeof(SVertex),
+ reinterpret_cast<GLvoid*>(character * sizeof(SVertex) * 4 + offsetof(SVertex, u)));
+
+ glDrawElements(GL_TRIANGLES, 6 * count, GL_UNSIGNED_SHORT, 0);
+ }
+
+ glMatrixModview.Pop();
+ }
+ // Restore the original scissor rectangle
+ renderSystem->SetScissors(scissor);
+ // Restore the original model view matrix
+ glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glMatrixModview.Get());
+ // Unbind GL_ARRAY_BUFFER and GL_ELEMENT_ARRAY_BUFFER
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ }
+
+ // Disable the attributes used by this shader
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(colLoc);
+ glDisableVertexAttribArray(tex0Loc);
+
+#ifdef HAS_GL
+ renderSystem->DisableShader();
+#else
+ renderSystem->DisableGUIShader();
+#endif
+}
+
+CVertexBuffer CGUIFontTTFGL::CreateVertexBuffer(const std::vector<SVertex>& vertices) const
+{
+ assert(vertices.size() % 4 == 0);
+ GLuint bufferHandle = 0;
+
+ // Do not create empty buffers, leave buffer as 0, it will be ignored in drawing stage
+ if (!vertices.empty())
+ {
+ // Generate a unique buffer object name and put it in bufferHandle
+ glGenBuffers(1, &bufferHandle);
+ // Bind the buffer to the OpenGL context's GL_ARRAY_BUFFER binding point
+ glBindBuffer(GL_ARRAY_BUFFER, bufferHandle);
+ // Create a data store for the buffer object bound to the GL_ARRAY_BUFFER
+ // binding point (i.e. our buffer object) and initialise it from the
+ // specified client-side pointer
+ glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(SVertex), vertices.data(),
+ GL_STATIC_DRAW);
+ // Unbind GL_ARRAY_BUFFER
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ }
+
+ return CVertexBuffer(bufferHandle, vertices.size() / 4, this);
+}
+
+void CGUIFontTTFGL::DestroyVertexBuffer(CVertexBuffer& buffer) const
+{
+ if (buffer.bufferHandle != 0)
+ {
+ // Release the buffer name for reuse
+ glDeleteBuffers(1, static_cast<GLuint*>(&buffer.bufferHandle));
+ buffer.bufferHandle = 0;
+ }
+}
+
+std::unique_ptr<CTexture> CGUIFontTTFGL::ReallocTexture(unsigned int& newHeight)
+{
+ newHeight = CTexture::PadPow2(newHeight);
+
+ std::unique_ptr<CTexture> newTexture =
+ CTexture::CreateTexture(m_textureWidth, newHeight, XB_FMT_A8);
+
+ if (!newTexture || !newTexture->GetPixels())
+ {
+ CLog::Log(LOGERROR, "GUIFontTTFGL::{}: Error creating new cache texture for size {:f}",
+ __func__, m_height);
+ return nullptr;
+ }
+
+ m_textureHeight = newTexture->GetHeight();
+ m_textureScaleY = 1.0f / m_textureHeight;
+ m_textureWidth = newTexture->GetWidth();
+ m_textureScaleX = 1.0f / m_textureWidth;
+ if (m_textureHeight < newHeight)
+ CLog::Log(LOGWARNING, "GUIFontTTFGL::{}: allocated new texture with height of {}, requested {}",
+ __func__, m_textureHeight, newHeight);
+ m_staticCache.Flush();
+ m_dynamicCache.Flush();
+
+ memset(newTexture->GetPixels(), 0, m_textureHeight * newTexture->GetPitch());
+ if (m_texture)
+ {
+ m_updateY1 = 0;
+ m_updateY2 = m_texture->GetHeight();
+
+ unsigned char* src = m_texture->GetPixels();
+ unsigned char* dst = newTexture->GetPixels();
+ for (unsigned int y = 0; y < m_texture->GetHeight(); y++)
+ {
+ memcpy(dst, src, m_texture->GetPitch());
+ src += m_texture->GetPitch();
+ dst += newTexture->GetPitch();
+ }
+ }
+
+ m_textureStatus = TEXTURE_REALLOCATED;
+
+ return newTexture;
+}
+
+bool CGUIFontTTFGL::CopyCharToTexture(
+ FT_BitmapGlyph bitGlyph, unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2)
+{
+ FT_Bitmap bitmap = bitGlyph->bitmap;
+
+ unsigned char* source = bitmap.buffer;
+ unsigned char* target = m_texture->GetPixels() + y1 * m_texture->GetPitch() + x1;
+
+ for (unsigned int y = y1; y < y2; y++)
+ {
+ memcpy(target, source, x2 - x1);
+ source += bitmap.width;
+ target += m_texture->GetPitch();
+ }
+
+ switch (m_textureStatus)
+ {
+ case TEXTURE_UPDATED:
+ {
+ m_updateY1 = std::min(m_updateY1, y1);
+ m_updateY2 = std::max(m_updateY2, y2);
+ }
+ break;
+
+ case TEXTURE_READY:
+ {
+ m_updateY1 = y1;
+ m_updateY2 = y2;
+ m_textureStatus = TEXTURE_UPDATED;
+ }
+ break;
+
+ case TEXTURE_REALLOCATED:
+ {
+ m_updateY2 = std::max(m_updateY2, y2);
+ }
+ break;
+
+ case TEXTURE_VOID:
+ default:
+ break;
+ }
+
+ return true;
+}
+
+void CGUIFontTTFGL::DeleteHardwareTexture()
+{
+ if (m_textureStatus != TEXTURE_VOID)
+ {
+ if (glIsTexture(m_nTexture))
+ CServiceBroker::GetGUI()->GetTextureManager().ReleaseHwTexture(m_nTexture);
+
+ m_textureStatus = TEXTURE_VOID;
+ m_updateY1 = m_updateY2 = 0;
+ }
+}
+
+void CGUIFontTTFGL::CreateStaticVertexBuffers(void)
+{
+ if (m_staticVertexBufferCreated)
+ return;
+
+ // Bind a new buffer to the OpenGL context's GL_ELEMENT_ARRAY_BUFFER binding point
+ glGenBuffers(1, &m_elementArrayHandle);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_elementArrayHandle);
+
+ // Create an array holding the mesh indices to convert quads to triangles
+ GLushort index[ELEMENT_ARRAY_MAX_CHAR_INDEX][6];
+ for (size_t i = 0; i < ELEMENT_ARRAY_MAX_CHAR_INDEX; i++)
+ {
+ index[i][0] = 4 * i;
+ index[i][1] = 4 * i + 1;
+ index[i][2] = 4 * i + 2;
+ index[i][3] = 4 * i + 1;
+ index[i][4] = 4 * i + 3;
+ index[i][5] = 4 * i + 2;
+ }
+
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof index, index, GL_STATIC_DRAW);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ m_staticVertexBufferCreated = true;
+}
+
+void CGUIFontTTFGL::DestroyStaticVertexBuffers(void)
+{
+ if (!m_staticVertexBufferCreated)
+ return;
+
+ glDeleteBuffers(1, &m_elementArrayHandle);
+ m_staticVertexBufferCreated = false;
+}
+
+GLuint CGUIFontTTFGL::m_elementArrayHandle{0};
+bool CGUIFontTTFGL::m_staticVertexBufferCreated{false};
diff --git a/xbmc/guilib/GUIFontTTFGL.h b/xbmc/guilib/GUIFontTTFGL.h
new file mode 100644
index 0000000..22fbe64
--- /dev/null
+++ b/xbmc/guilib/GUIFontTTFGL.h
@@ -0,0 +1,58 @@
+/*
+ * 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 "GUIFontTTF.h"
+
+#include <string>
+#include <vector>
+
+#include "system_gl.h"
+
+class CGUIFontTTFGL : public CGUIFontTTF
+{
+public:
+ explicit CGUIFontTTFGL(const std::string& fontIdent);
+ ~CGUIFontTTFGL(void) override;
+
+ bool FirstBegin() override;
+ void LastEnd() override;
+
+ CVertexBuffer CreateVertexBuffer(const std::vector<SVertex>& vertices) const override;
+ void DestroyVertexBuffer(CVertexBuffer& bufferHandle) const override;
+ static void CreateStaticVertexBuffers(void);
+ static void DestroyStaticVertexBuffers(void);
+
+protected:
+ std::unique_ptr<CTexture> ReallocTexture(unsigned int& newHeight) override;
+ bool CopyCharToTexture(FT_BitmapGlyph bitGlyph,
+ unsigned int x1,
+ unsigned int y1,
+ unsigned int x2,
+ unsigned int y2) override;
+ void DeleteHardwareTexture() override;
+
+ static GLuint m_elementArrayHandle;
+
+private:
+ unsigned int m_updateY1{0};
+ unsigned int m_updateY2{0};
+
+ enum TextureStatus
+ {
+ TEXTURE_VOID = 0,
+ TEXTURE_READY,
+ TEXTURE_REALLOCATED,
+ TEXTURE_UPDATED,
+ };
+
+ TextureStatus m_textureStatus{TEXTURE_VOID};
+
+ static bool m_staticVertexBufferCreated;
+};
diff --git a/xbmc/guilib/GUIImage.cpp b/xbmc/guilib/GUIImage.cpp
new file mode 100644
index 0000000..9b7ad23
--- /dev/null
+++ b/xbmc/guilib/GUIImage.cpp
@@ -0,0 +1,417 @@
+/*
+ * 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 "GUIImage.h"
+
+#include "GUIMessage.h"
+#include "utils/log.h"
+
+#include <cassert>
+
+using namespace KODI::GUILIB;
+
+CGUIImage::CGUIImage(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& texture)
+ : CGUIControl(parentID, controlID, posX, posY, width, height),
+ m_texture(CGUITexture::CreateTexture(posX, posY, width, height, texture))
+{
+ m_crossFadeTime = 0;
+ m_currentFadeTime = 0;
+ m_lastRenderTime = 0;
+ ControlType = GUICONTROL_IMAGE;
+ m_bDynamicResourceAlloc=false;
+}
+
+CGUIImage::CGUIImage(const CGUIImage& left)
+ : CGUIControl(left),
+ m_image(left.m_image),
+ m_info(left.m_info),
+ m_texture(left.m_texture->Clone()),
+ m_fadingTextures(),
+ m_currentTexture(),
+ m_currentFallback()
+{
+ m_crossFadeTime = left.m_crossFadeTime;
+ // defaults
+ m_currentFadeTime = 0;
+ m_lastRenderTime = 0;
+ ControlType = GUICONTROL_IMAGE;
+ m_bDynamicResourceAlloc=false;
+}
+
+CGUIImage::~CGUIImage(void) = default;
+
+void CGUIImage::UpdateVisibility(const CGUIListItem *item)
+{
+ CGUIControl::UpdateVisibility(item);
+
+ // now that we've checked for conditional info, we can
+ // check for allocation
+ AllocateOnDemand();
+}
+
+void CGUIImage::UpdateDiffuseColor(const CGUIListItem* item)
+{
+ if (m_texture->SetDiffuseColor(m_diffuseColor, item))
+ {
+ MarkDirtyRegion();
+ }
+}
+
+void CGUIImage::UpdateInfo(const CGUIListItem *item)
+{
+ // The texture may also depend on info conditions. Update the diffuse color in that case.
+ if (m_texture->GetDiffuseColor().HasInfo())
+ UpdateDiffuseColor(item);
+
+ if (m_info.IsConstant())
+ return; // nothing to do
+
+ // don't allow image to change while animating out
+ if (HasProcessed() && IsAnimating(ANIM_TYPE_HIDDEN) && !IsVisibleFromSkin())
+ return;
+
+ if (item)
+ SetFileName(m_info.GetItemLabel(item, true, &m_currentFallback));
+ else
+ SetFileName(m_info.GetLabel(m_parentID, true, &m_currentFallback));
+}
+
+void CGUIImage::AllocateOnDemand()
+{
+ // if we're hidden, we can free our resources and return
+ if (!IsVisible() && m_visible != DELAYED)
+ {
+ if (m_bDynamicResourceAlloc && m_texture->IsAllocated())
+ FreeResourcesButNotAnims();
+ return;
+ }
+
+ // either visible or delayed - we need the resources allocated in either case
+ if (!m_texture->IsAllocated())
+ AllocResources();
+}
+
+void CGUIImage::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ // check whether our image failed to allocate, and if so drop back to the fallback image
+ if (m_texture->FailedToAlloc() && m_texture->GetFileName() != m_info.GetFallback())
+ {
+ if (!m_currentFallback.empty() && m_texture->GetFileName() != m_currentFallback)
+ m_texture->SetFileName(m_currentFallback);
+ else
+ m_texture->SetFileName(m_info.GetFallback());
+ }
+
+ if (m_crossFadeTime)
+ {
+ // make sure our texture has started allocating
+ if (m_texture->AllocResources())
+ MarkDirtyRegion();
+
+ // compute the frame time
+ unsigned int frameTime = 0;
+ if (m_lastRenderTime)
+ frameTime = currentTime - m_lastRenderTime;
+ if (!frameTime)
+ frameTime = (unsigned int)(1000 / CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS());
+ m_lastRenderTime = currentTime;
+
+ if (m_fadingTextures.size()) // have some fading images
+ { // anything other than the last old texture needs to be faded out as per usual
+ for (std::vector<CFadingTexture *>::iterator i = m_fadingTextures.begin(); i != m_fadingTextures.end() - 1;)
+ {
+ if (!ProcessFading(*i, frameTime, currentTime))
+ i = m_fadingTextures.erase(i);
+ else
+ ++i;
+ }
+
+ if (m_texture->ReadyToRender() || m_texture->GetFileName().empty())
+ { // fade out the last one as well
+ if (!ProcessFading(m_fadingTextures[m_fadingTextures.size() - 1], frameTime, currentTime))
+ m_fadingTextures.erase(m_fadingTextures.end() - 1);
+ }
+ else
+ { // keep the last one fading in
+ CFadingTexture *texture = m_fadingTextures[m_fadingTextures.size() - 1];
+ texture->m_fadeTime += frameTime;
+ if (texture->m_fadeTime > m_crossFadeTime)
+ texture->m_fadeTime = m_crossFadeTime;
+
+ if (texture->m_texture->SetAlpha(GetFadeLevel(texture->m_fadeTime)))
+ MarkDirtyRegion();
+ if (texture->m_texture->SetDiffuseColor(m_diffuseColor))
+ MarkDirtyRegion();
+ if (texture->m_texture->Process(currentTime))
+ MarkDirtyRegion();
+ }
+ }
+
+ if (m_texture->ReadyToRender() || m_texture->GetFileName().empty())
+ { // fade the new one in
+ m_currentFadeTime += frameTime;
+ if (m_currentFadeTime > m_crossFadeTime || frameTime == 0) // for if we allocate straight away on creation
+ m_currentFadeTime = m_crossFadeTime;
+ }
+ if (m_texture->SetAlpha(GetFadeLevel(m_currentFadeTime)))
+ MarkDirtyRegion();
+ }
+
+ if (!m_texture->GetDiffuseColor().HasInfo())
+ UpdateDiffuseColor(nullptr);
+
+ if (m_texture->Process(currentTime))
+ MarkDirtyRegion();
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIImage::Render()
+{
+ if (!IsVisible()) return;
+
+ for (auto& itr : m_fadingTextures)
+ itr->m_texture->Render();
+
+ m_texture->Render();
+
+ CGUIControl::Render();
+}
+
+bool CGUIImage::ProcessFading(CGUIImage::CFadingTexture *texture, unsigned int frameTime, unsigned int currentTime)
+{
+ assert(texture);
+ if (texture->m_fadeTime <= frameTime)
+ { // time to kill off the texture
+ MarkDirtyRegion();
+ delete texture;
+ return false;
+ }
+ // render this texture
+ texture->m_fadeTime -= frameTime;
+
+ if (texture->m_texture->SetAlpha(GetFadeLevel(texture->m_fadeTime)))
+ MarkDirtyRegion();
+ if (texture->m_texture->SetDiffuseColor(m_diffuseColor))
+ MarkDirtyRegion();
+ if (texture->m_texture->Process(currentTime))
+ MarkDirtyRegion();
+
+ return true;
+}
+
+bool CGUIImage::OnAction(const CAction &action)
+{
+ return false;
+}
+
+bool CGUIImage::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_REFRESH_THUMBS)
+ {
+ if (!m_info.IsConstant())
+ FreeTextures(true); // true as we want to free the texture immediately
+ return true;
+ }
+ else if (message.GetMessage() == GUI_MSG_SET_FILENAME)
+ {
+ SetFileName(message.GetLabel());
+ return true;
+ }
+ else if (message.GetMessage() == GUI_MSG_GET_FILENAME)
+ {
+ message.SetLabel(GetFileName());
+ return true;
+ }
+ return CGUIControl::OnMessage(message);
+}
+
+void CGUIImage::AllocResources()
+{
+ if (m_texture->GetFileName().empty())
+ return;
+
+ CGUIControl::AllocResources();
+ m_texture->AllocResources();
+}
+
+void CGUIImage::FreeTextures(bool immediately /* = false */)
+{
+ m_texture->FreeResources(immediately);
+ for (unsigned int i = 0; i < m_fadingTextures.size(); i++)
+ delete m_fadingTextures[i];
+ m_fadingTextures.clear();
+ m_currentTexture.clear();
+ if (!m_info.IsConstant()) // constant textures never change
+ m_texture->SetFileName("");
+}
+
+void CGUIImage::FreeResources(bool immediately)
+{
+ FreeTextures(immediately);
+ CGUIControl::FreeResources(immediately);
+}
+
+void CGUIImage::SetInvalid()
+{
+ m_texture->SetInvalid();
+ CGUIControl::SetInvalid();
+}
+
+// WORKAROUND - we are currently resetting all animations when this is called, which shouldn't be the case
+// see CGUIControl::FreeResources() - this needs remedying.
+void CGUIImage::FreeResourcesButNotAnims()
+{
+ FreeTextures();
+ m_bAllocated=false;
+ m_hasProcessed = false;
+}
+
+void CGUIImage::DynamicResourceAlloc(bool bOnOff)
+{
+ m_bDynamicResourceAlloc = bOnOff;
+ m_texture->DynamicResourceAlloc(bOnOff);
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+}
+
+bool CGUIImage::CanFocus() const
+{
+ return false;
+}
+
+float CGUIImage::GetTextureWidth() const
+{
+ return m_texture->GetTextureWidth();
+}
+
+float CGUIImage::GetTextureHeight() const
+{
+ return m_texture->GetTextureHeight();
+}
+
+CRect CGUIImage::CalcRenderRegion() const
+{
+ CRect region = m_texture->GetRenderRect();
+
+ for (const auto& itr : m_fadingTextures)
+ region.Union(itr->m_texture->GetRenderRect());
+
+ return CGUIControl::CalcRenderRegion().Intersect(region);
+}
+
+const std::string &CGUIImage::GetFileName() const
+{
+ return m_texture->GetFileName();
+}
+
+void CGUIImage::SetAspectRatio(const CAspectRatio &aspect)
+{
+ m_texture->SetAspectRatio(aspect);
+}
+
+void CGUIImage::SetCrossFade(unsigned int time)
+{
+ m_crossFadeTime = time;
+ if (!m_crossFadeTime && m_texture->IsLazyLoaded() && !m_info.GetFallback().empty())
+ m_crossFadeTime = 1;
+}
+
+void CGUIImage::SetFileName(const std::string& strFileName, bool setConstant, const bool useCache)
+{
+ if (setConstant)
+ m_info.SetLabel(strFileName, "", GetParentID());
+
+ // Set whether or not to use cache
+ m_texture->SetUseCache(useCache);
+
+ if (m_crossFadeTime)
+ {
+ // set filename on the next texture
+ if (m_currentTexture == strFileName)
+ return; // nothing to do - we already have this image
+
+ if (m_texture->ReadyToRender() || m_texture->GetFileName().empty())
+ { // save the current image
+ m_fadingTextures.push_back(new CFadingTexture(m_texture.get(), m_currentFadeTime));
+ MarkDirtyRegion();
+ }
+ m_currentFadeTime = 0;
+ }
+ if (m_currentTexture != strFileName)
+ { // texture is changing - attempt to load it, and save the name in m_currentTexture.
+ // we'll check whether it loaded or not in Render()
+ m_currentTexture = strFileName;
+ if (m_texture->SetFileName(m_currentTexture))
+ MarkDirtyRegion();
+ }
+}
+
+#ifdef _DEBUG
+void CGUIImage::DumpTextureUse()
+{
+ if (m_texture->IsAllocated())
+ {
+ if (GetID())
+ CLog::Log(LOGDEBUG, "Image control {} using texture {}", GetID(), m_texture->GetFileName());
+ else
+ CLog::Log(LOGDEBUG, "Using texture {}", m_texture->GetFileName());
+ }
+}
+#endif
+
+void CGUIImage::SetWidth(float width)
+{
+ m_texture->SetWidth(width);
+ CGUIControl::SetWidth(m_texture->GetWidth());
+}
+
+void CGUIImage::SetHeight(float height)
+{
+ m_texture->SetHeight(height);
+ CGUIControl::SetHeight(m_texture->GetHeight());
+}
+
+void CGUIImage::SetPosition(float posX, float posY)
+{
+ m_texture->SetPosition(posX, posY);
+ CGUIControl::SetPosition(posX, posY);
+}
+
+void CGUIImage::SetInfo(const GUIINFO::CGUIInfoLabel &info)
+{
+ m_info = info;
+ // a constant image never needs updating
+ if (m_info.IsConstant())
+ m_texture->SetFileName(m_info.GetLabel(0));
+}
+
+unsigned char CGUIImage::GetFadeLevel(unsigned int time) const
+{
+ float amount = (float)time / m_crossFadeTime;
+ // we want a semi-transparent image, so we need to use a more complicated
+ // fade technique. Assuming a black background (not generally true, but still...)
+ // we have
+ // b(t) = [a - b(1-t)*a] / a*(1-b(1-t)*a),
+ // where a = alpha, and b(t):[0,1] -> [0,1] is the blend function.
+ // solving, we get
+ // b(t) = [1 - (1-a)^t] / a
+ const float alpha = 0.7f;
+ return (unsigned char)(255.0f * (1 - pow(1-alpha, amount))/alpha);
+}
+
+std::string CGUIImage::GetDescription(void) const
+{
+ return GetFileName();
+}
+
diff --git a/xbmc/guilib/GUIImage.dox b/xbmc/guilib/GUIImage.dox
new file mode 100644
index 0000000..cd56fe7
--- /dev/null
+++ b/xbmc/guilib/GUIImage.dox
@@ -0,0 +1,98 @@
+/*!
+
+\page Image_Control Image Control
+\brief **Used to show an image.**
+
+\tableofcontents
+
+The image control is used for displaying images in Kodi. You can choose the
+position, size, transparency and contents of the image to be displayed.
+
+--------------------------------------------------------------------------------
+\section Image_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="image" id="1">
+ <description>My first image control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+ <colordiffuse>FFFFFFFF</colordiffuse>
+ <fadetime>200</fadetime>
+ <texture border="5" flipy="true" flipx="false">mytexture.png</texture>
+ <bordertexture border="5">mybordertexture.png</bordertexture>
+ <bordersize>5</bordersize>
+ <texture>$INFO[MusicPlayer.Cover]</texture>
+ <aspectratio>keep</aspectratio>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Image_Control_sect2 Image Size and Type Restrictions
+For the <b>`<texture>`</b> tags, and for all <b>`<texture>`</b> tags in
+other controls, there is a small set of rules that you should follow if at all
+possible:
+
+\subsection Image_Control_sect2_1 Size
+Images can be any size, though some graphics cards allow only power of 2 textures,
+so this may be a consideration. For the most case, it doesn't really matter what
+size things are - Kodi will quite happily load most files.
+
+\subsection Image_Control_sect2_2 Formats
+If you wish to use transparency, then use PNG. It is suggested that you use PNG
+and JPG as much as possible. Note that once the images are injected into
+Textures.xbt, they are not stored as JPG or PNG – rather they are stored in
+a native format used for GPUs for faster loading, such as ARGB or DXTc textures.
+The size of the images (in kb) is therefore not as important as the size of the
+images in pixels – so feel free to store them in a lossless (eg PNG) manner if
+you wish.
+
+The only exception to this is if you require an animated texture. In this case,
+we support only animated GIF. There are also some animated gifs that may not
+work. Use ImageReady CS and make sure you set the gif-anim to “restore to
+background” and they should work fine.
+
+--------------------------------------------------------------------------------
+\section Image_Control_sect3 Available tags and attributes
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| aspectratio | This specifies how the image will be drawn inside the box defined by <b>`<width>`</b> and <b>`<height>`</b>. [See here for more information](http://kodi.wiki/view/Aspect_Ratio).
+| texture | Specifies the image file which should be displayed. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| bordertexture | Specifies the image file which should be displayed as a border around the image. Use the <b>`<bordersize>`</b> to specify the size of the border. The <b>`<width>`</b>, <b>`<height>`</b> box specifies the size of the image plus border.
+| bordersize | Specifies the size of the border. A single number specifies the border should be the same size all the way around the image, whereas a comma separated list of 4 values indicates left,top,right,bottom values.
+| info | Specifies the information that this image control is presenting. Kodi will select the texture to use based on this tag. [See here for more information](http://kodi.wiki/view/InfoLabels).
+| fadetime | This specifies a crossfade time that will be used whenever the underlying <b>`<texture>`</b> filename changes. The previous image will be held until the new image is ready, and then they will be crossfaded. This is particularly useful for a large thumbnail image showing the focused item in a list, or for fanart or other large backdrops.
+| background | For images inside a container, you can specify background="true" to load the textures in a background worker thread. [See here for additional information about background loading](http://kodi.wiki/view/Background_Image_Loader).
+
+For the tags "texture" and "bordertexture", the following attributes are available.
+
+| Attribute | Description |
+|--------------:|:--------------------------------------------------------------|
+| border | Used to specify a region of the texture to be considered a border that should not be scaled when the texture is scaled to fit a control's dimensions. The portion in the border region will remain unscaled. Particularly useful to achieve rounded corners that do not change size when a control is resized. Note that zoom animations and GUI rescaling will still affect the border region - it is just the scaling of the texture to the control size which is unaffected. Using border="5" will give a 5 pixel border all around the texture. You can specify each of the border amounts on each edge individually using border="left,top,right,bottom".
+| infill | Available if the "border" attribute is used. Hint for the GUI drawing routine if the center portion or just the outer frame should be drawn. Useful if the texture center is fully transparent. Accepts boolean values "true" (default) and "false".
+| flipx | Specifies that the texture should be flipped in the horizontal direction, useful for reflections.
+| flipy | Specifies that the texture should be flipped in the vertical direction, useful for reflections.
+| background | Used on the <b>`<imagepath>`</b> tag. When set to "true", this forces the image to be loaded via the large texture manager (except for textures located in Textures.xbt).
+| diffuse | Specifies a diffuse texture which is "modulated" or multiplied with the texture in order to give specific effects, such as making the image gain transparency, or tint it in various ways. As it's applied per-pixel many effects are possible, the most popular being by a transparent reflection which slowly gets more and more opaque. This is achieved using a WHITE image where the transparency increases as you move down the texture.
+| colordiffuse | This specifies the color to be used for the texture basis. It's is specified in hexadecimal notation using the AARRGGBB format. If you define <b>`<texture colordiffuse="FFFFAAFF">texture.png</texture>`</b> (magenta), the image will be given a magenta tint when rendered. You can also specify this as a name from the colour theme, or you can also use $VAR and $INFO for dynamic values.
+
+@skinning_v20 <b>infill</b> New texture attribute added.
+
+--------------------------------------------------------------------------------
+\section Image_Control_sect4 See also
+
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIImage.h b/xbmc/guilib/GUIImage.h
new file mode 100644
index 0000000..e9e9347
--- /dev/null
+++ b/xbmc/guilib/GUIImage.h
@@ -0,0 +1,119 @@
+/*
+ * 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
+
+/*!
+\file guiImage.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUITexture.h"
+#include "guilib/guiinfo/GUIInfoLabel.h"
+
+#include <vector>
+
+/*!
+ \ingroup controls
+ \brief
+ */
+
+class CGUIImage : public CGUIControl
+{
+public:
+ class CFadingTexture
+ {
+ public:
+ CFadingTexture(const CGUITexture* texture, unsigned int fadeTime)
+ {
+ // create a copy of our texture, and allocate resources
+ m_texture.reset(texture->Clone());
+ m_texture->AllocResources();
+ m_fadeTime = fadeTime;
+ m_fading = false;
+ };
+ ~CFadingTexture()
+ {
+ m_texture->FreeResources();
+ };
+
+ std::unique_ptr<CGUITexture> m_texture; ///< texture to fade out
+ unsigned int m_fadeTime; ///< time to fade out (ms)
+ bool m_fading; ///< whether we're fading out
+
+ private:
+ CFadingTexture(const CFadingTexture&) = delete;
+ CFadingTexture& operator=(const CFadingTexture&) = delete;
+ };
+
+ CGUIImage(int parentID, int controlID, float posX, float posY, float width, float height, const CTextureInfo& texture);
+ CGUIImage(const CGUIImage &left);
+ ~CGUIImage(void) override;
+ CGUIImage* Clone() const override { return new CGUIImage(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ void UpdateVisibility(const CGUIListItem *item = NULL) override;
+ bool OnAction(const CAction &action) override ;
+ bool OnMessage(CGUIMessage& message) override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ bool IsDynamicallyAllocated() override { return m_bDynamicResourceAlloc; }
+ void SetInvalid() override;
+ bool CanFocus() const override;
+ void UpdateInfo(const CGUIListItem *item = NULL) override;
+
+ virtual void SetInfo(const KODI::GUILIB::GUIINFO::CGUIInfoLabel &info);
+ virtual void SetFileName(const std::string& strFileName, bool setConstant = false, const bool useCache = true);
+ virtual void SetAspectRatio(const CAspectRatio &aspect);
+ void SetWidth(float width) override;
+ void SetHeight(float height) override;
+ void SetPosition(float posX, float posY) override;
+ std::string GetDescription() const override;
+ void SetCrossFade(unsigned int time);
+
+ const std::string& GetFileName() const;
+ float GetTextureWidth() const;
+ float GetTextureHeight() const;
+
+ CRect CalcRenderRegion() const override;
+
+#ifdef _DEBUG
+ void DumpTextureUse() override;
+#endif
+protected:
+ virtual void AllocateOnDemand();
+ virtual void FreeTextures(bool immediately = false);
+ void FreeResourcesButNotAnims();
+ unsigned char GetFadeLevel(unsigned int time) const;
+ bool ProcessFading(CFadingTexture *texture, unsigned int frameTime, unsigned int currentTime);
+
+ /*!
+ * \brief Update the diffuse color based on the current item infos
+ * \param item the item to for info resolution
+ */
+ void UpdateDiffuseColor(const CGUIListItem* item);
+
+ bool m_bDynamicResourceAlloc;
+
+ // border + conditional info
+ CTextureInfo m_image;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_info;
+
+ std::unique_ptr<CGUITexture> m_texture;
+ std::vector<CFadingTexture *> m_fadingTextures;
+ std::string m_currentTexture;
+ std::string m_currentFallback;
+
+ unsigned int m_crossFadeTime;
+ unsigned int m_currentFadeTime;
+ unsigned int m_lastRenderTime;
+};
+
diff --git a/xbmc/guilib/GUIIncludes.cpp b/xbmc/guilib/GUIIncludes.cpp
new file mode 100644
index 0000000..d950c2d
--- /dev/null
+++ b/xbmc/guilib/GUIIncludes.cpp
@@ -0,0 +1,703 @@
+/*
+ * 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 "GUIIncludes.h"
+
+#include "GUIInfoManager.h"
+#include "addons/Skin.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/guiinfo/GUIInfoLabel.h"
+#include "interfaces/info/SkinVariable.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+using namespace KODI::GUILIB;
+
+CGUIIncludes::CGUIIncludes()
+{
+ m_constantAttributes.insert("x");
+ m_constantAttributes.insert("y");
+ m_constantAttributes.insert("width");
+ m_constantAttributes.insert("height");
+ m_constantAttributes.insert("center");
+ m_constantAttributes.insert("max");
+ m_constantAttributes.insert("min");
+ m_constantAttributes.insert("w");
+ m_constantAttributes.insert("h");
+ m_constantAttributes.insert("time");
+ m_constantAttributes.insert("acceleration");
+ m_constantAttributes.insert("delay");
+ m_constantAttributes.insert("start");
+ m_constantAttributes.insert("end");
+ m_constantAttributes.insert("center");
+ m_constantAttributes.insert("border");
+ m_constantAttributes.insert("repeat");
+
+ m_constantNodes.insert("posx");
+ m_constantNodes.insert("posy");
+ m_constantNodes.insert("left");
+ m_constantNodes.insert("centerleft");
+ m_constantNodes.insert("right");
+ m_constantNodes.insert("centerright");
+ m_constantNodes.insert("top");
+ m_constantNodes.insert("centertop");
+ m_constantNodes.insert("bottom");
+ m_constantNodes.insert("centerbottom");
+ m_constantNodes.insert("width");
+ m_constantNodes.insert("height");
+ m_constantNodes.insert("offsetx");
+ m_constantNodes.insert("offsety");
+ m_constantNodes.insert("textoffsetx");
+ m_constantNodes.insert("textoffsety");
+ m_constantNodes.insert("textwidth");
+ m_constantNodes.insert("spinposx");
+ m_constantNodes.insert("spinposy");
+ m_constantNodes.insert("spinwidth");
+ m_constantNodes.insert("spinheight");
+ m_constantNodes.insert("radioposx");
+ m_constantNodes.insert("radioposy");
+ m_constantNodes.insert("radiowidth");
+ m_constantNodes.insert("radioheight");
+ m_constantNodes.insert("sliderwidth");
+ m_constantNodes.insert("sliderheight");
+ m_constantNodes.insert("itemgap");
+ m_constantNodes.insert("bordersize");
+ m_constantNodes.insert("timeperimage");
+ m_constantNodes.insert("fadetime");
+ m_constantNodes.insert("pauseatend");
+ m_constantNodes.insert("depth");
+ m_constantNodes.insert("movement");
+ m_constantNodes.insert("focusposition");
+
+ m_expressionAttributes.insert("condition");
+
+ m_expressionNodes.insert("visible");
+ m_expressionNodes.insert("enable");
+ m_expressionNodes.insert("usealttexture");
+ m_expressionNodes.insert("selected");
+}
+
+CGUIIncludes::~CGUIIncludes() = default;
+
+void CGUIIncludes::Clear()
+{
+ m_includes.clear();
+ m_defaults.clear();
+ m_constants.clear();
+ m_skinvariables.clear();
+ m_files.clear();
+ m_expressions.clear();
+}
+
+void CGUIIncludes::Load(const std::string &file)
+{
+ if (!Load_Internal(file))
+ return;
+ FlattenExpressions();
+ FlattenSkinVariableConditions();
+}
+
+bool CGUIIncludes::Load_Internal(const std::string &file)
+{
+ // check to see if we already have this loaded
+ if (HasLoaded(file))
+ return true;
+
+ CXBMCTinyXML doc;
+ if (!doc.LoadFile(file))
+ {
+ CLog::Log(LOGINFO, "Error loading include file {}: {} (row: {}, col: {})", file,
+ doc.ErrorDesc(), doc.ErrorRow(), doc.ErrorCol());
+ return false;
+ }
+
+ TiXmlElement *root = doc.RootElement();
+ if (!root || !StringUtils::EqualsNoCase(root->Value(), "includes"))
+ {
+ CLog::Log(LOGERROR, "Error loading include file {}: Root element <includes> required.", file);
+ return false;
+ }
+
+ // load components
+ LoadDefaults(root);
+ LoadConstants(root);
+ LoadExpressions(root);
+ LoadVariables(root);
+ LoadIncludes(root);
+
+ m_files.push_back(file);
+
+ return true;
+}
+
+void CGUIIncludes::LoadDefaults(const TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ const TiXmlElement* child = node->FirstChildElement("default");
+ while (child)
+ {
+ const char *type = child->Attribute("type");
+ if (type && child->FirstChild())
+ m_defaults.insert(std::make_pair(type, *child));
+
+ child = child->NextSiblingElement("default");
+ }
+}
+
+void CGUIIncludes::LoadExpressions(const TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ const TiXmlElement* child = node->FirstChildElement("expression");
+ while (child)
+ {
+ const char *tagName = child->Attribute("name");
+ if (tagName && child->FirstChild())
+ m_expressions.insert(std::make_pair(tagName, "[" + child->FirstChild()->ValueStr() + "]"));
+
+ child = child->NextSiblingElement("expression");
+ }
+}
+
+
+void CGUIIncludes::LoadConstants(const TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ const TiXmlElement* child = node->FirstChildElement("constant");
+ while (child)
+ {
+ const char *tagName = child->Attribute("name");
+ if (tagName && child->FirstChild())
+ m_constants.insert(std::make_pair(tagName, child->FirstChild()->ValueStr()));
+
+ child = child->NextSiblingElement("constant");
+ }
+}
+
+void CGUIIncludes::LoadVariables(const TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ const TiXmlElement* child = node->FirstChildElement("variable");
+ while (child)
+ {
+ const char *tagName = child->Attribute("name");
+ if (tagName && child->FirstChild())
+ m_skinvariables.insert(std::make_pair(tagName, *child));
+
+ child = child->NextSiblingElement("variable");
+ }
+}
+
+void CGUIIncludes::LoadIncludes(const TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ const TiXmlElement* child = node->FirstChildElement("include");
+ while (child)
+ {
+ const char *tagName = child->Attribute("name");
+ if (tagName && child->FirstChild())
+ {
+ // we'll parse and store parameter list with defaults when include definition is first encountered
+ // if there's a <definition> tag only use its body as the actually included part
+ const TiXmlElement *definitionTag = child->FirstChildElement("definition");
+ const TiXmlElement *includeBody = definitionTag ? definitionTag : child;
+
+ // if there's a <param> tag there also must be a <definition> tag
+ Params defaultParams;
+ bool haveParamTags = GetParameters(child, "default", defaultParams);
+ if (haveParamTags && !definitionTag)
+ CLog::Log(LOGWARNING, "Skin has invalid include definition: {}", tagName);
+ else
+ m_includes.insert({ tagName, { *includeBody, std::move(defaultParams) } });
+ }
+ else if (child->Attribute("file"))
+ {
+ std::string file = g_SkinInfo->GetSkinPath(child->Attribute("file"));
+ const char *condition = child->Attribute("condition");
+
+ if (condition)
+ { // load include file if condition evals to true
+ if (CServiceBroker::GetGUI()->GetInfoManager().Register(condition)->Get(
+ INFO::DEFAULT_CONTEXT))
+ Load_Internal(file);
+ }
+ else
+ Load_Internal(file);
+ }
+ child = child->NextSiblingElement("include");
+ }
+}
+
+void CGUIIncludes::FlattenExpressions()
+{
+ for (auto& expression : m_expressions)
+ {
+ std::vector<std::string> resolved = std::vector<std::string>();
+ resolved.push_back(expression.first);
+ FlattenExpression(expression.second, resolved);
+ }
+}
+
+void CGUIIncludes::FlattenExpression(std::string &expression, const std::vector<std::string> &resolved)
+{
+ std::string original(expression);
+ GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(expression, "EXP", [&](const std::string &expressionName) -> std::string {
+ if (std::find(resolved.begin(), resolved.end(), expressionName) != resolved.end())
+ {
+ CLog::Log(LOGERROR, "Skin has a circular expression \"{}\": {}", resolved.back(), original);
+ return std::string();
+ }
+ auto it = m_expressions.find(expressionName);
+ if (it == m_expressions.end())
+ return std::string();
+
+ std::vector<std::string> rescopy = resolved;
+ rescopy.push_back(expressionName);
+ FlattenExpression(it->second, rescopy);
+
+ return it->second;
+ });
+}
+
+void CGUIIncludes::FlattenSkinVariableConditions()
+{
+ for (auto& variable : m_skinvariables)
+ {
+ TiXmlElement* valueNode = variable.second.FirstChildElement("value");
+ while (valueNode)
+ {
+ const char *condition = valueNode->Attribute("condition");
+ if (condition)
+ valueNode->SetAttribute("condition", ResolveExpressions(condition));
+
+ valueNode = valueNode->NextSiblingElement("value");
+ }
+ }
+}
+
+bool CGUIIncludes::HasLoaded(const std::string &file) const
+{
+ for (const auto& loadedFile : m_files)
+ {
+ if (loadedFile == file)
+ return true;
+ }
+ return false;
+}
+
+void CGUIIncludes::Resolve(TiXmlElement *node, std::map<INFO::InfoPtr, bool>* xmlIncludeConditions /* = NULL */)
+{
+ if (!node)
+ return;
+
+ SetDefaults(node);
+ ResolveConstants(node);
+ ResolveExpressions(node);
+ ResolveIncludes(node, xmlIncludeConditions);
+
+ TiXmlElement *child = node->FirstChildElement();
+ while (child)
+ {
+ // recursive call
+ Resolve(child, xmlIncludeConditions);
+ child = child->NextSiblingElement();
+ }
+}
+
+void CGUIIncludes::SetDefaults(TiXmlElement *node)
+{
+ if (node->ValueStr() != "control")
+ return;
+
+ std::string type = XMLUtils::GetAttribute(node, "type");
+ const auto it = m_defaults.find(type);
+ if (it != m_defaults.end())
+ {
+ // we don't insert <left> et. al. if <posx> or <posy> is specified
+ bool hasPosX(node->FirstChild("posx") != nullptr);
+ bool hasPosY(node->FirstChild("posy") != nullptr);
+
+ const TiXmlElement &element = (*it).second;
+ const TiXmlElement *tag = element.FirstChildElement();
+ while (tag)
+ {
+ std::string value = tag->ValueStr();
+ bool skip(false);
+ if (hasPosX && (value == "left" || value == "right" || value == "centerleft" || value == "centerright"))
+ skip = true;
+ if (hasPosY && (value == "top" || value == "bottom" || value == "centertop" || value == "centerbottom"))
+ skip = true;
+ // we insert at the end of block
+ if (!skip)
+ node->InsertEndChild(*tag);
+ tag = tag->NextSiblingElement();
+ }
+ }
+}
+
+void CGUIIncludes::ResolveConstants(TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ TiXmlNode *child = node->FirstChild();
+ if (child && child->Type() == TiXmlNode::TINYXML_TEXT && m_constantNodes.count(node->ValueStr()))
+ {
+ child->SetValue(ResolveConstant(child->ValueStr()));
+ }
+ else
+ {
+ TiXmlAttribute *attribute = node->FirstAttribute();
+ while (attribute)
+ {
+ if (m_constantAttributes.count(attribute->Name()))
+ attribute->SetValue(ResolveConstant(attribute->ValueStr()));
+
+ attribute = attribute->Next();
+ }
+ }
+}
+
+void CGUIIncludes::ResolveExpressions(TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ TiXmlNode *child = node->FirstChild();
+ if (child && child->Type() == TiXmlNode::TINYXML_TEXT && m_expressionNodes.count(node->ValueStr()))
+ {
+ child->SetValue(ResolveExpressions(child->ValueStr()));
+ }
+ else
+ {
+ TiXmlAttribute *attribute = node->FirstAttribute();
+ while (attribute)
+ {
+ if (m_expressionAttributes.count(attribute->Name()))
+ attribute->SetValue(ResolveExpressions(attribute->ValueStr()));
+
+ attribute = attribute->Next();
+ }
+ }
+}
+
+void CGUIIncludes::ResolveIncludes(TiXmlElement *node, std::map<INFO::InfoPtr, bool>* xmlIncludeConditions /* = NULL */)
+{
+ if (!node)
+ return;
+
+ TiXmlElement *include = node->FirstChildElement("include");
+ while (include)
+ {
+ // file: load includes from specified XML file
+ const char *file = include->Attribute("file");
+ if (file)
+ Load(g_SkinInfo->GetSkinPath(file));
+
+ // condition: process include if condition evals to true
+ const char *condition = include->Attribute("condition");
+ if (condition)
+ {
+ INFO::InfoPtr conditionID =
+ CServiceBroker::GetGUI()->GetInfoManager().Register(ResolveExpressions(condition));
+ bool value = false;
+
+ if (conditionID)
+ {
+ value = conditionID->Get(INFO::DEFAULT_CONTEXT);
+
+ if (xmlIncludeConditions)
+ xmlIncludeConditions->insert(std::make_pair(conditionID, value));
+ }
+
+ if (!value)
+ {
+ include = include->NextSiblingElement("include");
+ continue;
+ }
+ }
+
+ Params params;
+ std::string tagName;
+
+ // determine which form of include call we have
+ const char *name = include->Attribute("content");
+ if (name)
+ {
+ // <include content="MyControl" />
+ // or
+ // <include content="MyControl">
+ // <param name="posx" value="225" />
+ // <param name="posy">150</param>
+ // ...
+ // </include>
+ tagName = name;
+ GetParameters(include, "value", params);
+ }
+ else
+ {
+ const TiXmlNode *child = include->FirstChild();
+ if (child && child->Type() == TiXmlNode::TINYXML_TEXT)
+ {
+ // <include>MyControl</include>
+ // old-style includes for backward compatibility
+ tagName = child->ValueStr();
+ }
+ }
+
+ // check, whether the include exists and therefore should be replaced by its definition
+ auto it = m_includes.find(tagName);
+ if (it != m_includes.end())
+ {
+ const TiXmlElement *includeDefinition = &it->second.first;
+
+ // combine passed include parameters with their default values into a single list (no overwrites)
+ const Params& defaultParams = it->second.second;
+ params.insert(defaultParams.begin(), defaultParams.end());
+
+ // process include definition
+ const TiXmlElement *includeDefinitionChild = includeDefinition->FirstChildElement();
+ while (includeDefinitionChild)
+ {
+ // insert before <include> element to keep order of occurrence in xml file
+ TiXmlElement *insertedNode = static_cast<TiXmlElement*>(node->InsertBeforeChild(include, *includeDefinitionChild));
+
+ // process nested
+ InsertNested(node, include, insertedNode);
+
+ // resolve parameters even if parameter list is empty (to remove param references)
+ ResolveParametersForNode(insertedNode, params);
+
+ includeDefinitionChild = includeDefinitionChild->NextSiblingElement();
+ }
+
+ // remove the include element itself
+ node->RemoveChild(include);
+
+ include = node->FirstChildElement("include");
+ }
+ else
+ { // invalid include
+ CLog::Log(LOGWARNING, "Skin has invalid include: {}", tagName);
+ include = include->NextSiblingElement("include");
+ }
+ }
+}
+
+void CGUIIncludes::InsertNested(TiXmlElement *controls, TiXmlElement *include, TiXmlElement *node)
+{
+ TiXmlElement *target;
+ TiXmlElement *nested;
+
+ if (node->ValueStr() == "nested")
+ {
+ nested = node;
+ target = controls;
+ }
+ else
+ {
+ nested = node->FirstChildElement("nested");
+ target = node;
+ }
+
+ if (nested)
+ {
+ // copy all child elements except param elements
+ const TiXmlElement *child = include->FirstChildElement();
+ while (child)
+ {
+ if (child->ValueStr() != "param")
+ {
+ // insert before <nested> element to keep order of occurrence in xml file
+ target->InsertBeforeChild(nested, *child);
+ }
+ child = child->NextSiblingElement();
+ }
+ target->RemoveChild(nested);
+ }
+
+}
+
+bool CGUIIncludes::GetParameters(const TiXmlElement *include, const char *valueAttribute, Params& params)
+{
+ bool foundAny = false;
+
+ // collect parameters from include tag
+ // <include content="MyControl">
+ // <param name="posx" value="225" /> <!-- comments and other tags are ignored here -->
+ // <param name="posy">150</param>
+ // ...
+ // </include>
+
+ if (include)
+ {
+ const TiXmlElement *param = include->FirstChildElement("param");
+ foundAny = param != NULL; // doesn't matter if param isn't entirely valid
+ while (param)
+ {
+ std::string paramName = XMLUtils::GetAttribute(param, "name");
+ if (!paramName.empty())
+ {
+ std::string paramValue;
+
+ // <param name="posx" value="120" />
+ const char *value = param->Attribute(valueAttribute); // try attribute first
+ if (value)
+ paramValue = value;
+ else
+ {
+ // <param name="posx">120</param>
+ const TiXmlNode *child = param->FirstChild();
+ if (child && child->Type() == TiXmlNode::TINYXML_TEXT)
+ paramValue = child->ValueStr(); // and then tag value
+ }
+
+ params.insert({ paramName, paramValue }); // no overwrites
+ }
+ param = param->NextSiblingElement("param");
+ }
+ }
+
+ return foundAny;
+}
+
+void CGUIIncludes::ResolveParametersForNode(TiXmlElement *node, const Params& params)
+{
+ if (!node)
+ return;
+ std::string newValue;
+ // run through this element's attributes, resolving any parameters
+ TiXmlAttribute *attribute = node->FirstAttribute();
+ while (attribute)
+ {
+ ResolveParamsResult result = ResolveParameters(attribute->ValueStr(), newValue, params);
+ if (result == SINGLE_UNDEFINED_PARAM_RESOLVED && strcmp(node->Value(), "param") == 0 &&
+ strcmp(attribute->Name(), "value") == 0 && node->Parent() && strcmp(node->Parent()->Value(), "include") == 0)
+ {
+ // special case: passing <param name="someName" value="$PARAM[undefinedParam]" /> to the nested include
+ // this usually happens when trying to forward a missing parameter from the enclosing include to the nested include
+ // problem: since 'undefinedParam' is not defined, it expands to <param name="someName" value="" /> and overrides any default value set with <param name="someName" default="someValue" /> in the nested include
+ // to prevent this, we'll completely remove this parameter from the nested include call so that the default value can be correctly picked up later
+ node->Parent()->RemoveChild(node);
+ return;
+ }
+ else if (result != NO_PARAMS_FOUND)
+ attribute->SetValue(newValue);
+ attribute = attribute->Next();
+ }
+ // run through this element's value and children, resolving any parameters
+ TiXmlNode *child = node->FirstChild();
+ if (child)
+ {
+ if (child->Type() == TiXmlNode::TINYXML_TEXT)
+ {
+ ResolveParamsResult result = ResolveParameters(child->ValueStr(), newValue, params);
+ if (result == SINGLE_UNDEFINED_PARAM_RESOLVED && strcmp(node->Value(), "param") == 0 &&
+ node->Parent() && strcmp(node->Parent()->Value(), "include") == 0)
+ {
+ // special case: passing <param name="someName">$PARAM[undefinedParam]</param> to the nested include
+ // we'll remove the offending param tag same as above
+ node->Parent()->RemoveChild(node);
+ }
+ else if (result != NO_PARAMS_FOUND)
+ child->SetValue(newValue);
+ }
+ else if (child->Type() == TiXmlNode::TINYXML_ELEMENT ||
+ child->Type() == TiXmlNode::TINYXML_COMMENT)
+ {
+ do
+ {
+ // save next as current child might be removed from the tree
+ TiXmlElement* next = child->NextSiblingElement();
+
+ if (child->Type() == TiXmlNode::TINYXML_ELEMENT)
+ ResolveParametersForNode(static_cast<TiXmlElement*>(child), params);
+
+ child = next;
+ }
+ while (child);
+ }
+ }
+}
+
+class ParamReplacer
+{
+ const std::map<std::string, std::string>& m_params;
+ // keep some stats so that we know exactly what's been resolved
+ int m_numTotalParams;
+ int m_numUndefinedParams;
+public:
+ explicit ParamReplacer(const std::map<std::string, std::string>& params)
+ : m_params(params), m_numTotalParams(0), m_numUndefinedParams(0) {}
+ int GetNumTotalParams() const { return m_numTotalParams; }
+ int GetNumDefinedParams() const { return m_numTotalParams - m_numUndefinedParams; }
+ int GetNumUndefinedParams() const { return m_numUndefinedParams; }
+
+ std::string operator()(const std::string &paramName)
+ {
+ m_numTotalParams++;
+ std::map<std::string, std::string>::const_iterator it = m_params.find(paramName);
+ if (it != m_params.end())
+ return it->second;
+ m_numUndefinedParams++;
+ return "";
+ }
+};
+
+CGUIIncludes::ResolveParamsResult CGUIIncludes::ResolveParameters(const std::string& strInput, std::string& strOutput, const Params& params)
+{
+ ParamReplacer paramReplacer(params);
+ if (GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(strInput, "PARAM", std::ref(paramReplacer), strOutput))
+ // detect special input values of the form "$PARAM[undefinedParam]" (with no extra characters around)
+ return paramReplacer.GetNumUndefinedParams() == 1 && paramReplacer.GetNumTotalParams() == 1 && strOutput.empty() ? SINGLE_UNDEFINED_PARAM_RESOLVED : PARAMS_RESOLVED;
+ return NO_PARAMS_FOUND;
+}
+
+std::string CGUIIncludes::ResolveConstant(const std::string &constant) const
+{
+ std::vector<std::string> values = StringUtils::Split(constant, ",");
+ for (auto& i : values)
+ {
+ std::map<std::string, std::string>::const_iterator it = m_constants.find(i);
+ if (it != m_constants.end())
+ i = it->second;
+ }
+ return StringUtils::Join(values, ",");
+}
+
+std::string CGUIIncludes::ResolveExpressions(const std::string &expression) const
+{
+ std::string work(expression);
+ GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(work, "EXP", [&](const std::string &str) -> std::string {
+ std::map<std::string, std::string>::const_iterator it = m_expressions.find(str);
+ if (it != m_expressions.end())
+ return it->second;
+ return "";
+ });
+
+ return work;
+}
+
+const INFO::CSkinVariableString* CGUIIncludes::CreateSkinVariable(const std::string& name, int context)
+{
+ std::map<std::string, TiXmlElement>::const_iterator it = m_skinvariables.find(name);
+ if (it != m_skinvariables.end())
+ return INFO::CSkinVariable::CreateFromXML(it->second, context);
+ return NULL;
+}
diff --git a/xbmc/guilib/GUIIncludes.h b/xbmc/guilib/GUIIncludes.h
new file mode 100644
index 0000000..cc11aa3
--- /dev/null
+++ b/xbmc/guilib/GUIIncludes.h
@@ -0,0 +1,133 @@
+/*
+ * 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 "interfaces/info/InfoBool.h"
+
+#include <map>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+// forward definitions
+class TiXmlElement;
+namespace INFO
+{
+ class CSkinVariableString;
+}
+
+class CGUIIncludes
+{
+public:
+ CGUIIncludes();
+ ~CGUIIncludes();
+
+ /*!
+ \brief Clear all include components (defaults, constants, variables, expressions and includes)
+ */
+ void Clear();
+
+ /*!
+ \brief Load all include components(defaults, constants, variables, expressions and includes)
+ from the main entrypoint \code{file}. Flattens nested expressions and expressions in variable
+ conditions after loading all other included files.
+
+ \param file the file to load
+ */
+ void Load(const std::string &file);
+
+ /*!
+ \brief Resolve all include components (defaults, constants, variables, expressions and includes)
+ for the given \code{node}. Place the conditions specified for <include> elements in \code{includeConditions}.
+
+ \param node the node from where we start to resolve the include components
+ \param includeConditions a map that holds the conditions for resolved includes
+ */
+ void Resolve(TiXmlElement *node, std::map<INFO::InfoPtr, bool>* includeConditions = NULL);
+
+ /*!
+ \brief Create a skin variable for the given \code{name} within the given \code{context}.
+
+ \param name the name of the skin variable
+ \param context the context where the variable is created in
+ \return skin variable
+ */
+ const INFO::CSkinVariableString* CreateSkinVariable(const std::string& name, int context);
+
+private:
+ enum ResolveParamsResult
+ {
+ NO_PARAMS_FOUND,
+ PARAMS_RESOLVED,
+ SINGLE_UNDEFINED_PARAM_RESOLVED
+ };
+
+ /*!
+ \brief Load all include components (defaults, constants, variables, expressions and includes)
+ from the given \code{file}.
+
+ \param file the file to load
+ \return true if the file was loaded otherwise false
+ */
+ bool Load_Internal(const std::string &file);
+
+ bool HasLoaded(const std::string &file) const;
+
+ void LoadDefaults(const TiXmlElement *node);
+ void LoadIncludes(const TiXmlElement *node);
+ void LoadVariables(const TiXmlElement *node);
+ void LoadConstants(const TiXmlElement *node);
+ void LoadExpressions(const TiXmlElement *node);
+
+ /*!
+ \brief Resolve all expressions containing other expressions to a single evaluatable expression.
+ */
+ void FlattenExpressions();
+
+ /*!
+ \brief Expand any expressions nested in this expression.
+
+ \param expression the expression to flatten
+ \param resolved list of already evaluated expression names, to avoid expanding circular references
+ */
+ void FlattenExpression(std::string &expression, const std::vector<std::string> &resolved);
+
+ /*!
+ \brief Resolve all variable conditions containing expressions to a single evaluatable condition.
+ */
+ void FlattenSkinVariableConditions();
+
+ void SetDefaults(TiXmlElement *node);
+ void ResolveIncludes(TiXmlElement *node, std::map<INFO::InfoPtr, bool>* xmlIncludeConditions = NULL);
+ void ResolveConstants(TiXmlElement *node);
+ void ResolveExpressions(TiXmlElement *node);
+
+ typedef std::map<std::string, std::string> Params;
+ static void InsertNested(TiXmlElement* controls, TiXmlElement* include, TiXmlElement* node);
+ static bool GetParameters(const TiXmlElement *include, const char *valueAttribute, Params& params);
+ static void ResolveParametersForNode(TiXmlElement *node, const Params& params);
+ static ResolveParamsResult ResolveParameters(const std::string& strInput, std::string& strOutput, const Params& params);
+
+ std::string ResolveConstant(const std::string &constant) const;
+ std::string ResolveExpressions(const std::string &expression) const;
+
+ std::vector<std::string> m_files;
+ std::map<std::string, std::pair<TiXmlElement, Params>> m_includes;
+ std::map<std::string, TiXmlElement> m_defaults;
+ std::map<std::string, TiXmlElement> m_skinvariables;
+ std::map<std::string, std::string> m_constants;
+ std::map<std::string, std::string> m_expressions;
+
+ std::set<std::string> m_constantAttributes;
+ std::set<std::string> m_constantNodes;
+
+ std::set<std::string> m_expressionAttributes;
+ std::set<std::string> m_expressionNodes;
+};
diff --git a/xbmc/guilib/GUIKeyboard.h b/xbmc/guilib/GUIKeyboard.h
new file mode 100644
index 0000000..a6ca5ad
--- /dev/null
+++ b/xbmc/guilib/GUIKeyboard.h
@@ -0,0 +1,84 @@
+/*
+ * 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/Timer.h"
+
+#include <string>
+
+class CGUIKeyboard;
+enum FILTERING { FILTERING_NONE = 0, FILTERING_CURRENT, FILTERING_SEARCH };
+typedef void (*char_callback_t) (CGUIKeyboard *ref, const std::string &typedString);
+
+#ifdef TARGET_WINDOWS // disable 4355: 'this' used in base member initializer list
+#pragma warning(push)
+#pragma warning(disable: 4355)
+#endif
+
+class CGUIKeyboard : public ITimerCallback
+{
+ public:
+ CGUIKeyboard() : m_idleTimer(this) {}
+ ~CGUIKeyboard() override = default;
+
+ // entrypoint
+ /*!
+ * \brief each native keyboard needs to implement this function with the following behaviour:
+ *
+ * \param pCallback implementation should call this on each keypress with the current whole string
+ * \param initialString implementation should show that initialstring
+ * \param typedstring returns the typed string after close if return is true
+ * \param heading implementation should show a heading (e.x. "search for a movie")
+ * \param bHiddenInput if true the implementation should obfuscate the user input (e.x. by printing "*" for each char)
+ *
+ * \return - true if typedstring is valid and user has confirmed input - false if typedstring is undefined and user canceled the input
+ *
+ */
+ virtual bool ShowAndGetInput(char_callback_t pCallback,
+ const std::string &initialString,
+ std::string &typedString,
+ const std::string &heading,
+ bool bHiddenInput = false) = 0;
+
+ /*!
+ *\brief This call should cancel a currently shown keyboard dialog. The implementation should
+ * return false from the modal ShowAndGetInput once anyone calls this method.
+ */
+ virtual void Cancel() = 0;
+
+ virtual int GetWindowId() const {return 0;}
+
+ // CTimer Interface for autoclose
+ void OnTimeout() override
+ {
+ Cancel();
+ }
+
+ // helpers for autoclose function
+ void startAutoCloseTimer(unsigned int autoCloseMs)
+ {
+ if ( autoCloseMs > 0 )
+ m_idleTimer.Start(std::chrono::milliseconds(autoCloseMs), false);
+ }
+
+ void resetAutoCloseTimer()
+ {
+ if (m_idleTimer.IsRunning())
+ m_idleTimer.Restart();
+ }
+
+ virtual bool SetTextToKeyboard(const std::string &text, bool closeKeyboard = false) { return false; }
+
+ private:
+ CTimer m_idleTimer;
+};
+
+#ifdef TARGET_WINDOWS
+#pragma warning(pop)
+#endif
diff --git a/xbmc/guilib/GUIKeyboardFactory.cpp b/xbmc/guilib/GUIKeyboardFactory.cpp
new file mode 100644
index 0000000..fbf7a22
--- /dev/null
+++ b/xbmc/guilib/GUIKeyboardFactory.cpp
@@ -0,0 +1,239 @@
+/*
+ * 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 "ServiceBroker.h"
+#include "GUIComponent.h"
+#include "messaging/ApplicationMessenger.h"
+#include "LocalizeStrings.h"
+#include "GUIKeyboardFactory.h"
+#include "GUIUserMessages.h"
+#include "GUIWindowManager.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include "dialogs/GUIDialogKeyboardGeneric.h"
+#if defined(TARGET_DARWIN_EMBEDDED)
+#include "dialogs/GUIDialogKeyboardTouch.h"
+
+#include "platform/darwin/ios-common/DarwinEmbedKeyboard.h"
+#endif
+
+using namespace KODI::MESSAGING;
+using KODI::UTILITY::CDigest;
+
+CGUIKeyboard *CGUIKeyboardFactory::g_activeKeyboard = NULL;
+FILTERING CGUIKeyboardFactory::m_filtering = FILTERING_NONE;
+
+CGUIKeyboardFactory::CGUIKeyboardFactory(void) = default;
+
+CGUIKeyboardFactory::~CGUIKeyboardFactory(void) = default;
+
+void CGUIKeyboardFactory::keyTypedCB(CGUIKeyboard *ref, const std::string &typedString)
+{
+ if(ref)
+ {
+ // send our search message in safe way (only the active window needs it)
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, ref->GetWindowId(), 0);
+ switch(m_filtering)
+ {
+ case FILTERING_SEARCH:
+ message.SetParam1(GUI_MSG_SEARCH_UPDATE);
+ message.SetStringParam(typedString);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(
+ message, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ break;
+ case FILTERING_CURRENT:
+ message.SetParam1(GUI_MSG_FILTER_ITEMS);
+ message.SetStringParam(typedString);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(message);
+ break;
+ case FILTERING_NONE:
+ break;
+ }
+ ref->resetAutoCloseTimer();
+ }
+}
+
+bool CGUIKeyboardFactory::SendTextToActiveKeyboard(const std::string &aTextString, bool closeKeyboard /* = false */)
+{
+ if (!g_activeKeyboard)
+ return false;
+ return g_activeKeyboard->SetTextToKeyboard(aTextString, closeKeyboard);
+}
+
+
+// Show keyboard with initial value (aTextString) and replace with result string.
+// Returns: true - successful display and input (empty result may return true or false depending on parameter)
+// false - unsuccessful display of the keyboard or cancelled editing
+bool CGUIKeyboardFactory::ShowAndGetInput(std::string& aTextString,
+ const CVariant& heading,
+ bool allowEmptyResult,
+ bool hiddenInput /* = false */,
+ unsigned int autoCloseMs /* = 0 */)
+{
+ bool confirmed = false;
+ //heading can be a string or a localization id
+ std::string headingStr;
+ if (heading.isString())
+ headingStr = heading.asString();
+ else if (heading.isInteger() && heading.asInteger())
+ headingStr = g_localizeStrings.Get((uint32_t)heading.asInteger());
+
+ bool useKodiKeyboard = true;
+#if defined(TARGET_DARWIN_EMBEDDED)
+#if defined(TARGET_DARWIN_TVOS)
+ useKodiKeyboard = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_INPUT_TVOSUSEKODIKEYBOARD);
+#else
+ useKodiKeyboard = CDarwinEmbedKeyboard::hasExternalKeyboard();
+#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;
+ kb->startAutoCloseTimer(autoCloseMs);
+ confirmed = kb->ShowAndGetInput(keyTypedCB, aTextString, aTextString, headingStr, hiddenInput);
+ g_activeKeyboard = NULL;
+ }
+
+ if (confirmed)
+ {
+ if (!allowEmptyResult && aTextString.empty())
+ confirmed = false;
+ }
+
+ return confirmed;
+}
+
+bool CGUIKeyboardFactory::ShowAndGetInput(std::string& aTextString, bool allowEmptyResult, unsigned int autoCloseMs /* = 0 */)
+{
+ return ShowAndGetInput(aTextString, CVariant{""}, allowEmptyResult, false, autoCloseMs);
+}
+
+// Shows keyboard and prompts for a password.
+// Differs from ShowAndVerifyNewPassword() in that no second verification is necessary.
+bool CGUIKeyboardFactory::ShowAndGetNewPassword(std::string& newPassword,
+ const CVariant& heading,
+ bool allowEmpty,
+ unsigned int autoCloseMs /* = 0 */)
+{
+ return ShowAndGetInput(newPassword, heading, allowEmpty, true, autoCloseMs);
+}
+
+// Shows keyboard and prompts for a password.
+// Differs from ShowAndVerifyNewPassword() in that no second verification is necessary.
+bool CGUIKeyboardFactory::ShowAndGetNewPassword(std::string& newPassword, unsigned int autoCloseMs /* = 0 */)
+{
+ return ShowAndGetNewPassword(newPassword, 12340, false, autoCloseMs);
+}
+
+bool CGUIKeyboardFactory::ShowAndGetFilter(std::string &filter, bool searching, unsigned int autoCloseMs /* = 0 */)
+{
+ m_filtering = searching ? FILTERING_SEARCH : FILTERING_CURRENT;
+ bool ret = ShowAndGetInput(filter, searching ? 16017 : 16028, true, false, autoCloseMs);
+ m_filtering = FILTERING_NONE;
+ return ret;
+}
+
+
+// \brief Show keyboard twice to get and confirm a user-entered password string.
+// \param newPassword Overwritten with user input if return=true.
+// \param heading Heading to display
+// \param allowEmpty Whether a blank password is valid or not.
+// \return true if successful display and user input entry/re-entry. false if unsuccessful display, no user input, or canceled editing.
+bool CGUIKeyboardFactory::ShowAndVerifyNewPassword(std::string& newPassword,
+ const CVariant& heading,
+ bool allowEmpty,
+ unsigned int autoCloseMs /* = 0 */)
+{
+ // Prompt user for password input
+ std::string userInput;
+ if (!ShowAndGetInput(userInput, heading, allowEmpty, true, autoCloseMs))
+ { // user cancelled, or invalid input
+ return false;
+ }
+ // success - verify the password
+ std::string checkInput;
+ if (!ShowAndGetInput(checkInput, 12341, allowEmpty, true, autoCloseMs))
+ { // user cancelled, or invalid input
+ return false;
+ }
+ // check the password
+ if (checkInput == userInput)
+ {
+ newPassword = CDigest::Calculate(CDigest::Type::MD5, userInput);
+ return true;
+ }
+ HELPERS::ShowOKDialogText(CVariant{12341}, CVariant{12344});
+ return false;
+}
+
+// \brief Show keyboard twice to get and confirm a user-entered password string.
+// \param strNewPassword Overwritten with user input if return=true.
+// \return true if successful display and user input entry/re-entry. false if unsuccessful display, no user input, or canceled editing.
+bool CGUIKeyboardFactory::ShowAndVerifyNewPassword(std::string& newPassword, unsigned int autoCloseMs /* = 0 */)
+{
+ const std::string& heading = g_localizeStrings.Get(12340);
+ return ShowAndVerifyNewPassword(newPassword, heading, false, autoCloseMs);
+}
+
+// \brief Show keyboard and verify user input against strPassword.
+// \param strPassword Value to compare against user input.
+// \param dlgHeading String shown on dialog title. Converts to localized string if contains a positive integer.
+// \param iRetries If greater than 0, shows "Incorrect password, %d retries left" on dialog line 2, else line 2 is blank.
+// \return 0 if successful display and user input. 1 if unsuccessful input. -1 if no user input or canceled editing.
+int CGUIKeyboardFactory::ShowAndVerifyPassword(std::string& strPassword, const std::string& strHeading, int iRetries, unsigned int autoCloseMs /* = 0 */)
+{
+ std::string strHeadingTemp;
+ if (1 > iRetries && strHeading.size())
+ strHeadingTemp = strHeading;
+ else
+ strHeadingTemp =
+ StringUtils::Format("{} - {} {}", g_localizeStrings.Get(12326),
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MASTERLOCK_MAXRETRIES) -
+ iRetries,
+ g_localizeStrings.Get(12343));
+
+ std::string strUserInput;
+ //! @todo GUI Setting to enable disable this feature y/n?
+ if (!ShowAndGetInput(strUserInput, strHeadingTemp, false, true, autoCloseMs)) //bool hiddenInput = false/true ?
+ return -1; // user canceled out
+
+ if (!strPassword.empty())
+ {
+ std::string md5pword2 = CDigest::Calculate(CDigest::Type::MD5, strUserInput);
+ if (StringUtils::EqualsNoCase(strPassword, md5pword2))
+ return 0; // user entered correct password
+ else return 1; // user must have entered an incorrect password
+ }
+ else
+ {
+ if (!strUserInput.empty())
+ {
+ strPassword = CDigest::Calculate(CDigest::Type::MD5, strUserInput);
+ return 0; // user entered correct password
+ }
+ else return 1;
+ }
+}
+
diff --git a/xbmc/guilib/GUIKeyboardFactory.h b/xbmc/guilib/GUIKeyboardFactory.h
new file mode 100644
index 0000000..04272af
--- /dev/null
+++ b/xbmc/guilib/GUIKeyboardFactory.h
@@ -0,0 +1,50 @@
+/*
+ * 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 "GUIKeyboard.h"
+
+#include <string>
+
+class CVariant;
+
+class CGUIKeyboardFactory
+{
+
+ public:
+ CGUIKeyboardFactory(void);
+ virtual ~CGUIKeyboardFactory(void);
+
+ static bool ShowAndGetInput(std::string& aTextString, bool allowEmptyResult, unsigned int autoCloseMs = 0);
+ static bool ShowAndGetInput(std::string& aTextString,
+ const CVariant& heading,
+ bool allowEmptyResult,
+ bool hiddenInput = false,
+ unsigned int autoCloseMs = 0);
+ static bool ShowAndGetNewPassword(std::string& strNewPassword, unsigned int autoCloseMs = 0);
+ static bool ShowAndGetNewPassword(std::string& newPassword,
+ const CVariant& heading,
+ bool allowEmpty,
+ unsigned int autoCloseMs = 0);
+ static bool ShowAndVerifyNewPassword(std::string& strNewPassword, unsigned int autoCloseMs = 0);
+ static bool ShowAndVerifyNewPassword(std::string& newPassword,
+ const CVariant& heading,
+ bool allowEmpty,
+ unsigned int autoCloseMs = 0);
+ static int ShowAndVerifyPassword(std::string& strPassword, const std::string& strHeading, int iRetries, unsigned int autoCloseMs = 0);
+ static bool ShowAndGetFilter(std::string& aTextString, bool searching, unsigned int autoCloseMs = 0);
+
+ static bool SendTextToActiveKeyboard(const std::string &aTextString, bool closeKeyboard = false);
+
+ static bool isKeyboardActivated() { return g_activeKeyboard != NULL; }
+ private:
+ static CGUIKeyboard *g_activeKeyboard;
+ static FILTERING m_filtering;
+ static void keyTypedCB(CGUIKeyboard *ref, const std::string &typedString);
+};
diff --git a/xbmc/guilib/GUILabel.cpp b/xbmc/guilib/GUILabel.cpp
new file mode 100644
index 0000000..2687862
--- /dev/null
+++ b/xbmc/guilib/GUILabel.cpp
@@ -0,0 +1,250 @@
+/*
+ * 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 "GUILabel.h"
+
+#include <limits>
+
+CGUILabel::CGUILabel(float posX, float posY, float width, float height, const CLabelInfo& labelInfo, CGUILabel::OVER_FLOW overflow)
+ : m_label(labelInfo)
+ , m_textLayout(labelInfo.font, overflow == OVER_FLOW_WRAP, height)
+ , m_scrolling(overflow == OVER_FLOW_SCROLL)
+ , m_overflowType(overflow)
+ , m_scrollInfo(50, 0, labelInfo.scrollSpeed, labelInfo.scrollSuffix)
+ , m_renderRect()
+ , m_maxRect(posX, posY, posX + width, posY + height)
+ , m_invalid(true)
+ , m_color(COLOR_TEXT)
+{
+}
+
+CGUILabel::CGUILabel(const CGUILabel& label)
+ : m_label(label.m_label),
+ m_textLayout(label.m_textLayout),
+ m_scrolling(label.m_scrolling),
+ m_overflowType(label.m_overflowType),
+ m_scrollInfo(label.m_scrollInfo),
+ m_renderRect(label.m_renderRect),
+ m_maxRect(label.m_maxRect),
+ m_invalid(label.m_invalid),
+ m_color(label.m_color),
+ m_maxScrollLoops(label.m_maxScrollLoops)
+{
+}
+
+bool CGUILabel::SetScrolling(bool scrolling)
+{
+ bool changed = m_scrolling != scrolling;
+
+ m_scrolling = scrolling;
+ if (changed)
+ m_scrollInfo.Reset();
+
+ return changed;
+}
+
+bool CGUILabel::SetOverflow(OVER_FLOW overflow)
+{
+ bool changed = m_overflowType != overflow;
+
+ m_overflowType = overflow;
+
+ return changed;
+}
+
+bool CGUILabel::SetColor(CGUILabel::COLOR color)
+{
+ bool changed = m_color != color;
+
+ m_color = color;
+
+ return changed;
+}
+
+UTILS::COLOR::Color CGUILabel::GetColor() const
+{
+ switch (m_color)
+ {
+ case COLOR_SELECTED:
+ return m_label.selectedColor;
+ case COLOR_DISABLED:
+ return m_label.disabledColor;
+ case COLOR_FOCUSED:
+ return m_label.focusedColor ? m_label.focusedColor : m_label.textColor;
+ case COLOR_INVALID:
+ return m_label.invalidColor ? m_label.invalidColor : m_label.textColor;
+ default:
+ break;
+ }
+ return m_label.textColor;
+}
+
+bool CGUILabel::Process(unsigned int currentTime)
+{
+ //! @todo Add the correct processing
+
+ bool overFlows = (m_renderRect.Width() + 0.5f < m_textLayout.GetTextWidth()); // 0.5f to deal with floating point rounding issues
+ bool renderSolid = (m_color == COLOR_DISABLED);
+
+ if (overFlows && m_scrolling && !renderSolid)
+ {
+ if (m_maxScrollLoops < m_scrollInfo.m_loopCount)
+ SetScrolling(false);
+ else
+ return m_textLayout.UpdateScrollinfo(m_scrollInfo);
+ }
+
+ return false;
+}
+
+void CGUILabel::Render()
+{
+ UTILS::COLOR::Color color = GetColor();
+ bool renderSolid = (m_color == COLOR_DISABLED);
+ bool overFlows = (m_renderRect.Width() + 0.5f < m_textLayout.GetTextWidth()); // 0.5f to deal with floating point rounding issues
+ if (overFlows && m_scrolling && !renderSolid)
+ m_textLayout.RenderScrolling(m_renderRect.x1, m_renderRect.y1, m_label.angle, color, m_label.shadowColor, 0, m_renderRect.Width(), m_scrollInfo);
+ else
+ {
+ float posX = m_renderRect.x1;
+ float posY = m_renderRect.y1;
+ uint32_t align = 0;
+ if (!overFlows)
+ { // hack for right and centered multiline text, as GUITextLayout::Render() treats posX as the right hand
+ // or center edge of the text (see GUIFontTTF::DrawTextInternal), and this has already been taken care of
+ // in UpdateRenderRect(), but we wish to still pass the horizontal alignment info through (so that multiline text
+ // is aligned correctly), so we must undo the UpdateRenderRect() changes for horizontal alignment.
+ if (m_label.align & XBFONT_RIGHT)
+ posX += m_renderRect.Width();
+ else if (m_label.align & XBFONT_CENTER_X)
+ posX += m_renderRect.Width() * 0.5f;
+ if (m_label.align & XBFONT_CENTER_Y) // need to pass a centered Y so that <angle> will rotate around the correct point.
+ posY += m_renderRect.Height() * 0.5f;
+ align = m_label.align;
+ }
+ else
+ align |= XBFONT_TRUNCATED;
+ m_textLayout.Render(posX, posY, m_label.angle, color, m_label.shadowColor, align, m_overflowType == OVER_FLOW_CLIP ? m_textLayout.GetTextWidth() : m_renderRect.Width(), renderSolid);
+ }
+}
+
+void CGUILabel::SetInvalid()
+{
+ m_invalid = true;
+}
+
+bool CGUILabel::UpdateColors()
+{
+ return m_label.UpdateColors();
+}
+
+bool CGUILabel::SetMaxRect(float x, float y, float w, float h)
+{
+ CRect oldRect = m_maxRect;
+
+ m_maxRect.SetRect(x, y, x + w, y + h);
+ UpdateRenderRect();
+
+ return oldRect != m_maxRect;
+}
+
+bool CGUILabel::SetAlign(uint32_t align)
+{
+ bool changed = m_label.align != align;
+
+ m_label.align = align;
+ UpdateRenderRect();
+
+ return changed;
+}
+
+bool CGUILabel::SetStyledText(const vecText& text, const std::vector<UTILS::COLOR::Color>& colors)
+{
+ m_textLayout.UpdateStyled(text, colors, m_maxRect.Width());
+ m_invalid = false;
+ return true;
+}
+
+bool CGUILabel::SetText(const std::string &label)
+{
+ if (m_textLayout.Update(label, m_maxRect.Width(), m_invalid))
+ { // needed an update - reset scrolling and update our text layout
+ m_scrollInfo.Reset();
+ UpdateRenderRect();
+ m_invalid = false;
+ return true;
+ }
+ else
+ return false;
+}
+
+bool CGUILabel::SetTextW(const std::wstring &label)
+{
+ if (m_textLayout.UpdateW(label, m_maxRect.Width(), m_invalid))
+ {
+ m_scrollInfo.Reset();
+ UpdateRenderRect();
+ m_invalid = false;
+ return true;
+ }
+ else
+ return false;
+}
+
+void CGUILabel::UpdateRenderRect()
+{
+ // recalculate our text layout
+ float width, height;
+ m_textLayout.GetTextExtent(width, height);
+ width = std::min(width, GetMaxWidth());
+ if (m_label.align & XBFONT_CENTER_Y)
+ m_renderRect.y1 = m_maxRect.y1 + (m_maxRect.Height() - height) * 0.5f;
+ else
+ m_renderRect.y1 = m_maxRect.y1 + m_label.offsetY;
+ if (m_label.align & XBFONT_RIGHT)
+ m_renderRect.x1 = m_maxRect.x2 - width - m_label.offsetX;
+ else if (m_label.align & XBFONT_CENTER_X)
+ m_renderRect.x1 = m_maxRect.x1 + (m_maxRect.Width() - width) * 0.5f;
+ else
+ m_renderRect.x1 = m_maxRect.x1 + m_label.offsetX;
+ m_renderRect.x2 = m_renderRect.x1 + width;
+ m_renderRect.y2 = m_renderRect.y1 + height;
+}
+
+float CGUILabel::GetMaxWidth() const
+{
+ if (m_label.width) return m_label.width;
+ return m_maxRect.Width() - 2*m_label.offsetX;
+}
+
+bool CGUILabel::CheckAndCorrectOverlap(CGUILabel &label1, CGUILabel &label2)
+{
+ CRect rect(label1.m_renderRect);
+ if (rect.Intersect(label2.m_renderRect).IsEmpty())
+ return false; // nothing to do (though it could potentially encroach on the min_space requirement)
+
+ // overlap vertically and horizontally - check alignment
+ CGUILabel &left = label1.m_renderRect.x1 <= label2.m_renderRect.x1 ? label1 : label2;
+ CGUILabel &right = label1.m_renderRect.x1 <= label2.m_renderRect.x1 ? label2 : label1;
+ if ((left.m_label.align & 3) == 0 && right.m_label.align & XBFONT_RIGHT)
+ {
+ static const float min_space = 10;
+ float chopPoint = (left.m_maxRect.x1 + left.GetMaxWidth() + right.m_maxRect.x2 - right.GetMaxWidth()) * 0.5f;
+ // [1 [2...[2 1].|..........1] 2]
+ // [1 [2.....[2 | 1]..1] 2]
+ // [1 [2..........|.[2 1]..1] 2]
+ if (right.m_renderRect.x1 > chopPoint)
+ chopPoint = right.m_renderRect.x1 - min_space;
+ else if (left.m_renderRect.x2 < chopPoint)
+ chopPoint = left.m_renderRect.x2 + min_space;
+ left.m_renderRect.x2 = chopPoint - min_space;
+ right.m_renderRect.x1 = chopPoint + min_space;
+ return true;
+ }
+ return false;
+}
diff --git a/xbmc/guilib/GUILabel.h b/xbmc/guilib/GUILabel.h
new file mode 100644
index 0000000..7344667
--- /dev/null
+++ b/xbmc/guilib/GUILabel.h
@@ -0,0 +1,240 @@
+/*
+ * 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
+
+/*!
+\file GUILabel.h
+\brief
+*/
+
+#include "GUIFont.h"
+#include "GUITextLayout.h"
+#include "guiinfo/GUIInfoColor.h"
+#include "utils/ColorUtils.h"
+#include "utils/Geometry.h"
+
+class CLabelInfo
+{
+public:
+ CLabelInfo():
+ scrollSuffix(" | ")
+ {
+ font = NULL;
+ align = XBFONT_LEFT;
+ offsetX = offsetY = 0;
+ width = 0;
+ angle = 0;
+ scrollSpeed = CScrollInfo::defaultSpeed;
+ };
+ bool UpdateColors()
+ {
+ bool changed = false;
+
+ changed |= textColor.Update();
+ changed |= shadowColor.Update();
+ changed |= selectedColor.Update();
+ changed |= disabledColor.Update();
+ changed |= focusedColor.Update();
+ changed |= invalidColor.Update();
+
+ return changed;
+ };
+
+ KODI::GUILIB::GUIINFO::CGUIInfoColor textColor;
+ KODI::GUILIB::GUIINFO::CGUIInfoColor shadowColor;
+ KODI::GUILIB::GUIINFO::CGUIInfoColor selectedColor;
+ KODI::GUILIB::GUIINFO::CGUIInfoColor disabledColor;
+ KODI::GUILIB::GUIINFO::CGUIInfoColor focusedColor;
+ KODI::GUILIB::GUIINFO::CGUIInfoColor invalidColor;
+ uint32_t align;
+ float offsetX;
+ float offsetY;
+ float width;
+ float angle;
+ CGUIFont *font;
+ int scrollSpeed;
+ std::string scrollSuffix;
+};
+
+/*!
+ \ingroup controls, labels
+ \brief Class for rendering text labels. Handles alignment and rendering of text within a control.
+ */
+class CGUILabel
+{
+public:
+ /*! \brief allowed color categories for labels, as defined by the skin
+ */
+ enum COLOR { COLOR_TEXT = 0,
+ COLOR_SELECTED,
+ COLOR_FOCUSED,
+ COLOR_DISABLED,
+ COLOR_INVALID };
+
+ /*! \brief allowed overflow handling techniques for labels, as defined by the skin
+ */
+ enum OVER_FLOW { OVER_FLOW_TRUNCATE = 0,
+ OVER_FLOW_SCROLL,
+ OVER_FLOW_WRAP,
+ OVER_FLOW_CLIP };
+
+ CGUILabel(float posX, float posY, float width, float height, const CLabelInfo& labelInfo, OVER_FLOW overflow = OVER_FLOW_TRUNCATE);
+ CGUILabel(const CGUILabel& label);
+
+ virtual ~CGUILabel() = default;
+
+ /*! \brief Process the label
+ \return bool stating if process caused control to change
+ */
+ bool Process(unsigned int currentTime);
+
+ /*! \brief Render the label on screen
+ */
+ void Render();
+
+ /*! \brief Set the maximal extent of the label
+ Sets the maximal size and positioning that the label may render in. Note that `textwidth>` can override
+ this, and `<textoffsetx>` and `<textoffsety>` may also allow the label to be moved outside this rectangle.
+ */
+ bool SetMaxRect(float x, float y, float w, float h);
+
+ bool SetAlign(uint32_t align);
+
+ /*! \brief Set the text to be displayed in the label
+ Updates the label control and recomputes final position and size
+ \param text std::string to set as this labels text
+ \sa SetTextW, SetStyledText
+ */
+ bool SetText(const std::string &label);
+
+ /*! \brief Set the text to be displayed in the label
+ Updates the label control and recomputes final position and size
+ \param text std::wstring to set as this labels text
+ \sa SetText, SetStyledText
+ */
+ bool SetTextW(const std::wstring &label);
+
+ /*! \brief Set styled text to be displayed in the label
+ Updates the label control and recomputes final position and size
+ \param text styled text to set.
+ \param colors colors referenced in the styled text.
+ \sa SetText, SetTextW
+ */
+ bool SetStyledText(const vecText& text, const std::vector<UTILS::COLOR::Color>& colors);
+
+ /*! \brief Set the color to use for the label
+ Sets the color to be used for this label. Takes effect at the next render
+ \param color color to be used for the label
+ */
+ bool SetColor(COLOR color);
+
+ /*! \brief Set the final layout of the current text
+ Overrides the calculated layout of the current text, forcing a particular size and position
+ \param rect CRect containing the extents of the current text
+ \sa GetRenderRect, UpdateRenderRect
+ */
+ void SetRenderRect(const CRect& rect) { m_renderRect = rect; }
+
+ /*! \brief Set whether or not this label control should scroll
+ \param scrolling true if this label should scroll.
+ */
+ bool SetScrolling(bool scrolling);
+
+ /*! \brief Set max. text scroll count
+ */
+ void SetScrollLoopCount(unsigned int loopCount) { m_maxScrollLoops = loopCount; }
+
+ /*! \brief Set how this label should handle overflowing text.
+ \param overflow the overflow type
+ \sa OVER_FLOW
+ */
+ bool SetOverflow(OVER_FLOW overflow);
+
+ /*! \brief Set this label invalid. Forces an update of the control
+ */
+ void SetInvalid();
+
+ /*! \brief Update this labels colors
+ */
+ bool UpdateColors();
+
+ /*! \brief Returns the precalculated final layout of the current text
+ \return CRect containing the extents of the current text
+ \sa SetRenderRect, UpdateRenderRect
+ */
+ const CRect& GetRenderRect() const { return m_renderRect; }
+
+ /*! \brief Returns the precalculated full width of the current text, regardless of layout
+ \return full width of the current text
+ \sa CalcTextWidth
+ */
+ float GetTextWidth() const { return m_textLayout.GetTextWidth(); }
+
+ /*! \brief Returns the maximal width that this label can render into
+ \return Maximal width that this label can render into. Note that this may differ from the
+ amount given in SetMaxRect as offsets and text width overrides have been taken into account.
+ \sa SetMaxRect
+ */
+ float GetMaxWidth() const;
+
+ /*! \brief Calculates the width of some text
+ \param text std::wstring of text whose width we want
+ \return width of the given text
+ \sa GetTextWidth
+ */
+ float CalcTextWidth(const std::wstring& text) const { return m_textLayout.GetTextWidth(text); }
+
+ const CLabelInfo& GetLabelInfo() const { return m_label; }
+ CLabelInfo& GetLabelInfo() { return m_label; }
+
+ /*! \brief Check a left aligned and right aligned label for overlap and cut the labels off so that no overlap occurs
+
+ If a left-aligned label occupies some of the same space on screen as a right-aligned label, then we may be able to
+ correct for this by restricting the width of one or both of them. This routine checks two labels to see whether they
+ satisfy this assumption and, if so, adjusts the render rect of both labels so that they no longer do so. The order
+ of the two labels is not important, but we do assume that the left-aligned label is also the left-most on screen, and
+ that the right-aligned label is the right most on-screen, so that they overlap due to the fact that one or both of
+ the labels are longer than anticipated. In the following diagram, [R...[R R] refers to the maximal allowed and
+ actual space occupied by the right label. Similarly, [L L]...L] refers to the maximal and actual space occupied
+ by the left label. | refers to the central cutting point, i.e. the point that would divide the maximal allowed
+ overlap perfectly in two. There are 3 scenarios to consider:
+
+ cut
+ [L [R...[R L].|..........L] R] left label ends to the left of the cut -> just crop the left label.
+ [L [R.....[R | L]..L] R] both left and right labels occupy more than the cut allows, so crop both.
+ [L [R..........|.[R L]..L] R] right label ends to the right of the cut -> just crop the right label.
+
+ \param label1 First label to check
+ \param label2 Second label to check
+ */
+ static bool CheckAndCorrectOverlap(CGUILabel &label1, CGUILabel &label2);
+
+protected:
+ UTILS::COLOR::Color GetColor() const;
+
+ /*! \brief Computes the final layout of the text
+ Uses the maximal position and width of the text, as well as the text length
+ and alignment to compute the final render rect of the text.
+ \sa GetRenderRect, SetRenderRect
+ */
+ void UpdateRenderRect();
+
+private:
+ CLabelInfo m_label;
+ CGUITextLayout m_textLayout;
+
+ bool m_scrolling;
+ OVER_FLOW m_overflowType;
+ CScrollInfo m_scrollInfo;
+ CRect m_renderRect; ///< actual sizing of text
+ CRect m_maxRect; ///< maximum sizing of text
+ bool m_invalid; ///< if true, the label needs recomputing
+ COLOR m_color; ///< color to render text \sa SetColor, GetColor
+ unsigned int m_maxScrollLoops = ~0U;
+};
diff --git a/xbmc/guilib/GUILabelControl.cpp b/xbmc/guilib/GUILabelControl.cpp
new file mode 100644
index 0000000..9728639
--- /dev/null
+++ b/xbmc/guilib/GUILabelControl.cpp
@@ -0,0 +1,293 @@
+/*
+ * 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 "GUILabelControl.h"
+
+#include "GUIFont.h"
+#include "GUIMessage.h"
+#include "utils/CharsetConverter.h"
+#include "utils/ColorUtils.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI::GUILIB;
+
+CGUILabelControl::CGUILabelControl(int parentID, int controlID, float posX, float posY, float width, float height, const CLabelInfo& labelInfo, bool wrapMultiLine, bool bHasPath)
+ : CGUIControl(parentID, controlID, posX, posY, width, height)
+ , m_label(posX, posY, width, height, labelInfo, wrapMultiLine ? CGUILabel::OVER_FLOW_WRAP : CGUILabel::OVER_FLOW_TRUNCATE)
+{
+ m_bHasPath = bHasPath;
+ m_iCursorPos = 0;
+ m_bShowCursor = false;
+ m_dwCounter = 0;
+ ControlType = GUICONTROL_LABEL;
+ m_startHighlight = m_endHighlight = 0;
+ m_startSelection = m_endSelection = 0;
+ m_minWidth = 0;
+ m_label.SetScrollLoopCount(2);
+}
+
+CGUILabelControl::~CGUILabelControl(void) = default;
+
+void CGUILabelControl::ShowCursor(bool bShow)
+{
+ m_bShowCursor = bShow;
+}
+
+void CGUILabelControl::SetCursorPos(int iPos)
+{
+ std::string labelUTF8 = m_infoLabel.GetLabel(m_parentID);
+ std::wstring label;
+ g_charsetConverter.utf8ToW(labelUTF8, label);
+ if (iPos > (int)label.length()) iPos = label.length();
+ if (iPos < 0) iPos = 0;
+
+ if (m_iCursorPos != iPos)
+ MarkDirtyRegion();
+
+ m_iCursorPos = iPos;
+}
+
+void CGUILabelControl::SetInfo(const GUIINFO::CGUIInfoLabel &infoLabel)
+{
+ m_infoLabel = infoLabel;
+}
+
+bool CGUILabelControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_label.UpdateColors();
+
+ return changed;
+}
+
+void CGUILabelControl::UpdateInfo(const CGUIListItem *item)
+{
+ std::string label(m_infoLabel.GetLabel(m_parentID));
+
+ bool changed = false;
+ if (m_startHighlight < m_endHighlight || m_startSelection < m_endSelection || m_bShowCursor)
+ {
+ std::wstring utf16;
+ g_charsetConverter.utf8ToW(label, utf16);
+ vecText text; text.reserve(utf16.size()+1);
+ std::vector<UTILS::COLOR::Color> colors;
+ colors.push_back(m_label.GetLabelInfo().textColor);
+ colors.push_back(m_label.GetLabelInfo().disabledColor);
+ UTILS::COLOR::Color select = m_label.GetLabelInfo().selectedColor;
+ if (!select)
+ select = 0xFFFF0000;
+ colors.push_back(select);
+ colors.push_back(0xFF000000);
+
+ CGUIFont* font = m_label.GetLabelInfo().font;
+ uint32_t style = (font ? font->GetStyle() : (FONT_STYLE_NORMAL & FONT_STYLE_MASK)) << 24;
+
+ for (unsigned int i = 0; i < utf16.size(); i++)
+ {
+ uint32_t ch = utf16[i] | style;
+ if ((m_startSelection < m_endSelection) && (m_startSelection <= i && i < m_endSelection))
+ ch |= (2 << 16);
+ else if ((m_startHighlight < m_endHighlight) && (i < m_startHighlight || i >= m_endHighlight))
+ ch |= (1 << 16);
+ text.push_back(ch);
+ }
+ if (m_bShowCursor && m_iCursorPos >= 0 && (unsigned int)m_iCursorPos <= utf16.size())
+ {
+ uint32_t ch = L'|' | style;
+ if ((++m_dwCounter % 50) <= 25)
+ ch |= (3 << 16);
+ text.insert(text.begin() + m_iCursorPos, ch);
+ }
+ changed |= m_label.SetMaxRect(m_posX, m_posY, GetMaxWidth(), m_height);
+ changed |= m_label.SetStyledText(text, colors);
+ }
+ else
+ {
+ if (m_bHasPath)
+ label = ShortenPath(label);
+
+ changed |= m_label.SetMaxRect(m_posX, m_posY, GetMaxWidth(), m_height);
+ changed |= m_label.SetText(label);
+ }
+ if (changed)
+ MarkDirtyRegion();
+}
+
+void CGUILabelControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ bool changed = false;
+
+ changed |= m_label.SetColor(IsDisabled() ? CGUILabel::COLOR_DISABLED : CGUILabel::COLOR_TEXT);
+ changed |= m_label.SetMaxRect(m_posX, m_posY, GetMaxWidth(), m_height);
+ changed |= m_label.Process(currentTime);
+
+ if (changed)
+ MarkDirtyRegion();
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+CRect CGUILabelControl::CalcRenderRegion() const
+{
+ return m_label.GetRenderRect();
+}
+
+void CGUILabelControl::Render()
+{
+ m_label.Render();
+ CGUIControl::Render();
+}
+
+
+bool CGUILabelControl::CanFocus() const
+{
+ return false;
+}
+
+void CGUILabelControl::SetLabel(const std::string &strLabel)
+{
+ // NOTE: this optimization handles fixed labels only (i.e. not info labels).
+ // One way it might be extended to all labels would be for GUIInfoLabel ( or here )
+ // to store the label prior to parsing, and then compare that against what you're setting.
+ if (m_infoLabel.GetLabel(GetParentID(), false) != strLabel)
+ {
+ m_infoLabel.SetLabel(strLabel, "", GetParentID());
+ if (m_iCursorPos > (int)strLabel.size())
+ m_iCursorPos = strLabel.size();
+
+ SetInvalid();
+ }
+}
+
+void CGUILabelControl::SetWidthControl(float minWidth, bool bScroll)
+{
+ if (m_label.SetScrolling(bScroll) || m_minWidth != minWidth)
+ MarkDirtyRegion();
+
+ m_minWidth = minWidth;
+}
+
+void CGUILabelControl::SetAlignment(uint32_t align)
+{
+ if (m_label.GetLabelInfo().align != align)
+ {
+ MarkDirtyRegion();
+ m_label.GetLabelInfo().align = align;
+ }
+}
+
+#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))
+
+float CGUILabelControl::GetWidth() const
+{
+ if (m_minWidth && m_minWidth != m_width)
+ return CLAMP(m_label.GetTextWidth(), m_minWidth, GetMaxWidth());
+ return m_width;
+}
+
+void CGUILabelControl::SetWidth(float width)
+{
+ m_width = width;
+ m_label.SetMaxRect(m_posX, m_posY, m_width, m_height);
+ CGUIControl::SetWidth(m_width);
+}
+
+bool CGUILabelControl::OnMessage(CGUIMessage& message)
+{
+ if (message.GetControlId() == GetID())
+ {
+ if (message.GetMessage() == GUI_MSG_LABEL_SET)
+ {
+ SetLabel(message.GetLabel());
+ return true;
+ }
+ }
+ if (message.GetMessage() == GUI_MSG_REFRESH_TIMER && IsVisible())
+ {
+ UpdateInfo();
+ }
+ else if (message.GetMessage() == GUI_MSG_WINDOW_RESIZE && IsVisible())
+ {
+ m_label.SetInvalid();
+ }
+
+ return CGUIControl::OnMessage(message);
+}
+
+std::string CGUILabelControl::ShortenPath(const std::string &path)
+{
+ if (m_width == 0 || path.empty())
+ return path;
+
+ char cDelim = '\0';
+ size_t nPos;
+
+ nPos = path.find_last_of( '\\' );
+ if ( nPos != std::string::npos )
+ cDelim = '\\';
+ else
+ {
+ nPos = path.find_last_of( '/' );
+ if ( nPos != std::string::npos )
+ cDelim = '/';
+ }
+ if ( cDelim == '\0' )
+ return path;
+
+ std::string workPath(path);
+ // remove trailing slashes
+ if (workPath.size() > 3)
+ if (!StringUtils::EndsWith(workPath, "://") &&
+ !StringUtils::EndsWith(workPath, ":\\"))
+ if (nPos == workPath.size() - 1)
+ {
+ workPath.erase(workPath.size() - 1);
+ nPos = workPath.find_last_of( cDelim );
+ }
+
+ if (m_label.SetText(workPath))
+ MarkDirtyRegion();
+
+ float textWidth = m_label.GetTextWidth();
+
+ while ( textWidth > m_width )
+ {
+ size_t nGreaterDelim = workPath.find_last_of( cDelim, nPos );
+ if (nGreaterDelim == std::string::npos)
+ break;
+
+ nPos = workPath.find_last_of( cDelim, nGreaterDelim - 1 );
+ if ( nPos == std::string::npos )
+ break;
+
+ workPath.replace( nPos + 1, nGreaterDelim - nPos - 1, "..." );
+
+ if (m_label.SetText(workPath))
+ MarkDirtyRegion();
+
+ textWidth = m_label.GetTextWidth();
+ }
+ return workPath;
+}
+
+void CGUILabelControl::SetHighlight(unsigned int start, unsigned int end)
+{
+ m_startHighlight = start;
+ m_endHighlight = end;
+}
+
+void CGUILabelControl::SetSelection(unsigned int start, unsigned int end)
+{
+ m_startSelection = start;
+ m_endSelection = end;
+}
+
+std::string CGUILabelControl::GetDescription() const
+{
+ return m_infoLabel.GetLabel(m_parentID);
+}
diff --git a/xbmc/guilib/GUILabelControl.dox b/xbmc/guilib/GUILabelControl.dox
new file mode 100644
index 0000000..8501c93
--- /dev/null
+++ b/xbmc/guilib/GUILabelControl.dox
@@ -0,0 +1,112 @@
+/*!
+
+\page Label_Control Label Control
+\brief **Used to show some lines of text.**
+
+\tableofcontents
+
+The label control is used for displaying text in Kodi. You can choose the font,
+size, colour, location and contents of the text to be displayed.
+
+
+--------------------------------------------------------------------------------
+\section Label_Control_sect1 Example
+
+~~~~~~~~~~~~~
+ <control type="label" id="1">
+ <description>My First label</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <visible>true</visible>
+ <align>center</align>
+ <aligny>center</aligny>
+ <scroll>false</scroll>
+ <label>6</label>
+ <info>MusicPlayer.Artist</info>
+ <number></number>
+ <angle>30</angle>
+ <haspath>false</haspath>
+ <font>font14</font>
+ <textcolor>FFB2D4F5</textcolor>
+ <shadowcolor>ff000000</shadowcolor>
+ <wrapmultiline>false</wrapmultiline>
+ <scrollspeed>50</scrollspeed>
+ <scrollsuffix> - </scrollsuffix>
+ </control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Label_Control_sect2 Auto size labels
+Wrapping your label in a grouplist with the auto width and appropriate minimum
+and maximum values. Allows the labels width to dynamically change relative to
+how long the label text is. This allows a image or other control to be aligned
+to the right of the actual label text no matter how wide the label is.
+
+~~~~~~~~~~~~~
+ <width min="29" max="200">auto</width>
+~~~~~~~~~~~~~
+
+As of XBMC Gotham, simply specifying <b>`<width>auto</width>`</b> is also supported.
+
+
+--------------------------------------------------------------------------------
+\section Label_Control_sect3 Multi-line labels
+
+If you want your label control to span multiple lines, you can insert a new line
+character in your label. For example:
+
+~~~~~~~~~~~~~
+ <label>This will be on the first line[CR]And this will be on the second line</label>
+~~~~~~~~~~~~~
+
+Also, if you want your label control to conform to the <b>`<width>`</b> parameter, but still
+want to be able to give it more content than will fit on one line, then setting:
+
+~~~~~~~~~~~~~
+ <wrapmultiline>true</wrapmultiline>
+~~~~~~~~~~~~~
+
+will cause the text to be cut up (at the spaces in the text) onto multiple lines.
+Note that if a single word is larger than <b>`<width>`</b> then it will not be cut, and
+will still overflow.
+
+
+--------------------------------------------------------------------------------
+\section Label_Control_sect4 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| align | Can be left, right, or center. Aligns the text within the given label <b>`<width>`</b>. Defaults to left
+| aligny | Can be top or center. Aligns the text within its given label <b>`<height>`</b>. Defaults to top
+| scroll | When true, the text will scroll if longer than the label's <b>`<width>`</b>. If false, the text will be truncated. Defaults to false.
+| label | Specifies the text which should be drawn. You should specify an entry from the strings.po here (either the Kodi strings.po or your skin's strings.po file), however you may also hardcode a piece of text also if you wish, though of course it will not be localisable. You can use the full [label formatting syntax](http://kodi.wiki/view/Label_Formatting) and [you may also specify more than one piece of information here by using the $INFO and $LOCALIZE formats](http://kodi.wiki/view/Label_Parsing).
+| info | Specifies the information that should be presented. Kodi will auto-fill in this info in place of the <b>`<label>`</b>. [See here for more information](http://kodi.wiki/view/InfoLabels).
+| number | Specifies a number that should be presented. This is just here to allow a skinner to use a number rather than a text label (as any number given to <b>`<label>`</b> will be used to lookup in strings.po)
+| angle | The angle the text should be rendered at, in degrees. A value of 0 is horizontal.
+| haspath | Specifies whether or not this label is filled with a path. Long paths are shortened by compressing the file path while keeping the actual filename full length.
+| font | Specifies the font to use from the font.xml file.
+| textcolor | Specifies the color the text should be, in hex **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| shadowcolor | Specifies the color of the drop shadow on the text, in **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| wrapmultiline | If true, any text that doesn't fit on one line will be wrapped onto multiple lines.
+| scrollspeed | Scroll speed of text in pixels per second. Defaults to 60.
+| scrollsuffix | Specifies the suffix used in scrolling labels. Defaults to <b>`"¦"`</b>.
+
+
+
+--------------------------------------------------------------------------------
+\section Label_Control_sect5 See also
+
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+
+*/
diff --git a/xbmc/guilib/GUILabelControl.h b/xbmc/guilib/GUILabelControl.h
new file mode 100644
index 0000000..ce7ba4e
--- /dev/null
+++ b/xbmc/guilib/GUILabelControl.h
@@ -0,0 +1,80 @@
+/*
+ * 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
+
+/*!
+\file GUILabelControl.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUILabel.h"
+#include "guilib/guiinfo/GUIInfoLabel.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUILabelControl :
+ public CGUIControl
+{
+public:
+ CGUILabelControl(int parentID, int controlID, float posX, float posY, float width, float height, const CLabelInfo& labelInfo, bool wrapMultiLine, bool bHasPath);
+ ~CGUILabelControl(void) override;
+ CGUILabelControl* Clone() const override { return new CGUILabelControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ void UpdateInfo(const CGUIListItem *item = NULL) override;
+ bool CanFocus() const override;
+ bool OnMessage(CGUIMessage& message) override;
+ std::string GetDescription() const override;
+ float GetWidth() const override;
+ void SetWidth(float width) override;
+ CRect CalcRenderRegion() const override;
+
+ const CLabelInfo& GetLabelInfo() const { return m_label.GetLabelInfo(); }
+ void SetLabel(const std::string &strLabel);
+ void ShowCursor(bool bShow = true);
+ void SetCursorPos(int iPos);
+ int GetCursorPos() const { return m_iCursorPos; }
+ void SetInfo(const KODI::GUILIB::GUIINFO::CGUIInfoLabel&labelInfo);
+ void SetWidthControl(float minWidth, bool bScroll);
+ void SetAlignment(uint32_t align);
+ void SetHighlight(unsigned int start, unsigned int end);
+ void SetSelection(unsigned int start, unsigned int end);
+
+protected:
+ bool UpdateColors(const CGUIListItem* item) override;
+ std::string ShortenPath(const std::string &path);
+
+ /*! \brief Return the maximum width of this label control.
+ \return Return the width of the control if available, else the width of the current text.
+ */
+ float GetMaxWidth() const { return m_width ? m_width : m_label.GetTextWidth(); }
+
+ CGUILabel m_label;
+
+ bool m_bHasPath;
+ bool m_bShowCursor;
+ int m_iCursorPos;
+ unsigned int m_dwCounter;
+
+ // stuff for autowidth
+ float m_minWidth;
+
+ // multi-info stuff
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_infoLabel;
+
+ unsigned int m_startHighlight;
+ unsigned int m_endHighlight;
+ unsigned int m_startSelection;
+ unsigned int m_endSelection;
+};
+
diff --git a/xbmc/guilib/GUIListContainer.cpp b/xbmc/guilib/GUIListContainer.cpp
new file mode 100644
index 0000000..b078263
--- /dev/null
+++ b/xbmc/guilib/GUIListContainer.cpp
@@ -0,0 +1,299 @@
+/*
+ * 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 "GUIListContainer.h"
+
+#include "GUIListItemLayout.h"
+#include "GUIMessage.h"
+#include "input/Key.h"
+#include "utils/StringUtils.h"
+
+CGUIListContainer::CGUIListContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems)
+ : CGUIBaseContainer(parentID, controlID, posX, posY, width, height, orientation, scroller, preloadItems)
+{
+ ControlType = GUICONTAINER_LIST;
+ m_type = VIEW_TYPE_LIST;
+}
+
+CGUIListContainer::CGUIListContainer(const CGUIListContainer& other) : CGUIBaseContainer(other)
+{
+}
+
+CGUIListContainer::~CGUIListContainer(void) = default;
+
+bool CGUIListContainer::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_PAGE_UP:
+ {
+ if (GetOffset() == 0)
+ { // already on the first page, so move to the first item
+ SetCursor(0);
+ }
+ else
+ { // scroll up to the previous page
+ Scroll( -m_itemsPerPage);
+ }
+ return true;
+ }
+ break;
+ case ACTION_PAGE_DOWN:
+ {
+ if (GetOffset() == (int)m_items.size() - m_itemsPerPage || (int)m_items.size() < m_itemsPerPage)
+ { // already at the last page, so move to the last item.
+ SetCursor(m_items.size() - GetOffset() - 1);
+ }
+ else
+ { // scroll down to the next page
+ Scroll(m_itemsPerPage);
+ }
+ return true;
+ }
+ break;
+ // smooth scrolling (for analog controls)
+ case ACTION_SCROLL_UP:
+ {
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+ if (GetOffset() > 0 && GetCursor() <= m_itemsPerPage / 2)
+ {
+ Scroll(-1);
+ }
+ else if (GetCursor() > 0)
+ {
+ SetCursor(GetCursor() - 1);
+ }
+ }
+ return handled;
+ }
+ break;
+ case ACTION_SCROLL_DOWN:
+ {
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+ if (GetOffset() + m_itemsPerPage < (int)m_items.size() && GetCursor() >= m_itemsPerPage / 2)
+ {
+ Scroll(1);
+ }
+ else if (GetCursor() < m_itemsPerPage - 1 && GetOffset() + GetCursor() < (int)m_items.size() - 1)
+ {
+ SetCursor(GetCursor() + 1);
+ }
+ }
+ return handled;
+ }
+ break;
+ }
+ return CGUIBaseContainer::OnAction(action);
+}
+
+bool CGUIListContainer::OnMessage(CGUIMessage& message)
+{
+ if (message.GetControlId() == GetID() )
+ {
+ if (message.GetMessage() == GUI_MSG_LABEL_RESET)
+ {
+ SetCursor(0);
+ SetOffset(0);
+ m_scroller.SetValue(0);
+ }
+ }
+ return CGUIBaseContainer::OnMessage(message);
+}
+
+bool CGUIListContainer::MoveUp(bool wrapAround)
+{
+ if (GetCursor() > 0)
+ {
+ SetCursor(GetCursor() - 1);
+ }
+ else if (GetCursor() == 0 && GetOffset())
+ {
+ ScrollToOffset(GetOffset() - 1);
+ }
+ else if (wrapAround)
+ {
+ if (!m_items.empty())
+ { // move 2 last item in list, and set our container moving up
+ int offset = m_items.size() - m_itemsPerPage;
+ if (offset < 0) offset = 0;
+ SetCursor(m_items.size() - offset - 1);
+ ScrollToOffset(offset);
+ SetContainerMoving(-1);
+ }
+ }
+ else
+ return false;
+ return true;
+}
+
+bool CGUIListContainer::MoveDown(bool wrapAround)
+{
+ if (GetOffset() + GetCursor() + 1 < (int)m_items.size())
+ {
+ if (GetCursor() + 1 < m_itemsPerPage)
+ {
+ SetCursor(GetCursor() + 1);
+ }
+ else
+ {
+ ScrollToOffset(GetOffset() + 1);
+ }
+ }
+ else if(wrapAround)
+ { // move first item in list, and set our container moving in the "down" direction
+ SetCursor(0);
+ ScrollToOffset(0);
+ SetContainerMoving(1);
+ }
+ else
+ return false;
+ return true;
+}
+
+// scrolls the said amount
+void CGUIListContainer::Scroll(int amount)
+{
+ // increase or decrease the offset
+ int offset = GetOffset() + amount;
+ if (offset > (int)m_items.size() - m_itemsPerPage)
+ {
+ offset = m_items.size() - m_itemsPerPage;
+ }
+ if (offset < 0) offset = 0;
+ ScrollToOffset(offset);
+}
+
+void CGUIListContainer::ValidateOffset()
+{
+ if (!m_layout) return;
+ // first thing is we check the range of our offset
+ // don't validate offset if we are scrolling in case the tween image exceed <0, 1> range
+ int minOffset, maxOffset;
+ GetOffsetRange(minOffset, maxOffset);
+ if (GetOffset() > maxOffset || (!m_scroller.IsScrolling() && m_scroller.GetValue() > maxOffset * m_layout->Size(m_orientation)))
+ {
+ SetOffset(std::max(0, maxOffset));
+ m_scroller.SetValue(GetOffset() * m_layout->Size(m_orientation));
+ }
+ if (GetOffset() < 0 || (!m_scroller.IsScrolling() && m_scroller.GetValue() < 0))
+ {
+ SetOffset(0);
+ m_scroller.SetValue(0);
+ }
+}
+
+void CGUIListContainer::SetCursor(int cursor)
+{
+ if (cursor > m_itemsPerPage - 1) cursor = m_itemsPerPage - 1;
+ if (cursor < 0) cursor = 0;
+ if (!m_wasReset)
+ SetContainerMoving(cursor - GetCursor());
+ CGUIBaseContainer::SetCursor(cursor);
+}
+
+void CGUIListContainer::SelectItem(int item)
+{
+ // Check that our offset is valid
+ ValidateOffset();
+ // only select an item if it's in a valid range
+ if (item >= 0 && item < (int)m_items.size())
+ {
+ // Select the item requested
+ if (item >= GetOffset() && item < GetOffset() + m_itemsPerPage)
+ { // the item is on the current page, so don't change it.
+ SetCursor(item - GetOffset());
+ }
+ else if (item < GetOffset())
+ { // item is on a previous page - make it the first item on the page
+ SetCursor(0);
+ ScrollToOffset(item);
+ }
+ else // (item >= GetOffset()+m_itemsPerPage)
+ { // item is on a later page - make it the last item on the page
+ SetCursor(m_itemsPerPage - 1);
+ ScrollToOffset(item - GetCursor());
+ }
+ }
+}
+
+int CGUIListContainer::GetCursorFromPoint(const CPoint &point, CPoint *itemPoint) const
+{
+ if (!m_focusedLayout || !m_layout)
+ return -1;
+
+ int row = 0;
+ float pos = (m_orientation == VERTICAL) ? point.y : point.x;
+ while (row < m_itemsPerPage + 1) // 1 more to ensure we get the (possible) half item at the end.
+ {
+ const CGUIListItemLayout *layout = (row == GetCursor()) ? m_focusedLayout : m_layout;
+ if (pos < layout->Size(m_orientation) && row + GetOffset() < (int)m_items.size())
+ { // found correct "row" -> check horizontal
+ if (!InsideLayout(layout, point))
+ return -1;
+
+ if (itemPoint)
+ *itemPoint = m_orientation == VERTICAL ? CPoint(point.x, pos) : CPoint(pos, point.y);
+ return row;
+ }
+ row++;
+ pos -= layout->Size(m_orientation);
+ }
+ return -1;
+}
+
+bool CGUIListContainer::SelectItemFromPoint(const CPoint &point)
+{
+ CPoint itemPoint;
+ int row = GetCursorFromPoint(point, &itemPoint);
+ if (row < 0)
+ return false;
+
+ SetCursor(row);
+ CGUIListItemLayout *focusedLayout = GetFocusedLayout();
+ if (focusedLayout)
+ focusedLayout->SelectItemFromPoint(itemPoint);
+ return true;
+}
+
+//#ifdef GUILIB_PYTHON_COMPATIBILITY
+CGUIListContainer::CGUIListContainer(int parentID, int controlID, float posX, float posY, float width, float height,
+ const CLabelInfo& labelInfo, const CLabelInfo& labelInfo2,
+ const CTextureInfo& textureButton, const CTextureInfo& textureButtonFocus,
+ float textureHeight, float itemWidth, float itemHeight, float spaceBetweenItems)
+: CGUIBaseContainer(parentID, controlID, posX, posY, width, height, VERTICAL, 200, 0)
+{
+ m_layouts.emplace_back();
+ m_layouts.back().CreateListControlLayouts(width, textureHeight + spaceBetweenItems, false, labelInfo, labelInfo2, textureButton, textureButtonFocus, textureHeight, itemWidth, itemHeight, "", "");
+ std::string condition = StringUtils::Format("control.hasfocus({})", controlID);
+ std::string condition2 = "!" + condition;
+ m_focusedLayouts.emplace_back();
+ m_focusedLayouts.back().CreateListControlLayouts(width, textureHeight + spaceBetweenItems, true, labelInfo, labelInfo2, textureButton, textureButtonFocus, textureHeight, itemWidth, itemHeight, condition2, condition);
+ m_height = floor(m_height / (textureHeight + spaceBetweenItems)) * (textureHeight + spaceBetweenItems);
+ ControlType = GUICONTAINER_LIST;
+}
+//#endif
+
+bool CGUIListContainer::HasNextPage() const
+{
+ return (GetOffset() != (int)m_items.size() - m_itemsPerPage && (int)m_items.size() >= m_itemsPerPage);
+}
+
+bool CGUIListContainer::HasPreviousPage() const
+{
+ return (GetOffset() > 0);
+}
diff --git a/xbmc/guilib/GUIListContainer.dox b/xbmc/guilib/GUIListContainer.dox
new file mode 100644
index 0000000..5e01df4
--- /dev/null
+++ b/xbmc/guilib/GUIListContainer.dox
@@ -0,0 +1,138 @@
+/*!
+
+\page List_Container List Container
+\brief **Used for a scrolling lists of items. Replaces the list control.**
+
+\tableofcontents
+
+The list container is one of several containers used to display items from file
+lists in various ways. The list container is very flexible - it's only
+restriction is that it is a list - i.e. a single column or row of items. The
+layout of the items is very flexible and is up to the skinner.
+
+--------------------------------------------------------------------------------
+\section List_Container_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="list" id="50">
+ <description>My first list container</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+ <viewtype label="3D list">list</viewtype>
+ <orientation>vertical</orientation>
+ <pagecontrol>25</pagecontrol>
+ <autoscroll>true</autoscroll>
+ <scrolltime tween="sine" easing="out">200</scrolltime>
+ <itemlayout width="250" height="29">
+ <control type="image">
+ <posx>5</posx>
+ <posy>3</posy>
+ <width>22</width>
+ <height>22</height>
+ <info>ListItem.Icon</info>
+ </control>
+ <control type="label">
+ <posx>30</posx>
+ <posy>3</posy>
+ <width>430</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <align>left</align>
+ <info>ListItem.Label</info>
+ </control>
+ <control type="label">
+ <posx>475</posx>
+ <posy>3</posy>
+ <width>300</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <textcolor>grey</textcolor>
+ <align>right</align>
+ <info>ListItem.Label2</info>
+ </control>
+ </itemlayout>
+ <focusedlayout height="29" width="250">
+ <control type="image">
+ <width>485</width>
+ <height>29</height>
+ <posx>0</posx>
+ <posy>0</posy>
+ <visible>Control.HasFocus(50)</visible>
+ <texture>list-focus.png</texture>
+ </control>
+ <control type="image">
+ <posx>5</posx>
+ <posy>3</posy>
+ <width>22</width>
+ <height>22</height>
+ <info>ListItem.Icon</info>
+ </control>
+ <control type="label">
+ <posx>30</posx>
+ <posy>3</posy>
+ <width>430</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <align>left</align>
+ <info>ListItem.Label</info>
+ </control>
+ <control type="label">
+ <posx>475</posx>
+ <posy>3</posy>
+ <width>300</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <textcolor>grey</textcolor>
+ <align>right</align>
+ <info>ListItem.Label2</info>
+ </control>
+ </focusedlayout>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section List_Container_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| viewtype | The type of view. Choices are list, icon, wide, wrap, biglist, bigicon, bigwide, bigwrap, info and biginfo. The label attribute indicates the label that will be used in the "View As" control within the GUI. It is localizable via strings.po. viewtype has no effect on the view itself. It is used by kodi when switching skin to automatically select a view with a similar layout. Skinners should try to set viewtype to describe the layout as best as possible.
+| orientation | The orientation of the list. Defaults to vertical.
+| pagecontrol | Used to set the <b>`<id>`</b> of the page control used to control this list.
+| scrolltime | The time (in ms) to scroll from one item to another. By default, this is 200ms. The list will scroll smoothly from one item to another as needed. Set it to zero to disable the smooth scrolling. The scroll movement can be further adjusted by selecting one of the available [tween](http://kodi.wiki/view/Tweeners) methods.
+| itemlayout | Specifies the layout of items in the list. Requires the height attribute set in a vertical list, and the width attribute set for a horizontal list. The <b>`<itemlayout>`</b> then contains as many label and image controls as required. [See here for more information](http://kodi.wiki/view/Container_Item_Layout).
+| focusedlayout | Specifies the layout of items in the list that have focus. Requires the height attribute set in a vertical list, and the width attribute set for a horizontal list. The <b>`<focusedlayout>`</b> then contains as many label and image controls as required. [See here for more information](http://kodi.wiki/view/Container_Item_Layout).
+| content | Used to set the item content that this list will contain. Allows the skinner to setup a list anywhere they want with a static set of content, as a useful alternative to the grouplist control. [See here for more information](http://kodi.wiki/view/Static_List_Content).
+| preloaditems | Used in association with the background image loader. [See here for more information](http://kodi.wiki/view/Background_Image_Loader).
+| autoscroll | Used to make the container scroll automatically
+
+
+--------------------------------------------------------------------------------
+\section List_Container_sect3 See also
+
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIListContainer.h b/xbmc/guilib/GUIListContainer.h
new file mode 100644
index 0000000..d16aa65
--- /dev/null
+++ b/xbmc/guilib/GUIListContainer.h
@@ -0,0 +1,55 @@
+/*
+ * 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
+
+/*!
+\file GUIListContainer.h
+\brief
+*/
+
+#include "GUIBaseContainer.h"
+
+class CLabelInfo;
+class CTextureInfo;
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIListContainer : public CGUIBaseContainer
+{
+public:
+ CGUIListContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems);
+ explicit CGUIListContainer(const CGUIListContainer& other);
+ //#ifdef GUILIB_PYTHON_COMPATIBILITY
+ CGUIListContainer(int parentID, int controlID, float posX, float posY, float width, float height,
+ const CLabelInfo& labelInfo, const CLabelInfo& labelInfo2,
+ const CTextureInfo& textureButton, const CTextureInfo& textureButtonFocus,
+ float textureHeight, float itemWidth, float itemHeight, float spaceBetweenItems);
+ //#endif
+ ~CGUIListContainer(void) override;
+ CGUIListContainer* Clone() const override { return new CGUIListContainer(*this); }
+
+ bool OnAction(const CAction &action) override;
+ bool OnMessage(CGUIMessage& message) override;
+
+ bool HasNextPage() const override;
+ bool HasPreviousPage() const override;
+
+protected:
+ void Scroll(int amount) override;
+ void SetCursor(int cursor) override;
+ bool MoveDown(bool wrapAround) override;
+ bool MoveUp(bool wrapAround) override;
+ void ValidateOffset() override;
+ void SelectItem(int item) override;
+ bool SelectItemFromPoint(const CPoint &point) override;
+ int GetCursorFromPoint(const CPoint &point, CPoint *itemPoint = NULL) const override;
+};
+
diff --git a/xbmc/guilib/GUIListGroup.cpp b/xbmc/guilib/GUIListGroup.cpp
new file mode 100644
index 0000000..67645c2
--- /dev/null
+++ b/xbmc/guilib/GUIListGroup.cpp
@@ -0,0 +1,238 @@
+/*
+ * 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 "GUIListGroup.h"
+
+#include "GUIListLabel.h"
+#include "utils/log.h"
+
+#include <set>
+
+namespace
+{
+// Supported control types. Keep sorted.
+const std::set<CGUIControl::GUICONTROLTYPES> supportedTypes = {
+ // clang-format off
+ CGUIControl::GUICONTROL_BORDEREDIMAGE,
+ CGUIControl::GUICONTROL_GAME,
+ CGUIControl::GUICONTROL_GAMECONTROLLER,
+ CGUIControl::GUICONTROL_IMAGE,
+ CGUIControl::GUICONTROL_LISTGROUP,
+ CGUIControl::GUICONTROL_LISTLABEL,
+ CGUIControl::GUICONTROL_MULTI_IMAGE,
+ CGUIControl::GUICONTROL_PROGRESS,
+ CGUIControl::GUICONTROL_TEXTBOX,
+ // clang-format on
+};
+} // namespace
+
+CGUIListGroup::CGUIListGroup(int parentID, int controlID, float posX, float posY, float width, float height)
+: CGUIControlGroup(parentID, controlID, posX, posY, width, height)
+{
+ m_item = NULL;
+ ControlType = GUICONTROL_LISTGROUP;
+}
+
+CGUIListGroup::CGUIListGroup(const CGUIListGroup &right)
+: CGUIControlGroup(right)
+{
+ m_item = NULL;
+ ControlType = GUICONTROL_LISTGROUP;
+}
+
+CGUIListGroup::~CGUIListGroup(void)
+{
+ FreeResources();
+}
+
+void CGUIListGroup::AddControl(CGUIControl *control, int position /*= -1*/)
+{
+ if (control)
+ {
+ if (supportedTypes.find(control->GetControlType()) == supportedTypes.end())
+ CLog::Log(LOGWARNING, "Trying to add unsupported control type {}", control->GetControlType());
+ }
+ CGUIControlGroup::AddControl(control, position);
+}
+
+void CGUIListGroup::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY);
+
+ CRect rect;
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *control = *it;
+ control->UpdateVisibility(m_item);
+ unsigned int oldDirty = dirtyregions.size();
+ control->DoProcess(currentTime, dirtyregions);
+ if (control->IsVisible() || (oldDirty != dirtyregions.size())) // visible or dirty (was visible?)
+ rect.Union(control->GetRenderRegion());
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
+ CGUIControl::Process(currentTime, dirtyregions);
+ m_renderRegion = rect;
+ m_item = NULL;
+}
+
+void CGUIListGroup::ResetAnimation(ANIMATION_TYPE type)
+{
+ CGUIControl::ResetAnimation(type);
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ (*it)->ResetAnimation(type);
+}
+
+void CGUIListGroup::UpdateVisibility(const CGUIListItem *item)
+{
+ CGUIControlGroup::UpdateVisibility(item);
+ m_item = item;
+}
+
+void CGUIListGroup::UpdateInfo(const CGUIListItem *item)
+{
+ for (iControls it = m_children.begin(); it != m_children.end(); it++)
+ {
+ (*it)->UpdateInfo(item);
+ (*it)->UpdateVisibility(item);
+ }
+ // now we have to check our overlapping label pairs
+ for (unsigned int i = 0; i < m_children.size(); i++)
+ {
+ if (m_children[i]->GetControlType() == CGUIControl::GUICONTROL_LISTLABEL && m_children[i]->IsVisible())
+ {
+ for (unsigned int j = i + 1; j < m_children.size(); j++)
+ {
+ if (m_children[j]->GetControlType() == CGUIControl::GUICONTROL_LISTLABEL && m_children[j]->IsVisible())
+ CGUIListLabel::CheckAndCorrectOverlap(*static_cast<CGUIListLabel*>(m_children[i]), *static_cast<CGUIListLabel*>(m_children[j]));
+ }
+ }
+ }
+}
+
+void CGUIListGroup::EnlargeWidth(float difference)
+{
+ // Alters the width of the controls that have an ID of 1 to 14
+ for (iControls it = m_children.begin(); it != m_children.end(); it++)
+ {
+ CGUIControl *child = *it;
+ if (child->GetID() >= 1 && child->GetID() <= 14)
+ {
+ if (child->GetID() == 1)
+ {
+ child->SetWidth(child->GetWidth() + difference);
+ child->SetVisible(child->GetWidth() > 10);
+ }
+ else
+ {
+ child->SetWidth(child->GetWidth() + difference);
+ }
+ }
+ }
+ SetInvalid();
+}
+
+void CGUIListGroup::EnlargeHeight(float difference)
+{
+ // Alters the height of the controls that have an ID of 1 to 14
+ for (iControls it = m_children.begin(); it != m_children.end(); it++)
+ {
+ CGUIControl *child = *it;
+ if (child->GetID() >= 1 && child->GetID() <= 14)
+ {
+ if (child->GetID() == 1)
+ {
+ child->SetHeight(child->GetHeight() + difference);
+ child->SetVisible(child->GetHeight() > 10);
+ }
+ else
+ {
+ child->SetHeight(child->GetHeight() + difference);
+ }
+ }
+ }
+ SetInvalid();
+}
+
+void CGUIListGroup::SetInvalid()
+{
+ if (!m_bInvalidated)
+ { // this can be triggered by an item change, so all children need invalidating rather than just the group
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ (*it)->SetInvalid();
+ CGUIControlGroup::SetInvalid();
+ }
+}
+
+void CGUIListGroup::SetFocusedItem(unsigned int focus)
+{
+ for (iControls it = m_children.begin(); it != m_children.end(); it++)
+ {
+ if ((*it)->GetControlType() == CGUIControl::GUICONTROL_LISTGROUP)
+ ((CGUIListGroup *)(*it))->SetFocusedItem(focus);
+ else
+ (*it)->SetFocus(focus > 0);
+ }
+ SetFocus(focus > 0);
+}
+
+unsigned int CGUIListGroup::GetFocusedItem() const
+{
+ for (ciControls it = m_children.begin(); it != m_children.end(); it++)
+ {
+ if ((*it)->GetControlType() == CGUIControl::GUICONTROL_LISTGROUP && ((CGUIListGroup *)(*it))->GetFocusedItem())
+ return ((CGUIListGroup *)(*it))->GetFocusedItem();
+ }
+ return m_bHasFocus ? 1 : 0;
+}
+
+bool CGUIListGroup::MoveLeft()
+{
+ for (iControls it = m_children.begin(); it != m_children.end(); it++)
+ {
+ if ((*it)->GetControlType() == CGUIControl::GUICONTROL_LISTGROUP && ((CGUIListGroup *)(*it))->MoveLeft())
+ return true;
+ }
+ return false;
+}
+
+bool CGUIListGroup::MoveRight()
+{
+ for (iControls it = m_children.begin(); it != m_children.end(); it++)
+ {
+ if ((*it)->GetControlType() == CGUIControl::GUICONTROL_LISTGROUP && ((CGUIListGroup *)(*it))->MoveRight())
+ return true;
+ }
+ return false;
+}
+
+void CGUIListGroup::SetState(bool selected, bool focused)
+{
+ for (iControls it = m_children.begin(); it != m_children.end(); it++)
+ {
+ if ((*it)->GetControlType() == CGUIControl::GUICONTROL_LISTLABEL)
+ {
+ CGUIListLabel *label = (CGUIListLabel *)(*it);
+ label->SetSelected(selected);
+ }
+ else if ((*it)->GetControlType() == CGUIControl::GUICONTROL_LISTGROUP)
+ ((CGUIListGroup *)(*it))->SetState(selected, focused);
+ }
+}
+
+void CGUIListGroup::SelectItemFromPoint(const CPoint &point)
+{
+ CPoint controlCoords(point);
+ m_transform.InverseTransformPosition(controlCoords.x, controlCoords.y);
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *child = *it;
+ if (child->GetControlType() == CGUIControl::GUICONTROL_LISTGROUP)
+ static_cast<CGUIListGroup*>(child)->SelectItemFromPoint(point);
+ }
+}
diff --git a/xbmc/guilib/GUIListGroup.dox b/xbmc/guilib/GUIListGroup.dox
new file mode 100644
index 0000000..d57791f
--- /dev/null
+++ b/xbmc/guilib/GUIListGroup.dox
@@ -0,0 +1,67 @@
+/*!
+
+\page Group_List_Control Group List Control
+\brief **Special case of the group control that forms a scrolling list of controls.**
+
+\tableofcontents
+
+The group list control is a special case of the group control. It is used for
+placing a set of controls into a list (either horizontally or vertically) and
+handles all the navigation within the list and placement within the list for
+you. It will be scrollable if the number of items exceeds the size of the
+list, and you can assign a scrollbar to it just like you can to any of the
+containers (list, panel, etc.).
+
+
+--------------------------------------------------------------------------------
+\section Group_List_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="grouplist" id="17">
+ <description>My first group list control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>300</height>
+ <itemgap>10</itemgap>
+ <pagecontrol>25</pagecontrol>
+ <scrolltime tween="sine" easing="out">200</scrolltime>
+ <orientation>vertical</orientation>
+ <usecontrolcoords>false</usecontrolcoords>
+ <visible>true</visible>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+ <align>right</align>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Group_List_Control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|------------------:|:--------------------------------------------------------------|
+| itemgap | Specifies the gap (in pixels) between controls in the list. Defaults to 5px.
+| orientation | Specifies whether the list is horizontal or vertical. Defaults to vertical.
+| pagecontrol | Specifies the page control used to scroll up and down the list. Set the page control's id here.
+| scrolltime | The time (in ms) to scroll from one item to another. By default, this is 200ms. The list will scroll smoothly from one item to another as needed. Set it to zero to disable the smooth scrolling. The scroll movement can be further adjusted by selecting one of the available [tween](http://kodi.wiki/view/Tweeners) methods.
+| usecontrolcoords | Specifies whether the list should use the control's coordinates as an offset location from the coordinates it would normally use to draw the control. Defaults to false. Useful for staggering a control in from the edge of the grouplist, or for having more space around a control than <c>`<itemgap>`</c> gives.
+| align | Specifies how to align the grouplist. possible values: left, right, center, justify. defaults to left.
+
+
+--------------------------------------------------------------------------------
+\section Group_List_Control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+
+*/
diff --git a/xbmc/guilib/GUIListGroup.h b/xbmc/guilib/GUIListGroup.h
new file mode 100644
index 0000000..548cdec
--- /dev/null
+++ b/xbmc/guilib/GUIListGroup.h
@@ -0,0 +1,50 @@
+/*
+ * 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
+
+/*!
+\file GUIListGroup.h
+\brief
+*/
+
+#include "GUIControlGroup.h"
+
+/*!
+ \ingroup controls
+ \brief a group of controls within a list/panel container
+ */
+class CGUIListGroup final : public CGUIControlGroup
+{
+public:
+ CGUIListGroup(int parentID, int controlID, float posX, float posY, float width, float height);
+ explicit CGUIListGroup(const CGUIListGroup& right);
+ ~CGUIListGroup(void) override;
+ CGUIListGroup* Clone() const override { return new CGUIListGroup(*this); }
+
+ void AddControl(CGUIControl *control, int position = -1) override;
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void ResetAnimation(ANIMATION_TYPE type) override;
+ void UpdateVisibility(const CGUIListItem *item = NULL) override;
+ void UpdateInfo(const CGUIListItem *item) override;
+ void SetInvalid() override;
+
+ void EnlargeWidth(float difference);
+ void EnlargeHeight(float difference);
+ void SetFocusedItem(unsigned int subfocus);
+ unsigned int GetFocusedItem() const;
+ bool MoveLeft();
+ bool MoveRight();
+ void SetState(bool selected, bool focused);
+ void SelectItemFromPoint(const CPoint &point);
+
+protected:
+ const CGUIListItem *m_item;
+};
+
diff --git a/xbmc/guilib/GUIListItem.cpp b/xbmc/guilib/GUIListItem.cpp
new file mode 100644
index 0000000..e815e4d
--- /dev/null
+++ b/xbmc/guilib/GUIListItem.cpp
@@ -0,0 +1,441 @@
+/*
+ * 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 "GUIListItem.h"
+
+#include "GUIListItemLayout.h"
+#include "utils/Archive.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <utility>
+
+bool CGUIListItem::icompare::operator()(const std::string &s1, const std::string &s2) const
+{
+ return StringUtils::CompareNoCase(s1, s2) < 0;
+}
+
+CGUIListItem::CGUIListItem(const CGUIListItem& item)
+{
+ *this = item;
+ SetInvalid();
+}
+
+CGUIListItem::CGUIListItem(void)
+{
+ m_bIsFolder = false;
+ m_bSelected = false;
+ m_overlayIcon = ICON_OVERLAY_NONE;
+ m_currentItem = 1;
+}
+
+CGUIListItem::CGUIListItem(const std::string& strLabel):
+ m_strLabel(strLabel)
+{
+ m_bIsFolder = false;
+ SetSortLabel(strLabel);
+ m_bSelected = false;
+ m_overlayIcon = ICON_OVERLAY_NONE;
+ m_currentItem = 1;
+}
+
+CGUIListItem::~CGUIListItem(void)
+{
+ FreeMemory();
+}
+
+void CGUIListItem::SetLabel(const std::string& strLabel)
+{
+ if (m_strLabel == strLabel)
+ return;
+ m_strLabel = strLabel;
+ if (m_sortLabel.empty())
+ SetSortLabel(strLabel);
+ SetInvalid();
+}
+
+const std::string& CGUIListItem::GetLabel() const
+{
+ return m_strLabel;
+}
+
+
+void CGUIListItem::SetLabel2(const std::string& strLabel2)
+{
+ if (m_strLabel2 == strLabel2)
+ return;
+ m_strLabel2 = strLabel2;
+ SetInvalid();
+}
+
+const std::string& CGUIListItem::GetLabel2() const
+{
+ return m_strLabel2;
+}
+
+void CGUIListItem::SetSortLabel(const std::string &label)
+{
+ g_charsetConverter.utf8ToW(label, m_sortLabel, false);
+ // no need to invalidate - this is never shown in the UI
+}
+
+void CGUIListItem::SetSortLabel(const std::wstring &label)
+{
+ m_sortLabel = label;
+}
+
+const std::wstring& CGUIListItem::GetSortLabel() const
+{
+ return m_sortLabel;
+}
+
+void CGUIListItem::SetArt(const std::string &type, const std::string &url)
+{
+ ArtMap::iterator i = m_art.find(type);
+ if (i == m_art.end() || i->second != url)
+ {
+ m_art[type] = url;
+ SetInvalid();
+ }
+}
+
+void CGUIListItem::SetArt(const ArtMap &art)
+{
+ m_art = art;
+ SetInvalid();
+}
+
+void CGUIListItem::SetArtFallback(const std::string &from, const std::string &to)
+{
+ m_artFallbacks[from] = to;
+}
+
+void CGUIListItem::ClearArt()
+{
+ m_art.clear();
+ m_artFallbacks.clear();
+ SetProperty("libraryartfilled", false);
+}
+
+void CGUIListItem::AppendArt(const ArtMap &art, const std::string &prefix)
+{
+ for (const auto& i : art)
+ SetArt(prefix.empty() ? i.first : prefix + '.' + i.first, i.second);
+}
+
+std::string CGUIListItem::GetArt(const std::string &type) const
+{
+ ArtMap::const_iterator i = m_art.find(type);
+ if (i != m_art.end())
+ return i->second;
+ i = m_artFallbacks.find(type);
+ if (i != m_artFallbacks.end())
+ {
+ ArtMap::const_iterator j = m_art.find(i->second);
+ if (j != m_art.end())
+ return j->second;
+ }
+ return "";
+}
+
+const CGUIListItem::ArtMap &CGUIListItem::GetArt() const
+{
+ return m_art;
+}
+
+bool CGUIListItem::HasArt(const std::string &type) const
+{
+ return !GetArt(type).empty();
+}
+
+void CGUIListItem::SetOverlayImage(GUIIconOverlay icon, bool bOnOff)
+{
+ GUIIconOverlay newIcon = (bOnOff) ? GUIIconOverlay((int)(icon)+1) : icon;
+ if (m_overlayIcon == newIcon)
+ return;
+ m_overlayIcon = newIcon;
+ SetInvalid();
+}
+
+std::string CGUIListItem::GetOverlayImage() const
+{
+ switch (m_overlayIcon)
+ {
+ case ICON_OVERLAY_RAR:
+ return "OverlayRAR.png";
+ case ICON_OVERLAY_ZIP:
+ return "OverlayZIP.png";
+ case ICON_OVERLAY_LOCKED:
+ return "OverlayLocked.png";
+ case ICON_OVERLAY_UNWATCHED:
+ return "OverlayUnwatched.png";
+ case ICON_OVERLAY_WATCHED:
+ return "OverlayWatched.png";
+ case ICON_OVERLAY_HD:
+ return "OverlayHD.png";
+ default:
+ return "";
+ }
+}
+
+void CGUIListItem::Select(bool bOnOff)
+{
+ m_bSelected = bOnOff;
+}
+
+bool CGUIListItem::HasOverlay() const
+{
+ return (m_overlayIcon != CGUIListItem::ICON_OVERLAY_NONE);
+}
+
+bool CGUIListItem::IsSelected() const
+{
+ return m_bSelected;
+}
+
+CGUIListItem& CGUIListItem::operator =(const CGUIListItem& item)
+{
+ if (&item == this) return * this;
+ m_strLabel2 = item.m_strLabel2;
+ m_strLabel = item.m_strLabel;
+ m_sortLabel = item.m_sortLabel;
+ FreeMemory();
+ m_bSelected = item.m_bSelected;
+ m_overlayIcon = item.m_overlayIcon;
+ m_bIsFolder = item.m_bIsFolder;
+ m_mapProperties = item.m_mapProperties;
+ m_art = item.m_art;
+ m_artFallbacks = item.m_artFallbacks;
+ SetInvalid();
+ return *this;
+}
+
+void CGUIListItem::Archive(CArchive &ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_bIsFolder;
+ ar << m_strLabel;
+ ar << m_strLabel2;
+ ar << m_sortLabel;
+ ar << m_bSelected;
+ ar << m_overlayIcon;
+ ar << (int)m_mapProperties.size();
+ for (const auto& it : m_mapProperties)
+ {
+ ar << it.first;
+ ar << it.second;
+ }
+ ar << (int)m_art.size();
+ for (const auto& i : m_art)
+ {
+ ar << i.first;
+ ar << i.second;
+ }
+ ar << (int)m_artFallbacks.size();
+ for (const auto& i : m_artFallbacks)
+ {
+ ar << i.first;
+ ar << i.second;
+ }
+ }
+ else
+ {
+ ar >> m_bIsFolder;
+ ar >> m_strLabel;
+ ar >> m_strLabel2;
+ ar >> m_sortLabel;
+ ar >> m_bSelected;
+
+ int overlayIcon;
+ ar >> overlayIcon;
+ m_overlayIcon = GUIIconOverlay(overlayIcon);
+
+ int mapSize;
+ ar >> mapSize;
+ for (int i = 0; i < mapSize; i++)
+ {
+ std::string key;
+ CVariant value;
+ ar >> key;
+ ar >> value;
+ SetProperty(key, value);
+ }
+ ar >> mapSize;
+ for (int i = 0; i < mapSize; i++)
+ {
+ std::string key, value;
+ ar >> key;
+ ar >> value;
+ m_art.insert(make_pair(key, value));
+ }
+ ar >> mapSize;
+ for (int i = 0; i < mapSize; i++)
+ {
+ std::string key, value;
+ ar >> key;
+ ar >> value;
+ m_artFallbacks.insert(make_pair(key, value));
+ }
+ SetInvalid();
+ }
+}
+void CGUIListItem::Serialize(CVariant &value)
+{
+ value["isFolder"] = m_bIsFolder;
+ value["strLabel"] = m_strLabel;
+ value["strLabel2"] = m_strLabel2;
+ value["sortLabel"] = m_sortLabel;
+ value["selected"] = m_bSelected;
+
+ for (const auto& it : m_mapProperties)
+ {
+ value["properties"][it.first] = it.second;
+ }
+ for (const auto& it : m_art)
+ value["art"][it.first] = it.second;
+}
+
+void CGUIListItem::FreeIcons()
+{
+ FreeMemory();
+ ClearArt();
+ SetInvalid();
+}
+
+void CGUIListItem::FreeMemory(bool immediately)
+{
+ if (m_layout)
+ {
+ m_layout->FreeResources(immediately);
+ m_layout.reset();
+ }
+ if (m_focusedLayout)
+ {
+ m_focusedLayout->FreeResources(immediately);
+ m_focusedLayout.reset();
+ }
+}
+
+void CGUIListItem::SetLayout(CGUIListItemLayoutPtr layout)
+{
+ m_layout = std::move(layout);
+}
+
+CGUIListItemLayout *CGUIListItem::GetLayout()
+{
+ return m_layout.get();
+}
+
+void CGUIListItem::SetFocusedLayout(CGUIListItemLayoutPtr layout)
+{
+ m_focusedLayout = std::move(layout);
+}
+
+CGUIListItemLayout *CGUIListItem::GetFocusedLayout()
+{
+ return m_focusedLayout.get();
+}
+
+void CGUIListItem::SetInvalid()
+{
+ if (m_layout) m_layout->SetInvalid();
+ if (m_focusedLayout) m_focusedLayout->SetInvalid();
+}
+
+void CGUIListItem::SetProperty(const std::string &strKey, const CVariant &value)
+{
+ PropertyMap::iterator iter = m_mapProperties.find(strKey);
+ if (iter == m_mapProperties.end())
+ {
+ m_mapProperties.insert(make_pair(strKey, value));
+ SetInvalid();
+ }
+ else if (iter->second != value)
+ {
+ iter->second = value;
+ SetInvalid();
+ }
+}
+
+const CVariant &CGUIListItem::GetProperty(const std::string &strKey) const
+{
+ PropertyMap::const_iterator iter = m_mapProperties.find(strKey);
+ static CVariant nullVariant = CVariant(CVariant::VariantTypeNull);
+
+ if (iter == m_mapProperties.end())
+ return nullVariant;
+
+ return iter->second;
+}
+
+bool CGUIListItem::HasProperty(const std::string &strKey) const
+{
+ PropertyMap::const_iterator iter = m_mapProperties.find(strKey);
+ if (iter == m_mapProperties.end())
+ return false;
+
+ return true;
+}
+
+void CGUIListItem::ClearProperty(const std::string &strKey)
+{
+ PropertyMap::iterator iter = m_mapProperties.find(strKey);
+ if (iter != m_mapProperties.end())
+ {
+ m_mapProperties.erase(iter);
+ SetInvalid();
+ }
+}
+
+void CGUIListItem::ClearProperties()
+{
+ if (!m_mapProperties.empty())
+ {
+ m_mapProperties.clear();
+ SetInvalid();
+ }
+}
+
+void CGUIListItem::IncrementProperty(const std::string &strKey, int nVal)
+{
+ int64_t i = GetProperty(strKey).asInteger();
+ i += nVal;
+ SetProperty(strKey, i);
+}
+
+void CGUIListItem::IncrementProperty(const std::string& strKey, int64_t nVal)
+{
+ int64_t i = GetProperty(strKey).asInteger();
+ i += nVal;
+ SetProperty(strKey, i);
+}
+
+void CGUIListItem::IncrementProperty(const std::string &strKey, double dVal)
+{
+ double d = GetProperty(strKey).asDouble();
+ d += dVal;
+ SetProperty(strKey, d);
+}
+
+void CGUIListItem::AppendProperties(const CGUIListItem &item)
+{
+ for (const auto& i : item.m_mapProperties)
+ SetProperty(i.first, i.second);
+}
+
+void CGUIListItem::SetCurrentItem(unsigned int position)
+{
+ m_currentItem = position;
+}
+
+unsigned int CGUIListItem::GetCurrentItem() const
+{
+ return m_currentItem;
+}
diff --git a/xbmc/guilib/GUIListItem.h b/xbmc/guilib/GUIListItem.h
new file mode 100644
index 0000000..7c2d0dd
--- /dev/null
+++ b/xbmc/guilib/GUIListItem.h
@@ -0,0 +1,201 @@
+/*
+ * 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
+
+/*!
+\file GUIListItem.h
+\brief
+*/
+
+#include <map>
+#include <memory>
+#include <string>
+
+// Forward
+class CGUIListItemLayout;
+using CGUIListItemLayoutPtr = std::unique_ptr<CGUIListItemLayout>;
+class CArchive;
+class CVariant;
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIListItem
+{
+public:
+ typedef std::map<std::string, std::string> ArtMap;
+
+ ///
+ /// @ingroup controls python_xbmcgui_listitem
+ /// @defgroup kodi_guilib_listitem_iconoverlay Overlay icon types
+ /// @brief Overlay icon types used on list item.
+ /// @{
+ enum GUIIconOverlay { ICON_OVERLAY_NONE = 0, //!< Value **0** - No overlay icon
+ ICON_OVERLAY_RAR, //!< Value **1** - Compressed *.rar files
+ ICON_OVERLAY_ZIP, //!< Value **2** - Compressed *.zip files
+ ICON_OVERLAY_LOCKED, //!< Value **3** - Locked files
+ ICON_OVERLAY_UNWATCHED, //!< Value **4** - For not watched files
+ ICON_OVERLAY_WATCHED, //!< Value **5** - For seen files
+ ICON_OVERLAY_HD //!< Value **6** - Is on hard disk stored
+ };
+ /// @}
+
+ CGUIListItem(void);
+ explicit CGUIListItem(const CGUIListItem& item);
+ explicit CGUIListItem(const std::string& strLabel);
+ virtual ~CGUIListItem(void);
+ virtual CGUIListItem* Clone() const { return new CGUIListItem(*this); }
+
+ CGUIListItem& operator =(const CGUIListItem& item);
+
+ virtual void SetLabel(const std::string& strLabel);
+ const std::string& GetLabel() const;
+
+ void SetLabel2(const std::string& strLabel);
+ const std::string& GetLabel2() const;
+
+ void SetOverlayImage(GUIIconOverlay icon, bool bOnOff=false);
+ std::string GetOverlayImage() const;
+
+ /*! \brief Set a particular art type for an item
+ \param type type of art to set.
+ \param url the url of the art.
+ */
+ void SetArt(const std::string &type, const std::string &url);
+
+ /*! \brief set artwork for an item
+ \param art a type:url map for artwork
+ \sa GetArt
+ */
+ void SetArt(const ArtMap &art);
+
+ /*! \brief append artwork to an item
+ \param art a type:url map for artwork
+ \param prefix a prefix for the art, if applicable.
+ \sa GetArt
+ */
+ void AppendArt(const ArtMap &art, const std::string &prefix = "");
+
+ /*! \brief set a fallback image for art
+ \param from the type to fallback from
+ \param to the type to fallback to
+ \sa SetArt
+ */
+ void SetArtFallback(const std::string &from, const std::string &to);
+
+ /*! \brief clear art on an item
+ \sa SetArt
+ */
+ void ClearArt();
+
+ /*! \brief Get a particular art type for an item
+ \param type type of art to fetch.
+ \return the art URL, if available, else empty.
+ */
+ std::string GetArt(const std::string &type) const;
+
+ /*! \brief get artwork for an item
+ Retrieves artwork in a type:url map
+ \return a type:url map for artwork
+ \sa SetArt
+ */
+ const ArtMap &GetArt() const;
+
+ /*! \brief Check whether an item has a particular piece of art
+ Equivalent to !GetArt(type).empty()
+ \param type type of art to set.
+ \return true if the item has that art set, false otherwise.
+ */
+ bool HasArt(const std::string &type) const;
+
+ void SetSortLabel(const std::string &label);
+ void SetSortLabel(const std::wstring &label);
+ const std::wstring &GetSortLabel() const;
+
+ void Select(bool bOnOff);
+ bool IsSelected() const;
+
+ bool HasOverlay() const;
+ virtual bool IsFileItem() const { return false; }
+
+ void SetLayout(CGUIListItemLayoutPtr layout);
+ CGUIListItemLayout *GetLayout();
+
+ void SetFocusedLayout(CGUIListItemLayoutPtr layout);
+ CGUIListItemLayout *GetFocusedLayout();
+
+ void FreeIcons();
+ void FreeMemory(bool immediately = false);
+ void SetInvalid();
+
+ bool m_bIsFolder; ///< is item a folder or a file
+
+ void SetProperty(const std::string &strKey, const CVariant &value);
+
+ void IncrementProperty(const std::string &strKey, int nVal);
+ void IncrementProperty(const std::string& strKey, int64_t nVal);
+ void IncrementProperty(const std::string &strKey, double dVal);
+
+ void ClearProperties();
+
+ /*! \brief Append the properties of one CGUIListItem to another.
+ Any existing properties in the current item will be overridden if they
+ are set in the passed in item.
+ \param item the item containing the properties to append.
+ */
+ void AppendProperties(const CGUIListItem &item);
+
+ void Archive(CArchive& ar);
+ void Serialize(CVariant& value);
+
+ bool HasProperty(const std::string &strKey) const;
+ bool HasProperties() const { return !m_mapProperties.empty(); }
+ void ClearProperty(const std::string &strKey);
+
+ const CVariant &GetProperty(const std::string &strKey) const;
+
+ /*! \brief Set the current item number within it's container
+ Our container classes will set this member with the items position
+ in the container starting at 1.
+ \param position Position of the item in the container starting at 1.
+ */
+ void SetCurrentItem(unsigned int position);
+
+ /*! \brief Get the current item number within it's container
+ Retrieve the items position in a container, this is useful to show
+ for example numbering in front of entities in an arbitrary list of entities,
+ like songs of a playlist.
+ */
+ unsigned int GetCurrentItem() const;
+
+protected:
+ std::string m_strLabel2; // text of column2
+ GUIIconOverlay m_overlayIcon; // type of overlay icon
+
+ CGUIListItemLayoutPtr m_layout;
+ CGUIListItemLayoutPtr m_focusedLayout;
+ bool m_bSelected; // item is selected or not
+ unsigned int m_currentItem; // current item number within container (starting at 1)
+
+ struct icompare
+ {
+ bool operator()(const std::string &s1, const std::string &s2) const;
+ };
+
+ typedef std::map<std::string, CVariant, icompare> PropertyMap;
+ PropertyMap m_mapProperties;
+private:
+ std::wstring m_sortLabel; // text for sorting. Need to be UTF16 for proper sorting
+ std::string m_strLabel; // text of column1
+
+ ArtMap m_art;
+ ArtMap m_artFallbacks;
+};
+
diff --git a/xbmc/guilib/GUIListItemLayout.cpp b/xbmc/guilib/GUIListItemLayout.cpp
new file mode 100644
index 0000000..3b0f774
--- /dev/null
+++ b/xbmc/guilib/GUIListItemLayout.cpp
@@ -0,0 +1,245 @@
+/*
+ * 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 "GUIListItemLayout.h"
+
+#include "FileItem.h"
+#include "GUIControlFactory.h"
+#include "GUIImage.h"
+#include "GUIInfoManager.h"
+#include "GUIListLabel.h"
+#include "utils/XBMCTinyXML.h"
+
+using namespace KODI::GUILIB;
+
+CGUIListItemLayout::CGUIListItemLayout()
+: m_group(0, 0, 0, 0, 0, 0)
+{
+ m_group.SetPushUpdates(true);
+}
+
+CGUIListItemLayout::CGUIListItemLayout(const CGUIListItemLayout& from)
+ : CGUIListItemLayout(from, nullptr)
+{
+}
+
+CGUIListItemLayout::CGUIListItemLayout(const CGUIListItemLayout& from, CGUIControl* control)
+ : m_group(from.m_group),
+ m_width(from.m_width),
+ m_height(from.m_height),
+ m_focused(from.m_focused),
+ m_condition(from.m_condition),
+ m_isPlaying(from.m_isPlaying),
+ m_infoUpdateMillis(from.m_infoUpdateMillis)
+{
+ m_group.SetParentControl(control);
+ m_infoUpdateTimeout.Set(m_infoUpdateMillis);
+
+ // m_group was just created, cloned controls with resources must be allocated
+ // before use
+ m_group.AllocResources();
+}
+
+bool CGUIListItemLayout::IsAnimating(ANIMATION_TYPE animType)
+{
+ return m_group.IsAnimating(animType);
+}
+
+void CGUIListItemLayout::ResetAnimation(ANIMATION_TYPE animType)
+{
+ return m_group.ResetAnimation(animType);
+}
+
+float CGUIListItemLayout::Size(ORIENTATION orientation) const
+{
+ return (orientation == HORIZONTAL) ? m_width : m_height;
+}
+
+void CGUIListItemLayout::Process(CGUIListItem *item, int parentID, unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_invalidated)
+ { // need to update our item
+ m_invalidated = false;
+ // could use a dynamic cast here if RTTI was enabled. As it's not,
+ // let's use a static cast with a virtual base function
+ CFileItem *fileItem = item->IsFileItem() ? static_cast<CFileItem*>(item) : new CFileItem(*item);
+ m_isPlaying.Update(INFO::DEFAULT_CONTEXT, item);
+ m_group.SetInvalid();
+ m_group.UpdateInfo(fileItem);
+ // delete our temporary fileitem
+ if (!item->IsFileItem())
+ delete fileItem;
+
+ m_infoUpdateTimeout.Set(m_infoUpdateMillis);
+ }
+ else if (m_infoUpdateTimeout.IsTimePast())
+ {
+ m_isPlaying.Update(INFO::DEFAULT_CONTEXT, item);
+ m_group.UpdateInfo(item);
+
+ m_infoUpdateTimeout.Set(m_infoUpdateMillis);
+ }
+
+ // update visibility, and render
+ m_group.SetState(item->IsSelected() || m_isPlaying, m_focused);
+ m_group.UpdateVisibility(item);
+ m_group.DoProcess(currentTime, dirtyregions);
+}
+
+void CGUIListItemLayout::Render(CGUIListItem *item, int parentID)
+{
+ m_group.DoRender();
+}
+
+void CGUIListItemLayout::SetFocusedItem(unsigned int focus)
+{
+ m_group.SetFocusedItem(focus);
+}
+
+unsigned int CGUIListItemLayout::GetFocusedItem() const
+{
+ return m_group.GetFocusedItem();
+}
+
+void CGUIListItemLayout::SetWidth(float width)
+{
+ if (m_width != width)
+ {
+ m_group.EnlargeWidth(width - m_width);
+ m_width = width;
+ SetInvalid();
+ }
+}
+
+void CGUIListItemLayout::SetHeight(float height)
+{
+ if (m_height != height)
+ {
+ m_group.EnlargeHeight(height - m_height);
+ m_height = height;
+ SetInvalid();
+ }
+}
+
+void CGUIListItemLayout::SelectItemFromPoint(const CPoint &point)
+{
+ m_group.SelectItemFromPoint(point);
+}
+
+bool CGUIListItemLayout::MoveLeft()
+{
+ return m_group.MoveLeft();
+}
+
+bool CGUIListItemLayout::MoveRight()
+{
+ return m_group.MoveRight();
+}
+
+bool CGUIListItemLayout::CheckCondition()
+{
+ return !m_condition || m_condition->Get(INFO::DEFAULT_CONTEXT);
+}
+
+void CGUIListItemLayout::LoadControl(TiXmlElement *child, CGUIControlGroup *group)
+{
+ if (!group) return;
+
+ CRect rect(group->GetXPosition(), group->GetYPosition(), group->GetXPosition() + group->GetWidth(), group->GetYPosition() + group->GetHeight());
+
+ CGUIControlFactory factory;
+ CGUIControl *control = factory.Create(0, rect, child, true); // true indicating we're inside a list for the
+ // different label control + defaults.
+ if (control)
+ {
+ group->AddControl(control);
+ if (control->IsGroup())
+ {
+ TiXmlElement *grandChild = child->FirstChildElement("control");
+ while (grandChild)
+ {
+ LoadControl(grandChild, static_cast<CGUIControlGroup*>(control));
+ grandChild = grandChild->NextSiblingElement("control");
+ }
+ }
+ }
+}
+
+void CGUIListItemLayout::LoadLayout(TiXmlElement *layout, int context, bool focused, float maxWidth, float maxHeight)
+{
+ m_focused = focused;
+ layout->QueryFloatAttribute("width", &m_width);
+ layout->QueryFloatAttribute("height", &m_height);
+ const char *condition = layout->Attribute("condition");
+ if (condition)
+ m_condition = CServiceBroker::GetGUI()->GetInfoManager().Register(condition, context);
+ unsigned int infoupdatemillis = 0;
+ layout->QueryUnsignedAttribute("infoupdate", &infoupdatemillis);
+ if (infoupdatemillis > 0)
+ m_infoUpdateMillis = std::chrono::milliseconds(infoupdatemillis);
+
+ m_infoUpdateTimeout.Set(m_infoUpdateMillis);
+ m_isPlaying.Parse("listitem.isplaying", context);
+ // ensure width and height are valid
+ if (!m_width)
+ m_width = maxWidth;
+ if (!m_height)
+ m_height = maxHeight;
+ m_width = std::max(1.0f, m_width);
+ m_height = std::max(1.0f, m_height);
+ m_group.SetWidth(m_width);
+ m_group.SetHeight(m_height);
+
+ TiXmlElement *child = layout->FirstChildElement("control");
+ while (child)
+ {
+ LoadControl(child, &m_group);
+ child = child->NextSiblingElement("control");
+ }
+}
+
+//#ifdef GUILIB_PYTHON_COMPATIBILITY
+void CGUIListItemLayout::CreateListControlLayouts(float width, float height, bool focused, const CLabelInfo &labelInfo, const CLabelInfo &labelInfo2, const CTextureInfo &texture, const CTextureInfo &textureFocus, float texHeight, float iconWidth, float iconHeight, const std::string &nofocusCondition, const std::string &focusCondition)
+{
+ m_width = width;
+ m_height = height;
+ m_focused = focused;
+ m_isPlaying.Parse("listitem.isplaying", 0);
+ CGUIImage *tex = new CGUIImage(0, 0, 0, 0, width, texHeight, texture);
+ tex->SetVisibleCondition(nofocusCondition);
+ m_group.AddControl(tex);
+ if (focused)
+ {
+ CGUIImage *tex = new CGUIImage(0, 0, 0, 0, width, texHeight, textureFocus);
+ tex->SetVisibleCondition(focusCondition);
+ m_group.AddControl(tex);
+ }
+ CGUIImage *image = new CGUIImage(0, 0, 8, 0, iconWidth, texHeight, CTextureInfo(""));
+ image->SetInfo(GUIINFO::CGUIInfoLabel("$INFO[ListItem.Icon]", "", m_group.GetParentID()));
+ image->SetAspectRatio(CAspectRatio::AR_KEEP);
+ m_group.AddControl(image);
+ float x = iconWidth + labelInfo.offsetX + 10;
+ CGUIListLabel *label = new CGUIListLabel(0, 0, x, labelInfo.offsetY, width - x - 18, height, labelInfo, GUIINFO::CGUIInfoLabel("$INFO[ListItem.Label]", "", m_group.GetParentID()), CGUIControl::FOCUS);
+ m_group.AddControl(label);
+ x = labelInfo2.offsetX ? labelInfo2.offsetX : m_width - 16;
+ label = new CGUIListLabel(0, 0, x, labelInfo2.offsetY, x - iconWidth - 20, height, labelInfo2, GUIINFO::CGUIInfoLabel("$INFO[ListItem.Label2]", "", m_group.GetParentID()), CGUIControl::FOCUS);
+ m_group.AddControl(label);
+}
+//#endif
+
+void CGUIListItemLayout::FreeResources(bool immediately)
+{
+ m_group.FreeResources(immediately);
+}
+
+#ifdef _DEBUG
+void CGUIListItemLayout::DumpTextureUse()
+{
+ m_group.DumpTextureUse();
+}
+#endif
diff --git a/xbmc/guilib/GUIListItemLayout.h b/xbmc/guilib/GUIListItemLayout.h
new file mode 100644
index 0000000..69d4c4f
--- /dev/null
+++ b/xbmc/guilib/GUIListItemLayout.h
@@ -0,0 +1,68 @@
+/*
+ * 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 "GUIListGroup.h"
+#include "GUITexture.h"
+#include "guilib/guiinfo/GUIInfoLabel.h"
+#include "threads/SystemClock.h"
+
+class CGUIListItem;
+class CFileItem;
+class CLabelInfo;
+
+class CGUIListItemLayout final
+{
+public:
+ CGUIListItemLayout();
+ explicit CGUIListItemLayout(const CGUIListItemLayout& from);
+ explicit CGUIListItemLayout(const CGUIListItemLayout& from, CGUIControl* control);
+ void LoadLayout(TiXmlElement *layout, int context, bool focused, float maxWidth, float maxHeight);
+ void Process(CGUIListItem *item, int parentID, unsigned int currentTime, CDirtyRegionList &dirtyregions);
+ void Render(CGUIListItem *item, int parentID);
+ float Size(ORIENTATION orientation) const;
+ unsigned int GetFocusedItem() const;
+ void SetFocusedItem(unsigned int focus);
+ bool IsAnimating(ANIMATION_TYPE animType);
+ void ResetAnimation(ANIMATION_TYPE animType);
+ void SetInvalid() { m_invalidated = true; }
+ void FreeResources(bool immediately = false);
+ void SetParentControl(CGUIControl* control) { m_group.SetParentControl(control); }
+
+ //#ifdef GUILIB_PYTHON_COMPATIBILITY
+ void CreateListControlLayouts(float width, float height, bool focused, const CLabelInfo &labelInfo, const CLabelInfo &labelInfo2, const CTextureInfo &texture, const CTextureInfo &textureFocus, float texHeight, float iconWidth, float iconHeight, const std::string &nofocusCondition, const std::string &focusCondition);
+//#endif
+
+ void SetWidth(float width);
+ void SetHeight(float height);
+ void SelectItemFromPoint(const CPoint &point);
+ bool MoveLeft();
+ bool MoveRight();
+
+#ifdef _DEBUG
+ void DumpTextureUse();
+#endif
+ bool CheckCondition();
+protected:
+ void LoadControl(TiXmlElement *child, CGUIControlGroup *group);
+
+ CGUIListGroup m_group;
+
+ float m_width{0};
+ float m_height{0};
+ bool m_focused{false};
+ bool m_invalidated{true};
+
+ INFO::InfoPtr m_condition;
+ KODI::GUILIB::GUIINFO::CGUIInfoBool m_isPlaying;
+ std::chrono::milliseconds m_infoUpdateMillis =
+ XbmcThreads::EndTime<decltype(m_infoUpdateMillis)>::Max();
+ XbmcThreads::EndTime<> m_infoUpdateTimeout;
+};
+
diff --git a/xbmc/guilib/GUIListLabel.cpp b/xbmc/guilib/GUIListLabel.cpp
new file mode 100644
index 0000000..b3138eb
--- /dev/null
+++ b/xbmc/guilib/GUIListLabel.cpp
@@ -0,0 +1,106 @@
+/*
+ * 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 "GUIListLabel.h"
+
+#include "addons/Skin.h"
+
+#include <limits>
+
+using namespace KODI::GUILIB;
+
+CGUIListLabel::CGUIListLabel(int parentID, int controlID, float posX, float posY, float width, float height,
+ const CLabelInfo& labelInfo, const GUIINFO::CGUIInfoLabel &info, CGUIControl::GUISCROLLVALUE scroll)
+ : CGUIControl(parentID, controlID, posX, posY, width, height)
+ , m_label(posX, posY, width, height, labelInfo, (scroll == CGUIControl::ALWAYS) ? CGUILabel::OVER_FLOW_SCROLL : CGUILabel::OVER_FLOW_TRUNCATE)
+ , m_info(info)
+{
+ m_scroll = scroll;
+ if (m_info.IsConstant())
+ SetLabel(m_info.GetLabel(m_parentID, true));
+ m_label.SetScrollLoopCount(2);
+ ControlType = GUICONTROL_LISTLABEL;
+}
+
+CGUIListLabel::~CGUIListLabel(void) = default;
+
+void CGUIListLabel::SetSelected(bool selected)
+{
+ if(m_label.SetColor(selected ? CGUILabel::COLOR_SELECTED : CGUILabel::COLOR_TEXT))
+ SetInvalid();
+}
+
+void CGUIListLabel::SetFocus(bool focus)
+{
+ CGUIControl::SetFocus(focus);
+ if (m_scroll == CGUIControl::FOCUS)
+ {
+ m_label.SetScrolling(focus);
+ }
+}
+
+CRect CGUIListLabel::CalcRenderRegion() const
+{
+ return m_label.GetRenderRect();
+}
+
+bool CGUIListLabel::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_label.UpdateColors();
+
+ return changed;
+}
+
+void CGUIListLabel::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_label.Process(currentTime))
+ MarkDirtyRegion();
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIListLabel::Render()
+{
+ m_label.Render();
+ CGUIControl::Render();
+}
+
+void CGUIListLabel::UpdateInfo(const CGUIListItem *item)
+{
+ if (m_info.IsConstant() && !m_bInvalidated)
+ return; // nothing to do
+
+ if (item)
+ SetLabel(m_info.GetItemLabel(item));
+ else
+ SetLabel(m_info.GetLabel(m_parentID, true));
+}
+
+void CGUIListLabel::SetInvalid()
+{
+ m_label.SetInvalid();
+ CGUIControl::SetInvalid();
+}
+
+void CGUIListLabel::SetWidth(float width)
+{
+ m_width = width;
+ if (m_label.GetLabelInfo().align & XBFONT_RIGHT)
+ m_label.SetMaxRect(m_posX - m_width, m_posY, m_width, m_height);
+ else if (m_label.GetLabelInfo().align & XBFONT_CENTER_X)
+ m_label.SetMaxRect(m_posX - m_width*0.5f, m_posY, m_width, m_height);
+ else
+ m_label.SetMaxRect(m_posX, m_posY, m_width, m_height);
+ CGUIControl::SetWidth(m_width);
+}
+
+void CGUIListLabel::SetLabel(const std::string &label)
+{
+ m_label.SetText(label);
+}
diff --git a/xbmc/guilib/GUIListLabel.h b/xbmc/guilib/GUIListLabel.h
new file mode 100644
index 0000000..64db806
--- /dev/null
+++ b/xbmc/guilib/GUIListLabel.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
+
+/*!
+\file GUIListLabel.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUILabel.h"
+#include "guilib/guiinfo/GUIInfoLabel.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIListLabel :
+ public CGUIControl
+{
+public:
+ CGUIListLabel(int parentID, int controlID, float posX, float posY, float width, float height,
+ const CLabelInfo& labelInfo, const KODI::GUILIB::GUIINFO::CGUIInfoLabel &label, CGUIControl::GUISCROLLVALUE scroll);
+ ~CGUIListLabel(void) override;
+ CGUIListLabel* Clone() const override { return new CGUIListLabel(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool CanFocus() const override { return false; }
+ void UpdateInfo(const CGUIListItem *item = NULL) override;
+ void SetFocus(bool focus) override;
+ void SetInvalid() override;
+ void SetWidth(float width) override;
+
+ void SetLabel(const std::string &label);
+ void SetSelected(bool selected);
+
+ static void CheckAndCorrectOverlap(CGUIListLabel &label1, CGUIListLabel &label2)
+ {
+ CGUILabel::CheckAndCorrectOverlap(label1.m_label, label2.m_label);
+ }
+
+ CRect CalcRenderRegion() const override;
+
+protected:
+ bool UpdateColors(const CGUIListItem* item) override;
+
+ CGUILabel m_label;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_info;
+ CGUIControl::GUISCROLLVALUE m_scroll;
+};
diff --git a/xbmc/guilib/GUIMessage.cpp b/xbmc/guilib/GUIMessage.cpp
new file mode 100644
index 0000000..fb6513f
--- /dev/null
+++ b/xbmc/guilib/GUIMessage.cpp
@@ -0,0 +1,146 @@
+/*
+ * 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 "GUIMessage.h"
+
+#include "LocalizeStrings.h"
+
+std::string CGUIMessage::empty_string;
+
+CGUIMessage::CGUIMessage(int msg, int senderID, int controlID, int param1, int param2)
+{
+ m_message = msg;
+ m_senderID = senderID;
+ m_controlID = controlID;
+ m_param1 = param1;
+ m_param2 = param2;
+ m_pointer = NULL;
+}
+
+CGUIMessage::CGUIMessage(int msg, int senderID, int controlID, int param1, int param2, CFileItemList *item)
+{
+ m_message = msg;
+ m_senderID = senderID;
+ m_controlID = controlID;
+ m_param1 = param1;
+ m_param2 = param2;
+ m_pointer = item;
+}
+
+CGUIMessage::CGUIMessage(
+ int msg, int senderID, int controlID, int param1, int param2, const CGUIListItemPtr& item)
+ : m_item(item)
+{
+ m_message = msg;
+ m_senderID = senderID;
+ m_controlID = controlID;
+ m_param1 = param1;
+ m_param2 = param2;
+ m_pointer = NULL;
+}
+
+CGUIMessage::CGUIMessage(const CGUIMessage& msg) = default;
+
+CGUIMessage::~CGUIMessage(void) = default;
+
+
+int CGUIMessage::GetControlId() const
+{
+ return m_controlID;
+}
+
+int CGUIMessage::GetMessage() const
+{
+ return m_message;
+}
+
+void* CGUIMessage::GetPointer() const
+{
+ return m_pointer;
+}
+
+CGUIListItemPtr CGUIMessage::GetItem() const
+{
+ return m_item;
+}
+
+int CGUIMessage::GetParam1() const
+{
+ return m_param1;
+}
+
+int CGUIMessage::GetParam2() const
+{
+ return m_param2;
+}
+
+int CGUIMessage::GetSenderId() const
+{
+ return m_senderID;
+}
+
+CGUIMessage& CGUIMessage::operator = (const CGUIMessage& msg) = default;
+
+void CGUIMessage::SetParam1(int param1)
+{
+ m_param1 = param1;
+}
+
+void CGUIMessage::SetParam2(int param2)
+{
+ m_param2 = param2;
+}
+
+void CGUIMessage::SetPointer(void* lpVoid)
+{
+ m_pointer = lpVoid;
+}
+
+void CGUIMessage::SetLabel(const std::string& strLabel)
+{
+ m_strLabel = strLabel;
+}
+
+const std::string& CGUIMessage::GetLabel() const
+{
+ return m_strLabel;
+}
+
+void CGUIMessage::SetLabel(int iString)
+{
+ m_strLabel = g_localizeStrings.Get(iString);
+}
+
+void CGUIMessage::SetStringParam(const std::string& strParam)
+{
+ m_params.clear();
+ if (strParam.size())
+ m_params.push_back(strParam);
+}
+
+void CGUIMessage::SetStringParams(const std::vector<std::string> &params)
+{
+ m_params = params;
+}
+
+const std::string& CGUIMessage::GetStringParam(size_t param) const
+{
+ if (param >= m_params.size())
+ return empty_string;
+ return m_params[param];
+}
+
+size_t CGUIMessage::GetNumStringParams() const
+{
+ return m_params.size();
+}
+
+void CGUIMessage::SetItem(CGUIListItemPtr item)
+{
+ m_item = std::move(item);
+}
diff --git a/xbmc/guilib/GUIMessage.h b/xbmc/guilib/GUIMessage.h
new file mode 100644
index 0000000..2fc6702
--- /dev/null
+++ b/xbmc/guilib/GUIMessage.h
@@ -0,0 +1,407 @@
+/*
+ * 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
+
+/*!
+\file GUIMessage.h
+\brief
+*/
+
+constexpr const int GUI_MSG_WINDOW_INIT = 1; // initialize window
+constexpr const int GUI_MSG_WINDOW_DEINIT = 2; // deinit window
+constexpr const int GUI_MSG_WINDOW_RESET = 27; // reset window to initial state
+
+constexpr const int GUI_MSG_SETFOCUS = 3; // set focus to control param1=up/down/left/right
+constexpr const int GUI_MSG_LOSTFOCUS = 4; // control lost focus
+
+constexpr const int GUI_MSG_CLICKED = 5; // control has been clicked
+
+constexpr const int GUI_MSG_VISIBLE = 6; // set control visible
+constexpr const int GUI_MSG_HIDDEN = 7; // set control hidden
+
+constexpr const int GUI_MSG_ENABLED = 8; // enable control
+constexpr const int GUI_MSG_DISABLED = 9; // disable control
+
+constexpr const int GUI_MSG_SET_SELECTED = 10; // control = selected
+constexpr const int GUI_MSG_SET_DESELECTED = 11; // control = not selected
+
+constexpr const int GUI_MSG_LABEL_ADD = 12; // add label control (for controls supporting more then 1 label)
+
+constexpr const int GUI_MSG_LABEL_SET = 13; // set the label of a control
+
+constexpr const int GUI_MSG_LABEL_RESET = 14; // clear all labels of a control // add label control (for controls supporting more then 1 label)
+
+constexpr const int GUI_MSG_ITEM_SELECTED = 15; // ask control 2 return the selected item
+constexpr const int GUI_MSG_ITEM_SELECT = 16; // ask control 2 select a specific item
+constexpr const int GUI_MSG_LABEL2_SET = 17;
+constexpr const int GUI_MSG_SHOWRANGE = 18;
+
+constexpr const int GUI_MSG_FULLSCREEN = 19; // should go to fullscreen window (vis or video)
+constexpr const int GUI_MSG_EXECUTE = 20; // user has clicked on a button with <execute> tag
+
+constexpr const int GUI_MSG_NOTIFY_ALL = 21; // message will be send to all active and inactive(!) windows, all active modal and modeless dialogs
+ // dwParam1 must contain an additional message the windows should react on
+
+constexpr const int GUI_MSG_REFRESH_THUMBS = 22; // message is sent to all windows to refresh all thumbs
+
+constexpr const int GUI_MSG_MOVE = 23; // message is sent to the window from the base control class when it's
+ // been asked to move. dwParam1 contains direction.
+
+constexpr const int GUI_MSG_LABEL_BIND = 24; // bind label control (for controls supporting more then 1 label)
+
+constexpr const int GUI_MSG_FOCUSED = 26; // a control has become focused
+
+constexpr const int GUI_MSG_PAGE_CHANGE = 28; // a page control has changed the page number
+
+constexpr const int GUI_MSG_REFRESH_LIST = 29; // message sent to all listing controls telling them to refresh their item layouts
+
+constexpr const int GUI_MSG_PAGE_UP = 30; // page up
+constexpr const int GUI_MSG_PAGE_DOWN = 31; // page down
+constexpr const int GUI_MSG_MOVE_OFFSET = 32; // Instruct the control to MoveUp or MoveDown by offset amount
+
+constexpr const int GUI_MSG_SET_TYPE = 33; ///< Instruct a control to set it's type appropriately
+
+/*!
+ \brief Message indicating the window has been resized
+ Any controls that keep stored sizing information based on aspect ratio or window size should
+ recalculate sizing information
+ */
+constexpr const int GUI_MSG_WINDOW_RESIZE = 34;
+
+/*!
+ \brief Message indicating loss of renderer, prior to reset
+ Any controls that keep shared resources should free them on receipt of this message, as the renderer
+ is about to be reset.
+ */
+constexpr const int GUI_MSG_RENDERER_LOST = 35;
+
+/*!
+ \brief Message indicating regain of renderer, after reset
+ Any controls that keep shared resources may reallocate them now that the renderer is back
+ */
+constexpr const int GUI_MSG_RENDERER_RESET = 36;
+
+/*!
+ \brief A control wishes to have (or release) exclusive access to mouse actions
+ */
+constexpr const int GUI_MSG_EXCLUSIVE_MOUSE = 37;
+
+/*!
+ \brief A request for supported gestures is made
+ */
+constexpr const int GUI_MSG_GESTURE_NOTIFY = 38;
+
+/*!
+ \brief A request to add a control
+ */
+constexpr const int GUI_MSG_ADD_CONTROL = 39;
+
+/*!
+ \brief A request to remove a control
+ */
+constexpr const int GUI_MSG_REMOVE_CONTROL = 40;
+
+/*!
+ \brief A request to unfocus all currently focused controls
+ */
+constexpr const int GUI_MSG_UNFOCUS_ALL = 41;
+
+constexpr const int GUI_MSG_SET_TEXT = 42;
+
+constexpr const int GUI_MSG_WINDOW_LOAD = 43;
+
+constexpr const int GUI_MSG_VALIDITY_CHANGED = 44;
+
+/*!
+ \brief Check whether a button is selected
+ */
+constexpr const int GUI_MSG_IS_SELECTED = 45;
+
+/*!
+ \brief Bind a set of labels to a spin (or similar) control
+ */
+constexpr const int GUI_MSG_SET_LABELS = 46;
+
+/*!
+ \brief Set the filename for an image control
+ */
+constexpr const int GUI_MSG_SET_FILENAME = 47;
+
+/*!
+ \brief Get the filename of an image control
+ */
+
+constexpr const int GUI_MSG_GET_FILENAME = 48;
+
+/*!
+ \brief The user interface is ready for usage
+ */
+constexpr const int GUI_MSG_UI_READY = 49;
+
+ /*!
+ \brief Called every 500ms to allow time dependent updates
+ */
+constexpr const int GUI_MSG_REFRESH_TIMER = 50;
+
+ /*!
+ \brief Called if state has changed which could lead to GUI changes
+ */
+constexpr const int GUI_MSG_STATE_CHANGED = 51;
+
+/*!
+ \brief Called when a subtitle download has finished
+ */
+constexpr const int GUI_MSG_SUBTITLE_DOWNLOADED = 52;
+
+
+constexpr const int GUI_MSG_USER = 1000;
+
+/*!
+\brief Complete to get codingtable page
+*/
+constexpr const int GUI_MSG_CODINGTABLE_LOOKUP_COMPLETED = 65000;
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+#define CONTROL_SELECT(controlID) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_SET_SELECTED, GetID(), controlID); \
+ OnMessage(_msg); \
+ } while (0)
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+#define CONTROL_DESELECT(controlID) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_SET_DESELECTED, GetID(), controlID); \
+ OnMessage(_msg); \
+ } while (0)
+
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+#define CONTROL_ENABLE(controlID) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_ENABLED, GetID(), controlID); \
+ OnMessage(_msg); \
+ } while (0)
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+#define CONTROL_DISABLE(controlID) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_DISABLED, GetID(), controlID); \
+ OnMessage(_msg); \
+ } while (0)
+
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+#define CONTROL_ENABLE_ON_CONDITION(controlID, bCondition) \
+ do \
+ { \
+ CGUIMessage _msg(bCondition ? GUI_MSG_ENABLED : GUI_MSG_DISABLED, GetID(), controlID); \
+ OnMessage(_msg); \
+ } while (0)
+
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+#define CONTROL_SELECT_ITEM(controlID, iItem) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_ITEM_SELECT, GetID(), controlID, iItem); \
+ OnMessage(_msg); \
+ } while (0)
+
+/*!
+ \ingroup winmsg
+ \brief Set the label of the current control
+ */
+#define SET_CONTROL_LABEL(controlID, label) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_LABEL_SET, GetID(), controlID); \
+ _msg.SetLabel(label); \
+ OnMessage(_msg); \
+ } while (0)
+
+/*!
+ \ingroup winmsg
+ \brief Set the label of the current control
+ */
+#define SET_CONTROL_LABEL_THREAD_SAFE(controlID, label) \
+ { \
+ CGUIMessage _msg(GUI_MSG_LABEL_SET, GetID(), controlID); \
+ _msg.SetLabel(label); \
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread()) \
+ OnMessage(_msg); \
+ else \
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(_msg, GetID()); \
+ }
+
+/*!
+ \ingroup winmsg
+ \brief Set the second label of the current control
+ */
+#define SET_CONTROL_LABEL2(controlID, label) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_LABEL2_SET, GetID(), controlID); \
+ _msg.SetLabel(label); \
+ OnMessage(_msg); \
+ } while (0)
+
+/*!
+ \ingroup winmsg
+ \brief Set a bunch of labels on the given control
+ */
+#define SET_CONTROL_LABELS(controlID, defaultValue, labels) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_SET_LABELS, GetID(), controlID, defaultValue); \
+ _msg.SetPointer(labels); \
+ OnMessage(_msg); \
+ } while (0)
+
+/*!
+ \ingroup winmsg
+ \brief Set the label of the current control
+ */
+#define SET_CONTROL_FILENAME(controlID, label) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_SET_FILENAME, GetID(), controlID); \
+ _msg.SetLabel(label); \
+ OnMessage(_msg); \
+ } while (0)
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+#define SET_CONTROL_HIDDEN(controlID) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_HIDDEN, GetID(), controlID); \
+ OnMessage(_msg); \
+ } while (0)
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+#define SET_CONTROL_FOCUS(controlID, dwParam) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_SETFOCUS, GetID(), controlID, dwParam); \
+ OnMessage(_msg); \
+ } while (0)
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+#define SET_CONTROL_VISIBLE(controlID) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_VISIBLE, GetID(), controlID); \
+ OnMessage(_msg); \
+ } while (0)
+
+#define SET_CONTROL_SELECTED(dwSenderId, controlID, bSelect) \
+ do \
+ { \
+ CGUIMessage _msg(bSelect ? GUI_MSG_SET_SELECTED : GUI_MSG_SET_DESELECTED, dwSenderId, \
+ controlID); \
+ OnMessage(_msg); \
+ } while (0)
+
+/*!
+\ingroup winmsg
+\brief Click message sent from controls to windows.
+ */
+#define SEND_CLICK_MESSAGE(id, parentID, action) \
+ do \
+ { \
+ CGUIMessage _msg(GUI_MSG_CLICKED, id, parentID, action); \
+ SendWindowMessage(_msg); \
+ } while (0)
+
+#include <string>
+#include <vector>
+#include <memory>
+
+// forwards
+class CGUIListItem; typedef std::shared_ptr<CGUIListItem> CGUIListItemPtr;
+class CFileItemList;
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+class CGUIMessage final
+{
+public:
+ CGUIMessage(int dwMsg, int senderID, int controlID, int param1 = 0, int param2 = 0);
+ CGUIMessage(int msg, int senderID, int controlID, int param1, int param2, CFileItemList* item);
+ CGUIMessage(int msg, int senderID, int controlID, int param1, int param2, const CGUIListItemPtr &item);
+ CGUIMessage(const CGUIMessage& msg);
+ ~CGUIMessage(void);
+ CGUIMessage& operator = (const CGUIMessage& msg);
+
+ int GetControlId() const ;
+ int GetMessage() const;
+ void* GetPointer() const;
+ CGUIListItemPtr GetItem() const;
+ int GetParam1() const;
+ int GetParam2() const;
+ int GetSenderId() const;
+ void SetParam1(int param1);
+ void SetParam2(int param2);
+ void SetPointer(void* pointer);
+ void SetLabel(const std::string& strLabel);
+ void SetLabel(int iString); // for convenience - looks up in strings.po
+ const std::string& GetLabel() const;
+ void SetStringParam(const std::string &strParam);
+ void SetStringParams(const std::vector<std::string> &params);
+ const std::string& GetStringParam(size_t param = 0) const;
+ size_t GetNumStringParams() const;
+ void SetItem(CGUIListItemPtr item);
+
+private:
+ std::string m_strLabel;
+ std::vector<std::string> m_params;
+ int m_senderID;
+ int m_controlID;
+ int m_message;
+ void* m_pointer;
+ int m_param1;
+ int m_param2;
+ CGUIListItemPtr m_item;
+
+ static std::string empty_string;
+};
+
diff --git a/xbmc/guilib/GUIMoverControl.cpp b/xbmc/guilib/GUIMoverControl.cpp
new file mode 100644
index 0000000..bb2c00f
--- /dev/null
+++ b/xbmc/guilib/GUIMoverControl.cpp
@@ -0,0 +1,254 @@
+/*
+ * 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 "GUIMoverControl.h"
+
+#include "GUIMessage.h"
+#include "input/Key.h"
+#include "input/mouse/MouseStat.h"
+#include "utils/TimeUtils.h"
+
+using namespace UTILS;
+
+CGUIMoverControl::CGUIMoverControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& textureFocus,
+ const CTextureInfo& textureNoFocus,
+ UTILS::MOVING_SPEED::MapEventConfig& movingSpeedCfg)
+ : CGUIControl(parentID, controlID, posX, posY, width, height),
+ m_imgFocus(CGUITexture::CreateTexture(posX, posY, width, height, textureFocus)),
+ m_imgNoFocus(CGUITexture::CreateTexture(posX, posY, width, height, textureNoFocus))
+{
+ m_frameCounter = 0;
+ m_movingSpeed.AddEventMapConfig(movingSpeedCfg);
+ m_fAnalogSpeed = 2.0f; //! @todo implement correct analog speed
+ ControlType = GUICONTROL_MOVER;
+ SetLimits(0, 0, 720, 576); // defaults
+ SetLocation(0, 0, false); // defaults
+}
+
+CGUIMoverControl::CGUIMoverControl(const CGUIMoverControl& control)
+ : CGUIControl(control),
+ m_imgFocus(control.m_imgFocus->Clone()),
+ m_imgNoFocus(control.m_imgNoFocus->Clone()),
+ m_frameCounter(control.m_frameCounter),
+ m_movingSpeed(control.m_movingSpeed),
+ m_fAnalogSpeed(control.m_fAnalogSpeed),
+ m_iX1(control.m_iX1),
+ m_iX2(control.m_iX2),
+ m_iY1(control.m_iY1),
+ m_iY2(control.m_iY2),
+ m_iLocationX(control.m_iLocationX),
+ m_iLocationY(control.m_iLocationY)
+{
+}
+
+void CGUIMoverControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_bInvalidated)
+ {
+ m_imgFocus->SetWidth(m_width);
+ m_imgFocus->SetHeight(m_height);
+
+ m_imgNoFocus->SetWidth(m_width);
+ m_imgNoFocus->SetHeight(m_height);
+ }
+ if (HasFocus())
+ {
+ unsigned int alphaCounter = m_frameCounter + 2;
+ unsigned int alphaChannel;
+ if ((alphaCounter % 128) >= 64)
+ alphaChannel = alphaCounter % 64;
+ else
+ alphaChannel = 63 - (alphaCounter % 64);
+
+ alphaChannel += 192;
+ if (SetAlpha( (unsigned char)alphaChannel ))
+ MarkDirtyRegion();
+ m_imgFocus->SetVisible(true);
+ m_imgNoFocus->SetVisible(false);
+ m_frameCounter++;
+ }
+ else
+ {
+ if (SetAlpha(0xff))
+ MarkDirtyRegion();
+ m_imgFocus->SetVisible(false);
+ m_imgNoFocus->SetVisible(true);
+ }
+ m_imgFocus->Process(currentTime);
+ m_imgNoFocus->Process(currentTime);
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIMoverControl::Render()
+{
+ // render both so the visibility settings cause the frame counter to resetcorrectly
+ m_imgFocus->Render();
+ m_imgNoFocus->Render();
+ CGUIControl::Render();
+}
+
+bool CGUIMoverControl::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_SELECT_ITEM)
+ {
+ // button selected - send message to parent
+ CGUIMessage message(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(message);
+ return true;
+ }
+ if (action.GetID() == ACTION_ANALOG_MOVE)
+ {
+ // if (m_dwAllowedDirections == ALLOWED_DIRECTIONS_UPDOWN)
+ // Move(0, (int)(-m_fAnalogSpeed*action.GetAmount(1)));
+ // else if (m_dwAllowedDirections == ALLOWED_DIRECTIONS_LEFTRIGHT)
+ // Move((int)(m_fAnalogSpeed*action.GetAmount()), 0);
+ // else // ALLOWED_DIRECTIONS_ALL
+ Move((int)(m_fAnalogSpeed*action.GetAmount()), (int)( -m_fAnalogSpeed*action.GetAmount(1)));
+ return true;
+ }
+ // base class
+ return CGUIControl::OnAction(action);
+}
+
+void CGUIMoverControl::OnUp()
+{
+ // if (m_dwAllowedDirections == ALLOWED_DIRECTIONS_LEFTRIGHT) return;
+ Move(0, -static_cast<int>(m_movingSpeed.GetUpdatedDistance(MOVING_SPEED::EventType::UP)));
+}
+
+void CGUIMoverControl::OnDown()
+{
+ // if (m_dwAllowedDirections == ALLOWED_DIRECTIONS_LEFTRIGHT) return;
+ Move(0, static_cast<int>(m_movingSpeed.GetUpdatedDistance(MOVING_SPEED::EventType::DOWN)));
+}
+
+void CGUIMoverControl::OnLeft()
+{
+ // if (m_dwAllowedDirections == ALLOWED_DIRECTIONS_UPDOWN) return;
+ Move(-static_cast<int>(m_movingSpeed.GetUpdatedDistance(MOVING_SPEED::EventType::LEFT)), 0);
+}
+
+void CGUIMoverControl::OnRight()
+{
+ // if (m_dwAllowedDirections == ALLOWED_DIRECTIONS_UPDOWN) return;
+ Move(static_cast<int>(m_movingSpeed.GetUpdatedDistance(MOVING_SPEED::EventType::RIGHT)), 0);
+}
+
+EVENT_RESULT CGUIMoverControl::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_DRAG || event.m_id == ACTION_MOUSE_DRAG_END)
+ {
+ if (static_cast<HoldAction>(event.m_state) == HoldAction::DRAG)
+ { // grab exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ }
+ else if (static_cast<HoldAction>(event.m_state) == HoldAction::DRAG_END)
+ { // release exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
+ SendWindowMessage(msg);
+ }
+ Move((int)event.m_offsetX, (int)event.m_offsetY);
+ return EVENT_RESULT_HANDLED;
+ }
+ return EVENT_RESULT_UNHANDLED;
+}
+
+void CGUIMoverControl::AllocResources()
+{
+ CGUIControl::AllocResources();
+ m_frameCounter = 0;
+ m_imgFocus->AllocResources();
+ m_imgNoFocus->AllocResources();
+ float width = m_width ? m_width : m_imgFocus->GetWidth();
+ float height = m_height ? m_height : m_imgFocus->GetHeight();
+ SetWidth(width);
+ SetHeight(height);
+}
+
+void CGUIMoverControl::FreeResources(bool immediately)
+{
+ CGUIControl::FreeResources(immediately);
+ m_imgFocus->FreeResources(immediately);
+ m_imgNoFocus->FreeResources(immediately);
+}
+
+void CGUIMoverControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+ m_imgFocus->DynamicResourceAlloc(bOnOff);
+ m_imgNoFocus->DynamicResourceAlloc(bOnOff);
+}
+
+void CGUIMoverControl::SetInvalid()
+{
+ CGUIControl::SetInvalid();
+ m_imgFocus->SetInvalid();
+ m_imgNoFocus->SetInvalid();
+}
+
+void CGUIMoverControl::Move(int iX, int iY)
+{
+ if (!m_enabled)
+ return;
+
+ int iLocX = m_iLocationX + iX;
+ int iLocY = m_iLocationY + iY;
+ // check if we are within the bounds
+ if (iLocX < m_iX1) iLocX = m_iX1;
+ if (iLocY < m_iY1) iLocY = m_iY1;
+ if (iLocX > m_iX2) iLocX = m_iX2;
+ if (iLocY > m_iY2) iLocY = m_iY2;
+ // ok, now set the location of the mover
+ SetLocation(iLocX, iLocY);
+}
+
+void CGUIMoverControl::SetLocation(int iLocX, int iLocY, bool bSetPosition)
+{
+ if (bSetPosition) SetPosition(GetXPosition() + iLocX - m_iLocationX, GetYPosition() + iLocY - m_iLocationY);
+ m_iLocationX = iLocX;
+ m_iLocationY = iLocY;
+}
+
+void CGUIMoverControl::SetPosition(float posX, float posY)
+{
+ CGUIControl::SetPosition(posX, posY);
+ m_imgFocus->SetPosition(posX, posY);
+ m_imgNoFocus->SetPosition(posX, posY);
+}
+
+bool CGUIMoverControl::SetAlpha(unsigned char alpha)
+{
+ bool changed = m_imgFocus->SetAlpha(alpha);
+ changed |= m_imgNoFocus->SetAlpha(alpha);
+ return changed;
+}
+
+bool CGUIMoverControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_imgFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgNoFocus->SetDiffuseColor(m_diffuseColor);
+
+ return changed;
+}
+
+void CGUIMoverControl::SetLimits(int iX1, int iY1, int iX2, int iY2)
+{
+ m_iX1 = iX1;
+ m_iY1 = iY1;
+ m_iX2 = iX2;
+ m_iY2 = iY2;
+}
diff --git a/xbmc/guilib/GUIMoverControl.dox b/xbmc/guilib/GUIMoverControl.dox
new file mode 100644
index 0000000..191231f
--- /dev/null
+++ b/xbmc/guilib/GUIMoverControl.dox
@@ -0,0 +1,92 @@
+/*!
+
+\page Mover_Control Mover Control
+\brief **Used in the calibration screens.**
+
+\tableofcontents
+
+The mover control is used for the screen calibration portions of Kodi. You can
+choose the size and look of the mover control.
+
+
+--------------------------------------------------------------------------------
+\section Mover_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="mover" id="3">
+ <description>My first mover control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <texturefocus>mytexture.png</texturefocus>
+ <texturenofocus>mytexture.png</texturenofocus>
+ <pulseonselect>true</pulseonselect>
+ <movingspeed acceleration="180" maxvelocity="300" resettimeout="200" delta="1">
+ <eventconfig type="up">
+ <eventconfig type="down">
+ <eventconfig type="left" acceleration="100" maxvelocity="100" resettimeout="160" delta="1">
+ <eventconfig type="right" acceleration="100" maxvelocity="100" resettimeout="160" delta="1">
+ </movingspeed>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Mover_Control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is lower case only. This is
+important, as xml tags are case-sensitive.
+
+| Tag | Description |
+|----------------:|:--------------------------------------------------------------|
+| texturefocus | Specifies the image file which should be displayed when the mover has focus. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| texturenofocus | Specifies the image file which should be displayed when the mover does not have focus.
+| movingspeed | Specifies the allowed directional movements and respective configurations. The <c>`<movingspeed>`</c> tag section can contain as many <c>`<eventconfig>`</c> tags as required.
+
+
+--------------------------------------------------------------------------------
+\section Mover_Control_sect3 Note on use of movingspeed tag
+
+The <c>`movingspeed`</c> tag must be specified to allow the control to be moved via
+an input device such as keyboard. Each direction can be specified and configured
+to simulate the motion effect with the <c>`eventconfig`</c> tag.
+
+The following attributes allow the motion effect of the control to be configured:
+
+| Attribute | Description |
+|----------------:|:--------------------------------------------------------------|
+| type | Specifies the direction of movement. Accepted values are: <c>`up`</c>, <c>`down`</c>, <c>`left`</c>, <c>`right`</c>. All directions are optional and do not have to be included.
+| acceleration | Specifies the acceleration in pixels per second (integer value).
+| maxvelocity | Specifies the maximum movement speed. This depends on the acceleration value, e.g. a suitable value could be (acceleration value)*3. Set to 0 to disable. (integer value).
+| resettimeout | Specifies the idle timeout before resetting the acceleration speed (in milliseconds, integer value).
+| delta | Specifies the minimal pixel increment step (integer value).
+
+The attributes <c>`acceleration`</c>, <c>`maxvelocity`</c>, <c>`resettimeout`</c>,
+<c>`delta`</c> can be specified individually for each <c>`eventconfig`</c> tag,
+or globally in the <c>`movingspeed`</c> tag, or a mixture, as the following example:
+
+~~~~~~~~~~~~~
+<movingspeed acceleration="180" maxvelocity="300" resettimeout="200" delta="1">
+ <eventconfig type="up">
+ <eventconfig type="down">
+ <eventconfig type="left" acceleration="100" maxvelocity="100" resettimeout="160">
+ <eventconfig type="right" acceleration="100" maxvelocity="100" resettimeout="160">
+</movingspeed>
+~~~~~~~~~~~~~
+
+--------------------------------------------------------------------------------
+\section Mover_Control_sect4 Revision History
+
+@skinning_v20 <b>[movingspeed]</b> New tag added.
+
+--------------------------------------------------------------------------------
+\section Mover_Control_sect5 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIMoverControl.h b/xbmc/guilib/GUIMoverControl.h
new file mode 100644
index 0000000..d958df1
--- /dev/null
+++ b/xbmc/guilib/GUIMoverControl.h
@@ -0,0 +1,78 @@
+/*
+ * 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
+
+/*!
+\file GUIMoverControl.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUITexture.h"
+#include "utils/MovingSpeed.h"
+
+#define ALLOWED_DIRECTIONS_ALL 0
+#define ALLOWED_DIRECTIONS_UPDOWN 1
+#define ALLOWED_DIRECTIONS_LEFTRIGHT 2
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIMoverControl : public CGUIControl
+{
+public:
+ CGUIMoverControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& textureFocus,
+ const CTextureInfo& textureNoFocus,
+ UTILS::MOVING_SPEED::MapEventConfig& movingSpeedCfg);
+
+ ~CGUIMoverControl(void) override = default;
+ CGUIMoverControl* Clone() const override { return new CGUIMoverControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnAction(const CAction &action) override;
+ void OnUp() override;
+ void OnDown() override;
+ void OnLeft() override;
+ void OnRight() override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ void SetPosition(float posX, float posY) override;
+ void SetLimits(int iX1, int iY1, int iX2, int iY2);
+ void SetLocation(int iLocX, int iLocY, bool bSetPosition = true);
+ int GetXLocation() const { return m_iLocationX; }
+ int GetYLocation() const { return m_iLocationY; }
+ bool CanFocus() const override { return true; }
+
+protected:
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ bool UpdateColors(const CGUIListItem* item) override;
+ bool SetAlpha(unsigned char alpha);
+ void Move(int iX, int iY);
+ std::unique_ptr<CGUITexture> m_imgFocus;
+ std::unique_ptr<CGUITexture> m_imgNoFocus;
+ unsigned int m_frameCounter;
+ UTILS::MOVING_SPEED::CMovingSpeed m_movingSpeed;
+ float m_fAnalogSpeed;
+ int m_iX1, m_iX2, m_iY1, m_iY2;
+ int m_iLocationX, m_iLocationY;
+
+private:
+ CGUIMoverControl(const CGUIMoverControl& control);
+};
+
diff --git a/xbmc/guilib/GUIMultiImage.cpp b/xbmc/guilib/GUIMultiImage.cpp
new file mode 100644
index 0000000..39593e6
--- /dev/null
+++ b/xbmc/guilib/GUIMultiImage.cpp
@@ -0,0 +1,323 @@
+/*
+ * 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 "GUIMultiImage.h"
+
+#include "FileItem.h"
+#include "GUIMessage.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "TextureManager.h"
+#include "WindowIDs.h"
+#include "filesystem/Directory.h"
+#include "input/Key.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/JobManager.h"
+#include "utils/Random.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <mutex>
+
+using namespace KODI::GUILIB;
+using namespace XFILE;
+
+CGUIMultiImage::CGUIMultiImage(int parentID, int controlID, float posX, float posY, float width, float height, const CTextureInfo& texture, unsigned int timePerImage, unsigned int fadeTime, bool randomized, bool loop, unsigned int timeToPauseAtEnd)
+ : CGUIControl(parentID, controlID, posX, posY, width, height),
+ m_image(0, 0, posX, posY, width, height, texture)
+{
+ m_currentImage = 0;
+ m_timePerImage = timePerImage + fadeTime;
+ m_timeToPauseAtEnd = timeToPauseAtEnd;
+ m_image.SetCrossFade(fadeTime);
+ m_randomized = randomized;
+ m_loop = loop;
+ ControlType = GUICONTROL_MULTI_IMAGE;
+ m_bDynamicResourceAlloc=false;
+ m_directoryStatus = UNLOADED;
+ m_jobID = 0;
+}
+
+CGUIMultiImage::CGUIMultiImage(const CGUIMultiImage &from)
+ : CGUIControl(from), m_texturePath(from.m_texturePath), m_imageTimer(), m_files(), m_image(from.m_image)
+{
+ m_timePerImage = from.m_timePerImage;
+ m_timeToPauseAtEnd = from.m_timeToPauseAtEnd;
+ m_randomized = from.m_randomized;
+ m_loop = from.m_loop;
+ m_bDynamicResourceAlloc=false;
+ m_directoryStatus = UNLOADED;
+ if (m_texturePath.IsConstant())
+ m_currentPath = m_texturePath.GetLabel(WINDOW_INVALID);
+ m_currentImage = 0;
+ ControlType = GUICONTROL_MULTI_IMAGE;
+ m_jobID = 0;
+}
+
+CGUIMultiImage::~CGUIMultiImage(void)
+{
+ CancelLoading();
+}
+
+void CGUIMultiImage::UpdateVisibility(const CGUIListItem *item)
+{
+ CGUIControl::UpdateVisibility(item);
+
+ // check if we're hidden, and deallocate if so
+ if (!IsVisible() && m_visible != DELAYED)
+ {
+ if (m_bDynamicResourceAlloc && m_bAllocated)
+ FreeResources();
+ return;
+ }
+
+ // we are either delayed or visible, so we can allocate our resources
+ if (m_directoryStatus == UNLOADED)
+ LoadDirectory();
+
+ if (!m_bAllocated)
+ AllocResources();
+
+ if (m_directoryStatus == LOADED)
+ OnDirectoryLoaded();
+}
+
+void CGUIMultiImage::UpdateInfo(const CGUIListItem *item)
+{
+ // check for conditional information before we
+ // alloc as this can free our resources
+ if (!m_texturePath.IsConstant())
+ {
+ std::string texturePath;
+ if (item)
+ texturePath = m_texturePath.GetItemLabel(item, true);
+ else
+ texturePath = m_texturePath.GetLabel(m_parentID);
+ if (texturePath != m_currentPath)
+ {
+ // a new path - set our current path and tell ourselves to load our directory
+ m_currentPath = texturePath;
+ CancelLoading();
+ }
+ }
+}
+
+void CGUIMultiImage::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ // Set a viewport so that we don't render outside the defined area
+ if (m_directoryStatus == READY && !m_files.empty())
+ {
+ unsigned int nextImage = m_currentImage + 1;
+ if (nextImage >= m_files.size())
+ nextImage = m_loop ? 0 : m_currentImage; // stay on the last image if <loop>no</loop>
+
+ if (nextImage != m_currentImage)
+ {
+ // check if we should be loading a new image yet
+ unsigned int timeToShow = m_timePerImage;
+ if (0 == nextImage) // last image should be paused for a bit longer if that's what the skinner wishes.
+ timeToShow += m_timeToPauseAtEnd;
+ if (m_imageTimer.IsRunning() && m_imageTimer.GetElapsedMilliseconds() > timeToShow)
+ {
+ // grab a new image
+ m_currentImage = nextImage;
+ m_image.SetFileName(m_files[m_currentImage]);
+ MarkDirtyRegion();
+
+ m_imageTimer.StartZero();
+ }
+ }
+ }
+ else if (m_directoryStatus != LOADING)
+ m_image.SetFileName("");
+
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height))
+ {
+ if (m_image.SetColorDiffuse(m_diffuseColor))
+ MarkDirtyRegion();
+
+ m_image.DoProcess(currentTime, dirtyregions);
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIMultiImage::Render()
+{
+ m_image.Render();
+ CGUIControl::Render();
+}
+
+bool CGUIMultiImage::OnAction(const CAction &action)
+{
+ return false;
+}
+
+bool CGUIMultiImage::OnMessage(CGUIMessage &message)
+{
+ if (message.GetMessage() == GUI_MSG_REFRESH_THUMBS)
+ {
+ if (!m_texturePath.IsConstant())
+ FreeResources();
+ return true;
+ }
+ return CGUIControl::OnMessage(message);
+}
+
+void CGUIMultiImage::AllocResources()
+{
+ FreeResources();
+ CGUIControl::AllocResources();
+
+ if (m_directoryStatus == UNLOADED)
+ LoadDirectory();
+}
+
+void CGUIMultiImage::FreeResources(bool immediately)
+{
+ m_image.FreeResources(immediately);
+ m_currentImage = 0;
+ CancelLoading();
+ m_files.clear();
+ CGUIControl::FreeResources(immediately);
+}
+
+void CGUIMultiImage::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+ m_bDynamicResourceAlloc=bOnOff;
+}
+
+void CGUIMultiImage::SetInvalid()
+{
+ m_image.SetInvalid();
+ CGUIControl::SetInvalid();
+}
+
+bool CGUIMultiImage::CanFocus() const
+{
+ return false;
+}
+
+void CGUIMultiImage::SetAspectRatio(const CAspectRatio &ratio)
+{
+ m_image.SetAspectRatio(ratio);
+}
+
+void CGUIMultiImage::LoadDirectory()
+{
+ // clear current stuff out
+ m_files.clear();
+
+ // don't load any images if our path is empty
+ if (m_currentPath.empty()) return;
+
+ /* Check the fast cases:
+ 1. Picture extension
+ 2. Cached picture (in case an extension is not present)
+ 3. Bundled folder
+ */
+ CFileItem item(m_currentPath, false);
+ if (item.IsPicture() || CServiceBroker::GetTextureCache()->HasCachedImage(m_currentPath))
+ m_files.push_back(m_currentPath);
+ else // bundled folder?
+ m_files =
+ CServiceBroker::GetGUI()->GetTextureManager().GetBundledTexturesFromPath(m_currentPath);
+ if (!m_files.empty())
+ { // found - nothing more to do
+ OnDirectoryLoaded();
+ return;
+ }
+ // slow(er) checks necessary - do them in the background
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_directoryStatus = LOADING;
+ m_jobID = CServiceBroker::GetJobManager()->AddJob(new CMultiImageJob(m_currentPath), this,
+ CJob::PRIORITY_NORMAL);
+}
+
+void CGUIMultiImage::OnDirectoryLoaded()
+{
+ // Randomize or sort our images if necessary
+ if (m_randomized)
+ KODI::UTILS::RandomShuffle(m_files.begin(), m_files.end());
+ else
+ sort(m_files.begin(), m_files.end());
+
+ // flag as loaded - no point in constantly reloading them
+ m_directoryStatus = READY;
+ m_imageTimer.StartZero();
+ m_currentImage = 0;
+ m_image.SetFileName(m_files.empty() ? "" : m_files[0]);
+}
+
+void CGUIMultiImage::CancelLoading()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_directoryStatus == LOADING)
+ CServiceBroker::GetJobManager()->CancelJob(m_jobID);
+ m_directoryStatus = UNLOADED;
+}
+
+void CGUIMultiImage::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_directoryStatus == LOADING && strncmp(job->GetType(), "multiimage", 10) == 0)
+ {
+ m_files = ((CMultiImageJob *)job)->m_files;
+ m_directoryStatus = LOADED;
+ }
+}
+
+void CGUIMultiImage::SetInfo(const GUIINFO::CGUIInfoLabel &info)
+{
+ m_texturePath = info;
+ if (m_texturePath.IsConstant())
+ m_currentPath = m_texturePath.GetLabel(WINDOW_INVALID);
+}
+
+std::string CGUIMultiImage::GetDescription() const
+{
+ return m_image.GetDescription();
+}
+
+CGUIMultiImage::CMultiImageJob::CMultiImageJob(const std::string &path)
+ : m_path(path)
+{
+}
+
+bool CGUIMultiImage::CMultiImageJob::DoWork()
+{
+ // check to see if we have a single image or a folder of images
+ CFileItem item(m_path, false);
+ item.FillInMimeType();
+ if (item.IsPicture() || StringUtils::StartsWithNoCase(item.GetMimeType(), "image/"))
+ {
+ m_files.push_back(m_path);
+ }
+ else
+ {
+ // Load in images from the directory specified
+ // m_path is relative (as are all skin paths)
+ std::string realPath = CServiceBroker::GetGUI()->GetTextureManager().GetTexturePath(m_path, true);
+ if (realPath.empty())
+ return true;
+
+ URIUtils::AddSlashAtEnd(realPath);
+ CFileItemList items;
+ CDirectory::GetDirectory(realPath, items, CServiceBroker::GetFileExtensionProvider().GetPictureExtensions()+ "|.tbn|.dds", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
+ for (int i=0; i < items.Size(); i++)
+ {
+ CFileItem* pItem = items[i].get();
+ if (pItem && (pItem->IsPicture() || StringUtils::StartsWithNoCase(pItem->GetMimeType(), "image/")))
+ m_files.push_back(pItem->GetPath());
+ }
+ }
+ return true;
+}
diff --git a/xbmc/guilib/GUIMultiImage.dox b/xbmc/guilib/GUIMultiImage.dox
new file mode 100644
index 0000000..c741b43
--- /dev/null
+++ b/xbmc/guilib/GUIMultiImage.dox
@@ -0,0 +1,78 @@
+/*!
+
+\page MultiImage_Control Group MultiImage Control
+\brief **Used to show a slideshow of images.**
+
+\tableofcontents
+
+The MultiImage control is used for displaying a slideshow of images from a folder
+in Kodi. You can choose the position and size of the slideshow, as well as
+timing information.
+
+
+--------------------------------------------------------------------------------
+\section MultiImage_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="multiimage" id="1">
+ <description>My first slideshow control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+ <imagepath>myimagepath</imagepath>
+ <info></info>
+ <timeperimage>5000</timeperimage>
+ <fadetime>2000</fadetime>
+ <pauseatend>10000</pauseatend>
+ <randomize>true</randomize>
+ <loop>no</loop>
+ <aspectratio>stretch</aspectratio>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section MultiImage_Control_sect2 Image Size and Type Restrictions
+For the <b>`<texture>`</b> tags, and for all <b>`<texture>`</b> tags in other
+controls, there is a small set of rules that you should follow if at all possible:
+
+\subsection MultiImage_Control_sect2_1 Formats
+If you wish to use full 8 bit transparency, then use PNG. If you only need a
+single transparent colour, then you can specify this in the <b>`<colorkey>`</b>
+tag, so any image will be fine. It is suggested that you use PNG and JPG as much
+as possible. The size of the images (in kb) is therefore not as important as the
+size of the images in pixels – so feel free to store them in a lossless (eg PNG)
+manner if you wish.
+
+The only exception to this is if you require an animated texture. In this case, we
+only support animated GIF. There are also SOME animated gifs that may not work.
+Use ImageReady CS and make sure you set the gif-anim to “restore to background”
+and they should work fine.
+
+\section MultiImage_Control_sect3 Available tags and attributes
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| imagepath | Specifies the path containing the images to use for the slideshow. Kodi will first look inside the compressed Textures.xbt file for images, and then will look in the actual folder. The path is relative to the media/ folder if it is not specified completely.
+| info | Specifies the information that this image control is presenting. Kodi will select the texture to use based on this tag. [See here for more information](http://kodi.wiki/view/InfoLabels).
+| timeperimage | Time in milliseconds that an image is shown for.
+| fadetime | Time in milliseconds to fade between images.
+| pauseatend | Time in milliseconds to pause (in addition to <b>`<timeperimage>`</b>) on the last image at the end of a complete cycle through the images. Only useful if <b>`<loop>`</b> is set to yes.
+| loop | If set to no, the last image will display indefinitely. Setting it to yes will loop around once they reach the last image. Defaults to yes.
+| aspectratio | This specifies how the image will be drawn inside the box defined by <b>`<width>`</b> and <b>`<height>`</b>. [See here for more info](http://kodi.wiki/view/Aspect_Ratio).
+
+--------------------------------------------------------------------------------
+\section MultiImage_Control_sect4 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIMultiImage.h b/xbmc/guilib/GUIMultiImage.h
new file mode 100644
index 0000000..c7a4a6a
--- /dev/null
+++ b/xbmc/guilib/GUIMultiImage.h
@@ -0,0 +1,89 @@
+/*
+ * 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
+
+/*!
+\file GUIMultiImage.h
+\brief
+*/
+
+#include "GUIImage.h"
+#include "threads/CriticalSection.h"
+#include "utils/Job.h"
+#include "utils/Stopwatch.h"
+
+#include <vector>
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIMultiImage : public CGUIControl, public IJobCallback
+{
+public:
+ CGUIMultiImage(int parentID, int controlID, float posX, float posY, float width, float height, const CTextureInfo& texture, unsigned int timePerImage, unsigned int fadeTime, bool randomized, bool loop, unsigned int timeToPauseAtEnd);
+ CGUIMultiImage(const CGUIMultiImage &from);
+ ~CGUIMultiImage(void) override;
+ CGUIMultiImage* Clone() const override { return new CGUIMultiImage(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ void UpdateVisibility(const CGUIListItem *item = NULL) override;
+ void UpdateInfo(const CGUIListItem *item = NULL) override;
+ bool OnAction(const CAction &action) override;
+ bool OnMessage(CGUIMessage &message) override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ bool IsDynamicallyAllocated() override { return m_bDynamicResourceAlloc; }
+ void SetInvalid() override;
+ bool CanFocus() const override;
+ std::string GetDescription() const override;
+
+ void SetInfo(const KODI::GUILIB::GUIINFO::CGUIInfoLabel &info);
+ void SetAspectRatio(const CAspectRatio &ratio);
+
+protected:
+ void LoadDirectory();
+ void OnDirectoryLoaded();
+ void CancelLoading();
+
+ enum DIRECTORY_STATUS { UNLOADED = 0, LOADING, LOADED, READY };
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ class CMultiImageJob : public CJob
+ {
+ public:
+ explicit CMultiImageJob(const std::string &path);
+ bool DoWork() override;
+ const char* GetType() const override { return "multiimage"; }
+
+ std::vector<std::string> m_files;
+ std::string m_path;
+ };
+
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_texturePath;
+ std::string m_currentPath;
+ unsigned int m_currentImage;
+ CStopWatch m_imageTimer;
+ unsigned int m_timePerImage;
+ unsigned int m_timeToPauseAtEnd;
+ bool m_randomized;
+ bool m_loop;
+
+ bool m_bDynamicResourceAlloc;
+ std::vector<std::string> m_files;
+
+ CGUIImage m_image;
+
+ CCriticalSection m_section;
+ DIRECTORY_STATUS m_directoryStatus;
+ unsigned int m_jobID;
+};
+
diff --git a/xbmc/guilib/GUIPanelContainer.cpp b/xbmc/guilib/GUIPanelContainer.cpp
new file mode 100644
index 0000000..0195d57
--- /dev/null
+++ b/xbmc/guilib/GUIPanelContainer.cpp
@@ -0,0 +1,567 @@
+/*
+ * 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 "GUIPanelContainer.h"
+
+#include "FileItem.h"
+#include "GUIListItemLayout.h"
+#include "GUIMessage.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/Key.h"
+#include "utils/StringUtils.h"
+
+#include <cassert>
+
+CGUIPanelContainer::CGUIPanelContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems)
+ : CGUIBaseContainer(parentID, controlID, posX, posY, width, height, orientation, scroller, preloadItems)
+{
+ ControlType = GUICONTAINER_PANEL;
+ m_type = VIEW_TYPE_ICON;
+ m_itemsPerRow = 1;
+}
+
+CGUIPanelContainer::~CGUIPanelContainer(void) = default;
+
+void CGUIPanelContainer::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ ValidateOffset();
+
+ if (m_bInvalidated)
+ UpdateLayout();
+
+ if (!m_layout || !m_focusedLayout)
+ return;
+
+ UpdateScrollOffset(currentTime);
+
+ int offset = (int)(m_scroller.GetValue() / m_layout->Size(m_orientation));
+
+ int cacheBefore, cacheAfter;
+ GetCacheOffsets(cacheBefore, cacheAfter);
+
+ // Free memory not used on screen
+ if ((int)m_items.size() > m_itemsPerPage + cacheBefore + cacheAfter)
+ FreeMemory(CorrectOffset(offset - cacheBefore, 0), CorrectOffset(offset + m_itemsPerPage + 1 + cacheAfter, 0));
+
+ CPoint origin = CPoint(m_posX, m_posY) + m_renderOffset;
+ float pos = (m_orientation == VERTICAL) ? origin.y : origin.x;
+ float end = (m_orientation == VERTICAL) ? m_posY + m_height : m_posX + m_width;
+ pos += (offset - cacheBefore) * m_layout->Size(m_orientation) - m_scroller.GetValue();
+ end += cacheAfter * m_layout->Size(m_orientation);
+
+ int current = (offset - cacheBefore) * m_itemsPerRow;
+ int col = 0;
+ while (pos < end && m_items.size())
+ {
+ if (current >= (int)m_items.size())
+ break;
+ if (current >= 0)
+ {
+ CGUIListItemPtr item = m_items[current];
+ item->SetCurrentItem(current + 1);
+ bool focused = (current == GetOffset() * m_itemsPerRow + GetCursor()) && m_bHasFocus;
+
+ if (m_orientation == VERTICAL)
+ ProcessItem(origin.x + col * m_layout->Size(HORIZONTAL), pos, item, focused, currentTime, dirtyregions);
+ else
+ ProcessItem(pos, origin.y + col * m_layout->Size(VERTICAL), item, focused, currentTime, dirtyregions);
+ }
+ // increment our position
+ if (col < m_itemsPerRow - 1)
+ col++;
+ else
+ {
+ pos += m_layout->Size(m_orientation);
+ col = 0;
+ }
+ current++;
+ }
+
+ // when we are scrolling up, offset will become lower (integer division, see offset calc)
+ // to have same behaviour when scrolling down, we need to set page control to offset+1
+ UpdatePageControl(offset + (m_scroller.IsScrollingDown() ? 1 : 0));
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+
+void CGUIPanelContainer::Render()
+{
+ if (!m_layout || !m_focusedLayout)
+ return;
+
+ int offset = (int)(m_scroller.GetValue() / m_layout->Size(m_orientation));
+
+ int cacheBefore, cacheAfter;
+ GetCacheOffsets(cacheBefore, cacheAfter);
+
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height))
+ {
+ CPoint origin = CPoint(m_posX, m_posY) + m_renderOffset;
+ float pos = (m_orientation == VERTICAL) ? origin.y : origin.x;
+ float end = (m_orientation == VERTICAL) ? m_posY + m_height : m_posX + m_width;
+ pos += (offset - cacheBefore) * m_layout->Size(m_orientation) - m_scroller.GetValue();
+ end += cacheAfter * m_layout->Size(m_orientation);
+
+ float focusedPos = 0;
+ int focusedCol = 0;
+ CGUIListItemPtr focusedItem;
+ int current = (offset - cacheBefore) * m_itemsPerRow;
+ int col = 0;
+ while (pos < end && m_items.size())
+ {
+ if (current >= (int)m_items.size())
+ break;
+ if (current >= 0)
+ {
+ CGUIListItemPtr item = m_items[current];
+ bool focused = (current == GetOffset() * m_itemsPerRow + GetCursor()) && m_bHasFocus;
+ // render our item
+ if (focused)
+ {
+ focusedPos = pos;
+ focusedCol = col;
+ focusedItem = item;
+ }
+ else
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(origin.x + col * m_layout->Size(HORIZONTAL), pos, item.get(), false);
+ else
+ RenderItem(pos, origin.y + col * m_layout->Size(VERTICAL), item.get(), false);
+ }
+ }
+ // increment our position
+ if (col < m_itemsPerRow - 1)
+ col++;
+ else
+ {
+ pos += m_layout->Size(m_orientation);
+ col = 0;
+ }
+ current++;
+ }
+ // and render the focused item last (for overlapping purposes)
+ if (focusedItem)
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(origin.x + focusedCol * m_layout->Size(HORIZONTAL), focusedPos, focusedItem.get(), true);
+ else
+ RenderItem(focusedPos, origin.y + focusedCol * m_layout->Size(VERTICAL), focusedItem.get(), true);
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+ CGUIControl::Render();
+}
+
+bool CGUIPanelContainer::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_PAGE_UP:
+ {
+ if (GetOffset() == 0)
+ { // already on the first page, so move to the first item
+ SetCursor(0);
+ }
+ else
+ { // scroll up to the previous page
+ Scroll( -m_itemsPerPage);
+ }
+ return true;
+ }
+ break;
+ case ACTION_PAGE_DOWN:
+ {
+ if ((GetOffset() + m_itemsPerPage) * m_itemsPerRow >= (int)m_items.size() || (int)m_items.size() < m_itemsPerPage)
+ { // already at the last page, so move to the last item.
+ SetCursor(m_items.size() - GetOffset() * m_itemsPerRow - 1);
+ }
+ else
+ { // scroll down to the next page
+ Scroll(m_itemsPerPage);
+ }
+ return true;
+ }
+ break;
+ // smooth scrolling (for analog controls)
+ case ACTION_SCROLL_UP:
+ {
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+ while (m_analogScrollCount > AnalogScrollSpeed())
+ {
+ handled = true;
+ m_analogScrollCount -= AnalogScrollSpeed();
+ if (GetOffset() > 0)// && GetCursor() <= m_itemsPerPage * m_itemsPerRow / 2)
+ {
+ Scroll(-1);
+ }
+ else if (GetCursor() > 0)
+ {
+ SetCursor(GetCursor() - 1);
+ }
+ }
+ return handled;
+ }
+ break;
+ case ACTION_SCROLL_DOWN:
+ {
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+ while (m_analogScrollCount > AnalogScrollSpeed())
+ {
+ handled = true;
+ m_analogScrollCount -= AnalogScrollSpeed();
+ if ((GetOffset() + m_itemsPerPage) * m_itemsPerRow < (int)m_items.size())// && GetCursor() >= m_itemsPerPage * m_itemsPerRow / 2)
+ {
+ Scroll(1);
+ }
+ else if (GetCursor() < m_itemsPerPage * m_itemsPerRow - 1 && GetOffset() * m_itemsPerRow + GetCursor() < (int)m_items.size() - 1)
+ {
+ SetCursor(GetCursor() + 1);
+ }
+ }
+ return handled;
+ }
+ break;
+ }
+ return CGUIBaseContainer::OnAction(action);
+}
+
+bool CGUIPanelContainer::OnMessage(CGUIMessage& message)
+{
+ if (message.GetControlId() == GetID() )
+ {
+ if (message.GetMessage() == GUI_MSG_LABEL_RESET)
+ {
+ SetCursor(0);
+ // fall through to base class
+ }
+ }
+ return CGUIBaseContainer::OnMessage(message);
+}
+
+void CGUIPanelContainer::OnLeft()
+{
+ CGUIAction action = GetAction(ACTION_MOVE_LEFT);
+ bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
+ if (m_orientation == VERTICAL && MoveLeft(wrapAround))
+ return;
+ if (m_orientation == HORIZONTAL && MoveUp(wrapAround))
+ return;
+ CGUIControl::OnLeft();
+}
+
+void CGUIPanelContainer::OnRight()
+{
+ CGUIAction action = GetAction(ACTION_MOVE_RIGHT);
+ bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
+ if (m_orientation == VERTICAL && MoveRight(wrapAround))
+ return;
+ if (m_orientation == HORIZONTAL && MoveDown(wrapAround))
+ return;
+ return CGUIControl::OnRight();
+}
+
+void CGUIPanelContainer::OnUp()
+{
+ CGUIAction action = GetAction(ACTION_MOVE_UP);
+ bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
+ if (m_orientation == VERTICAL && MoveUp(wrapAround))
+ return;
+ if (m_orientation == HORIZONTAL && MoveLeft(wrapAround))
+ return;
+ CGUIControl::OnUp();
+}
+
+void CGUIPanelContainer::OnDown()
+{
+ CGUIAction action = GetAction(ACTION_MOVE_DOWN);
+ bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
+ if (m_orientation == VERTICAL && MoveDown(wrapAround))
+ return;
+ if (m_orientation == HORIZONTAL && MoveRight(wrapAround))
+ return;
+ return CGUIControl::OnDown();
+}
+
+bool CGUIPanelContainer::MoveDown(bool wrapAround)
+{
+ if (GetCursor() + m_itemsPerRow < m_itemsPerPage * m_itemsPerRow && (GetOffset() + 1 + GetCursor() / m_itemsPerRow) * m_itemsPerRow < (int)m_items.size())
+ { // move to last item if necessary
+ if ((GetOffset() + 1)*m_itemsPerRow + GetCursor() >= (int)m_items.size())
+ SetCursor((int)m_items.size() - 1 - GetOffset()*m_itemsPerRow);
+ else
+ SetCursor(GetCursor() + m_itemsPerRow);
+ }
+ else if ((GetOffset() + 1 + GetCursor() / m_itemsPerRow) * m_itemsPerRow < (int)m_items.size())
+ { // we scroll to the next row, and move to last item if necessary
+ if ((GetOffset() + 1)*m_itemsPerRow + GetCursor() >= (int)m_items.size())
+ SetCursor((int)m_items.size() - 1 - (GetOffset() + 1)*m_itemsPerRow);
+ ScrollToOffset(GetOffset() + 1);
+ }
+ else if (wrapAround)
+ { // move first item in list
+ SetCursor(GetCursor() % m_itemsPerRow);
+ ScrollToOffset(0);
+ SetContainerMoving(1);
+ }
+ else
+ return false;
+ return true;
+}
+
+bool CGUIPanelContainer::MoveUp(bool wrapAround)
+{
+ if (GetCursor() >= m_itemsPerRow)
+ SetCursor(GetCursor() - m_itemsPerRow);
+ else if (GetOffset() > 0)
+ ScrollToOffset(GetOffset() - 1);
+ else if (wrapAround)
+ { // move last item in list in this column
+ SetCursor((GetCursor() % m_itemsPerRow) + (m_itemsPerPage - 1) * m_itemsPerRow);
+ int offset = std::max((int)GetRows() - m_itemsPerPage, 0);
+ // should check here whether cursor is actually allowed here, and reduce accordingly
+ if (offset * m_itemsPerRow + GetCursor() >= (int)m_items.size())
+ SetCursor((int)m_items.size() - offset * m_itemsPerRow - 1);
+ ScrollToOffset(offset);
+ SetContainerMoving(-1);
+ }
+ else
+ return false;
+ return true;
+}
+
+bool CGUIPanelContainer::MoveLeft(bool wrapAround)
+{
+ int col = GetCursor() % m_itemsPerRow;
+ if (col > 0)
+ SetCursor(GetCursor() - 1);
+ else if (wrapAround)
+ { // wrap around
+ SetCursor(GetCursor() + m_itemsPerRow - 1);
+ if (GetOffset() * m_itemsPerRow + GetCursor() >= (int)m_items.size())
+ SetCursor((int)m_items.size() - GetOffset() * m_itemsPerRow - 1);
+ }
+ else
+ return false;
+ return true;
+}
+
+bool CGUIPanelContainer::MoveRight(bool wrapAround)
+{
+ int col = GetCursor() % m_itemsPerRow;
+ if (col + 1 < m_itemsPerRow && GetOffset() * m_itemsPerRow + GetCursor() + 1 < (int)m_items.size())
+ SetCursor(GetCursor() + 1);
+ else if (wrapAround) // move first item in row
+ SetCursor(GetCursor() - col);
+ else
+ return false;
+ return true;
+}
+
+// scrolls the said amount
+void CGUIPanelContainer::Scroll(int amount)
+{
+ // increase or decrease the offset
+ int offset = GetOffset() + amount;
+ if (offset > ((int)GetRows() - m_itemsPerPage) * m_itemsPerRow)
+ {
+ offset = ((int)GetRows() - m_itemsPerPage) * m_itemsPerRow;
+ }
+ if (offset < 0) offset = 0;
+ ScrollToOffset(offset);
+}
+
+void CGUIPanelContainer::ValidateOffset()
+{
+ if (!m_layout) return;
+ // first thing is we check the range of our offset
+ // don't validate offset if we are scrolling in case the tween image exceed <0, 1> range
+ if (GetOffset() > (int)GetRows() - m_itemsPerPage || (!m_scroller.IsScrolling() && m_scroller.GetValue() > ((int)GetRows() - m_itemsPerPage) * m_layout->Size(m_orientation)))
+ {
+ SetOffset(std::max(0, (int)GetRows() - m_itemsPerPage));
+ m_scroller.SetValue(GetOffset() * m_layout->Size(m_orientation));
+ }
+ if (GetOffset() < 0 || (!m_scroller.IsScrolling() && m_scroller.GetValue() < 0))
+ {
+ SetOffset(0);
+ m_scroller.SetValue(0);
+ }
+}
+
+void CGUIPanelContainer::SetCursor(int cursor)
+{
+ if (cursor > m_itemsPerPage * m_itemsPerRow - 1)
+ cursor = m_itemsPerPage * m_itemsPerRow - 1;
+ if (cursor < 0) cursor = 0;
+ if (!m_wasReset)
+ SetContainerMoving(cursor - GetCursor());
+ CGUIBaseContainer::SetCursor(cursor);
+}
+
+void CGUIPanelContainer::CalculateLayout()
+{
+ GetCurrentLayouts();
+
+ if (!m_layout || !m_focusedLayout) return;
+ // calculate the number of items to display
+ if (m_orientation == HORIZONTAL)
+ {
+ m_itemsPerRow = (int)(m_height / m_layout->Size(VERTICAL));
+ m_itemsPerPage = (int)(m_width / m_layout->Size(HORIZONTAL));
+ }
+ else
+ {
+ m_itemsPerRow = (int)(m_width / m_layout->Size(HORIZONTAL));
+ m_itemsPerPage = (int)(m_height / m_layout->Size(VERTICAL));
+ }
+ if (m_itemsPerRow < 1) m_itemsPerRow = 1;
+ if (m_itemsPerPage < 1) m_itemsPerPage = 1;
+
+ // ensure that the scroll offset is a multiple of our size
+ m_scroller.SetValue(GetOffset() * m_layout->Size(m_orientation));
+}
+
+unsigned int CGUIPanelContainer::GetRows() const
+{
+ assert(m_itemsPerRow > 0);
+ return (m_items.size() + m_itemsPerRow - 1) / m_itemsPerRow;
+}
+
+float CGUIPanelContainer::AnalogScrollSpeed() const
+{
+ return 10.0f / m_itemsPerPage;
+}
+
+int CGUIPanelContainer::CorrectOffset(int offset, int cursor) const
+{
+ return offset * m_itemsPerRow + cursor;
+}
+
+int CGUIPanelContainer::GetCursorFromPoint(const CPoint &point, CPoint *itemPoint) const
+{
+ if (!m_layout)
+ return -1;
+
+ float sizeX = m_orientation == VERTICAL ? m_layout->Size(HORIZONTAL) : m_layout->Size(VERTICAL);
+ float sizeY = m_orientation == VERTICAL ? m_layout->Size(VERTICAL) : m_layout->Size(HORIZONTAL);
+
+ float posY = m_orientation == VERTICAL ? point.y : point.x;
+ for (int y = 0; y < m_itemsPerPage + 1; y++) // +1 to ensure if we have a half item we can select it
+ {
+ float posX = m_orientation == VERTICAL ? point.x : point.y;
+ for (int x = 0; x < m_itemsPerRow; x++)
+ {
+ int item = x + y * m_itemsPerRow;
+ if (posX < sizeX && posY < sizeY && item + GetOffset() < (int)m_items.size())
+ { // found
+ return item;
+ }
+ posX -= sizeX;
+ }
+ posY -= sizeY;
+ }
+ return -1;
+}
+
+bool CGUIPanelContainer::SelectItemFromPoint(const CPoint &point)
+{
+ int cursor = GetCursorFromPoint(point);
+ if (cursor < 0)
+ return false;
+ SetCursor(cursor);
+ return true;
+}
+
+int CGUIPanelContainer::GetCurrentRow() const
+{
+ return m_itemsPerRow > 0 ? GetCursor() / m_itemsPerRow : 0;
+}
+
+int CGUIPanelContainer::GetCurrentColumn() const
+{
+ return GetCursor() % m_itemsPerRow;
+}
+
+bool CGUIPanelContainer::GetCondition(int condition, int data) const
+{
+ int row = GetCurrentRow();
+ int col = GetCurrentColumn();
+
+ if (m_orientation == HORIZONTAL)
+ std::swap(row, col);
+
+ switch (condition)
+ {
+ case CONTAINER_ROW:
+ return (row == data);
+ case CONTAINER_COLUMN:
+ return (col == data);
+ default:
+ return CGUIBaseContainer::GetCondition(condition, data);
+ }
+}
+
+std::string CGUIPanelContainer::GetLabel(int info) const
+{
+ int row = GetCurrentRow();
+ int col = GetCurrentColumn();
+
+ if (m_orientation == HORIZONTAL)
+ std::swap(row, col);
+
+ switch (info)
+ {
+ case CONTAINER_ROW:
+ return std::to_string(row);
+ case CONTAINER_COLUMN:
+ return std::to_string(col);
+ default:
+ return CGUIBaseContainer::GetLabel(info);
+ }
+ return StringUtils::Empty;
+}
+
+void CGUIPanelContainer::SelectItem(int item)
+{
+ // Check that our offset is valid
+ ValidateOffset();
+ // only select an item if it's in a valid range
+ if (item >= 0 && item < (int)m_items.size())
+ {
+ // Select the item requested
+ if (item >= GetOffset() * m_itemsPerRow && item < (GetOffset() + m_itemsPerPage) * m_itemsPerRow)
+ { // the item is on the current page, so don't change it.
+ SetCursor(item - GetOffset() * m_itemsPerRow);
+ }
+ else if (item < GetOffset() * m_itemsPerRow)
+ { // item is on a previous page - make it the first item on the page
+ SetCursor(item % m_itemsPerRow);
+ ScrollToOffset((item - GetCursor()) / m_itemsPerRow);
+ }
+ else // (item >= GetOffset()+m_itemsPerPage)
+ { // item is on a later page - make it the last row on the page
+ SetCursor(item % m_itemsPerRow + m_itemsPerRow * (m_itemsPerPage - 1));
+ ScrollToOffset((item - GetCursor()) / m_itemsPerRow);
+ }
+ }
+}
+
+bool CGUIPanelContainer::HasPreviousPage() const
+{
+ return (GetOffset() > 0);
+}
+
+bool CGUIPanelContainer::HasNextPage() const
+{
+ return (GetOffset() != (int)GetRows() - m_itemsPerPage && (int)GetRows() > m_itemsPerPage);
+}
+
diff --git a/xbmc/guilib/GUIPanelContainer.dox b/xbmc/guilib/GUIPanelContainer.dox
new file mode 100644
index 0000000..1b36fd5
--- /dev/null
+++ b/xbmc/guilib/GUIPanelContainer.dox
@@ -0,0 +1,126 @@
+/*!
+
+\page Panel_Container Panel Container
+\brief **Used for a scrolling panel of items. Replaces the thumbnail panel.**
+
+\tableofcontents
+
+The panel container is one of several containers used to display items from file
+lists in various ways. The panel container is very flexible - it's essentially a
+multi-column list. The layout of the items is very flexible and is up to the
+skinner.
+
+
+--------------------------------------------------------------------------------
+\section Panel_Container_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="panel" id="52">
+ <posx>190</posx>
+ <posy>100</posy>
+ <width>485</width>
+ <height>425</height>
+ <onleft>9000</onleft>
+ <onright>60</onright>
+ <onup>52</onup>
+ <ondown>52</ondown>
+ <scrolltime tween="sine" easing="out">200</scrolltime>
+ <autoscroll>true</autoscroll>
+ <viewtype label="536">icon</viewtype>
+ <pagecontrol>60</pagecontrol>
+ <include>contentpanelslide</include>
+ <itemlayout height="141" width="120">
+ <control type="image">
+ <posx>10</posx>
+ <posy>10</posy>
+ <width>100</width>
+ <height>100</height>
+ <info>ListItem.Icon</info>
+ </control>
+ <control type="image">
+ <posx>80</posx>
+ <posy>75</posy>
+ <width>32</width>
+ <height>32</height>
+ <info>ListItem.Overlay</info>
+ </control>
+ <control type="label">
+ <posx>60</posx>
+ <posy>115</posy>
+ <width>110</width>
+ <height>22</height>
+ <font>font13</font>
+ <selectedcolor>green</selectedcolor>
+ <align>center</align>
+ <info>ListItem.Label</info>
+ </control>
+ </itemlayout>
+ <focusedlayout height="141" width="120">
+ <control type="image">
+ <width>110</width>
+ <height>110</height>
+ <posx>5</posx>
+ <posy>5</posy>
+ <texture>folder-focus.png</texture>
+ <animation effect="zoom" end="0,0,120,120" time="100">focus</animation>
+ </control>
+ <control type="image">
+ <posx>10</posx>
+ <posy>10</posy>
+ <width>100</width>
+ <height>100</height>
+ <info>ListItem.Icon</info>
+ <animation effect="zoom" end="5,5,110,110" time="100">focus</animation>
+ </control>
+ <control type="image">
+ <posx>80</posx>
+ <posy>75</posy>
+ <width>32</width>
+ <height>32</height>
+ <info>ListItem.Overlay</info>
+ <animation effect="slide" end="5,5" time="100">focus</animation>
+ </control>
+ <control type="label">
+ <posx>60</posx>
+ <posy>120</posy>
+ <width>110</width>
+ <height>22</height>
+ <font>font13</font>
+ <selectedcolor>green</selectedcolor>
+ <align>center</align>
+ <info>ListItem.Label</info>
+ </control>
+ </focusedlayout>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Panel_Container_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| viewtype | The type of view. Choices are list, icon, wide, wrap, biglist, bigicon, bigwide, bigwrap, info and biginfo. The label attribute indicates the label that will be used in the "View As" control within the GUI. It is localizable via strings.po. viewtype has no effect on the view itself. It is used by kodi when switching skin to automatically select a view with a similar layout. Skinners should try to set viewtype to describe the layout as best as possible.
+| orientation | The orientation of the panel. Defaults to vertical.
+| pagecontrol | Used to set the <b>`<id>`</b> of the page control used to control this panel.
+| scrolltime | The time (in ms) to scroll from one item to another. By default, this is 200ms. The panel will scroll smoothly from one item to another as needed. Set it to zero to disable the smooth scrolling. The scroll movement can be further adjusted by selecting one of the available [tween](http://kodi.wiki/view/Tweeners) methods.
+| itemlayout | Specifies the layout of items in the list. Requires both width and height attributes set. The <b>`<itemlayout>`</b> then contains as many label and image controls as required. [See here for more information](http://kodi.wiki/view/Container_Item_Layout).
+| focusedlayout | Specifies the layout of items in the list that have focus. Requires both width and height attributes set. The <b>`<focusedlayout>`</b> then contains as many label and image controls as required. [See here for more information](http://kodi.wiki/view/Container_Item_Layout).
+| content | Used to set the item content that this panel will contain. Allows the skinner to setup a panel anywhere they want with a static set of content, as a useful alternative to the grouplist control. [See here for more information](http://kodi.wiki/view/Static_List_Content)
+| preloaditems | Used in association with the background image loader. [See here for more information](http://kodi.wiki/view/Background_Image_Loader)
+| autoscroll | Used to make the container scroll automatically
+
+
+--------------------------------------------------------------------------------
+\section Panel_Container_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIPanelContainer.h b/xbmc/guilib/GUIPanelContainer.h
new file mode 100644
index 0000000..dff52af
--- /dev/null
+++ b/xbmc/guilib/GUIPanelContainer.h
@@ -0,0 +1,62 @@
+/*
+ * 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
+
+/*!
+\file GUIPanelContainer.h
+\brief
+*/
+
+#include "GUIBaseContainer.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIPanelContainer : public CGUIBaseContainer
+{
+public:
+ CGUIPanelContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems);
+ ~CGUIPanelContainer(void) override;
+ CGUIPanelContainer* Clone() const override { return new CGUIPanelContainer(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnAction(const CAction &action) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void OnLeft() override;
+ void OnRight() override;
+ void OnUp() override;
+ void OnDown() override;
+ bool GetCondition(int condition, int data) const override;
+ std::string GetLabel(int info) const override;
+protected:
+ bool MoveUp(bool wrapAround) override;
+ bool MoveDown(bool wrapAround) override;
+ virtual bool MoveLeft(bool wrapAround);
+ virtual bool MoveRight(bool wrapAround);
+ void Scroll(int amount) override;
+ float AnalogScrollSpeed() const;
+ void ValidateOffset() override;
+ void CalculateLayout() override;
+ unsigned int GetRows() const override;
+ int CorrectOffset(int offset, int cursor) const override;
+ bool SelectItemFromPoint(const CPoint &point) override;
+ int GetCursorFromPoint(const CPoint &point, CPoint *itemPoint = NULL) const override;
+ void SetCursor(int cursor) override;
+ void SelectItem(int item) override;
+ bool HasPreviousPage() const override;
+ bool HasNextPage() const override;
+
+ int GetCurrentRow() const;
+ int GetCurrentColumn() const;
+
+ int m_itemsPerRow;
+};
+
diff --git a/xbmc/guilib/GUIProgressControl.cpp b/xbmc/guilib/GUIProgressControl.cpp
new file mode 100644
index 0000000..1167d64
--- /dev/null
+++ b/xbmc/guilib/GUIProgressControl.cpp
@@ -0,0 +1,348 @@
+/*
+ * 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 "GUIProgressControl.h"
+
+#include "GUIInfoManager.h"
+#include "GUIListItem.h"
+#include "GUIMessage.h"
+#include "utils/StringUtils.h"
+
+CGUIProgressControl::CGUIProgressControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& backGroundTexture,
+ const CTextureInfo& leftTexture,
+ const CTextureInfo& midTexture,
+ const CTextureInfo& rightTexture,
+ const CTextureInfo& overlayTexture,
+ bool reveal)
+ : CGUIControl(parentID, controlID, posX, posY, width, height),
+ m_guiBackground(CGUITexture::CreateTexture(posX, posY, width, height, backGroundTexture)),
+ m_guiLeft(CGUITexture::CreateTexture(posX, posY, width, height, leftTexture)),
+ m_guiMid(CGUITexture::CreateTexture(posX, posY, width, height, midTexture)),
+ m_guiRight(CGUITexture::CreateTexture(posX, posY, width, height, rightTexture)),
+ m_guiOverlay(CGUITexture::CreateTexture(posX, posY, width, height, overlayTexture))
+{
+ m_fPercent = 0;
+ m_iInfoCode = 0;
+ ControlType = GUICONTROL_PROGRESS;
+ m_bReveal = reveal;
+ m_bChanged = false;
+}
+
+CGUIProgressControl::CGUIProgressControl(const CGUIProgressControl& control)
+ : CGUIControl(control),
+ m_guiBackground(control.m_guiBackground->Clone()),
+ m_guiLeft(control.m_guiLeft->Clone()),
+ m_guiMid(control.m_guiMid->Clone()),
+ m_guiRight(control.m_guiRight->Clone()),
+ m_guiOverlay(control.m_guiOverlay->Clone()),
+ m_guiMidClipRect(control.m_guiMidClipRect),
+ m_iInfoCode(control.m_iInfoCode),
+ m_iInfoCode2(control.m_iInfoCode2),
+ m_fPercent(control.m_fPercent),
+ m_fPercent2(control.m_fPercent2),
+ m_bReveal(control.m_bReveal),
+ m_bChanged(control.m_bChanged)
+{
+}
+
+void CGUIProgressControl::SetPosition(float posX, float posY)
+{
+ // everything is positioned based on the background image position
+ CGUIControl::SetPosition(posX, posY);
+ m_guiBackground->SetPosition(posX, posY);
+}
+
+void CGUIProgressControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ bool changed = false;
+
+ if (!IsDisabled())
+ changed |= UpdateLayout();
+ changed |= m_guiBackground->Process(currentTime);
+ changed |= m_guiMid->Process(currentTime);
+ changed |= m_guiLeft->Process(currentTime);
+ changed |= m_guiRight->Process(currentTime);
+ changed |= m_guiOverlay->Process(currentTime);
+
+ if (changed)
+ MarkDirtyRegion();
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIProgressControl::Render()
+{
+ if (!IsDisabled())
+ {
+ m_guiBackground->Render();
+
+ if (m_guiLeft->GetFileName().empty() && m_guiRight->GetFileName().empty())
+ {
+ if (m_bReveal && !m_guiMidClipRect.IsEmpty())
+ {
+ bool restore = CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_guiMidClipRect.x1, m_guiMidClipRect.y1, m_guiMidClipRect.Width(), m_guiMidClipRect.Height());
+ m_guiMid->Render();
+ if (restore)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+ else if (!m_bReveal && m_guiMid->GetWidth() > 0)
+ m_guiMid->Render();
+ }
+ else
+ {
+ m_guiLeft->Render();
+
+ if (m_bReveal && !m_guiMidClipRect.IsEmpty())
+ {
+ bool restore = CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_guiMidClipRect.x1, m_guiMidClipRect.y1, m_guiMidClipRect.Width(), m_guiMidClipRect.Height());
+ m_guiMid->Render();
+ if (restore)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+ else if (!m_bReveal && m_guiMid->GetWidth() > 0)
+ m_guiMid->Render();
+
+ m_guiRight->Render();
+ }
+
+ m_guiOverlay->Render();
+ }
+
+ CGUIControl::Render();
+}
+
+
+bool CGUIProgressControl::CanFocus() const
+{
+ return false;
+}
+
+
+bool CGUIProgressControl::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_ITEM_SELECT)
+ {
+ SetPercentage((float)message.GetParam1());
+ return true;
+ }
+ return CGUIControl::OnMessage(message);
+}
+
+void CGUIProgressControl::SetPercentage(float fPercent)
+{
+ fPercent = std::max(0.0f, std::min(fPercent, 100.0f));
+ if (m_fPercent != fPercent)
+ SetInvalid();
+ m_fPercent = fPercent;
+}
+
+float CGUIProgressControl::GetPercentage() const
+{
+ return m_fPercent;
+}
+void CGUIProgressControl::FreeResources(bool immediately)
+{
+ CGUIControl::FreeResources(immediately);
+ m_guiBackground->FreeResources(immediately);
+ m_guiMid->FreeResources(immediately);
+ m_guiRight->FreeResources(immediately);
+ m_guiLeft->FreeResources(immediately);
+ m_guiOverlay->FreeResources(immediately);
+}
+
+void CGUIProgressControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+ m_guiBackground->DynamicResourceAlloc(bOnOff);
+ m_guiMid->DynamicResourceAlloc(bOnOff);
+ m_guiRight->DynamicResourceAlloc(bOnOff);
+ m_guiLeft->DynamicResourceAlloc(bOnOff);
+ m_guiOverlay->DynamicResourceAlloc(bOnOff);
+}
+
+void CGUIProgressControl::AllocResources()
+{
+ CGUIControl::AllocResources();
+ m_guiBackground->AllocResources();
+ m_guiMid->AllocResources();
+ m_guiRight->AllocResources();
+ m_guiLeft->AllocResources();
+ m_guiOverlay->AllocResources();
+
+ m_guiBackground->SetHeight(25);
+ m_guiRight->SetHeight(20);
+ m_guiLeft->SetHeight(20);
+ m_guiMid->SetHeight(20);
+ m_guiOverlay->SetHeight(20);
+}
+
+void CGUIProgressControl::SetInvalid()
+{
+ CGUIControl::SetInvalid();
+ m_guiBackground->SetInvalid();
+ m_guiMid->SetInvalid();
+ m_guiRight->SetInvalid();
+ m_guiLeft->SetInvalid();
+ m_guiOverlay->SetInvalid();
+}
+
+void CGUIProgressControl::SetInfo(int iInfo, int iInfo2)
+{
+ m_iInfoCode = iInfo;
+ m_iInfoCode2 = iInfo2;
+}
+
+bool CGUIProgressControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_guiBackground->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiRight->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiLeft->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiMid->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiOverlay->SetDiffuseColor(m_diffuseColor);
+
+ return changed;
+}
+
+std::string CGUIProgressControl::GetDescription() const
+{
+ return StringUtils::Format("{:1.0f}", m_fPercent);
+}
+
+bool CGUIProgressControl::UpdateLayout(void)
+{
+ bool bChanged(false);
+
+ if (m_width == 0)
+ m_width = m_guiBackground->GetTextureWidth();
+ if (m_height == 0)
+ m_height = m_guiBackground->GetTextureHeight();
+
+ bChanged |= m_guiBackground->SetHeight(m_height);
+ bChanged |= m_guiBackground->SetWidth(m_width);
+
+ float fScaleX, fScaleY;
+ fScaleY =
+ m_guiBackground->GetTextureHeight() ? m_height / m_guiBackground->GetTextureHeight() : 1.0f;
+ fScaleX =
+ m_guiBackground->GetTextureWidth() ? m_width / m_guiBackground->GetTextureWidth() : 1.0f;
+
+ float posX = m_guiBackground->GetXPosition();
+ float posY = m_guiBackground->GetYPosition();
+
+ if (m_guiLeft->GetFileName().empty() && m_guiRight->GetFileName().empty())
+ { // rendering without left and right image - fill the mid image completely
+ float width = (m_fPercent - m_fPercent2) * m_width * 0.01f;
+ float offsetX = m_fPercent2 * m_width * 0.01f;;
+ float offsetY =
+ fabs(fScaleY * 0.5f * (m_guiMid->GetTextureHeight() - m_guiBackground->GetTextureHeight()));
+ bChanged |= m_guiMid->SetPosition(posX + (offsetX > 0 ? offsetX : 0),
+ posY + (offsetY > 0 ? offsetY : 0));
+ bChanged |= m_guiMid->SetHeight(fScaleY * m_guiMid->GetTextureHeight());
+ if (m_bReveal)
+ {
+ bChanged |= m_guiMid->SetWidth(m_width);
+ float x = posX + offsetX, y = posY + offsetY, w = width,
+ h = fScaleY * m_guiMid->GetTextureHeight();
+ CRect rect(x, y, x + w, y + h);
+ if (rect != m_guiMidClipRect)
+ {
+ m_guiMidClipRect = rect;
+ bChanged = true;
+ }
+ }
+ else
+ {
+ bChanged |= m_guiMid->SetWidth(width);
+ m_guiMidClipRect = CRect();
+ }
+ }
+ else
+ {
+ float fWidth = m_fPercent - m_fPercent2;
+ float fFullWidth = m_guiBackground->GetTextureWidth() - m_guiLeft->GetTextureWidth() -
+ m_guiRight->GetTextureWidth();
+ fWidth /= 100.0f;
+ fWidth *= fFullWidth;
+
+ float offsetY = fabs(fScaleY * 0.5f *
+ (m_guiLeft->GetTextureHeight() - m_guiBackground->GetTextureHeight()));
+ bChanged |= m_guiLeft->SetPosition(posX, posY + (offsetY > 0 ? offsetY : 0));
+ bChanged |= m_guiLeft->SetHeight(fScaleY * m_guiLeft->GetTextureHeight());
+ bChanged |= m_guiLeft->SetWidth(fScaleX * m_guiLeft->GetTextureWidth());
+
+ posX += fScaleX * m_guiLeft->GetTextureWidth();
+ float offsetX = m_fPercent2 * fWidth * 0.01f;;
+ offsetY =
+ fabs(fScaleY * 0.5f * (m_guiMid->GetTextureHeight() - m_guiBackground->GetTextureHeight()));
+ bChanged |= m_guiMid->SetPosition(posX + offsetX, posY + (offsetY > 0 ? offsetY : 0));
+ bChanged |= m_guiMid->SetHeight(fScaleY * m_guiMid->GetTextureHeight());
+ if (m_bReveal)
+ {
+ bChanged |= m_guiMid->SetWidth(fScaleX * fFullWidth);
+ float x = posX + offsetX, y = posY + offsetY, w = fScaleX * fWidth,
+ h = fScaleY * m_guiMid->GetTextureHeight();
+ CRect rect(x, y, x + w, y + h);
+ if (rect != m_guiMidClipRect)
+ {
+ m_guiMidClipRect = rect;
+ bChanged = true;
+ }
+ }
+ else
+ {
+ bChanged |= m_guiMid->SetWidth(fScaleX * fWidth);
+ m_guiMidClipRect = CRect();
+ }
+
+ posX += fWidth * fScaleX;
+
+ offsetY = fabs(fScaleY * 0.5f *
+ (m_guiRight->GetTextureHeight() - m_guiBackground->GetTextureHeight()));
+ bChanged |= m_guiRight->SetPosition(posX, posY + (offsetY > 0 ? offsetY : 0));
+ bChanged |= m_guiRight->SetHeight(fScaleY * m_guiRight->GetTextureHeight());
+ bChanged |= m_guiRight->SetWidth(fScaleX * m_guiRight->GetTextureWidth());
+ }
+ float offset = fabs(fScaleY * 0.5f *
+ (m_guiOverlay->GetTextureHeight() - m_guiBackground->GetTextureHeight()));
+ if (offset > 0) // Center texture to the background if necessary
+ bChanged |= m_guiOverlay->SetPosition(m_guiBackground->GetXPosition(),
+ m_guiBackground->GetYPosition() + offset);
+ else
+ bChanged |=
+ m_guiOverlay->SetPosition(m_guiBackground->GetXPosition(), m_guiBackground->GetYPosition());
+ bChanged |= m_guiOverlay->SetHeight(fScaleY * m_guiOverlay->GetTextureHeight());
+ bChanged |= m_guiOverlay->SetWidth(fScaleX * m_guiOverlay->GetTextureWidth());
+
+ return bChanged;
+}
+
+void CGUIProgressControl::UpdateInfo(const CGUIListItem *item)
+{
+ if (!IsDisabled())
+ {
+ if (m_iInfoCode)
+ {
+ int value;
+ if (CServiceBroker::GetGUI()->GetInfoManager().GetInt(value, m_iInfoCode, m_parentID, item))
+ m_fPercent = std::max(0.0f, std::min(static_cast<float>(value), 100.0f));
+ }
+ if (m_iInfoCode2)
+ {
+ int value;
+ if (CServiceBroker::GetGUI()->GetInfoManager().GetInt(value, m_iInfoCode2, m_parentID, item))
+ m_fPercent2 = std::max(0.0f, std::min(static_cast<float>(value), 100.0f));
+ }
+ }
+}
diff --git a/xbmc/guilib/GUIProgressControl.dox b/xbmc/guilib/GUIProgressControl.dox
new file mode 100644
index 0000000..c3b9ac4
--- /dev/null
+++ b/xbmc/guilib/GUIProgressControl.dox
@@ -0,0 +1,62 @@
+/*!
+
+\page Progress_Control Progress Control
+\brief **Used to show the progress of a particular operation.**
+
+\tableofcontents
+
+The progress control is used to show the progress of an item that may take a
+long time, or to show how far through a movie you are. You can choose the
+position, size, and look of the progress control.
+
+
+--------------------------------------------------------------------------------
+\section Progress_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="progress" id="12">
+ <description>My first progress control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>30</height>
+ <visible>true</visible>
+ <reveal>false</reveal>
+ <texturebg>mybackgroundtexture.png</texturebg>
+ <lefttexture>mydowntexture.png</lefttexture>
+ <midtexture>mymidtexture.png</midtexture>
+ <righttexture>myrighttexture.png</righttexture>
+ <overlaytexture>myoverlaytexture.png</overlaytexture>
+ <info></info>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Progress_Control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|----------------:|:--------------------------------------------------------------|
+| reveal | If set to true the midtexture will reveal itself instead of stretching to fill the gap (works best when its the same size as **texturebg**)
+| texturebg | Specifies the image file which should be displayed in the background of the progress control. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| lefttexture | Specifies the image file which should be displayed for the left side of the progress bar. This is rendered on the left side of the bar.
+| midtexture | Specifies the image file which should be displayed for the middle portion of the progress bar. This is the `fill` texture used to fill up the bar. It's positioned on the right of the <b>`<lefttexture>`</b> texture, and fills the gap between the <b>`<lefttexture>`</b> and <b>`<righttexture>`</b> textures, depending on how far progressed the item is.
+| righttexture | Specifies the image file which should be displayed for the right side of the progress bar. This is rendered on the right side of the bar.
+| overlaytexture | Specifies the image file which should be displayed over the top of all other images in the progress bar. It is centered vertically and horizontally within the space taken up by the background image.
+| info | Specifies the information that the progress bar is reporting on. [See here for more information](http://kodi.wiki/view/InfoLabels).
+
+
+--------------------------------------------------------------------------------
+\section Progress_Control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+
+*/
diff --git a/xbmc/guilib/GUIProgressControl.h b/xbmc/guilib/GUIProgressControl.h
new file mode 100644
index 0000000..9509e1c
--- /dev/null
+++ b/xbmc/guilib/GUIProgressControl.h
@@ -0,0 +1,71 @@
+/*
+ * 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
+
+/*!
+\file GUIProgressControl.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUITexture.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIProgressControl :
+ public CGUIControl
+{
+public:
+ CGUIProgressControl(int parentID, int controlID, float posX, float posY,
+ float width, float height, const CTextureInfo& backGroundTexture,
+ const CTextureInfo& leftTexture, const CTextureInfo& midTexture,
+ const CTextureInfo& rightTexture, const CTextureInfo& overlayTexture,
+ bool reveal=false);
+ ~CGUIProgressControl() override = default;
+ CGUIProgressControl* Clone() const override { return new CGUIProgressControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool CanFocus() const override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ bool OnMessage(CGUIMessage& message) override;
+ void SetPosition(float posX, float posY) override;
+ void SetPercentage(float fPercent);
+ void SetInfo(int iInfo, int iInfo2 = 0);
+ int GetInfo() const { return m_iInfoCode; }
+
+ float GetPercentage() const;
+ std::string GetDescription() const override;
+ void UpdateInfo(const CGUIListItem *item = NULL) override;
+ bool UpdateLayout(void);
+protected:
+ bool UpdateColors(const CGUIListItem* item) override;
+ std::unique_ptr<CGUITexture> m_guiBackground;
+ std::unique_ptr<CGUITexture> m_guiLeft;
+ std::unique_ptr<CGUITexture> m_guiMid;
+ std::unique_ptr<CGUITexture> m_guiRight;
+ std::unique_ptr<CGUITexture> m_guiOverlay;
+ CRect m_guiMidClipRect;
+
+ int m_iInfoCode;
+ int m_iInfoCode2 = 0;
+ float m_fPercent;
+ float m_fPercent2 = 0.0f;
+ bool m_bReveal;
+ bool m_bChanged;
+
+private:
+ CGUIProgressControl(const CGUIProgressControl& control);
+};
+
diff --git a/xbmc/guilib/GUIRSSControl.cpp b/xbmc/guilib/GUIRSSControl.cpp
new file mode 100644
index 0000000..f35441e
--- /dev/null
+++ b/xbmc/guilib/GUIRSSControl.cpp
@@ -0,0 +1,196 @@
+/*
+ * 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 "GUIRSSControl.h"
+
+#include "ServiceBroker.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/ColorUtils.h"
+#include "utils/RssManager.h"
+#include "utils/RssReader.h"
+#include "utils/StringUtils.h"
+
+#include <mutex>
+
+using namespace KODI::GUILIB;
+
+CGUIRSSControl::CGUIRSSControl(int parentID, int controlID, float posX, float posY, float width, float height,
+ const CLabelInfo& labelInfo, const GUIINFO::CGUIInfoColor &channelColor,
+ const GUIINFO::CGUIInfoColor &headlineColor, std::string& strRSSTags)
+: CGUIControl(parentID, controlID, posX, posY, width, height),
+ m_strRSSTags(strRSSTags),
+ m_label(labelInfo),
+ m_channelColor(channelColor),
+ m_headlineColor(headlineColor),
+ m_scrollInfo(0,0,labelInfo.scrollSpeed,""),
+ m_dirty(true)
+{
+ m_pReader = NULL;
+ m_rtl = false;
+ m_stopped = false;
+ m_urlset = 1;
+ ControlType = GUICONTROL_RSS;
+}
+
+CGUIRSSControl::CGUIRSSControl(const CGUIRSSControl &from)
+ : CGUIControl(from),
+ m_feed(),
+ m_strRSSTags(from.m_strRSSTags),
+ m_label(from.m_label),
+ m_channelColor(from.m_channelColor),
+ m_headlineColor(from.m_headlineColor),
+ m_vecUrls(),
+ m_vecIntervals(),
+ m_scrollInfo(from.m_scrollInfo),
+ m_dirty(true)
+{
+ m_pReader = NULL;
+ m_rtl = from.m_rtl;
+ m_stopped = from.m_stopped;
+ m_urlset = 1;
+ ControlType = GUICONTROL_RSS;
+}
+
+CGUIRSSControl::~CGUIRSSControl(void)
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ if (m_pReader)
+ m_pReader->SetObserver(NULL);
+ m_pReader = NULL;
+}
+
+void CGUIRSSControl::OnFocus()
+{
+ m_stopped = true;
+}
+
+void CGUIRSSControl::OnUnFocus()
+{
+ m_stopped = false;
+}
+
+void CGUIRSSControl::SetUrlSet(const int urlset)
+{
+ m_urlset = urlset;
+}
+
+bool CGUIRSSControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_label.UpdateColors();
+ changed |= m_headlineColor.Update();
+ changed |= m_channelColor.Update();
+ return changed;
+}
+
+void CGUIRSSControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ bool dirty = false;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_LOOKANDFEEL_ENABLERSSFEEDS) && CRssManager::GetInstance().IsActive())
+ {
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ // Create RSS background/worker thread if needed
+ if (m_pReader == NULL)
+ {
+
+ RssUrls::const_iterator iter = CRssManager::GetInstance().GetUrls().find(m_urlset);
+ if (iter != CRssManager::GetInstance().GetUrls().end())
+ {
+ m_rtl = iter->second.rtl;
+ m_vecUrls = iter->second.url;
+ m_vecIntervals = iter->second.interval;
+ m_scrollInfo.SetSpeed(m_label.scrollSpeed * (m_rtl ? -1 : 1));
+ }
+
+ dirty = true;
+
+ if (CRssManager::GetInstance().GetReader(GetID(), GetParentID(), this, m_pReader))
+ {
+ m_scrollInfo.m_pixelPos = m_pReader->m_savedScrollPixelPos;
+ }
+ else
+ {
+ if (m_strRSSTags != "")
+ {
+ std::vector<std::string> tags = StringUtils::Split(m_strRSSTags, ",");
+ for (const std::string& i : tags)
+ m_pReader->AddTag(i);
+ }
+ // use half the width of the control as spacing between feeds, and double this between feed sets
+ float spaceWidth = (m_label.font) ? m_label.font->GetCharWidth(L' ') : 15;
+ m_pReader->Create(this, m_vecUrls, m_vecIntervals, (int)(0.5f*GetWidth() / spaceWidth) + 1, m_rtl);
+ }
+ }
+
+ if(m_dirty)
+ dirty = true;
+ m_dirty = false;
+
+ if (m_label.font)
+ {
+ if ( m_stopped )
+ m_scrollInfo.SetSpeed(0);
+ else
+ m_scrollInfo.SetSpeed(m_label.scrollSpeed * (m_rtl ? -1 : 1));
+
+ if(m_label.font->UpdateScrollInfo(m_feed, m_scrollInfo))
+ dirty = true;
+ }
+ }
+
+ if(dirty)
+ MarkDirtyRegion();
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIRSSControl::Render()
+{
+ // only render the control if they are enabled
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_LOOKANDFEEL_ENABLERSSFEEDS) && CRssManager::GetInstance().IsActive())
+ {
+
+ if (m_label.font)
+ {
+ std::vector<UTILS::COLOR::Color> colors;
+ colors.push_back(m_label.textColor);
+ colors.push_back(m_headlineColor);
+ colors.push_back(m_channelColor);
+ m_label.font->DrawScrollingText(m_posX, m_posY, colors, m_label.shadowColor, m_feed, 0, m_width, m_scrollInfo);
+ }
+
+ if (m_pReader)
+ {
+ m_pReader->CheckForUpdates();
+ m_pReader->m_savedScrollPixelPos = m_scrollInfo.m_pixelPos;
+ }
+ }
+ CGUIControl::Render();
+}
+
+CRect CGUIRSSControl::CalcRenderRegion() const
+{
+ if (m_label.font)
+ return CRect(m_posX, m_posY, m_posX + m_width, m_posY + m_label.font->GetTextHeight(1));
+ return CGUIControl::CalcRenderRegion();
+}
+
+void CGUIRSSControl::OnFeedUpdate(const vecText &feed)
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ m_feed = feed;
+ m_dirty = true;
+}
+
+void CGUIRSSControl::OnFeedRelease()
+{
+ m_pReader = NULL;
+}
+
+
diff --git a/xbmc/guilib/GUIRSSControl.dox b/xbmc/guilib/GUIRSSControl.dox
new file mode 100644
index 0000000..e51430c
--- /dev/null
+++ b/xbmc/guilib/GUIRSSControl.dox
@@ -0,0 +1,106 @@
+/*!
+
+\page RSS_feed_Control RSS ticker
+\brief **Used to display scrolling RSS feeds.**
+
+\tableofcontents
+
+Kodi can display an RSS feed on the home screen of the default skin/interface
+(Estuary), as well as any other skin that supports RSS feeds. By default,
+the RSS news feed is taken from [http://kodi.tv](http://kodi.tv/), but the feed
+can be changed to almost any RSS feed.
+
+@note Don't confuse the RSS ticker with RSS media source, which allows access to
+video and/or audio RSS streams.
+
+
+--------------------------------------------------------------------------------
+\section RSS_feed_Control_sect1 RSS ticker settings
+
+The RSS ticker can be toggled on or off by going to
+<b>`Settings -> Appearance -> Skin -> Show RSS news feed`</b>
+
+Below this setting one can also change the RSS news feed address.
+
+
+--------------------------------------------------------------------------------
+\section RSS_feed_Control_sect2 Technical documentation for skinners
+
+ Main page: [Skin development](http://kodi.wiki/view/Skin_development)
+
+\subsection RSS_feed_Control_sect2_1 RSS control
+The rss control is used for displaying scrolling RSS feeds from the internet
+in Kodi. You can choose the font, size, colour, location and the RSS feed to
+be displayed.
+
+__Example:__
+
+~~~~~~~~~~~~~
+<control type="rss" id="1">
+ <description>My First RSS control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>500</width>
+ <visible>true</visible>
+ <font>font14</font>
+ <textcolor>FFB2D4F5</textcolor>
+ <headlinecolor>FFFFFFFF</headlinecolor>
+ <titlecolor>FF655656</titlecolor>
+</control>
+~~~~~~~~~~~~~
+
+
+\subsection RSS_feed_Control_sect2_2 Available tags and attributes
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| id | This refers to the feedset to be displayed. This is the id reference to the <b>`<set>`</b> section in [RssFeeds.xml](http://kodi.wiki/view/RssFeeds.xml) (see below):
+| font | Specifies the font to use from the font.xml file.
+| textcolor | Specified the color the text should be. In hex **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| shadowcolor | Specifies the color of the drop shadow on the text. In **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| headlinecolor | Specified the color that any highlighted text should be. In hex **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| titlecolor | Specified the color the titles of the feeds should be. In hex **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| scrollspeed | Scroll speed of text in pixels per second.
+
+
+\subsection RSS_feed_Control_sect2_3 RssFeeds.xml
+
+- <em>Main page: [RssFeeds.xml](http://kodi.wiki/view/RssFeeds.xml)</em>
+
+The actual content of the RSS feed is defined in the
+[RssFeeds.xml](http://kodi.wiki/view/RssFeeds.xml) file stored in the user's
+profile. Here is an example :
+
+~~~~~~~~~~~~~
+ <rssfeeds>
+ <set id="1">
+ <feed updateinterval="30">http://feeds.feedburner.com/XboxScene</feed>
+ <feed updateinterval="30">http://feeds.wired.com/wired/topheadlines</feed>
+ </set>
+ <set id="2">
+ <feed updateinterval="30">http://www.cnet.co.uk/feeds/public/rss_news_10.htm</feed>
+ </set>
+ </rssfeeds>
+~~~~~~~~~~~~~
+
+As can be seen, each feedset has an id attribute – this is what we are
+referencing in the <b>`<id>`</b> attribute of the control. There can be more
+than one <b>`<set>`</b> defined, and more than one <b>`<feed>`</b> per
+set. The <b>`<feed>`</b>'s must be escaped so that they're xml-safe (ie replace &
+with & etc.). Each feed in the set runs through in the order they are defined.
+
+
+--------------------------------------------------------------------------------
+\section RSS_feed_Control_sect3 See also
+
+- [RssFeeds.xml](http://kodi.wiki/view/RssFeeds.xml)
+
+#### Development:
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIRSSControl.h b/xbmc/guilib/GUIRSSControl.h
new file mode 100644
index 0000000..851f72f
--- /dev/null
+++ b/xbmc/guilib/GUIRSSControl.h
@@ -0,0 +1,72 @@
+/*
+ * 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
+
+/*!
+\file GUIRSSControl.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUILabel.h"
+#include "utils/IRssObserver.h"
+
+#include <vector>
+
+class CRssReader;
+
+/*!
+\ingroup controls
+\brief
+*/
+class CGUIRSSControl : public CGUIControl, public IRssObserver
+{
+public:
+ CGUIRSSControl(int parentID, int controlID, float posX, float posY, float width, float height,
+ const CLabelInfo& labelInfo, const KODI::GUILIB::GUIINFO::CGUIInfoColor &channelColor,
+ const KODI::GUILIB::GUIINFO::CGUIInfoColor &headlineColor, std::string& strRSSTags);
+ CGUIRSSControl(const CGUIRSSControl &from);
+ ~CGUIRSSControl(void) override;
+ CGUIRSSControl* Clone() const override { return new CGUIRSSControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ void OnFeedUpdate(const vecText &feed) override;
+ void OnFeedRelease() override;
+ bool CanFocus() const override { return true; }
+ CRect CalcRenderRegion() const override;
+
+ void OnFocus() override;
+ void OnUnFocus() override;
+
+ void SetUrlSet(const int urlset);
+
+protected:
+ bool UpdateColors(const CGUIListItem* item) override;
+
+ CCriticalSection m_criticalSection;
+
+ CRssReader* m_pReader;
+ vecText m_feed;
+
+ std::string m_strRSSTags;
+
+ CLabelInfo m_label;
+ KODI::GUILIB::GUIINFO::CGUIInfoColor m_channelColor;
+ KODI::GUILIB::GUIINFO::CGUIInfoColor m_headlineColor;
+
+ std::vector<std::string> m_vecUrls;
+ std::vector<int> m_vecIntervals;
+ bool m_rtl;
+ CScrollInfo m_scrollInfo;
+ bool m_dirty;
+ bool m_stopped;
+ int m_urlset;
+};
+
diff --git a/xbmc/guilib/GUIRadioButtonControl.cpp b/xbmc/guilib/GUIRadioButtonControl.cpp
new file mode 100644
index 0000000..cc169ec
--- /dev/null
+++ b/xbmc/guilib/GUIRadioButtonControl.cpp
@@ -0,0 +1,260 @@
+/*
+ * 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 "GUIRadioButtonControl.h"
+
+#include "GUIInfoManager.h"
+#include "LocalizeStrings.h"
+#include "input/Key.h"
+
+CGUIRadioButtonControl::CGUIRadioButtonControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& textureFocus,
+ const CTextureInfo& textureNoFocus,
+ const CLabelInfo& labelInfo,
+ const CTextureInfo& radioOnFocus,
+ const CTextureInfo& radioOnNoFocus,
+ const CTextureInfo& radioOffFocus,
+ const CTextureInfo& radioOffNoFocus,
+ const CTextureInfo& radioOnDisabled,
+ const CTextureInfo& radioOffDisabled)
+ : CGUIButtonControl(
+ parentID, controlID, posX, posY, width, height, textureFocus, textureNoFocus, labelInfo),
+ m_imgRadioOnFocus(CGUITexture::CreateTexture(posX, posY, 16, 16, radioOnFocus)),
+ m_imgRadioOnNoFocus(CGUITexture::CreateTexture(posX, posY, 16, 16, radioOnNoFocus)),
+ m_imgRadioOffFocus(CGUITexture::CreateTexture(posX, posY, 16, 16, radioOffFocus)),
+ m_imgRadioOffNoFocus(CGUITexture::CreateTexture(posX, posY, 16, 16, radioOffNoFocus)),
+ m_imgRadioOnDisabled(CGUITexture::CreateTexture(posX, posY, 16, 16, radioOnDisabled)),
+ m_imgRadioOffDisabled(CGUITexture::CreateTexture(posX, posY, 16, 16, radioOffDisabled))
+{
+ m_radioPosX = 0;
+ m_radioPosY = 0;
+ m_imgRadioOnFocus->SetAspectRatio(CAspectRatio::AR_KEEP);
+ m_imgRadioOnNoFocus->SetAspectRatio(CAspectRatio::AR_KEEP);
+ m_imgRadioOffFocus->SetAspectRatio(CAspectRatio::AR_KEEP);
+ m_imgRadioOffNoFocus->SetAspectRatio(CAspectRatio::AR_KEEP);
+ m_imgRadioOnDisabled->SetAspectRatio(CAspectRatio::AR_KEEP);
+ m_imgRadioOffDisabled->SetAspectRatio(CAspectRatio::AR_KEEP);
+ ControlType = GUICONTROL_RADIO;
+ m_useLabel2 = false;
+}
+
+CGUIRadioButtonControl::CGUIRadioButtonControl(const CGUIRadioButtonControl& control)
+ : CGUIButtonControl(control),
+ m_imgRadioOnFocus(control.m_imgRadioOnFocus->Clone()),
+ m_imgRadioOnNoFocus(control.m_imgRadioOnNoFocus->Clone()),
+ m_imgRadioOffFocus(control.m_imgRadioOffFocus->Clone()),
+ m_imgRadioOffNoFocus(control.m_imgRadioOffNoFocus->Clone()),
+ m_imgRadioOnDisabled(control.m_imgRadioOnDisabled->Clone()),
+ m_imgRadioOffDisabled(control.m_imgRadioOffDisabled->Clone()),
+ m_radioPosX(control.m_radioPosX),
+ m_radioPosY(control.m_radioPosY),
+ m_toggleSelect(control.m_toggleSelect),
+ m_useLabel2(control.m_useLabel2)
+{
+}
+
+void CGUIRadioButtonControl::Render()
+{
+ CGUIButtonControl::Render();
+
+ if ( IsSelected() && !IsDisabled() )
+ {
+ if (HasFocus())
+ m_imgRadioOnFocus->Render();
+ else
+ m_imgRadioOnNoFocus->Render();
+ }
+ else if ( !IsSelected() && !IsDisabled() )
+ {
+ if (HasFocus())
+ m_imgRadioOffFocus->Render();
+ else
+ m_imgRadioOffNoFocus->Render();
+ }
+ else if ( IsSelected() && IsDisabled() )
+ m_imgRadioOnDisabled->Render();
+ else
+ m_imgRadioOffDisabled->Render();
+}
+
+void CGUIRadioButtonControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_toggleSelect)
+ {
+ // ask our infoManager whether we are selected or not...
+ bool selected = m_toggleSelect->Get(INFO::DEFAULT_CONTEXT);
+
+ if (selected != m_bSelected)
+ {
+ MarkDirtyRegion();
+ m_bSelected = selected;
+ }
+ }
+
+ m_imgRadioOnFocus->Process(currentTime);
+ m_imgRadioOnNoFocus->Process(currentTime);
+ m_imgRadioOffFocus->Process(currentTime);
+ m_imgRadioOffNoFocus->Process(currentTime);
+ m_imgRadioOnDisabled->Process(currentTime);
+ m_imgRadioOffDisabled->Process(currentTime);
+
+ if (m_useLabel2)
+ SetLabel2(g_localizeStrings.Get(m_bSelected ? 16041 : 351));
+
+ CGUIButtonControl::Process(currentTime, dirtyregions);
+}
+
+bool CGUIRadioButtonControl::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_SELECT_ITEM)
+ {
+ m_bSelected = !m_bSelected;
+ MarkDirtyRegion();
+ }
+ return CGUIButtonControl::OnAction(action);
+}
+
+bool CGUIRadioButtonControl::OnMessage(CGUIMessage& message)
+{
+ return CGUIButtonControl::OnMessage(message);
+}
+
+void CGUIRadioButtonControl::AllocResources()
+{
+ CGUIButtonControl::AllocResources();
+ m_imgRadioOnFocus->AllocResources();
+ m_imgRadioOnNoFocus->AllocResources();
+ m_imgRadioOffFocus->AllocResources();
+ m_imgRadioOffNoFocus->AllocResources();
+ m_imgRadioOnDisabled->AllocResources();
+ m_imgRadioOffDisabled->AllocResources();
+ SetPosition(m_posX, m_posY);
+}
+
+void CGUIRadioButtonControl::FreeResources(bool immediately)
+{
+ CGUIButtonControl::FreeResources(immediately);
+ m_imgRadioOnFocus->FreeResources(immediately);
+ m_imgRadioOnNoFocus->FreeResources(immediately);
+ m_imgRadioOffFocus->FreeResources(immediately);
+ m_imgRadioOffNoFocus->FreeResources(immediately);
+ m_imgRadioOnDisabled->FreeResources(immediately);
+ m_imgRadioOffDisabled->FreeResources(immediately);
+}
+
+void CGUIRadioButtonControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+ m_imgRadioOnFocus->DynamicResourceAlloc(bOnOff);
+ m_imgRadioOnNoFocus->DynamicResourceAlloc(bOnOff);
+ m_imgRadioOffFocus->DynamicResourceAlloc(bOnOff);
+ m_imgRadioOffNoFocus->DynamicResourceAlloc(bOnOff);
+ m_imgRadioOnDisabled->DynamicResourceAlloc(bOnOff);
+ m_imgRadioOffDisabled->DynamicResourceAlloc(bOnOff);
+}
+
+void CGUIRadioButtonControl::SetInvalid()
+{
+ CGUIButtonControl::SetInvalid();
+ m_imgRadioOnFocus->SetInvalid();
+ m_imgRadioOnNoFocus->SetInvalid();
+ m_imgRadioOffFocus->SetInvalid();
+ m_imgRadioOffNoFocus->SetInvalid();
+ m_imgRadioOnDisabled->SetInvalid();
+ m_imgRadioOffDisabled->SetInvalid();
+}
+
+void CGUIRadioButtonControl::SetPosition(float posX, float posY)
+{
+ CGUIButtonControl::SetPosition(posX, posY);
+ float radioPosX =
+ m_radioPosX ? m_posX + m_radioPosX : (m_posX + m_width - 8) - m_imgRadioOnFocus->GetWidth();
+ float radioPosY =
+ m_radioPosY ? m_posY + m_radioPosY : m_posY + (m_height - m_imgRadioOnFocus->GetHeight()) / 2;
+ m_imgRadioOnFocus->SetPosition(radioPosX, radioPosY);
+ m_imgRadioOnNoFocus->SetPosition(radioPosX, radioPosY);
+ m_imgRadioOffFocus->SetPosition(radioPosX, radioPosY);
+ m_imgRadioOffNoFocus->SetPosition(radioPosX, radioPosY);
+ m_imgRadioOnDisabled->SetPosition(radioPosX, radioPosY);
+ m_imgRadioOffDisabled->SetPosition(radioPosX, radioPosY);
+}
+
+void CGUIRadioButtonControl::SetRadioDimensions(float posX, float posY, float width, float height)
+{
+ m_radioPosX = posX;
+ m_radioPosY = posY;
+ if (width)
+ {
+ m_imgRadioOnFocus->SetWidth(width);
+ m_imgRadioOnNoFocus->SetWidth(width);
+ m_imgRadioOffFocus->SetWidth(width);
+ m_imgRadioOffNoFocus->SetWidth(width);
+ m_imgRadioOnDisabled->SetWidth(width);
+ m_imgRadioOffDisabled->SetWidth(width);
+ }
+ if (height)
+ {
+ m_imgRadioOnFocus->SetHeight(height);
+ m_imgRadioOnNoFocus->SetHeight(height);
+ m_imgRadioOffFocus->SetHeight(height);
+ m_imgRadioOffNoFocus->SetHeight(height);
+ m_imgRadioOnDisabled->SetHeight(height);
+ m_imgRadioOffDisabled->SetHeight(height);
+ }
+
+ // use label2 to display the button value in case no
+ // dimensions were specified and there's no label2 yet.
+ if (GetLabel2().empty() && !width && !height)
+ m_useLabel2 = true;
+
+ SetPosition(GetXPosition(), GetYPosition());
+}
+
+void CGUIRadioButtonControl::SetWidth(float width)
+{
+ CGUIButtonControl::SetWidth(width);
+ SetPosition(GetXPosition(), GetYPosition());
+}
+
+void CGUIRadioButtonControl::SetHeight(float height)
+{
+ CGUIButtonControl::SetHeight(height);
+ SetPosition(GetXPosition(), GetYPosition());
+}
+
+std::string CGUIRadioButtonControl::GetDescription() const
+{
+ std::string strLabel = CGUIButtonControl::GetDescription();
+ if (m_bSelected)
+ strLabel += " (*)";
+ else
+ strLabel += " ( )";
+ return strLabel;
+}
+
+bool CGUIRadioButtonControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIButtonControl::UpdateColors(nullptr);
+ changed |= m_imgRadioOnFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgRadioOnNoFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgRadioOffFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgRadioOffNoFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgRadioOnDisabled->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgRadioOffDisabled->SetDiffuseColor(m_diffuseColor);
+ return changed;
+}
+
+void CGUIRadioButtonControl::SetToggleSelect(const std::string &toggleSelect)
+{
+ m_toggleSelect = CServiceBroker::GetGUI()->GetInfoManager().Register(toggleSelect, GetParentID());
+}
diff --git a/xbmc/guilib/GUIRadioButtonControl.dox b/xbmc/guilib/GUIRadioButtonControl.dox
new file mode 100644
index 0000000..d69eed2
--- /dev/null
+++ b/xbmc/guilib/GUIRadioButtonControl.dox
@@ -0,0 +1,102 @@
+/*!
+
+\page Radio_button_control Radio button control
+\brief **A radio button control (as used for on/off settings).**
+
+\tableofcontents
+
+The radio button control is used for creating push button on/off settings in
+Kodi. You can choose the position, size, and look of the button. When the user
+clicks on the radio button, the state will change, toggling the extra textures
+(`textureradioon` and `textureradiooff`). Used for settings controls.
+
+--------------------------------------------------------------------------------
+\section Radio_button_control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="radiobutton" id="2">
+ <description>My first radiobutton control</description>
+ <type>radiobutton</type>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+ <colordiffuse>FFFFFFFF</colordiffuse>
+ <texturefocus>myfocustexture.png</texturefocus>
+ <texturenofocus>mynormaltexture.png</texturenofocus>
+ <textureradioonfocus colordiffuse="FFFFAAFF">myradiobutton.png</textureradioonfocus>
+ <textureradioonnofocus colordiffuse="FFFFAAFF">myradiobutton.png</textureradioonnofocus>
+ <textureradioofffocus colordiffuse="FFFFAAFF">myradiobutton_nf.png</textureradioofffocus>
+ <textureradiooffnofocus colordiffuse="FFFFAAFF">myradiobutton_nf.png</textureradiooffnofocus>
+ <selected>Player.Paused</selected>
+ <onclick>PlayerControls(Pause)</onclick>
+ <label>29</label>
+ <font>font12</font>
+ <textcolor>FFFFFFFF</textcolor>
+ <focusedcolor>FFFFFFFF</focusedcolor>
+ <disabledcolor>80FFFFFF</disabledcolor>
+ <align>left</align>
+ <aligny>center</aligny>
+ <textoffsetx>4</textoffsetx>
+ <textoffsety>5</textoffsety>
+ <pulseonselect>false</pulseonselect>
+ <onfocus>-</onfocus>
+ <onunfocus>-</onunfocus>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Radio_button_control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|------------------------:|:--------------------------------------------------------------|
+| texturefocus | Specifies the image file which should be displayed when the button has focus. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| texturenofocus | Specifies the image file which should be displayed when the button does not have focus.
+| textureradioonfocus | Specifies the image file which should be displayed for the radio button portion when it's the button is on and focused. This texture is positioned on the right of the button – it's positioned 24 pixels from the right edge of the button, and 8 pixels above the center vertically.
+| textureradioonnofocus | Specifies the image file which should be displayed for the radio button portion when it's the button is on and unfocused. This texture is positioned on the right of the button – it's positioned 24 pixels from the right edge of the button, and 8 pixels above the center vertically.
+| textureradioon | A shortcut to set both of the above textures to the same image file.
+| textureradioondisabled | Specifies the image file which should be displayed for the radio button portion when the button is on and disabled.
+| textureradioofffocus | Specifies the image file which should be displayed for the radio button portion when the button is off and focused.
+| textureradiooffnofocus | Specifies the image file which should be displayed for the radio button portion when the button is off and unfocused.
+| textureradiooff | A shortcut to set both of the above textures to the same image file.
+| textureradioondisabled | Specifies the image file which should be displayed for the radio button portion when the button is off and disabled.
+| label | The label used on the button. It can be a link into strings.po, or an actual text label.
+| label2 | Optional. Will display an 'on' or 'off' label. Only available if you specify an empty radiowidth and radioheight.
+| font | Font used for the button label. From fonts.xml.
+| textcolor | Color used for displaying the button label. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| focusedcolor | Color used for the button label when the button has in focus. In **AARRGGBB** hex format or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| disabledcolor | Color used for the button label if the button is disabled. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| shadowcolor | Specifies the color of the drop shadow on the text, in **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| align | Label horizontal alignment on the button. Defaults to left, can also be center or right.
+| aligny | Label vertical alignment on the button. Defaults to top, can also be center.
+| textoffsetx | Amount to offset the label from the left (or right) edge of the button when using left or right alignment.
+| textoffsety | Amount to offset the label from the top edge of the button when using top alignment.
+| selected | The boolean condition that when met will cause the control to become selected. [see here for more information](http://kodi.wiki/view/Conditional_Visibility).
+| onclick | The function to perform when the radio button is clicked. Should be a [built in function](http://kodi.wiki/view/Built-in_functions_available_to_FTP,_Webserver,_skins,_keymap_and_to_python).
+| radioposx | X offset of the dot or radio button itself
+| radioposy | Y offset of the dot or radio button itself
+| radiowidth | Width in Pixels of the dot or radio button itself
+| radioheight | Height in Pixels of the dot or radio button itself
+| onfocus | Specifies the action to perform when the button is focused. Should be a built in function. The action is performed after any focus animations have completed. See here for more information.
+| onunfocus | Specifies the action to perform when the button loses focus. Should be a built in function.
+
+
+--------------------------------------------------------------------------------
+\section Radio_button_control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIRadioButtonControl.h b/xbmc/guilib/GUIRadioButtonControl.h
new file mode 100644
index 0000000..9befd52
--- /dev/null
+++ b/xbmc/guilib/GUIRadioButtonControl.h
@@ -0,0 +1,68 @@
+/*
+ * 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
+
+/*!
+\file GUIRadioButtonControl.h
+\brief
+*/
+
+#include "GUIButtonControl.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIRadioButtonControl :
+ public CGUIButtonControl
+{
+public:
+ CGUIRadioButtonControl(int parentID, int controlID,
+ float posX, float posY, float width, float height,
+ const CTextureInfo& textureFocus, const CTextureInfo& textureNoFocus,
+ const CLabelInfo& labelInfo,
+ const CTextureInfo& radioOnFocus, const CTextureInfo& radioOnNoFocus,
+ const CTextureInfo& radioOffFocus, const CTextureInfo& radioOffNoFocus,
+ const CTextureInfo& radioOnDisabled, const CTextureInfo& radioOffDisabled);
+
+ ~CGUIRadioButtonControl() override = default;
+ CGUIRadioButtonControl* Clone() const override { return new CGUIRadioButtonControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnAction(const CAction &action) override ;
+ bool OnMessage(CGUIMessage& message) override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ void SetPosition(float posX, float posY) override;
+ void SetWidth(float width) override;
+ void SetHeight(float height) override;
+ std::string GetDescription() const override;
+ void SetRadioDimensions(float posX, float posY, float width, float height);
+ void SetToggleSelect(const std::string &toggleSelect);
+ bool IsSelected() const { return m_bSelected; }
+
+protected:
+ bool UpdateColors(const CGUIListItem* item) override;
+ std::unique_ptr<CGUITexture> m_imgRadioOnFocus;
+ std::unique_ptr<CGUITexture> m_imgRadioOnNoFocus;
+ std::unique_ptr<CGUITexture> m_imgRadioOffFocus;
+ std::unique_ptr<CGUITexture> m_imgRadioOffNoFocus;
+ std::unique_ptr<CGUITexture> m_imgRadioOnDisabled;
+ std::unique_ptr<CGUITexture> m_imgRadioOffDisabled;
+ float m_radioPosX;
+ float m_radioPosY;
+ INFO::InfoPtr m_toggleSelect;
+ bool m_useLabel2;
+
+private:
+ CGUIRadioButtonControl(const CGUIRadioButtonControl& control);
+};
diff --git a/xbmc/guilib/GUIRangesControl.cpp b/xbmc/guilib/GUIRangesControl.cpp
new file mode 100644
index 0000000..6abfbd1
--- /dev/null
+++ b/xbmc/guilib/GUIRangesControl.cpp
@@ -0,0 +1,414 @@
+/*
+ * 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.
+ */
+
+#include "GUIRangesControl.h"
+
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <cmath>
+
+CGUIRangesControl::CGUIRange::CGUIRange(float fPosX,
+ float fPosY,
+ float fWidth,
+ float fHeight,
+ const CTextureInfo& lowerTextureInfo,
+ const CTextureInfo& fillTextureInfo,
+ const CTextureInfo& upperTextureInfo,
+ const std::pair<float, float>& percentages)
+ : m_guiLowerTexture(CGUITexture::CreateTexture(fPosX, fPosY, fWidth, fHeight, lowerTextureInfo)),
+ m_guiFillTexture(CGUITexture::CreateTexture(fPosX, fPosY, fWidth, fHeight, fillTextureInfo)),
+ m_guiUpperTexture(CGUITexture::CreateTexture(fPosX, fPosY, fWidth, fHeight, upperTextureInfo)),
+ m_percentValues(percentages)
+{
+}
+
+CGUIRangesControl::CGUIRange::CGUIRange(const CGUIRange& range)
+ : m_guiLowerTexture(range.m_guiLowerTexture->Clone()),
+ m_guiFillTexture(range.m_guiFillTexture->Clone()),
+ m_guiUpperTexture(range.m_guiUpperTexture->Clone()),
+ m_percentValues(range.m_percentValues)
+{
+}
+
+void CGUIRangesControl::CGUIRange::AllocResources()
+{
+ m_guiFillTexture->AllocResources();
+ m_guiUpperTexture->AllocResources();
+ m_guiLowerTexture->AllocResources();
+
+ m_guiFillTexture->SetHeight(20);
+ m_guiUpperTexture->SetHeight(20);
+ m_guiLowerTexture->SetHeight(20);
+}
+
+void CGUIRangesControl::CGUIRange::FreeResources(bool bImmediately)
+{
+ m_guiFillTexture->FreeResources(bImmediately);
+ m_guiUpperTexture->FreeResources(bImmediately);
+ m_guiLowerTexture->FreeResources(bImmediately);
+}
+
+void CGUIRangesControl::CGUIRange::DynamicResourceAlloc(bool bOnOff)
+{
+ m_guiFillTexture->DynamicResourceAlloc(bOnOff);
+ m_guiUpperTexture->DynamicResourceAlloc(bOnOff);
+ m_guiLowerTexture->DynamicResourceAlloc(bOnOff);
+}
+
+void CGUIRangesControl::CGUIRange::SetInvalid()
+{
+ m_guiFillTexture->SetInvalid();
+ m_guiUpperTexture->SetInvalid();
+ m_guiLowerTexture->SetInvalid();
+}
+
+bool CGUIRangesControl::CGUIRange::SetDiffuseColor(const KODI::GUILIB::GUIINFO::CGUIInfoColor& color)
+{
+ bool bChanged = false;
+ bChanged |= m_guiFillTexture->SetDiffuseColor(color);
+ bChanged |= m_guiUpperTexture->SetDiffuseColor(color);
+ bChanged |= m_guiLowerTexture->SetDiffuseColor(color);
+ return bChanged;
+}
+
+bool CGUIRangesControl::CGUIRange::Process(unsigned int currentTime)
+{
+ bool bChanged = false;
+ bChanged |= m_guiFillTexture->Process(currentTime);
+ bChanged |= m_guiUpperTexture->Process(currentTime);
+ bChanged |= m_guiLowerTexture->Process(currentTime);
+ return bChanged;
+}
+
+void CGUIRangesControl::CGUIRange::Render()
+{
+ if (m_guiLowerTexture->GetFileName().empty() && m_guiUpperTexture->GetFileName().empty())
+ {
+ if (m_guiFillTexture->GetWidth() > 0)
+ m_guiFillTexture->Render();
+ }
+ else
+ {
+ m_guiLowerTexture->Render();
+
+ if (m_guiFillTexture->GetWidth() > 0)
+ m_guiFillTexture->Render();
+
+ m_guiUpperTexture->Render();
+ }
+}
+
+bool CGUIRangesControl::CGUIRange::UpdateLayout(float fBackgroundTextureHeight,
+ float fPosX, float fPosY, float fWidth,
+ float fScaleX, float fScaleY)
+{
+ bool bChanged = false;
+
+ if (m_guiLowerTexture->GetFileName().empty() && m_guiUpperTexture->GetFileName().empty())
+ {
+ // rendering without left and right image - fill the mid image completely
+ float width = (m_percentValues.second - m_percentValues.first) * fWidth * 0.01f;
+ float offsetX = m_percentValues.first * fWidth * 0.01f;
+ float offsetY = std::fabs(fScaleY * 0.5f *
+ (m_guiFillTexture->GetTextureHeight() - fBackgroundTextureHeight));
+ bChanged |= m_guiFillTexture->SetPosition(fPosX + (offsetX > 0 ? offsetX : 0),
+ fPosY + (offsetY > 0 ? offsetY : 0));
+ bChanged |= m_guiFillTexture->SetHeight(fScaleY * m_guiFillTexture->GetTextureHeight());
+ bChanged |= m_guiFillTexture->SetWidth(width);
+ }
+ else
+ {
+ float offsetX =
+ m_percentValues.first * fWidth * 0.01f - m_guiLowerTexture->GetTextureWidth() * 0.5f;
+ float offsetY = std::fabs(fScaleY * 0.5f *
+ (m_guiLowerTexture->GetTextureHeight() - fBackgroundTextureHeight));
+ bChanged |= m_guiLowerTexture->SetPosition(fPosX + (offsetX > 0 ? offsetX : 0),
+ fPosY + (offsetY > 0 ? offsetY : 0));
+ bChanged |= m_guiLowerTexture->SetHeight(fScaleY * m_guiLowerTexture->GetTextureHeight());
+ bChanged |= m_guiLowerTexture->SetWidth(m_percentValues.first == 0.0f
+ ? m_guiLowerTexture->GetTextureWidth() * 0.5f
+ : m_guiLowerTexture->GetTextureWidth());
+
+ if (m_percentValues.first != m_percentValues.second)
+ {
+ float width = (m_percentValues.second - m_percentValues.first) * fWidth * 0.01f -
+ m_guiLowerTexture->GetTextureWidth() * 0.5f -
+ m_guiUpperTexture->GetTextureWidth() * 0.5f;
+
+ offsetX += m_guiLowerTexture->GetTextureWidth();
+ offsetY = std::fabs(fScaleY * 0.5f *
+ (m_guiFillTexture->GetTextureHeight() - fBackgroundTextureHeight));
+ bChanged |=
+ m_guiFillTexture->SetPosition(fPosX + offsetX, fPosY + (offsetY > 0 ? offsetY : 0));
+ bChanged |= m_guiFillTexture->SetHeight(fScaleY * m_guiFillTexture->GetTextureHeight());
+ bChanged |= m_guiFillTexture->SetWidth(width);
+
+ offsetX += width;
+ offsetY = std::fabs(fScaleY * 0.5f *
+ (m_guiUpperTexture->GetTextureHeight() - fBackgroundTextureHeight));
+ bChanged |=
+ m_guiUpperTexture->SetPosition(fPosX + offsetX, fPosY + (offsetY > 0 ? offsetY : 0));
+ bChanged |= m_guiUpperTexture->SetHeight(fScaleY * m_guiUpperTexture->GetTextureHeight());
+ bChanged |= m_guiUpperTexture->SetWidth(m_percentValues.first == 100.0f
+ ? m_guiUpperTexture->GetTextureWidth() * 0.5f
+ : m_guiUpperTexture->GetTextureWidth());
+ }
+ else
+ {
+ // render only lower texture if range is zero
+ bChanged |= m_guiFillTexture->SetVisible(false);
+ bChanged |= m_guiUpperTexture->SetVisible(false);
+ }
+ }
+ return bChanged;
+}
+
+CGUIRangesControl::CGUIRangesControl(int iParentID,
+ int iControlID,
+ float fPosX,
+ float fPosY,
+ float fWidth,
+ float fHeight,
+ const CTextureInfo& backGroundTextureInfo,
+ const CTextureInfo& lowerTextureInfo,
+ const CTextureInfo& fillTextureInfo,
+ const CTextureInfo& upperTextureInfo,
+ const CTextureInfo& overlayTextureInfo,
+ int iInfo)
+ : CGUIControl(iParentID, iControlID, fPosX, fPosY, fWidth, fHeight),
+ m_guiBackground(
+ CGUITexture::CreateTexture(fPosX, fPosY, fWidth, fHeight, backGroundTextureInfo)),
+ m_guiOverlay(CGUITexture::CreateTexture(fPosX, fPosY, fWidth, fHeight, overlayTextureInfo)),
+ m_guiLowerTextureInfo(lowerTextureInfo),
+ m_guiFillTextureInfo(fillTextureInfo),
+ m_guiUpperTextureInfo(upperTextureInfo),
+ m_iInfoCode(iInfo)
+{
+ ControlType = GUICONTROL_RANGES;
+}
+
+CGUIRangesControl::CGUIRangesControl(const CGUIRangesControl& control)
+ : CGUIControl(control),
+ m_guiBackground(control.m_guiBackground->Clone()),
+ m_guiOverlay(control.m_guiOverlay->Clone()),
+ m_guiLowerTextureInfo(control.m_guiLowerTextureInfo),
+ m_guiFillTextureInfo(control.m_guiFillTextureInfo),
+ m_guiUpperTextureInfo(control.m_guiUpperTextureInfo),
+ m_ranges(control.m_ranges),
+ m_iInfoCode(control.m_iInfoCode),
+ m_prevRanges(control.m_prevRanges)
+{
+}
+
+void CGUIRangesControl::SetPosition(float fPosX, float fPosY)
+{
+ // everything is positioned based on the background image position
+ CGUIControl::SetPosition(fPosX, fPosY);
+ m_guiBackground->SetPosition(fPosX, fPosY);
+}
+
+void CGUIRangesControl::Process(unsigned int iCurrentTime, CDirtyRegionList& dirtyregions)
+{
+ bool bChanged = false;
+
+ if (!IsDisabled())
+ bChanged |= UpdateLayout();
+
+ bChanged |= m_guiBackground->Process(iCurrentTime);
+ bChanged |= m_guiOverlay->Process(iCurrentTime);
+
+ for (auto& range : m_ranges)
+ bChanged |= range.Process(iCurrentTime);
+
+ if (bChanged)
+ MarkDirtyRegion();
+
+ CGUIControl::Process(iCurrentTime, dirtyregions);
+}
+
+void CGUIRangesControl::Render()
+{
+ if (!IsDisabled())
+ {
+ m_guiBackground->Render();
+
+ for (auto& range : m_ranges)
+ range.Render();
+
+ m_guiOverlay->Render();
+ }
+
+ CGUIControl::Render();
+}
+
+
+bool CGUIRangesControl::CanFocus() const
+{
+ return false;
+}
+
+
+void CGUIRangesControl::SetRanges(const std::vector<std::pair<float, float>>& ranges)
+{
+ ClearRanges();
+ for (const auto& range : ranges)
+ m_ranges.emplace_back(CGUIRange(m_posX, m_posY, m_width, m_height,
+ m_guiLowerTextureInfo, m_guiFillTextureInfo, m_guiUpperTextureInfo, range));
+
+ for (auto& range : m_ranges)
+ range.AllocResources(); // note: we need to alloc the instance actually inserted into the vector; hence the second loop.
+}
+
+void CGUIRangesControl::ClearRanges()
+{
+ for (auto& range : m_ranges)
+ range.FreeResources(true);
+
+ m_ranges.clear();
+ m_prevRanges.clear();
+}
+
+void CGUIRangesControl::FreeResources(bool bImmediately /* = false */)
+{
+ CGUIControl::FreeResources(bImmediately);
+
+ m_guiBackground->FreeResources(bImmediately);
+ m_guiOverlay->FreeResources(bImmediately);
+
+ for (auto& range : m_ranges)
+ range.FreeResources(bImmediately);
+}
+
+void CGUIRangesControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+
+ m_guiBackground->DynamicResourceAlloc(bOnOff);
+ m_guiOverlay->DynamicResourceAlloc(bOnOff);
+
+ for (auto& range : m_ranges)
+ range.DynamicResourceAlloc(bOnOff);
+}
+
+void CGUIRangesControl::AllocResources()
+{
+ CGUIControl::AllocResources();
+
+ m_guiBackground->AllocResources();
+ m_guiOverlay->AllocResources();
+
+ m_guiBackground->SetHeight(25);
+ m_guiOverlay->SetHeight(20);
+
+ for (auto& range : m_ranges)
+ range.AllocResources();
+}
+
+void CGUIRangesControl::SetInvalid()
+{
+ CGUIControl::SetInvalid();
+
+ m_guiBackground->SetInvalid();
+ m_guiOverlay->SetInvalid();
+
+ for (auto& range : m_ranges)
+ range.SetInvalid();
+}
+
+bool CGUIRangesControl::UpdateColors(const CGUIListItem* item)
+{
+ bool bChanged = CGUIControl::UpdateColors(nullptr);
+
+ bChanged |= m_guiBackground->SetDiffuseColor(m_diffuseColor);
+ bChanged |= m_guiOverlay->SetDiffuseColor(m_diffuseColor);
+
+ for (auto& range : m_ranges)
+ bChanged |= range.SetDiffuseColor(m_diffuseColor);
+
+ return bChanged;
+}
+
+bool CGUIRangesControl::UpdateLayout()
+{
+ bool bChanged = false;
+
+ if (m_width == 0)
+ m_width = m_guiBackground->GetTextureWidth();
+
+ if (m_height == 0)
+ m_height = m_guiBackground->GetTextureHeight();
+
+ bChanged |= m_guiBackground->SetHeight(m_height);
+ bChanged |= m_guiBackground->SetWidth(m_width);
+
+ float fScaleX =
+ m_guiBackground->GetTextureWidth() ? m_width / m_guiBackground->GetTextureWidth() : 1.0f;
+ float fScaleY =
+ m_guiBackground->GetTextureHeight() ? m_height / m_guiBackground->GetTextureHeight() : 1.0f;
+
+ float posX = m_guiBackground->GetXPosition();
+ float posY = m_guiBackground->GetYPosition();
+
+ for (auto& range : m_ranges)
+ bChanged |= range.UpdateLayout(m_guiBackground->GetTextureHeight(), posX, posY, m_width,
+ fScaleX, fScaleY);
+
+ float offset = std::fabs(
+ fScaleY * 0.5f * (m_guiOverlay->GetTextureHeight() - m_guiBackground->GetTextureHeight()));
+ if (offset > 0) // Center texture to the background if necessary
+ bChanged |= m_guiOverlay->SetPosition(m_guiBackground->GetXPosition(),
+ m_guiBackground->GetYPosition() + offset);
+ else
+ bChanged |=
+ m_guiOverlay->SetPosition(m_guiBackground->GetXPosition(), m_guiBackground->GetYPosition());
+
+ bChanged |= m_guiOverlay->SetHeight(fScaleY * m_guiOverlay->GetTextureHeight());
+ bChanged |= m_guiOverlay->SetWidth(fScaleX * m_guiOverlay->GetTextureWidth());
+
+ return bChanged;
+}
+
+void CGUIRangesControl::UpdateInfo(const CGUIListItem* item /* = nullptr */)
+{
+ if (!IsDisabled() && m_iInfoCode)
+ {
+ const std::string value = CServiceBroker::GetGUI()->GetInfoManager().GetLabel(m_iInfoCode, m_parentID);
+ if (value != m_prevRanges)
+ {
+ std::vector<std::pair<float, float>> ranges;
+
+ // Parse csv string into ranges...
+ const std::vector<std::string> values = StringUtils::Split(value, ',');
+
+ // we must have an even number of values
+ if (values.size() % 2 == 0)
+ {
+ for (auto it = values.begin(); it != values.end();)
+ {
+ float first = std::stof(*it, nullptr);
+ ++it;
+ float second = std::stof(*it, nullptr);
+ ++it;
+
+ if (first <= second)
+ ranges.emplace_back(std::make_pair(first, second));
+ else
+ CLog::Log(LOGERROR, "CGUIRangesControl::UpdateInfo - malformed ranges csv string (end element must be larger or equal than start element)");
+ }
+ }
+ else
+ CLog::Log(LOGERROR, "CGUIRangesControl::UpdateInfo - malformed ranges csv string (string must contain even number of elements)");
+
+ SetRanges(ranges);
+ m_prevRanges = value;
+ }
+ }
+}
diff --git a/xbmc/guilib/GUIRangesControl.dox b/xbmc/guilib/GUIRangesControl.dox
new file mode 100644
index 0000000..637da34
--- /dev/null
+++ b/xbmc/guilib/GUIRangesControl.dox
@@ -0,0 +1,56 @@
+/*!
+
+\page Ranges_Control Ranges Control
+\brief **Used to show multiple range blocks**
+
+\tableofcontents
+
+The ranges control is used for showing multiple range UI elements on the same control. It is used
+in Kodi, for example, to show the intervals of a cutlist (EDL) or chapters in the video seekbar.
+You can choose the position, size and look and feel of the control.
+
+--------------------------------------------------------------------------------
+\section Ranges_Control_sect1 Example
+
+~~~~~~~~~~~~~xml
+<control type="ranges">
+ <left>0</left>
+ <top>70</top>
+ <width>100%</width>
+ <height>8</height>
+ <texturebg border="3" colordiffuse="00FFFFFF">colors/white50.png</texturebg>
+ <lefttexture>colors/white.png</lefttexture>
+ <midtexture colordiffuse="FFFF0000">colors/white.png</midtexture>
+ <righttexture>colors/white.png</righttexture>
+ <info>Player.Editlist</info>
+</control>
+~~~~~~~~~~~~~
+
+--------------------------------------------------------------------------------
+\section Ranges_Control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is lower case only. This is
+important, as xml tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| texturebg | The background texture for the range control.
+| lefttexture | The texture used in the left hand side of the range
+| midtexture | The texture used for the mid section of the range
+| righttexture | The texture used in the right hand side of the range
+| info | Specifies the information the range control holds. It expects an infolabel that returns a string in CSV format: e.g. `"start1,end1,start2,end2,..."`. Tokens must have values in the range from 0.0 to 100.0. end token must be less or equal than start token. Examples of currently supported infolabels are \link Player_Editlist `Player.Editlist`\endlink, \link Player_Cutlist `Player.Cutlist`\endlink (@deprecated), \link Player_Cuts `Player.Cuts`\endlink, \link Player_SceneMarkers `Player.Cutlist`\endlink and \link Player_Chapters `Player.Chapters`\endlink.
+
+\section Ranges_Control_sect3 Revision History
+
+@skinning_v19 <b>[Ranges Control]</b> New control added.
+
+--------------------------------------------------------------------------------
+\section Ranges_Control_sect4 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIRangesControl.h b/xbmc/guilib/GUIRangesControl.h
new file mode 100644
index 0000000..a043c7d
--- /dev/null
+++ b/xbmc/guilib/GUIRangesControl.h
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+/*!
+\file GUIRangesControl.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUITexture.h"
+
+#include <utility>
+#include <vector>
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIRangesControl : public CGUIControl
+{
+ class CGUIRange
+ {
+ public:
+ CGUIRange(float fPosX, float fPosY, float fWidth, float fHeight,
+ const CTextureInfo& lowerTextureInfo, const CTextureInfo& fillTextureInfo,
+ const CTextureInfo& upperTextureInfo, const std::pair<float, float>& percentages);
+
+ CGUIRange(const CGUIRange& range);
+
+ void AllocResources();
+ void FreeResources(bool bImmediately);
+ void DynamicResourceAlloc(bool bOnOff);
+ void SetInvalid();
+ bool SetDiffuseColor(const KODI::GUILIB::GUIINFO::CGUIInfoColor& color);
+
+ bool Process(unsigned int iCurrentTime);
+ void Render();
+ bool UpdateLayout(float fBackgroundTextureHeight, float fPosX, float fPosY, float fWidth, float fScaleX, float fScaleY);
+
+ private:
+ std::unique_ptr<CGUITexture> m_guiLowerTexture;
+ std::unique_ptr<CGUITexture> m_guiFillTexture;
+ std::unique_ptr<CGUITexture> m_guiUpperTexture;
+ std::pair<float,float> m_percentValues;
+ };
+
+public:
+ CGUIRangesControl(int iParentID, int iControlID, float fPosX, float fPosY,
+ float fWidth, float fHeight, const CTextureInfo& backGroundTexture,
+ const CTextureInfo& leftTexture, const CTextureInfo& midTexture,
+ const CTextureInfo& rightTexture, const CTextureInfo& overlayTexture,
+ int iInfo);
+ ~CGUIRangesControl() override = default;
+ CGUIRangesControl* Clone() const override { return new CGUIRangesControl(*this); }
+
+ void Process(unsigned int iCurrentTime, CDirtyRegionList& dirtyregions) override;
+ void Render() override;
+ bool CanFocus() const override;
+ void AllocResources() override;
+ void FreeResources(bool bImmediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ void SetPosition(float fPosX, float fPosY) override;
+ void UpdateInfo(const CGUIListItem* item = nullptr) override;
+
+protected:
+ void SetRanges(const std::vector<std::pair<float, float>>& ranges);
+ void ClearRanges();
+
+ bool UpdateColors(const CGUIListItem* item) override;
+ bool UpdateLayout();
+
+ std::unique_ptr<CGUITexture> m_guiBackground;
+ std::unique_ptr<CGUITexture> m_guiOverlay;
+ const CTextureInfo m_guiLowerTextureInfo;
+ const CTextureInfo m_guiFillTextureInfo;
+ const CTextureInfo m_guiUpperTextureInfo;
+ std::vector<CGUIRange> m_ranges;
+ int m_iInfoCode = 0;
+ std::string m_prevRanges;
+
+private:
+ CGUIRangesControl(const CGUIRangesControl& control);
+};
diff --git a/xbmc/guilib/GUIRenderingControl.cpp b/xbmc/guilib/GUIRenderingControl.cpp
new file mode 100644
index 0000000..f6e460b
--- /dev/null
+++ b/xbmc/guilib/GUIRenderingControl.cpp
@@ -0,0 +1,117 @@
+/*
+ * 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 "GUIRenderingControl.h"
+
+#include "guilib/IRenderingCallback.h"
+
+#include <mutex>
+#ifdef TARGET_WINDOWS
+#include "rendering/dx/DeviceResources.h"
+#endif
+
+#define LABEL_ROW1 10
+#define LABEL_ROW2 11
+#define LABEL_ROW3 12
+
+CGUIRenderingControl::CGUIRenderingControl(int parentID, int controlID, float posX, float posY, float width, float height)
+ : CGUIControl(parentID, controlID, posX, posY, width, height)
+{
+ ControlType = GUICONTROL_RENDERADDON;
+ m_callback = NULL;
+}
+
+CGUIRenderingControl::CGUIRenderingControl(const CGUIRenderingControl &from)
+: CGUIControl(from)
+{
+ ControlType = GUICONTROL_RENDERADDON;
+ m_callback = NULL;
+}
+
+bool CGUIRenderingControl::InitCallback(IRenderingCallback *callback)
+{
+ if (!callback)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_rendering);
+ CServiceBroker::GetWinSystem()->GetGfxContext().CaptureStateBlock();
+ float x = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(GetXPosition(), GetYPosition());
+ float y = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(GetXPosition(), GetYPosition());
+ float w = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(GetXPosition() + GetWidth(), GetYPosition() + GetHeight()) - x;
+ float h = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(GetXPosition() + GetWidth(), GetYPosition() + GetHeight()) - y;
+ if (x < 0) x = 0;
+ if (y < 0) y = 0;
+ if (x + w > CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth()) w = CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth() - x;
+ if (y + h > CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight()) h = CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight() - y;
+
+ void *device = NULL;
+#if TARGET_WINDOWS
+ device = DX::DeviceResources::Get()->GetD3DDevice();
+#endif
+ if (callback->Create((int)(x+0.5f), (int)(y+0.5f), (int)(w+0.5f), (int)(h+0.5f), device))
+ m_callback = callback;
+ else
+ return false;
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().ApplyStateBlock();
+ return true;
+}
+
+void CGUIRenderingControl::UpdateVisibility(const CGUIListItem *item)
+{
+ // if made invisible, start timer, only free addonptr after
+ // some period, configurable by window class
+ CGUIControl::UpdateVisibility(item);
+ if (!IsVisible() && m_callback)
+ FreeResources();
+}
+
+void CGUIRenderingControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ //! @todo Add processing to the addon so it could mark when actually changing
+ std::unique_lock<CCriticalSection> lock(m_rendering);
+ if (m_callback && m_callback->IsDirty())
+ MarkDirtyRegion();
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIRenderingControl::Render()
+{
+ std::unique_lock<CCriticalSection> lock(m_rendering);
+ if (m_callback)
+ {
+ // set the viewport - note: We currently don't have any control over how
+ // the addon renders, so the best we can do is attempt to define
+ // a viewport??
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetViewPort(m_posX, m_posY, m_width, m_height);
+ CServiceBroker::GetWinSystem()->GetGfxContext().CaptureStateBlock();
+ m_callback->Render();
+ CServiceBroker::GetWinSystem()->GetGfxContext().ApplyStateBlock();
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreViewPort();
+ }
+
+ CGUIControl::Render();
+}
+
+void CGUIRenderingControl::FreeResources(bool immediately)
+{
+ std::unique_lock<CCriticalSection> lock(m_rendering);
+
+ if (!m_callback) return;
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().CaptureStateBlock(); //! @todo locking
+ m_callback->Stop();
+ CServiceBroker::GetWinSystem()->GetGfxContext().ApplyStateBlock();
+ m_callback = NULL;
+}
+
+bool CGUIRenderingControl::CanFocusFromPoint(const CPoint &point) const
+{ // mouse is allowed to focus this control, but it doesn't actually receive focus
+ return IsVisible() && HitTest(point);
+}
diff --git a/xbmc/guilib/GUIRenderingControl.dox b/xbmc/guilib/GUIRenderingControl.dox
new file mode 100644
index 0000000..1f1b44b
--- /dev/null
+++ b/xbmc/guilib/GUIRenderingControl.dox
@@ -0,0 +1,34 @@
+/*!
+
+\page Addon_Rendering_control Add-on Rendering control
+\brief **Control where rendering becomes performed from add-on to show on Kodi**
+
+\tableofcontents
+
+--------------------------------------------------------------------------------
+\section Addon_Rendering_control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="renderaddon" id ="9">
+ <left>5</left>
+ <top>50</top>
+ <width>780</width>
+ <height>515</height>
+</control>
+~~~~~~~~~~~~~
+
+--------------------------------------------------------------------------------
+\section Addon_Rendering_control_sect2 Available tags
+
+Only the [default control](http://kodi.wiki/view/Default_Control_Tags) tags are
+applicable to this control.
+
+
+--------------------------------------------------------------------------------
+\section Addon_Rendering_control_sect3 See also
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIRenderingControl.h b/xbmc/guilib/GUIRenderingControl.h
new file mode 100644
index 0000000..b901b8c
--- /dev/null
+++ b/xbmc/guilib/GUIRenderingControl.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "GUIControl.h"
+
+class IRenderingCallback;
+
+class CGUIRenderingControl : public CGUIControl
+{
+public:
+ CGUIRenderingControl(int parentID, int controlID, float posX, float posY, float width, float height);
+ CGUIRenderingControl(const CGUIRenderingControl &from);
+ CGUIRenderingControl *Clone() const override { return new CGUIRenderingControl(*this); }; //! @todo check for naughties
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ void UpdateVisibility(const CGUIListItem *item = NULL) override;
+ void FreeResources(bool immediately = false) override;
+ bool CanFocus() const override { return false; }
+ bool CanFocusFromPoint(const CPoint &point) const override;
+ bool InitCallback(IRenderingCallback *callback);
+
+protected:
+ CCriticalSection m_rendering;
+ IRenderingCallback *m_callback;
+};
diff --git a/xbmc/guilib/GUIResizeControl.cpp b/xbmc/guilib/GUIResizeControl.cpp
new file mode 100644
index 0000000..83f79d4
--- /dev/null
+++ b/xbmc/guilib/GUIResizeControl.cpp
@@ -0,0 +1,227 @@
+/*
+ * 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 "GUIResizeControl.h"
+
+#include "GUIMessage.h"
+#include "input/Key.h"
+#include "input/mouse/MouseStat.h"
+
+using namespace UTILS;
+
+CGUIResizeControl::CGUIResizeControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& textureFocus,
+ const CTextureInfo& textureNoFocus,
+ UTILS::MOVING_SPEED::MapEventConfig& movingSpeedCfg)
+ : CGUIControl(parentID, controlID, posX, posY, width, height),
+ m_imgFocus(CGUITexture::CreateTexture(posX, posY, width, height, textureFocus)),
+ m_imgNoFocus(CGUITexture::CreateTexture(posX, posY, width, height, textureNoFocus))
+{
+ m_frameCounter = 0;
+ m_movingSpeed.AddEventMapConfig(movingSpeedCfg);
+ m_fAnalogSpeed = 2.0f; //! @todo implement correct analog speed
+ ControlType = GUICONTROL_RESIZE;
+ SetLimits(0, 0, 720, 576); // defaults
+}
+
+CGUIResizeControl::CGUIResizeControl(const CGUIResizeControl& control)
+ : CGUIControl(control),
+ m_imgFocus(control.m_imgFocus->Clone()),
+ m_imgNoFocus(control.m_imgNoFocus->Clone()),
+ m_frameCounter(control.m_frameCounter),
+ m_movingSpeed(control.m_movingSpeed),
+ m_fAnalogSpeed(control.m_fAnalogSpeed),
+ m_x1(control.m_x1),
+ m_x2(control.m_x2),
+ m_y1(control.m_y1),
+ m_y2(control.m_y2)
+{
+}
+
+void CGUIResizeControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_bInvalidated)
+ {
+ m_imgFocus->SetWidth(m_width);
+ m_imgFocus->SetHeight(m_height);
+
+ m_imgNoFocus->SetWidth(m_width);
+ m_imgNoFocus->SetHeight(m_height);
+ }
+ if (HasFocus())
+ {
+ unsigned int alphaCounter = m_frameCounter + 2;
+ unsigned int alphaChannel;
+ if ((alphaCounter % 128) >= 64)
+ alphaChannel = alphaCounter % 64;
+ else
+ alphaChannel = 63 - (alphaCounter % 64);
+
+ alphaChannel += 192;
+ if (SetAlpha( (unsigned char)alphaChannel ))
+ MarkDirtyRegion();
+ m_imgFocus->SetVisible(true);
+ m_imgNoFocus->SetVisible(false);
+ m_frameCounter++;
+ }
+ else
+ {
+ if (SetAlpha(0xff))
+ MarkDirtyRegion();
+ m_imgFocus->SetVisible(false);
+ m_imgNoFocus->SetVisible(true);
+ }
+ m_imgFocus->Process(currentTime);
+ m_imgNoFocus->Process(currentTime);
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIResizeControl::Render()
+{
+ m_imgFocus->Render();
+ m_imgNoFocus->Render();
+ CGUIControl::Render();
+}
+
+bool CGUIResizeControl::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_SELECT_ITEM)
+ {
+ // button selected - send message to parent
+ CGUIMessage message(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(message);
+ return true;
+ }
+ if (action.GetID() == ACTION_ANALOG_MOVE)
+ {
+ Resize(m_fAnalogSpeed*action.GetAmount(), -m_fAnalogSpeed*action.GetAmount(1));
+ return true;
+ }
+ return CGUIControl::OnAction(action);
+}
+
+void CGUIResizeControl::OnUp()
+{
+ Resize(0, -m_movingSpeed.GetUpdatedDistance(MOVING_SPEED::EventType::UP));
+}
+
+void CGUIResizeControl::OnDown()
+{
+ Resize(0, m_movingSpeed.GetUpdatedDistance(MOVING_SPEED::EventType::DOWN));
+}
+
+void CGUIResizeControl::OnLeft()
+{
+ Resize(-m_movingSpeed.GetUpdatedDistance(MOVING_SPEED::EventType::LEFT), 0);
+}
+
+void CGUIResizeControl::OnRight()
+{
+ Resize(m_movingSpeed.GetUpdatedDistance(MOVING_SPEED::EventType::RIGHT), 0);
+}
+
+EVENT_RESULT CGUIResizeControl::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_DRAG || event.m_id == ACTION_MOUSE_DRAG_END)
+ {
+ if (static_cast<HoldAction>(event.m_state) == HoldAction::DRAG)
+ { // grab exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ }
+ else if (static_cast<HoldAction>(event.m_state) == HoldAction::DRAG_END)
+ { // release exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
+ SendWindowMessage(msg);
+ }
+ Resize(event.m_offsetX, event.m_offsetY);
+ return EVENT_RESULT_HANDLED;
+ }
+ return EVENT_RESULT_UNHANDLED;
+}
+
+void CGUIResizeControl::AllocResources()
+{
+ CGUIControl::AllocResources();
+ m_frameCounter = 0;
+ m_imgFocus->AllocResources();
+ m_imgNoFocus->AllocResources();
+ m_width = m_imgFocus->GetWidth();
+ m_height = m_imgFocus->GetHeight();
+}
+
+void CGUIResizeControl::FreeResources(bool immediately)
+{
+ CGUIControl::FreeResources(immediately);
+ m_imgFocus->FreeResources(immediately);
+ m_imgNoFocus->FreeResources(immediately);
+}
+
+void CGUIResizeControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+ m_imgFocus->DynamicResourceAlloc(bOnOff);
+ m_imgNoFocus->DynamicResourceAlloc(bOnOff);
+}
+
+void CGUIResizeControl::SetInvalid()
+{
+ CGUIControl::SetInvalid();
+ m_imgFocus->SetInvalid();
+ m_imgNoFocus->SetInvalid();
+}
+
+void CGUIResizeControl::Resize(float x, float y)
+{
+ float width = m_width + x;
+ float height = m_height + y;
+ // check if we are within the bounds
+ if (width < m_x1) width = m_x1;
+ if (height < m_y1) height = m_y1;
+ if (width > m_x2) width = m_x2;
+ if (height > m_y2) height = m_y2;
+ // ok, now set the default size of the resize control
+ SetWidth(width);
+ SetHeight(height);
+}
+
+void CGUIResizeControl::SetPosition(float posX, float posY)
+{
+ CGUIControl::SetPosition(posX, posY);
+ m_imgFocus->SetPosition(posX, posY);
+ m_imgNoFocus->SetPosition(posX, posY);
+}
+
+bool CGUIResizeControl::SetAlpha(unsigned char alpha)
+{
+ bool changed = m_imgFocus->SetAlpha(alpha);
+ changed |= m_imgNoFocus->SetAlpha(alpha);
+ return changed;
+}
+
+bool CGUIResizeControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_imgFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgNoFocus->SetDiffuseColor(m_diffuseColor);
+
+ return changed;
+}
+
+void CGUIResizeControl::SetLimits(float x1, float y1, float x2, float y2)
+{
+ m_x1 = x1;
+ m_y1 = y1;
+ m_x2 = x2;
+ m_y2 = y2;
+}
diff --git a/xbmc/guilib/GUIResizeControl.dox b/xbmc/guilib/GUIResizeControl.dox
new file mode 100644
index 0000000..d3bcd5a
--- /dev/null
+++ b/xbmc/guilib/GUIResizeControl.dox
@@ -0,0 +1,93 @@
+/*!
+
+\page Resize_Control Resize control
+\brief **Used to set the pixel ratio in Video Calibration.**
+
+\tableofcontents
+
+The resize control is used to specify an area of changeable ratio for use in
+the screen calibration portion of Kodi. You can choose the size, and look of
+the resizer.
+
+
+--------------------------------------------------------------------------------
+\section Resize_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="resize" id="3">
+ <description>My first resize control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <texturefocus>mytexture.png</texturefocus>
+ <texturenofocus>mytexture.png</texturenofocus>
+ <pulseonselect>true</pulseonselect>
+ <movingspeed acceleration="180" maxvelocity="300" resettimeout="200" delta="1">
+ <eventconfig type="up">
+ <eventconfig type="down">
+ <eventconfig type="left" acceleration="100" maxvelocity="100" resettimeout="160" delta="1">
+ <eventconfig type="right" acceleration="100" maxvelocity="100" resettimeout="160" delta="1">
+ </movingspeed>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Resize_Control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|----------------:|:--------------------------------------------------------------|
+| texturefocus | Specifies the image file which should be displayed when the resizer has focus. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| texturenofocus | Specifies the image file which should be displayed when the resizer does not have focus.
+| movingspeed | Specifies the allowed directional movements and respective configurations. The <c>`<movingspeed>`</c> tag section can contain as many <c>`<eventconfig>`</c> tags as required.
+
+
+--------------------------------------------------------------------------------
+\section Resize_Control_sect3 Note on use of movingspeed tag
+
+The <c>`movingspeed`</c> tag must be specified to allow the control to be moved via
+an input device such as keyboard. Each direction can be specified and configured
+to simulate the motion effect with the <c>`eventconfig`</c> tag.
+
+The following attributes allow the motion effect of the control to be configured:
+
+| Attribute | Description |
+|----------------:|:--------------------------------------------------------------|
+| type | Specifies the direction of movement. Accepted values are: <c>`up`</c>, <c>`down`</c>, <c>`left`</c>, <c>`right`</c>. All directions are optional and do not have to be included.
+| acceleration | Specifies the acceleration in pixels per second (integer value).
+| maxvelocity | Specifies the maximum movement speed. This depends on the acceleration value, e.g. a suitable value could be (acceleration value)*3. Set to 0 to disable. (integer value).
+| resettimeout | Specifies the idle timeout before resetting the acceleration speed (in milliseconds, integer value).
+| delta | Specifies the minimal pixel increment step (integer value).
+
+The attributes <c>`acceleration`</c>, <c>`maxvelocity`</c>, <c>`resettimeout`</c>,
+<c>`delta`</c> can be specified individually for each <c>`eventconfig`</c> tag,
+or globally in the <c>`movingspeed`</c> tag, or a mixture, as the following example:
+
+~~~~~~~~~~~~~
+<movingspeed acceleration="180" maxvelocity="300" resettimeout="200" delta="1">
+ <eventconfig type="up">
+ <eventconfig type="down">
+ <eventconfig type="left" acceleration="100" maxvelocity="100" resettimeout="160">
+ <eventconfig type="right" acceleration="100" maxvelocity="100" resettimeout="160">
+</movingspeed>
+~~~~~~~~~~~~~
+
+--------------------------------------------------------------------------------
+\section Resize_Control_sect4 Revision History
+
+@skinning_v20 <b>[movingspeed]</b> New tag added.
+
+--------------------------------------------------------------------------------
+\section Resize_Control_sect5 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIResizeControl.h b/xbmc/guilib/GUIResizeControl.h
new file mode 100644
index 0000000..998cc1b
--- /dev/null
+++ b/xbmc/guilib/GUIResizeControl.h
@@ -0,0 +1,70 @@
+/*
+ * 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
+
+/*!
+\file GUIRESIZEControl.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUITexture.h"
+#include "utils/MovingSpeed.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIResizeControl : public CGUIControl
+{
+public:
+ CGUIResizeControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& textureFocus,
+ const CTextureInfo& textureNoFocus,
+ UTILS::MOVING_SPEED::MapEventConfig& movingSpeedCfg);
+
+ ~CGUIResizeControl() override = default;
+ CGUIResizeControl* Clone() const override { return new CGUIResizeControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnAction(const CAction &action) override;
+ void OnUp() override;
+ void OnDown() override;
+ void OnLeft() override;
+ void OnRight() override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ void SetPosition(float posX, float posY) override;
+ void SetLimits(float x1, float y1, float x2, float y2);
+ bool CanFocus() const override { return true; }
+
+protected:
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ bool UpdateColors(const CGUIListItem* item) override;
+ bool SetAlpha(unsigned char alpha);
+ void Resize(float x, float y);
+ std::unique_ptr<CGUITexture> m_imgFocus;
+ std::unique_ptr<CGUITexture> m_imgNoFocus;
+ unsigned int m_frameCounter;
+ UTILS::MOVING_SPEED::CMovingSpeed m_movingSpeed;
+ float m_fAnalogSpeed;
+ float m_x1, m_x2, m_y1, m_y2;
+
+private:
+ CGUIResizeControl(const CGUIResizeControl& control);
+};
+
diff --git a/xbmc/guilib/GUIScrollBarControl.cpp b/xbmc/guilib/GUIScrollBarControl.cpp
new file mode 100644
index 0000000..bcb2101
--- /dev/null
+++ b/xbmc/guilib/GUIScrollBarControl.cpp
@@ -0,0 +1,397 @@
+/*
+ * 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 "GUIScrollBarControl.h"
+
+#include "GUIMessage.h"
+#include "input/Key.h"
+#include "input/mouse/MouseStat.h"
+#include "utils/StringUtils.h"
+
+#define MIN_NIB_SIZE 4.0f
+
+GUIScrollBarControl::GUIScrollBarControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& backGroundTexture,
+ const CTextureInfo& barTexture,
+ const CTextureInfo& barTextureFocus,
+ const CTextureInfo& nibTexture,
+ const CTextureInfo& nibTextureFocus,
+ ORIENTATION orientation,
+ bool showOnePage)
+ : CGUIControl(parentID, controlID, posX, posY, width, height),
+ m_guiBackground(CGUITexture::CreateTexture(posX, posY, width, height, backGroundTexture)),
+ m_guiBarNoFocus(CGUITexture::CreateTexture(posX, posY, width, height, barTexture)),
+ m_guiBarFocus(CGUITexture::CreateTexture(posX, posY, width, height, barTextureFocus)),
+ m_guiNibNoFocus(CGUITexture::CreateTexture(posX, posY, width, height, nibTexture)),
+ m_guiNibFocus(CGUITexture::CreateTexture(posX, posY, width, height, nibTextureFocus))
+{
+ m_guiNibNoFocus->SetAspectRatio(CAspectRatio::AR_CENTER);
+ m_guiNibFocus->SetAspectRatio(CAspectRatio::AR_CENTER);
+ m_numItems = 100;
+ m_offset = 0;
+ m_pageSize = 10;
+ ControlType = GUICONTROL_SCROLLBAR;
+ m_orientation = orientation;
+ m_showOnePage = showOnePage;
+}
+
+GUIScrollBarControl::GUIScrollBarControl(const GUIScrollBarControl& control)
+ : CGUIControl(control),
+ m_guiBackground(control.m_guiBackground->Clone()),
+ m_guiBarNoFocus(control.m_guiBarNoFocus->Clone()),
+ m_guiBarFocus(control.m_guiBarFocus->Clone()),
+ m_guiNibNoFocus(control.m_guiNibNoFocus->Clone()),
+ m_guiNibFocus(control.m_guiNibFocus->Clone()),
+ m_numItems(control.m_numItems),
+ m_pageSize(control.m_pageSize),
+ m_offset(control.m_offset),
+ m_showOnePage(control.m_showOnePage),
+ m_orientation(control.m_orientation)
+{
+}
+
+void GUIScrollBarControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ bool changed = false;
+
+ if (m_bInvalidated)
+ changed |= UpdateBarSize();
+
+ changed |= m_guiBackground->Process(currentTime);
+ changed |= m_guiBarNoFocus->Process(currentTime);
+ changed |= m_guiBarFocus->Process(currentTime);
+ changed |= m_guiNibNoFocus->Process(currentTime);
+ changed |= m_guiNibFocus->Process(currentTime);
+
+ if (changed)
+ MarkDirtyRegion();
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void GUIScrollBarControl::Render()
+{
+ m_guiBackground->Render();
+ if (m_bHasFocus)
+ {
+ m_guiBarFocus->Render();
+ m_guiNibFocus->Render();
+ }
+ else
+ {
+ m_guiBarNoFocus->Render();
+ m_guiNibNoFocus->Render();
+ }
+
+ CGUIControl::Render();
+}
+
+bool GUIScrollBarControl::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_ITEM_SELECT:
+ SetValue(message.GetParam1());
+ return true;
+ case GUI_MSG_LABEL_RESET:
+ SetRange(message.GetParam1(), message.GetParam2());
+ return true;
+ case GUI_MSG_PAGE_UP:
+ Move(-1);
+ return true;
+ case GUI_MSG_PAGE_DOWN:
+ Move(1);
+ return true;
+ }
+ return CGUIControl::OnMessage(message);
+}
+
+bool GUIScrollBarControl::OnAction(const CAction &action)
+{
+ switch ( action.GetID() )
+ {
+ case ACTION_MOVE_LEFT:
+ if (m_orientation == HORIZONTAL)
+ {
+ if(Move( -1))
+ return true;
+ }
+ break;
+
+ case ACTION_MOVE_RIGHT:
+ if (m_orientation == HORIZONTAL)
+ {
+ if(Move(1))
+ return true;
+ }
+ break;
+ case ACTION_MOVE_UP:
+ if (m_orientation == VERTICAL)
+ {
+ if(Move(-1))
+ return true;
+ }
+ break;
+
+ case ACTION_MOVE_DOWN:
+ if (m_orientation == VERTICAL)
+ {
+ if(Move(1))
+ return true;
+ }
+ break;
+ }
+ return CGUIControl::OnAction(action);
+}
+
+bool GUIScrollBarControl::Move(int numSteps)
+{
+ if (numSteps < 0 && m_offset == 0) // we are at the beginning - can't scroll up/left anymore
+ return false;
+ if (numSteps > 0 && m_offset == std::max(m_numItems - m_pageSize, 0)) // we are at the end - we can't scroll down/right anymore
+ return false;
+
+ m_offset += numSteps * m_pageSize;
+ if (m_offset > m_numItems - m_pageSize) m_offset = m_numItems - m_pageSize;
+ if (m_offset < 0) m_offset = 0;
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, GetParentID(), GetID(), GUI_MSG_PAGE_CHANGE, m_offset);
+ SendWindowMessage(message);
+ SetInvalid();
+ return true;
+}
+
+void GUIScrollBarControl::SetRange(int pageSize, int numItems)
+{
+ if (m_pageSize != pageSize || m_numItems != numItems)
+ {
+ m_pageSize = pageSize;
+ m_numItems = numItems;
+ m_offset = 0;
+ SetInvalid();
+ }
+}
+
+void GUIScrollBarControl::SetValue(int value)
+{
+ if (m_offset != value)
+ {
+ m_offset = value;
+ SetInvalid();
+ }
+}
+
+void GUIScrollBarControl::FreeResources(bool immediately)
+{
+ CGUIControl::FreeResources(immediately);
+ m_guiBackground->FreeResources(immediately);
+ m_guiBarNoFocus->FreeResources(immediately);
+ m_guiBarFocus->FreeResources(immediately);
+ m_guiNibNoFocus->FreeResources(immediately);
+ m_guiNibFocus->FreeResources(immediately);
+}
+
+void GUIScrollBarControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+ m_guiBackground->DynamicResourceAlloc(bOnOff);
+ m_guiBarNoFocus->DynamicResourceAlloc(bOnOff);
+ m_guiBarFocus->DynamicResourceAlloc(bOnOff);
+ m_guiNibNoFocus->DynamicResourceAlloc(bOnOff);
+ m_guiNibFocus->DynamicResourceAlloc(bOnOff);
+}
+
+void GUIScrollBarControl::AllocResources()
+{
+ CGUIControl::AllocResources();
+ m_guiBackground->AllocResources();
+ m_guiBarNoFocus->AllocResources();
+ m_guiBarFocus->AllocResources();
+ m_guiNibNoFocus->AllocResources();
+ m_guiNibFocus->AllocResources();
+}
+
+void GUIScrollBarControl::SetInvalid()
+{
+ CGUIControl::SetInvalid();
+ m_guiBackground->SetInvalid();
+ m_guiBarFocus->SetInvalid();
+ m_guiBarFocus->SetInvalid();
+ m_guiNibNoFocus->SetInvalid();
+ m_guiNibFocus->SetInvalid();
+}
+
+bool GUIScrollBarControl::UpdateBarSize()
+{
+ bool changed = false;
+
+ // scale our textures to suit
+ if (m_orientation == VERTICAL)
+ {
+ // calculate the height to display the nib at
+ float percent = (m_numItems == 0) ? 0 : (float)m_pageSize / m_numItems;
+ float nibSize = GetHeight() * percent;
+ if (nibSize < m_guiNibFocus->GetTextureHeight() + 2 * MIN_NIB_SIZE)
+ nibSize = m_guiNibFocus->GetTextureHeight() + 2 * MIN_NIB_SIZE;
+ if (nibSize > GetHeight()) nibSize = GetHeight();
+
+ changed |= m_guiBarNoFocus->SetHeight(nibSize);
+ changed |= m_guiBarFocus->SetHeight(nibSize);
+ changed |= m_guiNibNoFocus->SetHeight(nibSize);
+ changed |= m_guiNibFocus->SetHeight(nibSize);
+ // nibSize may be altered by the border size of the nib (and bar).
+ nibSize = std::max(m_guiBarFocus->GetHeight(), m_guiNibFocus->GetHeight());
+
+ // and the position
+ percent = (m_numItems == m_pageSize) ? 0 : (float)m_offset / (m_numItems - m_pageSize);
+ float nibPos = (GetHeight() - nibSize) * percent;
+ if (nibPos < 0) nibPos = 0;
+ if (nibPos > GetHeight() - nibSize) nibPos = GetHeight() - nibSize;
+
+ changed |= m_guiBarNoFocus->SetPosition(GetXPosition(), GetYPosition() + nibPos);
+ changed |= m_guiBarFocus->SetPosition(GetXPosition(), GetYPosition() + nibPos);
+ changed |= m_guiNibNoFocus->SetPosition(GetXPosition(), GetYPosition() + nibPos);
+ changed |= m_guiNibFocus->SetPosition(GetXPosition(), GetYPosition() + nibPos);
+ }
+ else
+ {
+ // calculate the height to display the nib at
+ float percent = (m_numItems == 0) ? 0 : (float)m_pageSize / m_numItems;
+ float nibSize = GetWidth() * percent + 0.5f;
+ if (nibSize < m_guiNibFocus->GetTextureWidth() + 2 * MIN_NIB_SIZE)
+ nibSize = m_guiNibFocus->GetTextureWidth() + 2 * MIN_NIB_SIZE;
+ if (nibSize > GetWidth()) nibSize = GetWidth();
+
+ changed |= m_guiBarNoFocus->SetWidth(nibSize);
+ changed |= m_guiBarFocus->SetWidth(nibSize);
+ changed |= m_guiNibNoFocus->SetWidth(nibSize);
+ changed |= m_guiNibFocus->SetWidth(nibSize);
+
+ // and the position
+ percent = (m_numItems == m_pageSize) ? 0 : (float)m_offset / (m_numItems - m_pageSize);
+ float nibPos = (GetWidth() - nibSize) * percent;
+ if (nibPos < 0) nibPos = 0;
+ if (nibPos > GetWidth() - nibSize) nibPos = GetWidth() - nibSize;
+
+ changed |= m_guiBarNoFocus->SetPosition(GetXPosition() + nibPos, GetYPosition());
+ changed |= m_guiBarFocus->SetPosition(GetXPosition() + nibPos, GetYPosition());
+ changed |= m_guiNibNoFocus->SetPosition(GetXPosition() + nibPos, GetYPosition());
+ changed |= m_guiNibFocus->SetPosition(GetXPosition() + nibPos, GetYPosition());
+ }
+
+ return changed;
+}
+
+void GUIScrollBarControl::SetFromPosition(const CPoint &point)
+{
+ float fPercent;
+ if (m_orientation == VERTICAL)
+ fPercent = (point.y - m_guiBackground->GetYPosition() - 0.5f * m_guiBarFocus->GetHeight()) /
+ (m_guiBackground->GetHeight() - m_guiBarFocus->GetHeight());
+ else
+ fPercent = (point.x - m_guiBackground->GetXPosition() - 0.5f * m_guiBarFocus->GetWidth()) /
+ (m_guiBackground->GetWidth() - m_guiBarFocus->GetWidth());
+ if (fPercent < 0) fPercent = 0;
+ if (fPercent > 1) fPercent = 1;
+
+ int offset = (int)(floor(fPercent * (m_numItems - m_pageSize) + 0.5f));
+
+ if (m_offset != offset)
+ {
+ m_offset = offset;
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, GetParentID(), GetID(), GUI_MSG_PAGE_CHANGE, m_offset);
+ SendWindowMessage(message);
+ SetInvalid();
+ }
+}
+
+EVENT_RESULT GUIScrollBarControl::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_DRAG || event.m_id == ACTION_MOUSE_DRAG_END)
+ {
+ if (static_cast<HoldAction>(event.m_state) == HoldAction::DRAG)
+ { // we want exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ }
+ else if (static_cast<HoldAction>(event.m_state) == HoldAction::DRAG_END)
+ { // we're done with exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
+ SendWindowMessage(msg);
+ }
+ SetFromPosition(point);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_MOUSE_LEFT_CLICK && m_guiBackground->HitTest(point))
+ {
+ SetFromPosition(point);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_MOUSE_WHEEL_UP)
+ {
+ Move(-1);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_MOUSE_WHEEL_DOWN)
+ {
+ Move(1);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_NOTIFY)
+ {
+ return (m_orientation == HORIZONTAL) ? EVENT_RESULT_PAN_HORIZONTAL_WITHOUT_INERTIA : EVENT_RESULT_PAN_VERTICAL_WITHOUT_INERTIA;
+ }
+ else if (event.m_id == ACTION_GESTURE_BEGIN)
+ { // grab exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_PAN)
+ { // do the drag
+ SetFromPosition(point);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_END || event.m_id == ACTION_GESTURE_ABORT)
+ { // release exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
+ SendWindowMessage(msg);
+ return EVENT_RESULT_HANDLED;
+ }
+
+ return EVENT_RESULT_UNHANDLED;
+}
+
+std::string GUIScrollBarControl::GetDescription() const
+{
+ return StringUtils::Format("{}/{}", m_offset, m_numItems);
+}
+
+bool GUIScrollBarControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_guiBackground->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiBarNoFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiBarFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiNibNoFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiNibFocus->SetDiffuseColor(m_diffuseColor);
+
+ return changed;
+}
+
+bool GUIScrollBarControl::IsVisible() const
+{
+ // page controls can be optionally disabled if the number of pages is 1
+ if (m_numItems <= m_pageSize && !m_showOnePage)
+ return false;
+ return CGUIControl::IsVisible();
+}
diff --git a/xbmc/guilib/GUIScrollBarControl.dox b/xbmc/guilib/GUIScrollBarControl.dox
new file mode 100644
index 0000000..4ee67a7
--- /dev/null
+++ b/xbmc/guilib/GUIScrollBarControl.dox
@@ -0,0 +1,79 @@
+/*!
+
+\page Scroll_Bar_Control Scroll Bar Control
+\brief **Used for a implementing a scroll bar.**
+
+\tableofcontents
+
+The scroll bar control is used as a page control for lists, panels, wraplists,
+fixedlists, textboxes, and grouplists. You can choose the position, size, and
+look of the scroll bar.
+
+
+--------------------------------------------------------------------------------
+\section Scroll_Bar_Control_sect Example
+
+~~~~~~~~~~~~~
+<control type="scrollbar" id="17">
+ <description>My first scroll bar control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>30</height>
+ <visible>true</visible>
+ <texturesliderbackground>scroll-background.png</texturesliderbackground>
+ <texturesliderbar>bar.png</texturesliderbar>
+ <texturesliderbarfocus>bar-focus.png</texturesliderbarfocus>
+ <textureslidernib>nib.png</textureslidernib>
+ <textureslidernibfocus>nib-focus.png</textureslidernibfocus>
+ <pulseonselect></pulseonselect>
+ <orientation>vertical</orientation>
+ <showonepage>false</showonepage>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Scroll_Bar_Control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is lower case only. This is
+important, as xml tags are case-sensitive.
+
+| Tag | Description |
+|------------------------:|:--------------------------------------------------------------|
+| texturesliderbackground | Specifies the image file which should be displayed in the background of the scroll bar control. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| texturesliderbar | Specifies the image file which should be displayed for the scroll bar. As the size of the scroll bar is dynamic, it is often useful to [use the border attribute of this texture](http://kodi.wiki/view/Texture_Attributes).
+| texturesliderbarfocus | Specifies the image file which should be displayed for the scroll bar when it has focus.
+| textureslidernib | Specifies the image file which should be displayed for the scroll bar nib. The nib is always centered within the scroll bar.
+| textureslidernibfocus | Specifies the image file which should be displayed for the scroll bar nib when it has focus. The nib is always centered within the scroll bar.
+| orientation | Specifies whether this scrollbar is horizontal or vertical. Defaults to vertical.
+| showonepage | Specifies whether the scrollbar will show if the container it's controlling has just one page. Defaults to true.
+
+\section Scroll_Bar_Control_sect3 Adding Up and Down buttons above and below a scrollbar
+To add arrow buttons above and below the scrollbar, you need to add 2 additional button controls to the window, and set their <b>`<onclick>`</b> tag to
+
+~~~~~~~~~~~~~
+<onclick>pageup(id)</onclick>
+~~~~~~~~~~~~~
+or
+~~~~~~~~~~~~~
+<onclick>pagedown(id)</onclick>
+~~~~~~~~~~~~~
+
+where id is the id of this scroll bar.
+
+
+--------------------------------------------------------------------------------
+\section Scroll_Bar_Control_sect4 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIScrollBarControl.h b/xbmc/guilib/GUIScrollBarControl.h
new file mode 100644
index 0000000..0000386
--- /dev/null
+++ b/xbmc/guilib/GUIScrollBarControl.h
@@ -0,0 +1,72 @@
+/*
+ * 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
+
+/*!
+\file
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUITexture.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class GUIScrollBarControl :
+ public CGUIControl
+{
+public:
+ GUIScrollBarControl(int parentID, int controlID, float posX, float posY,
+ float width, float height,
+ const CTextureInfo& backGroundTexture,
+ const CTextureInfo& barTexture, const CTextureInfo& barTextureFocus,
+ const CTextureInfo& nibTexture, const CTextureInfo& nibTextureFocus,
+ ORIENTATION orientation, bool showOnePage);
+ ~GUIScrollBarControl() override = default;
+ GUIScrollBarControl* Clone() const override { return new GUIScrollBarControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnAction(const CAction &action) override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ virtual void SetRange(int pageSize, int numItems);
+ bool OnMessage(CGUIMessage& message) override;
+ void SetValue(int value);
+ int GetValue() const;
+ std::string GetDescription() const override;
+ bool IsVisible() const override;
+protected:
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ bool UpdateColors(const CGUIListItem* item) override;
+ bool UpdateBarSize();
+ bool Move(int iNumSteps);
+ virtual void SetFromPosition(const CPoint &point);
+
+ std::unique_ptr<CGUITexture> m_guiBackground;
+ std::unique_ptr<CGUITexture> m_guiBarNoFocus;
+ std::unique_ptr<CGUITexture> m_guiBarFocus;
+ std::unique_ptr<CGUITexture> m_guiNibNoFocus;
+ std::unique_ptr<CGUITexture> m_guiNibFocus;
+
+ int m_numItems;
+ int m_pageSize;
+ int m_offset;
+
+ bool m_showOnePage;
+ ORIENTATION m_orientation;
+
+private:
+ GUIScrollBarControl(const GUIScrollBarControl& control);
+};
+
diff --git a/xbmc/guilib/GUISettingsSliderControl.cpp b/xbmc/guilib/GUISettingsSliderControl.cpp
new file mode 100644
index 0000000..7c275ab
--- /dev/null
+++ b/xbmc/guilib/GUISettingsSliderControl.cpp
@@ -0,0 +1,169 @@
+/*
+ * 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 "GUISettingsSliderControl.h"
+
+#include "input/Key.h"
+
+CGUISettingsSliderControl::CGUISettingsSliderControl(int parentID, int controlID, float posX, float posY, float width, float height, float sliderWidth, float sliderHeight, const CTextureInfo &textureFocus, const CTextureInfo &textureNoFocus, const CTextureInfo& backGroundTexture, const CTextureInfo& nibTexture, const CTextureInfo& nibTextureFocus, const CLabelInfo &labelInfo, int iType)
+ : CGUISliderControl(parentID, controlID, posX, posY, sliderWidth, sliderHeight, backGroundTexture, nibTexture,nibTextureFocus, iType, HORIZONTAL)
+ , m_buttonControl(parentID, controlID, posX, posY, width, height, textureFocus, textureNoFocus, labelInfo)
+ , m_label(posX, posY, width, height, labelInfo)
+{
+ m_label.SetAlign((labelInfo.align & XBFONT_CENTER_Y) | XBFONT_RIGHT);
+ ControlType = GUICONTROL_SETTINGS_SLIDER;
+ m_active = false;
+}
+
+CGUISettingsSliderControl::CGUISettingsSliderControl(const CGUISettingsSliderControl& control)
+ : CGUISliderControl(control),
+ m_buttonControl(control.m_buttonControl),
+ m_label(control.m_label),
+ m_active(control.m_active)
+{
+}
+
+void CGUISettingsSliderControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_bInvalidated)
+ {
+ float sliderPosX = m_buttonControl.GetXPosition() + m_buttonControl.GetWidth() - m_width - m_buttonControl.GetLabelInfo().offsetX;
+ float sliderPosY = m_buttonControl.GetYPosition() + (m_buttonControl.GetHeight() - m_height) * 0.5f;
+ CGUISliderControl::SetPosition(sliderPosX, sliderPosY);
+ }
+ m_buttonControl.SetFocus(HasFocus());
+ m_buttonControl.SetPulseOnSelect(m_pulseOnSelect);
+ m_buttonControl.SetEnabled(m_enabled);
+ m_buttonControl.DoProcess(currentTime, dirtyregions);
+ ProcessText();
+ CGUISliderControl::Process(currentTime, dirtyregions);
+}
+
+void CGUISettingsSliderControl::Render()
+{
+ m_buttonControl.Render();
+ CGUISliderControl::Render();
+ m_label.Render();
+}
+
+void CGUISettingsSliderControl::ProcessText()
+{
+ bool changed = false;
+
+ changed |= m_label.SetMaxRect(m_buttonControl.GetXPosition(), m_buttonControl.GetYPosition(), m_posX - m_buttonControl.GetXPosition(), m_buttonControl.GetHeight());
+ changed |= m_label.SetText(CGUISliderControl::GetDescription());
+ if (IsDisabled())
+ changed |= m_label.SetColor(CGUILabel::COLOR_DISABLED);
+ else if (HasFocus())
+ changed |= m_label.SetColor(CGUILabel::COLOR_FOCUSED);
+ else
+ changed |= m_label.SetColor(CGUILabel::COLOR_TEXT);
+
+ if (changed)
+ MarkDirtyRegion();
+}
+
+bool CGUISettingsSliderControl::OnAction(const CAction &action)
+{
+ // intercept ACTION_SELECT_ITEM because onclick functionality is different from base class
+ if (action.GetID() == ACTION_SELECT_ITEM)
+ {
+ if (!IsActive())
+ m_active = true;
+ // switch between the two sliders
+ else if (m_rangeSelection && m_currentSelector == RangeSelectorLower)
+ SwitchRangeSelector();
+ else
+ {
+ m_active = false;
+ if (m_rangeSelection)
+ SwitchRangeSelector();
+ }
+ return true;
+ }
+ return CGUISliderControl::OnAction(action);
+}
+
+void CGUISettingsSliderControl::OnUnFocus()
+{
+ m_active = false;
+}
+
+EVENT_RESULT CGUISettingsSliderControl::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ SetActive();
+ return CGUISliderControl::OnMouseEvent(point, event);
+}
+
+void CGUISettingsSliderControl::SetActive()
+{
+ m_active = true;
+}
+
+void CGUISettingsSliderControl::FreeResources(bool immediately)
+{
+ CGUISliderControl::FreeResources(immediately);
+ m_buttonControl.FreeResources(immediately);
+}
+
+void CGUISettingsSliderControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUISliderControl::DynamicResourceAlloc(bOnOff);
+ m_buttonControl.DynamicResourceAlloc(bOnOff);
+}
+
+void CGUISettingsSliderControl::AllocResources()
+{
+ CGUISliderControl::AllocResources();
+ m_buttonControl.AllocResources();
+}
+
+void CGUISettingsSliderControl::SetInvalid()
+{
+ CGUISliderControl::SetInvalid();
+ m_buttonControl.SetInvalid();
+}
+
+void CGUISettingsSliderControl::SetPosition(float posX, float posY)
+{
+ m_buttonControl.SetPosition(posX, posY);
+ CGUISliderControl::SetInvalid();
+}
+
+void CGUISettingsSliderControl::SetWidth(float width)
+{
+ m_buttonControl.SetWidth(width);
+ CGUISliderControl::SetInvalid();
+}
+
+void CGUISettingsSliderControl::SetHeight(float height)
+{
+ m_buttonControl.SetHeight(height);
+ CGUISliderControl::SetInvalid();
+}
+
+void CGUISettingsSliderControl::SetEnabled(bool bEnable)
+{
+ CGUISliderControl::SetEnabled(bEnable);
+ m_buttonControl.SetEnabled(bEnable);
+}
+
+std::string CGUISettingsSliderControl::GetDescription() const
+{
+ return m_buttonControl.GetDescription() + " " + CGUISliderControl::GetDescription();
+}
+
+bool CGUISettingsSliderControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUISliderControl::UpdateColors(nullptr);
+ changed |= m_buttonControl.SetColorDiffuse(m_diffuseColor);
+ changed |= m_buttonControl.UpdateColors(nullptr);
+ changed |= m_label.UpdateColors();
+
+ return changed;
+}
diff --git a/xbmc/guilib/GUISettingsSliderControl.dox b/xbmc/guilib/GUISettingsSliderControl.dox
new file mode 100644
index 0000000..ea85da8
--- /dev/null
+++ b/xbmc/guilib/GUISettingsSliderControl.dox
@@ -0,0 +1,80 @@
+/*!
+
+\page Settings_Slider_Control Settings Slider Control
+\brief **Used for a slider control in the settings menus.**
+
+\tableofcontents
+
+The settings slider control is used in the settings screens for when an option
+is best specified on a sliding scale. You can choose the position, size, and
+look of the slider control. It is basically a cross between the button control
+and a slider control. It has a label and focus and non focus textures, as well
+as a slider control on the right.
+
+--------------------------------------------------------------------------------
+\section Settings_Slider_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="sliderex" id="12">
+ <description>My first settings slider control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <sliderwidth>100</sliderwidth>
+ <sliderheight>20</sliderheight>
+ <visible>true</visible>
+ <texturefocus>myfocustexture.png</texturefocus>
+ <texturenofocus>mynofocustexture.png</texturenofocus>
+ <texturebg>mybackgroundtexture.png</texturebg>
+ <textureslidernib>mydowntexture.png</textureslidernib>
+ <textureslidernibfocus>mydownfocustexture.png</textureslidernibfocus>
+ <info></info>
+ <label>46</label>
+ <font>font12</font>
+ <textcolor>FFFFFFFF</textcolor>
+ <disabledcolor>80FFFFFF</disabledcolor>
+ <textoffsetx></textoffsetx>
+ <pulseonselect></pulseonselect>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Settings_Slider_Control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is lower case only. This is
+important, as xml tags are case-sensitive.
+
+| Tag | Description |
+|----------------------:|:--------------------------------------------------------------|
+| sliderwidth | Specifies the width of the slider portion of the slider control (ie without the text value, if present). The texture image for the slider background will be resized to fit into this width, and the nib textures will be resized by the same amount.
+| sliderheight | Specifies the height of the slider portion of the slider control (ie without the text value, if present). The texture image for the slider background will be resized to fit into this height, and the nib textures will be resized by the same amount.
+| texturefocus | Specifies the image file which should be displayed for the control when it has focus. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| texturenofocus | Specifies the image file which should be displayed for the control when it doesn't focus.
+| texturebg | Specifies the image file which should be displayed in the background of the slider portion of the control. Will be positioned so that the right edge is <b>`<textoffsetx>`</b> away from the right edge of the <b>`<texturefocus>`</b> image, and centered vertically.
+| textureslidernib | Specifies the image file which should be displayed for the slider nib.
+| textureslidernibfocus | Specifies the image file which should be displayed for the slider nib when it has focus.
+| label | Either a numeric reference into strings.po (for localization), or a string that will be shown on the left of the control.
+| font | Font used for the controls label. From fonts.xml.
+| textcolor | Color used for displaying the label. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| disabledcolor | Color used for the label if the control is disabled. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| shadowcolor | Specifies the color of the drop shadow on the text. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| textoffsetx | Amount to offset the label from the left edge of the control.
+| info | Specifies the information that the slider controls. [See here for more information](http://kodi.wiki/view/InfoLabels).
+
+
+--------------------------------------------------------------------------------
+\section Settings_Slider_Control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUISettingsSliderControl.h b/xbmc/guilib/GUISettingsSliderControl.h
new file mode 100644
index 0000000..98b0411
--- /dev/null
+++ b/xbmc/guilib/GUISettingsSliderControl.h
@@ -0,0 +1,66 @@
+/*
+ * 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
+
+/*!
+\file GUISliderControl.h
+\brief
+*/
+
+#include "GUIButtonControl.h"
+#include "GUISliderControl.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUISettingsSliderControl :
+ public CGUISliderControl
+{
+public:
+ CGUISettingsSliderControl(int parentID, int controlID, float posX, float posY, float width, float height, float sliderWidth, float sliderHeight, const CTextureInfo &textureFocus, const CTextureInfo &textureNoFocus, const CTextureInfo& backGroundTexture, const CTextureInfo& nibTexture, const CTextureInfo& nibTextureFocus, const CLabelInfo &labelInfo, int iType);
+ ~CGUISettingsSliderControl() override = default;
+ CGUISettingsSliderControl *Clone() const override { return new CGUISettingsSliderControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnAction(const CAction &action) override;
+ void OnUnFocus() override;
+ EVENT_RESULT OnMouseEvent(const CPoint& point, const CMouseEvent& event) override;
+ void SetActive();
+ bool IsActive() const override { return m_active; }
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ void SetPosition(float posX, float posY) override;
+ float GetWidth() const override { return m_buttonControl.GetWidth(); }
+ void SetWidth(float width) override;
+ float GetHeight() const override { return m_buttonControl.GetHeight(); }
+ void SetHeight(float height) override;
+ void SetEnabled(bool bEnable) override;
+
+ void SetText(const std::string& label) { m_buttonControl.SetLabel(label); }
+ float GetXPosition() const override { return m_buttonControl.GetXPosition(); }
+ float GetYPosition() const override { return m_buttonControl.GetYPosition(); }
+ std::string GetDescription() const override;
+ bool HitTest(const CPoint& point) const override { return m_buttonControl.HitTest(point); }
+
+protected:
+ bool UpdateColors(const CGUIListItem* item) override;
+ virtual void ProcessText();
+
+private:
+ CGUISettingsSliderControl(const CGUISettingsSliderControl& control);
+
+ CGUIButtonControl m_buttonControl;
+ CGUILabel m_label;
+ bool m_active; ///< Whether the slider has been activated by a click.
+};
+
diff --git a/xbmc/guilib/GUIShaderDX.cpp b/xbmc/guilib/GUIShaderDX.cpp
new file mode 100644
index 0000000..b9e801e
--- /dev/null
+++ b/xbmc/guilib/GUIShaderDX.cpp
@@ -0,0 +1,423 @@
+/*
+ * 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 "GUIShaderDX.h"
+#include "windowing/GraphicContext.h"
+#include "rendering/dx/DeviceResources.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/log.h"
+
+// shaders bytecode includes
+#include "guishader_vert.h"
+#include "guishader_checkerboard_right.h"
+#include "guishader_checkerboard_left.h"
+#include "guishader_default.h"
+#include "guishader_fonts.h"
+#include "guishader_interlaced_right.h"
+#include "guishader_interlaced_left.h"
+#include "guishader_multi_texture_blend.h"
+#include "guishader_texture.h"
+#include "guishader_texture_noblend.h"
+
+#include <d3dcompiler.h>
+
+using namespace DirectX;
+using namespace Microsoft::WRL;
+
+// shaders bytecode holder
+static const D3D_SHADER_DATA cbPSShaderCode[SHADER_METHOD_RENDER_COUNT] =
+{
+ { guishader_default, sizeof(guishader_default) }, // SHADER_METHOD_RENDER_DEFAULT
+ { guishader_texture_noblend, sizeof(guishader_texture_noblend) }, // SHADER_METHOD_RENDER_TEXTURE_NOBLEND
+ { guishader_fonts, sizeof(guishader_fonts) }, // SHADER_METHOD_RENDER_FONT
+ { guishader_texture, sizeof(guishader_texture) }, // SHADER_METHOD_RENDER_TEXTURE_BLEND
+ { guishader_multi_texture_blend, sizeof(guishader_multi_texture_blend) }, // SHADER_METHOD_RENDER_MULTI_TEXTURE_BLEND
+ { guishader_interlaced_left, sizeof(guishader_interlaced_left) }, // SHADER_METHOD_RENDER_STEREO_INTERLACED_LEFT
+ { guishader_interlaced_right, sizeof(guishader_interlaced_right) }, // SHADER_METHOD_RENDER_STEREO_INTERLACED_RIGHT
+ { guishader_checkerboard_left, sizeof(guishader_checkerboard_left) }, // SHADER_METHOD_RENDER_STEREO_CHECKERBOARD_LEFT
+ { guishader_checkerboard_right, sizeof(guishader_checkerboard_right) }, // SHADER_METHOD_RENDER_STEREO_CHECKERBOARD_RIGHT
+};
+
+CGUIShaderDX::CGUIShaderDX() :
+ m_pSampLinear(nullptr),
+ m_pVPBuffer(nullptr),
+ m_pWVPBuffer(nullptr),
+ m_pVertexBuffer(nullptr),
+ m_clipXFactor(0.0f),
+ m_clipXOffset(0.0f),
+ m_clipYFactor(0.0f),
+ m_clipYOffset(0.0f),
+ m_bIsWVPDirty(false),
+ m_bIsVPDirty(false),
+ m_bCreated(false),
+ m_currentShader(0),
+ m_clipPossible(false)
+{
+}
+
+CGUIShaderDX::~CGUIShaderDX()
+{
+ Release();
+}
+
+bool CGUIShaderDX::Initialize()
+{
+ // Create input layout
+ D3D11_INPUT_ELEMENT_DESC layout[] =
+ {
+ { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 28, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ { "TEXCOORD", 1, DXGI_FORMAT_R32G32_FLOAT, 0, 36, D3D11_INPUT_PER_VERTEX_DATA, 0 },
+ };
+
+ if (!m_vertexShader.Create(guishader_vert, sizeof(guishader_vert), layout, ARRAYSIZE(layout)))
+ return false;
+
+ size_t i;
+ bool bSuccess = true;
+ for (i = 0; i < SHADER_METHOD_RENDER_COUNT; i++)
+ {
+ if (!m_pixelShader[i].Create(cbPSShaderCode[i].pBytecode, cbPSShaderCode[i].BytecodeLength))
+ {
+ bSuccess = false;
+ break;
+ }
+ }
+
+ if (!bSuccess)
+ {
+ m_vertexShader.Release();
+ for (size_t j = 0; j < i; j++)
+ m_pixelShader[j].Release();
+ }
+
+ if (!bSuccess || !CreateBuffers() || !CreateSamplers())
+ return false;
+
+ m_bCreated = true;
+ return true;
+}
+
+bool CGUIShaderDX::CreateBuffers()
+{
+ ComPtr<ID3D11Device> pDevice = DX::DeviceResources::Get()->GetD3DDevice();
+
+ // create vertex buffer
+ CD3D11_BUFFER_DESC bufferDesc(sizeof(Vertex) * 4, D3D11_BIND_VERTEX_BUFFER, D3D11_USAGE_DYNAMIC, D3D11_CPU_ACCESS_WRITE);
+ if (FAILED(pDevice->CreateBuffer(&bufferDesc, NULL, m_pVertexBuffer.ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGERROR, "Failed to create GUI vertex buffer.");
+ return false;
+ }
+
+ // Create the constant buffer for WVP
+ size_t buffSize = (sizeof(cbWorld) + 15) & ~15;
+ CD3D11_BUFFER_DESC cbbd(buffSize, D3D11_BIND_CONSTANT_BUFFER, D3D11_USAGE_DYNAMIC, D3D11_CPU_ACCESS_WRITE); // it can change very frequently
+ if (FAILED(pDevice->CreateBuffer(&cbbd, NULL, m_pWVPBuffer.ReleaseAndGetAddressOf())))
+ {
+ CLog::LogF(LOGERROR, "Failed to create the constant buffer.");
+ return false;
+ }
+ m_bIsWVPDirty = true;
+
+ CRect viewPort;
+ DX::Windowing()->GetViewPort(viewPort);
+
+ // initial data for viewport buffer
+ m_cbViewPort.TopLeftX = viewPort.x1;
+ m_cbViewPort.TopLeftY = viewPort.y1;
+ m_cbViewPort.Width = viewPort.Width();
+ m_cbViewPort.Height = viewPort.Height();
+
+ cbbd.ByteWidth = sizeof(cbViewPort);
+ D3D11_SUBRESOURCE_DATA initData = { &m_cbViewPort, 0, 0 };
+ // create viewport buffer
+ if (FAILED(pDevice->CreateBuffer(&cbbd, &initData, m_pVPBuffer.ReleaseAndGetAddressOf())))
+ return false;
+
+ return true;
+}
+
+bool CGUIShaderDX::CreateSamplers()
+{
+ // Describe the Sampler State
+ D3D11_SAMPLER_DESC sampDesc = {};
+ sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
+ sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
+ sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
+ sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
+ sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
+ sampDesc.MinLOD = 0;
+ sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
+
+ if (FAILED(DX::DeviceResources::Get()->GetD3DDevice()->CreateSamplerState(&sampDesc, m_pSampLinear.ReleaseAndGetAddressOf())))
+ return false;
+
+ DX::DeviceResources::Get()->GetD3DContext()->PSSetSamplers(0, 1, m_pSampLinear.GetAddressOf());
+
+ return true;
+}
+
+void CGUIShaderDX::ApplyStateBlock(void)
+{
+ if (!m_bCreated)
+ return;
+
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext();
+
+ m_vertexShader.BindShader();
+ pContext->VSSetConstantBuffers(0, 1, m_pWVPBuffer.GetAddressOf());
+
+ m_pixelShader[m_currentShader].BindShader();
+ pContext->PSSetConstantBuffers(0, 1, m_pWVPBuffer.GetAddressOf());
+ pContext->PSSetConstantBuffers(1, 1, m_pVPBuffer.GetAddressOf());
+
+ pContext->PSSetSamplers(0, 1, m_pSampLinear.GetAddressOf());
+
+ RestoreBuffers();
+}
+
+void CGUIShaderDX::Begin(unsigned int flags)
+{
+ if (!m_bCreated)
+ return;
+
+ if (m_currentShader != flags)
+ {
+ m_currentShader = flags;
+ m_pixelShader[m_currentShader].BindShader();
+ }
+ ClipToScissorParams();
+}
+
+void CGUIShaderDX::End()
+{
+ if (!m_bCreated)
+ return;
+}
+
+void CGUIShaderDX::DrawQuad(Vertex& v1, Vertex& v2, Vertex& v3, Vertex& v4)
+{
+ if (!m_bCreated)
+ return;
+
+ ApplyChanges();
+
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext();
+
+ // update vertex buffer
+ D3D11_MAPPED_SUBRESOURCE resource;
+ if (SUCCEEDED(pContext->Map(m_pVertexBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &resource)))
+ {
+ // we are using strip topology
+ Vertex vertices[4] = { v2, v3, v1, v4 };
+ memcpy(resource.pData, &vertices, sizeof(Vertex) * 4);
+ pContext->Unmap(m_pVertexBuffer.Get(), 0);
+ // Draw primitives
+ pContext->Draw(4, 0);
+ }
+}
+
+void CGUIShaderDX::DrawIndexed(unsigned int indexCount, unsigned int startIndex, unsigned int startVertex)
+{
+ if (!m_bCreated)
+ return;
+
+ ApplyChanges();
+ DX::DeviceResources::Get()->GetD3DContext()->DrawIndexed(indexCount, startIndex, startVertex);
+}
+
+void CGUIShaderDX::Draw(unsigned int vertexCount, unsigned int startVertex)
+{
+ if (!m_bCreated)
+ return;
+
+ ApplyChanges();
+ DX::DeviceResources::Get()->GetD3DContext()->Draw(vertexCount, startVertex);
+}
+
+void CGUIShaderDX::SetShaderViews(unsigned int numViews, ID3D11ShaderResourceView** views)
+{
+ if (!m_bCreated)
+ return;
+
+ DX::DeviceResources::Get()->GetD3DContext()->PSSetShaderResources(0, numViews, views);
+}
+
+void CGUIShaderDX::Release()
+{
+ m_pVertexBuffer = nullptr;
+ m_pWVPBuffer = nullptr;
+ m_pVPBuffer = nullptr;
+ m_pSampLinear = nullptr;
+ m_bCreated = false;
+}
+
+void CGUIShaderDX::SetViewPort(D3D11_VIEWPORT viewPort)
+{
+ if (!m_pVPBuffer)
+ return;
+
+ if ( viewPort.TopLeftX != m_cbViewPort.TopLeftX
+ || viewPort.TopLeftY != m_cbViewPort.TopLeftY
+ || viewPort.Width != m_cbViewPort.Width
+ || viewPort.Height != m_cbViewPort.Height)
+ {
+ m_cbViewPort.TopLeftX = viewPort.TopLeftX;
+ m_cbViewPort.TopLeftY = viewPort.TopLeftY;
+ m_cbViewPort.Width = viewPort.Width;
+ m_cbViewPort.Height = viewPort.Height;
+ m_bIsVPDirty = true;
+ }
+}
+
+void CGUIShaderDX::Project(float &x, float &y, float &z)
+{
+#if defined(_XM_SSE_INTRINSICS_) && !defined(_XM_NO_INTRINSICS_)
+ XMVECTOR vLocation = { x, y, z };
+#elif defined(_XM_ARM_NEON_INTRINSICS_) && !defined(_XM_NO_INTRINSICS_)
+ XMVECTOR vLocation = { x, y };
+#endif
+ XMVECTOR vScreenCoord = XMVector3Project(vLocation, m_cbViewPort.TopLeftX, m_cbViewPort.TopLeftY,
+ m_cbViewPort.Width, m_cbViewPort.Height, 0, 1,
+ m_cbWorldViewProj.projection, m_cbWorldViewProj.view, m_cbWorldViewProj.world);
+ x = XMVectorGetX(vScreenCoord);
+ y = XMVectorGetY(vScreenCoord);
+ z = 0;
+}
+
+void XM_CALLCONV CGUIShaderDX::SetWVP(const XMMATRIX &w, const XMMATRIX &v, const XMMATRIX &p)
+{
+ m_bIsWVPDirty = true;
+ m_cbWorldViewProj.world = w;
+ m_cbWorldViewProj.view = v;
+ m_cbWorldViewProj.projection = p;
+}
+
+void CGUIShaderDX::SetWorld(const XMMATRIX &value)
+{
+ m_bIsWVPDirty = true;
+ m_cbWorldViewProj.world = value;
+}
+
+void CGUIShaderDX::SetView(const XMMATRIX &value)
+{
+ m_bIsWVPDirty = true;
+ m_cbWorldViewProj.view = value;
+}
+
+void CGUIShaderDX::SetProjection(const XMMATRIX &value)
+{
+ m_bIsWVPDirty = true;
+ m_cbWorldViewProj.projection = value;
+}
+
+void CGUIShaderDX::ApplyChanges(void)
+{
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext();
+ D3D11_MAPPED_SUBRESOURCE res;
+
+ if (m_bIsWVPDirty)
+ {
+ if (SUCCEEDED(pContext->Map(m_pWVPBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &res)))
+ {
+ XMMATRIX worldView = XMMatrixMultiply(m_cbWorldViewProj.world, m_cbWorldViewProj.view);
+ XMMATRIX worldViewProj = XMMatrixMultiplyTranspose(worldView, m_cbWorldViewProj.projection);
+
+ cbWorld* buffer = (cbWorld*)res.pData;
+ buffer->wvp = worldViewProj;
+ buffer->blackLevel = (DX::Windowing()->UseLimitedColor() ? 16.f / 255.f : 0.f);
+ buffer->colorRange = (DX::Windowing()->UseLimitedColor() ? (235.f - 16.f) / 255.f : 1.0f);
+ buffer->sdrPeakLum = (100 - DX::Windowing()->GetGuiSdrPeakLuminance()) + 10;
+ buffer->PQ = (DX::Windowing()->IsTransferPQ() ? 1 : 0);
+
+ pContext->Unmap(m_pWVPBuffer.Get(), 0);
+ m_bIsWVPDirty = false;
+ }
+ }
+
+ // update view port buffer
+ if (m_bIsVPDirty)
+ {
+ if (SUCCEEDED(pContext->Map(m_pVPBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &res)))
+ {
+ *(cbViewPort*)res.pData = m_cbViewPort;
+ pContext->Unmap(m_pVPBuffer.Get(), 0);
+ m_bIsVPDirty = false;
+ }
+ }
+}
+
+void CGUIShaderDX::RestoreBuffers(void)
+{
+ const unsigned stride = sizeof(Vertex);
+ const unsigned offset = 0;
+
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext();
+ // Set the vertex buffer to active in the input assembler so it can be rendered.
+ pContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &stride, &offset);
+ // Set the type of primitive that should be rendered from this vertex buffer, in this case triangles.
+ pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
+}
+
+void CGUIShaderDX::ClipToScissorParams(void)
+{
+ CRect viewPort; // absolute positions of corners
+ DX::Windowing()->GetViewPort(viewPort);
+
+ // get current GUI transform
+ const TransformMatrix &guiMatrix = CServiceBroker::GetWinSystem()->GetGfxContext().GetGUIMatrix();
+ // get current GPU transforms
+ XMFLOAT4X4 world, view, projection;
+ XMStoreFloat4x4(&world, m_cbWorldViewProj.world);
+ XMStoreFloat4x4(&view, m_cbWorldViewProj.view);
+ XMStoreFloat4x4(&projection, m_cbWorldViewProj.projection);
+
+ m_clipPossible = guiMatrix.m[0][1] == 0 &&
+ guiMatrix.m[1][0] == 0 &&
+ guiMatrix.m[2][0] == 0 &&
+ guiMatrix.m[2][1] == 0 &&
+ view.m[0][1] == 0 &&
+ view.m[0][2] == 0 &&
+ view.m[1][0] == 0 &&
+ view.m[1][2] == 0 &&
+ view.m[2][0] == 0 &&
+ view.m[2][1] == 0 &&
+ projection.m[0][1] == 0 &&
+ projection.m[0][2] == 0 &&
+ projection.m[0][3] == 0 &&
+ projection.m[1][0] == 0 &&
+ projection.m[1][2] == 0 &&
+ projection.m[1][3] == 0 &&
+ projection.m[3][0] == 0 &&
+ projection.m[3][1] == 0 &&
+ projection.m[3][3] == 0;
+
+ m_clipXFactor = 0.0f;
+ m_clipXOffset = 0.0f;
+ m_clipYFactor = 0.0f;
+ m_clipYOffset = 0.0f;
+
+ if (m_clipPossible)
+ {
+ m_clipXFactor = guiMatrix.m[0][0] * view.m[0][0] * projection.m[0][0];
+ m_clipXOffset = (guiMatrix.m[0][3] * view.m[0][0] + view.m[3][0]) * projection.m[0][0];
+ m_clipYFactor = guiMatrix.m[1][1] * view.m[1][1] * projection.m[1][1];
+ m_clipYOffset = (guiMatrix.m[1][3] * view.m[1][1] + view.m[3][1]) * projection.m[1][1];
+
+ float clipW = (guiMatrix.m[2][3] * view.m[2][2] + view.m[3][2]) * projection.m[2][3];
+ float xMult = (viewPort.x2 - viewPort.x1) / (2 * clipW);
+ float yMult = (viewPort.y1 - viewPort.y2) / (2 * clipW); // correct for inverted window coordinate scheme
+
+ m_clipXFactor = m_clipXFactor * xMult;
+ m_clipXOffset = m_clipXOffset * xMult + (viewPort.x2 + viewPort.x1) / 2;
+ m_clipYFactor = m_clipYFactor * yMult;
+ m_clipYOffset = m_clipYOffset * yMult + (viewPort.y2 + viewPort.y1) / 2;
+ }
+}
diff --git a/xbmc/guilib/GUIShaderDX.h b/xbmc/guilib/GUIShaderDX.h
new file mode 100644
index 0000000..7b6a170
--- /dev/null
+++ b/xbmc/guilib/GUIShaderDX.h
@@ -0,0 +1,142 @@
+/*
+ * 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 "D3DResource.h"
+#include "Texture.h"
+#include "utils/Geometry.h"
+#include "utils/MemUtils.h"
+
+#include <DirectXMath.h>
+#include <wrl/client.h>
+
+struct Vertex {
+ Vertex() {}
+
+ Vertex(DirectX::XMFLOAT3 p, DirectX::XMFLOAT4 c)
+ : pos(p), color(c) {}
+
+ Vertex(DirectX::XMFLOAT3 p, DirectX::XMFLOAT4 c, DirectX::XMFLOAT2 t1, DirectX::XMFLOAT2 t2)
+ : pos(p), color(c), texCoord(t1), texCoord2(t2) {}
+
+ DirectX::XMFLOAT3 pos;
+ DirectX::XMFLOAT4 color;
+ DirectX::XMFLOAT2 texCoord;
+ DirectX::XMFLOAT2 texCoord2;
+};
+
+class ID3DResource;
+
+class CGUIShaderDX
+{
+public:
+ CGUIShaderDX();
+ ~CGUIShaderDX();
+
+ bool Initialize();
+ void Begin(unsigned int flags);
+ void End(void);
+ void ApplyStateBlock(void);
+ void RestoreBuffers(void);
+
+ void SetShaderViews(unsigned int numViews, ID3D11ShaderResourceView** views);
+ void SetViewPort(D3D11_VIEWPORT viewPort);
+
+ void XM_CALLCONV GetWVP(DirectX::XMMATRIX &w, DirectX::XMMATRIX &v, DirectX::XMMATRIX &p)
+ {
+ w = m_cbWorldViewProj.world;
+ v = m_cbWorldViewProj.view;
+ p = m_cbWorldViewProj.projection;
+ }
+ DirectX::XMMATRIX XM_CALLCONV GetWorld() const { return m_cbWorldViewProj.world; }
+ DirectX::XMMATRIX XM_CALLCONV GetView() const { return m_cbWorldViewProj.view; }
+ DirectX::XMMATRIX XM_CALLCONV GetProjection() const { return m_cbWorldViewProj.projection; }
+ void XM_CALLCONV SetWVP(const DirectX::XMMATRIX &w, const DirectX::XMMATRIX &v, const DirectX::XMMATRIX &p);
+ void XM_CALLCONV SetWorld(const DirectX::XMMATRIX &value);
+ void XM_CALLCONV SetView(const DirectX::XMMATRIX &value);
+ void XM_CALLCONV SetProjection(const DirectX::XMMATRIX &value);
+ void Project(float &x, float &y, float &z);
+
+ void DrawQuad(Vertex& v1, Vertex& v2, Vertex& v3, Vertex& v4);
+ void DrawIndexed(unsigned int indexCount, unsigned int startIndex, unsigned int startVertex);
+ void Draw(unsigned int vertexCount, unsigned int startVertex);
+
+ bool HardwareClipIsPossible(void) const { return m_clipPossible; }
+ float GetClipXFactor(void) const { return m_clipXFactor; }
+ float GetClipXOffset(void) const { return m_clipXOffset; }
+ float GetClipYFactor(void) const { return m_clipYFactor; }
+ float GetClipYOffset(void) const { return m_clipYOffset; }
+
+ // need to use aligned allocation because we use XMMATRIX in structures.
+ void* operator new (size_t size)
+ {
+ void* ptr = KODI::MEMORY::AlignedMalloc(size, __alignof(CGUIShaderDX));
+ if (!ptr)
+ throw std::bad_alloc();
+ return ptr;
+ }
+ // free aligned memory.
+ void operator delete (void* ptr)
+ {
+ KODI::MEMORY::AlignedFree(ptr);
+ }
+
+private:
+ struct cbWorldViewProj
+ {
+ DirectX::XMMATRIX world;
+ DirectX::XMMATRIX view;
+ DirectX::XMMATRIX projection;
+ };
+ struct cbViewPort
+ {
+ float TopLeftX;
+ float TopLeftY;
+ float Width;
+ float Height;
+ };
+ struct cbWorld
+ {
+ DirectX::XMMATRIX wvp;
+ float blackLevel;
+ float colorRange;
+ float sdrPeakLum;
+ int PQ;
+ };
+
+ void Release(void);
+ bool CreateBuffers(void);
+ bool CreateSamplers(void);
+ void ApplyChanges(void);
+ void ClipToScissorParams(void);
+
+ // GUI constants
+ cbViewPort m_cbViewPort = {};
+ cbWorldViewProj m_cbWorldViewProj = {};
+
+ bool m_bCreated;
+ size_t m_currentShader;
+ CD3DVertexShader m_vertexShader;
+ CD3DPixelShader m_pixelShader[SHADER_METHOD_RENDER_COUNT];
+ Microsoft::WRL::ComPtr<ID3D11SamplerState> m_pSampLinear;
+
+ // GUI buffers
+ bool m_bIsWVPDirty;
+ bool m_bIsVPDirty;
+ Microsoft::WRL::ComPtr<ID3D11Buffer> m_pWVPBuffer;
+ Microsoft::WRL::ComPtr<ID3D11Buffer> m_pVPBuffer;
+ Microsoft::WRL::ComPtr<ID3D11Buffer> m_pVertexBuffer;
+
+ // clip to scissors params
+ bool m_clipPossible;
+ float m_clipXFactor;
+ float m_clipXOffset;
+ float m_clipYFactor;
+ float m_clipYOffset;
+};
diff --git a/xbmc/guilib/GUISliderControl.cpp b/xbmc/guilib/GUISliderControl.cpp
new file mode 100644
index 0000000..7fd5b6e
--- /dev/null
+++ b/xbmc/guilib/GUISliderControl.cpp
@@ -0,0 +1,773 @@
+/*
+ * 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 "GUISliderControl.h"
+
+#include "GUIComponent.h"
+#include "GUIInfoManager.h"
+#include "GUIWindowManager.h"
+#include "ServiceBroker.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/Key.h"
+#include "input/mouse/MouseStat.h"
+#include "utils/MathUtils.h"
+#include "utils/StringUtils.h"
+
+static const SliderAction actions[] = {
+ {"seek", "PlayerControl(SeekPercentage({:2f}))", PLAYER_PROGRESS, false},
+ {"pvr.seek", "PVR.SeekPercentage({:2f})", PVR_TIMESHIFT_PROGRESS_PLAY_POS, false},
+ {"volume", "SetVolume({:2f})", PLAYER_VOLUME, true}};
+
+CGUISliderControl::CGUISliderControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& backGroundTexture,
+ const CTextureInfo& nibTexture,
+ const CTextureInfo& nibTextureFocus,
+ int iType,
+ ORIENTATION orientation)
+ : CGUIControl(parentID, controlID, posX, posY, width, height),
+ m_guiBackground(CGUITexture::CreateTexture(posX, posY, width, height, backGroundTexture)),
+ m_guiSelectorLower(CGUITexture::CreateTexture(posX, posY, width, height, nibTexture)),
+ m_guiSelectorUpper(CGUITexture::CreateTexture(posX, posY, width, height, nibTexture)),
+ m_guiSelectorLowerFocus(CGUITexture::CreateTexture(posX, posY, width, height, nibTextureFocus)),
+ m_guiSelectorUpperFocus(CGUITexture::CreateTexture(posX, posY, width, height, nibTextureFocus))
+{
+ m_iType = iType;
+ m_rangeSelection = false;
+ m_currentSelector = RangeSelectorLower; // use lower selector by default
+ m_percentValues[0] = 0;
+ m_percentValues[1] = 100;
+ m_iStart = 0;
+ m_iEnd = 100;
+ m_iInterval = 1;
+ m_fStart = 0.0f;
+ m_fEnd = 1.0f;
+ m_fInterval = 0.1f;
+ m_intValues[0] = m_iStart;
+ m_intValues[1] = m_iEnd;
+ m_floatValues[0] = m_fStart;
+ m_floatValues[1] = m_fEnd;
+ ControlType = GUICONTROL_SLIDER;
+ m_orientation = orientation;
+ m_iInfoCode = 0;
+ m_dragging = false;
+ m_action = NULL;
+}
+
+CGUISliderControl::CGUISliderControl(const CGUISliderControl& control)
+ : CGUIControl(control),
+ m_guiBackground(control.m_guiBackground->Clone()),
+ m_guiSelectorLower(control.m_guiSelectorLower->Clone()),
+ m_guiSelectorUpper(control.m_guiSelectorUpper->Clone()),
+ m_guiSelectorLowerFocus(control.m_guiSelectorLowerFocus->Clone()),
+ m_guiSelectorUpperFocus(control.m_guiSelectorUpperFocus->Clone()),
+ m_iType(control.m_iType),
+ m_rangeSelection(control.m_rangeSelection),
+ m_currentSelector(control.m_currentSelector),
+ m_percentValues(control.m_percentValues),
+ m_intValues(control.m_intValues),
+ m_iStart(control.m_iStart),
+ m_iInterval(control.m_iInterval),
+ m_iEnd(control.m_iEnd),
+ m_floatValues(control.m_floatValues),
+ m_fStart(control.m_fStart),
+ m_fInterval(control.m_fInterval),
+ m_fEnd(control.m_fEnd),
+ m_iInfoCode(control.m_iInfoCode),
+ m_textValue(control.m_textValue),
+ m_action(control.m_action),
+ m_dragging(control.m_dragging),
+ m_orientation(control.m_orientation)
+{
+}
+
+void CGUISliderControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ bool dirty = false;
+
+ dirty |= m_guiBackground->SetPosition(m_posX, m_posY);
+ int infoCode = m_iInfoCode;
+ if (m_action && (!m_dragging || m_action->fireOnDrag))
+ infoCode = m_action->infoCode;
+ if (infoCode)
+ {
+ int val;
+ if (CServiceBroker::GetGUI()->GetInfoManager().GetInt(val, infoCode, INFO::DEFAULT_CONTEXT))
+ SetIntValue(val);
+ }
+
+ dirty |= m_guiBackground->SetHeight(m_height);
+ dirty |= m_guiBackground->SetWidth(m_width);
+ dirty |= m_guiBackground->Process(currentTime);
+
+ CGUITexture* nibLower =
+ (IsActive() && m_bHasFocus && !IsDisabled() && m_currentSelector == RangeSelectorLower)
+ ? m_guiSelectorLowerFocus.get()
+ : m_guiSelectorLower.get();
+
+ float fScale = 1.0f;
+
+ if (m_orientation == HORIZONTAL && m_guiBackground->GetTextureHeight() != 0)
+ fScale = m_height / m_guiBackground->GetTextureHeight();
+ else if (m_width != 0 && nibLower->GetTextureWidth() != 0)
+ fScale = m_width / nibLower->GetTextureWidth();
+ dirty |= ProcessSelector(nibLower, currentTime, fScale, RangeSelectorLower);
+ if (m_rangeSelection)
+ {
+ CGUITexture* nibUpper =
+ (IsActive() && m_bHasFocus && !IsDisabled() && m_currentSelector == RangeSelectorUpper)
+ ? m_guiSelectorUpperFocus.get()
+ : m_guiSelectorUpper.get();
+
+ if (m_orientation == HORIZONTAL && m_guiBackground->GetTextureHeight() != 0)
+ fScale = m_height / m_guiBackground->GetTextureHeight();
+ else if (m_width != 0 && nibUpper->GetTextureWidth() != 0)
+ fScale = m_width / nibUpper->GetTextureWidth();
+
+ dirty |= ProcessSelector(nibUpper, currentTime, fScale, RangeSelectorUpper);
+ }
+
+ if (dirty)
+ MarkDirtyRegion();
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+bool CGUISliderControl::ProcessSelector(CGUITexture* nib,
+ unsigned int currentTime,
+ float fScale,
+ RangeSelector selector)
+{
+ bool dirty = false;
+ // we render the nib centered at the appropriate percentage, except where the nib
+ // would overflow the background image
+ if (m_orientation == HORIZONTAL)
+ {
+ dirty |= nib->SetHeight(nib->GetTextureHeight() * fScale);
+ dirty |= nib->SetWidth(nib->GetHeight() * 2);
+ }
+ else
+ {
+ dirty |= nib->SetWidth(nib->GetTextureWidth() * fScale);
+ dirty |= nib->SetHeight(nib->GetWidth() * 2);
+ }
+ CAspectRatio ratio(CAspectRatio::AR_KEEP);
+ ratio.align = ASPECT_ALIGN_LEFT | ASPECT_ALIGNY_CENTER;
+ dirty |= nib->SetAspectRatio(ratio);
+ dirty |= nib->Process(currentTime);
+ CRect rect = nib->GetRenderRect();
+
+ float offset;
+ if (m_orientation == HORIZONTAL)
+ {
+ offset = GetProportion(selector) * m_width - rect.Width() / 2;
+ if (offset > m_width - rect.Width())
+ offset = m_width - rect.Width();
+ if (offset < 0)
+ offset = 0;
+ dirty |=
+ nib->SetPosition(m_guiBackground->GetXPosition() + offset, m_guiBackground->GetYPosition());
+ }
+ else
+ {
+ offset = GetProportion(selector) * m_height - rect.Height() / 2;
+ if (offset > m_height - rect.Height())
+ offset = m_height - rect.Height();
+ if (offset < 0)
+ offset = 0;
+ dirty |=
+ nib->SetPosition(m_guiBackground->GetXPosition(),
+ m_guiBackground->GetYPosition() + m_guiBackground->GetHeight() - offset -
+ ((nib->GetHeight() - rect.Height()) / 2 + rect.Height()));
+ }
+ dirty |= nib->Process(currentTime); // need to process again as the position may have changed
+
+ return dirty;
+}
+
+void CGUISliderControl::Render()
+{
+ m_guiBackground->Render();
+ CGUITexture* nibLower =
+ (IsActive() && m_bHasFocus && !IsDisabled() && m_currentSelector == RangeSelectorLower)
+ ? m_guiSelectorLowerFocus.get()
+ : m_guiSelectorLower.get();
+ nibLower->Render();
+ if (m_rangeSelection)
+ {
+ CGUITexture* nibUpper =
+ (IsActive() && m_bHasFocus && !IsDisabled() && m_currentSelector == RangeSelectorUpper)
+ ? m_guiSelectorUpperFocus.get()
+ : m_guiSelectorUpper.get();
+ nibUpper->Render();
+ }
+ CGUIControl::Render();
+}
+
+bool CGUISliderControl::OnMessage(CGUIMessage& message)
+{
+ if (message.GetControlId() == GetID() )
+ {
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_ITEM_SELECT:
+ SetPercentage( (float)message.GetParam1() );
+ return true;
+ break;
+
+ case GUI_MSG_LABEL_RESET:
+ {
+ SetPercentage(0, RangeSelectorLower);
+ SetPercentage(100, RangeSelectorUpper);
+ return true;
+ }
+ break;
+ }
+ }
+
+ return CGUIControl::OnMessage(message);
+}
+
+bool CGUISliderControl::OnAction(const CAction &action)
+{
+ switch ( action.GetID() )
+ {
+ case ACTION_MOVE_LEFT:
+ if (IsActive() && m_orientation == HORIZONTAL)
+ {
+ Move(-1);
+ return true;
+ }
+ break;
+
+ case ACTION_MOVE_RIGHT:
+ if (IsActive() && m_orientation == HORIZONTAL)
+ {
+ Move(1);
+ return true;
+ }
+ break;
+
+ case ACTION_MOVE_UP:
+ if (IsActive() && m_orientation == VERTICAL)
+ {
+ Move(1);
+ return true;
+ }
+ break;
+
+ case ACTION_MOVE_DOWN:
+ if (IsActive() && m_orientation == VERTICAL)
+ {
+ Move(-1);
+ return true;
+ }
+ break;
+
+ case ACTION_SELECT_ITEM:
+ if (m_rangeSelection)
+ SwitchRangeSelector();
+ return true;
+
+ default:
+ break;
+ }
+ return CGUIControl::OnAction(action);
+}
+
+void CGUISliderControl::Move(int iNumSteps)
+{
+ bool rangeSwap = false;
+ switch (m_iType)
+ {
+ case SLIDER_CONTROL_TYPE_FLOAT:
+ {
+ float &value = m_floatValues[m_currentSelector];
+ value += m_fInterval * iNumSteps;
+ if (value < m_fStart) value = m_fStart;
+ if (value > m_fEnd) value = m_fEnd;
+ if (m_floatValues[0] > m_floatValues[1])
+ {
+ float valueLower = m_floatValues[0];
+ m_floatValues[0] = m_floatValues[1];
+ m_floatValues[1] = valueLower;
+ rangeSwap = true;
+ }
+ break;
+ }
+
+ case SLIDER_CONTROL_TYPE_INT:
+ {
+ int &value = m_intValues[m_currentSelector];
+ value += m_iInterval * iNumSteps;
+ if (value < m_iStart) value = m_iStart;
+ if (value > m_iEnd) value = m_iEnd;
+ if (m_intValues[0] > m_intValues[1])
+ {
+ int valueLower = m_intValues[0];
+ m_intValues[0] = m_intValues[1];
+ m_intValues[1] = valueLower;
+ rangeSwap = true;
+ }
+ break;
+ }
+
+ case SLIDER_CONTROL_TYPE_PERCENTAGE:
+ default:
+ {
+ float &value = m_percentValues[m_currentSelector];
+ value += m_iInterval * iNumSteps;
+ if (value < 0) value = 0;
+ if (value > 100) value = 100;
+ if (m_percentValues[0] > m_percentValues[1])
+ {
+ float valueLower = m_percentValues[0];
+ m_percentValues[0] = m_percentValues[1];
+ m_percentValues[1] = valueLower;
+ rangeSwap = true;
+ }
+ break;
+ }
+ }
+
+ if (rangeSwap)
+ SwitchRangeSelector();
+
+ SendClick();
+}
+
+void CGUISliderControl::SendClick()
+{
+ float percent = 100*GetProportion();
+ SEND_CLICK_MESSAGE(GetID(), GetParentID(), MathUtils::round_int(static_cast<double>(percent)));
+ if (m_action && (!m_dragging || m_action->fireOnDrag))
+ {
+ std::string action = StringUtils::Format(m_action->formatString, percent);
+ CGUIMessage message(GUI_MSG_EXECUTE, m_controlID, m_parentID);
+ message.SetStringParam(action);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+ }
+}
+
+void CGUISliderControl::SetRangeSelection(bool rangeSelection)
+{
+ if (m_rangeSelection == rangeSelection)
+ return;
+
+ m_rangeSelection = rangeSelection;
+ SetRangeSelector(RangeSelectorLower);
+ SetInvalid();
+}
+
+void CGUISliderControl::SetRangeSelector(RangeSelector selector)
+{
+ if (m_currentSelector == selector)
+ return;
+
+ m_currentSelector = selector;
+ SetInvalid();
+}
+
+void CGUISliderControl::SwitchRangeSelector()
+{
+ if (m_currentSelector == RangeSelectorLower)
+ SetRangeSelector(RangeSelectorUpper);
+ else
+ SetRangeSelector(RangeSelectorLower);
+}
+
+void CGUISliderControl::SetPercentage(float percent, RangeSelector selector /* = RangeSelectorLower */, bool updateCurrent /* = false */)
+{
+ if (percent > 100) percent = 100;
+ else if (percent < 0) percent = 0;
+
+ float percentLower = selector == RangeSelectorLower ? percent : m_percentValues[0];
+ float percentUpper = selector == RangeSelectorUpper ? percent : m_percentValues[1];
+ const float oldValues[2] = {m_percentValues[0], m_percentValues[1]};
+
+ if (!m_rangeSelection || percentLower <= percentUpper)
+ {
+ m_percentValues[0] = percentLower;
+ m_percentValues[1] = percentUpper;
+ if (updateCurrent)
+ m_currentSelector = selector;
+ }
+ else
+ {
+ m_percentValues[0] = percentUpper;
+ m_percentValues[1] = percentLower;
+ if (updateCurrent)
+ m_currentSelector = (selector == RangeSelectorLower ? RangeSelectorUpper : RangeSelectorLower);
+ }
+ if (oldValues[0] != m_percentValues[0] || oldValues[1] != m_percentValues[1])
+ MarkDirtyRegion();
+}
+
+float CGUISliderControl::GetPercentage(RangeSelector selector /* = RangeSelectorLower */) const
+{
+ return m_percentValues[selector];
+}
+
+void CGUISliderControl::SetIntValue(int iValue, RangeSelector selector /* = RangeSelectorLower */, bool updateCurrent /* = false */)
+{
+ if (m_iType == SLIDER_CONTROL_TYPE_FLOAT)
+ SetFloatValue((float)iValue, selector, updateCurrent);
+ else if (m_iType == SLIDER_CONTROL_TYPE_INT)
+ {
+ if (iValue > m_iEnd) iValue = m_iEnd;
+ else if (iValue < m_iStart) iValue = m_iStart;
+
+ int iValueLower = selector == RangeSelectorLower ? iValue : m_intValues[0];
+ int iValueUpper = selector == RangeSelectorUpper ? iValue : m_intValues[1];
+
+ if (!m_rangeSelection || iValueLower <= iValueUpper)
+ {
+ m_intValues[0] = iValueLower;
+ m_intValues[1] = iValueUpper;
+ if (updateCurrent)
+ m_currentSelector = selector;
+ }
+ else
+ {
+ m_intValues[0] = iValueUpper;
+ m_intValues[1] = iValueLower;
+ if (updateCurrent)
+ m_currentSelector = (selector == RangeSelectorLower ? RangeSelectorUpper : RangeSelectorLower);
+ }
+ }
+ else
+ SetPercentage((float)iValue, selector, updateCurrent);
+}
+
+int CGUISliderControl::GetIntValue(RangeSelector selector /* = RangeSelectorLower */) const
+{
+ if (m_iType == SLIDER_CONTROL_TYPE_FLOAT)
+ return (int)m_floatValues[selector];
+ else if (m_iType == SLIDER_CONTROL_TYPE_INT)
+ return m_intValues[selector];
+ else
+ return MathUtils::round_int(static_cast<double>(m_percentValues[selector]));
+}
+
+void CGUISliderControl::SetFloatValue(float fValue, RangeSelector selector /* = RangeSelectorLower */, bool updateCurrent /* = false */)
+{
+ if (m_iType == SLIDER_CONTROL_TYPE_FLOAT)
+ {
+ if (fValue > m_fEnd) fValue = m_fEnd;
+ else if (fValue < m_fStart) fValue = m_fStart;
+
+ float fValueLower = selector == RangeSelectorLower ? fValue : m_floatValues[0];
+ float fValueUpper = selector == RangeSelectorUpper ? fValue : m_floatValues[1];
+
+ if (!m_rangeSelection || fValueLower <= fValueUpper)
+ {
+ m_floatValues[0] = fValueLower;
+ m_floatValues[1] = fValueUpper;
+ if (updateCurrent)
+ m_currentSelector = selector;
+ }
+ else
+ {
+ m_floatValues[0] = fValueUpper;
+ m_floatValues[1] = fValueLower;
+ if (updateCurrent)
+ m_currentSelector = (selector == RangeSelectorLower ? RangeSelectorUpper : RangeSelectorLower);
+ }
+ }
+ else if (m_iType == SLIDER_CONTROL_TYPE_INT)
+ SetIntValue((int)fValue, selector, updateCurrent);
+ else
+ SetPercentage(fValue, selector, updateCurrent);
+}
+
+float CGUISliderControl::GetFloatValue(RangeSelector selector /* = RangeSelectorLower */) const
+{
+ if (m_iType == SLIDER_CONTROL_TYPE_FLOAT)
+ return m_floatValues[selector];
+ else if (m_iType == SLIDER_CONTROL_TYPE_INT)
+ return (float)m_intValues[selector];
+ else
+ return m_percentValues[selector];
+}
+
+void CGUISliderControl::SetIntInterval(int iInterval)
+{
+ if (m_iType == SLIDER_CONTROL_TYPE_FLOAT)
+ m_fInterval = (float)iInterval;
+ else
+ m_iInterval = iInterval;
+}
+
+void CGUISliderControl::SetFloatInterval(float fInterval)
+{
+ if (m_iType == SLIDER_CONTROL_TYPE_FLOAT)
+ m_fInterval = fInterval;
+ else
+ m_iInterval = (int)fInterval;
+}
+
+void CGUISliderControl::SetRange(int iStart, int iEnd)
+{
+ if (m_iType == SLIDER_CONTROL_TYPE_FLOAT)
+ SetFloatRange((float)iStart,(float)iEnd);
+ else
+ {
+ m_intValues[0] = m_iStart = iStart;
+ m_intValues[1] = m_iEnd = iEnd;
+ }
+}
+
+void CGUISliderControl::SetFloatRange(float fStart, float fEnd)
+{
+ if (m_iType == SLIDER_CONTROL_TYPE_INT)
+ SetRange((int)fStart, (int)fEnd);
+ else
+ {
+ m_floatValues[0] = m_fStart = fStart;
+ m_floatValues[1] = m_fEnd = fEnd;
+ }
+}
+
+void CGUISliderControl::FreeResources(bool immediately)
+{
+ CGUIControl::FreeResources(immediately);
+ m_guiBackground->FreeResources(immediately);
+ m_guiSelectorLower->FreeResources(immediately);
+ m_guiSelectorUpper->FreeResources(immediately);
+ m_guiSelectorLowerFocus->FreeResources(immediately);
+ m_guiSelectorUpperFocus->FreeResources(immediately);
+}
+
+void CGUISliderControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+ m_guiBackground->DynamicResourceAlloc(bOnOff);
+ m_guiSelectorLower->DynamicResourceAlloc(bOnOff);
+ m_guiSelectorUpper->DynamicResourceAlloc(bOnOff);
+ m_guiSelectorLowerFocus->DynamicResourceAlloc(bOnOff);
+ m_guiSelectorUpperFocus->DynamicResourceAlloc(bOnOff);
+}
+
+void CGUISliderControl::AllocResources()
+{
+ CGUIControl::AllocResources();
+ m_guiBackground->AllocResources();
+ m_guiSelectorLower->AllocResources();
+ m_guiSelectorUpper->AllocResources();
+ m_guiSelectorLowerFocus->AllocResources();
+ m_guiSelectorUpperFocus->AllocResources();
+}
+
+void CGUISliderControl::SetInvalid()
+{
+ CGUIControl::SetInvalid();
+ m_guiBackground->SetInvalid();
+ m_guiSelectorLower->SetInvalid();
+ m_guiSelectorUpper->SetInvalid();
+ m_guiSelectorLowerFocus->SetInvalid();
+ m_guiSelectorUpperFocus->SetInvalid();
+}
+
+bool CGUISliderControl::HitTest(const CPoint &point) const
+{
+ if (m_guiBackground->HitTest(point))
+ return true;
+ if (m_guiSelectorLower->HitTest(point))
+ return true;
+ if (m_rangeSelection && m_guiSelectorUpper->HitTest(point))
+ return true;
+ return false;
+}
+
+void CGUISliderControl::SetFromPosition(const CPoint &point, bool guessSelector /* = false */)
+{
+
+ float fPercent;
+ if (m_orientation == HORIZONTAL)
+ fPercent = (point.x - m_guiBackground->GetXPosition()) / m_guiBackground->GetWidth();
+ else
+ fPercent = (m_guiBackground->GetYPosition() + m_guiBackground->GetHeight() - point.y) /
+ m_guiBackground->GetHeight();
+
+ if (fPercent < 0) fPercent = 0;
+ if (fPercent > 1) fPercent = 1;
+
+ if (m_rangeSelection && guessSelector)
+ {
+ // choose selector which value is closer to value calculated from position
+ if (fabs(GetPercentage(RangeSelectorLower) - 100 * fPercent) <= fabs(GetPercentage(RangeSelectorUpper) - 100 * fPercent))
+ m_currentSelector = RangeSelectorLower;
+ else
+ m_currentSelector = RangeSelectorUpper;
+ }
+
+ switch (m_iType)
+ {
+ case SLIDER_CONTROL_TYPE_FLOAT:
+ {
+ float fValue = m_fStart + (m_fEnd - m_fStart) * fPercent;
+ SetFloatValue(MathUtils::RoundF(fValue, m_fInterval), m_currentSelector, true);
+ break;
+ }
+
+ case SLIDER_CONTROL_TYPE_INT:
+ {
+ int iValue = (int)(m_iStart + (float)(m_iEnd - m_iStart) * fPercent + 0.49f);
+ SetIntValue(iValue, m_currentSelector, true);
+ break;
+ }
+
+ case SLIDER_CONTROL_TYPE_PERCENTAGE:
+ default:
+ {
+ SetPercentage(fPercent * 100, m_currentSelector, true);
+ break;
+ }
+ }
+ SendClick();
+}
+
+EVENT_RESULT CGUISliderControl::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ m_dragging = false;
+ if (event.m_id == ACTION_MOUSE_DRAG || event.m_id == ACTION_MOUSE_DRAG_END)
+ {
+ m_dragging = true;
+ bool guessSelector = false;
+ if (static_cast<HoldAction>(event.m_state) == HoldAction::DRAG)
+ { // grab exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ guessSelector = true;
+ }
+ else if (static_cast<HoldAction>(event.m_state) == HoldAction::DRAG_END)
+ { // release exclusive access
+ m_dragging = false;
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
+ SendWindowMessage(msg);
+ }
+ SetFromPosition(point, guessSelector);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_MOUSE_LEFT_CLICK && m_guiBackground->HitTest(point))
+ {
+ SetFromPosition(point, true);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_MOUSE_WHEEL_UP)
+ {
+ if (m_guiBackground->HitTest(point))
+ {
+ Move(10);
+ return EVENT_RESULT_HANDLED;
+ }
+ }
+ else if (event.m_id == ACTION_MOUSE_WHEEL_DOWN)
+ {
+ if (m_guiBackground->HitTest(point))
+ {
+ Move(-10);
+ return EVENT_RESULT_HANDLED;
+ }
+ }
+ else if (event.m_id == ACTION_GESTURE_NOTIFY)
+ {
+ return EVENT_RESULT_PAN_HORIZONTAL_WITHOUT_INERTIA;
+ }
+ else if (event.m_id == ACTION_GESTURE_BEGIN)
+ { // grab exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_PAN)
+ { // do the drag
+ SetFromPosition(point);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_END || event.m_id == ACTION_GESTURE_ABORT)
+ { // release exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
+ SendWindowMessage(msg);
+ return EVENT_RESULT_HANDLED;
+ }
+ return EVENT_RESULT_UNHANDLED;
+}
+
+void CGUISliderControl::SetInfo(int iInfo)
+{
+ m_iInfoCode = iInfo;
+}
+
+std::string CGUISliderControl::GetDescription() const
+{
+ if (!m_textValue.empty())
+ return m_textValue;
+ std::string description;
+ if (m_iType == SLIDER_CONTROL_TYPE_FLOAT)
+ {
+ if (m_rangeSelection)
+ description = StringUtils::Format("[{:2.2f}, {:2.2f}]", m_floatValues[0], m_floatValues[1]);
+ else
+ description = StringUtils::Format("{:2.2f}", m_floatValues[0]);
+ }
+ else if (m_iType == SLIDER_CONTROL_TYPE_INT)
+ {
+ if (m_rangeSelection)
+ description = StringUtils::Format("[{}, {}]", m_intValues[0], m_intValues[1]);
+ else
+ description = std::to_string(m_intValues[0]);
+ }
+ else
+ {
+ if (m_rangeSelection)
+ description = StringUtils::Format("[{}%, {}%]", MathUtils::round_int(static_cast<double>(m_percentValues[0])),
+ MathUtils::round_int(static_cast<double>(m_percentValues[1])));
+ else
+ description = StringUtils::Format("{}%", MathUtils::round_int(static_cast<double>(m_percentValues[0])));
+ }
+ return description;
+}
+
+bool CGUISliderControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_guiBackground->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiSelectorLower->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiSelectorUpper->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiSelectorLowerFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_guiSelectorUpperFocus->SetDiffuseColor(m_diffuseColor);
+
+ return changed;
+}
+
+float CGUISliderControl::GetProportion(RangeSelector selector /* = RangeSelectorLower */) const
+{
+ if (m_iType == SLIDER_CONTROL_TYPE_FLOAT)
+ return m_fStart != m_fEnd ? (GetFloatValue(selector) - m_fStart) / (m_fEnd - m_fStart) : 0.0f;
+ else if (m_iType == SLIDER_CONTROL_TYPE_INT)
+ return m_iStart != m_iEnd ? (float)(GetIntValue(selector) - m_iStart) / (float)(m_iEnd - m_iStart) : 0.0f;
+ return 0.01f * GetPercentage(selector);
+}
+
+void CGUISliderControl::SetAction(const std::string &action)
+{
+ for (const SliderAction& a : actions)
+ {
+ if (StringUtils::EqualsNoCase(action, a.action))
+ {
+ m_action = &a;
+ return;
+ }
+ }
+ m_action = NULL;
+}
diff --git a/xbmc/guilib/GUISliderControl.dox b/xbmc/guilib/GUISliderControl.dox
new file mode 100644
index 0000000..10bd2ba
--- /dev/null
+++ b/xbmc/guilib/GUISliderControl.dox
@@ -0,0 +1,71 @@
+/*!
+
+\page Slider_Control Slider Control
+\brief **Used for a volume slider.**
+
+\tableofcontents
+
+The slider control is used for things where a sliding bar best represents the
+operation at hand (such as a volume control or seek control). You can choose
+the position, size, and look of the slider control.
+
+
+--------------------------------------------------------------------------------
+\section Slider_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="slider" id="17">
+ <description>My first slider control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>30</height>
+ <visible>true</visible>
+ <texturesliderbar>mybackgroundtexture.png</texturesliderbar>
+ <textureslidernib>mydowntexture.png</textureslidernib>
+ <textureslidernibfocus>mydownfocustexture.png</textureslidernibfocus>
+ <info></info>
+ <action></action>
+ <controloffsetx></controloffsetx>
+ <controloffsety></controloffsety>
+ <pulseonselect></pulseonselect>
+ <orientation>vertical</orientation>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Slider_Control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is lower case only. This is
+important, as xml tags are case-sensitive.
+
+| Tag | Description |
+|----------------------:|:--------------------------------------------------------------|
+| texturesliderbar | Specifies the image file which should be displayed in the background of the slider control. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| textureslidernib | Specifies the image file which should be displayed for the slider nib.
+| textureslidernibfocus | Specifies the image file which should be displayed for the slider nib when it has focus.
+| controloffsetx | Amount to offset the slider background texture from the left edge of the control. Only useful if a value is being rendered as well (ie in int or float mode).
+| controloffsety | Amount to offset the slider background texture from the top edge of the control.
+| info | Specifies the information that the slider controls. [See here for more information](http://kodi.wiki/view/InfoLabels).
+| orientation | Specifies whether this scrollbar is horizontal or vertical. Defaults to vertical.
+| action | Can be <b>`volume`</b> to adjust the volume, <b>`seek`</b> to change the seek position, <b>`pvr.seek`</b> for timeshifting in PVR.
+
+\section Slider_Control_revhistory Revision History
+
+@skinning_v18 <b>[Slider Control]</b> Added <b>`pvr.seek`</b> as possible <b>action</b> tag value (timeshifting in PVR).
+
+--------------------------------------------------------------------------------
+\section Slider_Control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUISliderControl.h b/xbmc/guilib/GUISliderControl.h
new file mode 100644
index 0000000..196adb2
--- /dev/null
+++ b/xbmc/guilib/GUISliderControl.h
@@ -0,0 +1,129 @@
+/*
+ * 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
+
+/*!
+\file GUISliderControl.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUITexture.h"
+
+#include <array>
+
+#define SLIDER_CONTROL_TYPE_INT 1
+#define SLIDER_CONTROL_TYPE_FLOAT 2
+#define SLIDER_CONTROL_TYPE_PERCENTAGE 3
+
+typedef struct
+{
+ const char *action;
+ const char *formatString;
+ int infoCode;
+ bool fireOnDrag;
+} SliderAction;
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUISliderControl :
+ public CGUIControl
+{
+public:
+ typedef enum {
+ RangeSelectorLower = 0,
+ RangeSelectorUpper = 1
+ } RangeSelector;
+
+ CGUISliderControl(int parentID, int controlID, float posX, float posY, float width, float height, const CTextureInfo& backGroundTexture, const CTextureInfo& mibTexture, const CTextureInfo& nibTextureFocus, int iType, ORIENTATION orientation);
+ ~CGUISliderControl() override = default;
+ CGUISliderControl* Clone() const override { return new CGUISliderControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnAction(const CAction &action) override;
+ virtual bool IsActive() const { return true; }
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ virtual void SetRange(int iStart, int iEnd);
+ virtual void SetFloatRange(float fStart, float fEnd);
+ bool OnMessage(CGUIMessage& message) override;
+ bool ProcessSelector(CGUITexture* nib,
+ unsigned int currentTime,
+ float fScale,
+ RangeSelector selector);
+ void SetRangeSelection(bool rangeSelection);
+ bool GetRangeSelection() const { return m_rangeSelection; }
+ void SetRangeSelector(RangeSelector selector);
+ void SwitchRangeSelector();
+ void SetInfo(int iInfo);
+ void SetPercentage(float iPercent, RangeSelector selector = RangeSelectorLower, bool updateCurrent = false);
+ float GetPercentage(RangeSelector selector = RangeSelectorLower) const;
+ void SetIntValue(int iValue, RangeSelector selector = RangeSelectorLower, bool updateCurrent = false);
+ int GetIntValue(RangeSelector selector = RangeSelectorLower) const;
+ void SetFloatValue(float fValue, RangeSelector selector = RangeSelectorLower, bool updateCurrent = false);
+ float GetFloatValue(RangeSelector selector = RangeSelectorLower) const;
+ void SetIntInterval(int iInterval);
+ void SetFloatInterval(float fInterval);
+ void SetType(int iType) { m_iType = iType; }
+ int GetType() const { return m_iType; }
+ std::string GetDescription() const override;
+ void SetTextValue(const std::string& textValue) { m_textValue = textValue; }
+ void SetAction(const std::string &action);
+
+protected:
+ CGUISliderControl(const CGUISliderControl& control);
+
+ bool HitTest(const CPoint &point) const override;
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ bool UpdateColors(const CGUIListItem* item) override;
+ virtual void Move(int iNumSteps);
+ virtual void SetFromPosition(const CPoint &point, bool guessSelector = false);
+ /*! \brief Get the current position of the slider as a proportion
+ \return slider position in the range [0,1]
+ */
+ float GetProportion(RangeSelector selector = RangeSelectorLower) const;
+
+ /*! \brief Send a click message (and/or action) to the app in response to a slider move
+ */
+ void SendClick();
+
+ std::unique_ptr<CGUITexture> m_guiBackground;
+ std::unique_ptr<CGUITexture> m_guiSelectorLower;
+ std::unique_ptr<CGUITexture> m_guiSelectorUpper;
+ std::unique_ptr<CGUITexture> m_guiSelectorLowerFocus;
+ std::unique_ptr<CGUITexture> m_guiSelectorUpperFocus;
+ int m_iType;
+
+ bool m_rangeSelection;
+ RangeSelector m_currentSelector;
+
+ std::array<float, 2> m_percentValues;
+
+ std::array<int, 2> m_intValues;
+ int m_iStart;
+ int m_iInterval;
+ int m_iEnd;
+
+ std::array<float, 2> m_floatValues;
+ float m_fStart;
+ float m_fInterval;
+ float m_fEnd;
+
+ int m_iInfoCode;
+ std::string m_textValue; ///< Allows overriding of the text value to be displayed (parent must update when the slider updates)
+ const SliderAction *m_action; ///< Allows the skin to configure the action of a click on the slider \sa SendClick
+ bool m_dragging; ///< Whether we're in a (mouse/touch) drag operation or not - some actions are sent only on release.
+ ORIENTATION m_orientation;
+};
+
diff --git a/xbmc/guilib/GUISpinControl.cpp b/xbmc/guilib/GUISpinControl.cpp
new file mode 100644
index 0000000..f0a0f39
--- /dev/null
+++ b/xbmc/guilib/GUISpinControl.cpp
@@ -0,0 +1,1078 @@
+/*
+ * 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 "GUISpinControl.h"
+
+#include "GUIMessage.h"
+#include "input/Key.h"
+#include "utils/StringUtils.h"
+
+#include <stdio.h>
+
+#define SPIN_BUTTON_DOWN 1
+#define SPIN_BUTTON_UP 2
+
+namespace
+{
+// Additional space between text and spin buttons
+constexpr float TEXT_SPACE = 5.0f;
+} // unnamed namespace
+
+CGUISpinControl::CGUISpinControl(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ const CTextureInfo& textureUp,
+ const CTextureInfo& textureDown,
+ const CTextureInfo& textureUpFocus,
+ const CTextureInfo& textureDownFocus,
+ const CTextureInfo& textureUpDisabled,
+ const CTextureInfo& textureDownDisabled,
+ const CLabelInfo& labelInfo,
+ int iType)
+ : CGUIControl(parentID, controlID, posX, posY, width, height),
+ m_imgspinUp(CGUITexture::CreateTexture(posX, posY, width, height, textureUp)),
+ m_imgspinDown(CGUITexture::CreateTexture(posX, posY, width, height, textureDown)),
+ m_imgspinUpFocus(CGUITexture::CreateTexture(posX, posY, width, height, textureUpFocus)),
+ m_imgspinDownFocus(CGUITexture::CreateTexture(posX, posY, width, height, textureDownFocus)),
+ m_imgspinUpDisabled(CGUITexture::CreateTexture(posX, posY, width, height, textureUpDisabled)),
+ m_imgspinDownDisabled(
+ CGUITexture::CreateTexture(posX, posY, width, height, textureDownDisabled)),
+ m_label(posX, posY, width, height, labelInfo)
+{
+ m_bReverse = false;
+ m_iStart = 0;
+ m_iEnd = 100;
+ m_fStart = 0.0f;
+ m_fEnd = 1.0f;
+ m_fInterval = 0.1f;
+ m_iValue = 0;
+ m_fValue = 0.0;
+ m_iType = iType;
+ m_iSelect = SPIN_BUTTON_DOWN;
+ m_bShowRange = false;
+ m_iTypedPos = 0;
+ strcpy(m_szTyped, "");
+ ControlType = GUICONTROL_SPIN;
+ m_currentItem = 0;
+ m_numItems = 10;
+ m_itemsPerPage = 10;
+ m_showOnePage = true;
+}
+
+CGUISpinControl::CGUISpinControl(const CGUISpinControl& control)
+ : CGUIControl(control),
+ m_iStart(control.m_iStart),
+ m_iEnd(control.m_iEnd),
+ m_fStart(control.m_fStart),
+ m_fEnd(control.m_fEnd),
+ m_iValue(control.m_iValue),
+ m_fValue(control.m_fValue),
+ m_iType(control.m_iType),
+ m_iSelect(control.m_iSelect),
+ m_bReverse(control.m_bReverse),
+ m_fInterval(control.m_fInterval),
+ m_vecLabels(control.m_vecLabels),
+ m_vecValues(control.m_vecValues),
+ m_vecStrValues(control.m_vecStrValues),
+ m_imgspinUp(control.m_imgspinUp->Clone()),
+ m_imgspinDown(control.m_imgspinDown->Clone()),
+ m_imgspinUpFocus(control.m_imgspinUpFocus->Clone()),
+ m_imgspinDownFocus(control.m_imgspinDownFocus->Clone()),
+ m_imgspinUpDisabled(control.m_imgspinUpDisabled->Clone()),
+ m_imgspinDownDisabled(control.m_imgspinDownDisabled->Clone()),
+ m_label(control.m_label),
+ m_bShowRange(control.m_bShowRange),
+ m_iTypedPos(control.m_iTypedPos),
+ m_currentItem(control.m_currentItem),
+ m_itemsPerPage(control.m_itemsPerPage),
+ m_numItems(control.m_numItems),
+ m_showOnePage(control.m_showOnePage)
+{
+ std::strcpy(m_szTyped, control.m_szTyped);
+}
+
+bool CGUISpinControl::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case REMOTE_0:
+ case REMOTE_1:
+ case REMOTE_2:
+ case REMOTE_3:
+ case REMOTE_4:
+ case REMOTE_5:
+ case REMOTE_6:
+ case REMOTE_7:
+ case REMOTE_8:
+ case REMOTE_9:
+ {
+ if (strlen(m_szTyped) >= 3)
+ {
+ m_iTypedPos = 0;
+ strcpy(m_szTyped, "");
+ }
+ int iNumber = action.GetID() - REMOTE_0;
+
+ m_szTyped[m_iTypedPos] = iNumber + '0';
+ m_iTypedPos++;
+ m_szTyped[m_iTypedPos] = 0;
+ int iValue;
+ sscanf(m_szTyped, "%i", &iValue);
+ switch (m_iType)
+ {
+ case SPIN_CONTROL_TYPE_INT:
+ {
+ if (iValue < m_iStart || iValue > m_iEnd)
+ {
+ m_iTypedPos = 0;
+ m_szTyped[m_iTypedPos] = iNumber + '0';
+ m_iTypedPos++;
+ m_szTyped[m_iTypedPos] = 0;
+ sscanf(m_szTyped, "%i", &iValue);
+ if (iValue < m_iStart || iValue > m_iEnd)
+ {
+ m_iTypedPos = 0;
+ strcpy(m_szTyped, "");
+ return true;
+ }
+ }
+ m_iValue = iValue;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ }
+ break;
+
+ case SPIN_CONTROL_TYPE_TEXT:
+ {
+ if (iValue < 0 || iValue >= (int)m_vecLabels.size())
+ {
+ m_iTypedPos = 0;
+ m_szTyped[m_iTypedPos] = iNumber + '0';
+ m_iTypedPos++;
+ m_szTyped[m_iTypedPos] = 0;
+ sscanf(m_szTyped, "%i", &iValue);
+ if (iValue < 0 || iValue >= (int)m_vecLabels.size())
+ {
+ m_iTypedPos = 0;
+ strcpy(m_szTyped, "");
+ return true;
+ }
+ }
+ m_iValue = iValue;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ }
+ break;
+
+ }
+ return true;
+ }
+ break;
+ case ACTION_PAGE_UP:
+ if (!m_bReverse)
+ PageDown();
+ else
+ PageUp();
+ return true;
+ break;
+ case ACTION_PAGE_DOWN:
+ if (!m_bReverse)
+ PageUp();
+ else
+ PageDown();
+ return true;
+ break;
+ case ACTION_SELECT_ITEM:
+ if (m_iSelect == SPIN_BUTTON_UP)
+ {
+ MoveUp();
+ return true;
+ }
+ if (m_iSelect == SPIN_BUTTON_DOWN)
+ {
+ MoveDown();
+ return true;
+ }
+ break;
+ }
+/* static float m_fSmoothScrollOffset = 0.0f;
+ if (action.GetID() == ACTION_SCROLL_UP)
+ {
+ m_fSmoothScrollOffset += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+ while (m_fSmoothScrollOffset > 0.4)
+ {
+ handled = true;
+ m_fSmoothScrollOffset -= 0.4f;
+ MoveDown();
+ }
+ return handled;
+ }*/
+ return CGUIControl::OnAction(action);
+}
+
+void CGUISpinControl::OnLeft()
+{
+ if (m_iSelect == SPIN_BUTTON_UP)
+ {
+ // select the down button
+ m_iSelect = SPIN_BUTTON_DOWN;
+ MarkDirtyRegion();
+ }
+ else
+ { // base class
+ CGUIControl::OnLeft();
+ }
+}
+
+void CGUISpinControl::OnRight()
+{
+ if (m_iSelect == SPIN_BUTTON_DOWN)
+ {
+ // select the up button
+ m_iSelect = SPIN_BUTTON_UP;
+ MarkDirtyRegion();
+ }
+ else
+ { // base class
+ CGUIControl::OnRight();
+ }
+}
+
+void CGUISpinControl::Clear()
+{
+ m_vecLabels.clear();
+ m_vecValues.clear();
+ m_vecStrValues.clear();
+ SetValue(0);
+}
+
+bool CGUISpinControl::OnMessage(CGUIMessage& message)
+{
+ if (CGUIControl::OnMessage(message) )
+ return true;
+ if (message.GetControlId() == GetID() )
+ {
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_ITEM_SELECT:
+ if (SPIN_CONTROL_TYPE_PAGE == m_iType)
+ {
+ m_currentItem = message.GetParam1();
+ return true;
+ }
+ SetValue( message.GetParam1());
+ if (message.GetParam2() == SPIN_BUTTON_DOWN || message.GetParam2() == SPIN_BUTTON_UP)
+ m_iSelect = message.GetParam2();
+ return true;
+ break;
+
+ case GUI_MSG_LABEL_RESET:
+ if (SPIN_CONTROL_TYPE_PAGE == m_iType)
+ {
+ m_itemsPerPage = message.GetParam1();
+ m_numItems = message.GetParam2();
+ return true;
+ }
+ {
+ Clear();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_SHOWRANGE:
+ if (message.GetParam1() )
+ m_bShowRange = true;
+ else
+ m_bShowRange = false;
+ break;
+
+ case GUI_MSG_SET_LABELS:
+ if (message.GetPointer())
+ {
+ auto labels =
+ static_cast<const std::vector<std::pair<std::string, int>>*>(message.GetPointer());
+ Clear();
+ for (const auto& i : *labels)
+ AddLabel(i.first, i.second);
+ SetValue( message.GetParam1());
+ }
+ break;
+
+ case GUI_MSG_LABEL_ADD:
+ {
+ AddLabel(message.GetLabel(), message.GetParam1());
+ return true;
+ }
+ break;
+
+ case GUI_MSG_ITEM_SELECTED:
+ {
+ message.SetParam1( GetValue() );
+ message.SetParam2(m_iSelect);
+
+ if (m_iType == SPIN_CONTROL_TYPE_TEXT)
+ {
+ if ( m_iValue >= 0 && m_iValue < (int)m_vecLabels.size() )
+ message.SetLabel( m_vecLabels[m_iValue]);
+ }
+ return true;
+ }
+
+ case GUI_MSG_PAGE_UP:
+ if (CanMoveUp())
+ MoveUp();
+ return true;
+
+ case GUI_MSG_PAGE_DOWN:
+ if (CanMoveDown())
+ MoveDown();
+ return true;
+
+ case GUI_MSG_MOVE_OFFSET:
+ {
+ int count = message.GetParam1();
+ while (count < 0)
+ {
+ MoveUp();
+ count++;
+ }
+ while (count > 0)
+ {
+ MoveDown();
+ count--;
+ }
+ return true;
+ }
+
+ }
+ }
+ return false;
+}
+
+void CGUISpinControl::AllocResources()
+{
+ CGUIControl::AllocResources();
+ m_imgspinUp->AllocResources();
+ m_imgspinUpFocus->AllocResources();
+ m_imgspinDown->AllocResources();
+ m_imgspinDownFocus->AllocResources();
+ m_imgspinUpDisabled->AllocResources();
+ m_imgspinDownDisabled->AllocResources();
+
+ m_imgspinDownFocus->SetPosition(m_posX, m_posY);
+ m_imgspinDown->SetPosition(m_posX, m_posY);
+ m_imgspinDownDisabled->SetPosition(m_posX, m_posY);
+ m_imgspinUp->SetPosition(m_posX + m_imgspinDown->GetWidth(), m_posY);
+ m_imgspinUpFocus->SetPosition(m_posX + m_imgspinDownFocus->GetWidth(), m_posY);
+ m_imgspinUpDisabled->SetPosition(m_posX + m_imgspinDownDisabled->GetWidth(), m_posY);
+}
+
+void CGUISpinControl::FreeResources(bool immediately)
+{
+ CGUIControl::FreeResources(immediately);
+ m_imgspinUp->FreeResources(immediately);
+ m_imgspinUpFocus->FreeResources(immediately);
+ m_imgspinDown->FreeResources(immediately);
+ m_imgspinDownFocus->FreeResources(immediately);
+ m_imgspinUpDisabled->FreeResources(immediately);
+ m_imgspinDownDisabled->FreeResources(immediately);
+ m_iTypedPos = 0;
+ strcpy(m_szTyped, "");
+}
+
+void CGUISpinControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIControl::DynamicResourceAlloc(bOnOff);
+ m_imgspinUp->DynamicResourceAlloc(bOnOff);
+ m_imgspinUpFocus->DynamicResourceAlloc(bOnOff);
+ m_imgspinDown->DynamicResourceAlloc(bOnOff);
+ m_imgspinDownFocus->DynamicResourceAlloc(bOnOff);
+ m_imgspinUpDisabled->DynamicResourceAlloc(bOnOff);
+ m_imgspinDownDisabled->DynamicResourceAlloc(bOnOff);
+}
+
+void CGUISpinControl::SetInvalid()
+{
+ CGUIControl::SetInvalid();
+ m_label.SetInvalid();
+ m_imgspinUp->SetInvalid();
+ m_imgspinUpFocus->SetInvalid();
+ m_imgspinDown->SetInvalid();
+ m_imgspinDownFocus->SetInvalid();
+ m_imgspinUpDisabled->SetInvalid();
+ m_imgspinDownDisabled->SetInvalid();
+}
+
+void CGUISpinControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ bool changed = false;
+
+ if (!HasFocus())
+ {
+ m_iTypedPos = 0;
+ strcpy(m_szTyped, "");
+ }
+
+ std::string text;
+
+ if (m_iType == SPIN_CONTROL_TYPE_INT)
+ {
+ if (m_bShowRange)
+ {
+ text = StringUtils::Format("{}/{}", m_iValue, m_iEnd);
+ }
+ else
+ {
+ text = std::to_string(m_iValue);
+ }
+ }
+ else if (m_iType == SPIN_CONTROL_TYPE_PAGE)
+ {
+ // work out number of pages and current page
+ int numPages = (m_numItems + m_itemsPerPage - 1) / m_itemsPerPage;
+ int currentPage = m_currentItem / m_itemsPerPage + 1;
+ if (m_currentItem >= m_numItems - m_itemsPerPage)
+ currentPage = numPages;
+ text = StringUtils::Format("{}/{}", currentPage, numPages);
+ }
+ else if (m_iType == SPIN_CONTROL_TYPE_FLOAT)
+ {
+ if (m_bShowRange)
+ {
+ text = StringUtils::Format("{:02.2f}/{:02.2f}", m_fValue, m_fEnd);
+ }
+ else
+ {
+ text = StringUtils::Format("{:02.2f}", m_fValue);
+ }
+ }
+ else
+ {
+ if (m_iValue >= 0 && m_iValue < (int)m_vecLabels.size() )
+ {
+ if (m_bShowRange)
+ {
+ text = StringUtils::Format("({}/{}) {}", m_iValue + 1, (int)m_vecLabels.size(),
+ m_vecLabels[m_iValue]);
+ }
+ else
+ {
+ text = m_vecLabels[m_iValue];
+ }
+ }
+ else
+ text = StringUtils::Format("?{}?", m_iValue);
+ }
+
+ changed |= m_label.SetText(text);
+
+ float textWidth = m_label.GetTextWidth() + 2 * m_label.GetLabelInfo().offsetX;
+ // Position the arrows
+ bool arrowsOnRight(0 != (m_label.GetLabelInfo().align & (XBFONT_RIGHT | XBFONT_CENTER_X)));
+ if (!arrowsOnRight)
+ {
+ changed |= m_imgspinDownFocus->SetPosition(m_posX + textWidth + TEXT_SPACE, m_posY);
+ changed |= m_imgspinDown->SetPosition(m_posX + textWidth + TEXT_SPACE, m_posY);
+ changed |= m_imgspinDownDisabled->SetPosition(m_posX + textWidth + TEXT_SPACE, m_posY);
+ changed |= m_imgspinUpFocus->SetPosition(
+ m_posX + textWidth + TEXT_SPACE + m_imgspinDown->GetWidth(), m_posY);
+ changed |= m_imgspinUp->SetPosition(m_posX + textWidth + TEXT_SPACE + m_imgspinDown->GetWidth(),
+ m_posY);
+ changed |= m_imgspinUpDisabled->SetPosition(
+ m_posX + textWidth + TEXT_SPACE + m_imgspinDownDisabled->GetWidth(), m_posY);
+ }
+
+ changed |= m_imgspinDownFocus->Process(currentTime);
+ changed |= m_imgspinDown->Process(currentTime);
+ changed |= m_imgspinUp->Process(currentTime);
+ changed |= m_imgspinUpFocus->Process(currentTime);
+ changed |= m_imgspinUpDisabled->Process(currentTime);
+ changed |= m_imgspinDownDisabled->Process(currentTime);
+ changed |= m_label.Process(currentTime);
+
+ if (changed)
+ MarkDirtyRegion();
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUISpinControl::Render()
+{
+ if (m_label.GetLabelInfo().font)
+ {
+ float textWidth = m_label.GetTextWidth() + 2 * m_label.GetLabelInfo().offsetX;
+ // Position the arrows
+ bool arrowsOnRight(0 != (m_label.GetLabelInfo().align & (XBFONT_RIGHT | XBFONT_CENTER_X)));
+
+ if (arrowsOnRight)
+ RenderText(m_posX - TEXT_SPACE - textWidth, m_posY, textWidth, m_height);
+ else
+ RenderText(m_posX + m_imgspinDown->GetWidth() + m_imgspinUp->GetWidth() + TEXT_SPACE, m_posY,
+ textWidth, m_height);
+
+ // set our hit rectangle for MouseOver events
+ m_hitRect = m_label.GetRenderRect();
+ }
+
+ if (HasFocus())
+ {
+ if (m_iSelect == SPIN_BUTTON_UP)
+ m_imgspinUpFocus->Render();
+ else
+ m_imgspinUp->Render();
+
+ if (m_iSelect == SPIN_BUTTON_DOWN)
+ m_imgspinDownFocus->Render();
+ else
+ m_imgspinDown->Render();
+ }
+ else if (!HasFocus() && !IsDisabled())
+ {
+ m_imgspinUp->Render();
+ m_imgspinDown->Render();
+ }
+ else
+ {
+ m_imgspinUpDisabled->Render();
+ m_imgspinDownDisabled->Render();
+ }
+
+ CGUIControl::Render();
+}
+
+void CGUISpinControl::RenderText(float posX, float posY, float width, float height)
+{
+ m_label.SetMaxRect(posX, posY, width, height);
+ m_label.SetColor(GetTextColor());
+ m_label.Render();
+}
+
+CGUILabel::COLOR CGUISpinControl::GetTextColor() const
+{
+ if (IsDisabled())
+ return CGUILabel::COLOR_DISABLED;
+ else if (HasFocus())
+ return CGUILabel::COLOR_FOCUSED;
+ return CGUILabel::COLOR_TEXT;
+}
+
+void CGUISpinControl::SetRange(int iStart, int iEnd)
+{
+ m_iStart = iStart;
+ m_iEnd = iEnd;
+}
+
+void CGUISpinControl::SetFloatRange(float fStart, float fEnd)
+{
+ m_fStart = fStart;
+ m_fEnd = fEnd;
+}
+
+void CGUISpinControl::SetValueFromLabel(const std::string &label)
+{
+ if (m_iType == SPIN_CONTROL_TYPE_TEXT)
+ {
+ m_iValue = 0;
+ for (unsigned int i = 0; i < m_vecLabels.size(); i++)
+ if (label == m_vecLabels[i])
+ m_iValue = i;
+ }
+ else
+ m_iValue = atoi(label.c_str());
+
+ MarkDirtyRegion();
+ SetInvalid();
+}
+
+void CGUISpinControl::SetValue(int iValue)
+{
+ if (m_iType == SPIN_CONTROL_TYPE_TEXT)
+ {
+ m_iValue = 0;
+ for (unsigned int i = 0; i < m_vecValues.size(); i++)
+ if (iValue == m_vecValues[i])
+ m_iValue = i;
+ }
+ else
+ m_iValue = iValue;
+
+ MarkDirtyRegion();
+ SetInvalid();
+}
+
+void CGUISpinControl::SetFloatValue(float fValue)
+{
+ m_fValue = fValue;
+}
+
+void CGUISpinControl::SetStringValue(const std::string& strValue)
+{
+ if (m_iType == SPIN_CONTROL_TYPE_TEXT)
+ {
+ m_iValue = 0;
+ for (unsigned int i = 0; i < m_vecStrValues.size(); i++)
+ if (strValue == m_vecStrValues[i])
+ m_iValue = i;
+ }
+
+ SetInvalid();
+}
+
+int CGUISpinControl::GetValue() const
+{
+ if (m_iType == SPIN_CONTROL_TYPE_TEXT)
+ {
+ if (m_iValue >= 0 && m_iValue < (int)m_vecValues.size())
+ return m_vecValues[m_iValue];
+ }
+ return m_iValue;
+}
+
+float CGUISpinControl::GetFloatValue() const
+{
+ return m_fValue;
+}
+
+std::string CGUISpinControl::GetStringValue() const
+{
+ if (m_iType == SPIN_CONTROL_TYPE_TEXT && m_iValue >= 0 && m_iValue < (int)m_vecLabels.size())
+ {
+ if (m_iValue < (int)m_vecStrValues.size())
+ return m_vecStrValues[m_iValue];
+
+ return m_vecLabels[m_iValue];
+ }
+ return "";
+}
+
+void CGUISpinControl::AddLabel(const std::string& strLabel, int iValue)
+{
+ m_vecLabels.push_back(strLabel);
+ m_vecValues.push_back(iValue);
+}
+
+void CGUISpinControl::AddLabel(const std::string& strLabel, const std::string& strValue)
+{
+ m_vecLabels.push_back(strLabel);
+ m_vecStrValues.push_back(strValue);
+}
+
+const std::string CGUISpinControl::GetLabel() const
+{
+ if (m_iValue >= 0 && m_iValue < (int)m_vecLabels.size())
+ {
+ return m_vecLabels[m_iValue];
+ }
+ return "";
+}
+
+void CGUISpinControl::SetPosition(float posX, float posY)
+{
+ CGUIControl::SetPosition(posX, posY);
+
+ m_imgspinDownFocus->SetPosition(posX, posY);
+ m_imgspinDown->SetPosition(posX, posY);
+ m_imgspinDownDisabled->SetPosition(posX, posY);
+
+ m_imgspinUp->SetPosition(m_posX + m_imgspinDown->GetWidth(), m_posY);
+ m_imgspinUpFocus->SetPosition(m_posX + m_imgspinDownFocus->GetWidth(), m_posY);
+ m_imgspinUpDisabled->SetPosition(m_posX + m_imgspinDownDisabled->GetWidth(), m_posY);
+}
+
+float CGUISpinControl::GetWidth() const
+{
+ return m_imgspinDown->GetWidth() * 2;
+}
+
+bool CGUISpinControl::CanMoveUp(bool bTestReverse)
+{
+ // test for reverse...
+ if (bTestReverse && m_bReverse) return CanMoveDown(false);
+
+ switch (m_iType)
+ {
+ case SPIN_CONTROL_TYPE_PAGE:
+ return m_currentItem > 0;
+ case SPIN_CONTROL_TYPE_INT:
+ {
+ if (m_iValue - 1 >= m_iStart)
+ return true;
+ return false;
+ }
+ break;
+
+ case SPIN_CONTROL_TYPE_FLOAT:
+ {
+ if (m_fValue - m_fInterval >= m_fStart)
+ return true;
+ return false;
+ }
+ break;
+
+ case SPIN_CONTROL_TYPE_TEXT:
+ {
+ if (m_iValue - 1 >= 0)
+ return true;
+ return false;
+ }
+ break;
+ }
+ return false;
+}
+
+bool CGUISpinControl::CanMoveDown(bool bTestReverse)
+{
+ // test for reverse...
+ if (bTestReverse && m_bReverse) return CanMoveUp(false);
+ switch (m_iType)
+ {
+ case SPIN_CONTROL_TYPE_PAGE:
+ return m_currentItem < m_numItems;
+ case SPIN_CONTROL_TYPE_INT:
+ {
+ if (m_iValue + 1 <= m_iEnd)
+ return true;
+ return false;
+ }
+ break;
+
+ case SPIN_CONTROL_TYPE_FLOAT:
+ {
+ if (m_fValue + m_fInterval <= m_fEnd)
+ return true;
+ return false;
+ }
+ break;
+
+ case SPIN_CONTROL_TYPE_TEXT:
+ {
+ if (m_iValue + 1 < (int)m_vecLabels.size())
+ return true;
+ return false;
+ }
+ break;
+ }
+ return false;
+}
+
+void CGUISpinControl::PageUp()
+{
+ switch (m_iType)
+ {
+ case SPIN_CONTROL_TYPE_INT:
+ {
+ if (m_iValue - 10 >= m_iStart)
+ m_iValue -= 10;
+ else
+ m_iValue = m_iStart;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return ;
+ }
+ break;
+ case SPIN_CONTROL_TYPE_PAGE:
+ ChangePage(-10);
+ break;
+ case SPIN_CONTROL_TYPE_TEXT:
+ {
+ if (m_iValue - 10 >= 0)
+ m_iValue -= 10;
+ else
+ m_iValue = 0;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return ;
+ }
+ break;
+ }
+
+}
+
+void CGUISpinControl::PageDown()
+{
+ switch (m_iType)
+ {
+ case SPIN_CONTROL_TYPE_INT:
+ {
+ if (m_iValue + 10 <= m_iEnd)
+ m_iValue += 10;
+ else
+ m_iValue = m_iEnd;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return ;
+ }
+ break;
+ case SPIN_CONTROL_TYPE_PAGE:
+ ChangePage(10);
+ break;
+ case SPIN_CONTROL_TYPE_TEXT:
+ {
+ if (m_iValue + 10 < (int)m_vecLabels.size() )
+ m_iValue += 10;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ }
+ break;
+ }
+}
+
+void CGUISpinControl::MoveUp(bool bTestReverse)
+{
+ if (bTestReverse && m_bReverse)
+ { // actually should move down.
+ MoveDown(false);
+ return ;
+ }
+ switch (m_iType)
+ {
+ case SPIN_CONTROL_TYPE_INT:
+ {
+ if (m_iValue - 1 >= m_iStart)
+ m_iValue--;
+ else if (m_iValue == m_iStart)
+ m_iValue = m_iEnd;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return ;
+ }
+ break;
+
+ case SPIN_CONTROL_TYPE_PAGE:
+ ChangePage(-1);
+ break;
+
+ case SPIN_CONTROL_TYPE_FLOAT:
+ {
+ if (m_fValue - m_fInterval >= m_fStart)
+ m_fValue -= m_fInterval;
+ else
+ m_fValue = m_fEnd;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return ;
+ }
+ break;
+
+ case SPIN_CONTROL_TYPE_TEXT:
+ {
+ if (m_iValue - 1 >= 0)
+ m_iValue--;
+ else if (m_iValue == 0)
+ m_iValue = (int)m_vecLabels.size() - 1;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return ;
+ }
+ break;
+ }
+}
+
+void CGUISpinControl::MoveDown(bool bTestReverse)
+{
+ if (bTestReverse && m_bReverse)
+ { // actually should move up.
+ MoveUp(false);
+ return ;
+ }
+ switch (m_iType)
+ {
+ case SPIN_CONTROL_TYPE_INT:
+ {
+ if (m_iValue + 1 <= m_iEnd)
+ m_iValue++;
+ else if (m_iValue == m_iEnd)
+ m_iValue = m_iStart;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return ;
+ }
+ break;
+
+ case SPIN_CONTROL_TYPE_PAGE:
+ ChangePage(1);
+ break;
+
+ case SPIN_CONTROL_TYPE_FLOAT:
+ {
+ if (m_fValue + m_fInterval <= m_fEnd)
+ m_fValue += m_fInterval;
+ else
+ m_fValue = m_fStart;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return ;
+ }
+ break;
+
+ case SPIN_CONTROL_TYPE_TEXT:
+ {
+ if (m_iValue + 1 < (int)m_vecLabels.size() )
+ m_iValue++;
+ else if (m_iValue == (int)m_vecLabels.size() - 1)
+ m_iValue = 0;
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return ;
+ }
+ break;
+ }
+}
+void CGUISpinControl::SetReverse(bool bReverse)
+{
+ m_bReverse = bReverse;
+}
+
+void CGUISpinControl::SetFloatInterval(float fInterval)
+{
+ m_fInterval = fInterval;
+}
+
+void CGUISpinControl::SetShowRange(bool bOnoff)
+{
+ m_bShowRange = bOnoff;
+}
+
+int CGUISpinControl::GetMinimum() const
+{
+ switch (m_iType)
+ {
+ case SPIN_CONTROL_TYPE_PAGE:
+ return 0;
+ case SPIN_CONTROL_TYPE_INT:
+ return m_iStart;
+ break;
+
+ case SPIN_CONTROL_TYPE_TEXT:
+ return 1;
+ break;
+
+ case SPIN_CONTROL_TYPE_FLOAT:
+ return (int)(m_fStart*10.0f);
+ break;
+ }
+ return 0;
+}
+
+int CGUISpinControl::GetMaximum() const
+{
+ switch (m_iType)
+ {
+ case SPIN_CONTROL_TYPE_PAGE:
+ return m_numItems;
+ case SPIN_CONTROL_TYPE_INT:
+ return m_iEnd;
+ break;
+
+ case SPIN_CONTROL_TYPE_TEXT:
+ return (int)m_vecLabels.size();
+ break;
+
+ case SPIN_CONTROL_TYPE_FLOAT:
+ return (int)(m_fEnd*10.0f);
+ break;
+ }
+ return 100;
+}
+
+bool CGUISpinControl::HitTest(const CPoint &point) const
+{
+ if (m_imgspinUpFocus->HitTest(point) || m_imgspinDownFocus->HitTest(point))
+ return true;
+ return CGUIControl::HitTest(point);
+}
+
+bool CGUISpinControl::OnMouseOver(const CPoint &point)
+{
+ int select = m_iSelect;
+ if (m_imgspinDownFocus->HitTest(point))
+ m_iSelect = SPIN_BUTTON_DOWN;
+ else
+ m_iSelect = SPIN_BUTTON_UP;
+
+ if (select != m_iSelect)
+ MarkDirtyRegion();
+
+ return CGUIControl::OnMouseOver(point);
+}
+
+EVENT_RESULT CGUISpinControl::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_LEFT_CLICK)
+ {
+ if (m_imgspinUpFocus->HitTest(point))
+ MoveUp();
+ else if (m_imgspinDownFocus->HitTest(point))
+ MoveDown();
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_MOUSE_WHEEL_UP)
+ {
+ if (m_imgspinUpFocus->HitTest(point) || m_imgspinDownFocus->HitTest(point))
+ {
+ MoveUp();
+ return EVENT_RESULT_HANDLED;
+ }
+ }
+ else if (event.m_id == ACTION_MOUSE_WHEEL_DOWN)
+ {
+ if (m_imgspinUpFocus->HitTest(point) || m_imgspinDownFocus->HitTest(point))
+ {
+ MoveDown();
+ return EVENT_RESULT_HANDLED;
+ }
+ }
+ return EVENT_RESULT_UNHANDLED;
+}
+
+std::string CGUISpinControl::GetDescription() const
+{
+ return StringUtils::Format("{}/{}", 1 + GetValue(), GetMaximum());
+}
+
+bool CGUISpinControl::IsFocusedOnUp() const
+{
+ return (m_iSelect == SPIN_BUTTON_UP);
+}
+
+void CGUISpinControl::ChangePage(int amount)
+{
+ m_currentItem += amount * m_itemsPerPage;
+ if (m_currentItem > m_numItems - m_itemsPerPage)
+ m_currentItem = m_numItems - m_itemsPerPage;
+ if (m_currentItem < 0)
+ m_currentItem = 0;
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, GetParentID(), GetID(), GUI_MSG_PAGE_CHANGE, m_currentItem);
+ SendWindowMessage(message);
+}
+
+bool CGUISpinControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_label.UpdateColors();
+ changed |= m_imgspinDownFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgspinDown->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgspinUp->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgspinUpFocus->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgspinUpDisabled->SetDiffuseColor(m_diffuseColor);
+ changed |= m_imgspinDownDisabled->SetDiffuseColor(m_diffuseColor);
+
+ return changed;
+}
+
+bool CGUISpinControl::IsVisible() const
+{
+ // page controls can be optionally disabled if the number of pages is 1
+ if (m_iType == SPIN_CONTROL_TYPE_PAGE && m_numItems <= m_itemsPerPage && !m_showOnePage)
+ return false;
+ return CGUIControl::IsVisible();
+}
diff --git a/xbmc/guilib/GUISpinControl.dox b/xbmc/guilib/GUISpinControl.dox
new file mode 100644
index 0000000..0c42fcf
--- /dev/null
+++ b/xbmc/guilib/GUISpinControl.dox
@@ -0,0 +1,83 @@
+/*!
+
+\page Spin_Control Spin Control
+\brief **Used for cycling up/down controls.**
+
+\tableofcontents
+
+The spin control is used for when a list of options can be chosen (such as a
+page up/down control). You can choose the position, size, and look of the
+spin control.
+
+
+--------------------------------------------------------------------------------
+\section Spin_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="spincontrol" id="14">
+ <description>My first spin control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+ <colordiffuse>FFFFFFFF</colordiffuse>
+ <textureup colordiffuse="FFFFAAFF">myuptexture.png</textureup>
+ <textureupfocus colordiffuse="FFFFAAFF">myupfocustexture.png</textureupfocus>
+ <texturedown colordiffuse="FFFFAAFF">mydowntexture.png</texturedown>
+ <texturedownfocus colordiffuse="FFFFAAFF">mydownfocustexture.png</texturedownfocus>
+ <textureupdisabled colordiffuse="AAFFAAFF">mydowntexture.png</textureupdisabled>
+ <texturedowndisabled colordiffuse="AAFFAAFF">mydownfocustexture.png</texturedowndisabled>
+ <subtype>page</subtype>
+ <font>font12</font>
+ <textcolor>FFFFFFFF</textcolor>
+ <disabledcolor>80FFFFFF</disabledcolor>
+ <align></align>
+ <aligny></aligny>
+ <textoffsetx></textoffsetx>
+ <textoffsety></textoffsety>
+ <pulseonselect></pulseonselect>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Spin_Control_sect2 Available tags
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is lower case only. This is
+important, as xml tags are case-sensitive.
+
+| Tag | Description |
+|--------------------:|:--------------------------------------------------------------|
+| textureup | Specifies the image file which should be displayed for the up arrow when it doesn't have focus. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| textureupfocus | Specifies the image file which should be displayed for the up button when it has focus.
+| textureupdisabled | Specifies the image file which should be displayed for the up arrow when the button is disabled.
+| texturedown | Specifies the image file which should be displayed for the down button when it is not focused.
+| texturedownfocus | Specifies the image file which should be displayed for the down button when it has focus.
+| texturedowndisabled | Specifies the image file which should be displayed for the up arrow when the button is disabled.
+| font | Font used for the button label. From fonts.xml.
+| spincolor | The colour of the text used for this spin control. In **AARRGGBB** hex format. As of Helix, this doesn't actually get processed, use textcolor
+| textcolor | Color used for displaying the label. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| disabledcolor | Color used for the label if the control is disabled. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| shadowcolor | Specifies the color of the drop shadow on the text. In **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| subtype | Defines what type of information the spinner holds. Can be int, float, text or page. Defaults to text. Make sure you use page for a page control.
+| align | Label horizontal alignment on the control. Defaults to right, can also be left.
+| aligny | Label vertical alignment on the control. Defaults to top, can also be center.
+| textoffsetx | Amount to offset the label from the left (or right) edge of the button when using left or right alignment.
+| textoffsety | Amount to offset the label from the top edge of the button when using top alignment.
+| textwidth | Will truncate any text that's too long.
+
+
+--------------------------------------------------------------------------------
+\section Spin_Control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUISpinControl.h b/xbmc/guilib/GUISpinControl.h
new file mode 100644
index 0000000..f5aecd2
--- /dev/null
+++ b/xbmc/guilib/GUISpinControl.h
@@ -0,0 +1,133 @@
+/*
+ * 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
+
+/*!
+\file GUISpinControl.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUILabel.h"
+#include "GUITexture.h"
+
+#include <vector>
+
+#define SPIN_CONTROL_TYPE_INT 1
+#define SPIN_CONTROL_TYPE_FLOAT 2
+#define SPIN_CONTROL_TYPE_TEXT 3
+#define SPIN_CONTROL_TYPE_PAGE 4
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUISpinControl : public CGUIControl
+{
+public:
+ CGUISpinControl(int parentID, int controlID, float posX, float posY, float width, float height, const CTextureInfo& textureUp, const CTextureInfo& textureDown, const CTextureInfo& textureUpFocus, const CTextureInfo& textureDownFocus, const CTextureInfo& textureUpDisabled, const CTextureInfo& textureDownDisabled, const CLabelInfo& labelInfo, int iType);
+ ~CGUISpinControl() override = default;
+ CGUISpinControl* Clone() const override { return new CGUISpinControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnAction(const CAction &action) override;
+ void OnLeft() override;
+ void OnRight() override;
+ bool HitTest(const CPoint &point) const override;
+ bool OnMouseOver(const CPoint &point) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ void SetPosition(float posX, float posY) override;
+ float GetWidth() const override;
+ void SetRange(int iStart, int iEnd);
+ void SetFloatRange(float fStart, float fEnd);
+ void SetValue(int iValue);
+ void SetValueFromLabel(const std::string &label);
+ void SetFloatValue(float fValue);
+ void SetStringValue(const std::string& strValue);
+ int GetValue() const;
+ float GetFloatValue() const;
+ std::string GetStringValue() const;
+ void AddLabel(const std::string& strLabel, int iValue);
+ void AddLabel(const std::string& strLabel, const std::string& strValue);
+ const std::string GetLabel() const;
+ void SetReverse(bool bOnOff);
+ int GetMaximum() const;
+ int GetMinimum() const;
+ void SetSpinAlign(uint32_t align, float offsetX)
+ {
+ m_label.GetLabelInfo().align = align;
+ m_label.GetLabelInfo().offsetX = offsetX;
+ }
+ void SetType(int iType) { m_iType = iType; }
+ float GetSpinWidth() const { return m_imgspinUp->GetWidth(); }
+ float GetSpinHeight() const { return m_imgspinUp->GetHeight(); }
+ void SetFloatInterval(float fInterval);
+ void SetShowRange(bool bOnoff) ;
+ void SetShowOnePage(bool showOnePage) { m_showOnePage = showOnePage; }
+ void Clear();
+ std::string GetDescription() const override;
+ bool IsFocusedOnUp() const;
+
+ bool IsVisible() const override;
+
+protected:
+ CGUISpinControl(const CGUISpinControl& control);
+
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ bool UpdateColors(const CGUIListItem* item) override;
+ /*! \brief Render the spinner text
+ \param posX position of the left edge of the text
+ \param posY positing of the top edge of the text
+ \param width width of the text
+ \param height height of the text
+ */
+ virtual void RenderText(float posX, float posY, float width, float height);
+ CGUILabel::COLOR GetTextColor() const;
+ void PageUp();
+ void PageDown();
+ bool CanMoveDown(bool bTestReverse = true);
+ bool CanMoveUp(bool bTestReverse = true);
+ void MoveUp(bool bTestReverse = true);
+ void MoveDown(bool bTestReverse = true);
+ void ChangePage(int amount);
+ int m_iStart;
+ int m_iEnd;
+ float m_fStart;
+ float m_fEnd;
+ int m_iValue;
+ float m_fValue;
+ int m_iType;
+ int m_iSelect;
+ bool m_bReverse;
+ float m_fInterval;
+ std::vector<std::string> m_vecLabels;
+ std::vector<int> m_vecValues;
+ std::vector<std::string> m_vecStrValues;
+ std::unique_ptr<CGUITexture> m_imgspinUp;
+ std::unique_ptr<CGUITexture> m_imgspinDown;
+ std::unique_ptr<CGUITexture> m_imgspinUpFocus;
+ std::unique_ptr<CGUITexture> m_imgspinDownFocus;
+ std::unique_ptr<CGUITexture> m_imgspinUpDisabled;
+ std::unique_ptr<CGUITexture> m_imgspinDownDisabled;
+ CGUILabel m_label;
+ bool m_bShowRange;
+ char m_szTyped[10];
+ int m_iTypedPos;
+
+ int m_currentItem;
+ int m_itemsPerPage;
+ int m_numItems;
+ bool m_showOnePage;
+};
+
diff --git a/xbmc/guilib/GUISpinControlEx.cpp b/xbmc/guilib/GUISpinControlEx.cpp
new file mode 100644
index 0000000..709c2af
--- /dev/null
+++ b/xbmc/guilib/GUISpinControlEx.cpp
@@ -0,0 +1,161 @@
+/*
+ * 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 "GUISpinControlEx.h"
+
+#include "utils/StringUtils.h"
+
+CGUISpinControlEx::CGUISpinControlEx(int parentID, int controlID, float posX, float posY, float width, float height, float spinWidth, float spinHeight, const CLabelInfo& spinInfo, const CTextureInfo &textureFocus, const CTextureInfo &textureNoFocus, const CTextureInfo& textureUp, const CTextureInfo& textureDown, const CTextureInfo& textureUpFocus, const CTextureInfo& textureDownFocus, const CTextureInfo& textureUpDisabled, const CTextureInfo& textureDownDisabled, const CLabelInfo& labelInfo, int iType)
+ : CGUISpinControl(parentID, controlID, posX, posY, spinWidth, spinHeight, textureUp, textureDown, textureUpFocus, textureDownFocus, textureUpDisabled, textureDownDisabled, spinInfo, iType)
+ , m_buttonControl(parentID, controlID, posX, posY, width, height, textureFocus, textureNoFocus, labelInfo)
+{
+ ControlType = GUICONTROL_SPINEX;
+ m_spinPosX = 0;
+}
+
+CGUISpinControlEx::~CGUISpinControlEx(void) = default;
+
+void CGUISpinControlEx::AllocResources()
+{
+ // Correct alignment - we always align the spincontrol on the right
+ m_label.GetLabelInfo().align = (m_label.GetLabelInfo().align & XBFONT_CENTER_Y) | XBFONT_RIGHT;
+ CGUISpinControl::AllocResources();
+ m_buttonControl.AllocResources();
+ if (m_height == 0)
+ m_height = GetSpinHeight();
+}
+
+void CGUISpinControlEx::FreeResources(bool immediately)
+{
+ CGUISpinControl::FreeResources(immediately);
+ m_buttonControl.FreeResources(immediately);
+}
+
+void CGUISpinControlEx::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUISpinControl::DynamicResourceAlloc(bOnOff);
+ m_buttonControl.DynamicResourceAlloc(bOnOff);
+}
+
+
+void CGUISpinControlEx::SetInvalid()
+{
+ CGUISpinControl::SetInvalid();
+ m_buttonControl.SetInvalid();
+}
+
+void CGUISpinControlEx::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ // make sure the button has focus if it should have...
+ m_buttonControl.SetFocus(HasFocus());
+ m_buttonControl.SetPulseOnSelect(m_pulseOnSelect);
+ m_buttonControl.SetEnabled(m_enabled);
+ if (m_bInvalidated)
+ {
+ float spinPosX = m_buttonControl.GetXPosition() + m_buttonControl.GetWidth() - GetSpinWidth() * 2 - (m_spinPosX ? m_spinPosX : m_buttonControl.GetLabelInfo().offsetX);
+ float spinPosY = m_buttonControl.GetYPosition() + (m_buttonControl.GetHeight() - GetSpinHeight()) * 0.5f;
+ CGUISpinControl::SetPosition(spinPosX, spinPosY);
+ }
+ m_buttonControl.DoProcess(currentTime, dirtyregions);
+ CGUISpinControl::Process(currentTime, dirtyregions);
+}
+
+void CGUISpinControlEx::Render()
+{
+ CGUISpinControl::Render();
+}
+
+void CGUISpinControlEx::SetPosition(float posX, float posY)
+{
+ m_buttonControl.SetPosition(posX, posY);
+ CGUISpinControl::SetInvalid();
+}
+
+void CGUISpinControlEx::SetWidth(float width)
+{
+ m_buttonControl.SetWidth(width);
+ CGUISpinControl::SetInvalid();
+}
+
+void CGUISpinControlEx::SetHeight(float height)
+{
+ m_buttonControl.SetHeight(height);
+ CGUISpinControl::SetInvalid();
+}
+
+bool CGUISpinControlEx::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUISpinControl::UpdateColors(nullptr);
+ changed |= m_buttonControl.SetColorDiffuse(m_diffuseColor);
+ changed |= m_buttonControl.UpdateColors(nullptr);
+
+ return changed;
+}
+
+void CGUISpinControlEx::SetEnabled(bool bEnable)
+{
+ m_buttonControl.SetEnabled(bEnable);
+ CGUISpinControl::SetEnabled(bEnable);
+}
+
+const std::string CGUISpinControlEx::GetCurrentLabel() const
+{
+ return CGUISpinControl::GetLabel();
+}
+
+std::string CGUISpinControlEx::GetDescription() const
+{
+ return StringUtils::Format("{} ({})", m_buttonControl.GetDescription(), GetLabel());
+}
+
+void CGUISpinControlEx::SetItemInvalid(bool invalid)
+{
+ if (invalid)
+ {
+ m_label.GetLabelInfo().textColor = m_buttonControl.GetLabelInfo().disabledColor;
+ m_label.GetLabelInfo().focusedColor = m_buttonControl.GetLabelInfo().disabledColor;
+ }
+ else
+ {
+ m_label.GetLabelInfo().textColor = m_buttonControl.GetLabelInfo().textColor;
+ m_label.GetLabelInfo().focusedColor = m_buttonControl.GetLabelInfo().focusedColor;
+ }
+}
+
+void CGUISpinControlEx::SetSpinPosition(float spinPosX)
+{
+ m_spinPosX = spinPosX;
+ SetPosition(m_buttonControl.GetXPosition(), m_buttonControl.GetYPosition());
+}
+
+void CGUISpinControlEx::RenderText(float posX, float posY, float width, float height)
+{
+ const float freeSpaceWidth{m_buttonControl.GetWidth() - GetSpinWidth() * 2};
+
+ // Limit right label text width to max 50% of free space
+ // (will be slightly shifted due to offsetX padding)
+ const float rightTextMaxWidth{freeSpaceWidth * 0.5f};
+
+ float rightTextWidth{width};
+ if (rightTextWidth > rightTextMaxWidth)
+ {
+ rightTextWidth = rightTextMaxWidth - m_label.GetLabelInfo().offsetX;
+ }
+
+ m_label.SetScrolling(HasFocus());
+ // Replace posX by using our button position
+ posX = m_buttonControl.GetXPosition() + freeSpaceWidth - rightTextWidth -
+ m_label.GetLabelInfo().offsetX;
+
+ // Limit the max width for the left label to avoid text overlapping
+ m_buttonControl.SetMaxWidth(posX + m_label.GetLabelInfo().offsetX);
+ m_buttonControl.Render();
+
+ CGUISpinControl::RenderText(posX, m_buttonControl.GetYPosition(), rightTextWidth,
+ m_buttonControl.GetHeight());
+}
diff --git a/xbmc/guilib/GUISpinControlEx.dox b/xbmc/guilib/GUISpinControlEx.dox
new file mode 100644
index 0000000..a1ba4a0
--- /dev/null
+++ b/xbmc/guilib/GUISpinControlEx.dox
@@ -0,0 +1,96 @@
+/*!
+
+\page Settings_Spin_Control Settings Spin Control
+\brief **Used for cycling up/down controls in the settings menus.**
+
+\tableofcontents
+
+The settings spin control is used in the settings screens for when a list of
+options can be chosen from using up/down arrows. You can choose the position,
+size, and look of the spin control. It is basically a cross between the button
+control and a spin control. It has a label and focus and non focus textures, as
+well as a spin control on the right.
+
+--------------------------------------------------------------------------------
+\section Settings_Spin_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="spincontrolex" id="12">
+ <description>My first settings spin control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <spinposx>220</spinposx>
+ <spinposy>180</spinposy>
+ <spinwidth>16</spinwidth>
+ <spinheight>16</spinheight>
+ <visible>true</visible>
+ <colordiffuse>FFFFFFFF</colordiffuse>
+ <texturefocus>myfocustexture.png</texturefocus>
+ <texturenofocus>mynofocustexture.png</texturenofocus>
+ <textureup>myuptexture.png</textureup>
+ <textureupfocus>myupfocustexture.png</textureupfocus>
+ <texturedown>mydowntexture.png</texturedown>
+ <texturedownfocus>mydownfocustexture.png</texturedownfocus>
+ <textureupdisabled colordiffuse="AAFFAAFF">mydowntexture.png</textureupdisabled>
+ <texturedowndisabled colordiffuse="AAFFAAFF">mydownfocustexture.png</texturedowndisabled>
+ <label>46</label>
+ <font>font12</font>
+ <textcolor>FFFFFFFF</textcolor>
+ <disabledcolor>80FFFFFF</disabledcolor>
+ <align></align>
+ <aligny></aligny>
+ <textoffsetx></textoffsetx>
+ <textoffsety></textoffsety>
+ <pulseonselect></pulseonselect>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Settings_Spin_Control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|--------------------:|:--------------------------------------------------------------|
+| spinposx | The horizontal position of the spin control for multipage lists. This is offset from the top left of the list.
+| spinposy | The vertical position of the spin control for multipage lists. This is offset from the top left of the list.
+| spinwidth | The width of one of the spin control buttons. The textures for this spin control will be scaled to fit this width.
+| spinheight | The height of one of the spin control buttons. The textures for this spin control will be scaled to fit this height.
+| texturefocus | Specifies the image file which should be displayed for the control when it has focus. [See here for additional information about textures](http://kodi.wiki/view/Texture_Attributes).
+| texturenofocus | Specifies the image file which should be displayed for the control when it doesn't focus.
+| textureup | Specifies the image file which should be displayed for the up arrow when it doesn't have focus. It is displayed to the left of the down arrow.
+| textureupfocus | Specifies the image file which should be displayed for the up arrow when it has focus.
+| textureupdisabled | Specifies the image file which should be displayed for the up arrow when the button is disabled.
+| texturedown | Specifies the image file which should be displayed for the down arrow when it is not focused. It is displayed to the right of the up arrow so that it's right edge is <b>`<textoffsetx>`</b> pixels away from the right edge of the control.
+| texturedownfocus | Specifies the image file which should be displayed for the down arrow when it has focus.
+| texturedowndisabled | Specifies the image file which should be displayed for the up arrow when the button is disabled.
+| label | Either a numeric reference into strings.po (for localization), or a string that will be shown on the left of the control.
+| font | Font used for the controls label. From fonts.xml.
+| textcolor | Color used for displaying the label. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| disabledcolor | Color used for the label if the control is disabled. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| shadowcolor | Specifies the color of the drop shadow on the text. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| align | Label horizontal alignment on the control. Defaults to left.
+| aligny | Label vertical alignment on the control. Defaults to top, can also be center.
+| textoffsetx | Amount to offset the label from the left (or right) edge of the button when using left or right alignment.
+| textoffsety | Amount to offset the label from the top edge of the button when using top alignment.
+| textwidth | Will truncate any text that's too long.
+
+
+--------------------------------------------------------------------------------
+\section Settings_Spin_Control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUISpinControlEx.h b/xbmc/guilib/GUISpinControlEx.h
new file mode 100644
index 0000000..75c3963
--- /dev/null
+++ b/xbmc/guilib/GUISpinControlEx.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
+
+/*!
+\file GUISpinControlEx.h
+\brief
+*/
+
+#include "GUIButtonControl.h"
+#include "GUISpinControl.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUISpinControlEx : public CGUISpinControl
+{
+public:
+ CGUISpinControlEx(int parentID, int controlID, float posX, float posY, float width, float height, float spinWidth, float spinHeight, const CLabelInfo& spinInfo, const CTextureInfo &textureFocus, const CTextureInfo &textureNoFocus, const CTextureInfo& textureUp, const CTextureInfo& textureDown, const CTextureInfo& textureUpFocus, const CTextureInfo& textureDownFocus, const CTextureInfo& textureUpDisabled, const CTextureInfo& textureDownDisabled, const CLabelInfo& labelInfo, int iType);
+ ~CGUISpinControlEx(void) override;
+ CGUISpinControlEx* Clone() const override { return new CGUISpinControlEx(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ void SetPosition(float posX, float posY) override;
+ float GetWidth() const override { return m_buttonControl.GetWidth(); }
+ void SetWidth(float width) override;
+ float GetHeight() const override { return m_buttonControl.GetHeight(); }
+ void SetHeight(float height) override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ const std::string GetCurrentLabel() const;
+ void SetText(const std::string& aLabel) { m_buttonControl.SetLabel(aLabel); }
+ void SetEnabled(bool bEnable) override;
+ float GetXPosition() const override { return m_buttonControl.GetXPosition(); }
+ float GetYPosition() const override { return m_buttonControl.GetYPosition(); }
+ std::string GetDescription() const override;
+ bool HitTest(const CPoint& point) const override { return m_buttonControl.HitTest(point); }
+ void SetSpinPosition(float spinPosX);
+
+ void SetItemInvalid(bool invalid);
+protected:
+ void RenderText(float posX, float posY, float width, float height) override;
+ bool UpdateColors(const CGUIListItem* item) override;
+ CGUIButtonControl m_buttonControl;
+ float m_spinPosX;
+};
+
diff --git a/xbmc/guilib/GUIStaticItem.cpp b/xbmc/guilib/GUIStaticItem.cpp
new file mode 100644
index 0000000..97cd754
--- /dev/null
+++ b/xbmc/guilib/GUIStaticItem.cpp
@@ -0,0 +1,121 @@
+/*
+ * 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 "GUIStaticItem.h"
+
+#include "GUIControlFactory.h"
+#include "GUIInfoManager.h"
+#include "guilib/GUIComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+
+using namespace KODI::GUILIB;
+
+CGUIStaticItem::CGUIStaticItem(const TiXmlElement *item, int parentID) : CFileItem()
+{
+ m_visState = false;
+
+ assert(item);
+
+ GUIINFO::CGUIInfoLabel label, label2, thumb, icon;
+ CGUIControlFactory::GetInfoLabel(item, "label", label, parentID);
+ CGUIControlFactory::GetInfoLabel(item, "label2", label2, parentID);
+ CGUIControlFactory::GetInfoLabel(item, "thumb", thumb, parentID);
+ CGUIControlFactory::GetInfoLabel(item, "icon", icon, parentID);
+ const char *id = item->Attribute("id");
+ std::string condition;
+ CGUIControlFactory::GetConditionalVisibility(item, condition);
+ SetVisibleCondition(condition, parentID);
+ CGUIControlFactory::GetActions(item, "onclick", m_clickActions);
+ SetLabel(label.GetLabel(parentID));
+ SetLabel2(label2.GetLabel(parentID));
+ SetArt("thumb", thumb.GetLabel(parentID, true));
+ SetArt("icon", icon.GetLabel(parentID, true));
+ if (!label.IsConstant()) m_info.push_back(std::make_pair(label, "label"));
+ if (!label2.IsConstant()) m_info.push_back(std::make_pair(label2, "label2"));
+ if (!thumb.IsConstant()) m_info.push_back(std::make_pair(thumb, "thumb"));
+ if (!icon.IsConstant()) m_info.push_back(std::make_pair(icon, "icon"));
+ m_iprogramCount = id ? atoi(id) : 0;
+ // add any properties
+ const TiXmlElement *property = item->FirstChildElement("property");
+ while (property)
+ {
+ std::string name = XMLUtils::GetAttribute(property, "name");
+ GUIINFO::CGUIInfoLabel prop;
+ if (!name.empty() && CGUIControlFactory::GetInfoLabelFromElement(property, prop, parentID))
+ {
+ SetProperty(name, prop.GetLabel(parentID, true).c_str());
+ if (!prop.IsConstant())
+ m_info.push_back(std::make_pair(prop, name));
+ }
+ property = property->NextSiblingElement("property");
+ }
+}
+
+CGUIStaticItem::CGUIStaticItem(const CFileItem &item)
+: CFileItem(item)
+{
+ m_visState = false;
+}
+
+CGUIStaticItem::CGUIStaticItem(const CGUIStaticItem& other)
+ : CFileItem(other),
+ m_info(other.m_info),
+ m_visCondition(other.m_visCondition),
+ m_visState(other.m_visState),
+ m_clickActions(other.m_clickActions)
+{
+}
+
+void CGUIStaticItem::UpdateProperties(int contextWindow)
+{
+ for (const auto& i : m_info)
+ {
+ const GUIINFO::CGUIInfoLabel& info = i.first;
+ const std::string& name = i.second;
+ bool preferTexture = StringUtils::CompareNoCase("label", name, 5) != 0;
+ const std::string& value(info.GetLabel(contextWindow, preferTexture));
+ if (StringUtils::EqualsNoCase(name, "label"))
+ SetLabel(value);
+ else if (StringUtils::EqualsNoCase(name, "label2"))
+ SetLabel2(value);
+ else if (StringUtils::EqualsNoCase(name, "thumb"))
+ SetArt("thumb", value);
+ else if (StringUtils::EqualsNoCase(name, "icon"))
+ SetArt("icon", value);
+ else
+ SetProperty(name, value.c_str());
+ }
+}
+
+bool CGUIStaticItem::UpdateVisibility(int contextWindow)
+{
+ if (!m_visCondition)
+ return false;
+ bool state = m_visCondition->Get(contextWindow);
+ if (state != m_visState)
+ {
+ m_visState = state;
+ return true;
+ }
+ return false;
+}
+
+bool CGUIStaticItem::IsVisible() const
+{
+ if (m_visCondition)
+ return m_visState;
+ return true;
+}
+
+void CGUIStaticItem::SetVisibleCondition(const std::string &condition, int context)
+{
+ m_visCondition = CServiceBroker::GetGUI()->GetInfoManager().Register(condition, context);
+ m_visState = false;
+}
diff --git a/xbmc/guilib/GUIStaticItem.h b/xbmc/guilib/GUIStaticItem.h
new file mode 100644
index 0000000..a68f739
--- /dev/null
+++ b/xbmc/guilib/GUIStaticItem.h
@@ -0,0 +1,91 @@
+/*
+ * 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
+
+/*!
+ \file GUIStaticItem.h
+ \brief
+ */
+
+#include "FileItem.h"
+#include "GUIAction.h"
+#include "guilib/guiinfo/GUIInfoLabel.h"
+#include "interfaces/info/InfoBool.h"
+
+#include <utility>
+#include <vector>
+
+class TiXmlElement;
+
+/*!
+ \ingroup lists,items
+ \brief wrapper class for a static item in a list container
+
+ A wrapper class for the items in a container specified via the <content>
+ flag. Handles constructing items from XML and updating item labels, icons
+ and properties.
+
+ \sa CFileItem, CGUIBaseContainer
+ */
+class CGUIStaticItem : public CFileItem
+{
+public:
+ /*! \brief constructor
+ Construct an item based on an XML description:
+ <item>
+ <label>$INFO[MusicPlayer.Artist]</label>
+ <label2>$INFO[MusicPlayer.Album]</label2>
+ <thumb>bar.png</thumb>
+ <icon>foo.jpg</icon>
+ <onclick>ActivateWindow(Home)</onclick>
+ </item>
+
+ \param element XML element to construct from
+ \param contextWindow window context to use for any info labels
+ */
+ CGUIStaticItem(const TiXmlElement *element, int contextWindow);
+ explicit CGUIStaticItem(const CFileItem &item); // for python
+ explicit CGUIStaticItem(const CGUIStaticItem& other);
+ ~CGUIStaticItem() override = default;
+ CGUIListItem* Clone() const override { return new CGUIStaticItem(*this); }
+
+ /*! \brief update any infolabels in the items properties
+ Runs through all the items properties, updating any that should be
+ periodically recomputed
+ \param contextWindow window context to use for any info labels
+ */
+ void UpdateProperties(int contextWindow);
+
+ /*! \brief update visibility of this item
+ \param contextWindow window context to use for any info labels
+ \return true if visible state has changed, false otherwise
+ */
+ bool UpdateVisibility(int contextWindow);
+
+ /*! \brief whether this item is visible or not
+ */
+ bool IsVisible() const;
+
+ /*! \brief set a visible condition for this item.
+ \param condition the condition to use.
+ \param context the context for the condition (typically a window id).
+ */
+ void SetVisibleCondition(const std::string &condition, int context);
+
+ const CGUIAction& GetClickActions() const { return m_clickActions; }
+
+private:
+ typedef std::vector< std::pair<KODI::GUILIB::GUIINFO::CGUIInfoLabel, std::string> > InfoVector;
+ InfoVector m_info;
+ INFO::InfoPtr m_visCondition;
+ bool m_visState;
+ CGUIAction m_clickActions;
+};
+
+typedef std::shared_ptr<CGUIStaticItem> CGUIStaticItemPtr;
diff --git a/xbmc/guilib/GUITextBox.cpp b/xbmc/guilib/GUITextBox.cpp
new file mode 100644
index 0000000..dcfbb8e
--- /dev/null
+++ b/xbmc/guilib/GUITextBox.cpp
@@ -0,0 +1,451 @@
+/*
+ * 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 "GUITextBox.h"
+
+#include "GUIInfoManager.h"
+#include "GUIMessage.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "utils/MathUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+
+#include <algorithm>
+
+using namespace KODI::GUILIB;
+
+CGUITextBox::CGUITextBox(int parentID, int controlID, float posX, float posY, float width, float height,
+ const CLabelInfo& labelInfo, int scrollTime,
+ const CLabelInfo* labelInfoMono)
+ : CGUIControl(parentID, controlID, posX, posY, width, height)
+ , CGUITextLayout(labelInfo.font, true)
+ , m_label(labelInfo)
+{
+ m_offset = 0;
+ m_scrollOffset = 0;
+ m_scrollSpeed = 0;
+ m_itemsPerPage = 10;
+ m_itemHeight = 10;
+ ControlType = GUICONTROL_TEXTBOX;
+ m_pageControl = 0;
+ m_lastRenderTime = 0;
+ m_scrollTime = scrollTime;
+ m_autoScrollTime = 0;
+ m_autoScrollDelay = 3000;
+ m_autoScrollDelayTime = 0;
+ m_autoScrollRepeatAnim = NULL;
+ m_minHeight = 0;
+ m_renderHeight = height;
+ if (labelInfoMono)
+ SetMonoFont(labelInfoMono->font);
+}
+
+CGUITextBox::CGUITextBox(const CGUITextBox& from)
+ : CGUIControl(from), CGUITextLayout(from), m_autoScrollCondition(from.m_autoScrollCondition)
+{
+ m_pageControl = from.m_pageControl;
+ m_scrollTime = from.m_scrollTime;
+ m_autoScrollTime = from.m_autoScrollTime;
+ m_autoScrollDelay = from.m_autoScrollDelay;
+ m_minHeight = from.m_minHeight;
+ m_renderHeight = from.m_renderHeight;
+ m_autoScrollRepeatAnim = NULL;
+ if (from.m_autoScrollRepeatAnim)
+ m_autoScrollRepeatAnim = new CAnimation(*from.m_autoScrollRepeatAnim);
+ m_label = from.m_label;
+ m_info = from.m_info;
+ // defaults
+ m_offset = 0;
+ m_scrollOffset = 0;
+ m_scrollSpeed = 0;
+ m_itemsPerPage = 10;
+ m_itemHeight = 10;
+ m_lastRenderTime = 0;
+ m_autoScrollDelayTime = 0;
+ ControlType = GUICONTROL_TEXTBOX;
+}
+
+CGUITextBox::~CGUITextBox(void)
+{
+ delete m_autoScrollRepeatAnim;
+ m_autoScrollRepeatAnim = NULL;
+}
+
+bool CGUITextBox::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIControl::UpdateColors(nullptr);
+ changed |= m_label.UpdateColors();
+
+ return changed;
+}
+
+#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))
+
+void CGUITextBox::UpdateInfo(const CGUIListItem *item)
+{
+ m_textColor = m_label.textColor;
+ if (!CGUITextLayout::Update(item ? m_info.GetItemLabel(item) : m_info.GetLabel(m_parentID), m_width))
+ return; // nothing changed
+
+ // needed update, so reset to the top of the textbox and update our sizing/page control
+ SetInvalid();
+ m_offset = 0;
+ m_scrollOffset = 0;
+ ResetAutoScrolling();
+
+ m_itemHeight = m_font ? m_font->GetLineHeight() : 10;
+ float textHeight = m_font ? m_font->GetTextHeight(m_lines.size()) : m_itemHeight * m_lines.size();
+ float maxHeight = m_height ? m_height : textHeight;
+ m_renderHeight = m_minHeight ? CLAMP(textHeight, m_minHeight, maxHeight) : m_height;
+ m_itemsPerPage = (unsigned int)(m_renderHeight / m_itemHeight);
+
+ UpdatePageControl();
+}
+
+void CGUITextBox::DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ CGUIControl::DoProcess(currentTime, dirtyregions);
+
+ // if not visible, we reset the autoscroll timer and positioning
+ if (!IsVisible() && m_autoScrollTime)
+ {
+ ResetAutoScrolling();
+ m_lastRenderTime = 0;
+ m_offset = 0;
+ m_scrollOffset = 0;
+ m_scrollSpeed = 0;
+ }
+}
+
+void CGUITextBox::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ // update our auto-scrolling as necessary
+ if (m_autoScrollTime && m_lines.size() > m_itemsPerPage)
+ {
+ if (!m_autoScrollCondition || m_autoScrollCondition->Get(INFO::DEFAULT_CONTEXT))
+ {
+ if (m_lastRenderTime)
+ m_autoScrollDelayTime += currentTime - m_lastRenderTime;
+ if (m_autoScrollDelayTime > (unsigned int)m_autoScrollDelay && m_scrollSpeed == 0)
+ { // delay is finished - start scrolling
+ MarkDirtyRegion();
+ if (m_offset < (int)m_lines.size() - m_itemsPerPage)
+ ScrollToOffset(m_offset + 1, true);
+ else
+ { // at the end, run a delay and restart
+ if (m_autoScrollRepeatAnim)
+ {
+ if (m_autoScrollRepeatAnim->GetState() == ANIM_STATE_NONE)
+ m_autoScrollRepeatAnim->QueueAnimation(ANIM_PROCESS_NORMAL);
+ else if (m_autoScrollRepeatAnim->GetState() == ANIM_STATE_APPLIED)
+ { // reset to the start of the list and start the scrolling again
+ m_offset = 0;
+ m_scrollOffset = 0;
+ ResetAutoScrolling();
+ }
+ }
+ }
+ }
+ }
+ else if (m_autoScrollCondition)
+ ResetAutoScrolling(); // conditional is false, so reset the autoscrolling
+ }
+
+ // render the repeat anim as appropriate
+ if (m_autoScrollRepeatAnim)
+ {
+ if (m_autoScrollRepeatAnim->GetProcess() != ANIM_PROCESS_NONE)
+ MarkDirtyRegion();
+ m_autoScrollRepeatAnim->Animate(currentTime, true);
+ TransformMatrix matrix;
+ m_autoScrollRepeatAnim->RenderAnimation(matrix);
+ m_cachedTextMatrix = CServiceBroker::GetWinSystem()->GetGfxContext().AddTransform(matrix);
+ }
+
+ // update our scroll position as necessary
+ if (m_scrollSpeed != 0)
+ MarkDirtyRegion();
+
+ if (m_lastRenderTime)
+ m_scrollOffset += m_scrollSpeed * (currentTime - m_lastRenderTime);
+ if ((m_scrollSpeed < 0 && m_scrollOffset < m_offset * m_itemHeight) ||
+ (m_scrollSpeed > 0 && m_scrollOffset > m_offset * m_itemHeight))
+ {
+ m_scrollOffset = m_offset * m_itemHeight;
+ m_scrollSpeed = 0;
+ }
+ m_lastRenderTime = currentTime;
+
+ if (m_pageControl)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), m_pageControl,
+ MathUtils::round_int(static_cast<double>(m_scrollOffset / m_itemHeight)));
+ SendWindowMessage(msg);
+ }
+
+ CGUIControl::Process(currentTime, dirtyregions);
+
+ if (m_autoScrollRepeatAnim)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+}
+
+void CGUITextBox::Render()
+{
+ // render the repeat anim as appropriate
+ if (m_autoScrollRepeatAnim)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetTransform(m_cachedTextMatrix);
+
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_renderHeight))
+ {
+ // we offset our draw position to take into account scrolling and whether or not our focused
+ // item is offscreen "above" the list.
+ int offset = (int)(m_scrollOffset / m_itemHeight);
+ float posX = m_posX;
+ float posY = m_posY + offset * m_itemHeight - m_scrollOffset;
+
+ uint32_t alignment = m_label.align;
+
+ if (alignment & XBFONT_CENTER_Y)
+ {
+ if (m_font)
+ {
+ float textHeight = m_font->GetTextHeight(std::min((unsigned int)m_lines.size(), m_itemsPerPage));
+
+ if (textHeight <= m_renderHeight)
+ posY += (m_renderHeight - textHeight) * 0.5f;
+ }
+
+ alignment &= ~XBFONT_CENTER_Y;
+ }
+
+ // alignment correction
+ if (alignment & XBFONT_CENTER_X)
+ posX += m_width * 0.5f;
+ if (alignment & XBFONT_RIGHT)
+ posX += m_width;
+
+ if (m_font)
+ {
+ m_font->Begin();
+ int current = offset;
+
+ // set the main text color
+ if (m_colors.size())
+ m_colors[0] = m_label.textColor;
+
+ while (posY < m_posY + m_renderHeight && current < (int)m_lines.size())
+ {
+ uint32_t align = alignment;
+ if (m_lines[current].m_text.size() && m_lines[current].m_carriageReturn)
+ align &= ~XBFONT_JUSTIFIED; // last line of a paragraph shouldn't be justified
+ m_font->DrawText(posX, posY, m_colors, m_label.shadowColor, m_lines[current].m_text, align, m_width);
+ posY += m_itemHeight;
+ current++;
+ }
+ m_font->End();
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+ if (m_autoScrollRepeatAnim)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+ CGUIControl::Render();
+}
+
+bool CGUITextBox::OnMessage(CGUIMessage& message)
+{
+ if (message.GetControlId() == GetID())
+ {
+ if (message.GetMessage() == GUI_MSG_LABEL_SET)
+ {
+ m_offset = 0;
+ m_scrollOffset = 0;
+ ResetAutoScrolling();
+ CGUITextLayout::Reset();
+ m_info.SetLabel(message.GetLabel(), "", GetParentID());
+ }
+
+ if (message.GetMessage() == GUI_MSG_LABEL_RESET)
+ {
+ m_offset = 0;
+ m_scrollOffset = 0;
+ ResetAutoScrolling();
+ CGUITextLayout::Reset();
+ UpdatePageControl();
+ SetInvalid();
+ }
+
+ if (message.GetMessage() == GUI_MSG_PAGE_CHANGE)
+ {
+ if (message.GetSenderId() == m_pageControl)
+ { // update our page
+ Scroll(message.GetParam1());
+ return true;
+ }
+ }
+
+ if (message.GetMessage() == GUI_MSG_SET_TYPE)
+ {
+ UseMonoFont(message.GetParam1() == 1 ? true : false);
+ return true;
+ }
+ }
+
+ return CGUIControl::OnMessage(message);
+}
+
+float CGUITextBox::GetHeight() const
+{
+ return m_renderHeight;
+}
+
+void CGUITextBox::SetMinHeight(float minHeight)
+{
+ if (m_minHeight != minHeight)
+ SetInvalid();
+
+ m_minHeight = minHeight;
+}
+
+void CGUITextBox::UpdatePageControl()
+{
+ if (m_pageControl)
+ {
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), m_pageControl, m_itemsPerPage, m_lines.size());
+ SendWindowMessage(msg);
+ }
+}
+
+bool CGUITextBox::CanFocus() const
+{
+ return false;
+}
+
+void CGUITextBox::SetPageControl(int pageControl)
+{
+ m_pageControl = pageControl;
+}
+
+void CGUITextBox::SetInfo(const GUIINFO::CGUIInfoLabel &infoLabel)
+{
+ m_info = infoLabel;
+}
+
+void CGUITextBox::Scroll(unsigned int offset)
+{
+ ResetAutoScrolling();
+ if (m_lines.size() <= m_itemsPerPage)
+ return; // no need to scroll
+ if (offset > m_lines.size() - m_itemsPerPage)
+ offset = m_lines.size() - m_itemsPerPage; // on last page
+ ScrollToOffset(offset);
+}
+
+void CGUITextBox::ScrollToOffset(int offset, bool autoScroll)
+{
+ m_scrollOffset = m_offset * m_itemHeight;
+ int timeToScroll = autoScroll ? m_autoScrollTime : m_scrollTime;
+ m_scrollSpeed = (offset * m_itemHeight - m_scrollOffset) / timeToScroll;
+ m_offset = offset;
+}
+
+void CGUITextBox::SetAutoScrolling(const TiXmlNode *node)
+{
+ if (!node) return;
+ const TiXmlElement *scroll = node->FirstChildElement("autoscroll");
+ if (scroll)
+ {
+ scroll->Attribute("delay", &m_autoScrollDelay);
+ scroll->Attribute("time", &m_autoScrollTime);
+ if (scroll->FirstChild())
+ m_autoScrollCondition = CServiceBroker::GetGUI()->GetInfoManager().Register(scroll->FirstChild()->ValueStr(), GetParentID());
+ int repeatTime;
+ if (scroll->Attribute("repeat", &repeatTime))
+ m_autoScrollRepeatAnim = new CAnimation(CAnimation::CreateFader(100, 0, repeatTime, 1000));
+ }
+}
+
+void CGUITextBox::SetAutoScrolling(int delay, int time, int repeatTime, const std::string &condition /* = "" */)
+{
+ m_autoScrollDelay = delay;
+ m_autoScrollTime = time;
+ if (!condition.empty())
+ m_autoScrollCondition = CServiceBroker::GetGUI()->GetInfoManager().Register(condition, GetParentID());
+ m_autoScrollRepeatAnim = new CAnimation(CAnimation::CreateFader(100, 0, repeatTime, 1000));
+}
+
+void CGUITextBox::ResetAutoScrolling()
+{
+ m_autoScrollDelayTime = 0;
+ if (m_autoScrollRepeatAnim)
+ m_autoScrollRepeatAnim->ResetAnimation();
+}
+
+unsigned int CGUITextBox::GetRows() const
+{
+ return m_lines.size();
+}
+
+int CGUITextBox::GetNumPages() const
+{
+ return m_itemsPerPage > 0 ? (GetRows() + m_itemsPerPage - 1) / m_itemsPerPage : 0;
+}
+
+int CGUITextBox::GetCurrentPage() const
+{
+ if (m_offset + m_itemsPerPage >= GetRows()) // last page
+ return GetNumPages();
+ return m_offset / m_itemsPerPage + 1;
+}
+
+std::string CGUITextBox::GetLabel(int info) const
+{
+ std::string label;
+ switch (info)
+ {
+ case CONTAINER_NUM_PAGES:
+ label = std::to_string(GetNumPages());
+ break;
+ case CONTAINER_CURRENT_PAGE:
+ label = std::to_string(GetCurrentPage());
+ break;
+ default:
+ break;
+ }
+ return label;
+}
+
+bool CGUITextBox::GetCondition(int condition, int data) const
+{
+ switch (condition)
+ {
+ case CONTAINER_HAS_NEXT:
+ return (GetCurrentPage() < GetNumPages());
+ case CONTAINER_HAS_PREVIOUS:
+ return (GetCurrentPage() > 1);
+ default:
+ return false;
+ }
+}
+
+std::string CGUITextBox::GetDescription() const
+{
+ return GetText();
+}
+
+void CGUITextBox::UpdateVisibility(const CGUIListItem *item)
+{
+ // we have to update the page control when we become visible
+ // as another control may be sharing the same page control when we're
+ // not visible
+ bool wasVisible = IsVisible();
+ CGUIControl::UpdateVisibility(item);
+ if (IsVisible() && !wasVisible)
+ UpdatePageControl();
+}
diff --git a/xbmc/guilib/GUITextBox.dox b/xbmc/guilib/GUITextBox.dox
new file mode 100644
index 0000000..aba63ca
--- /dev/null
+++ b/xbmc/guilib/GUITextBox.dox
@@ -0,0 +1,63 @@
+/*!
+
+\page Text_Box Text Box
+\brief **Used to show a multi-page piece of text.**
+
+\tableofcontents
+
+The text box is used for showing a large multipage piece of text in Kodi. You
+can choose the position, size, and look of the text.
+
+
+--------------------------------------------------------------------------------
+\section Text_Box_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="textbox" id="2">
+ <description>My first text box control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+ <colordiffuse>FFFFFFFF</colordiffuse>
+ <font>font13</font>
+ <textcolor>FFFFFFFF</textcolor>
+ <pulseonselect></pulseonselect>
+ <pagecontrol>13</pagecontrol>
+ <scrolltime>200</scrolltime>
+ <autoscroll delay="3000" time="1000" repeat="10000">!Control.HasFocus(13)</autoscroll>
+ <label>Text to display goes here [CR] next line...</label>
+ <align>center</align>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Text_Box_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is lower case only. This is
+important, as xml tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| height | <b>`<height>auto</height>`</b> is supported in textbox controls
+| font | Font used for the items first label. From `fonts.xml`.
+| textcolor | Color used for displaying the text. In **AARRGGBB** hex format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| shadowcolor | Specifies the color of the drop shadow on the text. In **AARRGGBB** format, or a name from the [colour theme](http://kodi.wiki/view/Colour_Themes).
+| pagecontrol | Specifies the <b>`<id>`</b> of the page control used to control this textbox. The page control can either be a \ref Spin_Control "Spin Control" or a \ref Scroll_Bar_Control "Scroll Bar Control".
+| scrolltime | The time (in ms) to scroll from one item to another. By default, this is *200ms*. The list will scroll smoothly from one item to another as needed. Set it to zero to disable the smooth scrolling.
+| align | possible values for text alignment: left, right, center, justify
+| autoscroll | Specifies the timing and conditions of any autoscrolling this textbox should have. Times are in milliseconds. The content is delayed for the given delay, then scrolls at a rate of one line per time interval until the end. If the repeat tag is present, it then delays for the repeat time, fades out over 1 second, and repeats. It does not wrap or reset to the top at the end of the scroll. You can use any [bool condition](http://kodi.wiki/view/List_of_Boolean_Conditions) to specify when autoscrolling should be allowed.
+
+
+--------------------------------------------------------------------------------
+\section Text_Box_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUITextBox.h b/xbmc/guilib/GUITextBox.h
new file mode 100644
index 0000000..7027731
--- /dev/null
+++ b/xbmc/guilib/GUITextBox.h
@@ -0,0 +1,97 @@
+/*
+ * 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
+
+/*!
+\file GUITextBox.h
+\brief
+*/
+
+#include "GUIControl.h"
+#include "GUILabel.h"
+#include "GUITextLayout.h"
+#include "guilib/guiinfo/GUIInfoLabel.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+
+class TiXmlNode;
+
+class CGUITextBox : public CGUIControl, public CGUITextLayout
+{
+public:
+ CGUITextBox(int parentID, int controlID, float posX, float posY, float width, float height,
+ const CLabelInfo &labelInfo, int scrollTime = 200,
+ const CLabelInfo* labelInfoMono = nullptr);
+ CGUITextBox(const CGUITextBox &from);
+ ~CGUITextBox(void) override;
+ CGUITextBox* Clone() const override { return new CGUITextBox(*this); }
+
+ void DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnMessage(CGUIMessage& message) override;
+ float GetHeight() const override;
+ void SetMinHeight(float minHeight);
+
+ void SetPageControl(int pageControl);
+
+ bool CanFocus() const override;
+ void SetInfo(const KODI::GUILIB::GUIINFO::CGUIInfoLabel &info);
+ void SetAutoScrolling(const TiXmlNode *node);
+ void SetAutoScrolling(int delay, int time, int repeatTime, const std::string &condition = "");
+ void ResetAutoScrolling();
+
+ bool GetCondition(int condition, int data) const override;
+ virtual std::string GetLabel(int info) const;
+ std::string GetDescription() const override;
+
+ void Scroll(unsigned int offset);
+
+protected:
+ void UpdateVisibility(const CGUIListItem *item = NULL) override;
+ bool UpdateColors(const CGUIListItem* item) override;
+ void UpdateInfo(const CGUIListItem *item = NULL) override;
+ void UpdatePageControl();
+ void ScrollToOffset(int offset, bool autoScroll = false);
+ unsigned int GetRows() const;
+ int GetCurrentPage() const;
+ int GetNumPages() const;
+
+ // auto-height
+ float m_minHeight;
+ float m_renderHeight;
+
+ // offset of text in the control for scrolling
+ unsigned int m_offset;
+ float m_scrollOffset;
+ float m_scrollSpeed;
+ int m_scrollTime;
+ unsigned int m_itemsPerPage;
+ float m_itemHeight;
+ unsigned int m_lastRenderTime;
+
+ CLabelInfo m_label;
+
+ TransformMatrix m_cachedTextMatrix;
+
+ // autoscrolling
+ INFO::InfoPtr m_autoScrollCondition;
+ int m_autoScrollTime; // time to scroll 1 line (ms)
+ int m_autoScrollDelay; // delay before scroll (ms)
+ unsigned int m_autoScrollDelayTime; // current offset into the delay
+ CAnimation *m_autoScrollRepeatAnim;
+
+ int m_pageControl;
+
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_info;
+};
+
diff --git a/xbmc/guilib/GUITextLayout.cpp b/xbmc/guilib/GUITextLayout.cpp
new file mode 100644
index 0000000..5058853
--- /dev/null
+++ b/xbmc/guilib/GUITextLayout.cpp
@@ -0,0 +1,736 @@
+/*
+ * 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 "GUITextLayout.h"
+
+#include "GUIColorManager.h"
+#include "GUIComponent.h"
+#include "GUIControl.h"
+#include "GUIFont.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+
+CGUIString::CGUIString(iString start, iString end, bool carriageReturn)
+{
+ m_text.assign(start, end);
+ m_carriageReturn = carriageReturn;
+}
+
+std::string CGUIString::GetAsString() const
+{
+ std::string text;
+ for (unsigned int i = 0; i < m_text.size(); i++)
+ text += (char)(m_text[i] & 0xff);
+ return text;
+}
+
+CGUITextLayout::CGUITextLayout(CGUIFont *font, bool wrap, float fHeight, CGUIFont *borderFont)
+{
+ m_varFont = m_font = font;
+ m_borderFont = borderFont;
+ m_textColor = 0;
+ m_wrap = wrap;
+ m_maxHeight = fHeight;
+ m_textWidth = 0;
+ m_textHeight = 0;
+ m_lastUpdateW = false;
+}
+
+void CGUITextLayout::SetWrap(bool bWrap)
+{
+ m_wrap = bWrap;
+}
+
+void CGUITextLayout::Render(float x,
+ float y,
+ float angle,
+ UTILS::COLOR::Color color,
+ UTILS::COLOR::Color shadowColor,
+ uint32_t alignment,
+ float maxWidth,
+ bool solid)
+{
+ if (!m_font)
+ return;
+
+ // set the main text color
+ if (m_colors.size())
+ m_colors[0] = color;
+
+ // render the text at the required location, angle, and size
+ if (angle)
+ {
+ static const float degrees_to_radians = 0.01745329252f;
+ CServiceBroker::GetWinSystem()->GetGfxContext().AddTransform(TransformMatrix::CreateZRotation(angle * degrees_to_radians, x, y, CServiceBroker::GetWinSystem()->GetGfxContext().GetScalingPixelRatio()));
+ }
+ // center our text vertically
+ if (alignment & XBFONT_CENTER_Y)
+ {
+ y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;
+ alignment &= ~XBFONT_CENTER_Y;
+ }
+ m_font->Begin();
+ for (const auto& string : m_lines)
+ {
+ uint32_t align = alignment;
+ if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
+ align &= ~XBFONT_JUSTIFIED;
+ if (solid)
+ m_font->DrawText(x, y, m_colors[0], shadowColor, string.m_text, align, maxWidth);
+ else
+ m_font->DrawText(x, y, m_colors, shadowColor, string.m_text, align, maxWidth);
+ y += m_font->GetLineHeight();
+ }
+ m_font->End();
+ if (angle)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+}
+
+bool CGUITextLayout::UpdateScrollinfo(CScrollInfo &scrollInfo)
+{
+ if (!m_font)
+ return false;
+ if (m_lines.empty())
+ return false;
+
+ return m_font->UpdateScrollInfo(m_lines[0].m_text, scrollInfo);
+}
+
+
+void CGUITextLayout::RenderScrolling(float x,
+ float y,
+ float angle,
+ UTILS::COLOR::Color color,
+ UTILS::COLOR::Color shadowColor,
+ uint32_t alignment,
+ float maxWidth,
+ const CScrollInfo& scrollInfo)
+{
+ if (!m_font)
+ return;
+
+ // set the main text color
+ if (m_colors.size())
+ m_colors[0] = color;
+
+ // render the text at the required location, angle, and size
+ if (angle)
+ {
+ static const float degrees_to_radians = 0.01745329252f;
+ CServiceBroker::GetWinSystem()->GetGfxContext().AddTransform(TransformMatrix::CreateZRotation(angle * degrees_to_radians, x, y, CServiceBroker::GetWinSystem()->GetGfxContext().GetScalingPixelRatio()));
+ }
+ // center our text vertically
+ if (alignment & XBFONT_CENTER_Y)
+ {
+ y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;
+ alignment &= ~XBFONT_CENTER_Y;
+ }
+ m_font->Begin();
+ // NOTE: This workaround is needed as otherwise multi-line text that scrolls
+ // will scroll in proportion to the number of lines. Ideally we should
+ // do the DrawScrollingText calculation here. This probably won't make
+ // any difference to the smoothness of scrolling though which will be
+ // jumpy with this sort of thing. It's not exactly a well used situation
+ // though, so this hack is probably OK.
+ for (const auto& string : m_lines)
+ {
+ m_font->DrawScrollingText(x, y, m_colors, shadowColor, string.m_text, alignment, maxWidth, scrollInfo);
+ y += m_font->GetLineHeight();
+ }
+ m_font->End();
+ if (angle)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+}
+
+void CGUITextLayout::RenderOutline(float x,
+ float y,
+ UTILS::COLOR::Color color,
+ UTILS::COLOR::Color outlineColor,
+ uint32_t alignment,
+ float maxWidth)
+{
+ if (!m_font)
+ return;
+
+ // set the outline color
+ std::vector<UTILS::COLOR::Color> outlineColors;
+ if (m_colors.size())
+ outlineColors.push_back(outlineColor);
+
+ // center our text vertically
+ if (alignment & XBFONT_CENTER_Y)
+ {
+ y -= m_font->GetTextHeight(m_lines.size()) * 0.5f;
+ alignment &= ~XBFONT_CENTER_Y;
+ }
+ if (m_borderFont)
+ {
+ // adjust so the baselines of the fonts align
+ float by = y + m_font->GetTextBaseLine() - m_borderFont->GetTextBaseLine();
+ m_borderFont->Begin();
+ for (const auto& string : m_lines)
+ {
+ uint32_t align = alignment;
+ if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
+ align &= ~XBFONT_JUSTIFIED;
+ // text centered horizontally must be computed using the original font, not the bordered
+ // font, as the bordered font will be wider, and thus will end up uncentered.
+ //! @todo We should really have a better way to handle text extent - at the moment we assume
+ //! that text is rendered from a posx, posy, width, and height which isn't enough to
+ //! accurately position text. We need a vertical and horizontal offset of the baseline
+ //! and cursor as well.
+ float bx = x;
+ if (align & XBFONT_CENTER_X)
+ {
+ bx -= m_font->GetTextWidth(string.m_text) * 0.5f;
+ align &= ~XBFONT_CENTER_X;
+ }
+
+ // don't pass maxWidth through to the renderer for the same reason above: it will cause clipping
+ // on the left.
+ m_borderFont->DrawText(bx, by, outlineColors, 0, string.m_text, align, 0);
+ by += m_borderFont->GetLineHeight();
+ }
+ m_borderFont->End();
+ }
+
+ // set the main text color
+ if (m_colors.size())
+ m_colors[0] = color;
+
+ m_font->Begin();
+ for (const auto& string : m_lines)
+ {
+ uint32_t align = alignment;
+ if (align & XBFONT_JUSTIFIED && string.m_carriageReturn)
+ align &= ~XBFONT_JUSTIFIED;
+
+ // don't pass maxWidth through to the renderer for the reason above.
+ m_font->DrawText(x, y, m_colors, 0, string.m_text, align, 0);
+ y += m_font->GetLineHeight();
+ }
+ m_font->End();
+}
+
+bool CGUITextLayout::Update(const std::string &text, float maxWidth, bool forceUpdate /*= false*/, bool forceLTRReadingOrder /*= false*/)
+{
+ if (text == m_lastUtf8Text && !forceUpdate && !m_lastUpdateW)
+ return false;
+
+ m_lastUtf8Text = text;
+ m_lastUpdateW = false;
+ std::wstring utf16;
+ g_charsetConverter.utf8ToW(text, utf16, false);
+ UpdateCommon(utf16, maxWidth, forceLTRReadingOrder);
+ return true;
+}
+
+bool CGUITextLayout::UpdateW(const std::wstring &text, float maxWidth /*= 0*/, bool forceUpdate /*= false*/, bool forceLTRReadingOrder /*= false*/)
+{
+ if (text == m_lastText && !forceUpdate && m_lastUpdateW)
+ return false;
+
+ m_lastText = text;
+ m_lastUpdateW = true;
+ UpdateCommon(text, maxWidth, forceLTRReadingOrder);
+ return true;
+}
+
+void CGUITextLayout::UpdateCommon(const std::wstring &text, float maxWidth, bool forceLTRReadingOrder)
+{
+ // parse the text for style information
+ vecText parsedText;
+ std::vector<UTILS::COLOR::Color> colors;
+ ParseText(text, m_font ? m_font->GetStyle() : 0, m_textColor, colors, parsedText);
+
+ // and update
+ UpdateStyled(parsedText, colors, maxWidth, forceLTRReadingOrder);
+}
+
+void CGUITextLayout::UpdateStyled(const vecText& text,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ float maxWidth,
+ bool forceLTRReadingOrder)
+{
+ // empty out our previous string
+ m_lines.clear();
+ m_colors = colors;
+
+ // if we need to wrap the text, then do so
+ if (m_wrap && maxWidth > 0)
+ WrapText(text, maxWidth);
+ else
+ LineBreakText(text, m_lines);
+
+ // remove any trailing blank lines
+ while (!m_lines.empty() && m_lines.back().m_text.empty())
+ m_lines.pop_back();
+
+ BidiTransform(m_lines, forceLTRReadingOrder);
+
+ // and cache the width and height for later reading
+ CalcTextExtent();
+}
+
+// BidiTransform is used to handle RTL text flipping in the string
+void CGUITextLayout::BidiTransform(std::vector<CGUIString>& lines, bool forceLTRReadingOrder)
+{
+ for (unsigned int i = 0; i < lines.size(); i++)
+ {
+ CGUIString& line = lines[i];
+ unsigned int lineLength = line.m_text.size();
+ std::wstring logicalText;
+ vecText style;
+
+ logicalText.reserve(lineLength);
+ style.reserve(lineLength);
+
+ // Separate the text and style for the input styled text
+ for (const auto& it : line.m_text)
+ {
+ logicalText.push_back((wchar_t)(it & 0xffff));
+ style.push_back(it & 0xffff0000);
+ }
+
+ // Allocate memory for visual to logical map and call bidi
+ int* visualToLogicalMap = new (std::nothrow) int[lineLength + 1]();
+ std::wstring visualText = BidiFlip(logicalText, forceLTRReadingOrder, visualToLogicalMap);
+
+ vecText styledVisualText;
+ styledVisualText.reserve(lineLength);
+
+ // If memory allocation failed, fallback to text with no styling
+ if (!visualToLogicalMap)
+ {
+ for (unsigned int j = 0; j < visualText.size(); j++)
+ {
+ styledVisualText.push_back(visualText[j]);
+ }
+ }
+ else
+ {
+ for (unsigned int j = 0; j < visualText.size(); j++)
+ {
+ styledVisualText.push_back(style[visualToLogicalMap[j]] | visualText[j]);
+ }
+ }
+
+ delete[] visualToLogicalMap;
+
+ // replace the original line with the processed one
+ lines[i] = CGUIString(styledVisualText.begin(), styledVisualText.end(), line.m_carriageReturn);
+ }
+}
+
+std::wstring CGUITextLayout::BidiFlip(const std::wstring& text,
+ bool forceLTRReadingOrder,
+ int* visualToLogicalMap /*= nullptr*/)
+{
+ std::wstring visualText;
+ std::u32string utf32logical;
+ std::u32string utf32visual;
+
+ // Convert to utf32, call bidi then convert the result back to utf16
+ g_charsetConverter.wToUtf32(text, utf32logical);
+ g_charsetConverter.utf32logicalToVisualBiDi(utf32logical, utf32visual, forceLTRReadingOrder,
+ false,
+ visualToLogicalMap);
+ g_charsetConverter.utf32ToW(utf32visual, visualText);
+
+ return visualText;
+}
+
+void CGUITextLayout::Filter(std::string &text)
+{
+ std::wstring utf16;
+ g_charsetConverter.utf8ToW(text, utf16, false);
+ std::vector<UTILS::COLOR::Color> colors;
+ vecText parsedText;
+ ParseText(utf16, 0, 0xffffffff, colors, parsedText);
+ utf16.clear();
+ for (unsigned int i = 0; i < parsedText.size(); i++)
+ utf16 += (wchar_t)(0xffff & parsedText[i]);
+ g_charsetConverter.wToUTF8(utf16, text);
+}
+
+void CGUITextLayout::ParseText(const std::wstring& text,
+ uint32_t defaultStyle,
+ UTILS::COLOR::Color defaultColor,
+ std::vector<UTILS::COLOR::Color>& colors,
+ vecText& parsedText)
+{
+ // run through the string, searching for:
+ // [B] or [/B] -> toggle bold on and off
+ // [I] or [/I] -> toggle italics on and off
+ // [COLOR ffab007f] or [/COLOR] -> toggle color on and off
+ // [CAPS <option>] or [/CAPS] -> toggle capatilization on and off
+ // [TABS] tab amount [/TABS] -> add tabulator space in view
+
+ uint32_t currentStyle = defaultStyle; // start with the default font's style
+ UTILS::COLOR::Color currentColor = 0;
+
+ colors.push_back(defaultColor);
+ std::stack<UTILS::COLOR::Color> colorStack;
+ colorStack.push(0);
+
+ // these aren't independent, but that's probably not too much of an issue
+ // eg [UPPERCASE]Glah[LOWERCASE]FReD[/LOWERCASE]Georeg[/UPPERCASE] will work (lower case >> upper case)
+ // but [LOWERCASE]Glah[UPPERCASE]FReD[/UPPERCASE]Georeg[/LOWERCASE] won't
+
+ int startPos = 0;
+ size_t pos = text.find(L'[');
+ while (pos != std::string::npos && pos + 1 < text.size())
+ {
+ uint32_t newStyle = 0;
+ UTILS::COLOR::Color newColor = currentColor;
+ bool colorTagChange = false;
+ bool newLine = false;
+ int tabs = 0;
+ // have a [ - check if it's an ON or OFF switch
+ bool on(true);
+ size_t endPos = pos++; // finish of string
+ if (text[pos] == L'/')
+ {
+ on = false;
+ pos++;
+ }
+ // check for each type
+ if (text.compare(pos, 2, L"B]") == 0)
+ { // bold - finish the current text block and assign the bold state
+ pos += 2;
+ if ((on && text.find(L"[/B]",pos) != std::string::npos) || // check for a matching end point
+ (!on && (currentStyle & FONT_STYLE_BOLD))) // or matching start point
+ newStyle = FONT_STYLE_BOLD;
+ }
+ else if (text.compare(pos, 2, L"I]") == 0)
+ { // italics
+ pos += 2;
+ if ((on && text.find(L"[/I]", pos) != std::string::npos) || // check for a matching end point
+ (!on && (currentStyle & FONT_STYLE_ITALICS))) // or matching start point
+ newStyle = FONT_STYLE_ITALICS;
+ }
+ else if (text.compare(pos, 10, L"UPPERCASE]") == 0)
+ {
+ pos += 10;
+ if ((on && text.find(L"[/UPPERCASE]", pos) != std::string::npos) || // check for a matching end point
+ (!on && (currentStyle & FONT_STYLE_UPPERCASE))) // or matching start point
+ newStyle = FONT_STYLE_UPPERCASE;
+ }
+ else if (text.compare(pos, 10, L"LOWERCASE]") == 0)
+ {
+ pos += 10;
+ if ((on && text.find(L"[/LOWERCASE]", pos) != std::string::npos) || // check for a matching end point
+ (!on && (currentStyle & FONT_STYLE_LOWERCASE))) // or matching start point
+ newStyle = FONT_STYLE_LOWERCASE;
+ }
+ else if (text.compare(pos, 11, L"CAPITALIZE]") == 0)
+ {
+ pos += 11;
+ if ((on && text.find(L"[/CAPITALIZE]", pos) != std::string::npos) || // check for a matching end point
+ (!on && (currentStyle & FONT_STYLE_CAPITALIZE))) // or matching start point
+ newStyle = FONT_STYLE_CAPITALIZE;
+ }
+ else if (text.compare(pos, 6, L"LIGHT]") == 0)
+ {
+ pos += 6;
+ if ((on && text.find(L"[/LIGHT]", pos) != std::string::npos) ||
+ (!on && (currentStyle & FONT_STYLE_LIGHT)))
+ newStyle = FONT_STYLE_LIGHT;
+ }
+ else if (text.compare(pos, 5, L"TABS]") == 0 && on)
+ {
+ pos += 5;
+ const size_t end = text.find(L"[/TABS]", pos);
+ if (end != std::string::npos)
+ {
+ std::string t;
+ g_charsetConverter.wToUTF8(text.substr(pos), t);
+ tabs = atoi(t.c_str());
+ pos = end + 7;
+ }
+ }
+ else if (text.compare(pos, 3, L"CR]") == 0 && on)
+ {
+ newLine = true;
+ pos += 3;
+ }
+ else if (text.compare(pos,5, L"COLOR") == 0)
+ { // color
+ size_t finish = text.find(L']', pos + 5);
+ if (on && finish != std::string::npos && text.find(L"[/COLOR]",finish) != std::string::npos)
+ {
+ std::string t;
+ g_charsetConverter.wToUTF8(text.substr(pos + 5, finish - pos - 5), t);
+ UTILS::COLOR::Color color = CServiceBroker::GetGUI()->GetColorManager().GetColor(t);
+ const auto& it = std::find(colors.begin(), colors.end(), color);
+ if (it == colors.end())
+ { // create new color
+ if (colors.size() <= 0xFF)
+ {
+ newColor = colors.size();
+ colors.push_back(color);
+ }
+ else // we have only 8 bits for color index, fallback to first color if reach max.
+ newColor = 0;
+ }
+ else
+ // reuse existing color
+ newColor = it - colors.begin();
+ colorStack.push(newColor);
+ colorTagChange = true;
+ }
+ else if (!on && finish == pos + 5 && colorStack.size() > 1)
+ { // revert to previous color
+ colorStack.pop();
+ newColor = colorStack.top();
+ colorTagChange = true;
+ }
+ if (finish != std::string::npos)
+ pos = finish + 1;
+ }
+
+ if (newStyle || colorTagChange || newLine || tabs)
+ { // we have a new style or a new color, so format up the previous segment
+ std::wstring subText = text.substr(startPos, endPos - startPos);
+ if (currentStyle & FONT_STYLE_UPPERCASE)
+ StringUtils::ToUpper(subText);
+ if (currentStyle & FONT_STYLE_LOWERCASE)
+ StringUtils::ToLower(subText);
+ if (currentStyle & FONT_STYLE_CAPITALIZE)
+ StringUtils::ToCapitalize(subText);
+ AppendToUTF32(subText, ((currentStyle & FONT_STYLE_MASK) << 24) | (currentColor << 16), parsedText);
+ if (newLine)
+ parsedText.push_back(L'\n');
+ for (int i = 0; i < tabs; ++i)
+ parsedText.push_back(L'\t');
+
+ // and switch to the new style
+ startPos = pos;
+ currentColor = newColor;
+ if (on)
+ currentStyle |= newStyle;
+ else
+ currentStyle &= ~newStyle;
+ }
+ pos = text.find(L'[', pos);
+ }
+ // now grab the remainder of the string
+ std::wstring subText = text.substr(startPos);
+ if (currentStyle & FONT_STYLE_UPPERCASE)
+ StringUtils::ToUpper(subText);
+ if (currentStyle & FONT_STYLE_LOWERCASE)
+ StringUtils::ToLower(subText);
+ if (currentStyle & FONT_STYLE_CAPITALIZE)
+ StringUtils::ToCapitalize(subText);
+ AppendToUTF32(subText, ((currentStyle & FONT_STYLE_MASK) << 24) | (currentColor << 16), parsedText);
+}
+
+void CGUITextLayout::SetMaxHeight(float fHeight)
+{
+ m_maxHeight = fHeight;
+}
+
+void CGUITextLayout::WrapText(const vecText &text, float maxWidth)
+{
+ if (!m_font)
+ return;
+
+ int nMaxLines = (m_maxHeight > 0 && m_font->GetLineHeight() > 0)?(int)ceilf(m_maxHeight / m_font->GetLineHeight()):-1;
+
+ m_lines.clear();
+
+ std::vector<CGUIString> lines;
+ LineBreakText(text, lines);
+
+ for (unsigned int i = 0; i < lines.size(); i++)
+ {
+ const CGUIString &line = lines[i];
+ vecText::const_iterator lastSpace = line.m_text.begin();
+ vecText::const_iterator pos = line.m_text.begin();
+ unsigned int lastSpaceInLine = 0;
+ vecText curLine;
+ while (pos != line.m_text.end())
+ {
+ // Get the current letter in the string
+ character_t letter = *pos;
+ // check for a space
+ if (CanWrapAtLetter(letter))
+ {
+ float width = m_font->GetTextWidth(curLine);
+ if (width > maxWidth)
+ {
+ if (lastSpace != line.m_text.begin() && lastSpaceInLine > 0)
+ {
+ CGUIString string(curLine.begin(), curLine.begin() + lastSpaceInLine, false);
+ m_lines.push_back(string);
+ // check for exceeding our number of lines
+ if (nMaxLines > 0 && m_lines.size() >= (size_t)nMaxLines)
+ return;
+ // skip over spaces
+ pos = lastSpace;
+ while (pos != line.m_text.end() && IsSpace(*pos))
+ ++pos;
+ curLine.clear();
+ lastSpaceInLine = 0;
+ lastSpace = line.m_text.begin();
+ continue;
+ }
+ }
+ lastSpace = pos;
+ lastSpaceInLine = curLine.size();
+ }
+ curLine.push_back(letter);
+ ++pos;
+ }
+ // now add whatever we have left to the string
+ float width = m_font->GetTextWidth(curLine);
+ if (width > maxWidth)
+ {
+ // too long - put up to the last space on if we can + remove it from what's left.
+ if (lastSpace != line.m_text.begin() && lastSpaceInLine > 0)
+ {
+ CGUIString string(curLine.begin(), curLine.begin() + lastSpaceInLine, false);
+ m_lines.push_back(string);
+ // check for exceeding our number of lines
+ if (nMaxLines > 0 && m_lines.size() >= (size_t)nMaxLines)
+ return;
+ curLine.erase(curLine.begin(), curLine.begin() + lastSpaceInLine);
+ while (curLine.size() && IsSpace(curLine.at(0)))
+ curLine.erase(curLine.begin());
+ }
+ }
+ CGUIString string(curLine.begin(), curLine.end(), true);
+ m_lines.push_back(string);
+ // check for exceeding our number of lines
+ if (nMaxLines > 0 && m_lines.size() >= (size_t)nMaxLines)
+ return;
+ }
+}
+
+void CGUITextLayout::LineBreakText(const vecText &text, std::vector<CGUIString> &lines)
+{
+ int nMaxLines = (m_maxHeight > 0 && m_font && m_font->GetLineHeight() > 0)?(int)ceilf(m_maxHeight / m_font->GetLineHeight()):-1;
+ vecText::const_iterator lineStart = text.begin();
+ vecText::const_iterator pos = text.begin();
+ while (pos != text.end() && (nMaxLines <= 0 || lines.size() < (size_t)nMaxLines))
+ {
+ // Get the current letter in the string
+ character_t letter = *pos;
+
+ // Handle the newline character
+ if ((letter & 0xffff) == L'\n' )
+ { // push back everything up till now
+ CGUIString string(lineStart, pos, true);
+ lines.push_back(string);
+ lineStart = pos + 1;
+ }
+ ++pos;
+ }
+ // handle the last line if non-empty
+ if (lineStart < text.end() && (nMaxLines <= 0 || lines.size() < (size_t)nMaxLines))
+ {
+ CGUIString string(lineStart, text.end(), true);
+ lines.push_back(string);
+ }
+}
+
+void CGUITextLayout::GetTextExtent(float &width, float &height) const
+{
+ width = m_textWidth;
+ height = m_textHeight;
+}
+
+void CGUITextLayout::CalcTextExtent()
+{
+ m_textWidth = 0;
+ m_textHeight = 0;
+ if (!m_font) return;
+
+ for (const auto& string : m_lines)
+ {
+ float w = m_font->GetTextWidth(string.m_text);
+ if (w > m_textWidth)
+ m_textWidth = w;
+ }
+ m_textHeight = m_font->GetTextHeight(m_lines.size());
+}
+
+unsigned int CGUITextLayout::GetTextLength() const
+{
+ unsigned int length = 0;
+ for (const auto& string : m_lines)
+ length += string.m_text.size();
+ return length;
+}
+
+void CGUITextLayout::GetFirstText(vecText &text) const
+{
+ text.clear();
+ if (m_lines.size())
+ text = m_lines[0].m_text;
+}
+
+float CGUITextLayout::GetTextWidth(const std::wstring &text) const
+{
+ // NOTE: Assumes a single line of text
+ if (!m_font) return 0;
+ vecText utf32;
+ AppendToUTF32(text, (m_font->GetStyle() & FONT_STYLE_MASK) << 24, utf32);
+ return m_font->GetTextWidth(utf32);
+}
+
+std::string CGUITextLayout::GetText() const
+{
+ if (m_lastUpdateW)
+ {
+ std::string utf8;
+ g_charsetConverter.wToUTF8(m_lastText, utf8);
+ return utf8;
+ }
+ return m_lastUtf8Text;
+}
+
+void CGUITextLayout::DrawText(CGUIFont* font,
+ float x,
+ float y,
+ UTILS::COLOR::Color color,
+ UTILS::COLOR::Color shadowColor,
+ const std::string& text,
+ uint32_t align)
+{
+ if (!font) return;
+ vecText utf32;
+ AppendToUTF32(text, 0, utf32);
+ font->DrawText(x, y, color, shadowColor, utf32, align, 0);
+}
+
+void CGUITextLayout::AppendToUTF32(const std::wstring &utf16, character_t colStyle, vecText &utf32)
+{
+ // NOTE: Assumes a single line of text
+ utf32.reserve(utf32.size() + utf16.size());
+ for (unsigned int i = 0; i < utf16.size(); i++)
+ utf32.push_back(utf16[i] | colStyle);
+}
+
+void CGUITextLayout::AppendToUTF32(const std::string &utf8, character_t colStyle, vecText &utf32)
+{
+ std::wstring utf16;
+ // no need to bidiflip here - it's done in BidiTransform above
+ g_charsetConverter.utf8ToW(utf8, utf16, false);
+ AppendToUTF32(utf16, colStyle, utf32);
+}
+
+void CGUITextLayout::Reset()
+{
+ m_lines.clear();
+ m_lastText.clear();
+ m_lastUtf8Text.clear();
+ m_textWidth = m_textHeight = 0;
+}
+
+
diff --git a/xbmc/guilib/GUITextLayout.h b/xbmc/guilib/GUITextLayout.h
new file mode 100644
index 0000000..a3768a4
--- /dev/null
+++ b/xbmc/guilib/GUITextLayout.h
@@ -0,0 +1,195 @@
+/*
+ * 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 "utils/ColorUtils.h"
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+#ifdef __GNUC__
+// under gcc, inline will only take place if optimizations are applied (-O). this will force inline even without optimizations.
+#define XBMC_FORCE_INLINE __attribute__((always_inline))
+#else
+#define XBMC_FORCE_INLINE
+#endif
+
+class CGUIFont;
+class CScrollInfo;
+
+// Process will be:
+
+// 1. String is divided up into a "multiinfo" vector via the infomanager.
+// 2. The multiinfo vector is then parsed by the infomanager at rendertime and the resultant string is constructed.
+// 3. This is saved for comparison perhaps. If the same, we are done. If not, go to 4.
+// 4. The string is then parsed into a vector<CGUIString>.
+// 5. Each item in the vector is length-calculated, and then layout occurs governed by alignment and wrapping rules.
+// 6. A new vector<CGUIString> is constructed
+
+typedef uint32_t character_t;
+typedef std::vector<character_t> vecText;
+
+class CGUIString
+{
+public:
+ typedef vecText::const_iterator iString;
+
+ CGUIString(iString start, iString end, bool carriageReturn);
+
+ std::string GetAsString() const;
+
+ vecText m_text;
+ bool m_carriageReturn; // true if we have a carriage return here
+};
+
+class CGUITextLayout
+{
+public:
+ CGUITextLayout(CGUIFont *font, bool wrap, float fHeight=0.0f, CGUIFont *borderFont = NULL); // this may need changing - we may just use this class to replace CLabelInfo completely
+
+ bool UpdateScrollinfo(CScrollInfo &scrollInfo);
+
+ // main function to render strings
+ void Render(float x,
+ float y,
+ float angle,
+ UTILS::COLOR::Color color,
+ UTILS::COLOR::Color shadowColor,
+ uint32_t alignment,
+ float maxWidth,
+ bool solid = false);
+ void RenderScrolling(float x,
+ float y,
+ float angle,
+ UTILS::COLOR::Color color,
+ UTILS::COLOR::Color shadowColor,
+ uint32_t alignment,
+ float maxWidth,
+ const CScrollInfo& scrollInfo);
+ void RenderOutline(float x,
+ float y,
+ UTILS::COLOR::Color color,
+ UTILS::COLOR::Color outlineColor,
+ uint32_t alignment,
+ float maxWidth);
+
+ /*! \brief Returns the precalculated width and height of the text to be rendered (in constant time).
+ \param width [out] width of text
+ \param height [out] height of text
+ \sa GetTextWidth, CalcTextExtent
+ */
+ void GetTextExtent(float &width, float &height) const;
+
+ /*! \brief Returns the precalculated width of the text to be rendered (in constant time).
+ \return width of text
+ \sa GetTextExtent, CalcTextExtent
+ */
+ float GetTextWidth() const { return m_textWidth; }
+
+ float GetTextWidth(const std::wstring &text) const;
+
+ /*! \brief Returns the precalculated height of the text to be rendered (in constant time).
+ \return height of text
+ */
+ float GetTextHeight() const { return m_textHeight; }
+
+ bool Update(const std::string &text, float maxWidth = 0, bool forceUpdate = false, bool forceLTRReadingOrder = false);
+ bool UpdateW(const std::wstring &text, float maxWidth = 0, bool forceUpdate = false, bool forceLTRReadingOrder = false);
+
+ /*! \brief Update text from a pre-styled vecText/std::vector<UTILS::COLOR::Color> combination
+ Allows styled text to be passed directly to the text layout.
+ \param text the styled text to set.
+ \param colors the colors used on the text.
+ \param maxWidth the maximum width for wrapping text, defaults to 0 (no max width).
+ \param forceLTRReadingOrder whether to force left to right reading order, defaults to false.
+ */
+ void UpdateStyled(const vecText& text,
+ const std::vector<UTILS::COLOR::Color>& colors,
+ float maxWidth = 0,
+ bool forceLTRReadingOrder = false);
+
+ unsigned int GetTextLength() const;
+ void GetFirstText(vecText &text) const;
+ void Reset();
+
+ void SetWrap(bool bWrap=true);
+ void SetMaxHeight(float fHeight);
+
+
+ static void DrawText(CGUIFont* font,
+ float x,
+ float y,
+ UTILS::COLOR::Color color,
+ UTILS::COLOR::Color shadowColor,
+ const std::string& text,
+ uint32_t align);
+ static void Filter(std::string &text);
+
+protected:
+ void LineBreakText(const vecText &text, std::vector<CGUIString> &lines);
+ void WrapText(const vecText &text, float maxWidth);
+ static void BidiTransform(std::vector<CGUIString> &lines, bool forceLTRReadingOrder);
+ static std::wstring BidiFlip(const std::wstring& text,
+ bool forceLTRReadingOrder,
+ int* visualToLogicalMap = nullptr);
+ void CalcTextExtent();
+ void UpdateCommon(const std::wstring &text, float maxWidth, bool forceLTRReadingOrder);
+
+ /*! \brief Returns the text, utf8 encoded
+ \return utf8 text
+ */
+ std::string GetText() const;
+
+ //! \brief Set the monospaced font to use
+ void SetMonoFont(CGUIFont* font) { m_monoFont = font; }
+
+ //! \brief Set whether or not to use the monospace font
+ void UseMonoFont(bool use) { m_font = use && m_monoFont ? m_monoFont : m_varFont; }
+
+ // our text to render
+ std::vector<UTILS::COLOR::Color> m_colors;
+ std::vector<CGUIString> m_lines;
+ typedef std::vector<CGUIString>::iterator iLine;
+
+ // the layout and font details
+ CGUIFont *m_font; // has style, colour info
+ CGUIFont *m_borderFont; // only used for outlined text
+ CGUIFont* m_monoFont = nullptr; //!< Mono-space font to use
+ CGUIFont* m_varFont; //!< Varible-space font to use
+
+ bool m_wrap; // wrapping (true if justify is enabled!)
+ float m_maxHeight;
+ // the default color (may differ from the font objects defaults)
+ UTILS::COLOR::Color m_textColor;
+
+ std::string m_lastUtf8Text;
+ std::wstring m_lastText;
+ bool m_lastUpdateW; ///< true if the last string we updated was the wstring version
+ float m_textWidth;
+ float m_textHeight;
+private:
+ inline bool IsSpace(character_t letter) const XBMC_FORCE_INLINE
+ {
+ return (letter & 0xffff) == L' ';
+ };
+ inline bool CanWrapAtLetter(character_t letter) const XBMC_FORCE_INLINE
+ {
+ character_t ch = letter & 0xffff;
+ return ch == L' ' || (ch >=0x4e00 && ch <= 0x9fff);
+ };
+ static void AppendToUTF32(const std::string &utf8, character_t colStyle, vecText &utf32);
+ static void AppendToUTF32(const std::wstring &utf16, character_t colStyle, vecText &utf32);
+ static void ParseText(const std::wstring& text,
+ uint32_t defaultStyle,
+ UTILS::COLOR::Color defaultColor,
+ std::vector<UTILS::COLOR::Color>& colors,
+ vecText& parsedText);
+};
+
diff --git a/xbmc/guilib/GUITexture.cpp b/xbmc/guilib/GUITexture.cpp
new file mode 100644
index 0000000..7f2f51d
--- /dev/null
+++ b/xbmc/guilib/GUITexture.cpp
@@ -0,0 +1,714 @@
+/*
+ * 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 "GUITexture.h"
+
+#include "GUILargeTextureManager.h"
+#include "TextureManager.h"
+#include "utils/MathUtils.h"
+#include "utils/StringUtils.h"
+#include "windowing/GraphicContext.h"
+
+#include <stdexcept>
+
+CreateGUITextureFunc CGUITexture::m_createGUITextureFunc;
+DrawQuadFunc CGUITexture::m_drawQuadFunc;
+
+CTextureInfo::CTextureInfo()
+{
+ orientation = 0;
+ useLarge = false;
+}
+
+CTextureInfo::CTextureInfo(const std::string &file):
+ filename(file)
+{
+ orientation = 0;
+ useLarge = false;
+}
+
+void CGUITexture::Register(const CreateGUITextureFunc& createFunction,
+ const DrawQuadFunc& drawQuadFunction)
+{
+ m_createGUITextureFunc = createFunction;
+ m_drawQuadFunc = drawQuadFunction;
+}
+
+CGUITexture* CGUITexture::CreateTexture(
+ float posX, float posY, float width, float height, const CTextureInfo& texture)
+{
+ if (!m_createGUITextureFunc)
+ throw std::runtime_error(
+ "No GUITexture Create function available. Did you forget to register?");
+
+ return m_createGUITextureFunc(posX, posY, width, height, texture);
+}
+
+void CGUITexture::DrawQuad(const CRect& coords,
+ UTILS::COLOR::Color color,
+ CTexture* texture,
+ const CRect* texCoords)
+{
+ if (!m_drawQuadFunc)
+ throw std::runtime_error(
+ "No GUITexture DrawQuad function available. Did you forget to register?");
+
+ m_drawQuadFunc(coords, color, texture, texCoords);
+}
+
+CGUITexture::CGUITexture(
+ float posX, float posY, float width, float height, const CTextureInfo& texture)
+ : m_height(height), m_info(texture)
+{
+ m_posX = posX;
+ m_posY = posY;
+ m_width = width;
+
+ // defaults
+ m_visible = true;
+ m_diffuseColor = 0xffffffff;
+ m_alpha = 0xff;
+
+ m_vertex.SetRect(m_posX, m_posY, m_posX + m_width, m_posY + m_height);
+
+ m_frameWidth = 0;
+ m_frameHeight = 0;
+
+ m_texCoordsScaleU = 1.0f;
+ m_texCoordsScaleV = 1.0f;
+ m_diffuseU = 1.0f;
+ m_diffuseV = 1.0f;
+ m_diffuseScaleU = 1.0f;
+ m_diffuseScaleV = 1.0f;
+
+ // anim gifs
+ ResetAnimState();
+
+ m_allocateDynamically = false;
+ m_isAllocated = NO;
+ m_invalid = true;
+ m_use_cache = true;
+}
+
+CGUITexture::CGUITexture(const CGUITexture& right)
+ : m_visible(right.m_visible),
+ m_diffuseColor(right.m_diffuseColor),
+ m_posX(right.m_posX),
+ m_posY(right.m_posY),
+ m_width(right.m_width),
+ m_height(right.m_height),
+ m_use_cache(right.m_use_cache),
+ m_alpha(right.m_alpha),
+ m_allocateDynamically(right.m_allocateDynamically),
+ m_info(right.m_info),
+ m_aspect(right.m_aspect)
+{
+ // defaults
+ m_vertex.SetRect(m_posX, m_posY, m_posX + m_width, m_posY + m_height);
+
+ m_frameWidth = 0;
+ m_frameHeight = 0;
+
+ m_texCoordsScaleU = 1.0f;
+ m_texCoordsScaleV = 1.0f;
+ m_diffuseU = 1.0f;
+ m_diffuseV = 1.0f;
+ m_diffuseScaleU = 1.0f;
+ m_diffuseScaleV = 1.0f;
+
+ ResetAnimState();
+
+ m_isAllocated = NO;
+ m_invalid = true;
+}
+
+bool CGUITexture::AllocateOnDemand()
+{
+ if (m_visible)
+ { // visible, so make sure we're allocated
+ if (!IsAllocated() || (m_isAllocated == LARGE && !m_texture.size()))
+ return AllocResources();
+ }
+ else
+ { // hidden, so deallocate as applicable
+ if (m_allocateDynamically && IsAllocated())
+ FreeResources();
+ // reset animated textures (animgifs)
+ ResetAnimState();
+ }
+
+ return false;
+}
+
+bool CGUITexture::Process(unsigned int currentTime)
+{
+ bool changed = false;
+ // check if we need to allocate our resources
+ changed |= AllocateOnDemand();
+
+ if (m_texture.size() > 1)
+ changed |= UpdateAnimFrame(currentTime);
+
+ if (m_invalid)
+ changed |= CalculateSize();
+
+ if (m_isAllocated)
+ changed |= !ReadyToRender();
+
+ return changed;
+}
+
+void CGUITexture::Render()
+{
+ if (!m_visible || !m_texture.size())
+ return;
+
+ // see if we need to clip the image
+ if (m_vertex.Width() > m_width || m_vertex.Height() > m_height)
+ {
+ if (!CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height))
+ return;
+ }
+
+ // set our draw color
+ #define MIX_ALPHA(a,c) (((a * (c >> 24)) / 255) << 24) | (c & 0x00ffffff)
+
+ // diffuse color
+ UTILS::COLOR::Color color =
+ (m_info.diffuseColor) ? (UTILS::COLOR::Color)m_info.diffuseColor : m_diffuseColor;
+ if (m_alpha != 0xFF)
+ color = MIX_ALPHA(m_alpha, color);
+
+ color = CServiceBroker::GetWinSystem()->GetGfxContext().MergeColor(color);
+
+ // setup our renderer
+ Begin(color);
+
+ // compute the texture coordinates
+ float u1, u2, u3, v1, v2, v3;
+ u1 = m_info.border.x1;
+ u2 = m_frameWidth - m_info.border.x2;
+ u3 = m_frameWidth;
+ v1 = m_info.border.y1;
+ v2 = m_frameHeight - m_info.border.y2;
+ v3 = m_frameHeight;
+
+ if (!m_texture.m_texCoordsArePixels)
+ {
+ u1 *= m_texCoordsScaleU;
+ u2 *= m_texCoordsScaleU;
+ u3 *= m_texCoordsScaleU;
+ v1 *= m_texCoordsScaleV;
+ v2 *= m_texCoordsScaleV;
+ v3 *= m_texCoordsScaleV;
+ }
+
+ //! @todo The diffuse coloring applies to all vertices, which will
+ //! look weird for stuff with borders, as will the -ve height/width
+ //! for flipping
+
+ // left segment (0,0,u1,v3)
+ if (m_info.border.x1)
+ {
+ if (m_info.border.y1)
+ Render(m_vertex.x1, m_vertex.y1, m_vertex.x1 + m_info.border.x1, m_vertex.y1 + m_info.border.y1, 0, 0, u1, v1, u3, v3);
+ Render(m_vertex.x1, m_vertex.y1 + m_info.border.y1, m_vertex.x1 + m_info.border.x1, m_vertex.y2 - m_info.border.y2, 0, v1, u1, v2, u3, v3);
+ if (m_info.border.y2)
+ Render(m_vertex.x1, m_vertex.y2 - m_info.border.y2, m_vertex.x1 + m_info.border.x1, m_vertex.y2, 0, v2, u1, v3, u3, v3);
+ }
+ // middle segment (u1,0,u2,v3)
+ if (m_info.border.y1)
+ Render(m_vertex.x1 + m_info.border.x1, m_vertex.y1, m_vertex.x2 - m_info.border.x2, m_vertex.y1 + m_info.border.y1, u1, 0, u2, v1, u3, v3);
+ if (m_info.m_infill)
+ Render(m_vertex.x1 + m_info.border.x1, m_vertex.y1 + m_info.border.y1,
+ m_vertex.x2 - m_info.border.x2, m_vertex.y2 - m_info.border.y2, u1, v1, u2, v2, u3, v3);
+ if (m_info.border.y2)
+ Render(m_vertex.x1 + m_info.border.x1, m_vertex.y2 - m_info.border.y2, m_vertex.x2 - m_info.border.x2, m_vertex.y2, u1, v2, u2, v3, u3, v3);
+ // right segment
+ if (m_info.border.x2)
+ { // have a left border
+ if (m_info.border.y1)
+ Render(m_vertex.x2 - m_info.border.x2, m_vertex.y1, m_vertex.x2, m_vertex.y1 + m_info.border.y1, u2, 0, u3, v1, u3, v3);
+ Render(m_vertex.x2 - m_info.border.x2, m_vertex.y1 + m_info.border.y1, m_vertex.x2, m_vertex.y2 - m_info.border.y2, u2, v1, u3, v2, u3, v3);
+ if (m_info.border.y2)
+ Render(m_vertex.x2 - m_info.border.x2, m_vertex.y2 - m_info.border.y2, m_vertex.x2, m_vertex.y2, u2, v2, u3, v3, u3, v3);
+ }
+
+ // close off our renderer
+ End();
+
+ if (m_vertex.Width() > m_width || m_vertex.Height() > m_height)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+}
+
+void CGUITexture::Render(float left,
+ float top,
+ float right,
+ float bottom,
+ float u1,
+ float v1,
+ float u2,
+ float v2,
+ float u3,
+ float v3)
+{
+ CRect diffuse(u1, v1, u2, v2);
+ CRect texture(u1, v1, u2, v2);
+ CRect vertex(left, top, right, bottom);
+ CServiceBroker::GetWinSystem()->GetGfxContext().ClipRect(vertex, texture, m_diffuse.size() ? &diffuse : NULL);
+
+ if (vertex.IsEmpty())
+ return; // nothing to render
+
+ int orientation = GetOrientation();
+ OrientateTexture(texture, u3, v3, orientation);
+
+ if (m_diffuse.size())
+ {
+ // flip the texture as necessary. Diffuse just gets flipped according to m_info.orientation.
+ // Main texture gets flipped according to GetOrientation().
+ diffuse.x1 *= m_diffuseScaleU / u3; diffuse.x2 *= m_diffuseScaleU / u3;
+ diffuse.y1 *= m_diffuseScaleV / v3; diffuse.y2 *= m_diffuseScaleV / v3;
+ diffuse += m_diffuseOffset;
+ OrientateTexture(diffuse, m_diffuseU, m_diffuseV, m_info.orientation);
+ }
+
+ float x[4], y[4], z[4];
+
+#define ROUND_TO_PIXEL(x) static_cast<float>(MathUtils::round_int(static_cast<double>(x)))
+
+ x[0] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(vertex.x1, vertex.y1));
+ y[0] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(vertex.x1, vertex.y1));
+ z[0] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalZCoord(vertex.x1, vertex.y1));
+ x[1] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(vertex.x2, vertex.y1));
+ y[1] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(vertex.x2, vertex.y1));
+ z[1] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalZCoord(vertex.x2, vertex.y1));
+ x[2] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(vertex.x2, vertex.y2));
+ y[2] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(vertex.x2, vertex.y2));
+ z[2] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalZCoord(vertex.x2, vertex.y2));
+ x[3] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(vertex.x1, vertex.y2));
+ y[3] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(vertex.x1, vertex.y2));
+ z[3] = ROUND_TO_PIXEL(CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalZCoord(vertex.x1, vertex.y2));
+
+ if (y[2] == y[0]) y[2] += 1.0f;
+ if (x[2] == x[0]) x[2] += 1.0f;
+ if (y[3] == y[1]) y[3] += 1.0f;
+ if (x[3] == x[1]) x[3] += 1.0f;
+
+ Draw(x, y, z, texture, diffuse, orientation);
+}
+
+bool CGUITexture::AllocResources()
+{
+ if (m_info.filename.empty())
+ return false;
+
+ if (m_texture.size())
+ return false; // already have our texture
+
+ // reset our animstate
+ ResetAnimState();
+
+ bool changed = false;
+ bool useLarge = m_info.useLarge || !CServiceBroker::GetGUI()->GetTextureManager().CanLoad(m_info.filename);
+ if (useLarge)
+ { // we want to use the large image loader, but we first check for bundled textures
+ if (!IsAllocated())
+ {
+ CTextureArray texture;
+ texture = CServiceBroker::GetGUI()->GetTextureManager().Load(m_info.filename, true);
+ if (texture.size())
+ {
+ m_isAllocated = NORMAL;
+ m_texture = texture;
+ changed = true;
+ }
+ }
+ if (m_isAllocated != NORMAL)
+ { // use our large image background loader
+ CTextureArray texture;
+ if (CServiceBroker::GetGUI()->GetLargeTextureManager().GetImage(m_info.filename, texture, !IsAllocated(), m_use_cache))
+ {
+ m_isAllocated = LARGE;
+
+ if (!texture.size()) // not ready as yet
+ return false;
+
+ m_texture = texture;
+
+ changed = true;
+ }
+ else
+ m_isAllocated = LARGE_FAILED;
+ }
+ }
+ else if (!IsAllocated())
+ {
+ CTextureArray texture = CServiceBroker::GetGUI()->GetTextureManager().Load(m_info.filename);
+
+ // set allocated to true even if we couldn't load the image to save
+ // us hitting the disk every frame
+ m_isAllocated = texture.size() ? NORMAL : NORMAL_FAILED;
+ if (!texture.size())
+ return false;
+ m_texture = texture;
+ changed = true;
+ }
+ m_frameWidth = (float)m_texture.m_width;
+ m_frameHeight = (float)m_texture.m_height;
+
+ // load the diffuse texture (if necessary)
+ if (!m_info.diffuse.empty())
+ {
+ m_diffuse = CServiceBroker::GetGUI()->GetTextureManager().Load(m_info.diffuse);
+ }
+
+ CalculateSize();
+
+ // call our implementation
+ Allocate();
+
+ return changed;
+}
+
+bool CGUITexture::CalculateSize()
+{
+ if (m_currentFrame >= m_texture.size())
+ return false;
+
+ m_texCoordsScaleU = 1.0f / m_texture.m_texWidth;
+ m_texCoordsScaleV = 1.0f / m_texture.m_texHeight;
+
+ if (m_width == 0)
+ m_width = m_frameWidth;
+ if (m_height == 0)
+ m_height = m_frameHeight;
+
+ float newPosX = m_posX;
+ float newPosY = m_posY;
+ float newWidth = m_width;
+ float newHeight = m_height;
+
+ if (m_aspect.ratio != CAspectRatio::AR_STRETCH && m_frameWidth && m_frameHeight)
+ {
+ // to get the pixel ratio, we must use the SCALED output sizes
+ float pixelRatio = CServiceBroker::GetWinSystem()->GetGfxContext().GetScalingPixelRatio();
+
+ float fSourceFrameRatio = m_frameWidth / m_frameHeight;
+ if (GetOrientation() & 4)
+ fSourceFrameRatio = m_frameHeight / m_frameWidth;
+ float fOutputFrameRatio = fSourceFrameRatio / pixelRatio;
+
+ // maximize the width
+ newHeight = m_width / fOutputFrameRatio;
+
+ if ((m_aspect.ratio == CAspectRatio::AR_SCALE && newHeight < m_height) ||
+ (m_aspect.ratio == CAspectRatio::AR_KEEP && newHeight > m_height))
+ {
+ newHeight = m_height;
+ newWidth = newHeight * fOutputFrameRatio;
+ }
+ if (m_aspect.ratio == CAspectRatio::AR_CENTER)
+ { // keep original size + center
+ newWidth = m_frameWidth / sqrt(pixelRatio);
+ newHeight = m_frameHeight * sqrt(pixelRatio);
+ }
+
+ if (m_aspect.align & ASPECT_ALIGN_LEFT)
+ newPosX = m_posX;
+ else if (m_aspect.align & ASPECT_ALIGN_RIGHT)
+ newPosX = m_posX + m_width - newWidth;
+ else
+ newPosX = m_posX + (m_width - newWidth) * 0.5f;
+ if (m_aspect.align & ASPECT_ALIGNY_TOP)
+ newPosY = m_posY;
+ else if (m_aspect.align & ASPECT_ALIGNY_BOTTOM)
+ newPosY = m_posY + m_height - newHeight;
+ else
+ newPosY = m_posY + (m_height - newHeight) * 0.5f;
+ }
+
+ m_vertex.SetRect(newPosX, newPosY, newPosX + newWidth, newPosY + newHeight);
+
+ // scale the diffuse coords as well
+ if (m_diffuse.size())
+ { // calculate scaling for the texcoords
+ if (m_diffuse.m_texCoordsArePixels)
+ {
+ m_diffuseU = float(m_diffuse.m_width);
+ m_diffuseV = float(m_diffuse.m_height);
+ }
+ else
+ {
+ m_diffuseU = float(m_diffuse.m_width) / float(m_diffuse.m_texWidth);
+ m_diffuseV = float(m_diffuse.m_height) / float(m_diffuse.m_texHeight);
+ }
+
+ if (m_aspect.scaleDiffuse)
+ {
+ m_diffuseScaleU = m_diffuseU;
+ m_diffuseScaleV = m_diffuseV;
+ m_diffuseOffset = CPoint(0,0);
+ }
+ else // stretching diffuse
+ { // scale diffuse up or down to match output rect size, rather than image size
+ //(m_fX, mfY) -> (m_fX + m_fNW, m_fY + m_fNH)
+ //(0,0) -> (m_fU*m_diffuseScaleU, m_fV*m_diffuseScaleV)
+ // x = u/(m_fU*m_diffuseScaleU)*m_fNW + m_fX
+ // -> u = (m_posX - m_fX) * m_fU * m_diffuseScaleU / m_fNW
+ m_diffuseScaleU = m_diffuseU * m_vertex.Width() / m_width;
+ m_diffuseScaleV = m_diffuseV * m_vertex.Height() / m_height;
+ m_diffuseOffset = CPoint((m_vertex.x1 - m_posX) / m_vertex.Width() * m_diffuseScaleU, (m_vertex.y1 - m_posY) / m_vertex.Height() * m_diffuseScaleV);
+ }
+ }
+
+ m_invalid = false;
+ return true;
+}
+
+void CGUITexture::FreeResources(bool immediately /* = false */)
+{
+ if (m_isAllocated == LARGE || m_isAllocated == LARGE_FAILED)
+ CServiceBroker::GetGUI()->GetLargeTextureManager().ReleaseImage(m_info.filename, immediately || (m_isAllocated == LARGE_FAILED));
+ else if (m_isAllocated == NORMAL && m_texture.size())
+ CServiceBroker::GetGUI()->GetTextureManager().ReleaseTexture(m_info.filename, immediately);
+
+ if (m_diffuse.size())
+ CServiceBroker::GetGUI()->GetTextureManager().ReleaseTexture(m_info.diffuse, immediately);
+ m_diffuse.Reset();
+
+ m_texture.Reset();
+
+ ResetAnimState();
+
+ m_texCoordsScaleU = 1.0f;
+ m_texCoordsScaleV = 1.0f;
+
+ // call our implementation
+ Free();
+
+ m_isAllocated = NO;
+}
+
+void CGUITexture::DynamicResourceAlloc(bool allocateDynamically)
+{
+ m_allocateDynamically = allocateDynamically;
+}
+
+void CGUITexture::SetInvalid()
+{
+ m_invalid = true;
+}
+
+bool CGUITexture::UpdateAnimFrame(unsigned int currentTime)
+{
+ bool changed = false;
+ unsigned int delay = m_texture.m_delays[m_currentFrame];
+
+ if (m_lasttime == 0)
+ {
+ m_lasttime = currentTime;
+ }
+ else
+ {
+ if ((currentTime - m_lasttime) >= delay)
+ {
+ if (m_currentFrame + 1 >= m_texture.size())
+ {
+ if (m_texture.m_loops > 0)
+ {
+ if (m_currentLoop + 1 < m_texture.m_loops)
+ {
+ m_currentLoop++;
+ m_currentFrame = 0;
+ m_lasttime = currentTime;
+ changed = true;
+ }
+ }
+ else
+ {
+ // 0 == loop forever
+ m_currentFrame = 0;
+ m_lasttime = currentTime;
+ changed = true;
+ }
+ }
+ else
+ {
+ m_currentFrame++;
+ m_lasttime = currentTime;
+ changed = true;
+ }
+ }
+ }
+
+ return changed;
+}
+
+bool CGUITexture::SetVisible(bool visible)
+{
+ bool changed = m_visible != visible;
+ m_visible = visible;
+ return changed;
+}
+
+bool CGUITexture::SetAlpha(unsigned char alpha)
+{
+ bool changed = m_alpha != alpha;
+ m_alpha = alpha;
+ return changed;
+}
+
+bool CGUITexture::SetDiffuseColor(UTILS::COLOR::Color color,
+ const CGUIListItem* item /* = nullptr */)
+{
+ bool changed = m_diffuseColor != color;
+ m_diffuseColor = color;
+ changed |= m_info.diffuseColor.Update(item);
+ return changed;
+}
+
+bool CGUITexture::ReadyToRender() const
+{
+ return m_texture.size() > 0;
+}
+
+void CGUITexture::OrientateTexture(CRect& rect, float width, float height, int orientation)
+{
+ switch (orientation & 3)
+ {
+ case 0:
+ // default
+ break;
+ case 1:
+ // flip in X direction
+ rect.x1 = width - rect.x1;
+ rect.x2 = width - rect.x2;
+ break;
+ case 2:
+ // rotate 180 degrees
+ rect.x1 = width - rect.x1;
+ rect.x2 = width - rect.x2;
+ rect.y1 = height - rect.y1;
+ rect.y2 = height - rect.y2;
+ break;
+ case 3:
+ // flip in Y direction
+ rect.y1 = height - rect.y1;
+ rect.y2 = height - rect.y2;
+ break;
+ }
+ if (orientation & 4)
+ {
+ // we need to swap x and y coordinates but only within the width,height block
+ float temp = rect.x1;
+ rect.x1 = rect.y1 * width/height;
+ rect.y1 = temp * height/width;
+ temp = rect.x2;
+ rect.x2 = rect.y2 * width/height;
+ rect.y2 = temp * height/width;
+ }
+}
+
+void CGUITexture::ResetAnimState()
+{
+ m_lasttime = 0;
+ m_currentFrame = 0;
+ m_currentLoop = 0;
+}
+
+bool CGUITexture::SetWidth(float width)
+{
+ if (width < m_info.border.x1 + m_info.border.x2)
+ width = m_info.border.x1 + m_info.border.x2;
+ if (m_width != width)
+ {
+ m_width = width;
+ m_invalid = true;
+ return true;
+ }
+ else
+ return false;
+}
+
+bool CGUITexture::SetHeight(float height)
+{
+ if (height < m_info.border.y1 + m_info.border.y2)
+ height = m_info.border.y1 + m_info.border.y2;
+ if (m_height != height)
+ {
+ m_height = height;
+ m_invalid = true;
+ return true;
+ }
+ else
+ return false;
+}
+
+bool CGUITexture::SetPosition(float posX, float posY)
+{
+ if (m_posX != posX || m_posY != posY)
+ {
+ m_posX = posX;
+ m_posY = posY;
+ m_invalid = true;
+ return true;
+ }
+ else
+ return false;
+}
+
+bool CGUITexture::SetAspectRatio(const CAspectRatio& aspect)
+{
+ if (m_aspect != aspect)
+ {
+ m_aspect = aspect;
+ m_invalid = true;
+ return true;
+ }
+ else
+ return false;
+}
+
+bool CGUITexture::SetFileName(const std::string& filename)
+{
+ if (m_info.filename == filename) return false;
+ // Don't completely free resources here - we may be just changing
+ // filenames mid-animation
+ FreeResources();
+ m_info.filename = filename;
+
+ // disable large loader and cache for gifs
+ if (StringUtils::EndsWithNoCase(m_info.filename, ".gif"))
+ {
+ m_info.useLarge = false;
+ SetUseCache(false);
+ }
+
+ // Don't allocate resources here as this is done at render time
+ return true;
+}
+
+void CGUITexture::SetUseCache(const bool useCache)
+{
+ m_use_cache = useCache;
+}
+
+int CGUITexture::GetOrientation() const
+{
+ // multiply our orientations
+ static char orient_table[] = { 0, 1, 2, 3, 4, 5, 6, 7,
+ 1, 0, 3, 2, 5, 4, 7, 6,
+ 2, 3, 0, 1, 6, 7, 4, 5,
+ 3, 2, 1, 0, 7, 6, 5, 4,
+ 4, 7, 6, 5, 0, 3, 2, 1,
+ 5, 6, 7, 4, 1, 2, 3, 0,
+ 6, 5, 4, 7, 2, 1, 0, 3,
+ 7, 4, 5, 6, 3, 0, 1, 2 };
+ return (int)orient_table[8 * m_info.orientation + m_texture.m_orientation];
+}
diff --git a/xbmc/guilib/GUITexture.h b/xbmc/guilib/GUITexture.h
new file mode 100644
index 0000000..e530233
--- /dev/null
+++ b/xbmc/guilib/GUITexture.h
@@ -0,0 +1,207 @@
+/*
+ * 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 "TextureManager.h"
+#include "guiinfo/GUIInfoColor.h"
+#include "utils/ColorUtils.h"
+#include "utils/Geometry.h"
+
+#include <functional>
+
+// image alignment for <aspect>keep</aspect>, <aspect>scale</aspect> or <aspect>center</aspect>
+#define ASPECT_ALIGN_CENTER 0
+#define ASPECT_ALIGN_LEFT 1
+#define ASPECT_ALIGN_RIGHT 2
+#define ASPECT_ALIGNY_CENTER 0
+#define ASPECT_ALIGNY_TOP 4
+#define ASPECT_ALIGNY_BOTTOM 8
+#define ASPECT_ALIGN_MASK 3
+#define ASPECT_ALIGNY_MASK ~3
+
+class CAspectRatio
+{
+public:
+ enum ASPECT_RATIO { AR_STRETCH = 0, AR_SCALE, AR_KEEP, AR_CENTER };
+ CAspectRatio(ASPECT_RATIO aspect = AR_STRETCH)
+ {
+ ratio = aspect;
+ align = ASPECT_ALIGN_CENTER | ASPECT_ALIGNY_CENTER;
+ scaleDiffuse = true;
+ };
+ bool operator!=(const CAspectRatio &right) const
+ {
+ if (ratio != right.ratio) return true;
+ if (align != right.align) return true;
+ if (scaleDiffuse != right.scaleDiffuse) return true;
+ return false;
+ };
+
+ ASPECT_RATIO ratio;
+ uint32_t align;
+ bool scaleDiffuse;
+};
+
+class CTextureInfo
+{
+public:
+ CTextureInfo();
+ explicit CTextureInfo(const std::string &file);
+ bool useLarge;
+ CRect border; // scaled - unneeded if we get rid of scale on load
+ bool m_infill{
+ true}; // if false, the main body of a texture is not drawn. useful for borders with no inner filling
+ int orientation; // orientation of the texture (0 - 7 == EXIForientation - 1)
+ std::string diffuse; // diffuse overlay texture
+ KODI::GUILIB::GUIINFO::CGUIInfoColor diffuseColor; // diffuse color
+ std::string filename; // main texture file
+};
+
+class CGUITexture;
+
+using CreateGUITextureFunc = std::function<CGUITexture*(
+ float posX, float posY, float width, float height, const CTextureInfo& texture)>;
+using DrawQuadFunc = std::function<void(
+ const CRect& coords, UTILS::COLOR::Color color, CTexture* texture, const CRect* texCoords)>;
+
+class CGUITexture
+{
+public:
+ virtual ~CGUITexture() = default;
+
+ static void Register(const CreateGUITextureFunc& createFunction,
+ const DrawQuadFunc& drawQuadFunction);
+
+ static CGUITexture* CreateTexture(
+ float posX, float posY, float width, float height, const CTextureInfo& texture);
+ virtual CGUITexture* Clone() const = 0;
+
+ static void DrawQuad(const CRect& coords,
+ UTILS::COLOR::Color color,
+ CTexture* texture = nullptr,
+ const CRect* texCoords = nullptr);
+
+ bool Process(unsigned int currentTime);
+ void Render();
+
+ void DynamicResourceAlloc(bool bOnOff);
+ bool AllocResources();
+ void FreeResources(bool immediately = false);
+ void SetInvalid();
+
+ bool SetVisible(bool visible);
+ bool SetAlpha(unsigned char alpha);
+ bool SetDiffuseColor(UTILS::COLOR::Color color, const CGUIListItem* item = nullptr);
+ bool SetPosition(float x, float y);
+ bool SetWidth(float width);
+ bool SetHeight(float height);
+ bool SetFileName(const std::string &filename);
+ void SetUseCache(const bool useCache = true);
+ bool SetAspectRatio(const CAspectRatio &aspect);
+
+ const std::string& GetFileName() const { return m_info.filename; }
+ float GetTextureWidth() const { return m_frameWidth; }
+ float GetTextureHeight() const { return m_frameHeight; }
+ float GetWidth() const { return m_width; }
+ float GetHeight() const { return m_height; }
+ float GetXPosition() const { return m_posX; }
+ float GetYPosition() const { return m_posY; }
+ int GetOrientation() const;
+ const CRect& GetRenderRect() const { return m_vertex; }
+ bool IsLazyLoaded() const { return m_info.useLarge; }
+
+ /*!
+ * @brief Get the diffuse color (info color) associated to this texture
+ * @return the infocolor associated to this texture
+ */
+ KODI::GUILIB::GUIINFO::CGUIInfoColor GetDiffuseColor() const { return m_info.diffuseColor; }
+
+ bool HitTest(const CPoint& point) const
+ {
+ return CRect(m_posX, m_posY, m_posX + m_width, m_posY + m_height).PtInRect(point);
+ }
+ bool IsAllocated() const { return m_isAllocated != NO; }
+ bool FailedToAlloc() const
+ {
+ return m_isAllocated == NORMAL_FAILED || m_isAllocated == LARGE_FAILED;
+ }
+ bool ReadyToRender() const;
+
+protected:
+ CGUITexture(float posX, float posY, float width, float height, const CTextureInfo& texture);
+ CGUITexture(const CGUITexture& left);
+
+ bool CalculateSize();
+ void LoadDiffuseImage();
+ bool AllocateOnDemand();
+ bool UpdateAnimFrame(unsigned int currentTime);
+ void Render(float left,
+ float top,
+ float right,
+ float bottom,
+ float u1,
+ float v1,
+ float u2,
+ float v2,
+ float u3,
+ float v3);
+ static void OrientateTexture(CRect &rect, float width, float height, int orientation);
+ void ResetAnimState();
+
+ // functions that our implementation classes handle
+ virtual void Allocate() {}; ///< called after our textures have been allocated
+ virtual void Free() {}; ///< called after our textures have been freed
+ virtual void Begin(UTILS::COLOR::Color color) = 0;
+ virtual void Draw(float* x,
+ float* y,
+ float* z,
+ const CRect& texture,
+ const CRect& diffuse,
+ int orientation) = 0;
+ virtual void End() = 0;
+
+ bool m_visible;
+ UTILS::COLOR::Color m_diffuseColor;
+
+ float m_posX; // size of the frame
+ float m_posY;
+ float m_width;
+ float m_height;
+
+ CRect m_vertex; // vertex coords to render
+ bool m_invalid; // if true, we need to recalculate
+ bool m_use_cache;
+ unsigned char m_alpha;
+
+ float m_frameWidth, m_frameHeight; // size in pixels of the actual frame within the texture
+ float m_texCoordsScaleU, m_texCoordsScaleV; // scale factor for pixel->texture coordinates
+
+ // animations
+ int m_currentLoop;
+ unsigned int m_currentFrame;
+ uint32_t m_lasttime;
+
+ float m_diffuseU, m_diffuseV; // size of the diffuse frame (in tex coords)
+ float m_diffuseScaleU, m_diffuseScaleV; // scale factor of the diffuse frame (from texture coords to diffuse tex coords)
+ CPoint m_diffuseOffset; // offset into the diffuse frame (it's not always the origin)
+
+ bool m_allocateDynamically;
+ enum ALLOCATE_TYPE { NO = 0, NORMAL, LARGE, NORMAL_FAILED, LARGE_FAILED };
+ ALLOCATE_TYPE m_isAllocated;
+
+ CTextureInfo m_info;
+ CAspectRatio m_aspect;
+
+ CTextureArray m_diffuse;
+ CTextureArray m_texture;
+
+private:
+ static CreateGUITextureFunc m_createGUITextureFunc;
+ static DrawQuadFunc m_drawQuadFunc;
+};
diff --git a/xbmc/guilib/GUITextureD3D.cpp b/xbmc/guilib/GUITextureD3D.cpp
new file mode 100644
index 0000000..d031cac
--- /dev/null
+++ b/xbmc/guilib/GUITextureD3D.cpp
@@ -0,0 +1,156 @@
+/*
+ * 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 "GUITextureD3D.h"
+
+#include "D3DResource.h"
+#include "GUIShaderDX.h"
+#include "TextureDX.h"
+#include "rendering/dx/RenderContext.h"
+
+#include <DirectXMath.h>
+
+using namespace DirectX;
+
+void CGUITextureD3D::Register()
+{
+ CGUITexture::Register(CGUITextureD3D::CreateTexture, CGUITextureD3D::DrawQuad);
+}
+
+CGUITexture* CGUITextureD3D::CreateTexture(
+ float posX, float posY, float width, float height, const CTextureInfo& texture)
+{
+ return new CGUITextureD3D(posX, posY, width, height, texture);
+}
+
+CGUITextureD3D::CGUITextureD3D(
+ float posX, float posY, float width, float height, const CTextureInfo& texture)
+ : CGUITexture(posX, posY, width, height, texture)
+{
+}
+
+CGUITextureD3D* CGUITextureD3D::Clone() const
+{
+ return new CGUITextureD3D(*this);
+}
+
+void CGUITextureD3D::Begin(UTILS::COLOR::Color color)
+{
+ CTexture* texture = m_texture.m_textures[m_currentFrame].get();
+ texture->LoadToGPU();
+
+ if (m_diffuse.size())
+ m_diffuse.m_textures[0]->LoadToGPU();
+
+ m_col = color;
+
+ DX::Windowing()->SetAlphaBlendEnable(true);
+}
+
+void CGUITextureD3D::End()
+{
+}
+
+void CGUITextureD3D::Draw(float *x, float *y, float *z, const CRect &texture, const CRect &diffuse, int orientation)
+{
+ XMFLOAT4 xcolor;
+ CD3DHelper::XMStoreColor(&xcolor, m_col);
+
+ Vertex verts[4];
+ verts[0].pos.x = x[0]; verts[0].pos.y = y[0]; verts[0].pos.z = z[0];
+ verts[0].texCoord.x = texture.x1; verts[0].texCoord.y = texture.y1;
+ verts[0].texCoord2.x = diffuse.x1; verts[0].texCoord2.y = diffuse.y1;
+ verts[0].color = xcolor;
+
+ verts[1].pos.x = x[1]; verts[1].pos.y = y[1]; verts[1].pos.z = z[1];
+ if (orientation & 4)
+ {
+ verts[1].texCoord.x = texture.x1;
+ verts[1].texCoord.y = texture.y2;
+ }
+ else
+ {
+ verts[1].texCoord.x = texture.x2;
+ verts[1].texCoord.y = texture.y1;
+ }
+ if (m_info.orientation & 4)
+ {
+ verts[1].texCoord2.x = diffuse.x1;
+ verts[1].texCoord2.y = diffuse.y2;
+ }
+ else
+ {
+ verts[1].texCoord2.x = diffuse.x2;
+ verts[1].texCoord2.y = diffuse.y1;
+ }
+ verts[1].color = xcolor;
+
+ verts[2].pos.x = x[2]; verts[2].pos.y = y[2]; verts[2].pos.z = z[2];
+ verts[2].texCoord.x = texture.x2; verts[2].texCoord.y = texture.y2;
+ verts[2].texCoord2.x = diffuse.x2; verts[2].texCoord2.y = diffuse.y2;
+ verts[2].color = xcolor;
+
+ verts[3].pos.x = x[3]; verts[3].pos.y = y[3]; verts[3].pos.z = z[3];
+ if (orientation & 4)
+ {
+ verts[3].texCoord.x = texture.x2;
+ verts[3].texCoord.y = texture.y1;
+ }
+ else
+ {
+ verts[3].texCoord.x = texture.x1;
+ verts[3].texCoord.y = texture.y2;
+ }
+ if (m_info.orientation & 4)
+ {
+ verts[3].texCoord2.x = diffuse.x2;
+ verts[3].texCoord2.y = diffuse.y1;
+ }
+ else
+ {
+ verts[3].texCoord2.x = diffuse.x1;
+ verts[3].texCoord2.y = diffuse.y2;
+ }
+ verts[3].color = xcolor;
+
+ CDXTexture* tex = static_cast<CDXTexture*>(m_texture.m_textures[m_currentFrame].get());
+ CGUIShaderDX* pGUIShader = DX::Windowing()->GetGUIShader();
+
+ pGUIShader->Begin(m_diffuse.size() ? SHADER_METHOD_RENDER_MULTI_TEXTURE_BLEND : SHADER_METHOD_RENDER_TEXTURE_BLEND);
+
+ if (m_diffuse.size())
+ {
+ CDXTexture* diff = static_cast<CDXTexture*>(m_diffuse.m_textures[0].get());
+ ID3D11ShaderResourceView* resource[] = { tex->GetShaderResource(), diff->GetShaderResource() };
+ pGUIShader->SetShaderViews(ARRAYSIZE(resource), resource);
+ }
+ else
+ {
+ ID3D11ShaderResourceView* resource = tex->GetShaderResource();
+ pGUIShader->SetShaderViews(1, &resource);
+ }
+ pGUIShader->DrawQuad(verts[0], verts[1], verts[2], verts[3]);
+}
+
+void CGUITextureD3D::DrawQuad(const CRect& rect,
+ UTILS::COLOR::Color color,
+ CTexture* texture,
+ const CRect* texCoords)
+{
+ unsigned numViews = 0;
+ ID3D11ShaderResourceView* views = nullptr;
+
+ if (texture)
+ {
+ texture->LoadToGPU();
+ numViews = 1;
+ views = ((CDXTexture *)texture)->GetShaderResource();
+ }
+
+ CD3DTexture::DrawQuad(rect, color, numViews, &views, texCoords, texture ? SHADER_METHOD_RENDER_TEXTURE_BLEND : SHADER_METHOD_RENDER_DEFAULT);
+}
diff --git a/xbmc/guilib/GUITextureD3D.h b/xbmc/guilib/GUITextureD3D.h
new file mode 100644
index 0000000..a649f85
--- /dev/null
+++ b/xbmc/guilib/GUITextureD3D.h
@@ -0,0 +1,41 @@
+/*
+ * 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 "GUITexture.h"
+#include "utils/ColorUtils.h"
+
+class CGUITextureD3D : public CGUITexture
+{
+public:
+ static void Register();
+ static CGUITexture* CreateTexture(
+ float posX, float posY, float width, float height, const CTextureInfo& texture);
+
+ static void DrawQuad(const CRect& coords,
+ UTILS::COLOR::Color color,
+ CTexture* texture = nullptr,
+ const CRect* texCoords = nullptr);
+
+ CGUITextureD3D(float posX, float posY, float width, float height, const CTextureInfo& texture);
+ ~CGUITextureD3D() override = default;
+
+ CGUITextureD3D* Clone() const override;
+
+protected:
+ void Begin(UTILS::COLOR::Color color);
+ void Draw(float *x, float *y, float *z, const CRect &texture, const CRect &diffuse, int orientation);
+ void End();
+
+private:
+ CGUITextureD3D(const CGUITextureD3D& texture) = default;
+
+ UTILS::COLOR::Color m_col;
+};
+
diff --git a/xbmc/guilib/GUITextureGL.cpp b/xbmc/guilib/GUITextureGL.cpp
new file mode 100644
index 0000000..6e46aa7
--- /dev/null
+++ b/xbmc/guilib/GUITextureGL.cpp
@@ -0,0 +1,361 @@
+/*
+ * 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 "GUITextureGL.h"
+
+#include "ServiceBroker.h"
+#include "Texture.h"
+#include "rendering/gl/RenderSystemGL.h"
+#include "utils/GLUtils.h"
+#include "utils/Geometry.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+
+#include <cstddef>
+
+#include "PlatformDefs.h"
+
+void CGUITextureGL::Register()
+{
+ CGUITexture::Register(CGUITextureGL::CreateTexture, CGUITextureGL::DrawQuad);
+}
+
+CGUITexture* CGUITextureGL::CreateTexture(
+ float posX, float posY, float width, float height, const CTextureInfo& texture)
+{
+ return new CGUITextureGL(posX, posY, width, height, texture);
+}
+
+CGUITextureGL::CGUITextureGL(
+ float posX, float posY, float width, float height, const CTextureInfo& texture)
+ : CGUITexture(posX, posY, width, height, texture)
+{
+ m_renderSystem = dynamic_cast<CRenderSystemGL*>(CServiceBroker::GetRenderSystem());
+}
+
+CGUITextureGL* CGUITextureGL::Clone() const
+{
+ return new CGUITextureGL(*this);
+}
+
+void CGUITextureGL::Begin(UTILS::COLOR::Color color)
+{
+ CTexture* texture = m_texture.m_textures[m_currentFrame].get();
+ texture->LoadToGPU();
+ if (m_diffuse.size())
+ m_diffuse.m_textures[0]->LoadToGPU();
+
+ texture->BindToUnit(0);
+
+ // Setup Colors
+ m_col[0] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color);
+ m_col[1] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color);
+ m_col[2] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color);
+ m_col[3] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color);
+
+ bool hasAlpha = m_texture.m_textures[m_currentFrame]->HasAlpha() || m_col[3] < 255;
+
+ if (m_diffuse.size())
+ {
+ if (m_col[0] == 255 && m_col[1] == 255 && m_col[2] == 255 && m_col[3] == 255 )
+ {
+ m_renderSystem->EnableShader(ShaderMethodGL::SM_MULTI);
+ }
+ else
+ {
+ m_renderSystem->EnableShader(ShaderMethodGL::SM_MULTI_BLENDCOLOR);
+ }
+
+ hasAlpha |= m_diffuse.m_textures[0]->HasAlpha();
+
+ m_diffuse.m_textures[0]->BindToUnit(1);
+ }
+ else
+ {
+ if (m_col[0] == 255 && m_col[1] == 255 && m_col[2] == 255 && m_col[3] == 255)
+ {
+ m_renderSystem->EnableShader(ShaderMethodGL::SM_TEXTURE_NOBLEND);
+ }
+ else
+ {
+ m_renderSystem->EnableShader(ShaderMethodGL::SM_TEXTURE);
+ }
+ }
+
+ if (hasAlpha)
+ {
+ glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_ONE);
+ glEnable(GL_BLEND);
+ }
+ else
+ {
+ glDisable(GL_BLEND);
+ }
+ m_packedVertices.clear();
+ m_idx.clear();
+}
+
+void CGUITextureGL::End()
+{
+ if (m_packedVertices.size())
+ {
+ GLint posLoc = m_renderSystem->ShaderGetPos();
+ GLint tex0Loc = m_renderSystem->ShaderGetCoord0();
+ GLint tex1Loc = m_renderSystem->ShaderGetCoord1();
+ GLint uniColLoc = m_renderSystem->ShaderGetUniCol();
+
+ GLuint VertexVBO;
+ GLuint IndexVBO;
+
+ glGenBuffers(1, &VertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, VertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex)*m_packedVertices.size(), &m_packedVertices[0], GL_STATIC_DRAW);
+
+ if (uniColLoc >= 0)
+ {
+ glUniform4f(uniColLoc,(m_col[0] / 255.0f), (m_col[1] / 255.0f), (m_col[2] / 255.0f), (m_col[3] / 255.0f));
+ }
+
+ if (m_diffuse.size())
+ {
+ glVertexAttribPointer(tex1Loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u2)));
+ glEnableVertexAttribArray(tex1Loc);
+ }
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glEnableVertexAttribArray(posLoc);
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+ glEnableVertexAttribArray(tex0Loc);
+
+ glGenBuffers(1, &IndexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IndexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(ushort)*m_idx.size(), m_idx.data(), GL_STATIC_DRAW);
+
+ glDrawElements(GL_TRIANGLES, m_packedVertices.size()*6 / 4, GL_UNSIGNED_SHORT, 0);
+
+ if (m_diffuse.size())
+ glDisableVertexAttribArray(tex1Loc);
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(tex0Loc);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &VertexVBO);
+ glDeleteBuffers(1, &IndexVBO);
+ }
+
+ if (m_diffuse.size())
+ glActiveTexture(GL_TEXTURE0);
+ glEnable(GL_BLEND);
+
+ m_renderSystem->DisableShader();
+}
+
+void CGUITextureGL::Draw(float *x, float *y, float *z, const CRect &texture, const CRect &diffuse, int orientation)
+{
+ PackedVertex vertices[4];
+
+ // Setup texture coordinates
+ // TopLeft
+ vertices[0].u1 = texture.x1;
+ vertices[0].v1 = texture.y1;
+
+ // TopRight
+ if (orientation & 4)
+ {
+ vertices[1].u1 = texture.x1;
+ vertices[1].v1 = texture.y2;
+ }
+ else
+ {
+ vertices[1].u1 = texture.x2;
+ vertices[1].v1 = texture.y1;
+ }
+
+ // BottomRight
+ vertices[2].u1 = texture.x2;
+ vertices[2].v1 = texture.y2;
+
+ // BottomLeft
+ if (orientation & 4)
+ {
+ vertices[3].u1 = texture.x2;
+ vertices[3].v1 = texture.y1;
+ }
+ else
+ {
+ vertices[3].u1 = texture.x1;
+ vertices[3].v1 = texture.y2;
+ }
+
+ if (m_diffuse.size())
+ {
+ // TopLeft
+ vertices[0].u2 = diffuse.x1;
+ vertices[0].v2 = diffuse.y1;
+
+ // TopRight
+ if (m_info.orientation & 4)
+ {
+ vertices[1].u2 = diffuse.x1;
+ vertices[1].v2 = diffuse.y2;
+ }
+ else
+ {
+ vertices[1].u2 = diffuse.x2;
+ vertices[1].v2 = diffuse.y1;
+ }
+
+ // BottomRight
+ vertices[2].u2 = diffuse.x2;
+ vertices[2].v2 = diffuse.y2;
+
+ // BottomLeft
+ if (m_info.orientation & 4)
+ {
+ vertices[3].u2 = diffuse.x2;
+ vertices[3].v2 = diffuse.y1;
+ }
+ else
+ {
+ vertices[3].u2 = diffuse.x1;
+ vertices[3].v2 = diffuse.y2;
+ }
+ }
+
+ for (int i=0; i<4; i++)
+ {
+ vertices[i].x = x[i];
+ vertices[i].y = y[i];
+ vertices[i].z = z[i];
+ m_packedVertices.push_back(vertices[i]);
+ }
+
+ if ((m_packedVertices.size() / 4) > (m_idx.size() / 6))
+ {
+ size_t i = m_packedVertices.size() - 4;
+ m_idx.push_back(i+0);
+ m_idx.push_back(i+1);
+ m_idx.push_back(i+2);
+ m_idx.push_back(i+2);
+ m_idx.push_back(i+3);
+ m_idx.push_back(i+0);
+ }
+}
+
+void CGUITextureGL::DrawQuad(const CRect& rect,
+ UTILS::COLOR::Color color,
+ CTexture* texture,
+ const CRect* texCoords)
+{
+ CRenderSystemGL *renderSystem = dynamic_cast<CRenderSystemGL*>(CServiceBroker::GetRenderSystem());
+ if (texture)
+ {
+ texture->LoadToGPU();
+ texture->BindToUnit(0);
+ }
+
+ glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
+ glEnable(GL_BLEND); // Turn Blending On
+
+ VerifyGLState();
+
+ GLubyte col[4];
+ GLubyte idx[4] = {0, 1, 3, 2}; //determines order of the vertices
+ GLuint vertexVBO;
+ GLuint indexVBO;
+
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ }vertex[4];
+
+ if (texture)
+ renderSystem->EnableShader(ShaderMethodGL::SM_TEXTURE);
+ else
+ renderSystem->EnableShader(ShaderMethodGL::SM_DEFAULT);
+
+ GLint posLoc = renderSystem->ShaderGetPos();
+ GLint tex0Loc = renderSystem->ShaderGetCoord0();
+ GLint uniColLoc = renderSystem->ShaderGetUniCol();
+
+ // Setup Colors
+ col[0] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color);
+ col[1] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color);
+ col[2] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color);
+ col[3] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color);
+
+ glUniform4f(uniColLoc, col[0] / 255.0f, col[1] / 255.0f, col[2] / 255.0f, col[3] / 255.0f);
+
+ // bottom left
+ vertex[0].x = rect.x1;
+ vertex[0].y = rect.y1;
+ vertex[0].z = 0;
+
+ // bottom right
+ vertex[1].x = rect.x2;
+ vertex[1].y = rect.y1;
+ vertex[1].z = 0;
+
+ // top right
+ vertex[2].x = rect.x2;
+ vertex[2].y = rect.y2;
+ vertex[2].z = 0;
+
+ // top left
+ vertex[3].x = rect.x1;
+ vertex[3].y = rect.y2;
+ vertex[3].z = 0;
+
+ if (texture)
+ {
+ CRect coords = texCoords ? *texCoords : CRect(0.0f, 0.0f, 1.0f, 1.0f);
+ vertex[0].u1 = vertex[3].u1 = coords.x1;
+ vertex[0].v1 = vertex[1].v1 = coords.y1;
+ vertex[1].u1 = vertex[2].u1 = coords.x2;
+ vertex[2].v1 = vertex[3].v1 = coords.y2;
+ }
+
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex)*4, &vertex[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glEnableVertexAttribArray(posLoc);
+
+ if (texture)
+ {
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+ glEnableVertexAttribArray(tex0Loc);
+ }
+
+ glGenBuffers(1, &indexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte)*4, idx, GL_STATIC_DRAW);
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+
+ glDisableVertexAttribArray(posLoc);
+ if (texture)
+ glDisableVertexAttribArray(tex0Loc);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &indexVBO);
+
+ renderSystem->DisableShader();
+}
+
diff --git a/xbmc/guilib/GUITextureGL.h b/xbmc/guilib/GUITextureGL.h
new file mode 100644
index 0000000..4acba1e
--- /dev/null
+++ b/xbmc/guilib/GUITextureGL.h
@@ -0,0 +1,58 @@
+/*
+ * 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 "GUITexture.h"
+#include "utils/ColorUtils.h"
+
+#include <array>
+
+#include "system_gl.h"
+
+class CRenderSystemGL;
+
+class CGUITextureGL : public CGUITexture
+{
+public:
+ static void Register();
+ static CGUITexture* CreateTexture(
+ float posX, float posY, float width, float height, const CTextureInfo& texture);
+
+ static void DrawQuad(const CRect& coords,
+ UTILS::COLOR::Color color,
+ CTexture* texture = nullptr,
+ const CRect* texCoords = nullptr);
+
+ CGUITextureGL(float posX, float posY, float width, float height, const CTextureInfo& texture);
+ ~CGUITextureGL() override = default;
+
+ CGUITextureGL* Clone() const override;
+
+protected:
+ void Begin(UTILS::COLOR::Color color) override;
+ void Draw(float *x, float *y, float *z, const CRect &texture, const CRect &diffuse, int orientation) override;
+ void End() override;
+
+private:
+ CGUITextureGL(const CGUITextureGL& texture) = default;
+
+ std::array<GLubyte, 4> m_col;
+
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ float u2, v2;
+ };
+
+ std::vector<PackedVertex> m_packedVertices;
+ std::vector<GLushort> m_idx;
+ CRenderSystemGL *m_renderSystem;
+};
+
diff --git a/xbmc/guilib/GUITextureGLES.cpp b/xbmc/guilib/GUITextureGLES.cpp
new file mode 100644
index 0000000..d92201a
--- /dev/null
+++ b/xbmc/guilib/GUITextureGLES.cpp
@@ -0,0 +1,304 @@
+/*
+ * 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 "GUITextureGLES.h"
+
+#include "ServiceBroker.h"
+#include "Texture.h"
+#include "rendering/gles/RenderSystemGLES.h"
+#include "utils/GLUtils.h"
+#include "utils/MathUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include <cstddef>
+
+void CGUITextureGLES::Register()
+{
+ CGUITexture::Register(CGUITextureGLES::CreateTexture, CGUITextureGLES::DrawQuad);
+}
+
+CGUITexture* CGUITextureGLES::CreateTexture(
+ float posX, float posY, float width, float height, const CTextureInfo& texture)
+{
+ return new CGUITextureGLES(posX, posY, width, height, texture);
+}
+
+CGUITextureGLES::CGUITextureGLES(
+ float posX, float posY, float width, float height, const CTextureInfo& texture)
+ : CGUITexture(posX, posY, width, height, texture)
+{
+ m_renderSystem = dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+}
+
+CGUITextureGLES* CGUITextureGLES::Clone() const
+{
+ return new CGUITextureGLES(*this);
+}
+
+void CGUITextureGLES::Begin(UTILS::COLOR::Color color)
+{
+ CTexture* texture = m_texture.m_textures[m_currentFrame].get();
+ texture->LoadToGPU();
+ if (m_diffuse.size())
+ m_diffuse.m_textures[0]->LoadToGPU();
+
+ texture->BindToUnit(0);
+
+ // Setup Colors
+ m_col[0] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color);
+ m_col[1] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color);
+ m_col[2] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color);
+ m_col[3] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color);
+
+ if (CServiceBroker::GetWinSystem()->UseLimitedColor())
+ {
+ m_col[0] = (235 - 16) * m_col[0] / 255 + 16;
+ m_col[1] = (235 - 16) * m_col[1] / 255 + 16;
+ m_col[2] = (235 - 16) * m_col[2] / 255 + 16;
+ }
+
+ bool hasAlpha = m_texture.m_textures[m_currentFrame]->HasAlpha() || m_col[3] < 255;
+
+ if (m_diffuse.size())
+ {
+ if (m_col[0] == 255 && m_col[1] == 255 && m_col[2] == 255 && m_col[3] == 255 )
+ {
+ m_renderSystem->EnableGUIShader(ShaderMethodGLES::SM_MULTI);
+ }
+ else
+ {
+ m_renderSystem->EnableGUIShader(ShaderMethodGLES::SM_MULTI_BLENDCOLOR);
+ }
+
+ hasAlpha |= m_diffuse.m_textures[0]->HasAlpha();
+
+ m_diffuse.m_textures[0]->BindToUnit(1);
+
+ }
+ else
+ {
+ if (m_col[0] == 255 && m_col[1] == 255 && m_col[2] == 255 && m_col[3] == 255)
+ {
+ m_renderSystem->EnableGUIShader(ShaderMethodGLES::SM_TEXTURE_NOBLEND);
+ }
+ else
+ {
+ m_renderSystem->EnableGUIShader(ShaderMethodGLES::SM_TEXTURE);
+ }
+ }
+
+ if ( hasAlpha )
+ {
+ glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_ONE);
+ glEnable( GL_BLEND );
+ }
+ else
+ {
+ glDisable(GL_BLEND);
+ }
+ m_packedVertices.clear();
+}
+
+void CGUITextureGLES::End()
+{
+ if (m_packedVertices.size())
+ {
+ GLint posLoc = m_renderSystem->GUIShaderGetPos();
+ GLint tex0Loc = m_renderSystem->GUIShaderGetCoord0();
+ GLint tex1Loc = m_renderSystem->GUIShaderGetCoord1();
+ GLint uniColLoc = m_renderSystem->GUIShaderGetUniCol();
+
+ if(uniColLoc >= 0)
+ {
+ glUniform4f(uniColLoc,(m_col[0] / 255.0f), (m_col[1] / 255.0f), (m_col[2] / 255.0f), (m_col[3] / 255.0f));
+ }
+
+ if(m_diffuse.size())
+ {
+ glVertexAttribPointer(tex1Loc, 2, GL_FLOAT, 0, sizeof(PackedVertex), (char*)&m_packedVertices[0] + offsetof(PackedVertex, u2));
+ glEnableVertexAttribArray(tex1Loc);
+ }
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex), (char*)&m_packedVertices[0] + offsetof(PackedVertex, x));
+ glEnableVertexAttribArray(posLoc);
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, sizeof(PackedVertex), (char*)&m_packedVertices[0] + offsetof(PackedVertex, u1));
+ glEnableVertexAttribArray(tex0Loc);
+
+ glDrawElements(GL_TRIANGLES, m_packedVertices.size()*6 / 4, GL_UNSIGNED_SHORT, m_idx.data());
+
+ if (m_diffuse.size())
+ glDisableVertexAttribArray(tex1Loc);
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(tex0Loc);
+ }
+
+ if (m_diffuse.size())
+ glActiveTexture(GL_TEXTURE0);
+ glEnable(GL_BLEND);
+ m_renderSystem->DisableGUIShader();
+}
+
+void CGUITextureGLES::Draw(float *x, float *y, float *z, const CRect &texture, const CRect &diffuse, int orientation)
+{
+ PackedVertex vertices[4];
+
+ // Setup texture coordinates
+ //TopLeft
+ vertices[0].u1 = texture.x1;
+ vertices[0].v1 = texture.y1;
+ //TopRight
+ if (orientation & 4)
+ {
+ vertices[1].u1 = texture.x1;
+ vertices[1].v1 = texture.y2;
+ }
+ else
+ {
+ vertices[1].u1 = texture.x2;
+ vertices[1].v1 = texture.y1;
+ }
+ //BottomRight
+ vertices[2].u1 = texture.x2;
+ vertices[2].v1 = texture.y2;
+ //BottomLeft
+ if (orientation & 4)
+ {
+ vertices[3].u1 = texture.x2;
+ vertices[3].v1 = texture.y1;
+ }
+ else
+ {
+ vertices[3].u1 = texture.x1;
+ vertices[3].v1 = texture.y2;
+ }
+
+ if (m_diffuse.size())
+ {
+ //TopLeft
+ vertices[0].u2 = diffuse.x1;
+ vertices[0].v2 = diffuse.y1;
+ //TopRight
+ if (m_info.orientation & 4)
+ {
+ vertices[1].u2 = diffuse.x1;
+ vertices[1].v2 = diffuse.y2;
+ }
+ else
+ {
+ vertices[1].u2 = diffuse.x2;
+ vertices[1].v2 = diffuse.y1;
+ }
+ //BottomRight
+ vertices[2].u2 = diffuse.x2;
+ vertices[2].v2 = diffuse.y2;
+ //BottomLeft
+ if (m_info.orientation & 4)
+ {
+ vertices[3].u2 = diffuse.x2;
+ vertices[3].v2 = diffuse.y1;
+ }
+ else
+ {
+ vertices[3].u2 = diffuse.x1;
+ vertices[3].v2 = diffuse.y2;
+ }
+ }
+
+ for (int i=0; i<4; i++)
+ {
+ vertices[i].x = x[i];
+ vertices[i].y = y[i];
+ vertices[i].z = z[i];
+ m_packedVertices.push_back(vertices[i]);
+ }
+
+ if ((m_packedVertices.size() / 4) > (m_idx.size() / 6))
+ {
+ size_t i = m_packedVertices.size() - 4;
+ m_idx.push_back(i+0);
+ m_idx.push_back(i+1);
+ m_idx.push_back(i+2);
+ m_idx.push_back(i+2);
+ m_idx.push_back(i+3);
+ m_idx.push_back(i+0);
+ }
+}
+
+void CGUITextureGLES::DrawQuad(const CRect& rect,
+ UTILS::COLOR::Color color,
+ CTexture* texture,
+ const CRect* texCoords)
+{
+ CRenderSystemGLES *renderSystem = dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+ if (texture)
+ {
+ texture->LoadToGPU();
+ texture->BindToUnit(0);
+ }
+
+ glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
+ glEnable(GL_BLEND); // Turn Blending On
+
+ VerifyGLState();
+
+ GLubyte col[4];
+ GLfloat ver[4][3];
+ GLfloat tex[4][2];
+ GLubyte idx[4] = {0, 1, 3, 2}; //determines order of triangle strip
+
+ if (texture)
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_TEXTURE);
+ else
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_DEFAULT);
+
+ GLint posLoc = renderSystem->GUIShaderGetPos();
+ GLint tex0Loc = renderSystem->GUIShaderGetCoord0();
+ GLint uniColLoc= renderSystem->GUIShaderGetUniCol();
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, 0, 0, ver);
+ if (texture)
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, 0, tex);
+
+ glEnableVertexAttribArray(posLoc);
+ if (texture)
+ glEnableVertexAttribArray(tex0Loc);
+
+ // Setup Colors
+ col[0] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color);
+ col[1] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color);
+ col[2] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color);
+ col[3] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color);
+
+ glUniform4f(uniColLoc, col[0] / 255.0f, col[1] / 255.0f, col[2] / 255.0f, col[3] / 255.0f);
+
+ ver[0][0] = ver[3][0] = rect.x1;
+ ver[0][1] = ver[1][1] = rect.y1;
+ ver[1][0] = ver[2][0] = rect.x2;
+ ver[2][1] = ver[3][1] = rect.y2;
+ ver[0][2] = ver[1][2] = ver[2][2] = ver[3][2]= 0;
+
+ if (texture)
+ {
+ // Setup texture coordinates
+ CRect coords = texCoords ? *texCoords : CRect(0.0f, 0.0f, 1.0f, 1.0f);
+ tex[0][0] = tex[3][0] = coords.x1;
+ tex[0][1] = tex[1][1] = coords.y1;
+ tex[1][0] = tex[2][0] = coords.x2;
+ tex[2][1] = tex[3][1] = coords.y2;
+ }
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, idx);
+
+ glDisableVertexAttribArray(posLoc);
+ if (texture)
+ glDisableVertexAttribArray(tex0Loc);
+
+ renderSystem->DisableGUIShader();
+}
+
diff --git a/xbmc/guilib/GUITextureGLES.h b/xbmc/guilib/GUITextureGLES.h
new file mode 100644
index 0000000..a9b3610
--- /dev/null
+++ b/xbmc/guilib/GUITextureGLES.h
@@ -0,0 +1,60 @@
+/*
+ * 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 "GUITexture.h"
+#include "utils/ColorUtils.h"
+
+#include <array>
+#include <vector>
+
+#include "system_gl.h"
+
+struct PackedVertex
+{
+ float x, y, z;
+ float u1, v1;
+ float u2, v2;
+};
+typedef std::vector<PackedVertex> PackedVertices;
+
+class CRenderSystemGLES;
+
+class CGUITextureGLES : public CGUITexture
+{
+public:
+ static void Register();
+ static CGUITexture* CreateTexture(
+ float posX, float posY, float width, float height, const CTextureInfo& texture);
+
+ static void DrawQuad(const CRect& coords,
+ UTILS::COLOR::Color color,
+ CTexture* texture = nullptr,
+ const CRect* texCoords = nullptr);
+
+ CGUITextureGLES(float posX, float posY, float width, float height, const CTextureInfo& texture);
+ ~CGUITextureGLES() override = default;
+
+ CGUITextureGLES* Clone() const override;
+
+protected:
+ void Begin(UTILS::COLOR::Color color) override;
+ void Draw(float* x, float* y, float* z, const CRect& texture, const CRect& diffuse, int orientation) override;
+ void End() override;
+
+private:
+ CGUITextureGLES(const CGUITextureGLES& texture) = default;
+
+ std::array<GLubyte, 4> m_col;
+
+ PackedVertices m_packedVertices;
+ std::vector<GLushort> m_idx;
+ CRenderSystemGLES *m_renderSystem;
+};
+
diff --git a/xbmc/guilib/GUIToggleButtonControl.cpp b/xbmc/guilib/GUIToggleButtonControl.cpp
new file mode 100644
index 0000000..42bd7ee
--- /dev/null
+++ b/xbmc/guilib/GUIToggleButtonControl.cpp
@@ -0,0 +1,175 @@
+/*
+ * 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 "GUIToggleButtonControl.h"
+
+#include "GUIDialog.h"
+#include "GUIInfoManager.h"
+#include "GUIWindowManager.h"
+#include "input/Key.h"
+
+CGUIToggleButtonControl::CGUIToggleButtonControl(int parentID, int controlID, float posX, float posY, float width, float height, const CTextureInfo& textureFocus, const CTextureInfo& textureNoFocus, const CTextureInfo& altTextureFocus, const CTextureInfo& altTextureNoFocus, const CLabelInfo &labelInfo, bool wrapMultiLine)
+ : CGUIButtonControl(parentID, controlID, posX, posY, width, height, textureFocus, textureNoFocus, labelInfo, wrapMultiLine)
+ , m_selectButton(parentID, controlID, posX, posY, width, height, altTextureFocus, altTextureNoFocus, labelInfo, wrapMultiLine)
+{
+ ControlType = GUICONTROL_TOGGLEBUTTON;
+}
+
+CGUIToggleButtonControl::~CGUIToggleButtonControl(void) = default;
+
+void CGUIToggleButtonControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ // ask our infoManager whether we are selected or not...
+ if (m_toggleSelect)
+ m_bSelected = m_toggleSelect->Get(INFO::DEFAULT_CONTEXT);
+
+ if (m_bSelected)
+ {
+ // render our Alternate textures...
+ m_selectButton.SetFocus(HasFocus());
+ m_selectButton.SetVisible(IsVisible());
+ m_selectButton.SetEnabled(!IsDisabled());
+ m_selectButton.SetPulseOnSelect(m_pulseOnSelect);
+ ProcessToggle(currentTime);
+ m_selectButton.DoProcess(currentTime, dirtyregions);
+ }
+ else
+ CGUIButtonControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIToggleButtonControl::ProcessToggle(unsigned int currentTime)
+{
+ bool changed = false;
+
+ changed |= m_label.SetMaxRect(m_posX, m_posY, GetWidth(), m_height);
+ changed |= m_label.SetText(GetDescription());
+ changed |= m_label.SetColor(GetTextColor());
+ changed |= m_label.SetScrolling(HasFocus());
+ changed |= m_label.Process(currentTime);
+
+ if (changed)
+ MarkDirtyRegion();
+}
+
+void CGUIToggleButtonControl::Render()
+{
+ if (m_bSelected)
+ {
+ m_selectButton.Render();
+ CGUIControl::Render();
+ }
+ else
+ { // render our Normal textures...
+ CGUIButtonControl::Render();
+ }
+}
+
+bool CGUIToggleButtonControl::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_SELECT_ITEM)
+ {
+ m_bSelected = !m_bSelected;
+ SetInvalid();
+ }
+ return CGUIButtonControl::OnAction(action);
+}
+
+void CGUIToggleButtonControl::AllocResources()
+{
+ CGUIButtonControl::AllocResources();
+ m_selectButton.AllocResources();
+}
+
+void CGUIToggleButtonControl::FreeResources(bool immediately)
+{
+ CGUIButtonControl::FreeResources(immediately);
+ m_selectButton.FreeResources(immediately);
+}
+
+void CGUIToggleButtonControl::DynamicResourceAlloc(bool bOnOff)
+{
+ CGUIButtonControl::DynamicResourceAlloc(bOnOff);
+ m_selectButton.DynamicResourceAlloc(bOnOff);
+}
+
+void CGUIToggleButtonControl::SetInvalid()
+{
+ CGUIButtonControl::SetInvalid();
+ m_selectButton.SetInvalid();
+}
+
+void CGUIToggleButtonControl::SetPosition(float posX, float posY)
+{
+ CGUIButtonControl::SetPosition(posX, posY);
+ m_selectButton.SetPosition(posX, posY);
+}
+
+void CGUIToggleButtonControl::SetWidth(float width)
+{
+ CGUIButtonControl::SetWidth(width);
+ m_selectButton.SetWidth(width);
+}
+
+void CGUIToggleButtonControl::SetHeight(float height)
+{
+ CGUIButtonControl::SetHeight(height);
+ m_selectButton.SetHeight(height);
+}
+
+void CGUIToggleButtonControl::SetMinWidth(float minWidth)
+{
+ CGUIButtonControl::SetMinWidth(minWidth);
+ m_selectButton.SetMinWidth(minWidth);
+}
+
+bool CGUIToggleButtonControl::UpdateColors(const CGUIListItem* item)
+{
+ bool changed = CGUIButtonControl::UpdateColors(nullptr);
+ changed |= m_selectButton.SetColorDiffuse(m_diffuseColor);
+ changed |= m_selectButton.UpdateColors(nullptr);
+
+ return changed;
+}
+
+void CGUIToggleButtonControl::SetLabel(const std::string &label)
+{
+ CGUIButtonControl::SetLabel(label);
+ m_selectButton.SetLabel(label);
+}
+
+void CGUIToggleButtonControl::SetAltLabel(const std::string &label)
+{
+ if (label.size())
+ m_selectButton.SetLabel(label);
+}
+
+std::string CGUIToggleButtonControl::GetDescription() const
+{
+ if (m_bSelected)
+ return m_selectButton.GetDescription();
+ return CGUIButtonControl::GetDescription();
+}
+
+void CGUIToggleButtonControl::SetAltClickActions(const CGUIAction &clickActions)
+{
+ m_selectButton.SetClickActions(clickActions);
+}
+
+void CGUIToggleButtonControl::OnClick()
+{
+ // the ! is here as m_bSelected gets updated before this is called
+ if (!m_bSelected && m_selectButton.HasClickActions())
+ m_selectButton.OnClick();
+ else
+ CGUIButtonControl::OnClick();
+}
+
+void CGUIToggleButtonControl::SetToggleSelect(const std::string &toggleSelect)
+{
+ m_toggleSelect = CServiceBroker::GetGUI()->GetInfoManager().Register(toggleSelect, GetParentID());
+}
diff --git a/xbmc/guilib/GUIToggleButtonControl.dox b/xbmc/guilib/GUIToggleButtonControl.dox
new file mode 100644
index 0000000..0605d20
--- /dev/null
+++ b/xbmc/guilib/GUIToggleButtonControl.dox
@@ -0,0 +1,91 @@
+/*!
+
+\page skin_Toggle_button_control Toggle button control
+\brief **A toggle on/off button that can take 2 different states.**
+
+\tableofcontents
+
+The toggle button control is used for creating buttons that have 2 states. You
+can choose the position, size, and look of the button. When the user clicks on
+the toggle button, the state will change, toggling the extra textures
+(alttexturefocus and alttexturenofocus). Used for controls where two states are
+needed (pushed in and pushed out for instance).
+
+--------------------------------------------------------------------------------
+\section skin_Toggle_button_control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="togglebutton" id="25">
+ <description>My first togglebutton control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+ <colordiffuse>FFFFFFFF</colordiffuse>
+ <texturefocus>myfocustexture.png</texturefocus>
+ <texturenofocus>mynormaltexture.png</texturenofocus>
+ <alttexturefocus>myselectedTexture.png</alttexturefocus>
+ <alttexturenofocus>myselectedTexture_nf.png</alttexturenofocus>
+ <usealttexture>!Player.IsPaused</usealttexture>
+ <label>29</label>
+ <altlabel>29</altlabel>
+ <font>font12</font>
+ <textcolor>FFFFFFFF</textcolor>
+ <disabledcolor>80FFFFFF</disabledcolor>
+ <align>left</align>
+ <aligny>center</aligny>
+ <textoffsetx>4</textoffsetx>
+ <textoffsety>5</textoffsety>
+ <pulseonselect>false</pulseonselect>
+ <onclick>Player.Pause</onclick>
+ <onfocus>-</onfocus>
+ <onunfocus>-</onunfocus>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+ <wrapmultiline>false</wrapmultiline>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section skin_Toggle_button_control_sect2 Available tags
+
+In addition to the Default Control Tags the following tags are available. Note
+that each tag is lower case only. This is important, as xml tags are case-sensitive.
+
+| Tag | Description |
+|------------------:|:--------------------------------------------------------------|
+| texturefocus | Specifies the image file which should be displayed when the button has focus. See here for additional information about texture tags.
+| texturenofocus | Specifies the image file which should be displayed when the button does not have focus.
+| alttexturefocus | Specifies the image file which should be displayed when the toggle button is in it's selected state. This texture replaces the <b>`<texturefocus>`</b> texture when the toggle button is selected.
+| alttexturenofocus | Specifies the image file which should be displayed when the button is in it's selected state but unfocused.
+| usealttexture | Specifies the conditions under which the Alternative Textures should be shown. Some toggle button controls are handled by Kodi internally, but any extra ones that the skinner has can be controlled using this tag. See here for more information.
+| label | The label used on the button. It can be a link into strings.po, or an actual text label.
+| altlabel | The alternate label used on the button. It can be a link into strings.po, or an actual text label.
+| altclick | The alternate action to perform when the button is pressed. Should be a built in function. See here for more information. You may have more than one <b>`<altclick>`</b> tag, and they'll be executed in sequence.
+| font | Font used for the button label. From fonts.xml.
+| textcolor | Color used for displaying the button label. In AARRGGBB hex format, or a name from the colour theme.
+| disabledcolor | Color used for the button label if the button is disabled. In AARRGGBB hex format, or a name from the colour theme.
+| shadowcolor | Specifies the color of the drop shadow on the text. In AARRGGBB hex format, or a name from the colour theme.
+| align | Label horizontal alignment on the button. Defaults to left, can also be center or right.
+| aligny | Label vertical alignment on the button. Defaults to top, can also be center.
+| textoffsetx | Amount to offset the label from the left (or right) edge of the button when using left or right alignment.
+| textoffsety | Amount to offset the label from the top edge of the button when using top alignment.
+| textwidth | Will truncate any text that's too long.
+| onclick | Specifies the action to perform when the button is pressed. Should be a built in function. See here for more information. You may have more than one <b>`<onclick>`</b> tag, and they'll be executed in sequence.
+| onfocus | Specifies the action to perform when the button is focused. Should be a built in function. The action is performed after any focus animations have completed. See here for more information.
+| onunfocus | Specifies the action to perform when the button loses focus. Should be a built in function.
+| wrapmultiline | Allows wrapping on the label across multiple lines. Defaults to false.
+
+
+--------------------------------------------------------------------------------
+\section skin_Toggle_button_control_sect3 See also
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIToggleButtonControl.h b/xbmc/guilib/GUIToggleButtonControl.h
new file mode 100644
index 0000000..732adc7
--- /dev/null
+++ b/xbmc/guilib/GUIToggleButtonControl.h
@@ -0,0 +1,56 @@
+/*
+ * 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
+
+/*!
+\file GUIToggleButtonControl.h
+\brief
+*/
+
+#include "GUIButtonControl.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIToggleButtonControl : public CGUIButtonControl
+{
+public:
+ CGUIToggleButtonControl(int parentID, int controlID, float posX, float posY, float width, float height, const CTextureInfo& textureFocus, const CTextureInfo& textureNoFocus, const CTextureInfo& altTextureFocus, const CTextureInfo& altTextureNoFocus, const CLabelInfo &labelInfo, bool wrapMultiline = false);
+ ~CGUIToggleButtonControl(void) override;
+ CGUIToggleButtonControl* Clone() const override { return new CGUIToggleButtonControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnAction(const CAction &action) override;
+ void AllocResources() override;
+ void FreeResources(bool immediately = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ void SetInvalid() override;
+ void SetPosition(float posX, float posY) override;
+ void SetWidth(float width) override;
+ void SetHeight(float height) override;
+ void SetMinWidth(float minWidth) override;
+ void SetLabel(const std::string& label) override;
+ void SetAltLabel(const std::string& label);
+ std::string GetDescription() const override;
+ void SetToggleSelect(const std::string &toggleSelect);
+ void SetAltClickActions(const CGUIAction &clickActions);
+
+protected:
+ bool UpdateColors(const CGUIListItem* item) override;
+ void OnClick() override;
+ CGUIButtonControl m_selectButton;
+ INFO::InfoPtr m_toggleSelect;
+
+private:
+ void ProcessToggle(unsigned int currentTime);
+ std::string m_altLabel;
+};
+
diff --git a/xbmc/guilib/GUIVideoControl.cpp b/xbmc/guilib/GUIVideoControl.cpp
new file mode 100644
index 0000000..91a949f
--- /dev/null
+++ b/xbmc/guilib/GUIVideoControl.cpp
@@ -0,0 +1,108 @@
+/*
+ * 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 "GUIVideoControl.h"
+
+#include "GUIComponent.h"
+#include "GUIWindowManager.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationPowerHandling.h"
+#include "input/Key.h"
+#include "utils/ColorUtils.h"
+
+CGUIVideoControl::CGUIVideoControl(int parentID, int controlID, float posX, float posY, float width, float height)
+ : CGUIControl(parentID, controlID, posX, posY, width, height)
+{
+ ControlType = GUICONTROL_VIDEO;
+}
+
+CGUIVideoControl::~CGUIVideoControl(void) = default;
+
+void CGUIVideoControl::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ //! @todo Proper processing which marks when its actually changed. Just mark always for now.
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsRenderingGuiLayer())
+ MarkDirtyRegion();
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIVideoControl::Render()
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsRenderingVideo())
+ {
+ if (!appPlayer->IsPausedPlayback())
+ {
+ auto& appComponents = CServiceBroker::GetAppComponents();
+ const auto appPower = appComponents.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetViewWindow(m_posX, m_posY, m_posX + m_width, m_posY + m_height);
+ TransformMatrix mat;
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetTransform(mat, 1.0, 1.0);
+
+ UTILS::COLOR::Color alpha =
+ CServiceBroker::GetWinSystem()->GetGfxContext().MergeAlpha(0xFF000000) >> 24;
+ if (appPlayer->IsRenderingVideoLayer())
+ {
+ CRect old = CServiceBroker::GetWinSystem()->GetGfxContext().GetScissors();
+ CRect region = GetRenderRegion();
+ region.Intersect(old);
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetScissors(region);
+ CServiceBroker::GetWinSystem()->GetGfxContext().Clear(0);
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetScissors(old);
+ }
+ else
+ appPlayer->Render(false, alpha);
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+ }
+ CGUIControl::Render();
+}
+
+void CGUIVideoControl::RenderEx()
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsRenderingVideo())
+ appPlayer->Render(false, 255, false);
+
+ CGUIControl::RenderEx();
+}
+
+EVENT_RESULT CGUIVideoControl::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlayingVideo())
+ return EVENT_RESULT_UNHANDLED;
+ if (event.m_id == ACTION_MOUSE_LEFT_CLICK)
+ { // switch to fullscreen
+ CGUIMessage message(GUI_MSG_FULLSCREEN, GetID(), GetParentID());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+ return EVENT_RESULT_HANDLED;
+ }
+ return EVENT_RESULT_UNHANDLED;
+}
+
+bool CGUIVideoControl::CanFocus() const
+{ // unfocusable
+ return false;
+}
+
+bool CGUIVideoControl::CanFocusFromPoint(const CPoint &point) const
+{ // mouse is allowed to focus this control, but it doesn't actually receive focus
+ return IsVisible() && HitTest(point);
+}
diff --git a/xbmc/guilib/GUIVideoControl.dox b/xbmc/guilib/GUIVideoControl.dox
new file mode 100644
index 0000000..24a2042
--- /dev/null
+++ b/xbmc/guilib/GUIVideoControl.dox
@@ -0,0 +1,42 @@
+/*!
+
+\page Video_Control Video Control
+\brief **Used to display the currently playing video whilst in the GUI.**
+
+\tableofcontents
+
+The videowindow control is used for displaying the currently playing video
+elsewhere in the Kodi GUI. You can choose the position, and size of the video
+displayed. Note that the control is only rendered if video is being played.
+
+
+--------------------------------------------------------------------------------
+\section Video_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="videowindow" id="2">
+ <description>My first video control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Video_Control_sect2 Available tags
+
+Only the [default control](http://kodi.wiki/view/Default_Control_Tags) tags are applicable to this control.
+
+
+--------------------------------------------------------------------------------
+\section Video_Control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIVideoControl.h b/xbmc/guilib/GUIVideoControl.h
new file mode 100644
index 0000000..70c20dc
--- /dev/null
+++ b/xbmc/guilib/GUIVideoControl.h
@@ -0,0 +1,37 @@
+/*
+ * 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
+
+/*!
+\file GUIVideoControl.h
+\brief
+*/
+
+#include "GUIControl.h"
+
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIVideoControl :
+ public CGUIControl
+{
+public:
+ CGUIVideoControl(int parentID, int controlID, float posX, float posY, float width, float height);
+ ~CGUIVideoControl(void) override;
+ CGUIVideoControl* Clone() const override { return new CGUIVideoControl(*this); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ void RenderEx() override;
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ bool CanFocus() const override;
+ bool CanFocusFromPoint(const CPoint &point) const override;
+};
+
diff --git a/xbmc/guilib/GUIVisualisationControl.cpp b/xbmc/guilib/GUIVisualisationControl.cpp
new file mode 100644
index 0000000..0399edd
--- /dev/null
+++ b/xbmc/guilib/GUIVisualisationControl.cpp
@@ -0,0 +1,452 @@
+/*
+ * 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 "GUIVisualisationControl.h"
+
+#include "GUIComponent.h"
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "GUIWindowManager.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/Visualization.h"
+#include "addons/addoninfo/AddonType.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+namespace
+{
+constexpr unsigned int MAX_AUDIO_BUFFERS = 16;
+} // namespace
+
+CAudioBuffer::CAudioBuffer(int iSize)
+{
+ m_iLen = iSize;
+ m_pBuffer = new float[iSize];
+}
+
+CAudioBuffer::~CAudioBuffer()
+{
+ delete[] m_pBuffer;
+}
+
+const float* CAudioBuffer::Get() const
+{
+ return m_pBuffer;
+}
+
+int CAudioBuffer::Size() const
+{
+ return m_iLen;
+}
+
+void CAudioBuffer::Set(const float* psBuffer, int iSize)
+{
+ if (iSize < 0)
+ return;
+
+ memcpy(m_pBuffer, psBuffer, iSize * sizeof(float));
+ for (int i = iSize; i < m_iLen; ++i)
+ m_pBuffer[i] = 0;
+}
+
+CGUIVisualisationControl::CGUIVisualisationControl(
+ int parentID, int controlID, float posX, float posY, float width, float height)
+ : CGUIControl(parentID, controlID, posX, posY, width, height)
+{
+ ControlType = GUICONTROL_VISUALISATION;
+}
+
+CGUIVisualisationControl::CGUIVisualisationControl(const CGUIVisualisationControl& from)
+ : CGUIControl(from)
+{
+ ControlType = GUICONTROL_VISUALISATION;
+}
+
+std::string CGUIVisualisationControl::Name()
+{
+ if (m_instance == nullptr)
+ return "";
+ return m_instance->Name();
+}
+
+bool CGUIVisualisationControl::OnMessage(CGUIMessage& message)
+{
+ if (m_alreadyStarted)
+ {
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_GET_VISUALISATION:
+ message.SetPointer(this);
+ return true;
+ case GUI_MSG_VISUALISATION_RELOAD:
+ FreeResources(true);
+ return true;
+ case GUI_MSG_PLAYBACK_STARTED:
+ m_updateTrack = true;
+ return true;
+ default:
+ break;
+ }
+ }
+ return CGUIControl::OnMessage(message);
+}
+
+bool CGUIVisualisationControl::OnAction(const CAction& action)
+{
+ if (m_alreadyStarted)
+ {
+ switch (action.GetID())
+ {
+ case ACTION_VIS_PRESET_NEXT:
+ m_instance->NextPreset();
+ break;
+ case ACTION_VIS_PRESET_PREV:
+ m_instance->PrevPreset();
+ break;
+ case ACTION_VIS_PRESET_RANDOM:
+ m_instance->RandomPreset();
+ break;
+ case ACTION_VIS_RATE_PRESET_PLUS:
+ m_instance->RatePreset(true);
+ break;
+ case ACTION_VIS_RATE_PRESET_MINUS:
+ m_instance->RatePreset(false);
+ break;
+ case ACTION_VIS_PRESET_LOCK:
+ m_instance->LockPreset();
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ return CGUIControl::OnAction(action);
+}
+
+void CGUIVisualisationControl::Process(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingAudio())
+ {
+ if (m_bInvalidated)
+ FreeResources(true);
+
+ if (!m_instance && !m_attemptedLoad)
+ {
+ InitVisualization();
+
+ m_attemptedLoad = true;
+ }
+ else if (m_callStart && m_instance)
+ {
+ auto& context = CServiceBroker::GetWinSystem()->GetGfxContext();
+
+ context.CaptureStateBlock();
+ if (m_alreadyStarted)
+ {
+ m_instance->Stop();
+ m_alreadyStarted = false;
+ }
+
+ std::string songTitle = URIUtils::GetFileName(g_application.CurrentFile());
+ const MUSIC_INFO::CMusicInfoTag* tag =
+ CServiceBroker::GetGUI()->GetInfoManager().GetCurrentSongTag();
+ if (tag && !tag->GetTitle().empty())
+ songTitle = tag->GetTitle();
+ m_alreadyStarted = m_instance->Start(m_channels, m_samplesPerSec, m_bitsPerSample, songTitle);
+ context.ApplyStateBlock();
+ m_callStart = false;
+ m_updateTrack = true;
+ }
+ else if (m_updateTrack)
+ {
+ /* Initial update of currently processed track */
+ UpdateTrack();
+ m_updateTrack = false;
+ }
+
+ if (m_instance && m_instance->IsDirty())
+ MarkDirtyRegion();
+ }
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIVisualisationControl::Render()
+{
+ if (m_instance && m_alreadyStarted)
+ {
+ auto& context = CServiceBroker::GetWinSystem()->GetGfxContext();
+
+ /*
+ * set the viewport - note: We currently don't have any control over how
+ * the addon renders, so the best we can do is attempt to define
+ * a viewport??
+ */
+ context.SetViewPort(m_posX, m_posY, m_width, m_height);
+ context.CaptureStateBlock();
+ m_instance->Render();
+ context.ApplyStateBlock();
+ context.RestoreViewPort();
+ }
+
+ CGUIControl::Render();
+}
+
+void CGUIVisualisationControl::UpdateVisibility(const CGUIListItem* item /* = nullptr*/)
+{
+ // if made invisible, start timer, only free addonptr after
+ // some period, configurable by window class
+ CGUIControl::UpdateVisibility(item);
+ if (!IsVisible() && m_attemptedLoad)
+ FreeResources();
+}
+
+bool CGUIVisualisationControl::CanFocusFromPoint(const CPoint& point) const
+{ // mouse is allowed to focus this control, but it doesn't actually receive focus
+ return IsVisible() && HitTest(point);
+}
+
+void CGUIVisualisationControl::FreeResources(bool immediately)
+{
+ DeInitVisualization();
+
+ CGUIControl::FreeResources(immediately);
+
+ CLog::Log(LOGDEBUG, "FreeVisualisation() done");
+}
+
+void CGUIVisualisationControl::OnInitialize(int channels, int samplesPerSec, int bitsPerSample)
+{
+ m_channels = channels;
+ m_samplesPerSec = samplesPerSec;
+ m_bitsPerSample = bitsPerSample;
+ m_callStart = true;
+}
+
+void CGUIVisualisationControl::OnAudioData(const float* audioData, unsigned int audioDataLength)
+{
+ if (!m_instance || !m_alreadyStarted || !audioData || audioDataLength == 0)
+ return;
+
+ // Save our audio data in the buffers
+ std::unique_ptr<CAudioBuffer> pBuffer(new CAudioBuffer(audioDataLength));
+ pBuffer->Set(audioData, audioDataLength);
+ m_vecBuffers.emplace_back(std::move(pBuffer));
+
+ if (m_vecBuffers.size() < m_numBuffers)
+ return;
+
+ std::unique_ptr<CAudioBuffer> ptrAudioBuffer = std::move(m_vecBuffers.front());
+ m_vecBuffers.pop_front();
+
+ // Transfer data to our visualisation
+ m_instance->AudioData(ptrAudioBuffer->Get(), ptrAudioBuffer->Size());
+}
+
+void CGUIVisualisationControl::UpdateTrack()
+{
+ if (!m_instance || !m_alreadyStarted)
+ return;
+
+ // get the current album art filename
+ m_albumThumb = CSpecialProtocol::TranslatePath(
+ CServiceBroker::GetGUI()->GetInfoManager().GetImage(MUSICPLAYER_COVER, WINDOW_INVALID));
+ if (m_albumThumb == "DefaultAlbumCover.png")
+ m_albumThumb = "";
+ else
+ CLog::Log(LOGDEBUG, "Updating visualization albumart: {}", m_albumThumb);
+
+ m_instance->UpdateAlbumart(m_albumThumb.c_str());
+
+ const MUSIC_INFO::CMusicInfoTag* tag =
+ CServiceBroker::GetGUI()->GetInfoManager().GetCurrentSongTag();
+ if (!tag)
+ return;
+
+ const std::string artist(tag->GetArtistString());
+ const std::string albumArtist(tag->GetAlbumArtistString());
+ const std::string genre(StringUtils::Join(
+ tag->GetGenre(),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+
+ KODI_ADDON_VISUALIZATION_TRACK track = {};
+ track.title = tag->GetTitle().c_str();
+ track.artist = artist.c_str();
+ track.album = tag->GetAlbum().c_str();
+ track.albumArtist = albumArtist.c_str();
+ track.genre = genre.c_str();
+ track.comment = tag->GetComment().c_str();
+ track.lyrics = tag->GetLyrics().c_str();
+ track.trackNumber = tag->GetTrackNumber();
+ track.discNumber = tag->GetDiscNumber();
+ track.duration = tag->GetDuration();
+ track.year = tag->GetYear();
+ track.rating = tag->GetUserrating();
+
+ m_instance->UpdateTrack(&track);
+}
+
+bool CGUIVisualisationControl::IsLocked()
+{
+ if (m_instance && m_alreadyStarted)
+ return m_instance->IsLocked();
+
+ return false;
+}
+
+bool CGUIVisualisationControl::HasPresets()
+{
+ if (m_instance && m_alreadyStarted)
+ return m_instance->HasPresets();
+
+ return false;
+}
+
+int CGUIVisualisationControl::GetActivePreset()
+{
+ if (m_instance && m_alreadyStarted)
+ return m_instance->GetActivePreset();
+
+ return -1;
+}
+
+void CGUIVisualisationControl::SetPreset(int idx)
+{
+ if (m_instance && m_alreadyStarted)
+ m_instance->LoadPreset(idx);
+}
+
+std::string CGUIVisualisationControl::GetActivePresetName()
+{
+ if (m_instance && m_alreadyStarted)
+ return m_instance->GetActivePresetName();
+
+ return "";
+}
+
+bool CGUIVisualisationControl::GetPresetList(std::vector<std::string>& vecpresets)
+{
+ if (m_instance && m_alreadyStarted)
+ return m_instance->GetPresetList(vecpresets);
+
+ return false;
+}
+
+bool CGUIVisualisationControl::InitVisualization()
+{
+ IAE* ae = CServiceBroker::GetActiveAE();
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!ae || !winSystem)
+ return false;
+
+ const std::string addon = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_MUSICPLAYER_VISUALISATION);
+ const ADDON::AddonInfoPtr addonBase =
+ CServiceBroker::GetAddonMgr().GetAddonInfo(addon, ADDON::AddonType::VISUALIZATION);
+ if (!addonBase)
+ return false;
+
+ ae->RegisterAudioCallback(this);
+
+ auto& context = winSystem->GetGfxContext();
+
+ context.CaptureStateBlock();
+
+ float x = context.ScaleFinalXCoord(GetXPosition(), GetYPosition());
+ float y = context.ScaleFinalYCoord(GetXPosition(), GetYPosition());
+ float w = context.ScaleFinalXCoord(GetXPosition() + GetWidth(), GetYPosition() + GetHeight()) - x;
+ float h = context.ScaleFinalYCoord(GetXPosition() + GetWidth(), GetYPosition() + GetHeight()) - y;
+ if (x < 0)
+ x = 0;
+ if (y < 0)
+ y = 0;
+ if (x + w > context.GetWidth())
+ w = context.GetWidth() - x;
+ if (y + h > context.GetHeight())
+ h = context.GetHeight() - y;
+
+ m_instance.reset(new KODI::ADDONS::CVisualization(addonBase, x, y, w, h));
+ CreateBuffers();
+
+ m_alreadyStarted = false;
+ context.ApplyStateBlock();
+ return true;
+}
+
+void CGUIVisualisationControl::DeInitVisualization()
+{
+ if (!m_attemptedLoad)
+ return;
+
+ CWinSystemBase* const winSystem = CServiceBroker::GetWinSystem();
+ if (!winSystem)
+ return;
+
+ IAE* ae = CServiceBroker::GetActiveAE();
+ if (ae)
+ ae->UnregisterAudioCallback(this);
+
+ m_attemptedLoad = false;
+
+ CGUIMessage msg(GUI_MSG_VISUALISATION_UNLOADING, m_controlID, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ CLog::Log(LOGDEBUG, "FreeVisualisation() started");
+
+ if (m_instance)
+ {
+ if (m_alreadyStarted)
+ {
+ auto& context = winSystem->GetGfxContext();
+
+ context.CaptureStateBlock();
+ m_instance->Stop();
+ context.ApplyStateBlock();
+ m_alreadyStarted = false;
+ }
+
+ m_instance.reset();
+ }
+
+ ClearBuffers();
+}
+
+void CGUIVisualisationControl::CreateBuffers()
+{
+ ClearBuffers();
+
+ m_numBuffers = 1;
+ if (m_instance)
+ m_numBuffers += m_instance->GetSyncDelay();
+ if (m_numBuffers > MAX_AUDIO_BUFFERS)
+ m_numBuffers = MAX_AUDIO_BUFFERS;
+ if (m_numBuffers < 1)
+ m_numBuffers = 1;
+}
+
+void CGUIVisualisationControl::ClearBuffers()
+{
+ m_numBuffers = 0;
+ m_vecBuffers.clear();
+}
diff --git a/xbmc/guilib/GUIVisualisationControl.dox b/xbmc/guilib/GUIVisualisationControl.dox
new file mode 100644
index 0000000..256b684
--- /dev/null
+++ b/xbmc/guilib/GUIVisualisationControl.dox
@@ -0,0 +1,42 @@
+/*!
+
+\page Visualisation_Control Visualisation Control
+\brief **Used to display a visualisation while music is playing.**
+
+\tableofcontents
+
+The visualisation control is used for displaying those funky patterns that jump
+to the music in Kodi. You can choose the position, and size of the visualisation
+displayed. Note that the control is only rendered if music is being played.
+
+
+--------------------------------------------------------------------------------
+\section Visualisation_Control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="visualisation" id ="3">
+ <description>My first visualisation control</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section Visualisation_Control_sect2 Available tags
+Only the [default control](http://kodi.wiki/view/Default_Control_Tags) tags are
+applicable to this control.
+
+
+--------------------------------------------------------------------------------
+\section Visualisation_Control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIVisualisationControl.h b/xbmc/guilib/GUIVisualisationControl.h
new file mode 100644
index 0000000..5d674aa
--- /dev/null
+++ b/xbmc/guilib/GUIVisualisationControl.h
@@ -0,0 +1,103 @@
+/*
+ * 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 "GUIControl.h"
+#include "cores/AudioEngine/Interfaces/IAudioCallback.h"
+
+#include <list>
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace ADDONS
+{
+class CVisualization;
+} // namespace ADDONS
+} // namespace KODI
+
+class CAudioBuffer
+{
+public:
+ explicit CAudioBuffer(int iSize);
+ virtual ~CAudioBuffer();
+ const float* Get() const;
+ int Size() const;
+ void Set(const float* psBuffer, int iSize);
+
+private:
+ CAudioBuffer(const CAudioBuffer&) = delete;
+ CAudioBuffer& operator=(const CAudioBuffer&) = delete;
+ CAudioBuffer();
+ float* m_pBuffer;
+ int m_iLen;
+};
+
+class CGUIVisualisationControl : public CGUIControl, public IAudioCallback
+{
+public:
+ CGUIVisualisationControl(
+ int parentID, int controlID, float posX, float posY, float width, float height);
+ CGUIVisualisationControl(const CGUIVisualisationControl& from);
+ CGUIVisualisationControl* Clone() const override
+ {
+ return new CGUIVisualisationControl(*this);
+ }; //! @todo check for naughties
+
+ // Child functions related to IAudioCallback
+ void OnInitialize(int channels, int samplesPerSec, int bitsPerSample) override;
+ void OnAudioData(const float* audioData, unsigned int audioDataLength) override;
+
+ // Child functions related to CGUIControl
+ void FreeResources(bool immediately = false) override;
+ void Process(unsigned int currentTime, CDirtyRegionList& dirtyregions) override;
+ void Render() override;
+ void UpdateVisibility(const CGUIListItem* item = nullptr) override;
+ bool OnAction(const CAction& action) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool CanFocus() const override { return false; }
+ bool CanFocusFromPoint(const CPoint& point) const override;
+
+ std::string Name();
+ void UpdateTrack();
+ bool HasPresets();
+ void SetPreset(int idx);
+ bool IsLocked();
+ int GetActivePreset();
+ std::string GetActivePresetName();
+ bool GetPresetList(std::vector<std::string>& vecpresets);
+
+private:
+ bool InitVisualization();
+ void DeInitVisualization();
+ inline void CreateBuffers();
+ inline void ClearBuffers();
+
+ bool m_callStart{false};
+ bool m_alreadyStarted{false};
+ bool m_attemptedLoad{false};
+ bool m_updateTrack{false};
+
+ std::list<std::unique_ptr<CAudioBuffer>> m_vecBuffers;
+ unsigned int m_numBuffers; /*!< Number of Audio buffers */
+ std::vector<std::string> m_presets; /*!< cached preset list */
+
+ /* values set from "OnInitialize" IAudioCallback */
+ int m_channels;
+ int m_samplesPerSec;
+ int m_bitsPerSample;
+
+ std::string m_albumThumb; /*!< track information */
+ std::string m_name; /*!< To add-on sended name */
+ std::string m_presetsPath; /*!< To add-on sended preset path */
+ std::string m_profilePath; /*!< To add-on sended profile path */
+
+ std::unique_ptr<KODI::ADDONS::CVisualization> m_instance;
+};
diff --git a/xbmc/guilib/GUIWindow.cpp b/xbmc/guilib/GUIWindow.cpp
new file mode 100644
index 0000000..a7aa0fe
--- /dev/null
+++ b/xbmc/guilib/GUIWindow.cpp
@@ -0,0 +1,1089 @@
+/*
+ * 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 "GUIWindow.h"
+
+#include "GUIAudioManager.h"
+#include "GUIComponent.h"
+#include "GUIControlFactory.h"
+#include "GUIControlGroup.h"
+#include "GUIControlProfiler.h"
+#include "GUIInfoManager.h"
+#include "GUIWindowManager.h"
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "input/Key.h"
+#include "input/WindowTranslator.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SingleLock.h"
+#include "utils/ColorUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+bool CGUIWindow::icompare::operator()(const std::string &s1, const std::string &s2) const
+{
+ return StringUtils::CompareNoCase(s1, s2) < 0;
+}
+
+CGUIWindow::CGUIWindow(int id, const std::string &xmlFile)
+{
+ CGUIWindow::SetID(id);
+ SetProperty("xmlfile", xmlFile);
+ m_lastControlID = 0;
+ m_needsScaling = true;
+ m_windowLoaded = false;
+ m_loadType = LOAD_EVERY_TIME;
+ m_closing = false;
+ m_active = false;
+ m_renderOrder = RENDER_ORDER_WINDOW;
+ m_dynamicResourceAlloc = true;
+ m_previousWindow = WINDOW_INVALID;
+ m_animationsEnabled = true;
+ m_manualRunActions = false;
+ m_exclusiveMouseControl = 0;
+ m_clearBackground = 0xff000000; // opaque black -> always clear
+ m_menuControlID = 0;
+ m_menuLastFocusedControlID = 0;
+ m_custom = false;
+}
+
+CGUIWindow::~CGUIWindow()
+{
+}
+
+bool CGUIWindow::Load(const std::string& strFileName, bool bContainsPath)
+{
+ if (m_windowLoaded || !g_SkinInfo)
+ return true; // no point loading if it's already there
+
+#ifdef _DEBUG
+ const auto start = std::chrono::steady_clock::now();
+#endif
+
+ const char* strLoadType;
+ switch (m_loadType)
+ {
+ case LOAD_ON_GUI_INIT:
+ strLoadType = "LOAD_ON_GUI_INIT";
+ break;
+ case KEEP_IN_MEMORY:
+ strLoadType = "KEEP_IN_MEMORY";
+ break;
+ case LOAD_EVERY_TIME:
+ default:
+ strLoadType = "LOAD_EVERY_TIME";
+ break;
+ }
+ CLog::Log(LOGINFO, "Loading skin file: {}, load type: {}", strFileName, strLoadType);
+
+ // Find appropriate skin folder + resolution to load from
+ std::string strPath;
+ std::string strLowerPath;
+ if (bContainsPath)
+ strPath = strFileName;
+ else
+ {
+ // FIXME: strLowerPath needs to eventually go since resToUse can get incorrectly overridden
+ std::string strFileNameLower = strFileName;
+ StringUtils::ToLower(strFileNameLower);
+ strLowerPath = g_SkinInfo->GetSkinPath(strFileNameLower, &m_coordsRes);
+ strPath = g_SkinInfo->GetSkinPath(strFileName, &m_coordsRes);
+ }
+
+ bool ret = LoadXML(strPath, strLowerPath);
+ if (ret)
+ {
+ m_windowLoaded = true;
+ OnWindowLoaded();
+
+#ifdef _DEBUG
+ const auto end = std::chrono::steady_clock::now();
+ const std::chrono::duration<double, std::milli> duration = end - start;
+ CLog::Log(LOGDEBUG, "Skin file {} loaded in {:.2f} ms", strPath, duration.count());
+#endif
+ }
+
+ return ret;
+}
+
+bool CGUIWindow::LoadXML(const std::string &strPath, const std::string &strLowerPath)
+{
+ // load window xml if we don't have it stored yet
+ if (!m_windowXMLRootElement)
+ {
+ CXBMCTinyXML xmlDoc;
+ std::string strPathLower = strPath;
+ StringUtils::ToLower(strPathLower);
+ if (!xmlDoc.LoadFile(strPath) && !xmlDoc.LoadFile(strPathLower) && !xmlDoc.LoadFile(strLowerPath))
+ {
+ CLog::Log(LOGERROR, "Unable to load window XML: {}. Line {}\n{}", strPath, xmlDoc.ErrorRow(),
+ xmlDoc.ErrorDesc());
+ SetID(WINDOW_INVALID);
+ return false;
+ }
+
+ // xml need a <window> root element
+ if (!StringUtils::EqualsNoCase(xmlDoc.RootElement()->Value(), "window"))
+ {
+ CLog::Log(LOGERROR, "XML file {} does not contain a <window> root element",
+ GetProperty("xmlfile").asString());
+ return false;
+ }
+
+ // store XML for further processing if window's load type is LOAD_EVERY_TIME or a reload is needed
+ m_windowXMLRootElement.reset(static_cast<TiXmlElement*>(xmlDoc.RootElement()->Clone()));
+ }
+ else
+ CLog::Log(LOGDEBUG, "Using already stored xml root node for {}", strPath);
+
+ return Load(Prepare(m_windowXMLRootElement).get());
+}
+
+std::unique_ptr<TiXmlElement> CGUIWindow::Prepare(const std::unique_ptr<TiXmlElement>& rootElement)
+{
+ if (!rootElement)
+ return nullptr;
+
+ // copy the root element as we will manipulate it
+ auto preparedRoot = std::make_unique<TiXmlElement>(*rootElement);
+
+ // Resolve any includes, constants, expressions that may be present
+ // and save include's conditions to the given map
+ g_SkinInfo->ResolveIncludes(preparedRoot.get(), &m_xmlIncludeConditions);
+
+ return preparedRoot;
+}
+
+bool CGUIWindow::Load(TiXmlElement *pRootElement)
+{
+ if (!pRootElement)
+ return false;
+
+ // set the scaling resolution so that any control creation or initialisation can
+ // be done with respect to the correct aspect ratio
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetScalingResolution(m_coordsRes, m_needsScaling);
+
+ // now load in the skin file
+ SetDefaults();
+
+ CGUIControlFactory::GetInfoColor(pRootElement, "backgroundcolor", m_clearBackground, GetID());
+ CGUIControlFactory::GetActions(pRootElement, "onload", m_loadActions);
+ CGUIControlFactory::GetActions(pRootElement, "onunload", m_unloadActions);
+ CRect parentRect(0, 0, static_cast<float>(m_coordsRes.iWidth), static_cast<float>(m_coordsRes.iHeight));
+ CGUIControlFactory::GetHitRect(pRootElement, m_hitRect, parentRect);
+
+ TiXmlElement *pChild = pRootElement->FirstChildElement();
+ while (pChild)
+ {
+ std::string strValue = pChild->Value();
+ if (strValue == "previouswindow" && pChild->FirstChild())
+ {
+ m_previousWindow = CWindowTranslator::TranslateWindow(pChild->FirstChild()->Value());
+ }
+ else if (strValue == "defaultcontrol" && pChild->FirstChild())
+ {
+ const char *always = pChild->Attribute("always");
+ if (always && StringUtils::EqualsNoCase(always, "true"))
+ m_defaultAlways = true;
+ m_defaultControl = atoi(pChild->FirstChild()->Value());
+ }
+ else if(strValue == "menucontrol" && pChild->FirstChild())
+ {
+ m_menuControlID = atoi(pChild->FirstChild()->Value());
+ }
+ else if (strValue == "visible" && pChild->FirstChild())
+ {
+ std::string condition;
+ CGUIControlFactory::GetConditionalVisibility(pRootElement, condition);
+ m_visibleCondition = CServiceBroker::GetGUI()->GetInfoManager().Register(condition, GetID());
+ }
+ else if (strValue == "animation" && pChild->FirstChild())
+ {
+ CRect rect(0, 0, static_cast<float>(m_coordsRes.iWidth), static_cast<float>(m_coordsRes.iHeight));
+ CAnimation anim;
+ anim.Create(pChild, rect, GetID());
+ m_animations.push_back(anim);
+ }
+ else if (strValue == "zorder" && pChild->FirstChild())
+ {
+ m_renderOrder = atoi(pChild->FirstChild()->Value());
+ }
+ else if (strValue == "coordinates")
+ {
+ XMLUtils::GetFloat(pChild, "posx", m_posX);
+ XMLUtils::GetFloat(pChild, "posy", m_posY);
+ XMLUtils::GetFloat(pChild, "left", m_posX);
+ XMLUtils::GetFloat(pChild, "top", m_posY);
+
+ TiXmlElement *originElement = pChild->FirstChildElement("origin");
+ while (originElement)
+ {
+ COrigin origin;
+ origin.x = CGUIControlFactory::ParsePosition(originElement->Attribute("x"), static_cast<float>(m_coordsRes.iWidth));
+ origin.y = CGUIControlFactory::ParsePosition(originElement->Attribute("y"), static_cast<float>(m_coordsRes.iHeight));
+ if (originElement->FirstChild())
+ origin.condition = CServiceBroker::GetGUI()->GetInfoManager().Register(originElement->FirstChild()->Value(), GetID());
+ m_origins.push_back(origin);
+ originElement = originElement->NextSiblingElement("origin");
+ }
+ }
+ else if (strValue == "camera")
+ { // z is fixed
+ m_camera.x = CGUIControlFactory::ParsePosition(pChild->Attribute("x"), static_cast<float>(m_coordsRes.iWidth));
+ m_camera.y = CGUIControlFactory::ParsePosition(pChild->Attribute("y"), static_cast<float>(m_coordsRes.iHeight));
+ m_hasCamera = true;
+ }
+ else if (strValue == "depth" && pChild->FirstChild())
+ {
+ float stereo = static_cast<float>(atof(pChild->FirstChild()->Value()));
+ m_stereo = std::max(-1.f, std::min(1.f, stereo));
+ }
+ else if (strValue == "controls")
+ {
+ TiXmlElement *pControl = pChild->FirstChildElement();
+ while (pControl)
+ {
+ if (StringUtils::EqualsNoCase(pControl->Value(), "control"))
+ {
+ LoadControl(pControl, nullptr, CRect(0, 0, static_cast<float>(m_coordsRes.iWidth), static_cast<float>(m_coordsRes.iHeight)));
+ }
+ pControl = pControl->NextSiblingElement();
+ }
+ }
+
+ pChild = pChild->NextSiblingElement();
+ }
+
+ return true;
+}
+
+void CGUIWindow::LoadControl(TiXmlElement* pControl, CGUIControlGroup *pGroup, const CRect &rect)
+{
+ // get control type
+ CGUIControlFactory factory;
+
+ CGUIControl* pGUIControl = factory.Create(GetID(), rect, pControl);
+ if (pGUIControl)
+ {
+ float maxX = pGUIControl->GetXPosition() + pGUIControl->GetWidth();
+ if (maxX > m_width)
+ {
+ m_width = maxX;
+ }
+
+ float maxY = pGUIControl->GetYPosition() + pGUIControl->GetHeight();
+ if (maxY > m_height)
+ {
+ m_height = maxY;
+ }
+ // if we are in a group, add to the group, else add to our window
+ if (pGroup)
+ pGroup->AddControl(pGUIControl);
+ else
+ AddControl(pGUIControl);
+ // if the new control is a group, then add it's controls
+ if (pGUIControl->IsGroup())
+ {
+ CGUIControlGroup *grp = static_cast<CGUIControlGroup*>(pGUIControl);
+ TiXmlElement *pSubControl = pControl->FirstChildElement("control");
+ CRect grpRect(grp->GetXPosition(), grp->GetYPosition(),
+ grp->GetXPosition() + grp->GetWidth(), grp->GetYPosition() + grp->GetHeight());
+ while (pSubControl)
+ {
+ LoadControl(pSubControl, grp, grpRect);
+ pSubControl = pSubControl->NextSiblingElement("control");
+ }
+ }
+ }
+}
+
+void CGUIWindow::OnWindowLoaded()
+{
+ DynamicResourceAlloc(true);
+}
+
+void CGUIWindow::CenterWindow()
+{
+ m_posX = (m_coordsRes.iWidth - GetWidth()) / 2;
+ m_posY = (m_coordsRes.iHeight - GetHeight()) / 2;
+}
+
+void CGUIWindow::DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (!IsControlDirty() && CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_guiSmartRedraw)
+ return;
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(m_coordsRes, m_needsScaling);
+ CServiceBroker::GetWinSystem()->GetGfxContext().AddGUITransform();
+ CGUIControlGroup::DoProcess(currentTime, dirtyregions);
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+
+ // check if currently focused control can have it
+ // and fallback to default control if not
+ CGUIControl* focusedControl = GetFocusedControl();
+ if (focusedControl && !focusedControl->CanFocus())
+ SET_CONTROL_FOCUS(m_defaultControl, 0);
+}
+
+void CGUIWindow::DoRender()
+{
+ // If we're rendering from a different thread, then we should wait for the main
+ // app thread to finish AllocResources(), as dynamic resources (images in particular)
+ // will try and be allocated from 2 different threads, which causes nasty things
+ // to occur.
+ if (!m_bAllocated) return;
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(m_coordsRes, m_needsScaling);
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().AddGUITransform();
+ CGUIControlGroup::DoRender();
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+
+ if (CGUIControlProfiler::IsRunning()) CGUIControlProfiler::Instance().EndFrame();
+}
+
+void CGUIWindow::AfterRender()
+{
+ // Check to see if we should close at this point
+ // We check after the controls have finished rendering, as we may have to close due to
+ // the controls rendering after the window has finished it's animation
+ // we call the base class instead of this class so that we can find the change
+ if (m_closing && !CGUIControlGroup::IsAnimating(ANIM_TYPE_WINDOW_CLOSE))
+ Close(true);
+
+}
+
+void CGUIWindow::Close_Internal(bool forceClose /*= false*/, int nextWindowID /*= 0*/, bool enableSound /*= true*/)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ if (!m_active)
+ return;
+
+ forceClose |= (nextWindowID == WINDOW_FULLSCREEN_VIDEO ||
+ nextWindowID == WINDOW_FULLSCREEN_GAME);
+
+ if (!forceClose && HasAnimation(ANIM_TYPE_WINDOW_CLOSE))
+ {
+ if (!m_closing)
+ {
+ if (enableSound && IsSoundEnabled())
+ CServiceBroker::GetGUI()->GetAudioManager().PlayWindowSound(GetID(), SOUND_DEINIT);
+
+ // Perform the window out effect
+ QueueAnimation(ANIM_TYPE_WINDOW_CLOSE);
+ m_closing = true;
+ }
+ return;
+ }
+
+ m_closing = false;
+ CGUIMessage msg(GUI_MSG_WINDOW_DEINIT, 0, 0, nextWindowID);
+ OnMessage(msg);
+}
+
+void CGUIWindow::Close(bool forceClose /*= false*/, int nextWindowID /*= 0*/, bool enableSound /*= true*/, bool bWait /* = true */)
+{
+ if (!CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ // make sure graphics lock is not held
+ CSingleExit leaveIt(CServiceBroker::GetWinSystem()->GetGfxContext());
+ int param2 = (forceClose ? 0x01 : 0) | (enableSound ? 0x02 : 0);
+ if (bWait)
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_WINDOW_CLOSE, nextWindowID, param2,
+ static_cast<void*>(this));
+ else
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_WINDOW_CLOSE, nextWindowID, param2,
+ static_cast<void*>(this));
+ }
+ else
+ Close_Internal(forceClose, nextWindowID, enableSound);
+}
+
+bool CGUIWindow::OnAction(const CAction &action)
+{
+ if (action.IsMouse() || action.IsGesture())
+ return EVENT_RESULT_UNHANDLED != OnMouseAction(action);
+
+ CGUIControl *focusedControl = GetFocusedControl();
+ if (focusedControl)
+ {
+ if (focusedControl->OnAction(action))
+ return true;
+ }
+ else
+ {
+ // no control has focus?
+ // set focus to the default control then
+ CGUIMessage msg(GUI_MSG_SETFOCUS, GetID(), m_defaultControl);
+ OnMessage(msg);
+ }
+
+ // default implementations
+ switch(action.GetID())
+ {
+ case ACTION_NAV_BACK:
+ case ACTION_PREVIOUS_MENU:
+ return OnBack(action.GetID());
+ case ACTION_SHOW_INFO:
+ return OnInfo(action.GetID());
+ case ACTION_MENU:
+ if (m_menuControlID > 0)
+ {
+ CGUIControl *menu = GetControl(m_menuControlID);
+ if (menu)
+ {
+ int focusControlId;
+ if (!menu->HasFocus())
+ {
+ // focus the menu control
+ focusControlId = m_menuControlID;
+ // To support a toggle behaviour we store the last focused control id
+ // to restore (focus) this control if the menu control has the focus
+ // while you press the menu button again.
+ m_menuLastFocusedControlID = GetFocusedControlID();
+ }
+ else
+ {
+ // restore the last focused control or if not exists use the default control
+ focusControlId = m_menuLastFocusedControlID > 0 ? m_menuLastFocusedControlID : m_defaultControl;
+ }
+
+ CGUIMessage msg = CGUIMessage(GUI_MSG_SETFOCUS, GetID(), focusControlId);
+ return OnMessage(msg);
+ }
+ }
+ break;
+ }
+
+ return false;
+}
+
+CPoint CGUIWindow::GetPosition() const
+{
+ for (unsigned int i = 0; i < m_origins.size(); i++)
+ {
+ // no condition implies true
+ if (!m_origins[i].condition || m_origins[i].condition->Get(INFO::DEFAULT_CONTEXT))
+ { // found origin
+ return CPoint(m_origins[i].x, m_origins[i].y);
+ }
+ }
+ return CGUIControlGroup::GetPosition();
+}
+
+// OnMouseAction - called by OnAction()
+EVENT_RESULT CGUIWindow::OnMouseAction(const CAction &action)
+{
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetScalingResolution(m_coordsRes, m_needsScaling);
+ CPoint mousePoint(action.GetAmount(0), action.GetAmount(1));
+ CServiceBroker::GetWinSystem()->GetGfxContext().InvertFinalCoords(mousePoint.x, mousePoint.y);
+
+ // create the mouse event
+ CMouseEvent event(action.GetID(), action.GetHoldTime(), action.GetAmount(2), action.GetAmount(3));
+ if (m_exclusiveMouseControl)
+ {
+ CGUIControl *child = GetControl(m_exclusiveMouseControl);
+ if (child)
+ {
+ CPoint renderPos = child->GetRenderPosition() - CPoint(child->GetXPosition(), child->GetYPosition());
+ return child->OnMouseEvent(mousePoint - renderPos, event);
+ }
+ }
+
+ UnfocusFromPoint(mousePoint);
+
+ return SendMouseEvent(mousePoint, event);
+}
+
+EVENT_RESULT CGUIWindow::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_RIGHT_CLICK)
+ { // no control found to absorb this click - go to previous menu
+ return OnAction(CAction(ACTION_PREVIOUS_MENU)) ? EVENT_RESULT_HANDLED : EVENT_RESULT_UNHANDLED;
+ }
+ return EVENT_RESULT_UNHANDLED;
+}
+
+/// \brief Called on window open.
+/// * Restores the control state(s)
+/// * Sets initial visibility of controls
+/// * Queue WindowOpen animation
+/// * Set overlay state
+/// Override this function and do any window-specific initialisation such
+/// as filling control contents and setting control focus before
+/// calling the base method.
+void CGUIWindow::OnInitWindow()
+{
+ // Play the window specific init sound
+ if (IsSoundEnabled())
+ CServiceBroker::GetGUI()->GetAudioManager().PlayWindowSound(GetID(), SOUND_INIT);
+
+ // set our rendered state
+ m_hasProcessed = false;
+ m_closing = false;
+ m_active = true;
+ ResetAnimations(); // we need to reset our animations as those windows that don't dynamically allocate
+ // need their anims reset. An alternative solution is turning off all non-dynamic
+ // allocation (which in some respects may be nicer, but it kills hdd spindown and the like)
+
+ // set our initial control visibility before restoring control state and
+ // focusing the default control, and again afterward to make sure that
+ // any controls that depend on the state of the focused control (and or on
+ // control states) are active.
+ SetInitialVisibility();
+ RestoreControlStates();
+ SetInitialVisibility();
+ QueueAnimation(ANIM_TYPE_WINDOW_OPEN);
+
+ if (!m_manualRunActions)
+ {
+ RunLoadActions();
+ }
+}
+
+// Called on window close.
+// * Saves control state(s)
+// Override this function and call the base class before doing any dynamic memory freeing
+void CGUIWindow::OnDeinitWindow(int nextWindowID)
+{
+ if (!m_manualRunActions)
+ {
+ RunUnloadActions();
+ }
+
+ SaveControlStates();
+ m_active = false;
+}
+
+bool CGUIWindow::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_LOAD:
+ {
+ Initialize();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CLog::Log(LOGDEBUG, "------ Window Init ({}) ------", GetProperty("xmlfile").asString());
+ if (m_dynamicResourceAlloc || !m_bAllocated) AllocResources(false);
+ OnInitWindow();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ CLog::Log(LOGDEBUG, "------ Window Deinit ({}) ------", GetProperty("xmlfile").asString());
+ OnDeinitWindow(message.GetParam1());
+ // now free the window
+ if (m_dynamicResourceAlloc) FreeResources();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_UNFOCUS_ALL:
+ {
+ //unfocus the current focused control in this window
+ CGUIControl *control = GetFocusedControl();
+ if(control)
+ {
+ //tell focused control that it has lost the focus
+ CGUIMessage msgLostFocus(GUI_MSG_LOSTFOCUS, GetID(), control->GetID(), control->GetID());
+ control->OnMessage(msgLostFocus);
+ CLog::Log(LOGDEBUG, "Unfocus WindowID: {}, ControlID: {}", GetID(), control->GetID());
+ }
+ return true;
+ }
+
+ case GUI_MSG_FOCUSED:
+ { // a control has been focused
+ if (HasID(message.GetSenderId()))
+ {
+ m_focusedControl = message.GetControlId();
+ // We ensure that all others childrens have focus disabled,
+ // this can happen when one control overlap the other one in the same
+ // coordinates with similar sizes and the mouse pointer is over them
+ // in this case only the control in the highest layer will have the focus
+ for (CGUIControl* control : m_children)
+ {
+ if (control->GetID() != m_focusedControl)
+ control->SetFocus(false);
+ }
+ return true;
+ }
+ break;
+ }
+ case GUI_MSG_LOSTFOCUS:
+ {
+ // nothing to do at the window level when we lose focus
+ return true;
+ }
+ case GUI_MSG_MOVE:
+ {
+ if (HasID(message.GetSenderId()))
+ return OnMove(message.GetControlId(), message.GetParam1());
+ break;
+ }
+ case GUI_MSG_SETFOCUS:
+ {
+ // CLog::Log(LOGDEBUG,"set focus to control:{} window:{} ({})", message.GetControlId(),message.GetSenderId(), GetID());
+ if ( message.GetControlId() )
+ {
+ // first unfocus the current control
+ CGUIControl *control = GetFocusedControl();
+ if (control)
+ {
+ CGUIMessage msgLostFocus(GUI_MSG_LOSTFOCUS, GetID(), control->GetID(), message.GetControlId());
+ control->OnMessage(msgLostFocus);
+ }
+
+ // get the control to focus
+ CGUIControl* pFocusedControl = GetFirstFocusableControl(message.GetControlId());
+ if (!pFocusedControl) pFocusedControl = GetControl(message.GetControlId());
+
+ // and focus it
+ if (pFocusedControl)
+ return pFocusedControl->OnMessage(message);
+ }
+ return true;
+ }
+ break;
+ case GUI_MSG_EXCLUSIVE_MOUSE:
+ {
+ m_exclusiveMouseControl = message.GetSenderId();
+ return true;
+ }
+ break;
+ case GUI_MSG_GESTURE_NOTIFY:
+ {
+ CAction action(ACTION_GESTURE_NOTIFY, 0, static_cast<float>(message.GetParam1()), static_cast<float>(message.GetParam2()), 0, 0);
+ EVENT_RESULT result = OnMouseAction(action);
+ auto res = new int(result);
+ message.SetPointer(static_cast<void*>(res));
+ return result != EVENT_RESULT_UNHANDLED;
+ }
+ case GUI_MSG_ADD_CONTROL:
+ {
+ if (message.GetPointer())
+ {
+ CGUIControl *control = static_cast<CGUIControl*>(message.GetPointer());
+ control->AllocResources();
+ AddControl(control);
+ }
+ return true;
+ }
+ case GUI_MSG_REMOVE_CONTROL:
+ {
+ if (message.GetPointer())
+ {
+ CGUIControl *control = static_cast<CGUIControl*>(message.GetPointer());
+ RemoveControl(control);
+ control->FreeResources(true);
+ delete control;
+ }
+ return true;
+ }
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ // only process those notifications that come from this window, or those intended for every window
+ if (HasID(message.GetSenderId()) || !message.GetSenderId())
+ {
+ if (message.GetParam1() == GUI_MSG_PAGE_CHANGE ||
+ message.GetParam1() == GUI_MSG_REFRESH_THUMBS ||
+ message.GetParam1() == GUI_MSG_REFRESH_LIST ||
+ message.GetParam1() == GUI_MSG_WINDOW_RESIZE)
+ { // alter the message accordingly, and send to all controls
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ {
+ CGUIControl *control = *it;
+ CGUIMessage msg(message.GetParam1(), message.GetControlId(), control->GetID(), message.GetParam2());
+ control->OnMessage(msg);
+ }
+ }
+ else if (message.GetParam1() == GUI_MSG_STATE_CHANGED)
+ MarkDirtyRegion(DIRTY_STATE_CHILD); //Don't force an dirtyRect, we don't know if / what has changed.
+ }
+ }
+ break;
+ }
+
+ return CGUIControlGroup::OnMessage(message);
+}
+
+bool CGUIWindow::NeedLoad() const
+{
+ return !m_windowLoaded || CServiceBroker::GetGUI()->GetInfoManager().ConditionsChangedValues(m_xmlIncludeConditions);
+}
+
+void CGUIWindow::AllocResources(bool forceLoad /*= false */)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+#ifdef _DEBUG
+ const auto start = std::chrono::steady_clock::now();
+#endif
+ // use forceLoad to determine if window needs (re)loading
+ forceLoad |= NeedLoad() || (m_loadType == LOAD_EVERY_TIME);
+
+ // if window is loaded and load is forced we have to free window resources first
+ if (m_windowLoaded && forceLoad)
+ FreeResources(true);
+
+ if (forceLoad)
+ {
+ std::string xmlFile = GetProperty("xmlfile").asString();
+ if (xmlFile.size())
+ {
+ bool bHasPath =
+ xmlFile.find('\\') != std::string::npos || xmlFile.find('/') != std::string::npos;
+ Load(xmlFile, bHasPath);
+ }
+ }
+
+#ifdef _DEBUG
+ const auto skinLoadEnd = std::chrono::steady_clock::now();
+#endif
+
+ // and now allocate resources
+ CGUIControlGroup::AllocResources();
+
+#ifdef _DEBUG
+ const auto end = std::chrono::steady_clock::now();
+ const std::chrono::duration<double, std::milli> skinLoadDuration = skinLoadEnd - start;
+ const std::chrono::duration<double, std::milli> duration = end - start;
+
+ if (forceLoad)
+ CLog::Log(LOGDEBUG, "Alloc resources: {:.2f} ms ({:.2f} ms skin load)", duration.count(),
+ skinLoadDuration.count());
+ else
+ {
+ CLog::Log(LOGDEBUG, "Window {} was already loaded", GetProperty("xmlfile").asString());
+ CLog::Log(LOGDEBUG, "Alloc resources: {:.2f} ms", duration.count());
+ }
+#endif
+ m_bAllocated = true;
+}
+
+void CGUIWindow::FreeResources(bool forceUnload /*= false */)
+{
+ m_bAllocated = false;
+ CGUIControlGroup::FreeResources();
+ //CServiceBroker::GetGUI()->GetTextureManager().Dump();
+ // unload the skin
+ if (m_loadType == LOAD_EVERY_TIME || forceUnload) ClearAll();
+ if (forceUnload)
+ {
+ m_windowXMLRootElement.reset();
+ m_xmlIncludeConditions.clear();
+ }
+}
+
+void CGUIWindow::DynamicResourceAlloc(bool bOnOff)
+{
+ m_dynamicResourceAlloc = bOnOff;
+ CGUIControlGroup::DynamicResourceAlloc(bOnOff);
+}
+
+void CGUIWindow::ClearAll()
+{
+ OnWindowUnload();
+ CGUIControlGroup::ClearAll();
+ m_windowLoaded = false;
+ m_dynamicResourceAlloc = true;
+ m_visibleCondition.reset();
+}
+
+bool CGUIWindow::Initialize()
+{
+ if (!CServiceBroker::GetGUI()->GetWindowManager().Initialized())
+ return false;
+ if (!NeedLoad())
+ return true;
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread())
+ AllocResources(false);
+ else
+ {
+ // if not app thread, send gui msg via app messenger
+ // and wait for results, so windowLoaded flag would be updated
+ CGUIMessage msg(GUI_MSG_WINDOW_LOAD, 0, 0);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, GetID(), true);
+ }
+ return m_windowLoaded;
+}
+
+void CGUIWindow::SetInitialVisibility()
+{
+ // reset our info manager caches
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ infoMgr.ResetCache();
+ infoMgr.GetInfoProviders().GetGUIControlsInfoProvider().ResetContainerMovingCache();
+ CGUIControlGroup::SetInitialVisibility();
+}
+
+bool CGUIWindow::IsActive() const
+{
+ return CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(GetID());
+}
+
+bool CGUIWindow::CheckAnimation(ANIMATION_TYPE animType)
+{
+ // special cases first
+ if (animType == ANIM_TYPE_WINDOW_CLOSE)
+ {
+ if (!m_bAllocated || !HasProcessed()) // can't process an animation if we aren't allocated or haven't processed
+ return false;
+ // make sure we update our visibility prior to queuing the window close anim
+ for (unsigned int i = 0; i < m_children.size(); i++)
+ m_children[i]->UpdateVisibility(nullptr);
+ }
+ return true;
+}
+
+bool CGUIWindow::IsAnimating(ANIMATION_TYPE animType)
+{
+ if (!m_animationsEnabled)
+ return false;
+ if (animType == ANIM_TYPE_WINDOW_CLOSE)
+ return m_closing;
+ return CGUIControlGroup::IsAnimating(animType);
+}
+
+bool CGUIWindow::Animate(unsigned int currentTime)
+{
+ if (m_animationsEnabled)
+ return CGUIControlGroup::Animate(currentTime);
+ else
+ {
+ m_transform.Reset();
+ return false;
+ }
+}
+
+void CGUIWindow::DisableAnimations()
+{
+ m_animationsEnabled = false;
+}
+
+// returns true if the control group with id groupID has controlID as
+// its focused control
+bool CGUIWindow::ControlGroupHasFocus(int groupID, int controlID)
+{
+ // 1. Run through and get control with groupID (assume unique)
+ // 2. Get it's selected item.
+ CGUIControl *group = GetFirstFocusableControl(groupID);
+ if (!group) group = GetControl(groupID);
+
+ if (group && group->IsGroup())
+ {
+ if (controlID == 0)
+ { // just want to know if the group is focused
+ return group->HasFocus();
+ }
+ else
+ {
+ CGUIMessage message(GUI_MSG_ITEM_SELECTED, GetID(), group->GetID());
+ group->OnMessage(message);
+ return (controlID == message.GetParam1());
+ }
+ }
+ return false;
+}
+
+void CGUIWindow::SaveControlStates()
+{
+ ResetControlStates();
+ if (!m_defaultAlways)
+ m_lastControlID = GetFocusedControlID();
+ for (iControls it = m_children.begin(); it != m_children.end(); ++it)
+ (*it)->SaveStates(m_controlStates);
+}
+
+void CGUIWindow::RestoreControlStates()
+{
+ for (const auto& it : m_controlStates)
+ {
+ CGUIMessage message(GUI_MSG_ITEM_SELECT, GetID(), it.m_id, it.m_data);
+ OnMessage(message);
+ }
+ int focusControl = (!m_defaultAlways && m_lastControlID) ? m_lastControlID : m_defaultControl;
+ SET_CONTROL_FOCUS(focusControl, 0);
+}
+
+void CGUIWindow::ResetControlStates()
+{
+ m_lastControlID = 0;
+ m_focusedControl = 0;
+ m_controlStates.clear();
+}
+
+bool CGUIWindow::OnBack(int actionID)
+{
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return true;
+}
+
+bool CGUIWindow::OnMove(int fromControl, int moveAction)
+{
+ const CGUIControl *control = GetFirstFocusableControl(fromControl);
+ if (!control) control = GetControl(fromControl);
+ if (!control)
+ { // no current control??
+ CLog::Log(LOGERROR, "Unable to find control {} in window {}", fromControl, GetID());
+ return false;
+ }
+ std::vector<int> moveHistory;
+ int nextControl = fromControl;
+ while (control)
+ { // grab the next control direction
+ moveHistory.push_back(nextControl);
+ CGUIAction action = control->GetAction(moveAction);
+ action.ExecuteActions(nextControl, GetParentID());
+ nextControl = action.GetNavigation();
+ if (!nextControl) // 0 isn't valid control id
+ return false;
+ // check our history - if the nextControl is in it, we can't focus it
+ for (unsigned int i = 0; i < moveHistory.size(); i++)
+ {
+ if (nextControl == moveHistory[i])
+ return false; // no control to focus so do nothing
+ }
+ control = GetFirstFocusableControl(nextControl);
+ if (control)
+ break; // found a focusable control
+ control = GetControl(nextControl); // grab the next control and try again
+ }
+ if (!control)
+ return false; // no control to focus
+ // if we get here we have our new control so focus it (and unfocus the current control)
+ SET_CONTROL_FOCUS(nextControl, 0);
+ return true;
+}
+
+void CGUIWindow::SetDefaults()
+{
+ m_renderOrder = RENDER_ORDER_WINDOW;
+ m_defaultAlways = false;
+ m_defaultControl = 0;
+ m_posX = m_posY = m_width = m_height = 0;
+ m_previousWindow = WINDOW_INVALID;
+ m_animations.clear();
+ m_origins.clear();
+ m_hasCamera = false;
+ m_stereo = 0.f;
+ m_animationsEnabled = true;
+ m_clearBackground = 0xff000000; // opaque black -> clear
+ m_hitRect.SetRect(0, 0, static_cast<float>(m_coordsRes.iWidth), static_cast<float>(m_coordsRes.iHeight));
+ m_menuControlID = 0;
+ m_menuLastFocusedControlID = 0;
+}
+
+CRect CGUIWindow::GetScaledBounds() const
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetScalingResolution(m_coordsRes, m_needsScaling);
+ CPoint pos(GetPosition());
+ CRect rect(pos.x, pos.y, pos.x + m_width, pos.y + m_height);
+ float z = 0;
+ CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalCoords(rect.x1, rect.y1, z);
+ CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalCoords(rect.x2, rect.y2, z);
+ return rect;
+}
+
+void CGUIWindow::OnEditChanged(int id, std::string &text)
+{
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), id);
+ OnMessage(msg);
+ text = msg.GetLabel();
+}
+
+bool CGUIWindow::SendMessage(int message, int id, int param1 /* = 0*/, int param2 /* = 0*/)
+{
+ CGUIMessage msg(message, GetID(), id, param1, param2);
+ return OnMessage(msg);
+}
+#ifdef _DEBUG
+void CGUIWindow::DumpTextureUse()
+{
+ CLog::Log(LOGDEBUG, "{} for window {}", __FUNCTION__, GetID());
+ CGUIControlGroup::DumpTextureUse();
+}
+#endif
+
+void CGUIWindow::SetProperty(const std::string &strKey, const CVariant &value)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ m_mapProperties[strKey] = value;
+}
+
+CVariant CGUIWindow::GetProperty(const std::string &strKey) const
+{
+ std::unique_lock<CCriticalSection> lock(const_cast<CGUIWindow&>(*this));
+ std::map<std::string, CVariant, icompare>::const_iterator iter = m_mapProperties.find(strKey);
+ if (iter == m_mapProperties.end())
+ return CVariant(CVariant::VariantTypeNull);
+
+ return iter->second;
+}
+
+void CGUIWindow::ClearProperties()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ m_mapProperties.clear();
+}
+
+void CGUIWindow::SetRunActionsManually()
+{
+ m_manualRunActions = true;
+}
+
+void CGUIWindow::RunLoadActions() const
+{
+ m_loadActions.ExecuteActions(GetID(), GetParentID());
+}
+
+void CGUIWindow::RunUnloadActions() const
+{
+ m_unloadActions.ExecuteActions(GetID(), GetParentID());
+}
+
+void CGUIWindow::ClearBackground()
+{
+ m_clearBackground.Update();
+ UTILS::COLOR::Color color = m_clearBackground;
+ if (color)
+ CServiceBroker::GetWinSystem()->GetGfxContext().Clear(color);
+}
+
+void CGUIWindow::SetID(int id)
+{
+ CGUIControlGroup::SetID(id);
+ m_idRange.clear();
+ m_idRange.push_back(id);
+}
+
+bool CGUIWindow::HasID(int controlID) const
+{
+ for (const auto& it : m_idRange)
+ {
+ if (controlID == it)
+ return true;
+ }
+ return false;
+}
diff --git a/xbmc/guilib/GUIWindow.h b/xbmc/guilib/GUIWindow.h
new file mode 100644
index 0000000..20d92f1
--- /dev/null
+++ b/xbmc/guilib/GUIWindow.h
@@ -0,0 +1,293 @@
+/*
+ * 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
+
+/*!
+\file GUIWindow.h
+\brief
+*/
+
+#include "GUIAction.h"
+#include "GUIControlGroup.h"
+#include <memory>
+#include "threads/CriticalSection.h"
+
+class CFileItem; typedef std::shared_ptr<CFileItem> CFileItemPtr;
+
+#include <limits.h>
+#include <map>
+#include <vector>
+
+enum RenderOrder {
+ RENDER_ORDER_WINDOW = 0,
+ RENDER_ORDER_DIALOG = 1,
+ RENDER_ORDER_WINDOW_SCREENSAVER = INT_MAX,
+ RENDER_ORDER_WINDOW_POINTER = INT_MAX - 1,
+ RENDER_ORDER_WINDOW_DEBUG = INT_MAX - 2,
+ RENDER_ORDER_DIALOG_TELETEXT = INT_MAX - 3
+};
+
+// forward
+class TiXmlNode;
+class TiXmlElement;
+class CXBMCTinyXML;
+class CVariant;
+
+class COrigin
+{
+public:
+ COrigin()
+ {
+ x = y = 0;
+ };
+ float x;
+ float y;
+ INFO::InfoPtr condition;
+};
+
+/*!
+ \ingroup winmsg
+ \brief
+ */
+class CGUIWindow : public CGUIControlGroup, protected CCriticalSection
+{
+public:
+ enum LOAD_TYPE { LOAD_EVERY_TIME, LOAD_ON_GUI_INIT, KEEP_IN_MEMORY };
+
+ CGUIWindow(int id, const std::string &xmlFile);
+ ~CGUIWindow(void) override;
+
+ bool Initialize(); // loads the window
+ bool Load(const std::string& strFileName, bool bContainsPath = false);
+
+ void CenterWindow();
+
+ void DoProcess(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+
+ /*! \brief Main render function, called every frame.
+ Window classes should override this only if they need to alter how something is rendered.
+ General updating on a per-frame basis should be handled in FrameMove instead, as DoRender
+ is not necessarily re-entrant.
+ \sa FrameMove
+ */
+ void DoRender() override;
+
+ /*! \brief Do any post render activities.
+ Check if window closing animation is finished and finalize window closing.
+ */
+ void AfterRender();
+
+ /*! \brief Main update function, called every frame prior to rendering
+ Any window that requires updating on a frame by frame basis (such as to maintain
+ timers and the like) should override this function.
+ */
+ virtual void FrameMove() {}
+
+ void Close(bool forceClose = false, int nextWindowID = 0, bool enableSound = true, bool bWait = true);
+
+ // OnAction() is called by our window manager. We should process any messages
+ // that should be handled at the window level in the derived classes, and any
+ // unhandled messages should be dropped through to here where we send the message
+ // on to the currently focused control. Returns true if the action has been handled
+ // and does not need to be passed further down the line (to our global action handlers)
+ bool OnAction(const CAction &action) override;
+
+ using CGUIControlGroup::OnBack;
+ virtual bool OnBack(int actionID);
+ using CGUIControlGroup::OnInfo;
+ virtual bool OnInfo(int actionID) { return false; }
+
+ /*! \brief Clear the background (if necessary) prior to rendering the window
+ */
+ virtual void ClearBackground();
+
+ bool OnMove(int fromControl, int moveAction);
+ bool OnMessage(CGUIMessage& message) override;
+
+ bool ControlGroupHasFocus(int groupID, int controlID);
+ void SetID(int id) override;
+ virtual bool HasID(int controlID) const;
+ const std::vector<int>& GetIDRange() const { return m_idRange; }
+ int GetPreviousWindow() { return m_previousWindow; }
+ CRect GetScaledBounds() const;
+ void ClearAll() override;
+ using CGUIControlGroup::AllocResources;
+ virtual void AllocResources(bool forceLoad = false);
+ void FreeResources(bool forceUnLoad = false) override;
+ void DynamicResourceAlloc(bool bOnOff) override;
+ virtual bool IsDialog() const { return false; }
+ virtual bool IsDialogRunning() const { return false; }
+ virtual bool IsModalDialog() const { return false; }
+ virtual bool IsMediaWindow() const { return false; }
+ virtual bool HasListItems() const { return false; }
+ virtual bool IsSoundEnabled() const { return true; }
+ virtual CFileItemPtr GetCurrentListItem(int offset = 0) { return CFileItemPtr(); }
+ virtual int GetViewContainerID() const { return 0; }
+ virtual int GetViewCount() const { return 0; }
+ virtual bool CanBeActivated() const { return true; }
+ virtual bool IsActive() const;
+ void SetCoordsRes(const RESOLUTION_INFO& res) { m_coordsRes = res; }
+ const RESOLUTION_INFO& GetCoordsRes() const { return m_coordsRes; }
+ void SetLoadType(LOAD_TYPE loadType) { m_loadType = loadType; }
+ LOAD_TYPE GetLoadType() { return m_loadType; }
+ int GetRenderOrder() { return m_renderOrder; }
+ void SetInitialVisibility() override;
+ bool IsVisible() const override { return true; }; // windows are always considered visible as they implement their own
+ // versions of UpdateVisibility, and are deemed visible if they're in
+ // the window manager's active list.
+ virtual bool HasVisibleControls() { return true; }; //Assume that window always has visible controls
+
+ bool IsAnimating(ANIMATION_TYPE animType) override;
+
+ /*!
+ \brief Return if the window is a custom window
+ \return true if the window is an custom window otherwise false
+ */
+ bool IsCustom() const { return m_custom; }
+
+ /*!
+ \brief Mark this window as custom window
+ \param custom true if this window is a custom window, false if not
+ */
+ void SetCustom(bool custom) { m_custom = custom; }
+
+ void DisableAnimations();
+
+ virtual void ResetControlStates();
+ void UpdateControlStats() override {}; // Do not count window itself
+
+ void SetRunActionsManually();
+ void RunLoadActions() const;
+ void RunUnloadActions() const;
+
+ /*! \brief Set a property
+ Sets the value of a property referenced by a key.
+ \param key name of the property to set
+ \param value value to set, may be a string, integer, boolean or double.
+ \sa GetProperty
+ */
+ void SetProperty(const std::string &key, const CVariant &value);
+
+ /*! \brief Retrieve a property
+ \param key name of the property to retrieve
+ \return value of the property, empty if it doesn't exist
+ */
+ CVariant GetProperty(const std::string &key) const;
+
+ /*! \brief Clear a all the window's properties
+ \sa SetProperty, HasProperty, GetProperty
+ */
+ void ClearProperties();
+
+#ifdef _DEBUG
+ void DumpTextureUse() override;
+#endif
+ bool HasSaveLastControl() const { return !m_defaultAlways; }
+
+ virtual void OnDeinitWindow(int nextWindowID);
+protected:
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+
+ /*!
+ \brief Load the window XML from the given path
+ \param strPath the path to the window XML
+ \param strLowerPath a lowered path to the window XML
+ */
+ virtual bool LoadXML(const std::string& strPath, const std::string &strLowerPath);
+
+ /*!
+ \brief Loads the window from the given XML element
+ \param pRootElement the XML element
+ \return true if the window is loaded from the given XML otherwise false.
+ */
+ virtual bool Load(TiXmlElement *pRootElement);
+
+ /*!
+ \brief Prepare the XML for load
+ \param rootElement the original XML element
+ \return the prepared XML (resolved includes, constants and expression)
+ */
+ virtual std::unique_ptr<TiXmlElement> Prepare(const std::unique_ptr<TiXmlElement>& rootElement);
+
+ /*!
+ \brief Check if window needs a (re)load. The window need to be (re)loaded when window is not loaded or include conditions values were changed
+ */
+ bool NeedLoad() const;
+
+ virtual void SetDefaults();
+ virtual void OnWindowUnload() {}
+ virtual void OnWindowLoaded();
+ virtual void OnInitWindow();
+ void Close_Internal(bool forceClose = false, int nextWindowID = 0, bool enableSound = true);
+ EVENT_RESULT OnMouseAction(const CAction &action);
+ bool Animate(unsigned int currentTime) override;
+ bool CheckAnimation(ANIMATION_TYPE animType) override;
+
+ // control state saving on window close
+ virtual void SaveControlStates();
+ virtual void RestoreControlStates();
+
+ // methods for updating controls and sending messages
+ void OnEditChanged(int id, std::string &text);
+ bool SendMessage(int message, int id, int param1 = 0, int param2 = 0);
+
+ void LoadControl(TiXmlElement* pControl, CGUIControlGroup *pGroup, const CRect &rect);
+
+ std::vector<int> m_idRange;
+ RESOLUTION_INFO m_coordsRes; // resolution that the window coordinates are in.
+ bool m_needsScaling;
+ bool m_windowLoaded; // true if the window's xml file has been loaded
+ LOAD_TYPE m_loadType;
+ bool m_dynamicResourceAlloc;
+ bool m_closing;
+ bool m_active; // true if window is active or dialog is running
+ KODI::GUILIB::GUIINFO::CGUIInfoColor m_clearBackground; // colour to clear the window
+
+ int m_renderOrder; // for render order of dialogs
+
+ /*! \brief Grabs the window's top,left position in skin coordinates
+ The window origin may change based on `<origin>` tag conditions in the skin.
+
+ \return the window's origin in skin coordinates
+ */
+ CPoint GetPosition() const override;
+ std::vector<COrigin> m_origins; // positions of dialogs depending on base window
+
+ // control states
+ int m_lastControlID;
+ std::vector<CControlState> m_controlStates;
+ int m_previousWindow;
+
+ bool m_animationsEnabled;
+ struct icompare
+ {
+ bool operator()(const std::string &s1, const std::string &s2) const;
+ };
+
+ CGUIAction m_loadActions;
+ CGUIAction m_unloadActions;
+
+ /*! \brief window root xml definition after resolving any skin includes.
+ Stored to avoid parsing the XML every time the window is loaded.
+ */
+ std::unique_ptr<TiXmlElement> m_windowXMLRootElement;
+
+ bool m_manualRunActions;
+
+ int m_exclusiveMouseControl; ///< \brief id of child control that wishes to receive all mouse events \sa GUI_MSG_EXCLUSIVE_MOUSE
+
+ int m_menuControlID;
+ int m_menuLastFocusedControlID;
+ bool m_custom;
+
+private:
+ std::map<std::string, CVariant, icompare> m_mapProperties;
+ std::map<INFO::InfoPtr, bool> m_xmlIncludeConditions; ///< \brief used to store conditions used to resolve includes for this window
+};
+
diff --git a/xbmc/guilib/GUIWindowManager.cpp b/xbmc/guilib/GUIWindowManager.cpp
new file mode 100644
index 0000000..f15489e
--- /dev/null
+++ b/xbmc/guilib/GUIWindowManager.cpp
@@ -0,0 +1,1808 @@
+/*
+ * 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 "GUIWindowManager.h"
+
+#include "GUIAudioManager.h"
+#include "GUIDialog.h"
+#include "GUIInfoManager.h"
+#include "GUIPassword.h"
+#include "GUITexture.h"
+#include "ServiceBroker.h"
+#include "WindowIDs.h"
+#include "addons/Skin.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "addons/interfaces/gui/Window.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "events/windows/GUIWindowEventLog.h"
+#include "favourites/GUIDialogFavourites.h"
+#include "favourites/GUIWindowFavourites.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "music/dialogs/GUIDialogInfoProviderSettings.h"
+#include "music/dialogs/GUIDialogMusicInfo.h"
+#include "music/windows/GUIWindowMusicNav.h"
+#include "music/windows/GUIWindowMusicPlaylist.h"
+#include "music/windows/GUIWindowMusicPlaylistEditor.h"
+#include "music/windows/GUIWindowVisualisation.h"
+#include "pictures/GUIWindowPictures.h"
+#include "pictures/GUIWindowSlideShow.h"
+#include "profiles/windows/GUIWindowSettingsProfile.h"
+#include "programs/GUIWindowPrograms.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/windows/GUIWindowSettings.h"
+#include "settings/windows/GUIWindowSettingsCategory.h"
+#include "settings/windows/GUIWindowSettingsScreenCalibration.h"
+#include "threads/SingleLock.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+#include "video/dialogs/GUIDialogVideoOSD.h"
+#include "video/windows/GUIWindowFullScreen.h"
+#include "video/windows/GUIWindowVideoNav.h"
+#include "video/windows/GUIWindowVideoPlaylist.h"
+#include "weather/GUIWindowWeather.h"
+#include "windows/GUIWindowDebugInfo.h"
+#include "windows/GUIWindowFileManager.h"
+#include "windows/GUIWindowHome.h"
+#include "windows/GUIWindowLoginScreen.h"
+#include "windows/GUIWindowPointer.h"
+#include "windows/GUIWindowScreensaver.h"
+#include "windows/GUIWindowScreensaverDim.h"
+#include "windows/GUIWindowSplash.h"
+#include "windows/GUIWindowStartup.h"
+#include "windows/GUIWindowSystemInfo.h"
+
+#include <mutex>
+
+// Dialog includes
+#include "music/dialogs/GUIDialogMusicOSD.h"
+#include "music/dialogs/GUIDialogVisualisationPresetList.h"
+#include "dialogs/GUIDialogTextViewer.h"
+#include "network/GUIDialogNetworkSetup.h"
+#include "dialogs/GUIDialogMediaSource.h"
+#if defined(HAS_GL) || defined(HAS_DX)
+#include "video/dialogs/GUIDialogCMSSettings.h"
+#endif
+#include "addons/gui/GUIDialogAddonInfo.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogBusyNoCancel.h"
+#include "dialogs/GUIDialogButtonMenu.h"
+#include "dialogs/GUIDialogColorPicker.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "dialogs/GUIDialogGamepad.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogKeyboardGeneric.h"
+#include "dialogs/GUIDialogKeyboardTouch.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "dialogs/GUIDialogOK.h"
+#include "dialogs/GUIDialogPlayerControls.h"
+#include "dialogs/GUIDialogPlayerProcessInfo.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSeekBar.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogSmartPlaylistEditor.h"
+#include "dialogs/GUIDialogSmartPlaylistRule.h"
+#include "dialogs/GUIDialogSubMenu.h"
+#include "dialogs/GUIDialogVolumeBar.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "music/dialogs/GUIDialogSongInfo.h"
+#include "pictures/GUIDialogPictureInfo.h"
+#include "profiles/dialogs/GUIDialogLockSettings.h"
+#include "profiles/dialogs/GUIDialogProfileSettings.h"
+#include "settings/dialogs/GUIDialogContentSettings.h"
+#include "settings/dialogs/GUIDialogLibExportSettings.h"
+#include "video/dialogs/GUIDialogAudioSettings.h"
+#include "video/dialogs/GUIDialogSubtitleSettings.h"
+#include "video/dialogs/GUIDialogVideoBookmarks.h"
+#include "video/dialogs/GUIDialogVideoSettings.h"
+
+/* PVR related include Files */
+#include "pvr/dialogs/GUIDialogPVRChannelGuide.h"
+#include "pvr/dialogs/GUIDialogPVRChannelManager.h"
+#include "pvr/dialogs/GUIDialogPVRChannelsOSD.h"
+#include "pvr/dialogs/GUIDialogPVRClientPriorities.h"
+#include "pvr/dialogs/GUIDialogPVRGroupManager.h"
+#include "pvr/dialogs/GUIDialogPVRGuideControls.h"
+#include "pvr/dialogs/GUIDialogPVRGuideInfo.h"
+#include "pvr/dialogs/GUIDialogPVRGuideSearch.h"
+#include "pvr/dialogs/GUIDialogPVRRadioRDSInfo.h"
+#include "pvr/dialogs/GUIDialogPVRRecordingInfo.h"
+#include "pvr/dialogs/GUIDialogPVRRecordingSettings.h"
+#include "pvr/dialogs/GUIDialogPVRTimerSettings.h"
+#include "pvr/windows/GUIWindowPVRChannels.h"
+#include "pvr/windows/GUIWindowPVRGuide.h"
+#include "pvr/windows/GUIWindowPVRRecordings.h"
+#include "pvr/windows/GUIWindowPVRSearch.h"
+#include "pvr/windows/GUIWindowPVRTimerRules.h"
+#include "pvr/windows/GUIWindowPVRTimers.h"
+
+#include "video/dialogs/GUIDialogTeletext.h"
+#include "dialogs/GUIDialogSlider.h"
+#ifdef HAS_DVD_DRIVE
+#include "dialogs/GUIDialogPlayEject.h"
+#endif
+#include "dialogs/GUIDialogMediaFilter.h"
+#include "video/dialogs/GUIDialogSubtitles.h"
+
+#include "peripherals/dialogs/GUIDialogPeripherals.h"
+#include "peripherals/dialogs/GUIDialogPeripheralSettings.h"
+
+/* Game related include files */
+#include "cores/RetroPlayer/guiwindows/GameWindowFullScreen.h"
+#include "games/controllers/windows/GUIControllerWindow.h"
+#include "games/dialogs/osd/DialogGameAdvancedSettings.h"
+#include "games/dialogs/osd/DialogGameOSD.h"
+#include "games/dialogs/osd/DialogGameSaves.h"
+#include "games/dialogs/osd/DialogGameStretchMode.h"
+#include "games/dialogs/osd/DialogGameVideoFilter.h"
+#include "games/dialogs/osd/DialogGameVideoRotation.h"
+#include "games/dialogs/osd/DialogGameVolume.h"
+#include "games/dialogs/osd/DialogInGameSaves.h"
+#include "games/ports/windows/GUIPortWindow.h"
+#include "games/windows/GUIWindowGames.h"
+
+using namespace KODI;
+using namespace PVR;
+using namespace PERIPHERALS;
+
+CGUIWindowManager::CGUIWindowManager()
+{
+ m_pCallback = nullptr;
+ m_iNested = 0;
+ m_initialized = false;
+}
+
+CGUIWindowManager::~CGUIWindowManager() = default;
+
+void CGUIWindowManager::Initialize()
+{
+ m_tracker.SelectAlgorithm();
+
+ m_initialized = true;
+
+ LoadNotOnDemandWindows();
+
+ CServiceBroker::GetAppMessenger()->RegisterReceiver(this);
+}
+
+void CGUIWindowManager::CreateWindows()
+{
+ Add(new CGUIWindowHome);
+ Add(new CGUIWindowPrograms);
+ Add(new CGUIWindowPictures);
+ Add(new CGUIWindowFileManager);
+ Add(new CGUIWindowSettings);
+ Add(new CGUIWindowSystemInfo);
+ Add(new CGUIWindowSettingsScreenCalibration);
+ Add(new CGUIWindowSettingsCategory);
+ Add(new CGUIWindowVideoNav);
+ Add(new CGUIWindowVideoPlaylist);
+ Add(new CGUIWindowLoginScreen);
+ Add(new CGUIWindowSettingsProfile);
+ Add(new CGUIWindow(WINDOW_SKIN_SETTINGS, "SkinSettings.xml"));
+ Add(new CGUIWindowAddonBrowser);
+ Add(new CGUIWindowScreensaverDim);
+ Add(new CGUIWindowDebugInfo);
+ Add(new CGUIWindowPointer);
+ Add(new CGUIDialogYesNo);
+ Add(new CGUIDialogProgress);
+ Add(new CGUIDialogExtendedProgressBar);
+ Add(new CGUIDialogKeyboardGeneric);
+ Add(new CGUIDialogKeyboardTouch);
+ Add(new CGUIDialogVolumeBar);
+ Add(new CGUIDialogSeekBar);
+ Add(new CGUIDialogSubMenu);
+ Add(new CGUIDialogContextMenu);
+ Add(new CGUIDialogKaiToast);
+ Add(new CGUIDialogNumeric);
+ Add(new CGUIDialogGamepad);
+ Add(new CGUIDialogButtonMenu);
+ Add(new CGUIDialogPlayerControls);
+ Add(new CGUIDialogPlayerProcessInfo);
+ Add(new CGUIDialogSlider);
+ Add(new CGUIDialogMusicOSD);
+ Add(new CGUIDialogVisualisationPresetList);
+#if defined(HAS_GL) || defined(HAS_DX)
+ Add(new CGUIDialogCMSSettings);
+#endif
+ Add(new CGUIDialogVideoSettings);
+ Add(new CGUIDialogAudioSettings);
+ Add(new CGUIDialogSubtitleSettings);
+ Add(new CGUIDialogVideoBookmarks);
+ // Don't add the filebrowser dialog - it's created and added when it's needed
+ Add(new CGUIDialogNetworkSetup);
+ Add(new CGUIDialogMediaSource);
+ Add(new CGUIDialogProfileSettings);
+ Add(new CGUIDialogFavourites);
+ Add(new CGUIDialogSongInfo);
+ Add(new CGUIDialogSmartPlaylistEditor);
+ Add(new CGUIDialogSmartPlaylistRule);
+ Add(new CGUIDialogBusy);
+ Add(new CGUIDialogBusyNoCancel);
+ Add(new CGUIDialogPictureInfo);
+ Add(new CGUIDialogAddonInfo);
+ Add(new CGUIDialogAddonSettings);
+
+ Add(new CGUIDialogLockSettings);
+
+ Add(new CGUIDialogContentSettings);
+
+ Add(new CGUIDialogLibExportSettings);
+
+ Add(new CGUIDialogInfoProviderSettings);
+
+#ifdef HAS_DVD_DRIVE
+ Add(new CGUIDialogPlayEject);
+#endif
+
+ Add(new CGUIDialogPeripherals);
+ Add(new CGUIDialogPeripheralSettings);
+
+ Add(new CGUIDialogMediaFilter);
+ Add(new CGUIDialogSubtitles);
+
+ Add(new CGUIWindowMusicPlayList);
+ Add(new CGUIWindowMusicNav);
+ Add(new CGUIWindowMusicPlaylistEditor);
+
+ /* Load PVR related Windows and Dialogs */
+ Add(new CGUIDialogTeletext);
+ Add(new CGUIWindowPVRTVChannels);
+ Add(new CGUIWindowPVRTVRecordings);
+ Add(new CGUIWindowPVRTVGuide);
+ Add(new CGUIWindowPVRTVTimers);
+ Add(new CGUIWindowPVRTVTimerRules);
+ Add(new CGUIWindowPVRTVSearch);
+ Add(new CGUIWindowPVRRadioChannels);
+ Add(new CGUIWindowPVRRadioRecordings);
+ Add(new CGUIWindowPVRRadioGuide);
+ Add(new CGUIWindowPVRRadioTimers);
+ Add(new CGUIWindowPVRRadioTimerRules);
+ Add(new CGUIWindowPVRRadioSearch);
+ Add(new CGUIDialogPVRRadioRDSInfo);
+ Add(new CGUIDialogPVRGuideInfo);
+ Add(new CGUIDialogPVRRecordingInfo);
+ Add(new CGUIDialogPVRTimerSettings);
+ Add(new CGUIDialogPVRGroupManager);
+ Add(new CGUIDialogPVRChannelManager);
+ Add(new CGUIDialogPVRGuideSearch);
+ Add(new CGUIDialogPVRChannelsOSD);
+ Add(new CGUIDialogPVRChannelGuide);
+ Add(new CGUIDialogPVRRecordingSettings);
+ Add(new CGUIDialogPVRClientPriorities);
+ Add(new CGUIDialogPVRGuideControls);
+
+ Add(new CGUIDialogSelect);
+ Add(new CGUIDialogColorPicker);
+ Add(new CGUIDialogMusicInfo);
+ Add(new CGUIDialogOK);
+ Add(new CGUIDialogVideoInfo);
+ Add(new CGUIDialogTextViewer);
+ Add(new CGUIWindowFullScreen);
+ Add(new CGUIWindowVisualisation);
+ Add(new CGUIWindowSlideShow);
+
+ Add(new CGUIDialogVideoOSD);
+ Add(new CGUIWindowScreensaver);
+ Add(new CGUIWindowWeather);
+ Add(new CGUIWindowStartup);
+ Add(new CGUIWindowSplash);
+
+ Add(new CGUIWindowEventLog);
+
+ Add(new CGUIWindowFavourites);
+
+ Add(new GAME::CGUIControllerWindow);
+ Add(new GAME::CGUIPortWindow);
+ Add(new GAME::CGUIWindowGames);
+ Add(new GAME::CDialogGameOSD);
+ Add(new GAME::CDialogGameSaves);
+ Add(new GAME::CDialogGameVideoFilter);
+ Add(new GAME::CDialogGameStretchMode);
+ Add(new GAME::CDialogGameVolume);
+ Add(new GAME::CDialogGameAdvancedSettings);
+ Add(new GAME::CDialogGameVideoRotation);
+ Add(new GAME::CDialogInGameSaves);
+ Add(new RETRO::CGameWindowFullScreen);
+}
+
+bool CGUIWindowManager::DestroyWindows()
+{
+ try
+ {
+ DestroyWindow(WINDOW_SPLASH);
+ DestroyWindow(WINDOW_MUSIC_PLAYLIST);
+ DestroyWindow(WINDOW_MUSIC_PLAYLIST_EDITOR);
+ DestroyWindow(WINDOW_MUSIC_NAV);
+ DestroyWindow(WINDOW_DIALOG_MUSIC_INFO);
+ DestroyWindow(WINDOW_DIALOG_VIDEO_INFO);
+ DestroyWindow(WINDOW_VIDEO_PLAYLIST);
+ DestroyWindow(WINDOW_VIDEO_NAV);
+ DestroyWindow(WINDOW_FILES);
+ DestroyWindow(WINDOW_DIALOG_YES_NO);
+ DestroyWindow(WINDOW_DIALOG_PROGRESS);
+ DestroyWindow(WINDOW_DIALOG_NUMERIC);
+ DestroyWindow(WINDOW_DIALOG_GAMEPAD);
+ DestroyWindow(WINDOW_DIALOG_SUB_MENU);
+ DestroyWindow(WINDOW_DIALOG_BUTTON_MENU);
+ DestroyWindow(WINDOW_DIALOG_CONTEXT_MENU);
+ DestroyWindow(WINDOW_DIALOG_PLAYER_CONTROLS);
+ DestroyWindow(WINDOW_DIALOG_PLAYER_PROCESS_INFO);
+ DestroyWindow(WINDOW_DIALOG_MUSIC_OSD);
+ DestroyWindow(WINDOW_DIALOG_VIS_PRESET_LIST);
+ DestroyWindow(WINDOW_DIALOG_SELECT);
+ DestroyWindow(WINDOW_DIALOG_OK);
+ DestroyWindow(WINDOW_DIALOG_KEYBOARD);
+ DestroyWindow(WINDOW_DIALOG_KEYBOARD_TOUCH);
+ DestroyWindow(WINDOW_FULLSCREEN_VIDEO);
+ DestroyWindow(WINDOW_DIALOG_PROFILE_SETTINGS);
+ DestroyWindow(WINDOW_DIALOG_LOCK_SETTINGS);
+ DestroyWindow(WINDOW_DIALOG_NETWORK_SETUP);
+ DestroyWindow(WINDOW_DIALOG_MEDIA_SOURCE);
+ DestroyWindow(WINDOW_DIALOG_CMS_OSD_SETTINGS);
+ DestroyWindow(WINDOW_DIALOG_VIDEO_OSD_SETTINGS);
+ DestroyWindow(WINDOW_DIALOG_AUDIO_OSD_SETTINGS);
+ DestroyWindow(WINDOW_DIALOG_SUBTITLE_OSD_SETTINGS);
+ DestroyWindow(WINDOW_DIALOG_VIDEO_BOOKMARKS);
+ DestroyWindow(WINDOW_DIALOG_CONTENT_SETTINGS);
+ DestroyWindow(WINDOW_DIALOG_INFOPROVIDER_SETTINGS);
+ DestroyWindow(WINDOW_DIALOG_LIBEXPORT_SETTINGS);
+ DestroyWindow(WINDOW_DIALOG_FAVOURITES);
+ DestroyWindow(WINDOW_DIALOG_SONG_INFO);
+ DestroyWindow(WINDOW_DIALOG_SMART_PLAYLIST_EDITOR);
+ DestroyWindow(WINDOW_DIALOG_SMART_PLAYLIST_RULE);
+ DestroyWindow(WINDOW_DIALOG_BUSY);
+ DestroyWindow(WINDOW_DIALOG_BUSY_NOCANCEL);
+ DestroyWindow(WINDOW_DIALOG_PICTURE_INFO);
+ DestroyWindow(WINDOW_DIALOG_ADDON_INFO);
+ DestroyWindow(WINDOW_DIALOG_ADDON_SETTINGS);
+ DestroyWindow(WINDOW_DIALOG_SLIDER);
+ DestroyWindow(WINDOW_DIALOG_MEDIA_FILTER);
+ DestroyWindow(WINDOW_DIALOG_SUBTITLES);
+ DestroyWindow(WINDOW_DIALOG_COLOR_PICKER);
+
+ /* Delete PVR related windows and dialogs */
+ DestroyWindow(WINDOW_TV_CHANNELS);
+ DestroyWindow(WINDOW_TV_RECORDINGS);
+ DestroyWindow(WINDOW_TV_GUIDE);
+ DestroyWindow(WINDOW_TV_TIMERS);
+ DestroyWindow(WINDOW_TV_TIMER_RULES);
+ DestroyWindow(WINDOW_TV_SEARCH);
+ DestroyWindow(WINDOW_RADIO_CHANNELS);
+ DestroyWindow(WINDOW_RADIO_RECORDINGS);
+ DestroyWindow(WINDOW_RADIO_GUIDE);
+ DestroyWindow(WINDOW_RADIO_TIMERS);
+ DestroyWindow(WINDOW_RADIO_TIMER_RULES);
+ DestroyWindow(WINDOW_RADIO_SEARCH);
+ DestroyWindow(WINDOW_DIALOG_PVR_GUIDE_INFO);
+ DestroyWindow(WINDOW_DIALOG_PVR_RECORDING_INFO);
+ DestroyWindow(WINDOW_DIALOG_PVR_TIMER_SETTING);
+ DestroyWindow(WINDOW_DIALOG_PVR_GROUP_MANAGER);
+ DestroyWindow(WINDOW_DIALOG_PVR_CHANNEL_MANAGER);
+ DestroyWindow(WINDOW_DIALOG_PVR_GUIDE_SEARCH);
+ DestroyWindow(WINDOW_DIALOG_PVR_CHANNEL_SCAN);
+ DestroyWindow(WINDOW_DIALOG_PVR_RADIO_RDS_INFO);
+ DestroyWindow(WINDOW_DIALOG_PVR_UPDATE_PROGRESS);
+ DestroyWindow(WINDOW_DIALOG_PVR_OSD_CHANNELS);
+ DestroyWindow(WINDOW_DIALOG_PVR_CHANNEL_GUIDE);
+ DestroyWindow(WINDOW_DIALOG_OSD_TELETEXT);
+ DestroyWindow(WINDOW_DIALOG_PVR_RECORDING_SETTING);
+ DestroyWindow(WINDOW_DIALOG_PVR_CLIENT_PRIORITIES);
+ DestroyWindow(WINDOW_DIALOG_PVR_GUIDE_CONTROLS);
+
+ DestroyWindow(WINDOW_DIALOG_TEXT_VIEWER);
+#ifdef HAS_DVD_DRIVE
+ DestroyWindow(WINDOW_DIALOG_PLAY_EJECT);
+#endif
+ DestroyWindow(WINDOW_STARTUP_ANIM);
+ DestroyWindow(WINDOW_LOGIN_SCREEN);
+ DestroyWindow(WINDOW_VISUALISATION);
+ DestroyWindow(WINDOW_SETTINGS_MENU);
+ DestroyWindow(WINDOW_SETTINGS_PROFILES);
+ DestroyWindow(WINDOW_SCREEN_CALIBRATION);
+ DestroyWindow(WINDOW_SYSTEM_INFORMATION);
+ DestroyWindow(WINDOW_SCREENSAVER);
+ DestroyWindow(WINDOW_DIALOG_VIDEO_OSD);
+ DestroyWindow(WINDOW_SLIDESHOW);
+ DestroyWindow(WINDOW_ADDON_BROWSER);
+ DestroyWindow(WINDOW_SKIN_SETTINGS);
+
+ DestroyWindow(WINDOW_HOME);
+ DestroyWindow(WINDOW_PROGRAMS);
+ DestroyWindow(WINDOW_PICTURES);
+ DestroyWindow(WINDOW_WEATHER);
+ DestroyWindow(WINDOW_DIALOG_GAME_CONTROLLERS);
+ DestroyWindow(WINDOW_DIALOG_GAME_PORTS);
+ DestroyWindow(WINDOW_GAMES);
+ DestroyWindow(WINDOW_DIALOG_GAME_OSD);
+ DestroyWindow(WINDOW_DIALOG_GAME_SAVES);
+ DestroyWindow(WINDOW_DIALOG_GAME_VIDEO_FILTER);
+ DestroyWindow(WINDOW_DIALOG_GAME_STRETCH_MODE);
+ DestroyWindow(WINDOW_DIALOG_GAME_VOLUME);
+ DestroyWindow(WINDOW_DIALOG_GAME_ADVANCED_SETTINGS);
+ DestroyWindow(WINDOW_DIALOG_GAME_VIDEO_ROTATION);
+ DestroyWindow(WINDOW_DIALOG_IN_GAME_SAVES);
+ DestroyWindow(WINDOW_FULLSCREEN_GAME);
+
+ Remove(WINDOW_SETTINGS_SERVICE);
+ Remove(WINDOW_SETTINGS_MYPVR);
+ Remove(WINDOW_SETTINGS_PLAYER);
+ Remove(WINDOW_SETTINGS_MEDIA);
+ Remove(WINDOW_SETTINGS_INTERFACE);
+ Remove(WINDOW_SETTINGS_MYGAMES);
+ DestroyWindow(WINDOW_SETTINGS_SYSTEM); // all the settings categories
+
+ Remove(WINDOW_DIALOG_KAI_TOAST);
+ Remove(WINDOW_DIALOG_SEEK_BAR);
+ Remove(WINDOW_DIALOG_VOLUME_BAR);
+
+ DestroyWindow(WINDOW_EVENT_LOG);
+
+ DestroyWindow(WINDOW_FAVOURITES);
+
+ DestroyWindow(WINDOW_DIALOG_PERIPHERALS);
+ DestroyWindow(WINDOW_DIALOG_PERIPHERAL_SETTINGS);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CGUIWindowManager::DestroyWindows()");
+ return false;
+ }
+
+ return true;
+}
+
+void CGUIWindowManager::DestroyWindow(int id)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ CGUIWindow *pWindow = GetWindow(id);
+ if (pWindow)
+ {
+ Remove(id);
+ pWindow->FreeResources(true);
+ delete pWindow;
+ }
+}
+
+bool CGUIWindowManager::SendMessage(int message, int senderID, int destID, int param1, int param2)
+{
+ CGUIMessage msg(message, senderID, destID, param1, param2);
+ return SendMessage(msg);
+}
+
+bool CGUIWindowManager::SendMessage(CGUIMessage& message)
+{
+ bool handled = false;
+ // CLog::Log(LOGDEBUG,"SendMessage: mess={} send={} control={} param1={}", message.GetMessage(), message.GetSenderId(), message.GetControlId(), message.GetParam1());
+ // Send the message to all none window targets
+ for (int i = 0; i < int(m_vecMsgTargets.size()); i++)
+ {
+ IMsgTargetCallback* pMsgTarget = m_vecMsgTargets[i];
+
+ if (pMsgTarget)
+ {
+ if (pMsgTarget->OnMessage( message )) handled = true;
+ }
+ }
+
+ // A GUI_MSG_NOTIFY_ALL is send to any active modal dialog
+ // and all windows whether they are active or not
+ if (message.GetMessage()==GUI_MSG_NOTIFY_ALL)
+ {
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ for (auto it = m_activeDialogs.rbegin(); it != m_activeDialogs.rend(); ++it)
+ {
+ (*it)->OnMessage(message);
+ }
+
+ for (const auto& entry : m_mapWindows)
+ {
+ entry.second->OnMessage(message);
+ }
+
+ return true;
+ }
+
+ // Normal messages are sent to:
+ // 1. All active modeless dialogs
+ // 2. The topmost dialog that accepts the message
+ // 3. The underlying window (only if it is the sender or receiver if a modal dialog is active)
+
+ bool hasModalDialog(false);
+ bool modalAcceptedMessage(false);
+ // don't use an iterator for this loop, as some messages mean that m_activeDialogs is altered,
+ // which will invalidate any iterator
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ size_t topWindow = m_activeDialogs.size();
+ while (topWindow)
+ {
+ CGUIWindow* dialog = m_activeDialogs[--topWindow];
+
+ if (!modalAcceptedMessage && dialog->IsModalDialog())
+ { // modal window
+ hasModalDialog = true;
+ if (!modalAcceptedMessage && dialog->OnMessage( message ))
+ {
+ modalAcceptedMessage = handled = true;
+ }
+ }
+ else if (!dialog->IsModalDialog())
+ { // modeless
+ if (dialog->OnMessage( message ))
+ handled = true;
+ }
+
+ if (topWindow > m_activeDialogs.size())
+ topWindow = m_activeDialogs.size();
+ }
+
+ // now send to the underlying window
+ CGUIWindow* window = GetWindow(GetActiveWindow());
+ if (window)
+ {
+ if (hasModalDialog)
+ {
+ // only send the message to the underlying window if it's the recipient
+ // or sender (or we have no sender)
+ if (message.GetSenderId() == window->GetID() ||
+ message.GetControlId() == window->GetID() ||
+ message.GetSenderId() == 0 )
+ {
+ if (window->OnMessage(message)) handled = true;
+ }
+ }
+ else
+ {
+ if (window->OnMessage(message)) handled = true;
+ }
+ }
+ return handled;
+}
+
+bool CGUIWindowManager::SendMessage(CGUIMessage& message, int window)
+{
+ if (window == 0)
+ // send to no specified windows.
+ return SendMessage(message);
+ CGUIWindow* pWindow = GetWindow(window);
+ if(pWindow)
+ return pWindow->OnMessage(message);
+ else
+ return false;
+}
+
+void CGUIWindowManager::AddUniqueInstance(CGUIWindow *window)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ // increment our instance (upper word of windowID)
+ // until we get a window we don't have
+ int instance = 0;
+ while (GetWindow(window->GetID()))
+ window->SetID(window->GetID() + (++instance << 16));
+ Add(window);
+}
+
+void CGUIWindowManager::Add(CGUIWindow* pWindow)
+{
+ if (!pWindow)
+ {
+ CLog::Log(LOGERROR, "Attempted to add a NULL window pointer to the window manager.");
+ return;
+ }
+ // push back all the windows if there are more than one covered by this class
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ for (int id : pWindow->GetIDRange())
+ {
+ auto it = m_mapWindows.find(id);
+ if (it != m_mapWindows.end())
+ {
+ CLog::Log(LOGERROR,
+ "Error, trying to add a second window with id {} "
+ "to the window manager",
+ id);
+ return;
+ }
+
+ m_mapWindows.insert(std::make_pair(id, pWindow));
+ }
+}
+
+void CGUIWindowManager::AddCustomWindow(CGUIWindow* pWindow)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ Add(pWindow);
+ m_vecCustomWindows.emplace_back(pWindow);
+}
+
+void CGUIWindowManager::RegisterDialog(CGUIWindow* dialog)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ // only add the window if it does not exists
+ for (const auto& window : m_activeDialogs)
+ {
+ if (window->GetID() == dialog->GetID())
+ return;
+ }
+ m_activeDialogs.emplace_back(dialog);
+}
+
+void CGUIWindowManager::Remove(int id)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ auto it = m_mapWindows.find(id);
+ if (it != m_mapWindows.end())
+ {
+ CGUIWindow *window = it->second;
+ m_windowHistory.erase(std::remove_if(m_windowHistory.begin(),
+ m_windowHistory.end(),
+ [id](int winId){ return winId == id; }),
+ m_windowHistory.end());
+ m_activeDialogs.erase(std::remove_if(m_activeDialogs.begin(),
+ m_activeDialogs.end(),
+ [window](CGUIWindow* w){ return w == window; }),
+ m_activeDialogs.end());
+ m_mapWindows.erase(it);
+ }
+ else
+ {
+ CLog::Log(LOGWARNING,
+ "Attempted to remove window {} "
+ "from the window manager when it didn't exist",
+ id);
+ }
+}
+
+// removes and deletes the window. Should only be called
+// from the class that created the window using new.
+void CGUIWindowManager::Delete(int id)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ CGUIWindow *pWindow = GetWindow(id);
+ if (pWindow)
+ {
+ Remove(id);
+ m_deleteWindows.emplace_back(pWindow);
+ }
+}
+
+void CGUIWindowManager::PreviousWindow()
+{
+ // deactivate any window
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ CLog::Log(LOGDEBUG,"CGUIWindowManager::PreviousWindow: Deactivate");
+ int currentWindow = GetActiveWindow();
+ CGUIWindow *pCurrentWindow = GetWindow(currentWindow);
+ if (!pCurrentWindow)
+ return; // no windows or window history yet
+
+ // check to see whether our current window has a <previouswindow> tag
+ if (pCurrentWindow->GetPreviousWindow() != WINDOW_INVALID)
+ {
+ //! @todo we may need to test here for the
+ //! whether our history should be changed
+
+ // don't reactivate the previouswindow if it is ourselves.
+ if (currentWindow != pCurrentWindow->GetPreviousWindow())
+ ActivateWindow(pCurrentWindow->GetPreviousWindow());
+ return;
+ }
+ // get the previous window in our stack
+ if (m_windowHistory.size() < 2)
+ {
+ // no previous window history yet - check if we should just activate home
+ if (GetActiveWindow() != WINDOW_INVALID && GetActiveWindow() != WINDOW_HOME)
+ {
+ CloseWindowSync(pCurrentWindow);
+ ClearWindowHistory();
+ ActivateWindow(WINDOW_HOME);
+ }
+ return;
+ }
+ m_windowHistory.pop_back();
+ int previousWindow = GetActiveWindow();
+ m_windowHistory.emplace_back(currentWindow);
+
+ CGUIWindow *pNewWindow = GetWindow(previousWindow);
+ if (!pNewWindow)
+ {
+ CLog::Log(LOGERROR, "Unable to activate the previous window");
+ CloseWindowSync(pCurrentWindow);
+ ClearWindowHistory();
+ ActivateWindow(WINDOW_HOME);
+ return;
+ }
+
+ // ok to go to the previous window now
+
+ // tell our info manager which window we are going to
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetGUIControlsInfoProvider().SetNextWindow(previousWindow);
+
+ // deinitialize our window
+ CloseWindowSync(pCurrentWindow);
+
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetGUIControlsInfoProvider().SetNextWindow(WINDOW_INVALID);
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetGUIControlsInfoProvider().SetPreviousWindow(currentWindow);
+
+ // remove the current window off our window stack
+ m_windowHistory.pop_back();
+
+ // ok, initialize the new window
+ CLog::Log(LOGDEBUG,"CGUIWindowManager::PreviousWindow: Activate new");
+ CGUIMessage msg2(GUI_MSG_WINDOW_INIT, 0, 0, WINDOW_INVALID, GetActiveWindow());
+ pNewWindow->OnMessage(msg2);
+
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetGUIControlsInfoProvider().SetPreviousWindow(WINDOW_INVALID);
+}
+
+void CGUIWindowManager::ChangeActiveWindow(int newWindow, const std::string& strPath)
+{
+ std::vector<std::string> params;
+ if (!strPath.empty())
+ params.emplace_back(strPath);
+ ActivateWindow(newWindow, params, true);
+}
+
+void CGUIWindowManager::ActivateWindow(int iWindowID, const std::string& strPath)
+{
+ std::vector<std::string> params;
+ if (!strPath.empty())
+ params.emplace_back(strPath);
+ ActivateWindow(iWindowID, params, false);
+}
+
+void CGUIWindowManager::ForceActivateWindow(int iWindowID, const std::string& strPath)
+{
+ std::vector<std::string> params;
+ if (!strPath.empty())
+ params.emplace_back(strPath);
+ ActivateWindow(iWindowID, params, false, true);
+}
+
+void CGUIWindowManager::ActivateWindow(int iWindowID, const std::vector<std::string>& params, bool swappingWindows /* = false */, bool force /* = false */)
+{
+ if (!CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ // make sure graphics lock is not held
+ CSingleExit leaveIt(CServiceBroker::GetWinSystem()->GetGfxContext());
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTIVATE_WINDOW, iWindowID,
+ swappingWindows ? 1 : 0, nullptr, "", params);
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ ActivateWindow_Internal(iWindowID, params, swappingWindows, force);
+ }
+}
+
+void CGUIWindowManager::ActivateWindow_Internal(int iWindowID, const std::vector<std::string>& params, bool swappingWindows, bool force /* = false */)
+{
+ // translate virtual windows
+ if (iWindowID == WINDOW_START)
+ { // virtual start window
+ iWindowID = g_SkinInfo->GetStartWindow();
+ }
+
+ // debug
+ CLog::Log(LOGDEBUG, "Activating window ID: {}", iWindowID);
+
+ // make sure we check mediasources from home
+ if (GetActiveWindow() == WINDOW_HOME)
+ {
+ g_passwordManager.SetMediaSourcePath(!params.empty() ? params[0] : "");
+ }
+ else
+ {
+ g_passwordManager.SetMediaSourcePath("");
+ }
+
+ if (!g_passwordManager.CheckMenuLock(iWindowID))
+ {
+ CLog::Log(LOGERROR,
+ "MasterCode or MediaSource-code is wrong: Window with id {} will not be loaded! "
+ "Enter a correct code!",
+ iWindowID);
+ if (GetActiveWindow() == WINDOW_INVALID && iWindowID != WINDOW_HOME)
+ ActivateWindow(WINDOW_HOME);
+ return;
+ }
+
+ // first check existence of the window we wish to activate.
+ CGUIWindow *pNewWindow = GetWindow(iWindowID);
+ if (!pNewWindow)
+ { // nothing to see here - move along
+ CLog::Log(LOGERROR, "Unable to locate window with id {}. Check skin files",
+ iWindowID - WINDOW_HOME);
+ if (IsWindowActive(WINDOW_STARTUP_ANIM))
+ ActivateWindow(WINDOW_HOME);
+ return ;
+ }
+ else if (!pNewWindow->CanBeActivated())
+ {
+ if (IsWindowActive(WINDOW_STARTUP_ANIM))
+ ActivateWindow(WINDOW_HOME);
+ return;
+ }
+ else if (pNewWindow->IsDialog())
+ { // if we have a dialog, we do a DoModal() rather than activate the window
+ if (!pNewWindow->IsDialogRunning())
+ {
+ CSingleExit exitit(CServiceBroker::GetWinSystem()->GetGfxContext());
+ static_cast<CGUIDialog *>(pNewWindow)->Open(params.size() > 0 ? params[0] : "");
+ // Invalidate underlying windows after closing a modal dialog
+ MarkDirty();
+ }
+ return;
+ }
+
+ // don't activate a window if there are active modal dialogs of type MODAL
+ if (!force && HasModalDialog(true))
+ {
+ CLog::Log(LOGINFO, "Activate of window '{}' refused because there are active modal dialogs",
+ iWindowID);
+ CServiceBroker::GetGUI()->GetAudioManager().PlayActionSound(CAction(ACTION_ERROR));
+ return;
+ }
+
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetGUIControlsInfoProvider().SetNextWindow(iWindowID);
+
+ // deactivate any window
+ int currentWindow = GetActiveWindow();
+ CGUIWindow *pWindow = GetWindow(currentWindow);
+ if (pWindow)
+ CloseWindowSync(pWindow, iWindowID);
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetGUIControlsInfoProvider().SetNextWindow(WINDOW_INVALID);
+
+ // Add window to the history list (we must do this before we activate it,
+ // as all messages done in WINDOW_INIT will want to be sent to the new
+ // topmost window). If we are swapping windows, we pop the old window
+ // off the history stack
+ if (swappingWindows && !m_windowHistory.empty())
+ m_windowHistory.pop_back();
+ AddToWindowHistory(iWindowID);
+
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetGUIControlsInfoProvider().SetPreviousWindow(currentWindow);
+ // Send the init message
+ CGUIMessage msg(GUI_MSG_WINDOW_INIT, 0, 0, currentWindow, iWindowID);
+ msg.SetStringParams(params);
+ pNewWindow->OnMessage(msg);
+// CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetGUIControlsInfoProvider().SetPreviousWindow(WINDOW_INVALID);
+}
+
+void CGUIWindowManager::CloseDialogs(bool forceClose) const
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ //This is to avoid an assert about out of bounds iterator
+ //when m_activeDialogs happens to be empty
+ if (m_activeDialogs.empty())
+ return;
+
+ auto activeDialogs = m_activeDialogs;
+ for (const auto& window : activeDialogs)
+ {
+ if (window->IsModalDialog())
+ window->Close(forceClose);
+ }
+}
+
+void CGUIWindowManager::CloseInternalModalDialogs(bool forceClose) const
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ if (m_activeDialogs.empty())
+ return;
+
+ auto activeDialogs = m_activeDialogs;
+ for (const auto& window : activeDialogs)
+ {
+ if (window->IsModalDialog() && !IsAddonWindow(window->GetID()) && !IsPythonWindow(window->GetID()))
+ window->Close(forceClose);
+ }
+}
+
+// SwitchToFullScreen() returns true if a switch is made, else returns false
+bool CGUIWindowManager::SwitchToFullScreen(bool force /* = false */)
+{
+ // don't switch if the slideshow is active
+ if (IsWindowActive(WINDOW_SLIDESHOW))
+ return false;
+
+ // if playing from the video info window, close it first!
+ if (IsModalDialogTopmost(WINDOW_DIALOG_VIDEO_INFO))
+ {
+ CGUIDialogVideoInfo* pDialog = GetWindow<CGUIDialogVideoInfo>(WINDOW_DIALOG_VIDEO_INFO);
+ if (pDialog)
+ pDialog->Close(true);
+ }
+
+ // if playing from the album info window, close it first!
+ if (IsModalDialogTopmost(WINDOW_DIALOG_MUSIC_INFO))
+ {
+ CGUIDialogVideoInfo* pDialog = GetWindow<CGUIDialogVideoInfo>(WINDOW_DIALOG_MUSIC_INFO);
+ if (pDialog)
+ pDialog->Close(true);
+ }
+
+ // if playing from the song info window, close it first!
+ if (IsModalDialogTopmost(WINDOW_DIALOG_SONG_INFO))
+ {
+ CGUIDialogVideoInfo* pDialog = GetWindow<CGUIDialogVideoInfo>(WINDOW_DIALOG_SONG_INFO);
+ if (pDialog)
+ pDialog->Close(true);
+ }
+
+ const int activeWindowID = GetActiveWindow();
+ int windowID = WINDOW_INVALID;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ // See if we're playing a game
+ if (activeWindowID != WINDOW_FULLSCREEN_GAME && appPlayer->IsPlayingGame())
+ windowID = WINDOW_FULLSCREEN_GAME;
+
+ // See if we're playing a video
+ else if (activeWindowID != WINDOW_FULLSCREEN_VIDEO && appPlayer->IsPlayingVideo())
+ windowID = WINDOW_FULLSCREEN_VIDEO;
+
+ // See if we're playing an audio song
+ if (activeWindowID != WINDOW_VISUALISATION && appPlayer->IsPlayingAudio())
+ windowID = WINDOW_VISUALISATION;
+
+ if (windowID != WINDOW_INVALID && (force || windowID != activeWindowID))
+ {
+ if (force)
+ ForceActivateWindow(windowID);
+ else
+ ActivateWindow(windowID);
+
+ return true;
+ }
+
+ return false;
+}
+
+void CGUIWindowManager::OnApplicationMessage(ThreadMessage* pMsg)
+{
+ switch (pMsg->dwMessage)
+ {
+ case TMSG_GUI_DIALOG_OPEN:
+ {
+ if (pMsg->lpVoid)
+ static_cast<CGUIDialog*>(pMsg->lpVoid)->Open(pMsg->param2, pMsg->strParam);
+ else
+ {
+ CGUIDialog* pDialog = static_cast<CGUIDialog*>(GetWindow(pMsg->param1));
+ if (pDialog)
+ pDialog->Open(pMsg->strParam);
+ }
+ }
+ break;
+
+ case TMSG_GUI_WINDOW_CLOSE:
+ {
+ CGUIWindow *window = static_cast<CGUIWindow *>(pMsg->lpVoid);
+ if (window)
+ window->Close((pMsg->param1 & 0x1) ? true : false, pMsg->param1, (pMsg->param1 & 0x2) ? true : false);
+ }
+ break;
+
+ case TMSG_GUI_ACTIVATE_WINDOW:
+ {
+ ActivateWindow(pMsg->param1, pMsg->params, pMsg->param2 > 0);
+ }
+ break;
+
+ case TMSG_GUI_PREVIOUS_WINDOW:
+ {
+ PreviousWindow();
+ }
+ break;
+
+ case TMSG_GUI_ADDON_DIALOG:
+ {
+ if (pMsg->lpVoid)
+ {
+ static_cast<ADDON::CGUIAddonWindowDialog*>(pMsg->lpVoid)->Show_Internal(pMsg->param2 > 0);
+ }
+ }
+ break;
+
+#ifdef HAS_PYTHON
+ case TMSG_GUI_PYTHON_DIALOG:
+ {
+ // This hack is not much better but at least I don't need to make ApplicationMessenger
+ // know about Addon (Python) specific classes.
+ CAction caction(pMsg->param1);
+ static_cast<CGUIWindow*>(pMsg->lpVoid)->OnAction(caction);
+ }
+ break;
+#endif
+
+ case TMSG_GUI_ACTION:
+ {
+ if (pMsg->lpVoid)
+ {
+ CAction *action = static_cast<CAction *>(pMsg->lpVoid);
+ if (pMsg->param1 == WINDOW_INVALID)
+ g_application.OnAction(*action);
+ else
+ {
+ CGUIWindow *pWindow = GetWindow(pMsg->param1);
+ if (pWindow)
+ pWindow->OnAction(*action);
+ else
+ CLog::Log(LOGWARNING, "Failed to get window with ID {} to send an action to",
+ pMsg->param1);
+ }
+ delete action;
+ }
+ }
+ break;
+
+ case TMSG_GUI_MESSAGE:
+ if (pMsg->lpVoid)
+ {
+ CGUIMessage *message = static_cast<CGUIMessage *>(pMsg->lpVoid);
+ SendMessage(*message, pMsg->param1);
+ delete message;
+ }
+ break;
+
+ case TMSG_GUI_DIALOG_YESNO:
+ {
+
+ if (!pMsg->lpVoid && pMsg->param1 < 0 && pMsg->param2 < 0)
+ return;
+
+ auto dialog = static_cast<CGUIDialogYesNo*>(GetWindow(WINDOW_DIALOG_YES_NO));
+ if (!dialog)
+ return;
+
+ if (pMsg->lpVoid)
+ pMsg->SetResult(dialog->ShowAndGetInput(*static_cast<HELPERS::DialogYesNoMessage*>(pMsg->lpVoid)));
+ else
+ {
+ HELPERS::DialogYesNoMessage options;
+ options.heading = pMsg->param1;
+ options.text = pMsg->param2;
+ pMsg->SetResult(dialog->ShowAndGetInput(options));
+ }
+
+ }
+ break;
+
+ case TMSG_GUI_DIALOG_OK:
+ {
+
+ if (!pMsg->lpVoid && pMsg->param1 < 0 && pMsg->param2 < 0)
+ return;
+
+ auto dialogOK = static_cast<CGUIDialogOK*>(GetWindow(WINDOW_DIALOG_OK));
+ if (!dialogOK)
+ return;
+
+ if (pMsg->lpVoid)
+ dialogOK->ShowAndGetInput(*static_cast<HELPERS::DialogOKMessage*>(pMsg->lpVoid));
+ else
+ {
+ HELPERS::DialogOKMessage options;
+ options.heading = pMsg->param1;
+ options.text = pMsg->param2;
+ dialogOK->ShowAndGetInput(options);
+ }
+ pMsg->SetResult(static_cast<int>(dialogOK->IsConfirmed()));
+ }
+ break;
+ }
+}
+
+int CGUIWindowManager::GetMessageMask()
+{
+ return TMSG_MASK_WINDOWMANAGER;
+}
+
+bool CGUIWindowManager::OnAction(const CAction &action) const
+{
+ auto actionId = action.GetID();
+ if (actionId == ACTION_GESTURE_BEGIN)
+ {
+ m_touchGestureActive = true;
+ }
+
+ bool ret;
+ if (!m_inhibitTouchGestureEvents || !action.IsGesture())
+ {
+ ret = HandleAction(action);
+ }
+ else
+ {
+ // We swallow the event, so it is handled
+ ret = true;
+ CLog::Log(LOGDEBUG, "Swallowing touch action {} due to inhibition on window switch", actionId);
+ }
+
+ if (actionId == ACTION_GESTURE_END || actionId == ACTION_GESTURE_ABORT)
+ {
+ m_touchGestureActive = false;
+ m_inhibitTouchGestureEvents = false;
+ }
+
+ return ret;
+}
+
+bool CGUIWindowManager::HandleAction(CAction const& action) const
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ size_t topmost = m_activeDialogs.size();
+ while (topmost)
+ {
+ CGUIWindow *dialog = m_activeDialogs[--topmost];
+ lock.unlock();
+ if (dialog->IsModalDialog())
+ { // we have the topmost modal dialog
+ if (!dialog->IsAnimating(ANIM_TYPE_WINDOW_CLOSE))
+ {
+ bool fallThrough = (dialog->GetID() == WINDOW_DIALOG_FULLSCREEN_INFO);
+ if (dialog->OnAction(action))
+ return true;
+ // dialog didn't want the action - we'd normally return false
+ // but for some dialogs we want to drop the actions through
+ if (fallThrough)
+ {
+ lock.lock();
+ break;
+ }
+ return false;
+ }
+ CLog::Log(LOGWARNING,
+ "CGUIWindowManager - {} - ignoring action {}, because topmost modal dialog closing "
+ "animation is running",
+ __FUNCTION__, action.GetID());
+ return true; // do nothing with the action until the anim is finished
+ }
+ lock.lock();
+ if (topmost > m_activeDialogs.size())
+ topmost = m_activeDialogs.size();
+ }
+ lock.unlock();
+ CGUIWindow* window = GetWindow(GetActiveWindow());
+ if (window)
+ return window->OnAction(action);
+ return false;
+}
+
+bool RenderOrderSortFunction(CGUIWindow *first, CGUIWindow *second)
+{
+ return first->GetRenderOrder() < second->GetRenderOrder();
+}
+
+void CGUIWindowManager::Process(unsigned int currentTime)
+{
+ assert(CServiceBroker::GetAppMessenger()->IsProcessThread());
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ m_dirtyregions.clear();
+
+ CGUIWindow* pWindow = GetWindow(GetActiveWindow());
+ if (pWindow)
+ pWindow->DoProcess(currentTime, m_dirtyregions);
+
+ // process all dialogs - visibility may change etc.
+ for (const auto& entry : m_mapWindows)
+ {
+ CGUIWindow *pWindow = entry.second;
+ if (pWindow && pWindow->IsDialog())
+ pWindow->DoProcess(currentTime, m_dirtyregions);
+ }
+
+ for (auto& itr : m_dirtyregions)
+ m_tracker.MarkDirtyRegion(itr);
+}
+
+void CGUIWindowManager::MarkDirty()
+{
+ MarkDirty(CRect(0, 0, float(CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth()), float(CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight())));
+}
+
+void CGUIWindowManager::MarkDirty(const CRect& rect)
+{
+ m_tracker.MarkDirtyRegion(CDirtyRegion(rect));
+
+ CGUIWindow* pWindow = GetWindow(GetActiveWindow());
+ if (pWindow)
+ pWindow->MarkDirtyRegion();
+
+ // make copy of vector as we may remove items from it as we go
+ auto activeDialogs = m_activeDialogs;
+ for (const auto& window : activeDialogs)
+ if (window->IsDialogRunning())
+ window->MarkDirtyRegion();
+}
+
+void CGUIWindowManager::RenderPass() const
+{
+ CGUIWindow* pWindow = GetWindow(GetActiveWindow());
+ if (pWindow)
+ {
+ pWindow->ClearBackground();
+ pWindow->DoRender();
+ }
+
+ // we render the dialogs based on their render order.
+ auto renderList = m_activeDialogs;
+ stable_sort(renderList.begin(), renderList.end(), RenderOrderSortFunction);
+
+ for (const auto& window : renderList)
+ {
+ if (window->IsDialogRunning())
+ window->DoRender();
+ }
+}
+
+void CGUIWindowManager::RenderEx() const
+{
+ CGUIWindow* pWindow = GetWindow(GetActiveWindow());
+ if (pWindow)
+ pWindow->RenderEx();
+
+ // We don't call RenderEx for now on dialogs since it is used
+ // to trigger non gui video rendering. We can activate it later at any time.
+ /*
+ vector<CGUIWindow *> &activeDialogs = m_activeDialogs;
+ for (iDialog it = activeDialogs.begin(); it != activeDialogs.end(); ++it)
+ {
+ if ((*it)->IsDialogRunning())
+ (*it)->RenderEx();
+ }
+ */
+}
+
+bool CGUIWindowManager::Render()
+{
+ assert(CServiceBroker::GetAppMessenger()->IsProcessThread());
+ CSingleExit lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ CDirtyRegionList dirtyRegions = m_tracker.GetDirtyRegions();
+
+ bool hasRendered = false;
+ // If we visualize the regions we will always render the entire viewport
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_guiVisualizeDirtyRegions || CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_guiAlgorithmDirtyRegions == DIRTYREGION_SOLVER_FILL_VIEWPORT_ALWAYS)
+ {
+ RenderPass();
+ hasRendered = true;
+ }
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_guiAlgorithmDirtyRegions == DIRTYREGION_SOLVER_FILL_VIEWPORT_ON_CHANGE)
+ {
+ if (!dirtyRegions.empty())
+ {
+ RenderPass();
+ hasRendered = true;
+ }
+ }
+ else
+ {
+ for (const auto& i : dirtyRegions)
+ {
+ if (i.IsEmpty())
+ continue;
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetScissors(i);
+ RenderPass();
+ hasRendered = true;
+ }
+ CServiceBroker::GetWinSystem()->GetGfxContext().ResetScissors();
+ }
+
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_guiVisualizeDirtyRegions)
+ {
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(), false);
+ const CDirtyRegionList &markedRegions = m_tracker.GetMarkedRegions();
+ for (const auto& i : markedRegions)
+ CGUITexture::DrawQuad(i, 0x0fff0000);
+ for (const auto& i : dirtyRegions)
+ CGUITexture::DrawQuad(i, 0x4c00ff00);
+ }
+
+ return hasRendered;
+}
+
+void CGUIWindowManager::AfterRender()
+{
+ m_tracker.CleanMarkedRegions();
+
+ CGUIWindow* pWindow = GetWindow(GetActiveWindow());
+ if (pWindow)
+ pWindow->AfterRender();
+
+ // make copy of vector as we may remove items from it as we go
+ auto activeDialogs = m_activeDialogs;
+ for (const auto& window : activeDialogs)
+ {
+ if (window->IsDialogRunning())
+ {
+ window->AfterRender();
+ // Dialog state can affect visibility states
+ if (pWindow && window->IsControlDirty())
+ pWindow->MarkDirtyRegion();
+ }
+ }
+}
+
+void CGUIWindowManager::FrameMove()
+{
+ assert(CServiceBroker::GetAppMessenger()->IsProcessThread());
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ if(m_iNested == 0)
+ {
+ // delete any windows queued for deletion
+ for (const auto& window : m_deleteWindows)
+ {
+ // Free any window resources
+ window->FreeResources(true);
+ delete window;
+ }
+ m_deleteWindows.clear();
+ }
+
+ CGUIWindow* pWindow = GetWindow(GetActiveWindow());
+ if (pWindow)
+ pWindow->FrameMove();
+ // update any dialogs - we take a copy of the vector as some dialogs may close themselves
+ // during this call
+ auto dialogs = m_activeDialogs;
+ for (const auto& window : dialogs)
+ {
+ window->FrameMove();
+ }
+
+ CServiceBroker::GetGUI()->GetInfoManager().UpdateAVInfo();
+}
+
+CGUIDialog* CGUIWindowManager::GetDialog(int id) const
+{
+ CGUIWindow *window = GetWindow(id);
+ if (window && window->IsDialog())
+ return dynamic_cast<CGUIDialog*>(window);
+ return nullptr;
+}
+
+CGUIWindow* CGUIWindowManager::GetWindow(int id) const
+{
+ if (id == 0 || id == WINDOW_INVALID)
+ return nullptr;
+
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ auto it = m_mapWindows.find(id);
+ if (it != m_mapWindows.end())
+ return it->second;
+ return nullptr;
+}
+
+bool CGUIWindowManager::ProcessRenderLoop(bool renderOnly)
+{
+ bool renderGui = true;
+
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread() && m_pCallback)
+ {
+ renderGui = m_pCallback->GetRenderGUI();
+ m_iNested++;
+ if (!renderOnly)
+ m_pCallback->Process();
+ m_pCallback->FrameMove(!renderOnly);
+ m_pCallback->Render();
+ m_iNested--;
+ }
+ if (g_application.m_bStop || !renderGui)
+ return false;
+ else
+ return true;
+}
+
+void CGUIWindowManager::SetCallback(IWindowManagerCallback& callback)
+{
+ m_pCallback = &callback;
+}
+
+void CGUIWindowManager::DeInitialize()
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ // Need a copy because addon-dialogs removes itself on Close()
+ std::unordered_map<int, CGUIWindow*> closeMap(m_mapWindows);
+ for (const auto& entry : closeMap)
+ {
+ CGUIWindow* pWindow = entry.second;
+ if (IsWindowActive(entry.first, false))
+ {
+ pWindow->DisableAnimations();
+ pWindow->Close(true);
+ }
+ pWindow->ResetControlStates();
+ pWindow->FreeResources(true);
+ }
+ UnloadNotOnDemandWindows();
+
+ m_vecMsgTargets.erase( m_vecMsgTargets.begin(), m_vecMsgTargets.end() );
+
+ // destroy our custom windows...
+ for (int i = 0; i < int(m_vecCustomWindows.size()); i++)
+ {
+ CGUIWindow *pWindow = m_vecCustomWindows[i];
+ RemoveFromWindowHistory(pWindow->GetID());
+ Remove(pWindow->GetID());
+ delete pWindow;
+ }
+
+ // clear our vectors of windows
+ m_vecCustomWindows.clear();
+ m_activeDialogs.clear();
+
+ m_initialized = false;
+}
+
+/// \brief Unroute window
+/// \param id ID of the window routed
+void CGUIWindowManager::RemoveDialog(int id)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ m_activeDialogs.erase(std::remove_if(m_activeDialogs.begin(),
+ m_activeDialogs.end(),
+ [id](CGUIWindow* dialog) { return dialog->GetID() == id; }),
+ m_activeDialogs.end());
+}
+
+bool CGUIWindowManager::HasModalDialog(bool ignoreClosing) const
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ for (const auto& window : m_activeDialogs)
+ {
+ if (window->IsDialog() &&
+ window->IsModalDialog() &&
+ (!ignoreClosing || !window->IsAnimating(ANIM_TYPE_WINDOW_CLOSE)))
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIWindowManager::HasVisibleModalDialog() const
+{
+ return HasModalDialog(false);
+}
+
+int CGUIWindowManager::GetTopmostDialog(bool modal, bool ignoreClosing) const
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ for (auto it = m_activeDialogs.rbegin(); it != m_activeDialogs.rend(); ++it)
+ {
+ CGUIWindow *dialog = *it;
+ if ((!modal || dialog->IsModalDialog()) && (!ignoreClosing || !dialog->IsAnimating(ANIM_TYPE_WINDOW_CLOSE)))
+ return dialog->GetID();
+ }
+ return WINDOW_INVALID;
+}
+
+int CGUIWindowManager::GetTopmostDialog(bool ignoreClosing /*= false*/) const
+{
+ return GetTopmostDialog(false, ignoreClosing);
+}
+
+int CGUIWindowManager::GetTopmostModalDialog(bool ignoreClosing /*= false*/) const
+{
+ return GetTopmostDialog(true, ignoreClosing);
+}
+
+void CGUIWindowManager::SendThreadMessage(CGUIMessage& message, int window /*= 0*/)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CGUIMessage* msg = new CGUIMessage(message);
+ m_vecThreadMessages.emplace_back(std::pair<CGUIMessage*, int>(msg,window));
+}
+
+void CGUIWindowManager::DispatchThreadMessages()
+{
+ // This method only be called in the xbmc main thread.
+
+ // XXX: for more info of this method
+ // check the pr here: https://github.com/xbmc/xbmc/pull/2253
+
+ // As a thread message queue service, it should follow these rules:
+ // 1. [Must] Thread safe, message can be pushed into queue in arbitrary thread context.
+ // 2. Messages [must] be processed in dispatch message thread context with the same
+ // order as they be pushed into the queue.
+ // 3. Dispatch function [must] support call itself during message process procedure,
+ // and do not break other rules listed here. to make it clear: in the
+ // SendMessage(), it could start another xbmc main thread loop, calling
+ // DispatchThreadMessages() in it's internal loop, this must be supported.
+ // 4. During DispatchThreadMessages() processing, any new pushed message [should] not
+ // be processed by the current loop in DispatchThreadMessages(), prevent dead loop.
+ // 5. If possible, queued messages can be removed by certain filter condition
+ // and not break above.
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ while (!m_vecThreadMessages.empty())
+ {
+ // pop up one message per time to make messages be processed by order.
+ // this will ensure rule No.2 & No.3
+ CGUIMessage *pMsg = m_vecThreadMessages.front().first;
+ int window = m_vecThreadMessages.front().second;
+ m_vecThreadMessages.pop_front();
+
+ lock.unlock();
+
+ // XXX: during SendMessage(), there could be a deeper 'xbmc main loop' inited by e.g. doModal
+ // which may loop there and callback to DispatchThreadMessages() multiple times.
+ if (window)
+ SendMessage( *pMsg, window );
+ else
+ SendMessage( *pMsg );
+ delete pMsg;
+
+ lock.lock();
+ }
+}
+
+int CGUIWindowManager::RemoveThreadMessageByMessageIds(int *pMessageIDList)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ int removedMsgCount = 0;
+ for (std::list < std::pair<CGUIMessage*,int> >::iterator it = m_vecThreadMessages.begin();
+ it != m_vecThreadMessages.end();)
+ {
+ CGUIMessage *pMsg = it->first;
+ int *pMsgID;
+ for(pMsgID = pMessageIDList; *pMsgID != 0; ++pMsgID)
+ if (pMsg->GetMessage() == *pMsgID)
+ break;
+ if (*pMsgID)
+ {
+ it = m_vecThreadMessages.erase(it);
+ delete pMsg;
+ ++removedMsgCount;
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ return removedMsgCount;
+}
+
+void CGUIWindowManager::AddMsgTarget(IMsgTargetCallback* pMsgTarget)
+{
+ m_vecMsgTargets.emplace_back(pMsgTarget);
+}
+
+int CGUIWindowManager::GetActiveWindow() const
+{
+ if (!m_windowHistory.empty())
+ return m_windowHistory.back();
+ return WINDOW_INVALID;
+}
+
+int CGUIWindowManager::GetActiveWindowOrDialog() const
+{
+ // if there is a dialog active get the dialog id instead
+ int iWin = GetTopmostModalDialog() & WINDOW_ID_MASK;
+ if (iWin != WINDOW_INVALID)
+ return iWin;
+
+ // get the currently active window
+ return GetActiveWindow() & WINDOW_ID_MASK;
+}
+
+bool CGUIWindowManager::IsWindowActive(int id, bool ignoreClosing /* = true */) const
+{
+ // mask out multiple instances of the same window
+ id &= WINDOW_ID_MASK;
+ if ((GetActiveWindow() & WINDOW_ID_MASK) == id)
+ return true;
+ // run through the dialogs
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ for (const auto& window : m_activeDialogs)
+ {
+ if ((window->GetID() & WINDOW_ID_MASK) == id && (!ignoreClosing || !window->IsAnimating(ANIM_TYPE_WINDOW_CLOSE)))
+ return true;
+ }
+ return false; // window isn't active
+}
+
+bool CGUIWindowManager::IsWindowActive(const std::string &xmlFile, bool ignoreClosing /* = true */) const
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ CGUIWindow *window = GetWindow(GetActiveWindow());
+ if (window && StringUtils::EqualsNoCase(URIUtils::GetFileName(window->GetProperty("xmlfile").asString()), xmlFile))
+ return true;
+ // run through the dialogs
+ for (const auto& window : m_activeDialogs)
+ {
+ if (StringUtils::EqualsNoCase(URIUtils::GetFileName(window->GetProperty("xmlfile").asString()), xmlFile) &&
+ (!ignoreClosing || !window->IsAnimating(ANIM_TYPE_WINDOW_CLOSE)))
+ return true;
+ }
+ return false; // window isn't active
+}
+
+bool CGUIWindowManager::IsWindowVisible(int id) const
+{
+ return IsWindowActive(id, false);
+}
+
+bool CGUIWindowManager::IsWindowVisible(const std::string &xmlFile) const
+{
+ return IsWindowActive(xmlFile, false);
+}
+
+void CGUIWindowManager::LoadNotOnDemandWindows()
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ for (const auto& entry : m_mapWindows)
+ {
+ CGUIWindow *pWindow = entry.second;
+ if (pWindow->GetLoadType() == CGUIWindow::LOAD_ON_GUI_INIT)
+ {
+ pWindow->FreeResources(true);
+ pWindow->Initialize();
+ }
+ }
+}
+
+void CGUIWindowManager::UnloadNotOnDemandWindows()
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ for (const auto& entry : m_mapWindows)
+ {
+ CGUIWindow *pWindow = entry.second;
+ if (pWindow->GetLoadType() == CGUIWindow::LOAD_ON_GUI_INIT ||
+ pWindow->GetLoadType() == CGUIWindow::KEEP_IN_MEMORY)
+ {
+ pWindow->FreeResources(true);
+ }
+ }
+}
+
+void CGUIWindowManager::AddToWindowHistory(int newWindowID)
+{
+ // Check the window stack to see if this window is in our history,
+ // and if so, pop all the other windows off the stack so that we
+ // always have a predictable "Back" behaviour for each window
+ std::deque<int> history = m_windowHistory;
+ while (!history.empty())
+ {
+ if (history.back() == newWindowID)
+ break;
+ history.pop_back();
+ }
+ if (!history.empty())
+ { // found window in history
+ m_windowHistory.swap(history);
+ }
+ else
+ {
+ // didn't find window in history - add it to the stack
+ m_windowHistory.emplace_back(newWindowID);
+ }
+}
+
+void CGUIWindowManager::RemoveFromWindowHistory(int windowID)
+{
+ std::deque<int> history = m_windowHistory;
+
+ // pop windows from stack until we found the window
+ while (!history.empty())
+ {
+ if (history.back() == windowID)
+ break;
+ history.pop_back();
+ }
+
+ // found window in history
+ if (!history.empty())
+ {
+ history.pop_back(); // remove window from stack
+ m_windowHistory.swap(history);
+ }
+}
+
+bool CGUIWindowManager::IsModalDialogTopmost(int id) const
+{
+ return IsDialogTopmost(id, true);
+}
+
+bool CGUIWindowManager::IsModalDialogTopmost(const std::string &xmlFile) const
+{
+ return IsDialogTopmost(xmlFile, true);
+}
+
+bool CGUIWindowManager::IsDialogTopmost(int id, bool modal /* = false */) const
+{
+ CGUIWindow *topmost = GetWindow(GetTopmostDialog(modal, false));
+ if (topmost && (topmost->GetID() & WINDOW_ID_MASK) == id)
+ return true;
+ return false;
+}
+
+bool CGUIWindowManager::IsDialogTopmost(const std::string &xmlFile, bool modal /* = false */) const
+{
+ CGUIWindow *topmost = GetWindow(GetTopmostDialog(modal, false));
+ if (topmost && StringUtils::EqualsNoCase(URIUtils::GetFileName(topmost->GetProperty("xmlfile").asString()), xmlFile))
+ return true;
+ return false;
+}
+
+bool CGUIWindowManager::HasVisibleControls()
+{
+ CSingleExit lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ if (m_activeDialogs.empty())
+ {
+ CGUIWindow *window(GetWindow(GetActiveWindow()));
+ return !window || window->HasVisibleControls();
+ }
+ else
+ return true;
+}
+
+void CGUIWindowManager::ClearWindowHistory()
+{
+ while (!m_windowHistory.empty())
+ m_windowHistory.pop_back();
+}
+
+void CGUIWindowManager::CloseWindowSync(CGUIWindow *window, int nextWindowID /*= 0*/)
+{
+ // Abort touch action if active
+ if (m_touchGestureActive && !m_inhibitTouchGestureEvents)
+ {
+ CLog::Log(LOGDEBUG, "Closing window {} with active touch gesture, sending gesture abort event",
+ window->GetID());
+ window->OnAction({ACTION_GESTURE_ABORT});
+ // Don't send any mid-gesture events to next window until new touch starts
+ m_inhibitTouchGestureEvents = true;
+ }
+
+ window->Close(false, nextWindowID);
+
+ bool renderLoopProcessed = true;
+ while (window->IsAnimating(ANIM_TYPE_WINDOW_CLOSE) && renderLoopProcessed)
+ renderLoopProcessed = ProcessRenderLoop(true);
+}
+
+#ifdef _DEBUG
+void CGUIWindowManager::DumpTextureUse()
+{
+ CGUIWindow* pWindow = GetWindow(GetActiveWindow());
+ if (pWindow)
+ pWindow->DumpTextureUse();
+
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ for (const auto& window : m_activeDialogs)
+ {
+ if (window->IsDialogRunning())
+ window->DumpTextureUse();
+ }
+}
+#endif
diff --git a/xbmc/guilib/GUIWindowManager.h b/xbmc/guilib/GUIWindowManager.h
new file mode 100644
index 0000000..9bbd281
--- /dev/null
+++ b/xbmc/guilib/GUIWindowManager.h
@@ -0,0 +1,285 @@
+/*
+ * 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 "DirtyRegionTracker.h"
+#include "GUIWindow.h"
+#include "IMsgTargetCallback.h"
+#include "IWindowManagerCallback.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/IMessageTarget.h"
+
+#include <list>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+class CGUIDialog;
+class CGUIMediaWindow;
+
+#ifdef TARGET_WINDOWS_STORE
+#pragma pack(push, 8)
+#endif
+enum class DialogModalityType;
+#ifdef TARGET_WINDOWS_STORE
+#pragma pack(pop)
+#endif
+
+namespace KODI
+{
+ namespace MESSAGING
+ {
+ class CApplicationMessenger;
+ }
+}
+
+#define WINDOW_ID_MASK 0xffff
+
+/*!
+ \ingroup winman
+ \brief
+ */
+class CGUIWindowManager : public KODI::MESSAGING::IMessageTarget
+{
+ friend CGUIDialog;
+ friend CGUIMediaWindow;
+public:
+ CGUIWindowManager();
+ ~CGUIWindowManager() override;
+ bool SendMessage(CGUIMessage& message);
+ bool SendMessage(int message, int senderID, int destID, int param1 = 0, int param2 = 0);
+ bool SendMessage(CGUIMessage& message, int window);
+ void Initialize();
+ void Add(CGUIWindow* pWindow);
+ void AddUniqueInstance(CGUIWindow *window);
+ void AddCustomWindow(CGUIWindow* pWindow);
+ void Remove(int id);
+ void Delete(int id);
+ void ActivateWindow(int iWindowID, const std::string &strPath = "");
+ void ForceActivateWindow(int iWindowID, const std::string &strPath = "");
+ void ChangeActiveWindow(int iNewID, const std::string &strPath = "");
+ void ActivateWindow(int iWindowID, const std::vector<std::string>& params, bool swappingWindows = false, bool force = false);
+ void PreviousWindow();
+
+ /**
+ * \brief Switch window to fullscreen
+ *
+ * \param force enforce fullscreen switch
+ * \return True if switch is made to fullscreen, otherwise False
+ */
+ bool SwitchToFullScreen(bool force = false);
+
+ void CloseDialogs(bool forceClose = false) const;
+ void CloseInternalModalDialogs(bool forceClose = false) const;
+
+ void OnApplicationMessage(KODI::MESSAGING::ThreadMessage* pMsg) override;
+ int GetMessageMask() override;
+
+ // OnAction() runs through our active dialogs and windows and sends the message
+ // off to the callbacks (application, python, playlist player) and to the
+ // currently focused window(s). Returns true only if the message is handled.
+ bool OnAction(const CAction &action) const;
+
+ /*! \brief Process active controls allowing them to animate before rendering.
+ */
+ void Process(unsigned int currentTime);
+
+ /*! \brief Mark the screen as dirty, forcing a redraw at the next Render()
+ */
+ void MarkDirty();
+
+ /*! \brief Mark a region as dirty, forcing a redraw at the next Render()
+ */
+ void MarkDirty(const CRect& rect);
+
+ /*! \brief Rendering of the current window and any dialogs
+ Render is called every frame to draw the current window and any dialogs.
+ It should only be called from the application thread.
+ Returns true only if it has rendered something.
+ */
+ bool Render();
+
+ void RenderEx() const;
+
+ /*! \brief Do any post render activities.
+ */
+ void AfterRender();
+
+ /*! \brief Per-frame updating of the current window and any dialogs
+ FrameMove is called every frame to update the current window and any dialogs
+ on screen. It should only be called from the application thread.
+ */
+ void FrameMove();
+
+ /*! \brief Return whether the window manager is initialized.
+ The window manager is initialized on skin load - if the skin isn't yet loaded,
+ no windows should be able to be initialized.
+ \return true if the window manager is initialized, false otherwise.
+ */
+ bool Initialized() const { return m_initialized; }
+
+ /*! \brief Create and initialize all windows and dialogs
+ */
+ void CreateWindows();
+
+ /*! \brief Destroy and remove all windows and dialogs
+ *
+ * \return true on success, false if destruction fails for any window
+ */
+ bool DestroyWindows();
+
+ /*! \brief Destroy and remove the window or dialog with the given id
+ *
+ *\param id the window id
+ */
+ void DestroyWindow(int id);
+
+ /*! \brief Return the window of type \code{T} with the given id or
+ * null if no window exists with the given id.
+ *
+ * \tparam T the window class type
+ * \param id the window id
+ * \return the window with for the given type \code{T} or null
+ */
+ template<typename T,
+ typename std::enable_if<std::is_base_of<CGUIWindow, T>::value>::type* = nullptr>
+ T* GetWindow(int id) const
+ {
+ return dynamic_cast<T*>(GetWindow(id));
+ }
+
+ /*! \brief Return the window with the given id or null.
+ *
+ * \param id the window id
+ * \return the window with the given id or null
+ */
+ CGUIWindow* GetWindow(int id) const;
+
+ /*! \brief Return the dialog window with the given id or null.
+ *
+ * \param id the dialog window id
+ * \return the dialog window with the given id or null
+ */
+ CGUIDialog* GetDialog(int id) const;
+
+ void SetCallback(IWindowManagerCallback& callback);
+ void DeInitialize();
+
+ /*! \brief Register a dialog as active dialog
+ *
+ * \param dialog The dialog to register as active dialog
+ */
+ void RegisterDialog(CGUIWindow* dialog);
+ void RemoveDialog(int id);
+
+ /*! \brief Get the ID of the topmost dialog
+ *
+ * \param ignoreClosing ignore dialog is closing
+ * \return the ID of the topmost dialog or WINDOW_INVALID if no dialog is active
+ */
+ int GetTopmostDialog(bool ignoreClosing = false) const;
+
+ /*! \brief Get the ID of the topmost modal dialog
+ *
+ * \param ignoreClosing ignore dialog is closing
+ * \return the ID of the topmost modal dialog or WINDOW_INVALID if no modal dialog is active
+ */
+ int GetTopmostModalDialog(bool ignoreClosing = false) const;
+
+ void SendThreadMessage(CGUIMessage& message, int window = 0);
+ void DispatchThreadMessages();
+ // method to removed queued messages with message id in the requested message id list.
+ // pMessageIDList: point to first integer of a 0 ends integer array.
+ int RemoveThreadMessageByMessageIds(int *pMessageIDList);
+ void AddMsgTarget( IMsgTargetCallback* pMsgTarget );
+ int GetActiveWindow() const;
+ int GetActiveWindowOrDialog() const;
+ bool HasModalDialog(bool ignoreClosing) const;
+ bool HasVisibleModalDialog() const;
+ bool IsDialogTopmost(int id, bool modal = false) const;
+ bool IsDialogTopmost(const std::string &xmlFile, bool modal = false) const;
+ bool IsModalDialogTopmost(int id) const;
+ bool IsModalDialogTopmost(const std::string &xmlFile) const;
+ bool IsWindowActive(int id, bool ignoreClosing = true) const;
+ bool IsWindowVisible(int id) const;
+ bool IsWindowActive(const std::string &xmlFile, bool ignoreClosing = true) const;
+ bool IsWindowVisible(const std::string &xmlFile) const;
+ /*! \brief Checks if the given window is an addon window.
+ *
+ * \return true if the given window is an addon window, otherwise false.
+ */
+ bool IsAddonWindow(int id) const { return (id >= WINDOW_ADDON_START && id <= WINDOW_ADDON_END); }
+ /*! \brief Checks if the given window is a python window.
+ *
+ * \return true if the given window is a python window, otherwise false.
+ */
+ bool IsPythonWindow(int id) const
+ {
+ return (id >= WINDOW_PYTHON_START && id <= WINDOW_PYTHON_END);
+ }
+
+ bool HasVisibleControls();
+
+#ifdef _DEBUG
+ void DumpTextureUse();
+#endif
+private:
+ void RenderPass() const;
+
+ void LoadNotOnDemandWindows();
+ void UnloadNotOnDemandWindows();
+ void AddToWindowHistory(int newWindowID);
+
+ /*!
+ \brief Check if the given window id is in the window history, and if so, remove this
+ window and all overlying windows from the history so that we always have a predictable
+ "Back" behaviour for each window.
+
+ \param windowID the window id to remove from the window history
+ */
+ void RemoveFromWindowHistory(int windowID);
+ void ClearWindowHistory();
+ void CloseWindowSync(CGUIWindow *window, int nextWindowID = 0);
+ int GetTopmostDialog(bool modal, bool ignoreClosing) const;
+
+ friend class KODI::MESSAGING::CApplicationMessenger;
+
+ /*! \brief Activate the given window.
+ *
+ * \param windowID The window ID to activate.
+ * \param params Parameter
+ * \param swappingWindows True if the window should be swapped with the previous window instead of put it in the window history, otherwise false
+ * \param force True to ignore checks which refuses opening the window, otherwise false
+ */
+ void ActivateWindow_Internal(int windowID, const std::vector<std::string> &params, bool swappingWindows, bool force = false);
+
+ bool ProcessRenderLoop(bool renderOnly);
+
+ bool HandleAction(const CAction &action) const;
+
+ std::unordered_map<int, CGUIWindow*> m_mapWindows;
+ std::vector<CGUIWindow*> m_vecCustomWindows;
+ std::vector<CGUIWindow*> m_activeDialogs;
+ std::vector<CGUIWindow*> m_deleteWindows;
+
+ std::deque<int> m_windowHistory;
+
+ IWindowManagerCallback* m_pCallback;
+ std::list< std::pair<CGUIMessage*,int> > m_vecThreadMessages;
+ CCriticalSection m_critSection;
+ std::vector<IMsgTargetCallback*> m_vecMsgTargets;
+
+ int m_iNested;
+ bool m_initialized;
+ mutable bool m_touchGestureActive{false};
+ mutable bool m_inhibitTouchGestureEvents{false};
+
+ CDirtyRegionList m_dirtyregions;
+ CDirtyRegionTracker m_tracker;
+};
diff --git a/xbmc/guilib/GUIWrappingListContainer.cpp b/xbmc/guilib/GUIWrappingListContainer.cpp
new file mode 100644
index 0000000..e1bc3c0
--- /dev/null
+++ b/xbmc/guilib/GUIWrappingListContainer.cpp
@@ -0,0 +1,240 @@
+/*
+ * 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 "GUIWrappingListContainer.h"
+
+#include "FileItem.h"
+#include "GUIListItemLayout.h"
+#include "GUIMessage.h"
+#include "input/Key.h"
+
+CGUIWrappingListContainer::CGUIWrappingListContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems, int fixedPosition)
+ : CGUIBaseContainer(parentID, controlID, posX, posY, width, height, orientation, scroller, preloadItems)
+{
+ SetCursor(fixedPosition);
+ ControlType = GUICONTAINER_WRAPLIST;
+ m_type = VIEW_TYPE_LIST;
+ m_extraItems = 0;
+}
+
+CGUIWrappingListContainer::~CGUIWrappingListContainer(void) = default;
+
+void CGUIWrappingListContainer::UpdatePageControl(int offset)
+{
+ if (m_pageControl)
+ { // tell our pagecontrol (scrollbar or whatever) to update (offset it by our cursor position)
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), m_pageControl, GetNumItems() ? CorrectOffset(offset, GetCursor()) % GetNumItems() : 0);
+ SendWindowMessage(msg);
+ }
+}
+
+bool CGUIWrappingListContainer::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_PAGE_UP:
+ Scroll(-m_itemsPerPage);
+ return true;
+ case ACTION_PAGE_DOWN:
+ Scroll(m_itemsPerPage);
+ return true;
+ // smooth scrolling (for analog controls)
+ case ACTION_SCROLL_UP:
+ {
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+ Scroll(-1);
+ }
+ return handled;
+ }
+ break;
+ case ACTION_SCROLL_DOWN:
+ {
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+ Scroll(1);
+ }
+ return handled;
+ }
+ break;
+ }
+ return CGUIBaseContainer::OnAction(action);
+}
+
+bool CGUIWrappingListContainer::OnMessage(CGUIMessage& message)
+{
+ if (message.GetControlId() == GetID() )
+ {
+ if (message.GetMessage() == GUI_MSG_PAGE_CHANGE)
+ {
+ if (message.GetSenderId() == m_pageControl && IsVisible())
+ { // offset by our cursor position
+ message.SetParam1(message.GetParam1() - GetCursor());
+ }
+ }
+ }
+ return CGUIBaseContainer::OnMessage(message);
+}
+
+bool CGUIWrappingListContainer::MoveUp(bool wrapAround)
+{
+ Scroll(-1);
+ return true;
+}
+
+bool CGUIWrappingListContainer::MoveDown(bool wrapAround)
+{
+ Scroll(+1);
+ return true;
+}
+
+// scrolls the said amount
+void CGUIWrappingListContainer::Scroll(int amount)
+{
+ ScrollToOffset(GetOffset() + amount);
+}
+
+bool CGUIWrappingListContainer::GetOffsetRange(int &minOffset, int &maxOffset) const
+{
+ return false;
+}
+
+void CGUIWrappingListContainer::ValidateOffset()
+{
+ // our minimal amount of items - we need to take into account extra items to display wrapped items when scrolling
+ unsigned int minItems = (unsigned int)m_itemsPerPage + ScrollCorrectionRange() + GetCacheCount() / 2;
+ if (minItems <= m_items.size())
+ return;
+
+ // no need to check the range here, but we need to check we have
+ // more items than slots.
+ ResetExtraItems();
+ if (m_items.size())
+ {
+ size_t numItems = m_items.size();
+ while (m_items.size() < minItems)
+ {
+ // add additional copies of items, as we require extras at render time
+ for (unsigned int i = 0; i < numItems; i++)
+ {
+ m_items.push_back(CGUIListItemPtr(m_items[i]->Clone()));
+ m_extraItems++;
+ }
+ }
+ }
+}
+
+int CGUIWrappingListContainer::CorrectOffset(int offset, int cursor) const
+{
+ if (m_items.size())
+ {
+ int correctOffset = (offset + cursor) % (int)m_items.size();
+ if (correctOffset < 0) correctOffset += m_items.size();
+ return correctOffset;
+ }
+ return 0;
+}
+
+int CGUIWrappingListContainer::GetSelectedItem() const
+{
+ if (m_items.size() > m_extraItems)
+ {
+ int numItems = (int)(m_items.size() - m_extraItems);
+ int correctOffset = (GetOffset() + GetCursor()) % numItems;
+ if (correctOffset < 0) correctOffset += numItems;
+ return correctOffset;
+ }
+ return 0;
+}
+
+bool CGUIWrappingListContainer::SelectItemFromPoint(const CPoint &point)
+{
+ if (!m_focusedLayout || !m_layout)
+ return false;
+
+ const float mouse_scroll_speed = 0.05f;
+ const float mouse_max_amount = 1.0f; // max speed: 1 item per frame
+ float sizeOfItem = m_layout->Size(m_orientation);
+ // see if the point is either side of our focused item
+ float start = GetCursor() * sizeOfItem;
+ float end = start + m_focusedLayout->Size(m_orientation);
+ float pos = (m_orientation == VERTICAL) ? point.y : point.x;
+ if (pos < start - 0.5f * sizeOfItem)
+ { // scroll backward
+ if (!InsideLayout(m_layout, point))
+ return false;
+ float amount = std::min((start - pos) / sizeOfItem, mouse_max_amount);
+ m_analogScrollCount += amount * amount * mouse_scroll_speed;
+ if (m_analogScrollCount > 1)
+ {
+ Scroll(-1);
+ m_analogScrollCount-=1.0f;
+ }
+ return true;
+ }
+ else if (pos > end + 0.5f * sizeOfItem)
+ { // scroll forward
+ if (!InsideLayout(m_layout, point))
+ return false;
+
+ float amount = std::min((pos - end) / sizeOfItem, mouse_max_amount);
+ m_analogScrollCount += amount * amount * mouse_scroll_speed;
+ if (m_analogScrollCount > 1)
+ {
+ Scroll(1);
+ m_analogScrollCount-=1.0f;
+ }
+ return true;
+ }
+ return InsideLayout(m_focusedLayout, point);
+}
+
+void CGUIWrappingListContainer::SelectItem(int item)
+{
+ if (item >= 0 && item < (int)m_items.size())
+ ScrollToOffset(item - GetCursor());
+}
+
+void CGUIWrappingListContainer::ResetExtraItems()
+{
+ // delete any extra items
+ if (m_extraItems)
+ m_items.erase(m_items.begin() + m_items.size() - m_extraItems, m_items.end());
+ m_extraItems = 0;
+}
+
+void CGUIWrappingListContainer::Reset()
+{
+ ResetExtraItems();
+ CGUIBaseContainer::Reset();
+}
+
+int CGUIWrappingListContainer::GetCurrentPage() const
+{
+ int offset = CorrectOffset(GetOffset(), GetCursor());
+ if (offset + m_itemsPerPage - GetCursor() >= (int)GetRows()) // last page
+ return (GetRows() + m_itemsPerPage - 1) / m_itemsPerPage;
+ return offset / m_itemsPerPage + 1;
+}
+
+void CGUIWrappingListContainer::SetPageControlRange()
+{
+ if (m_pageControl)
+ {
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), m_pageControl, m_itemsPerPage, GetNumItems());
+ SendWindowMessage(msg);
+ }
+}
diff --git a/xbmc/guilib/GUIWrappingListContainer.dox b/xbmc/guilib/GUIWrappingListContainer.dox
new file mode 100644
index 0000000..bcf8771
--- /dev/null
+++ b/xbmc/guilib/GUIWrappingListContainer.dox
@@ -0,0 +1,139 @@
+/*!
+
+\page Wrap_List_Container Wrap List Container
+\brief **Used for a wrapping list of items with fixed focus.**
+
+\tableofcontents
+
+The wrap list container is one of several containers used to display items
+fromfile lists in various ways. The wrap list container is the same as the
+\ref List_Container "List Container", with two exceptions:
+ 1. The focused item is fixed.
+ 2. The items "wrap" around once they reach the end.
+
+As with all container controls, the layout of the items within the control is
+very flexible.
+
+--------------------------------------------------------------------------------
+\section Wrap_List_Container_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="wraplist" id="50">
+ <description>My first wraplist container</description>
+ <posx>80</posx>
+ <posy>60</posy>
+ <width>250</width>
+ <height>200</height>
+ <visible>true</visible>
+ <onup>2</onup>
+ <ondown>3</ondown>
+ <onleft>1</onleft>
+ <onright>1</onright>
+ <viewtype label="3D list">list</viewtype>
+ <orientation>vertical</orientation>
+ <pagecontrol>25</pagecontrol>
+ <focusposition>3</focusposition>
+ <scrolltime tween="sine" easing="out">200</scrolltime>
+ <autoscroll>true</autoscroll>
+ <itemlayout width="250" height="29">
+ <control type="image">
+ <posx>5</posx>
+ <posy>3</posy>
+ <width>22</width>
+ <height>22</height>
+ <info>ListItem.Icon</info>
+ </control>
+ <control type="label">
+ <posx>30</posx>
+ <posy>3</posy>
+ <width>430</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <align>left</align>
+ <info>ListItem.Label</info>
+ </control>
+ <control type="label">
+ <posx>475</posx>
+ <posy>3</posy>
+ <width>300</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <textcolor>grey</textcolor>
+ <align>right</align>
+ <info>ListItem.Label2</info>
+ </control>
+ </itemlayout>
+ <focusedlayout height="29" width="250">
+ <control type="image">
+ <width>485</width>
+ <height>29</height>
+ <posx>0</posx>
+ <posy>0</posy>
+ <visible>Control.HasFocus(50)</visible>
+ <texture>list-focus.png</texture>
+ </control>
+ <control type="image">
+ <posx>5</posx>
+ <posy>3</posy>
+ <width>22</width>
+ <height>22</height>
+ <info>ListItem.Icon</info>
+ </control>
+ <control type="label">
+ <posx>30</posx>
+ <posy>3</posy>
+ <width>430</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <align>left</align>
+ <info>ListItem.Label</info>
+ </control>
+ <control type="label">
+ <posx>475</posx>
+ <posy>3</posy>
+ <width>300</width>
+ <height>22</height>
+ <font>font13</font>
+ <aligny>center</aligny>
+ <selectedcolor>green</selectedcolor>
+ <textcolor>grey</textcolor>
+ <align>right</align>
+ <info>ListItem.Label2</info>
+ </control>
+ </focusedlayout>
+</control>
+~~~~~~~~~~~~~
+
+--------------------------------------------------------------------------------
+\section Wrap_List_Container_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|--------------:|:--------------------------------------------------------------|
+| viewtype | The type of view. Choices are list, icon, wide, wrap, biglist, bigicon, bigwide, bigwrap, info and biginfo. The label attribute indicates the label that will be used in the <i>"View As"</i> control within the GUI. It is localizable via strings.po. viewtype has no effect on the view itself. It is used by kodi when switching skin to automatically select a view with a similar layout. Skinners should try to set _viewtype_ to describe the layout as best as possible.
+| orientation | The orientation of the list. Defaults to vertical.
+| pagecontrol | Used to set the <b>`<id>`</b> of the page control used to control this list.
+| scrolltime | The time (in ms) to scroll from one item to another. By default, this is 200ms. The list will scroll smoothly from one item to another as needed. Set it to zero to disable the smooth scrolling. The scroll movement can be further adjusted by selecting one of the available [tween](http://kodi.wiki/view/Tweeners) methods.
+| focusposition | Specifies the index (from 0 -> number items displayable - 1) of the focused item. The focused item doesn't move - as the user moves up and down (or left and right) the items scroll instead.
+| itemlayout | Specifies the layout of items in the list. Requires the height attribute set in a vertical list, and the width attribute set for a horizontal list. The <b>`<itemlayout>`</b> then contains as many label and image controls as required. [See here for more information](http://kodi.wiki/view/Container_Item_Layout).
+| focusedlayout | Specifies the layout of items in the list that have focus. Requires the height attribute set in a vertical list, and the width attribute set for a horizontal list. The <b>`<focusedlayout>`</b> then contains as many label and image controls as required. [See here for more information](http://kodi.wiki/view/Container_Item_Layout).
+| content | Used to set the item content that this list will contain. Allows the skinner to setup a list anywhere they want with a static set of content, as a useful alternative to the grouplist control. [See here for more information](http://kodi.wiki/view/Static_List_Content)
+| autoscroll | Used to make the container scroll automatically
+
+--------------------------------------------------------------------------------
+\section Wrap_List_Container_sect3 See also
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/guilib/GUIWrappingListContainer.h b/xbmc/guilib/GUIWrappingListContainer.h
new file mode 100644
index 0000000..da1e106
--- /dev/null
+++ b/xbmc/guilib/GUIWrappingListContainer.h
@@ -0,0 +1,50 @@
+/*
+ * 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
+
+/*!
+\file GUIListContainer.h
+\brief
+*/
+
+#include "GUIBaseContainer.h"
+/*!
+ \ingroup controls
+ \brief
+ */
+class CGUIWrappingListContainer : public CGUIBaseContainer
+{
+public:
+ CGUIWrappingListContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems, int fixedPosition);
+ ~CGUIWrappingListContainer(void) override;
+ CGUIWrappingListContainer* Clone() const override { return new CGUIWrappingListContainer(*this); }
+
+ bool OnAction(const CAction &action) override;
+ bool OnMessage(CGUIMessage& message) override;
+ int GetSelectedItem() const override;
+
+protected:
+ void Scroll(int amount) override;
+ bool MoveDown(bool wrapAround) override;
+ bool MoveUp(bool wrapAround) override;
+ bool GetOffsetRange(int &minOffset, int &maxOffset) const override;
+ void ValidateOffset() override;
+ int CorrectOffset(int offset, int cursor) const override;
+ bool SelectItemFromPoint(const CPoint &point) override;
+ void SelectItem(int item) override;
+ void Reset() override;
+ size_t GetNumItems() const override { return m_items.size() - m_extraItems; }
+ int GetCurrentPage() const override;
+ void SetPageControlRange() override;
+ void UpdatePageControl(int offset) override;
+
+ void ResetExtraItems();
+ unsigned int m_extraItems;
+};
+
diff --git a/xbmc/guilib/IAudioDeviceChangedCallback.h b/xbmc/guilib/IAudioDeviceChangedCallback.h
new file mode 100644
index 0000000..54800fd
--- /dev/null
+++ b/xbmc/guilib/IAudioDeviceChangedCallback.h
@@ -0,0 +1,18 @@
+/*
+ * 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
+
+class IAudioDeviceChangedCallback
+{
+public:
+ virtual void Initialize(int iDevice)=0;
+ virtual void DeInitialize(int iDevice)=0;
+ virtual ~IAudioDeviceChangedCallback() {}
+};
+
diff --git a/xbmc/guilib/IDirtyRegionSolver.h b/xbmc/guilib/IDirtyRegionSolver.h
new file mode 100644
index 0000000..6702d07
--- /dev/null
+++ b/xbmc/guilib/IDirtyRegionSolver.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "DirtyRegion.h"
+
+#define DIRTYREGION_SOLVER_FILL_VIEWPORT_ALWAYS 0
+#define DIRTYREGION_SOLVER_UNION 1
+#define DIRTYREGION_SOLVER_COST_REDUCTION 2
+#define DIRTYREGION_SOLVER_FILL_VIEWPORT_ON_CHANGE 3
+
+class IDirtyRegionSolver
+{
+public:
+ virtual ~IDirtyRegionSolver() = default;
+
+ // Takes a number of dirty regions which will become a number of needed rendering passes.
+ virtual void Solve(const CDirtyRegionList &input, CDirtyRegionList &output) = 0;
+};
diff --git a/xbmc/guilib/IGUIContainer.h b/xbmc/guilib/IGUIContainer.h
new file mode 100644
index 0000000..93b404f
--- /dev/null
+++ b/xbmc/guilib/IGUIContainer.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "GUIControl.h"
+
+#include <memory>
+
+typedef std::shared_ptr<CGUIListItem> CGUIListItemPtr;
+
+/*!
+ \ingroup controls
+ \brief
+ */
+
+class IGUIContainer : public CGUIControl
+{
+protected:
+ VIEW_TYPE m_type;
+ std::string m_label;
+public:
+ IGUIContainer(int parentID, int controlID, float posX, float posY, float width, float height)
+ : CGUIControl(parentID, controlID, posX, posY, width, height), m_type(VIEW_TYPE_NONE) {}
+
+ bool IsContainer() const override { return true; }
+
+ VIEW_TYPE GetType() const { return m_type; }
+ const std::string& GetLabel() const { return m_label; }
+ void SetType(VIEW_TYPE type, const std::string &label)
+ {
+ m_type = type;
+ m_label = label;
+ }
+
+ virtual CGUIListItemPtr GetListItem(int offset, unsigned int flag = 0) const = 0;
+ virtual std::string GetLabel(int info) const = 0;
+};
diff --git a/xbmc/guilib/IMsgTargetCallback.h b/xbmc/guilib/IMsgTargetCallback.h
new file mode 100644
index 0000000..09d888e
--- /dev/null
+++ b/xbmc/guilib/IMsgTargetCallback.h
@@ -0,0 +1,27 @@
+/*
+ * 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
+
+/*!
+\file IMsgTargetCallback.h
+\brief
+*/
+
+#include "GUIMessage.h"
+
+/*!
+ \ingroup winman
+ \brief
+ */
+class IMsgTargetCallback
+{
+public:
+ virtual bool OnMessage(CGUIMessage& message) = 0;
+ virtual ~IMsgTargetCallback() = default;
+};
diff --git a/xbmc/guilib/IRenderingCallback.h b/xbmc/guilib/IRenderingCallback.h
new file mode 100644
index 0000000..fb86f0a
--- /dev/null
+++ b/xbmc/guilib/IRenderingCallback.h
@@ -0,0 +1,19 @@
+/*
+ * 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
+
+class IRenderingCallback
+{
+public:
+ virtual ~IRenderingCallback() = default;
+ virtual bool Create(int x, int y, int w, int h, void *device) = 0;
+ virtual void Render() = 0;
+ virtual void Stop() = 0;
+ virtual bool IsDirty() { return true; }
+};
diff --git a/xbmc/guilib/ISliderCallback.h b/xbmc/guilib/ISliderCallback.h
new file mode 100644
index 0000000..20834e2
--- /dev/null
+++ b/xbmc/guilib/ISliderCallback.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013-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 CGUISliderControl;
+
+/*!
+ \brief Interface class for callback from the slider dialog
+
+ Used to pass feedback from the slider dialog to a caller. Users of the
+ slider dialog should derive from this class if they wish to respond to changes
+ in the slider by the user as they happen. OnSliderChange is called in response
+ to the user moving the slider. The caller may then update the text on the slider
+ and update anything that should be changed as the slider is adjusted.
+
+ \sa CGUIDialogSlider
+ */
+class ISliderCallback
+{
+public:
+ virtual ~ISliderCallback() = default;
+
+ /*!
+ \brief Callback function called whenever the user moves the slider
+
+ \param data pointer of callbackData
+ \param slider pointer to the slider control
+ */
+ virtual void OnSliderChange(void *data, CGUISliderControl *slider) = 0;
+};
diff --git a/xbmc/guilib/IWindowManagerCallback.cpp b/xbmc/guilib/IWindowManagerCallback.cpp
new file mode 100644
index 0000000..6909f1e
--- /dev/null
+++ b/xbmc/guilib/IWindowManagerCallback.cpp
@@ -0,0 +1,14 @@
+/*
+ * 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 "IWindowManagerCallback.h"
+
+
+IWindowManagerCallback::IWindowManagerCallback(void) = default;
+
+IWindowManagerCallback::~IWindowManagerCallback(void) = default;
diff --git a/xbmc/guilib/IWindowManagerCallback.h b/xbmc/guilib/IWindowManagerCallback.h
new file mode 100644
index 0000000..f8cdb73
--- /dev/null
+++ b/xbmc/guilib/IWindowManagerCallback.h
@@ -0,0 +1,30 @@
+/*
+ * 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
+
+/*!
+\file IWindowManagerCallback.h
+\brief
+*/
+
+/*!
+ \ingroup winman
+ \brief
+ */
+class IWindowManagerCallback
+{
+public:
+ IWindowManagerCallback(void);
+ virtual ~IWindowManagerCallback(void);
+
+ virtual void FrameMove(bool processEvents, bool processGUI = true) = 0;
+ virtual void Render() = 0;
+ virtual void Process() = 0;
+ virtual bool GetRenderGUI() const { return false; }
+};
diff --git a/xbmc/guilib/LocalizeStrings.cpp b/xbmc/guilib/LocalizeStrings.cpp
new file mode 100644
index 0000000..659eb3d
--- /dev/null
+++ b/xbmc/guilib/LocalizeStrings.cpp
@@ -0,0 +1,250 @@
+/*
+ * 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 "LocalizeStrings.h"
+
+#include "addons/LanguageResource.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "threads/SharedSection.h"
+#include "utils/CharsetConverter.h"
+#include "utils/POUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <shared_mutex>
+
+/*! \brief Tries to load ids and strings from a strings.po file to the `strings` map.
+ * It should only be called from the LoadStr2Mem function to have a fallback.
+ \param pathname The directory name, where we look for the strings file.
+ \param strings [out] The resulting strings map.
+ \param encoding Encoding of the strings. For PO files we only use utf-8.
+ \param offset An offset value to place strings from the id value.
+ \param bSourceLanguage If we are loading the source English strings.po.
+ \return false if no strings.po file was loaded.
+ */
+static bool LoadPO(const std::string &filename, std::map<uint32_t, LocStr>& strings,
+ std::string &encoding, uint32_t offset = 0 , bool bSourceLanguage = false)
+{
+ CPODocument PODoc;
+ if (!PODoc.LoadFile(filename))
+ return false;
+
+ int counter = 0;
+
+ while ((PODoc.GetNextEntry()))
+ {
+ uint32_t id;
+ if (PODoc.GetEntryType() == ID_FOUND)
+ {
+ bool bStrInMem = strings.find((id = PODoc.GetEntryID()) + offset) != strings.end();
+ PODoc.ParseEntry(bSourceLanguage);
+
+ if (bSourceLanguage && !PODoc.GetMsgid().empty())
+ {
+ if (bStrInMem && (strings[id + offset].strOriginal.empty() ||
+ PODoc.GetMsgid() == strings[id + offset].strOriginal))
+ continue;
+ else if (bStrInMem)
+ CLog::Log(
+ LOGDEBUG,
+ "POParser: id:{} was recently re-used in the English string file, which is not yet "
+ "changed in the translated file. Using the English string instead",
+ id);
+ strings[id + offset].strTranslated = PODoc.GetMsgid();
+ counter++;
+ }
+ else if (!bSourceLanguage && !bStrInMem && !PODoc.GetMsgstr().empty())
+ {
+ strings[id + offset].strTranslated = PODoc.GetMsgstr();
+ strings[id + offset].strOriginal = PODoc.GetMsgid();
+ counter++;
+ }
+ }
+ else if (PODoc.GetEntryType() == MSGID_FOUND)
+ {
+ //! @todo implement reading of non-id based string entries from the PO files.
+ //! These entries would go into a separate memory map, using hash codes for fast look-up.
+ //! With this memory map we can implement using gettext(), ngettext(), pgettext() calls,
+ //! so that we don't have to use new IDs for new strings. Even we can start converting
+ //! the ID based calls to normal gettext calls.
+ }
+ else if (PODoc.GetEntryType() == MSGID_PLURAL_FOUND)
+ {
+ //! @todo implement reading of non-id based pluralized string entries from the PO files.
+ //! We can store the pluralforms for each language, in the langinfo.xml files.
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "LocalizeStrings: loaded {} strings from file {}", counter, filename);
+ return true;
+}
+
+/*! \brief Loads language ids and strings to memory map `strings`.
+ \param pathname The directory name, where we look for the strings file.
+ \param language We load the strings for this language. Fallback language is always English.
+ \param strings [out] The resulting strings map.
+ \param encoding Encoding of the strings. For PO files we only use utf-8.
+ \param offset An offset value to place strings from the id value.
+ \return false if no strings.po file was loaded.
+ */
+static bool LoadStr2Mem(const std::string &pathname_in, const std::string &language,
+ std::map<uint32_t, LocStr>& strings, std::string &encoding, uint32_t offset = 0 )
+{
+ std::string pathname = CSpecialProtocol::TranslatePathConvertCase(pathname_in + language);
+ if (!XFILE::CDirectory::Exists(pathname))
+ {
+ bool exists = false;
+ std::string lang;
+ // check if there's a language addon using the old language naming convention
+ if (ADDON::CLanguageResource::FindLegacyLanguage(language, lang))
+ {
+ pathname = CSpecialProtocol::TranslatePathConvertCase(pathname_in + lang);
+ exists = XFILE::CDirectory::Exists(pathname);
+ }
+
+ if (!exists)
+ return false;
+ }
+
+ bool useSourceLang = StringUtils::EqualsNoCase(language, LANGUAGE_DEFAULT) || StringUtils::EqualsNoCase(language, LANGUAGE_OLD_DEFAULT);
+
+ return LoadPO(URIUtils::AddFileToFolder(pathname, "strings.po"), strings, encoding, offset, useSourceLang);
+}
+
+static bool LoadWithFallback(const std::string& path, const std::string& language, std::map<uint32_t, LocStr>& strings)
+{
+ std::string encoding;
+ if (!LoadStr2Mem(path, language, strings, encoding))
+ {
+ if (StringUtils::EqualsNoCase(language, LANGUAGE_DEFAULT)) // no fallback, nothing to do
+ return false;
+ }
+
+ // load the fallback
+ if (!StringUtils::EqualsNoCase(language, LANGUAGE_DEFAULT))
+ LoadStr2Mem(path, LANGUAGE_DEFAULT, strings, encoding);
+
+ return true;
+}
+
+CLocalizeStrings::CLocalizeStrings(void) = default;
+
+CLocalizeStrings::~CLocalizeStrings(void) = default;
+
+void CLocalizeStrings::ClearSkinStrings()
+{
+ // clear the skin strings
+ std::unique_lock<CSharedSection> lock(m_stringsMutex);
+ Clear(31000, 31999);
+}
+
+bool CLocalizeStrings::LoadSkinStrings(const std::string& path, const std::string& language)
+{
+ //! @todo shouldn't hold lock while loading file
+ std::unique_lock<CSharedSection> lock(m_stringsMutex);
+ ClearSkinStrings();
+ // load the skin strings in.
+ return LoadWithFallback(path, language, m_strings);
+}
+
+bool CLocalizeStrings::Load(const std::string& strPathName, const std::string& strLanguage)
+{
+ std::map<uint32_t, LocStr> strings;
+ if (!LoadWithFallback(strPathName, strLanguage, strings))
+ return false;
+
+ // fill in the constant strings
+ strings[20022].strTranslated = "";
+ strings[20027].strTranslated = "°F";
+ strings[20028].strTranslated = "K";
+ strings[20029].strTranslated = "°C";
+ strings[20030].strTranslated = "°Ré";
+ strings[20031].strTranslated = "°Ra";
+ strings[20032].strTranslated = "°Rø";
+ strings[20033].strTranslated = "°De";
+ strings[20034].strTranslated = "°N";
+
+ strings[20200].strTranslated = "km/h";
+ strings[20201].strTranslated = "m/min";
+ strings[20202].strTranslated = "m/s";
+ strings[20203].strTranslated = "ft/h";
+ strings[20204].strTranslated = "ft/min";
+ strings[20205].strTranslated = "ft/s";
+ strings[20206].strTranslated = "mph";
+ strings[20207].strTranslated = "kts";
+ strings[20208].strTranslated = "Beaufort";
+ strings[20209].strTranslated = "inch/s";
+ strings[20210].strTranslated = "yard/s";
+ strings[20211].strTranslated = "Furlong/Fortnight";
+
+ std::unique_lock<CSharedSection> lock(m_stringsMutex);
+ Clear();
+ m_strings = std::move(strings);
+ return true;
+}
+
+const std::string& CLocalizeStrings::Get(uint32_t dwCode) const
+{
+ std::shared_lock<CSharedSection> lock(m_stringsMutex);
+ ciStrings i = m_strings.find(dwCode);
+ if (i == m_strings.end())
+ {
+ return StringUtils::Empty;
+ }
+ return i->second.strTranslated;
+}
+
+void CLocalizeStrings::Clear()
+{
+ std::unique_lock<CSharedSection> lock(m_stringsMutex);
+ m_strings.clear();
+}
+
+void CLocalizeStrings::Clear(uint32_t start, uint32_t end)
+{
+ std::unique_lock<CSharedSection> lock(m_stringsMutex);
+ iStrings it = m_strings.begin();
+ while (it != m_strings.end())
+ {
+ if (it->first >= start && it->first <= end)
+ m_strings.erase(it++);
+ else
+ ++it;
+ }
+}
+
+bool CLocalizeStrings::LoadAddonStrings(const std::string& path, const std::string& language, const std::string& addonId)
+{
+ std::map<uint32_t, LocStr> strings;
+ if (!LoadWithFallback(path, language, strings))
+ return false;
+
+ std::unique_lock<CSharedSection> lock(m_addonStringsMutex);
+ auto it = m_addonStrings.find(addonId);
+ if (it != m_addonStrings.end())
+ m_addonStrings.erase(it);
+
+ return m_addonStrings.emplace(std::string(addonId), std::move(strings)).second;
+}
+
+std::string CLocalizeStrings::GetAddonString(const std::string& addonId, uint32_t code)
+{
+ std::shared_lock<CSharedSection> lock(m_addonStringsMutex);
+ auto i = m_addonStrings.find(addonId);
+ if (i == m_addonStrings.end())
+ return StringUtils::Empty;
+
+ auto j = i->second.find(code);
+ if (j == i->second.end())
+ return StringUtils::Empty;
+
+ return j->second.strTranslated;
+}
diff --git a/xbmc/guilib/LocalizeStrings.h b/xbmc/guilib/LocalizeStrings.h
new file mode 100644
index 0000000..8d73cf5
--- /dev/null
+++ b/xbmc/guilib/LocalizeStrings.h
@@ -0,0 +1,72 @@
+/*
+ * 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
+
+/*!
+\file LocalizeStrings.h
+\brief
+*/
+
+#include "threads/SharedSection.h"
+#include "utils/ILocalizer.h"
+
+#include <map>
+#include <stdint.h>
+#include <string>
+
+/*!
+ \ingroup strings
+ \brief
+ */
+
+struct LocStr
+{
+ std::string strTranslated; // string to be used in xbmc GUI
+ std::string strOriginal; // the original English string the translation is based on
+};
+
+// The default fallback language is fixed to be English
+const std::string LANGUAGE_DEFAULT = "resource.language.en_gb";
+const std::string LANGUAGE_OLD_DEFAULT = "English";
+
+class CLocalizeStrings : public ILocalizer
+{
+public:
+ CLocalizeStrings(void);
+ ~CLocalizeStrings(void) override;
+ bool Load(const std::string& strPathName, const std::string& strLanguage);
+ bool LoadSkinStrings(const std::string& path, const std::string& language);
+ bool LoadAddonStrings(const std::string& path, const std::string& language, const std::string& addonId);
+ void ClearSkinStrings();
+ const std::string& Get(uint32_t code) const;
+ std::string GetAddonString(const std::string& addonId, uint32_t code);
+ void Clear();
+
+ // implementation of ILocalizer
+ std::string Localize(std::uint32_t code) const override { return Get(code); }
+
+protected:
+ void Clear(uint32_t start, uint32_t end);
+
+ std::map<uint32_t, LocStr> m_strings;
+ std::map<std::string, std::map<uint32_t, LocStr>> m_addonStrings;
+ typedef std::map<uint32_t, LocStr>::const_iterator ciStrings;
+ typedef std::map<uint32_t, LocStr>::iterator iStrings;
+
+ mutable CSharedSection m_stringsMutex;
+ CSharedSection m_addonStringsMutex;
+};
+
+/*!
+ \ingroup strings
+ \brief
+ */
+extern CLocalizeStrings g_localizeStrings;
+extern CLocalizeStrings g_localizeStringsTemp;
+
diff --git a/xbmc/guilib/Shader.cpp b/xbmc/guilib/Shader.cpp
new file mode 100644
index 0000000..37aedcf
--- /dev/null
+++ b/xbmc/guilib/Shader.cpp
@@ -0,0 +1,384 @@
+/*
+ * 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 "Shader.h"
+
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "rendering/RenderSystem.h"
+#include "utils/GLUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#ifdef HAS_GLES
+#define GLchar char
+#endif
+
+#define LOG_SIZE 1024
+
+using namespace Shaders;
+using namespace XFILE;
+
+//////////////////////////////////////////////////////////////////////
+// CShader
+//////////////////////////////////////////////////////////////////////
+bool CShader::LoadSource(const std::string& filename, const std::string& prefix)
+{
+ if(filename.empty())
+ return true;
+
+ CFileStream file;
+
+ std::string path = "special://xbmc/system/shaders/";
+ path += CServiceBroker::GetRenderSystem()->GetShaderPath(filename);
+ path += filename;
+ if(!file.Open(path))
+ {
+ CLog::Log(LOGERROR, "CYUVShaderGLSL::CYUVShaderGLSL - failed to open file {}", filename);
+ return false;
+ }
+ getline(file, m_source, '\0');
+
+ size_t pos = 0;
+ size_t versionPos = m_source.find("#version");
+ if (versionPos != std::string::npos)
+ {
+ versionPos = m_source.find('\n', versionPos);
+ if (versionPos != std::string::npos)
+ pos = versionPos + 1;
+ }
+ m_source.insert(pos, prefix);
+
+ m_filenames = filename;
+
+ return true;
+}
+
+bool CShader::AppendSource(const std::string& filename)
+{
+ if(filename.empty())
+ return true;
+
+ CFileStream file;
+ std::string temp;
+
+ std::string path = "special://xbmc/system/shaders/";
+ path += CServiceBroker::GetRenderSystem()->GetShaderPath(filename);
+ path += filename;
+ if(!file.Open(path))
+ {
+ CLog::Log(LOGERROR, "CShader::AppendSource - failed to open file {}", filename);
+ return false;
+ }
+ getline(file, temp, '\0');
+ m_source.append(temp);
+
+ m_filenames.append(" " + filename);
+
+ return true;
+}
+
+bool CShader::InsertSource(const std::string& filename, const std::string& loc)
+{
+ if(filename.empty())
+ return true;
+
+ CFileStream file;
+ std::string temp;
+
+ std::string path = "special://xbmc/system/shaders/";
+ path += CServiceBroker::GetRenderSystem()->GetShaderPath(filename);
+ path += filename;
+ if(!file.Open(path))
+ {
+ CLog::Log(LOGERROR, "CShader::InsertSource - failed to open file {}", filename);
+ return false;
+ }
+ getline(file, temp, '\0');
+
+ size_t locPos = m_source.find(loc);
+ if (locPos == std::string::npos)
+ {
+ CLog::Log(LOGERROR, "CShader::InsertSource - could not find location {}", loc);
+ return false;
+ }
+
+ m_source.insert(locPos, temp);
+
+ m_filenames.append(" " + filename);
+
+ return true;
+}
+
+std::string CShader::GetSourceWithLineNumbers() const
+{
+ int i{1};
+ auto lines = StringUtils::Split(m_source, "\n");
+ for (auto& line : lines)
+ {
+ line.insert(0, StringUtils::Format("{:3}: ", i));
+ i++;
+ }
+
+ auto output = StringUtils::Join(lines, "\n");
+
+ return output;
+}
+
+
+//////////////////////////////////////////////////////////////////////
+// CGLSLVertexShader
+//////////////////////////////////////////////////////////////////////
+
+bool CGLSLVertexShader::Compile()
+{
+ GLint params[4];
+
+ Free();
+
+ m_vertexShader = glCreateShader(GL_VERTEX_SHADER);
+ const char *ptr = m_source.c_str();
+ glShaderSource(m_vertexShader, 1, &ptr, 0);
+ glCompileShader(m_vertexShader);
+ glGetShaderiv(m_vertexShader, GL_COMPILE_STATUS, params);
+ VerifyGLState();
+ if (params[0] != GL_TRUE)
+ {
+ GLchar log[LOG_SIZE];
+ CLog::Log(LOGERROR, "GL: Error compiling vertex shader");
+ glGetShaderInfoLog(m_vertexShader, LOG_SIZE, NULL, log);
+ CLog::Log(LOGERROR, "{}", log);
+ m_lastLog = log;
+ m_compiled = false;
+ }
+ else
+ {
+ GLchar log[LOG_SIZE];
+ GLsizei length;
+ glGetShaderInfoLog(m_vertexShader, LOG_SIZE, &length, log);
+ if (length > 0)
+ {
+ CLog::Log(LOGDEBUG, "GL: Vertex Shader compilation log:");
+ CLog::Log(LOGDEBUG, "{}", log);
+ }
+ m_lastLog = log;
+ m_compiled = true;
+ }
+ return m_compiled;
+}
+
+void CGLSLVertexShader::Free()
+{
+ if (m_vertexShader)
+ glDeleteShader(m_vertexShader);
+ m_vertexShader = 0;
+}
+
+//////////////////////////////////////////////////////////////////////
+// CGLSLPixelShader
+//////////////////////////////////////////////////////////////////////
+bool CGLSLPixelShader::Compile()
+{
+ GLint params[4];
+
+ Free();
+
+ // Pixel shaders are not mandatory.
+ if (m_source.length()==0)
+ {
+ CLog::Log(LOGINFO, "GL: No pixel shader, fixed pipeline in use");
+ return true;
+ }
+
+ m_pixelShader = glCreateShader(GL_FRAGMENT_SHADER);
+ const char *ptr = m_source.c_str();
+ glShaderSource(m_pixelShader, 1, &ptr, 0);
+ glCompileShader(m_pixelShader);
+ glGetShaderiv(m_pixelShader, GL_COMPILE_STATUS, params);
+ if (params[0] != GL_TRUE)
+ {
+ GLchar log[LOG_SIZE];
+ CLog::Log(LOGERROR, "GL: Error compiling pixel shader");
+ glGetShaderInfoLog(m_pixelShader, LOG_SIZE, NULL, log);
+ CLog::Log(LOGERROR, "{}", log);
+ m_lastLog = log;
+ m_compiled = false;
+ }
+ else
+ {
+ GLchar log[LOG_SIZE];
+ GLsizei length;
+ glGetShaderInfoLog(m_pixelShader, LOG_SIZE, &length, log);
+ if (length > 0)
+ {
+ CLog::Log(LOGDEBUG, "GL: Pixel Shader compilation log:");
+ CLog::Log(LOGDEBUG, "{}", log);
+ }
+ m_lastLog = log;
+ m_compiled = true;
+ }
+ return m_compiled;
+}
+
+void CGLSLPixelShader::Free()
+{
+ if (m_pixelShader)
+ glDeleteShader(m_pixelShader);
+ m_pixelShader = 0;
+}
+
+//////////////////////////////////////////////////////////////////////
+// CGLSLShaderProgram
+//////////////////////////////////////////////////////////////////////
+CGLSLShaderProgram::CGLSLShaderProgram()
+{
+ m_pFP = new CGLSLPixelShader();
+ m_pVP = new CGLSLVertexShader();
+}
+
+CGLSLShaderProgram::CGLSLShaderProgram(const std::string& vert,
+ const std::string& frag)
+{
+ m_pFP = new CGLSLPixelShader();
+ m_pFP->LoadSource(frag);
+ m_pVP = new CGLSLVertexShader();
+ m_pVP->LoadSource(vert);
+}
+
+CGLSLShaderProgram::~CGLSLShaderProgram()
+{
+ Free();
+}
+
+void CGLSLShaderProgram::Free()
+{
+ m_pVP->Free();
+ VerifyGLState();
+ m_pFP->Free();
+ VerifyGLState();
+ if (m_shaderProgram)
+ {
+ glDeleteProgram(m_shaderProgram);
+ }
+ m_shaderProgram = 0;
+ m_ok = false;
+ m_lastProgram = 0;
+}
+
+bool CGLSLShaderProgram::CompileAndLink()
+{
+ GLint params[4];
+
+ // free resources
+ Free();
+
+ // compiled vertex shader
+ if (!m_pVP->Compile())
+ {
+ CLog::Log(LOGERROR, "GL: Error compiling vertex shader: {}", m_pVP->GetName());
+ CLog::Log(LOGDEBUG, "GL: vertex shader source:\n{}", m_pVP->GetSourceWithLineNumbers());
+ return false;
+ }
+
+ // compile pixel shader
+ if (!m_pFP->Compile())
+ {
+ m_pVP->Free();
+ CLog::Log(LOGERROR, "GL: Error compiling fragment shader: {}", m_pFP->GetName());
+ CLog::Log(LOGDEBUG, "GL: fragment shader source:\n{}", m_pFP->GetSourceWithLineNumbers());
+ return false;
+ }
+
+ // create program object
+ if (!(m_shaderProgram = glCreateProgram()))
+ {
+ CLog::Log(LOGERROR, "GL: Error creating shader program handle");
+ goto error;
+ }
+
+ // attach the vertex shader
+ glAttachShader(m_shaderProgram, m_pVP->Handle());
+ VerifyGLState();
+
+ // if we have a pixel shader, attach it. If not, fixed pipeline
+ // will be used.
+ if (m_pFP->Handle())
+ {
+ glAttachShader(m_shaderProgram, m_pFP->Handle());
+ VerifyGLState();
+ }
+
+ // link the program
+ glLinkProgram(m_shaderProgram);
+ glGetProgramiv(m_shaderProgram, GL_LINK_STATUS, params);
+ if (params[0]!=GL_TRUE)
+ {
+ GLchar log[LOG_SIZE];
+ CLog::Log(LOGERROR, "GL: Error linking shader");
+ glGetProgramInfoLog(m_shaderProgram, LOG_SIZE, NULL, log);
+ CLog::Log(LOGERROR, "{}", log);
+ goto error;
+ }
+ VerifyGLState();
+
+ m_validated = false;
+ m_ok = true;
+ OnCompiledAndLinked();
+ VerifyGLState();
+ return true;
+
+ error:
+ m_ok = false;
+ Free();
+ return false;
+}
+
+bool CGLSLShaderProgram::Enable()
+{
+ if (OK())
+ {
+ glUseProgram(m_shaderProgram);
+ if (OnEnabled())
+ {
+ if (!m_validated)
+ {
+ // validate the program
+ GLint params[4];
+ glValidateProgram(m_shaderProgram);
+ glGetProgramiv(m_shaderProgram, GL_VALIDATE_STATUS, params);
+ if (params[0]!=GL_TRUE)
+ {
+ GLchar log[LOG_SIZE];
+ CLog::Log(LOGERROR, "GL: Error validating shader");
+ glGetProgramInfoLog(m_shaderProgram, LOG_SIZE, NULL, log);
+ CLog::Log(LOGERROR, "{}", log);
+ }
+ m_validated = true;
+ }
+ VerifyGLState();
+ return true;
+ }
+ else
+ {
+ glUseProgram(0);
+ return false;
+ }
+ return true;
+ }
+ return false;
+}
+
+void CGLSLShaderProgram::Disable()
+{
+ if (OK())
+ {
+ glUseProgram(0);
+ OnDisabled();
+ }
+}
+
diff --git a/xbmc/guilib/Shader.h b/xbmc/guilib/Shader.h
new file mode 100644
index 0000000..d86ca2b
--- /dev/null
+++ b/xbmc/guilib/Shader.h
@@ -0,0 +1,174 @@
+/*
+ * 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 <string>
+#include <vector>
+
+#include "system_gl.h"
+
+namespace Shaders {
+
+ //////////////////////////////////////////////////////////////////////
+ // CShader - base class
+ //////////////////////////////////////////////////////////////////////
+ class CShader
+ {
+ public:
+ CShader() = default;
+ virtual ~CShader() = default;
+ virtual bool Compile() = 0;
+ virtual void Free() = 0;
+ virtual GLuint Handle() = 0;
+ virtual void SetSource(const std::string& src) { m_source = src; }
+ virtual bool LoadSource(const std::string& filename, const std::string& prefix = "");
+ virtual bool AppendSource(const std::string& filename);
+ virtual bool InsertSource(const std::string& filename, const std::string& loc);
+ bool OK() const { return m_compiled; }
+
+ std::string GetName() const { return m_filenames; }
+ std::string GetSourceWithLineNumbers() const;
+
+ protected:
+ std::string m_source;
+ std::string m_lastLog;
+ std::vector<std::string> m_attr;
+ bool m_compiled = false;
+
+ private:
+ std::string m_filenames;
+ };
+
+
+ //////////////////////////////////////////////////////////////////////
+ // CVertexShader - vertex shader class
+ //////////////////////////////////////////////////////////////////////
+ class CVertexShader : public CShader
+ {
+ public:
+ CVertexShader() = default;
+ ~CVertexShader() override { Free(); }
+ void Free() override {}
+ GLuint Handle() override { return m_vertexShader; }
+
+ protected:
+ GLuint m_vertexShader = 0;
+ };
+
+ class CGLSLVertexShader : public CVertexShader
+ {
+ public:
+ void Free() override;
+ bool Compile() override;
+ };
+
+
+ //////////////////////////////////////////////////////////////////////
+ // CPixelShader - abstract pixel shader class
+ //////////////////////////////////////////////////////////////////////
+ class CPixelShader : public CShader
+ {
+ public:
+ CPixelShader() = default;
+ ~CPixelShader() override { Free(); }
+ void Free() override {}
+ GLuint Handle() override { return m_pixelShader; }
+
+ protected:
+ GLuint m_pixelShader = 0;
+ };
+
+ class CGLSLPixelShader : public CPixelShader
+ {
+ public:
+ void Free() override;
+ bool Compile() override;
+ };
+
+ //////////////////////////////////////////////////////////////////////
+ // CShaderProgram - the complete shader consisting of both the vertex
+ // and pixel programs. (abstract)
+ //////////////////////////////////////////////////////////////////////
+ class CShaderProgram
+ {
+ public:
+ CShaderProgram() = default;
+
+ virtual ~CShaderProgram()
+ {
+ delete m_pFP;
+ delete m_pVP;
+ }
+
+ // enable the shader
+ virtual bool Enable() = 0;
+
+ // disable the shader
+ virtual void Disable() = 0;
+
+ // returns true if shader is compiled and linked
+ bool OK() const { return m_ok; }
+
+ // return the vertex shader object
+ CVertexShader* VertexShader() { return m_pVP; }
+
+ // return the pixel shader object
+ CPixelShader* PixelShader() { return m_pFP; }
+
+ // compile and link the shaders
+ virtual bool CompileAndLink() = 0;
+
+ // override to perform custom tasks on successful compilation
+ // and linkage. E.g. obtaining handles to shader attributes.
+ virtual void OnCompiledAndLinked() {}
+
+ // override to perform custom tasks before shader is enabled
+ // and after it is disabled. Return false in OnDisabled() to
+ // disable shader.
+ // E.g. setting attributes, disabling texture unites, etc
+ virtual bool OnEnabled() { return true; }
+ virtual void OnDisabled() { }
+
+ virtual GLuint ProgramHandle() { return m_shaderProgram; }
+
+ protected:
+ CVertexShader* m_pVP = nullptr;
+ CPixelShader* m_pFP = nullptr;
+ GLuint m_shaderProgram = 0;
+ bool m_ok = false;
+ };
+
+
+ class CGLSLShaderProgram : virtual public CShaderProgram
+ {
+ public:
+ CGLSLShaderProgram();
+ CGLSLShaderProgram(const std::string& vert
+ , const std::string& frag);
+ ~CGLSLShaderProgram() override;
+
+ // enable the shader
+ bool Enable() override;
+
+ // disable the shader
+ void Disable() override;
+
+ // compile and link the shaders
+ bool CompileAndLink() override;
+
+ protected:
+ void Free();
+
+ GLint m_lastProgram;
+ bool m_validated = false;
+ };
+
+
+} // close namespace
+
diff --git a/xbmc/guilib/StereoscopicsManager.cpp b/xbmc/guilib/StereoscopicsManager.cpp
new file mode 100644
index 0000000..3aa269b
--- /dev/null
+++ b/xbmc/guilib/StereoscopicsManager.cpp
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+/*!
+ * @file StereoscopicsManager.cpp
+ * @brief This class acts as container for stereoscopic related functions
+ */
+
+#include "StereoscopicsManager.h"
+
+#include "GUIComponent.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/DataCacheCore.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "rendering/RenderSystem.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <stdlib.h>
+
+struct StereoModeMap
+{
+ const char* name;
+ RENDER_STEREO_MODE mode;
+};
+
+static const struct StereoModeMap VideoModeToGuiModeMap[] =
+{
+ { "mono", RENDER_STEREO_MODE_OFF },
+ { "left_right", RENDER_STEREO_MODE_SPLIT_VERTICAL },
+ { "right_left", RENDER_STEREO_MODE_SPLIT_VERTICAL },
+ { "top_bottom", RENDER_STEREO_MODE_SPLIT_HORIZONTAL },
+ { "bottom_top", RENDER_STEREO_MODE_SPLIT_HORIZONTAL },
+ { "checkerboard_rl", RENDER_STEREO_MODE_CHECKERBOARD },
+ { "checkerboard_lr", RENDER_STEREO_MODE_CHECKERBOARD },
+ { "row_interleaved_rl", RENDER_STEREO_MODE_INTERLACED },
+ { "row_interleaved_lr", RENDER_STEREO_MODE_INTERLACED },
+ { "col_interleaved_rl", RENDER_STEREO_MODE_OFF }, // unsupported
+ { "col_interleaved_lr", RENDER_STEREO_MODE_OFF }, // unsupported
+ { "anaglyph_cyan_red", RENDER_STEREO_MODE_ANAGLYPH_RED_CYAN },
+ { "anaglyph_green_magenta", RENDER_STEREO_MODE_ANAGLYPH_GREEN_MAGENTA },
+ { "anaglyph_yellow_blue", RENDER_STEREO_MODE_ANAGLYPH_YELLOW_BLUE },
+ { "block_lr", RENDER_STEREO_MODE_OFF }, // unsupported
+ { "block_rl", RENDER_STEREO_MODE_OFF }, // unsupported
+ {}
+};
+
+static const struct StereoModeMap StringToGuiModeMap[] =
+{
+ { "off", RENDER_STEREO_MODE_OFF },
+ { "split_vertical", RENDER_STEREO_MODE_SPLIT_VERTICAL },
+ { "side_by_side", RENDER_STEREO_MODE_SPLIT_VERTICAL }, // alias
+ { "sbs", RENDER_STEREO_MODE_SPLIT_VERTICAL }, // alias
+ { "split_horizontal", RENDER_STEREO_MODE_SPLIT_HORIZONTAL },
+ { "over_under", RENDER_STEREO_MODE_SPLIT_HORIZONTAL }, // alias
+ { "tab", RENDER_STEREO_MODE_SPLIT_HORIZONTAL }, // alias
+ { "row_interleaved", RENDER_STEREO_MODE_INTERLACED },
+ { "interlaced", RENDER_STEREO_MODE_INTERLACED }, // alias
+ { "checkerboard", RENDER_STEREO_MODE_CHECKERBOARD },
+ { "anaglyph_cyan_red", RENDER_STEREO_MODE_ANAGLYPH_RED_CYAN },
+ { "anaglyph_green_magenta", RENDER_STEREO_MODE_ANAGLYPH_GREEN_MAGENTA },
+ { "anaglyph_yellow_blue", RENDER_STEREO_MODE_ANAGLYPH_YELLOW_BLUE },
+ { "hardware_based", RENDER_STEREO_MODE_HARDWAREBASED },
+ { "monoscopic", RENDER_STEREO_MODE_MONO },
+ {}
+};
+
+CStereoscopicsManager::CStereoscopicsManager()
+ : m_settings(CServiceBroker::GetSettingsComponent()->GetSettings())
+{
+ m_stereoModeSetByUser = RENDER_STEREO_MODE_UNDEFINED;
+ m_lastStereoModeSetByUser = RENDER_STEREO_MODE_UNDEFINED;
+
+ //! @todo Move this to Initialize() to avoid potential problems in ctor
+ std::set<std::string> settingSet{
+ CSettings::SETTING_VIDEOSCREEN_STEREOSCOPICMODE
+ };
+ m_settings->GetSettingsManager()->RegisterCallback(this, settingSet);
+}
+
+CStereoscopicsManager::~CStereoscopicsManager(void)
+{
+ m_settings->GetSettingsManager()->UnregisterCallback(this);
+}
+
+void CStereoscopicsManager::Initialize()
+{
+ // turn off stereo mode on XBMC startup
+ SetStereoMode(RENDER_STEREO_MODE_OFF);
+}
+
+RENDER_STEREO_MODE CStereoscopicsManager::GetStereoMode(void) const
+{
+ return static_cast<RENDER_STEREO_MODE>(m_settings->GetInt(CSettings::SETTING_VIDEOSCREEN_STEREOSCOPICMODE));
+}
+
+void CStereoscopicsManager::SetStereoModeByUser(const RENDER_STEREO_MODE &mode)
+{
+ // only update last user mode if desired mode is different from current
+ if (mode != m_stereoModeSetByUser)
+ m_lastStereoModeSetByUser = m_stereoModeSetByUser;
+
+ m_stereoModeSetByUser = mode;
+ SetStereoMode(mode);
+}
+
+void CStereoscopicsManager::SetStereoMode(const RENDER_STEREO_MODE &mode)
+{
+ RENDER_STEREO_MODE currentMode = GetStereoMode();
+ RENDER_STEREO_MODE applyMode = mode;
+
+ // resolve automatic mode before applying
+ if (mode == RENDER_STEREO_MODE_AUTO)
+ applyMode = GetStereoModeOfPlayingVideo();
+
+ if (applyMode != currentMode && applyMode >= RENDER_STEREO_MODE_OFF)
+ {
+ if (CServiceBroker::GetRenderSystem()->SupportsStereo(applyMode))
+ m_settings->SetInt(CSettings::SETTING_VIDEOSCREEN_STEREOSCOPICMODE, applyMode);
+ }
+}
+
+RENDER_STEREO_MODE CStereoscopicsManager::GetNextSupportedStereoMode(const RENDER_STEREO_MODE &currentMode, int step) const
+{
+ RENDER_STEREO_MODE mode = currentMode;
+
+ do
+ {
+ mode = static_cast<RENDER_STEREO_MODE>((mode + step) % RENDER_STEREO_MODE_COUNT);
+
+ if (CServiceBroker::GetRenderSystem()->SupportsStereo(mode))
+ break;
+ } while (mode != currentMode);
+
+ return mode;
+}
+
+std::string CStereoscopicsManager::DetectStereoModeByString(const std::string &needle) const
+{
+ std::string stereoMode;
+ const std::string& searchString(needle);
+ CRegExp re(true);
+
+ if (!re.RegComp(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_stereoscopicregex_3d.c_str()))
+ {
+ CLog::Log(
+ LOGERROR, "{}: Invalid RegExp for matching 3d content:'{}'", __FUNCTION__,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_stereoscopicregex_3d);
+ return stereoMode;
+ }
+
+ if (re.RegFind(searchString) == -1)
+ return stereoMode; // no match found for 3d content, assume mono mode
+
+ if (!re.RegComp(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_stereoscopicregex_sbs.c_str()))
+ {
+ CLog::Log(
+ LOGERROR, "{}: Invalid RegExp for matching 3d SBS content:'{}'", __FUNCTION__,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_stereoscopicregex_sbs);
+ return stereoMode;
+ }
+
+ if (re.RegFind(searchString) > -1)
+ {
+ stereoMode = "left_right";
+ return stereoMode;
+ }
+
+ if (!re.RegComp(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_stereoscopicregex_tab.c_str()))
+ {
+ CLog::Log(
+ LOGERROR, "{}: Invalid RegExp for matching 3d TAB content:'{}'", __FUNCTION__,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_stereoscopicregex_tab);
+ return stereoMode;
+ }
+
+ if (re.RegFind(searchString) > -1)
+ stereoMode = "top_bottom";
+
+ return stereoMode;
+}
+
+RENDER_STEREO_MODE CStereoscopicsManager::GetStereoModeByUserChoice() const
+{
+ RENDER_STEREO_MODE mode = GetStereoMode();
+
+ // if no stereo mode is set already, suggest mode of current video by preselecting it
+ if (mode == RENDER_STEREO_MODE_OFF)
+ mode = GetStereoModeOfPlayingVideo();
+
+ CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ pDlgSelect->Reset();
+
+ // "Select stereoscopic 3D mode"
+ pDlgSelect->SetHeading(CVariant{g_localizeStrings.Get(36528)});
+
+ // prepare selectable stereo modes
+ std::vector<RENDER_STEREO_MODE> selectableModes;
+ for (int i = RENDER_STEREO_MODE_OFF; i < RENDER_STEREO_MODE_COUNT; i++)
+ {
+ RENDER_STEREO_MODE selectableMode = static_cast<RENDER_STEREO_MODE>(i);
+ if (CServiceBroker::GetRenderSystem()->SupportsStereo(selectableMode))
+ {
+ selectableModes.push_back(selectableMode);
+ std::string label = GetLabelForStereoMode((RENDER_STEREO_MODE) i);
+ pDlgSelect->Add( label );
+ if (mode == selectableMode)
+ pDlgSelect->SetSelected( label );
+ }
+
+ // inject AUTO pseudo mode after OFF
+ if (i == RENDER_STEREO_MODE_OFF)
+ {
+ selectableModes.push_back(RENDER_STEREO_MODE_AUTO);
+ pDlgSelect->Add(GetLabelForStereoMode(RENDER_STEREO_MODE_AUTO));
+ }
+ }
+
+ pDlgSelect->Open();
+
+ int iItem = pDlgSelect->GetSelectedItem();
+ if (iItem > -1 && pDlgSelect->IsConfirmed())
+ mode = selectableModes[iItem];
+ else
+ mode = GetStereoMode();
+
+ return mode;
+}
+
+RENDER_STEREO_MODE CStereoscopicsManager::GetStereoModeOfPlayingVideo(void) const
+{
+ RENDER_STEREO_MODE mode = RENDER_STEREO_MODE_OFF;
+ std::string playerMode = GetVideoStereoMode();
+
+ if (!playerMode.empty())
+ {
+ int convertedMode = ConvertVideoToGuiStereoMode(playerMode);
+ if (convertedMode > -1)
+ mode = static_cast<RENDER_STEREO_MODE>(convertedMode);
+ }
+
+ CLog::Log(LOGDEBUG, "StereoscopicsManager: autodetected stereo mode for movie mode {} is: {}",
+ playerMode, ConvertGuiStereoModeToString(mode));
+ return mode;
+}
+
+std::string CStereoscopicsManager::GetLabelForStereoMode(const RENDER_STEREO_MODE &mode) const
+{
+ int msgId;
+ switch(mode) {
+ case RENDER_STEREO_MODE_AUTO:
+ msgId = 36532;
+ break;
+ case RENDER_STEREO_MODE_ANAGLYPH_YELLOW_BLUE:
+ msgId = 36510;
+ break;
+ case RENDER_STEREO_MODE_INTERLACED:
+ msgId = 36507;
+ break;
+ case RENDER_STEREO_MODE_CHECKERBOARD:
+ msgId = 36511;
+ break;
+ case RENDER_STEREO_MODE_HARDWAREBASED:
+ msgId = 36508;
+ break;
+ case RENDER_STEREO_MODE_MONO:
+ msgId = 36509;
+ break;
+ default:
+ msgId = 36502 + mode;
+ }
+
+ return g_localizeStrings.Get(msgId);
+}
+
+RENDER_STEREO_MODE CStereoscopicsManager::GetPreferredPlaybackMode(void) const
+{
+ return static_cast<RENDER_STEREO_MODE>(m_settings->GetInt(CSettings::SETTING_VIDEOSCREEN_PREFEREDSTEREOSCOPICMODE));
+}
+
+int CStereoscopicsManager::ConvertVideoToGuiStereoMode(const std::string &mode)
+{
+ size_t i = 0;
+ while (VideoModeToGuiModeMap[i].name)
+ {
+ if (mode == VideoModeToGuiModeMap[i].name)
+ return VideoModeToGuiModeMap[i].mode;
+ i++;
+ }
+ return -1;
+}
+
+int CStereoscopicsManager::ConvertStringToGuiStereoMode(const std::string &mode)
+{
+ size_t i = 0;
+ while (StringToGuiModeMap[i].name)
+ {
+ if (mode == StringToGuiModeMap[i].name)
+ return StringToGuiModeMap[i].mode;
+ i++;
+ }
+ return ConvertVideoToGuiStereoMode(mode);
+}
+
+const char* CStereoscopicsManager::ConvertGuiStereoModeToString(const RENDER_STEREO_MODE &mode)
+{
+ size_t i = 0;
+ while (StringToGuiModeMap[i].name)
+ {
+ if (StringToGuiModeMap[i].mode == mode)
+ return StringToGuiModeMap[i].name;
+ i++;
+ }
+ return "";
+}
+
+std::string CStereoscopicsManager::NormalizeStereoMode(const std::string &mode)
+{
+ if (!mode.empty() && mode != "mono")
+ {
+ int guiMode = ConvertStringToGuiStereoMode(mode);
+
+ if (guiMode > -1)
+ return ConvertGuiStereoModeToString((RENDER_STEREO_MODE) guiMode);
+ else
+ return mode;
+ }
+
+ return "mono";
+}
+
+CAction CStereoscopicsManager::ConvertActionCommandToAction(const std::string &command, const std::string &parameter)
+{
+ std::string cmd = command;
+ std::string para = parameter;
+ StringUtils::ToLower(cmd);
+ StringUtils::ToLower(para);
+ if (cmd == "setstereomode")
+ {
+ int actionId = -1;
+ if (para == "next")
+ actionId = ACTION_STEREOMODE_NEXT;
+ else if (para == "previous")
+ actionId = ACTION_STEREOMODE_PREVIOUS;
+ else if (para == "toggle")
+ actionId = ACTION_STEREOMODE_TOGGLE;
+ else if (para == "select")
+ actionId = ACTION_STEREOMODE_SELECT;
+ else if (para == "tomono")
+ actionId = ACTION_STEREOMODE_TOMONO;
+
+ // already have a valid actionID return it
+ if (actionId > -1)
+ return CAction(actionId);
+
+ // still no valid action ID, check if parameter is a supported stereomode
+ if (ConvertStringToGuiStereoMode(para) > -1)
+ return CAction(ACTION_STEREOMODE_SET, para);
+ }
+ return CAction(ACTION_NONE);
+}
+
+void CStereoscopicsManager::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == CSettings::SETTING_VIDEOSCREEN_STEREOSCOPICMODE)
+ {
+ RENDER_STEREO_MODE mode = GetStereoMode();
+ CLog::Log(LOGDEBUG, "StereoscopicsManager: stereo mode setting changed to {}",
+ ConvertGuiStereoModeToString(mode));
+ ApplyStereoMode(mode);
+ }
+}
+
+bool CStereoscopicsManager::OnMessage(CGUIMessage &message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_PLAYBACK_STOPPED:
+ case GUI_MSG_PLAYLISTPLAYER_STOPPED:
+ OnPlaybackStopped();
+ break;
+ }
+
+ return false;
+}
+
+bool CStereoscopicsManager::OnAction(const CAction &action)
+{
+ RENDER_STEREO_MODE mode = GetStereoMode();
+
+ if (action.GetID() == ACTION_STEREOMODE_NEXT)
+ {
+ SetStereoModeByUser(GetNextSupportedStereoMode(mode));
+ return true;
+ }
+ else if (action.GetID() == ACTION_STEREOMODE_PREVIOUS)
+ {
+ SetStereoModeByUser(GetNextSupportedStereoMode(mode, RENDER_STEREO_MODE_COUNT - 1));
+ return true;
+ }
+ else if (action.GetID() == ACTION_STEREOMODE_TOGGLE)
+ {
+ if (mode == RENDER_STEREO_MODE_OFF)
+ {
+ RENDER_STEREO_MODE targetMode = GetPreferredPlaybackMode();
+
+ // if user selected a specific mode before, make sure to
+ // switch back into that mode on toggle.
+ if (m_stereoModeSetByUser != RENDER_STEREO_MODE_UNDEFINED)
+ {
+ // if user mode is set to OFF, he manually turned it off before. In this case use the last user applied mode
+ if (m_stereoModeSetByUser != RENDER_STEREO_MODE_OFF)
+ targetMode = m_stereoModeSetByUser;
+ else if (m_lastStereoModeSetByUser != RENDER_STEREO_MODE_UNDEFINED && m_lastStereoModeSetByUser != RENDER_STEREO_MODE_OFF)
+ targetMode = m_lastStereoModeSetByUser;
+ }
+
+ SetStereoModeByUser(targetMode);
+ }
+ else
+ {
+ SetStereoModeByUser(RENDER_STEREO_MODE_OFF);
+ }
+ return true;
+ }
+ else if (action.GetID() == ACTION_STEREOMODE_SELECT)
+ {
+ SetStereoModeByUser(GetStereoModeByUserChoice());
+ return true;
+ }
+ else if (action.GetID() == ACTION_STEREOMODE_TOMONO)
+ {
+ if (mode == RENDER_STEREO_MODE_MONO)
+ {
+ RENDER_STEREO_MODE targetMode = GetPreferredPlaybackMode();
+
+ // if we have an old userdefined stereomode, use that one as toggle target
+ if (m_stereoModeSetByUser != RENDER_STEREO_MODE_UNDEFINED)
+ {
+ // if user mode is set to OFF, he manually turned it off before. In this case use the last user applied mode
+ if (m_stereoModeSetByUser != RENDER_STEREO_MODE_OFF && m_stereoModeSetByUser != mode)
+ targetMode = m_stereoModeSetByUser;
+ else if (m_lastStereoModeSetByUser != RENDER_STEREO_MODE_UNDEFINED && m_lastStereoModeSetByUser != RENDER_STEREO_MODE_OFF && m_lastStereoModeSetByUser != mode)
+ targetMode = m_lastStereoModeSetByUser;
+ }
+
+ SetStereoModeByUser(targetMode);
+ }
+ else
+ {
+ SetStereoModeByUser(RENDER_STEREO_MODE_MONO);
+ }
+ return true;
+ }
+ else if (action.GetID() == ACTION_STEREOMODE_SET)
+ {
+ int stereoMode = ConvertStringToGuiStereoMode(action.GetName());
+ if (stereoMode > -1)
+ SetStereoModeByUser(static_cast<RENDER_STEREO_MODE>(stereoMode));
+ return true;
+ }
+
+ return false;
+}
+
+void CStereoscopicsManager::ApplyStereoMode(const RENDER_STEREO_MODE &mode, bool notify)
+{
+ RENDER_STEREO_MODE currentMode = CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode();
+ CLog::Log(LOGDEBUG,
+ "StereoscopicsManager::ApplyStereoMode: trying to apply stereo mode. Current: {} | "
+ "Target: {}",
+ ConvertGuiStereoModeToString(currentMode), ConvertGuiStereoModeToString(mode));
+ if (currentMode != mode)
+ {
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetStereoMode(mode);
+ CLog::Log(LOGDEBUG, "StereoscopicsManager: stereo mode changed to {}",
+ ConvertGuiStereoModeToString(mode));
+ if (notify)
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36501), GetLabelForStereoMode(mode));
+ }
+}
+
+std::string CStereoscopicsManager::GetVideoStereoMode() const
+{
+ std::string playerMode;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ playerMode = CServiceBroker::GetDataCacheCore().GetVideoStereoMode();
+
+ return playerMode;
+}
+
+bool CStereoscopicsManager::IsVideoStereoscopic() const
+{
+ std::string mode = GetVideoStereoMode();
+ return !mode.empty() && mode != "mono";
+}
+
+void CStereoscopicsManager::OnStreamChange()
+{
+ STEREOSCOPIC_PLAYBACK_MODE playbackMode = static_cast<STEREOSCOPIC_PLAYBACK_MODE>(m_settings->GetInt(CSettings::SETTING_VIDEOPLAYER_STEREOSCOPICPLAYBACKMODE));
+ RENDER_STEREO_MODE mode = GetStereoMode();
+
+ // early return if playback mode should be ignored and we're in no stereoscopic mode right now
+ if (playbackMode == STEREOSCOPIC_PLAYBACK_MODE_IGNORE && mode == RENDER_STEREO_MODE_OFF)
+ return;
+
+ if (!CStereoscopicsManager::IsVideoStereoscopic())
+ {
+ // exit stereo mode if started item is not stereoscopic
+ // and if user prefers to stop 3D playback when movie is finished
+ if (mode != RENDER_STEREO_MODE_OFF && m_settings->GetBool(CSettings::SETTING_VIDEOPLAYER_QUITSTEREOMODEONSTOP))
+ SetStereoMode(RENDER_STEREO_MODE_OFF);
+ return;
+ }
+
+ // if we're not in stereomode yet, restore previously selected stereo mode in case it was user selected
+ if (m_stereoModeSetByUser != RENDER_STEREO_MODE_UNDEFINED)
+ {
+ SetStereoMode(m_stereoModeSetByUser);
+ return;
+ }
+
+ RENDER_STEREO_MODE preferred = GetPreferredPlaybackMode();
+ RENDER_STEREO_MODE playing = GetStereoModeOfPlayingVideo();
+
+ if (mode != RENDER_STEREO_MODE_OFF)
+ {
+ // don't change mode if user selected to not exit stereomode on playback stop
+ // users selecting this option usually have to manually switch their TV into 3D mode
+ // and would be annoyed by having to switch TV modes when next movies comes up
+ // @todo probably add a new setting for just this behavior
+ if (m_settings->GetBool(CSettings::SETTING_VIDEOPLAYER_QUITSTEREOMODEONSTOP) == false)
+ return;
+
+ // only change to new stereo mode if not yet in preferred stereo mode
+ if (mode == preferred || (preferred == RENDER_STEREO_MODE_AUTO && mode == playing))
+ return;
+ }
+
+ switch (playbackMode)
+ {
+ case STEREOSCOPIC_PLAYBACK_MODE_ASK: // Ask
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+
+ CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ pDlgSelect->Reset();
+ pDlgSelect->SetHeading(CVariant{g_localizeStrings.Get(36527)});
+
+ int idx_playing = -1;
+
+ // add choices
+ int idx_preferred = pDlgSelect->Add(g_localizeStrings.Get(36524) // preferred
+ + " ("
+ + GetLabelForStereoMode(preferred)
+ + ")");
+
+ int idx_mono = pDlgSelect->Add(GetLabelForStereoMode(RENDER_STEREO_MODE_MONO)); // mono / 2d
+
+ if (playing != RENDER_STEREO_MODE_OFF && playing != preferred && preferred != RENDER_STEREO_MODE_AUTO && CServiceBroker::GetRenderSystem()->SupportsStereo(playing)) // same as movie
+ idx_playing = pDlgSelect->Add(g_localizeStrings.Get(36532)
+ + " ("
+ + GetLabelForStereoMode(playing)
+ + ")");
+
+ int idx_select = pDlgSelect->Add( g_localizeStrings.Get(36531) ); // other / select
+
+ pDlgSelect->Open();
+
+ if (pDlgSelect->IsConfirmed())
+ {
+ int iItem = pDlgSelect->GetSelectedItem();
+ if (iItem == idx_preferred) mode = preferred;
+ else if (iItem == idx_mono) mode = RENDER_STEREO_MODE_MONO;
+ else if (iItem == idx_playing) mode = RENDER_STEREO_MODE_AUTO;
+ else if (iItem == idx_select) mode = GetStereoModeByUserChoice();
+
+ SetStereoModeByUser(mode);
+ }
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_UNPAUSE);
+ }
+ break;
+ case STEREOSCOPIC_PLAYBACK_MODE_PREFERRED: // Stereoscopic
+ SetStereoMode(preferred);
+ break;
+ case 2: // Mono
+ SetStereoMode(RENDER_STEREO_MODE_MONO);
+ break;
+ default:
+ break;
+ }
+}
+
+void CStereoscopicsManager::OnPlaybackStopped(void)
+{
+ RENDER_STEREO_MODE mode = GetStereoMode();
+
+ if (m_settings->GetBool(CSettings::SETTING_VIDEOPLAYER_QUITSTEREOMODEONSTOP) && mode != RENDER_STEREO_MODE_OFF)
+ SetStereoMode(RENDER_STEREO_MODE_OFF);
+
+ // reset user modes on playback end to start over new on next playback and not end up in a probably unwanted mode
+ if (m_stereoModeSetByUser != RENDER_STEREO_MODE_OFF)
+ m_lastStereoModeSetByUser = m_stereoModeSetByUser;
+
+ m_stereoModeSetByUser = RENDER_STEREO_MODE_UNDEFINED;
+}
diff --git a/xbmc/guilib/StereoscopicsManager.h b/xbmc/guilib/StereoscopicsManager.h
new file mode 100644
index 0000000..8de93dc
--- /dev/null
+++ b/xbmc/guilib/StereoscopicsManager.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+/*!
+ * @file StereoscopicsManager.cpp
+ * @brief This class acts as container for stereoscopic related functions
+ */
+
+#include "guilib/IMsgTargetCallback.h"
+#include "rendering/RenderSystemTypes.h"
+#include "settings/lib/ISettingCallback.h"
+
+#include <stdlib.h>
+
+class CAction;
+class CDataCacheCore;
+class CGUIWindowManager;
+class CSettings;
+
+enum STEREOSCOPIC_PLAYBACK_MODE
+{
+ STEREOSCOPIC_PLAYBACK_MODE_ASK,
+ STEREOSCOPIC_PLAYBACK_MODE_PREFERRED,
+ STEREOSCOPIC_PLAYBACK_MODE_MONO,
+
+ STEREOSCOPIC_PLAYBACK_MODE_IGNORE = 100,
+};
+
+class CStereoscopicsManager : public ISettingCallback,
+ public IMsgTargetCallback
+{
+public:
+ CStereoscopicsManager();
+
+ ~CStereoscopicsManager(void) override;
+
+ void Initialize();
+
+ RENDER_STEREO_MODE GetStereoMode(void) const;
+ std::string DetectStereoModeByString(const std::string &needle) const;
+ std::string GetLabelForStereoMode(const RENDER_STEREO_MODE &mode) const;
+
+ void SetStereoMode(const RENDER_STEREO_MODE &mode);
+
+ static const char* ConvertGuiStereoModeToString(const RENDER_STEREO_MODE &mode);
+ /**
+ * @brief Converts a stereoscopics related action/command from Builtins and JsonRPC into the according cAction ID.
+ * @param command The command/action
+ * @param parameter The parameter of the command
+ * @return The integer of the according cAction or -1 if not valid
+ */
+ static CAction ConvertActionCommandToAction(const std::string &command, const std::string &parameter);
+ static std::string NormalizeStereoMode(const std::string &mode);
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnStreamChange();
+ bool OnMessage(CGUIMessage &message) override;
+ /*!
+ * @brief Handle 3D specific cActions
+ * @param action The action to process
+ * @return True if action could be handled, false otherwise.
+ */
+ bool OnAction(const CAction &action);
+
+private:
+ RENDER_STEREO_MODE GetNextSupportedStereoMode(const RENDER_STEREO_MODE &currentMode, int step = 1) const;
+ RENDER_STEREO_MODE GetStereoModeByUserChoice() const;
+ RENDER_STEREO_MODE GetStereoModeOfPlayingVideo(void) const;
+ RENDER_STEREO_MODE GetPreferredPlaybackMode(void) const;
+ std::string GetVideoStereoMode() const;
+ bool IsVideoStereoscopic() const;
+
+ void SetStereoModeByUser(const RENDER_STEREO_MODE &mode);
+
+ void ApplyStereoMode(const RENDER_STEREO_MODE &mode, bool notify = true);
+ void OnPlaybackStopped(void);
+
+ /**
+ * @brief will convert a string representation into a GUI stereo mode
+ * @param mode The string to convert
+ * @return -1 if not found, otherwise the according int of the RENDER_STEREO_MODE enum
+ */
+ static int ConvertStringToGuiStereoMode(const std::string &mode);
+ static int ConvertVideoToGuiStereoMode(const std::string &mode);
+
+ // Construction parameters
+ std::shared_ptr<CSettings> m_settings;
+
+ // Stereoscopic parameters
+ RENDER_STEREO_MODE m_stereoModeSetByUser;
+ RENDER_STEREO_MODE m_lastStereoModeSetByUser;
+};
diff --git a/xbmc/guilib/Texture.cpp b/xbmc/guilib/Texture.cpp
new file mode 100644
index 0000000..4a2f80e
--- /dev/null
+++ b/xbmc/guilib/Texture.cpp
@@ -0,0 +1,495 @@
+/*
+ * 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 "Texture.h"
+
+#include "DDSImage.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "commons/ilog.h"
+#include "filesystem/File.h"
+#include "filesystem/ResourceFile.h"
+#include "filesystem/XbtFile.h"
+#include "guilib/iimage.h"
+#include "guilib/imagefactory.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#if defined(TARGET_DARWIN_EMBEDDED)
+#include <ImageIO/ImageIO.h>
+#include "filesystem/File.h"
+#endif
+#if defined(TARGET_ANDROID)
+#include "platform/android/filesystem/AndroidAppFile.h"
+#endif
+#include "rendering/RenderSystem.h"
+#include "utils/MemUtils.h"
+
+#include <algorithm>
+#include <cstring>
+#include <exception>
+#include <utility>
+
+/************************************************************************/
+/* */
+/************************************************************************/
+CTexture::CTexture(unsigned int width, unsigned int height, unsigned int format)
+{
+ m_pixels = NULL;
+ m_loadedToGPU = false;
+ Allocate(width, height, format);
+}
+
+CTexture::~CTexture()
+{
+ KODI::MEMORY::AlignedFree(m_pixels);
+ m_pixels = NULL;
+}
+
+void CTexture::Allocate(unsigned int width, unsigned int height, unsigned int format)
+{
+ m_imageWidth = m_originalWidth = width;
+ m_imageHeight = m_originalHeight = height;
+ m_format = format;
+ m_orientation = 0;
+
+ m_textureWidth = m_imageWidth;
+ m_textureHeight = m_imageHeight;
+
+ if (m_format & XB_FMT_DXT_MASK)
+ {
+ while (GetPitch() < CServiceBroker::GetRenderSystem()->GetMinDXTPitch())
+ m_textureWidth += GetBlockSize();
+ }
+
+ if (!CServiceBroker::GetRenderSystem()->SupportsNPOT((m_format & XB_FMT_DXT_MASK) != 0))
+ {
+ m_textureWidth = PadPow2(m_textureWidth);
+ m_textureHeight = PadPow2(m_textureHeight);
+ }
+
+ if (m_format & XB_FMT_DXT_MASK)
+ {
+ // DXT textures must be a multiple of 4 in width and height
+ m_textureWidth = ((m_textureWidth + 3) / 4) * 4;
+ m_textureHeight = ((m_textureHeight + 3) / 4) * 4;
+ }
+ else
+ {
+ // align all textures so that they have an even width
+ // in some circumstances when we downsize a thumbnail
+ // which has an uneven number of pixels in width
+ // we crash in CPicture::ScaleImage in ffmpegs swscale
+ // because it tries to access beyond the source memory
+ // (happens on osx and ios)
+ // UPDATE: don't just update to be on an even width;
+ // ffmpegs swscale relies on a 16-byte stride on some systems
+ // so the textureWidth needs to be a multiple of 16. see ffmpeg
+ // swscale headers for more info.
+ m_textureWidth = ((m_textureWidth + 15) / 16) * 16;
+ }
+
+ // check for max texture size
+ #define CLAMP(x, y) { if (x > y) x = y; }
+ CLAMP(m_textureWidth, CServiceBroker::GetRenderSystem()->GetMaxTextureSize());
+ CLAMP(m_textureHeight, CServiceBroker::GetRenderSystem()->GetMaxTextureSize());
+ CLAMP(m_imageWidth, m_textureWidth);
+ CLAMP(m_imageHeight, m_textureHeight);
+
+ KODI::MEMORY::AlignedFree(m_pixels);
+ m_pixels = NULL;
+ if (GetPitch() * GetRows() > 0)
+ {
+ size_t size = GetPitch() * GetRows();
+ m_pixels = static_cast<unsigned char*>(KODI::MEMORY::AlignedMalloc(size, 32));
+
+ if (m_pixels == nullptr)
+ {
+ CLog::Log(LOGERROR, "{} - Could not allocate {} bytes. Out of memory.", __FUNCTION__, size);
+ }
+ }
+}
+
+void CTexture::Update(unsigned int width,
+ unsigned int height,
+ unsigned int pitch,
+ unsigned int format,
+ const unsigned char* pixels,
+ bool loadToGPU)
+{
+ if (pixels == NULL)
+ return;
+
+ if (format & XB_FMT_DXT_MASK)
+ return;
+
+ Allocate(width, height, format);
+
+ if (m_pixels == nullptr)
+ return;
+
+ unsigned int srcPitch = pitch ? pitch : GetPitch(width);
+ unsigned int srcRows = GetRows(height);
+ unsigned int dstPitch = GetPitch(m_textureWidth);
+ unsigned int dstRows = GetRows(m_textureHeight);
+
+ if (srcPitch == dstPitch)
+ memcpy(m_pixels, pixels, srcPitch * std::min(srcRows, dstRows));
+ else
+ {
+ const unsigned char *src = pixels;
+ unsigned char* dst = m_pixels;
+ for (unsigned int y = 0; y < srcRows && y < dstRows; y++)
+ {
+ memcpy(dst, src, std::min(srcPitch, dstPitch));
+ src += srcPitch;
+ dst += dstPitch;
+ }
+ }
+ ClampToEdge();
+
+ if (loadToGPU)
+ LoadToGPU();
+}
+
+void CTexture::ClampToEdge()
+{
+ if (m_pixels == nullptr)
+ return;
+
+ unsigned int imagePitch = GetPitch(m_imageWidth);
+ unsigned int imageRows = GetRows(m_imageHeight);
+ unsigned int texturePitch = GetPitch(m_textureWidth);
+ unsigned int textureRows = GetRows(m_textureHeight);
+ if (imagePitch < texturePitch)
+ {
+ unsigned int blockSize = GetBlockSize();
+ unsigned char *src = m_pixels + imagePitch - blockSize;
+ unsigned char *dst = m_pixels;
+ for (unsigned int y = 0; y < imageRows; y++)
+ {
+ for (unsigned int x = imagePitch; x < texturePitch; x += blockSize)
+ memcpy(dst + x, src, blockSize);
+ dst += texturePitch;
+ }
+ }
+
+ if (imageRows < textureRows)
+ {
+ unsigned char *dst = m_pixels + imageRows * texturePitch;
+ for (unsigned int y = imageRows; y < textureRows; y++)
+ {
+ memcpy(dst, dst - texturePitch, texturePitch);
+ dst += texturePitch;
+ }
+ }
+}
+
+std::unique_ptr<CTexture> CTexture::LoadFromFile(const std::string& texturePath,
+ unsigned int idealWidth,
+ unsigned int idealHeight,
+ bool requirePixels,
+ const std::string& strMimeType)
+{
+#if defined(TARGET_ANDROID)
+ CURL url(texturePath);
+ if (url.IsProtocol("androidapp"))
+ {
+ XFILE::CFileAndroidApp file;
+ if (file.Open(url))
+ {
+ unsigned char* inputBuff;
+ unsigned int width;
+ unsigned int height;
+ unsigned int inputBuffSize = file.ReadIcon(&inputBuff, &width, &height);
+ file.Close();
+ if (!inputBuffSize)
+ return NULL;
+
+ std::unique_ptr<CTexture> texture = CTexture::CreateTexture();
+ texture->LoadFromMemory(width, height, width*4, XB_FMT_RGBA8, true, inputBuff);
+ delete[] inputBuff;
+ return texture;
+ }
+ }
+#endif
+ std::unique_ptr<CTexture> texture = CTexture::CreateTexture();
+ if (texture->LoadFromFileInternal(texturePath, idealWidth, idealHeight, requirePixels, strMimeType))
+ return texture;
+ return {};
+}
+
+std::unique_ptr<CTexture> CTexture::LoadFromFileInMemory(unsigned char* buffer,
+ size_t bufferSize,
+ const std::string& mimeType,
+ unsigned int idealWidth,
+ unsigned int idealHeight)
+{
+ std::unique_ptr<CTexture> texture = CTexture::CreateTexture();
+ if (texture->LoadFromFileInMem(buffer, bufferSize, mimeType, idealWidth, idealHeight))
+ return texture;
+ return {};
+}
+
+bool CTexture::LoadFromFileInternal(const std::string& texturePath,
+ unsigned int maxWidth,
+ unsigned int maxHeight,
+ bool requirePixels,
+ const std::string& strMimeType)
+{
+ if (URIUtils::HasExtension(texturePath, ".dds"))
+ { // special case for DDS images
+ CDDSImage image;
+ if (image.ReadFile(texturePath))
+ {
+ Update(image.GetWidth(), image.GetHeight(), 0, image.GetFormat(), image.GetData(), false);
+ return true;
+ }
+ return false;
+ }
+
+ unsigned int width = maxWidth ? std::min(maxWidth, CServiceBroker::GetRenderSystem()->GetMaxTextureSize()) :
+ CServiceBroker::GetRenderSystem()->GetMaxTextureSize();
+ unsigned int height = maxHeight ? std::min(maxHeight, CServiceBroker::GetRenderSystem()->GetMaxTextureSize()) :
+ CServiceBroker::GetRenderSystem()->GetMaxTextureSize();
+
+ // Read image into memory to use our vfs
+ XFILE::CFile file;
+ std::vector<uint8_t> buf;
+
+ if (file.LoadFile(texturePath, buf) <= 0)
+ return false;
+
+ CURL url(texturePath);
+ // make sure resource:// paths are properly resolved
+ if (url.IsProtocol("resource"))
+ {
+ std::string translatedPath;
+ if (XFILE::CResourceFile::TranslatePath(url, translatedPath))
+ url.Parse(translatedPath);
+ }
+
+ // handle xbt:// paths differently because it allows loading the texture directly from memory
+ if (url.IsProtocol("xbt"))
+ {
+ XFILE::CXbtFile xbtFile;
+ if (!xbtFile.Open(url))
+ return false;
+
+ return LoadFromMemory(xbtFile.GetImageWidth(), xbtFile.GetImageHeight(), 0,
+ xbtFile.GetImageFormat(), xbtFile.HasImageAlpha(), buf.data());
+ }
+
+ IImage* pImage;
+
+ if(strMimeType.empty())
+ pImage = ImageFactory::CreateLoader(texturePath);
+ else
+ pImage = ImageFactory::CreateLoaderFromMimeType(strMimeType);
+
+ if (!LoadIImage(pImage, buf.data(), buf.size(), width, height))
+ {
+ CLog::Log(LOGDEBUG, "{} - Load of {} failed.", __FUNCTION__, CURL::GetRedacted(texturePath));
+ delete pImage;
+ return false;
+ }
+ delete pImage;
+
+ return true;
+}
+
+bool CTexture::LoadFromFileInMem(unsigned char* buffer,
+ size_t size,
+ const std::string& mimeType,
+ unsigned int maxWidth,
+ unsigned int maxHeight)
+{
+ if (!buffer || !size)
+ return false;
+
+ unsigned int width = maxWidth ? std::min(maxWidth, CServiceBroker::GetRenderSystem()->GetMaxTextureSize()) :
+ CServiceBroker::GetRenderSystem()->GetMaxTextureSize();
+ unsigned int height = maxHeight ? std::min(maxHeight, CServiceBroker::GetRenderSystem()->GetMaxTextureSize()) :
+ CServiceBroker::GetRenderSystem()->GetMaxTextureSize();
+
+ IImage* pImage = ImageFactory::CreateLoaderFromMimeType(mimeType);
+ if(!LoadIImage(pImage, buffer, size, width, height))
+ {
+ delete pImage;
+ return false;
+ }
+ delete pImage;
+ return true;
+}
+
+bool CTexture::LoadIImage(IImage* pImage,
+ unsigned char* buffer,
+ unsigned int bufSize,
+ unsigned int width,
+ unsigned int height)
+{
+ if(pImage != NULL && pImage->LoadImageFromMemory(buffer, bufSize, width, height))
+ {
+ if (pImage->Width() > 0 && pImage->Height() > 0)
+ {
+ Allocate(pImage->Width(), pImage->Height(), XB_FMT_A8R8G8B8);
+ if (m_pixels != nullptr && pImage->Decode(m_pixels, GetTextureWidth(), GetRows(), GetPitch(), XB_FMT_A8R8G8B8))
+ {
+ if (pImage->Orientation())
+ m_orientation = pImage->Orientation() - 1;
+ m_hasAlpha = pImage->hasAlpha();
+ m_originalWidth = pImage->originalWidth();
+ m_originalHeight = pImage->originalHeight();
+ m_imageWidth = pImage->Width();
+ m_imageHeight = pImage->Height();
+ ClampToEdge();
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+bool CTexture::LoadFromMemory(unsigned int width,
+ unsigned int height,
+ unsigned int pitch,
+ unsigned int format,
+ bool hasAlpha,
+ const unsigned char* pixels)
+{
+ m_imageWidth = m_originalWidth = width;
+ m_imageHeight = m_originalHeight = height;
+ m_format = format;
+ m_hasAlpha = hasAlpha;
+ Update(width, height, pitch, format, pixels, false);
+ return true;
+}
+
+bool CTexture::LoadPaletted(unsigned int width,
+ unsigned int height,
+ unsigned int pitch,
+ unsigned int format,
+ const unsigned char* pixels,
+ const COLOR* palette)
+{
+ if (pixels == NULL || palette == NULL)
+ return false;
+
+ Allocate(width, height, format);
+
+ for (unsigned int y = 0; y < m_imageHeight; y++)
+ {
+ unsigned char *dest = m_pixels + y * GetPitch();
+ const unsigned char *src = pixels + y * pitch;
+ for (unsigned int x = 0; x < m_imageWidth; x++)
+ {
+ COLOR col = palette[*src++];
+ *dest++ = col.b;
+ *dest++ = col.g;
+ *dest++ = col.r;
+ *dest++ = col.x;
+ }
+ }
+ ClampToEdge();
+ return true;
+}
+
+unsigned int CTexture::PadPow2(unsigned int x)
+{
+ --x;
+ x |= x >> 1;
+ x |= x >> 2;
+ x |= x >> 4;
+ x |= x >> 8;
+ x |= x >> 16;
+ return ++x;
+}
+
+bool CTexture::SwapBlueRed(unsigned char* pixels,
+ unsigned int height,
+ unsigned int pitch,
+ unsigned int elements,
+ unsigned int offset)
+{
+ if (!pixels) return false;
+ unsigned char *dst = pixels;
+ for (unsigned int y = 0; y < height; y++)
+ {
+ dst = pixels + (y * pitch);
+ for (unsigned int x = 0; x < pitch; x+=elements)
+ std::swap(dst[x+offset], dst[x+2+offset]);
+ }
+ return true;
+}
+
+unsigned int CTexture::GetPitch(unsigned int width) const
+{
+ switch (m_format)
+ {
+ case XB_FMT_DXT1:
+ return ((width + 3) / 4) * 8;
+ case XB_FMT_DXT3:
+ case XB_FMT_DXT5:
+ case XB_FMT_DXT5_YCoCg:
+ return ((width + 3) / 4) * 16;
+ case XB_FMT_A8:
+ return width;
+ case XB_FMT_RGB8:
+ return (((width + 1)* 3 / 4) * 4);
+ case XB_FMT_RGBA8:
+ case XB_FMT_A8R8G8B8:
+ default:
+ return width*4;
+ }
+}
+
+unsigned int CTexture::GetRows(unsigned int height) const
+{
+ switch (m_format)
+ {
+ case XB_FMT_DXT1:
+ return (height + 3) / 4;
+ case XB_FMT_DXT3:
+ case XB_FMT_DXT5:
+ case XB_FMT_DXT5_YCoCg:
+ return (height + 3) / 4;
+ default:
+ return height;
+ }
+}
+
+unsigned int CTexture::GetBlockSize() const
+{
+ switch (m_format)
+ {
+ case XB_FMT_DXT1:
+ return 8;
+ case XB_FMT_DXT3:
+ case XB_FMT_DXT5:
+ case XB_FMT_DXT5_YCoCg:
+ return 16;
+ case XB_FMT_A8:
+ return 1;
+ default:
+ return 4;
+ }
+}
+
+bool CTexture::HasAlpha() const
+{
+ return m_hasAlpha;
+}
+
+void CTexture::SetMipmapping()
+{
+ m_mipmapping = true;
+}
+
+bool CTexture::IsMipmapped() const
+{
+ return m_mipmapping;
+}
diff --git a/xbmc/guilib/Texture.h b/xbmc/guilib/Texture.h
new file mode 100644
index 0000000..343e996
--- /dev/null
+++ b/xbmc/guilib/Texture.h
@@ -0,0 +1,144 @@
+/*
+ * 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 "guilib/TextureFormats.h"
+
+#include <cstddef>
+#include <memory>
+#include <string>
+
+class IImage;
+
+
+#pragma pack(1)
+struct COLOR {unsigned char b,g,r,x;}; // Windows GDI expects 4bytes per color
+#pragma pack()
+
+enum class TEXTURE_SCALING
+{
+ LINEAR,
+ NEAREST,
+};
+
+/*!
+\ingroup textures
+\brief Base texture class, subclasses of which depend on the render spec (DX, GL etc.)
+*/
+class CTexture
+{
+
+public:
+ CTexture(unsigned int width = 0, unsigned int height = 0, unsigned int format = XB_FMT_A8R8G8B8);
+ virtual ~CTexture();
+
+ static std::unique_ptr<CTexture> CreateTexture(unsigned int width = 0,
+ unsigned int height = 0,
+ unsigned int format = XB_FMT_A8R8G8B8);
+
+ /*! \brief Load a texture from a file
+ Loads a texture from a file, restricting in size if needed based on maxHeight and maxWidth.
+ Note that these are the ideal size to load at - the returned texture may be smaller or larger than these.
+ \param texturePath the path of the texture to load.
+ \param idealWidth the ideal width of the texture (defaults to 0, no ideal width).
+ \param idealHeight the ideal height of the texture (defaults to 0, no ideal height).
+ \param strMimeType mimetype of the given texture if available (defaults to empty)
+ \return a CTexture std::unique_ptr to the created texture - nullptr if the texture failed to load.
+ */
+ static std::unique_ptr<CTexture> LoadFromFile(const std::string& texturePath,
+ unsigned int idealWidth = 0,
+ unsigned int idealHeight = 0,
+ bool requirePixels = false,
+ const std::string& strMimeType = "");
+
+ /*! \brief Load a texture from a file in memory
+ Loads a texture from a file in memory, restricting in size if needed based on maxHeight and maxWidth.
+ Note that these are the ideal size to load at - the returned texture may be smaller or larger than these.
+ \param buffer the memory buffer holding the file.
+ \param bufferSize the size of buffer.
+ \param mimeType the mime type of the file in buffer.
+ \param idealWidth the ideal width of the texture (defaults to 0, no ideal width).
+ \param idealHeight the ideal height of the texture (defaults to 0, no ideal height).
+ \return a CTexture std::unique_ptr to the created texture - nullptr if the texture failed to load.
+ */
+ static std::unique_ptr<CTexture> LoadFromFileInMemory(unsigned char* buffer,
+ size_t bufferSize,
+ const std::string& mimeType,
+ unsigned int idealWidth = 0,
+ unsigned int idealHeight = 0);
+
+ bool LoadFromMemory(unsigned int width, unsigned int height, unsigned int pitch, unsigned int format, bool hasAlpha, const unsigned char* pixels);
+ bool LoadPaletted(unsigned int width, unsigned int height, unsigned int pitch, unsigned int format, const unsigned char *pixels, const COLOR *palette);
+
+ bool HasAlpha() const;
+
+ void SetMipmapping();
+ bool IsMipmapped() const;
+ void SetScalingMethod(TEXTURE_SCALING scalingMethod) { m_scalingMethod = scalingMethod; }
+ TEXTURE_SCALING GetScalingMethod() const { return m_scalingMethod; }
+ void SetCacheMemory(bool bCacheMemory) { m_bCacheMemory = bCacheMemory; }
+ bool GetCacheMemory() const { return m_bCacheMemory; }
+
+ virtual void CreateTextureObject() = 0;
+ virtual void DestroyTextureObject() = 0;
+ virtual void LoadToGPU() = 0;
+ virtual void BindToUnit(unsigned int unit) = 0;
+
+ unsigned char* GetPixels() const { return m_pixels; }
+ unsigned int GetPitch() const { return GetPitch(m_textureWidth); }
+ unsigned int GetRows() const { return GetRows(m_textureHeight); }
+ unsigned int GetTextureWidth() const { return m_textureWidth; }
+ unsigned int GetTextureHeight() const { return m_textureHeight; }
+ unsigned int GetWidth() const { return m_imageWidth; }
+ unsigned int GetHeight() const { return m_imageHeight; }
+ /*! \brief return the original width of the image, before scaling/cropping */
+ unsigned int GetOriginalWidth() const { return m_originalWidth; }
+ /*! \brief return the original height of the image, before scaling/cropping */
+ unsigned int GetOriginalHeight() const { return m_originalHeight; }
+
+ int GetOrientation() const { return m_orientation; }
+ void SetOrientation(int orientation) { m_orientation = orientation; }
+
+ void Update(unsigned int width, unsigned int height, unsigned int pitch, unsigned int format, const unsigned char *pixels, bool loadToGPU);
+ void Allocate(unsigned int width, unsigned int height, unsigned int format);
+ void ClampToEdge();
+
+ static unsigned int PadPow2(unsigned int x);
+ static bool SwapBlueRed(unsigned char *pixels, unsigned int height, unsigned int pitch, unsigned int elements = 4, unsigned int offset=0);
+
+private:
+ // no copy constructor
+ CTexture(const CTexture& copy) = delete;
+
+protected:
+ bool LoadFromFileInMem(unsigned char* buffer, size_t size, const std::string& mimeType,
+ unsigned int maxWidth, unsigned int maxHeight);
+ bool LoadFromFileInternal(const std::string& texturePath, unsigned int maxWidth, unsigned int maxHeight, bool requirePixels, const std::string& strMimeType = "");
+ bool LoadIImage(IImage* pImage, unsigned char* buffer, unsigned int bufSize, unsigned int width, unsigned int height);
+ // helpers for computation of texture parameters for compressed textures
+ unsigned int GetPitch(unsigned int width) const;
+ unsigned int GetRows(unsigned int height) const;
+ unsigned int GetBlockSize() const;
+
+ unsigned int m_imageWidth;
+ unsigned int m_imageHeight;
+ unsigned int m_textureWidth;
+ unsigned int m_textureHeight;
+ unsigned int m_originalWidth; ///< original image width before scaling or cropping
+ unsigned int m_originalHeight; ///< original image height before scaling or cropping
+
+ unsigned char* m_pixels;
+ bool m_loadedToGPU;
+ unsigned int m_format;
+ int m_orientation;
+ bool m_hasAlpha = true ;
+ bool m_mipmapping = false ;
+ TEXTURE_SCALING m_scalingMethod = TEXTURE_SCALING::LINEAR;
+ bool m_bCacheMemory = false;
+};
diff --git a/xbmc/guilib/TextureBundle.cpp b/xbmc/guilib/TextureBundle.cpp
new file mode 100644
index 0000000..491ca69
--- /dev/null
+++ b/xbmc/guilib/TextureBundle.cpp
@@ -0,0 +1,83 @@
+/*
+ * 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 "TextureBundle.h"
+
+#include "guilib/TextureBundleXBT.h"
+
+class CTexture;
+
+CTextureBundle::CTextureBundle() : m_tbXBT{false}, m_useXBT{false}
+{
+}
+
+CTextureBundle::CTextureBundle(bool useXBT) : m_tbXBT{useXBT}, m_useXBT{useXBT}
+{
+}
+
+bool CTextureBundle::HasFile(const std::string& Filename)
+{
+ if (m_useXBT)
+ {
+ return m_tbXBT.HasFile(Filename);
+ }
+
+ if (m_tbXBT.HasFile(Filename))
+ {
+ m_useXBT = true;
+ return true;
+ }
+
+ return false;
+}
+
+std::vector<std::string> CTextureBundle::GetTexturesFromPath(const std::string& path)
+{
+ if (m_useXBT)
+ return m_tbXBT.GetTexturesFromPath(path);
+ else
+ return {};
+}
+
+bool CTextureBundle::LoadTexture(const std::string& filename,
+ std::unique_ptr<CTexture>& texture,
+ int& width,
+ int& height)
+{
+ if (m_useXBT)
+ return m_tbXBT.LoadTexture(filename, texture, width, height);
+ else
+ return false;
+}
+
+bool CTextureBundle::LoadAnim(const std::string& filename,
+ std::vector<std::pair<std::unique_ptr<CTexture>, int>>& textures,
+ int& width,
+ int& height,
+ int& nLoops)
+{
+ if (m_useXBT)
+ return m_tbXBT.LoadAnim(filename, textures, width, height, nLoops);
+ else
+ return false;
+}
+
+void CTextureBundle::Close()
+{
+ m_tbXBT.CloseBundle();
+}
+
+void CTextureBundle::SetThemeBundle(bool themeBundle)
+{
+ m_tbXBT.SetThemeBundle(themeBundle);
+}
+
+std::string CTextureBundle::Normalize(std::string name)
+{
+ return CTextureBundleXBT::Normalize(std::move(name));
+}
diff --git a/xbmc/guilib/TextureBundle.h b/xbmc/guilib/TextureBundle.h
new file mode 100644
index 0000000..a736192
--- /dev/null
+++ b/xbmc/guilib/TextureBundle.h
@@ -0,0 +1,76 @@
+/*
+ * 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 "TextureBundleXBT.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CTexture;
+
+class CTextureBundle
+{
+public:
+ CTextureBundle();
+ explicit CTextureBundle(bool useXBT);
+ ~CTextureBundle() = default;
+
+ void SetThemeBundle(bool themeBundle);
+ bool HasFile(const std::string& Filename);
+ std::vector<std::string> GetTexturesFromPath(const std::string& path);
+ static std::string Normalize(std::string name);
+
+ /*!
+ * \brief Load texture from bundle
+ *
+ * \param[in] filename name of the texture to load
+ * \param[out] texture holds the pointer to the texture after successful loading
+ * \param[out] width width of the loaded texture
+ * \param[out] height height of the loaded texture
+ * \return true if texture was loaded
+ *
+ * \todo With c++17 this should be changed to return a std::optional that's
+ * wrapping a struct containing the output values. Same for
+ * CTextureBundleXBT::LoadTexture.
+ */
+ bool LoadTexture(const std::string& filename,
+ std::unique_ptr<CTexture>& texture,
+ int& width,
+ int& height);
+
+ /*!
+ * \brief Load animation from bundle
+ *
+ * \param[in] filename name of the animation to load
+ * \param[out] texture vector of frames. Each frame is pair of a texture and
+ * the duration the frame
+ * \param[out] width width of the loaded textures
+ * \param[out] height height of the loaded textures
+ * \return true if animation was loaded
+ *
+ * \todo With c++17 this should be changed to return a std::optional that's
+ * wrapping a struct containing the output values. Same for
+ * CTextureBundleXBT::LoadAnim.
+ */
+ bool LoadAnim(const std::string& filename,
+ std::vector<std::pair<std::unique_ptr<CTexture>, int>>& textures,
+ int& width,
+ int& height,
+ int& nLoops);
+ void Close();
+private:
+ CTextureBundleXBT m_tbXBT;
+
+ bool m_useXBT;
+};
+
+
diff --git a/xbmc/guilib/TextureBundleXBT.cpp b/xbmc/guilib/TextureBundleXBT.cpp
new file mode 100644
index 0000000..981ddad
--- /dev/null
+++ b/xbmc/guilib/TextureBundleXBT.cpp
@@ -0,0 +1,301 @@
+/*
+ * 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 "TextureBundleXBT.h"
+
+#include "ServiceBroker.h"
+#include "Texture.h"
+#include "URL.h"
+#include "XBTF.h"
+#include "XBTFReader.h"
+#include "commons/ilog.h"
+#include "filesystem/SpecialProtocol.h"
+#include "filesystem/XbtManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <exception>
+
+#include <lzo/lzo1x.h>
+#include <lzo/lzoconf.h>
+
+
+#ifdef TARGET_WINDOWS_DESKTOP
+#ifdef NDEBUG
+#pragma comment(lib,"lzo2.lib")
+#else
+#pragma comment(lib, "lzo2d.lib")
+#endif
+#endif
+
+CTextureBundleXBT::CTextureBundleXBT()
+ : m_TimeStamp{0}
+ , m_themeBundle{false}
+{
+}
+
+CTextureBundleXBT::CTextureBundleXBT(bool themeBundle)
+ : m_TimeStamp{0}
+ , m_themeBundle{themeBundle}
+{
+}
+
+CTextureBundleXBT::~CTextureBundleXBT(void)
+{
+ CloseBundle();
+}
+
+void CTextureBundleXBT::CloseBundle()
+{
+ if (m_XBTFReader != nullptr && m_XBTFReader->IsOpen())
+ {
+ XFILE::CXbtManager::GetInstance().Release(CURL(m_path));
+ CLog::Log(LOGDEBUG, "{} - Closed {}bundle", __FUNCTION__, m_themeBundle ? "theme " : "");
+ }
+}
+
+bool CTextureBundleXBT::OpenBundle()
+{
+ // Find the correct texture file (skin or theme)
+ if (m_themeBundle)
+ {
+ // if we are the theme bundle, we only load if the user has chosen
+ // a valid theme (or the skin has a default one)
+ std::string theme = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKINTHEME);
+ if (!theme.empty() && !StringUtils::EqualsNoCase(theme, "SKINDEFAULT"))
+ {
+ std::string themeXBT(URIUtils::ReplaceExtension(theme, ".xbt"));
+ m_path = URIUtils::AddFileToFolder(CServiceBroker::GetWinSystem()->GetGfxContext().GetMediaDir(), "media", themeXBT);
+ }
+ else
+ {
+ return false;
+ }
+ }
+ else
+ {
+ m_path = URIUtils::AddFileToFolder(CServiceBroker::GetWinSystem()->GetGfxContext().GetMediaDir(), "media", "Textures.xbt");
+ }
+
+ m_path = CSpecialProtocol::TranslatePathConvertCase(m_path);
+
+ // Load the texture file
+ if (!XFILE::CXbtManager::GetInstance().GetReader(CURL(m_path), m_XBTFReader))
+ {
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "{} - Opened bundle {}", __FUNCTION__, m_path);
+
+ m_TimeStamp = m_XBTFReader->GetLastModificationTimestamp();
+
+ if (lzo_init() != LZO_E_OK)
+ {
+ return false;
+ }
+
+ return true;
+}
+
+bool CTextureBundleXBT::HasFile(const std::string& Filename)
+{
+ if ((m_XBTFReader == nullptr || !m_XBTFReader->IsOpen()) && !OpenBundle())
+ return false;
+
+ if (m_XBTFReader->GetLastModificationTimestamp() > m_TimeStamp)
+ {
+ CLog::Log(LOGINFO, "Texture bundle has changed, reloading");
+ if (!OpenBundle())
+ return false;
+ }
+
+ std::string name = Normalize(Filename);
+ return m_XBTFReader->Exists(name);
+}
+
+std::vector<std::string> CTextureBundleXBT::GetTexturesFromPath(const std::string& path)
+{
+ if (path.size() > 1 && path[1] == ':')
+ return {};
+
+ if ((m_XBTFReader == nullptr || !m_XBTFReader->IsOpen()) && !OpenBundle())
+ return {};
+
+ std::string testPath = Normalize(path);
+ URIUtils::AddSlashAtEnd(testPath);
+
+ std::vector<std::string> textures;
+ std::vector<CXBTFFile> files = m_XBTFReader->GetFiles();
+ for (size_t i = 0; i < files.size(); i++)
+ {
+ std::string filePath = files[i].GetPath();
+ if (StringUtils::StartsWithNoCase(filePath, testPath))
+ textures.emplace_back(std::move(filePath));
+ }
+
+ return textures;
+}
+
+bool CTextureBundleXBT::LoadTexture(const std::string& filename,
+ std::unique_ptr<CTexture>& texture,
+ int& width,
+ int& height)
+{
+ std::string name = Normalize(filename);
+
+ CXBTFFile file;
+ if (!m_XBTFReader->Get(name, file))
+ return false;
+
+ if (file.GetFrames().empty())
+ return false;
+
+ const CXBTFFrame& frame = file.GetFrames().at(0);
+ if (!ConvertFrameToTexture(filename, frame, texture))
+ {
+ return false;
+ }
+
+ width = frame.GetWidth();
+ height = frame.GetHeight();
+
+ return true;
+}
+
+bool CTextureBundleXBT::LoadAnim(const std::string& filename,
+ std::vector<std::pair<std::unique_ptr<CTexture>, int>>& textures,
+ int& width,
+ int& height,
+ int& nLoops)
+{
+ std::string name = Normalize(filename);
+
+ CXBTFFile file;
+ if (!m_XBTFReader->Get(name, file))
+ return false;
+
+ if (file.GetFrames().empty())
+ return false;
+
+ size_t nTextures = file.GetFrames().size();
+ textures.reserve(nTextures);
+
+ for (size_t i = 0; i < nTextures; i++)
+ {
+ CXBTFFrame& frame = file.GetFrames().at(i);
+
+ std::unique_ptr<CTexture> texture;
+ if (!ConvertFrameToTexture(filename, frame, texture))
+ return false;
+
+ textures.emplace_back(std::move(texture), frame.GetDuration());
+ }
+
+ width = file.GetFrames().at(0).GetWidth();
+ height = file.GetFrames().at(0).GetHeight();
+ nLoops = file.GetLoop();
+
+ return true;
+}
+
+bool CTextureBundleXBT::ConvertFrameToTexture(const std::string& name,
+ const CXBTFFrame& frame,
+ std::unique_ptr<CTexture>& texture)
+{
+ // found texture - allocate the necessary buffers
+ std::vector<unsigned char> buffer(static_cast<size_t>(frame.GetPackedSize()));
+
+ // load the compressed texture
+ if (!m_XBTFReader->Load(frame, buffer.data()))
+ {
+ CLog::Log(LOGERROR, "Error loading texture: {}", name);
+ return false;
+ }
+
+ // check if it's packed with lzo
+ if (frame.IsPacked())
+ { // unpack
+ std::vector<unsigned char> unpacked(static_cast<size_t>(frame.GetUnpackedSize()));
+ lzo_uint s = (lzo_uint)frame.GetUnpackedSize();
+ if (lzo1x_decompress_safe(buffer.data(), static_cast<lzo_uint>(buffer.size()), unpacked.data(),
+ &s, NULL) != LZO_E_OK ||
+ s != frame.GetUnpackedSize())
+ {
+ CLog::Log(LOGERROR, "Error loading texture: {}: Decompression error", name);
+ return false;
+ }
+ buffer = std::move(unpacked);
+ }
+
+ // create an xbmc texture
+ texture = CTexture::CreateTexture();
+ texture->LoadFromMemory(frame.GetWidth(), frame.GetHeight(), 0, frame.GetFormat(),
+ frame.HasAlpha(), buffer.data());
+
+ return true;
+}
+
+void CTextureBundleXBT::SetThemeBundle(bool themeBundle)
+{
+ m_themeBundle = themeBundle;
+}
+
+// normalize to how it's stored within the bundle
+// lower case + using forward slash rather than back slash
+std::string CTextureBundleXBT::Normalize(std::string name)
+{
+ StringUtils::Trim(name);
+ StringUtils::ToLower(name);
+ StringUtils::Replace(name, '\\', '/');
+
+ return name;
+}
+
+std::vector<uint8_t> CTextureBundleXBT::UnpackFrame(const CXBTFReader& reader,
+ const CXBTFFrame& frame)
+{
+ // load the compressed texture
+ std::vector<uint8_t> packedBuffer(static_cast<size_t>(frame.GetPackedSize()));
+ if (!reader.Load(frame, packedBuffer.data()))
+ {
+ CLog::Log(LOGERROR, "CTextureBundleXBT: error loading frame");
+ return {};
+ }
+
+ // if the frame isn't packed there's nothing else to be done
+ if (!frame.IsPacked())
+ return packedBuffer;
+
+ // make sure lzo is initialized
+ if (lzo_init() != LZO_E_OK)
+ {
+ CLog::Log(LOGERROR, "CTextureBundleXBT: failed to initialize lzo");
+ return {};
+ }
+
+ lzo_uint size = static_cast<lzo_uint>(frame.GetUnpackedSize());
+ std::vector<uint8_t> unpackedBuffer(static_cast<size_t>(frame.GetUnpackedSize()));
+ if (lzo1x_decompress_safe(packedBuffer.data(), static_cast<lzo_uint>(packedBuffer.size()),
+ unpackedBuffer.data(), &size, nullptr) != LZO_E_OK ||
+ size != frame.GetUnpackedSize())
+ {
+ CLog::Log(LOGERROR,
+ "CTextureBundleXBT: failed to decompress frame with {} unpacked bytes to {} bytes",
+ frame.GetPackedSize(), frame.GetUnpackedSize());
+ return {};
+ }
+
+ return unpackedBuffer;
+}
diff --git a/xbmc/guilib/TextureBundleXBT.h b/xbmc/guilib/TextureBundleXBT.h
new file mode 100644
index 0000000..915b52b
--- /dev/null
+++ b/xbmc/guilib/TextureBundleXBT.h
@@ -0,0 +1,69 @@
+/*
+ * 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 <cstdint>
+#include <ctime>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CTexture;
+class CXBTFReader;
+class CXBTFFrame;
+
+class CTextureBundleXBT
+{
+public:
+ CTextureBundleXBT();
+ explicit CTextureBundleXBT(bool themeBundle);
+ ~CTextureBundleXBT();
+
+ void SetThemeBundle(bool themeBundle);
+ bool HasFile(const std::string& Filename);
+ std::vector<std::string> GetTexturesFromPath(const std::string& path);
+ static std::string Normalize(std::string name);
+
+ /*!
+ * \brief See CTextureBundle::LoadTexture
+ */
+ bool LoadTexture(const std::string& filename,
+ std::unique_ptr<CTexture>& texture,
+ int& width,
+ int& height);
+
+ /*!
+ * \brief See CTextureBundle::LoadAnim
+ */
+ bool LoadAnim(const std::string& filename,
+ std::vector<std::pair<std::unique_ptr<CTexture>, int>>& textures,
+ int& width,
+ int& height,
+ int& nLoops);
+
+ //! @todo Change return to std::optional<std::vector<uint8_t>>> when c++17 is allowed
+ static std::vector<uint8_t> UnpackFrame(const CXBTFReader& reader, const CXBTFFrame& frame);
+
+ void CloseBundle();
+
+private:
+ bool OpenBundle();
+ bool ConvertFrameToTexture(const std::string& name,
+ const CXBTFFrame& frame,
+ std::unique_ptr<CTexture>& texture);
+
+ time_t m_TimeStamp;
+
+ bool m_themeBundle;
+ std::string m_path;
+ std::shared_ptr<CXBTFReader> m_XBTFReader;
+};
+
+
diff --git a/xbmc/guilib/TextureDX.cpp b/xbmc/guilib/TextureDX.cpp
new file mode 100644
index 0000000..1690add
--- /dev/null
+++ b/xbmc/guilib/TextureDX.cpp
@@ -0,0 +1,193 @@
+/*
+ * 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 "TextureDX.h"
+
+#include "utils/MemUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+
+std::unique_ptr<CTexture> CTexture::CreateTexture(unsigned int width,
+ unsigned int height,
+ unsigned int format)
+{
+ return std::make_unique<CDXTexture>(width, height, format);
+}
+
+CDXTexture::CDXTexture(unsigned int width, unsigned int height, unsigned int format)
+ : CTexture(width, height, format)
+{
+}
+
+CDXTexture::~CDXTexture()
+{
+ DestroyTextureObject();
+}
+
+void CDXTexture::CreateTextureObject()
+{
+ m_texture.Create(m_textureWidth, m_textureHeight, 1, D3D11_USAGE_DEFAULT, GetFormat());
+}
+
+DXGI_FORMAT CDXTexture::GetFormat()
+{
+ DXGI_FORMAT format = DXGI_FORMAT_UNKNOWN; // DXGI_FORMAT_UNKNOWN
+
+ switch (m_format)
+ {
+ case XB_FMT_DXT1:
+ format = DXGI_FORMAT_BC1_UNORM; // D3DFMT_DXT1 -> DXGI_FORMAT_BC1_UNORM & DXGI_FORMAT_BC1_UNORM_SRGB
+ break;
+ case XB_FMT_DXT3:
+ format = DXGI_FORMAT_BC2_UNORM; // D3DFMT_DXT3 -> DXGI_FORMAT_BC2_UNORM & DXGI_FORMAT_BC2_UNORM_SRGB
+ break;
+ case XB_FMT_DXT5:
+ case XB_FMT_DXT5_YCoCg:
+ format = DXGI_FORMAT_BC3_UNORM; // XB_FMT_DXT5 -> DXGI_FORMAT_BC3_UNORM & DXGI_FORMAT_BC3_UNORM_SRGB
+ break;
+ case XB_FMT_RGB8:
+ case XB_FMT_A8R8G8B8:
+ format = DXGI_FORMAT_B8G8R8A8_UNORM; // D3DFMT_A8R8G8B8 -> DXGI_FORMAT_B8G8R8A8_UNORM | DXGI_FORMAT_B8G8R8A8_UNORM_SRGB
+ break;
+ case XB_FMT_A8:
+ format = DXGI_FORMAT_R8_UNORM; // XB_FMT_A8 -> DXGI_FORMAT_A8_UNORM
+ break;
+ }
+
+ return format;
+}
+
+void CDXTexture::DestroyTextureObject()
+{
+ m_texture.Release();
+}
+
+void CDXTexture::LoadToGPU()
+{
+ if (!m_pixels)
+ {
+ // nothing to load - probably same image (no change)
+ return;
+ }
+
+ bool needUpdate = true;
+ D3D11_USAGE usage = D3D11_USAGE_DEFAULT;
+ if (m_format == XB_FMT_RGB8)
+ usage = D3D11_USAGE_DYNAMIC; // fallback to dynamic to allow CPU write to texture
+
+ if (m_texture.Get() == nullptr)
+ {
+ // creates texture with initial data
+ if (m_format != XB_FMT_RGB8)
+ {
+ // this is faster way to create texture with initial data instead of create empty and then copy to it
+ m_texture.Create(m_textureWidth, m_textureHeight, IsMipmapped() ? 0 : 1, usage, GetFormat(), m_pixels, GetPitch());
+ if (m_texture.Get() != nullptr)
+ needUpdate = false;
+ }
+ else
+ m_texture.Create(m_textureWidth, m_textureHeight, IsMipmapped() ? 0 : 1, usage, GetFormat());
+
+ if (m_texture.Get() == nullptr)
+ {
+ CLog::Log(LOGDEBUG, "CDXTexture::CDXTexture: Error creating new texture for size {} x {}.",
+ m_textureWidth, m_textureHeight);
+ return;
+ }
+ }
+ else
+ {
+ // need to update texture, check usage first
+ D3D11_TEXTURE2D_DESC texDesc;
+ m_texture.GetDesc(&texDesc);
+ usage = texDesc.Usage;
+
+ // if usage is not dynamic re-create texture with dynamic usage for future updates
+ if (usage != D3D11_USAGE_DYNAMIC && usage != D3D11_USAGE_STAGING)
+ {
+ m_texture.Release();
+ usage = D3D11_USAGE_DYNAMIC;
+
+ m_texture.Create(m_textureWidth, m_textureHeight, IsMipmapped() ? 0 : 1, usage, GetFormat(), m_pixels, GetPitch());
+ if (m_texture.Get() == nullptr)
+ {
+ CLog::Log(LOGDEBUG, "CDXTexture::CDXTexture: Error creating new texture for size {} x {}.",
+ m_textureWidth, m_textureHeight);
+ return;
+ }
+
+ needUpdate = false;
+ }
+ }
+
+ if (needUpdate)
+ {
+ D3D11_MAP mapType = (usage == D3D11_USAGE_STAGING) ? D3D11_MAP_WRITE : D3D11_MAP_WRITE_DISCARD;
+ D3D11_MAPPED_SUBRESOURCE lr;
+ if (m_texture.LockRect(0, &lr, mapType))
+ {
+ unsigned char *dst = (unsigned char *)lr.pData;
+ unsigned char *src = m_pixels;
+ unsigned int dstPitch = lr.RowPitch;
+ unsigned int srcPitch = GetPitch();
+ unsigned int minPitch = std::min(srcPitch, dstPitch);
+
+ unsigned int rows = GetRows();
+ if (m_format == XB_FMT_RGB8)
+ {
+ for (unsigned int y = 0; y < rows; y++)
+ {
+ unsigned char *dst2 = dst;
+ unsigned char *src2 = src;
+ for (unsigned int x = 0; x < srcPitch / 3; x++, dst2 += 4, src2 += 3)
+ {
+ dst2[0] = src2[2];
+ dst2[1] = src2[1];
+ dst2[2] = src2[0];
+ dst2[3] = 0xff;
+ }
+ src += srcPitch;
+ dst += dstPitch;
+ }
+ }
+ else if (srcPitch == dstPitch)
+ {
+ memcpy(dst, src, srcPitch * rows);
+ }
+ else
+ {
+ for (unsigned int y = 0; y < rows; y++)
+ {
+ memcpy(dst, src, minPitch);
+ src += srcPitch;
+ dst += dstPitch;
+ }
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "failed to lock texture.");
+ }
+ m_texture.UnlockRect(0);
+ if (usage != D3D11_USAGE_STAGING && IsMipmapped())
+ m_texture.GenerateMipmaps();
+ }
+
+ if (!m_bCacheMemory)
+ {
+ KODI::MEMORY::AlignedFree(m_pixels);
+ m_pixels = nullptr;
+ }
+
+ m_loadedToGPU = true;
+}
+
+void CDXTexture::BindToUnit(unsigned int unit)
+{
+}
diff --git a/xbmc/guilib/TextureDX.h b/xbmc/guilib/TextureDX.h
new file mode 100644
index 0000000..0858780
--- /dev/null
+++ b/xbmc/guilib/TextureDX.h
@@ -0,0 +1,41 @@
+/*
+ * 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 "D3DResource.h"
+#include "Texture.h"
+
+/************************************************************************/
+/* CDXTexture */
+/************************************************************************/
+class CDXTexture : public CTexture
+{
+public:
+ CDXTexture(unsigned int width = 0, unsigned int height = 0, unsigned int format = XB_FMT_UNKNOWN);
+ virtual ~CDXTexture();
+
+ void CreateTextureObject();
+ void DestroyTextureObject();
+ virtual void LoadToGPU();
+ void BindToUnit(unsigned int unit);
+
+ ID3D11Texture2D* GetTextureObject()
+ {
+ return m_texture.Get();
+ };
+
+ ID3D11ShaderResourceView* GetShaderResource()
+ {
+ return m_texture.GetShaderResource();
+ };
+
+private:
+ CD3DTexture m_texture;
+ DXGI_FORMAT GetFormat();
+};
diff --git a/xbmc/guilib/TextureFormats.h b/xbmc/guilib/TextureFormats.h
new file mode 100644
index 0000000..95fff02
--- /dev/null
+++ b/xbmc/guilib/TextureFormats.h
@@ -0,0 +1,22 @@
+/*
+ * 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
+
+#define XB_FMT_MASK 0xffff ///< mask for format info - other flags are outside this
+#define XB_FMT_DXT_MASK 15
+#define XB_FMT_UNKNOWN 0
+#define XB_FMT_DXT1 1
+#define XB_FMT_DXT3 2
+#define XB_FMT_DXT5 4
+#define XB_FMT_DXT5_YCoCg 8
+#define XB_FMT_A8R8G8B8 16 // texture.xbt byte order (matches BGRA8)
+#define XB_FMT_A8 32
+#define XB_FMT_RGBA8 64
+#define XB_FMT_RGB8 128
+#define XB_FMT_OPAQUE 65536
diff --git a/xbmc/guilib/TextureGL.cpp b/xbmc/guilib/TextureGL.cpp
new file mode 100644
index 0000000..ef01bdf
--- /dev/null
+++ b/xbmc/guilib/TextureGL.cpp
@@ -0,0 +1,226 @@
+/*
+ * 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 "TextureGL.h"
+
+#include "ServiceBroker.h"
+#include "guilib/TextureManager.h"
+#include "rendering/RenderSystem.h"
+#include "settings/AdvancedSettings.h"
+#include "utils/GLUtils.h"
+#include "utils/MemUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+
+std::unique_ptr<CTexture> CTexture::CreateTexture(unsigned int width,
+ unsigned int height,
+ unsigned int format)
+{
+ return std::make_unique<CGLTexture>(width, height, format);
+}
+
+CGLTexture::CGLTexture(unsigned int width, unsigned int height, unsigned int format)
+ : CTexture(width, height, format)
+{
+ unsigned int major, minor;
+ CServiceBroker::GetRenderSystem()->GetRenderVersion(major, minor);
+ if (major >= 3)
+ m_isOglVersion3orNewer = true;
+}
+
+CGLTexture::~CGLTexture()
+{
+ DestroyTextureObject();
+}
+
+void CGLTexture::CreateTextureObject()
+{
+ glGenTextures(1, (GLuint*) &m_texture);
+}
+
+void CGLTexture::DestroyTextureObject()
+{
+ if (m_texture)
+ CServiceBroker::GetGUI()->GetTextureManager().ReleaseHwTexture(m_texture);
+}
+
+void CGLTexture::LoadToGPU()
+{
+ if (!m_pixels)
+ {
+ // nothing to load - probably same image (no change)
+ return;
+ }
+ if (m_texture == 0)
+ {
+ // Have OpenGL generate a texture object handle for us
+ // this happens only one time - the first time the texture is loaded
+ CreateTextureObject();
+ }
+
+ // Bind the texture object
+ glBindTexture(GL_TEXTURE_2D, m_texture);
+
+ GLenum filter = (m_scalingMethod == TEXTURE_SCALING::NEAREST ? GL_NEAREST : GL_LINEAR);
+
+ // Set the texture's stretching properties
+ if (IsMipmapped())
+ {
+ GLenum mipmapFilter = (m_scalingMethod == TEXTURE_SCALING::NEAREST ? GL_LINEAR_MIPMAP_NEAREST : GL_LINEAR_MIPMAP_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mipmapFilter);
+
+#ifndef HAS_GLES
+ // Lower LOD bias equals more sharpness, but less smooth animation
+ glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.5f);
+ if (!m_isOglVersion3orNewer)
+ glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
+#endif
+ }
+ else
+ {
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
+ }
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+ unsigned int maxSize = CServiceBroker::GetRenderSystem()->GetMaxTextureSize();
+ if (m_textureHeight > maxSize)
+ {
+ CLog::Log(LOGERROR,
+ "GL: Image height {} too big to fit into single texture unit, truncating to {}",
+ m_textureHeight, maxSize);
+ m_textureHeight = maxSize;
+ }
+ if (m_textureWidth > maxSize)
+ {
+ CLog::Log(LOGERROR,
+ "GL: Image width {} too big to fit into single texture unit, truncating to {}",
+ m_textureWidth, maxSize);
+#ifndef HAS_GLES
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, m_textureWidth);
+#endif
+ m_textureWidth = maxSize;
+ }
+
+#ifndef HAS_GLES
+ GLenum format = GL_BGRA;
+ GLint numcomponents = GL_RGBA;
+
+ switch (m_format)
+ {
+ case XB_FMT_DXT1:
+ format = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
+ break;
+ case XB_FMT_DXT3:
+ format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
+ break;
+ case XB_FMT_DXT5:
+ case XB_FMT_DXT5_YCoCg:
+ format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
+ break;
+ case XB_FMT_RGB8:
+ format = GL_RGB;
+ numcomponents = GL_RGB;
+ break;
+ case XB_FMT_A8R8G8B8:
+ default:
+ break;
+ }
+
+ if ((m_format & XB_FMT_DXT_MASK) == 0)
+ {
+ glTexImage2D(GL_TEXTURE_2D, 0, numcomponents,
+ m_textureWidth, m_textureHeight, 0,
+ format, GL_UNSIGNED_BYTE, m_pixels);
+ }
+ else
+ {
+ glCompressedTexImage2D(GL_TEXTURE_2D, 0, format,
+ m_textureWidth, m_textureHeight, 0,
+ GetPitch() * GetRows(), m_pixels);
+ }
+
+ if (IsMipmapped() && m_isOglVersion3orNewer)
+ {
+ glGenerateMipmap(GL_TEXTURE_2D);
+ }
+
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+
+#else // GLES version
+
+ // All incoming textures are BGRA, which GLES does not necessarily support.
+ // Some (most?) hardware supports BGRA textures via an extension.
+ // If not, we convert to RGBA first to avoid having to swizzle in shaders.
+ // Explicitly define GL_BGRA_EXT here in the case that it's not defined by
+ // system headers, and trust the extension list instead.
+#ifndef GL_BGRA_EXT
+#define GL_BGRA_EXT 0x80E1
+#endif
+
+ GLint internalformat;
+ GLenum pixelformat;
+
+ switch (m_format)
+ {
+ default:
+ case XB_FMT_RGBA8:
+ internalformat = pixelformat = GL_RGBA;
+ break;
+ case XB_FMT_RGB8:
+ internalformat = pixelformat = GL_RGB;
+ break;
+ case XB_FMT_A8R8G8B8:
+ if (CServiceBroker::GetRenderSystem()->IsExtSupported("GL_EXT_texture_format_BGRA8888") ||
+ CServiceBroker::GetRenderSystem()->IsExtSupported("GL_IMG_texture_format_BGRA8888"))
+ {
+ internalformat = pixelformat = GL_BGRA_EXT;
+ }
+ else if (CServiceBroker::GetRenderSystem()->IsExtSupported("GL_APPLE_texture_format_BGRA8888"))
+ {
+ // Apple's implementation does not conform to spec. Instead, they require
+ // differing format/internalformat, more like GL.
+ internalformat = GL_RGBA;
+ pixelformat = GL_BGRA_EXT;
+ }
+ else
+ {
+ SwapBlueRed(m_pixels, m_textureHeight, GetPitch());
+ internalformat = pixelformat = GL_RGBA;
+ }
+ break;
+ }
+ glTexImage2D(GL_TEXTURE_2D, 0, internalformat, m_textureWidth, m_textureHeight, 0,
+ pixelformat, GL_UNSIGNED_BYTE, m_pixels);
+
+ if (IsMipmapped())
+ {
+ glGenerateMipmap(GL_TEXTURE_2D);
+ }
+
+#endif
+ VerifyGLState();
+
+ if (!m_bCacheMemory)
+ {
+ KODI::MEMORY::AlignedFree(m_pixels);
+ m_pixels = NULL;
+ }
+
+ m_loadedToGPU = true;
+}
+
+void CGLTexture::BindToUnit(unsigned int unit)
+{
+ glActiveTexture(GL_TEXTURE0 + unit);
+ glBindTexture(GL_TEXTURE_2D, m_texture);
+}
+
diff --git a/xbmc/guilib/TextureGL.h b/xbmc/guilib/TextureGL.h
new file mode 100644
index 0000000..51c7035
--- /dev/null
+++ b/xbmc/guilib/TextureGL.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "Texture.h"
+
+#include "system_gl.h"
+
+/************************************************************************/
+/* CGLTexture */
+/************************************************************************/
+class CGLTexture : public CTexture
+{
+public:
+ CGLTexture(unsigned int width = 0, unsigned int height = 0, unsigned int format = XB_FMT_A8R8G8B8);
+ ~CGLTexture() override;
+
+ void CreateTextureObject() override;
+ void DestroyTextureObject() override;
+ void LoadToGPU() override;
+ void BindToUnit(unsigned int unit) override;
+
+protected:
+ GLuint m_texture = 0;
+ bool m_isOglVersion3orNewer = false;
+};
+
diff --git a/xbmc/guilib/TextureManager.cpp b/xbmc/guilib/TextureManager.cpp
new file mode 100644
index 0000000..ddf2e77
--- /dev/null
+++ b/xbmc/guilib/TextureManager.cpp
@@ -0,0 +1,658 @@
+/*
+ * 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 "TextureManager.h"
+
+#include "ServiceBroker.h"
+#include "Texture.h"
+#include "URL.h"
+#include "commons/ilog.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "guilib/TextureBundle.h"
+#include "guilib/TextureFormats.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+
+#ifdef _DEBUG_TEXTURES
+#include "utils/TimeUtils.h"
+#endif
+#if defined(TARGET_DARWIN_IOS)
+#define WIN_SYSTEM_CLASS CWinSystemIOS
+#include "windowing/ios/WinSystemIOS.h" // for g_Windowing in CGUITextureManager::FreeUnusedTextures
+#elif defined(TARGET_DARWIN_TVOS)
+#define WIN_SYSTEM_CLASS CWinSystemTVOS
+#include "windowing/tvos/WinSystemTVOS.h" // for g_Windowing in CGUITextureManager::FreeUnusedTextures
+#endif
+
+#if defined(HAS_GL) || defined(HAS_GLES)
+#include "system_gl.h"
+#endif
+
+#include "FFmpegImage.h"
+
+#include <algorithm>
+#include <cassert>
+#include <exception>
+
+/************************************************************************/
+/* */
+/************************************************************************/
+CTextureArray::CTextureArray(int width, int height, int loops, bool texCoordsArePixels)
+{
+ m_width = width;
+ m_height = height;
+ m_loops = loops;
+ m_orientation = 0;
+ m_texWidth = 0;
+ m_texHeight = 0;
+ m_texCoordsArePixels = false;
+}
+
+CTextureArray::CTextureArray()
+{
+ Reset();
+}
+
+CTextureArray::~CTextureArray() = default;
+
+unsigned int CTextureArray::size() const
+{
+ return m_textures.size();
+}
+
+
+void CTextureArray::Reset()
+{
+ m_textures.clear();
+ m_delays.clear();
+ m_width = 0;
+ m_height = 0;
+ m_loops = 0;
+ m_orientation = 0;
+ m_texWidth = 0;
+ m_texHeight = 0;
+ m_texCoordsArePixels = false;
+}
+
+void CTextureArray::Add(std::shared_ptr<CTexture> texture, int delay)
+{
+ if (!texture)
+ return;
+
+ m_texWidth = texture->GetTextureWidth();
+ m_texHeight = texture->GetTextureHeight();
+ m_texCoordsArePixels = false;
+
+ m_textures.emplace_back(std::move(texture));
+ m_delays.push_back(delay);
+}
+
+void CTextureArray::Set(std::shared_ptr<CTexture> texture, int width, int height)
+{
+ assert(!m_textures.size()); // don't try and set a texture if we already have one!
+ m_width = width;
+ m_height = height;
+ m_orientation = texture ? texture->GetOrientation() : 0;
+ Add(std::move(texture), 2);
+}
+
+void CTextureArray::Free()
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ Reset();
+}
+
+
+/************************************************************************/
+/* */
+/************************************************************************/
+
+CTextureMap::CTextureMap()
+{
+ m_referenceCount = 0;
+ m_memUsage = 0;
+}
+
+CTextureMap::CTextureMap(const std::string& textureName, int width, int height, int loops)
+: m_texture(width, height, loops)
+, m_textureName(textureName)
+{
+ m_referenceCount = 0;
+ m_memUsage = 0;
+}
+
+CTextureMap::~CTextureMap()
+{
+ FreeTexture();
+}
+
+bool CTextureMap::Release()
+{
+ if (!m_texture.m_textures.size())
+ return true;
+ if (!m_referenceCount)
+ return true;
+
+ m_referenceCount--;
+ if (!m_referenceCount)
+ {
+ return true;
+ }
+ return false;
+}
+
+const std::string& CTextureMap::GetName() const
+{
+ return m_textureName;
+}
+
+const CTextureArray& CTextureMap::GetTexture()
+{
+ m_referenceCount++;
+ return m_texture;
+}
+
+void CTextureMap::Dump() const
+{
+ if (!m_referenceCount)
+ return; // nothing to see here
+
+ CLog::Log(LOGDEBUG, "{0}: texture:{1} has {2} frames {3} refcount", __FUNCTION__, m_textureName,
+ m_texture.m_textures.size(), m_referenceCount);
+}
+
+unsigned int CTextureMap::GetMemoryUsage() const
+{
+ return m_memUsage;
+}
+
+void CTextureMap::Flush()
+{
+ if (!m_referenceCount)
+ FreeTexture();
+}
+
+
+void CTextureMap::FreeTexture()
+{
+ m_texture.Free();
+}
+
+void CTextureMap::SetHeight(int height)
+{
+ m_texture.m_height = height;
+}
+
+void CTextureMap::SetWidth(int height)
+{
+ m_texture.m_width = height;
+}
+
+bool CTextureMap::IsEmpty() const
+{
+ return m_texture.m_textures.empty();
+}
+
+void CTextureMap::Add(std::unique_ptr<CTexture> texture, int delay)
+{
+ if (texture)
+ m_memUsage += sizeof(CTexture) + (texture->GetTextureWidth() * texture->GetTextureHeight() * 4);
+
+ m_texture.Add(std::move(texture), delay);
+}
+
+/************************************************************************/
+/* */
+/************************************************************************/
+CGUITextureManager::CGUITextureManager(void)
+{
+ // we set the theme bundle to be the first bundle (thus prioritizing it)
+ m_TexBundle[0].SetThemeBundle(true);
+}
+
+CGUITextureManager::~CGUITextureManager(void)
+{
+ Cleanup();
+}
+
+/************************************************************************/
+/* */
+/************************************************************************/
+bool CGUITextureManager::CanLoad(const std::string &texturePath)
+{
+ if (texturePath.empty())
+ return false;
+
+ if (!CURL::IsFullPath(texturePath))
+ return true; // assume we have it
+
+ // we can't (or shouldn't) be loading from remote paths, so check these
+ return URIUtils::IsHD(texturePath);
+}
+
+bool CGUITextureManager::HasTexture(const std::string &textureName, std::string *path, int *bundle, int *size)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ // default values
+ if (bundle) *bundle = -1;
+ if (size) *size = 0;
+ if (path) *path = textureName;
+
+ if (textureName.empty())
+ return false;
+
+ if (!CanLoad(textureName))
+ return false;
+
+ // Check our loaded and bundled textures - we store in bundles using \\.
+ std::string bundledName = CTextureBundle::Normalize(textureName);
+ for (int i = 0; i < (int)m_vecTextures.size(); ++i)
+ {
+ CTextureMap *pMap = m_vecTextures[i];
+ if (pMap->GetName() == textureName)
+ {
+ if (size) *size = 1;
+ return true;
+ }
+ }
+
+ for (int i = 0; i < 2; i++)
+ {
+ if (m_TexBundle[i].HasFile(bundledName))
+ {
+ if (bundle) *bundle = i;
+ return true;
+ }
+ }
+
+ std::string fullPath = GetTexturePath(textureName);
+ if (path)
+ *path = fullPath;
+
+ return !fullPath.empty();
+}
+
+const CTextureArray& CGUITextureManager::Load(const std::string& strTextureName, bool checkBundleOnly /*= false */)
+{
+ std::string strPath;
+ static CTextureArray emptyTexture;
+ int bundle = -1;
+ int size = 0;
+
+ if (strTextureName.empty())
+ return emptyTexture;
+
+ if (!HasTexture(strTextureName, &strPath, &bundle, &size))
+ return emptyTexture;
+
+ if (size) // we found the texture
+ {
+ for (int i = 0; i < (int)m_vecTextures.size(); ++i)
+ {
+ CTextureMap *pMap = m_vecTextures[i];
+ if (pMap->GetName() == strTextureName)
+ {
+ //CLog::Log(LOGDEBUG, "Total memusage {}", GetMemoryUsage());
+ return pMap->GetTexture();
+ }
+ }
+ // Whoops, not there.
+ return emptyTexture;
+ }
+
+ for (auto i = m_unusedTextures.begin(); i != m_unusedTextures.end(); ++i)
+ {
+ CTextureMap* pMap = i->first;
+
+ auto timestamp = i->second.time_since_epoch();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(timestamp);
+
+ if (pMap->GetName() == strTextureName && duration.count() > 0)
+ {
+ m_vecTextures.push_back(pMap);
+ m_unusedTextures.erase(i);
+ return pMap->GetTexture();
+ }
+ }
+
+ if (checkBundleOnly && bundle == -1)
+ return emptyTexture;
+
+ //Lock here, we will do stuff that could break rendering
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+#ifdef _DEBUG_TEXTURES
+ const auto start = std::chrono::steady_clock::now();
+#endif
+
+ if (bundle >= 0 && StringUtils::EndsWithNoCase(strPath, ".gif"))
+ {
+ CTextureMap* pMap = nullptr;
+ std::vector<std::pair<std::unique_ptr<CTexture>, int>> textures;
+ int nLoops = 0, width = 0, height = 0;
+ bool success = m_TexBundle[bundle].LoadAnim(strTextureName, textures, width, height, nLoops);
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "Texture manager unable to load bundled file: {}", strTextureName);
+ return emptyTexture;
+ }
+
+ unsigned int maxWidth = 0;
+ unsigned int maxHeight = 0;
+ pMap = new CTextureMap(strTextureName, width, height, nLoops);
+ for (auto& texture : textures)
+ {
+ maxWidth = std::max(maxWidth, texture.first->GetWidth());
+ maxHeight = std::max(maxHeight, texture.first->GetHeight());
+ pMap->Add(std::move(texture.first), texture.second);
+ }
+
+ pMap->SetWidth((int)maxWidth);
+ pMap->SetHeight((int)maxHeight);
+
+ m_vecTextures.push_back(pMap);
+ return pMap->GetTexture();
+ }
+ else if (StringUtils::EndsWithNoCase(strPath, ".gif") ||
+ StringUtils::EndsWithNoCase(strPath, ".apng"))
+ {
+ std::string mimeType;
+ if (StringUtils::EndsWithNoCase(strPath, ".gif"))
+ mimeType = "image/gif";
+ else if (StringUtils::EndsWithNoCase(strPath, ".apng"))
+ mimeType = "image/apng";
+
+ XFILE::CFile file;
+ std::vector<uint8_t> buf;
+ CFFmpegImage anim(mimeType);
+
+ if (file.LoadFile(strPath, buf) <= 0 || !anim.Initialize(buf.data(), buf.size()))
+ {
+ CLog::Log(LOGERROR, "Texture manager unable to load file: {}", CURL::GetRedacted(strPath));
+ file.Close();
+ return emptyTexture;
+ }
+
+ CTextureMap* pMap = new CTextureMap(strTextureName, 0, 0, 0);
+ unsigned int maxWidth = 0;
+ unsigned int maxHeight = 0;
+ uint64_t maxMemoryUsage = 91238400;// 1920*1080*4*11 bytes, i.e, a total of approx. 12 full hd frames
+
+ auto frame = anim.ReadFrame();
+ while (frame)
+ {
+ std::unique_ptr<CTexture> glTexture = CTexture::CreateTexture();
+ if (glTexture)
+ {
+ glTexture->LoadFromMemory(anim.Width(), anim.Height(), frame->GetPitch(), XB_FMT_A8R8G8B8, true, frame->m_pImage);
+ maxWidth = std::max(maxWidth, glTexture->GetWidth());
+ maxHeight = std::max(maxHeight, glTexture->GetHeight());
+ pMap->Add(std::move(glTexture), frame->m_delay);
+ }
+
+ if (pMap->GetMemoryUsage() <= maxMemoryUsage)
+ {
+ frame = anim.ReadFrame();
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Memory limit ({} bytes) exceeded, {} frames extracted from file : {}",
+ (maxMemoryUsage / 11) * 12, pMap->GetTexture().size(),
+ CURL::GetRedacted(strPath));
+ break;
+ }
+ }
+
+ pMap->SetWidth((int)maxWidth);
+ pMap->SetHeight((int)maxHeight);
+
+ file.Close();
+
+ m_vecTextures.push_back(pMap);
+ return pMap->GetTexture();
+ }
+
+ std::unique_ptr<CTexture> pTexture;
+ int width = 0, height = 0;
+ if (bundle >= 0)
+ {
+ if (!m_TexBundle[bundle].LoadTexture(strTextureName, pTexture, width, height))
+ {
+ CLog::Log(LOGERROR, "Texture manager unable to load bundled file: {}", strTextureName);
+ return emptyTexture;
+ }
+ }
+ else
+ {
+ pTexture = CTexture::LoadFromFile(strPath);
+ if (!pTexture)
+ return emptyTexture;
+ width = pTexture->GetWidth();
+ height = pTexture->GetHeight();
+ }
+
+ if (!pTexture) return emptyTexture;
+
+ CTextureMap* pMap = new CTextureMap(strTextureName, width, height, 0);
+ pMap->Add(std::move(pTexture), 100);
+ m_vecTextures.push_back(pMap);
+
+#ifdef _DEBUG_TEXTURES
+ const auto end = std::chrono::steady_clock::now();
+ const std::chrono::duration<double, std::milli> duration = end - start;
+ CLog::Log(LOGDEBUG, "Load {}: {:.3f} ms {}", strPath, duration.count(),
+ (bundle >= 0) ? "(bundled)" : "");
+#endif
+
+ return pMap->GetTexture();
+}
+
+
+void CGUITextureManager::ReleaseTexture(const std::string& strTextureName, bool immediately /*= false */)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ ivecTextures i;
+ i = m_vecTextures.begin();
+ while (i != m_vecTextures.end())
+ {
+ CTextureMap* pMap = *i;
+ if (pMap->GetName() == strTextureName)
+ {
+ if (pMap->Release())
+ {
+ //CLog::Log(LOGINFO, " cleanup:{}", strTextureName);
+ // add to our textures to free
+ std::chrono::time_point<std::chrono::steady_clock> timestamp;
+
+ if (!immediately)
+ timestamp = std::chrono::steady_clock::now();
+
+ m_unusedTextures.emplace_back(pMap, timestamp);
+ i = m_vecTextures.erase(i);
+ }
+ return;
+ }
+ ++i;
+ }
+ CLog::Log(LOGWARNING, "{}: Unable to release texture {}", __FUNCTION__, strTextureName);
+}
+
+void CGUITextureManager::FreeUnusedTextures(unsigned int timeDelay)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ for (auto i = m_unusedTextures.begin(); i != m_unusedTextures.end();)
+ {
+ auto now = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - i->second);
+
+ if (duration.count() >= timeDelay)
+ {
+ delete i->first;
+ i = m_unusedTextures.erase(i);
+ }
+ else
+ ++i;
+ }
+
+#if defined(HAS_GL) || defined(HAS_GLES)
+ for (unsigned int i = 0; i < m_unusedHwTextures.size(); ++i)
+ {
+ // 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]);
+ }
+#endif
+ m_unusedHwTextures.clear();
+}
+
+void CGUITextureManager::ReleaseHwTexture(unsigned int texture)
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ m_unusedHwTextures.push_back(texture);
+}
+
+void CGUITextureManager::Cleanup()
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ ivecTextures i;
+ i = m_vecTextures.begin();
+ while (i != m_vecTextures.end())
+ {
+ CTextureMap* pMap = *i;
+ CLog::Log(LOGWARNING, "{}: Having to cleanup texture {}", __FUNCTION__, pMap->GetName());
+ delete pMap;
+ i = m_vecTextures.erase(i);
+ }
+ m_TexBundle[0].Close();
+ m_TexBundle[1].Close();
+ m_TexBundle[0] = CTextureBundle(true);
+ m_TexBundle[1] = CTextureBundle();
+ FreeUnusedTextures();
+}
+
+void CGUITextureManager::Dump() const
+{
+ CLog::Log(LOGDEBUG, "{0}: total texturemaps size: {1}", __FUNCTION__, m_vecTextures.size());
+
+ for (int i = 0; i < (int)m_vecTextures.size(); ++i)
+ {
+ const CTextureMap* pMap = m_vecTextures[i];
+ if (!pMap->IsEmpty())
+ pMap->Dump();
+ }
+}
+
+void CGUITextureManager::Flush()
+{
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ ivecTextures i;
+ i = m_vecTextures.begin();
+ while (i != m_vecTextures.end())
+ {
+ CTextureMap* pMap = *i;
+ pMap->Flush();
+ if (pMap->IsEmpty() )
+ {
+ delete pMap;
+ i = m_vecTextures.erase(i);
+ }
+ else
+ {
+ ++i;
+ }
+ }
+}
+
+unsigned int CGUITextureManager::GetMemoryUsage() const
+{
+ unsigned int memUsage = 0;
+ for (int i = 0; i < (int)m_vecTextures.size(); ++i)
+ {
+ memUsage += m_vecTextures[i]->GetMemoryUsage();
+ }
+ return memUsage;
+}
+
+void CGUITextureManager::SetTexturePath(const std::string &texturePath)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_texturePaths.clear();
+ AddTexturePath(texturePath);
+}
+
+void CGUITextureManager::AddTexturePath(const std::string &texturePath)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (!texturePath.empty())
+ m_texturePaths.push_back(texturePath);
+}
+
+void CGUITextureManager::RemoveTexturePath(const std::string &texturePath)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for (std::vector<std::string>::iterator it = m_texturePaths.begin(); it != m_texturePaths.end(); ++it)
+ {
+ if (*it == texturePath)
+ {
+ m_texturePaths.erase(it);
+ return;
+ }
+ }
+}
+
+std::string CGUITextureManager::GetTexturePath(const std::string &textureName, bool directory /* = false */)
+{
+ if (CURL::IsFullPath(textureName))
+ return textureName;
+ else
+ { // texture doesn't include the full path, so check all fallbacks
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for (const std::string& it : m_texturePaths)
+ {
+ std::string path = URIUtils::AddFileToFolder(it, "media", textureName);
+ if (directory)
+ {
+ if (XFILE::CDirectory::Exists(path))
+ return path;
+ }
+ else
+ {
+ if (XFILE::CFile::Exists(path))
+ return path;
+ }
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "[Warning] CGUITextureManager::GetTexturePath: could not find texture '{}'",
+ textureName);
+ return "";
+}
+
+std::vector<std::string> CGUITextureManager::GetBundledTexturesFromPath(
+ const std::string& texturePath)
+{
+ std::vector<std::string> items = m_TexBundle[0].GetTexturesFromPath(texturePath);
+ if (items.empty())
+ items = m_TexBundle[1].GetTexturesFromPath(texturePath);
+ return items;
+}
diff --git a/xbmc/guilib/TextureManager.h b/xbmc/guilib/TextureManager.h
new file mode 100644
index 0000000..ef3128e
--- /dev/null
+++ b/xbmc/guilib/TextureManager.h
@@ -0,0 +1,131 @@
+/*
+ * 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 "GUIComponent.h"
+#include "TextureBundle.h"
+#include "threads/CriticalSection.h"
+
+#include <chrono>
+#include <cstddef>
+#include <cstdint>
+#include <list>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CTexture;
+
+/************************************************************************/
+/* */
+/************************************************************************/
+class CTextureArray
+{
+public:
+ CTextureArray(int width, int height, int loops, bool texCoordsArePixels = false);
+ CTextureArray();
+
+ virtual ~CTextureArray();
+
+ void Reset();
+
+ void Add(std::shared_ptr<CTexture> texture, int delay);
+ void Set(std::shared_ptr<CTexture> texture, int width, int height);
+ void Free();
+ unsigned int size() const;
+
+ std::vector<std::shared_ptr<CTexture>> m_textures;
+ std::vector<int> m_delays;
+ int m_width;
+ int m_height;
+ int m_orientation;
+ int m_loops;
+ int m_texWidth;
+ int m_texHeight;
+ bool m_texCoordsArePixels;
+};
+
+/*!
+ \ingroup textures
+ \brief
+ */
+/************************************************************************/
+/* */
+/************************************************************************/
+class CTextureMap
+{
+public:
+ CTextureMap();
+ CTextureMap(const std::string& textureName, int width, int height, int loops);
+ virtual ~CTextureMap();
+
+ void Add(std::unique_ptr<CTexture> texture, int delay);
+ bool Release();
+
+ const std::string& GetName() const;
+ const CTextureArray& GetTexture();
+ void Dump() const;
+ uint32_t GetMemoryUsage() const;
+ void Flush();
+ bool IsEmpty() const;
+ void SetHeight(int height);
+ void SetWidth(int height);
+protected:
+ void FreeTexture();
+
+ CTextureArray m_texture;
+ std::string m_textureName;
+ unsigned int m_referenceCount;
+ uint32_t m_memUsage;
+};
+
+/*!
+ \ingroup textures
+ \brief
+ */
+/************************************************************************/
+/* */
+/************************************************************************/
+class CGUITextureManager
+{
+public:
+ CGUITextureManager(void);
+ virtual ~CGUITextureManager(void);
+
+ bool HasTexture(const std::string &textureName, std::string *path = NULL, int *bundle = NULL, int *size = NULL);
+ static bool CanLoad(const std::string &texturePath); ///< Returns true if the texture manager can load this texture
+ const CTextureArray& Load(const std::string& strTextureName, bool checkBundleOnly = false);
+ void ReleaseTexture(const std::string& strTextureName, bool immediately = false);
+ void Cleanup();
+ void Dump() const;
+ uint32_t GetMemoryUsage() const;
+ void Flush();
+ std::string GetTexturePath(const std::string& textureName, bool directory = false);
+ std::vector<std::string> GetBundledTexturesFromPath(const std::string& texturePath);
+
+ void AddTexturePath(const std::string &texturePath); ///< Add a new path to the paths to check when loading media
+ void SetTexturePath(const std::string &texturePath); ///< Set a single path as the path to check when loading media (clear then add)
+ void RemoveTexturePath(const std::string &texturePath); ///< Remove a path from the paths to check when loading media
+
+ void FreeUnusedTextures(unsigned int timeDelay = 0); ///< Free textures (called from app thread only)
+ void ReleaseHwTexture(unsigned int texture);
+protected:
+ std::vector<CTextureMap*> m_vecTextures;
+ std::list<std::pair<CTextureMap*, std::chrono::time_point<std::chrono::steady_clock>>>
+ m_unusedTextures;
+ std::vector<unsigned int> m_unusedHwTextures;
+ typedef std::vector<CTextureMap*>::iterator ivecTextures;
+ // we have 2 texture bundles (one for the base textures, one for the theme)
+ CTextureBundle m_TexBundle[2];
+
+ std::vector<std::string> m_texturePaths;
+ CCriticalSection m_section;
+};
+
diff --git a/xbmc/guilib/Tween.h b/xbmc/guilib/Tween.h
new file mode 100644
index 0000000..6aea302
--- /dev/null
+++ b/xbmc/guilib/Tween.h
@@ -0,0 +1,401 @@
+/*
+ * 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
+
+///////////////////////////////////////////////////////////////////////
+// Tween.h
+// A couple of tweening classes implemented in C++.
+// ref: http://www.robertpenner.com/easing/
+//
+// Author: d4rk <d4rk@xbmc.org>
+///////////////////////////////////////////////////////////////////////
+
+
+///////////////////////////////////////////////////////////////////////
+// Current list of classes:
+//
+// LinearTweener
+// QuadTweener
+// CubicTweener
+// SineTweener
+// CircleTweener
+// BackTweener
+// BounceTweener
+// ElasticTweener
+//
+///////////////////////////////////////////////////////////////////////
+
+#include <math.h>
+
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+
+enum TweenerType
+{
+ EASE_IN,
+ EASE_OUT,
+ EASE_INOUT
+};
+
+
+class Tweener
+{
+public:
+ explicit Tweener(TweenerType tweenerType = EASE_OUT) { m_tweenerType = tweenerType; }
+ virtual ~Tweener() = default;
+
+ void SetEasing(TweenerType type) { m_tweenerType = type; }
+ virtual float Tween(float time, float start, float change, float duration)=0;
+ virtual bool HasResumePoint() const { return m_tweenerType == EASE_INOUT; }
+protected:
+ TweenerType m_tweenerType;
+};
+
+
+class LinearTweener : public Tweener
+{
+public:
+ float Tween(float time, float start, float change, float duration) override
+ {
+ return change * time / duration + start;
+ }
+ bool HasResumePoint() const override { return false; }
+};
+
+
+class QuadTweener : public Tweener
+{
+public:
+ explicit QuadTweener(float a = 1.0f) { _a=a; }
+ float Tween(float time, float start, float change, float duration) override
+ {
+ switch (m_tweenerType)
+ {
+ case EASE_IN:
+ time /= duration;
+ return change * time * (_a * time + 1 - _a) + start;
+ break;
+
+ case EASE_OUT:
+ time /= duration;
+ return -change * time * (_a * time - 1 - _a) + start;
+ break;
+
+ case EASE_INOUT:
+ time /= duration/2;
+ if (time < 1)
+ return (change) * time * (_a * time + 1 - _a) + start;
+ time--;
+ return (-change) * time * (_a * time - 1 - _a) + start;
+ break;
+ }
+ return change * time * time + start;
+ }
+private:
+ float _a;
+};
+
+
+class CubicTweener : public Tweener
+{
+public:
+ float Tween(float time, float start, float change, float duration) override
+ {
+ switch (m_tweenerType)
+ {
+ case EASE_IN:
+ time /= duration;
+ return change * time * time * time + start;
+ break;
+
+ case EASE_OUT:
+ time /= duration;
+ time--;
+ return change * (time * time * time + 1) + start;
+ break;
+
+ case EASE_INOUT:
+ time /= duration/2;
+ if (time < 1)
+ return (change/2) * time * time * time + start;
+ time-=2;
+ return (change/2) * (time * time * time + 2) + start;
+ break;
+ }
+ return change * time * time + start;
+ }
+};
+
+class CircleTweener : public Tweener
+{
+public:
+ float Tween(float time, float start, float change, float duration) override
+ {
+ switch (m_tweenerType)
+ {
+ case EASE_IN:
+ time /= duration;
+ return (-change) * (sqrt(1 - time * time) - 1) + start;
+ break;
+
+ case EASE_OUT:
+ time /= duration;
+ time--;
+ return change * sqrt(1 - time * time) + start;
+ break;
+
+ case EASE_INOUT:
+ time /= duration/2;
+ if (time < 1)
+ return (-change/2) * (sqrt(1 - time * time) - 1) + start;
+ time-=2;
+ return change/2 * (sqrt(1 - time * time) + 1) + start;
+ break;
+ }
+ return change * sqrt(1 - time * time) + start;
+ }
+};
+
+class BackTweener : public Tweener
+{
+public:
+ explicit BackTweener(float s=1.70158) { _s=s; }
+
+ float Tween(float time, float start, float change, float duration) override
+ {
+ float s = _s;
+ switch (m_tweenerType)
+ {
+ case EASE_IN:
+ time /= duration;
+ return change * time * time * ((s + 1) * time - s) + start;
+ break;
+
+ case EASE_OUT:
+ time /= duration;
+ time--;
+ return change * (time * time * ((s + 1) * time + s) + 1) + start;
+ break;
+
+ case EASE_INOUT:
+ time /= duration/2;
+ s*=(1.525f);
+ if ((time ) < 1)
+ {
+ return (change/2) * (time * time * ((s + 1) * time - s)) + start;
+ }
+ time-=2;
+ return (change/2) * (time * time * ((s + 1) * time + s) + 2) + start;
+ break;
+ }
+ return change * ((time-1) * time * ((s + 1) * time + s) + 1) + start;
+ }
+private:
+ float _s;
+
+};
+
+
+class SineTweener : public Tweener
+{
+public:
+ float Tween(float time, float start, float change, float duration) override
+ {
+ time /= duration;
+ switch (m_tweenerType)
+ {
+ case EASE_IN:
+ return change * (1 - cos(time * static_cast<float>(M_PI) / 2.0f)) + start;
+ break;
+
+ case EASE_OUT:
+ return change * sin(time * static_cast<float>(M_PI) / 2.0f) + start;
+ break;
+
+ case EASE_INOUT:
+ return change / 2 * (1 - cos(static_cast<float>(M_PI) * time)) + start;
+ break;
+ }
+ return (change / 2) * (1 - cos(static_cast<float>(M_PI) * time)) + start;
+ }
+};
+
+
+class BounceTweener : public Tweener
+{
+public:
+ float Tween(float time, float start, float change, float duration) override
+ {
+ switch (m_tweenerType)
+ {
+ case EASE_IN:
+ return (change - easeOut(duration - time, 0, change, duration)) + start;
+ break;
+
+ case EASE_OUT:
+ return easeOut(time, start, change, duration);
+ break;
+
+ case EASE_INOUT:
+ if (time < duration/2)
+ return (change - easeOut (duration - (time * 2), 0, change, duration) + start) * .5f + start;
+ else
+ return (easeOut (time * 2 - duration, 0, change, duration) * .5f + change * .5f) + start;
+ break;
+ }
+
+ return easeOut(time, start, change, duration);
+ }
+protected:
+ static float easeOut(float time, float start, float change, float duration)
+ {
+ time /= duration;
+ if (time < (1 / 2.75f))
+ {
+ return change * (7.5625f * time * time) + start;
+ }
+ else if (time < (2 / 2.75f))
+ {
+ time -= (1.5f/2.75f);
+ return change * (7.5625f * time * time + .75f) + start;
+ }
+ else if (time < (2.5f / 2.75f))
+ {
+ time -= (2.25f/2.75f);
+ return change * (7.5625f * time * time + .9375f) + start;
+ }
+ else
+ {
+ time -= (2.625f/2.75f);
+ return change * (7.5625f * time * time + .984375f) + start;
+ }
+ }
+};
+
+
+class ElasticTweener : public Tweener
+{
+public:
+ ElasticTweener(float a=0.0, float p=0.0) { _a=a; _p=p; }
+
+ float Tween(float time, float start, float change, float duration) override
+ {
+ switch (m_tweenerType)
+ {
+ case EASE_IN:
+ return easeIn(time, start, change, duration);
+ break;
+
+ case EASE_OUT:
+ return easeOut(time, start, change, duration);
+ break;
+
+ case EASE_INOUT:
+ return easeInOut(time, start, change, duration);
+ break;
+ }
+ return easeOut(time, start, change, duration);
+ }
+protected:
+ float _a;
+ float _p;
+
+ float easeIn(float time, float start, float change, float duration) const
+ {
+ float s=0;
+ float a=_a;
+ float p=_p;
+
+ if (time==0)
+ return start;
+ time /= duration;
+ if (time==1)
+ return start + change;
+ if (!p)
+ p=duration*.3f;
+ if (!a || a < fabs(change))
+ {
+ a = change;
+ s = p / 4.0f;
+ }
+ else
+ {
+ s = p / (2 * static_cast<float>(M_PI)) * asin(change / a);
+ }
+ time--;
+ return -(a * pow(2.0f, 10 * time) *
+ sin((time * duration - s) * (2 * static_cast<float>(M_PI)) / p)) +
+ start;
+ }
+
+ float easeOut(float time, float start, float change, float duration) const
+ {
+ float s=0;
+ float a=_a;
+ float p=_p;
+
+ if (time==0)
+ return start;
+ time /= duration;
+ if (time==1)
+ return start + change;
+ if (!p)
+ p=duration*.3f;
+ if (!a || a < fabs(change))
+ {
+ a = change;
+ s = p / 4.0f;
+ }
+ else
+ {
+ s = p / (2 * static_cast<float>(M_PI)) * asin(change / a);
+ }
+ return (a * pow(2.0f, -10 * time) *
+ sin((time * duration - s) * (2 * static_cast<float>(M_PI)) / p)) +
+ change + start;
+ }
+
+ float easeInOut(float time, float start, float change, float duration) const
+ {
+ float s=0;
+ float a=_a;
+ float p=_p;
+
+ if (time==0)
+ return start;
+ time /= duration/2;
+ if (time==2)
+ return start + change;
+ if (!p)
+ p=duration*.3f*1.5f;
+ if (!a || a < fabs(change))
+ {
+ a = change;
+ s = p / 4.0f;
+ }
+ else
+ {
+ s = p / (2 * static_cast<float>(M_PI)) * asin(change / a);
+ }
+
+ if (time < 1)
+ {
+ time--;
+ return -.5f * (a * pow(2.0f, 10 * (time)) *
+ sin((time * duration - s) * (2 * static_cast<float>(M_PI)) / p)) +
+ start;
+ }
+ time--;
+ return a * pow(2.0f, -10 * (time)) *
+ sin((time * duration - s) * (2 * static_cast<float>(M_PI)) / p) * .5f +
+ change + start;
+ }
+};
+
diff --git a/xbmc/guilib/VisibleEffect.cpp b/xbmc/guilib/VisibleEffect.cpp
new file mode 100644
index 0000000..17b2daa
--- /dev/null
+++ b/xbmc/guilib/VisibleEffect.cpp
@@ -0,0 +1,835 @@
+/*
+ * 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 "VisibleEffect.h"
+
+#include "GUIColorManager.h"
+#include "GUIControlFactory.h"
+#include "GUIInfoManager.h"
+#include "Tween.h"
+#include "addons/Skin.h" // for the effect time adjustments
+#include "guilib/GUIComponent.h"
+#include "utils/ColorUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <utility>
+
+CAnimEffect::CAnimEffect(const TiXmlElement *node, EFFECT_TYPE effect)
+{
+ m_effect = effect;
+ // defaults
+ m_delay = m_length = 0;
+ m_pTweener.reset();
+ // time and delay
+
+ float temp;
+ if (TIXML_SUCCESS == node->QueryFloatAttribute("time", &temp)) m_length = (unsigned int)(temp * g_SkinInfo->GetEffectsSlowdown());
+ if (TIXML_SUCCESS == node->QueryFloatAttribute("delay", &temp)) m_delay = (unsigned int)(temp * g_SkinInfo->GetEffectsSlowdown());
+
+ m_pTweener = GetTweener(node);
+}
+
+CAnimEffect::CAnimEffect(unsigned int delay, unsigned int length, EFFECT_TYPE effect)
+{
+ m_delay = delay;
+ m_length = length;
+ m_effect = effect;
+ m_pTweener = std::shared_ptr<Tweener>(new LinearTweener());
+}
+
+CAnimEffect::~CAnimEffect() = default;
+
+CAnimEffect::CAnimEffect(const CAnimEffect &src)
+{
+ m_pTweener.reset();
+ *this = src;
+}
+
+CAnimEffect& CAnimEffect::operator=(const CAnimEffect &src)
+{
+ if (&src == this) return *this;
+
+ m_matrix = src.m_matrix;
+ m_effect = src.m_effect;
+ m_length = src.m_length;
+ m_delay = src.m_delay;
+
+ m_pTweener = src.m_pTweener;
+ return *this;
+}
+
+void CAnimEffect::Calculate(unsigned int time, const CPoint &center)
+{
+ assert(m_delay + m_length);
+ // calculate offset and tweening
+ float offset = 0.0f; // delayed forward, or finished reverse
+ if (time >= m_delay && time < m_delay + m_length)
+ offset = (float)(time - m_delay) / m_length;
+ else if (time >= m_delay + m_length)
+ offset = 1.0f;
+ if (m_pTweener)
+ offset = m_pTweener->Tween(offset, 0.0f, 1.0f, 1.0f);
+ // and apply the effect
+ ApplyEffect(offset, center);
+}
+
+void CAnimEffect::ApplyState(ANIMATION_STATE state, const CPoint &center)
+{
+ float offset = (state == ANIM_STATE_APPLIED) ? 1.0f : 0.0f;
+ ApplyEffect(offset, center);
+}
+
+std::shared_ptr<Tweener> CAnimEffect::GetTweener(const TiXmlElement *pAnimationNode)
+{
+ std::shared_ptr<Tweener> m_pTweener;
+ const char *tween = pAnimationNode->Attribute("tween");
+ if (tween)
+ {
+ if (StringUtils::CompareNoCase(tween, "linear") == 0)
+ m_pTweener = std::shared_ptr<Tweener>(new LinearTweener());
+ else if (StringUtils::CompareNoCase(tween, "quadratic") == 0)
+ m_pTweener = std::shared_ptr<Tweener>(new QuadTweener());
+ else if (StringUtils::CompareNoCase(tween, "cubic") == 0)
+ m_pTweener = std::shared_ptr<Tweener>(new CubicTweener());
+ else if (StringUtils::CompareNoCase(tween, "sine") == 0)
+ m_pTweener = std::shared_ptr<Tweener>(new SineTweener());
+ else if (StringUtils::CompareNoCase(tween, "back") == 0)
+ m_pTweener = std::shared_ptr<Tweener>(new BackTweener());
+ else if (StringUtils::CompareNoCase(tween, "circle") == 0)
+ m_pTweener = std::shared_ptr<Tweener>(new CircleTweener());
+ else if (StringUtils::CompareNoCase(tween, "bounce") == 0)
+ m_pTweener = std::shared_ptr<Tweener>(new BounceTweener());
+ else if (StringUtils::CompareNoCase(tween, "elastic") == 0)
+ m_pTweener = std::shared_ptr<Tweener>(new ElasticTweener());
+
+ const char *easing = pAnimationNode->Attribute("easing");
+ if (m_pTweener && easing)
+ {
+ if (StringUtils::CompareNoCase(easing, "in") == 0)
+ m_pTweener->SetEasing(EASE_IN);
+ else if (StringUtils::CompareNoCase(easing, "out") == 0)
+ m_pTweener->SetEasing(EASE_OUT);
+ else if (StringUtils::CompareNoCase(easing, "inout") == 0)
+ m_pTweener->SetEasing(EASE_INOUT);
+ }
+ }
+
+ float accel = 0;
+ pAnimationNode->QueryFloatAttribute("acceleration", &accel);
+
+ if (!m_pTweener)
+ { // no tweener is specified - use a linear tweener
+ // or quadratic if we have acceleration
+ if (accel)
+ {
+ m_pTweener = std::shared_ptr<Tweener>(new QuadTweener(accel));
+ m_pTweener->SetEasing(EASE_IN);
+ }
+ else
+ m_pTweener = std::shared_ptr<Tweener>(new LinearTweener());
+ }
+
+ return m_pTweener;
+}
+
+CFadeEffect::CFadeEffect(const TiXmlElement* node, bool reverseDefaults, EFFECT_TYPE effect)
+ : CAnimEffect(node, effect)
+{
+ if (reverseDefaults)
+ { // out effect defaults
+ m_startAlpha = 100.0f;
+ m_endAlpha = 0;
+ }
+ else
+ { // in effect defaults
+ m_startAlpha = 0;
+ m_endAlpha = 100.0f;
+ }
+
+ m_startColor.alpha = m_startColor.red = m_startColor.green = m_startColor.blue = 1.0f;
+ m_endColor.alpha = m_endColor.red = m_endColor.green = m_endColor.blue = 1.0f;
+
+ if (effect == EFFECT_TYPE_FADE)
+ {
+ node->QueryFloatAttribute("start", &m_startAlpha);
+ node->QueryFloatAttribute("end", &m_endAlpha);
+ if (m_startAlpha > 100.0f)
+ m_startAlpha = 100.0f;
+ if (m_endAlpha > 100.0f)
+ m_endAlpha = 100.0f;
+ if (m_startAlpha < 0)
+ m_startAlpha = 0;
+ if (m_endAlpha < 0)
+ m_endAlpha = 0;
+ m_startColor.alpha = m_startAlpha * 0.01f;
+ m_endColor.alpha = m_endAlpha * 0.01f;
+ }
+ else if (effect == EFFECT_TYPE_FADE_DIFFUSE)
+ {
+ const char* start = node->Attribute("start");
+ const char* end = node->Attribute("end");
+ if (start)
+ m_startColor = UTILS::COLOR::ConvertToFloats(
+ CServiceBroker::GetGUI()->GetColorManager().GetColor(start));
+ if (end)
+ m_endColor =
+ UTILS::COLOR::ConvertToFloats(CServiceBroker::GetGUI()->GetColorManager().GetColor(end));
+ }
+}
+
+CFadeEffect::CFadeEffect(float start, float end, unsigned int delay, unsigned int length) : CAnimEffect(delay, length, EFFECT_TYPE_FADE)
+{
+ m_startAlpha = start;
+ m_endAlpha = end;
+}
+
+CFadeEffect::CFadeEffect(UTILS::COLOR::Color start,
+ UTILS::COLOR::Color end,
+ unsigned int delay,
+ unsigned int length)
+ : CAnimEffect(delay, length, EFFECT_TYPE_FADE_DIFFUSE)
+{
+ m_startAlpha = m_endAlpha = 1.0f;
+ m_startColor = UTILS::COLOR::ConvertToFloats(start);
+ m_endColor = UTILS::COLOR::ConvertToFloats(end);
+}
+
+void CFadeEffect::ApplyEffect(float offset, const CPoint &center)
+{
+ if (m_effect == EFFECT_TYPE_FADE)
+ {
+ m_matrix.SetFader(((m_endAlpha - m_startAlpha) * offset + m_startAlpha) * 0.01f);
+ }
+ else if (m_effect == EFFECT_TYPE_FADE_DIFFUSE)
+ {
+ m_matrix.SetFader(((m_endColor.alpha - m_startColor.alpha) * offset + m_startColor.alpha),
+ ((m_endColor.red - m_startColor.red) * offset + m_startColor.red),
+ ((m_endColor.green - m_startColor.green) * offset + m_startColor.green),
+ ((m_endColor.blue - m_startColor.blue) * offset + m_startColor.blue));
+ }
+}
+
+CSlideEffect::CSlideEffect(const TiXmlElement *node) : CAnimEffect(node, EFFECT_TYPE_SLIDE)
+{
+ m_startX = m_endX = 0;
+ m_startY = m_endY = 0;
+ const char *startPos = node->Attribute("start");
+ if (startPos)
+ {
+ std::vector<std::string> commaSeparated = StringUtils::Split(startPos, ",");
+ if (commaSeparated.size() > 1)
+ m_startY = (float)atof(commaSeparated[1].c_str());
+ if (!commaSeparated.empty())
+ m_startX = (float)atof(commaSeparated[0].c_str());
+ }
+ const char *endPos = node->Attribute("end");
+ if (endPos)
+ {
+ std::vector<std::string> commaSeparated = StringUtils::Split(endPos, ",");
+ if (commaSeparated.size() > 1)
+ m_endY = (float)atof(commaSeparated[1].c_str());
+ if (!commaSeparated.empty())
+ m_endX = (float)atof(commaSeparated[0].c_str());
+ }
+}
+
+void CSlideEffect::ApplyEffect(float offset, const CPoint &center)
+{
+ m_matrix.SetTranslation((m_endX - m_startX)*offset + m_startX, (m_endY - m_startY)*offset + m_startY, 0);
+}
+
+CRotateEffect::CRotateEffect(const TiXmlElement *node, EFFECT_TYPE effect) : CAnimEffect(node, effect)
+{
+ m_startAngle = m_endAngle = 0;
+ m_autoCenter = false;
+ node->QueryFloatAttribute("start", &m_startAngle);
+ node->QueryFloatAttribute("end", &m_endAngle);
+
+ // convert to a negative to account for our reversed Y axis (Needed for X and Z ???)
+ m_startAngle *= -1;
+ m_endAngle *= -1;
+
+ const char *centerPos = node->Attribute("center");
+ if (centerPos)
+ {
+ if (StringUtils::CompareNoCase(centerPos, "auto") == 0)
+ m_autoCenter = true;
+ else
+ {
+ std::vector<std::string> commaSeparated = StringUtils::Split(centerPos, ",");
+ if (commaSeparated.size() > 1)
+ m_center.y = (float)atof(commaSeparated[1].c_str());
+ if (!commaSeparated.empty())
+ m_center.x = (float)atof(commaSeparated[0].c_str());
+ }
+ }
+}
+
+void CRotateEffect::ApplyEffect(float offset, const CPoint &center)
+{
+ static const float degree_to_radian = 0.01745329252f;
+ if (m_autoCenter)
+ m_center = center;
+ if (m_effect == EFFECT_TYPE_ROTATE_X)
+ m_matrix.SetXRotation(((m_endAngle - m_startAngle)*offset + m_startAngle) * degree_to_radian, m_center.x, m_center.y, 1.0f);
+ else if (m_effect == EFFECT_TYPE_ROTATE_Y)
+ m_matrix.SetYRotation(((m_endAngle - m_startAngle)*offset + m_startAngle) * degree_to_radian, m_center.x, m_center.y, 1.0f);
+ else if (m_effect == EFFECT_TYPE_ROTATE_Z) // note coordinate aspect ratio is not generally square in the XY plane, so correct for it.
+ m_matrix.SetZRotation(((m_endAngle - m_startAngle)*offset + m_startAngle) * degree_to_radian, m_center.x, m_center.y, CServiceBroker::GetWinSystem()->GetGfxContext().GetScalingPixelRatio());
+}
+
+CZoomEffect::CZoomEffect(const TiXmlElement *node, const CRect &rect) : CAnimEffect(node, EFFECT_TYPE_ZOOM), m_center(CPoint(0,0))
+{
+ // effect defaults
+ m_startX = m_startY = 100;
+ m_endX = m_endY = 100;
+ m_autoCenter = false;
+
+ float startPosX = rect.x1;
+ float startPosY = rect.y1;
+ float endPosX = rect.x1;
+ float endPosY = rect.y1;
+
+ float width = std::max(rect.Width(), 0.001f);
+ float height = std::max(rect.Height(),0.001f);
+
+ const char *start = node->Attribute("start");
+ if (start)
+ {
+ std::vector<std::string> params = StringUtils::Split(start, ",");
+ if (params.size() == 1)
+ {
+ m_startX = CGUIControlFactory::ParsePosition(params[0].c_str(), rect.Width());
+ m_startY = m_startX;
+ }
+ else if (params.size() == 2)
+ {
+ m_startX = CGUIControlFactory::ParsePosition(params[0].c_str(), rect.Width());
+ m_startY = CGUIControlFactory::ParsePosition(params[1].c_str(), rect.Height());
+ }
+ else if (params.size() == 4)
+ { // format is start="x,y,width,height"
+ // use width and height from our rect to calculate our sizing
+ startPosX = CGUIControlFactory::ParsePosition(params[0].c_str(), rect.Width());
+ startPosY = CGUIControlFactory::ParsePosition(params[1].c_str(), rect.Height());
+ m_startX = CGUIControlFactory::ParsePosition(params[2].c_str(), rect.Width());
+ m_startY = CGUIControlFactory::ParsePosition(params[3].c_str(), rect.Height());
+ m_startX *= 100.0f / width;
+ m_startY *= 100.0f / height;
+ }
+ }
+ const char *end = node->Attribute("end");
+ if (end)
+ {
+ std::vector<std::string> params = StringUtils::Split(end, ",");
+ if (params.size() == 1)
+ {
+ m_endX = CGUIControlFactory::ParsePosition(params[0].c_str(), rect.Width());
+ m_endY = m_endX;
+ }
+ else if (params.size() == 2)
+ {
+ m_endX = CGUIControlFactory::ParsePosition(params[0].c_str(), rect.Width());
+ m_endY = CGUIControlFactory::ParsePosition(params[1].c_str(), rect.Height());
+ }
+ else if (params.size() == 4)
+ { // format is start="x,y,width,height"
+ // use width and height from our rect to calculate our sizing
+ endPosX = CGUIControlFactory::ParsePosition(params[0].c_str(), rect.Width());
+ endPosY = CGUIControlFactory::ParsePosition(params[1].c_str(), rect.Height());
+ m_endX = CGUIControlFactory::ParsePosition(params[2].c_str(), rect.Width());
+ m_endY = CGUIControlFactory::ParsePosition(params[3].c_str(), rect.Height());
+ m_endX *= 100.0f / width;
+ m_endY *= 100.0f / height;
+ }
+ }
+ const char *centerPos = node->Attribute("center");
+ if (centerPos)
+ {
+ if (StringUtils::CompareNoCase(centerPos, "auto") == 0)
+ m_autoCenter = true;
+ else
+ {
+ std::vector<std::string> commaSeparated = StringUtils::Split(centerPos, ",");
+ if (commaSeparated.size() > 1)
+ m_center.y = CGUIControlFactory::ParsePosition(commaSeparated[1].c_str(), rect.Height());
+ if (!commaSeparated.empty())
+ m_center.x = CGUIControlFactory::ParsePosition(commaSeparated[0].c_str(), rect.Width());
+ }
+ }
+ else
+ { // no center specified
+ // calculate the center position...
+ if (m_startX)
+ {
+ float scale = m_endX / m_startX;
+ if (scale != 1)
+ m_center.x = (endPosX - scale*startPosX) / (1 - scale);
+ }
+ if (m_startY)
+ {
+ float scale = m_endY / m_startY;
+ if (scale != 1)
+ m_center.y = (endPosY - scale*startPosY) / (1 - scale);
+ }
+ }
+}
+
+void CZoomEffect::ApplyEffect(float offset, const CPoint &center)
+{
+ if (m_autoCenter)
+ m_center = center;
+ float scaleX = ((m_endX - m_startX)*offset + m_startX) * 0.01f;
+ float scaleY = ((m_endY - m_startY)*offset + m_startY) * 0.01f;
+ m_matrix.SetScaler(scaleX, scaleY, m_center.x, m_center.y);
+}
+
+CAnimation::CAnimation()
+{
+ m_type = ANIM_TYPE_NONE;
+ m_reversible = true;
+ m_repeatAnim = ANIM_REPEAT_NONE;
+ m_currentState = ANIM_STATE_NONE;
+ m_currentProcess = ANIM_PROCESS_NONE;
+ m_queuedProcess = ANIM_PROCESS_NONE;
+ m_lastCondition = false;
+ m_length = 0;
+ m_delay = 0;
+ m_start = 0;
+ m_amount = 0;
+}
+
+CAnimation::CAnimation(const CAnimation &src)
+{
+ *this = src;
+}
+
+CAnimation::~CAnimation()
+{
+ for (unsigned int i = 0; i < m_effects.size(); i++)
+ delete m_effects[i];
+ m_effects.clear();
+}
+
+CAnimation &CAnimation::operator =(const CAnimation &src)
+{
+ if (this == &src) return *this; // same
+ m_type = src.m_type;
+ m_reversible = src.m_reversible;
+ m_condition = src.m_condition;
+ m_repeatAnim = src.m_repeatAnim;
+ m_lastCondition = src.m_lastCondition;
+ m_queuedProcess = src.m_queuedProcess;
+ m_currentProcess = src.m_currentProcess;
+ m_currentState = src.m_currentState;
+ m_start = src.m_start;
+ m_length = src.m_length;
+ m_delay = src.m_delay;
+ m_amount = src.m_amount;
+ // clear all our effects
+ for (unsigned int i = 0; i < m_effects.size(); i++)
+ delete m_effects[i];
+ m_effects.clear();
+ // and assign the others across
+ for (unsigned int i = 0; i < src.m_effects.size(); i++)
+ {
+ CAnimEffect *newEffect = NULL;
+ if (src.m_effects[i]->GetType() == CAnimEffect::EFFECT_TYPE_FADE)
+ newEffect = new CFadeEffect(*static_cast<CFadeEffect*>(src.m_effects[i]));
+ else if (src.m_effects[i]->GetType() == CAnimEffect::EFFECT_TYPE_FADE_DIFFUSE)
+ newEffect = new CFadeEffect(*static_cast<CFadeEffect*>(src.m_effects[i]));
+ else if (src.m_effects[i]->GetType() == CAnimEffect::EFFECT_TYPE_ZOOM)
+ newEffect = new CZoomEffect(*static_cast<CZoomEffect*>(src.m_effects[i]));
+ else if (src.m_effects[i]->GetType() == CAnimEffect::EFFECT_TYPE_SLIDE)
+ newEffect = new CSlideEffect(*static_cast<CSlideEffect*>(src.m_effects[i]));
+ else if (src.m_effects[i]->GetType() == CAnimEffect::EFFECT_TYPE_ROTATE_X ||
+ src.m_effects[i]->GetType() == CAnimEffect::EFFECT_TYPE_ROTATE_Y ||
+ src.m_effects[i]->GetType() == CAnimEffect::EFFECT_TYPE_ROTATE_Z)
+ newEffect = new CRotateEffect(*static_cast<CRotateEffect*>(src.m_effects[i]));
+ if (newEffect)
+ m_effects.push_back(newEffect);
+ }
+ return *this;
+}
+
+void CAnimation::Animate(unsigned int time, bool startAnim)
+{
+ // First start any queued animations
+ if (m_queuedProcess == ANIM_PROCESS_NORMAL)
+ {
+ if (m_currentProcess == ANIM_PROCESS_REVERSE)
+ m_start = time - m_amount; // reverse direction of animation
+ else
+ m_start = time;
+ m_currentProcess = ANIM_PROCESS_NORMAL;
+ }
+ else if (m_queuedProcess == ANIM_PROCESS_REVERSE)
+ {
+ if (m_currentProcess == ANIM_PROCESS_NORMAL)
+ m_start = time - (m_length - m_amount); // reverse direction of animation
+ else if (m_currentProcess == ANIM_PROCESS_NONE)
+ m_start = time;
+ m_currentProcess = ANIM_PROCESS_REVERSE;
+ }
+ // reset the queued state once we've rendered to ensure allocation has occurred
+ m_queuedProcess = ANIM_PROCESS_NONE;
+
+ // Update our animation process
+ if (m_currentProcess == ANIM_PROCESS_NORMAL)
+ {
+ if (time - m_start < m_delay)
+ {
+ m_amount = 0;
+ m_currentState = ANIM_STATE_DELAYED;
+ }
+ else if (time - m_start < m_length + m_delay)
+ {
+ m_amount = time - m_start - m_delay;
+ m_currentState = ANIM_STATE_IN_PROCESS;
+ }
+ else
+ {
+ m_amount = m_length;
+ if (m_repeatAnim == ANIM_REPEAT_PULSE && m_lastCondition)
+ { // pulsed anims auto-reverse
+ m_currentProcess = ANIM_PROCESS_REVERSE;
+ m_start = time;
+ }
+ else if (m_repeatAnim == ANIM_REPEAT_LOOP && m_lastCondition)
+ { // looped anims start over
+ m_amount = 0;
+ m_start = time;
+ }
+ else
+ m_currentState = ANIM_STATE_APPLIED;
+ }
+ }
+ else if (m_currentProcess == ANIM_PROCESS_REVERSE)
+ {
+ if (time - m_start < m_length)
+ {
+ m_amount = m_length - (time - m_start);
+ m_currentState = ANIM_STATE_IN_PROCESS;
+ }
+ else
+ {
+ m_amount = 0;
+ if (m_repeatAnim == ANIM_REPEAT_PULSE && m_lastCondition)
+ { // pulsed anims auto-reverse
+ m_currentProcess = ANIM_PROCESS_NORMAL;
+ m_start = time;
+ }
+ else
+ m_currentState = ANIM_STATE_APPLIED;
+ }
+ }
+}
+
+void CAnimation::ResetAnimation()
+{
+ m_queuedProcess = ANIM_PROCESS_NONE;
+ m_currentProcess = ANIM_PROCESS_NONE;
+ m_currentState = ANIM_STATE_NONE;
+}
+
+void CAnimation::ApplyAnimation()
+{
+ m_queuedProcess = ANIM_PROCESS_NONE;
+ if (m_repeatAnim == ANIM_REPEAT_PULSE)
+ { // pulsed anims auto-reverse
+ m_amount = m_length;
+ m_currentProcess = ANIM_PROCESS_REVERSE;
+ m_currentState = ANIM_STATE_IN_PROCESS;
+ }
+ else if (m_repeatAnim == ANIM_REPEAT_LOOP)
+ { // looped anims start over
+ m_amount = 0;
+ m_currentProcess = ANIM_PROCESS_NORMAL;
+ m_currentState = ANIM_STATE_IN_PROCESS;
+ }
+ else
+ { // set normal process, so that Calculate() knows that we're finishing for zero length effects
+ // it will be reset in RenderAnimation()
+ m_currentProcess = ANIM_PROCESS_NORMAL;
+ m_currentState = ANIM_STATE_APPLIED;
+ m_amount = m_length;
+ }
+ Calculate(CPoint());
+}
+
+void CAnimation::Calculate(const CPoint &center)
+{
+ for (unsigned int i = 0; i < m_effects.size(); i++)
+ {
+ CAnimEffect *effect = m_effects[i];
+ if (effect->GetLength())
+ effect->Calculate(m_delay + m_amount, center);
+ else
+ { // effect has length zero, so either apply complete
+ if (m_currentProcess == ANIM_PROCESS_NORMAL)
+ effect->ApplyState(ANIM_STATE_APPLIED, center);
+ else
+ effect->ApplyState(ANIM_STATE_NONE, center);
+ }
+ }
+}
+
+void CAnimation::RenderAnimation(TransformMatrix &matrix, const CPoint &center)
+{
+ if (m_currentProcess != ANIM_PROCESS_NONE)
+ Calculate(center);
+ // If we have finished an animation, reset the animation state
+ // We do this here (rather than in Animate()) as we need the
+ // currentProcess information in the UpdateStates() function of the
+ // window and control classes.
+ if (m_currentState == ANIM_STATE_APPLIED)
+ {
+ m_currentProcess = ANIM_PROCESS_NONE;
+ m_queuedProcess = ANIM_PROCESS_NONE;
+ }
+ if (m_currentState != ANIM_STATE_NONE)
+ {
+ for (unsigned int i = 0; i < m_effects.size(); i++)
+ matrix *= m_effects[i]->GetTransform();
+ }
+}
+
+void CAnimation::QueueAnimation(ANIMATION_PROCESS process)
+{
+ m_queuedProcess = process;
+}
+
+CAnimation CAnimation::CreateFader(float start, float end, unsigned int delay, unsigned int length, ANIMATION_TYPE type)
+{
+ CAnimation anim;
+ anim.m_type = type;
+ anim.m_delay = delay;
+ anim.m_length = length;
+ anim.m_effects.push_back(new CFadeEffect(start, end, delay, length));
+ return anim;
+}
+
+bool CAnimation::CheckCondition()
+{
+ return !m_condition || m_condition->Get(INFO::DEFAULT_CONTEXT);
+}
+
+void CAnimation::UpdateCondition(const CGUIListItem *item)
+{
+ if (!m_condition)
+ return;
+ bool condition = m_condition->Get(INFO::DEFAULT_CONTEXT, item);
+ if (condition && !m_lastCondition)
+ QueueAnimation(ANIM_PROCESS_NORMAL);
+ else if (!condition && m_lastCondition)
+ {
+ if (m_reversible)
+ QueueAnimation(ANIM_PROCESS_REVERSE);
+ else
+ ResetAnimation();
+ }
+ m_lastCondition = condition;
+}
+
+void CAnimation::SetInitialCondition()
+{
+ m_lastCondition = m_condition ? m_condition->Get(INFO::DEFAULT_CONTEXT) : false;
+ if (m_lastCondition)
+ ApplyAnimation();
+ else
+ ResetAnimation();
+}
+
+void CAnimation::Create(const TiXmlElement *node, const CRect &rect, int context)
+{
+ if (!node || !node->FirstChild())
+ return;
+
+ // conditions and reversibility
+ const char *condition = node->Attribute("condition");
+ if (condition)
+ m_condition = CServiceBroker::GetGUI()->GetInfoManager().Register(condition, context);
+ const char *reverse = node->Attribute("reversible");
+ if (reverse && StringUtils::CompareNoCase(reverse, "false") == 0)
+ m_reversible = false;
+
+ const TiXmlElement *effect = node->FirstChildElement("effect");
+
+ std::string type = node->FirstChild()->Value();
+ m_type = ANIM_TYPE_CONDITIONAL;
+ if (effect) // new layout
+ type = XMLUtils::GetAttribute(node, "type");
+
+ if (StringUtils::StartsWithNoCase(type, "visible")) m_type = ANIM_TYPE_VISIBLE;
+ else if (StringUtils::EqualsNoCase(type, "hidden")) m_type = ANIM_TYPE_HIDDEN;
+ else if (StringUtils::EqualsNoCase(type, "focus")) m_type = ANIM_TYPE_FOCUS;
+ else if (StringUtils::EqualsNoCase(type, "unfocus")) m_type = ANIM_TYPE_UNFOCUS;
+ else if (StringUtils::EqualsNoCase(type, "windowopen")) m_type = ANIM_TYPE_WINDOW_OPEN;
+ else if (StringUtils::EqualsNoCase(type, "windowclose")) m_type = ANIM_TYPE_WINDOW_CLOSE;
+ // sanity check
+ if (m_type == ANIM_TYPE_CONDITIONAL)
+ {
+ if (!m_condition)
+ {
+ CLog::Log(LOGERROR, "Control has invalid animation type (no condition or no type)");
+ return;
+ }
+
+ // pulsed or loop animations
+ const char *pulse = node->Attribute("pulse");
+ if (pulse && StringUtils::CompareNoCase(pulse, "true") == 0)
+ m_repeatAnim = ANIM_REPEAT_PULSE;
+ const char *loop = node->Attribute("loop");
+ if (loop && StringUtils::CompareNoCase(loop, "true") == 0)
+ m_repeatAnim = ANIM_REPEAT_LOOP;
+ }
+
+ if (!effect)
+ { // old layout:
+ // <animation effect="fade" start="0" end="100" delay="10" time="2000" condition="blahdiblah" reversible="false">focus</animation>
+ std::string type = XMLUtils::GetAttribute(node, "effect");
+ AddEffect(type, node, rect);
+ }
+ while (effect)
+ { // new layout:
+ // <animation type="focus" condition="blahdiblah" reversible="false">
+ // <effect type="fade" start="0" end="100" delay="10" time="2000" />
+ // ...
+ // </animation>
+ std::string type = XMLUtils::GetAttribute(effect, "type");
+ AddEffect(type, effect, rect);
+ effect = effect->NextSiblingElement("effect");
+ }
+ // compute the minimum delay and maximum length
+ m_delay = 0xffffffff;
+ unsigned int total = 0;
+ for (const auto& i : m_effects)
+ {
+ m_delay = std::min(m_delay, i->GetDelay());
+ total = std::max(total, i->GetLength());
+ }
+ m_length = total - m_delay;
+}
+
+void CAnimation::AddEffect(const std::string &type, const TiXmlElement *node, const CRect &rect)
+{
+ CAnimEffect *effect = NULL;
+ if (StringUtils::EqualsNoCase(type, "fade"))
+ effect = new CFadeEffect(node, m_type < 0, CAnimEffect::EFFECT_TYPE_FADE);
+ else if (StringUtils::EqualsNoCase(type, "fadediffuse"))
+ effect = new CFadeEffect(node, m_type < 0, CAnimEffect::EFFECT_TYPE_FADE_DIFFUSE);
+ else if (StringUtils::EqualsNoCase(type, "slide"))
+ effect = new CSlideEffect(node);
+ else if (StringUtils::EqualsNoCase(type, "rotate"))
+ effect = new CRotateEffect(node, CAnimEffect::EFFECT_TYPE_ROTATE_Z);
+ else if (StringUtils::EqualsNoCase(type, "rotatey"))
+ effect = new CRotateEffect(node, CAnimEffect::EFFECT_TYPE_ROTATE_Y);
+ else if (StringUtils::EqualsNoCase(type, "rotatex"))
+ effect = new CRotateEffect(node, CAnimEffect::EFFECT_TYPE_ROTATE_X);
+ else if (StringUtils::EqualsNoCase(type, "zoom"))
+ effect = new CZoomEffect(node, rect);
+
+ if (effect)
+ m_effects.push_back(effect);
+}
+
+CScroller::CScroller(unsigned int duration /* = 200 */, std::shared_ptr<Tweener> tweener /* = NULL */)
+{
+ m_scrollValue = 0;
+ m_delta = 0;
+ m_startTime = 0;
+ m_startPosition = 0;
+ m_hasResumePoint = false;
+ m_duration = duration > 0 ? duration : 1;
+ m_pTweener = std::move(tweener);
+}
+
+CScroller::CScroller(const CScroller& right)
+{
+ m_pTweener.reset();
+ *this = right;
+}
+
+CScroller& CScroller::operator=(const CScroller &right)
+{
+ if (&right == this) return *this;
+
+ m_scrollValue = right.m_scrollValue;
+ m_delta = right.m_delta;
+ m_startTime = right.m_startTime;
+ m_startPosition = right.m_startPosition;
+ m_hasResumePoint = right.m_hasResumePoint;
+ m_duration = right.m_duration;
+ m_pTweener = right.m_pTweener;
+ return *this;
+}
+
+CScroller::~CScroller() = default;
+
+void CScroller::ScrollTo(float endPos)
+{
+ float delta = endPos - m_scrollValue;
+ // if there is scrolling running in same direction - set resume point
+ m_hasResumePoint = m_delta != 0 && delta * m_delta > 0 && m_pTweener ? m_pTweener->HasResumePoint() : false;
+
+ m_delta = delta;
+ m_startPosition = m_scrollValue;
+ m_startTime = 0;
+}
+
+float CScroller::Tween(float progress)
+{
+ if (m_pTweener)
+ {
+ if (m_hasResumePoint) // tweener with in_and_out easing
+ {
+ // time linear transformation (y = a*x + b): 0 -> resumePoint and 1 -> 1
+ // resumePoint = a * 0 + b and 1 = a * 1 + b
+ // a = 1 - resumePoint , b = resumePoint
+ // our resume point is 0.5
+ // a = 0.5 , b = 0.5
+ progress = 0.5f * progress + 0.5f;
+ // tweener value linear transformation (y = a*x + b): resumePointValue -> 0 and 1 -> 1
+ // 0 = a * resumePointValue and 1 = a * 1 + b
+ // a = 1 / ( 1 - resumePointValue) , b = -resumePointValue / (1 - resumePointValue)
+ // we assume resumePointValue = Tween(resumePoint) = Tween(0.5) = 0.5
+ // (this is true for tweener with in_and_out easing - it's rotationally symmetric about (0.5,0.5) continuous function)
+ // a = 2 , b = -1
+ return (2 * m_pTweener->Tween(progress, 0, 1, 1) - 1);
+ }
+ else
+ return m_pTweener->Tween(progress, 0, 1, 1);
+ }
+ else
+ return progress;
+}
+
+bool CScroller::Update(unsigned int time)
+{
+ if (!m_startTime)
+ m_startTime = time;
+ if (m_delta != 0)
+ {
+ if (time - m_startTime >= m_duration) // we are finished
+ {
+ m_scrollValue = m_startPosition + m_delta;
+ m_startTime = 0;
+ m_hasResumePoint = false;
+ m_delta = 0;
+ m_startPosition = 0;
+ }
+ else
+ m_scrollValue = m_startPosition + Tween((float)(time - m_startTime) / m_duration) * m_delta;
+ return true;
+ }
+ else
+ return false;
+}
diff --git a/xbmc/guilib/VisibleEffect.h b/xbmc/guilib/VisibleEffect.h
new file mode 100644
index 0000000..1cc4a09
--- /dev/null
+++ b/xbmc/guilib/VisibleEffect.h
@@ -0,0 +1,269 @@
+/*
+ * 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
+
+enum ANIMATION_PROCESS { ANIM_PROCESS_NONE = 0, ANIM_PROCESS_NORMAL, ANIM_PROCESS_REVERSE };
+enum ANIMATION_STATE { ANIM_STATE_NONE = 0, ANIM_STATE_DELAYED, ANIM_STATE_IN_PROCESS, ANIM_STATE_APPLIED };
+
+// forward definitions
+
+class TiXmlElement;
+class Tweener;
+class CGUIListItem;
+
+#include "interfaces/info/InfoBool.h"
+#include "utils/ColorUtils.h"
+#include "utils/Geometry.h" // for CPoint, CRect
+#include "utils/TransformMatrix.h" // needed for the TransformMatrix member
+
+#include <memory>
+#include <string>
+#include <vector>
+
+enum ANIMATION_TYPE
+{
+ ANIM_TYPE_UNFOCUS = -3,
+ ANIM_TYPE_HIDDEN,
+ ANIM_TYPE_WINDOW_CLOSE,
+ ANIM_TYPE_NONE,
+ ANIM_TYPE_WINDOW_OPEN,
+ ANIM_TYPE_VISIBLE,
+ ANIM_TYPE_FOCUS,
+ ANIM_TYPE_CONDITIONAL // for animations triggered by a condition change
+};
+
+class CAnimEffect
+{
+public:
+ enum EFFECT_TYPE
+ {
+ EFFECT_TYPE_NONE = 0,
+ EFFECT_TYPE_FADE,
+ EFFECT_TYPE_FADE_DIFFUSE,
+ EFFECT_TYPE_SLIDE,
+ EFFECT_TYPE_ROTATE_X,
+ EFFECT_TYPE_ROTATE_Y,
+ EFFECT_TYPE_ROTATE_Z,
+ EFFECT_TYPE_ZOOM
+ };
+
+ CAnimEffect(const TiXmlElement *node, EFFECT_TYPE effect);
+ CAnimEffect(unsigned int delay, unsigned int length, EFFECT_TYPE effect);
+ CAnimEffect(const CAnimEffect &src);
+
+ virtual ~CAnimEffect();
+ CAnimEffect& operator=(const CAnimEffect &src);
+
+ void Calculate(unsigned int time, const CPoint &center);
+ void ApplyState(ANIMATION_STATE state, const CPoint &center);
+
+ unsigned int GetDelay() const { return m_delay; }
+ unsigned int GetLength() const { return m_delay + m_length; }
+ const TransformMatrix& GetTransform() const { return m_matrix; }
+ EFFECT_TYPE GetType() const { return m_effect; }
+
+ static std::shared_ptr<Tweener> GetTweener(const TiXmlElement *pAnimationNode);
+protected:
+ TransformMatrix m_matrix;
+ EFFECT_TYPE m_effect;
+
+private:
+ virtual void ApplyEffect(float offset, const CPoint &center)=0;
+
+ // timing variables
+ unsigned int m_length;
+ unsigned int m_delay;
+
+ std::shared_ptr<Tweener> m_pTweener;
+};
+
+class CFadeEffect : public CAnimEffect
+{
+public:
+ CFadeEffect(const TiXmlElement* node, bool reverseDefaults, EFFECT_TYPE effect);
+ CFadeEffect(float start, float end, unsigned int delay, unsigned int length);
+ CFadeEffect(UTILS::COLOR::Color start,
+ UTILS::COLOR::Color end,
+ unsigned int delay,
+ unsigned int length);
+ ~CFadeEffect() override = default;
+private:
+ void ApplyEffect(float offset, const CPoint &center) override;
+
+ float m_startAlpha;
+ float m_endAlpha;
+ UTILS::COLOR::ColorFloats m_startColor;
+ UTILS::COLOR::ColorFloats m_endColor;
+};
+
+class CSlideEffect : public CAnimEffect
+{
+public:
+ explicit CSlideEffect(const TiXmlElement *node);
+ ~CSlideEffect() override = default;
+private:
+ void ApplyEffect(float offset, const CPoint &center) override;
+
+ float m_startX;
+ float m_startY;
+ float m_endX;
+ float m_endY;
+};
+
+class CRotateEffect : public CAnimEffect
+{
+public:
+ CRotateEffect(const TiXmlElement *node, EFFECT_TYPE effect);
+ ~CRotateEffect() override = default;
+private:
+ void ApplyEffect(float offset, const CPoint &center) override;
+
+ float m_startAngle;
+ float m_endAngle;
+
+ bool m_autoCenter;
+ CPoint m_center;
+};
+
+class CZoomEffect : public CAnimEffect
+{
+public:
+ CZoomEffect(const TiXmlElement *node, const CRect &rect);
+ ~CZoomEffect() override = default;
+private:
+ void ApplyEffect(float offset, const CPoint &center) override;
+
+ float m_startX;
+ float m_startY;
+ float m_endX;
+ float m_endY;
+
+ bool m_autoCenter;
+ CPoint m_center;
+};
+
+class CAnimation
+{
+public:
+ CAnimation();
+ CAnimation(const CAnimation &src);
+
+ virtual ~CAnimation();
+
+ CAnimation& operator=(const CAnimation &src);
+
+ static CAnimation CreateFader(float start, float end, unsigned int delay, unsigned int length, ANIMATION_TYPE type = ANIM_TYPE_NONE);
+
+ void Create(const TiXmlElement *node, const CRect &rect, int context);
+
+ void Animate(unsigned int time, bool startAnim);
+ void ResetAnimation();
+ void ApplyAnimation();
+ inline void RenderAnimation(TransformMatrix &matrix)
+ {
+ RenderAnimation(matrix, CPoint());
+ }
+ void RenderAnimation(TransformMatrix &matrix, const CPoint &center);
+ void QueueAnimation(ANIMATION_PROCESS process);
+
+ inline bool IsReversible() const { return m_reversible; }
+ inline ANIMATION_TYPE GetType() const { return m_type; }
+ inline ANIMATION_STATE GetState() const { return m_currentState; }
+ inline ANIMATION_PROCESS GetProcess() const { return m_currentProcess; }
+ inline ANIMATION_PROCESS GetQueuedProcess() const { return m_queuedProcess; }
+
+ bool CheckCondition();
+ void UpdateCondition(const CGUIListItem *item = NULL);
+ void SetInitialCondition();
+
+private:
+ void Calculate(const CPoint &point);
+ void AddEffect(const std::string &type, const TiXmlElement *node, const CRect &rect);
+
+ enum ANIM_REPEAT { ANIM_REPEAT_NONE = 0, ANIM_REPEAT_PULSE, ANIM_REPEAT_LOOP };
+
+ // type of animation
+ ANIMATION_TYPE m_type;
+ bool m_reversible;
+ INFO::InfoPtr m_condition;
+
+ // conditional anims can repeat
+ ANIM_REPEAT m_repeatAnim;
+ bool m_lastCondition;
+
+ // state of animation
+ ANIMATION_PROCESS m_queuedProcess;
+ ANIMATION_PROCESS m_currentProcess;
+ ANIMATION_STATE m_currentState;
+
+ // timing of animation
+ unsigned int m_start;
+ unsigned int m_length;
+ unsigned int m_delay;
+ unsigned int m_amount;
+
+ std::vector<CAnimEffect *> m_effects;
+};
+
+/**
+ * Class used to handle scrolling, allow using tweeners.
+ * Usage:
+ * start scrolling using ScrollTo() method / stop scrolling using Stop() method
+ * update scroll value each frame with current time using Update() method
+ * get/set scroll value using GetValue()/SetValue()
+ */
+class CScroller
+{
+public:
+ CScroller(unsigned int duration = 200, std::shared_ptr<Tweener> tweener = std::shared_ptr<Tweener>());
+ CScroller(const CScroller& right);
+ CScroller& operator=(const CScroller &src);
+ ~CScroller();
+
+ /**
+ * Set target value scroller will be scrolling to
+ * @param endPos target
+ */
+ void ScrollTo(float endPos);
+
+ /**
+ * Immediately stop scrolling
+ */
+ void Stop() { m_delta = 0; }
+ /**
+ * Update the scroller to where it would be at the given time point, calculating a new Value.
+ * @param time time point
+ * @return True if we are scrolling at given time point
+ */
+ bool Update(unsigned int time);
+
+ /**
+ * Value of scroll
+ */
+ float GetValue() const { return m_scrollValue; }
+ void SetValue(float scrollValue) { m_scrollValue = scrollValue; }
+
+ bool IsScrolling() const { return m_delta != 0; }
+ bool IsScrollingUp() const { return m_delta < 0; }
+ bool IsScrollingDown() const { return m_delta > 0; }
+
+ unsigned int GetDuration() const { return m_duration; }
+
+private:
+ float Tween(float progress);
+
+ float m_scrollValue;
+ float m_delta; //!< Brief distance that we have to travel during scroll
+ float m_startPosition; //!< Brief starting position of scroll
+ bool m_hasResumePoint; //!< Brief check if we should tween from middle of the tween
+ unsigned int m_startTime; //!< Brief starting time of scroll
+
+ unsigned int m_duration; //!< Brief duration of scroll
+ std::shared_ptr<Tweener> m_pTweener;
+};
diff --git a/xbmc/guilib/WindowIDs.dox b/xbmc/guilib/WindowIDs.dox
new file mode 100644
index 0000000..68fdc7d
--- /dev/null
+++ b/xbmc/guilib/WindowIDs.dox
@@ -0,0 +1,145 @@
+/*!
+
+\page window_ids WindowIDs
+
+This page shows the window names, the window definition, the window ID and the specific XML file in use.
+
+@note Window names are case-insensitive
+
+| Name | Definition | WindowID | XML Filename | Revision history |
+|:------------------------|:-------------------------------------|:----------|:------------------------------------|:-----------------------------------|
+| Home | WINDOW_HOME | 10000 | Home.xml | |
+| Programs | WINDOW_PROGRAMS | 10001 | MyPrograms.xml | |
+| Pictures | WINDOW_PICTURES | 10002 | MyPics.xml | |
+| FileManager | WINDOW_FILES | 10003 | FileManager.xml | |
+| Settings | WINDOW_SETTINGS_MENU | 10004 | Settings.xml | |
+| SystemInfo | WINDOW_SYSTEM_INFORMATION | 10007 | SettingsSystemInfo.xml | |
+| ScreenCalibration | WINDOW_SCREEN_CALIBRATION | 10011 | SettingsScreenCalibration.xml | |
+| SystemSettings | WINDOW_SETTINGS_SYSTEM | 10016 | SettingsCategory.xml | |
+| ServiceSettings | WINDOW_SETTINGS_SERVICE | 10018 | SettingsCategory.xml | |
+| PVRSettings | WINDOW_SETTINGS_MYPVR | 10021 | SettingsCategory.xml | |
+| GameSettings | WINDOW_SETTINGS_MYGAMES | 10022 | SettingsCategory.xml | |
+| Videos | WINDOW_VIDEO_NAV | 10025 | MyVideoNav.xml | |
+| VideoPlaylist | WINDOW_VIDEO_PLAYLIST | 10028 | MyPlaylist.xml | |
+| LoginScreen | WINDOW_LOGIN_SCREEN | 10029 | LoginScreen.xml | |
+| PlayerSettings | WINDOW_SETTINGS_PLAYER | 10030 | SettingsCategory.xml | |
+| MediaSettings | WINDOW_SETTINGS_MEDIA | 10031 | SettingsCategory.xml | |
+| InterfaceSettings | WINDOW_SETTINGS_INTERFACE | 10032 | SettingsCategory.xml | |
+| Profiles | WINDOW_SETTINGS_PROFILES | 10034 | SettingsProfile.xml | |
+| SkinSettings | WINDOW_SKIN_SETTINGS | 10035 | SkinSettings.xml | |
+| AddonBrowser | WINDOW_ADDON_BROWSER | 10040 | AddonBrowser.xml | |
+| EventLog | WINDOW_EVENT_LOG | 10050 | EventLog.xml | |
+| Pointer | WINDOW_DIALOG_POINTER | 10099 | Pointer.xml | |
+| YesNoDialog | WINDOW_DIALOG_YES_NO | 10100 | DialogConfirm.xml | |
+| ProgressDialog | WINDOW_DIALOG_PROGRESS | 10101 | DialogConfirm.xml | |
+| VirtualKeyboard | WINDOW_DIALOG_KEYBOARD | 10103 | DialogKeyboard.xml | |
+| VolumeBar | WINDOW_DIALOG_VOLUME_BAR | 10104 | DialogVolumeBar.xml | |
+| SubMenu | WINDOW_DIALOG_SUB_MENU | 10105 | DialogSubMenu.xml | |
+| ContextMenu | WINDOW_DIALOG_CONTEXT_MENU | 10106 | DialogContextMenu.xml | |
+| Notification | WINDOW_DIALOG_KAI_TOAST | 10107 | DialogNotification.xml | |
+| NumericInput | WINDOW_DIALOG_NUMERIC | 10109 | DialogNumeric.xml | |
+| GamepadInput | WINDOW_DIALOG_GAMEPAD | 10110 | DialogSelect.xml | |
+| ShutdownMenu | WINDOW_DIALOG_BUTTON_MENU | 10111 | DialogButtonMenu.xml | |
+| PlayerControls | WINDOW_DIALOG_PLAYER_CONTROLS | 10114 | PlayerControls.xml | |
+| SeekBar | WINDOW_DIALOG_SEEK_BAR | 10115 | DialogSeekBar.xml | |
+| PlayerProcessInfo | WINDOW_DIALOG_PLAYER_PROCESS_INFO | 10116 | DialogPlayerProcessInfo.xml | |
+| MusicOSD | WINDOW_DIALOG_MUSIC_OSD | 10120 | MusicOSD.xml | |
+| | WINDOW_DIALOG_VIS_SETTINGS | 10121 | | |
+| VisualisationPresetList | WINDOW_DIALOG_VIS_PRESET_LIST | 10122 | DialogSelect.xml | |
+| OSDVideoSettings | WINDOW_DIALOG_VIDEO_OSD_SETTINGS | 10123 | DialogSettings.xml | |
+| OSDAudioSettings | WINDOW_DIALOG_AUDIO_OSD_SETTINGS | 10124 | DialogSettings.xml | |
+| VideoBookmarks | WINDOW_DIALOG_VIDEO_BOOKMARKS | 10125 | VideoOSDBookmarks.xml | |
+| FileBrowser | WINDOW_DIALOG_FILE_BROWSER | 10126 | FileBrowser.xml | |
+| NetworkSetup | WINDOW_DIALOG_NETWORK_SETUP | 10128 | DialogSettings.xml | |
+| MediaSource | WINDOW_DIALOG_MEDIA_SOURCE | 10129 | DialogMediaSource.xml | |
+| ProfileSettings | WINDOW_DIALOG_PROFILE_SETTINGS | 10130 | DialogSettings.xml | |
+| LockSettings | WINDOW_DIALOG_LOCK_SETTINGS | 10131 | DialogSettings.xml | |
+| ContentSettings | WINDOW_DIALOG_CONTENT_SETTINGS | 10132 | DialogSettings.xml | |
+| LibexportSettings | WINDOW_DIALOG_LIBEXPORT_SETTINGS | 10133 | DialogSettings.xml | |
+| Favourites | WINDOW_DIALOG_FAVOURITES | 10134 | DialogFavourites.xml | @deprecated Dialog **Favourites** is deprecated and is scheduled for removal in v21.<p></p> @skinning_v20 Deprecated. Please use **FavouritesBrowser** window instead. <p></p> |
+| SongInformation | WINDOW_DIALOG_SONG_INFO | 10135 | DialogMusicInfo.xml | |
+| SmartPlaylistEditor | WINDOW_DIALOG_SMART_PLAYLIST_EDITOR | 10136 | SmartPlaylistEditor.xml | |
+| SmartPlaylistRule | WINDOW_DIALOG_SMART_PLAYLIST_RULE | 10137 | SmartPlaylistRule.xml | |
+| BusyDialog | WINDOW_DIALOG_BUSY | 10138 | DialogBusy.xml | |
+| PictureInfo | WINDOW_DIALOG_PICTURE_INFO | 10139 | DialogPictureInfo.xml | |
+| AddonSettings | WINDOW_DIALOG_ADDON_SETTINGS | 10140 | DialogAddonSettings.xml | |
+| FullscreenInfo | WINDOW_DIALOG_FULLSCREEN_INFO | 10142 | DialogFullScreenInfo.xml | |
+| SliderDialog | WINDOW_DIALOG_SLIDER | 10145 | DialogSlider.xml | |
+| AddonInformation | WINDOW_DIALOG_ADDON_INFO | 10146 | DialogAddonInfo.xml | |
+| TextViewer | WINDOW_DIALOG_TEXT_VIEWER | 10147 | DialogTextViewer.xml | |
+| | WINDOW_DIALOG_PLAY_EJECT | 10148 | DialogConfirm.xml | |
+| | WINDOW_DIALOG_PERIPHERALS | 10149 | DialogSelect.xml | |
+| PeripheralSettings | WINDOW_DIALOG_PERIPHERAL_SETTINGS | 10150 | DialogSettings.xml | |
+| ExtendedProgressDialog | WINDOW_DIALOG_EXT_PROGRESS | 10151 | DialogExtendedProgressBar.xml | |
+| MediaFilter | WINDOW_DIALOG_MEDIA_FILTER | 10152 | DialogSettings.xml | |
+| SubtitleSearch | WINDOW_DIALOG_SUBTITLES | 10153 | DialogSubtitles.xml | |
+| | WINDOW_DIALOG_KEYBOARD_TOUCH | 10156 | | |
+| OSDCMSSettings | WINDOW_DIALOG_CMS_OSD_SETTINGS | 10157 | DialogSettings.xml | |
+| InfoproviderSettings | WINDOW_DIALOG_INFOPROVIDER_SETTINGS | 10158 | DialogSettings.xml | |
+| OSDSubtitleSettings | WINDOW_DIALOG_SUBTITLE_OSD_SETTINGS | 10159 | DialogSettings.xml | |
+| BusyDialogNoCancel | WINDOW_DIALOG_BUSY_NOCANCEL | 10160 | DialogBusy.xml | |
+| MusicPlaylist | WINDOW_MUSIC_PLAYLIST | 10500 | MyPlaylist.xml | |
+| Music | WINDOW_MUSIC_NAV | 10502 | MyMusicNav.xml | |
+| MusicPlaylistEditor | WINDOW_MUSIC_PLAYLIST_EDITOR | 10503 | MyMusicPlaylistEditor.xml | |
+| Teletext | WINDOW_DIALOG_OSD_TELETEXT | 10550 | | |
+| PVRGuideInfo | WINDOW_DIALOG_PVR_GUIDE_INFO | 10600 | DialogPVRInfo.xml | |
+| PVRRecordingInfo | WINDOW_DIALOG_PVR_RECORDING_INFO | 10601 | DialogPVRInfo.xml | |
+| PVRTimerSetting | WINDOW_DIALOG_PVR_TIMER_SETTING | 10602 | DialogSettings.xml | |
+| PVRGroupManager | WINDOW_DIALOG_PVR_GROUP_MANAGER | 10603 | DialogPVRGroupManager.xml | |
+| PVRChannelManager | WINDOW_DIALOG_PVR_CHANNEL_MANAGER | 10604 | DialogPVRChannelManager.xml | |
+| PVRGuideSearch | WINDOW_DIALOG_PVR_GUIDE_SEARCH | 10605 | DialogPVRGuideSearch.xml | |
+| PVRChannelScan | WINDOW_DIALOG_PVR_CHANNEL_SCAN | 10606 | none (unused) | |
+| PVRUpdateProgress | WINDOW_DIALOG_PVR_UPDATE_PROGRESS | 10607 | none (unused) | |
+| PVROSDChannels | WINDOW_DIALOG_PVR_OSD_CHANNELS | 10608 | DialogPVRChannelsOSD.xml | |
+| PVRChannelGuide | WINDOW_DIALOG_PVR_CHANNEL_GUIDE | 10609 | DialogPVRChannelGuide.xml | |
+| PVRRadioRDSInfo | WINDOW_DIALOG_PVR_RADIO_RDS_INFO | 10610 | DialogPVRRadioRDSInfo.xml | |
+| PVRRecordingSettings | WINDOW_DIALOG_PVR_RECORDING_SETTING | 10611 | DialogSettings.xml | |
+| | WINDOW_DIALOG_PVR_CLIENT_PRIORITIES | 10612 | DialogSettings.xml | |
+| TVChannels | WINDOW_TV_CHANNELS | 10700 | MyPVRChannels.xml | |
+| TVRecordings | WINDOW_TV_RECORDINGS | 10701 | MyPVRRecordings.xml | |
+| TVGuide | WINDOW_TV_GUIDE | 10702 | MyPVRGuide.xml | |
+| TVTimers | WINDOW_TV_TIMERS | 10703 | MyPVRTimers.xml | |
+| TVSearch | WINDOW_TV_SEARCH | 10704 | MyPVRSearch.xml | |
+| RadioChannels | WINDOW_RADIO_CHANNELS | 10705 | MyPVRChannels.xml | |
+| RadioRecordings | WINDOW_RADIO_RECORDINGS | 10706 | MyPVRRecordings.xml | |
+| RadioGuide | WINDOW_RADIO_GUIDE | 10707 | MyPVRGuide.xml | |
+| RadioTimers | WINDOW_RADIO_TIMERS | 10708 | MyPVRTimers.xml | |
+| RadioSearch | WINDOW_RADIO_SEARCH | 10709 | MyPVRSearch.xml | |
+| TVTimerRules | WINDOW_TV_TIMER_RULES | 10710 | MyPVRTimers.xml | |
+| RadioTimerRules | WINDOW_RADIO_TIMER_RULES | 10711 | MyPVRTimers.xml | |
+| FullscreenLiveTV | WINDOW_FULLSCREEN_LIVETV | 10800 | None (shortcut to fullscreenvideo) | |
+| FullscreenRadio | WINDOW_FULLSCREEN_RADIO | 10801 | None (shortcut to visualisation) | |
+| FullscreenLivetvPreview | WINDOW_FULLSCREEN_LIVETV_PREVIEW | 10802 | None (shortcut to fullscreenlivetv) | |
+| FullscreenRadioPreview | WINDOW_FULLSCREEN_RADIO_PREVIEW | 10803 | None (shortcut to fullscreenradio) | |
+| FullscreenLivetvInput | WINDOW_FULLSCREEN_LIVETV_INPUT | 10804 | None (shortcut to fullscreenlivetv) | |
+| FullscreenRadioInput | WINDOW_FULLSCREEN_RADIO_INPUT | 10805 | None (shortcut to fullscreenradio) | |
+| GameControllers | WINDOW_DIALOG_GAME_CONTROLLERS | 10820 | DialogGameControllers.xml | |
+| Games | WINDOW_GAMES | 10821 | MyGames.xml | |
+| GameOSD | WINDOW_DIALOG_GAME_OSD | 10822 | GameOSD.xml | |
+| GameVideoFilter | WINDOW_DIALOG_GAME_VIDEO_FILTER | 10823 | DialogSelect.xml | |
+| GameStretchMode | WINDOW_DIALOG_GAME_STRETCH_MODE | 10824 | DialogSelect.xml | |
+| GameVolume | WINDOW_DIALOG_GAME_VOLUME | 10825 | DialogVolumeBar.xml | |
+| GameAdvancedSettings | WINDOW_DIALOG_GAME_ADVANCED_SETTINGS | 10826 | DialogAddonSettings.xml | |
+| GameVideoRotation | WINDOW_DIALOG_GAME_VIDEO_ROTATION | 10827 | DialogSelect.xml | |
+| Custom Skin Windows | - | - | custom*.xml | |
+| SelectDialog | WINDOW_DIALOG_SELECT | 12000 | DialogSelect.xml | |
+| MusicInformation | WINDOW_DIALOG_MUSIC_INFO | 12001 | DialogMusicInfo.xml | |
+| OKDialog | WINDOW_DIALOG_OK | 12002 | DialogConfirm.xml | |
+| MovieInformation | WINDOW_DIALOG_VIDEO_INFO | 12003 | DialogVideoInfo.xml | |
+| FullscreenVideo | WINDOW_FULLSCREEN_VIDEO | 12005 | VideoFullScreen.xml | |
+| Visualisation | WINDOW_VISUALISATION | 12006 | MusicVisualisation.xml | |
+| Slideshow | WINDOW_SLIDESHOW | 12007 | SlideShow.xml | |
+| DialogColorPicker | WINDOW_DIALOG_COLOR_PICKER | 12008 | DialogColorPicker.xml | |
+| Weather | WINDOW_WEATHER | 12600 | MyWeather.xml | |
+| Screensaver | WINDOW_SCREENSAVER | 12900 | none | |
+| VideoOSD | WINDOW_DIALOG_VIDEO_OSD | 12901 | VideoOSD.xml | |
+| VideoMenu | WINDOW_VIDEO_MENU | 12902 | none | |
+| VideoTimeSeek | WINDOW_VIDEO_TIME_SEEK | 12905 | none | |
+| FullscreenGame | WINDOW_FULLSCREEN_GAME | 12906 | none | |
+| Splash | WINDOW_SPLASH | 12997 | none | |
+| StartWindow | WINDOW_START | 12998 | shortcut to the current startwindow | |
+| Startup | WINDOW_STARTUP_ANIM | 12999 | Startup.xml | |
+| FavouritesBrowser | WINDOW_FAVOURITES | 10060 | MyFavourites.xml | @skinning_v20 **New window** FavouritesBrowser, replaces the old Favourites dialog.<p></p> |
+
+
+*/
diff --git a/xbmc/guilib/WindowIDs.h b/xbmc/guilib/WindowIDs.h
new file mode 100644
index 0000000..02ff8eb
--- /dev/null
+++ b/xbmc/guilib/WindowIDs.h
@@ -0,0 +1,193 @@
+/*
+ * 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
+
+// Window ID defines to make the code a bit more readable
+#define WINDOW_INVALID 9999 // do not change. value is used to avoid include in headers.
+#define WINDOW_HOME 10000
+#define WINDOW_PROGRAMS 10001
+#define WINDOW_PICTURES 10002
+#define WINDOW_FILES 10003
+#define WINDOW_SETTINGS_MENU 10004
+#define WINDOW_SYSTEM_INFORMATION 10007
+#define WINDOW_SCREEN_CALIBRATION 10011
+
+#define WINDOW_SETTINGS_START 10016
+#define WINDOW_SETTINGS_SYSTEM 10016
+#define WINDOW_SETTINGS_SERVICE 10018
+
+#define WINDOW_SETTINGS_MYPVR 10021
+#define WINDOW_SETTINGS_MYGAMES 10022
+
+#define WINDOW_VIDEO_NAV 10025
+#define WINDOW_VIDEO_PLAYLIST 10028
+
+#define WINDOW_LOGIN_SCREEN 10029
+
+#define WINDOW_SETTINGS_PLAYER 10030
+#define WINDOW_SETTINGS_MEDIA 10031
+#define WINDOW_SETTINGS_INTERFACE 10032
+
+#define WINDOW_SETTINGS_PROFILES 10034
+#define WINDOW_SKIN_SETTINGS 10035
+
+#define WINDOW_ADDON_BROWSER 10040
+
+#define WINDOW_EVENT_LOG 10050
+
+#define WINDOW_FAVOURITES 10060
+
+#define WINDOW_SCREENSAVER_DIM 97
+#define WINDOW_DEBUG_INFO 98
+#define WINDOW_DIALOG_POINTER 10099
+#define WINDOW_DIALOG_YES_NO 10100
+#define WINDOW_DIALOG_PROGRESS 10101
+#define WINDOW_DIALOG_KEYBOARD 10103
+#define WINDOW_DIALOG_VOLUME_BAR 10104
+#define WINDOW_DIALOG_SUB_MENU 10105
+#define WINDOW_DIALOG_CONTEXT_MENU 10106
+#define WINDOW_DIALOG_KAI_TOAST 10107
+#define WINDOW_DIALOG_NUMERIC 10109
+#define WINDOW_DIALOG_GAMEPAD 10110
+#define WINDOW_DIALOG_BUTTON_MENU 10111
+#define WINDOW_DIALOG_PLAYER_CONTROLS 10114
+#define WINDOW_DIALOG_SEEK_BAR 10115
+#define WINDOW_DIALOG_PLAYER_PROCESS_INFO 10116
+#define WINDOW_DIALOG_MUSIC_OSD 10120
+#define WINDOW_DIALOG_VIS_SETTINGS 10121
+#define WINDOW_DIALOG_VIS_PRESET_LIST 10122
+#define WINDOW_DIALOG_VIDEO_OSD_SETTINGS 10123
+#define WINDOW_DIALOG_AUDIO_OSD_SETTINGS 10124
+#define WINDOW_DIALOG_VIDEO_BOOKMARKS 10125
+#define WINDOW_DIALOG_FILE_BROWSER 10126
+#define WINDOW_DIALOG_NETWORK_SETUP 10128
+#define WINDOW_DIALOG_MEDIA_SOURCE 10129
+#define WINDOW_DIALOG_PROFILE_SETTINGS 10130
+#define WINDOW_DIALOG_LOCK_SETTINGS 10131
+#define WINDOW_DIALOG_CONTENT_SETTINGS 10132
+#define WINDOW_DIALOG_LIBEXPORT_SETTINGS 10133
+#define WINDOW_DIALOG_FAVOURITES 10134
+#define WINDOW_DIALOG_SONG_INFO 10135
+#define WINDOW_DIALOG_SMART_PLAYLIST_EDITOR 10136
+#define WINDOW_DIALOG_SMART_PLAYLIST_RULE 10137
+#define WINDOW_DIALOG_BUSY 10138
+#define WINDOW_DIALOG_PICTURE_INFO 10139
+#define WINDOW_DIALOG_ADDON_SETTINGS 10140
+#define WINDOW_DIALOG_FULLSCREEN_INFO 10142
+#define WINDOW_DIALOG_SLIDER 10145
+#define WINDOW_DIALOG_ADDON_INFO 10146
+#define WINDOW_DIALOG_TEXT_VIEWER 10147
+#ifdef HAS_DVD_DRIVE
+#define WINDOW_DIALOG_PLAY_EJECT 10148
+#endif
+#define WINDOW_DIALOG_PERIPHERALS 10149
+#define WINDOW_DIALOG_PERIPHERAL_SETTINGS 10150
+#define WINDOW_DIALOG_EXT_PROGRESS 10151
+#define WINDOW_DIALOG_MEDIA_FILTER 10152
+#define WINDOW_DIALOG_SUBTITLES 10153
+#define WINDOW_DIALOG_KEYBOARD_TOUCH 10156
+#define WINDOW_DIALOG_CMS_OSD_SETTINGS 10157
+#define WINDOW_DIALOG_INFOPROVIDER_SETTINGS 10158
+#define WINDOW_DIALOG_SUBTITLE_OSD_SETTINGS 10159
+#define WINDOW_DIALOG_BUSY_NOCANCEL 10160
+
+#define WINDOW_MUSIC_PLAYLIST 10500
+#define WINDOW_MUSIC_NAV 10502
+#define WINDOW_MUSIC_PLAYLIST_EDITOR 10503
+
+#define WINDOW_DIALOG_OSD_TELETEXT 10550
+
+// PVR related Window and Dialog ID's
+
+#define WINDOW_DIALOG_PVR_ID_START 10600
+#define WINDOW_DIALOG_PVR_GUIDE_INFO (WINDOW_DIALOG_PVR_ID_START)
+#define WINDOW_DIALOG_PVR_RECORDING_INFO (WINDOW_DIALOG_PVR_ID_START+1)
+#define WINDOW_DIALOG_PVR_TIMER_SETTING (WINDOW_DIALOG_PVR_ID_START+2)
+#define WINDOW_DIALOG_PVR_GROUP_MANAGER (WINDOW_DIALOG_PVR_ID_START+3)
+#define WINDOW_DIALOG_PVR_CHANNEL_MANAGER (WINDOW_DIALOG_PVR_ID_START+4)
+#define WINDOW_DIALOG_PVR_GUIDE_SEARCH (WINDOW_DIALOG_PVR_ID_START+5)
+#define WINDOW_DIALOG_PVR_CHANNEL_SCAN (WINDOW_DIALOG_PVR_ID_START+6)
+#define WINDOW_DIALOG_PVR_UPDATE_PROGRESS (WINDOW_DIALOG_PVR_ID_START+7)
+#define WINDOW_DIALOG_PVR_OSD_CHANNELS (WINDOW_DIALOG_PVR_ID_START+8)
+#define WINDOW_DIALOG_PVR_CHANNEL_GUIDE (WINDOW_DIALOG_PVR_ID_START+9)
+#define WINDOW_DIALOG_PVR_RADIO_RDS_INFO (WINDOW_DIALOG_PVR_ID_START+10)
+#define WINDOW_DIALOG_PVR_RECORDING_SETTING (WINDOW_DIALOG_PVR_ID_START+11)
+#define WINDOW_DIALOG_PVR_CLIENT_PRIORITIES (WINDOW_DIALOG_PVR_ID_START+12)
+#define WINDOW_DIALOG_PVR_GUIDE_CONTROLS (WINDOW_DIALOG_PVR_ID_START+13)
+#define WINDOW_DIALOG_PVR_ID_END WINDOW_DIALOG_PVR_GUIDE_CONTROLS
+
+#define WINDOW_PVR_ID_START 10700
+#define WINDOW_TV_CHANNELS (WINDOW_PVR_ID_START)
+#define WINDOW_TV_RECORDINGS (WINDOW_PVR_ID_START+1)
+#define WINDOW_TV_GUIDE (WINDOW_PVR_ID_START+2)
+#define WINDOW_TV_TIMERS (WINDOW_PVR_ID_START+3)
+#define WINDOW_TV_SEARCH (WINDOW_PVR_ID_START+4)
+#define WINDOW_RADIO_CHANNELS (WINDOW_PVR_ID_START+5)
+#define WINDOW_RADIO_RECORDINGS (WINDOW_PVR_ID_START+6)
+#define WINDOW_RADIO_GUIDE (WINDOW_PVR_ID_START+7)
+#define WINDOW_RADIO_TIMERS (WINDOW_PVR_ID_START+8)
+#define WINDOW_RADIO_SEARCH (WINDOW_PVR_ID_START+9)
+#define WINDOW_TV_TIMER_RULES (WINDOW_PVR_ID_START+10)
+#define WINDOW_RADIO_TIMER_RULES (WINDOW_PVR_ID_START+11)
+#define WINDOW_PVR_ID_END WINDOW_RADIO_TIMER_RULES
+
+// virtual windows for PVR specific keymap bindings in fullscreen playback
+#define WINDOW_FULLSCREEN_LIVETV 10800
+#define WINDOW_FULLSCREEN_RADIO 10801
+#define WINDOW_FULLSCREEN_LIVETV_PREVIEW 10802
+#define WINDOW_FULLSCREEN_RADIO_PREVIEW 10803
+#define WINDOW_FULLSCREEN_LIVETV_INPUT 10804
+#define WINDOW_FULLSCREEN_RADIO_INPUT 10805
+
+#define WINDOW_DIALOG_GAME_CONTROLLERS 10820
+#define WINDOW_GAMES 10821
+#define WINDOW_DIALOG_GAME_OSD 10822
+#define WINDOW_DIALOG_GAME_VIDEO_FILTER 10823
+#define WINDOW_DIALOG_GAME_STRETCH_MODE 10824
+#define WINDOW_DIALOG_GAME_VOLUME 10825
+#define WINDOW_DIALOG_GAME_ADVANCED_SETTINGS 10826
+#define WINDOW_DIALOG_GAME_VIDEO_ROTATION 10827
+#define WINDOW_DIALOG_GAME_PORTS 10828
+#define WINDOW_DIALOG_IN_GAME_SAVES 10829
+#define WINDOW_DIALOG_GAME_SAVES 10830
+
+//#define WINDOW_VIRTUAL_KEYBOARD 11000
+// WINDOW_ID's from 11100 to 11199 reserved for Skins
+
+#define WINDOW_DIALOG_SELECT 12000
+#define WINDOW_DIALOG_MUSIC_INFO 12001
+#define WINDOW_DIALOG_OK 12002
+#define WINDOW_DIALOG_VIDEO_INFO 12003
+#define WINDOW_FULLSCREEN_VIDEO 12005
+#define WINDOW_VISUALISATION 12006
+#define WINDOW_SLIDESHOW 12007
+#define WINDOW_DIALOG_COLOR_PICKER 12008
+#define WINDOW_WEATHER 12600
+#define WINDOW_SCREENSAVER 12900
+#define WINDOW_DIALOG_VIDEO_OSD 12901
+
+#define WINDOW_VIDEO_MENU 12902
+#define WINDOW_VIDEO_TIME_SEEK 12905 // virtual window for time seeking during fullscreen video
+
+#define WINDOW_FULLSCREEN_GAME 12906
+
+#define WINDOW_SPLASH 12997 // splash window
+#define WINDOW_START 12998 // first window to load
+#define WINDOW_STARTUP_ANIM 12999 // for startup animations
+
+// WINDOW_ID's from 13000 to 13099 reserved for Python
+
+#define WINDOW_PYTHON_START 13000
+#define WINDOW_PYTHON_END 13099
+
+// WINDOW_ID's from 14000 to 14099 reserved for Addons
+
+#define WINDOW_ADDON_START 14000
+#define WINDOW_ADDON_END 14099
+
diff --git a/xbmc/guilib/XBTF.cpp b/xbmc/guilib/XBTF.cpp
new file mode 100644
index 0000000..aa095cc
--- /dev/null
+++ b/xbmc/guilib/XBTF.cpp
@@ -0,0 +1,235 @@
+/*
+ * 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 "XBTF.h"
+
+#include <cstring>
+#include <utility>
+
+CXBTFFrame::CXBTFFrame()
+{
+ m_width = 0;
+ m_height = 0;
+ m_packedSize = 0;
+ m_unpackedSize = 0;
+ m_offset = 0;
+ m_format = XB_FMT_UNKNOWN;
+ m_duration = 0;
+}
+
+uint32_t CXBTFFrame::GetWidth() const
+{
+ return m_width;
+}
+
+void CXBTFFrame::SetWidth(uint32_t width)
+{
+ m_width = width;
+}
+
+uint32_t CXBTFFrame::GetHeight() const
+{
+ return m_height;
+}
+
+void CXBTFFrame::SetHeight(uint32_t height)
+{
+ m_height = height;
+}
+
+uint64_t CXBTFFrame::GetPackedSize() const
+{
+ return m_packedSize;
+}
+
+void CXBTFFrame::SetPackedSize(uint64_t size)
+{
+ m_packedSize = size;
+}
+
+bool CXBTFFrame::IsPacked() const
+{
+ return m_unpackedSize != m_packedSize;
+}
+
+bool CXBTFFrame::HasAlpha() const
+{
+ return (m_format & XB_FMT_OPAQUE) == 0;
+}
+
+uint64_t CXBTFFrame::GetUnpackedSize() const
+{
+ return m_unpackedSize;
+}
+
+void CXBTFFrame::SetUnpackedSize(uint64_t size)
+{
+ m_unpackedSize = size;
+}
+
+void CXBTFFrame::SetFormat(uint32_t format)
+{
+ m_format = format;
+}
+
+uint32_t CXBTFFrame::GetFormat(bool raw) const
+{
+ return raw ? m_format : (m_format & XB_FMT_MASK);
+}
+
+uint64_t CXBTFFrame::GetOffset() const
+{
+ return m_offset;
+}
+
+void CXBTFFrame::SetOffset(uint64_t offset)
+{
+ m_offset = offset;
+}
+
+uint32_t CXBTFFrame::GetDuration() const
+{
+ return m_duration;
+}
+
+void CXBTFFrame::SetDuration(uint32_t duration)
+{
+ m_duration = duration;
+}
+
+uint64_t CXBTFFrame::GetHeaderSize() const
+{
+ uint64_t result =
+ sizeof(m_width) +
+ sizeof(m_height) +
+ sizeof(m_format) +
+ sizeof(m_packedSize) +
+ sizeof(m_unpackedSize) +
+ sizeof(m_offset) +
+ sizeof(m_duration);
+
+ return result;
+}
+
+CXBTFFile::CXBTFFile()
+ : m_path(),
+ m_frames()
+{ }
+
+const std::string& CXBTFFile::GetPath() const
+{
+ return m_path;
+}
+
+void CXBTFFile::SetPath(const std::string& path)
+{
+ m_path = path;
+}
+
+uint32_t CXBTFFile::GetLoop() const
+{
+ return m_loop;
+}
+
+void CXBTFFile::SetLoop(uint32_t loop)
+{
+ m_loop = loop;
+}
+
+const std::vector<CXBTFFrame>& CXBTFFile::GetFrames() const
+{
+ return m_frames;
+}
+
+std::vector<CXBTFFrame>& CXBTFFile::GetFrames()
+{
+ return m_frames;
+}
+
+uint64_t CXBTFFile::GetPackedSize() const
+{
+ uint64_t size = 0;
+ for (const auto& frame : m_frames)
+ size += frame.GetPackedSize();
+
+ return size;
+}
+
+uint64_t CXBTFFile::GetUnpackedSize() const
+{
+ uint64_t size = 0;
+ for (const auto& frame : m_frames)
+ size += frame.GetUnpackedSize();
+
+ return size;
+}
+
+uint64_t CXBTFFile::GetHeaderSize() const
+{
+ uint64_t result =
+ MaximumPathLength +
+ sizeof(m_loop) +
+ sizeof(uint32_t); /* Number of frames */
+
+ for (const auto& frame : m_frames)
+ result += frame.GetHeaderSize();
+
+ return result;
+}
+
+uint64_t CXBTFBase::GetHeaderSize() const
+{
+ uint64_t result = XBTF_MAGIC.size() + XBTF_VERSION.size() +
+ sizeof(uint32_t) /* number of files */;
+
+ for (const auto& file : m_files)
+ result += file.second.GetHeaderSize();
+
+ return result;
+}
+
+bool CXBTFBase::Exists(const std::string& name) const
+{
+ CXBTFFile dummy;
+ return Get(name, dummy);
+}
+
+bool CXBTFBase::Get(const std::string& name, CXBTFFile& file) const
+{
+ const auto& iter = m_files.find(name);
+ if (iter == m_files.end())
+ return false;
+
+ file = iter->second;
+ return true;
+}
+
+std::vector<CXBTFFile> CXBTFBase::GetFiles() const
+{
+ std::vector<CXBTFFile> files;
+ files.reserve(m_files.size());
+
+ for (const auto& file : m_files)
+ files.push_back(file.second);
+
+ return files;
+}
+
+void CXBTFBase::AddFile(const CXBTFFile& file)
+{
+ m_files.insert(std::make_pair(file.GetPath(), file));
+}
+
+void CXBTFBase::UpdateFile(const CXBTFFile& file)
+{
+ auto&& it = m_files.find(file.GetPath());
+ if (it == m_files.end())
+ return;
+
+ it->second = file;
+}
diff --git a/xbmc/guilib/XBTF.h b/xbmc/guilib/XBTF.h
new file mode 100644
index 0000000..3829439
--- /dev/null
+++ b/xbmc/guilib/XBTF.h
@@ -0,0 +1,107 @@
+/*
+ * 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 <ctime>
+#include <map>
+#include <string>
+#include <vector>
+
+#include <stdint.h>
+
+static const std::string XBTF_MAGIC = "XBTF";
+static const std::string XBTF_VERSION = "2";
+
+#include "TextureFormats.h"
+
+class CXBTFFrame
+{
+public:
+ CXBTFFrame();
+
+ uint32_t GetWidth() const;
+ void SetWidth(uint32_t width);
+
+ uint32_t GetFormat(bool raw = false) const;
+ void SetFormat(uint32_t format);
+
+ uint32_t GetHeight() const;
+ void SetHeight(uint32_t height);
+
+ uint64_t GetUnpackedSize() const;
+ void SetUnpackedSize(uint64_t size);
+
+ uint64_t GetPackedSize() const;
+ void SetPackedSize(uint64_t size);
+
+ uint64_t GetOffset() const;
+ void SetOffset(uint64_t offset);
+
+ uint64_t GetHeaderSize() const;
+
+ uint32_t GetDuration() const;
+ void SetDuration(uint32_t duration);
+
+ bool IsPacked() const;
+ bool HasAlpha() const;
+
+private:
+ uint32_t m_width;
+ uint32_t m_height;
+ uint32_t m_format;
+ uint64_t m_packedSize;
+ uint64_t m_unpackedSize;
+ uint64_t m_offset;
+ uint32_t m_duration;
+};
+
+class CXBTFFile
+{
+public:
+ CXBTFFile();
+
+ const std::string& GetPath() const;
+ void SetPath(const std::string& path);
+
+ uint32_t GetLoop() const;
+ void SetLoop(uint32_t loop);
+
+ const std::vector<CXBTFFrame>& GetFrames() const;
+ std::vector<CXBTFFrame>& GetFrames();
+
+ uint64_t GetPackedSize() const;
+ uint64_t GetUnpackedSize() const;
+ uint64_t GetHeaderSize() const;
+
+ static const size_t MaximumPathLength = 256;
+
+private:
+ std::string m_path;
+ uint32_t m_loop = 0;
+ std::vector<CXBTFFrame> m_frames;
+};
+
+class CXBTFBase
+{
+public:
+ virtual ~CXBTFBase() = default;
+
+ uint64_t GetHeaderSize() const;
+
+ bool Exists(const std::string& name) const;
+ bool Get(const std::string& name, CXBTFFile& file) const;
+ std::vector<CXBTFFile> GetFiles() const;
+ void AddFile(const CXBTFFile& file);
+ void UpdateFile(const CXBTFFile& file);
+
+protected:
+ CXBTFBase() = default;
+
+ std::map<std::string, CXBTFFile> m_files;
+};
diff --git a/xbmc/guilib/XBTFReader.cpp b/xbmc/guilib/XBTFReader.cpp
new file mode 100644
index 0000000..cca8e83
--- /dev/null
+++ b/xbmc/guilib/XBTFReader.cpp
@@ -0,0 +1,216 @@
+/*
+ * 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 <inttypes.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#include "XBTFReader.h"
+#include "guilib/XBTF.h"
+#include "utils/EndianSwap.h"
+
+#ifdef TARGET_WINDOWS
+#include "filesystem/SpecialProtocol.h"
+#include "utils/CharsetConverter.h"
+#include "platform/win32/PlatformDefs.h"
+#endif
+
+static bool ReadString(FILE* file, char* str, size_t max_length)
+{
+ if (file == nullptr || str == nullptr || max_length <= 0)
+ return false;
+
+ return (fread(str, max_length, 1, file) == 1);
+}
+
+static bool ReadUInt32(FILE* file, uint32_t& value)
+{
+ if (file == nullptr)
+ return false;
+
+ if (fread(&value, sizeof(uint32_t), 1, file) != 1)
+ return false;
+
+ value = Endian_SwapLE32(value);
+ return true;
+}
+
+static bool ReadUInt64(FILE* file, uint64_t& value)
+{
+ if (file == nullptr)
+ return false;
+
+ if (fread(&value, sizeof(uint64_t), 1, file) != 1)
+ return false;
+
+ value = Endian_SwapLE64(value);
+ return true;
+}
+
+CXBTFReader::CXBTFReader()
+ : CXBTFBase(),
+ m_path()
+{ }
+
+CXBTFReader::~CXBTFReader()
+{
+ Close();
+}
+
+bool CXBTFReader::Open(const std::string& path)
+{
+ if (path.empty())
+ return false;
+
+ m_path = path;
+
+#ifdef TARGET_WINDOWS
+ std::wstring strPathW;
+ g_charsetConverter.utf8ToW(CSpecialProtocol::TranslatePath(m_path), strPathW, false);
+ m_file = _wfopen(strPathW.c_str(), L"rb");
+#else
+ m_file = fopen(m_path.c_str(), "rb");
+#endif
+ if (m_file == nullptr)
+ return false;
+
+ // read the magic word
+ char magic[4];
+ if (!ReadString(m_file, magic, sizeof(magic)))
+ return false;
+
+ if (strncmp(XBTF_MAGIC.c_str(), magic, sizeof(magic)) != 0)
+ return false;
+
+ // read the version
+ char version[1];
+ if (!ReadString(m_file, version, sizeof(version)))
+ return false;
+
+ if (strncmp(XBTF_VERSION.c_str(), version, sizeof(version)) != 0)
+ return false;
+
+ unsigned int nofFiles;
+ if (!ReadUInt32(m_file, nofFiles))
+ return false;
+
+ for (uint32_t i = 0; i < nofFiles; i++)
+ {
+ CXBTFFile xbtfFile;
+ uint32_t u32;
+ uint64_t u64;
+
+ // one extra char to null terminate the string
+ char path[CXBTFFile::MaximumPathLength + 1] = {};
+ if (!ReadString(m_file, path, sizeof(path) - 1))
+ return false;
+ xbtfFile.SetPath(path);
+
+ if (!ReadUInt32(m_file, u32))
+ return false;
+ xbtfFile.SetLoop(u32);
+
+ unsigned int nofFrames;
+ if (!ReadUInt32(m_file, nofFrames))
+ return false;
+
+ for (uint32_t j = 0; j < nofFrames; j++)
+ {
+ CXBTFFrame frame;
+
+ if (!ReadUInt32(m_file, u32))
+ return false;
+ frame.SetWidth(u32);
+
+ if (!ReadUInt32(m_file, u32))
+ return false;
+ frame.SetHeight(u32);
+
+ if (!ReadUInt32(m_file, u32))
+ return false;
+ frame.SetFormat(u32);
+
+ if (!ReadUInt64(m_file, u64))
+ return false;
+ frame.SetPackedSize(u64);
+
+ if (!ReadUInt64(m_file, u64))
+ return false;
+ frame.SetUnpackedSize(u64);
+
+ if (!ReadUInt32(m_file, u32))
+ return false;
+ frame.SetDuration(u32);
+
+ if (!ReadUInt64(m_file, u64))
+ return false;
+ frame.SetOffset(u64);
+
+ xbtfFile.GetFrames().push_back(frame);
+ }
+
+ AddFile(xbtfFile);
+ }
+
+ // Sanity check
+ uint64_t pos = static_cast<uint64_t>(ftell(m_file));
+ if (pos != GetHeaderSize())
+ return false;
+
+ return true;
+}
+
+bool CXBTFReader::IsOpen() const
+{
+ return m_file != nullptr;
+}
+
+void CXBTFReader::Close()
+{
+ if (m_file != nullptr)
+ {
+ fclose(m_file);
+ m_file = nullptr;
+ }
+
+ m_path.clear();
+ m_files.clear();
+}
+
+time_t CXBTFReader::GetLastModificationTimestamp() const
+{
+ if (m_file == nullptr)
+ return 0;
+
+ struct stat fileStat;
+ if (fstat(fileno(m_file), &fileStat) == -1)
+ return 0;
+
+ return fileStat.st_mtime;
+}
+
+bool CXBTFReader::Load(const CXBTFFrame& frame, unsigned char* buffer) const
+{
+ if (m_file == nullptr)
+ return false;
+
+#if defined(TARGET_DARWIN) || defined(TARGET_FREEBSD)
+ if (fseeko(m_file, static_cast<off_t>(frame.GetOffset()), SEEK_SET) == -1)
+#elif defined(TARGET_ANDROID)
+ if (fseek(m_file, static_cast<long>(frame.GetOffset()), SEEK_SET) == -1) // No fseeko64 before N
+#else
+ if (fseeko64(m_file, static_cast<off_t>(frame.GetOffset()), SEEK_SET) == -1)
+#endif
+ return false;
+
+ if (fread(buffer, 1, static_cast<size_t>(frame.GetPackedSize()), m_file) != frame.GetPackedSize())
+ return false;
+
+ return true;
+}
diff --git a/xbmc/guilib/XBTFReader.h b/xbmc/guilib/XBTFReader.h
new file mode 100644
index 0000000..cfe9976
--- /dev/null
+++ b/xbmc/guilib/XBTFReader.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "XBTF.h"
+
+#include <memory>
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+class CXBTFReader : public CXBTFBase
+{
+public:
+ CXBTFReader();
+ ~CXBTFReader() override;
+
+ bool Open(const std::string& path);
+ bool IsOpen() const;
+ void Close();
+
+ time_t GetLastModificationTimestamp() const;
+
+ bool Load(const CXBTFFrame& frame, unsigned char* buffer) const;
+
+private:
+ std::string m_path;
+ FILE* m_file = nullptr;
+};
+
+typedef std::shared_ptr<CXBTFReader> CXBTFReaderPtr;
diff --git a/xbmc/guilib/_Controls.dox b/xbmc/guilib/_Controls.dox
new file mode 100644
index 0000000..c9947c3
--- /dev/null
+++ b/xbmc/guilib/_Controls.dox
@@ -0,0 +1,40 @@
+/*!
+
+\page skin_controls Controls
+\brief Controls are the substance of your skin. They define everything from
+buttons, to text labels, to visualization placement. This portion of the
+manual will explain each and every control in detail.
+
+- \subpage Addon_Rendering_control - Control where rendering becomes performed from add-on
+- \subpage skin_Button_control - a standard push button control.
+- \subpage Color_button_control - used for creating push buttons in Kodi with a box for color preview.
+- \subpage EPGGrid_control - used to display the EPG guide in the PVR section.
+- \subpage skin_Edit_control - used as an input control for the osd keyboard and other input fields.
+- \subpage Fade_Label_Control - used to show multiple pieces of text in the same position, by fading from one to the other.
+- \subpage Fixed_List_Container - used for a list of items with a fixed focus. Same as the \ref Wrap_List_Container "Wrap List Container" except it doesn't wrap.
+- \subpage Game_Control - used to display the currently playing game, with optional effects, whilst in the GUI.
+- \subpage Group_Control - used to group controls together.
+- \subpage Group_List_Control - special case of the group control that forms a scrolling list of controls.
+- \subpage Image_Control - used to show an image.
+- \subpage Label_Control - used to show some lines of text.
+- \subpage List_Container - used for a scrolling lists of items. Replaces the list control.
+- \subpage Mover_Control - used in the calibration screens.
+- \subpage MultiImage_Control - used to show a slideshow of images.
+- \subpage Panel_Container - used for a scrolling panel of items. Replaces the thumbnail panel.
+- \subpage Progress_Control - Used to show the progress of a particular operation.
+- \subpage RSS_feed_Control - used to display scrolling RSS feeds.
+- \subpage Ranges_Control - Used to show multiple range blocks.
+- \subpage Radio_button_control - a radio button control (as used for on/off settings).
+- \subpage Resize_Control - used to set the pixel ratio in Video Calibration.
+- \subpage Scroll_Bar_Control - used for a implementing a scroll bar.
+- \subpage Settings_Slider_Control - used for a slider control in the settings menus.
+- \subpage Settings_Spin_Control - used for cycling up/down controls in the settings menus.
+- \subpage Slider_Control - used for a volume slider.
+- \subpage Spin_Control - used for cycling up/down controls.
+- \subpage Text_Box - used to show a multi-page piece of text.
+- \subpage skin_Toggle_button_control - a toggle on/off button that can take 2 different states.
+- \subpage Video_Control - used to display the currently playing video whilst in the GUI.
+- \subpage Visualisation_Control - used to display a visualisation while music is playing.
+- \subpage Wrap_List_Container - used for a wrapping list of items with fixed focus.
+
+*/
diff --git a/xbmc/guilib/gui3d.h b/xbmc/guilib/gui3d.h
new file mode 100644
index 0000000..f747336
--- /dev/null
+++ b/xbmc/guilib/gui3d.h
@@ -0,0 +1,44 @@
+/*
+ * 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 "PlatformDefs.h" // for TARGET_WINDOWS types
+
+#define GAMMA_RAMP_FLAG D3DSGR_CALIBRATE
+
+#define D3DFMT_LIN_A8R8G8B8 D3DFMT_A8R8G8B8
+#define D3DFMT_LIN_X8R8G8B8 D3DFMT_X8R8G8B8
+#define D3DFMT_LIN_L8 D3DFMT_L8
+#define D3DFMT_LIN_D16 D3DFMT_D16
+#define D3DFMT_LIN_A8 D3DFMT_A8
+
+#define D3DPIXELSHADERDEF DWORD
+
+struct D3DTexture
+{
+ DWORD Common;
+ DWORD Data;
+ DWORD Lock;
+
+ DWORD Format; // Format information about the texture.
+ DWORD Size; // Size of a non power-of-2 texture, must be zero otherwise
+};
+
+#define D3DCOMMON_TYPE_MASK 0x0070000
+#define D3DCOMMON_TYPE_TEXTURE 0x0040000
+
+struct D3DPalette
+{
+ DWORD Common;
+ DWORD Data;
+ DWORD Lock;
+};
+
+typedef D3DPalette* LPDIRECT3DPALETTE8;
+
diff --git a/xbmc/guilib/guiinfo/AddonsGUIInfo.cpp b/xbmc/guilib/guiinfo/AddonsGUIInfo.cpp
new file mode 100644
index 0000000..6630fa1
--- /dev/null
+++ b/xbmc/guilib/guiinfo/AddonsGUIInfo.cpp
@@ -0,0 +1,279 @@
+/*
+ * 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 "guilib/guiinfo/AddonsGUIInfo.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI::GUILIB::GUIINFO;
+
+bool CAddonsGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ return false;
+}
+
+bool CAddonsGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ const std::shared_ptr<const ADDON::IAddon> addonInfo = item->GetAddonInfo();
+ if (addonInfo)
+ {
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // LISTITEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case LISTITEM_ADDON_NAME:
+ value = addonInfo->Name();
+ return true;
+ case LISTITEM_ADDON_VERSION:
+ value = addonInfo->Version().asString();
+ return true;
+ case LISTITEM_ADDON_CREATOR:
+ value = addonInfo->Author();
+ return true;
+ case LISTITEM_ADDON_SUMMARY:
+ value = addonInfo->Summary();
+ return true;
+ case LISTITEM_ADDON_DESCRIPTION:
+ value = addonInfo->Description();
+ return true;
+ case LISTITEM_ADDON_DISCLAIMER:
+ value = addonInfo->Disclaimer();
+ return true;
+ case LISTITEM_ADDON_NEWS:
+ value = addonInfo->ChangeLog();
+ return true;
+ case LISTITEM_ADDON_BROKEN:
+ {
+ // Fallback for old GUI info
+ if (addonInfo->LifecycleState() == ADDON::AddonLifecycleState::BROKEN)
+ value = addonInfo->LifecycleStateDescription();
+ else
+ value = "";
+ return true;
+ }
+ case LISTITEM_ADDON_LIFECYCLE_TYPE:
+ {
+ const ADDON::AddonLifecycleState state = addonInfo->LifecycleState();
+ switch (state)
+ {
+ case ADDON::AddonLifecycleState::BROKEN:
+ value = g_localizeStrings.Get(24171); // "Broken"
+ break;
+ case ADDON::AddonLifecycleState::DEPRECATED:
+ value = g_localizeStrings.Get(24170); // "Deprecated";
+ break;
+ case ADDON::AddonLifecycleState::NORMAL:
+ default:
+ value = g_localizeStrings.Get(24169); // "Normal";
+ break;
+ }
+ return true;
+ }
+ case LISTITEM_ADDON_LIFECYCLE_DESC:
+ value = addonInfo->LifecycleStateDescription();
+ return true;
+ case LISTITEM_ADDON_TYPE:
+ value = ADDON::CAddonInfo::TranslateType(addonInfo->Type(), true);
+ return true;
+ case LISTITEM_ADDON_INSTALL_DATE:
+ value = addonInfo->InstallDate().GetAsLocalizedDateTime();
+ return true;
+ case LISTITEM_ADDON_LAST_UPDATED:
+ if (addonInfo->LastUpdated().IsValid())
+ {
+ value = addonInfo->LastUpdated().GetAsLocalizedDateTime();
+ return true;
+ }
+ break;
+ case LISTITEM_ADDON_LAST_USED:
+ if (addonInfo->LastUsed().IsValid())
+ {
+ value = addonInfo->LastUsed().GetAsLocalizedDateTime();
+ return true;
+ }
+ break;
+ case LISTITEM_ADDON_ORIGIN:
+ {
+ if (item->GetAddonInfo()->Origin() == ADDON::ORIGIN_SYSTEM)
+ {
+ value = g_localizeStrings.Get(24992);
+ return true;
+ }
+ if (!item->GetAddonInfo()->OriginName().empty())
+ {
+ value = item->GetAddonInfo()->OriginName();
+ return true;
+ }
+ else if (!item->GetAddonInfo()->Origin().empty())
+ {
+ value = item->GetAddonInfo()->Origin();
+ return true;
+ }
+ value = g_localizeStrings.Get(25014);
+ return true;
+ }
+ case LISTITEM_ADDON_SIZE:
+ {
+ uint64_t packageSize = item->GetAddonInfo()->PackageSize();
+ if (packageSize > 0)
+ {
+ value = StringUtils::FormatFileSize(packageSize);
+ return true;
+ }
+ break;
+ }
+ }
+ }
+
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ADDON_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case ADDON_SETTING_STRING:
+ {
+ ADDON::AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(info.GetData3(), addon,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ return false;
+ }
+ value = addon->GetSetting(info.GetData5());
+ return true;
+ }
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SYSTEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case SYSTEM_ADDON_TITLE:
+ case SYSTEM_ADDON_ICON:
+ case SYSTEM_ADDON_VERSION:
+ {
+ // This logic does not check/care whether an addon has been disabled/marked as broken,
+ // it simply retrieves it's name or icon that means if an addon is placed on the home screen it
+ // will stay there even if it's disabled/marked as broken. This might need to be changed/fixed
+ // in the future.
+ ADDON::AddonPtr addon;
+ if (!info.GetData3().empty())
+ {
+ bool success = CServiceBroker::GetAddonMgr().GetAddon(info.GetData3(), addon,
+ ADDON::OnlyEnabled::CHOICE_YES);
+ if (!success || !addon)
+ break;
+
+ if (info.m_info == SYSTEM_ADDON_TITLE)
+ {
+ value = addon->Name();
+ return true;
+ }
+ if (info.m_info == SYSTEM_ADDON_ICON)
+ {
+ value = addon->Icon();
+ return true;
+ }
+ if (info.m_info == SYSTEM_ADDON_VERSION)
+ {
+ value = addon->Version().asString();
+ return true;
+ }
+ }
+ break;
+ }
+ }
+
+ return false;
+}
+
+bool CAddonsGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ADDON_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case ADDON_SETTING_INT:
+ {
+ ADDON::AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(info.GetData3(), addon,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ return false;
+ }
+ return addon->GetSettingInt(info.GetData5(), value);
+ }
+ }
+ return false;
+}
+
+bool CAddonsGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ADDON_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case ADDON_SETTING_BOOL:
+ {
+ ADDON::AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(info.GetData3(), addon,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ return false;
+ }
+ return addon->GetSettingBool(info.GetData5(), value);
+ }
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SYSTEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case SYSTEM_HAS_ADDON:
+ {
+ ADDON::AddonPtr addon;
+ value = CServiceBroker::GetAddonMgr().IsAddonInstalled(info.GetData3());
+ return true;
+ }
+ case SYSTEM_ADDON_IS_ENABLED:
+ {
+ value = false;
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(info.GetData3(), addon,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ value = !CServiceBroker::GetAddonMgr().IsAddonDisabled(info.GetData3());
+ return true;
+ }
+ case LISTITEM_ISAUTOUPDATEABLE:
+ {
+ value = true;
+ const CFileItem* item = static_cast<const CFileItem*>(gitem);
+ if (item->GetAddonInfo())
+ value = CServiceBroker::GetAddonMgr().IsAutoUpdateable(item->GetAddonInfo()->ID()) ||
+ !CServiceBroker::GetAddonMgr().IsAddonInstalled(item->GetAddonInfo()->ID(),
+ item->GetAddonInfo()->Origin());
+
+ //! @Todo: store origin of not-autoupdateable installed addons in table 'update_rules'
+ // of the addon database. this is needed to pin ambiguous addon-id's that are
+ // available from multiple origins accordingly.
+ //
+ // after this is done the above call should be changed to
+ //
+ // value = CServiceBroker::GetAddonMgr().IsAutoUpdateable(item->GetAddonInfo()->ID(),
+ // item->GetAddonInfo()->Origin());
+
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/guilib/guiinfo/AddonsGUIInfo.h b/xbmc/guilib/guiinfo/AddonsGUIInfo.h
new file mode 100644
index 0000000..e06ef51
--- /dev/null
+++ b/xbmc/guilib/guiinfo/AddonsGUIInfo.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProvider.h"
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CAddonsGUIInfo : public CGUIInfoProvider
+{
+public:
+ CAddonsGUIInfo() = default;
+ ~CAddonsGUIInfo() override = default;
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/CMakeLists.txt b/xbmc/guilib/guiinfo/CMakeLists.txt
new file mode 100644
index 0000000..9203511
--- /dev/null
+++ b/xbmc/guilib/guiinfo/CMakeLists.txt
@@ -0,0 +1,42 @@
+set(SOURCES GUIInfo.cpp
+ GUIInfoHelper.cpp
+ GUIInfoProviders.cpp
+ GUIInfoLabel.cpp
+ GUIInfoBool.cpp
+ GUIInfoColor.cpp
+ AddonsGUIInfo.cpp
+ GamesGUIInfo.cpp
+ GUIControlsGUIInfo.cpp
+ LibraryGUIInfo.cpp
+ MusicGUIInfo.cpp
+ PicturesGUIInfo.cpp
+ PlayerGUIInfo.cpp
+ SkinGUIInfo.cpp
+ SystemGUIInfo.cpp
+ VideoGUIInfo.cpp
+ VisualisationGUIInfo.cpp
+ WeatherGUIInfo.cpp)
+
+set(HEADERS GUIInfo.h
+ GUIInfoHelper.h
+ GUIInfoLabels.h
+ GUIInfoProvider.h
+ GUIInfoProviders.h
+ GUIInfoLabel.h
+ GUIInfoBool.h
+ GUIInfoColor.h
+ IGUIInfoProvider.h
+ AddonsGUIInfo.h
+ GamesGUIInfo.h
+ GUIControlsGUIInfo.h
+ LibraryGUIInfo.h
+ MusicGUIInfo.h
+ PicturesGUIInfo.h
+ PlayerGUIInfo.h
+ SkinGUIInfo.h
+ SystemGUIInfo.h
+ VideoGUIInfo.h
+ VisualisationGUIInfo.h
+ WeatherGUIInfo.h)
+
+core_add_library(guilib_guiinfo)
diff --git a/xbmc/guilib/guiinfo/GUIControlsGUIInfo.cpp b/xbmc/guilib/guiinfo/GUIControlsGUIInfo.cpp
new file mode 100644
index 0000000..a5ee133
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIControlsGUIInfo.cpp
@@ -0,0 +1,814 @@
+/*
+ * 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 "guilib/guiinfo/GUIControlsGUIInfo.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "dialogs/GUIDialogKeyboardGeneric.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIControlGroupList.h"
+#include "guilib/GUITextBox.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/IGUIContainer.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoHelper.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "music/dialogs/GUIDialogMusicInfo.h"
+#include "music/dialogs/GUIDialogSongInfo.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "video/VideoInfoTag.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+#include "view/GUIViewState.h"
+#include "windows/GUIMediaWindow.h"
+
+using namespace KODI::GUILIB;
+using namespace KODI::GUILIB::GUIINFO;
+
+void CGUIControlsGUIInfo::SetContainerMoving(int id, bool next, bool scrolling)
+{
+ // magnitude 2 indicates a scroll, sign indicates direction
+ m_containerMoves[id] = (next ? 1 : -1) * (scrolling ? 2 : 1);
+}
+
+void CGUIControlsGUIInfo::ResetContainerMovingCache()
+{
+ m_containerMoves.clear();
+}
+
+bool CGUIControlsGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ return false;
+}
+
+bool CGUIControlsGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // CONTAINER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case CONTAINER_FOLDERPATH:
+ case CONTAINER_FOLDERNAME:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ if (info.m_info == CONTAINER_FOLDERNAME)
+ value = window->CurrentDirectory().GetLabel();
+ else
+ value = CURL(window->CurrentDirectory().GetPath()).GetWithoutUserDetails();
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_PLUGINNAME:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ const CURL url(window->CurrentDirectory().GetPath());
+ if (url.IsProtocol("plugin"))
+ {
+ value = URIUtils::GetFileName(url.GetHostName());
+ return true;
+ }
+ }
+ break;
+ }
+ case CONTAINER_VIEWCOUNT:
+ case CONTAINER_VIEWMODE:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ const CGUIControl *control = window->GetControl(window->GetViewContainerID());
+ if (control && control->IsContainer())
+ {
+ if (info.m_info == CONTAINER_VIEWMODE)
+ {
+ value = static_cast<const IGUIContainer*>(control)->GetLabel();
+ return true;
+ }
+ else if (info.m_info == CONTAINER_VIEWCOUNT)
+ {
+ value = std::to_string(window->GetViewCount());
+ return true;
+ }
+ }
+ }
+ break;
+ }
+ case CONTAINER_SORT_METHOD:
+ case CONTAINER_SORT_ORDER:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ const CGUIViewState *viewState = window->GetViewState();
+ if (viewState)
+ {
+ if (info.m_info == CONTAINER_SORT_METHOD)
+ {
+ value = g_localizeStrings.Get(viewState->GetSortMethodLabel());
+ return true;
+ }
+ else if (info.m_info == CONTAINER_SORT_ORDER)
+ {
+ value = g_localizeStrings.Get(viewState->GetSortOrderLabel());
+ return true;
+ }
+ }
+ }
+ break;
+ }
+ case CONTAINER_PROPERTY:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().GetProperty(info.GetData3()).asString();
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_ART:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().GetArt(info.GetData3());
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_CONTENT:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().GetContent();
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_SHOWPLOT:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().GetProperty("showplot").asString();
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_SHOWTITLE:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().GetProperty("showtitle").asString();
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_PLUGINCATEGORY:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().GetProperty("plugincategory").asString();
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_TOTALTIME:
+ case CONTAINER_TOTALWATCHED:
+ case CONTAINER_TOTALUNWATCHED:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ int count = 0;
+ const CFileItemList& items = window->CurrentDirectory();
+ for (const auto& item : items)
+ {
+ // Iterate through container and count watched, unwatched and total duration.
+ if (info.m_info == CONTAINER_TOTALWATCHED && item->HasVideoInfoTag() && item->GetVideoInfoTag()->GetPlayCount() > 0)
+ count += 1;
+ else if (info.m_info == CONTAINER_TOTALUNWATCHED && item->HasVideoInfoTag() && item->GetVideoInfoTag()->GetPlayCount() == 0)
+ count += 1;
+ else if (info.m_info == CONTAINER_TOTALTIME && item->HasMusicInfoTag())
+ count += item->GetMusicInfoTag()->GetDuration();
+ else if (info.m_info == CONTAINER_TOTALTIME && item->HasVideoInfoTag())
+ count += item->GetVideoInfoTag()->m_streamDetails.GetVideoDuration();
+ }
+ if (info.m_info == CONTAINER_TOTALTIME && count > 0)
+ {
+ value = StringUtils::SecondsToTimeString(count);
+ return true;
+ }
+ else if (info.m_info == CONTAINER_TOTALWATCHED || info.m_info == CONTAINER_TOTALUNWATCHED)
+ {
+ value = std::to_string(count);
+ return true;
+ }
+ }
+ break;
+ }
+ case CONTAINER_NUM_PAGES:
+ case CONTAINER_CURRENT_PAGE:
+ case CONTAINER_NUM_ITEMS:
+ case CONTAINER_POSITION:
+ case CONTAINER_ROW:
+ case CONTAINER_COLUMN:
+ case CONTAINER_CURRENT_ITEM:
+ case CONTAINER_NUM_ALL_ITEMS:
+ case CONTAINER_NUM_NONFOLDER_ITEMS:
+ {
+ const CGUIControl *control = nullptr;
+ if (info.GetData1())
+ { // container specified
+ CGUIWindow *window = GUIINFO::GetWindow(contextWindow);
+ if (window)
+ control = window->GetControl(info.GetData1());
+ }
+ else
+ { // no container specified - assume a mediawindow
+ CGUIMediaWindow *window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ control = window->GetControl(window->GetViewContainerID());
+ }
+ if (control)
+ {
+ if (control->IsContainer())
+ value = static_cast<const IGUIContainer*>(control)->GetLabel(info.m_info);
+ else if (control->GetControlType() == CGUIControl::GUICONTROL_GROUPLIST)
+ value = static_cast<const CGUIControlGroupList*>(control)->GetLabel(info.m_info);
+ else if (control->GetControlType() == CGUIControl::GUICONTROL_TEXTBOX)
+ value = static_cast<const CGUITextBox*>(control)->GetLabel(info.m_info);
+ return true;
+ }
+ break;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // CONTROL_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case CONTROL_GET_LABEL:
+ {
+ CGUIWindow *window = GUIINFO::GetWindow(contextWindow);
+ if (window)
+ {
+ const CGUIControl *control = window->GetControl(info.GetData1());
+ if (control)
+ {
+ int data2 = info.GetData2();
+ if (data2)
+ value = control->GetDescriptionByIndex(data2);
+ else
+ value = control->GetDescription();
+ return true;
+ }
+ }
+ break;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // WINDOW_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case WINDOW_PROPERTY:
+ {
+ CGUIWindow *window = nullptr;
+ if (info.GetData1())
+ { // window specified
+ window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(info.GetData1());
+ }
+ else
+ { // no window specified - assume active
+ window = GUIINFO::GetWindow(contextWindow);
+ }
+ if (window)
+ {
+ value = window->GetProperty(info.GetData3()).asString();
+ return true;
+ }
+ break;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SYSTEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case SYSTEM_CURRENT_WINDOW:
+ value = g_localizeStrings.Get(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog());
+ return true;
+ case SYSTEM_STARTUP_WINDOW:
+ value = std::to_string(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_LOOKANDFEEL_STARTUPWINDOW));
+ return true;
+ case SYSTEM_CURRENT_CONTROL:
+ case SYSTEM_CURRENT_CONTROL_ID:
+ {
+ CGUIWindow *window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog());
+ if (window)
+ {
+ CGUIControl *control = window->GetFocusedControl();
+ if (control)
+ {
+ if (info.m_info == SYSTEM_CURRENT_CONTROL_ID)
+ value = std::to_string(control->GetID());
+ else if (info.m_info == SYSTEM_CURRENT_CONTROL)
+ value = control->GetDescription();
+ return true;
+ }
+ }
+ break;
+ }
+ case SYSTEM_PROGRESS_BAR:
+ {
+ CGUIDialogProgress *bar = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (bar && bar->IsDialogRunning())
+ value = std::to_string(bar->GetPercentage());
+ return true;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // FANART_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case FANART_COLOR1:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().GetProperty("fanart_color1").asString();
+ return true;
+ }
+ break;
+ }
+ case FANART_COLOR2:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().GetProperty("fanart_color2").asString();
+ return true;
+ }
+ break;
+ }
+ case FANART_COLOR3:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().GetProperty("fanart_color3").asString();
+ return true;
+ }
+ break;
+ }
+ case FANART_IMAGE:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().GetArt("fanart");
+ return true;
+ }
+ break;
+ }
+ }
+
+ return false;
+}
+
+bool CGUIControlsGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SYSTEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case SYSTEM_PROGRESS_BAR:
+ {
+ CGUIDialogProgress *bar = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (bar && bar->IsDialogRunning())
+ value = bar->GetPercentage();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CGUIControlsGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // CONTAINER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case CONTAINER_HASFILES:
+ case CONTAINER_HASFOLDERS:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ const CFileItemList& items = window->CurrentDirectory();
+ for (const auto& item : items)
+ {
+ if ((!item->m_bIsFolder && info.m_info == CONTAINER_HASFILES) ||
+ (item->m_bIsFolder && !item->IsParentFolder() && info.m_info == CONTAINER_HASFOLDERS))
+ {
+ value = true;
+ return true;
+ }
+ }
+ }
+ break;
+ }
+ case CONTAINER_STACKED:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().GetProperty("isstacked").asBoolean();
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_HAS_THUMB:
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CurrentDirectory().HasArt("thumb");
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_CAN_FILTER:
+ {
+ CGUIMediaWindow *window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = !window->CanFilterAdvanced();
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_CAN_FILTERADVANCED:
+ {
+ CGUIMediaWindow *window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->CanFilterAdvanced();
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_FILTERED:
+ {
+ CGUIMediaWindow *window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = window->IsFiltered();
+ return true;
+ }
+ break;
+ }
+ case CONTAINER_SORT_METHOD:
+ {
+ CGUIMediaWindow *window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ const CGUIViewState *viewState = window->GetViewState();
+ if (viewState)
+ {
+ value = (static_cast<int>(viewState->GetSortMethod().sortBy) == info.GetData2());
+ return true;
+ }
+ }
+ break;
+ }
+ case CONTAINER_SORT_DIRECTION:
+ {
+ CGUIMediaWindow *window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ const CGUIViewState *viewState = window->GetViewState();
+ if (viewState)
+ {
+ value = (static_cast<unsigned int>(viewState->GetSortOrder()) == info.GetData1());
+ return true;
+ }
+ }
+ break;
+ }
+ case CONTAINER_CONTENT:
+ {
+ std::string content;
+ CGUIWindow *window = GUIINFO::GetWindow(contextWindow);
+ if (window)
+ {
+ if (window->GetID() == WINDOW_DIALOG_MUSIC_INFO)
+ content = static_cast<CGUIDialogMusicInfo*>(window)->GetContent();
+ else if (window->GetID() == WINDOW_DIALOG_SONG_INFO)
+ content = static_cast<CGUIDialogSongInfo*>(window)->GetContent();
+ else if (window->GetID() == WINDOW_DIALOG_VIDEO_INFO)
+ content = static_cast<CGUIDialogVideoInfo*>(window)->CurrentDirectory().GetContent();
+ }
+ if (content.empty())
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ content = window->CurrentDirectory().GetContent();
+ }
+ value = StringUtils::EqualsNoCase(info.GetData3(), content);
+ return true;
+ }
+ case CONTAINER_ROW:
+ case CONTAINER_COLUMN:
+ case CONTAINER_POSITION:
+ case CONTAINER_HAS_NEXT:
+ case CONTAINER_HAS_PREVIOUS:
+ case CONTAINER_SCROLLING:
+ case CONTAINER_SUBITEM:
+ case CONTAINER_ISUPDATING:
+ case CONTAINER_HAS_PARENT_ITEM:
+ {
+ if (info.GetData1())
+ {
+ CGUIWindow *window = GUIINFO::GetWindow(contextWindow);
+ if (window)
+ {
+ const CGUIControl *control = window->GetControl(info.GetData1());
+ if (control)
+ {
+ value = control->GetCondition(info.m_info, info.GetData2());
+ return true;
+ }
+ }
+ }
+ else
+ {
+ const CGUIControl *activeContainer = GUIINFO::GetActiveContainer(0, contextWindow);
+ if (activeContainer)
+ {
+ value = activeContainer->GetCondition(info.m_info, info.GetData2());
+ return true;
+ }
+ }
+ break;
+ }
+ case CONTAINER_HAS_FOCUS:
+ { // grab our container
+ CGUIWindow *window = GUIINFO::GetWindow(contextWindow);
+ if (window)
+ {
+ const CGUIControl *control = window->GetControl(info.GetData1());
+ if (control && control->IsContainer())
+ {
+ const CFileItemPtr item = std::static_pointer_cast<CFileItem>(static_cast<const IGUIContainer*>(control)->GetListItem(0));
+ if (item && item->m_iprogramCount == info.GetData2()) // programcount used to store item id
+ {
+ value = true;
+ return true;
+ }
+ }
+ }
+ break;
+ }
+ case CONTAINER_SCROLL_PREVIOUS:
+ case CONTAINER_MOVE_PREVIOUS:
+ case CONTAINER_MOVE_NEXT:
+ case CONTAINER_SCROLL_NEXT:
+ {
+ int containerId = -1;
+ if (info.GetData1())
+ {
+ containerId = info.GetData1();
+ }
+ else
+ {
+ // no parameters, so we assume it's just requested for a media window. It therefore
+ // can only happen if the list has focus.
+ CGUIMediaWindow *window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ containerId = window->GetViewContainerID();
+ }
+ if (containerId != -1)
+ {
+ const std::map<int,int>::const_iterator it = m_containerMoves.find(containerId);
+ if (it != m_containerMoves.end())
+ {
+ if (info.m_info == CONTAINER_SCROLL_PREVIOUS)
+ value = it->second <= -2;
+ else if (info.m_info == CONTAINER_MOVE_PREVIOUS)
+ value = it->second <= -1;
+ else if (info.m_info == CONTAINER_MOVE_NEXT)
+ value = it->second >= 1;
+ else if (info.m_info == CONTAINER_SCROLL_NEXT)
+ value = it->second >= 2;
+ return true;
+ }
+ }
+ break;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // CONTROL_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case CONTROL_IS_VISIBLE:
+ {
+ CGUIWindow *window = GUIINFO::GetWindow(contextWindow);
+ if (window)
+ {
+ // Note: This'll only work for unique id's
+ const CGUIControl *control = window->GetControl(info.GetData1());
+ if (control)
+ {
+ value = control->IsVisible();
+ return true;
+ }
+ }
+ break;
+ }
+ case CONTROL_IS_ENABLED:
+ {
+ CGUIWindow *window = GUIINFO::GetWindow(contextWindow);
+ if (window)
+ {
+ // Note: This'll only work for unique id's
+ const CGUIControl *control = window->GetControl(info.GetData1());
+ if (control)
+ {
+ value = !control->IsDisabled();
+ return true;
+ }
+ }
+ break;
+ }
+ case CONTROL_HAS_FOCUS:
+ {
+ CGUIWindow *window = GUIINFO::GetWindow(contextWindow);
+ if (window)
+ {
+ value = (window->GetFocusedControlID() == static_cast<int>(info.GetData1()));
+ return true;
+ }
+ break;
+ }
+ case CONTROL_GROUP_HAS_FOCUS:
+ {
+ CGUIWindow *window = GUIINFO::GetWindow(contextWindow);
+ if (window)
+ {
+ value = window->ControlGroupHasFocus(info.GetData1(), info.GetData2());
+ return true;
+ }
+ break;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // WINDOW_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case WINDOW_IS_MEDIA:
+ { // note: This doesn't return true for dialogs (content, favourites, login, videoinfo)
+ CGUIWindowManager& windowMgr = CServiceBroker::GetGUI()->GetWindowManager();
+ CGUIWindow *window = windowMgr.GetWindow(windowMgr.GetActiveWindow());
+ if (window)
+ {
+ value = window->IsMediaWindow();
+ return true;
+ }
+ break;
+ }
+ case WINDOW_IS:
+ {
+ if (info.GetData1())
+ {
+ CGUIWindowManager& windowMgr = CServiceBroker::GetGUI()->GetWindowManager();
+ CGUIWindow *window = windowMgr.GetWindow(contextWindow);
+ if (!window)
+ {
+ // try topmost dialog
+ window = windowMgr.GetWindow(windowMgr.GetTopmostModalDialog());
+ if (!window)
+ {
+ // try active window
+ window = windowMgr.GetWindow(windowMgr.GetActiveWindow());
+ }
+ }
+ if (window)
+ {
+ value = (window && window->GetID() == static_cast<int>(info.GetData1()));
+ return true;
+ }
+ }
+ break;
+ }
+ case WINDOW_IS_VISIBLE:
+ {
+ if (info.GetData1())
+ value = CServiceBroker::GetGUI()->GetWindowManager().IsWindowVisible(info.GetData1());
+ else
+ value = CServiceBroker::GetGUI()->GetWindowManager().IsWindowVisible(info.GetData3());
+ return true;
+ }
+ case WINDOW_IS_ACTIVE:
+ {
+ if (info.GetData1())
+ value = CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(info.GetData1());
+ else
+ value = CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(info.GetData3());
+ return true;
+ }
+ case WINDOW_IS_DIALOG_TOPMOST:
+ {
+ if (info.GetData1())
+ value = CServiceBroker::GetGUI()->GetWindowManager().IsDialogTopmost(info.GetData1());
+ else
+ value = CServiceBroker::GetGUI()->GetWindowManager().IsDialogTopmost(info.GetData3());
+ return true;
+ }
+ case WINDOW_IS_MODAL_DIALOG_TOPMOST:
+ {
+ if (info.GetData1())
+ value = CServiceBroker::GetGUI()->GetWindowManager().IsModalDialogTopmost(info.GetData1());
+ else
+ value = CServiceBroker::GetGUI()->GetWindowManager().IsModalDialogTopmost(info.GetData3());
+ return true;
+ }
+ case WINDOW_NEXT:
+ {
+ if (info.GetData1())
+ {
+ value = (static_cast<int>(info.GetData1()) == m_nextWindowID);
+ return true;
+ }
+ else
+ {
+ CGUIWindow *window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(m_nextWindowID);
+ if (window && StringUtils::EqualsNoCase(URIUtils::GetFileName(window->GetProperty("xmlfile").asString()), info.GetData3()))
+ {
+ value = true;
+ return true;
+ }
+ }
+ break;
+ }
+ case WINDOW_PREVIOUS:
+ {
+ if (info.GetData1())
+ {
+ value = (static_cast<int>(info.GetData1()) == m_prevWindowID);
+ return true;
+ }
+ else
+ {
+ CGUIWindow *window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(m_prevWindowID);
+ if (window && StringUtils::EqualsNoCase(URIUtils::GetFileName(window->GetProperty("xmlfile").asString()), info.GetData3()))
+ {
+ value = true;
+ return true;
+ }
+ }
+ break;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SYSTEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case SYSTEM_HAS_ACTIVE_MODAL_DIALOG:
+ value = CServiceBroker::GetGUI()->GetWindowManager().HasModalDialog(true);
+ return true;
+ case SYSTEM_HAS_VISIBLE_MODAL_DIALOG:
+ value = CServiceBroker::GetGUI()->GetWindowManager().HasVisibleModalDialog();
+ return true;
+ case SYSTEM_HAS_INPUT_HIDDEN:
+ {
+ CGUIDialogNumeric *pNumeric = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogNumeric>(WINDOW_DIALOG_NUMERIC);
+ CGUIDialogKeyboardGeneric *pKeyboard = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogKeyboardGeneric>(WINDOW_DIALOG_KEYBOARD);
+
+ if (pNumeric && pNumeric->IsActive())
+ value = pNumeric->IsInputHidden();
+ else if (pKeyboard && pKeyboard->IsActive())
+ value = pKeyboard->IsInputHidden();
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/guilib/guiinfo/GUIControlsGUIInfo.h b/xbmc/guilib/guiinfo/GUIControlsGUIInfo.h
new file mode 100644
index 0000000..f6b8b68
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIControlsGUIInfo.h
@@ -0,0 +1,56 @@
+/*
+ * 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 "guilib/WindowIDs.h"
+#include "guilib/guiinfo/GUIInfoProvider.h"
+
+#include <map>
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CGUIControlsGUIInfo : public CGUIInfoProvider
+{
+public:
+ ~CGUIControlsGUIInfo() override = default;
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+
+ void SetNextWindow(int windowID) { m_nextWindowID = windowID; }
+ void SetPreviousWindow(int windowID) { m_prevWindowID = windowID; }
+
+ /*! \brief containers call this to specify that the focus is changing
+ \param id control id
+ \param next true if we're moving to the next item, false if previous
+ \param scrolling true if the container is scrolling, false if the movement requires no scroll
+ */
+ void SetContainerMoving(int id, bool next, bool scrolling);
+ void ResetContainerMovingCache();
+
+private:
+ int m_nextWindowID = WINDOW_INVALID;
+ int m_prevWindowID = WINDOW_INVALID;
+
+ std::map<int, int> m_containerMoves; // direction of list moving
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/GUIInfo.cpp b/xbmc/guilib/guiinfo/GUIInfo.cpp
new file mode 100644
index 0000000..5542a78
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfo.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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 "GUIInfo.h"
+
+#include <assert.h>
+
+using namespace KODI::GUILIB::GUIINFO;
+
+void CGUIInfo::SetInfoFlag(uint32_t flag)
+{
+ assert(flag >= (1 << 24));
+ m_data1 |= flag;
+}
+
+uint32_t CGUIInfo::GetInfoFlag() const
+{
+ // we strip out the bottom 24 bits, where we keep data
+ // and return the flag only
+ return m_data1 & 0xff000000;
+}
+
+uint32_t CGUIInfo::GetData1() const
+{
+ // we strip out the top 8 bits, where we keep flags
+ // and return the unflagged data
+ return m_data1 & ((1 << 24) -1);
+}
diff --git a/xbmc/guilib/guiinfo/GUIInfo.h b/xbmc/guilib/guiinfo/GUIInfo.h
new file mode 100644
index 0000000..c5bffe8
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfo.h
@@ -0,0 +1,110 @@
+/*
+ * 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 <stdint.h>
+#include <string>
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+// class to hold multiple integer data
+// for storage referenced from a single integer
+class CGUIInfo
+{
+public:
+ CGUIInfo(int info, uint32_t data1, int data2, uint32_t flag, const std::string& data3, int data4)
+ : m_info(info),
+ m_data1(data1),
+ m_data2(data2),
+ m_data3(data3),
+ m_data4(data4)
+ {
+ if (flag)
+ SetInfoFlag(flag);
+ }
+
+ explicit CGUIInfo(int info, uint32_t data1 = 0, int data2 = 0, uint32_t flag = 0)
+ : m_info(info),
+ m_data1(data1),
+ m_data2(data2),
+ m_data4(0)
+ {
+ if (flag)
+ SetInfoFlag(flag);
+ }
+
+ CGUIInfo(int info, uint32_t data1, int data2, const std::string& data3)
+ : m_info(info), m_data1(data1), m_data2(data2), m_data3(data3), m_data4(0)
+ {
+ }
+
+ CGUIInfo(int info, uint32_t data1, const std::string& data3)
+ : m_info(info),
+ m_data1(data1),
+ m_data2(0),
+ m_data3(data3),
+ m_data4(0)
+ {
+ }
+
+ CGUIInfo(int info, const std::string& data3)
+ : m_info(info),
+ m_data1(0),
+ m_data2(0),
+ m_data3(data3),
+ m_data4(0)
+ {
+ }
+
+ CGUIInfo(int info, const std::string& data3, int data2)
+ : m_info(info),
+ m_data1(0),
+ m_data2(data2),
+ m_data3(data3),
+ m_data4(0)
+ {
+ }
+
+ CGUIInfo(int info, const std::string& data3, const std::string& data5)
+ : m_info(info), m_data1(0), m_data3(data3), m_data4(0), m_data5(data5)
+ {
+ }
+
+ bool operator ==(const CGUIInfo &right) const
+ {
+ return (m_info == right.m_info && m_data1 == right.m_data1 && m_data2 == right.m_data2 &&
+ m_data3 == right.m_data3 && m_data4 == right.m_data4 && m_data5 == right.m_data5);
+ }
+
+ uint32_t GetInfoFlag() const;
+ uint32_t GetData1() const;
+ int GetData2() const { return m_data2; }
+ const std::string& GetData3() const { return m_data3; }
+ int GetData4() const { return m_data4; }
+ const std::string& GetData5() const { return m_data5; }
+
+ int m_info;
+private:
+ void SetInfoFlag(uint32_t flag);
+
+ uint32_t m_data1;
+ int m_data2;
+ std::string m_data3;
+ int m_data4;
+ std::string m_data5;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/GUIInfoBool.cpp b/xbmc/guilib/guiinfo/GUIInfoBool.cpp
new file mode 100644
index 0000000..2f6b4f9
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoBool.cpp
@@ -0,0 +1,41 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoBool.h"
+
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+
+using namespace KODI::GUILIB::GUIINFO;
+
+CGUIInfoBool::CGUIInfoBool(bool value)
+{
+ m_value = value;
+}
+
+CGUIInfoBool::~CGUIInfoBool() = default;
+
+void CGUIInfoBool::Parse(const std::string &expression, int context)
+{
+ if (expression == "true")
+ m_value = true;
+ else if (expression == "false")
+ m_value = false;
+ else
+ {
+ m_info = CServiceBroker::GetGUI()->GetInfoManager().Register(expression, context);
+ Update(context);
+ }
+}
+
+void CGUIInfoBool::Update(int contextWindow, const CGUIListItem* item /*= nullptr*/)
+{
+ if (m_info)
+ m_value = m_info->Get(contextWindow, item);
+}
diff --git a/xbmc/guilib/guiinfo/GUIInfoBool.h b/xbmc/guilib/guiinfo/GUIInfoBool.h
new file mode 100644
index 0000000..1913684
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoBool.h
@@ -0,0 +1,46 @@
+/*
+ * 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
+
+/*!
+\file GUIInfoBool.h
+\brief
+*/
+
+#include "interfaces/info/InfoBool.h"
+
+#include <string>
+
+class CGUIListItem;
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfoBool
+{
+public:
+ explicit CGUIInfoBool(bool value = false);
+ ~CGUIInfoBool();
+
+ operator bool() const { return m_value; }
+
+ void Update(int contextWindow, const CGUIListItem* item = nullptr);
+ void Parse(const std::string &expression, int context);
+private:
+ INFO::InfoPtr m_info;
+ bool m_value;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/GUIInfoColor.cpp b/xbmc/guilib/guiinfo/GUIInfoColor.cpp
new file mode 100644
index 0000000..c20326d
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoColor.cpp
@@ -0,0 +1,69 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoColor.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "guilib/GUIColorManager.h"
+#include "guilib/GUIComponent.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI::GUILIB::GUIINFO;
+
+bool CGUIInfoColor::Update(const CGUIListItem* item /* = nullptr */)
+{
+ if (!m_info)
+ return false; // no infolabel
+
+ // Expand the infolabel, and then convert it to a color
+ std::string infoLabel;
+ if (item && item->IsFileItem())
+ infoLabel = CServiceBroker::GetGUI()->GetInfoManager().GetItemLabel(
+ static_cast<const CFileItem*>(item), 0, m_info);
+ else
+ infoLabel = CServiceBroker::GetGUI()->GetInfoManager().GetLabel(m_info, INFO::DEFAULT_CONTEXT);
+
+ UTILS::COLOR::Color color =
+ !infoLabel.empty() ? CServiceBroker::GetGUI()->GetColorManager().GetColor(infoLabel) : 0;
+ if (m_color != color)
+ {
+ m_color = color;
+ return true;
+ }
+ else
+ return false;
+}
+
+void CGUIInfoColor::Parse(const std::string &label, int context)
+{
+ if (label.empty())
+ return;
+
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+
+ // Check for the standard $INFO[] block layout, and strip it if present
+ std::string label2 = label;
+ if (StringUtils::StartsWithNoCase(label, "$var["))
+ {
+ label2 = label.substr(5, label.length() - 6);
+ m_info = infoMgr.TranslateSkinVariableString(label2, context);
+ if (!m_info)
+ m_info = infoMgr.RegisterSkinVariableString(g_SkinInfo->CreateSkinVariable(label2, context));
+ return;
+ }
+
+ if (StringUtils::StartsWithNoCase(label, "$info["))
+ label2 = label.substr(6, label.length()-7);
+
+ m_info = infoMgr.TranslateString(label2);
+ if (!m_info)
+ m_color = CServiceBroker::GetGUI()->GetColorManager().GetColor(label);
+}
diff --git a/xbmc/guilib/guiinfo/GUIInfoColor.h b/xbmc/guilib/guiinfo/GUIInfoColor.h
new file mode 100644
index 0000000..a711047
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoColor.h
@@ -0,0 +1,53 @@
+/*
+ * 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
+
+/*!
+\file GUIInfoColor.h
+\brief
+*/
+
+#include "guilib/GUIListItem.h"
+#include "utils/ColorUtils.h"
+
+#include <string>
+
+class CGUIListItem;
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfoColor
+{
+public:
+ constexpr CGUIInfoColor(UTILS::COLOR::Color color = 0) : m_color(color) {}
+
+ constexpr operator UTILS::COLOR::Color() const { return m_color; }
+
+ bool Update(const CGUIListItem* item = nullptr);
+ void Parse(const std::string &label, int context);
+
+ /*!
+ * @brief Check if the infocolor has an info condition bound to its color definition (or otherwise, if it's constant color)
+ * @return true if the color depends on an info condition, false otherwise
+ */
+ bool HasInfo() const { return m_info != 0; }
+
+private:
+ int m_info = 0;
+ UTILS::COLOR::Color m_color;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/GUIInfoHelper.cpp b/xbmc/guilib/guiinfo/GUIInfoHelper.cpp
new file mode 100644
index 0000000..f025b5e
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoHelper.cpp
@@ -0,0 +1,205 @@
+/*
+ * 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 "GUIInfoHelper.h"
+
+#include "FileItem.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindow.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/IGUIContainer.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "playlists/PlayList.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "windows/GUIMediaWindow.h"
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+// conditions for window retrieval
+static const int WINDOW_CONDITION_HAS_LIST_ITEMS = 1;
+static const int WINDOW_CONDITION_IS_MEDIA_WINDOW = 2;
+
+std::string GetPlaylistLabel(int item, PLAYLIST::Id playlistId /* = TYPE_NONE */)
+{
+ PLAYLIST::CPlayListPlayer& player = CServiceBroker::GetPlaylistPlayer();
+
+ if (playlistId == PLAYLIST::TYPE_NONE)
+ playlistId = player.GetCurrentPlaylist();
+
+ switch (item)
+ {
+ case PLAYLIST_LENGTH:
+ {
+ return std::to_string(player.GetPlaylist(playlistId).size());
+ }
+ case PLAYLIST_POSITION:
+ {
+ int currentSong = player.GetCurrentSong();
+ if (currentSong > -1)
+ return std::to_string(currentSong + 1);
+ break;
+ }
+ case PLAYLIST_RANDOM:
+ {
+ if (player.IsShuffled(playlistId))
+ return g_localizeStrings.Get(16041); // 16041: On
+ else
+ return g_localizeStrings.Get(591); // 591: Off
+ }
+ case PLAYLIST_REPEAT:
+ {
+ PLAYLIST::RepeatState state = player.GetRepeat(playlistId);
+ if (state == PLAYLIST::RepeatState::ONE)
+ return g_localizeStrings.Get(592); // 592: One
+ else if (state == PLAYLIST::RepeatState::ALL)
+ return g_localizeStrings.Get(593); // 593: All
+ else
+ return g_localizeStrings.Get(594); // 594: Off
+ }
+ }
+ return std::string();
+}
+
+namespace
+{
+
+bool CheckWindowCondition(CGUIWindow *window, int condition)
+{
+ // check if it satisfies our condition
+ if (!window)
+ return false;
+ if ((condition & WINDOW_CONDITION_HAS_LIST_ITEMS) && !window->HasListItems())
+ return false;
+ if ((condition & WINDOW_CONDITION_IS_MEDIA_WINDOW) && !window->IsMediaWindow())
+ return false;
+ return true;
+}
+
+CGUIWindow* GetWindowWithCondition(int contextWindow, int condition)
+{
+ const CGUIWindowManager& windowMgr = CServiceBroker::GetGUI()->GetWindowManager();
+
+ CGUIWindow *window = windowMgr.GetWindow(contextWindow);
+ if (CheckWindowCondition(window, condition))
+ return window;
+
+ // try topmost dialog
+ window = windowMgr.GetWindow(windowMgr.GetTopmostModalDialog());
+ if (CheckWindowCondition(window, condition))
+ return window;
+
+ // try active window
+ window = windowMgr.GetWindow(windowMgr.GetActiveWindow());
+ if (CheckWindowCondition(window, condition))
+ return window;
+
+ return nullptr;
+}
+
+} // unnamed namespace
+
+CGUIWindow* GetWindow(int contextWindow)
+{
+ return GetWindowWithCondition(contextWindow, 0);
+}
+
+CFileItemPtr GetCurrentListItemFromWindow(int contextWindow)
+{
+ CGUIWindow* window = GetWindowWithCondition(contextWindow, WINDOW_CONDITION_HAS_LIST_ITEMS);
+ if (window)
+ return window->GetCurrentListItem();
+
+ return CFileItemPtr();
+}
+
+CGUIMediaWindow* GetMediaWindow(int contextWindow)
+{
+ CGUIWindow* window = GetWindowWithCondition(contextWindow, WINDOW_CONDITION_IS_MEDIA_WINDOW);
+ if (window)
+ return static_cast<CGUIMediaWindow*>(window);
+
+ return nullptr;
+}
+
+CGUIControl* GetActiveContainer(int containerId, int contextWindow)
+{
+ CGUIWindow *window = GetWindow(contextWindow);
+ if (!window)
+ return nullptr;
+
+ CGUIControl *control = nullptr;
+ if (!containerId) // No container specified, so we lookup the current view container
+ {
+ if (window->IsMediaWindow())
+ containerId = static_cast<CGUIMediaWindow*>(window)->GetViewContainerID();
+ else
+ control = window->GetFocusedControl();
+ }
+
+ if (!control)
+ control = window->GetControl(containerId);
+
+ if (control && control->IsContainer())
+ return control;
+
+ return nullptr;
+}
+
+CGUIListItemPtr GetCurrentListItem(int contextWindow, int containerId /* = 0 */, int itemOffset /* = 0 */, unsigned int itemFlags /* = 0 */)
+{
+ CGUIListItemPtr item;
+
+ if (containerId == 0 &&
+ itemOffset == 0 &&
+ !(itemFlags & INFOFLAG_LISTITEM_CONTAINER) &&
+ !(itemFlags & INFOFLAG_LISTITEM_ABSOLUTE) &&
+ !(itemFlags & INFOFLAG_LISTITEM_POSITION))
+ item = GetCurrentListItemFromWindow(contextWindow);
+
+ if (!item)
+ {
+ CGUIControl* activeContainer = GetActiveContainer(containerId, contextWindow);
+ if (activeContainer)
+ item = static_cast<IGUIContainer *>(activeContainer)->GetListItem(itemOffset, itemFlags);
+ }
+
+ return item;
+}
+
+std::string GetFileInfoLabelValueFromPath(int info, const std::string& filenameAndPath)
+{
+ std::string value = filenameAndPath;
+
+ if (info == PLAYER_PATH)
+ {
+ // do this twice since we want the path outside the archive if this is to be of use.
+ if (URIUtils::IsInArchive(value))
+ value = URIUtils::GetParentPath(value);
+
+ value = URIUtils::GetParentPath(value);
+ }
+ else if (info == PLAYER_FILENAME)
+ {
+ value = URIUtils::GetFileName(value);
+ }
+
+ return value;
+}
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/GUIInfoHelper.h b/xbmc/guilib/guiinfo/GUIInfoHelper.h
new file mode 100644
index 0000000..ce5191e
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoHelper.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "playlists/PlayListTypes.h"
+
+#include <memory>
+#include <string>
+
+class CFileItem;
+
+class CGUIListItem;
+typedef std::shared_ptr<CGUIListItem> CGUIListItemPtr;
+
+class CGUIControl;
+class CGUIMediaWindow;
+class CGUIWindow;
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+std::string GetPlaylistLabel(int item, PLAYLIST::Id playlistid = PLAYLIST::TYPE_NONE);
+
+CGUIWindow* GetWindow(int contextWindow);
+CGUIControl* GetActiveContainer(int containerId, int contextWindow);
+CGUIMediaWindow* GetMediaWindow(int contextWindow);
+CGUIListItemPtr GetCurrentListItem(int contextWindow, int containerId = 0, int itemOffset = 0, unsigned int itemFlags = 0);
+
+std::string GetFileInfoLabelValueFromPath(int info, const std::string& filenameAndPath);
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/GUIInfoLabel.cpp b/xbmc/guilib/guiinfo/GUIInfoLabel.cpp
new file mode 100644
index 0000000..4a6ce13
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoLabel.cpp
@@ -0,0 +1,337 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoLabel.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "addons/Skin.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIListItem.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+using namespace KODI::GUILIB::GUIINFO;
+
+CGUIInfoLabel::CGUIInfoLabel(const std::string &label, const std::string &fallback /*= ""*/, int context /*= 0*/)
+{
+ SetLabel(label, fallback, context);
+}
+
+int CGUIInfoLabel::GetIntValue(int contextWindow) const
+{
+ const std::string label = GetLabel(contextWindow);
+ if (!label.empty())
+ return strtol(label.c_str(), NULL, 10);
+
+ return 0;
+}
+
+void CGUIInfoLabel::SetLabel(const std::string &label, const std::string &fallback, int context /*= 0*/)
+{
+ m_fallback = fallback;
+ Parse(label, context);
+}
+
+const std::string &CGUIInfoLabel::GetLabel(int contextWindow, bool preferImage, std::string *fallback /*= NULL*/) const
+{
+ bool needsUpdate = m_dirty;
+ if (!m_info.empty())
+ {
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ for (const auto &portion : m_info)
+ {
+ if (portion.m_info)
+ {
+ std::string infoLabel;
+ if (preferImage)
+ infoLabel = infoMgr.GetImage(portion.m_info, contextWindow, fallback);
+ if (infoLabel.empty())
+ infoLabel = infoMgr.GetLabel(portion.m_info, contextWindow, fallback);
+ needsUpdate |= portion.NeedsUpdate(infoLabel);
+ }
+ }
+ }
+ else
+ needsUpdate = !m_label.empty();
+
+ return CacheLabel(needsUpdate);
+}
+
+const std::string &CGUIInfoLabel::GetItemLabel(const CGUIListItem *item, bool preferImages, std::string *fallback /*= NULL*/) const
+{
+ bool needsUpdate = m_dirty;
+ if (item->IsFileItem() && !m_info.empty())
+ {
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ for (const auto &portion : m_info)
+ {
+ if (portion.m_info)
+ {
+ std::string infoLabel;
+ if (preferImages)
+ infoLabel = infoMgr.GetItemImage(item, 0, portion.m_info, fallback);
+ else
+ infoLabel = infoMgr.GetItemLabel(static_cast<const CFileItem *>(item), 0, portion.m_info, fallback);
+ needsUpdate |= portion.NeedsUpdate(infoLabel);
+ }
+ }
+ }
+ else
+ needsUpdate = !m_label.empty();
+
+ return CacheLabel(needsUpdate);
+}
+
+const std::string &CGUIInfoLabel::CacheLabel(bool rebuild) const
+{
+ if (rebuild)
+ {
+ m_label.clear();
+ for (const auto &portion : m_info)
+ m_label += portion.Get();
+ m_dirty = false;
+ }
+ if (m_label.empty()) // empty label, use the fallback
+ return m_fallback;
+ return m_label;
+}
+
+bool CGUIInfoLabel::IsEmpty() const
+{
+ return m_info.empty();
+}
+
+bool CGUIInfoLabel::IsConstant() const
+{
+ return m_info.empty() || (m_info.size() == 1 && m_info[0].m_info == 0);
+}
+
+bool CGUIInfoLabel::ReplaceSpecialKeywordReferences(const std::string &strInput, const std::string &strKeyword, const StringReplacerFunc &func, std::string &strOutput)
+{
+ // replace all $strKeyword[value] with resolved strings
+ const std::string dollarStrPrefix = "$" + strKeyword + "[";
+
+ size_t index = 0;
+ size_t startPos;
+
+ while ((startPos = strInput.find(dollarStrPrefix, index)) != std::string::npos)
+ {
+ size_t valuePos = startPos + dollarStrPrefix.size();
+ size_t endPos = StringUtils::FindEndBracket(strInput, '[', ']', valuePos);
+ if (endPos != std::string::npos)
+ {
+ if (index == 0) // first occurrence?
+ strOutput.clear();
+ strOutput.append(strInput, index, startPos - index); // append part from the left side
+ strOutput += func(strInput.substr(valuePos, endPos - valuePos)); // resolve and append value part
+ index = endPos + 1;
+ }
+ else
+ {
+ // if closing bracket is missing, report error and leave incomplete reference in
+ CLog::Log(LOGERROR, "Error parsing value - missing ']' in \"{}\"", strInput);
+ break;
+ }
+ }
+
+ if (index) // if we replaced anything
+ {
+ strOutput.append(strInput, index, std::string::npos); // append leftover from the right side
+ return true;
+ }
+
+ return false;
+}
+
+bool CGUIInfoLabel::ReplaceSpecialKeywordReferences(std::string &work, const std::string &strKeyword, const StringReplacerFunc &func)
+{
+ std::string output;
+ if (ReplaceSpecialKeywordReferences(work, strKeyword, func, output))
+ {
+ work = std::move(output);
+ return true;
+ }
+ return false;
+}
+
+std::string LocalizeReplacer(const std::string &str)
+{
+ std::string replace = g_localizeStringsTemp.Get(atoi(str.c_str()));
+ if (replace.empty())
+ replace = g_localizeStrings.Get(atoi(str.c_str()));
+ return replace;
+}
+
+std::string AddonReplacer(const std::string &str)
+{
+ // assumes "addon.id #####"
+ size_t length = str.find(' ');
+ const std::string addonid = str.substr(0, length);
+ int stringid = atoi(str.substr(length + 1).c_str());
+ return g_localizeStrings.GetAddonString(addonid, stringid);
+}
+
+std::string NumberReplacer(const std::string &str)
+{
+ return str;
+}
+
+std::string CGUIInfoLabel::ReplaceLocalize(const std::string &label)
+{
+ std::string work(label);
+ ReplaceSpecialKeywordReferences(work, "LOCALIZE", LocalizeReplacer);
+ ReplaceSpecialKeywordReferences(work, "NUMBER", NumberReplacer);
+ return work;
+}
+
+std::string CGUIInfoLabel::ReplaceAddonStrings(std::string &&label)
+{
+ ReplaceSpecialKeywordReferences(label, "ADDON", AddonReplacer);
+ return std::move(label);
+}
+
+enum EINFOFORMAT { NONE = 0, FORMATINFO, FORMATESCINFO, FORMATVAR, FORMATESCVAR };
+
+typedef struct
+{
+ const char *str;
+ EINFOFORMAT val;
+} infoformat;
+
+const static infoformat infoformatmap[] = {{ "$INFO[", FORMATINFO},
+ { "$ESCINFO[", FORMATESCINFO},
+ { "$VAR[", FORMATVAR},
+ { "$ESCVAR[", FORMATESCVAR}};
+
+void CGUIInfoLabel::Parse(const std::string &label, int context)
+{
+ m_info.clear();
+ m_dirty = true;
+ // Step 1: Replace all $LOCALIZE[number] with the real string
+ std::string work = ReplaceLocalize(label);
+ // Step 2: Replace all $ADDON[id number] with the real string
+ work = ReplaceAddonStrings(std::move(work));
+ // Step 3: Find all $INFO[info,prefix,postfix] blocks
+ EINFOFORMAT format;
+ do
+ {
+ format = NONE;
+ size_t pos1 = work.size();
+ size_t pos2;
+ size_t len = 0;
+ for (const infoformat& infoformat : infoformatmap)
+ {
+ pos2 = work.find(infoformat.str);
+ if (pos2 != std::string::npos && pos2 < pos1)
+ {
+ pos1 = pos2;
+ len = strlen(infoformat.str);
+ format = infoformat.val;
+ }
+ }
+
+ if (format != NONE)
+ {
+ if (pos1 > 0)
+ m_info.emplace_back(0, work.substr(0, pos1), "");
+
+ pos2 = StringUtils::FindEndBracket(work, '[', ']', pos1 + len);
+ if (pos2 != std::string::npos)
+ {
+ // decipher the block
+ std::vector<std::string> params = StringUtils::Split(work.substr(pos1 + len, pos2 - pos1 - len), ",");
+ if (!params.empty())
+ {
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+
+ int info;
+ if (format == FORMATVAR || format == FORMATESCVAR)
+ {
+ info = infoMgr.TranslateSkinVariableString(params[0], context);
+ if (info == 0)
+ info = infoMgr.RegisterSkinVariableString(g_SkinInfo->CreateSkinVariable(params[0], context));
+ if (info == 0) // skinner didn't define this conditional label!
+ CLog::Log(LOGWARNING, "Label Formatting: $VAR[{}] is not defined", params[0]);
+ }
+ else
+ info = infoMgr.TranslateString(params[0]);
+ std::string prefix, postfix;
+ if (params.size() > 1)
+ prefix = params[1];
+ if (params.size() > 2)
+ postfix = params[2];
+ m_info.emplace_back(info, prefix, postfix, format == FORMATESCINFO || format == FORMATESCVAR);
+ }
+ // and delete it from our work string
+ work.erase(0, pos2 + 1);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Error parsing label - missing ']' in \"{}\"", label);
+ return;
+ }
+ }
+ }
+ while (format != NONE);
+
+ if (!work.empty())
+ m_info.emplace_back(0, work, "");
+}
+
+CGUIInfoLabel::CInfoPortion::CInfoPortion(int info, const std::string &prefix, const std::string &postfix, bool escaped /*= false */):
+ m_prefix(prefix),
+ m_postfix(postfix)
+{
+ m_info = info;
+ m_escaped = escaped;
+ // filter our prefix and postfix for comma's
+ StringUtils::Replace(m_prefix, "$COMMA", ",");
+ StringUtils::Replace(m_postfix, "$COMMA", ",");
+ StringUtils::Replace(m_prefix, "$LBRACKET", "["); StringUtils::Replace(m_prefix, "$RBRACKET", "]");
+ StringUtils::Replace(m_postfix, "$LBRACKET", "["); StringUtils::Replace(m_postfix, "$RBRACKET", "]");
+}
+
+bool CGUIInfoLabel::CInfoPortion::NeedsUpdate(const std::string &label) const
+{
+ if (m_label != label)
+ {
+ m_label = label;
+ return true;
+ }
+ return false;
+}
+
+std::string CGUIInfoLabel::CInfoPortion::Get() const
+{
+ if (!m_info)
+ return m_prefix;
+ else if (m_label.empty())
+ return "";
+ std::string label = m_prefix + m_label + m_postfix;
+ if (m_escaped) // escape all quotes and backslashes, then quote
+ {
+ StringUtils::Replace(label, "\\", "\\\\");
+ StringUtils::Replace(label, "\"", "\\\"");
+ return "\"" + label + "\"";
+ }
+ return label;
+}
+
+std::string CGUIInfoLabel::GetLabel(const std::string &label, int contextWindow /*= 0*/, bool preferImage /*= false */)
+{ // translate the label
+ const CGUIInfoLabel info(label, "", contextWindow);
+ return info.GetLabel(contextWindow, preferImage);
+}
+
+std::string CGUIInfoLabel::GetItemLabel(const std::string &label, const CGUIListItem *item, bool preferImage /*= false */)
+{ // translate the label
+ const CGUIInfoLabel info(label);
+ return info.GetItemLabel(item, preferImage);
+}
diff --git a/xbmc/guilib/guiinfo/GUIInfoLabel.h b/xbmc/guilib/guiinfo/GUIInfoLabel.h
new file mode 100644
index 0000000..c9ae1a8
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoLabel.h
@@ -0,0 +1,146 @@
+/*
+ * 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
+
+/*!
+\file GUIInfoLabel.h
+\brief
+*/
+
+#include "interfaces/info/Info.h"
+
+#include <functional>
+#include <string>
+#include <vector>
+
+class CGUIListItem;
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfoLabel
+{
+public:
+ CGUIInfoLabel() = default;
+ CGUIInfoLabel(const std::string& label,
+ const std::string& fallback = "",
+ int context = INFO::DEFAULT_CONTEXT);
+
+ void SetLabel(const std::string& label,
+ const std::string& fallback,
+ int context = INFO::DEFAULT_CONTEXT);
+
+ /*!
+ \brief Gets a label (or image) for a given window context from the info manager.
+ \param contextWindow the context in which to evaluate the expression.
+ \param preferImage caller is specifically wanting an image rather than a label. Defaults to false.
+ \param fallback if non-NULL, is set to an alternate value to use should the actual value be not appropriate. Defaults to NULL.
+ \return label (or image).
+ */
+ const std::string &GetLabel(int contextWindow, bool preferImage = false, std::string *fallback = NULL) const;
+
+ /*!
+ \brief Gets the label and returns it as an int value
+ \param contextWindow the context in which to evaluate the expression.
+ \return int value.
+ \sa GetLabel
+ */
+ int GetIntValue(int contextWindow) const;
+
+ /*!
+ \brief Gets a label (or image) for a given listitem from the info manager.
+ \param item listitem in question.
+ \param preferImage caller is specifically wanting an image rather than a label. Defaults to false.
+ \param fallback if non-NULL, is set to an alternate value to use should the actual value be not appropriate. Defaults to NULL.
+ \return label (or image).
+ */
+ const std::string &GetItemLabel(const CGUIListItem *item, bool preferImage = false, std::string *fallback = NULL) const;
+
+ bool IsConstant() const;
+ bool IsEmpty() const;
+
+ const std::string& GetFallback() const { return m_fallback; }
+
+ static std::string GetLabel(const std::string& label,
+ int contextWindow,
+ bool preferImage = false);
+ static std::string GetItemLabel(const std::string &label, const CGUIListItem *item, bool preferImage = false);
+
+ /*!
+ \brief Replaces instances of $LOCALIZE[number] with the appropriate localized string
+ \param label text to replace
+ \return text with any localized strings filled in.
+ */
+ static std::string ReplaceLocalize(const std::string &label);
+
+ /*!
+ \brief Replaces instances of $ADDON[id number] with the appropriate localized addon string
+ \param label text to replace
+ \return text with any localized strings filled in.
+ */
+ static std::string ReplaceAddonStrings(std::string &&label);
+
+ typedef std::function<std::string(const std::string&)> StringReplacerFunc;
+
+ /*!
+ \brief Replaces instances of $strKeyword[value] with the appropriate resolved string
+ \param strInput text to replace
+ \param strKeyword keyword to look for
+ \param func function that does the actual replacement of each bracketed value found
+ \param strOutput the output string
+ \return whether anything has been replaced.
+ */
+ static bool ReplaceSpecialKeywordReferences(const std::string &strInput, const std::string &strKeyword, const StringReplacerFunc &func, std::string &strOutput);
+
+ /*!
+ \brief Replaces instances of $strKeyword[value] with the appropriate resolved string in-place
+ \param work text to replace in-place
+ \param strKeyword keyword to look for
+ \param func function that does the actual replacement of each bracketed value found
+ \return whether anything has been replaced.
+ */
+ static bool ReplaceSpecialKeywordReferences(std::string &work, const std::string &strKeyword, const StringReplacerFunc &func);
+
+private:
+ void Parse(const std::string &label, int context);
+
+ /*! \brief return (and cache) built label from info portions.
+ \param rebuild whether we need to rebuild the label
+ \sa GetLabel, GetItemLabel
+ */
+ const std::string &CacheLabel(bool rebuild) const;
+
+ class CInfoPortion
+ {
+ public:
+ CInfoPortion(int info, const std::string &prefix, const std::string &postfix, bool escaped = false);
+ bool NeedsUpdate(const std::string &label) const;
+ std::string Get() const;
+ int m_info;
+ private:
+ bool m_escaped;
+ mutable std::string m_label;
+ std::string m_prefix;
+ std::string m_postfix;
+ };
+
+ mutable bool m_dirty = false;
+ mutable std::string m_label;
+ std::string m_fallback;
+ std::vector<CInfoPortion> m_info;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
+
diff --git a/xbmc/guilib/guiinfo/GUIInfoLabels.h b/xbmc/guilib/guiinfo/GUIInfoLabels.h
new file mode 100644
index 0000000..3e2b163
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoLabels.h
@@ -0,0 +1,985 @@
+/*
+ * 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
+
+// clang-format off
+#define PLAYER_HAS_MEDIA 1
+#define PLAYER_HAS_AUDIO 2
+#define PLAYER_HAS_VIDEO 3
+#define PLAYER_PLAYING 4
+#define PLAYER_PAUSED 5
+#define PLAYER_REWINDING 6
+#define PLAYER_REWINDING_2x 7
+#define PLAYER_REWINDING_4x 8
+#define PLAYER_REWINDING_8x 9
+#define PLAYER_REWINDING_16x 10
+#define PLAYER_REWINDING_32x 11
+#define PLAYER_FORWARDING 12
+#define PLAYER_FORWARDING_2x 13
+#define PLAYER_FORWARDING_4x 14
+#define PLAYER_FORWARDING_8x 15
+#define PLAYER_FORWARDING_16x 16
+#define PLAYER_FORWARDING_32x 17
+#define PLAYER_CACHING 20
+// unused id 21
+#define PLAYER_PROGRESS 22
+#define PLAYER_SEEKBAR 23
+#define PLAYER_SEEKTIME 24
+#define PLAYER_SEEKING 25
+#define PLAYER_SHOWTIME 26
+#define PLAYER_TIME 27
+#define PLAYER_TIME_REMAINING 28
+#define PLAYER_DURATION 29
+#define PLAYER_HASPERFORMEDSEEK 30
+#define PLAYER_SHOWINFO 31
+#define PLAYER_VOLUME 32
+#define PLAYER_MUTED 33
+#define PLAYER_HASDURATION 34
+#define PLAYER_CHAPTER 35
+#define PLAYER_CHAPTERCOUNT 36
+#define PLAYER_TIME_SPEED 37
+#define PLAYER_FINISH_TIME 38
+#define PLAYER_CACHELEVEL 39
+#define PLAYER_CHAPTERNAME 41
+#define PLAYER_SUBTITLE_DELAY 42
+#define PLAYER_AUDIO_DELAY 43
+#define PLAYER_PASSTHROUGH 44
+// unused 45
+// unused 46
+#define PLAYER_SEEKOFFSET 47
+#define PLAYER_PROGRESS_CACHE 48
+#define PLAYER_ITEM_ART 49
+#define PLAYER_CAN_PAUSE 50
+#define PLAYER_CAN_SEEK 51
+#define PLAYER_START_TIME 52
+// unused 53
+#define PLAYER_ISINTERNETSTREAM 54
+// unused 55
+#define PLAYER_SEEKSTEPSIZE 56
+#define PLAYER_IS_CHANNEL_PREVIEW_ACTIVE 57
+#define PLAYER_SUPPORTS_TEMPO 58
+#define PLAYER_IS_TEMPO 59
+#define PLAYER_PLAYSPEED 60
+#define PLAYER_SEEKNUMERIC 61
+#define PLAYER_HAS_GAME 62
+#define PLAYER_HAS_PROGRAMS 63
+#define PLAYER_HAS_RESOLUTIONS 64
+#define PLAYER_FRAMEADVANCE 65
+#define PLAYER_ICON 66
+#define PLAYER_CUTLIST 67
+#define PLAYER_CHAPTERS 68
+#define PLAYER_EDITLIST 69
+#define PLAYER_CUTS 70
+#define PLAYER_SCENE_MARKERS 71
+#define PLAYER_HAS_SCENE_MARKERS 72
+// Keep player infolabels that work with offset and position together
+#define PLAYER_PATH 81
+#define PLAYER_FILEPATH 82
+#define PLAYER_TITLE 83
+#define PLAYER_FILENAME 84
+
+// Range of player infolabels that work with offset and position
+#define PLAYER_OFFSET_POSITION_FIRST PLAYER_PATH
+#define PLAYER_OFFSET_POSITION_LAST PLAYER_FILENAME
+
+#define WEATHER_CONDITIONS_TEXT 100
+#define WEATHER_TEMPERATURE 101
+#define WEATHER_LOCATION 102
+#define WEATHER_IS_FETCHED 103
+#define WEATHER_FANART_CODE 104
+#define WEATHER_PLUGIN 105
+#define WEATHER_CONDITIONS_ICON 106
+
+#define SYSTEM_TEMPERATURE_UNITS 107
+#define SYSTEM_PROGRESS_BAR 108
+#define SYSTEM_LANGUAGE 109
+#define SYSTEM_TIME 110
+#define SYSTEM_DATE 111
+#define SYSTEM_CPU_TEMPERATURE 112
+#define SYSTEM_GPU_TEMPERATURE 113
+#define SYSTEM_FAN_SPEED 114
+#define SYSTEM_FREE_SPACE_C 115
+// #define SYSTEM_FREE_SPACE_D 116 //116 is reserved for space on D
+#define SYSTEM_FREE_SPACE_E 117
+#define SYSTEM_FREE_SPACE_F 118
+#define SYSTEM_FREE_SPACE_G 119
+#define SYSTEM_BUILD_VERSION 120
+#define SYSTEM_BUILD_DATE 121
+#define SYSTEM_ETHERNET_LINK_ACTIVE 122
+#define SYSTEM_FPS 123
+#define SYSTEM_ALWAYS_TRUE 125 // useful for <visible fade="10" start="hidden">true</visible>, to fade in a control
+#define SYSTEM_ALWAYS_FALSE 126 // used for <visible fade="10">false</visible>, to fade out a control (ie not particularly useful!)
+#define SYSTEM_MEDIA_DVD 127
+#define SYSTEM_DVDREADY 128
+#define SYSTEM_HAS_ALARM 129
+#define SYSTEM_SUPPORTS_CPU_USAGE 130
+#define SYSTEM_SCREEN_MODE 132
+#define SYSTEM_SCREEN_WIDTH 133
+#define SYSTEM_SCREEN_HEIGHT 134
+#define SYSTEM_CURRENT_WINDOW 135
+#define SYSTEM_CURRENT_CONTROL 136
+#define SYSTEM_CURRENT_CONTROL_ID 137
+#define SYSTEM_DVD_LABEL 138
+#define SYSTEM_HASLOCKS 140
+#define SYSTEM_ISMASTER 141
+#define SYSTEM_TRAYOPEN 142
+#define SYSTEM_SHOW_EXIT_BUTTON 143
+#define SYSTEM_ALARM_POS 144
+#define SYSTEM_LOGGEDON 145
+#define SYSTEM_PROFILENAME 146
+#define SYSTEM_PROFILETHUMB 147
+#define SYSTEM_HAS_LOGINSCREEN 148
+#define SYSTEM_HAS_ACTIVE_MODAL_DIALOG 149
+#define SYSTEM_HDD_SMART 150
+#define SYSTEM_HDD_TEMPERATURE 151
+#define SYSTEM_HDD_MODEL 152
+#define SYSTEM_HDD_SERIAL 153
+#define SYSTEM_HDD_FIRMWARE 154
+#define SYSTEM_HAS_VISIBLE_MODAL_DIALOG 155
+#define SYSTEM_HDD_PASSWORD 156
+#define SYSTEM_HDD_LOCKSTATE 157
+#define SYSTEM_HDD_LOCKKEY 158
+#define SYSTEM_INTERNET_STATE 159
+#define SYSTEM_HAS_INPUT_HIDDEN 160
+#define SYSTEM_HAS_PVR_ADDON 161
+#define SYSTEM_ALARM_LESS_OR_EQUAL 180
+#define SYSTEM_PROFILECOUNT 181
+#define SYSTEM_ISFULLSCREEN 182
+#define SYSTEM_ISSTANDALONE 183
+#define SYSTEM_IDLE_SHUTDOWN_INHIBITED 184
+#define SYSTEM_HAS_SHUTDOWN 185
+#define SYSTEM_HAS_PVR 186
+#define SYSTEM_STARTUP_WINDOW 187
+#define SYSTEM_STEREOSCOPIC_MODE 188
+#define SYSTEM_BUILD_VERSION_SHORT 189
+
+#define NETWORK_IP_ADDRESS 190
+#define NETWORK_MAC_ADDRESS 191
+#define NETWORK_IS_DHCP 192
+#define NETWORK_LINK_STATE 193
+#define NETWORK_SUBNET_MASK 194
+#define NETWORK_GATEWAY_ADDRESS 195
+#define NETWORK_DNS1_ADDRESS 196
+#define NETWORK_DNS2_ADDRESS 197
+#define NETWORK_DHCP_ADDRESS 198
+
+// Keep musicplayer infolabels that work with offset and position together
+#define MUSICPLAYER_TITLE 200
+#define MUSICPLAYER_ALBUM 201
+#define MUSICPLAYER_ARTIST 202
+#define MUSICPLAYER_GENRE 203
+#define MUSICPLAYER_YEAR 204
+#define MUSICPLAYER_DURATION 205
+#define MUSICPLAYER_TRACK_NUMBER 208
+#define MUSICPLAYER_COVER 210
+#define MUSICPLAYER_BITRATE 211
+#define MUSICPLAYER_PLAYLISTLEN 212
+#define MUSICPLAYER_PLAYLISTPOS 213
+#define MUSICPLAYER_CHANNELS 214
+#define MUSICPLAYER_BITSPERSAMPLE 215
+#define MUSICPLAYER_SAMPLERATE 216
+#define MUSICPLAYER_CODEC 217
+#define MUSICPLAYER_DISC_NUMBER 218
+#define MUSICPLAYER_RATING 219
+#define MUSICPLAYER_COMMENT 220
+#define MUSICPLAYER_LYRICS 221
+#define MUSICPLAYER_ALBUM_ARTIST 222
+#define MUSICPLAYER_PLAYCOUNT 223
+#define MUSICPLAYER_LASTPLAYED 224
+#define MUSICPLAYER_USER_RATING 225
+#define MUSICPLAYER_RATING_AND_VOTES 226
+#define MUSICPLAYER_VOTES 227
+#define MUSICPLAYER_MOOD 228
+#define MUSICPLAYER_CONTRIBUTORS 229
+#define MUSICPLAYER_CONTRIBUTOR_AND_ROLE 230
+#define MUSICPLAYER_DBID 231
+#define MUSICPLAYER_DISC_TITLE 232
+#define MUSICPLAYER_RELEASEDATE 233
+#define MUSICPLAYER_ORIGINALDATE 234
+#define MUSICPLAYER_BPM 235
+
+// Range of musicplayer infolabels that work with offset and position
+#define MUSICPLAYER_OFFSET_POSITION_FIRST MUSICPLAYER_TITLE
+#define MUSICPLAYER_OFFSET_POSITION_LAST MUSICPLAYER_BPM
+
+#define MUSICPLAYER_PROPERTY 236
+#define MUSICPLAYER_CHANNEL_NAME 237
+#define MUSICPLAYER_CHANNEL_GROUP 238
+#define MUSICPLAYER_CHANNEL_NUMBER 239
+#define MUSICPLAYER_TOTALDISCS 240
+#define MUSICPLAYER_STATIONNAME 241
+
+// Musicplayer infobools
+#define MUSICPLAYER_HASPREVIOUS 242
+#define MUSICPLAYER_HASNEXT 243
+#define MUSICPLAYER_EXISTS 244
+#define MUSICPLAYER_PLAYLISTPLAYING 245
+#define MUSICPLAYER_CONTENT 246
+#define MUSICPLAYER_ISMULTIDISC 247
+
+// Videoplayer infolabels
+#define VIDEOPLAYER_HDR_TYPE 249
+// Keep videoplayer infolabels that work with offset and position together
+#define VIDEOPLAYER_TITLE 250
+#define VIDEOPLAYER_GENRE 251
+#define VIDEOPLAYER_DIRECTOR 252
+#define VIDEOPLAYER_YEAR 253
+#define VIDEOPLAYER_COVER 254
+#define VIDEOPLAYER_ORIGINALTITLE 255
+#define VIDEOPLAYER_PLOT 256
+#define VIDEOPLAYER_PLOT_OUTLINE 257
+#define VIDEOPLAYER_EPISODE 258
+#define VIDEOPLAYER_SEASON 259
+#define VIDEOPLAYER_RATING 260
+#define VIDEOPLAYER_TVSHOW 261
+#define VIDEOPLAYER_PREMIERED 262
+#define VIDEOPLAYER_STUDIO 263
+#define VIDEOPLAYER_MPAA 264
+#define VIDEOPLAYER_ARTIST 265
+#define VIDEOPLAYER_ALBUM 266
+#define VIDEOPLAYER_WRITER 267
+#define VIDEOPLAYER_TAGLINE 268
+#define VIDEOPLAYER_TOP250 269
+#define VIDEOPLAYER_RATING_AND_VOTES 270
+#define VIDEOPLAYER_TRAILER 271
+#define VIDEOPLAYER_COUNTRY 272
+#define VIDEOPLAYER_PLAYCOUNT 273
+#define VIDEOPLAYER_LASTPLAYED 274
+#define VIDEOPLAYER_VOTES 275
+#define VIDEOPLAYER_IMDBNUMBER 276
+#define VIDEOPLAYER_USER_RATING 277
+#define VIDEOPLAYER_DBID 278
+#define VIDEOPLAYER_TVSHOWDBID 279
+#define VIDEOPLAYER_ART 280
+
+// Range of videoplayer infolabels that work with offset and position
+#define VIDEOPLAYER_OFFSET_POSITION_FIRST VIDEOPLAYER_TITLE
+#define VIDEOPLAYER_OFFSET_POSITION_LAST VIDEOPLAYER_ART
+
+#define VIDEOPLAYER_AUDIO_BITRATE 281
+#define VIDEOPLAYER_VIDEO_BITRATE 282
+#define VIDEOPLAYER_VIDEO_CODEC 283
+#define VIDEOPLAYER_VIDEO_RESOLUTION 284
+#define VIDEOPLAYER_AUDIO_CODEC 285
+#define VIDEOPLAYER_AUDIO_CHANNELS 286
+#define VIDEOPLAYER_VIDEO_ASPECT 287
+#define VIDEOPLAYER_SUBTITLES_LANG 288
+#define VIDEOPLAYER_AUDIO_LANG 290
+#define VIDEOPLAYER_STEREOSCOPIC_MODE 291
+#define VIDEOPLAYER_CAST 292
+#define VIDEOPLAYER_CAST_AND_ROLE 293
+#define VIDEOPLAYER_UNIQUEID 294
+#define VIDEOPLAYER_AUDIOSTREAMCOUNT 295
+
+// Videoplayer infobools
+#define VIDEOPLAYER_HASSUBTITLES 300
+#define VIDEOPLAYER_SUBTITLESENABLED 301
+#define VIDEOPLAYER_USING_OVERLAYS 302
+#define VIDEOPLAYER_ISFULLSCREEN 303
+#define VIDEOPLAYER_HASMENU 304
+#define VIDEOPLAYER_PLAYLISTLEN 305
+#define VIDEOPLAYER_PLAYLISTPOS 306
+#define VIDEOPLAYER_CONTENT 307
+#define VIDEOPLAYER_HAS_INFO 308
+#define VIDEOPLAYER_HASTELETEXT 309
+#define VIDEOPLAYER_IS_STEREOSCOPIC 310
+
+// PVR infolabels
+#define VIDEOPLAYER_EVENT 313
+#define VIDEOPLAYER_EPISODENAME 314
+#define VIDEOPLAYER_STARTTIME 315
+#define VIDEOPLAYER_ENDTIME 316
+#define VIDEOPLAYER_NEXT_TITLE 317
+#define VIDEOPLAYER_NEXT_GENRE 318
+#define VIDEOPLAYER_NEXT_PLOT 319
+#define VIDEOPLAYER_NEXT_PLOT_OUTLINE 320
+#define VIDEOPLAYER_NEXT_STARTTIME 321
+#define VIDEOPLAYER_NEXT_ENDTIME 322
+#define VIDEOPLAYER_NEXT_DURATION 323
+#define VIDEOPLAYER_CHANNEL_NAME 324
+#define VIDEOPLAYER_CHANNEL_GROUP 325
+#define VIDEOPLAYER_PARENTAL_RATING 326
+#define VIDEOPLAYER_CHANNEL_NUMBER 327
+
+// PVR infobools
+#define VIDEOPLAYER_HAS_EPG 328
+#define VIDEOPLAYER_CAN_RESUME_LIVE_TV 329
+
+#define RETROPLAYER_VIDEO_FILTER 330
+#define RETROPLAYER_STRETCH_MODE 331
+#define RETROPLAYER_VIDEO_ROTATION 332
+
+#define CONTAINER_HAS_PARENT_ITEM 341
+#define CONTAINER_CAN_FILTER 342
+#define CONTAINER_CAN_FILTERADVANCED 343
+#define CONTAINER_FILTERED 344
+
+#define CONTAINER_SCROLL_PREVIOUS 345
+#define CONTAINER_MOVE_PREVIOUS 346
+// unused 347
+#define CONTAINER_MOVE_NEXT 348
+#define CONTAINER_SCROLL_NEXT 349
+#define CONTAINER_ISUPDATING 350
+#define CONTAINER_HASFILES 351
+#define CONTAINER_HASFOLDERS 352
+#define CONTAINER_STACKED 353
+#define CONTAINER_FOLDERNAME 354
+#define CONTAINER_SCROLLING 355
+#define CONTAINER_PLUGINNAME 356
+#define CONTAINER_PROPERTY 357
+#define CONTAINER_SORT_DIRECTION 358
+#define CONTAINER_NUM_ITEMS 359
+#define CONTAINER_FOLDERPATH 360
+#define CONTAINER_CONTENT 361
+#define CONTAINER_HAS_THUMB 362
+#define CONTAINER_SORT_METHOD 363
+#define CONTAINER_CURRENT_ITEM 364
+#define CONTAINER_ART 365
+#define CONTAINER_HAS_FOCUS 366
+#define CONTAINER_ROW 367
+#define CONTAINER_COLUMN 368
+#define CONTAINER_POSITION 369
+#define CONTAINER_VIEWMODE 370
+#define CONTAINER_HAS_NEXT 371
+#define CONTAINER_HAS_PREVIOUS 372
+#define CONTAINER_SUBITEM 373
+#define CONTAINER_NUM_PAGES 374
+#define CONTAINER_CURRENT_PAGE 375
+#define CONTAINER_SHOWPLOT 376
+#define CONTAINER_TOTALTIME 377
+#define CONTAINER_SORT_ORDER 378
+#define CONTAINER_TOTALWATCHED 379
+#define CONTAINER_TOTALUNWATCHED 380
+#define CONTAINER_VIEWCOUNT 381
+#define CONTAINER_SHOWTITLE 382
+#define CONTAINER_PLUGINCATEGORY 383
+#define CONTAINER_NUM_ALL_ITEMS 384
+#define CONTAINER_NUM_NONFOLDER_ITEMS 385
+
+#define MUSICPM_ENABLED 390
+#define MUSICPM_SONGSPLAYED 391
+#define MUSICPM_MATCHINGSONGS 392
+#define MUSICPM_MATCHINGSONGSPICKED 393
+#define MUSICPM_MATCHINGSONGSLEFT 394
+#define MUSICPM_RELAXEDSONGSPICKED 395
+#define MUSICPM_RANDOMSONGSPICKED 396
+
+#define PLAYLIST_LENGTH 400
+#define PLAYLIST_POSITION 401
+#define PLAYLIST_RANDOM 402
+#define PLAYLIST_REPEAT 403
+#define PLAYLIST_ISRANDOM 404
+#define PLAYLIST_ISREPEAT 405
+#define PLAYLIST_ISREPEATONE 406
+
+#define VISUALISATION_LOCKED 410
+#define VISUALISATION_PRESET 411
+#define VISUALISATION_NAME 412
+#define VISUALISATION_ENABLED 413
+#define VISUALISATION_HAS_PRESETS 414
+
+#define STRING_IS_EMPTY 420
+#define STRING_IS_EQUAL 421
+#define STRING_STARTS_WITH 422
+#define STRING_ENDS_WITH 423
+#define STRING_CONTAINS 424
+
+#define INTEGER_IS_EQUAL 450
+#define INTEGER_GREATER_THAN 451
+#define INTEGER_GREATER_OR_EQUAL 452
+#define INTEGER_LESS_THAN 453
+#define INTEGER_LESS_OR_EQUAL 454
+#define INTEGER_EVEN 455
+#define INTEGER_ODD 456
+#define INTEGER_VALUEOF 457
+
+#define SKIN_BOOL 600
+#define SKIN_STRING 601
+#define SKIN_STRING_IS_EQUAL 602
+#define SKIN_THEME 604
+#define SKIN_COLOUR_THEME 605
+#define SKIN_HAS_THEME 606
+#define SKIN_ASPECT_RATIO 607
+#define SKIN_FONT 608
+#define SKIN_INTEGER 609
+#define SKIN_TIMER_IS_RUNNING 610
+#define SKIN_TIMER_ELAPSEDSECS 611
+
+#define SYSTEM_IS_SCREENSAVER_INHIBITED 641
+#define SYSTEM_ADDON_UPDATE_COUNT 642
+#define SYSTEM_PRIVACY_POLICY 643
+#define SYSTEM_TOTAL_MEMORY 644
+#define SYSTEM_CPU_USAGE 645
+#define SYSTEM_USED_MEMORY_PERCENT 646
+#define SYSTEM_USED_MEMORY 647
+#define SYSTEM_FREE_MEMORY 648
+#define SYSTEM_FREE_MEMORY_PERCENT 649
+#define SYSTEM_UPTIME 654
+#define SYSTEM_TOTALUPTIME 655
+#define SYSTEM_CPUFREQUENCY 656
+#define SYSTEM_SCREEN_RESOLUTION 659
+#define SYSTEM_VIDEO_ENCODER_INFO 660
+#define SYSTEM_OS_VERSION_INFO 667
+#define SYSTEM_FREE_SPACE 679
+#define SYSTEM_USED_SPACE 680
+#define SYSTEM_TOTAL_SPACE 681
+#define SYSTEM_USED_SPACE_PERCENT 682
+#define SYSTEM_FREE_SPACE_PERCENT 683
+#define SYSTEM_ADDON_IS_ENABLED 703
+#define SYSTEM_GET_BOOL 704
+#define SYSTEM_GET_CORE_USAGE 705
+#define SYSTEM_HAS_CORE_ID 706
+#define SYSTEM_RENDER_VENDOR 707
+#define SYSTEM_RENDER_RENDERER 708
+#define SYSTEM_RENDER_VERSION 709
+#define SYSTEM_SETTING 710
+#define SYSTEM_HAS_ADDON 711
+#define SYSTEM_ADDON_TITLE 712
+#define SYSTEM_ADDON_ICON 713
+#define SYSTEM_BATTERY_LEVEL 714
+#define SYSTEM_IDLE_TIME 715
+#define SYSTEM_FRIENDLY_NAME 716
+#define SYSTEM_SCREENSAVER_ACTIVE 717
+#define SYSTEM_ADDON_VERSION 718
+#define SYSTEM_DPMS_ACTIVE 719
+
+#define LIBRARY_HAS_MUSIC 720
+#define LIBRARY_HAS_VIDEO 721
+#define LIBRARY_HAS_MOVIES 722
+#define LIBRARY_HAS_MOVIE_SETS 723
+#define LIBRARY_HAS_TVSHOWS 724
+#define LIBRARY_HAS_MUSICVIDEOS 725
+#define LIBRARY_HAS_SINGLES 726
+#define LIBRARY_HAS_COMPILATIONS 727
+#define LIBRARY_IS_SCANNING 728
+#define LIBRARY_IS_SCANNING_VIDEO 729
+#define LIBRARY_IS_SCANNING_MUSIC 730
+#define LIBRARY_HAS_ROLE 735
+#define LIBRARY_HAS_BOXSETS 736
+#define LIBRARY_HAS_NODE 737
+
+#define SYSTEM_PLATFORM_LINUX 741
+#define SYSTEM_PLATFORM_WINDOWS 742
+#define SYSTEM_PLATFORM_DARWIN 743
+#define SYSTEM_PLATFORM_DARWIN_OSX 744
+#define SYSTEM_PLATFORM_DARWIN_IOS 745
+#define SYSTEM_PLATFORM_UWP 746
+#define SYSTEM_PLATFORM_ANDROID 747
+#define SYSTEM_PLATFORM_WINDOWING 748
+#define SYSTEM_PLATFORM_WIN10 749
+
+#define SYSTEM_CAN_POWERDOWN 750
+#define SYSTEM_CAN_SUSPEND 751
+#define SYSTEM_CAN_HIBERNATE 752
+#define SYSTEM_CAN_REBOOT 753
+#define SYSTEM_MEDIA_AUDIO_CD 754
+
+#define SYSTEM_PLATFORM_DARWIN_TVOS 755
+#define SYSTEM_SUPPORTED_HDR_TYPES 756
+
+#define SLIDESHOW_ISPAUSED 800
+#define SLIDESHOW_ISRANDOM 801
+#define SLIDESHOW_ISACTIVE 802
+#define SLIDESHOW_ISVIDEO 803
+
+#define SLIDESHOW_LABELS_START 900
+#define SLIDESHOW_FILE_NAME (SLIDESHOW_LABELS_START)
+#define SLIDESHOW_FILE_PATH (SLIDESHOW_LABELS_START + 1)
+#define SLIDESHOW_FILE_SIZE (SLIDESHOW_LABELS_START + 2)
+#define SLIDESHOW_FILE_DATE (SLIDESHOW_LABELS_START + 3)
+#define SLIDESHOW_INDEX (SLIDESHOW_LABELS_START + 4)
+#define SLIDESHOW_RESOLUTION (SLIDESHOW_LABELS_START + 5)
+#define SLIDESHOW_COMMENT (SLIDESHOW_LABELS_START + 6)
+#define SLIDESHOW_COLOUR (SLIDESHOW_LABELS_START + 7)
+#define SLIDESHOW_PROCESS (SLIDESHOW_LABELS_START + 8)
+
+#define SLIDESHOW_EXIF_LONG_DATE (SLIDESHOW_LABELS_START + 17)
+#define SLIDESHOW_EXIF_LONG_DATE_TIME (SLIDESHOW_LABELS_START + 18)
+#define SLIDESHOW_EXIF_DATE (SLIDESHOW_LABELS_START + 19) /* Implementation only to just get localized date */
+#define SLIDESHOW_EXIF_DATE_TIME (SLIDESHOW_LABELS_START + 20)
+#define SLIDESHOW_EXIF_DESCRIPTION (SLIDESHOW_LABELS_START + 21)
+#define SLIDESHOW_EXIF_CAMERA_MAKE (SLIDESHOW_LABELS_START + 22)
+#define SLIDESHOW_EXIF_CAMERA_MODEL (SLIDESHOW_LABELS_START + 23)
+#define SLIDESHOW_EXIF_COMMENT (SLIDESHOW_LABELS_START + 24)
+#define SLIDESHOW_EXIF_SOFTWARE (SLIDESHOW_LABELS_START + 25)
+#define SLIDESHOW_EXIF_APERTURE (SLIDESHOW_LABELS_START + 26)
+#define SLIDESHOW_EXIF_FOCAL_LENGTH (SLIDESHOW_LABELS_START + 27)
+#define SLIDESHOW_EXIF_FOCUS_DIST (SLIDESHOW_LABELS_START + 28)
+#define SLIDESHOW_EXIF_EXPOSURE (SLIDESHOW_LABELS_START + 29)
+#define SLIDESHOW_EXIF_EXPOSURE_TIME (SLIDESHOW_LABELS_START + 30)
+#define SLIDESHOW_EXIF_EXPOSURE_BIAS (SLIDESHOW_LABELS_START + 31)
+#define SLIDESHOW_EXIF_EXPOSURE_MODE (SLIDESHOW_LABELS_START + 32)
+#define SLIDESHOW_EXIF_FLASH_USED (SLIDESHOW_LABELS_START + 33)
+#define SLIDESHOW_EXIF_WHITE_BALANCE (SLIDESHOW_LABELS_START + 34)
+#define SLIDESHOW_EXIF_LIGHT_SOURCE (SLIDESHOW_LABELS_START + 35)
+#define SLIDESHOW_EXIF_METERING_MODE (SLIDESHOW_LABELS_START + 36)
+#define SLIDESHOW_EXIF_ISO_EQUIV (SLIDESHOW_LABELS_START + 37)
+#define SLIDESHOW_EXIF_DIGITAL_ZOOM (SLIDESHOW_LABELS_START + 38)
+#define SLIDESHOW_EXIF_CCD_WIDTH (SLIDESHOW_LABELS_START + 39)
+#define SLIDESHOW_EXIF_GPS_LATITUDE (SLIDESHOW_LABELS_START + 40)
+#define SLIDESHOW_EXIF_GPS_LONGITUDE (SLIDESHOW_LABELS_START + 41)
+#define SLIDESHOW_EXIF_GPS_ALTITUDE (SLIDESHOW_LABELS_START + 42)
+#define SLIDESHOW_EXIF_ORIENTATION (SLIDESHOW_LABELS_START + 43)
+#define SLIDESHOW_EXIF_XPCOMMENT (SLIDESHOW_LABELS_START + 44)
+
+#define SLIDESHOW_IPTC_SUBLOCATION (SLIDESHOW_LABELS_START + 57)
+#define SLIDESHOW_IPTC_IMAGETYPE (SLIDESHOW_LABELS_START + 58)
+#define SLIDESHOW_IPTC_TIMECREATED (SLIDESHOW_LABELS_START + 59)
+#define SLIDESHOW_IPTC_SUP_CATEGORIES (SLIDESHOW_LABELS_START + 60)
+#define SLIDESHOW_IPTC_KEYWORDS (SLIDESHOW_LABELS_START + 61)
+#define SLIDESHOW_IPTC_CAPTION (SLIDESHOW_LABELS_START + 62)
+#define SLIDESHOW_IPTC_AUTHOR (SLIDESHOW_LABELS_START + 63)
+#define SLIDESHOW_IPTC_HEADLINE (SLIDESHOW_LABELS_START + 64)
+#define SLIDESHOW_IPTC_SPEC_INSTR (SLIDESHOW_LABELS_START + 65)
+#define SLIDESHOW_IPTC_CATEGORY (SLIDESHOW_LABELS_START + 66)
+#define SLIDESHOW_IPTC_BYLINE (SLIDESHOW_LABELS_START + 67)
+#define SLIDESHOW_IPTC_BYLINE_TITLE (SLIDESHOW_LABELS_START + 68)
+#define SLIDESHOW_IPTC_CREDIT (SLIDESHOW_LABELS_START + 69)
+#define SLIDESHOW_IPTC_SOURCE (SLIDESHOW_LABELS_START + 70)
+#define SLIDESHOW_IPTC_COPYRIGHT_NOTICE (SLIDESHOW_LABELS_START + 71)
+#define SLIDESHOW_IPTC_OBJECT_NAME (SLIDESHOW_LABELS_START + 72)
+#define SLIDESHOW_IPTC_CITY (SLIDESHOW_LABELS_START + 73)
+#define SLIDESHOW_IPTC_STATE (SLIDESHOW_LABELS_START + 74)
+#define SLIDESHOW_IPTC_COUNTRY (SLIDESHOW_LABELS_START + 75)
+#define SLIDESHOW_IPTC_TX_REFERENCE (SLIDESHOW_LABELS_START + 76)
+#define SLIDESHOW_IPTC_DATE (SLIDESHOW_LABELS_START + 77)
+#define SLIDESHOW_IPTC_URGENCY (SLIDESHOW_LABELS_START + 78)
+#define SLIDESHOW_IPTC_COUNTRY_CODE (SLIDESHOW_LABELS_START + 79)
+#define SLIDESHOW_IPTC_REF_SERVICE (SLIDESHOW_LABELS_START + 80)
+#define SLIDESHOW_LABELS_END SLIDESHOW_IPTC_REF_SERVICE
+
+#define FANART_COLOR1 1000
+#define FANART_COLOR2 1001
+#define FANART_COLOR3 1002
+#define FANART_IMAGE 1003
+
+#define SYSTEM_PROFILEAUTOLOGIN 1004
+
+#define SYSTEM_HAS_CMS 1006
+#define SYSTEM_BUILD_VERSION_CODE 1007
+#define SYSTEM_BUILD_VERSION_GIT 1008
+
+#define PVR_CONDITIONS_START 1100
+#define PVR_IS_RECORDING (PVR_CONDITIONS_START)
+#define PVR_HAS_TIMER (PVR_CONDITIONS_START + 1)
+#define PVR_HAS_NONRECORDING_TIMER (PVR_CONDITIONS_START + 2)
+#define PVR_IS_PLAYING_TV (PVR_CONDITIONS_START + 3)
+#define PVR_IS_PLAYING_RADIO (PVR_CONDITIONS_START + 4)
+#define PVR_IS_PLAYING_RECORDING (PVR_CONDITIONS_START + 5)
+#define PVR_ACTUAL_STREAM_ENCRYPTED (PVR_CONDITIONS_START + 6)
+#define PVR_HAS_TV_CHANNELS (PVR_CONDITIONS_START + 7)
+#define PVR_HAS_RADIO_CHANNELS (PVR_CONDITIONS_START + 8)
+#define PVR_IS_TIMESHIFTING (PVR_CONDITIONS_START + 9)
+#define PVR_IS_RECORDING_TV (PVR_CONDITIONS_START + 10)
+#define PVR_HAS_TV_TIMER (PVR_CONDITIONS_START + 11)
+#define PVR_HAS_NONRECORDING_TV_TIMER (PVR_CONDITIONS_START + 12)
+#define PVR_IS_RECORDING_RADIO (PVR_CONDITIONS_START + 13)
+#define PVR_HAS_RADIO_TIMER (PVR_CONDITIONS_START + 14)
+#define PVR_HAS_NONRECORDING_RADIO_TIMER (PVR_CONDITIONS_START + 15)
+#define PVR_IS_PLAYING_EPGTAG (PVR_CONDITIONS_START + 16)
+#define PVR_CAN_RECORD_PLAYING_CHANNEL (PVR_CONDITIONS_START + 17)
+#define PVR_IS_RECORDING_PLAYING_CHANNEL (PVR_CONDITIONS_START + 18)
+#define PVR_IS_PLAYING_ACTIVE_RECORDING (PVR_CONDITIONS_START + 19)
+#define PVR_CONDITIONS_END PVR_IS_PLAYING_ACTIVE_RECORDING
+
+#define PVR_STRINGS_START 1200
+#define PVR_NEXT_RECORDING_CHANNEL (PVR_STRINGS_START)
+#define PVR_NEXT_RECORDING_CHAN_ICO (PVR_STRINGS_START + 1)
+#define PVR_NEXT_RECORDING_DATETIME (PVR_STRINGS_START + 2)
+#define PVR_NEXT_RECORDING_TITLE (PVR_STRINGS_START + 3)
+#define PVR_NOW_RECORDING_CHANNEL (PVR_STRINGS_START + 4)
+#define PVR_NOW_RECORDING_CHAN_ICO (PVR_STRINGS_START + 5)
+#define PVR_NOW_RECORDING_DATETIME (PVR_STRINGS_START + 6)
+#define PVR_NOW_RECORDING_TITLE (PVR_STRINGS_START + 7)
+#define PVR_BACKEND_NAME (PVR_STRINGS_START + 8)
+#define PVR_BACKEND_VERSION (PVR_STRINGS_START + 9)
+#define PVR_BACKEND_HOST (PVR_STRINGS_START + 10)
+#define PVR_BACKEND_DISKSPACE (PVR_STRINGS_START + 11)
+#define PVR_BACKEND_CHANNELS (PVR_STRINGS_START + 12)
+#define PVR_BACKEND_TIMERS (PVR_STRINGS_START + 13)
+#define PVR_BACKEND_RECORDINGS (PVR_STRINGS_START + 14)
+#define PVR_BACKEND_DELETED_RECORDINGS (PVR_STRINGS_START + 15)
+#define PVR_BACKEND_NUMBER (PVR_STRINGS_START + 16)
+#define PVR_TOTAL_DISKSPACE (PVR_STRINGS_START + 17)
+#define PVR_NEXT_TIMER (PVR_STRINGS_START + 18)
+#define PVR_EPG_EVENT_DURATION (PVR_STRINGS_START + 19)
+#define PVR_EPG_EVENT_ELAPSED_TIME (PVR_STRINGS_START + 20)
+#define PVR_EPG_EVENT_PROGRESS (PVR_STRINGS_START + 21)
+#define PVR_ACTUAL_STREAM_CLIENT (PVR_STRINGS_START + 22)
+#define PVR_ACTUAL_STREAM_DEVICE (PVR_STRINGS_START + 23)
+#define PVR_ACTUAL_STREAM_STATUS (PVR_STRINGS_START + 24)
+#define PVR_ACTUAL_STREAM_SIG (PVR_STRINGS_START + 25)
+#define PVR_ACTUAL_STREAM_SNR (PVR_STRINGS_START + 26)
+#define PVR_ACTUAL_STREAM_SIG_PROGR (PVR_STRINGS_START + 27)
+#define PVR_ACTUAL_STREAM_SNR_PROGR (PVR_STRINGS_START + 28)
+#define PVR_ACTUAL_STREAM_BER (PVR_STRINGS_START + 29)
+#define PVR_ACTUAL_STREAM_UNC (PVR_STRINGS_START + 30)
+#define PVR_ACTUAL_STREAM_CRYPTION (PVR_STRINGS_START + 34)
+#define PVR_ACTUAL_STREAM_SERVICE (PVR_STRINGS_START + 35)
+#define PVR_ACTUAL_STREAM_MUX (PVR_STRINGS_START + 36)
+#define PVR_ACTUAL_STREAM_PROVIDER (PVR_STRINGS_START + 37)
+#define PVR_BACKEND_DISKSPACE_PROGR (PVR_STRINGS_START + 38)
+#define PVR_TIMESHIFT_START_TIME (PVR_STRINGS_START + 39)
+#define PVR_TIMESHIFT_END_TIME (PVR_STRINGS_START + 40)
+#define PVR_TIMESHIFT_PLAY_TIME (PVR_STRINGS_START + 41)
+#define PVR_TIMESHIFT_PROGRESS (PVR_STRINGS_START + 42)
+#define PVR_TV_NOW_RECORDING_TITLE (PVR_STRINGS_START + 43)
+#define PVR_TV_NOW_RECORDING_CHANNEL (PVR_STRINGS_START + 44)
+#define PVR_TV_NOW_RECORDING_CHAN_ICO (PVR_STRINGS_START + 45)
+#define PVR_TV_NOW_RECORDING_DATETIME (PVR_STRINGS_START + 46)
+#define PVR_TV_NEXT_RECORDING_TITLE (PVR_STRINGS_START + 47)
+#define PVR_TV_NEXT_RECORDING_CHANNEL (PVR_STRINGS_START + 48)
+#define PVR_TV_NEXT_RECORDING_CHAN_ICO (PVR_STRINGS_START + 49)
+#define PVR_TV_NEXT_RECORDING_DATETIME (PVR_STRINGS_START + 50)
+#define PVR_RADIO_NOW_RECORDING_TITLE (PVR_STRINGS_START + 51)
+#define PVR_RADIO_NOW_RECORDING_CHANNEL (PVR_STRINGS_START + 52)
+#define PVR_RADIO_NOW_RECORDING_CHAN_ICO (PVR_STRINGS_START + 53)
+#define PVR_RADIO_NOW_RECORDING_DATETIME (PVR_STRINGS_START + 54)
+#define PVR_RADIO_NEXT_RECORDING_TITLE (PVR_STRINGS_START + 55)
+#define PVR_RADIO_NEXT_RECORDING_CHANNEL (PVR_STRINGS_START + 56)
+#define PVR_RADIO_NEXT_RECORDING_CHAN_ICO (PVR_STRINGS_START + 57)
+#define PVR_RADIO_NEXT_RECORDING_DATETIME (PVR_STRINGS_START + 58)
+#define PVR_CHANNEL_NUMBER_INPUT (PVR_STRINGS_START + 59)
+#define PVR_EPG_EVENT_REMAINING_TIME (PVR_STRINGS_START + 60)
+#define PVR_EPG_EVENT_FINISH_TIME (PVR_STRINGS_START + 61)
+#define PVR_TIMESHIFT_OFFSET (PVR_STRINGS_START + 62)
+#define PVR_EPG_EVENT_SEEK_TIME (PVR_STRINGS_START + 63)
+#define PVR_TIMESHIFT_PROGRESS_PLAY_POS (PVR_STRINGS_START + 64)
+#define PVR_TIMESHIFT_PROGRESS_DURATION (PVR_STRINGS_START + 65)
+#define PVR_TIMESHIFT_PROGRESS_EPG_START (PVR_STRINGS_START + 66)
+#define PVR_TIMESHIFT_PROGRESS_EPG_END (PVR_STRINGS_START + 67)
+#define PVR_TIMESHIFT_PROGRESS_BUFFER_START (PVR_STRINGS_START + 68)
+#define PVR_TIMESHIFT_PROGRESS_BUFFER_END (PVR_STRINGS_START + 69)
+#define PVR_TIMESHIFT_PROGRESS_START_TIME (PVR_STRINGS_START + 70)
+#define PVR_TIMESHIFT_PROGRESS_END_TIME (PVR_STRINGS_START + 71)
+#define PVR_EPG_EVENT_ICON (PVR_STRINGS_START + 72)
+#define PVR_TIMESHIFT_SEEKBAR (PVR_STRINGS_START + 73)
+#define PVR_BACKEND_PROVIDERS (PVR_STRINGS_START + 74)
+#define PVR_BACKEND_CHANNEL_GROUPS (PVR_STRINGS_START + 75)
+#define PVR_STRINGS_END PVR_BACKEND_CHANNEL_GROUPS
+
+#define RDS_DATA_START 1400
+#define RDS_HAS_RDS (RDS_DATA_START)
+#define RDS_HAS_RADIOTEXT (RDS_DATA_START + 1)
+#define RDS_HAS_RADIOTEXT_PLUS (RDS_DATA_START + 2)
+#define RDS_GET_RADIOTEXT_LINE (RDS_DATA_START + 3)
+#define RDS_TITLE (RDS_DATA_START + 4)
+#define RDS_BAND (RDS_DATA_START + 5)
+#define RDS_ARTIST (RDS_DATA_START + 6)
+#define RDS_COMPOSER (RDS_DATA_START + 7)
+#define RDS_CONDUCTOR (RDS_DATA_START + 8)
+#define RDS_ALBUM (RDS_DATA_START + 9)
+#define RDS_ALBUM_TRACKNUMBER (RDS_DATA_START + 10)
+#define RDS_GET_RADIO_STYLE (RDS_DATA_START + 11)
+#define RDS_COMMENT (RDS_DATA_START + 12)
+#define RDS_INFO_NEWS (RDS_DATA_START + 13)
+#define RDS_INFO_NEWS_LOCAL (RDS_DATA_START + 14)
+#define RDS_INFO_STOCK (RDS_DATA_START + 15)
+#define RDS_INFO_STOCK_SIZE (RDS_DATA_START + 16)
+#define RDS_INFO_SPORT (RDS_DATA_START + 17)
+#define RDS_INFO_SPORT_SIZE (RDS_DATA_START + 18)
+#define RDS_INFO_LOTTERY (RDS_DATA_START + 19)
+#define RDS_INFO_LOTTERY_SIZE (RDS_DATA_START + 20)
+#define RDS_INFO_WEATHER (RDS_DATA_START + 21)
+#define RDS_INFO_WEATHER_SIZE (RDS_DATA_START + 22)
+#define RDS_INFO_CINEMA (RDS_DATA_START + 23)
+#define RDS_INFO_CINEMA_SIZE (RDS_DATA_START + 24)
+#define RDS_INFO_HOROSCOPE (RDS_DATA_START + 25)
+#define RDS_INFO_HOROSCOPE_SIZE (RDS_DATA_START + 26)
+#define RDS_INFO_OTHER (RDS_DATA_START + 27)
+#define RDS_INFO_OTHER_SIZE (RDS_DATA_START + 28)
+#define RDS_PROG_STATION (RDS_DATA_START + 29)
+#define RDS_PROG_NOW (RDS_DATA_START + 30)
+#define RDS_PROG_NEXT (RDS_DATA_START + 31)
+#define RDS_PROG_HOST (RDS_DATA_START + 32)
+#define RDS_PROG_EDIT_STAFF (RDS_DATA_START + 33)
+#define RDS_PROG_HOMEPAGE (RDS_DATA_START + 34)
+#define RDS_PROG_STYLE (RDS_DATA_START + 35)
+#define RDS_PHONE_HOTLINE (RDS_DATA_START + 36)
+#define RDS_PHONE_STUDIO (RDS_DATA_START + 37)
+#define RDS_SMS_STUDIO (RDS_DATA_START + 38)
+#define RDS_EMAIL_HOTLINE (RDS_DATA_START + 39)
+#define RDS_EMAIL_STUDIO (RDS_DATA_START + 40)
+#define RDS_HAS_HOTLINE_DATA (RDS_DATA_START + 41)
+#define RDS_HAS_STUDIO_DATA (RDS_DATA_START + 42)
+#define RDS_AUDIO_LANG (RDS_DATA_START + 43)
+#define RDS_CHANNEL_COUNTRY (RDS_DATA_START + 44)
+#define RDS_DATA_END RDS_CHANNEL_COUNTRY
+
+#define PLAYER_PROCESS 1500
+#define PLAYER_PROCESS_VIDEODECODER (PLAYER_PROCESS)
+#define PLAYER_PROCESS_DEINTMETHOD (PLAYER_PROCESS + 1)
+#define PLAYER_PROCESS_PIXELFORMAT (PLAYER_PROCESS + 2)
+#define PLAYER_PROCESS_VIDEOWIDTH (PLAYER_PROCESS + 3)
+#define PLAYER_PROCESS_VIDEOHEIGHT (PLAYER_PROCESS + 4)
+#define PLAYER_PROCESS_VIDEOFPS (PLAYER_PROCESS + 5)
+#define PLAYER_PROCESS_VIDEODAR (PLAYER_PROCESS + 6)
+#define PLAYER_PROCESS_VIDEOHWDECODER (PLAYER_PROCESS + 7)
+#define PLAYER_PROCESS_AUDIODECODER (PLAYER_PROCESS + 8)
+#define PLAYER_PROCESS_AUDIOCHANNELS (PLAYER_PROCESS + 9)
+#define PLAYER_PROCESS_AUDIOSAMPLERATE (PLAYER_PROCESS + 10)
+#define PLAYER_PROCESS_AUDIOBITSPERSAMPLE (PLAYER_PROCESS + 11)
+#define PLAYER_PROCESS_VIDEOSCANTYPE (PLAYER_PROCESS + 12)
+
+#define ADDON_INFOS 1600
+#define ADDON_SETTING_STRING (ADDON_INFOS)
+#define ADDON_SETTING_BOOL (ADDON_INFOS + 1)
+#define ADDON_SETTING_INT (ADDON_INFOS + 2)
+
+#define WINDOW_PROPERTY 9993
+#define WINDOW_IS_VISIBLE 9995
+#define WINDOW_NEXT 9996
+#define WINDOW_PREVIOUS 9997
+#define WINDOW_IS_MEDIA 9998
+#define WINDOW_IS_ACTIVE 9999
+#define WINDOW_IS 10000
+#define WINDOW_IS_DIALOG_TOPMOST 10001
+#define WINDOW_IS_MODAL_DIALOG_TOPMOST 10002
+
+#define CONTROL_GET_LABEL 29996
+#define CONTROL_IS_ENABLED 29997
+#define CONTROL_IS_VISIBLE 29998
+#define CONTROL_GROUP_HAS_FOCUS 29999
+#define CONTROL_HAS_FOCUS 30000
+
+#define LISTITEM_START 35000
+#define LISTITEM_THUMB (LISTITEM_START)
+#define LISTITEM_LABEL (LISTITEM_START + 1)
+#define LISTITEM_TITLE (LISTITEM_START + 2)
+#define LISTITEM_TRACKNUMBER (LISTITEM_START + 3)
+#define LISTITEM_ARTIST (LISTITEM_START + 4)
+#define LISTITEM_ALBUM (LISTITEM_START + 5)
+#define LISTITEM_YEAR (LISTITEM_START + 6)
+#define LISTITEM_GENRE (LISTITEM_START + 7)
+#define LISTITEM_ICON (LISTITEM_START + 8)
+#define LISTITEM_DIRECTOR (LISTITEM_START + 9)
+#define LISTITEM_OVERLAY (LISTITEM_START + 10)
+#define LISTITEM_LABEL2 (LISTITEM_START + 11)
+#define LISTITEM_FILENAME (LISTITEM_START + 12)
+#define LISTITEM_DATE (LISTITEM_START + 13)
+#define LISTITEM_SIZE (LISTITEM_START + 14)
+#define LISTITEM_RATING (LISTITEM_START + 15)
+#define LISTITEM_PROGRAM_COUNT (LISTITEM_START + 16)
+#define LISTITEM_DURATION (LISTITEM_START + 17)
+#define LISTITEM_ISPLAYING (LISTITEM_START + 18)
+#define LISTITEM_ISSELECTED (LISTITEM_START + 19)
+#define LISTITEM_PLOT (LISTITEM_START + 20)
+#define LISTITEM_PLOT_OUTLINE (LISTITEM_START + 21)
+#define LISTITEM_EPISODE (LISTITEM_START + 22)
+#define LISTITEM_SEASON (LISTITEM_START + 23)
+#define LISTITEM_TVSHOW (LISTITEM_START + 24)
+#define LISTITEM_PREMIERED (LISTITEM_START + 25)
+#define LISTITEM_COMMENT (LISTITEM_START + 26)
+#define LISTITEM_ACTUAL_ICON (LISTITEM_START + 27)
+#define LISTITEM_PATH (LISTITEM_START + 28)
+#define LISTITEM_PICTURE_PATH (LISTITEM_START + 29)
+
+#define LISTITEM_PICTURE_START (LISTITEM_START + 30)
+#define LISTITEM_PICTURE_RESOLUTION (LISTITEM_PICTURE_START) // => SLIDESHOW_RESOLUTION
+#define LISTITEM_PICTURE_LONGDATE (LISTITEM_START + 31) // => SLIDESHOW_EXIF_LONG_DATE
+#define LISTITEM_PICTURE_LONGDATETIME (LISTITEM_START + 32) // => SLIDESHOW_EXIF_LONG_DATE_TIME
+#define LISTITEM_PICTURE_DATE (LISTITEM_START + 33) // => SLIDESHOW_EXIF_DATE
+#define LISTITEM_PICTURE_DATETIME (LISTITEM_START + 34) // => SLIDESHOW_EXIF_DATE_TIME
+#define LISTITEM_PICTURE_COMMENT (LISTITEM_START + 35) // => SLIDESHOW_COMMENT
+#define LISTITEM_PICTURE_CAPTION (LISTITEM_START + 36) // => SLIDESHOW_IPTC_CAPTION
+#define LISTITEM_PICTURE_DESC (LISTITEM_START + 37) // => SLIDESHOW_EXIF_DESCRIPTION
+#define LISTITEM_PICTURE_KEYWORDS (LISTITEM_START + 38) // => SLIDESHOW_IPTC_KEYWORDS
+#define LISTITEM_PICTURE_CAM_MAKE (LISTITEM_START + 39) // => SLIDESHOW_EXIF_CAMERA_MAKE
+#define LISTITEM_PICTURE_CAM_MODEL (LISTITEM_START + 40) // => SLIDESHOW_EXIF_CAMERA_MODEL
+#define LISTITEM_PICTURE_APERTURE (LISTITEM_START + 41) // => SLIDESHOW_EXIF_APERTURE
+#define LISTITEM_PICTURE_FOCAL_LEN (LISTITEM_START + 42) // => SLIDESHOW_EXIF_FOCAL_LENGTH
+#define LISTITEM_PICTURE_FOCUS_DIST (LISTITEM_START + 43) // => SLIDESHOW_EXIF_FOCUS_DIST
+#define LISTITEM_PICTURE_EXP_MODE (LISTITEM_START + 44) // => SLIDESHOW_EXIF_EXPOSURE_MODE
+#define LISTITEM_PICTURE_EXP_TIME (LISTITEM_START + 45) // => SLIDESHOW_EXIF_EXPOSURE_TIME
+#define LISTITEM_PICTURE_ISO (LISTITEM_START + 46) // => SLIDESHOW_EXIF_ISO_EQUIV
+#define LISTITEM_PICTURE_AUTHOR (LISTITEM_START + 47) // => SLIDESHOW_IPTC_AUTHOR
+#define LISTITEM_PICTURE_BYLINE (LISTITEM_START + 48) // => SLIDESHOW_IPTC_BYLINE
+#define LISTITEM_PICTURE_BYLINE_TITLE (LISTITEM_START + 49) // => SLIDESHOW_IPTC_BYLINE_TITLE
+#define LISTITEM_PICTURE_CATEGORY (LISTITEM_START + 50) // => SLIDESHOW_IPTC_CATEGORY
+#define LISTITEM_PICTURE_CCD_WIDTH (LISTITEM_START + 51) // => SLIDESHOW_EXIF_CCD_WIDTH
+#define LISTITEM_PICTURE_CITY (LISTITEM_START + 52) // => SLIDESHOW_IPTC_CITY
+#define LISTITEM_PICTURE_URGENCY (LISTITEM_START + 53) // => SLIDESHOW_IPTC_URGENCY
+#define LISTITEM_PICTURE_COPYRIGHT_NOTICE (LISTITEM_START + 54) // => SLIDESHOW_IPTC_COPYRIGHT_NOTICE
+#define LISTITEM_PICTURE_COUNTRY (LISTITEM_START + 55) // => SLIDESHOW_IPTC_COUNTRY
+#define LISTITEM_PICTURE_COUNTRY_CODE (LISTITEM_START + 56) // => SLIDESHOW_IPTC_COUNTRY_CODE
+#define LISTITEM_PICTURE_CREDIT (LISTITEM_START + 57) // => SLIDESHOW_IPTC_CREDIT
+#define LISTITEM_PICTURE_IPTCDATE (LISTITEM_START + 58) // => SLIDESHOW_IPTC_DATE
+#define LISTITEM_PICTURE_DIGITAL_ZOOM (LISTITEM_START + 59) // => SLIDESHOW_EXIF_DIGITAL_ZOOM
+#define LISTITEM_PICTURE_EXPOSURE (LISTITEM_START + 60) // => SLIDESHOW_EXIF_EXPOSURE
+#define LISTITEM_PICTURE_EXPOSURE_BIAS (LISTITEM_START + 61) // => SLIDESHOW_EXIF_EXPOSURE_BIAS
+#define LISTITEM_PICTURE_FLASH_USED (LISTITEM_START + 62) // => SLIDESHOW_EXIF_FLASH_USED
+#define LISTITEM_PICTURE_HEADLINE (LISTITEM_START + 63) // => SLIDESHOW_IPTC_HEADLINE
+#define LISTITEM_PICTURE_COLOUR (LISTITEM_START + 64) // => SLIDESHOW_COLOUR
+#define LISTITEM_PICTURE_LIGHT_SOURCE (LISTITEM_START + 65) // => SLIDESHOW_EXIF_LIGHT_SOURCE
+#define LISTITEM_PICTURE_METERING_MODE (LISTITEM_START + 66) // => SLIDESHOW_EXIF_METERING_MODE
+#define LISTITEM_PICTURE_OBJECT_NAME (LISTITEM_START + 67) // => SLIDESHOW_IPTC_OBJECT_NAME
+#define LISTITEM_PICTURE_ORIENTATION (LISTITEM_START + 68) // => SLIDESHOW_EXIF_ORIENTATION
+#define LISTITEM_PICTURE_PROCESS (LISTITEM_START + 69) // => SLIDESHOW_PROCESS
+#define LISTITEM_PICTURE_REF_SERVICE (LISTITEM_START + 70) // => SLIDESHOW_IPTC_REF_SERVICE
+#define LISTITEM_PICTURE_SOURCE (LISTITEM_START + 71) // => SLIDESHOW_IPTC_SOURCE
+#define LISTITEM_PICTURE_SPEC_INSTR (LISTITEM_START + 72) // => SLIDESHOW_IPTC_SPEC_INSTR
+#define LISTITEM_PICTURE_STATE (LISTITEM_START + 73) // => SLIDESHOW_IPTC_STATE
+#define LISTITEM_PICTURE_SUP_CATEGORIES (LISTITEM_START + 74) // => SLIDESHOW_IPTC_SUP_CATEGORIES
+#define LISTITEM_PICTURE_TX_REFERENCE (LISTITEM_START + 75) // => SLIDESHOW_IPTC_TX_REFERENCE
+#define LISTITEM_PICTURE_WHITE_BALANCE (LISTITEM_START + 76) // => SLIDESHOW_EXIF_WHITE_BALANCE
+#define LISTITEM_PICTURE_IMAGETYPE (LISTITEM_START + 77) // => SLIDESHOW_IPTC_IMAGETYPE
+#define LISTITEM_PICTURE_SUBLOCATION (LISTITEM_START + 78) // => SLIDESHOW_IPTC_SUBLOCATION
+#define LISTITEM_PICTURE_TIMECREATED (LISTITEM_START + 79) // => SLIDESHOW_IPTC_TIMECREATED
+#define LISTITEM_PICTURE_GPS_LAT (LISTITEM_START + 80) // => SLIDESHOW_EXIF_GPS_LATITUDE
+#define LISTITEM_PICTURE_GPS_LON (LISTITEM_START + 81) // => SLIDESHOW_EXIF_GPS_LONGITUDE
+#define LISTITEM_PICTURE_GPS_ALT (LISTITEM_START + 82) // => SLIDESHOW_EXIF_GPS_ALTITUDE
+#define LISTITEM_PICTURE_END (LISTITEM_PICTURE_GPS_ALT)
+
+#define LISTITEM_STUDIO (LISTITEM_START + 83)
+#define LISTITEM_MPAA (LISTITEM_START + 84)
+#define LISTITEM_CAST (LISTITEM_START + 85)
+#define LISTITEM_CAST_AND_ROLE (LISTITEM_START + 86)
+#define LISTITEM_WRITER (LISTITEM_START + 87)
+#define LISTITEM_TAGLINE (LISTITEM_START + 88)
+#define LISTITEM_TOP250 (LISTITEM_START + 89)
+#define LISTITEM_RATING_AND_VOTES (LISTITEM_START + 90)
+#define LISTITEM_TRAILER (LISTITEM_START + 91)
+#define LISTITEM_APPEARANCES (LISTITEM_START + 92)
+#define LISTITEM_FILENAME_AND_PATH (LISTITEM_START + 93)
+#define LISTITEM_SORT_LETTER (LISTITEM_START + 94)
+#define LISTITEM_ALBUM_ARTIST (LISTITEM_START + 95)
+#define LISTITEM_FOLDERNAME (LISTITEM_START + 96)
+#define LISTITEM_VIDEO_CODEC (LISTITEM_START + 97)
+#define LISTITEM_VIDEO_RESOLUTION (LISTITEM_START + 98)
+#define LISTITEM_VIDEO_ASPECT (LISTITEM_START + 99)
+#define LISTITEM_AUDIO_CODEC (LISTITEM_START + 100)
+#define LISTITEM_AUDIO_CHANNELS (LISTITEM_START + 101)
+#define LISTITEM_AUDIO_LANGUAGE (LISTITEM_START + 102)
+#define LISTITEM_SUBTITLE_LANGUAGE (LISTITEM_START + 103)
+#define LISTITEM_IS_FOLDER (LISTITEM_START + 104)
+#define LISTITEM_ORIGINALTITLE (LISTITEM_START + 105)
+#define LISTITEM_COUNTRY (LISTITEM_START + 106)
+#define LISTITEM_PLAYCOUNT (LISTITEM_START + 107)
+#define LISTITEM_LASTPLAYED (LISTITEM_START + 108)
+#define LISTITEM_FOLDERPATH (LISTITEM_START + 109)
+#define LISTITEM_DISC_NUMBER (LISTITEM_START + 110)
+#define LISTITEM_FILE_EXTENSION (LISTITEM_START + 111)
+#define LISTITEM_IS_RESUMABLE (LISTITEM_START + 112)
+#define LISTITEM_PERCENT_PLAYED (LISTITEM_START + 113)
+#define LISTITEM_DATE_ADDED (LISTITEM_START + 114)
+#define LISTITEM_DBTYPE (LISTITEM_START + 115)
+#define LISTITEM_DBID (LISTITEM_START + 116)
+#define LISTITEM_ART (LISTITEM_START + 117)
+#define LISTITEM_STARTTIME (LISTITEM_START + 118)
+#define LISTITEM_ENDTIME (LISTITEM_START + 119)
+#define LISTITEM_STARTDATE (LISTITEM_START + 120)
+#define LISTITEM_ENDDATE (LISTITEM_START + 121)
+#define LISTITEM_NEXT_TITLE (LISTITEM_START + 122)
+#define LISTITEM_NEXT_GENRE (LISTITEM_START + 123)
+#define LISTITEM_NEXT_PLOT (LISTITEM_START + 124)
+#define LISTITEM_NEXT_PLOT_OUTLINE (LISTITEM_START + 125)
+#define LISTITEM_NEXT_STARTTIME (LISTITEM_START + 126)
+#define LISTITEM_NEXT_ENDTIME (LISTITEM_START + 127)
+#define LISTITEM_NEXT_STARTDATE (LISTITEM_START + 128)
+#define LISTITEM_NEXT_ENDDATE (LISTITEM_START + 129)
+#define LISTITEM_NEXT_DURATION (LISTITEM_START + 130)
+#define LISTITEM_CHANNEL_NAME (LISTITEM_START + 131)
+#define LISTITEM_CHANNEL_GROUP (LISTITEM_START + 132)
+#define LISTITEM_HASTIMER (LISTITEM_START + 133)
+#define LISTITEM_ISRECORDING (LISTITEM_START + 134)
+#define LISTITEM_ISENCRYPTED (LISTITEM_START + 135)
+#define LISTITEM_PARENTAL_RATING (LISTITEM_START + 136)
+#define LISTITEM_PROGRESS (LISTITEM_START + 137)
+#define LISTITEM_HAS_EPG (LISTITEM_START + 138)
+#define LISTITEM_VOTES (LISTITEM_START + 139)
+#define LISTITEM_STEREOSCOPIC_MODE (LISTITEM_START + 140)
+#define LISTITEM_IS_STEREOSCOPIC (LISTITEM_START + 141)
+#define LISTITEM_INPROGRESS (LISTITEM_START + 142)
+#define LISTITEM_HASRECORDING (LISTITEM_START + 143)
+#define LISTITEM_HASREMINDER (LISTITEM_START + 144)
+#define LISTITEM_CHANNEL_NUMBER (LISTITEM_START + 145)
+#define LISTITEM_IMDBNUMBER (LISTITEM_START + 146)
+#define LISTITEM_EPISODENAME (LISTITEM_START + 147)
+#define LISTITEM_IS_COLLECTION (LISTITEM_START + 148)
+#define LISTITEM_HASTIMERSCHEDULE (LISTITEM_START + 149)
+#define LISTITEM_TIMERTYPE (LISTITEM_START + 150)
+#define LISTITEM_EPG_EVENT_TITLE (LISTITEM_START + 151)
+#define LISTITEM_DATETIME (LISTITEM_START + 152)
+#define LISTITEM_USER_RATING (LISTITEM_START + 153)
+#define LISTITEM_TAG (LISTITEM_START + 154)
+#define LISTITEM_SET (LISTITEM_START + 155)
+#define LISTITEM_SETID (LISTITEM_START + 156)
+#define LISTITEM_IS_PARENTFOLDER (LISTITEM_START + 157)
+#define LISTITEM_MOOD (LISTITEM_START + 158)
+#define LISTITEM_CONTRIBUTORS (LISTITEM_START + 159)
+#define LISTITEM_CONTRIBUTOR_AND_ROLE (LISTITEM_START + 160)
+#define LISTITEM_TIMERISACTIVE (LISTITEM_START + 161)
+#define LISTITEM_TIMERHASCONFLICT (LISTITEM_START + 162)
+#define LISTITEM_TIMERHASERROR (LISTITEM_START + 163)
+
+#define LISTITEM_ADDON_NAME (LISTITEM_START + 164)
+#define LISTITEM_ADDON_VERSION (LISTITEM_START + 165)
+#define LISTITEM_ADDON_CREATOR (LISTITEM_START + 166)
+#define LISTITEM_ADDON_SUMMARY (LISTITEM_START + 167)
+#define LISTITEM_ADDON_DESCRIPTION (LISTITEM_START + 168)
+#define LISTITEM_ADDON_DISCLAIMER (LISTITEM_START + 169)
+#define LISTITEM_ADDON_BROKEN (LISTITEM_START + 170)
+#define LISTITEM_ADDON_LIFECYCLE_TYPE (LISTITEM_START + 171)
+#define LISTITEM_ADDON_LIFECYCLE_DESC (LISTITEM_START + 172)
+#define LISTITEM_ADDON_TYPE (LISTITEM_START + 173)
+#define LISTITEM_ADDON_INSTALL_DATE (LISTITEM_START + 174)
+#define LISTITEM_ADDON_LAST_UPDATED (LISTITEM_START + 175)
+#define LISTITEM_ADDON_LAST_USED (LISTITEM_START + 176)
+#define LISTITEM_STATUS (LISTITEM_START + 177)
+#define LISTITEM_ENDTIME_RESUME (LISTITEM_START + 178)
+#define LISTITEM_ADDON_ORIGIN (LISTITEM_START + 179)
+#define LISTITEM_ADDON_NEWS (LISTITEM_START + 180)
+#define LISTITEM_ADDON_SIZE (LISTITEM_START + 181)
+#define LISTITEM_EXPIRATION_DATE (LISTITEM_START + 182)
+#define LISTITEM_EXPIRATION_TIME (LISTITEM_START + 183)
+#define LISTITEM_PROPERTY (LISTITEM_START + 184)
+#define LISTITEM_EPG_EVENT_ICON (LISTITEM_START + 185)
+#define LISTITEM_HASREMINDERRULE (LISTITEM_START + 186)
+#define LISTITEM_HASARCHIVE (LISTITEM_START + 187)
+#define LISTITEM_ISPLAYABLE (LISTITEM_START + 188)
+#define LISTITEM_FILENAME_NO_EXTENSION (LISTITEM_START + 189)
+#define LISTITEM_CURRENTITEM (LISTITEM_START + 190)
+#define LISTITEM_IS_NEW (LISTITEM_START + 191)
+#define LISTITEM_DISC_TITLE (LISTITEM_START + 192)
+#define LISTITEM_IS_BOXSET (LISTITEM_START + 193)
+#define LISTITEM_TOTALDISCS (LISTITEM_START + 194)
+#define LISTITEM_RELEASEDATE (LISTITEM_START + 195)
+#define LISTITEM_ORIGINALDATE (LISTITEM_START + 196)
+#define LISTITEM_BPM (LISTITEM_START + 197)
+#define LISTITEM_UNIQUEID (LISTITEM_START + 198)
+#define LISTITEM_BITRATE (LISTITEM_START + 199)
+#define LISTITEM_SAMPLERATE (LISTITEM_START + 200)
+#define LISTITEM_MUSICCHANNELS (LISTITEM_START + 201)
+#define LISTITEM_IS_PREMIERE (LISTITEM_START + 202)
+#define LISTITEM_IS_FINALE (LISTITEM_START + 203)
+#define LISTITEM_IS_LIVE (LISTITEM_START + 204)
+#define LISTITEM_TVSHOWDBID (LISTITEM_START + 205)
+#define LISTITEM_ALBUMSTATUS (LISTITEM_START + 206)
+#define LISTITEM_ISAUTOUPDATEABLE (LISTITEM_START + 207)
+#define LISTITEM_VIDEO_HDR_TYPE (LISTITEM_START + 208)
+
+#define LISTITEM_END (LISTITEM_START + 2500)
+
+#define CONDITIONAL_LABEL_START (LISTITEM_END + 1) // 37501
+#define CONDITIONAL_LABEL_END 39999
+
+// the multiple information vector
+#define MULTI_INFO_START 40000
+#define MULTI_INFO_END 99999
+#define COMBINED_VALUES_START 100000
+
+// listitem info Flags
+// Stored in the top 8 bits of GUIInfo::m_data1
+// therefore we only have room for 8 flags
+#define INFOFLAG_LISTITEM_WRAP (static_cast<uint32_t>(1 << 25)) // Wrap ListItem lookups
+#define INFOFLAG_LISTITEM_POSITION (static_cast<uint32_t>(1 << 26)) // ListItem lookups based on cursor position
+#define INFOFLAG_LISTITEM_ABSOLUTE (static_cast<uint32_t>(1 << 27)) // Absolute ListItem lookups
+#define INFOFLAG_LISTITEM_NOWRAP (static_cast<uint32_t>(1 << 28)) // Do not wrap ListItem lookups
+#define INFOFLAG_LISTITEM_CONTAINER (static_cast<uint32_t>(1 << 29)) // Lookup the item in given container
+// clang-format on
diff --git a/xbmc/guilib/guiinfo/GUIInfoProvider.h b/xbmc/guilib/guiinfo/GUIInfoProvider.h
new file mode 100644
index 0000000..a03159b
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoProvider.h
@@ -0,0 +1,49 @@
+/*
+ * 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/VideoPlayer/Interface/StreamInfo.h"
+#include "guilib/guiinfo/IGUIInfoProvider.h"
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CGUIInfoProvider : public IGUIInfoProvider
+{
+public:
+ CGUIInfoProvider() = default;
+ ~CGUIInfoProvider() override = default;
+
+ bool GetFallbackLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const CGUIInfo& info,
+ std::string* fallback) override
+ {
+ return false;
+ }
+
+ void UpdateAVInfo(const AudioStreamInfo& audioInfo, const VideoStreamInfo& videoInfo, const SubtitleStreamInfo& subtitleInfo) override
+ { m_audioInfo = audioInfo, m_videoInfo = videoInfo, m_subtitleInfo = subtitleInfo; }
+
+protected:
+ VideoStreamInfo m_videoInfo;
+ AudioStreamInfo m_audioInfo;
+ SubtitleStreamInfo m_subtitleInfo;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/GUIInfoProviders.cpp b/xbmc/guilib/guiinfo/GUIInfoProviders.cpp
new file mode 100644
index 0000000..274318e
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoProviders.cpp
@@ -0,0 +1,122 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProviders.h"
+
+#include "guilib/guiinfo/IGUIInfoProvider.h"
+
+#include <algorithm>
+
+using namespace KODI::GUILIB::GUIINFO;
+
+CGUIInfoProviders::CGUIInfoProviders()
+{
+ RegisterProvider(&m_guiControlsGUIInfo);
+ RegisterProvider(&m_videoGUIInfo); // Note: video info provider must be registered before music info provider,
+ // because of music videos having both a video info tag and a music info tag
+ // and video info tag always has to be evaluated first.
+ RegisterProvider(&m_musicGUIInfo);
+ RegisterProvider(&m_picturesGUIInfo);
+ RegisterProvider(&m_playerGUIInfo);
+ RegisterProvider(&m_libraryGUIInfo);
+ RegisterProvider(&m_addonsGUIInfo);
+ RegisterProvider(&m_weatherGUIInfo);
+ RegisterProvider(&m_gamesGUIInfo);
+ RegisterProvider(&m_systemGUIInfo);
+ RegisterProvider(&m_visualisationGUIInfo);
+ RegisterProvider(&m_skinGUIInfo);
+}
+
+CGUIInfoProviders::~CGUIInfoProviders()
+{
+ UnregisterProvider(&m_skinGUIInfo);
+ UnregisterProvider(&m_visualisationGUIInfo);
+ UnregisterProvider(&m_systemGUIInfo);
+ UnregisterProvider(&m_gamesGUIInfo);
+ UnregisterProvider(&m_weatherGUIInfo);
+ UnregisterProvider(&m_addonsGUIInfo);
+ UnregisterProvider(&m_libraryGUIInfo);
+ UnregisterProvider(&m_playerGUIInfo);
+ UnregisterProvider(&m_picturesGUIInfo);
+ UnregisterProvider(&m_musicGUIInfo);
+ UnregisterProvider(&m_videoGUIInfo);
+ UnregisterProvider(&m_guiControlsGUIInfo);
+}
+
+void CGUIInfoProviders::RegisterProvider(IGUIInfoProvider *provider, bool bAppend /* = true */)
+{
+ auto it = std::find(m_providers.begin(), m_providers.end(), provider);
+ if (it == m_providers.end())
+ {
+ if (bAppend)
+ m_providers.emplace_back(provider);
+ else
+ m_providers.insert(m_providers.begin(), provider);
+ }
+}
+
+void CGUIInfoProviders::UnregisterProvider(IGUIInfoProvider *provider)
+{
+ auto it = std::find(m_providers.begin(), m_providers.end(), provider);
+ if (it != m_providers.end())
+ m_providers.erase(it);
+}
+
+bool CGUIInfoProviders::InitCurrentItem(CFileItem *item)
+{
+ bool bReturn = false;
+
+ for (const auto& provider : m_providers)
+ {
+ bReturn = provider->InitCurrentItem(item);
+ }
+ return bReturn;
+}
+
+bool CGUIInfoProviders::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ for (const auto& provider : m_providers)
+ {
+ if (provider->GetLabel(value, item, contextWindow, info, fallback))
+ return true;
+ }
+ for (const auto& provider : m_providers)
+ {
+ if (provider->GetFallbackLabel(value, item, contextWindow, info, fallback))
+ return true;
+ }
+ return false;
+}
+
+bool CGUIInfoProviders::GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const
+{
+ for (const auto& provider : m_providers)
+ {
+ if (provider->GetInt(value, item, contextWindow, info))
+ return true;
+ }
+ return false;
+}
+
+bool CGUIInfoProviders::GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const
+{
+ for (const auto& provider : m_providers)
+ {
+ if (provider->GetBool(value, item, contextWindow, info))
+ return true;
+ }
+ return false;
+}
+
+void CGUIInfoProviders::UpdateAVInfo(const AudioStreamInfo& audioInfo, const VideoStreamInfo& videoInfo, const SubtitleStreamInfo& subtitleInfo)
+{
+ for (const auto& provider : m_providers)
+ {
+ provider->UpdateAVInfo(audioInfo, videoInfo, subtitleInfo);
+ }
+}
diff --git a/xbmc/guilib/guiinfo/GUIInfoProviders.h b/xbmc/guilib/guiinfo/GUIInfoProviders.h
new file mode 100644
index 0000000..1aed710
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GUIInfoProviders.h
@@ -0,0 +1,157 @@
+/*
+ * 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 "guilib/guiinfo/AddonsGUIInfo.h"
+#include "guilib/guiinfo/GUIControlsGUIInfo.h"
+#include "guilib/guiinfo/GamesGUIInfo.h"
+#include "guilib/guiinfo/LibraryGUIInfo.h"
+#include "guilib/guiinfo/MusicGUIInfo.h"
+#include "guilib/guiinfo/PicturesGUIInfo.h"
+#include "guilib/guiinfo/PlayerGUIInfo.h"
+#include "guilib/guiinfo/SkinGUIInfo.h"
+#include "guilib/guiinfo/SystemGUIInfo.h"
+#include "guilib/guiinfo/VideoGUIInfo.h"
+#include "guilib/guiinfo/VisualisationGUIInfo.h"
+#include "guilib/guiinfo/WeatherGUIInfo.h"
+
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CGUIListItem;
+
+struct AudioStreamInfo;
+struct VideoStreamInfo;
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+class IGUIInfoProvider;
+
+class CGUIInfoProviders
+{
+public:
+ CGUIInfoProviders();
+ virtual ~CGUIInfoProviders();
+
+ /*!
+ * @brief Register a guiinfo provider.
+ * @param provider The provider to register.
+ * @param bAppend True to append to the list of providers, false to insert before the first provider
+ */
+ void RegisterProvider(IGUIInfoProvider *provider, bool bAppend = true);
+
+ /*!
+ * @brief Unregister a guiinfo provider.
+ * @param provider The provider to unregister.
+ */
+ void UnregisterProvider(IGUIInfoProvider *provider);
+
+ /*!
+ * @brief Init a new current guiinfo manager item. Gets called whenever the active guiinfo manager item changes.
+ * @param item The new item.
+ * @return True if the item was inited by one of the providers, false otherwise.
+ */
+ bool InitCurrentItem(CFileItem *item);
+
+ /*!
+ * @brief Get a GUIInfoManager label string from one of the registered providers.
+ * @param value Will be filled with the requested value.
+ * @param item The item to get the value for. Can be nullptr.
+ * @param contextWindow The context window. Can be 0.
+ * @param info The GUI info (label id + additional data).
+ * @param fallback A fallback value. Can be nullptr.
+ * @return True if the value was filled successfully by one of the providers, false otherwise.
+ */
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const;
+
+ /*!
+ * @brief Get a GUIInfoManager integer value from one of the registered providers.
+ * @param value Will be filled with the requested value.
+ * @param item The item to get the value for. Can be nullptr.
+ * @param contextWindow The context window. Can be 0.
+ * @param info The GUI info (label id + additional data).
+ * @return True if the value was filled successfully by one of the providers, false otherwise.
+ */
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const;
+
+ /*!
+ * @brief Get a GUIInfoManager bool value from one of the registered providers.
+ * @param value Will be filled with the requested value.
+ * @param item The item to get the value for. Can be nullptr.
+ * @param contextWindow The context window. Can be 0.
+ * @param info The GUI info (label id + additional data).
+ * @return True if the value was filled successfully by one of the providers, false otherwise.
+ */
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const;
+
+ /*!
+ * @brief Set new audio/video/subtitle stream info data at all registered providers.
+ * @param audioInfo New audio stream info.
+ * @param videoInfo New video stream info.
+ * @param subtitleInfo New subtitle stream info.
+ */
+ void UpdateAVInfo(const AudioStreamInfo& audioInfo, const VideoStreamInfo& videoInfo, const SubtitleStreamInfo& subtitleInfo);
+
+ /*!
+ * @brief Get the player guiinfo provider.
+ * @return The player guiinfo provider.
+ */
+ CPlayerGUIInfo& GetPlayerInfoProvider() { return m_playerGUIInfo; }
+
+ /*!
+ * @brief Get the system guiinfo provider.
+ * @return The system guiinfo provider.
+ */
+ CSystemGUIInfo& GetSystemInfoProvider() { return m_systemGUIInfo; }
+
+ /*!
+ * @brief Get the pictures guiinfo provider.
+ * @return The pictures guiinfo provider.
+ */
+ CPicturesGUIInfo& GetPicturesInfoProvider() { return m_picturesGUIInfo; }
+
+ /*!
+ * @brief Get the gui controls guiinfo provider.
+ * @return The gui controls guiinfo provider.
+ */
+ CGUIControlsGUIInfo& GetGUIControlsInfoProvider() { return m_guiControlsGUIInfo; }
+
+ /*!
+ * @brief Get the library guiinfo provider.
+ * @return The library guiinfo provider.
+ */
+ CLibraryGUIInfo& GetLibraryInfoProvider() { return m_libraryGUIInfo; }
+
+private:
+ std::vector<IGUIInfoProvider *> m_providers;
+
+ CAddonsGUIInfo m_addonsGUIInfo;
+ CGamesGUIInfo m_gamesGUIInfo;
+ CGUIControlsGUIInfo m_guiControlsGUIInfo;
+ CLibraryGUIInfo m_libraryGUIInfo;
+ CMusicGUIInfo m_musicGUIInfo;
+ CPicturesGUIInfo m_picturesGUIInfo;
+ CPlayerGUIInfo m_playerGUIInfo;
+ CSkinGUIInfo m_skinGUIInfo;
+ CSystemGUIInfo m_systemGUIInfo;
+ CVideoGUIInfo m_videoGUIInfo;
+ CVisualisationGUIInfo m_visualisationGUIInfo;
+ CWeatherGUIInfo m_weatherGUIInfo;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/GamesGUIInfo.cpp b/xbmc/guilib/guiinfo/GamesGUIInfo.cpp
new file mode 100644
index 0000000..5428c3a
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GamesGUIInfo.cpp
@@ -0,0 +1,87 @@
+/*
+ * 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 "guilib/guiinfo/GamesGUIInfo.h"
+
+#include "FileItem.h"
+#include "Util.h"
+#include "cores/RetroPlayer/RetroPlayerUtils.h"
+#include "games/tags/GameInfoTag.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "settings/MediaSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace KODI::GUILIB::GUIINFO;
+using namespace KODI::GAME;
+using namespace KODI::RETRO;
+
+//! @todo Savestates were removed from v18
+//#define FILEITEM_PROPERTY_SAVESTATE_DURATION "duration"
+
+bool CGamesGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ if (item && item->IsGame())
+ {
+ CLog::Log(LOGDEBUG, "CGamesGUIInfo::InitCurrentItem({})", item->GetPath());
+
+ item->LoadGameTag();
+ CGameInfoTag* tag = item->GetGameInfoTag(); // creates item if not yet set, so no nullptr checks needed
+
+ if (tag->GetTitle().empty())
+ {
+ // No title in tag, show filename only
+ tag->SetTitle(CUtil::GetTitleFromPath(item->GetPath()));
+ }
+ return true;
+ }
+ return false;
+}
+
+bool CGamesGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // RETROPLAYER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case RETROPLAYER_VIDEO_FILTER:
+ {
+ value = CMediaSettings::GetInstance().GetCurrentGameSettings().VideoFilter();
+ return true;
+ }
+ case RETROPLAYER_STRETCH_MODE:
+ {
+ STRETCHMODE stretchMode = CMediaSettings::GetInstance().GetCurrentGameSettings().StretchMode();
+ value = CRetroPlayerUtils::StretchModeToIdentifier(stretchMode);
+ return true;
+ }
+ case RETROPLAYER_VIDEO_ROTATION:
+ {
+ const unsigned int rotationDegCCW = CMediaSettings::GetInstance().GetCurrentGameSettings().RotationDegCCW();
+ value = std::to_string(rotationDegCCW);
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool CGamesGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ return false;
+}
+
+bool CGamesGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ return false;
+}
diff --git a/xbmc/guilib/guiinfo/GamesGUIInfo.h b/xbmc/guilib/guiinfo/GamesGUIInfo.h
new file mode 100644
index 0000000..5ad78d9
--- /dev/null
+++ b/xbmc/guilib/guiinfo/GamesGUIInfo.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProvider.h"
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CGamesGUIInfo : public CGUIInfoProvider
+{
+public:
+ CGamesGUIInfo() = default;
+ ~CGamesGUIInfo() override = default;
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/IGUIInfoProvider.h b/xbmc/guilib/guiinfo/IGUIInfoProvider.h
new file mode 100644
index 0000000..4560b08
--- /dev/null
+++ b/xbmc/guilib/guiinfo/IGUIInfoProvider.h
@@ -0,0 +1,97 @@
+/*
+ * 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 <string>
+
+class CFileItem;
+class CGUIListItem;
+
+struct AudioStreamInfo;
+struct VideoStreamInfo;
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class IGUIInfoProvider
+{
+public:
+ virtual ~IGUIInfoProvider() = default;
+
+ /*!
+ * @brief Init a new current guiinfo manager item. Gets called whenever the active guiinfo manager item changes.
+ * @param item The new item.
+ * @return True if the item was inited by the provider, false otherwise.
+ */
+ virtual bool InitCurrentItem(CFileItem *item) = 0;
+
+ /*!
+ * @brief Get a GUIInfoManager label string.
+ * @param value Will be filled with the requested value.
+ * @param item The item to get the value for. Can be nullptr.
+ * @param contextWindow The context window. Can be 0.
+ * @param info The GUI info (label id + additional data).
+ * @param fallback A fallback value. Can be nullptr.
+ * @return True if the value was filled successfully, false otherwise.
+ */
+ virtual bool GetLabel(std::string &value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const = 0;
+
+ /*!
+ * @brief Get a GUIInfoManager label fallback string. Will be called if none of the registered
+ * provider's GetLabel() implementation has returned success.
+ * @param value Will be filled with the requested value.
+ * @param item The item to get the value for. Can be nullptr.
+ * @param contextWindow The context window. Can be 0.
+ * @param info The GUI info (label id + additional data).
+ * @param fallback A fallback value. Can be nullptr.
+ * @return True if the value was filled successfully, false otherwise.
+ */
+ virtual bool GetFallbackLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const CGUIInfo& info,
+ std::string* fallback) = 0;
+
+ /*!
+ * @brief Get a GUIInfoManager integer value.
+ * @param value Will be filled with the requested value.
+ * @param item The item to get the value for. Can be nullptr.
+ * @param contextWindow The context window. Can be 0.
+ * @param info The GUI info (label id + additional data).
+ * @return True if the value was filled successfully, false otherwise.
+ */
+ virtual bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const = 0;
+
+ /*!
+ * @brief Get a GUIInfoManager bool value.
+ * @param value Will be filled with the requested value.
+ * @param item The item to get the value for. Can be nullptr.
+ * @param contextWindow The context window. Can be 0.
+ * @param info The GUI info (label id + additional data).
+ * @return True if the value was filled successfully, false otherwise.
+ */
+ virtual bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const = 0;
+
+ /*!
+ * @brief Set new audio/video stream info data.
+ * @param audioInfo New audio stream info.
+ * @param videoInfo New video stream info.
+ */
+ virtual void UpdateAVInfo(const AudioStreamInfo& audioInfo, const VideoStreamInfo& videoInfo, const SubtitleStreamInfo& subtitleInfo) = 0;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/LibraryGUIInfo.cpp b/xbmc/guilib/guiinfo/LibraryGUIInfo.cpp
new file mode 100644
index 0000000..fc0f471
--- /dev/null
+++ b/xbmc/guilib/guiinfo/LibraryGUIInfo.cpp
@@ -0,0 +1,290 @@
+/*
+ * 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 "guilib/guiinfo/LibraryGUIInfo.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "filesystem/Directory.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicLibraryQueue.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoLibraryQueue.h"
+
+using namespace KODI::GUILIB::GUIINFO;
+
+CLibraryGUIInfo::CLibraryGUIInfo()
+{
+ ResetLibraryBools();
+}
+
+bool CLibraryGUIInfo::GetLibraryBool(int condition) const
+{
+ bool value = false;
+ GetBool(value, nullptr, 0, CGUIInfo(condition));
+ return value;
+}
+
+void CLibraryGUIInfo::SetLibraryBool(int condition, bool value)
+{
+ switch (condition)
+ {
+ case LIBRARY_HAS_MUSIC:
+ m_libraryHasMusic = value ? 1 : 0;
+ break;
+ case LIBRARY_HAS_MOVIES:
+ m_libraryHasMovies = value ? 1 : 0;
+ break;
+ case LIBRARY_HAS_MOVIE_SETS:
+ m_libraryHasMovieSets = value ? 1 : 0;
+ break;
+ case LIBRARY_HAS_TVSHOWS:
+ m_libraryHasTVShows = value ? 1 : 0;
+ break;
+ case LIBRARY_HAS_MUSICVIDEOS:
+ m_libraryHasMusicVideos = value ? 1 : 0;
+ break;
+ case LIBRARY_HAS_SINGLES:
+ m_libraryHasSingles = value ? 1 : 0;
+ break;
+ case LIBRARY_HAS_COMPILATIONS:
+ m_libraryHasCompilations = value ? 1 : 0;
+ break;
+ case LIBRARY_HAS_BOXSETS:
+ m_libraryHasBoxsets = value ? 1 : 0;
+ break;
+ default:
+ break;
+ }
+}
+
+void CLibraryGUIInfo::ResetLibraryBools()
+{
+ m_libraryHasMusic = -1;
+ m_libraryHasMovies = -1;
+ m_libraryHasTVShows = -1;
+ m_libraryHasMusicVideos = -1;
+ m_libraryHasMovieSets = -1;
+ m_libraryHasSingles = -1;
+ m_libraryHasCompilations = -1;
+ m_libraryHasBoxsets = -1;
+ m_libraryRoleCounts.clear();
+}
+
+bool CLibraryGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ return false;
+}
+
+bool CLibraryGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ return false;
+}
+
+bool CLibraryGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ return false;
+}
+
+bool CLibraryGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // LIBRARY_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case LIBRARY_HAS_MUSIC:
+ {
+ if (m_libraryHasMusic < 0)
+ { // query
+ CMusicDatabase db;
+ if (db.Open())
+ {
+ m_libraryHasMusic = (db.GetSongsCount() > 0) ? 1 : 0;
+ db.Close();
+ }
+ }
+ value = m_libraryHasMusic > 0;
+ return true;
+ }
+ case LIBRARY_HAS_MOVIES:
+ {
+ if (m_libraryHasMovies < 0)
+ {
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ m_libraryHasMovies = db.HasContent(VideoDbContentType::MOVIES) ? 1 : 0;
+ db.Close();
+ }
+ }
+ value = m_libraryHasMovies > 0;
+ return true;
+ }
+ case LIBRARY_HAS_MOVIE_SETS:
+ {
+ if (m_libraryHasMovieSets < 0)
+ {
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ m_libraryHasMovieSets = db.HasSets() ? 1 : 0;
+ db.Close();
+ }
+ }
+ value = m_libraryHasMovieSets > 0;
+ return true;
+ }
+ case LIBRARY_HAS_TVSHOWS:
+ {
+ if (m_libraryHasTVShows < 0)
+ {
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ m_libraryHasTVShows = db.HasContent(VideoDbContentType::TVSHOWS) ? 1 : 0;
+ db.Close();
+ }
+ }
+ value = m_libraryHasTVShows > 0;
+ return true;
+ }
+ case LIBRARY_HAS_MUSICVIDEOS:
+ {
+ if (m_libraryHasMusicVideos < 0)
+ {
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ m_libraryHasMusicVideos = db.HasContent(VideoDbContentType::MUSICVIDEOS) ? 1 : 0;
+ db.Close();
+ }
+ }
+ value = m_libraryHasMusicVideos > 0;
+ return true;
+ }
+ case LIBRARY_HAS_SINGLES:
+ {
+ if (m_libraryHasSingles < 0)
+ {
+ CMusicDatabase db;
+ if (db.Open())
+ {
+ m_libraryHasSingles = (db.GetSinglesCount() > 0) ? 1 : 0;
+ db.Close();
+ }
+ }
+ value = m_libraryHasSingles > 0;
+ return true;
+ }
+ case LIBRARY_HAS_COMPILATIONS:
+ {
+ if (m_libraryHasCompilations < 0)
+ {
+ CMusicDatabase db;
+ if (db.Open())
+ {
+ m_libraryHasCompilations = (db.GetCompilationAlbumsCount() > 0) ? 1 : 0;
+ db.Close();
+ }
+ }
+ value = m_libraryHasCompilations > 0;
+ return true;
+ }
+ case LIBRARY_HAS_BOXSETS:
+ {
+ if (m_libraryHasBoxsets < 0)
+ {
+ CMusicDatabase db;
+ if (db.Open())
+ {
+ m_libraryHasBoxsets = (db.GetBoxsetsCount() > 0) ? 1 : 0;
+ db.Close();
+ }
+ }
+ value = m_libraryHasBoxsets > 0;
+ return true;
+ }
+ case LIBRARY_HAS_VIDEO:
+ {
+ return (GetBool(value, gitem, contextWindow, CGUIInfo(LIBRARY_HAS_MOVIES)) ||
+ GetBool(value, gitem, contextWindow, CGUIInfo(LIBRARY_HAS_TVSHOWS)) ||
+ GetBool(value, gitem, contextWindow, CGUIInfo(LIBRARY_HAS_MUSICVIDEOS)));
+ }
+ case LIBRARY_HAS_ROLE:
+ {
+ std::string strRole = info.GetData3();
+ // Find value for role if already stored
+ int artistcount = -1;
+ for (const auto &role : m_libraryRoleCounts)
+ {
+ if (StringUtils::EqualsNoCase(strRole, role.first))
+ {
+ artistcount = role.second;
+ break;
+ }
+ }
+ // Otherwise get from DB and store
+ if (artistcount < 0)
+ {
+ CMusicDatabase db;
+ if (db.Open())
+ {
+ artistcount = db.GetArtistCountForRole(strRole);
+ db.Close();
+ m_libraryRoleCounts.emplace_back(std::make_pair(strRole, artistcount));
+ }
+ }
+ value = artistcount > 0;
+ return true;
+ }
+ case LIBRARY_HAS_NODE:
+ {
+ const CURL url(info.GetData3());
+ const std::shared_ptr<CProfileManager> profileManager =
+ CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ CFileItemList items;
+
+ std::string libDir = profileManager->GetLibraryFolder();
+ XFILE::CDirectory::GetDirectory(libDir, items, "", XFILE::DIR_FLAG_NO_FILE_DIRS);
+ if (items.Size() == 0)
+ libDir = "special://xbmc/system/library/";
+
+ std::string nodePath = URIUtils::AddFileToFolder(libDir, url.GetHostName() + "/");
+ nodePath = URIUtils::AddFileToFolder(nodePath, url.GetFileName());
+ value = CFileUtils::Exists(nodePath);
+ return true;
+ }
+ case LIBRARY_IS_SCANNING:
+ {
+ value = (CMusicLibraryQueue::GetInstance().IsScanningLibrary() ||
+ CVideoLibraryQueue::GetInstance().IsScanningLibrary());
+ return true;
+ }
+ case LIBRARY_IS_SCANNING_VIDEO:
+ {
+ value = CVideoLibraryQueue::GetInstance().IsScanningLibrary();
+ return true;
+ }
+ case LIBRARY_IS_SCANNING_MUSIC:
+ {
+ value = CMusicLibraryQueue::GetInstance().IsScanningLibrary();
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/guilib/guiinfo/LibraryGUIInfo.h b/xbmc/guilib/guiinfo/LibraryGUIInfo.h
new file mode 100644
index 0000000..a9d47fc
--- /dev/null
+++ b/xbmc/guilib/guiinfo/LibraryGUIInfo.h
@@ -0,0 +1,59 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProvider.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CLibraryGUIInfo : public CGUIInfoProvider
+{
+public:
+ CLibraryGUIInfo();
+ ~CLibraryGUIInfo() override = default;
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+
+ bool GetLibraryBool(int condition) const;
+ void SetLibraryBool(int condition, bool value);
+ void ResetLibraryBools();
+
+private:
+ mutable int m_libraryHasMusic;
+ mutable int m_libraryHasMovies;
+ mutable int m_libraryHasTVShows;
+ mutable int m_libraryHasMusicVideos;
+ mutable int m_libraryHasMovieSets;
+ mutable int m_libraryHasSingles;
+ mutable int m_libraryHasCompilations;
+ mutable int m_libraryHasBoxsets;
+
+ //Count of artists in music library contributing to song by role e.g. composers, conductors etc.
+ //For checking visibility of custom nodes for a role.
+ mutable std::vector<std::pair<std::string, int>> m_libraryRoleCounts;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/MusicGUIInfo.cpp b/xbmc/guilib/guiinfo/MusicGUIInfo.cpp
new file mode 100644
index 0000000..f58a9ae
--- /dev/null
+++ b/xbmc/guilib/guiinfo/MusicGUIInfo.cpp
@@ -0,0 +1,713 @@
+/*
+ * 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 "guilib/guiinfo/MusicGUIInfo.h"
+
+#include "FileItem.h"
+#include "PartyModeManager.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoHelper.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "music/MusicInfoLoader.h"
+#include "music/MusicThumbLoader.h"
+#include "music/tags/MusicInfoTag.h"
+#include "playlists/PlayList.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace KODI::GUILIB;
+using namespace KODI::GUILIB::GUIINFO;
+using namespace MUSIC_INFO;
+
+bool CMusicGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (item && (item->IsAudio() || (item->IsInternetStream() && appPlayer->IsPlayingAudio())))
+ {
+ CLog::Log(LOGDEBUG, "CMusicGUIInfo::InitCurrentItem({})", item->GetPath());
+
+ item->LoadMusicTag();
+
+ CMusicInfoTag* tag = item->GetMusicInfoTag(); // creates item if not yet set, so no nullptr checks needed
+ tag->SetLoaded(true);
+
+ // find a thumb for this file.
+ if (item->IsInternetStream() && !item->IsMusicDb())
+ {
+ if (!g_application.m_strPlayListFile.empty())
+ {
+ CLog::Log(LOGDEBUG, "Streaming media detected... using {} to find a thumb",
+ g_application.m_strPlayListFile);
+ CFileItem streamingItem(g_application.m_strPlayListFile,false);
+
+ CMusicThumbLoader loader;
+ loader.FillThumb(streamingItem);
+ if (streamingItem.HasArt("thumb"))
+ item->SetArt("thumb", streamingItem.GetArt("thumb"));
+ }
+ }
+ else
+ {
+ CMusicThumbLoader loader;
+ loader.LoadItem(item);
+ }
+
+ CMusicInfoLoader::LoadAdditionalTagInfo(item);
+ return true;
+ }
+ return false;
+}
+
+bool CMusicGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ // For musicplayer "offset" and "position" info labels check playlist
+ if (info.GetData1() && ((info.m_info >= MUSICPLAYER_OFFSET_POSITION_FIRST &&
+ info.m_info <= MUSICPLAYER_OFFSET_POSITION_LAST) ||
+ (info.m_info >= PLAYER_OFFSET_POSITION_FIRST && info.m_info <= PLAYER_OFFSET_POSITION_LAST)))
+ return GetPlaylistInfo(value, info);
+
+ const CMusicInfoTag* tag = item->GetMusicInfoTag();
+ if (tag)
+ {
+ switch (info.m_info)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // PLAYER_* / MUSICPLAYER_* / LISTITEM_*
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ case PLAYER_PATH:
+ case PLAYER_FILENAME:
+ case PLAYER_FILEPATH:
+ value = tag->GetURL();
+ if (value.empty())
+ value = item->GetPath();
+ value = GUIINFO::GetFileInfoLabelValueFromPath(info.m_info, value);
+ return true;
+ case PLAYER_TITLE:
+ value = tag->GetTitle();
+ return !value.empty();
+ case MUSICPLAYER_TITLE:
+ value = tag->GetTitle();
+ return !value.empty();
+ case LISTITEM_TITLE:
+ value = tag->GetTitle();
+ return true;
+ case MUSICPLAYER_PLAYCOUNT:
+ case LISTITEM_PLAYCOUNT:
+ if (tag->GetPlayCount() > 0)
+ {
+ value = std::to_string(tag->GetPlayCount());
+ return true;
+ }
+ break;
+ case MUSICPLAYER_LASTPLAYED:
+ case LISTITEM_LASTPLAYED:
+ {
+ const CDateTime& dateTime = tag->GetLastPlayed();
+ if (dateTime.IsValid())
+ {
+ value = dateTime.GetAsLocalizedDate();
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_TRACK_NUMBER:
+ case LISTITEM_TRACKNUMBER:
+ if (tag->Loaded() && tag->GetTrackNumber() > 0)
+ {
+ value = StringUtils::Format("{:02}", tag->GetTrackNumber());
+ return true;
+ }
+ break;
+ case MUSICPLAYER_DISC_NUMBER:
+ case LISTITEM_DISC_NUMBER:
+ if (tag->GetDiscNumber() > 0)
+ {
+ value = std::to_string(tag->GetDiscNumber());
+ return true;
+ }
+ break;
+ case MUSICPLAYER_TOTALDISCS:
+ case LISTITEM_TOTALDISCS:
+ value = std::to_string(tag->GetTotalDiscs());
+ return true;
+ case MUSICPLAYER_DISC_TITLE:
+ case LISTITEM_DISC_TITLE:
+ value = tag->GetDiscSubtitle();
+ return true;
+ case MUSICPLAYER_ARTIST:
+ case LISTITEM_ARTIST:
+ value = tag->GetArtistString();
+ return true;
+ case MUSICPLAYER_ALBUM_ARTIST:
+ case LISTITEM_ALBUM_ARTIST:
+ value = tag->GetAlbumArtistString();
+ return true;
+ case MUSICPLAYER_CONTRIBUTORS:
+ case LISTITEM_CONTRIBUTORS:
+ if (tag->HasContributors())
+ {
+ value = tag->GetContributorsText();
+ return true;
+ }
+ break;
+ case MUSICPLAYER_CONTRIBUTOR_AND_ROLE:
+ case LISTITEM_CONTRIBUTOR_AND_ROLE:
+ if (tag->HasContributors())
+ {
+ value = tag->GetContributorsAndRolesText();
+ return true;
+ }
+ break;
+ case MUSICPLAYER_ALBUM:
+ case LISTITEM_ALBUM:
+ value = tag->GetAlbum();
+ return true;
+ case MUSICPLAYER_YEAR:
+ case LISTITEM_YEAR:
+ value = tag->GetYearString();
+ return true;
+ case MUSICPLAYER_GENRE:
+ case LISTITEM_GENRE:
+ value = StringUtils::Join(tag->GetGenre(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ return true;
+ case MUSICPLAYER_LYRICS:
+ value = tag->GetLyrics();
+ return true;
+ case MUSICPLAYER_RATING:
+ case LISTITEM_RATING:
+ {
+ float rating = tag->GetRating();
+ if (rating > 0.f)
+ {
+ value = StringUtils::FormatNumber(rating);
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_RATING_AND_VOTES:
+ case LISTITEM_RATING_AND_VOTES:
+ {
+ float rating = tag->GetRating();
+ if (rating > 0.f)
+ {
+ int votes = tag->GetVotes();
+ if (votes <= 0)
+ value = StringUtils::FormatNumber(rating);
+ else
+ value =
+ StringUtils::Format(g_localizeStrings.Get(20350), StringUtils::FormatNumber(rating),
+ StringUtils::FormatNumber(votes));
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_USER_RATING:
+ case LISTITEM_USER_RATING:
+ if (tag->GetUserrating() > 0)
+ {
+ value = std::to_string(tag->GetUserrating());
+ return true;
+ }
+ break;
+ case MUSICPLAYER_COMMENT:
+ case LISTITEM_COMMENT:
+ value = tag->GetComment();
+ return true;
+ case MUSICPLAYER_MOOD:
+ case LISTITEM_MOOD:
+ value = tag->GetMood();
+ return true;
+ case LISTITEM_DBTYPE:
+ value = tag->GetType();
+ return true;
+ case MUSICPLAYER_DBID:
+ case LISTITEM_DBID:
+ {
+ int dbId = tag->GetDatabaseId();
+ if (dbId > -1)
+ {
+ value = std::to_string(dbId);
+ return true;
+ }
+ break;
+ }
+ case PLAYER_DURATION:
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlayingAudio())
+ break;
+ }
+ [[fallthrough]];
+ case MUSICPLAYER_DURATION:
+ case LISTITEM_DURATION:
+ {
+ int iDuration = tag->GetDuration();
+ if (iDuration > 0)
+ {
+ value = StringUtils::SecondsToTimeString(iDuration,
+ static_cast<TIME_FORMAT>(info.m_info == LISTITEM_DURATION
+ ? info.GetData4()
+ : info.GetData1()));
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_BPM:
+ case LISTITEM_BPM:
+ if (tag->GetBPM() > 0)
+ {
+ value = std::to_string(tag->GetBPM());
+ return true;
+ }
+ break;
+ case MUSICPLAYER_STATIONNAME:
+ // This property can be used for example by addons to enforce/override the station name.
+ value = item->GetProperty("StationName").asString();
+ if (value.empty())
+ value = tag->GetStationName();
+ return true;
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // LISTITEM_*
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ case LISTITEM_PROPERTY:
+ if (StringUtils::StartsWithNoCase(info.GetData3(), "Role."))
+ {
+ // "Role.xxxx" properties are held in music tag
+ std::string property = info.GetData3();
+ property.erase(0, 5); //Remove Role.
+ value = tag->GetArtistStringForRole(property);
+ return true;
+ }
+ break;
+ case LISTITEM_VOTES:
+ value = StringUtils::FormatNumber(tag->GetVotes());
+ return true;
+ case MUSICPLAYER_ORIGINALDATE:
+ case LISTITEM_ORIGINALDATE:
+ {
+ value = tag->GetOriginalDate();
+ if (!CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_bMusicLibraryUseISODates)
+ value = StringUtils::ISODateToLocalizedDate(value);
+ return true;
+ }
+ case MUSICPLAYER_RELEASEDATE:
+ case LISTITEM_RELEASEDATE:
+ {
+ value = tag->GetReleaseDate();
+ if (!CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_bMusicLibraryUseISODates)
+ value = StringUtils::ISODateToLocalizedDate(value);
+ return true;
+ }
+ break;
+ case LISTITEM_BITRATE:
+ {
+ int BitRate = tag->GetBitRate();
+ if (BitRate > 0)
+ {
+ value = std::to_string(BitRate);
+ return true;
+ }
+ break;
+ }
+ case LISTITEM_SAMPLERATE:
+ {
+ int sampleRate = tag->GetSampleRate();
+ if (sampleRate > 0)
+ {
+ value = StringUtils::Format("{:.5}", static_cast<double>(sampleRate) / 1000.0);
+ return true;
+ }
+ break;
+ }
+ case LISTITEM_MUSICCHANNELS:
+ {
+ int channels = tag->GetNoOfChannels();
+ if (channels > 0)
+ {
+ value = std::to_string(channels);
+ return true;
+ }
+ break;
+ }
+ case LISTITEM_ALBUMSTATUS:
+ value = tag->GetAlbumReleaseStatus();
+ return true;
+ case LISTITEM_FILENAME:
+ case LISTITEM_FILE_EXTENSION:
+ if (item->IsMusicDb())
+ value = URIUtils::GetFileName(tag->GetURL());
+ else if (item->HasVideoInfoTag()) // special handling for music videos, which have both a videotag and a musictag
+ break;
+ else
+ value = URIUtils::GetFileName(item->GetPath());
+
+ if (info.m_info == LISTITEM_FILE_EXTENSION)
+ {
+ std::string strExtension = URIUtils::GetExtension(value);
+ value = StringUtils::TrimLeft(strExtension, ".");
+ }
+ return true;
+ case LISTITEM_FOLDERNAME:
+ case LISTITEM_PATH:
+ if (item->IsMusicDb())
+ value = URIUtils::GetDirectory(tag->GetURL());
+ else if (item->HasVideoInfoTag()) // special handling for music videos, which have both a videotag and a musictag
+ break;
+ else
+ URIUtils::GetParentPath(item->GetPath(), value);
+
+ value = CURL(value).GetWithoutUserDetails();
+
+ if (info.m_info == LISTITEM_FOLDERNAME)
+ {
+ URIUtils::RemoveSlashAtEnd(value);
+ value = URIUtils::GetFileName(value);
+ }
+ return true;
+ case LISTITEM_FILENAME_AND_PATH:
+ if (item->IsMusicDb())
+ value = tag->GetURL();
+ else if (item->HasVideoInfoTag()) // special handling for music videos, which have both a videotag and a musictag
+ break;
+ else
+ value = item->GetPath();
+
+ value = CURL(value).GetWithoutUserDetails();
+ return true;
+ case LISTITEM_DATE_ADDED:
+ if (tag->GetDateAdded().IsValid())
+ {
+ value = tag->GetDateAdded().GetAsLocalizedDate();
+ return true;
+ }
+ break;
+ }
+ }
+
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // MUSICPLAYER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case MUSICPLAYER_PROPERTY:
+ if (StringUtils::StartsWithNoCase(info.GetData3(), "Role.") && item->HasMusicInfoTag())
+ {
+ // "Role.xxxx" properties are held in music tag
+ std::string property = info.GetData3();
+ property.erase(0, 5); //Remove Role.
+ value = item->GetMusicInfoTag()->GetArtistStringForRole(property);
+ return true;
+ }
+ value = item->GetProperty(info.GetData3()).asString();
+ return true;
+ case MUSICPLAYER_PLAYLISTLEN:
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC)
+ {
+ value = GUIINFO::GetPlaylistLabel(PLAYLIST_LENGTH);
+ return true;
+ }
+ break;
+ case MUSICPLAYER_PLAYLISTPOS:
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC)
+ {
+ value = GUIINFO::GetPlaylistLabel(PLAYLIST_POSITION);
+ return true;
+ }
+ break;
+ case MUSICPLAYER_COVER:
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingAudio())
+ {
+ if (fallback)
+ *fallback = "DefaultAlbumCover.png";
+ value = item->HasArt("thumb") ? item->GetArt("thumb") : "DefaultAlbumCover.png";
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_BITRATE:
+ {
+ int iBitrate = m_audioInfo.bitrate;
+ if (iBitrate > 0)
+ {
+ value = std::to_string(std::lrint(static_cast<double>(iBitrate) / 1000.0));
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_CHANNELS:
+ {
+ int iChannels = m_audioInfo.channels;
+ if (iChannels > 0)
+ {
+ value = std::to_string(iChannels);
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_BITSPERSAMPLE:
+ {
+ int iBPS = m_audioInfo.bitspersample;
+ if (iBPS > 0)
+ {
+ value = std::to_string(iBPS);
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_SAMPLERATE:
+ {
+ int iSamplerate = m_audioInfo.samplerate;
+ if (iSamplerate > 0)
+ {
+ value = StringUtils::Format("{:.5}", static_cast<double>(iSamplerate) / 1000.0);
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_CODEC:
+ value = m_audioInfo.codecName;
+ return true;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // MUSICPM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ if (GetPartyModeLabel(value, info))
+ return true;
+
+ return false;
+}
+
+bool CMusicGUIInfo::GetPartyModeLabel(std::string& value, const CGUIInfo &info) const
+{
+ int iSongs = -1;
+
+ switch (info.m_info)
+ {
+ case MUSICPM_SONGSPLAYED:
+ iSongs = g_partyModeManager.GetSongsPlayed();
+ break;
+ case MUSICPM_MATCHINGSONGS:
+ iSongs = g_partyModeManager.GetMatchingSongs();
+ break;
+ case MUSICPM_MATCHINGSONGSPICKED:
+ iSongs = g_partyModeManager.GetMatchingSongsPicked();
+ break;
+ case MUSICPM_MATCHINGSONGSLEFT:
+ iSongs = g_partyModeManager.GetMatchingSongsLeft();
+ break;
+ case MUSICPM_RELAXEDSONGSPICKED:
+ iSongs = g_partyModeManager.GetRelaxedSongs();
+ break;
+ case MUSICPM_RANDOMSONGSPICKED:
+ iSongs = g_partyModeManager.GetRandomSongs();
+ break;
+ }
+
+ if (iSongs >= 0)
+ {
+ value = std::to_string(iSongs);
+ return true;
+ }
+
+ return false;
+}
+
+bool CMusicGUIInfo::GetPlaylistInfo(std::string& value, const CGUIInfo &info) const
+{
+ const PLAYLIST::CPlayList& playlist =
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC);
+ if (playlist.size() < 1)
+ return false;
+
+ int index = info.GetData2();
+ if (info.GetData1() == 1)
+ { // relative index (requires current playlist is TYPE_MUSIC)
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() != PLAYLIST::TYPE_MUSIC)
+ return false;
+
+ index = CServiceBroker::GetPlaylistPlayer().GetNextSong(index);
+ }
+
+ if (index < 0 || index >= playlist.size())
+ return false;
+
+ const CFileItemPtr playlistItem = playlist[index];
+ if (playlistItem->HasMusicInfoTag() && !playlistItem->GetMusicInfoTag()->Loaded())
+ {
+ playlistItem->LoadMusicTag();
+ playlistItem->GetMusicInfoTag()->SetLoaded();
+ }
+ // try to set a thumbnail
+ if (!playlistItem->HasArt("thumb"))
+ {
+ CMusicThumbLoader loader;
+ loader.LoadItem(playlistItem.get());
+ // still no thumb? then just the set the default cover
+ if (!playlistItem->HasArt("thumb"))
+ playlistItem->SetArt("thumb", "DefaultAlbumCover.png");
+ }
+ if (info.m_info == MUSICPLAYER_PLAYLISTPOS)
+ {
+ value = std::to_string(index + 1);
+ return true;
+ }
+ else if (info.m_info == MUSICPLAYER_COVER)
+ {
+ value = playlistItem->GetArt("thumb");
+ return true;
+ }
+
+ return GetLabel(value, playlistItem.get(), 0, CGUIInfo(info.m_info), nullptr);
+}
+
+bool CMusicGUIInfo::GetFallbackLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const CGUIInfo& info,
+ std::string* fallback)
+{
+ // No fallback for musicplayer "offset" and "position" info labels
+ if (info.GetData1() && ((info.m_info >= MUSICPLAYER_OFFSET_POSITION_FIRST &&
+ info.m_info <= MUSICPLAYER_OFFSET_POSITION_LAST) ||
+ (info.m_info >= PLAYER_OFFSET_POSITION_FIRST && info.m_info <= PLAYER_OFFSET_POSITION_LAST)))
+ return false;
+
+ const CMusicInfoTag* tag = item->GetMusicInfoTag();
+ if (tag)
+ {
+ switch (info.m_info)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // MUSICPLAYER_*
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ case MUSICPLAYER_TITLE:
+ value = item->GetLabel();
+ if (value.empty())
+ value = CUtil::GetTitleFromPath(item->GetPath());
+ return true;
+ default:
+ break;
+ }
+ }
+ return false;
+}
+
+bool CMusicGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ return false;
+}
+
+bool CMusicGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ const CFileItem* item = static_cast<const CFileItem*>(gitem);
+ const CMusicInfoTag* tag = item->GetMusicInfoTag();
+
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // MUSICPLAYER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case MUSICPLAYER_CONTENT:
+ value = StringUtils::EqualsNoCase(info.GetData3(), "files");
+ return value; // if no match for this provider, other providers shall be asked.
+ case MUSICPLAYER_HASPREVIOUS:
+ // requires current playlist be TYPE_MUSIC
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC)
+ {
+ value = (CServiceBroker::GetPlaylistPlayer().GetCurrentSong() > 0); // not first song
+ return true;
+ }
+ break;
+ case MUSICPLAYER_HASNEXT:
+ // requires current playlist be TYPE_MUSIC
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC)
+ {
+ value = (CServiceBroker::GetPlaylistPlayer().GetCurrentSong() <
+ (CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC).size() -
+ 1)); // not last song
+ return true;
+ }
+ break;
+ case MUSICPLAYER_PLAYLISTPLAYING:
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingAudio() &&
+ CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC)
+ {
+ value = true;
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_EXISTS:
+ {
+ int index = info.GetData2();
+ if (info.GetData1() == 1)
+ { // relative index
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() != PLAYLIST::TYPE_MUSIC)
+ {
+ value = false;
+ return true;
+ }
+ index += CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ }
+ value =
+ (index >= 0 &&
+ index < CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC).size());
+ return true;
+ }
+ case MUSICPLAYER_ISMULTIDISC:
+ if (tag)
+ {
+ value = (item->GetMusicInfoTag()->GetTotalDiscs() > 1);
+ return true;
+ }
+ break;
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // MUSICPM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case MUSICPM_ENABLED:
+ value = g_partyModeManager.IsEnabled();
+ return true;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // LISTITEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case LISTITEM_IS_BOXSET:
+ if (tag)
+ {
+ value = tag->GetBoxset() == true;
+ return true;
+ }
+ break;
+ }
+
+ return false;
+}
diff --git a/xbmc/guilib/guiinfo/MusicGUIInfo.h b/xbmc/guilib/guiinfo/MusicGUIInfo.h
new file mode 100644
index 0000000..4132ca6
--- /dev/null
+++ b/xbmc/guilib/guiinfo/MusicGUIInfo.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProvider.h"
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CMusicGUIInfo : public CGUIInfoProvider
+{
+public:
+ CMusicGUIInfo() = default;
+ ~CMusicGUIInfo() override = default;
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetFallbackLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const CGUIInfo& info,
+ std::string* fallback) override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+
+private:
+ bool GetPartyModeLabel(std::string& value, const CGUIInfo &info) const;
+ bool GetPlaylistInfo(std::string& value, const CGUIInfo &info) const;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/PicturesGUIInfo.cpp b/xbmc/guilib/guiinfo/PicturesGUIInfo.cpp
new file mode 100644
index 0000000..6115517
--- /dev/null
+++ b/xbmc/guilib/guiinfo/PicturesGUIInfo.cpp
@@ -0,0 +1,259 @@
+/*
+ * 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 "guilib/guiinfo/PicturesGUIInfo.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "pictures/GUIWindowSlideShow.h"
+#include "pictures/PictureInfoTag.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <map>
+
+using namespace KODI::GUILIB::GUIINFO;
+
+static const std::map<int, int> listitem2slideshow_map =
+{
+ { LISTITEM_PICTURE_RESOLUTION , SLIDESHOW_RESOLUTION },
+ { LISTITEM_PICTURE_LONGDATE , SLIDESHOW_EXIF_LONG_DATE },
+ { LISTITEM_PICTURE_LONGDATETIME , SLIDESHOW_EXIF_LONG_DATE_TIME },
+ { LISTITEM_PICTURE_DATE , SLIDESHOW_EXIF_DATE },
+ { LISTITEM_PICTURE_DATETIME , SLIDESHOW_EXIF_DATE_TIME },
+ { LISTITEM_PICTURE_COMMENT , SLIDESHOW_COMMENT },
+ { LISTITEM_PICTURE_CAPTION , SLIDESHOW_IPTC_CAPTION },
+ { LISTITEM_PICTURE_DESC , SLIDESHOW_EXIF_DESCRIPTION },
+ { LISTITEM_PICTURE_KEYWORDS , SLIDESHOW_IPTC_KEYWORDS },
+ { LISTITEM_PICTURE_CAM_MAKE , SLIDESHOW_EXIF_CAMERA_MAKE },
+ { LISTITEM_PICTURE_CAM_MODEL , SLIDESHOW_EXIF_CAMERA_MODEL },
+ { LISTITEM_PICTURE_APERTURE , SLIDESHOW_EXIF_APERTURE },
+ { LISTITEM_PICTURE_FOCAL_LEN , SLIDESHOW_EXIF_FOCAL_LENGTH },
+ { LISTITEM_PICTURE_FOCUS_DIST , SLIDESHOW_EXIF_FOCUS_DIST },
+ { LISTITEM_PICTURE_EXP_MODE , SLIDESHOW_EXIF_EXPOSURE_MODE },
+ { LISTITEM_PICTURE_EXP_TIME , SLIDESHOW_EXIF_EXPOSURE_TIME },
+ { LISTITEM_PICTURE_ISO , SLIDESHOW_EXIF_ISO_EQUIV },
+ { LISTITEM_PICTURE_AUTHOR , SLIDESHOW_IPTC_AUTHOR },
+ { LISTITEM_PICTURE_BYLINE , SLIDESHOW_IPTC_BYLINE },
+ { LISTITEM_PICTURE_BYLINE_TITLE , SLIDESHOW_IPTC_BYLINE_TITLE },
+ { LISTITEM_PICTURE_CATEGORY , SLIDESHOW_IPTC_CATEGORY },
+ { LISTITEM_PICTURE_CCD_WIDTH , SLIDESHOW_EXIF_CCD_WIDTH },
+ { LISTITEM_PICTURE_CITY , SLIDESHOW_IPTC_CITY },
+ { LISTITEM_PICTURE_URGENCY , SLIDESHOW_IPTC_URGENCY },
+ { LISTITEM_PICTURE_COPYRIGHT_NOTICE , SLIDESHOW_IPTC_COPYRIGHT_NOTICE },
+ { LISTITEM_PICTURE_COUNTRY , SLIDESHOW_IPTC_COUNTRY },
+ { LISTITEM_PICTURE_COUNTRY_CODE , SLIDESHOW_IPTC_COUNTRY_CODE },
+ { LISTITEM_PICTURE_CREDIT , SLIDESHOW_IPTC_CREDIT },
+ { LISTITEM_PICTURE_IPTCDATE , SLIDESHOW_IPTC_DATE },
+ { LISTITEM_PICTURE_DIGITAL_ZOOM , SLIDESHOW_EXIF_DIGITAL_ZOOM, },
+ { LISTITEM_PICTURE_EXPOSURE , SLIDESHOW_EXIF_EXPOSURE },
+ { LISTITEM_PICTURE_EXPOSURE_BIAS , SLIDESHOW_EXIF_EXPOSURE_BIAS },
+ { LISTITEM_PICTURE_FLASH_USED , SLIDESHOW_EXIF_FLASH_USED },
+ { LISTITEM_PICTURE_HEADLINE , SLIDESHOW_IPTC_HEADLINE },
+ { LISTITEM_PICTURE_COLOUR , SLIDESHOW_COLOUR },
+ { LISTITEM_PICTURE_LIGHT_SOURCE , SLIDESHOW_EXIF_LIGHT_SOURCE },
+ { LISTITEM_PICTURE_METERING_MODE , SLIDESHOW_EXIF_METERING_MODE },
+ { LISTITEM_PICTURE_OBJECT_NAME , SLIDESHOW_IPTC_OBJECT_NAME },
+ { LISTITEM_PICTURE_ORIENTATION , SLIDESHOW_EXIF_ORIENTATION },
+ { LISTITEM_PICTURE_PROCESS , SLIDESHOW_PROCESS },
+ { LISTITEM_PICTURE_REF_SERVICE , SLIDESHOW_IPTC_REF_SERVICE },
+ { LISTITEM_PICTURE_SOURCE , SLIDESHOW_IPTC_SOURCE },
+ { LISTITEM_PICTURE_SPEC_INSTR , SLIDESHOW_IPTC_SPEC_INSTR },
+ { LISTITEM_PICTURE_STATE , SLIDESHOW_IPTC_STATE },
+ { LISTITEM_PICTURE_SUP_CATEGORIES , SLIDESHOW_IPTC_SUP_CATEGORIES },
+ { LISTITEM_PICTURE_TX_REFERENCE , SLIDESHOW_IPTC_TX_REFERENCE },
+ { LISTITEM_PICTURE_WHITE_BALANCE , SLIDESHOW_EXIF_WHITE_BALANCE },
+ { LISTITEM_PICTURE_IMAGETYPE , SLIDESHOW_IPTC_IMAGETYPE },
+ { LISTITEM_PICTURE_SUBLOCATION , SLIDESHOW_IPTC_SUBLOCATION },
+ { LISTITEM_PICTURE_TIMECREATED , SLIDESHOW_IPTC_TIMECREATED },
+ { LISTITEM_PICTURE_GPS_LAT , SLIDESHOW_EXIF_GPS_LATITUDE },
+ { LISTITEM_PICTURE_GPS_LON , SLIDESHOW_EXIF_GPS_LONGITUDE },
+ { LISTITEM_PICTURE_GPS_ALT , SLIDESHOW_EXIF_GPS_ALTITUDE }
+};
+
+CPicturesGUIInfo::CPicturesGUIInfo() = default;
+
+CPicturesGUIInfo::~CPicturesGUIInfo() = default;
+
+void CPicturesGUIInfo::SetCurrentSlide(CFileItem *item)
+{
+ if (m_currentSlide && item && m_currentSlide->GetPath() == item->GetPath())
+ return;
+
+ if (item)
+ {
+ if (item->HasPictureInfoTag()) // Note: item may also be a video
+ {
+ CPictureInfoTag* tag = item->GetPictureInfoTag();
+ if (!tag->Loaded()) // If picture metadata has not been loaded yet, load it now
+ tag->Load(item->GetPath());
+ }
+ m_currentSlide.reset(new CFileItem(*item));
+ }
+ else if (m_currentSlide)
+ {
+ m_currentSlide.reset();
+ }
+}
+
+const CFileItem* CPicturesGUIInfo::GetCurrentSlide() const
+{
+ return m_currentSlide.get();
+}
+
+bool CPicturesGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ return false;
+}
+
+bool CPicturesGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ if (item->IsPicture() && info.m_info >= LISTITEM_PICTURE_START && info.m_info <= LISTITEM_PICTURE_END)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // LISTITEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ const auto& it = listitem2slideshow_map.find(info.m_info);
+ if (it != listitem2slideshow_map.end())
+ {
+ if (item->HasPictureInfoTag())
+ {
+ value = item->GetPictureInfoTag()->GetInfo(it->second);
+ return true;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR,
+ "CPicturesGUIInfo::GetLabel - cannot map LISTITEM ({}) to SLIDESHOW label!",
+ info.m_info);
+ return false;
+ }
+ }
+ else if (m_currentSlide && info.m_info >= SLIDESHOW_LABELS_START && info.m_info <= SLIDESHOW_LABELS_END)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SLIDESHOW_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ switch (info.m_info)
+ {
+ case SLIDESHOW_FILE_NAME:
+ {
+ value = URIUtils::GetFileName(m_currentSlide->GetPath());
+ return true;
+ }
+ case SLIDESHOW_FILE_PATH:
+ {
+ const std::string path = URIUtils::GetDirectory(m_currentSlide->GetPath());
+ value = CURL(path).GetWithoutUserDetails();
+ return true;
+ }
+ case SLIDESHOW_FILE_SIZE:
+ {
+ if (!m_currentSlide->m_bIsFolder || m_currentSlide->m_dwSize)
+ {
+ value = StringUtils::SizeToString(m_currentSlide->m_dwSize);
+ return true;
+ }
+ break;
+ }
+ case SLIDESHOW_FILE_DATE:
+ {
+ if (m_currentSlide->m_dateTime.IsValid())
+ {
+ value = m_currentSlide->m_dateTime.GetAsLocalizedDate();
+ return true;
+ }
+ break;
+ }
+ case SLIDESHOW_INDEX:
+ {
+ CGUIWindowSlideShow *slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (slideshow && slideshow->NumSlides())
+ {
+ value = StringUtils::Format("{}/{}", slideshow->CurrentSlide(), slideshow->NumSlides());
+ return true;
+ }
+ break;
+ }
+ default:
+ {
+ value = m_currentSlide->GetPictureInfoTag()->GetInfo(info.m_info);
+ return true;
+ }
+ }
+ }
+
+ if (item->IsPicture())
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // LISTITEM_*
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ switch (info.m_info)
+ {
+ case LISTITEM_PICTURE_PATH:
+ {
+ if (!(item->IsZIP() || item->IsRAR() || item->IsCBZ() || item->IsCBR()))
+ {
+ value = item->GetPath();
+ return true;
+ }
+ break;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool CPicturesGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ return false;
+}
+
+bool CPicturesGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SLIDESHOW_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case SLIDESHOW_ISPAUSED:
+ {
+ CGUIWindowSlideShow *slideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ value = (slideShow && slideShow->IsPaused());
+ return true;
+ }
+ case SLIDESHOW_ISRANDOM:
+ {
+ CGUIWindowSlideShow *slideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ value = (slideShow && slideShow->IsShuffled());
+ return true;
+ }
+ case SLIDESHOW_ISACTIVE:
+ {
+ CGUIWindowSlideShow *slideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ value = (slideShow && slideShow->InSlideShow());
+ return true;
+ }
+ case SLIDESHOW_ISVIDEO:
+ {
+ CGUIWindowSlideShow *slideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ value = (slideShow && slideShow->GetCurrentSlide() && slideShow->GetCurrentSlide()->IsVideo());
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/guilib/guiinfo/PicturesGUIInfo.h b/xbmc/guilib/guiinfo/PicturesGUIInfo.h
new file mode 100644
index 0000000..68cdb4c
--- /dev/null
+++ b/xbmc/guilib/guiinfo/PicturesGUIInfo.h
@@ -0,0 +1,45 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProvider.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CPicturesGUIInfo : public CGUIInfoProvider
+{
+public:
+ CPicturesGUIInfo();
+ ~CPicturesGUIInfo() override;
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+
+ void SetCurrentSlide(CFileItem *item);
+ const CFileItem* GetCurrentSlide() const;
+
+private:
+ std::unique_ptr<CFileItem> m_currentSlide;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/PlayerGUIInfo.cpp b/xbmc/guilib/guiinfo/PlayerGUIInfo.cpp
new file mode 100644
index 0000000..dfb9455
--- /dev/null
+++ b/xbmc/guilib/guiinfo/PlayerGUIInfo.cpp
@@ -0,0 +1,726 @@
+/*
+ * 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 "guilib/guiinfo/PlayerGUIInfo.h"
+
+#include "FileItem.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "cores/DataCacheCore.h"
+#include "cores/EdlEdit.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIDialog.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoHelper.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <charconv>
+#include <cmath>
+
+using namespace KODI::GUILIB::GUIINFO;
+
+CPlayerGUIInfo::CPlayerGUIInfo()
+ : m_appPlayer(CServiceBroker::GetAppComponents().GetComponent<CApplicationPlayer>()),
+ m_appVolume(CServiceBroker::GetAppComponents().GetComponent<CApplicationVolumeHandling>())
+{
+}
+
+CPlayerGUIInfo::~CPlayerGUIInfo() = default;
+
+int CPlayerGUIInfo::GetTotalPlayTime() const
+{
+ return std::lrint(g_application.GetTotalTime());
+}
+
+int CPlayerGUIInfo::GetPlayTime() const
+{
+ return std::lrint(g_application.GetTime());
+}
+
+int CPlayerGUIInfo::GetPlayTimeRemaining() const
+{
+ int iReverse = GetTotalPlayTime() - std::lrint(g_application.GetTime());
+ return iReverse > 0 ? iReverse : 0;
+}
+
+float CPlayerGUIInfo::GetSeekPercent() const
+{
+ int iTotal = GetTotalPlayTime();
+ if (iTotal == 0)
+ return 0.0f;
+
+ float fPercentPlayTime = static_cast<float>(GetPlayTime() * 1000) / iTotal * 0.1f;
+ float fPercentPerSecond = 100.0f / static_cast<float>(iTotal);
+ float fPercent =
+ fPercentPlayTime + fPercentPerSecond * m_appPlayer->GetSeekHandler().GetSeekSize();
+ fPercent = std::max(0.0f, std::min(fPercent, 100.0f));
+ return fPercent;
+}
+
+std::string CPlayerGUIInfo::GetCurrentPlayTime(TIME_FORMAT format) const
+{
+ if (format == TIME_FORMAT_GUESS && GetTotalPlayTime() >= 3600)
+ format = TIME_FORMAT_HH_MM_SS;
+
+ return StringUtils::SecondsToTimeString(std::lrint(GetPlayTime()), format);
+}
+
+std::string CPlayerGUIInfo::GetCurrentPlayTimeRemaining(TIME_FORMAT format) const
+{
+ if (format == TIME_FORMAT_GUESS && GetTotalPlayTime() >= 3600)
+ format = TIME_FORMAT_HH_MM_SS;
+
+ int iTimeRemaining = GetPlayTimeRemaining();
+ if (iTimeRemaining)
+ return StringUtils::SecondsToTimeString(iTimeRemaining, format);
+
+ return std::string();
+}
+
+std::string CPlayerGUIInfo::GetDuration(TIME_FORMAT format) const
+{
+ int iTotal = GetTotalPlayTime();
+ if (iTotal > 0)
+ {
+ if (format == TIME_FORMAT_GUESS && iTotal >= 3600)
+ format = TIME_FORMAT_HH_MM_SS;
+ return StringUtils::SecondsToTimeString(iTotal, format);
+ }
+ return std::string();
+}
+
+std::string CPlayerGUIInfo::GetCurrentSeekTime(TIME_FORMAT format) const
+{
+ if (format == TIME_FORMAT_GUESS && GetTotalPlayTime() >= 3600)
+ format = TIME_FORMAT_HH_MM_SS;
+
+ return StringUtils::SecondsToTimeString(
+ g_application.GetTime() + m_appPlayer->GetSeekHandler().GetSeekSize(), format);
+}
+
+std::string CPlayerGUIInfo::GetSeekTime(TIME_FORMAT format) const
+{
+ if (!m_appPlayer->GetSeekHandler().HasTimeCode())
+ return std::string();
+
+ int iSeekTimeCode = m_appPlayer->GetSeekHandler().GetTimeCodeSeconds();
+ if (format == TIME_FORMAT_GUESS && iSeekTimeCode >= 3600)
+ format = TIME_FORMAT_HH_MM_SS;
+
+ return StringUtils::SecondsToTimeString(iSeekTimeCode, format);
+}
+
+void CPlayerGUIInfo::SetShowInfo(bool showinfo)
+{
+ if (showinfo != m_playerShowInfo)
+ {
+ m_playerShowInfo = showinfo;
+ m_events.Publish(PlayerShowInfoChangedEvent(m_playerShowInfo));
+ }
+}
+
+bool CPlayerGUIInfo::ToggleShowInfo()
+{
+ SetShowInfo(!m_playerShowInfo);
+ return m_playerShowInfo;
+}
+
+bool CPlayerGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ if (item && m_appPlayer->IsPlaying())
+ {
+ CLog::Log(LOGDEBUG, "CPlayerGUIInfo::InitCurrentItem({})", CURL::GetRedacted(item->GetPath()));
+ m_currentItem.reset(new CFileItem(*item));
+ }
+ else
+ {
+ m_currentItem.reset();
+ }
+ return false;
+}
+
+bool CPlayerGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // PLAYER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case PLAYER_SEEKOFFSET:
+ {
+ int lastSeekOffset = CServiceBroker::GetDataCacheCore().GetSeekOffSet();
+ std::string seekOffset = StringUtils::SecondsToTimeString(
+ std::abs(lastSeekOffset / 1000), static_cast<TIME_FORMAT>(info.GetData1()));
+ if (lastSeekOffset < 0)
+ value = "-" + seekOffset;
+ else if (lastSeekOffset > 0)
+ value = "+" + seekOffset;
+ return true;
+ }
+ case PLAYER_PROGRESS:
+ value = std::to_string(std::lrintf(g_application.GetPercentage()));
+ return true;
+ case PLAYER_PROGRESS_CACHE:
+ value = std::to_string(std::lrintf(g_application.GetCachePercentage()));
+ return true;
+ case PLAYER_VOLUME:
+ value =
+ StringUtils::Format("{:2.1f} dB", CAEUtil::PercentToGain(m_appVolume->GetVolumeRatio()));
+ return true;
+ case PLAYER_SUBTITLE_DELAY:
+ value = StringUtils::Format("{:2.3f} s", m_appPlayer->GetVideoSettings().m_SubtitleDelay);
+ return true;
+ case PLAYER_AUDIO_DELAY:
+ value = StringUtils::Format("{:2.3f} s", m_appPlayer->GetVideoSettings().m_AudioDelay);
+ return true;
+ case PLAYER_CHAPTER:
+ value = StringUtils::Format("{:02}", m_appPlayer->GetChapter());
+ return true;
+ case PLAYER_CHAPTERCOUNT:
+ value = StringUtils::Format("{:02}", m_appPlayer->GetChapterCount());
+ return true;
+ case PLAYER_CHAPTERNAME:
+ m_appPlayer->GetChapterName(value);
+ return true;
+ case PLAYER_PATH:
+ case PLAYER_FILENAME:
+ case PLAYER_FILEPATH:
+ value = GUIINFO::GetFileInfoLabelValueFromPath(info.m_info, item->GetPath());
+ return true;
+ case PLAYER_TITLE:
+ // use label or drop down to title from path
+ value = item->GetLabel();
+ if (value.empty())
+ value = CUtil::GetTitleFromPath(item->GetPath());
+ return true;
+ case PLAYER_PLAYSPEED:
+ {
+ float speed = m_appPlayer->GetPlaySpeed();
+ if (speed == 1.0f)
+ speed = m_appPlayer->GetPlayTempo();
+ value = StringUtils::Format("{:.2f}", speed);
+ return true;
+ }
+ case PLAYER_TIME:
+ value = GetCurrentPlayTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PLAYER_START_TIME:
+ {
+ const CDateTime time(m_appPlayer->GetStartTime());
+ value = time.GetAsLocalizedTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PLAYER_DURATION:
+ value = GetDuration(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PLAYER_TIME_REMAINING:
+ value = GetCurrentPlayTimeRemaining(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PLAYER_FINISH_TIME:
+ {
+ CDateTime time(CDateTime::GetCurrentDateTime());
+ int playTimeRemaining = GetPlayTimeRemaining();
+ float speed = m_appPlayer->GetPlaySpeed();
+ float tempo = m_appPlayer->GetPlayTempo();
+ if (speed == 1.0f)
+ playTimeRemaining /= tempo;
+ time += CDateTimeSpan(0, 0, 0, playTimeRemaining);
+ value = time.GetAsLocalizedTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PLAYER_TIME_SPEED:
+ {
+ float speed = m_appPlayer->GetPlaySpeed();
+ if (speed != 1.0f)
+ value = StringUtils::Format("{} ({}x)",
+ GetCurrentPlayTime(static_cast<TIME_FORMAT>(info.GetData1())),
+ static_cast<int>(speed));
+ else
+ value = GetCurrentPlayTime(TIME_FORMAT_GUESS);
+ return true;
+ }
+ case PLAYER_SEEKTIME:
+ value = GetCurrentSeekTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PLAYER_SEEKSTEPSIZE:
+ {
+ int seekSize = m_appPlayer->GetSeekHandler().GetSeekSize();
+ std::string strSeekSize = StringUtils::SecondsToTimeString(abs(seekSize), static_cast<TIME_FORMAT>(info.GetData1()));
+ if (seekSize < 0)
+ value = "-" + strSeekSize;
+ if (seekSize > 0)
+ value = "+" + strSeekSize;
+ return true;
+ }
+ case PLAYER_SEEKNUMERIC:
+ value = GetSeekTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return !value.empty();
+ case PLAYER_CACHELEVEL:
+ {
+ int iLevel = m_appPlayer->GetCacheLevel();
+ if (iLevel >= 0)
+ {
+ value = std::to_string(iLevel);
+ return true;
+ }
+ break;
+ }
+ case PLAYER_ITEM_ART:
+ value = item->GetArt(info.GetData3());
+ return true;
+ case PLAYER_ICON:
+ value = item->GetArt("thumb");
+ if (value.empty())
+ value = item->GetArt("icon");
+ if (fallback)
+ *fallback = item->GetArt("icon");
+ return true;
+ case PLAYER_EDITLIST:
+ case PLAYER_CUTS:
+ case PLAYER_SCENE_MARKERS:
+ case PLAYER_CUTLIST:
+ case PLAYER_CHAPTERS:
+ value = GetContentRanges(info.m_info);
+ return true;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // PLAYER_PROCESS_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case PLAYER_PROCESS_VIDEODECODER:
+ value = CServiceBroker::GetDataCacheCore().GetVideoDecoderName();
+ return true;
+ case PLAYER_PROCESS_DEINTMETHOD:
+ value = CServiceBroker::GetDataCacheCore().GetVideoDeintMethod();
+ return true;
+ case PLAYER_PROCESS_PIXELFORMAT:
+ value = CServiceBroker::GetDataCacheCore().GetVideoPixelFormat();
+ return true;
+ case PLAYER_PROCESS_VIDEOFPS:
+ value = StringUtils::Format("{:.3f}", CServiceBroker::GetDataCacheCore().GetVideoFps());
+ return true;
+ case PLAYER_PROCESS_VIDEODAR:
+ value = StringUtils::Format("{:.2f}", CServiceBroker::GetDataCacheCore().GetVideoDAR());
+ return true;
+ case PLAYER_PROCESS_VIDEOWIDTH:
+ value = StringUtils::FormatNumber(CServiceBroker::GetDataCacheCore().GetVideoWidth());
+ return true;
+ case PLAYER_PROCESS_VIDEOHEIGHT:
+ value = StringUtils::FormatNumber(CServiceBroker::GetDataCacheCore().GetVideoHeight());
+ return true;
+ case PLAYER_PROCESS_VIDEOSCANTYPE:
+ value = CServiceBroker::GetDataCacheCore().IsVideoInterlaced() ? "i" : "p";
+ return true;
+ case PLAYER_PROCESS_AUDIODECODER:
+ value = CServiceBroker::GetDataCacheCore().GetAudioDecoderName();
+ return true;
+ case PLAYER_PROCESS_AUDIOCHANNELS:
+ value = CServiceBroker::GetDataCacheCore().GetAudioChannels();
+ return true;
+ case PLAYER_PROCESS_AUDIOSAMPLERATE:
+ value = StringUtils::FormatNumber(CServiceBroker::GetDataCacheCore().GetAudioSampleRate());
+ return true;
+ case PLAYER_PROCESS_AUDIOBITSPERSAMPLE:
+ value = StringUtils::FormatNumber(CServiceBroker::GetDataCacheCore().GetAudioBitsPerSample());
+ return true;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // PLAYLIST_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case PLAYLIST_LENGTH:
+ case PLAYLIST_POSITION:
+ case PLAYLIST_RANDOM:
+ case PLAYLIST_REPEAT:
+ value = GUIINFO::GetPlaylistLabel(info.m_info, info.GetData1());
+ return true;
+ }
+
+ return false;
+}
+
+bool CPlayerGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // PLAYER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case PLAYER_VOLUME:
+ value = static_cast<int>(m_appVolume->GetVolumePercent());
+ return true;
+ case PLAYER_PROGRESS:
+ value = std::lrintf(g_application.GetPercentage());
+ return true;
+ case PLAYER_PROGRESS_CACHE:
+ value = std::lrintf(g_application.GetCachePercentage());
+ return true;
+ case PLAYER_SEEKBAR:
+ value = std::lrintf(GetSeekPercent());
+ return true;
+ case PLAYER_CACHELEVEL:
+ value = m_appPlayer->GetCacheLevel();
+ return true;
+ case PLAYER_CHAPTER:
+ value = m_appPlayer->GetChapter();
+ return true;
+ case PLAYER_CHAPTERCOUNT:
+ value = m_appPlayer->GetChapterCount();
+ return true;
+ case PLAYER_SUBTITLE_DELAY:
+ value = m_appPlayer->GetSubtitleDelay();
+ return true;
+ case PLAYER_AUDIO_DELAY:
+ value = m_appPlayer->GetAudioDelay();
+ return true;
+ }
+
+ return false;
+}
+
+bool CPlayerGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ const CFileItem *item = nullptr;
+ if (gitem->IsFileItem())
+ item = static_cast<const CFileItem*>(gitem);
+
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // PLAYER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case PLAYER_SHOWINFO:
+ value = m_playerShowInfo;
+ return true;
+ case PLAYER_SHOWTIME:
+ value = m_playerShowTime;
+ return true;
+ case PLAYER_MUTED:
+ value = (m_appVolume->IsMuted() ||
+ m_appVolume->GetVolumeRatio() <= CApplicationVolumeHandling::VOLUME_MINIMUM);
+ return true;
+ case PLAYER_HAS_MEDIA:
+ value = m_appPlayer->IsPlaying();
+ return true;
+ case PLAYER_HAS_AUDIO:
+ value = m_appPlayer->IsPlayingAudio();
+ return true;
+ case PLAYER_HAS_VIDEO:
+ value = m_appPlayer->IsPlayingVideo();
+ return true;
+ case PLAYER_HAS_GAME:
+ value = m_appPlayer->IsPlayingGame();
+ return true;
+ case PLAYER_PLAYING:
+ value = m_appPlayer->GetPlaySpeed() == 1.0f;
+ return true;
+ case PLAYER_PAUSED:
+ value = m_appPlayer->IsPausedPlayback();
+ return true;
+ case PLAYER_REWINDING:
+ value = m_appPlayer->GetPlaySpeed() < 0.0f;
+ return true;
+ case PLAYER_FORWARDING:
+ value = m_appPlayer->GetPlaySpeed() > 1.5f;
+ return true;
+ case PLAYER_REWINDING_2x:
+ value = m_appPlayer->GetPlaySpeed() == -2;
+ return true;
+ case PLAYER_REWINDING_4x:
+ value = m_appPlayer->GetPlaySpeed() == -4;
+ return true;
+ case PLAYER_REWINDING_8x:
+ value = m_appPlayer->GetPlaySpeed() == -8;
+ return true;
+ case PLAYER_REWINDING_16x:
+ value = m_appPlayer->GetPlaySpeed() == -16;
+ return true;
+ case PLAYER_REWINDING_32x:
+ value = m_appPlayer->GetPlaySpeed() == -32;
+ return true;
+ case PLAYER_FORWARDING_2x:
+ value = m_appPlayer->GetPlaySpeed() == 2;
+ return true;
+ case PLAYER_FORWARDING_4x:
+ value = m_appPlayer->GetPlaySpeed() == 4;
+ return true;
+ case PLAYER_FORWARDING_8x:
+ value = m_appPlayer->GetPlaySpeed() == 8;
+ return true;
+ case PLAYER_FORWARDING_16x:
+ value = m_appPlayer->GetPlaySpeed() == 16;
+ return true;
+ case PLAYER_FORWARDING_32x:
+ value = m_appPlayer->GetPlaySpeed() == 32;
+ return true;
+ case PLAYER_CAN_PAUSE:
+ value = m_appPlayer->CanPause();
+ return true;
+ case PLAYER_CAN_SEEK:
+ value = m_appPlayer->CanSeek();
+ return true;
+ case PLAYER_SUPPORTS_TEMPO:
+ value = m_appPlayer->SupportsTempo();
+ return true;
+ case PLAYER_IS_TEMPO:
+ value = (m_appPlayer->GetPlayTempo() != 1.0f && m_appPlayer->GetPlaySpeed() == 1.0f);
+ return true;
+ case PLAYER_CACHING:
+ value = m_appPlayer->IsCaching();
+ return true;
+ case PLAYER_SEEKBAR:
+ {
+ CGUIDialog *seekBar = CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_SEEK_BAR);
+ value = seekBar ? seekBar->IsDialogRunning() : false;
+ return true;
+ }
+ case PLAYER_SEEKING:
+ value = m_appPlayer->GetSeekHandler().InProgress();
+ return true;
+ case PLAYER_HASPERFORMEDSEEK:
+ {
+ int requestedLastSecondInterval{0};
+ std::from_chars_result result =
+ std::from_chars(info.GetData3().data(), info.GetData3().data() + info.GetData3().size(),
+ requestedLastSecondInterval);
+ if (result.ec == std::errc::invalid_argument)
+ {
+ value = false;
+ return false;
+ }
+
+ value = CServiceBroker::GetDataCacheCore().HasPerformedSeek(requestedLastSecondInterval);
+ return true;
+ }
+ case PLAYER_PASSTHROUGH:
+ value = m_appPlayer->IsPassthrough();
+ return true;
+ case PLAYER_ISINTERNETSTREAM:
+ if (item)
+ {
+ value = URIUtils::IsInternetStream(item->GetDynPath());
+ return true;
+ }
+ break;
+ case PLAYER_HAS_PROGRAMS:
+ value = (m_appPlayer->GetProgramsCount() > 1) ? true : false;
+ return true;
+ case PLAYER_HAS_RESOLUTIONS:
+ value = CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot() &&
+ CResolutionUtils::HasWhitelist();
+ return true;
+ case PLAYER_HASDURATION:
+ value = g_application.GetTotalTime() > 0;
+ return true;
+ case PLAYER_FRAMEADVANCE:
+ value = CServiceBroker::GetDataCacheCore().IsFrameAdvance();
+ return true;
+ case PLAYER_HAS_SCENE_MARKERS:
+ value = !CServiceBroker::GetDataCacheCore().GetSceneMarkers().empty();
+ return true;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // PLAYLIST_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case PLAYLIST_ISRANDOM:
+ {
+ PLAYLIST::CPlayListPlayer& player = CServiceBroker::GetPlaylistPlayer();
+ PLAYLIST::Id playlistid = info.GetData1();
+ if (info.GetData2() > 0 && playlistid != PLAYLIST::TYPE_NONE)
+ value = player.IsShuffled(playlistid);
+ else
+ value = player.IsShuffled(player.GetCurrentPlaylist());
+ return true;
+ }
+ case PLAYLIST_ISREPEAT:
+ {
+ PLAYLIST::CPlayListPlayer& player = CServiceBroker::GetPlaylistPlayer();
+ PLAYLIST::Id playlistid = info.GetData1();
+ if (info.GetData2() > 0 && playlistid != PLAYLIST::TYPE_NONE)
+ value = (player.GetRepeat(playlistid) == PLAYLIST::RepeatState::ALL);
+ else
+ value = player.GetRepeat(player.GetCurrentPlaylist()) == PLAYLIST::RepeatState::ALL;
+ return true;
+ }
+ case PLAYLIST_ISREPEATONE:
+ {
+ PLAYLIST::CPlayListPlayer& player = CServiceBroker::GetPlaylistPlayer();
+ PLAYLIST::Id playlistid = info.GetData1();
+ if (info.GetData2() > 0 && playlistid != PLAYLIST::TYPE_NONE)
+ value = (player.GetRepeat(playlistid) == PLAYLIST::RepeatState::ONE);
+ else
+ value = player.GetRepeat(player.GetCurrentPlaylist()) == PLAYLIST::RepeatState::ONE;
+ return true;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // PLAYER_PROCESS_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case PLAYER_PROCESS_VIDEOHWDECODER:
+ value = CServiceBroker::GetDataCacheCore().IsVideoHwDecoder();
+ return true;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // LISTITEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case LISTITEM_ISPLAYING:
+ {
+ if (item)
+ {
+ if (item->HasProperty("playlistposition"))
+ {
+ value = static_cast<int>(item->GetProperty("playlisttype").asInteger()) == CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() &&
+ static_cast<int>(item->GetProperty("playlistposition").asInteger()) == CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ return true;
+ }
+ else if (m_currentItem && !m_currentItem->GetPath().empty())
+ {
+ if (!g_application.m_strPlayListFile.empty())
+ {
+ //playlist file that is currently playing or the playlistitem that is currently playing.
+ value = item->IsPath(g_application.m_strPlayListFile) || m_currentItem->IsSamePath(item);
+ }
+ else
+ {
+ value = m_currentItem->IsSamePath(item);
+ }
+ return true;
+ }
+ }
+ break;
+ }
+ }
+
+ return false;
+}
+
+std::string CPlayerGUIInfo::GetContentRanges(int iInfo) const
+{
+ std::string values;
+
+ CDataCacheCore& data = CServiceBroker::GetDataCacheCore();
+ std::vector<std::pair<float, float>> ranges;
+
+ std::time_t start;
+ int64_t current;
+ int64_t min;
+ int64_t max;
+ data.GetPlayTimes(start, current, min, max);
+
+ std::time_t duration = max - start * 1000;
+ if (duration > 0)
+ {
+ switch (iInfo)
+ {
+ case PLAYER_EDITLIST:
+ case PLAYER_CUTLIST:
+ ranges = GetEditList(data, duration);
+ break;
+ case PLAYER_CUTS:
+ ranges = GetCuts(data, duration);
+ break;
+ case PLAYER_SCENE_MARKERS:
+ ranges = GetSceneMarkers(data, duration);
+ break;
+ case PLAYER_CHAPTERS:
+ ranges = GetChapters(data, duration);
+ break;
+ default:
+ CLog::Log(LOGERROR, "CPlayerGUIInfo::GetContentRanges({}) - unhandled guiinfo", iInfo);
+ break;
+ }
+
+ // create csv string from ranges
+ for (const auto& range : ranges)
+ values += StringUtils::Format("{:.5f},{:.5f},", range.first, range.second);
+
+ if (!values.empty())
+ values.pop_back(); // remove trailing comma
+ }
+
+ return values;
+}
+
+std::vector<std::pair<float, float>> CPlayerGUIInfo::GetEditList(const CDataCacheCore& data,
+ std::time_t duration) const
+{
+ std::vector<std::pair<float, float>> ranges;
+
+ const std::vector<EDL::Edit>& edits = data.GetEditList();
+ for (const auto& edit : edits)
+ {
+ float editStart = edit.start * 100.0f / duration;
+ float editEnd = edit.end * 100.0f / duration;
+ ranges.emplace_back(std::make_pair(editStart, editEnd));
+ }
+ return ranges;
+}
+
+std::vector<std::pair<float, float>> CPlayerGUIInfo::GetCuts(const CDataCacheCore& data,
+ std::time_t duration) const
+{
+ std::vector<std::pair<float, float>> ranges;
+
+ const std::vector<int64_t>& cuts = data.GetCuts();
+ float lastMarker = 0.0f;
+ for (const auto& cut : cuts)
+ {
+ float marker = cut * 100.0f / duration;
+ if (marker != 0)
+ ranges.emplace_back(std::make_pair(lastMarker, marker));
+
+ lastMarker = marker;
+ }
+ return ranges;
+}
+
+std::vector<std::pair<float, float>> CPlayerGUIInfo::GetSceneMarkers(const CDataCacheCore& data,
+ std::time_t duration) const
+{
+ std::vector<std::pair<float, float>> ranges;
+
+ const std::vector<int64_t>& scenes = data.GetSceneMarkers();
+ float lastMarker = 0.0f;
+ for (const auto& scene : scenes)
+ {
+ float marker = scene * 100.0f / duration;
+ if (marker != 0)
+ ranges.emplace_back(std::make_pair(lastMarker, marker));
+
+ lastMarker = marker;
+ }
+ return ranges;
+}
+
+std::vector<std::pair<float, float>> CPlayerGUIInfo::GetChapters(const CDataCacheCore& data,
+ std::time_t duration) const
+{
+ std::vector<std::pair<float, float>> ranges;
+
+ const std::vector<std::pair<std::string, int64_t>>& chapters = data.GetChapters();
+ float lastMarker = 0.0f;
+ for (const auto& chapter : chapters)
+ {
+ float marker = chapter.second * 1000 * 100.0f / duration;
+ if (marker != 0)
+ ranges.emplace_back(std::make_pair(lastMarker, marker));
+
+ lastMarker = marker;
+ }
+ return ranges;
+}
diff --git a/xbmc/guilib/guiinfo/PlayerGUIInfo.h b/xbmc/guilib/guiinfo/PlayerGUIInfo.h
new file mode 100644
index 0000000..e0cb3c8
--- /dev/null
+++ b/xbmc/guilib/guiinfo/PlayerGUIInfo.h
@@ -0,0 +1,93 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProvider.h"
+#include "utils/EventStream.h"
+#include "utils/TimeFormat.h"
+
+#include <atomic>
+#include <ctime>
+#include <memory>
+#include <utility>
+#include <vector>
+
+class CApplicationPlayer;
+class CApplicationVolumeHandling;
+class CDataCacheCore;
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+struct PlayerShowInfoChangedEvent
+{
+ explicit PlayerShowInfoChangedEvent(bool showInfo) : m_showInfo(showInfo) {}
+ virtual ~PlayerShowInfoChangedEvent() = default;
+
+ bool m_showInfo{false};
+};
+
+class CPlayerGUIInfo : public CGUIInfoProvider
+{
+public:
+ CPlayerGUIInfo();
+ ~CPlayerGUIInfo() override;
+
+ CEventStream<PlayerShowInfoChangedEvent>& Events() { return m_events; }
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+
+ void SetShowTime(bool showtime) { m_playerShowTime = showtime; }
+ void SetShowInfo(bool showinfo);
+ bool GetShowInfo() const { return m_playerShowInfo; }
+ bool ToggleShowInfo();
+
+private:
+ int GetTotalPlayTime() const;
+ int GetPlayTime() const;
+ int GetPlayTimeRemaining() const;
+ float GetSeekPercent() const;
+
+ std::string GetCurrentPlayTime(TIME_FORMAT format) const;
+ std::string GetCurrentPlayTimeRemaining(TIME_FORMAT format) const;
+ std::string GetDuration(TIME_FORMAT format) const;
+ std::string GetCurrentSeekTime(TIME_FORMAT format) const;
+ std::string GetSeekTime(TIME_FORMAT format) const;
+
+ std::string GetContentRanges(int iInfo) const;
+ std::vector<std::pair<float, float>> GetEditList(const CDataCacheCore& data,
+ std::time_t duration) const;
+ std::vector<std::pair<float, float>> GetCuts(const CDataCacheCore& data,
+ std::time_t duration) const;
+ std::vector<std::pair<float, float>> GetSceneMarkers(const CDataCacheCore& data,
+ std::time_t duration) const;
+ std::vector<std::pair<float, float>> GetChapters(const CDataCacheCore& data,
+ std::time_t duration) const;
+
+ std::unique_ptr<CFileItem> m_currentItem;
+ std::atomic_bool m_playerShowTime{false};
+ std::atomic_bool m_playerShowInfo{false};
+ const std::shared_ptr<CApplicationPlayer> m_appPlayer;
+ const std::shared_ptr<CApplicationVolumeHandling> m_appVolume;
+ CEventSource<PlayerShowInfoChangedEvent> m_events;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/SkinGUIInfo.cpp b/xbmc/guilib/guiinfo/SkinGUIInfo.cpp
new file mode 100644
index 0000000..1d51b96
--- /dev/null
+++ b/xbmc/guilib/guiinfo/SkinGUIInfo.cpp
@@ -0,0 +1,140 @@
+/*
+ * 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 "guilib/guiinfo/SkinGUIInfo.h"
+
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SkinSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace KODI::GUILIB::GUIINFO;
+
+bool CSkinGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ return false;
+}
+
+bool CSkinGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SKIN_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case SKIN_BOOL:
+ {
+ bool bInfo = CSkinSettings::GetInstance().GetBool(info.GetData1());
+ if (bInfo)
+ {
+ value = g_localizeStrings.Get(20122); // True
+ return true;
+ }
+ break;
+ }
+ case SKIN_STRING:
+ {
+ value = CSkinSettings::GetInstance().GetString(info.GetData1());
+ return true;
+ }
+ case SKIN_THEME:
+ {
+ value = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKINTHEME);
+ return true;
+ }
+ case SKIN_COLOUR_THEME:
+ {
+ value = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKINCOLORS);
+ return true;
+ }
+ case SKIN_ASPECT_RATIO:
+ {
+ if (g_SkinInfo)
+ {
+ value = g_SkinInfo->GetCurrentAspect();
+ return true;
+ }
+ break;
+ }
+ case SKIN_FONT:
+ {
+ value = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_FONT);
+ return true;
+ }
+ case SKIN_TIMER_ELAPSEDSECS:
+ {
+ value = std::to_string(g_SkinInfo->GetTimerElapsedSeconds(info.GetData3()));
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CSkinGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ case SKIN_INTEGER:
+ {
+ value = CSkinSettings::GetInstance().GetInt(info.GetData1());
+ return true;
+ }
+ case SKIN_TIMER_ELAPSEDSECS:
+ {
+ value = g_SkinInfo->GetTimerElapsedSeconds(info.GetData3());
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CSkinGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SKIN_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case SKIN_BOOL:
+ {
+ value = CSkinSettings::GetInstance().GetBool(info.GetData1());
+ return true;
+ }
+ case SKIN_STRING_IS_EQUAL:
+ {
+ value = StringUtils::EqualsNoCase(CSkinSettings::GetInstance().GetString(info.GetData1()), info.GetData3());
+ return true;
+ }
+ case SKIN_STRING:
+ {
+ value = !CSkinSettings::GetInstance().GetString(info.GetData1()).empty();
+ return true;
+ }
+ case SKIN_HAS_THEME:
+ {
+ std::string theme = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKINTHEME);
+ URIUtils::RemoveExtension(theme);
+ value = StringUtils::EqualsNoCase(theme, info.GetData3());
+ return true;
+ }
+ case SKIN_TIMER_IS_RUNNING:
+ {
+ value = g_SkinInfo->TimerIsRunning(info.GetData3());
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/guilib/guiinfo/SkinGUIInfo.h b/xbmc/guilib/guiinfo/SkinGUIInfo.h
new file mode 100644
index 0000000..7f6c1c4
--- /dev/null
+++ b/xbmc/guilib/guiinfo/SkinGUIInfo.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProvider.h"
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CSkinGUIInfo : public CGUIInfoProvider
+{
+public:
+ CSkinGUIInfo() = default;
+ ~CSkinGUIInfo() override = default;
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/SystemGUIInfo.cpp b/xbmc/guilib/guiinfo/SystemGUIInfo.cpp
new file mode 100644
index 0000000..bc01047
--- /dev/null
+++ b/xbmc/guilib/guiinfo/SystemGUIInfo.cpp
@@ -0,0 +1,727 @@
+/*
+ * 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 "guilib/guiinfo/SystemGUIInfo.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "application/AppParams.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "network/Network.h"
+#if defined(TARGET_DARWIN_OSX)
+#include "platform/darwin/osx/smc.h"
+#endif
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoHelper.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "powermanagement/PowerManager.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/SettingUtils.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "storage/discs/IDiscDriveHandler.h"
+#include "utils/AlarmClock.h"
+#include "utils/CPUInfo.h"
+#include "utils/HDRCapabilities.h"
+#include "utils/MemUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/TimeUtils.h"
+#include "windowing/WinSystem.h"
+#include "windows/GUIMediaWindow.h"
+
+using namespace KODI::GUILIB;
+using namespace KODI::GUILIB::GUIINFO;
+
+CSystemGUIInfo::CSystemGUIInfo()
+: m_lastSysHeatInfoTime(-SYSTEM_HEAT_UPDATE_INTERVAL)
+{
+}
+
+std::string CSystemGUIInfo::GetSystemHeatInfo(int info) const
+{
+ if (CTimeUtils::GetFrameTime() - m_lastSysHeatInfoTime >= SYSTEM_HEAT_UPDATE_INTERVAL)
+ {
+ m_lastSysHeatInfoTime = CTimeUtils::GetFrameTime();
+#if defined(TARGET_POSIX)
+ CServiceBroker::GetCPUInfo()->GetTemperature(m_cpuTemp);
+ m_gpuTemp = GetGPUTemperature();
+#endif
+ }
+
+ std::string text;
+ switch(info)
+ {
+ case SYSTEM_CPU_TEMPERATURE:
+ return m_cpuTemp.IsValid() ? g_langInfo.GetTemperatureAsString(m_cpuTemp) : g_localizeStrings.Get(10005); // Not available
+ case SYSTEM_GPU_TEMPERATURE:
+ return m_gpuTemp.IsValid() ? g_langInfo.GetTemperatureAsString(m_gpuTemp) : g_localizeStrings.Get(10005);
+ case SYSTEM_FAN_SPEED:
+ text = StringUtils::Format("{}%", m_fanSpeed * 2);
+ break;
+ case SYSTEM_CPU_USAGE:
+ if (CServiceBroker::GetCPUInfo()->SupportsCPUUsage())
+#if defined(TARGET_DARWIN) || defined(TARGET_WINDOWS)
+ text = StringUtils::Format("{}%", CServiceBroker::GetCPUInfo()->GetUsedPercentage());
+#else
+ text = CServiceBroker::GetCPUInfo()->GetCoresUsageString();
+#endif
+ else
+ text = g_localizeStrings.Get(10005); // Not available
+ break;
+ }
+ return text;
+}
+
+CTemperature CSystemGUIInfo::GetGPUTemperature() const
+{
+ int value = 0;
+ char scale = 0;
+
+#if defined(TARGET_DARWIN_OSX)
+ value = SMCGetTemperature(SMC_KEY_GPU_TEMP);
+ return CTemperature::CreateFromCelsius(value);
+#elif defined(TARGET_WINDOWS_STORE)
+ return CTemperature::CreateFromCelsius(0);
+#else
+ std::string cmd = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_gpuTempCmd;
+ int ret = 0;
+ FILE* p = NULL;
+
+ if (cmd.empty() || !(p = popen(cmd.c_str(), "r")))
+ return CTemperature();
+
+ ret = fscanf(p, "%d %c", &value, &scale);
+ pclose(p);
+
+ if (ret != 2)
+ return CTemperature();
+#endif
+
+ if (scale == 'C' || scale == 'c')
+ return CTemperature::CreateFromCelsius(value);
+ if (scale == 'F' || scale == 'f')
+ return CTemperature::CreateFromFahrenheit(value);
+ return CTemperature();
+}
+
+void CSystemGUIInfo::UpdateFPS()
+{
+ m_frameCounter++;
+ unsigned int curTime = CTimeUtils::GetFrameTime();
+
+ float fTimeSpan = static_cast<float>(curTime - m_lastFPSTime);
+ if (fTimeSpan >= 1000.0f)
+ {
+ fTimeSpan /= 1000.0f;
+ m_fps = m_frameCounter / fTimeSpan;
+ m_lastFPSTime = curTime;
+ m_frameCounter = 0;
+ }
+}
+
+bool CSystemGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ return false;
+}
+
+bool CSystemGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SYSTEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case SYSTEM_TIME:
+ value = CDateTime::GetCurrentDateTime().GetAsLocalizedTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case SYSTEM_DATE:
+ if (info.GetData3().empty())
+ value = CDateTime::GetCurrentDateTime().GetAsLocalizedDate(true);
+ else
+ value = CDateTime::GetCurrentDateTime().GetAsLocalizedDate(info.GetData3());
+ return true;
+ case SYSTEM_FREE_SPACE:
+ case SYSTEM_USED_SPACE:
+ case SYSTEM_TOTAL_SPACE:
+ case SYSTEM_FREE_SPACE_PERCENT:
+ case SYSTEM_USED_SPACE_PERCENT:
+ value = g_sysinfo.GetHddSpaceInfo(info.m_info);
+ return true;
+ case SYSTEM_CPU_TEMPERATURE:
+ case SYSTEM_GPU_TEMPERATURE:
+ case SYSTEM_FAN_SPEED:
+ case SYSTEM_CPU_USAGE:
+ value = GetSystemHeatInfo(info.m_info);
+ return true;
+ case SYSTEM_VIDEO_ENCODER_INFO:
+ case NETWORK_MAC_ADDRESS:
+ case SYSTEM_OS_VERSION_INFO:
+ case SYSTEM_CPUFREQUENCY:
+ case SYSTEM_INTERNET_STATE:
+ case SYSTEM_UPTIME:
+ case SYSTEM_TOTALUPTIME:
+ case SYSTEM_BATTERY_LEVEL:
+ value = g_sysinfo.GetInfo(info.m_info);
+ return true;
+ case SYSTEM_PRIVACY_POLICY:
+ value = g_sysinfo.GetPrivacyPolicy();
+ return true;
+ case SYSTEM_SCREEN_RESOLUTION:
+ {
+ const auto winSystem = CServiceBroker::GetWinSystem();
+ if (winSystem)
+ {
+ const RESOLUTION_INFO& resInfo = winSystem->GetGfxContext().GetResInfo();
+
+ if (winSystem->IsFullScreen())
+ value = StringUtils::Format("{}x{} @ {:.2f} Hz - {}", resInfo.iScreenWidth,
+ resInfo.iScreenHeight, resInfo.fRefreshRate,
+ g_localizeStrings.Get(244));
+ else
+ value = StringUtils::Format("{}x{} - {}", resInfo.iScreenWidth, resInfo.iScreenHeight,
+ g_localizeStrings.Get(242));
+ }
+ else
+ {
+ value = "";
+ }
+ return true;
+ }
+ case SYSTEM_BUILD_VERSION_SHORT:
+ value = CSysInfo::GetVersionShort();
+ return true;
+ case SYSTEM_BUILD_VERSION:
+ value = CSysInfo::GetVersion();
+ return true;
+ case SYSTEM_BUILD_DATE:
+ value = CSysInfo::GetBuildDate();
+ return true;
+ case SYSTEM_BUILD_VERSION_CODE:
+ value = CSysInfo::GetVersionCode();
+ return true;
+ case SYSTEM_BUILD_VERSION_GIT:
+ value = CSysInfo::GetVersionGit();
+ return true;
+ case SYSTEM_FREE_MEMORY:
+ case SYSTEM_FREE_MEMORY_PERCENT:
+ case SYSTEM_USED_MEMORY:
+ case SYSTEM_USED_MEMORY_PERCENT:
+ case SYSTEM_TOTAL_MEMORY:
+ {
+ KODI::MEMORY::MemoryStatus stat;
+ KODI::MEMORY::GetMemoryStatus(&stat);
+ int iMemPercentFree = 100 - static_cast<int>(100.0f * (stat.totalPhys - stat.availPhys) / stat.totalPhys + 0.5f);
+ int iMemPercentUsed = 100 - iMemPercentFree;
+
+ if (info.m_info == SYSTEM_FREE_MEMORY)
+ value = StringUtils::Format("{}MB", static_cast<unsigned int>(stat.availPhys / MB));
+ else if (info.m_info == SYSTEM_FREE_MEMORY_PERCENT)
+ value = StringUtils::Format("{}%", iMemPercentFree);
+ else if (info.m_info == SYSTEM_USED_MEMORY)
+ value = StringUtils::Format(
+ "{}MB", static_cast<unsigned int>((stat.totalPhys - stat.availPhys) / MB));
+ else if (info.m_info == SYSTEM_USED_MEMORY_PERCENT)
+ value = StringUtils::Format("{}%", iMemPercentUsed);
+ else if (info.m_info == SYSTEM_TOTAL_MEMORY)
+ value = StringUtils::Format("{}MB", static_cast<unsigned int>(stat.totalPhys / MB));
+ return true;
+ }
+ case SYSTEM_SCREEN_MODE:
+ value = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo().strMode;
+ return true;
+ case SYSTEM_SCREEN_WIDTH:
+ value = StringUtils::Format(
+ "{}", CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo().iScreenWidth);
+ return true;
+ case SYSTEM_SCREEN_HEIGHT:
+ value = StringUtils::Format(
+ "{}", CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo().iScreenHeight);
+ return true;
+ case SYSTEM_FPS:
+ value = StringUtils::Format("{:02.2f}", m_fps);
+ return true;
+#ifdef HAS_DVD_DRIVE
+ case SYSTEM_DVD_LABEL:
+ value = CServiceBroker::GetMediaManager().GetDiskLabel();
+ return true;
+#endif
+ case SYSTEM_ALARM_POS:
+ if (g_alarmClock.GetRemaining("shutdowntimer") == 0.0)
+ value.clear();
+ else
+ {
+ double fTime = g_alarmClock.GetRemaining("shutdowntimer");
+ if (fTime > 60.0)
+ value = StringUtils::Format(g_localizeStrings.Get(13213),
+ g_alarmClock.GetRemaining("shutdowntimer") / 60.0);
+ else
+ value = StringUtils::Format(g_localizeStrings.Get(13214),
+ g_alarmClock.GetRemaining("shutdowntimer"));
+ }
+ return true;
+ case SYSTEM_PROFILENAME:
+ value = CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetCurrentProfile().getName();
+ return true;
+ case SYSTEM_PROFILECOUNT:
+ value = StringUtils::Format("{0}", CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetNumberOfProfiles());
+ return true;
+ case SYSTEM_PROFILEAUTOLOGIN:
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ int iProfileId = profileManager->GetAutoLoginProfileId();
+ if ((iProfileId < 0) || !profileManager->GetProfileName(iProfileId, value))
+ value = g_localizeStrings.Get(37014); // Last used profile
+ return true;
+ }
+ case SYSTEM_PROFILETHUMB:
+ {
+ const std::string& thumb = CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetCurrentProfile().getThumb();
+ value = thumb.empty() ? "DefaultUser.png" : thumb;
+ return true;
+ }
+ case SYSTEM_LANGUAGE:
+ value = g_langInfo.GetEnglishLanguageName();
+ return true;
+ case SYSTEM_TEMPERATURE_UNITS:
+ value = g_langInfo.GetTemperatureUnitString();
+ return true;
+ case SYSTEM_FRIENDLY_NAME:
+ value = CSysInfo::GetDeviceName();
+ return true;
+ case SYSTEM_STEREOSCOPIC_MODE:
+ {
+ int iStereoMode = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOSCREEN_STEREOSCOPICMODE);
+ value = std::to_string(iStereoMode);
+ return true;
+ }
+ case SYSTEM_GET_CORE_USAGE:
+ value = StringUtils::Format(
+ "{:4.2f}",
+ CServiceBroker::GetCPUInfo()->GetCoreInfo(std::stoi(info.GetData3())).m_usagePercent);
+ return true;
+ case SYSTEM_RENDER_VENDOR:
+ value = CServiceBroker::GetRenderSystem()->GetRenderVendor();
+ return true;
+ case SYSTEM_RENDER_RENDERER:
+ value = CServiceBroker::GetRenderSystem()->GetRenderRenderer();
+ return true;
+ case SYSTEM_RENDER_VERSION:
+ value = CServiceBroker::GetRenderSystem()->GetRenderVersionString();
+ return true;
+ case SYSTEM_ADDON_UPDATE_COUNT:
+ value = CServiceBroker::GetAddonMgr().GetLastAvailableUpdatesCountAsString();
+ return true;
+#if defined(TARGET_LINUX)
+ case SYSTEM_PLATFORM_WINDOWING:
+ value = CServiceBroker::GetWinSystem()->GetName();
+ StringUtils::ToCapitalize(value);
+ return true;
+#endif
+ case SYSTEM_SUPPORTED_HDR_TYPES:
+ {
+ if (CServiceBroker::GetWinSystem()->IsHDRDisplay())
+ {
+ // Assumes HDR10 minimum requirement for HDR
+ std::string types = "HDR10";
+
+ const CHDRCapabilities caps = CServiceBroker::GetWinSystem()->GetDisplayHDRCapabilities();
+
+ if (caps.SupportsHLG())
+ types += ", HLG";
+ if (caps.SupportsHDR10Plus())
+ types += ", HDR10+";
+ if (caps.SupportsDolbyVision())
+ types += ", Dolby Vision";
+
+ value = types;
+ }
+
+ return true;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // NETWORK_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case NETWORK_IP_ADDRESS:
+ {
+ CNetworkInterface* iface = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+ if (iface)
+ {
+ value = iface->GetCurrentIPAddress();
+ return true;
+ }
+ break;
+ }
+ case NETWORK_SUBNET_MASK:
+ {
+ CNetworkInterface* iface = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+ if (iface)
+ {
+ value = iface->GetCurrentNetmask();
+ return true;
+ }
+ break;
+ }
+ case NETWORK_GATEWAY_ADDRESS:
+ {
+ CNetworkInterface* iface = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+ if (iface)
+ {
+ value = iface->GetCurrentDefaultGateway();
+ return true;
+ }
+ break;
+ }
+ case NETWORK_DNS1_ADDRESS:
+ {
+ const std::vector<std::string> nss = CServiceBroker::GetNetwork().GetNameServers();
+ if (nss.size() >= 1)
+ {
+ value = nss[0];
+ return true;
+ }
+ break;
+ }
+ case NETWORK_DNS2_ADDRESS:
+ {
+ const std::vector<std::string> nss = CServiceBroker::GetNetwork().GetNameServers();
+ if (nss.size() >= 2)
+ {
+ value = nss[1];
+ return true;
+ }
+ break;
+ }
+ case NETWORK_DHCP_ADDRESS:
+ {
+ // wtf?
+ std::string dhcpserver;
+ value = dhcpserver;
+ return true;
+ }
+ case NETWORK_LINK_STATE:
+ {
+ std::string linkStatus = g_localizeStrings.Get(151);
+ linkStatus += " ";
+ CNetworkInterface* iface = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+ if (iface && iface->IsConnected())
+ linkStatus += g_localizeStrings.Get(15207);
+ else
+ linkStatus += g_localizeStrings.Get(15208);
+ value = linkStatus;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CSystemGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SYSTEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case SYSTEM_FREE_MEMORY:
+ case SYSTEM_USED_MEMORY:
+ {
+ KODI::MEMORY::MemoryStatus stat;
+ KODI::MEMORY::GetMemoryStatus(&stat);
+ int memPercentUsed = static_cast<int>(100.0f * (stat.totalPhys - stat.availPhys) / stat.totalPhys + 0.5f);
+ if (info.m_info == SYSTEM_FREE_MEMORY)
+ value = 100 - memPercentUsed;
+ else
+ value = memPercentUsed;
+ return true;
+ }
+ case SYSTEM_FREE_SPACE:
+ case SYSTEM_USED_SPACE:
+ {
+ g_sysinfo.GetHddSpaceInfo(value, info.m_info, true);
+ return true;
+ }
+ case SYSTEM_CPU_USAGE:
+ value = CServiceBroker::GetCPUInfo()->GetUsedPercentage();
+ return true;
+ case SYSTEM_BATTERY_LEVEL:
+ value = CServiceBroker::GetPowerManager().BatteryLevel();
+ return true;
+ }
+
+ return false;
+}
+
+bool CSystemGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // SYSTEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case SYSTEM_ALWAYS_TRUE:
+ value = true;
+ return true;
+ case SYSTEM_ALWAYS_FALSE:
+ value = false;
+ return true;
+ case SYSTEM_ETHERNET_LINK_ACTIVE:
+ // wtf: not implemented - always returns true?!
+ value = true;
+ return true;
+ case SYSTEM_PLATFORM_LINUX:
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
+ value = true;
+#else
+ value = false;
+#endif
+ return true;
+ case SYSTEM_PLATFORM_WINDOWS:
+#ifdef TARGET_WINDOWS
+ value = true;
+#else
+ value = false;
+#endif
+ return true;
+ case SYSTEM_PLATFORM_UWP:
+#ifdef TARGET_WINDOWS_STORE
+ value = true;
+#else
+ value = false;
+#endif
+ return true;
+ case SYSTEM_PLATFORM_DARWIN:
+#ifdef TARGET_DARWIN
+ value = true;
+#else
+ value = false;
+#endif
+ return true;
+ case SYSTEM_PLATFORM_DARWIN_OSX:
+#ifdef TARGET_DARWIN_OSX
+ value = true;
+#else
+ value = false;
+#endif
+ return true;
+ case SYSTEM_PLATFORM_DARWIN_IOS:
+#ifdef TARGET_DARWIN_IOS
+ value = true;
+#else
+ 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;
+#else
+ value = false;
+#endif
+ return true;
+ case SYSTEM_MEDIA_DVD:
+ value = CServiceBroker::GetMediaManager().IsDiscInDrive();
+ return true;
+ case SYSTEM_MEDIA_AUDIO_CD:
+ #ifdef HAS_DVD_DRIVE
+ if (CServiceBroker::GetMediaManager().IsDiscInDrive())
+ {
+ MEDIA_DETECT::CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
+ value = pCdInfo && (pCdInfo->IsAudio(1) || pCdInfo->IsCDExtra(1) || pCdInfo->IsMixedMode(1));
+ }
+ else
+ #endif
+ {
+ value = false;
+ }
+ return true;
+#ifdef HAS_DVD_DRIVE
+ case SYSTEM_DVDREADY:
+ value = CServiceBroker::GetMediaManager().GetDriveStatus() != DriveState::NOT_READY;
+ return true;
+ case SYSTEM_TRAYOPEN:
+ value = CServiceBroker::GetMediaManager().GetDriveStatus() == DriveState::OPEN;
+ return true;
+#endif
+ case SYSTEM_CAN_POWERDOWN:
+ value = CServiceBroker::GetPowerManager().CanPowerdown();
+ return true;
+ case SYSTEM_CAN_SUSPEND:
+ value = CServiceBroker::GetPowerManager().CanSuspend();
+ return true;
+ case SYSTEM_CAN_HIBERNATE:
+ value = CServiceBroker::GetPowerManager().CanHibernate();
+ return true;
+ case SYSTEM_CAN_REBOOT:
+ value = CServiceBroker::GetPowerManager().CanReboot();
+ return true;
+ case SYSTEM_SCREENSAVER_ACTIVE:
+ case SYSTEM_IS_SCREENSAVER_INHIBITED:
+ case SYSTEM_DPMS_ACTIVE:
+ case SYSTEM_IDLE_SHUTDOWN_INHIBITED:
+ case SYSTEM_IDLE_TIME:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ switch (info.m_info)
+ {
+ case SYSTEM_SCREENSAVER_ACTIVE:
+ value = appPower->IsInScreenSaver();
+ return true;
+ case SYSTEM_IS_SCREENSAVER_INHIBITED:
+ value = appPower->IsScreenSaverInhibited();
+ return true;
+ case SYSTEM_DPMS_ACTIVE:
+ value = appPower->IsDPMSActive();
+ return true;
+ case SYSTEM_IDLE_SHUTDOWN_INHIBITED:
+ value = appPower->IsIdleShutdownInhibited();
+ return true;
+ case SYSTEM_IDLE_TIME:
+ value = appPower->GlobalIdleTime() >= static_cast<int>(info.GetData1());
+ return true;
+ default:
+ return false;
+ }
+ }
+ case SYSTEM_HASLOCKS:
+ value = CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE;
+ return true;
+ case SYSTEM_HAS_PVR:
+ value = true;
+ return true;
+ case SYSTEM_HAS_PVR_ADDON:
+ value = CServiceBroker::GetAddonMgr().HasAddons(ADDON::AddonType::PVRDLL);
+ return true;
+ case SYSTEM_HAS_CMS:
+#if defined(HAS_GL) || defined(HAS_DX)
+ value = true;
+#else
+ value = false;
+#endif
+ return true;
+ case SYSTEM_ISMASTER:
+ value = CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && g_passwordManager.bMasterUser;
+ return true;
+ case SYSTEM_ISFULLSCREEN:
+ value = CServiceBroker::GetWinSystem()->IsFullScreen();
+ return true;
+ case SYSTEM_ISSTANDALONE:
+ value = CServiceBroker::GetAppParams()->IsStandAlone();
+ return true;
+ case SYSTEM_HAS_SHUTDOWN:
+ value = (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNTIME) > 0);
+ return true;
+ case SYSTEM_LOGGEDON:
+ value = !(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_LOGIN_SCREEN);
+ return true;
+ case SYSTEM_SHOW_EXIT_BUTTON:
+ value = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_showExitButton;
+ return true;
+ case SYSTEM_HAS_LOGINSCREEN:
+ value = CServiceBroker::GetSettingsComponent()->GetProfileManager()->UsingLoginScreen();
+ return true;
+ case SYSTEM_INTERNET_STATE:
+ {
+ g_sysinfo.GetInfo(info.m_info);
+ value = g_sysinfo.HasInternet();
+ return true;
+ }
+ case SYSTEM_HAS_CORE_ID:
+ value = CServiceBroker::GetCPUInfo()->HasCoreId(info.GetData1());
+ return true;
+ case SYSTEM_DATE:
+ {
+ if (info.GetData2() == -1) // info doesn't contain valid startDate
+ return false;
+ const CDateTime date = CDateTime::GetCurrentDateTime();
+ int currentDate = date.GetMonth() * 100 + date.GetDay();
+ int startDate = info.GetData1();
+ int stopDate = info.GetData2();
+
+ if (stopDate < startDate)
+ value = currentDate >= startDate || currentDate < stopDate;
+ else
+ value = currentDate >= startDate && currentDate < stopDate;
+ return true;
+ }
+ case SYSTEM_TIME:
+ {
+ int currentTime = CDateTime::GetCurrentDateTime().GetMinuteOfDay();
+ int startTime = info.GetData1();
+ int stopTime = info.GetData2();
+
+ if (stopTime < startTime)
+ value = currentTime >= startTime || currentTime < stopTime;
+ else
+ value = currentTime >= startTime && currentTime < stopTime;
+ return true;
+ }
+ case SYSTEM_ALARM_LESS_OR_EQUAL:
+ {
+ int time = std::lrint(g_alarmClock.GetRemaining(info.GetData3()));
+ int timeCompare = info.GetData2();
+ if (time > 0)
+ value = timeCompare >= time;
+ else
+ value = false;
+ return true;
+ }
+ case SYSTEM_HAS_ALARM:
+ value = g_alarmClock.HasAlarm(info.GetData3());
+ return true;
+ case SYSTEM_SUPPORTS_CPU_USAGE:
+ value = CServiceBroker::GetCPUInfo()->SupportsCPUUsage();
+ return true;
+ case SYSTEM_GET_BOOL:
+ value = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(info.GetData3());
+ return true;
+ case SYSTEM_SETTING:
+ {
+ if (StringUtils::EqualsNoCase(info.GetData3(), "hidewatched"))
+ {
+ CGUIMediaWindow* window = GUIINFO::GetMediaWindow(contextWindow);
+ if (window)
+ {
+ value = CMediaSettings::GetInstance().GetWatchedMode(window->CurrentDirectory().GetContent()) == WatchedModeUnwatched;
+ return true;
+ }
+ }
+ else if (StringUtils::EqualsNoCase(info.GetData3(), "hideunwatchedepisodethumbs"))
+ {
+ const std::shared_ptr<CSettingList> setting(std::dynamic_pointer_cast<CSettingList>(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(
+ CSettings::SETTING_VIDEOLIBRARY_SHOWUNWATCHEDPLOTS)));
+ value = setting && !CSettingUtils::FindIntInList(
+ setting, CSettings::VIDEOLIBRARY_THUMB_SHOW_UNWATCHED_EPISODE);
+ return true;
+ }
+ break;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/guilib/guiinfo/SystemGUIInfo.h b/xbmc/guilib/guiinfo/SystemGUIInfo.h
new file mode 100644
index 0000000..ed2ea03
--- /dev/null
+++ b/xbmc/guilib/guiinfo/SystemGUIInfo.h
@@ -0,0 +1,55 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProvider.h"
+#include "utils/Temperature.h"
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CSystemGUIInfo : public CGUIInfoProvider
+{
+public:
+ CSystemGUIInfo();
+ ~CSystemGUIInfo() override = default;
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+
+ float GetFPS() const { return m_fps; }
+ void UpdateFPS();
+
+private:
+ std::string GetSystemHeatInfo(int info) const;
+ CTemperature GetGPUTemperature() const;
+
+ static const int SYSTEM_HEAT_UPDATE_INTERVAL = 60000;
+
+ mutable unsigned int m_lastSysHeatInfoTime;
+ mutable CTemperature m_gpuTemp;
+ mutable CTemperature m_cpuTemp;
+ int m_fanSpeed = 0;
+ float m_fps = 0.0;
+ unsigned int m_frameCounter = 0;
+ unsigned int m_lastFPSTime = 0;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/VideoGUIInfo.cpp b/xbmc/guilib/guiinfo/VideoGUIInfo.cpp
new file mode 100644
index 0000000..794434f
--- /dev/null
+++ b/xbmc/guilib/guiinfo/VideoGUIInfo.cpp
@@ -0,0 +1,818 @@
+/*
+ * 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 "guilib/guiinfo/VideoGUIInfo.h"
+
+#include "FileItem.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/DataCacheCore.h"
+#include "cores/VideoPlayer/VideoRenderers/BaseRenderer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/StereoscopicsManager.h"
+#include "guilib/WindowIDs.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoHelper.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "playlists/PlayList.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingUtils.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+#include "video/VideoThumbLoader.h"
+
+#include <math.h>
+
+using namespace KODI::GUILIB;
+using namespace KODI::GUILIB::GUIINFO;
+
+CVideoGUIInfo::CVideoGUIInfo()
+ : m_appPlayer(CServiceBroker::GetAppComponents().GetComponent<CApplicationPlayer>())
+{
+}
+
+int CVideoGUIInfo::GetPercentPlayed(const CVideoInfoTag* tag) const
+{
+ CBookmark bookmark = tag->GetResumePoint();
+ if (bookmark.IsPartWay())
+ return std::lrintf(static_cast<float>(bookmark.timeInSeconds) /
+ static_cast<float>(bookmark.totalTimeInSeconds) * 100.0f);
+ else
+ return 0;
+}
+
+bool CVideoGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ if (item && item->IsVideo())
+ {
+ // special case where .strm is used to start an audio stream
+ if (item->IsInternetStream() && m_appPlayer->IsPlayingAudio())
+ return false;
+
+ CLog::Log(LOGDEBUG, "CVideoGUIInfo::InitCurrentItem({})", CURL::GetRedacted(item->GetPath()));
+
+ // Find a thumb for this file.
+ if (!item->HasArt("thumb"))
+ {
+ CVideoThumbLoader loader;
+ loader.LoadItem(item);
+ }
+
+ // find a thumb for this stream
+ if (item->IsInternetStream())
+ {
+ if (!g_application.m_strPlayListFile.empty())
+ {
+ CLog::Log(LOGDEBUG, "Streaming media detected... using {} to find a thumb",
+ g_application.m_strPlayListFile);
+ CFileItem thumbItem(g_application.m_strPlayListFile,false);
+
+ CVideoThumbLoader loader;
+ if (loader.FillThumb(thumbItem))
+ item->SetArt("thumb", thumbItem.GetArt("thumb"));
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+bool CVideoGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ // For videoplayer "offset" and "position" info labels check playlist
+ if (info.GetData1() && ((info.m_info >= VIDEOPLAYER_OFFSET_POSITION_FIRST &&
+ info.m_info <= VIDEOPLAYER_OFFSET_POSITION_LAST) ||
+ (info.m_info >= PLAYER_OFFSET_POSITION_FIRST && info.m_info <= PLAYER_OFFSET_POSITION_LAST)))
+ return GetPlaylistInfo(value, info);
+
+ const CVideoInfoTag* tag = item->GetVideoInfoTag();
+ if (tag)
+ {
+ switch (info.m_info)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // PLAYER_* / VIDEOPLAYER_* / LISTITEM_*
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ case VIDEOPLAYER_ART:
+ value = item->GetArt(info.GetData3());
+ return true;
+ case PLAYER_PATH:
+ case PLAYER_FILENAME:
+ case PLAYER_FILEPATH:
+ if (item->HasMusicInfoTag()) // special handling for music videos, which have both a videotag and a musictag
+ break;
+
+ value = tag->m_strFileNameAndPath;
+ if (value.empty())
+ value = item->GetPath();
+ value = GUIINFO::GetFileInfoLabelValueFromPath(info.m_info, value);
+ return true;
+ case PLAYER_TITLE:
+ value = tag->m_strTitle;
+ return !value.empty();
+ case VIDEOPLAYER_TITLE:
+ value = tag->m_strTitle;
+ return !value.empty();
+ case LISTITEM_TITLE:
+ value = tag->m_strTitle;
+ return true;
+ case VIDEOPLAYER_ORIGINALTITLE:
+ case LISTITEM_ORIGINALTITLE:
+ value = tag->m_strOriginalTitle;
+ return true;
+ case VIDEOPLAYER_GENRE:
+ case LISTITEM_GENRE:
+ value = StringUtils::Join(tag->m_genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ return true;
+ case VIDEOPLAYER_DIRECTOR:
+ case LISTITEM_DIRECTOR:
+ value = StringUtils::Join(tag->m_director, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ return true;
+ case VIDEOPLAYER_IMDBNUMBER:
+ case LISTITEM_IMDBNUMBER:
+ value = tag->GetUniqueID();
+ return true;
+ case VIDEOPLAYER_DBID:
+ case LISTITEM_DBID:
+ if (tag->m_iDbId > -1)
+ {
+ value = std::to_string(tag->m_iDbId);
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_TVSHOWDBID:
+ case LISTITEM_TVSHOWDBID:
+ if (tag->m_iIdShow > -1)
+ {
+ value = std::to_string(tag->m_iIdShow);
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_UNIQUEID:
+ case LISTITEM_UNIQUEID:
+ if (!info.GetData3().empty())
+ value = tag->GetUniqueID(info.GetData3());
+ return true;
+ case VIDEOPLAYER_RATING:
+ case LISTITEM_RATING:
+ {
+ float rating = tag->GetRating(info.GetData3()).rating;
+ if (rating > 0.f)
+ {
+ value = StringUtils::FormatNumber(rating);
+ return true;
+ }
+ break;
+ }
+ case VIDEOPLAYER_RATING_AND_VOTES:
+ case LISTITEM_RATING_AND_VOTES:
+ {
+ CRating rating = tag->GetRating(info.GetData3());
+ if (rating.rating >= 0.f)
+ {
+ if (rating.rating > 0.f && rating.votes == 0)
+ value = StringUtils::FormatNumber(rating.rating);
+ else if (rating.votes > 0)
+ value = StringUtils::Format(g_localizeStrings.Get(20350),
+ StringUtils::FormatNumber(rating.rating),
+ StringUtils::FormatNumber(rating.votes));
+ else
+ break;
+ return true;
+ }
+ break;
+ }
+ case VIDEOPLAYER_USER_RATING:
+ case LISTITEM_USER_RATING:
+ if (tag->m_iUserRating > 0)
+ {
+ value = std::to_string(tag->m_iUserRating);
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_VOTES:
+ case LISTITEM_VOTES:
+ value = StringUtils::FormatNumber(tag->GetRating(info.GetData3()).votes);
+ return true;
+ case VIDEOPLAYER_YEAR:
+ case LISTITEM_YEAR:
+ if (tag->HasYear())
+ {
+ value = std::to_string(tag->GetYear());
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_PREMIERED:
+ case LISTITEM_PREMIERED:
+ {
+ CDateTime dateTime;
+ if (tag->m_firstAired.IsValid())
+ dateTime = tag->m_firstAired;
+ else if (tag->HasPremiered())
+ dateTime = tag->GetPremiered();
+
+ if (dateTime.IsValid())
+ {
+ value = dateTime.GetAsLocalizedDate();
+ return true;
+ }
+ break;
+ }
+ case VIDEOPLAYER_PLOT:
+ value = tag->m_strPlot;
+ return true;
+ case VIDEOPLAYER_TRAILER:
+ case LISTITEM_TRAILER:
+ value = tag->m_strTrailer;
+ return true;
+ case VIDEOPLAYER_PLOT_OUTLINE:
+ case LISTITEM_PLOT_OUTLINE:
+ value = tag->m_strPlotOutline;
+ return true;
+ case VIDEOPLAYER_EPISODE:
+ case LISTITEM_EPISODE:
+ {
+ int iEpisode = -1;
+ if (tag->m_iEpisode > 0)
+ {
+ iEpisode = tag->m_iEpisode;
+ }
+
+ if (iEpisode >= 0)
+ {
+ value = std::to_string(iEpisode);
+ return true;
+ }
+ break;
+ }
+ case VIDEOPLAYER_SEASON:
+ case LISTITEM_SEASON:
+ if (tag->m_iSeason >= 0)
+ {
+ value = std::to_string(tag->m_iSeason);
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_TVSHOW:
+ case LISTITEM_TVSHOW:
+ value = tag->m_strShowTitle;
+ return true;
+ case VIDEOPLAYER_STUDIO:
+ case LISTITEM_STUDIO:
+ value = StringUtils::Join(tag->m_studio, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ return true;
+ case VIDEOPLAYER_COUNTRY:
+ case LISTITEM_COUNTRY:
+ value = StringUtils::Join(tag->m_country, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ return true;
+ case VIDEOPLAYER_MPAA:
+ case LISTITEM_MPAA:
+ value = tag->m_strMPAARating;
+ return true;
+ case VIDEOPLAYER_TOP250:
+ case LISTITEM_TOP250:
+ if (tag->m_iTop250 > 0)
+ {
+ value = std::to_string(tag->m_iTop250);
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_CAST:
+ case LISTITEM_CAST:
+ value = tag->GetCast();
+ return true;
+ case VIDEOPLAYER_CAST_AND_ROLE:
+ case LISTITEM_CAST_AND_ROLE:
+ value = tag->GetCast(true);
+ return true;
+ case VIDEOPLAYER_ARTIST:
+ case LISTITEM_ARTIST:
+ value = StringUtils::Join(tag->m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ return true;
+ case VIDEOPLAYER_ALBUM:
+ case LISTITEM_ALBUM:
+ value = tag->m_strAlbum;
+ return true;
+ case VIDEOPLAYER_WRITER:
+ case LISTITEM_WRITER:
+ value = StringUtils::Join(tag->m_writingCredits, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ return true;
+ case VIDEOPLAYER_TAGLINE:
+ case LISTITEM_TAGLINE:
+ value = tag->m_strTagLine;
+ return true;
+ case VIDEOPLAYER_LASTPLAYED:
+ case LISTITEM_LASTPLAYED:
+ {
+ const CDateTime dateTime = tag->m_lastPlayed;
+ if (dateTime.IsValid())
+ {
+ value = dateTime.GetAsLocalizedDate();
+ return true;
+ }
+ break;
+ }
+ case VIDEOPLAYER_PLAYCOUNT:
+ case LISTITEM_PLAYCOUNT:
+ if (tag->GetPlayCount() > 0)
+ {
+ value = std::to_string(tag->GetPlayCount());
+ return true;
+ }
+ break;
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // LISTITEM_*
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ case LISTITEM_DURATION:
+ {
+ int iDuration = tag->GetDuration();
+ if (iDuration > 0)
+ {
+ value = StringUtils::SecondsToTimeString(iDuration, static_cast<TIME_FORMAT>(info.GetData4()));
+ return true;
+ }
+ break;
+ }
+ case LISTITEM_TRACKNUMBER:
+ if (tag->m_iTrack > -1 )
+ {
+ value = std::to_string(tag->m_iTrack);
+ return true;
+ }
+ break;
+ case LISTITEM_PLOT:
+ {
+ std::shared_ptr<CSettingList> setting(std::dynamic_pointer_cast<CSettingList>(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(
+ CSettings::SETTING_VIDEOLIBRARY_SHOWUNWATCHEDPLOTS)));
+ if (tag->m_type != MediaTypeTvShow && tag->m_type != MediaTypeVideoCollection &&
+ tag->GetPlayCount() == 0 && setting &&
+ ((tag->m_type == MediaTypeMovie &&
+ !CSettingUtils::FindIntInList(
+ setting, CSettings::VIDEOLIBRARY_PLOTS_SHOW_UNWATCHED_MOVIES)) ||
+ (tag->m_type == MediaTypeEpisode &&
+ !CSettingUtils::FindIntInList(
+ setting, CSettings::VIDEOLIBRARY_PLOTS_SHOW_UNWATCHED_TVSHOWEPISODES))))
+ {
+ value = g_localizeStrings.Get(20370);
+ }
+ else
+ {
+ value = tag->m_strPlot;
+ }
+ return true;
+ }
+ case LISTITEM_STATUS:
+ value = tag->m_strStatus;
+ return true;
+ case LISTITEM_TAG:
+ value = StringUtils::Join(tag->m_tags, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ return true;
+ case LISTITEM_SET:
+ value = tag->m_set.title;
+ return true;
+ case LISTITEM_SETID:
+ if (tag->m_set.id > 0)
+ {
+ value = std::to_string(tag->m_set.id);
+ return true;
+ }
+ break;
+ case LISTITEM_ENDTIME_RESUME:
+ {
+ const CDateTimeSpan duration(0, 0, 0, tag->GetDuration() - tag->GetResumePoint().timeInSeconds);
+ value = (CDateTime::GetCurrentDateTime() + duration).GetAsLocalizedTime("", false);
+ return true;
+ }
+ case LISTITEM_ENDTIME:
+ {
+ const CDateTimeSpan duration(0, 0, 0, tag->GetDuration());
+ value = (CDateTime::GetCurrentDateTime() + duration).GetAsLocalizedTime("", false);
+ return true;
+ }
+ case LISTITEM_DATE_ADDED:
+ if (tag->m_dateAdded.IsValid())
+ {
+ value = tag->m_dateAdded.GetAsLocalizedDate();
+ return true;
+ }
+ break;
+ case LISTITEM_DBTYPE:
+ value = tag->m_type;
+ return true;
+ case LISTITEM_APPEARANCES:
+ if (tag->m_relevance > -1)
+ {
+ value = std::to_string(tag->m_relevance);
+ return true;
+ }
+ break;
+ case LISTITEM_PERCENT_PLAYED:
+ value = std::to_string(GetPercentPlayed(tag));
+ return true;
+ case LISTITEM_VIDEO_CODEC:
+ value = tag->m_streamDetails.GetVideoCodec();
+ return true;
+ case LISTITEM_VIDEO_RESOLUTION:
+ value = CStreamDetails::VideoDimsToResolutionDescription(tag->m_streamDetails.GetVideoWidth(), tag->m_streamDetails.GetVideoHeight());
+ return true;
+ case LISTITEM_VIDEO_ASPECT:
+ value = CStreamDetails::VideoAspectToAspectDescription(tag->m_streamDetails.GetVideoAspect());
+ return true;
+ case LISTITEM_AUDIO_CODEC:
+ value = tag->m_streamDetails.GetAudioCodec();
+ return true;
+ case LISTITEM_AUDIO_CHANNELS:
+ {
+ int iChannels = tag->m_streamDetails.GetAudioChannels();
+ if (iChannels > 0)
+ {
+ value = std::to_string(iChannels);
+ return true;
+ }
+ break;
+ }
+ case LISTITEM_AUDIO_LANGUAGE:
+ value = tag->m_streamDetails.GetAudioLanguage();
+ return true;
+ case LISTITEM_SUBTITLE_LANGUAGE:
+ value = tag->m_streamDetails.GetSubtitleLanguage();
+ return true;
+ case LISTITEM_FILENAME:
+ case LISTITEM_FILE_EXTENSION:
+ if (item->IsVideoDb())
+ value = URIUtils::GetFileName(tag->m_strFileNameAndPath);
+ else if (item->HasMusicInfoTag()) // special handling for music videos, which have both a videotag and a musictag
+ break;
+ else
+ value = URIUtils::GetFileName(item->GetPath());
+
+ if (info.m_info == LISTITEM_FILE_EXTENSION)
+ {
+ std::string strExtension = URIUtils::GetExtension(value);
+ value = StringUtils::TrimLeft(strExtension, ".");
+ }
+ return true;
+ case LISTITEM_FOLDERNAME:
+ case LISTITEM_PATH:
+ if (item->IsVideoDb())
+ {
+ if (item->m_bIsFolder)
+ value = tag->m_strPath;
+ else
+ URIUtils::GetParentPath(tag->m_strFileNameAndPath, value);
+ }
+ else if (item->HasMusicInfoTag()) // special handling for music videos, which have both a videotag and a musictag
+ break;
+ else
+ URIUtils::GetParentPath(item->GetPath(), value);
+
+ value = CURL(value).GetWithoutUserDetails();
+
+ if (info.m_info == LISTITEM_FOLDERNAME)
+ {
+ URIUtils::RemoveSlashAtEnd(value);
+ value = URIUtils::GetFileName(value);
+ }
+ return true;
+ case LISTITEM_FILENAME_AND_PATH:
+ if (item->IsVideoDb())
+ value = tag->m_strFileNameAndPath;
+ else if (item->HasMusicInfoTag()) // special handling for music videos, which have both a videotag and a musictag
+ break;
+ else
+ value = item->GetPath();
+
+ value = CURL(value).GetWithoutUserDetails();
+ return true;
+ case LISTITEM_VIDEO_HDR_TYPE:
+ value = tag->m_streamDetails.GetVideoHdrType();
+ return true;
+ }
+ }
+
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // VIDEOPLAYER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case VIDEOPLAYER_PLAYLISTLEN:
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO)
+ {
+ value = GUIINFO::GetPlaylistLabel(PLAYLIST_LENGTH);
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_PLAYLISTPOS:
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO)
+ {
+ value = GUIINFO::GetPlaylistLabel(PLAYLIST_POSITION);
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_VIDEO_ASPECT:
+ value = CStreamDetails::VideoAspectToAspectDescription(CServiceBroker::GetDataCacheCore().GetVideoDAR());
+ return true;
+ case VIDEOPLAYER_STEREOSCOPIC_MODE:
+ value = CServiceBroker::GetDataCacheCore().GetVideoStereoMode();
+ return true;
+ case VIDEOPLAYER_SUBTITLES_LANG:
+ value = m_subtitleInfo.language;
+ return true;
+ break;
+ case VIDEOPLAYER_COVER:
+ if (m_appPlayer->IsPlayingVideo())
+ {
+ if (fallback)
+ *fallback = "DefaultVideoCover.png";
+
+ value = item->HasArt("thumb") ? item->GetArt("thumb") : "DefaultVideoCover.png";
+ return true;
+ }
+ break;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // LISTITEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case LISTITEM_STEREOSCOPIC_MODE:
+ value = item->GetProperty("stereomode").asString();
+ if (value.empty() && tag)
+ value = CStereoscopicsManager::NormalizeStereoMode(tag->m_streamDetails.GetStereoMode());
+ return true;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // VIDEOPLAYER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case VIDEOPLAYER_VIDEO_CODEC:
+ value = m_videoInfo.codecName;
+ return true;
+ case VIDEOPLAYER_VIDEO_RESOLUTION:
+ value = CStreamDetails::VideoDimsToResolutionDescription(m_videoInfo.width, m_videoInfo.height);
+ return true;
+ case VIDEOPLAYER_HDR_TYPE:
+ value = CStreamDetails::HdrTypeToString(m_videoInfo.hdrType);
+ return true;
+ case VIDEOPLAYER_AUDIO_CODEC:
+ value = m_audioInfo.codecName;
+ return true;
+ case VIDEOPLAYER_AUDIO_CHANNELS:
+ {
+ int iChannels = m_audioInfo.channels;
+ if (iChannels > 0)
+ {
+ value = std::to_string(iChannels);
+ return true;
+ }
+ break;
+ }
+ case VIDEOPLAYER_AUDIO_BITRATE:
+ {
+ int iBitrate = m_audioInfo.bitrate;
+ if (iBitrate > 0)
+ {
+ value = std::to_string(std::lrint(static_cast<double>(iBitrate) / 1000.0));
+ return true;
+ }
+ break;
+ }
+ case VIDEOPLAYER_VIDEO_BITRATE:
+ {
+ int iBitrate = m_videoInfo.bitrate;
+ if (iBitrate > 0)
+ {
+ value = std::to_string(std::lrint(static_cast<double>(iBitrate) / 1000.0));
+ return true;
+ }
+ break;
+ }
+ case VIDEOPLAYER_AUDIO_LANG:
+ value = m_audioInfo.language;
+ return true;
+ }
+
+ return false;
+}
+
+bool CVideoGUIInfo::GetPlaylistInfo(std::string& value, const CGUIInfo& info) const
+{
+ const PLAYLIST::CPlayList& playlist =
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_VIDEO);
+ if (playlist.size() < 1)
+ return false;
+
+ int index = info.GetData2();
+ if (info.GetData1() == 1)
+ { // relative index (requires current playlist is TYPE_VIDEO)
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() != PLAYLIST::TYPE_VIDEO)
+ return false;
+
+ index = CServiceBroker::GetPlaylistPlayer().GetNextSong(index);
+ }
+
+ if (index < 0 || index >= playlist.size())
+ return false;
+
+ const CFileItemPtr playlistItem = playlist[index];
+ // try to set a thumbnail
+ if (!playlistItem->HasArt("thumb"))
+ {
+ CVideoThumbLoader loader;
+ loader.LoadItem(playlistItem.get());
+ // still no thumb? then just the set the default cover
+ if (!playlistItem->HasArt("thumb"))
+ playlistItem->SetArt("thumb", "DefaultVideoCover.png");
+ }
+ if (info.m_info == VIDEOPLAYER_PLAYLISTPOS)
+ {
+ value = std::to_string(index + 1);
+ return true;
+ }
+ else if (info.m_info == VIDEOPLAYER_COVER)
+ {
+ value = playlistItem->GetArt("thumb");
+ return true;
+ }
+ else if (info.m_info == VIDEOPLAYER_ART)
+ {
+ value = playlistItem->GetArt(info.GetData3());
+ return true;
+ }
+
+ return GetLabel(value, playlistItem.get(), 0, CGUIInfo(info.m_info), nullptr);
+}
+
+bool CVideoGUIInfo::GetFallbackLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const CGUIInfo& info,
+ std::string* fallback)
+{
+ // No fallback for videoplayer "offset" and "position" info labels
+ if (info.GetData1() && ((info.m_info >= VIDEOPLAYER_OFFSET_POSITION_FIRST &&
+ info.m_info <= VIDEOPLAYER_OFFSET_POSITION_LAST) ||
+ (info.m_info >= PLAYER_OFFSET_POSITION_FIRST && info.m_info <= PLAYER_OFFSET_POSITION_LAST)))
+ return false;
+
+ const CVideoInfoTag* tag = item->GetVideoInfoTag();
+ if (tag)
+ {
+ switch (info.m_info)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // VIDEOPLAYER_*
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ case VIDEOPLAYER_TITLE:
+ value = item->GetLabel();
+ if (value.empty())
+ value = CUtil::GetTitleFromPath(item->GetPath());
+ return true;
+ default:
+ break;
+ }
+ }
+ return false;
+}
+
+bool CVideoGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ if (!gitem->IsFileItem())
+ return false;
+
+ const CFileItem *item = static_cast<const CFileItem*>(gitem);
+ const CVideoInfoTag* tag = item->GetVideoInfoTag();
+ if (tag)
+ {
+ switch (info.m_info)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // LISTITEM_*
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ case LISTITEM_PERCENT_PLAYED:
+ value = GetPercentPlayed(tag);
+ return true;
+ }
+ }
+
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // VIDEOPLAYER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case VIDEOPLAYER_AUDIOSTREAMCOUNT:
+ value = m_appPlayer->GetAudioStreamCount();
+ return true;
+
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool CVideoGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ if (!gitem->IsFileItem())
+ return false;
+
+ const CFileItem *item = static_cast<const CFileItem*>(gitem);
+ const CVideoInfoTag* tag = item->GetVideoInfoTag();
+ if (tag)
+ {
+ switch (info.m_info)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // VIDEOPLAYER_*
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ case VIDEOPLAYER_HAS_INFO:
+ value = !tag->IsEmpty();
+ return true;
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // LISTITEM_*
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ case LISTITEM_IS_RESUMABLE:
+ value = tag->GetResumePoint().timeInSeconds > 0;
+ return true;
+ case LISTITEM_IS_COLLECTION:
+ value = tag->m_type == MediaTypeVideoCollection;
+ return true;
+ }
+ }
+
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // VIDEOPLAYER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case VIDEOPLAYER_CONTENT:
+ {
+ std::string strContent = "files";
+ if (tag)
+ {
+ if (tag->m_type == MediaTypeMovie)
+ strContent = "movies";
+ else if (tag->m_type == MediaTypeEpisode)
+ strContent = "episodes";
+ else if (tag->m_type == MediaTypeMusicVideo)
+ strContent = "musicvideos";
+ }
+ value = StringUtils::EqualsNoCase(info.GetData3(), strContent);
+ return value; // if no match for this provider, other providers shall be asked.
+ }
+ case VIDEOPLAYER_USING_OVERLAYS:
+ value = (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_RENDERMETHOD) == RENDER_OVERLAYS);
+ return true;
+ case VIDEOPLAYER_ISFULLSCREEN:
+ value = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_VIDEO ||
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_GAME;
+ return true;
+ case VIDEOPLAYER_HASMENU:
+ value = m_appPlayer->GetSupportedMenuType() != MenuType::NONE;
+ return true;
+ case VIDEOPLAYER_HASTELETEXT:
+ value = m_appPlayer->HasTeletextCache();
+ return true;
+ case VIDEOPLAYER_HASSUBTITLES:
+ value = m_appPlayer->GetSubtitleCount() > 0;
+ return true;
+ case VIDEOPLAYER_SUBTITLESENABLED:
+ value = m_appPlayer->GetSubtitleVisible();
+ return true;
+ case VIDEOPLAYER_IS_STEREOSCOPIC:
+ value = !CServiceBroker::GetDataCacheCore().GetVideoStereoMode().empty();
+ return true;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // LISTITEM_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case LISTITEM_IS_STEREOSCOPIC:
+ {
+ std::string stereoMode = item->GetProperty("stereomode").asString();
+ if (stereoMode.empty() && tag)
+ stereoMode = CStereoscopicsManager::NormalizeStereoMode(tag->m_streamDetails.GetStereoMode());
+ if (!stereoMode.empty() && stereoMode != "mono")
+ value = true;
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/guilib/guiinfo/VideoGUIInfo.h b/xbmc/guilib/guiinfo/VideoGUIInfo.h
new file mode 100644
index 0000000..55202b2
--- /dev/null
+++ b/xbmc/guilib/guiinfo/VideoGUIInfo.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProvider.h"
+
+#include <memory>
+
+class CApplicationPlayer;
+class CVideoInfoTag;
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CVideoGUIInfo : public CGUIInfoProvider
+{
+public:
+ CVideoGUIInfo();
+ ~CVideoGUIInfo() override = default;
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetFallbackLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const CGUIInfo& info,
+ std::string* fallback) override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+
+private:
+ int GetPercentPlayed(const CVideoInfoTag* tag) const;
+ bool GetPlaylistInfo(std::string& value, const CGUIInfo& info) const;
+
+ const std::shared_ptr<CApplicationPlayer> m_appPlayer;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/VisualisationGUIInfo.cpp b/xbmc/guilib/guiinfo/VisualisationGUIInfo.cpp
new file mode 100644
index 0000000..27a892f
--- /dev/null
+++ b/xbmc/guilib/guiinfo/VisualisationGUIInfo.cpp
@@ -0,0 +1,115 @@
+/*
+ * 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 "guilib/guiinfo/VisualisationGUIInfo.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIVisualisationControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+
+using namespace KODI::GUILIB::GUIINFO;
+
+bool CVisualisationGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ return false;
+}
+
+bool CVisualisationGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // VISUALISATION_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case VISUALISATION_PRESET:
+ {
+ CGUIMessage msg(GUI_MSG_GET_VISUALISATION, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ if (msg.GetPointer())
+ {
+ CGUIVisualisationControl* viz = static_cast<CGUIVisualisationControl*>(msg.GetPointer());
+ if (viz)
+ {
+ value = viz->GetActivePresetName();
+ URIUtils::RemoveExtension(value);
+ return true;
+ }
+ }
+ break;
+ }
+ case VISUALISATION_NAME:
+ {
+ ADDON::AddonPtr addon;
+ value = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICPLAYER_VISUALISATION);
+ if (CServiceBroker::GetAddonMgr().GetAddon(value, addon, ADDON::OnlyEnabled::CHOICE_YES) &&
+ addon)
+ {
+ value = addon->Name();
+ return true;
+ }
+ break;
+ }
+ }
+
+ return false;
+}
+
+bool CVisualisationGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ return false;
+}
+
+bool CVisualisationGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // VISUALISATION_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case VISUALISATION_LOCKED:
+ {
+ CGUIMessage msg(GUI_MSG_GET_VISUALISATION, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ if (msg.GetPointer())
+ {
+ CGUIVisualisationControl *pVis = static_cast<CGUIVisualisationControl*>(msg.GetPointer());
+ value = pVis->IsLocked();
+ return true;
+ }
+ break;
+ }
+ case VISUALISATION_ENABLED:
+ {
+ value = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICPLAYER_VISUALISATION).empty();
+ return true;
+ }
+ case VISUALISATION_HAS_PRESETS:
+ {
+ CGUIMessage msg(GUI_MSG_GET_VISUALISATION, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ if (msg.GetPointer())
+ {
+ CGUIVisualisationControl* viz = static_cast<CGUIVisualisationControl*>(msg.GetPointer());
+ value = (viz && viz->HasPresets());
+ return true;
+ }
+ break;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/guilib/guiinfo/VisualisationGUIInfo.h b/xbmc/guilib/guiinfo/VisualisationGUIInfo.h
new file mode 100644
index 0000000..d3cb05d
--- /dev/null
+++ b/xbmc/guilib/guiinfo/VisualisationGUIInfo.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProvider.h"
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CVisualisationGUIInfo : public CGUIInfoProvider
+{
+public:
+ CVisualisationGUIInfo() = default;
+ ~CVisualisationGUIInfo() override = default;
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/guiinfo/WeatherGUIInfo.cpp b/xbmc/guilib/guiinfo/WeatherGUIInfo.cpp
new file mode 100644
index 0000000..3aa2044
--- /dev/null
+++ b/xbmc/guilib/guiinfo/WeatherGUIInfo.cpp
@@ -0,0 +1,81 @@
+/*
+ * 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 "guilib/guiinfo/WeatherGUIInfo.h"
+
+#include "FileItem.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "weather/WeatherManager.h"
+
+using namespace KODI::GUILIB::GUIINFO;
+
+bool CWeatherGUIInfo::InitCurrentItem(CFileItem *item)
+{
+ return false;
+}
+
+bool CWeatherGUIInfo::GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // WEATHER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case WEATHER_CONDITIONS_TEXT:
+ value = CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_COND);
+ StringUtils::Trim(value);
+ return true;
+ case WEATHER_CONDITIONS_ICON:
+ value = CServiceBroker::GetWeatherManager().GetInfo(WEATHER_IMAGE_CURRENT_ICON);
+ return true;
+ case WEATHER_TEMPERATURE:
+ value = StringUtils::Format(
+ "{}{}", CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_TEMP),
+ g_langInfo.GetTemperatureUnitString());
+ return true;
+ case WEATHER_LOCATION:
+ value = CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_LOCATION);
+ return true;
+ case WEATHER_FANART_CODE:
+ value = URIUtils::GetFileName(CServiceBroker::GetWeatherManager().GetInfo(WEATHER_IMAGE_CURRENT_ICON));
+ URIUtils::RemoveExtension(value);
+ return true;
+ case WEATHER_PLUGIN:
+ value = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_WEATHER_ADDON);
+ return true;
+ }
+
+ return false;
+}
+
+bool CWeatherGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ return false;
+}
+
+bool CWeatherGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWindow, const CGUIInfo &info) const
+{
+ switch (info.m_info)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // WEATHER_*
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ case WEATHER_IS_FETCHED:
+ value = CServiceBroker::GetWeatherManager().IsFetched();
+ return true;;
+ }
+
+ return false;
+}
diff --git a/xbmc/guilib/guiinfo/WeatherGUIInfo.h b/xbmc/guilib/guiinfo/WeatherGUIInfo.h
new file mode 100644
index 0000000..560d025
--- /dev/null
+++ b/xbmc/guilib/guiinfo/WeatherGUIInfo.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoProvider.h"
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+
+class CGUIInfo;
+
+class CWeatherGUIInfo : public CGUIInfoProvider
+{
+public:
+ CWeatherGUIInfo() = default;
+ ~CWeatherGUIInfo() override = default;
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem *item) override;
+ bool GetLabel(std::string& value, const CFileItem *item, int contextWindow, const CGUIInfo &info, std::string *fallback) const override;
+ bool GetInt(int& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+ bool GetBool(bool& value, const CGUIListItem *item, int contextWindow, const CGUIInfo &info) const override;
+};
+
+} // namespace GUIINFO
+} // namespace GUILIB
+} // namespace KODI
diff --git a/xbmc/guilib/iimage.h b/xbmc/guilib/iimage.h
new file mode 100644
index 0000000..299e74c
--- /dev/null
+++ b/xbmc/guilib/iimage.h
@@ -0,0 +1,74 @@
+/*
+ * 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 <string>
+
+class IImage
+{
+public:
+
+ virtual ~IImage() = default;
+
+ /*!
+ \brief Load an image from memory with the format m_strMimeType to determine it's size and orientation
+ \param buffer The memory location where the image data can be found
+ \param bufSize The size of the buffer
+ \param width The ideal width of the texture
+ \param height The ideal height of the texture
+ \return true if the image could be loaded
+ */
+ virtual bool LoadImageFromMemory(unsigned char* buffer, unsigned int bufSize, unsigned int width, unsigned int height)=0;
+ /*!
+ \brief Decodes the previously loaded image data to the output buffer in 32 bit raw bits
+ \param pixels The output buffer
+ \param width The width of the image
+ \param height The height of the image
+ \param pitch The pitch of the output buffer
+ \param format The format of the output buffer (JpegIO only)
+ \return true if the image data could be decoded to the output buffer
+ */
+ virtual bool Decode(unsigned char* const pixels, unsigned int width, unsigned int height, unsigned int pitch, unsigned int format)=0;
+ /*!
+ \brief Encodes an thumbnail from raw bits of given memory location
+ \remarks Caller need to call ReleaseThumbnailBuffer() afterwards to free the output buffer
+ \param bufferin The memory location where the image data can be found
+ \param width The width of the thumbnail
+ \param height The height of the thumbnail
+ \param format The format of the input buffer (JpegIO only)
+ \param pitch The pitch of the input texture stored in bufferin
+ \param destFile The destination path of the thumbnail to determine the image format from the extension
+ \param bufferout The output buffer (will be allocated inside the method)
+ \param bufferoutSize The output buffer size
+ \return true if the thumbnail was successfully created
+ */
+ virtual bool CreateThumbnailFromSurface(unsigned char* bufferin, unsigned int width, unsigned int height, unsigned int format, unsigned int pitch, const std::string& destFile,
+ unsigned char* &bufferout, unsigned int &bufferoutSize)=0;
+ /*!
+ \brief Frees the output buffer allocated by CreateThumbnailFromSurface
+ */
+ virtual void ReleaseThumbnailBuffer() {}
+
+ unsigned int Width() const { return m_width; }
+ unsigned int Height() const { return m_height; }
+ unsigned int originalWidth() const { return m_originalWidth; }
+ unsigned int originalHeight() const { return m_originalHeight; }
+ unsigned int Orientation() const { return m_orientation; }
+ bool hasAlpha() const { return m_hasAlpha; }
+
+protected:
+
+ unsigned int m_width = 0;
+ unsigned int m_height = 0;
+ unsigned int m_originalWidth = 0; ///< original image width before scaling or cropping
+ unsigned int m_originalHeight = 0; ///< original image height before scaling or cropping
+ unsigned int m_orientation = 0;
+ bool m_hasAlpha = false;
+
+};
diff --git a/xbmc/guilib/imagefactory.cpp b/xbmc/guilib/imagefactory.cpp
new file mode 100644
index 0000000..8156782
--- /dev/null
+++ b/xbmc/guilib/imagefactory.cpp
@@ -0,0 +1,59 @@
+/*
+ * 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 "imagefactory.h"
+
+#include "ServiceBroker.h"
+#include "addons/ExtsMimeSupportList.h"
+#include "addons/ImageDecoder.h"
+#include "addons/addoninfo/AddonType.h"
+#include "guilib/FFmpegImage.h"
+#include "utils/Mime.h"
+
+#include <mutex>
+
+CCriticalSection ImageFactory::m_createSec;
+
+using namespace KODI::ADDONS;
+
+IImage* ImageFactory::CreateLoader(const std::string& strFileName)
+{
+ CURL url(strFileName);
+ return CreateLoader(url);
+}
+
+IImage* ImageFactory::CreateLoader(const CURL& url)
+{
+ if(!url.GetFileType().empty())
+ return CreateLoaderFromMimeType("image/"+url.GetFileType());
+
+ return CreateLoaderFromMimeType(CMime::GetMimeType(url));
+}
+
+IImage* ImageFactory::CreateLoaderFromMimeType(const std::string& strMimeType)
+{
+ auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetMimetypeSupportedAddonInfos(
+ strMimeType, CExtsMimeSupportList::FilterSelect::all);
+ for (const auto& addonInfo : addonInfos)
+ {
+ // Check asked and given mime type is supported by only for here allowed imagedecoder addons.
+ if (addonInfo.first != ADDON::AddonType::IMAGEDECODER)
+ continue;
+
+ std::unique_lock<CCriticalSection> lock(m_createSec);
+ std::unique_ptr<CImageDecoder> result =
+ std::make_unique<CImageDecoder>(addonInfo.second, strMimeType);
+ if (!result->IsCreated())
+ {
+ continue;
+ }
+ return result.release();
+ }
+
+ return new CFFmpegImage(strMimeType);
+}
diff --git a/xbmc/guilib/imagefactory.h b/xbmc/guilib/imagefactory.h
new file mode 100644
index 0000000..b224ef6
--- /dev/null
+++ b/xbmc/guilib/imagefactory.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "URL.h"
+#include "iimage.h"
+#include "threads/CriticalSection.h"
+
+class ImageFactory
+{
+public:
+ ImageFactory() = default;
+ virtual ~ImageFactory() = default;
+
+ static IImage* CreateLoader(const std::string& strFileName);
+ static IImage* CreateLoader(const CURL& url);
+ static IImage* CreateLoaderFromMimeType(const std::string& strMimeType);
+
+private:
+ static CCriticalSection m_createSec; //!< Critical section for add-on creation.
+};
diff --git a/xbmc/input/AppTranslator.cpp b/xbmc/input/AppTranslator.cpp
new file mode 100644
index 0000000..11d62a4
--- /dev/null
+++ b/xbmc/input/AppTranslator.cpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017-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 "AppTranslator.h"
+
+#include "Key.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <map>
+
+namespace
+{
+
+using ActionName = std::string;
+using CommandID = uint32_t;
+
+#ifdef TARGET_WINDOWS
+static const std::map<ActionName, CommandID> AppCommands = {
+ {"browser_back", APPCOMMAND_BROWSER_BACKWARD},
+ {"browser_forward", APPCOMMAND_BROWSER_FORWARD},
+ {"browser_refresh", APPCOMMAND_BROWSER_REFRESH},
+ {"browser_stop", APPCOMMAND_BROWSER_STOP},
+ {"browser_search", APPCOMMAND_BROWSER_SEARCH},
+ {"browser_favorites", APPCOMMAND_BROWSER_FAVORITES},
+ {"browser_home", APPCOMMAND_BROWSER_HOME},
+ {"volume_mute", APPCOMMAND_VOLUME_MUTE},
+ {"volume_down", APPCOMMAND_VOLUME_DOWN},
+ {"volume_up", APPCOMMAND_VOLUME_UP},
+ {"next_track", APPCOMMAND_MEDIA_NEXTTRACK},
+ {"prev_track", APPCOMMAND_MEDIA_PREVIOUSTRACK},
+ {"stop", APPCOMMAND_MEDIA_STOP},
+ {"play_pause", APPCOMMAND_MEDIA_PLAY_PAUSE},
+ {"launch_mail", APPCOMMAND_LAUNCH_MAIL},
+ {"launch_media_select", APPCOMMAND_LAUNCH_MEDIA_SELECT},
+ {"launch_app1", APPCOMMAND_LAUNCH_APP1},
+ {"launch_app2", APPCOMMAND_LAUNCH_APP2},
+ {"play", APPCOMMAND_MEDIA_PLAY},
+ {"pause", APPCOMMAND_MEDIA_PAUSE},
+ {"fastforward", APPCOMMAND_MEDIA_FAST_FORWARD},
+ {"rewind", APPCOMMAND_MEDIA_REWIND},
+ {"channelup", APPCOMMAND_MEDIA_CHANNEL_UP},
+ {"channeldown", APPCOMMAND_MEDIA_CHANNEL_DOWN}};
+#endif
+
+} // anonymous namespace
+
+uint32_t CAppTranslator::TranslateAppCommand(const std::string& szButton)
+{
+#ifdef TARGET_WINDOWS
+ std::string strAppCommand = szButton;
+ StringUtils::ToLower(strAppCommand);
+
+ auto it = AppCommands.find(strAppCommand);
+ if (it != AppCommands.end())
+ return it->second | KEY_APPCOMMAND;
+
+ CLog::Log(LOGERROR, "{}: Can't find appcommand {}", __FUNCTION__, szButton);
+#endif
+
+ return 0;
+}
diff --git a/xbmc/input/AppTranslator.h b/xbmc/input/AppTranslator.h
new file mode 100644
index 0000000..3b331e1
--- /dev/null
+++ b/xbmc/input/AppTranslator.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2017-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 <stdint.h>
+#include <string>
+
+class CAppTranslator
+{
+public:
+ static uint32_t TranslateAppCommand(const std::string& szButton);
+};
diff --git a/xbmc/input/ButtonTranslator.cpp b/xbmc/input/ButtonTranslator.cpp
new file mode 100644
index 0000000..193fc44
--- /dev/null
+++ b/xbmc/input/ButtonTranslator.cpp
@@ -0,0 +1,436 @@
+/*
+ * 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 "ButtonTranslator.h"
+
+#include "AppTranslator.h"
+#include "CustomControllerTranslator.h"
+#include "FileItem.h"
+#include "GamepadTranslator.h"
+#include "IButtonMapper.h"
+#include "IRTranslator.h"
+#include "Key.h"
+#include "KeyboardTranslator.h"
+#include "WindowTranslator.h"
+#include "filesystem/Directory.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/ActionIDs.h"
+#include "input/actions/ActionTranslator.h"
+#include "input/mouse/MouseTranslator.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+
+// Add the supplied device name to the list of connected devices
+bool CButtonTranslator::AddDevice(const std::string& strDevice)
+{
+ // Only add the device if it isn't already in the list
+ if (m_deviceList.find(strDevice) != m_deviceList.end())
+ return false;
+
+ // Add the device
+ m_deviceList.insert(strDevice);
+
+ // New device added so reload the key mappings
+ Load();
+
+ return true;
+}
+
+bool CButtonTranslator::RemoveDevice(const std::string& strDevice)
+{
+ // Find the device
+ auto it = m_deviceList.find(strDevice);
+ if (it == m_deviceList.end())
+ return false;
+
+ // Remove the device
+ m_deviceList.erase(it);
+
+ // Device removed so reload the key mappings
+ Load();
+
+ return true;
+}
+
+bool CButtonTranslator::Load()
+{
+ Clear();
+
+ // Directories to search for keymaps. They're applied in this order,
+ // so keymaps in profile/keymaps/ override e.g. system/keymaps
+ static std::vector<std::string> DIRS_TO_CHECK = {"special://xbmc/system/keymaps/",
+ "special://masterprofile/keymaps/",
+ "special://profile/keymaps/"};
+
+ bool success = false;
+
+ for (const auto& dir : DIRS_TO_CHECK)
+ {
+ if (XFILE::CDirectory::Exists(dir))
+ {
+ CFileItemList files;
+ XFILE::CDirectory::GetDirectory(dir, files, ".xml", XFILE::DIR_FLAG_DEFAULTS);
+ // Sort the list for filesystem based priorities, e.g. 01-keymap.xml, 02-keymap-overrides.xml
+ files.Sort(SortByFile, SortOrderAscending);
+ for (int fileIndex = 0; fileIndex < files.Size(); ++fileIndex)
+ {
+ if (!files[fileIndex]->m_bIsFolder)
+ success |= LoadKeymap(files[fileIndex]->GetPath());
+ }
+
+ // Load mappings for any HID devices we have connected
+ for (const auto& device : m_deviceList)
+ {
+ std::string devicedir = dir;
+ devicedir.append(device);
+ devicedir.append("/");
+ if (XFILE::CDirectory::Exists(devicedir))
+ {
+ CFileItemList files;
+ XFILE::CDirectory::GetDirectory(devicedir, files, ".xml", XFILE::DIR_FLAG_DEFAULTS);
+ // Sort the list for filesystem based priorities, e.g. 01-keymap.xml,
+ // 02-keymap-overrides.xml
+ files.Sort(SortByFile, SortOrderAscending);
+ for (int fileIndex = 0; fileIndex < files.Size(); ++fileIndex)
+ {
+ if (!files[fileIndex]->m_bIsFolder)
+ success |= LoadKeymap(files[fileIndex]->GetPath());
+ }
+ }
+ }
+ }
+ }
+
+ if (!success)
+ {
+ CLog::Log(LOGERROR, "Error loading keymaps from: {} or {} or {}", DIRS_TO_CHECK[0],
+ DIRS_TO_CHECK[1], DIRS_TO_CHECK[2]);
+ return false;
+ }
+
+ // Done!
+ return true;
+}
+
+bool CButtonTranslator::LoadKeymap(const std::string& keymapPath)
+{
+ CXBMCTinyXML xmlDoc;
+
+ CLog::Log(LOGINFO, "Loading {}", keymapPath);
+ if (!xmlDoc.LoadFile(keymapPath))
+ {
+ CLog::Log(LOGERROR, "Error loading keymap: {}, Line {}\n{}", keymapPath, xmlDoc.ErrorRow(),
+ xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement* pRoot = xmlDoc.RootElement();
+ if (pRoot == nullptr)
+ {
+ CLog::Log(LOGERROR, "Error getting keymap root: {}", keymapPath);
+ return false;
+ }
+
+ std::string strValue = pRoot->Value();
+ if (strValue != "keymap")
+ {
+ CLog::Log(LOGERROR, "{} Doesn't contain <keymap>", keymapPath);
+ return false;
+ }
+
+ // run through our window groups
+ TiXmlNode* pWindow = pRoot->FirstChild();
+ while (pWindow != nullptr)
+ {
+ if (pWindow->Type() == TiXmlNode::TINYXML_ELEMENT)
+ {
+ int windowID = WINDOW_INVALID;
+ const char* szWindow = pWindow->Value();
+ if (szWindow != nullptr)
+ {
+ if (StringUtils::CompareNoCase(szWindow, "global") == 0)
+ windowID = -1;
+ else
+ windowID = CWindowTranslator::TranslateWindow(szWindow);
+ }
+ MapWindowActions(pWindow, windowID);
+ }
+ pWindow = pWindow->NextSibling();
+ }
+
+ return true;
+}
+
+CAction CButtonTranslator::GetAction(int window, const CKey& key, bool fallback)
+{
+ std::string strAction;
+
+ // handle virtual windows
+ window = CWindowTranslator::GetVirtualWindow(window);
+
+ // try to get the action from the current window
+ unsigned int actionID = GetActionCode(window, key, strAction);
+
+ if (fallback)
+ {
+ // if it's invalid, try to get it from fallback windows or the global map (window == -1)
+ while (actionID == ACTION_NONE && window > -1)
+ {
+ window = CWindowTranslator::GetFallbackWindow(window);
+ actionID = GetActionCode(window, key, strAction);
+ }
+ }
+
+ return CAction(actionID, strAction, key);
+}
+
+bool CButtonTranslator::HasLongpressMapping(int window, const CKey& key)
+{
+ // handle virtual windows
+ window = CWindowTranslator::GetVirtualWindow(window);
+ return HasLongpressMapping_Internal(window, key);
+}
+
+bool CButtonTranslator::HasLongpressMapping_Internal(int window, const CKey& key)
+{
+ std::map<int, buttonMap>::const_iterator it = m_translatorMap.find(window);
+ if (it != m_translatorMap.end())
+ {
+ uint32_t code = key.GetButtonCode();
+ code |= CKey::MODIFIER_LONG;
+ buttonMap::const_iterator it2 = (*it).second.find(code);
+
+ if (it2 != (*it).second.end())
+ return it2->second.id != ACTION_NOOP;
+
+#ifdef TARGET_POSIX
+ // Some buttoncodes changed in Hardy
+ if ((code & KEY_VKEY) == KEY_VKEY && (code & 0x0F00))
+ {
+ code &= ~0x0F00;
+ it2 = (*it).second.find(code);
+ if (it2 != (*it).second.end())
+ return true;
+ }
+#endif
+ }
+
+ // no key mapping found for the current window do the fallback handling
+ if (window > -1)
+ {
+ // first check if we have a fallback for the window
+ int fallbackWindow = CWindowTranslator::GetFallbackWindow(window);
+ if (fallbackWindow > -1 && HasLongpressMapping_Internal(fallbackWindow, key))
+ return true;
+
+ // fallback to default section
+ return HasLongpressMapping_Internal(-1, key);
+ }
+
+ return false;
+}
+
+unsigned int CButtonTranslator::GetActionCode(int window,
+ const CKey& key,
+ std::string& strAction) const
+{
+ uint32_t code = key.GetButtonCode();
+
+ std::map<int, buttonMap>::const_iterator it = m_translatorMap.find(window);
+ if (it == m_translatorMap.end())
+ return ACTION_NONE;
+
+ buttonMap::const_iterator it2 = (*it).second.find(code);
+ unsigned int action = ACTION_NONE;
+ if (it2 == (*it).second.end() &&
+ code & CKey::MODIFIER_LONG) // If long action not found, try short one
+ {
+ code &= ~CKey::MODIFIER_LONG;
+ it2 = (*it).second.find(code);
+ }
+
+ if (it2 != (*it).second.end())
+ {
+ action = (*it2).second.id;
+ strAction = (*it2).second.strID;
+ }
+
+#ifdef TARGET_POSIX
+ // Some buttoncodes changed in Hardy
+ if (action == ACTION_NONE && (code & KEY_VKEY) == KEY_VKEY && (code & 0x0F00))
+ {
+ CLog::Log(LOGDEBUG, "{}: Trying Hardy keycode for {:#04x}", __FUNCTION__, code);
+ code &= ~0x0F00;
+ it2 = (*it).second.find(code);
+ if (it2 != (*it).second.end())
+ {
+ action = (*it2).second.id;
+ strAction = (*it2).second.strID;
+ }
+ }
+#endif
+
+ return action;
+}
+
+void CButtonTranslator::MapAction(uint32_t buttonCode, const std::string& szAction, buttonMap& map)
+{
+ unsigned int action = ACTION_NONE;
+ if (!CActionTranslator::TranslateString(szAction, action) || buttonCode == 0)
+ return; // no valid action, or an invalid buttoncode
+
+ // have a valid action, and a valid button - map it.
+ // check to see if we've already got this (button,action) pair defined
+ buttonMap::iterator it = map.find(buttonCode);
+ if (it == map.end() || (*it).second.id != action || (*it).second.strID != szAction)
+ {
+ // NOTE: This multimap is only being used as a normal map at this point (no support
+ // for multiple actions per key)
+ if (it != map.end())
+ map.erase(it);
+ CButtonAction button;
+ button.id = action;
+ button.strID = szAction;
+ map.insert(std::pair<uint32_t, CButtonAction>(buttonCode, button));
+ }
+}
+
+void CButtonTranslator::MapWindowActions(const TiXmlNode* pWindow, int windowID)
+{
+ if (pWindow == nullptr || windowID == WINDOW_INVALID)
+ return;
+
+ const TiXmlNode* pDevice;
+
+ static const std::vector<std::string> types = {"gamepad", "remote", "universalremote",
+ "keyboard", "mouse", "appcommand"};
+
+ for (const auto& type : types)
+ {
+ for (pDevice = pWindow->FirstChild(type); pDevice != nullptr;
+ pDevice = pDevice->NextSiblingElement(type))
+ {
+ buttonMap map;
+ std::map<int, buttonMap>::iterator it = m_translatorMap.find(windowID);
+ if (it != m_translatorMap.end())
+ {
+ map = std::move(it->second);
+ m_translatorMap.erase(it);
+ }
+
+ const TiXmlElement* pButton = pDevice->FirstChildElement();
+
+ while (pButton != nullptr)
+ {
+ uint32_t buttonCode = 0;
+
+ if (type == "gamepad")
+ buttonCode = CGamepadTranslator::TranslateString(pButton->Value());
+ else if (type == "remote")
+ buttonCode = CIRTranslator::TranslateString(pButton->Value());
+ else if (type == "universalremote")
+ buttonCode = CIRTranslator::TranslateUniversalRemoteString(pButton->Value());
+ else if (type == "keyboard")
+ buttonCode = CKeyboardTranslator::TranslateButton(pButton);
+ else if (type == "mouse")
+ buttonCode = CMouseTranslator::TranslateCommand(pButton);
+ else if (type == "appcommand")
+ buttonCode = CAppTranslator::TranslateAppCommand(pButton->Value());
+
+ if (buttonCode != 0)
+ {
+ if (pButton->FirstChild() && pButton->FirstChild()->Value()[0])
+ MapAction(buttonCode, pButton->FirstChild()->Value(), map);
+ else
+ {
+ buttonMap::iterator it = map.find(buttonCode);
+ while (it != map.end())
+ {
+ map.erase(it);
+ it = map.find(buttonCode);
+ }
+ }
+ }
+ pButton = pButton->NextSiblingElement();
+ }
+
+ // add our map to our table
+ if (!map.empty())
+ m_translatorMap.insert(std::make_pair(windowID, std::move(map)));
+ }
+ }
+
+ for (const auto& it : m_buttonMappers)
+ {
+ const std::string& device = it.first;
+ IButtonMapper* mapper = it.second;
+
+ // Map device actions
+ pDevice = pWindow->FirstChild(device);
+ while (pDevice != nullptr)
+ {
+ mapper->MapActions(windowID, pDevice);
+ pDevice = pDevice->NextSibling(device);
+ }
+ }
+}
+
+void CButtonTranslator::Clear()
+{
+ m_translatorMap.clear();
+
+ for (auto it : m_buttonMappers)
+ it.second->Clear();
+}
+
+void CButtonTranslator::RegisterMapper(const std::string& device, IButtonMapper* mapper)
+{
+ m_buttonMappers[device] = mapper;
+}
+
+void CButtonTranslator::UnregisterMapper(const IButtonMapper* mapper)
+{
+ for (auto it = m_buttonMappers.begin(); it != m_buttonMappers.end(); ++it)
+ {
+ if (it->second == mapper)
+ {
+ m_buttonMappers.erase(it);
+ break;
+ }
+ }
+}
+
+uint32_t CButtonTranslator::TranslateString(const std::string& strMap, const std::string& strButton)
+{
+ if (strMap == "KB") // standard keyboard map
+ {
+ return CKeyboardTranslator::TranslateString(strButton);
+ }
+ else if (strMap == "XG") // xbox gamepad map
+ {
+ return CGamepadTranslator::TranslateString(strButton);
+ }
+ else if (strMap == "R1") // xbox remote map
+ {
+ return CIRTranslator::TranslateString(strButton);
+ }
+ else if (strMap == "R2") // xbox universal remote map
+ {
+ return CIRTranslator::TranslateUniversalRemoteString(strButton);
+ }
+ else
+ {
+ return 0;
+ }
+}
diff --git a/xbmc/input/ButtonTranslator.h b/xbmc/input/ButtonTranslator.h
new file mode 100644
index 0000000..3f33c27
--- /dev/null
+++ b/xbmc/input/ButtonTranslator.h
@@ -0,0 +1,93 @@
+/*
+ * 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 "input/actions/Action.h"
+#include "network/EventClient.h"
+
+#include <map>
+#include <set>
+#include <string>
+
+class CKey;
+class TiXmlNode;
+class CCustomControllerTranslator;
+class CTouchTranslator;
+class IButtonMapper;
+class IWindowKeymap;
+
+/// singleton class to map from buttons to actions
+/// Warning: _not_ threadsafe!
+class CButtonTranslator
+{
+ friend class EVENTCLIENT::CEventButtonState;
+
+public:
+ CButtonTranslator() = default;
+ CButtonTranslator(const CButtonTranslator&) = delete;
+ CButtonTranslator const& operator=(CButtonTranslator const&) = delete;
+ virtual ~CButtonTranslator() = default;
+
+ // Add/remove a HID device with custom mappings
+ bool AddDevice(const std::string& strDevice);
+ bool RemoveDevice(const std::string& strDevice);
+
+ /// loads Keymap.xml
+ bool Load();
+
+ /// clears the maps
+ void Clear();
+
+ /*! \brief Finds out if a longpress mapping exists for this key
+ \param window id of the current window
+ \param key to search a mapping for
+ \return true if a longpress mapping exists
+ */
+ bool HasLongpressMapping(int window, const CKey& key);
+
+ /*! \brief Obtain the action configured for a given window and key
+ \param window the window id
+ \param key the key to query the action for
+ \param fallback if no action is directly configured for the given window, obtain the action from
+ fallback window, if exists or from global config as last resort
+ \return the action matching the key
+ */
+ CAction GetAction(int window, const CKey& key, bool fallback = true);
+
+ void RegisterMapper(const std::string& device, IButtonMapper* mapper);
+ void UnregisterMapper(const IButtonMapper* mapper);
+
+ static uint32_t TranslateString(const std::string& strMap, const std::string& strButton);
+
+private:
+ struct CButtonAction
+ {
+ unsigned int id;
+ std::string strID; // needed for "ActivateWindow()" type actions
+ };
+
+ typedef std::multimap<uint32_t, CButtonAction> buttonMap; // our button map to fill in
+
+ // m_translatorMap contains all mappings i.e. m_BaseMap + HID device mappings
+ std::map<int, buttonMap> m_translatorMap;
+
+ // m_deviceList contains the list of connected HID devices
+ std::set<std::string> m_deviceList;
+
+ unsigned int GetActionCode(int window, const CKey& key, std::string& strAction) const;
+
+ void MapWindowActions(const TiXmlNode* pWindow, int wWindowID);
+ void MapAction(uint32_t buttonCode, const std::string& szAction, buttonMap& map);
+
+ bool LoadKeymap(const std::string& keymapPath);
+
+ bool HasLongpressMapping_Internal(int window, const CKey& key);
+
+ std::map<std::string, IButtonMapper*> m_buttonMappers;
+};
diff --git a/xbmc/input/CMakeLists.txt b/xbmc/input/CMakeLists.txt
new file mode 100644
index 0000000..3408cf0
--- /dev/null
+++ b/xbmc/input/CMakeLists.txt
@@ -0,0 +1,59 @@
+set(SOURCES AppTranslator.cpp
+ ButtonTranslator.cpp
+ CustomControllerTranslator.cpp
+ GamepadTranslator.cpp
+ InertialScrollingHandler.cpp
+ InputCodingTableBasePY.cpp
+ InputCodingTableFactory.cpp
+ InputCodingTableKorean.cpp
+ InputManager.cpp
+ InputTranslator.cpp
+ IRTranslator.cpp
+ JoystickMapper.cpp
+ Key.cpp
+ KeyboardLayout.cpp
+ KeyboardLayoutManager.cpp
+ KeyboardStat.cpp
+ KeyboardTranslator.cpp
+ Keymap.cpp
+ KeymapEnvironment.cpp
+ TouchTranslator.cpp
+ WindowKeymap.cpp
+ WindowTranslator.cpp
+ XBMC_keytable.cpp)
+
+set(HEADERS hardware/IHardwareInput.h
+ remote/IRRemote.h
+ AppTranslator.h
+ ButtonTranslator.h
+ CustomControllerTranslator.h
+ GamepadTranslator.h
+ IButtonMapper.h
+ IKeymap.h
+ IKeymapEnvironment.h
+ InertialScrollingHandler.h
+ InputCodingTable.h
+ InputCodingTableBasePY.h
+ InputCodingTableFactory.h
+ InputCodingTableKorean.h
+ InputManager.h
+ InputTranslator.h
+ InputTypes.h
+ IRTranslator.h
+ JoystickMapper.h
+ Key.h
+ KeyboardLayout.h
+ KeyboardLayoutManager.h
+ KeyboardStat.h
+ KeyboardTranslator.h
+ Keymap.h
+ KeymapEnvironment.h
+ TouchTranslator.h
+ WindowTranslator.h
+ WindowKeymap.h
+ XBMC_keyboard.h
+ XBMC_keysym.h
+ XBMC_keytable.h
+ XBMC_vkeys.h)
+
+core_add_library(input)
diff --git a/xbmc/input/CustomControllerTranslator.cpp b/xbmc/input/CustomControllerTranslator.cpp
new file mode 100644
index 0000000..9d25a6e
--- /dev/null
+++ b/xbmc/input/CustomControllerTranslator.cpp
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2017-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 "CustomControllerTranslator.h"
+
+#include "WindowTranslator.h" //! @todo
+#include "input/actions/ActionIDs.h"
+#include "input/actions/ActionTranslator.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+void CCustomControllerTranslator::MapActions(int windowID, const TiXmlNode* pCustomController)
+{
+ CustomControllerButtonMap buttonMap;
+ std::string controllerName;
+
+ const TiXmlElement* pController = pCustomController->ToElement();
+ if (pController != nullptr)
+ {
+ // Transform loose name to new family, including altnames
+ const char* name = pController->Attribute("name");
+ if (name != nullptr)
+ controllerName = name;
+ }
+
+ if (controllerName.empty())
+ {
+ CLog::Log(LOGERROR, "Missing attribute \"name\" for tag \"customcontroller\"");
+ return;
+ }
+
+ // Parse map
+ const TiXmlElement* pButton = pCustomController->FirstChildElement();
+ int id = 0;
+ while (pButton != nullptr)
+ {
+ std::string action;
+ if (!pButton->NoChildren())
+ action = pButton->FirstChild()->ValueStr();
+
+ if ((pButton->QueryIntAttribute("id", &id) == TIXML_SUCCESS) && id >= 0)
+ {
+ buttonMap[id] = action;
+ }
+ else
+ CLog::Log(LOGERROR, "Error reading customController map element, Invalid id: {}", id);
+
+ pButton = pButton->NextSiblingElement();
+ }
+
+ // Add/overwrite button with mapped actions
+ for (auto button : buttonMap)
+ m_customControllersMap[controllerName][windowID][button.first] = std::move(button.second);
+}
+
+void CCustomControllerTranslator::Clear()
+{
+ m_customControllersMap.clear();
+}
+
+bool CCustomControllerTranslator::TranslateCustomControllerString(int windowId,
+ const std::string& controllerName,
+ int buttonId,
+ int& action,
+ std::string& strAction)
+{
+ unsigned int actionId = ACTION_NONE;
+
+ // handle virtual windows
+ windowId = CWindowTranslator::GetVirtualWindow(windowId);
+
+ // Try to get the action from the current window
+ if (!TranslateString(windowId, controllerName, buttonId, actionId, strAction))
+ {
+ // if it's invalid, try to get it from fallback windows or the global map (windowId == -1)
+ while (actionId == ACTION_NONE && windowId > -1)
+ {
+ windowId = CWindowTranslator::GetFallbackWindow(windowId);
+ TranslateString(windowId, controllerName, buttonId, actionId, strAction);
+ }
+ }
+
+ action = actionId;
+ return actionId != ACTION_NONE;
+}
+
+bool CCustomControllerTranslator::TranslateString(int windowId,
+ const std::string& controllerName,
+ int buttonId,
+ unsigned int& actionId,
+ std::string& strAction)
+{
+ // Resolve the correct custom controller
+ auto it = m_customControllersMap.find(controllerName);
+ if (it == m_customControllersMap.end())
+ return false;
+
+ const CustomControllerWindowMap& windowMap = it->second;
+ auto it2 = windowMap.find(windowId);
+ if (it2 != windowMap.end())
+ {
+ const CustomControllerButtonMap& buttonMap = it2->second;
+ auto it3 = buttonMap.find(buttonId);
+ if (it3 != buttonMap.end())
+ {
+ strAction = it3->second;
+ CActionTranslator::TranslateString(strAction, actionId);
+ }
+ }
+
+ return actionId != ACTION_NONE;
+}
diff --git a/xbmc/input/CustomControllerTranslator.h b/xbmc/input/CustomControllerTranslator.h
new file mode 100644
index 0000000..5c06fe7
--- /dev/null
+++ b/xbmc/input/CustomControllerTranslator.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017-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 "IButtonMapper.h"
+
+#include <map>
+#include <string>
+
+class TiXmlNode;
+
+class CCustomControllerTranslator : public IButtonMapper
+{
+public:
+ CCustomControllerTranslator() = default;
+
+ // implementation of IButtonMapper
+ void MapActions(int windowID, const TiXmlNode* pDevice) override;
+ void Clear() override;
+
+ bool TranslateCustomControllerString(int windowId,
+ const std::string& controllerName,
+ int buttonId,
+ int& action,
+ std::string& strAction);
+
+private:
+ bool TranslateString(int windowId,
+ const std::string& controllerName,
+ int buttonId,
+ unsigned int& actionId,
+ std::string& strAction);
+
+ // Maps button id to action
+ using CustomControllerButtonMap = std::map<int, std::string>;
+
+ // Maps window id to controller button map
+ using CustomControllerWindowMap = std::map<int, CustomControllerButtonMap>;
+
+ // Maps custom controller name to controller Window map
+ std::map<std::string, CustomControllerWindowMap> m_customControllersMap;
+};
diff --git a/xbmc/input/GamepadTranslator.cpp b/xbmc/input/GamepadTranslator.cpp
new file mode 100644
index 0000000..9d2db28
--- /dev/null
+++ b/xbmc/input/GamepadTranslator.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2017-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 "GamepadTranslator.h"
+
+#include "Key.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <string>
+
+uint32_t CGamepadTranslator::TranslateString(std::string strButton)
+{
+ if (strButton.empty())
+ return 0;
+
+ StringUtils::ToLower(strButton);
+
+ uint32_t buttonCode = 0;
+ if (strButton == "a")
+ buttonCode = KEY_BUTTON_A;
+ else if (strButton == "b")
+ buttonCode = KEY_BUTTON_B;
+ else if (strButton == "x")
+ buttonCode = KEY_BUTTON_X;
+ else if (strButton == "y")
+ buttonCode = KEY_BUTTON_Y;
+ else if (strButton == "white")
+ buttonCode = KEY_BUTTON_WHITE;
+ else if (strButton == "black")
+ buttonCode = KEY_BUTTON_BLACK;
+ else if (strButton == "start")
+ buttonCode = KEY_BUTTON_START;
+ else if (strButton == "back")
+ buttonCode = KEY_BUTTON_BACK;
+ else if (strButton == "leftthumbbutton")
+ buttonCode = KEY_BUTTON_LEFT_THUMB_BUTTON;
+ else if (strButton == "rightthumbbutton")
+ buttonCode = KEY_BUTTON_RIGHT_THUMB_BUTTON;
+ else if (strButton == "leftthumbstick")
+ buttonCode = KEY_BUTTON_LEFT_THUMB_STICK;
+ else if (strButton == "leftthumbstickup")
+ buttonCode = KEY_BUTTON_LEFT_THUMB_STICK_UP;
+ else if (strButton == "leftthumbstickdown")
+ buttonCode = KEY_BUTTON_LEFT_THUMB_STICK_DOWN;
+ else if (strButton == "leftthumbstickleft")
+ buttonCode = KEY_BUTTON_LEFT_THUMB_STICK_LEFT;
+ else if (strButton == "leftthumbstickright")
+ buttonCode = KEY_BUTTON_LEFT_THUMB_STICK_RIGHT;
+ else if (strButton == "rightthumbstick")
+ buttonCode = KEY_BUTTON_RIGHT_THUMB_STICK;
+ else if (strButton == "rightthumbstickup")
+ buttonCode = KEY_BUTTON_RIGHT_THUMB_STICK_UP;
+ else if (strButton == "rightthumbstickdown")
+ buttonCode = KEY_BUTTON_RIGHT_THUMB_STICK_DOWN;
+ else if (strButton == "rightthumbstickleft")
+ buttonCode = KEY_BUTTON_RIGHT_THUMB_STICK_LEFT;
+ else if (strButton == "rightthumbstickright")
+ buttonCode = KEY_BUTTON_RIGHT_THUMB_STICK_RIGHT;
+ else if (strButton == "lefttrigger")
+ buttonCode = KEY_BUTTON_LEFT_TRIGGER;
+ else if (strButton == "righttrigger")
+ buttonCode = KEY_BUTTON_RIGHT_TRIGGER;
+ else if (strButton == "leftanalogtrigger")
+ buttonCode = KEY_BUTTON_LEFT_ANALOG_TRIGGER;
+ else if (strButton == "rightanalogtrigger")
+ buttonCode = KEY_BUTTON_RIGHT_ANALOG_TRIGGER;
+ else if (strButton == "dpadleft")
+ buttonCode = KEY_BUTTON_DPAD_LEFT;
+ else if (strButton == "dpadright")
+ buttonCode = KEY_BUTTON_DPAD_RIGHT;
+ else if (strButton == "dpadup")
+ buttonCode = KEY_BUTTON_DPAD_UP;
+ else if (strButton == "dpaddown")
+ buttonCode = KEY_BUTTON_DPAD_DOWN;
+ else
+ CLog::Log(LOGERROR, "Gamepad Translator: Can't find button {}", strButton);
+
+ return buttonCode;
+}
diff --git a/xbmc/input/GamepadTranslator.h b/xbmc/input/GamepadTranslator.h
new file mode 100644
index 0000000..7dd339d
--- /dev/null
+++ b/xbmc/input/GamepadTranslator.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2017-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 <stdint.h>
+#include <string>
+
+class CGamepadTranslator
+{
+public:
+ static uint32_t TranslateString(std::string strButton);
+};
diff --git a/xbmc/input/IButtonMapper.h b/xbmc/input/IButtonMapper.h
new file mode 100644
index 0000000..e535df1
--- /dev/null
+++ b/xbmc/input/IButtonMapper.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2017-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 TiXmlNode;
+
+/*!
+ * \brief Interface for classes that can map buttons to Kodi actions
+ */
+class IButtonMapper
+{
+public:
+ virtual ~IButtonMapper() = default;
+
+ virtual void MapActions(int windowId, const TiXmlNode* pDevice) = 0;
+
+ virtual void Clear() = 0;
+};
diff --git a/xbmc/input/IKeymap.h b/xbmc/input/IKeymap.h
new file mode 100644
index 0000000..22c0b6e
--- /dev/null
+++ b/xbmc/input/IKeymap.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017-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 "input/joysticks/JoystickTypes.h"
+
+#include <string>
+
+class IKeymapEnvironment;
+
+/*!
+ * \brief Interface for mapping buttons to Kodi actions
+ *
+ * \sa CJoystickUtils::MakeKeyName
+ */
+class IKeymap
+{
+public:
+ virtual ~IKeymap() = default;
+
+ /*!
+ * \brief The controller ID
+ *
+ * This is required because key names are specific to each controller
+ */
+ virtual std::string ControllerID() const = 0;
+
+ /*!
+ * \brief Access properties of the keymapping environment
+ */
+ virtual const IKeymapEnvironment* Environment() const = 0;
+
+ /*!
+ * \brief Get the actions for a given key name
+ *
+ * \param keyName The key name created by CJoystickUtils::MakeKeyName()
+ *
+ * \return A list of actions associated with the given key
+ */
+ virtual const KODI::JOYSTICK::KeymapActionGroup& GetActions(const std::string& keyName) const = 0;
+};
+
+/*!
+ * \brief Interface for mapping buttons to Kodi actions for specific windows
+ *
+ * \sa CJoystickUtils::MakeKeyName
+ */
+class IWindowKeymap
+{
+public:
+ virtual ~IWindowKeymap() = default;
+
+ /*!
+ * \brief The controller ID
+ *
+ * This is required because key names are specific to each controller
+ */
+ virtual std::string ControllerID() const = 0;
+
+ /*!
+ * \brief Add an action to the keymap for a given key name and window ID
+ *
+ * \param windowId The window ID to look up
+ * \param keyName The key name created by CJoystickUtils::MakeKeyName()
+ * \param action The action to map
+ */
+ virtual void MapAction(int windowId,
+ const std::string& keyName,
+ KODI::JOYSTICK::KeymapAction action) = 0;
+
+ /*!
+ * \brief Get the actions for a given key name and window ID
+ *
+ * \param windowId The window ID to look up
+ * \param keyName The key name created by CJoystickUtils::MakeKeyName()
+ *
+ * \return A list of actions associated with the given key for the given window
+ */
+ virtual const KODI::JOYSTICK::KeymapActionGroup& GetActions(int windowId,
+ const std::string& keyName) const = 0;
+};
diff --git a/xbmc/input/IKeymapEnvironment.h b/xbmc/input/IKeymapEnvironment.h
new file mode 100644
index 0000000..8d327fd
--- /dev/null
+++ b/xbmc/input/IKeymapEnvironment.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017-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
+
+/*!
+ * \brief Customizes the environment in which keymapping is performed
+ *
+ * By overriding GetWindowID() and GetFallthrough(), an agent can customize
+ * the behavior of the keymap by forcing a window and preventing the use of
+ * a fallback window, respectively.
+ *
+ * An agent can also inform the keymap that it isn't accepting input currently,
+ * allowing the input to fall through to the next input handler.
+ */
+class IKeymapEnvironment
+{
+public:
+ virtual ~IKeymapEnvironment() = default;
+
+ /*!
+ * \brief Get the window ID for which actions should be translated
+ *
+ * \return The window ID
+ */
+ virtual int GetWindowID() const = 0;
+
+ /*!
+ * \brief Set the window ID
+ *
+ * \param The window ID, used for translating actions
+ */
+ virtual void SetWindowID(int windowId) = 0;
+
+ /*!
+ * \brief Get the fallthrough window to when a key definition is missing
+ *
+ * \param windowId The window ID
+ *
+ * \return The window ID, or -1 for no fallthrough
+ */
+ virtual int GetFallthrough(int windowId) const = 0;
+
+ /*!
+ * \brief Specify if the global keymap should be used when the window and
+ * fallback window are undefined
+ */
+ virtual bool UseGlobalFallthrough() const = 0;
+
+ /*!
+ * \brief Specify if the agent should monitor for easter egg presses
+ */
+ virtual bool UseEasterEgg() const = 0;
+};
diff --git a/xbmc/input/IRTranslator.cpp b/xbmc/input/IRTranslator.cpp
new file mode 100644
index 0000000..906e4f5
--- /dev/null
+++ b/xbmc/input/IRTranslator.cpp
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2017-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 "IRTranslator.h"
+
+#include "ServiceBroker.h"
+#include "input/remote/IRRemote.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <stdlib.h>
+#include <vector>
+
+void CIRTranslator::Load(const std::string& irMapName)
+{
+ if (irMapName.empty())
+ return;
+
+ Clear();
+
+ bool success = false;
+
+ std::string irMapPath = URIUtils::AddFileToFolder("special://xbmc/system/", irMapName);
+ if (CFileUtils::Exists(irMapPath))
+ success |= LoadIRMap(irMapPath);
+ else
+ CLog::Log(LOGDEBUG, "CIRTranslator::Load - no system {} found, skipping", irMapName);
+
+ irMapPath =
+ CServiceBroker::GetSettingsComponent()->GetProfileManager()->GetUserDataItem(irMapName);
+ if (CFileUtils::Exists(irMapPath))
+ success |= LoadIRMap(irMapPath);
+ else
+ CLog::Log(LOGDEBUG, "CIRTranslator::Load - no userdata {} found, skipping", irMapName);
+
+ if (!success)
+ CLog::Log(LOGERROR, "CIRTranslator::Load - unable to load remote map {}", irMapName);
+}
+
+bool CIRTranslator::LoadIRMap(const std::string& irMapPath)
+{
+ std::string remoteMapTag = URIUtils::GetFileName(irMapPath);
+ size_t lastindex = remoteMapTag.find_last_of('.');
+ if (lastindex != std::string::npos)
+ remoteMapTag = remoteMapTag.substr(0, lastindex);
+ StringUtils::ToLower(remoteMapTag);
+
+ // Load our xml file, and fill up our mapping tables
+ CXBMCTinyXML xmlDoc;
+
+ // Load the config file
+ CLog::Log(LOGINFO, "Loading {}", irMapPath);
+ if (!xmlDoc.LoadFile(irMapPath))
+ {
+ CLog::Log(LOGERROR, "{}, Line {}\n{}", irMapPath, xmlDoc.ErrorRow(), xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement* pRoot = xmlDoc.RootElement();
+ std::string strValue = pRoot->Value();
+ if (strValue != remoteMapTag)
+ {
+ CLog::Log(LOGERROR, "{} Doesn't contain <{}>", irMapPath, remoteMapTag);
+ return false;
+ }
+
+ // Run through our window groups
+ TiXmlNode* pRemote = pRoot->FirstChild();
+ while (pRemote != nullptr)
+ {
+ if (pRemote->Type() == TiXmlNode::TINYXML_ELEMENT)
+ {
+ const char* szRemote = pRemote->Value();
+ if (szRemote != nullptr)
+ {
+ TiXmlAttribute* pAttr = pRemote->ToElement()->FirstAttribute();
+ if (pAttr != nullptr)
+ MapRemote(pRemote, pAttr->Value());
+ }
+ }
+ pRemote = pRemote->NextSibling();
+ }
+
+ return true;
+}
+
+void CIRTranslator::MapRemote(TiXmlNode* pRemote, const std::string& szDevice)
+{
+ CLog::Log(LOGINFO, "* Adding remote mapping for device '{}'", szDevice);
+
+ std::vector<std::string> remoteNames;
+
+ auto it = m_irRemotesMap.find(szDevice);
+ if (it == m_irRemotesMap.end())
+ m_irRemotesMap[szDevice].reset(new IRButtonMap);
+
+ const std::shared_ptr<IRButtonMap>& buttons = m_irRemotesMap[szDevice];
+
+ TiXmlElement* pButton = pRemote->FirstChildElement();
+ while (pButton != nullptr)
+ {
+ if (!pButton->NoChildren())
+ {
+ if (pButton->ValueStr() == "altname")
+ remoteNames.push_back(pButton->FirstChild()->ValueStr());
+ else
+ (*buttons)[pButton->FirstChild()->ValueStr()] = pButton->ValueStr();
+ }
+ pButton = pButton->NextSiblingElement();
+ }
+
+ for (const auto& remoteName : remoteNames)
+ {
+ CLog::Log(LOGINFO, "* Linking remote mapping for '{}' to '{}'", szDevice, remoteName);
+ m_irRemotesMap[remoteName] = buttons;
+ }
+}
+
+void CIRTranslator::Clear()
+{
+ m_irRemotesMap.clear();
+}
+
+unsigned int CIRTranslator::TranslateButton(const std::string& szDevice,
+ const std::string& szButton)
+{
+ // Find the device
+ auto it = m_irRemotesMap.find(szDevice);
+ if (it == m_irRemotesMap.end())
+ return 0;
+
+ // Find the button
+ auto it2 = (*it).second->find(szButton);
+ if (it2 == (*it).second->end())
+ return 0;
+
+ // Convert the button to code
+ if (StringUtils::CompareNoCase((*it2).second, "obc", 3) == 0)
+ return TranslateUniversalRemoteString((*it2).second);
+
+ return TranslateString((*it2).second);
+}
+
+uint32_t CIRTranslator::TranslateString(std::string strButton)
+{
+ if (strButton.empty())
+ return 0;
+
+ uint32_t buttonCode = 0;
+
+ StringUtils::ToLower(strButton);
+
+ if (strButton == "left")
+ buttonCode = XINPUT_IR_REMOTE_LEFT;
+ else if (strButton == "right")
+ buttonCode = XINPUT_IR_REMOTE_RIGHT;
+ else if (strButton == "up")
+ buttonCode = XINPUT_IR_REMOTE_UP;
+ else if (strButton == "down")
+ buttonCode = XINPUT_IR_REMOTE_DOWN;
+ else if (strButton == "select")
+ buttonCode = XINPUT_IR_REMOTE_SELECT;
+ else if (strButton == "back")
+ buttonCode = XINPUT_IR_REMOTE_BACK;
+ else if (strButton == "menu")
+ buttonCode = XINPUT_IR_REMOTE_MENU;
+ else if (strButton == "info")
+ buttonCode = XINPUT_IR_REMOTE_INFO;
+ else if (strButton == "display")
+ buttonCode = XINPUT_IR_REMOTE_DISPLAY;
+ else if (strButton == "title")
+ buttonCode = XINPUT_IR_REMOTE_TITLE;
+ else if (strButton == "play")
+ buttonCode = XINPUT_IR_REMOTE_PLAY;
+ else if (strButton == "pause")
+ buttonCode = XINPUT_IR_REMOTE_PAUSE;
+ else if (strButton == "reverse")
+ buttonCode = XINPUT_IR_REMOTE_REVERSE;
+ else if (strButton == "forward")
+ buttonCode = XINPUT_IR_REMOTE_FORWARD;
+ else if (strButton == "skipplus")
+ buttonCode = XINPUT_IR_REMOTE_SKIP_PLUS;
+ else if (strButton == "skipminus")
+ buttonCode = XINPUT_IR_REMOTE_SKIP_MINUS;
+ else if (strButton == "stop")
+ buttonCode = XINPUT_IR_REMOTE_STOP;
+ else if (strButton == "zero")
+ buttonCode = XINPUT_IR_REMOTE_0;
+ else if (strButton == "one")
+ buttonCode = XINPUT_IR_REMOTE_1;
+ else if (strButton == "two")
+ buttonCode = XINPUT_IR_REMOTE_2;
+ else if (strButton == "three")
+ buttonCode = XINPUT_IR_REMOTE_3;
+ else if (strButton == "four")
+ buttonCode = XINPUT_IR_REMOTE_4;
+ else if (strButton == "five")
+ buttonCode = XINPUT_IR_REMOTE_5;
+ else if (strButton == "six")
+ buttonCode = XINPUT_IR_REMOTE_6;
+ else if (strButton == "seven")
+ buttonCode = XINPUT_IR_REMOTE_7;
+ else if (strButton == "eight")
+ buttonCode = XINPUT_IR_REMOTE_8;
+ else if (strButton == "nine")
+ buttonCode = XINPUT_IR_REMOTE_9;
+ // Additional keys from the media center extender for xbox remote
+ else if (strButton == "power")
+ buttonCode = XINPUT_IR_REMOTE_POWER;
+ else if (strButton == "mytv")
+ buttonCode = XINPUT_IR_REMOTE_MY_TV;
+ else if (strButton == "mymusic")
+ buttonCode = XINPUT_IR_REMOTE_MY_MUSIC;
+ else if (strButton == "mypictures")
+ buttonCode = XINPUT_IR_REMOTE_MY_PICTURES;
+ else if (strButton == "myvideo")
+ buttonCode = XINPUT_IR_REMOTE_MY_VIDEOS;
+ else if (strButton == "record")
+ buttonCode = XINPUT_IR_REMOTE_RECORD;
+ else if (strButton == "start")
+ buttonCode = XINPUT_IR_REMOTE_START;
+ else if (strButton == "volumeplus")
+ buttonCode = XINPUT_IR_REMOTE_VOLUME_PLUS;
+ else if (strButton == "volumeminus")
+ buttonCode = XINPUT_IR_REMOTE_VOLUME_MINUS;
+ else if (strButton == "channelplus")
+ buttonCode = XINPUT_IR_REMOTE_CHANNEL_PLUS;
+ else if (strButton == "channelminus")
+ buttonCode = XINPUT_IR_REMOTE_CHANNEL_MINUS;
+ else if (strButton == "pageplus")
+ buttonCode = XINPUT_IR_REMOTE_CHANNEL_PLUS;
+ else if (strButton == "pageminus")
+ buttonCode = XINPUT_IR_REMOTE_CHANNEL_MINUS;
+ else if (strButton == "mute")
+ buttonCode = XINPUT_IR_REMOTE_MUTE;
+ else if (strButton == "recordedtv")
+ buttonCode = XINPUT_IR_REMOTE_RECORDED_TV;
+ else if (strButton == "guide")
+ buttonCode = XINPUT_IR_REMOTE_GUIDE;
+ else if (strButton == "livetv")
+ buttonCode = XINPUT_IR_REMOTE_LIVE_TV;
+ else if (strButton == "liveradio")
+ buttonCode = XINPUT_IR_REMOTE_LIVE_RADIO;
+ else if (strButton == "epgsearch")
+ buttonCode = XINPUT_IR_REMOTE_EPG_SEARCH;
+ else if (strButton == "star")
+ buttonCode = XINPUT_IR_REMOTE_STAR;
+ else if (strButton == "hash")
+ buttonCode = XINPUT_IR_REMOTE_HASH;
+ else if (strButton == "clear")
+ buttonCode = XINPUT_IR_REMOTE_CLEAR;
+ else if (strButton == "enter")
+ buttonCode = XINPUT_IR_REMOTE_ENTER;
+ else if (strButton == "xbox")
+ buttonCode = XINPUT_IR_REMOTE_DISPLAY; // Same as display
+ else if (strButton == "playlist")
+ buttonCode = XINPUT_IR_REMOTE_PLAYLIST;
+ else if (strButton == "teletext")
+ buttonCode = XINPUT_IR_REMOTE_TELETEXT;
+ else if (strButton == "red")
+ buttonCode = XINPUT_IR_REMOTE_RED;
+ else if (strButton == "green")
+ buttonCode = XINPUT_IR_REMOTE_GREEN;
+ else if (strButton == "yellow")
+ buttonCode = XINPUT_IR_REMOTE_YELLOW;
+ else if (strButton == "blue")
+ buttonCode = XINPUT_IR_REMOTE_BLUE;
+ else if (strButton == "subtitle")
+ buttonCode = XINPUT_IR_REMOTE_SUBTITLE;
+ else if (strButton == "language")
+ buttonCode = XINPUT_IR_REMOTE_LANGUAGE;
+ else if (strButton == "eject")
+ buttonCode = XINPUT_IR_REMOTE_EJECT;
+ else if (strButton == "contentsmenu")
+ buttonCode = XINPUT_IR_REMOTE_CONTENTS_MENU;
+ else if (strButton == "rootmenu")
+ buttonCode = XINPUT_IR_REMOTE_ROOT_MENU;
+ else if (strButton == "topmenu")
+ buttonCode = XINPUT_IR_REMOTE_TOP_MENU;
+ else if (strButton == "dvdmenu")
+ buttonCode = XINPUT_IR_REMOTE_DVD_MENU;
+ else if (strButton == "print")
+ buttonCode = XINPUT_IR_REMOTE_PRINT;
+ else
+ CLog::Log(LOGERROR, "Remote Translator: Can't find button {}", strButton);
+ return buttonCode;
+}
+
+uint32_t CIRTranslator::TranslateUniversalRemoteString(const std::string& szButton)
+{
+ if (szButton.empty() || szButton.length() < 4 || StringUtils::CompareNoCase(szButton, "obc", 3))
+ return 0;
+
+ const char* szCode = szButton.c_str() + 3;
+
+ // Button Code is 255 - OBC (Original Button Code) of the button
+ uint32_t buttonCode = 255 - atol(szCode);
+ if (buttonCode > 255)
+ buttonCode = 0;
+
+ return buttonCode;
+}
diff --git a/xbmc/input/IRTranslator.h b/xbmc/input/IRTranslator.h
new file mode 100644
index 0000000..d32363e
--- /dev/null
+++ b/xbmc/input/IRTranslator.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017-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 <map>
+#include <memory>
+#include <string>
+
+class TiXmlNode;
+
+class CIRTranslator
+{
+public:
+ CIRTranslator() = default;
+
+ /*!
+ * \brief Loads Lircmap.xml/IRSSmap.xml
+ */
+ void Load(const std::string& irMapName);
+
+ /*!
+ * \brief Clears the map
+ */
+ void Clear();
+
+ unsigned int TranslateButton(const std::string& szDevice, const std::string& szButton);
+
+ static uint32_t TranslateString(std::string strButton);
+ static uint32_t TranslateUniversalRemoteString(const std::string& szButton);
+
+private:
+ bool LoadIRMap(const std::string& irMapPath);
+ void MapRemote(TiXmlNode* pRemote, const std::string& szDevice);
+
+ using IRButtonMap = std::map<std::string, std::string>;
+
+ std::map<std::string, std::shared_ptr<IRButtonMap>> m_irRemotesMap;
+};
diff --git a/xbmc/input/InertialScrollingHandler.cpp b/xbmc/input/InertialScrollingHandler.cpp
new file mode 100644
index 0000000..1248722
--- /dev/null
+++ b/xbmc/input/InertialScrollingHandler.cpp
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2011-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 "InertialScrollingHandler.h"
+
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "input/touch/generic/GenericTouchInputHandler.h"
+#include "utils/TimeUtils.h"
+#include "windowing/WinSystem.h"
+
+#include <cmath>
+#include <numeric>
+
+// time for reaching velocity 0 in secs
+#define TIME_TO_ZERO_SPEED 1.0f
+// minimum speed for doing inertial scroll is 100 pixels / s
+#define MINIMUM_SPEED_FOR_INERTIA 200
+// maximum speed for reducing time to zero
+#define MAXIMUM_SPEED_FOR_REDUCTION 750
+// maximum time between last movement and gesture end in ms to consider as moving
+#define MAXIMUM_DELAY_FOR_INERTIA 200
+
+CInertialScrollingHandler::CInertialScrollingHandler() : m_iLastGesturePoint(CPoint(0, 0))
+{
+}
+
+unsigned int CInertialScrollingHandler::PanPoint::TimeElapsed() const
+{
+ return CTimeUtils::GetFrameTime() - time;
+}
+
+bool CInertialScrollingHandler::CheckForInertialScrolling(const CAction* action)
+{
+ bool ret = false; // return value - false no inertial scrolling - true - inertial scrolling
+
+ if (CServiceBroker::GetWinSystem()->HasInertialGestures())
+ {
+ return ret; // no need for emulating inertial scrolling - windowing does support it natively.
+ }
+
+ // reset screensaver during pan
+ if (action->GetID() == ACTION_GESTURE_PAN)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (appPower)
+ appPower->ResetScreenSaver();
+ if (!m_bScrolling)
+ {
+ m_panPoints.emplace_back(CTimeUtils::GetFrameTime(),
+ CVector{action->GetAmount(4), action->GetAmount(5)});
+ }
+ return false;
+ }
+
+ // mouse click aborts scrolling
+ if (m_bScrolling && action->GetID() == ACTION_MOUSE_LEFT_CLICK)
+ {
+ ret = true;
+ m_bAborting = true; // lets abort
+ }
+
+ // trim saved pan points to time range that qualifies for inertial scrolling
+ while (!m_panPoints.empty() && m_panPoints.front().TimeElapsed() > MAXIMUM_DELAY_FOR_INERTIA)
+ m_panPoints.pop_front();
+
+ // on begin/tap stop all inertial scrolling
+ if (action->GetID() == ACTION_GESTURE_BEGIN)
+ {
+ // release any former exclusive mouse mode
+ // for making switching between multiple lists
+ // possible
+ CGUIMessage message(GUI_MSG_EXCLUSIVE_MOUSE, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+ m_bScrolling = false;
+ // wakeup screensaver on pan begin
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+ appPower->WakeUpScreenSaverAndDPMS();
+ }
+ else if (action->GetID() == ACTION_GESTURE_END &&
+ !m_panPoints.empty()) // do we need to animate inertial scrolling?
+ {
+ // Calculate velocity in the last MAXIMUM_DELAY_FOR_INERTIA milliseconds.
+ // Do not use the velocity given by the ACTION_GESTURE_END data - it is calculated
+ // for the whole duration of the touch and thus useless for inertia. The user
+ // may scroll around for a few seconds and then only at the end flick in one
+ // direction. Only the last flick should be relevant here.
+ auto velocitySum =
+ std::accumulate(m_panPoints.cbegin(), m_panPoints.cend(), CVector{},
+ [](CVector val, PanPoint const& p) { return val + p.velocity; });
+ auto velocityX = velocitySum.x / m_panPoints.size();
+ auto velocityY = velocitySum.y / m_panPoints.size();
+
+ m_timeToZero = TIME_TO_ZERO_SPEED;
+ auto velocityMax = std::max(std::abs(velocityX), std::abs(velocityY));
+#ifdef TARGET_DARWIN_OSX
+ float dpiScale = 1.0;
+#else
+ float dpiScale = CGenericTouchInputHandler::GetInstance().GetScreenDPI() / 160.0f;
+#endif
+ if (velocityMax > MINIMUM_SPEED_FOR_INERTIA * dpiScale)
+ {
+ if (velocityMax < MAXIMUM_SPEED_FOR_REDUCTION * dpiScale)
+ m_timeToZero = (m_timeToZero * velocityMax) / (MAXIMUM_SPEED_FOR_REDUCTION * dpiScale);
+
+ bool inertialRequested = false;
+ CGUIMessage message(GUI_MSG_GESTURE_NOTIFY, 0, 0, static_cast<int>(velocityX),
+ static_cast<int>(velocityY));
+
+ // ask if the control wants inertial scrolling
+ if (CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message))
+ {
+ int result = 0;
+ if (message.GetPointer())
+ {
+ int* p = static_cast<int*>(message.GetPointer());
+ message.SetPointer(nullptr);
+ result = *p;
+ delete p;
+ }
+ if (result == EVENT_RESULT_PAN_HORIZONTAL || result == EVENT_RESULT_PAN_VERTICAL)
+ {
+ inertialRequested = true;
+ }
+ }
+
+ if (inertialRequested)
+ {
+ m_iFlickVelocity.x = velocityX; // in pixels per sec
+ m_iFlickVelocity.y = velocityY; // in pixels per sec
+ m_iLastGesturePoint.x = action->GetAmount(2); // last gesture point x
+ m_iLastGesturePoint.y = action->GetAmount(3); // last gesture point y
+
+ // calc deacceleration for fullstop in TIME_TO_ZERO_SPEED secs
+ // v = a*t + v0 -> set v = 0 because we want to stop scrolling
+ // a = -v0 / t
+ m_inertialDeacceleration.x = -1 * m_iFlickVelocity.x / m_timeToZero;
+ m_inertialDeacceleration.y = -1 * m_iFlickVelocity.y / m_timeToZero;
+
+ m_inertialStartTime = CTimeUtils::GetFrameTime(); // start time of inertial scrolling
+ ret = true;
+ m_bScrolling = true; // activate the inertial scrolling animation
+ }
+ }
+ }
+
+ if (action->GetID() == ACTION_GESTURE_BEGIN || action->GetID() == ACTION_GESTURE_END ||
+ action->GetID() == ACTION_GESTURE_ABORT)
+ {
+ m_panPoints.clear();
+ }
+
+ return ret;
+}
+
+bool CInertialScrollingHandler::ProcessInertialScroll(float frameTime)
+{
+ // do inertial scroll animation by sending gesture_pan
+ if (m_bScrolling)
+ {
+ float xMovement = 0.0;
+ float yMovement = 0.0;
+
+ // decrease based on negative acceleration
+ // calc the overall inertial scrolling time in secs
+ float absoluteInertialTime = (CTimeUtils::GetFrameTime() - m_inertialStartTime) / (float)1000;
+
+ // as long as we aren't over the overall inertial scroll time - do the deacceleration
+ if (absoluteInertialTime < m_timeToZero)
+ {
+ // v = s/t -> s = t * v
+ xMovement = frameTime * m_iFlickVelocity.x;
+ yMovement = frameTime * m_iFlickVelocity.y;
+
+ // save new gesture point
+ m_iLastGesturePoint.x += xMovement;
+ m_iLastGesturePoint.y += yMovement;
+
+ // fire the pan action
+ if (!g_application.OnAction(CAction(ACTION_GESTURE_PAN, 0, m_iLastGesturePoint.x,
+ m_iLastGesturePoint.y, xMovement, yMovement,
+ m_iFlickVelocity.x, m_iFlickVelocity.y)))
+ {
+ m_bAborting = true; // we are done
+ }
+
+ // calc new velocity based on deacceleration
+ // v = a*t + v0
+ m_iFlickVelocity.x = m_inertialDeacceleration.x * frameTime + m_iFlickVelocity.x;
+ m_iFlickVelocity.y = m_inertialDeacceleration.y * frameTime + m_iFlickVelocity.y;
+
+ // check if the signs are equal - which would mean we deaccelerated to long and reversed the
+ // direction
+ if ((m_inertialDeacceleration.x < 0) == (m_iFlickVelocity.x < 0))
+ {
+ m_iFlickVelocity.x = 0;
+ }
+ if ((m_inertialDeacceleration.y < 0) == (m_iFlickVelocity.y < 0))
+ {
+ m_iFlickVelocity.y = 0;
+ }
+ }
+ else // no movement -> done
+ {
+ m_bAborting = true; // we are done
+ }
+ }
+
+ // if we are done - or we where aborted
+ if (m_bAborting)
+ {
+ // fire gesture end action
+ g_application.OnAction(CAction(ACTION_GESTURE_END, 0, 0.0f, 0.0f, 0.0f, 0.0f));
+ m_bAborting = false;
+ m_bScrolling = false; // stop scrolling
+ m_iFlickVelocity.x = 0;
+ m_iFlickVelocity.y = 0;
+ }
+
+ return true;
+}
diff --git a/xbmc/input/InertialScrollingHandler.h b/xbmc/input/InertialScrollingHandler.h
new file mode 100644
index 0000000..cff8516
--- /dev/null
+++ b/xbmc/input/InertialScrollingHandler.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011-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 "utils/Geometry.h"
+#include "utils/Vector.h"
+
+#include <deque>
+
+class CApplication;
+class CAction;
+
+class CInertialScrollingHandler
+{
+ friend class CApplication;
+
+public:
+ CInertialScrollingHandler();
+
+ bool IsScrolling() { return m_bScrolling; }
+
+private:
+ bool CheckForInertialScrolling(const CAction* action);
+ bool ProcessInertialScroll(float frameTime);
+
+ /*
+ * vars for inertial scrolling animation with gestures
+ */
+
+ // flag indicating that we currently do the inertial scrolling emulation
+ bool m_bScrolling = false;
+
+ // flag indicating an abort of scrolling
+ bool m_bAborting = false;
+
+ CVector m_iFlickVelocity;
+
+ struct PanPoint
+ {
+ unsigned int time;
+ CVector velocity;
+ PanPoint(unsigned int time, CVector velocity) : time(time), velocity(velocity) {}
+ unsigned int TimeElapsed() const;
+ };
+ std::deque<PanPoint> m_panPoints;
+ CPoint m_iLastGesturePoint;
+ CVector m_inertialDeacceleration;
+ unsigned int m_inertialStartTime = 0;
+ float m_timeToZero = 0.0f;
+};
diff --git a/xbmc/input/InputCodingTable.h b/xbmc/input/InputCodingTable.h
new file mode 100644
index 0000000..a071356
--- /dev/null
+++ b/xbmc/input/InputCodingTable.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 <memory>
+#include <string>
+#include <vector>
+
+class IInputCodingTable
+{
+public:
+ enum
+ {
+ TYPE_WORD_LIST,
+ TYPE_CONVERT_STRING
+ };
+ virtual int GetType() { return TYPE_WORD_LIST; }
+
+ virtual ~IInputCodingTable() = default;
+ /*! \brief Called for the active keyboard layout when it's loaded, stick any initialization here
+
+ This won't be needed for most implementations so we don't set it =0 but provide a default
+ implementation.
+ */
+ virtual void Initialize() {}
+
+ /*! \brief Called for the active keyboard layout when it's unloaded, stick any cleanup here
+
+ This won't be needed for most implementations so we don't set it =0 but provide a default
+ implementation.
+ */
+ virtual void Deinitialize() {}
+
+ /*! \brief Can be overridden if initialization is expensive to avoid calling initialize more than
+ needed
+
+ \return true if initialization has been done and was successful, false otherwise.
+ */
+ virtual bool IsInitialized() const { return true; }
+ virtual bool GetWordListPage(const std::string& strCode, bool isFirstPage) = 0;
+ virtual std::vector<std::wstring> GetResponse(int response) = 0;
+ const std::string& GetCodeChars() const { return m_codechars; }
+
+ virtual void SetTextPrev(const std::string& strTextPrev) {}
+ virtual std::string ConvertString(const std::string& strCode) { return std::string(""); }
+
+protected:
+ std::string m_codechars;
+};
+
+typedef std::shared_ptr<IInputCodingTable> IInputCodingTablePtr;
diff --git a/xbmc/input/InputCodingTableBasePY.cpp b/xbmc/input/InputCodingTableBasePY.cpp
new file mode 100644
index 0000000..0ac41a3
--- /dev/null
+++ b/xbmc/input/InputCodingTableBasePY.cpp
@@ -0,0 +1,1314 @@
+/*
+ * 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 "InputCodingTableBasePY.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/CharsetConverter.h"
+
+#include <stdlib.h>
+
+static std::map<std::string, std::wstring> codemap = {
+ {"a", // L"啊阿吖嗄腌锕"
+ {0x554a, 0x963f, 0x5416, 0x55c4, 0x814c, 0x9515}},
+ {"ai", // L"爱矮挨哎碍癌艾唉哀蔼隘埃皑嗌嫒瑷暧捱砹嗳锿霭"
+ {0x7231, 0x77ee, 0x6328, 0x54ce, 0x788d, 0x764c, 0x827e, 0x5509, 0x54c0, 0x853c, 0x9698,
+ 0x57c3, 0x7691, 0x55cc, 0x5ad2, 0x7477, 0x66a7, 0x6371, 0x7839, 0x55f3, 0x953f, 0x972d}},
+ {"an", // L"按安暗岸俺案鞍氨胺庵揞犴铵桉谙鹌埯黯"
+ {0x6309, 0x5b89, 0x6697, 0x5cb8, 0x4ffa, 0x6848, 0x978d, 0x6c28, 0x80fa, 0x5eb5, 0x63de,
+ 0x72b4, 0x94f5, 0x6849, 0x8c19, 0x9e4c, 0x57ef, 0x9eef}},
+ {"ang", // L"昂肮盎"
+ {0x6602, 0x80ae, 0x76ce}},
+ {"ao", // L"袄凹傲奥熬懊敖翱澳拗媪廒骜嗷坳遨聱螯獒鏊鳌鏖岙"
+ {0x8884, 0x51f9, 0x50b2, 0x5965, 0x71ac, 0x61ca, 0x6556, 0x7ff1,
+ 0x6fb3, 0x62d7, 0x5aaa, 0x5ed2, 0x9a9c, 0x55f7, 0x5773, 0x9068,
+ 0x8071, 0x87af, 0x7352, 0x93ca, 0x9ccc, 0x93d6, 0x5c99}},
+ {"ba", // L"把八吧爸拔罢跋巴芭扒坝霸叭靶笆疤耙捌粑茇岜鲅钯魃菝灞"
+ {0x628a, 0x516b, 0x5427, 0x7238, 0x62d4, 0x7f62, 0x8dcb, 0x5df4, 0x82ad,
+ 0x6252, 0x575d, 0x9738, 0x53ed, 0x9776, 0x7b06, 0x75a4, 0x8019, 0x634c,
+ 0x7c91, 0x8307, 0x5c9c, 0x9c85, 0x94af, 0x9b43, 0x83dd, 0x705e}},
+ {"bai", // L"百白摆败柏拜佰伯稗捭掰"
+ {0x767e, 0x767d, 0x6446, 0x8d25, 0x67cf, 0x62dc, 0x4f70, 0x4f2f, 0x7a17, 0x636d, 0x63b0}},
+ {"ban", // L"半办班般拌搬版斑板伴扳扮瓣颁绊癍坂钣舨阪瘢"
+ {0x534a, 0x529e, 0x73ed, 0x822c, 0x62cc, 0x642c, 0x7248, 0x6591, 0x677f, 0x4f34, 0x6273,
+ 0x626e, 0x74e3, 0x9881, 0x7eca, 0x764d, 0x5742, 0x94a3, 0x8228, 0x962a, 0x7622}},
+ {"bang", // L"帮棒绑磅镑邦榜蚌傍梆膀谤浜蒡"
+ {0x5e2e, 0x68d2, 0x7ed1, 0x78c5, 0x9551, 0x90a6, 0x699c, 0x868c, 0x508d, 0x6886, 0x8180,
+ 0x8c24, 0x6d5c, 0x84a1}},
+ {"bao", // L"包抱报饱保暴薄宝爆剥豹刨雹褒堡苞胞鲍炮龅孢煲褓鸨趵葆勹"
+ {0x5305, 0x62b1, 0x62a5, 0x9971, 0x4fdd, 0x66b4, 0x8584, 0x5b9d, 0x7206,
+ 0x5265, 0x8c79, 0x5228, 0x96f9, 0x8912, 0x5821, 0x82de, 0x80de, 0x9c8d,
+ 0x70ae, 0x9f85, 0x5b62, 0x7172, 0x8913, 0x9e28, 0x8db5, 0x8446, 0x52f9}},
+ {"bei", //L"被北倍杯背悲备碑卑贝辈钡焙狈惫臂褙悖蓓鹎鐾呗邶鞴孛陂碚埤萆"
+ {0x88ab, 0x5317, 0x500d, 0x676f, 0x80cc, 0x60b2, 0x5907, 0x7891, 0x5351, 0x8d1d,
+ 0x8f88, 0x94a1, 0x7119, 0x72c8, 0x60eb, 0x81c2, 0x8919, 0x6096, 0x84d3, 0x9e4e,
+ 0x943e, 0x5457, 0x90b6, 0x97b4, 0x5b5b, 0x9642, 0x789a, 0x57e4, 0x8406}},
+ {"ben", //L"本奔苯笨锛贲畚坌"
+ {0x672c, 0x5954, 0x82ef, 0x7b28, 0x951b, 0x8d32, 0x755a, 0x574c}},
+ {"beng", //L"蹦绷甭崩迸蚌泵甏嘣堋"
+ {0x8e66, 0x7ef7, 0x752d, 0x5d29, 0x8ff8, 0x868c, 0x6cf5, 0x750f, 0x5623, 0x580b}},
+ {"bi", // L"比笔闭鼻碧必避逼毕臂彼鄙壁蓖币弊辟蔽毙庇敝陛毖痹秘泌秕薜荸芘萆匕裨畀俾嬖狴筚箅篦舭荜襞庳铋跸吡愎贲滗濞璧哔髀弼妣婢埤"
+ {0x6bd4, 0x7b14, 0x95ed, 0x9f3b, 0x78a7, 0x5fc5, 0x907f, 0x903c, 0x6bd5, 0x81c2,
+ 0x5f7c, 0x9119, 0x58c1, 0x84d6, 0x5e01, 0x5f0a, 0x8f9f, 0x853d, 0x6bd9, 0x5e87,
+ 0x655d, 0x965b, 0x6bd6, 0x75f9, 0x79d8, 0x6ccc, 0x79d5, 0x859c, 0x8378, 0x8298,
+ 0x8406, 0x5315, 0x88e8, 0x7540, 0x4ffe, 0x5b16, 0x72f4, 0x7b5a, 0x7b85, 0x7be6,
+ 0x822d, 0x835c, 0x895e, 0x5eb3, 0x94cb, 0x8df8, 0x5421, 0x610e, 0x8d32, 0x6ed7,
+ 0x6fde, 0x74a7, 0x54d4, 0x9ac0, 0x5f3c, 0x59a3, 0x5a62, 0x57e4}},
+ {"bian", // L"边变便遍编辩扁贬鞭卞辨辫忭砭匾汴碥蝙褊鳊笾苄窆弁缏煸"
+ {0x8fb9, 0x53d8, 0x4fbf, 0x904d, 0x7f16, 0x8fa9, 0x6241, 0x8d2c, 0x97ad,
+ 0x535e, 0x8fa8, 0x8fab, 0x5fed, 0x782d, 0x533e, 0x6c74, 0x78a5, 0x8759,
+ 0x890a, 0x9cca, 0x7b3e, 0x82c4, 0x7a86, 0x5f01, 0x7f0f, 0x7178}},
+ {"biao", // L"表标彪膘杓婊飑飙鳔瘭飚镳裱骠镖髟"
+ {0x8868, 0x6807, 0x5f6a, 0x8198, 0x6753, 0x5a4a, 0x98d1, 0x98d9, 0x9cd4, 0x762d, 0x98da,
+ 0x9573, 0x88f1, 0x9aa0, 0x9556, 0x9adf}},
+ {"bie", // L"别憋鳖瘪蹩"
+ {0x522b, 0x618b, 0x9cd6, 0x762a, 0x8e69}},
+ {"bin", // L"宾濒摈彬斌滨豳膑殡缤髌傧槟鬓镔玢"
+ {0x5bbe, 0x6fd2, 0x6448, 0x5f6c, 0x658c, 0x6ee8, 0x8c73, 0x8191, 0x6ba1, 0x7f24, 0x9acc,
+ 0x50a7, 0x69df, 0x9b13, 0x9554, 0x73a2}},
+ {"bing", // L"并病兵冰丙饼屏秉柄炳摒槟禀邴"
+ {0x5e76, 0x75c5, 0x5175, 0x51b0, 0x4e19, 0x997c, 0x5c4f, 0x79c9, 0x67c4, 0x70b3, 0x6452,
+ 0x69df, 0x7980, 0x90b4}},
+ {"bo", // L"拨波播泊博伯驳玻剥薄勃菠钵搏脖帛柏舶渤铂箔膊卜礴跛檗亳鹁踣啵蕃簸钹饽擘孛百趵"
+ {0x62e8, 0x6ce2, 0x64ad, 0x6cca, 0x535a, 0x4f2f, 0x9a73, 0x73bb, 0x5265, 0x8584,
+ 0x52c3, 0x83e0, 0x94b5, 0x640f, 0x8116, 0x5e1b, 0x67cf, 0x8236, 0x6e24, 0x94c2,
+ 0x7b94, 0x818a, 0x535c, 0x7934, 0x8ddb, 0x6a97, 0x4eb3, 0x9e41, 0x8e23, 0x5575,
+ 0x8543, 0x7c38, 0x94b9, 0x997d, 0x64d8, 0x5b5b, 0x767e, 0x8db5}},
+ {"bu", // L"不步补布部捕卜簿哺堡埠怖埔瓿逋晡钸钚醭卟"
+ {0x4e0d, 0x6b65, 0x8865, 0x5e03, 0x90e8, 0x6355, 0x535c, 0x7c3f, 0x54fa, 0x5821,
+ 0x57e0, 0x6016, 0x57d4, 0x74ff, 0x900b, 0x6661, 0x94b8, 0x949a, 0x91ad, 0x535f}},
+ {"ca", // L"擦礤嚓"
+ {0x64e6, 0x7924, 0x5693}},
+ {"cai", // L"才菜采材财裁猜踩睬蔡彩"
+ {0x624d, 0x83dc, 0x91c7, 0x6750, 0x8d22, 0x88c1, 0x731c, 0x8e29, 0x776c, 0x8521, 0x5f69}},
+ {"can", // L"蚕残掺参惨惭餐灿骖璨孱黪粲"
+ {0x8695, 0x6b8b, 0x63ba, 0x53c2, 0x60e8, 0x60ed, 0x9910, 0x707f, 0x9a96, 0x74a8, 0x5b71,
+ 0x9eea, 0x7cb2}},
+ {"cang", // L"藏仓沧舱苍伧"
+ {0x85cf, 0x4ed3, 0x6ca7, 0x8231, 0x82cd, 0x4f27}},
+ {"cao", // L"草操曹槽糙嘈艚螬漕艹"
+ {0x8349, 0x64cd, 0x66f9, 0x69fd, 0x7cd9, 0x5608, 0x825a, 0x87ac, 0x6f15, 0x8279}},
+ {"ce", // L"册侧策测厕恻"
+ {0x518c, 0x4fa7, 0x7b56, 0x6d4b, 0x5395, 0x607b}},
+ {"cen", // L"参岑涔"
+ {0x53c2, 0x5c91, 0x6d94}},
+ {"ceng", // L"曾层蹭噌"
+ {0x66fe, 0x5c42, 0x8e6d, 0x564c}},
+ {"cha", // L"查插叉茶差岔搽察茬碴刹诧楂槎镲衩汊馇檫姹杈锸嚓猹"
+ {0x67e5, 0x63d2, 0x53c9, 0x8336, 0x5dee, 0x5c94, 0x643d, 0x5bdf,
+ 0x832c, 0x78b4, 0x5239, 0x8be7, 0x6942, 0x69ce, 0x9572, 0x8869,
+ 0x6c4a, 0x9987, 0x6aab, 0x59f9, 0x6748, 0x9538, 0x5693, 0x7339}},
+ {"chai", // L"柴拆差豺钗瘥虿侪龇"
+ {0x67f4, 0x62c6, 0x5dee, 0x8c7a, 0x9497, 0x7625, 0x867f, 0x4faa, 0x9f87}},
+ {"chan", // L"产缠掺搀阐颤铲谗蝉单馋觇婵蒇谄冁廛孱蟾羼镡忏潺禅躔澶"
+ {0x4ea7, 0x7f20, 0x63ba, 0x6400, 0x9610, 0x98a4, 0x94f2, 0x8c17, 0x8749,
+ 0x5355, 0x998b, 0x89c7, 0x5a75, 0x8487, 0x8c04, 0x5181, 0x5edb, 0x5b71,
+ 0x87fe, 0x7fbc, 0x9561, 0x5fcf, 0x6f7a, 0x7985, 0x8e94, 0x6fb6}},
+ {"chang", // L"长唱常场厂尝肠畅昌敞倡偿猖鲳氅菖惝嫦徜鬯阊怅伥昶苌娼"
+ {0x957f, 0x5531, 0x5e38, 0x573a, 0x5382, 0x5c1d, 0x80a0, 0x7545, 0x660c,
+ 0x655e, 0x5021, 0x507f, 0x7316, 0x9cb3, 0x6c05, 0x83d6, 0x60dd, 0x5ae6,
+ 0x5f9c, 0x9b2f, 0x960a, 0x6005, 0x4f25, 0x6636, 0x82cc, 0x5a3c}},
+ {"chao", // L"朝抄超吵潮巢炒嘲剿绰钞怊耖晁"
+ {0x671d, 0x6284, 0x8d85, 0x5435, 0x6f6e, 0x5de2, 0x7092, 0x5632, 0x527f, 0x7ef0, 0x949e,
+ 0x600a, 0x8016, 0x6641}},
+ {"che", // L"车撤扯掣彻澈坼砗屮"
+ {0x8f66, 0x64a4, 0x626f, 0x63a3, 0x5f7b, 0x6f88, 0x577c, 0x7817, 0x5c6e}},
+ {"chen", // L"趁称辰臣尘晨沉陈衬忱郴榇抻谌碜谶宸龀嗔琛"
+ {0x8d81, 0x79f0, 0x8fb0, 0x81e3, 0x5c18, 0x6668, 0x6c89, 0x9648, 0x886c, 0x5ff1,
+ 0x90f4, 0x6987, 0x62bb, 0x8c0c, 0x789c, 0x8c36, 0x5bb8, 0x9f80, 0x55d4, 0x741b}},
+ {"cheng", // L"成乘盛撑称城程呈诚秤惩逞骋澄橙承塍柽埕铖噌铛酲裎枨蛏丞瞠徵"
+ {0x6210, 0x4e58, 0x76db, 0x6491, 0x79f0, 0x57ce, 0x7a0b, 0x5448, 0x8bda, 0x79e4,
+ 0x60e9, 0x901e, 0x9a8b, 0x6f84, 0x6a59, 0x627f, 0x584d, 0x67fd, 0x57d5, 0x94d6,
+ 0x564c, 0x94db, 0x9172, 0x88ce, 0x67a8, 0x86cf, 0x4e1e, 0x77a0, 0x5fb5}},
+ {"chi", // L"吃尺迟池翅痴赤齿耻持斥侈弛驰炽匙踟坻茌墀饬媸豉褫敕哧瘛蚩啻鸱眵螭篪魑叱彳笞嗤傺"
+ {0x5403, 0x5c3a, 0x8fdf, 0x6c60, 0x7fc5, 0x75f4, 0x8d64, 0x9f7f, 0x803b, 0x6301,
+ 0x65a5, 0x4f88, 0x5f1b, 0x9a70, 0x70bd, 0x5319, 0x8e1f, 0x577b, 0x830c, 0x5880,
+ 0x996c, 0x5ab8, 0x8c49, 0x892b, 0x6555, 0x54e7, 0x761b, 0x86a9, 0x557b, 0x9e31,
+ 0x7735, 0x87ad, 0x7bea, 0x9b51, 0x53f1, 0x5f73, 0x7b1e, 0x55e4, 0x50ba}},
+ {"chong", // L"冲重虫充宠崇种艟忡舂铳憧茺"
+ {0x51b2, 0x91cd, 0x866b, 0x5145, 0x5ba0, 0x5d07, 0x79cd, 0x825f, 0x5fe1, 0x8202, 0x94f3,
+ 0x61a7, 0x833a}},
+ {"chou", // L"抽愁臭仇丑稠绸酬筹踌畴瞅惆俦帱瘳雠"
+ {0x62bd, 0x6101, 0x81ed, 0x4ec7, 0x4e11, 0x7a20, 0x7ef8, 0x916c, 0x7b79, 0x8e0c, 0x7574,
+ 0x7785, 0x60c6, 0x4fe6, 0x5e31, 0x7633, 0x96e0}},
+ {"chu", // L"出处初锄除触橱楚础储畜滁矗搐躇厨雏楮杵刍怵绌亍憷蹰黜蜍樗褚"
+ {0x51fa, 0x5904, 0x521d, 0x9504, 0x9664, 0x89e6, 0x6a71, 0x695a, 0x7840, 0x50a8,
+ 0x755c, 0x6ec1, 0x77d7, 0x6410, 0x8e87, 0x53a8, 0x96cf, 0x696e, 0x6775, 0x520d,
+ 0x6035, 0x7ecc, 0x4e8d, 0x61b7, 0x8e70, 0x9edc, 0x870d, 0x6a17, 0x891a}},
+ {"chuai", // L"揣膪嘬搋踹"
+ {0x63e3, 0x81aa, 0x562c, 0x640b, 0x8e39}},
+ {"chuan", // L"穿船传串川喘椽氚遄钏舡舛巛"
+ {0x7a7f, 0x8239, 0x4f20, 0x4e32, 0x5ddd, 0x5598, 0x693d, 0x6c1a, 0x9044, 0x948f, 0x8221,
+ 0x821b, 0x5ddb}},
+ {"chuang", // L"窗床闯创疮幢怆"
+ {0x7a97, 0x5e8a, 0x95ef, 0x521b, 0x75ae, 0x5e62, 0x6006}},
+ {"chui", // L"吹垂炊锤捶槌棰陲"
+ {0x5439, 0x5782, 0x708a, 0x9524, 0x6376, 0x69cc, 0x68f0, 0x9672}},
+ {"chun", // L"春唇纯蠢醇淳椿蝽莼鹑"
+ {0x6625, 0x5507, 0x7eaf, 0x8822, 0x9187, 0x6df3, 0x693f, 0x877d, 0x83bc, 0x9e51}},
+ {"chuo", // L"戳绰踔啜龊辍辶"
+ {0x6233, 0x7ef0, 0x8e14, 0x555c, 0x9f8a, 0x8f8d, 0x8fb6}},
+ {"ci", // L"次此词瓷慈雌磁辞刺茨伺疵赐差兹呲鹚祠糍粢茈"
+ {0x6b21, 0x6b64, 0x8bcd, 0x74f7, 0x6148, 0x96cc, 0x78c1, 0x8f9e, 0x523a, 0x8328, 0x4f3a,
+ 0x75b5, 0x8d50, 0x5dee, 0x5179, 0x5472, 0x9e5a, 0x7960, 0x7ccd, 0x7ca2, 0x8308}},
+ {"cong", // L"从丛葱匆聪囱琮枞淙璁骢苁"
+ {0x4ece, 0x4e1b, 0x8471, 0x5306, 0x806a, 0x56f1, 0x742e, 0x679e, 0x6dd9, 0x7481, 0x9aa2,
+ 0x82c1}},
+ {"cou", // L"凑楱辏腠"
+ {0x51d1, 0x6971, 0x8f8f, 0x8160}},
+ {"cu", // L"粗醋簇促徂猝蔟蹙酢殂蹴"
+ {0x7c97, 0x918b, 0x7c07, 0x4fc3, 0x5f82, 0x731d, 0x851f, 0x8e59, 0x9162, 0x6b82, 0x8e74}},
+ {"cuan", // L"窜蹿篡攒汆爨镩撺"
+ {0x7a9c, 0x8e7f, 0x7be1, 0x6512, 0x6c46, 0x7228, 0x9569, 0x64ba}},
+ {"cui", // L"催脆摧翠崔淬瘁粹璀啐悴萃毳榱隹"
+ {0x50ac, 0x8106, 0x6467, 0x7fe0, 0x5d14, 0x6dec, 0x7601, 0x7cb9, 0x7480, 0x5550, 0x60b4,
+ 0x8403, 0x6bf3, 0x69b1, 0x96b9}},
+ {"cun", // L"村寸存忖皴"
+ {0x6751, 0x5bf8, 0x5b58, 0x5fd6, 0x76b4}},
+ {"cuo", // L"错撮搓挫措磋嵯厝鹾脞痤蹉瘥锉矬躜"
+ {0x9519, 0x64ae, 0x6413, 0x632b, 0x63aa, 0x78cb, 0x5d6f, 0x539d, 0x9e7e, 0x811e, 0x75e4,
+ 0x8e49, 0x7625, 0x9509, 0x77ec, 0x8e9c}},
+ {"da", // L"大答达打搭瘩笪耷哒褡疸怛靼妲沓嗒鞑"
+ {0x5927, 0x7b54, 0x8fbe, 0x6253, 0x642d, 0x7629, 0x7b2a, 0x8037, 0x54d2, 0x8921, 0x75b8,
+ 0x601b, 0x977c, 0x59b2, 0x6c93, 0x55d2, 0x9791}},
+ {"dai", // L"带代呆戴待袋逮歹贷怠傣大殆呔玳迨岱甙黛骀绐埭"
+ {0x5e26, 0x4ee3, 0x5446, 0x6234, 0x5f85, 0x888b, 0x902e, 0x6b79, 0x8d37, 0x6020, 0x50a3,
+ 0x5927, 0x6b86, 0x5454, 0x73b3, 0x8fe8, 0x5cb1, 0x7519, 0x9edb, 0x9a80, 0x7ed0, 0x57ed}},
+ {"dan", // L"但单蛋担弹掸胆淡丹耽旦氮诞郸惮石疸澹瘅萏殚眈聃箪赕儋啖赡"
+ {0x4f46, 0x5355, 0x86cb, 0x62c5, 0x5f39, 0x63b8, 0x80c6, 0x6de1, 0x4e39, 0x803d,
+ 0x65e6, 0x6c2e, 0x8bde, 0x90f8, 0x60ee, 0x77f3, 0x75b8, 0x6fb9, 0x7605, 0x840f,
+ 0x6b9a, 0x7708, 0x8043, 0x7baa, 0x8d55, 0x510b, 0x5556, 0x8d61}},
+ {"dang", // L"当党挡档荡谠铛宕菪凼裆砀"
+ {0x5f53, 0x515a, 0x6321, 0x6863, 0x8361, 0x8c20, 0x94db, 0x5b95, 0x83ea, 0x51fc, 0x88c6,
+ 0x7800}},
+ {"dao", // L"到道倒刀岛盗稻捣悼导蹈祷帱纛忉焘氘叨刂"
+ {0x5230, 0x9053, 0x5012, 0x5200, 0x5c9b, 0x76d7, 0x7a3b, 0x6363, 0x60bc, 0x5bfc, 0x8e48,
+ 0x7977, 0x5e31, 0x7e9b, 0x5fc9, 0x7118, 0x6c18, 0x53e8, 0x5202}},
+ {"de", // L"的地得德锝"
+ {0x7684, 0x5730, 0x5f97, 0x5fb7, 0x951d}},
+ {"dei", // L"得"
+ {0x5f97}},
+ {"deng", // L"等灯邓登澄瞪凳蹬磴镫噔嶝戥簦"
+ {0x7b49, 0x706f, 0x9093, 0x767b, 0x6f84, 0x77aa, 0x51f3, 0x8e6c, 0x78f4, 0x956b, 0x5654,
+ 0x5d9d, 0x6225, 0x7c26}},
+ {"di", // L"地第底低敌抵滴帝递嫡弟缔堤的涤提笛迪狄翟蒂觌邸谛诋嘀柢骶羝氐棣睇娣荻碲镝坻籴砥"
+ {0x5730, 0x7b2c, 0x5e95, 0x4f4e, 0x654c, 0x62b5, 0x6ef4, 0x5e1d, 0x9012, 0x5ae1,
+ 0x5f1f, 0x7f14, 0x5824, 0x7684, 0x6da4, 0x63d0, 0x7b1b, 0x8fea, 0x72c4, 0x7fdf,
+ 0x8482, 0x89cc, 0x90b8, 0x8c1b, 0x8bcb, 0x5600, 0x67e2, 0x9ab6, 0x7f9d, 0x6c10,
+ 0x68e3, 0x7747, 0x5a23, 0x837b, 0x78b2, 0x955d, 0x577b, 0x7c74, 0x7825}},
+ {"dia", // L"嗲"
+ {0x55f2}},
+ {"dian", // L"点电店殿淀掂颠垫碘惦奠典佃靛滇甸踮钿坫阽癫簟玷巅癜"
+ {0x70b9, 0x7535, 0x5e97, 0x6bbf, 0x6dc0, 0x6382, 0x98a0, 0x57ab, 0x7898,
+ 0x60e6, 0x5960, 0x5178, 0x4f43, 0x975b, 0x6ec7, 0x7538, 0x8e2e, 0x94bf,
+ 0x576b, 0x963d, 0x766b, 0x7c1f, 0x73b7, 0x5dc5, 0x765c}},
+ {"diao", // L"掉钓叼吊雕调刁碉凋铞铫鲷貂"
+ {0x6389, 0x9493, 0x53fc, 0x540a, 0x96d5, 0x8c03, 0x5201, 0x7889, 0x51cb, 0x94de, 0x94eb,
+ 0x9cb7, 0x8c82}},
+ {"die", // L"爹跌叠碟蝶迭谍牒堞瓞揲蹀耋鲽垤喋"
+ {0x7239, 0x8dcc, 0x53e0, 0x789f, 0x8776, 0x8fed, 0x8c0d, 0x7252, 0x581e, 0x74de, 0x63f2,
+ 0x8e40, 0x800b, 0x9cbd, 0x57a4, 0x558b}},
+ {"ding", // L"顶定盯订叮丁钉鼎锭町玎铤腚碇疔仃耵酊啶"
+ {0x9876, 0x5b9a, 0x76ef, 0x8ba2, 0x53ee, 0x4e01, 0x9489, 0x9f0e, 0x952d, 0x753a, 0x738e,
+ 0x94e4, 0x815a, 0x7887, 0x7594, 0x4ec3, 0x8035, 0x914a, 0x5576}},
+ {"diu", // L"丢铥"
+ {0x4e22, 0x94e5}},
+ {"dong", // L"动东懂洞冻冬董栋侗恫峒鸫胨胴硐氡岽咚"
+ {0x52a8, 0x4e1c, 0x61c2, 0x6d1e, 0x51bb, 0x51ac, 0x8463, 0x680b, 0x4f97, 0x606b, 0x5cd2,
+ 0x9e2b, 0x80e8, 0x80f4, 0x7850, 0x6c21, 0x5cbd, 0x549a}},
+ {"dou", // L"都斗豆逗陡抖痘兜蚪窦篼蔸"
+ {0x90fd, 0x6597, 0x8c46, 0x9017, 0x9661, 0x6296, 0x75d8, 0x515c, 0x86aa, 0x7aa6, 0x7bfc,
+ 0x8538}},
+ {"du", // L"读度毒渡堵独肚镀赌睹杜督都犊妒蠹笃嘟渎椟牍黩髑芏"
+ {0x8bfb, 0x5ea6, 0x6bd2, 0x6e21, 0x5835, 0x72ec, 0x809a, 0x9540,
+ 0x8d4c, 0x7779, 0x675c, 0x7763, 0x90fd, 0x728a, 0x5992, 0x8839,
+ 0x7b03, 0x561f, 0x6e0e, 0x691f, 0x724d, 0x9ee9, 0x9ad1, 0x828f}},
+ {"duan", // L"段短断端锻缎椴煅簖"
+ {0x6bb5, 0x77ed, 0x65ad, 0x7aef, 0x953b, 0x7f0e, 0x6934, 0x7145, 0x7c16}},
+ {"dui", // L"对队堆兑碓怼憝"
+ {0x5bf9, 0x961f, 0x5806, 0x5151, 0x7893, 0x603c, 0x619d}},
+ {"dun", // L"吨顿蹲墩敦钝盾囤遁趸沌盹镦礅炖砘"
+ {0x5428, 0x987f, 0x8e72, 0x58a9, 0x6566, 0x949d, 0x76fe, 0x56e4, 0x9041, 0x8db8, 0x6c8c,
+ 0x76f9, 0x9566, 0x7905, 0x7096, 0x7818}},
+ {"duo", // L"多朵夺舵剁垛跺惰堕掇哆驮度躲踱沲咄铎裰哚缍"
+ {0x591a, 0x6735, 0x593a, 0x8235, 0x5241, 0x579b, 0x8dfa, 0x60f0, 0x5815, 0x6387, 0x54c6,
+ 0x9a6e, 0x5ea6, 0x8eb2, 0x8e31, 0x6cb2, 0x5484, 0x94ce, 0x88f0, 0x54da, 0x7f0d}},
+ {"e", // L"饿哦额鹅蛾扼俄讹阿遏峨娥恶厄鄂锇谔垩锷萼苊轭婀莪鳄颚腭愕呃噩鹗屙"
+ {0x997f, 0x54e6, 0x989d, 0x9e45, 0x86fe, 0x627c, 0x4fc4, 0x8bb9, 0x963f, 0x904f, 0x5ce8,
+ 0x5a25, 0x6076, 0x5384, 0x9102, 0x9507, 0x8c14, 0x57a9, 0x9537, 0x843c, 0x82ca, 0x8f6d,
+ 0x5a40, 0x83aa, 0x9cc4, 0x989a, 0x816d, 0x6115, 0x5443, 0x5669, 0x9e57, 0x5c59}},
+ {"ei", // L"诶"
+ {0x8bf6}},
+ {"en", // L"恩摁蒽嗯"
+ {0x6069, 0x6441, 0x84bd, 0x55ef}},
+ {"er", // L"而二耳儿饵尔贰洱珥鲕鸸迩铒"
+ {0x800c, 0x4e8c, 0x8033, 0x513f, 0x9975, 0x5c14, 0x8d30, 0x6d31, 0x73e5, 0x9c95, 0x9e38,
+ 0x8fe9, 0x94d2}},
+ {"fa", // L"发法罚伐乏筏阀珐垡砝"
+ {0x53d1, 0x6cd5, 0x7f5a, 0x4f10, 0x4e4f, 0x7b4f, 0x9600, 0x73d0, 0x57a1, 0x781d}},
+ {"fan", // L"反饭翻番犯凡帆返泛繁烦贩范樊藩矾钒燔蘩畈蕃蹯梵幡"
+ {0x53cd, 0x996d, 0x7ffb, 0x756a, 0x72af, 0x51e1, 0x5e06, 0x8fd4,
+ 0x6cdb, 0x7e41, 0x70e6, 0x8d29, 0x8303, 0x6a0a, 0x85e9, 0x77fe,
+ 0x9492, 0x71d4, 0x8629, 0x7548, 0x8543, 0x8e6f, 0x68b5, 0x5e61}},
+ {"fang", // L"放房防纺芳方访仿坊妨肪钫邡枋舫鲂匚"
+ {0x653e, 0x623f, 0x9632, 0x7eba, 0x82b3, 0x65b9, 0x8bbf, 0x4eff, 0x574a, 0x59a8, 0x80aa,
+ 0x94ab, 0x90a1, 0x678b, 0x822b, 0x9c82, 0x531a}},
+ {"fei", // L"非飞肥费肺废匪吠沸菲诽啡篚蜚腓扉妃斐狒芾悱镄霏翡榧淝鲱绯痱砩"
+ {0x975e, 0x98de, 0x80a5, 0x8d39, 0x80ba, 0x5e9f, 0x532a, 0x5420, 0x6cb8, 0x83f2,
+ 0x8bfd, 0x5561, 0x7bda, 0x871a, 0x8153, 0x6249, 0x5983, 0x6590, 0x72d2, 0x82be,
+ 0x60b1, 0x9544, 0x970f, 0x7fe1, 0x69a7, 0x6ddd, 0x9cb1, 0x7eef, 0x75f1, 0x7829}},
+ {"fen", // L"分份芬粉坟奋愤纷忿粪酚焚吩氛汾棼瀵鲼玢偾鼢贲"
+ {0x5206, 0x4efd, 0x82ac, 0x7c89, 0x575f, 0x594b, 0x6124, 0x7eb7, 0x5fff, 0x7caa, 0x915a,
+ 0x711a, 0x5429, 0x6c1b, 0x6c7e, 0x68fc, 0x7035, 0x9cbc, 0x73a2, 0x507e, 0x9f22, 0x8d32}},
+ {"feng", // L"风封逢缝蜂丰枫疯冯奉讽凤峰锋烽砜俸酆葑沣唪"
+ {0x98ce, 0x5c01, 0x9022, 0x7f1d, 0x8702, 0x4e30, 0x67ab, 0x75af, 0x51af, 0x5949, 0x8bbd,
+ 0x51e4, 0x5cf0, 0x950b, 0x70fd, 0x781c, 0x4ff8, 0x9146, 0x8451, 0x6ca3, 0x552a}},
+ {"fo", // L"佛"
+ {0x4f5b}},
+ {"fou", // L"否缶"
+ {0x5426, 0x7f36}},
+ {"fu", // L"副幅扶浮富福负伏付复服附俯斧赴缚拂夫父符孵敷赋辅府腐腹妇抚覆辐肤氟佛俘傅讣弗涪袱甫釜脯腑阜咐黼砩苻趺跗蚨芾鲋幞茯滏蜉拊菔蝠鳆蝮绂绋赙罘稃匐麸凫桴莩孚馥驸怫祓呋郛芙艴黻哺阝"
+ {0x526f, 0x5e45, 0x6276, 0x6d6e, 0x5bcc, 0x798f, 0x8d1f, 0x4f0f, 0x4ed8, 0x590d, 0x670d,
+ 0x9644, 0x4fef, 0x65a7, 0x8d74, 0x7f1a, 0x62c2, 0x592b, 0x7236, 0x7b26, 0x5b75, 0x6577,
+ 0x8d4b, 0x8f85, 0x5e9c, 0x8150, 0x8179, 0x5987, 0x629a, 0x8986, 0x8f90, 0x80a4, 0x6c1f,
+ 0x4f5b, 0x4fd8, 0x5085, 0x8ba3, 0x5f17, 0x6daa, 0x88b1, 0x752b, 0x91dc, 0x812f, 0x8151,
+ 0x961c, 0x5490, 0x9efc, 0x7829, 0x82fb, 0x8dba, 0x8dd7, 0x86a8, 0x82be, 0x9c8b, 0x5e5e,
+ 0x832f, 0x6ecf, 0x8709, 0x62ca, 0x83d4, 0x8760, 0x9cc6, 0x876e, 0x7ec2, 0x7ecb, 0x8d59,
+ 0x7f58, 0x7a03, 0x5310, 0x9eb8, 0x51eb, 0x6874, 0x83a9, 0x5b5a, 0x99a5, 0x9a78, 0x602b,
+ 0x7953, 0x544b, 0x90db, 0x8299, 0x8274, 0x9efb, 0x54fa, 0x961d}},
+ {"ga", // L"噶夹嘎咖钆伽旮尬尕尜"
+ {0x5676, 0x5939, 0x560e, 0x5496, 0x9486, 0x4f3d, 0x65ee, 0x5c2c, 0x5c15, 0x5c1c}},
+ {"gai", // L"该改盖概钙芥溉戤垓丐陔赅胲"
+ {0x8be5, 0x6539, 0x76d6, 0x6982, 0x9499, 0x82a5, 0x6e89, 0x6224, 0x5793, 0x4e10, 0x9654,
+ 0x8d45, 0x80f2}},
+ {"gan", // L"赶干感敢竿甘肝柑杆赣秆旰酐矸疳泔苷擀绀橄澉淦尴坩"
+ {0x8d76, 0x5e72, 0x611f, 0x6562, 0x7aff, 0x7518, 0x809d, 0x67d1,
+ 0x6746, 0x8d63, 0x79c6, 0x65f0, 0x9150, 0x77f8, 0x75b3, 0x6cd4,
+ 0x82f7, 0x64c0, 0x7ec0, 0x6a44, 0x6f89, 0x6de6, 0x5c34, 0x5769}},
+ {"gang", // L"刚钢纲港缸岗杠冈肛扛筻罡戆"
+ {0x521a, 0x94a2, 0x7eb2, 0x6e2f, 0x7f38, 0x5c97, 0x6760, 0x5188, 0x809b, 0x625b, 0x7b7b,
+ 0x7f61, 0x6206}},
+ {"gao", // L"高搞告稿膏篙羔糕镐皋郜诰杲缟睾槔锆槁藁"
+ {0x9ad8, 0x641e, 0x544a, 0x7a3f, 0x818f, 0x7bd9, 0x7f94, 0x7cd5, 0x9550, 0x768b, 0x90dc,
+ 0x8bf0, 0x6772, 0x7f1f, 0x777e, 0x69d4, 0x9506, 0x69c1, 0x85c1}},
+ {"ge", // L"个各歌割哥搁格阁隔革咯胳葛蛤戈鸽疙盖合铬骼袼塥虼圪镉仡舸鬲嗝膈搿纥哿铪"
+ {0x4e2a, 0x5404, 0x6b4c, 0x5272, 0x54e5, 0x6401, 0x683c, 0x9601, 0x9694,
+ 0x9769, 0x54af, 0x80f3, 0x845b, 0x86e4, 0x6208, 0x9e3d, 0x7599, 0x76d6,
+ 0x5408, 0x94ec, 0x9abc, 0x88bc, 0x5865, 0x867c, 0x572a, 0x9549, 0x4ee1,
+ 0x8238, 0x9b32, 0x55dd, 0x8188, 0x643f, 0x7ea5, 0x54ff, 0x94ea}},
+ {"gei", // L"给"
+ {0x7ed9}},
+ {"gen", // L"跟根哏茛亘艮"
+ {0x8ddf, 0x6839, 0x54cf, 0x831b, 0x4e98, 0x826e}},
+ {"geng", // L"更耕颈梗耿庚羹埂赓鲠哽绠"
+ {0x66f4, 0x8015, 0x9888, 0x6897, 0x803f, 0x5e9a, 0x7fb9, 0x57c2, 0x8d53, 0x9ca0, 0x54fd,
+ 0x7ee0}},
+ {"gong", // L"工公功共弓攻宫供恭拱贡躬巩汞龚肱觥珙蚣廾"
+ {0x5de5, 0x516c, 0x529f, 0x5171, 0x5f13, 0x653b, 0x5bab, 0x4f9b, 0x606d, 0x62f1,
+ 0x8d21, 0x8eac, 0x5de9, 0x6c5e, 0x9f9a, 0x80b1, 0x89e5, 0x73d9, 0x86a3, 0x5efe}},
+ {"gou", // L"够沟狗钩勾购构苟垢岣彀枸鞲觏缑笱诟遘媾篝佝"
+ {0x591f, 0x6c9f, 0x72d7, 0x94a9, 0x52fe, 0x8d2d, 0x6784, 0x82df, 0x57a2, 0x5ca3, 0x5f40,
+ 0x67b8, 0x97b2, 0x89cf, 0x7f11, 0x7b31, 0x8bdf, 0x9058, 0x5abe, 0x7bdd, 0x4f5d}},
+ {"gu", // L"古股鼓谷故孤箍姑顾固雇估咕骨辜沽蛊贾菇梏鸪汩轱崮菰鹄鹘钴臌酤鲴诂牯瞽毂锢牿痼觚蛄罟嘏"
+ {0x53e4, 0x80a1, 0x9f13, 0x8c37, 0x6545, 0x5b64, 0x7b8d, 0x59d1, 0x987e, 0x56fa, 0x96c7,
+ 0x4f30, 0x5495, 0x9aa8, 0x8f9c, 0x6cbd, 0x86ca, 0x8d3e, 0x83c7, 0x688f, 0x9e2a, 0x6c69,
+ 0x8f71, 0x5d2e, 0x83f0, 0x9e44, 0x9e58, 0x94b4, 0x81cc, 0x9164, 0x9cb4, 0x8bc2, 0x726f,
+ 0x77bd, 0x6bc2, 0x9522, 0x727f, 0x75fc, 0x89da, 0x86c4, 0x7f5f, 0x560f}},
+ {"gua", // L"挂刮瓜寡剐褂卦呱胍鸹栝诖"
+ {0x6302, 0x522e, 0x74dc, 0x5be1, 0x5250, 0x8902, 0x5366, 0x5471, 0x80cd, 0x9e39, 0x681d,
+ 0x8bd6}},
+ {"guai", // L"怪拐乖掴"
+ {0x602a, 0x62d0, 0x4e56, 0x63b4}},
+ {"guan", // L"关管官观馆惯罐灌冠贯棺纶盥莞掼涫鳏鹳倌"
+ {0x5173, 0x7ba1, 0x5b98, 0x89c2, 0x9986, 0x60ef, 0x7f50, 0x704c, 0x51a0, 0x8d2f, 0x68fa,
+ 0x7eb6, 0x76e5, 0x839e, 0x63bc, 0x6dab, 0x9ccf, 0x9e73, 0x500c}},
+ {"guang", // L"光广逛桄犷咣胱"
+ {0x5149, 0x5e7f, 0x901b, 0x6844, 0x72b7, 0x54a3, 0x80f1}},
+ {"gui", // L"归贵鬼跪轨规硅桂柜龟诡闺瑰圭刽癸炔庋宄桧刿鳜鲑皈匦妫晷簋炅"
+ {0x5f52, 0x8d35, 0x9b3c, 0x8dea, 0x8f68, 0x89c4, 0x7845, 0x6842, 0x67dc, 0x9f9f,
+ 0x8be1, 0x95fa, 0x7470, 0x572d, 0x523d, 0x7678, 0x7094, 0x5e8b, 0x5b84, 0x6867,
+ 0x523f, 0x9cdc, 0x9c91, 0x7688, 0x5326, 0x59ab, 0x6677, 0x7c0b, 0x7085}},
+ {"gun", // L"滚棍辊鲧衮磙绲"
+ {0x6eda, 0x68cd, 0x8f8a, 0x9ca7, 0x886e, 0x78d9, 0x7ef2}},
+ {"guo", // L"过国果裹锅郭涡埚椁聒馘猓崞掴帼呙虢蜾蝈锞"
+ {0x8fc7, 0x56fd, 0x679c, 0x88f9, 0x9505, 0x90ed, 0x6da1, 0x57da, 0x6901, 0x8052,
+ 0x9998, 0x7313, 0x5d1e, 0x63b4, 0x5e3c, 0x5459, 0x8662, 0x873e, 0x8748, 0x951e}},
+ {"ha", // L"哈蛤铪"
+ {0x54c8, 0x86e4, 0x94ea}},
+ {"hai", // L"还海害咳氦孩骇咳骸亥嗨醢胲"
+ {0x8fd8, 0x6d77, 0x5bb3, 0x54b3, 0x6c26, 0x5b69, 0x9a87, 0x54b3, 0x9ab8, 0x4ea5, 0x55e8,
+ 0x91a2, 0x80f2}},
+ {"han", // L"喊含汗寒汉旱酣韩焊涵函憨翰罕撼捍憾悍邯邗菡撖瀚顸蚶焓颔晗鼾"
+ {0x558a, 0x542b, 0x6c57, 0x5bd2, 0x6c49, 0x65f1, 0x9163, 0x97e9, 0x710a, 0x6db5,
+ 0x51fd, 0x61a8, 0x7ff0, 0x7f55, 0x64bc, 0x634d, 0x61be, 0x608d, 0x90af, 0x9097,
+ 0x83e1, 0x6496, 0x701a, 0x9878, 0x86b6, 0x7113, 0x9894, 0x6657, 0x9f3e}},
+ {"hang", // L"行巷航夯杭吭颃沆绗"
+ {0x884c, 0x5df7, 0x822a, 0x592f, 0x676d, 0x542d, 0x9883, 0x6c86, 0x7ed7}},
+ {"hao", // L"好号浩嚎壕郝毫豪耗貉镐昊颢灏嚆蚝嗥皓蒿濠薅"
+ {0x597d, 0x53f7, 0x6d69, 0x568e, 0x58d5, 0x90dd, 0x6beb, 0x8c6a, 0x8017, 0x8c89, 0x9550,
+ 0x660a, 0x98a2, 0x704f, 0x5686, 0x869d, 0x55e5, 0x7693, 0x84bf, 0x6fe0, 0x8585}},
+ {"he", // L"和喝合河禾核何呵荷贺赫褐盒鹤菏貉阂涸吓嗬劾盍翮阖颌壑诃纥曷蚵"
+ {0x548c, 0x559d, 0x5408, 0x6cb3, 0x79be, 0x6838, 0x4f55, 0x5475, 0x8377, 0x8d3a,
+ 0x8d6b, 0x8910, 0x76d2, 0x9e64, 0x83cf, 0x8c89, 0x9602, 0x6db8, 0x5413, 0x55ec,
+ 0x52be, 0x76cd, 0x7fee, 0x9616, 0x988c, 0x58d1, 0x8bc3, 0x7ea5, 0x66f7, 0x86b5}},
+ {"hei", // L"黑嘿"
+ {0x9ed1, 0x563f}},
+ {"hen", // L"很狠恨痕"
+ {0x5f88, 0x72e0, 0x6068, 0x75d5}},
+ {"heng", // L"横恒哼衡亨桁珩蘅"
+ {0x6a2a, 0x6052, 0x54fc, 0x8861, 0x4ea8, 0x6841, 0x73e9, 0x8605}},
+ {"hong", // L"红轰哄虹洪宏烘鸿弘讧訇蕻闳薨黉荭泓"
+ {0x7ea2, 0x8f70, 0x54c4, 0x8679, 0x6d2a, 0x5b8f, 0x70d8, 0x9e3f, 0x5f18, 0x8ba7, 0x8a07,
+ 0x857b, 0x95f3, 0x85a8, 0x9ec9, 0x836d, 0x6cd3}},
+ {"hou", // L"后厚吼喉侯候猴鲎篌堠後逅糇骺瘊"
+ {0x540e, 0x539a, 0x543c, 0x5589, 0x4faf, 0x5019, 0x7334, 0x9c8e, 0x7bcc, 0x5820, 0x5f8c,
+ 0x9005, 0x7cc7, 0x9aba, 0x760a}},
+ {"hu", // L"湖户呼虎壶互胡护糊弧忽狐蝴葫沪乎核瑚唬鹕冱怙鹱笏戽扈鹘浒祜醐琥囫烀轷瓠煳斛鹄猢惚岵滹觳唿槲虍"
+ {0x6e56, 0x6237, 0x547c, 0x864e, 0x58f6, 0x4e92, 0x80e1, 0x62a4, 0x7cca, 0x5f27,
+ 0x5ffd, 0x72d0, 0x8774, 0x846b, 0x6caa, 0x4e4e, 0x6838, 0x745a, 0x552c, 0x9e55,
+ 0x51b1, 0x6019, 0x9e71, 0x7b0f, 0x623d, 0x6248, 0x9e58, 0x6d52, 0x795c, 0x9190,
+ 0x7425, 0x56eb, 0x70c0, 0x8f77, 0x74e0, 0x7173, 0x659b, 0x9e44, 0x7322, 0x60da,
+ 0x5cb5, 0x6ef9, 0x89f3, 0x553f, 0x69f2, 0x864d}},
+ {"hua", // L"话花化画华划滑哗猾铧桦骅砉"
+ {0x8bdd, 0x82b1, 0x5316, 0x753b, 0x534e, 0x5212, 0x6ed1, 0x54d7, 0x733e, 0x94e7, 0x6866,
+ 0x9a85, 0x7809}},
+ {"huai", // L"坏怀淮槐徊踝"
+ {0x574f, 0x6000, 0x6dee, 0x69d0, 0x5f8a, 0x8e1d}},
+ {"huan", // L"换还唤环患缓欢幻宦涣焕豢桓痪漶獾擐逭鲩郇鬟寰奂锾圜洹萑缳浣垸"
+ {0x6362, 0x8fd8, 0x5524, 0x73af, 0x60a3, 0x7f13, 0x6b22, 0x5e7b, 0x5ba6, 0x6da3,
+ 0x7115, 0x8c62, 0x6853, 0x75ea, 0x6f36, 0x737e, 0x64d0, 0x902d, 0x9ca9, 0x90c7,
+ 0x9b1f, 0x5bf0, 0x5942, 0x953e, 0x571c, 0x6d39, 0x8411, 0x7f33, 0x6d63, 0x57b8}},
+ {"huang", // L"黄慌晃荒簧凰皇谎惶蝗磺恍煌幌隍肓潢篁徨鳇遑癀湟蟥璜"
+ {0x9ec4, 0x614c, 0x6643, 0x8352, 0x7c27, 0x51f0, 0x7687, 0x8c0e, 0x60f6,
+ 0x8757, 0x78fa, 0x604d, 0x714c, 0x5e4c, 0x968d, 0x8093, 0x6f62, 0x7bc1,
+ 0x5fa8, 0x9cc7, 0x9051, 0x7640, 0x6e5f, 0x87e5, 0x749c}},
+ {"hui", // L"回会灰绘挥汇辉毁悔惠晦徽恢秽慧贿蛔讳卉烩诲彗浍蕙喙恚哕晖隳麾诙蟪茴洄咴虺荟缋桧"
+ {0x56de, 0x4f1a, 0x7070, 0x7ed8, 0x6325, 0x6c47, 0x8f89, 0x6bc1, 0x6094, 0x60e0,
+ 0x6666, 0x5fbd, 0x6062, 0x79fd, 0x6167, 0x8d3f, 0x86d4, 0x8bb3, 0x5349, 0x70e9,
+ 0x8bf2, 0x5f57, 0x6d4d, 0x8559, 0x5599, 0x605a, 0x54d5, 0x6656, 0x96b3, 0x9ebe,
+ 0x8bd9, 0x87ea, 0x8334, 0x6d04, 0x54b4, 0x867a, 0x835f, 0x7f0b, 0x6867}},
+ {"hun", // L"混昏荤浑婚魂阍珲馄溷诨"
+ {0x6df7, 0x660f, 0x8364, 0x6d51, 0x5a5a, 0x9b42, 0x960d, 0x73f2, 0x9984, 0x6eb7, 0x8be8}},
+ {"huo", // L"或活火伙货和获祸豁霍惑嚯镬耠劐藿攉锪蠖钬夥"
+ {0x6216, 0x6d3b, 0x706b, 0x4f19, 0x8d27, 0x548c, 0x83b7, 0x7978, 0x8c41, 0x970d, 0x60d1,
+ 0x56af, 0x956c, 0x8020, 0x5290, 0x85ff, 0x6509, 0x952a, 0x8816, 0x94ac, 0x5925}},
+ {"ji", // L"几及急既即机鸡积记级极计挤己季寄纪系基激吉脊际汲肌嫉姬绩缉饥迹棘蓟技冀辑伎祭剂悸济籍寂奇忌妓继集给击圾箕讥畸稽疾墼洎鲚屐齑戟鲫嵇矶稷戢虮笈暨笄剞叽蒺跻嵴掎跽霁唧畿荠瘠玑羁丌偈芨佶赍楫髻咭蕺觊麂骥殛岌亟犄乩芰哜彐萁藉"
+ {0x51e0, 0x53ca, 0x6025, 0x65e2, 0x5373, 0x673a, 0x9e21, 0x79ef, 0x8bb0, 0x7ea7, 0x6781,
+ 0x8ba1, 0x6324, 0x5df1, 0x5b63, 0x5bc4, 0x7eaa, 0x7cfb, 0x57fa, 0x6fc0, 0x5409, 0x810a,
+ 0x9645, 0x6c72, 0x808c, 0x5ac9, 0x59ec, 0x7ee9, 0x7f09, 0x9965, 0x8ff9, 0x68d8, 0x84df,
+ 0x6280, 0x5180, 0x8f91, 0x4f0e, 0x796d, 0x5242, 0x60b8, 0x6d4e, 0x7c4d, 0x5bc2, 0x5947,
+ 0x5fcc, 0x5993, 0x7ee7, 0x96c6, 0x7ed9, 0x51fb, 0x573e, 0x7b95, 0x8ba5, 0x7578, 0x7a3d,
+ 0x75be, 0x58bc, 0x6d0e, 0x9c9a, 0x5c50, 0x9f51, 0x621f, 0x9cab, 0x5d47, 0x77f6, 0x7a37,
+ 0x6222, 0x866e, 0x7b08, 0x66a8, 0x7b04, 0x525e, 0x53fd, 0x84ba, 0x8dfb, 0x5d74, 0x638e,
+ 0x8dfd, 0x9701, 0x5527, 0x757f, 0x8360, 0x7620, 0x7391, 0x7f81, 0x4e0c, 0x5048, 0x82a8,
+ 0x4f76, 0x8d4d, 0x696b, 0x9afb, 0x54ad, 0x857a, 0x89ca, 0x9e82, 0x9aa5, 0x6b9b, 0x5c8c,
+ 0x4e9f, 0x7284, 0x4e69, 0x82b0, 0x54dc, 0x5f50, 0x8401, 0x85c9}},
+ {"jia", // L"家加假价架甲佳夹嘉驾嫁枷荚颊钾稼茄贾铗葭迦戛浃镓痂恝岬跏嘏伽胛笳珈瘕郏袈蛱袷铪"
+ {0x5bb6, 0x52a0, 0x5047, 0x4ef7, 0x67b6, 0x7532, 0x4f73, 0x5939, 0x5609, 0x9a7e,
+ 0x5ac1, 0x67b7, 0x835a, 0x988a, 0x94be, 0x7a3c, 0x8304, 0x8d3e, 0x94d7, 0x846d,
+ 0x8fe6, 0x621b, 0x6d43, 0x9553, 0x75c2, 0x605d, 0x5cac, 0x8dcf, 0x560f, 0x4f3d,
+ 0x80db, 0x7b33, 0x73c8, 0x7615, 0x90cf, 0x8888, 0x86f1, 0x88b7, 0x94ea}},
+ {"jian", // L"见件减尖间键贱肩兼建检箭煎简剪歼监坚奸健艰荐剑渐溅涧鉴践捡柬笺俭碱硷拣舰槛缄茧饯翦鞯戋谏牮枧腱趼缣搛戬毽菅鲣笕谫楗囝蹇裥踺睑謇鹣蒹僭锏湔犍谮"
+ {0x89c1, 0x4ef6, 0x51cf, 0x5c16, 0x95f4, 0x952e, 0x8d31, 0x80a9, 0x517c, 0x5efa,
+ 0x68c0, 0x7bad, 0x714e, 0x7b80, 0x526a, 0x6b7c, 0x76d1, 0x575a, 0x5978, 0x5065,
+ 0x8270, 0x8350, 0x5251, 0x6e10, 0x6e85, 0x6da7, 0x9274, 0x8df5, 0x6361, 0x67ec,
+ 0x7b3a, 0x4fed, 0x78b1, 0x7877, 0x62e3, 0x8230, 0x69db, 0x7f04, 0x8327, 0x996f,
+ 0x7fe6, 0x97af, 0x620b, 0x8c0f, 0x726e, 0x67a7, 0x8171, 0x8dbc, 0x7f23, 0x641b,
+ 0x622c, 0x6bfd, 0x83c5, 0x9ca3, 0x7b15, 0x8c2b, 0x6957, 0x56dd, 0x8e47, 0x88e5,
+ 0x8e3a, 0x7751, 0x8b07, 0x9e63, 0x84b9, 0x50ed, 0x950f, 0x6e54, 0x728d, 0x8c2e}},
+ {"jiang", // L"将讲江奖降浆僵姜酱蒋疆匠强桨虹豇礓缰犟耩绛茳糨洚"
+ {0x5c06, 0x8bb2, 0x6c5f, 0x5956, 0x964d, 0x6d46, 0x50f5, 0x59dc,
+ 0x9171, 0x848b, 0x7586, 0x5320, 0x5f3a, 0x6868, 0x8679, 0x8c47,
+ 0x7913, 0x7f30, 0x729f, 0x8029, 0x7edb, 0x8333, 0x7ce8, 0x6d1a}},
+ {"jiao", // L"叫脚交角教较缴觉焦胶娇绞校搅骄狡浇矫郊嚼蕉轿窖椒礁饺铰酵侥剿徼艽僬蛟敫峤跤姣皎茭鹪噍醮佼鲛挢"
+ {0x53eb, 0x811a, 0x4ea4, 0x89d2, 0x6559, 0x8f83, 0x7f34, 0x89c9, 0x7126, 0x80f6,
+ 0x5a07, 0x7ede, 0x6821, 0x6405, 0x9a84, 0x72e1, 0x6d47, 0x77eb, 0x90ca, 0x56bc,
+ 0x8549, 0x8f7f, 0x7a96, 0x6912, 0x7901, 0x997a, 0x94f0, 0x9175, 0x4fa5, 0x527f,
+ 0x5fbc, 0x827d, 0x50ec, 0x86df, 0x656b, 0x5ce4, 0x8de4, 0x59e3, 0x768e, 0x832d,
+ 0x9e6a, 0x564d, 0x91ae, 0x4f7c, 0x9c9b, 0x6322}},
+ {"jie", // L"接节街借皆截解界结届姐揭戒介阶劫芥竭洁疥藉秸桔杰捷诫睫偈桀喈拮骱羯蚧嗟颉鲒婕碣讦孑疖诘卩锴"
+ {0x63a5, 0x8282, 0x8857, 0x501f, 0x7686, 0x622a, 0x89e3, 0x754c, 0x7ed3,
+ 0x5c4a, 0x59d0, 0x63ed, 0x6212, 0x4ecb, 0x9636, 0x52ab, 0x82a5, 0x7aed,
+ 0x6d01, 0x75a5, 0x85c9, 0x79f8, 0x6854, 0x6770, 0x6377, 0x8beb, 0x776b,
+ 0x5048, 0x6840, 0x5588, 0x62ee, 0x9ab1, 0x7faf, 0x86a7, 0x55df, 0x9889,
+ 0x9c92, 0x5a55, 0x78a3, 0x8ba6, 0x5b51, 0x7596, 0x8bd8, 0x5369, 0x9534}},
+ {"jin", // L"进近今仅紧金斤尽劲禁浸锦晋筋津谨巾襟烬靳廑瑾馑槿衿堇荩矜噤缙卺妗赆觐钅"
+ {0x8fdb, 0x8fd1, 0x4eca, 0x4ec5, 0x7d27, 0x91d1, 0x65a4, 0x5c3d, 0x52b2,
+ 0x7981, 0x6d78, 0x9526, 0x664b, 0x7b4b, 0x6d25, 0x8c28, 0x5dfe, 0x895f,
+ 0x70ec, 0x9773, 0x5ed1, 0x747e, 0x9991, 0x69ff, 0x887f, 0x5807, 0x8369,
+ 0x77dc, 0x5664, 0x7f19, 0x537a, 0x5997, 0x8d46, 0x89d0, 0x9485}},
+ {"jing", // L"竟静井惊经镜京净敬精景警竞境径荆晶鲸粳颈兢茎睛劲痉靖肼獍阱腈弪刭憬婧胫菁儆旌迳靓泾陉"
+ {0x7adf, 0x9759, 0x4e95, 0x60ca, 0x7ecf, 0x955c, 0x4eac, 0x51c0, 0x656c, 0x7cbe, 0x666f,
+ 0x8b66, 0x7ade, 0x5883, 0x5f84, 0x8346, 0x6676, 0x9cb8, 0x7cb3, 0x9888, 0x5162, 0x830e,
+ 0x775b, 0x52b2, 0x75c9, 0x9756, 0x80bc, 0x734d, 0x9631, 0x8148, 0x5f2a, 0x522d, 0x61ac,
+ 0x5a67, 0x80eb, 0x83c1, 0x5106, 0x65cc, 0x8ff3, 0x9753, 0x6cfe, 0x9649}},
+ {"jiong", // L"窘炯扃迥冂"
+ {0x7a98, 0x70af, 0x6243, 0x8fe5, 0x5182}},
+ {"jiu", // L"就九酒旧久揪救纠舅究韭厩臼玖灸咎疚赳鹫僦柩桕鬏鸠阄啾"
+ {0x5c31, 0x4e5d, 0x9152, 0x65e7, 0x4e45, 0x63ea, 0x6551, 0x7ea0, 0x8205,
+ 0x7a76, 0x97ed, 0x53a9, 0x81fc, 0x7396, 0x7078, 0x548e, 0x759a, 0x8d73,
+ 0x9e6b, 0x50e6, 0x67e9, 0x6855, 0x9b0f, 0x9e20, 0x9604, 0x557e}},
+ {"ju", // L"句举巨局具距锯剧居聚拘菊矩沮拒惧鞠狙驹据柜俱车咀疽踞炬倨醵裾屦犋苴窭飓锔椐苣琚掬榘龃趄莒雎遽橘踽榉鞫钜讵枸瞿蘧"
+ {0x53e5, 0x4e3e, 0x5de8, 0x5c40, 0x5177, 0x8ddd, 0x952f, 0x5267, 0x5c45, 0x805a, 0x62d8,
+ 0x83ca, 0x77e9, 0x6cae, 0x62d2, 0x60e7, 0x97a0, 0x72d9, 0x9a79, 0x636e, 0x67dc, 0x4ff1,
+ 0x8f66, 0x5480, 0x75bd, 0x8e1e, 0x70ac, 0x5028, 0x91b5, 0x88fe, 0x5c66, 0x728b, 0x82f4,
+ 0x7aad, 0x98d3, 0x9514, 0x6910, 0x82e3, 0x741a, 0x63ac, 0x6998, 0x9f83, 0x8d84, 0x8392,
+ 0x96ce, 0x907d, 0x6a58, 0x8e3d, 0x6989, 0x97ab, 0x949c, 0x8bb5, 0x67b8, 0x77bf, 0x8627}},
+ {"juan", // L"卷圈倦鹃捐娟眷绢鄄锩蠲镌狷桊涓隽"
+ {0x5377, 0x5708, 0x5026, 0x9e43, 0x6350, 0x5a1f, 0x7737, 0x7ee2, 0x9104, 0x9529, 0x8832,
+ 0x954c, 0x72f7, 0x684a, 0x6d93, 0x96bd}},
+ {"jue", // L"决绝觉角爵掘诀撅倔抉攫嚼脚桷橛觖劂爝矍镢獗珏崛蕨噘谲蹶孓厥阙"
+ {0x51b3, 0x7edd, 0x89c9, 0x89d2, 0x7235, 0x6398, 0x8bc0, 0x6485, 0x5014, 0x6289,
+ 0x652b, 0x56bc, 0x811a, 0x6877, 0x6a5b, 0x89d6, 0x5282, 0x721d, 0x77cd, 0x9562,
+ 0x7357, 0x73cf, 0x5d1b, 0x8568, 0x5658, 0x8c32, 0x8e76, 0x5b53, 0x53a5, 0x9619}},
+ {"jun", // L"军君均菌俊峻龟竣骏钧浚郡筠麇皲捃"
+ {0x519b, 0x541b, 0x5747, 0x83cc, 0x4fca, 0x5cfb, 0x9f9f, 0x7ae3, 0x9a8f, 0x94a7, 0x6d5a,
+ 0x90e1, 0x7b60, 0x9e87, 0x76b2, 0x6343}},
+ {"ka", // L"卡喀咯咖胩咔佧"
+ {0x5361, 0x5580, 0x54af, 0x5496, 0x80e9, 0x5494, 0x4f67}},
+ {"kai", // L"开揩凯慨楷垲剀锎铠锴忾恺蒈"
+ {0x5f00, 0x63e9, 0x51ef, 0x6168, 0x6977, 0x57b2, 0x5240, 0x950e, 0x94e0, 0x9534, 0x5ffe,
+ 0x607a, 0x8488}},
+ {"kan", // L"看砍堪刊坎槛勘龛戡侃瞰莰阚凵"
+ {0x770b, 0x780d, 0x582a, 0x520a, 0x574e, 0x69db, 0x52d8, 0x9f9b, 0x6221, 0x4f83, 0x77b0,
+ 0x83b0, 0x961a, 0x51f5}},
+ {"kang", // L"抗炕扛糠康慷亢钪闶伉"
+ {0x6297, 0x7095, 0x625b, 0x7ce0, 0x5eb7, 0x6177, 0x4ea2, 0x94aa, 0x95f6, 0x4f09}},
+ {"kao", // L"靠考烤拷栲犒尻铐"
+ {0x9760, 0x8003, 0x70e4, 0x62f7, 0x6832, 0x7292, 0x5c3b, 0x94d0}},
+ {"ke", // L"可克棵科颗刻课客壳渴苛柯磕咳坷呵恪岢蝌缂蚵轲窠钶氪颏瞌锞稞珂髁疴嗑溘骒铪"
+ {0x53ef, 0x514b, 0x68f5, 0x79d1, 0x9897, 0x523b, 0x8bfe, 0x5ba2, 0x58f3,
+ 0x6e34, 0x82db, 0x67ef, 0x78d5, 0x54b3, 0x5777, 0x5475, 0x606a, 0x5ca2,
+ 0x874c, 0x7f02, 0x86b5, 0x8f72, 0x7aa0, 0x94b6, 0x6c2a, 0x988f, 0x778c,
+ 0x951e, 0x7a1e, 0x73c2, 0x9ac1, 0x75b4, 0x55d1, 0x6e98, 0x9a92, 0x94ea}},
+ {"ken", // L"肯啃恳垦裉"
+ {0x80af, 0x5543, 0x6073, 0x57a6, 0x88c9}},
+ {"keng", // L"坑吭铿胫铒"
+ {0x5751, 0x542d, 0x94ff, 0x80eb, 0x94d2}},
+ {"kong", // L"空孔控恐倥崆箜"
+ {0x7a7a, 0x5b54, 0x63a7, 0x6050, 0x5025, 0x5d06, 0x7b9c}},
+ {"kou", // L"口扣抠寇蔻芤眍筘叩"
+ {0x53e3, 0x6263, 0x62a0, 0x5bc7, 0x853b, 0x82a4, 0x770d, 0x7b58, 0x53e9}},
+ {"ku", // L"哭库苦枯裤窟酷刳骷喾堀绔"
+ {0x54ed, 0x5e93, 0x82e6, 0x67af, 0x88e4, 0x7a9f, 0x9177, 0x5233, 0x9ab7, 0x55be, 0x5800,
+ 0x7ed4}},
+ {"kua", // L"跨垮挎夸胯侉锞"
+ {0x8de8, 0x57ae, 0x630e, 0x5938, 0x80ef, 0x4f89, 0x951e}},
+ {"kuai", // L"快块筷会侩哙蒯郐狯脍"
+ {0x5feb, 0x5757, 0x7b77, 0x4f1a, 0x4fa9, 0x54d9, 0x84af, 0x90d0, 0x72ef, 0x810d}},
+ {"kuan", // L"宽款髋"
+ {0x5bbd, 0x6b3e, 0x9acb}},
+ {"kuang", // L"矿筐狂框况旷匡眶诳邝纩夼诓圹贶哐"
+ {0x77ff, 0x7b50, 0x72c2, 0x6846, 0x51b5, 0x65f7, 0x5321, 0x7736, 0x8bf3, 0x909d, 0x7ea9,
+ 0x593c, 0x8bd3, 0x5739, 0x8d36, 0x54d0}},
+ {"kui", // L"亏愧奎窥溃葵魁馈盔傀岿匮愦揆睽跬聩篑喹逵暌蒉悝喟馗蝰隗夔"
+ {0x4e8f, 0x6127, 0x594e, 0x7aa5, 0x6e83, 0x8475, 0x9b41, 0x9988, 0x76d4, 0x5080,
+ 0x5cbf, 0x532e, 0x6126, 0x63c6, 0x777d, 0x8dec, 0x8069, 0x7bd1, 0x55b9, 0x9035,
+ 0x668c, 0x8489, 0x609d, 0x559f, 0x9997, 0x8770, 0x9697, 0x5914}},
+ {"kun", // L"捆困昆坤鲲锟髡琨醌阃悃顽"
+ {0x6346, 0x56f0, 0x6606, 0x5764, 0x9cb2, 0x951f, 0x9ae1, 0x7428, 0x918c, 0x9603, 0x6083,
+ 0x987d}},
+ {"kuo", // L"阔扩括廓蛞"
+ {0x9614, 0x6269, 0x62ec, 0x5ed3, 0x86de}},
+ {"la", // L"拉啦辣蜡腊喇垃落瘌邋砬剌旯"
+ {0x62c9, 0x5566, 0x8fa3, 0x8721, 0x814a, 0x5587, 0x5783, 0x843d, 0x760c, 0x908b, 0x782c,
+ 0x524c, 0x65ef}},
+ {"lai", // L"来赖莱濑赉崃涞铼籁徕癞睐"
+ {0x6765, 0x8d56, 0x83b1, 0x6fd1, 0x8d49, 0x5d03, 0x6d9e, 0x94fc, 0x7c41, 0x5f95, 0x765e,
+ 0x7750}},
+ {"lan", // L"蓝兰烂拦篮懒栏揽缆滥阑谰婪澜览榄岚褴镧斓罱漤"
+ {0x84dd, 0x5170, 0x70c2, 0x62e6, 0x7bee, 0x61d2, 0x680f, 0x63fd, 0x7f06, 0x6ee5, 0x9611,
+ 0x8c30, 0x5a6a, 0x6f9c, 0x89c8, 0x6984, 0x5c9a, 0x8934, 0x9567, 0x6593, 0x7f71, 0x6f24}},
+ {"lang", // L"浪狼廊郎朗榔琅稂螂莨啷锒阆蒗"
+ {0x6d6a, 0x72fc, 0x5eca, 0x90ce, 0x6717, 0x6994, 0x7405, 0x7a02, 0x8782, 0x83a8, 0x5577,
+ 0x9512, 0x9606, 0x8497}},
+ {"lao", // L"老捞牢劳烙涝落姥酪络佬耢铹醪铑唠栳崂痨"
+ {0x8001, 0x635e, 0x7262, 0x52b3, 0x70d9, 0x6d9d, 0x843d, 0x59e5, 0x916a, 0x7edc, 0x4f6c,
+ 0x8022, 0x94f9, 0x91aa, 0x94d1, 0x5520, 0x6833, 0x5d02, 0x75e8}},
+ {"le", // L"了乐勒鳓仂叻泐"
+ {0x4e86, 0x4e50, 0x52d2, 0x9cd3, 0x4ec2, 0x53fb, 0x6cd0}},
+ {"lei", // L"类累泪雷垒勒擂蕾肋镭儡磊缧诔耒酹羸嫘檑嘞"
+ {0x7c7b, 0x7d2f, 0x6cea, 0x96f7, 0x5792, 0x52d2, 0x64c2, 0x857e, 0x808b, 0x956d,
+ 0x5121, 0x78ca, 0x7f27, 0x8bd4, 0x8012, 0x9179, 0x7fb8, 0x5ad8, 0x6a91, 0x561e}},
+ {"leng", // L"冷棱楞愣塄"
+ {0x51b7, 0x68f1, 0x695e, 0x6123, 0x5844}},
+ {"li", // L"里离力立李例哩理利梨厘礼历丽吏砾漓莉傈荔俐痢狸粒沥隶栗璃鲤厉励犁黎篱郦鹂笠坜苈鳢缡跞蜊锂澧粝蓠枥蠡鬲呖砺嫠篥疠疬猁藜溧鲡戾栎唳醴轹詈骊罹逦俪喱雳黧莅俚蛎娌砬"
+ {0x91cc, 0x79bb, 0x529b, 0x7acb, 0x674e, 0x4f8b, 0x54e9, 0x7406, 0x5229, 0x68a8,
+ 0x5398, 0x793c, 0x5386, 0x4e3d, 0x540f, 0x783e, 0x6f13, 0x8389, 0x5088, 0x8354,
+ 0x4fd0, 0x75e2, 0x72f8, 0x7c92, 0x6ca5, 0x96b6, 0x6817, 0x7483, 0x9ca4, 0x5389,
+ 0x52b1, 0x7281, 0x9ece, 0x7bf1, 0x90e6, 0x9e42, 0x7b20, 0x575c, 0x82c8, 0x9ce2,
+ 0x7f21, 0x8dde, 0x870a, 0x9502, 0x6fa7, 0x7c9d, 0x84e0, 0x67a5, 0x8821, 0x9b32,
+ 0x5456, 0x783a, 0x5ae0, 0x7be5, 0x75a0, 0x75ac, 0x7301, 0x85dc, 0x6ea7, 0x9ca1,
+ 0x623e, 0x680e, 0x5533, 0x91b4, 0x8f79, 0x8a48, 0x9a8a, 0x7f79, 0x9026, 0x4fea,
+ 0x55b1, 0x96f3, 0x9ee7, 0x8385, 0x4fda, 0x86ce, 0x5a0c, 0x782c}},
+ {"lia", // L"俩"
+ {0x4fe9}},
+ {"lian", // L"连联练莲恋脸炼链敛怜廉帘镰涟蠊琏殓蔹鲢奁潋臁裢濂裣楝"
+ {0x8fde, 0x8054, 0x7ec3, 0x83b2, 0x604b, 0x8138, 0x70bc, 0x94fe, 0x655b,
+ 0x601c, 0x5ec9, 0x5e18, 0x9570, 0x6d9f, 0x880a, 0x740f, 0x6b93, 0x8539,
+ 0x9ca2, 0x5941, 0x6f4b, 0x81c1, 0x88e2, 0x6fc2, 0x88e3, 0x695d}},
+ {"liang", // L"两亮辆凉粮梁量良晾谅俩粱墚踉椋魉莨"
+ {0x4e24, 0x4eae, 0x8f86, 0x51c9, 0x7cae, 0x6881, 0x91cf, 0x826f, 0x667e, 0x8c05, 0x4fe9,
+ 0x7cb1, 0x589a, 0x8e09, 0x690b, 0x9b49, 0x83a8}},
+ {"liao", // L"了料撩聊撂疗廖燎辽僚寥镣潦钌蓼尥寮缭獠鹩嘹"
+ {0x4e86, 0x6599, 0x64a9, 0x804a, 0x6482, 0x7597, 0x5ed6, 0x71ce, 0x8fbd, 0x50da, 0x5be5,
+ 0x9563, 0x6f66, 0x948c, 0x84fc, 0x5c25, 0x5bee, 0x7f2d, 0x7360, 0x9e69, 0x5639}},
+ {"lie", // L"列裂猎劣烈咧埒捩鬣趔躐冽洌"
+ {0x5217, 0x88c2, 0x730e, 0x52a3, 0x70c8, 0x54a7, 0x57d2, 0x6369, 0x9b23, 0x8d94, 0x8e90,
+ 0x51bd, 0x6d0c}},
+ {"lin", // L"林临淋邻磷鳞赁吝拎琳霖凛遴嶙蔺粼麟躏辚廪懔瞵檩膦啉"
+ {0x6797, 0x4e34, 0x6dcb, 0x90bb, 0x78f7, 0x9cde, 0x8d41, 0x541d, 0x62ce,
+ 0x7433, 0x9716, 0x51db, 0x9074, 0x5d99, 0x853a, 0x7cbc, 0x9e9f, 0x8e8f,
+ 0x8f9a, 0x5eea, 0x61d4, 0x77b5, 0x6aa9, 0x81a6, 0x5549}},
+ {"ling", // L"另令领零铃玲灵岭龄凌陵菱伶羚棱翎蛉苓绫瓴酃呤泠棂柃鲮聆囹"
+ {0x53e6, 0x4ee4, 0x9886, 0x96f6, 0x94c3, 0x73b2, 0x7075, 0x5cad, 0x9f84, 0x51cc,
+ 0x9675, 0x83f1, 0x4f36, 0x7f9a, 0x68f1, 0x7fce, 0x86c9, 0x82d3, 0x7eeb, 0x74f4,
+ 0x9143, 0x5464, 0x6ce0, 0x68c2, 0x67c3, 0x9cae, 0x8046, 0x56f9}},
+ {"liu", // L"六流留刘柳溜硫瘤榴琉馏碌陆绺锍鎏镏浏骝旒鹨熘遛"
+ {0x516d, 0x6d41, 0x7559, 0x5218, 0x67f3, 0x6e9c, 0x786b, 0x7624,
+ 0x69b4, 0x7409, 0x998f, 0x788c, 0x9646, 0x7efa, 0x950d, 0x938f,
+ 0x954f, 0x6d4f, 0x9a9d, 0x65d2, 0x9e68, 0x7198, 0x905b}},
+ {"lo", // L"咯"
+ {0x54af}},
+ {"long", // L"龙拢笼聋隆垄弄咙窿陇垅胧珑茏泷栊癃砻"
+ {0x9f99, 0x62e2, 0x7b3c, 0x804b, 0x9686, 0x5784, 0x5f04, 0x5499, 0x7abf, 0x9647, 0x5785,
+ 0x80e7, 0x73d1, 0x830f, 0x6cf7, 0x680a, 0x7643, 0x783b}},
+ {"lou", // L"楼搂漏陋露娄篓偻蝼镂蒌耧髅喽瘘嵝"
+ {0x697c, 0x6402, 0x6f0f, 0x964b, 0x9732, 0x5a04, 0x7bd3, 0x507b, 0x877c, 0x9542, 0x848c,
+ 0x8027, 0x9ac5, 0x55bd, 0x7618, 0x5d5d}},
+ {"lu", // L"路露录鹿陆炉卢鲁卤芦颅庐碌掳绿虏赂戮潞禄麓六鲈栌渌逯泸轳氇簏橹辂垆胪噜镥辘漉撸璐鸬鹭舻"
+ {0x8def, 0x9732, 0x5f55, 0x9e7f, 0x9646, 0x7089, 0x5362, 0x9c81, 0x5364, 0x82a6, 0x9885,
+ 0x5e90, 0x788c, 0x63b3, 0x7eff, 0x864f, 0x8d42, 0x622e, 0x6f5e, 0x7984, 0x9e93, 0x516d,
+ 0x9c88, 0x680c, 0x6e0c, 0x902f, 0x6cf8, 0x8f73, 0x6c07, 0x7c0f, 0x6a79, 0x8f82, 0x5786,
+ 0x80ea, 0x565c, 0x9565, 0x8f98, 0x6f09, 0x64b8, 0x7490, 0x9e2c, 0x9e6d, 0x823b}},
+ {"luan", // L"乱卵滦峦孪挛栾銮脔娈鸾"
+ {0x4e71, 0x5375, 0x6ee6, 0x5ce6, 0x5b6a, 0x631b, 0x683e, 0x92ae, 0x8114, 0x5a08, 0x9e3e}},
+ {"lue", // L"略掠锊"
+ {0x7565, 0x63a0, 0x950a}},
+ {"lun", // L"论轮抡伦沦仑纶囵"
+ {0x8bba, 0x8f6e, 0x62a1, 0x4f26, 0x6ca6, 0x4ed1, 0x7eb6, 0x56f5}},
+ {"luo", // L"落罗锣裸骡烙箩螺萝洛骆逻络咯荦漯蠃雒倮硌椤捋脶瘰摞泺珞镙猡铬"
+ {0x843d, 0x7f57, 0x9523, 0x88f8, 0x9aa1, 0x70d9, 0x7ba9, 0x87ba, 0x841d, 0x6d1b,
+ 0x9a86, 0x903b, 0x7edc, 0x54af, 0x8366, 0x6f2f, 0x8803, 0x96d2, 0x502e, 0x784c,
+ 0x6924, 0x634b, 0x8136, 0x7630, 0x645e, 0x6cfa, 0x73de, 0x9559, 0x7321, 0x94ec}},
+ {"lv", // L"绿率铝驴旅屡滤吕律氯缕侣虑履偻膂榈闾捋褛稆"
+ {0x7eff, 0x7387, 0x94dd, 0x9a74, 0x65c5, 0x5c61, 0x6ee4, 0x5415, 0x5f8b, 0x6c2f, 0x7f15,
+ 0x4fa3, 0x8651, 0x5c65, 0x507b, 0x8182, 0x6988, 0x95fe, 0x634b, 0x891b, 0x7a06}},
+ {"lve", // L"略掠锊"
+ {0x7565, 0x63a0, 0x950a}},
+ {"m", // L"呒"
+ {0x5452}},
+ {"ma", // L"吗妈马嘛麻骂抹码玛蚂摩唛蟆犸嬷杩"
+ {0x5417, 0x5988, 0x9a6c, 0x561b, 0x9ebb, 0x9a82, 0x62b9, 0x7801, 0x739b, 0x8682, 0x6469,
+ 0x551b, 0x87c6, 0x72b8, 0x5b37, 0x6769}},
+ {"mai", // L"买卖迈埋麦脉劢霾荬"
+ {0x4e70, 0x5356, 0x8fc8, 0x57cb, 0x9ea6, 0x8109, 0x52a2, 0x973e, 0x836c}},
+ {"man", // L"满慢瞒漫蛮蔓曼馒埋谩幔鳗墁螨镘颟鞔缦熳"
+ {0x6ee1, 0x6162, 0x7792, 0x6f2b, 0x86ee, 0x8513, 0x66fc, 0x9992, 0x57cb, 0x8c29, 0x5e54,
+ 0x9cd7, 0x5881, 0x87a8, 0x9558, 0x989f, 0x9794, 0x7f26, 0x71b3}},
+ {"mang", // L"忙芒盲莽茫氓硭邙蟒漭"
+ {0x5fd9, 0x8292, 0x76f2, 0x83bd, 0x832b, 0x6c13, 0x786d, 0x9099, 0x87d2, 0x6f2d}},
+ {"mao", // L"毛冒帽猫矛卯貌茂贸铆锚茅耄茆瑁蝥髦懋昴牦瞀峁袤蟊旄泖"
+ {0x6bdb, 0x5192, 0x5e3d, 0x732b, 0x77db, 0x536f, 0x8c8c, 0x8302, 0x8d38,
+ 0x94c6, 0x951a, 0x8305, 0x8004, 0x8306, 0x7441, 0x8765, 0x9ae6, 0x61cb,
+ 0x6634, 0x7266, 0x7780, 0x5cc1, 0x88a4, 0x87ca, 0x65c4, 0x6cd6}},
+ {"me", // L"么"
+ {0x4e48}},
+ {"mei", // L"没每煤镁美酶妹枚霉玫眉梅寐昧媒媚嵋猸袂湄浼鹛莓魅镅楣"
+ {0x6ca1, 0x6bcf, 0x7164, 0x9541, 0x7f8e, 0x9176, 0x59b9, 0x679a, 0x9709,
+ 0x73ab, 0x7709, 0x6885, 0x5bd0, 0x6627, 0x5a92, 0x5a9a, 0x5d4b, 0x7338,
+ 0x8882, 0x6e44, 0x6d7c, 0x9e5b, 0x8393, 0x9b45, 0x9545, 0x6963}},
+ {"men", // L"门们闷懑扪钔焖"
+ {0x95e8, 0x4eec, 0x95f7, 0x61d1, 0x626a, 0x9494, 0x7116}},
+ {"meng", // L"猛梦蒙锰孟盟檬萌礞蜢勐懵甍蠓虻朦艋艨瞢"
+ {0x731b, 0x68a6, 0x8499, 0x9530, 0x5b5f, 0x76df, 0x6aac, 0x840c, 0x791e, 0x8722, 0x52d0,
+ 0x61f5, 0x750d, 0x8813, 0x867b, 0x6726, 0x824b, 0x8268, 0x77a2}},
+ {"mi", // L"米密迷眯蜜谜觅秘弥幂靡糜泌醚蘼縻咪汨麋祢猕弭谧芈脒宓敉嘧糸冖"
+ {0x7c73, 0x5bc6, 0x8ff7, 0x772f, 0x871c, 0x8c1c, 0x89c5, 0x79d8, 0x5f25, 0x5e42,
+ 0x9761, 0x7cdc, 0x6ccc, 0x919a, 0x863c, 0x7e3b, 0x54aa, 0x6c68, 0x9e8b, 0x7962,
+ 0x7315, 0x5f2d, 0x8c27, 0x8288, 0x8112, 0x5b93, 0x6549, 0x5627, 0x7cf8, 0x5196}},
+ {"mian", // L"面棉免绵眠缅勉冕娩腼湎眄沔渑宀"
+ {0x9762, 0x68c9, 0x514d, 0x7ef5, 0x7720, 0x7f05, 0x52c9, 0x5195, 0x5a29, 0x817c, 0x6e4e,
+ 0x7704, 0x6c94, 0x6e11, 0x5b80}},
+ {"miao", // L"秒苗庙妙描瞄藐渺眇缪缈淼喵杪鹋邈"
+ {0x79d2, 0x82d7, 0x5e99, 0x5999, 0x63cf, 0x7784, 0x85d0, 0x6e3a, 0x7707, 0x7f2a, 0x7f08,
+ 0x6dfc, 0x55b5, 0x676a, 0x9e4b, 0x9088}},
+ {"mie", // L"灭蔑咩篾蠛乜"
+ {0x706d, 0x8511, 0x54a9, 0x7bfe, 0x881b, 0x4e5c}},
+ {"min", // L"民抿敏闽皿悯珉愍缗闵玟苠泯黾鳘岷"
+ {0x6c11, 0x62bf, 0x654f, 0x95fd, 0x76bf, 0x60af, 0x73c9, 0x610d, 0x7f17, 0x95f5, 0x739f,
+ 0x82e0, 0x6cef, 0x9efe, 0x9cd8, 0x5cb7}},
+ {"ming", // L"名明命鸣铭螟冥瞑暝茗溟酩"
+ {0x540d, 0x660e, 0x547d, 0x9e23, 0x94ed, 0x879f, 0x51a5, 0x7791, 0x669d, 0x8317, 0x6e9f,
+ 0x9169}},
+ {"miu", // L"谬缪"
+ {0x8c2c, 0x7f2a}},
+ {"mo", // L"摸磨抹末膜墨没莫默魔模摩摹漠陌蘑脉沫万寞秣瘼殁镆嫫谟蓦貊貘麽茉馍耱"
+ {0x6478, 0x78e8, 0x62b9, 0x672b, 0x819c, 0x58a8, 0x6ca1, 0x83ab, 0x9ed8, 0x9b54, 0x6a21,
+ 0x6469, 0x6479, 0x6f20, 0x964c, 0x8611, 0x8109, 0x6cab, 0x4e07, 0x5bde, 0x79e3, 0x763c,
+ 0x6b81, 0x9546, 0x5aeb, 0x8c1f, 0x84e6, 0x8c8a, 0x8c98, 0x9ebd, 0x8309, 0x998d, 0x8031}},
+ {"mou", // L"某谋牟眸蛑鍪侔缪哞"
+ {0x67d0, 0x8c0b, 0x725f, 0x7738, 0x86d1, 0x936a, 0x4f94, 0x7f2a, 0x54de}},
+ {"mu", // L"木母亩幕目墓牧牟模穆暮牡拇募慕睦姆钼毪坶沐仫苜"
+ {0x6728, 0x6bcd, 0x4ea9, 0x5e55, 0x76ee, 0x5893, 0x7267, 0x725f,
+ 0x6a21, 0x7a46, 0x66ae, 0x7261, 0x62c7, 0x52df, 0x6155, 0x7766,
+ 0x59c6, 0x94bc, 0x6bea, 0x5776, 0x6c90, 0x4eeb, 0x82dc}},
+ {"na", // L"那拿哪纳钠娜呐衲捺镎肭"
+ {0x90a3, 0x62ff, 0x54ea, 0x7eb3, 0x94a0, 0x5a1c, 0x5450, 0x8872, 0x637a, 0x954e, 0x80ad}},
+ {"nai", // L"乃耐奶奈氖萘艿柰鼐佴"
+ {0x4e43, 0x8010, 0x5976, 0x5948, 0x6c16, 0x8418, 0x827f, 0x67f0, 0x9f10, 0x4f74}},
+ {"nan", // L"难南男赧囡蝻楠喃腩"
+ {0x96be, 0x5357, 0x7537, 0x8d67, 0x56e1, 0x877b, 0x6960, 0x5583, 0x8169}},
+ {"nang", // L"囊馕曩囔攮"
+ {0x56ca, 0x9995, 0x66e9, 0x56d4, 0x652e}},
+ {"nao", // L"闹脑恼挠淖孬铙瑙垴呶蛲猱硇"
+ {0x95f9, 0x8111, 0x607c, 0x6320, 0x6dd6, 0x5b6c, 0x94d9, 0x7459, 0x57b4, 0x5476, 0x86f2,
+ 0x7331, 0x7847}},
+ {"ne", // L"呢哪讷"
+ {0x5462, 0x54ea, 0x8bb7}},
+ {"nei", // L"内馁"
+ {0x5185, 0x9981}},
+ {"nen", // L"嫩恁"
+ {0x5ae9, 0x6041}},
+ {"neng", // L"能"
+ {0x80fd}},
+ {"ni", // L"你泥拟腻逆呢溺倪尼匿妮霓铌昵坭祢猊伲怩鲵睨旎慝"
+ {0x4f60, 0x6ce5, 0x62df, 0x817b, 0x9006, 0x5462, 0x6eba, 0x502a,
+ 0x5c3c, 0x533f, 0x59ae, 0x9713, 0x94cc, 0x6635, 0x576d, 0x7962,
+ 0x730a, 0x4f32, 0x6029, 0x9cb5, 0x7768, 0x65ce, 0x615d}},
+ {"nian", // L"年念捻撵拈碾蔫廿黏辇鲇鲶埝"
+ {0x5e74, 0x5ff5, 0x637b, 0x64b5, 0x62c8, 0x78be, 0x852b, 0x5eff, 0x9ecf, 0x8f87, 0x9c87,
+ 0x9cb6, 0x57dd}},
+ {"niang", // L"娘酿"
+ {0x5a18, 0x917f}},
+ {"niao", // L"鸟尿袅茑脲嬲"
+ {0x9e1f, 0x5c3f, 0x8885, 0x8311, 0x8132, 0x5b32}},
+ {"nie", // L"捏镍聂孽涅镊啮陧蘖嗫臬蹑颞乜"
+ {0x634f, 0x954d, 0x8042, 0x5b7d, 0x6d85, 0x954a, 0x556e, 0x9667, 0x8616, 0x55eb, 0x81ec,
+ 0x8e51, 0x989e, 0x4e5c}},
+ {"nin", // L"您"
+ {0x60a8}},
+ {"ning", // L"拧凝宁柠狞泞佞甯咛聍"
+ {0x62e7, 0x51dd, 0x5b81, 0x67e0, 0x72de, 0x6cde, 0x4f5e, 0x752f, 0x549b, 0x804d}},
+ {"niu", // L"牛扭纽钮拗妞狃忸"
+ {0x725b, 0x626d, 0x7ebd, 0x94ae, 0x62d7, 0x599e, 0x72c3, 0x5ff8}},
+ {"nong", // L"弄浓农脓哝侬"
+ {0x5f04, 0x6d53, 0x519c, 0x8113, 0x54dd, 0x4fac}},
+ {"nou", // L"耨"
+ {0x8028}},
+ {"nu", // L"怒努奴孥胬驽弩"
+ {0x6012, 0x52aa, 0x5974, 0x5b65, 0x80ec, 0x9a7d, 0x5f29}},
+ {"nuan", // L"暖"
+ {0x6696}},
+ {"nue", // L"虐疟"
+ {0x8650, 0x759f}},
+ {"nuo", // L"挪诺懦糯娜喏傩锘搦"
+ {0x632a, 0x8bfa, 0x61e6, 0x7cef, 0x5a1c, 0x558f, 0x50a9, 0x9518, 0x6426}},
+ {"nv", // L"女衄钕恧"
+ {0x5973, 0x8844, 0x9495, 0x6067}},
+ {"nve", // L"虐疟"
+ {0x8650, 0x759f}},
+ {"o", // L"哦喔噢"
+ {0x54e6, 0x5594, 0x5662}},
+ {"ou", // L"偶呕欧藕鸥区沤殴怄瓯讴耦"
+ {0x5076, 0x5455, 0x6b27, 0x85d5, 0x9e25, 0x533a, 0x6ca4, 0x6bb4, 0x6004, 0x74ef, 0x8bb4,
+ 0x8026}},
+ {"pa", // L"怕爬趴啪耙扒帕琶筢杷葩"
+ {0x6015, 0x722c, 0x8db4, 0x556a, 0x8019, 0x6252, 0x5e15, 0x7436, 0x7b62, 0x6777, 0x8469}},
+ {"pai", // L"派排拍牌迫徘湃哌俳蒎"
+ {0x6d3e, 0x6392, 0x62cd, 0x724c, 0x8feb, 0x5f98, 0x6e43, 0x54cc, 0x4ff3, 0x848e}},
+ {"pan", // L"盘盼判攀畔潘叛磐番胖襻蟠袢泮拚爿蹒"
+ {0x76d8, 0x76fc, 0x5224, 0x6500, 0x7554, 0x6f58, 0x53db, 0x78d0, 0x756a, 0x80d6, 0x897b,
+ 0x87e0, 0x88a2, 0x6cee, 0x62da, 0x723f, 0x8e52}},
+ {"pang", // L"旁胖耪庞乓膀磅滂彷逄螃镑"
+ {0x65c1, 0x80d6, 0x802a, 0x5e9e, 0x4e53, 0x8180, 0x78c5, 0x6ec2, 0x5f77, 0x9004, 0x8783,
+ 0x9551}},
+ {"pao", // L"跑抛炮泡刨袍咆狍匏庖疱脬"
+ {0x8dd1, 0x629b, 0x70ae, 0x6ce1, 0x5228, 0x888d, 0x5486, 0x72cd, 0x530f, 0x5e96, 0x75b1,
+ 0x812c}},
+ {"pei", // L"陪配赔呸胚佩培沛裴旆锫帔醅霈辔"
+ {0x966a, 0x914d, 0x8d54, 0x5478, 0x80da, 0x4f69, 0x57f9, 0x6c9b, 0x88f4, 0x65c6, 0x952b,
+ 0x5e14, 0x9185, 0x9708, 0x8f94}},
+ {"pen", // L"喷盆湓"
+ {0x55b7, 0x76c6, 0x6e53}},
+ {"peng", // L"碰捧棚砰蓬朋彭鹏烹硼膨抨澎篷怦堋蟛嘭"
+ {0x78b0, 0x6367, 0x68da, 0x7830, 0x84ec, 0x670b, 0x5f6d, 0x9e4f, 0x70f9, 0x787c, 0x81a8,
+ 0x62a8, 0x6f8e, 0x7bf7, 0x6026, 0x580b, 0x87db, 0x562d}},
+ {"pi", // L"批皮披匹劈辟坯屁脾僻疲痞霹琵毗啤譬砒否貔丕圮媲癖仳擗郫甓枇睥蜱鼙邳陂铍庀罴埤纰陴淠噼蚍裨疋芘"
+ {0x6279, 0x76ae, 0x62ab, 0x5339, 0x5288, 0x8f9f, 0x576f, 0x5c41, 0x813e, 0x50fb,
+ 0x75b2, 0x75de, 0x9739, 0x7435, 0x6bd7, 0x5564, 0x8b6c, 0x7812, 0x5426, 0x8c94,
+ 0x4e15, 0x572e, 0x5ab2, 0x7656, 0x4ef3, 0x64d7, 0x90eb, 0x7513, 0x6787, 0x7765,
+ 0x8731, 0x9f19, 0x90b3, 0x9642, 0x94cd, 0x5e80, 0x7f74, 0x57e4, 0x7eb0, 0x9674,
+ 0x6de0, 0x567c, 0x868d, 0x88e8, 0x758b, 0x8298}},
+ {"pian", // L"片篇骗偏便扁翩缏犏骈胼蹁谝"
+ {0x7247, 0x7bc7, 0x9a97, 0x504f, 0x4fbf, 0x6241, 0x7fe9, 0x7f0f, 0x728f, 0x9a88, 0x80fc,
+ 0x8e41, 0x8c1d}},
+ {"piao", // L"票飘漂瓢朴螵嫖瞟殍缥嘌骠剽"
+ {0x7968, 0x98d8, 0x6f02, 0x74e2, 0x6734, 0x87b5, 0x5ad6, 0x779f, 0x6b8d, 0x7f25, 0x560c,
+ 0x9aa0, 0x527d}},
+ {"pie", // L"瞥撇氕苤"
+ {0x77a5, 0x6487, 0x6c15, 0x82e4}},
+ {"pin", // L"品贫聘拼频嫔榀姘牝颦"
+ {0x54c1, 0x8d2b, 0x8058, 0x62fc, 0x9891, 0x5ad4, 0x6980, 0x59d8, 0x725d, 0x98a6}},
+ {"ping", // L"平凭瓶评屏乒萍苹坪冯娉鲆枰俜"
+ {0x5e73, 0x51ed, 0x74f6, 0x8bc4, 0x5c4f, 0x4e52, 0x840d, 0x82f9, 0x576a, 0x51af, 0x5a09,
+ 0x9c86, 0x67b0, 0x4fdc}},
+ {"po", // L"破坡颇婆泼迫泊魄朴繁粕笸皤钋陂鄱攴叵珀钷"
+ {0x7834, 0x5761, 0x9887, 0x5a46, 0x6cfc, 0x8feb, 0x6cca, 0x9b44, 0x6734, 0x7e41,
+ 0x7c95, 0x7b38, 0x76a4, 0x948b, 0x9642, 0x9131, 0x6534, 0x53f5, 0x73c0, 0x94b7}},
+ {"pou", // L"剖掊裒"
+ {0x5256, 0x638a, 0x88d2}},
+ {"pu", // L"扑铺谱脯仆蒲葡朴菩曝莆瀑埔圃浦堡普暴镨噗匍溥濮氆蹼璞镤"
+ {0x6251, 0x94fa, 0x8c31, 0x812f, 0x4ec6, 0x84b2, 0x8461, 0x6734, 0x83e9,
+ 0x66dd, 0x8386, 0x7011, 0x57d4, 0x5703, 0x6d66, 0x5821, 0x666e, 0x66b4,
+ 0x9568, 0x5657, 0x530d, 0x6ea5, 0x6fee, 0x6c06, 0x8e7c, 0x749e, 0x9564}},
+ {"qi", // L"起其七气期齐器妻骑汽棋奇欺漆启戚柒岂砌弃泣祁凄企乞契歧祈栖畦脐崎稽迄缉沏讫旗祺颀骐屺岐蹊蕲桤憩芪荠萋芑汔亟鳍俟槭嘁蛴綦亓欹琪麒琦蜞圻杞葺碛淇耆绮綮"
+ {0x8d77, 0x5176, 0x4e03, 0x6c14, 0x671f, 0x9f50, 0x5668, 0x59bb, 0x9a91, 0x6c7d, 0x68cb,
+ 0x5947, 0x6b3a, 0x6f06, 0x542f, 0x621a, 0x67d2, 0x5c82, 0x780c, 0x5f03, 0x6ce3, 0x7941,
+ 0x51c4, 0x4f01, 0x4e5e, 0x5951, 0x6b67, 0x7948, 0x6816, 0x7566, 0x8110, 0x5d0e, 0x7a3d,
+ 0x8fc4, 0x7f09, 0x6c8f, 0x8bab, 0x65d7, 0x797a, 0x9880, 0x9a90, 0x5c7a, 0x5c90, 0x8e4a,
+ 0x8572, 0x6864, 0x61a9, 0x82aa, 0x8360, 0x840b, 0x8291, 0x6c54, 0x4e9f, 0x9ccd, 0x4fdf,
+ 0x69ed, 0x5601, 0x86f4, 0x7da6, 0x4e93, 0x6b39, 0x742a, 0x9e92, 0x7426, 0x871e, 0x573b,
+ 0x675e, 0x847a, 0x789b, 0x6dc7, 0x8006, 0x7eee, 0x7dae}},
+ {"qia", // L"恰卡掐洽髂袷葜"
+ {0x6070, 0x5361, 0x6390, 0x6d3d, 0x9ac2, 0x88b7, 0x845c}},
+ {"qian", // L"前钱千牵浅签欠铅嵌钎迁钳乾谴谦潜歉纤扦遣黔堑仟岍钤褰箝掮搴倩慊悭愆虔芡荨缱佥芊阡肷茜椠犍骞羟赶"
+ {0x524d, 0x94b1, 0x5343, 0x7275, 0x6d45, 0x7b7e, 0x6b20, 0x94c5, 0x5d4c, 0x948e,
+ 0x8fc1, 0x94b3, 0x4e7e, 0x8c34, 0x8c26, 0x6f5c, 0x6b49, 0x7ea4, 0x6266, 0x9063,
+ 0x9ed4, 0x5811, 0x4edf, 0x5c8d, 0x94a4, 0x8930, 0x7b9d, 0x63ae, 0x6434, 0x5029,
+ 0x614a, 0x60ad, 0x6106, 0x8654, 0x82a1, 0x8368, 0x7f31, 0x4f65, 0x828a, 0x9621,
+ 0x80b7, 0x831c, 0x6920, 0x728d, 0x9a9e, 0x7f9f, 0x8d76}},
+ {"qiang", // L"强枪墙抢腔呛羌蔷蜣跄戗襁戕炝镪锵羟樯嫱"
+ {0x5f3a, 0x67aa, 0x5899, 0x62a2, 0x8154, 0x545b, 0x7f8c, 0x8537, 0x8723, 0x8dc4, 0x6217,
+ 0x8941, 0x6215, 0x709d, 0x956a, 0x9535, 0x7f9f, 0x6a2f, 0x5af1}},
+ {"qiao", // L"桥瞧敲巧翘锹壳鞘撬悄俏窍雀乔侨峭橇樵荞跷硗憔谯鞒愀缲诮劁峤搞铫"
+ {0x6865, 0x77a7, 0x6572, 0x5de7, 0x7fd8, 0x9539, 0x58f3, 0x9798, 0x64ac, 0x6084, 0x4fcf,
+ 0x7a8d, 0x96c0, 0x4e54, 0x4fa8, 0x5ced, 0x6a47, 0x6a35, 0x835e, 0x8df7, 0x7857, 0x6194,
+ 0x8c2f, 0x9792, 0x6100, 0x7f32, 0x8bee, 0x5281, 0x5ce4, 0x641e, 0x94eb}},
+ {"qie", // L"切且怯窃茄郄趄惬锲妾箧慊伽挈"
+ {0x5207, 0x4e14, 0x602f, 0x7a83, 0x8304, 0x90c4, 0x8d84, 0x60ec, 0x9532, 0x59be, 0x7ba7,
+ 0x614a, 0x4f3d, 0x6308}},
+ {"qin", // L"亲琴侵勤擒寝秦芹沁禽钦吣覃矜衾芩廑嗪螓噙揿檎锓"
+ {0x4eb2, 0x7434, 0x4fb5, 0x52e4, 0x64d2, 0x5bdd, 0x79e6, 0x82b9,
+ 0x6c81, 0x79bd, 0x94a6, 0x5423, 0x8983, 0x77dc, 0x887e, 0x82a9,
+ 0x5ed1, 0x55ea, 0x8793, 0x5659, 0x63ff, 0x6a8e, 0x9513}},
+ {"qing", // L"请轻清青情晴氢倾庆擎顷亲卿氰圊謦檠箐苘蜻黥罄鲭磬綮"
+ {0x8bf7, 0x8f7b, 0x6e05, 0x9752, 0x60c5, 0x6674, 0x6c22, 0x503e, 0x5e86,
+ 0x64ce, 0x9877, 0x4eb2, 0x537f, 0x6c30, 0x570a, 0x8b26, 0x6aa0, 0x7b90,
+ 0x82d8, 0x873b, 0x9ee5, 0x7f44, 0x9cad, 0x78ec, 0x7dae}},
+ {"qiong", // L"穷琼跫穹邛蛩茕銎筇"
+ {0x7a77, 0x743c, 0x8deb, 0x7a79, 0x909b, 0x86e9, 0x8315, 0x928e, 0x7b47}},
+ {"qiu", // L"求球秋丘泅仇邱囚酋龟楸蚯裘糗蝤巯逑俅虬赇鳅犰湫遒"
+ {0x6c42, 0x7403, 0x79cb, 0x4e18, 0x6cc5, 0x4ec7, 0x90b1, 0x56da,
+ 0x914b, 0x9f9f, 0x6978, 0x86af, 0x88d8, 0x7cd7, 0x8764, 0x5def,
+ 0x9011, 0x4fc5, 0x866c, 0x8d47, 0x9cc5, 0x72b0, 0x6e6b, 0x9052}},
+ {"qu", // L"去取区娶渠曲趋趣屈驱蛆躯龋戌蠼蘧祛蕖磲劬诎鸲阒麴癯衢黢璩氍觑蛐朐瞿岖苣"
+ {0x53bb, 0x53d6, 0x533a, 0x5a36, 0x6e20, 0x66f2, 0x8d8b, 0x8da3, 0x5c48,
+ 0x9a71, 0x86c6, 0x8eaf, 0x9f8b, 0x620c, 0x883c, 0x8627, 0x795b, 0x8556,
+ 0x78f2, 0x52ac, 0x8bce, 0x9e32, 0x9612, 0x9eb4, 0x766f, 0x8862, 0x9ee2,
+ 0x74a9, 0x6c0d, 0x89d1, 0x86d0, 0x6710, 0x77bf, 0x5c96, 0x82e3}},
+ {"quan", // L"全权劝圈拳犬泉券颧痊醛铨筌绻诠辁畎鬈悛蜷荃犭"
+ {0x5168, 0x6743, 0x529d, 0x5708, 0x62f3, 0x72ac, 0x6cc9, 0x5238, 0x98a7, 0x75ca, 0x919b,
+ 0x94e8, 0x7b4c, 0x7efb, 0x8be0, 0x8f81, 0x754e, 0x9b08, 0x609b, 0x8737, 0x8343, 0x72ad}},
+ {"que", // L"却缺确雀瘸鹊炔榷阙阕悫"
+ {0x5374, 0x7f3a, 0x786e, 0x96c0, 0x7638, 0x9e4a, 0x7094, 0x69b7, 0x9619, 0x9615, 0x60ab}},
+ {"qun", // L"群裙麇逡"
+ {0x7fa4, 0x88d9, 0x9e87, 0x9021}},
+ {"ran", // L"染燃然冉髯苒蚺"
+ {0x67d3, 0x71c3, 0x7136, 0x5189, 0x9aef, 0x82d2, 0x86ba}},
+ {"rang", // L"让嚷瓤攘壤穰禳"
+ {0x8ba9, 0x56b7, 0x74e4, 0x6518, 0x58e4, 0x7a70, 0x79b3}},
+ {"rao", // L"饶绕扰荛桡娆"
+ {0x9976, 0x7ed5, 0x6270, 0x835b, 0x6861, 0x5a06}},
+ {"re", // L"热惹喏"
+ {0x70ed, 0x60f9, 0x558f}},
+ {"ren", // L"人任忍认刃仁韧妊纫壬饪轫仞荏葚衽稔亻"
+ {0x4eba, 0x4efb, 0x5fcd, 0x8ba4, 0x5203, 0x4ec1, 0x97e7, 0x598a, 0x7eab, 0x58ec, 0x996a,
+ 0x8f6b, 0x4ede, 0x834f, 0x845a, 0x887d, 0x7a14, 0x4ebb}},
+ {"reng", // L"仍扔"
+ {0x4ecd, 0x6254}},
+ {"ri", // L"日"
+ {0x65e5}},
+ {"rong", // L"容绒融溶熔荣戎蓉冗茸榕狨嵘肜蝾"
+ {0x5bb9, 0x7ed2, 0x878d, 0x6eb6, 0x7194, 0x8363, 0x620e, 0x84c9, 0x5197, 0x8338, 0x6995,
+ 0x72e8, 0x5d58, 0x809c, 0x877e}},
+ {"rou", // L"肉揉柔糅蹂鞣"
+ {0x8089, 0x63c9, 0x67d4, 0x7cc5, 0x8e42, 0x97a3}},
+ {"ru", // L"如入汝儒茹乳褥辱蠕孺蓐襦铷嚅缛濡薷颥溽洳"
+ {0x5982, 0x5165, 0x6c5d, 0x5112, 0x8339, 0x4e73, 0x8925, 0x8fb1, 0x8815, 0x5b7a,
+ 0x84d0, 0x8966, 0x94f7, 0x5685, 0x7f1b, 0x6fe1, 0x85b7, 0x98a5, 0x6ebd, 0x6d33}},
+ {"ruan", // L"软阮朊"
+ {0x8f6f, 0x962e, 0x670a}},
+ {"rui", // L"瑞蕊锐睿芮蚋枘蕤"
+ {0x745e, 0x854a, 0x9510, 0x777f, 0x82ae, 0x868b, 0x6798, 0x8564}},
+ {"run", // L"润闰"
+ {0x6da6, 0x95f0}},
+ {"ruo", // L"若弱箬偌"
+ {0x82e5, 0x5f31, 0x7bac, 0x504c}},
+ {"sa", // L"撒洒萨仨卅飒脎"
+ {0x6492, 0x6d12, 0x8428, 0x4ee8, 0x5345, 0x98d2, 0x810e}},
+ {"sai", // L"塞腮鳃赛噻"
+ {0x585e, 0x816e, 0x9cc3, 0x8d5b, 0x567b}},
+ {"san", // L"三散伞叁馓糁毵"
+ {0x4e09, 0x6563, 0x4f1e, 0x53c1, 0x9993, 0x7cc1, 0x6bf5}},
+ {"sang", // L"桑丧嗓颡磉搡"
+ {0x6851, 0x4e27, 0x55d3, 0x98a1, 0x78c9, 0x6421}},
+ {"sao", // L"扫嫂搔骚埽鳋臊缫瘙"
+ {0x626b, 0x5ac2, 0x6414, 0x9a9a, 0x57fd, 0x9ccb, 0x81ca, 0x7f2b, 0x7619}},
+ {"se", // L"色涩瑟塞啬铯穑"
+ {0x8272, 0x6da9, 0x745f, 0x585e, 0x556c, 0x94ef, 0x7a51}},
+ {"sen", // L"森"
+ {0x68ee}},
+ {"seng", // L"僧"
+ {0x50e7}},
+ {"sha", // L"杀沙啥纱傻砂刹莎厦煞杉唼鲨霎铩痧裟歃"
+ {0x6740, 0x6c99, 0x5565, 0x7eb1, 0x50bb, 0x7802, 0x5239, 0x838e, 0x53a6, 0x715e, 0x6749,
+ 0x553c, 0x9ca8, 0x970e, 0x94e9, 0x75e7, 0x88df, 0x6b43}},
+ {"shai", // L"晒筛色"
+ {0x6652, 0x7b5b, 0x8272}},
+ {"shan", // L"山闪衫善扇杉删煽单珊掺赡栅苫膳陕汕擅缮嬗蟮芟禅跚鄯潸鳝姗剡骟疝膻讪钐舢埏彡髟"
+ {0x5c71, 0x95ea, 0x886b, 0x5584, 0x6247, 0x6749, 0x5220, 0x717d, 0x5355, 0x73ca,
+ 0x63ba, 0x8d61, 0x6805, 0x82eb, 0x81b3, 0x9655, 0x6c55, 0x64c5, 0x7f2e, 0x5b17,
+ 0x87ee, 0x829f, 0x7985, 0x8dda, 0x912f, 0x6f78, 0x9cdd, 0x59d7, 0x5261, 0x9a9f,
+ 0x759d, 0x81bb, 0x8baa, 0x9490, 0x8222, 0x57cf, 0x5f61, 0x9adf}},
+ {"shang", // L"上伤尚商赏晌墒裳熵觞绱殇垧"
+ {0x4e0a, 0x4f24, 0x5c1a, 0x5546, 0x8d4f, 0x664c, 0x5892, 0x88f3, 0x71b5, 0x89de, 0x7ef1,
+ 0x6b87, 0x57a7}},
+ {"shao", // L"少烧捎哨勺梢稍邵韶绍芍鞘苕劭潲艄蛸筲"
+ {0x5c11, 0x70e7, 0x634e, 0x54e8, 0x52fa, 0x68a2, 0x7a0d, 0x90b5, 0x97f6, 0x7ecd, 0x828d,
+ 0x9798, 0x82d5, 0x52ad, 0x6f72, 0x8244, 0x86f8, 0x7b72}},
+ {"she", // L"社射蛇设舌摄舍折涉赊赦慑奢歙厍畲猞麝滠佘揲"
+ {0x793e, 0x5c04, 0x86c7, 0x8bbe, 0x820c, 0x6444, 0x820d, 0x6298, 0x6d89, 0x8d4a, 0x8d66,
+ 0x6151, 0x5962, 0x6b59, 0x538d, 0x7572, 0x731e, 0x9e9d, 0x6ee0, 0x4f58, 0x63f2}},
+ {"shei", // L"谁"
+ {0x8c01}},
+ {"shen", // L"身伸深婶神甚渗肾审申沈绅呻参砷什娠慎葚莘诜谂矧椹渖蜃哂胂"
+ {0x8eab, 0x4f38, 0x6df1, 0x5a76, 0x795e, 0x751a, 0x6e17, 0x80be, 0x5ba1, 0x7533,
+ 0x6c88, 0x7ec5, 0x547b, 0x53c2, 0x7837, 0x4ec0, 0x5a20, 0x614e, 0x845a, 0x8398,
+ 0x8bdc, 0x8c02, 0x77e7, 0x6939, 0x6e16, 0x8703, 0x54c2, 0x80c2}},
+ {"sheng", // L"声省剩生升绳胜盛圣甥牲乘晟渑眚笙嵊"
+ {0x58f0, 0x7701, 0x5269, 0x751f, 0x5347, 0x7ef3, 0x80dc, 0x76db, 0x5723, 0x7525, 0x7272,
+ 0x4e58, 0x665f, 0x6e11, 0x771a, 0x7b19, 0x5d4a}},
+ {"shi", // L"是使十时事室市石师试史式识虱矢拾屎驶始似示士世柿匙拭誓逝势什殖峙嗜噬失适仕侍释饰氏狮食恃蚀视实施湿诗尸豕莳埘铈舐鲥鲺贳轼蓍筮炻谥弑酾螫礻铊饣"
+ {0x662f, 0x4f7f, 0x5341, 0x65f6, 0x4e8b, 0x5ba4, 0x5e02, 0x77f3, 0x5e08, 0x8bd5,
+ 0x53f2, 0x5f0f, 0x8bc6, 0x8671, 0x77e2, 0x62fe, 0x5c4e, 0x9a76, 0x59cb, 0x4f3c,
+ 0x793a, 0x58eb, 0x4e16, 0x67ff, 0x5319, 0x62ed, 0x8a93, 0x901d, 0x52bf, 0x4ec0,
+ 0x6b96, 0x5cd9, 0x55dc, 0x566c, 0x5931, 0x9002, 0x4ed5, 0x4f8d, 0x91ca, 0x9970,
+ 0x6c0f, 0x72ee, 0x98df, 0x6043, 0x8680, 0x89c6, 0x5b9e, 0x65bd, 0x6e7f, 0x8bd7,
+ 0x5c38, 0x8c55, 0x83b3, 0x57d8, 0x94c8, 0x8210, 0x9ca5, 0x9cba, 0x8d33, 0x8f7c,
+ 0x84cd, 0x7b6e, 0x70bb, 0x8c25, 0x5f11, 0x917e, 0x87ab, 0x793b, 0x94ca, 0x9963}},
+ {"shou", // L"手受收首守瘦授兽售寿艏狩绶扌"
+ {0x624b, 0x53d7, 0x6536, 0x9996, 0x5b88, 0x7626, 0x6388, 0x517d, 0x552e, 0x5bff, 0x824f,
+ 0x72e9, 0x7ef6, 0x624c}},
+ {"shu", // L"书树数熟输梳叔属束术述蜀黍鼠淑赎孰蔬疏戍竖墅庶薯漱恕枢暑殊抒曙署舒姝摅秫纾沭毹腧塾菽殳澍倏疋镯"
+ {0x4e66, 0x6811, 0x6570, 0x719f, 0x8f93, 0x68b3, 0x53d4, 0x5c5e, 0x675f, 0x672f,
+ 0x8ff0, 0x8700, 0x9ecd, 0x9f20, 0x6dd1, 0x8d4e, 0x5b70, 0x852c, 0x758f, 0x620d,
+ 0x7ad6, 0x5885, 0x5eb6, 0x85af, 0x6f31, 0x6055, 0x67a2, 0x6691, 0x6b8a, 0x6292,
+ 0x66d9, 0x7f72, 0x8212, 0x59dd, 0x6445, 0x79eb, 0x7ebe, 0x6cad, 0x6bf9, 0x8167,
+ 0x587e, 0x83fd, 0x6bb3, 0x6f8d, 0x500f, 0x758b, 0x956f}},
+ {"shua", // L"刷耍唰"
+ {0x5237, 0x800d, 0x5530}},
+ {"shuai", // L"摔甩率帅衰蟀"
+ {0x6454, 0x7529, 0x7387, 0x5e05, 0x8870, 0x87c0}},
+ {"shuan", // L"栓拴闩涮"
+ {0x6813, 0x62f4, 0x95e9, 0x6dae}},
+ {"shuang", // L"双霜爽泷孀"
+ {0x53cc, 0x971c, 0x723d, 0x6cf7, 0x5b40}},
+ {"shui", // L"水睡税说氵"
+ {0x6c34, 0x7761, 0x7a0e, 0x8bf4, 0x6c35}},
+ {"shun", // L"顺吮瞬舜"
+ {0x987a, 0x542e, 0x77ac, 0x821c}},
+ {"shuo", // L"说数硕烁朔搠妁槊蒴铄"
+ {0x8bf4, 0x6570, 0x7855, 0x70c1, 0x6714, 0x6420, 0x5981, 0x69ca, 0x84b4, 0x94c4}},
+ {"si", // L"四死丝撕似私嘶思寺司斯伺肆饲嗣巳耜驷兕蛳厮汜锶泗笥咝鸶姒厶缌祀澌俟徙"
+ {0x56db, 0x6b7b, 0x4e1d, 0x6495, 0x4f3c, 0x79c1, 0x5636, 0x601d, 0x5bfa,
+ 0x53f8, 0x65af, 0x4f3a, 0x8086, 0x9972, 0x55e3, 0x5df3, 0x801c, 0x9a77,
+ 0x5155, 0x86f3, 0x53ae, 0x6c5c, 0x9536, 0x6cd7, 0x7b25, 0x549d, 0x9e36,
+ 0x59d2, 0x53b6, 0x7f0c, 0x7940, 0x6f8c, 0x4fdf, 0x5f99}},
+ {"song", // L"送松耸宋颂诵怂讼竦菘淞悚嵩凇崧忪"
+ {0x9001, 0x677e, 0x8038, 0x5b8b, 0x9882, 0x8bf5, 0x6002, 0x8bbc, 0x7ae6, 0x83d8, 0x6dde,
+ 0x609a, 0x5d69, 0x51c7, 0x5d27, 0x5fea}},
+ {"sou", // L"艘搜擞嗽嗾嗖飕叟薮锼馊瞍溲螋"
+ {0x8258, 0x641c, 0x64de, 0x55fd, 0x55fe, 0x55d6, 0x98d5, 0x53df, 0x85ae, 0x953c, 0x998a,
+ 0x778d, 0x6eb2, 0x878b}},
+ {"su", // L"素速诉塑宿俗苏肃粟酥缩溯僳愫簌觫稣夙嗉谡蔌涑"
+ {0x7d20, 0x901f, 0x8bc9, 0x5851, 0x5bbf, 0x4fd7, 0x82cf, 0x8083, 0x7c9f, 0x9165, 0x7f29,
+ 0x6eaf, 0x50f3, 0x612b, 0x7c0c, 0x89eb, 0x7a23, 0x5919, 0x55c9, 0x8c21, 0x850c, 0x6d91}},
+ {"suan", // L"酸算蒜狻"
+ {0x9178, 0x7b97, 0x849c, 0x72fb}},
+ {"sui", // L"岁随碎虽穗遂尿隋髓绥隧祟眭谇濉邃燧荽睢"
+ {0x5c81, 0x968f, 0x788e, 0x867d, 0x7a57, 0x9042, 0x5c3f, 0x968b, 0x9ad3, 0x7ee5, 0x96a7,
+ 0x795f, 0x772d, 0x8c07, 0x6fc9, 0x9083, 0x71e7, 0x837d, 0x7762}},
+ {"sun", // L"孙损笋榫荪飧狲隼"
+ {0x5b59, 0x635f, 0x7b0b, 0x69ab, 0x836a, 0x98e7, 0x72f2, 0x96bc}},
+ {"suo", // L"所缩锁琐索梭蓑莎唆挲睃嗍唢桫嗦娑羧"
+ {0x6240, 0x7f29, 0x9501, 0x7410, 0x7d22, 0x68ad, 0x84d1, 0x838e, 0x5506, 0x6332, 0x7743,
+ 0x55cd, 0x5522, 0x686b, 0x55e6, 0x5a11, 0x7fa7}},
+ {"ta", // L"他她它踏塔塌拓獭挞蹋溻趿鳎沓榻漯遢铊闼"
+ {0x4ed6, 0x5979, 0x5b83, 0x8e0f, 0x5854, 0x584c, 0x62d3, 0x736d, 0x631e, 0x8e4b, 0x6ebb,
+ 0x8dbf, 0x9cce, 0x6c93, 0x69bb, 0x6f2f, 0x9062, 0x94ca, 0x95fc}},
+ {"tai", // L"太抬台态胎苔泰酞汰炱肽跆鲐钛薹邰骀"
+ {0x592a, 0x62ac, 0x53f0, 0x6001, 0x80ce, 0x82d4, 0x6cf0, 0x915e, 0x6c70, 0x70b1, 0x80bd,
+ 0x8dc6, 0x9c90, 0x949b, 0x85b9, 0x90b0, 0x9a80}},
+ {"tan", // L"谈叹探滩弹碳摊潭贪坛痰毯坦炭瘫谭坍檀袒钽郯镡锬覃澹昙忐赕"
+ {0x8c08, 0x53f9, 0x63a2, 0x6ee9, 0x5f39, 0x78b3, 0x644a, 0x6f6d, 0x8d2a, 0x575b,
+ 0x75f0, 0x6bef, 0x5766, 0x70ad, 0x762b, 0x8c2d, 0x574d, 0x6a80, 0x8892, 0x94bd,
+ 0x90ef, 0x9561, 0x952c, 0x8983, 0x6fb9, 0x6619, 0x5fd0, 0x8d55}},
+ {"tang", // L"躺趟堂糖汤塘烫倘淌唐搪棠膛螳樘羰醣瑭镗傥饧溏耥帑铴螗铛"
+ {0x8eba, 0x8d9f, 0x5802, 0x7cd6, 0x6c64, 0x5858, 0x70eb, 0x5018, 0x6dcc,
+ 0x5510, 0x642a, 0x68e0, 0x819b, 0x87b3, 0x6a18, 0x7fb0, 0x91a3, 0x746d,
+ 0x9557, 0x50a5, 0x9967, 0x6e8f, 0x8025, 0x5e11, 0x94f4, 0x8797, 0x94db}},
+ {"tao", // L"套掏逃桃讨淘涛滔陶绦萄鼗洮焘啕饕韬叨"
+ {0x5957, 0x638f, 0x9003, 0x6843, 0x8ba8, 0x6dd8, 0x6d9b, 0x6ed4, 0x9676, 0x7ee6, 0x8404,
+ 0x9f17, 0x6d2e, 0x7118, 0x5555, 0x9955, 0x97ec, 0x53e8}},
+ {"te", // L"特铽忑忒"
+ {0x7279, 0x94fd, 0x5fd1, 0x5fd2}},
+ {"teng", // L"疼腾藤誊滕"
+ {0x75bc, 0x817e, 0x85e4, 0x8a8a, 0x6ed5}},
+ {"ti", // L"提替体题踢蹄剃剔梯锑啼涕嚏惕屉醍鹈绨缇倜裼逖荑悌"
+ {0x63d0, 0x66ff, 0x4f53, 0x9898, 0x8e22, 0x8e44, 0x5243, 0x5254,
+ 0x68af, 0x9511, 0x557c, 0x6d95, 0x568f, 0x60d5, 0x5c49, 0x918d,
+ 0x9e48, 0x7ee8, 0x7f07, 0x501c, 0x88fc, 0x9016, 0x8351, 0x608c}},
+ {"tian", // L"天田添填甜舔恬腆掭钿阗忝殄畋锘"
+ {0x5929, 0x7530, 0x6dfb, 0x586b, 0x751c, 0x8214, 0x606c, 0x8146, 0x63ad, 0x94bf, 0x9617,
+ 0x5fdd, 0x6b84, 0x754b, 0x9518}},
+ {"tiao", // L"条跳挑调迢眺龆笤祧蜩髫佻窕鲦苕粜铫"
+ {0x6761, 0x8df3, 0x6311, 0x8c03, 0x8fe2, 0x773a, 0x9f86, 0x7b24, 0x7967, 0x8729, 0x9aeb,
+ 0x4f7b, 0x7a95, 0x9ca6, 0x82d5, 0x7c9c, 0x94eb}},
+ {"tie", // L"铁贴帖萜餮锇"
+ {0x94c1, 0x8d34, 0x5e16, 0x841c, 0x992e, 0x9507}},
+ {"ting", // L"听停挺厅亭艇庭廷烃汀莛铤葶婷蜓梃霆"
+ {0x542c, 0x505c, 0x633a, 0x5385, 0x4ead, 0x8247, 0x5ead, 0x5ef7, 0x70c3, 0x6c40, 0x839b,
+ 0x94e4, 0x8476, 0x5a77, 0x8713, 0x6883, 0x9706}},
+ {"tong", // L"同通痛铜桶筒捅统童彤桐瞳酮潼茼仝砼峒恸佟嗵垌僮"
+ {0x540c, 0x901a, 0x75db, 0x94dc, 0x6876, 0x7b52, 0x6345, 0x7edf,
+ 0x7ae5, 0x5f64, 0x6850, 0x77b3, 0x916e, 0x6f7c, 0x833c, 0x4edd,
+ 0x783c, 0x5cd2, 0x6078, 0x4f5f, 0x55f5, 0x578c, 0x50ee}},
+ {"tou", // L"头偷透投钭骰亠"
+ {0x5934, 0x5077, 0x900f, 0x6295, 0x94ad, 0x9ab0, 0x4ea0}},
+ {"tu", // L"土图兔涂吐秃突徒凸途屠酴荼钍菟堍"
+ {0x571f, 0x56fe, 0x5154, 0x6d82, 0x5410, 0x79c3, 0x7a81, 0x5f92, 0x51f8, 0x9014, 0x5c60,
+ 0x9174, 0x837c, 0x948d, 0x83df, 0x580d}},
+ {"tuan", // L"团湍疃抟彖"
+ {0x56e2, 0x6e4d, 0x7583, 0x629f, 0x5f56}},
+ {"tui", // L"腿推退褪颓蜕煺"
+ {0x817f, 0x63a8, 0x9000, 0x892a, 0x9893, 0x8715, 0x717a}},
+ {"tun", // L"吞屯褪臀囤氽饨豚暾"
+ {0x541e, 0x5c6f, 0x892a, 0x81c0, 0x56e4, 0x6c3d, 0x9968, 0x8c5a, 0x66be}},
+ {"tuo", // L"拖脱托妥驮拓驼椭唾鸵陀橐柝跎乇坨佗庹酡柁鼍沱箨砣说铊"
+ {0x62d6, 0x8131, 0x6258, 0x59a5, 0x9a6e, 0x62d3, 0x9a7c, 0x692d, 0x553e,
+ 0x9e35, 0x9640, 0x6a50, 0x67dd, 0x8dce, 0x4e47, 0x5768, 0x4f57, 0x5eb9,
+ 0x9161, 0x67c1, 0x9f0d, 0x6cb1, 0x7ba8, 0x7823, 0x8bf4, 0x94ca}},
+ {"wa", // L"挖瓦蛙哇娃洼袜佤娲腽"
+ {0x6316, 0x74e6, 0x86d9, 0x54c7, 0x5a03, 0x6d3c, 0x889c, 0x4f64, 0x5a32, 0x817d}},
+ {"wai", // L"外歪崴"
+ {0x5916, 0x6b6a, 0x5d34}},
+ {"wan", // L"完万晚碗玩弯挽湾丸腕宛婉烷顽豌惋皖蔓莞脘蜿绾芄琬纨剜畹菀"
+ {0x5b8c, 0x4e07, 0x665a, 0x7897, 0x73a9, 0x5f2f, 0x633d, 0x6e7e, 0x4e38, 0x8155,
+ 0x5b9b, 0x5a49, 0x70f7, 0x987d, 0x8c4c, 0x60cb, 0x7696, 0x8513, 0x839e, 0x8118,
+ 0x873f, 0x7efe, 0x8284, 0x742c, 0x7ea8, 0x525c, 0x7579, 0x83c0}},
+ {"wang", // L"望忘王往网亡枉旺汪妄辋魍惘罔尢"
+ {0x671b, 0x5fd8, 0x738b, 0x5f80, 0x7f51, 0x4ea1, 0x6789, 0x65fa, 0x6c6a, 0x5984, 0x8f8b,
+ 0x9b4d, 0x60d8, 0x7f54, 0x5c22}},
+ {"wei", // L"为位未围喂胃微味尾伪威伟卫危违委魏唯维畏惟韦巍蔚谓尉潍纬慰桅萎苇渭葳帏艉鲔娓逶闱隈沩玮涠帷崴隗诿洧偎猥猬嵬軎韪炜煨圩薇痿囗"
+ {0x4e3a, 0x4f4d, 0x672a, 0x56f4, 0x5582, 0x80c3, 0x5fae, 0x5473, 0x5c3e, 0x4f2a, 0x5a01,
+ 0x4f1f, 0x536b, 0x5371, 0x8fdd, 0x59d4, 0x9b4f, 0x552f, 0x7ef4, 0x754f, 0x60df, 0x97e6,
+ 0x5dcd, 0x851a, 0x8c13, 0x5c09, 0x6f4d, 0x7eac, 0x6170, 0x6845, 0x840e, 0x82c7, 0x6e2d,
+ 0x8473, 0x5e0f, 0x8249, 0x9c94, 0x5a13, 0x9036, 0x95f1, 0x9688, 0x6ca9, 0x73ae, 0x6da0,
+ 0x5e37, 0x5d34, 0x9697, 0x8bff, 0x6d27, 0x504e, 0x7325, 0x732c, 0x5d6c, 0x8ece, 0x97ea,
+ 0x709c, 0x7168, 0x5729, 0x8587, 0x75ff, 0x56d7}},
+ {"wen", // L"问文闻稳温吻蚊纹瘟紊汶阌刎雯璺"
+ {0x95ee, 0x6587, 0x95fb, 0x7a33, 0x6e29, 0x543b, 0x868a, 0x7eb9, 0x761f, 0x7d0a, 0x6c76,
+ 0x960c, 0x520e, 0x96ef, 0x74ba}},
+ {"weng", // L"翁嗡瓮蕹蓊"
+ {0x7fc1, 0x55e1, 0x74ee, 0x8579, 0x84ca}},
+ {"wo", // L"我握窝卧挝沃蜗涡斡倭幄龌肟莴喔渥硪"
+ {0x6211, 0x63e1, 0x7a9d, 0x5367, 0x631d, 0x6c83, 0x8717, 0x6da1, 0x65a1, 0x502d, 0x5e44,
+ 0x9f8c, 0x809f, 0x83b4, 0x5594, 0x6e25, 0x786a}},
+ {"wu", // L"无五屋物舞雾误捂污悟勿钨武戊务呜伍吴午吾侮乌毋恶诬芜巫晤梧坞妩蜈牾寤兀怃阢邬唔忤骛於鋈仵杌鹜婺迕痦芴焐庑鹉鼯浯圬"
+ {0x65e0, 0x4e94, 0x5c4b, 0x7269, 0x821e, 0x96fe, 0x8bef, 0x6342, 0x6c61, 0x609f,
+ 0x52ff, 0x94a8, 0x6b66, 0x620a, 0x52a1, 0x545c, 0x4f0d, 0x5434, 0x5348, 0x543e,
+ 0x4fae, 0x4e4c, 0x6bcb, 0x6076, 0x8bec, 0x829c, 0x5deb, 0x6664, 0x68a7, 0x575e,
+ 0x59a9, 0x8708, 0x727e, 0x5be4, 0x5140, 0x6003, 0x9622, 0x90ac, 0x5514, 0x5fe4,
+ 0x9a9b, 0x65bc, 0x92c8, 0x4ef5, 0x674c, 0x9e5c, 0x5a7a, 0x8fd5, 0x75e6, 0x82b4,
+ 0x7110, 0x5e91, 0x9e49, 0x9f2f, 0x6d6f, 0x572c}},
+ {"xi", // L"西洗细吸戏系喜席稀溪熄锡膝息袭惜习嘻夕悉矽熙希檄牺晰昔媳硒铣烯析隙汐犀蜥奚浠葸饩屣玺嬉禊兮翕穸禧僖淅蓰舾蹊醯郗欷皙蟋羲茜徙隰唏曦螅歙樨阋粞熹觋菥鼷裼舄"
+ {0x897f, 0x6d17, 0x7ec6, 0x5438, 0x620f, 0x7cfb, 0x559c, 0x5e2d, 0x7a00, 0x6eaa, 0x7184,
+ 0x9521, 0x819d, 0x606f, 0x88ad, 0x60dc, 0x4e60, 0x563b, 0x5915, 0x6089, 0x77fd, 0x7199,
+ 0x5e0c, 0x6a84, 0x727a, 0x6670, 0x6614, 0x5ab3, 0x7852, 0x94e3, 0x70ef, 0x6790, 0x9699,
+ 0x6c50, 0x7280, 0x8725, 0x595a, 0x6d60, 0x8478, 0x9969, 0x5c63, 0x73ba, 0x5b09, 0x798a,
+ 0x516e, 0x7fd5, 0x7a78, 0x79a7, 0x50d6, 0x6dc5, 0x84f0, 0x823e, 0x8e4a, 0x91af, 0x90d7,
+ 0x6b37, 0x7699, 0x87cb, 0x7fb2, 0x831c, 0x5f99, 0x96b0, 0x550f, 0x66e6, 0x8785, 0x6b59,
+ 0x6a28, 0x960b, 0x7c9e, 0x71b9, 0x89cb, 0x83e5, 0x9f37, 0x88fc, 0x8204}},
+ {"xia", // L"下吓夏峡虾瞎霞狭匣侠辖厦暇狎柙呷黠硖罅遐瑕"
+ {0x4e0b, 0x5413, 0x590f, 0x5ce1, 0x867e, 0x778e, 0x971e, 0x72ed, 0x5323, 0x4fa0, 0x8f96,
+ 0x53a6, 0x6687, 0x72ce, 0x67d9, 0x5477, 0x9ee0, 0x7856, 0x7f45, 0x9050, 0x7455}},
+ {"xian", // L"先线县现显掀闲献嫌陷险鲜弦衔馅限咸锨仙腺贤纤宪舷涎羡铣苋藓岘痫莶籼娴蚬猃祆冼燹跣跹酰暹氙鹇筅霰洗"
+ {0x5148, 0x7ebf, 0x53bf, 0x73b0, 0x663e, 0x6380, 0x95f2, 0x732e, 0x5acc, 0x9677,
+ 0x9669, 0x9c9c, 0x5f26, 0x8854, 0x9985, 0x9650, 0x54b8, 0x9528, 0x4ed9, 0x817a,
+ 0x8d24, 0x7ea4, 0x5baa, 0x8237, 0x6d8e, 0x7fa1, 0x94e3, 0x82cb, 0x85d3, 0x5c98,
+ 0x75eb, 0x83b6, 0x7c7c, 0x5a34, 0x86ac, 0x7303, 0x7946, 0x51bc, 0x71f9, 0x8de3,
+ 0x8df9, 0x9170, 0x66b9, 0x6c19, 0x9e47, 0x7b45, 0x9730, 0x6d17}},
+ {"xiang", // L"想向象项响香乡相像箱巷享镶厢降翔祥橡详湘襄飨鲞骧蟓庠芗饷缃葙"
+ {0x60f3, 0x5411, 0x8c61, 0x9879, 0x54cd, 0x9999, 0x4e61, 0x76f8, 0x50cf, 0x7bb1,
+ 0x5df7, 0x4eab, 0x9576, 0x53a2, 0x964d, 0x7fd4, 0x7965, 0x6a61, 0x8be6, 0x6e58,
+ 0x8944, 0x98e8, 0x9c9e, 0x9aa7, 0x87d3, 0x5ea0, 0x8297, 0x9977, 0x7f03, 0x8459}},
+ {"xiao", // L"小笑消削销萧效宵晓肖孝硝淆啸霄哮嚣校魈蛸骁枵哓筱潇逍枭绡箫"
+ {0x5c0f, 0x7b11, 0x6d88, 0x524a, 0x9500, 0x8427, 0x6548, 0x5bb5, 0x6653, 0x8096,
+ 0x5b5d, 0x785d, 0x6dc6, 0x5578, 0x9704, 0x54ee, 0x56a3, 0x6821, 0x9b48, 0x86f8,
+ 0x9a81, 0x67b5, 0x54d3, 0x7b71, 0x6f47, 0x900d, 0x67ad, 0x7ee1, 0x7bab}},
+ {"xie", // L"写些鞋歇斜血谢卸挟屑蟹泻懈泄楔邪协械谐蝎携胁解叶绁颉缬獬榭廨撷偕瀣渫亵榍邂薤躞燮勰骱鲑"
+ {0x5199, 0x4e9b, 0x978b, 0x6b47, 0x659c, 0x8840, 0x8c22, 0x5378, 0x631f, 0x5c51, 0x87f9,
+ 0x6cfb, 0x61c8, 0x6cc4, 0x6954, 0x90aa, 0x534f, 0x68b0, 0x8c10, 0x874e, 0x643a, 0x80c1,
+ 0x89e3, 0x53f6, 0x7ec1, 0x9889, 0x7f2c, 0x736c, 0x69ad, 0x5ee8, 0x64b7, 0x5055, 0x7023,
+ 0x6e2b, 0x4eb5, 0x698d, 0x9082, 0x85a4, 0x8e9e, 0x71ee, 0x52f0, 0x9ab1, 0x9c91}},
+ {"xin", // L"新心欣信芯薪锌辛衅忻歆囟莘镡馨鑫昕忄"
+ {0x65b0, 0x5fc3, 0x6b23, 0x4fe1, 0x82af, 0x85aa, 0x950c, 0x8f9b, 0x8845, 0x5ffb, 0x6b46,
+ 0x56df, 0x8398, 0x9561, 0x99a8, 0x946b, 0x6615, 0x5fc4}},
+ {"xing", // L"性行型形星醒姓腥刑杏兴幸邢猩惺省硎悻荥陉擤荇研饧"
+ {0x6027, 0x884c, 0x578b, 0x5f62, 0x661f, 0x9192, 0x59d3, 0x8165,
+ 0x5211, 0x674f, 0x5174, 0x5e78, 0x90a2, 0x7329, 0x60fa, 0x7701,
+ 0x784e, 0x60bb, 0x8365, 0x9649, 0x64e4, 0x8347, 0x7814, 0x9967}},
+ {"xiong", // L"胸雄凶兄熊汹匈芎"
+ {0x80f8, 0x96c4, 0x51f6, 0x5144, 0x718a, 0x6c79, 0x5308, 0x828e}},
+ {"xiu", // L"修锈绣休羞宿嗅袖秀朽臭溴貅馐髹鸺咻庥岫"
+ {0x4fee, 0x9508, 0x7ee3, 0x4f11, 0x7f9e, 0x5bbf, 0x55c5, 0x8896, 0x79c0, 0x673d, 0x81ed,
+ 0x6eb4, 0x8c85, 0x9990, 0x9af9, 0x9e3a, 0x54bb, 0x5ea5, 0x5cab}},
+ {"xu", // L"许须需虚嘘蓄续序叙畜絮婿戌徐旭绪吁酗恤墟糈勖栩浒蓿顼圩洫胥醑诩溆煦盱"
+ {0x8bb8, 0x987b, 0x9700, 0x865a, 0x5618, 0x84c4, 0x7eed, 0x5e8f, 0x53d9,
+ 0x755c, 0x7d6e, 0x5a7f, 0x620c, 0x5f90, 0x65ed, 0x7eea, 0x5401, 0x9157,
+ 0x6064, 0x589f, 0x7cc8, 0x52d6, 0x6829, 0x6d52, 0x84ff, 0x987c, 0x5729,
+ 0x6d2b, 0x80e5, 0x9191, 0x8be9, 0x6e86, 0x7166, 0x76f1}},
+ {"xuan", // L"选悬旋玄宣喧轩绚眩癣券暄楦儇渲漩泫铉璇煊碹镟炫揎萱谖"
+ {0x9009, 0x60ac, 0x65cb, 0x7384, 0x5ba3, 0x55a7, 0x8f69, 0x7eda, 0x7729,
+ 0x7663, 0x5238, 0x6684, 0x6966, 0x5107, 0x6e32, 0x6f29, 0x6ceb, 0x94c9,
+ 0x7487, 0x714a, 0x78b9, 0x955f, 0x70ab, 0x63ce, 0x8431, 0x8c16}},
+ {"xue", // L"学雪血靴穴削薛踅噱鳕泶谑"
+ {0x5b66, 0x96ea, 0x8840, 0x9774, 0x7a74, 0x524a, 0x859b, 0x8e05, 0x5671, 0x9cd5, 0x6cf6,
+ 0x8c11}},
+ {"xun", // L"寻讯熏训循殉旬巡迅驯汛逊勋询浚巽鲟浔埙恂獯醺洵郇峋蕈薰荀窨曛徇荨"
+ {0x5bfb, 0x8baf, 0x718f, 0x8bad, 0x5faa, 0x6b89, 0x65ec, 0x5de1, 0x8fc5, 0x9a6f, 0x6c5b,
+ 0x900a, 0x52cb, 0x8be2, 0x6d5a, 0x5dfd, 0x9c9f, 0x6d54, 0x57d9, 0x6042, 0x736f, 0x91ba,
+ 0x6d35, 0x90c7, 0x5ccb, 0x8548, 0x85b0, 0x8340, 0x7aa8, 0x66db, 0x5f87, 0x8368}},
+ {"ya", // L"呀压牙押芽鸭轧崖哑亚涯丫雅衙鸦讶蚜垭疋砑琊桠睚娅痖岈氩伢迓揠"
+ {0x5440, 0x538b, 0x7259, 0x62bc, 0x82bd, 0x9e2d, 0x8f67, 0x5d16, 0x54d1, 0x4e9a,
+ 0x6daf, 0x4e2b, 0x96c5, 0x8859, 0x9e26, 0x8bb6, 0x869c, 0x57ad, 0x758b, 0x7811,
+ 0x740a, 0x6860, 0x775a, 0x5a05, 0x75d6, 0x5c88, 0x6c29, 0x4f22, 0x8fd3, 0x63e0}},
+ {"yan", // L"眼烟沿盐言演严咽淹炎掩厌宴岩研延堰验艳殷阉砚雁唁彦焰蜒衍谚燕颜阎铅焉奄芫厣阏菸魇琰滟焱赝筵腌兖剡餍恹罨檐湮偃谳胭晏闫俨郾酽鄢妍鼹崦阽嫣涎讠"
+ {0x773c, 0x70df, 0x6cbf, 0x76d0, 0x8a00, 0x6f14, 0x4e25, 0x54bd, 0x6df9, 0x708e,
+ 0x63a9, 0x538c, 0x5bb4, 0x5ca9, 0x7814, 0x5ef6, 0x5830, 0x9a8c, 0x8273, 0x6bb7,
+ 0x9609, 0x781a, 0x96c1, 0x5501, 0x5f66, 0x7130, 0x8712, 0x884d, 0x8c1a, 0x71d5,
+ 0x989c, 0x960e, 0x94c5, 0x7109, 0x5944, 0x82ab, 0x53a3, 0x960f, 0x83f8, 0x9b47,
+ 0x7430, 0x6edf, 0x7131, 0x8d5d, 0x7b75, 0x814c, 0x5156, 0x5261, 0x990d, 0x6079,
+ 0x7f68, 0x6a90, 0x6e6e, 0x5043, 0x8c33, 0x80ed, 0x664f, 0x95eb, 0x4fe8, 0x90fe,
+ 0x917d, 0x9122, 0x598d, 0x9f39, 0x5d26, 0x963d, 0x5ae3, 0x6d8e, 0x8ba0}},
+ {"yang", // L"样养羊洋仰扬秧氧痒杨漾阳殃央鸯佯疡炀恙徉鞅泱蛘烊怏"
+ {0x6837, 0x517b, 0x7f8a, 0x6d0b, 0x4ef0, 0x626c, 0x79e7, 0x6c27, 0x75d2,
+ 0x6768, 0x6f3e, 0x9633, 0x6b83, 0x592e, 0x9e2f, 0x4f6f, 0x75a1, 0x7080,
+ 0x6059, 0x5f89, 0x9785, 0x6cf1, 0x86d8, 0x70ca, 0x600f}},
+ {"yao", // L"要摇药咬腰窑舀邀妖谣遥姚瑶耀尧钥侥疟珧夭鳐鹞轺爻吆铫幺崾肴曜徭杳窈啮繇"
+ {0x8981, 0x6447, 0x836f, 0x54ac, 0x8170, 0x7a91, 0x8200, 0x9080, 0x5996,
+ 0x8c23, 0x9065, 0x59da, 0x7476, 0x8000, 0x5c27, 0x94a5, 0x4fa5, 0x759f,
+ 0x73e7, 0x592d, 0x9cd0, 0x9e5e, 0x8f7a, 0x723b, 0x5406, 0x94eb, 0x5e7a,
+ 0x5d3e, 0x80b4, 0x66dc, 0x5fad, 0x6773, 0x7a88, 0x556e, 0x7e47}},
+ {"ye", // L"也夜业野叶爷页液掖腋冶噎耶咽曳椰邪谒邺晔烨揶铘靥"
+ {0x4e5f, 0x591c, 0x4e1a, 0x91ce, 0x53f6, 0x7237, 0x9875, 0x6db2,
+ 0x6396, 0x814b, 0x51b6, 0x564e, 0x8036, 0x54bd, 0x66f3, 0x6930,
+ 0x90aa, 0x8c12, 0x90ba, 0x6654, 0x70e8, 0x63f6, 0x94d8, 0x9765}},
+ {"yi", // L"一以已亿衣移依易医乙仪亦椅益倚姨翼译伊遗艾胰疑沂宜异彝壹蚁谊揖铱矣翌艺抑绎邑屹尾役臆逸肄疫颐裔意毅忆义夷溢诣议怿痍镒癔怡驿旖熠酏翊欹峄圯殪咦懿噫劓诒饴漪佚咿瘗猗眙羿弈苡荑佾贻钇缢迤刈悒黟翳弋奕蜴埸挹嶷薏呓轶镱舣奇硪衤铊"
+ {0x4e00, 0x4ee5, 0x5df2, 0x4ebf, 0x8863, 0x79fb, 0x4f9d, 0x6613, 0x533b, 0x4e59, 0x4eea,
+ 0x4ea6, 0x6905, 0x76ca, 0x501a, 0x59e8, 0x7ffc, 0x8bd1, 0x4f0a, 0x9057, 0x827e, 0x80f0,
+ 0x7591, 0x6c82, 0x5b9c, 0x5f02, 0x5f5d, 0x58f9, 0x8681, 0x8c0a, 0x63d6, 0x94f1, 0x77e3,
+ 0x7fcc, 0x827a, 0x6291, 0x7ece, 0x9091, 0x5c79, 0x5c3e, 0x5f79, 0x81c6, 0x9038, 0x8084,
+ 0x75ab, 0x9890, 0x88d4, 0x610f, 0x6bc5, 0x5fc6, 0x4e49, 0x5937, 0x6ea2, 0x8be3, 0x8bae,
+ 0x603f, 0x75cd, 0x9552, 0x7654, 0x6021, 0x9a7f, 0x65d6, 0x71a0, 0x914f, 0x7fca, 0x6b39,
+ 0x5cc4, 0x572f, 0x6baa, 0x54a6, 0x61ff, 0x566b, 0x5293, 0x8bd2, 0x9974, 0x6f2a, 0x4f5a,
+ 0x54bf, 0x7617, 0x7317, 0x7719, 0x7fbf, 0x5f08, 0x82e1, 0x8351, 0x4f7e, 0x8d3b, 0x9487,
+ 0x7f22, 0x8fe4, 0x5208, 0x6092, 0x9edf, 0x7ff3, 0x5f0b, 0x5955, 0x8734, 0x57f8, 0x6339,
+ 0x5db7, 0x858f, 0x5453, 0x8f76, 0x9571, 0x8223, 0x5947, 0x786a, 0x8864, 0x94ca}},
+ {"yin", // L"因引印银音饮阴隐荫吟尹寅茵淫殷姻堙鄞喑夤胤龈吲狺垠霪蚓氤铟窨瘾洇茚廴"
+ {0x56e0, 0x5f15, 0x5370, 0x94f6, 0x97f3, 0x996e, 0x9634, 0x9690, 0x836b,
+ 0x541f, 0x5c39, 0x5bc5, 0x8335, 0x6deb, 0x6bb7, 0x59fb, 0x5819, 0x911e,
+ 0x5591, 0x5924, 0x80e4, 0x9f88, 0x5432, 0x72fa, 0x57a0, 0x972a, 0x8693,
+ 0x6c24, 0x94df, 0x7aa8, 0x763e, 0x6d07, 0x831a, 0x5ef4}},
+ {"ying", // L"应硬影营迎映蝇赢鹰英颖莹盈婴樱缨荧萤萦楹蓥瘿茔鹦媵莺璎郢嘤撄瑛滢潆嬴罂瀛膺荥颍"
+ {0x5e94, 0x786c, 0x5f71, 0x8425, 0x8fce, 0x6620, 0x8747, 0x8d62, 0x9e70, 0x82f1,
+ 0x9896, 0x83b9, 0x76c8, 0x5a74, 0x6a31, 0x7f28, 0x8367, 0x8424, 0x8426, 0x6979,
+ 0x84e5, 0x763f, 0x8314, 0x9e66, 0x5ab5, 0x83ba, 0x748e, 0x90e2, 0x5624, 0x6484,
+ 0x745b, 0x6ee2, 0x6f46, 0x5b34, 0x7f42, 0x701b, 0x81ba, 0x8365, 0x988d}},
+ {"yo", // L"哟育唷"
+ {0x54df, 0x80b2, 0x5537}},
+ {"yong", // L"用涌永拥蛹勇雍咏泳佣踊痈庸臃恿壅慵俑墉鳙邕喁甬饔镛"
+ {0x7528, 0x6d8c, 0x6c38, 0x62e5, 0x86f9, 0x52c7, 0x96cd, 0x548f, 0x6cf3,
+ 0x4f63, 0x8e0a, 0x75c8, 0x5eb8, 0x81c3, 0x607f, 0x58c5, 0x6175, 0x4fd1,
+ 0x5889, 0x9cd9, 0x9095, 0x5581, 0x752c, 0x9954, 0x955b}},
+ {"you", // L"有又由右油游幼优友铀忧尤犹诱悠邮酉佑釉幽疣攸蚰莠鱿卣黝莸猷蚴宥牖囿柚蝣莜鼬铕蝤繇呦侑尢"
+ {0x6709, 0x53c8, 0x7531, 0x53f3, 0x6cb9, 0x6e38, 0x5e7c, 0x4f18, 0x53cb, 0x94c0, 0x5fe7,
+ 0x5c24, 0x72b9, 0x8bf1, 0x60a0, 0x90ae, 0x9149, 0x4f51, 0x91c9, 0x5e7d, 0x75a3, 0x6538,
+ 0x86b0, 0x83a0, 0x9c7f, 0x5363, 0x9edd, 0x83b8, 0x7337, 0x86b4, 0x5ba5, 0x7256, 0x56ff,
+ 0x67da, 0x8763, 0x839c, 0x9f2c, 0x94d5, 0x8764, 0x7e47, 0x5466, 0x4f91, 0x5c22}},
+ {"yu", // L"与于欲鱼雨余遇语愈狱玉渔予誉育愚羽虞娱淤舆屿禹宇迂俞逾域芋郁吁盂喻峪御愉渝尉榆隅浴寓裕预豫驭蔚妪嵛雩馀阈窬鹆妤揄窳觎臾舁龉蓣煜钰谀纡於竽瑜禺聿欤俣伛圄鹬庾昱萸瘐谕鬻圉瘀熨饫毓燠腴狳菀蜮蝓吾"
+ {0x4e0e, 0x4e8e, 0x6b32, 0x9c7c, 0x96e8, 0x4f59, 0x9047, 0x8bed, 0x6108, 0x72f1, 0x7389,
+ 0x6e14, 0x4e88, 0x8a89, 0x80b2, 0x611a, 0x7fbd, 0x865e, 0x5a31, 0x6de4, 0x8206, 0x5c7f,
+ 0x79b9, 0x5b87, 0x8fc2, 0x4fde, 0x903e, 0x57df, 0x828b, 0x90c1, 0x5401, 0x76c2, 0x55bb,
+ 0x5cea, 0x5fa1, 0x6109, 0x6e1d, 0x5c09, 0x6986, 0x9685, 0x6d74, 0x5bd3, 0x88d5, 0x9884,
+ 0x8c6b, 0x9a6d, 0x851a, 0x59aa, 0x5d5b, 0x96e9, 0x9980, 0x9608, 0x7aac, 0x9e46, 0x59a4,
+ 0x63c4, 0x7ab3, 0x89ce, 0x81fe, 0x8201, 0x9f89, 0x84e3, 0x715c, 0x94b0, 0x8c00, 0x7ea1,
+ 0x65bc, 0x7afd, 0x745c, 0x79ba, 0x807f, 0x6b24, 0x4fe3, 0x4f1b, 0x5704, 0x9e6c, 0x5ebe,
+ 0x6631, 0x8438, 0x7610, 0x8c15, 0x9b3b, 0x5709, 0x7600, 0x71a8, 0x996b, 0x6bd3, 0x71e0,
+ 0x8174, 0x72f3, 0x83c0, 0x872e, 0x8753, 0x543e}},
+ {"yuan", // L"远员元院圆原愿园援猿怨冤源缘袁渊苑垣鸳辕圜鼋橼媛爰眢鸢掾芫沅瑗螈箢塬"
+ {0x8fdc, 0x5458, 0x5143, 0x9662, 0x5706, 0x539f, 0x613f, 0x56ed, 0x63f4,
+ 0x733f, 0x6028, 0x51a4, 0x6e90, 0x7f18, 0x8881, 0x6e0a, 0x82d1, 0x57a3,
+ 0x9e33, 0x8f95, 0x571c, 0x9f0b, 0x6a7c, 0x5a9b, 0x7230, 0x7722, 0x9e22,
+ 0x63be, 0x82ab, 0x6c85, 0x7457, 0x8788, 0x7ba2, 0x586c}},
+ {"yue", // L"月越约跃阅乐岳悦曰说粤钥瀹钺刖龠栎樾哕"
+ {0x6708, 0x8d8a, 0x7ea6, 0x8dc3, 0x9605, 0x4e50, 0x5cb3, 0x60a6, 0x66f0, 0x8bf4, 0x7ca4,
+ 0x94a5, 0x7039, 0x94ba, 0x5216, 0x9fa0, 0x680e, 0x6a3e, 0x54d5}},
+ {"yun", // L"云运晕允匀韵陨孕耘蕴酝郧员氲恽愠郓芸筠韫昀狁殒纭熨"
+ {0x4e91, 0x8fd0, 0x6655, 0x5141, 0x5300, 0x97f5, 0x9668, 0x5b55, 0x8018,
+ 0x8574, 0x915d, 0x90e7, 0x5458, 0x6c32, 0x607d, 0x6120, 0x90d3, 0x82b8,
+ 0x7b60, 0x97eb, 0x6600, 0x72c1, 0x6b92, 0x7ead, 0x71a8}},
+ {"za", // L"杂砸咋匝扎咂拶"
+ {0x6742, 0x7838, 0x548b, 0x531d, 0x624e, 0x5482, 0x62f6}},
+ {"zai", // L"在再灾载栽宰哉甾崽"
+ {0x5728, 0x518d, 0x707e, 0x8f7d, 0x683d, 0x5bb0, 0x54c9, 0x753e, 0x5d3d}},
+ {"zan", // L"咱暂攒赞簪趱糌瓒拶昝錾"
+ {0x54b1, 0x6682, 0x6512, 0x8d5e, 0x7c2a, 0x8db1, 0x7ccc, 0x74d2, 0x62f6, 0x661d, 0x933e}},
+ {"zang", // L"脏葬赃藏臧驵"
+ {0x810f, 0x846c, 0x8d43, 0x85cf, 0x81e7, 0x9a75}},
+ {"zao", // L"早造遭糟灶燥枣凿躁藻皂噪澡蚤唣"
+ {0x65e9, 0x9020, 0x906d, 0x7cdf, 0x7076, 0x71e5, 0x67a3, 0x51ff, 0x8e81, 0x85fb, 0x7682,
+ 0x566a, 0x6fa1, 0x86a4, 0x5523}},
+ {"ze", // L"则责择泽咋箦舴帻迮啧仄昃笮赜"
+ {0x5219, 0x8d23, 0x62e9, 0x6cfd, 0x548b, 0x7ba6, 0x8234, 0x5e3b, 0x8fee, 0x5567, 0x4ec4,
+ 0x6603, 0x7b2e, 0x8d5c}},
+ {"zei", // L"贼"
+ {0x8d3c}},
+ {"zen", // L"怎谮"
+ {0x600e, 0x8c2e}},
+ {"zeng", // L"增赠憎曾缯罾甑锃"
+ {0x589e, 0x8d60, 0x618e, 0x66fe, 0x7f2f, 0x7f7e, 0x7511, 0x9503}},
+ {"zha", // L"扎炸渣闸眨榨乍轧诈铡札查栅咋喳砟痄吒哳楂蚱揸喋柞咤齄龃"
+ {0x624e, 0x70b8, 0x6e23, 0x95f8, 0x7728, 0x69a8, 0x4e4d, 0x8f67, 0x8bc8,
+ 0x94e1, 0x672d, 0x67e5, 0x6805, 0x548b, 0x55b3, 0x781f, 0x75c4, 0x5412,
+ 0x54f3, 0x6942, 0x86b1, 0x63f8, 0x558b, 0x67de, 0x54a4, 0x9f44, 0x9f83}},
+ {"zhai", // L"摘窄债斋寨择翟宅砦瘵"
+ {0x6458, 0x7a84, 0x503a, 0x658b, 0x5be8, 0x62e9, 0x7fdf, 0x5b85, 0x7826, 0x7635}},
+ {"zhan", // L"站占战盏沾粘毡展栈詹颤蘸湛绽斩辗崭瞻谵搌旃骣"
+ {0x7ad9, 0x5360, 0x6218, 0x76cf, 0x6cbe, 0x7c98, 0x6be1, 0x5c55, 0x6808, 0x8a79, 0x98a4,
+ 0x8638, 0x6e5b, 0x7efd, 0x65a9, 0x8f97, 0x5d2d, 0x77bb, 0x8c35, 0x640c, 0x65c3, 0x9aa3}},
+ {"zhang", // L"张章长帐仗丈掌涨账樟杖彰漳胀瘴障仉嫜幛鄣璋嶂獐蟑"
+ {0x5f20, 0x7ae0, 0x957f, 0x5e10, 0x4ed7, 0x4e08, 0x638c, 0x6da8,
+ 0x8d26, 0x6a1f, 0x6756, 0x5f70, 0x6f33, 0x80c0, 0x7634, 0x969c,
+ 0x4ec9, 0x5adc, 0x5e5b, 0x9123, 0x748b, 0x5d82, 0x7350, 0x87d1}},
+ {"zhao", // L"找着照招罩爪兆朝昭沼肇召赵棹啁钊笊诏"
+ {0x627e, 0x7740, 0x7167, 0x62db, 0x7f69, 0x722a, 0x5146, 0x671d, 0x662d, 0x6cbc, 0x8087,
+ 0x53ec, 0x8d75, 0x68f9, 0x5541, 0x948a, 0x7b0a, 0x8bcf}},
+ {"zhe", // L"着这者折遮蛰哲蔗锗辙浙柘辄赭摺鹧磔褶蜇谪"
+ {0x7740, 0x8fd9, 0x8005, 0x6298, 0x906e, 0x86f0, 0x54f2, 0x8517, 0x9517, 0x8f99,
+ 0x6d59, 0x67d8, 0x8f84, 0x8d6d, 0x647a, 0x9e67, 0x78d4, 0x8936, 0x8707, 0x8c2a}},
+ {"zhen", // L"真阵镇针震枕振斟珍疹诊甄砧臻贞侦缜蓁祯箴轸榛稹赈朕鸩胗浈桢畛圳椹溱"
+ {0x771f, 0x9635, 0x9547, 0x9488, 0x9707, 0x6795, 0x632f, 0x659f, 0x73cd, 0x75b9, 0x8bca,
+ 0x7504, 0x7827, 0x81fb, 0x8d1e, 0x4fa6, 0x7f1c, 0x84c1, 0x796f, 0x7bb4, 0x8f78, 0x699b,
+ 0x7a39, 0x8d48, 0x6715, 0x9e29, 0x80d7, 0x6d48, 0x6862, 0x755b, 0x5733, 0x6939, 0x6eb1}},
+ {"zheng", // L"正整睁争挣征怔证症郑拯蒸狰政峥钲铮筝诤徵鲭"
+ {0x6b63, 0x6574, 0x7741, 0x4e89, 0x6323, 0x5f81, 0x6014, 0x8bc1, 0x75c7, 0x90d1, 0x62ef,
+ 0x84b8, 0x72f0, 0x653f, 0x5ce5, 0x94b2, 0x94ee, 0x7b5d, 0x8be4, 0x5fb5, 0x9cad}},
+ {"zhi", // L"只之直知制指纸支芝枝稚吱蜘质肢脂汁炙织职痔植抵殖执值侄址滞止趾治旨窒志挚掷至致置帜识峙智秩帙摭黹桎枳轵忮祉蛭膣觯郅栀彘芷祗咫鸷絷踬胝骘轾痣陟踯雉埴贽卮酯豸跖栉夂徵"
+ {0x53ea, 0x4e4b, 0x76f4, 0x77e5, 0x5236, 0x6307, 0x7eb8, 0x652f, 0x829d, 0x679d, 0x7a1a,
+ 0x5431, 0x8718, 0x8d28, 0x80a2, 0x8102, 0x6c41, 0x7099, 0x7ec7, 0x804c, 0x75d4, 0x690d,
+ 0x62b5, 0x6b96, 0x6267, 0x503c, 0x4f84, 0x5740, 0x6ede, 0x6b62, 0x8dbe, 0x6cbb, 0x65e8,
+ 0x7a92, 0x5fd7, 0x631a, 0x63b7, 0x81f3, 0x81f4, 0x7f6e, 0x5e1c, 0x8bc6, 0x5cd9, 0x667a,
+ 0x79e9, 0x5e19, 0x646d, 0x9ef9, 0x684e, 0x67b3, 0x8f75, 0x5fee, 0x7949, 0x86ed, 0x81a3,
+ 0x89ef, 0x90c5, 0x6800, 0x5f58, 0x82b7, 0x7957, 0x54ab, 0x9e37, 0x7d77, 0x8e2c, 0x80dd,
+ 0x9a98, 0x8f7e, 0x75e3, 0x965f, 0x8e2f, 0x96c9, 0x57f4, 0x8d3d, 0x536e, 0x916f, 0x8c78,
+ 0x8dd6, 0x6809, 0x5902, 0x5fb5}},
+ {"zhong", // L"中重种钟肿众终盅忠仲衷踵舯螽锺冢忪"
+ {0x4e2d, 0x91cd, 0x79cd, 0x949f, 0x80bf, 0x4f17, 0x7ec8, 0x76c5, 0x5fe0, 0x4ef2, 0x8877,
+ 0x8e35, 0x822f, 0x87bd, 0x953a, 0x51a2, 0x5fea}},
+ {"zhou", // L"周洲皱粥州轴舟昼骤宙诌肘帚咒繇胄纣荮啁碡绉籀妯酎"
+ {0x5468, 0x6d32, 0x76b1, 0x7ca5, 0x5dde, 0x8f74, 0x821f, 0x663c,
+ 0x9aa4, 0x5b99, 0x8bcc, 0x8098, 0x5e1a, 0x5492, 0x7e47, 0x80c4,
+ 0x7ea3, 0x836e, 0x5541, 0x78a1, 0x7ec9, 0x7c40, 0x59af, 0x914e}},
+ {"zhu", // L"住主猪竹株煮筑著贮铸嘱拄注祝驻属术珠瞩蛛朱柱诸诛逐助烛蛀潴洙伫瘃翥茱苎橥舳杼箸炷侏铢疰渚褚躅麈邾槠竺丶"
+ {0x4f4f, 0x4e3b, 0x732a, 0x7af9, 0x682a, 0x716e, 0x7b51, 0x8457, 0x8d2e, 0x94f8, 0x5631,
+ 0x62c4, 0x6ce8, 0x795d, 0x9a7b, 0x5c5e, 0x672f, 0x73e0, 0x77a9, 0x86db, 0x6731, 0x67f1,
+ 0x8bf8, 0x8bdb, 0x9010, 0x52a9, 0x70db, 0x86c0, 0x6f74, 0x6d19, 0x4f2b, 0x7603, 0x7fe5,
+ 0x8331, 0x82ce, 0x6a65, 0x8233, 0x677c, 0x7bb8, 0x70b7, 0x4f8f, 0x94e2, 0x75b0, 0x6e1a,
+ 0x891a, 0x8e85, 0x9e88, 0x90be, 0x69e0, 0x7afa, 0x4e36}},
+ {"zhua", // L"抓爪挝"
+ {0x6293, 0x722a, 0x631d}},
+ {"zhuai", // L"拽转"
+ {0x62fd, 0x8f6c}},
+ {"zhuan", // L"转专砖赚传撰篆颛馔啭沌"
+ {0x8f6c, 0x4e13, 0x7816, 0x8d5a, 0x4f20, 0x64b0, 0x7bc6, 0x989b, 0x9994, 0x556d, 0x6c8c}},
+ {"zhuang", // L"装撞庄壮桩状幢妆奘戆"
+ {0x88c5, 0x649e, 0x5e84, 0x58ee, 0x6869, 0x72b6, 0x5e62, 0x5986, 0x5958, 0x6206}},
+ {"zhui", // L"追坠缀锥赘椎骓惴缒隹"
+ {0x8ffd, 0x5760, 0x7f00, 0x9525, 0x8d58, 0x690e, 0x9a93, 0x60f4, 0x7f12, 0x96b9}},
+ {"zhun", // L"准谆肫窀饨"
+ {0x51c6, 0x8c06, 0x80ab, 0x7a80, 0x9968}},
+ {"zhuo", // L"捉桌着啄拙灼浊卓琢茁酌擢焯濯诼浞涿倬镯禚斫淖"
+ {0x6349, 0x684c, 0x7740, 0x5544, 0x62d9, 0x707c, 0x6d4a, 0x5353, 0x7422, 0x8301, 0x914c,
+ 0x64e2, 0x712f, 0x6fef, 0x8bfc, 0x6d5e, 0x6dbf, 0x502c, 0x956f, 0x799a, 0x65ab, 0x6dd6}},
+ {"zi", // L"字自子紫籽资姿吱滓仔兹咨孜渍滋淄笫粢龇秭恣谘趑缁梓鲻锱孳耔觜髭赀茈訾嵫眦姊辎"
+ {0x5b57, 0x81ea, 0x5b50, 0x7d2b, 0x7c7d, 0x8d44, 0x59ff, 0x5431, 0x6ed3, 0x4ed4,
+ 0x5179, 0x54a8, 0x5b5c, 0x6e0d, 0x6ecb, 0x6dc4, 0x7b2b, 0x7ca2, 0x9f87, 0x79ed,
+ 0x6063, 0x8c18, 0x8d91, 0x7f01, 0x6893, 0x9cbb, 0x9531, 0x5b73, 0x8014, 0x89dc,
+ 0x9aed, 0x8d40, 0x8308, 0x8a3e, 0x5d6b, 0x7726, 0x59ca, 0x8f8e}},
+ {"zong", // L"总纵宗棕综踪鬃偬粽枞腙"
+ {0x603b, 0x7eb5, 0x5b97, 0x68d5, 0x7efc, 0x8e2a, 0x9b03, 0x506c, 0x7cbd, 0x679e, 0x8159}},
+ {"zou", // L"走揍奏邹鲰鄹陬驺诹"
+ {0x8d70, 0x63cd, 0x594f, 0x90b9, 0x9cb0, 0x9139, 0x966c, 0x9a7a, 0x8bf9}},
+ {"zu", // L"组族足阻租祖诅菹镞卒俎"
+ {0x7ec4, 0x65cf, 0x8db3, 0x963b, 0x79df, 0x7956, 0x8bc5, 0x83f9, 0x955e, 0x5352, 0x4fce}},
+ {"zuan", // L"钻纂缵躜攥"
+ {0x94bb, 0x7e82, 0x7f35, 0x8e9c, 0x6525}},
+ {"zui", // L"最嘴醉罪觜蕞"
+ {0x6700, 0x5634, 0x9189, 0x7f6a, 0x89dc, 0x855e}},
+ {"zun", // L"尊遵鳟撙樽"
+ {0x5c0a, 0x9075, 0x9cdf, 0x6499, 0x6a3d}},
+ {"zuo", // L"做作坐左座昨琢撮佐嘬酢唑祚胙怍阼柞砟"
+ {0x505a, 0x4f5c, 0x5750, 0x5de6, 0x5ea7, 0x6628, 0x7422, 0x64ae, 0x4f50, 0x562c, 0x9162,
+ 0x5511, 0x795a, 0x80d9, 0x600d, 0x963c, 0x67de, 0x781f}},
+};
+
+CInputCodingTableBasePY::CInputCodingTableBasePY()
+{
+ m_codechars = "abcdefghijklmnopqrstuvwxyz";
+}
+
+std::vector<std::wstring> CInputCodingTableBasePY::GetResponse(int)
+{
+ return m_words;
+}
+
+bool CInputCodingTableBasePY::GetWordListPage(const std::string& strCode, bool isFirstPage)
+{
+ if (!isFirstPage)
+ return false;
+
+ m_words.clear();
+ auto finder = codemap.find(strCode);
+ if (finder != codemap.end())
+ {
+ for (unsigned int i = 0; i < finder->second.size(); i++)
+ {
+ m_words.push_back(finder->second.substr(i, 1));
+ }
+ }
+ CGUIMessage msg(GUI_MSG_CODINGTABLE_LOOKUP_COMPLETED, 0, 0, 0);
+ msg.SetStringParam(strCode);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(
+ msg, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog());
+ return true;
+}
diff --git a/xbmc/input/InputCodingTableBasePY.h b/xbmc/input/InputCodingTableBasePY.h
new file mode 100644
index 0000000..243b4de
--- /dev/null
+++ b/xbmc/input/InputCodingTableBasePY.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "InputCodingTable.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+class CInputCodingTableBasePY : public IInputCodingTable
+{
+public:
+ CInputCodingTableBasePY();
+ ~CInputCodingTableBasePY() override = default;
+
+ bool GetWordListPage(const std::string& strCode, bool isFirstPage) override;
+ std::vector<std::wstring> GetResponse(int) override;
+
+private:
+ std::vector<std::wstring> m_words;
+};
diff --git a/xbmc/input/InputCodingTableFactory.cpp b/xbmc/input/InputCodingTableFactory.cpp
new file mode 100644
index 0000000..9dbcfd2
--- /dev/null
+++ b/xbmc/input/InputCodingTableFactory.cpp
@@ -0,0 +1,24 @@
+/*
+ * 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 "InputCodingTableFactory.h"
+
+#include "InputCodingTableBasePY.h"
+#include "InputCodingTableKorean.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+IInputCodingTable* CInputCodingTableFactory::CreateCodingTable(const std::string& strTableName,
+ const TiXmlElement* element)
+{
+ if (strTableName == "BasePY")
+ return new CInputCodingTableBasePY();
+ if (strTableName == "Korean")
+ return new CInputCodingTableKorean();
+ return nullptr;
+}
diff --git a/xbmc/input/InputCodingTableFactory.h b/xbmc/input/InputCodingTableFactory.h
new file mode 100644
index 0000000..a2dc247
--- /dev/null
+++ b/xbmc/input/InputCodingTableFactory.h
@@ -0,0 +1,21 @@
+/*
+ * 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 <string>
+
+class TiXmlElement;
+class IInputCodingTable;
+
+class CInputCodingTableFactory
+{
+public:
+ static IInputCodingTable* CreateCodingTable(const std::string& strTableName,
+ const TiXmlElement* element);
+};
diff --git a/xbmc/input/InputCodingTableKorean.cpp b/xbmc/input/InputCodingTableKorean.cpp
new file mode 100644
index 0000000..223f81d
--- /dev/null
+++ b/xbmc/input/InputCodingTableKorean.cpp
@@ -0,0 +1,380 @@
+/*
+ * 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 "InputCodingTableKorean.h"
+
+#include "utils/CharsetConverter.h"
+
+#include <stdlib.h>
+
+CInputCodingTableKorean::CInputCodingTableKorean() = default;
+
+std::vector<std::wstring> CInputCodingTableKorean::GetResponse(int)
+{
+ return m_words;
+}
+
+bool CInputCodingTableKorean::GetWordListPage(const std::string& strCode, bool isFirstPage)
+{
+ return false;
+}
+
+void CInputCodingTableKorean::SetTextPrev(const std::string& strTextPrev)
+{
+ m_strTextPrev = strTextPrev;
+}
+
+int CInputCodingTableKorean::MergeCode(int choseong, int jungseong, int jongseong)
+{
+ return (unsigned short)0xAC00 + choseong * 21 * 28 + jungseong * 28 + jongseong + 1;
+}
+
+// Reference
+// https://en.wikipedia.org/wiki/Hangul
+// http://www.theyt.net/wiki/%ED%95%9C%EC%98%81%ED%83%80%EB%B3%80%ED%99%98%EA%B8%B0
+
+std::wstring CInputCodingTableKorean::InputToKorean(const std::wstring& input)
+{
+ std::wstring dicEnglish = // L"rRseEfaqQtTdwWczxvgkoiOjpuPhynbml";
+ {0x72, 0x52, 0x73, 0x65, 0x45, 0x66, 0x61, 0x71, 0x51, 0x74, 0x54,
+ 0x64, 0x77, 0x57, 0x63, 0x7A, 0x78, 0x76, 0x67, 0x6B, 0x6F, 0x69,
+ 0x4F, 0x6A, 0x70, 0x75, 0x50, 0x68, 0x79, 0x6E, 0x62, 0x6D, 0x6C};
+ std::wstring dicKorean = // L"ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎㅏㅐㅑㅒㅓㅔㅕㅖㅗㅛㅜㅠㅡㅣ";
+ {0x3131, 0x3132, 0x3134, 0x3137, 0x3138, 0x3139, 0x3141, 0x3142, 0x3143, 0x3145, 0x3146,
+ 0x3147, 0x3148, 0x3149, 0x314a, 0x314b, 0x314c, 0x314d, 0x314e, 0x314f, 0x3150, 0x3151,
+ 0x3152, 0x3153, 0x3154, 0x3155, 0x3156, 0x3157, 0x315b, 0x315c, 0x3160, 0x3161, 0x3163};
+ std::wstring dicChoseong = // L"ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ";
+ {0x3131, 0x3132, 0x3134, 0x3137, 0x3138, 0x3139, 0x3141, 0x3142, 0x3143, 0x3145,
+ 0x3146, 0x3147, 0x3148, 0x3149, 0x314a, 0x314b, 0x314c, 0x314d, 0x314e};
+ std::wstring dicJungseong = // L"ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ";
+ {0x314f, 0x3150, 0x3151, 0x3152, 0x3153, 0x3154, 0x3155, 0x3156, 0x3157, 0x3158, 0x3159,
+ 0x315a, 0x315b, 0x315c, 0x315d, 0x315e, 0x315f, 0x3160, 0x3161, 0x3162, 0x3163};
+ std::wstring dicJongseong = // L"ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ";
+ {0x3131, 0x3132, 0x3133, 0x3134, 0x3135, 0x3136, 0x3137, 0x3139, 0x313a,
+ 0x313b, 0x313c, 0x313d, 0x313e, 0x313f, 0x3140, 0x3141, 0x3142, 0x3144,
+ 0x3145, 0x3146, 0x3147, 0x3148, 0x314a, 0x314b, 0x314c, 0x314d, 0x314e};
+
+ std::wstring korean;
+
+ if (input.empty())
+ return korean;
+
+ int choseong = -1, jungseong = -1, jongseong = -1;
+
+ for (unsigned int i = 0; i < input.size(); i++)
+ {
+ wchar_t ch = input.at(i);
+ int key = dicKorean.find(ch);
+
+ // H/W Keyboard input with English will be changed to Korean
+ // because H/W input in Korean is not supported.
+ if (key == -1)
+ key = dicEnglish.find(ch);
+
+ if (key == -1) // If not Korean and English
+ {
+ // If there is remained Korean, merge code into character
+ if (choseong != -1) // There is choseong
+ {
+ if (jungseong != -1) // choseong+jungseong+(jongseong)
+ korean += MergeCode(choseong, jungseong, jongseong);
+ else // Only choseong
+ korean += dicChoseong.at(choseong);
+ }
+ else
+ {
+ if (jungseong != -1) // Jungseong
+ korean += dicJungseong.at(jungseong);
+
+ if (jongseong != -1) // Jongseong
+ korean += dicJongseong.at(jongseong);
+ }
+ choseong = -1;
+ jungseong = -1;
+ jongseong = -1;
+ korean += ch;
+ }
+ else if (key < 19) // If key is consonant, key could be choseong or jungseong
+ {
+ if (jungseong != -1)
+ {
+ // Jungseong without choseong cannot have jongseong.
+ // So inputted key is jungseong character, new character is begun.
+ if (choseong == -1)
+ {
+ korean += dicJungseong.at(jungseong);
+ jungseong = -1;
+ choseong = key;
+ }
+ else // Jungseong with choseong can have jongseong.
+ {
+ // Chongseong can have two consonant. So this is first consonant of chongseong.
+ if (jongseong == -1)
+ {
+ jongseong = dicJongseong.find(dicKorean.at(key));
+ if (jongseong == -1) // This consonant cannot be jongseong. ex) "ㄸ", "ㅃ", "ㅉ"
+ {
+ korean += MergeCode(choseong, jungseong, jongseong);
+ choseong = dicChoseong.find(dicKorean.at(key));
+ jungseong = -1;
+ }
+ }
+ else if (jongseong == 0 && key == 9) // "ㄳ"
+ jongseong = 2;
+ else if (jongseong == 3 && key == 12) // "ㄵ"
+ jongseong = 4;
+ else if (jongseong == 3 && key == 18) // "ㄶ"
+ jongseong = 5;
+ else if (jongseong == 7 && key == 0) // "ㄺ"
+ jongseong = 8;
+ else if (jongseong == 7 && key == 6) // "ㄻ"
+ jongseong = 9;
+ else if (jongseong == 7 && key == 7) // "ㄼ"
+ jongseong = 10;
+ else if (jongseong == 7 && key == 9) // "ㄽ"
+ jongseong = 11;
+ else if (jongseong == 7 && key == 16) // "ㄾ"
+ jongseong = 12;
+ else if (jongseong == 7 && key == 17) // "ㄿ"
+ jongseong = 13;
+ else if (jongseong == 7 && key == 18) // "ㅀ"
+ jongseong = 14;
+ else if (jongseong == 16 && key == 9) // "ㅄ"
+ jongseong = 17;
+ else // Jongseong is completed. So new consonant is choseong.
+ {
+ korean += MergeCode(choseong, jungseong, jongseong);
+ choseong = dicChoseong.find(dicKorean.at(key));
+ jungseong = -1;
+ jongseong = -1;
+ }
+ }
+ }
+ else // If there is no jungseong, new consonant can be choseong or second part of double
+ // consonant.
+ {
+ // New consonant is choseong. Also it could be first part of double consonant.
+ if (choseong == -1)
+ {
+ // If choseong is already completed, new consonant is another choseong.
+ // So previous character has only jongseong.
+ if (jongseong != -1)
+ {
+ korean += dicJongseong.at(jongseong);
+ jongseong = -1;
+ }
+ choseong = dicChoseong.find(dicKorean.at(key));
+ }
+ // Find double consonant of chongseong
+ else if (choseong == 0 && key == 9) // "ㄳ"
+ {
+ choseong = -1;
+ jongseong = 2;
+ }
+ else if (choseong == 2 && key == 12) // "ㄵ"
+ {
+ choseong = -1;
+ jongseong = 4;
+ }
+ else if (choseong == 2 && key == 18) // "ㄶ"
+ {
+ choseong = -1;
+ jongseong = 5;
+ }
+ else if (choseong == 5 && key == 0) // "ㄺ"
+ {
+ choseong = -1;
+ jongseong = 8;
+ }
+ else if (choseong == 5 && key == 6) // "ㄻ"
+ {
+ choseong = -1;
+ jongseong = 9;
+ }
+ else if (choseong == 5 && key == 7) // "ㄼ"
+ {
+ choseong = -1;
+ jongseong = 10;
+ }
+ else if (choseong == 5 && key == 9) // "ㄽ"
+ {
+ choseong = -1;
+ jongseong = 11;
+ }
+ else if (choseong == 5 && key == 16) // "ㄾ"
+ {
+ choseong = -1;
+ jongseong = 12;
+ }
+ else if (choseong == 5 && key == 17) // "ㄿ"
+ {
+ choseong = -1;
+ jongseong = 13;
+ }
+ else if (choseong == 5 && key == 18) // "ㅀ"
+ {
+ choseong = -1;
+ jongseong = 14;
+ }
+ else if (choseong == 7 && key == 9) // "ㅄ"
+ {
+ choseong = -1;
+ jongseong = 17;
+ }
+ else // In this case, previous character has only choseong. And new consonant is choseong.
+ {
+ korean += dicChoseong.at(choseong);
+ choseong = dicChoseong.find(dicKorean.at(key));
+ }
+ }
+ }
+ else // If key is vowel, key is jungseong.
+ {
+ // If previous character has jongseong and this key is jungseong,
+ // actually latest vowel is not jongseong. It's choseong of new character.
+ if (jongseong != -1)
+ {
+ // If jongseong of previous character is double consonant, we will separate it to two vowel
+ // again. First part of double consonant is jongseong of previous character. Second part of
+ // double consonant is choseong of current character.
+ int newCho;
+ if (jongseong == 2) // "ㄱ, ㅅ"
+ {
+ jongseong = 0;
+ newCho = 9;
+ }
+ else if (jongseong == 4) // "ㄴ, ㅈ"
+ {
+ jongseong = 3;
+ newCho = 12;
+ }
+ else if (jongseong == 5) // "ㄴ, ㅎ"
+ {
+ jongseong = 3;
+ newCho = 18;
+ }
+ else if (jongseong == 8) // "ㄹ, ㄱ"
+ {
+ jongseong = 7;
+ newCho = 0;
+ }
+ else if (jongseong == 9) // "ㄹ, ㅁ"
+ {
+ jongseong = 7;
+ newCho = 6;
+ }
+ else if (jongseong == 10) // "ㄹ, ㅂ"
+ {
+ jongseong = 7;
+ newCho = 7;
+ }
+ else if (jongseong == 11) // "ㄹ, ㅅ"
+ {
+ jongseong = 7;
+ newCho = 9;
+ }
+ else if (jongseong == 12) // "ㄹ, ㅌ"
+ {
+ jongseong = 7;
+ newCho = 16;
+ }
+ else if (jongseong == 13) // "ㄹ, ㅍ"
+ {
+ jongseong = 7;
+ newCho = 17;
+ }
+ else if (jongseong == 14) // "ㄹ, ㅎ"
+ {
+ jongseong = 7;
+ newCho = 18;
+ }
+ else if (jongseong == 17) // "ㅂ, ㅅ"
+ {
+ jongseong = 16;
+ newCho = 9;
+ }
+ else
+ {
+ // If jongseong is single consonant, previous character has no chongseong.
+ // It's choseong of current character.
+ newCho = dicChoseong.find(dicJongseong.at(jongseong));
+ jongseong = -1;
+ }
+ if (choseong != -1) // If previous character has choseong and jungseong.
+ korean += MergeCode(choseong, jungseong, jongseong);
+ else // If previous character has Jongseong only.
+ korean += dicJongseong.at(jongseong);
+
+ choseong = newCho;
+ jungseong = -1;
+ jongseong = -1;
+ }
+ if (jungseong == -1) // If this key is first vowel, it's first part of jungseong.
+ {
+ jungseong = dicJungseong.find(dicKorean.at(key));
+ }
+ // If there is jungseong already, jungseong is double vowel.
+ else if (jungseong == 8 && key == 19) // "ㅘ"
+ jungseong = 9;
+ else if (jungseong == 8 && key == 20) // "ㅙ"
+ jungseong = 10;
+ else if (jungseong == 8 && key == 32) // "ㅚ"
+ jungseong = 11;
+ else if (jungseong == 13 && key == 23) // "ㅝ"
+ jungseong = 14;
+ else if (jungseong == 13 && key == 24) // "ㅞ"
+ jungseong = 15;
+ else if (jungseong == 13 && key == 32) // "ㅟ"
+ jungseong = 16;
+ else if (jungseong == 18 && key == 32) // "ㅢ"
+ jungseong = 19;
+ else // If two vowel cannot be double vowel.
+ {
+ // Previous character is completed.
+ // Current character is begin with jungseong.
+ if (choseong != -1)
+ {
+ korean += MergeCode(choseong, jungseong, jongseong);
+ choseong = -1;
+ }
+ else // Previous character has jungseon only.
+ korean += dicJungseong.at(jungseong);
+ jungseong = -1;
+ korean += dicKorean.at(key);
+ }
+ }
+ }
+
+ // Process last character
+ if (choseong != -1)
+ {
+ if (jungseong != -1) // Current character has choseong and jungseong.
+ korean += MergeCode(choseong, jungseong, jongseong);
+ else // Current character has choseong only.
+ korean += dicChoseong.at(choseong);
+ }
+ else
+ {
+ if (jungseong != -1) // Current character has jungseong only
+ korean += dicJungseong.at(jungseong);
+ else if (jongseong != -1) // Current character has jongseong only
+ korean += dicJongseong.at(jongseong);
+ }
+
+ return korean;
+}
+
+std::string CInputCodingTableKorean::ConvertString(const std::string& strCode)
+{
+ std::wstring input;
+ std::string result;
+ g_charsetConverter.utf8ToW(strCode, input);
+ InputToKorean(input);
+ g_charsetConverter.wToUTF8(InputToKorean(input), result);
+ return m_strTextPrev + result;
+}
diff --git a/xbmc/input/InputCodingTableKorean.h b/xbmc/input/InputCodingTableKorean.h
new file mode 100644
index 0000000..55a64d6
--- /dev/null
+++ b/xbmc/input/InputCodingTableKorean.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "InputCodingTable.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+class CInputCodingTableKorean : public IInputCodingTable
+{
+public:
+ CInputCodingTableKorean();
+ ~CInputCodingTableKorean() override = default;
+
+ bool GetWordListPage(const std::string& strCode, bool isFirstPage) override;
+ std::vector<std::wstring> GetResponse(int) override;
+
+ void SetTextPrev(const std::string& strTextPrev) override;
+ std::string ConvertString(const std::string& strCode) override;
+ int GetType() override { return TYPE_CONVERT_STRING; }
+
+protected:
+ int MergeCode(int choseong, int jungseong, int jongseong);
+ std::wstring InputToKorean(const std::wstring& input);
+
+private:
+ std::vector<std::wstring> m_words;
+ std::string m_strTextPrev;
+};
diff --git a/xbmc/input/InputManager.cpp b/xbmc/input/InputManager.cpp
new file mode 100644
index 0000000..571b259
--- /dev/null
+++ b/xbmc/input/InputManager.cpp
@@ -0,0 +1,938 @@
+/*
+ * 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 "InputManager.h"
+
+#include "ButtonTranslator.h"
+#include "CustomControllerTranslator.h"
+#include "JoystickMapper.h"
+#include "KeymapEnvironment.h"
+#include "ServiceBroker.h"
+#include "TouchTranslator.h"
+#include "XBMC_vkeys.h"
+#include "application/AppInboundProtocol.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "guilib/GUIAudioManager.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIControl.h"
+#include "guilib/GUIWindow.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/Key.h"
+#include "input/keyboard/KeyboardEasterEgg.h"
+#include "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+#include "input/mouse/MouseTranslator.h"
+#include "input/mouse/interfaces/IMouseDriverHandler.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/EventServer.h"
+#include "peripherals/Peripherals.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/ExecString.h"
+#include "utils/Geometry.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <math.h>
+#include <mutex>
+
+using EVENTSERVER::CEventServer;
+
+using namespace KODI;
+
+const std::string CInputManager::SETTING_INPUT_ENABLE_CONTROLLER = "input.enablejoystick";
+
+CInputManager::CInputManager()
+ : m_keymapEnvironment(new CKeymapEnvironment),
+ m_buttonTranslator(new CButtonTranslator),
+ m_customControllerTranslator(new CCustomControllerTranslator),
+ m_touchTranslator(new CTouchTranslator),
+ m_joystickTranslator(new CJoystickMapper),
+ m_keyboardEasterEgg(new KEYBOARD::CKeyboardEasterEgg)
+{
+ m_buttonTranslator->RegisterMapper("touch", m_touchTranslator.get());
+ m_buttonTranslator->RegisterMapper("customcontroller", m_customControllerTranslator.get());
+ m_buttonTranslator->RegisterMapper("joystick", m_joystickTranslator.get());
+
+ RegisterKeyboardDriverHandler(m_keyboardEasterEgg.get());
+
+ // Register settings
+ std::set<std::string> settingSet;
+ settingSet.insert(CSettings::SETTING_INPUT_ENABLEMOUSE);
+ settingSet.insert(SETTING_INPUT_ENABLE_CONTROLLER);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->RegisterCallback(this, settingSet);
+}
+
+CInputManager::~CInputManager()
+{
+ Deinitialize();
+
+ // Unregister settings
+ CServiceBroker::GetSettingsComponent()->GetSettings()->UnregisterCallback(this);
+
+ UnregisterKeyboardDriverHandler(m_keyboardEasterEgg.get());
+
+ m_buttonTranslator->UnregisterMapper(m_touchTranslator.get());
+ m_buttonTranslator->UnregisterMapper(m_customControllerTranslator.get());
+ m_buttonTranslator->UnregisterMapper(m_joystickTranslator.get());
+}
+
+void CInputManager::InitializeInputs()
+{
+ m_Keyboard.Initialize();
+
+ m_Mouse.Initialize();
+ m_Mouse.SetEnabled(CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_INPUT_ENABLEMOUSE));
+
+ m_enableController = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ SETTING_INPUT_ENABLE_CONTROLLER);
+}
+
+void CInputManager::Deinitialize()
+{
+}
+
+bool CInputManager::ProcessPeripherals(float frameTime)
+{
+ CKey key;
+ if (CServiceBroker::GetPeripherals().GetNextKeypress(frameTime, key))
+ return OnKey(key);
+ return false;
+}
+
+bool CInputManager::ProcessMouse(int windowId)
+{
+ if (!m_Mouse.IsActive() || !g_application.IsAppFocused())
+ return false;
+
+ // Get the mouse command ID
+ uint32_t mousekey = m_Mouse.GetKey();
+ if (mousekey == KEY_MOUSE_NOOP)
+ return true;
+
+ // Reset the screensaver and idle timers
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetSystemIdleTimer();
+ appPower->ResetScreenSaver();
+
+ if (appPower->WakeUpScreenSaverAndDPMS())
+ return true;
+
+ // Retrieve the corresponding action
+ CKey key(mousekey, (unsigned int)0);
+ CAction mouseaction = m_buttonTranslator->GetAction(windowId, key);
+
+ // Deactivate mouse if non-mouse action
+ if (!mouseaction.IsMouse())
+ m_Mouse.SetActive(false);
+
+ // Consume ACTION_NOOP.
+ // Some views or dialogs gets closed after any ACTION and
+ // a sensitive mouse might cause problems.
+ if (mouseaction.GetID() == ACTION_NOOP)
+ return false;
+
+ // If we couldn't find an action return false to indicate we have not
+ // handled this mouse action
+ if (!mouseaction.GetID())
+ {
+ CLog::LogF(LOGDEBUG, "unknown mouse command {}", mousekey);
+ return false;
+ }
+
+ // Log mouse actions except for move and noop
+ if (mouseaction.GetID() != ACTION_MOUSE_MOVE && mouseaction.GetID() != ACTION_NOOP)
+ CLog::LogF(LOGDEBUG, "trying mouse action {}", mouseaction.GetName());
+
+ // The action might not be a mouse action. For example wheel moves might
+ // be mapped to volume up/down in mouse.xml. In this case we do not want
+ // the mouse position saved in the action.
+ if (!mouseaction.IsMouse())
+ return g_application.OnAction(mouseaction);
+
+ // This is a mouse action so we need to record the mouse position
+ return g_application.OnAction(
+ CAction(mouseaction.GetID(), static_cast<uint32_t>(m_Mouse.GetHold(MOUSE_LEFT_BUTTON)),
+ static_cast<float>(m_Mouse.GetX()), static_cast<float>(m_Mouse.GetY()),
+ static_cast<float>(m_Mouse.GetDX()), static_cast<float>(m_Mouse.GetDY()), 0.0f, 0.0f,
+ mouseaction.GetName()));
+}
+
+bool CInputManager::ProcessEventServer(int windowId, float frameTime)
+{
+ CEventServer* es = CEventServer::GetInstance();
+ if (!es || !es->Running() || es->GetNumberOfClients() == 0)
+ return false;
+
+ // process any queued up actions
+ if (es->ExecuteNextAction())
+ {
+ // reset idle timers
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetSystemIdleTimer();
+ appPower->ResetScreenSaver();
+ appPower->WakeUpScreenSaverAndDPMS();
+ }
+
+ // now handle any buttons or axis
+ std::string strMapName;
+ bool isAxis = false;
+ float fAmount = 0.0;
+ bool isJoystick = false;
+
+ // es->ExecuteNextAction() invalidates the ref to the CEventServer instance
+ // when the action exits XBMC
+ es = CEventServer::GetInstance();
+ if (!es || !es->Running() || es->GetNumberOfClients() == 0)
+ return false;
+ unsigned int wKeyID = es->GetButtonCode(strMapName, isAxis, fAmount, isJoystick);
+
+ if (wKeyID)
+ {
+ if (strMapName.length() > 0)
+ {
+ // joysticks are not supported via eventserver
+ if (isJoystick)
+ {
+ return false;
+ }
+ else // it is a customcontroller
+ {
+ int actionID;
+ std::string actionName;
+
+ // Translate using custom controller translator.
+ if (m_customControllerTranslator->TranslateCustomControllerString(
+ windowId, strMapName, wKeyID, actionID, actionName))
+ {
+ // break screensaver
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetSystemIdleTimer();
+ appPower->ResetScreenSaver();
+
+ // in case we wokeup the screensaver or screen - eat that action...
+ if (appPower->WakeUpScreenSaverAndDPMS())
+ return true;
+
+ m_Mouse.SetActive(false);
+
+ CLog::Log(LOGDEBUG, "EventServer: key {} translated to action {}", wKeyID, actionName);
+
+ return ExecuteInputAction(CAction(actionID, fAmount, 0.0f, actionName));
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "ERROR mapping customcontroller action. CustomController: {} {}",
+ strMapName, wKeyID);
+ }
+ }
+ }
+ else
+ {
+ CKey key;
+ if (wKeyID & ES_FLAG_UNICODE)
+ {
+ key = CKey(0u, 0u, static_cast<wchar_t>(wKeyID & ~ES_FLAG_UNICODE), 0, 0, 0, 0);
+ return OnKey(key);
+ }
+
+ if (wKeyID == KEY_BUTTON_LEFT_ANALOG_TRIGGER)
+ key = CKey(wKeyID, static_cast<uint8_t>(255 * fAmount), 0, 0.0, 0.0, 0.0, 0.0, frameTime);
+ else if (wKeyID == KEY_BUTTON_RIGHT_ANALOG_TRIGGER)
+ key = CKey(wKeyID, 0, static_cast<uint8_t>(255 * fAmount), 0.0, 0.0, 0.0, 0.0, frameTime);
+ else if (wKeyID == KEY_BUTTON_LEFT_THUMB_STICK_LEFT)
+ key = CKey(wKeyID, 0, 0, -fAmount, 0.0, 0.0, 0.0, frameTime);
+ else if (wKeyID == KEY_BUTTON_LEFT_THUMB_STICK_RIGHT)
+ key = CKey(wKeyID, 0, 0, fAmount, 0.0, 0.0, 0.0, frameTime);
+ else if (wKeyID == KEY_BUTTON_LEFT_THUMB_STICK_UP)
+ key = CKey(wKeyID, 0, 0, 0.0, fAmount, 0.0, 0.0, frameTime);
+ else if (wKeyID == KEY_BUTTON_LEFT_THUMB_STICK_DOWN)
+ key = CKey(wKeyID, 0, 0, 0.0, -fAmount, 0.0, 0.0, frameTime);
+ else if (wKeyID == KEY_BUTTON_RIGHT_THUMB_STICK_LEFT)
+ key = CKey(wKeyID, 0, 0, 0.0, 0.0, -fAmount, 0.0, frameTime);
+ else if (wKeyID == KEY_BUTTON_RIGHT_THUMB_STICK_RIGHT)
+ key = CKey(wKeyID, 0, 0, 0.0, 0.0, fAmount, 0.0, frameTime);
+ else if (wKeyID == KEY_BUTTON_RIGHT_THUMB_STICK_UP)
+ key = CKey(wKeyID, 0, 0, 0.0, 0.0, 0.0, fAmount, frameTime);
+ else if (wKeyID == KEY_BUTTON_RIGHT_THUMB_STICK_DOWN)
+ key = CKey(wKeyID, 0, 0, 0.0, 0.0, 0.0, -fAmount, frameTime);
+ else
+ key = CKey(wKeyID);
+ key.SetFromService(true);
+ return OnKey(key);
+ }
+ }
+
+ {
+ CPoint pos;
+ if (es->GetMousePos(pos.x, pos.y) && m_Mouse.IsEnabled())
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEMOTION;
+ newEvent.motion.x = (uint16_t)pos.x;
+ newEvent.motion.y = (uint16_t)pos.y;
+ CServiceBroker::GetAppPort()->OnEvent(
+ newEvent); // had to call this to update g_Mouse position
+ return g_application.OnAction(CAction(ACTION_MOUSE_MOVE, pos.x, pos.y));
+ }
+ }
+
+ return false;
+}
+
+void CInputManager::ProcessQueuedActions()
+{
+ std::vector<CAction> queuedActions;
+ {
+ std::unique_lock<CCriticalSection> lock(m_actionMutex);
+ queuedActions.swap(m_queuedActions);
+ }
+
+ for (const CAction& action : queuedActions)
+ g_application.OnAction(action);
+}
+
+void CInputManager::QueueAction(const CAction& action)
+{
+ std::unique_lock<CCriticalSection> lock(m_actionMutex);
+
+ // Avoid dispatching multiple analog actions per frame with the same ID
+ if (action.IsAnalog())
+ {
+ m_queuedActions.erase(std::remove_if(m_queuedActions.begin(), m_queuedActions.end(),
+ [&action](const CAction& queuedAction) {
+ return action.GetID() == queuedAction.GetID();
+ }),
+ m_queuedActions.end());
+ }
+
+ m_queuedActions.push_back(action);
+}
+
+bool CInputManager::Process(int windowId, float frameTime)
+{
+ // process input actions
+ ProcessEventServer(windowId, frameTime);
+ ProcessPeripherals(frameTime);
+ ProcessQueuedActions();
+
+ // Inform the environment of the new active window ID
+ m_keymapEnvironment->SetWindowID(windowId);
+
+ return true;
+}
+
+bool CInputManager::OnEvent(XBMC_Event& newEvent)
+{
+ switch (newEvent.type)
+ {
+ case XBMC_KEYDOWN:
+ {
+ m_Keyboard.ProcessKeyDown(newEvent.key.keysym);
+ CKey key = m_Keyboard.TranslateKey(newEvent.key.keysym);
+ OnKey(key);
+ break;
+ }
+ case XBMC_KEYUP:
+ m_Keyboard.ProcessKeyUp();
+ OnKeyUp(m_Keyboard.TranslateKey(newEvent.key.keysym));
+ break;
+ case XBMC_MOUSEBUTTONDOWN:
+ case XBMC_MOUSEBUTTONUP:
+ case XBMC_MOUSEMOTION:
+ {
+ bool handled = false;
+
+ for (auto driverHandler : m_mouseHandlers)
+ {
+ switch (newEvent.type)
+ {
+ case XBMC_MOUSEMOTION:
+ {
+ if (driverHandler->OnPosition(newEvent.motion.x, newEvent.motion.y))
+ handled = true;
+ break;
+ }
+ case XBMC_MOUSEBUTTONDOWN:
+ {
+ MOUSE::BUTTON_ID buttonId;
+ if (CMouseTranslator::TranslateEventID(newEvent.button.button, buttonId))
+ {
+ if (driverHandler->OnButtonPress(buttonId))
+ handled = true;
+ }
+ break;
+ }
+ case XBMC_MOUSEBUTTONUP:
+ {
+ MOUSE::BUTTON_ID buttonId;
+ if (CMouseTranslator::TranslateEventID(newEvent.button.button, buttonId))
+ driverHandler->OnButtonRelease(buttonId);
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (handled)
+ break;
+ }
+
+ if (!handled)
+ {
+ m_Mouse.HandleEvent(newEvent);
+ ProcessMouse(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog());
+ }
+ break;
+ }
+ case XBMC_TOUCH:
+ {
+ if (newEvent.touch.action == ACTION_TOUCH_TAP)
+ { // Send a mouse motion event with no dx,dy for getting the current guiitem selected
+ g_application.OnAction(
+ CAction(ACTION_MOUSE_MOVE, 0, newEvent.touch.x, newEvent.touch.y, 0, 0));
+ }
+ int actionId = 0;
+ std::string actionString;
+ if (newEvent.touch.action == ACTION_GESTURE_BEGIN ||
+ newEvent.touch.action == ACTION_GESTURE_END ||
+ newEvent.touch.action == ACTION_GESTURE_ABORT)
+ actionId = newEvent.touch.action;
+ else
+ {
+ int iWin = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog();
+ m_touchTranslator->TranslateTouchAction(iWin, newEvent.touch.action,
+ newEvent.touch.pointers, actionId, actionString);
+ }
+
+ if (actionId <= 0)
+ return false;
+
+ if ((actionId >= ACTION_TOUCH_TAP && actionId <= ACTION_GESTURE_END) ||
+ (actionId >= ACTION_MOUSE_START && actionId <= ACTION_MOUSE_END))
+ {
+ auto action =
+ new CAction(actionId, 0, newEvent.touch.x, newEvent.touch.y, newEvent.touch.x2,
+ newEvent.touch.y2, newEvent.touch.x3, newEvent.touch.y3);
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(action));
+ }
+ else
+ {
+ if (actionId == ACTION_BUILT_IN_FUNCTION && !actionString.empty())
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(actionId, actionString)));
+ else
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(actionId)));
+ }
+
+ break;
+ } // case
+ case XBMC_BUTTON:
+ {
+ HandleKey(
+ m_buttonStat.TranslateKey(CKey(newEvent.keybutton.button, newEvent.keybutton.holdtime)));
+ break;
+ }
+ } // switch
+
+ return true;
+}
+
+// OnKey() translates the key into a CAction which is sent on to our Window Manager.
+// The window manager will return true if the event is processed, false otherwise.
+// If not already processed, this routine handles global keypresses. It returns
+// true if the key has been processed, false otherwise.
+
+bool CInputManager::OnKey(const CKey& key)
+{
+ bool bHandled = false;
+
+ for (auto handler : m_keyboardHandlers)
+ {
+ if (handler->OnKeyPress(key))
+ {
+ bHandled = true;
+ break;
+ }
+ }
+
+ if (bHandled)
+ {
+ m_LastKey.Reset();
+ }
+ else
+ {
+ if (key.GetButtonCode() == m_LastKey.GetButtonCode() &&
+ (m_LastKey.GetButtonCode() & CKey::MODIFIER_LONG))
+ {
+ // Do not repeat long presses
+ }
+ else
+ {
+ // Event server keyboard doesn't give normal key up and key down, so don't
+ // process for long press if that is the source
+ if (key.GetFromService() ||
+ !m_buttonTranslator->HasLongpressMapping(
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog(), key))
+ {
+ m_LastKey.Reset();
+ bHandled = HandleKey(key);
+ }
+ else
+ {
+ if (key.GetButtonCode() != m_LastKey.GetButtonCode() &&
+ (key.GetButtonCode() & CKey::MODIFIER_LONG))
+ {
+ m_LastKey = key; // OnKey is reentrant; need to do this before entering
+ bHandled = HandleKey(key);
+ }
+
+ m_LastKey = key;
+ }
+ }
+ }
+
+ return bHandled;
+}
+
+bool CInputManager::HandleKey(const CKey& key)
+{
+ // Turn the mouse off, as we've just got a keypress from controller or remote
+ m_Mouse.SetActive(false);
+
+ // get the current active window
+ int iWin = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog();
+
+ // this will be checked for certain keycodes that need
+ // special handling if the screensaver is active
+ CAction action = m_buttonTranslator->GetAction(iWin, key);
+
+ // a key has been pressed.
+ // reset Idle Timer
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetSystemIdleTimer();
+ bool processKey = AlwaysProcess(action);
+
+ if (StringUtils::StartsWithNoCase(action.GetName(), "CECToggleState") ||
+ StringUtils::StartsWithNoCase(action.GetName(), "CECStandby"))
+ {
+ // do not wake up the screensaver right after switching off the playing device
+ if (StringUtils::StartsWithNoCase(action.GetName(), "CECToggleState"))
+ {
+ CLog::LogF(LOGDEBUG, "action {} [{}], toggling state of playing device", action.GetName(),
+ action.GetID());
+ bool result;
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_CECTOGGLESTATE, 0, 0,
+ static_cast<void*>(&result));
+ if (!result)
+ return true;
+ }
+ else
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_CECSTANDBY);
+ return true;
+ }
+ }
+
+ appPower->ResetScreenSaver();
+
+ // allow some keys to be processed while the screensaver is active
+ if (appPower->WakeUpScreenSaverAndDPMS(processKey) && !processKey)
+ {
+ CLog::LogF(LOGDEBUG, "{} pressed, screen saver/dpms woken up",
+ m_Keyboard.GetKeyName((int)key.GetButtonCode()));
+ return true;
+ }
+
+ if (iWin != WINDOW_FULLSCREEN_VIDEO && iWin != WINDOW_FULLSCREEN_GAME)
+ {
+ // current active window isnt the fullscreen window
+ // just use corresponding section from keymap.xml
+ // to map key->action
+
+ // first determine if we should use keyboard input directly
+ bool useKeyboard =
+ key.FromKeyboard() && (iWin == WINDOW_DIALOG_KEYBOARD || iWin == WINDOW_DIALOG_NUMERIC);
+ CGUIWindow* window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(iWin);
+ if (window)
+ {
+ CGUIControl* control = window->GetFocusedControl();
+ if (control)
+ {
+ // If this is an edit control set usekeyboard to true. This causes the
+ // keypress to be processed directly not through the key mappings.
+ if (control->GetControlType() == CGUIControl::GUICONTROL_EDIT)
+ useKeyboard = true;
+
+ // If the key pressed is shift-A to shift-Z set usekeyboard to true.
+ // This causes the keypress to be used for list navigation.
+ if (control->IsContainer() && key.GetModifiers() == CKey::MODIFIER_SHIFT &&
+ key.GetUnicode())
+ useKeyboard = true;
+ }
+ }
+ if (useKeyboard)
+ {
+ // use the virtualkeyboard section of the keymap, and send keyboard-specific or navigation
+ // actions through if that's what they are
+ CAction action = m_buttonTranslator->GetAction(WINDOW_DIALOG_KEYBOARD, key);
+ if (!(action.GetID() == ACTION_MOVE_LEFT || action.GetID() == ACTION_MOVE_RIGHT ||
+ action.GetID() == ACTION_MOVE_UP || action.GetID() == ACTION_MOVE_DOWN ||
+ action.GetID() == ACTION_SELECT_ITEM || action.GetID() == ACTION_ENTER ||
+ action.GetID() == ACTION_PREVIOUS_MENU || action.GetID() == ACTION_NAV_BACK ||
+ action.GetID() == ACTION_VOICE_RECOGNIZE))
+ {
+ // the action isn't plain navigation - check for a keyboard-specific keymap
+ action = m_buttonTranslator->GetAction(WINDOW_DIALOG_KEYBOARD, key, false);
+ if (!(action.GetID() >= REMOTE_0 && action.GetID() <= REMOTE_9) ||
+ action.GetID() == ACTION_BACKSPACE || action.GetID() == ACTION_SHIFT ||
+ action.GetID() == ACTION_SYMBOLS || action.GetID() == ACTION_CURSOR_LEFT ||
+ action.GetID() == ACTION_CURSOR_RIGHT)
+ action = CAction(0); // don't bother with this action
+ }
+ // else pass the keys through directly
+ if (!action.GetID())
+ {
+ if (key.GetFromService())
+ action = CAction(key.GetButtonCode() != KEY_INVALID ? key.GetButtonCode() : 0,
+ key.GetUnicode());
+ else
+ {
+ // Check for paste keypress
+#ifdef TARGET_WINDOWS
+ // In Windows paste is ctrl-V
+ if (key.GetVKey() == XBMCVK_V && key.GetModifiers() == CKey::MODIFIER_CTRL)
+#elif defined(TARGET_LINUX)
+ // In Linux paste is ctrl-V
+ if (key.GetVKey() == XBMCVK_V && key.GetModifiers() == CKey::MODIFIER_CTRL)
+#elif defined(TARGET_DARWIN_OSX)
+ // In OSX paste is cmd-V
+ if (key.GetVKey() == XBMCVK_V && key.GetModifiers() == CKey::MODIFIER_META)
+#else
+ // Placeholder for other operating systems
+ if (false)
+#endif
+ action = CAction(ACTION_PASTE);
+ // If the unicode is non-zero the keypress is a non-printing character
+ else if (key.GetUnicode())
+ action = CAction(KEY_UNICODE, key.GetUnicode());
+ // The keypress is a non-printing character
+ else
+ action = CAction(key.GetVKey() | KEY_VKEY);
+ }
+ }
+
+ CLog::LogF(LOGDEBUG, "{} pressed, trying keyboard action {:x}",
+ m_Keyboard.GetKeyName((int)key.GetButtonCode()), action.GetID());
+
+ if (g_application.OnAction(action))
+ return true;
+ // failed to handle the keyboard action, drop down through to standard action
+ }
+ if (key.GetFromService())
+ {
+ if (key.GetButtonCode() != KEY_INVALID)
+ action = m_buttonTranslator->GetAction(iWin, key);
+ }
+ else
+ action = m_buttonTranslator->GetAction(iWin, key);
+ }
+ if (!key.IsAnalogButton())
+ CLog::LogF(LOGDEBUG, "{} pressed, window {}, action is {}",
+ m_Keyboard.GetKeyName((int)key.GetButtonCode()), iWin, action.GetName());
+
+ return ExecuteInputAction(action);
+}
+
+void CInputManager::OnKeyUp(const CKey& key)
+{
+ for (auto handler : m_keyboardHandlers)
+ handler->OnKeyRelease(key);
+
+ if (m_LastKey.GetButtonCode() != KEY_INVALID &&
+ !(m_LastKey.GetButtonCode() & CKey::MODIFIER_LONG))
+ {
+ CKey key = m_LastKey;
+ m_LastKey.Reset(); // OnKey is reentrant; need to do this before entering
+ HandleKey(key);
+ }
+ else
+ m_LastKey.Reset();
+}
+
+bool CInputManager::AlwaysProcess(const CAction& action)
+{
+ // check if this button is mapped to a built-in function
+ if (!action.GetName().empty())
+ {
+ const CExecString exec(action.GetName());
+ if (exec.IsValid())
+ {
+ const std::string builtInFunction = exec.GetFunction();
+
+ // should this button be handled normally or just cancel the screensaver?
+ if (builtInFunction == "powerdown" || builtInFunction == "reboot" ||
+ builtInFunction == "restart" || builtInFunction == "restartapp" ||
+ builtInFunction == "suspend" || builtInFunction == "hibernate" ||
+ builtInFunction == "quit" || builtInFunction == "shutdown" ||
+ builtInFunction == "volumeup" || builtInFunction == "volumedown" ||
+ builtInFunction == "mute" || builtInFunction == "RunAppleScript" ||
+ builtInFunction == "RunAddon" || builtInFunction == "RunPlugin" ||
+ builtInFunction == "RunScript" || builtInFunction == "System.Exec" ||
+ builtInFunction == "System.ExecWait")
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool CInputManager::ExecuteInputAction(const CAction& action)
+{
+ bool bResult = false;
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+
+ // play sound before the action unless the button is held,
+ // where we execute after the action as held actions aren't fired every time.
+ if (action.GetHoldTime())
+ {
+ bResult = g_application.OnAction(action);
+ if (bResult && gui)
+ gui->GetAudioManager().PlayActionSound(action);
+ }
+ else
+ {
+ if (gui)
+ gui->GetAudioManager().PlayActionSound(action);
+
+ bResult = g_application.OnAction(action);
+ }
+ return bResult;
+}
+
+bool CInputManager::HasBuiltin(const std::string& command)
+{
+ return false;
+}
+
+int CInputManager::ExecuteBuiltin(const std::string& execute,
+ const std::vector<std::string>& params)
+{
+ return 0;
+}
+
+void CInputManager::SetMouseActive(bool active /* = true */)
+{
+ m_Mouse.SetActive(active);
+}
+
+void CInputManager::SetMouseEnabled(bool mouseEnabled /* = true */)
+{
+ m_Mouse.SetEnabled(mouseEnabled);
+}
+
+bool CInputManager::IsMouseActive()
+{
+ return m_Mouse.IsActive();
+}
+
+MOUSE_STATE CInputManager::GetMouseState()
+{
+ return m_Mouse.GetState();
+}
+
+MousePosition CInputManager::GetMousePosition()
+{
+ return m_Mouse.GetPosition();
+}
+
+void CInputManager::SetMouseResolution(int maxX, int maxY, float speedX, float speedY)
+{
+ m_Mouse.SetResolution(maxX, maxY, speedX, speedY);
+}
+
+void CInputManager::SetMouseState(MOUSE_STATE mouseState)
+{
+ m_Mouse.SetState(mouseState);
+}
+
+bool CInputManager::IsControllerEnabled() const
+{
+ return m_enableController;
+}
+
+void CInputManager::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_INPUT_ENABLEMOUSE)
+ m_Mouse.SetEnabled(std::dynamic_pointer_cast<const CSettingBool>(setting)->GetValue());
+
+ else if (settingId == SETTING_INPUT_ENABLE_CONTROLLER)
+ m_enableController = std::dynamic_pointer_cast<const CSettingBool>(setting)->GetValue();
+}
+
+bool CInputManager::OnAction(const CAction& action)
+{
+ if (action.GetID() != ACTION_NONE)
+ {
+ if (action.IsAnalog())
+ {
+ QueueAction(action);
+ }
+ else
+ {
+ // If button was pressed this frame, send action
+ if (action.GetHoldTime() == 0)
+ {
+ QueueAction(action);
+ }
+ else
+ {
+ // Only send repeated actions for basic navigation commands
+ bool bIsNavigation = false;
+
+ switch (action.GetID())
+ {
+ case ACTION_MOVE_LEFT:
+ case ACTION_MOVE_RIGHT:
+ case ACTION_MOVE_UP:
+ case ACTION_MOVE_DOWN:
+ case ACTION_PAGE_UP:
+ case ACTION_PAGE_DOWN:
+ bIsNavigation = true;
+ break;
+
+ default:
+ break;
+ }
+
+ if (bIsNavigation)
+ QueueAction(action);
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+bool CInputManager::LoadKeymaps()
+{
+ bool bSuccess = false;
+
+ if (m_buttonTranslator->Load())
+ {
+ bSuccess = true;
+ }
+
+ SetChanged();
+ NotifyObservers(ObservableMessageButtonMapsChanged);
+
+ return bSuccess;
+}
+
+bool CInputManager::ReloadKeymaps()
+{
+ return LoadKeymaps();
+}
+
+void CInputManager::ClearKeymaps()
+{
+ m_buttonTranslator->Clear();
+
+ SetChanged();
+ NotifyObservers(ObservableMessageButtonMapsChanged);
+}
+
+void CInputManager::AddKeymap(const std::string& keymap)
+{
+ if (m_buttonTranslator->AddDevice(keymap))
+ {
+ SetChanged();
+ NotifyObservers(ObservableMessageButtonMapsChanged);
+ }
+}
+
+void CInputManager::RemoveKeymap(const std::string& keymap)
+{
+ if (m_buttonTranslator->RemoveDevice(keymap))
+ {
+ SetChanged();
+ NotifyObservers(ObservableMessageButtonMapsChanged);
+ }
+}
+
+CAction CInputManager::GetAction(int window, const CKey& key, bool fallback /* = true */)
+{
+ return m_buttonTranslator->GetAction(window, key, fallback);
+}
+
+bool CInputManager::TranslateCustomControllerString(int windowId,
+ const std::string& controllerName,
+ int buttonId,
+ int& action,
+ std::string& strAction)
+{
+ return m_customControllerTranslator->TranslateCustomControllerString(windowId, controllerName,
+ buttonId, action, strAction);
+}
+
+bool CInputManager::TranslateTouchAction(
+ int windowId, int touchAction, int touchPointers, int& action, std::string& actionString)
+{
+ return m_touchTranslator->TranslateTouchAction(windowId, touchAction, touchPointers, action,
+ actionString);
+}
+
+std::vector<std::shared_ptr<const IWindowKeymap>> CInputManager::GetJoystickKeymaps() const
+{
+ return m_joystickTranslator->GetJoystickKeymaps();
+}
+
+void CInputManager::RegisterKeyboardDriverHandler(KEYBOARD::IKeyboardDriverHandler* handler)
+{
+ if (std::find(m_keyboardHandlers.begin(), m_keyboardHandlers.end(), handler) ==
+ m_keyboardHandlers.end())
+ m_keyboardHandlers.insert(m_keyboardHandlers.begin(), handler);
+}
+
+void CInputManager::UnregisterKeyboardDriverHandler(KEYBOARD::IKeyboardDriverHandler* handler)
+{
+ m_keyboardHandlers.erase(
+ std::remove(m_keyboardHandlers.begin(), m_keyboardHandlers.end(), handler),
+ m_keyboardHandlers.end());
+}
+
+void CInputManager::RegisterMouseDriverHandler(MOUSE::IMouseDriverHandler* handler)
+{
+ if (std::find(m_mouseHandlers.begin(), m_mouseHandlers.end(), handler) == m_mouseHandlers.end())
+ m_mouseHandlers.insert(m_mouseHandlers.begin(), handler);
+}
+
+void CInputManager::UnregisterMouseDriverHandler(MOUSE::IMouseDriverHandler* handler)
+{
+ m_mouseHandlers.erase(std::remove(m_mouseHandlers.begin(), m_mouseHandlers.end(), handler),
+ m_mouseHandlers.end());
+}
diff --git a/xbmc/input/InputManager.h b/xbmc/input/InputManager.h
new file mode 100644
index 0000000..52ad459
--- /dev/null
+++ b/xbmc/input/InputManager.h
@@ -0,0 +1,310 @@
+/*
+ * 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 "input/KeyboardStat.h"
+#include "input/actions/Action.h"
+#include "input/button/ButtonStat.h"
+#include "input/mouse/MouseStat.h"
+#include "input/mouse/interfaces/IMouseInputProvider.h"
+#include "interfaces/IActionListener.h"
+#include "settings/lib/ISettingCallback.h"
+#include "threads/CriticalSection.h"
+#include "utils/Observer.h"
+#include "windowing/XBMC_events.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CButtonTranslator;
+class CCustomControllerTranslator;
+class CJoystickMapper;
+class CKey;
+class CProfileManager;
+class CTouchTranslator;
+class IKeymapEnvironment;
+class IWindowKeymap;
+
+namespace KODI
+{
+
+namespace KEYBOARD
+{
+class IKeyboardDriverHandler;
+}
+
+namespace MOUSE
+{
+class IMouseDriverHandler;
+}
+} // namespace KODI
+
+/// \addtogroup input
+/// \{
+
+/*!
+ * \ingroup input keyboard mouse touch joystick
+ * \brief Main input processing class.
+ *
+ * This class consolidates all input generated from different sources such as
+ * mouse, keyboard, joystick or touch (in \ref OnEvent).
+ *
+ * \copydoc keyboard
+ * \copydoc mouse
+ */
+class CInputManager : public ISettingCallback, public IActionListener, public Observable
+{
+public:
+ CInputManager();
+ CInputManager(const CInputManager&) = delete;
+ CInputManager const& operator=(CInputManager const&) = delete;
+ ~CInputManager() override;
+
+ /*! \brief decode a mouse event and reset idle timers.
+ *
+ * \param windowId Currently active window
+ * \return true if event is handled, false otherwise
+ */
+ bool ProcessMouse(int windowId);
+
+ /*! \brief decode an event from the event service, this can be mouse, key, joystick, reset idle
+ * timers.
+ *
+ * \param windowId Currently active window
+ * \param frameTime Time in seconds since last call
+ * \return true if event is handled, false otherwise
+ */
+ bool ProcessEventServer(int windowId, float frameTime);
+
+ /*! \brief decode an event from peripherals.
+ *
+ * \param frameTime Time in seconds since last call
+ * \return true if event is handled, false otherwise
+ */
+ bool ProcessPeripherals(float frameTime);
+
+ /*! \brief Process all inputs
+ *
+ * \param windowId Currently active window
+ * \param frameTime Time in seconds since last call
+ * \return true on success, false otherwise
+ */
+ bool Process(int windowId, float frameTime);
+
+ /*!
+ * \brief Call once during application startup to initialize peripherals that need it
+ */
+ void InitializeInputs();
+
+ /*!
+ * \brief Deinitialize input and keymaps
+ */
+ void Deinitialize();
+
+ /*! \brief Handle an input event
+ *
+ * \param newEvent event details
+ * \return true on successfully handled event
+ * \sa XBMC_Event
+ */
+ bool OnEvent(XBMC_Event& newEvent);
+
+ /*! \brief Control if the mouse is actively used or not
+ *
+ * \param[in] active sets mouse active or inactive
+ */
+ void SetMouseActive(bool active = true);
+
+ /*! \brief Control if we should use a mouse or not
+ *
+ * \param[in] mouseEnabled sets mouse enabled or disabled
+ */
+ void SetMouseEnabled(bool mouseEnabled = true);
+
+ /*! \brief Set the current state of the mouse such as click, drag operation
+ *
+ * \param[in] mouseState which state the mouse should be set to
+ * \sa MOUSE_STATE
+ */
+ void SetMouseState(MOUSE_STATE mouseState);
+
+ /*! \brief Check if the mouse is currently active
+ *
+ * \return true if active, false otherwise
+ */
+ bool IsMouseActive();
+
+ /*! \brief Get the current state of the mouse, such as click or drag operation
+ *
+ * \return the current state of the mouse as a value from MOUSE_STATE
+ * \sa MOUSE_STATE
+ */
+ MOUSE_STATE GetMouseState();
+
+ /*! \brief Get the current mouse positions x and y coordinates
+ *
+ * \return a struct containing the x and y coordinates
+ * \sa MousePosition
+ */
+ MousePosition GetMousePosition();
+
+ /*! \brief Set the current screen resolution and pointer speed
+ *
+ * \param[in] maxX screen width
+ * \param[in] maxY screen height
+ * \param[in] speedX mouse speed in x dimension
+ * \param[in] speedY mouse speed in y dimension
+ * \return
+ */
+ void SetMouseResolution(int maxX, int maxY, float speedX, float speedY);
+
+ /*! \brief Get the status of the controller-enable setting
+ * \return True if controller input is enabled for the UI, false otherwise
+ */
+ bool IsControllerEnabled() const;
+
+ /*! \brief Returns whether or not we can handle a given built-in command.
+ *
+ */
+ bool HasBuiltin(const std::string& command);
+
+ /*! \brief Parse a builtin command and execute any input action
+ * currently only LIRC commands implemented
+ *
+ * \param[in] execute Command to execute
+ * \param[in] params parameters that was passed to the command
+ * \return 0 on success, -1 on failure
+ */
+ int ExecuteBuiltin(const std::string& execute, const std::vector<std::string>& params);
+
+ // Button translation
+ bool LoadKeymaps();
+ bool ReloadKeymaps();
+ void ClearKeymaps();
+ void AddKeymap(const std::string& keymap);
+ void RemoveKeymap(const std::string& keymap);
+
+ const IKeymapEnvironment* KeymapEnvironment() const { return m_keymapEnvironment.get(); }
+
+ /*! \brief Obtain the action configured for a given window and key
+ *
+ * \param window the window id
+ * \param key the key to query the action for
+ * \param fallback if no action is directly configured for the given window, obtain the action
+ * from fallback window, if exists or from global config as last resort
+ *
+ * \return the action matching the key
+ */
+ CAction GetAction(int window, const CKey& key, bool fallback = true);
+
+ bool TranslateCustomControllerString(int windowId,
+ const std::string& controllerName,
+ int buttonId,
+ int& action,
+ std::string& strAction);
+
+ bool TranslateTouchAction(
+ int windowId, int touchAction, int touchPointers, int& action, std::string& actionString);
+
+ std::vector<std::shared_ptr<const IWindowKeymap>> GetJoystickKeymaps() const;
+
+ /*!
+ * \brief Queue an action to be processed on the next call to Process()
+ */
+ void QueueAction(const CAction& action);
+
+ // implementation of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ // implementation of IActionListener
+ bool OnAction(const CAction& action) override;
+
+ void RegisterKeyboardDriverHandler(KODI::KEYBOARD::IKeyboardDriverHandler* handler);
+ void UnregisterKeyboardDriverHandler(KODI::KEYBOARD::IKeyboardDriverHandler* handler);
+
+ virtual void RegisterMouseDriverHandler(KODI::MOUSE::IMouseDriverHandler* handler);
+ virtual void UnregisterMouseDriverHandler(KODI::MOUSE::IMouseDriverHandler* handler);
+
+private:
+ /*! \brief Process keyboard event and translate into an action
+ *
+ * \param key keypress details
+ * \return true on successfully handled event
+ * \sa CKey
+ */
+ bool OnKey(const CKey& key);
+
+ /*! \brief Process key up event
+ *
+ * \param key details of released key
+ * \sa CKey
+ */
+ void OnKeyUp(const CKey& key);
+
+ /*! \brief Handle keypress
+ *
+ * \param key keypress details
+ * \return true on successfully handled event
+ */
+ bool HandleKey(const CKey& key);
+
+ /*! \brief Determine if an action should be processed or just
+ * cancel the screensaver
+ *
+ * \param action Action that is about to be processed
+ * \return true on any poweractions such as shutdown/reboot/sleep/suspend, false otherwise
+ * \sa CAction
+ */
+ bool AlwaysProcess(const CAction& action);
+
+ /*! \brief Send the Action to CApplication for further handling,
+ * play a sound before or after sending the action.
+ *
+ * \param action Action to send to CApplication
+ * \return result from CApplication::OnAction
+ * \sa CAction
+ */
+ bool ExecuteInputAction(const CAction& action);
+
+ /*! \brief Dispatch actions queued since the last call to Process()
+ */
+ void ProcessQueuedActions();
+
+ CKeyboardStat m_Keyboard;
+ KODI::INPUT::CButtonStat m_buttonStat;
+ CMouseStat m_Mouse;
+ CKey m_LastKey;
+
+ std::map<std::string, std::map<int, float>> m_lastAxisMap;
+
+ std::vector<CAction> m_queuedActions;
+ CCriticalSection m_actionMutex;
+
+ // Button translation
+ std::unique_ptr<IKeymapEnvironment> m_keymapEnvironment;
+ std::unique_ptr<CButtonTranslator> m_buttonTranslator;
+ std::unique_ptr<CCustomControllerTranslator> m_customControllerTranslator;
+ std::unique_ptr<CTouchTranslator> m_touchTranslator;
+ std::unique_ptr<CJoystickMapper> m_joystickTranslator;
+
+ std::vector<KODI::KEYBOARD::IKeyboardDriverHandler*> m_keyboardHandlers;
+ std::vector<KODI::MOUSE::IMouseDriverHandler*> m_mouseHandlers;
+
+ std::unique_ptr<KODI::KEYBOARD::IKeyboardDriverHandler> m_keyboardEasterEgg;
+
+ // Input state
+ bool m_enableController = true;
+
+ // Settings
+ static const std::string SETTING_INPUT_ENABLE_CONTROLLER;
+};
+
+/// \}
diff --git a/xbmc/input/InputTranslator.cpp b/xbmc/input/InputTranslator.cpp
new file mode 100644
index 0000000..6e3786d
--- /dev/null
+++ b/xbmc/input/InputTranslator.cpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 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 "InputTranslator.h"
+
+using namespace KODI;
+using namespace INPUT;
+
+#define TAN_1_8_PI 0.4142136f // tan(1/8*PI)
+#define TAN_3_8_PI 2.4142136f // tan(3/8*PI)
+
+CARDINAL_DIRECTION CInputTranslator::VectorToCardinalDirection(float x, float y)
+{
+ if (y >= -x && y > x)
+ return CARDINAL_DIRECTION::UP;
+ else if (y <= x && y > -x)
+ return CARDINAL_DIRECTION::RIGHT;
+ else if (y <= -x && y < x)
+ return CARDINAL_DIRECTION::DOWN;
+ else if (y >= x && y < -x)
+ return CARDINAL_DIRECTION::LEFT;
+
+ return CARDINAL_DIRECTION::NONE;
+}
+
+INTERCARDINAL_DIRECTION CInputTranslator::VectorToIntercardinalDirection(float x, float y)
+{
+ if (y >= TAN_3_8_PI * -x && y > TAN_3_8_PI * x)
+ return INTERCARDINAL_DIRECTION::UP;
+ else if (y <= TAN_3_8_PI * x && y > TAN_1_8_PI * x)
+ return INTERCARDINAL_DIRECTION::RIGHTUP;
+ else if (y <= TAN_1_8_PI * x && y > TAN_1_8_PI * -x)
+ return INTERCARDINAL_DIRECTION::RIGHT;
+ else if (y <= TAN_1_8_PI * -x && y > TAN_3_8_PI * -x)
+ return INTERCARDINAL_DIRECTION::RIGHTDOWN;
+ else if (y <= TAN_3_8_PI * -x && y < TAN_3_8_PI * x)
+ return INTERCARDINAL_DIRECTION::DOWN;
+ else if (y >= TAN_3_8_PI * x && y < TAN_1_8_PI * x)
+ return INTERCARDINAL_DIRECTION::LEFTDOWN;
+ else if (y >= TAN_1_8_PI * x && y < TAN_1_8_PI * -x)
+ return INTERCARDINAL_DIRECTION::LEFT;
+ else if (y >= TAN_1_8_PI * -x && y < TAN_3_8_PI * -x)
+ return INTERCARDINAL_DIRECTION::LEFTUP;
+
+ return INTERCARDINAL_DIRECTION::NONE;
+}
diff --git a/xbmc/input/InputTranslator.h b/xbmc/input/InputTranslator.h
new file mode 100644
index 0000000..70047f5
--- /dev/null
+++ b/xbmc/input/InputTranslator.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 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 "InputTypes.h"
+
+namespace KODI
+{
+namespace INPUT
+{
+class CInputTranslator
+{
+public:
+ /*!
+ * \brief Get the closest cardinal direction to the given vector
+ *
+ * This function assumes a right-handed cartesian coordinate system; positive
+ * X is right, positive Y is up.
+ *
+ * Ties are resolved in the clockwise direction: (0.5, 0.5) will resolve to
+ * RIGHT.
+ *
+ * \param x The x component of the vector
+ * \param y The y component of the vector
+ *
+ * \return The closest cardinal direction (up, down, right or left), or
+ * CARDINAL_DIRECTION::NONE if x and y are both 0
+ */
+ static CARDINAL_DIRECTION VectorToCardinalDirection(float x, float y);
+
+ /*!
+ * \brief Get the closest cardinal or intercardinal direction to the given
+ * vector
+ *
+ * This function assumes a right-handed cartesian coordinate system; positive
+ * X is right, positive Y is up.
+ *
+ * Ties are resolved in the clockwise direction.
+ *
+ * \param x The x component of the vector
+ * \param y The y component of the vector
+ *
+ * \return The closest intercardinal direction, or
+ * INTERCARDINAL_DIRECTION::NONE if x and y are both 0
+ */
+ static INTERCARDINAL_DIRECTION VectorToIntercardinalDirection(float x, float y);
+};
+} // namespace INPUT
+} // namespace KODI
diff --git a/xbmc/input/InputTypes.h b/xbmc/input/InputTypes.h
new file mode 100644
index 0000000..851beb7
--- /dev/null
+++ b/xbmc/input/InputTypes.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 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
+
+namespace KODI
+{
+namespace INPUT
+{
+/*!
+ * \brief Cardinal directions, used for input device motions
+ */
+enum class CARDINAL_DIRECTION
+{
+ NONE = 0x0,
+ UP = 0x1,
+ DOWN = 0x2,
+ RIGHT = 0x4,
+ LEFT = 0x8,
+};
+
+/*!
+ * \brief Cardinal and intercardinal directions, used for input device motions
+ */
+enum class INTERCARDINAL_DIRECTION
+{
+ NONE = static_cast<unsigned int>(CARDINAL_DIRECTION::NONE),
+ UP = static_cast<unsigned int>(CARDINAL_DIRECTION::UP),
+ DOWN = static_cast<unsigned int>(CARDINAL_DIRECTION::DOWN),
+ RIGHT = static_cast<unsigned int>(CARDINAL_DIRECTION::RIGHT),
+ LEFT = static_cast<unsigned int>(CARDINAL_DIRECTION::LEFT),
+ RIGHTUP = RIGHT | UP,
+ RIGHTDOWN = RIGHT | DOWN,
+ LEFTUP = LEFT | UP,
+ LEFTDOWN = LEFT | DOWN,
+};
+
+const unsigned int HOLD_TRESHOLD = 250;
+
+} // namespace INPUT
+} // namespace KODI
diff --git a/xbmc/input/JoystickMapper.cpp b/xbmc/input/JoystickMapper.cpp
new file mode 100644
index 0000000..233238d
--- /dev/null
+++ b/xbmc/input/JoystickMapper.cpp
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2017-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 "JoystickMapper.h"
+
+#include "input/WindowKeymap.h"
+#include "input/actions/ActionIDs.h"
+#include "input/actions/ActionTranslator.h"
+#include "input/joysticks/JoystickTranslator.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+
+#include <algorithm>
+#include <sstream>
+#include <utility>
+
+using namespace KODI;
+
+#define JOYSTICK_XML_NODE_PROFILE "profile"
+#define JOYSTICK_XML_ATTR_DIRECTION "direction"
+#define JOYSTICK_XML_ATTR_HOLDTIME "holdtime"
+#define JOYSTICK_XML_ATTR_HOTKEY "hotkey"
+
+void CJoystickMapper::MapActions(int windowID, const TiXmlNode* pDevice)
+{
+ std::string controllerId;
+ DeserializeJoystickNode(pDevice, controllerId);
+ if (controllerId.empty())
+ return;
+
+ // Update Controller IDs
+ if (std::find(m_controllerIds.begin(), m_controllerIds.end(), controllerId) ==
+ m_controllerIds.end())
+ m_controllerIds.emplace_back(controllerId);
+
+ // Create/overwrite keymap
+ auto& keymap = m_joystickKeymaps[controllerId];
+ if (!keymap)
+ keymap.reset(new CWindowKeymap(controllerId));
+
+ const TiXmlElement* pButton = pDevice->FirstChildElement();
+ while (pButton != nullptr)
+ {
+ std::string feature;
+ JOYSTICK::ANALOG_STICK_DIRECTION dir;
+ unsigned int holdtimeMs;
+ std::set<std::string> hotkeys;
+ std::string actionString;
+ if (DeserializeButton(pButton, feature, dir, holdtimeMs, hotkeys, actionString))
+ {
+ // Update keymap
+ unsigned int actionId = ACTION_NONE;
+ if (CActionTranslator::TranslateString(actionString, actionId))
+ {
+ JOYSTICK::KeymapAction action = {
+ actionId,
+ std::move(actionString),
+ holdtimeMs,
+ std::move(hotkeys),
+ };
+ keymap->MapAction(windowID, JOYSTICK::CJoystickUtils::MakeKeyName(feature, dir),
+ std::move(action));
+ }
+ }
+ pButton = pButton->NextSiblingElement();
+ }
+}
+
+void CJoystickMapper::Clear()
+{
+ m_joystickKeymaps.clear();
+ m_controllerIds.clear();
+}
+
+std::vector<std::shared_ptr<const IWindowKeymap>> CJoystickMapper::GetJoystickKeymaps() const
+{
+ std::vector<std::shared_ptr<const IWindowKeymap>> keymaps;
+
+ for (const auto& controllerId : m_controllerIds)
+ {
+ auto it = m_joystickKeymaps.find(controllerId);
+ if (it != m_joystickKeymaps.end())
+ keymaps.emplace_back(it->second);
+ }
+
+ return keymaps;
+}
+
+void CJoystickMapper::DeserializeJoystickNode(const TiXmlNode* pDevice, std::string& controllerId)
+{
+ const TiXmlElement* deviceElem = pDevice->ToElement();
+ if (deviceElem != nullptr)
+ deviceElem->QueryValueAttribute(JOYSTICK_XML_NODE_PROFILE, &controllerId);
+}
+
+bool CJoystickMapper::DeserializeButton(const TiXmlElement* pButton,
+ std::string& feature,
+ JOYSTICK::ANALOG_STICK_DIRECTION& dir,
+ unsigned int& holdtimeMs,
+ std::set<std::string>& hotkeys,
+ std::string& actionStr)
+{
+ const char* szButton = pButton->Value();
+ if (szButton != nullptr)
+ {
+ const char* szAction = nullptr;
+
+ const TiXmlNode* actionNode = pButton->FirstChild();
+ if (actionNode != nullptr)
+ szAction = actionNode->Value();
+
+ if (szAction != nullptr)
+ {
+ feature = szButton;
+ StringUtils::ToLower(feature);
+ actionStr = szAction;
+ }
+ }
+
+ if (!feature.empty() && !actionStr.empty())
+ {
+ // Handle direction
+ dir = JOYSTICK::ANALOG_STICK_DIRECTION::NONE;
+ const char* szDirection = pButton->Attribute(JOYSTICK_XML_ATTR_DIRECTION);
+ if (szDirection != nullptr)
+ dir = JOYSTICK::CJoystickTranslator::TranslateAnalogStickDirection(szDirection);
+
+ // Process holdtime parameter
+ holdtimeMs = 0;
+ std::string strHoldTime;
+ if (pButton->QueryValueAttribute(JOYSTICK_XML_ATTR_HOLDTIME, &strHoldTime) == TIXML_SUCCESS)
+ {
+ std::istringstream ss(strHoldTime);
+ ss >> holdtimeMs;
+ }
+
+ // Process hotkeys
+ hotkeys.clear();
+ std::string strHotkeys;
+ if (pButton->QueryValueAttribute(JOYSTICK_XML_ATTR_HOTKEY, &strHotkeys) == TIXML_SUCCESS)
+ {
+ std::vector<std::string> vecHotkeys = StringUtils::Split(strHotkeys, ",");
+ for (auto& hotkey : vecHotkeys)
+ hotkeys.insert(std::move(hotkey));
+ }
+
+ return true;
+ }
+
+ return false;
+}
diff --git a/xbmc/input/JoystickMapper.h b/xbmc/input/JoystickMapper.h
new file mode 100644
index 0000000..ed5e61a
--- /dev/null
+++ b/xbmc/input/JoystickMapper.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017-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 "IButtonMapper.h"
+#include "input/joysticks/JoystickTypes.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+class IWindowKeymap;
+class TiXmlElement;
+class TiXmlNode;
+
+class CJoystickMapper : public IButtonMapper
+{
+public:
+ CJoystickMapper() = default;
+ ~CJoystickMapper() override = default;
+
+ // implementation of IButtonMapper
+ void MapActions(int windowID, const TiXmlNode* pDevice) override;
+ void Clear() override;
+
+ std::vector<std::shared_ptr<const IWindowKeymap>> GetJoystickKeymaps() const;
+
+private:
+ void DeserializeJoystickNode(const TiXmlNode* pDevice, std::string& controllerId);
+ bool DeserializeButton(const TiXmlElement* pButton,
+ std::string& feature,
+ KODI::JOYSTICK::ANALOG_STICK_DIRECTION& dir,
+ unsigned int& holdtimeMs,
+ std::set<std::string>& hotkeys,
+ std::string& actionStr);
+
+ using ControllerID = std::string;
+ std::map<ControllerID, std::shared_ptr<IWindowKeymap>> m_joystickKeymaps;
+
+ std::vector<std::string> m_controllerIds;
+};
diff --git a/xbmc/input/Key.cpp b/xbmc/input/Key.cpp
new file mode 100644
index 0000000..24f6a0c
--- /dev/null
+++ b/xbmc/input/Key.cpp
@@ -0,0 +1,178 @@
+/*
+ * 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 "input/Key.h"
+
+CKey::CKey(void)
+{
+ Reset();
+}
+
+CKey::~CKey(void) = default;
+
+CKey::CKey(uint32_t buttonCode,
+ uint8_t leftTrigger,
+ uint8_t rightTrigger,
+ float leftThumbX,
+ float leftThumbY,
+ float rightThumbX,
+ float rightThumbY,
+ float repeat)
+{
+ Reset();
+ m_buttonCode = buttonCode;
+ m_leftTrigger = leftTrigger;
+ m_rightTrigger = rightTrigger;
+ m_leftThumbX = leftThumbX;
+ m_leftThumbY = leftThumbY;
+ m_rightThumbX = rightThumbX;
+ m_rightThumbY = rightThumbY;
+ m_repeat = repeat;
+}
+
+CKey::CKey(uint32_t buttonCode, unsigned int held)
+{
+ Reset();
+ m_buttonCode = buttonCode;
+ m_held = held;
+}
+
+CKey::CKey(uint32_t keycode,
+ uint8_t vkey,
+ wchar_t unicode,
+ char ascii,
+ uint32_t modifiers,
+ uint32_t lockingModifiers,
+ unsigned int held)
+{
+ Reset();
+ if (vkey) // FIXME: This needs cleaning up - should we always use the unicode key where available?
+ m_buttonCode = vkey | KEY_VKEY;
+ else
+ m_buttonCode = KEY_UNICODE;
+ m_buttonCode |= modifiers;
+ m_keycode = keycode;
+ m_vkey = vkey;
+ m_unicode = unicode;
+ m_ascii = ascii;
+ m_modifiers = modifiers;
+ m_lockingModifiers = lockingModifiers;
+ m_held = held;
+}
+
+CKey::CKey(const CKey& key)
+{
+ *this = key;
+}
+
+void CKey::Reset()
+{
+ m_leftTrigger = 0;
+ m_rightTrigger = 0;
+ m_leftThumbX = 0.0f;
+ m_leftThumbY = 0.0f;
+ m_rightThumbX = 0.0f;
+ m_rightThumbY = 0.0f;
+ m_repeat = 0.0f;
+ m_fromService = false;
+ m_buttonCode = KEY_INVALID;
+ m_keycode = 0;
+ m_vkey = 0;
+ m_unicode = 0;
+ m_ascii = 0;
+ m_modifiers = 0;
+ m_lockingModifiers = 0;
+ m_held = 0;
+}
+
+CKey& CKey::operator=(const CKey& key)
+{
+ if (&key == this)
+ return *this;
+ m_leftTrigger = key.m_leftTrigger;
+ m_rightTrigger = key.m_rightTrigger;
+ m_leftThumbX = key.m_leftThumbX;
+ m_leftThumbY = key.m_leftThumbY;
+ m_rightThumbX = key.m_rightThumbX;
+ m_rightThumbY = key.m_rightThumbY;
+ m_repeat = key.m_repeat;
+ m_fromService = key.m_fromService;
+ m_buttonCode = key.m_buttonCode;
+ m_keycode = key.m_keycode;
+ m_vkey = key.m_vkey;
+ m_unicode = key.m_unicode;
+ m_ascii = key.m_ascii;
+ m_modifiers = key.m_modifiers;
+ m_lockingModifiers = key.m_lockingModifiers;
+ m_held = key.m_held;
+ return *this;
+}
+
+uint8_t CKey::GetLeftTrigger() const
+{
+ return m_leftTrigger;
+}
+
+uint8_t CKey::GetRightTrigger() const
+{
+ return m_rightTrigger;
+}
+
+float CKey::GetLeftThumbX() const
+{
+ return m_leftThumbX;
+}
+
+float CKey::GetLeftThumbY() const
+{
+ return m_leftThumbY;
+}
+
+float CKey::GetRightThumbX() const
+{
+ return m_rightThumbX;
+}
+
+float CKey::GetRightThumbY() const
+{
+ return m_rightThumbY;
+}
+
+bool CKey::FromKeyboard() const
+{
+ return (m_buttonCode >= KEY_VKEY && m_buttonCode != KEY_INVALID);
+}
+
+bool CKey::IsAnalogButton() const
+{
+ if ((GetButtonCode() > 261 && GetButtonCode() < 270) ||
+ (GetButtonCode() > 279 && GetButtonCode() < 284))
+ return true;
+
+ return false;
+}
+
+bool CKey::IsIRRemote() const
+{
+ if (GetButtonCode() < 256)
+ return true;
+ return false;
+}
+
+float CKey::GetRepeat() const
+{
+ return m_repeat;
+}
+
+void CKey::SetFromService(bool fromService)
+{
+ if (fromService && (m_buttonCode & KEY_VKEY))
+ m_unicode = m_buttonCode - KEY_VKEY;
+
+ m_fromService = fromService;
+}
diff --git a/xbmc/input/Key.h b/xbmc/input/Key.h
new file mode 100644
index 0000000..5e3e819
--- /dev/null
+++ b/xbmc/input/Key.h
@@ -0,0 +1,215 @@
+/*
+ * 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
+
+/*!
+ \file Key.h
+ \brief
+ */
+
+//! @todo Remove dependence on CAction
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+
+#include <stdint.h>
+#include <string>
+
+// Reserved 0 - 255
+// IRRemote.h
+// XINPUT_IR_REMOTE-*
+
+/*
+ * EventServer "gamepad" keys based on original Xbox controller
+ */
+// Analogue - don't change order
+#define KEY_BUTTON_A 256
+#define KEY_BUTTON_B 257
+#define KEY_BUTTON_X 258
+#define KEY_BUTTON_Y 259
+#define KEY_BUTTON_BLACK 260
+#define KEY_BUTTON_WHITE 261
+#define KEY_BUTTON_LEFT_TRIGGER 262
+#define KEY_BUTTON_RIGHT_TRIGGER 263
+
+#define KEY_BUTTON_LEFT_THUMB_STICK 264
+#define KEY_BUTTON_RIGHT_THUMB_STICK 265
+
+#define KEY_BUTTON_RIGHT_THUMB_STICK_UP 266 // right thumb stick directions
+#define KEY_BUTTON_RIGHT_THUMB_STICK_DOWN 267 // for defining different actions per direction
+#define KEY_BUTTON_RIGHT_THUMB_STICK_LEFT 268
+#define KEY_BUTTON_RIGHT_THUMB_STICK_RIGHT 269
+
+// Digital - don't change order
+#define KEY_BUTTON_DPAD_UP 270
+#define KEY_BUTTON_DPAD_DOWN 271
+#define KEY_BUTTON_DPAD_LEFT 272
+#define KEY_BUTTON_DPAD_RIGHT 273
+
+#define KEY_BUTTON_START 274
+#define KEY_BUTTON_BACK 275
+
+#define KEY_BUTTON_LEFT_THUMB_BUTTON 276
+#define KEY_BUTTON_RIGHT_THUMB_BUTTON 277
+
+#define KEY_BUTTON_LEFT_ANALOG_TRIGGER 278
+#define KEY_BUTTON_RIGHT_ANALOG_TRIGGER 279
+
+#define KEY_BUTTON_LEFT_THUMB_STICK_UP 280 // left thumb stick directions
+#define KEY_BUTTON_LEFT_THUMB_STICK_DOWN 281 // for defining different actions per direction
+#define KEY_BUTTON_LEFT_THUMB_STICK_LEFT 282
+#define KEY_BUTTON_LEFT_THUMB_STICK_RIGHT 283
+
+// 0xF000 -> 0xF200 is reserved for the keyboard; a keyboard press is either
+#define KEY_VKEY 0xF000 // a virtual key/functional key e.g. cursor left
+#define KEY_UNICODE \
+ 0xF200 // another printable character whose range is not included in this KEY code
+
+// 0xE000 -> 0xEFFF is reserved for mouse actions
+#define KEY_VMOUSE 0xEFFF
+
+#define KEY_MOUSE_START 0xE000
+#define KEY_MOUSE_CLICK 0xE000
+#define KEY_MOUSE_RIGHTCLICK 0xE001
+#define KEY_MOUSE_MIDDLECLICK 0xE002
+#define KEY_MOUSE_DOUBLE_CLICK 0xE010
+#define KEY_MOUSE_LONG_CLICK 0xE020
+#define KEY_MOUSE_WHEEL_UP 0xE101
+#define KEY_MOUSE_WHEEL_DOWN 0xE102
+#define KEY_MOUSE_MOVE 0xE103
+#define KEY_MOUSE_DRAG 0xE104
+#define KEY_MOUSE_DRAG_START 0xE105
+#define KEY_MOUSE_DRAG_END 0xE106
+#define KEY_MOUSE_RDRAG 0xE107
+#define KEY_MOUSE_RDRAG_START 0xE108
+#define KEY_MOUSE_RDRAG_END 0xE109
+#define KEY_MOUSE_NOOP 0xEFFF
+#define KEY_MOUSE_END 0xEFFF
+
+// 0xD000 -> 0xD0FF is reserved for WM_APPCOMMAND messages
+#define KEY_APPCOMMAND 0xD000
+
+#define KEY_INVALID 0xFFFF
+
+#define ICON_TYPE_NONE 101
+#define ICON_TYPE_PROGRAMS 102
+#define ICON_TYPE_MUSIC 103
+#define ICON_TYPE_PICTURES 104
+#define ICON_TYPE_VIDEOS 105
+#define ICON_TYPE_FILES 106
+#define ICON_TYPE_WEATHER 107
+#define ICON_TYPE_SETTINGS 109
+
+#ifndef SWIG
+
+/*!
+ \ingroup actionkeys, mouse
+ \brief Simple class for mouse events
+ */
+class CMouseEvent
+{
+public:
+ CMouseEvent(int actionID, int state = 0, float offsetX = 0, float offsetY = 0)
+ {
+ m_id = actionID;
+ m_state = state;
+ m_offsetX = offsetX;
+ m_offsetY = offsetY;
+ };
+
+ int m_id;
+ int m_state;
+ float m_offsetX;
+ float m_offsetY;
+};
+
+/*!
+ \ingroup actionkeys
+ \brief
+ */
+class CKey
+{
+public:
+ CKey(void);
+ CKey(uint32_t buttonCode,
+ uint8_t leftTrigger = 0,
+ uint8_t rightTrigger = 0,
+ float leftThumbX = 0.0f,
+ float leftThumbY = 0.0f,
+ float rightThumbX = 0.0f,
+ float rightThumbY = 0.0f,
+ float repeat = 0.0f);
+ CKey(uint32_t buttonCode, unsigned int held);
+ CKey(uint32_t keycode,
+ uint8_t vkey,
+ wchar_t unicode,
+ char ascii,
+ uint32_t modifiers,
+ uint32_t lockingModifiers,
+ unsigned int held);
+ CKey(const CKey& key);
+ void Reset();
+
+ virtual ~CKey(void);
+ CKey& operator=(const CKey& key);
+ uint8_t GetLeftTrigger() const;
+ uint8_t GetRightTrigger() const;
+ float GetLeftThumbX() const;
+ float GetLeftThumbY() const;
+ float GetRightThumbX() const;
+ float GetRightThumbY() const;
+ float GetRepeat() const;
+ bool FromKeyboard() const;
+ bool IsAnalogButton() const;
+ bool IsIRRemote() const;
+ void SetFromService(bool fromService);
+ bool GetFromService() const { return m_fromService; }
+
+ inline uint32_t GetButtonCode() const { return m_buttonCode; }
+ inline uint32_t GetKeycode() const { return m_keycode; } // XBMCKey enum in XBMC_keysym.h
+ inline uint8_t GetVKey() const { return m_vkey; }
+ inline wchar_t GetUnicode() const { return m_unicode; }
+ inline char GetAscii() const { return m_ascii; }
+ inline uint32_t GetModifiers() const { return m_modifiers; }
+ inline uint32_t GetLockingModifiers() const { return m_lockingModifiers; }
+ inline unsigned int GetHeld() const { return m_held; }
+
+ enum Modifier
+ {
+ MODIFIER_CTRL = 0x00010000,
+ MODIFIER_SHIFT = 0x00020000,
+ MODIFIER_ALT = 0x00040000,
+ MODIFIER_RALT = 0x00080000,
+ MODIFIER_SUPER = 0x00100000,
+ MODIFIER_META = 0X00200000,
+ MODIFIER_LONG = 0X01000000,
+ MODIFIER_NUMLOCK = 0X02000000,
+ MODIFIER_CAPSLOCK = 0X04000000,
+ MODIFIER_SCROLLLOCK = 0X08000000,
+ };
+
+private:
+ uint32_t m_buttonCode;
+ uint32_t m_keycode;
+ uint8_t m_vkey;
+ wchar_t m_unicode;
+ char m_ascii;
+ uint32_t m_modifiers;
+ uint32_t m_lockingModifiers;
+ unsigned int m_held;
+
+ uint8_t m_leftTrigger;
+ uint8_t m_rightTrigger;
+ float m_leftThumbX;
+ float m_leftThumbY;
+ float m_rightThumbX;
+ float m_rightThumbY;
+ float m_repeat; // time since last keypress
+ bool m_fromService;
+};
+#endif // undef SWIG
diff --git a/xbmc/input/KeyboardLayout.cpp b/xbmc/input/KeyboardLayout.cpp
new file mode 100644
index 0000000..4759627
--- /dev/null
+++ b/xbmc/input/KeyboardLayout.cpp
@@ -0,0 +1,165 @@
+/*
+ * 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 "KeyboardLayout.h"
+
+#include "InputCodingTableFactory.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <set>
+
+CKeyboardLayout::~CKeyboardLayout() = default;
+
+bool CKeyboardLayout::Load(const TiXmlElement* element)
+{
+ const char* language = element->Attribute("language");
+ if (language == NULL)
+ {
+ CLog::Log(LOGWARNING, "CKeyboardLayout: invalid \"language\" attribute");
+ return false;
+ }
+
+ m_language = language;
+ if (m_language.empty())
+ {
+ CLog::Log(LOGWARNING, "CKeyboardLayout: empty \"language\" attribute");
+ return false;
+ }
+
+ const char* layout = element->Attribute("layout");
+ if (layout == NULL)
+ {
+ CLog::Log(LOGWARNING, "CKeyboardLayout: invalid \"layout\" attribute");
+ return false;
+ }
+
+ m_layout = layout;
+ if (m_layout.empty())
+ {
+ CLog::Log(LOGWARNING, "CKeyboardLayout: empty \"layout\" attribute");
+ return false;
+ }
+
+ const TiXmlElement* keyboard = element->FirstChildElement("keyboard");
+ if (element->Attribute("codingtable"))
+ m_codingtable = IInputCodingTablePtr(
+ CInputCodingTableFactory::CreateCodingTable(element->Attribute("codingtable"), element));
+ else
+ m_codingtable = NULL;
+ while (keyboard != NULL)
+ {
+ // parse modifiers keys
+ std::set<unsigned int> modifierKeysSet;
+
+ const char* strModifiers = keyboard->Attribute("modifiers");
+ if (strModifiers != NULL)
+ {
+ std::string modifiers = strModifiers;
+ StringUtils::ToLower(modifiers);
+
+ std::vector<std::string> variants = StringUtils::Split(modifiers, ",");
+ for (const auto& itv : variants)
+ {
+ unsigned int iKeys = ModifierKeyNone;
+ std::vector<std::string> keys = StringUtils::Split(itv, "+");
+ for (const std::string& strKey : keys)
+ {
+ if (strKey == "shift")
+ iKeys |= ModifierKeyShift;
+ else if (strKey == "symbol")
+ iKeys |= ModifierKeySymbol;
+ }
+
+ modifierKeysSet.insert(iKeys);
+ }
+ }
+
+ // parse keyboard rows
+ const TiXmlNode* row = keyboard->FirstChild("row");
+ while (row != NULL)
+ {
+ if (!row->NoChildren())
+ {
+ std::string strRow = row->FirstChild()->ValueStr();
+ std::vector<std::string> chars = BreakCharacters(strRow);
+ if (!modifierKeysSet.empty())
+ {
+ for (const auto& it : modifierKeysSet)
+ m_keyboards[it].push_back(chars);
+ }
+ else
+ m_keyboards[ModifierKeyNone].push_back(chars);
+ }
+
+ row = row->NextSibling();
+ }
+
+ keyboard = keyboard->NextSiblingElement();
+ }
+
+ if (m_keyboards.empty())
+ {
+ CLog::Log(LOGWARNING, "CKeyboardLayout: no keyboard layout found");
+ return false;
+ }
+
+ return true;
+}
+
+std::string CKeyboardLayout::GetIdentifier() const
+{
+ return StringUtils::Format("{} {}", m_language, m_layout);
+}
+
+std::string CKeyboardLayout::GetName() const
+{
+ return StringUtils::Format(g_localizeStrings.Get(311), m_language, m_layout);
+}
+
+std::string CKeyboardLayout::GetCharAt(unsigned int row,
+ unsigned int column,
+ unsigned int modifiers) const
+{
+ Keyboards::const_iterator mod = m_keyboards.find(modifiers);
+ if (modifiers != ModifierKeyNone && mod != m_keyboards.end() && mod->second.empty())
+ {
+ // fallback to basic keyboard
+ mod = m_keyboards.find(ModifierKeyNone);
+ }
+
+ if (mod != m_keyboards.end())
+ {
+ if (row < mod->second.size() && column < mod->second[row].size())
+ {
+ std::string ch = mod->second[row][column];
+ if (ch != " ")
+ return ch;
+ }
+ }
+
+ return "";
+}
+
+std::vector<std::string> CKeyboardLayout::BreakCharacters(const std::string& chars)
+{
+ std::vector<std::string> result;
+ // break into utf8 characters
+ std::u32string chars32 = g_charsetConverter.utf8ToUtf32(chars);
+ for (const auto& it : chars32)
+ {
+ std::u32string char32(1, it);
+ result.push_back(g_charsetConverter.utf32ToUtf8(char32));
+ }
+
+ return result;
+}
diff --git a/xbmc/input/KeyboardLayout.h b/xbmc/input/KeyboardLayout.h
new file mode 100644
index 0000000..653b114
--- /dev/null
+++ b/xbmc/input/KeyboardLayout.h
@@ -0,0 +1,52 @@
+/*
+ * 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 "InputCodingTable.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+class TiXmlElement;
+
+class CKeyboardLayout
+{
+public:
+ CKeyboardLayout() = default;
+ virtual ~CKeyboardLayout();
+ IInputCodingTablePtr GetCodingTable() { return m_codingtable; }
+
+ bool Load(const TiXmlElement* element);
+
+ std::string GetIdentifier() const;
+ std::string GetName() const;
+ const std::string& GetLanguage() const { return m_language; }
+ const std::string& GetLayout() const { return m_layout; }
+
+ enum ModifierKey
+ {
+ ModifierKeyNone = 0x00,
+ ModifierKeyShift = 0x01,
+ ModifierKeySymbol = 0x02
+ };
+
+ std::string GetCharAt(unsigned int row, unsigned int column, unsigned int modifiers = 0) const;
+
+private:
+ static std::vector<std::string> BreakCharacters(const std::string& chars);
+
+ typedef std::vector<std::vector<std::string>> KeyboardRows;
+ typedef std::map<unsigned int, KeyboardRows> Keyboards;
+
+ std::string m_language;
+ std::string m_layout;
+ Keyboards m_keyboards;
+ IInputCodingTablePtr m_codingtable;
+};
diff --git a/xbmc/input/KeyboardLayoutManager.cpp b/xbmc/input/KeyboardLayoutManager.cpp
new file mode 100644
index 0000000..66ed817
--- /dev/null
+++ b/xbmc/input/KeyboardLayoutManager.cpp
@@ -0,0 +1,148 @@
+/*
+ * 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 "KeyboardLayoutManager.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "filesystem/Directory.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+#define KEYBOARD_LAYOUTS_PATH "special://xbmc/system/keyboardlayouts"
+
+CKeyboardLayoutManager::~CKeyboardLayoutManager()
+{
+ Unload();
+}
+
+bool CKeyboardLayoutManager::Load(const std::string& path /* = "" */)
+{
+ std::string layoutDirectory = path;
+ if (layoutDirectory.empty())
+ layoutDirectory = KEYBOARD_LAYOUTS_PATH;
+
+ if (!XFILE::CDirectory::Exists(layoutDirectory))
+ {
+ CLog::Log(LOGWARNING,
+ "CKeyboardLayoutManager: unable to load keyboard layouts from non-existing directory "
+ "\"{}\"",
+ layoutDirectory);
+ return false;
+ }
+
+ CFileItemList layouts;
+ if (!XFILE::CDirectory::GetDirectory(CURL(layoutDirectory), layouts, ".xml",
+ XFILE::DIR_FLAG_DEFAULTS) ||
+ layouts.IsEmpty())
+ {
+ CLog::Log(LOGWARNING, "CKeyboardLayoutManager: no keyboard layouts found in {}",
+ layoutDirectory);
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CKeyboardLayoutManager: loading keyboard layouts from {}...",
+ layoutDirectory);
+ size_t oldLayoutCount = m_layouts.size();
+ for (int i = 0; i < layouts.Size(); i++)
+ {
+ std::string layoutPath = layouts[i]->GetPath();
+ if (layoutPath.empty())
+ continue;
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(layoutPath))
+ {
+ CLog::Log(LOGWARNING, "CKeyboardLayoutManager: unable to open {}", layoutPath);
+ continue;
+ }
+
+ const TiXmlElement* rootElement = xmlDoc.RootElement();
+ if (rootElement == NULL)
+ {
+ CLog::Log(LOGWARNING, "CKeyboardLayoutManager: missing or invalid XML root element in {}",
+ layoutPath);
+ continue;
+ }
+
+ if (rootElement->ValueStr() != "keyboardlayouts")
+ {
+ CLog::Log(LOGWARNING, "CKeyboardLayoutManager: unexpected XML root element \"{}\" in {}",
+ rootElement->Value(), layoutPath);
+ continue;
+ }
+
+ const TiXmlElement* layoutElement = rootElement->FirstChildElement("layout");
+ while (layoutElement != NULL)
+ {
+ CKeyboardLayout layout;
+ if (!layout.Load(layoutElement))
+ CLog::Log(LOGWARNING, "CKeyboardLayoutManager: failed to load {}", layoutPath);
+ else if (m_layouts.find(layout.GetIdentifier()) != m_layouts.end())
+ CLog::Log(LOGWARNING,
+ "CKeyboardLayoutManager: duplicate layout with identifier \"{}\" in {}",
+ layout.GetIdentifier(), layoutPath);
+ else
+ {
+ CLog::Log(LOGDEBUG, "CKeyboardLayoutManager: keyboard layout \"{}\" successfully loaded",
+ layout.GetIdentifier());
+ m_layouts.insert(std::make_pair(layout.GetIdentifier(), layout));
+ }
+
+ layoutElement = layoutElement->NextSiblingElement();
+ }
+ }
+
+ return m_layouts.size() > oldLayoutCount;
+}
+
+void CKeyboardLayoutManager::Unload()
+{
+ m_layouts.clear();
+}
+
+bool CKeyboardLayoutManager::GetLayout(const std::string& name, CKeyboardLayout& layout) const
+{
+ if (name.empty())
+ return false;
+
+ KeyboardLayouts::const_iterator it = m_layouts.find(name);
+ if (it == m_layouts.end())
+ return false;
+
+ layout = it->second;
+ return true;
+}
+
+namespace
+{
+inline bool LayoutSort(const StringSettingOption& i, const StringSettingOption& j)
+{
+ return (i.value < j.value);
+}
+} // namespace
+
+void CKeyboardLayoutManager::SettingOptionsKeyboardLayoutsFiller(
+ const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ for (const auto& it : CServiceBroker::GetKeyboardLayoutManager()->m_layouts)
+ {
+ std::string name = it.second.GetName();
+ list.emplace_back(name, name);
+ }
+
+ std::sort(list.begin(), list.end(), LayoutSort);
+}
diff --git a/xbmc/input/KeyboardLayoutManager.h b/xbmc/input/KeyboardLayoutManager.h
new file mode 100644
index 0000000..49c6473
--- /dev/null
+++ b/xbmc/input/KeyboardLayoutManager.h
@@ -0,0 +1,45 @@
+/*
+ * 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 "input/KeyboardLayout.h"
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CSetting;
+struct StringSettingOption;
+
+typedef std::map<std::string, CKeyboardLayout> KeyboardLayouts;
+
+class CKeyboardLayoutManager
+{
+public:
+ CKeyboardLayoutManager() = default;
+ virtual ~CKeyboardLayoutManager();
+
+ bool Load(const std::string& path = "");
+ void Unload();
+
+ const KeyboardLayouts& GetLayouts() const { return m_layouts; }
+ bool GetLayout(const std::string& name, CKeyboardLayout& layout) const;
+
+ static void SettingOptionsKeyboardLayoutsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+
+private:
+ CKeyboardLayoutManager(const CKeyboardLayoutManager&) = delete;
+ CKeyboardLayoutManager const& operator=(CKeyboardLayoutManager const&) = delete;
+
+ KeyboardLayouts m_layouts;
+};
diff --git a/xbmc/input/KeyboardStat.cpp b/xbmc/input/KeyboardStat.cpp
new file mode 100644
index 0000000..ac36fe4
--- /dev/null
+++ b/xbmc/input/KeyboardStat.cpp
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2007-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.
+ */
+
+// C++ Implementation: CKeyboard
+
+// Comment OUT, if not really debugging!!!:
+//#define DEBUG_KEYBOARD_GETCHAR
+
+#include "KeyboardStat.h"
+
+#include "ServiceBroker.h"
+#include "input/InputTypes.h"
+#include "input/XBMC_keytable.h"
+#include "input/XBMC_vkeys.h"
+#include "peripherals/Peripherals.h"
+#include "peripherals/devices/PeripheralHID.h"
+#include "utils/log.h"
+#include "windowing/XBMC_events.h"
+
+using namespace KODI;
+using namespace INPUT;
+
+bool operator==(const XBMC_keysym& lhs, const XBMC_keysym& rhs)
+{
+ return lhs.mod == rhs.mod && lhs.scancode == rhs.scancode && lhs.sym == rhs.sym &&
+ lhs.unicode == rhs.unicode;
+}
+
+CKeyboardStat::CKeyboardStat()
+{
+ memset(&m_lastKeysym, 0, sizeof(m_lastKeysym));
+ m_lastKeyTime = {};
+}
+
+void CKeyboardStat::Initialize()
+{
+}
+
+bool CKeyboardStat::LookupSymAndUnicodePeripherals(XBMC_keysym& keysym, uint8_t* key, char* unicode)
+{
+ using namespace PERIPHERALS;
+
+ PeripheralVector hidDevices;
+ if (CServiceBroker::GetPeripherals().GetPeripheralsWithFeature(hidDevices, FEATURE_HID))
+ {
+ for (auto& peripheral : hidDevices)
+ {
+ std::shared_ptr<CPeripheralHID> hidDevice =
+ std::static_pointer_cast<CPeripheralHID>(peripheral);
+ if (hidDevice->LookupSymAndUnicode(keysym, key, unicode))
+ return true;
+ }
+ }
+ return false;
+}
+
+CKey CKeyboardStat::TranslateKey(XBMC_keysym& keysym) const
+{
+ uint32_t keycode;
+ uint8_t vkey;
+ wchar_t unicode;
+ char ascii;
+ uint32_t modifiers;
+ uint32_t lockingModifiers;
+ std::chrono::milliseconds held;
+ XBMCKEYTABLE keytable;
+
+ modifiers = 0;
+ if (keysym.mod & XBMCKMOD_CTRL)
+ modifiers |= CKey::MODIFIER_CTRL;
+ if (keysym.mod & XBMCKMOD_SHIFT)
+ modifiers |= CKey::MODIFIER_SHIFT;
+ if (keysym.mod & XBMCKMOD_ALT)
+ modifiers |= CKey::MODIFIER_ALT;
+ if (keysym.mod & XBMCKMOD_SUPER)
+ modifiers |= CKey::MODIFIER_SUPER;
+ if (keysym.mod & XBMCKMOD_META)
+ modifiers |= CKey::MODIFIER_META;
+
+ lockingModifiers = 0;
+ if (keysym.mod & XBMCKMOD_NUM)
+ lockingModifiers |= CKey::MODIFIER_NUMLOCK;
+ if (keysym.mod & XBMCKMOD_CAPS)
+ lockingModifiers |= CKey::MODIFIER_CAPSLOCK;
+ if (keysym.mod & XBMCKMOD_MODE)
+ lockingModifiers |= CKey::MODIFIER_SCROLLLOCK;
+
+ CLog::Log(LOGDEBUG,
+ "Keyboard: scancode: {:#02x}, sym: {:#04x}, unicode: {:#04x}, modifier: 0x{:x}",
+ keysym.scancode, keysym.sym, keysym.unicode, keysym.mod);
+
+ // The keysym.unicode is usually valid, even if it is zero. A zero
+ // unicode just means this is a non-printing keypress. The ascii and
+ // vkey will be set below.
+ keycode = keysym.sym;
+ unicode = keysym.unicode;
+ ascii = 0;
+ vkey = 0;
+ held = std::chrono::milliseconds(0);
+
+ // Start by check whether any of the HID peripherals wants to translate this keypress
+ if (LookupSymAndUnicodePeripherals(keysym, &vkey, &ascii))
+ {
+ CLog::Log(LOGDEBUG, "{} - keypress translated by a HID peripheral", __FUNCTION__);
+ }
+
+ // Continue by trying to match both the sym and unicode. This will identify
+ // the majority of keypresses
+ else if (KeyTableLookupSymAndUnicode(keysym.sym, keysym.unicode, &keytable))
+ {
+ vkey = keytable.vkey;
+ ascii = keytable.ascii;
+ }
+
+ // If we failed to match the sym and unicode try just the unicode. This
+ // will match keys like \ that are on different keys on regional keyboards.
+ else if (KeyTableLookupUnicode(keysym.unicode, &keytable))
+ {
+ if (keycode == 0)
+ keycode = keytable.sym;
+ vkey = keytable.vkey;
+ ascii = keytable.ascii;
+ }
+
+ // If there is still no match try the sym
+ else if (KeyTableLookupSym(keysym.sym, &keytable))
+ {
+ vkey = keytable.vkey;
+
+ // Occasionally we get non-printing keys that have a non-zero value in
+ // the keysym.unicode. Check for this here and replace any rogue unicode
+ // values.
+ if (keytable.unicode == 0 && unicode != 0)
+ unicode = 0;
+ else if (keysym.unicode > 32 && keysym.unicode < 128)
+ ascii = unicode & 0x7f;
+ }
+
+ // The keysym.sym is unknown ...
+ else
+ {
+ if (!vkey && !ascii)
+ {
+ if (keysym.mod & XBMCKMOD_LSHIFT)
+ vkey = 0xa0;
+ else if (keysym.mod & XBMCKMOD_RSHIFT)
+ vkey = 0xa1;
+ else if (keysym.mod & XBMCKMOD_LALT)
+ vkey = 0xa4;
+ else if (keysym.mod & XBMCKMOD_RALT)
+ vkey = 0xa5;
+ else if (keysym.mod & XBMCKMOD_LCTRL)
+ vkey = 0xa2;
+ else if (keysym.mod & XBMCKMOD_RCTRL)
+ vkey = 0xa3;
+ else if (keysym.unicode > 32 && keysym.unicode < 128)
+ // only TRUE ASCII! (Otherwise XBMC crashes! No unicode not even latin 1!)
+ ascii = (char)(keysym.unicode & 0xff);
+ }
+ }
+
+ if (keysym == m_lastKeysym)
+ {
+ auto now = std::chrono::steady_clock::now();
+
+ held = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastKeyTime);
+ if (held.count() > HOLD_TRESHOLD)
+ modifiers |= CKey::MODIFIER_LONG;
+ }
+
+ // For all shift-X keys except shift-A to shift-Z and shift-F1 to shift-F24 the
+ // shift modifier is ignored. This so that, for example, the * keypress (shift-8)
+ // is seen as <asterisk> not <asterisk mod="shift">.
+ // The A-Z keys are exempted because shift-A-Z is used for navigation in lists.
+ // The function keys are exempted because function keys have no shifted value and
+ // the Nyxboard remote uses keys like Shift-F3 for some buttons.
+ if (modifiers == CKey::MODIFIER_SHIFT)
+ if ((unicode < 'A' || unicode > 'Z') && (unicode < 'a' || unicode > 'z') &&
+ (vkey < XBMCVK_F1 || vkey > XBMCVK_F24))
+ modifiers = 0;
+
+ // Create and return a CKey
+
+ CKey key(keycode, vkey, unicode, ascii, modifiers, lockingModifiers, held.count());
+
+ return key;
+}
+
+void CKeyboardStat::ProcessKeyDown(XBMC_keysym& keysym)
+{
+ if (!(m_lastKeysym == keysym))
+ {
+ m_lastKeysym = keysym;
+ m_lastKeyTime = std::chrono::steady_clock::now();
+ }
+}
+
+void CKeyboardStat::ProcessKeyUp(void)
+{
+ memset(&m_lastKeysym, 0, sizeof(m_lastKeysym));
+ m_lastKeyTime = {};
+}
+
+// Return the key name given a key ID
+// Used to make the debug log more intelligible
+// The KeyID includes the flags for ctrl, alt etc
+
+std::string CKeyboardStat::GetKeyName(int KeyID)
+{
+ int keyid;
+ std::string keyname;
+ XBMCKEYTABLE keytable;
+
+ keyname.clear();
+
+ // Get modifiers
+
+ if (KeyID & CKey::MODIFIER_CTRL)
+ keyname.append("ctrl-");
+ if (KeyID & CKey::MODIFIER_SHIFT)
+ keyname.append("shift-");
+ if (KeyID & CKey::MODIFIER_ALT)
+ keyname.append("alt-");
+ if (KeyID & CKey::MODIFIER_SUPER)
+ keyname.append("win-");
+ if (KeyID & CKey::MODIFIER_META)
+ keyname.append("meta-");
+ if (KeyID & CKey::MODIFIER_LONG)
+ keyname.append("long-");
+
+ // Now get the key name
+
+ keyid = KeyID & 0xFF;
+ bool VKeyFound = KeyTableLookupVKeyName(keyid, &keytable);
+ if (VKeyFound)
+ keyname.append(keytable.keyname);
+ else
+ keyname += std::to_string(keyid);
+
+ // in case this might be an universalremote keyid
+ // we also print the possible corresponding obc code
+ // so users can easily find it in their universalremote
+ // map xml
+ if (VKeyFound || keyid > 255)
+ keyname += StringUtils::Format(" ({:#02x})", KeyID);
+ else // obc keys are 255 -rawid
+ keyname += StringUtils::Format(" ({:#02x}, obc{})", KeyID, 255 - KeyID);
+
+ return keyname;
+}
diff --git a/xbmc/input/KeyboardStat.h b/xbmc/input/KeyboardStat.h
new file mode 100644
index 0000000..77d59a9
--- /dev/null
+++ b/xbmc/input/KeyboardStat.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2007-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
+
+//
+// C++ Interface: CKeyboard
+//
+// Description: Adds features like international keyboard layout mapping on top of the
+// platform specific low level keyboard classes.
+// Here it must be done only once. Within the other mentioned classes it would have to be done
+// several times.
+//
+// Keyboards always deliver printable characters, logical keys for functional behaviour, modifiers
+// ... alongside Based on the same hardware with the same scancodes (also alongside) but delivered
+// with different labels to customers the software must solve the mapping to the real labels. This
+// is done here. The mapping must be specified by an xml configuration that should be able to access
+// everything available, but this allows for double/redundant or ambiguous mapping definition, e.g.
+// ASCII/unicode could be derived from scancodes, virtual keys, modifiers and/or other
+// ASCII/unicode.
+
+#include "input/Key.h"
+#include "input/XBMC_keyboard.h"
+
+#include <chrono>
+#include <string>
+
+class CKeyboardStat
+{
+public:
+ CKeyboardStat();
+ ~CKeyboardStat() = default;
+
+ void Initialize();
+
+ CKey TranslateKey(XBMC_keysym& keysym) const;
+
+ void ProcessKeyDown(XBMC_keysym& keysym);
+ void ProcessKeyUp(void);
+
+ std::string GetKeyName(int KeyID);
+
+private:
+ static bool LookupSymAndUnicodePeripherals(XBMC_keysym& keysym, uint8_t* key, char* unicode);
+
+ XBMC_keysym m_lastKeysym;
+ std::chrono::time_point<std::chrono::steady_clock> m_lastKeyTime;
+};
diff --git a/xbmc/input/KeyboardTranslator.cpp b/xbmc/input/KeyboardTranslator.cpp
new file mode 100644
index 0000000..1b50151
--- /dev/null
+++ b/xbmc/input/KeyboardTranslator.cpp
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017-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 "KeyboardTranslator.h"
+
+#include "Key.h"
+#include "XBMC_keytable.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <string>
+#include <vector>
+
+uint32_t CKeyboardTranslator::TranslateButton(const TiXmlElement* pButton)
+{
+ uint32_t button_id = 0;
+ const char* szButton = pButton->Value();
+
+ if (szButton == nullptr)
+ return 0;
+
+ const std::string strKey = szButton;
+ if (strKey == "key")
+ {
+ std::string strID;
+ if (pButton->QueryValueAttribute("id", &strID) == TIXML_SUCCESS)
+ {
+ const char* str = strID.c_str();
+ char* endptr;
+ long int id = strtol(str, &endptr, 0);
+ if (endptr - str != (int)strlen(str) || id <= 0 || id > 0x00FFFFFF)
+ CLog::Log(LOGDEBUG, "{} - invalid key id {}", __FUNCTION__, strID);
+ else
+ button_id = (uint32_t)id;
+ }
+ else
+ CLog::Log(LOGERROR, "Keyboard Translator: `key' button has no id");
+ }
+ else
+ button_id = TranslateString(szButton);
+
+ // Process the ctrl/shift/alt modifiers
+ std::string strMod;
+ if (pButton->QueryValueAttribute("mod", &strMod) == TIXML_SUCCESS)
+ {
+ StringUtils::ToLower(strMod);
+
+ std::vector<std::string> modArray = StringUtils::Split(strMod, ",");
+ for (auto substr : modArray)
+ {
+ StringUtils::Trim(substr);
+
+ if (substr == "ctrl" || substr == "control")
+ button_id |= CKey::MODIFIER_CTRL;
+ else if (substr == "shift")
+ button_id |= CKey::MODIFIER_SHIFT;
+ else if (substr == "alt")
+ button_id |= CKey::MODIFIER_ALT;
+ else if (substr == "super" || substr == "win")
+ button_id |= CKey::MODIFIER_SUPER;
+ else if (substr == "meta" || substr == "cmd")
+ button_id |= CKey::MODIFIER_META;
+ else if (substr == "longpress")
+ button_id |= CKey::MODIFIER_LONG;
+ else
+ CLog::Log(LOGERROR, "Keyboard Translator: Unknown key modifier {} in {}", substr, strMod);
+ }
+ }
+
+ return button_id;
+}
+
+uint32_t CKeyboardTranslator::TranslateString(const std::string& szButton)
+{
+ uint32_t buttonCode = 0;
+ XBMCKEYTABLE keytable;
+
+ // Look up the key name
+ if (KeyTableLookupName(szButton, &keytable))
+ {
+ buttonCode = keytable.vkey;
+ }
+ else
+ {
+ // The lookup failed i.e. the key name wasn't found
+ CLog::Log(LOGERROR, "Keyboard Translator: Can't find button {}", szButton);
+ }
+
+ buttonCode |= KEY_VKEY;
+
+ return buttonCode;
+}
diff --git a/xbmc/input/KeyboardTranslator.h b/xbmc/input/KeyboardTranslator.h
new file mode 100644
index 0000000..7432037
--- /dev/null
+++ b/xbmc/input/KeyboardTranslator.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2017-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 <stdint.h>
+#include <string>
+
+class TiXmlElement;
+
+class CKeyboardTranslator
+{
+public:
+ static uint32_t TranslateButton(const TiXmlElement* pButton);
+ static uint32_t TranslateString(const std::string& szButton);
+};
diff --git a/xbmc/input/Keymap.cpp b/xbmc/input/Keymap.cpp
new file mode 100644
index 0000000..a2b431d
--- /dev/null
+++ b/xbmc/input/Keymap.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017-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 "Keymap.h"
+
+#include "IKeymapEnvironment.h"
+
+using namespace KODI;
+
+CKeymap::CKeymap(std::shared_ptr<const IWindowKeymap> keymap, const IKeymapEnvironment* environment)
+ : m_keymap(std::move(keymap)), m_environment(environment)
+{
+}
+
+std::string CKeymap::ControllerID() const
+{
+ return m_keymap->ControllerID();
+}
+
+const JOYSTICK::KeymapActionGroup& CKeymap::GetActions(const std::string& keyName) const
+{
+ const int windowId = m_environment->GetWindowID();
+ const auto& actions = m_keymap->GetActions(windowId, keyName);
+ if (!actions.actions.empty())
+ return actions;
+
+ const int fallbackWindowId = m_environment->GetFallthrough(windowId);
+ if (fallbackWindowId >= 0)
+ {
+ const auto& fallbackActions = m_keymap->GetActions(fallbackWindowId, keyName);
+ if (!fallbackActions.actions.empty())
+ return fallbackActions;
+ }
+
+ if (m_environment->UseGlobalFallthrough())
+ {
+ const auto& globalActions = m_keymap->GetActions(-1, keyName);
+ if (!globalActions.actions.empty())
+ return globalActions;
+ }
+
+ static const JOYSTICK::KeymapActionGroup empty{};
+ return empty;
+}
diff --git a/xbmc/input/Keymap.h b/xbmc/input/Keymap.h
new file mode 100644
index 0000000..822ed67
--- /dev/null
+++ b/xbmc/input/Keymap.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017-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 "IKeymap.h"
+
+#include <memory>
+
+class IKeymapEnvironment;
+
+class CKeymap : public IKeymap
+{
+public:
+ CKeymap(std::shared_ptr<const IWindowKeymap> keymap, const IKeymapEnvironment* environment);
+
+ // implementation of IKeymap
+ std::string ControllerID() const override;
+ const IKeymapEnvironment* Environment() const override { return m_environment; }
+ const KODI::JOYSTICK::KeymapActionGroup& GetActions(const std::string& keyName) const override;
+
+private:
+ // Construction parameters
+ const std::shared_ptr<const IWindowKeymap> m_keymap;
+ const IKeymapEnvironment* const m_environment;
+};
diff --git a/xbmc/input/KeymapEnvironment.cpp b/xbmc/input/KeymapEnvironment.cpp
new file mode 100644
index 0000000..dbfe9f7
--- /dev/null
+++ b/xbmc/input/KeymapEnvironment.cpp
@@ -0,0 +1,16 @@
+/*
+ * Copyright (C) 2017-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 "KeymapEnvironment.h"
+
+#include "WindowTranslator.h"
+
+int CKeymapEnvironment::GetFallthrough(int windowId) const
+{
+ return CWindowTranslator::GetFallbackWindow(windowId);
+}
diff --git a/xbmc/input/KeymapEnvironment.h b/xbmc/input/KeymapEnvironment.h
new file mode 100644
index 0000000..64ee67a
--- /dev/null
+++ b/xbmc/input/KeymapEnvironment.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017-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 "IKeymapEnvironment.h"
+
+class CKeymapEnvironment : public IKeymapEnvironment
+{
+public:
+ ~CKeymapEnvironment() override = default;
+
+ // implementation of IKeymapEnvironment
+ int GetWindowID() const override { return m_windowId; }
+ void SetWindowID(int windowId) override { m_windowId = windowId; }
+ int GetFallthrough(int windowId) const override;
+ bool UseGlobalFallthrough() const override { return true; }
+ bool UseEasterEgg() const override { return true; }
+
+private:
+ int m_windowId = -1;
+};
diff --git a/xbmc/input/TouchTranslator.cpp b/xbmc/input/TouchTranslator.cpp
new file mode 100644
index 0000000..e1eb843
--- /dev/null
+++ b/xbmc/input/TouchTranslator.cpp
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2017-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 "TouchTranslator.h"
+
+#include "WindowTranslator.h" //! @todo
+#include "input/actions/ActionIDs.h"
+#include "input/actions/ActionTranslator.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <map>
+
+using ActionName = std::string;
+using TouchCommandID = unsigned int;
+
+#define TOUCH_COMMAND_NONE 0
+
+static const std::map<ActionName, TouchCommandID> TouchCommands = {
+ {"tap", ACTION_TOUCH_TAP},
+ {"longpress", ACTION_TOUCH_LONGPRESS},
+ {"pan", ACTION_GESTURE_PAN},
+ {"zoom", ACTION_GESTURE_ZOOM},
+ {"rotate", ACTION_GESTURE_ROTATE},
+ {"swipeleft", ACTION_GESTURE_SWIPE_LEFT},
+ {"swiperight", ACTION_GESTURE_SWIPE_RIGHT},
+ {"swipeup", ACTION_GESTURE_SWIPE_UP},
+ {"swipedown", ACTION_GESTURE_SWIPE_DOWN}};
+
+void CTouchTranslator::MapActions(int windowID, const TiXmlNode* pTouch)
+{
+ if (pTouch == nullptr)
+ return;
+
+ TouchActionMap map;
+
+ // Check if there already is a touch map for the window ID
+ auto it = m_touchMap.find(windowID);
+ if (it != m_touchMap.end())
+ {
+ // Get the existing touch map and remove it from the window mapping as it
+ // will be inserted later on
+ map = std::move(it->second);
+ m_touchMap.erase(it);
+ }
+
+ const TiXmlElement* pTouchElem = pTouch->ToElement();
+ if (pTouchElem == nullptr)
+ return;
+
+ const TiXmlElement* pButton = pTouchElem->FirstChildElement();
+ while (pButton != nullptr)
+ {
+ CTouchAction action;
+ unsigned int touchActionKey = TranslateTouchCommand(pButton, action);
+ if (touchActionKey != ACTION_NONE)
+ {
+ // check if there already is a mapping for the parsed action
+ // and remove it if necessary
+ TouchActionMap::iterator actionIt = map.find(touchActionKey);
+ if (actionIt != map.end())
+ map.erase(actionIt);
+
+ map.insert(std::make_pair(touchActionKey, std::move(action)));
+ }
+
+ pButton = pButton->NextSiblingElement();
+ }
+
+ // add the modified touch map with the window ID
+ if (!map.empty())
+ m_touchMap.insert(std::make_pair(windowID, std::move(map)));
+}
+
+void CTouchTranslator::Clear()
+{
+ m_touchMap.clear();
+}
+
+bool CTouchTranslator::TranslateTouchAction(
+ int window, int touchAction, int touchPointers, int& action, std::string& actionString)
+{
+ if (touchAction < 0)
+ return false;
+
+ unsigned int actionId = ACTION_NONE;
+
+ // handle virtual windows
+ window = CWindowTranslator::GetVirtualWindow(window);
+
+ if (!TranslateAction(window, touchAction, touchPointers, actionId, actionString))
+ {
+ // if it's invalid, try to get it from fallback windows or the global map (window == -1)
+ while (actionId == ACTION_NONE && window > -1)
+ {
+ window = CWindowTranslator::GetFallbackWindow(window);
+ TranslateAction(window, touchAction, touchPointers, actionId, actionString);
+ }
+ }
+
+ action = actionId;
+ return actionId != ACTION_NONE;
+}
+
+bool CTouchTranslator::TranslateAction(int window,
+ unsigned int touchCommand,
+ int touchPointers,
+ unsigned int& actionId,
+ std::string& actionString)
+{
+ unsigned int touchActionKey = GetTouchActionKey(touchCommand, touchPointers);
+
+ actionId = GetActionID(window, touchActionKey, actionString);
+
+ return actionId != ACTION_NONE;
+}
+
+unsigned int CTouchTranslator::GetActionID(WindowID window,
+ TouchActionKey touchActionKey,
+ std::string& actionString)
+{
+ auto windowIt = m_touchMap.find(window);
+ if (windowIt == m_touchMap.end())
+ return ACTION_NONE;
+
+ auto touchIt = windowIt->second.find(touchActionKey);
+ if (touchIt == windowIt->second.end())
+ return ACTION_NONE;
+
+ actionString = touchIt->second.strAction;
+ return touchIt->second.actionId;
+}
+
+unsigned int CTouchTranslator::TranslateTouchCommand(const TiXmlElement* pButton,
+ CTouchAction& action)
+{
+ const char* szButton = pButton->Value();
+ if (szButton == nullptr || pButton->FirstChild() == nullptr)
+ return ACTION_NONE;
+
+ const char* szAction = pButton->FirstChild()->Value();
+ if (szAction == nullptr)
+ return ACTION_NONE;
+
+ std::string strTouchCommand = szButton;
+ StringUtils::ToLower(strTouchCommand);
+
+ // Handle direction
+ const char* attrVal = pButton->Attribute("direction");
+ if (attrVal != nullptr)
+ strTouchCommand += attrVal;
+
+ // Lookup command
+ unsigned int touchCommandId = TOUCH_COMMAND_NONE;
+ auto it = TouchCommands.find(strTouchCommand);
+ if (it != TouchCommands.end())
+ touchCommandId = it->second;
+
+ if (touchCommandId == TOUCH_COMMAND_NONE)
+ {
+ CLog::Log(LOGERROR, "{}: Can't find touch command {}", __FUNCTION__, szButton);
+ return ACTION_NONE;
+ }
+
+ // Handle pointers
+ int pointers = 1;
+ attrVal = pButton->Attribute("pointers");
+ if (attrVal != nullptr)
+ pointers = (int)strtol(attrVal, nullptr, 0);
+
+ unsigned int touchActionKey = GetTouchActionKey(touchCommandId, pointers);
+
+ action.strAction = szAction;
+ if (!CActionTranslator::TranslateString(action.strAction, action.actionId) ||
+ action.actionId == ACTION_NONE)
+ return ACTION_NONE;
+
+ return touchActionKey;
+}
+
+unsigned int CTouchTranslator::GetTouchActionKey(unsigned int touchCommandId, int touchPointers)
+{
+ if (touchPointers <= 0)
+ touchPointers = 1;
+
+ return touchCommandId + touchPointers - 1;
+}
diff --git a/xbmc/input/TouchTranslator.h b/xbmc/input/TouchTranslator.h
new file mode 100644
index 0000000..989857e
--- /dev/null
+++ b/xbmc/input/TouchTranslator.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017-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 "IButtonMapper.h"
+
+#include <map>
+#include <string>
+
+class TiXmlElement;
+
+class CTouchTranslator : public IButtonMapper
+{
+public:
+ CTouchTranslator() = default;
+
+ // implementation of IButtonMapper
+ void MapActions(int windowID, const TiXmlNode* bDevice) override;
+ void Clear() override;
+
+ bool TranslateTouchAction(
+ int window, int touchAction, int touchPointers, int& action, std::string& actionString);
+
+private:
+ bool TranslateAction(int window,
+ unsigned int touchCommand,
+ int touchPointers,
+ unsigned int& actionId,
+ std::string& actionString);
+
+ struct CTouchAction
+ {
+ unsigned int actionId;
+ std::string strAction; // Needed for "ActivateWindow()" type actions
+ };
+
+ using TouchActionKey = unsigned int;
+ using TouchActionMap = std::map<TouchActionKey, CTouchAction>;
+
+ using WindowID = int;
+ using TouchMap = std::map<WindowID, TouchActionMap>;
+
+ unsigned int GetActionID(WindowID window,
+ TouchActionKey touchActionKey,
+ std::string& actionString);
+
+ static unsigned int TranslateTouchCommand(const TiXmlElement* pButton, CTouchAction& action);
+
+ static unsigned int GetTouchActionKey(unsigned int touchCommandId, int touchPointers);
+
+ TouchMap m_touchMap;
+};
diff --git a/xbmc/input/WindowKeymap.cpp b/xbmc/input/WindowKeymap.cpp
new file mode 100644
index 0000000..4fd2ae0
--- /dev/null
+++ b/xbmc/input/WindowKeymap.cpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017-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 "WindowKeymap.h"
+
+#include "WindowTranslator.h"
+
+using namespace KODI;
+
+CWindowKeymap::CWindowKeymap(const std::string& controllerId) : m_controllerId(controllerId)
+{
+}
+
+void CWindowKeymap::MapAction(int windowId,
+ const std::string& keyName,
+ JOYSTICK::KeymapAction action)
+{
+ auto& actionGroup = m_windowKeymap[windowId][keyName];
+
+ actionGroup.windowId = windowId;
+ auto it = actionGroup.actions.begin();
+ while (it != actionGroup.actions.end())
+ {
+ if (it->holdTimeMs == action.holdTimeMs && it->hotkeys == action.hotkeys)
+ it = actionGroup.actions.erase(it);
+ else
+ it++;
+ }
+ actionGroup.actions.insert(std::move(action));
+}
+
+const JOYSTICK::KeymapActionGroup& CWindowKeymap::GetActions(int windowId,
+ const std::string& keyName) const
+{
+ // handle virtual windows
+ windowId = CWindowTranslator::GetVirtualWindow(windowId);
+
+ auto it = m_windowKeymap.find(windowId);
+ if (it != m_windowKeymap.end())
+ {
+ auto& keymap = it->second;
+ auto it2 = keymap.find(keyName);
+ if (it2 != keymap.end())
+ return it2->second;
+ }
+
+ static const JOYSTICK::KeymapActionGroup empty{};
+ return empty;
+}
diff --git a/xbmc/input/WindowKeymap.h b/xbmc/input/WindowKeymap.h
new file mode 100644
index 0000000..f6c0612
--- /dev/null
+++ b/xbmc/input/WindowKeymap.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017-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 "IKeymap.h"
+#include "input/joysticks/JoystickTypes.h"
+
+#include <map>
+#include <string>
+
+class CWindowKeymap : public IWindowKeymap
+{
+public:
+ explicit CWindowKeymap(const std::string& controllerId);
+
+ // implementation of IWindowKeymap
+ std::string ControllerID() const override { return m_controllerId; }
+ void MapAction(int windowId,
+ const std::string& keyName,
+ KODI::JOYSTICK::KeymapAction action) override;
+ const KODI::JOYSTICK::KeymapActionGroup& GetActions(int windowId,
+ const std::string& keyName) const override;
+
+private:
+ // Construction parameter
+ const std::string m_controllerId;
+
+ using KeyName = std::string;
+ using Keymap = std::map<KeyName, KODI::JOYSTICK::KeymapActionGroup>;
+
+ using WindowID = int;
+ using WindowMap = std::map<WindowID, Keymap>;
+
+ WindowMap m_windowKeymap;
+};
diff --git a/xbmc/input/WindowTranslator.cpp b/xbmc/input/WindowTranslator.cpp
new file mode 100644
index 0000000..cceb3f0
--- /dev/null
+++ b/xbmc/input/WindowTranslator.cpp
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2017-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 "WindowTranslator.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "guilib/WindowIDs.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cstring>
+#include <stdlib.h>
+
+const CWindowTranslator::WindowMapByName CWindowTranslator::WindowMappingByName = {
+ {"home", WINDOW_HOME},
+ {"programs", WINDOW_PROGRAMS},
+ {"pictures", WINDOW_PICTURES},
+ {"filemanager", WINDOW_FILES},
+ {"settings", WINDOW_SETTINGS_MENU},
+ {"music", WINDOW_MUSIC_NAV},
+ {"videos", WINDOW_VIDEO_NAV},
+ {"tvchannels", WINDOW_TV_CHANNELS},
+ {"tvrecordings", WINDOW_TV_RECORDINGS},
+ {"tvguide", WINDOW_TV_GUIDE},
+ {"tvtimers", WINDOW_TV_TIMERS},
+ {"tvsearch", WINDOW_TV_SEARCH},
+ {"radiochannels", WINDOW_RADIO_CHANNELS},
+ {"radiorecordings", WINDOW_RADIO_RECORDINGS},
+ {"radioguide", WINDOW_RADIO_GUIDE},
+ {"radiotimers", WINDOW_RADIO_TIMERS},
+ {"radiosearch", WINDOW_RADIO_SEARCH},
+ {"gamecontrollers", WINDOW_DIALOG_GAME_CONTROLLERS},
+ {"gameports", WINDOW_DIALOG_GAME_PORTS},
+ {"games", WINDOW_GAMES},
+ {"pvrguidecontrols", WINDOW_DIALOG_PVR_GUIDE_CONTROLS},
+ {"pvrguideinfo", WINDOW_DIALOG_PVR_GUIDE_INFO},
+ {"pvrrecordinginfo", WINDOW_DIALOG_PVR_RECORDING_INFO},
+ {"pvrradiordsinfo", WINDOW_DIALOG_PVR_RADIO_RDS_INFO},
+ {"pvrtimersetting", WINDOW_DIALOG_PVR_TIMER_SETTING},
+ {"pvrgroupmanager", WINDOW_DIALOG_PVR_GROUP_MANAGER},
+ {"pvrchannelmanager", WINDOW_DIALOG_PVR_CHANNEL_MANAGER},
+ {"pvrguidesearch", WINDOW_DIALOG_PVR_GUIDE_SEARCH},
+ {"pvrchannelscan", WINDOW_DIALOG_PVR_CHANNEL_SCAN},
+ {"pvrupdateprogress", WINDOW_DIALOG_PVR_UPDATE_PROGRESS},
+ {"pvrosdchannels", WINDOW_DIALOG_PVR_OSD_CHANNELS},
+ {"pvrchannelguide", WINDOW_DIALOG_PVR_CHANNEL_GUIDE},
+ {"pvrosdguide", WINDOW_DIALOG_PVR_CHANNEL_GUIDE}, // backward compatibility to v17
+ {"pvrosdteletext", WINDOW_DIALOG_OSD_TELETEXT},
+ {"systeminfo", WINDOW_SYSTEM_INFORMATION},
+ {"screencalibration", WINDOW_SCREEN_CALIBRATION},
+ {"systemsettings", WINDOW_SETTINGS_SYSTEM},
+ {"servicesettings", WINDOW_SETTINGS_SERVICE},
+ {"pvrsettings", WINDOW_SETTINGS_MYPVR},
+ {"playersettings", WINDOW_SETTINGS_PLAYER},
+ {"mediasettings", WINDOW_SETTINGS_MEDIA},
+ {"interfacesettings", WINDOW_SETTINGS_INTERFACE},
+ {"appearancesettings", WINDOW_SETTINGS_INTERFACE}, // backward compatibility to v16
+ {"gamesettings", WINDOW_SETTINGS_MYGAMES},
+ {"videoplaylist", WINDOW_VIDEO_PLAYLIST},
+ {"loginscreen", WINDOW_LOGIN_SCREEN},
+ {"profiles", WINDOW_SETTINGS_PROFILES},
+ {"skinsettings", WINDOW_SKIN_SETTINGS},
+ {"addonbrowser", WINDOW_ADDON_BROWSER},
+ {"yesnodialog", WINDOW_DIALOG_YES_NO},
+ {"progressdialog", WINDOW_DIALOG_PROGRESS},
+ {"virtualkeyboard", WINDOW_DIALOG_KEYBOARD},
+ {"volumebar", WINDOW_DIALOG_VOLUME_BAR},
+ {"submenu", WINDOW_DIALOG_SUB_MENU},
+ {"favourites", WINDOW_DIALOG_FAVOURITES},
+ {"contextmenu", WINDOW_DIALOG_CONTEXT_MENU},
+ {"notification", WINDOW_DIALOG_KAI_TOAST},
+ {"numericinput", WINDOW_DIALOG_NUMERIC},
+ {"gamepadinput", WINDOW_DIALOG_GAMEPAD},
+ {"shutdownmenu", WINDOW_DIALOG_BUTTON_MENU},
+ {"playercontrols", WINDOW_DIALOG_PLAYER_CONTROLS},
+ {"playerprocessinfo", WINDOW_DIALOG_PLAYER_PROCESS_INFO},
+ {"seekbar", WINDOW_DIALOG_SEEK_BAR},
+ {"musicosd", WINDOW_DIALOG_MUSIC_OSD},
+ {"addonsettings", WINDOW_DIALOG_ADDON_SETTINGS},
+ {"visualisationpresetlist", WINDOW_DIALOG_VIS_PRESET_LIST},
+ {"osdcmssettings", WINDOW_DIALOG_CMS_OSD_SETTINGS},
+ {"osdvideosettings", WINDOW_DIALOG_VIDEO_OSD_SETTINGS},
+ {"osdaudiosettings", WINDOW_DIALOG_AUDIO_OSD_SETTINGS},
+ {"osdsubtitlesettings", WINDOW_DIALOG_SUBTITLE_OSD_SETTINGS},
+ {"videobookmarks", WINDOW_DIALOG_VIDEO_BOOKMARKS},
+ {"filebrowser", WINDOW_DIALOG_FILE_BROWSER},
+ {"networksetup", WINDOW_DIALOG_NETWORK_SETUP},
+ {"mediasource", WINDOW_DIALOG_MEDIA_SOURCE},
+ {"profilesettings", WINDOW_DIALOG_PROFILE_SETTINGS},
+ {"locksettings", WINDOW_DIALOG_LOCK_SETTINGS},
+ {"contentsettings", WINDOW_DIALOG_CONTENT_SETTINGS},
+ {"libexportsettings", WINDOW_DIALOG_LIBEXPORT_SETTINGS},
+ {"songinformation", WINDOW_DIALOG_SONG_INFO},
+ {"smartplaylisteditor", WINDOW_DIALOG_SMART_PLAYLIST_EDITOR},
+ {"smartplaylistrule", WINDOW_DIALOG_SMART_PLAYLIST_RULE},
+ {"busydialog", WINDOW_DIALOG_BUSY},
+ {"busydialognocancel", WINDOW_DIALOG_BUSY_NOCANCEL},
+ {"pictureinfo", WINDOW_DIALOG_PICTURE_INFO},
+ {"fullscreeninfo", WINDOW_DIALOG_FULLSCREEN_INFO},
+ {"sliderdialog", WINDOW_DIALOG_SLIDER},
+ {"addoninformation", WINDOW_DIALOG_ADDON_INFO},
+ {"subtitlesearch", WINDOW_DIALOG_SUBTITLES},
+ {"musicplaylist", WINDOW_MUSIC_PLAYLIST},
+ {"musicplaylisteditor", WINDOW_MUSIC_PLAYLIST_EDITOR},
+ {"infoprovidersettings", WINDOW_DIALOG_INFOPROVIDER_SETTINGS},
+ {"teletext", WINDOW_DIALOG_OSD_TELETEXT},
+ {"selectdialog", WINDOW_DIALOG_SELECT},
+ {"musicinformation", WINDOW_DIALOG_MUSIC_INFO},
+ {"okdialog", WINDOW_DIALOG_OK},
+ {"movieinformation", WINDOW_DIALOG_VIDEO_INFO},
+ {"textviewer", WINDOW_DIALOG_TEXT_VIEWER},
+ {"fullscreenvideo", WINDOW_FULLSCREEN_VIDEO},
+ {"dialogcolorpicker", WINDOW_DIALOG_COLOR_PICKER},
+
+ // Virtual window for fullscreen radio, uses WINDOW_FULLSCREEN_VIDEO as
+ // fallback
+ {"fullscreenlivetv", WINDOW_FULLSCREEN_LIVETV},
+
+ // Live TV channel preview
+ {"fullscreenlivetvpreview", WINDOW_FULLSCREEN_LIVETV_PREVIEW},
+
+ // Live TV direct channel number input
+ {"fullscreenlivetvinput", WINDOW_FULLSCREEN_LIVETV_INPUT},
+
+ // Virtual window for fullscreen radio, uses WINDOW_VISUALISATION as fallback
+ {"fullscreenradio", WINDOW_FULLSCREEN_RADIO},
+
+ // PVR Radio channel preview
+ {"fullscreenradiopreview", WINDOW_FULLSCREEN_RADIO_PREVIEW},
+
+ // PVR radio direct channel number input
+ {"fullscreenradioinput", WINDOW_FULLSCREEN_RADIO_INPUT},
+
+ {"fullscreengame", WINDOW_FULLSCREEN_GAME},
+ {"visualisation", WINDOW_VISUALISATION},
+ {"slideshow", WINDOW_SLIDESHOW},
+ {"weather", WINDOW_WEATHER},
+ {"screensaver", WINDOW_SCREENSAVER},
+ {"videoosd", WINDOW_DIALOG_VIDEO_OSD},
+ {"videomenu", WINDOW_VIDEO_MENU},
+ {"videotimeseek", WINDOW_VIDEO_TIME_SEEK},
+ {"splash", WINDOW_SPLASH},
+ {"startwindow", WINDOW_START},
+ {"startup", WINDOW_STARTUP_ANIM},
+ {"peripheralsettings", WINDOW_DIALOG_PERIPHERAL_SETTINGS},
+ {"extendedprogressdialog", WINDOW_DIALOG_EXT_PROGRESS},
+ {"mediafilter", WINDOW_DIALOG_MEDIA_FILTER},
+ {"addon", WINDOW_ADDON_START},
+ {"eventlog", WINDOW_EVENT_LOG},
+ {"favouritesbrowser", WINDOW_FAVOURITES},
+ {"tvtimerrules", WINDOW_TV_TIMER_RULES},
+ {"radiotimerrules", WINDOW_RADIO_TIMER_RULES},
+ {"gameosd", WINDOW_DIALOG_GAME_OSD},
+ {"gamevideofilter", WINDOW_DIALOG_GAME_VIDEO_FILTER},
+ {"gamestretchmode", WINDOW_DIALOG_GAME_STRETCH_MODE},
+ {"gamevolume", WINDOW_DIALOG_GAME_VOLUME},
+ {"gameadvancedsettings", WINDOW_DIALOG_GAME_ADVANCED_SETTINGS},
+ {"gamevideorotation", WINDOW_DIALOG_GAME_VIDEO_ROTATION},
+ {"ingamesaves", WINDOW_DIALOG_IN_GAME_SAVES},
+ {"gamesaves", WINDOW_DIALOG_GAME_SAVES}};
+
+namespace
+{
+struct FallbackWindowMapping
+{
+ int origin;
+ int target;
+};
+
+static const std::vector<FallbackWindowMapping> FallbackWindows = {
+ {WINDOW_FULLSCREEN_LIVETV, WINDOW_FULLSCREEN_VIDEO},
+ {WINDOW_FULLSCREEN_LIVETV_INPUT, WINDOW_FULLSCREEN_LIVETV},
+ {WINDOW_FULLSCREEN_LIVETV_PREVIEW, WINDOW_FULLSCREEN_LIVETV},
+ {WINDOW_FULLSCREEN_RADIO, WINDOW_VISUALISATION},
+ {WINDOW_FULLSCREEN_RADIO_INPUT, WINDOW_FULLSCREEN_RADIO},
+ {WINDOW_FULLSCREEN_RADIO_PREVIEW, WINDOW_FULLSCREEN_RADIO},
+};
+} // anonymous namespace
+
+bool CWindowTranslator::WindowNameCompare::operator()(const WindowMapItem& lhs,
+ const WindowMapItem& rhs) const
+{
+ return std::strcmp(lhs.windowName, rhs.windowName) < 0;
+}
+
+bool CWindowTranslator::WindowIDCompare::operator()(const WindowMapItem& lhs,
+ const WindowMapItem& rhs) const
+{
+ return lhs.windowId < rhs.windowId;
+}
+
+void CWindowTranslator::GetWindows(std::vector<std::string>& windowList)
+{
+ windowList.clear();
+ windowList.reserve(WindowMappingByName.size());
+ for (auto itMapping : WindowMappingByName)
+ windowList.emplace_back(itMapping.windowName);
+}
+
+int CWindowTranslator::TranslateWindow(const std::string& window)
+{
+ std::string strWindow(window);
+ if (strWindow.empty())
+ return WINDOW_INVALID;
+
+ StringUtils::ToLower(strWindow);
+
+ // Eliminate .xml
+ if (StringUtils::EndsWith(strWindow, ".xml"))
+ strWindow = strWindow.substr(0, strWindow.size() - 4);
+
+ // window12345, for custom window to be keymapped
+ if (strWindow.length() > 6 && StringUtils::StartsWith(strWindow, "window"))
+ strWindow = strWindow.substr(6);
+
+ // Drop "my" prefix
+ if (StringUtils::StartsWith(strWindow, "my"))
+ strWindow = strWindow.substr(2);
+
+ if (StringUtils::IsNaturalNumber(strWindow))
+ {
+ // Allow a full window ID or a delta ID
+ int iWindow = atoi(strWindow.c_str());
+ if (iWindow > WINDOW_INVALID)
+ return iWindow;
+
+ return WINDOW_HOME + iWindow;
+ }
+
+ // Run through the window structure
+ auto it = WindowMappingByName.find({strWindow.c_str(), {}});
+ if (it != WindowMappingByName.end())
+ return it->windowId;
+
+ CLog::Log(LOGERROR, "Window Translator: Can't find window {}", window);
+
+ return WINDOW_INVALID;
+}
+
+std::string CWindowTranslator::TranslateWindow(int windowId)
+{
+ static auto reverseWindowMapping = CreateWindowMappingByID();
+
+ windowId = GetVirtualWindow(windowId);
+
+ auto it = reverseWindowMapping.find(WindowMapItem{"", windowId});
+ if (it != reverseWindowMapping.end())
+ return it->windowName;
+
+ return "";
+}
+
+int CWindowTranslator::GetFallbackWindow(int windowId)
+{
+ auto it = std::find_if(
+ FallbackWindows.begin(), FallbackWindows.end(),
+ [windowId](const FallbackWindowMapping& mapping) { return mapping.origin == windowId; });
+
+ if (it != FallbackWindows.end())
+ return it->target;
+
+ // For add-on windows use WINDOW_ADDON_START because ID is dynamic
+ if (WINDOW_ADDON_START < windowId && windowId <= WINDOW_ADDON_END)
+ return WINDOW_ADDON_START;
+
+ return -1;
+}
+
+CWindowTranslator::WindowMapByID CWindowTranslator::CreateWindowMappingByID()
+{
+ WindowMapByID reverseWindowMapping;
+
+ reverseWindowMapping.insert(WindowMappingByName.begin(), WindowMappingByName.end());
+
+ return reverseWindowMapping;
+}
+
+int CWindowTranslator::GetVirtualWindow(int windowId)
+{
+ if (windowId == WINDOW_FULLSCREEN_VIDEO)
+ {
+ if (g_application.CurrentFileItem().HasPVRChannelInfoTag())
+ {
+ // special casing for Live TV
+ if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .HasChannelNumber())
+ return WINDOW_FULLSCREEN_LIVETV_INPUT;
+ else if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNavigator()
+ .IsPreview())
+ return WINDOW_FULLSCREEN_LIVETV_PREVIEW;
+ else
+ return WINDOW_FULLSCREEN_LIVETV;
+ }
+ else
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ // check if we're in a DVD menu
+ if (appPlayer->IsInMenu())
+ return WINDOW_VIDEO_MENU;
+ // special casing for numeric seek
+ else if (appPlayer->GetSeekHandler().HasTimeCode())
+ return WINDOW_VIDEO_TIME_SEEK;
+ }
+ }
+ else if (windowId == WINDOW_VISUALISATION)
+ {
+ if (g_application.CurrentFileItem().HasPVRChannelInfoTag())
+ {
+ // special casing for PVR radio
+ if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .HasChannelNumber())
+ return WINDOW_FULLSCREEN_RADIO_INPUT;
+ else if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNavigator()
+ .IsPreview())
+ return WINDOW_FULLSCREEN_RADIO_PREVIEW;
+ else
+ return WINDOW_FULLSCREEN_RADIO;
+ }
+ else
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ // special casing for numeric seek
+ if (appPlayer->GetSeekHandler().HasTimeCode())
+ return WINDOW_VIDEO_TIME_SEEK;
+ }
+ }
+
+ return windowId;
+}
diff --git a/xbmc/input/WindowTranslator.h b/xbmc/input/WindowTranslator.h
new file mode 100644
index 0000000..9d99c15
--- /dev/null
+++ b/xbmc/input/WindowTranslator.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017-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 <set>
+#include <string>
+#include <vector>
+
+class CWindowTranslator
+{
+public:
+ /*!
+ * \brief Get a list of all known window names
+ */
+ static void GetWindows(std::vector<std::string>& windowList);
+
+ /*!
+ * \brief Translate between a window name and its ID
+ * \param window The name of the window
+ * \return ID of the window, or WINDOW_INVALID if not found
+ */
+ static int TranslateWindow(const std::string& window);
+
+ /*!
+ * \brief Translate between a window id and it's name
+ * \param window id of the window
+ * \return name of the window, or an empty string if not found
+ */
+ static std::string TranslateWindow(int windowId);
+
+ /*!
+ * \brief Get the window ID that should be used as fallback for keymap input
+ * \return The fallback window, or -1 for no fallback window
+ */
+ static int GetFallbackWindow(int windowId);
+
+ /*!
+ * \brief Get the special window ID if conditions met
+ * \return The special window ID or the given window ID
+ */
+ static int GetVirtualWindow(int windowId);
+
+private:
+ struct WindowMapItem
+ {
+ const char* windowName;
+ int windowId;
+ };
+
+ struct WindowNameCompare
+ {
+ bool operator()(const WindowMapItem& lhs, const WindowMapItem& rhs) const;
+ };
+
+ struct WindowIDCompare
+ {
+ bool operator()(const WindowMapItem& lhs, const WindowMapItem& rhs) const;
+ };
+
+ using WindowMapByName = std::set<WindowMapItem, WindowNameCompare>;
+ using WindowMapByID = std::set<WindowMapItem, WindowIDCompare>;
+
+ static WindowMapByID CreateWindowMappingByID();
+
+ static const WindowMapByName WindowMappingByName;
+};
diff --git a/xbmc/input/XBMC_keyboard.h b/xbmc/input/XBMC_keyboard.h
new file mode 100644
index 0000000..09f2af7
--- /dev/null
+++ b/xbmc/input/XBMC_keyboard.h
@@ -0,0 +1,45 @@
+/*
+ * SDL - Simple DirectMedia Layer
+ * Copyright (C) 1997-2009 Sam Lantinga
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ *
+ * Sam Lantinga
+ * slouken@libsdl.org
+ */
+
+#pragma once
+
+/* Include file for SDL keyboard event handling */
+
+#include "XBMC_keysym.h"
+
+#include <stdint.h>
+
+/* Keysym structure
+ - The scancode is hardware dependent, and should not be used by general
+ applications. If no hardware scancode is available, it will be 0.
+
+ - The 'unicode' translated character is only available when character
+ translation is enabled by the XBMC_EnableUNICODE() API. If non-zero,
+ this is a UNICODE character corresponding to the keypress. If the
+ high 9 bits of the character are 0, then this maps to the equivalent
+ ASCII character:
+ char ch;
+ if ( (keysym.unicode & 0xFF80) == 0 ) {
+ ch = keysym.unicode & 0x7F;
+ } else {
+ An international character..
+ }
+ */
+typedef struct XBMC_keysym
+{
+ unsigned char scancode; /* hardware specific scancode */
+ XBMCKey sym; /* SDL virtual keysym */
+ XBMCMod mod; /* current key modifiers */
+ uint16_t unicode; /* translated character */
+} XBMC_keysym;
+
+/* This is the mask which refers to all hotkey bindings */
+#define XBMC_ALL_HOTKEYS 0xFFFFFFFF
diff --git a/xbmc/input/XBMC_keysym.h b/xbmc/input/XBMC_keysym.h
new file mode 100644
index 0000000..a417c0c
--- /dev/null
+++ b/xbmc/input/XBMC_keysym.h
@@ -0,0 +1,263 @@
+/*
+ * SDL - Simple DirectMedia Layer
+ * Copyright (C) 1997-2009 Sam Lantinga
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ *
+ * Sam Lantinga
+ * slouken@libsdl.org
+ */
+
+#pragma once
+
+// The XBMC_keysym identifies a physical key on the keyboard i.e. it is
+// analogous to a scan code but is hardware independent.
+// These values are bazsed on the SDL_keysym standards, see:
+//
+// http://www.libsdl.org/tmp/SDL-1.3-docs/SDL__keysym_8h.html
+//
+// On SDL_KEYDOWN messages the keysym.sym will be one of these values.
+//
+// On OSs that don't support SDL (i.e. Windows) the OS dependant key
+// handling code converts keypresses to an XBMC_keysym value.
+
+typedef enum
+{
+ // The keyboard syms have been cleverly chosen to map to ASCII
+ XBMCK_UNKNOWN = 0x00,
+ XBMCK_FIRST = 0x00,
+ XBMCK_CTRLF = 0x06,
+ XBMCK_BACKSPACE = 0x08,
+ XBMCK_TAB = 0x09,
+ XBMCK_CLEAR = 0x0C,
+ XBMCK_RETURN = 0x0D,
+ XBMCK_PAUSE = 0x13,
+ XBMCK_ESCAPE = 0x1B,
+ XBMCK_SPACE = 0x20,
+ XBMCK_EXCLAIM = 0x21,
+ XBMCK_QUOTEDBL = 0x22,
+ XBMCK_HASH = 0x23,
+ XBMCK_DOLLAR = 0x24,
+ XBMCK_PERCENT = 0x25,
+ XBMCK_AMPERSAND = 0x26,
+ XBMCK_QUOTE = 0x27,
+ XBMCK_LEFTPAREN = 0x28,
+ XBMCK_RIGHTPAREN = 0x29,
+ XBMCK_ASTERISK = 0x2A,
+ XBMCK_PLUS = 0x2B,
+ XBMCK_COMMA = 0x2C,
+ XBMCK_MINUS = 0x2D,
+ XBMCK_PERIOD = 0x2E,
+ XBMCK_SLASH = 0x2F,
+ XBMCK_0 = 0x30,
+ XBMCK_1 = 0x31,
+ XBMCK_2 = 0x32,
+ XBMCK_3 = 0x33,
+ XBMCK_4 = 0x34,
+ XBMCK_5 = 0x35,
+ XBMCK_6 = 0x36,
+ XBMCK_7 = 0x37,
+ XBMCK_8 = 0x38,
+ XBMCK_9 = 0x39,
+ XBMCK_COLON = 0x3A,
+ XBMCK_SEMICOLON = 0x3B,
+ XBMCK_LESS = 0x3C,
+ XBMCK_EQUALS = 0x3D,
+ XBMCK_GREATER = 0x3E,
+ XBMCK_QUESTION = 0x3F,
+ XBMCK_AT = 0x40,
+ // Skip uppercase letters
+ XBMCK_LEFTBRACKET = 0x5B,
+ XBMCK_BACKSLASH = 0x5C,
+ XBMCK_RIGHTBRACKET = 0x5D,
+ XBMCK_CARET = 0x5E,
+ XBMCK_UNDERSCORE = 0x5F,
+ XBMCK_BACKQUOTE = 0x60,
+ XBMCK_a = 0x61,
+ XBMCK_b = 0x62,
+ XBMCK_c = 0x63,
+ XBMCK_d = 0x64,
+ XBMCK_e = 0x65,
+ XBMCK_f = 0x66,
+ XBMCK_g = 0x67,
+ XBMCK_h = 0x68,
+ XBMCK_i = 0x69,
+ XBMCK_j = 0x6A,
+ XBMCK_k = 0x6B,
+ XBMCK_l = 0x6C,
+ XBMCK_m = 0x6D,
+ XBMCK_n = 0x6E,
+ XBMCK_o = 0x6F,
+ XBMCK_p = 0x70,
+ XBMCK_q = 0x71,
+ XBMCK_r = 0x72,
+ XBMCK_s = 0x73,
+ XBMCK_t = 0x74,
+ XBMCK_u = 0x75,
+ XBMCK_v = 0x76,
+ XBMCK_w = 0x77,
+ XBMCK_x = 0x78,
+ XBMCK_y = 0x79,
+ XBMCK_z = 0x7A,
+ XBMCK_LEFTBRACE = 0x7b,
+ XBMCK_PIPE = 0x7C,
+ XBMCK_RIGHTBRACE = 0x7D,
+ XBMCK_TILDE = 0x7E,
+ XBMCK_DELETE = 0x7F,
+ // End of ASCII mapped keysyms
+
+ // Multimedia keys
+ // These are the Windows VK_ codes. SDL doesn't define codes for
+ // these keys.
+ XBMCK_BROWSER_BACK = 0xA6,
+ XBMCK_BROWSER_FORWARD = 0xA7,
+ XBMCK_BROWSER_REFRESH = 0xA8,
+ XBMCK_BROWSER_STOP = 0xA9,
+ XBMCK_BROWSER_SEARCH = 0xAA,
+ XBMCK_BROWSER_FAVORITES = 0xAB,
+ XBMCK_BROWSER_HOME = 0xAC,
+ XBMCK_VOLUME_MUTE = 0xAD,
+ XBMCK_VOLUME_DOWN = 0xAE,
+ XBMCK_VOLUME_UP = 0xAF,
+ XBMCK_MEDIA_NEXT_TRACK = 0xB0,
+ XBMCK_MEDIA_PREV_TRACK = 0xB1,
+ XBMCK_MEDIA_STOP = 0xB2,
+ XBMCK_MEDIA_PLAY_PAUSE = 0xB3,
+ XBMCK_LAUNCH_MAIL = 0xB4,
+ XBMCK_LAUNCH_MEDIA_SELECT = 0xB5,
+ XBMCK_LAUNCH_APP1 = 0xB6,
+ XBMCK_LAUNCH_APP2 = 0xB7,
+ XBMCK_LAUNCH_FILE_BROWSER = 0xB8,
+ XBMCK_LAUNCH_MEDIA_CENTER = 0xB9,
+ XBMCK_MEDIA_REWIND = 0xBA,
+ XBMCK_MEDIA_FASTFORWARD = 0xBB,
+
+ // Numeric keypad
+ XBMCK_KP0 = 0x100,
+ XBMCK_KP1 = 0x101,
+ XBMCK_KP2 = 0x102,
+ XBMCK_KP3 = 0x103,
+ XBMCK_KP4 = 0x104,
+ XBMCK_KP5 = 0x105,
+ XBMCK_KP6 = 0x106,
+ XBMCK_KP7 = 0x107,
+ XBMCK_KP8 = 0x108,
+ XBMCK_KP9 = 0x109,
+ XBMCK_KP_PERIOD = 0x10A,
+ XBMCK_KP_DIVIDE = 0x10B,
+ XBMCK_KP_MULTIPLY = 0x10C,
+ XBMCK_KP_MINUS = 0x10D,
+ XBMCK_KP_PLUS = 0x10E,
+ XBMCK_KP_ENTER = 0x10F,
+ XBMCK_KP_EQUALS = 0x110,
+
+ // Arrows + Home/End pad
+ XBMCK_UP = 0x111,
+ XBMCK_DOWN = 0x112,
+ XBMCK_RIGHT = 0x113,
+ XBMCK_LEFT = 0x114,
+ XBMCK_INSERT = 0x115,
+ XBMCK_HOME = 0x116,
+ XBMCK_END = 0x117,
+ XBMCK_PAGEUP = 0x118,
+ XBMCK_PAGEDOWN = 0x119,
+
+ // Function keys
+ XBMCK_F1 = 0x11A,
+ XBMCK_F2 = 0x11B,
+ XBMCK_F3 = 0x11C,
+ XBMCK_F4 = 0x11D,
+ XBMCK_F5 = 0x11E,
+ XBMCK_F6 = 0x11F,
+ XBMCK_F7 = 0x120,
+ XBMCK_F8 = 0x121,
+ XBMCK_F9 = 0x122,
+ XBMCK_F10 = 0x123,
+ XBMCK_F11 = 0x124,
+ XBMCK_F12 = 0x125,
+ XBMCK_F13 = 0x126,
+ XBMCK_F14 = 0x127,
+ XBMCK_F15 = 0x128,
+
+ // Key state modifier keys
+ XBMCK_NUMLOCK = 0x12C,
+ XBMCK_CAPSLOCK = 0x12D,
+ XBMCK_SCROLLOCK = 0x12E,
+ XBMCK_RSHIFT = 0x12F,
+ XBMCK_LSHIFT = 0x130,
+ XBMCK_RCTRL = 0x131,
+ XBMCK_LCTRL = 0x132,
+ XBMCK_RALT = 0x133,
+ XBMCK_LALT = 0x134,
+ XBMCK_RMETA = 0x135,
+ XBMCK_LMETA = 0x136,
+ XBMCK_LSUPER = 0x137, // Left "Windows" key
+ XBMCK_RSUPER = 0x138, // Right "Windows" key
+ XBMCK_MODE = 0x139, // "Alt Gr" key
+ XBMCK_COMPOSE = 0x13A, // Multi-key compose key
+
+ // Miscellaneous function keys
+ XBMCK_HELP = 0x13B,
+ XBMCK_PRINT = 0x13C,
+ XBMCK_SYSREQ = 0x13D,
+ XBMCK_BREAK = 0x13E,
+ XBMCK_MENU = 0x13F,
+ XBMCK_POWER = 0x140, // Power Macintosh power key
+ XBMCK_EURO = 0x141, // Some european keyboards
+ XBMCK_UNDO = 0x142, // Atari keyboard has Undo
+ XBMCK_SLEEP = 0x143, // Sleep button on Nyxboard remote (and others?)
+ XBMCK_GUIDE = 0x144,
+ XBMCK_SETTINGS = 0x145,
+ XBMCK_INFO = 0x146,
+ XBMCK_RED = 0x147,
+ XBMCK_GREEN = 0x148,
+ XBMCK_YELLOW = 0x149,
+ XBMCK_BLUE = 0x14a,
+ XBMCK_ZOOM = 0x14b,
+ XBMCK_TEXT = 0x14c,
+ XBMCK_FAVORITES = 0x14d,
+ XBMCK_HOMEPAGE = 0x14e,
+ XBMCK_CONFIG = 0x14f,
+ XBMCK_EPG = 0x150,
+
+ // Add any other keys here
+
+ /* Media keys */
+ XBMCK_STOP = 337,
+ XBMCK_RECORD = 338,
+ XBMCK_REWIND = 339,
+ XBMCK_PHONE = 340,
+ XBMCK_PLAY = 341,
+ XBMCK_SHUFFLE = 342,
+ XBMCK_FASTFORWARD = 343,
+ XBMCK_EJECT = 344,
+
+ XBMCK_LAST
+} XBMCKey;
+
+// Enumeration of valid key mods (possibly OR'd together)
+typedef enum
+{
+ XBMCKMOD_NONE = 0x0000,
+ XBMCKMOD_LSHIFT = 0x0001,
+ XBMCKMOD_RSHIFT = 0x0002,
+ XBMCKMOD_LSUPER = 0x0010,
+ XBMCKMOD_RSUPER = 0x0020,
+ XBMCKMOD_LCTRL = 0x0040,
+ XBMCKMOD_RCTRL = 0x0080,
+ XBMCKMOD_LALT = 0x0100,
+ XBMCKMOD_RALT = 0x0200,
+ XBMCKMOD_LMETA = 0x0400,
+ XBMCKMOD_RMETA = 0x0800,
+ XBMCKMOD_NUM = 0x1000,
+ XBMCKMOD_CAPS = 0x2000,
+ XBMCKMOD_MODE = 0x4000,
+ XBMCKMOD_RESERVED = 0x8000
+} XBMCMod;
+
+#define XBMCKMOD_CTRL (XBMCKMOD_LCTRL | XBMCKMOD_RCTRL)
+#define XBMCKMOD_SHIFT (XBMCKMOD_LSHIFT | XBMCKMOD_RSHIFT)
+#define XBMCKMOD_ALT (XBMCKMOD_LALT | XBMCKMOD_RALT)
+#define XBMCKMOD_META (XBMCKMOD_LMETA | XBMCKMOD_RMETA)
+#define XBMCKMOD_SUPER (XBMCKMOD_LSUPER | XBMCKMOD_RSUPER)
diff --git a/xbmc/input/XBMC_keytable.cpp b/xbmc/input/XBMC_keytable.cpp
new file mode 100644
index 0000000..95ddb4c
--- /dev/null
+++ b/xbmc/input/XBMC_keytable.cpp
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2007-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 "input/XBMC_keytable.h"
+
+#include "input/XBMC_keysym.h"
+#include "input/XBMC_vkeys.h"
+#include "utils/StringUtils.h"
+
+// The array of XBMCKEYTABLEs used in XBMC.
+// scancode, sym, unicode, ascii, vkey, keyname
+static const XBMCKEYTABLE XBMCKeyTable[] = {
+ {XBMCK_BACKSPACE, 0, 0, XBMCVK_BACK, "backspace"},
+ {XBMCK_TAB, 0, 0, XBMCVK_TAB, "tab"},
+ {XBMCK_RETURN, 0, 0, XBMCVK_RETURN, "return"},
+ {XBMCK_ESCAPE, 0, 0, XBMCVK_ESCAPE, "escape"},
+ {0, 0, 0, XBMCVK_ESCAPE, "esc"} // Allowed abbreviation for "escape"
+
+ // Number keys on the main keyboard
+ ,
+ {XBMCK_0, '0', '0', XBMCVK_0, "zero"},
+ {XBMCK_1, '1', '1', XBMCVK_1, "one"},
+ {XBMCK_2, '2', '2', XBMCVK_2, "two"},
+ {XBMCK_3, '3', '3', XBMCVK_3, "three"},
+ {XBMCK_4, '4', '4', XBMCVK_4, "four"},
+ {XBMCK_5, '5', '5', XBMCVK_5, "five"},
+ {XBMCK_6, '6', '6', XBMCVK_6, "six"},
+ {XBMCK_7, '7', '7', XBMCVK_7, "seven"},
+ {XBMCK_8, '8', '8', XBMCVK_8, "eight"},
+ {XBMCK_9, '9', '9', XBMCVK_9, "nine"}
+
+ // A to Z - note that upper case A-Z don't have a matching name or
+ // vkey. Only the lower case a-z are used in key mappings.
+ ,
+ {XBMCK_a, 'A', 'A', XBMCVK_A, NULL},
+ {XBMCK_b, 'B', 'B', XBMCVK_B, NULL},
+ {XBMCK_c, 'C', 'C', XBMCVK_C, NULL},
+ {XBMCK_d, 'D', 'D', XBMCVK_D, NULL},
+ {XBMCK_e, 'E', 'E', XBMCVK_E, NULL},
+ {XBMCK_f, 'F', 'F', XBMCVK_F, NULL},
+ {XBMCK_g, 'G', 'G', XBMCVK_G, NULL},
+ {XBMCK_h, 'H', 'H', XBMCVK_H, NULL},
+ {XBMCK_i, 'I', 'I', XBMCVK_I, NULL},
+ {XBMCK_j, 'J', 'J', XBMCVK_J, NULL},
+ {XBMCK_k, 'K', 'K', XBMCVK_K, NULL},
+ {XBMCK_l, 'L', 'L', XBMCVK_L, NULL},
+ {XBMCK_m, 'M', 'M', XBMCVK_M, NULL},
+ {XBMCK_n, 'N', 'N', XBMCVK_N, NULL},
+ {XBMCK_o, 'O', 'O', XBMCVK_O, NULL},
+ {XBMCK_p, 'P', 'P', XBMCVK_P, NULL},
+ {XBMCK_q, 'Q', 'Q', XBMCVK_Q, NULL},
+ {XBMCK_r, 'R', 'R', XBMCVK_R, NULL},
+ {XBMCK_s, 'S', 'S', XBMCVK_S, NULL},
+ {XBMCK_t, 'T', 'T', XBMCVK_T, NULL},
+ {XBMCK_u, 'U', 'U', XBMCVK_U, NULL},
+ {XBMCK_v, 'V', 'V', XBMCVK_V, NULL},
+ {XBMCK_w, 'W', 'W', XBMCVK_W, NULL},
+ {XBMCK_x, 'X', 'X', XBMCVK_X, NULL},
+ {XBMCK_y, 'Y', 'Y', XBMCVK_Y, NULL},
+ {XBMCK_z, 'Z', 'Z', XBMCVK_Z, NULL}
+
+ ,
+ {XBMCK_a, 'a', 'a', XBMCVK_A, "a"},
+ {XBMCK_b, 'b', 'b', XBMCVK_B, "b"},
+ {XBMCK_c, 'c', 'c', XBMCVK_C, "c"},
+ {XBMCK_d, 'd', 'd', XBMCVK_D, "d"},
+ {XBMCK_e, 'e', 'e', XBMCVK_E, "e"},
+ {XBMCK_f, 'f', 'f', XBMCVK_F, "f"},
+ {XBMCK_g, 'g', 'g', XBMCVK_G, "g"},
+ {XBMCK_h, 'h', 'h', XBMCVK_H, "h"},
+ {XBMCK_i, 'i', 'i', XBMCVK_I, "i"},
+ {XBMCK_j, 'j', 'j', XBMCVK_J, "j"},
+ {XBMCK_k, 'k', 'k', XBMCVK_K, "k"},
+ {XBMCK_l, 'l', 'l', XBMCVK_L, "l"},
+ {XBMCK_m, 'm', 'm', XBMCVK_M, "m"},
+ {XBMCK_n, 'n', 'n', XBMCVK_N, "n"},
+ {XBMCK_o, 'o', 'o', XBMCVK_O, "o"},
+ {XBMCK_p, 'p', 'p', XBMCVK_P, "p"},
+ {XBMCK_q, 'q', 'q', XBMCVK_Q, "q"},
+ {XBMCK_r, 'r', 'r', XBMCVK_R, "r"},
+ {XBMCK_s, 's', 's', XBMCVK_S, "s"},
+ {XBMCK_t, 't', 't', XBMCVK_T, "t"},
+ {XBMCK_u, 'u', 'u', XBMCVK_U, "u"},
+ {XBMCK_v, 'v', 'v', XBMCVK_V, "v"},
+ {XBMCK_w, 'w', 'w', XBMCVK_W, "w"},
+ {XBMCK_x, 'x', 'x', XBMCVK_X, "x"},
+ {XBMCK_y, 'y', 'y', XBMCVK_Y, "y"},
+ {XBMCK_z, 'z', 'z', XBMCVK_Z, "z"}
+
+ // Misc printing characters
+ ,
+ {XBMCK_SPACE, ' ', ' ', XBMCVK_SPACE, "space"},
+ {XBMCK_EXCLAIM, '!', '!', XBMCVK_EXCLAIM, "exclaim"},
+ {XBMCK_QUOTEDBL, '"', '"', XBMCVK_QUOTEDBL, "doublequote"},
+ {XBMCK_HASH, '#', '#', XBMCVK_HASH, "hash"},
+ {XBMCK_DOLLAR, '$', '$', XBMCVK_DOLLAR, "dollar"},
+ {XBMCK_PERCENT, '%', '%', XBMCVK_PERCENT, "percent"},
+ {XBMCK_AMPERSAND, '&', '&', XBMCVK_AMPERSAND, "ampersand"},
+ {XBMCK_QUOTE, '\'', '\'', XBMCVK_QUOTE, "quote"},
+ {XBMCK_LEFTPAREN, '(', '(', XBMCVK_LEFTPAREN, "leftbracket"},
+ {XBMCK_RIGHTPAREN, ')', ')', XBMCVK_RIGHTPAREN, "rightbracket"},
+ {XBMCK_ASTERISK, '*', '*', XBMCVK_ASTERISK, "asterisk"},
+ {XBMCK_PLUS, '+', '+', XBMCVK_PLUS, "plus"},
+ {XBMCK_COMMA, ',', ',', XBMCVK_COMMA, "comma"},
+ {XBMCK_MINUS, '-', '-', XBMCVK_MINUS, "minus"},
+ {XBMCK_PERIOD, '.', '.', XBMCVK_PERIOD, "period"},
+ {XBMCK_SLASH, '/', '/', XBMCVK_SLASH, "forwardslash"}
+
+ ,
+ {XBMCK_COLON, ':', ':', XBMCVK_COLON, "colon"},
+ {XBMCK_SEMICOLON, ';', ';', XBMCVK_SEMICOLON, "semicolon"},
+ {XBMCK_LESS, '<', '<', XBMCVK_LESS, "lessthan"},
+ {XBMCK_EQUALS, '=', '=', XBMCVK_EQUALS, "equals"},
+ {XBMCK_GREATER, '>', '>', XBMCVK_GREATER, "greaterthan"},
+ {XBMCK_QUESTION, '?', '?', XBMCVK_QUESTION, "questionmark"},
+ {XBMCK_AT, '@', '@', XBMCVK_AT, "at"}
+
+ ,
+ {XBMCK_LEFTBRACKET, '[', '[', XBMCVK_LEFTBRACKET, "opensquarebracket"},
+ {XBMCK_BACKSLASH, '\\', '\\', XBMCVK_BACKSLASH, "backslash"},
+ {XBMCK_RIGHTBRACKET, ']', ']', XBMCVK_RIGHTBRACKET, "closesquarebracket"},
+ {XBMCK_CARET, '^', '^', XBMCVK_CARET, "caret"},
+ {XBMCK_UNDERSCORE, '_', '_', XBMCVK_UNDERSCORE, "underline"},
+ {XBMCK_BACKQUOTE, '`', '`', XBMCVK_BACKQUOTE, "leftquote"}
+
+ ,
+ {XBMCK_LEFTBRACE, '{', '{', XBMCVK_LEFTBRACE, "openbrace"},
+ {XBMCK_PIPE, '|', '|', XBMCVK_PIPE, "pipe"},
+ {XBMCK_RIGHTBRACE, '}', '}', XBMCVK_RIGHTBRACE, "closebrace"},
+ {XBMCK_TILDE, '~', '~', XBMCVK_TILDE, "tilde"}
+
+ // Numeric keypad
+ ,
+ {XBMCK_KP0, '0', '0', XBMCVK_NUMPAD0, "numpadzero"},
+ {XBMCK_KP1, '1', '1', XBMCVK_NUMPAD1, "numpadone"},
+ {XBMCK_KP2, '2', '2', XBMCVK_NUMPAD2, "numpadtwo"},
+ {XBMCK_KP3, '3', '3', XBMCVK_NUMPAD3, "numpadthree"},
+ {XBMCK_KP4, '4', '4', XBMCVK_NUMPAD4, "numpadfour"},
+ {XBMCK_KP5, '5', '5', XBMCVK_NUMPAD5, "numpadfive"},
+ {XBMCK_KP6, '6', '6', XBMCVK_NUMPAD6, "numpadsix"},
+ {XBMCK_KP7, '7', '7', XBMCVK_NUMPAD7, "numpadseven"},
+ {XBMCK_KP8, '8', '8', XBMCVK_NUMPAD8, "numpadeight"},
+ {XBMCK_KP9, '9', '9', XBMCVK_NUMPAD9, "numpadnine"}
+
+ ,
+ {XBMCK_KP_DIVIDE, '/', '/', XBMCVK_NUMPADDIVIDE, "numpaddivide"},
+ {XBMCK_KP_MULTIPLY, '*', '*', XBMCVK_NUMPADTIMES, "numpadtimes"},
+ {XBMCK_KP_MINUS, '-', '-', XBMCVK_NUMPADMINUS, "numpadminus"},
+ {XBMCK_KP_PLUS, '+', '+', XBMCVK_NUMPADPLUS, "numpadplus"},
+ {XBMCK_KP_ENTER, 0, 0, XBMCVK_NUMPADENTER, "enter"},
+ {XBMCK_KP_PERIOD, '.', '.', XBMCVK_NUMPADPERIOD, "numpadperiod"}
+
+ // Multimedia keys
+ ,
+ {XBMCK_BROWSER_BACK, 0, 0, XBMCVK_BROWSER_BACK, "browser_back"},
+ {XBMCK_BROWSER_FORWARD, 0, 0, XBMCVK_BROWSER_FORWARD, "browser_forward"},
+ {XBMCK_BROWSER_REFRESH, 0, 0, XBMCVK_BROWSER_REFRESH, "browser_refresh"},
+ {XBMCK_BROWSER_STOP, 0, 0, XBMCVK_BROWSER_STOP, "browser_stop"},
+ {XBMCK_BROWSER_SEARCH, 0, 0, XBMCVK_BROWSER_SEARCH, "browser_search"},
+ {XBMCK_BROWSER_FAVORITES, 0, 0, XBMCVK_BROWSER_FAVORITES, "browser_favorites"},
+ {XBMCK_BROWSER_HOME, 0, 0, XBMCVK_BROWSER_HOME, "browser_home"},
+ {XBMCK_VOLUME_MUTE, 0, 0, XBMCVK_VOLUME_MUTE, "volume_mute"},
+ {XBMCK_VOLUME_DOWN, 0, 0, XBMCVK_VOLUME_DOWN, "volume_down"},
+ {XBMCK_VOLUME_UP, 0, 0, XBMCVK_VOLUME_UP, "volume_up"},
+ {XBMCK_MEDIA_NEXT_TRACK, 0, 0, XBMCVK_MEDIA_NEXT_TRACK, "next_track"},
+ {XBMCK_MEDIA_PREV_TRACK, 0, 0, XBMCVK_MEDIA_PREV_TRACK, "prev_track"},
+ {XBMCK_MEDIA_STOP, 0, 0, XBMCVK_MEDIA_STOP, "stop"},
+ {XBMCK_MEDIA_PLAY_PAUSE, 0, 0, XBMCVK_MEDIA_PLAY_PAUSE, "play_pause"},
+ {XBMCK_MEDIA_REWIND, 0, 0, XBMCVK_MEDIA_REWIND, "rewind"},
+ {XBMCK_MEDIA_FASTFORWARD, 0, 0, XBMCVK_MEDIA_FASTFORWARD, "fastforward"},
+ {XBMCK_LAUNCH_MAIL, 0, 0, XBMCVK_LAUNCH_MAIL, "launch_mail"},
+ {XBMCK_LAUNCH_MEDIA_SELECT, 0, 0, XBMCVK_LAUNCH_MEDIA_SELECT, "launch_media_select"},
+ {XBMCK_LAUNCH_APP1, 0, 0, XBMCVK_LAUNCH_APP1, "launch_app1_pc_icon"},
+ {XBMCK_LAUNCH_APP2, 0, 0, XBMCVK_LAUNCH_APP2, "launch_app2_pc_icon"},
+ {XBMCK_LAUNCH_FILE_BROWSER, 0, 0, XBMCVK_LAUNCH_FILE_BROWSER, "launch_file_browser"},
+ {XBMCK_LAUNCH_MEDIA_CENTER, 0, 0, XBMCVK_LAUNCH_MEDIA_CENTER, "launch_media_center"},
+ {XBMCK_PLAY, 0, 0, XBMCVK_MEDIA_PLAY_PAUSE, "play_pause"},
+ {XBMCK_STOP, 0, 0, XBMCVK_MEDIA_STOP, "stop"},
+ {XBMCK_REWIND, 0, 0, XBMCVK_MEDIA_REWIND, "rewind"},
+ {XBMCK_FASTFORWARD, 0, 0, XBMCVK_MEDIA_FASTFORWARD, "fastforward"},
+ {XBMCK_RECORD, 0, 0, XBMCVK_MEDIA_RECORD, "record"}
+
+ // Function keys
+ ,
+ {XBMCK_F1, 0, 0, XBMCVK_F1, "f1"},
+ {XBMCK_F2, 0, 0, XBMCVK_F2, "f2"},
+ {XBMCK_F3, 0, 0, XBMCVK_F3, "f3"},
+ {XBMCK_F4, 0, 0, XBMCVK_F4, "f4"},
+ {XBMCK_F5, 0, 0, XBMCVK_F5, "f5"},
+ {XBMCK_F6, 0, 0, XBMCVK_F6, "f6"},
+ {XBMCK_F7, 0, 0, XBMCVK_F7, "f7"},
+ {XBMCK_F8, 0, 0, XBMCVK_F8, "f8"},
+ {XBMCK_F9, 0, 0, XBMCVK_F9, "f9"},
+ {XBMCK_F10, 0, 0, XBMCVK_F10, "f10"},
+ {XBMCK_F11, 0, 0, XBMCVK_F11, "f11"},
+ {XBMCK_F12, 0, 0, XBMCVK_F12, "f12"},
+ {XBMCK_F13, 0, 0, XBMCVK_F13, "f13"},
+ {XBMCK_F14, 0, 0, XBMCVK_F14, "f14"},
+ {XBMCK_F15, 0, 0, XBMCVK_F15, "f15"}
+
+ // Misc non-printing keys
+ ,
+ {XBMCK_UP, 0, 0, XBMCVK_UP, "up"},
+ {XBMCK_DOWN, 0, 0, XBMCVK_DOWN, "down"},
+ {XBMCK_RIGHT, 0, 0, XBMCVK_RIGHT, "right"},
+ {XBMCK_LEFT, 0, 0, XBMCVK_LEFT, "left"},
+ {XBMCK_INSERT, 0, 0, XBMCVK_INSERT, "insert"},
+ {XBMCK_DELETE, 0, 0, XBMCVK_DELETE, "delete"},
+ {XBMCK_HOME, 0, 0, XBMCVK_HOME, "home"},
+ {XBMCK_END, 0, 0, XBMCVK_END, "end"},
+ {XBMCK_PAGEUP, 0, 0, XBMCVK_PAGEUP, "pageup"},
+ {XBMCK_PAGEDOWN, 0, 0, XBMCVK_PAGEDOWN, "pagedown"},
+ {XBMCK_NUMLOCK, 0, 0, XBMCVK_NUMLOCK, "numlock"},
+ {XBMCK_CAPSLOCK, 0, 0, XBMCVK_CAPSLOCK, "capslock"},
+ {XBMCK_RSHIFT, 0, 0, XBMCVK_RSHIFT, "rightshift"},
+ {XBMCK_LSHIFT, 0, 0, XBMCVK_LSHIFT, "leftshift"},
+ {XBMCK_RCTRL, 0, 0, XBMCVK_RCONTROL, "rightctrl"},
+ {XBMCK_LCTRL, 0, 0, XBMCVK_LCONTROL, "leftctrl"},
+ {XBMCK_LALT, 0, 0, XBMCVK_LMENU, "leftalt"},
+ {XBMCK_LSUPER, 0, 0, XBMCVK_LWIN, "leftwindows"},
+ {XBMCK_RSUPER, 0, 0, XBMCVK_RWIN, "rightwindows"},
+ {XBMCK_MENU, 0, 0, XBMCVK_MENU, "menu"},
+ {XBMCK_PAUSE, 0, 0, XBMCVK_PAUSE, "pause"},
+ {XBMCK_SCROLLOCK, 0, 0, XBMCVK_SCROLLLOCK, "scrolllock"},
+ {XBMCK_PRINT, 0, 0, XBMCVK_PRINTSCREEN, "printscreen"},
+ {XBMCK_POWER, 0, 0, XBMCVK_POWER, "power"},
+ {XBMCK_SLEEP, 0, 0, XBMCVK_SLEEP, "sleep"},
+ {XBMCK_GUIDE, 0, 0, XBMCVK_GUIDE, "guide"},
+ {XBMCK_SETTINGS, 0, 0, XBMCVK_SETTINGS, "settings"},
+ {XBMCK_INFO, 0, 0, XBMCVK_INFO, "info"},
+ {XBMCK_RED, 0, 0, XBMCVK_RED, "red"},
+ {XBMCK_GREEN, 0, 0, XBMCVK_GREEN, "green"},
+ {XBMCK_YELLOW, 0, 0, XBMCVK_YELLOW, "yellow"},
+ {XBMCK_BLUE, 0, 0, XBMCVK_BLUE, "blue"},
+ {XBMCK_ZOOM, 0, 0, XBMCVK_ZOOM, "zoom"},
+ {XBMCK_TEXT, 0, 0, XBMCVK_TEXT, "text"},
+ {XBMCK_FAVORITES, 0, 0, XBMCVK_FAVORITES, "favorites"},
+ {XBMCK_HOMEPAGE, 0, 0, XBMCVK_HOMEPAGE, "homepage"},
+ {XBMCK_CONFIG, 0, 0, XBMCVK_CONFIG, "config"},
+ {XBMCK_EPG, 0, 0, XBMCVK_EPG, "epg"}};
+
+static int XBMCKeyTableSize = sizeof(XBMCKeyTable) / sizeof(XBMCKEYTABLE);
+
+bool KeyTableLookupName(std::string keyname, XBMCKEYTABLE* keytable)
+{
+ // If the name being searched for is empty there will be no match
+ if (keyname.empty())
+ return false;
+
+ // We need the button name to be in lowercase
+ StringUtils::ToLower(keyname);
+
+ // Look up the key name in XBMCKeyTable
+ for (int i = 0; i < XBMCKeyTableSize; i++)
+ {
+ if (XBMCKeyTable[i].keyname)
+ {
+ if (strcmp(keyname.c_str(), XBMCKeyTable[i].keyname) == 0)
+ {
+ *keytable = XBMCKeyTable[i];
+ return true;
+ }
+ }
+ }
+
+ // The name wasn't found
+ return false;
+}
+
+bool KeyTableLookupSym(uint16_t sym, XBMCKEYTABLE* keytable)
+{
+ // If the sym being searched for is zero there will be no match
+ if (sym == 0)
+ return false;
+
+ // Look up the sym in XBMCKeyTable
+ for (int i = 0; i < XBMCKeyTableSize; i++)
+ {
+ if (sym == XBMCKeyTable[i].sym)
+ {
+ *keytable = XBMCKeyTable[i];
+ return true;
+ }
+ }
+
+ // The name wasn't found
+ return false;
+}
+
+bool KeyTableLookupUnicode(uint16_t unicode, XBMCKEYTABLE* keytable)
+{
+ // If the unicode being searched for is zero there will be no match
+ if (unicode == 0)
+ return false;
+
+ // Look up the unicode in XBMCKeyTable
+ for (int i = 0; i < XBMCKeyTableSize; i++)
+ {
+ if (unicode == XBMCKeyTable[i].unicode)
+ {
+ *keytable = XBMCKeyTable[i];
+ return true;
+ }
+ }
+
+ // The name wasn't found
+ return false;
+}
+
+bool KeyTableLookupSymAndUnicode(uint16_t sym, uint16_t unicode, XBMCKEYTABLE* keytable)
+{
+ // If the sym being searched for is zero there will be no match (the
+ // unicode can be zero if the sym is non-zero)
+ if (sym == 0)
+ return false;
+
+ // Look up the sym and unicode in XBMCKeyTable
+ for (int i = 0; i < XBMCKeyTableSize; i++)
+ {
+ if (sym == XBMCKeyTable[i].sym && unicode == XBMCKeyTable[i].unicode)
+ {
+ *keytable = XBMCKeyTable[i];
+ return true;
+ }
+ }
+
+ // The sym and unicode weren't found
+ return false;
+}
+
+bool KeyTableLookupVKeyName(uint32_t vkey, XBMCKEYTABLE* keytable)
+{
+ // If the vkey being searched for is zero there will be no match
+ if (vkey == 0)
+ return false;
+
+ // Look up the vkey in XBMCKeyTable
+ for (int i = 0; i < XBMCKeyTableSize; i++)
+ {
+ if (vkey == XBMCKeyTable[i].vkey && XBMCKeyTable[i].keyname)
+ {
+ *keytable = XBMCKeyTable[i];
+ return true;
+ }
+ }
+
+ // The name wasn't found
+ return false;
+}
diff --git a/xbmc/input/XBMC_keytable.h b/xbmc/input/XBMC_keytable.h
new file mode 100644
index 0000000..1a4990e
--- /dev/null
+++ b/xbmc/input/XBMC_keytable.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2007-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 <stdint.h>
+#include <string>
+
+typedef struct struct_XBMCKEYTABLE
+{
+
+ // The sym is a value that identifies which key was pressed. Note
+ // that it specifies the key not the character so it is unaffected by
+ // shift, control, etc.
+ uint16_t sym;
+
+ // If the keypress generates a printing character the unicode and
+ // ascii member variables contain the character generated. If the
+ // key is a non-printing character, e.g. a function or arrow key,
+ // the unicode and ascii member variables are zero.
+ uint16_t unicode;
+ char ascii;
+
+ // The following two member variables are used to specify the
+ // action/function assigned to a key.
+ // The keynames are used as tags in keyboard.xml. When reading keyboard.xml
+ // TranslateKeyboardString uses the keyname to look up the vkey, and
+ // this is used in the mapping table.
+ uint32_t vkey;
+ const char* keyname;
+
+} XBMCKEYTABLE;
+
+bool KeyTableLookupName(std::string keyname, XBMCKEYTABLE* keytable);
+bool KeyTableLookupSym(uint16_t sym, XBMCKEYTABLE* keytable);
+bool KeyTableLookupUnicode(uint16_t unicode, XBMCKEYTABLE* keytable);
+bool KeyTableLookupSymAndUnicode(uint16_t sym, uint16_t unicode, XBMCKEYTABLE* keytable);
+bool KeyTableLookupVKeyName(uint32_t vkey, XBMCKEYTABLE* keytable);
diff --git a/xbmc/input/XBMC_vkeys.h b/xbmc/input/XBMC_vkeys.h
new file mode 100644
index 0000000..833d2da
--- /dev/null
+++ b/xbmc/input/XBMC_vkeys.h
@@ -0,0 +1,273 @@
+/*
+ * SDL - Simple DirectMedia Layer
+ * Copyright (C) 1997-2009 Sam Lantinga
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ *
+ * Sam Lantinga
+ * slouken@libsdl.org
+ */
+
+#pragma once
+
+// The XBMC_vkey identifies a key that is mapped to an action or function.
+// The keysym.sym generated by SDL_KEYDOWN is mapped to a vkey and the vkey
+// is used to lookup an action in the global mapping table.
+// The vkey values are the ASCII code of the character where this is possible.
+// Non-printing keypresses get a value in the range 0x80 - 0xFF.
+// Note that the vkey is a byte value so it cannot be greater than 0xFF.
+
+typedef enum
+{
+ XBMCVK_BACK = 0x08,
+ XBMCVK_TAB = 0x09,
+ XBMCVK_RETURN = 0x0D,
+ XBMCVK_ESCAPE = 0x1B,
+
+ XBMCVK_SPACE = 0x20,
+ XBMCVK_EXCLAIM = 0x21,
+ XBMCVK_QUOTEDBL = 0x22,
+ XBMCVK_HASH = 0x23,
+ XBMCVK_DOLLAR = 0x24,
+ XBMCVK_PERCENT = 0x25,
+ XBMCVK_AMPERSAND = 0x26,
+ XBMCVK_QUOTE = 0x27,
+ XBMCVK_LEFTPAREN = 0x28,
+ XBMCVK_RIGHTPAREN = 0x29,
+ XBMCVK_ASTERISK = 0x2A,
+ XBMCVK_PLUS = 0x2B,
+ XBMCVK_COMMA = 0x2C,
+ XBMCVK_MINUS = 0x2D,
+ XBMCVK_PERIOD = 0x2E,
+ XBMCVK_SLASH = 0x2F,
+
+ XBMCVK_0 = 0x30,
+ XBMCVK_1 = 0x31,
+ XBMCVK_2 = 0x32,
+ XBMCVK_3 = 0x33,
+ XBMCVK_4 = 0x34,
+ XBMCVK_5 = 0x35,
+ XBMCVK_6 = 0x36,
+ XBMCVK_7 = 0x37,
+ XBMCVK_8 = 0x38,
+ XBMCVK_9 = 0x39,
+
+ XBMCVK_COLON = 0x3A,
+ XBMCVK_SEMICOLON = 0x3B,
+ XBMCVK_LESS = 0x3C,
+ XBMCVK_EQUALS = 0x3D,
+ XBMCVK_GREATER = 0x3E,
+ XBMCVK_QUESTION = 0x3F,
+ XBMCVK_AT = 0x40,
+
+ XBMCVK_A = 0x41,
+ XBMCVK_B = 0x42,
+ XBMCVK_C = 0x43,
+ XBMCVK_D = 0x44,
+ XBMCVK_E = 0x45,
+ XBMCVK_F = 0x46,
+ XBMCVK_G = 0x47,
+ XBMCVK_H = 0x48,
+ XBMCVK_I = 0x49,
+ XBMCVK_J = 0x4A,
+ XBMCVK_K = 0x4B,
+ XBMCVK_L = 0x4C,
+ XBMCVK_M = 0x4D,
+ XBMCVK_N = 0x4E,
+ XBMCVK_O = 0x4F,
+ XBMCVK_P = 0x50,
+ XBMCVK_Q = 0x51,
+ XBMCVK_R = 0x52,
+ XBMCVK_S = 0x53,
+ XBMCVK_T = 0x54,
+ XBMCVK_U = 0x55,
+ XBMCVK_V = 0x56,
+ XBMCVK_W = 0x57,
+ XBMCVK_X = 0x58,
+ XBMCVK_Y = 0x59,
+ XBMCVK_Z = 0x5A,
+
+ XBMCVK_LEFTBRACKET = 0x5B,
+ XBMCVK_BACKSLASH = 0x5C,
+ XBMCVK_RIGHTBRACKET = 0x5D,
+ XBMCVK_CARET = 0x5E,
+ XBMCVK_UNDERSCORE = 0x5F,
+ XBMCVK_BACKQUOTE = 0x60,
+
+ // Lowercase letters 0x61 - 0x7a have the same vkey as uppercase, so
+ // use this block for the numpad keys
+ XBMCVK_NUMPADDIVIDE = 0x61,
+ XBMCVK_NUMPADTIMES = 0x62,
+ XBMCVK_NUMPADMINUS = 0x63,
+ XBMCVK_NUMPADPLUS = 0x64,
+ XBMCVK_NUMPADENTER = 0x65,
+ XBMCVK_NUMPADPERIOD = 0x66,
+ XBMCVK_NUMPAD0 = 0x70,
+ XBMCVK_NUMPAD1 = 0x71,
+ XBMCVK_NUMPAD2 = 0x72,
+ XBMCVK_NUMPAD3 = 0x73,
+ XBMCVK_NUMPAD4 = 0x74,
+ XBMCVK_NUMPAD5 = 0x75,
+ XBMCVK_NUMPAD6 = 0x76,
+ XBMCVK_NUMPAD7 = 0x77,
+ XBMCVK_NUMPAD8 = 0x78,
+ XBMCVK_NUMPAD9 = 0x79,
+
+ XBMCVK_LEFTBRACE = 0x7B,
+ XBMCVK_PIPE = 0x7C,
+ XBMCVK_RIGHTBRACE = 0x7D,
+ XBMCVK_TILDE = 0x7E,
+
+ // Non-printing characters
+
+ XBMCVK_UP = 0x80,
+ XBMCVK_DOWN = 0x81,
+ XBMCVK_LEFT = 0x82,
+ XBMCVK_RIGHT = 0x83,
+ XBMCVK_PAGEUP = 0x84,
+ XBMCVK_PAGEDOWN = 0x85,
+ XBMCVK_INSERT = 0x86,
+ XBMCVK_DELETE = 0x87,
+ XBMCVK_HOME = 0x88,
+ XBMCVK_END = 0x89,
+
+ XBMCVK_F1 = 0x90,
+ XBMCVK_F2 = 0x91,
+ XBMCVK_F3 = 0x92,
+ XBMCVK_F4 = 0x93,
+ XBMCVK_F5 = 0x94,
+ XBMCVK_F6 = 0x95,
+ XBMCVK_F7 = 0x96,
+ XBMCVK_F8 = 0x97,
+ XBMCVK_F9 = 0x98,
+ XBMCVK_F10 = 0x99,
+ XBMCVK_F11 = 0x9A,
+ XBMCVK_F12 = 0x9B,
+ XBMCVK_F13 = 0x9C,
+ XBMCVK_F14 = 0x9D,
+ XBMCVK_F15 = 0x9E,
+ XBMCVK_F16 = 0x9F,
+ XBMCVK_F17 = 0xA0,
+ XBMCVK_F18 = 0xA1,
+ XBMCVK_F19 = 0xA2,
+ XBMCVK_F20 = 0xA3,
+ XBMCVK_F21 = 0xA4,
+ XBMCVK_F22 = 0xA5,
+ XBMCVK_F23 = 0xA6,
+ XBMCVK_F24 = 0xA7,
+
+ XBMCVK_BROWSER_BACK = 0xB0,
+ XBMCVK_BROWSER_FORWARD = 0xB1,
+ XBMCVK_BROWSER_REFRESH = 0xB2,
+ XBMCVK_BROWSER_STOP = 0xB3,
+ XBMCVK_BROWSER_SEARCH = 0xB4,
+ XBMCVK_BROWSER_FAVORITES = 0xB5,
+ XBMCVK_BROWSER_HOME = 0xB6,
+ XBMCVK_VOLUME_MUTE = 0xB7,
+ XBMCVK_VOLUME_DOWN = 0xB8,
+ XBMCVK_VOLUME_UP = 0xB9,
+ XBMCVK_MEDIA_NEXT_TRACK = 0xBA,
+ XBMCVK_MEDIA_PREV_TRACK = 0xBB,
+ XBMCVK_MEDIA_STOP = 0xBC,
+ XBMCVK_MEDIA_PLAY_PAUSE = 0xBD,
+ XBMCVK_LAUNCH_MAIL = 0xBE,
+ XBMCVK_LAUNCH_MEDIA_SELECT = 0xBF,
+ XBMCVK_LAUNCH_APP1 = 0xC0,
+ XBMCVK_LAUNCH_APP2 = 0xC1,
+ XBMCVK_LAUNCH_FILE_BROWSER = 0xC2,
+ XBMCVK_LAUNCH_MEDIA_CENTER = 0xC3,
+ XBMCVK_MEDIA_REWIND = 0xC4,
+ XBMCVK_MEDIA_FASTFORWARD = 0xC5,
+ XBMCVK_MEDIA_RECORD = 0xC6,
+
+ XBMCVK_LCONTROL = 0xD0,
+ XBMCVK_RCONTROL = 0xD1,
+ XBMCVK_LSHIFT = 0xD2,
+ XBMCVK_RSHIFT = 0xD3,
+ XBMCVK_LMENU = 0xD4,
+ XBMCVK_RMENU = 0xD5,
+ XBMCVK_LWIN = 0xD6,
+ XBMCVK_RWIN = 0xD7,
+ XBMCVK_MENU = 0xD8,
+ XBMCVK_CAPSLOCK = 0xD9,
+ XBMCVK_NUMLOCK = 0xDA,
+
+ XBMCVK_PRINTSCREEN = 0xDB,
+ XBMCVK_SCROLLLOCK = 0xDC,
+ XBMCVK_PAUSE = 0XDD,
+ XBMCVK_POWER = 0XDE,
+ XBMCVK_SLEEP = 0XDF,
+ XBMCVK_GUIDE = 0xE0,
+ XBMCVK_SETTINGS = 0xE1,
+ XBMCVK_INFO = 0xE2,
+ XBMCVK_RED = 0xE3,
+ XBMCVK_GREEN = 0xE4,
+ XBMCVK_YELLOW = 0xE5,
+ XBMCVK_BLUE = 0xE6,
+ XBMCVK_ZOOM = 0xE7,
+ XBMCVK_TEXT = 0xE8,
+ XBMCVK_FAVORITES = 0xE9,
+ XBMCVK_HOMEPAGE = 0xEA,
+ XBMCVK_CONFIG = 0xEB,
+ XBMCVK_EPG = 0xEC,
+
+ XBMCVK_LAST = 0xFF
+} XBMCVKey;
+
+// These should be in winuser.h. Not sure why they have been defined here
+#ifndef VK_0
+#define VK_0 '0'
+#define VK_1 '1'
+#define VK_2 '2'
+#define VK_3 '3'
+#define VK_4 '4'
+#define VK_5 '5'
+#define VK_6 '6'
+#define VK_7 '7'
+#define VK_8 '8'
+#define VK_9 '9'
+#define VK_A 'A'
+#define VK_B 'B'
+#define VK_C 'C'
+#define VK_D 'D'
+#define VK_E 'E'
+#define VK_F 'F'
+#define VK_G 'G'
+#define VK_H 'H'
+#define VK_I 'I'
+#define VK_J 'J'
+#define VK_K 'K'
+#define VK_L 'L'
+#define VK_M 'M'
+#define VK_N 'N'
+#define VK_O 'O'
+#define VK_P 'P'
+#define VK_Q 'Q'
+#define VK_R 'R'
+#define VK_S 'S'
+#define VK_T 'T'
+#define VK_U 'U'
+#define VK_V 'V'
+#define VK_W 'W'
+#define VK_X 'X'
+#define VK_Y 'Y'
+#define VK_Z 'Z'
+#endif /* VK_0 */
+
+/* These keys haven't been defined, but were experimentally determined */
+#ifndef VK_SEMICOLON
+#define VK_SEMICOLON 0xBA
+#define VK_EQUALS 0xBB
+#define VK_COMMA 0xBC
+#define VK_MINUS 0xBD
+#define VK_PERIOD 0xBE
+#define VK_SLASH 0xBF
+#define VK_GRAVE 0xC0
+#define VK_LBRACKET 0xDB
+#define VK_BACKSLASH 0xDC
+#define VK_RBRACKET 0xDD
+#define VK_APOSTROPHE 0xDE
+#define VK_BACKTICK 0xDF
+#define VK_OEM_102 0xE2
+#endif
diff --git a/xbmc/input/actions/Action.cpp b/xbmc/input/actions/Action.cpp
new file mode 100644
index 0000000..8449e42
--- /dev/null
+++ b/xbmc/input/actions/Action.cpp
@@ -0,0 +1,154 @@
+/*
+ * 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 "Action.h"
+
+#include "ActionIDs.h"
+#include "ActionTranslator.h"
+#include "input/Key.h"
+
+CAction::CAction() : m_id(ACTION_NONE)
+{
+}
+
+CAction::CAction(int actionID,
+ float amount1 /* = 1.0f */,
+ float amount2 /* = 0.0f */,
+ const std::string& name /* = "" */,
+ unsigned int holdTime /*= 0*/)
+ : m_name(name)
+{
+ m_id = actionID;
+ m_amount[0] = amount1;
+ m_amount[1] = amount2;
+ m_repeat = 0;
+ m_buttonCode = 0;
+ m_unicode = 0;
+ m_holdTime = holdTime;
+}
+
+CAction::CAction(int actionID,
+ unsigned int state,
+ float posX,
+ float posY,
+ float offsetX,
+ float offsetY,
+ float velocityX,
+ float velocityY,
+ const std::string& name)
+ : m_name(name)
+{
+ m_id = actionID;
+ m_amount[0] = posX;
+ m_amount[1] = posY;
+ m_amount[2] = offsetX;
+ m_amount[3] = offsetY;
+ m_amount[4] = velocityX;
+ m_amount[5] = velocityY;
+ m_repeat = 0;
+ m_buttonCode = 0;
+ m_unicode = 0;
+ m_holdTime = state;
+}
+
+CAction::CAction(int actionID, wchar_t unicode)
+{
+ m_id = actionID;
+ m_repeat = 0;
+ m_buttonCode = 0;
+ m_unicode = unicode;
+ m_holdTime = 0;
+}
+
+CAction::CAction(int actionID, const std::string& name, const CKey& key) : m_name(name)
+{
+ m_id = actionID;
+ m_amount[0] = 1; // digital button (could change this for repeat acceleration)
+ m_repeat = key.GetRepeat();
+ m_buttonCode = key.GetButtonCode();
+ m_unicode = key.GetUnicode();
+ m_holdTime = key.GetHeld();
+ // get the action amounts of the analog buttons
+ if (key.GetButtonCode() == KEY_BUTTON_LEFT_ANALOG_TRIGGER)
+ m_amount[0] = (float)key.GetLeftTrigger() / 255.0f;
+ else if (key.GetButtonCode() == KEY_BUTTON_RIGHT_ANALOG_TRIGGER)
+ m_amount[0] = (float)key.GetRightTrigger() / 255.0f;
+ else if (key.GetButtonCode() == KEY_BUTTON_LEFT_THUMB_STICK)
+ {
+ m_amount[0] = key.GetLeftThumbX();
+ m_amount[1] = key.GetLeftThumbY();
+ }
+ else if (key.GetButtonCode() == KEY_BUTTON_RIGHT_THUMB_STICK)
+ {
+ m_amount[0] = key.GetRightThumbX();
+ m_amount[1] = key.GetRightThumbY();
+ }
+ else if (key.GetButtonCode() == KEY_BUTTON_LEFT_THUMB_STICK_UP)
+ m_amount[0] = key.GetLeftThumbY();
+ else if (key.GetButtonCode() == KEY_BUTTON_LEFT_THUMB_STICK_DOWN)
+ m_amount[0] = -key.GetLeftThumbY();
+ else if (key.GetButtonCode() == KEY_BUTTON_LEFT_THUMB_STICK_LEFT)
+ m_amount[0] = -key.GetLeftThumbX();
+ else if (key.GetButtonCode() == KEY_BUTTON_LEFT_THUMB_STICK_RIGHT)
+ m_amount[0] = key.GetLeftThumbX();
+ else if (key.GetButtonCode() == KEY_BUTTON_RIGHT_THUMB_STICK_UP)
+ m_amount[0] = key.GetRightThumbY();
+ else if (key.GetButtonCode() == KEY_BUTTON_RIGHT_THUMB_STICK_DOWN)
+ m_amount[0] = -key.GetRightThumbY();
+ else if (key.GetButtonCode() == KEY_BUTTON_RIGHT_THUMB_STICK_LEFT)
+ m_amount[0] = -key.GetRightThumbX();
+ else if (key.GetButtonCode() == KEY_BUTTON_RIGHT_THUMB_STICK_RIGHT)
+ m_amount[0] = key.GetRightThumbX();
+}
+
+CAction::CAction(int actionID, const std::string& name) : m_name(name)
+{
+ m_id = actionID;
+ m_repeat = 0;
+ m_buttonCode = 0;
+ m_unicode = 0;
+ m_holdTime = 0;
+}
+
+CAction& CAction::operator=(const CAction& rhs)
+{
+ if (this != &rhs)
+ {
+ m_id = rhs.m_id;
+ for (unsigned int i = 0; i < max_amounts; i++)
+ m_amount[i] = rhs.m_amount[i];
+ m_name = rhs.m_name;
+ m_repeat = rhs.m_repeat;
+ m_buttonCode = rhs.m_buttonCode;
+ m_unicode = rhs.m_unicode;
+ m_holdTime = rhs.m_holdTime;
+ m_text = rhs.m_text;
+ }
+ return *this;
+}
+
+void CAction::ClearAmount()
+{
+ for (float& amount : m_amount)
+ amount = 0;
+}
+
+bool CAction::IsMouse() const
+{
+ return (m_id >= ACTION_MOUSE_START && m_id <= ACTION_MOUSE_END);
+}
+
+bool CAction::IsGesture() const
+{
+ return (m_id >= ACTION_GESTURE_NOTIFY && m_id <= ACTION_GESTURE_END);
+}
+
+bool CAction::IsAnalog() const
+{
+ return CActionTranslator::IsAnalog(m_id);
+}
diff --git a/xbmc/input/actions/Action.h b/xbmc/input/actions/Action.h
new file mode 100644
index 0000000..f1d4173
--- /dev/null
+++ b/xbmc/input/actions/Action.h
@@ -0,0 +1,123 @@
+/*
+ * 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 <string>
+
+#ifndef SWIG
+
+class CKey;
+
+/*!
+ \ingroup actionkeys
+ \brief class encapsulating information regarding a particular user action to be sent to windows
+ and controls
+ */
+class CAction
+{
+public:
+ CAction();
+ CAction(int actionID,
+ float amount1 = 1.0f,
+ float amount2 = 0.0f,
+ const std::string& name = "",
+ unsigned int holdTime = 0);
+ CAction(int actionID, wchar_t unicode);
+ CAction(int actionID,
+ unsigned int state,
+ float posX,
+ float posY,
+ float offsetX,
+ float offsetY,
+ float velocityX = 0.0f,
+ float velocityY = 0.0f,
+ const std::string& name = "");
+ CAction(int actionID, const std::string& name, const CKey& key);
+ CAction(int actionID, const std::string& name);
+
+ CAction(const CAction& other) { *this = other; }
+ CAction& operator=(const CAction& rhs);
+
+ /*! \brief Identifier of the action
+ \return id of the action
+ */
+ int GetID() const { return m_id; }
+
+ /*! \brief Is this an action from the mouse
+ \return true if this is a mouse action, false otherwise
+ */
+ bool IsMouse() const;
+
+ bool IsGesture() const;
+
+ /*! \brief Human-readable name of the action
+ \return name of the action
+ */
+ const std::string& GetName() const { return m_name; }
+
+ /*! \brief Text of the action if any
+ \return text payload of this action.
+ */
+ const std::string& GetText() const { return m_text; }
+
+ /*! \brief Set the text payload of the action
+ \param text to be set
+ */
+ void SetText(const std::string& text) { m_text = text; }
+
+ /*! \brief Get an amount associated with this action
+ \param zero-based index of amount to retrieve, defaults to 0
+ \return an amount associated with this action
+ */
+ float GetAmount(unsigned int index = 0) const
+ {
+ return (index < max_amounts) ? m_amount[index] : 0;
+ };
+
+ /*! \brief Reset all amount values to zero
+ */
+ void ClearAmount();
+
+ /*! \brief Unicode value associated with this action
+ \return unicode value associated with this action, for keyboard input.
+ */
+ wchar_t GetUnicode() const { return m_unicode; }
+
+ /*! \brief Time in ms that the key has been held
+ \return time that the key has been held down in ms.
+ */
+ unsigned int GetHoldTime() const { return m_holdTime; }
+
+ /*! \brief Time since last repeat in ms
+ \return time since last repeat in ms. Returns 0 if unknown.
+ */
+ float GetRepeat() const { return m_repeat; }
+
+ /*! \brief Button code that triggered this action
+ \return button code
+ */
+ unsigned int GetButtonCode() const { return m_buttonCode; }
+
+ bool IsAnalog() const;
+
+private:
+ int m_id;
+ std::string m_name;
+
+ static const unsigned int max_amounts = 6; // Must be at least 6
+ float m_amount[max_amounts] = {};
+
+ float m_repeat = 0.0f;
+ unsigned int m_holdTime = 0;
+ unsigned int m_buttonCode = 0;
+ wchar_t m_unicode = 0;
+ std::string m_text;
+};
+
+#endif
diff --git a/xbmc/input/actions/ActionIDs.h b/xbmc/input/actions/ActionIDs.h
new file mode 100644
index 0000000..50fa662
--- /dev/null
+++ b/xbmc/input/actions/ActionIDs.h
@@ -0,0 +1,493 @@
+/*
+ * 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
+
+/**
+ * \defgroup kodi_key_action_ids Action Id's
+ * \ingroup python_xbmcgui_window_cb
+ * \ingroup python_xbmcgui_action
+ * @{
+ * @brief Actions that we have defined.
+ */
+
+constexpr const int ACTION_NONE = 0;
+constexpr const int ACTION_MOVE_LEFT = 1;
+constexpr const int ACTION_MOVE_RIGHT = 2;
+constexpr const int ACTION_MOVE_UP = 3;
+constexpr const int ACTION_MOVE_DOWN = 4;
+constexpr const int ACTION_PAGE_UP = 5;
+constexpr const int ACTION_PAGE_DOWN = 6;
+constexpr const int ACTION_SELECT_ITEM = 7;
+constexpr const int ACTION_HIGHLIGHT_ITEM = 8;
+constexpr const int ACTION_PARENT_DIR = 9;
+constexpr const int ACTION_PREVIOUS_MENU = 10;
+constexpr const int ACTION_SHOW_INFO = 11;
+
+constexpr const int ACTION_PAUSE = 12;
+constexpr const int ACTION_STOP = 13;
+constexpr const int ACTION_NEXT_ITEM = 14;
+constexpr const int ACTION_PREV_ITEM = 15;
+
+//! Can be used to specify specific action in a window, Playback control is
+//! handled in ACTION_PLAYER_*
+constexpr const int ACTION_FORWARD = 16;
+
+//! Can be used to specify specific action in a window, Playback control is
+//! handled in ACTION_PLAYER_*
+constexpr const int ACTION_REWIND = 17;
+
+//! Toggle between GUI and movie or GUI and visualisation.
+constexpr const int ACTION_SHOW_GUI = 18;
+
+//! Toggle quick-access zoom modes. Can be used in videoFullScreen.zml window id=2005
+constexpr const int ACTION_ASPECT_RATIO = 19;
+
+//! Seek +1% in the movie. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_STEP_FORWARD = 20;
+
+//! Seek -1% in the movie. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_STEP_BACK = 21;
+
+//! Seek +10% in the movie. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_BIG_STEP_FORWARD = 22;
+
+//! Seek -10% in the movie. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_BIG_STEP_BACK = 23;
+
+//! Show/hide OSD. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_SHOW_OSD = 24;
+
+//! Turn subtitles on/off. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_SHOW_SUBTITLES = 25;
+
+//! Switch to next subtitle of movie. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_NEXT_SUBTITLE = 26;
+
+//! Show debug info for VideoPlayer
+constexpr const int ACTION_PLAYER_DEBUG = 27;
+
+//! Show next picture of slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_NEXT_PICTURE = 28;
+
+//! Show previous picture of slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_PREV_PICTURE = 29;
+
+//! Zoom in picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_OUT = 30;
+
+//! Zoom out picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_IN = 31;
+
+//! Used to toggle between source view and destination view. Can be used in
+//! myfiles.xml window < id=3
+constexpr const int ACTION_TOGGLE_SOURCE_DEST = 32;
+
+//! Used to toggle between current view and playlist view. Can be used in all mymusic xml files
+constexpr const int ACTION_SHOW_PLAYLIST = 33;
+
+//! Used to queue a item to the playlist. Can be used in all mymusic xml files
+constexpr const int ACTION_QUEUE_ITEM = 34;
+
+//! Not used anymore
+constexpr const int ACTION_REMOVE_ITEM = 35;
+
+//! Not used anymore
+constexpr const int ACTION_SHOW_FULLSCREEN = 36;
+
+//! Zoom 1x picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_LEVEL_NORMAL = 37;
+
+//! Zoom 2x picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_LEVEL_1 = 38;
+
+//! Zoom 3x picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_LEVEL_2 = 39;
+
+//! Zoom 4x picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_LEVEL_3 = 40;
+
+//! Zoom 5x picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_LEVEL_4 = 41;
+
+//! Zoom 6x picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_LEVEL_5 = 42;
+
+//! Zoom 7x picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_LEVEL_6 = 43;
+
+//! Zoom 8x picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_LEVEL_7 = 44;
+
+//! Zoom 9x picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_LEVEL_8 = 45;
+
+//< Zoom 10x picture during slideshow. Can be used in slideshow.xml window id=2007
+constexpr const int ACTION_ZOOM_LEVEL_9 = 46;
+
+//< Select next arrow. Can be used in: settingsScreenCalibration.xml windowid=11
+constexpr const int ACTION_CALIBRATE_SWAP_ARROWS = 47;
+
+//! Reset calibration to defaults. Can be used in: `settingsScreenCalibration.xml`
+//! windowid=11/settingsUICalibration.xml windowid=10
+constexpr const int ACTION_CALIBRATE_RESET = 48;
+
+//! Analog thumbstick move. Can be used in: `slideshow.xml`
+//! windowid=2007/settingsScreenCalibration.xml windowid=11/settingsUICalibration.xml
+//! windowid=10
+//! @note see also ACTION_ANALOG_MOVE_X_LEFT, ACTION_ANALOG_MOVE_X_RIGHT,
+//! ACTION_ANALOG_MOVE_Y_UP, ACTION_ANALOG_MOVE_Y_DOWN
+constexpr const int ACTION_ANALOG_MOVE = 49;
+
+//! Rotate current picture clockwise during slideshow. Can be used in
+//! slideshow.xml window < id=2007
+constexpr const int ACTION_ROTATE_PICTURE_CW = 50;
+
+//! Rotate current picture counterclockwise during slideshow. Can be used in
+//! slideshow.xml window id=2007
+constexpr const int ACTION_ROTATE_PICTURE_CCW = 51;
+
+//! Decrease subtitle/movie Delay. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_SUBTITLE_DELAY_MIN = 52;
+
+//! Increase subtitle/movie Delay. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_SUBTITLE_DELAY_PLUS = 53;
+
+//! Increase avsync delay. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_AUDIO_DELAY_MIN = 54;
+
+//! Decrease avsync delay. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_AUDIO_DELAY_PLUS = 55;
+
+//! Select next language in movie. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_AUDIO_NEXT_LANGUAGE = 56;
+
+//! Switch 2 next resolution. Can b used during screen calibration
+//! settingsScreenCalibration.xml windowid=11
+constexpr const int ACTION_CHANGE_RESOLUTION = 57;
+
+//! Remote keys 0-9 are used by multiple windows.
+//!
+//! For example, in videoFullScreen.xml window id=2005 you can enter
+//! time (mmss) to jump to particular point in the movie.
+//!
+//! With spincontrols you can enter a 3-digit number to quickly set the
+//! spincontrol to desired value.
+//!@{
+constexpr const int REMOTE_0 = 58;
+
+//! @see REMOTE_0 about details.
+constexpr const int REMOTE_1 = 59;
+
+//! @see REMOTE_0 about details.
+constexpr const int REMOTE_2 = 60;
+
+//! @see REMOTE_0 about details.
+constexpr const int REMOTE_3 = 61;
+
+//! @see REMOTE_0 about details.
+constexpr const int REMOTE_4 = 62;
+
+//! @see REMOTE_0 about details.
+constexpr const int REMOTE_5 = 63;
+
+//! @see REMOTE_0 about details.
+constexpr const int REMOTE_6 = 64;
+
+//! @see REMOTE_0 about details.
+constexpr const int REMOTE_7 = 65;
+
+//! @see REMOTE_0 about details.
+constexpr const int REMOTE_8 = 66;
+
+//! @see REMOTE_0 about details.
+constexpr const int REMOTE_9 = 67;
+//!@}
+
+//! Show player process info (video decoder, pixel format, pvr signal strength
+//! and the like
+constexpr const int ACTION_PLAYER_PROCESS_INFO = 69;
+
+constexpr const int ACTION_PLAYER_PROGRAM_SELECT = 70;
+
+constexpr const int ACTION_PLAYER_RESOLUTION_SELECT = 71;
+
+//! Jumps a few seconds back during playback of movie. Can be used in videoFullScreen.xml
+//! window id=2005
+constexpr const int ACTION_SMALL_STEP_BACK = 76;
+
+//! FF in current file played. global action, can be used anywhere
+constexpr const int ACTION_PLAYER_FORWARD = 77;
+
+//! RW in current file played. global action, can be used anywhere
+constexpr const int ACTION_PLAYER_REWIND = 78;
+
+//! Play current song. Unpauses song and sets playspeed to 1x. global action,
+//! can be used anywhere
+constexpr const int ACTION_PLAYER_PLAY = 79;
+
+//! Delete current selected item. Can be used in myfiles.xml window id=3 and in
+//! myvideoTitle.xml window id=25
+constexpr const int ACTION_DELETE_ITEM = 80;
+
+//! Copy current selected item. Can be used in myfiles.xml window id=3
+constexpr const int ACTION_COPY_ITEM = 81;
+
+//! Move current selected item. Can be used in myfiles.xml window id=3
+constexpr const int ACTION_MOVE_ITEM = 82;
+
+//! Take a screenshot
+constexpr const int ACTION_TAKE_SCREENSHOT = 85;
+
+//! Rename item
+constexpr const int ACTION_RENAME_ITEM = 87;
+
+constexpr const int ACTION_VOLUME_UP = 88;
+constexpr const int ACTION_VOLUME_DOWN = 89;
+constexpr const int ACTION_VOLAMP = 90;
+constexpr const int ACTION_MUTE = 91;
+constexpr const int ACTION_NAV_BACK = 92;
+constexpr const int ACTION_VOLAMP_UP = 93;
+constexpr const int ACTION_VOLAMP_DOWN = 94;
+
+//! Creates an episode bookmark on the currently playing video file containing
+//! more than one episode
+constexpr const int ACTION_CREATE_EPISODE_BOOKMARK = 95;
+
+//! Creates a bookmark of the currently playing video file
+constexpr const int ACTION_CREATE_BOOKMARK = 96;
+
+//! Goto the next chapter, if not available perform a big step forward
+constexpr const int ACTION_CHAPTER_OR_BIG_STEP_FORWARD = 97;
+
+//! Goto the previous chapter, if not available perform a big step back
+constexpr const int ACTION_CHAPTER_OR_BIG_STEP_BACK = 98;
+
+//! Switch to next subtitle of movie, but will not enable/disable the subtitles.
+//! Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_CYCLE_SUBTITLE = 99;
+
+constexpr const int ACTION_MOUSE_START = 100;
+constexpr const int ACTION_MOUSE_LEFT_CLICK = 100;
+constexpr const int ACTION_MOUSE_RIGHT_CLICK = 101;
+constexpr const int ACTION_MOUSE_MIDDLE_CLICK = 102;
+constexpr const int ACTION_MOUSE_DOUBLE_CLICK = 103;
+constexpr const int ACTION_MOUSE_WHEEL_UP = 104;
+constexpr const int ACTION_MOUSE_WHEEL_DOWN = 105;
+constexpr const int ACTION_MOUSE_DRAG = 106;
+constexpr const int ACTION_MOUSE_MOVE = 107;
+constexpr const int ACTION_MOUSE_LONG_CLICK = 108;
+constexpr const int ACTION_MOUSE_DRAG_END = 109;
+constexpr const int ACTION_MOUSE_END = 109;
+
+constexpr const int ACTION_BACKSPACE = 110;
+constexpr const int ACTION_SCROLL_UP = 111;
+constexpr const int ACTION_SCROLL_DOWN = 112;
+constexpr const int ACTION_ANALOG_FORWARD = 113;
+constexpr const int ACTION_ANALOG_REWIND = 114;
+
+constexpr const int ACTION_MOVE_ITEM_UP = 115; //!< move item up in playlist
+constexpr const int ACTION_MOVE_ITEM_DOWN = 116; //!< move item down in playlist
+constexpr const int ACTION_CONTEXT_MENU = 117; //!< pops up the context menu
+
+// stuff for virtual keyboard shortcuts
+constexpr const int ACTION_SHIFT = 118; //!< stuff for virtual keyboard shortcuts
+constexpr const int ACTION_SYMBOLS = 119; //!< stuff for virtual keyboard shortcuts
+constexpr const int ACTION_CURSOR_LEFT = 120; //!< stuff for virtual keyboard shortcuts
+constexpr const int ACTION_CURSOR_RIGHT = 121; //!< stuff for virtual keyboard shortcuts
+
+constexpr const int ACTION_BUILT_IN_FUNCTION = 122;
+
+//! Displays current time, can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_SHOW_OSD_TIME = 123;
+
+constexpr const int ACTION_ANALOG_SEEK_FORWARD = 124; //!< seeks forward, and displays the seek bar.
+constexpr const int ACTION_ANALOG_SEEK_BACK = 125; //!< seeks backward, and displays the seek bar.
+
+constexpr const int ACTION_VIS_PRESET_SHOW = 126;
+constexpr const int ACTION_VIS_PRESET_NEXT = 128;
+constexpr const int ACTION_VIS_PRESET_PREV = 129;
+constexpr const int ACTION_VIS_PRESET_LOCK = 130;
+constexpr const int ACTION_VIS_PRESET_RANDOM = 131;
+constexpr const int ACTION_VIS_RATE_PRESET_PLUS = 132;
+constexpr const int ACTION_VIS_RATE_PRESET_MINUS = 133;
+
+constexpr const int ACTION_SHOW_VIDEOMENU = 134;
+constexpr const int ACTION_ENTER = 135;
+
+constexpr const int ACTION_INCREASE_RATING = 136;
+constexpr const int ACTION_DECREASE_RATING = 137;
+
+constexpr const int ACTION_NEXT_SCENE = 138; //!< switch to next scene/cutpoint in movie
+constexpr const int ACTION_PREV_SCENE = 139; //!< switch to previous scene/cutpoint in movie
+
+constexpr const int ACTION_NEXT_LETTER = 140; //!< jump through a list or container by letter
+constexpr const int ACTION_PREV_LETTER = 141;
+
+constexpr const int ACTION_JUMP_SMS2 = 142; //!< jump direct to a particular letter using SMS-style input
+constexpr const int ACTION_JUMP_SMS3 = 143;
+constexpr const int ACTION_JUMP_SMS4 = 144;
+constexpr const int ACTION_JUMP_SMS5 = 145;
+constexpr const int ACTION_JUMP_SMS6 = 146;
+constexpr const int ACTION_JUMP_SMS7 = 147;
+constexpr const int ACTION_JUMP_SMS8 = 148;
+constexpr const int ACTION_JUMP_SMS9 = 149;
+
+constexpr const int ACTION_FILTER_CLEAR = 150;
+constexpr const int ACTION_FILTER_SMS2 = 151;
+constexpr const int ACTION_FILTER_SMS3 = 152;
+constexpr const int ACTION_FILTER_SMS4 = 153;
+constexpr const int ACTION_FILTER_SMS5 = 154;
+constexpr const int ACTION_FILTER_SMS6 = 155;
+constexpr const int ACTION_FILTER_SMS7 = 156;
+constexpr const int ACTION_FILTER_SMS8 = 157;
+constexpr const int ACTION_FILTER_SMS9 = 158;
+
+constexpr const int ACTION_FIRST_PAGE = 159;
+constexpr const int ACTION_LAST_PAGE = 160;
+
+constexpr const int ACTION_AUDIO_DELAY = 161;
+constexpr const int ACTION_SUBTITLE_DELAY = 162;
+constexpr const int ACTION_MENU = 163;
+
+constexpr const int ACTION_SET_RATING = 164;
+
+constexpr const int ACTION_RECORD = 170;
+
+constexpr const int ACTION_PASTE = 180;
+constexpr const int ACTION_NEXT_CONTROL = 181;
+constexpr const int ACTION_PREV_CONTROL = 182;
+constexpr const int ACTION_CHANNEL_SWITCH = 183;
+constexpr const int ACTION_CHANNEL_UP = 184;
+constexpr const int ACTION_CHANNEL_DOWN = 185;
+constexpr const int ACTION_NEXT_CHANNELGROUP = 186;
+constexpr const int ACTION_PREVIOUS_CHANNELGROUP = 187;
+constexpr const int ACTION_PVR_PLAY = 188;
+constexpr const int ACTION_PVR_PLAY_TV = 189;
+constexpr const int ACTION_PVR_PLAY_RADIO = 190;
+constexpr const int ACTION_PVR_SHOW_TIMER_RULE = 191;
+constexpr const int ACTION_CHANNEL_NUMBER_SEP = 192;
+constexpr const int ACTION_PVR_ANNOUNCE_REMINDERS = 193;
+
+constexpr const int ACTION_TOGGLE_FULLSCREEN = 199; //!< switch 2 desktop resolution
+constexpr const int ACTION_TOGGLE_WATCHED = 200; //!< Toggle watched status (videos)
+constexpr const int ACTION_SCAN_ITEM = 201; //!< scan item
+constexpr const int ACTION_TOGGLE_DIGITAL_ANALOG = 202; //!< switch digital <-> analog
+constexpr const int ACTION_RELOAD_KEYMAPS = 203; //!< reloads CButtonTranslator's keymaps
+constexpr const int ACTION_GUIPROFILE_BEGIN = 204; //!< start the GUIControlProfiler running
+
+constexpr const int ACTION_TELETEXT_RED = 215; //!< Teletext Color button <b>Red</b> to control TopText
+constexpr const int ACTION_TELETEXT_GREEN = 216; //!< Teletext Color button <b>Green</b> to control TopText
+constexpr const int ACTION_TELETEXT_YELLOW = 217; //!< Teletext Color button <b>Yellow</b> to control TopText
+constexpr const int ACTION_TELETEXT_BLUE = 218; //!< Teletext Color button <b>Blue</b> to control TopText
+
+constexpr const int ACTION_INCREASE_PAR = 219;
+constexpr const int ACTION_DECREASE_PAR = 220;
+
+constexpr const int ACTION_VSHIFT_UP = 227; //!< shift up video image in VideoPlayer
+constexpr const int ACTION_VSHIFT_DOWN = 228; //!< shift down video image in VideoPlayer
+
+constexpr const int ACTION_PLAYER_PLAYPAUSE = 229; //!< Play/pause. If playing it pauses, if paused it plays.
+
+constexpr const int ACTION_SUBTITLE_VSHIFT_UP = 230; //!< shift up subtitles in VideoPlayer
+constexpr const int ACTION_SUBTITLE_VSHIFT_DOWN = 231; //!< shift down subtitles in VideoPlayer
+constexpr const int ACTION_SUBTITLE_ALIGN = 232; //!< toggle vertical alignment of subtitles
+
+constexpr const int ACTION_FILTER = 233;
+
+constexpr const int ACTION_SWITCH_PLAYER = 234;
+
+constexpr const int ACTION_STEREOMODE_NEXT = 235;
+constexpr const int ACTION_STEREOMODE_PREVIOUS = 236;
+constexpr const int ACTION_STEREOMODE_TOGGLE = 237; //!< turns 3d mode on/off
+constexpr const int ACTION_STEREOMODE_SELECT = 238;
+constexpr const int ACTION_STEREOMODE_TOMONO = 239;
+constexpr const int ACTION_STEREOMODE_SET = 240;
+
+constexpr const int ACTION_SETTINGS_RESET = 241;
+constexpr const int ACTION_SETTINGS_LEVEL_CHANGE = 242;
+
+//! Show autoclosing OSD. Can be used in videoFullScreen.xml window id=2005
+constexpr const int ACTION_TRIGGER_OSD = 243;
+constexpr const int ACTION_INPUT_TEXT = 244;
+constexpr const int ACTION_VOLUME_SET = 245;
+constexpr const int ACTION_TOGGLE_COMMSKIP = 246;
+
+constexpr const int ACTION_BROWSE_SUBTITLE = 247; //!< Browse for subtitle. Can be used in videofullscreen
+
+constexpr const int ACTION_PLAYER_RESET = 248; //!< Send a reset command to the active game
+
+constexpr const int ACTION_TOGGLE_FONT = 249; //!< Toggle font. Used in TextViewer dialog
+
+constexpr const int ACTION_VIDEO_NEXT_STREAM = 250; //!< Cycle video streams. Used in videofullscreen.
+
+//! Used to queue an item to the next position in the playlist
+constexpr const int ACTION_QUEUE_ITEM_NEXT = 251;
+
+constexpr const int ACTION_HDR_TOGGLE = 260; //!< Toggle display HDR on/off
+
+constexpr const int ACTION_CYCLE_TONEMAP_METHOD = 261; //!< Switch to next tonemap method
+
+//! Show debug info for video (source format, metadata, shaders, render flags and output format)
+constexpr const int ACTION_PLAYER_DEBUG_VIDEO = 262;
+
+// Voice actions
+constexpr const int ACTION_VOICE_RECOGNIZE = 300;
+
+// Touch actions
+constexpr const int ACTION_TOUCH_TAP = 401; //!< touch actions
+constexpr const int ACTION_TOUCH_TAP_TEN = 410; //!< touch actions
+constexpr const int ACTION_TOUCH_LONGPRESS = 411; //!< touch actions
+constexpr const int ACTION_TOUCH_LONGPRESS_TEN = 420; //!< touch actions
+
+constexpr const int ACTION_GESTURE_NOTIFY = 500;
+constexpr const int ACTION_GESTURE_BEGIN = 501;
+
+//! sendaction with point and currentPinchScale (fingers together < 1.0 ->
+//! fingers apart > 1.0)
+constexpr const int ACTION_GESTURE_ZOOM = 502;
+constexpr const int ACTION_GESTURE_ROTATE = 503;
+constexpr const int ACTION_GESTURE_PAN = 504;
+constexpr const int ACTION_GESTURE_ABORT = 505; //!< gesture was interrupted in unspecified state
+
+constexpr const int ACTION_GESTURE_SWIPE_LEFT = 511;
+constexpr const int ACTION_GESTURE_SWIPE_LEFT_TEN = 520;
+constexpr const int ACTION_GESTURE_SWIPE_RIGHT = 521;
+constexpr const int ACTION_GESTURE_SWIPE_RIGHT_TEN = 530;
+constexpr const int ACTION_GESTURE_SWIPE_UP = 531;
+constexpr const int ACTION_GESTURE_SWIPE_UP_TEN = 540;
+constexpr const int ACTION_GESTURE_SWIPE_DOWN = 541;
+constexpr const int ACTION_GESTURE_SWIPE_DOWN_TEN = 550;
+
+//! 5xx is reserved for additional gesture actions
+constexpr const int ACTION_GESTURE_END = 599;
+
+/*!
+ * @brief Other, non-gesture actions
+ */
+///@{
+
+//!< analog thumbstick move, horizontal axis, left; see ACTION_ANALOG_MOVE
+constexpr const int ACTION_ANALOG_MOVE_X_LEFT = 601;
+
+//!< analog thumbstick move, horizontal axis, right; see ACTION_ANALOG_MOVE
+constexpr const int ACTION_ANALOG_MOVE_X_RIGHT = 602;
+
+//!< analog thumbstick move, vertical axis, up; see ACTION_ANALOG_MOVE
+constexpr const int ACTION_ANALOG_MOVE_Y_UP = 603;
+
+//!< analog thumbstick move, vertical axis, down; see ACTION_ANALOG_MOVE
+constexpr const int ACTION_ANALOG_MOVE_Y_DOWN = 604;
+
+///@}
+
+// The NOOP action can be specified to disable an input event. This is
+// useful in user keyboard.xml etc to disable actions specified in the
+// system mappings. ERROR action is used to play an error sound
+constexpr const int ACTION_ERROR = 998;
+constexpr const int ACTION_NOOP = 999;
diff --git a/xbmc/input/actions/ActionTranslator.cpp b/xbmc/input/actions/ActionTranslator.cpp
new file mode 100644
index 0000000..51d531c
--- /dev/null
+++ b/xbmc/input/actions/ActionTranslator.cpp
@@ -0,0 +1,311 @@
+/*
+ * 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 "ActionTranslator.h"
+
+#include "ActionIDs.h"
+#include "interfaces/builtins/Builtins.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <map>
+
+namespace
+{
+using ActionName = std::string;
+using ActionID = unsigned int;
+
+static const std::map<ActionName, ActionID> ActionMappings = {
+ {"left", ACTION_MOVE_LEFT},
+ {"right", ACTION_MOVE_RIGHT},
+ {"up", ACTION_MOVE_UP},
+ {"down", ACTION_MOVE_DOWN},
+ {"pageup", ACTION_PAGE_UP},
+ {"pagedown", ACTION_PAGE_DOWN},
+ {"select", ACTION_SELECT_ITEM},
+ {"highlight", ACTION_HIGHLIGHT_ITEM},
+ {"parentdir", ACTION_NAV_BACK}, // backward compatibility
+ {"parentfolder", ACTION_PARENT_DIR},
+ {"back", ACTION_NAV_BACK},
+ {"menu", ACTION_MENU},
+ {"previousmenu", ACTION_PREVIOUS_MENU},
+ {"info", ACTION_SHOW_INFO},
+ {"pause", ACTION_PAUSE},
+ {"stop", ACTION_STOP},
+ {"skipnext", ACTION_NEXT_ITEM},
+ {"skipprevious", ACTION_PREV_ITEM},
+ {"fullscreen", ACTION_SHOW_GUI},
+ {"aspectratio", ACTION_ASPECT_RATIO},
+ {"stepforward", ACTION_STEP_FORWARD},
+ {"stepback", ACTION_STEP_BACK},
+ {"bigstepforward", ACTION_BIG_STEP_FORWARD},
+ {"bigstepback", ACTION_BIG_STEP_BACK},
+ {"chapterorbigstepforward", ACTION_CHAPTER_OR_BIG_STEP_FORWARD},
+ {"chapterorbigstepback", ACTION_CHAPTER_OR_BIG_STEP_BACK},
+ {"osd", ACTION_SHOW_OSD},
+ {"showsubtitles", ACTION_SHOW_SUBTITLES},
+ {"nextsubtitle", ACTION_NEXT_SUBTITLE},
+ {"browsesubtitle", ACTION_BROWSE_SUBTITLE},
+ {"cyclesubtitle", ACTION_CYCLE_SUBTITLE},
+ {"playerdebug", ACTION_PLAYER_DEBUG},
+ {"playerdebugvideo", ACTION_PLAYER_DEBUG_VIDEO},
+ {"codecinfo", ACTION_PLAYER_PROCESS_INFO},
+ {"playerprocessinfo", ACTION_PLAYER_PROCESS_INFO},
+ {"playerprogramselect", ACTION_PLAYER_PROGRAM_SELECT},
+ {"playerresolutionselect", ACTION_PLAYER_RESOLUTION_SELECT},
+ {"nextpicture", ACTION_NEXT_PICTURE},
+ {"previouspicture", ACTION_PREV_PICTURE},
+ {"zoomout", ACTION_ZOOM_OUT},
+ {"zoomin", ACTION_ZOOM_IN},
+ {"playlist", ACTION_SHOW_PLAYLIST},
+ {"queue", ACTION_QUEUE_ITEM},
+ {"playnext", ACTION_QUEUE_ITEM_NEXT},
+ {"zoomnormal", ACTION_ZOOM_LEVEL_NORMAL},
+ {"zoomlevel1", ACTION_ZOOM_LEVEL_1},
+ {"zoomlevel2", ACTION_ZOOM_LEVEL_2},
+ {"zoomlevel3", ACTION_ZOOM_LEVEL_3},
+ {"zoomlevel4", ACTION_ZOOM_LEVEL_4},
+ {"zoomlevel5", ACTION_ZOOM_LEVEL_5},
+ {"zoomlevel6", ACTION_ZOOM_LEVEL_6},
+ {"zoomlevel7", ACTION_ZOOM_LEVEL_7},
+ {"zoomlevel8", ACTION_ZOOM_LEVEL_8},
+ {"zoomlevel9", ACTION_ZOOM_LEVEL_9},
+ {"nextcalibration", ACTION_CALIBRATE_SWAP_ARROWS},
+ {"resetcalibration", ACTION_CALIBRATE_RESET},
+ {"analogmove", ACTION_ANALOG_MOVE},
+ {"analogmovexleft", ACTION_ANALOG_MOVE_X_LEFT},
+ {"analogmovexright", ACTION_ANALOG_MOVE_X_RIGHT},
+ {"analogmoveyup", ACTION_ANALOG_MOVE_Y_UP},
+ {"analogmoveydown", ACTION_ANALOG_MOVE_Y_DOWN},
+ {"rotate", ACTION_ROTATE_PICTURE_CW},
+ {"rotateccw", ACTION_ROTATE_PICTURE_CCW},
+ {"close", ACTION_NAV_BACK}, // backwards compatibility
+ {"subtitledelayminus", ACTION_SUBTITLE_DELAY_MIN},
+ {"subtitledelay", ACTION_SUBTITLE_DELAY},
+ {"subtitledelayplus", ACTION_SUBTITLE_DELAY_PLUS},
+ {"audiodelayminus", ACTION_AUDIO_DELAY_MIN},
+ {"audiodelay", ACTION_AUDIO_DELAY},
+ {"audiodelayplus", ACTION_AUDIO_DELAY_PLUS},
+ {"subtitleshiftup", ACTION_SUBTITLE_VSHIFT_UP},
+ {"subtitleshiftdown", ACTION_SUBTITLE_VSHIFT_DOWN},
+ {"subtitlealign", ACTION_SUBTITLE_ALIGN},
+ {"audionextlanguage", ACTION_AUDIO_NEXT_LANGUAGE},
+ {"verticalshiftup", ACTION_VSHIFT_UP},
+ {"verticalshiftdown", ACTION_VSHIFT_DOWN},
+ {"nextresolution", ACTION_CHANGE_RESOLUTION},
+ {"audiotoggledigital", ACTION_TOGGLE_DIGITAL_ANALOG},
+ {"number0", REMOTE_0},
+ {"number1", REMOTE_1},
+ {"number2", REMOTE_2},
+ {"number3", REMOTE_3},
+ {"number4", REMOTE_4},
+ {"number5", REMOTE_5},
+ {"number6", REMOTE_6},
+ {"number7", REMOTE_7},
+ {"number8", REMOTE_8},
+ {"number9", REMOTE_9},
+ {"smallstepback", ACTION_SMALL_STEP_BACK},
+ {"fastforward", ACTION_PLAYER_FORWARD},
+ {"rewind", ACTION_PLAYER_REWIND},
+ {"play", ACTION_PLAYER_PLAY},
+ {"playpause", ACTION_PLAYER_PLAYPAUSE},
+ {"switchplayer", ACTION_SWITCH_PLAYER},
+ {"delete", ACTION_DELETE_ITEM},
+ {"copy", ACTION_COPY_ITEM},
+ {"move", ACTION_MOVE_ITEM},
+ {"screenshot", ACTION_TAKE_SCREENSHOT},
+ {"rename", ACTION_RENAME_ITEM},
+ {"togglewatched", ACTION_TOGGLE_WATCHED},
+ {"scanitem", ACTION_SCAN_ITEM},
+ {"reloadkeymaps", ACTION_RELOAD_KEYMAPS},
+ {"volumeup", ACTION_VOLUME_UP},
+ {"volumedown", ACTION_VOLUME_DOWN},
+ {"mute", ACTION_MUTE},
+ {"backspace", ACTION_BACKSPACE},
+ {"scrollup", ACTION_SCROLL_UP},
+ {"scrolldown", ACTION_SCROLL_DOWN},
+ {"analogfastforward", ACTION_ANALOG_FORWARD},
+ {"analogrewind", ACTION_ANALOG_REWIND},
+ {"moveitemup", ACTION_MOVE_ITEM_UP},
+ {"moveitemdown", ACTION_MOVE_ITEM_DOWN},
+ {"contextmenu", ACTION_CONTEXT_MENU},
+ {"shift", ACTION_SHIFT},
+ {"symbols", ACTION_SYMBOLS},
+ {"cursorleft", ACTION_CURSOR_LEFT},
+ {"cursorright", ACTION_CURSOR_RIGHT},
+ {"showtime", ACTION_SHOW_OSD_TIME},
+ {"analogseekforward", ACTION_ANALOG_SEEK_FORWARD},
+ {"analogseekback", ACTION_ANALOG_SEEK_BACK},
+ {"showpreset", ACTION_VIS_PRESET_SHOW},
+ {"nextpreset", ACTION_VIS_PRESET_NEXT},
+ {"previouspreset", ACTION_VIS_PRESET_PREV},
+ {"lockpreset", ACTION_VIS_PRESET_LOCK},
+ {"randompreset", ACTION_VIS_PRESET_RANDOM},
+ {"increasevisrating", ACTION_VIS_RATE_PRESET_PLUS},
+ {"decreasevisrating", ACTION_VIS_RATE_PRESET_MINUS},
+ {"showvideomenu", ACTION_SHOW_VIDEOMENU},
+ {"enter", ACTION_ENTER},
+ {"increaserating", ACTION_INCREASE_RATING},
+ {"decreaserating", ACTION_DECREASE_RATING},
+ {"setrating", ACTION_SET_RATING},
+ {"togglefullscreen", ACTION_TOGGLE_FULLSCREEN},
+ {"nextscene", ACTION_NEXT_SCENE},
+ {"previousscene", ACTION_PREV_SCENE},
+ {"nextletter", ACTION_NEXT_LETTER},
+ {"prevletter", ACTION_PREV_LETTER},
+ {"jumpsms2", ACTION_JUMP_SMS2},
+ {"jumpsms3", ACTION_JUMP_SMS3},
+ {"jumpsms4", ACTION_JUMP_SMS4},
+ {"jumpsms5", ACTION_JUMP_SMS5},
+ {"jumpsms6", ACTION_JUMP_SMS6},
+ {"jumpsms7", ACTION_JUMP_SMS7},
+ {"jumpsms8", ACTION_JUMP_SMS8},
+ {"jumpsms9", ACTION_JUMP_SMS9},
+ {"filter", ACTION_FILTER},
+ {"filterclear", ACTION_FILTER_CLEAR},
+ {"filtersms2", ACTION_FILTER_SMS2},
+ {"filtersms3", ACTION_FILTER_SMS3},
+ {"filtersms4", ACTION_FILTER_SMS4},
+ {"filtersms5", ACTION_FILTER_SMS5},
+ {"filtersms6", ACTION_FILTER_SMS6},
+ {"filtersms7", ACTION_FILTER_SMS7},
+ {"filtersms8", ACTION_FILTER_SMS8},
+ {"filtersms9", ACTION_FILTER_SMS9},
+ {"firstpage", ACTION_FIRST_PAGE},
+ {"lastpage", ACTION_LAST_PAGE},
+ {"guiprofile", ACTION_GUIPROFILE_BEGIN},
+ {"red", ACTION_TELETEXT_RED},
+ {"green", ACTION_TELETEXT_GREEN},
+ {"yellow", ACTION_TELETEXT_YELLOW},
+ {"blue", ACTION_TELETEXT_BLUE},
+ {"increasepar", ACTION_INCREASE_PAR},
+ {"decreasepar", ACTION_DECREASE_PAR},
+ {"volampup", ACTION_VOLAMP_UP},
+ {"volampdown", ACTION_VOLAMP_DOWN},
+ {"volumeamplification", ACTION_VOLAMP},
+ {"createbookmark", ACTION_CREATE_BOOKMARK},
+ {"createepisodebookmark", ACTION_CREATE_EPISODE_BOOKMARK},
+ {"settingsreset", ACTION_SETTINGS_RESET},
+ {"settingslevelchange", ACTION_SETTINGS_LEVEL_CHANGE},
+ {"togglefont", ACTION_TOGGLE_FONT},
+ {"videonextstream", ACTION_VIDEO_NEXT_STREAM},
+
+ // 3D movie playback/GUI
+ {"stereomode", ACTION_STEREOMODE_SELECT}, // cycle 3D modes, for now an alias for next
+ {"nextstereomode", ACTION_STEREOMODE_NEXT},
+ {"previousstereomode", ACTION_STEREOMODE_PREVIOUS},
+ {"togglestereomode", ACTION_STEREOMODE_TOGGLE},
+ {"stereomodetomono", ACTION_STEREOMODE_TOMONO},
+
+ // HDR display support
+ {"hdrtoggle", ACTION_HDR_TOGGLE},
+
+ // Tone mapping
+ {"cycletonemapmethod", ACTION_CYCLE_TONEMAP_METHOD},
+
+ // PVR actions
+ {"channelup", ACTION_CHANNEL_UP},
+ {"channeldown", ACTION_CHANNEL_DOWN},
+ {"previouschannelgroup", ACTION_PREVIOUS_CHANNELGROUP},
+ {"nextchannelgroup", ACTION_NEXT_CHANNELGROUP},
+ {"playpvr", ACTION_PVR_PLAY},
+ {"playpvrtv", ACTION_PVR_PLAY_TV},
+ {"playpvrradio", ACTION_PVR_PLAY_RADIO},
+ {"record", ACTION_RECORD},
+ {"togglecommskip", ACTION_TOGGLE_COMMSKIP},
+ {"showtimerrule", ACTION_PVR_SHOW_TIMER_RULE},
+ {"channelnumberseparator", ACTION_CHANNEL_NUMBER_SEP},
+
+ // Mouse actions
+ {"leftclick", ACTION_MOUSE_LEFT_CLICK},
+ {"rightclick", ACTION_MOUSE_RIGHT_CLICK},
+ {"middleclick", ACTION_MOUSE_MIDDLE_CLICK},
+ {"doubleclick", ACTION_MOUSE_DOUBLE_CLICK},
+ {"longclick", ACTION_MOUSE_LONG_CLICK},
+ {"wheelup", ACTION_MOUSE_WHEEL_UP},
+ {"wheeldown", ACTION_MOUSE_WHEEL_DOWN},
+ {"mousedrag", ACTION_MOUSE_DRAG},
+ {"mousedragend", ACTION_MOUSE_DRAG_END},
+ {"mousemove", ACTION_MOUSE_MOVE},
+
+ // Touch
+ {"tap", ACTION_TOUCH_TAP},
+ {"longpress", ACTION_TOUCH_LONGPRESS},
+ {"pangesture", ACTION_GESTURE_PAN},
+ {"zoomgesture", ACTION_GESTURE_ZOOM},
+ {"rotategesture", ACTION_GESTURE_ROTATE},
+ {"swipeleft", ACTION_GESTURE_SWIPE_LEFT},
+ {"swiperight", ACTION_GESTURE_SWIPE_RIGHT},
+ {"swipeup", ACTION_GESTURE_SWIPE_UP},
+ {"swipedown", ACTION_GESTURE_SWIPE_DOWN},
+
+ // Voice
+ {"voicerecognizer", ACTION_VOICE_RECOGNIZE},
+
+ // Do nothing / error action
+ {"error", ACTION_ERROR},
+ {"noop", ACTION_NOOP}};
+} // namespace
+
+void CActionTranslator::GetActions(std::vector<std::string>& actionList)
+{
+ actionList.reserve(ActionMappings.size());
+ for (auto& actionMapping : ActionMappings)
+ actionList.push_back(actionMapping.first);
+}
+
+bool CActionTranslator::IsAnalog(unsigned int actionID)
+{
+ switch (actionID)
+ {
+ case ACTION_ANALOG_SEEK_FORWARD:
+ case ACTION_ANALOG_SEEK_BACK:
+ case ACTION_SCROLL_UP:
+ case ACTION_SCROLL_DOWN:
+ case ACTION_ANALOG_FORWARD:
+ case ACTION_ANALOG_REWIND:
+ case ACTION_ANALOG_MOVE:
+ case ACTION_ANALOG_MOVE_X_LEFT:
+ case ACTION_ANALOG_MOVE_X_RIGHT:
+ case ACTION_ANALOG_MOVE_Y_UP:
+ case ACTION_ANALOG_MOVE_Y_DOWN:
+ case ACTION_CURSOR_LEFT:
+ case ACTION_CURSOR_RIGHT:
+ case ACTION_VOLUME_UP:
+ case ACTION_VOLUME_DOWN:
+ case ACTION_ZOOM_IN:
+ case ACTION_ZOOM_OUT:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool CActionTranslator::TranslateString(std::string strAction, unsigned int& actionId)
+{
+ actionId = ACTION_NONE;
+
+ if (strAction.empty())
+ return false;
+
+ StringUtils::ToLower(strAction);
+
+ auto it = ActionMappings.find(strAction);
+ if (it != ActionMappings.end())
+ actionId = it->second;
+ else if (CBuiltins::GetInstance().HasCommand(strAction))
+ actionId = ACTION_BUILT_IN_FUNCTION;
+
+ if (actionId == ACTION_NONE)
+ {
+ CLog::Log(LOGERROR, "Keymapping error: no such action '{}' defined", strAction);
+ return false;
+ }
+
+ return true;
+}
diff --git a/xbmc/input/actions/ActionTranslator.h b/xbmc/input/actions/ActionTranslator.h
new file mode 100644
index 0000000..91c146a
--- /dev/null
+++ b/xbmc/input/actions/ActionTranslator.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017-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 <string>
+#include <vector>
+
+class CActionTranslator
+{
+public:
+ static void GetActions(std::vector<std::string>& actionList);
+ static bool IsAnalog(unsigned int actionId);
+ static bool TranslateString(std::string strAction, unsigned int& actionId);
+};
diff --git a/xbmc/input/actions/CMakeLists.txt b/xbmc/input/actions/CMakeLists.txt
new file mode 100644
index 0000000..a7a4d87
--- /dev/null
+++ b/xbmc/input/actions/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(SOURCES Action.cpp
+ ActionTranslator.cpp
+)
+
+set(HEADERS Action.h
+ ActionIDs.h
+ ActionTranslator.h
+)
+
+core_add_library(input_actions)
diff --git a/xbmc/input/button/ButtonStat.cpp b/xbmc/input/button/ButtonStat.cpp
new file mode 100644
index 0000000..679947f
--- /dev/null
+++ b/xbmc/input/button/ButtonStat.cpp
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2020 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 "ButtonStat.h"
+
+#include "xbmc/input/InputTypes.h"
+
+using namespace KODI;
+using namespace INPUT;
+
+CButtonStat::CButtonStat() = default;
+
+CKey CButtonStat::TranslateKey(const CKey& key) const
+{
+ uint32_t buttonCode = key.GetButtonCode();
+ if (key.GetHeld() > HOLD_TRESHOLD)
+ buttonCode |= CKey::MODIFIER_LONG;
+
+ CKey translatedKey(buttonCode, key.GetHeld());
+ return translatedKey;
+}
diff --git a/xbmc/input/button/ButtonStat.h b/xbmc/input/button/ButtonStat.h
new file mode 100644
index 0000000..04ee51a
--- /dev/null
+++ b/xbmc/input/button/ButtonStat.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 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 "input/Key.h"
+#include "input/XBMC_keyboard.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace INPUT
+{
+
+class CButtonStat
+{
+public:
+ CButtonStat();
+ ~CButtonStat() = default;
+
+ CKey TranslateKey(const CKey& key) const;
+};
+} // namespace INPUT
+} // namespace KODI
diff --git a/xbmc/input/button/CMakeLists.txt b/xbmc/input/button/CMakeLists.txt
new file mode 100644
index 0000000..489f901
--- /dev/null
+++ b/xbmc/input/button/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES ButtonStat.cpp)
+
+set(HEADERS ButtonStat.h)
+
+core_add_library(input_button) \ No newline at end of file
diff --git a/xbmc/input/hardware/IHardwareInput.h b/xbmc/input/hardware/IHardwareInput.h
new file mode 100644
index 0000000..6bfa757
--- /dev/null
+++ b/xbmc/input/hardware/IHardwareInput.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017-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 <string>
+
+namespace KODI
+{
+namespace HARDWARE
+{
+/*!
+ * \ingroup hardware
+ * \brief Handles events for hardware such as reset buttons on a game console
+ */
+class IHardwareInput
+{
+public:
+ virtual ~IHardwareInput() = default;
+
+ /*!
+ * \brief A hardware reset button has been pressed
+ */
+ virtual void OnResetButton() = 0;
+};
+} // namespace HARDWARE
+} // namespace KODI
diff --git a/xbmc/input/joysticks/CMakeLists.txt b/xbmc/input/joysticks/CMakeLists.txt
new file mode 100644
index 0000000..0155e5e
--- /dev/null
+++ b/xbmc/input/joysticks/CMakeLists.txt
@@ -0,0 +1,30 @@
+set(SOURCES DeadzoneFilter.cpp
+ DriverPrimitive.cpp
+ JoystickEasterEgg.cpp
+ JoystickMonitor.cpp
+ JoystickTranslator.cpp
+ JoystickUtils.cpp
+ RumbleGenerator.cpp)
+
+set(HEADERS interfaces/IButtonMap.h
+ interfaces/IButtonMapCallback.h
+ interfaces/IButtonMapper.h
+ interfaces/IButtonSequence.h
+ interfaces/IDriverHandler.h
+ interfaces/IDriverReceiver.h
+ interfaces/IInputHandler.h
+ interfaces/IInputProvider.h
+ interfaces/IInputReceiver.h
+ interfaces/IKeyHandler.h
+ interfaces/IKeymapHandler.h
+ DeadzoneFilter.h
+ DriverPrimitive.h
+ JoystickEasterEgg.h
+ JoystickIDs.h
+ JoystickMonitor.h
+ JoystickTranslator.h
+ JoystickTypes.h
+ JoystickUtils.h
+ RumbleGenerator.h)
+
+core_add_library(input_joystick)
diff --git a/xbmc/input/joysticks/DeadzoneFilter.cpp b/xbmc/input/joysticks/DeadzoneFilter.cpp
new file mode 100644
index 0000000..c437f30
--- /dev/null
+++ b/xbmc/input/joysticks/DeadzoneFilter.cpp
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016-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 "DeadzoneFilter.h"
+
+#include "JoystickIDs.h"
+#include "games/controllers/ControllerIDs.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "peripherals/devices/Peripheral.h"
+#include "utils/log.h"
+
+#include <cmath>
+#include <vector>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+//! Allowed noise for detecting discrete D-pads (value of 0.007 when centered
+//! has been observed)
+#define AXIS_EPSILON 0.01f
+
+// Settings for analog sticks
+#define SETTING_LEFT_STICK_DEADZONE "left_stick_deadzone"
+#define SETTING_RIGHT_STICK_DEADZONE "right_stick_deadzone"
+
+CDeadzoneFilter::CDeadzoneFilter(IButtonMap* buttonMap, PERIPHERALS::CPeripheral* peripheral)
+ : m_buttonMap(buttonMap), m_peripheral(peripheral)
+{
+ if (m_buttonMap->ControllerID() != DEFAULT_CONTROLLER_ID)
+ CLog::Log(LOGERROR, "ERROR: Must use default controller profile instead of {}",
+ m_buttonMap->ControllerID());
+}
+
+float CDeadzoneFilter::FilterAxis(unsigned int axisIndex, float axisValue)
+{
+ float deadzone = 0.0f;
+
+ bool bSuccess =
+ GetDeadzone(axisIndex, deadzone, DEFAULT_LEFT_STICK_NAME, SETTING_LEFT_STICK_DEADZONE) ||
+ GetDeadzone(axisIndex, deadzone, DEFAULT_RIGHT_STICK_NAME, SETTING_RIGHT_STICK_DEADZONE);
+
+ if (bSuccess)
+ return ApplyDeadzone(axisValue, deadzone);
+
+ // Always filter noise about the center
+ if (std::abs(axisValue) < AXIS_EPSILON)
+ axisValue = 0.0f;
+
+ return axisValue;
+}
+
+bool CDeadzoneFilter::GetDeadzone(unsigned int axisIndex,
+ float& deadzone,
+ const char* featureName,
+ const char* settingName)
+{
+ std::vector<ANALOG_STICK_DIRECTION> dirs = {
+ ANALOG_STICK_DIRECTION::UP,
+ ANALOG_STICK_DIRECTION::RIGHT,
+ ANALOG_STICK_DIRECTION::DOWN,
+ ANALOG_STICK_DIRECTION::LEFT,
+ };
+
+ CDriverPrimitive primitive;
+ for (auto dir : dirs)
+ {
+ if (m_buttonMap->GetAnalogStick(featureName, dir, primitive))
+ {
+ if (primitive.Type() == PRIMITIVE_TYPE::SEMIAXIS && primitive.Index() == axisIndex)
+ {
+ deadzone = m_peripheral->GetSettingFloat(settingName);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+float CDeadzoneFilter::ApplyDeadzone(float value, float deadzone)
+{
+ if (deadzone < 0.0f || deadzone >= 1.0f)
+ return 0.0f;
+
+ if (value > deadzone)
+ return (value - deadzone) / (1.0f - deadzone);
+ else if (value < -deadzone)
+ return (value + deadzone) / (1.0f - deadzone);
+
+ return 0.0f;
+}
diff --git a/xbmc/input/joysticks/DeadzoneFilter.h b/xbmc/input/joysticks/DeadzoneFilter.h
new file mode 100644
index 0000000..4547050
--- /dev/null
+++ b/xbmc/input/joysticks/DeadzoneFilter.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016-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
+
+namespace PERIPHERALS
+{
+class CPeripheral;
+}
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IButtonMap;
+
+/*!
+ * \ingroup joystick
+ * \brief Analog axis deadzone filtering
+ *
+ * Axis is scaled appropriately, so position is continuous
+ * from -1.0 to 1.0:
+ *
+ * | / 1.0
+ * | /
+ * __|__/
+ * / |
+ * / |--| Deadzone
+ * -1.0 / |
+ *
+ * After deadzone filtering, the value will be:
+ *
+ * - Negative in the interval [-1.0, -deadzone)
+ * - Zero in the interval [-deadzone, deadzone]
+ * - Positive in the interval (deadzone, 1.0]
+ */
+class CDeadzoneFilter
+{
+public:
+ CDeadzoneFilter(IButtonMap* buttonMap, PERIPHERALS::CPeripheral* peripheral);
+
+ /*!
+ * \brief Apply deadzone filtering to an axis
+ * \param axisIndex The axis index
+ * \param axisValue The axis value
+ * \return The value after applying deadzone filtering
+ */
+ float FilterAxis(unsigned int axisIndex, float axisValue);
+
+private:
+ /*!
+ * \brief Get the deadzone value from the peripheral's settings
+ * \param axisIndex The axis index
+ * \param[out] result The deadzone value
+ * \param featureName The feature that axisIndex is mapped to
+ * \param settingName The setting corresponding to the given feature
+ * \return True if the feature is an analog stick and the peripheral has the setting
+ */
+ bool GetDeadzone(unsigned int axisIndex,
+ float& result,
+ const char* featureName,
+ const char* settingName);
+
+ /*!
+ * \brief Utility function to calculate the deadzone
+ * \param value The value
+ * \param deadzone The deadzone
+ * \return The scaled deadzone
+ */
+ static float ApplyDeadzone(float value, float deadzone);
+
+ // Construction parameters
+ IButtonMap* const m_buttonMap;
+ PERIPHERALS::CPeripheral* const m_peripheral;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/DriverPrimitive.cpp b/xbmc/input/joysticks/DriverPrimitive.cpp
new file mode 100644
index 0000000..166e926
--- /dev/null
+++ b/xbmc/input/joysticks/DriverPrimitive.cpp
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2014-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 "DriverPrimitive.h"
+
+#include "games/controllers/ControllerTranslator.h"
+#include "utils/StringUtils.h"
+
+#include <utility>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+CDriverPrimitive::CDriverPrimitive(void) = default;
+
+CDriverPrimitive::CDriverPrimitive(PRIMITIVE_TYPE type, unsigned int index)
+ : m_type(type), m_driverIndex(index)
+{
+}
+
+CDriverPrimitive::CDriverPrimitive(unsigned int hatIndex, HAT_DIRECTION direction)
+ : m_type(PRIMITIVE_TYPE::HAT), m_driverIndex(hatIndex), m_hatDirection(direction)
+{
+}
+
+CDriverPrimitive::CDriverPrimitive(unsigned int axisIndex,
+ int center,
+ SEMIAXIS_DIRECTION direction,
+ unsigned int range)
+ : m_type(PRIMITIVE_TYPE::SEMIAXIS),
+ m_driverIndex(axisIndex),
+ m_center(center),
+ m_semiAxisDirection(direction),
+ m_range(range)
+{
+}
+
+CDriverPrimitive::CDriverPrimitive(XBMCKey keycode)
+ : m_type(PRIMITIVE_TYPE::KEY), m_keycode(keycode)
+{
+}
+
+CDriverPrimitive::CDriverPrimitive(MOUSE::BUTTON_ID index)
+ : m_type(PRIMITIVE_TYPE::MOUSE_BUTTON), m_driverIndex(static_cast<unsigned int>(index))
+{
+}
+
+CDriverPrimitive::CDriverPrimitive(RELATIVE_POINTER_DIRECTION direction)
+ : m_type(PRIMITIVE_TYPE::RELATIVE_POINTER), m_pointerDirection(direction)
+{
+}
+
+bool CDriverPrimitive::operator==(const CDriverPrimitive& rhs) const
+{
+ if (m_type == rhs.m_type)
+ {
+ switch (m_type)
+ {
+ case PRIMITIVE_TYPE::BUTTON:
+ case PRIMITIVE_TYPE::MOTOR:
+ case PRIMITIVE_TYPE::MOUSE_BUTTON:
+ return m_driverIndex == rhs.m_driverIndex;
+ case PRIMITIVE_TYPE::HAT:
+ return m_driverIndex == rhs.m_driverIndex && m_hatDirection == rhs.m_hatDirection;
+ case PRIMITIVE_TYPE::SEMIAXIS:
+ return m_driverIndex == rhs.m_driverIndex && m_center == rhs.m_center &&
+ m_semiAxisDirection == rhs.m_semiAxisDirection && m_range == rhs.m_range;
+ case PRIMITIVE_TYPE::KEY:
+ return m_keycode == rhs.m_keycode;
+ case PRIMITIVE_TYPE::RELATIVE_POINTER:
+ return m_pointerDirection == rhs.m_pointerDirection;
+ default:
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CDriverPrimitive::operator<(const CDriverPrimitive& rhs) const
+{
+ if (m_type < rhs.m_type)
+ return true;
+ if (m_type > rhs.m_type)
+ return false;
+
+ if (m_type == PRIMITIVE_TYPE::BUTTON || m_type == PRIMITIVE_TYPE::HAT ||
+ m_type == PRIMITIVE_TYPE::SEMIAXIS || m_type == PRIMITIVE_TYPE::MOTOR ||
+ m_type == PRIMITIVE_TYPE::MOUSE_BUTTON)
+ {
+ if (m_driverIndex < rhs.m_driverIndex)
+ return true;
+ if (m_driverIndex > rhs.m_driverIndex)
+ return false;
+ }
+
+ if (m_type == PRIMITIVE_TYPE::HAT)
+ {
+ if (m_hatDirection < rhs.m_hatDirection)
+ return true;
+ if (m_hatDirection > rhs.m_hatDirection)
+ return false;
+ }
+
+ if (m_type == PRIMITIVE_TYPE::SEMIAXIS)
+ {
+ if (m_center < rhs.m_center)
+ return true;
+ if (m_center > rhs.m_center)
+ return false;
+
+ if (m_semiAxisDirection < rhs.m_semiAxisDirection)
+ return true;
+ if (m_semiAxisDirection > rhs.m_semiAxisDirection)
+ return false;
+
+ if (m_range < rhs.m_range)
+ return true;
+ if (m_range > rhs.m_range)
+ return false;
+ }
+
+ if (m_type == PRIMITIVE_TYPE::KEY)
+ {
+ if (m_keycode < rhs.m_keycode)
+ return true;
+ if (m_keycode > rhs.m_keycode)
+ return false;
+ }
+
+ if (m_type == PRIMITIVE_TYPE::RELATIVE_POINTER)
+ {
+ if (m_pointerDirection < rhs.m_pointerDirection)
+ return true;
+ if (m_pointerDirection > rhs.m_pointerDirection)
+ return false;
+ }
+
+ return false;
+}
+
+bool CDriverPrimitive::IsValid(void) const
+{
+ if (m_type == PRIMITIVE_TYPE::BUTTON || m_type == PRIMITIVE_TYPE::MOTOR ||
+ m_type == PRIMITIVE_TYPE::MOUSE_BUTTON)
+ return true;
+
+ if (m_type == PRIMITIVE_TYPE::HAT)
+ {
+ return m_hatDirection == HAT_DIRECTION::UP || m_hatDirection == HAT_DIRECTION::DOWN ||
+ m_hatDirection == HAT_DIRECTION::RIGHT || m_hatDirection == HAT_DIRECTION::LEFT;
+ }
+
+ if (m_type == PRIMITIVE_TYPE::SEMIAXIS)
+ {
+ unsigned int maxRange = 1;
+
+ switch (m_center)
+ {
+ case -1:
+ {
+ if (m_semiAxisDirection != SEMIAXIS_DIRECTION::POSITIVE)
+ return false;
+ maxRange = 2;
+ break;
+ }
+ case 0:
+ {
+ if (m_semiAxisDirection != SEMIAXIS_DIRECTION::POSITIVE &&
+ m_semiAxisDirection != SEMIAXIS_DIRECTION::NEGATIVE)
+ return false;
+ break;
+ }
+ case 1:
+ {
+ if (m_semiAxisDirection != SEMIAXIS_DIRECTION::POSITIVE)
+ return false;
+ maxRange = 2;
+ break;
+ }
+ default:
+ break;
+ }
+
+ return 1 <= m_range && m_range <= maxRange;
+ }
+
+ if (m_type == PRIMITIVE_TYPE::KEY)
+ return m_keycode != XBMCK_UNKNOWN;
+
+ if (m_type == PRIMITIVE_TYPE::RELATIVE_POINTER)
+ {
+ return m_pointerDirection == RELATIVE_POINTER_DIRECTION::UP ||
+ m_pointerDirection == RELATIVE_POINTER_DIRECTION::DOWN ||
+ m_pointerDirection == RELATIVE_POINTER_DIRECTION::RIGHT ||
+ m_pointerDirection == RELATIVE_POINTER_DIRECTION::LEFT;
+ }
+
+ return false;
+}
+
+std::string CDriverPrimitive::ToString() const
+{
+ switch (m_type)
+ {
+ case PRIMITIVE_TYPE::BUTTON:
+ return StringUtils::Format("button {}", m_driverIndex);
+ case PRIMITIVE_TYPE::MOTOR:
+ return StringUtils::Format("motor {}", m_driverIndex);
+ case PRIMITIVE_TYPE::MOUSE_BUTTON:
+ return StringUtils::Format("mouse button {}", m_driverIndex);
+ case PRIMITIVE_TYPE::HAT:
+ {
+ switch (m_hatDirection)
+ {
+ case HAT_DIRECTION::UP:
+ return StringUtils::Format("hat {} up", m_driverIndex);
+ case HAT_DIRECTION::DOWN:
+ return StringUtils::Format("hat {} down", m_driverIndex);
+ case HAT_DIRECTION::RIGHT:
+ return StringUtils::Format("hat {} right", m_driverIndex);
+ case HAT_DIRECTION::LEFT:
+ return StringUtils::Format("hat {} left", m_driverIndex);
+ default:
+ break;
+ }
+ break;
+ }
+ case PRIMITIVE_TYPE::SEMIAXIS:
+ {
+ switch (m_semiAxisDirection)
+ {
+ case SEMIAXIS_DIRECTION::POSITIVE:
+ return StringUtils::Format("semiaxis +{}", m_driverIndex);
+ case SEMIAXIS_DIRECTION::NEGATIVE:
+ return StringUtils::Format("semiaxis -{}", m_driverIndex);
+ default:
+ break;
+ }
+ break;
+ }
+ case PRIMITIVE_TYPE::KEY:
+ return StringUtils::Format("key {}",
+ GAME::CControllerTranslator::TranslateKeycode(m_keycode));
+ case PRIMITIVE_TYPE::RELATIVE_POINTER:
+ {
+ switch (m_pointerDirection)
+ {
+ case RELATIVE_POINTER_DIRECTION::UP:
+ return StringUtils::Format("pointer {} up", m_driverIndex);
+ case RELATIVE_POINTER_DIRECTION::DOWN:
+ return StringUtils::Format("pointer {} down", m_driverIndex);
+ case RELATIVE_POINTER_DIRECTION::RIGHT:
+ return StringUtils::Format("pointer {} right", m_driverIndex);
+ case RELATIVE_POINTER_DIRECTION::LEFT:
+ return StringUtils::Format("pointer {} left", m_driverIndex);
+ default:
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return "";
+}
diff --git a/xbmc/input/joysticks/DriverPrimitive.h b/xbmc/input/joysticks/DriverPrimitive.h
new file mode 100644
index 0000000..11d391b
--- /dev/null
+++ b/xbmc/input/joysticks/DriverPrimitive.h
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2014-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 "JoystickTypes.h"
+#include "input/keyboard/KeyboardTypes.h"
+#include "input/mouse/MouseTypes.h"
+
+#include <stdint.h>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+/*!
+ * \ingroup joystick
+ * \brief Basic driver element associated with input events
+ *
+ * Driver input (bools, floats and enums) is split into primitives that better
+ * map to the physical features on a joystick.
+ *
+ * A bool obviously only maps to a single feature, so it is a driver
+ * primitive. Here, these are called "buttons".
+ *
+ * A hat enum encodes the state of the four hat directions. Each direction
+ * can map to a different feature, so a hat enum consists of four driver
+ * primitives called "hat directions".
+ *
+ * A float is a little trickier. Trivially, it can map to an analog stick or
+ * trigger. However, DirectInput combines two triggers onto a single axis.
+ * Therefore, the axis is split into two primitives called "semiaxes".
+ *
+ * The type determines the fields in use:
+ *
+ * Button:
+ * - driver index
+ *
+ * Hat direction:
+ * - driver index
+ * - hat direction (up/right/down/left)
+ *
+ * Semiaxis:
+ * - driver index
+ * - center (-1, 0 or 1)
+ * - semiaxis direction (positive/negative)
+ * - range (1 or 2)
+ *
+ * Motor:
+ * - driver index
+ *
+ * Key:
+ * - keycode
+ *
+ * Mouse button:
+ * - driver index
+ *
+ * Relative pointer:
+ * - pointer direction
+ *
+ * For more info, see "Chapter 2. Joystick drivers" in the documentation
+ * thread: http://forum.kodi.tv/showthread.php?tid=257764
+ */
+class CDriverPrimitive
+{
+public:
+ /*!
+ * \brief Construct an invalid driver primitive
+ */
+ CDriverPrimitive(void);
+
+ /*!
+ * \brief Construct a driver primitive representing a button or motor
+ */
+ CDriverPrimitive(PRIMITIVE_TYPE type, unsigned int index);
+
+ /*!
+ * \brief Construct a driver primitive representing one of the four
+ * direction arrows on a dpad
+ */
+ CDriverPrimitive(unsigned int hatIndex, HAT_DIRECTION direction);
+
+ /*!
+ * \brief Construct a driver primitive representing the positive or negative
+ * half of an axis
+ */
+ CDriverPrimitive(unsigned int axisIndex,
+ int center,
+ SEMIAXIS_DIRECTION direction,
+ unsigned int range);
+
+ /*!
+ * \brief Construct a driver primitive representing a key on a keyboard
+ */
+ CDriverPrimitive(KEYBOARD::KeySymbol keycode);
+
+ /*!
+ * \brief Construct a driver primitive representing a mouse button
+ */
+ CDriverPrimitive(MOUSE::BUTTON_ID index);
+
+ /*!
+ * \brief Construct a driver primitive representing a relative pointer
+ */
+ CDriverPrimitive(RELATIVE_POINTER_DIRECTION direction);
+
+ bool operator==(const CDriverPrimitive& rhs) const;
+ bool operator<(const CDriverPrimitive& rhs) const;
+
+ bool operator!=(const CDriverPrimitive& rhs) const { return !operator==(rhs); }
+ bool operator>(const CDriverPrimitive& rhs) const { return !(operator<(rhs) || operator==(rhs)); }
+ bool operator<=(const CDriverPrimitive& rhs) const { return operator<(rhs) || operator==(rhs); }
+ bool operator>=(const CDriverPrimitive& rhs) const { return !operator<(rhs); }
+
+ /*!
+ * \brief The type of driver primitive
+ */
+ PRIMITIVE_TYPE Type(void) const { return m_type; }
+
+ /*!
+ * \brief The index used by the joystick driver
+ *
+ * Valid for:
+ * - buttons
+ * - hats
+ * - semiaxes
+ * - motors
+ */
+ unsigned int Index(void) const { return m_driverIndex; }
+
+ /*!
+ * \brief The direction arrow (valid for hat directions)
+ */
+ HAT_DIRECTION HatDirection(void) const { return m_hatDirection; }
+
+ /*!
+ * \brief The location of the zero point of the semiaxis
+ */
+ int Center() const { return m_center; }
+
+ /*!
+ * \brief The semiaxis direction (valid for semiaxes)
+ */
+ SEMIAXIS_DIRECTION SemiAxisDirection(void) const { return m_semiAxisDirection; }
+
+ /*!
+ * \brief The distance between the center and the farthest valid value (valid for semiaxes)
+ */
+ unsigned int Range() const { return m_range; }
+
+ /*!
+ * \brief The keyboard symbol (valid for keys)
+ */
+ KEYBOARD::KeySymbol Keycode() const { return m_keycode; }
+
+ /*!
+ * \brief The mouse button ID (valid for mouse buttons)
+ */
+ MOUSE::BUTTON_ID MouseButton() const { return static_cast<MOUSE::BUTTON_ID>(m_driverIndex); }
+
+ /*!
+ * \brief The relative pointer direction (valid for relative pointers)
+ */
+ RELATIVE_POINTER_DIRECTION PointerDirection() const { return m_pointerDirection; }
+
+ /*!
+ * \brief Test if an driver primitive is valid
+ *
+ * A driver primitive is valid if it has a known type and:
+ *
+ * 1) for hats, it is a cardinal direction
+ * 2) for semi-axes, it is a positive or negative direction
+ * 3) for keys, the keycode is non-empty
+ */
+ bool IsValid(void) const;
+
+ /*!
+ * \brief Convert primitive to a string suitable for logging
+ *
+ * \return The primitive as described by a short string, or empty if invalid
+ */
+ std::string ToString() const;
+
+private:
+ PRIMITIVE_TYPE m_type = PRIMITIVE_TYPE::UNKNOWN;
+ unsigned int m_driverIndex = 0;
+ HAT_DIRECTION m_hatDirection = HAT_DIRECTION::NONE;
+ int m_center = 0;
+ SEMIAXIS_DIRECTION m_semiAxisDirection = SEMIAXIS_DIRECTION::ZERO;
+ unsigned int m_range = 1;
+ KEYBOARD::KeySymbol m_keycode = XBMCK_UNKNOWN;
+ RELATIVE_POINTER_DIRECTION m_pointerDirection = RELATIVE_POINTER_DIRECTION::NONE;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/JoystickEasterEgg.cpp b/xbmc/input/joysticks/JoystickEasterEgg.cpp
new file mode 100644
index 0000000..b0259a6
--- /dev/null
+++ b/xbmc/input/joysticks/JoystickEasterEgg.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2016-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 "JoystickEasterEgg.h"
+
+#include "ServiceBroker.h"
+#include "games/GameServices.h"
+#include "games/GameSettings.h"
+#include "games/controllers/ControllerIDs.h"
+#include "games/controllers/DefaultController.h"
+#include "guilib/GUIAudioManager.h"
+#include "guilib/WindowIDs.h"
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+const std::map<std::string, std::vector<FeatureName>> CJoystickEasterEgg::m_sequence = {
+ {
+ DEFAULT_CONTROLLER_ID,
+ {
+ GAME::CDefaultController::FEATURE_UP,
+ GAME::CDefaultController::FEATURE_UP,
+ GAME::CDefaultController::FEATURE_DOWN,
+ GAME::CDefaultController::FEATURE_DOWN,
+ GAME::CDefaultController::FEATURE_LEFT,
+ GAME::CDefaultController::FEATURE_RIGHT,
+ GAME::CDefaultController::FEATURE_LEFT,
+ GAME::CDefaultController::FEATURE_RIGHT,
+ GAME::CDefaultController::FEATURE_B,
+ GAME::CDefaultController::FEATURE_A,
+ },
+ },
+ {
+ DEFAULT_REMOTE_ID,
+ {
+ "up",
+ "up",
+ "down",
+ "down",
+ "left",
+ "right",
+ "left",
+ "right",
+ "back",
+ "ok",
+ },
+ },
+};
+
+CJoystickEasterEgg::CJoystickEasterEgg(const std::string& controllerId)
+ : m_controllerId(controllerId), m_state(0)
+{
+}
+
+bool CJoystickEasterEgg::OnButtonPress(const FeatureName& feature)
+{
+ bool bHandled = false;
+
+ auto it = m_sequence.find(m_controllerId);
+ if (it != m_sequence.end())
+ {
+ const auto& sequence = it->second;
+
+ // Reset state if it previously finished
+ if (m_state >= sequence.size())
+ m_state = 0;
+
+ if (feature == sequence[m_state])
+ m_state++;
+ else
+ m_state = 0;
+
+ if (IsCapturing())
+ {
+ bHandled = true;
+
+ if (m_state >= sequence.size())
+ OnFinish();
+ }
+ }
+
+ return bHandled;
+}
+
+bool CJoystickEasterEgg::IsCapturing()
+{
+ // Capture input when finished with arrows (2 x up/down/left/right)
+ return m_state > 8;
+}
+
+void CJoystickEasterEgg::OnFinish(void)
+{
+ GAME::CGameSettings& gameSettings = CServiceBroker::GetGameServices().GameSettings();
+ gameSettings.ToggleGames();
+
+ WINDOW_SOUND sound = gameSettings.GamesEnabled() ? SOUND_INIT : SOUND_DEINIT;
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetAudioManager().PlayWindowSound(WINDOW_DIALOG_KAI_TOAST, sound);
+
+ //! @todo Shake screen
+}
diff --git a/xbmc/input/joysticks/JoystickEasterEgg.h b/xbmc/input/joysticks/JoystickEasterEgg.h
new file mode 100644
index 0000000..7059d13
--- /dev/null
+++ b/xbmc/input/joysticks/JoystickEasterEgg.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016-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 "input/joysticks/interfaces/IButtonSequence.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+/*!
+ * \brief Hush!!!
+ */
+class CJoystickEasterEgg : public IButtonSequence
+{
+public:
+ explicit CJoystickEasterEgg(const std::string& controllerId);
+ ~CJoystickEasterEgg() override = default;
+
+ // implementation of IButtonSequence
+ bool OnButtonPress(const FeatureName& feature) override;
+ bool IsCapturing() override;
+
+ static void OnFinish(void);
+
+private:
+ // Construction parameters
+ const std::string m_controllerId;
+
+ static const std::map<std::string, std::vector<FeatureName>> m_sequence;
+
+ unsigned int m_state;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/JoystickIDs.h b/xbmc/input/joysticks/JoystickIDs.h
new file mode 100644
index 0000000..52ffcf4
--- /dev/null
+++ b/xbmc/input/joysticks/JoystickIDs.h
@@ -0,0 +1,13 @@
+/*
+ * Copyright (C) 2017-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
+
+// Analog sticks on the default controller
+#define DEFAULT_LEFT_STICK_NAME "leftstick"
+#define DEFAULT_RIGHT_STICK_NAME "rightstick"
diff --git a/xbmc/input/joysticks/JoystickMonitor.cpp b/xbmc/input/joysticks/JoystickMonitor.cpp
new file mode 100644
index 0000000..6375a41
--- /dev/null
+++ b/xbmc/input/joysticks/JoystickMonitor.cpp
@@ -0,0 +1,111 @@
+/*
+ * 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 "JoystickMonitor.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "games/controllers/ControllerIDs.h"
+#include "input/InputManager.h"
+
+#include <cmath>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+#define AXIS_DEADZONE 0.05f
+
+std::string CJoystickMonitor::ControllerID() const
+{
+ return DEFAULT_CONTROLLER_ID;
+}
+
+bool CJoystickMonitor::AcceptsInput(const FeatureName& feature) const
+{
+ // Only accept input when screen saver is active
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ return appPower->IsInScreenSaver();
+}
+
+bool CJoystickMonitor::OnButtonPress(const FeatureName& feature, bool bPressed)
+{
+ if (bPressed)
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ return ResetTimers();
+ }
+
+ return false;
+}
+
+bool CJoystickMonitor::OnButtonMotion(const FeatureName& feature,
+ float magnitude,
+ unsigned int motionTimeMs)
+{
+ if (std::fabs(magnitude) > AXIS_DEADZONE)
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ return ResetTimers();
+ }
+
+ return false;
+}
+
+bool CJoystickMonitor::OnAnalogStickMotion(const FeatureName& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs)
+{
+ // Analog stick deadzone already processed
+ if (x != 0.0f || y != 0.0f)
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ return ResetTimers();
+ }
+
+ return false;
+}
+
+bool CJoystickMonitor::OnWheelMotion(const FeatureName& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ if (std::fabs(position) > AXIS_DEADZONE)
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ return ResetTimers();
+ }
+
+ return false;
+}
+
+bool CJoystickMonitor::OnThrottleMotion(const FeatureName& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ if (std::fabs(position) > AXIS_DEADZONE)
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ return ResetTimers();
+ }
+
+ return false;
+}
+
+bool CJoystickMonitor::ResetTimers(void)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetSystemIdleTimer();
+ appPower->ResetScreenSaver();
+ return appPower->WakeUpScreenSaverAndDPMS();
+
+ return true;
+}
diff --git a/xbmc/input/joysticks/JoystickMonitor.h b/xbmc/input/joysticks/JoystickMonitor.h
new file mode 100644
index 0000000..783fff1
--- /dev/null
+++ b/xbmc/input/joysticks/JoystickMonitor.h
@@ -0,0 +1,58 @@
+/*
+ * 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 "input/joysticks/interfaces/IInputHandler.h"
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+/*!
+ * \ingroup joystick
+ * \brief Monitors joystick input and resets screensaver/shutdown timers
+ * whenever motion occurs.
+ */
+class CJoystickMonitor : public IInputHandler
+{
+public:
+ // implementation of IInputHandler
+ std::string ControllerID() const override;
+ bool HasFeature(const FeatureName& feature) const override { return true; }
+ bool AcceptsInput(const FeatureName& feature) const override;
+ bool OnButtonPress(const FeatureName& feature, bool bPressed) override;
+ void OnButtonHold(const FeatureName& feature, unsigned int holdTimeMs) override {}
+ bool OnButtonMotion(const FeatureName& feature,
+ float magnitude,
+ unsigned int motionTimeMs) override;
+ bool OnAnalogStickMotion(const FeatureName& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs) override;
+ bool OnAccelerometerMotion(const FeatureName& feature, float x, float y, float z) override
+ {
+ return false;
+ }
+ bool OnWheelMotion(const FeatureName& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ bool OnThrottleMotion(const FeatureName& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ void OnInputFrame() override {}
+
+private:
+ /*!
+ * \brief Reset screensaver and shutdown timers
+ * \return True if the application was woken from screensaver
+ */
+ bool ResetTimers(void);
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/JoystickTranslator.cpp b/xbmc/input/joysticks/JoystickTranslator.cpp
new file mode 100644
index 0000000..7edfeaf
--- /dev/null
+++ b/xbmc/input/joysticks/JoystickTranslator.cpp
@@ -0,0 +1,175 @@
+/*
+ * 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 "JoystickTranslator.h"
+
+#include "guilib/LocalizeStrings.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+const char* CJoystickTranslator::HatStateToString(HAT_STATE state)
+{
+ switch (state)
+ {
+ case HAT_STATE::UP:
+ return "UP";
+ case HAT_STATE::DOWN:
+ return "DOWN";
+ case HAT_STATE::RIGHT:
+ return "RIGHT";
+ case HAT_STATE::LEFT:
+ return "LEFT";
+ case HAT_STATE::RIGHTUP:
+ return "UP RIGHT";
+ case HAT_STATE::RIGHTDOWN:
+ return "DOWN RIGHT";
+ case HAT_STATE::LEFTUP:
+ return "UP LEFT";
+ case HAT_STATE::LEFTDOWN:
+ return "DOWN LEFT";
+ default:
+ break;
+ }
+
+ return "RELEASED";
+}
+
+const char* CJoystickTranslator::TranslateAnalogStickDirection(ANALOG_STICK_DIRECTION dir)
+{
+ switch (dir)
+ {
+ case ANALOG_STICK_DIRECTION::UP:
+ return "up";
+ case ANALOG_STICK_DIRECTION::DOWN:
+ return "down";
+ case ANALOG_STICK_DIRECTION::RIGHT:
+ return "right";
+ case ANALOG_STICK_DIRECTION::LEFT:
+ return "left";
+ default:
+ break;
+ }
+
+ return "";
+}
+
+ANALOG_STICK_DIRECTION CJoystickTranslator::TranslateAnalogStickDirection(const std::string& dir)
+{
+ if (dir == "up")
+ return ANALOG_STICK_DIRECTION::UP;
+ if (dir == "down")
+ return ANALOG_STICK_DIRECTION::DOWN;
+ if (dir == "right")
+ return ANALOG_STICK_DIRECTION::RIGHT;
+ if (dir == "left")
+ return ANALOG_STICK_DIRECTION::LEFT;
+
+ return ANALOG_STICK_DIRECTION::NONE;
+}
+
+const char* CJoystickTranslator::TranslateWheelDirection(WHEEL_DIRECTION dir)
+{
+ switch (dir)
+ {
+ case WHEEL_DIRECTION::RIGHT:
+ return "right";
+ case WHEEL_DIRECTION::LEFT:
+ return "left";
+ default:
+ break;
+ }
+
+ return "";
+}
+
+WHEEL_DIRECTION CJoystickTranslator::TranslateWheelDirection(const std::string& dir)
+{
+ if (dir == "right")
+ return WHEEL_DIRECTION::RIGHT;
+ if (dir == "left")
+ return WHEEL_DIRECTION::LEFT;
+
+ return WHEEL_DIRECTION::NONE;
+}
+
+const char* CJoystickTranslator::TranslateThrottleDirection(THROTTLE_DIRECTION dir)
+{
+ switch (dir)
+ {
+ case THROTTLE_DIRECTION::UP:
+ return "up";
+ case THROTTLE_DIRECTION::DOWN:
+ return "down";
+ default:
+ break;
+ }
+
+ return "";
+}
+
+THROTTLE_DIRECTION CJoystickTranslator::TranslateThrottleDirection(const std::string& dir)
+{
+ if (dir == "up")
+ return THROTTLE_DIRECTION::UP;
+ if (dir == "down")
+ return THROTTLE_DIRECTION::DOWN;
+
+ return THROTTLE_DIRECTION::NONE;
+}
+
+SEMIAXIS_DIRECTION CJoystickTranslator::PositionToSemiAxisDirection(float position)
+{
+ if (position > 0)
+ return SEMIAXIS_DIRECTION::POSITIVE;
+ else if (position < 0)
+ return SEMIAXIS_DIRECTION::NEGATIVE;
+
+ return SEMIAXIS_DIRECTION::ZERO;
+}
+
+WHEEL_DIRECTION CJoystickTranslator::PositionToWheelDirection(float position)
+{
+ if (position > 0.0f)
+ return WHEEL_DIRECTION::RIGHT;
+ else if (position < 0.0f)
+ return WHEEL_DIRECTION::LEFT;
+
+ return WHEEL_DIRECTION::NONE;
+}
+
+THROTTLE_DIRECTION CJoystickTranslator::PositionToThrottleDirection(float position)
+{
+ if (position > 0.0f)
+ return THROTTLE_DIRECTION::UP;
+ else if (position < 0.0f)
+ return THROTTLE_DIRECTION::DOWN;
+
+ return THROTTLE_DIRECTION::NONE;
+}
+
+std::string CJoystickTranslator::GetPrimitiveName(const CDriverPrimitive& primitive)
+{
+ std::string primitiveTemplate;
+
+ switch (primitive.Type())
+ {
+ case PRIMITIVE_TYPE::BUTTON:
+ primitiveTemplate = g_localizeStrings.Get(35015); // "Button %d"
+ break;
+ case PRIMITIVE_TYPE::SEMIAXIS:
+ primitiveTemplate = g_localizeStrings.Get(35016); // "Axis %d"
+ break;
+ default:
+ break;
+ }
+
+ return StringUtils::Format(primitiveTemplate, primitive.Index());
+}
diff --git a/xbmc/input/joysticks/JoystickTranslator.h b/xbmc/input/joysticks/JoystickTranslator.h
new file mode 100644
index 0000000..2efeacc
--- /dev/null
+++ b/xbmc/input/joysticks/JoystickTranslator.h
@@ -0,0 +1,125 @@
+/*
+ * 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 "JoystickTypes.h"
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CDriverPrimitive;
+
+/*!
+ * \brief Joystick translation utilities
+ */
+class CJoystickTranslator
+{
+public:
+ /*!
+ * \brief Translate a hat state to a string representation
+ *
+ * \param state The hat state
+ *
+ * \return A capitalized string representation, or "RELEASED" if the hat is centered.
+ */
+ static const char* HatStateToString(HAT_STATE state);
+
+ /*!
+ * \brief Translate an analog stick direction to a lower-case string
+ *
+ * \param dir The analog stick direction
+ *
+ * \return A lower-case string representation, or "" if the direction is invalid
+ */
+ static const char* TranslateAnalogStickDirection(ANALOG_STICK_DIRECTION dir);
+
+ /*!
+ * \brief Translate an analog stick direction string to an enum value
+ *
+ * \param dir The analog stick direction
+ *
+ * \return The translated direction, or ANALOG_STICK_DIRECTION::UNKNOWN if unknown
+ */
+ static ANALOG_STICK_DIRECTION TranslateAnalogStickDirection(const std::string& dir);
+
+ /*!
+ * \brief Translate a wheel direction to a lower-case string
+ *
+ * \param dir The wheel direction
+ *
+ * \return A lower-case string representation, or "" if the direction is invalid
+ */
+ static const char* TranslateWheelDirection(WHEEL_DIRECTION dir);
+
+ /*!
+ * \brief Translate a wheel direction string to an enum value
+ *
+ * \param dir The wheel direction
+ *
+ * \return The translated direction, or WHEEL_DIRECTION::UNKNOWN if unknown
+ */
+ static WHEEL_DIRECTION TranslateWheelDirection(const std::string& dir);
+
+ /*!
+ * \brief Translate a throttle direction to a lower-case string
+ *
+ * \param dir The analog stick direction
+ *
+ * \return A lower-case string representation, or "" if the direction is invalid
+ */
+ static const char* TranslateThrottleDirection(THROTTLE_DIRECTION dir);
+
+ /*!
+ * \brief Translate a throttle direction string to an enum value
+ *
+ * \param dir The throttle direction
+ *
+ * \return The translated direction, or THROTTLE_DIRECTION::UNKNOWN if unknown
+ */
+ static THROTTLE_DIRECTION TranslateThrottleDirection(const std::string& dir);
+
+ /*!
+ * \brief Get the semi-axis direction containing the specified position
+ *
+ * \param position The position of the axis
+ *
+ * \return POSITIVE, NEGATIVE, or UNKNOWN if position is 0
+ */
+ static SEMIAXIS_DIRECTION PositionToSemiAxisDirection(float position);
+
+ /*!
+ * \brief Get the wheel direction containing the specified position
+ *
+ * \param position The position of the axis
+ *
+ * \return LEFT, RIGHT, or UNKNOWN if position is 0
+ */
+ static WHEEL_DIRECTION PositionToWheelDirection(float position);
+
+ /*!
+ * \brief Get the throttle direction containing the specified position
+ *
+ * \param position The position of the axis
+ *
+ * \return UP, DOWN, or UNKNOWN if position is 0
+ */
+ static THROTTLE_DIRECTION PositionToThrottleDirection(float position);
+
+ /*!
+ * \brief Get the localized name of the primitive
+ *
+ * \param primitive The primitive, currently only buttons and axes are supported
+ *
+ * \return A title for the primitive, e.g. "Button 0" or "Axis 1"
+ */
+ static std::string GetPrimitiveName(const CDriverPrimitive& primitive);
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/JoystickTypes.h b/xbmc/input/joysticks/JoystickTypes.h
new file mode 100644
index 0000000..5925793
--- /dev/null
+++ b/xbmc/input/joysticks/JoystickTypes.h
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2014-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
+
+/*!
+ \file
+ \ingroup joystick
+ */
+
+#include "input/InputTypes.h"
+
+#include <set>
+#include <string>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+/*!
+ * \brief Name of a physical feature belonging to the joystick
+ */
+using FeatureName = std::string;
+
+/*!
+ * \brief Types of features used in the joystick library
+ *
+ * Available types:
+ *
+ * 1) scalar[*]
+ * 2) analog stick
+ * 3) accelerometer
+ * 4) rumble motor
+ * 5) relative pointer
+ * 6) absolute pointer
+ * 7) wheel
+ * 8) throttle
+ * 9) keyboard key
+ *
+ * [*] All three driver primitives (buttons, hats and axes) have a state that
+ * can be represented using a single scalar value. For this reason,
+ * features that map to a single primitive are called "scalar features".
+ */
+enum class FEATURE_TYPE
+{
+ UNKNOWN,
+ SCALAR,
+ ANALOG_STICK,
+ ACCELEROMETER,
+ MOTOR,
+ RELPOINTER,
+ ABSPOINTER,
+ WHEEL,
+ THROTTLE,
+ KEY,
+};
+
+/*!
+ * \brief Categories of features used in the joystick library
+ */
+enum class FEATURE_CATEGORY
+{
+ UNKNOWN,
+ FACE,
+ SHOULDER,
+ TRIGGER,
+ ANALOG_STICK,
+ ACCELEROMETER,
+ HAPTICS,
+ MOUSE_BUTTON,
+ POINTER,
+ LIGHTGUN,
+ OFFSCREEN, // Virtual button to shoot light gun offscreen
+ KEY, // A keyboard key
+ KEYPAD, // A key on a numeric keymap, including star and pound
+ HARDWARE, // A button or functionality on the console
+ WHEEL,
+ JOYSTICK,
+ PADDLE,
+};
+
+/*!
+ * \brief Direction arrows on the hat (directional pad)
+ */
+using HAT_DIRECTION = INPUT::CARDINAL_DIRECTION;
+
+/*!
+ * \brief States in which a hat can be
+ */
+using HAT_STATE = INPUT::INTERCARDINAL_DIRECTION;
+
+/*!
+ * \brief Typedef for analog stick directions
+ */
+using ANALOG_STICK_DIRECTION = INPUT::CARDINAL_DIRECTION;
+
+/*!
+ * \brief Directions of motion for a relative pointer
+ */
+using RELATIVE_POINTER_DIRECTION = INPUT::CARDINAL_DIRECTION;
+
+/*!
+ * \brief Directions in which a semiaxis can point
+ */
+enum class SEMIAXIS_DIRECTION
+{
+ NEGATIVE = -1, // semiaxis lies in the interval [-1.0, 0.0]
+ ZERO = 0, // semiaxis is unknown or invalid
+ POSITIVE = 1, // semiaxis lies in the interval [0.0, 1.0]
+};
+
+/*!
+ * \brief Directions on a wheel
+ */
+enum class WHEEL_DIRECTION
+{
+ NONE,
+ RIGHT,
+ LEFT,
+};
+
+/*!
+ * \brief Directions on a throttle
+ */
+enum class THROTTLE_DIRECTION
+{
+ NONE,
+ UP,
+ DOWN,
+};
+
+/*!
+ * \brief Types of input available for scalar features
+ */
+enum class INPUT_TYPE
+{
+ UNKNOWN,
+ DIGITAL,
+ ANALOG,
+};
+
+/*!
+ * \brief Type of driver primitive
+ */
+enum class PRIMITIVE_TYPE
+{
+ UNKNOWN = 0, // primitive has no type (invalid)
+ BUTTON, // a digital button
+ HAT, // one of the four direction arrows on a D-pad
+ SEMIAXIS, // the positive or negative half of an axis
+ MOTOR, // a rumble motor
+ KEY, // a keyboard key
+ MOUSE_BUTTON, // a mouse button
+ RELATIVE_POINTER, // a relative pointer, such as on a mouse
+};
+
+/*!
+ * \ingroup joystick
+ * \brief Action entry in joystick.xml
+ */
+struct KeymapAction
+{
+ unsigned int actionId;
+ std::string actionString;
+ unsigned int holdTimeMs;
+ std::set<std::string> hotkeys;
+
+ bool operator<(const KeymapAction& rhs) const { return holdTimeMs < rhs.holdTimeMs; }
+};
+
+/*!
+ * \ingroup joystick
+ * \brief Container that sorts action entries by their holdtime
+ */
+struct KeymapActionGroup
+{
+ int windowId = -1;
+ std::set<KeymapAction> actions;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/JoystickUtils.cpp b/xbmc/input/joysticks/JoystickUtils.cpp
new file mode 100644
index 0000000..3c25121
--- /dev/null
+++ b/xbmc/input/joysticks/JoystickUtils.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2017-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 "JoystickUtils.h"
+
+#include "JoystickTranslator.h"
+#include "utils/StringUtils.h"
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+std::string CJoystickUtils::MakeKeyName(const FeatureName& feature)
+{
+ return feature;
+}
+
+std::string CJoystickUtils::MakeKeyName(const FeatureName& feature, ANALOG_STICK_DIRECTION dir)
+{
+ std::string keyName = feature;
+
+ if (dir != ANALOG_STICK_DIRECTION::NONE)
+ keyName += CJoystickTranslator::TranslateAnalogStickDirection(dir);
+
+ return keyName;
+}
+
+std::string CJoystickUtils::MakeKeyName(const FeatureName& feature, WHEEL_DIRECTION dir)
+{
+ ANALOG_STICK_DIRECTION stickDir = ANALOG_STICK_DIRECTION::NONE;
+
+ switch (dir)
+ {
+ case WHEEL_DIRECTION::LEFT:
+ stickDir = ANALOG_STICK_DIRECTION::LEFT;
+ break;
+ case WHEEL_DIRECTION::RIGHT:
+ stickDir = ANALOG_STICK_DIRECTION::RIGHT;
+ break;
+ default:
+ break;
+ }
+
+ return MakeKeyName(feature, stickDir);
+}
+
+std::string CJoystickUtils::MakeKeyName(const FeatureName& feature, THROTTLE_DIRECTION dir)
+{
+ ANALOG_STICK_DIRECTION stickDir = ANALOG_STICK_DIRECTION::NONE;
+
+ switch (dir)
+ {
+ case THROTTLE_DIRECTION::UP:
+ stickDir = ANALOG_STICK_DIRECTION::UP;
+ break;
+ case THROTTLE_DIRECTION::DOWN:
+ stickDir = ANALOG_STICK_DIRECTION::DOWN;
+ break;
+ default:
+ break;
+ }
+
+ return MakeKeyName(feature, stickDir);
+}
+
+const std::vector<ANALOG_STICK_DIRECTION>& CJoystickUtils::GetAnalogStickDirections()
+{
+ static std::vector<ANALOG_STICK_DIRECTION> directions;
+ if (directions.empty())
+ {
+ directions.push_back(ANALOG_STICK_DIRECTION::UP);
+ directions.push_back(ANALOG_STICK_DIRECTION::DOWN);
+ directions.push_back(ANALOG_STICK_DIRECTION::RIGHT);
+ directions.push_back(ANALOG_STICK_DIRECTION::LEFT);
+ }
+ return directions;
+}
+
+const std::vector<WHEEL_DIRECTION>& CJoystickUtils::GetWheelDirections()
+{
+ static std::vector<WHEEL_DIRECTION> directions;
+ if (directions.empty())
+ {
+ directions.push_back(WHEEL_DIRECTION::RIGHT);
+ directions.push_back(WHEEL_DIRECTION::LEFT);
+ }
+ return directions;
+}
+
+const std::vector<THROTTLE_DIRECTION>& CJoystickUtils::GetThrottleDirections()
+{
+ static std::vector<THROTTLE_DIRECTION> directions;
+ if (directions.empty())
+ {
+ directions.push_back(THROTTLE_DIRECTION::UP);
+ directions.push_back(THROTTLE_DIRECTION::DOWN);
+ }
+ return directions;
+}
diff --git a/xbmc/input/joysticks/JoystickUtils.h b/xbmc/input/joysticks/JoystickUtils.h
new file mode 100644
index 0000000..49095d2
--- /dev/null
+++ b/xbmc/input/joysticks/JoystickUtils.h
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2014-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 "JoystickTypes.h"
+
+#include <string>
+#include <vector>
+
+/// \ingroup joystick
+/// \{
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+
+inline HAT_DIRECTION& operator|=(HAT_DIRECTION& lhs, HAT_DIRECTION rhs)
+{
+ return lhs = static_cast<HAT_DIRECTION>(static_cast<int>(lhs) | static_cast<int>(rhs));
+}
+
+inline HAT_STATE& operator|=(HAT_STATE& lhs, HAT_STATE rhs)
+{
+ return lhs = static_cast<HAT_STATE>(static_cast<int>(lhs) | static_cast<int>(rhs));
+}
+
+inline bool operator&(HAT_STATE lhs, HAT_DIRECTION rhs)
+{
+ return (static_cast<int>(lhs) & static_cast<int>(rhs)) ? true : false;
+}
+
+inline SEMIAXIS_DIRECTION operator*(SEMIAXIS_DIRECTION lhs, int rhs)
+{
+ return static_cast<SEMIAXIS_DIRECTION>(static_cast<int>(lhs) * rhs);
+}
+
+inline float operator*(float lhs, SEMIAXIS_DIRECTION rhs)
+{
+ return lhs * static_cast<int>(rhs);
+}
+
+class CJoystickUtils
+{
+public:
+ /*!
+ * \brief Create a key name used to index an action in the keymap
+ *
+ * \param feature The feature name
+ *
+ * \return A valid name for a key in the joystick keymap
+ */
+ static std::string MakeKeyName(const FeatureName& feature);
+
+ /*!
+ * \brief Create a key name used to index an action in the keymap
+ *
+ * \param feature The feature name
+ * \param dir The direction for analog sticks
+ *
+ * \return A valid name for a key in the joystick keymap
+ */
+ static std::string MakeKeyName(const FeatureName& feature, ANALOG_STICK_DIRECTION dir);
+
+ /*!
+ * \brief Create a key name used to index an action in the keymap
+ *
+ * \param feature The feature name
+ * \param dir The direction for a wheel to turn
+ *
+ * \return A valid name for a key in the joystick keymap
+ */
+ static std::string MakeKeyName(const FeatureName& feature, WHEEL_DIRECTION dir);
+
+ /*!
+ * \brief Create a key name used to index an action in the keymap
+ *
+ * \param feature The feature name
+ * \param dir The direction for a throttle to move
+ *
+ * \return A valid name for a key in the joystick keymap
+ */
+ static std::string MakeKeyName(const FeatureName& feature, THROTTLE_DIRECTION dir);
+
+ /*!
+ * \brief Return a vector of the four cardinal directions
+ */
+ static const std::vector<ANALOG_STICK_DIRECTION>& GetAnalogStickDirections();
+
+ /*!
+ * \brief Return a vector of the two wheel directions
+ */
+ static const std::vector<WHEEL_DIRECTION>& GetWheelDirections();
+
+ /*!
+ * \brief Return a vector of the two throttle directions
+ */
+ static const std::vector<THROTTLE_DIRECTION>& GetThrottleDirections();
+};
+
+} // namespace JOYSTICK
+} // namespace KODI
+
+/// \}
diff --git a/xbmc/input/joysticks/RumbleGenerator.cpp b/xbmc/input/joysticks/RumbleGenerator.cpp
new file mode 100644
index 0000000..db60cfa
--- /dev/null
+++ b/xbmc/input/joysticks/RumbleGenerator.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2016-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 "RumbleGenerator.h"
+
+#include "ServiceBroker.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerIDs.h"
+#include "games/controllers/ControllerManager.h"
+#include "input/joysticks/interfaces/IInputReceiver.h"
+
+#include <algorithm>
+
+using namespace std::chrono_literals;
+
+namespace
+{
+constexpr auto RUMBLE_TEST_DURATION_MS = 1000ms; // Per motor
+constexpr auto RUMBLE_NOTIFICATION_DURATION_MS = 300ms;
+
+// From game.controller.default profile
+#define WEAK_MOTOR_NAME "rightmotor"
+} // namespace
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+CRumbleGenerator::CRumbleGenerator()
+ : CThread("RumbleGenerator"), m_motors(GetMotors(ControllerID()))
+{
+}
+
+std::string CRumbleGenerator::ControllerID() const
+{
+ return DEFAULT_CONTROLLER_ID;
+}
+
+void CRumbleGenerator::NotifyUser(IInputReceiver* receiver)
+{
+ if (receiver && !m_motors.empty())
+ {
+ if (IsRunning())
+ StopThread(true);
+
+ m_receiver = receiver;
+ m_type = RUMBLE_NOTIFICATION;
+ Create();
+ }
+}
+
+bool CRumbleGenerator::DoTest(IInputReceiver* receiver)
+{
+ if (receiver && !m_motors.empty())
+ {
+ if (IsRunning())
+ StopThread(true);
+
+ m_receiver = receiver;
+ m_type = RUMBLE_TEST;
+ Create();
+
+ return true;
+ }
+ return false;
+}
+
+void CRumbleGenerator::Process(void)
+{
+ switch (m_type)
+ {
+ case RUMBLE_NOTIFICATION:
+ {
+ std::vector<std::string> motors;
+
+ if (std::find(m_motors.begin(), m_motors.end(), WEAK_MOTOR_NAME) != m_motors.end())
+ motors.emplace_back(WEAK_MOTOR_NAME);
+ else
+ motors = m_motors; // Not using default profile? Just rumble all motors
+
+ for (const std::string& motor : motors)
+ m_receiver->SetRumbleState(motor, 1.0f);
+
+ CThread::Sleep(RUMBLE_NOTIFICATION_DURATION_MS);
+
+ if (m_bStop)
+ break;
+
+ for (const std::string& motor : motors)
+ m_receiver->SetRumbleState(motor, 0.0f);
+
+ break;
+ }
+ case RUMBLE_TEST:
+ {
+ for (const std::string& motor : m_motors)
+ {
+ m_receiver->SetRumbleState(motor, 1.0f);
+
+ CThread::Sleep(RUMBLE_TEST_DURATION_MS);
+
+ if (m_bStop)
+ break;
+
+ m_receiver->SetRumbleState(motor, 0.0f);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+std::vector<std::string> CRumbleGenerator::GetMotors(const std::string& controllerId)
+{
+ using namespace GAME;
+
+ std::vector<std::string> motors;
+
+ CControllerManager& controllerManager = CServiceBroker::GetGameControllerManager();
+ ControllerPtr controller = controllerManager.GetController(controllerId);
+ if (controller)
+ controller->GetFeatures(motors, FEATURE_TYPE::MOTOR);
+
+ return motors;
+}
diff --git a/xbmc/input/joysticks/RumbleGenerator.h b/xbmc/input/joysticks/RumbleGenerator.h
new file mode 100644
index 0000000..a6e9853
--- /dev/null
+++ b/xbmc/input/joysticks/RumbleGenerator.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016-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/Thread.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IInputReceiver;
+
+class CRumbleGenerator : public CThread
+{
+public:
+ CRumbleGenerator();
+
+ ~CRumbleGenerator() override { AbortRumble(); }
+
+ std::string ControllerID() const;
+
+ void NotifyUser(IInputReceiver* receiver);
+ bool DoTest(IInputReceiver* receiver);
+
+ void AbortRumble(void) { StopThread(); }
+
+protected:
+ // implementation of CThread
+ void Process() override;
+
+private:
+ enum RUMBLE_TYPE
+ {
+ RUMBLE_UNKNOWN,
+ RUMBLE_NOTIFICATION,
+ RUMBLE_TEST,
+ };
+
+ static std::vector<std::string> GetMotors(const std::string& controllerId);
+
+ // Construction param
+ const std::vector<std::string> m_motors;
+
+ // Test param
+ IInputReceiver* m_receiver = nullptr;
+ RUMBLE_TYPE m_type = RUMBLE_UNKNOWN;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/dialogs/CMakeLists.txt b/xbmc/input/joysticks/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..73adcec
--- /dev/null
+++ b/xbmc/input/joysticks/dialogs/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES GUIDialogNewJoystick.cpp)
+
+set(HEADERS GUIDialogNewJoystick.h)
+
+core_add_library(input_joystick_dialogs)
diff --git a/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.cpp b/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.cpp
new file mode 100644
index 0000000..b66ce89
--- /dev/null
+++ b/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016-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 "GUIDialogNewJoystick.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+CGUIDialogNewJoystick::CGUIDialogNewJoystick() : CThread("NewJoystickDlg")
+{
+}
+
+void CGUIDialogNewJoystick::ShowAsync()
+{
+ bool bShow = true;
+
+ if (IsRunning())
+ bShow = false;
+ else if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_INPUT_ASKNEWCONTROLLERS))
+ bShow = false;
+ else if (CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(
+ WINDOW_DIALOG_GAME_CONTROLLERS, false))
+ bShow = false;
+
+ if (bShow)
+ Create();
+}
+
+void CGUIDialogNewJoystick::Process()
+{
+ using namespace MESSAGING::HELPERS;
+
+ // "New controller detected"
+ // "A new controller has been detected. Configuration can be done at any time in "Settings ->
+ // System Settings -> Input". Would you like to configure it now?"
+ if (ShowYesNoDialogText(CVariant{35011}, CVariant{35012}) == DialogResponse::CHOICE_YES)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_DIALOG_GAME_CONTROLLERS);
+ }
+ else
+ {
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetBool(
+ CSettings::SETTING_INPUT_ASKNEWCONTROLLERS, false);
+ }
+}
diff --git a/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.h b/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.h
new file mode 100644
index 0000000..334191f
--- /dev/null
+++ b/xbmc/input/joysticks/dialogs/GUIDialogNewJoystick.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016-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/Thread.h"
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CGUIDialogNewJoystick : protected CThread
+{
+public:
+ CGUIDialogNewJoystick();
+ ~CGUIDialogNewJoystick() override = default;
+
+ void ShowAsync();
+
+protected:
+ // implementation of CThread
+ void Process() override;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/generic/ButtonMapping.cpp b/xbmc/input/joysticks/generic/ButtonMapping.cpp
new file mode 100644
index 0000000..665a233
--- /dev/null
+++ b/xbmc/input/joysticks/generic/ButtonMapping.cpp
@@ -0,0 +1,592 @@
+/*
+ * Copyright (C) 2014-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 "ButtonMapping.h"
+
+#include "ServiceBroker.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerManager.h"
+#include "games/controllers/input/PhysicalFeature.h"
+#include "input/IKeymap.h"
+#include "input/InputTranslator.h"
+#include "input/Key.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/JoystickTranslator.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IButtonMapper.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <assert.h>
+#include <cmath>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+#define MAPPING_COOLDOWN_MS 50 // Guard against repeated input
+#define AXIS_THRESHOLD 0.75f // Axis must exceed this value to be mapped
+#define TRIGGER_DELAY_MS \
+ 200 // Delay trigger detection to handle anomalous triggers with non-zero center
+
+// --- CPrimitiveDetector ------------------------------------------------------
+
+CPrimitiveDetector::CPrimitiveDetector(CButtonMapping* buttonMapping)
+ : m_buttonMapping(buttonMapping)
+{
+}
+
+bool CPrimitiveDetector::MapPrimitive(const CDriverPrimitive& primitive)
+{
+ if (primitive.IsValid())
+ return m_buttonMapping->MapPrimitive(primitive);
+
+ return false;
+}
+
+// --- CButtonDetector ---------------------------------------------------------
+
+CButtonDetector::CButtonDetector(CButtonMapping* buttonMapping, unsigned int buttonIndex)
+ : CPrimitiveDetector(buttonMapping), m_buttonIndex(buttonIndex)
+{
+}
+
+bool CButtonDetector::OnMotion(bool bPressed)
+{
+ if (bPressed)
+ return MapPrimitive(CDriverPrimitive(PRIMITIVE_TYPE::BUTTON, m_buttonIndex));
+
+ return false;
+}
+
+// --- CHatDetector ------------------------------------------------------------
+
+CHatDetector::CHatDetector(CButtonMapping* buttonMapping, unsigned int hatIndex)
+ : CPrimitiveDetector(buttonMapping), m_hatIndex(hatIndex)
+{
+}
+
+bool CHatDetector::OnMotion(HAT_STATE state)
+{
+ return MapPrimitive(CDriverPrimitive(m_hatIndex, static_cast<HAT_DIRECTION>(state)));
+}
+
+// --- CAxisDetector -----------------------------------------------------------
+
+CAxisDetector::CAxisDetector(CButtonMapping* buttonMapping,
+ unsigned int axisIndex,
+ const AxisConfiguration& config)
+ : CPrimitiveDetector(buttonMapping),
+ m_axisIndex(axisIndex),
+ m_config(config),
+ m_state(AXIS_STATE::INACTIVE),
+ m_type(AXIS_TYPE::UNKNOWN),
+ m_initialPositionKnown(false),
+ m_initialPosition(0.0f),
+ m_initialPositionChanged(false)
+{
+}
+
+bool CAxisDetector::OnMotion(float position)
+{
+ DetectType(position);
+
+ if (m_type != AXIS_TYPE::UNKNOWN)
+ {
+ // Update position if this axis is an anomalous trigger
+ if (m_type == AXIS_TYPE::OFFSET)
+ position = (position - m_config.center) / m_config.range;
+
+ // Reset state if position crosses zero
+ if (m_state == AXIS_STATE::MAPPED)
+ {
+ SEMIAXIS_DIRECTION activatedDir = m_activatedPrimitive.SemiAxisDirection();
+ SEMIAXIS_DIRECTION newDir = CJoystickTranslator::PositionToSemiAxisDirection(position);
+
+ if (activatedDir != newDir)
+ m_state = AXIS_STATE::INACTIVE;
+ }
+
+ // Check if axis has become activated
+ if (m_state == AXIS_STATE::INACTIVE)
+ {
+ if (std::abs(position) >= AXIS_THRESHOLD)
+ m_state = AXIS_STATE::ACTIVATED;
+
+ if (m_state == AXIS_STATE::ACTIVATED)
+ {
+ // Range is set later for anomalous triggers
+ m_activatedPrimitive =
+ CDriverPrimitive(m_axisIndex, m_config.center,
+ CJoystickTranslator::PositionToSemiAxisDirection(position), 1);
+ m_activationTimeMs = std::chrono::steady_clock::now();
+ }
+ }
+ }
+
+ return true;
+}
+
+void CAxisDetector::ProcessMotion()
+{
+ // Process newly-activated axis
+ if (m_state == AXIS_STATE::ACTIVATED)
+ {
+ // Ignore anomalous triggers for a bit so we can detect the full range
+ bool bIgnore = false;
+ if (m_type == AXIS_TYPE::OFFSET)
+ {
+ auto now = std::chrono::steady_clock::now();
+ auto duration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - m_activationTimeMs);
+
+ if (duration.count() < TRIGGER_DELAY_MS)
+ bIgnore = true;
+ }
+
+ if (!bIgnore)
+ {
+ // Update driver primitive's range if we're mapping an anomalous trigger
+ if (m_type == AXIS_TYPE::OFFSET)
+ {
+ m_activatedPrimitive =
+ CDriverPrimitive(m_activatedPrimitive.Index(), m_activatedPrimitive.Center(),
+ m_activatedPrimitive.SemiAxisDirection(), m_config.range);
+ }
+
+ // Map primitive
+ if (!MapPrimitive(m_activatedPrimitive))
+ {
+ if (m_type == AXIS_TYPE::OFFSET)
+ CLog::Log(LOGDEBUG, "Mapping offset axis {} failed", m_axisIndex);
+ else
+ CLog::Log(LOGDEBUG, "Mapping normal axis {} failed", m_axisIndex);
+ }
+
+ m_state = AXIS_STATE::MAPPED;
+ }
+ }
+}
+
+void CAxisDetector::SetEmitted(const CDriverPrimitive& activePrimitive)
+{
+ m_state = AXIS_STATE::MAPPED;
+ m_activatedPrimitive = activePrimitive;
+}
+
+void CAxisDetector::DetectType(float position)
+{
+ // Some platforms don't report a value until the axis is first changed.
+ // Detection relies on an initial value, so this axis will be disabled until
+ // the user begins button mapping again.
+ if (m_config.bLateDiscovery)
+ return;
+
+ // Update range if a range of > 1 is observed
+ if (std::abs(position - m_config.center) > 1.0f)
+ m_config.range = 2;
+
+ if (m_type != AXIS_TYPE::UNKNOWN)
+ return;
+
+ if (m_config.bKnown)
+ {
+ if (m_config.center == 0)
+ m_type = AXIS_TYPE::NORMAL;
+ else
+ m_type = AXIS_TYPE::OFFSET;
+ }
+
+ if (m_type != AXIS_TYPE::UNKNOWN)
+ return;
+
+ if (!m_initialPositionKnown)
+ {
+ m_initialPositionKnown = true;
+ m_initialPosition = position;
+ }
+
+ if (position != m_initialPosition)
+ m_initialPositionChanged = true;
+
+ if (m_initialPositionChanged)
+ {
+ // Calculate center based on initial position.
+ if (m_initialPosition < -0.5f)
+ {
+ m_config.center = -1;
+ m_type = AXIS_TYPE::OFFSET;
+ CLog::Log(LOGDEBUG, "Anomalous trigger detected on axis {} with center {}", m_axisIndex,
+ m_config.center);
+ }
+ else if (m_initialPosition > 0.5f)
+ {
+ m_config.center = 1;
+ m_type = AXIS_TYPE::OFFSET;
+ CLog::Log(LOGDEBUG, "Anomalous trigger detected on axis {} with center {}", m_axisIndex,
+ m_config.center);
+ }
+ else
+ {
+ m_type = AXIS_TYPE::NORMAL;
+ CLog::Log(LOGDEBUG, "Normal axis detected on axis {}", m_axisIndex);
+ }
+ }
+}
+
+// --- CKeyDetector ---------------------------------------------------------
+
+CKeyDetector::CKeyDetector(CButtonMapping* buttonMapping, XBMCKey keycode)
+ : CPrimitiveDetector(buttonMapping), m_keycode(keycode)
+{
+}
+
+bool CKeyDetector::OnMotion(bool bPressed)
+{
+ if (bPressed)
+ return MapPrimitive(CDriverPrimitive(m_keycode));
+
+ return false;
+}
+
+// --- CMouseButtonDetector ----------------------------------------------------
+
+CMouseButtonDetector::CMouseButtonDetector(CButtonMapping* buttonMapping,
+ MOUSE::BUTTON_ID buttonIndex)
+ : CPrimitiveDetector(buttonMapping), m_buttonIndex(buttonIndex)
+{
+}
+
+bool CMouseButtonDetector::OnMotion(bool bPressed)
+{
+ if (bPressed)
+ return MapPrimitive(CDriverPrimitive(m_buttonIndex));
+
+ return false;
+}
+
+// --- CPointerDetector --------------------------------------------------------
+
+CPointerDetector::CPointerDetector(CButtonMapping* buttonMapping)
+ : CPrimitiveDetector(buttonMapping)
+{
+}
+
+bool CPointerDetector::OnMotion(int x, int y)
+{
+ if (!m_bStarted)
+ {
+ m_bStarted = true;
+ m_startX = x;
+ m_startY = y;
+ m_frameCount = 0;
+ }
+
+ if (m_frameCount++ >= MIN_FRAME_COUNT)
+ {
+ int dx = x - m_startX;
+ int dy = y - m_startY;
+
+ INPUT::INTERCARDINAL_DIRECTION dir = GetPointerDirection(dx, dy);
+
+ CDriverPrimitive primitive(static_cast<RELATIVE_POINTER_DIRECTION>(dir));
+ if (primitive.IsValid())
+ {
+ if (MapPrimitive(primitive))
+ m_bStarted = false;
+ }
+ }
+
+ return true;
+}
+
+KODI::INPUT::INTERCARDINAL_DIRECTION CPointerDetector::GetPointerDirection(int x, int y)
+{
+ using namespace INPUT;
+
+ // Translate from left-handed coordinate system to right-handed coordinate system
+ y *= -1;
+
+ return CInputTranslator::VectorToIntercardinalDirection(static_cast<float>(x),
+ static_cast<float>(y));
+}
+
+// --- CButtonMapping ----------------------------------------------------------
+
+CButtonMapping::CButtonMapping(IButtonMapper* buttonMapper, IButtonMap* buttonMap, IKeymap* keymap)
+ : m_buttonMapper(buttonMapper), m_buttonMap(buttonMap), m_keymap(keymap), m_frameCount(0)
+{
+ assert(m_buttonMapper != nullptr);
+ assert(m_buttonMap != nullptr);
+
+ // Make sure axes mapped to Select are centered before they can be mapped.
+ // This ensures that they are not immediately mapped to the first button.
+ if (m_keymap)
+ {
+ using namespace GAME;
+
+ CControllerManager& controllerManager = CServiceBroker::GetGameControllerManager();
+ ControllerPtr controller = controllerManager.GetController(m_keymap->ControllerID());
+
+ const auto& features = controller->Features();
+ for (const auto& feature : features)
+ {
+ bool bIsSelectAction = false;
+
+ const auto& actions =
+ m_keymap->GetActions(CJoystickUtils::MakeKeyName(feature.Name())).actions;
+ if (!actions.empty() && actions.begin()->actionId == ACTION_SELECT_ITEM)
+ bIsSelectAction = true;
+
+ if (!bIsSelectAction)
+ continue;
+
+ CDriverPrimitive primitive;
+ if (!m_buttonMap->GetScalar(feature.Name(), primitive))
+ continue;
+
+ if (primitive.Type() != PRIMITIVE_TYPE::SEMIAXIS)
+ continue;
+
+ // Set initial config, as detection will fail because axis is already activated
+ AxisConfiguration axisConfig;
+ axisConfig.bKnown = true;
+ axisConfig.center = primitive.Center();
+ axisConfig.range = primitive.Range();
+
+ GetAxis(primitive.Index(), static_cast<float>(primitive.Center()), axisConfig)
+ .SetEmitted(primitive);
+ }
+ }
+}
+
+bool CButtonMapping::OnButtonMotion(unsigned int buttonIndex, bool bPressed)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::BUTTON))
+ return false;
+
+ return GetButton(buttonIndex).OnMotion(bPressed);
+}
+
+bool CButtonMapping::OnHatMotion(unsigned int hatIndex, HAT_STATE state)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::HAT))
+ return false;
+
+ return GetHat(hatIndex).OnMotion(state);
+}
+
+bool CButtonMapping::OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::SEMIAXIS))
+ return false;
+
+ return GetAxis(axisIndex, position).OnMotion(position);
+}
+
+void CButtonMapping::OnInputFrame(void)
+{
+ for (auto& axis : m_axes)
+ axis.second.ProcessMotion();
+
+ m_buttonMapper->OnEventFrame(m_buttonMap, IsMapping());
+
+ m_frameCount++;
+}
+
+bool CButtonMapping::OnKeyPress(const CKey& key)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::KEY))
+ return false;
+
+ return GetKey(static_cast<XBMCKey>(key.GetKeycode())).OnMotion(true);
+}
+
+bool CButtonMapping::OnPosition(int x, int y)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::RELATIVE_POINTER))
+ return false;
+
+ return GetPointer().OnMotion(x, y);
+}
+
+bool CButtonMapping::OnButtonPress(MOUSE::BUTTON_ID button)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::MOUSE_BUTTON))
+ return false;
+
+ return GetMouseButton(button).OnMotion(true);
+}
+
+void CButtonMapping::OnButtonRelease(MOUSE::BUTTON_ID button)
+{
+ if (!m_buttonMapper->AcceptsPrimitive(PRIMITIVE_TYPE::MOUSE_BUTTON))
+ return;
+
+ GetMouseButton(button).OnMotion(false);
+}
+
+void CButtonMapping::SaveButtonMap()
+{
+ m_buttonMap->SaveButtonMap();
+}
+
+void CButtonMapping::ResetIgnoredPrimitives()
+{
+ std::vector<CDriverPrimitive> empty;
+ m_buttonMap->SetIgnoredPrimitives(empty);
+}
+
+void CButtonMapping::RevertButtonMap()
+{
+ m_buttonMap->RevertButtonMap();
+}
+
+bool CButtonMapping::MapPrimitive(const CDriverPrimitive& primitive)
+{
+ bool bHandled = false;
+
+ if (m_buttonMap->IsIgnored(primitive))
+ {
+ bHandled = true;
+ }
+ else
+ {
+ auto now = std::chrono::steady_clock::now();
+
+ bool bTimeoutElapsed = true;
+
+ if (m_buttonMapper->NeedsCooldown())
+ bTimeoutElapsed = (now >= m_lastAction + std::chrono::milliseconds(MAPPING_COOLDOWN_MS));
+
+ if (bTimeoutElapsed)
+ {
+ bHandled = m_buttonMapper->MapPrimitive(m_buttonMap, m_keymap, primitive);
+
+ if (bHandled)
+ m_lastAction = std::chrono::steady_clock::now();
+ }
+ else
+ {
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastAction);
+
+ CLog::Log(LOGDEBUG, "Button mapping: rapid input after {}ms dropped for profile \"{}\"",
+ duration.count(), m_buttonMapper->ControllerID());
+ bHandled = true;
+ }
+ }
+
+ return bHandled;
+}
+
+bool CButtonMapping::IsMapping() const
+{
+ for (auto itAxis : m_axes)
+ {
+ if (itAxis.second.IsMapping())
+ return true;
+ }
+
+ return false;
+}
+
+CButtonDetector& CButtonMapping::GetButton(unsigned int buttonIndex)
+{
+ auto itButton = m_buttons.find(buttonIndex);
+
+ if (itButton == m_buttons.end())
+ {
+ m_buttons.insert(std::make_pair(buttonIndex, CButtonDetector(this, buttonIndex)));
+ itButton = m_buttons.find(buttonIndex);
+ }
+
+ return itButton->second;
+}
+
+CHatDetector& CButtonMapping::GetHat(unsigned int hatIndex)
+{
+ auto itHat = m_hats.find(hatIndex);
+
+ if (itHat == m_hats.end())
+ {
+ m_hats.insert(std::make_pair(hatIndex, CHatDetector(this, hatIndex)));
+ itHat = m_hats.find(hatIndex);
+ }
+
+ return itHat->second;
+}
+
+CAxisDetector& CButtonMapping::GetAxis(
+ unsigned int axisIndex,
+ float position,
+ const AxisConfiguration& initialConfig /* = AxisConfiguration() */)
+{
+ auto itAxis = m_axes.find(axisIndex);
+
+ if (itAxis == m_axes.end())
+ {
+ AxisConfiguration config(initialConfig);
+
+ if (m_frameCount >= 2)
+ {
+ config.bLateDiscovery = true;
+ OnLateDiscovery(axisIndex);
+ }
+
+ // Report axis
+ CLog::Log(LOGDEBUG, "Axis {} discovered at position {:.4f} after {} frames", axisIndex,
+ position, static_cast<unsigned long>(m_frameCount));
+
+ m_axes.insert(std::make_pair(axisIndex, CAxisDetector(this, axisIndex, config)));
+ itAxis = m_axes.find(axisIndex);
+ }
+
+ return itAxis->second;
+}
+
+CKeyDetector& CButtonMapping::GetKey(XBMCKey keycode)
+{
+ auto itKey = m_keys.find(keycode);
+
+ if (itKey == m_keys.end())
+ {
+ m_keys.insert(std::make_pair(keycode, CKeyDetector(this, keycode)));
+ itKey = m_keys.find(keycode);
+ }
+
+ return itKey->second;
+}
+
+CMouseButtonDetector& CButtonMapping::GetMouseButton(MOUSE::BUTTON_ID buttonIndex)
+{
+ auto itButton = m_mouseButtons.find(buttonIndex);
+
+ if (itButton == m_mouseButtons.end())
+ {
+ m_mouseButtons.insert(std::make_pair(buttonIndex, CMouseButtonDetector(this, buttonIndex)));
+ itButton = m_mouseButtons.find(buttonIndex);
+ }
+
+ return itButton->second;
+}
+
+CPointerDetector& CButtonMapping::GetPointer()
+{
+ if (!m_pointer)
+ m_pointer.reset(new CPointerDetector(this));
+
+ return *m_pointer;
+}
+
+void CButtonMapping::OnLateDiscovery(unsigned int axisIndex)
+{
+ m_buttonMapper->OnLateAxis(m_buttonMap, axisIndex);
+}
diff --git a/xbmc/input/joysticks/generic/ButtonMapping.h b/xbmc/input/joysticks/generic/ButtonMapping.h
new file mode 100644
index 0000000..3f76767
--- /dev/null
+++ b/xbmc/input/joysticks/generic/ButtonMapping.h
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2014-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 "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/interfaces/IButtonMapCallback.h"
+#include "input/joysticks/interfaces/IDriverHandler.h"
+#include "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+#include "input/mouse/MouseTypes.h"
+#include "input/mouse/interfaces/IMouseDriverHandler.h"
+
+#include <chrono>
+#include <map>
+#include <memory>
+#include <stdint.h>
+
+class IKeymap;
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CButtonMapping;
+class IButtonMap;
+class IButtonMapper;
+
+/*!
+ * \brief Detects and dispatches mapping events
+ *
+ * A mapping event usually occurs when a driver primitive is pressed or
+ * exceeds a certain threshold.
+ *
+ * Detection can be quite complicated due to driver bugs, so each type of
+ * driver primitive is given its own detector class inheriting from this one.
+ */
+class CPrimitiveDetector
+{
+protected:
+ CPrimitiveDetector(CButtonMapping* buttonMapping);
+
+ /*!
+ * \brief Dispatch a mapping event
+ *
+ * \return True if the primitive was mapped, false otherwise
+ */
+ bool MapPrimitive(const CDriverPrimitive& primitive);
+
+private:
+ CButtonMapping* const m_buttonMapping;
+};
+
+/*!
+ * \brief Detects when a button should be mapped
+ */
+class CButtonDetector : public CPrimitiveDetector
+{
+public:
+ CButtonDetector(CButtonMapping* buttonMapping, unsigned int buttonIndex);
+
+ /*!
+ * \brief Button state has been updated
+ *
+ * \param bPressed The new state
+ *
+ * \return True if this press was handled, false if it should fall through
+ * to the next driver handler
+ */
+ bool OnMotion(bool bPressed);
+
+private:
+ // Construction parameters
+ const unsigned int m_buttonIndex;
+};
+
+/*!
+ * \brief Detects when a D-pad direction should be mapped
+ */
+class CHatDetector : public CPrimitiveDetector
+{
+public:
+ CHatDetector(CButtonMapping* buttonMapping, unsigned int hatIndex);
+
+ /*!
+ * \brief Hat state has been updated
+ *
+ * \param state The new state
+ *
+ * \return True if state is a cardinal direction, false otherwise
+ */
+ bool OnMotion(HAT_STATE state);
+
+private:
+ // Construction parameters
+ const unsigned int m_hatIndex;
+};
+
+struct AxisConfiguration
+{
+ bool bKnown = false;
+ int center = 0;
+ unsigned int range = 1;
+ bool bLateDiscovery = false;
+};
+
+/*!
+ * \brief Detects when an axis should be mapped
+ */
+class CAxisDetector : public CPrimitiveDetector
+{
+public:
+ CAxisDetector(CButtonMapping* buttonMapping,
+ unsigned int axisIndex,
+ const AxisConfiguration& config);
+
+ /*!
+ * \brief Axis state has been updated
+ *
+ * \param position The new state
+ *
+ * \return Always true - axis motion events are always absorbed while button mapping
+ */
+ bool OnMotion(float position);
+
+ /*!
+ * \brief Called once per frame
+ *
+ * If an axis was activated, the button mapping command will be emitted
+ * here.
+ */
+ void ProcessMotion();
+
+ /*!
+ * \brief Check if the axis was mapped and is still in motion
+ *
+ * \return True between when the axis is mapped and when it crosses zero
+ */
+ bool IsMapping() const { return m_state == AXIS_STATE::MAPPED; }
+
+ /*!
+ * \brief Set the state such that this axis has generated a mapping event
+ *
+ * If an axis is mapped to the Select action, it may be pressed when button
+ * mapping begins. This function is used to indicate that the axis shouldn't
+ * be mapped until after it crosses zero again.
+ */
+ void SetEmitted(const CDriverPrimitive& activePrimitive);
+
+private:
+ enum class AXIS_STATE
+ {
+ /*!
+ * \brief Axis is inactive (position is less than threshold)
+ */
+ INACTIVE,
+
+ /*!
+ * \brief Axis is activated (position has exceeded threshold)
+ */
+ ACTIVATED,
+
+ /*!
+ * \brief Axis has generated a mapping event, but has not been centered yet
+ */
+ MAPPED,
+ };
+
+ enum class AXIS_TYPE
+ {
+ /*!
+ * \brief Axis type is initially unknown
+ */
+ UNKNOWN,
+
+ /*!
+ * \brief Axis is centered about 0
+ *
+ * - If the axis is an analog stick, it can travel to -1 or +1.
+ * - If the axis is a pressure-sensitive button or a normal trigger,
+ * it can travel to +1.
+ * - If the axis is a DirectInput trigger, then it is possible that two
+ * triggers can be on the same axis in opposite directions.
+ * - Normally, D-pads appear as a hat or four buttons. However, some
+ * D-pads are reported as two axes that can have the discrete values
+ * -1, 0 or 1. This is called a "discrete D-pad".
+ */
+ NORMAL,
+
+ /*!
+ * \brief Axis is centered about -1 or 1
+ *
+ * - On OSX, with the cocoa driver, triggers are centered about -1 and
+ * travel to +1. In this case, the range is 2 and the direction is
+ * positive.
+ * - The author of SDL has observed triggers centered at +1 and travel
+ * to 0. In this case, the range is 1 and the direction is negative.
+ */
+ OFFSET,
+ };
+
+ void DetectType(float position);
+
+ // Construction parameters
+ const unsigned int m_axisIndex;
+ AxisConfiguration m_config; // mutable
+
+ // State variables
+ AXIS_STATE m_state;
+ CDriverPrimitive m_activatedPrimitive;
+ AXIS_TYPE m_type;
+ bool m_initialPositionKnown; // set to true on first motion
+ float m_initialPosition; // set to position of first motion
+ bool m_initialPositionChanged; // set to true when position differs from the initial position
+ std::chrono::time_point<std::chrono::steady_clock>
+ m_activationTimeMs; // only used to delay anomalous trigger mapping to detect full range
+};
+
+/*!
+ * \brief Detects when a keyboard key should be mapped
+ */
+class CKeyDetector : public CPrimitiveDetector
+{
+public:
+ CKeyDetector(CButtonMapping* buttonMapping, XBMCKey keycode);
+
+ /*!
+ * \brief Key state has been updated
+ *
+ * \param bPressed The new state
+ *
+ * \return True if this press was handled, false if it should fall through
+ * to the next driver handler
+ */
+ bool OnMotion(bool bPressed);
+
+private:
+ // Construction parameters
+ const XBMCKey m_keycode;
+};
+
+/*!
+ * \brief Detects when a mouse button should be mapped
+ */
+class CMouseButtonDetector : public CPrimitiveDetector
+{
+public:
+ CMouseButtonDetector(CButtonMapping* buttonMapping, MOUSE::BUTTON_ID buttonIndex);
+
+ /*!
+ * \brief Button state has been updated
+ *
+ * \param bPressed The new state
+ *
+ * \return True if this press was handled, false if it should fall through
+ * to the next driver handler
+ */
+ bool OnMotion(bool bPressed);
+
+private:
+ // Construction parameters
+ const MOUSE::BUTTON_ID m_buttonIndex;
+};
+
+/*!
+ * \brief Detects when a mouse button should be mapped
+ */
+class CPointerDetector : public CPrimitiveDetector
+{
+public:
+ CPointerDetector(CButtonMapping* buttonMapping);
+
+ /*!
+ * \brief Pointer position has been updated
+ *
+ * \param x The new x coordinate
+ * \param y The new y coordinate
+ *
+ * \return Always true - pointer motion events are always absorbed while
+ * button mapping
+ */
+ bool OnMotion(int x, int y);
+
+private:
+ // Utility function
+ static INPUT::INTERCARDINAL_DIRECTION GetPointerDirection(int x, int y);
+
+ static const unsigned int MIN_FRAME_COUNT = 10;
+
+ // State variables
+ bool m_bStarted = false;
+ int m_startX = 0;
+ int m_startY = 0;
+ unsigned int m_frameCount = 0;
+};
+
+/*!
+ * \ingroup joystick
+ * \brief Generic implementation of a class that provides button mapping by
+ * translating driver events to button mapping commands
+ *
+ * Button mapping commands are invoked instantly for buttons and hats.
+ *
+ * Button mapping commands are deferred for a short while after an axis is
+ * activated, and only one button mapping command will be invoked per
+ * activation.
+ */
+class CButtonMapping : public IDriverHandler,
+ public KEYBOARD::IKeyboardDriverHandler,
+ public MOUSE::IMouseDriverHandler,
+ public IButtonMapCallback
+{
+public:
+ /*!
+ * \brief Constructor for CButtonMapping
+ *
+ * \param buttonMapper Carries out button-mapping commands using <buttonMap>
+ * \param buttonMap The button map given to <buttonMapper> on each command
+ */
+ CButtonMapping(IButtonMapper* buttonMapper, IButtonMap* buttonMap, IKeymap* keymap);
+
+ ~CButtonMapping() override = default;
+
+ // implementation of IDriverHandler
+ bool OnButtonMotion(unsigned int buttonIndex, bool bPressed) override;
+ bool OnHatMotion(unsigned int hatIndex, HAT_STATE state) override;
+ bool OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range) override;
+ void OnInputFrame() override;
+
+ // implementation of IKeyboardDriverHandler
+ bool OnKeyPress(const CKey& key) override;
+ void OnKeyRelease(const CKey& key) override {}
+
+ // implementation of IMouseDriverHandler
+ bool OnPosition(int x, int y) override;
+ bool OnButtonPress(MOUSE::BUTTON_ID button) override;
+ void OnButtonRelease(MOUSE::BUTTON_ID button) override;
+
+ // implementation of IButtonMapCallback
+ void SaveButtonMap() override;
+ void ResetIgnoredPrimitives() override;
+ void RevertButtonMap() override;
+
+ /*!
+ * \brief Process the primitive mapping command
+ *
+ * First, this function checks if the input should be dropped. This can
+ * happen if the input is ignored or the cooldown period is active. If the
+ * input is dropped, this returns true with no effect, effectively absorbing
+ * the input. Otherwise, the mapping command is sent to m_buttonMapper.
+ *
+ * \param primitive The primitive being mapped
+ * \return True if the mapping command was handled, false otherwise
+ */
+ bool MapPrimitive(const CDriverPrimitive& primitive);
+
+private:
+ bool IsMapping() const;
+
+ void OnLateDiscovery(unsigned int axisIndex);
+
+ CButtonDetector& GetButton(unsigned int buttonIndex);
+ CHatDetector& GetHat(unsigned int hatIndex);
+ CAxisDetector& GetAxis(unsigned int axisIndex,
+ float position,
+ const AxisConfiguration& initialConfig = AxisConfiguration());
+ CKeyDetector& GetKey(XBMCKey keycode);
+ CMouseButtonDetector& GetMouseButton(MOUSE::BUTTON_ID buttonIndex);
+ CPointerDetector& GetPointer();
+
+ // Construction parameters
+ IButtonMapper* const m_buttonMapper;
+ IButtonMap* const m_buttonMap;
+ IKeymap* const m_keymap;
+
+ std::map<unsigned int, CButtonDetector> m_buttons;
+ std::map<unsigned int, CHatDetector> m_hats;
+ std::map<unsigned int, CAxisDetector> m_axes;
+ std::map<XBMCKey, CKeyDetector> m_keys;
+ std::map<MOUSE::BUTTON_ID, CMouseButtonDetector> m_mouseButtons;
+ std::unique_ptr<CPointerDetector> m_pointer;
+ std::chrono::time_point<std::chrono::steady_clock> m_lastAction;
+ uint64_t m_frameCount;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/generic/CMakeLists.txt b/xbmc/input/joysticks/generic/CMakeLists.txt
new file mode 100644
index 0000000..f44258a
--- /dev/null
+++ b/xbmc/input/joysticks/generic/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES ButtonMapping.cpp
+ DriverReceiving.cpp
+ FeatureHandling.cpp
+ InputHandling.cpp)
+
+set(HEADERS ButtonMapping.h
+ DriverReceiving.h
+ FeatureHandling.h
+ InputHandling.h)
+
+core_add_library(input_joystick_generic)
diff --git a/xbmc/input/joysticks/generic/DriverReceiving.cpp b/xbmc/input/joysticks/generic/DriverReceiving.cpp
new file mode 100644
index 0000000..bf52f4f
--- /dev/null
+++ b/xbmc/input/joysticks/generic/DriverReceiving.cpp
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016-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 "DriverReceiving.h"
+
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IDriverReceiver.h"
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+CDriverReceiving::CDriverReceiving(IDriverReceiver* receiver, IButtonMap* buttonMap)
+ : m_receiver(receiver), m_buttonMap(buttonMap)
+{
+}
+
+bool CDriverReceiving::SetRumbleState(const FeatureName& feature, float magnitude)
+{
+ bool bHandled = false;
+
+ if (m_receiver != nullptr && m_buttonMap != nullptr)
+ {
+ CDriverPrimitive primitive;
+ if (m_buttonMap->GetScalar(feature, primitive))
+ {
+ if (primitive.Type() == PRIMITIVE_TYPE::MOTOR)
+ bHandled = m_receiver->SetMotorState(primitive.Index(), magnitude);
+ }
+ }
+
+ return bHandled;
+}
diff --git a/xbmc/input/joysticks/generic/DriverReceiving.h b/xbmc/input/joysticks/generic/DriverReceiving.h
new file mode 100644
index 0000000..758b560
--- /dev/null
+++ b/xbmc/input/joysticks/generic/DriverReceiving.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016-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 "input/joysticks/JoystickTypes.h"
+#include "input/joysticks/interfaces/IInputReceiver.h"
+
+#include <map>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IDriverReceiver;
+class IButtonMap;
+
+/*!
+ * \ingroup joystick
+ * \brief Class to translate input events from higher-level features to driver primitives
+ *
+ * A button map is used to translate controller features to driver primitives.
+ * The button map has been abstracted away behind the IButtonMap interface
+ * so that it can be provided by an add-on.
+ */
+class CDriverReceiving : public IInputReceiver
+{
+public:
+ CDriverReceiving(IDriverReceiver* receiver, IButtonMap* buttonMap);
+
+ ~CDriverReceiving() override = default;
+
+ // implementation of IInputReceiver
+ bool SetRumbleState(const FeatureName& feature, float magnitude) override;
+
+private:
+ IDriverReceiver* const m_receiver;
+ IButtonMap* const m_buttonMap;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/generic/FeatureHandling.cpp b/xbmc/input/joysticks/generic/FeatureHandling.cpp
new file mode 100644
index 0000000..639ae5b
--- /dev/null
+++ b/xbmc/input/joysticks/generic/FeatureHandling.cpp
@@ -0,0 +1,551 @@
+/*
+ * Copyright (C) 2014-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 "FeatureHandling.h"
+
+#include "ServiceBroker.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerManager.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+#include "utils/log.h"
+
+#include <vector>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+#define ANALOG_DIGITAL_THRESHOLD 0.5f
+#define DISCRETE_ANALOG_RAMPUP_TIME_MS 1500
+#define DISCRETE_ANALOG_START_VALUE 0.3f
+
+// --- CJoystickFeature --------------------------------------------------------
+
+CJoystickFeature::CJoystickFeature(const FeatureName& name,
+ IInputHandler* handler,
+ IButtonMap* buttonMap)
+ : m_name(name),
+ m_handler(handler),
+ m_buttonMap(buttonMap),
+ m_bEnabled(m_handler->HasFeature(name))
+{
+}
+
+bool CJoystickFeature::AcceptsInput(bool bActivation)
+{
+ bool bAcceptsInput = false;
+
+ if (m_bEnabled)
+ {
+ if (m_handler->AcceptsInput(m_name))
+ bAcceptsInput = true;
+ }
+
+ return bAcceptsInput;
+}
+
+void CJoystickFeature::ResetMotion()
+{
+ m_motionStartTimeMs = {};
+}
+
+void CJoystickFeature::StartMotion()
+{
+ m_motionStartTimeMs = std::chrono::steady_clock::now();
+}
+
+bool CJoystickFeature::InMotion() const
+{
+ return m_motionStartTimeMs.time_since_epoch().count() > 0;
+}
+
+unsigned int CJoystickFeature::MotionTimeMs() const
+{
+ if (!InMotion())
+ return 0;
+
+ auto now = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_motionStartTimeMs);
+
+ return duration.count();
+}
+
+// --- CScalarFeature ----------------------------------------------------------
+
+CScalarFeature::CScalarFeature(const FeatureName& name,
+ IInputHandler* handler,
+ IButtonMap* buttonMap)
+ : CJoystickFeature(name, handler, buttonMap),
+ m_bDigitalState(false),
+ m_analogState(0.0f),
+ m_bActivated(false),
+ m_bDiscrete(true)
+{
+ GAME::ControllerPtr controller =
+ CServiceBroker::GetGameControllerManager().GetController(handler->ControllerID());
+ if (controller)
+ m_inputType = controller->GetInputType(name);
+}
+
+bool CScalarFeature::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed)
+{
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(bPressed);
+
+ if (m_inputType == INPUT_TYPE::DIGITAL)
+ bHandled &= OnDigitalMotion(bPressed);
+ else if (m_inputType == INPUT_TYPE::ANALOG)
+ bHandled &= OnAnalogMotion(bPressed ? 1.0f : 0.0f);
+
+ return bHandled;
+}
+
+bool CScalarFeature::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ // Update activated status
+ if (magnitude > 0.0f)
+ m_bActivated = true;
+
+ // Update discrete status
+ if (magnitude != 0.0f && magnitude != 1.0f)
+ m_bDiscrete = false;
+
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(magnitude > 0.0f);
+
+ if (m_inputType == INPUT_TYPE::DIGITAL)
+ bHandled &= OnDigitalMotion(magnitude >= ANALOG_DIGITAL_THRESHOLD);
+ else if (m_inputType == INPUT_TYPE::ANALOG)
+ bHandled &= OnAnalogMotion(magnitude);
+
+ return bHandled;
+}
+
+void CScalarFeature::ProcessMotions(void)
+{
+ if (m_inputType == INPUT_TYPE::DIGITAL && m_bDigitalState)
+ ProcessDigitalMotion();
+ else if (m_inputType == INPUT_TYPE::ANALOG)
+ ProcessAnalogMotion();
+}
+
+bool CScalarFeature::OnDigitalMotion(bool bPressed)
+{
+ bool bHandled = false;
+
+ if (m_bDigitalState != bPressed)
+ {
+ m_bDigitalState = bPressed;
+
+ // Motion is initiated in ProcessMotions()
+ ResetMotion();
+
+ bHandled = m_bInitialPressHandled = m_handler->OnButtonPress(m_name, bPressed);
+
+ if (m_bDigitalState)
+ CLog::Log(LOGDEBUG, "FEATURE [ {} ] on {} pressed ({})", m_name, m_handler->ControllerID(),
+ bHandled ? "handled" : "ignored");
+ else
+ CLog::Log(LOGDEBUG, "FEATURE [ {} ] on {} released", m_name, m_handler->ControllerID());
+ }
+ else if (m_bDigitalState)
+ {
+ bHandled = m_bInitialPressHandled;
+ }
+
+ return bHandled;
+}
+
+bool CScalarFeature::OnAnalogMotion(float magnitude)
+{
+ const bool bActivated = (magnitude != 0.0f);
+
+ // Update analog state
+ m_analogState = magnitude;
+
+ // Update motion time
+ if (!bActivated)
+ ResetMotion();
+ else if (!InMotion())
+ StartMotion();
+
+ // Log activation/deactivation
+ if (m_bDigitalState != bActivated)
+ {
+ m_bDigitalState = bActivated;
+ CLog::Log(LOGDEBUG, "FEATURE [ {} ] on {} {}", m_name, m_handler->ControllerID(),
+ bActivated ? "activated" : "deactivated");
+ }
+
+ return true;
+}
+
+void CScalarFeature::ProcessDigitalMotion()
+{
+ if (!InMotion())
+ {
+ // Button was just pressed, record start time and exit (button press event
+ // was already sent this frame)
+ StartMotion();
+ }
+ else
+ {
+ // Button has been pressed more than one event frame
+ const unsigned int elapsed = MotionTimeMs();
+ m_handler->OnButtonHold(m_name, elapsed);
+ }
+}
+
+void CScalarFeature::ProcessAnalogMotion()
+{
+ float magnitude = m_analogState;
+
+ // Calculate time elapsed since motion began
+ unsigned int elapsed = MotionTimeMs();
+
+ // If analog value is discrete, ramp up magnitude
+ if (m_bActivated && m_bDiscrete)
+ {
+ if (elapsed < DISCRETE_ANALOG_RAMPUP_TIME_MS)
+ {
+ magnitude *= static_cast<float>(elapsed) / DISCRETE_ANALOG_RAMPUP_TIME_MS;
+ if (magnitude < DISCRETE_ANALOG_START_VALUE)
+ magnitude = DISCRETE_ANALOG_START_VALUE;
+ }
+ }
+
+ m_handler->OnButtonMotion(m_name, magnitude, elapsed);
+}
+
+// --- CAxisFeature ------------------------------------------------------------
+
+CAxisFeature::CAxisFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap)
+ : CJoystickFeature(name, handler, buttonMap), m_state(0.0f)
+{
+}
+
+bool CAxisFeature::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed)
+{
+ return OnAnalogMotion(source, bPressed ? 1.0f : 0.0f);
+}
+
+void CAxisFeature::ProcessMotions(void)
+{
+ const float newState = m_axis.GetPosition();
+
+ const bool bActivated = (newState != 0.0f);
+
+ if (!AcceptsInput(bActivated))
+ return;
+
+ const bool bWasActivated = (m_state != 0.0f);
+
+ if (!bActivated && bWasActivated)
+ CLog::Log(LOGDEBUG, "Feature [ {} ] on {} deactivated", m_name, m_handler->ControllerID());
+ else if (bActivated && !bWasActivated)
+ {
+ CLog::Log(LOGDEBUG, "Feature [ {} ] on {} activated {}", m_name, m_handler->ControllerID(),
+ newState > 0.0f ? "positive" : "negative");
+ }
+
+ if (bActivated || bWasActivated)
+ {
+ m_state = newState;
+
+ unsigned int motionTimeMs = 0;
+
+ if (bActivated)
+ {
+ if (!InMotion())
+ StartMotion();
+ else
+ motionTimeMs = MotionTimeMs();
+ }
+ else
+ ResetMotion();
+
+ switch (m_buttonMap->GetFeatureType(m_name))
+ {
+ case FEATURE_TYPE::WHEEL:
+ m_handler->OnWheelMotion(m_name, newState, motionTimeMs);
+ break;
+ case FEATURE_TYPE::THROTTLE:
+ m_handler->OnThrottleMotion(m_name, newState, motionTimeMs);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+// --- CWheel ------------------------------------------------------------------
+
+CWheel::CWheel(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap)
+ : CAxisFeature(name, handler, buttonMap)
+{
+}
+
+bool CWheel::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ WHEEL_DIRECTION direction = WHEEL_DIRECTION::NONE;
+
+ std::vector<WHEEL_DIRECTION> dirs = {
+ WHEEL_DIRECTION::RIGHT,
+ WHEEL_DIRECTION::LEFT,
+ };
+
+ CDriverPrimitive primitive;
+ for (auto dir : dirs)
+ {
+ if (m_buttonMap->GetWheel(m_name, dir, primitive) && primitive == source)
+ {
+ direction = dir;
+ break;
+ }
+ }
+
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(magnitude > 0.0f);
+
+ switch (direction)
+ {
+ case WHEEL_DIRECTION::RIGHT:
+ m_axis.SetPositiveDistance(magnitude);
+ break;
+ case WHEEL_DIRECTION::LEFT:
+ m_axis.SetNegativeDistance(magnitude);
+ break;
+ default:
+ // Just in case, avoid sticking
+ m_axis.Reset();
+ break;
+ }
+
+ return bHandled;
+}
+
+// --- CThrottle ---------------------------------------------------------------
+
+CThrottle::CThrottle(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap)
+ : CAxisFeature(name, handler, buttonMap)
+{
+}
+
+bool CThrottle::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ THROTTLE_DIRECTION direction = THROTTLE_DIRECTION::NONE;
+
+ std::vector<THROTTLE_DIRECTION> dirs = {
+ THROTTLE_DIRECTION::UP,
+ THROTTLE_DIRECTION::DOWN,
+ };
+
+ CDriverPrimitive primitive;
+ for (auto dir : dirs)
+ {
+ if (m_buttonMap->GetThrottle(m_name, dir, primitive) && primitive == source)
+ {
+ direction = dir;
+ break;
+ }
+ }
+
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(magnitude > 0.0f);
+
+ switch (direction)
+ {
+ case THROTTLE_DIRECTION::UP:
+ m_axis.SetPositiveDistance(magnitude);
+ break;
+ case THROTTLE_DIRECTION::DOWN:
+ m_axis.SetNegativeDistance(magnitude);
+ break;
+ default:
+ // Just in case, avoid sticking
+ m_axis.Reset();
+ break;
+ }
+
+ return bHandled;
+}
+
+// --- CAnalogStick ------------------------------------------------------------
+
+CAnalogStick::CAnalogStick(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap)
+ : CJoystickFeature(name, handler, buttonMap), m_vertState(0.0f), m_horizState(0.0f)
+{
+}
+
+bool CAnalogStick::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed)
+{
+ return OnAnalogMotion(source, bPressed ? 1.0f : 0.0f);
+}
+
+bool CAnalogStick::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ ANALOG_STICK_DIRECTION direction = ANALOG_STICK_DIRECTION::NONE;
+
+ std::vector<ANALOG_STICK_DIRECTION> dirs = {
+ ANALOG_STICK_DIRECTION::UP,
+ ANALOG_STICK_DIRECTION::DOWN,
+ ANALOG_STICK_DIRECTION::RIGHT,
+ ANALOG_STICK_DIRECTION::LEFT,
+ };
+
+ CDriverPrimitive primitive;
+ for (auto dir : dirs)
+ {
+ if (m_buttonMap->GetAnalogStick(m_name, dir, primitive) && primitive == source)
+ {
+ direction = dir;
+ break;
+ }
+ }
+
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(magnitude > 0.0f);
+
+ switch (direction)
+ {
+ case ANALOG_STICK_DIRECTION::UP:
+ m_vertAxis.SetPositiveDistance(magnitude);
+ break;
+ case ANALOG_STICK_DIRECTION::DOWN:
+ m_vertAxis.SetNegativeDistance(magnitude);
+ break;
+ case ANALOG_STICK_DIRECTION::RIGHT:
+ m_horizAxis.SetPositiveDistance(magnitude);
+ break;
+ case ANALOG_STICK_DIRECTION::LEFT:
+ m_horizAxis.SetNegativeDistance(magnitude);
+ break;
+ default:
+ // Just in case, avoid sticking
+ m_vertAxis.Reset();
+ m_horizAxis.Reset();
+ break;
+ }
+
+ return bHandled;
+}
+
+void CAnalogStick::ProcessMotions(void)
+{
+ const float newVertState = m_vertAxis.GetPosition();
+ const float newHorizState = m_horizAxis.GetPosition();
+
+ const bool bActivated = (newVertState != 0.0f || newHorizState != 0.0f);
+
+ if (!AcceptsInput(bActivated))
+ return;
+
+ const bool bWasActivated = (m_vertState != 0.0f || m_horizState != 0.0f);
+
+ if (bActivated ^ bWasActivated)
+ {
+ CLog::Log(LOGDEBUG, "Feature [ {} ] on {} {}", m_name, m_handler->ControllerID(),
+ bActivated ? "activated" : "deactivated");
+ }
+
+ if (bActivated || bWasActivated)
+ {
+ m_vertState = newVertState;
+ m_horizState = newHorizState;
+
+ unsigned int motionTimeMs = 0;
+
+ if (bActivated)
+ {
+ if (!InMotion())
+ StartMotion();
+ else
+ motionTimeMs = MotionTimeMs();
+ }
+ else
+ {
+ ResetMotion();
+ }
+
+ m_handler->OnAnalogStickMotion(m_name, newHorizState, newVertState, motionTimeMs);
+ }
+}
+
+// --- CAccelerometer ----------------------------------------------------------
+
+CAccelerometer::CAccelerometer(const FeatureName& name,
+ IInputHandler* handler,
+ IButtonMap* buttonMap)
+ : CJoystickFeature(name, handler, buttonMap),
+ m_xAxisState(0.0f),
+ m_yAxisState(0.0f),
+ m_zAxisState(0.0f)
+{
+}
+
+bool CAccelerometer::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed)
+{
+ return OnAnalogMotion(source, bPressed ? 1.0f : 0.0f);
+}
+
+bool CAccelerometer::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ // Feature must accept input to be considered handled
+ bool bHandled = AcceptsInput(true);
+
+ CDriverPrimitive positiveX;
+ CDriverPrimitive positiveY;
+ CDriverPrimitive positiveZ;
+
+ m_buttonMap->GetAccelerometer(m_name, positiveX, positiveY, positiveZ);
+
+ if (source == positiveX)
+ {
+ m_xAxis.SetPositiveDistance(magnitude);
+ }
+ else if (source == positiveY)
+ {
+ m_yAxis.SetPositiveDistance(magnitude);
+ }
+ else if (source == positiveZ)
+ {
+ m_zAxis.SetPositiveDistance(magnitude);
+ }
+ else
+ {
+ // Just in case, avoid sticking
+ m_xAxis.Reset();
+ m_xAxis.Reset();
+ m_yAxis.Reset();
+ }
+
+ return bHandled;
+}
+
+void CAccelerometer::ProcessMotions(void)
+{
+ const float newXAxis = m_xAxis.GetPosition();
+ const float newYAxis = m_yAxis.GetPosition();
+ const float newZAxis = m_zAxis.GetPosition();
+
+ const bool bActivated = (newXAxis != 0.0f || newYAxis != 0.0f || newZAxis != 0.0f);
+
+ if (!AcceptsInput(bActivated))
+ return;
+
+ const bool bWasActivated = (m_xAxisState != 0.0f || m_yAxisState != 0.0f || m_zAxisState != 0.0f);
+
+ if (bActivated || bWasActivated)
+ {
+ m_xAxisState = newXAxis;
+ m_yAxisState = newYAxis;
+ m_zAxisState = newZAxis;
+ m_handler->OnAccelerometerMotion(m_name, newXAxis, newYAxis, newZAxis);
+ }
+}
diff --git a/xbmc/input/joysticks/generic/FeatureHandling.h b/xbmc/input/joysticks/generic/FeatureHandling.h
new file mode 100644
index 0000000..1a42d5c
--- /dev/null
+++ b/xbmc/input/joysticks/generic/FeatureHandling.h
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2014-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 "input/joysticks/JoystickTypes.h"
+
+#include <chrono>
+#include <memory>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CDriverPrimitive;
+class IInputHandler;
+class IButtonMap;
+
+class CJoystickFeature;
+using FeaturePtr = std::shared_ptr<CJoystickFeature>;
+
+/*!
+ * \ingroup joystick
+ * \brief Base class for joystick features
+ *
+ * See list of feature types in JoystickTypes.h.
+ */
+class CJoystickFeature
+{
+public:
+ CJoystickFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ virtual ~CJoystickFeature() = default;
+
+ /*!
+ * \brief A digital motion has occurred
+ *
+ * \param source The source of the motion. Must be digital (button or hat)
+ * \param bPressed True for press motion, false for release motion
+ *
+ * \return true if the motion was handled, false otherwise
+ */
+ virtual bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) = 0;
+
+ /*!
+ * \brief An analog motion has occurred
+ *
+ * \param source The source of the motion. Must be a semiaxis
+ * \param magnitude The magnitude of the press or motion in the interval [0.0, 1.0]
+ *
+ * For semiaxes, the magnitude is the force or travel distance in the
+ * direction of the semiaxis. If the value is in the opposite direction,
+ * the magnitude is 0.0.
+ *
+ * For example, if the analog stick goes left, the negative semiaxis will
+ * have a value of 1.0 and the positive semiaxis will have a value of 0.0.
+ */
+ virtual bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) = 0;
+
+ /*!
+ * \brief Process the motions that have occurred since the last invocation
+ *
+ * This allows features with motion on multiple driver primitives to call
+ * their handler once all driver primitives are accounted for.
+ */
+ virtual void ProcessMotions(void) = 0;
+
+ /*!
+ * \brief Check if the input handler is accepting input
+ *
+ * \param bActivation True if the motion is activating (true or positive),
+ * false if the motion is deactivating (false or zero)
+ *
+ * \return True if input should be sent to the input handler, false otherwise
+ */
+ bool AcceptsInput(bool bActivation);
+
+protected:
+ /*!
+ * \brief Reset motion timer
+ */
+ void ResetMotion();
+
+ /*!
+ * \brief Start the motion timer
+ */
+ void StartMotion();
+
+ /*!
+ * \brief Check if the feature is in motion
+ */
+ bool InMotion() const;
+
+ /*!
+ * \brief Get the time for which the feature has been in motion
+ */
+ unsigned int MotionTimeMs() const;
+
+ const FeatureName m_name;
+ IInputHandler* const m_handler;
+ IButtonMap* const m_buttonMap;
+ const bool m_bEnabled;
+
+private:
+ std::chrono::time_point<std::chrono::steady_clock> m_motionStartTimeMs;
+};
+
+class CScalarFeature : public CJoystickFeature
+{
+public:
+ CScalarFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CScalarFeature() override = default;
+
+ // implementation of CJoystickFeature
+ bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override;
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override;
+ void ProcessMotions() override;
+
+private:
+ bool OnDigitalMotion(bool bPressed);
+ bool OnAnalogMotion(float magnitude);
+
+ void ProcessDigitalMotion();
+ void ProcessAnalogMotion();
+
+ // State variables
+ INPUT_TYPE m_inputType = INPUT_TYPE::UNKNOWN;
+ bool m_bDigitalState;
+ bool m_bInitialPressHandled = false;
+
+ // Analog state variables
+ float m_analogState; // The current magnitude
+ float m_bActivated; // Set to true when first activated (magnitude > 0.0)
+ bool m_bDiscrete; // Set to false when a non-discrete axis is detected
+};
+
+/*!
+ * \ingroup joystick
+ * \brief Axis of a feature (analog stick, accelerometer, etc)
+ *
+ * Axes are composed of two driver primitives, one for the positive semiaxis
+ * and one for the negative semiaxis.
+ *
+ * This effectively means that an axis is two-dimensional, with each dimension
+ * either:
+ *
+ * - a digital value (0.0 or 1.0)
+ * - an analog value (continuous in the interval [0.0, 1.0])
+ */
+class CFeatureAxis
+{
+public:
+ CFeatureAxis(void) { Reset(); }
+
+ /*!
+ * \brief Set value of positive axis
+ */
+ void SetPositiveDistance(float distance) { m_positiveDistance = distance; }
+
+ /*!
+ * \brief Set value of negative axis
+ */
+ void SetNegativeDistance(float distance) { m_negativeDistance = distance; }
+
+ /*!
+ * \brief Get the final value of this axis.
+ *
+ * This axis is two-dimensional, so we need to compress these into a single
+ * dimension. This is done by subtracting the negative from the positive.
+ * Some examples:
+ *
+ * Positive axis: 1.0 (User presses right or analog stick moves right)
+ * Negative axis: 0.0
+ * -------------------
+ * Pos - Neg: 1.0 (Emulated analog stick moves right)
+ *
+ *
+ * Positive axis: 0.0
+ * Negative axis: 1.0 (User presses left or analog stick moves left)
+ * -------------------
+ * Pos - Neg: -1.0 (Emulated analog stick moves left)
+ *
+ *
+ * Positive axis: 1.0 (User presses both buttons)
+ * Negative axis: 1.0
+ * -------------------
+ * Pos - Neg: 0.0 (Emulated analog stick is centered)
+ *
+ */
+ float GetPosition(void) const { return m_positiveDistance - m_negativeDistance; }
+
+ /*!
+ * \brief Reset both positive and negative values to zero
+ */
+ void Reset(void) { m_positiveDistance = m_negativeDistance = 0.0f; }
+
+protected:
+ float m_positiveDistance;
+ float m_negativeDistance;
+};
+
+class CAxisFeature : public CJoystickFeature
+{
+public:
+ CAxisFeature(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CAxisFeature() override = default;
+
+ // partial implementation of CJoystickFeature
+ bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override;
+ void ProcessMotions() override;
+
+protected:
+ CFeatureAxis m_axis;
+
+ float m_state;
+};
+
+class CWheel : public CAxisFeature
+{
+public:
+ CWheel(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CWheel() override = default;
+
+ // partial implementation of CJoystickFeature
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override;
+};
+
+class CThrottle : public CAxisFeature
+{
+public:
+ CThrottle(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CThrottle() override = default;
+
+ // partial implementation of CJoystickFeature
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override;
+};
+
+class CAnalogStick : public CJoystickFeature
+{
+public:
+ CAnalogStick(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CAnalogStick() override = default;
+
+ // implementation of CJoystickFeature
+ bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override;
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override;
+ void ProcessMotions() override;
+
+protected:
+ CFeatureAxis m_vertAxis;
+ CFeatureAxis m_horizAxis;
+
+ float m_vertState;
+ float m_horizState;
+};
+
+class CAccelerometer : public CJoystickFeature
+{
+public:
+ CAccelerometer(const FeatureName& name, IInputHandler* handler, IButtonMap* buttonMap);
+ ~CAccelerometer() override = default;
+
+ // implementation of CJoystickFeature
+ bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed) override;
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude) override;
+ void ProcessMotions() override;
+
+protected:
+ CFeatureAxis m_xAxis;
+ CFeatureAxis m_yAxis;
+ CFeatureAxis m_zAxis;
+
+ float m_xAxisState;
+ float m_yAxisState;
+ float m_zAxisState;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/generic/InputHandling.cpp b/xbmc/input/joysticks/generic/InputHandling.cpp
new file mode 100644
index 0000000..f6cae6d
--- /dev/null
+++ b/xbmc/input/joysticks/generic/InputHandling.cpp
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2014-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 "InputHandling.h"
+
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "input/joysticks/dialogs/GUIDialogNewJoystick.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+#include "utils/log.h"
+
+#include <array>
+#include <cmath>
+#include <tuple>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+CGUIDialogNewJoystick* const CInputHandling::m_dialog = new CGUIDialogNewJoystick;
+
+CInputHandling::CInputHandling(IInputHandler* handler, IButtonMap* buttonMap)
+ : m_handler(handler), m_buttonMap(buttonMap)
+{
+}
+
+CInputHandling::~CInputHandling(void) = default;
+
+bool CInputHandling::OnButtonMotion(unsigned int buttonIndex, bool bPressed)
+{
+ return OnDigitalMotion(CDriverPrimitive(PRIMITIVE_TYPE::BUTTON, buttonIndex), bPressed);
+}
+
+bool CInputHandling::OnHatMotion(unsigned int hatIndex, HAT_STATE state)
+{
+ bool bHandled = false;
+
+ bHandled |=
+ OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::UP), state & HAT_DIRECTION::UP);
+ bHandled |= OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::RIGHT),
+ state & HAT_DIRECTION::RIGHT);
+ bHandled |=
+ OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::DOWN), state & HAT_DIRECTION::DOWN);
+ bHandled |=
+ OnDigitalMotion(CDriverPrimitive(hatIndex, HAT_DIRECTION::LEFT), state & HAT_DIRECTION::LEFT);
+
+ return bHandled;
+}
+
+bool CInputHandling::OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range)
+{
+ bool bHandled = false;
+
+ if (center != 0)
+ {
+ float translatedPostion = std::min((position - center) / range, 1.0f);
+
+ // Calculate the direction the trigger travels from the center point
+ SEMIAXIS_DIRECTION dir;
+ if (center > 0)
+ dir = SEMIAXIS_DIRECTION::NEGATIVE;
+ else
+ dir = SEMIAXIS_DIRECTION::POSITIVE;
+
+ CDriverPrimitive offsetSemiaxis(axisIndex, center, dir, range);
+
+ bHandled = OnAnalogMotion(offsetSemiaxis, translatedPostion);
+ }
+ else
+ {
+ CDriverPrimitive positiveSemiaxis(axisIndex, 0, SEMIAXIS_DIRECTION::POSITIVE, 1);
+ CDriverPrimitive negativeSemiaxis(axisIndex, 0, SEMIAXIS_DIRECTION::NEGATIVE, 1);
+
+ bHandled |= OnAnalogMotion(positiveSemiaxis, position > 0.0f ? position : 0.0f);
+ bHandled |= OnAnalogMotion(negativeSemiaxis, position < 0.0f ? -position : 0.0f);
+ }
+
+ return bHandled;
+}
+
+void CInputHandling::OnInputFrame(void)
+{
+ // Handle driver input
+ for (auto& it : m_features)
+ it.second->ProcessMotions();
+
+ // Handle higher-level controller input
+ m_handler->OnInputFrame();
+}
+
+bool CInputHandling::OnDigitalMotion(const CDriverPrimitive& source, bool bPressed)
+{
+ bool bHandled = false;
+
+ FeatureName featureName;
+ if (m_buttonMap->GetFeature(source, featureName))
+ {
+ auto it = m_features.find(featureName);
+ if (it == m_features.end())
+ {
+ FeaturePtr feature(CreateFeature(featureName));
+ if (feature)
+ std::tie(it, std::ignore) = m_features.insert({featureName, std::move(feature)});
+ }
+
+ if (it != m_features.end())
+ bHandled = it->second->OnDigitalMotion(source, bPressed);
+ }
+ else if (bPressed)
+ {
+ // If button didn't resolve to a feature, check if the button map is empty
+ // and ask the user if they would like to start mapping the controller
+ if (m_buttonMap->IsEmpty())
+ {
+ CLog::Log(LOGDEBUG, "Empty button map detected for {}", m_buttonMap->ControllerID());
+ m_dialog->ShowAsync();
+ }
+ }
+
+ return bHandled;
+}
+
+bool CInputHandling::OnAnalogMotion(const CDriverPrimitive& source, float magnitude)
+{
+ bool bHandled = false;
+
+ FeatureName featureName;
+ if (m_buttonMap->GetFeature(source, featureName))
+ {
+ auto it = m_features.find(featureName);
+ if (it == m_features.end())
+ {
+ FeaturePtr feature(CreateFeature(featureName));
+ if (feature)
+ std::tie(it, std::ignore) = m_features.insert({featureName, std::move(feature)});
+ }
+
+ if (it != m_features.end())
+ bHandled = it->second->OnAnalogMotion(source, magnitude);
+ }
+
+ return bHandled;
+}
+
+CJoystickFeature* CInputHandling::CreateFeature(const FeatureName& featureName)
+{
+ CJoystickFeature* feature = nullptr;
+
+ switch (m_buttonMap->GetFeatureType(featureName))
+ {
+ case FEATURE_TYPE::SCALAR:
+ {
+ feature = new CScalarFeature(featureName, m_handler, m_buttonMap);
+ break;
+ }
+ case FEATURE_TYPE::ANALOG_STICK:
+ {
+ feature = new CAnalogStick(featureName, m_handler, m_buttonMap);
+ break;
+ }
+ case FEATURE_TYPE::ACCELEROMETER:
+ {
+ feature = new CAccelerometer(featureName, m_handler, m_buttonMap);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return feature;
+}
diff --git a/xbmc/input/joysticks/generic/InputHandling.h b/xbmc/input/joysticks/generic/InputHandling.h
new file mode 100644
index 0000000..772d9d1
--- /dev/null
+++ b/xbmc/input/joysticks/generic/InputHandling.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2014-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 "FeatureHandling.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "input/joysticks/interfaces/IDriverHandler.h"
+
+#include <map>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CDriverPrimitive;
+class CGUIDialogNewJoystick;
+class IInputHandler;
+class IButtonMap;
+
+/*!
+ * \ingroup joystick
+ * \brief Class to translate input from the driver into higher-level features
+ *
+ * Raw driver input arrives for three elements: buttons, hats and axes. When
+ * driver input is handled by this class, it translates the raw driver
+ * elements into physical joystick features, such as buttons, analog sticks,
+ * etc.
+ *
+ * A button map is used to translate driver primitives to controller features.
+ * The button map has been abstracted away behind the IButtonMap
+ * interface so that it can be provided by an add-on.
+ */
+class CInputHandling : public IDriverHandler
+{
+public:
+ CInputHandling(IInputHandler* handler, IButtonMap* buttonMap);
+
+ ~CInputHandling() override;
+
+ // implementation of IDriverHandler
+ bool OnButtonMotion(unsigned int buttonIndex, bool bPressed) override;
+ bool OnHatMotion(unsigned int hatIndex, HAT_STATE state) override;
+ bool OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range) override;
+ void OnInputFrame() override;
+
+private:
+ bool OnDigitalMotion(const CDriverPrimitive& source, bool bPressed);
+ bool OnAnalogMotion(const CDriverPrimitive& source, float magnitude);
+
+ CJoystickFeature* CreateFeature(const FeatureName& featureName);
+
+ IInputHandler* const m_handler;
+ IButtonMap* const m_buttonMap;
+
+ std::map<FeatureName, FeaturePtr> m_features;
+
+ static CGUIDialogNewJoystick* const m_dialog;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/interfaces/IButtonMap.h b/xbmc/input/joysticks/interfaces/IButtonMap.h
new file mode 100644
index 0000000..0b4b7d5
--- /dev/null
+++ b/xbmc/input/joysticks/interfaces/IButtonMap.h
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2014-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 "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/JoystickTypes.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+/*!
+ * \ingroup joystick
+ * \brief Button map interface to translate between the driver's raw
+ * button/hat/axis elements and physical joystick features.
+ *
+ * \sa IButtonMapper
+ */
+class IButtonMap
+{
+public:
+ virtual ~IButtonMap() = default;
+
+ /*!
+ * \brief The add-on ID of the game controller associated with this button map
+ *
+ * The controller ID provided by the implementation serves as the context
+ * for the feature names below.
+ *
+ * \return The ID of this button map's game controller add-on
+ */
+ virtual std::string ControllerID(void) const = 0;
+
+ /*!
+ * \brief The Location of the peripheral associated with this button map
+ *
+ * \return The peripheral's location
+ */
+ virtual std::string Location(void) const = 0;
+
+ /*!
+ * \brief Load the button map into memory
+ *
+ * \return True if button map is ready to start translating buttons, false otherwise
+ */
+ virtual bool Load(void) = 0;
+
+ /*!
+ * \brief Reset the button map to its defaults, or clear button map if no defaults
+ */
+ virtual void Reset(void) = 0;
+
+ /*!
+ * \brief Check if the button map is empty
+ *
+ * \return True if the button map is empty, false if it has features
+ */
+ virtual bool IsEmpty(void) const = 0;
+
+ /*!
+ * \brief Get the ID of the controller profile that best represents the
+ * appearance of the peripheral
+ *
+ * \return The controller ID, or empty if the appearance is unknown
+ */
+ virtual std::string GetAppearance() const = 0;
+
+ /*!
+ * \brief Set the ID of the controller that best represents the appearance
+ * of the peripheral
+ *
+ * \param controllerId The controller ID, or empty to unset the appearance
+ *
+ * \return True if the appearance was set, false on error
+ */
+ virtual bool SetAppearance(const std::string& controllerId) const = 0;
+
+ /*!
+ * \brief Get the feature associated with a driver primitive
+ *
+ * Multiple primitives can be mapped to the same feature. For example,
+ * analog sticks use one primitive for each direction.
+ *
+ * \param primitive The driver primitive
+ * \param feature The name of the resolved joystick feature, or
+ * invalid if false is returned
+ *
+ * \return True if the driver primitive is associated with a feature, false otherwise
+ */
+ virtual bool GetFeature(const CDriverPrimitive& primitive, FeatureName& feature) = 0;
+
+ /*!
+ * \brief Get the type of the feature for the given name
+ *
+ * \param feature The feature to look up
+ *
+ * \return The feature's type
+ */
+ virtual FEATURE_TYPE GetFeatureType(const FeatureName& feature) = 0;
+
+ /*!
+ * \brief Get the driver primitive for a scalar feature
+ *
+ * When a feature can be represented by a single driver primitive, it is
+ * called a scalar feature.
+ *
+ * - This includes buttons and triggers, because they can be mapped to a
+ * single button/hat/semiaxis
+ *
+ * - This does not include analog sticks, because they require two axes
+ * and four driver primitives (one for each semiaxis)
+ *
+ * \param feature Must be a scalar feature (a feature that only
+ * requires a single driver primitive)
+ * \param primitive The resolved driver primitive
+ *
+ * \return True if the feature resolved to a driver primitive, false if the
+ * feature didn't resolve or isn't a scalar feature
+ */
+ virtual bool GetScalar(const FeatureName& feature, CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Add or update a scalar feature
+ *
+ * \param feature Must be a scalar feature
+ * \param primitive The feature's driver primitive
+ *
+ * \return True if the feature was updated, false if the feature is
+ * unchanged or failure occurs
+ */
+ virtual void AddScalar(const FeatureName& feature, const CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Get an analog stick direction from the button map
+ *
+ * \param feature Must be an analog stick or this will return false
+ * \param direction The direction whose primitive is to be retrieved
+ * \param[out] primitive The primitive mapped to the specified direction
+ *
+ * \return True if the feature and direction resolved to a driver primitive
+ */
+ virtual bool GetAnalogStick(const FeatureName& feature,
+ ANALOG_STICK_DIRECTION direction,
+ CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Add or update an analog stick direction
+ *
+ * \param feature Must be an analog stick or this will return false
+ * \param direction The direction being mapped
+ * \param primitive The driver primitive for the specified analog stick and direction
+ *
+ * \return True if the analog stick was updated, false otherwise
+ */
+ virtual void AddAnalogStick(const FeatureName& feature,
+ ANALOG_STICK_DIRECTION direction,
+ const CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Get a relative pointer direction from the button map
+ *
+ * \param feature Must be a relative pointer stick or this will return false
+ * \param direction The direction whose primitive is to be retrieved
+ * \param[out] primitive The primitive mapped to the specified direction
+ *
+ * \return True if the feature and direction resolved to a driver primitive
+ */
+ virtual bool GetRelativePointer(const FeatureName& feature,
+ RELATIVE_POINTER_DIRECTION direction,
+ CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Add or update a relative pointer direction
+ *
+ * \param feature Must be a relative pointer or this will return false
+ * \param direction The direction being mapped
+ * \param primitive The driver primitive for the specified analog stick and direction
+ *
+ * \return True if the analog stick was updated, false otherwise
+ */
+ virtual void AddRelativePointer(const FeatureName& feature,
+ RELATIVE_POINTER_DIRECTION direction,
+ const CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Get an accelerometer from the button map
+ *
+ * \param feature Must be an accelerometer or this will return false
+ * \param positiveX The semiaxis mapped to the positive X direction (possibly unknown)
+ * \param positiveY The semiaxis mapped to the positive Y direction (possibly unknown)
+ * \param positiveZ The semiaxis mapped to the positive Z direction (possibly unknown)
+ *
+ * \return True if the feature resolved to an accelerometer with at least 1 known axis
+ */
+ virtual bool GetAccelerometer(const FeatureName& feature,
+ CDriverPrimitive& positiveX,
+ CDriverPrimitive& positiveY,
+ CDriverPrimitive& positiveZ) = 0;
+
+ /*!
+ * \brief Get or update an accelerometer
+ *
+ * \param feature Must be an accelerometer or this will return false
+ * \param positiveX The semiaxis corresponding to the positive X direction
+ * \param positiveY The semiaxis corresponding to the positive Y direction
+ * \param positiveZ The semiaxis corresponding to the positive Z direction
+ *
+ * The driver primitives must be mapped to a semiaxis or this function will fail.
+ *
+ * \return True if the accelerometer was updated, false if unchanged or failure occurred
+ */
+ virtual void AddAccelerometer(const FeatureName& feature,
+ const CDriverPrimitive& positiveX,
+ const CDriverPrimitive& positiveY,
+ const CDriverPrimitive& positiveZ) = 0;
+
+ /*!
+ * \brief Get a wheel direction from the button map
+ *
+ * \param feature Must be a wheel or this will return false
+ * \param direction The direction whose primitive is to be retrieved
+ * \param[out] primitive The primitive mapped to the specified direction
+ *
+ * \return True if the feature and direction resolved to a driver primitive
+ */
+ virtual bool GetWheel(const FeatureName& feature,
+ WHEEL_DIRECTION direction,
+ CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Add or update a wheel direction
+ *
+ * \param feature Must be a wheel or this will return false
+ * \param direction The direction being mapped
+ * \param primitive The driver primitive for the specified analog stick and direction
+ *
+ * \return True if the analog stick was updated, false otherwise
+ */
+ virtual void AddWheel(const FeatureName& feature,
+ WHEEL_DIRECTION direction,
+ const CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Get a throttle direction from the button map
+ *
+ * \param feature Must be a throttle or this will return false
+ * \param direction The direction whose primitive is to be retrieved
+ * \param[out] primitive The primitive mapped to the specified direction
+ *
+ * \return True if the feature and direction resolved to a driver primitive
+ */
+ virtual bool GetThrottle(const FeatureName& feature,
+ THROTTLE_DIRECTION direction,
+ CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Add or update a throttle direction
+ *
+ * \param feature Must be a throttle or this will return false
+ * \param direction The direction being mapped
+ * \param primitive The driver primitive for the specified analog stick and direction
+ *
+ * \return True if the analog stick was updated, false otherwise
+ */
+ virtual void AddThrottle(const FeatureName& feature,
+ THROTTLE_DIRECTION direction,
+ const CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Get the driver primitive for a keyboard key
+ *
+ * \param feature Must be a key
+ * \param primitive The resolved driver primitive
+ *
+ * \return True if the feature resolved to a driver primitive, false if the
+ * feature didn't resolve or isn't a scalar feature
+ */
+ virtual bool GetKey(const FeatureName& feature, CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Add or update a key
+ *
+ * \param feature Must be a key
+ * \param primitive The feature's driver primitive
+ *
+ * \return True if the feature was updated, false if the feature is
+ * unchanged or failure occurs
+ */
+ virtual void AddKey(const FeatureName& feature, const CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Set a list of driver primitives to be ignored
+ *
+ * This is necessary to prevent features from interfering with the button
+ * mapping process. This includes accelerometers, as well as PS4 triggers
+ * which send both a button press and an analog value.
+ *
+ * \param primitives The driver primitives to be ignored
+ */
+ virtual void SetIgnoredPrimitives(const std::vector<CDriverPrimitive>& primitives) = 0;
+
+ /*!
+ * \brief Check if a primitive is in the list of primitives to be ignored
+ *
+ * \param primitive The primitive to check
+ *
+ * \return True if the primitive should be ignored in the mapping process
+ */
+ virtual bool IsIgnored(const CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Get the properties of an axis
+ *
+ * \param axisIndex The index of the axis to check
+ * \param center[out] The center, if known
+ * \param range[out] The range, if known
+ *
+ * \return True if the properties are known, false otherwise
+ */
+ virtual bool GetAxisProperties(unsigned int axisIndex, int& center, unsigned int& range) = 0;
+
+ /*!
+ * \brief Save the button map
+ */
+ virtual void SaveButtonMap() = 0;
+
+ /*!
+ * \brief Revert changes to the button map since the last time it was loaded
+ * or committed to disk
+ */
+ virtual void RevertButtonMap() = 0;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/interfaces/IButtonMapCallback.h b/xbmc/input/joysticks/interfaces/IButtonMapCallback.h
new file mode 100644
index 0000000..40744f8
--- /dev/null
+++ b/xbmc/input/joysticks/interfaces/IButtonMapCallback.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016-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
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+/*!
+ * \brief Interface for handling button maps
+ */
+class IButtonMapCallback
+{
+public:
+ virtual ~IButtonMapCallback() = default;
+
+ /*!
+ * \brief Save the button map
+ */
+ virtual void SaveButtonMap() = 0;
+
+ /*!
+ * \brief Clear the list of ignored driver primitives
+ *
+ * Called if the user begins capturing primitives to be ignored, and
+ * no primitives are captured before the dialog is accepted by the user.
+ *
+ * In this case, the button mapper won't have been given access to the
+ * button map, so a callback is needed to indicate that no primitives were
+ * captured and the user accepted this.
+ */
+ virtual void ResetIgnoredPrimitives() = 0;
+
+ /*!
+ * \brief Revert changes to the button map since the last time it was loaded
+ * or committed to disk
+ */
+ virtual void RevertButtonMap() = 0;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/interfaces/IButtonMapper.h b/xbmc/input/joysticks/interfaces/IButtonMapper.h
new file mode 100644
index 0000000..d0784df
--- /dev/null
+++ b/xbmc/input/joysticks/interfaces/IButtonMapper.h
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2014-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 "input/joysticks/JoystickTypes.h"
+
+#include <map>
+#include <string>
+
+class IKeymap;
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CDriverPrimitive;
+class IButtonMap;
+class IButtonMapCallback;
+
+/*!
+ * \ingroup joystick
+ * \brief Button mapper interface to assign the driver's raw button/hat/axis
+ * elements to physical joystick features using a provided button map.
+ *
+ * \sa IButtonMap
+ */
+class IButtonMapper
+{
+public:
+ IButtonMapper() = default;
+
+ virtual ~IButtonMapper() = default;
+
+ /*!
+ * \brief The add-on ID of the game controller associated with this button mapper
+ *
+ * \return The ID of the add-on extending kodi.game.controller
+ */
+ virtual std::string ControllerID(void) const = 0;
+
+ /*!
+ * \brief Return true if the button mapper wants a cooldown between button
+ * mapping commands
+ *
+ * \return True to only send button mapping commands that occur after a small
+ * timeout from the previous command.
+ */
+ virtual bool NeedsCooldown(void) const = 0;
+
+ /*!
+ * \brief Return true if the button mapper accepts primitives of the given type
+ *
+ * \param type The primitive type
+ *
+ * \return True if the button mapper can map the primitive type, false otherwise
+ */
+ virtual bool AcceptsPrimitive(PRIMITIVE_TYPE type) const = 0;
+
+ /*!
+ * \brief Handle button/hat press or axis threshold
+ *
+ * \param buttonMap The button map being manipulated
+ * \param keymap An interface capable of translating features to Kodi actions
+ * \param primitive The driver primitive
+ *
+ * Called in the same thread as \ref IButtonMapper::OnFrame.
+ *
+ * \return True if driver primitive was mapped to a feature
+ */
+ virtual bool MapPrimitive(IButtonMap* buttonMap,
+ IKeymap* keyMap,
+ const CDriverPrimitive& primitive) = 0;
+
+ /*!
+ * \brief Called once per event frame to notify the implementation of motion status
+ *
+ * \param buttonMap The button map passed to MapPrimitive() (shall not be modified)
+ * \param bMotion True if a previously-mapped axis is still in motion
+ *
+ * This allows the implementer to wait for an axis to be centered before
+ * allowing it to be used as Kodi input.
+ *
+ * If mapping finishes on an axis, then the axis will still be pressed and
+ * sending input every frame when the mapping ends. For example, when the
+ * right analog stick is the last feature to be mapped, it is still pressed
+ * when mapping ends and immediately sends Volume Down actions.
+ *
+ * The fix is to allow implementers to wait until all axes are motionless
+ * before detaching themselves.
+ *
+ * Called in the same thread as \ref IButtonMapper::MapPrimitive.
+ */
+ virtual void OnEventFrame(const IButtonMap* buttonMap, bool bMotion) = 0;
+
+ /*!
+ * \brief Called when an axis has been detected after mapping began
+ *
+ * \param axisIndex The index of the axis being discovered
+ *
+ * Some joystick drivers don't report an initial value for analog axes.
+ *
+ * Called in the same thread as \ref IButtonMapper::MapPrimitive.
+ */
+ virtual void OnLateAxis(const IButtonMap* buttonMap, unsigned int axisIndex) = 0;
+
+ // Button map callback interface
+ void SetButtonMapCallback(const std::string& deviceLocation, IButtonMapCallback* callback)
+ {
+ m_callbacks[deviceLocation] = callback;
+ }
+ void ResetButtonMapCallbacks(void) { m_callbacks.clear(); }
+ std::map<std::string, IButtonMapCallback*>& ButtonMapCallbacks(void) { return m_callbacks; }
+
+private:
+ std::map<std::string, IButtonMapCallback*> m_callbacks; // Device location -> callback
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/interfaces/IButtonSequence.h b/xbmc/input/joysticks/interfaces/IButtonSequence.h
new file mode 100644
index 0000000..5c642e6
--- /dev/null
+++ b/xbmc/input/joysticks/interfaces/IButtonSequence.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016-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 "input/joysticks/JoystickTypes.h"
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IButtonSequence
+{
+public:
+ virtual ~IButtonSequence() = default;
+
+ virtual bool OnButtonPress(const FeatureName& feature) = 0;
+
+ /*!
+ * \brief Returns true if a sequence is being captured to prevent input
+ * from falling through to the application
+ */
+ virtual bool IsCapturing() = 0;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/interfaces/IDriverHandler.h b/xbmc/input/joysticks/interfaces/IDriverHandler.h
new file mode 100644
index 0000000..48e4da5
--- /dev/null
+++ b/xbmc/input/joysticks/interfaces/IDriverHandler.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014-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 "input/joysticks/JoystickTypes.h"
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+/*!
+ * \brief Interface defining methods to handle joystick events for raw driver
+ * elements (buttons, hats, axes)
+ */
+class IDriverHandler
+{
+public:
+ virtual ~IDriverHandler() = default;
+
+ /*!
+ * \brief Handle button motion
+ *
+ * \param buttonIndex The index of the button as reported by the driver
+ * \param bPressed true for press motion, false for release motion
+ *
+ * \return True if a press was handled, false otherwise
+ */
+ virtual bool OnButtonMotion(unsigned int buttonIndex, bool bPressed) = 0;
+
+ /*!
+ * \brief Handle hat motion
+ *
+ * \param hatIndex The index of the hat as reported by the driver
+ * \param state The direction the hat is now being pressed
+ *
+ * \return True if the new direction was handled, false otherwise
+ */
+ virtual bool OnHatMotion(unsigned int hatIndex, HAT_STATE state) = 0;
+
+ /*!
+ * \brief Handle axis motion
+ *
+ * If a joystick feature requires multiple axes (analog sticks, accelerometers),
+ * they can be buffered for later processing.
+ *
+ * \param axisIndex The index of the axis as reported by the driver
+ * \param position The position of the axis in the closed interval [-1.0, 1.0]
+ * \param center The center point of the axis (either -1, 0 or 1)
+ * \param range The maximum distance the axis can move (either 1 or 2)
+ *
+ * \return True if the motion was handled, false otherwise
+ */
+ virtual bool OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range) = 0;
+
+ /*!
+ * \brief Handle buffered input motion for features that require multiple axes
+ *
+ * OnInputFrame() is called at the end of the frame when all axis motions
+ * have been reported. This has several uses, including:
+ *
+ * - Combining multiple axes into a single analog stick or accelerometer event
+ * - Imitating an analog feature with a digital button so that events can be
+ * dispatched every frame.
+ */
+ virtual void OnInputFrame(void) = 0;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/interfaces/IDriverReceiver.h b/xbmc/input/joysticks/interfaces/IDriverReceiver.h
new file mode 100644
index 0000000..2a4ff6c
--- /dev/null
+++ b/xbmc/input/joysticks/interfaces/IDriverReceiver.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016-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
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+/*!
+ * \ingroup joystick
+ * \brief Interface for sending input events to joystick drivers
+ */
+class IDriverReceiver
+{
+public:
+ virtual ~IDriverReceiver() = default;
+
+ /*!
+ * \brief Set the value of a rumble motor
+ *
+ * \param motorIndex The driver index of the motor to rumble
+ * \param magnitude The motor's new magnitude of vibration in the closed interval [0, 1]
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool SetMotorState(unsigned int motorIndex, float magnitude) = 0;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/interfaces/IInputHandler.h b/xbmc/input/joysticks/interfaces/IInputHandler.h
new file mode 100644
index 0000000..2ddbf5c
--- /dev/null
+++ b/xbmc/input/joysticks/interfaces/IInputHandler.h
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2014-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 "input/joysticks/JoystickTypes.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IInputReceiver;
+
+/*!
+ * \ingroup joystick
+ * \brief Interface for handling input events for game controllers
+ */
+class IInputHandler
+{
+public:
+ virtual ~IInputHandler() = default;
+
+ /*!
+ * \brief The add-on ID of the game controller associated with this input handler
+ *
+ * \return The ID of the add-on extending kodi.game.controller
+ */
+ virtual std::string ControllerID(void) const = 0;
+
+ /*!
+ * \brief Return true if the input handler accepts the given feature
+ *
+ * \param feature A feature belonging to the controller specified by ControllerID()
+ *
+ * \return True if the feature is used for input, false otherwise
+ */
+ virtual bool HasFeature(const FeatureName& feature) const = 0;
+
+ /*!
+ * \brief Return true if the input handler is currently accepting input for the
+ * given feature
+ *
+ * \param feature A feature belonging to the controller specified by ControllerID()
+ *
+ * \return True if the feature is currently accepting input, false otherwise
+ *
+ * This does not prevent the input events from being called, but can return
+ * false to indicate that input wasn't handled for the specified feature.
+ */
+ virtual bool AcceptsInput(const FeatureName& feature) const = 0;
+
+ /*!
+ * \brief A digital button has been pressed or released
+ *
+ * \param feature The feature being pressed
+ * \param bPressed True if pressed, false if released
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnButtonPress(const FeatureName& feature, bool bPressed) = 0;
+
+ /*!
+ * \brief A digital button has been pressed for more than one event frame
+ *
+ * \param feature The feature being held
+ * \param holdTimeMs The time elapsed since the initial press (ms)
+ *
+ * If OnButtonPress() returns true for the initial press, then this callback
+ * is invoked on subsequent frames until the button is released.
+ */
+ virtual void OnButtonHold(const FeatureName& feature, unsigned int holdTimeMs) = 0;
+
+ /*!
+ * \brief An analog button (trigger or a pressure-sensitive button) has changed state
+ *
+ * \param feature The feature changing state
+ * \param magnitude The button pressure or trigger travel distance in the
+ * closed interval [0, 1]
+ * \param motionTimeMs The time elapsed since the magnitude was 0
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnButtonMotion(const FeatureName& feature,
+ float magnitude,
+ unsigned int motionTimeMs) = 0;
+
+ /*!
+ * \brief An analog stick has moved
+ *
+ * \param feature The analog stick being moved
+ * \param x The x coordinate in the closed interval [-1, 1]
+ * \param y The y coordinate in the closed interval [-1, 1]
+ * \param motionTimeMs The time elapsed since this analog stick was centered,
+ * or 0 if the analog stick is centered
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnAnalogStickMotion(const FeatureName& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs) = 0;
+
+ /*!
+ * \brief An accelerometer's state has changed
+ *
+ * \param feature The accelerometer being accelerated
+ * \param x The x coordinate in the closed interval [-1, 1]
+ * \param y The y coordinate in the closed interval [-1, 1]
+ * \param z The z coordinate in the closed interval [-1, 1]
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnAccelerometerMotion(const FeatureName& feature, float x, float y, float z)
+ {
+ return false;
+ }
+
+ /*!
+ * \brief A wheel has changed state
+ *
+ * Left is negative position, right is positive position
+ *
+ * \param feature The wheel changing state
+ * \param position The position in the closed interval [-1, 1]
+ * \param motionTimeMs The time elapsed since the position was 0
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnWheelMotion(const FeatureName& feature,
+ float position,
+ unsigned int motionTimeMs) = 0;
+
+ /*!
+ * \brief A throttle has changed state
+ *
+ * Up is positive position, down is negative position.
+ *
+ * \param feature The wheel changing state
+ * \param position The position in the closed interval [-1, 1]
+ * \param motionTimeMs The time elapsed since the position was 0
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnThrottleMotion(const FeatureName& feature,
+ float position,
+ unsigned int motionTimeMs) = 0;
+
+ /*!
+ * \brief Called at the end of the frame that provided input
+ */
+ virtual void OnInputFrame() = 0;
+
+ // Input receiver interface
+ void SetInputReceiver(IInputReceiver* receiver) { m_receiver = receiver; }
+ void ResetInputReceiver(void) { m_receiver = nullptr; }
+ IInputReceiver* InputReceiver(void) { return m_receiver; }
+
+private:
+ IInputReceiver* m_receiver = nullptr;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/interfaces/IInputProvider.h b/xbmc/input/joysticks/interfaces/IInputProvider.h
new file mode 100644
index 0000000..09a0366
--- /dev/null
+++ b/xbmc/input/joysticks/interfaces/IInputProvider.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017-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
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IInputHandler;
+
+/*!
+ * \ingroup joystick
+ * \brief Interface for classes that can provide input
+ */
+class IInputProvider
+{
+public:
+ virtual ~IInputProvider() = default;
+
+ /*!
+ * \brief Register a handler for the provided input
+ *
+ * \param handler The handler to receive input provided by this class
+ * \param bPromiscuous If true, receives all input (including handled input)
+ * in the background
+ */
+ virtual void RegisterInputHandler(IInputHandler* handler, bool bPromiscuous) = 0;
+
+ /*!
+ * \brief Unregister a handler
+ *
+ * \param handler The handler that was receiving input
+ */
+ virtual void UnregisterInputHandler(IInputHandler* handler) = 0;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/interfaces/IInputReceiver.h b/xbmc/input/joysticks/interfaces/IInputReceiver.h
new file mode 100644
index 0000000..400da3f
--- /dev/null
+++ b/xbmc/input/joysticks/interfaces/IInputReceiver.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014-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 "input/joysticks/JoystickTypes.h"
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+/*!
+ * \ingroup joystick
+ * \brief Interface for sending input events to game controllers
+ */
+class IInputReceiver
+{
+public:
+ virtual ~IInputReceiver() = default;
+
+ /*!
+ * \brief Set the value of a rumble motor
+ *
+ * \param feature The name of the motor to rumble
+ * \param magnitude The motor's new magnitude of vibration in the closed interval [0, 1]
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool SetRumbleState(const FeatureName& feature, float magnitude) = 0;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/interfaces/IKeyHandler.h b/xbmc/input/joysticks/interfaces/IKeyHandler.h
new file mode 100644
index 0000000..30f49c5
--- /dev/null
+++ b/xbmc/input/joysticks/interfaces/IKeyHandler.h
@@ -0,0 +1,56 @@
+/*
+ * 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
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+/*!
+ * \ingroup joystick
+ * \brief Interface for handling keymap keys
+ *
+ * Keys can be mapped to analog actions (e.g. "AnalogSeekForward") or digital
+ * actions (e.g. "Up").
+ */
+class IKeyHandler
+{
+public:
+ virtual ~IKeyHandler() = default;
+
+ /*!
+ * \brief Return true if the key is "pressed" (has a magnitude greater
+ * than 0.5)
+ *
+ * \return True if the key is "pressed", false otherwise
+ */
+ virtual bool IsPressed() const = 0;
+
+ /*!
+ * \brief A key mapped to a digital feature has been pressed or released
+ *
+ * \param bPressed true if the key's button/axis is activated, false if deactivated
+ * \param holdTimeMs The held time in ms for pressed buttons, or 0 for released
+ *
+ * \return True if the key is mapped to an action, false otherwise
+ */
+ virtual bool OnDigitalMotion(bool bPressed, unsigned int holdTimeMs) = 0;
+
+ /*!
+ * \brief Callback for keys mapped to analog features
+ *
+ * \param magnitude The amount of the analog action
+ * \param motionTimeMs The time since the magnitude was 0
+ *
+ * \return True if the key is mapped to an action, false otherwise
+ */
+ virtual bool OnAnalogMotion(float magnitude, unsigned int motionTimeMs) = 0;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/interfaces/IKeymapHandler.h b/xbmc/input/joysticks/interfaces/IKeymapHandler.h
new file mode 100644
index 0000000..f0af427
--- /dev/null
+++ b/xbmc/input/joysticks/interfaces/IKeymapHandler.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017-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 <set>
+#include <string>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+/*!
+ * \ingroup joystick
+ * \brief Interface for a class working with a keymap
+ */
+class IKeymapHandler
+{
+public:
+ virtual ~IKeymapHandler() = default;
+
+ /*!
+ * \brief Get the pressed state of the given keys
+ *
+ * \param keyNames The key names
+ *
+ * \return True if all keys are pressed or no keys are given, false otherwise
+ */
+ virtual bool HotkeysPressed(const std::set<std::string>& keyNames) const = 0;
+
+ /*!
+ * \brief Get the key name of the last button pressed
+ *
+ * \return The key name of the last-pressed button, or empty if no button
+ * is pressed
+ */
+ virtual std::string GetLastPressed() const = 0;
+
+ /*!
+ * \brief Called when a key has emitted an action after bring pressed
+ *
+ * \param keyName the key name that emitted the action
+ */
+ virtual void OnPress(const std::string& keyName) = 0;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/keymaps/CMakeLists.txt b/xbmc/input/joysticks/keymaps/CMakeLists.txt
new file mode 100644
index 0000000..c854b36
--- /dev/null
+++ b/xbmc/input/joysticks/keymaps/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES KeyHandler.cpp
+ KeymapHandler.cpp
+ KeymapHandling.cpp
+)
+
+set(HEADERS KeyHandler.h
+ KeymapHandler.h
+ KeymapHandling.h
+)
+
+core_add_library(input_joystick_keymaps)
diff --git a/xbmc/input/joysticks/keymaps/KeyHandler.cpp b/xbmc/input/joysticks/keymaps/KeyHandler.cpp
new file mode 100644
index 0000000..cd0f30a
--- /dev/null
+++ b/xbmc/input/joysticks/keymaps/KeyHandler.cpp
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2017-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 "KeyHandler.h"
+
+#include "input/IKeymap.h"
+#include "input/actions/ActionIDs.h"
+#include "input/actions/ActionTranslator.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "input/joysticks/interfaces/IKeymapHandler.h"
+#include "interfaces/IActionListener.h"
+
+#include <algorithm>
+#include <assert.h>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+#define DIGITAL_ANALOG_THRESHOLD 0.5f
+
+#define HOLD_TIMEOUT_MS 500
+#define REPEAT_TIMEOUT_MS 50
+
+CKeyHandler::CKeyHandler(const std::string& keyName,
+ IActionListener* actionHandler,
+ const IKeymap* keymap,
+ IKeymapHandler* keymapHandler)
+ : m_keyName(keyName),
+ m_actionHandler(actionHandler),
+ m_keymap(keymap),
+ m_keymapHandler(keymapHandler)
+{
+ assert(m_actionHandler != nullptr);
+ assert(m_keymap != nullptr);
+ assert(m_keymapHandler != nullptr);
+
+ Reset();
+}
+
+void CKeyHandler::Reset()
+{
+ m_bHeld = false;
+ m_magnitude = 0.0f;
+ m_holdStartTimeMs = 0;
+ m_lastHoldTimeMs = 0;
+ m_bActionSent = false;
+ m_lastActionMs = 0;
+ m_activeWindowId = -1;
+ m_lastAction = CAction();
+}
+
+bool CKeyHandler::OnDigitalMotion(bool bPressed, unsigned int holdTimeMs)
+{
+ return OnAnalogMotion(bPressed ? 1.0f : 0.0f, holdTimeMs);
+}
+
+bool CKeyHandler::OnAnalogMotion(float magnitude, unsigned int motionTimeMs)
+{
+ // Don't send deactivation event more than once
+ if (m_magnitude == 0.0f && magnitude == 0.0f)
+ return false;
+
+ // Get actions for the key
+ const auto& actionGroup = m_keymap->GetActions(m_keyName);
+ const int windowId = actionGroup.windowId;
+ const auto& actions = actionGroup.actions;
+
+ // Calculate press state
+ const bool bPressed = IsPressed(magnitude);
+ const bool bJustPressed = bPressed && !m_bHeld;
+
+ if (bJustPressed)
+ {
+ // Reset key if just pressed
+ Reset();
+
+ // Record hold start time if just pressed
+ m_holdStartTimeMs = motionTimeMs;
+
+ // Record window ID
+ if (windowId >= 0)
+ m_activeWindowId = windowId;
+ }
+
+ // Calculate holdtime relative to when magnitude crossed the threshold
+ unsigned int holdTimeMs = 0;
+ if (bPressed)
+ holdTimeMs = motionTimeMs - m_holdStartTimeMs;
+
+ // Give priority to actions with hotkeys
+ std::vector<const KeymapAction*> actionsWithHotkeys;
+
+ for (const auto& action : actions)
+ {
+ if (!action.hotkeys.empty())
+ actionsWithHotkeys.emplace_back(&action);
+ }
+
+ CAction dispatchAction =
+ ProcessActions(std::move(actionsWithHotkeys), windowId, magnitude, holdTimeMs);
+
+ // If that failed, try again with all actions
+ if (dispatchAction.GetID() == ACTION_NONE)
+ {
+ std::vector<const KeymapAction*> allActions;
+
+ allActions.reserve(actions.size());
+ for (const auto& action : actions)
+ allActions.emplace_back(&action);
+
+ dispatchAction = ProcessActions(std::move(allActions), windowId, magnitude, holdTimeMs);
+ }
+
+ // If specific action was sent last frame but not this one, send a release event
+ if (dispatchAction.GetID() != m_lastAction.GetID())
+ {
+ if (CActionTranslator::IsAnalog(m_lastAction.GetID()) && m_lastAction.GetAmount() > 0.0f)
+ {
+ m_lastAction.ClearAmount();
+ m_actionHandler->OnAction(m_lastAction);
+ }
+ }
+
+ // Dispatch action
+ bool bHandled = false;
+ if (dispatchAction.GetID() != ACTION_NONE)
+ {
+ m_actionHandler->OnAction(dispatchAction);
+ bHandled = true;
+ }
+
+ m_bHeld = bPressed;
+ m_magnitude = magnitude;
+ m_lastHoldTimeMs = holdTimeMs;
+ m_lastAction = dispatchAction;
+
+ return bHandled;
+}
+
+CAction CKeyHandler::ProcessActions(std::vector<const KeymapAction*> actions,
+ int windowId,
+ float magnitude,
+ unsigned int holdTimeMs)
+{
+ CAction dispatchAction;
+
+ // Filter out actions without pressed hotkeys
+ actions.erase(std::remove_if(actions.begin(), actions.end(),
+ [this](const KeymapAction* action) {
+ return !m_keymapHandler->HotkeysPressed(action->hotkeys);
+ }),
+ actions.end());
+
+ if (actions.empty())
+ return false;
+
+ // Actions are sorted by holdtime, so the final action is the one with the
+ // greatest holdtime
+ const KeymapAction& finalAction = **actions.rbegin();
+ const unsigned int maxHoldTimeMs = finalAction.holdTimeMs;
+
+ const bool bHasDelay = (maxHoldTimeMs > 0);
+ if (!bHasDelay)
+ {
+ dispatchAction = ProcessAction(finalAction, windowId, magnitude, holdTimeMs);
+ }
+ else
+ {
+ // If holdtime has exceeded the last action, execute it now
+ if (holdTimeMs >= finalAction.holdTimeMs)
+ {
+ // Force holdtime to zero for the initial press
+ if (!m_bActionSent)
+ holdTimeMs = 0;
+ else
+ holdTimeMs -= finalAction.holdTimeMs;
+
+ dispatchAction = ProcessAction(finalAction, windowId, magnitude, holdTimeMs);
+ }
+ else
+ {
+ // Calculate press state
+ const bool bPressed = IsPressed(magnitude);
+ const bool bJustReleased = m_bHeld && !bPressed;
+
+ // If button was just released, send a release action
+ if (bJustReleased)
+ dispatchAction = ProcessRelease(actions, windowId);
+ }
+ }
+
+ return dispatchAction;
+}
+
+CAction CKeyHandler::ProcessRelease(std::vector<const KeymapAction*> actions, int windowId)
+{
+ CAction dispatchAction;
+
+ // Use previous holdtime from before button release
+ const unsigned int holdTimeMs = m_lastHoldTimeMs;
+
+ // Send an action on release if one occurs before the holdtime
+ for (auto it = actions.begin(); it != actions.end();)
+ {
+ const KeymapAction& action = **it;
+
+ unsigned int thisHoldTime = (*it)->holdTimeMs;
+
+ ++it;
+ if (it == actions.end())
+ break;
+
+ unsigned int nextHoldTime = (*it)->holdTimeMs;
+
+ if (thisHoldTime <= holdTimeMs && holdTimeMs < nextHoldTime)
+ {
+ dispatchAction = ProcessAction(action, windowId, 1.0f, 0);
+ break;
+ }
+ }
+
+ return dispatchAction;
+}
+
+CAction CKeyHandler::ProcessAction(const KeymapAction& action,
+ int windowId,
+ float magnitude,
+ unsigned int holdTimeMs)
+{
+ CAction dispatchAction;
+
+ bool bSendAction = false;
+
+ if (windowId != m_activeWindowId)
+ {
+ // Don't send actions if the window has changed since being pressed
+ }
+ else if (CActionTranslator::IsAnalog(action.actionId))
+ {
+ bSendAction = true;
+ }
+ else if (IsPressed(magnitude))
+ {
+ // Dispatch action if button was pressed this frame
+ if (holdTimeMs == 0)
+ bSendAction = true;
+ else
+ bSendAction = SendRepeatAction(holdTimeMs);
+ }
+
+ if (bSendAction)
+ {
+ const CAction guiAction(action.actionId, magnitude, 0.0f, action.actionString, holdTimeMs);
+ m_keymapHandler->OnPress(m_keyName);
+ m_bActionSent = true;
+ m_lastActionMs = holdTimeMs;
+ dispatchAction = guiAction;
+ }
+
+ return dispatchAction;
+}
+
+bool CKeyHandler::SendRepeatAction(unsigned int holdTimeMs)
+{
+ bool bSendRepeat = true;
+
+ // Don't send a repeat action if the last key has changed
+ if (m_keymapHandler->GetLastPressed() != m_keyName)
+ bSendRepeat = false;
+
+ // Ensure initial timeout has elapsed
+ else if (holdTimeMs < HOLD_TIMEOUT_MS)
+ bSendRepeat = false;
+
+ // Ensure repeat timeout has elapsed
+ else if (holdTimeMs < m_lastActionMs + REPEAT_TIMEOUT_MS)
+ bSendRepeat = false;
+
+ return bSendRepeat;
+}
+
+bool CKeyHandler::IsPressed(float magnitude)
+{
+ return magnitude >= DIGITAL_ANALOG_THRESHOLD;
+}
diff --git a/xbmc/input/joysticks/keymaps/KeyHandler.h b/xbmc/input/joysticks/keymaps/KeyHandler.h
new file mode 100644
index 0000000..3b5a46c
--- /dev/null
+++ b/xbmc/input/joysticks/keymaps/KeyHandler.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2017-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 "input/actions/Action.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "input/joysticks/interfaces/IKeyHandler.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+class CAction;
+class IActionListener;
+class IKeymap;
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IKeymapHandler;
+
+/*!
+ * \ingroup joystick
+ * \brief
+ */
+class CKeyHandler : public IKeyHandler
+{
+public:
+ CKeyHandler(const std::string& keyName,
+ IActionListener* actionHandler,
+ const IKeymap* keymap,
+ IKeymapHandler* keymapHandler);
+
+ ~CKeyHandler() override = default;
+
+ // implementation of IKeyHandler
+ bool IsPressed() const override { return m_bHeld; }
+ bool OnDigitalMotion(bool bPressed, unsigned int holdTimeMs) override;
+ bool OnAnalogMotion(float magnitude, unsigned int motionTimeMs) override;
+
+private:
+ void Reset();
+
+ /*!
+ * \brief Process actions to see if an action should be dispatched
+ *
+ * \param actions All actions from the keymap defined for the current window
+ * \param windowId The current window ID
+ * \param magnitude The magnitude or distance of the feature being handled
+ * \param holdTimeMs The time which the feature has been past the hold threshold
+ *
+ * \return The action to dispatch, or action with ID ACTION_NONE if no action should be dispatched
+ */
+ CAction ProcessActions(std::vector<const KeymapAction*> actions,
+ int windowId,
+ float magnitude,
+ unsigned int holdTimeMs);
+
+ /*!
+ * \brief Process actions after release event to see if an action should be dispatched
+ *
+ * \param actions All actions from the keymap defined for the current window
+ * \param windowId The current window ID
+ *
+ * \return The action to dispatch, or action with ID ACTION_NONE if no action should be dispatched
+ */
+ CAction ProcessRelease(std::vector<const KeymapAction*> actions, int windowId);
+
+ /*!
+ * \brief Process an action to see if it should be dispatched
+ *
+ * \param action The action chosen to be dispatched
+ * \param windowId The current window ID
+ * \param magnitude The magnitude or distance of the feature being handled
+ * \param holdTimeMs The time which the feature has been past the hold threshold
+ *
+ * \return The action to dispatch, or action with ID ACTION_NONE if no action should be dispatched
+ */
+ CAction ProcessAction(const KeymapAction& action,
+ int windowId,
+ float magnitude,
+ unsigned int holdTimeMs);
+
+ // Check criteria for sending a repeat action
+ bool SendRepeatAction(unsigned int holdTimeMs);
+
+ // Helper function
+ static bool IsPressed(float magnitude);
+
+ // Construction parameters
+ const std::string m_keyName;
+ IActionListener* const m_actionHandler;
+ const IKeymap* const m_keymap;
+ IKeymapHandler* const m_keymapHandler;
+
+ // State variables
+ bool m_bHeld;
+ float m_magnitude;
+ unsigned int m_holdStartTimeMs;
+ unsigned int m_lastHoldTimeMs;
+ bool m_bActionSent;
+ unsigned int m_lastActionMs;
+ int m_activeWindowId = -1; // Window that activated the key
+ CAction m_lastAction;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/keymaps/KeymapHandler.cpp b/xbmc/input/joysticks/keymaps/KeymapHandler.cpp
new file mode 100644
index 0000000..24e2a0b
--- /dev/null
+++ b/xbmc/input/joysticks/keymaps/KeymapHandler.cpp
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2017-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 "KeymapHandler.h"
+
+#include "KeyHandler.h"
+#include "games/controllers/Controller.h"
+#include "input/IKeymap.h"
+#include "input/IKeymapEnvironment.h"
+#include "input/InputTranslator.h"
+#include "input/joysticks/JoystickEasterEgg.h"
+#include "input/joysticks/JoystickTranslator.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "input/joysticks/interfaces/IKeyHandler.h"
+
+#include <algorithm>
+#include <assert.h>
+#include <cmath>
+#include <utility>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+CKeymapHandler::CKeymapHandler(IActionListener* actionHandler, const IKeymap* keymap)
+ : m_actionHandler(actionHandler), m_keymap(keymap)
+{
+ assert(m_actionHandler != nullptr);
+ assert(m_keymap != nullptr);
+
+ if (m_keymap->Environment()->UseEasterEgg())
+ m_easterEgg.reset(new CJoystickEasterEgg(ControllerID()));
+}
+
+bool CKeymapHandler::HotkeysPressed(const std::set<std::string>& keyNames) const
+{
+ bool bHotkeysPressed = true;
+
+ for (const auto& hotkey : keyNames)
+ {
+ auto it = m_keyHandlers.find(hotkey);
+ if (it == m_keyHandlers.end() || !it->second->IsPressed())
+ {
+ bHotkeysPressed = false;
+ break;
+ }
+ }
+
+ return bHotkeysPressed;
+}
+
+std::string CKeymapHandler::ControllerID() const
+{
+ return m_keymap->ControllerID();
+}
+
+bool CKeymapHandler::AcceptsInput(const FeatureName& feature) const
+{
+ if (HasAction(CJoystickUtils::MakeKeyName(feature)))
+ return true;
+
+ for (auto dir : CJoystickUtils::GetAnalogStickDirections())
+ {
+ if (HasAction(CJoystickUtils::MakeKeyName(feature, dir)))
+ return true;
+ }
+
+ return false;
+}
+
+bool CKeymapHandler::OnButtonPress(const FeatureName& feature, bool bPressed)
+{
+ if (bPressed && m_easterEgg && m_easterEgg->OnButtonPress(feature))
+ return true;
+
+ const std::string keyName = CJoystickUtils::MakeKeyName(feature);
+
+ IKeyHandler* handler = GetKeyHandler(keyName);
+ return handler->OnDigitalMotion(bPressed, 0);
+}
+
+void CKeymapHandler::OnButtonHold(const FeatureName& feature, unsigned int holdTimeMs)
+{
+ if (m_easterEgg && m_easterEgg->IsCapturing())
+ return;
+
+ const std::string keyName = CJoystickUtils::MakeKeyName(feature);
+
+ IKeyHandler* handler = GetKeyHandler(keyName);
+ handler->OnDigitalMotion(true, holdTimeMs);
+}
+
+bool CKeymapHandler::OnButtonMotion(const FeatureName& feature,
+ float magnitude,
+ unsigned int motionTimeMs)
+{
+ const std::string keyName = CJoystickUtils::MakeKeyName(feature);
+
+ IKeyHandler* handler = GetKeyHandler(keyName);
+ return handler->OnAnalogMotion(magnitude, motionTimeMs);
+}
+
+bool CKeymapHandler::OnAnalogStickMotion(const FeatureName& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs)
+{
+ using namespace INPUT;
+
+ bool bHandled = false;
+
+ // Calculate the direction of the stick's position
+ const ANALOG_STICK_DIRECTION analogStickDir = CInputTranslator::VectorToCardinalDirection(x, y);
+
+ // Calculate the magnitude projected onto that direction
+ const float magnitude = std::max(std::fabs(x), std::fabs(y));
+
+ // Deactivate directions in which the stick is not pointing first
+ for (auto dir : CJoystickUtils::GetAnalogStickDirections())
+ {
+ if (dir != analogStickDir)
+ DeactivateDirection(feature, dir);
+ }
+
+ // Now activate direction the analog stick is pointing
+ if (analogStickDir != ANALOG_STICK_DIRECTION::NONE)
+ bHandled = ActivateDirection(feature, magnitude, analogStickDir, motionTimeMs);
+
+ return bHandled;
+}
+
+bool CKeymapHandler::OnWheelMotion(const FeatureName& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ bool bHandled = false;
+
+ // Calculate the direction of the wheel's position
+ const WHEEL_DIRECTION direction = CJoystickTranslator::PositionToWheelDirection(position);
+
+ // Calculate the magnitude projected onto that direction
+ const float magnitude = std::fabs(position);
+
+ // Deactivate directions in which the wheel is not pointing first
+ for (auto dir : CJoystickUtils::GetWheelDirections())
+ {
+ if (dir != direction)
+ DeactivateDirection(feature, dir);
+ }
+
+ // Now activate direction in which the wheel is positioned
+ if (direction != WHEEL_DIRECTION::NONE)
+ bHandled = ActivateDirection(feature, magnitude, direction, motionTimeMs);
+
+ return bHandled;
+}
+
+bool CKeymapHandler::OnThrottleMotion(const FeatureName& feature,
+ float position,
+ unsigned int motionTimeMs)
+{
+ bool bHandled = false;
+
+ // Calculate the direction of the throttle's position
+ const THROTTLE_DIRECTION direction = CJoystickTranslator::PositionToThrottleDirection(position);
+
+ // Calculate the magnitude projected onto that direction
+ const float magnitude = std::fabs(position);
+
+ // Deactivate directions in which the throttle is not pointing first
+ for (auto dir : CJoystickUtils::GetThrottleDirections())
+ {
+ if (dir != direction)
+ DeactivateDirection(feature, dir);
+ }
+
+ // Now activate direction in which the throttle is positioned
+ if (direction != THROTTLE_DIRECTION::NONE)
+ bHandled = ActivateDirection(feature, magnitude, direction, motionTimeMs);
+
+ return bHandled;
+}
+
+bool CKeymapHandler::OnAccelerometerMotion(const FeatureName& feature, float x, float y, float z)
+{
+ return false; //! @todo implement
+}
+
+bool CKeymapHandler::ActivateDirection(const FeatureName& feature,
+ float magnitude,
+ ANALOG_STICK_DIRECTION dir,
+ unsigned int motionTimeMs)
+{
+ const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir);
+
+ IKeyHandler* handler = GetKeyHandler(keyName);
+ return handler->OnAnalogMotion(magnitude, motionTimeMs);
+}
+
+void CKeymapHandler::DeactivateDirection(const FeatureName& feature, ANALOG_STICK_DIRECTION dir)
+{
+ const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir);
+
+ IKeyHandler* handler = GetKeyHandler(keyName);
+ handler->OnAnalogMotion(0.0f, 0);
+}
+
+bool CKeymapHandler::ActivateDirection(const FeatureName& feature,
+ float magnitude,
+ WHEEL_DIRECTION dir,
+ unsigned int motionTimeMs)
+{
+ const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir);
+
+ IKeyHandler* handler = GetKeyHandler(keyName);
+ return handler->OnAnalogMotion(magnitude, motionTimeMs);
+}
+
+void CKeymapHandler::DeactivateDirection(const FeatureName& feature, WHEEL_DIRECTION dir)
+{
+ const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir);
+
+ IKeyHandler* handler = GetKeyHandler(keyName);
+ handler->OnAnalogMotion(0.0f, 0);
+}
+
+bool CKeymapHandler::ActivateDirection(const FeatureName& feature,
+ float magnitude,
+ THROTTLE_DIRECTION dir,
+ unsigned int motionTimeMs)
+{
+ const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir);
+
+ IKeyHandler* handler = GetKeyHandler(keyName);
+ return handler->OnAnalogMotion(magnitude, motionTimeMs);
+}
+
+void CKeymapHandler::DeactivateDirection(const FeatureName& feature, THROTTLE_DIRECTION dir)
+{
+ const std::string keyName = CJoystickUtils::MakeKeyName(feature, dir);
+
+ IKeyHandler* handler = GetKeyHandler(keyName);
+ handler->OnAnalogMotion(0.0f, 0);
+}
+
+IKeyHandler* CKeymapHandler::GetKeyHandler(const std::string& keyName)
+{
+ auto it = m_keyHandlers.find(keyName);
+ if (it == m_keyHandlers.end())
+ {
+ std::unique_ptr<IKeyHandler> handler(new CKeyHandler(keyName, m_actionHandler, m_keymap, this));
+ m_keyHandlers.insert(std::make_pair(keyName, std::move(handler)));
+ it = m_keyHandlers.find(keyName);
+ }
+
+ return it->second.get();
+}
+
+bool CKeymapHandler::HasAction(const std::string& keyName) const
+{
+ bool bHasAction = false;
+
+ const auto& actions = m_keymap->GetActions(keyName).actions;
+ for (const auto& action : actions)
+ {
+ if (HotkeysPressed(action.hotkeys))
+ {
+ bHasAction = true;
+ break;
+ }
+ }
+
+ return bHasAction;
+}
diff --git a/xbmc/input/joysticks/keymaps/KeymapHandler.h b/xbmc/input/joysticks/keymaps/KeymapHandler.h
new file mode 100644
index 0000000..a2b1d4f
--- /dev/null
+++ b/xbmc/input/joysticks/keymaps/KeymapHandler.h
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017-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 "input/joysticks/JoystickTypes.h"
+#include "input/joysticks/interfaces/IButtonSequence.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+#include "input/joysticks/interfaces/IKeymapHandler.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+class IActionListener;
+class IKeymap;
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IKeyHandler;
+
+/*!
+ * \ingroup joystick
+ * \brief
+ */
+class CKeymapHandler : public IKeymapHandler, public IInputHandler
+{
+public:
+ CKeymapHandler(IActionListener* actionHandler, const IKeymap* keymap);
+
+ ~CKeymapHandler() override = default;
+
+ // implementation of IKeymapHandler
+ bool HotkeysPressed(const std::set<std::string>& keyNames) const override;
+ std::string GetLastPressed() const override { return m_lastPressed; }
+ void OnPress(const std::string& keyName) override { m_lastPressed = keyName; }
+
+ // implementation of IInputHandler
+ std::string ControllerID() const override;
+ bool HasFeature(const FeatureName& feature) const override { return true; }
+ bool AcceptsInput(const FeatureName& feature) const override;
+ bool OnButtonPress(const FeatureName& feature, bool bPressed) override;
+ void OnButtonHold(const FeatureName& feature, unsigned int holdTimeMs) override;
+ bool OnButtonMotion(const FeatureName& feature,
+ float magnitude,
+ unsigned int motionTimeMs) override;
+ bool OnAnalogStickMotion(const FeatureName& feature,
+ float x,
+ float y,
+ unsigned int motionTimeMs) override;
+ bool OnAccelerometerMotion(const FeatureName& feature, float x, float y, float z) override;
+ bool OnWheelMotion(const FeatureName& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ bool OnThrottleMotion(const FeatureName& feature,
+ float position,
+ unsigned int motionTimeMs) override;
+ void OnInputFrame() override {}
+
+protected:
+ // Keep track of cheat code presses
+ std::unique_ptr<IButtonSequence> m_easterEgg;
+
+private:
+ // Analog stick helper functions
+ bool ActivateDirection(const FeatureName& feature,
+ float magnitude,
+ ANALOG_STICK_DIRECTION dir,
+ unsigned int motionTimeMs);
+ void DeactivateDirection(const FeatureName& feature, ANALOG_STICK_DIRECTION dir);
+
+ // Wheel helper functions
+ bool ActivateDirection(const FeatureName& feature,
+ float magnitude,
+ WHEEL_DIRECTION dir,
+ unsigned int motionTimeMs);
+ void DeactivateDirection(const FeatureName& feature, WHEEL_DIRECTION dir);
+
+ // Throttle helper functions
+ bool ActivateDirection(const FeatureName& feature,
+ float magnitude,
+ THROTTLE_DIRECTION dir,
+ unsigned int motionTimeMs);
+ void DeactivateDirection(const FeatureName& feature, THROTTLE_DIRECTION dir);
+
+ // Helper functions
+ IKeyHandler* GetKeyHandler(const std::string& keyName);
+ bool HasAction(const std::string& keyName) const;
+
+ // Construction parameters
+ IActionListener* const m_actionHandler;
+ const IKeymap* const m_keymap;
+
+ // Handlers for individual keys
+ std::map<std::string, std::unique_ptr<IKeyHandler>> m_keyHandlers; // Key name -> handler
+
+ // Last pressed key
+ std::string m_lastPressed;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/joysticks/keymaps/KeymapHandling.cpp b/xbmc/input/joysticks/keymaps/KeymapHandling.cpp
new file mode 100644
index 0000000..b87ea68
--- /dev/null
+++ b/xbmc/input/joysticks/keymaps/KeymapHandling.cpp
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017-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 "KeymapHandling.h"
+
+#include "KeymapHandler.h"
+#include "ServiceBroker.h"
+#include "input/ButtonTranslator.h"
+#include "input/InputManager.h"
+#include "input/Keymap.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+#include "input/joysticks/interfaces/IInputProvider.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace KODI;
+using namespace JOYSTICK;
+
+CKeymapHandling::CKeymapHandling(IInputProvider* inputProvider,
+ bool pPromiscuous,
+ const IKeymapEnvironment* environment)
+ : m_inputProvider(inputProvider), m_pPromiscuous(pPromiscuous), m_environment(environment)
+{
+ LoadKeymaps();
+ CServiceBroker::GetInputManager().RegisterObserver(this);
+}
+
+CKeymapHandling::~CKeymapHandling()
+{
+ CServiceBroker::GetInputManager().UnregisterObserver(this);
+ UnloadKeymaps();
+}
+
+IInputReceiver* CKeymapHandling::GetInputReceiver(const std::string& controllerId) const
+{
+ auto it = std::find_if(m_inputHandlers.begin(), m_inputHandlers.end(),
+ [&controllerId](const std::unique_ptr<IInputHandler>& inputHandler) {
+ return inputHandler->ControllerID() == controllerId;
+ });
+
+ if (it != m_inputHandlers.end())
+ return (*it)->InputReceiver();
+
+ return nullptr;
+}
+
+IKeymap* CKeymapHandling::GetKeymap(const std::string& controllerId) const
+{
+ auto it = std::find_if(m_keymaps.begin(), m_keymaps.end(),
+ [&controllerId](const std::unique_ptr<IKeymap>& keymap) {
+ return keymap->ControllerID() == controllerId;
+ });
+
+ if (it != m_keymaps.end())
+ return it->get();
+
+ return nullptr;
+}
+
+void CKeymapHandling::Notify(const Observable& obs, const ObservableMessage msg)
+{
+ if (msg == ObservableMessageButtonMapsChanged)
+ LoadKeymaps();
+}
+
+void CKeymapHandling::LoadKeymaps()
+{
+ UnloadKeymaps();
+
+ auto& inputManager = CServiceBroker::GetInputManager();
+
+ for (auto& windowKeymap : inputManager.GetJoystickKeymaps())
+ {
+ // Create keymap
+ std::unique_ptr<IKeymap> keymap(new CKeymap(std::move(windowKeymap), m_environment));
+
+ // Create keymap handler
+ std::unique_ptr<IInputHandler> inputHandler(new CKeymapHandler(&inputManager, keymap.get()));
+
+ // Register the handler with the input provider
+ m_inputProvider->RegisterInputHandler(inputHandler.get(), m_pPromiscuous);
+
+ // Save the keymap and handler
+ m_keymaps.emplace_back(std::move(keymap));
+ m_inputHandlers.emplace_back(std::move(inputHandler));
+ }
+}
+
+void CKeymapHandling::UnloadKeymaps()
+{
+ if (m_inputProvider != nullptr)
+ {
+ for (auto it = m_inputHandlers.rbegin(); it != m_inputHandlers.rend(); ++it)
+ m_inputProvider->UnregisterInputHandler(it->get());
+ }
+ m_inputHandlers.clear();
+ m_keymaps.clear();
+}
diff --git a/xbmc/input/joysticks/keymaps/KeymapHandling.h b/xbmc/input/joysticks/keymaps/KeymapHandling.h
new file mode 100644
index 0000000..fdc64b7
--- /dev/null
+++ b/xbmc/input/joysticks/keymaps/KeymapHandling.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017-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 "utils/Observer.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class IKeymap;
+class IKeymapEnvironment;
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IInputHandler;
+class IInputProvider;
+class IInputReceiver;
+
+/*!
+ * \ingroup joystick
+ * \brief
+ */
+class CKeymapHandling : public Observer
+{
+public:
+ CKeymapHandling(IInputProvider* inputProvider,
+ bool pPromiscuous,
+ const IKeymapEnvironment* environment);
+
+ ~CKeymapHandling() override;
+
+ /*!
+ * \brief Unregister the input provider
+ *
+ * Call this if the input provider is invalidated, such as if a user
+ * disconnects a controller. This prevents accessing the invalidated
+ * input provider when keymaps are unloaded upon destruction.
+ */
+ void UnregisterInputProvider() { m_inputProvider = nullptr; }
+
+ /*!
+ * \brief
+ */
+ IInputReceiver* GetInputReceiver(const std::string& controllerId) const;
+
+ /*!
+ * \brief
+ */
+ IKeymap* GetKeymap(const std::string& controllerId) const;
+
+ // implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override;
+
+private:
+ void LoadKeymaps();
+ void UnloadKeymaps();
+
+ // Construction parameter
+ IInputProvider* m_inputProvider;
+ const bool m_pPromiscuous;
+ const IKeymapEnvironment* const m_environment;
+
+ std::vector<std::unique_ptr<IKeymap>> m_keymaps;
+ std::vector<std::unique_ptr<IInputHandler>> m_inputHandlers;
+};
+} // namespace JOYSTICK
+} // namespace KODI
diff --git a/xbmc/input/keyboard/CMakeLists.txt b/xbmc/input/keyboard/CMakeLists.txt
new file mode 100644
index 0000000..14d48a8
--- /dev/null
+++ b/xbmc/input/keyboard/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES KeyboardEasterEgg.cpp
+ KeymapActionMap.cpp
+)
+
+set(HEADERS interfaces/IActionMap.h
+ interfaces/IKeyboardDriverHandler.h
+ interfaces/IKeyboardInputHandler.h
+ interfaces/IKeyboardInputProvider.h
+ KeyboardEasterEgg.h
+ KeyboardTypes.h
+ KeymapActionMap.h
+)
+
+core_add_library(input_keyboard)
diff --git a/xbmc/input/keyboard/KeyboardEasterEgg.cpp b/xbmc/input/keyboard/KeyboardEasterEgg.cpp
new file mode 100644
index 0000000..8b7049d
--- /dev/null
+++ b/xbmc/input/keyboard/KeyboardEasterEgg.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017-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 "input/keyboard/KeyboardEasterEgg.h"
+
+#include "input/Key.h"
+#include "input/joysticks/JoystickEasterEgg.h"
+
+using namespace KODI;
+using namespace KEYBOARD;
+
+std::vector<XBMCVKey> CKeyboardEasterEgg::m_sequence = {
+ XBMCVK_UP, XBMCVK_UP, XBMCVK_DOWN, XBMCVK_DOWN, XBMCVK_LEFT,
+ XBMCVK_RIGHT, XBMCVK_LEFT, XBMCVK_RIGHT, XBMCVK_B, XBMCVK_A,
+};
+
+bool CKeyboardEasterEgg::OnKeyPress(const CKey& key)
+{
+ bool bHandled = false;
+
+ // Update state
+ if (key.GetVKey() == m_sequence[m_state])
+ m_state++;
+ else
+ m_state = 0;
+
+ // Capture input when finished with arrows (2 x up/down/left/right)
+ if (m_state > 8)
+ {
+ bHandled = true;
+
+ if (m_state >= m_sequence.size())
+ {
+ JOYSTICK::CJoystickEasterEgg::OnFinish();
+ m_state = 0;
+ }
+ }
+
+ return bHandled;
+}
diff --git a/xbmc/input/keyboard/KeyboardEasterEgg.h b/xbmc/input/keyboard/KeyboardEasterEgg.h
new file mode 100644
index 0000000..00c1791
--- /dev/null
+++ b/xbmc/input/keyboard/KeyboardEasterEgg.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016-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 "input/XBMC_vkeys.h"
+#include "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+
+#include <vector>
+
+namespace KODI
+{
+namespace KEYBOARD
+{
+/*!
+ * \brief Hush!!!
+ */
+class CKeyboardEasterEgg : public IKeyboardDriverHandler
+{
+public:
+ ~CKeyboardEasterEgg() override = default;
+
+ // implementation of IKeyboardDriverHandler
+ bool OnKeyPress(const CKey& key) override;
+ void OnKeyRelease(const CKey& key) override {}
+
+private:
+ static std::vector<XBMCVKey> m_sequence;
+
+ unsigned int m_state = 0;
+};
+} // namespace KEYBOARD
+} // namespace KODI
diff --git a/xbmc/input/keyboard/KeyboardTypes.h b/xbmc/input/keyboard/KeyboardTypes.h
new file mode 100644
index 0000000..5a5d39b
--- /dev/null
+++ b/xbmc/input/keyboard/KeyboardTypes.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017-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 "input/Key.h"
+#include "input/XBMC_keysym.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace KEYBOARD
+{
+/*!
+ * \brief Symbol of a hardware-independent key
+ */
+using KeySymbol = XBMCKey;
+
+/*!
+ * \brief Name of a hardware-indendent symbol representing a key
+ *
+ * Names are defined in the keyboard's controller profile.
+ */
+using KeyName = std::string;
+
+/*!
+ * \brief Modifier keys on a keyboard that can be held when
+ * sending a key press
+ *
+ * \todo Move CKey enum to this file
+ */
+using Modifier = CKey::Modifier;
+} // namespace KEYBOARD
+} // namespace KODI
diff --git a/xbmc/input/keyboard/KeymapActionMap.cpp b/xbmc/input/keyboard/KeymapActionMap.cpp
new file mode 100644
index 0000000..f6e6919
--- /dev/null
+++ b/xbmc/input/keyboard/KeymapActionMap.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017-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 "KeymapActionMap.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/InputManager.h"
+#include "input/Key.h"
+#include "input/actions/Action.h"
+
+using namespace KODI;
+using namespace KEYBOARD;
+
+unsigned int CKeymapActionMap::GetActionID(const CKey& key)
+{
+ CAction action = CServiceBroker::GetInputManager().GetAction(
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog(), key);
+ return action.GetID();
+}
diff --git a/xbmc/input/keyboard/KeymapActionMap.h b/xbmc/input/keyboard/KeymapActionMap.h
new file mode 100644
index 0000000..6ee9344
--- /dev/null
+++ b/xbmc/input/keyboard/KeymapActionMap.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017-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 "input/keyboard/interfaces/IActionMap.h"
+
+namespace KODI
+{
+namespace KEYBOARD
+{
+class CKeymapActionMap : public IActionMap
+{
+public:
+ CKeymapActionMap(void) = default;
+
+ ~CKeymapActionMap(void) override = default;
+
+ // implementation of IActionMap
+ unsigned int GetActionID(const CKey& key) override;
+};
+} // namespace KEYBOARD
+} // namespace KODI
diff --git a/xbmc/input/keyboard/generic/CMakeLists.txt b/xbmc/input/keyboard/generic/CMakeLists.txt
new file mode 100644
index 0000000..993a1f4
--- /dev/null
+++ b/xbmc/input/keyboard/generic/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES KeyboardInputHandling.cpp)
+
+set(HEADERS KeyboardInputHandling.h)
+
+core_add_library(input_keyboard_generic)
diff --git a/xbmc/input/keyboard/generic/KeyboardInputHandling.cpp b/xbmc/input/keyboard/generic/KeyboardInputHandling.cpp
new file mode 100644
index 0000000..5e84081
--- /dev/null
+++ b/xbmc/input/keyboard/generic/KeyboardInputHandling.cpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017-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 "KeyboardInputHandling.h"
+
+#include "input/XBMC_keysym.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/keyboard/interfaces/IKeyboardInputHandler.h"
+
+using namespace KODI;
+using namespace KEYBOARD;
+
+CKeyboardInputHandling::CKeyboardInputHandling(IKeyboardInputHandler* handler,
+ JOYSTICK::IButtonMap* buttonMap)
+ : m_handler(handler), m_buttonMap(buttonMap)
+{
+}
+
+bool CKeyboardInputHandling::OnKeyPress(const CKey& key)
+{
+ bool bHandled = false;
+
+ JOYSTICK::CDriverPrimitive source(static_cast<XBMCKey>(key.GetKeycode()));
+
+ KeyName keyName;
+ if (m_buttonMap->GetFeature(source, keyName))
+ {
+ const Modifier mod = static_cast<Modifier>(key.GetModifiers() | key.GetLockingModifiers());
+ bHandled = m_handler->OnKeyPress(keyName, mod, key.GetUnicode());
+ }
+
+ return bHandled;
+}
+
+void CKeyboardInputHandling::OnKeyRelease(const CKey& key)
+{
+ JOYSTICK::CDriverPrimitive source(static_cast<XBMCKey>(key.GetKeycode()));
+
+ KeyName keyName;
+ if (m_buttonMap->GetFeature(source, keyName))
+ {
+ const Modifier mod = static_cast<Modifier>(key.GetModifiers() | key.GetLockingModifiers());
+ m_handler->OnKeyRelease(keyName, mod, key.GetUnicode());
+ }
+}
diff --git a/xbmc/input/keyboard/generic/KeyboardInputHandling.h b/xbmc/input/keyboard/generic/KeyboardInputHandling.h
new file mode 100644
index 0000000..4cd2d26
--- /dev/null
+++ b/xbmc/input/keyboard/generic/KeyboardInputHandling.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017-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 "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IButtonMap;
+}
+
+namespace KEYBOARD
+{
+class IKeyboardInputHandler;
+
+/*!
+ * \ingroup keyboard
+ * \brief Class to translate input from Kodi keycodes to key names defined
+ * by the keyboard's controller profile
+ */
+class CKeyboardInputHandling : public IKeyboardDriverHandler
+{
+public:
+ CKeyboardInputHandling(IKeyboardInputHandler* handler, JOYSTICK::IButtonMap* buttonMap);
+
+ ~CKeyboardInputHandling(void) override = default;
+
+ // implementation of IKeyboardDriverHandler
+ bool OnKeyPress(const CKey& key) override;
+ void OnKeyRelease(const CKey& key) override;
+
+private:
+ // Construction parameters
+ IKeyboardInputHandler* const m_handler;
+ JOYSTICK::IButtonMap* const m_buttonMap;
+};
+} // namespace KEYBOARD
+} // namespace KODI
diff --git a/xbmc/input/keyboard/interfaces/IActionMap.h b/xbmc/input/keyboard/interfaces/IActionMap.h
new file mode 100644
index 0000000..e77643e
--- /dev/null
+++ b/xbmc/input/keyboard/interfaces/IActionMap.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017-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 CKey;
+
+namespace KODI
+{
+namespace KEYBOARD
+{
+/*!
+ * \brief Interface for translating keys to action IDs
+ */
+class IActionMap
+{
+public:
+ virtual ~IActionMap() = default;
+
+ /*!
+ * \brief Get the action ID mapped to the specified key
+ *
+ * \param key The key to look up
+ *
+ * \return The action ID from ActionIDs.h, or ACTION_NONE if no action is
+ * mapped to the specified key
+ */
+ virtual unsigned int GetActionID(const CKey& key) = 0;
+};
+} // namespace KEYBOARD
+} // namespace KODI
diff --git a/xbmc/input/keyboard/interfaces/IKeyboardDriverHandler.h b/xbmc/input/keyboard/interfaces/IKeyboardDriverHandler.h
new file mode 100644
index 0000000..828f0a9
--- /dev/null
+++ b/xbmc/input/keyboard/interfaces/IKeyboardDriverHandler.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
+
+class CKey;
+
+namespace KODI
+{
+namespace KEYBOARD
+{
+/*!
+ * \ingroup keyboard
+ * \brief Interface for handling keyboard events
+ */
+class IKeyboardDriverHandler
+{
+public:
+ virtual ~IKeyboardDriverHandler() = default;
+
+ /*!
+ * \brief A key has been pressed
+ *
+ * \param key The pressed key
+ *
+ * \return True if the event was handled, false otherwise
+ */
+ virtual bool OnKeyPress(const CKey& key) = 0;
+
+ /*!
+ * \brief A key has been released
+ *
+ * \param key The released key
+ */
+ virtual void OnKeyRelease(const CKey& key) = 0;
+};
+} // namespace KEYBOARD
+} // namespace KODI
diff --git a/xbmc/input/keyboard/interfaces/IKeyboardInputHandler.h b/xbmc/input/keyboard/interfaces/IKeyboardInputHandler.h
new file mode 100644
index 0000000..f0df6a3
--- /dev/null
+++ b/xbmc/input/keyboard/interfaces/IKeyboardInputHandler.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2017-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 "input/keyboard/KeyboardTypes.h"
+
+#include <stdint.h>
+#include <string>
+
+namespace KODI
+{
+namespace KEYBOARD
+{
+/*!
+ * \ingroup keyboard
+ * \brief Interface for handling input events for keyboards
+ *
+ * Input events are an abstraction over driver events. Keys are identified by
+ * the name defined in the keyboard's controller profile.
+ */
+class IKeyboardInputHandler
+{
+public:
+ virtual ~IKeyboardInputHandler() = default;
+
+ /*!
+ * \brief The add-on ID of the keyboard's controller profile
+ *
+ * \return The ID of the controller profile add-on
+ */
+ virtual std::string ControllerID() const = 0;
+
+ /*!
+ * \brief Return true if the input handler accepts the given key
+ *
+ * \param key A key belonging to the controller specified by ControllerID()
+ *
+ * \return True if the key is used for input, false otherwise
+ */
+ virtual bool HasKey(const KeyName& key) const = 0;
+
+ /*!
+ * \brief A key has been pressed
+ *
+ * \param key A key belonging to the controller specified by ControllerID()
+ * \param mod A combination of modifiers
+ * \param unicode The unicode value associated with the key, or 0 if unknown
+ *
+ * \return True if the event was handled, false otherwise
+ */
+ virtual bool OnKeyPress(const KeyName& key, Modifier mod, uint32_t unicode) = 0;
+
+ /*!
+ * \brief A key has been released
+ *
+ * \param key A key belonging to the controller specified by ControllerID()
+ * \param mod A combination of modifiers
+ * \param unicode The unicode value associated with the key, or 0 if unknown
+ */
+ virtual void OnKeyRelease(const KeyName& key, Modifier mod, uint32_t unicode) = 0;
+};
+} // namespace KEYBOARD
+} // namespace KODI
diff --git a/xbmc/input/keyboard/interfaces/IKeyboardInputProvider.h b/xbmc/input/keyboard/interfaces/IKeyboardInputProvider.h
new file mode 100644
index 0000000..5d964d5
--- /dev/null
+++ b/xbmc/input/keyboard/interfaces/IKeyboardInputProvider.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017-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
+
+namespace KODI
+{
+namespace KEYBOARD
+{
+class IKeyboardInputHandler;
+
+/*!
+ * \ingroup mouse
+ * \brief Interface for classes that can provide keyboard input
+ */
+class IKeyboardInputProvider
+{
+public:
+ virtual ~IKeyboardInputProvider() = default;
+
+ /*!
+ * \brief Registers a handler to be called on keyboard input
+ *
+ * \param handler The handler to receive keyboard input provided by this class
+ * \param bPromiscuous True to observe all events without affecting the
+ * input's destination
+ */
+ virtual void RegisterKeyboardHandler(IKeyboardInputHandler* handler, bool bPromiscuous) = 0;
+
+ /*!
+ * \brief Unregisters handler from keyboard input
+ *
+ * \param handler The handler that was receiving keyboard input
+ */
+ virtual void UnregisterKeyboardHandler(IKeyboardInputHandler* handler) = 0;
+};
+} // namespace KEYBOARD
+} // namespace KODI
diff --git a/xbmc/input/mouse/CMakeLists.txt b/xbmc/input/mouse/CMakeLists.txt
new file mode 100644
index 0000000..214adf0
--- /dev/null
+++ b/xbmc/input/mouse/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES MouseStat.cpp
+ MouseTranslator.cpp
+)
+
+set(HEADERS interfaces/IMouseDriverHandler.h
+ interfaces/IMouseInputHandler.h
+ interfaces/IMouseInputProvider.h
+ MouseStat.h
+ MouseTranslator.h
+ MouseTypes.h
+)
+
+core_add_library(input_mouse)
diff --git a/xbmc/input/mouse/MouseStat.cpp b/xbmc/input/mouse/MouseStat.cpp
new file mode 100644
index 0000000..8b6ce3b
--- /dev/null
+++ b/xbmc/input/mouse/MouseStat.cpp
@@ -0,0 +1,378 @@
+/*
+ * 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 "MouseStat.h"
+
+#include "ServiceBroker.h"
+#include "input/Key.h"
+#include "utils/TimeUtils.h"
+#include "windowing/WinSystem.h"
+
+#include <algorithm>
+#include <cstring>
+
+CMouseStat::CMouseStat()
+{
+ SetEnabled();
+ m_Key = KEY_MOUSE_NOOP;
+}
+
+CMouseStat::~CMouseStat() = default;
+
+void CMouseStat::Initialize()
+{
+ // Set the default resolution (PAL)
+ SetResolution(720, 576, 1, 1);
+}
+
+void CMouseStat::HandleEvent(const XBMC_Event& newEvent)
+{
+ // Save the mouse position and the size of the last move
+ int dx, dy;
+ if (newEvent.type == XBMC_MOUSEMOTION)
+ {
+ dx = newEvent.motion.x - m_mouseState.x;
+ dy = newEvent.motion.y - m_mouseState.y;
+ }
+ else if (newEvent.type == XBMC_MOUSEBUTTONDOWN || newEvent.type == XBMC_MOUSEBUTTONUP)
+ {
+ dx = newEvent.button.x - m_mouseState.x;
+ dy = newEvent.button.y - m_mouseState.y;
+ }
+ else
+ {
+ return;
+ }
+ m_mouseState.dx = dx;
+ m_mouseState.dy = dy;
+ m_mouseState.x = std::max(0, std::min(m_maxX, m_mouseState.x + dx));
+ m_mouseState.y = std::max(0, std::min(m_maxY, m_mouseState.y + dy));
+
+ // Fill in the public members
+ if (newEvent.type == XBMC_MOUSEBUTTONDOWN)
+ {
+ if (newEvent.button.button == XBMC_BUTTON_LEFT)
+ m_mouseState.button[MOUSE_LEFT_BUTTON] = true;
+ if (newEvent.button.button == XBMC_BUTTON_RIGHT)
+ m_mouseState.button[MOUSE_RIGHT_BUTTON] = true;
+ if (newEvent.button.button == XBMC_BUTTON_MIDDLE)
+ m_mouseState.button[MOUSE_MIDDLE_BUTTON] = true;
+ if (newEvent.button.button == XBMC_BUTTON_X1)
+ m_mouseState.button[MOUSE_EXTRA_BUTTON1] = true;
+ if (newEvent.button.button == XBMC_BUTTON_X2)
+ m_mouseState.button[MOUSE_EXTRA_BUTTON2] = true;
+ if (newEvent.button.button == XBMC_BUTTON_X3)
+ m_mouseState.button[MOUSE_EXTRA_BUTTON3] = true;
+ if (newEvent.button.button == XBMC_BUTTON_X4)
+ m_mouseState.button[MOUSE_EXTRA_BUTTON4] = true;
+ if (newEvent.button.button == XBMC_BUTTON_WHEELUP)
+ m_mouseState.dz = 1;
+ if (newEvent.button.button == XBMC_BUTTON_WHEELDOWN)
+ m_mouseState.dz = -1;
+ }
+ else if (newEvent.type == XBMC_MOUSEBUTTONUP)
+ {
+ if (newEvent.button.button == XBMC_BUTTON_LEFT)
+ m_mouseState.button[MOUSE_LEFT_BUTTON] = false;
+ if (newEvent.button.button == XBMC_BUTTON_RIGHT)
+ m_mouseState.button[MOUSE_RIGHT_BUTTON] = false;
+ if (newEvent.button.button == XBMC_BUTTON_MIDDLE)
+ m_mouseState.button[MOUSE_MIDDLE_BUTTON] = false;
+ if (newEvent.button.button == XBMC_BUTTON_X1)
+ m_mouseState.button[MOUSE_EXTRA_BUTTON1] = false;
+ if (newEvent.button.button == XBMC_BUTTON_X2)
+ m_mouseState.button[MOUSE_EXTRA_BUTTON2] = false;
+ if (newEvent.button.button == XBMC_BUTTON_X3)
+ m_mouseState.button[MOUSE_EXTRA_BUTTON3] = false;
+ if (newEvent.button.button == XBMC_BUTTON_X4)
+ m_mouseState.button[MOUSE_EXTRA_BUTTON4] = false;
+ if (newEvent.button.button == XBMC_BUTTON_WHEELUP)
+ m_mouseState.dz = 0;
+ if (newEvent.button.button == XBMC_BUTTON_WHEELDOWN)
+ m_mouseState.dz = 0;
+ }
+
+ // Now check the current message and the previous state to find out if
+ // this is a click, doubleclick, drag etc
+ uint32_t now = CTimeUtils::GetFrameTime();
+ bool bNothingDown = true;
+
+ for (int i = 0; i < MOUSE_MAX_BUTTON; i++)
+ {
+ bClick[i] = false;
+ bLongClick[i] = false;
+ bDoubleClick[i] = false;
+ m_hold[i] = HoldAction::NONE;
+
+ // CButtonState::Update does the hard work of checking the button state
+ // and spotting drags, doubleclicks etc
+ CButtonState::BUTTON_ACTION action =
+ m_buttonState[i].Update(now, m_mouseState.x, m_mouseState.y, m_mouseState.button[i]);
+ switch (action)
+ {
+ case CButtonState::MB_SHORT_CLICK:
+ bClick[i] = true;
+ bNothingDown = false;
+ break;
+ case CButtonState::MB_LONG_CLICK:
+ bLongClick[i] = true;
+ bNothingDown = false;
+ break;
+ case CButtonState::MB_DOUBLE_CLICK:
+ bDoubleClick[i] = true;
+ bNothingDown = false;
+ break;
+ case CButtonState::MB_DRAG_START:
+ m_hold[i] = HoldAction::DRAG_START;
+ bNothingDown = false;
+ break;
+ case CButtonState::MB_DRAG:
+ m_hold[i] = HoldAction::DRAG;
+ bNothingDown = false;
+ break;
+ case CButtonState::MB_DRAG_END:
+ m_hold[i] = HoldAction::DRAG_END;
+ bNothingDown = false;
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Now work out what action ID to send to XBMC.
+
+ // ignore any mouse messages by default
+ m_Key = KEY_MOUSE_NOOP;
+
+ for (int button = 0; button < MOUSE_MAX_BUTTON; ++button)
+ {
+ // The bClick array is set true if CButtonState::Update spots a click
+ // i.e. a button down followed by a button up.
+ if (bClick[button])
+ m_Key = KEY_MOUSE_CLICK + button;
+ // The bDoubleClick array is set true if CButtonState::Update spots a
+ // button down within double_click_time (500ms) of the last click
+ else if (bDoubleClick[button])
+ m_Key = KEY_MOUSE_DOUBLE_CLICK + button;
+ else if (bLongClick[button])
+ m_Key = KEY_MOUSE_LONG_CLICK + button;
+
+ if (m_Key != KEY_MOUSE_NOOP)
+ break;
+ }
+
+ if (m_Key == KEY_MOUSE_NOOP)
+ {
+ // The m_hold array is set to the drag action
+ if (m_hold[MOUSE_LEFT_BUTTON] != HoldAction::NONE)
+ {
+ switch (m_hold[MOUSE_LEFT_BUTTON])
+ {
+ case HoldAction::DRAG:
+ m_Key = KEY_MOUSE_DRAG;
+ break;
+ case HoldAction::DRAG_START:
+ m_Key = KEY_MOUSE_DRAG_START;
+ break;
+ case HoldAction::DRAG_END:
+ m_Key = KEY_MOUSE_DRAG_END;
+ break;
+ default:
+ break;
+ }
+ }
+ else if (m_hold[MOUSE_RIGHT_BUTTON] != HoldAction::NONE)
+ {
+ switch (m_hold[MOUSE_RIGHT_BUTTON])
+ {
+ case HoldAction::DRAG:
+ m_Key = KEY_MOUSE_RDRAG;
+ break;
+ case HoldAction::DRAG_START:
+ m_Key = KEY_MOUSE_RDRAG_START;
+ break;
+ case HoldAction::DRAG_END:
+ m_Key = KEY_MOUSE_RDRAG_END;
+ break;
+ default:
+ break;
+ }
+ }
+
+ // dz is +1 on wheel up and -1 on wheel down
+ else if (m_mouseState.dz > 0)
+ m_Key = KEY_MOUSE_WHEEL_UP;
+ else if (m_mouseState.dz < 0)
+ m_Key = KEY_MOUSE_WHEEL_DOWN;
+
+ // Check for a mouse move that isn't a drag, ignoring messages with no movement at all
+ else if (newEvent.type == XBMC_MOUSEMOTION && (m_mouseState.dx || m_mouseState.dy))
+ m_Key = KEY_MOUSE_MOVE;
+ }
+
+ // activate the mouse pointer if we have an action or the mouse has moved far enough
+ if ((MovedPastThreshold() && m_Key == KEY_MOUSE_MOVE) ||
+ (m_Key != KEY_MOUSE_NOOP && m_Key != KEY_MOUSE_MOVE))
+ SetActive();
+
+ // reset the mouse state if nothing is held down
+ if (bNothingDown)
+ SetState(MOUSE_STATE_NORMAL);
+}
+
+void CMouseStat::SetResolution(int maxX, int maxY, float speedX, float speedY)
+{
+ m_maxX = maxX;
+ m_maxY = maxY;
+
+ // speed is currently unused
+ m_speedX = speedX;
+ m_speedY = speedY;
+}
+
+void CMouseStat::SetActive(bool active /*=true*/)
+{
+ m_lastActiveTime = CTimeUtils::GetFrameTime();
+ m_mouseState.active = active;
+ // we show the OS mouse if:
+ // 1. The mouse is active (it has been moved) AND
+ // 2. The XBMC mouse is disabled in settings AND
+ // 3. XBMC is not in fullscreen.
+ CWinSystemBase* winSystem = CServiceBroker::GetWinSystem();
+ if (winSystem)
+ winSystem->ShowOSMouse(m_mouseState.active && !IsEnabled() &&
+ !CServiceBroker::GetWinSystem()->IsFullScreen());
+}
+
+// IsActive - returns true if we have been active in the last MOUSE_ACTIVE_LENGTH period
+bool CMouseStat::IsActive()
+{
+ if (m_mouseState.active && (CTimeUtils::GetFrameTime() - m_lastActiveTime > MOUSE_ACTIVE_LENGTH))
+ SetActive(false);
+ return (m_mouseState.active && IsEnabled());
+}
+
+void CMouseStat::SetEnabled(bool enabled)
+{
+ m_mouseEnabled = enabled;
+ SetActive(enabled);
+}
+
+// IsEnabled - returns true if mouse is enabled
+bool CMouseStat::IsEnabled() const
+{
+ return m_mouseEnabled;
+}
+
+bool CMouseStat::MovedPastThreshold() const
+{
+ return (m_mouseState.dx * m_mouseState.dx + m_mouseState.dy * m_mouseState.dy >=
+ MOUSE_MINIMUM_MOVEMENT * MOUSE_MINIMUM_MOVEMENT);
+}
+
+uint32_t CMouseStat::GetKey() const
+{
+ return m_Key;
+}
+
+HoldAction CMouseStat::GetHold(int ButtonID) const
+{
+ switch (ButtonID)
+ {
+ case MOUSE_LEFT_BUTTON:
+ return m_hold[MOUSE_LEFT_BUTTON];
+ }
+ return HoldAction::NONE;
+}
+
+CMouseStat::CButtonState::CButtonState()
+{
+ m_state = STATE_RELEASED;
+ m_time = 0;
+ m_x = 0;
+ m_y = 0;
+}
+
+bool CMouseStat::CButtonState::InClickRange(int x, int y) const
+{
+ int dx = x - m_x;
+ int dy = y - m_y;
+ return (unsigned int)(dx * dx + dy * dy) <= click_confines * click_confines;
+}
+
+CMouseStat::CButtonState::BUTTON_ACTION CMouseStat::CButtonState::Update(unsigned int time,
+ int x,
+ int y,
+ bool down)
+{
+ if (m_state == STATE_IN_DRAG)
+ {
+ if (down)
+ return MB_DRAG;
+ m_state = STATE_RELEASED;
+ return MB_DRAG_END;
+ }
+ else if (m_state == STATE_RELEASED)
+ {
+ if (down)
+ {
+ m_state = STATE_IN_CLICK;
+ m_time = time;
+ m_x = x;
+ m_y = y;
+ }
+ }
+ else if (m_state == STATE_IN_CLICK)
+ {
+ if (down)
+ {
+ if (!InClickRange(x, y))
+ { // beginning a drag
+ m_state = STATE_IN_DRAG;
+ return MB_DRAG_START;
+ }
+ }
+ else
+ { // button up
+ if (time - m_time < short_click_time)
+ { // single click
+ m_state = STATE_IN_DOUBLE_CLICK;
+ m_time = time; // double click time and positioning is measured from the
+ m_x = x; // end of a single click
+ m_y = y;
+ return MB_SHORT_CLICK;
+ }
+ else
+ { // long click
+ m_state = STATE_RELEASED;
+ return MB_LONG_CLICK;
+ }
+ }
+ }
+ else if (m_state == STATE_IN_DOUBLE_CLICK)
+ {
+ if (time - m_time > double_click_time || !InClickRange(x, y))
+ { // too long, or moved to much - reset to released state and re-update, as we may be starting a
+ // new click
+ m_state = STATE_RELEASED;
+ return Update(time, x, y, down);
+ }
+ if (down)
+ {
+ m_state = STATE_IN_DOUBLE_IGNORE;
+ return MB_DOUBLE_CLICK;
+ }
+ }
+ else if (m_state == STATE_IN_DOUBLE_IGNORE)
+ {
+ if (!down)
+ m_state = STATE_RELEASED;
+ }
+
+ return MB_NONE;
+}
diff --git a/xbmc/input/mouse/MouseStat.h b/xbmc/input/mouse/MouseStat.h
new file mode 100644
index 0000000..354d1c3
--- /dev/null
+++ b/xbmc/input/mouse/MouseStat.h
@@ -0,0 +1,227 @@
+/*
+ * 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 "windowing/XBMC_events.h"
+
+#define XBMC_BUTTON(X) (1 << ((X)-1))
+#define XBMC_BUTTON_LEFT 1
+#define XBMC_BUTTON_MIDDLE 2
+#define XBMC_BUTTON_RIGHT 3
+#define XBMC_BUTTON_WHEELUP 4
+#define XBMC_BUTTON_WHEELDOWN 5
+#define XBMC_BUTTON_X1 6
+#define XBMC_BUTTON_X2 7
+#define XBMC_BUTTON_X3 8
+#define XBMC_BUTTON_X4 9
+#define XBMC_BUTTON_LMASK XBMC_BUTTON(XBMC_BUTTON_LEFT)
+#define XBMC_BUTTON_MMASK XBMC_BUTTON(XBMC_BUTTON_MIDDLE)
+#define XBMC_BUTTON_RMASK XBMC_BUTTON(XBMC_BUTTON_RIGHT)
+#define XBMC_BUTTON_X1MASK XBMC_BUTTON(XBMC_BUTTON_X1)
+#define XBMC_BUTTON_X2MASK XBMC_BUTTON(XBMC_BUTTON_X2)
+#define XBMC_BUTTON_X3MASK XBMC_BUTTON(XBMC_BUTTON_X3)
+#define XBMC_BUTTON_X4MASK XBMC_BUTTON(XBMC_BUTTON_X4)
+
+#define MOUSE_MINIMUM_MOVEMENT 2
+#define MOUSE_DOUBLE_CLICK_LENGTH 500L
+#define MOUSE_ACTIVE_LENGTH 5000L
+
+#define MOUSE_MAX_BUTTON 7
+
+enum MOUSE_STATE
+{
+ /*! Normal state */
+ MOUSE_STATE_NORMAL = 1,
+ /*! Control below the mouse is currently in focus */
+ MOUSE_STATE_FOCUS,
+ /*! A drag operation is being performed */
+ MOUSE_STATE_DRAG,
+ /*! A mousebutton is being clicked */
+ MOUSE_STATE_CLICK
+};
+
+enum MOUSE_BUTTON
+{
+ MOUSE_LEFT_BUTTON = 0,
+ MOUSE_RIGHT_BUTTON,
+ MOUSE_MIDDLE_BUTTON,
+ MOUSE_EXTRA_BUTTON1,
+ MOUSE_EXTRA_BUTTON2,
+ MOUSE_EXTRA_BUTTON3,
+ MOUSE_EXTRA_BUTTON4
+};
+
+enum class HoldAction
+{
+ /*! No action should occur */
+ NONE,
+ /*! A drag action has started */
+ DRAG_START,
+ /*! A drag action is in progress */
+ DRAG,
+ /*! A drag action has finished */
+ DRAG_END
+};
+
+//! Holds everything we know about the current state of the mouse
+struct MouseState
+{
+ /*! X location */
+ int x;
+ /*! Y location */
+ int y;
+ /*! Change in x */
+ int16_t dx;
+ /*! Change in y */
+ int16_t dy;
+ /*! Change in z (wheel) */
+ int8_t dz;
+ /*! Current state of the buttons */
+ bool button[MOUSE_MAX_BUTTON];
+ /*! True if the mouse is active */
+ bool active;
+};
+
+struct MousePosition
+{
+ int x;
+ int y;
+};
+
+class CAction;
+
+class CMouseStat
+{
+public:
+ CMouseStat();
+ virtual ~CMouseStat();
+
+ void Initialize();
+ void HandleEvent(const XBMC_Event& newEvent);
+ void SetResolution(int maxX, int maxY, float speedX, float speedY);
+ bool IsActive();
+ bool IsEnabled() const;
+
+ void SetActive(bool active = true);
+ void SetState(MOUSE_STATE state) { m_pointerState = state; }
+ void SetEnabled(bool enabled = true);
+ MOUSE_STATE GetState() const { return m_pointerState; }
+ uint32_t GetKey() const;
+
+ HoldAction GetHold(int ButtonID) const;
+ inline int GetX(void) const { return m_mouseState.x; }
+ inline int GetY(void) const { return m_mouseState.y; }
+ inline int GetDX(void) const { return m_mouseState.dx; }
+ inline int GetDY(void) const { return m_mouseState.dy; }
+ MousePosition GetPosition() { return MousePosition{m_mouseState.x, m_mouseState.y}; }
+
+private:
+ /*! \brief Holds information regarding a particular mouse button state
+
+ The CButtonState class is used to track where in a button event the mouse currently is.
+ There is effectively 5 BUTTON_STATE's available, and transitioning between those states
+ is handled by the Update() function.
+
+ The actions we detect are:
+ * short clicks - down/up press of the mouse within short_click_time ms, where the pointer stays
+ within click_confines pixels
+ * long clicks - down/up press of the mouse greater than short_click_time ms, where the pointers
+ stays within click_confines pixels
+ * double clicks - a further down press of the mouse within double_click_time of the up press of
+ a short click, where the pointer stays within click_confines pixels
+ * drag - the mouse is down and has been moved more than click_confines pixels
+
+ \sa CMouseStat
+ */
+ class CButtonState
+ {
+ public:
+ /*! \brief enum for the actions to perform as a result of an Update function
+ */
+ enum BUTTON_ACTION
+ {
+ MB_NONE = 0, ///< no action should occur
+ MB_SHORT_CLICK, ///< a short click has occurred (a double click may be in process)
+ MB_LONG_CLICK, ///< a long click has occurred
+ MB_DOUBLE_CLICK, ///< a double click has occurred
+ MB_DRAG_START, ///< a drag action has started
+ MB_DRAG, ///< a drag action is in progress
+ MB_DRAG_END
+ }; ///< a drag action has finished
+
+ CButtonState();
+
+ /*! \brief Update the button state, with where the mouse is, and whether the button is down or
+ not
+
+ \param time frame time in ms
+ \param x horizontal coordinate of the mouse
+ \param y vertical coordinate of the mouse
+ \param down true if the button is down
+ \return action that should be performed
+ */
+ BUTTON_ACTION Update(unsigned int time, int x, int y, bool down);
+
+ private:
+ //! number of pixels that the pointer may move while the button is down to
+ //! trigger a click
+ static const unsigned int click_confines = 5;
+
+ //! Time for mouse down/up to trigger a short click rather than a long click
+ static const unsigned int short_click_time = 1000;
+
+ //! Time for mouse down following a short click to trigger a double click
+ static const unsigned int double_click_time = 500;
+
+ bool InClickRange(int x, int y) const;
+
+ enum BUTTON_STATE
+ {
+ STATE_RELEASED = 0, ///< mouse button is released, no events pending
+ STATE_IN_CLICK, ///< mouse button is down, a click is pending
+ STATE_IN_DOUBLE_CLICK, ///< mouse button is released, pending double click
+ STATE_IN_DOUBLE_IGNORE, ///< mouse button is down following double click
+ STATE_IN_DRAG
+ }; ///< mouse button is down during a drag
+
+ BUTTON_STATE m_state;
+ unsigned int m_time;
+ int m_x;
+ int m_y;
+ };
+
+ /*! \brief detect whether the mouse has moved
+
+ Uses a trigger threshold of 2 pixels to detect mouse movement
+
+ \return whether the mouse has moved past the trigger threshold.
+ */
+ bool MovedPastThreshold() const;
+
+ // state of the mouse
+ MOUSE_STATE m_pointerState{MOUSE_STATE_NORMAL};
+ MouseState m_mouseState{};
+ bool m_mouseEnabled;
+ CButtonState m_buttonState[MOUSE_MAX_BUTTON];
+
+ int m_maxX{0};
+ int m_maxY{0};
+ float m_speedX{0.0f};
+ float m_speedY{0.0f};
+
+ // active/click timers
+ unsigned int m_lastActiveTime;
+
+ bool bClick[MOUSE_MAX_BUTTON]{};
+ bool bDoubleClick[MOUSE_MAX_BUTTON]{};
+ HoldAction m_hold[MOUSE_MAX_BUTTON]{};
+ bool bLongClick[MOUSE_MAX_BUTTON]{};
+
+ uint32_t m_Key;
+};
diff --git a/xbmc/input/mouse/MouseTranslator.cpp b/xbmc/input/mouse/MouseTranslator.cpp
new file mode 100644
index 0000000..b5cac7d
--- /dev/null
+++ b/xbmc/input/mouse/MouseTranslator.cpp
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2017-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 "MouseTranslator.h"
+
+#include "MouseStat.h"
+#include "input/Key.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <map>
+#include <string>
+
+using namespace KODI;
+using namespace MOUSE;
+
+namespace
+{
+
+using ActionName = std::string;
+using KeyID = uint32_t;
+
+static const std::map<ActionName, KeyID> MouseKeys = {{"click", KEY_MOUSE_CLICK},
+ {"leftclick", KEY_MOUSE_CLICK},
+ {"rightclick", KEY_MOUSE_RIGHTCLICK},
+ {"middleclick", KEY_MOUSE_MIDDLECLICK},
+ {"doubleclick", KEY_MOUSE_DOUBLE_CLICK},
+ {"longclick", KEY_MOUSE_LONG_CLICK},
+ {"wheelup", KEY_MOUSE_WHEEL_UP},
+ {"wheeldown", KEY_MOUSE_WHEEL_DOWN},
+ {"mousemove", KEY_MOUSE_MOVE},
+ {"mousedrag", KEY_MOUSE_DRAG},
+ {"mousedragstart", KEY_MOUSE_DRAG_START},
+ {"mousedragend", KEY_MOUSE_DRAG_END},
+ {"mouserdrag", KEY_MOUSE_RDRAG},
+ {"mouserdragstart", KEY_MOUSE_RDRAG_START},
+ {"mouserdragend", KEY_MOUSE_RDRAG_END}};
+
+} // anonymous namespace
+
+uint32_t CMouseTranslator::TranslateCommand(const TiXmlElement* pButton)
+{
+ uint32_t buttonId = 0;
+
+ if (pButton != nullptr)
+ {
+ std::string szKey = pButton->ValueStr();
+ if (!szKey.empty())
+ {
+ StringUtils::ToLower(szKey);
+
+ auto it = MouseKeys.find(szKey);
+ if (it != MouseKeys.end())
+ buttonId = it->second;
+
+ if (buttonId == 0)
+ {
+ CLog::Log(LOGERROR, "Unknown mouse action ({}), skipping", pButton->Value());
+ }
+ else
+ {
+ int id = 0;
+ if ((pButton->QueryIntAttribute("id", &id) == TIXML_SUCCESS))
+ {
+ if (0 <= id && id < MOUSE_MAX_BUTTON)
+ buttonId += id;
+ }
+ }
+ }
+ }
+
+ return buttonId;
+}
+
+bool CMouseTranslator::TranslateEventID(unsigned int eventId, BUTTON_ID& buttonId)
+{
+ switch (eventId)
+ {
+ case XBMC_BUTTON_LEFT:
+ {
+ buttonId = BUTTON_ID::LEFT;
+ return true;
+ }
+ case XBMC_BUTTON_MIDDLE:
+ {
+ buttonId = BUTTON_ID::MIDDLE;
+ return true;
+ }
+ case XBMC_BUTTON_RIGHT:
+ {
+ buttonId = BUTTON_ID::RIGHT;
+ return true;
+ }
+ case XBMC_BUTTON_WHEELUP:
+ {
+ buttonId = BUTTON_ID::WHEEL_UP;
+ return true;
+ }
+ case XBMC_BUTTON_WHEELDOWN:
+ {
+ buttonId = BUTTON_ID::WHEEL_DOWN;
+ return true;
+ }
+ case XBMC_BUTTON_X1:
+ {
+ buttonId = BUTTON_ID::BUTTON4;
+ return true;
+ }
+ case XBMC_BUTTON_X2:
+ {
+ buttonId = BUTTON_ID::BUTTON5;
+ return true;
+ }
+ case XBMC_BUTTON_X3:
+ {
+ buttonId = BUTTON_ID::HORIZ_WHEEL_LEFT;
+ return true;
+ }
+ case XBMC_BUTTON_X4:
+ {
+ buttonId = BUTTON_ID::HORIZ_WHEEL_RIGHT;
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
diff --git a/xbmc/input/mouse/MouseTranslator.h b/xbmc/input/mouse/MouseTranslator.h
new file mode 100644
index 0000000..e3d4d4b
--- /dev/null
+++ b/xbmc/input/mouse/MouseTranslator.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017-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 "MouseTypes.h"
+
+#include <stdint.h>
+
+class TiXmlElement;
+
+class CMouseTranslator
+{
+public:
+ /*!
+ * \brief Translate a keymap element to a key ID
+ */
+ static uint32_t TranslateCommand(const TiXmlElement* pButton);
+
+ /*!
+ * \brief Translate a mouse event ID to a mouse button index
+ *
+ * \param eventId The event ID from MouseStat.h
+ * \param[out] buttonId The button ID from MouseTypes.h, or unmodified if unsuccessful
+ *
+ * \return True if successful, false otherwise
+ */
+ static bool TranslateEventID(unsigned int eventId, KODI::MOUSE::BUTTON_ID& buttonId);
+};
diff --git a/xbmc/input/mouse/MouseTypes.h b/xbmc/input/mouse/MouseTypes.h
new file mode 100644
index 0000000..f11f7ea
--- /dev/null
+++ b/xbmc/input/mouse/MouseTypes.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 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 "input/InputTypes.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace MOUSE
+{
+/*!
+ * \brief Buttons on a mouse
+ */
+enum class BUTTON_ID
+{
+ UNKNOWN,
+ LEFT,
+ RIGHT,
+ MIDDLE,
+ BUTTON4,
+ BUTTON5,
+ WHEEL_UP,
+ WHEEL_DOWN,
+ HORIZ_WHEEL_LEFT,
+ HORIZ_WHEEL_RIGHT,
+};
+
+/*!
+ * \brief Name of a mouse button
+ *
+ * Names are defined in the mouse's controller profile.
+ */
+using ButtonName = std::string;
+
+/*!
+ * \brief Directions of motion for a mouse pointer
+ */
+using POINTER_DIRECTION = INPUT::CARDINAL_DIRECTION;
+
+/*!
+ * \brief Name of the mouse pointer
+ *
+ * Names are defined in the mouse's controller profile.
+ */
+using PointerName = std::string;
+} // namespace MOUSE
+} // namespace KODI
diff --git a/xbmc/input/mouse/generic/CMakeLists.txt b/xbmc/input/mouse/generic/CMakeLists.txt
new file mode 100644
index 0000000..b5c2858
--- /dev/null
+++ b/xbmc/input/mouse/generic/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES MouseInputHandling.cpp)
+
+set(HEADERS MouseInputHandling.h)
+
+core_add_library(input_mouse_generic)
diff --git a/xbmc/input/mouse/generic/MouseInputHandling.cpp b/xbmc/input/mouse/generic/MouseInputHandling.cpp
new file mode 100644
index 0000000..0f51ede
--- /dev/null
+++ b/xbmc/input/mouse/generic/MouseInputHandling.cpp
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2016-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 "MouseInputHandling.h"
+
+#include "input/InputTranslator.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "input/mouse/interfaces/IMouseInputHandler.h"
+
+using namespace KODI;
+using namespace MOUSE;
+
+CMouseInputHandling::CMouseInputHandling(IMouseInputHandler* handler,
+ JOYSTICK::IButtonMap* buttonMap)
+ : m_handler(handler), m_buttonMap(buttonMap)
+{
+}
+
+bool CMouseInputHandling::OnPosition(int x, int y)
+{
+ using namespace JOYSTICK;
+
+ if (!m_bHasPosition)
+ {
+ m_bHasPosition = true;
+ m_x = x;
+ m_y = y;
+ return true;
+ }
+
+ int dx = x - m_x;
+ int dy = y - m_y;
+
+ bool bHandled = false;
+
+ // Get direction of motion
+ POINTER_DIRECTION dir = GetPointerDirection(dx, dy);
+
+ CDriverPrimitive source(dir);
+ if (source.IsValid())
+ {
+ // Get pointer in direction of motion
+ PointerName pointerName;
+ if (m_buttonMap->GetFeature(source, pointerName))
+ {
+ // Get orthogonal direction of motion
+ POINTER_DIRECTION dirCCW = GetOrthogonalDirectionCCW(dir);
+
+ // Get mapped directions of motion for rotation and reflection
+ CDriverPrimitive target;
+ CDriverPrimitive targetCCW;
+
+ if (m_buttonMap->GetRelativePointer(pointerName, dir, target))
+ m_buttonMap->GetRelativePointer(pointerName, dirCCW, targetCCW);
+
+ if (target.IsValid())
+ {
+ // Invert y to right-handed cartesian system
+ dy *= -1;
+
+ // Perform rotation
+ int rotation[2][2] = {{1, 0}, {0, 1}};
+
+ GetRotation(dir, target.PointerDirection(), rotation);
+
+ dx = rotation[0][0] * dx + rotation[0][1] * dy;
+ dy = rotation[1][0] * dx + rotation[1][1] * dy;
+
+ if (targetCCW.IsValid())
+ {
+ // Perform reflection
+ int reflection[2][2] = {{1, 0}, {0, 1}};
+
+ GetReflectionCCW(target.PointerDirection(), targetCCW.PointerDirection(), reflection);
+
+ dx = reflection[0][0] * dx + reflection[0][1] * dy;
+ dy = reflection[1][0] * dx + reflection[1][1] * dy;
+ }
+
+ // Invert y back to left-handed coordinate system
+ dy *= -1;
+ }
+
+ bHandled = m_handler->OnMotion(pointerName, dx, dy);
+ }
+ }
+ else
+ {
+ // Don't fall through - might disrupt the game
+ bHandled = true;
+ }
+
+ m_x = x;
+ m_y = y;
+
+ return bHandled;
+}
+
+bool CMouseInputHandling::OnButtonPress(BUTTON_ID button)
+{
+ bool bHandled = false;
+
+ JOYSTICK::CDriverPrimitive source(button);
+
+ ButtonName buttonName;
+ if (m_buttonMap->GetFeature(source, buttonName))
+ bHandled = m_handler->OnButtonPress(buttonName);
+
+ return bHandled;
+}
+
+void CMouseInputHandling::OnButtonRelease(BUTTON_ID button)
+{
+ JOYSTICK::CDriverPrimitive source(button);
+
+ ButtonName buttonName;
+ if (m_buttonMap->GetFeature(source, buttonName))
+ m_handler->OnButtonRelease(buttonName);
+}
+
+POINTER_DIRECTION CMouseInputHandling::GetPointerDirection(int x, int y)
+{
+ using namespace INPUT;
+
+ return CInputTranslator::VectorToCardinalDirection(static_cast<float>(x), static_cast<float>(-y));
+}
+
+POINTER_DIRECTION CMouseInputHandling::GetOrthogonalDirectionCCW(POINTER_DIRECTION direction)
+{
+ switch (direction)
+ {
+ case POINTER_DIRECTION::RIGHT:
+ return POINTER_DIRECTION::UP;
+ case POINTER_DIRECTION::UP:
+ return POINTER_DIRECTION::LEFT;
+ case POINTER_DIRECTION::LEFT:
+ return POINTER_DIRECTION::DOWN;
+ case POINTER_DIRECTION::DOWN:
+ return POINTER_DIRECTION::RIGHT;
+ default:
+ break;
+ }
+
+ return POINTER_DIRECTION::NONE;
+}
+
+void CMouseInputHandling::GetRotation(POINTER_DIRECTION source,
+ POINTER_DIRECTION target,
+ int (&rotation)[2][2])
+{
+ switch (source)
+ {
+ case POINTER_DIRECTION::RIGHT:
+ {
+ switch (target)
+ {
+ case POINTER_DIRECTION::UP:
+ GetRotation(90, rotation);
+ break;
+ case POINTER_DIRECTION::LEFT:
+ GetRotation(180, rotation);
+ break;
+ case POINTER_DIRECTION::DOWN:
+ GetRotation(270, rotation);
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case POINTER_DIRECTION::UP:
+ {
+ switch (target)
+ {
+ case POINTER_DIRECTION::LEFT:
+ GetRotation(90, rotation);
+ break;
+ case POINTER_DIRECTION::DOWN:
+ GetRotation(180, rotation);
+ break;
+ case POINTER_DIRECTION::RIGHT:
+ GetRotation(270, rotation);
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case POINTER_DIRECTION::LEFT:
+ {
+ switch (target)
+ {
+ case POINTER_DIRECTION::DOWN:
+ GetRotation(90, rotation);
+ break;
+ case POINTER_DIRECTION::RIGHT:
+ GetRotation(180, rotation);
+ break;
+ case POINTER_DIRECTION::UP:
+ GetRotation(270, rotation);
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case POINTER_DIRECTION::DOWN:
+ {
+ switch (target)
+ {
+ case POINTER_DIRECTION::RIGHT:
+ GetRotation(90, rotation);
+ break;
+ case POINTER_DIRECTION::UP:
+ GetRotation(180, rotation);
+ break;
+ case POINTER_DIRECTION::LEFT:
+ GetRotation(270, rotation);
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void CMouseInputHandling::GetRotation(int deg, int (&rotation)[2][2])
+{
+ switch (deg)
+ {
+ case 90:
+ {
+ rotation[0][0] = 0;
+ rotation[0][1] = -1;
+ rotation[1][0] = 1;
+ rotation[1][1] = 0;
+ break;
+ }
+ case 180:
+ {
+ rotation[0][0] = -1;
+ rotation[0][1] = 0;
+ rotation[1][0] = 0;
+ rotation[1][1] = -1;
+ break;
+ }
+ case 270:
+ {
+ rotation[0][0] = 0;
+ rotation[0][1] = 1;
+ rotation[1][0] = -1;
+ rotation[1][1] = 0;
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void CMouseInputHandling::GetReflectionCCW(POINTER_DIRECTION source,
+ POINTER_DIRECTION target,
+ int (&rotation)[2][2])
+{
+ switch (source)
+ {
+ case POINTER_DIRECTION::RIGHT:
+ {
+ switch (target)
+ {
+ case POINTER_DIRECTION::DOWN:
+ GetReflection(0, rotation);
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case POINTER_DIRECTION::UP:
+ {
+ switch (target)
+ {
+ case POINTER_DIRECTION::RIGHT:
+ GetReflection(90, rotation);
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case POINTER_DIRECTION::LEFT:
+ {
+ switch (target)
+ {
+ case POINTER_DIRECTION::UP:
+ GetReflection(180, rotation);
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ case POINTER_DIRECTION::DOWN:
+ {
+ switch (target)
+ {
+ case POINTER_DIRECTION::LEFT:
+ GetReflection(270, rotation);
+ break;
+ default:
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void CMouseInputHandling::GetReflection(int deg, int (&reflection)[2][2])
+{
+ switch (deg)
+ {
+ case 0:
+ case 180:
+ {
+ reflection[0][0] = 1;
+ reflection[0][1] = 0;
+ reflection[1][0] = 0;
+ reflection[1][1] = -1;
+ break;
+ }
+ case 90:
+ case 270:
+ {
+ reflection[0][0] = -1;
+ reflection[0][1] = 0;
+ reflection[1][0] = 0;
+ reflection[1][1] = 1;
+ break;
+ }
+ default:
+ break;
+ }
+}
diff --git a/xbmc/input/mouse/generic/MouseInputHandling.h b/xbmc/input/mouse/generic/MouseInputHandling.h
new file mode 100644
index 0000000..6d19da9
--- /dev/null
+++ b/xbmc/input/mouse/generic/MouseInputHandling.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016-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 "input/mouse/MouseTypes.h"
+#include "input/mouse/interfaces/IMouseDriverHandler.h"
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IButtonMap;
+}
+
+namespace MOUSE
+{
+class IMouseInputHandler;
+
+/*!
+ * \ingroup mouse
+ * \brief Class to translate input from driver info to higher-level features
+ */
+class CMouseInputHandling : public IMouseDriverHandler
+{
+public:
+ CMouseInputHandling(IMouseInputHandler* handler, JOYSTICK::IButtonMap* buttonMap);
+
+ ~CMouseInputHandling(void) override = default;
+
+ // implementation of IMouseDriverHandler
+ bool OnPosition(int x, int y) override;
+ bool OnButtonPress(BUTTON_ID button) override;
+ void OnButtonRelease(BUTTON_ID button) override;
+
+private:
+ // Utility functions
+ static POINTER_DIRECTION GetPointerDirection(int x, int y);
+ static POINTER_DIRECTION GetOrthogonalDirectionCCW(POINTER_DIRECTION direction);
+
+ static void GetRotation(POINTER_DIRECTION source,
+ POINTER_DIRECTION target,
+ int (&rotation)[2][2]);
+ static void GetRotation(int deg, int (&rotation)[2][2]);
+
+ static void GetReflectionCCW(POINTER_DIRECTION source,
+ POINTER_DIRECTION target,
+ int (&reflection)[2][2]);
+ static void GetReflection(int deg, int (&reflection)[2][2]);
+
+ // Construction parameters
+ IMouseInputHandler* const m_handler;
+ JOYSTICK::IButtonMap* const m_buttonMap;
+
+ // Mouse parameters
+ bool m_bHasPosition = false;
+ int m_x = 0;
+ int m_y = 0;
+};
+} // namespace MOUSE
+} // namespace KODI
diff --git a/xbmc/input/mouse/interfaces/IMouseDriverHandler.h b/xbmc/input/mouse/interfaces/IMouseDriverHandler.h
new file mode 100644
index 0000000..7989e39
--- /dev/null
+++ b/xbmc/input/mouse/interfaces/IMouseDriverHandler.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016-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 "input/mouse/MouseTypes.h"
+
+namespace KODI
+{
+namespace MOUSE
+{
+/*!
+ * \ingroup mouse
+ * \brief Interface for handling mouse driver events
+ */
+class IMouseDriverHandler
+{
+public:
+ virtual ~IMouseDriverHandler(void) = default;
+
+ /*!
+ * \brief Handle mouse position updates
+ *
+ * \param x The new x coordinate of the pointer
+ * \param y The new y coordinate of the pointer
+ *
+ * The mouse uses a left-handed (graphics) cartesian coordinate system.
+ * Positive X is right, positive Y is down.
+ *
+ * \return True if the event was handled, false otherwise
+ */
+ virtual bool OnPosition(int x, int y) = 0;
+
+ /*!
+ * \brief A mouse button has been pressed
+ *
+ * \param button The index of the pressed button
+ *
+ * \return True if the event was handled, otherwise false
+ */
+ virtual bool OnButtonPress(BUTTON_ID button) = 0;
+
+ /*!
+ * \brief A mouse button has been released
+ *
+ * \param button The index of the released button
+ */
+ virtual void OnButtonRelease(BUTTON_ID button) = 0;
+};
+} // namespace MOUSE
+} // namespace KODI
diff --git a/xbmc/input/mouse/interfaces/IMouseInputHandler.h b/xbmc/input/mouse/interfaces/IMouseInputHandler.h
new file mode 100644
index 0000000..910ddd4
--- /dev/null
+++ b/xbmc/input/mouse/interfaces/IMouseInputHandler.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016-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 "input/mouse/MouseTypes.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace MOUSE
+{
+/*!
+ * \ingroup mouse
+ * \brief Interface for handling mouse events
+ */
+class IMouseInputHandler
+{
+public:
+ virtual ~IMouseInputHandler(void) = default;
+
+ /*!
+ * \brief The controller profile for this mouse input handler
+ *
+ * \return The ID of the add-on extending kodi.game.controller
+ */
+ virtual std::string ControllerID(void) const = 0;
+
+ /*!
+ * \brief A relative pointer has moved
+ *
+ * \param relpointer The name of the relative pointer being moved
+ * \param dx The relative x coordinate of motion
+ * \param dy The relative y coordinate of motion
+ *
+ * The mouse uses a left-handed (graphics) cartesian coordinate system.
+ * Positive X is right, positive Y is down.
+ *
+ * \return True if the event was handled, otherwise false
+ */
+ virtual bool OnMotion(const PointerName& relpointer, int dx, int dy) = 0;
+
+ /*!
+ * \brief A mouse button has been pressed
+ *
+ * \param button The name of the feature being pressed
+ *
+ * \return True if the event was handled, otherwise false
+ */
+ virtual bool OnButtonPress(const ButtonName& button) = 0;
+
+ /*!
+ * \brief A mouse button has been released
+ *
+ * \param button The name of the feature being released
+ */
+ virtual void OnButtonRelease(const ButtonName& button) = 0;
+};
+} // namespace MOUSE
+} // namespace KODI
diff --git a/xbmc/input/mouse/interfaces/IMouseInputProvider.h b/xbmc/input/mouse/interfaces/IMouseInputProvider.h
new file mode 100644
index 0000000..f289070
--- /dev/null
+++ b/xbmc/input/mouse/interfaces/IMouseInputProvider.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017-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
+
+namespace KODI
+{
+namespace MOUSE
+{
+class IMouseInputHandler;
+
+/*!
+ * \ingroup mouse
+ * \brief Interface for classes that can provide mouse input
+ */
+class IMouseInputProvider
+{
+public:
+ virtual ~IMouseInputProvider() = default;
+
+ /*!
+ * \brief Registers a handler to be called on mouse input
+ *
+ * \param handler The handler to receive mouse input provided by this class
+ * \param bPromiscuous True to observe all events without affecting
+ * subsequent handlers
+ */
+ virtual void RegisterMouseHandler(IMouseInputHandler* handler, bool bPromiscuous) = 0;
+
+ /*!
+ * \brief Unregisters handler from mouse input
+ *
+ * \param handler The handler that was receiving mouse input
+ */
+ virtual void UnregisterMouseHandler(IMouseInputHandler* handler) = 0;
+};
+} // namespace MOUSE
+} // namespace KODI
diff --git a/xbmc/input/remote/IRRemote.h b/xbmc/input/remote/IRRemote.h
new file mode 100644
index 0000000..84a7dac
--- /dev/null
+++ b/xbmc/input/remote/IRRemote.h
@@ -0,0 +1,106 @@
+/*
+ * 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
+
+#define XINPUT_IR_REMOTE_DISPLAY 213
+#define XINPUT_IR_REMOTE_REVERSE 226
+#define XINPUT_IR_REMOTE_PLAY 234
+#define XINPUT_IR_REMOTE_FORWARD 227
+#define XINPUT_IR_REMOTE_SKIP_MINUS 221
+#define XINPUT_IR_REMOTE_STOP 224
+#define XINPUT_IR_REMOTE_PAUSE 230
+#define XINPUT_IR_REMOTE_SKIP_PLUS 223
+#define XINPUT_IR_REMOTE_TITLE 229
+#define XINPUT_IR_REMOTE_INFO 195
+
+#define XINPUT_IR_REMOTE_UP 166
+#define XINPUT_IR_REMOTE_DOWN 167
+#define XINPUT_IR_REMOTE_LEFT 169
+#define XINPUT_IR_REMOTE_RIGHT 168
+
+#define XINPUT_IR_REMOTE_SELECT 11
+#define XINPUT_IR_REMOTE_ENTER 22
+
+#define XINPUT_IR_REMOTE_SUBTITLE 44
+#define XINPUT_IR_REMOTE_LANGUAGE 45
+
+#define XINPUT_IR_REMOTE_MENU 247
+#define XINPUT_IR_REMOTE_BACK 216
+
+#define XINPUT_IR_REMOTE_1 206
+#define XINPUT_IR_REMOTE_2 205
+#define XINPUT_IR_REMOTE_3 204
+#define XINPUT_IR_REMOTE_4 203
+#define XINPUT_IR_REMOTE_5 202
+#define XINPUT_IR_REMOTE_6 201
+#define XINPUT_IR_REMOTE_7 200
+#define XINPUT_IR_REMOTE_8 199
+#define XINPUT_IR_REMOTE_9 198
+#define XINPUT_IR_REMOTE_0 207
+
+// additional keys from the media center extender for xbox remote
+#define XINPUT_IR_REMOTE_POWER 196
+#define XINPUT_IR_REMOTE_MY_TV 49
+#define XINPUT_IR_REMOTE_MY_MUSIC 9
+#define XINPUT_IR_REMOTE_MY_PICTURES 6
+#define XINPUT_IR_REMOTE_MY_VIDEOS 7
+
+#define XINPUT_IR_REMOTE_RECORD 232
+
+#define XINPUT_IR_REMOTE_START 37
+#define XINPUT_IR_REMOTE_VOLUME_PLUS 208
+#define XINPUT_IR_REMOTE_VOLUME_MINUS 209
+#define XINPUT_IR_REMOTE_CHANNEL_PLUS 210
+#define XINPUT_IR_REMOTE_CHANNEL_MINUS 211
+#define XINPUT_IR_REMOTE_MUTE 192
+
+#define XINPUT_IR_REMOTE_RECORDED_TV 101
+#define XINPUT_IR_REMOTE_LIVE_TV 24
+#define XINPUT_IR_REMOTE_STAR 40
+#define XINPUT_IR_REMOTE_HASH 41
+#define XINPUT_IR_REMOTE_CLEAR 249
+
+// additional keys not defined by xbox remotes but present on generic remotes
+#define XINPUT_IR_REMOTE_TELETEXT 250
+#define XINPUT_IR_REMOTE_RED 251
+#define XINPUT_IR_REMOTE_GREEN 252
+#define XINPUT_IR_REMOTE_YELLOW 253
+#define XINPUT_IR_REMOTE_BLUE 254
+#define XINPUT_IR_REMOTE_PLAYLIST 255
+#define XINPUT_IR_REMOTE_GUIDE 50
+
+#define XINPUT_IR_REMOTE_LIVE_RADIO 248
+#define XINPUT_IR_REMOTE_EPG_SEARCH 246
+
+#define XINPUT_IR_REMOTE_EJECT 235
+#define XINPUT_IR_REMOTE_CONTENTS_MENU 236
+#define XINPUT_IR_REMOTE_ROOT_MENU 237
+#define XINPUT_IR_REMOTE_TOP_MENU 238
+#define XINPUT_IR_REMOTE_DVD_MENU 239
+
+#define XINPUT_IR_REMOTE_PRINT 240
+
+// Reserved 256 -> ...
+// Key.h
+// KEY_BUTTON_*
+
+typedef struct _XINPUT_IR_REMOTE
+{
+ unsigned char wButtons;
+ unsigned char region; // just a guess
+
+ //! Some value that is changing while a button is pressed... could be the
+ //! state of the buffer
+ unsigned char counter;
+
+ //! If > 0: first event triggered after a button was pressed on the remote
+ //! If 0: not first event
+ unsigned char firstEvent;
+
+} XINPUT_IR_REMOTE, *PIR_REMOTE;
diff --git a/xbmc/input/touch/CMakeLists.txt b/xbmc/input/touch/CMakeLists.txt
new file mode 100644
index 0000000..12a228b
--- /dev/null
+++ b/xbmc/input/touch/CMakeLists.txt
@@ -0,0 +1,8 @@
+set(SOURCES ITouchInputHandling.cpp)
+
+set(HEADERS ITouchActionHandler.h
+ ITouchInputHandler.h
+ ITouchInputHandling.h
+ TouchTypes.h)
+
+core_add_library(input_touch)
diff --git a/xbmc/input/touch/ITouchActionHandler.h b/xbmc/input/touch/ITouchActionHandler.h
new file mode 100644
index 0000000..28afdd7
--- /dev/null
+++ b/xbmc/input/touch/ITouchActionHandler.h
@@ -0,0 +1,261 @@
+/*
+ * 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 <stdint.h>
+
+/*!
+ * \ingroup touch
+ * \brief Directions in which a touch can moved
+ *
+ * These values can be combined (bitwise OR) to specify multiple directions.
+ */
+typedef enum
+{
+ TouchMoveDirectionNone = 0x0,
+ TouchMoveDirectionLeft = 0x1,
+ TouchMoveDirectionRight = 0x2,
+ TouchMoveDirectionUp = 0x4,
+ TouchMoveDirectionDown = 0x8
+} TouchMoveDirection;
+
+/*!
+ * \ingroup touch
+ * \brief Interface defining all supported touch action events
+ */
+class ITouchActionHandler
+{
+public:
+ virtual ~ITouchActionHandler() = default;
+
+ /*!
+ * \brief A touch action has been aborted
+ */
+ virtual void OnTouchAbort() {}
+
+ /*!
+ * \brief A single touch has started
+ *
+ * \param x The x coordinate (with sub-pixel) of the touch
+ * \param y The y coordinate (with sub-pixel) of the touch
+ *
+ * \return True if the event was handled otherwise false
+ *
+ * \sa OnSingleTap
+ */
+ virtual bool OnSingleTouchStart(float x, float y) { return true; }
+ /*!
+ * \brief A single touch has been held down for a certain amount of time
+ *
+ * \param x The x coordinate (with sub-pixel) of the touch
+ * \param y The y coordinate (with sub-pixel) of the touch
+ *
+ * \return True if the event was handled otherwise false
+ *
+ * \sa OnSingleLongPress
+ */
+ virtual bool OnSingleTouchHold(float x, float y) { return true; }
+ /*!
+ * \brief A single touch has moved
+ *
+ * \param x The x coordinate (with sub-pixel) of the current touch
+ * \param y The y coordinate (with sub-pixel) of the current touch
+ * \param offsetX The covered distance on the x axis (with sub-pixel)
+ * \param offsetX The covered distance on the y axis (with sub-pixel)
+ * \param velocityX The velocity of the gesture in x direction (pixels/second)
+ * \param velocityX The velocity of the gesture in y direction (pixels/second)
+ *
+ * \return True if the event was handled otherwise false
+ *
+ * \sa OnTouchGesturePan
+ */
+ virtual bool OnSingleTouchMove(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY)
+ {
+ return true;
+ }
+ /*!
+ * \brief A single touch has been lifted
+ *
+ * \param x The x coordinate (with sub-pixel) of the touch
+ * \param y The y coordinate (with sub-pixel) of the touch
+ *
+ * \return True if the event was handled otherwise false
+ *
+ * \sa OnSingleTap
+ */
+ virtual bool OnSingleTouchEnd(float x, float y) { return true; }
+
+ /*!
+ * \brief An additional touch has been performed
+ *
+ * \param x The x coordinate (with sub-pixel) of the touch
+ * \param y The y coordinate (with sub-pixel) of the touch
+ * \param pointer The pointer that has performed the touch
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnMultiTouchDown(float x, float y, int32_t pointer) { return true; }
+ /*!
+ * \brief Multiple simultaneous touches have been held down for a certain amount of time
+ *
+ * \param x The x coordinate (with sub-pixel) of the touch
+ * \param y The y coordinate (with sub-pixel) of the touch
+ * \param pointers The number of pointers involved (default 2)
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnMultiTouchHold(float x, float y, int32_t pointers = 2) { return true; }
+ /*!
+ * \brief A touch has moved
+ *
+ * \param x The x coordinate (with sub-pixel) of the current touch
+ * \param y The y coordinate (with sub-pixel) of the current touch
+ * \param offsetX The covered distance on the x axis (with sub-pixel)
+ * \param offsetX The covered distance on the y axis (with sub-pixel)
+ * \param velocityX The velocity of the gesture in x direction (pixels/second)
+ * \param velocityX The velocity of the gesture in y direction (pixels/second)
+ * \param pointer The pointer that has performed the touch
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnMultiTouchMove(float x,
+ float y,
+ float offsetX,
+ float offsetY,
+ float velocityX,
+ float velocityY,
+ int32_t pointer)
+ {
+ return true;
+ }
+ /*!
+ * \brief A touch has been lifted (but there are still active touches)
+ *
+ * \param x The x coordinate (with sub-pixel) of the touch
+ * \param y The y coordinate (with sub-pixel) of the touch
+ * \param pointer The pointer that has performed the touch
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnMultiTouchUp(float x, float y, int32_t pointer) { return true; }
+
+ /*!
+ * \brief A pan gesture with a single touch has been started
+ *
+ * \param x The x coordinate (with sub-pixel) of the initial touch
+ * \param y The y coordinate (with sub-pixel) of the initial touch
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnTouchGestureStart(float x, float y) { return true; }
+ /*!
+ * \brief A pan gesture with a single touch is in progress
+ *
+ * \param x The x coordinate (with sub-pixel) of the current touch
+ * \param y The y coordinate (with sub-pixel) of the current touch
+ * \param offsetX The covered distance on the x axis (with sub-pixel)
+ * \param offsetX The covered distance on the y axis (with sub-pixel)
+ * \param velocityX The velocity of the gesture in x direction (pixels/second)
+ * \param velocityX The velocity of the gesture in y direction (pixels/second)
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnTouchGesturePan(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY)
+ {
+ return true;
+ }
+ /*!
+ * \brief A pan gesture with a single touch has ended
+ *
+ * \param x The x coordinate (with sub-pixel) of the current touch
+ * \param y The y coordinate (with sub-pixel) of the current touch
+ * \param offsetX The covered distance on the x axis (with sub-pixel)
+ * \param offsetX The covered distance on the y axis (with sub-pixel)
+ * \param velocityX The velocity of the gesture in x direction (pixels/second)
+ * \param velocityX The velocity of the gesture in y direction (pixels/second)
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnTouchGestureEnd(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY)
+ {
+ return true;
+ }
+
+ // convenience events
+ /*!
+ * \brief A tap with a one or more touches has been performed
+ *
+ * \param x The x coordinate (with sub-pixel) of the touch
+ * \param y The y coordinate (with sub-pixel) of the touch
+ * \param pointers The number of pointers involved (default 1)
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual void OnTap(float x, float y, int32_t pointers = 1) {}
+ /*!
+ * \brief One or more touches have been held down for a certain amount of time
+ *
+ * \param x The x coordinate (with sub-pixel) of the touch
+ * \param y The y coordinate (with sub-pixel) of the touch
+ * \param pointers The number of pointers involved (default 1)
+ *
+ * \return True if the event was handled otherwise false
+ *
+ * \sa OnSingleTouchHold
+ */
+ virtual void OnLongPress(float x, float y, int32_t pointers = 1) {}
+ /*!
+ * \brief One or more touches has been moved quickly in a single direction in a short time
+ *
+ * \param direction The direction (left, right, up, down) of the swipe gesture
+ * \param xDown The x coordinate (with sub-pixel) of the first touch
+ * \param yDown The y coordinate (with sub-pixel) of the first touch
+ * \param xUp The x coordinate (with sub-pixel) of the last touch
+ * \param yUp The y coordinate (with sub-pixel) of the last touch
+ * \param velocityX The velocity of the gesture in x direction (pixels/second)
+ * \param velocityX The velocity of the gesture in y direction (pixels/second)
+ * \param pointers The number of pointers involved (default 1)
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual void OnSwipe(TouchMoveDirection direction,
+ float xDown,
+ float yDown,
+ float xUp,
+ float yUp,
+ float velocityX,
+ float velocityY,
+ int32_t pointers = 1)
+ {
+ }
+ /*!
+ * \brief Two simultaneous touches have been held down and moved to perform a zooming/pinching
+ * gesture
+ *
+ * \param centerX The x coordinate (with sub-pixel) of the center of the two touches
+ * \param centerY The y coordinate (with sub-pixel) of the center of the two touches
+ * \param zoomFactor The zoom (> 1.0) or pinch (< 1.0) factor of the two touches
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual void OnZoomPinch(float centerX, float centerY, float zoomFactor) {}
+ /*!
+ * \brief Two simultaneous touches have been held down and moved to perform a rotating gesture
+ *
+ * \param centerX The x coordinate (with sub-pixel) of the center of the two touches
+ * \param centerY The y coordinate (with sub-pixel) of the center of the two touches
+ * \param angle The clockwise angle in degrees of the rotation
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual void OnRotate(float centerX, float centerY, float angle) {}
+};
diff --git a/xbmc/input/touch/ITouchInputHandler.h b/xbmc/input/touch/ITouchInputHandler.h
new file mode 100644
index 0000000..1275839
--- /dev/null
+++ b/xbmc/input/touch/ITouchInputHandler.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2013-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 "input/touch/ITouchInputHandling.h"
+
+#include <atomic>
+#include <stdint.h>
+
+/*!
+ * \ingroup touch
+ * \brief Touch input event
+ */
+typedef enum
+{
+ TouchInputUnchanged = 0,
+ TouchInputAbort,
+ TouchInputDown,
+ TouchInputUp,
+ TouchInputMove
+} TouchInput;
+
+/*!
+ * \ingroup touch
+ * \brief Interface (implements ITouchInputHandling) defining methods to handle
+ * raw touch input events (down, up, move).
+ *
+ * This interface should be implemented on platforms only supporting low level
+ * (raw) touch input events like touch down/up/move and with no gesture
+ * recognition logic.
+ */
+class ITouchInputHandler : public ITouchInputHandling
+{
+public:
+ ITouchInputHandler() : m_dpi(160.0f) {}
+ ~ITouchInputHandler() override = default;
+
+ /*!
+ * \brief Handle a touch event
+ *
+ * Handles the given touch event at the given location.
+ * This takes into account all the currently active pointers
+ * which need to be updated before calling this method to
+ * actually interpret and handle the changes in touch.
+ *
+ * \param event The actual touch event (abort, down, up, move)
+ * \param x The x coordinate (with sub-pixel) of the touch
+ * \param y The y coordinate (with sub-pixel) of the touch
+ * \param time The time (in nanoseconds) when this touch occurred
+ * \param pointer The number of the touch pointer which caused this event (default 0)
+ * \param size The size of the touch pointer (with sub-pixel) (default 0.0)
+ *
+ * \return True if the event was handled otherwise false.
+ *
+ * \sa Update
+ */
+ virtual bool HandleTouchInput(
+ TouchInput event, float x, float y, int64_t time, int32_t pointer = 0, float size = 0.0f) = 0;
+
+ /*!
+ * \brief Update the coordinates of a pointer
+ *
+ * Depending on how a platform handles touch input and provides the necessary events
+ * this method needs to be called at different times. If there's an event for every
+ * touch action this method does not need to be called at all. If there's only a
+ * touch event for the primary pointer (and no special events for any secondary
+ * pointers in a multi touch gesture) this method should be called for every active
+ * secondary pointer before calling Handle.
+ *
+ * \param pointer The number of the touch pointer which caused this event (default 0)
+ * \param x The x coordinate (with sub-pixel) of the touch
+ * \param y The y coordinate (with sub-pixel) of the touch
+ * \param time The time (in nanoseconds) when this touch occurred
+ * \param size The size of the touch pointer (with sub-pixel) (default 0.0)
+ *
+ * \return True if the pointer was updated otherwise false.
+ *
+ * \sa Handle
+ */
+ virtual bool UpdateTouchPointer(
+ int32_t pointer, float x, float y, int64_t time, float size = 0.0f)
+ {
+ return false;
+ }
+
+ void SetScreenDPI(float dpi)
+ {
+ if (dpi > 0.0f)
+ m_dpi = dpi;
+ }
+ float GetScreenDPI() { return m_dpi; }
+
+protected:
+ /*!
+ * \brief DPI value of the touch screen
+ */
+ std::atomic<float> m_dpi;
+};
diff --git a/xbmc/input/touch/ITouchInputHandling.cpp b/xbmc/input/touch/ITouchInputHandling.cpp
new file mode 100644
index 0000000..e799f72
--- /dev/null
+++ b/xbmc/input/touch/ITouchInputHandling.cpp
@@ -0,0 +1,159 @@
+/*
+ * 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 "ITouchInputHandling.h"
+
+void ITouchInputHandling::RegisterHandler(ITouchActionHandler* touchHandler)
+{
+ m_handler = touchHandler;
+}
+
+void ITouchInputHandling::UnregisterHandler()
+{
+ m_handler = NULL;
+}
+
+void ITouchInputHandling::OnTouchAbort()
+{
+ if (m_handler)
+ m_handler->OnTouchAbort();
+}
+
+bool ITouchInputHandling::OnSingleTouchStart(float x, float y)
+{
+ if (m_handler)
+ return m_handler->OnSingleTouchStart(x, y);
+
+ return true;
+}
+
+bool ITouchInputHandling::OnSingleTouchHold(float x, float y)
+{
+ if (m_handler)
+ return m_handler->OnSingleTouchHold(x, y);
+
+ return true;
+}
+
+bool ITouchInputHandling::OnSingleTouchMove(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY)
+{
+ if (m_handler)
+ return m_handler->OnSingleTouchMove(x, y, offsetX, offsetY, velocityX, velocityY);
+
+ return true;
+}
+
+bool ITouchInputHandling::OnSingleTouchEnd(float x, float y)
+{
+ if (m_handler)
+ return m_handler->OnSingleTouchEnd(x, y);
+
+ return true;
+}
+
+bool ITouchInputHandling::OnMultiTouchDown(float x, float y, int32_t pointer)
+{
+ if (m_handler)
+ return m_handler->OnMultiTouchDown(x, y, pointer);
+
+ return true;
+}
+
+bool ITouchInputHandling::OnMultiTouchHold(float x, float y, int32_t pointers /* = 2 */)
+{
+ if (m_handler)
+ return m_handler->OnMultiTouchHold(x, y, pointers);
+
+ return true;
+}
+
+bool ITouchInputHandling::OnMultiTouchMove(float x,
+ float y,
+ float offsetX,
+ float offsetY,
+ float velocityX,
+ float velocityY,
+ int32_t pointer)
+{
+ if (m_handler)
+ return m_handler->OnMultiTouchMove(x, y, offsetX, offsetY, velocityX, velocityY, pointer);
+
+ return true;
+}
+
+bool ITouchInputHandling::OnMultiTouchUp(float x, float y, int32_t pointer)
+{
+ if (m_handler)
+ return m_handler->OnMultiTouchUp(x, y, pointer);
+
+ return true;
+}
+
+bool ITouchInputHandling::OnTouchGestureStart(float x, float y)
+{
+ if (m_handler)
+ return m_handler->OnTouchGestureStart(x, y);
+
+ return true;
+}
+
+bool ITouchInputHandling::OnTouchGesturePan(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY)
+{
+ if (m_handler)
+ return m_handler->OnTouchGesturePan(x, y, offsetX, offsetY, velocityX, velocityY);
+
+ return true;
+}
+
+bool ITouchInputHandling::OnTouchGestureEnd(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY)
+{
+ if (m_handler)
+ return m_handler->OnTouchGestureEnd(x, y, offsetX, offsetY, velocityX, velocityY);
+
+ return true;
+}
+
+void ITouchInputHandling::OnTap(float x, float y, int32_t pointers /* = 1 */)
+{
+ if (m_handler)
+ m_handler->OnTap(x, y, pointers);
+}
+
+void ITouchInputHandling::OnLongPress(float x, float y, int32_t pointers /* = 1 */)
+{
+ if (m_handler)
+ m_handler->OnLongPress(x, y, pointers);
+}
+
+void ITouchInputHandling::OnSwipe(TouchMoveDirection direction,
+ float xDown,
+ float yDown,
+ float xUp,
+ float yUp,
+ float velocityX,
+ float velocityY,
+ int32_t pointers /* = 1 */)
+{
+ if (m_handler)
+ m_handler->OnSwipe(direction, xDown, yDown, xUp, yUp, velocityX, velocityY, pointers);
+}
+
+void ITouchInputHandling::OnZoomPinch(float centerX, float centerY, float zoomFactor)
+{
+ if (m_handler)
+ m_handler->OnZoomPinch(centerX, centerY, zoomFactor);
+}
+
+void ITouchInputHandling::OnRotate(float centerX, float centerY, float angle)
+{
+ if (m_handler)
+ m_handler->OnRotate(centerX, centerY, angle);
+}
diff --git a/xbmc/input/touch/ITouchInputHandling.h b/xbmc/input/touch/ITouchInputHandling.h
new file mode 100644
index 0000000..cabbf47
--- /dev/null
+++ b/xbmc/input/touch/ITouchInputHandling.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013-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 "input/touch/ITouchActionHandler.h"
+
+#include <stdlib.h>
+
+/*!
+ * \ingroup touch
+ * \brief Convenience interface implementing ITouchActionHandler with an
+ * implementation that forwards any ITouchActionHandler-related calls
+ * to a previously registered ITouchActionHandler
+ *
+ * \sa ITouchActionHandler
+ */
+class ITouchInputHandling : protected ITouchActionHandler
+{
+public:
+ ITouchInputHandling() : m_handler(NULL) {}
+ ~ITouchInputHandling() override = default;
+
+ /*!
+ * \brief Register a touch input handler
+ *
+ * There can only be one touch input handler.
+ *
+ * \param touchHandler An instance of a touch handler implementing the
+ * ITouchActionHandler interface
+ *
+ * \sa UnregisterHandler
+ */
+ void RegisterHandler(ITouchActionHandler* touchHandler);
+ /*!
+ * \brief Unregister the previously registered touch handler
+ *
+ * \sa RegisterHandler
+ */
+ void UnregisterHandler();
+
+protected:
+ // implementation of ITouchActionHandler
+ void OnTouchAbort() override;
+
+ bool OnSingleTouchStart(float x, float y) override;
+ bool OnSingleTouchHold(float x, float y) override;
+ bool OnSingleTouchMove(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY) override;
+ bool OnSingleTouchEnd(float x, float y) override;
+
+ bool OnMultiTouchDown(float x, float y, int32_t pointer) override;
+ bool OnMultiTouchHold(float x, float y, int32_t pointers = 2) override;
+ bool OnMultiTouchMove(float x,
+ float y,
+ float offsetX,
+ float offsetY,
+ float velocityX,
+ float velocityY,
+ int32_t pointer) override;
+ bool OnMultiTouchUp(float x, float y, int32_t pointer) override;
+
+ bool OnTouchGestureStart(float x, float y) override;
+ bool OnTouchGesturePan(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY) override;
+ bool OnTouchGestureEnd(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY) override;
+
+ // convenience events
+ void OnTap(float x, float y, int32_t pointers = 1) override;
+ void OnLongPress(float x, float y, int32_t pointers = 1) override;
+ void OnSwipe(TouchMoveDirection direction,
+ float xDown,
+ float yDown,
+ float xUp,
+ float yUp,
+ float velocityX,
+ float velocityY,
+ int32_t pointers = 1) override;
+ void OnZoomPinch(float centerX, float centerY, float zoomFactor) override;
+ void OnRotate(float centerX, float centerY, float angle) override;
+
+private:
+ ITouchActionHandler* m_handler;
+};
diff --git a/xbmc/input/touch/TouchTypes.h b/xbmc/input/touch/TouchTypes.h
new file mode 100644
index 0000000..b3450c4
--- /dev/null
+++ b/xbmc/input/touch/TouchTypes.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2013-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 "utils/Vector.h"
+
+/*!
+ * \ingroup touch
+ * \brief A class representing a touch consisting of an x and y coordinate and
+ * a time
+ */
+class Touch : public CVector
+{
+public:
+ /*!
+ * \brief Checks if the touch is valid i.e. if the x/y coordinates and the
+ * time are >= 0
+ *
+ * \return True if the touch is valid otherwise false
+ */
+ bool valid() const { return x >= 0.0f && y >= 0.0f && time >= 0; }
+
+ /*!
+ * \brief Copies the x/y coordinates and the time from the given touch
+ *
+ * \param other Touch to copy x/y coordinates and time from
+ */
+ void copy(const Touch& other)
+ {
+ x = other.x;
+ y = other.y;
+ time = other.time;
+ }
+
+ int64_t time = -1; // in nanoseconds
+};
+
+/*!
+ * \ingroup touch
+ * \brief A class representing a touch pointer interaction consisting of an
+ * down touch, the last touch and the current touch.
+ */
+class Pointer
+{
+public:
+ Pointer() { reset(); }
+
+ /*!
+ * \brief Resets the pointer and all its touches
+ */
+ void reset()
+ {
+ down = {};
+ last = {};
+ moving = false;
+ size = 0.0f;
+ }
+
+ /*!
+ * \brief Checks if the "down" touch is valid
+ *
+ * \return True if the "down" touch is valid otherwise false
+ */
+ bool valid() const { return down.valid(); }
+
+ /*!
+ * \brief Calculates the velocity in x/y direction using the "down" and
+ * either the "last" or "current" touch
+ *
+ * \param velocityX Placeholder for velocity in x direction
+ * \param velocityY Placeholder for velocity in y direction
+ * \param fromLast Whether to calculate the velocity with the "last" or
+ * the "current" touch
+ *
+ * \return True if the velocity is valid otherwise false
+ */
+ bool velocity(float& velocityX, float& velocityY, bool fromLast = true) const
+ {
+ int64_t fromTime = last.time;
+ float fromX = last.x;
+ float fromY = last.y;
+ if (!fromLast)
+ {
+ fromTime = down.time;
+ fromX = down.x;
+ fromY = down.y;
+ }
+
+ velocityX = 0.0f; // number of pixels per second
+ velocityY = 0.0f; // number of pixels per second
+
+ int64_t timeDiff = current.time - fromTime;
+ if (timeDiff <= 0)
+ return false;
+
+ velocityX = ((current.x - fromX) * 1000000000) / timeDiff;
+ velocityY = ((current.y - fromY) * 1000000000) / timeDiff;
+ return true;
+ }
+
+ Touch down;
+ Touch last;
+ Touch current;
+ bool moving;
+ float size;
+};
diff --git a/xbmc/input/touch/generic/CMakeLists.txt b/xbmc/input/touch/generic/CMakeLists.txt
new file mode 100644
index 0000000..d5e8adc
--- /dev/null
+++ b/xbmc/input/touch/generic/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES GenericTouchActionHandler.cpp
+ GenericTouchInputHandler.cpp
+ GenericTouchPinchDetector.cpp
+ GenericTouchRotateDetector.cpp
+ GenericTouchSwipeDetector.cpp)
+
+set(HEADERS GenericTouchActionHandler.h
+ GenericTouchInputHandler.h
+ GenericTouchPinchDetector.h
+ GenericTouchRotateDetector.h
+ GenericTouchSwipeDetector.h
+ IGenericTouchGestureDetector.h)
+
+core_add_library(input_touch_generic)
diff --git a/xbmc/input/touch/generic/GenericTouchActionHandler.cpp b/xbmc/input/touch/generic/GenericTouchActionHandler.cpp
new file mode 100644
index 0000000..d601074
--- /dev/null
+++ b/xbmc/input/touch/generic/GenericTouchActionHandler.cpp
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2013-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 "GenericTouchActionHandler.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/Key.h"
+
+#include <cmath>
+
+using namespace KODI::MESSAGING;
+
+CGenericTouchActionHandler& CGenericTouchActionHandler::GetInstance()
+{
+ static CGenericTouchActionHandler sTouchAction;
+ return sTouchAction;
+}
+
+void CGenericTouchActionHandler::OnTouchAbort()
+{
+ sendEvent(ACTION_GESTURE_ABORT, 0.0f, 0.0f);
+}
+
+bool CGenericTouchActionHandler::OnSingleTouchStart(float x, float y)
+{
+ focusControl(x, y);
+
+ return true;
+}
+
+bool CGenericTouchActionHandler::OnSingleTouchHold(float x, float y)
+{
+ return true;
+}
+
+bool CGenericTouchActionHandler::OnSingleTouchMove(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY)
+{
+ return true;
+}
+
+bool CGenericTouchActionHandler::OnSingleTouchEnd(float x, float y)
+{
+ return true;
+}
+
+bool CGenericTouchActionHandler::OnMultiTouchDown(float x, float y, int32_t pointer)
+{
+ return true;
+}
+
+bool CGenericTouchActionHandler::OnMultiTouchHold(float x, float y, int32_t pointers /* = 2 */)
+{
+ return true;
+}
+
+bool CGenericTouchActionHandler::OnMultiTouchMove(float x,
+ float y,
+ float offsetX,
+ float offsetY,
+ float velocityX,
+ float velocityY,
+ int32_t pointer)
+{
+ return true;
+}
+
+bool CGenericTouchActionHandler::OnMultiTouchUp(float x, float y, int32_t pointer)
+{
+ return true;
+}
+
+bool CGenericTouchActionHandler::OnTouchGestureStart(float x, float y)
+{
+ sendEvent(ACTION_GESTURE_BEGIN, x, y, 0.0f, 0.0f);
+
+ return true;
+}
+
+bool CGenericTouchActionHandler::OnTouchGesturePan(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY)
+{
+ sendEvent(ACTION_GESTURE_PAN, x, y, offsetX, offsetY, velocityX, velocityY);
+
+ return true;
+}
+
+bool CGenericTouchActionHandler::OnTouchGestureEnd(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY)
+{
+ sendEvent(ACTION_GESTURE_END, velocityX, velocityY, x, y, offsetX, offsetY);
+
+ return true;
+}
+
+void CGenericTouchActionHandler::OnTap(float x, float y, int32_t pointers /* = 1 */)
+{
+ if (pointers <= 0 || pointers > 10)
+ return;
+
+ sendEvent(ACTION_TOUCH_TAP, x, y, 0.0f, 0.0f, 0.0f, 0.0f, pointers);
+}
+
+void CGenericTouchActionHandler::OnLongPress(float x, float y, int32_t pointers /* = 1 */)
+{
+ if (pointers <= 0 || pointers > 10)
+ return;
+
+ sendEvent(ACTION_TOUCH_LONGPRESS, x, y, 0.0f, 0.0f, 0.0f, 0.0f, pointers);
+}
+
+void CGenericTouchActionHandler::OnSwipe(TouchMoveDirection direction,
+ float xDown,
+ float yDown,
+ float xUp,
+ float yUp,
+ float velocityX,
+ float velocityY,
+ int32_t pointers /* = 1 */)
+{
+ if (pointers <= 0 || pointers > 10)
+ return;
+
+ int actionId = 0;
+ if (direction == TouchMoveDirectionLeft)
+ actionId = ACTION_GESTURE_SWIPE_LEFT;
+ else if (direction == TouchMoveDirectionRight)
+ actionId = ACTION_GESTURE_SWIPE_RIGHT;
+ else if (direction == TouchMoveDirectionUp)
+ actionId = ACTION_GESTURE_SWIPE_UP;
+ else if (direction == TouchMoveDirectionDown)
+ actionId = ACTION_GESTURE_SWIPE_DOWN;
+ else
+ return;
+
+ sendEvent(actionId, xUp, yUp, velocityX, velocityY, xDown, yDown, pointers);
+}
+
+void CGenericTouchActionHandler::OnZoomPinch(float centerX, float centerY, float zoomFactor)
+{
+ sendEvent(ACTION_GESTURE_ZOOM, centerX, centerY, zoomFactor, 0.0f);
+}
+
+void CGenericTouchActionHandler::OnRotate(float centerX, float centerY, float angle)
+{
+ sendEvent(ACTION_GESTURE_ROTATE, centerX, centerY, angle, 0.0f);
+}
+
+int CGenericTouchActionHandler::QuerySupportedGestures(float x, float y)
+{
+ CGUIMessage msg(GUI_MSG_GESTURE_NOTIFY, 0, 0, static_cast<int>(std::round(x)),
+ static_cast<int>(std::round(y)));
+ if (!CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg))
+ return 0;
+
+ int result = 0;
+ if (msg.GetPointer())
+ {
+ int* p = static_cast<int*>(msg.GetPointer());
+ msg.SetPointer(nullptr);
+ result = *p;
+ delete p;
+ }
+ return result;
+}
+
+void CGenericTouchActionHandler::sendEvent(int actionId,
+ float x,
+ float y,
+ float x2 /* = 0.0f */,
+ float y2 /* = 0.0f */,
+ float x3,
+ float y3,
+ int pointers /* = 1 */)
+{
+ XBMC_Event newEvent{};
+ newEvent.type = XBMC_TOUCH;
+
+ newEvent.touch.action = actionId;
+ newEvent.touch.x = x;
+ newEvent.touch.y = y;
+ newEvent.touch.x2 = x2;
+ newEvent.touch.y2 = y2;
+ newEvent.touch.x3 = x3;
+ newEvent.touch.y3 = y3;
+ newEvent.touch.pointers = pointers;
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(newEvent);
+}
+
+void CGenericTouchActionHandler::focusControl(float x, float y)
+{
+ XBMC_Event newEvent{};
+ newEvent.type = XBMC_SETFOCUS;
+
+ newEvent.focus.x = static_cast<int>(std::round(x));
+ newEvent.focus.y = static_cast<int>(std::round(y));
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(newEvent);
+}
diff --git a/xbmc/input/touch/generic/GenericTouchActionHandler.h b/xbmc/input/touch/generic/GenericTouchActionHandler.h
new file mode 100644
index 0000000..e8697ce
--- /dev/null
+++ b/xbmc/input/touch/generic/GenericTouchActionHandler.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2013-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 "input/touch/ITouchActionHandler.h"
+
+/*!
+ * \ingroup touch_generic
+ * \brief Generic implementation of ITouchActionHandler to translate
+ * touch actions into XBMC specific and mappable actions.
+ *
+ * \sa ITouchActionHandler
+ */
+class CGenericTouchActionHandler : public ITouchActionHandler
+{
+public:
+ /*!
+ \brief Get an instance of the touch input manager
+ */
+ static CGenericTouchActionHandler& GetInstance();
+
+ // implementation of ITouchActionHandler
+ void OnTouchAbort() override;
+
+ bool OnSingleTouchStart(float x, float y) override;
+ bool OnSingleTouchHold(float x, float y) override;
+ bool OnSingleTouchMove(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY) override;
+ bool OnSingleTouchEnd(float x, float y) override;
+
+ bool OnMultiTouchDown(float x, float y, int32_t pointer) override;
+ bool OnMultiTouchHold(float x, float y, int32_t pointers = 2) override;
+ bool OnMultiTouchMove(float x,
+ float y,
+ float offsetX,
+ float offsetY,
+ float velocityX,
+ float velocityY,
+ int32_t pointer) override;
+ bool OnMultiTouchUp(float x, float y, int32_t pointer) override;
+
+ bool OnTouchGestureStart(float x, float y) override;
+ bool OnTouchGesturePan(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY) override;
+ bool OnTouchGestureEnd(
+ float x, float y, float offsetX, float offsetY, float velocityX, float velocityY) override;
+
+ // convenience events
+ void OnTap(float x, float y, int32_t pointers = 1) override;
+ void OnLongPress(float x, float y, int32_t pointers = 1) override;
+ void OnSwipe(TouchMoveDirection direction,
+ float xDown,
+ float yDown,
+ float xUp,
+ float yUp,
+ float velocityX,
+ float velocityY,
+ int32_t pointers = 1) override;
+ void OnZoomPinch(float centerX, float centerY, float zoomFactor) override;
+ void OnRotate(float centerX, float centerY, float angle) override;
+
+ /*!
+ \brief Asks the control at the given coordinates for a list of the supported gestures.
+
+ \param x The x coordinate (with sub-pixel) of the touch
+ \param y The y coordinate (with sub-pixel) of the touch
+
+ \return EVENT_RESULT value of bitwise ORed gestures.
+ */
+ int QuerySupportedGestures(float x, float y);
+
+private:
+ // private construction, and no assignments; use the provided singleton methods
+ CGenericTouchActionHandler() = default;
+ CGenericTouchActionHandler(const CGenericTouchActionHandler&) = delete;
+ CGenericTouchActionHandler const& operator=(CGenericTouchActionHandler const&) = delete;
+ ~CGenericTouchActionHandler() override = default;
+
+ void sendEvent(int actionId,
+ float x,
+ float y,
+ float x2 = 0.0f,
+ float y2 = 0.0f,
+ float x3 = 0.0f,
+ float y3 = 0.0f,
+ int pointers = 1);
+ void focusControl(float x, float y);
+};
diff --git a/xbmc/input/touch/generic/GenericTouchInputHandler.cpp b/xbmc/input/touch/generic/GenericTouchInputHandler.cpp
new file mode 100644
index 0000000..6290860
--- /dev/null
+++ b/xbmc/input/touch/generic/GenericTouchInputHandler.cpp
@@ -0,0 +1,397 @@
+/*
+ * 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 "GenericTouchInputHandler.h"
+
+#include "input/touch/generic/GenericTouchPinchDetector.h"
+#include "input/touch/generic/GenericTouchRotateDetector.h"
+#include "input/touch/generic/GenericTouchSwipeDetector.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cmath>
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+namespace
+{
+constexpr auto TOUCH_HOLD_TIMEOUT = 500ms;
+}
+
+CGenericTouchInputHandler::CGenericTouchInputHandler() : m_holdTimer(new CTimer(this))
+{
+}
+
+CGenericTouchInputHandler::~CGenericTouchInputHandler() = default;
+
+CGenericTouchInputHandler& CGenericTouchInputHandler::GetInstance()
+{
+ static CGenericTouchInputHandler sTouchInput;
+ return sTouchInput;
+}
+
+float CGenericTouchInputHandler::AdjustPointerSize(float size)
+{
+ if (size > 0.0f)
+ return size;
+ else
+ // Set a default size if touch input layer does not have anything useful,
+ // approx. 3.2mm
+ return m_dpi / 8.0f;
+}
+
+bool CGenericTouchInputHandler::HandleTouchInput(TouchInput event,
+ float x,
+ float y,
+ int64_t time,
+ int32_t pointer /* = 0 */,
+ float size /* = 0.0f */)
+{
+ if (time < 0 || pointer < 0 || pointer >= MAX_POINTERS)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ bool result = true;
+
+ m_pointers[pointer].current.x = x;
+ m_pointers[pointer].current.y = y;
+ m_pointers[pointer].current.time = time;
+
+ switch (event)
+ {
+ case TouchInputAbort:
+ {
+ triggerDetectors(event, pointer);
+
+ setGestureState(TouchGestureUnknown);
+ for (auto& pointer : m_pointers)
+ pointer.reset();
+
+ OnTouchAbort();
+ break;
+ }
+
+ case TouchInputDown:
+ {
+ m_pointers[pointer].down.x = x;
+ m_pointers[pointer].down.y = y;
+ m_pointers[pointer].down.time = time;
+ m_pointers[pointer].moving = false;
+ m_pointers[pointer].size = AdjustPointerSize(size);
+
+ // If this is the down event of the primary pointer
+ // we start by assuming that it's a single touch
+ if (pointer == 0)
+ {
+ // create new gesture detectors
+ m_detectors.emplace(new CGenericTouchSwipeDetector(this, m_dpi));
+ m_detectors.emplace(new CGenericTouchPinchDetector(this, m_dpi));
+ m_detectors.emplace(new CGenericTouchRotateDetector(this, m_dpi));
+ triggerDetectors(event, pointer);
+
+ setGestureState(TouchGestureSingleTouch);
+ result = OnSingleTouchStart(x, y);
+
+ m_holdTimer->Start(TOUCH_HOLD_TIMEOUT);
+ }
+ // Otherwise it's the down event of another pointer
+ else
+ {
+ triggerDetectors(event, pointer);
+
+ // If we so far assumed single touch or still have the primary
+ // pointer of a previous multi touch pressed down, we can update to multi touch
+ if (m_gestureState == TouchGestureSingleTouch ||
+ m_gestureState == TouchGestureSingleTouchHold ||
+ m_gestureState == TouchGestureMultiTouchDone)
+ {
+ result = OnMultiTouchDown(x, y, pointer);
+ m_holdTimer->Stop(true);
+
+ if (m_gestureState == TouchGestureSingleTouch ||
+ m_gestureState == TouchGestureSingleTouchHold)
+ m_holdTimer->Start(TOUCH_HOLD_TIMEOUT);
+
+ setGestureState(TouchGestureMultiTouchStart);
+ }
+ // Otherwise we should ignore this pointer
+ else
+ {
+ m_pointers[pointer].reset();
+ break;
+ }
+ }
+ return result;
+ }
+
+ case TouchInputUp:
+ {
+ // unexpected event => abort
+ if (!m_pointers[pointer].valid() || m_gestureState == TouchGestureUnknown)
+ break;
+
+ triggerDetectors(event, pointer);
+
+ m_holdTimer->Stop(false);
+
+ // Just a single tap with a pointer
+ if (m_gestureState == TouchGestureSingleTouch ||
+ m_gestureState == TouchGestureSingleTouchHold)
+ {
+ result = OnSingleTouchEnd(x, y);
+
+ if (m_gestureState == TouchGestureSingleTouch)
+ OnTap(x, y, 1);
+ }
+ // A pan gesture started with a single pointer (ignoring any other pointers)
+ else if (m_gestureState == TouchGesturePan)
+ {
+ float velocityX = 0.0f; // number of pixels per second
+ float velocityY = 0.0f; // number of pixels per second
+ m_pointers[pointer].velocity(velocityX, velocityY, false);
+
+ result = OnTouchGestureEnd(x, y, x - m_pointers[pointer].down.x,
+ y - m_pointers[pointer].down.y, velocityX, velocityY);
+ }
+ // we are in multi-touch
+ else
+ result = OnMultiTouchUp(x, y, pointer);
+
+ // If we were in multi touch mode and lifted one pointer
+ // we can go into the TouchGestureMultiTouchDone state which will allow
+ // the user to go back into multi touch mode without lifting the primary pointer
+ if (m_gestureState == TouchGestureMultiTouchStart ||
+ m_gestureState == TouchGestureMultiTouchHold || m_gestureState == TouchGestureMultiTouch)
+ {
+ setGestureState(TouchGestureMultiTouchDone);
+
+ // after lifting the primary pointer, the secondary pointer will
+ // become the primary pointer in the next event
+ if (pointer == 0)
+ {
+ m_pointers[0] = m_pointers[1];
+ pointer = 1;
+ }
+ }
+ // Otherwise abort
+ else
+ {
+ if (m_gestureState == TouchGestureMultiTouchDone)
+ {
+ float velocityX = 0.0f; // number of pixels per second
+ float velocityY = 0.0f; // number of pixels per second
+ m_pointers[pointer].velocity(velocityX, velocityY, false);
+
+ result = OnTouchGestureEnd(x, y, x - m_pointers[pointer].down.x,
+ y - m_pointers[pointer].down.y, velocityX, velocityY);
+
+ // if neither of the two pointers moved we have a single tap with multiple pointers
+ if (m_gestureStateOld != TouchGestureMultiTouchHold &&
+ m_gestureStateOld != TouchGestureMultiTouch)
+ OnTap(std::abs((m_pointers[0].down.x + m_pointers[1].down.x) / 2),
+ std::abs((m_pointers[0].down.y + m_pointers[1].down.y) / 2), 2);
+ }
+
+ setGestureState(TouchGestureUnknown);
+ }
+ m_pointers[pointer].reset();
+
+ return result;
+ }
+
+ case TouchInputMove:
+ {
+ // unexpected event => abort
+ if (!m_pointers[pointer].valid() || m_gestureState == TouchGestureUnknown ||
+ m_gestureState == TouchGestureMultiTouchDone)
+ break;
+
+ bool moving = std::any_of(m_pointers.cbegin(), m_pointers.cend(),
+ [](Pointer const& p) { return p.valid() && p.moving; });
+
+ if (moving)
+ {
+ m_holdTimer->Stop();
+
+ // the touch is moving so we start a gesture
+ if (m_gestureState == TouchGestureSingleTouch ||
+ m_gestureState == TouchGestureMultiTouchStart)
+ result = OnTouchGestureStart(m_pointers[pointer].down.x, m_pointers[pointer].down.y);
+ }
+
+ triggerDetectors(event, pointer);
+
+ // Check if the touch has moved far enough to count as movement
+ if ((m_gestureState == TouchGestureSingleTouch ||
+ m_gestureState == TouchGestureMultiTouchStart) &&
+ !m_pointers[pointer].moving)
+ break;
+
+ if (m_gestureState == TouchGestureSingleTouch)
+ {
+ m_pointers[pointer].last.copy(m_pointers[pointer].down);
+ setGestureState(TouchGesturePan);
+ }
+ else if (m_gestureState == TouchGestureMultiTouchStart)
+ {
+ setGestureState(TouchGestureMultiTouch);
+
+ // set the starting point
+ saveLastTouch();
+ }
+
+ float offsetX = x - m_pointers[pointer].last.x;
+ float offsetY = y - m_pointers[pointer].last.y;
+ float velocityX = 0.0f; // number of pixels per second
+ float velocityY = 0.0f; // number of pixels per second
+ m_pointers[pointer].velocity(velocityX, velocityY);
+
+ if (m_pointers[pointer].moving &&
+ (m_gestureState == TouchGestureSingleTouch ||
+ m_gestureState == TouchGestureSingleTouchHold || m_gestureState == TouchGesturePan))
+ result = OnSingleTouchMove(x, y, offsetX, offsetY, velocityX, velocityY);
+
+ // Let's see if we have a pan gesture (i.e. the primary and only pointer moving)
+ if (m_gestureState == TouchGesturePan)
+ {
+ result = OnTouchGesturePan(x, y, offsetX, offsetY, velocityX, velocityY);
+
+ m_pointers[pointer].last.x = x;
+ m_pointers[pointer].last.y = y;
+ }
+ else if (m_gestureState == TouchGestureMultiTouch)
+ {
+ if (moving)
+ result = OnMultiTouchMove(x, y, offsetX, offsetY, velocityX, velocityY, pointer);
+ }
+ else
+ break;
+
+ return result;
+ }
+
+ default:
+ CLog::Log(LOGDEBUG, "CGenericTouchInputHandler: unknown TouchInput");
+ break;
+ }
+
+ return false;
+}
+
+bool CGenericTouchInputHandler::UpdateTouchPointer(
+ int32_t pointer, float x, float y, int64_t time, float size /* = 0.0f */)
+{
+ if (pointer < 0 || pointer >= MAX_POINTERS)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ m_pointers[pointer].last.copy(m_pointers[pointer].current);
+
+ m_pointers[pointer].current.x = x;
+ m_pointers[pointer].current.y = y;
+ m_pointers[pointer].current.time = time;
+ m_pointers[pointer].size = AdjustPointerSize(size);
+
+ // calculate whether the pointer has moved at all
+ if (!m_pointers[pointer].moving)
+ {
+ CVector down = m_pointers[pointer].down;
+ CVector current = m_pointers[pointer].current;
+ CVector distance = down - current;
+
+ if (distance.length() > m_pointers[pointer].size)
+ m_pointers[pointer].moving = true;
+ }
+
+ for (auto const& detector : m_detectors)
+ detector->OnTouchUpdate(pointer, m_pointers[pointer]);
+
+ return true;
+}
+
+void CGenericTouchInputHandler::saveLastTouch()
+{
+ for (auto& pointer : m_pointers)
+ pointer.last.copy(pointer.current);
+}
+
+void CGenericTouchInputHandler::OnTimeout()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ switch (m_gestureState)
+ {
+ case TouchGestureSingleTouch:
+ setGestureState(TouchGestureSingleTouchHold);
+
+ OnSingleTouchHold(m_pointers[0].down.x, m_pointers[0].down.y);
+ OnLongPress(m_pointers[0].down.x, m_pointers[0].down.y, 1);
+ break;
+
+ case TouchGestureMultiTouchStart:
+ if (!m_pointers[0].moving && !m_pointers[1].moving)
+ {
+ setGestureState(TouchGestureMultiTouchHold);
+
+ OnMultiTouchHold(m_pointers[0].down.x, m_pointers[0].down.y);
+ OnLongPress(std::abs((m_pointers[0].down.x + m_pointers[1].down.x) / 2),
+ std::abs((m_pointers[0].down.y + m_pointers[1].down.y) / 2), 2);
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+void CGenericTouchInputHandler::triggerDetectors(TouchInput event, int32_t pointer)
+{
+ switch (event)
+ {
+ case TouchInputAbort:
+ {
+ m_detectors.clear();
+ break;
+ }
+
+ case TouchInputDown:
+ {
+ for (auto const& detector : m_detectors)
+ detector->OnTouchDown(pointer, m_pointers[pointer]);
+ break;
+ }
+
+ case TouchInputUp:
+ {
+ for (auto const& detector : m_detectors)
+ detector->OnTouchUp(pointer, m_pointers[pointer]);
+ break;
+ }
+
+ case TouchInputMove:
+ {
+ for (auto const& detector : m_detectors)
+ detector->OnTouchMove(pointer, m_pointers[pointer]);
+ break;
+ }
+
+ default:
+ return;
+ }
+
+ for (auto it = m_detectors.begin(); it != m_detectors.end();)
+ {
+ if ((*it)->IsDone())
+ it = m_detectors.erase(it);
+ else
+ it++;
+ }
+}
diff --git a/xbmc/input/touch/generic/GenericTouchInputHandler.h b/xbmc/input/touch/generic/GenericTouchInputHandler.h
new file mode 100644
index 0000000..154f5f2
--- /dev/null
+++ b/xbmc/input/touch/generic/GenericTouchInputHandler.h
@@ -0,0 +1,98 @@
+/*
+ * 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 "input/touch/ITouchInputHandler.h"
+#include "input/touch/TouchTypes.h"
+#include "threads/CriticalSection.h"
+#include "threads/Timer.h"
+
+#include <array>
+#include <memory>
+#include <set>
+
+class IGenericTouchGestureDetector;
+
+/*!
+ * \ingroup touch_generic
+ * \brief Generic implementation of ITouchInputHandler to handle low level (raw)
+ * touch events and translate them into touch actions which are passed
+ * on to the registered ITouchActionHandler implementation.
+ *
+ * The generic implementation supports single a double touch and hold
+ * actions and basic gesture recognition for panning, swiping, pinching/zooming
+ * and rotating.
+ *
+ * \sa ITouchInputHandler
+ */
+class CGenericTouchInputHandler : public ITouchInputHandler, private ITimerCallback
+{
+public:
+ /*!
+ \brief Get an instance of the touch input manager
+ */
+ static CGenericTouchInputHandler& GetInstance();
+ static constexpr int MAX_POINTERS = 2;
+
+ // implementation of ITouchInputHandler
+ bool HandleTouchInput(TouchInput event,
+ float x,
+ float y,
+ int64_t time,
+ int32_t pointer = 0,
+ float size = 0.0f) override;
+ bool UpdateTouchPointer(
+ int32_t pointer, float x, float y, int64_t time, float size = 0.0f) override;
+
+private:
+ // private construction, and no assignments; use the provided singleton methods
+ CGenericTouchInputHandler();
+ ~CGenericTouchInputHandler() override;
+ CGenericTouchInputHandler(const CGenericTouchInputHandler&) = delete;
+ CGenericTouchInputHandler const& operator=(CGenericTouchInputHandler const&) = delete;
+
+ typedef enum
+ {
+ TouchGestureUnknown = 0,
+ // only primary pointer active but stationary so far
+ TouchGestureSingleTouch,
+ // primary pointer active but stationary for a certain time
+ TouchGestureSingleTouchHold,
+ // primary pointer moving
+ TouchGesturePan,
+ // at least two pointers active but stationary so far
+ TouchGestureMultiTouchStart,
+ // at least two pointers active but stationary for a certain time
+ TouchGestureMultiTouchHold,
+ // at least two pointers active and moving
+ TouchGestureMultiTouch,
+ // all but primary pointer have been lifted
+ TouchGestureMultiTouchDone
+ } TouchGestureState;
+
+ // implementation of ITimerCallback
+ void OnTimeout() override;
+
+ void saveLastTouch();
+ void setGestureState(TouchGestureState gestureState)
+ {
+ m_gestureStateOld = m_gestureState;
+ m_gestureState = gestureState;
+ }
+ void triggerDetectors(TouchInput event, int32_t pointer);
+ float AdjustPointerSize(float size);
+
+ CCriticalSection m_critical;
+ std::unique_ptr<CTimer> m_holdTimer;
+ std::array<Pointer, MAX_POINTERS> m_pointers;
+ std::set<std::unique_ptr<IGenericTouchGestureDetector>> m_detectors;
+
+ TouchGestureState m_gestureState = TouchGestureUnknown;
+ TouchGestureState m_gestureStateOld = TouchGestureUnknown;
+};
diff --git a/xbmc/input/touch/generic/GenericTouchPinchDetector.cpp b/xbmc/input/touch/generic/GenericTouchPinchDetector.cpp
new file mode 100644
index 0000000..3cff00d
--- /dev/null
+++ b/xbmc/input/touch/generic/GenericTouchPinchDetector.cpp
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2013-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 "GenericTouchPinchDetector.h"
+
+bool CGenericTouchPinchDetector::OnTouchDown(unsigned int index, const Pointer& pointer)
+{
+ if (index >= MAX_POINTERS)
+ return false;
+
+ if (m_done)
+ return true;
+
+ m_pointers[index] = pointer;
+ return true;
+}
+
+bool CGenericTouchPinchDetector::OnTouchUp(unsigned int index, const Pointer& pointer)
+{
+ if (index >= MAX_POINTERS)
+ return false;
+
+ if (m_done)
+ return true;
+
+ // after lifting the primary pointer, the secondary pointer will
+ // become the primary pointer in the next event
+ if (index == 0)
+ {
+ m_pointers[0] = m_pointers[1];
+ index = 1;
+ }
+
+ m_pointers[index].reset();
+
+ if (!m_pointers[0].valid() && !m_pointers[1].valid())
+ m_done = true;
+
+ return true;
+}
+
+bool CGenericTouchPinchDetector::OnTouchMove(unsigned int index, const Pointer& pointer)
+{
+ if (index >= MAX_POINTERS)
+ return false;
+
+ if (m_done)
+ return true;
+
+ // update the internal pointers
+ m_pointers[index] = pointer;
+
+ const Pointer& primaryPointer = m_pointers[0];
+ const Pointer& secondaryPointer = m_pointers[1];
+
+ if (!primaryPointer.valid() || !secondaryPointer.valid() ||
+ (!primaryPointer.moving && !secondaryPointer.moving))
+ return false;
+
+ // calculate zoom/pinch
+ CVector primary = primaryPointer.down;
+ CVector secondary = secondaryPointer.down;
+ CVector diagonal = primary - secondary;
+
+ float baseDiffLength = diagonal.length();
+ if (baseDiffLength != 0.0f)
+ {
+ CVector primaryNow = primaryPointer.current;
+ CVector secondaryNow = secondaryPointer.current;
+ CVector diagonalNow = primaryNow - secondaryNow;
+ float curDiffLength = diagonalNow.length();
+
+ float centerX = (primary.x + secondary.x) / 2;
+ float centerY = (primary.y + secondary.y) / 2;
+
+ float zoom = curDiffLength / baseDiffLength;
+
+ OnZoomPinch(centerX, centerY, zoom);
+ }
+
+ return true;
+}
diff --git a/xbmc/input/touch/generic/GenericTouchPinchDetector.h b/xbmc/input/touch/generic/GenericTouchPinchDetector.h
new file mode 100644
index 0000000..191e950
--- /dev/null
+++ b/xbmc/input/touch/generic/GenericTouchPinchDetector.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013-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 "input/touch/generic/IGenericTouchGestureDetector.h"
+
+/*!
+ * \ingroup touch_generic
+ * \brief Implementation of IGenericTouchGestureDetector to detect pinch/zoom
+ * gestures with at least two active touch pointers.
+ *
+ * \sa IGenericTouchGestureDetector
+ */
+class CGenericTouchPinchDetector : public IGenericTouchGestureDetector
+{
+public:
+ CGenericTouchPinchDetector(ITouchActionHandler* handler, float dpi)
+ : IGenericTouchGestureDetector(handler, dpi)
+ {
+ }
+ ~CGenericTouchPinchDetector() override = default;
+
+ bool OnTouchDown(unsigned int index, const Pointer& pointer) override;
+ bool OnTouchUp(unsigned int index, const Pointer& pointer) override;
+ bool OnTouchMove(unsigned int index, const Pointer& pointer) override;
+};
diff --git a/xbmc/input/touch/generic/GenericTouchRotateDetector.cpp b/xbmc/input/touch/generic/GenericTouchRotateDetector.cpp
new file mode 100644
index 0000000..79db638
--- /dev/null
+++ b/xbmc/input/touch/generic/GenericTouchRotateDetector.cpp
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2013-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 "GenericTouchRotateDetector.h"
+
+#include <math.h>
+
+#ifndef M_PI
+#define M_PI 3.1415926535897932384626433832795028842
+#endif
+
+CGenericTouchRotateDetector::CGenericTouchRotateDetector(ITouchActionHandler* handler, float dpi)
+ : IGenericTouchGestureDetector(handler, dpi), m_angle(0.0f)
+{
+}
+
+bool CGenericTouchRotateDetector::OnTouchDown(unsigned int index, const Pointer& pointer)
+{
+ if (index >= MAX_POINTERS)
+ return false;
+
+ if (m_done)
+ return true;
+
+ m_pointers[index] = pointer;
+ m_angle = 0.0f;
+ return true;
+}
+
+bool CGenericTouchRotateDetector::OnTouchUp(unsigned int index, const Pointer& pointer)
+{
+ if (index >= MAX_POINTERS)
+ return false;
+
+ if (m_done)
+ return true;
+
+ // after lifting the primary pointer, the secondary pointer will
+ // become the primary pointer in the next event
+ if (index == 0)
+ {
+ m_pointers[0] = m_pointers[1];
+ index = 1;
+ }
+
+ m_pointers[index].reset();
+
+ if (!m_pointers[0].valid() && !m_pointers[1].valid())
+ m_done = true;
+
+ return true;
+}
+
+bool CGenericTouchRotateDetector::OnTouchMove(unsigned int index, const Pointer& pointer)
+{
+ if (index >= MAX_POINTERS)
+ return false;
+
+ if (m_done)
+ return true;
+
+ // update the internal pointers
+ m_pointers[index] = pointer;
+
+ Pointer& primaryPointer = m_pointers[0];
+ Pointer& secondaryPointer = m_pointers[1];
+
+ if (!primaryPointer.valid() || !secondaryPointer.valid() ||
+ (!primaryPointer.moving && !secondaryPointer.moving))
+ return false;
+
+ CVector last = primaryPointer.last - secondaryPointer.last;
+ CVector current = primaryPointer.current - secondaryPointer.current;
+
+ float length = last.length() * current.length();
+ if (length != 0.0f)
+ {
+ float centerX = (primaryPointer.current.x + secondaryPointer.current.x) / 2;
+ float centerY = (primaryPointer.current.y + secondaryPointer.current.y) / 2;
+ float scalar = last.scalar(current);
+ float angle = acos(scalar / length) * 180.0f / static_cast<float>(M_PI);
+
+ // make sure the result of acos is a valid number
+ if (angle == angle)
+ {
+ // calculate the direction of the rotation using the
+ // z-component of the cross-product of last and current
+ float direction = last.x * current.y - current.x * last.y;
+ if (direction < 0.0f)
+ m_angle -= angle;
+ else
+ m_angle += angle;
+
+ OnRotate(centerX, centerY, m_angle);
+ }
+ }
+
+ return true;
+}
+
+bool CGenericTouchRotateDetector::OnTouchUpdate(unsigned int index, const Pointer& pointer)
+{
+ if (index >= MAX_POINTERS)
+ return false;
+
+ if (m_done)
+ return true;
+
+ // update the internal pointers
+ m_pointers[index] = pointer;
+ return true;
+}
diff --git a/xbmc/input/touch/generic/GenericTouchRotateDetector.h b/xbmc/input/touch/generic/GenericTouchRotateDetector.h
new file mode 100644
index 0000000..695879a
--- /dev/null
+++ b/xbmc/input/touch/generic/GenericTouchRotateDetector.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013-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 "input/touch/generic/IGenericTouchGestureDetector.h"
+
+/*!
+ * \ingroup touch_generic
+ * \brief Implementation of IGenericTouchGestureDetector to detect rotation
+ * gestures with at least two active touch pointers.
+ *
+ * \sa IGenericTouchGestureDetector
+ */
+class CGenericTouchRotateDetector : public IGenericTouchGestureDetector
+{
+public:
+ CGenericTouchRotateDetector(ITouchActionHandler* handler, float dpi);
+ ~CGenericTouchRotateDetector() override = default;
+
+ bool OnTouchDown(unsigned int index, const Pointer& pointer) override;
+ bool OnTouchUp(unsigned int index, const Pointer& pointer) override;
+ bool OnTouchMove(unsigned int index, const Pointer& pointer) override;
+ bool OnTouchUpdate(unsigned int index, const Pointer& pointer) override;
+
+private:
+ /*!
+ * \brief Angle of the detected rotation
+ */
+ float m_angle;
+};
diff --git a/xbmc/input/touch/generic/GenericTouchSwipeDetector.cpp b/xbmc/input/touch/generic/GenericTouchSwipeDetector.cpp
new file mode 100644
index 0000000..7d772b5
--- /dev/null
+++ b/xbmc/input/touch/generic/GenericTouchSwipeDetector.cpp
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2013-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.
+ */
+
+#ifndef _USE_MATH_DEFINES
+#define _USE_MATH_DEFINES
+#endif
+#include "GenericTouchSwipeDetector.h"
+
+#include <math.h>
+#include <stdlib.h>
+
+// maximum time between touch down and up (in nanoseconds)
+#define SWIPE_MAX_TIME 500000000
+// maximum swipe distance between touch down and up (in multiples of screen DPI)
+#define SWIPE_MIN_DISTANCE 0.5f
+// original maximum variance of the touch movement
+#define SWIPE_MAX_VARIANCE 0.2f
+// tangents of the maximum angle (20 degrees) the touch movement may vary in a
+// direction perpendicular to the swipe direction (in radians)
+// => tan(20 deg) = tan(20 * M_PI / 180)
+#define SWIPE_MAX_VARIANCE_ANGLE 0.36397023f
+
+CGenericTouchSwipeDetector::CGenericTouchSwipeDetector(ITouchActionHandler* handler, float dpi)
+ : IGenericTouchGestureDetector(handler, dpi),
+ m_directions(TouchMoveDirectionLeft | TouchMoveDirectionRight | TouchMoveDirectionUp |
+ TouchMoveDirectionDown),
+ m_swipeDetected(false),
+ m_size(0)
+{
+}
+
+bool CGenericTouchSwipeDetector::OnTouchDown(unsigned int index, const Pointer& pointer)
+{
+ if (index >= MAX_POINTERS)
+ return false;
+
+ m_size += 1;
+ if (m_size > 1)
+ return true;
+
+ // reset all values
+ m_done = false;
+ m_swipeDetected = false;
+ m_directions = TouchMoveDirectionLeft | TouchMoveDirectionRight | TouchMoveDirectionUp |
+ TouchMoveDirectionDown;
+
+ return true;
+}
+
+bool CGenericTouchSwipeDetector::OnTouchUp(unsigned int index, const Pointer& pointer)
+{
+ if (index >= MAX_POINTERS)
+ return false;
+
+ m_size -= 1;
+ if (m_done)
+ return false;
+
+ m_done = true;
+
+ // check if a swipe has been detected and if it has a valid direction
+ if (!m_swipeDetected || m_directions == TouchMoveDirectionNone)
+ return false;
+
+ // check if the swipe has been performed in the proper time span
+ if ((pointer.current.time - pointer.down.time) > SWIPE_MAX_TIME)
+ return false;
+
+ // calculate the velocity of the swipe
+ float velocityX = 0.0f; // number of pixels per second
+ float velocityY = 0.0f; // number of pixels per second
+ pointer.velocity(velocityX, velocityY, false);
+
+ // call the OnSwipe() callback
+ OnSwipe((TouchMoveDirection)m_directions, pointer.down.x, pointer.down.y, pointer.current.x,
+ pointer.current.y, velocityX, velocityY, m_size + 1);
+ return true;
+}
+
+bool CGenericTouchSwipeDetector::OnTouchMove(unsigned int index, const Pointer& pointer)
+{
+ if (index >= MAX_POINTERS)
+ return false;
+
+ // only handle swipes of moved pointers
+ if (index >= m_size || m_done || !pointer.moving)
+ return false;
+
+ float deltaXmovement = pointer.current.x - pointer.last.x;
+ float deltaYmovement = pointer.current.y - pointer.last.y;
+
+ if (deltaXmovement > 0.0f)
+ m_directions &= ~TouchMoveDirectionLeft;
+ else if (deltaXmovement < 0.0f)
+ m_directions &= ~TouchMoveDirectionRight;
+
+ if (deltaYmovement > 0.0f)
+ m_directions &= ~TouchMoveDirectionUp;
+ else if (deltaYmovement < 0.0f)
+ m_directions &= ~TouchMoveDirectionDown;
+
+ if (m_directions == TouchMoveDirectionNone)
+ {
+ m_done = true;
+ return false;
+ }
+
+ float deltaXabs = fabs(pointer.current.x - pointer.down.x);
+ float deltaYabs = fabs(pointer.current.y - pointer.down.y);
+ float varXabs = deltaYabs * SWIPE_MAX_VARIANCE_ANGLE + (m_dpi * SWIPE_MAX_VARIANCE) / 2;
+ float varYabs = deltaXabs * SWIPE_MAX_VARIANCE_ANGLE + (m_dpi * SWIPE_MAX_VARIANCE) / 2;
+
+ if (m_directions & TouchMoveDirectionLeft)
+ {
+ // check if the movement went too much in Y direction
+ if (deltaYabs > varYabs)
+ m_directions &= ~TouchMoveDirectionLeft;
+ // check if the movement went far enough in the X direction
+ else if (deltaXabs > m_dpi * SWIPE_MIN_DISTANCE)
+ m_swipeDetected = true;
+ }
+
+ if (m_directions & TouchMoveDirectionRight)
+ {
+ // check if the movement went too much in Y direction
+ if (deltaYabs > varYabs)
+ m_directions &= ~TouchMoveDirectionRight;
+ // check if the movement went far enough in the X direction
+ else if (deltaXabs > m_dpi * SWIPE_MIN_DISTANCE)
+ m_swipeDetected = true;
+ }
+
+ if (m_directions & TouchMoveDirectionUp)
+ {
+ // check if the movement went too much in X direction
+ if (deltaXabs > varXabs)
+ m_directions &= ~TouchMoveDirectionUp;
+ // check if the movement went far enough in the Y direction
+ else if (deltaYabs > m_dpi * SWIPE_MIN_DISTANCE)
+ m_swipeDetected = true;
+ }
+
+ if (m_directions & TouchMoveDirectionDown)
+ {
+ // check if the movement went too much in X direction
+ if (deltaXabs > varXabs)
+ m_directions &= ~TouchMoveDirectionDown;
+ // check if the movement went far enough in the Y direction
+ else if (deltaYabs > m_dpi * SWIPE_MIN_DISTANCE)
+ m_swipeDetected = true;
+ }
+
+ if (m_directions == TouchMoveDirectionNone)
+ {
+ m_done = true;
+ return false;
+ }
+
+ return true;
+}
+
+bool CGenericTouchSwipeDetector::OnTouchUpdate(unsigned int index, const Pointer& pointer)
+{
+ if (index >= MAX_POINTERS)
+ return false;
+
+ if (m_done)
+ return true;
+
+ return OnTouchMove(index, pointer);
+}
diff --git a/xbmc/input/touch/generic/GenericTouchSwipeDetector.h b/xbmc/input/touch/generic/GenericTouchSwipeDetector.h
new file mode 100644
index 0000000..7fe88b1
--- /dev/null
+++ b/xbmc/input/touch/generic/GenericTouchSwipeDetector.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2013-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 "input/touch/generic/IGenericTouchGestureDetector.h"
+
+/*!
+ * \ingroup touch_generic
+ * \brief Implementation of IGenericTouchGestureDetector to detect swipe
+ * gestures in any direction.
+ *
+ * \sa IGenericTouchGestureDetector
+ */
+class CGenericTouchSwipeDetector : public IGenericTouchGestureDetector
+{
+public:
+ CGenericTouchSwipeDetector(ITouchActionHandler* handler, float dpi);
+ ~CGenericTouchSwipeDetector() override = default;
+
+ bool OnTouchDown(unsigned int index, const Pointer& pointer) override;
+ bool OnTouchUp(unsigned int index, const Pointer& pointer) override;
+ bool OnTouchMove(unsigned int index, const Pointer& pointer) override;
+ bool OnTouchUpdate(unsigned int index, const Pointer& pointer) override;
+
+private:
+ /*!
+ * \brief Swipe directions that are still possible to detect
+ *
+ * The directions are stored as a combination (bitwise OR) of
+ * TouchMoveDirection enum values
+ *
+ * \sa TouchMoveDirection
+ */
+ unsigned int m_directions;
+ /*!
+ * \brief Whether a swipe gesture has been detected or not
+ */
+ bool m_swipeDetected;
+ /*!
+ * \brief Number of active pointers
+ */
+ unsigned int m_size;
+};
diff --git a/xbmc/input/touch/generic/IGenericTouchGestureDetector.h b/xbmc/input/touch/generic/IGenericTouchGestureDetector.h
new file mode 100644
index 0000000..7e2cb88
--- /dev/null
+++ b/xbmc/input/touch/generic/IGenericTouchGestureDetector.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2013-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 "input/touch/ITouchInputHandling.h"
+#include "input/touch/TouchTypes.h"
+
+#include <array>
+
+/*!
+ * \ingroup touch_generic
+ * \brief Interface defining methods to perform gesture recognition
+ */
+class IGenericTouchGestureDetector : public ITouchInputHandling
+{
+public:
+ IGenericTouchGestureDetector(ITouchActionHandler* handler, float dpi) : m_done(false), m_dpi(dpi)
+ {
+ RegisterHandler(handler);
+ }
+ ~IGenericTouchGestureDetector() override = default;
+ static constexpr int MAX_POINTERS = 2;
+
+ /*!
+ * \brief Check whether the gesture recognition is finished or not
+ *
+ * \return True if the gesture recognition is finished otherwise false
+ */
+ bool IsDone() { return m_done; }
+
+ /*!
+ * \brief A new touch pointer has been recognised.
+ *
+ * \param index Index of the given touch pointer
+ * \param pointer Touch pointer that has changed
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnTouchDown(unsigned int index, const Pointer& pointer) = 0;
+ /*!
+ * \brief An active touch pointer has vanished.
+ *
+ * If the first touch pointer is lifted and there are more active touch
+ * pointers, the remaining pointers change their index.
+ *
+ * \param index Index of the given touch pointer
+ * \param pointer Touch pointer that has changed
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnTouchUp(unsigned int index, const Pointer& pointer) { return false; }
+ /*!
+ * \brief An active touch pointer has moved.
+ *
+ * \param index Index of the given touch pointer
+ * \param pointer Touch pointer that has changed
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnTouchMove(unsigned int index, const Pointer& pointer) { return false; }
+ /*!
+ * \brief An active touch pointer's values have been updated but no event has
+ * occurred.
+ *
+ * \param index Index of the given touch pointer
+ * \param pointer Touch pointer that has changed
+ *
+ * \return True if the event was handled otherwise false
+ */
+ virtual bool OnTouchUpdate(unsigned int index, const Pointer& pointer) { return false; }
+
+protected:
+ /*!
+ * \brief Whether the gesture recognition is finished or not
+ */
+ bool m_done;
+ /*!
+ * \brief DPI value of the touch screen
+ */
+ float m_dpi;
+ /*!
+ * \brief Local list of all known touch pointers
+ */
+ std::array<Pointer, MAX_POINTERS> m_pointers;
+};
diff --git a/xbmc/interfaces/AnnouncementManager.cpp b/xbmc/interfaces/AnnouncementManager.cpp
new file mode 100644
index 0000000..2f5101b
--- /dev/null
+++ b/xbmc/interfaces/AnnouncementManager.cpp
@@ -0,0 +1,342 @@
+/*
+ * 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 "AnnouncementManager.h"
+
+#include "FileItem.h"
+#include "music/MusicDatabase.h"
+#include "music/tags/MusicInfoTag.h"
+#include "playlists/PlayListTypes.h"
+#include "pvr/channels/PVRChannel.h"
+#include "threads/SingleLock.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <mutex>
+#include <stdio.h>
+
+#define LOOKUP_PROPERTY "database-lookup"
+
+using namespace ANNOUNCEMENT;
+
+const std::string CAnnouncementManager::ANNOUNCEMENT_SENDER = "xbmc";
+
+CAnnouncementManager::CAnnouncementManager() : CThread("Announce")
+{
+}
+
+CAnnouncementManager::~CAnnouncementManager()
+{
+ Deinitialize();
+}
+
+void CAnnouncementManager::Start()
+{
+ Create();
+}
+
+void CAnnouncementManager::Deinitialize()
+{
+ m_bStop = true;
+ m_queueEvent.Set();
+ StopThread();
+ std::unique_lock<CCriticalSection> lock(m_announcersCritSection);
+ m_announcers.clear();
+}
+
+void CAnnouncementManager::AddAnnouncer(IAnnouncer *listener)
+{
+ if (!listener)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_announcersCritSection);
+ m_announcers.push_back(listener);
+}
+
+void CAnnouncementManager::RemoveAnnouncer(IAnnouncer *listener)
+{
+ if (!listener)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_announcersCritSection);
+ for (unsigned int i = 0; i < m_announcers.size(); i++)
+ {
+ if (m_announcers[i] == listener)
+ {
+ m_announcers.erase(m_announcers.begin() + i);
+ return;
+ }
+ }
+}
+
+void CAnnouncementManager::Announce(AnnouncementFlag flag, const std::string& message)
+{
+ CVariant data;
+ Announce(flag, ANNOUNCEMENT_SENDER, message, CFileItemPtr(), data);
+}
+
+void CAnnouncementManager::Announce(AnnouncementFlag flag,
+ const std::string& message,
+ const CVariant& data)
+{
+ Announce(flag, ANNOUNCEMENT_SENDER, message, CFileItemPtr(), data);
+}
+
+void CAnnouncementManager::Announce(AnnouncementFlag flag,
+ const std::string& message,
+ const std::shared_ptr<const CFileItem>& item)
+{
+ CVariant data;
+ Announce(flag, ANNOUNCEMENT_SENDER, message, item, data);
+}
+
+void CAnnouncementManager::Announce(AnnouncementFlag flag,
+ const std::string& message,
+ const std::shared_ptr<const CFileItem>& item,
+ const CVariant& data)
+{
+ Announce(flag, ANNOUNCEMENT_SENDER, message, item, data);
+}
+
+
+void CAnnouncementManager::Announce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message)
+{
+ CVariant data;
+ Announce(flag, sender, message, CFileItemPtr(), data);
+}
+
+void CAnnouncementManager::Announce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ Announce(flag, sender, message, CFileItemPtr(), data);
+}
+
+void CAnnouncementManager::Announce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const std::shared_ptr<const CFileItem>& item,
+ const CVariant& data)
+{
+ CAnnounceData announcement;
+ announcement.flag = flag;
+ announcement.sender = sender;
+ announcement.message = message;
+ announcement.data = data;
+
+ if (item != nullptr)
+ announcement.item = CFileItemPtr(new CFileItem(*item));
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_queueCritSection);
+ m_announcementQueue.push_back(announcement);
+ }
+ m_queueEvent.Set();
+}
+
+void CAnnouncementManager::DoAnnounce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ CLog::Log(LOGDEBUG, LOGANNOUNCE, "CAnnouncementManager - Announcement: {} from {}", message, sender);
+
+ std::unique_lock<CCriticalSection> lock(m_announcersCritSection);
+
+ // Make a copy of announcers. They may be removed or even remove themselves during execution of IAnnouncer::Announce()!
+
+ std::vector<IAnnouncer *> announcers(m_announcers);
+ for (unsigned int i = 0; i < announcers.size(); i++)
+ announcers[i]->Announce(flag, sender, message, data);
+}
+
+void CAnnouncementManager::DoAnnounce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const std::shared_ptr<CFileItem>& item,
+ const CVariant& data)
+{
+ if (item == nullptr)
+ {
+ DoAnnounce(flag, sender, message, data);
+ return;
+ }
+
+ // Extract db id of item
+ CVariant object = data.isNull() || data.isObject() ? data : CVariant::VariantTypeObject;
+ std::string type;
+ int id = 0;
+
+ if(item->HasPVRChannelInfoTag())
+ {
+ const std::shared_ptr<PVR::CPVRChannel> channel(item->GetPVRChannelInfoTag());
+ id = channel->ChannelID();
+ type = "channel";
+
+ object["item"]["title"] = channel->ChannelName();
+ object["item"]["channeltype"] = channel->IsRadio() ? "radio" : "tv";
+
+ if (data.isMember("player") && data["player"].isMember("playerid"))
+ {
+ object["player"]["playerid"] =
+ channel->IsRadio() ? PLAYLIST::TYPE_MUSIC : PLAYLIST::TYPE_VIDEO;
+ }
+ }
+ else if (item->HasVideoInfoTag() && !item->HasPVRRecordingInfoTag())
+ {
+ id = item->GetVideoInfoTag()->m_iDbId;
+
+ //! @todo Can be removed once this is properly handled when starting playback of a file
+ if (id <= 0 && !item->GetPath().empty() &&
+ (!item->HasProperty(LOOKUP_PROPERTY) || item->GetProperty(LOOKUP_PROPERTY).asBoolean()))
+ {
+ CVideoDatabase videodatabase;
+ if (videodatabase.Open())
+ {
+ std::string path = item->GetPath();
+ std::string videoInfoTagPath(item->GetVideoInfoTag()->m_strFileNameAndPath);
+ if (StringUtils::StartsWith(videoInfoTagPath, "removable://"))
+ path = videoInfoTagPath;
+ if (videodatabase.LoadVideoInfo(path, *item->GetVideoInfoTag(), VideoDbDetailsNone))
+ id = item->GetVideoInfoTag()->m_iDbId;
+
+ videodatabase.Close();
+ }
+ }
+
+ if (!item->GetVideoInfoTag()->m_type.empty())
+ type = item->GetVideoInfoTag()->m_type;
+ else
+ CVideoDatabase::VideoContentTypeToString(item->GetVideoContentType(), type);
+
+ if (id <= 0)
+ {
+ //! @todo Can be removed once this is properly handled when starting playback of a file
+ item->SetProperty(LOOKUP_PROPERTY, false);
+
+ std::string title = item->GetVideoInfoTag()->m_strTitle;
+ if (title.empty())
+ title = item->GetLabel();
+ object["item"]["title"] = title;
+
+ switch (item->GetVideoContentType())
+ {
+ case VideoDbContentType::MOVIES:
+ if (item->GetVideoInfoTag()->HasYear())
+ object["item"]["year"] = item->GetVideoInfoTag()->GetYear();
+ break;
+ case VideoDbContentType::EPISODES:
+ if (item->GetVideoInfoTag()->m_iEpisode >= 0)
+ object["item"]["episode"] = item->GetVideoInfoTag()->m_iEpisode;
+ if (item->GetVideoInfoTag()->m_iSeason >= 0)
+ object["item"]["season"] = item->GetVideoInfoTag()->m_iSeason;
+ if (!item->GetVideoInfoTag()->m_strShowTitle.empty())
+ object["item"]["showtitle"] = item->GetVideoInfoTag()->m_strShowTitle;
+ break;
+ case VideoDbContentType::MUSICVIDEOS:
+ if (!item->GetVideoInfoTag()->m_strAlbum.empty())
+ object["item"]["album"] = item->GetVideoInfoTag()->m_strAlbum;
+ if (!item->GetVideoInfoTag()->m_artist.empty())
+ object["item"]["artist"] = StringUtils::Join(item->GetVideoInfoTag()->m_artist, " / ");
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ else if (item->HasMusicInfoTag())
+ {
+ id = item->GetMusicInfoTag()->GetDatabaseId();
+ type = MediaTypeSong;
+
+ //! @todo Can be removed once this is properly handled when starting playback of a file
+ if (id <= 0 && !item->GetPath().empty() &&
+ (!item->HasProperty(LOOKUP_PROPERTY) || item->GetProperty(LOOKUP_PROPERTY).asBoolean()))
+ {
+ CMusicDatabase musicdatabase;
+ if (musicdatabase.Open())
+ {
+ CSong song;
+ if (musicdatabase.GetSongByFileName(item->GetPath(), song, item->GetStartOffset()))
+ {
+ item->GetMusicInfoTag()->SetSong(song);
+ id = item->GetMusicInfoTag()->GetDatabaseId();
+ }
+
+ musicdatabase.Close();
+ }
+ }
+
+ if (id <= 0)
+ {
+ //! @todo Can be removed once this is properly handled when starting playback of a file
+ item->SetProperty(LOOKUP_PROPERTY, false);
+
+ std::string title = item->GetMusicInfoTag()->GetTitle();
+ if (title.empty())
+ title = item->GetLabel();
+ object["item"]["title"] = title;
+
+ if (item->GetMusicInfoTag()->GetTrackNumber() > 0)
+ object["item"]["track"] = item->GetMusicInfoTag()->GetTrackNumber();
+ if (!item->GetMusicInfoTag()->GetAlbum().empty())
+ object["item"]["album"] = item->GetMusicInfoTag()->GetAlbum();
+ if (!item->GetMusicInfoTag()->GetArtist().empty())
+ object["item"]["artist"] = item->GetMusicInfoTag()->GetArtist();
+ }
+ }
+ else if (item->IsVideo())
+ {
+ // video item but has no video info tag.
+ type = "movie";
+ object["item"]["title"] = item->GetLabel();
+ }
+ else if (item->HasPictureInfoTag())
+ {
+ type = "picture";
+ object["item"]["file"] = item->GetPath();
+ }
+ else
+ type = "unknown";
+
+ object["item"]["type"] = type;
+ if (id > 0)
+ object["item"]["id"] = id;
+
+ DoAnnounce(flag, sender, message, object);
+}
+
+void CAnnouncementManager::Process()
+{
+ SetPriority(ThreadPriority::LOWEST);
+
+ while (!m_bStop)
+ {
+ std::unique_lock<CCriticalSection> lock(m_queueCritSection);
+ if (!m_announcementQueue.empty())
+ {
+ auto announcement = m_announcementQueue.front();
+ m_announcementQueue.pop_front();
+ {
+ CSingleExit ex(m_queueCritSection);
+ DoAnnounce(announcement.flag, announcement.sender, announcement.message, announcement.item,
+ announcement.data);
+ }
+ }
+ else
+ {
+ CSingleExit ex(m_queueCritSection);
+ m_queueEvent.Wait();
+ }
+ }
+}
diff --git a/xbmc/interfaces/AnnouncementManager.h b/xbmc/interfaces/AnnouncementManager.h
new file mode 100644
index 0000000..44b6d97
--- /dev/null
+++ b/xbmc/interfaces/AnnouncementManager.h
@@ -0,0 +1,95 @@
+/*
+ * 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 "IAnnouncer.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "utils/Variant.h"
+
+#include <list>
+#include <memory>
+#include <vector>
+
+class CFileItem;
+class CVariant;
+
+namespace ANNOUNCEMENT
+{
+ class CAnnouncementManager : public CThread
+ {
+ public:
+ CAnnouncementManager();
+ ~CAnnouncementManager() override;
+
+ void Start();
+ void Deinitialize();
+
+ void AddAnnouncer(IAnnouncer *listener);
+ void RemoveAnnouncer(IAnnouncer *listener);
+
+ void Announce(AnnouncementFlag flag, const std::string& message);
+ void Announce(AnnouncementFlag flag, const std::string& message, const CVariant& data);
+ void Announce(AnnouncementFlag flag,
+ const std::string& message,
+ const std::shared_ptr<const CFileItem>& item);
+ void Announce(AnnouncementFlag flag,
+ const std::string& message,
+ const std::shared_ptr<const CFileItem>& item,
+ const CVariant& data);
+
+ void Announce(AnnouncementFlag flag, const std::string& sender, const std::string& message);
+ void Announce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data);
+ void Announce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const std::shared_ptr<const CFileItem>& item,
+ const CVariant& data);
+
+ // The sender is not related to the application name.
+ // Also it's part of Kodi's API - changing it will break
+ // a big number of python addons and third party json consumers.
+ static const std::string ANNOUNCEMENT_SENDER;
+
+ protected:
+ void Process() override;
+ void DoAnnounce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const std::shared_ptr<CFileItem>& item,
+ const CVariant& data);
+ void DoAnnounce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data);
+
+ struct CAnnounceData
+ {
+ AnnouncementFlag flag;
+ std::string sender;
+ std::string message;
+ std::shared_ptr<CFileItem> item;
+ CVariant data;
+ };
+ std::list<CAnnounceData> m_announcementQueue;
+ CEvent m_queueEvent;
+
+ private:
+ CAnnouncementManager(const CAnnouncementManager&) = delete;
+ CAnnouncementManager const& operator=(CAnnouncementManager const&) = delete;
+
+ CCriticalSection m_announcersCritSection;
+ CCriticalSection m_queueCritSection;
+ std::vector<IAnnouncer *> m_announcers;
+ };
+}
diff --git a/xbmc/interfaces/CMakeLists.txt b/xbmc/interfaces/CMakeLists.txt
new file mode 100644
index 0000000..0b98732
--- /dev/null
+++ b/xbmc/interfaces/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES AnnouncementManager.cpp)
+
+set(HEADERS AnnouncementManager.h
+ IActionListener.h
+ IAnnouncer.h)
+
+core_add_library(interfaces)
diff --git a/xbmc/interfaces/IActionListener.h b/xbmc/interfaces/IActionListener.h
new file mode 100644
index 0000000..0419034
--- /dev/null
+++ b/xbmc/interfaces/IActionListener.h
@@ -0,0 +1,19 @@
+/*
+ * 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
+
+class CAction;
+
+class IActionListener
+{
+public:
+ virtual ~IActionListener() = default;
+
+ virtual bool OnAction(const CAction &action) = 0;
+};
diff --git a/xbmc/interfaces/IAnnouncer.h b/xbmc/interfaces/IAnnouncer.h
new file mode 100644
index 0000000..7c20203
--- /dev/null
+++ b/xbmc/interfaces/IAnnouncer.h
@@ -0,0 +1,80 @@
+/*
+ * 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 <string>
+
+class CVariant;
+namespace ANNOUNCEMENT
+{
+ enum AnnouncementFlag
+ {
+ Player = 0x001,
+ Playlist = 0x002,
+ GUI = 0x004,
+ System = 0x008,
+ VideoLibrary = 0x010,
+ AudioLibrary = 0x020,
+ Application = 0x040,
+ Input = 0x080,
+ PVR = 0x100,
+ Other = 0x200,
+ Info = 0x400
+ };
+
+ const auto ANNOUNCE_ALL = (Player | Playlist | GUI | System | VideoLibrary | AudioLibrary | Application | Input | ANNOUNCEMENT::PVR | Other);
+
+ /*!
+ \brief Returns a string representation for the
+ given AnnouncementFlag
+ \param notification Specific AnnouncementFlag
+ \return String representation of the given AnnouncementFlag
+ */
+ inline const char *AnnouncementFlagToString(const AnnouncementFlag &notification)
+ {
+ switch (notification)
+ {
+ case Player:
+ return "Player";
+ case Playlist:
+ return "Playlist";
+ case GUI:
+ return "GUI";
+ case System:
+ return "System";
+ case VideoLibrary:
+ return "VideoLibrary";
+ case AudioLibrary:
+ return "AudioLibrary";
+ case Application:
+ return "Application";
+ case Input:
+ return "Input";
+ case PVR:
+ return "PVR";
+ case Other:
+ return "Other";
+ case Info:
+ return "Info";
+ default:
+ return "Unknown";
+ }
+ }
+
+ class IAnnouncer
+ {
+ public:
+ IAnnouncer() = default;
+ virtual ~IAnnouncer() = default;
+ virtual void Announce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) = 0;
+ };
+}
diff --git a/xbmc/interfaces/builtins/AddonBuiltins.cpp b/xbmc/interfaces/builtins/AddonBuiltins.cpp
new file mode 100644
index 0000000..056128d
--- /dev/null
+++ b/xbmc/interfaces/builtins/AddonBuiltins.cpp
@@ -0,0 +1,522 @@
+/*
+ * 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 "AddonBuiltins.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/PluginSource.h"
+#include "addons/RepositoryUpdater.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "application/Application.h"
+#include "filesystem/PluginDirectory.h"
+#include "games/tags/GameInfoTag.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "playlists/PlayListTypes.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+
+#if defined(TARGET_DARWIN)
+#include "filesystem/SpecialProtocol.h"
+#if defined(TARGET_DARWIN_OSX)
+#include "platform/darwin/osx/CocoaInterface.h"
+#endif
+#endif
+
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+/*! \brief Install an addon.
+ * \param params The parameters.
+ * \details params[0] = add-on id.
+ */
+static int InstallAddon(const std::vector<std::string>& params)
+{
+ const std::string& addonid = params[0];
+
+ AddonPtr addon;
+ CAddonInstaller::GetInstance().InstallModal(addonid, addon, InstallModalPrompt::CHOICE_YES);
+
+ return 0;
+}
+
+/*! \brief Enable an addon.
+ * \param params The parameters.
+ * \details params[0] = add-on id.
+ */
+static int EnableAddon(const std::vector<std::string>& params)
+{
+ const std::string& addonid = params[0];
+
+ if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
+ return -1;
+
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(addonid, addon, OnlyEnabled::CHOICE_NO))
+ return -1;
+
+ auto response = HELPERS::ShowYesNoDialogLines(CVariant{24076}, CVariant{24135}, CVariant{addon->Name()}, CVariant{24136});
+ if (response == DialogResponse::CHOICE_YES)
+ CServiceBroker::GetAddonMgr().EnableAddon(addonid);
+
+ return 0;
+}
+
+/*! \brief Run a plugin.
+ * \param params The parameters.
+ * \details params[0] = plugin:// URL to script.
+ */
+static int RunPlugin(const std::vector<std::string>& params)
+{
+ if (params.size())
+ {
+ CFileItem item(params[0]);
+ if (!item.m_bIsFolder)
+ {
+ item.SetPath(params[0]);
+ XFILE::CPluginDirectory::RunScriptWithParams(item.GetPath(), false);
+ }
+ }
+ else
+ CLog::Log(LOGERROR, "RunPlugin called with no arguments.");
+
+ return 0;
+}
+
+/*! \brief Run a script, plugin or game add-on.
+ * \param params The parameters.
+ * \details params[0] = add-on id.
+ * params[1] is blank for no add-on parameters
+ * or
+ * params[1] = add-on parameters in url format
+ * or
+ * params[1,...] = additional parameters in format param=value.
+ */
+static int RunAddon(const std::vector<std::string>& params)
+{
+ if (params.size())
+ {
+ const std::string& addonid = params[0];
+
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(addonid, addon, AddonType::PLUGIN,
+ OnlyEnabled::CHOICE_YES))
+ {
+ const auto plugin = std::dynamic_pointer_cast<CPluginSource>(addon);
+ std::string urlParameters;
+ std::vector<std::string> parameters;
+ if (params.size() == 2 &&
+ (StringUtils::StartsWith(params[1], "/") || StringUtils::StartsWith(params[1], "?")))
+ urlParameters = params[1];
+ else if (params.size() > 1)
+ {
+ parameters.insert(parameters.begin(), params.begin() + 1, params.end());
+ urlParameters = "?" + StringUtils::Join(parameters, "&");
+ }
+ else
+ {
+ // Add '/' if addon is run without params (will be removed later so it's safe)
+ // Otherwise there are 2 entries for the same plugin in ViewModesX.db
+ urlParameters = "/";
+ }
+
+ std::string cmd;
+ if (plugin->Provides(CPluginSource::VIDEO))
+ cmd = StringUtils::Format("ActivateWindow(Videos,plugin://{}{},return)", addonid,
+ urlParameters);
+ else if (plugin->Provides(CPluginSource::AUDIO))
+ cmd = StringUtils::Format("ActivateWindow(Music,plugin://{}{},return)", addonid,
+ urlParameters);
+ else if (plugin->Provides(CPluginSource::EXECUTABLE))
+ cmd = StringUtils::Format("ActivateWindow(Programs,plugin://{}{},return)", addonid,
+ urlParameters);
+ else if (plugin->Provides(CPluginSource::IMAGE))
+ cmd = StringUtils::Format("ActivateWindow(Pictures,plugin://{}{},return)", addonid,
+ urlParameters);
+ else if (plugin->Provides(CPluginSource::GAME))
+ cmd = StringUtils::Format("ActivateWindow(Games,plugin://{}{},return)", addonid,
+ urlParameters);
+ else
+ // Pass the script name (addonid) and all the parameters
+ // (params[1] ... params[x]) separated by a comma to RunPlugin
+ cmd = StringUtils::Format("RunPlugin({})", StringUtils::Join(params, ","));
+ CBuiltins::GetInstance().Execute(cmd);
+ }
+ else if (CServiceBroker::GetAddonMgr().GetAddon(addonid, addon, AddonType::SCRIPT,
+ OnlyEnabled::CHOICE_YES) ||
+ CServiceBroker::GetAddonMgr().GetAddon(addonid, addon, AddonType::SCRIPT_WEATHER,
+ OnlyEnabled::CHOICE_YES) ||
+ CServiceBroker::GetAddonMgr().GetAddon(addonid, addon, AddonType::SCRIPT_LYRICS,
+ OnlyEnabled::CHOICE_YES) ||
+ CServiceBroker::GetAddonMgr().GetAddon(addonid, addon, AddonType::SCRIPT_LIBRARY,
+ OnlyEnabled::CHOICE_YES))
+ {
+ // Pass the script name (addonid) and all the parameters
+ // (params[1] ... params[x]) separated by a comma to RunScript
+ CBuiltins::GetInstance().Execute(
+ StringUtils::Format("RunScript({})", StringUtils::Join(params, ",")));
+ }
+ else if (CServiceBroker::GetAddonMgr().GetAddon(addonid, addon, AddonType::GAMEDLL,
+ OnlyEnabled::CHOICE_YES))
+ {
+ CFileItem item;
+
+ if (params.size() >= 2)
+ {
+ item = CFileItem(params[1], false);
+ item.GetGameInfoTag()->SetGameClient(addonid);
+ }
+ else
+ item = CFileItem(addon);
+
+ if (!g_application.PlayMedia(item, "", PLAYLIST::TYPE_NONE))
+ {
+ CLog::Log(LOGERROR, "RunAddon could not start {}", addonid);
+ return false;
+ }
+ }
+ else
+ CLog::Log(
+ LOGERROR,
+ "RunAddon: unknown add-on id '{}', or unexpected add-on type (not a script or plugin).",
+ addonid);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "RunAddon called with no arguments.");
+ }
+
+ return 0;
+}
+
+/*! \brief Run a script add-on or an apple script.
+ * \param params The parameters.
+ * \details params[0] is the URL to the apple script
+ * or
+ * params[0] is the addon-ID to the script add-on
+ * or
+ * params[0] is the URL to the python script.
+ *
+ * Set the OnlyApple template parameter to true to only attempt
+ * execution of applescripts.
+ */
+template<bool OnlyApple=false>
+static int RunScript(const std::vector<std::string>& params)
+{
+#if defined(TARGET_DARWIN_OSX)
+ std::string execute;
+ std::string parameter = params.size() ? params[0] : "";
+ if (URIUtils::HasExtension(parameter, ".applescript|.scpt"))
+ {
+ std::string osxPath = CSpecialProtocol::TranslatePath(parameter);
+ Cocoa_DoAppleScriptFile(osxPath.c_str());
+ }
+ else if (OnlyApple)
+ return 1;
+ else
+#endif
+ {
+ AddonPtr addon;
+ std::string scriptpath;
+ // Test to see if the param is an addon ID
+ if (CServiceBroker::GetAddonMgr().GetAddon(params[0], addon, OnlyEnabled::CHOICE_YES))
+ {
+ //Get the correct extension point to run
+ if (CServiceBroker::GetAddonMgr().GetAddon(params[0], addon, AddonType::SCRIPT,
+ OnlyEnabled::CHOICE_YES) ||
+ CServiceBroker::GetAddonMgr().GetAddon(params[0], addon, AddonType::SCRIPT_WEATHER,
+ OnlyEnabled::CHOICE_YES) ||
+ CServiceBroker::GetAddonMgr().GetAddon(params[0], addon, AddonType::SCRIPT_LYRICS,
+ OnlyEnabled::CHOICE_YES) ||
+ CServiceBroker::GetAddonMgr().GetAddon(params[0], addon, AddonType::SCRIPT_LIBRARY,
+ OnlyEnabled::CHOICE_YES))
+ {
+ scriptpath = addon->LibPath();
+ }
+ else
+ {
+ // Run a random extension point (old behaviour).
+ if (CServiceBroker::GetAddonMgr().GetAddon(params[0], addon, OnlyEnabled::CHOICE_YES))
+ {
+ scriptpath = addon->LibPath();
+ CLog::Log(LOGWARNING,
+ "RunScript called for a non-script addon '{}'. This behaviour is deprecated.",
+ params[0]);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{} - Could not get addon: {}", __FUNCTION__, params[0]);
+ }
+ }
+ }
+ else
+ scriptpath = params[0];
+
+ // split the path up to find the filename
+ std::vector<std::string> argv = params;
+ std::string filename = URIUtils::GetFileName(scriptpath);
+ if (!filename.empty())
+ argv[0] = filename;
+
+ CScriptInvocationManager::GetInstance().ExecuteAsync(scriptpath, addon, argv);
+ }
+
+ return 0;
+}
+
+/*! \brief Open the settings for the default add-on of a given type.
+ * \param params The parameters.
+ * \details params[0] = The add-on type.
+ */
+static int OpenDefaultSettings(const std::vector<std::string>& params)
+{
+ AddonPtr addon;
+ AddonType type = CAddonInfo::TranslateType(params[0]);
+ if (CAddonSystemSettings::GetInstance().GetActive(type, addon))
+ {
+ bool changed = CGUIDialogAddonSettings::ShowForAddon(addon);
+ if (type == AddonType::VISUALIZATION && changed)
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_VISUALISATION_RELOAD, 0, 0);
+ }
+
+ return 0;
+}
+
+/*! \brief Set the default add-on for a given type.
+ * \param params The parameters.
+ * \details params[0] = The add-on type
+ */
+static int SetDefaultAddon(const std::vector<std::string>& params)
+{
+ std::string addonID;
+ AddonType type = CAddonInfo::TranslateType(params[0]);
+ bool allowNone = false;
+ if (type == AddonType::VISUALIZATION)
+ allowNone = true;
+
+ if (type != AddonType::UNKNOWN && CGUIWindowAddonBrowser::SelectAddonID(type, addonID, allowNone))
+ {
+ CAddonSystemSettings::GetInstance().SetActive(type, addonID);
+ if (type == AddonType::VISUALIZATION)
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_VISUALISATION_RELOAD, 0, 0);
+ }
+
+ return 0;
+}
+
+/*! \brief Open the settings for a given add-on.
+ * \param params The parameters.
+ * \details params[0] = The add-on ID.
+ */
+static int AddonSettings(const std::vector<std::string>& params)
+{
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(params[0], addon, OnlyEnabled::CHOICE_YES))
+ CGUIDialogAddonSettings::ShowForAddon(addon);
+
+ return 0;
+}
+
+/*! \brief Open the settings for a given add-on.
+* \param params The parameters.
+*/
+static int InstallFromZip(const std::vector<std::string>& params)
+{
+ CGUIWindowAddonBrowser::InstallFromZip();
+ return 0;
+}
+
+/*! \brief Stop a running script.
+ * \param params The parameters.
+ * \details params[0] = The add-on ID of the script to stop
+ * or
+ * params[0] = The URL of the script to stop.
+ */
+static int StopScript(const std::vector<std::string>& params)
+{
+ //! @todo FIXME: This does not work for addons with multiple extension points!
+ //! Are there any use for this? TODO: Fix hack in CScreenSaver::Destroy() and deprecate.
+ std::string scriptpath(params[0]);
+ // Test to see if the param is an addon ID
+ AddonPtr script;
+ if (CServiceBroker::GetAddonMgr().GetAddon(params[0], script, OnlyEnabled::CHOICE_YES))
+ scriptpath = script->LibPath();
+ CScriptInvocationManager::GetInstance().Stop(scriptpath);
+
+ return 0;
+}
+
+/*! \brief Check add-on repositories for updates.
+ * \param params (ignored)
+ */
+static int UpdateRepos(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetRepositoryUpdater().CheckForUpdates();
+
+ return 0;
+}
+
+/*! \brief Check local add-on directories for updates.
+ * \param params (ignored)
+ */
+static int UpdateLocals(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAddonMgr().FindAddons();
+
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions List of built-in functions
+/// \section built_in_functions_1 Add-on built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`Addon.Default.OpenSettings(extensionpoint)`</b>
+/// ,
+/// Open a settings dialog for the default addon of the given type
+/// (extensionpoint)
+/// @param[in] extensionpoint The add-on type
+/// }
+/// \table_row2_l{
+/// <b>`Addon.Default.Set(extensionpoint)`</b>
+/// ,
+/// Open a select dialog to allow choosing the default addon of the given type
+/// (extensionpoint)
+/// @param[in] extensionpoint The add-on type
+/// }
+/// \table_row2_l{
+/// <b>`Addon.OpenSettings(id)`</b>
+/// ,
+/// Open a settings dialog for the addon of the given id
+/// @param[in] id The add-on ID
+/// }
+/// \table_row2_l{
+/// <b>`EnableAddon(id)`</b>
+/// \anchor Builtin_EnableAddonId,
+/// Enable the specified plugin/script
+/// @param[in] id The add-on id
+/// <p><hr>
+/// @skinning_v19 **[New builtin]** \link Builtin_EnableAddonId `EnableAddon(id)`\endlink
+/// <p>
+/// }
+/// \table_row2_l{
+/// <b>`InstallAddon(id)`</b>
+/// ,
+/// Install the specified plugin/script
+/// @param[in] id The add-on id
+/// }
+/// \table_row2_l{
+/// <b>`InstallFromZip`</b>
+/// ,
+/// Opens the "Install from zip" dialog if "Unknown sources" is enabled. Prompts the warning message if not.
+/// }
+/// \table_row2_l{
+/// <b>`RunAddon(id[\,opt])`</b>
+/// ,
+/// Runs the specified plugin/script
+/// @param[in] id The add-on id.
+/// @param[in] opt is blank for no add-on parameters\n
+/// or
+/// @param[in] opt Add-on parameters in url format\n
+/// or
+/// @param[in] opt[\,...] Additional parameters in format param=value.
+/// }
+/// \table_row2_l{
+/// <b>`RunAppleScript(script[\,args]*)`</b>
+/// ,
+/// Run the specified AppleScript command
+/// @param[in] script Is the URL to the apple script\n
+/// or
+/// @param[in] script Is the addon-ID to the script add-on\n
+/// or
+/// @param[in] script Is the URL to the python script.
+///
+/// @note Set the OnlyApple template parameter to true to only attempt
+/// execution of applescripts.
+/// }
+/// \table_row2_l{
+/// <b>`RunPlugin(plugin)`</b>
+/// ,
+/// Runs the plugin. Full path must be specified. Does not work for folder
+/// plugins
+/// @param[in] plugin plugin:// URL to script.
+/// }
+/// \table_row2_l{
+/// <b>`RunScript(script[\,args]*)`</b>
+/// ,
+/// Runs the python script. You must specify the full path to the script. If
+/// the script is an add-on\, you can also execute it using its add-on id. As
+/// of 2007/02/24\, all extra parameters are passed to the script as arguments
+/// and can be accessed by python using sys.argv
+/// @param[in] script Is the addon-ID to the script add-on\n
+/// or
+/// @param[in] script Is the URL to the python script.
+/// }
+/// \table_row2_l{
+/// <b>`StopScript(id)`</b>
+/// ,
+/// Stop the script by ID or path\, if running
+/// @param[in] id The add-on ID of the script to stop\n
+/// or
+/// @param[in] id The URL of the script to stop.
+/// }
+/// \table_row2_l{
+/// <b>`UpdateAddonRepos`</b>
+/// ,
+/// Triggers a forced update of enabled add-on repositories.
+/// }
+/// \table_row2_l{
+/// <b>`UpdateLocalAddons`</b>
+/// ,
+/// Triggers a scan of local add-on directories.
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CAddonBuiltins::GetOperations() const
+{
+ return {
+ {"addon.default.opensettings", {"Open a settings dialog for the default addon of the given type", 1, OpenDefaultSettings}},
+ {"addon.default.set", {"Open a select dialog to allow choosing the default addon of the given type", 1, SetDefaultAddon}},
+ {"addon.opensettings", {"Open a settings dialog for the addon of the given id", 1, AddonSettings}},
+ {"enableaddon", {"Enables the specified plugin/script", 1, EnableAddon}},
+ {"installaddon", {"Install the specified plugin/script", 1, InstallAddon}},
+ {"installfromzip", { "Open the install from zip dialog", 0, InstallFromZip}},
+ {"runaddon", {"Run the specified plugin/script", 1, RunAddon}},
+#ifdef TARGET_DARWIN
+ {"runapplescript", {"Run the specified AppleScript command", 1, RunScript<true>}},
+#endif
+ {"runplugin", {"Run the specified plugin", 1, RunPlugin}},
+ {"runscript", {"Run the specified script", 1, RunScript}},
+ {"stopscript", {"Stop the script by ID or path, if running", 1, StopScript}},
+ {"updateaddonrepos", {"Check add-on repositories for updates", 0, UpdateRepos}},
+ {"updatelocaladdons", {"Check for local add-on changes", 0, UpdateLocals}}
+ };
+}
diff --git a/xbmc/interfaces/builtins/AddonBuiltins.h b/xbmc/interfaces/builtins/AddonBuiltins.h
new file mode 100644
index 0000000..9418420
--- /dev/null
+++ b/xbmc/interfaces/builtins/AddonBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing add-on related built-in commands.
+class CAddonBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/AndroidBuiltins.cpp b/xbmc/interfaces/builtins/AndroidBuiltins.cpp
new file mode 100644
index 0000000..17f4076
--- /dev/null
+++ b/xbmc/interfaces/builtins/AndroidBuiltins.cpp
@@ -0,0 +1,73 @@
+/*
+ * 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 "AndroidBuiltins.h"
+
+#include "ServiceBroker.h"
+#include "messaging/ApplicationMessenger.h"
+
+/*! \brief Launch an android system activity.
+ * \param params The parameters.
+ * \details params[0] = package
+ * params[1] = intent (optional)
+ * params[2] = datatype (optional)
+ * params[3] = dataURI (optional)
+ * params[4] = flags (optional)
+ * params[5] = extras (optional)
+ * params[6] = action (optional)
+ * params[7] = category (optional)
+ * params[8] = className (optional)
+ */
+static int LaunchAndroidActivity(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_START_ANDROID_ACTIVITY, -1, -1, nullptr, "",
+ params);
+
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_2 Android built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`StartAndroidActivity(package\,[intent\,dataType\,dataURI\,flags\,extras\,action\,category\,className])`</b>
+/// ,
+/// Launch an Android native app with the given package name. Optional parms
+/// (in order): intent\, dataType\, dataURI\, flags\, extras\, action\,
+/// category\, className.
+/// @param[in] package
+/// @param[in] intent (optional)
+/// @param[in] datatype (optional)
+/// @param[in] dataURI (optional)
+/// @param[in] flags (optional)
+/// @param[in] extras (optional)
+/// @param[in] action (optional)
+/// @param[in] category (optional)
+/// @param[in] className (optional)
+/// <p><hr>
+/// @skinning_v20 Added parameters `flags`\,`extras`\,`action`\,`category`\,`className`.
+/// <p>
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CAndroidBuiltins::GetOperations() const
+{
+ return {{"startandroidactivity",
+ {"Launch an Android native app with the given package name. Optional parms (in order): "
+ "intent, dataType, dataURI, flags, extras, action, category, className.",
+ 1, LaunchAndroidActivity}}};
+}
diff --git a/xbmc/interfaces/builtins/AndroidBuiltins.h b/xbmc/interfaces/builtins/AndroidBuiltins.h
new file mode 100644
index 0000000..d4059e2
--- /dev/null
+++ b/xbmc/interfaces/builtins/AndroidBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing CEC related built-in commands.
+class CAndroidBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/ApplicationBuiltins.cpp b/xbmc/interfaces/builtins/ApplicationBuiltins.cpp
new file mode 100644
index 0000000..dfec66d
--- /dev/null
+++ b/xbmc/interfaces/builtins/ApplicationBuiltins.cpp
@@ -0,0 +1,222 @@
+/*
+ * 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 "ApplicationBuiltins.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "filesystem/ZipManager.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <stdlib.h>
+
+/*! \brief Extract an archive.
+ * \param params The parameters
+ * \details params[0] = The archive URL.
+ * params[1] = Destination path (optional).
+ * If not given, extracts to folder with archive.
+ */
+static int Extract(const std::vector<std::string>& params)
+{
+ // Detects if file is zip or rar then extracts
+ std::string strDestDirect;
+ if (params.size() < 2)
+ strDestDirect = URIUtils::GetDirectory(params[0]);
+ else
+ strDestDirect = params[1];
+
+ URIUtils::AddSlashAtEnd(strDestDirect);
+
+ if (URIUtils::IsZIP(params[0]))
+ g_ZipManager.ExtractArchive(params[0],strDestDirect);
+ else
+ CLog::Log(LOGERROR, "Extract, No archive given");
+
+ return 0;
+}
+
+/*! \brief Mute volume.
+ * \param params (ignored)
+ */
+static int Mute(const std::vector<std::string>& params)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->ToggleMute();
+
+ return 0;
+}
+
+/*! \brief Notify all listeners on announcement bus.
+ * \param params The parameters.
+ * \details params[0] = sender.
+ * params[1] = data.
+ * params[2] = JSON with extra parameters (optional).
+ */
+static int NotifyAll(const std::vector<std::string>& params)
+{
+ CVariant data;
+ if (params.size() > 2)
+ {
+ if (!CJSONVariantParser::Parse(params[2], data))
+ {
+ CLog::Log(LOGERROR, "NotifyAll failed to parse data: {}", params[2]);
+ return -3;
+ }
+ }
+
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Other, params[0].c_str(), params[1].c_str(), data);
+
+ return 0;
+}
+
+/*! \brief Set volume.
+ * \param params the parameters.
+ * \details params[0] = Volume level.
+ * params[1] = "showVolumeBar" to show volume bar (optional).
+ */
+static int SetVolume(const std::vector<std::string>& params)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ float oldVolume = appVolume->GetVolumePercent();
+ float volume = static_cast<float>(strtod(params[0].c_str(), nullptr));
+
+ appVolume->SetVolume(volume);
+ if (oldVolume != volume)
+ {
+ if (params.size() > 1 && StringUtils::EqualsNoCase(params[1], "showVolumeBar"))
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_VOLUME_SHOW, oldVolume < volume ? ACTION_VOLUME_UP : ACTION_VOLUME_DOWN);
+ }
+ }
+
+ return 0;
+}
+
+/*! \brief Toggle debug info.
+ * \param params (ignored)
+ */
+static int ToggleDebug(const std::vector<std::string>& params)
+{
+ bool debug = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_DEBUG_SHOWLOGINFO);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetBool(CSettings::SETTING_DEBUG_SHOWLOGINFO, !debug);
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->SetDebugMode(!debug);
+
+ return 0;
+}
+
+/*! \brief Toggle DPMS state.
+ * \param params (ignored)
+ */
+static int ToggleDPMS(const std::vector<std::string>& params)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ToggleDPMS(true);
+
+ return 0;
+}
+
+/*! \brief Send a WOL packet to a given host.
+ * \param params The parameters.
+ * \details params[0] = The MAC of the host to wake.
+ */
+static int WakeOnLAN(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetNetwork().WakeOnLan(params[0].c_str());
+
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_3 Application built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`Extract(url [\, dest])`</b>
+/// ,
+/// Extracts a specified archive to an optionally specified 'absolute' path.
+/// @param[in] url The archive URL.
+/// @param[in] dest Destination path (optional).
+/// @note If not given\, extracts to folder with archive.
+/// }
+/// \table_row2_l{
+/// <b>`Mute`</b>
+/// ,
+/// Mutes (or unmutes) the volume.
+/// }
+/// \table_row2_l{
+/// <b>`NotifyAll(sender\, data [\, json])`</b>
+/// ,
+/// Notify all connected clients
+/// @param[in] sender Sender.
+/// @param[in] data Data.
+/// @param[in] json JSON with extra parameters (optional).
+/// }
+/// \table_row2_l{
+/// <b>`SetVolume(percent[\,showvolumebar])`</b>
+/// ,
+/// Sets the volume to the percentage specified. Optionally\, show the Volume
+/// Dialog in Kodi when setting the volume.
+/// @param[in] percent Volume level.
+/// @param[in] showvolumebar Add "showVolumeBar" to show volume bar (optional).
+/// }
+/// \table_row2_l{
+/// <b>`ToggleDebug`</b>
+/// ,
+/// Toggles debug mode on/off
+/// }
+/// \table_row2_l{
+/// <b>`ToggleDPMS`</b>
+/// ,
+/// Toggle DPMS mode manually
+/// }
+/// \table_row2_l{
+/// <b>`WakeOnLan(mac)`</b>
+/// ,
+/// Sends the wake-up packet to the broadcast address for the specified MAC
+/// address (Format: FF:FF:FF:FF:FF:FF or FF-FF-FF-FF-FF-FF).
+/// @param[in] mac The MAC of the host to wake.
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CApplicationBuiltins::GetOperations() const
+{
+ return {
+ {"extract", {"Extracts the specified archive", 1, Extract}},
+ {"mute", {"Mute the player", 0, Mute}},
+ {"notifyall", {"Notify all connected clients", 2, NotifyAll}},
+ {"setvolume", {"Set the current volume", 1, SetVolume}},
+ {"toggledebug", {"Enables/disables debug mode", 0, ToggleDebug}},
+ {"toggledpms", {"Toggle DPMS mode manually", 0, ToggleDPMS}},
+ {"wakeonlan", {"Sends the wake-up packet to the broadcast address for the specified MAC address", 1, WakeOnLAN}}
+ };
+}
diff --git a/xbmc/interfaces/builtins/ApplicationBuiltins.h b/xbmc/interfaces/builtins/ApplicationBuiltins.h
new file mode 100644
index 0000000..f237fe4
--- /dev/null
+++ b/xbmc/interfaces/builtins/ApplicationBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing application related built-in commands.
+class CApplicationBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/Builtins.cpp b/xbmc/interfaces/builtins/Builtins.cpp
new file mode 100644
index 0000000..624d9d1
--- /dev/null
+++ b/xbmc/interfaces/builtins/Builtins.cpp
@@ -0,0 +1,168 @@
+/*
+ * 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 "Builtins.h"
+
+#include "AddonBuiltins.h"
+#include "ApplicationBuiltins.h"
+#include "CECBuiltins.h"
+#include "GUIBuiltins.h"
+#include "GUIContainerBuiltins.h"
+#include "GUIControlBuiltins.h"
+#include "LibraryBuiltins.h"
+#include "OpticalBuiltins.h"
+#include "PVRBuiltins.h"
+#include "PictureBuiltins.h"
+#include "PlayerBuiltins.h"
+#include "ProfileBuiltins.h"
+#include "ServiceBroker.h"
+#include "SkinBuiltins.h"
+#include "SystemBuiltins.h"
+#include "WeatherBuiltins.h"
+#include "input/InputManager.h"
+#include "powermanagement/PowerTypes.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/ExecString.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#if defined(TARGET_ANDROID)
+#include "AndroidBuiltins.h"
+#endif
+
+#if defined(TARGET_POSIX)
+#include "PlatformDefs.h"
+#endif
+
+CBuiltins::CBuiltins()
+{
+ RegisterCommands<CAddonBuiltins>();
+ RegisterCommands<CApplicationBuiltins>();
+ RegisterCommands<CGUIBuiltins>();
+ RegisterCommands<CGUIContainerBuiltins>();
+ RegisterCommands<CGUIControlBuiltins>();
+ RegisterCommands<CLibraryBuiltins>();
+ RegisterCommands<COpticalBuiltins>();
+ RegisterCommands<CPictureBuiltins>();
+ RegisterCommands<CPlayerBuiltins>();
+ RegisterCommands<CProfileBuiltins>();
+ RegisterCommands<CPVRBuiltins>();
+ RegisterCommands<CSkinBuiltins>();
+ RegisterCommands<CSystemBuiltins>();
+ RegisterCommands<CWeatherBuiltins>();
+
+#if defined(HAVE_LIBCEC)
+ RegisterCommands<CCECBuiltins>();
+#endif
+
+#if defined(TARGET_ANDROID)
+ RegisterCommands<CAndroidBuiltins>();
+#endif
+}
+
+CBuiltins::~CBuiltins() = default;
+
+CBuiltins& CBuiltins::GetInstance()
+{
+ static CBuiltins sBuiltins;
+ return sBuiltins;
+}
+
+bool CBuiltins::HasCommand(const std::string& execString)
+{
+ const CExecString exec(execString);
+ if (!exec.IsValid())
+ return false;
+
+ const std::string function = exec.GetFunction();
+ const std::vector<std::string> parameters = exec.GetParams();
+
+ if (CServiceBroker::GetInputManager().HasBuiltin(function))
+ return true;
+
+ const auto& it = m_command.find(function);
+ if (it != m_command.end())
+ {
+ if (it->second.parameters == 0 || it->second.parameters <= parameters.size())
+ return true;
+ }
+
+ return false;
+}
+
+bool CBuiltins::IsSystemPowerdownCommand(const std::string& execString)
+{
+ const CExecString exec(execString);
+ if (!exec.IsValid())
+ return false;
+
+ const std::string execute = exec.GetFunction();
+
+ // Check if action is resulting in system powerdown.
+ if (execute == "reboot" ||
+ execute == "restart" ||
+ execute == "reset" ||
+ execute == "powerdown" ||
+ execute == "hibernate" ||
+ execute == "suspend" )
+ {
+ return true;
+ }
+ else if (execute == "shutdown")
+ {
+ switch (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNSTATE))
+ {
+ case POWERSTATE_SHUTDOWN:
+ case POWERSTATE_SUSPEND:
+ case POWERSTATE_HIBERNATE:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+ return false;
+}
+
+void CBuiltins::GetHelp(std::string &help)
+{
+ help.clear();
+
+ for (const auto& it : m_command)
+ {
+ help += it.first;
+ help += "\t";
+ help += it.second.description;
+ help += "\n";
+ }
+}
+
+int CBuiltins::Execute(const std::string& execString)
+{
+ const CExecString exec(execString);
+ if (!exec.IsValid())
+ return -1;
+
+ const std::string execute = exec.GetFunction();
+ const std::vector<std::string> params = exec.GetParams();
+
+ const auto& it = m_command.find(execute);
+ if (it != m_command.end())
+ {
+ if (it->second.parameters == 0 || params.size() >= it->second.parameters)
+ return it->second.Execute(params);
+ else
+ {
+ CLog::Log(LOGERROR, "{0} called with invalid number of parameters (should be: {1}, is {2})",
+ execute, it->second.parameters, params.size());
+ return -1;
+ }
+ }
+ else
+ return CServiceBroker::GetInputManager().ExecuteBuiltin(execute, params);
+}
diff --git a/xbmc/interfaces/builtins/Builtins.h b/xbmc/interfaces/builtins/Builtins.h
new file mode 100644
index 0000000..7d0345a
--- /dev/null
+++ b/xbmc/interfaces/builtins/Builtins.h
@@ -0,0 +1,55 @@
+/*
+ * 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 <map>
+#include <string>
+#include <vector>
+
+class CBuiltins
+{
+public:
+ //! \brief Struct representing a command from handler classes.
+ struct BUILT_IN
+ {
+ std::string description; //!< Description of command (help string)
+ size_t parameters; //!< Number of required parameters (can be 0)
+ int (*Execute)(const std::vector<std::string>& params); //!< Function to handle command
+ };
+
+ //! \brief A map of commands
+ typedef std::map<std::string,CBuiltins::BUILT_IN> CommandMap;
+
+ static CBuiltins& GetInstance();
+
+ bool HasCommand(const std::string& execString);
+ bool IsSystemPowerdownCommand(const std::string& execString);
+ void GetHelp(std::string &help);
+ int Execute(const std::string& execString);
+
+protected:
+ CBuiltins();
+ CBuiltins(const CBuiltins&) = delete;
+ const CBuiltins& operator=(const CBuiltins&) = delete;
+ virtual ~CBuiltins();
+
+private:
+ CommandMap m_command; //!< Map of registered commands
+
+
+ //! \brief Convenience template used to register commands from providers
+ template<class T>
+ void RegisterCommands()
+ {
+ T t;
+ CommandMap map = t.GetOperations();
+ m_command.insert(map.begin(), map.end());
+ }
+};
+
diff --git a/xbmc/interfaces/builtins/CECBuiltins.cpp b/xbmc/interfaces/builtins/CECBuiltins.cpp
new file mode 100644
index 0000000..d381816
--- /dev/null
+++ b/xbmc/interfaces/builtins/CECBuiltins.cpp
@@ -0,0 +1,82 @@
+/*
+ * 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 "CECBuiltins.h"
+
+#include "ServiceBroker.h"
+#include "messaging/ApplicationMessenger.h"
+
+/*! \brief Wake up device through CEC.
+ * \param params (ignored)
+ */
+static int ActivateSource(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_CECACTIVATESOURCE);
+
+ return 0;
+}
+
+/*! \brief Put device in standby through CEC.
+ * \param params (ignored)
+ */
+static int Standby(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_CECSTANDBY);
+
+ return 0;
+}
+
+/*! \brief Toggle device state through CEC.
+ * \param params (ignored)
+ */
+static int ToggleState(const std::vector<std::string>& params)
+{
+ bool result;
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_CECTOGGLESTATE, 0, 0,
+ static_cast<void*>(&result));
+
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_4 CEC built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`CECActivateSource`</b>
+/// ,
+/// Wake up playing device via a CEC peripheral
+/// }
+/// \table_row2_l{
+/// <b>`CECStandby`</b>
+/// ,
+/// Put playing device on standby via a CEC peripheral
+/// }
+/// \table_row2_l{
+/// <b>`CECToggleState`</b>
+/// ,
+/// Toggle state of playing device via a CEC peripheral
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CCECBuiltins::GetOperations() const
+{
+ return {
+ {"cectogglestate", {"Toggle state of playing device via a CEC peripheral", 0, ToggleState}},
+ {"cecactivatesource", {"Wake up playing device via a CEC peripheral", 0, ActivateSource}},
+ {"cecstandby", {"Put playing device on standby via a CEC peripheral", 0, Standby}}
+ };
+}
diff --git a/xbmc/interfaces/builtins/CECBuiltins.h b/xbmc/interfaces/builtins/CECBuiltins.h
new file mode 100644
index 0000000..d06c291
--- /dev/null
+++ b/xbmc/interfaces/builtins/CECBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing CEC related built-in commands.
+class CCECBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/CMakeLists.txt b/xbmc/interfaces/builtins/CMakeLists.txt
new file mode 100644
index 0000000..27785af
--- /dev/null
+++ b/xbmc/interfaces/builtins/CMakeLists.txt
@@ -0,0 +1,41 @@
+set(SOURCES AddonBuiltins.cpp
+ ApplicationBuiltins.cpp
+ Builtins.cpp
+ CECBuiltins.cpp
+ GUIBuiltins.cpp
+ GUIControlBuiltins.cpp
+ GUIContainerBuiltins.cpp
+ LibraryBuiltins.cpp
+ OpticalBuiltins.cpp
+ PictureBuiltins.cpp
+ PlayerBuiltins.cpp
+ ProfileBuiltins.cpp
+ PVRBuiltins.cpp
+ SkinBuiltins.cpp
+ SystemBuiltins.cpp
+ WeatherBuiltins.cpp)
+
+set(HEADERS AddonBuiltins.h
+ AndroidBuiltins.h
+ ApplicationBuiltins.h
+ Builtins.h
+ CECBuiltins.h
+ GUIBuiltins.h
+ GUIContainerBuiltins.h
+ GUIControlBuiltins.h
+ LibraryBuiltins.h
+ OpticalBuiltins.h
+ PictureBuiltins.h
+ PlayerBuiltins.h
+ ProfileBuiltins.h
+ PVRBuiltins.h
+ SkinBuiltins.h
+ SystemBuiltins.h
+ WeatherBuiltins.h)
+
+if(CORE_SYSTEM_NAME STREQUAL android)
+ list(APPEND SOURCES AndroidBuiltins.cpp)
+ list(APPEND HEADERS AndroidBuiltins.h)
+endif()
+
+core_add_library(interfaces_builtins)
diff --git a/xbmc/interfaces/builtins/GUIBuiltins.cpp b/xbmc/interfaces/builtins/GUIBuiltins.cpp
new file mode 100644
index 0000000..f368e69
--- /dev/null
+++ b/xbmc/interfaces/builtins/GUIBuiltins.cpp
@@ -0,0 +1,601 @@
+/*
+ * 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 "GUIBuiltins.h"
+
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/StereoscopicsManager.h"
+#include "input/WindowTranslator.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "input/actions/ActionTranslator.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/AlarmClock.h"
+#include "utils/RssManager.h"
+#include "utils/Screenshot.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "windows/GUIMediaWindow.h"
+
+/*! \brief Execute a GUI action.
+ * \param params The parameters.
+ * \details params[0] = Action to execute.
+ * params[1] = Window to send action to (optional).
+ */
+static int Action(const std::vector<std::string>& params)
+{
+ // try translating the action from our ButtonTranslator
+ unsigned int actionID;
+ if (CActionTranslator::TranslateString(params[0], actionID))
+ {
+ int windowID = params.size() == 2 ? CWindowTranslator::TranslateWindow(params[1]) : WINDOW_INVALID;
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, windowID, -1,
+ static_cast<void*>(new CAction(actionID)));
+ }
+
+ return 0;
+}
+
+/*! \brief Activate a window.
+ * \param params The parameters.
+ * \details params[0] = The window name.
+ * params[1] = Window starting folder (optional).
+ *
+ * Set the Replace template parameter to true to replace current
+ * window in history.
+ */
+ template<bool Replace>
+static int ActivateWindow(const std::vector<std::string>& params2)
+{
+ std::vector<std::string> params(params2);
+ // get the parameters
+ std::string strWindow;
+ if (params.size())
+ {
+ strWindow = params[0];
+ params.erase(params.begin());
+ }
+
+ // confirm the window destination is valid prior to switching
+ int iWindow = CWindowTranslator::TranslateWindow(strWindow);
+ if (iWindow != WINDOW_INVALID)
+ {
+ // compare the given directory param with the current active directory
+ // if no directory is given, and you switch from a video window to another
+ // we retain history, so it makes sense to not switch to the same window in
+ // that case
+ bool bIsSameStartFolder = true;
+ if (!params.empty())
+ {
+ CGUIWindow *activeWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ if (activeWindow && activeWindow->IsMediaWindow())
+ bIsSameStartFolder = static_cast<CGUIMediaWindow*>(activeWindow)->IsSameStartFolder(params[0]);
+ }
+
+ // activate window only if window and path differ from the current active window
+ if (iWindow != CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() || !bIsSameStartFolder)
+ {
+ // if the window doesn't change, make sure it knows it's gonna be replaced
+ // this ensures setting the start directory if we switch paths
+ // if we change windows, that's done anyway
+ if (Replace && !params.empty() &&
+ iWindow == CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow())
+ params.emplace_back("replace");
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->WakeUpScreenSaverAndDPMS();
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(iWindow, params, Replace);
+ return 0;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Activate/ReplaceWindow called with invalid destination window: {}",
+ strWindow);
+ return false;
+ }
+
+ return 1;
+}
+
+/*! \brief Activate a window and give given controls focus.
+ * \param params The parameters.
+ * \details params[0] = The window name.
+ * params[1,...] = Pair of (container ID, focus item).
+ *
+ * Set the Replace template parameter to true to replace current
+ * window in history.
+ */
+ template<bool Replace>
+static int ActivateAndFocus(const std::vector<std::string>& params)
+{
+ std::string strWindow = params[0];
+
+ // confirm the window destination is valid prior to switching
+ int iWindow = CWindowTranslator::TranslateWindow(strWindow);
+ if (iWindow != WINDOW_INVALID)
+ {
+ if (iWindow != CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow())
+ {
+ // disable the screensaver
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->WakeUpScreenSaverAndDPMS();
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(iWindow, {}, Replace);
+
+ unsigned int iPtr = 1;
+ while (params.size() > iPtr + 1)
+ {
+ CGUIMessage msg(GUI_MSG_SETFOCUS, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog(),
+ atol(params[iPtr].c_str()),
+ (params.size() >= iPtr + 2) ? atol(params[iPtr + 1].c_str())+1 : 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ iPtr += 2;
+ }
+ return 0;
+ }
+
+ }
+ else
+ CLog::Log(LOGERROR, "Replace/ActivateWindowAndFocus called with invalid destination window: {}",
+ strWindow);
+
+ return 1;
+}
+
+/*! \brief Start an alarm clock
+ * \param params The parameters.
+ * \details param[0] = name
+ * param[1] = command
+ * param[2] = Length in seconds (optional).
+ * param[3] = "silent" to suppress notifications.
+ * param[3] = "loop" to loop the alarm.
+ */
+static int AlarmClock(const std::vector<std::string>& params)
+{
+ // format is alarmclock(name,command[,time,true,false]);
+ float seconds = 0;
+ if (params.size() > 2)
+ {
+ if (params[2].find(':') == std::string::npos)
+ seconds = static_cast<float>(atoi(params[2].c_str())*60);
+ else
+ seconds = (float)StringUtils::TimeStringToSeconds(params[2]);
+ }
+ else
+ { // check if shutdown is specified in particular, and get the time for it
+ std::string strHeading;
+ if (StringUtils::EqualsNoCase(params[0], "shutdowntimer"))
+ strHeading = g_localizeStrings.Get(20145);
+ else
+ strHeading = g_localizeStrings.Get(13209);
+ std::string strTime;
+ if( CGUIDialogNumeric::ShowAndGetNumber(strTime, strHeading) )
+ seconds = static_cast<float>(atoi(strTime.c_str())*60);
+ else
+ return false;
+ }
+ bool silent = false;
+ bool loop = false;
+ for (unsigned int i = 3; i < params.size() ; i++)
+ {
+ // check "true" for backward comp
+ if (StringUtils::EqualsNoCase(params[i], "true") || StringUtils::EqualsNoCase(params[i], "silent"))
+ silent = true;
+ else if (StringUtils::EqualsNoCase(params[i], "loop"))
+ loop = true;
+ }
+
+ if( g_alarmClock.IsRunning() )
+ g_alarmClock.Stop(params[0],silent);
+ // no negative times not allowed, loop must have a positive time
+ if (seconds < 0 || (seconds == 0 && loop))
+ return false;
+ g_alarmClock.Start(params[0], seconds, params[1], silent, loop);
+
+ return 0;
+}
+
+/*! \brief Cancel an alarm clock.
+ * \param params The parameters.
+ * \details params[0] = "true" to silently cancel alarm (optional).
+ */
+static int CancelAlarm(const std::vector<std::string>& params)
+{
+ bool silent = (params.size() > 1 &&
+ (StringUtils::EqualsNoCase(params[1], "true") ||
+ StringUtils::EqualsNoCase(params[1], "silent")));
+ g_alarmClock.Stop(params[0],silent);
+
+ return 0;
+}
+
+/*! \brief Clear a property in a window.
+ * \param params The parameters.
+ * \details params[0] = The property to clear.
+ * params[1] = The window to clear property in (optional).
+ */
+static int ClearProperty(const std::vector<std::string>& params)
+{
+ CGUIWindow *window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(params.size() > 1 ? CWindowTranslator::TranslateWindow(params[1]) : CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog());
+ if (window)
+ window->SetProperty(params[0],"");
+
+ return 0;
+}
+
+/*! \brief Close a dialog.
+ * \param params The parameters.
+ * \details params[0] = "all" to close all dialogs, or dialog name.
+ * params[1] = "true" to force close (skip animations) (optional).
+ */
+static int CloseDialog(const std::vector<std::string>& params)
+{
+ bool bForce = false;
+ if (params.size() > 1 && StringUtils::EqualsNoCase(params[1], "true"))
+ bForce = true;
+ if (StringUtils::EqualsNoCase(params[0], "all"))
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().CloseDialogs(bForce);
+ }
+ else
+ {
+ int id = CWindowTranslator::TranslateWindow(params[0]);
+ CGUIWindow *window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(id);
+ if (window && window->IsDialog())
+ static_cast<CGUIDialog*>(window)->Close(bForce);
+ }
+
+ return 0;
+}
+
+/*! \brief Send a notification.
+ * \param params The parameters.
+ * \details params[0] = Notification title.
+ * params[1] = Notification text.
+ * params[2] = Display time in milliseconds (optional).
+ * params[3] = Notification icon (optional).
+ */
+static int Notification(const std::vector<std::string>& params)
+{
+ if (params.size() < 2)
+ return -1;
+ if (params.size() == 4)
+ CGUIDialogKaiToast::QueueNotification(params[3],params[0],params[1],atoi(params[2].c_str()));
+ else if (params.size() == 3)
+ CGUIDialogKaiToast::QueueNotification("",params[0],params[1],atoi(params[2].c_str()));
+ else
+ CGUIDialogKaiToast::QueueNotification(params[0],params[1]);
+
+ return 0;
+}
+
+/*! \brief Refresh RSS feed.
+ * \param params (ignored)
+ */
+static int RefreshRSS(const std::vector<std::string>& params)
+{
+ CRssManager::GetInstance().Reload();
+
+ return 0;
+}
+
+/*! \brief Take a screenshot.
+ * \param params The parameters.
+ * \details params[0] = URL to save file to. Blank to use default.
+ * params[1] = "sync" to run synchronously (optional).
+ */
+static int Screenshot(const std::vector<std::string>& params)
+{
+ if (!params.empty())
+ {
+ // get the parameters
+ std::string strSaveToPath = params[0];
+ bool sync = false;
+ if (params.size() >= 2)
+ sync = StringUtils::EqualsNoCase(params[1], "sync");
+
+ if (!strSaveToPath.empty())
+ {
+ if (XFILE::CDirectory::Exists(strSaveToPath))
+ {
+ std::string file = CUtil::GetNextFilename(
+ URIUtils::AddFileToFolder(strSaveToPath, "screenshot{:05}.png"), 65535);
+
+ if (!file.empty())
+ {
+ CScreenShot::TakeScreenshot(file, sync);
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Too many screen shots or invalid folder {}", strSaveToPath);
+ }
+ }
+ else
+ CScreenShot::TakeScreenshot(strSaveToPath, sync);
+ }
+ }
+ else
+ CScreenShot::TakeScreenshot();
+
+ return 0;
+}
+
+/*! \brief Set GUI language.
+ * \param params The parameters.
+ * \details params[0] = The language to use.
+ */
+static int SetLanguage(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SETLANGUAGE, -1, -1, nullptr, params[0]);
+
+ return 0;
+}
+
+/*! \brief Set a property in a window.
+ * \param params The parameters.
+ * \details params[0] = The property to set.
+ * params[1] = The property value.
+ * params[2] = The window to set property in (optional).
+ */
+static int SetProperty(const std::vector<std::string>& params)
+{
+ CGUIWindow *window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(params.size() > 2 ? CWindowTranslator::TranslateWindow(params[2]) : CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog());
+ if (window)
+ window->SetProperty(params[0],params[1]);
+
+ return 0;
+}
+
+/*! \brief Set GUI stereo mode.
+ * \param params The parameters.
+ * \details param[0] = Stereo mode identifier.
+ */
+static int SetStereoMode(const std::vector<std::string>& params)
+{
+ CAction action = CStereoscopicsManager::ConvertActionCommandToAction("SetStereoMode", params[0]);
+ if (action.GetID() != ACTION_NONE)
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(action)));
+ else
+ {
+ CLog::Log(LOGERROR, "Builtin 'SetStereoMode' called with unknown parameter: {}", params[0]);
+ return -2;
+ }
+
+ return 0;
+}
+
+/*! \brief Toggle visualization of dirty regions.
+ * \param params Ignored.
+ */
+static int ToggleDirty(const std::vector<std::string>&)
+{
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->ToggleDirtyRegionVisualization();
+
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_5 GUI built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`Action(action[\,window])`</b>
+/// ,
+/// Executes an action (same as in keymap) for the given window or the
+/// active window if the parameter window is omitted. The parameter window
+/// can either be the window's id\, or in the case of a standard window\, the
+/// window's name. See here for a list of window names\, and their respective
+/// ids.
+/// @param[in] action Action to execute.
+/// @param[in] window Window to send action to (optional).
+/// }
+/// \table_row2_l{
+/// <b>`CancelAlarm(name[\,silent])`</b>
+/// ,
+/// Cancel a running alarm. Set silent to true to hide the alarm notification.
+/// @param[in] silent Send "true" or "silent" to silently cancel alarm (optional).
+/// }
+/// \table_row2_l{
+/// <b>`AlarmClock(name\,command[\,time\,silent\,loop])`</b>
+/// ,
+/// Pops up a dialog asking for the length of time for the alarm (unless the
+/// parameter time is specified)\, and starts a timer. When the timer runs out\,
+/// it'll execute the built-in command (the parameter command) if it is
+/// specified\, otherwise it'll pop up an alarm notice. Add silent to hide the
+/// alarm notification. Add loop for the alarm to execute the command each
+/// time the specified time interval expires.
+/// @note if using any of the last optional parameters (silent or loop)\, both must
+/// be provided for any to take effect.
+/// <p>
+/// <b>Example:</b>
+/// The following example will create an alarmclock named `mytimer` which will silently
+/// fire (a single time) and set a property (timerelapsed) with value 1 in the window with
+/// id 1109 after 5 seconds have passed.
+/// ~~~~~~~~~~~~~
+/// AlarmClock(mytimer\,SetProperty(timerelapsed\,1\,1109)\,00:00:05\,silent\,false])
+/// ~~~~~~~~~~~~~
+/// <p>
+/// @param[in] name name
+/// @param[in] command command
+/// @param[in] time [opt] <b>(a)</b> Length in minutes or <b>(b)</b> a timestring in the format `hh:mm:ss` or `mm min`.
+/// @param[in] silent [opt] Send "silent" to suppress notifications.
+/// @param[in] loop [opt] Send "loop" to loop the alarm.
+/// }
+/// \table_row2_l{
+/// <b>`ActivateWindow(window[\,dir\, return])`</b>
+/// ,
+/// Opens the given window. The parameter window can either be the window's id\,
+/// or in the case of a standard window\, the window's name. See \ref window_ids "here" for a list
+/// of window names\, and their respective ids.
+/// If\, furthermore\, the window is
+/// Music\, Video\, Pictures\, or Program files\, then the optional dir parameter
+/// specifies which folder Kodi should default to once the window is opened.
+/// This must be a source as specified in sources.xml\, or a subfolder of a
+/// valid source. For some windows (MusicLibrary and VideoLibrary)\, a third
+/// parameter (return) may be specified\, which indicates that Kodi should use this
+/// folder as the "root" of the level\, and thus the "parent directory" action
+/// from within this folder will return the user to where they were prior to
+/// the window activating.
+/// @param[in] window The window name.
+/// @param[in] dir Window starting folder (optional).
+/// @param[in] return if dir should be used as the rootfolder of the level
+/// }
+/// \table_row2_l{
+/// <b>`ActivateWindowAndFocus(id1\, id2\,item1\, id3\,item2)`</b>
+/// ,
+/// Activate window with id1\, first focus control id2 and then focus control
+/// id3. if either of the controls is a container\, you can specify which
+/// item to focus (else\, set it to 0).
+/// @param[in] id1 The window name.
+/// @param[in] params[1\,...] Pair of (container ID\, focus item).
+/// }
+/// \table_row2_l{
+/// <b>`ClearProperty(key[\,id])`</b>
+/// ,
+/// Clears a window property for the current focused window/dialog(key)\, or
+/// the specified window (key\,id).
+/// @param[in] key The property to clear.
+/// @param[in] id The window to clear property in (optional).
+/// }
+/// \table_row2_l{
+/// <b>`Dialog.Close(dialog[\,force])`</b>
+/// ,
+/// Close a dialog. Set force to true to bypass animations. Use (all\,true)
+/// to close all opened dialogs at once.
+/// @param[in] dialog Send "all" to close all dialogs\, or dialog name.
+/// @param[in] force Send "true" to force close (skip animations) (optional).
+/// }
+/// \table_row2_l{
+/// <b>`Notification(header\,message[\,time\,image])`</b>
+/// ,
+/// Will display a notification dialog with the specified header and message\,
+/// in addition you can set the length of time it displays in milliseconds
+/// and a icon image.
+/// @param[in] header Notification title.
+/// @param[in] message Notification text.
+/// @param[in] time Display time in milliseconds (optional).
+/// @param[in] image Notification icon (optional).
+/// }
+/// \table_row2_l{
+/// <b>`RefreshRSS`</b>
+/// ,
+/// Reload RSS feeds from RSSFeeds.xml
+/// }
+/// \table_row2_l{
+/// <b>`ReplaceWindow(window\,dir)`</b>
+/// ,
+/// Replaces the current window with the given window. This is the same as
+/// ActivateWindow() but it doesn't update the window history list\, so when
+/// you go back from the new window it will not return to the previous
+/// window\, rather will return to the previous window's previous window.
+/// @param[in] window The window name.
+/// @param[in] dir Window starting folder (optional).
+/// }
+/// \table_row2_l{
+/// <b>`ReplaceWindowAndFocus(id1\, id2\,item1\, id3\,item2)`</b>
+/// ,
+/// Replace window with id1\, first focus control id2 and then focus control
+/// id3. if either of the controls is a container\, you can specify which
+/// item to focus (else\, set it to 0).
+/// @param[in] id1 The window name.
+/// @param[in] params[1\,...] Pair of (container ID\, focus item).
+/// }
+/// \table_row2_l{
+/// <b>`Resolution(resIdent)`</b>
+/// ,
+/// Change Kodi's Resolution (default is 4x3).
+/// param[in] resIdent A resolution identifier.
+/// | | Identifiers | |
+/// |:--------:|:-----------:|:--------:|
+/// | pal | pal16x9 | ntsc |
+/// | ntsc16x9 | 720p | 720psbs |
+/// | 720ptb | 1080psbs | 1080ptb |
+/// | 1080i | | |
+/// }
+/// \table_row2_l{
+/// <b>`SetGUILanguage(lang)`</b>
+/// ,
+/// Set GUI Language
+/// @param[in] lang The language to use.
+/// }
+/// \table_row2_l{
+/// <b>`SetProperty(key\,value[\,id])`</b>
+/// ,
+/// Sets a window property for the current window (key\,value)\, or the
+/// specified window (key\,value\,id).
+/// @param[in] key The property to set.
+/// @param[in] value The property value.
+/// @param[in] id The window to set property in (optional).
+/// }
+/// \table_row2_l{
+/// <b>`SetStereoMode(ident)`</b>
+/// ,
+/// Changes the stereo mode of the GUI.
+/// Params can be:
+/// toggle\, next\, previous\, select\, tomono or any of the supported stereomodes (off\,
+/// split_vertical\, split_horizontal\, row_interleaved\, hardware_based\, anaglyph_cyan_red\, anaglyph_green_magenta\, monoscopic)
+/// @param[in] ident Stereo mode identifier.
+/// }
+/// \table_row2_l{
+/// <b>`TakeScreenshot(url[\,sync)`</b>
+/// ,
+/// Takes a Screenshot
+/// @param[in] url URL to save file to. Blank to use default.
+/// @param[in] sync Add "sync" to run synchronously (optional).
+/// }
+/// \table_row2_l{
+/// <b>`ToggleDirtyRegionVisualization`</b>
+/// ,
+/// makes dirty regions visible for debugging proposes.
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CGUIBuiltins::GetOperations() const
+{
+ return {
+ {"action", {"Executes an action for the active window (same as in keymap)", 1, Action}},
+ {"cancelalarm", {"Cancels an alarm", 1, CancelAlarm}},
+ {"alarmclock", {"Prompt for a length of time and start an alarm clock", 2, AlarmClock}},
+ {"activatewindow", {"Activate the specified window", 1, ActivateWindow<false>}},
+ {"activatewindowandfocus", {"Activate the specified window and sets focus to the specified id", 1, ActivateAndFocus<false>}},
+ {"clearproperty", {"Clears a window property for the current focused window/dialog (key,value)", 1, ClearProperty}},
+ {"dialog.close", {"Close a dialog", 1, CloseDialog}},
+ {"notification", {"Shows a notification on screen, specify header, then message, and optionally time in milliseconds and a icon.", 2, Notification}},
+ {"refreshrss", {"Reload RSS feeds from RSSFeeds.xml", 0, RefreshRSS}},
+ {"replacewindow", {"Replaces the current window with the new one", 1, ActivateWindow<true>}},
+ {"replacewindowandfocus", {"Replaces the current window with the new one and sets focus to the specified id", 1, ActivateAndFocus<true>}},
+ {"setguilanguage", {"Set GUI Language", 1, SetLanguage}},
+ {"setproperty", {"Sets a window property for the current focused window/dialog (key,value)", 2, SetProperty}},
+ {"setstereomode", {"Changes the stereo mode of the GUI. Params can be: toggle, next, previous, select, tomono or any of the supported stereomodes (off, split_vertical, split_horizontal, row_interleaved, hardware_based, anaglyph_cyan_red, anaglyph_green_magenta, anaglyph_yellow_blue, monoscopic)", 1, SetStereoMode}},
+ {"takescreenshot", {"Takes a Screenshot", 0, Screenshot}},
+ {"toggledirtyregionvisualization", {"Enables/disables dirty-region visualization", 0, ToggleDirty}}
+ };
+}
diff --git a/xbmc/interfaces/builtins/GUIBuiltins.h b/xbmc/interfaces/builtins/GUIBuiltins.h
new file mode 100644
index 0000000..fd8f238
--- /dev/null
+++ b/xbmc/interfaces/builtins/GUIBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing GUI related built-in commands.
+class CGUIBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/GUIContainerBuiltins.cpp b/xbmc/interfaces/builtins/GUIContainerBuiltins.cpp
new file mode 100644
index 0000000..a97565c
--- /dev/null
+++ b/xbmc/interfaces/builtins/GUIContainerBuiltins.cpp
@@ -0,0 +1,189 @@
+/*
+ * 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 "GUIContainerBuiltins.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/StringUtils.h"
+
+/*! \brief Change sort method.
+ * \param params (ignored)
+ *
+ * Set the Dir template parameter to 1 to switch to next sort method
+ * or -1 to switch to previous sort method.
+ */
+ template<int Dir>
+static int ChangeSortMethod(const std::vector<std::string>& params)
+{
+ CGUIMessage message(GUI_MSG_CHANGE_SORT_METHOD, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, 0, Dir);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+
+ return 0;
+}
+
+/*! \brief Change view mode.
+ * \param params (ignored)
+ *
+ * Set the Dir template parameter to 1 to switch to next view mode
+ * or -1 to switch to previous view mode.
+ */
+ template<int Dir>
+static int ChangeViewMode(const std::vector<std::string>& params)
+{
+ CGUIMessage message(GUI_MSG_CHANGE_VIEW_MODE, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, 0, Dir);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+
+ return 0;
+}
+
+/*! \brief Refresh a media window.
+ * \param params The parameters.
+ * \details params[0] = The URL to refresh window at.
+ */
+static int Refresh(const std::vector<std::string>& params)
+{ // NOTE: These messages require a media window, thus they're sent to the current activewindow.
+ // This shouldn't stop a dialog intercepting it though.
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, GUI_MSG_UPDATE, 1); // 1 to reset the history
+ message.SetStringParam(!params.empty() ? params[0] : "");
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+
+ return 0;
+}
+
+/*! \brief Set sort method.
+ * \param params The parameters.
+ * \details params[0] = ID of sort method.
+ */
+static int SetSortMethod(const std::vector<std::string>& params)
+{
+ CGUIMessage message(GUI_MSG_CHANGE_SORT_METHOD, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, atoi(params[0].c_str()));
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+
+ return 0;
+}
+
+/*! \brief Set view mode.
+ * \param params The parameters.
+ * \details params[0] = ID of view mode.
+ */
+static int SetViewMode(const std::vector<std::string>& params)
+{
+ CGUIMessage message(GUI_MSG_CHANGE_VIEW_MODE, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, atoi(params[0].c_str()));
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+
+ return 0;
+}
+
+/*! \brief Toggle sort direction.
+ * \param params (ignored)
+ */
+static int ToggleSortDirection(const std::vector<std::string>& params)
+{
+ CGUIMessage message(GUI_MSG_CHANGE_SORT_DIRECTION, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+
+ return 0;
+}
+
+/*! \brief Update a listing in a media window.
+ * \param params The parameters.
+ * \details params[0] = The URL to update listing at.
+ * params[1] = "replace" to reset history (optional).
+ */
+static int Update(const std::vector<std::string>& params)
+{
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, GUI_MSG_UPDATE, 0);
+ message.SetStringParam(params[0]);
+ if (params.size() > 1 && StringUtils::EqualsNoCase(params[1], "replace"))
+ message.SetParam2(1); // reset the history
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_6 GUI container built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`Container.NextSortMethod`</b>
+/// ,
+/// Change to the next sort method.
+/// }
+/// \table_row2_l{
+/// <b>`Container.NextViewMode`</b>
+/// ,
+/// Select the next view mode.
+/// }
+/// \table_row2_l{
+/// <b>`Container.PreviousSortMethod`</b>
+/// ,
+/// Change to the previous sort method.
+/// }
+/// \table_row2_l{
+/// <b>`Container.PreviousViewMode`</b>
+/// ,
+/// Select the previous view mode.
+/// }
+/// \table_row2_l{
+/// <b>`Container.Refresh(url)`</b>
+/// ,
+/// Refresh current listing
+/// @param[in] url The URL to refresh window at.
+/// }
+/// \table_row2_l{
+/// <b>`Container.SetSortMethod(id)`</b>
+/// ,
+/// Change to the specified sort method. (For list of ID's \ref SortBy "see List" of sort methods below)
+/// @param[in] id ID of sort method.
+/// }
+/// \table_row2_l{
+/// <b>`Container.SetViewMode(id)`</b>
+/// ,
+/// Set the current view mode (list\, icons etc.) to the given container id.
+/// @param[in] id ID of view mode.
+/// }
+/// \table_row2_l{
+/// <b>`Container.SortDirection`</b>
+/// ,
+/// Toggle the sort direction
+/// }
+/// \table_row2_l{
+/// <b>`Container.Update(url\,[replace])`</b>
+/// ,
+/// Update current listing. Send `Container.Update(path\,replace)` to reset the path history.
+/// @param[in] url The URL to update listing at.
+/// @param[in] replace "replace" to reset history (optional).
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CGUIContainerBuiltins::GetOperations() const
+{
+ return {
+ {"container.nextsortmethod", {"Change to the next sort method", 0, ChangeSortMethod<1>}},
+ {"container.nextviewmode", {"Move to the next view type (and refresh the listing)", 0, ChangeViewMode<1>}},
+ {"container.previoussortmethod", {"Change to the previous sort method", 0, ChangeSortMethod<-1>}},
+ {"container.previousviewmode", {"Move to the previous view type (and refresh the listing)", 0, ChangeViewMode<-1>}},
+ {"container.refresh", {"Refresh current listing", 0, Refresh}},
+ {"container.setsortdirection", {"Toggle the sort direction", 0, ToggleSortDirection}},
+ {"container.setsortmethod", {"Change to the specified sort method", 1, SetSortMethod}},
+ {"container.setviewmode", {"Move to the view with the given id", 1, SetViewMode}},
+ {"container.update", {"Update current listing. Send Container.Update(path,replace) to reset the path history", 1, Update}}
+ };
+}
diff --git a/xbmc/interfaces/builtins/GUIContainerBuiltins.h b/xbmc/interfaces/builtins/GUIContainerBuiltins.h
new file mode 100644
index 0000000..906afa1
--- /dev/null
+++ b/xbmc/interfaces/builtins/GUIContainerBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing GUI container related built-in commands.
+class CGUIContainerBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/GUIControlBuiltins.cpp b/xbmc/interfaces/builtins/GUIControlBuiltins.cpp
new file mode 100644
index 0000000..d8d89fa
--- /dev/null
+++ b/xbmc/interfaces/builtins/GUIControlBuiltins.cpp
@@ -0,0 +1,238 @@
+/*
+ * 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 "GUIControlBuiltins.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/WindowTranslator.h"
+#include "utils/StringUtils.h"
+
+/*! \brief Send a move event to a GUI control.
+ * \param params The parameters.
+ * \details params[0] = ID of control.
+ * params[1] = Offset of move.
+ */
+static int ControlMove(const std::vector<std::string>& params)
+{
+ CGUIMessage message(GUI_MSG_MOVE_OFFSET, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog(),
+ atoi(params[0].c_str()), atoi(params[1].c_str()));
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+
+ return 0;
+}
+
+/*! \brief Send a click event to a GUI control.
+ * \param params The parameters.
+ * \details params[0] = ID of control.
+ * params[1] = ID for window with control (optional).
+ */
+static int SendClick(const std::vector<std::string>& params)
+{
+ if (params.size() == 2)
+ {
+ // have a window - convert it
+ int windowID = CWindowTranslator::TranslateWindow(params[0]);
+ CGUIMessage message(GUI_MSG_CLICKED, atoi(params[1].c_str()), windowID);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+ }
+ else
+ { // single param - assume you meant the focused window
+ CGUIMessage message(GUI_MSG_CLICKED, atoi(params[0].c_str()), CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+ }
+
+ return 0;
+}
+
+/*! \brief Send a message to a control.
+ * \param params The parameters.
+ * \details params[0] = ID of control.
+ * params[1] = Action name.
+ * \params[2] = ID of window with control (optional).
+ */
+static int SendMessage(const std::vector<std::string>& params)
+{
+ int controlID = atoi(params[0].c_str());
+ int windowID = (params.size() == 3) ? CWindowTranslator::TranslateWindow(params[2]) : CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ if (params[1] == "moveup")
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_MOVE_OFFSET, windowID, controlID, 1);
+ else if (params[1] == "movedown")
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_MOVE_OFFSET, windowID, controlID, -1);
+ else if (params[1] == "pageup")
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_PAGE_UP, windowID, controlID);
+ else if (params[1] == "pagedown")
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_PAGE_DOWN, windowID, controlID);
+ else if (params[1] == "click")
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_CLICKED, controlID, windowID);
+
+ return 0;
+}
+
+/*! \brief Give a control focus.
+ * \param params The parameters.
+ * \details params[0] = ID of control.
+ * params[1] = ID of subitem of control (optional).
+ * params[2] = "absolute" to focus the absolute position instead of the relative one (optional).
+ */
+static int SetFocus(const std::vector<std::string>& params)
+{
+ int controlID = atol(params[0].c_str());
+ int subItem = (params.size() > 1) ? atol(params[1].c_str())+1 : 0;
+ int absID = 0;
+ if (params.size() > 2 && StringUtils::EqualsNoCase(params[2].c_str(), "absolute"))
+ absID = 1;
+ CGUIMessage msg(GUI_MSG_SETFOCUS, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog(), controlID, subItem, absID);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ return 0;
+}
+
+/*! \brief Set a control to visible.
+ * \param params The parameters.
+ * \details params[0] = ID of control.
+ */
+static int SetVisible(const std::vector<std::string>& params)
+{
+ int controlID = std::stol(params[0]);
+ CGUIMessage msg{GUI_MSG_VISIBLE,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog(),
+ controlID};
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ return 0;
+}
+
+/*! \brief Set a control to hidden.
+ * \param params The parameters.
+ * \details params[0] = ID of control.
+ */
+static int SetHidden(const std::vector<std::string>& params)
+{
+ int controlID = std::stol(params[0]);
+ CGUIMessage msg{GUI_MSG_HIDDEN,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog(),
+ controlID};
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ return 0;
+}
+
+/*! \brief Shift page in a control.
+ * \param params The parameters.
+ * \details params[0] = ID of control
+ *
+ * Set Message template parameter to GUI_MSG_PAGE_DOWN/GUI_MSG_PAGE_UP.
+ */
+ template<int Message>
+static int ShiftPage(const std::vector<std::string>& params)
+{
+ int id = atoi(params[0].c_str());
+ CGUIMessage message(Message, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog(), id);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_7 GUI control built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`control.message(controlId\, action[\, windowId])`</b>
+/// ,
+/// Send a given message to a control within a given window
+/// @param[in] controlId ID of control.
+/// @param[in] action Action name.
+/// @param[in] windowId ID of window with control (optional).
+/// }
+/// \table_row2_l{
+/// <b>`control.move(id\, offset)`</b>
+/// ,
+/// Tells the specified control to 'move' to another entry specified by offset
+/// @param[in] id ID of control.
+/// @param[in] offset Offset of move.
+/// }
+/// \table_row2_l{
+/// <b>`control.setfocus(controlId[\, subitemId])`</b>
+/// ,
+/// Change current focus to a different control id
+/// @param[in] controlId ID of control.
+/// @param[in] subitemId ID of subitem of control (optional).
+/// @param[in] absolute "absolute" to focus the absolute position instead of the relative one (optional).
+/// }
+/// \table_row2_l{
+/// <b>`control.setvisible(controlId)`</b>
+/// \anchor Builtin_SetVisible,
+/// Set the control id to visible
+/// @param[in] controlId ID of control.
+/// <p><hr>
+/// @skinning_v20 **[New builtin]** \link Builtin_SetVisible `SetVisible(id)`\endlink
+/// <p>
+/// }
+/// \table_row2_l{
+/// <b>`control.sethidden(controlId)`</b>
+/// \anchor Builtin_SetHidden,
+/// Set the control id to hidden
+/// @param[in] controlId ID of control.
+/// <p><hr>
+/// @skinning_v20 **[New builtin]** \link Builtin_SetHidden `SetHidden(id)`\endlink
+/// <p>
+/// }
+/// \table_row2_l{
+/// <b>`pagedown(controlId)`</b>
+/// ,
+/// Send a page down event to the pagecontrol with given id
+/// @param[in] controlId ID of control.
+/// }
+/// \table_row2_l{
+/// <b>`pageup(controlId)`</b>
+/// ,
+/// Send a page up event to the pagecontrol with given id
+/// @param[in] controlId ID of control.
+/// }
+/// \table_row2_l{
+/// <b>`sendclick(controlId [\, windowId])`</b>
+/// ,
+/// Send a click message from the given control to the given window
+/// @param[in] controlId ID of control.
+/// @param[in] windowId ID for window with control (optional).
+/// }
+/// \table_row2_l{
+/// <b>`setfocus`</b>
+/// ,
+/// Change current focus to a different control id
+/// @param[in] controlId ID of control.
+/// @param[in] subitemId ID of subitem of control (optional).
+/// @param[in] absolute "absolute" to focus the absolute position instead of the relative one (optional).
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CGUIControlBuiltins::GetOperations() const
+{
+ return {
+ {"control.message", {"Send a given message to a control within a given window", 2, SendMessage}},
+ {"control.move", {"Tells the specified control to 'move' to another entry specified by offset", 2, ControlMove}},
+ {"control.setfocus", {"Change current focus to a different control id", 1, SetFocus}},
+ {"control.setvisible", {"Set the control id to visible", 1, SetVisible}},
+ {"control.sethidden", {"Set the control id to Hidden", 1, SetHidden}},
+ {"pagedown", {"Send a page down event to the pagecontrol with given id", 1, ShiftPage<GUI_MSG_PAGE_DOWN>}},
+ {"pageup", {"Send a page up event to the pagecontrol with given id", 1, ShiftPage<GUI_MSG_PAGE_UP>}},
+ {"sendclick", {"Send a click message from the given control to the given window", 1, SendClick}},
+ {"setfocus", {"Change current focus to a different control id", 1, SetFocus}},
+ };
+}
diff --git a/xbmc/interfaces/builtins/GUIControlBuiltins.h b/xbmc/interfaces/builtins/GUIControlBuiltins.h
new file mode 100644
index 0000000..70831d6
--- /dev/null
+++ b/xbmc/interfaces/builtins/GUIControlBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing GUI control related built-in commands.
+class CGUIControlBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/LibraryBuiltins.cpp b/xbmc/interfaces/builtins/LibraryBuiltins.cpp
new file mode 100644
index 0000000..5cb678a
--- /dev/null
+++ b/xbmc/interfaces/builtins/LibraryBuiltins.cpp
@@ -0,0 +1,416 @@
+/*
+ * 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 "LibraryBuiltins.h"
+
+#include "GUIUserMessages.h"
+#include "MediaSource.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/infoscanner/MusicInfoScanner.h"
+#include "settings/LibExportSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoLibraryQueue.h"
+
+using namespace KODI::MESSAGING;
+
+/*! \brief Clean a library.
+ * \param params The parameters.
+ * \details params[0] = "video" or "music".
+ */
+static int CleanLibrary(const std::vector<std::string>& params)
+{
+ bool userInitiated = true;
+ if (params.size() > 1)
+ userInitiated = StringUtils::EqualsNoCase(params[1], "true");
+ if (!params.size() || StringUtils::EqualsNoCase(params[0], "video")
+ || StringUtils::EqualsNoCase(params[0], "movies")
+ || StringUtils::EqualsNoCase(params[0], "tvshows")
+ || StringUtils::EqualsNoCase(params[0], "musicvideos"))
+ {
+ if (!CVideoLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ if (userInitiated && CVideoLibraryQueue::GetInstance().IsRunning())
+ HELPERS::ShowOKDialogText(CVariant{700}, CVariant{703});
+ else
+ {
+ const std::string content = (params.empty() || params[0] == "video") ? "" : params[0];
+ const std::string directory = params.size() > 2 ? params[2] : "";
+
+ std::set<int> paths;
+ if (!content.empty() || !directory.empty())
+ {
+ CVideoDatabase db;
+ std::set<std::string> contentPaths;
+ if (db.Open())
+ {
+ if (!directory.empty())
+ contentPaths.insert(directory);
+ else
+ db.GetPaths(contentPaths);
+ for (const std::string& path : contentPaths)
+ {
+ if (db.GetContentForPath(path) == content)
+ {
+ paths.insert(db.GetPathId(path));
+ std::vector<std::pair<int, std::string>> sub;
+ if (db.GetSubPaths(path, sub))
+ {
+ for (const auto& it : sub)
+ paths.insert(it.first);
+ }
+ }
+ }
+ }
+ if (paths.empty())
+ return 0;
+ }
+
+ if (userInitiated)
+ CVideoLibraryQueue::GetInstance().CleanLibraryModal(paths);
+ else
+ CVideoLibraryQueue::GetInstance().CleanLibrary(paths, true);
+ }
+ }
+ else
+ CLog::Log(LOGERROR, "CleanLibrary is not possible while scanning or cleaning");
+ }
+ else if (StringUtils::EqualsNoCase(params[0], "music"))
+ {
+ if (!CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ if (!(userInitiated && CMusicLibraryQueue::GetInstance().IsRunning()))
+ CMusicLibraryQueue::GetInstance().CleanLibrary(userInitiated);
+ }
+ else
+ CLog::Log(LOGERROR, "CleanLibrary is not possible while scanning for media info");
+ }
+ else
+ CLog::Log(LOGERROR, "Unknown content type '{}' passed to CleanLibrary, ignoring", params[0]);
+
+ return 0;
+}
+
+/*! \brief Export a library.
+ * \param params The parameters.
+ * \details params[0] = "video" or "music".
+ * params[1] = "true" to export to separate files (optional).
+ * params[2] = "true" to export thumbs (optional) or the file path for export to singlefile.
+ * params[3] = "true" to overwrite existing files (optional).
+ * params[4] = "true" to export actor thumbs (optional).
+ */
+static int ExportLibrary(const std::vector<std::string>& params)
+{
+ int iHeading = 647;
+ if (StringUtils::EqualsNoCase(params[0], "music"))
+ iHeading = 20196;
+ std::string path;
+ VECSOURCES shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ CServiceBroker::GetMediaManager().GetNetworkLocations(shares);
+ CServiceBroker::GetMediaManager().GetRemovableDrives(shares);
+ bool singleFile;
+ bool thumbs=false;
+ bool actorThumbs=false;
+ bool overwrite=false;
+ bool cancelled=false;
+
+ if (params.size() > 1)
+ singleFile = StringUtils::EqualsNoCase(params[1], "false");
+ else
+ {
+ HELPERS::DialogResponse result = HELPERS::ShowYesNoDialogText(CVariant{iHeading}, CVariant{20426}, CVariant{20428}, CVariant{20429});
+ cancelled = result == HELPERS::DialogResponse::CHOICE_CANCELLED;
+ singleFile = result != HELPERS::DialogResponse::CHOICE_YES;
+ }
+
+ if (cancelled)
+ return -1;
+
+ if (!singleFile)
+ {
+ if (params.size() > 2)
+ thumbs = StringUtils::EqualsNoCase(params[2], "true");
+ else
+ {
+ HELPERS::DialogResponse result = HELPERS::ShowYesNoDialogText(CVariant{iHeading}, CVariant{20430});
+ cancelled = result == HELPERS::DialogResponse::CHOICE_CANCELLED;
+ thumbs = result == HELPERS::DialogResponse::CHOICE_YES;
+ }
+ }
+
+ if (cancelled)
+ return -1;
+
+ if (thumbs && !singleFile && StringUtils::EqualsNoCase(params[0], "video"))
+ {
+ std::string movieSetsInfoPath = CServiceBroker::GetSettingsComponent()->GetSettings()->
+ GetString(CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER);
+ if (movieSetsInfoPath.empty())
+ {
+ auto result = HELPERS::ShowYesNoDialogText(CVariant{iHeading}, CVariant{36301});
+ cancelled = result != HELPERS::DialogResponse::CHOICE_YES;
+ }
+ }
+
+ if (cancelled)
+ return -1;
+
+ if (thumbs && StringUtils::EqualsNoCase(params[0], "video"))
+ {
+ if (params.size() > 4)
+ actorThumbs = StringUtils::EqualsNoCase(params[4], "true");
+ else
+ {
+ HELPERS::DialogResponse result = HELPERS::ShowYesNoDialogText(CVariant{iHeading}, CVariant{20436});
+ cancelled = result == HELPERS::DialogResponse::CHOICE_CANCELLED;
+ actorThumbs = result == HELPERS::DialogResponse::CHOICE_YES;
+ }
+ }
+
+ if (cancelled)
+ return -1;
+
+ if (!singleFile)
+ {
+ if (params.size() > 3)
+ overwrite = StringUtils::EqualsNoCase(params[3], "true");
+ else
+ {
+ HELPERS::DialogResponse result = HELPERS::ShowYesNoDialogText(CVariant{iHeading}, CVariant{20431});
+ cancelled = result == HELPERS::DialogResponse::CHOICE_CANCELLED;
+ overwrite = result == HELPERS::DialogResponse::CHOICE_YES;
+ }
+ }
+
+ if (cancelled)
+ return -1;
+
+ if (params.size() > 2)
+ path=params[2];
+ if (!singleFile || !path.empty() ||
+ CGUIDialogFileBrowser::ShowAndGetDirectory(shares, g_localizeStrings.Get(661),
+ path, true))
+ {
+ if (StringUtils::EqualsNoCase(params[0], "video"))
+ {
+ CVideoDatabase videodatabase;
+ videodatabase.Open();
+ videodatabase.ExportToXML(path, singleFile, thumbs, actorThumbs, overwrite);
+ videodatabase.Close();
+ }
+ else
+ {
+ CLibExportSettings settings;
+ // ELIBEXPORT_SINGLEFILE, ELIBEXPORT_ALBUMS + ELIBEXPORT_ALBUMARTISTS by default
+ settings.m_strPath = path;
+ if (!singleFile)
+ settings.SetExportType(ELIBEXPORT_TOLIBRARYFOLDER);
+ settings.m_artwork = thumbs;
+ settings.m_overwrite = overwrite;
+ // Export music library (not showing progress dialog)
+ CMusicLibraryQueue::GetInstance().ExportLibrary(settings, false);
+ }
+ }
+
+ return 0;
+}
+
+/*! \brief Export a library with extended parameters
+Avoiding breaking change to original ExportLibrary routine parameters
+* \param params The parameters.
+* \details params[0] = "video" or "music".
+* params[1] = export type "singlefile", "separate", or "library".
+* params[2] = path of destination folder.
+* params[3,...] = "unscraped" to include unscraped items
+* params[3,...] = "overwrite" to overwrite existing files.
+* params[3,...] = "artwork" to include images such as thumbs and fanart.
+* params[3,...] = "skipnfo" to not include nfo files (just art).
+* params[3,...] = "ablums" to include albums.
+* params[3,...] = "albumartists" to include album artists.
+* params[3,...] = "songartists" to include song artists.
+* params[3,...] = "otherartists" to include other artists.
+*/
+static int ExportLibrary2(const std::vector<std::string>& params)
+{
+ CLibExportSettings settings;
+ if (params.size() < 3)
+ return -1;
+ settings.m_strPath = params[2];
+ settings.SetExportType(ELIBEXPORT_SINGLEFILE);
+ if (StringUtils::EqualsNoCase(params[1], "separate"))
+ settings.SetExportType(ELIBEXPORT_SEPARATEFILES);
+ else if (StringUtils::EqualsNoCase(params[1], "library"))
+ {
+ settings.SetExportType(ELIBEXPORT_TOLIBRARYFOLDER);
+ settings.m_strPath.clear();
+ }
+ settings.ClearItems();
+
+ for (unsigned int i = 2; i < params.size(); i++)
+ {
+ if (StringUtils::EqualsNoCase(params[i], "artwork"))
+ settings.m_artwork = true;
+ else if (StringUtils::EqualsNoCase(params[i], "overwrite"))
+ settings.m_overwrite = true;
+ else if (StringUtils::EqualsNoCase(params[i], "unscraped"))
+ settings.m_unscraped = true;
+ else if (StringUtils::EqualsNoCase(params[i], "skipnfo"))
+ settings.m_skipnfo = true;
+ else if (StringUtils::EqualsNoCase(params[i], "albums"))
+ settings.AddItem(ELIBEXPORT_ALBUMS);
+ else if (StringUtils::EqualsNoCase(params[i], "albumartists"))
+ settings.AddItem(ELIBEXPORT_ALBUMARTISTS);
+ else if (StringUtils::EqualsNoCase(params[i], "songartists"))
+ settings.AddItem(ELIBEXPORT_SONGARTISTS);
+ else if (StringUtils::EqualsNoCase(params[i], "otherartists"))
+ settings.AddItem(ELIBEXPORT_OTHERARTISTS);
+ else if (StringUtils::EqualsNoCase(params[i], "actorthumbs"))
+ settings.AddItem(ELIBEXPORT_ACTORTHUMBS);
+ }
+ if (StringUtils::EqualsNoCase(params[0], "music"))
+ {
+ // Export music library (not showing progress dialog)
+ CMusicLibraryQueue::GetInstance().ExportLibrary(settings, false);
+ }
+ else
+ {
+ CVideoDatabase videodatabase;
+ videodatabase.Open();
+ videodatabase.ExportToXML(settings.m_strPath, settings.IsSingleFile(),
+ settings.m_artwork, settings.IsItemExported(ELIBEXPORT_ACTORTHUMBS), settings.m_overwrite);
+ videodatabase.Close();
+ }
+ return 0;
+}
+
+
+/*! \brief Update a library.
+ * \param params The parameters.
+ * \details params[0] = "video" or "music".
+ * params[1] = "true" to suppress dialogs (optional).
+ */
+static int UpdateLibrary(const std::vector<std::string>& params)
+{
+ bool userInitiated = true;
+ if (params.size() > 2)
+ userInitiated = StringUtils::EqualsNoCase(params[2], "true");
+ if (StringUtils::EqualsNoCase(params[0], "music"))
+ {
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ CMusicLibraryQueue::GetInstance().StopLibraryScanning();
+ else
+ CMusicLibraryQueue::GetInstance().ScanLibrary(params.size() > 1 ? params[1] : "",
+ MUSIC_INFO::CMusicInfoScanner::SCAN_NORMAL,
+ userInitiated);
+ }
+ else if (StringUtils::EqualsNoCase(params[0], "video"))
+ {
+ if (CVideoLibraryQueue::GetInstance().IsScanningLibrary())
+ CVideoLibraryQueue::GetInstance().StopLibraryScanning();
+ else
+ CVideoLibraryQueue::GetInstance().ScanLibrary(params.size() > 1 ? params[1] : "", false,
+ userInitiated);
+ }
+
+ return 0;
+}
+
+/*! \brief Open a video library search.
+ * \param params (ignored)
+ */
+static int SearchVideoLibrary(const std::vector<std::string>& params)
+{
+ CGUIMessage msg(GUI_MSG_SEARCH, 0, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg, WINDOW_VIDEO_NAV);
+
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_8 Library built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`cleanlibrary(type)`</b>
+/// ,
+/// Clean the video/music library
+/// @param[in] type "video"\, "movies"\, "tvshows"\, "musicvideos" or "music".
+/// }
+/// \table_row2_l{
+/// <b>`exportlibrary(type [\, exportSingeFile\, exportThumbs\, overwrite\, exportActorThumbs])`</b>
+/// ,
+/// Export the video/music library
+/// @param[in] type "video" or "music".
+/// @param[in] exportSingleFile Add "true" to export to separate files (optional).
+/// @param[in] exportThumbs Add "true" to export thumbs (optional).
+/// @param[in] overwrite Add "true" to overwrite existing files (optional).
+/// @param[in] exportActorThumbs Add "true" to export actor thumbs (optional).
+/// }
+/// \table_row2_l{
+/// <b>`exportlibrary2(library\, exportFiletype\, path [\, unscraped][\, overwrite][\, artwork][\, skipnfo]
+/// [\, albums][\, albumartists][\, songartists][\, otherartists][\, actorthumbs])`</b>
+/// ,
+/// Export the video/music library with extended parameters
+/// @param[in] library "video" or "music".
+/// @param[in] exportFiletype "singlefile"\, "separate" or "library".
+/// @param[in] path Path to destination folder.
+/// @param[in] unscraped Add "unscraped" to include unscraped items.
+/// @param[in] overwrite Add "overwrite" to overwrite existing files.
+/// @param[in] artwork Add "artwork" to include images such as thumbs and fanart.
+/// @param[in] skipnfo Add "skipnfo" to not include nfo files(just art).
+/// @param[in] albums Add "ablums" to include albums.
+/// @param[in] albumartists Add "albumartists" to include album artists.
+/// @param[in] songartists Add "songartists" to include song artists.
+/// @param[in] otherartists Add "otherartists" to include other artists.
+/// @param[in] actorthumbs Add "actorthumbs" to include other actor thumbs.
+/// }
+/// \table_row2_l{
+/// <b>`updatelibrary([type\, suppressDialogs])`</b>
+/// ,
+/// Update the selected library (music or video)
+/// @param[in] type "video" or "music".
+/// @param[in] suppressDialogs Add "true" to suppress dialogs (optional).
+/// }
+/// \table_row2_l{
+/// <b>`videolibrary.search`</b>
+/// ,
+/// Brings up a search dialog which will search the library
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CLibraryBuiltins::GetOperations() const
+{
+ return {
+ {"cleanlibrary", {"Clean the video/music library", 1, CleanLibrary}},
+ {"exportlibrary", {"Export the video/music library", 1, ExportLibrary}},
+ {"exportlibrary2", {"Export the video/music library", 1, ExportLibrary2}},
+ {"updatelibrary", {"Update the selected library (music or video)", 1, UpdateLibrary}},
+ {"videolibrary.search", {"Brings up a search dialog which will search the library", 0, SearchVideoLibrary}}
+ };
+}
diff --git a/xbmc/interfaces/builtins/LibraryBuiltins.h b/xbmc/interfaces/builtins/LibraryBuiltins.h
new file mode 100644
index 0000000..1bc8744
--- /dev/null
+++ b/xbmc/interfaces/builtins/LibraryBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing library related built-in commands.
+class CLibraryBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/OpticalBuiltins.cpp b/xbmc/interfaces/builtins/OpticalBuiltins.cpp
new file mode 100644
index 0000000..4a0ec5d
--- /dev/null
+++ b/xbmc/interfaces/builtins/OpticalBuiltins.cpp
@@ -0,0 +1,75 @@
+/*
+ * 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 "OpticalBuiltins.h"
+
+#include "ServiceBroker.h"
+
+#ifdef HAS_DVD_DRIVE
+#include "storage/MediaManager.h"
+#endif
+
+#ifdef HAS_CDDA_RIPPER
+#include "cdrip/CDDARipper.h"
+#endif
+
+/*! \brief Eject the tray of an optical drive.
+ * \param params (ignored)
+ */
+static int Eject(const std::vector<std::string>& params)
+{
+#ifdef HAS_DVD_DRIVE
+ CServiceBroker::GetMediaManager().ToggleTray();
+#endif
+
+ return 0;
+}
+
+/*! \brief Rip currently inserted CD.
+ * \param params (ignored)
+ */
+static int RipCD(const std::vector<std::string>& params)
+{
+#ifdef HAS_CDDA_RIPPER
+ KODI::CDRIP::CCDDARipper::GetInstance().RipCD();
+#endif
+
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_9 Optical container built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`EjectTray`</b>
+/// ,
+/// Either opens or closes the DVD tray\, depending on its current state.
+/// }
+/// \table_row2_l{
+/// <b>`RipCD`</b>
+/// ,
+/// Will rip the inserted CD from the DVD-ROM drive.
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap COpticalBuiltins::GetOperations() const
+{
+ return {
+ {"ejecttray", {"Close or open the DVD tray", 0, Eject}},
+ {"ripcd", {"Rip the currently inserted audio CD", 0, RipCD}}
+ };
+}
diff --git a/xbmc/interfaces/builtins/OpticalBuiltins.h b/xbmc/interfaces/builtins/OpticalBuiltins.h
new file mode 100644
index 0000000..c1b218e
--- /dev/null
+++ b/xbmc/interfaces/builtins/OpticalBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing optical media related built-in commands.
+class COpticalBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/PVRBuiltins.cpp b/xbmc/interfaces/builtins/PVRBuiltins.cpp
new file mode 100644
index 0000000..f87d0d6
--- /dev/null
+++ b/xbmc/interfaces/builtins/PVRBuiltins.cpp
@@ -0,0 +1,224 @@
+/*
+ * 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 "PVRBuiltins.h"
+
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/windows/GUIWindowPVRGuide.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <regex>
+
+using namespace PVR;
+
+/*! \brief Search for missing channel icons
+ * \param params (ignored)
+ */
+static int SearchMissingIcons(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetPVRManager().TriggerSearchMissingChannelIcons();
+ return 0;
+}
+
+/*! \brief will toggle recording of playing channel, if any.
+ * \param params (ignored)
+ */
+static int ToggleRecordPlayingChannel(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleRecordingOnPlayingChannel();
+ return 0;
+}
+
+/*! \brief seeks to the given percentage in timeshift buffer, if timeshifting is supported.
+ * \param params The parameters
+ * \details params[0] = percentage to seek to in the timeshift buffer.
+ */
+static int SeekPercentage(const std::vector<std::string>& params)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ if (params.empty())
+ {
+ CLog::Log(LOGERROR,"PVR.SeekPercentage(n) - No argument given");
+ }
+ else
+ {
+ const float fTimeshiftPercentage = static_cast<float>(std::atof(params.front().c_str()));
+ if (fTimeshiftPercentage < 0 || fTimeshiftPercentage > 100)
+ {
+ CLog::Log(LOGERROR, "PVR.SeekPercentage(n) - Invalid argument ({:f}), must be in range 0-100",
+ fTimeshiftPercentage);
+ }
+ else if (appPlayer->IsPlaying())
+ {
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+
+ int iTimeshiftProgressDuration = 0;
+ infoMgr.GetInt(iTimeshiftProgressDuration, PVR_TIMESHIFT_PROGRESS_DURATION,
+ INFO::DEFAULT_CONTEXT);
+
+ int iTimeshiftBufferStart = 0;
+ infoMgr.GetInt(iTimeshiftBufferStart, PVR_TIMESHIFT_PROGRESS_BUFFER_START,
+ INFO::DEFAULT_CONTEXT);
+
+ float fPlayerPercentage = static_cast<float>(iTimeshiftProgressDuration) /
+ static_cast<float>(g_application.GetTotalTime()) *
+ (fTimeshiftPercentage - static_cast<float>(iTimeshiftBufferStart));
+ fPlayerPercentage = std::max(0.0f, std::min(fPlayerPercentage, 100.0f));
+
+ g_application.SeekPercentage(fPlayerPercentage);
+ }
+ }
+ return 0;
+}
+
+namespace
+{
+/*! \brief Control PVR Guide window's EPG grid.
+ * \param params The parameters
+ * \details params[0] = Control to execute.
+ */
+int EpgGridControl(const std::vector<std::string>& params)
+{
+ if (params.empty())
+ {
+ CLog::Log(LOGERROR, "EpgGridControl(n) - No argument given");
+ return 0;
+ }
+
+ int activeWindow = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ if (activeWindow != WINDOW_TV_GUIDE && activeWindow != WINDOW_RADIO_GUIDE)
+ {
+ CLog::Log(LOGERROR, "EpgGridControl(n) - Guide window not active");
+ return 0;
+ }
+
+ CGUIWindowPVRGuideBase* guideWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowPVRGuideBase>(activeWindow);
+ if (!guideWindow)
+ {
+ CLog::Log(LOGERROR, "EpgGridControl(n) - Unable to get Guide window instance");
+ return 0;
+ }
+
+ std::string param(params[0]);
+ StringUtils::ToLower(param);
+
+ if (param == "firstprogramme")
+ {
+ guideWindow->GotoBegin();
+ }
+ else if (param == "lastprogramme")
+ {
+ guideWindow->GotoEnd();
+ }
+ else if (param == "currentprogramme")
+ {
+ guideWindow->GotoCurrentProgramme();
+ }
+ else if (param == "selectdate")
+ {
+ guideWindow->OpenDateSelectionDialog();
+ }
+ else if (StringUtils::StartsWithNoCase(param, "+") || StringUtils::StartsWithNoCase(param, "-"))
+ {
+ // jump back/forward n hours
+ if (std::regex_match(param, std::regex("[(-|+)|][0-9]+")))
+ {
+ guideWindow->GotoDate(std::atoi(param.c_str()));
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "EpgGridControl(n) - invalid argument");
+ }
+ }
+ else if (param == "firstchannel")
+ {
+ guideWindow->GotoFirstChannel();
+ }
+ else if (param == "playingchannel")
+ {
+ guideWindow->GotoPlayingChannel();
+ }
+ else if (param == "lastchannel")
+ {
+ guideWindow->GotoLastChannel();
+ }
+ else if (param == "previousgroup")
+ {
+ guideWindow->ActivatePreviousChannelGroup();
+ }
+ else if (param == "nextgroup")
+ {
+ guideWindow->ActivateNextChannelGroup();
+ }
+ else if (param == "selectgroup")
+ {
+ guideWindow->OpenChannelGroupSelectionDialog();
+ }
+
+ return 0;
+}
+
+} // unnamed namespace
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_10 PVR built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`PVR.SearchMissingChannelIcons`</b>
+/// ,
+/// Will start a search for missing channel icons
+/// }
+/// \table_row2_l{
+/// <b>`PVR.ToggleRecordPlayingChannel`</b>
+/// ,
+/// Will toggle recording on playing channel\, if any
+/// }
+/// \table_row2_l{
+/// <b>`PVR.SeekPercentage`</b>
+/// ,
+/// Performs a seek to the given percentage in timeshift buffer\, if timeshifting is supported
+/// }
+/// \table_row2_l{
+/// <b>`PVR.EpgGridControl`</b>
+/// ,
+/// Control PVR Guide window's EPG grid
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CPVRBuiltins::GetOperations() const
+{
+ return {
+ {"pvr.searchmissingchannelicons", {"Search for missing channel icons", 0, SearchMissingIcons}},
+ {"pvr.togglerecordplayingchannel", {"Toggle recording on playing channel", 0, ToggleRecordPlayingChannel}},
+ {"pvr.seekpercentage", {"Performs a seek to the given percentage in timeshift buffer", 1, SeekPercentage}},
+ {"pvr.epggridcontrol", {"Control PVR Guide window's EPG grid", 1, EpgGridControl}},
+ };
+}
diff --git a/xbmc/interfaces/builtins/PVRBuiltins.h b/xbmc/interfaces/builtins/PVRBuiltins.h
new file mode 100644
index 0000000..a22259b
--- /dev/null
+++ b/xbmc/interfaces/builtins/PVRBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing PVR related built-in commands.
+class CPVRBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/PictureBuiltins.cpp b/xbmc/interfaces/builtins/PictureBuiltins.cpp
new file mode 100644
index 0000000..15cfe0a
--- /dev/null
+++ b/xbmc/interfaces/builtins/PictureBuiltins.cpp
@@ -0,0 +1,135 @@
+/*
+ * 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 "PictureBuiltins.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/StringUtils.h"
+
+/*! \brief Show a picture.
+ * \param params The parameters.
+ * \details params[0] = URL of picture.
+ */
+static int Show(const std::vector<std::string>& params)
+{
+ CGUIMessage msg(GUI_MSG_SHOW_PICTURE, 0, 0);
+ msg.SetStringParam(params[0]);
+ CGUIWindow *pWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_SLIDESHOW);
+ if (pWindow)
+ pWindow->OnMessage(msg);
+
+ return 0;
+}
+
+/*! \brief Start a slideshow.
+ * \param params The parameters.
+ * \details params[0] = Path to run slideshow for.
+ * params[1,..] = "recursive" to run a recursive slideshow.
+ * params[1,...] = "random" to randomize slideshow.
+ * params[1,...] = "notrandom" to not randomize slideshow.
+ * params[1,...] = "pause" to start slideshow paused.
+ * params[1,...] = "beginslide=<number>" to start at a given slide.
+ *
+ * Set the template parameter Recursive to true to run a recursive slideshow.
+ */
+ template<bool Recursive>
+static int Slideshow(const std::vector<std::string>& params)
+{
+ std::string beginSlidePath;
+ // leave RecursiveSlideShow command as-is
+ unsigned int flags = 0;
+ if (Recursive)
+ flags |= 1;
+
+ // SlideShow(dir[,recursive][,[not]random][,pause][,beginslide="/path/to/start/slide.jpg"])
+ // the beginslide value need be escaped (for '"' or '\' in it, by backslash)
+ // and then quoted, or not. See CUtil::SplitParams()
+ else
+ {
+ for (unsigned int i = 1 ; i < params.size() ; i++)
+ {
+ if (StringUtils::EqualsNoCase(params[i], "recursive"))
+ flags |= 1;
+ else if (StringUtils::EqualsNoCase(params[i], "random")) // set fullscreen or windowed
+ flags |= 2;
+ else if (StringUtils::EqualsNoCase(params[i], "notrandom"))
+ flags |= 4;
+ else if (StringUtils::EqualsNoCase(params[i], "pause"))
+ flags |= 8;
+ else if (StringUtils::StartsWithNoCase(params[i], "beginslide="))
+ beginSlidePath = params[i].substr(11);
+ }
+ }
+
+ CGUIMessage msg(GUI_MSG_START_SLIDESHOW, 0, 0, flags);
+ std::vector<std::string> strParams;
+ strParams.push_back(params[0]);
+ strParams.push_back(beginSlidePath);
+ msg.SetStringParams(strParams);
+ CGUIWindow *pWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_SLIDESHOW);
+ if (pWindow)
+ pWindow->OnMessage(msg);
+
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_11 Picture built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`RecursiveSlideShow(dir)`</b>
+/// ,
+/// Run a slideshow from the specified directory\, including all subdirs.
+/// @param[in] dir Path to run slideshow for.
+/// @param[in] random Add "random" to randomize slideshow (optional).
+/// @param[in] notrandom Add "notrandom" to not randomize slideshow (optional).
+/// @param[in] pause Add "pause" to start slideshow paused (optional).
+/// @param[in] beginslide Add "beginslide=<number>" to start at a given slide (optional).
+/// }
+/// \table_row2_l{
+/// <b>`ShowPicture(picture)`</b>
+/// ,
+/// Display a picture by file path.
+/// @param[in] url URL of picture.
+/// }
+/// \table_row2_l{
+/// <b>`SlideShow(dir [\,recursive\, [not]random])`</b>
+/// ,
+/// Starts a slideshow of pictures in the folder dir. Optional parameters are
+/// <b>recursive</b>\, and **random** or **notrandom** slideshow\, adding images
+/// from sub-folders. The **random** and **notrandom** parameters override
+/// the Randomize setting found in the pictures media window.
+/// @param[in] dir Path to run slideshow for.
+/// @param[in] recursive Add "recursive" to run a recursive slideshow (optional).
+/// @param[in] random Add "random" to randomize slideshow (optional).
+/// @param[in] notrandom Add "notrandom" to not randomize slideshow (optional).
+/// @param[in] pause Add "pause" to start slideshow paused (optional).
+/// @param[in] beginslide Add "beginslide=<number>" to start at a given slide (optional).
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CPictureBuiltins::GetOperations() const
+{
+ return {
+ {"recursiveslideshow", {"Run a slideshow from the specified directory, including all subdirs", 1, Slideshow<true>}},
+ {"showpicture", {"Display a picture by file path", 1, Show}},
+ {"slideshow", {"Run a slideshow from the specified directory", 1, Slideshow<false>}}
+ };
+}
diff --git a/xbmc/interfaces/builtins/PictureBuiltins.h b/xbmc/interfaces/builtins/PictureBuiltins.h
new file mode 100644
index 0000000..4dd396b
--- /dev/null
+++ b/xbmc/interfaces/builtins/PictureBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing picture related built-in commands.
+class CPictureBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/PlayerBuiltins.cpp b/xbmc/interfaces/builtins/PlayerBuiltins.cpp
new file mode 100644
index 0000000..ca69d69
--- /dev/null
+++ b/xbmc/interfaces/builtins/PlayerBuiltins.cpp
@@ -0,0 +1,842 @@
+/*
+ * 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 "PlayerBuiltins.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "PartyModeManager.h"
+#include "PlayListPlayer.h"
+#include "SeekHandler.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationPowerHandling.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "music/MusicUtils.h"
+#include "playlists/PlayList.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/PlayerController.h"
+#include "video/VideoUtils.h"
+#include "video/windows/GUIWindowVideoBase.h"
+
+#include <math.h>
+
+#ifdef HAS_DVD_DRIVE
+#include "Autorun.h"
+#endif
+
+/*! \brief Clear current playlist
+ * \param params (ignored)
+ */
+static int ClearPlaylist(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetPlaylistPlayer().Clear();
+
+ return 0;
+}
+
+/*! \brief Start a playlist from a given offset.
+ * \param params The parameters.
+ * \details params[0] = Position in playlist or playlist type.
+ * params[1] = Position in playlist if params[0] is playlist type (optional).
+ */
+static int PlayOffset(const std::vector<std::string>& params)
+{
+ // playlist.playoffset(offset)
+ // playlist.playoffset(music|video,offset)
+ std::string strPos = params[0];
+ std::string paramlow(params[0]);
+ StringUtils::ToLower(paramlow);
+ if (params.size() > 1)
+ {
+ // ignore any other parameters if present
+ std::string strPlaylist = params[0];
+ strPos = params[1];
+
+ PLAYLIST::Id playlistId = PLAYLIST::TYPE_NONE;
+ if (paramlow == "music")
+ playlistId = PLAYLIST::TYPE_MUSIC;
+ else if (paramlow == "video")
+ playlistId = PLAYLIST::TYPE_VIDEO;
+
+ // unknown playlist
+ if (playlistId == PLAYLIST::TYPE_NONE)
+ {
+ CLog::Log(LOGERROR, "Playlist.PlayOffset called with unknown playlist: {}", strPlaylist);
+ return false;
+ }
+
+ // user wants to play the 'other' playlist
+ if (playlistId != CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist())
+ {
+ g_application.StopPlaying();
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(playlistId);
+ }
+ }
+ // play the desired offset
+ int pos = atol(strPos.c_str());
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ // playlist is already playing
+ if (appPlayer->IsPlaying())
+ CServiceBroker::GetPlaylistPlayer().PlayNext(pos);
+ // we start playing the 'other' playlist so we need to use play to initialize the player state
+ else
+ CServiceBroker::GetPlaylistPlayer().Play(pos, "");
+
+ return 0;
+}
+
+/*! \brief Control player.
+ * \param params The parameters
+ * \details params[0] = Control to execute.
+ * params[1] = "notify" to notify user (optional, certain controls).
+ */
+static int PlayerControl(const std::vector<std::string>& params)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+ appPower->WakeUpScreenSaverAndDPMS();
+
+ std::string paramlow(params[0]);
+ StringUtils::ToLower(paramlow);
+
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ if (paramlow == "play")
+ { // play/pause
+ // either resume playing, or pause
+ if (appPlayer->IsPlaying())
+ {
+ if (appPlayer->GetPlaySpeed() != 1)
+ appPlayer->SetPlaySpeed(1);
+ else
+ appPlayer->Pause();
+ }
+ }
+ else if (paramlow == "stop")
+ {
+ g_application.StopPlaying();
+ }
+ else if (StringUtils::StartsWithNoCase(params[0], "frameadvance"))
+ {
+ std::string strFrames;
+ if (params[0].size() == 12)
+ CLog::Log(LOGERROR, "PlayerControl(frameadvance(n)) called with no argument");
+ else if (params[0].size() < 15) // arg must be at least "(N)"
+ CLog::Log(LOGERROR, "PlayerControl(frameadvance(n)) called with invalid argument: \"{}\"",
+ params[0].substr(13));
+ else
+
+ strFrames = params[0].substr(13);
+ StringUtils::TrimRight(strFrames, ")");
+ float frames = (float) atof(strFrames.c_str());
+ appPlayer->FrameAdvance(frames);
+ }
+ else if (paramlow =="rewind" || paramlow == "forward")
+ {
+ if (appPlayer->IsPlaying() && !appPlayer->IsPaused())
+ {
+ float playSpeed = appPlayer->GetPlaySpeed();
+
+ if (paramlow == "rewind" && playSpeed == 1) // Enables Rewinding
+ playSpeed *= -2;
+ else if (paramlow == "rewind" && playSpeed > 1) //goes down a notch if you're FFing
+ playSpeed /= 2;
+ else if (paramlow == "forward" && playSpeed < 1) //goes up a notch if you're RWing
+ {
+ playSpeed /= 2;
+ if (playSpeed == -1)
+ playSpeed = 1;
+ }
+ else
+ playSpeed *= 2;
+
+ if (playSpeed > 32 || playSpeed < -32)
+ playSpeed = 1;
+
+ appPlayer->SetPlaySpeed(playSpeed);
+ }
+ }
+ else if (paramlow =="tempoup" || paramlow == "tempodown")
+ {
+ if (appPlayer->SupportsTempo() && appPlayer->IsPlaying() && !appPlayer->IsPaused())
+ {
+ float playTempo = appPlayer->GetPlayTempo();
+ if (paramlow == "tempodown")
+ playTempo -= 0.1f;
+ else if (paramlow == "tempoup")
+ playTempo += 0.1f;
+
+ appPlayer->SetTempo(playTempo);
+ }
+ }
+ else if (StringUtils::StartsWithNoCase(params[0], "tempo"))
+ {
+ if (params[0].size() == 5)
+ CLog::Log(LOGERROR, "PlayerControl(tempo(n)) called with no argument");
+ else if (params[0].size() < 8) // arg must be at least "(N)"
+ CLog::Log(LOGERROR, "PlayerControl(tempo(n)) called with invalid argument: \"{}\"",
+ params[0].substr(6));
+ else
+ {
+ if (appPlayer->SupportsTempo() && appPlayer->IsPlaying() && !appPlayer->IsPaused())
+ {
+ std::string strTempo = params[0].substr(6);
+ StringUtils::TrimRight(strTempo, ")");
+ float playTempo = strtof(strTempo.c_str(), nullptr);
+
+ appPlayer->SetTempo(playTempo);
+ }
+ }
+ }
+ else if (paramlow == "next")
+ {
+ g_application.OnAction(CAction(ACTION_NEXT_ITEM));
+ }
+ else if (paramlow == "previous")
+ {
+ g_application.OnAction(CAction(ACTION_PREV_ITEM));
+ }
+ else if (paramlow == "bigskipbackward")
+ {
+ if (appPlayer->IsPlaying())
+ appPlayer->Seek(false, true);
+ }
+ else if (paramlow == "bigskipforward")
+ {
+ if (appPlayer->IsPlaying())
+ appPlayer->Seek(true, true);
+ }
+ else if (paramlow == "smallskipbackward")
+ {
+ if (appPlayer->IsPlaying())
+ appPlayer->Seek(false, false);
+ }
+ else if (paramlow == "smallskipforward")
+ {
+ if (appPlayer->IsPlaying())
+ appPlayer->Seek(true, false);
+ }
+ else if (StringUtils::StartsWithNoCase(params[0], "seekpercentage"))
+ {
+ std::string offset;
+ if (params[0].size() == 14)
+ CLog::Log(LOGERROR,"PlayerControl(seekpercentage(n)) called with no argument");
+ else if (params[0].size() < 17) // arg must be at least "(N)"
+ CLog::Log(LOGERROR, "PlayerControl(seekpercentage(n)) called with invalid argument: \"{}\"",
+ params[0].substr(14));
+ else
+ {
+ // Don't bother checking the argument: an invalid arg will do seek(0)
+ offset = params[0].substr(15);
+ StringUtils::TrimRight(offset, ")");
+ float offsetpercent = (float) atof(offset.c_str());
+ if (offsetpercent < 0 || offsetpercent > 100)
+ CLog::Log(LOGERROR, "PlayerControl(seekpercentage(n)) argument, {:f}, must be 0-100",
+ offsetpercent);
+ else if (appPlayer->IsPlaying())
+ g_application.SeekPercentage(offsetpercent);
+ }
+ }
+ else if (paramlow == "showvideomenu")
+ {
+ if (appPlayer->IsPlaying())
+ appPlayer->OnAction(CAction(ACTION_SHOW_VIDEOMENU));
+ }
+ else if (StringUtils::StartsWithNoCase(params[0], "partymode"))
+ {
+ std::string strXspPath;
+ //empty param=music, "music"=music, "video"=video, else xsp path
+ PartyModeContext context = PARTYMODECONTEXT_MUSIC;
+ if (params[0].size() > 9)
+ {
+ if (params[0].size() == 16 && StringUtils::EndsWithNoCase(params[0], "video)"))
+ context = PARTYMODECONTEXT_VIDEO;
+ else if (params[0].size() != 16 || !StringUtils::EndsWithNoCase(params[0], "music)"))
+ {
+ strXspPath = params[0].substr(10);
+ StringUtils::TrimRight(strXspPath, ")");
+ context = PARTYMODECONTEXT_UNKNOWN;
+ }
+ }
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Disable();
+ else
+ g_partyModeManager.Enable(context, strXspPath);
+ }
+ else if (paramlow == "random" || paramlow == "randomoff" || paramlow == "randomon")
+ {
+ // get current playlist
+ PLAYLIST::Id playlistId = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+
+ // reverse the current setting
+ bool shuffled = CServiceBroker::GetPlaylistPlayer().IsShuffled(playlistId);
+ if ((shuffled && paramlow == "randomon") || (!shuffled && paramlow == "randomoff"))
+ return 0;
+
+ // check to see if we should notify the user
+ bool notify = (params.size() == 2 && StringUtils::EqualsNoCase(params[1], "notify"));
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(playlistId, !shuffled, notify);
+
+ // save settings for now playing windows
+ switch (playlistId)
+ {
+ case PLAYLIST::TYPE_MUSIC:
+ CMediaSettings::GetInstance().SetMusicPlaylistShuffled(
+ CServiceBroker::GetPlaylistPlayer().IsShuffled(playlistId));
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ break;
+ case PLAYLIST::TYPE_VIDEO:
+ CMediaSettings::GetInstance().SetVideoPlaylistShuffled(
+ CServiceBroker::GetPlaylistPlayer().IsShuffled(playlistId));
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ default:
+ break;
+ }
+
+ // send message
+ CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_RANDOM, 0, 0, playlistId,
+ CServiceBroker::GetPlaylistPlayer().IsShuffled(playlistId));
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+ else if (StringUtils::StartsWithNoCase(params[0], "repeat"))
+ {
+ // get current playlist
+ PLAYLIST::Id playlistId = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+ PLAYLIST::RepeatState prevRepeatState =
+ CServiceBroker::GetPlaylistPlayer().GetRepeat(playlistId);
+
+ std::string paramlow(params[0]);
+ StringUtils::ToLower(paramlow);
+
+ PLAYLIST::RepeatState repeatState;
+ if (paramlow == "repeatall")
+ repeatState = PLAYLIST::RepeatState::ALL;
+ else if (paramlow == "repeatone")
+ repeatState = PLAYLIST::RepeatState::ONE;
+ else if (paramlow == "repeatoff")
+ repeatState = PLAYLIST::RepeatState::NONE;
+ else if (prevRepeatState == PLAYLIST::RepeatState::NONE)
+ repeatState = PLAYLIST::RepeatState::ALL;
+ else if (prevRepeatState == PLAYLIST::RepeatState::ALL)
+ repeatState = PLAYLIST::RepeatState::ONE;
+ else
+ repeatState = PLAYLIST::RepeatState::NONE;
+
+ if (repeatState == prevRepeatState)
+ return 0;
+
+ // check to see if we should notify the user
+ bool notify = (params.size() == 2 && StringUtils::EqualsNoCase(params[1], "notify"));
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(playlistId, repeatState, notify);
+
+ // save settings for now playing windows
+ switch (playlistId)
+ {
+ case PLAYLIST::TYPE_MUSIC:
+ CMediaSettings::GetInstance().SetMusicPlaylistRepeat(repeatState ==
+ PLAYLIST::RepeatState::ALL);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ break;
+ case PLAYLIST::TYPE_VIDEO:
+ CMediaSettings::GetInstance().SetVideoPlaylistRepeat(repeatState ==
+ PLAYLIST::RepeatState::ALL);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ }
+
+ // send messages so now playing window can get updated
+ CGUIMessage msg(GUI_MSG_PLAYLISTPLAYER_REPEAT, 0, 0, playlistId, static_cast<int>(repeatState));
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+ else if (StringUtils::StartsWithNoCase(params[0], "resumelivetv"))
+ {
+ CFileItem& fileItem(g_application.CurrentFileItem());
+ std::shared_ptr<PVR::CPVRChannel> channel = fileItem.HasPVRRecordingInfoTag() ? fileItem.GetPVRRecordingInfoTag()->Channel() : std::shared_ptr<PVR::CPVRChannel>();
+
+ if (channel)
+ {
+ const std::shared_ptr<PVR::CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(channel);
+ if (!groupMember)
+ {
+ CLog::Log(LOGERROR, "ResumeLiveTv could not obtain channel group member for channel: {}",
+ channel->ChannelName());
+ return false;
+ }
+
+ CFileItem playItem(groupMember);
+ if (!g_application.PlayMedia(
+ playItem, "", channel->IsRadio() ? PLAYLIST::TYPE_MUSIC : PLAYLIST::TYPE_VIDEO))
+ {
+ CLog::Log(LOGERROR, "ResumeLiveTv could not play channel: {}", channel->ChannelName());
+ return false;
+ }
+ }
+ }
+ else if (paramlow == "reset")
+ {
+ g_application.OnAction(CAction(ACTION_PLAYER_RESET));
+ }
+
+ return 0;
+}
+
+/*! \brief Play currently inserted DVD.
+ * \param params The parameters.
+ * \details params[0] = "restart" to restart from resume point (optional).
+ */
+static int PlayDVD(const std::vector<std::string>& params)
+{
+#ifdef HAS_DVD_DRIVE
+ bool restart = false;
+ if (!params.empty() && StringUtils::EqualsNoCase(params[0], "restart"))
+ restart = true;
+ MEDIA_DETECT::CAutorun::PlayDisc(CServiceBroker::GetMediaManager().GetDiscPath(), true, restart);
+#endif
+
+ return 0;
+}
+
+namespace
+{
+void GetItemsForPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems)
+{
+ if (VIDEO_UTILS::IsItemPlayable(*item))
+ VIDEO_UTILS::GetItemsForPlayList(item, queuedItems);
+ else if (MUSIC_UTILS::IsItemPlayable(*item))
+ MUSIC_UTILS::GetItemsForPlayList(item, queuedItems);
+ else
+ CLog::LogF(LOGERROR, "Unable to get playlist items for {}", item->GetPath());
+}
+
+int PlayOrQueueMedia(const std::vector<std::string>& params, bool forcePlay)
+{
+ // restore to previous window if needed
+ if( CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW ||
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_VIDEO ||
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_FULLSCREEN_GAME ||
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VISUALISATION )
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+
+ // reset screensaver
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+ appPower->WakeUpScreenSaverAndDPMS();
+
+ CFileItem item(params[0], URIUtils::HasSlashAtEnd(params[0], true));
+
+ // at this point the item instance has only the path and the folder flag set. We
+ // need some extended item properties to process resume successfully. Load them.
+ item.LoadDetails();
+
+ // ask if we need to check guisettings to resume
+ bool askToResume = true;
+ int playOffset = 0;
+ bool hasPlayOffset = false;
+ bool playNext = true;
+ for (unsigned int i = 1 ; i < params.size() ; i++)
+ {
+ if (StringUtils::EqualsNoCase(params[i], "isdir"))
+ item.m_bIsFolder = true;
+ else if (params[i] == "1") // set fullscreen or windowed
+ CMediaSettings::GetInstance().SetMediaStartWindowed(true);
+ else if (StringUtils::EqualsNoCase(params[i], "resume"))
+ {
+ // force the item to resume (if applicable)
+ if (VIDEO_UTILS::GetItemResumeInformation(item).isResumable)
+ item.SetStartOffset(STARTOFFSET_RESUME);
+ else
+ item.SetStartOffset(0);
+
+ askToResume = false;
+ }
+ else if (StringUtils::EqualsNoCase(params[i], "noresume"))
+ {
+ // force the item to start at the beginning
+ item.SetStartOffset(0);
+ askToResume = false;
+ }
+ else if (StringUtils::StartsWithNoCase(params[i], "playoffset="))
+ {
+ playOffset = atoi(params[i].substr(11).c_str()) - 1;
+ item.SetProperty("playlist_starting_track", playOffset);
+ hasPlayOffset = true;
+ }
+ else if (StringUtils::StartsWithNoCase(params[i], "playlist_type_hint="))
+ {
+ // Set the playlist type for the playlist file (e.g. STRM)
+ int playlistTypeHint = std::stoi(params[i].substr(19));
+ item.SetProperty("playlist_type_hint", playlistTypeHint);
+ }
+ else if (StringUtils::EqualsNoCase(params[i], "playnext"))
+ {
+ // If app player is currently playing, the queued media shall be played next.
+ playNext = true;
+ }
+ }
+
+ if (!item.m_bIsFolder && item.IsPlugin())
+ item.SetProperty("IsPlayable", true);
+
+ if (askToResume == true)
+ {
+ if (CGUIWindowVideoBase::ShowResumeMenu(item) == false)
+ return false;
+ item.SetProperty("check_resume", false);
+ }
+
+ if (item.m_bIsFolder || item.IsPlayList())
+ {
+ CFileItemList items;
+ GetItemsForPlayList(std::make_shared<CFileItem>(item), items);
+ if (!items.IsEmpty()) // fall through on non expandable playlist
+ {
+ bool containsMusic = false;
+ bool containsVideo = false;
+ for (const auto& i : items)
+ {
+ const bool isVideo = i->IsVideo();
+ containsMusic |= !isVideo;
+ containsVideo |= isVideo;
+
+ if (containsMusic && containsVideo)
+ break;
+ }
+
+ PLAYLIST::Id playlistId = containsVideo ? PLAYLIST::TYPE_VIDEO : PLAYLIST::TYPE_MUSIC;
+ // Mixed playlist item played by music player, mixed content folder has music removed
+ if (containsMusic && containsVideo)
+ {
+ if (item.IsPlayList())
+ playlistId = PLAYLIST::TYPE_MUSIC;
+ else
+ {
+ for (int i = items.Size() - 1; i >= 0; i--) //remove music entries
+ {
+ if (!items[i]->IsVideo())
+ items.Remove(i);
+ }
+ }
+ }
+
+ auto& playlistPlayer = CServiceBroker::GetPlaylistPlayer();
+
+ // Play vs. Queue (+Play)
+ if (forcePlay)
+ {
+ playlistPlayer.ClearPlaylist(playlistId);
+ playlistPlayer.Reset();
+ playlistPlayer.Add(playlistId, items);
+ playlistPlayer.SetCurrentPlaylist(playlistId);
+ playlistPlayer.Play(playOffset, "");
+ }
+ else
+ {
+ const int oldSize = playlistPlayer.GetPlaylist(playlistId).size();
+
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (playNext)
+ {
+ if (appPlayer->IsPlaying())
+ playlistPlayer.Insert(playlistId, items, playlistPlayer.GetCurrentSong() + 1);
+ else
+ playlistPlayer.Add(playlistId, items);
+ }
+ else
+ {
+ playlistPlayer.Add(playlistId, items);
+ }
+
+ if (items.Size() && !appPlayer->IsPlaying())
+ {
+ playlistPlayer.SetCurrentPlaylist(playlistId);
+
+ if (containsMusic)
+ {
+ // video does not auto play on queue like music
+ playlistPlayer.Play(hasPlayOffset ? playOffset : oldSize, "");
+ }
+ }
+ }
+
+ return 0;
+ }
+ }
+
+ if (forcePlay)
+ {
+ if (item.HasVideoInfoTag() && item.GetStartOffset() == STARTOFFSET_RESUME)
+ {
+ const CBookmark bookmark = item.GetVideoInfoTag()->GetResumePoint();
+ if (bookmark.IsSet())
+ item.SetStartOffset(CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds));
+ }
+
+ if ((item.IsAudio() || item.IsVideo()) && !item.IsSmartPlayList())
+ {
+ if (!item.HasProperty("playlist_type_hint"))
+ item.SetProperty("playlist_type_hint", PLAYLIST::TYPE_MUSIC);
+
+ CServiceBroker::GetPlaylistPlayer().Play(std::make_shared<CFileItem>(item), "");
+ }
+ else
+ {
+ g_application.PlayMedia(item, "", PLAYLIST::TYPE_NONE);
+ }
+ }
+
+ return 0;
+}
+
+/*! \brief Start playback of media.
+ * \param params The parameters.
+ * \details params[0] = URL to media to play (optional).
+ * params[1,...] = "isdir" if media is a directory (optional).
+ * params[1,...] = "1" to start playback in fullscreen (optional).
+ * params[1,...] = "resume" to force resuming (optional).
+ * params[1,...] = "noresume" to force not resuming (optional).
+ * params[1,...] = "playoffset=<offset>" to start playback from a given position in a playlist (optional).
+ * params[1,...] = "playlist_type_hint=<id>" to set the playlist type if a playlist file (e.g. STRM) is played (optional),
+ * for <id> value refer to PLAYLIST::TYPE_MUSIC / PLAYLIST::TYPE_VIDEO values, if not set will fallback to music playlist.
+ */
+int PlayMedia(const std::vector<std::string>& params)
+{
+ return PlayOrQueueMedia(params, true);
+}
+
+/*! \brief Queue media in the video or music playlist, according to type of media items. If both audio and video items are contained, queue to video
+ * playlist. Start playback at requested position if player is not playing.
+ * \param params The parameters.
+ * \details params[0] = URL of media to queue.
+ * params[1,...] = "isdir" if media is a directory (optional).
+ * params[1,...] = "1" to start playback in fullscreen (optional).
+ * params[1,...] = "resume" to force resuming (optional).
+ * params[1,...] = "noresume" to force not resuming (optional).
+ * params[1,...] = "playoffset=<offset>" to start playback from a given position in a playlist (optional).
+ * params[1,...] = "playlist_type_hint=<id>" to set the playlist type if a playlist file (e.g. STRM) is played (optional),
+ * for <id> value refer to PLAYLIST::TYPE_MUSIC / PLAYLIST::TYPE_VIDEO values, if not set will fallback to music playlist.
+ * params[1,...] = "playnext" if player is currently playing, to play the media right after the currently playing item. If player is not
+ * playing, append media to current playlist (optional).
+ */
+int QueueMedia(const std::vector<std::string>& params)
+{
+ return PlayOrQueueMedia(params, false);
+}
+
+} // unnamed namespace
+
+/*! \brief Start playback with a given playback core.
+ * \param params The parameters.
+ * \details params[0] = Name of playback core.
+ */
+static int PlayWith(const std::vector<std::string>& params)
+{
+ g_application.OnAction(CAction(ACTION_PLAYER_PLAY, params[0]));
+
+ return 0;
+}
+
+/*! \brief Seek in currently playing media.
+ * \param params The parameters.
+ * \details params[0] = Number of seconds to seek.
+ */
+static int Seek(const std::vector<std::string>& params)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ appPlayer->GetSeekHandler().SeekSeconds(atoi(params[0].c_str()));
+
+ return 0;
+}
+
+static int SubtitleShiftUp(const std::vector<std::string>& params)
+{
+ CAction action{ACTION_SUBTITLE_VSHIFT_UP};
+ if (!params.empty() && params[0] == "save")
+ action.SetText("save");
+ CPlayerController::GetInstance().OnAction(action);
+ return 0;
+}
+
+static int SubtitleShiftDown(const std::vector<std::string>& params)
+{
+ CAction action{ACTION_SUBTITLE_VSHIFT_DOWN};
+ if (!params.empty() && params[0] == "save")
+ action.SetText("save");
+ CPlayerController::GetInstance().OnAction(action);
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_12 Player built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`PlaysDisc(parm)`</b>\n
+/// <b>`PlayDVD(param)`</b>(deprecated)
+/// ,
+/// Plays the inserted disc\, like CD\, DVD or Blu-ray\, in the disc drive.
+/// @param[in] param "restart" to restart from resume point (optional)
+/// }
+/// \table_row2_l{
+/// <b>`PlayerControl(control[\,param])`</b>
+/// ,
+/// Allows control of music and videos. <br>
+/// <br>
+/// | Control | Video playback behaviour | Audio playback behaviour | Added in |
+/// |:------------------------|:---------------------------------------|:----------------------------|:------------|
+/// | Play | Play/Pause | Play/Pause | |
+/// | Stop | Stop | Stop | |
+/// | Forward | Fast Forward | Fast Forward | |
+/// | Rewind | Rewind | Rewind | |
+/// | Next | Next chapter or movie in playlists | Next track | |
+/// | Previous | Previous chapter or movie in playlists | Previous track | |
+/// | TempoUp | Increases playback speed | none | Kodi v18 |
+/// | TempoDown | Decreases playback speed | none | Kodi v18 |
+/// | Tempo(n) | Sets playback speed to given value | none | Kodi v19 |
+/// | BigSkipForward | Big Skip Forward | Big Skip Forward | Kodi v15 |
+/// | BigSkipBackward | Big Skip Backward | Big Skip Backward | Kodi v15 |
+/// | SmallSkipForward | Small Skip Forward | Small Skip Forward | Kodi v15 |
+/// | SmallSkipBackward | Small Skip Backward | Small Skip Backward | Kodi v15 |
+/// | SeekPercentage(n) | Seeks to given percentage | Seeks to given percentage | |
+/// | Random * | Toggle Random Playback | Toggle Random Playback | |
+/// | RandomOn | Sets 'Random' to 'on' | Sets 'Random' to 'on' | |
+/// | RandomOff | Sets 'Random' to 'off' | Sets 'Random' to 'off' | |
+/// | Repeat * | Cycles through repeat modes | Cycles through repeat modes | |
+/// | RepeatOne | Repeats a single video | Repeats a single track | |
+/// | RepeatAll | Repeat all videos in a list | Repeats all tracks in a list| |
+/// | RepeatOff | Sets 'Repeat' to 'off' | Sets 'Repeat' to 'off' | |
+/// | Partymode(music) ** | none | Toggles music partymode | |
+/// | Partymode(video) ** | Toggles video partymode | none | |
+/// | Partymode(path to .xsp) | Partymode for *.xsp-file | Partymode for *.xsp-file | |
+/// | ShowVideoMenu | Shows the DVD/BR menu if available | none | |
+/// | FrameAdvance(n) *** | Advance video by _n_ frames | none | Kodi v18 |
+/// | SubtitleShiftUp(save) | Shift up the subtitle position, add "save" to save the change permanently | none | Kodi v20 |
+/// | SubtitleShiftDown(save) | Shift down the subtitle position, add "save" to save the change permanently | none | Kodi v20 |
+/// <br>
+/// '*' = For these controls\, the PlayerControl built-in function can make use of the 'notify'-parameter. For example: PlayerControl(random\, notify)
+/// <br>
+/// '**' = If no argument is given for 'partymode'\, the control will default to music.
+/// <br>
+/// '***' = This only works if the player is paused.
+/// <br>
+/// @param[in] control Control to execute.
+/// @param[in] param "notify" to notify user (optional\, certain controls).
+///
+/// @note 'TempoUp' or 'TempoDown' only works if "Sync playback to display" is enabled.
+/// @note 'Next' will behave differently while using video playlists. In those\, chapters will be ignored and the next movie will be played.
+/// }
+/// \table_row2_l{
+/// <b>`Playlist.Clear`</b>
+/// ,
+/// Clear the current playlist
+/// @param[in] (ignored)
+/// }
+/// \table_row2_l{
+/// <b>`Playlist.PlayOffset(positionType[\,position])`</b>
+/// ,
+/// Start playing from a particular offset in the playlist
+/// @param[in] positionType Position in playlist or playlist type.
+/// @param[in] position Position in playlist if params[0] is playlist type (optional).
+/// }
+/// \table_row2_l{
+/// <b>`PlayMedia(media[\,isdir][\,1]\,[playoffset=xx])`</b>
+/// ,
+/// Plays the media. This can be a playlist\, music\, or video file\, directory\,
+/// plugin or an Url. The optional parameter "\,isdir" can be used for playing
+/// a directory. "\,1" will start the media without switching to fullscreen.
+/// If media is a playlist\, you can use playoffset=xx where xx is
+/// the position to start playback from.
+/// @param[in] media URL to media to play (optional).
+/// @param[in] isdir Set "isdir" if media is a directory (optional).
+/// @param[in] windowed Set "1" to start playback without switching to fullscreen (optional).
+/// @param[in] resume Set "resume" to force resuming (optional).
+/// @param[in] noresume Set "noresume" to force not resuming (optional).
+/// @param[in] playeroffset Set "playoffset=<offset>" to start playback from a given position in a playlist (optional).
+/// }
+/// \table_row2_l{
+/// <b>`PlayWith(core)`</b>
+/// ,
+/// Play the selected item with the specified player core.
+/// @param[in] core Name of playback core.
+/// }
+/// \table_row2_l{
+/// <b>`Seek(seconds)`</b>
+/// ,
+/// Seeks to the specified relative amount of seconds within the current
+/// playing media. A negative value will seek backward and a positive value forward.
+/// @param[in] seconds Number of seconds to seek.
+/// }
+/// \table_row2_l{
+/// <b>`QueueMedia(media[\,isdir][\,1][\,playnext]\,[playoffset=xx])`</b>
+/// \anchor Builtin_QueueMedia,
+/// Queues the given media. This can be a playlist\, music\, or video file\, directory\,
+/// plugin or an Url. The optional parameter "\,isdir" can be used for playing
+/// a directory. "\,1" will start the media without switching to fullscreen.
+/// If media is a playlist\, you can use playoffset=xx where xx is
+/// the position to start playback from.
+/// @param[in] media URL of media to queue.
+/// @param[in] isdir Set "isdir" if media is a directory (optional).
+/// @param[in] 1 Set "1" to start playback without switching to fullscreen (optional).
+/// @param[in] resume Set "resume" to force resuming (optional).
+/// @param[in] noresume Set "noresume" to force not resuming (optional).
+/// @param[in] playeroffset Set "playoffset=<offset>" to start playback from a given position in a playlist (optional).
+/// @param[in] playnext Set "playnext" to play the media right after the currently playing item, if player is currently
+/// playing. If player is not playing, append media to current playlist (optional).
+/// <p><hr>
+/// @skinning_v20 **[New builtin]** \link Builtin_QueueMedia `QueueMedia(media[\,isdir][\,1][\,playnext]\,[playoffset=xx])`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+
+// clang-format off
+CBuiltins::CommandMap CPlayerBuiltins::GetOperations() const
+{
+ return {
+ {"playdisc", {"Plays the inserted disc, like CD, DVD or Blu-ray, in the disc drive.", 0, PlayDVD}},
+ {"playdvd", {"Plays the inserted disc, like CD, DVD or Blu-ray, in the disc drive.", 0, PlayDVD}},
+ {"playlist.clear", {"Clear the current playlist", 0, ClearPlaylist}},
+ {"playlist.playoffset", {"Start playing from a particular offset in the playlist", 1, PlayOffset}},
+ {"playercontrol", {"Control the music or video player", 1, PlayerControl}},
+ {"playmedia", {"Play the specified media file (or playlist)", 1, PlayMedia}},
+ {"queuemedia", {"Queue the specified media in video or music playlist", 1, QueueMedia}},
+ {"playwith", {"Play the selected item with the specified core", 1, PlayWith}},
+ {"seek", {"Performs a seek in seconds on the current playing media file", 1, Seek}},
+ {"subtitleshiftup", {"Shift up the subtitle position", 0, SubtitleShiftUp}},
+ {"subtitleshiftdown", {"Shift down the subtitle position", 0, SubtitleShiftDown}},
+ };
+}
+// clang-format on
diff --git a/xbmc/interfaces/builtins/PlayerBuiltins.h b/xbmc/interfaces/builtins/PlayerBuiltins.h
new file mode 100644
index 0000000..efa9474
--- /dev/null
+++ b/xbmc/interfaces/builtins/PlayerBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing player related built-in commands.
+class CPlayerBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/ProfileBuiltins.cpp b/xbmc/interfaces/builtins/ProfileBuiltins.cpp
new file mode 100644
index 0000000..448f821
--- /dev/null
+++ b/xbmc/interfaces/builtins/ProfileBuiltins.cpp
@@ -0,0 +1,133 @@
+/*
+ * 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 "ProfileBuiltins.h"
+
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "favourites/FavouritesService.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+
+/*! \brief Load a profile.
+ * \param params The parameters.
+ * \details params[0] = The profile name.
+ * params[1] = "prompt" to allow unlocking dialogs (optional)
+ */
+static int LoadProfile(const std::vector<std::string>& params)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ int index = profileManager->GetProfileIndex(params[0]);
+ bool prompt = (params.size() == 2 && StringUtils::EqualsNoCase(params[1], "prompt"));
+ bool bCanceled;
+ if (index >= 0
+ && (profileManager->GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE
+ || g_passwordManager.IsProfileLockUnlocked(index,bCanceled,prompt)))
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_LOADPROFILE, index);
+ }
+
+ return 0;
+}
+
+/*! \brief Log off currently signed in profile.
+ * \param params (ignored)
+ */
+static int LogOff(const std::vector<std::string>& params)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ profileManager->LogOff();
+
+ return 0;
+}
+
+/*! \brief Toggle master mode.
+ * \param params (ignored)
+ */
+static int MasterMode(const std::vector<std::string>& params)
+{
+ if (g_passwordManager.bMasterUser)
+ {
+ g_passwordManager.bMasterUser = false;
+ g_passwordManager.LockSources(true);
+
+ // master mode turned OFF => refresh favourites due to possible visibility changes
+ CServiceBroker::GetFavouritesService().RefreshFavourites();
+
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, g_localizeStrings.Get(20052),g_localizeStrings.Get(20053));
+ }
+ else if (g_passwordManager.IsMasterLockUnlocked(true)) // prompt user for code
+ {
+ g_passwordManager.LockSources(false);
+ g_passwordManager.bMasterUser = true;
+
+ // master mode turned ON => refresh favourites due to possible visibility changes
+ CServiceBroker::GetFavouritesService().RefreshFavourites();
+
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, g_localizeStrings.Get(20052),g_localizeStrings.Get(20054));
+ }
+
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ return 0;
+}
+
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_13 Profile built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`LoadProfile(profilename\,[prompt])`</b>
+/// ,
+/// Load the specified profile. If prompt is not specified\, and a password
+/// would be required for the requested profile\, this command will silently
+/// fail. If prompt is specified and a password is required\, a password
+/// dialog will be shown.
+/// @param[in] profilename The profile name.
+/// @param[in] prompt Add "prompt" to allow unlocking dialogs (optional)
+/// }
+/// \table_row2_l{
+/// <b>`Mastermode`</b>
+/// ,
+/// Runs Kodi in master mode
+/// }
+/// \table_row2_l{
+/// <b>`System.LogOff`</b>
+/// ,
+/// Log off current user.
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CProfileBuiltins::GetOperations() const
+{
+ return {
+ {"loadprofile", {"Load the specified profile (note; if locks are active it won't work)", 1, LoadProfile}},
+ {"mastermode", {"Control master mode", 0, MasterMode}},
+ {"system.logoff", {"Log off current user", 0, LogOff}}
+ };
+}
diff --git a/xbmc/interfaces/builtins/ProfileBuiltins.h b/xbmc/interfaces/builtins/ProfileBuiltins.h
new file mode 100644
index 0000000..e30c1ee
--- /dev/null
+++ b/xbmc/interfaces/builtins/ProfileBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing profile related built-in commands.
+class CProfileBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/SkinBuiltins.cpp b/xbmc/interfaces/builtins/SkinBuiltins.cpp
new file mode 100644
index 0000000..5b2b4c2
--- /dev/null
+++ b/xbmc/interfaces/builtins/SkinBuiltins.cpp
@@ -0,0 +1,687 @@
+/*
+ * 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 "SkinBuiltins.h"
+
+#include "MediaSource.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationSkinHandling.h"
+#include "dialogs/GUIDialogColorPicker.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SkinSettings.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace ADDON;
+
+/*! \brief Reload current skin.
+ * \param params The parameters.
+ * \details params[0] = "confirm" to show a confirmation dialog (optional).
+ */
+static int ReloadSkin(const std::vector<std::string>& params)
+{
+ // Reload the skin
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appSkin = components.GetComponent<CApplicationSkinHandling>();
+ appSkin->ReloadSkin(!params.empty() && StringUtils::EqualsNoCase(params[0], "confirm"));
+
+ return 0;
+}
+
+/*! \brief Unload current skin.
+ * \param params (ignored)
+ */
+static int UnloadSkin(const std::vector<std::string>& params)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appSkin = components.GetComponent<CApplicationSkinHandling>();
+ appSkin->UnloadSkin();
+
+ return 0;
+}
+
+/*! \brief Toggle a skin bool setting.
+ * \param params The parameters.
+ * \details params[0] = Skin setting to toggle.
+ */
+static int ToggleSetting(const std::vector<std::string>& params)
+{
+ int setting = CSkinSettings::GetInstance().TranslateBool(params[0]);
+ CSkinSettings::GetInstance().SetBool(setting, !CSkinSettings::GetInstance().GetBool(setting));
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ return 0;
+}
+
+/*! \brief Set an add-on type skin setting.
+ * \param params The parameters.
+ * \details params[0] = Skin setting to store result in.
+ * params[1,...] = Add-on types to allow selecting.
+ */
+static int SetAddon(const std::vector<std::string>& params)
+{
+ int string = CSkinSettings::GetInstance().TranslateString(params[0]);
+ std::vector<ADDON::AddonType> types;
+ for (unsigned int i = 1 ; i < params.size() ; i++)
+ {
+ ADDON::AddonType type = CAddonInfo::TranslateType(params[i]);
+ if (type != AddonType::UNKNOWN)
+ types.push_back(type);
+ }
+ std::string result;
+ if (!types.empty() && CGUIWindowAddonBrowser::SelectAddonID(types, result, true) == 1)
+ {
+ CSkinSettings::GetInstance().SetString(string, result);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ }
+
+ return 0;
+}
+
+/*! \brief Select and set a skin bool setting.
+ * \param params The parameters.
+ * \details params[0] = Names of skin settings.
+ */
+static int SelectBool(const std::vector<std::string>& params)
+{
+ std::vector<std::pair<std::string, std::string>> settings;
+
+ CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ pDlgSelect->Reset();
+ pDlgSelect->SetHeading(CVariant{g_localizeStrings.Get(atoi(params[0].c_str()))});
+
+ for (unsigned int i = 1 ; i < params.size() ; i++)
+ {
+ if (params[i].find('|') != std::string::npos)
+ {
+ std::vector<std::string> values = StringUtils::Split(params[i], '|');
+ std::string label = g_localizeStrings.Get(atoi(values[0].c_str()));
+ settings.emplace_back(label, values[1].c_str());
+ pDlgSelect->Add(label);
+ }
+ }
+
+ pDlgSelect->Open();
+
+ if(pDlgSelect->IsConfirmed())
+ {
+ unsigned int iItem = pDlgSelect->GetSelectedItem();
+
+ for (unsigned int i = 0 ; i < settings.size() ; i++)
+ {
+ std::string item = settings[i].second;
+ int setting = CSkinSettings::GetInstance().TranslateBool(item);
+ if (i == iItem)
+ CSkinSettings::GetInstance().SetBool(setting, true);
+ else
+ CSkinSettings::GetInstance().SetBool(setting, false);
+ }
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ }
+
+ return 0;
+}
+
+/*! \brief Set a skin bool setting.
+ * \param params The parameters.
+ * \details params[0] = Name of skin setting.
+ * params[1] = Value to set ("false", or "true") (optional).
+ */
+static int SetBool(const std::vector<std::string>& params)
+{
+ if (params.size() > 1)
+ {
+ int string = CSkinSettings::GetInstance().TranslateBool(params[0]);
+ CSkinSettings::GetInstance().SetBool(string, StringUtils::EqualsNoCase(params[1], "true"));
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ return 0;
+ }
+ // default is to set it to true
+ int setting = CSkinSettings::GetInstance().TranslateBool(params[0]);
+ CSkinSettings::GetInstance().SetBool(setting, true);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ return 0;
+}
+
+/*! \brief Set a numeric skin setting.
+ * \param params The parameters.
+ * \details params[0] = Name of skin setting.
+ */
+static int SetNumeric(const std::vector<std::string>& params)
+{
+ int string = CSkinSettings::GetInstance().TranslateString(params[0]);
+ std::string value = CSkinSettings::GetInstance().GetString(string);
+ if (CGUIDialogNumeric::ShowAndGetNumber(value, g_localizeStrings.Get(611)))
+ CSkinSettings::GetInstance().SetString(string, value);
+
+ return 0;
+}
+
+/*! \brief Set a path skin setting.
+ * \param params The parameters.
+ * \details params[0] = Name of skin setting.
+ * params[1] = Extra URL to allow selection from (optional).
+ */
+static int SetPath(const std::vector<std::string>& params)
+{
+ int string = CSkinSettings::GetInstance().TranslateString(params[0]);
+ std::string value = CSkinSettings::GetInstance().GetString(string);
+ VECSOURCES localShares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(localShares);
+ CServiceBroker::GetMediaManager().GetNetworkLocations(localShares);
+ if (params.size() > 1)
+ {
+ value = params[1];
+ URIUtils::AddSlashAtEnd(value);
+ bool bIsSource;
+ if (CUtil::GetMatchingSource(value,localShares,bIsSource) < 0) // path is outside shares - add it as a separate one
+ {
+ CMediaSource share;
+ share.strName = g_localizeStrings.Get(13278);
+ share.strPath = value;
+ localShares.push_back(share);
+ }
+ }
+
+ if (CGUIDialogFileBrowser::ShowAndGetDirectory(localShares, g_localizeStrings.Get(657), value))
+ CSkinSettings::GetInstance().SetString(string, value);
+
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ return 0;
+}
+
+/*! \brief Set a skin file setting.
+ * \param params The parameters.
+ * \details params[0] = Name of skin setting.
+ * params[1] = File mask or add-on type (optional).
+ * params[2] = Extra URL to allow selection from or
+ * content type if mask is an addon-on type (optional).
+ */
+static int SetFile(const std::vector<std::string>& params)
+{
+ int string = CSkinSettings::GetInstance().TranslateString(params[0]);
+ std::string value = CSkinSettings::GetInstance().GetString(string);
+ VECSOURCES localShares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(localShares);
+
+ // Note. can only browse one addon type from here
+ // if browsing for addons, required param[1] is addontype string, with optional param[2]
+ // as contenttype string see IAddon.h & ADDON::TranslateXX
+ std::string strMask = (params.size() > 1) ? params[1] : "";
+ StringUtils::ToLower(strMask);
+ ADDON::AddonType type;
+ if ((type = CAddonInfo::TranslateType(strMask)) != AddonType::UNKNOWN)
+ {
+ CURL url;
+ url.SetProtocol("addons");
+ url.SetHostName("enabled");
+ url.SetFileName(strMask+"/");
+ localShares.clear();
+ std::string content = (params.size() > 2) ? params[2] : "";
+ StringUtils::ToLower(content);
+ url.SetPassword(content);
+ std::string strMask;
+ if (type == AddonType::SCRIPT)
+ strMask = ".py";
+ std::string replace;
+ if (CGUIDialogFileBrowser::ShowAndGetFile(url.Get(), strMask, CAddonInfo::TranslateType(type, true), replace, true, true, true))
+ {
+ if (StringUtils::StartsWithNoCase(replace, "addons://"))
+ CSkinSettings::GetInstance().SetString(string, URIUtils::GetFileName(replace));
+ else
+ CSkinSettings::GetInstance().SetString(string, replace);
+ }
+ }
+ else
+ {
+ if (params.size() > 2)
+ {
+ value = params[2];
+ URIUtils::AddSlashAtEnd(value);
+ bool bIsSource;
+ if (CUtil::GetMatchingSource(value,localShares,bIsSource) < 0) // path is outside shares - add it as a separate one
+ {
+ CMediaSource share;
+ share.strName = g_localizeStrings.Get(13278);
+ share.strPath = value;
+ localShares.push_back(share);
+ }
+ }
+ if (CGUIDialogFileBrowser::ShowAndGetFile(localShares, strMask, g_localizeStrings.Get(1033), value))
+ CSkinSettings::GetInstance().SetString(string, value);
+ }
+
+ return 0;
+}
+
+/*! \brief Set a skin image setting.
+ * \param params The parameters.
+ * \details params[0] = Name of skin setting.
+ * params[1] = Extra URL to allow selection from (optional).
+ */
+static int SetImage(const std::vector<std::string>& params)
+{
+ int string = CSkinSettings::GetInstance().TranslateString(params[0]);
+ std::string value = CSkinSettings::GetInstance().GetString(string);
+ VECSOURCES localShares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(localShares);
+ if (params.size() > 1)
+ {
+ value = params[1];
+ URIUtils::AddSlashAtEnd(value);
+ bool bIsSource;
+ if (CUtil::GetMatchingSource(value,localShares,bIsSource) < 0) // path is outside shares - add it as a separate one
+ {
+ CMediaSource share;
+ share.strName = g_localizeStrings.Get(13278);
+ share.strPath = value;
+ localShares.push_back(share);
+ }
+ }
+ if (CGUIDialogFileBrowser::ShowAndGetImage(localShares, g_localizeStrings.Get(1030), value))
+ CSkinSettings::GetInstance().SetString(string, value);
+
+ return 0;
+}
+
+/*! \brief Set a skin color setting.
+ * \param params The parameters.
+ * \details params[0] = Name of skin setting.
+ * params[1] = Dialog header text.
+ * params[2] = Hex value of the preselected color (optional).
+ * params[3] = XML file containing color definitions (optional).
+ */
+static int SetColor(const std::vector<std::string>& params)
+{
+ int string = CSkinSettings::GetInstance().TranslateString(params[0]);
+ std::string value = CSkinSettings::GetInstance().GetString(string);
+
+ if (value.empty() && params.size() > 2)
+ {
+ value = params[2];
+ }
+
+ CGUIDialogColorPicker* pDlgColorPicker =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogColorPicker>(
+ WINDOW_DIALOG_COLOR_PICKER);
+ pDlgColorPicker->Reset();
+ pDlgColorPicker->SetHeading(CVariant{g_localizeStrings.Get(atoi(params[1].c_str()))});
+
+ if (params.size() > 3)
+ {
+ pDlgColorPicker->LoadColors(params[3]);
+ }
+ else
+ {
+ pDlgColorPicker->LoadColors();
+ }
+
+ pDlgColorPicker->SetSelectedColor(value);
+
+ pDlgColorPicker->Open();
+
+ if (pDlgColorPicker->IsConfirmed())
+ {
+ value = pDlgColorPicker->GetSelectedColor();
+ CSkinSettings::GetInstance().SetString(string, value);
+ }
+
+ return 0;
+}
+
+/*! \brief Set a string skin setting.
+ * \param params The parameters.
+ * \details params[0] = Name of skin setting.
+ * params[1] = Value of skin setting (optional).
+ */
+static int SetString(const std::vector<std::string>& params)
+{
+ // break the parameter up if necessary
+ int string = 0;
+ if (params.size() > 1)
+ {
+ string = CSkinSettings::GetInstance().TranslateString(params[0]);
+ CSkinSettings::GetInstance().SetString(string, params[1]);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ return 0;
+ }
+ else
+ string = CSkinSettings::GetInstance().TranslateString(params[0]);
+
+ std::string value = CSkinSettings::GetInstance().GetString(string);
+ if (CGUIKeyboardFactory::ShowAndGetInput(value, CVariant{g_localizeStrings.Get(1029)}, true))
+ CSkinSettings::GetInstance().SetString(string, value);
+
+ return 0;
+}
+
+/*! \brief Select skin theme.
+ * \param params The parameters.
+ * \details params[0] = 0 or 1 to increase theme, -1 to decrease.
+ */
+static int SetTheme(const std::vector<std::string>& params)
+{
+ // enumerate themes
+ std::vector<std::string> vecTheme;
+ CUtil::GetSkinThemes(vecTheme);
+
+ int iTheme = -1;
+
+ // find current theme
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ const std::string strTheme = settings->GetString(CSettings::SETTING_LOOKANDFEEL_SKINTHEME);
+ if (!StringUtils::EqualsNoCase(strTheme, "SKINDEFAULT"))
+ {
+ for (size_t i=0;i<vecTheme.size();++i)
+ {
+ std::string strTmpTheme(strTheme);
+ URIUtils::RemoveExtension(strTmpTheme);
+ if (StringUtils::EqualsNoCase(vecTheme[i], strTmpTheme))
+ {
+ iTheme=i;
+ break;
+ }
+ }
+ }
+
+ int iParam = atoi(params[0].c_str());
+ if (iParam == 0 || iParam == 1)
+ iTheme++;
+ else if (iParam == -1)
+ iTheme--;
+ if (iTheme > (int)vecTheme.size()-1)
+ iTheme = -1;
+ if (iTheme < -1)
+ iTheme = vecTheme.size()-1;
+
+ std::string strSkinTheme = "SKINDEFAULT";
+ if (iTheme != -1 && iTheme < (int)vecTheme.size())
+ strSkinTheme = vecTheme[iTheme];
+
+ settings->SetString(CSettings::SETTING_LOOKANDFEEL_SKINTHEME, strSkinTheme);
+ // also set the default color theme
+ std::string colorTheme(URIUtils::ReplaceExtension(strSkinTheme, ".xml"));
+ if (StringUtils::EqualsNoCase(colorTheme, "Textures.xml"))
+ colorTheme = "defaults.xml";
+ settings->SetString(CSettings::SETTING_LOOKANDFEEL_SKINCOLORS, colorTheme);
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appSkin = components.GetComponent<CApplicationSkinHandling>();
+ appSkin->ReloadSkin();
+
+ return 0;
+}
+
+/*! \brief Reset a skin setting.
+ * \param params The parameters.
+ * \details params[0] = Name of setting to reset.
+ */
+static int SkinReset(const std::vector<std::string>& params)
+{
+ CSkinSettings::GetInstance().Reset(params[0]);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ return 0;
+}
+
+/*! \brief Reset all skin settings.
+ * \param params (ignored)
+ */
+static int SkinResetAll(const std::vector<std::string>& params)
+{
+ CSkinSettings::GetInstance().Reset();
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ return 0;
+}
+
+/*! \brief Toggle skin debug.
+ * \param params (ignored)
+ */
+static int SkinDebug(const std::vector<std::string>& params)
+{
+ g_SkinInfo->ToggleDebug();
+
+ return 0;
+}
+
+/*! \brief Starts a given skin timer
+ * \param params The parameters.
+ * \details params[0] = Name of the timer.
+ * \return -1 in case of error, 0 in case of success
+ */
+static int SkinTimerStart(const std::vector<std::string>& params)
+{
+ if (params.empty())
+ {
+ return -1;
+ }
+
+ g_SkinInfo->TimerStart(params[0]);
+ return 0;
+}
+
+/*! \brief Stops a given skin timer
+ * \param params The parameters.
+ * \details params[0] = Name of the timer.
+ * \return -1 in case of error, 0 in case of success
+ */
+static int SkinTimerStop(const std::vector<std::string>& params)
+{
+ if (params.empty())
+ {
+ return -1;
+ }
+
+ g_SkinInfo->TimerStop(params[0]);
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_14 Skin built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`ReloadSkin(reload)`</b>
+/// ,
+/// Reloads the current skin – useful for Skinners to use after they upload
+/// modified skin files (saves power cycling)
+/// @param[in] reload <b>"confirm"</b> to show a confirmation dialog (optional).
+/// }
+/// \table_row2_l{
+/// <b>`UnloadSkin()`</b>
+/// ,
+/// Unloads the current skin
+/// }
+/// \table_row2_l{
+/// <b>`Skin.Reset(setting)`</b>
+/// ,
+/// Resets the skin `setting`. If `setting` is a bool setting (i.e. set via
+/// `SetBool` or `ToggleSetting`) then the setting is reset to false. If
+/// `setting` is a string (Set via <c>SetString</c>\, <c>SetImage</c> or
+/// <c>SetPath</c>) then it is set to empty.
+/// @param[in] setting Name of setting to reset.
+/// }
+/// \table_row2_l{
+/// <b>`Skin.ResetSettings()`</b>
+/// ,
+/// Resets all the above skin settings to their defaults (toggles all set to
+/// false\, strings all set to empty.)
+/// }
+/// \table_row2_l{
+/// <b>`Skin.SetAddon(string\,type)`</b>
+/// ,
+/// Pops up a select dialog and allows the user to select an add-on of the
+/// given type to be used elsewhere in the skin via the info tag
+/// `Skin.String(string)`. The most common types are xbmc.addon.video\,
+/// xbmc.addon.audio\, xbmc.addon.image and xbmc.addon.executable.
+/// @param[in] string[0] Skin setting to store result in.
+/// @param[in] type[1\,...] Add-on types to allow selecting.
+/// }
+/// \table_row2_l{
+/// <b>`Skin.SetBool(setting[\,value])`</b>
+/// \anchor Skin_SetBool,
+/// Sets the skin `setting` to true\, for use with the conditional visibility
+/// tags containing \link Skin_HasSetting `Skin.HasSetting(setting)`\endlink. The settings are saved
+/// per-skin in settings.xml just like all the other Kodi settings.
+/// @param[in] setting Name of skin setting.
+/// @param[in] value Value to set ("false"\, or "true") (optional).
+/// }
+/// \table_row2_l{
+/// <b>`Skin.SetFile(string\,mask\,folderpath)`</b>
+/// ,
+/// \" minus quotes. If the folderpath parameter is set the file browser will
+/// start in that folder.
+/// @param[in] string Name of skin setting.
+/// @param[in] mask File mask or add-on type (optional).
+/// @param[in] folderpath Extra URL to allow selection from or.
+/// content type if mask is an addon-on type (optional).
+/// }
+/// \table_row2_l{
+/// <b>`Skin.SetImage(string[\,url])`</b>
+/// ,
+/// Pops up a file browser and allows the user to select an image file to be
+/// used in an image control elsewhere in the skin via the info tag
+/// `Skin.String(string)`. If the value parameter is specified\, then the
+/// file browser dialog does not pop up\, and the image path is set directly.
+/// The path option allows you to open the file browser in the specified
+/// folder.
+/// @param[in] string Name of skin setting.
+/// @param[in] url Extra URL to allow selection from (optional).
+/// }
+/// \table_row2_l{
+/// <b>`Skin.SetColor(string\,header[\,colorfile\,selectedcolor])`</b>
+/// \anchor Builtin_SetColor,
+/// Pops up a color selection dialog and allows the user to select a color to be
+/// used to define the color of a label control or as a colordiffuse value for a texture
+/// elsewhere in the skin via the info tag `Skin.String(string)`.
+/// Skinners can optionally set the color that needs to be preselected in the
+/// dialog by specifying the hex value of this color.
+/// Also optionally\, skinners can include their own color definition file. If not specified\,
+/// the default colorfile included with Kodi will be used.
+/// @param[in] string Name of skin setting.
+/// @param[in] string Dialog header text.
+/// @param[in] string Hex value of the color to preselect (optional)\,
+/// example: FF00FF00.
+/// @param[in] string Filepath of the color definition file (optional).
+/// <p><hr>
+/// @skinning_v20 **[New builtin]** \link Builtin_SetColor `SetColor(string\,header[\,colorfile\,selectedcolor])`\endlink
+/// <p>
+/// }
+/// \table_row2_l{
+/// <b>`Skin.SetNumeric(numeric[\,value])`</b>
+/// \anchor Skin_SetNumeric,
+/// Pops up a keyboard dialog and allows the user to input a numerical.
+/// @param[in] numeric Name of skin setting.
+/// @param[in] value Value of skin setting (optional).
+/// }
+/// \table_row2_l{
+/// <b>`Skin.SetPath(string[\,value])`</b>
+/// ,
+/// Pops up a folder browser and allows the user to select a folder of images
+/// to be used in a multi image control else where in the skin via the info
+/// tag `Skin.String(string)`. If the value parameter is specified\, then the
+/// file browser dialog does not pop up\, and the path is set directly.
+/// @param[in] string Name of skin setting.
+/// @param[in] value Extra URL to allow selection from (optional).
+/// }
+/// \table_row2_l{
+/// <b>`Skin.SetString(string[\,value])`</b>
+/// \anchor Skin_SetString,
+/// Pops up a keyboard dialog and allows the user to input a string which can
+/// be used in a label control elsewhere in the skin via the info tag
+/// \link Skin_StringValue `Skin.String(string)`\endlink. The value of the setting
+/// can also be compared to another value using the info bool \link Skin_StringCompare `Skin.String(string\, value)`\endlink.
+/// If the value parameter is specified\, then the
+/// keyboard dialog does not pop up\, and the string is set directly.
+/// @param[in] string Name of skin setting.
+/// @param[in] value Value of skin setting (optional).
+/// }
+/// \table_row2_l{
+/// <b>`Skin.Theme(cycle)`</b>
+/// \anchor Skin_CycleTheme,
+/// Cycles the skin theme. Skin.theme(-1) will go backwards.
+/// @param[in] cycle 0 or 1 to increase theme\, -1 to decrease.
+/// }
+/// \table_row2_l{
+/// <b>`Skin.ToggleDebug`</b>
+/// ,
+/// Toggles skin debug info on/off
+/// }
+/// \table_row2_l{
+/// <b>`Skin.ToggleSetting(setting)`</b>
+/// ,
+/// Toggles the skin `setting` for use with conditional visibility tags
+/// containing `Skin.HasSetting(setting)`.
+/// @param[in] setting Skin setting to toggle
+/// }
+/// \table_row2_l{
+/// <b>`Skin.TimerStart(timer)`</b>
+/// \anchor Builtin_SkinStartTimer,
+/// Starts the timer with name `timer`
+/// @param[in] timer The name of the timer
+/// <p><hr>
+/// @skinning_v20 **[New builtin]** \link Builtin_SkinStartTimer `Skin.TimerStart(timer)`\endlink
+/// <p>
+/// }
+/// \table_row2_l{
+/// <b>`Skin.TimerStop(timer)`</b>
+/// \anchor Builtin_SkinStopTimer,
+/// Stops the timer with name `timer`
+/// @param[in] timer The name of the timer
+/// <p><hr>
+/// @skinning_v20 **[New builtin]** \link Builtin_SkinStopTimer `Skin.TimerStop(timer)`\endlink
+/// <p>
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CSkinBuiltins::GetOperations() const
+{
+ return {{"reloadskin", {"Reload Kodi's skin", 0, ReloadSkin}},
+ {"unloadskin", {"Unload Kodi's skin", 0, UnloadSkin}},
+ {"skin.reset", {"Resets a skin setting to default", 1, SkinReset}},
+ {"skin.resetsettings", {"Resets all skin settings", 0, SkinResetAll}},
+ {"skin.setaddon", {"Prompts and set an addon", 2, SetAddon}},
+ {"skin.selectbool", {"Prompts and set a skin setting", 2, SelectBool}},
+ {"skin.setbool", {"Sets a skin setting on", 1, SetBool}},
+ {"skin.setfile", {"Prompts and sets a file", 1, SetFile}},
+ {"skin.setimage", {"Prompts and sets a skin image", 1, SetImage}},
+ {"skin.setcolor", {"Prompts and sets a skin color", 1, SetColor}},
+ {"skin.setnumeric", {"Prompts and sets numeric input", 1, SetNumeric}},
+ {"skin.setpath", {"Prompts and sets a skin path", 1, SetPath}},
+ {"skin.setstring", {"Prompts and sets skin string", 1, SetString}},
+ {"skin.theme", {"Control skin theme", 1, SetTheme}},
+ {"skin.toggledebug", {"Toggle skin debug", 0, SkinDebug}},
+ {"skin.togglesetting", {"Toggles a skin setting on or off", 1, ToggleSetting}},
+ {"skin.timerstart", {"Starts a given skin timer", 1, SkinTimerStart}},
+ {"skin.timerstop", {"Stops a given skin timer", 1, SkinTimerStop}}};
+}
diff --git a/xbmc/interfaces/builtins/SkinBuiltins.h b/xbmc/interfaces/builtins/SkinBuiltins.h
new file mode 100644
index 0000000..48a4306
--- /dev/null
+++ b/xbmc/interfaces/builtins/SkinBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing skin related built-in commands.
+class CSkinBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/SystemBuiltins.cpp b/xbmc/interfaces/builtins/SystemBuiltins.cpp
new file mode 100644
index 0000000..3a41a5f
--- /dev/null
+++ b/xbmc/interfaces/builtins/SystemBuiltins.cpp
@@ -0,0 +1,267 @@
+/*
+ * 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 "SystemBuiltins.h"
+
+#include "ServiceBroker.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/StringUtils.h"
+
+/*! \brief Execute a system executable.
+ * \param params The parameters.
+ * \details params[0] = The path to the executable.
+ *
+ * Set the template parameter Wait to true to wait for execution exit.
+ */
+ template<int Wait=0>
+static int Exec(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MINIMIZE);
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_OS, Wait, -1, nullptr, params[0]);
+
+ return 0;
+}
+
+/*! \brief Hibernate system.
+ * \param params (ignored)
+ */
+static int Hibernate(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_HIBERNATE);
+
+ return 0;
+}
+
+/*! \brief Inhibit idle shutdown timer.
+ * \param params The parameters.
+ * \details params[0] = "true" to inhibit shutdown timer (optional).
+ */
+static int InhibitIdle(const std::vector<std::string>& params)
+{
+ bool inhibit = (params.size() == 1 && StringUtils::EqualsNoCase(params[0], "true"));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_INHIBITIDLESHUTDOWN, inhibit);
+
+ return 0;
+}
+
+/*! \brief Minimize application.
+ * \param params (ignored)
+ */
+static int Minimize(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MINIMIZE);
+
+ return 0;
+}
+
+/*! \brief Powerdown system.
+ * \param params (ignored)
+ */
+static int Powerdown(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_POWERDOWN);
+
+ return 0;
+}
+
+/*! \brief Quit application.
+ * \param params (ignored)
+ */
+static int Quit(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+
+ return 0;
+}
+
+/*! \brief Reboot system.
+ * \param params (ignored)
+ */
+static int Reboot(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_RESTART);
+
+ return 0;
+}
+
+/*! \brief Restart application.
+ * \param params (ignored)
+ */
+static int RestartApp(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_RESTARTAPP);
+
+ return 0;
+}
+
+/*! \brief Activate screensaver.
+ * \param params (ignored)
+ */
+static int ActivateScreensaver(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_ACTIVATESCREENSAVER);
+
+ return 0;
+}
+
+/*! \brief Reset screensaver.
+ * \param params (ignored)
+ */
+static int ResetScreensaver(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_RESETSCREENSAVER);
+
+ return 0;
+}
+
+/*! \brief Inhibit screensaver.
+ * \param params The parameters.
+ * \details params[0] = "true" to inhibit screensaver (optional).
+ */
+static int InhibitScreenSaver(const std::vector<std::string>& params)
+{
+ bool inhibit = (params.size() == 1 && StringUtils::EqualsNoCase(params[0], "true"));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_INHIBITSCREENSAVER, inhibit);
+
+ return 0;
+}
+
+/*! \brief Shutdown system.
+ * \param params (ignored)
+ */
+static int Shutdown(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SHUTDOWN);
+
+ return 0;
+}
+
+/*! \brief Suspend system.
+ * \param params (ignored)
+ */
+static int Suspend(const std::vector<std::string>& params)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SUSPEND);
+
+ return 0;
+}
+
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_15 System built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`ActivateScreensaver`</b>
+/// ,
+/// Starts the screensaver
+/// }
+/// \table_row2_l{
+/// <b>`InhibitScreensaver(yesNo)`</b>
+/// ,
+/// Inhibit the screensaver
+/// @param[in] yesNo value with "true" or "false" to inhibit or allow screensaver (leaving empty defaults to false)
+/// }
+/// \table_row2_l{
+/// <b>`Hibernate`</b>
+/// ,
+/// Hibernate (S4) the System
+/// }
+/// \table_row2_l{
+/// <b>`InhibitIdleShutdown(true/false)`</b>
+/// ,
+/// Prevent the system to shutdown on idle.
+/// @param[in] value "true" to inhibit shutdown timer (optional).
+/// }
+/// \table_row2_l{
+/// <b>`Minimize`</b>
+/// ,
+/// Minimizes Kodi
+/// }
+/// \table_row2_l{
+/// <b>`Powerdown`</b>
+/// ,
+/// Powerdown system
+/// }
+/// \table_row2_l{
+/// <b>`Quit`</b>
+/// ,
+/// Quits Kodi
+/// }
+/// \table_row2_l{
+/// <b>`Reboot`</b>
+/// ,
+/// Cold reboots the system (power cycle)
+/// }
+/// \table_row2_l{
+/// <b>`Reset`</b>
+/// ,
+/// Reset the system (same as reboot)
+/// }
+/// \table_row2_l{
+/// <b>`Restart`</b>
+/// ,
+/// Restart the system (same as reboot)
+/// }
+/// \table_row2_l{
+/// <b>`RestartApp`</b>
+/// ,
+/// Restarts Kodi (only implemented under Windows and Linux)
+/// }
+/// \table_row2_l{
+/// <b>`ShutDown`</b>
+/// ,
+/// Trigger default Shutdown action defined in System Settings
+/// }
+/// \table_row2_l{
+/// <b>`Suspend`</b>
+/// ,
+/// Suspends (S3 / S1 depending on bios setting) the System
+/// }
+/// \table_row2_l{
+/// <b>`System.Exec(exec)`</b>
+/// ,
+/// Execute shell commands
+/// @param[in] exec The path to the executable
+/// }
+/// \table_row2_l{
+/// <b>`System.ExecWait(exec)`</b>
+/// ,
+/// Execute shell commands and freezes Kodi until shell is closed
+/// @param[in] exec The path to the executable
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CSystemBuiltins::GetOperations() const
+{
+ return {{"activatescreensaver", {"Activate Screensaver", 0, ActivateScreensaver}},
+ {"resetscreensaver", {"Reset Screensaver", 0, ResetScreensaver}},
+ {"hibernate", {"Hibernates the system", 0, Hibernate}},
+ {"inhibitidleshutdown", {"Inhibit idle shutdown", 0, InhibitIdle}},
+ {"inhibitscreensaver", {"Inhibit Screensaver", 0, InhibitScreenSaver}},
+ {"minimize", {"Minimize Kodi", 0, Minimize}},
+ {"powerdown", {"Powerdown system", 0, Powerdown}},
+ {"quit", {"Quit Kodi", 0, Quit}},
+ {"reboot", {"Reboot the system", 0, Reboot}},
+ {"reset", {"Reset the system (same as reboot)", 0, Reboot}},
+ {"restart", {"Restart the system (same as reboot)", 0, Reboot}},
+ {"restartapp", {"Restart Kodi", 0, RestartApp}},
+ {"shutdown", {"Shutdown the system", 0, Shutdown}},
+ {"suspend", {"Suspends the system", 0, Suspend}},
+ {"system.exec", {"Execute shell commands", 1, Exec<0>}},
+ {"system.execwait",
+ {"Execute shell commands and freezes Kodi until shell is closed", 1, Exec<1>}}};
+}
diff --git a/xbmc/interfaces/builtins/SystemBuiltins.h b/xbmc/interfaces/builtins/SystemBuiltins.h
new file mode 100644
index 0000000..ad5c16c
--- /dev/null
+++ b/xbmc/interfaces/builtins/SystemBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing system related built-in commands.
+class CSystemBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/builtins/WeatherBuiltins.cpp b/xbmc/interfaces/builtins/WeatherBuiltins.cpp
new file mode 100644
index 0000000..8769a88
--- /dev/null
+++ b/xbmc/interfaces/builtins/WeatherBuiltins.cpp
@@ -0,0 +1,88 @@
+/*
+ * 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 "WeatherBuiltins.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+
+#include <stdlib.h>
+
+/*! \brief Switch to a given weather location.
+ * \param params The parameters.
+ * \details params[0] = 1, 2 or 3.
+ */
+static int SetLocation(const std::vector<std::string>& params)
+{
+ int loc = atoi(params[0].c_str());
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, 0, 0, loc);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg, WINDOW_WEATHER);
+
+ return 0;
+}
+
+/*! \brief Switch weather location or refresh current.
+ * \param params (ignored)
+ *
+ * The Direction template parameter can be -1 for previous location,
+ * 1 for next location or 0 to refresh current location.
+ */
+ template<int Direction>
+static int SwitchLocation(const std::vector<std::string>& params)
+{
+ CGUIMessage msg(GUI_MSG_MOVE_OFFSET, 0, 0, Direction);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg, WINDOW_WEATHER);
+
+ return 0;
+}
+
+// Note: For new Texts with comma add a "\" before!!! Is used for table text.
+//
+/// \page page_List_of_built_in_functions
+/// \section built_in_functions_16 Weather built-in's
+///
+/// -----------------------------------------------------------------------------
+///
+/// \table_start
+/// \table_h2_l{
+/// Function,
+/// Description }
+/// \table_row2_l{
+/// <b>`Weather.Refresh`</b>
+/// ,
+/// Force weather data refresh.
+/// }
+/// \table_row2_l{
+/// <b>`Weather.LocationNext`</b>
+/// ,
+/// Switch to next weather location
+/// }
+/// \table_row2_l{
+/// <b>`Weather.LocationPrevious`</b>
+/// ,
+/// Switch to previous weather location
+/// }
+/// \table_row2_l{
+/// <b>`Weather.LocationSet`</b>
+/// ,
+/// Switch to given weather location (parameter can be 1-3).
+/// @param[in] parameter 1-3
+/// }
+/// \table_end
+///
+
+CBuiltins::CommandMap CWeatherBuiltins::GetOperations() const
+{
+ return {
+ {"weather.refresh", {"Force weather data refresh", 0, SwitchLocation<0>}},
+ {"weather.locationnext", {"Switch to next weather location", 0, SwitchLocation<1>}},
+ {"weather.locationprevious", {"Switch to previous weather location", 0, SwitchLocation<-1>}},
+ {"weather.locationset", {"Switch to given weather location (parameter can be 1-3)", 1, SetLocation}}
+ };
+}
diff --git a/xbmc/interfaces/builtins/WeatherBuiltins.h b/xbmc/interfaces/builtins/WeatherBuiltins.h
new file mode 100644
index 0000000..6827cfc
--- /dev/null
+++ b/xbmc/interfaces/builtins/WeatherBuiltins.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "Builtins.h"
+
+//! \brief Class providing weather related built-in commands.
+class CWeatherBuiltins
+{
+public:
+ //! \brief Returns the map of operations.
+ CBuiltins::CommandMap GetOperations() const;
+};
diff --git a/xbmc/interfaces/generic/CMakeLists.txt b/xbmc/interfaces/generic/CMakeLists.txt
new file mode 100644
index 0000000..8fd742d
--- /dev/null
+++ b/xbmc/interfaces/generic/CMakeLists.txt
@@ -0,0 +1,15 @@
+set(SOURCES ILanguageInvoker.cpp
+ LanguageInvokerThread.cpp
+ RunningScriptObserver.cpp
+ ScriptInvocationManager.cpp
+ ScriptRunner.cpp)
+
+set(HEADERS ILanguageInvocationHandler.h
+ ILanguageInvoker.h
+ LanguageInvokerThread.h
+ RunningScriptsHandler.h
+ RunningScriptObserver.h
+ ScriptInvocationManager.h
+ ScriptRunner.h)
+
+core_add_library(generic_interface)
diff --git a/xbmc/interfaces/generic/ILanguageInvocationHandler.h b/xbmc/interfaces/generic/ILanguageInvocationHandler.h
new file mode 100644
index 0000000..233e20d
--- /dev/null
+++ b/xbmc/interfaces/generic/ILanguageInvocationHandler.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013-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 ILanguageInvoker;
+
+class ILanguageInvocationHandler
+{
+public:
+ ILanguageInvocationHandler() = default;
+ virtual ~ILanguageInvocationHandler() = default;
+
+ virtual bool Initialize() { return true; }
+ virtual void Process() { }
+ virtual void PulseGlobalEvent() { }
+ virtual void Uninitialize() { }
+
+ virtual bool OnScriptInitialized(ILanguageInvoker *invoker) { return true; }
+ virtual void OnScriptStarted(ILanguageInvoker *invoker) { }
+ virtual void NotifyScriptAborting(ILanguageInvoker *invoker) { }
+ virtual void OnExecutionEnded(ILanguageInvoker* invoker) {}
+ virtual void OnScriptFinalized(ILanguageInvoker *invoker) { }
+
+ virtual ILanguageInvoker* CreateInvoker() = 0;
+};
diff --git a/xbmc/interfaces/generic/ILanguageInvoker.cpp b/xbmc/interfaces/generic/ILanguageInvoker.cpp
new file mode 100644
index 0000000..df1193e
--- /dev/null
+++ b/xbmc/interfaces/generic/ILanguageInvoker.cpp
@@ -0,0 +1,96 @@
+/*
+ * 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 "ILanguageInvoker.h"
+
+#include "interfaces/generic/ILanguageInvocationHandler.h"
+
+#include <string>
+#include <vector>
+
+ILanguageInvoker::ILanguageInvoker(ILanguageInvocationHandler *invocationHandler)
+ : m_id(-1),
+ m_state(InvokerStateUninitialized),
+ m_invocationHandler(invocationHandler)
+{ }
+
+ILanguageInvoker::~ILanguageInvoker() = default;
+
+bool ILanguageInvoker::Execute(const std::string &script, const std::vector<std::string> &arguments /* = std::vector<std::string>() */)
+{
+ if (m_invocationHandler)
+ m_invocationHandler->OnScriptStarted(this);
+
+ return execute(script, arguments);
+}
+
+bool ILanguageInvoker::Stop(bool abort /* = false */)
+{
+ return stop(abort);
+}
+
+bool ILanguageInvoker::IsActive() const
+{
+ return GetState() > InvokerStateUninitialized && GetState() < InvokerStateScriptDone;
+}
+
+bool ILanguageInvoker::IsRunning() const
+{
+ return GetState() == InvokerStateRunning;
+}
+
+bool ILanguageInvoker::IsStopping() const
+{
+ return GetState() == InvokerStateStopping;
+}
+
+void ILanguageInvoker::pulseGlobalEvent()
+{
+ if (m_invocationHandler)
+ m_invocationHandler->PulseGlobalEvent();
+}
+
+bool ILanguageInvoker::onExecutionInitialized()
+{
+ if (m_invocationHandler == NULL)
+ return false;
+
+ return m_invocationHandler->OnScriptInitialized(this);
+}
+
+void ILanguageInvoker::AbortNotification()
+{
+ if (m_invocationHandler)
+ m_invocationHandler->NotifyScriptAborting(this);
+}
+
+void ILanguageInvoker::onExecutionFailed()
+{
+ if (m_invocationHandler)
+ m_invocationHandler->OnExecutionEnded(this);
+}
+
+void ILanguageInvoker::onExecutionDone()
+{
+ if (m_invocationHandler)
+ m_invocationHandler->OnExecutionEnded(this);
+}
+
+void ILanguageInvoker::onExecutionFinalized()
+{
+ if (m_invocationHandler)
+ m_invocationHandler->OnScriptFinalized(this);
+}
+
+void ILanguageInvoker::setState(InvokerState state)
+{
+ if (state <= m_state)
+ return;
+
+ m_state = state;
+}
diff --git a/xbmc/interfaces/generic/ILanguageInvoker.h b/xbmc/interfaces/generic/ILanguageInvoker.h
new file mode 100644
index 0000000..da4001e
--- /dev/null
+++ b/xbmc/interfaces/generic/ILanguageInvoker.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013-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 "addons/IAddon.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CLanguageInvokerThread;
+class ILanguageInvocationHandler;
+
+typedef enum
+{
+ InvokerStateUninitialized,
+ InvokerStateInitialized,
+ InvokerStateRunning,
+ InvokerStateStopping,
+ InvokerStateScriptDone,
+ InvokerStateExecutionDone,
+ InvokerStateFailed
+} InvokerState;
+
+class ILanguageInvoker
+{
+public:
+ explicit ILanguageInvoker(ILanguageInvocationHandler *invocationHandler);
+ virtual ~ILanguageInvoker();
+
+ virtual bool Execute(const std::string &script, const std::vector<std::string> &arguments = std::vector<std::string>());
+ virtual bool Stop(bool abort = false);
+ virtual bool IsStopping() const;
+
+ void SetId(int id) { m_id = id; }
+ int GetId() const { return m_id; }
+ const ADDON::AddonPtr& GetAddon() const { return m_addon; }
+ void SetAddon(const ADDON::AddonPtr &addon) { m_addon = addon; }
+ InvokerState GetState() const { return m_state; }
+ bool IsActive() const;
+ bool IsRunning() const;
+ void Reset() { m_state = InvokerStateUninitialized; }
+
+protected:
+ friend class CLanguageInvokerThread;
+
+ /**
+ * Called to notify the script is aborting.
+ */
+ virtual void AbortNotification();
+
+ virtual bool execute(const std::string &script, const std::vector<std::string> &arguments) = 0;
+ virtual bool stop(bool abort) = 0;
+
+ virtual void pulseGlobalEvent();
+ virtual bool onExecutionInitialized();
+ virtual void onExecutionFailed();
+ virtual void onExecutionDone();
+ virtual void onExecutionFinalized();
+
+ void setState(InvokerState state);
+
+ ADDON::AddonPtr m_addon;
+
+private:
+ int m_id;
+ InvokerState m_state;
+ ILanguageInvocationHandler *m_invocationHandler;
+};
+
+typedef std::shared_ptr<ILanguageInvoker> LanguageInvokerPtr;
diff --git a/xbmc/interfaces/generic/LanguageInvokerThread.cpp b/xbmc/interfaces/generic/LanguageInvokerThread.cpp
new file mode 100644
index 0000000..67f3d14
--- /dev/null
+++ b/xbmc/interfaces/generic/LanguageInvokerThread.cpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2013-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 "LanguageInvokerThread.h"
+
+#include "ScriptInvocationManager.h"
+
+#include <utility>
+
+CLanguageInvokerThread::CLanguageInvokerThread(LanguageInvokerPtr invoker,
+ CScriptInvocationManager* invocationManager,
+ bool reuseable)
+ : ILanguageInvoker(NULL),
+ CThread("LanguageInvoker"),
+ m_invoker(std::move(invoker)),
+ m_invocationManager(invocationManager),
+ m_reusable(reuseable)
+{ }
+
+CLanguageInvokerThread::~CLanguageInvokerThread()
+{
+ Stop(true);
+}
+
+InvokerState CLanguageInvokerThread::GetState() const
+{
+ if (m_invoker == NULL)
+ return InvokerStateFailed;
+
+ return m_invoker->GetState();
+}
+
+void CLanguageInvokerThread::Release()
+{
+ m_bStop = true;
+ m_condition.notify_one();
+}
+
+bool CLanguageInvokerThread::execute(const std::string &script, const std::vector<std::string> &arguments)
+{
+ if (m_invoker == NULL || script.empty())
+ return false;
+
+ m_script = script;
+ m_args = arguments;
+
+ if (CThread::IsRunning())
+ {
+ std::unique_lock<std::mutex> lck(m_mutex);
+ m_restart = true;
+ m_condition.notify_one();
+ }
+ else
+ Create();
+
+ //Todo wait until running
+
+ return true;
+}
+
+bool CLanguageInvokerThread::stop(bool wait)
+{
+ if (m_invoker == NULL)
+ return false;
+
+ if (!CThread::IsRunning())
+ return false;
+
+ Release();
+
+ bool result = true;
+ if (m_invoker->GetState() < InvokerStateExecutionDone)
+ {
+ // stop the language-specific invoker
+ result = m_invoker->Stop(wait);
+ }
+ // stop the thread
+ CThread::StopThread(wait);
+
+ return result;
+}
+
+void CLanguageInvokerThread::OnStartup()
+{
+ if (m_invoker == NULL)
+ return;
+
+ m_invoker->SetId(GetId());
+ if (m_addon != NULL)
+ m_invoker->SetAddon(m_addon);
+}
+
+void CLanguageInvokerThread::Process()
+{
+ if (m_invoker == NULL)
+ return;
+
+ std::unique_lock<std::mutex> lckdl(m_mutex);
+ do
+ {
+ m_restart = false;
+ m_invoker->Execute(m_script, m_args);
+
+ if (m_invoker->GetState() != InvokerStateScriptDone)
+ m_reusable = false;
+
+ m_condition.wait(lckdl, [this] { return m_bStop || m_restart || !m_reusable; });
+
+ } while (m_reusable && !m_bStop);
+}
+
+void CLanguageInvokerThread::OnExit()
+{
+ if (m_invoker == NULL)
+ return;
+
+ m_invoker->onExecutionDone();
+ m_invocationManager->OnExecutionDone(GetId());
+}
+
+void CLanguageInvokerThread::OnException()
+{
+ if (m_invoker == NULL)
+ return;
+
+ m_invoker->onExecutionFailed();
+ m_invocationManager->OnExecutionDone(GetId());
+} \ No newline at end of file
diff --git a/xbmc/interfaces/generic/LanguageInvokerThread.h b/xbmc/interfaces/generic/LanguageInvokerThread.h
new file mode 100644
index 0000000..a1844ba
--- /dev/null
+++ b/xbmc/interfaces/generic/LanguageInvokerThread.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013-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 "interfaces/generic/ILanguageInvoker.h"
+#include "threads/Thread.h"
+
+#include <string>
+#include <vector>
+
+class CScriptInvocationManager;
+
+class CLanguageInvokerThread : public ILanguageInvoker, protected CThread
+{
+public:
+ CLanguageInvokerThread(LanguageInvokerPtr invoker,
+ CScriptInvocationManager* invocationManager,
+ bool reuseable);
+ ~CLanguageInvokerThread() override;
+
+ virtual InvokerState GetState() const;
+
+ const std::string& GetScript() const { return m_script; }
+ LanguageInvokerPtr GetInvoker() const { return m_invoker; }
+ bool Reuseable(const std::string& script) const
+ {
+ return !m_bStop && m_reusable && GetState() == InvokerStateScriptDone && m_script == script;
+ };
+ virtual void Release();
+
+protected:
+ bool execute(const std::string &script, const std::vector<std::string> &arguments) override;
+ bool stop(bool wait) override;
+
+ void OnStartup() override;
+ void Process() override;
+ void OnExit() override;
+ void OnException() override;
+
+private:
+ LanguageInvokerPtr m_invoker;
+ CScriptInvocationManager *m_invocationManager;
+ std::string m_script;
+ std::vector<std::string> m_args;
+
+ std::mutex m_mutex;
+ std::condition_variable m_condition;
+ bool m_restart = false;
+ bool m_reusable = false;
+};
diff --git a/xbmc/interfaces/generic/RunningScriptObserver.cpp b/xbmc/interfaces/generic/RunningScriptObserver.cpp
new file mode 100644
index 0000000..c89c9f7
--- /dev/null
+++ b/xbmc/interfaces/generic/RunningScriptObserver.cpp
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017-2021 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 "RunningScriptObserver.h"
+
+#include "interfaces/generic/ScriptInvocationManager.h"
+
+using namespace std::chrono_literals;
+
+CRunningScriptObserver::CRunningScriptObserver(int scriptId, CEvent& evt)
+ : m_scriptId(scriptId), m_event(evt), m_stopEvent(true), m_thread(this, "ScriptObs")
+{
+ m_thread.Create();
+}
+
+CRunningScriptObserver::~CRunningScriptObserver()
+{
+ Abort();
+}
+
+void CRunningScriptObserver::Run()
+{
+ do
+ {
+ if (!CScriptInvocationManager::GetInstance().IsRunning(m_scriptId))
+ {
+ m_event.Set();
+ break;
+ }
+ } while (!m_stopEvent.Wait(20ms));
+}
+
+void CRunningScriptObserver::Abort()
+{
+ m_stopEvent.Set();
+ m_thread.StopThread();
+}
diff --git a/xbmc/interfaces/generic/RunningScriptObserver.h b/xbmc/interfaces/generic/RunningScriptObserver.h
new file mode 100644
index 0000000..2622411
--- /dev/null
+++ b/xbmc/interfaces/generic/RunningScriptObserver.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017-2021 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/Event.h"
+#include "threads/IRunnable.h"
+#include "threads/Thread.h"
+
+#include <atomic>
+
+class CRunningScriptObserver : public IRunnable
+{
+public:
+ CRunningScriptObserver(int scriptId, CEvent& evt);
+ ~CRunningScriptObserver();
+
+ void Abort();
+
+protected:
+ // implementation of IRunnable
+ void Run() override;
+
+ int m_scriptId;
+ CEvent& m_event;
+
+ CEvent m_stopEvent;
+ CThread m_thread;
+};
diff --git a/xbmc/interfaces/generic/RunningScriptsHandler.h b/xbmc/interfaces/generic/RunningScriptsHandler.h
new file mode 100644
index 0000000..59f18bb
--- /dev/null
+++ b/xbmc/interfaces/generic/RunningScriptsHandler.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017-2021 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 "interfaces/generic/ScriptInvocationManager.h"
+#include "interfaces/generic/ScriptRunner.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+
+#include <cstdint>
+#include <map>
+#include <mutex>
+
+template<class TScript>
+class CRunningScriptsHandler : protected CScriptRunner
+{
+protected:
+ using HandleType = int;
+
+ CRunningScriptsHandler() = default;
+ virtual ~CRunningScriptsHandler() = default;
+
+ using CScriptRunner::ExecuteScript;
+ using CScriptRunner::GetAddon;
+ using CScriptRunner::SetDone;
+ using CScriptRunner::StartScript;
+
+ bool RunScript(TScript* script,
+ const ADDON::AddonPtr& addon,
+ const std::string& path,
+ bool resume)
+ {
+ if (script == nullptr || addon == nullptr || path.empty())
+ return false;
+
+ // reuse an existing script handle or get a new one if necessary
+ int handle = CScriptInvocationManager::GetInstance().GetReusablePluginHandle(addon->LibPath());
+ if (handle < 0)
+ handle = GetNewScriptHandle(script);
+ else
+ ReuseScriptHandle(handle, script);
+
+ // run the script
+ auto result = CScriptRunner::RunScript(addon, path, handle, resume);
+
+ // remove the script handle if necessary
+ RemoveScriptHandle(handle);
+
+ return result;
+ }
+
+ static HandleType GetNewScriptHandle(TScript* script)
+ {
+ std::unique_lock<CCriticalSection> lock(s_critical);
+ uint32_t handle = ++s_scriptHandleCounter;
+ s_scriptHandles[handle] = script;
+
+ return handle;
+ }
+
+ static void ReuseScriptHandle(HandleType handle, TScript* script)
+ {
+ std::unique_lock<CCriticalSection> lock(s_critical);
+ s_scriptHandles[handle] = script;
+ }
+
+ static void RemoveScriptHandle(HandleType handle)
+ {
+ std::unique_lock<CCriticalSection> lock(s_critical);
+ s_scriptHandles.erase(handle);
+ }
+
+ static TScript* GetScriptFromHandle(HandleType handle)
+ {
+ std::unique_lock<CCriticalSection> lock(s_critical);
+ auto scriptHandle = s_scriptHandles.find(handle);
+ if (scriptHandle == s_scriptHandles.end())
+ return nullptr;
+
+ return scriptHandle->second;
+ }
+
+ static inline CCriticalSection& GetScriptsLock() { return s_critical; }
+
+private:
+ static std::map<HandleType, TScript*> s_scriptHandles;
+ static CCriticalSection s_critical;
+ static HandleType s_scriptHandleCounter;
+};
+
+template<class TScript>
+std::map<typename CRunningScriptsHandler<TScript>::HandleType, TScript*>
+ CRunningScriptsHandler<TScript>::s_scriptHandles;
+template<class TScript>
+CCriticalSection CRunningScriptsHandler<TScript>::s_critical;
+template<class TScript>
+typename CRunningScriptsHandler<TScript>::HandleType
+ CRunningScriptsHandler<TScript>::s_scriptHandleCounter = 0;
diff --git a/xbmc/interfaces/generic/ScriptInvocationManager.cpp b/xbmc/interfaces/generic/ScriptInvocationManager.cpp
new file mode 100644
index 0000000..9daae0f
--- /dev/null
+++ b/xbmc/interfaces/generic/ScriptInvocationManager.cpp
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2013-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 "ScriptInvocationManager.h"
+
+#include "interfaces/generic/ILanguageInvocationHandler.h"
+#include "interfaces/generic/ILanguageInvoker.h"
+#include "interfaces/generic/LanguageInvokerThread.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <cerrno>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+CScriptInvocationManager::~CScriptInvocationManager()
+{
+ Uninitialize();
+}
+
+CScriptInvocationManager& CScriptInvocationManager::GetInstance()
+{
+ static CScriptInvocationManager s_instance;
+ return s_instance;
+}
+
+void CScriptInvocationManager::Process()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ // go through all active threads and find and remove all which are done
+ std::vector<LanguageInvokerThread> tempList;
+ for (LanguageInvokerThreadMap::iterator it = m_scripts.begin(); it != m_scripts.end(); )
+ {
+ if (it->second.done)
+ {
+ tempList.push_back(it->second);
+ m_scripts.erase(it++);
+ }
+ else
+ ++it;
+ }
+
+ // remove the finished scripts from the script path map as well
+ for (const auto& it : tempList)
+ m_scriptPaths.erase(it.script);
+
+ // we can leave the lock now
+ lock.unlock();
+
+ // finally remove the finished threads but we do it outside of any locks in
+ // case of any callbacks from the destruction of the CLanguageInvokerThread
+ tempList.clear();
+
+ // let the invocation handlers do their processing
+ for (auto& it : m_invocationHandlers)
+ it.second->Process();
+}
+
+void CScriptInvocationManager::Uninitialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // execute Process() once more to handle the remaining scripts
+ Process();
+
+ // it is safe to release early, thread must be in m_scripts too
+ m_lastInvokerThread = nullptr;
+
+ // make sure all scripts are done
+ std::vector<LanguageInvokerThread> tempList;
+ for (const auto& script : m_scripts)
+ tempList.push_back(script.second);
+
+ m_scripts.clear();
+ m_scriptPaths.clear();
+
+ // we can leave the lock now
+ lock.unlock();
+
+ // finally stop and remove the finished threads but we do it outside of any
+ // locks in case of any callbacks from the stop or destruction logic of
+ // CLanguageInvokerThread or the ILanguageInvoker implementation
+ for (auto& it : tempList)
+ {
+ if (!it.done)
+ it.thread->Stop(true);
+ }
+
+ lock.lock();
+
+ tempList.clear();
+
+ // uninitialize all invocation handlers and then remove them
+ for (auto& it : m_invocationHandlers)
+ it.second->Uninitialize();
+
+ m_invocationHandlers.clear();
+}
+
+void CScriptInvocationManager::RegisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler, const std::string &extension)
+{
+ if (invocationHandler == NULL || extension.empty())
+ return;
+
+ std::string ext = extension;
+ StringUtils::ToLower(ext);
+ if (!StringUtils::StartsWithNoCase(ext, "."))
+ ext = "." + ext;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_invocationHandlers.find(ext) != m_invocationHandlers.end())
+ return;
+
+ m_invocationHandlers.insert(std::make_pair(extension, invocationHandler));
+
+ bool known = false;
+ for (const auto& it : m_invocationHandlers)
+ {
+ if (it.second == invocationHandler)
+ {
+ known = true;
+ break;
+ }
+ }
+
+ // automatically initialize the invocation handler if it's a new one
+ if (!known)
+ invocationHandler->Initialize();
+}
+
+void CScriptInvocationManager::RegisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler, const std::set<std::string> &extensions)
+{
+ if (invocationHandler == NULL || extensions.empty())
+ return;
+
+ for (const auto& extension : extensions)
+ RegisterLanguageInvocationHandler(invocationHandler, extension);
+}
+
+void CScriptInvocationManager::UnregisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler)
+{
+ if (invocationHandler == NULL)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ // get all extensions of the given language invoker
+ for (std::map<std::string, ILanguageInvocationHandler*>::iterator it = m_invocationHandlers.begin(); it != m_invocationHandlers.end(); )
+ {
+ if (it->second == invocationHandler)
+ m_invocationHandlers.erase(it++);
+ else
+ ++it;
+ }
+
+ // automatically uninitialize the invocation handler
+ invocationHandler->Uninitialize();
+}
+
+bool CScriptInvocationManager::HasLanguageInvoker(const std::string &script) const
+{
+ std::string extension = URIUtils::GetExtension(script);
+ StringUtils::ToLower(extension);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::map<std::string, ILanguageInvocationHandler*>::const_iterator it = m_invocationHandlers.find(extension);
+ return it != m_invocationHandlers.end() && it->second != NULL;
+}
+
+int CScriptInvocationManager::GetReusablePluginHandle(const std::string& script)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_lastInvokerThread)
+ {
+ if (m_lastInvokerThread->Reuseable(script))
+ return m_lastPluginHandle;
+ m_lastInvokerThread->Release();
+ m_lastInvokerThread = nullptr;
+ }
+ return -1;
+}
+
+LanguageInvokerPtr CScriptInvocationManager::GetLanguageInvoker(const std::string& script)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_lastInvokerThread)
+ {
+ if (m_lastInvokerThread->Reuseable(script))
+ {
+ CLog::Log(LOGDEBUG, "{} - Reusing LanguageInvokerThread {} for script {}", __FUNCTION__,
+ m_lastInvokerThread->GetId(), script);
+ m_lastInvokerThread->GetInvoker()->Reset();
+ return m_lastInvokerThread->GetInvoker();
+ }
+ m_lastInvokerThread->Release();
+ m_lastInvokerThread = nullptr;
+ }
+
+ std::string extension = URIUtils::GetExtension(script);
+ StringUtils::ToLower(extension);
+
+ std::map<std::string, ILanguageInvocationHandler*>::const_iterator it = m_invocationHandlers.find(extension);
+ if (it != m_invocationHandlers.end() && it->second != NULL)
+ return LanguageInvokerPtr(it->second->CreateInvoker());
+
+ return LanguageInvokerPtr();
+}
+
+int CScriptInvocationManager::ExecuteAsync(
+ const std::string& script,
+ const ADDON::AddonPtr& addon /* = ADDON::AddonPtr() */,
+ const std::vector<std::string>& arguments /* = std::vector<std::string>() */,
+ bool reuseable /* = false */,
+ int pluginHandle /* = -1 */)
+{
+ if (script.empty())
+ return -1;
+
+ if (!CFileUtils::Exists(script, false))
+ {
+ CLog::Log(LOGERROR, "{} - Not executing non-existing script {}", __FUNCTION__, script);
+ return -1;
+ }
+
+ LanguageInvokerPtr invoker = GetLanguageInvoker(script);
+ return ExecuteAsync(script, invoker, addon, arguments, reuseable, pluginHandle);
+}
+
+int CScriptInvocationManager::ExecuteAsync(
+ const std::string& script,
+ const LanguageInvokerPtr& languageInvoker,
+ const ADDON::AddonPtr& addon /* = ADDON::AddonPtr() */,
+ const std::vector<std::string>& arguments /* = std::vector<std::string>() */,
+ bool reuseable /* = false */,
+ int pluginHandle /* = -1 */)
+{
+ if (script.empty() || languageInvoker == NULL)
+ return -1;
+
+ if (!CFileUtils::Exists(script, false))
+ {
+ CLog::Log(LOGERROR, "{} - Not executing non-existing script {}", __FUNCTION__, script);
+ return -1;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_lastInvokerThread && m_lastInvokerThread->GetInvoker() == languageInvoker)
+ {
+ if (addon != NULL)
+ m_lastInvokerThread->SetAddon(addon);
+
+ // After we leave the lock, m_lastInvokerThread can be released -> copy!
+ CLanguageInvokerThreadPtr invokerThread = m_lastInvokerThread;
+ lock.unlock();
+ invokerThread->Execute(script, arguments);
+
+ return invokerThread->GetId();
+ }
+
+ m_lastInvokerThread =
+ CLanguageInvokerThreadPtr(new CLanguageInvokerThread(languageInvoker, this, reuseable));
+ if (m_lastInvokerThread == NULL)
+ return -1;
+
+ if (addon != NULL)
+ m_lastInvokerThread->SetAddon(addon);
+
+ m_lastInvokerThread->SetId(m_nextId++);
+ m_lastPluginHandle = pluginHandle;
+
+ LanguageInvokerThread thread = {m_lastInvokerThread, script, false};
+ m_scripts.insert(std::make_pair(m_lastInvokerThread->GetId(), thread));
+ m_scriptPaths.insert(std::make_pair(script, m_lastInvokerThread->GetId()));
+ // After we leave the lock, m_lastInvokerThread can be released -> copy!
+ CLanguageInvokerThreadPtr invokerThread = m_lastInvokerThread;
+ lock.unlock();
+ invokerThread->Execute(script, arguments);
+
+ return invokerThread->GetId();
+}
+
+int CScriptInvocationManager::ExecuteSync(
+ const std::string& script,
+ const ADDON::AddonPtr& addon /* = ADDON::AddonPtr() */,
+ const std::vector<std::string>& arguments /* = std::vector<std::string>() */,
+ uint32_t timeoutMs /* = 0 */,
+ bool waitShutdown /* = false */)
+{
+ if (script.empty())
+ return -1;
+
+ if (!CFileUtils::Exists(script, false))
+ {
+ CLog::Log(LOGERROR, "{} - Not executing non-existing script {}", __FUNCTION__, script);
+ return -1;
+ }
+
+ LanguageInvokerPtr invoker = GetLanguageInvoker(script);
+ return ExecuteSync(script, invoker, addon, arguments, timeoutMs, waitShutdown);
+}
+
+int CScriptInvocationManager::ExecuteSync(
+ const std::string& script,
+ const LanguageInvokerPtr& languageInvoker,
+ const ADDON::AddonPtr& addon /* = ADDON::AddonPtr() */,
+ const std::vector<std::string>& arguments /* = std::vector<std::string>() */,
+ uint32_t timeoutMs /* = 0 */,
+ bool waitShutdown /* = false */)
+{
+ int scriptId = ExecuteAsync(script, languageInvoker, addon, arguments);
+ if (scriptId < 0)
+ return -1;
+
+ bool timeout = timeoutMs > 0;
+ while ((!timeout || timeoutMs > 0) && IsRunning(scriptId))
+ {
+ unsigned int sleepMs = 100U;
+ if (timeout && timeoutMs < sleepMs)
+ sleepMs = timeoutMs;
+
+ KODI::TIME::Sleep(std::chrono::milliseconds(sleepMs));
+
+ if (timeout)
+ timeoutMs -= sleepMs;
+ }
+
+ if (IsRunning(scriptId))
+ {
+ Stop(scriptId, waitShutdown);
+ return ETIMEDOUT;
+ }
+
+ return 0;
+}
+
+bool CScriptInvocationManager::Stop(int scriptId, bool wait /* = false */)
+{
+ if (scriptId < 0)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CLanguageInvokerThreadPtr invokerThread = getInvokerThread(scriptId).thread;
+ if (invokerThread == NULL)
+ return false;
+
+ return invokerThread->Stop(wait);
+}
+
+void CScriptInvocationManager::StopRunningScripts(bool wait /* = false */)
+{
+ for (auto& it : m_scripts)
+ {
+ if (!it.second.done)
+ Stop(it.second.script, wait);
+ }
+}
+
+bool CScriptInvocationManager::Stop(const std::string &scriptPath, bool wait /* = false */)
+{
+ if (scriptPath.empty())
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::map<std::string, int>::const_iterator script = m_scriptPaths.find(scriptPath);
+ if (script == m_scriptPaths.end())
+ return false;
+
+ return Stop(script->second, wait);
+}
+
+bool CScriptInvocationManager::IsRunning(int scriptId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ LanguageInvokerThread invokerThread = getInvokerThread(scriptId);
+ if (invokerThread.thread == NULL)
+ return false;
+
+ return !invokerThread.done;
+}
+
+bool CScriptInvocationManager::IsRunning(const std::string& scriptPath) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto it = m_scriptPaths.find(scriptPath);
+ if (it == m_scriptPaths.end())
+ return false;
+
+ return IsRunning(it->second);
+}
+
+void CScriptInvocationManager::OnExecutionDone(int scriptId)
+{
+ if (scriptId < 0)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ LanguageInvokerThreadMap::iterator script = m_scripts.find(scriptId);
+ if (script != m_scripts.end())
+ script->second.done = true;
+}
+
+CScriptInvocationManager::LanguageInvokerThread CScriptInvocationManager::getInvokerThread(int scriptId) const
+{
+ if (scriptId < 0)
+ return LanguageInvokerThread();
+
+ LanguageInvokerThreadMap::const_iterator script = m_scripts.find(scriptId);
+ if (script == m_scripts.end())
+ return LanguageInvokerThread();
+
+ return script->second;
+}
diff --git a/xbmc/interfaces/generic/ScriptInvocationManager.h b/xbmc/interfaces/generic/ScriptInvocationManager.h
new file mode 100644
index 0000000..6e409b8
--- /dev/null
+++ b/xbmc/interfaces/generic/ScriptInvocationManager.h
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2013-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 "addons/IAddon.h"
+#include "interfaces/generic/ILanguageInvoker.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <vector>
+
+class CLanguageInvokerThread;
+typedef std::shared_ptr<CLanguageInvokerThread> CLanguageInvokerThreadPtr;
+
+class CScriptInvocationManager
+{
+public:
+ static CScriptInvocationManager& GetInstance();
+
+ void Process();
+ void Uninitialize();
+
+ void RegisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler, const std::string &extension);
+ void RegisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler, const std::set<std::string> &extensions);
+ void UnregisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler);
+ bool HasLanguageInvoker(const std::string &script) const;
+ LanguageInvokerPtr GetLanguageInvoker(const std::string& script);
+
+ /*!
+ * \brief Returns addon_handle if last reusable invoker is ready to use.
+ */
+ int GetReusablePluginHandle(const std::string& script);
+
+ /*!
+ * \brief Executes the given script asynchronously in a separate thread.
+ *
+ * \param script Path to the script to be executed
+ * \param addon (Optional) Addon to which the script belongs
+ * \param arguments (Optional) List of arguments passed to the script
+ * \return -1 if an error occurred, otherwise the ID of the script
+ */
+ int ExecuteAsync(const std::string& script,
+ const ADDON::AddonPtr& addon = ADDON::AddonPtr(),
+ const std::vector<std::string>& arguments = std::vector<std::string>(),
+ bool reuseable = false,
+ int pluginHandle = -1);
+ /*!
+ * \brief Executes the given script asynchronously in a separate thread.
+ *
+ * \param script Path to the script to be executed
+ * \param languageInvoker Language invoker to be used to execute the script
+ * \param addon (Optional) Addon to which the script belongs
+ * \param arguments (Optional) List of arguments passed to the script
+ * \return -1 if an error occurred, otherwise the ID of the script
+ */
+ int ExecuteAsync(const std::string& script,
+ const LanguageInvokerPtr& languageInvoker,
+ const ADDON::AddonPtr& addon = ADDON::AddonPtr(),
+ const std::vector<std::string>& arguments = std::vector<std::string>(),
+ bool reuseable = false,
+ int pluginHandle = -1);
+
+ /*!
+ * \brief Executes the given script synchronously.
+ *
+ * \details The script is actually executed asynchronously but the calling
+ * thread is blocked until either the script has finished or the given timeout
+ * has expired. If the given timeout has expired the script's execution is
+ * stopped and depending on the specified wait behaviour we wait for the
+ * script's execution to finish or not.
+ *
+ * \param script Path to the script to be executed
+ * \param addon (Optional) Addon to which the script belongs
+ * \param arguments (Optional) List of arguments passed to the script
+ * \param timeout (Optional) Timeout (in milliseconds) for the script's execution
+ * \param waitShutdown (Optional) Whether to wait when having to forcefully stop the script's execution or not.
+ * \return -1 if an error occurred, 0 if the script terminated or ETIMEDOUT if the given timeout expired
+ */
+ int ExecuteSync(const std::string& script,
+ const ADDON::AddonPtr& addon = ADDON::AddonPtr(),
+ const std::vector<std::string>& arguments = std::vector<std::string>(),
+ uint32_t timeoutMs = 0,
+ bool waitShutdown = false);
+ /*!
+ * \brief Executes the given script synchronously.
+ *
+ * \details The script is actually executed asynchronously but the calling
+ * thread is blocked until either the script has finished or the given timeout
+ * has expired. If the given timeout has expired the script's execution is
+ * stopped and depending on the specified wait behaviour we wait for the
+ * script's execution to finish or not.
+ *
+ * \param script Path to the script to be executed
+ * \param languageInvoker Language invoker to be used to execute the script
+ * \param addon (Optional) Addon to which the script belongs
+ * \param arguments (Optional) List of arguments passed to the script
+ * \param timeout (Optional) Timeout (in milliseconds) for the script's execution
+ * \param waitShutdown (Optional) Whether to wait when having to forcefully stop the script's execution or not.
+ * \return -1 if an error occurred, 0 if the script terminated or ETIMEDOUT if the given timeout expired
+ */
+ int ExecuteSync(const std::string& script,
+ const LanguageInvokerPtr& languageInvoker,
+ const ADDON::AddonPtr& addon = ADDON::AddonPtr(),
+ const std::vector<std::string>& arguments = std::vector<std::string>(),
+ uint32_t timeoutMs = 0,
+ bool waitShutdown = false);
+ bool Stop(int scriptId, bool wait = false);
+ bool Stop(const std::string &scriptPath, bool wait = false);
+
+ /*!
+ *\brief Stop all running scripts
+ *\param wait if kodi should wait for each script to finish (default false)
+ */
+ void StopRunningScripts(bool wait = false);
+
+ bool IsRunning(int scriptId) const;
+ bool IsRunning(const std::string& scriptPath) const;
+
+protected:
+ friend class CLanguageInvokerThread;
+
+ void OnExecutionDone(int scriptId);
+
+private:
+ CScriptInvocationManager() = default;
+ CScriptInvocationManager(const CScriptInvocationManager&) = delete;
+ CScriptInvocationManager const& operator=(CScriptInvocationManager const&) = delete;
+ virtual ~CScriptInvocationManager();
+
+ typedef struct {
+ CLanguageInvokerThreadPtr thread;
+ std::string script;
+ bool done;
+ } LanguageInvokerThread;
+ typedef std::map<int, LanguageInvokerThread> LanguageInvokerThreadMap;
+ typedef std::map<std::string, ILanguageInvocationHandler*> LanguageInvocationHandlerMap;
+
+ LanguageInvokerThread getInvokerThread(int scriptId) const;
+
+ LanguageInvocationHandlerMap m_invocationHandlers;
+ LanguageInvokerThreadMap m_scripts;
+ CLanguageInvokerThreadPtr m_lastInvokerThread;
+ int m_lastPluginHandle = -1;
+
+ std::map<std::string, int> m_scriptPaths;
+ int m_nextId = 0;
+ mutable CCriticalSection m_critSection;
+};
diff --git a/xbmc/interfaces/generic/ScriptRunner.cpp b/xbmc/interfaces/generic/ScriptRunner.cpp
new file mode 100644
index 0000000..b88dbac
--- /dev/null
+++ b/xbmc/interfaces/generic/ScriptRunner.cpp
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2017-2021 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 "ScriptRunner.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "interfaces/generic/RunningScriptObserver.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "threads/SystemClock.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <vector>
+
+using namespace std::chrono_literals;
+
+ADDON::AddonPtr CScriptRunner::GetAddon() const
+{
+ return m_addon;
+}
+
+CScriptRunner::CScriptRunner() : m_scriptDone(true)
+{ }
+
+bool CScriptRunner::StartScript(const ADDON::AddonPtr& addon, const std::string& path)
+{
+ return RunScriptInternal(addon, path, 0, false);
+}
+
+bool CScriptRunner::RunScript(const ADDON::AddonPtr& addon,
+ const std::string& path,
+ int handle,
+ bool resume)
+{
+ return RunScriptInternal(addon, path, handle, resume, true);
+}
+
+void CScriptRunner::SetDone()
+{
+ m_scriptDone.Set();
+}
+
+int CScriptRunner::ExecuteScript(const ADDON::AddonPtr& addon, const std::string& path, bool resume)
+{
+ return ExecuteScript(addon, path, -1, resume);
+}
+
+int CScriptRunner::ExecuteScript(const ADDON::AddonPtr& addon,
+ const std::string& path,
+ int handle,
+ bool resume)
+{
+ if (addon == nullptr || path.empty())
+ return false;
+
+ CURL url(path);
+
+ // get options and remove them from the URL because we can then use the url
+ // to generate the base path which is passed to the add-on script
+ auto options = url.GetOptions();
+ url.SetOptions("");
+
+ // setup our parameters to send the script
+ std::vector<std::string> argv = {url.Get(), // base path
+ StringUtils::Format("{:d}", handle), options,
+ StringUtils::Format("resume:{}", resume)};
+
+ bool reuseLanguageInvoker = false;
+ const auto reuseLanguageInvokerIt = addon->ExtraInfo().find("reuselanguageinvoker");
+ if (reuseLanguageInvokerIt != addon->ExtraInfo().end())
+ reuseLanguageInvoker = reuseLanguageInvokerIt->second == "true";
+
+ // run the script
+ CLog::Log(LOGDEBUG, "CScriptRunner: running add-on script {:s}('{:s}', '{:s}', '{:s}')",
+ addon->Name(), argv[0], argv[1], argv[2]);
+ int scriptId = CScriptInvocationManager::GetInstance().ExecuteAsync(addon->LibPath(), addon, argv,
+ reuseLanguageInvoker, handle);
+ if (scriptId < 0)
+ CLog::Log(LOGERROR, "CScriptRunner: unable to run add-on script {:s}", addon->Name());
+
+ return scriptId;
+}
+
+bool CScriptRunner::RunScriptInternal(const ADDON::AddonPtr& addon,
+ const std::string& path,
+ int handle,
+ bool resume,
+ bool wait /* = true */)
+{
+ if (addon == nullptr || path.empty())
+ return false;
+
+ // reset our wait event
+ m_scriptDone.Reset();
+
+ // store the add-on
+ m_addon = addon;
+
+ int scriptId = ExecuteScript(addon, path, handle, resume);
+ if (scriptId < 0)
+ return false;
+
+ // we don't need to wait for the script to end
+ if (!wait)
+ return true;
+
+ // wait for our script to finish
+ return WaitOnScriptResult(scriptId, addon->LibPath(), addon->Name());
+}
+
+bool CScriptRunner::WaitOnScriptResult(int scriptId,
+ const std::string& path,
+ const std::string& name)
+{
+ bool cancelled = false;
+
+ // Add-on scripts can be called from the main and other threads. If called
+ // form the main thread, we need to bring up the BusyDialog in order to
+ // keep the render loop alive
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ if (!m_scriptDone.Wait(20ms))
+ {
+ // observe the script until it's finished while showing the busy dialog
+ CRunningScriptObserver scriptObs(scriptId, m_scriptDone);
+
+ auto& wm = CServiceBroker::GetGUI()->GetWindowManager();
+ if (wm.IsModalDialogTopmost(WINDOW_DIALOG_PROGRESS))
+ {
+ auto progress = wm.GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (!progress->WaitOnEvent(m_scriptDone))
+ cancelled = true;
+ }
+ else if (!CGUIDialogBusy::WaitOnEvent(m_scriptDone, 200))
+ cancelled = true;
+
+ scriptObs.Abort();
+ }
+ }
+ else
+ {
+ // wait for the script to finish or be cancelled
+ while (!IsCancelled() && CScriptInvocationManager::GetInstance().IsRunning(scriptId) &&
+ !m_scriptDone.Wait(20ms))
+ ;
+
+ // give the script 30 seconds to exit before we attempt to stop it
+ XbmcThreads::EndTime<> timer(30s);
+ while (!timer.IsTimePast() && CScriptInvocationManager::GetInstance().IsRunning(scriptId) &&
+ !m_scriptDone.Wait(20ms))
+ ;
+ }
+
+ if (cancelled || IsCancelled())
+ {
+ // cancel the script
+ if (scriptId != -1 && CScriptInvocationManager::GetInstance().IsRunning(scriptId))
+ {
+ CLog::Log(LOGDEBUG, "CScriptRunner: cancelling add-on script {:s} (id = {:d})", name,
+ scriptId);
+ CScriptInvocationManager::GetInstance().Stop(scriptId);
+ }
+ }
+
+ return !cancelled && !IsCancelled() && IsSuccessful();
+}
diff --git a/xbmc/interfaces/generic/ScriptRunner.h b/xbmc/interfaces/generic/ScriptRunner.h
new file mode 100644
index 0000000..77d511d
--- /dev/null
+++ b/xbmc/interfaces/generic/ScriptRunner.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017-2021 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 "addons/IAddon.h"
+#include "threads/Event.h"
+
+#include <string>
+
+class CScriptRunner
+{
+protected:
+ CScriptRunner();
+ virtual ~CScriptRunner() = default;
+
+ virtual bool IsSuccessful() const = 0;
+ virtual bool IsCancelled() const = 0;
+
+ ADDON::AddonPtr GetAddon() const;
+
+ bool StartScript(const ADDON::AddonPtr& addon, const std::string& path);
+ bool RunScript(const ADDON::AddonPtr& addon, const std::string& path, int handle, bool resume);
+
+ void SetDone();
+
+ static int ExecuteScript(const ADDON::AddonPtr& addon, const std::string& path, bool resume);
+ static int ExecuteScript(const ADDON::AddonPtr& addon,
+ const std::string& path,
+ int handle,
+ bool resume);
+
+private:
+ bool RunScriptInternal(const ADDON::AddonPtr& addon,
+ const std::string& path,
+ int handle,
+ bool resume,
+ bool wait = true);
+ bool WaitOnScriptResult(int scriptId, const std::string& path, const std::string& name);
+
+ ADDON::AddonPtr m_addon;
+
+ CEvent m_scriptDone;
+};
diff --git a/xbmc/interfaces/info/CMakeLists.txt b/xbmc/interfaces/info/CMakeLists.txt
new file mode 100644
index 0000000..621ecfc
--- /dev/null
+++ b/xbmc/interfaces/info/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(SOURCES InfoBool.cpp
+ InfoExpression.cpp
+ SkinVariable.cpp)
+
+set(HEADERS Info.h
+ InfoBool.h
+ InfoExpression.h
+ SkinVariable.h)
+
+core_add_library(info_interface)
diff --git a/xbmc/interfaces/info/Info.h b/xbmc/interfaces/info/Info.h
new file mode 100644
index 0000000..e454e3c
--- /dev/null
+++ b/xbmc/interfaces/info/Info.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 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
+
+namespace INFO
+{
+/*! Default context of the INFO interface
+ @note when info conditions are evaluated different contexts can be passed. Context usually refers to the window ids where
+ the conditions are being evaluated. By default conditions, skin variables and labels are initialized using the DEFAULT_CONTEXT
+ value unless specifically bound to a given window.
+ */
+constexpr int DEFAULT_CONTEXT = 0;
+} // namespace INFO
diff --git a/xbmc/interfaces/info/InfoBool.cpp b/xbmc/interfaces/info/InfoBool.cpp
new file mode 100644
index 0000000..65b1f44
--- /dev/null
+++ b/xbmc/interfaces/info/InfoBool.cpp
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013-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 "InfoBool.h"
+
+#include "utils/StringUtils.h"
+
+namespace INFO
+{
+ InfoBool::InfoBool(const std::string &expression, int context, unsigned int &refreshCounter)
+ : m_value(false),
+ m_context(context),
+ m_listItemDependent(false),
+ m_expression(expression),
+ m_refreshCounter(0),
+ m_parentRefreshCounter(refreshCounter)
+ {
+ StringUtils::ToLower(m_expression);
+ }
+}
diff --git a/xbmc/interfaces/info/InfoBool.h b/xbmc/interfaces/info/InfoBool.h
new file mode 100644
index 0000000..6906276
--- /dev/null
+++ b/xbmc/interfaces/info/InfoBool.h
@@ -0,0 +1,83 @@
+/*
+ * 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 <memory>
+#include <string>
+
+class CGUIListItem;
+
+namespace INFO
+{
+/*!
+ \ingroup info
+ \brief Base class, wrapping boolean conditions and expressions
+ */
+class InfoBool
+{
+public:
+ InfoBool(const std::string &expression, int context, unsigned int &refreshCounter);
+ virtual ~InfoBool() = default;
+
+ virtual void Initialize() {}
+
+ /*! \brief Get the value of this info bool
+ This is called to update (if dirty) and fetch the value of the info bool
+ \param contextWindow the context (window id) where this condition is being evaluated
+ \param item the item used to evaluate the bool
+ */
+ inline bool Get(int contextWindow, const CGUIListItem* item = nullptr)
+ {
+ if (item && m_listItemDependent)
+ Update(contextWindow, item);
+ else if (m_refreshCounter != m_parentRefreshCounter || m_refreshCounter == 0)
+ {
+ Update(contextWindow, nullptr);
+ m_refreshCounter = m_parentRefreshCounter;
+ }
+ return m_value;
+ }
+
+ bool operator==(const InfoBool &right) const
+ {
+ return (m_context == right.m_context &&
+ m_expression == right.m_expression);
+ }
+
+ bool operator<(const InfoBool &right) const
+ {
+ if (m_context < right.m_context)
+ return true;
+ else if (m_context == right.m_context)
+ return m_expression < right.m_expression;
+ else
+ return false;
+ }
+
+ /*! \brief Update the value of this info bool
+ This is called if and only if the info bool is dirty, allowing it to update it's current value
+ */
+ virtual void Update(int contextWindow, const CGUIListItem* item) {}
+
+ const std::string &GetExpression() const { return m_expression; }
+ bool ListItemDependent() const { return m_listItemDependent; }
+protected:
+
+ bool m_value; ///< current value
+ int m_context; ///< contextual information to go with the condition
+ bool m_listItemDependent; ///< do not cache if a listitem pointer is given
+ std::string m_expression; ///< original expression
+
+private:
+ unsigned int m_refreshCounter;
+ unsigned int &m_parentRefreshCounter;
+};
+
+typedef std::shared_ptr<InfoBool> InfoPtr;
+};
diff --git a/xbmc/interfaces/info/InfoExpression.cpp b/xbmc/interfaces/info/InfoExpression.cpp
new file mode 100644
index 0000000..dc9aa63
--- /dev/null
+++ b/xbmc/interfaces/info/InfoExpression.cpp
@@ -0,0 +1,311 @@
+/*
+ * 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 "InfoExpression.h"
+
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "utils/log.h"
+
+#include <list>
+#include <memory>
+#include <stack>
+
+using namespace INFO;
+
+void InfoSingle::Initialize()
+{
+ m_condition = CServiceBroker::GetGUI()->GetInfoManager().TranslateSingleString(m_expression, m_listItemDependent);
+}
+
+void InfoSingle::Update(int contextWindow, const CGUIListItem* item)
+{
+ // use propagated context in case this info has the default context (i.e. if not tied to a specific window)
+ // its value might depend on the context in which the evaluation was called
+ int context = m_context == DEFAULT_CONTEXT ? contextWindow : m_context;
+ m_value = CServiceBroker::GetGUI()->GetInfoManager().GetBool(m_condition, context, item);
+}
+
+void InfoExpression::Initialize()
+{
+ if (!Parse(m_expression))
+ {
+ CLog::Log(LOGERROR, "Error parsing boolean expression {}", m_expression);
+ m_expression_tree = std::make_shared<InfoLeaf>(CServiceBroker::GetGUI()->GetInfoManager().Register("false", 0), false);
+ }
+}
+
+void InfoExpression::Update(int contextWindow, const CGUIListItem* item)
+{
+ // use propagated context in case this info expression has the default context (i.e. if not tied to a specific window)
+ // its value might depend on the context in which the evaluation was called
+ int context = m_context == DEFAULT_CONTEXT ? contextWindow : m_context;
+ m_value = m_expression_tree->Evaluate(context, item);
+}
+
+/* Expressions are rewritten at parse time into a form which favours the
+ * formation of groups of associative nodes. These groups are then reordered at
+ * evaluation time such that nodes whose value renders the evaluation of the
+ * remainder of the group unnecessary tend to be evaluated first (these are
+ * true nodes for OR subexpressions, or false nodes for AND subexpressions).
+ * The end effect is to minimise the number of leaf nodes that need to be
+ * evaluated in order to determine the value of the expression. The runtime
+ * adaptability has the advantage of not being customised for any particular skin.
+ *
+ * The modifications to the expression at parse time fall into two groups:
+ * 1) Moving logical NOTs so that they are only applied to leaf nodes.
+ * For example, rewriting ![A+B]|C as !A|!B|C allows reordering such that
+ * any of the three leaves can be evaluated first.
+ * 2) Combining adjacent AND or OR operations such that each path from the root
+ * to a leaf encounters a strictly alternating pattern of AND and OR
+ * operations. So [A|B]|[C|D+[[E|F]|G] becomes A|B|C|[D+[E|F|G]].
+ */
+
+bool InfoExpression::InfoLeaf::Evaluate(int contextWindow, const CGUIListItem* item)
+{
+ return m_invert ^ m_info->Get(contextWindow, item);
+}
+
+InfoExpression::InfoAssociativeGroup::InfoAssociativeGroup(
+ node_type_t type,
+ const InfoSubexpressionPtr &left,
+ const InfoSubexpressionPtr &right)
+ : m_type(type)
+{
+ AddChild(right);
+ AddChild(left);
+}
+
+void InfoExpression::InfoAssociativeGroup::AddChild(const InfoSubexpressionPtr &child)
+{
+ m_children.push_front(child); // largely undoes the effect of parsing right-associative
+}
+
+void InfoExpression::InfoAssociativeGroup::Merge(const std::shared_ptr<InfoAssociativeGroup>& other)
+{
+ m_children.splice(m_children.end(), other->m_children);
+}
+
+bool InfoExpression::InfoAssociativeGroup::Evaluate(int contextWindow, const CGUIListItem* item)
+{
+ /* Handle either AND or OR by using the relation
+ * A AND B == !(!A OR !B)
+ * to convert ANDs into ORs
+ */
+ std::list<InfoSubexpressionPtr>::iterator last = m_children.end();
+ std::list<InfoSubexpressionPtr>::iterator it = m_children.begin();
+ bool use_and = (m_type == NODE_AND);
+ bool result = use_and ^ (*it)->Evaluate(contextWindow, item);
+ while (!result && ++it != last)
+ {
+ result = use_and ^ (*it)->Evaluate(contextWindow, item);
+ if (result)
+ {
+ /* Move this child to the head of the list so we evaluate faster next time */
+ m_children.push_front(*it);
+ m_children.erase(it);
+ }
+ }
+ return use_and ^ result;
+}
+
+/* Expressions are parsed using the shunting-yard algorithm. Binary operators
+ * (AND/OR) are treated as right-associative so that we don't need to make a
+ * special case for the unary NOT operator. This has no effect upon the answers
+ * generated, though the initial sequence of evaluation of leaves may be
+ * different from what you might expect.
+ */
+
+InfoExpression::operator_t InfoExpression::GetOperator(char ch)
+{
+ if (ch == '[')
+ return OPERATOR_LB;
+ else if (ch == ']')
+ return OPERATOR_RB;
+ else if (ch == '!')
+ return OPERATOR_NOT;
+ else if (ch == '+')
+ return OPERATOR_AND;
+ else if (ch == '|')
+ return OPERATOR_OR;
+ else
+ return OPERATOR_NONE;
+}
+
+void InfoExpression::OperatorPop(std::stack<operator_t> &operator_stack, bool &invert, std::stack<InfoSubexpressionPtr> &nodes)
+{
+ operator_t op2 = operator_stack.top();
+ operator_stack.pop();
+ if (op2 == OPERATOR_NOT)
+ {
+ invert = !invert;
+ }
+ else
+ {
+ // At this point, it can only be OPERATOR_AND or OPERATOR_OR
+ if (invert)
+ op2 = (operator_t) (OPERATOR_AND ^ OPERATOR_OR ^ op2);
+ node_type_t new_type = op2 == OPERATOR_AND ? NODE_AND : NODE_OR;
+
+ InfoSubexpressionPtr right = nodes.top();
+ nodes.pop();
+ InfoSubexpressionPtr left = nodes.top();
+
+ node_type_t right_type = right->Type();
+ node_type_t left_type = left->Type();
+
+ // Combine associative operations into the same node where possible
+ if (left_type == new_type && right_type == new_type)
+ /* For example: AND
+ * / \ ____ AND ____
+ * AND AND -> / / \ \
+ * / \ / \ leaf leaf leaf leaf
+ * leaf leaf leaf leaf
+ */
+ std::static_pointer_cast<InfoAssociativeGroup>(left)->Merge(std::static_pointer_cast<InfoAssociativeGroup>(right));
+ else if (left_type == new_type)
+ /* For example: AND AND
+ * / \ / | \
+ * AND OR -> leaf leaf OR
+ * / \ / \ / \
+ * leaf leaf leaf leaf leaf leaf
+ */
+ std::static_pointer_cast<InfoAssociativeGroup>(left)->AddChild(right);
+ else
+ {
+ nodes.pop();
+ if (right_type == new_type)
+ {
+ /* For example: AND AND
+ * / \ / | \
+ * OR AND -> OR leaf leaf
+ * / \ / \ / \
+ * leaf leaf leaf leaf leaf leaf
+ */
+ std::static_pointer_cast<InfoAssociativeGroup>(right)->AddChild(left);
+ nodes.push(right);
+ }
+ else
+ /* For example: AND which can't be simplified, and
+ * / \ requires a new AND node to be
+ * OR OR created with the two OR nodes
+ * / \ / \ as children
+ * leaf leaf leaf leaf
+ */
+ nodes.push(std::make_shared<InfoAssociativeGroup>(new_type, left, right));
+ }
+ }
+}
+
+bool InfoExpression::Parse(const std::string &expression)
+{
+ const char *s = expression.c_str();
+ std::string operand;
+ std::stack<operator_t> operator_stack;
+ bool invert = false;
+ std::stack<InfoSubexpressionPtr> nodes;
+ // The next two are for syntax-checking purposes
+ bool after_binaryoperator = true;
+ int bracket_count = 0;
+
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+
+ char c;
+ // Skip leading whitespace - don't want it to count as an operand if that's all there is
+ while (isspace((unsigned char)(c=*s)))
+ s++;
+
+ while ((c = *s++) != '\0')
+ {
+ operator_t op;
+ if ((op = GetOperator(c)) != OPERATOR_NONE)
+ {
+ // Character is an operator
+ if ((!after_binaryoperator && (c == '!' || c == '[')) ||
+ (after_binaryoperator && (c == ']' || c == '+' || c == '|')))
+ {
+ CLog::Log(LOGERROR, "Misplaced {}", c);
+ return false;
+ }
+ if (c == '[')
+ bracket_count++;
+ else if (c == ']' && bracket_count-- == 0)
+ {
+ CLog::Log(LOGERROR, "Unmatched ]");
+ return false;
+ }
+ if (!operand.empty())
+ {
+ InfoPtr info = infoMgr.Register(operand, m_context);
+ if (!info)
+ {
+ CLog::Log(LOGERROR, "Bad operand '{}'", operand);
+ return false;
+ }
+ /* Propagate any listItem dependency from the operand to the expression */
+ m_listItemDependent |= info->ListItemDependent();
+ nodes.push(std::make_shared<InfoLeaf>(info, invert));
+ /* Reuse operand string for next operand */
+ operand.clear();
+ }
+
+ // Handle any higher-priority stacked operators, except when the new operator is left-bracket.
+ // For a right-bracket, this will stop with the matching left-bracket at the top of the operator stack.
+ if (op != OPERATOR_LB)
+ {
+ while (!operator_stack.empty() && operator_stack.top() > op)
+ OperatorPop(operator_stack, invert, nodes);
+ }
+ if (op == OPERATOR_RB)
+ operator_stack.pop(); // remove the matching left-bracket
+ else
+ operator_stack.push(op);
+ if (op == OPERATOR_NOT)
+ invert = !invert;
+
+ if (c == '+' || c == '|')
+ after_binaryoperator = true;
+ // Skip trailing whitespace - don't want it to count as an operand if that's all there is
+ while (isspace((unsigned char)(c=*s))) s++;
+ }
+ else
+ {
+ // Character is part of operand
+ operand += c;
+ after_binaryoperator = false;
+ }
+ }
+ if (bracket_count > 0)
+ {
+ CLog::Log(LOGERROR, "Unmatched [");
+ return false;
+ }
+ if (after_binaryoperator)
+ {
+ CLog::Log(LOGERROR, "Missing operand");
+ return false;
+ }
+ if (!operand.empty())
+ {
+ InfoPtr info = infoMgr.Register(operand, m_context);
+ if (!info)
+ {
+ CLog::Log(LOGERROR, "Bad operand '{}'", operand);
+ return false;
+ }
+ /* Propagate any listItem dependency from the operand to the expression */
+ m_listItemDependent |= info->ListItemDependent();
+ nodes.push(std::make_shared<InfoLeaf>(info, invert));
+ }
+ while (!operator_stack.empty())
+ OperatorPop(operator_stack, invert, nodes);
+
+ m_expression_tree = nodes.top();
+ return true;
+}
diff --git a/xbmc/interfaces/info/InfoExpression.h b/xbmc/interfaces/info/InfoExpression.h
new file mode 100644
index 0000000..1a0877c
--- /dev/null
+++ b/xbmc/interfaces/info/InfoExpression.h
@@ -0,0 +1,117 @@
+/*
+ * 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 "InfoBool.h"
+
+#include <list>
+#include <stack>
+#include <utility>
+#include <vector>
+
+class CGUIListItem;
+
+namespace INFO
+{
+/*! \brief Class to wrap active boolean conditions
+ */
+class InfoSingle : public InfoBool
+{
+public:
+ InfoSingle(const std::string& expression, int context, unsigned int& refreshCounter)
+ : InfoBool(expression, context, refreshCounter)
+ {
+ }
+ void Initialize() override;
+
+ void Update(int contextWindow, const CGUIListItem* item) override;
+
+private:
+ int m_condition; ///< actual condition this represents
+};
+
+/*! \brief Class to wrap active boolean expressions
+ */
+class InfoExpression : public InfoBool
+{
+public:
+ InfoExpression(const std::string& expression, int context, unsigned int& refreshCounter)
+ : InfoBool(expression, context, refreshCounter)
+ {
+ }
+ ~InfoExpression() override = default;
+
+ void Initialize() override;
+
+ void Update(int contextWindow, const CGUIListItem* item) override;
+
+private:
+ typedef enum
+ {
+ OPERATOR_NONE = 0,
+ OPERATOR_LB, // 1
+ OPERATOR_RB, // 2
+ OPERATOR_OR, // 3
+ OPERATOR_AND, // 4
+ OPERATOR_NOT, // 5
+ } operator_t;
+
+ typedef enum
+ {
+ NODE_LEAF,
+ NODE_AND,
+ NODE_OR,
+ } node_type_t;
+
+ // An abstract base class for nodes in the expression tree
+ class InfoSubexpression
+ {
+ public:
+ virtual ~InfoSubexpression(void) = default; // so we can destruct derived classes using a pointer to their base class
+ virtual bool Evaluate(int contextWindow, const CGUIListItem* item) = 0;
+ virtual node_type_t Type() const=0;
+ };
+
+ typedef std::shared_ptr<InfoSubexpression> InfoSubexpressionPtr;
+
+ // A leaf node in the expression tree
+ class InfoLeaf : public InfoSubexpression
+ {
+ public:
+ InfoLeaf(InfoPtr info, bool invert) : m_info(std::move(info)), m_invert(invert) {}
+ bool Evaluate(int contextWindow, const CGUIListItem* item) override;
+ node_type_t Type() const override { return NODE_LEAF; }
+
+ private:
+ InfoPtr m_info;
+ bool m_invert;
+ };
+
+ // A branch node in the expression tree
+ class InfoAssociativeGroup : public InfoSubexpression
+ {
+ public:
+ InfoAssociativeGroup(node_type_t type, const InfoSubexpressionPtr &left, const InfoSubexpressionPtr &right);
+ void AddChild(const InfoSubexpressionPtr &child);
+ void Merge(const std::shared_ptr<InfoAssociativeGroup>& other);
+ bool Evaluate(int contextWindow, const CGUIListItem* item) override;
+ node_type_t Type() const override { return m_type; }
+
+ private:
+ node_type_t m_type;
+ std::list<InfoSubexpressionPtr> m_children;
+ };
+
+ static operator_t GetOperator(char ch);
+ static void OperatorPop(std::stack<operator_t> &operator_stack, bool &invert, std::stack<InfoSubexpressionPtr> &nodes);
+ bool Parse(const std::string &expression);
+ InfoSubexpressionPtr m_expression_tree;
+};
+
+};
diff --git a/xbmc/interfaces/info/SkinVariable.cpp b/xbmc/interfaces/info/SkinVariable.cpp
new file mode 100644
index 0000000..53980b9
--- /dev/null
+++ b/xbmc/interfaces/info/SkinVariable.cpp
@@ -0,0 +1,82 @@
+/*
+ * 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 "SkinVariable.h"
+
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "utils/XBMCTinyXML.h"
+
+using namespace INFO;
+using namespace KODI;
+
+const CSkinVariableString* CSkinVariable::CreateFromXML(const TiXmlElement& node, int context)
+{
+ const char* name = node.Attribute("name");
+ if (name)
+ {
+ CSkinVariableString* tmp = new CSkinVariableString;
+ tmp->m_name = name;
+ tmp->m_context = context;
+ const TiXmlElement* valuenode = node.FirstChildElement("value");
+ while (valuenode)
+ {
+ CSkinVariableString::ConditionLabelPair pair;
+ const char *condition = valuenode->Attribute("condition");
+ if (condition)
+ pair.m_condition = CServiceBroker::GetGUI()->GetInfoManager().Register(condition, context);
+
+ auto label = valuenode->FirstChild() ? valuenode->FirstChild()->ValueStr() : "";
+ pair.m_label = GUILIB::GUIINFO::CGUIInfoLabel(label);
+ tmp->m_conditionLabelPairs.push_back(pair);
+ if (!pair.m_condition)
+ break; // once we reach default value (without condition) break iterating
+
+ valuenode = valuenode->NextSiblingElement("value");
+ }
+ if (!tmp->m_conditionLabelPairs.empty())
+ return tmp;
+ delete tmp;
+ }
+ return NULL;
+}
+
+CSkinVariableString::CSkinVariableString() = default;
+
+int CSkinVariableString::GetContext() const
+{
+ return m_context;
+}
+
+const std::string& CSkinVariableString::GetName() const
+{
+ return m_name;
+}
+
+std::string CSkinVariableString::GetValue(int contextWindow,
+ bool preferImage /* = false */,
+ const CGUIListItem* item /* = nullptr */) const
+{
+ for (const auto& it : m_conditionLabelPairs)
+ {
+ // use propagated context in case this skin variable has the default context (i.e. if not tied to a specific window)
+ // nested skin variables are supported
+ int context = m_context == INFO::DEFAULT_CONTEXT ? contextWindow : m_context;
+ if (!it.m_condition || it.m_condition->Get(context, item))
+ {
+ if (item)
+ return it.m_label.GetItemLabel(item, preferImage);
+ else
+ {
+ return it.m_label.GetLabel(context, preferImage);
+ }
+ }
+ }
+ return "";
+}
diff --git a/xbmc/interfaces/info/SkinVariable.h b/xbmc/interfaces/info/SkinVariable.h
new file mode 100644
index 0000000..f00f96f
--- /dev/null
+++ b/xbmc/interfaces/info/SkinVariable.h
@@ -0,0 +1,56 @@
+/*
+ * 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 "guilib/guiinfo/GUIInfoLabel.h"
+#include "interfaces/info/InfoBool.h"
+
+#include <string>
+#include <vector>
+
+class TiXmlElement;
+
+namespace INFO
+{
+class CSkinVariableString;
+
+class CSkinVariable
+{
+public:
+ static const CSkinVariableString* CreateFromXML(const TiXmlElement& node, int context);
+};
+
+class CSkinVariableString
+{
+public:
+ const std::string& GetName() const;
+ int GetContext() const;
+ std::string GetValue(int contextWindow,
+ bool preferImage = false,
+ const CGUIListItem* item = nullptr) const;
+
+private:
+ CSkinVariableString();
+
+ std::string m_name;
+ int m_context;
+
+ struct ConditionLabelPair
+ {
+ INFO::InfoPtr m_condition;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_label;
+ };
+
+ typedef std::vector<ConditionLabelPair> VECCONDITIONLABELPAIR;
+ VECCONDITIONLABELPAIR m_conditionLabelPairs;
+
+ friend class CSkinVariable;
+};
+
+}
diff --git a/xbmc/interfaces/json-rpc/AddonsOperations.cpp b/xbmc/interfaces/json-rpc/AddonsOperations.cpp
new file mode 100644
index 0000000..db6c90a
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/AddonsOperations.cpp
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2011-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 "AddonsOperations.h"
+
+#include "JSONUtils.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "addons/AddonDatabase.h"
+#include "addons/AddonManager.h"
+#include "addons/PluginSource.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+using namespace JSONRPC;
+using namespace ADDON;
+
+JSONRPC_STATUS CAddonsOperations::GetAddons(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::vector<AddonType> addonTypes;
+ AddonType addonType = CAddonInfo::TranslateType(parameterObject["type"].asString());
+ CPluginSource::Content content = CPluginSource::Translate(parameterObject["content"].asString());
+ CVariant enabled = parameterObject["enabled"];
+ CVariant installed = parameterObject["installed"];
+
+ // ignore the "content" parameter if the type is specified but not a plugin or script
+ if (addonType != AddonType::UNKNOWN && addonType != AddonType::PLUGIN &&
+ addonType != AddonType::SCRIPT)
+ content = CPluginSource::UNKNOWN;
+
+ if (addonType >= AddonType::VIDEO && addonType <= AddonType::EXECUTABLE)
+ {
+ addonTypes.push_back(AddonType::PLUGIN);
+ addonTypes.push_back(AddonType::SCRIPT);
+
+ switch (addonType)
+ {
+ case AddonType::VIDEO:
+ content = CPluginSource::VIDEO;
+ break;
+ case AddonType::AUDIO:
+ content = CPluginSource::AUDIO;
+ break;
+ case AddonType::IMAGE:
+ content = CPluginSource::IMAGE;
+ break;
+ case AddonType::GAME:
+ content = CPluginSource::GAME;
+ break;
+ case AddonType::EXECUTABLE:
+ content = CPluginSource::EXECUTABLE;
+ break;
+ default:
+ break;
+ }
+ }
+ else
+ addonTypes.push_back(addonType);
+
+ VECADDONS addons;
+ for (const auto& typeIt : addonTypes)
+ {
+ VECADDONS typeAddons;
+ if (typeIt == AddonType::UNKNOWN)
+ {
+ if (!enabled.isBoolean()) //All
+ {
+ if (!installed.isBoolean() || installed.asBoolean())
+ CServiceBroker::GetAddonMgr().GetInstalledAddons(typeAddons);
+ if (!installed.isBoolean() || (installed.isBoolean() && !installed.asBoolean()))
+ CServiceBroker::GetAddonMgr().GetInstallableAddons(typeAddons);
+ }
+ else if (enabled.asBoolean() && (!installed.isBoolean() || installed.asBoolean())) //Enabled
+ CServiceBroker::GetAddonMgr().GetAddons(typeAddons);
+ else if (!installed.isBoolean() || installed.asBoolean())
+ CServiceBroker::GetAddonMgr().GetDisabledAddons(typeAddons);
+ }
+ else
+ {
+ if (!enabled.isBoolean()) //All
+ {
+ if (!installed.isBoolean() || installed.asBoolean())
+ CServiceBroker::GetAddonMgr().GetInstalledAddons(typeAddons, typeIt);
+ if (!installed.isBoolean() || (installed.isBoolean() && !installed.asBoolean()))
+ CServiceBroker::GetAddonMgr().GetInstallableAddons(typeAddons, typeIt);
+ }
+ else if (enabled.asBoolean() && (!installed.isBoolean() || installed.asBoolean())) //Enabled
+ CServiceBroker::GetAddonMgr().GetAddons(typeAddons, typeIt);
+ else if (!installed.isBoolean() || installed.asBoolean())
+ CServiceBroker::GetAddonMgr().GetDisabledAddons(typeAddons, typeIt);
+ }
+
+ addons.insert(addons.end(), typeAddons.begin(), typeAddons.end());
+ }
+
+ // remove library addons
+ for (int index = 0; index < (int)addons.size(); index++)
+ {
+ std::shared_ptr<CPluginSource> plugin;
+ if (content != CPluginSource::UNKNOWN)
+ plugin = std::dynamic_pointer_cast<CPluginSource>(addons.at(index));
+
+ if ((addons.at(index)->Type() <= AddonType::UNKNOWN ||
+ addons.at(index)->Type() >= AddonType::MAX_TYPES) ||
+ ((content != CPluginSource::UNKNOWN && plugin == NULL) ||
+ (plugin != NULL && !plugin->Provides(content))))
+ {
+ addons.erase(addons.begin() + index);
+ index--;
+ }
+ }
+
+ int start, end;
+ HandleLimits(parameterObject, result, addons.size(), start, end);
+
+ CAddonDatabase addondb;
+ for (int index = start; index < end; index++)
+ FillDetails(addons.at(index), parameterObject["properties"], result["addons"], addondb, true);
+
+ return OK;
+}
+
+JSONRPC_STATUS CAddonsOperations::GetAddonDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string id = parameterObject["addonid"].asString();
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(id, addon, OnlyEnabled::CHOICE_NO) ||
+ addon.get() == NULL || addon->Type() <= AddonType::UNKNOWN ||
+ addon->Type() >= AddonType::MAX_TYPES)
+ return InvalidParams;
+
+ CAddonDatabase addondb;
+ FillDetails(addon, parameterObject["properties"], result["addon"], addondb);
+
+ return OK;
+}
+
+JSONRPC_STATUS CAddonsOperations::SetAddonEnabled(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string id = parameterObject["addonid"].asString();
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(id, addon, OnlyEnabled::CHOICE_NO) ||
+ addon == nullptr || addon->Type() <= AddonType::UNKNOWN ||
+ addon->Type() >= AddonType::MAX_TYPES)
+ return InvalidParams;
+
+ bool disabled = false;
+ if (parameterObject["enabled"].isBoolean())
+ {
+ disabled = !parameterObject["enabled"].asBoolean();
+ }
+ // we need to toggle the current disabled state of the addon
+ else if (parameterObject["enabled"].isString())
+ {
+ disabled = !CServiceBroker::GetAddonMgr().IsAddonDisabled(id);
+ }
+ else
+ {
+ return InvalidParams;
+ }
+
+ bool success = disabled
+ ? CServiceBroker::GetAddonMgr().DisableAddon(id, AddonDisabledReason::USER)
+ : CServiceBroker::GetAddonMgr().EnableAddon(id);
+
+ return success ? ACK : InvalidParams;
+}
+
+JSONRPC_STATUS CAddonsOperations::ExecuteAddon(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string id = parameterObject["addonid"].asString();
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(id, addon, OnlyEnabled::CHOICE_YES) ||
+ addon.get() == NULL || addon->Type() < AddonType::VISUALIZATION ||
+ addon->Type() >= AddonType::MAX_TYPES)
+ return InvalidParams;
+
+ std::string argv;
+ CVariant params = parameterObject["params"];
+ if (params.isObject())
+ {
+ for (CVariant::const_iterator_map it = params.begin_map(); it != params.end_map(); ++it)
+ {
+ if (it != params.begin_map())
+ argv += ",";
+ argv += it->first + "=" + it->second.asString();
+ }
+ }
+ else if (params.isArray())
+ {
+ for (CVariant::const_iterator_array it = params.begin_array(); it != params.end_array(); ++it)
+ {
+ if (it != params.begin_array())
+ argv += ",";
+ argv += StringUtils::Paramify(it->asString());
+ }
+ }
+ else if (params.isString())
+ {
+ if (!params.empty())
+ argv = StringUtils::Paramify(params.asString());
+ }
+
+ std::string cmd;
+ if (params.empty())
+ cmd = StringUtils::Format("RunAddon({})", id);
+ else
+ cmd = StringUtils::Format("RunAddon({}, {})", id, argv);
+
+ if (params["wait"].asBoolean())
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr, cmd);
+ else
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr, cmd);
+
+ return ACK;
+}
+
+static CVariant Serialize(const AddonPtr& addon)
+{
+ CVariant variant;
+ variant["addonid"] = addon->ID();
+ variant["type"] = CAddonInfo::TranslateType(addon->Type(), false);
+ variant["name"] = addon->Name();
+ variant["version"] = addon->Version().asString();
+ variant["summary"] = addon->Summary();
+ variant["description"] = addon->Description();
+ variant["path"] = addon->Path();
+ variant["author"] = addon->Author();
+ variant["thumbnail"] = addon->Icon();
+ variant["disclaimer"] = addon->Disclaimer();
+ variant["fanart"] = addon->FanArt();
+
+ variant["dependencies"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& dep : addon->GetDependencies())
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["addonid"] = dep.id;
+ info["minversion"] = dep.versionMin.asString();
+ info["version"] = dep.version.asString();
+ info["optional"] = dep.optional;
+ variant["dependencies"].push_back(std::move(info));
+ }
+ if (addon->LifecycleState() == AddonLifecycleState::BROKEN)
+ variant["broken"] = addon->LifecycleStateDescription();
+ else
+ variant["broken"] = false;
+ if (addon->LifecycleState() == AddonLifecycleState::DEPRECATED)
+ variant["deprecated"] = addon->LifecycleStateDescription();
+ else
+ variant["deprecated"] = false;
+ variant["extrainfo"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& kv : addon->ExtraInfo())
+ {
+ CVariant info(CVariant::VariantTypeObject);
+ info["key"] = kv.first;
+ info["value"] = kv.second;
+ variant["extrainfo"].push_back(std::move(info));
+ }
+ variant["rating"] = -1;
+ return variant;
+}
+
+void CAddonsOperations::FillDetails(const std::shared_ptr<ADDON::IAddon>& addon,
+ const CVariant& fields,
+ CVariant& result,
+ CAddonDatabase& addondb,
+ bool append /* = false */)
+{
+ if (addon.get() == NULL)
+ return;
+
+ CVariant addonInfo = Serialize(addon);
+
+ CVariant object;
+ object["addonid"] = addonInfo["addonid"];
+ object["type"] = addonInfo["type"];
+
+ for (unsigned int index = 0; index < fields.size(); index++)
+ {
+ std::string field = fields[index].asString();
+
+ // we need to manually retrieve the enabled / installed state of every addon
+ // from the addon database because it can't be read from addon.xml
+ if (field == "enabled")
+ {
+ object[field] = !CServiceBroker::GetAddonMgr().IsAddonDisabled(addon->ID());
+ }
+ else if (field == "installed")
+ {
+ object[field] = CServiceBroker::GetAddonMgr().IsAddonInstalled(addon->ID());
+ }
+ else if (field == "fanart" || field == "thumbnail")
+ {
+ std::string url = addonInfo[field].asString();
+ // We need to check the existence of fanart and thumbnails as the addon simply
+ // holds where the art will be, not whether it exists.
+ bool needsRecaching;
+ std::string image = CServiceBroker::GetTextureCache()->CheckCachedImage(url, needsRecaching);
+ if (!image.empty() || CFileUtils::Exists(url))
+ object[field] = CTextureUtils::GetWrappedImageURL(url);
+ else
+ object[field] = "";
+ }
+ else if (addonInfo.isMember(field))
+ object[field] = addonInfo[field];
+ }
+
+ if (append)
+ result.append(object);
+ else
+ result = object;
+}
diff --git a/xbmc/interfaces/json-rpc/AddonsOperations.h b/xbmc/interfaces/json-rpc/AddonsOperations.h
new file mode 100644
index 0000000..727f418
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/AddonsOperations.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2011-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 "JSONRPC.h"
+
+#include <memory>
+
+namespace ADDON
+{
+class CAddonDatabase;
+class IAddon;
+}
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CAddonsOperations : public CJSONUtils
+ {
+ public:
+ static JSONRPC_STATUS GetAddons(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetAddonDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS SetAddonEnabled(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS ExecuteAddon(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ private:
+ static void FillDetails(const std::shared_ptr<ADDON::IAddon>& addon,
+ const CVariant& fields,
+ CVariant& result,
+ ADDON::CAddonDatabase& addondb,
+ bool append = false);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/ApplicationOperations.cpp b/xbmc/interfaces/json-rpc/ApplicationOperations.cpp
new file mode 100644
index 0000000..aab680a
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/ApplicationOperations.cpp
@@ -0,0 +1,165 @@
+/*
+ * 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 "ApplicationOperations.h"
+
+#include "CompileInfo.h"
+#include "InputOperations.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <cmath>
+#include <string.h>
+
+using namespace JSONRPC;
+
+JSONRPC_STATUS CApplicationOperations::GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVariant properties = CVariant(CVariant::VariantTypeObject);
+ for (unsigned int index = 0; index < parameterObject["properties"].size(); index++)
+ {
+ std::string propertyName = parameterObject["properties"][index].asString();
+ CVariant property;
+ JSONRPC_STATUS ret;
+ if ((ret = GetPropertyValue(propertyName, property)) != OK)
+ return ret;
+
+ properties[propertyName] = property;
+ }
+
+ result = properties;
+
+ return OK;
+}
+
+JSONRPC_STATUS CApplicationOperations::SetVolume(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ bool up = false;
+ if (parameterObject["volume"].isInteger())
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ int oldVolume = static_cast<int>(appVolume->GetVolumePercent());
+ int volume = static_cast<int>(parameterObject["volume"].asInteger());
+
+ appVolume->SetVolume(static_cast<float>(volume), true);
+
+ up = oldVolume < volume;
+ }
+ else if (parameterObject["volume"].isString())
+ {
+ JSONRPC_STATUS ret;
+ std::string direction = parameterObject["volume"].asString();
+ if (direction.compare("increment") == 0)
+ {
+ ret = CInputOperations::SendAction(ACTION_VOLUME_UP, false, true);
+ up = true;
+ }
+ else if (direction.compare("decrement") == 0)
+ {
+ ret = CInputOperations::SendAction(ACTION_VOLUME_DOWN, false, true);
+ up = false;
+ }
+ else
+ return InvalidParams;
+
+ if (ret != ACK && ret != OK)
+ return ret;
+ }
+ else
+ return InvalidParams;
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_VOLUME_SHOW,
+ up ? ACTION_VOLUME_UP : ACTION_VOLUME_DOWN);
+
+ return GetPropertyValue("volume", result);
+}
+
+JSONRPC_STATUS CApplicationOperations::SetMute(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ if ((parameterObject["mute"].isString() &&
+ parameterObject["mute"].asString().compare("toggle") == 0) ||
+ (parameterObject["mute"].isBoolean() &&
+ parameterObject["mute"].asBoolean() != appVolume->IsMuted()))
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(ACTION_MUTE)));
+ else if (!parameterObject["mute"].isBoolean() && !parameterObject["mute"].isString())
+ return InvalidParams;
+
+ return GetPropertyValue("muted", result);
+}
+
+JSONRPC_STATUS CApplicationOperations::Quit(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+ return ACK;
+}
+
+JSONRPC_STATUS CApplicationOperations::GetPropertyValue(const std::string &property, CVariant &result)
+{
+ if (property == "volume" || property == "muted")
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ if (property == "volume")
+ result = static_cast<int>(std::lroundf(appVolume->GetVolumePercent()));
+ else if (property == "muted")
+ result = appVolume->IsMuted();
+ }
+ else if (property == "name")
+ result = CCompileInfo::GetAppName();
+ else if (property == "version")
+ {
+ result = CVariant(CVariant::VariantTypeObject);
+ result["major"] = CCompileInfo::GetMajor();
+ result["minor"] = CCompileInfo::GetMinor();
+ result["revision"] = CCompileInfo::GetSCMID();
+ std::string tag = CCompileInfo::GetSuffix();
+ if (StringUtils::StartsWithNoCase(tag, "alpha"))
+ {
+ result["tag"] = "alpha";
+ result["tagversion"] = StringUtils::Mid(tag, 5);
+ }
+ else if (StringUtils::StartsWithNoCase(tag, "beta"))
+ {
+ result["tag"] = "beta";
+ result["tagversion"] = StringUtils::Mid(tag, 4);
+ }
+ else if (StringUtils::StartsWithNoCase(tag, "rc"))
+ {
+ result["tag"] = "releasecandidate";
+ result["tagversion"] = StringUtils::Mid(tag, 2);
+ }
+ else if (tag.empty())
+ result["tag"] = "stable";
+ else
+ result["tag"] = "prealpha";
+ }
+ else if (property == "sorttokens")
+ {
+ result = CVariant(CVariant::VariantTypeArray); // Ensure no tokens returns as []
+ std::set<std::string> sortTokens = g_langInfo.GetSortTokens();
+ for (const auto& token : sortTokens)
+ result.append(token);
+ }
+ else if (property == "language")
+ result = g_langInfo.GetLocale().ToShortString();
+ else
+ return InvalidParams;
+
+ return OK;
+}
diff --git a/xbmc/interfaces/json-rpc/ApplicationOperations.h b/xbmc/interfaces/json-rpc/ApplicationOperations.h
new file mode 100644
index 0000000..464f824
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/ApplicationOperations.h
@@ -0,0 +1,30 @@
+/*
+ * 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 "FileItemHandler.h"
+#include "JSONRPC.h"
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CApplicationOperations : CFileItemHandler
+ {
+ public:
+ static JSONRPC_STATUS GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS SetVolume(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetMute(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS Quit(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ private:
+ static JSONRPC_STATUS GetPropertyValue(const std::string &property, CVariant &result);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/AudioLibrary.cpp b/xbmc/interfaces/json-rpc/AudioLibrary.cpp
new file mode 100644
index 0000000..3c73a84
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/AudioLibrary.cpp
@@ -0,0 +1,1372 @@
+/*
+ * 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 "AudioLibrary.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "Util.h"
+#include "filesystem/Directory.h"
+#include "messaging/ApplicationMessenger.h"
+#include "music/Album.h"
+#include "music/Artist.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicDbUrl.h"
+#include "music/MusicThumbLoader.h"
+#include "music/Song.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+using namespace MUSIC_INFO;
+using namespace JSONRPC;
+using namespace XFILE;
+
+JSONRPC_STATUS CAudioLibrary::GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVariant properties = CVariant(CVariant::VariantTypeObject);
+ CMusicDatabase musicdatabase;
+ // Make db connection once if one or more properties needs db access
+ for (CVariant::const_iterator_array it = parameterObject["properties"].begin_array();
+ it != parameterObject["properties"].end_array(); ++it)
+ {
+ std::string propertyName = it->asString();
+ if (propertyName == "librarylastupdated" || propertyName == "librarylastcleaned" ||
+ propertyName == "artistlinksupdated" || propertyName == "songslastadded" ||
+ propertyName == "albumslastadded" || propertyName == "artistslastadded" ||
+ propertyName == "songsmodified" || propertyName == "albumsmodified" ||
+ propertyName == "artistsmodified")
+ {
+ if (!musicdatabase.Open())
+ return InternalError;
+ else
+ break;
+ }
+ }
+
+ for (CVariant::const_iterator_array it = parameterObject["properties"].begin_array();
+ it != parameterObject["properties"].end_array(); ++it)
+ {
+ std::string propertyName = it->asString();
+ CVariant property;
+ if (propertyName == "missingartistid")
+ property = (int)BLANKARTIST_ID;
+ else if (propertyName == "librarylastupdated")
+ property = musicdatabase.GetLibraryLastUpdated();
+ else if (propertyName == "librarylastcleaned")
+ property = musicdatabase.GetLibraryLastCleaned();
+ else if (propertyName == "artistlinksupdated")
+ property = musicdatabase.GetArtistLinksUpdated();
+ else if (propertyName == "songslastadded")
+ property = musicdatabase.GetSongsLastAdded();
+ else if (propertyName == "albumslastadded")
+ property = musicdatabase.GetAlbumsLastAdded();
+ else if (propertyName == "artistslastadded")
+ property = musicdatabase.GetArtistsLastAdded();
+ else if (propertyName == "genreslastadded")
+ property = musicdatabase.GetGenresLastAdded();
+ else if (propertyName == "songsmodified")
+ property = musicdatabase.GetSongsLastModified();
+ else if (propertyName == "albumsmodified")
+ property = musicdatabase.GetAlbumsLastModified();
+ else if (propertyName == "artistsmodified")
+ property = musicdatabase.GetArtistsLastModified();
+
+ properties[propertyName] = property;
+ }
+
+ result = properties;
+ return OK;
+}
+
+
+JSONRPC_STATUS CAudioLibrary::GetArtists(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString("musicdb://artists/"))
+ return InternalError;
+
+ bool allroles = false;
+ if (parameterObject["allroles"].isBoolean())
+ allroles = parameterObject["allroles"].asBoolean();
+
+ const CVariant &filter = parameterObject["filter"];
+
+ if (allroles)
+ musicUrl.AddOption("roleid", -1000); //All roles, any negative parameter overrides implicit roleid=1 filter required for backward compatibility
+ else if (filter.isMember("roleid"))
+ musicUrl.AddOption("roleid", static_cast<int>(filter["roleid"].asInteger()));
+ else if (filter.isMember("role"))
+ musicUrl.AddOption("role", filter["role"].asString());
+ // Only one of (song) genreid/genre, albumid/album or songid/song or rules type filter is allowed by filter syntax
+ if (filter.isMember("genreid")) //Deprecated. Use "songgenre" or "artistgenre"
+ musicUrl.AddOption("genreid", static_cast<int>(filter["genreid"].asInteger()));
+ else if (filter.isMember("genre"))
+ musicUrl.AddOption("genre", filter["genre"].asString());
+ if (filter.isMember("songgenreid"))
+ musicUrl.AddOption("genreid", static_cast<int>(filter["songgenreid"].asInteger()));
+ else if (filter.isMember("songgenre"))
+ musicUrl.AddOption("genre", filter["songgenre"].asString());
+ else if (filter.isMember("albumid"))
+ musicUrl.AddOption("albumid", static_cast<int>(filter["albumid"].asInteger()));
+ else if (filter.isMember("album"))
+ musicUrl.AddOption("album", filter["album"].asString());
+ else if (filter.isMember("songid"))
+ musicUrl.AddOption("songid", static_cast<int>(filter["songid"].asInteger()));
+ else if (filter.isObject())
+ {
+ std::string xsp;
+ if (!GetXspFiltering("artists", filter, xsp))
+ return InvalidParams;
+
+ musicUrl.AddOption("xsp", xsp);
+ }
+
+ bool albumArtistsOnly = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS);
+ if (parameterObject["albumartistsonly"].isBoolean())
+ albumArtistsOnly = parameterObject["albumartistsonly"].asBoolean();
+ musicUrl.AddOption("albumartistsonly", albumArtistsOnly);
+
+ SortDescription sorting;
+ ParseLimits(parameterObject, sorting.limitStart, sorting.limitEnd);
+ if (!ParseSorting(parameterObject, sorting.sortBy, sorting.sortOrder, sorting.sortAttributes))
+ return InvalidParams;
+
+ int total;
+ std::set<std::string> fields;
+ if (parameterObject.isMember("properties") && parameterObject["properties"].isArray())
+ {
+ for (CVariant::const_iterator_array field = parameterObject["properties"].begin_array();
+ field != parameterObject["properties"].end_array(); ++field)
+ fields.insert(field->asString());
+ }
+
+ musicdatabase.SetTranslateBlankArtist(false);
+ if (!musicdatabase.GetArtistsByWhereJSON(fields, musicUrl.ToString(), result, total, sorting))
+ return InternalError;
+
+ int start, end;
+ HandleLimits(parameterObject, result, total, start, end);
+
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetArtistDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int artistID = (int)parameterObject["artistid"].asInteger();
+
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString("musicdb://artists/"))
+ return InternalError;
+
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ musicUrl.AddOption("artistid", artistID);
+
+ CFileItemList items;
+ CDatabase::Filter filter;
+ if (!musicdatabase.GetArtistsByWhere(musicUrl.ToString(), filter, items) || items.Size() != 1)
+ return InvalidParams;
+
+ // Add "artist" to "properties" array by default
+ CVariant param = parameterObject;
+ if (!param.isMember("properties"))
+ param["properties"] = CVariant(CVariant::VariantTypeArray);
+ param["properties"].append("artist");
+
+ //Get roleids, roles etc. if needed
+ JSONRPC_STATUS ret = GetAdditionalArtistDetails(parameterObject, items, musicdatabase);
+ if (ret != OK)
+ return ret;
+
+ HandleFileItem("artistid", false, "artistdetails", items[0], param, param["properties"], result, false);
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetAlbums(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString("musicdb://albums/"))
+ return InternalError;
+
+ if (parameterObject["includesingles"].asBoolean())
+ musicUrl.AddOption("show_singles", true);
+
+ bool allroles = false;
+ if (parameterObject["allroles"].isBoolean())
+ allroles = parameterObject["allroles"].asBoolean();
+
+ const CVariant &filter = parameterObject["filter"];
+
+ if (allroles)
+ musicUrl.AddOption("roleid", -1000); //All roles, override implicit roleid=1 filter required for backward compatibility
+ else if (filter.isMember("roleid"))
+ musicUrl.AddOption("roleid", static_cast<int>(filter["roleid"].asInteger()));
+ else if (filter.isMember("role"))
+ musicUrl.AddOption("role", filter["role"].asString());
+ // Only one of genreid/genre, artistid/artist or rules type filter is allowed by filter syntax
+ if (filter.isMember("artistid"))
+ musicUrl.AddOption("artistid", static_cast<int>(filter["artistid"].asInteger()));
+ else if (filter.isMember("artist"))
+ musicUrl.AddOption("artist", filter["artist"].asString());
+ else if (filter.isMember("genreid"))
+ musicUrl.AddOption("genreid", static_cast<int>(filter["genreid"].asInteger()));
+ else if (filter.isMember("genre"))
+ musicUrl.AddOption("genre", filter["genre"].asString());
+ else if (filter.isObject())
+ {
+ std::string xsp;
+ if (!GetXspFiltering("albums", filter, xsp))
+ return InvalidParams;
+
+ musicUrl.AddOption("xsp", xsp);
+ }
+
+ SortDescription sorting;
+ ParseLimits(parameterObject, sorting.limitStart, sorting.limitEnd);
+ if (!ParseSorting(parameterObject, sorting.sortBy, sorting.sortOrder, sorting.sortAttributes))
+ return InvalidParams;
+
+ int total;
+ std::set<std::string> fields;
+ if (parameterObject.isMember("properties") && parameterObject["properties"].isArray())
+ {
+ for (CVariant::const_iterator_array field = parameterObject["properties"].begin_array();
+ field != parameterObject["properties"].end_array(); ++field)
+ fields.insert(field->asString());
+ }
+
+ if (!musicdatabase.GetAlbumsByWhereJSON(fields, musicUrl.ToString(), result, total, sorting))
+ return InternalError;
+
+ if (!result.isNull())
+ {
+ bool bFetchArt = fields.find("art") != fields.end();
+ bool bFetchFanart = fields.find("fanart") != fields.end();
+ if (bFetchArt || bFetchFanart)
+ {
+ CThumbLoader* thumbLoader = new CMusicThumbLoader();
+ thumbLoader->OnLoaderStart();
+
+ std::set<std::string> artfields;
+ if (bFetchArt)
+ artfields.insert("art");
+ if (bFetchFanart)
+ artfields.insert("fanart");
+
+ for (unsigned int index = 0; index < result["albums"].size(); index++)
+ {
+ CFileItem item;
+ item.GetMusicInfoTag()->SetDatabaseId(result["albums"][index]["albumid"].asInteger32(), MediaTypeAlbum);
+
+ // Could use FillDetails, but it does unnecessary serialization of empty MusiInfoTag
+ // CFileItemPtr itemptr(new CFileItem(item));
+ // FillDetails(item.GetMusicInfoTag(), itemptr, artfields, result["albums"][index], thumbLoader);
+
+ thumbLoader->FillLibraryArt(item);
+
+ if (bFetchFanart)
+ {
+ if (item.HasArt("fanart"))
+ result["albums"][index]["fanart"] = CTextureUtils::GetWrappedImageURL(item.GetArt("fanart"));
+ else
+ result["albums"][index]["fanart"] = "";
+ }
+ if (bFetchArt)
+ {
+ CGUIListItem::ArtMap artMap = item.GetArt();
+ CVariant artObj(CVariant::VariantTypeObject);
+ for (const auto& artIt : artMap)
+ {
+ if (!artIt.second.empty())
+ artObj[artIt.first] = CTextureUtils::GetWrappedImageURL(artIt.second);
+ }
+ result["albums"][index]["art"] = artObj;
+ }
+ }
+
+ delete thumbLoader;
+ }
+ }
+
+ int start, end;
+ HandleLimits(parameterObject, result, total, start, end);
+
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetAlbumDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int albumID = (int)parameterObject["albumid"].asInteger();
+
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CAlbum album;
+ if (!musicdatabase.GetAlbum(albumID, album, false))
+ return InvalidParams;
+
+ std::string path = StringUtils::Format("musicdb://albums/{}/", albumID);
+
+ CFileItemPtr albumItem;
+ FillAlbumItem(album, path, albumItem);
+
+ CFileItemList items;
+ items.Add(albumItem);
+ JSONRPC_STATUS ret = GetAdditionalAlbumDetails(parameterObject, items, musicdatabase);
+ if (ret != OK)
+ return ret;
+
+ HandleFileItem("albumid", false, "albumdetails", items[0], parameterObject, parameterObject["properties"], result, false);
+
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetSongs(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString("musicdb://songs/"))
+ return InternalError;
+
+ if (parameterObject["singlesonly"].asBoolean())
+ musicUrl.AddOption("singles", true);
+ else if (!parameterObject["includesingles"].asBoolean())
+ musicUrl.AddOption("singles", false);
+
+ bool allroles = false;
+ if (parameterObject["allroles"].isBoolean())
+ allroles = parameterObject["allroles"].asBoolean();
+
+ const CVariant &filter = parameterObject["filter"];
+
+ if (allroles)
+ musicUrl.AddOption("roleid", -1000); //All roles, override implicit roleid=1 filter required for backward compatibility
+ else if (filter.isMember("roleid"))
+ musicUrl.AddOption("roleid", static_cast<int>(filter["roleid"].asInteger()));
+ else if (filter.isMember("role"))
+ musicUrl.AddOption("role", filter["role"].asString());
+ // Only one of genreid/genre, artistid/artist, albumid/album or rules type filter is allowed by filter syntax
+ if (filter.isMember("artistid"))
+ musicUrl.AddOption("artistid", static_cast<int>(filter["artistid"].asInteger()));
+ else if (filter.isMember("artist"))
+ musicUrl.AddOption("artist", filter["artist"].asString());
+ else if (filter.isMember("genreid"))
+ musicUrl.AddOption("genreid", static_cast<int>(filter["genreid"].asInteger()));
+ else if (filter.isMember("genre"))
+ musicUrl.AddOption("genre", filter["genre"].asString());
+ else if (filter.isMember("albumid"))
+ musicUrl.AddOption("albumid", static_cast<int>(filter["albumid"].asInteger()));
+ else if (filter.isMember("album"))
+ musicUrl.AddOption("album", filter["album"].asString());
+ else if (filter.isObject())
+ {
+ std::string xsp;
+ if (!GetXspFiltering("songs", filter, xsp))
+ return InvalidParams;
+
+ musicUrl.AddOption("xsp", xsp);
+ }
+
+ SortDescription sorting;
+ ParseLimits(parameterObject, sorting.limitStart, sorting.limitEnd);
+ if (!ParseSorting(parameterObject, sorting.sortBy, sorting.sortOrder, sorting.sortAttributes))
+ return InvalidParams;
+
+ int total;
+ std::set<std::string> fields;
+ if (parameterObject.isMember("properties") && parameterObject["properties"].isArray())
+ {
+ for (CVariant::const_iterator_array field = parameterObject["properties"].begin_array();
+ field != parameterObject["properties"].end_array(); ++field)
+ fields.insert(field->asString());
+ }
+
+ if (!musicdatabase.GetSongsByWhereJSON(fields, musicUrl.ToString(), result, total, sorting))
+ return InternalError;
+
+ if (!result.isNull())
+ {
+ bool bFetchArt = fields.find("art") != fields.end();
+ bool bFetchFanart = fields.find("fanart") != fields.end();
+ bool bFetchThumb = fields.find("thumbnail") != fields.end();
+ if (bFetchArt || bFetchFanart || bFetchThumb)
+ {
+ CThumbLoader* thumbLoader = new CMusicThumbLoader();
+ thumbLoader->OnLoaderStart();
+
+ std::set<std::string> artfields;
+ if (bFetchArt)
+ artfields.insert("art");
+ if (bFetchFanart)
+ artfields.insert("fanart");
+ if (bFetchThumb)
+ artfields.insert("thumbnail");
+
+ for (unsigned int index = 0; index < result["songs"].size(); index++)
+ {
+ CFileItem item;
+ // Only needs song and album id (if we have it) set to get art
+ // Getting art is quicker if "albumid" has been fetched
+ item.GetMusicInfoTag()->SetDatabaseId(result["songs"][index]["songid"].asInteger32(), MediaTypeSong);
+ if (result["songs"][index].isMember("albumid"))
+ item.GetMusicInfoTag()->SetAlbumId(result["songs"][index]["albumid"].asInteger32());
+ else
+ item.GetMusicInfoTag()->SetAlbumId(-1);
+
+ // Could use FillDetails, but it does unnecessary serialization of empty MusiInfoTag
+ // CFileItemPtr itemptr(new CFileItem(item));
+ // FillDetails(item.GetMusicInfoTag(), itemptr, artfields, result["songs"][index], thumbLoader);
+
+ thumbLoader->FillLibraryArt(item);
+
+ if (bFetchThumb)
+ {
+ if (item.HasArt("thumb"))
+ result["songs"][index]["thumbnail"] = CTextureUtils::GetWrappedImageURL(item.GetArt("thumb"));
+ else
+ result["songs"][index]["thumbnail"] = "";
+ }
+ if (bFetchFanart)
+ {
+ if (item.HasArt("fanart"))
+ result["songs"][index]["fanart"] = CTextureUtils::GetWrappedImageURL(item.GetArt("fanart"));
+ else
+ result["songs"][index]["fanart"] = "";
+ }
+ if (bFetchArt)
+ {
+ CGUIListItem::ArtMap artMap = item.GetArt();
+ CVariant artObj(CVariant::VariantTypeObject);
+ for (const auto& artIt : artMap)
+ {
+ if (!artIt.second.empty())
+ artObj[artIt.first] = CTextureUtils::GetWrappedImageURL(artIt.second);
+ }
+ result["songs"][index]["art"] = artObj;
+ }
+ }
+
+ delete thumbLoader;
+ }
+ }
+
+ int start, end;
+ HandleLimits(parameterObject, result, total, start, end);
+
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetSongDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int idSong = (int)parameterObject["songid"].asInteger();
+
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CSong song;
+ if (!musicdatabase.GetSong(idSong, song))
+ return InvalidParams;
+
+ CFileItemList items;
+ CFileItemPtr item = CFileItemPtr(new CFileItem(song));
+ FillItemArtistIDs(song.GetArtistIDArray(), item);
+ items.Add(item);
+
+ JSONRPC_STATUS ret = GetAdditionalSongDetails(parameterObject, items, musicdatabase);
+ if (ret != OK)
+ return ret;
+
+ HandleFileItem("songid", true, "songdetails", items[0], parameterObject, parameterObject["properties"], result, false);
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetRecentlyAddedAlbums(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ VECALBUMS albums;
+ if (!musicdatabase.GetRecentlyAddedAlbums(albums))
+ return InternalError;
+
+ CFileItemList items;
+ for (unsigned int index = 0; index < albums.size(); index++)
+ {
+ std::string path =
+ StringUtils::Format("musicdb://recentlyaddedalbums/{}/", albums[index].idAlbum);
+
+ CFileItemPtr item;
+ FillAlbumItem(albums[index], path, item);
+ items.Add(item);
+ }
+
+ JSONRPC_STATUS ret = GetAdditionalAlbumDetails(parameterObject, items, musicdatabase);
+ if (ret != OK)
+ return ret;
+
+ HandleFileItemList("albumid", false, "albums", items, parameterObject, result);
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetRecentlyAddedSongs(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ int amount = (int)parameterObject["albumlimit"].asInteger();
+ if (amount < 0)
+ amount = 0;
+
+ CFileItemList items;
+ if (!musicdatabase.GetRecentlyAddedAlbumSongs("musicdb://songs/", items, (unsigned int)amount))
+ return InternalError;
+
+ JSONRPC_STATUS ret = GetAdditionalSongDetails(parameterObject, items, musicdatabase);
+ if (ret != OK)
+ return ret;
+
+ HandleFileItemList("songid", true, "songs", items, parameterObject, result);
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetRecentlyPlayedAlbums(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ VECALBUMS albums;
+ if (!musicdatabase.GetRecentlyPlayedAlbums(albums))
+ return InternalError;
+
+ CFileItemList items;
+ for (unsigned int index = 0; index < albums.size(); index++)
+ {
+ std::string path =
+ StringUtils::Format("musicdb://recentlyplayedalbums/{}/", albums[index].idAlbum);
+
+ CFileItemPtr item;
+ FillAlbumItem(albums[index], path, item);
+ items.Add(item);
+ }
+
+ JSONRPC_STATUS ret = GetAdditionalAlbumDetails(parameterObject, items, musicdatabase);
+ if (ret != OK)
+ return ret;
+
+ HandleFileItemList("albumid", false, "albums", items, parameterObject, result);
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetRecentlyPlayedSongs(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CFileItemList items;
+ if (!musicdatabase.GetRecentlyPlayedAlbumSongs("musicdb://songs/", items))
+ return InternalError;
+
+ JSONRPC_STATUS ret = GetAdditionalSongDetails(parameterObject, items, musicdatabase);
+ if (ret != OK)
+ return ret;
+
+ HandleFileItemList("songid", true, "songs", items, parameterObject, result);
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetGenres(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ // Check if sources for genre wanted
+ bool sourcesneeded(false);
+ std::set<std::string> checkProperties;
+ checkProperties.insert("sourceid");
+ std::set<std::string> additionalProperties;
+ if (CheckForAdditionalProperties(parameterObject["properties"], checkProperties, additionalProperties))
+ sourcesneeded = (additionalProperties.find("sourceid") != additionalProperties.end());
+
+ CFileItemList items;
+ if (!musicdatabase.GetGenresJSON(items, sourcesneeded))
+ return InternalError;
+
+ HandleFileItemList("genreid", false, "genres", items, parameterObject, result);
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetRoles(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CFileItemList items;
+ if (!musicdatabase.GetRolesNav("musicdb://songs/", items))
+ return InternalError;
+
+ /* need to set strTitle in each item*/
+ for (unsigned int i = 0; i < (unsigned int)items.Size(); i++)
+ items[i]->GetMusicInfoTag()->SetTitle(items[i]->GetLabel());
+
+ HandleFileItemList("roleid", false, "roles", items, parameterObject, result);
+ return OK;
+}
+
+JSONRPC_STATUS JSONRPC::CAudioLibrary::GetSources(const std::string& method, ITransportLayer* transport, IClient* client, const CVariant& parameterObject, CVariant& result)
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ // Add "file" to "properties" array by default
+ CVariant param = parameterObject;
+ if (!param.isMember("properties"))
+ param["properties"] = CVariant(CVariant::VariantTypeArray);
+ if (!param["properties"].isMember("file"))
+ param["properties"].append("file");
+
+ CFileItemList items;
+ if (!musicdatabase.GetSources(items))
+ return InternalError;
+
+ HandleFileItemList("sourceid", true, "sources", items, param, result);
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetAvailableArtTypes(const std::string& method, ITransportLayer* transport, IClient* client, const CVariant& parameterObject, CVariant& result)
+{
+ std::string mediaType;
+ int mediaID = -1;
+ if (parameterObject["item"].isMember("albumid"))
+ {
+ mediaType = MediaTypeAlbum;
+ mediaID = parameterObject["item"]["albumid"].asInteger32();
+ }
+ if (parameterObject["item"].isMember("artistid"))
+ {
+ mediaType = MediaTypeArtist;
+ mediaID = parameterObject["item"]["artistid"].asInteger32();
+ }
+ if (mediaID == -1)
+ return InternalError;
+
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CVariant availablearttypes = CVariant(CVariant::VariantTypeArray);
+ for (const auto& artType : musicdatabase.GetAvailableArtTypesForItem(mediaID, mediaType))
+ {
+ availablearttypes.append(artType);
+ }
+ result = CVariant(CVariant::VariantTypeObject);
+ result["availablearttypes"] = availablearttypes;
+
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetAvailableArt(const std::string& method, ITransportLayer* transport, IClient* client, const CVariant& parameterObject, CVariant& result)
+{
+ std::string mediaType;
+ int mediaID = -1;
+ if (parameterObject["item"].isMember("albumid"))
+ {
+ mediaType = MediaTypeAlbum;
+ mediaID = parameterObject["item"]["albumid"].asInteger32();
+ }
+ if (parameterObject["item"].isMember("artistid"))
+ {
+ mediaType = MediaTypeArtist;
+ mediaID = parameterObject["item"]["artistid"].asInteger32();
+ }
+ if (mediaID == -1)
+ return InternalError;
+
+ std::string artType = parameterObject["arttype"].asString();
+ StringUtils::ToLower(artType);
+
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CVariant availableart = CVariant(CVariant::VariantTypeArray);
+ for (const auto& artentry : musicdatabase.GetAvailableArtForItem(mediaID, mediaType, artType))
+ {
+ CVariant item = CVariant(CVariant::VariantTypeObject);
+ item["url"] = CTextureUtils::GetWrappedImageURL(artentry.m_url);
+ item["arttype"] = artentry.m_aspect;
+ if (!artentry.m_preview.empty())
+ item["previewurl"] = CTextureUtils::GetWrappedImageURL(artentry.m_preview);
+ availableart.append(item);
+ }
+ result = CVariant(CVariant::VariantTypeObject);
+ result["availableart"] = availableart;
+
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::SetArtistDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["artistid"].asInteger();
+
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CArtist artist;
+ if (!musicdatabase.GetArtist(id, artist) || artist.idArtist <= 0)
+ return InvalidParams;
+
+ if (ParameterNotNull(parameterObject, "artist"))
+ artist.strArtist = parameterObject["artist"].asString();
+ if (ParameterNotNull(parameterObject, "instrument"))
+ CopyStringArray(parameterObject["instrument"], artist.instruments);
+ if (ParameterNotNull(parameterObject, "style"))
+ CopyStringArray(parameterObject["style"], artist.styles);
+ if (ParameterNotNull(parameterObject, "mood"))
+ CopyStringArray(parameterObject["mood"], artist.moods);
+ if (ParameterNotNull(parameterObject, "born"))
+ artist.strBorn = parameterObject["born"].asString();
+ if (ParameterNotNull(parameterObject, "formed"))
+ artist.strFormed = parameterObject["formed"].asString();
+ if (ParameterNotNull(parameterObject, "description"))
+ artist.strBiography = parameterObject["description"].asString();
+ if (ParameterNotNull(parameterObject, "genre"))
+ CopyStringArray(parameterObject["genre"], artist.genre);
+ if (ParameterNotNull(parameterObject, "died"))
+ artist.strDied = parameterObject["died"].asString();
+ if (ParameterNotNull(parameterObject, "disbanded"))
+ artist.strDisbanded = parameterObject["disbanded"].asString();
+ if (ParameterNotNull(parameterObject, "yearsactive"))
+ CopyStringArray(parameterObject["yearsactive"], artist.yearsActive);
+ if (ParameterNotNull(parameterObject, "musicbrainzartistid"))
+ artist.strMusicBrainzArtistID = parameterObject["musicbrainzartistid"].asString();
+ if (ParameterNotNull(parameterObject, "sortname"))
+ artist.strSortName = parameterObject["sortname"].asString();
+ if (ParameterNotNull(parameterObject, "type"))
+ artist.strType = parameterObject["type"].asString();
+ if (ParameterNotNull(parameterObject, "gender"))
+ artist.strGender = parameterObject["gender"].asString();
+ if (ParameterNotNull(parameterObject, "disambiguation"))
+ artist.strDisambiguation = parameterObject["disambiguation"].asString();
+
+ // Update existing art. Any existing artwork that isn't specified in this request stays as is.
+ // If the value is null then the existing art with that type is removed.
+ if (ParameterNotNull(parameterObject, "art"))
+ {
+ // Get current artwork
+ musicdatabase.GetArtForItem(artist.idArtist, MediaTypeArtist, artist.art);
+
+ std::set<std::string> removedArtwork;
+ CVariant art = parameterObject["art"];
+ for (CVariant::const_iterator_map artIt = art.begin_map(); artIt != art.end_map(); ++artIt)
+ {
+ if (artIt->second.isString() && !artIt->second.asString().empty())
+ artist.art[artIt->first] = CTextureUtils::UnwrapImageURL(artIt->second.asString());
+ else if (artIt->second.isNull())
+ {
+ artist.art.erase(artIt->first);
+ removedArtwork.insert(artIt->first);
+ }
+ }
+ // Remove null art now, as not done by update
+ if (!musicdatabase.RemoveArtForItem(artist.idArtist, MediaTypeArtist, removedArtwork))
+ return InternalError;
+ }
+
+ // Update artist including adding or replacing (but not removing) art
+ if (!musicdatabase.UpdateArtist(artist))
+ return InternalError;
+
+ CJSONRPCUtils::NotifyItemUpdated();
+ return ACK;
+}
+
+JSONRPC_STATUS CAudioLibrary::SetAlbumDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["albumid"].asInteger();
+
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CAlbum album;
+ // Get current album details, but not songs as we do not want to update them here
+ if (!musicdatabase.GetAlbum(id, album, false) || album.idAlbum <= 0)
+ return InvalidParams;
+
+ if (ParameterNotNull(parameterObject, "title"))
+ album.strAlbum = parameterObject["title"].asString();
+ if (ParameterNotNull(parameterObject, "displayartist"))
+ album.strArtistDesc = parameterObject["displayartist"].asString();
+ // Set album sort string before processing artist credits
+ if (ParameterNotNull(parameterObject, "sortartist"))
+ album.strArtistSort = parameterObject["sortartist"].asString();
+
+ // Match up artist names and mbids to make new artist credits
+ // Mbid values only apply if there are names
+ if (ParameterNotNull(parameterObject, "artist"))
+ {
+ std::vector<std::string> artists;
+ std::vector<std::string> mbids;
+ CopyStringArray(parameterObject["artist"], artists);
+ // Check for Musicbrainz ids
+ if (ParameterNotNull(parameterObject, "musicbrainzalbumartistid"))
+ CopyStringArray(parameterObject["musicbrainzalbumartistid"], mbids);
+ // When display artist is not provided and yet artists is changing make by concatenation
+ if (!ParameterNotNull(parameterObject, "displayartist"))
+ album.strArtistDesc = StringUtils::Join(artists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ album.SetArtistCredits(artists, std::vector<std::string>(), mbids);
+ // On updatealbum artists will be changed
+ album.bArtistSongMerge = true;
+ }
+
+ if (ParameterNotNull(parameterObject, "description"))
+ album.strReview = parameterObject["description"].asString();
+ if (ParameterNotNull(parameterObject, "genre"))
+ CopyStringArray(parameterObject["genre"], album.genre);
+ if (ParameterNotNull(parameterObject, "theme"))
+ CopyStringArray(parameterObject["theme"], album.themes);
+ if (ParameterNotNull(parameterObject, "mood"))
+ CopyStringArray(parameterObject["mood"], album.moods);
+ if (ParameterNotNull(parameterObject, "style"))
+ CopyStringArray(parameterObject["style"], album.styles);
+ if (ParameterNotNull(parameterObject, "type"))
+ album.strType = parameterObject["type"].asString();
+ if (ParameterNotNull(parameterObject, "albumlabel"))
+ album.strLabel = parameterObject["albumlabel"].asString();
+ if (ParameterNotNull(parameterObject, "rating"))
+ album.fRating = parameterObject["rating"].asFloat();
+ if (ParameterNotNull(parameterObject, "userrating"))
+ album.iUserrating = static_cast<int>(parameterObject["userrating"].asInteger());
+ if (ParameterNotNull(parameterObject, "votes"))
+ album.iVotes = static_cast<int>(parameterObject["votes"].asInteger());
+ if (ParameterNotNull(parameterObject, "year"))
+ album.strReleaseDate = parameterObject["year"].asString();
+ if (ParameterNotNull(parameterObject, "musicbrainzalbumid"))
+ album.strMusicBrainzAlbumID = parameterObject["musicbrainzalbumid"].asString();
+ if (ParameterNotNull(parameterObject, "musicbrainzreleasegroupid"))
+ album.strReleaseGroupMBID = parameterObject["musicbrainzreleasegroupid"].asString();
+ if (ParameterNotNull(parameterObject, "isboxset"))
+ album.bBoxedSet = parameterObject["isboxset"].asBoolean();
+ if (ParameterNotNull(parameterObject, "originaldate"))
+ album.strOrigReleaseDate = parameterObject["originaldate"].asString();
+ if (ParameterNotNull(parameterObject, "releasedate"))
+ album.strReleaseDate = parameterObject["releasedate"].asString();
+ if (ParameterNotNull(parameterObject, "albumstatus"))
+ album.strReleaseStatus = parameterObject["albumstatus"].asString();
+
+ // Update existing art. Any existing artwork that isn't specified in this request stays as is.
+ // If the value is null then the existing art with that type is removed.
+ if (ParameterNotNull(parameterObject, "art"))
+ {
+ // Get current artwork
+ musicdatabase.GetArtForItem(album.idAlbum, MediaTypeAlbum, album.art);
+
+ std::set<std::string> removedArtwork;
+ CVariant art = parameterObject["art"];
+ for (CVariant::const_iterator_map artIt = art.begin_map(); artIt != art.end_map(); ++artIt)
+ {
+ if (artIt->second.isString() && !artIt->second.asString().empty())
+ album.art[artIt->first] = CTextureUtils::UnwrapImageURL(artIt->second.asString());
+ else if (artIt->second.isNull())
+ {
+ album.art.erase(artIt->first);
+ removedArtwork.insert(artIt->first);
+ }
+ }
+ // Remove null art now, as not done by update
+ if (!musicdatabase.RemoveArtForItem(album.idAlbum, MediaTypeAlbum, removedArtwork))
+ return InternalError;
+ }
+
+ // Update artist including adding or replacing (but not removing) art
+ if (!musicdatabase.UpdateAlbum(album))
+ return InternalError;
+
+ CJSONRPCUtils::NotifyItemUpdated();
+ return ACK;
+}
+
+JSONRPC_STATUS CAudioLibrary::SetSongDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["songid"].asInteger();
+
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ CSong song;
+ if (!musicdatabase.GetSong(id, song) || song.idSong != id)
+ return InvalidParams;
+
+ if (ParameterNotNull(parameterObject, "title"))
+ song.strTitle = parameterObject["title"].asString();
+
+ if (ParameterNotNull(parameterObject, "displayartist"))
+ song.strArtistDesc = parameterObject["displayartist"].asString();
+ // Set album sort string before processing artist credits
+ if (ParameterNotNull(parameterObject, "sortartist"))
+ song.strArtistSort = parameterObject["sortartist"].asString();
+
+ // Match up artist names and mbids to make new artist credits
+ // Mbid values only apply if there are names
+ bool updateartists = false;
+ if (ParameterNotNull(parameterObject, "artist"))
+ {
+ std::vector<std::string> artists, mbids;
+ updateartists = true;
+ CopyStringArray(parameterObject["artist"], artists);
+ // Check for Musicbrainz ids
+ if (ParameterNotNull(parameterObject, "musicbrainzartistid"))
+ CopyStringArray(parameterObject["musicbrainzartistid"], mbids);
+ // When display artist is not provided and yet artists is changing make by concatenation
+ if (!ParameterNotNull(parameterObject, "displayartist"))
+ song.strArtistDesc = StringUtils::Join(artists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ song.SetArtistCredits(artists, std::vector<std::string>(), mbids);
+ }
+
+ if (ParameterNotNull(parameterObject, "genre"))
+ CopyStringArray(parameterObject["genre"], song.genre);
+ if (ParameterNotNull(parameterObject, "year"))
+ song.strReleaseDate = parameterObject["year"].asString();
+ if (ParameterNotNull(parameterObject, "rating"))
+ song.rating = parameterObject["rating"].asFloat();
+ if (ParameterNotNull(parameterObject, "userrating"))
+ song.userrating = static_cast<int>(parameterObject["userrating"].asInteger());
+ if (ParameterNotNull(parameterObject, "track"))
+ song.iTrack = (song.iTrack & 0xffff0000) | ((int)parameterObject["track"].asInteger() & 0xffff);
+ if (ParameterNotNull(parameterObject, "disc"))
+ song.iTrack = (song.iTrack & 0xffff) | ((int)parameterObject["disc"].asInteger() << 16);
+ if (ParameterNotNull(parameterObject, "duration"))
+ song.iDuration = (int)parameterObject["duration"].asInteger();
+ if (ParameterNotNull(parameterObject, "comment"))
+ song.strComment = parameterObject["comment"].asString();
+ if (ParameterNotNull(parameterObject, "musicbrainztrackid"))
+ song.strMusicBrainzTrackID = parameterObject["musicbrainztrackid"].asString();
+ if (ParameterNotNull(parameterObject, "playcount"))
+ song.iTimesPlayed = static_cast<int>(parameterObject["playcount"].asInteger());
+ if (ParameterNotNull(parameterObject, "lastplayed"))
+ song.lastPlayed.SetFromDBDateTime(parameterObject["lastplayed"].asString());
+ if (ParameterNotNull(parameterObject, "mood"))
+ song.strMood = parameterObject["mood"].asString();
+ if (ParameterNotNull(parameterObject, "disctitle"))
+ song.strDiscSubtitle = parameterObject["disctitle"].asString();
+ if (ParameterNotNull(parameterObject, "bpm"))
+ song.iBPM = static_cast<int>(parameterObject["bpm"].asInteger());
+ if (ParameterNotNull(parameterObject, "originaldate"))
+ song.strOrigReleaseDate = parameterObject["originaldate"].asString();
+ if (ParameterNotNull(parameterObject, "albumreleasedate"))
+ song.strReleaseDate = parameterObject["albumreleasedate"].asString();
+
+ // Update existing art. Any existing artwork that isn't specified in this request stays as is.
+ // If the value is null then the existing art with that type is removed.
+ if (ParameterNotNull(parameterObject, "art"))
+ {
+ // Get current artwork
+ std::map<std::string, std::string> artwork;
+ musicdatabase.GetArtForItem(song.idSong, MediaTypeSong, artwork);
+
+ std::set<std::string> removedArtwork;
+ CVariant art = parameterObject["art"];
+ for (CVariant::const_iterator_map artIt = art.begin_map(); artIt != art.end_map(); ++artIt)
+ {
+ if (artIt->second.isString() && !artIt->second.asString().empty())
+ artwork[artIt->first] = CTextureUtils::UnwrapImageURL(artIt->second.asString());
+ else if (artIt->second.isNull())
+ {
+ artwork.erase(artIt->first);
+ removedArtwork.insert(artIt->first);
+ }
+ }
+ //Update artwork, not done in update song
+ musicdatabase.SetArtForItem(song.idSong, MediaTypeSong, artwork);
+ if (!musicdatabase.RemoveArtForItem(song.idSong, MediaTypeSong, removedArtwork))
+ return InternalError;
+ }
+
+ // Update song (not including artwork)
+ if (!musicdatabase.UpdateSong(song, updateartists))
+ return InternalError;
+
+ CJSONRPCUtils::NotifyItemUpdated();
+ return ACK;
+}
+
+JSONRPC_STATUS CAudioLibrary::Scan(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string directory = parameterObject["directory"].asString();
+ std::string cmd =
+ StringUtils::Format("updatelibrary(music, {}, {})", StringUtils::Paramify(directory),
+ parameterObject["showdialogs"].asBoolean() ? "true" : "false");
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr, cmd);
+ return ACK;
+}
+
+JSONRPC_STATUS CAudioLibrary::Export(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string cmd;
+ if (parameterObject["options"].isMember("path"))
+ cmd = StringUtils::Format("exportlibrary2(music, singlefile, {}, albums, albumartists)",
+ StringUtils::Paramify(parameterObject["options"]["path"].asString()));
+ else
+ {
+ cmd = "exportlibrary2(music, library, dummy, albums, albumartists";
+ if (parameterObject["options"]["images"].isBoolean() &&
+ parameterObject["options"]["images"].asBoolean() == true)
+ cmd += ", artwork";
+ if (parameterObject["options"]["overwrite"].isBoolean() &&
+ parameterObject["options"]["overwrite"].asBoolean() == true)
+ cmd += ", overwrite";
+ cmd += ")";
+ }
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr, cmd);
+ return ACK;
+}
+
+JSONRPC_STATUS CAudioLibrary::Clean(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string cmd = StringUtils::Format(
+ "cleanlibrary(music, {})", parameterObject["showdialogs"].asBoolean() ? "true" : "false");
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr, cmd);
+ return ACK;
+}
+
+bool CAudioLibrary::FillFileItem(
+ const std::string& strFilename,
+ std::shared_ptr<CFileItem>& item,
+ const CVariant& parameterObject /* = CVariant(CVariant::VariantTypeArray) */)
+{
+ CMusicDatabase musicdatabase;
+ if (strFilename.empty())
+ return false;
+
+ bool filled = false;
+ if (musicdatabase.Open())
+ {
+ if (CDirectory::Exists(strFilename))
+ {
+ CAlbum album;
+ int albumid = musicdatabase.GetAlbumIdByPath(strFilename);
+ if (musicdatabase.GetAlbum(albumid, album, false))
+ {
+ item->SetFromAlbum(album);
+ FillItemArtistIDs(album.GetArtistIDArray(), item);
+
+ CFileItemList items;
+ items.Add(item);
+
+ if (GetAdditionalAlbumDetails(parameterObject, items, musicdatabase) == OK)
+ filled = true;
+ }
+ }
+ else
+ {
+ CSong song;
+ if (musicdatabase.GetSongByFileName(strFilename, song))
+ {
+ item->SetFromSong(song);
+ FillItemArtistIDs(song.GetArtistIDArray(), item);
+
+ CFileItemList items;
+ items.Add(item);
+ if (GetAdditionalSongDetails(parameterObject, items, musicdatabase) == OK)
+ filled = true;
+ }
+ }
+ }
+
+ if (item->GetLabel().empty())
+ {
+ item->SetLabel(CUtil::GetTitleFromPath(strFilename, false));
+ if (item->GetLabel().empty())
+ item->SetLabel(URIUtils::GetFileName(strFilename));
+ }
+
+ return filled;
+}
+
+bool CAudioLibrary::FillFileItemList(const CVariant &parameterObject, CFileItemList &list)
+{
+ CMusicDatabase musicdatabase;
+ if (!musicdatabase.Open())
+ return false;
+
+ std::string file = parameterObject["file"].asString();
+ int artistID = (int)parameterObject["artistid"].asInteger(-1);
+ int albumID = (int)parameterObject["albumid"].asInteger(-1);
+ int genreID = (int)parameterObject["genreid"].asInteger(-1);
+
+ bool success = false;
+ CFileItemPtr fileItem(new CFileItem());
+ if (FillFileItem(file, fileItem, parameterObject))
+ {
+ success = true;
+ list.Add(fileItem);
+ }
+
+ if (artistID != -1 || albumID != -1 || genreID != -1)
+ success |= musicdatabase.GetSongsNav("musicdb://songs/", list, genreID, artistID, albumID);
+
+ int songID = (int)parameterObject["songid"].asInteger(-1);
+ if (songID != -1)
+ {
+ CSong song;
+ if (musicdatabase.GetSong(songID, song))
+ {
+ list.Add(CFileItemPtr(new CFileItem(song)));
+ success = true;
+ }
+ }
+
+ if (success)
+ {
+ // If we retrieved the list of songs by "artistid"
+ // we sort by album (and implicitly by track number)
+ if (artistID != -1)
+ list.Sort(SortByAlbum, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ // If we retrieve the list of songs by "genreid"
+ // we sort by artist (and implicitly by album and track number)
+ else if (genreID != -1)
+ list.Sort(SortByArtist, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ // otherwise we sort by track number
+ else
+ list.Sort(SortByTrackNumber, SortOrderAscending);
+
+ }
+
+ return success;
+}
+
+void CAudioLibrary::FillItemArtistIDs(const std::vector<int>& artistids,
+ std::shared_ptr<CFileItem>& item)
+{
+ // Add artistIds as separate property as not part of CMusicInfoTag
+ CVariant artistidObj(CVariant::VariantTypeArray);
+ for (const auto& artistid : artistids)
+ artistidObj.push_back(artistid);
+
+ item->SetProperty("artistid", artistidObj);
+}
+
+void CAudioLibrary::FillAlbumItem(const CAlbum& album,
+ const std::string& path,
+ std::shared_ptr<CFileItem>& item)
+{
+ item = CFileItemPtr(new CFileItem(path, album));
+ // Add album artistIds as separate property as not part of CMusicInfoTag
+ std::vector<int> artistids = album.GetArtistIDArray();
+ FillItemArtistIDs(artistids, item);
+}
+
+JSONRPC_STATUS CAudioLibrary::GetAdditionalDetails(const CVariant &parameterObject, CFileItemList &items)
+{
+ if (items.IsEmpty())
+ return OK;
+
+ CMusicDatabase musicdb;
+ if (CMediaTypes::IsMediaType(items.GetContent(), MediaTypeArtist))
+ return GetAdditionalArtistDetails(parameterObject, items, musicdb);
+ else if (CMediaTypes::IsMediaType(items.GetContent(), MediaTypeAlbum))
+ return GetAdditionalAlbumDetails(parameterObject, items, musicdb);
+ else if (CMediaTypes::IsMediaType(items.GetContent(), MediaTypeSong))
+ return GetAdditionalSongDetails(parameterObject, items, musicdb);
+
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetAdditionalArtistDetails(const CVariant& parameterObject,
+ const CFileItemList& items,
+ CMusicDatabase& musicdatabase)
+{
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ std::set<std::string> checkProperties;
+ checkProperties.insert("roles");
+ checkProperties.insert("songgenres");
+ checkProperties.insert("isalbumartist");
+ checkProperties.insert("sourceid");
+ std::set<std::string> additionalProperties;
+ if (!CheckForAdditionalProperties(parameterObject["properties"], checkProperties, additionalProperties))
+ return OK;
+
+ if (additionalProperties.find("roles") != additionalProperties.end())
+ {
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ musicdatabase.GetRolesByArtist(item->GetMusicInfoTag()->GetDatabaseId(), item.get());
+ }
+ }
+ if (additionalProperties.find("songgenres") != additionalProperties.end())
+ {
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ musicdatabase.GetGenresByArtist(item->GetMusicInfoTag()->GetDatabaseId(), item.get());
+ }
+ }
+ if (additionalProperties.find("isalbumartist") != additionalProperties.end())
+ {
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ musicdatabase.GetIsAlbumArtist(item->GetMusicInfoTag()->GetDatabaseId(), item.get());
+ }
+ }
+ if (additionalProperties.find("sourceid") != additionalProperties.end())
+ {
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ musicdatabase.GetSourcesByArtist(item->GetMusicInfoTag()->GetDatabaseId(), item.get());
+ }
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetAdditionalAlbumDetails(const CVariant& parameterObject,
+ const CFileItemList& items,
+ CMusicDatabase& musicdatabase)
+{
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ std::set<std::string> checkProperties;
+ checkProperties.insert("songgenres");
+ checkProperties.insert("sourceid");
+ std::set<std::string> additionalProperties;
+ if (!CheckForAdditionalProperties(parameterObject["properties"], checkProperties, additionalProperties))
+ return OK;
+
+ if (additionalProperties.find("songgenres") != additionalProperties.end())
+ {
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ musicdatabase.GetGenresByAlbum(item->GetMusicInfoTag()->GetDatabaseId(), item.get());
+ }
+ }
+ if (additionalProperties.find("sourceid") != additionalProperties.end())
+ {
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ musicdatabase.GetSourcesByAlbum(item->GetMusicInfoTag()->GetDatabaseId(), item.get());
+ }
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CAudioLibrary::GetAdditionalSongDetails(const CVariant& parameterObject,
+ const CFileItemList& items,
+ CMusicDatabase& musicdatabase)
+{
+ if (!musicdatabase.Open())
+ return InternalError;
+
+ std::set<std::string> checkProperties;
+ checkProperties.insert("genreid");
+ checkProperties.insert("sourceid");
+ // Query (songview join songartistview) returns song.strAlbumArtists = CMusicInfoTag.m_strAlbumArtistDesc only
+ // Actual album artist data, if required, comes from album_artist and artist tables.
+ // It may differ from just splitting album artist description string
+ checkProperties.insert("albumartist");
+ checkProperties.insert("albumartistid");
+ checkProperties.insert("musicbrainzalbumartistid");
+ std::set<std::string> additionalProperties;
+ if (!CheckForAdditionalProperties(parameterObject["properties"], checkProperties, additionalProperties))
+ return OK;
+
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ if (additionalProperties.find("genreid") != additionalProperties.end())
+ {
+ std::vector<int> genreids;
+ if (musicdatabase.GetGenresBySong(item->GetMusicInfoTag()->GetDatabaseId(), genreids))
+ {
+ CVariant genreidObj(CVariant::VariantTypeArray);
+ for (const auto& genreid : genreids)
+ genreidObj.push_back(genreid);
+
+ item->SetProperty("genreid", genreidObj);
+ }
+ }
+ if (additionalProperties.find("sourceid") != additionalProperties.end())
+ {
+ musicdatabase.GetSourcesBySong(item->GetMusicInfoTag()->GetDatabaseId(), item->GetPath(), item.get());
+ }
+ if (item->GetMusicInfoTag()->GetAlbumId() > 0)
+ {
+ if (additionalProperties.find("albumartist") != additionalProperties.end() ||
+ additionalProperties.find("albumartistid") != additionalProperties.end() ||
+ additionalProperties.find("musicbrainzalbumartistid") != additionalProperties.end())
+ {
+ musicdatabase.GetArtistsByAlbum(item->GetMusicInfoTag()->GetAlbumId(), item.get());
+ }
+ }
+ }
+
+ return OK;
+}
+
+bool CAudioLibrary::CheckForAdditionalProperties(const CVariant &properties, const std::set<std::string> &checkProperties, std::set<std::string> &foundProperties)
+{
+ if (!properties.isArray() || properties.empty())
+ return false;
+
+ std::set<std::string> checkingProperties = checkProperties;
+ for (CVariant::const_iterator_array itr = properties.begin_array();
+ itr != properties.end_array() && !checkingProperties.empty(); ++itr)
+ {
+ if (!itr->isString())
+ continue;
+
+ std::string property = itr->asString();
+ if (checkingProperties.find(property) != checkingProperties.end())
+ {
+ checkingProperties.erase(property);
+ foundProperties.insert(property);
+ }
+ }
+
+ return !foundProperties.empty();
+}
diff --git a/xbmc/interfaces/json-rpc/AudioLibrary.h b/xbmc/interfaces/json-rpc/AudioLibrary.h
new file mode 100644
index 0000000..9946f9d
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/AudioLibrary.h
@@ -0,0 +1,82 @@
+/*
+ * 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 "FileItemHandler.h"
+#include "JSONRPC.h"
+
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+class CAlbum;
+class CFileitem;
+class CFileitemList;
+class CMusicDatabase;
+class CVariant;
+
+namespace JSONRPC
+{
+ class CAudioLibrary : public CFileItemHandler
+ {
+ public:
+ static JSONRPC_STATUS GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetArtists(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetArtistDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetAlbums(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetAlbumDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetSongs(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetSongDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetGenres(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetRoles(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetSources(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetAvailableArtTypes(const std::string& method, ITransportLayer* transport, IClient* client, const CVariant& parameterObject, CVariant& result);
+ static JSONRPC_STATUS GetAvailableArt(const std::string& method, ITransportLayer* transport, IClient* client, const CVariant& parameterObject, CVariant& result);
+
+ static JSONRPC_STATUS GetRecentlyAddedAlbums(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetRecentlyAddedSongs(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetRecentlyPlayedAlbums(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetRecentlyPlayedSongs(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS SetArtistDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetAlbumDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetSongDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS Scan(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Export(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Clean(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static bool FillFileItem(
+ const std::string& strFilename,
+ std::shared_ptr<CFileItem>& item,
+ const CVariant& parameterObject = CVariant(CVariant::VariantTypeArray));
+ static bool FillFileItemList(const CVariant &parameterObject, CFileItemList &list);
+
+ static JSONRPC_STATUS GetAdditionalDetails(const CVariant &parameterObject, CFileItemList &items);
+ static JSONRPC_STATUS GetAdditionalArtistDetails(const CVariant& parameterObject,
+ const CFileItemList& items,
+ CMusicDatabase& musicdatabase);
+ static JSONRPC_STATUS GetAdditionalAlbumDetails(const CVariant& parameterObject,
+ const CFileItemList& items,
+ CMusicDatabase& musicdatabase);
+ static JSONRPC_STATUS GetAdditionalSongDetails(const CVariant& parameterObject,
+ const CFileItemList& items,
+ CMusicDatabase& musicdatabase);
+
+ private:
+ static void FillAlbumItem(const CAlbum& album,
+ const std::string& path,
+ std::shared_ptr<CFileItem>& item);
+ static void FillItemArtistIDs(const std::vector<int>& artistids,
+ std::shared_ptr<CFileItem>& item);
+
+ static bool CheckForAdditionalProperties(const CVariant &properties, const std::set<std::string> &checkProperties, std::set<std::string> &foundProperties);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/CMakeLists.txt b/xbmc/interfaces/json-rpc/CMakeLists.txt
new file mode 100644
index 0000000..59ced89
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/CMakeLists.txt
@@ -0,0 +1,48 @@
+set(SOURCES AddonsOperations.cpp
+ ApplicationOperations.cpp
+ AudioLibrary.cpp
+ FavouritesOperations.cpp
+ FileItemHandler.cpp
+ FileOperations.cpp
+ GUIOperations.cpp
+ InputOperations.cpp
+ JSONRPC.cpp
+ JSONServiceDescription.cpp
+ JSONUtils.cpp
+ PlayerOperations.cpp
+ PlaylistOperations.cpp
+ ProfilesOperations.cpp
+ PVROperations.cpp
+ SettingsOperations.cpp
+ SystemOperations.cpp
+ TextureOperations.cpp
+ VideoLibrary.cpp
+ XBMCOperations.cpp)
+
+set(HEADERS AddonsOperations.h
+ ApplicationOperations.h
+ AudioLibrary.h
+ FavouritesOperations.h
+ FileItemHandler.h
+ FileOperations.h
+ GUIOperations.h
+ IClient.h
+ IJSONRPCAnnouncer.h
+ InputOperations.h
+ ITransportLayer.h
+ JSONRPC.h
+ JSONRPCUtils.h
+ JSONServiceDescription.h
+ JSONUtils.h
+ PlayerOperations.h
+ PlaylistOperations.h
+ ProfilesOperations.h
+ PVROperations.h
+ SettingsOperations.h
+ SystemOperations.h
+ TextureOperations.h
+ VideoLibrary.h
+ XBMCOperations.h)
+
+core_add_library(jsonrpc_interface)
+add_dependencies(${CORE_LIBRARY} generate_json_header)
diff --git a/xbmc/interfaces/json-rpc/FavouritesOperations.cpp b/xbmc/interfaces/json-rpc/FavouritesOperations.cpp
new file mode 100644
index 0000000..99277e5
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/FavouritesOperations.cpp
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2011-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 "FavouritesOperations.h"
+
+#include "ServiceBroker.h"
+#include "favourites/FavouritesService.h"
+#include "favourites/FavouritesURL.h"
+#include "guilib/WindowIDs.h"
+#include "input/WindowTranslator.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+#include <vector>
+
+using namespace JSONRPC;
+
+JSONRPC_STATUS CFavouritesOperations::GetFavourites(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CFileItemList favourites;
+ CServiceBroker::GetFavouritesService().GetAll(favourites);
+
+ std::string type = !parameterObject["type"].isNull() ? parameterObject["type"].asString() : "";
+
+ std::set<std::string> fields;
+ if (parameterObject.isMember("properties") && parameterObject["properties"].isArray())
+ {
+ for (CVariant::const_iterator_array field = parameterObject["properties"].begin_array();
+ field != parameterObject["properties"].end_array(); ++field)
+ fields.insert(field->asString());
+ }
+
+ for (int i = 0; i < favourites.Size(); i++)
+ {
+ CVariant object;
+ CFileItemPtr item = favourites.Get(i);
+
+ const CFavouritesURL url(item->GetPath());
+ if (!url.IsValid())
+ continue;
+
+ const CFavouritesURL::Action function = url.GetAction();
+
+ object["title"] = item->GetLabel();
+ if (fields.find("thumbnail") != fields.end())
+ object["thumbnail"] = item->GetArt("thumb");
+
+ if (function == CFavouritesURL::Action::ACTIVATE_WINDOW)
+ {
+ object["type"] = "window";
+ if (fields.find("window") != fields.end())
+ {
+ object["window"] = CWindowTranslator::TranslateWindow(url.GetWindowID());
+ }
+ if (fields.find("windowparameter") != fields.end())
+ {
+ object["windowparameter"] = url.GetTarget();
+ }
+ }
+ else if (function == CFavouritesURL::Action::PLAY_MEDIA)
+ {
+ object["type"] = "media";
+ if (fields.find("path") != fields.end())
+ object["path"] = url.GetTarget();
+ }
+ else if (function == CFavouritesURL::Action::RUN_SCRIPT)
+ {
+ object["type"] = "script";
+ if (fields.find("path") != fields.end())
+ object["path"] = url.GetTarget();
+ }
+ else if (function == CFavouritesURL::Action::START_ANDROID_ACTIVITY)
+ {
+ object["type"] = "androidapp";
+ if (fields.find("path") != fields.end())
+ object["path"] = url.GetTarget();
+ }
+ else
+ object["type"] = "unknown";
+
+ if (type.empty() || type.compare(object["type"].asString()) == 0)
+ result["favourites"].append(object);
+ }
+
+ int start, end;
+ HandleLimits(parameterObject, result, result["favourites"].size(), start, end);
+
+ return OK;
+}
+
+JSONRPC_STATUS CFavouritesOperations::AddFavourite(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string type = parameterObject["type"].asString();
+
+ if (type.compare("unknown") == 0)
+ return InvalidParams;
+
+ if ((type.compare("media") == 0 || type.compare("script") == 0 || type.compare("androidapp") == 0) && !ParameterNotNull(parameterObject, "path"))
+ {
+ result["method"] = "Favourites.AddFavourite";
+ result["stack"]["message"] = "Missing parameter";
+ result["stack"]["name"] = "path";
+ result["stack"]["type"] = "string";
+ return InvalidParams;
+ }
+
+ if (type.compare("window") == 0 && !ParameterNotNull(parameterObject, "window"))
+ {
+ result["method"] = "Favourites.AddFavourite";
+ result["stack"]["message"] = "Missing parameter";
+ result["stack"]["name"] = "window";
+ result["stack"]["type"] = "string";
+ return InvalidParams;
+ }
+
+ std::string title = parameterObject["title"].asString();
+ std::string path = parameterObject["path"].asString();
+
+ CFileItem item;
+ int contextWindow = 0;
+ if (type.compare("window") == 0)
+ {
+ item = CFileItem(parameterObject["windowparameter"].asString(), true);
+ contextWindow = CWindowTranslator::TranslateWindow(parameterObject["window"].asString());
+ if (contextWindow == WINDOW_INVALID)
+ return InvalidParams;
+ }
+ else if (type.compare("script") == 0)
+ {
+ if (!URIUtils::IsScript(path))
+ path = "script://" + path;
+ item = CFileItem(path, false);
+ }
+ else if (type.compare("androidapp") == 0)
+ {
+ if (!URIUtils::IsAndroidApp(path))
+ path = "androidapp://" + path;
+ item = CFileItem(path, false);
+ }
+ else if (type.compare("media") == 0)
+ {
+ item = CFileItem(path, false);
+ }
+ else
+ return InvalidParams;
+
+ item.SetLabel(title);
+ if (ParameterNotNull(parameterObject,"thumbnail"))
+ item.SetArt("thumb", parameterObject["thumbnail"].asString());
+
+ if (CServiceBroker::GetFavouritesService().AddOrRemove(item, contextWindow))
+ return ACK;
+ else
+ return FailedToExecute;
+}
diff --git a/xbmc/interfaces/json-rpc/FavouritesOperations.h b/xbmc/interfaces/json-rpc/FavouritesOperations.h
new file mode 100644
index 0000000..bb24b61
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/FavouritesOperations.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011-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 "JSONRPC.h"
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CFavouritesOperations : public CJSONUtils
+ {
+ public:
+ static JSONRPC_STATUS GetFavourites(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS AddFavourite(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/FileItemHandler.cpp b/xbmc/interfaces/json-rpc/FileItemHandler.cpp
new file mode 100644
index 0000000..9d649d3
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/FileItemHandler.cpp
@@ -0,0 +1,554 @@
+/*
+ * 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 "FileItemHandler.h"
+
+#include "AudioLibrary.h"
+#include "FileOperations.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "Util.h"
+#include "VideoLibrary.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h" // EPG_TAG_INVALID_UID
+#include "filesystem/Directory.h"
+#include "music/MusicThumbLoader.h"
+#include "music/tags/MusicInfoTag.h"
+#include "pictures/PictureInfoTag.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "utils/FileUtils.h"
+#include "utils/ISerializable.h"
+#include "utils/SortUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoInfoTag.h"
+#include "video/VideoThumbLoader.h"
+
+#include <map>
+#include <string.h>
+
+using namespace MUSIC_INFO;
+using namespace JSONRPC;
+using namespace XFILE;
+
+bool CFileItemHandler::GetField(const std::string& field,
+ const CVariant& info,
+ const std::shared_ptr<CFileItem>& item,
+ CVariant& result,
+ bool& fetchedArt,
+ CThumbLoader* thumbLoader /* = NULL */)
+{
+ if (result.isMember(field) && !result[field].empty())
+ return true;
+
+ // overwrite serialized values
+ if (item)
+ {
+ if (field == "mimetype" && item->GetMimeType().empty())
+ {
+ item->FillInMimeType(false);
+ result[field] = item->GetMimeType();
+ return true;
+ }
+
+ if (item->HasPVRChannelInfoTag())
+ {
+ // Translate PVR.Details.Broadcast -> List.Item.Base format
+ if (field == "cast")
+ {
+ // string -> Video.Cast
+ const std::vector<std::string> actors =
+ StringUtils::Split(info[field].asString(), EPG_STRING_TOKEN_SEPARATOR);
+
+ result[field] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& actor : actors)
+ {
+ CVariant actorVar;
+ actorVar["name"] = actor;
+ result[field].push_back(actorVar);
+ }
+ return true;
+ }
+ else if (field == "director" || field == "writer")
+ {
+ // string -> Array.String
+ result[field] = StringUtils::Split(info[field].asString(), EPG_STRING_TOKEN_SEPARATOR);
+ return true;
+ }
+ else if (field == "isrecording")
+ {
+ result[field] = CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(
+ *item->GetPVRChannelInfoTag());
+ return true;
+ }
+ }
+
+ if (item->HasEPGInfoTag())
+ {
+ if (field == "hastimer")
+ {
+ const std::shared_ptr<PVR::CPVRTimerInfoTag> timer =
+ CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(item->GetEPGInfoTag());
+ result[field] = (timer != nullptr);
+ return true;
+ }
+ else if (field == "hasreminder")
+ {
+ const std::shared_ptr<PVR::CPVRTimerInfoTag> timer =
+ CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(item->GetEPGInfoTag());
+ result[field] = (timer && timer->IsReminder());
+ return true;
+ }
+ else if (field == "hastimerrule")
+ {
+ const std::shared_ptr<PVR::CPVRTimerInfoTag> timer =
+ CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(item->GetEPGInfoTag());
+ result[field] = (timer && timer->HasParent());
+ return true;
+ }
+ else if (field == "hasrecording")
+ {
+ const std::shared_ptr<PVR::CPVRRecording> recording =
+ CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(
+ item->GetEPGInfoTag());
+ result[field] = (recording != nullptr);
+ return true;
+ }
+ else if (field == "recording")
+ {
+ const std::shared_ptr<PVR::CPVRRecording> recording =
+ CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(
+ item->GetEPGInfoTag());
+ result[field] = recording ? recording->m_strFileNameAndPath : "";
+ return true;
+ }
+ }
+ }
+
+ // check for serialized values
+ if (info.isMember(field) && !info[field].isNull())
+ {
+ result[field] = info[field];
+ return true;
+ }
+
+ // check if the field requires special handling
+ if (item)
+ {
+ if (item->IsAlbum())
+ {
+ if (field == "albumlabel")
+ {
+ result[field] = item->GetProperty("album_label");
+ return true;
+ }
+ if (item->HasProperty("album_" + field + "_array"))
+ {
+ result[field] = item->GetProperty("album_" + field + "_array");
+ return true;
+ }
+ if (item->HasProperty("album_" + field))
+ {
+ result[field] = item->GetProperty("album_" + field);
+ return true;
+ }
+ }
+
+ if (item->HasProperty("artist_" + field + "_array"))
+ {
+ result[field] = item->GetProperty("artist_" + field + "_array");
+ return true;
+ }
+ if (item->HasProperty("artist_" + field))
+ {
+ result[field] = item->GetProperty("artist_" + field);
+ return true;
+ }
+
+ if (field == "art")
+ {
+ if (thumbLoader && !item->GetProperty("libraryartfilled").asBoolean() && !fetchedArt &&
+ ((item->HasVideoInfoTag() && item->GetVideoInfoTag()->m_iDbId > -1) ||
+ (item->HasMusicInfoTag() && item->GetMusicInfoTag()->GetDatabaseId() > -1)))
+ {
+ thumbLoader->FillLibraryArt(*item);
+ fetchedArt = true;
+ }
+
+ CGUIListItem::ArtMap artMap = item->GetArt();
+ CVariant artObj(CVariant::VariantTypeObject);
+ for (const auto& artIt : artMap)
+ {
+ if (!artIt.second.empty())
+ artObj[artIt.first] = CTextureUtils::GetWrappedImageURL(artIt.second);
+ }
+
+ result["art"] = artObj;
+ return true;
+ }
+
+ if (field == "thumbnail")
+ {
+ if (thumbLoader != NULL && !item->HasArt("thumb") && !fetchedArt &&
+ ((item->HasVideoInfoTag() && item->GetVideoInfoTag()->m_iDbId > -1) || (item->HasMusicInfoTag() && item->GetMusicInfoTag()->GetDatabaseId() > -1)))
+ {
+ thumbLoader->FillLibraryArt(*item);
+ fetchedArt = true;
+ }
+ else if (item->HasPictureInfoTag() && !item->HasArt("thumb"))
+ item->SetArt("thumb", CTextureUtils::GetWrappedThumbURL(item->GetPath()));
+
+ if (item->HasArt("thumb"))
+ result["thumbnail"] = CTextureUtils::GetWrappedImageURL(item->GetArt("thumb"));
+ else
+ result["thumbnail"] = "";
+
+ return true;
+ }
+
+ if (field == "fanart")
+ {
+ if (thumbLoader != NULL && !item->HasArt("fanart") && !fetchedArt &&
+ ((item->HasVideoInfoTag() && item->GetVideoInfoTag()->m_iDbId > -1) || (item->HasMusicInfoTag() && item->GetMusicInfoTag()->GetDatabaseId() > -1)))
+ {
+ thumbLoader->FillLibraryArt(*item);
+ fetchedArt = true;
+ }
+
+ if (item->HasArt("fanart"))
+ result["fanart"] = CTextureUtils::GetWrappedImageURL(item->GetArt("fanart"));
+ else
+ result["fanart"] = "";
+
+ return true;
+ }
+
+ if (item->HasVideoInfoTag() && item->GetVideoContentType() == VideoDbContentType::TVSHOWS)
+ {
+ if (item->GetVideoInfoTag()->m_iSeason < 0 && field == "season")
+ {
+ result[field] = (int)item->GetProperty("totalseasons").asInteger();
+ return true;
+ }
+ if (field == "watchedepisodes")
+ {
+ result[field] = (int)item->GetProperty("watchedepisodes").asInteger();
+ return true;
+ }
+ }
+
+ if (item->HasProperty(field))
+ {
+ result[field] = item->GetProperty(field);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CFileItemHandler::FillDetails(const ISerializable* info,
+ const std::shared_ptr<CFileItem>& item,
+ std::set<std::string>& fields,
+ CVariant& result,
+ CThumbLoader* thumbLoader /* = NULL */)
+{
+ if (info == NULL || fields.empty())
+ return;
+
+ CVariant serialization;
+ info->Serialize(serialization);
+
+ bool fetchedArt = false;
+
+ std::set<std::string> originalFields = fields;
+
+ for (const auto& fieldIt : originalFields)
+ {
+ if (GetField(fieldIt, serialization, item, result, fetchedArt, thumbLoader) &&
+ result.isMember(fieldIt) && !result[fieldIt].empty())
+ fields.erase(fieldIt);
+ }
+}
+
+void CFileItemHandler::HandleFileItemList(const char *ID, bool allowFile, const char *resultname, CFileItemList &items, const CVariant &parameterObject, CVariant &result, bool sortLimit /* = true */)
+{
+ HandleFileItemList(ID, allowFile, resultname, items, parameterObject, result, items.Size(), sortLimit);
+}
+
+void CFileItemHandler::HandleFileItemList(const char *ID, bool allowFile, const char *resultname, CFileItemList &items, const CVariant &parameterObject, CVariant &result, int size, bool sortLimit /* = true */)
+{
+ int start, end;
+ HandleLimits(parameterObject, result, size, start, end);
+
+ if (sortLimit)
+ Sort(items, parameterObject);
+ else
+ {
+ start = 0;
+ end = items.Size();
+ }
+
+ CThumbLoader *thumbLoader = NULL;
+ if (end - start > 0)
+ {
+ if (items.Get(start)->HasVideoInfoTag())
+ thumbLoader = new CVideoThumbLoader();
+ else if (items.Get(start)->HasMusicInfoTag())
+ thumbLoader = new CMusicThumbLoader();
+
+ if (thumbLoader != NULL)
+ thumbLoader->OnLoaderStart();
+ }
+
+ std::set<std::string> fields;
+ if (parameterObject.isMember("properties") && parameterObject["properties"].isArray())
+ {
+ for (CVariant::const_iterator_array field = parameterObject["properties"].begin_array();
+ field != parameterObject["properties"].end_array(); ++field)
+ fields.insert(field->asString());
+ }
+
+ result[resultname].reserve(static_cast<size_t>(end - start));
+ for (int i = start; i < end; i++)
+ {
+ CFileItemPtr item = items.Get(i);
+ HandleFileItem(ID, allowFile, resultname, item, parameterObject, fields, result, true, thumbLoader);
+ }
+
+ delete thumbLoader;
+}
+
+void CFileItemHandler::HandleFileItem(const char* ID,
+ bool allowFile,
+ const char* resultname,
+ const std::shared_ptr<CFileItem>& item,
+ const CVariant& parameterObject,
+ const CVariant& validFields,
+ CVariant& result,
+ bool append /* = true */,
+ CThumbLoader* thumbLoader /* = NULL */)
+{
+ std::set<std::string> fields;
+ if (parameterObject.isMember("properties") && parameterObject["properties"].isArray())
+ {
+ for (CVariant::const_iterator_array field = parameterObject["properties"].begin_array();
+ field != parameterObject["properties"].end_array(); ++field)
+ fields.insert(field->asString());
+ }
+
+ HandleFileItem(ID, allowFile, resultname, item, parameterObject, fields, result, append, thumbLoader);
+}
+
+void CFileItemHandler::HandleFileItem(const char* ID,
+ bool allowFile,
+ const char* resultname,
+ const std::shared_ptr<CFileItem>& item,
+ const CVariant& parameterObject,
+ const std::set<std::string>& validFields,
+ CVariant& result,
+ bool append /* = true */,
+ CThumbLoader* thumbLoader /* = NULL */)
+{
+ CVariant object;
+ std::set<std::string> fields(validFields.begin(), validFields.end());
+
+ if (item.get())
+ {
+ std::set<std::string>::const_iterator fileField = fields.find("file");
+ if (fileField != fields.end())
+ {
+ if (allowFile)
+ {
+ if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->GetPath().empty())
+ object["file"] = item->GetVideoInfoTag()->GetPath().c_str();
+ if (item->HasMusicInfoTag() && !item->GetMusicInfoTag()->GetURL().empty())
+ object["file"] = item->GetMusicInfoTag()->GetURL().c_str();
+ if (item->HasPVRTimerInfoTag() && !item->GetPVRTimerInfoTag()->Path().empty())
+ object["file"] = item->GetPVRTimerInfoTag()->Path().c_str();
+
+ if (!object.isMember("file"))
+ object["file"] = item->GetDynPath().c_str();
+ }
+ fields.erase(fileField);
+ }
+
+ fileField = fields.find("mediapath");
+ if (fileField != fields.end())
+ {
+ object["mediapath"] = item->GetPath().c_str();
+ fields.erase(fileField);
+ }
+
+ fileField = fields.find("dynpath");
+ if (fileField != fields.end())
+ {
+ object["dynpath"] = item->GetDynPath().c_str();
+ fields.erase(fileField);
+ }
+
+ if (ID)
+ {
+ if (item->HasPVRChannelInfoTag() && item->GetPVRChannelInfoTag()->ChannelID() > 0)
+ object[ID] = item->GetPVRChannelInfoTag()->ChannelID();
+ else if (item->HasEPGInfoTag() && item->GetEPGInfoTag()->DatabaseID() > 0)
+ object[ID] = item->GetEPGInfoTag()->DatabaseID();
+ else if (item->HasPVRRecordingInfoTag() && item->GetPVRRecordingInfoTag()->RecordingID() > 0)
+ object[ID] = item->GetPVRRecordingInfoTag()->RecordingID();
+ else if (item->HasPVRTimerInfoTag() && item->GetPVRTimerInfoTag()->TimerID() > 0)
+ object[ID] = item->GetPVRTimerInfoTag()->TimerID();
+ else if (item->HasMusicInfoTag() && item->GetMusicInfoTag()->GetDatabaseId() > 0)
+ object[ID] = item->GetMusicInfoTag()->GetDatabaseId();
+ else if (item->HasVideoInfoTag() && item->GetVideoInfoTag()->m_iDbId > 0)
+ object[ID] = item->GetVideoInfoTag()->m_iDbId;
+
+ if (StringUtils::CompareNoCase(ID, "id") == 0)
+ {
+ if (item->HasPVRChannelInfoTag())
+ object["type"] = "channel";
+ else if (item->HasPVRRecordingInfoTag())
+ object["type"] = "recording";
+ else if (item->HasMusicInfoTag())
+ {
+ std::string type = item->GetMusicInfoTag()->GetType();
+ if (type == MediaTypeAlbum || type == MediaTypeSong || type == MediaTypeArtist)
+ object["type"] = type;
+ else if (!item->m_bIsFolder)
+ object["type"] = MediaTypeSong;
+ }
+ else if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_type.empty())
+ {
+ std::string type = item->GetVideoInfoTag()->m_type;
+ if (type == MediaTypeMovie || type == MediaTypeTvShow || type == MediaTypeEpisode || type == MediaTypeMusicVideo)
+ object["type"] = type;
+ }
+ else if (item->HasPictureInfoTag())
+ object["type"] = "picture";
+
+ if (!object.isMember("type"))
+ object["type"] = "unknown";
+
+ if (fields.find("filetype") != fields.end())
+ {
+ if (item->m_bIsFolder)
+ object["filetype"] = "directory";
+ else
+ object["filetype"] = "file";
+ }
+ }
+ }
+
+ bool deleteThumbloader = false;
+ if (thumbLoader == NULL)
+ {
+ if (item->HasVideoInfoTag())
+ thumbLoader = new CVideoThumbLoader();
+ else if (item->HasMusicInfoTag())
+ thumbLoader = new CMusicThumbLoader();
+
+ if (thumbLoader != NULL)
+ {
+ deleteThumbloader = true;
+ thumbLoader->OnLoaderStart();
+ }
+ }
+
+ if (item->HasPVRChannelInfoTag())
+ FillDetails(item->GetPVRChannelInfoTag().get(), item, fields, object, thumbLoader);
+ if (item->HasPVRChannelGroupMemberInfoTag())
+ FillDetails(item->GetPVRChannelGroupMemberInfoTag().get(), item, fields, object, thumbLoader);
+ if (item->HasEPGInfoTag())
+ FillDetails(item->GetEPGInfoTag().get(), item, fields, object, thumbLoader);
+ if (item->HasPVRRecordingInfoTag())
+ FillDetails(item->GetPVRRecordingInfoTag().get(), item, fields, object, thumbLoader);
+ if (item->HasPVRTimerInfoTag())
+ FillDetails(item->GetPVRTimerInfoTag().get(), item, fields, object, thumbLoader);
+ if (item->HasVideoInfoTag())
+ FillDetails(item->GetVideoInfoTag(), item, fields, object, thumbLoader);
+ if (item->HasMusicInfoTag())
+ FillDetails(item->GetMusicInfoTag(), item, fields, object, thumbLoader);
+ if (item->HasPictureInfoTag())
+ FillDetails(item->GetPictureInfoTag(), item, fields, object, thumbLoader);
+
+ FillDetails(item.get(), item, fields, object, thumbLoader);
+
+ if (deleteThumbloader)
+ delete thumbLoader;
+
+ object["label"] = item->GetLabel().c_str();
+ }
+ else
+ object = CVariant(CVariant::VariantTypeNull);
+
+ if (resultname)
+ {
+ if (append)
+ result[resultname].append(object);
+ else
+ result[resultname] = object;
+ }
+}
+
+bool CFileItemHandler::FillFileItemList(const CVariant &parameterObject, CFileItemList &list)
+{
+ CAudioLibrary::FillFileItemList(parameterObject, list);
+ CVideoLibrary::FillFileItemList(parameterObject, list);
+ CFileOperations::FillFileItemList(parameterObject, list);
+
+ std::string file = parameterObject["file"].asString();
+ if (!file.empty() &&
+ (URIUtils::IsURL(file) || (CFileUtils::Exists(file) && !CDirectory::Exists(file))))
+ {
+ bool added = false;
+ for (int index = 0; index < list.Size(); index++)
+ {
+ if (list[index]->GetDynPath() == file ||
+ list[index]->GetMusicInfoTag()->GetURL() == file || list[index]->GetVideoInfoTag()->GetPath() == file)
+ {
+ added = true;
+ break;
+ }
+ }
+
+ if (!added)
+ {
+ CFileItemPtr item = CFileItemPtr(new CFileItem(file, false));
+ if (item->IsPicture())
+ {
+ CPictureInfoTag picture;
+ picture.Load(item->GetPath());
+ *item->GetPictureInfoTag() = picture;
+ }
+ if (item->GetLabel().empty())
+ {
+ item->SetLabel(CUtil::GetTitleFromPath(file, false));
+ if (item->GetLabel().empty())
+ item->SetLabel(URIUtils::GetFileName(file));
+ }
+ list.Add(item);
+ }
+ }
+
+ return (list.Size() > 0);
+}
+
+void CFileItemHandler::Sort(CFileItemList &items, const CVariant &parameterObject)
+{
+ SortDescription sorting;
+ if (!ParseSorting(parameterObject, sorting.sortBy, sorting.sortOrder, sorting.sortAttributes))
+ return;
+
+ items.Sort(sorting);
+}
diff --git a/xbmc/interfaces/json-rpc/FileItemHandler.h b/xbmc/interfaces/json-rpc/FileItemHandler.h
new file mode 100644
index 0000000..ba7e636
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/FileItemHandler.h
@@ -0,0 +1,64 @@
+/*
+ * 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 "JSONRPC.h"
+#include "JSONUtils.h"
+
+#include <memory>
+#include <set>
+
+class CFileItem;
+class CFileItemList;
+class CThumbLoader;
+class CVariant;
+class ISerializable;
+
+namespace JSONRPC
+{
+ class CFileItemHandler : public CJSONUtils
+ {
+ protected:
+ static void FillDetails(const ISerializable* info,
+ const std::shared_ptr<CFileItem>& item,
+ std::set<std::string>& fields,
+ CVariant& result,
+ CThumbLoader* thumbLoader = nullptr);
+ static void HandleFileItemList(const char *ID, bool allowFile, const char *resultname, CFileItemList &items, const CVariant &parameterObject, CVariant &result, bool sortLimit = true);
+ static void HandleFileItemList(const char *ID, bool allowFile, const char *resultname, CFileItemList &items, const CVariant &parameterObject, CVariant &result, int size, bool sortLimit = true);
+ static void HandleFileItem(const char* ID,
+ bool allowFile,
+ const char* resultname,
+ const std::shared_ptr<CFileItem>& item,
+ const CVariant& parameterObject,
+ const CVariant& validFields,
+ CVariant& result,
+ bool append = true,
+ CThumbLoader* thumbLoader = nullptr);
+ static void HandleFileItem(const char* ID,
+ bool allowFile,
+ const char* resultname,
+ const std::shared_ptr<CFileItem>& item,
+ const CVariant& parameterObject,
+ const std::set<std::string>& validFields,
+ CVariant& result,
+ bool append = true,
+ CThumbLoader* thumbLoader = nullptr);
+
+ static bool FillFileItemList(const CVariant &parameterObject, CFileItemList &list);
+ private:
+ static void Sort(CFileItemList &items, const CVariant& parameterObject);
+ static bool GetField(const std::string& field,
+ const CVariant& info,
+ const std::shared_ptr<CFileItem>& item,
+ CVariant& result,
+ bool& fetchedArt,
+ CThumbLoader* thumbLoader = nullptr);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/FileOperations.cpp b/xbmc/interfaces/json-rpc/FileOperations.cpp
new file mode 100644
index 0000000..6329a78
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/FileOperations.cpp
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2005-2020 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 "FileOperations.h"
+
+#include "AudioLibrary.h"
+#include "FileItem.h"
+#include "MediaSource.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "VideoLibrary.h"
+#include "filesystem/Directory.h"
+#include "media/MediaLockState.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "video/VideoDatabase.h"
+
+using namespace XFILE;
+using namespace JSONRPC;
+
+JSONRPC_STATUS CFileOperations::GetRootDirectory(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string media = parameterObject["media"].asString();
+ StringUtils::ToLower(media);
+
+ VECSOURCES *sources = CMediaSourceSettings::GetInstance().GetSources(media);
+ if (sources)
+ {
+ CFileItemList items;
+ for (unsigned int i = 0; i < (unsigned int)sources->size(); i++)
+ {
+ // Do not show sources which are locked
+ if (sources->at(i).m_iHasLock == LOCK_STATE_LOCKED)
+ continue;
+
+ items.Add(CFileItemPtr(new CFileItem(sources->at(i))));
+ }
+
+ for (unsigned int i = 0; i < (unsigned int)items.Size(); i++)
+ {
+ if (items[i]->IsSmb())
+ {
+ CURL url(items[i]->GetPath());
+ items[i]->SetPath(url.GetWithoutUserDetails());
+ }
+ }
+
+ CVariant param = parameterObject;
+ param["properties"] = CVariant(CVariant::VariantTypeArray);
+ param["properties"].append("file");
+
+ HandleFileItemList(NULL, true, "sources", items, param, result);
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CFileOperations::GetDirectory(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string media = parameterObject["media"].asString();
+ StringUtils::ToLower(media);
+
+ CFileItemList items;
+ std::string strPath = parameterObject["directory"].asString();
+
+ if (!CFileUtils::RemoteAccessAllowed(strPath))
+ return InvalidParams;
+
+ std::vector<std::string> regexps;
+ std::string extensions;
+ if (media == "video")
+ {
+ regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoExcludeFromListingRegExps;
+ extensions = CServiceBroker::GetFileExtensionProvider().GetVideoExtensions();
+ items.SetProperty("set_videodb_details",
+ CVideoLibrary::GetDetailsFromJsonParameters(parameterObject));
+ }
+ else if (media == "music")
+ {
+ regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromListingRegExps;
+ extensions = CServiceBroker::GetFileExtensionProvider().GetMusicExtensions();
+ }
+ else if (media == "pictures")
+ {
+ regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_pictureExcludeFromListingRegExps;
+ extensions = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ }
+
+ if (CDirectory::GetDirectory(strPath, items, extensions, DIR_FLAG_DEFAULTS))
+ {
+ // we might need to get additional information for music items
+ if (media == "music")
+ {
+ JSONRPC_STATUS status = CAudioLibrary::GetAdditionalDetails(parameterObject, items);
+ if (status != OK)
+ return status;
+ }
+
+ CFileItemList filteredFiles;
+ for (unsigned int i = 0; i < (unsigned int)items.Size(); i++)
+ {
+ if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps))
+ continue;
+
+ if (items[i]->IsSmb())
+ {
+ CURL url(items[i]->GetPath());
+ items[i]->SetPath(url.GetWithoutUserDetails());
+ }
+
+ if ((media == "video" && items[i]->HasVideoInfoTag()) ||
+ (media == "music" && items[i]->HasMusicInfoTag()) ||
+ (media == "picture" && items[i]->HasPictureInfoTag()) ||
+ media == "files" ||
+ URIUtils::IsUPnP(items.GetPath()))
+ filteredFiles.Add(items[i]);
+ else
+ {
+ CFileItemPtr fileItem(new CFileItem());
+ if (FillFileItem(items[i], fileItem, media, parameterObject))
+ filteredFiles.Add(fileItem);
+ else
+ filteredFiles.Add(items[i]);
+ }
+ }
+
+ // Check if the "properties" list exists
+ // and make sure it contains the "file" and "filetype"
+ // fields
+ CVariant param = parameterObject;
+ if (!param.isMember("properties"))
+ param["properties"] = CVariant(CVariant::VariantTypeArray);
+
+ bool hasFileField = false;
+ for (CVariant::const_iterator_array itr = param["properties"].begin_array();
+ itr != param["properties"].end_array(); ++itr)
+ {
+ if (itr->asString().compare("file") == 0)
+ {
+ hasFileField = true;
+ break;
+ }
+ }
+
+ if (!hasFileField)
+ param["properties"].append("file");
+ param["properties"].append("filetype");
+
+ HandleFileItemList("id", true, "files", filteredFiles, param, result);
+
+ return OK;
+ }
+
+ return InvalidParams;
+}
+
+JSONRPC_STATUS CFileOperations::GetFileDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string file = parameterObject["file"].asString();
+ if (!CFileUtils::Exists(file))
+ return InvalidParams;
+
+ if (!CFileUtils::RemoteAccessAllowed(file))
+ return InvalidParams;
+
+ std::string path = URIUtils::GetDirectory(file);
+
+ CFileItemList items;
+ if (path.empty())
+ return InvalidParams;
+
+ CFileItemPtr item;
+ if (CDirectory::GetDirectory(path, items, "", DIR_FLAG_DEFAULTS) && items.Contains(file))
+ item = items.Get(file);
+ else
+ item = CFileItemPtr(new CFileItem(file, false));
+
+ if (!URIUtils::IsUPnP(file))
+ FillFileItem(item, item, parameterObject["media"].asString(), parameterObject);
+
+ // Check if the "properties" list exists
+ // and make sure it contains the "file"
+ // field
+ CVariant param = parameterObject;
+ if (!param.isMember("properties"))
+ param["properties"] = CVariant(CVariant::VariantTypeArray);
+
+ bool hasFileField = false;
+ for (CVariant::const_iterator_array itr = param["properties"].begin_array();
+ itr != param["properties"].end_array(); ++itr)
+ {
+ if (itr->asString().compare("file") == 0)
+ {
+ hasFileField = true;
+ break;
+ }
+ }
+
+ if (!hasFileField)
+ param["properties"].append("file");
+ param["properties"].append("filetype");
+
+ HandleFileItem("id", true, "filedetails", item, parameterObject, param["properties"], result, false);
+ return OK;
+}
+
+JSONRPC_STATUS CFileOperations::SetFileDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string media = parameterObject["media"].asString();
+ StringUtils::ToLower(media);
+
+ if (media.compare("video") != 0)
+ return InvalidParams;
+
+ std::string file = parameterObject["file"].asString();
+ if (!CFileUtils::Exists(file))
+ return InvalidParams;
+
+ if (!CFileUtils::RemoteAccessAllowed(file))
+ return InvalidParams;
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ int fileId = videodatabase.AddFile(file);
+
+ CVideoInfoTag infos;
+ if (!videodatabase.GetFileInfo("", infos, fileId))
+ return InvalidParams;
+
+ CDateTime lastPlayed = infos.m_lastPlayed;
+ int playcount = infos.GetPlayCount();
+ if (!parameterObject["lastplayed"].isNull())
+ {
+ lastPlayed.Reset();
+ SetFromDBDateTime(parameterObject["lastplayed"], lastPlayed);
+ playcount = lastPlayed.IsValid() ? std::max(1, playcount) : 0;
+ }
+ if (!parameterObject["playcount"].isNull())
+ playcount = parameterObject["playcount"].asInteger();
+ if (playcount != infos.GetPlayCount() || lastPlayed != infos.m_lastPlayed)
+ videodatabase.SetPlayCount(CFileItem(infos), playcount, lastPlayed);
+
+ CVideoLibrary::UpdateResumePoint(parameterObject, infos, videodatabase);
+
+ videodatabase.GetFileInfo("", infos, fileId);
+ CJSONRPCUtils::NotifyItemUpdated(infos, std::map<std::string, std::string>{});
+ return ACK;
+}
+
+JSONRPC_STATUS CFileOperations::PrepareDownload(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string protocol;
+ if (transport->PrepareDownload(parameterObject["path"].asString().c_str(), result["details"], protocol))
+ {
+ result["protocol"] = protocol;
+
+ if ((transport->GetCapabilities() & FileDownloadDirect) == FileDownloadDirect)
+ result["mode"] = "direct";
+ else
+ result["mode"] = "redirect";
+
+ return OK;
+ }
+
+ return InvalidParams;
+}
+
+JSONRPC_STATUS CFileOperations::Download(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return transport->Download(parameterObject["path"].asString().c_str(), result) ? OK : InvalidParams;
+}
+
+bool CFileOperations::FillFileItem(
+ const std::shared_ptr<CFileItem>& originalItem,
+ std::shared_ptr<CFileItem>& item,
+ const std::string& media /* = "" */,
+ const CVariant& parameterObject /* = CVariant(CVariant::VariantTypeArray) */)
+{
+ if (originalItem.get() == NULL)
+ return false;
+
+ // copy all the available details
+ *item = *originalItem;
+
+ bool status = false;
+ std::string strFilename = originalItem->GetPath();
+ if (!strFilename.empty() && (CDirectory::Exists(strFilename) || CFileUtils::Exists(strFilename)))
+ {
+ if (media == "video")
+ status = CVideoLibrary::FillFileItem(strFilename, item, parameterObject);
+ else if (media == "music")
+ status = CAudioLibrary::FillFileItem(strFilename, item, parameterObject);
+
+ if (status && item->GetLabel().empty())
+ {
+ std::string label = originalItem->GetLabel();
+ if (label.empty())
+ {
+ bool isDir = CDirectory::Exists(strFilename);
+ label = CUtil::GetTitleFromPath(strFilename, isDir);
+ if (label.empty())
+ label = URIUtils::GetFileName(strFilename);
+ }
+
+ item->SetLabel(label);
+ }
+ else if (!status)
+ {
+ if (originalItem->GetLabel().empty())
+ {
+ bool isDir = CDirectory::Exists(strFilename);
+ std::string label = CUtil::GetTitleFromPath(strFilename, isDir);
+ if (label.empty())
+ return false;
+
+ item->SetLabel(label);
+ item->SetPath(strFilename);
+ item->m_bIsFolder = isDir;
+ }
+ else
+ *item = *originalItem;
+
+ status = true;
+ }
+ }
+
+ return status;
+}
+
+bool CFileOperations::FillFileItemList(const CVariant &parameterObject, CFileItemList &list)
+{
+ if (parameterObject.isMember("directory"))
+ {
+ std::string media = parameterObject["media"].asString();
+ StringUtils::ToLower(media);
+
+ std::string strPath = parameterObject["directory"].asString();
+ if (!strPath.empty())
+ {
+ CFileItemList items;
+ std::string extensions;
+ std::vector<std::string> regexps;
+
+ if (media == "video")
+ {
+ regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoExcludeFromListingRegExps;
+ extensions = CServiceBroker::GetFileExtensionProvider().GetVideoExtensions();
+ }
+ else if (media == "music")
+ {
+ regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromListingRegExps;
+ extensions = CServiceBroker::GetFileExtensionProvider().GetMusicExtensions();
+ }
+ else if (media == "pictures")
+ {
+ regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_pictureExcludeFromListingRegExps;
+ extensions = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ }
+
+ CDirectory directory;
+ if (directory.GetDirectory(strPath, items, extensions, DIR_FLAG_DEFAULTS))
+ {
+ // Sort folders and files by filename to avoid reverse item order bug on some platforms,
+ // but leave items from a playlist, smartplaylist or upnp container in order supplied
+ if (!items.IsPlayList() && !items.IsSmartPlayList() && !URIUtils::IsUPnP(items.GetPath()))
+ items.Sort(SortByFile, SortOrderAscending);
+
+ CFileItemList filteredDirectories;
+ for (unsigned int i = 0; i < (unsigned int)items.Size(); i++)
+ {
+ if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps))
+ continue;
+
+ if (items[i]->m_bIsFolder)
+ filteredDirectories.Add(items[i]);
+ else if ((media == "video" && items[i]->HasVideoInfoTag()) ||
+ (media == "music" && items[i]->HasMusicInfoTag()))
+ list.Add(items[i]);
+ else
+ {
+ CFileItemPtr fileItem(new CFileItem());
+ if (FillFileItem(items[i], fileItem, media, parameterObject))
+ list.Add(fileItem);
+ else if (media == "files")
+ list.Add(items[i]);
+ }
+ }
+
+ if (parameterObject.isMember("recursive") && parameterObject["recursive"].isBoolean())
+ {
+ for (int i = 0; i < filteredDirectories.Size(); i++)
+ {
+ CVariant val = parameterObject;
+ val["directory"] = filteredDirectories[i]->GetPath();
+ FillFileItemList(val, list);
+ }
+ }
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/interfaces/json-rpc/FileOperations.h b/xbmc/interfaces/json-rpc/FileOperations.h
new file mode 100644
index 0000000..6af05bb
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/FileOperations.h
@@ -0,0 +1,39 @@
+/*
+ * 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 "FileItemHandler.h"
+#include "JSONRPC.h"
+
+#include <memory>
+
+class CFileItem;
+class CVariant;
+
+namespace JSONRPC
+{
+ class CFileOperations : public CFileItemHandler
+ {
+ public:
+ static JSONRPC_STATUS GetRootDirectory(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetDirectory(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetFileDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetFileDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS PrepareDownload(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Download(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static bool FillFileItem(
+ const std::shared_ptr<CFileItem>& originalItem,
+ std::shared_ptr<CFileItem>& item,
+ const std::string& media = "",
+ const CVariant& parameterObject = CVariant(CVariant::VariantTypeArray));
+ static bool FillFileItemList(const CVariant &parameterObject, CFileItemList &list);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/GUIOperations.cpp b/xbmc/interfaces/json-rpc/GUIOperations.cpp
new file mode 100644
index 0000000..06b1f75
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/GUIOperations.cpp
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2011-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 "GUIOperations.h"
+
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonType.h"
+#include "application/Application.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/StereoscopicsManager.h"
+#include "input/Key.h"
+#include "input/WindowTranslator.h"
+#include "messaging/ApplicationMessenger.h"
+#include "rendering/RenderSystem.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Variant.h"
+
+using namespace JSONRPC;
+using namespace ADDON;
+
+JSONRPC_STATUS CGUIOperations::GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVariant properties = CVariant(CVariant::VariantTypeObject);
+ for (unsigned int index = 0; index < parameterObject["properties"].size(); index++)
+ {
+ std::string propertyName = parameterObject["properties"][index].asString();
+ CVariant property;
+ JSONRPC_STATUS ret;
+ if ((ret = GetPropertyValue(propertyName, property)) != OK)
+ return ret;
+
+ properties[propertyName] = property;
+ }
+
+ result = properties;
+
+ return OK;
+}
+
+JSONRPC_STATUS CGUIOperations::ActivateWindow(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int iWindow = CWindowTranslator::TranslateWindow(parameterObject["window"].asString());
+ if (iWindow != WINDOW_INVALID)
+ {
+ std::vector<std::string> params;
+ for (CVariant::const_iterator_array param = parameterObject["parameters"].begin_array();
+ param != parameterObject["parameters"].end_array(); ++param)
+ {
+ if (param->isString() && !param->empty())
+ params.push_back(param->asString());
+ }
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTIVATE_WINDOW, iWindow, 0, nullptr, "",
+ params);
+ return ACK;
+ }
+
+ return InvalidParams;
+}
+
+JSONRPC_STATUS CGUIOperations::ShowNotification(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string image = parameterObject["image"].asString();
+ std::string title = parameterObject["title"].asString();
+ std::string message = parameterObject["message"].asString();
+ unsigned int displaytime = (unsigned int)parameterObject["displaytime"].asUnsignedInteger();
+
+ if (image.compare("info") == 0)
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, title, message, displaytime);
+ else if (image.compare("warning") == 0)
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, title, message, displaytime);
+ else if (image.compare("error") == 0)
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, title, message, displaytime);
+ else
+ CGUIDialogKaiToast::QueueNotification(image, title, message, displaytime);
+
+ return ACK;
+}
+
+JSONRPC_STATUS CGUIOperations::SetFullscreen(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if ((parameterObject["fullscreen"].isString() &&
+ parameterObject["fullscreen"].asString().compare("toggle") == 0) ||
+ (parameterObject["fullscreen"].isBoolean() &&
+ parameterObject["fullscreen"].asBoolean() != g_application.IsFullScreen()))
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(ACTION_SHOW_GUI)));
+ }
+ else if (!parameterObject["fullscreen"].isBoolean() && !parameterObject["fullscreen"].isString())
+ return InvalidParams;
+
+ return GetPropertyValue("fullscreen", result);
+}
+
+JSONRPC_STATUS CGUIOperations::SetStereoscopicMode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CAction action = CStereoscopicsManager::ConvertActionCommandToAction("SetStereoMode", parameterObject["mode"].asString());
+ if (action.GetID() != ACTION_NONE)
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(action)));
+ return ACK;
+ }
+
+ return InvalidParams;
+}
+
+JSONRPC_STATUS CGUIOperations::GetStereoscopicModes(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ for (int i = RENDER_STEREO_MODE_OFF; i < RENDER_STEREO_MODE_COUNT; i++)
+ {
+ RENDER_STEREO_MODE mode = (RENDER_STEREO_MODE) i;
+ if (CServiceBroker::GetRenderSystem()->SupportsStereo(mode))
+ result["stereoscopicmodes"].push_back(GetStereoModeObjectFromGuiMode(mode));
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CGUIOperations::GetPropertyValue(const std::string &property, CVariant &result)
+{
+ if (property == "currentwindow")
+ {
+ result["label"] = CServiceBroker::GetGUI()->GetInfoManager().GetLabel(
+ CServiceBroker::GetGUI()->GetInfoManager().TranslateString("System.CurrentWindow"),
+ INFO::DEFAULT_CONTEXT);
+ result["id"] = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog();
+ }
+ else if (property == "currentcontrol")
+ result["label"] = CServiceBroker::GetGUI()->GetInfoManager().GetLabel(
+ CServiceBroker::GetGUI()->GetInfoManager().TranslateString("System.CurrentControl"),
+ INFO::DEFAULT_CONTEXT);
+ else if (property == "skin")
+ {
+ std::string skinId = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN);
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(skinId, addon, AddonType::SKIN,
+ OnlyEnabled::CHOICE_YES))
+ return InternalError;
+
+ result["id"] = skinId;
+ if (addon.get())
+ result["name"] = addon->Name();
+ }
+ else if (property == "fullscreen")
+ result = g_application.IsFullScreen();
+ else if (property == "stereoscopicmode")
+ {
+ const CStereoscopicsManager &stereoscopicsManager = CServiceBroker::GetGUI()->GetStereoscopicsManager();
+
+ result = GetStereoModeObjectFromGuiMode(stereoscopicsManager.GetStereoMode());
+ }
+ else
+ return InvalidParams;
+
+ return OK;
+}
+
+CVariant CGUIOperations::GetStereoModeObjectFromGuiMode(const RENDER_STEREO_MODE &mode)
+{
+ const CStereoscopicsManager &stereoscopicsManager = CServiceBroker::GetGUI()->GetStereoscopicsManager();
+
+ CVariant modeObj(CVariant::VariantTypeObject);
+ modeObj["mode"] = stereoscopicsManager.ConvertGuiStereoModeToString(mode);
+ modeObj["label"] = stereoscopicsManager.GetLabelForStereoMode(mode);
+ return modeObj;
+}
diff --git a/xbmc/interfaces/json-rpc/GUIOperations.h b/xbmc/interfaces/json-rpc/GUIOperations.h
new file mode 100644
index 0000000..04c3f81
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/GUIOperations.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "JSONRPC.h"
+#include "rendering/RenderSystemTypes.h"
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CGUIOperations
+ {
+ public:
+ static JSONRPC_STATUS GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS ActivateWindow(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS ShowNotification(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetFullscreen(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetStereoscopicMode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetStereoscopicModes(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ private:
+ static JSONRPC_STATUS GetPropertyValue(const std::string &property, CVariant &result);
+ static CVariant GetStereoModeObjectFromGuiMode(const RENDER_STEREO_MODE &mode);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/IClient.h b/xbmc/interfaces/json-rpc/IClient.h
new file mode 100644
index 0000000..18a14df
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/IClient.h
@@ -0,0 +1,21 @@
+/*
+ * 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
+
+namespace JSONRPC
+{
+ class IClient
+ {
+ public:
+ virtual ~IClient() = default;
+ virtual int GetPermissionFlags() = 0;
+ virtual int GetAnnouncementFlags() = 0;
+ virtual bool SetAnnouncementFlags(int flags) = 0;
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/IJSONRPCAnnouncer.h b/xbmc/interfaces/json-rpc/IJSONRPCAnnouncer.h
new file mode 100644
index 0000000..af7d2e8
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/IJSONRPCAnnouncer.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "interfaces/IAnnouncer.h"
+#include "utils/JSONVariantWriter.h"
+#include "utils/Variant.h"
+
+namespace JSONRPC
+{
+ class IJSONRPCAnnouncer : public ANNOUNCEMENT::IAnnouncer
+ {
+ public:
+ ~IJSONRPCAnnouncer() override = default;
+
+ protected:
+ static std::string AnnouncementToJSONRPC(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& method,
+ const CVariant& data,
+ bool compactOutput)
+ {
+ CVariant root;
+ root["jsonrpc"] = "2.0";
+
+ std::string namespaceMethod = ANNOUNCEMENT::AnnouncementFlagToString(flag);
+ namespaceMethod += ".";
+ namespaceMethod += method;
+ root["method"] = namespaceMethod;
+
+ root["params"]["data"] = data;
+ root["params"]["sender"] = sender;
+
+ std::string str;
+ CJSONVariantWriter::Write(root, str, compactOutput);
+
+ return str;
+ }
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/ITransportLayer.h b/xbmc/interfaces/json-rpc/ITransportLayer.h
new file mode 100644
index 0000000..11a0c78
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/ITransportLayer.h
@@ -0,0 +1,35 @@
+/*
+ * 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 <string>
+
+class CVariant;
+
+namespace JSONRPC
+{
+ enum TransportLayerCapability
+ {
+ Response = 0x1,
+ Announcing = 0x2,
+ FileDownloadRedirect = 0x4,
+ FileDownloadDirect = 0x8
+ };
+
+ #define TRANSPORT_LAYER_CAPABILITY_ALL (Response | Announcing | FileDownloadRedirect | FileDownloadDirect)
+
+ class ITransportLayer
+ {
+ public:
+ virtual ~ITransportLayer() = default;
+ virtual bool PrepareDownload(const char *path, CVariant &details, std::string &protocol) = 0;
+ virtual bool Download(const char *path, CVariant &result) = 0;
+ virtual int GetCapabilities() = 0;
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/InputOperations.cpp b/xbmc/interfaces/json-rpc/InputOperations.cpp
new file mode 100644
index 0000000..0b637fb
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/InputOperations.cpp
@@ -0,0 +1,180 @@
+/*
+ * 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 "InputOperations.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "guilib/GUIAudioManager.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindow.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/ButtonTranslator.h"
+#include "input/actions/ActionIDs.h"
+#include "input/actions/ActionTranslator.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/Variant.h"
+
+using namespace JSONRPC;
+
+//! @todo the breakage of the screensaver should be refactored
+//! to one central super duper place for getting rid of
+//! 1 million dupes
+bool CInputOperations::handleScreenSaver()
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+ return appPower->WakeUpScreenSaverAndDPMS();
+}
+
+JSONRPC_STATUS CInputOperations::SendAction(int actionID, bool wakeScreensaver /* = true */, bool waitResult /* = false */)
+{
+ if (!wakeScreensaver || !handleScreenSaver())
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetSystemIdleTimer();
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetAudioManager().PlayActionSound(actionID);
+
+ if (waitResult)
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(actionID)));
+ else
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(actionID)));
+ }
+ return ACK;
+}
+
+JSONRPC_STATUS CInputOperations::activateWindow(int windowID)
+{
+ if(!handleScreenSaver())
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTIVATE_WINDOW, windowID, 0);
+
+ return ACK;
+}
+
+JSONRPC_STATUS CInputOperations::SendText(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (CGUIKeyboardFactory::SendTextToActiveKeyboard(parameterObject["text"].asString(), parameterObject["done"].asBoolean()))
+ return ACK;
+
+ CGUIWindow *window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog());
+ if (!window)
+ return ACK;
+
+ CGUIMessage msg(GUI_MSG_SET_TEXT, 0, window->GetFocusedControlID());
+ msg.SetLabel(parameterObject["text"].asString());
+ msg.SetParam1(parameterObject["done"].asBoolean() ? 1 : 0);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, window->GetID());
+
+ return ACK;
+}
+
+JSONRPC_STATUS CInputOperations::ExecuteAction(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ unsigned int action;
+ if (!CActionTranslator::TranslateString(parameterObject["action"].asString(), action))
+ return InvalidParams;
+
+ return SendAction(action);
+}
+
+JSONRPC_STATUS CInputOperations::ButtonEvent(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result)
+{
+ std::string button = parameterObject["button"].asString();
+ std::string keymap = parameterObject["keymap"].asString();
+ int holdtime = static_cast<int>(parameterObject["holdtime"].asInteger());
+ if (holdtime < 0)
+ {
+ return InvalidParams;
+ }
+
+ uint32_t keycode = CButtonTranslator::TranslateString(keymap, button);
+ if (keycode == 0)
+ {
+ return InvalidParams;
+ }
+
+ XBMC_Event* newEvent = new XBMC_Event;
+ newEvent->type = XBMC_BUTTON;
+ newEvent->keybutton.button = keycode;
+ newEvent->keybutton.holdtime = holdtime;
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EVENT, -1, -1, static_cast<void*>(newEvent));
+
+ return ACK;
+}
+
+JSONRPC_STATUS CInputOperations::Left(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return SendAction(ACTION_MOVE_LEFT);
+}
+
+JSONRPC_STATUS CInputOperations::Right(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return SendAction(ACTION_MOVE_RIGHT);
+}
+
+JSONRPC_STATUS CInputOperations::Down(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return SendAction(ACTION_MOVE_DOWN);
+}
+
+JSONRPC_STATUS CInputOperations::Up(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return SendAction(ACTION_MOVE_UP);
+}
+
+JSONRPC_STATUS CInputOperations::Select(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return SendAction(ACTION_SELECT_ITEM);
+}
+
+JSONRPC_STATUS CInputOperations::Back(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return SendAction(ACTION_NAV_BACK);
+}
+
+JSONRPC_STATUS CInputOperations::ContextMenu(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return SendAction(ACTION_CONTEXT_MENU);
+}
+
+JSONRPC_STATUS CInputOperations::Info(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return SendAction(ACTION_SHOW_INFO);
+}
+
+JSONRPC_STATUS CInputOperations::Home(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return activateWindow(WINDOW_HOME);
+}
+
+JSONRPC_STATUS CInputOperations::ShowCodec(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return MethodNotFound;
+}
+
+JSONRPC_STATUS CInputOperations::ShowOSD(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return SendAction(ACTION_SHOW_OSD);
+}
+
+JSONRPC_STATUS CInputOperations::ShowPlayerProcessInfo(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return SendAction(ACTION_PLAYER_PROCESS_INFO);
+}
diff --git a/xbmc/interfaces/json-rpc/InputOperations.h b/xbmc/interfaces/json-rpc/InputOperations.h
new file mode 100644
index 0000000..a431a1c
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/InputOperations.h
@@ -0,0 +1,50 @@
+/*
+ * 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 "JSONRPC.h"
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CInputOperations
+ {
+ public:
+ static JSONRPC_STATUS SendText(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS ExecuteAction(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS ButtonEvent(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result);
+
+ static JSONRPC_STATUS Left(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Right(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Down(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Up(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS Select(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Back(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS ContextMenu(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Info(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Home(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS ShowCodec(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS ShowOSD(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS ShowPlayerProcessInfo(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS SendAction(int actionID, bool wakeScreensaver = true, bool waitResult = false);
+
+ private:
+ static JSONRPC_STATUS activateWindow(int windowID);
+ static bool handleScreenSaver();
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/JSONRPC.cpp b/xbmc/interfaces/json-rpc/JSONRPC.cpp
new file mode 100644
index 0000000..4b86188
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/JSONRPC.cpp
@@ -0,0 +1,392 @@
+/*
+ * 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 "JSONRPC.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "ServiceDescription.h"
+#include "TextureDatabase.h"
+#include "addons/Addon.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dbwrappers/DatabaseQuery.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/WindowTranslator.h"
+#include "input/actions/ActionTranslator.h"
+#include "interfaces/AnnouncementManager.h"
+#include "playlists/SmartPlayList.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <string.h>
+
+using namespace JSONRPC;
+
+bool CJSONRPC::m_initialized = false;
+
+void CJSONRPC::Initialize()
+{
+ if (m_initialized)
+ return;
+
+ // Add some types/enums at runtime
+ std::vector<std::string> enumList;
+ for (int addonType = static_cast<int>(ADDON::AddonType::UNKNOWN);
+ addonType < static_cast<int>(ADDON::AddonType::MAX_TYPES); addonType++)
+ enumList.push_back(
+ ADDON::CAddonInfo::TranslateType(static_cast<ADDON::AddonType>(addonType), false));
+ CJSONServiceDescription::AddEnum("Addon.Types", enumList);
+
+ enumList.clear();
+ CActionTranslator::GetActions(enumList);
+ CJSONServiceDescription::AddEnum("Input.Action", enumList);
+
+ enumList.clear();
+ CWindowTranslator::GetWindows(enumList);
+ CJSONServiceDescription::AddEnum("GUI.Window", enumList);
+
+ // filter-related enums
+ std::vector<std::string> smartplaylistList;
+ CDatabaseQueryRule::GetAvailableOperators(smartplaylistList);
+ CJSONServiceDescription::AddEnum("List.Filter.Operators", smartplaylistList);
+
+ smartplaylistList.clear();
+ CSmartPlaylist::GetAvailableFields("movies", smartplaylistList);
+ CJSONServiceDescription::AddEnum("List.Filter.Fields.Movies", smartplaylistList);
+
+ smartplaylistList.clear();
+ CSmartPlaylist::GetAvailableFields("tvshows", smartplaylistList);
+ CJSONServiceDescription::AddEnum("List.Filter.Fields.TVShows", smartplaylistList);
+
+ smartplaylistList.clear();
+ CSmartPlaylist::GetAvailableFields("episodes", smartplaylistList);
+ CJSONServiceDescription::AddEnum("List.Filter.Fields.Episodes", smartplaylistList);
+
+ smartplaylistList.clear();
+ CSmartPlaylist::GetAvailableFields("musicvideos", smartplaylistList);
+ CJSONServiceDescription::AddEnum("List.Filter.Fields.MusicVideos", smartplaylistList);
+
+ smartplaylistList.clear();
+ CSmartPlaylist::GetAvailableFields("artists", smartplaylistList);
+ CJSONServiceDescription::AddEnum("List.Filter.Fields.Artists", smartplaylistList);
+
+ smartplaylistList.clear();
+ CSmartPlaylist::GetAvailableFields("albums", smartplaylistList);
+ CJSONServiceDescription::AddEnum("List.Filter.Fields.Albums", smartplaylistList);
+
+ smartplaylistList.clear();
+ CSmartPlaylist::GetAvailableFields("songs", smartplaylistList);
+ CJSONServiceDescription::AddEnum("List.Filter.Fields.Songs", smartplaylistList);
+
+ smartplaylistList.clear();
+ CTextureRule::GetAvailableFields(smartplaylistList);
+ CJSONServiceDescription::AddEnum("List.Filter.Fields.Textures", smartplaylistList);
+
+ unsigned int size = sizeof(JSONRPC_SERVICE_TYPES) / sizeof(char*);
+
+ for (unsigned int index = 0; index < size; index++)
+ CJSONServiceDescription::AddType(JSONRPC_SERVICE_TYPES[index]);
+
+ size = sizeof(JSONRPC_SERVICE_METHODS) / sizeof(char*);
+
+ for (unsigned int index = 0; index < size; index++)
+ CJSONServiceDescription::AddBuiltinMethod(JSONRPC_SERVICE_METHODS[index]);
+
+ size = sizeof(JSONRPC_SERVICE_NOTIFICATIONS) / sizeof(char*);
+
+ for (unsigned int index = 0; index < size; index++)
+ CJSONServiceDescription::AddNotification(JSONRPC_SERVICE_NOTIFICATIONS[index]);
+
+ CJSONServiceDescription::ResolveReferences();
+
+ m_initialized = true;
+ CLog::Log(LOGINFO, "JSONRPC v{}: Successfully initialized",
+ CJSONServiceDescription::GetVersion());
+}
+
+void CJSONRPC::Cleanup()
+{
+ CJSONServiceDescription::Cleanup();
+ m_initialized = false;
+}
+
+JSONRPC_STATUS CJSONRPC::Introspect(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
+{
+ return CJSONServiceDescription::Print(result, transport, client,
+ parameterObject["getdescriptions"].asBoolean(), parameterObject["getmetadata"].asBoolean(), parameterObject["filterbytransport"].asBoolean(),
+ parameterObject["filter"]["id"].asString(), parameterObject["filter"]["type"].asString(), parameterObject["filter"]["getreferences"].asBoolean());
+}
+
+JSONRPC_STATUS CJSONRPC::Version(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
+{
+ result["version"]["major"] = 0;
+ result["version"]["minor"] = 0;
+ result["version"]["patch"] = 0;
+
+ const char* version = CJSONServiceDescription::GetVersion();
+ if (version != NULL)
+ {
+ std::vector<std::string> parts = StringUtils::Split(version, ".");
+ if (!parts.empty())
+ result["version"]["major"] = (int)strtol(parts[0].c_str(), NULL, 10);
+ if (parts.size() > 1)
+ result["version"]["minor"] = (int)strtol(parts[1].c_str(), NULL, 10);
+ if (parts.size() > 2)
+ result["version"]["patch"] = (int)strtol(parts[2].c_str(), NULL, 10);
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CJSONRPC::Permission(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
+{
+ int flags = client->GetPermissionFlags();
+
+ for (int i = 1; i <= OPERATION_PERMISSION_ALL; i *= 2)
+ result[PermissionToString((OperationPermission)i)] = (flags & i) == i;
+
+ return OK;
+}
+
+JSONRPC_STATUS CJSONRPC::Ping(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
+{
+ CVariant temp = "pong";
+ result.swap(temp);
+ return OK;
+}
+
+JSONRPC_STATUS CJSONRPC::GetConfiguration(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
+{
+ int flags = client->GetAnnouncementFlags();
+
+ for (int i = 1; i <= ANNOUNCEMENT::ANNOUNCE_ALL; i *= 2)
+ result["notifications"][AnnouncementFlagToString((ANNOUNCEMENT::AnnouncementFlag)i)] = (flags & i) == i;
+
+ return OK;
+}
+
+JSONRPC_STATUS CJSONRPC::SetConfiguration(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
+{
+ int flags = 0;
+ int oldFlags = client->GetAnnouncementFlags();
+
+ if (parameterObject.isMember("notifications"))
+ {
+ CVariant notifications = parameterObject["notifications"];
+ if ((notifications["Player"].isNull() && (oldFlags & ANNOUNCEMENT::Player)) ||
+ (notifications["Player"].isBoolean() && notifications["Player"].asBoolean()))
+ flags |= ANNOUNCEMENT::Player;
+ if ((notifications["Playlist"].isNull() && (oldFlags & ANNOUNCEMENT::Playlist)) ||
+ (notifications["Playlist"].isBoolean() && notifications["Playlist"].asBoolean()))
+ flags |= ANNOUNCEMENT::Playlist;
+ if ((notifications["GUI"].isNull() && (oldFlags & ANNOUNCEMENT::GUI)) ||
+ (notifications["GUI"].isBoolean() && notifications["GUI"].asBoolean()))
+ flags |= ANNOUNCEMENT::GUI;
+ if ((notifications["System"].isNull() && (oldFlags & ANNOUNCEMENT::System)) ||
+ (notifications["System"].isBoolean() && notifications["System"].asBoolean()))
+ flags |= ANNOUNCEMENT::System;
+ if ((notifications["VideoLibrary"].isNull() && (oldFlags & ANNOUNCEMENT::VideoLibrary)) ||
+ (notifications["VideoLibrary"].isBoolean() && notifications["VideoLibrary"].asBoolean()))
+ flags |= ANNOUNCEMENT::VideoLibrary;
+ if ((notifications["AudioLibrary"].isNull() && (oldFlags & ANNOUNCEMENT::AudioLibrary)) ||
+ (notifications["AudioLibrary"].isBoolean() && notifications["AudioLibrary"].asBoolean()))
+ flags |= ANNOUNCEMENT::AudioLibrary;
+ if ((notifications["Application"].isNull() && (oldFlags & ANNOUNCEMENT::Other)) ||
+ (notifications["Application"].isBoolean() && notifications["Application"].asBoolean()))
+ flags |= ANNOUNCEMENT::Application;
+ if ((notifications["Input"].isNull() && (oldFlags & ANNOUNCEMENT::Input)) ||
+ (notifications["Input"].isBoolean() && notifications["Input"].asBoolean()))
+ flags |= ANNOUNCEMENT::Input;
+ if ((notifications["Other"].isNull() && (oldFlags & ANNOUNCEMENT::Other)) ||
+ (notifications["Other"].isBoolean() && notifications["Other"].asBoolean()))
+ flags |= ANNOUNCEMENT::Other;
+ }
+
+ if (!client->SetAnnouncementFlags(flags))
+ return BadPermission;
+
+ return GetConfiguration(method, transport, client, parameterObject, result);
+}
+
+JSONRPC_STATUS CJSONRPC::NotifyAll(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
+{
+ if (parameterObject["data"].isNull())
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Other,
+ parameterObject["sender"].asString(),
+ parameterObject["message"].asString());
+ else
+ {
+ CVariant data = parameterObject["data"];
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Other,
+ parameterObject["sender"].asString(),
+ parameterObject["message"].asString(), data);
+ }
+
+ return ACK;
+}
+
+std::string CJSONRPC::MethodCall(const std::string &inputString, ITransportLayer *transport, IClient *client)
+{
+ CVariant inputroot, outputroot, result;
+ bool hasResponse = false;
+
+ CLog::Log(LOGDEBUG, LOGJSONRPC, "JSONRPC: Incoming request: {}", inputString);
+
+ if (CJSONVariantParser::Parse(inputString, inputroot) && !inputroot.isNull())
+ {
+ if (inputroot.isArray())
+ {
+ if (inputroot.size() <= 0)
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Empty batch call");
+ BuildResponse(inputroot, InvalidRequest, CVariant(), outputroot);
+ hasResponse = true;
+ }
+ else
+ {
+ for (CVariant::const_iterator_array itr = inputroot.begin_array();
+ itr != inputroot.end_array(); ++itr)
+ {
+ CVariant response;
+ if (HandleMethodCall(*itr, response, transport, client))
+ {
+ outputroot.append(response);
+ hasResponse = true;
+ }
+ }
+ }
+ }
+ else
+ hasResponse = HandleMethodCall(inputroot, outputroot, transport, client);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Failed to parse '{}'", inputString);
+ BuildResponse(inputroot, ParseError, CVariant(), outputroot);
+ hasResponse = true;
+ }
+
+ std::string str;
+ if (hasResponse)
+ CJSONVariantWriter::Write(outputroot, str, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_jsonOutputCompact);
+
+ return str;
+}
+
+bool CJSONRPC::HandleMethodCall(const CVariant& request, CVariant& response, ITransportLayer *transport, IClient *client)
+{
+ JSONRPC_STATUS errorCode = OK;
+ CVariant result;
+ bool isNotification = false;
+
+ if (IsProperJSONRPC(request))
+ {
+ isNotification = !request.isMember("id");
+
+ std::string methodName = request["method"].asString();
+ StringUtils::ToLower(methodName);
+
+ JSONRPC::MethodCall method;
+ CVariant params;
+
+ if ((errorCode = CJSONServiceDescription::CheckCall(methodName.c_str(), request["params"], transport, client, isNotification, method, params)) == OK)
+ errorCode = method(methodName, transport, client, params, result);
+ else
+ result = params;
+ }
+ else
+ {
+ std::string str;
+ CJSONVariantWriter::Write(request, str, true);
+
+ CLog::Log(LOGERROR, "JSONRPC: Failed to parse '{}'", str);
+ errorCode = InvalidRequest;
+ }
+
+ BuildResponse(request, errorCode, result, response);
+
+ return !isNotification;
+}
+
+inline bool CJSONRPC::IsProperJSONRPC(const CVariant& inputroot)
+{
+ return inputroot.isMember("jsonrpc") && inputroot["jsonrpc"].isString() && inputroot["jsonrpc"] == CVariant("2.0") && inputroot.isMember("method") && inputroot["method"].isString() && (!inputroot.isMember("params") || inputroot["params"].isArray() || inputroot["params"].isObject());
+}
+
+inline void CJSONRPC::BuildResponse(const CVariant& request, JSONRPC_STATUS code, const CVariant& result, CVariant& response)
+{
+ response["jsonrpc"] = "2.0";
+ response["id"] = request.isMember("id") ? request["id"] : CVariant();
+
+ switch (code)
+ {
+ case OK:
+ response["result"] = result;
+ break;
+ case ACK:
+ response["result"] = "OK";
+ break;
+ case InvalidRequest:
+ response["error"]["code"] = InvalidRequest;
+ response["error"]["message"] = "Invalid request.";
+ break;
+ case InvalidParams:
+ response["error"]["code"] = InvalidParams;
+ response["error"]["message"] = "Invalid params.";
+ if (!result.isNull())
+ response["error"]["data"] = result;
+ break;
+ case MethodNotFound:
+ response["error"]["code"] = MethodNotFound;
+ response["error"]["message"] = "Method not found.";
+ break;
+ case ParseError:
+ response["error"]["code"] = ParseError;
+ response["error"]["message"] = "Parse error.";
+ break;
+ case BadPermission:
+ response["error"]["code"] = BadPermission;
+ response["error"]["message"] = "Bad client permission.";
+ break;
+ case FailedToExecute:
+ response["error"]["code"] = FailedToExecute;
+ response["error"]["message"] = "Failed to execute method.";
+ break;
+ default:
+ response["error"]["code"] = InternalError;
+ response["error"]["message"] = "Internal error.";
+ break;
+ }
+}
+
+void CJSONRPCUtils::NotifyItemUpdated()
+{
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+}
+
+void CJSONRPCUtils::NotifyItemUpdated(const CVideoInfoTag& info,
+ const std::map<std::string, std::string>& artwork)
+{
+ CFileItemPtr msgItem(new CFileItem(info));
+ if (!artwork.empty())
+ msgItem->SetArt(artwork);
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0,
+ GUI_MSG_UPDATE_ITEM, 0, msgItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+}
diff --git a/xbmc/interfaces/json-rpc/JSONRPC.h b/xbmc/interfaces/json-rpc/JSONRPC.h
new file mode 100644
index 0000000..2e94576
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/JSONRPC.h
@@ -0,0 +1,72 @@
+/*
+ * 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 "JSONRPCUtils.h"
+#include "JSONServiceDescription.h"
+
+#include <iostream>
+#include <map>
+#include <stdio.h>
+#include <string>
+
+class CVariant;
+
+namespace JSONRPC
+{
+ /*!
+ \ingroup jsonrpc
+ \brief JSON RPC handler
+
+ Sets up and manages all needed information to process
+ JSON-RPC requests and answering with the appropriate
+ JSON-RPC response (actual response or error message).
+ */
+ class CJSONRPC
+ {
+ public:
+ /*!
+ \brief Initializes the JSON-RPC handler
+ */
+ static void Initialize();
+
+ static void Cleanup();
+
+ /*
+ \brief Handles an incoming JSON-RPC request
+ \param inputString received JSON-RPC request
+ \param transport Transport protocol on which the request arrived
+ \param client Client which sent the request
+ \return JSON-RPC response to be sent back to the client
+
+ Parses the received input string for the called method and provided
+ parameters. If the request does not conform to the JSON-RPC 2.0
+ specification an error is returned. Otherwise the parameters provided
+ in the request are checked for validity and completeness. If the request
+ is valid and the requested method exists it is called and executed.
+ */
+ static std::string MethodCall(const std::string &inputString, ITransportLayer *transport, IClient *client);
+
+ static JSONRPC_STATUS Introspect(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result);
+ static JSONRPC_STATUS Version(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result);
+ static JSONRPC_STATUS Permission(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result);
+ static JSONRPC_STATUS Ping(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetConfiguration(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetConfiguration(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result);
+ static JSONRPC_STATUS NotifyAll(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result);
+
+ private:
+ static bool HandleMethodCall(const CVariant& request, CVariant& response, ITransportLayer *transport, IClient *client);
+ static inline bool IsProperJSONRPC(const CVariant& inputroot);
+
+ inline static void BuildResponse(const CVariant& request, JSONRPC_STATUS code, const CVariant& result, CVariant& response);
+
+ static bool m_initialized;
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/JSONRPCUtils.h b/xbmc/interfaces/json-rpc/JSONRPCUtils.h
new file mode 100644
index 0000000..0c0a8f8
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/JSONRPCUtils.h
@@ -0,0 +1,163 @@
+/*
+ * 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 "IClient.h"
+#include "ITransportLayer.h"
+
+#include <map>
+#include <string>
+
+class CVariant;
+class CVideoInfoTag;
+
+namespace JSONRPC
+{
+ /*!
+ \ingroup jsonrpc
+ \brief Possible statuc codes of a response
+ to a JSON-RPC request
+ */
+ enum JSONRPC_STATUS
+ {
+ OK = 0,
+ ACK = -1,
+ InvalidRequest = -32600,
+ MethodNotFound = -32601,
+ InvalidParams = -32602,
+ InternalError = -32603,
+ ParseError = -32700,
+ //-32099..-32000 Reserved for implementation-defined server-errors.
+ BadPermission = -32099,
+ FailedToExecute = -32100
+ };
+
+ /*!
+ \brief Function pointer for JSON-RPC methods
+ */
+ typedef JSONRPC_STATUS (*MethodCall) (const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result);
+
+ /*!
+ \ingroup jsonrpc
+ \brief Permission categories for json rpc methods
+
+ A JSON-RPC method will only be called if the caller
+ has the correct permissions to execute the method.
+ The method call needs to be perfectly threadsafe.
+ */
+ enum OperationPermission
+ {
+ ReadData = 0x1,
+ ControlPlayback = 0x2,
+ ControlNotify = 0x4,
+ ControlPower = 0x8,
+ UpdateData = 0x10,
+ RemoveData = 0x20,
+ Navigate = 0x40,
+ WriteFile = 0x80,
+ ControlSystem = 0x100,
+ ControlGUI = 0x200,
+ ManageAddon = 0x400,
+ ExecuteAddon = 0x800,
+ ControlPVR = 0x1000
+ };
+
+ const int OPERATION_PERMISSION_ALL = (ReadData | ControlPlayback | ControlNotify | ControlPower |
+ UpdateData | RemoveData | Navigate | WriteFile | ControlSystem |
+ ControlGUI | ManageAddon | ExecuteAddon | ControlPVR);
+
+ const int OPERATION_PERMISSION_NOTIFICATION = (ControlPlayback | ControlNotify | ControlPower | UpdateData |
+ RemoveData | Navigate | WriteFile | ControlSystem |
+ ControlGUI | ManageAddon | ExecuteAddon | ControlPVR);
+
+ /*!
+ \brief Returns a string representation for the
+ given OperationPermission
+ \param permission Specific OperationPermission
+ \return String representation of the given OperationPermission
+ */
+ inline const char *PermissionToString(const OperationPermission &permission)
+ {
+ switch (permission)
+ {
+ case ReadData:
+ return "ReadData";
+ case ControlPlayback:
+ return "ControlPlayback";
+ case ControlNotify:
+ return "ControlNotify";
+ case ControlPower:
+ return "ControlPower";
+ case UpdateData:
+ return "UpdateData";
+ case RemoveData:
+ return "RemoveData";
+ case Navigate:
+ return "Navigate";
+ case WriteFile:
+ return "WriteFile";
+ case ControlSystem:
+ return "ControlSystem";
+ case ControlGUI:
+ return "ControlGUI";
+ case ManageAddon:
+ return "ManageAddon";
+ case ExecuteAddon:
+ return "ExecuteAddon";
+ case ControlPVR:
+ return "ControlPVR";
+ default:
+ return "Unknown";
+ }
+ }
+
+ /*!
+ \brief Returns a OperationPermission value for the given
+ string representation
+ \param permission String representation of the OperationPermission
+ \return OperationPermission value of the given string representation
+ */
+ inline OperationPermission StringToPermission(const std::string& permission)
+ {
+ if (permission.compare("ControlPlayback") == 0)
+ return ControlPlayback;
+ if (permission.compare("ControlNotify") == 0)
+ return ControlNotify;
+ if (permission.compare("ControlPower") == 0)
+ return ControlPower;
+ if (permission.compare("UpdateData") == 0)
+ return UpdateData;
+ if (permission.compare("RemoveData") == 0)
+ return RemoveData;
+ if (permission.compare("Navigate") == 0)
+ return Navigate;
+ if (permission.compare("WriteFile") == 0)
+ return WriteFile;
+ if (permission.compare("ControlSystem") == 0)
+ return ControlSystem;
+ if (permission.compare("ControlGUI") == 0)
+ return ControlGUI;
+ if (permission.compare("ManageAddon") == 0)
+ return ManageAddon;
+ if (permission.compare("ExecuteAddon") == 0)
+ return ExecuteAddon;
+ if (permission.compare("ControlPVR") == 0)
+ return ControlPVR;
+
+ return ReadData;
+ }
+
+ class CJSONRPCUtils
+ {
+ public:
+ static void NotifyItemUpdated();
+ static void NotifyItemUpdated(const CVideoInfoTag& info,
+ const std::map<std::string, std::string>& artwork);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/JSONServiceDescription.cpp b/xbmc/interfaces/json-rpc/JSONServiceDescription.cpp
new file mode 100644
index 0000000..da100b2
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/JSONServiceDescription.cpp
@@ -0,0 +1,2153 @@
+/*
+ * Copyright (C) 2016-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 "JSONServiceDescription.h"
+
+#include "AddonsOperations.h"
+#include "ApplicationOperations.h"
+#include "AudioLibrary.h"
+#include "FavouritesOperations.h"
+#include "FileOperations.h"
+#include "GUIOperations.h"
+#include "InputOperations.h"
+#include "JSONRPC.h"
+#include "PVROperations.h"
+#include "PlayerOperations.h"
+#include "PlaylistOperations.h"
+#include "ProfilesOperations.h"
+#include "ServiceDescription.h"
+#include "SettingsOperations.h"
+#include "SystemOperations.h"
+#include "TextureOperations.h"
+#include "VideoLibrary.h"
+#include "XBMCOperations.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+using namespace JSONRPC;
+
+std::map<std::string, CVariant> CJSONServiceDescription::m_notifications = std::map<std::string, CVariant>();
+CJSONServiceDescription::CJsonRpcMethodMap CJSONServiceDescription::m_actionMap;
+std::map<std::string, JSONSchemaTypeDefinitionPtr> CJSONServiceDescription::m_types = std::map<std::string, JSONSchemaTypeDefinitionPtr>();
+CJSONServiceDescription::IncompleteSchemaDefinitionMap CJSONServiceDescription::m_incompleteDefinitions = CJSONServiceDescription::IncompleteSchemaDefinitionMap();
+
+// clang-format off
+
+JsonRpcMethodMap CJSONServiceDescription::m_methodMaps[] = {
+// JSON-RPC
+ { "JSONRPC.Introspect", CJSONRPC::Introspect },
+ { "JSONRPC.Version", CJSONRPC::Version },
+ { "JSONRPC.Permission", CJSONRPC::Permission },
+ { "JSONRPC.Ping", CJSONRPC::Ping },
+ { "JSONRPC.GetConfiguration", CJSONRPC::GetConfiguration },
+ { "JSONRPC.SetConfiguration", CJSONRPC::SetConfiguration },
+ { "JSONRPC.NotifyAll", CJSONRPC::NotifyAll },
+
+// Player
+ { "Player.GetActivePlayers", CPlayerOperations::GetActivePlayers },
+ { "Player.GetPlayers", CPlayerOperations::GetPlayers },
+ { "Player.GetProperties", CPlayerOperations::GetProperties },
+ { "Player.GetItem", CPlayerOperations::GetItem },
+
+ { "Player.PlayPause", CPlayerOperations::PlayPause },
+ { "Player.Stop", CPlayerOperations::Stop },
+ { "Player.GetAudioDelay", CPlayerOperations::GetAudioDelay },
+ { "Player.SetAudioDelay", CPlayerOperations::SetAudioDelay },
+ { "Player.SetSpeed", CPlayerOperations::SetSpeed },
+ { "Player.Seek", CPlayerOperations::Seek },
+ { "Player.Move", CPlayerOperations::Move },
+ { "Player.Zoom", CPlayerOperations::Zoom },
+ { "Player.SetViewMode", CPlayerOperations::SetViewMode },
+ { "Player.GetViewMode", CPlayerOperations::GetViewMode },
+ { "Player.Rotate", CPlayerOperations::Rotate },
+
+ { "Player.Open", CPlayerOperations::Open },
+ { "Player.GoTo", CPlayerOperations::GoTo },
+ { "Player.SetShuffle", CPlayerOperations::SetShuffle },
+ { "Player.SetRepeat", CPlayerOperations::SetRepeat },
+ { "Player.SetPartymode", CPlayerOperations::SetPartymode },
+
+ { "Player.SetAudioStream", CPlayerOperations::SetAudioStream },
+ { "Player.AddSubtitle", CPlayerOperations::AddSubtitle },
+ { "Player.SetSubtitle", CPlayerOperations::SetSubtitle },
+ { "Player.SetVideoStream", CPlayerOperations::SetVideoStream },
+
+// Playlist
+ { "Playlist.GetPlaylists", CPlaylistOperations::GetPlaylists },
+ { "Playlist.GetProperties", CPlaylistOperations::GetProperties },
+ { "Playlist.GetItems", CPlaylistOperations::GetItems },
+ { "Playlist.Add", CPlaylistOperations::Add },
+ { "Playlist.Insert", CPlaylistOperations::Insert },
+ { "Playlist.Clear", CPlaylistOperations::Clear },
+ { "Playlist.Remove", CPlaylistOperations::Remove },
+ { "Playlist.Swap", CPlaylistOperations::Swap },
+
+// Files
+ { "Files.GetSources", CFileOperations::GetRootDirectory },
+ { "Files.GetDirectory", CFileOperations::GetDirectory },
+ { "Files.GetFileDetails", CFileOperations::GetFileDetails },
+ { "Files.SetFileDetails", CFileOperations::SetFileDetails },
+ { "Files.PrepareDownload", CFileOperations::PrepareDownload },
+ { "Files.Download", CFileOperations::Download },
+
+// Music Library
+ { "AudioLibrary.GetProperties", CAudioLibrary::GetProperties },
+ { "AudioLibrary.GetArtists", CAudioLibrary::GetArtists },
+ { "AudioLibrary.GetArtistDetails", CAudioLibrary::GetArtistDetails },
+ { "AudioLibrary.GetAlbums", CAudioLibrary::GetAlbums },
+ { "AudioLibrary.GetAlbumDetails", CAudioLibrary::GetAlbumDetails },
+ { "AudioLibrary.GetSongs", CAudioLibrary::GetSongs },
+ { "AudioLibrary.GetSongDetails", CAudioLibrary::GetSongDetails },
+ { "AudioLibrary.GetRecentlyAddedAlbums", CAudioLibrary::GetRecentlyAddedAlbums },
+ { "AudioLibrary.GetRecentlyAddedSongs", CAudioLibrary::GetRecentlyAddedSongs },
+ { "AudioLibrary.GetRecentlyPlayedAlbums", CAudioLibrary::GetRecentlyPlayedAlbums },
+ { "AudioLibrary.GetRecentlyPlayedSongs", CAudioLibrary::GetRecentlyPlayedSongs },
+ { "AudioLibrary.GetGenres", CAudioLibrary::GetGenres },
+ { "AudioLibrary.GetRoles", CAudioLibrary::GetRoles },
+ { "AudioLibrary.GetSources", CAudioLibrary::GetSources },
+ { "AudioLibrary.GetAvailableArtTypes", CAudioLibrary::GetAvailableArtTypes },
+ { "AudioLibrary.GetAvailableArt", CAudioLibrary::GetAvailableArt },
+ { "AudioLibrary.SetArtistDetails", CAudioLibrary::SetArtistDetails },
+ { "AudioLibrary.SetAlbumDetails", CAudioLibrary::SetAlbumDetails },
+ { "AudioLibrary.SetSongDetails", CAudioLibrary::SetSongDetails },
+ { "AudioLibrary.Scan", CAudioLibrary::Scan },
+ { "AudioLibrary.Export", CAudioLibrary::Export },
+ { "AudioLibrary.Clean", CAudioLibrary::Clean },
+
+// Video Library
+ { "VideoLibrary.GetGenres", CVideoLibrary::GetGenres },
+ { "VideoLibrary.GetTags", CVideoLibrary::GetTags },
+ { "VideoLibrary.GetAvailableArtTypes", CVideoLibrary::GetAvailableArtTypes },
+ { "VideoLibrary.GetAvailableArt", CVideoLibrary::GetAvailableArt },
+ { "VideoLibrary.GetMovies", CVideoLibrary::GetMovies },
+ { "VideoLibrary.GetMovieDetails", CVideoLibrary::GetMovieDetails },
+ { "VideoLibrary.GetMovieSets", CVideoLibrary::GetMovieSets },
+ { "VideoLibrary.GetMovieSetDetails", CVideoLibrary::GetMovieSetDetails },
+ { "VideoLibrary.GetTVShows", CVideoLibrary::GetTVShows },
+ { "VideoLibrary.GetTVShowDetails", CVideoLibrary::GetTVShowDetails },
+ { "VideoLibrary.GetSeasons", CVideoLibrary::GetSeasons },
+ { "VideoLibrary.GetSeasonDetails", CVideoLibrary::GetSeasonDetails },
+ { "VideoLibrary.GetEpisodes", CVideoLibrary::GetEpisodes },
+ { "VideoLibrary.GetEpisodeDetails", CVideoLibrary::GetEpisodeDetails },
+ { "VideoLibrary.GetMusicVideos", CVideoLibrary::GetMusicVideos },
+ { "VideoLibrary.GetMusicVideoDetails", CVideoLibrary::GetMusicVideoDetails },
+ { "VideoLibrary.GetRecentlyAddedMovies", CVideoLibrary::GetRecentlyAddedMovies },
+ { "VideoLibrary.GetRecentlyAddedEpisodes", CVideoLibrary::GetRecentlyAddedEpisodes },
+ { "VideoLibrary.GetRecentlyAddedMusicVideos", CVideoLibrary::GetRecentlyAddedMusicVideos },
+ { "VideoLibrary.GetInProgressTVShows", CVideoLibrary::GetInProgressTVShows },
+ { "VideoLibrary.SetMovieDetails", CVideoLibrary::SetMovieDetails },
+ { "VideoLibrary.SetMovieSetDetails", CVideoLibrary::SetMovieSetDetails },
+ { "VideoLibrary.SetTVShowDetails", CVideoLibrary::SetTVShowDetails },
+ { "VideoLibrary.SetSeasonDetails", CVideoLibrary::SetSeasonDetails },
+ { "VideoLibrary.SetEpisodeDetails", CVideoLibrary::SetEpisodeDetails },
+ { "VideoLibrary.SetMusicVideoDetails", CVideoLibrary::SetMusicVideoDetails },
+ { "VideoLibrary.RefreshMovie", CVideoLibrary::RefreshMovie },
+ { "VideoLibrary.RefreshTVShow", CVideoLibrary::RefreshTVShow },
+ { "VideoLibrary.RefreshEpisode", CVideoLibrary::RefreshEpisode },
+ { "VideoLibrary.RefreshMusicVideo", CVideoLibrary::RefreshMusicVideo },
+ { "VideoLibrary.RemoveMovie", CVideoLibrary::RemoveMovie },
+ { "VideoLibrary.RemoveTVShow", CVideoLibrary::RemoveTVShow },
+ { "VideoLibrary.RemoveEpisode", CVideoLibrary::RemoveEpisode },
+ { "VideoLibrary.RemoveMusicVideo", CVideoLibrary::RemoveMusicVideo },
+ { "VideoLibrary.Scan", CVideoLibrary::Scan },
+ { "VideoLibrary.Export", CVideoLibrary::Export },
+ { "VideoLibrary.Clean", CVideoLibrary::Clean },
+
+// Addon operations
+ { "Addons.GetAddons", CAddonsOperations::GetAddons },
+ { "Addons.GetAddonDetails", CAddonsOperations::GetAddonDetails },
+ { "Addons.SetAddonEnabled", CAddonsOperations::SetAddonEnabled },
+ { "Addons.ExecuteAddon", CAddonsOperations::ExecuteAddon },
+
+// GUI operations
+ { "GUI.GetProperties", CGUIOperations::GetProperties },
+ { "GUI.ActivateWindow", CGUIOperations::ActivateWindow },
+ { "GUI.ShowNotification", CGUIOperations::ShowNotification },
+ { "GUI.SetFullscreen", CGUIOperations::SetFullscreen },
+ { "GUI.SetStereoscopicMode", CGUIOperations::SetStereoscopicMode },
+ { "GUI.GetStereoscopicModes", CGUIOperations::GetStereoscopicModes },
+
+// PVR operations
+ { "PVR.GetProperties", CPVROperations::GetProperties },
+ { "PVR.GetChannelGroups", CPVROperations::GetChannelGroups },
+ { "PVR.GetChannelGroupDetails", CPVROperations::GetChannelGroupDetails },
+ { "PVR.GetChannels", CPVROperations::GetChannels },
+ { "PVR.GetChannelDetails", CPVROperations::GetChannelDetails },
+ { "PVR.GetClients", CPVROperations::GetClients },
+ { "PVR.GetBroadcasts", CPVROperations::GetBroadcasts },
+ { "PVR.GetBroadcastDetails", CPVROperations::GetBroadcastDetails },
+ { "PVR.GetBroadcastIsPlayable", CPVROperations::GetBroadcastIsPlayable },
+ { "PVR.GetTimers", CPVROperations::GetTimers },
+ { "PVR.GetTimerDetails", CPVROperations::GetTimerDetails },
+ { "PVR.GetRecordings", CPVROperations::GetRecordings },
+ { "PVR.GetRecordingDetails", CPVROperations::GetRecordingDetails },
+ { "PVR.AddTimer", CPVROperations::AddTimer },
+ { "PVR.DeleteTimer", CPVROperations::DeleteTimer },
+ { "PVR.ToggleTimer", CPVROperations::ToggleTimer },
+ { "PVR.Record", CPVROperations::Record },
+ { "PVR.Scan", CPVROperations::Scan },
+
+// Profiles operations
+ { "Profiles.GetProfiles", CProfilesOperations::GetProfiles},
+ { "Profiles.GetCurrentProfile", CProfilesOperations::GetCurrentProfile},
+ { "Profiles.LoadProfile", CProfilesOperations::LoadProfile},
+
+// System operations
+ { "System.GetProperties", CSystemOperations::GetProperties },
+ { "System.EjectOpticalDrive", CSystemOperations::EjectOpticalDrive },
+ { "System.Shutdown", CSystemOperations::Shutdown },
+ { "System.Suspend", CSystemOperations::Suspend },
+ { "System.Hibernate", CSystemOperations::Hibernate },
+ { "System.Reboot", CSystemOperations::Reboot },
+
+// Input operations
+ { "Input.SendText", CInputOperations::SendText },
+ { "Input.ExecuteAction", CInputOperations::ExecuteAction },
+ { "Input.ButtonEvent", CInputOperations::ButtonEvent },
+ { "Input.Left", CInputOperations::Left },
+ { "Input.Right", CInputOperations::Right },
+ { "Input.Down", CInputOperations::Down },
+ { "Input.Up", CInputOperations::Up },
+ { "Input.Select", CInputOperations::Select },
+ { "Input.Back", CInputOperations::Back },
+ { "Input.ContextMenu", CInputOperations::ContextMenu },
+ { "Input.Info", CInputOperations::Info },
+ { "Input.Home", CInputOperations::Home },
+ { "Input.ShowCodec", CInputOperations::ShowCodec },
+ { "Input.ShowOSD", CInputOperations::ShowOSD },
+ { "Input.ShowPlayerProcessInfo", CInputOperations::ShowPlayerProcessInfo },
+
+// Application operations
+ { "Application.GetProperties", CApplicationOperations::GetProperties },
+ { "Application.SetVolume", CApplicationOperations::SetVolume },
+ { "Application.SetMute", CApplicationOperations::SetMute },
+ { "Application.Quit", CApplicationOperations::Quit },
+
+// Favourites operations
+ { "Favourites.GetFavourites", CFavouritesOperations::GetFavourites },
+ { "Favourites.AddFavourite", CFavouritesOperations::AddFavourite },
+
+// Textures operations
+ { "Textures.GetTextures", CTextureOperations::GetTextures },
+ { "Textures.RemoveTexture", CTextureOperations::RemoveTexture },
+
+// Settings operations
+ { "Settings.GetSections", CSettingsOperations::GetSections },
+ { "Settings.GetCategories", CSettingsOperations::GetCategories },
+ { "Settings.GetSettings", CSettingsOperations::GetSettings },
+ { "Settings.GetSettingValue", CSettingsOperations::GetSettingValue },
+ { "Settings.SetSettingValue", CSettingsOperations::SetSettingValue },
+ { "Settings.ResetSettingValue", CSettingsOperations::ResetSettingValue },
+ { "Settings.GetSkinSettings", CSettingsOperations::GetSkinSettings },
+ { "Settings.GetSkinSettingValue", CSettingsOperations::GetSkinSettingValue },
+ { "Settings.SetSkinSettingValue", CSettingsOperations::SetSkinSettingValue },
+
+// XBMC operations
+ { "XBMC.GetInfoLabels", CXBMCOperations::GetInfoLabels },
+ { "XBMC.GetInfoBooleans", CXBMCOperations::GetInfoBooleans }
+};
+
+// clang-format on
+
+JSONSchemaTypeDefinition::JSONSchemaTypeDefinition()
+ : missingReference(),
+ name(),
+ ID(),
+ referencedType(nullptr),
+ extends(),
+ description(),
+ unionTypes(),
+ defaultValue(),
+ minimum(-std::numeric_limits<double>::max()),
+ maximum(std::numeric_limits<double>::max()),
+ enums(),
+ items(),
+ additionalItems(),
+ properties(),
+ additionalProperties(nullptr)
+{ }
+
+bool JSONSchemaTypeDefinition::Parse(const CVariant &value, bool isParameter /* = false */)
+{
+ bool hasReference = false;
+
+ // Check if the type of the parameter defines a json reference
+ // to a type defined somewhere else
+ if (value.isMember("$ref") && value["$ref"].isString())
+ {
+ // Get the name of the referenced type
+ std::string refType = value["$ref"].asString();
+ // Check if the referenced type exists
+ JSONSchemaTypeDefinitionPtr referencedTypeDef = CJSONServiceDescription::GetType(refType);
+ if (refType.length() <= 0 || referencedTypeDef.get() == NULL)
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: JSON schema type {} references an unknown type {}", name,
+ refType);
+ missingReference = refType;
+ return false;
+ }
+
+ std::string typeName = name;
+ *this = *referencedTypeDef;
+ if (!typeName.empty())
+ name = typeName;
+ referencedType = referencedTypeDef;
+ hasReference = true;
+ }
+ else if (value.isMember("id") && value["id"].isString())
+ ID = GetString(value["id"], "");
+
+ // Check if the "required" field has been defined
+ optional = value.isMember("required") && value["required"].isBoolean() ? !value["required"].asBoolean() : true;
+
+ // Get the "description"
+ if (!hasReference || (value.isMember("description") && value["description"].isString()))
+ description = GetString(value["description"], "");
+
+ if (hasReference)
+ {
+ // If there is a specific default value, read it
+ if (value.isMember("default") && IsType(value["default"], type))
+ {
+ bool ok = false;
+ if (enums.size() <= 0)
+ ok = true;
+ // If the type has an enum definition we must make
+ // sure that the default value is a valid enum value
+ else
+ {
+ for (const auto& itr : enums)
+ {
+ if (value["default"] == itr)
+ {
+ ok = true;
+ break;
+ }
+ }
+ }
+
+ if (ok)
+ defaultValue = value["default"];
+ }
+
+ return true;
+ }
+
+ // Check whether this type extends an existing type
+ if (value.isMember("extends"))
+ {
+ if (value["extends"].isString())
+ {
+ std::string extendsName = GetString(value["extends"], "");
+ if (!extendsName.empty())
+ {
+ JSONSchemaTypeDefinitionPtr extendedTypeDef = CJSONServiceDescription::GetType(extendsName);
+ if (extendedTypeDef.get() == NULL)
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: JSON schema type {} extends an unknown type {}", name,
+ extendsName);
+ missingReference = extendsName;
+ return false;
+ }
+
+ type = extendedTypeDef->type;
+ extends.push_back(extendedTypeDef);
+ }
+ }
+ else if (value["extends"].isArray())
+ {
+ JSONSchemaType extendedType = AnyValue;
+ for (unsigned int extendsIndex = 0; extendsIndex < value["extends"].size(); extendsIndex++)
+ {
+ std::string extendsName = GetString(value["extends"][extendsIndex], "");
+ if (!extendsName.empty())
+ {
+ JSONSchemaTypeDefinitionPtr extendedTypeDef = CJSONServiceDescription::GetType(extendsName);
+ if (extendedTypeDef.get() == NULL)
+ {
+ extends.clear();
+ CLog::Log(LOGDEBUG, "JSONRPC: JSON schema type {} extends an unknown type {}", name,
+ extendsName);
+ missingReference = extendsName;
+ return false;
+ }
+
+ if (extendsIndex == 0)
+ extendedType = extendedTypeDef->type;
+ else if (extendedType != extendedTypeDef->type)
+ {
+ extends.clear();
+ CLog::Log(LOGDEBUG,
+ "JSONRPC: JSON schema type {} extends multiple JSON schema types of "
+ "mismatching types",
+ name);
+ return false;
+ }
+
+ extends.push_back(extendedTypeDef);
+ }
+ }
+
+ type = extendedType;
+ }
+ }
+
+ // Only read the "type" attribute if it's
+ // not an extending type
+ if (extends.size() <= 0)
+ {
+ // Get the defined type of the parameter
+ if (!CJSONServiceDescription::parseJSONSchemaType(value["type"], unionTypes, type, missingReference))
+ return false;
+ }
+
+ if (HasType(type, ObjectValue))
+ {
+ // If the type definition is of type "object"
+ // and has a "properties" definition we need
+ // to handle these as well
+ if (value.isMember("properties") && value["properties"].isObject())
+ {
+ // Get all child elements of the "properties"
+ // object and loop through them
+ for (CVariant::const_iterator_map itr = value["properties"].begin_map(); itr != value["properties"].end_map(); ++itr)
+ {
+ // Create a new type definition, store the name
+ // of the current property into it, parse it
+ // recursively and add its default value
+ // to the current type's default value
+ JSONSchemaTypeDefinitionPtr propertyType = JSONSchemaTypeDefinitionPtr(new JSONSchemaTypeDefinition());
+ propertyType->name = itr->first;
+ if (!propertyType->Parse(itr->second))
+ {
+ missingReference = propertyType->missingReference;
+ return false;
+ }
+ defaultValue[itr->first] = propertyType->defaultValue;
+ properties.add(propertyType);
+ }
+ }
+
+ hasAdditionalProperties = true;
+ additionalProperties = JSONSchemaTypeDefinitionPtr(new JSONSchemaTypeDefinition());
+ if (value.isMember("additionalProperties"))
+ {
+ if (value["additionalProperties"].isBoolean())
+ {
+ hasAdditionalProperties = value["additionalProperties"].asBoolean();
+ if (!hasAdditionalProperties)
+ {
+ additionalProperties.reset();
+ }
+ }
+ else if (value["additionalProperties"].isObject() && !value["additionalProperties"].isNull())
+ {
+ if (!additionalProperties->Parse(value["additionalProperties"]))
+ {
+ missingReference = additionalProperties->missingReference;
+ hasAdditionalProperties = false;
+ additionalProperties.reset();
+
+ CLog::Log(LOGDEBUG, "JSONRPC: Invalid additionalProperties schema definition in type {}",
+ name);
+ return false;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Invalid additionalProperties definition in type {}", name);
+ return false;
+ }
+ }
+ }
+
+ // If the defined parameter is an array
+ // we need to check for detailed definitions
+ // of the array items
+ if (HasType(type, ArrayValue))
+ {
+ // Check for "uniqueItems" field
+ if (value.isMember("uniqueItems") && value["uniqueItems"].isBoolean())
+ uniqueItems = value["uniqueItems"].asBoolean();
+ else
+ uniqueItems = false;
+
+ // Check for "additionalItems" field
+ if (value.isMember("additionalItems"))
+ {
+ // If it is an object, there is only one schema for it
+ if (value["additionalItems"].isObject())
+ {
+ JSONSchemaTypeDefinitionPtr additionalItem = JSONSchemaTypeDefinitionPtr(new JSONSchemaTypeDefinition());
+ if (additionalItem->Parse(value["additionalItems"]))
+ additionalItems.push_back(additionalItem);
+ else
+ {
+ CLog::Log(LOGDEBUG, "Invalid \"additionalItems\" value for type {}", name);
+ missingReference = additionalItem->missingReference;
+ return false;
+ }
+ }
+ // If it is an array there may be multiple schema definitions
+ else if (value["additionalItems"].isArray())
+ {
+ for (unsigned int itemIndex = 0; itemIndex < value["additionalItems"].size(); itemIndex++)
+ {
+ JSONSchemaTypeDefinitionPtr additionalItem = JSONSchemaTypeDefinitionPtr(new JSONSchemaTypeDefinition());
+
+ if (additionalItem->Parse(value["additionalItems"][itemIndex]))
+ additionalItems.push_back(additionalItem);
+ else
+ {
+ CLog::Log(LOGDEBUG, "Invalid \"additionalItems\" value (item {}) for type {}",
+ itemIndex, name);
+ missingReference = additionalItem->missingReference;
+ return false;
+ }
+ }
+ }
+ // If it is not a (array of) schema and not a bool (default value is false)
+ // it has an invalid value
+ else if (!value["additionalItems"].isBoolean())
+ {
+ CLog::Log(LOGDEBUG, "Invalid \"additionalItems\" definition for type {}", name);
+ return false;
+ }
+ }
+
+ // If the "items" field is a single object
+ // we can parse that directly
+ if (value.isMember("items"))
+ {
+ if (value["items"].isObject())
+ {
+ JSONSchemaTypeDefinitionPtr item = JSONSchemaTypeDefinitionPtr(new JSONSchemaTypeDefinition());
+ if (!item->Parse(value["items"]))
+ {
+ CLog::Log(LOGDEBUG, "Invalid item definition in \"items\" for type {}", name);
+ missingReference = item->missingReference;
+ return false;
+ }
+ items.push_back(item);
+ }
+ // Otherwise if it is an array we need to
+ // parse all elements and store them
+ else if (value["items"].isArray())
+ {
+ for (CVariant::const_iterator_array itemItr = value["items"].begin_array(); itemItr != value["items"].end_array(); ++itemItr)
+ {
+ JSONSchemaTypeDefinitionPtr item = JSONSchemaTypeDefinitionPtr(new JSONSchemaTypeDefinition());
+ if (!item->Parse(*itemItr))
+ {
+ CLog::Log(LOGDEBUG, "Invalid item definition in \"items\" array for type {}", name);
+ missingReference = item->missingReference;
+ return false;
+ }
+ items.push_back(item);
+ }
+ }
+ }
+
+ minItems = (unsigned int)value["minItems"].asUnsignedInteger(0);
+ maxItems = (unsigned int)value["maxItems"].asUnsignedInteger(0);
+ }
+
+ if (HasType(type, NumberValue) || HasType(type, IntegerValue))
+ {
+ if ((type & NumberValue) == NumberValue)
+ {
+ minimum = value["minimum"].asDouble(-std::numeric_limits<double>::max());
+ maximum = value["maximum"].asDouble(std::numeric_limits<double>::max());
+ }
+ else if ((type & IntegerValue) == IntegerValue)
+ {
+ minimum = (double)value["minimum"].asInteger(std::numeric_limits<int>::min());
+ maximum = (double)value["maximum"].asInteger(std::numeric_limits<int>::max());
+ }
+
+ exclusiveMinimum = value["exclusiveMinimum"].asBoolean(false);
+ exclusiveMaximum = value["exclusiveMaximum"].asBoolean(false);
+ divisibleBy = (unsigned int)value["divisibleBy"].asUnsignedInteger(0);
+ }
+
+ if (HasType(type, StringValue))
+ {
+ minLength = (int)value["minLength"].asInteger(-1);
+ maxLength = (int)value["maxLength"].asInteger(-1);
+ }
+
+ // If the type definition is neither an
+ // "object" nor an "array" we can check
+ // for an "enum" definition
+ if (value.isMember("enum") && value["enum"].isArray())
+ {
+ // Loop through all elements in the "enum" array
+ for (CVariant::const_iterator_array enumItr = value["enum"].begin_array(); enumItr != value["enum"].end_array(); ++enumItr)
+ {
+ // Check for duplicates and eliminate them
+ bool approved = true;
+ for (unsigned int approvedIndex = 0; approvedIndex < enums.size(); approvedIndex++)
+ {
+ if (*enumItr == enums.at(approvedIndex))
+ {
+ approved = false;
+ break;
+ }
+ }
+
+ // Only add the current item to the enum value
+ // list if it is not duplicate
+ if (approved)
+ enums.push_back(*enumItr);
+ }
+ }
+
+ if (type != ObjectValue)
+ {
+ // If there is a definition for a default value and its type
+ // matches the type of the parameter we can parse it
+ bool ok = false;
+ if (value.isMember("default") && IsType(value["default"], type))
+ {
+ if (enums.size() <= 0)
+ ok = true;
+ // If the type has an enum definition we must make
+ // sure that the default value is a valid enum value
+ else
+ {
+ for (std::vector<CVariant>::const_iterator itr = enums.begin(); itr != enums.end(); ++itr)
+ {
+ if (value["default"] == *itr)
+ {
+ ok = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (ok)
+ defaultValue = value["default"];
+ else
+ {
+ // If the type of the default value definition does not
+ // match the type of the parameter we have to log this
+ if (value.isMember("default") && !IsType(value["default"], type))
+ CLog::Log(LOGDEBUG, "JSONRPC: Parameter {} has an invalid default value", name);
+
+ // If the type contains an "enum" we need to get the
+ // default value from the first enum value
+ if (enums.size() > 0)
+ defaultValue = enums.at(0);
+ // otherwise set a default value instead
+ else
+ SetDefaultValue(defaultValue, type);
+ }
+ }
+
+ return true;
+}
+
+JSONRPC_STATUS JSONSchemaTypeDefinition::Check(const CVariant& value,
+ CVariant& outputValue,
+ CVariant& errorData) const
+{
+ if (!name.empty())
+ errorData["name"] = name;
+ SchemaValueTypeToJson(type, errorData["type"]);
+ std::string errorMessage;
+
+ // Let's check the type of the provided parameter
+ if (!IsType(value, type))
+ {
+ errorMessage = StringUtils::Format("Invalid type {} received", ValueTypeToString(value.type()));
+ errorData["message"] = errorMessage.c_str();
+ return InvalidParams;
+ }
+ else if (value.isNull() && !HasType(type, NullValue))
+ {
+ errorData["message"] = "Received value is null";
+ return InvalidParams;
+ }
+
+ // Let's check if we have to handle a union type
+ if (unionTypes.size() > 0)
+ {
+ bool ok = false;
+ for (unsigned int unionIndex = 0; unionIndex < unionTypes.size(); unionIndex++)
+ {
+ CVariant dummyError;
+ CVariant testOutput = outputValue;
+ if (unionTypes.at(unionIndex)->Check(value, testOutput, dummyError) == OK)
+ {
+ ok = true;
+ outputValue = testOutput;
+ break;
+ }
+ }
+
+ if (!ok)
+ {
+ errorData["message"] = "Received value does not match any of the union type definitions";
+ return InvalidParams;
+ }
+ }
+
+ // First we need to check if this type extends another
+ // type and if so we need to check against the extended
+ // type first
+ if (extends.size() > 0)
+ {
+ for (unsigned int extendsIndex = 0; extendsIndex < extends.size(); extendsIndex++)
+ {
+ JSONRPC_STATUS status = extends.at(extendsIndex)->Check(value, outputValue, errorData);
+
+ if (status != OK)
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Value does not match extended type {} of type {}",
+ extends.at(extendsIndex)->ID, name);
+ errorMessage = StringUtils::Format("value does not match extended type {}",
+ extends.at(extendsIndex)->ID);
+ errorData["message"] = errorMessage.c_str();
+ return status;
+ }
+ }
+ }
+
+ // If it is an array we need to
+ // - check the type of every element ("items")
+ // - check if they need to be unique ("uniqueItems")
+ if (HasType(type, ArrayValue) && value.isArray())
+ {
+ outputValue = CVariant(CVariant::VariantTypeArray);
+ // Check the number of items against minItems and maxItems
+ if ((minItems > 0 && value.size() < minItems) || (maxItems > 0 && value.size() > maxItems))
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "JSONRPC: Number of array elements does not match minItems and/or maxItems in type {}",
+ name);
+ if (minItems > 0 && maxItems > 0)
+ errorMessage = StringUtils::Format("Between {} and {} array items expected but {} received",
+ minItems, maxItems, value.size());
+ else if (minItems > 0)
+ errorMessage = StringUtils::Format("At least {} array items expected but only {} received",
+ minItems, value.size());
+ else
+ errorMessage = StringUtils::Format("Only {} array items expected but {} received", maxItems,
+ value.size());
+ errorData["message"] = errorMessage.c_str();
+ return InvalidParams;
+ }
+
+ if (items.size() == 0)
+ outputValue = value;
+ else if (items.size() == 1)
+ {
+ JSONSchemaTypeDefinitionPtr itemType = items.at(0);
+
+ // Loop through all array elements
+ for (unsigned int arrayIndex = 0; arrayIndex < value.size(); arrayIndex++)
+ {
+ CVariant temp;
+ JSONRPC_STATUS status = itemType->Check(value[arrayIndex], temp, errorData["property"]);
+ outputValue.push_back(temp);
+ if (status != OK)
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Array element at index {} does not match in type {}",
+ arrayIndex, name);
+ errorMessage =
+ StringUtils::Format("array element at index {} does not match", arrayIndex);
+ errorData["message"] = errorMessage.c_str();
+ return status;
+ }
+ }
+ }
+ // We have more than one element in "items"
+ // so we have tuple typing, which means that
+ // every element in the value array must match
+ // with the type at the same position in the
+ // "items" array
+ else
+ {
+ // If the number of elements in the value array
+ // does not match the number of elements in the
+ // "items" array and additional items are not
+ // allowed there is no need to check every element
+ if (value.size() < items.size() || (value.size() != items.size() && additionalItems.size() == 0))
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: One of the array elements does not match in type {}", name);
+ errorMessage = StringUtils::Format("{0} array elements expected but {1} received", items.size(), value.size());
+ errorData["message"] = errorMessage.c_str();
+ return InvalidParams;
+ }
+
+ // Loop through all array elements until there
+ // are either no more schemas in the "items"
+ // array or no more elements in the value's array
+ unsigned int arrayIndex;
+ for (arrayIndex = 0; arrayIndex < std::min(items.size(), (size_t)value.size()); arrayIndex++)
+ {
+ JSONRPC_STATUS status = items.at(arrayIndex)->Check(value[arrayIndex], outputValue[arrayIndex], errorData["property"]);
+ if (status != OK)
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "JSONRPC: Array element at index {} does not match with items schema in type {}",
+ arrayIndex, name);
+ return status;
+ }
+ }
+
+ if (additionalItems.size() > 0)
+ {
+ // Loop through the rest of the elements
+ // in the array and check them against the
+ // "additionalItems"
+ for (; arrayIndex < value.size(); arrayIndex++)
+ {
+ bool ok = false;
+ for (unsigned int additionalIndex = 0; additionalIndex < additionalItems.size(); additionalIndex++)
+ {
+ CVariant dummyError;
+ if (additionalItems.at(additionalIndex)->Check(value[arrayIndex], outputValue[arrayIndex], dummyError) == OK)
+ {
+ ok = true;
+ break;
+ }
+ }
+
+ if (!ok)
+ {
+ CLog::Log(LOGDEBUG,
+ "JSONRPC: Array contains non-conforming additional items in type {}", name);
+ errorMessage = StringUtils::Format(
+ "Array element at index {} does not match the \"additionalItems\" schema",
+ arrayIndex);
+ errorData["message"] = errorMessage.c_str();
+ return InvalidParams;
+ }
+ }
+ }
+ }
+
+ // If every array element is unique we need to check each one
+ if (uniqueItems)
+ {
+ for (unsigned int checkingIndex = 0; checkingIndex < outputValue.size(); checkingIndex++)
+ {
+ for (unsigned int checkedIndex = checkingIndex + 1; checkedIndex < outputValue.size(); checkedIndex++)
+ {
+ // If two elements are the same they are not unique
+ if (outputValue[checkingIndex] == outputValue[checkedIndex])
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Not unique array element at index {} and {} in type {}",
+ checkingIndex, checkedIndex, name);
+ errorMessage = StringUtils::Format(
+ "Array element at index {} is not unique (same as array element at index {})",
+ checkingIndex, checkedIndex);
+ errorData["message"] = errorMessage.c_str();
+ return InvalidParams;
+ }
+ }
+ }
+ }
+
+ return OK;
+ }
+
+ // If it is an object we need to check every element
+ // against the defined "properties"
+ if (HasType(type, ObjectValue) && value.isObject())
+ {
+ unsigned int handled = 0;
+ JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::JSONSchemaPropertiesIterator propertiesEnd = properties.end();
+ JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::JSONSchemaPropertiesIterator propertiesIterator;
+ for (propertiesIterator = properties.begin(); propertiesIterator != propertiesEnd; ++propertiesIterator)
+ {
+ if (value.isMember(propertiesIterator->second->name))
+ {
+ JSONRPC_STATUS status = propertiesIterator->second->Check(value[propertiesIterator->second->name], outputValue[propertiesIterator->second->name], errorData["property"]);
+ if (status != OK)
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Invalid property \"{}\" in type {}",
+ propertiesIterator->second->name, name);
+ return status;
+ }
+ handled++;
+ }
+ else if (propertiesIterator->second->optional)
+ outputValue[propertiesIterator->second->name] = propertiesIterator->second->defaultValue;
+ else
+ {
+ errorData["property"]["name"] = propertiesIterator->second->name.c_str();
+ errorData["property"]["type"] = SchemaValueTypeToString(propertiesIterator->second->type);
+ errorData["message"] = "Missing property";
+ return InvalidParams;
+ }
+ }
+
+ // Additional properties are not allowed
+ if (handled < value.size())
+ {
+ // If additional properties are allowed we need to check if
+ // they match the defined schema
+ if (hasAdditionalProperties && additionalProperties != NULL)
+ {
+ CVariant::const_iterator_map iter;
+ CVariant::const_iterator_map iterEnd = value.end_map();
+ for (iter = value.begin_map(); iter != iterEnd; ++iter)
+ {
+ if (properties.find(iter->first) != properties.end())
+ continue;
+
+ // If the additional property is of type "any"
+ // we can simply copy its value to the output
+ // object
+ if (additionalProperties->type == AnyValue)
+ {
+ outputValue[iter->first] = value[iter->first];
+ continue;
+ }
+
+ JSONRPC_STATUS status = additionalProperties->Check(value[iter->first], outputValue[iter->first], errorData["property"]);
+ if (status != OK)
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Invalid additional property \"{}\" in type {}",
+ iter->first, name);
+ return status;
+ }
+ }
+ }
+ // If we still have unchecked properties but additional
+ // properties are not allowed, we have invalid parameters
+ else if (!hasAdditionalProperties || additionalProperties == NULL)
+ {
+ errorData["message"] = "Unexpected additional properties received";
+ errorData.erase("property");
+ return InvalidParams;
+ }
+ }
+
+ return OK;
+ }
+
+ // It's neither an array nor an object
+
+ // If it can only take certain values ("enum")
+ // we need to check against those
+ if (enums.size() > 0)
+ {
+ bool valid = false;
+ for (const auto& enumItr : enums)
+ {
+ if (enumItr == value)
+ {
+ valid = true;
+ break;
+ }
+ }
+
+ if (!valid)
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Value does not match any of the enum values in type {}", name);
+ errorData["message"] = "Received value does not match any of the defined enum values";
+ return InvalidParams;
+ }
+ }
+
+ // If we have a number or an integer type, we need
+ // to check the minimum and maximum values
+ if ((HasType(type, NumberValue) && value.isDouble()) || (HasType(type, IntegerValue) && value.isInteger()))
+ {
+ double numberValue;
+ if (value.isDouble())
+ numberValue = value.asDouble();
+ else
+ numberValue = (double)value.asInteger();
+ // Check minimum
+ if ((exclusiveMinimum && numberValue <= minimum) || (!exclusiveMinimum && numberValue < minimum) ||
+ // Check maximum
+ (exclusiveMaximum && numberValue >= maximum) || (!exclusiveMaximum && numberValue > maximum))
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Value does not lay between minimum and maximum in type {}",
+ name);
+ if (value.isDouble())
+ errorMessage =
+ StringUtils::Format("Value between {:f} ({}) and {:f} ({}) expected but {:f} received",
+ minimum, exclusiveMinimum ? "exclusive" : "inclusive", maximum,
+ exclusiveMaximum ? "exclusive" : "inclusive", numberValue);
+ else
+ errorMessage = StringUtils::Format(
+ "Value between {} ({}) and {} ({}) expected but {} received", (int)minimum,
+ exclusiveMinimum ? "exclusive" : "inclusive", (int)maximum,
+ exclusiveMaximum ? "exclusive" : "inclusive", (int)numberValue);
+ errorData["message"] = errorMessage.c_str();
+ return InvalidParams;
+ }
+ // Check divisibleBy
+ if ((HasType(type, IntegerValue) && divisibleBy > 0 && ((int)numberValue % divisibleBy) != 0))
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Value does not meet divisibleBy requirements in type {}", name);
+ errorMessage = StringUtils::Format("Value should be divisible by {} but {} received",
+ divisibleBy, (int)numberValue);
+ errorData["message"] = errorMessage.c_str();
+ return InvalidParams;
+ }
+ }
+
+ // If we have a string, we need to check the length
+ if (HasType(type, StringValue) && value.isString())
+ {
+ int size = static_cast<int>(value.asString().size());
+ if (size < minLength)
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Value does not meet minLength requirements in type {}", name);
+ errorMessage = StringUtils::Format(
+ "Value should have a minimum length of {} but has a length of {}", minLength, size);
+ errorData["message"] = errorMessage.c_str();
+ return InvalidParams;
+ }
+
+ if (maxLength >= 0 && size > maxLength)
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Value does not meet maxLength requirements in type {}", name);
+ errorMessage = StringUtils::Format(
+ "Value should have a maximum length of {} but has a length of {}", maxLength, size);
+ errorData["message"] = errorMessage.c_str();
+ return InvalidParams;
+ }
+ }
+
+ // Otherwise it can have any value
+ outputValue = value;
+ return OK;
+}
+
+void JSONSchemaTypeDefinition::Print(bool isParameter, bool isGlobal, bool printDefault, bool printDescriptions, CVariant &output) const
+{
+ bool typeReference = false;
+
+ // Printing general fields
+ if (isParameter)
+ output["name"] = name;
+
+ if (isGlobal)
+ output["id"] = ID;
+ else if (!ID.empty())
+ {
+ output["$ref"] = ID;
+ typeReference = true;
+ }
+
+ if (printDescriptions && !description.empty())
+ output["description"] = description;
+
+ if (isParameter || printDefault)
+ {
+ if (!optional)
+ output["required"] = true;
+ if (optional && type != ObjectValue && type != ArrayValue)
+ output["default"] = defaultValue;
+ }
+
+ if (!typeReference)
+ {
+ if (extends.size() == 1)
+ {
+ output["extends"] = extends.at(0)->ID;
+ }
+ else if (extends.size() > 1)
+ {
+ output["extends"] = CVariant(CVariant::VariantTypeArray);
+ for (unsigned int extendsIndex = 0; extendsIndex < extends.size(); extendsIndex++)
+ output["extends"].append(extends.at(extendsIndex)->ID);
+ }
+ else if (unionTypes.size() > 0)
+ {
+ output["type"] = CVariant(CVariant::VariantTypeArray);
+ for (unsigned int unionIndex = 0; unionIndex < unionTypes.size(); unionIndex++)
+ {
+ CVariant unionOutput = CVariant(CVariant::VariantTypeObject);
+ unionTypes.at(unionIndex)->Print(false, false, false, printDescriptions, unionOutput);
+ output["type"].append(unionOutput);
+ }
+ }
+ else
+ CJSONUtils::SchemaValueTypeToJson(type, output["type"]);
+
+ // Printing enum field
+ if (enums.size() > 0)
+ {
+ output["enums"] = CVariant(CVariant::VariantTypeArray);
+ for (unsigned int enumIndex = 0; enumIndex < enums.size(); enumIndex++)
+ output["enums"].append(enums.at(enumIndex));
+ }
+
+ // Printing integer/number fields
+ if (CJSONUtils::HasType(type, IntegerValue) || CJSONUtils::HasType(type, NumberValue))
+ {
+ if (CJSONUtils::HasType(type, NumberValue))
+ {
+ if (minimum > -std::numeric_limits<double>::max())
+ output["minimum"] = minimum;
+ if (maximum < std::numeric_limits<double>::max())
+ output["maximum"] = maximum;
+ }
+ else
+ {
+ if (minimum > std::numeric_limits<int>::min())
+ output["minimum"] = (int)minimum;
+ if (maximum < std::numeric_limits<int>::max())
+ output["maximum"] = (int)maximum;
+ }
+
+ if (exclusiveMinimum)
+ output["exclusiveMinimum"] = true;
+ if (exclusiveMaximum)
+ output["exclusiveMaximum"] = true;
+ if (divisibleBy > 0)
+ output["divisibleBy"] = divisibleBy;
+ }
+ if (CJSONUtils::HasType(type, StringValue))
+ {
+ if (minLength >= 0)
+ output["minLength"] = minLength;
+ if (maxLength >= 0)
+ output["maxLength"] = maxLength;
+ }
+
+ // Print array fields
+ if (CJSONUtils::HasType(type, ArrayValue))
+ {
+ if (items.size() == 1)
+ {
+ items.at(0)->Print(false, false, false, printDescriptions, output["items"]);
+ }
+ else if (items.size() > 1)
+ {
+ output["items"] = CVariant(CVariant::VariantTypeArray);
+ for (unsigned int itemIndex = 0; itemIndex < items.size(); itemIndex++)
+ {
+ CVariant item = CVariant(CVariant::VariantTypeObject);
+ items.at(itemIndex)->Print(false, false, false, printDescriptions, item);
+ output["items"].append(item);
+ }
+ }
+
+ if (minItems > 0)
+ output["minItems"] = minItems;
+ if (maxItems > 0)
+ output["maxItems"] = maxItems;
+
+ if (additionalItems.size() == 1)
+ {
+ additionalItems.at(0)->Print(false, false, false, printDescriptions, output["additionalItems"]);
+ }
+ else if (additionalItems.size() > 1)
+ {
+ output["additionalItems"] = CVariant(CVariant::VariantTypeArray);
+ for (unsigned int addItemIndex = 0; addItemIndex < additionalItems.size(); addItemIndex++)
+ {
+ CVariant item = CVariant(CVariant::VariantTypeObject);
+ additionalItems.at(addItemIndex)->Print(false, false, false, printDescriptions, item);
+ output["additionalItems"].append(item);
+ }
+ }
+
+ if (uniqueItems)
+ output["uniqueItems"] = true;
+ }
+
+ // Print object fields
+ if (CJSONUtils::HasType(type, ObjectValue))
+ {
+ if (properties.size() > 0)
+ {
+ output["properties"] = CVariant(CVariant::VariantTypeObject);
+
+ JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::JSONSchemaPropertiesIterator propertiesEnd = properties.end();
+ JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::JSONSchemaPropertiesIterator propertiesIterator;
+ for (propertiesIterator = properties.begin(); propertiesIterator != propertiesEnd; ++propertiesIterator)
+ {
+ propertiesIterator->second->Print(false, false, true, printDescriptions, output["properties"][propertiesIterator->first]);
+ }
+ }
+
+ if (!hasAdditionalProperties)
+ output["additionalProperties"] = false;
+ else if (additionalProperties != NULL && additionalProperties->type != AnyValue)
+ additionalProperties->Print(false, false, true, printDescriptions, output["additionalProperties"]);
+ }
+ }
+}
+
+void JSONSchemaTypeDefinition::ResolveReference()
+{
+ // Check and set the reference type before recursing
+ // to guard against cycles
+ if (referencedTypeSet)
+ return;
+
+ referencedTypeSet = true;
+
+ // Take care of all nested types
+ for (const auto& it : extends)
+ it->ResolveReference();
+ for (const auto& it : unionTypes)
+ it->ResolveReference();
+ for (const auto& it : items)
+ it->ResolveReference();
+ for (const auto& it : additionalItems)
+ it->ResolveReference();
+ for (const auto& it : properties)
+ it.second->ResolveReference();
+
+ if (additionalProperties)
+ additionalProperties->ResolveReference();
+
+ if (referencedType == nullptr)
+ return;
+
+ std::string origName = name;
+ std::string origDescription = description;
+ bool origOptional = optional;
+ CVariant origDefaultValue = defaultValue;
+ JSONSchemaTypeDefinitionPtr referencedTypeDef = referencedType;
+
+ // set all the values from the given type definition
+ *this = *referencedType;
+
+ // restore the original values
+ if (!origName.empty())
+ name = origName;
+
+ if (!origDescription.empty())
+ description = origDescription;
+
+ if (!origOptional)
+ optional = origOptional;
+
+ if (!origDefaultValue.isNull())
+ defaultValue = origDefaultValue;
+
+ if (referencedTypeDef.get() != NULL)
+ referencedType = referencedTypeDef;
+
+ // This will have been overwritten by the copy of the reference
+ // type so we need to set it again
+ referencedTypeSet = true;
+}
+
+JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::CJsonSchemaPropertiesMap() :
+ m_propertiesmap(std::map<std::string, JSONSchemaTypeDefinitionPtr>())
+{
+}
+
+void JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::add(
+ const JSONSchemaTypeDefinitionPtr& property)
+{
+ std::string name = property->name;
+ StringUtils::ToLower(name);
+ m_propertiesmap[name] = property;
+}
+
+JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::JSONSchemaPropertiesIterator JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::begin() const
+{
+ return m_propertiesmap.begin();
+}
+
+JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::JSONSchemaPropertiesIterator JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::find(const std::string& key) const
+{
+ return m_propertiesmap.find(key);
+}
+
+JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::JSONSchemaPropertiesIterator JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::end() const
+{
+ return m_propertiesmap.end();
+}
+
+unsigned int JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::size() const
+{
+ return static_cast<unsigned int>(m_propertiesmap.size());
+}
+
+JsonRpcMethod::JsonRpcMethod()
+ : missingReference(),
+ name(),
+ method(NULL),
+ description(),
+ parameters(),
+ returns(new JSONSchemaTypeDefinition())
+{ }
+
+bool JsonRpcMethod::Parse(const CVariant &value)
+{
+ // Parse XBMC specific information about the method
+ if (value.isMember("transport") && value["transport"].isArray())
+ {
+ int transport = 0;
+ for (unsigned int index = 0; index < value["transport"].size(); index++)
+ transport |= StringToTransportLayer(value["transport"][index].asString());
+
+ transportneed = (TransportLayerCapability)transport;
+ }
+ else
+ transportneed = StringToTransportLayer(value.isMember("transport") ? value["transport"].asString() : "");
+
+ if (value.isMember("permission") && value["permission"].isArray())
+ {
+ int permissions = 0;
+ for (unsigned int index = 0; index < value["permission"].size(); index++)
+ permissions |= StringToPermission(value["permission"][index].asString());
+
+ permission = (OperationPermission)permissions;
+ }
+ else
+ permission = StringToPermission(value.isMember("permission") ? value["permission"].asString() : "");
+
+ description = GetString(value["description"], "");
+
+ // Check whether there are parameters defined
+ if (value.isMember("params") && value["params"].isArray())
+ {
+ // Loop through all defined parameters
+ for (unsigned int paramIndex = 0; paramIndex < value["params"].size(); paramIndex++)
+ {
+ CVariant parameter = value["params"][paramIndex];
+ // If the parameter definition does not contain a valid "name" or
+ // "type" element we will ignore it
+ if (!parameter.isMember("name") || !parameter["name"].isString() ||
+ (!parameter.isMember("type") && !parameter.isMember("$ref") && !parameter.isMember("extends")) ||
+ (parameter.isMember("type") && !parameter["type"].isString() && !parameter["type"].isArray()) ||
+ (parameter.isMember("$ref") && !parameter["$ref"].isString()) ||
+ (parameter.isMember("extends") && !parameter["extends"].isString() && !parameter["extends"].isArray()))
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC: Method {} has a badly defined parameter", name);
+ return false;
+ }
+
+ // Parse the parameter and add it to the list
+ // of defined parameters
+ JSONSchemaTypeDefinitionPtr param = JSONSchemaTypeDefinitionPtr(new JSONSchemaTypeDefinition());
+ if (!parseParameter(parameter, param))
+ {
+ missingReference = param->missingReference;
+ return false;
+ }
+ parameters.push_back(param);
+ }
+ }
+
+ // Parse the return value of the method
+ if (!parseReturn(value))
+ {
+ missingReference = returns->missingReference;
+ return false;
+ }
+
+ return true;
+}
+
+JSONRPC_STATUS JsonRpcMethod::Check(const CVariant &requestParameters, ITransportLayer *transport, IClient *client, bool notification, MethodCall &methodCall, CVariant &outputParameters) const
+{
+ if (transport != NULL && (transport->GetCapabilities() & transportneed) == transportneed)
+ {
+ if (client != NULL && (client->GetPermissionFlags() & permission) == permission && (!notification || (permission & OPERATION_PERMISSION_NOTIFICATION) == permission))
+ {
+ methodCall = method;
+
+ // Count the number of actually handled (present)
+ // parameters
+ unsigned int handled = 0;
+ CVariant errorData = CVariant(CVariant::VariantTypeObject);
+ errorData["method"] = name;
+
+ // Loop through all the parameters to check
+ for (unsigned int i = 0; i < parameters.size(); i++)
+ {
+ // Evaluate the current parameter
+ JSONRPC_STATUS status = checkParameter(requestParameters, parameters.at(i), i, outputParameters, handled, errorData);
+ if (status != OK)
+ {
+ // Return the error data object in the outputParameters reference
+ outputParameters = errorData;
+ return status;
+ }
+ }
+
+ // Check if there were unnecessary parameters
+ if (handled < requestParameters.size())
+ {
+ errorData["message"] = "Too many parameters";
+ outputParameters = errorData;
+ return InvalidParams;
+ }
+
+ return OK;
+ }
+ else
+ return BadPermission;
+ }
+
+ return MethodNotFound;
+}
+
+bool JsonRpcMethod::parseParameter(const CVariant& value,
+ const JSONSchemaTypeDefinitionPtr& parameter)
+{
+ parameter->name = GetString(value["name"], "");
+
+ // Parse the type and default value of the parameter
+ return parameter->Parse(value, true);
+}
+
+bool JsonRpcMethod::parseReturn(const CVariant &value)
+{
+ // Only parse the "returns" definition if there is one
+ if (!value.isMember("returns"))
+ {
+ returns->type = NullValue;
+ return true;
+ }
+
+ // If the type of the return value is defined as a simple string we can parse it directly
+ if (value["returns"].isString())
+ return CJSONServiceDescription::parseJSONSchemaType(value["returns"], returns->unionTypes, returns->type, missingReference);
+
+ // otherwise we have to parse the whole type definition
+ if (!returns->Parse(value["returns"]))
+ {
+ missingReference = returns->missingReference;
+ return false;
+ }
+
+ return true;
+}
+
+JSONRPC_STATUS JsonRpcMethod::checkParameter(const CVariant& requestParameters,
+ const JSONSchemaTypeDefinitionPtr& type,
+ unsigned int position,
+ CVariant& outputParameters,
+ unsigned int& handled,
+ CVariant& errorData)
+{
+ // Let's check if the parameter has been provided
+ if (ParameterExists(requestParameters, type->name, position))
+ {
+ // Get the parameter
+ CVariant parameterValue = GetParameter(requestParameters, type->name, position);
+
+ // Evaluate the type of the parameter
+ JSONRPC_STATUS status = type->Check(parameterValue, outputParameters[type->name], errorData["stack"]);
+ if (status != OK)
+ return status;
+
+ // The parameter was present and valid
+ handled++;
+ }
+ // If the parameter has not been provided but is optional
+ // we can use its default value
+ else if (type->optional)
+ outputParameters[type->name] = type->defaultValue;
+ // The parameter is required but has not been provided => invalid
+ else
+ {
+ errorData["stack"]["name"] = type->name;
+ SchemaValueTypeToJson(type->type, errorData["stack"]["type"]);
+ errorData["stack"]["message"] = "Missing parameter";
+ return InvalidParams;
+ }
+
+ return OK;
+}
+
+void CJSONServiceDescription::ResolveReferences()
+{
+ for (const auto& it : m_types)
+ it.second->ResolveReference();
+}
+
+void CJSONServiceDescription::Cleanup()
+{
+ // reset all of the static data
+ m_notifications.clear();
+ m_actionMap.clear();
+ m_types.clear();
+ m_incompleteDefinitions.clear();
+}
+
+bool CJSONServiceDescription::prepareDescription(std::string &description, CVariant &descriptionObject, std::string &name)
+{
+ if (description.empty())
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Missing JSON Schema definition for \"{}\"", name);
+ return false;
+ }
+
+ if (description.at(0) != '{')
+ description = StringUtils::Format("{{{:s}}}", description);
+
+ // Make sure the method description actually exists and represents an object
+ if (!CJSONVariantParser::Parse(description, descriptionObject) || !descriptionObject.isObject())
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Unable to parse JSON Schema definition for \"{}\"", name);
+ return false;
+ }
+
+ CVariant::const_iterator_map member = descriptionObject.begin_map();
+ if (member != descriptionObject.end_map())
+ name = member->first;
+
+ if (name.empty() ||
+ (!descriptionObject[name].isMember("type") && !descriptionObject[name].isMember("$ref") && !descriptionObject[name].isMember("extends")))
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Invalid JSON Schema definition for \"{}\"", name);
+ return false;
+ }
+
+ return true;
+}
+
+bool CJSONServiceDescription::addMethod(const std::string &jsonMethod, MethodCall method)
+{
+ CVariant descriptionObject;
+ std::string methodName;
+
+ std::string modJsonMethod = jsonMethod;
+ // Make sure the method description actually exists and represents an object
+ if (!prepareDescription(modJsonMethod, descriptionObject, methodName))
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Invalid JSON Schema definition for method \"{}\"", methodName);
+ return false;
+ }
+
+ if (m_actionMap.find(methodName) != m_actionMap.end())
+ {
+ CLog::Log(LOGERROR, "JSONRPC: There already is a method with the name \"{}\"", methodName);
+ return false;
+ }
+
+ std::string type = GetString(descriptionObject[methodName]["type"], "");
+ if (type.compare("method") != 0)
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Invalid JSON type for method \"{}\"", methodName);
+ return false;
+ }
+
+ if (method == NULL)
+ {
+ unsigned int size = sizeof(m_methodMaps) / sizeof(JsonRpcMethodMap);
+ for (unsigned int index = 0; index < size; index++)
+ {
+ if (methodName.compare(m_methodMaps[index].name) == 0)
+ {
+ method = m_methodMaps[index].method;
+ break;
+ }
+ }
+
+ if (method == NULL)
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Missing implementation for method \"{}\"", methodName);
+ return false;
+ }
+ }
+
+ // Parse the details of the method
+ JsonRpcMethod newMethod;
+ newMethod.name = methodName;
+ newMethod.method = method;
+
+ if (!newMethod.Parse(descriptionObject[newMethod.name]))
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Could not parse method \"{}\"", methodName);
+ if (!newMethod.missingReference.empty())
+ {
+ IncompleteSchemaDefinition incomplete;
+ incomplete.Schema = modJsonMethod;
+ incomplete.Type = SchemaDefinitionMethod;
+ incomplete.Method = method;
+
+ IncompleteSchemaDefinitionMap::iterator iter = m_incompleteDefinitions.find(newMethod.missingReference);
+ if (iter == m_incompleteDefinitions.end())
+ m_incompleteDefinitions[newMethod.missingReference] = std::vector<IncompleteSchemaDefinition>();
+
+ CLog::Log(
+ LOGINFO,
+ "JSONRPC: Adding method \"{}\" to list of incomplete definitions (waiting for \"{}\")",
+ methodName, newMethod.missingReference);
+ m_incompleteDefinitions[newMethod.missingReference].push_back(incomplete);
+ }
+
+ return false;
+ }
+
+ m_actionMap.add(newMethod);
+
+ return true;
+}
+
+bool CJSONServiceDescription::AddType(const std::string &jsonType)
+{
+ CVariant descriptionObject;
+ std::string typeName;
+
+ std::string modJsonType = jsonType;
+ if (!prepareDescription(modJsonType, descriptionObject, typeName))
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Invalid JSON Schema definition for type \"{}\"", typeName);
+ return false;
+ }
+
+ if (m_types.find(typeName) != m_types.end())
+ {
+ CLog::Log(LOGERROR, "JSONRPC: There already is a type with the name \"{}\"", typeName);
+ return false;
+ }
+
+ // Make sure the "id" attribute is correctly populated
+ descriptionObject[typeName]["id"] = typeName;
+
+ JSONSchemaTypeDefinitionPtr globalType = JSONSchemaTypeDefinitionPtr(new JSONSchemaTypeDefinition());
+ globalType->name = typeName;
+ globalType->ID = typeName;
+ CJSONServiceDescription::addReferenceTypeDefinition(globalType);
+
+ if (!globalType->Parse(descriptionObject[typeName]))
+ {
+ CLog::Log(LOGWARNING, "JSONRPC: Could not parse type \"{}\"", typeName);
+ CJSONServiceDescription::removeReferenceTypeDefinition(typeName);
+ if (!globalType->missingReference.empty())
+ {
+ IncompleteSchemaDefinition incomplete;
+ incomplete.Schema = modJsonType;
+ incomplete.Type = SchemaDefinitionType;
+
+ IncompleteSchemaDefinitionMap::iterator iter = m_incompleteDefinitions.find(globalType->missingReference);
+ if (iter == m_incompleteDefinitions.end())
+ m_incompleteDefinitions[globalType->missingReference] = std::vector<IncompleteSchemaDefinition>();
+
+ CLog::Log(
+ LOGINFO,
+ "JSONRPC: Adding type \"{}\" to list of incomplete definitions (waiting for \"{}\")",
+ typeName, globalType->missingReference);
+ m_incompleteDefinitions[globalType->missingReference].push_back(incomplete);
+ }
+
+ globalType.reset();
+
+ return false;
+ }
+
+ return true;
+}
+
+bool CJSONServiceDescription::AddMethod(const std::string &jsonMethod, MethodCall method)
+{
+ if (method == NULL)
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Invalid JSONRPC method implementation");
+ return false;
+ }
+
+ return addMethod(jsonMethod, method);
+}
+
+bool CJSONServiceDescription::AddBuiltinMethod(const std::string &jsonMethod)
+{
+ return addMethod(jsonMethod, NULL);
+}
+
+bool CJSONServiceDescription::AddNotification(const std::string &jsonNotification)
+{
+ CVariant descriptionObject;
+ std::string notificationName;
+
+ std::string modJsonNotification = jsonNotification;
+ // Make sure the notification description actually exists and represents an object
+ if (!prepareDescription(modJsonNotification, descriptionObject, notificationName))
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Invalid JSON Schema definition for notification \"{}\"",
+ notificationName);
+ return false;
+ }
+
+ if (m_notifications.find(notificationName) != m_notifications.end())
+ {
+ CLog::Log(LOGERROR, "JSONRPC: There already is a notification with the name \"{}\"",
+ notificationName);
+ return false;
+ }
+
+ std::string type = GetString(descriptionObject[notificationName]["type"], "");
+ if (type.compare("notification") != 0)
+ {
+ CLog::Log(LOGERROR, "JSONRPC: Invalid JSON type for notification \"{}\"", notificationName);
+ return false;
+ }
+
+ m_notifications[notificationName] = descriptionObject;
+
+ return true;
+}
+
+bool CJSONServiceDescription::AddEnum(const std::string &name, const std::vector<CVariant> &values, CVariant::VariantType type /* = CVariant::VariantTypeNull */, const CVariant &defaultValue /* = CVariant::ConstNullVariant */)
+{
+ if (name.empty() || m_types.find(name) != m_types.end() ||
+ values.size() == 0)
+ return false;
+
+ JSONSchemaTypeDefinitionPtr definition = JSONSchemaTypeDefinitionPtr(new JSONSchemaTypeDefinition());
+ definition->ID = name;
+
+ std::vector<CVariant::VariantType> types;
+ bool autoType = false;
+ if (type == CVariant::VariantTypeNull)
+ autoType = true;
+ else
+ types.push_back(type);
+
+ for (unsigned int index = 0; index < values.size(); index++)
+ {
+ if (autoType)
+ types.push_back(values[index].type());
+ else if (type != CVariant::VariantTypeConstNull && type != values[index].type())
+ return false;
+ }
+ definition->enums.insert(definition->enums.begin(), values.begin(), values.end());
+
+ int schemaType = (int)AnyValue;
+ for (unsigned int index = 0; index < types.size(); index++)
+ {
+ JSONSchemaType currentType;
+ switch (type)
+ {
+ case CVariant::VariantTypeString:
+ currentType = StringValue;
+ break;
+ case CVariant::VariantTypeDouble:
+ currentType = NumberValue;
+ break;
+ case CVariant::VariantTypeInteger:
+ case CVariant::VariantTypeUnsignedInteger:
+ currentType = IntegerValue;
+ break;
+ case CVariant::VariantTypeBoolean:
+ currentType = BooleanValue;
+ break;
+ case CVariant::VariantTypeArray:
+ currentType = ArrayValue;
+ break;
+ case CVariant::VariantTypeObject:
+ currentType = ObjectValue;
+ break;
+ case CVariant::VariantTypeConstNull:
+ currentType = AnyValue;
+ break;
+ default:
+ case CVariant::VariantTypeNull:
+ return false;
+ }
+
+ if (index == 0)
+ schemaType = currentType;
+ else
+ schemaType |= (int)currentType;
+ }
+ definition->type = (JSONSchemaType)schemaType;
+
+ if (defaultValue.type() == CVariant::VariantTypeConstNull)
+ definition->defaultValue = definition->enums.at(0);
+ else
+ definition->defaultValue = defaultValue;
+
+ addReferenceTypeDefinition(definition);
+
+ return true;
+}
+
+bool CJSONServiceDescription::AddEnum(const std::string &name, const std::vector<std::string> &values)
+{
+ std::vector<CVariant> enums;
+ enums.reserve(values.size());
+ for (const auto& it : values)
+ enums.emplace_back(it);
+
+ return AddEnum(name, enums, CVariant::VariantTypeString);
+}
+
+bool CJSONServiceDescription::AddEnum(const std::string &name, const std::vector<int> &values)
+{
+ std::vector<CVariant> enums;
+ enums.reserve(values.size());
+ for (const auto& it : values)
+ enums.emplace_back(it);
+
+ return AddEnum(name, enums, CVariant::VariantTypeInteger);
+}
+
+const char* CJSONServiceDescription::GetVersion()
+{
+ return JSONRPC_SERVICE_VERSION;
+}
+
+JSONRPC_STATUS CJSONServiceDescription::Print(CVariant &result, ITransportLayer *transport, IClient *client,
+ bool printDescriptions /* = true */, bool printMetadata /* = false */, bool filterByTransport /* = true */,
+ const std::string &filterByName /* = "" */, const std::string &filterByType /* = "" */, bool printReferences /* = true */)
+{
+ std::map<std::string, JSONSchemaTypeDefinitionPtr> types;
+ CJsonRpcMethodMap methods;
+ std::map<std::string, CVariant> notifications;
+
+ int clientPermissions = client->GetPermissionFlags();
+ int transportCapabilities = transport->GetCapabilities();
+
+ if (filterByName.size() > 0)
+ {
+ std::string name = filterByName;
+
+ if (filterByType == "method")
+ {
+ StringUtils::ToLower(name);
+
+ CJsonRpcMethodMap::JsonRpcMethodIterator methodIterator = m_actionMap.find(name);
+ if (methodIterator != m_actionMap.end() &&
+ (clientPermissions & methodIterator->second.permission) == methodIterator->second.permission && ((transportCapabilities & methodIterator->second.transportneed) == methodIterator->second.transportneed || !filterByTransport))
+ methods.add(methodIterator->second);
+ else
+ return InvalidParams;
+ }
+ else if (filterByType == "namespace")
+ {
+ // append a . delimiter to make sure we check for a namespace
+ StringUtils::ToLower(name);
+ name.append(".");
+
+ CJsonRpcMethodMap::JsonRpcMethodIterator methodIterator;
+ CJsonRpcMethodMap::JsonRpcMethodIterator methodIteratorEnd = m_actionMap.end();
+ for (methodIterator = m_actionMap.begin(); methodIterator != methodIteratorEnd; methodIterator++)
+ {
+ // Check if the given name is at the very beginning of the method name
+ if (methodIterator->first.find(name) == 0 &&
+ (clientPermissions & methodIterator->second.permission) == methodIterator->second.permission && ((transportCapabilities & methodIterator->second.transportneed) == methodIterator->second.transportneed || !filterByTransport))
+ methods.add(methodIterator->second);
+ }
+
+ if (methods.begin() == methods.end())
+ return InvalidParams;
+ }
+ else if (filterByType == "type")
+ {
+ std::map<std::string, JSONSchemaTypeDefinitionPtr>::const_iterator typeIterator = m_types.find(name);
+ if (typeIterator != m_types.end())
+ types[typeIterator->first] = typeIterator->second;
+ else
+ return InvalidParams;
+ }
+ else if (filterByType == "notification")
+ {
+ std::map<std::string, CVariant>::const_iterator notificationIterator = m_notifications.find(name);
+ if (notificationIterator != m_notifications.end())
+ notifications[notificationIterator->first] = notificationIterator->second;
+ else
+ return InvalidParams;
+ }
+ else
+ return InvalidParams;
+
+ // If we need to print all referenced types we have to go through all parameters etc
+ if (printReferences)
+ {
+ std::vector<std::string> referencedTypes;
+
+ // Loop through all printed types to get all referenced types
+ std::map<std::string, JSONSchemaTypeDefinitionPtr>::const_iterator typeIterator;
+ std::map<std::string, JSONSchemaTypeDefinitionPtr>::const_iterator typeIteratorEnd = types.end();
+ for (typeIterator = types.begin(); typeIterator != typeIteratorEnd; ++typeIterator)
+ getReferencedTypes(typeIterator->second, referencedTypes);
+
+ // Loop through all printed method's parameters and return value to get all referenced types
+ CJsonRpcMethodMap::JsonRpcMethodIterator methodIterator;
+ CJsonRpcMethodMap::JsonRpcMethodIterator methodIteratorEnd = methods.end();
+ for (methodIterator = methods.begin(); methodIterator != methodIteratorEnd; methodIterator++)
+ {
+ for (unsigned int index = 0; index < methodIterator->second.parameters.size(); index++)
+ getReferencedTypes(methodIterator->second.parameters.at(index), referencedTypes);
+
+ getReferencedTypes(methodIterator->second.returns, referencedTypes);
+ }
+
+ for (unsigned int index = 0; index < referencedTypes.size(); index++)
+ {
+ std::map<std::string, JSONSchemaTypeDefinitionPtr>::const_iterator typeIterator = m_types.find(referencedTypes.at(index));
+ if (typeIterator != m_types.end())
+ types[typeIterator->first] = typeIterator->second;
+ }
+ }
+ }
+ else
+ {
+ types = m_types;
+ methods = m_actionMap;
+ notifications = m_notifications;
+ }
+
+ // Print the header
+ result["id"] = JSONRPC_SERVICE_ID;
+ result["version"] = JSONRPC_SERVICE_VERSION;
+ result["description"] = JSONRPC_SERVICE_DESCRIPTION;
+
+ std::map<std::string, JSONSchemaTypeDefinitionPtr>::const_iterator typeIterator;
+ std::map<std::string, JSONSchemaTypeDefinitionPtr>::const_iterator typeIteratorEnd = types.end();
+ for (typeIterator = types.begin(); typeIterator != typeIteratorEnd; ++typeIterator)
+ {
+ CVariant currentType = CVariant(CVariant::VariantTypeObject);
+ typeIterator->second->Print(false, true, true, printDescriptions, currentType);
+
+ result["types"][typeIterator->first] = currentType;
+ }
+
+ // Iterate through all json rpc methods
+ CJsonRpcMethodMap::JsonRpcMethodIterator methodIterator;
+ CJsonRpcMethodMap::JsonRpcMethodIterator methodIteratorEnd = methods.end();
+ for (methodIterator = methods.begin(); methodIterator != methodIteratorEnd; methodIterator++)
+ {
+ if ((clientPermissions & methodIterator->second.permission) != methodIterator->second.permission || ((transportCapabilities & methodIterator->second.transportneed) != methodIterator->second.transportneed && filterByTransport))
+ continue;
+
+ CVariant currentMethod = CVariant(CVariant::VariantTypeObject);
+
+ currentMethod["type"] = "method";
+ if (printDescriptions && !methodIterator->second.description.empty())
+ currentMethod["description"] = methodIterator->second.description;
+ if (printMetadata)
+ {
+ CVariant permissions(CVariant::VariantTypeArray);
+ for (int i = ReadData; i <= OPERATION_PERMISSION_ALL; i *= 2)
+ {
+ if ((methodIterator->second.permission & i) == i)
+ permissions.push_back(PermissionToString((OperationPermission)i));
+ }
+
+ if (permissions.size() == 1)
+ currentMethod["permission"] = permissions[0];
+ else
+ currentMethod["permission"] = permissions;
+ }
+
+ currentMethod["params"] = CVariant(CVariant::VariantTypeArray);
+ for (unsigned int paramIndex = 0; paramIndex < methodIterator->second.parameters.size(); paramIndex++)
+ {
+ CVariant param = CVariant(CVariant::VariantTypeObject);
+ methodIterator->second.parameters.at(paramIndex)->Print(true, false, true, printDescriptions, param);
+ currentMethod["params"].append(param);
+ }
+
+ methodIterator->second.returns->Print(false, false, false, printDescriptions, currentMethod["returns"]);
+
+ result["methods"][methodIterator->second.name] = currentMethod;
+ }
+
+ // Print notification description
+ std::map<std::string, CVariant>::const_iterator notificationIterator;
+ std::map<std::string, CVariant>::const_iterator notificationIteratorEnd = notifications.end();
+ for (notificationIterator = notifications.begin(); notificationIterator != notificationIteratorEnd; ++notificationIterator)
+ result["notifications"][notificationIterator->first] = notificationIterator->second[notificationIterator->first];
+
+ return OK;
+}
+
+JSONRPC_STATUS CJSONServiceDescription::CheckCall(const char* const method, const CVariant &requestParameters, ITransportLayer *transport, IClient *client, bool notification, MethodCall &methodCall, CVariant &outputParameters)
+{
+ CJsonRpcMethodMap::JsonRpcMethodIterator iter = m_actionMap.find(method);
+ if (iter != m_actionMap.end())
+ return iter->second.Check(requestParameters, transport, client, notification, methodCall, outputParameters);
+
+ return MethodNotFound;
+}
+
+JSONSchemaTypeDefinitionPtr CJSONServiceDescription::GetType(const std::string &identification)
+{
+ std::map<std::string, JSONSchemaTypeDefinitionPtr>::iterator iter = m_types.find(identification);
+ if (iter == m_types.end())
+ return JSONSchemaTypeDefinitionPtr();
+
+ return iter->second;
+}
+
+bool CJSONServiceDescription::parseJSONSchemaType(const CVariant &value, std::vector<JSONSchemaTypeDefinitionPtr>& typeDefinitions, JSONSchemaType &schemaType, std::string &missingReference)
+{
+ missingReference.clear();
+ schemaType = AnyValue;
+
+ if (value.isArray())
+ {
+ int parsedType = 0;
+ // If the defined type is an array, we have
+ // to handle a union type
+ for (unsigned int typeIndex = 0; typeIndex < value.size(); typeIndex++)
+ {
+ JSONSchemaTypeDefinitionPtr definition = JSONSchemaTypeDefinitionPtr(new JSONSchemaTypeDefinition());
+ // If the type is a string try to parse it
+ if (value[typeIndex].isString())
+ definition->type = StringToSchemaValueType(value[typeIndex].asString());
+ else if (value[typeIndex].isObject())
+ {
+ if (!definition->Parse(value[typeIndex]))
+ {
+ missingReference = definition->missingReference;
+ CLog::Log(LOGERROR, "JSONRPC: Invalid type schema in union type definition");
+ return false;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "JSONRPC: Invalid type in union type definition");
+ return false;
+ }
+
+ definition->optional = false;
+ typeDefinitions.push_back(definition);
+ parsedType |= definition->type;
+ }
+
+ // If the type has not been set yet set it to "any"
+ if (parsedType != 0)
+ schemaType = (JSONSchemaType)parsedType;
+
+ return true;
+ }
+
+ if (value.isString())
+ {
+ schemaType = StringToSchemaValueType(value.asString());
+ return true;
+ }
+
+ return false;
+}
+
+void CJSONServiceDescription::addReferenceTypeDefinition(
+ const JSONSchemaTypeDefinitionPtr& typeDefinition)
+{
+ // If the given json value is no object or does not contain an "id" field
+ // of type string it is no valid type definition
+ if (typeDefinition->ID.empty())
+ return;
+
+ // If the id has already been defined we ignore the type definition
+ if (m_types.find(typeDefinition->ID) != m_types.end())
+ return;
+
+ // Add the type to the list of type definitions
+ m_types[typeDefinition->ID] = typeDefinition;
+
+ IncompleteSchemaDefinitionMap::iterator iter = m_incompleteDefinitions.find(typeDefinition->ID);
+ if (iter == m_incompleteDefinitions.end())
+ return;
+
+ CLog::Log(LOGINFO, "JSONRPC: Resolving incomplete types/methods referencing {}",
+ typeDefinition->ID);
+ for (unsigned int index = 0; index < iter->second.size(); index++)
+ {
+ if (iter->second[index].Type == SchemaDefinitionType)
+ AddType(iter->second[index].Schema);
+ else
+ AddMethod(iter->second[index].Schema, iter->second[index].Method);
+ }
+
+ m_incompleteDefinitions.erase(typeDefinition->ID);
+}
+
+void CJSONServiceDescription::removeReferenceTypeDefinition(const std::string &typeID)
+{
+ if (typeID.empty())
+ return;
+
+ std::map<std::string, JSONSchemaTypeDefinitionPtr>::iterator type = m_types.find(typeID);
+ if (type != m_types.end())
+ m_types.erase(type);
+}
+
+void CJSONServiceDescription::getReferencedTypes(const JSONSchemaTypeDefinitionPtr& type,
+ std::vector<std::string>& referencedTypes)
+{
+ // If the current type is a referenceable object, we can add it to the list
+ if (type->ID.size() > 0)
+ {
+ for (unsigned int index = 0; index < referencedTypes.size(); index++)
+ {
+ // The referenceable object has already been added to the list so we can just skip it
+ if (type->ID == referencedTypes.at(index))
+ return;
+ }
+
+ referencedTypes.push_back(type->ID);
+ }
+
+ // If the current type is an object we need to check its properties
+ if (HasType(type->type, ObjectValue))
+ {
+ JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::JSONSchemaPropertiesIterator iter;
+ JSONSchemaTypeDefinition::CJsonSchemaPropertiesMap::JSONSchemaPropertiesIterator iterEnd = type->properties.end();
+ for (iter = type->properties.begin(); iter != iterEnd; ++iter)
+ getReferencedTypes(iter->second, referencedTypes);
+ }
+ // If the current type is an array we need to check its items
+ if (HasType(type->type, ArrayValue))
+ {
+ unsigned int index;
+ for (index = 0; index < type->items.size(); index++)
+ getReferencedTypes(type->items.at(index), referencedTypes);
+
+ for (index = 0; index < type->additionalItems.size(); index++)
+ getReferencedTypes(type->additionalItems.at(index), referencedTypes);
+ }
+
+ // If the current type extends others type we need to check those types
+ for (unsigned int index = 0; index < type->extends.size(); index++)
+ getReferencedTypes(type->extends.at(index), referencedTypes);
+
+ // If the current type is a union type we need to check those types
+ for (unsigned int index = 0; index < type->unionTypes.size(); index++)
+ getReferencedTypes(type->unionTypes.at(index), referencedTypes);
+}
+
+CJSONServiceDescription::CJsonRpcMethodMap::CJsonRpcMethodMap():
+ m_actionmap(std::map<std::string, JsonRpcMethod>())
+{
+}
+
+void CJSONServiceDescription::CJsonRpcMethodMap::clear()
+{
+ m_actionmap.clear();
+}
+
+void CJSONServiceDescription::CJsonRpcMethodMap::add(const JsonRpcMethod &method)
+{
+ std::string name = method.name;
+ StringUtils::ToLower(name);
+ m_actionmap[name] = method;
+}
+
+CJSONServiceDescription::CJsonRpcMethodMap::JsonRpcMethodIterator CJSONServiceDescription::CJsonRpcMethodMap::begin() const
+{
+ return m_actionmap.begin();
+}
+
+CJSONServiceDescription::CJsonRpcMethodMap::JsonRpcMethodIterator CJSONServiceDescription::CJsonRpcMethodMap::find(const std::string& key) const
+{
+ return m_actionmap.find(key);
+}
+
+CJSONServiceDescription::CJsonRpcMethodMap::JsonRpcMethodIterator CJSONServiceDescription::CJsonRpcMethodMap::end() const
+{
+ return m_actionmap.end();
+}
diff --git a/xbmc/interfaces/json-rpc/JSONServiceDescription.h b/xbmc/interfaces/json-rpc/JSONServiceDescription.h
new file mode 100644
index 0000000..ee48920
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/JSONServiceDescription.h
@@ -0,0 +1,442 @@
+/*
+ * 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 "JSONUtils.h"
+#include "utils/Variant.h"
+
+#include <limits>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace JSONRPC
+{
+ class JSONSchemaTypeDefinition;
+ typedef std::shared_ptr<JSONSchemaTypeDefinition> JSONSchemaTypeDefinitionPtr;
+
+ /*!
+ \ingroup jsonrpc
+ \brief Class for a parameter of a
+ json rpc method.
+
+ Represents a parameter of a defined
+ json rpc method and is used to verify
+ and extract the value of the parameter
+ in a method call.
+ */
+ class JSONSchemaTypeDefinition : protected CJSONUtils
+ {
+ public:
+ JSONSchemaTypeDefinition();
+
+ bool Parse(const CVariant &value, bool isParameter = false);
+ JSONRPC_STATUS Check(const CVariant& value, CVariant& outputValue, CVariant& errorData) const;
+ void Print(bool isParameter, bool isGlobal, bool printDefault, bool printDescriptions, CVariant &output) const;
+ void ResolveReference();
+
+ std::string missingReference;
+
+ /*!
+ \brief Name of the parameter (for
+ by-name calls)
+ */
+ std::string name;
+
+ /*!
+ \brief Id of the type (for
+ referenced types)
+ Renamed from "id" because of possible
+ issues with Objective-C.
+ */
+ std::string ID;
+
+ /*!
+ \brief Referenced object
+ */
+ JSONSchemaTypeDefinitionPtr referencedType;
+
+ /*!
+ \brief Whether the type has been set
+ based on the referenced type
+ */
+ bool referencedTypeSet = false;
+
+ /*!
+ \brief Array of reference types
+ which are extended by this type.
+ */
+ std::vector<JSONSchemaTypeDefinitionPtr> extends;
+
+ /*!
+ \brief Description of the parameter
+ */
+ std::string description;
+
+ /*!
+ \brief JSON schema type of the parameter's value
+ */
+ JSONSchemaType type = AnyValue;
+
+ /*!
+ \brief JSON schema type definitions in case
+ of a union type
+ */
+ std::vector<JSONSchemaTypeDefinitionPtr> unionTypes;
+
+ /*!
+ \brief Whether or not the parameter is
+ optional
+ */
+ bool optional = true;
+
+ /*!
+ \brief Default value of the parameter
+ (only needed when it is optional)
+ */
+ CVariant defaultValue;
+
+ /*!
+ \brief Minimum value for Integer
+ or Number types
+ */
+ double minimum;
+
+ /*!
+ \brief Maximum value for Integer or Number types
+ */
+ double maximum;
+
+ /*!
+ \brief Whether to exclude the defined Minimum
+ value from the valid range or not
+ */
+ bool exclusiveMinimum = false;
+
+ /*!
+ \brief Whether to exclude the defined Maximum
+ value from the valid range or not
+ */
+ bool exclusiveMaximum = false;
+
+ /*!
+ \brief Integer by which the value (of type
+ Integer) must be divisible without rest
+ */
+ unsigned int divisibleBy = 0;
+
+ /*!
+ \brief Minimum length for String types
+ */
+ int minLength = -1;
+
+ /*!
+ \brief Maximum length for String types
+ */
+ int maxLength = -1;
+
+ /*!
+ \brief (Optional) List of allowed values
+ for the type
+ */
+ std::vector<CVariant> enums;
+
+ /*!
+ \brief List of possible values in an array
+ */
+ std::vector<JSONSchemaTypeDefinitionPtr> items;
+
+ /*!
+ \brief Minimum amount of items in the array
+ */
+ unsigned int minItems = 0;
+
+ /*!
+ \brief Maximum amount of items in the array
+ */
+ unsigned int maxItems = 0;
+
+ /*!
+ \brief Whether every value in the array
+ must be unique or not
+ */
+ bool uniqueItems = false;
+
+ /*!
+ \brief List of json schema definitions for
+ additional items in an array with tuple
+ typing (defined schemas in "items")
+ */
+ std::vector<JSONSchemaTypeDefinitionPtr> additionalItems;
+
+ /*!
+ \brief Maps a properties name to its
+ json schema type definition
+ */
+ class CJsonSchemaPropertiesMap
+ {
+ public:
+ CJsonSchemaPropertiesMap();
+
+ void add(const JSONSchemaTypeDefinitionPtr& property);
+
+ typedef std::map<std::string, JSONSchemaTypeDefinitionPtr>::const_iterator JSONSchemaPropertiesIterator;
+ JSONSchemaPropertiesIterator begin() const;
+ JSONSchemaPropertiesIterator find(const std::string& key) const;
+ JSONSchemaPropertiesIterator end() const;
+ unsigned int size() const;
+ private:
+ std::map<std::string, JSONSchemaTypeDefinitionPtr> m_propertiesmap;
+ };
+
+ /*!
+ \brief List of properties of the parameter (only needed when the
+ parameter is an object)
+ */
+ CJsonSchemaPropertiesMap properties;
+
+ /*!
+ \brief Whether the type can have additional properties
+ or not
+ */
+ bool hasAdditionalProperties = false;
+
+ /*!
+ \brief Type definition for additional properties
+ */
+ JSONSchemaTypeDefinitionPtr additionalProperties;
+ };
+
+ /*!
+ \ingroup jsonrpc
+ \brief Structure for a published json
+ rpc method.
+
+ Represents a published json rpc method
+ and is used to verify an incoming json
+ rpc request against a defined method.
+ */
+ class JsonRpcMethod : protected CJSONUtils
+ {
+ public:
+ JsonRpcMethod();
+
+ bool Parse(const CVariant &value);
+ JSONRPC_STATUS Check(const CVariant &requestParameters, ITransportLayer *transport, IClient *client, bool notification, MethodCall &methodCall, CVariant &outputParameters) const;
+
+ std::string missingReference;
+
+ /*!
+ \brief Name of the represented method
+ */
+ std::string name;
+ /*!
+ \brief Pointer tot he implementation
+ of the represented method
+ */
+ MethodCall method;
+ /*!
+ \brief Definition of the type of
+ request/response
+ */
+ TransportLayerCapability transportneed = Response;
+ /*!
+ \brief Definition of the permissions needed
+ to execute the method
+ */
+ OperationPermission permission = ReadData;
+ /*!
+ \brief Description of the method
+ */
+ std::string description;
+ /*!
+ \brief List of accepted parameters
+ */
+ std::vector<JSONSchemaTypeDefinitionPtr> parameters;
+ /*!
+ \brief Definition of the return value
+ */
+ JSONSchemaTypeDefinitionPtr returns;
+
+ private:
+ bool parseParameter(const CVariant& value, const JSONSchemaTypeDefinitionPtr& parameter);
+ bool parseReturn(const CVariant &value);
+ static JSONRPC_STATUS checkParameter(const CVariant& requestParameters,
+ const JSONSchemaTypeDefinitionPtr& type,
+ unsigned int position,
+ CVariant& outputParameters,
+ unsigned int& handled,
+ CVariant& errorData);
+ };
+
+ /*!
+ \ingroup jsonrpc
+ \brief Structure mapping a json rpc method
+ definition to an actual method implementation.
+ */
+ typedef struct
+ {
+ /*!
+ \brief Name of the json rpc method.
+ */
+ std::string name;
+ /*!
+ \brief Pointer to the actual
+ implementation of the json rpc
+ method.
+ */
+ MethodCall method;
+ } JsonRpcMethodMap;
+
+ /*!
+ \ingroup jsonrpc
+ \brief Helper class for json schema service descriptor based
+ service descriptions for the json rpc API
+
+ Provides static functions to parse a complete json schema
+ service descriptor of a published service containing json rpc
+ methods, print the json schema service descriptor representation
+ into a string (mainly for output purposes) and evaluate and verify
+ parameters provided in a call to one of the publish json rpc methods
+ against a parameter definition parsed from a json schema service
+ descriptor.
+ */
+ class CJSONServiceDescription : public CJSONUtils
+ {
+ friend class JSONSchemaTypeDefinition;
+ friend class JsonRpcMethod;
+ public:
+ /*!
+ \brief Parses the given json schema description and evaluates
+ and stores the defined type
+ \param jsonType json schema description to parse
+ \return True if the json schema description has been parsed successfully otherwise false
+ */
+ static bool AddType(const std::string &jsonType);
+
+ /*!
+ \brief Parses the given json schema description and evaluates
+ and stores the defined method
+ \param jsonMethod json schema description to parse
+ \param method pointer to the implementation
+ \return True if the json schema description has been parsed successfully otherwise false
+ */
+ static bool AddMethod(const std::string &jsonMethod, MethodCall method);
+
+ /*!
+ \brief Parses the given json schema description and evaluates
+ and stores the defined builtin method
+ \param jsonMethod json schema description to parse
+ \return True if the json schema description has been parsed successfully otherwise false
+ */
+ static bool AddBuiltinMethod(const std::string &jsonMethod);
+
+ /*!
+ \brief Parses the given json schema description and evaluates
+ and stores the defined notification
+ \param jsonNotification json schema description to parse
+ \return True if the json schema description has been parsed successfully otherwise false
+ */
+ static bool AddNotification(const std::string &jsonNotification);
+
+ static bool AddEnum(const std::string &name, const std::vector<CVariant> &values, CVariant::VariantType type = CVariant::VariantTypeNull, const CVariant &defaultValue = CVariant::ConstNullVariant);
+ static bool AddEnum(const std::string &name, const std::vector<std::string> &values);
+ static bool AddEnum(const std::string &name, const std::vector<int> &values);
+
+ /*!
+ \brief Gets the version of the json
+ schema description
+ \return Version of the json schema description
+ */
+ static const char* GetVersion();
+
+ /*!
+ \brief Prints the json schema description into the given result object
+ \param result Object into which the json schema description is printed
+ \param transport Transport layer capabilities
+ \param client Client requesting a print
+ \param printDescriptions Whether to print descriptions or not
+ \param printMetadata Whether to print XBMC specific data or not
+ \param filterByTransport Whether to filter by transport or not
+ */
+ static JSONRPC_STATUS Print(CVariant &result, ITransportLayer *transport, IClient *client, bool printDescriptions = true, bool printMetadata = false, bool filterByTransport = true, const std::string &filterByName = "", const std::string &filterByType = "", bool printReferences = true);
+
+ /*!
+ \brief Checks the given parameters from the request against the
+ json schema description for the given method
+ \param method Called method
+ \param requestParameters Parameters from the request
+ \param client Client who sent the request
+ \param notification Whether the request was sent as a notification or not
+ \param methodCall Object which will contain the actual C/C++ method to be called
+ \param outputParameters Cleaned up parameter list
+ \return OK if the validation of the request succeeded otherwise an appropriate error code
+
+ Checks if the given method is a valid json rpc method, if the client has the permission
+ to call this method, if the method can be called as a notification or not, assigns the
+ actual C/C++ implementation of the method to the "methodCall" parameter and checks the
+ given parameters from the request against the json schema description for the given method.
+ */
+ static JSONRPC_STATUS CheckCall(const char* method, const CVariant &requestParameters, ITransportLayer *transport, IClient *client, bool notification, MethodCall &methodCall, CVariant &outputParameters);
+
+ static JSONSchemaTypeDefinitionPtr GetType(const std::string &identification);
+
+ static void ResolveReferences();
+ static void Cleanup();
+
+ private:
+ static bool prepareDescription(std::string &description, CVariant &descriptionObject, std::string &name);
+ static bool addMethod(const std::string &jsonMethod, MethodCall method);
+ static void parseHeader(const CVariant &descriptionObject);
+ static bool parseJSONSchemaType(const CVariant &value, std::vector<JSONSchemaTypeDefinitionPtr>& typeDefinitions, JSONSchemaType &schemaType, std::string &missingReference);
+ static void addReferenceTypeDefinition(const JSONSchemaTypeDefinitionPtr& typeDefinition);
+ static void removeReferenceTypeDefinition(const std::string &typeID);
+
+ static void getReferencedTypes(const JSONSchemaTypeDefinitionPtr& type,
+ std::vector<std::string>& referencedTypes);
+
+ class CJsonRpcMethodMap
+ {
+ public:
+ CJsonRpcMethodMap();
+
+ void add(const JsonRpcMethod &method);
+
+ typedef std::map<std::string, JsonRpcMethod>::const_iterator JsonRpcMethodIterator;
+ JsonRpcMethodIterator begin() const;
+ JsonRpcMethodIterator find(const std::string& key) const;
+ JsonRpcMethodIterator end() const;
+
+ void clear();
+ private:
+ std::map<std::string, JsonRpcMethod> m_actionmap;
+ };
+
+ static CJsonRpcMethodMap m_actionMap;
+ static std::map<std::string, JSONSchemaTypeDefinitionPtr> m_types;
+ static std::map<std::string, CVariant> m_notifications;
+ static JsonRpcMethodMap m_methodMaps[];
+
+ typedef enum SchemaDefinition
+ {
+ SchemaDefinitionType,
+ SchemaDefinitionMethod
+ } SchemaDefinition;
+
+ typedef struct IncompleteSchemaDefinition
+ {
+ std::string Schema;
+ SchemaDefinition Type;
+ MethodCall Method;
+ } IncompleteSchemaDefinition;
+
+ typedef std::map<std::string, std::vector<IncompleteSchemaDefinition> > IncompleteSchemaDefinitionMap;
+ static IncompleteSchemaDefinitionMap m_incompleteDefinitions;
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/JSONUtils.cpp b/xbmc/interfaces/json-rpc/JSONUtils.cpp
new file mode 100644
index 0000000..a8d60d1
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/JSONUtils.cpp
@@ -0,0 +1,38 @@
+/*
+ * 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 "JSONUtils.h"
+
+#include "XBDateTime.h"
+
+namespace JSONRPC
+{
+
+void CJSONUtils::SetFromDBDate(const CVariant& jsonDate, CDateTime& date)
+{
+ if (!jsonDate.isString())
+ return;
+
+ if (jsonDate.empty())
+ date.Reset();
+ else
+ date.SetFromDBDate(jsonDate.asString());
+}
+
+void CJSONUtils::SetFromDBDateTime(const CVariant& jsonDate, CDateTime& date)
+{
+ if (!jsonDate.isString())
+ return;
+
+ if (jsonDate.empty())
+ date.Reset();
+ else
+ date.SetFromDBDateTime(jsonDate.asString());
+}
+
+} // namespace JSONRPC
diff --git a/xbmc/interfaces/json-rpc/JSONUtils.h b/xbmc/interfaces/json-rpc/JSONUtils.h
new file mode 100644
index 0000000..150e23b
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/JSONUtils.h
@@ -0,0 +1,490 @@
+/*
+ * 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 "JSONRPCUtils.h"
+#include "playlists/SmartPlayList.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/JSONVariantWriter.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <vector>
+
+class CDateTime;
+
+namespace JSONRPC
+{
+ /*!
+ \brief Possible value types of a parameter or return type
+ */
+ enum JSONSchemaType
+ {
+ NullValue = 0x01,
+ StringValue = 0x02,
+ NumberValue = 0x04,
+ IntegerValue = 0x08,
+ BooleanValue = 0x10,
+ ArrayValue = 0x20,
+ ObjectValue = 0x40,
+ AnyValue = 0x80
+ };
+
+ /*!
+ \ingroup jsonrpc
+ \brief Helper class containing utility methods to handle
+ json rpc method calls.*/
+ class CJSONUtils
+ {
+ public:
+ static void MillisecondsToTimeObject(int time, CVariant &result)
+ {
+ int ms = time % 1000;
+ result["milliseconds"] = ms;
+ time = (time - ms) / 1000;
+
+ int s = time % 60;
+ result["seconds"] = s;
+ time = (time - s) / 60;
+
+ int m = time % 60;
+ result["minutes"] = m;
+ time = (time -m) / 60;
+
+ result["hours"] = time;
+ }
+
+ protected:
+ static void HandleLimits(const CVariant &parameterObject, CVariant &result, int size, int &start, int &end)
+ {
+ if (size < 0)
+ size = 0;
+
+ start = (int)parameterObject["limits"]["start"].asInteger();
+ end = (int)parameterObject["limits"]["end"].asInteger();
+ end = (end <= 0 || end > size) ? size : end;
+ start = start > end ? end : start;
+
+ result["limits"]["start"] = start;
+ result["limits"]["end"] = end;
+ result["limits"]["total"] = size;
+ }
+
+ static bool ParseSorting(const CVariant &parameterObject, SortBy &sortBy, SortOrder &sortOrder, SortAttribute &sortAttributes)
+ {
+ std::string method = parameterObject["sort"]["method"].asString();
+ std::string order = parameterObject["sort"]["order"].asString();
+ StringUtils::ToLower(method);
+ StringUtils::ToLower(order);
+
+ // parse the sort attributes
+ sortAttributes = SortAttributeNone;
+ if (parameterObject["sort"]["ignorearticle"].asBoolean())
+ sortAttributes = static_cast<SortAttribute>(sortAttributes | SortAttributeIgnoreArticle);
+ if (parameterObject["sort"]["useartistsortname"].asBoolean())
+ sortAttributes = static_cast<SortAttribute>(sortAttributes | SortAttributeUseArtistSortName);
+
+ // parse the sort order
+ sortOrder = SortUtils::SortOrderFromString(order);
+ if (sortOrder == SortOrderNone)
+ return false;
+
+ // parse the sort method
+ sortBy = SortUtils::SortMethodFromString(method);
+
+ return true;
+ }
+
+ static void ParseLimits(const CVariant &parameterObject, int &limitStart, int &limitEnd)
+ {
+ limitStart = (int)parameterObject["limits"]["start"].asInteger();
+ limitEnd = (int)parameterObject["limits"]["end"].asInteger();
+ }
+
+ /*!
+ \brief Checks if the given object contains a parameter
+ \param parameterObject Object to check for a parameter
+ \param key Possible name of the parameter
+ \param position Possible position of the parameter
+ \return True if the parameter is available otherwise false
+
+ Checks the given object for a parameter with the given key (if
+ the given object is not an array) or for a parameter at the
+ given position (if the given object is an array).
+ */
+ static inline bool ParameterExists(const CVariant& parameterObject,
+ const std::string& key,
+ unsigned int position)
+ {
+ return IsValueMember(parameterObject, key) ||
+ (parameterObject.isArray() && parameterObject.size() > position);
+ }
+
+ /*!
+ \brief Checks if the given object contains a value
+ with the given key
+ \param value Value to check for the member
+ \param key Key of the member to check for
+ \return True if the given object contains a member with
+ the given key otherwise false
+ */
+ static inline bool IsValueMember(const CVariant& value, const std::string& key)
+ {
+ return value.isMember(key);
+ }
+
+ /*!
+ \brief Returns the json value of a parameter
+ \param parameterObject Object containing all provided parameters
+ \param key Possible name of the parameter
+ \param position Possible position of the parameter
+ \return Json value of the parameter with the given name or at the
+ given position
+
+ Returns the value of the parameter with the given key (if
+ the given object is not an array) or of the parameter at the
+ given position (if the given object is an array).
+ */
+ static inline CVariant GetParameter(const CVariant& parameterObject,
+ const std::string& key,
+ unsigned int position)
+ {
+ return IsValueMember(parameterObject, key) ? parameterObject[key] : parameterObject[position];
+ }
+
+ /*!
+ \brief Returns the json value of a parameter or the given
+ default value
+ \param parameterObject Object containing all provided parameters
+ \param key Possible name of the parameter
+ \param position Possible position of the parameter
+ \param fallback Default value of the parameter
+ \return Json value of the parameter with the given name or at the
+ given position or the default value if the parameter does not exist
+
+ Returns the value of the parameter with the given key (if
+ the given object is not an array) or of the parameter at the
+ given position (if the given object is an array). If the
+ parameter does not exist the given default value is returned.
+ */
+ static inline CVariant GetParameter(const CVariant& parameterObject,
+ const std::string& key,
+ unsigned int position,
+ const CVariant& fallback)
+ {
+ return IsValueMember(parameterObject, key)
+ ? parameterObject[key]
+ : ((parameterObject.isArray() && parameterObject.size() > position)
+ ? parameterObject[position]
+ : fallback);
+ }
+
+ /*!
+ \brief Returns the given json value as a string
+ \param value Json value to convert to a string
+ \param defaultValue Default string value
+ \return String value of the given json value or the default value
+ if the given json value is no string
+ */
+ static inline std::string GetString(const CVariant &value, const char* defaultValue)
+ {
+ std::string str = defaultValue;
+ if (value.isString())
+ {
+ str = value.asString();
+ }
+
+ return str;
+ }
+
+ /*!
+ \brief Returns a TransportLayerCapability value of the
+ given string representation
+ \param transport String representation of the TransportLayerCapability
+ \return TransportLayerCapability value of the given string representation
+ */
+ static inline TransportLayerCapability StringToTransportLayer(const std::string& transport)
+ {
+ if (transport.compare("Announcing") == 0)
+ return Announcing;
+ if (transport.compare("FileDownloadDirect") == 0)
+ return FileDownloadDirect;
+ if (transport.compare("FileDownloadRedirect") == 0)
+ return FileDownloadRedirect;
+
+ return Response;
+ }
+
+ /*!
+ \brief Returns a JSONSchemaType value for the given
+ string representation
+ \param valueType String representation of the JSONSchemaType
+ \return JSONSchemaType value of the given string representation
+ */
+ static inline JSONSchemaType StringToSchemaValueType(const std::string& valueType)
+ {
+ if (valueType.compare("null") == 0)
+ return NullValue;
+ if (valueType.compare("string") == 0)
+ return StringValue;
+ if (valueType.compare("number") == 0)
+ return NumberValue;
+ if (valueType.compare("integer") == 0)
+ return IntegerValue;
+ if (valueType.compare("boolean") == 0)
+ return BooleanValue;
+ if (valueType.compare("array") == 0)
+ return ArrayValue;
+ if (valueType.compare("object") == 0)
+ return ObjectValue;
+
+ return AnyValue;
+ }
+
+ /*!
+ \brief Returns a string representation for the
+ given JSONSchemaType
+ \param valueType Specific JSONSchemaType
+ \return String representation of the given JSONSchemaType
+ */
+ static inline std::string SchemaValueTypeToString(JSONSchemaType valueType)
+ {
+ std::vector<JSONSchemaType> types = std::vector<JSONSchemaType>();
+ for (unsigned int value = 0x01; value <= (unsigned int)AnyValue; value *= 2)
+ {
+ if (HasType(valueType, (JSONSchemaType)value))
+ types.push_back((JSONSchemaType)value);
+ }
+
+ std::string strType;
+ if (types.size() > 1)
+ strType.append("[");
+
+ for (unsigned int index = 0; index < types.size(); index++)
+ {
+ if (index > 0)
+ strType.append(", ");
+
+ switch (types.at(index))
+ {
+ case StringValue:
+ strType.append("string");
+ break;
+ case NumberValue:
+ strType.append("number");
+ break;
+ case IntegerValue:
+ strType.append("integer");
+ break;
+ case BooleanValue:
+ strType.append("boolean");
+ break;
+ case ArrayValue:
+ strType.append("array");
+ break;
+ case ObjectValue:
+ strType.append("object");
+ break;
+ case AnyValue:
+ strType.append("any");
+ break;
+ case NullValue:
+ strType.append("null");
+ break;
+ default:
+ strType.append("unknown");
+ }
+ }
+
+ if (types.size() > 1)
+ strType.append("]");
+
+ return strType;
+ }
+
+ /*!
+ \brief Converts the given json schema type into
+ a json object
+ \param valueTye json schema type(s)
+ \param jsonObject json object into which the json schema type(s) are stored
+ */
+ static inline void SchemaValueTypeToJson(JSONSchemaType valueType, CVariant &jsonObject)
+ {
+ jsonObject = CVariant(CVariant::VariantTypeArray);
+ for (unsigned int value = 0x01; value <= (unsigned int)AnyValue; value *= 2)
+ {
+ if (HasType(valueType, (JSONSchemaType)value))
+ jsonObject.append(SchemaValueTypeToString((JSONSchemaType)value));
+ }
+
+ if (jsonObject.size() == 1)
+ {
+ CVariant jsonType = jsonObject[0];
+ jsonObject = jsonType;
+ }
+ }
+
+ static inline const char *ValueTypeToString(CVariant::VariantType valueType)
+ {
+ switch (valueType)
+ {
+ case CVariant::VariantTypeString:
+ return "string";
+ case CVariant::VariantTypeDouble:
+ return "number";
+ case CVariant::VariantTypeInteger:
+ case CVariant::VariantTypeUnsignedInteger:
+ return "integer";
+ case CVariant::VariantTypeBoolean:
+ return "boolean";
+ case CVariant::VariantTypeArray:
+ return "array";
+ case CVariant::VariantTypeObject:
+ return "object";
+ case CVariant::VariantTypeNull:
+ case CVariant::VariantTypeConstNull:
+ return "null";
+ default:
+ return "unknown";
+ }
+ }
+
+ /*!
+ \brief Checks if the parameter with the given name or at
+ the given position is of a certain type
+ \param parameterObject Object containing all provided parameters
+ \param key Possible name of the parameter
+ \param position Possible position of the parameter
+ \param valueType Expected type of the parameter
+ \return True if the specific parameter is of the given type otherwise false
+ */
+ static inline bool IsParameterType(const CVariant &parameterObject, const char *key, unsigned int position, JSONSchemaType valueType)
+ {
+ if ((valueType & AnyValue) == AnyValue)
+ return true;
+
+ CVariant parameter;
+ if (IsValueMember(parameterObject, key))
+ parameter = parameterObject[key];
+ else if(parameterObject.isArray() && parameterObject.size() > position)
+ parameter = parameterObject[position];
+
+ return IsType(parameter, valueType);
+ }
+
+ /*!
+ \brief Checks if the given json value is of the given type
+ \param value Json value to check
+ \param valueType Expected type of the json value
+ \return True if the given json value is of the given type otherwise false
+ */
+ static inline bool IsType(const CVariant &value, JSONSchemaType valueType)
+ {
+ if (HasType(valueType, AnyValue))
+ return true;
+ if (HasType(valueType, StringValue) && value.isString())
+ return true;
+ if (HasType(valueType, NumberValue) && (value.isInteger() || value.isUnsignedInteger() || value.isDouble()))
+ return true;
+ if (HasType(valueType, IntegerValue) && (value.isInteger() || value.isUnsignedInteger()))
+ return true;
+ if (HasType(valueType, BooleanValue) && value.isBoolean())
+ return true;
+ if (HasType(valueType, ArrayValue) && value.isArray())
+ return true;
+ if (HasType(valueType, ObjectValue) && value.isObject())
+ return true;
+
+ return value.isNull();
+ }
+
+ /*!
+ \brief Sets the value of the given json value to the
+ default value of the given type
+ \param value Json value to be set
+ \param valueType Type of the default value
+ */
+ static inline void SetDefaultValue(CVariant &value, JSONSchemaType valueType)
+ {
+ switch (valueType)
+ {
+ case StringValue:
+ value = CVariant("");
+ break;
+ case NumberValue:
+ value = CVariant(CVariant::VariantTypeDouble);
+ break;
+ case IntegerValue:
+ value = CVariant(CVariant::VariantTypeInteger);
+ break;
+ case BooleanValue:
+ value = CVariant(CVariant::VariantTypeBoolean);
+ break;
+ case ArrayValue:
+ value = CVariant(CVariant::VariantTypeArray);
+ break;
+ case ObjectValue:
+ value = CVariant(CVariant::VariantTypeObject);
+ break;
+ default:
+ value = CVariant(CVariant::VariantTypeNull);
+ }
+ }
+
+ static inline bool HasType(JSONSchemaType typeObject, JSONSchemaType type) { return (typeObject & type) == type; }
+
+ static inline bool ParameterNotNull(const CVariant& parameterObject, const std::string& key)
+ {
+ return parameterObject.isMember(key) && !parameterObject[key].isNull();
+ }
+
+ /*!
+ \brief Copies the values from the jsonStringArray to the stringArray.
+ stringArray is cleared.
+ \param jsonStringArray JSON object representing a string array
+ \param stringArray String array where the values are copied into (cleared)
+ */
+ static void CopyStringArray(const CVariant &jsonStringArray, std::vector<std::string> &stringArray)
+ {
+ if (!jsonStringArray.isArray())
+ return;
+
+ stringArray.clear();
+ for (CVariant::const_iterator_array it = jsonStringArray.begin_array(); it != jsonStringArray.end_array(); ++it)
+ stringArray.push_back(it->asString());
+ }
+
+ static void SetFromDBDate(const CVariant& jsonDate, CDateTime& date);
+
+ static void SetFromDBDateTime(const CVariant& jsonDate, CDateTime& date);
+
+ static bool GetXspFiltering(const std::string &type, const CVariant &filter, std::string &xsp)
+ {
+ if (type.empty() || !filter.isObject())
+ return false;
+
+ CVariant xspObj(CVariant::VariantTypeObject);
+ xspObj["type"] = type;
+
+ if (filter.isMember("field"))
+ {
+ xspObj["rules"]["and"] = CVariant(CVariant::VariantTypeArray);
+ xspObj["rules"]["and"].push_back(filter);
+ }
+ else
+ xspObj["rules"] = filter;
+
+ CSmartPlaylist playlist;
+ return playlist.Load(xspObj) && playlist.SaveAsJson(xsp, false);
+ }
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/PVROperations.cpp b/xbmc/interfaces/json-rpc/PVROperations.cpp
new file mode 100644
index 0000000..3611ad6
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/PVROperations.cpp
@@ -0,0 +1,543 @@
+/*
+ * 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 "PVROperations.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "utils/Variant.h"
+
+using namespace JSONRPC;
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+JSONRPC_STATUS CPVROperations::GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ CVariant properties = CVariant(CVariant::VariantTypeObject);
+ for (unsigned int index = 0; index < parameterObject["properties"].size(); index++)
+ {
+ std::string propertyName = parameterObject["properties"][index].asString();
+ CVariant property;
+ JSONRPC_STATUS ret;
+ if ((ret = GetPropertyValue(propertyName, property)) != OK)
+ return ret;
+
+ properties[propertyName] = property;
+ }
+
+ result = properties;
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::GetChannelGroups(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRChannelGroupsContainer> channelGroupContainer = CServiceBroker::GetPVRManager().ChannelGroups();
+ if (!channelGroupContainer)
+ return FailedToExecute;
+
+ CPVRChannelGroups *channelGroups = channelGroupContainer->Get(parameterObject["channeltype"].asString().compare("radio") == 0);
+ if (channelGroups == NULL)
+ return FailedToExecute;
+
+ int start, end;
+
+ std::vector<std::shared_ptr<CPVRChannelGroup>> groupList = channelGroups->GetMembers(true);
+ HandleLimits(parameterObject, result, groupList.size(), start, end);
+ for (int index = start; index < end; index++)
+ FillChannelGroupDetails(groupList.at(index), parameterObject, result["channelgroups"], true);
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::GetChannelGroupDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRChannelGroupsContainer> channelGroupContainer = CServiceBroker::GetPVRManager().ChannelGroups();
+ if (!channelGroupContainer)
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRChannelGroup> channelGroup;
+ CVariant id = parameterObject["channelgroupid"];
+ if (id.isInteger())
+ channelGroup = channelGroupContainer->GetByIdFromAll((int)id.asInteger());
+ else if (id.isString())
+ channelGroup = channelGroupContainer->GetGroupAll(id.asString() == "allradio");
+
+ if (channelGroup == NULL)
+ return InvalidParams;
+
+ FillChannelGroupDetails(channelGroup, parameterObject, result["channelgroupdetails"], false);
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::GetChannels(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRChannelGroupsContainer> channelGroupContainer = CServiceBroker::GetPVRManager().ChannelGroups();
+ if (!channelGroupContainer)
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRChannelGroup> channelGroup;
+ CVariant id = parameterObject["channelgroupid"];
+ if (id.isInteger())
+ channelGroup = channelGroupContainer->GetByIdFromAll((int)id.asInteger());
+ else if (id.isString())
+ channelGroup = channelGroupContainer->GetGroupAll(id.asString() == "allradio");
+
+ if (channelGroup == NULL)
+ return InvalidParams;
+
+ CFileItemList channels;
+ const auto groupMembers = channelGroup->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ for (const auto& groupMember : groupMembers)
+ {
+ channels.Add(std::make_shared<CFileItem>(groupMember));
+ }
+
+ HandleFileItemList("channelid", false, "channels", channels, parameterObject, result, true);
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::GetChannelDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRChannelGroupsContainer> channelGroupContainer = CServiceBroker::GetPVRManager().ChannelGroups();
+ if (!channelGroupContainer)
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRChannel> channel = channelGroupContainer->GetChannelById(
+ static_cast<int>(parameterObject["channelid"].asInteger()));
+ if (channel == NULL)
+ return InvalidParams;
+
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(channel);
+ if (!groupMember)
+ return InvalidParams;
+
+ HandleFileItem("channelid", false, "channeldetails", std::make_shared<CFileItem>(groupMember),
+ parameterObject, parameterObject["properties"], result, false);
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::GetClients(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ int start, end;
+
+ auto clientInfos = CServiceBroker::GetPVRManager().Clients()->GetEnabledClientInfos();
+ HandleLimits(parameterObject, result, clientInfos.size(), start, end);
+ for (int index = start; index < end; index++)
+ result["clients"].append(clientInfos[index]);
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::GetBroadcasts(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRChannelGroupsContainer> channelGroupContainer = CServiceBroker::GetPVRManager().ChannelGroups();
+ if (!channelGroupContainer)
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRChannel> channel = channelGroupContainer->GetChannelById((int)parameterObject["channelid"].asInteger());
+ if (channel == NULL)
+ return InvalidParams;
+
+ std::shared_ptr<CPVREpg> channelEpg = channel->GetEPG();
+ if (!channelEpg)
+ return InternalError;
+
+ CFileItemList programFull;
+
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags = channelEpg->GetTags();
+ for (const auto& tag : tags)
+ {
+ programFull.Add(std::make_shared<CFileItem>(tag));
+ }
+
+ HandleFileItemList("broadcastid", false, "broadcasts", programFull, parameterObject, result, programFull.Size(), true);
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::GetBroadcastDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ CServiceBroker::GetPVRManager().EpgContainer().GetTagByDatabaseId(
+ parameterObject["broadcastid"].asInteger());
+
+ if (!epgTag)
+ return InvalidParams;
+
+ HandleFileItem("broadcastid", false, "broadcastdetails", CFileItemPtr(new CFileItem(epgTag)), parameterObject, parameterObject["properties"], result, false);
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::GetBroadcastIsPlayable(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ CServiceBroker::GetPVRManager().EpgContainer().GetTagByDatabaseId(
+ parameterObject["broadcastid"].asInteger());
+
+ if (!epgTag)
+ return InvalidParams;
+
+ result = epgTag->IsPlayable();
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::Record(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRChannel> pChannel;
+ CVariant channel = parameterObject["channel"];
+ if (channel.isString() && channel.asString() == "current")
+ {
+ pChannel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (!pChannel)
+ return InternalError;
+ }
+ else if (channel.isInteger())
+ {
+ std::shared_ptr<CPVRChannelGroupsContainer> channelGroupContainer = CServiceBroker::GetPVRManager().ChannelGroups();
+ if (!channelGroupContainer)
+ return FailedToExecute;
+
+ pChannel = channelGroupContainer->GetChannelById((int)channel.asInteger());
+ }
+ else
+ return InvalidParams;
+
+ if (pChannel == NULL)
+ return InvalidParams;
+ else if (!pChannel->CanRecord())
+ return FailedToExecute;
+
+ CVariant record = parameterObject["record"];
+ bool bIsRecording = CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*pChannel);
+ bool toggle = true;
+ if (record.isBoolean() && record.asBoolean() == bIsRecording)
+ toggle = false;
+
+ if (toggle)
+ {
+ if (!CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().SetRecordingOnChannel(
+ pChannel, !bIsRecording))
+ return FailedToExecute;
+ }
+
+ return ACK;
+}
+
+JSONRPC_STATUS CPVROperations::Scan(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ if (parameterObject.isMember("clientid"))
+ {
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().StartChannelScan(
+ parameterObject["clientid"].asInteger()))
+ return ACK;
+ }
+ else
+ {
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().StartChannelScan())
+ return ACK;
+ }
+
+ return FailedToExecute;
+}
+
+JSONRPC_STATUS CPVROperations::GetPropertyValue(const std::string &property, CVariant &result)
+{
+ bool started = CServiceBroker::GetPVRManager().IsStarted();
+
+ if (property == "available")
+ result = started;
+ else if (property == "recording")
+ {
+ if (started)
+ result = CServiceBroker::GetPVRManager().PlaybackState()->IsRecording();
+ else
+ result = false;
+ }
+ else if (property == "scanning")
+ {
+ if (started)
+ result = CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().IsRunningChannelScan();
+ else
+ result = false;
+ }
+ else
+ return InvalidParams;
+
+ return OK;
+}
+
+void CPVROperations::FillChannelGroupDetails(const std::shared_ptr<CPVRChannelGroup> &channelGroup, const CVariant &parameterObject, CVariant &result, bool append /* = false */)
+{
+ if (channelGroup == NULL)
+ return;
+
+ CVariant object(CVariant::VariantTypeObject);
+ object["channelgroupid"] = channelGroup->GroupID();
+ object["channeltype"] = channelGroup->IsRadio() ? "radio" : "tv";
+ object["label"] = channelGroup->GroupName();
+
+ if (append)
+ result.append(object);
+ else
+ {
+ CFileItemList channels;
+ const auto groupMembers = channelGroup->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ for (const auto& groupMember : groupMembers)
+ {
+ channels.Add(std::make_shared<CFileItem>(groupMember));
+ }
+
+ object["channels"] = CVariant(CVariant::VariantTypeArray);
+ HandleFileItemList("channelid", false, "channels", channels, parameterObject["channels"], object, false);
+
+ result = object;
+ }
+}
+
+JSONRPC_STATUS CPVROperations::GetTimers(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRTimers> timers = CServiceBroker::GetPVRManager().Timers();
+ if (!timers)
+ return FailedToExecute;
+
+ CFileItemList timerList;
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>> tags = timers->GetAll();
+ for (const auto& timer : tags)
+ {
+ timerList.Add(std::make_shared<CFileItem>(timer));
+ }
+
+ HandleFileItemList("timerid", false, "timers", timerList, parameterObject, result, true);
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::GetTimerDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRTimers> timers = CServiceBroker::GetPVRManager().Timers();
+ if (!timers)
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRTimerInfoTag> timer = timers->GetById((int)parameterObject["timerid"].asInteger());
+ if (!timer)
+ return InvalidParams;
+
+ HandleFileItem("timerid", false, "timerdetails", CFileItemPtr(new CFileItem(timer)), parameterObject, parameterObject["properties"], result, false);
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::AddTimer(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ CServiceBroker::GetPVRManager().EpgContainer().GetTagByDatabaseId(
+ parameterObject["broadcastid"].asInteger());
+
+ if (!epgTag)
+ return InvalidParams;
+
+ if (CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epgTag))
+ return InvalidParams;
+
+ const std::shared_ptr<CPVRTimerInfoTag> newTimer =
+ CPVRTimerInfoTag::CreateFromEpg(epgTag, parameterObject["timerrule"].asBoolean(false),
+ parameterObject["reminder"].asBoolean(false));
+ if (newTimer)
+ {
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimer(newTimer))
+ return ACK;
+ }
+ return FailedToExecute;
+}
+
+
+JSONRPC_STATUS CPVROperations::DeleteTimer(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRTimers> timers = CServiceBroker::GetPVRManager().Timers();
+ if (!timers)
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRTimerInfoTag> timer = timers->GetById(parameterObject["timerid"].asInteger());
+ if (!timer)
+ return InvalidParams;
+
+ if (timers->DeleteTimer(timer, timer->IsRecording(), false) == TimerOperationResult::OK)
+ return ACK;
+
+ return FailedToExecute;
+}
+
+JSONRPC_STATUS CPVROperations::ToggleTimer(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ CServiceBroker::GetPVRManager().EpgContainer().GetTagByDatabaseId(
+ parameterObject["broadcastid"].asInteger());
+
+ if (!epgTag)
+ return InvalidParams;
+
+ bool timerrule = parameterObject["timerrule"].asBoolean(false);
+ bool sentOkay = false;
+ std::shared_ptr<CPVRTimerInfoTag> timer = CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epgTag);
+ if (timer)
+ {
+ if (timerrule)
+ timer = CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer);
+
+ if (timer)
+ sentOkay = (CServiceBroker::GetPVRManager().Timers()->DeleteTimer(timer, timer->IsRecording(), false) == TimerOperationResult::OK);
+ }
+ else
+ {
+ timer = CPVRTimerInfoTag::CreateFromEpg(epgTag, timerrule);
+ if (!timer)
+ return InvalidParams;
+
+ sentOkay = CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimer(timer);
+ }
+
+ if (sentOkay)
+ return ACK;
+
+ return FailedToExecute;
+}
+
+JSONRPC_STATUS CPVROperations::GetRecordings(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRRecordings> recordings = CServiceBroker::GetPVRManager().Recordings();
+ if (!recordings)
+ return FailedToExecute;
+
+ CFileItemList recordingsList;
+ const std::vector<std::shared_ptr<CPVRRecording>> recs = recordings->GetAll();
+ for (const auto& recording : recs)
+ {
+ recordingsList.Add(std::make_shared<CFileItem>(recording));
+ }
+
+ HandleFileItemList("recordingid", true, "recordings", recordingsList, parameterObject, result, true);
+
+ return OK;
+}
+
+JSONRPC_STATUS CPVROperations::GetRecordingDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return FailedToExecute;
+
+ std::shared_ptr<CPVRRecordings> recordings = CServiceBroker::GetPVRManager().Recordings();
+ if (!recordings)
+ return FailedToExecute;
+
+ const std::shared_ptr<CPVRRecording> recording = recordings->GetById(static_cast<int>(parameterObject["recordingid"].asInteger()));
+ if (!recording)
+ return InvalidParams;
+
+ HandleFileItem("recordingid", true, "recordingdetails", std::make_shared<CFileItem>(recording), parameterObject, parameterObject["properties"], result, false);
+
+ return OK;
+}
+
+std::shared_ptr<CFileItem> CPVROperations::GetRecordingFileItem(int recordingId)
+{
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ const std::shared_ptr<PVR::CPVRRecordings> recordings =
+ CServiceBroker::GetPVRManager().Recordings();
+
+ if (recordings)
+ {
+ const std::shared_ptr<PVR::CPVRRecording> recording = recordings->GetById(recordingId);
+ if (recording)
+ return std::make_shared<CFileItem>(recording);
+ }
+ }
+
+ return {};
+}
diff --git a/xbmc/interfaces/json-rpc/PVROperations.h b/xbmc/interfaces/json-rpc/PVROperations.h
new file mode 100644
index 0000000..08aa703
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/PVROperations.h
@@ -0,0 +1,58 @@
+/*
+ * 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 "FileItemHandler.h"
+#include "pvr/channels/PVRChannelGroup.h"
+
+#include <memory>
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CPVROperations : public CFileItemHandler
+ {
+ public:
+ static JSONRPC_STATUS GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetChannelGroups(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetChannelGroupDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetChannels(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetChannelDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetClients(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result);
+ static JSONRPC_STATUS GetBroadcasts(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetBroadcastDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetBroadcastIsPlayable(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result);
+ static JSONRPC_STATUS GetTimers(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetTimerDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetRecordings(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetRecordingDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS AddTimer(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS DeleteTimer(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS ToggleTimer(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS Record(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Scan(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static std::shared_ptr<CFileItem> GetRecordingFileItem(int recordingId);
+
+ private:
+ static JSONRPC_STATUS GetPropertyValue(const std::string &property, CVariant &result);
+ static void FillChannelGroupDetails(const std::shared_ptr<PVR::CPVRChannelGroup> &channelGroup, const CVariant &parameterObject, CVariant &result, bool append = false);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/PlayerOperations.cpp b/xbmc/interfaces/json-rpc/PlayerOperations.cpp
new file mode 100644
index 0000000..9912a72
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/PlayerOperations.cpp
@@ -0,0 +1,2158 @@
+/*
+ * 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 "PlayerOperations.h"
+
+#include "AudioLibrary.h"
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "PartyModeManager.h"
+#include "PlayListPlayer.h"
+#include "SeekHandler.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "VideoLibrary.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationPowerHandling.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/builtins/Builtins.h"
+#include "messaging/ApplicationMessenger.h"
+#include "music/MusicDatabase.h"
+#include "pictures/GUIWindowSlideShow.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/DisplaySettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/MathUtils.h"
+#include "utils/Variant.h"
+#include "video/VideoDatabase.h"
+
+#include <map>
+#include <tuple>
+
+using namespace JSONRPC;
+using namespace PVR;
+
+namespace
+{
+
+void AppendAudioStreamFlagsAsBooleans(CVariant& list, StreamFlags flags)
+{
+ list["isdefault"] = ((flags & StreamFlags::FLAG_DEFAULT) != 0);
+ list["isoriginal"] = ((flags & StreamFlags::FLAG_ORIGINAL) != 0);
+ list["isimpaired"] = ((flags & StreamFlags::FLAG_VISUAL_IMPAIRED) != 0);
+}
+
+void AppendSubtitleStreamFlagsAsBooleans(CVariant& list, StreamFlags flags)
+{
+ list["isdefault"] = ((flags & StreamFlags::FLAG_DEFAULT) != 0);
+ list["isforced"] = ((flags & StreamFlags::FLAG_FORCED) != 0);
+ list["isimpaired"] = ((flags & StreamFlags::FLAG_HEARING_IMPAIRED) != 0);
+}
+
+} // namespace
+
+JSONRPC_STATUS CPlayerOperations::GetActivePlayers(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int activePlayers = GetActivePlayers();
+ result = CVariant(CVariant::VariantTypeArray);
+
+ std::string strPlayerType = "internal";
+ if (activePlayers & External)
+ strPlayerType = "external";
+ else if (activePlayers & Remote)
+ strPlayerType = "remote";
+
+ if (activePlayers & Video)
+ {
+ CVariant video = CVariant(CVariant::VariantTypeObject);
+ video["playerid"] = GetPlaylist(Video);
+ video["type"] = "video";
+ video["playertype"] = strPlayerType;
+ result.append(video);
+ }
+ if (activePlayers & Audio)
+ {
+ CVariant audio = CVariant(CVariant::VariantTypeObject);
+ audio["playerid"] = GetPlaylist(Audio);
+ audio["type"] = "audio";
+ audio["playertype"] = strPlayerType;
+ result.append(audio);
+ }
+ if (activePlayers & Picture)
+ {
+ CVariant picture = CVariant(CVariant::VariantTypeObject);
+ picture["playerid"] = GetPlaylist(Picture);
+ picture["type"] = "picture";
+ picture["playertype"] = "internal";
+ result.append(picture);
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CPlayerOperations::GetPlayers(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ std::string media = parameterObject["media"].asString();
+ result = CVariant(CVariant::VariantTypeArray);
+ std::vector<std::string> players;
+
+ if (media == "all")
+ {
+ playerCoreFactory.GetPlayers(players);
+ }
+ else
+ {
+ bool video = false;
+ if (media == "video")
+ video = true;
+ playerCoreFactory.GetPlayers(players, true, video);
+ }
+
+ for (const auto& playername : players)
+ {
+ CVariant player(CVariant::VariantTypeObject);
+ player["name"] = playername;
+
+ player["playsvideo"] = playerCoreFactory.PlaysVideo(playername);
+ player["playsaudio"] = playerCoreFactory.PlaysAudio(playername);
+ player["type"] = playerCoreFactory.GetPlayerType(playername);
+
+ result.push_back(player);
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CPlayerOperations::GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ PlayerType player = GetPlayer(parameterObject["playerid"]);
+
+ CVariant properties = CVariant(CVariant::VariantTypeObject);
+ for (unsigned int index = 0; index < parameterObject["properties"].size(); index++)
+ {
+ std::string propertyName = parameterObject["properties"][index].asString();
+ CVariant property;
+ JSONRPC_STATUS ret;
+ if ((ret = GetPropertyValue(player, propertyName, property)) != OK)
+ return ret;
+
+ properties[propertyName] = property;
+ }
+
+ result = properties;
+
+ return OK;
+}
+
+JSONRPC_STATUS CPlayerOperations::GetItem(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ PlayerType player = GetPlayer(parameterObject["playerid"]);
+ CFileItemPtr fileItem;
+
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ {
+ fileItem = std::make_shared<CFileItem>(g_application.CurrentFileItem());
+ if (IsPVRChannel())
+ break;
+
+ if (player == Video)
+ {
+ if (!CVideoLibrary::FillFileItem(fileItem->GetPath(), fileItem, parameterObject))
+ {
+ // Fallback to item details held by GUI but ensure path unchanged
+ //! @todo remove this once there is no route to playback that updates
+ // GUI item without also updating app item e.g. start playback of a
+ // non-library item via JSON
+ const CVideoInfoTag *currentVideoTag = CServiceBroker::GetGUI()->GetInfoManager().GetCurrentMovieTag();
+ if (currentVideoTag != NULL)
+ {
+ std::string originalLabel = fileItem->GetLabel();
+ std::string originalPath = fileItem->GetPath();
+ fileItem->SetFromVideoInfoTag(*currentVideoTag);
+ if (fileItem->GetLabel().empty())
+ fileItem->SetLabel(originalLabel);
+ fileItem->SetPath(originalPath); // Ensure path unchanged
+ }
+ }
+
+ bool additionalInfo = false;
+ for (CVariant::const_iterator_array itr = parameterObject["properties"].begin_array();
+ itr != parameterObject["properties"].end_array(); ++itr)
+ {
+ std::string fieldValue = itr->asString();
+ if (fieldValue == "cast" || fieldValue == "set" || fieldValue == "setid" || fieldValue == "showlink" || fieldValue == "resume" ||
+ (fieldValue == "streamdetails" && !fileItem->GetVideoInfoTag()->m_streamDetails.HasItems()))
+ additionalInfo = true;
+ }
+
+ CVideoDatabase videodatabase;
+ if ((additionalInfo) &&
+ videodatabase.Open())
+ {
+ switch (fileItem->GetVideoContentType())
+ {
+ case VideoDbContentType::MOVIES:
+ videodatabase.GetMovieInfo("", *(fileItem->GetVideoInfoTag()),
+ fileItem->GetVideoInfoTag()->m_iDbId);
+ break;
+
+ case VideoDbContentType::MUSICVIDEOS:
+ videodatabase.GetMusicVideoInfo("", *(fileItem->GetVideoInfoTag()),
+ fileItem->GetVideoInfoTag()->m_iDbId);
+ break;
+
+ case VideoDbContentType::EPISODES:
+ videodatabase.GetEpisodeInfo("", *(fileItem->GetVideoInfoTag()),
+ fileItem->GetVideoInfoTag()->m_iDbId);
+ break;
+
+ case VideoDbContentType::TVSHOWS:
+ case VideoDbContentType::MOVIE_SETS:
+ default:
+ break;
+ }
+
+ videodatabase.Close();
+ }
+ }
+ else // Audio
+ {
+ if (!CAudioLibrary::FillFileItem(fileItem->GetPath(), fileItem, parameterObject))
+ {
+ // Fallback to item details held by GUI but ensure path unchanged
+ //! @todo remove this once there is no route to playback that updates
+ // GUI item without also updating app item e.g. start playback of a
+ // non-library item via JSON
+ const MUSIC_INFO::CMusicInfoTag* currentMusicTag =
+ CServiceBroker::GetGUI()->GetInfoManager().GetCurrentSongTag();
+ if (currentMusicTag != NULL)
+ {
+ std::string originalLabel = fileItem->GetLabel();
+ std::string originalPath = fileItem->GetPath();
+ fileItem->SetFromMusicInfoTag(*currentMusicTag);
+ if (fileItem->GetLabel().empty())
+ fileItem->SetLabel(originalLabel);
+ fileItem->SetPath(originalPath); // Ensure path unchanged
+ }
+ }
+
+ if (fileItem->IsMusicDb())
+ {
+ CMusicDatabase musicdb;
+ CFileItemList items;
+ items.Add(fileItem);
+ CAudioLibrary::GetAdditionalSongDetails(parameterObject, items, musicdb);
+ }
+ }
+ break;
+ }
+
+ case Picture:
+ {
+ CGUIWindowSlideShow *slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (!slideshow)
+ return FailedToExecute;
+
+ CFileItemList slides;
+ slideshow->GetSlideShowContents(slides);
+ fileItem = slides[slideshow->CurrentSlide() - 1];
+ break;
+ }
+
+ case None:
+ default:
+ return FailedToExecute;
+ }
+
+ HandleFileItem("id", !IsPVRChannel(), "item", fileItem, parameterObject, parameterObject["properties"], result, false);
+ return OK;
+}
+
+JSONRPC_STATUS CPlayerOperations::PlayPause(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CGUIWindowSlideShow *slideshow = NULL;
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Video:
+ case Audio:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->CanPause())
+ return FailedToExecute;
+
+ if (parameterObject["play"].isString())
+ CBuiltins::GetInstance().Execute("playercontrol(play)");
+ else
+ {
+ if (parameterObject["play"].asBoolean())
+ {
+ if (appPlayer->IsPausedPlayback())
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ else if (appPlayer->GetPlaySpeed() != 1)
+ appPlayer->SetPlaySpeed(1);
+ }
+ else if (!appPlayer->IsPausedPlayback())
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ result["speed"] = appPlayer->IsPausedPlayback() ? 0 : (int)lrint(appPlayer->GetPlaySpeed());
+ return OK;
+ }
+
+ case Picture:
+ slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (slideshow && slideshow->IsPlaying() &&
+ (parameterObject["play"].isString() ||
+ (parameterObject["play"].isBoolean() && parameterObject["play"].asBoolean() == slideshow->IsPaused())))
+ SendSlideshowAction(ACTION_PAUSE);
+
+ if (slideshow && slideshow->IsPlaying() && !slideshow->IsPaused())
+ result["speed"] = slideshow->GetDirection();
+ else
+ result["speed"] = 0;
+ return OK;
+
+ case None:
+ default:
+ return FailedToExecute;
+ }
+}
+
+JSONRPC_STATUS CPlayerOperations::Stop(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Video:
+ case Audio:
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_MEDIA_STOP, static_cast<int>(parameterObject["playerid"].asInteger()));
+ return ACK;
+
+ case Picture:
+ SendSlideshowAction(ACTION_STOP);
+ return ACK;
+
+ case None:
+ default:
+ return FailedToExecute;
+ }
+}
+
+JSONRPC_STATUS CPlayerOperations::GetAudioDelay(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ result["offset"] = appPlayer->GetVideoSettings().m_AudioDelay;
+ return OK;
+}
+
+JSONRPC_STATUS CPlayerOperations::SetAudioDelay(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result)
+{
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Video:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ float videoAudioDelayRange =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAudioDelayRange;
+
+ if (parameterObject["offset"].isDouble())
+ {
+ float offset = static_cast<float>(parameterObject["offset"].asDouble());
+ offset = MathUtils::RoundF(offset, AUDIO_DELAY_STEP);
+ if (offset > videoAudioDelayRange)
+ offset = videoAudioDelayRange;
+ else if (offset < -videoAudioDelayRange)
+ offset = -videoAudioDelayRange;
+
+ appPlayer->SetAVDelay(offset);
+ }
+ else if (parameterObject["offset"].isString())
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ if (parameterObject["offset"].asString().compare("increment") == 0)
+ {
+ vs.m_AudioDelay += AUDIO_DELAY_STEP;
+ if (vs.m_AudioDelay > videoAudioDelayRange)
+ vs.m_AudioDelay = videoAudioDelayRange;
+ appPlayer->SetAVDelay(vs.m_AudioDelay);
+ }
+ else
+ {
+ vs.m_AudioDelay -= AUDIO_DELAY_STEP;
+ if (vs.m_AudioDelay < -videoAudioDelayRange)
+ vs.m_AudioDelay = -videoAudioDelayRange;
+ appPlayer->SetAVDelay(vs.m_AudioDelay);
+ }
+ }
+ else
+ return InvalidParams;
+
+ result["offset"] = appPlayer->GetVideoSettings().m_AudioDelay;
+ return OK;
+ }
+ case Audio:
+ case Picture:
+ case None:
+ default:
+ return FailedToExecute;
+ }
+}
+
+JSONRPC_STATUS CPlayerOperations::SetSpeed(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Video:
+ case Audio:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (parameterObject["speed"].isInteger())
+ {
+ int speed = (int)parameterObject["speed"].asInteger();
+ if (speed != 0)
+ {
+ // If the player is paused we first need to unpause
+ if (appPlayer->IsPausedPlayback())
+ appPlayer->Pause();
+ appPlayer->SetPlaySpeed(speed);
+ }
+ else
+ appPlayer->Pause();
+ }
+ else if (parameterObject["speed"].isString())
+ {
+ if (parameterObject["speed"].asString().compare("increment") == 0)
+ CBuiltins::GetInstance().Execute("playercontrol(forward)");
+ else
+ CBuiltins::GetInstance().Execute("playercontrol(rewind)");
+ }
+ else
+ return InvalidParams;
+
+ result["speed"] = appPlayer->IsPausedPlayback() ? 0 : (int)lrint(appPlayer->GetPlaySpeed());
+ return OK;
+ }
+
+ case Picture:
+ case None:
+ default:
+ return FailedToExecute;
+ }
+}
+
+JSONRPC_STATUS CPlayerOperations::Seek(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ PlayerType player = GetPlayer(parameterObject["playerid"]);
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->CanSeek())
+ return FailedToExecute;
+
+ const CVariant& value = parameterObject["value"];
+ if (value.isMember("percentage"))
+ g_application.SeekPercentage(value["percentage"].asFloat());
+ else if (value.isMember("step"))
+ {
+ std::string step = value["step"].asString();
+ if (step == "smallforward")
+ CBuiltins::GetInstance().Execute("playercontrol(smallskipforward)");
+ else if (step == "smallbackward")
+ CBuiltins::GetInstance().Execute("playercontrol(smallskipbackward)");
+ else if (step == "bigforward")
+ CBuiltins::GetInstance().Execute("playercontrol(bigskipforward)");
+ else if (step == "bigbackward")
+ CBuiltins::GetInstance().Execute("playercontrol(bigskipbackward)");
+ else
+ return InvalidParams;
+ }
+ else if (value.isMember("seconds"))
+ appPlayer->GetSeekHandler().SeekSeconds(static_cast<int>(value["seconds"].asInteger()));
+ else if (value.isMember("time"))
+ g_application.SeekTime(ParseTimeInSeconds(value["time"]));
+ else
+ return InvalidParams;
+
+ GetPropertyValue(player, "percentage", result["percentage"]);
+ GetPropertyValue(player, "time", result["time"]);
+ GetPropertyValue(player, "totaltime", result["totaltime"]);
+ return OK;
+ }
+
+ case Picture:
+ case None:
+ default:
+ return FailedToExecute;
+ }
+}
+
+JSONRPC_STATUS CPlayerOperations::Move(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string direction = parameterObject["direction"].asString();
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Picture:
+ if (direction == "left")
+ SendSlideshowAction(ACTION_MOVE_LEFT);
+ else if (direction == "right")
+ SendSlideshowAction(ACTION_MOVE_RIGHT);
+ else if (direction == "up")
+ SendSlideshowAction(ACTION_MOVE_UP);
+ else if (direction == "down")
+ SendSlideshowAction(ACTION_MOVE_DOWN);
+ else
+ return InvalidParams;
+
+ return ACK;
+
+ case Video:
+ case Audio:
+ if (direction == "left" || direction == "up")
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1, static_cast<void*>(new CAction(ACTION_PREV_ITEM)));
+ else if (direction == "right" || direction == "down")
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1, static_cast<void*>(new CAction(ACTION_NEXT_ITEM)));
+ else
+ return InvalidParams;
+
+ return ACK;
+
+ case None:
+ default:
+ return FailedToExecute;
+ }
+}
+
+JSONRPC_STATUS CPlayerOperations::Zoom(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVariant zoom = parameterObject["zoom"];
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Picture:
+ if (zoom.isInteger())
+ SendSlideshowAction(ACTION_ZOOM_LEVEL_NORMAL + ((int)zoom.asInteger() - 1));
+ else if (zoom.isString())
+ {
+ std::string strZoom = zoom.asString();
+ if (strZoom == "in")
+ SendSlideshowAction(ACTION_ZOOM_IN);
+ else if (strZoom == "out")
+ SendSlideshowAction(ACTION_ZOOM_OUT);
+ else
+ return InvalidParams;
+ }
+ else
+ return InvalidParams;
+
+ return ACK;
+
+ case Video:
+ case Audio:
+ case None:
+ default:
+ return FailedToExecute;
+ }
+}
+
+// Matching pairs of values from JSON type "Player.ViewMode" and C++ enum ViewMode
+// Additions to enum ViewMode need to be added here and in the JSON type
+std::map<std::string, ViewMode> viewModes =
+{
+ {"normal", ViewModeNormal},
+ {"zoom", ViewModeZoom},
+ {"stretch4x3", ViewModeStretch4x3},
+ {"widezoom", ViewModeWideZoom, },
+ {"stretch16x9", ViewModeStretch16x9},
+ {"original", ViewModeOriginal},
+ {"stretch16x9nonlin", ViewModeStretch16x9Nonlin},
+ {"zoom120width", ViewModeZoom120Width},
+ {"zoom110width", ViewModeZoom110Width}
+};
+
+std::string GetStringFromViewMode(ViewMode viewMode)
+{
+ std::string result = "custom";
+
+ auto it = find_if(viewModes.begin(), viewModes.end(), [viewMode](const std::pair<std::string, ViewMode> & p)
+ {
+ return p.second == viewMode;
+ });
+
+ if (it != viewModes.end())
+ {
+ std::pair<std::string, ViewMode> value = *it;
+ result = value.first;
+ }
+
+ return result;
+}
+
+void GetNewValueForViewModeParameter(const CVariant &parameter, float stepSize, float minValue, float maxValue, float &result)
+{
+ if (parameter.isDouble())
+ {
+ result = parameter.asDouble();
+ }
+ else if (parameter.isString())
+ {
+ if (parameter == "decrease")
+ {
+ stepSize *= -1;
+ }
+
+ result += stepSize;
+ }
+
+ result = std::max(minValue, std::min(result, maxValue));
+}
+
+JSONRPC_STATUS CPlayerOperations::SetViewMode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ JSONRPC_STATUS jsonStatus = InvalidParams;
+ // init with current values from settings
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ ViewMode mode = ViewModeNormal;
+
+ CVariant viewMode = parameterObject["viewmode"];
+ if (viewMode.isString())
+ {
+ std::string modestr = viewMode.asString();
+ if (viewModes.find(modestr) != viewModes.end())
+ {
+ mode = viewModes[modestr];
+ jsonStatus = ACK;
+ }
+ }
+ else if (viewMode.isObject())
+ {
+ mode = ViewModeCustom;
+ CVariant zoom = viewMode["zoom"];
+ CVariant pixelRatio = viewMode["pixelratio"];
+ CVariant verticalShift = viewMode["verticalshift"];
+ CVariant stretch = viewMode["nonlinearstretch"];
+
+ if (!zoom.isNull())
+ {
+ GetNewValueForViewModeParameter(zoom, 0.01f, 0.5f, 2.f, vs.m_CustomZoomAmount);
+ jsonStatus = ACK;
+ }
+
+ if (!pixelRatio.isNull())
+ {
+ GetNewValueForViewModeParameter(pixelRatio, 0.01f, 0.5f, 2.f, vs.m_CustomPixelRatio);
+ jsonStatus = ACK;
+ }
+
+ if (!verticalShift.isNull())
+ {
+ GetNewValueForViewModeParameter(verticalShift, -0.01f, -2.f, 2.f, vs.m_CustomVerticalShift);
+ jsonStatus = ACK;
+ }
+
+ if (stretch.isBoolean())
+ {
+ vs.m_CustomNonLinStretch = stretch.asBoolean();
+ jsonStatus = ACK;
+ }
+ }
+
+ if (jsonStatus == ACK)
+ {
+ appPlayer->SetRenderViewMode(static_cast<int>(mode), vs.m_CustomZoomAmount,
+ vs.m_CustomPixelRatio, vs.m_CustomVerticalShift,
+ vs.m_CustomNonLinStretch);
+ }
+
+ return jsonStatus;
+}
+
+JSONRPC_STATUS CPlayerOperations::GetViewMode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ int mode = appPlayer->GetVideoSettings().m_ViewMode;
+
+ result["viewmode"] = GetStringFromViewMode(static_cast<ViewMode>(mode));
+
+ result["zoom"] = CDisplaySettings::GetInstance().GetZoomAmount();
+ result["pixelratio"] = CDisplaySettings::GetInstance().GetPixelRatio();
+ result["verticalshift"] = CDisplaySettings::GetInstance().GetVerticalShift();
+ result["nonlinearstretch"] = CDisplaySettings::GetInstance().IsNonLinearStretched();
+ return OK;
+}
+
+JSONRPC_STATUS CPlayerOperations::Rotate(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Picture:
+ if (parameterObject["value"].asString().compare("clockwise") == 0)
+ SendSlideshowAction(ACTION_ROTATE_PICTURE_CW);
+ else
+ SendSlideshowAction(ACTION_ROTATE_PICTURE_CCW);
+ return ACK;
+
+ case Video:
+ case Audio:
+ case None:
+ default:
+ return FailedToExecute;
+ }
+}
+
+JSONRPC_STATUS CPlayerOperations::Open(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVariant options = parameterObject["options"];
+ CVariant optionShuffled = options["shuffled"];
+ CVariant optionRepeat = options["repeat"];
+ CVariant optionResume = options["resume"];
+ CVariant optionPlayer = options["playername"];
+
+ if (parameterObject["item"].isMember("playlistid"))
+ {
+ PLAYLIST::Id playlistid = parameterObject["item"]["playlistid"].asInteger();
+
+ if (playlistid == PLAYLIST::TYPE_MUSIC || playlistid == PLAYLIST::TYPE_VIDEO)
+ {
+ // Apply the "shuffled" option if available
+ if (optionShuffled.isBoolean())
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(playlistid, optionShuffled.asBoolean(), false);
+ // Apply the "repeat" option if available
+ if (!optionRepeat.isNull())
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(playlistid, ParseRepeatState(optionRepeat),
+ false);
+ }
+
+ int playlistStartPosition = (int)parameterObject["item"]["position"].asInteger();
+
+ switch (playlistid)
+ {
+ case PLAYLIST::TYPE_MUSIC:
+ case PLAYLIST::TYPE_VIDEO:
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, playlistid,
+ playlistStartPosition);
+ break;
+
+ case PLAYLIST::TYPE_PICTURE:
+ {
+ std::string firstPicturePath;
+ if (playlistStartPosition > 0)
+ {
+ CGUIWindowSlideShow *slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (slideshow != NULL)
+ {
+ CFileItemList list;
+ slideshow->GetSlideShowContents(list);
+ if (playlistStartPosition < list.Size())
+ firstPicturePath = list.Get(playlistStartPosition)->GetPath();
+ }
+ }
+
+ return StartSlideshow("", false, optionShuffled.isBoolean() && optionShuffled.asBoolean(), firstPicturePath);
+ break;
+ }
+ }
+
+ return ACK;
+ }
+ else if (parameterObject["item"].isMember("path"))
+ {
+ bool random = (optionShuffled.isBoolean() && optionShuffled.asBoolean()) ||
+ (!optionShuffled.isBoolean() && parameterObject["item"]["random"].asBoolean());
+ return StartSlideshow(parameterObject["item"]["path"].asString(), parameterObject["item"]["recursive"].asBoolean(), random);
+ }
+ else if (parameterObject["item"].isObject() && parameterObject["item"].isMember("partymode"))
+ {
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Disable();
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ "playercontrol(partymode(" + parameterObject["item"]["partymode"].asString() + "))");
+ return ACK;
+ }
+ else if (parameterObject["item"].isMember("broadcastid"))
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ CServiceBroker::GetPVRManager().EpgContainer().GetTagByDatabaseId(
+ static_cast<unsigned int>(parameterObject["item"]["broadcastid"].asInteger()));
+
+ if (!epgTag || !epgTag->IsPlayable())
+ return InvalidParams;
+
+ if (!CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayEpgTag(CFileItem(epgTag)))
+ return FailedToExecute;
+
+ return ACK;
+ }
+ else if (parameterObject["item"].isMember("channelid"))
+ {
+ const std::shared_ptr<CPVRChannelGroupsContainer> channelGroupContainer = CServiceBroker::GetPVRManager().ChannelGroups();
+ if (!channelGroupContainer)
+ return FailedToExecute;
+
+ const std::shared_ptr<CPVRChannel> channel = channelGroupContainer->GetChannelById(static_cast<int>(parameterObject["item"]["channelid"].asInteger()));
+ if (!channel)
+ return InvalidParams;
+
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(channel);
+ if (!groupMember)
+ return InvalidParams;
+
+ if (!CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayMedia(
+ CFileItem(groupMember)))
+ return FailedToExecute;
+
+ return ACK;
+ }
+ else if (parameterObject["item"].isMember("recordingid"))
+ {
+ const std::shared_ptr<CPVRRecordings> recordingsContainer = CServiceBroker::GetPVRManager().Recordings();
+ if (!recordingsContainer)
+ return FailedToExecute;
+
+ const std::shared_ptr<CPVRRecording> recording = recordingsContainer->GetById(static_cast<int>(parameterObject["item"]["recordingid"].asInteger()));
+ if (!recording)
+ return InvalidParams;
+
+ if (!CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayMedia(CFileItem(recording)))
+ return FailedToExecute;
+
+ return ACK;
+ }
+ else
+ {
+ CFileItemList list;
+ if (FillFileItemList(parameterObject["item"], list) && list.Size() > 0)
+ {
+ bool slideshow = true;
+ for (int index = 0; index < list.Size(); index++)
+ {
+ if (!list[index]->IsPicture())
+ {
+ slideshow = false;
+ break;
+ }
+ }
+
+ if (slideshow)
+ {
+ CGUIWindowSlideShow *slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (!slideshow)
+ return FailedToExecute;
+
+ SendSlideshowAction(ACTION_STOP);
+ slideshow->Reset();
+ for (int index = 0; index < list.Size(); index++)
+ slideshow->Add(list[index].get());
+
+ return StartSlideshow("", false, optionShuffled.isBoolean() && optionShuffled.asBoolean());
+ }
+ else
+ {
+ std::string playername;
+ // Handle the "playerid" option
+ if (!optionPlayer.isNull())
+ {
+ if (optionPlayer.isString())
+ {
+ playername = optionPlayer.asString();
+
+ if (playername != "default")
+ {
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ // check if the there's actually a player with the given name
+ if (playerCoreFactory.GetPlayerType(playername).empty())
+ return InvalidParams;
+
+ // check if the player can handle at least the first item in the list
+ std::vector<std::string> possiblePlayers;
+ playerCoreFactory.GetPlayers(*list.Get(0).get(), possiblePlayers);
+
+ bool match = false;
+ for (const auto& entry : possiblePlayers)
+ {
+ if (StringUtils::EqualsNoCase(entry, playername))
+ {
+ match = true;
+ break;
+ }
+ }
+ if (!match)
+ return InvalidParams;
+ }
+ }
+ else
+ return InvalidParams;
+ }
+
+ // Handle "shuffled" option
+ if (optionShuffled.isBoolean())
+ list.SetProperty("shuffled", optionShuffled);
+ // Handle "repeat" option
+ if (!optionRepeat.isNull())
+ list.SetProperty("repeat", static_cast<int>(ParseRepeatState(optionRepeat)));
+ // Handle "resume" option
+ if (list.Size() == 1)
+ {
+ if (optionResume.isBoolean() && optionResume.asBoolean())
+ list[0]->SetStartOffset(STARTOFFSET_RESUME);
+ else if (optionResume.isDouble())
+ list[0]->SetProperty("StartPercent", optionResume);
+ else if (optionResume.isObject())
+ list[0]->SetStartOffset(
+ CUtil::ConvertSecsToMilliSecs(ParseTimeInSeconds(optionResume)));
+ }
+
+ auto l = new CFileItemList(); //don't delete
+ l->Copy(list);
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, -1, -1, static_cast<void*>(l),
+ playername);
+ }
+
+ return ACK;
+ }
+ else
+ return InvalidParams;
+ }
+
+ return InvalidParams;
+}
+
+JSONRPC_STATUS CPlayerOperations::GoTo(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVariant to = parameterObject["to"];
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Video:
+ case Audio:
+ if (to.isString())
+ {
+ std::string strTo = to.asString();
+ int actionID;
+ if (strTo == "previous")
+ actionID = ACTION_PREV_ITEM;
+ else if (strTo == "next")
+ actionID = ACTION_NEXT_ITEM;
+ else
+ return InvalidParams;
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(actionID)));
+ }
+ else if (to.isInteger())
+ {
+ if (IsPVRChannel())
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(
+ new CAction(ACTION_CHANNEL_SWITCH, static_cast<float>(to.asInteger()))));
+ else
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_PLAY,
+ static_cast<int>(to.asInteger()));
+ }
+ else
+ return InvalidParams;
+ break;
+
+ case Picture:
+ if (to.isString())
+ {
+ std::string strTo = to.asString();
+ int actionID;
+ if (strTo == "previous")
+ actionID = ACTION_PREV_PICTURE;
+ else if (strTo == "next")
+ actionID = ACTION_NEXT_PICTURE;
+ else
+ return InvalidParams;
+
+ SendSlideshowAction(actionID);
+ }
+ else
+ return FailedToExecute;
+ break;
+
+ case None:
+ default:
+ return FailedToExecute;
+ }
+
+ return ACK;
+}
+
+JSONRPC_STATUS CPlayerOperations::SetShuffle(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CGUIWindowSlideShow *slideshow = NULL;
+ CVariant shuffle = parameterObject["shuffle"];
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Video:
+ case Audio:
+ {
+ if (IsPVRChannel())
+ return FailedToExecute;
+
+ PLAYLIST::Id playlistid = GetPlaylist(GetPlayer(parameterObject["playerid"]));
+ if (CServiceBroker::GetPlaylistPlayer().IsShuffled(playlistid))
+ {
+ if ((shuffle.isBoolean() && !shuffle.asBoolean()) ||
+ (shuffle.isString() && shuffle.asString() == "toggle"))
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_SHUFFLE, playlistid, 0);
+ }
+ }
+ else
+ {
+ if ((shuffle.isBoolean() && shuffle.asBoolean()) ||
+ (shuffle.isString() && shuffle.asString() == "toggle"))
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_SHUFFLE, playlistid, 1);
+ }
+ }
+ break;
+ }
+
+ case Picture:
+ slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (slideshow == NULL)
+ return FailedToExecute;
+ if (slideshow->IsShuffled())
+ {
+ if ((shuffle.isBoolean() && !shuffle.asBoolean()) ||
+ (shuffle.isString() && shuffle.asString() == "toggle"))
+ return FailedToExecute;
+ }
+ else
+ {
+ if ((shuffle.isBoolean() && shuffle.asBoolean()) ||
+ (shuffle.isString() && shuffle.asString() == "toggle"))
+ slideshow->Shuffle();
+ }
+ break;
+
+ default:
+ return FailedToExecute;
+ }
+ return ACK;
+}
+
+JSONRPC_STATUS CPlayerOperations::SetRepeat(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Video:
+ case Audio:
+ {
+ if (IsPVRChannel())
+ return FailedToExecute;
+
+ PLAYLIST::RepeatState repeat = PLAYLIST::RepeatState::NONE;
+ PLAYLIST::Id playlistid = GetPlaylist(GetPlayer(parameterObject["playerid"]));
+ if (parameterObject["repeat"].asString() == "cycle")
+ {
+ PLAYLIST::RepeatState repeatPrev =
+ CServiceBroker::GetPlaylistPlayer().GetRepeat(playlistid);
+ if (repeatPrev == PLAYLIST::RepeatState::NONE)
+ repeat = PLAYLIST::RepeatState::ALL;
+ else if (repeatPrev == PLAYLIST::RepeatState::ALL)
+ repeat = PLAYLIST::RepeatState::ONE;
+ else
+ repeat = PLAYLIST::RepeatState::NONE;
+ }
+ else
+ repeat = ParseRepeatState(parameterObject["repeat"]);
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_REPEAT, playlistid,
+ static_cast<int>(repeat));
+ break;
+ }
+
+ case Picture:
+ default:
+ return FailedToExecute;
+ }
+
+ return ACK;
+}
+
+JSONRPC_STATUS CPlayerOperations::SetPartymode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ PlayerType player = GetPlayer(parameterObject["playerid"]);
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ {
+ if (IsPVRChannel())
+ return FailedToExecute;
+
+ bool change = false;
+ PartyModeContext context = PARTYMODECONTEXT_UNKNOWN;
+ std::string strContext;
+ if (player == Video)
+ {
+ context = PARTYMODECONTEXT_VIDEO;
+ strContext = "video";
+ }
+ else if (player == Audio)
+ {
+ context = PARTYMODECONTEXT_MUSIC;
+ strContext = "music";
+ }
+
+ bool toggle = parameterObject["partymode"].isString();
+ if (g_partyModeManager.IsEnabled())
+ {
+ if (g_partyModeManager.GetType() != context)
+ return InvalidParams;
+
+ if (toggle || parameterObject["partymode"].asBoolean() == false)
+ change = true;
+ }
+ else
+ {
+ if (toggle || parameterObject["partymode"].asBoolean() == true)
+ change = true;
+ }
+
+ if (change)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ "playercontrol(partymode(" + strContext + "))");
+ break;
+ }
+
+ case Picture:
+ default:
+ return FailedToExecute;
+ }
+
+ return ACK;
+}
+
+JSONRPC_STATUS CPlayerOperations::SetAudioStream(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Video:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->HasPlayer())
+ {
+ int index = -1;
+ if (parameterObject["stream"].isString())
+ {
+ std::string action = parameterObject["stream"].asString();
+ if (action.compare("previous") == 0)
+ {
+ index = appPlayer->GetAudioStream() - 1;
+ if (index < 0)
+ index = appPlayer->GetAudioStreamCount() - 1;
+ }
+ else if (action.compare("next") == 0)
+ {
+ index = appPlayer->GetAudioStream() + 1;
+ if (index >= appPlayer->GetAudioStreamCount())
+ index = 0;
+ }
+ else
+ return InvalidParams;
+ }
+ else if (parameterObject["stream"].isInteger())
+ index = (int)parameterObject["stream"].asInteger();
+
+ if (index < 0 || appPlayer->GetAudioStreamCount() <= index)
+ return InvalidParams;
+
+ appPlayer->SetAudioStream(index);
+ }
+ else
+ return FailedToExecute;
+ break;
+ }
+
+ case Audio:
+ case Picture:
+ default:
+ return FailedToExecute;
+ }
+
+ return ACK;
+}
+
+JSONRPC_STATUS CPlayerOperations::AddSubtitle(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (GetPlayer(parameterObject["playerid"]) != Video)
+ return FailedToExecute;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ if (!appPlayer->HasPlayer())
+ return FailedToExecute;
+
+ if (!parameterObject["subtitle"].isString())
+ return FailedToExecute;
+
+ std::string sub = parameterObject["subtitle"].asString();
+ appPlayer->AddSubtitle(sub);
+ return ACK;
+}
+
+JSONRPC_STATUS CPlayerOperations::SetSubtitle(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Video:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->HasPlayer())
+ {
+ int index = -1;
+ if (parameterObject["subtitle"].isString())
+ {
+ std::string action = parameterObject["subtitle"].asString();
+ if (action.compare("previous") == 0)
+ {
+ index = appPlayer->GetSubtitle() - 1;
+ if (index < 0)
+ index = appPlayer->GetSubtitleCount() - 1;
+ }
+ else if (action.compare("next") == 0)
+ {
+ index = appPlayer->GetSubtitle() + 1;
+ if (index >= appPlayer->GetSubtitleCount())
+ index = 0;
+ }
+ else if (action.compare("off") == 0)
+ {
+ appPlayer->SetSubtitleVisible(false);
+ return ACK;
+ }
+ else if (action.compare("on") == 0)
+ {
+ appPlayer->SetSubtitleVisible(true);
+ return ACK;
+ }
+ else
+ return InvalidParams;
+ }
+ else if (parameterObject["subtitle"].isInteger())
+ index = (int)parameterObject["subtitle"].asInteger();
+
+ if (index < 0 || appPlayer->GetSubtitleCount() <= index)
+ return InvalidParams;
+
+ appPlayer->SetSubtitle(index);
+
+ // Check if we need to enable subtitles to be displayed
+ if (parameterObject["enable"].asBoolean() && !appPlayer->GetSubtitleVisible())
+ appPlayer->SetSubtitleVisible(true);
+ }
+ else
+ return FailedToExecute;
+ break;
+ }
+
+ case Audio:
+ case Picture:
+ default:
+ return FailedToExecute;
+ }
+
+ return ACK;
+}
+
+JSONRPC_STATUS CPlayerOperations::SetVideoStream(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ switch (GetPlayer(parameterObject["playerid"]))
+ {
+ case Video:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ int streamCount = appPlayer->GetVideoStreamCount();
+ if (streamCount > 0)
+ {
+ int index = appPlayer->GetVideoStream();
+ if (parameterObject["stream"].isString())
+ {
+ std::string action = parameterObject["stream"].asString();
+ if (action.compare("previous") == 0)
+ {
+ index--;
+ if (index < 0)
+ index = streamCount - 1;
+ }
+ else if (action.compare("next") == 0)
+ {
+ index++;
+ if (index >= streamCount)
+ index = 0;
+ }
+ else
+ return InvalidParams;
+ }
+ else if (parameterObject["stream"].isInteger())
+ index = (int)parameterObject["stream"].asInteger();
+
+ if (index < 0 || streamCount <= index)
+ return InvalidParams;
+
+ appPlayer->SetVideoStream(index);
+ }
+ else
+ return FailedToExecute;
+ break;
+ }
+ case Audio:
+ case Picture:
+ default:
+ return FailedToExecute;
+ }
+
+ return ACK;
+}
+
+int CPlayerOperations::GetActivePlayers()
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ int activePlayers = 0;
+ if (appPlayer->IsPlayingVideo() ||
+ CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV() ||
+ CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRecording())
+ activePlayers |= Video;
+ if (appPlayer->IsPlayingAudio() ||
+ CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio())
+ activePlayers |= Audio;
+ if (CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_SLIDESHOW))
+ activePlayers |= Picture;
+ if (appPlayer->IsExternalPlaying())
+ activePlayers |= External;
+ if (appPlayer->IsRemotePlaying())
+ activePlayers |= Remote;
+
+ return activePlayers;
+}
+
+PlayerType CPlayerOperations::GetPlayer(const CVariant &player)
+{
+ PLAYLIST::Id playerPlaylistId = player.asInteger();
+ PlayerType playerID;
+
+ switch (playerPlaylistId)
+ {
+ case PLAYLIST::TYPE_VIDEO:
+ playerID = Video;
+ break;
+
+ case PLAYLIST::TYPE_MUSIC:
+ playerID = Audio;
+ break;
+
+ case PLAYLIST::TYPE_PICTURE:
+ playerID = Picture;
+ break;
+
+ default:
+ playerID = None;
+ break;
+ }
+
+ if (GetPlaylist(playerID) == playerPlaylistId)
+ return playerID;
+ else
+ return None;
+}
+
+PLAYLIST::Id CPlayerOperations::GetPlaylist(PlayerType player)
+{
+ PLAYLIST::Id playlistId = CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist();
+ if (playlistId == PLAYLIST::TYPE_NONE) // No active playlist, try guessing
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ playlistId = appPlayer->GetPreferredPlaylist();
+ }
+
+ switch (player)
+ {
+ case Video:
+ return playlistId == PLAYLIST::TYPE_NONE ? PLAYLIST::TYPE_VIDEO : playlistId;
+
+ case Audio:
+ return playlistId == PLAYLIST::TYPE_NONE ? PLAYLIST::TYPE_MUSIC : playlistId;
+
+ case Picture:
+ return PLAYLIST::TYPE_PICTURE;
+
+ default:
+ return playlistId;
+ }
+}
+
+JSONRPC_STATUS CPlayerOperations::StartSlideshow(const std::string& path, bool recursive, bool random, const std::string &firstPicturePath /* = "" */)
+{
+ int flags = 0;
+ if (recursive)
+ flags |= 1;
+ if (random)
+ flags |= 2;
+ else
+ flags |= 4;
+
+ std::vector<std::string> params;
+ params.push_back(path);
+ if (!firstPicturePath.empty())
+ params.push_back(firstPicturePath);
+
+ // Reset screensaver when started from JSON only to avoid potential conflict with slideshow screensavers
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+ appPower->WakeUpScreenSaverAndDPMS();
+ CGUIMessage msg(GUI_MSG_START_SLIDESHOW, 0, 0, flags);
+ msg.SetStringParams(params);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, WINDOW_SLIDESHOW);
+
+ return ACK;
+}
+
+void CPlayerOperations::SendSlideshowAction(int actionID)
+{
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(actionID)));
+}
+
+JSONRPC_STATUS CPlayerOperations::GetPropertyValue(PlayerType player, const std::string &property, CVariant &result)
+{
+ if (player == None)
+ return FailedToExecute;
+
+ PLAYLIST::Id playlistId = GetPlaylist(player);
+
+ if (property == "type")
+ {
+ switch (player)
+ {
+ case Video:
+ result = "video";
+ break;
+
+ case Audio:
+ result = "audio";
+ break;
+
+ case Picture:
+ result = "picture";
+ break;
+
+ default:
+ return FailedToExecute;
+ }
+ }
+ else if (property == "partymode")
+ {
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ if (IsPVRChannel())
+ {
+ result = false;
+ break;
+ }
+
+ result = g_partyModeManager.IsEnabled();
+ break;
+
+ case Picture:
+ result = false;
+ break;
+
+ default:
+ return FailedToExecute;
+ }
+ }
+ else if (property == "speed")
+ {
+ CGUIWindowSlideShow *slideshow = NULL;
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ result = appPlayer->IsPausedPlayback() ? 0 : (int)lrint(appPlayer->GetPlaySpeed());
+ break;
+ }
+
+ case Picture:
+ slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (slideshow && slideshow->IsPlaying() && !slideshow->IsPaused())
+ result = slideshow->GetDirection();
+ else
+ result = 0;
+ break;
+
+ default:
+ return FailedToExecute;
+ }
+ }
+ else if (property == "time")
+ {
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ {
+ int ms = 0;
+ if (!IsPVRChannel())
+ ms = (int)(g_application.GetTime() * 1000.0);
+ else
+ {
+ std::shared_ptr<CPVREpgInfoTag> epg(GetCurrentEpg());
+ if (epg)
+ ms = epg->Progress() * 1000;
+ }
+
+ MillisecondsToTimeObject(ms, result);
+ break;
+ }
+
+ case Picture:
+ MillisecondsToTimeObject(0, result);
+ break;
+
+ default:
+ return FailedToExecute;
+ }
+ }
+ else if (property == "percentage")
+ {
+ CGUIWindowSlideShow *slideshow = NULL;
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ {
+ if (!IsPVRChannel())
+ result = g_application.GetPercentage();
+ else
+ {
+ std::shared_ptr<CPVREpgInfoTag> epg(GetCurrentEpg());
+ if (epg)
+ result = epg->ProgressPercentage();
+ else
+ result = 0;
+ }
+ break;
+ }
+
+ case Picture:
+ slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (slideshow && slideshow->NumSlides() > 0)
+ result = (double)slideshow->CurrentSlide() / slideshow->NumSlides();
+ else
+ result = 0.0;
+ break;
+
+ default:
+ return FailedToExecute;
+ }
+ }
+ else if (property == "cachepercentage")
+ {
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ {
+ result = g_application.GetCachePercentage();
+ break;
+ }
+
+ case Picture:
+ {
+ result = 0.0;
+ break;
+ }
+
+ default:
+ return FailedToExecute;
+ }
+ }
+ else if (property == "totaltime")
+ {
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ {
+ int ms = 0;
+ if (!IsPVRChannel())
+ ms = (int)(g_application.GetTotalTime() * 1000.0);
+ else
+ {
+ std::shared_ptr<CPVREpgInfoTag> epg(GetCurrentEpg());
+ if (epg)
+ ms = epg->GetDuration() * 1000;
+ }
+
+ MillisecondsToTimeObject(ms, result);
+ break;
+ }
+
+ case Picture:
+ MillisecondsToTimeObject(0, result);
+ break;
+
+ default:
+ return FailedToExecute;
+ }
+ }
+ else if (property == "playlistid")
+ {
+ result = playlistId;
+ }
+ else if (property == "position")
+ {
+ CGUIWindowSlideShow *slideshow = NULL;
+ switch (player)
+ {
+ case Video:
+ case Audio: /* Return the position of current item if there is an active playlist */
+ if (!IsPVRChannel() &&
+ CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == playlistId)
+ {
+ result = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ }
+ else
+ result = -1;
+ break;
+
+ case Picture:
+ slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (slideshow && slideshow->IsPlaying())
+ result = slideshow->CurrentSlide() - 1;
+ else
+ result = -1;
+ break;
+
+ default:
+ result = -1;
+ break;
+ }
+ }
+ else if (property == "repeat")
+ {
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ if (IsPVRChannel())
+ {
+ result = "off";
+ break;
+ }
+
+ switch (CServiceBroker::GetPlaylistPlayer().GetRepeat(playlistId))
+ {
+ case PLAYLIST::RepeatState::ONE:
+ result = "one";
+ break;
+ case PLAYLIST::RepeatState::ALL:
+ result = "all";
+ break;
+ default:
+ result = "off";
+ break;
+ }
+ break;
+
+ case Picture:
+ default:
+ result = "off";
+ break;
+ }
+ }
+ else if (property == "shuffled")
+ {
+ CGUIWindowSlideShow *slideshow = NULL;
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ if (IsPVRChannel())
+ {
+ result = false;
+ break;
+ }
+
+ result = CServiceBroker::GetPlaylistPlayer().IsShuffled(playlistId);
+ break;
+
+ case Picture:
+ slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (slideshow && slideshow->IsPlaying())
+ result = slideshow->IsShuffled();
+ else
+ result = -1;
+ break;
+
+ default:
+ result = -1;
+ break;
+ }
+ }
+ else if (property == "canseek")
+ {
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ result = appPlayer->CanSeek();
+ break;
+ }
+
+ case Picture:
+ default:
+ result = false;
+ break;
+ }
+ }
+ else if (property == "canchangespeed")
+ {
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ result = !IsPVRChannel();
+ break;
+
+ case Picture:
+ default:
+ result = false;
+ break;
+ }
+ }
+ else if (property == "canmove")
+ {
+ switch (player)
+ {
+ case Picture:
+ result = true;
+ break;
+
+ case Video:
+ case Audio:
+ default:
+ result = false;
+ break;
+ }
+ }
+ else if (property == "canzoom")
+ {
+ switch (player)
+ {
+ case Picture:
+ result = true;
+ break;
+
+ case Video:
+ case Audio:
+ default:
+ result = false;
+ break;
+ }
+ }
+ else if (property == "canrotate")
+ {
+ switch (player)
+ {
+ case Picture:
+ result = true;
+ break;
+
+ case Video:
+ case Audio:
+ default:
+ result = false;
+ break;
+ }
+ }
+ else if (property == "canshuffle")
+ {
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ case Picture:
+ result = !IsPVRChannel();
+ break;
+
+ default:
+ result = false;
+ break;
+ }
+ }
+ else if (property == "canrepeat")
+ {
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ result = !IsPVRChannel();
+ break;
+
+ case Picture:
+ default:
+ result = false;
+ break;
+ }
+ }
+ else if (property == "currentaudiostream")
+ {
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->HasPlayer())
+ {
+ result = CVariant(CVariant::VariantTypeObject);
+ int index = appPlayer->GetAudioStream();
+ if (index >= 0)
+ {
+ AudioStreamInfo info;
+ appPlayer->GetAudioStreamInfo(index, info);
+
+ result["index"] = index;
+ result["name"] = info.name;
+ result["language"] = info.language;
+ result["codec"] = info.codecName;
+ result["bitrate"] = info.bitrate;
+ result["channels"] = info.channels;
+ result["samplerate"] = info.samplerate;
+ AppendAudioStreamFlagsAsBooleans(result, info.flags);
+ }
+ }
+ else
+ result = CVariant(CVariant::VariantTypeNull);
+ break;
+ }
+
+ case Picture:
+ default:
+ result = CVariant(CVariant::VariantTypeNull);
+ break;
+ }
+ }
+ else if (property == "audiostreams")
+ {
+ result = CVariant(CVariant::VariantTypeArray);
+ switch (player)
+ {
+ case Video:
+ case Audio:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->HasPlayer())
+ {
+ for (int index = 0; index < appPlayer->GetAudioStreamCount(); index++)
+ {
+ AudioStreamInfo info;
+ appPlayer->GetAudioStreamInfo(index, info);
+
+ CVariant audioStream(CVariant::VariantTypeObject);
+ audioStream["index"] = index;
+ audioStream["name"] = info.name;
+ audioStream["language"] = info.language;
+ audioStream["codec"] = info.codecName;
+ audioStream["bitrate"] = info.bitrate;
+ audioStream["channels"] = info.channels;
+ audioStream["samplerate"] = info.samplerate;
+ AppendAudioStreamFlagsAsBooleans(audioStream, info.flags);
+
+ result.append(audioStream);
+ }
+ }
+ break;
+ }
+
+ case Picture:
+ default:
+ break;
+ }
+ }
+ else if (property == "currentvideostream")
+ {
+ switch (player)
+ {
+ case Video:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ int index = appPlayer->GetVideoStream();
+ if (index >= 0)
+ {
+ result = CVariant(CVariant::VariantTypeObject);
+ VideoStreamInfo info;
+ appPlayer->GetVideoStreamInfo(index, info);
+
+ result["index"] = index;
+ result["name"] = info.name;
+ result["language"] = info.language;
+ result["codec"] = info.codecName;
+ result["width"] = info.width;
+ result["height"] = info.height;
+ }
+ else
+ result = CVariant(CVariant::VariantTypeNull);
+ break;
+ }
+ case Audio:
+ case Picture:
+ default:
+ result = CVariant(CVariant::VariantTypeNull);
+ break;
+ }
+ }
+ else if (property == "videostreams")
+ {
+ result = CVariant(CVariant::VariantTypeArray);
+ switch (player)
+ {
+ case Video:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ int streamCount = appPlayer->GetVideoStreamCount();
+ if (streamCount >= 0)
+ {
+ for (int index = 0; index < streamCount; ++index)
+ {
+ VideoStreamInfo info;
+ appPlayer->GetVideoStreamInfo(index, info);
+
+ CVariant videoStream(CVariant::VariantTypeObject);
+ videoStream["index"] = index;
+ videoStream["name"] = info.name;
+ videoStream["language"] = info.language;
+ videoStream["codec"] = info.codecName;
+ videoStream["width"] = info.width;
+ videoStream["height"] = info.height;
+
+ result.append(videoStream);
+ }
+ }
+ break;
+ }
+ case Audio:
+ case Picture:
+ default:
+ break;
+ }
+ }
+ else if (property == "subtitleenabled")
+ {
+ switch (player)
+ {
+ case Video:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ result = appPlayer->GetSubtitleVisible();
+ break;
+ }
+
+ case Audio:
+ case Picture:
+ default:
+ result = false;
+ break;
+ }
+ }
+ else if (property == "currentsubtitle")
+ {
+ switch (player)
+ {
+ case Video:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->HasPlayer())
+ {
+ result = CVariant(CVariant::VariantTypeObject);
+ int index = appPlayer->GetSubtitle();
+ if (index >= 0)
+ {
+ SubtitleStreamInfo info;
+ appPlayer->GetSubtitleStreamInfo(index, info);
+
+ result["index"] = index;
+ result["name"] = info.name;
+ result["language"] = info.language;
+ AppendSubtitleStreamFlagsAsBooleans(result, info.flags);
+ }
+ }
+ else
+ result = CVariant(CVariant::VariantTypeNull);
+ break;
+ }
+
+ case Audio:
+ case Picture:
+ default:
+ result = CVariant(CVariant::VariantTypeNull);
+ break;
+ }
+ }
+ else if (property == "subtitles")
+ {
+ result = CVariant(CVariant::VariantTypeArray);
+ switch (player)
+ {
+ case Video:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->HasPlayer())
+ {
+ for (int index = 0; index < appPlayer->GetSubtitleCount(); index++)
+ {
+ SubtitleStreamInfo info;
+ appPlayer->GetSubtitleStreamInfo(index, info);
+
+ CVariant subtitle(CVariant::VariantTypeObject);
+ subtitle["index"] = index;
+ subtitle["name"] = info.name;
+ subtitle["language"] = info.language;
+ AppendSubtitleStreamFlagsAsBooleans(subtitle, info.flags);
+
+ result.append(subtitle);
+ }
+ }
+ break;
+ }
+
+ case Audio:
+ case Picture:
+ default:
+ break;
+ }
+ }
+ else if (property == "live")
+ result = IsPVRChannel();
+ else
+ return InvalidParams;
+
+ return OK;
+}
+
+PLAYLIST::RepeatState CPlayerOperations::ParseRepeatState(const CVariant& repeat)
+{
+ PLAYLIST::RepeatState state = PLAYLIST::RepeatState::NONE;
+ std::string strState = repeat.asString();
+
+ if (strState.compare("one") == 0)
+ state = PLAYLIST::RepeatState::ONE;
+ else if (strState.compare("all") == 0)
+ state = PLAYLIST::RepeatState::ALL;
+
+ return state;
+}
+
+double CPlayerOperations::ParseTimeInSeconds(const CVariant &time)
+{
+ double seconds = 0.0;
+ if (time.isMember("hours"))
+ seconds += time["hours"].asInteger() * 60 * 60;
+ if (time.isMember("minutes"))
+ seconds += time["minutes"].asInteger() * 60;
+ if (time.isMember("seconds"))
+ seconds += time["seconds"].asInteger();
+ if (time.isMember("milliseconds"))
+ seconds += time["milliseconds"].asDouble() / 1000.0;
+
+ return seconds;
+}
+
+bool CPlayerOperations::IsPVRChannel()
+{
+ const std::shared_ptr<CPVRPlaybackState> state = CServiceBroker::GetPVRManager().PlaybackState();
+ return state->IsPlayingTV() || state->IsPlayingRadio();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPlayerOperations::GetCurrentEpg()
+{
+ const std::shared_ptr<CPVRPlaybackState> state = CServiceBroker::GetPVRManager().PlaybackState();
+ if (!state->IsPlayingTV() && !state->IsPlayingRadio())
+ return {};
+
+ const std::shared_ptr<CPVRChannel> currentChannel = state->GetPlayingChannel();
+ if (!currentChannel)
+ return {};
+
+ return currentChannel->GetEPGNow();
+}
diff --git a/xbmc/interfaces/json-rpc/PlayerOperations.h b/xbmc/interfaces/json-rpc/PlayerOperations.h
new file mode 100644
index 0000000..b681bec
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/PlayerOperations.h
@@ -0,0 +1,96 @@
+/*
+ * 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 "FileItemHandler.h"
+#include "JSONRPC.h"
+
+#include <string>
+
+class CVariant;
+
+namespace PVR
+{
+class CPVRChannelGroup;
+class CPVREpgInfoTag;
+}
+
+namespace PLAYLIST
+{
+using Id = int;
+enum class RepeatState;
+} // namespace PLAYLIST
+
+namespace JSONRPC
+{
+ enum PlayerType
+ {
+ None = 0,
+ Video = 0x1,
+ Audio = 0x2,
+ Picture = 0x4,
+ External = 0x8,
+ Remote = 0x10
+ };
+
+ static const int PlayerImplicit = (Video | Audio | Picture);
+
+ class CPlayerOperations : CFileItemHandler
+ {
+ public:
+ static JSONRPC_STATUS GetActivePlayers(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetPlayers(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetItem(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS PlayPause(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Stop(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetAudioDelay(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result);
+ static JSONRPC_STATUS SetAudioDelay(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result);
+ static JSONRPC_STATUS SetSpeed(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Seek(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS Move(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Zoom(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetViewMode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetViewMode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Rotate(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS Open(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GoTo(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetShuffle(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetRepeat(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetPartymode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS SetAudioStream(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS AddSubtitle(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetSubtitle(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetVideoStream(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ private:
+ static int GetActivePlayers();
+ static PlayerType GetPlayer(const CVariant &player);
+ static PLAYLIST::Id GetPlaylist(PlayerType player);
+ static JSONRPC_STATUS StartSlideshow(const std::string& path, bool recursive, bool random, const std::string &firstPicturePath = "");
+ static void SendSlideshowAction(int actionID);
+ static JSONRPC_STATUS GetPropertyValue(PlayerType player, const std::string &property, CVariant &result);
+
+ static PLAYLIST::RepeatState ParseRepeatState(const CVariant& repeat);
+ static double ParseTimeInSeconds(const CVariant &time);
+ static bool IsPVRChannel();
+ static std::shared_ptr<PVR::CPVREpgInfoTag> GetCurrentEpg();
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/PlaylistOperations.cpp b/xbmc/interfaces/json-rpc/PlaylistOperations.cpp
new file mode 100644
index 0000000..b98540a
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/PlaylistOperations.cpp
@@ -0,0 +1,323 @@
+/*
+ * 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 "PlaylistOperations.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/Key.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pictures/GUIWindowSlideShow.h"
+#include "pictures/PictureInfoTag.h"
+#include "utils/Variant.h"
+
+using namespace JSONRPC;
+
+JSONRPC_STATUS CPlaylistOperations::GetPlaylists(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ result = CVariant(CVariant::VariantTypeArray);
+ CVariant playlist = CVariant(CVariant::VariantTypeObject);
+
+ playlist["playlistid"] = PLAYLIST::TYPE_MUSIC;
+ playlist["type"] = "audio";
+ result.append(playlist);
+
+ playlist["playlistid"] = PLAYLIST::TYPE_VIDEO;
+ playlist["type"] = "video";
+ result.append(playlist);
+
+ playlist["playlistid"] = PLAYLIST::TYPE_PICTURE;
+ playlist["type"] = "picture";
+ result.append(playlist);
+
+ return OK;
+}
+
+JSONRPC_STATUS CPlaylistOperations::GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ PLAYLIST::Id playlistId = GetPlaylist(parameterObject["playlistid"]);
+ for (unsigned int index = 0; index < parameterObject["properties"].size(); index++)
+ {
+ std::string propertyName = parameterObject["properties"][index].asString();
+ CVariant property;
+ JSONRPC_STATUS ret;
+ if ((ret = GetPropertyValue(playlistId, propertyName, property)) != OK)
+ return ret;
+
+ result[propertyName] = property;
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CPlaylistOperations::GetItems(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CFileItemList list;
+ PLAYLIST::Id playlistId = GetPlaylist(parameterObject["playlistid"]);
+
+ CGUIWindowSlideShow *slideshow = NULL;
+ switch (playlistId)
+ {
+ case PLAYLIST::TYPE_VIDEO:
+ case PLAYLIST::TYPE_MUSIC:
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_GET_ITEMS, playlistId, -1,
+ static_cast<void*>(&list));
+ break;
+
+ case PLAYLIST::TYPE_PICTURE:
+ slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (slideshow)
+ slideshow->GetSlideShowContents(list);
+ break;
+ }
+
+ HandleFileItemList("id", true, "items", list, parameterObject, result);
+
+ return OK;
+}
+
+bool CPlaylistOperations::CheckMediaParameter(PLAYLIST::Id playlistId, const CVariant& itemObject)
+{
+ if (itemObject.isMember("media") && itemObject["media"].asString().compare("files") != 0)
+ {
+ if (playlistId == PLAYLIST::TYPE_VIDEO && itemObject["media"].asString().compare("video") != 0)
+ return false;
+ if (playlistId == PLAYLIST::TYPE_MUSIC && itemObject["media"].asString().compare("music") != 0)
+ return false;
+ if (playlistId == PLAYLIST::TYPE_PICTURE &&
+ itemObject["media"].asString().compare("video") != 0 &&
+ itemObject["media"].asString().compare("pictures") != 0)
+ return false;
+ }
+ return true;
+}
+
+JSONRPC_STATUS CPlaylistOperations::Add(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ PLAYLIST::Id playlistId = GetPlaylist(parameterObject["playlistid"]);
+
+ CGUIWindowSlideShow *slideshow = NULL;
+ if (playlistId == PLAYLIST::TYPE_PICTURE)
+ {
+ slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (slideshow == NULL)
+ return FailedToExecute;
+ }
+
+ CFileItemList list;
+ if (!HandleItemsParameter(playlistId, parameterObject["item"], list))
+ return InvalidParams;
+
+ switch (playlistId)
+ {
+ case PLAYLIST::TYPE_VIDEO:
+ case PLAYLIST::TYPE_MUSIC:
+ {
+ auto tmpList = new CFileItemList();
+ tmpList->Copy(list);
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_PLAYLISTPLAYER_ADD, playlistId, -1,
+ static_cast<void*>(tmpList));
+ break;
+ }
+ case PLAYLIST::TYPE_PICTURE:
+ for (int index = 0; index < list.Size(); index++)
+ {
+ CPictureInfoTag picture = CPictureInfoTag();
+ if (!picture.Load(list[index]->GetPath()))
+ continue;
+
+ *list[index]->GetPictureInfoTag() = picture;
+ slideshow->Add(list[index].get());
+ }
+ break;
+
+ default:
+ return InvalidParams;
+ }
+
+ return ACK;
+}
+
+JSONRPC_STATUS CPlaylistOperations::Insert(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ PLAYLIST::Id playlistId = GetPlaylist(parameterObject["playlistid"]);
+ if (playlistId == PLAYLIST::TYPE_PICTURE)
+ return FailedToExecute;
+
+ CFileItemList list;
+ if (!HandleItemsParameter(playlistId, parameterObject["item"], list))
+ return InvalidParams;
+
+ auto tmpList = new CFileItemList();
+ tmpList->Copy(list);
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_PLAYLISTPLAYER_INSERT, playlistId,
+ static_cast<int>(parameterObject["position"].asInteger()), static_cast<void*>(tmpList));
+
+ return ACK;
+}
+
+JSONRPC_STATUS CPlaylistOperations::Remove(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ PLAYLIST::Id playlistId = GetPlaylist(parameterObject["playlistid"]);
+ if (playlistId == PLAYLIST::TYPE_PICTURE)
+ return FailedToExecute;
+
+ int position = (int)parameterObject["position"].asInteger();
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == playlistId &&
+ CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == position)
+ return InvalidParams;
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_PLAYLISTPLAYER_REMOVE, playlistId, position);
+
+ return ACK;
+}
+
+JSONRPC_STATUS CPlaylistOperations::Clear(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ PLAYLIST::Id playlistId = GetPlaylist(parameterObject["playlistid"]);
+ CGUIWindowSlideShow *slideshow = NULL;
+ switch (playlistId)
+ {
+ case PLAYLIST::TYPE_MUSIC:
+ case PLAYLIST::TYPE_VIDEO:
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_PLAYLISTPLAYER_CLEAR, playlistId);
+ break;
+
+ case PLAYLIST::TYPE_PICTURE:
+ slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (!slideshow)
+ return FailedToExecute;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_STOP)));
+ slideshow->Reset();
+ break;
+ }
+
+ return ACK;
+}
+
+JSONRPC_STATUS CPlaylistOperations::Swap(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ PLAYLIST::Id playlistId = GetPlaylist(parameterObject["playlistid"]);
+ if (playlistId == PLAYLIST::TYPE_PICTURE)
+ return FailedToExecute;
+
+ auto tmpVec = new std::vector<int>();
+ tmpVec->push_back(static_cast<int>(parameterObject["position1"].asInteger()));
+ tmpVec->push_back(static_cast<int>(parameterObject["position2"].asInteger()));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_PLAYLISTPLAYER_SWAP, playlistId, -1,
+ static_cast<void*>(tmpVec));
+
+ return ACK;
+}
+
+PLAYLIST::Id CPlaylistOperations::GetPlaylist(const CVariant& playlist)
+{
+ PLAYLIST::Id playlistId = playlist.asInteger(PLAYLIST::TYPE_NONE);
+ if (playlistId != PLAYLIST::TYPE_NONE)
+ return playlistId;
+
+ return PLAYLIST::TYPE_NONE;
+}
+
+JSONRPC_STATUS CPlaylistOperations::GetPropertyValue(PLAYLIST::Id playlistId,
+ const std::string& property,
+ CVariant& result)
+{
+ if (property == "type")
+ {
+ switch (playlistId)
+ {
+ case PLAYLIST::TYPE_MUSIC:
+ result = "audio";
+ break;
+
+ case PLAYLIST::TYPE_VIDEO:
+ result = "video";
+ break;
+
+ case PLAYLIST::TYPE_PICTURE:
+ result = "pictures";
+ break;
+
+ default:
+ result = "unknown";
+ break;
+ }
+ }
+ else if (property == "size")
+ {
+ CFileItemList list;
+ CGUIWindowSlideShow *slideshow = NULL;
+ switch (playlistId)
+ {
+ case PLAYLIST::TYPE_MUSIC:
+ case PLAYLIST::TYPE_VIDEO:
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_GET_ITEMS, playlistId, -1,
+ static_cast<void*>(&list));
+ result = list.Size();
+ break;
+
+ case PLAYLIST::TYPE_PICTURE:
+ slideshow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (slideshow)
+ result = slideshow->NumSlides();
+ else
+ result = 0;
+ break;
+
+ default:
+ result = 0;
+ break;
+ }
+ }
+ else
+ return InvalidParams;
+
+ return OK;
+}
+
+bool CPlaylistOperations::HandleItemsParameter(PLAYLIST::Id playlistId,
+ const CVariant& itemParam,
+ CFileItemList& items)
+{
+ std::vector<CVariant> vecItems;
+ if (itemParam.isArray())
+ vecItems.assign(itemParam.begin_array(), itemParam.end_array());
+ else
+ vecItems.push_back(itemParam);
+
+ bool success = false;
+ for (auto& itemIt : vecItems)
+ {
+ if (!CheckMediaParameter(playlistId, itemIt))
+ continue;
+
+ switch (playlistId)
+ {
+ case PLAYLIST::TYPE_VIDEO:
+ itemIt["media"] = "video";
+ break;
+ case PLAYLIST::TYPE_MUSIC:
+ itemIt["media"] = "music";
+ break;
+ case PLAYLIST::TYPE_PICTURE:
+ itemIt["media"] = "pictures";
+ break;
+ }
+
+ success |= FillFileItemList(itemIt, items);
+ }
+
+ return success;
+}
diff --git a/xbmc/interfaces/json-rpc/PlaylistOperations.h b/xbmc/interfaces/json-rpc/PlaylistOperations.h
new file mode 100644
index 0000000..a6a6f83
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/PlaylistOperations.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "FileItemHandler.h"
+#include "JSONRPC.h"
+
+class CFileItemList;
+class CVariant;
+
+namespace PLAYLIST
+{
+using Id = int;
+} // namespace PLAYLIST
+
+namespace JSONRPC
+{
+ class CPlaylistOperations : public CFileItemHandler
+ {
+ public:
+ static JSONRPC_STATUS GetPlaylists(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS GetItems(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Add(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Remove(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Insert(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Clear(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Swap(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ private:
+ static PLAYLIST::Id GetPlaylist(const CVariant& playlist);
+ static JSONRPC_STATUS GetPropertyValue(PLAYLIST::Id playlistId,
+ const std::string& property,
+ CVariant& result);
+ static bool CheckMediaParameter(PLAYLIST::Id playlistId, const CVariant& itemObject);
+ static bool HandleItemsParameter(PLAYLIST::Id playlistId,
+ const CVariant& itemParam,
+ CFileItemList& items);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/ProfilesOperations.cpp b/xbmc/interfaces/json-rpc/ProfilesOperations.cpp
new file mode 100644
index 0000000..adfb010
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/ProfilesOperations.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2013-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 "ProfilesOperations.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/Variant.h"
+
+using namespace JSONRPC;
+using KODI::UTILITY::CDigest;
+
+JSONRPC_STATUS CProfilesOperations::GetProfiles(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ CFileItemList listItems;
+
+ for (unsigned int i = 0; i < profileManager->GetNumberOfProfiles(); ++i)
+ {
+ const CProfile *profile = profileManager->GetProfile(i);
+ CFileItemPtr item(new CFileItem(profile->getName()));
+ item->SetArt("thumb", profile->getThumb());
+ listItems.Add(item);
+ }
+
+ HandleFileItemList("profileid", false, "profiles", listItems, parameterObject, result);
+
+ for (CVariant::const_iterator_array propertyiter = parameterObject["properties"].begin_array(); propertyiter != parameterObject["properties"].end_array(); ++propertyiter)
+ {
+ if (propertyiter->isString() &&
+ propertyiter->asString() == "lockmode")
+ {
+ for (CVariant::iterator_array profileiter = result["profiles"].begin_array(); profileiter != result["profiles"].end_array(); ++profileiter)
+ {
+ std::string profilename = (*profileiter)["label"].asString();
+ int index = profileManager->GetProfileIndex(profilename);
+ const CProfile *profile = profileManager->GetProfile(index);
+ LockType locktype = LOCK_MODE_UNKNOWN;
+ if (index == 0)
+ locktype = profileManager->GetMasterProfile().getLockMode();
+ else
+ locktype = profile->getLockMode();
+ (*profileiter)["lockmode"] = locktype;
+ }
+ break;
+ }
+ }
+ return OK;
+}
+
+JSONRPC_STATUS CProfilesOperations::GetCurrentProfile(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ const CProfile& currentProfile = profileManager->GetCurrentProfile();
+ CVariant profileVariant = CVariant(CVariant::VariantTypeObject);
+ profileVariant["label"] = currentProfile.getName();
+ for (CVariant::const_iterator_array propertyiter = parameterObject["properties"].begin_array(); propertyiter != parameterObject["properties"].end_array(); ++propertyiter)
+ {
+ if (propertyiter->isString())
+ {
+ if (propertyiter->asString() == "lockmode")
+ profileVariant["lockmode"] = currentProfile.getLockMode();
+ else if (propertyiter->asString() == "thumbnail")
+ profileVariant["thumbnail"] = currentProfile.getThumb();
+ }
+ }
+
+ result = profileVariant;
+
+ return OK;
+}
+
+JSONRPC_STATUS CProfilesOperations::LoadProfile(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::string profilename = parameterObject["profile"].asString();
+ int index = profileManager->GetProfileIndex(profilename);
+
+ if (index < 0)
+ return InvalidParams;
+
+ // get the profile
+ const CProfile *profile = profileManager->GetProfile(index);
+ if (profile == NULL)
+ return InvalidParams;
+
+ bool bPrompt = parameterObject["prompt"].asBoolean();
+ bool bCanceled = false;
+ bool bLoadProfile = false;
+
+ // if the profile does not require a password or
+ // the user is prompted and provides the correct password
+ // we can load the requested profile
+ if (profile->getLockMode() == LOCK_MODE_EVERYONE ||
+ (bPrompt && g_passwordManager.IsProfileLockUnlocked(index, bCanceled, bPrompt)))
+ bLoadProfile = true;
+ else if (!bCanceled) // Password needed and user provided it
+ {
+ const CVariant &passwordObject = parameterObject["password"];
+ const std::string& strToVerify = profile->getLockCode();
+ std::string password = passwordObject["value"].asString();
+
+ // Create password hash from the provided password if md5 is not used
+ std::string md5pword2;
+ std::string encryption = passwordObject["encryption"].asString();
+ if (encryption == "none")
+ md5pword2 = CDigest::Calculate(CDigest::Type::MD5, password);
+ else if (encryption == "md5")
+ md5pword2 = password;
+
+ // Verify provided password
+ if (StringUtils::EqualsNoCase(strToVerify, md5pword2))
+ bLoadProfile = true;
+ }
+
+ if (bLoadProfile)
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_LOADPROFILE, index);
+ return ACK;
+ }
+ return InvalidParams;
+}
diff --git a/xbmc/interfaces/json-rpc/ProfilesOperations.h b/xbmc/interfaces/json-rpc/ProfilesOperations.h
new file mode 100644
index 0000000..7f11b83
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/ProfilesOperations.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013-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 "FileItemHandler.h"
+#include "JSONRPC.h"
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CProfilesOperations : CFileItemHandler
+ {
+ public:
+ static JSONRPC_STATUS GetProfiles(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetCurrentProfile(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS LoadProfile(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/SettingsOperations.cpp b/xbmc/interfaces/json-rpc/SettingsOperations.cpp
new file mode 100644
index 0000000..09992f2
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/SettingsOperations.cpp
@@ -0,0 +1,905 @@
+/*
+ * Copyright (C) 2013-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 "SettingsOperations.h"
+
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/Skin.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/SettingAddon.h"
+#include "settings/SettingControl.h"
+#include "settings/SettingDateTime.h"
+#include "settings/SettingPath.h"
+#include "settings/SettingUtils.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SkinSettings.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingSection.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+using namespace JSONRPC;
+
+JSONRPC_STATUS CSettingsOperations::GetSections(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ SettingLevel level = ParseSettingLevel(parameterObject["level"].asString());
+ bool listCategories = !parameterObject["properties"].empty() && parameterObject["properties"][0].asString() == "categories";
+
+ result["sections"] = CVariant(CVariant::VariantTypeArray);
+
+ // apply the level filter
+ SettingSectionList allSections = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSections();
+ for (const auto& itSection : allSections)
+ {
+ SettingCategoryList categories = itSection->GetCategories(level);
+ if (categories.empty())
+ continue;
+
+ CVariant varSection(CVariant::VariantTypeObject);
+ if (!SerializeSettingSection(itSection, varSection))
+ continue;
+
+ if (listCategories)
+ {
+ varSection["categories"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& itCategory : categories)
+ {
+ CVariant varCategory(CVariant::VariantTypeObject);
+ if (!SerializeSettingCategory(itCategory, varCategory))
+ continue;
+
+ varSection["categories"].push_back(varCategory);
+ }
+ }
+
+ result["sections"].push_back(varSection);
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CSettingsOperations::GetCategories(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ SettingLevel level = ParseSettingLevel(parameterObject["level"].asString());
+ std::string strSection = parameterObject["section"].asString();
+ bool listSettings = !parameterObject["properties"].empty() && parameterObject["properties"][0].asString() == "settings";
+
+ std::vector<SettingSectionPtr> sections;
+ if (!strSection.empty())
+ {
+ SettingSectionPtr section = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSection(strSection);
+ if (section == NULL)
+ return InvalidParams;
+
+ sections.push_back(section);
+ }
+ else
+ sections = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSections();
+
+ result["categories"] = CVariant(CVariant::VariantTypeArray);
+
+ for (const auto& itSection : sections)
+ {
+ SettingCategoryList categories = itSection->GetCategories(level);
+ for (const auto& itCategory : categories)
+ {
+ CVariant varCategory(CVariant::VariantTypeObject);
+ if (!SerializeSettingCategory(itCategory, varCategory))
+ continue;
+
+ if (listSettings)
+ {
+ varCategory["groups"] = CVariant(CVariant::VariantTypeArray);
+
+ SettingGroupList groups = itCategory->GetGroups(level);
+ for (const auto& itGroup : groups)
+ {
+ CVariant varGroup(CVariant::VariantTypeObject);
+ if (!SerializeSettingGroup(itGroup, varGroup))
+ continue;
+
+ varGroup["settings"] = CVariant(CVariant::VariantTypeArray);
+ SettingList settings = itGroup->GetSettings(level);
+ for (const auto& itSetting : settings)
+ {
+ if (itSetting->IsVisible())
+ {
+ CVariant varSetting(CVariant::VariantTypeObject);
+ if (!SerializeSetting(itSetting, varSetting))
+ continue;
+
+ varGroup["settings"].push_back(varSetting);
+ }
+ }
+
+ varCategory["groups"].push_back(varGroup);
+ }
+ }
+
+ result["categories"].push_back(varCategory);
+ }
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CSettingsOperations::GetSettings(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ SettingLevel level = ParseSettingLevel(parameterObject["level"].asString());
+ const CVariant &filter = parameterObject["filter"];
+ bool doFilter = filter.isMember("section") && filter.isMember("category");
+ std::string strSection, strCategory;
+ if (doFilter)
+ {
+ strSection = filter["section"].asString();
+ strCategory = filter["category"].asString();
+ }
+
+ std::vector<SettingSectionPtr> sections;
+
+ if (doFilter)
+ {
+ SettingSectionPtr section = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSection(strSection);
+ if (section == NULL)
+ return InvalidParams;
+
+ sections.push_back(section);
+ }
+ else
+ sections = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSections();
+
+ result["settings"] = CVariant(CVariant::VariantTypeArray);
+
+ for (const auto& itSection : sections)
+ {
+ SettingCategoryList categories = itSection->GetCategories(level);
+ bool found = !doFilter;
+ for (const auto& itCategory : categories)
+ {
+ if (!doFilter || StringUtils::EqualsNoCase(itCategory->GetId(), strCategory))
+ {
+ SettingGroupList groups = itCategory->GetGroups(level);
+ for (const auto& itGroup : groups)
+ {
+ SettingList settings = itGroup->GetSettings(level);
+ for (const auto& itSetting : settings)
+ {
+ if (itSetting->IsVisible())
+ {
+ CVariant varSetting(CVariant::VariantTypeObject);
+ if (!SerializeSetting(itSetting, varSetting))
+ continue;
+
+ result["settings"].push_back(varSetting);
+ }
+ }
+ }
+ found = true;
+
+ if (doFilter)
+ break;
+ }
+ }
+
+ if (doFilter && !found)
+ return InvalidParams;
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CSettingsOperations::GetSettingValue(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string settingId = parameterObject["setting"].asString();
+
+ SettingPtr setting = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(settingId);
+ if (setting == NULL ||
+ !setting->IsVisible())
+ return InvalidParams;
+
+ CVariant value;
+ switch (setting->GetType())
+ {
+ case SettingType::Boolean:
+ value = std::static_pointer_cast<CSettingBool>(setting)->GetValue();
+ break;
+
+ case SettingType::Integer:
+ value = std::static_pointer_cast<CSettingInt>(setting)->GetValue();
+ break;
+
+ case SettingType::Number:
+ value = std::static_pointer_cast<CSettingNumber>(setting)->GetValue();
+ break;
+
+ case SettingType::String:
+ value = std::static_pointer_cast<CSettingString>(setting)->GetValue();
+ break;
+
+ case SettingType::List:
+ {
+ SerializeSettingListValues(CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(settingId), value);
+ break;
+ }
+
+ case SettingType::Unknown:
+ case SettingType::Action:
+ default:
+ return InvalidParams;
+ }
+
+ result["value"] = value;
+
+ return OK;
+}
+
+JSONRPC_STATUS CSettingsOperations::SetSettingValue(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string settingId = parameterObject["setting"].asString();
+ CVariant value = parameterObject["value"];
+
+ SettingPtr setting = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(settingId);
+ if (setting == NULL ||
+ !setting->IsVisible())
+ return InvalidParams;
+
+ switch (setting->GetType())
+ {
+ case SettingType::Boolean:
+ if (!value.isBoolean())
+ return InvalidParams;
+
+ result = std::static_pointer_cast<CSettingBool>(setting)->SetValue(value.asBoolean());
+ break;
+
+ case SettingType::Integer:
+ if (!value.isInteger() && !value.isUnsignedInteger())
+ return InvalidParams;
+
+ result = std::static_pointer_cast<CSettingInt>(setting)->SetValue((int)value.asInteger());
+ break;
+
+ case SettingType::Number:
+ if (!value.isDouble())
+ return InvalidParams;
+
+ result = std::static_pointer_cast<CSettingNumber>(setting)->SetValue(value.asDouble());
+ break;
+
+ case SettingType::String:
+ if (!value.isString())
+ return InvalidParams;
+
+ result = std::static_pointer_cast<CSettingString>(setting)->SetValue(value.asString());
+ break;
+
+ case SettingType::List:
+ {
+ if (!value.isArray())
+ return InvalidParams;
+
+ std::vector<CVariant> values;
+ for (CVariant::const_iterator_array itValue = value.begin_array(); itValue != value.end_array(); ++itValue)
+ values.push_back(*itValue);
+
+ result = CServiceBroker::GetSettingsComponent()->GetSettings()->SetList(settingId, values);
+ break;
+ }
+
+ case SettingType::Unknown:
+ case SettingType::Action:
+ default:
+ return InvalidParams;
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CSettingsOperations::ResetSettingValue(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string settingId = parameterObject["setting"].asString();
+
+ SettingPtr setting = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(settingId);
+ if (setting == NULL ||
+ !setting->IsVisible())
+ return InvalidParams;
+
+ switch (setting->GetType())
+ {
+ case SettingType::Boolean:
+ case SettingType::Integer:
+ case SettingType::Number:
+ case SettingType::String:
+ case SettingType::List:
+ setting->Reset();
+ break;
+
+ case SettingType::Unknown:
+ case SettingType::Action:
+ default:
+ return InvalidParams;
+ }
+
+ return ACK;
+}
+
+SettingLevel CSettingsOperations::ParseSettingLevel(const std::string &strLevel)
+{
+ if (StringUtils::EqualsNoCase(strLevel, "basic"))
+ return SettingLevel::Basic;
+ if (StringUtils::EqualsNoCase(strLevel, "advanced"))
+ return SettingLevel::Advanced;
+ if (StringUtils::EqualsNoCase(strLevel, "expert"))
+ return SettingLevel::Expert;
+
+ return SettingLevel::Standard;
+}
+
+bool CSettingsOperations::SerializeISetting(const std::shared_ptr<const ISetting>& setting,
+ CVariant& obj)
+{
+ if (setting == NULL)
+ return false;
+
+ obj["id"] = setting->GetId();
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingSection(
+ const std::shared_ptr<const CSettingSection>& setting, CVariant& obj)
+{
+ if (!SerializeISetting(setting, obj))
+ return false;
+
+ obj["label"] = g_localizeStrings.Get(setting->GetLabel());
+ if (setting->GetHelp() >= 0)
+ obj["help"] = g_localizeStrings.Get(setting->GetHelp());
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingCategory(
+ const std::shared_ptr<const CSettingCategory>& setting, CVariant& obj)
+{
+ if (!SerializeISetting(setting, obj))
+ return false;
+
+ obj["label"] = g_localizeStrings.Get(setting->GetLabel());
+ if (setting->GetHelp() >= 0)
+ obj["help"] = g_localizeStrings.Get(setting->GetHelp());
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingGroup(const std::shared_ptr<const CSettingGroup>& setting,
+ CVariant& obj)
+{
+ return SerializeISetting(setting, obj);
+}
+
+bool CSettingsOperations::SerializeSetting(const std::shared_ptr<const CSetting>& setting,
+ CVariant& obj)
+{
+ if (!SerializeISetting(setting, obj))
+ return false;
+
+ obj["label"] = g_localizeStrings.Get(setting->GetLabel());
+ if (setting->GetHelp() >= 0)
+ obj["help"] = g_localizeStrings.Get(setting->GetHelp());
+
+ switch (setting->GetLevel())
+ {
+ case SettingLevel::Basic:
+ obj["level"] = "basic";
+ break;
+
+ case SettingLevel::Standard:
+ obj["level"] = "standard";
+ break;
+
+ case SettingLevel::Advanced:
+ obj["level"] = "advanced";
+ break;
+
+ case SettingLevel::Expert:
+ obj["level"] = "expert";
+ break;
+
+ default:
+ return false;
+ }
+
+ obj["enabled"] = setting->IsEnabled();
+ obj["parent"] = setting->GetParent();
+
+ obj["control"] = CVariant(CVariant::VariantTypeObject);
+ if (!SerializeSettingControl(setting->GetControl(), obj["control"]))
+ return false;
+
+ switch (setting->GetType())
+ {
+ case SettingType::Boolean:
+ obj["type"] = "boolean";
+ if (!SerializeSettingBool(std::static_pointer_cast<const CSettingBool>(setting), obj))
+ return false;
+ break;
+
+ case SettingType::Integer:
+ obj["type"] = "integer";
+ if (!SerializeSettingInt(std::static_pointer_cast<const CSettingInt>(setting), obj))
+ return false;
+ break;
+
+ case SettingType::Number:
+ obj["type"] = "number";
+ if (!SerializeSettingNumber(std::static_pointer_cast<const CSettingNumber>(setting), obj))
+ return false;
+ break;
+
+ case SettingType::String:
+ obj["type"] = "string";
+ if (!SerializeSettingString(std::static_pointer_cast<const CSettingString>(setting), obj))
+ return false;
+ break;
+
+ case SettingType::Action:
+ obj["type"] = "action";
+ if (!SerializeSettingAction(std::static_pointer_cast<const CSettingAction>(setting), obj))
+ return false;
+ break;
+
+ case SettingType::List:
+ obj["type"] = "list";
+ if (!SerializeSettingList(std::static_pointer_cast<const CSettingList>(setting), obj))
+ return false;
+ break;
+
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingBool(const std::shared_ptr<const CSettingBool>& setting,
+ CVariant& obj)
+{
+ if (setting == NULL)
+ return false;
+
+ obj["value"] = setting->GetValue();
+ obj["default"] = setting->GetDefault();
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingInt(const std::shared_ptr<const CSettingInt>& setting,
+ CVariant& obj)
+{
+ if (setting == NULL)
+ return false;
+
+ obj["default"] = setting->GetDefault();
+
+ switch (setting->GetOptionsType())
+ {
+ case SettingOptionsType::StaticTranslatable:
+ {
+ obj["options"] = CVariant(CVariant::VariantTypeArray);
+ const TranslatableIntegerSettingOptions& options = setting->GetTranslatableOptions();
+ for (const auto& itOption : options)
+ {
+ CVariant varOption(CVariant::VariantTypeObject);
+ varOption["label"] = g_localizeStrings.Get(itOption.label);
+ varOption["value"] = itOption.value;
+ obj["options"].push_back(varOption);
+ }
+ break;
+ }
+
+ case SettingOptionsType::Static:
+ {
+ obj["options"] = CVariant(CVariant::VariantTypeArray);
+ const IntegerSettingOptions& options = setting->GetOptions();
+ for (const auto& itOption : options)
+ {
+ CVariant varOption(CVariant::VariantTypeObject);
+ varOption["label"] = itOption.label;
+ varOption["value"] = itOption.value;
+ obj["options"].push_back(varOption);
+ }
+ break;
+ }
+
+ case SettingOptionsType::Dynamic:
+ {
+ obj["options"] = CVariant(CVariant::VariantTypeArray);
+ IntegerSettingOptions options = std::const_pointer_cast<CSettingInt>(setting)->UpdateDynamicOptions();
+ for (const auto& itOption : options)
+ {
+ CVariant varOption(CVariant::VariantTypeObject);
+ varOption["label"] = itOption.label;
+ varOption["value"] = itOption.value;
+ obj["options"].push_back(varOption);
+ }
+ break;
+ }
+
+ case SettingOptionsType::Unknown:
+ default:
+ obj["minimum"] = setting->GetMinimum();
+ obj["step"] = setting->GetStep();
+ obj["maximum"] = setting->GetMaximum();
+ break;
+ }
+
+ // this must be done after potentially calling CSettingInt::UpdateDynamicOptions() because it can
+ // change the value of the setting
+ obj["value"] = setting->GetValue();
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingNumber(
+ const std::shared_ptr<const CSettingNumber>& setting, CVariant& obj)
+{
+ if (setting == NULL)
+ return false;
+
+ obj["value"] = setting->GetValue();
+ obj["default"] = setting->GetDefault();
+
+ obj["minimum"] = setting->GetMinimum();
+ obj["step"] = setting->GetStep();
+ obj["maximum"] = setting->GetMaximum();
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingString(
+ const std::shared_ptr<const CSettingString>& setting, CVariant& obj)
+{
+ if (setting == NULL)
+ return false;
+
+ obj["default"] = setting->GetDefault();
+
+ obj["allowempty"] = setting->AllowEmpty();
+ obj["allownewoption"] = setting->AllowNewOption();
+
+ switch (setting->GetOptionsType())
+ {
+ case SettingOptionsType::StaticTranslatable:
+ {
+ obj["options"] = CVariant(CVariant::VariantTypeArray);
+ const TranslatableStringSettingOptions& options = setting->GetTranslatableOptions();
+ for (const auto& itOption : options)
+ {
+ CVariant varOption(CVariant::VariantTypeObject);
+ varOption["label"] = g_localizeStrings.Get(itOption.first);
+ varOption["value"] = itOption.second;
+ obj["options"].push_back(varOption);
+ }
+ break;
+ }
+
+ case SettingOptionsType::Static:
+ {
+ obj["options"] = CVariant(CVariant::VariantTypeArray);
+ const StringSettingOptions& options = setting->GetOptions();
+ for (const auto& itOption : options)
+ {
+ CVariant varOption(CVariant::VariantTypeObject);
+ varOption["label"] = itOption.label;
+ varOption["value"] = itOption.value;
+ obj["options"].push_back(varOption);
+ }
+ break;
+ }
+
+ case SettingOptionsType::Dynamic:
+ {
+ obj["options"] = CVariant(CVariant::VariantTypeArray);
+ StringSettingOptions options = std::const_pointer_cast<CSettingString>(setting)->UpdateDynamicOptions();
+ for (const auto& itOption : options)
+ {
+ CVariant varOption(CVariant::VariantTypeObject);
+ varOption["label"] = itOption.label;
+ varOption["value"] = itOption.value;
+ obj["options"].push_back(varOption);
+ }
+ break;
+ }
+
+ case SettingOptionsType::Unknown:
+ default:
+ break;
+ }
+
+ // this must be done after potentially calling CSettingString::UpdateDynamicOptions() because it
+ // can change the value of the setting
+ obj["value"] = setting->GetValue();
+
+ std::shared_ptr<const ISettingControl> control = setting->GetControl();
+ if (control->GetFormat() == "path")
+ {
+ if (!SerializeSettingPath(std::static_pointer_cast<const CSettingPath>(setting), obj))
+ return false;
+ }
+ if (control->GetFormat() == "addon")
+ {
+ if (!SerializeSettingAddon(std::static_pointer_cast<const CSettingAddon>(setting), obj))
+ return false;
+ }
+ if (control->GetFormat() == "date")
+ {
+ if (!SerializeSettingDate(std::static_pointer_cast<const CSettingDate>(setting), obj))
+ return false;
+ }
+ if (control->GetFormat() == "time")
+ {
+ if (!SerializeSettingTime(std::static_pointer_cast<const CSettingTime>(setting), obj))
+ return false;
+ }
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingAction(
+ const std::shared_ptr<const CSettingAction>& setting, CVariant& obj)
+{
+ if (setting == NULL)
+ return false;
+
+ obj["data"] = setting->GetData();
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingList(const std::shared_ptr<const CSettingList>& setting,
+ CVariant& obj)
+{
+ if (setting == NULL ||
+ !SerializeSetting(setting->GetDefinition(), obj["definition"]))
+ return false;
+
+ SerializeSettingListValues(CSettingUtils::GetList(setting), obj["value"]);
+ SerializeSettingListValues(CSettingUtils::ListToValues(setting, setting->GetDefault()), obj["default"]);
+
+ obj["elementtype"] = obj["definition"]["type"];
+ obj["delimiter"] = setting->GetDelimiter();
+ obj["minimumItems"] = setting->GetMinimumItems();
+ obj["maximumItems"] = setting->GetMaximumItems();
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingPath(const std::shared_ptr<const CSettingPath>& setting,
+ CVariant& obj)
+{
+ if (setting == NULL)
+ return false;
+
+ obj["type"] = "path";
+ obj["writable"] = setting->Writable();
+ obj["sources"] = setting->GetSources();
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingAddon(const std::shared_ptr<const CSettingAddon>& setting,
+ CVariant& obj)
+{
+ if (setting == NULL)
+ return false;
+
+ obj["type"] = "addon";
+ obj["addontype"] = ADDON::CAddonInfo::TranslateType(setting->GetAddonType());
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingDate(const std::shared_ptr<const CSettingDate>& setting,
+ CVariant& obj)
+{
+ if (setting == NULL)
+ return false;
+
+ obj["type"] = "date";
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingTime(const std::shared_ptr<const CSettingTime>& setting,
+ CVariant& obj)
+{
+ if (setting == NULL)
+ return false;
+
+ obj["type"] = "time";
+
+ return true;
+}
+
+bool CSettingsOperations::SerializeSettingControl(
+ const std::shared_ptr<const ISettingControl>& control, CVariant& obj)
+{
+ if (control == NULL)
+ return false;
+
+ const std::string& type = control->GetType();
+ obj["type"] = type;
+ obj["format"] = control->GetFormat();
+ obj["delayed"] = control->GetDelayed();
+
+ if (type == "spinner")
+ {
+ std::shared_ptr<const CSettingControlSpinner> spinner = std::static_pointer_cast<const CSettingControlSpinner>(control);
+ if (spinner->GetFormatLabel() >= 0)
+ obj["formatlabel"] = g_localizeStrings.Get(spinner->GetFormatLabel());
+ else if (!spinner->GetFormatString().empty() && spinner->GetFormatString() != "{:d}")
+ obj["formatlabel"] = spinner->GetFormatString();
+ if (spinner->GetMinimumLabel() >= 0)
+ obj["minimumlabel"] = g_localizeStrings.Get(spinner->GetMinimumLabel());
+ }
+ else if (type == "edit")
+ {
+ std::shared_ptr<const CSettingControlEdit> edit = std::static_pointer_cast<const CSettingControlEdit>(control);
+ obj["hidden"] = edit->IsHidden();
+ obj["verifynewvalue"] = edit->VerifyNewValue();
+ if (edit->GetHeading() >= 0)
+ obj["heading"] = g_localizeStrings.Get(edit->GetHeading());
+ }
+ else if (type == "button")
+ {
+ std::shared_ptr<const CSettingControlButton> button = std::static_pointer_cast<const CSettingControlButton>(control);
+ if (button->GetHeading() >= 0)
+ obj["heading"] = g_localizeStrings.Get(button->GetHeading());
+ }
+ else if (type == "list")
+ {
+ std::shared_ptr<const CSettingControlList> list = std::static_pointer_cast<const CSettingControlList>(control);
+ if (list->GetHeading() >= 0)
+ obj["heading"] = g_localizeStrings.Get(list->GetHeading());
+ obj["multiselect"] = list->CanMultiSelect();
+ }
+ else if (type == "slider")
+ {
+ std::shared_ptr<const CSettingControlSlider> slider = std::static_pointer_cast<const CSettingControlSlider>(control);
+ if (slider->GetHeading() >= 0)
+ obj["heading"] = g_localizeStrings.Get(slider->GetHeading());
+ obj["popup"] = slider->UsePopup();
+ if (slider->GetFormatLabel() >= 0)
+ obj["formatlabel"] = g_localizeStrings.Get(slider->GetFormatLabel());
+ else
+ obj["formatlabel"] = slider->GetFormatString();
+ }
+ else if (type == "range")
+ {
+ std::shared_ptr<const CSettingControlRange> range = std::static_pointer_cast<const CSettingControlRange>(control);
+ if (range->GetFormatLabel() >= 0)
+ obj["formatlabel"] = g_localizeStrings.Get(range->GetFormatLabel());
+ else
+ obj["formatlabel"] = "";
+ if (range->GetValueFormatLabel() >= 0)
+ obj["formatvalue"] = g_localizeStrings.Get(range->GetValueFormatLabel());
+ else
+ obj["formatvalue"] = range->GetValueFormat();
+ }
+ else if (type != "toggle" && type != "label")
+ return false;
+
+ return true;
+}
+
+void CSettingsOperations::SerializeSettingListValues(const std::vector<CVariant> &values, CVariant &obj)
+{
+ obj = CVariant(CVariant::VariantTypeArray);
+ for (const auto& itValue : values)
+ obj.push_back(itValue);
+}
+
+JSONRPC_STATUS CSettingsOperations::GetSkinSettings(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result)
+{
+ const std::set<ADDON::CSkinSettingPtr> settings = CSkinSettings::GetInstance().GetSettings();
+ CVariant varSettings(CVariant::VariantTypeArray);
+
+ for (const auto& setting : settings)
+ {
+ CVariant varSetting(CVariant::VariantTypeObject);
+ varSetting["id"] = setting->name;
+
+ if (setting->GetType() == "bool")
+ {
+ varSetting["value"] = std::static_pointer_cast<ADDON::CSkinSettingBool>(setting)->value;
+ varSetting["type"] = "boolean";
+ }
+ else if (setting->GetType() == "string")
+ {
+ varSetting["value"] = std::static_pointer_cast<ADDON::CSkinSettingString>(setting)->value;
+ varSetting["type"] = setting->GetType();
+ }
+ else
+ continue;
+
+ varSettings.push_back(varSetting);
+ }
+
+ result["skin"] = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_LOOKANDFEEL_SKIN);
+ result["settings"] = varSettings;
+ return OK;
+}
+
+JSONRPC_STATUS CSettingsOperations::GetSkinSettingValue(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result)
+{
+ const std::string settingId = parameterObject["setting"].asString();
+ ADDON::CSkinSettingPtr setting = CSkinSettings::GetInstance().GetSetting(settingId);
+
+ if (setting == nullptr)
+ return InvalidParams;
+
+ CVariant value;
+ if (setting->GetType() == "string")
+ value = std::static_pointer_cast<ADDON::CSkinSettingString>(setting)->value;
+ else if (setting->GetType() == "bool")
+ value = std::static_pointer_cast<ADDON::CSkinSettingBool>(setting)->value;
+ else
+ return InvalidParams;
+
+ result["value"] = value;
+ return OK;
+}
+
+JSONRPC_STATUS CSettingsOperations::SetSkinSettingValue(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result)
+{
+ const std::string settingId = parameterObject["setting"].asString();
+ ADDON::CSkinSettingPtr setting = CSkinSettings::GetInstance().GetSetting(settingId);
+
+ if (setting == nullptr)
+ return InvalidParams;
+
+ CVariant value = parameterObject["value"];
+ if (setting->GetType() == "string")
+ {
+ if (!value.isString())
+ return InvalidParams;
+
+ result = std::static_pointer_cast<ADDON::CSkinSettingString>(setting)->value = value.asString();
+ }
+ else if (setting->GetType() == "bool")
+ {
+ if (!value.isBoolean())
+ return InvalidParams;
+
+ result = std::static_pointer_cast<ADDON::CSkinSettingBool>(setting)->value = value.asBoolean();
+ }
+ else
+ {
+ return InvalidParams;
+ }
+
+ return OK;
+}
diff --git a/xbmc/interfaces/json-rpc/SettingsOperations.h b/xbmc/interfaces/json-rpc/SettingsOperations.h
new file mode 100644
index 0000000..6267e73
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/SettingsOperations.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2013-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 "JSONRPC.h"
+#include "settings/lib/SettingLevel.h"
+
+#include <vector>
+
+class CVariant;
+class ISetting;
+class CSettingSection;
+class CSettingCategory;
+class CSettingGroup;
+class CSetting;
+class CSettingBool;
+class CSettingInt;
+class CSettingNumber;
+class CSettingString;
+class CSettingAction;
+class CSettingList;
+class CSettingPath;
+class CSettingAddon;
+class CSettingDate;
+class CSettingTime;
+class ISettingControl;
+
+namespace JSONRPC
+{
+ class CSettingsOperations
+ {
+ public:
+ static JSONRPC_STATUS GetSections(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetCategories(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetSettings(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS GetSettingValue(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetSettingValue(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS ResetSettingValue(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS GetSkinSettings(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result);
+ static JSONRPC_STATUS GetSkinSettingValue(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result);
+ static JSONRPC_STATUS SetSkinSettingValue(const std::string& method,
+ ITransportLayer* transport,
+ IClient* client,
+ const CVariant& parameterObject,
+ CVariant& result);
+
+ private:
+ static SettingLevel ParseSettingLevel(const std::string &strLevel);
+
+ static bool SerializeISetting(const std::shared_ptr<const ISetting>& setting, CVariant& obj);
+ static bool SerializeSettingSection(const std::shared_ptr<const CSettingSection>& setting,
+ CVariant& obj);
+ static bool SerializeSettingCategory(const std::shared_ptr<const CSettingCategory>& setting,
+ CVariant& obj);
+ static bool SerializeSettingGroup(const std::shared_ptr<const CSettingGroup>& setting,
+ CVariant& obj);
+ static bool SerializeSetting(const std::shared_ptr<const CSetting>& setting, CVariant& obj);
+ static bool SerializeSettingBool(const std::shared_ptr<const CSettingBool>& setting,
+ CVariant& obj);
+ static bool SerializeSettingInt(const std::shared_ptr<const CSettingInt>& setting,
+ CVariant& obj);
+ static bool SerializeSettingNumber(const std::shared_ptr<const CSettingNumber>& setting,
+ CVariant& obj);
+ static bool SerializeSettingString(const std::shared_ptr<const CSettingString>& setting,
+ CVariant& obj);
+ static bool SerializeSettingAction(const std::shared_ptr<const CSettingAction>& setting,
+ CVariant& obj);
+ static bool SerializeSettingList(const std::shared_ptr<const CSettingList>& setting,
+ CVariant& obj);
+ static bool SerializeSettingPath(const std::shared_ptr<const CSettingPath>& setting,
+ CVariant& obj);
+ static bool SerializeSettingAddon(const std::shared_ptr<const CSettingAddon>& setting,
+ CVariant& obj);
+ static bool SerializeSettingDate(const std::shared_ptr<const CSettingDate>& setting,
+ CVariant& obj);
+ static bool SerializeSettingTime(const std::shared_ptr<const CSettingTime>& setting,
+ CVariant& obj);
+ static bool SerializeSettingControl(const std::shared_ptr<const ISettingControl>& control,
+ CVariant& obj);
+
+ static void SerializeSettingListValues(const std::vector<CVariant> &values, CVariant &obj);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/SystemOperations.cpp b/xbmc/interfaces/json-rpc/SystemOperations.cpp
new file mode 100644
index 0000000..b4d508c
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/SystemOperations.cpp
@@ -0,0 +1,101 @@
+/*
+ * 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 "SystemOperations.h"
+
+#include "ServiceBroker.h"
+#include "interfaces/builtins/Builtins.h"
+#include "messaging/ApplicationMessenger.h"
+#include "powermanagement/PowerManager.h"
+#include "utils/Variant.h"
+
+using namespace JSONRPC;
+
+JSONRPC_STATUS CSystemOperations::GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVariant properties = CVariant(CVariant::VariantTypeObject);
+ for (unsigned int index = 0; index < parameterObject["properties"].size(); index++)
+ {
+ std::string propertyName = parameterObject["properties"][index].asString();
+ CVariant property;
+ JSONRPC_STATUS ret;
+ if ((ret = GetPropertyValue(client->GetPermissionFlags(), propertyName, property)) != OK)
+ return ret;
+
+ properties[propertyName] = property;
+ }
+
+ result = properties;
+
+ return OK;
+}
+
+JSONRPC_STATUS CSystemOperations::EjectOpticalDrive(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return CBuiltins::GetInstance().Execute("EjectTray") == 0 ? ACK : FailedToExecute;
+}
+
+JSONRPC_STATUS CSystemOperations::Shutdown(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (CServiceBroker::GetPowerManager().CanPowerdown())
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_POWERDOWN);
+ return ACK;
+ }
+ else
+ return FailedToExecute;
+}
+
+JSONRPC_STATUS CSystemOperations::Suspend(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (CServiceBroker::GetPowerManager().CanSuspend())
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SUSPEND);
+ return ACK;
+ }
+ else
+ return FailedToExecute;
+}
+
+JSONRPC_STATUS CSystemOperations::Hibernate(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (CServiceBroker::GetPowerManager().CanHibernate())
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_HIBERNATE);
+ return ACK;
+ }
+ else
+ return FailedToExecute;
+}
+
+JSONRPC_STATUS CSystemOperations::Reboot(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ if (CServiceBroker::GetPowerManager().CanReboot())
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_RESTART);
+ return ACK;
+ }
+ else
+ return FailedToExecute;
+}
+
+JSONRPC_STATUS CSystemOperations::GetPropertyValue(int permissions, const std::string &property, CVariant &result)
+{
+ if (property == "canshutdown")
+ result = CServiceBroker::GetPowerManager().CanPowerdown() && (permissions & ControlPower);
+ else if (property == "cansuspend")
+ result = CServiceBroker::GetPowerManager().CanSuspend() && (permissions & ControlPower);
+ else if (property == "canhibernate")
+ result = CServiceBroker::GetPowerManager().CanHibernate() && (permissions & ControlPower);
+ else if (property == "canreboot")
+ result = CServiceBroker::GetPowerManager().CanReboot() && (permissions & ControlPower);
+ else
+ return InvalidParams;
+
+ return OK;
+}
diff --git a/xbmc/interfaces/json-rpc/SystemOperations.h b/xbmc/interfaces/json-rpc/SystemOperations.h
new file mode 100644
index 0000000..1bc1ce2
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/SystemOperations.h
@@ -0,0 +1,31 @@
+/*
+ * 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 "JSONRPC.h"
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CSystemOperations
+ {
+ public:
+ static JSONRPC_STATUS GetProperties(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS EjectOpticalDrive(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS Shutdown(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Suspend(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Hibernate(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Reboot(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ private:
+ static JSONRPC_STATUS GetPropertyValue(int permissions, const std::string &property, CVariant &result);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/TextureOperations.cpp b/xbmc/interfaces/json-rpc/TextureOperations.cpp
new file mode 100644
index 0000000..326093b
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/TextureOperations.cpp
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2013-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 "TextureOperations.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "TextureDatabase.h"
+#include "utils/Variant.h"
+
+using namespace JSONRPC;
+
+JSONRPC_STATUS CTextureOperations::GetTextures(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CFileItemList listItems;
+
+ CTextureDatabase db;
+ if (!db.Open())
+ return InternalError;
+
+ CDatabase::Filter dbFilter;
+ const CVariant &filter = parameterObject["filter"];
+ if (filter.isObject())
+ {
+ CVariant xspObj(CVariant::VariantTypeObject);
+
+ if (filter.isMember("field"))
+ {
+ xspObj["and"] = CVariant(CVariant::VariantTypeArray);
+ xspObj["and"].push_back(filter);
+ }
+ else
+ xspObj = filter;
+
+ // decipher the rules
+ CDatabaseQueryRuleCombination rule;
+ if (!rule.Load(xspObj, &db))
+ return InvalidParams;
+
+ dbFilter.AppendWhere(rule.GetWhereClause(db, ""));
+ }
+
+ // fetch textures from the database
+ CVariant items = CVariant(CVariant::VariantTypeArray);
+ if (!db.GetTextures(items, dbFilter))
+ return InternalError;
+
+ // return only what was asked for, plus textureid
+ CVariant prop = parameterObject["properties"];
+ prop.push_back("textureid");
+ if (!items.empty() && prop.isArray())
+ {
+ std::set<std::string> fields;
+ CVariant &item = items[0];
+ for (CVariant::const_iterator_map field = item.begin_map(); field != item.end_map(); ++field)
+ {
+ if (std::find(prop.begin_array(), prop.end_array(), field->first) == prop.end_array())
+ fields.insert(field->first);
+ }
+ // erase these fields
+ for (CVariant::iterator_array item = items.begin_array(); item != items.end_array(); ++item)
+ {
+ for (const auto& i : fields)
+ item->erase(i);
+ }
+ if (fields.find("url") == fields.end())
+ {
+ // wrap cached url to something retrieval from Files.GetFiles()
+ for (CVariant::iterator_array item = items.begin_array(); item != items.end_array(); ++item)
+ {
+ CVariant &cachedUrl = (*item)["url"];
+ cachedUrl = CTextureUtils::GetWrappedImageURL(cachedUrl.asString());
+ }
+ }
+ }
+
+ result["textures"] = items;
+ return OK;
+}
+
+JSONRPC_STATUS CTextureOperations::RemoveTexture(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["textureid"].asInteger();
+
+ if (!CServiceBroker::GetTextureCache()->ClearCachedImage(id))
+ return InvalidParams;
+
+ return ACK;
+}
diff --git a/xbmc/interfaces/json-rpc/TextureOperations.h b/xbmc/interfaces/json-rpc/TextureOperations.h
new file mode 100644
index 0000000..63a53a3
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/TextureOperations.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2013-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 "JSONRPC.h"
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CTextureOperations
+ {
+ public:
+ static JSONRPC_STATUS GetTextures(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS RemoveTexture(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/VideoLibrary.cpp b/xbmc/interfaces/json-rpc/VideoLibrary.cpp
new file mode 100644
index 0000000..db152cd
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/VideoLibrary.cpp
@@ -0,0 +1,1385 @@
+/*
+ * Copyright (C) 2016-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 "VideoLibrary.h"
+
+#include "FileItem.h"
+#include "PVROperations.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "Util.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoDbUrl.h"
+#include "video/VideoLibraryQueue.h"
+
+using namespace JSONRPC;
+
+JSONRPC_STATUS CVideoLibrary::GetMovies(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ SortDescription sorting;
+ ParseLimits(parameterObject, sorting.limitStart, sorting.limitEnd);
+ if (!ParseSorting(parameterObject, sorting.sortBy, sorting.sortOrder, sorting.sortAttributes))
+ return InvalidParams;
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString("videodb://movies/titles/"))
+ return InternalError;
+
+ int genreID = -1, year = -1, setID = 0;
+ const CVariant &filter = parameterObject["filter"];
+ if (filter.isMember("genreid"))
+ genreID = (int)filter["genreid"].asInteger();
+ else if (filter.isMember("genre"))
+ videoUrl.AddOption("genre", filter["genre"].asString());
+ else if (filter.isMember("year"))
+ year = (int)filter["year"].asInteger();
+ else if (filter.isMember("actor"))
+ videoUrl.AddOption("actor", filter["actor"].asString());
+ else if (filter.isMember("director"))
+ videoUrl.AddOption("director", filter["director"].asString());
+ else if (filter.isMember("studio"))
+ videoUrl.AddOption("studio", filter["studio"].asString());
+ else if (filter.isMember("country"))
+ videoUrl.AddOption("country", filter["country"].asString());
+ else if (filter.isMember("setid"))
+ setID = (int)filter["setid"].asInteger();
+ else if (filter.isMember("set"))
+ videoUrl.AddOption("set", filter["set"].asString());
+ else if (filter.isMember("tag"))
+ videoUrl.AddOption("tag", filter["tag"].asString());
+ else if (filter.isObject())
+ {
+ std::string xsp;
+ if (!GetXspFiltering("movies", filter, xsp))
+ return InvalidParams;
+
+ videoUrl.AddOption("xsp", xsp);
+ }
+
+ // setID must not be -1 otherwise GetMoviesNav() will return sets
+ if (setID < 0)
+ setID = 0;
+
+ CFileItemList items;
+ if (!videodatabase.GetMoviesNav(videoUrl.ToString(), items, genreID, year, -1, -1, -1, -1, setID, -1, sorting, RequiresAdditionalDetails(MediaTypeMovie, parameterObject)))
+ return InvalidParams;
+
+ return HandleItems("movieid", "movies", items, parameterObject, result, false);
+}
+
+JSONRPC_STATUS CVideoLibrary::GetMovieDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["movieid"].asInteger();
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVideoInfoTag infos;
+ if (!videodatabase.GetMovieInfo("", infos, id, RequiresAdditionalDetails(MediaTypeMovie, parameterObject)) || infos.m_iDbId <= 0)
+ return InvalidParams;
+
+ HandleFileItem("movieid", true, "moviedetails", CFileItemPtr(new CFileItem(infos)), parameterObject, parameterObject["properties"], result, false);
+ return OK;
+}
+
+JSONRPC_STATUS CVideoLibrary::GetMovieSets(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CFileItemList items;
+ if (!videodatabase.GetSetsNav("videodb://movies/sets/", items, VideoDbContentType::MOVIES))
+ return InternalError;
+
+ HandleFileItemList("setid", false, "sets", items, parameterObject, result);
+ return OK;
+}
+
+JSONRPC_STATUS CVideoLibrary::GetMovieSetDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["setid"].asInteger();
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ // Get movie set details
+ CVideoInfoTag infos;
+ if (!videodatabase.GetSetInfo(id, infos) || infos.m_iDbId <= 0)
+ return InvalidParams;
+
+ HandleFileItem("setid", false, "setdetails", CFileItemPtr(new CFileItem(infos)), parameterObject, parameterObject["properties"], result, false);
+
+ // Get movies from the set
+ CFileItemList items;
+ if (!videodatabase.GetMoviesNav("videodb://movies/titles/", items, -1, -1, -1, -1, -1, -1, id, -1, SortDescription(), RequiresAdditionalDetails(MediaTypeMovie, parameterObject["movies"])))
+ return InternalError;
+
+ return HandleItems("movieid", "movies", items, parameterObject["movies"], result["setdetails"], true);
+}
+
+JSONRPC_STATUS CVideoLibrary::GetTVShows(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ SortDescription sorting;
+ ParseLimits(parameterObject, sorting.limitStart, sorting.limitEnd);
+ if (!ParseSorting(parameterObject, sorting.sortBy, sorting.sortOrder, sorting.sortAttributes))
+ return InvalidParams;
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString("videodb://tvshows/titles/"))
+ return InternalError;
+
+ const CVariant &filter = parameterObject["filter"];
+ if (filter.isMember("genreid"))
+ videoUrl.AddOption("genreid", (int)filter["genreid"].asInteger());
+ else if (filter.isMember("genre"))
+ videoUrl.AddOption("genre", filter["genre"].asString());
+ else if (filter.isMember("year"))
+ videoUrl.AddOption("year", (int)filter["year"].asInteger());
+ else if (filter.isMember("actor"))
+ videoUrl.AddOption("actor", filter["actor"].asString());
+ else if (filter.isMember("studio"))
+ videoUrl.AddOption("studio", filter["studio"].asString());
+ else if (filter.isMember("tag"))
+ videoUrl.AddOption("tag", filter["tag"].asString());
+ else if (filter.isObject())
+ {
+ std::string xsp;
+ if (!GetXspFiltering("tvshows", filter, xsp))
+ return InvalidParams;
+
+ videoUrl.AddOption("xsp", xsp);
+ }
+
+ CFileItemList items;
+ CDatabase::Filter nofilter;
+ if (!videodatabase.GetTvShowsByWhere(videoUrl.ToString(), nofilter, items, sorting, RequiresAdditionalDetails(MediaTypeTvShow, parameterObject)))
+ return InvalidParams;
+
+ return HandleItems("tvshowid", "tvshows", items, parameterObject, result, false);
+}
+
+JSONRPC_STATUS CVideoLibrary::GetTVShowDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ int id = (int)parameterObject["tvshowid"].asInteger();
+
+ CFileItemPtr fileItem(new CFileItem());
+ CVideoInfoTag infos;
+ if (!videodatabase.GetTvShowInfo("", infos, id, fileItem.get(), RequiresAdditionalDetails(MediaTypeTvShow, parameterObject)) || infos.m_iDbId <= 0)
+ return InvalidParams;
+
+ fileItem->SetFromVideoInfoTag(infos);
+ HandleFileItem("tvshowid", true, "tvshowdetails", fileItem, parameterObject, parameterObject["properties"], result, false);
+ return OK;
+}
+
+JSONRPC_STATUS CVideoLibrary::GetSeasons(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ int tvshowID = (int)parameterObject["tvshowid"].asInteger();
+
+ std::string strPath = StringUtils::Format("videodb://tvshows/titles/{}/", tvshowID);
+ CFileItemList items;
+ if (!videodatabase.GetSeasonsNav(strPath, items, -1, -1, -1, -1, tvshowID, false))
+ return InternalError;
+
+ HandleFileItemList("seasonid", false, "seasons", items, parameterObject, result);
+ return OK;
+}
+
+JSONRPC_STATUS CVideoLibrary::GetSeasonDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ int id = (int)parameterObject["seasonid"].asInteger();
+
+ CVideoInfoTag infos;
+ if (!videodatabase.GetSeasonInfo(id, infos) ||
+ infos.m_iDbId <= 0 || infos.m_iIdShow <= 0)
+ return InvalidParams;
+
+ CFileItemPtr pItem = CFileItemPtr(new CFileItem(infos));
+ HandleFileItem("seasonid", false, "seasondetails", pItem, parameterObject, parameterObject["properties"], result, false);
+ return OK;
+}
+
+JSONRPC_STATUS CVideoLibrary::GetEpisodes(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ SortDescription sorting;
+ ParseLimits(parameterObject, sorting.limitStart, sorting.limitEnd);
+ if (!ParseSorting(parameterObject, sorting.sortBy, sorting.sortOrder, sorting.sortAttributes))
+ return InvalidParams;
+
+ int tvshowID = (int)parameterObject["tvshowid"].asInteger();
+ int season = (int)parameterObject["season"].asInteger();
+
+ std::string strPath = StringUtils::Format("videodb://tvshows/titles/{}/{}/", tvshowID, season);
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(strPath))
+ return InternalError;
+
+ const CVariant &filter = parameterObject["filter"];
+ if (filter.isMember("genreid"))
+ videoUrl.AddOption("genreid", (int)filter["genreid"].asInteger());
+ else if (filter.isMember("genre"))
+ videoUrl.AddOption("genre", filter["genre"].asString());
+ else if (filter.isMember("year"))
+ videoUrl.AddOption("year", (int)filter["year"].asInteger());
+ else if (filter.isMember("actor"))
+ videoUrl.AddOption("actor", filter["actor"].asString());
+ else if (filter.isMember("director"))
+ videoUrl.AddOption("director", filter["director"].asString());
+ else if (filter.isObject())
+ {
+ std::string xsp;
+ if (!GetXspFiltering("episodes", filter, xsp))
+ return InvalidParams;
+
+ videoUrl.AddOption("xsp", xsp);
+ }
+
+ if (tvshowID <= 0 && (season > 0 || videoUrl.HasOption("genreid") || videoUrl.HasOption("genre") || videoUrl.HasOption("actor")))
+ return InvalidParams;
+
+ if (tvshowID > 0)
+ {
+ videoUrl.AddOption("tvshowid", tvshowID);
+ if (season >= 0)
+ videoUrl.AddOption("season", season);
+ }
+
+ CFileItemList items;
+ if (!videodatabase.GetEpisodesByWhere(videoUrl.ToString(), CDatabase::Filter(), items, false, sorting, RequiresAdditionalDetails(MediaTypeEpisode, parameterObject)))
+ return InvalidParams;
+
+ return HandleItems("episodeid", "episodes", items, parameterObject, result, false);
+}
+
+JSONRPC_STATUS CVideoLibrary::GetEpisodeDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ int id = (int)parameterObject["episodeid"].asInteger();
+
+ CVideoInfoTag infos;
+ if (!videodatabase.GetEpisodeInfo("", infos, id, RequiresAdditionalDetails(MediaTypeEpisode, parameterObject)) || infos.m_iDbId <= 0)
+ return InvalidParams;
+
+ CFileItemPtr pItem = CFileItemPtr(new CFileItem(infos));
+ // We need to set the correct base path to get the valid fanart
+ int tvshowid = infos.m_iIdShow;
+ if (tvshowid <= 0)
+ tvshowid = videodatabase.GetTvShowForEpisode(id);
+
+ std::string basePath =
+ StringUtils::Format("videodb://tvshows/titles/{}/{}/{}", tvshowid, infos.m_iSeason, id);
+ pItem->SetPath(basePath);
+
+ HandleFileItem("episodeid", true, "episodedetails", pItem, parameterObject, parameterObject["properties"], result, false);
+ return OK;
+}
+
+JSONRPC_STATUS CVideoLibrary::GetMusicVideos(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ SortDescription sorting;
+ ParseLimits(parameterObject, sorting.limitStart, sorting.limitEnd);
+ if (!ParseSorting(parameterObject, sorting.sortBy, sorting.sortOrder, sorting.sortAttributes))
+ return InvalidParams;
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString("videodb://musicvideos/titles/"))
+ return InternalError;
+
+ int genreID = -1, year = -1;
+ const CVariant &filter = parameterObject["filter"];
+ if (filter.isMember("artist"))
+ videoUrl.AddOption("artist", filter["artist"].asString());
+ else if (filter.isMember("genreid"))
+ genreID = (int)filter["genreid"].asInteger();
+ else if (filter.isMember("genre"))
+ videoUrl.AddOption("genre", filter["genre"].asString());
+ else if (filter.isMember("year"))
+ year = (int)filter["year"].asInteger();
+ else if (filter.isMember("director"))
+ videoUrl.AddOption("director", filter["director"].asString());
+ else if (filter.isMember("studio"))
+ videoUrl.AddOption("studio", filter["studio"].asString());
+ else if (filter.isMember("tag"))
+ videoUrl.AddOption("tag", filter["tag"].asString());
+ else if (filter.isObject())
+ {
+ std::string xsp;
+ if (!GetXspFiltering("musicvideos", filter, xsp))
+ return InvalidParams;
+
+ videoUrl.AddOption("xsp", xsp);
+ }
+
+ CFileItemList items;
+ if (!videodatabase.GetMusicVideosNav(videoUrl.ToString(), items, genreID, year, -1, -1, -1, -1, -1, sorting, RequiresAdditionalDetails(MediaTypeMusicVideo, parameterObject)))
+ return InternalError;
+
+ return HandleItems("musicvideoid", "musicvideos", items, parameterObject, result, false);
+}
+
+JSONRPC_STATUS CVideoLibrary::GetMusicVideoDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ int id = (int)parameterObject["musicvideoid"].asInteger();
+
+ CVideoInfoTag infos;
+ if (!videodatabase.GetMusicVideoInfo("", infos, id, RequiresAdditionalDetails(MediaTypeMusicVideo, parameterObject)) || infos.m_iDbId <= 0)
+ return InvalidParams;
+
+ HandleFileItem("musicvideoid", true, "musicvideodetails", CFileItemPtr(new CFileItem(infos)), parameterObject, parameterObject["properties"], result, false);
+ return OK;
+}
+
+JSONRPC_STATUS CVideoLibrary::GetRecentlyAddedMovies(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CFileItemList items;
+ if (!videodatabase.GetRecentlyAddedMoviesNav("videodb://recentlyaddedmovies/", items, 0, RequiresAdditionalDetails(MediaTypeMovie, parameterObject)))
+ return InternalError;
+
+ return HandleItems("movieid", "movies", items, parameterObject, result, true);
+}
+
+JSONRPC_STATUS CVideoLibrary::GetRecentlyAddedEpisodes(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CFileItemList items;
+ if (!videodatabase.GetRecentlyAddedEpisodesNav("videodb://recentlyaddedepisodes/", items, 0, RequiresAdditionalDetails(MediaTypeEpisode, parameterObject)))
+ return InternalError;
+
+ return HandleItems("episodeid", "episodes", items, parameterObject, result, true);
+}
+
+JSONRPC_STATUS CVideoLibrary::GetRecentlyAddedMusicVideos(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CFileItemList items;
+ if (!videodatabase.GetRecentlyAddedMusicVideosNav("videodb://recentlyaddedmusicvideos/", items, 0, RequiresAdditionalDetails(MediaTypeMusicVideo, parameterObject)))
+ return InternalError;
+
+ return HandleItems("musicvideoid", "musicvideos", items, parameterObject, result, true);
+}
+
+JSONRPC_STATUS CVideoLibrary::GetInProgressTVShows(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CFileItemList items;
+ if (!videodatabase.GetInProgressTvShowsNav("videodb://inprogresstvshows/", items, 0, RequiresAdditionalDetails(MediaTypeTvShow, parameterObject)))
+ return InternalError;
+
+ return HandleItems("tvshowid", "tvshows", items, parameterObject, result, false);
+}
+
+JSONRPC_STATUS CVideoLibrary::GetGenres(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string media = parameterObject["type"].asString();
+ StringUtils::ToLower(media);
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN;
+
+ std::string strPath = "videodb://";
+ /* select which video content to get genres from*/
+ if (media == MediaTypeMovie)
+ {
+ idContent = VideoDbContentType::MOVIES;
+ strPath += "movies";
+ }
+ else if (media == MediaTypeTvShow)
+ {
+ idContent = VideoDbContentType::TVSHOWS;
+ strPath += "tvshows";
+ }
+ else if (media == MediaTypeMusicVideo)
+ {
+ idContent = VideoDbContentType::MUSICVIDEOS;
+ strPath += "musicvideos";
+ }
+ strPath += "/genres/";
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CFileItemList items;
+ if (!videodatabase.GetGenresNav(strPath, items, idContent))
+ return InternalError;
+
+ /* need to set strTitle in each item*/
+ for (unsigned int i = 0; i < (unsigned int)items.Size(); i++)
+ items[i]->GetVideoInfoTag()->m_strTitle = items[i]->GetLabel();
+
+ HandleFileItemList("genreid", false, "genres", items, parameterObject, result);
+ return OK;
+}
+
+JSONRPC_STATUS CVideoLibrary::GetTags(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string media = parameterObject["type"].asString();
+ StringUtils::ToLower(media);
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN;
+
+ std::string strPath = "videodb://";
+ /* select which video content to get tags from*/
+ if (media == MediaTypeMovie)
+ {
+ idContent = VideoDbContentType::MOVIES;
+ strPath += "movies";
+ }
+ else if (media == MediaTypeTvShow)
+ {
+ idContent = VideoDbContentType::TVSHOWS;
+ strPath += "tvshows";
+ }
+ else if (media == MediaTypeMusicVideo)
+ {
+ idContent = VideoDbContentType::MUSICVIDEOS;
+ strPath += "musicvideos";
+ }
+ strPath += "/tags/";
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CFileItemList items;
+ if (!videodatabase.GetTagsNav(strPath, items, idContent))
+ return InternalError;
+
+ /* need to set strTitle in each item*/
+ for (int i = 0; i < items.Size(); i++)
+ items[i]->GetVideoInfoTag()->m_strTitle = items[i]->GetLabel();
+
+ HandleFileItemList("tagid", false, "tags", items, parameterObject, result);
+ return OK;
+}
+
+namespace
+{
+ const std::map<std::string, std::string> mediaIDTypes = {
+ {"episodeid", MediaTypeEpisode},
+ {"tvshowid", MediaTypeTvShow},
+ {"seasonid", MediaTypeSeason},
+ {"movieid", MediaTypeMovie},
+ {"setid", MediaTypeVideoCollection},
+ {"musicvideoid", MediaTypeMusicVideo},
+ };
+}
+
+JSONRPC_STATUS CVideoLibrary::GetAvailableArtTypes(const std::string& method, ITransportLayer* transport, IClient* client, const CVariant& parameterObject, CVariant& result)
+{
+ std::string mediaType;
+ int mediaID = -1;
+ for (const auto& mediaIDType : mediaIDTypes) {
+ if (parameterObject["item"].isMember(mediaIDType.first))
+ {
+ mediaType = mediaIDType.second;
+ mediaID = parameterObject["item"][mediaIDType.first].asInteger32();
+ break;
+ }
+ }
+ if (mediaID == -1)
+ return InternalError;
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVariant availablearttypes = CVariant(CVariant::VariantTypeArray);
+ for (const auto& artType : videodatabase.GetAvailableArtTypesForItem(mediaID, mediaType))
+ {
+ availablearttypes.append(artType);
+ }
+ result = CVariant(CVariant::VariantTypeObject);
+ result["availablearttypes"] = availablearttypes;
+
+ return OK;
+}
+
+JSONRPC_STATUS CVideoLibrary::GetAvailableArt(const std::string& method, ITransportLayer* transport, IClient* client, const CVariant& parameterObject, CVariant& result)
+{
+ std::string mediaType;
+ int mediaID = -1;
+ for (const auto& mediaIDType : mediaIDTypes) {
+ if (parameterObject["item"].isMember(mediaIDType.first))
+ {
+ mediaType = mediaIDType.second;
+ mediaID = parameterObject["item"][mediaIDType.first].asInteger32();
+ break;
+ }
+ }
+ if (mediaID == -1)
+ return InternalError;
+
+ std::string artType = parameterObject["arttype"].asString();
+ StringUtils::ToLower(artType);
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVariant availableart = CVariant(CVariant::VariantTypeArray);
+ for (const auto& artentry : videodatabase.GetAvailableArtForItem(mediaID, mediaType, artType))
+ {
+ CVariant item = CVariant(CVariant::VariantTypeObject);
+ item["url"] = CTextureUtils::GetWrappedImageURL(artentry.m_url);
+ item["arttype"] = artentry.m_aspect;
+ if (!artentry.m_preview.empty())
+ item["previewurl"] = CTextureUtils::GetWrappedImageURL(artentry.m_preview);
+ availableart.append(item);
+ }
+ result = CVariant(CVariant::VariantTypeObject);
+ result["availableart"] = availableart;
+
+ return OK;
+}
+
+JSONRPC_STATUS CVideoLibrary::SetMovieDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["movieid"].asInteger();
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVideoInfoTag infos;
+ if (!videodatabase.GetMovieInfo("", infos, id) || infos.m_iDbId <= 0)
+ return InvalidParams;
+
+ // get artwork
+ std::map<std::string, std::string> artwork;
+ videodatabase.GetArtForItem(infos.m_iDbId, infos.m_type, artwork);
+
+ int playcount = infos.GetPlayCount();
+ CDateTime lastPlayed = infos.m_lastPlayed;
+
+ std::set<std::string> removedArtwork;
+ std::set<std::string> updatedDetails;
+ UpdateVideoTag(parameterObject, infos, artwork, removedArtwork, updatedDetails);
+
+ if (videodatabase.UpdateDetailsForMovie(id, infos, artwork, updatedDetails) <= 0)
+ return InternalError;
+
+ if (!videodatabase.RemoveArtForItem(infos.m_iDbId, MediaTypeMovie, removedArtwork))
+ return InternalError;
+
+ if (playcount != infos.GetPlayCount() || lastPlayed != infos.m_lastPlayed)
+ {
+ // restore original playcount or the new one won't be announced
+ int newPlaycount = infos.GetPlayCount();
+ infos.SetPlayCount(playcount);
+ videodatabase.SetPlayCount(CFileItem(infos), newPlaycount, infos.m_lastPlayed);
+ }
+
+ UpdateResumePoint(parameterObject, infos, videodatabase);
+
+ CJSONRPCUtils::NotifyItemUpdated(infos, artwork);
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::SetMovieSetDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["setid"].asInteger();
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVideoInfoTag infos;
+ videodatabase.GetSetInfo(id, infos);
+ if (infos.m_iDbId <= 0)
+ {
+ videodatabase.Close();
+ return InvalidParams;
+ }
+
+ // get artwork
+ std::map<std::string, std::string> artwork;
+ videodatabase.GetArtForItem(infos.m_iDbId, infos.m_type, artwork);
+
+ std::set<std::string> removedArtwork;
+ std::set<std::string> updatedDetails;
+ UpdateVideoTag(parameterObject, infos, artwork, removedArtwork, updatedDetails);
+
+ if (videodatabase.SetDetailsForMovieSet(infos, artwork, id) <= 0)
+ return InternalError;
+
+ if (!videodatabase.RemoveArtForItem(infos.m_iDbId, "set", removedArtwork))
+ return InternalError;
+
+ CJSONRPCUtils::NotifyItemUpdated();
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::SetTVShowDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["tvshowid"].asInteger();
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVideoInfoTag infos;
+ if (!videodatabase.GetTvShowInfo("", infos, id) || infos.m_iDbId <= 0)
+ return InvalidParams;
+
+ // get artwork
+ std::map<std::string, std::string> artwork;
+ videodatabase.GetArtForItem(infos.m_iDbId, infos.m_type, artwork);
+
+ std::map<int, std::map<std::string, std::string> > seasonArt;
+ videodatabase.GetTvShowSeasonArt(infos.m_iDbId, seasonArt);
+
+ std::set<std::string> removedArtwork;
+ std::set<std::string> updatedDetails;
+ UpdateVideoTag(parameterObject, infos, artwork, removedArtwork, updatedDetails);
+
+ // we need to manually remove tags/taglinks for now because they aren't replaced
+ // due to scrapers not supporting them
+ videodatabase.RemoveTagsFromItem(id, MediaTypeTvShow);
+
+ if (!videodatabase.UpdateDetailsForTvShow(id, infos, artwork, seasonArt))
+ return InternalError;
+
+ if (!videodatabase.RemoveArtForItem(infos.m_iDbId, MediaTypeTvShow, removedArtwork))
+ return InternalError;
+
+ CJSONRPCUtils::NotifyItemUpdated();
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::SetSeasonDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["seasonid"].asInteger();
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVideoInfoTag infos;
+ videodatabase.GetSeasonInfo(id, infos);
+ if (infos.m_iDbId <= 0 || infos.m_iIdShow <= 0)
+ {
+ videodatabase.Close();
+ return InvalidParams;
+ }
+
+ // get artwork
+ std::map<std::string, std::string> artwork;
+ videodatabase.GetArtForItem(infos.m_iDbId, infos.m_type, artwork);
+
+ std::set<std::string> removedArtwork;
+ std::set<std::string> updatedDetails;
+ UpdateVideoTag(parameterObject, infos, artwork, removedArtwork, updatedDetails);
+ if (ParameterNotNull(parameterObject, "title"))
+ infos.SetSortTitle(parameterObject["title"].asString());
+
+ if (videodatabase.SetDetailsForSeason(infos, artwork, infos.m_iIdShow, id) <= 0)
+ return InternalError;
+
+ if (!videodatabase.RemoveArtForItem(infos.m_iDbId, MediaTypeSeason, removedArtwork))
+ return InternalError;
+
+ CJSONRPCUtils::NotifyItemUpdated();
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::SetEpisodeDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["episodeid"].asInteger();
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVideoInfoTag infos;
+ videodatabase.GetEpisodeInfo("", infos, id);
+ if (infos.m_iDbId <= 0)
+ {
+ videodatabase.Close();
+ return InvalidParams;
+ }
+
+ int tvshowid = videodatabase.GetTvShowForEpisode(id);
+ if (tvshowid <= 0)
+ {
+ videodatabase.Close();
+ return InvalidParams;
+ }
+
+ // get artwork
+ std::map<std::string, std::string> artwork;
+ videodatabase.GetArtForItem(infos.m_iDbId, infos.m_type, artwork);
+
+ int playcount = infos.GetPlayCount();
+ CDateTime lastPlayed = infos.m_lastPlayed;
+
+ std::set<std::string> removedArtwork;
+ std::set<std::string> updatedDetails;
+ UpdateVideoTag(parameterObject, infos, artwork, removedArtwork, updatedDetails);
+
+ if (videodatabase.SetDetailsForEpisode(infos, artwork, tvshowid, id) <= 0)
+ return InternalError;
+
+ if (!videodatabase.RemoveArtForItem(infos.m_iDbId, MediaTypeEpisode, removedArtwork))
+ return InternalError;
+
+ if (playcount != infos.GetPlayCount() || lastPlayed != infos.m_lastPlayed)
+ {
+ // restore original playcount or the new one won't be announced
+ int newPlaycount = infos.GetPlayCount();
+ infos.SetPlayCount(playcount);
+ videodatabase.SetPlayCount(CFileItem(infos), newPlaycount, infos.m_lastPlayed);
+ }
+
+ UpdateResumePoint(parameterObject, infos, videodatabase);
+
+ CJSONRPCUtils::NotifyItemUpdated();
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::SetMusicVideoDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["musicvideoid"].asInteger();
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVideoInfoTag infos;
+ videodatabase.GetMusicVideoInfo("", infos, id);
+ if (infos.m_iDbId <= 0)
+ {
+ videodatabase.Close();
+ return InvalidParams;
+ }
+
+ // get artwork
+ std::map<std::string, std::string> artwork;
+ videodatabase.GetArtForItem(infos.m_iDbId, infos.m_type, artwork);
+
+ int playcount = infos.GetPlayCount();
+ CDateTime lastPlayed = infos.m_lastPlayed;
+
+ std::set<std::string> removedArtwork;
+ std::set<std::string> updatedDetails;
+ UpdateVideoTag(parameterObject, infos, artwork, removedArtwork, updatedDetails);
+
+ // we need to manually remove tags/taglinks for now because they aren't replaced
+ // due to scrapers not supporting them
+ videodatabase.RemoveTagsFromItem(id, MediaTypeMusicVideo);
+
+ if (videodatabase.SetDetailsForMusicVideo(infos, artwork, id) <= 0)
+ return InternalError;
+
+ if (!videodatabase.RemoveArtForItem(infos.m_iDbId, MediaTypeMusicVideo, removedArtwork))
+ return InternalError;
+
+ if (playcount != infos.GetPlayCount()|| lastPlayed != infos.m_lastPlayed)
+ {
+ // restore original playcount or the new one won't be announced
+ int newPlaycount = infos.GetPlayCount();
+ infos.SetPlayCount(playcount);
+ videodatabase.SetPlayCount(CFileItem(infos), newPlaycount, infos.m_lastPlayed);
+ }
+
+ UpdateResumePoint(parameterObject, infos, videodatabase);
+
+ CJSONRPCUtils::NotifyItemUpdated();
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::RefreshMovie(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = static_cast<int>(parameterObject["movieid"].asInteger());
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVideoInfoTag infos;
+ if (!videodatabase.GetMovieInfo("", infos, id) || infos.m_iDbId <= 0)
+ return InvalidParams;
+
+ bool ignoreNfo = parameterObject["ignorenfo"].asBoolean();
+ std::string searchTitle = parameterObject["title"].asString();
+ CVideoLibraryQueue::GetInstance().RefreshItem(CFileItemPtr(new CFileItem(infos)), ignoreNfo, true, false, searchTitle);
+
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::RefreshTVShow(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = static_cast<int>(parameterObject["tvshowid"].asInteger());
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CFileItemPtr item(new CFileItem());
+ CVideoInfoTag infos;
+ if (!videodatabase.GetTvShowInfo("", infos, id, item.get()) || infos.m_iDbId <= 0)
+ return InvalidParams;
+
+ item->SetFromVideoInfoTag(infos);
+
+ bool ignoreNfo = parameterObject["ignorenfo"].asBoolean();
+ bool refreshEpisodes = parameterObject["refreshepisodes"].asBoolean();
+ std::string searchTitle = parameterObject["title"].asString();
+ CVideoLibraryQueue::GetInstance().RefreshItem(item, ignoreNfo, true, refreshEpisodes, searchTitle);
+
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::RefreshEpisode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = (int)parameterObject["episodeid"].asInteger();
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVideoInfoTag infos;
+ if (!videodatabase.GetEpisodeInfo("", infos, id) || infos.m_iDbId <= 0)
+ return InvalidParams;
+
+ CFileItemPtr item = CFileItemPtr(new CFileItem(infos));
+ // We need to set the correct base path to get the valid fanart
+ int tvshowid = infos.m_iIdShow;
+ if (tvshowid <= 0)
+ tvshowid = videodatabase.GetTvShowForEpisode(id);
+
+ bool ignoreNfo = parameterObject["ignorenfo"].asBoolean();
+ std::string searchTitle = parameterObject["title"].asString();
+ CVideoLibraryQueue::GetInstance().RefreshItem(item, ignoreNfo, true, false, searchTitle);
+
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::RefreshMusicVideo(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ int id = static_cast<int>(parameterObject["musicvideoid"].asInteger());
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ CVideoInfoTag infos;
+ if (!videodatabase.GetMusicVideoInfo("", infos, id) || infos.m_iDbId <= 0)
+ return InvalidParams;
+
+ bool ignoreNfo = parameterObject["ignorenfo"].asBoolean();
+ std::string searchTitle = parameterObject["title"].asString();
+ CVideoLibraryQueue::GetInstance().RefreshItem(CFileItemPtr(new CFileItem(infos)), ignoreNfo, true, false, searchTitle);
+
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::RemoveMovie(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return RemoveVideo(parameterObject);
+}
+
+JSONRPC_STATUS CVideoLibrary::RemoveTVShow(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return RemoveVideo(parameterObject);
+}
+
+JSONRPC_STATUS CVideoLibrary::RemoveEpisode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return RemoveVideo(parameterObject);
+}
+
+JSONRPC_STATUS CVideoLibrary::RemoveMusicVideo(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ return RemoveVideo(parameterObject);
+}
+
+JSONRPC_STATUS CVideoLibrary::Scan(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string directory = parameterObject["directory"].asString();
+ std::string cmd =
+ StringUtils::Format("updatelibrary(video, {}, {})", StringUtils::Paramify(directory),
+ parameterObject["showdialogs"].asBoolean() ? "true" : "false");
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr, cmd);
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::Export(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string cmd;
+ if (parameterObject["options"].isMember("path"))
+ cmd = StringUtils::Format("exportlibrary2(video, singlefile, {})",
+ StringUtils::Paramify(parameterObject["options"]["path"].asString()));
+ else
+ {
+ cmd = "exportlibrary2(video, separate, dummy";
+ if (parameterObject["options"]["images"].isBoolean() &&
+ parameterObject["options"]["images"].asBoolean() == true)
+ cmd += ", artwork";
+ if (parameterObject["options"]["overwrite"].isBoolean() &&
+ parameterObject["options"]["overwrite"].asBoolean() == true)
+ cmd += ", overwrite";
+ if (parameterObject["options"]["actorthumbs"].isBoolean() &&
+ parameterObject["options"]["actorthumbs"].asBoolean() == true)
+ cmd += ", actorthumbs";
+ cmd += ")";
+ }
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr, cmd);
+ return ACK;
+}
+
+JSONRPC_STATUS CVideoLibrary::Clean(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::string directory = parameterObject["directory"].asString();
+ std::string cmd;
+ if (parameterObject["content"].empty())
+ cmd = StringUtils::Format("cleanlibrary(video, {0}, {1})",
+ parameterObject["showdialogs"].asBoolean() ? "true" : "false",
+ StringUtils::Paramify(directory));
+ else
+ cmd = StringUtils::Format("cleanlibrary({0}, {1}, {2})", parameterObject["content"].asString(),
+ parameterObject["showdialogs"].asBoolean() ? "true" : "false",
+ StringUtils::Paramify(directory));
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr, cmd);
+ return ACK;
+}
+
+bool CVideoLibrary::FillFileItem(
+ const std::string& strFilename,
+ std::shared_ptr<CFileItem>& item,
+ const CVariant& parameterObject /* = CVariant(CVariant::VariantTypeArray) */)
+{
+ CVideoDatabase videodatabase;
+ if (strFilename.empty())
+ return false;
+
+ bool filled = false;
+ if (videodatabase.Open())
+ {
+ CVideoInfoTag details;
+ if (videodatabase.LoadVideoInfo(strFilename, details))
+ {
+ item->SetFromVideoInfoTag(details);
+ item->SetDynPath(strFilename);
+ filled = true;
+ }
+ }
+
+ if (item->GetLabel().empty())
+ {
+ item->SetLabel(CUtil::GetTitleFromPath(strFilename, false));
+ if (item->GetLabel().empty())
+ item->SetLabel(URIUtils::GetFileName(strFilename));
+ }
+
+ return filled;
+}
+
+bool CVideoLibrary::FillFileItemList(const CVariant &parameterObject, CFileItemList &list)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return false;
+
+ std::string file = parameterObject["file"].asString();
+ int movieID = (int)parameterObject["movieid"].asInteger(-1);
+ int episodeID = (int)parameterObject["episodeid"].asInteger(-1);
+ int musicVideoID = (int)parameterObject["musicvideoid"].asInteger(-1);
+ int recordingID = static_cast<int>(parameterObject["recordingid"].asInteger());
+
+ bool success = false;
+ CFileItemPtr fileItem(new CFileItem());
+ if (FillFileItem(file, fileItem))
+ {
+ success = true;
+ list.Add(fileItem);
+ }
+
+ if (movieID > 0)
+ {
+ CVideoInfoTag details;
+ videodatabase.GetMovieInfo("", details, movieID);
+ if (!details.IsEmpty())
+ {
+ list.Add(CFileItemPtr(new CFileItem(details)));
+ success = true;
+ }
+ }
+ if (episodeID > 0)
+ {
+ CVideoInfoTag details;
+ if (videodatabase.GetEpisodeInfo("", details, episodeID) && !details.IsEmpty())
+ {
+ list.Add(CFileItemPtr(new CFileItem(details)));
+ success = true;
+ }
+ }
+ if (musicVideoID > 0)
+ {
+ CVideoInfoTag details;
+ videodatabase.GetMusicVideoInfo("", details, musicVideoID);
+ if (!details.IsEmpty())
+ {
+ list.Add(CFileItemPtr(new CFileItem(details)));
+ success = true;
+ }
+ }
+ if (recordingID > 0)
+ {
+ std::shared_ptr<CFileItem> recordingFileItem =
+ CPVROperations::GetRecordingFileItem(recordingID);
+
+ if (recordingFileItem)
+ {
+ list.Add(recordingFileItem);
+ success = true;
+ }
+ }
+
+ return success;
+}
+
+int CVideoLibrary::RequiresAdditionalDetails(const MediaType& mediaType, const CVariant &parameterObject)
+{
+ if (mediaType != MediaTypeMovie && mediaType != MediaTypeTvShow && mediaType != MediaTypeEpisode && mediaType != MediaTypeMusicVideo)
+ return VideoDbDetailsNone;
+
+ return GetDetailsFromJsonParameters(parameterObject);
+}
+
+int CVideoLibrary::GetDetailsFromJsonParameters(const CVariant& parameterObject)
+{
+ const CVariant& properties = parameterObject["properties"];
+ int details = VideoDbDetailsNone;
+ for (CVariant::const_iterator_array itr = properties.begin_array(); itr != properties.end_array();
+ ++itr)
+ {
+ std::string propertyValue = itr->asString();
+ if (propertyValue == "cast")
+ details = details | VideoDbDetailsCast;
+ else if (propertyValue == "ratings")
+ details = details | VideoDbDetailsRating;
+ else if (propertyValue == "uniqueid")
+ details = details | VideoDbDetailsUniqueID;
+ else if (propertyValue == "showlink")
+ details = details | VideoDbDetailsShowLink;
+ else if (propertyValue == "streamdetails")
+ details = details | VideoDbDetailsStream;
+ else if (propertyValue == "tag")
+ details = details | VideoDbDetailsTag;
+ }
+ return details;
+}
+
+JSONRPC_STATUS CVideoLibrary::HandleItems(const char *idProperty, const char *resultName, CFileItemList &items, const CVariant &parameterObject, CVariant &result, bool limit /* = true */)
+{
+ int size = items.Size();
+ if (!limit && items.HasProperty("total") && items.GetProperty("total").asInteger() > size)
+ size = (int)items.GetProperty("total").asInteger();
+ HandleFileItemList(idProperty, true, resultName, items, parameterObject, result, size, limit);
+
+ return OK;
+}
+
+JSONRPC_STATUS CVideoLibrary::RemoveVideo(const CVariant &parameterObject)
+{
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ return InternalError;
+
+ if (parameterObject.isMember("movieid"))
+ videodatabase.DeleteMovie((int)parameterObject["movieid"].asInteger());
+ else if (parameterObject.isMember("tvshowid"))
+ videodatabase.DeleteTvShow((int)parameterObject["tvshowid"].asInteger());
+ else if (parameterObject.isMember("episodeid"))
+ videodatabase.DeleteEpisode((int)parameterObject["episodeid"].asInteger());
+ else if (parameterObject.isMember("musicvideoid"))
+ videodatabase.DeleteMusicVideo((int)parameterObject["musicvideoid"].asInteger());
+
+ CJSONRPCUtils::NotifyItemUpdated();
+ return ACK;
+}
+
+void CVideoLibrary::UpdateResumePoint(const CVariant &parameterObject, CVideoInfoTag &details, CVideoDatabase &videodatabase)
+{
+ if (!parameterObject["resume"].isNull())
+ {
+ double position = (double)parameterObject["resume"]["position"].asDouble();
+ if (position == 0.0)
+ videodatabase.ClearBookMarksOfFile(details.m_strFileNameAndPath, CBookmark::RESUME);
+ else
+ {
+ CBookmark bookmark;
+ double total = (double)parameterObject["resume"]["total"].asDouble();
+ if (total <= 0.0 && !videodatabase.GetResumeBookMark(details.m_strFileNameAndPath, bookmark))
+ bookmark.totalTimeInSeconds = details.m_streamDetails.GetVideoDuration();
+ else
+ bookmark.totalTimeInSeconds = total;
+
+ bookmark.timeInSeconds = position;
+ videodatabase.AddBookMarkToFile(details.m_strFileNameAndPath, bookmark, CBookmark::RESUME);
+ }
+ }
+}
+
+void CVideoLibrary::UpdateVideoTagField(const CVariant& parameterObject, const std::string& fieldName, std::vector<std::string>& fieldValue, std::set<std::string>& updatedDetails)
+{
+ if (ParameterNotNull(parameterObject, fieldName))
+ {
+ CopyStringArray(parameterObject[fieldName], fieldValue);
+ updatedDetails.insert(fieldName);
+ }
+}
+
+void CVideoLibrary::UpdateVideoTag(const CVariant &parameterObject, CVideoInfoTag& details, std::map<std::string, std::string> &artwork, std::set<std::string> &removedArtwork, std::set<std::string> &updatedDetails)
+{
+ if (ParameterNotNull(parameterObject, "title"))
+ details.SetTitle(parameterObject["title"].asString());
+ if (ParameterNotNull(parameterObject, "playcount"))
+ details.SetPlayCount(static_cast<int>(parameterObject["playcount"].asInteger()));
+ if (ParameterNotNull(parameterObject, "runtime"))
+ details.SetDuration(static_cast<int>(parameterObject["runtime"].asInteger()));
+
+ std::vector<std::string> director(details.m_director);
+ UpdateVideoTagField(parameterObject, "director", director, updatedDetails);
+ details.SetDirector(director);
+
+ std::vector<std::string> studio(details.m_studio);
+ UpdateVideoTagField(parameterObject, "studio", studio, updatedDetails);
+ details.SetStudio(studio);
+
+ if (ParameterNotNull(parameterObject, "plot"))
+ details.SetPlot(parameterObject["plot"].asString());
+ if (ParameterNotNull(parameterObject, "album"))
+ details.SetAlbum(parameterObject["album"].asString());
+
+ std::vector<std::string> artist(details.m_artist);
+ UpdateVideoTagField(parameterObject, "artist", artist, updatedDetails);
+ details.SetArtist(artist);
+
+ std::vector<std::string> genre(details.m_genre);
+ UpdateVideoTagField(parameterObject, "genre", genre, updatedDetails);
+ details.SetGenre(genre);
+
+ if (ParameterNotNull(parameterObject, "track"))
+ details.m_iTrack = (int)parameterObject["track"].asInteger();
+ if (ParameterNotNull(parameterObject, "rating"))
+ {
+ details.SetRating(parameterObject["rating"].asFloat());
+ updatedDetails.insert("ratings");
+ }
+ if (ParameterNotNull(parameterObject, "votes"))
+ {
+ details.SetVotes(StringUtils::ReturnDigits(parameterObject["votes"].asString()));
+ updatedDetails.insert("ratings"); //Votes and ratings both need updates now, this will trigger those
+ }
+ if (ParameterNotNull(parameterObject, "ratings"))
+ {
+ CVariant ratings = parameterObject["ratings"];
+ for (CVariant::const_iterator_map rIt = ratings.begin_map(); rIt != ratings.end_map(); ++rIt)
+ {
+ if (rIt->second.isObject() && ParameterNotNull(rIt->second, "rating"))
+ {
+ const auto& rating = rIt->second;
+ if (ParameterNotNull(rating, "votes"))
+ {
+ details.SetRating(rating["rating"].asFloat(),
+ static_cast<int>(rating["votes"].asInteger()),
+ rIt->first,
+ (ParameterNotNull(rating, "default") && rating["default"].asBoolean()));
+ }
+ else
+ details.SetRating(rating["rating"].asFloat(), rIt->first, (ParameterNotNull(rating, "default") && rating["default"].asBoolean()));
+
+ updatedDetails.insert("ratings");
+ }
+ else if (rIt->second.isNull())
+ {
+ details.RemoveRating(rIt->first);
+ updatedDetails.insert("ratings");
+ }
+ }
+ }
+ if (ParameterNotNull(parameterObject, "userrating"))
+ details.m_iUserRating = static_cast<int>(parameterObject["userrating"].asInteger());
+ if (ParameterNotNull(parameterObject, "mpaa"))
+ details.SetMPAARating(parameterObject["mpaa"].asString());
+ if (ParameterNotNull(parameterObject, "imdbnumber"))
+ {
+ details.SetUniqueID(parameterObject["imdbnumber"].asString());
+ updatedDetails.insert("uniqueid");
+ }
+ if (ParameterNotNull(parameterObject, "uniqueid"))
+ {
+ CVariant uniqueids = parameterObject["uniqueid"];
+ for (CVariant::const_iterator_map idIt = uniqueids.begin_map(); idIt != uniqueids.end_map();
+ ++idIt)
+ {
+ if (idIt->second.isString() && !idIt->second.asString().empty())
+ {
+ details.SetUniqueID(idIt->second.asString(), idIt->first);
+ updatedDetails.insert("uniqueid");
+ }
+ else if (idIt->second.isNull() && idIt->first != details.GetDefaultUniqueID())
+ {
+ details.RemoveUniqueID(idIt->first);
+ updatedDetails.insert("uniqueid");
+ }
+ }
+ }
+ if (ParameterNotNull(parameterObject, "premiered"))
+ {
+ CDateTime premiered;
+ SetFromDBDate(parameterObject["premiered"], premiered);
+ details.SetPremiered(premiered);
+ }
+ else if (ParameterNotNull(parameterObject, "year"))
+ details.SetYear((int)parameterObject["year"].asInteger());
+ if (ParameterNotNull(parameterObject, "lastplayed"))
+ SetFromDBDateTime(parameterObject["lastplayed"], details.m_lastPlayed);
+ if (ParameterNotNull(parameterObject, "firstaired"))
+ SetFromDBDate(parameterObject["firstaired"], details.m_firstAired);
+ if (ParameterNotNull(parameterObject, "productioncode"))
+ details.SetProductionCode(parameterObject["productioncode"].asString());
+ if (ParameterNotNull(parameterObject, "season"))
+ details.m_iSeason = (int)parameterObject["season"].asInteger();
+ if (ParameterNotNull(parameterObject, "episode"))
+ details.m_iEpisode = (int)parameterObject["episode"].asInteger();
+ if (ParameterNotNull(parameterObject, "originaltitle"))
+ details.SetOriginalTitle(parameterObject["originaltitle"].asString());
+ if (ParameterNotNull(parameterObject, "trailer"))
+ details.SetTrailer(parameterObject["trailer"].asString());
+ if (ParameterNotNull(parameterObject, "tagline"))
+ details.SetTagLine(parameterObject["tagline"].asString());
+ if (ParameterNotNull(parameterObject, "status"))
+ details.SetStatus(parameterObject["status"].asString());
+ if (ParameterNotNull(parameterObject, "plotoutline"))
+ details.SetPlotOutline(parameterObject["plotoutline"].asString());
+
+ std::vector<std::string> credits(details.m_writingCredits);
+ UpdateVideoTagField(parameterObject, "writer", credits, updatedDetails);
+ details.SetWritingCredits(credits);
+
+ std::vector<std::string> country(details.m_country);
+ UpdateVideoTagField(parameterObject, "country", country, updatedDetails);
+ details.SetCountry(country);
+
+ if (ParameterNotNull(parameterObject, "top250"))
+ details.m_iTop250 = (int)parameterObject["top250"].asInteger();
+ if (ParameterNotNull(parameterObject, "sorttitle"))
+ details.SetSortTitle(parameterObject["sorttitle"].asString());
+ if (ParameterNotNull(parameterObject, "episodeguide"))
+ details.SetEpisodeGuide(parameterObject["episodeguide"].asString());
+ if (ParameterNotNull(parameterObject, "set"))
+ {
+ details.SetSet(parameterObject["set"].asString());
+ updatedDetails.insert("set");
+ }
+
+ std::vector<std::string> showLink(details.m_showLink);
+ UpdateVideoTagField(parameterObject, "showlink", showLink, updatedDetails);
+ details.SetShowLink(showLink);
+
+ std::vector<std::string> tags(details.m_tags);
+ UpdateVideoTagField(parameterObject, "tag", tags, updatedDetails);
+ details.SetTags(tags);
+
+ if (ParameterNotNull(parameterObject, "thumbnail"))
+ {
+ std::string value = parameterObject["thumbnail"].asString();
+ artwork["thumb"] = StringUtils::Trim(value);
+ updatedDetails.insert("art.altered");
+ }
+ if (ParameterNotNull(parameterObject, "fanart"))
+ {
+ std::string value = parameterObject["fanart"].asString();
+ artwork["fanart"] = StringUtils::Trim(value);
+ updatedDetails.insert("art.altered");
+ }
+
+ if (ParameterNotNull(parameterObject, "art"))
+ {
+ CVariant art = parameterObject["art"];
+ for (CVariant::const_iterator_map artIt = art.begin_map(); artIt != art.end_map(); ++artIt)
+ {
+ if (artIt->second.isString() && !artIt->second.asString().empty())
+ {
+ artwork[artIt->first] = CTextureUtils::UnwrapImageURL(artIt->second.asString());
+ updatedDetails.insert("art.altered");
+ }
+ else if (artIt->second.isNull())
+ {
+ artwork.erase(artIt->first);
+ removedArtwork.insert(artIt->first);
+ }
+ }
+ }
+
+ if (ParameterNotNull(parameterObject, "dateadded"))
+ {
+ SetFromDBDateTime(parameterObject["dateadded"], details.m_dateAdded);
+ updatedDetails.insert("dateadded");
+ }
+}
diff --git a/xbmc/interfaces/json-rpc/VideoLibrary.h b/xbmc/interfaces/json-rpc/VideoLibrary.h
new file mode 100644
index 0000000..f91c4c8
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/VideoLibrary.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2016-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 "FileItemHandler.h"
+#include "JSONRPC.h"
+#include "utils/DatabaseUtils.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CFileItemList;
+class CVideoDatabase;
+class CVariant;
+
+namespace JSONRPC
+{
+ class CVideoLibrary : public CFileItemHandler
+ {
+ public:
+ static JSONRPC_STATUS GetMovies(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetMovieDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetMovieSets(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetMovieSetDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS GetTVShows(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetTVShowDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetSeasons(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetSeasonDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetEpisodes(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetEpisodeDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS GetMusicVideos(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetMusicVideoDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS GetRecentlyAddedMovies(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetRecentlyAddedEpisodes(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetRecentlyAddedMusicVideos(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetInProgressTVShows(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS GetGenres(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetTags(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetAvailableArtTypes(const std::string& method, ITransportLayer* transport, IClient* client, const CVariant& parameterObject, CVariant& result);
+ static JSONRPC_STATUS GetAvailableArt(const std::string& method, ITransportLayer* transport, IClient* client, const CVariant& parameterObject, CVariant& result);
+
+ static JSONRPC_STATUS SetMovieDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetMovieSetDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetTVShowDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetSeasonDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetEpisodeDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS SetMusicVideoDetails(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS RefreshMovie(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS RefreshTVShow(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS RefreshEpisode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS RefreshMusicVideo(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS RemoveMovie(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS RemoveTVShow(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS RemoveEpisode(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS RemoveMusicVideo(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static JSONRPC_STATUS Scan(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Export(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS Clean(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+
+ static bool FillFileItem(
+ const std::string& strFilename,
+ std::shared_ptr<CFileItem>& item,
+ const CVariant& parameterObject = CVariant(CVariant::VariantTypeArray));
+ static bool FillFileItemList(const CVariant &parameterObject, CFileItemList &list);
+ static void UpdateResumePoint(const CVariant &parameterObject, CVideoInfoTag &details, CVideoDatabase &videodatabase);
+
+ /*! \brief Provided the JSON-RPC parameter object compute the VideoDbDetails mask
+ * \param parameterObject the JSON parameter mask
+ * \return the mask value for the requested properties
+ */
+ static int GetDetailsFromJsonParameters(const CVariant& parameterObject);
+
+ private:
+ static int RequiresAdditionalDetails(const MediaType& mediaType, const CVariant &parameterObject);
+ static JSONRPC_STATUS HandleItems(const char *idProperty, const char *resultName, CFileItemList &items, const CVariant &parameterObject, CVariant &result, bool limit = true);
+ static JSONRPC_STATUS RemoveVideo(const CVariant &parameterObject);
+ static void UpdateVideoTag(const CVariant &parameterObject, CVideoInfoTag &details, std::map<std::string, std::string> &artwork, std::set<std::string> &removedArtwork, std::set<std::string>& updatedDetails);
+ static void UpdateVideoTagField(const CVariant& parameterObject, const std::string& fieldName, std::vector<std::string>& fieldValue, std::set<std::string>& updatedDetails);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/XBMCOperations.cpp b/xbmc/interfaces/json-rpc/XBMCOperations.cpp
new file mode 100644
index 0000000..67edc29
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/XBMCOperations.cpp
@@ -0,0 +1,88 @@
+/*
+ * 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 "XBMCOperations.h"
+
+#include "ServiceBroker.h"
+#include "messaging/ApplicationMessenger.h"
+#include "powermanagement/PowerManager.h"
+#include "utils/Variant.h"
+
+using namespace JSONRPC;
+
+JSONRPC_STATUS CXBMCOperations::GetInfoLabels(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::vector<std::string> info;
+
+ for (unsigned int i = 0; i < parameterObject["labels"].size(); i++)
+ {
+ std::string field = parameterObject["labels"][i].asString();
+ StringUtils::ToLower(field);
+
+ info.push_back(parameterObject["labels"][i].asString());
+ }
+
+ if (!info.empty())
+ {
+ std::vector<std::string> infoLabels;
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_INFOLABEL, -1, -1,
+ static_cast<void*>(&infoLabels), "", info);
+
+ for (unsigned int i = 0; i < info.size(); i++)
+ {
+ if (i >= infoLabels.size())
+ break;
+ result[info[i]] = infoLabels[i];
+ }
+ }
+
+ return OK;
+}
+
+JSONRPC_STATUS CXBMCOperations::GetInfoBooleans(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result)
+{
+ std::vector<std::string> info;
+
+ bool CanControlPower = (client->GetPermissionFlags() & ControlPower) > 0;
+
+ for (unsigned int i = 0; i < parameterObject["booleans"].size(); i++)
+ {
+ std::string field = parameterObject["booleans"][i].asString();
+ StringUtils::ToLower(field);
+
+ // Need to override power management of whats in infomanager since jsonrpc
+ // have a security layer aswell.
+ if (field == "system.canshutdown")
+ result[parameterObject["booleans"][i].asString()] = (CServiceBroker::GetPowerManager().CanPowerdown() && CanControlPower);
+ else if (field == "system.canpowerdown")
+ result[parameterObject["booleans"][i].asString()] = (CServiceBroker::GetPowerManager().CanPowerdown() && CanControlPower);
+ else if (field == "system.cansuspend")
+ result[parameterObject["booleans"][i].asString()] = (CServiceBroker::GetPowerManager().CanSuspend() && CanControlPower);
+ else if (field == "system.canhibernate")
+ result[parameterObject["booleans"][i].asString()] = (CServiceBroker::GetPowerManager().CanHibernate() && CanControlPower);
+ else if (field == "system.canreboot")
+ result[parameterObject["booleans"][i].asString()] = (CServiceBroker::GetPowerManager().CanReboot() && CanControlPower);
+ else
+ info.push_back(parameterObject["booleans"][i].asString());
+ }
+
+ if (!info.empty())
+ {
+ std::vector<bool> infoLabels;
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_INFOBOOL, -1, -1,
+ static_cast<void*>(&infoLabels), "", info);
+ for (unsigned int i = 0; i < info.size(); i++)
+ {
+ if (i >= infoLabels.size())
+ break;
+ result[info[i].c_str()] = CVariant(infoLabels[i]);
+ }
+ }
+
+ return OK;
+}
diff --git a/xbmc/interfaces/json-rpc/XBMCOperations.h b/xbmc/interfaces/json-rpc/XBMCOperations.h
new file mode 100644
index 0000000..af56172
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/XBMCOperations.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "JSONRPC.h"
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CXBMCOperations
+ {
+ public:
+ static JSONRPC_STATUS GetInfoLabels(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ static JSONRPC_STATUS GetInfoBooleans(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant &parameterObject, CVariant &result);
+ };
+}
diff --git a/xbmc/interfaces/json-rpc/schema/CMakeLists.txt b/xbmc/interfaces/json-rpc/schema/CMakeLists.txt
new file mode 100644
index 0000000..a4d5583
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/schema/CMakeLists.txt
@@ -0,0 +1,30 @@
+set(JSON_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/version.txt
+ ${CMAKE_CURRENT_SOURCE_DIR}/license.txt
+ ${CMAKE_CURRENT_SOURCE_DIR}/methods.json
+ ${CMAKE_CURRENT_SOURCE_DIR}/types.json
+ ${CMAKE_CURRENT_SOURCE_DIR}/notifications.json)
+
+add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/${CORE_BUILD_DIR}/ServiceDescription.h
+ COMMAND JsonSchemaBuilder::JsonSchemaBuilder ${JSON_SRCS}
+ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/${CORE_BUILD_DIR}
+ DEPENDS ${JSON_SRCS}
+ COMMENT "Generating ServiceDescription.h")
+
+add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/addons/xbmc.json/addon.xml
+ COMMAND ${CMAKE_COMMAND}
+ -DCMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}
+ -DCORE_BINARY_DIR=${CMAKE_BINARY_DIR}
+ -DCORE_SYSTEM_NAME=${CORE_SYSTEM_NAME}
+ -P ${CMAKE_CURRENT_SOURCE_DIR}/GenerateAddonXml.cmake
+ WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
+ DEPENDS ${JSON_SRCS} ${CMAKE_SOURCE_DIR}/addons/xbmc.json/addon.xml.in
+ COMMENT "Generating xbmc.json/addon.xml")
+
+add_custom_target(generate_json_header ALL
+ DEPENDS ${CMAKE_BINARY_DIR}/${CORE_BUILD_DIR}/ServiceDescription.h
+ ${CMAKE_BINARY_DIR}/addons/xbmc.json/addon.xml)
+set_target_properties(generate_json_header PROPERTIES FOLDER "Build Utilities")
+
+if(BOOTSTRAP_IN_TREE)
+ add_dependencies(generate_json_header JsonSchemaBuilder::JsonSchemaBuilder)
+endif()
diff --git a/xbmc/interfaces/json-rpc/schema/GenerateAddonXml.cmake b/xbmc/interfaces/json-rpc/schema/GenerateAddonXml.cmake
new file mode 100644
index 0000000..7f0817b
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/schema/GenerateAddonXml.cmake
@@ -0,0 +1,6 @@
+include(${CMAKE_SOURCE_DIR}/cmake/scripts/common/Macros.cmake)
+core_find_versions()
+
+file(REMOVE ${CORE_BINARY_DIR}/addons/xbmc.json/addon.xml)
+configure_file(${CMAKE_SOURCE_DIR}/addons/xbmc.json/addon.xml.in
+ ${CORE_BINARY_DIR}/addons/xbmc.json/addon.xml @ONLY)
diff --git a/xbmc/interfaces/json-rpc/schema/license.txt b/xbmc/interfaces/json-rpc/schema/license.txt
new file mode 100644
index 0000000..97af310
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/schema/license.txt
@@ -0,0 +1,7 @@
+/*
+ * 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.
+ */
diff --git a/xbmc/interfaces/json-rpc/schema/methods.json b/xbmc/interfaces/json-rpc/schema/methods.json
new file mode 100644
index 0000000..4d396cf
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/schema/methods.json
@@ -0,0 +1,2941 @@
+{
+ "JSONRPC.Introspect": {
+ "type": "method",
+ "description": "Enumerates all actions and descriptions",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "getdescriptions", "type": "boolean", "default": true },
+ { "name": "getmetadata", "type": "boolean", "default": false },
+ { "name": "filterbytransport", "type": "boolean", "default": true },
+ { "name": "filter", "type": "object",
+ "properties": {
+ "id": { "type": "string", "required": true, "description": "Name of a namespace, method or type" },
+ "type": { "type": "string", "required": true, "enum": [ "method", "namespace", "type", "notification" ], "description": "Type of the given name" },
+ "getreferences": { "type": "boolean", "default": true, "description": "Whether or not to print the schema for referenced types" }
+ }
+ }
+ ],
+ "returns": "object"
+ },
+ "JSONRPC.Version": {
+ "type": "method",
+ "description": "Retrieve the JSON-RPC protocol version.",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "version": { "type": "object", "required": true,
+ "properties": {
+ "major": { "type": "integer", "minimum": 0, "required": true, "description": "Bumped on backwards incompatible changes to the API definition" },
+ "minor": { "type": "integer", "minimum": 0, "required": true, "description": "Bumped on backwards compatible additions/changes to the API definition" },
+ "patch": { "type": "integer", "minimum": 0, "required": true, "description": "Bumped on any changes to the internal implementation but not to the API definition" }
+ }
+ }
+ }
+ }
+ },
+ "JSONRPC.Permission": {
+ "type": "method",
+ "description": "Retrieve the clients permissions",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "ReadData": { "type": "boolean", "required": true },
+ "ControlPlayback": { "type": "boolean", "required": true },
+ "ControlNotify": { "type": "boolean", "required": true },
+ "ControlPower": { "type": "boolean", "required": true },
+ "UpdateData": { "type": "boolean", "required": true },
+ "RemoveData": { "type": "boolean", "required": true },
+ "Navigate": { "type": "boolean", "required": true },
+ "WriteFile": { "type": "boolean", "required": true },
+ "ControlSystem": { "type": "boolean", "required": true },
+ "ControlGUI": { "type": "boolean", "required": true },
+ "ManageAddon": { "type": "boolean", "required": true },
+ "ExecuteAddon": { "type": "boolean", "required": true },
+ "ControlPVR": { "type": "boolean", "required": true }
+ }
+ }
+ },
+ "JSONRPC.Ping": {
+ "type": "method",
+ "description": "Ping responder",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [],
+ "returns": "string"
+ },
+ "JSONRPC.GetConfiguration": {
+ "type": "method",
+ "description": "Get client-specific configurations",
+ "transport": "Announcing",
+ "permission": "ReadData",
+ "params": [],
+ "returns": { "$ref": "Configuration" }
+ },
+ "JSONRPC.SetConfiguration": {
+ "type": "method",
+ "description": "Change the client-specific configuration",
+ "transport": "Announcing",
+ "permission": "ControlNotify",
+ "params": [
+ { "name": "notifications", "type": "object",
+ "properties": {
+ "Player": { "$ref": "Optional.Boolean" },
+ "Playlist": { "$ref": "Optional.Boolean" },
+ "GUI": { "$ref": "Optional.Boolean" },
+ "System": { "$ref": "Optional.Boolean" },
+ "AudioLibrary": { "$ref": "Optional.Boolean" },
+ "VideoLibrary": { "$ref": "Optional.Boolean" },
+ "Application": { "$ref": "Optional.Boolean" },
+ "Input": { "$ref": "Optional.Boolean" },
+ "Other": { "$ref": "Optional.Boolean" }
+ }
+ }
+ ],
+ "returns": { "$ref": "Configuration" }
+ },
+ "JSONRPC.NotifyAll": {
+ "type": "method",
+ "description": "Notify all other connected clients",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "message", "type": "string", "required": true },
+ { "name": "data", "type": "any", "default": null }
+ ],
+ "returns": "any"
+ },
+ "Player.Open": {
+ "type": "method",
+ "description": "Start playback of either the playlist with the given ID, a slideshow with the pictures from the given directory or a single file or an item from the database.",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "item",
+ "type": [
+ { "type": "object", "required": true, "additionalProperties": false,
+ "properties": {
+ "playlistid": { "$ref": "Playlist.Id", "required": true },
+ "position": { "$ref": "Playlist.Position", "default": 0 }
+ }
+ },
+ { "$ref": "Playlist.Item", "required": true },
+ { "type": "object", "required": true, "additionalProperties": false,
+ "properties": {
+ "path": { "type": "string", "required": true },
+ "random": { "type": "boolean", "default": true, "description": "Deprecated, use the shuffled property of the options parameter instead" },
+ "recursive": { "type": "boolean", "default": true }
+ }
+ },
+ { "type": "object", "required": true, "additionalProperties": false,
+ "properties": {
+ "partymode": { "type": [
+ { "type": "string", "required": true, "enum": [ "music", "video" ] },
+ { "type": "string", "required": true, "minLength": 5, "description": "Path to a smartplaylist (*.xsp) file" }
+ ]
+ }
+ }
+ },
+ { "type": "object", "required": true, "additionalProperties": false,
+ "properties": {
+ "broadcastid": { "$ref": "Library.Id", "required": true }
+ }
+ },
+ { "type": "object", "required": true, "additionalProperties": false,
+ "properties": {
+ "channelid": { "$ref": "Library.Id", "required": true }
+ }
+ },
+ { "type": "object", "required": true, "additionalProperties": false,
+ "properties": {
+ "recordingid": { "$ref": "Library.Id", "required": true }
+ }
+ }
+ ]
+ },
+ { "name": "options", "type": "object", "additionalProperties": false,
+ "properties": {
+ "playername": { "type": [
+ "null",
+ { "type": "string", "enum": [ "default" ], "required": true },
+ { "type": "string", "minLength": 1, "required": true, "description": "name of player" }
+ ],
+ "default": null
+ },
+ "shuffled": { "$ref": "Optional.Boolean" },
+ "repeat": { "type": [ "null", { "$ref": "Player.Repeat", "required": true } ], "default": null },
+ "resume": { "type": [
+ { "type": "boolean", "required": true, "description": "Whether to resume from the resume point or not" },
+ { "$ref": "Player.Position.Percentage", "required": true, "description": "Percentage value to start from" },
+ { "$ref": "Player.Position.Time", "required": true, "description": "Time to start from" }
+ ],
+ "default": false
+ }
+ }
+ }
+ ],
+ "returns": "string"
+ },
+ "Player.GetActivePlayers": {
+ "type": "method",
+ "description": "Returns all active players",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [],
+ "returns": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "object",
+ "properties": {
+ "playerid": { "$ref": "Player.Id", "required": true },
+ "type": { "$ref": "Player.Type", "required": true },
+ "playertype": { "type": "string", "enum": [ "internal", "external", "remote" ], "required": true }
+ }
+ }
+ }
+ },
+ "Player.GetPlayers": {
+ "type": "method",
+ "description": "Get a list of available players",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "media", "type": "string", "enum": [ "all", "video", "audio" ], "default": "all" }
+ ],
+ "returns": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": { "$ref": "Global.String.NotEmpty", "required": true },
+ "type": { "type": "string", "enum": [ "internal", "external", "remote" ], "required": true },
+ "playsvideo": { "type": "boolean", "required": true },
+ "playsaudio": { "type": "boolean", "required": true }
+ }
+ }
+ }
+ },
+ "Player.GetProperties": {
+ "type": "method",
+ "description": "Retrieves the values of the given properties",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "properties", "type": "array", "uniqueItems": true, "required": true, "items": { "$ref": "Player.Property.Name" } }
+ ],
+ "returns": { "$ref": "Player.Property.Value", "required": true }
+ },
+ "Player.GetItem": {
+ "type": "method",
+ "description": "Retrieves the currently played item",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "properties", "$ref": "List.Fields.All" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "item": { "$ref": "List.Item.All", "required": true }
+ }
+ }
+ },
+ "Player.PlayPause": {
+ "type": "method",
+ "description": "Pauses or unpause playback and returns the new state",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "play", "$ref": "Global.Toggle", "default": "toggle" }
+ ],
+ "returns": { "$ref": "Player.Speed" }
+ },
+ "Player.Stop": {
+ "type": "method",
+ "description": "Stops playback",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true }
+ ],
+ "returns": "string"
+ },
+ "Player.GetAudioDelay": {
+ "type": "method",
+ "description": "Get the audio delay for the current playback",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "offset": {
+ "type": "number",
+ "description": "The offset value used in the current playback.",
+ "required": true
+ }
+ }
+ }
+ },
+ "Player.SetAudioDelay": {
+ "type": "method",
+ "description": "Set the audio delay for the current playback",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ {
+ "name": "playerid",
+ "$ref": "Player.Id",
+ "required": true
+ },
+ {
+ "name": "offset",
+ "type": [
+ {
+ "type": "number",
+ "description": "The value should be a multiple of 0.025 in a range of +/-10 (the default range can be overriden by advancedsettings.xml).",
+ "required": true
+ },
+ {
+ "$ref": "Global.IncrementDecrement",
+ "required": true
+ }
+ ],
+ "required": true
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "offset": {
+ "type": "number",
+ "description": "The offset value used in the current playback.",
+ "required": true
+ }
+ }
+ }
+ },
+ "Player.SetSpeed": {
+ "type": "method",
+ "description": "Set the speed of the current playback",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "speed", "type": [
+ { "type": "integer", "required": true, "enum": [ -32, -16, -8, -4, -2, -1, 0, 1, 2, 4, 8, 16, 32 ] },
+ { "$ref": "Global.IncrementDecrement", "required": true }
+ ],
+ "required": true
+ }
+ ],
+ "returns": { "$ref": "Player.Speed" }
+ },
+ "Player.Seek": {
+ "type": "method",
+ "description": "Seek through the playing item",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "value", "required": true, "type": [
+ { "type": "object", "properties": { "percentage": { "$ref": "Player.Position.Percentage", "required": true, "description": "Percentage value to seek to" } }, "additionalProperties": false, "required": true },
+ { "type": "object", "properties": { "time": { "$ref": "Player.Position.Time", "required": true, "description": "Time to seek to" } }, "additionalProperties": false, "required": true },
+ { "type": "object", "properties": { "step": { "type": "string", "enum": [ "smallforward", "smallbackward", "bigforward", "bigbackward" ], "required": true, "description": "Seek by predefined jumps" } }, "additionalProperties": false, "required": true },
+ { "type": "object", "properties": { "seconds": { "type": "integer", "required": true, "description": "Seek by the given number of seconds" } }, "additionalProperties": false, "required": true }
+ ]
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "percentage": { "$ref": "Player.Position.Percentage" },
+ "time": { "$ref": "Global.Time" },
+ "totaltime": { "$ref": "Global.Time" }
+ }
+ }
+ },
+ "Player.Move": {
+ "type": "method",
+ "description": "If picture is zoomed move viewport left/right/up/down otherwise skip previous/next",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "direction", "type": "string", "enum": [ "left", "right", "up", "down" ], "required": true }
+ ],
+ "returns": "string"
+ },
+ "Player.Zoom": {
+ "type": "method",
+ "description": "Zoom current picture",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "zoom", "type": [
+ { "type": "string", "enum": [ "in", "out" ], "required": true },
+ { "type": "integer", "minimum": 1, "maximum": 10, "description": "zoom level", "required": true }
+ ],
+ "required": true }
+ ],
+ "returns": "string"
+ },
+ "Player.SetViewMode": {
+ "type": "method",
+ "description": "Set view mode of video player",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "viewmode", "type": [
+ { "$ref": "Player.CustomViewMode", "description": "Custom view mode", "required": true },
+ { "name": "value", "$ref": "Player.ViewMode", "required": true}
+ ],
+ "required": true }
+ ],
+ "returns": "string"
+ },
+ "Player.GetViewMode": {
+ "type": "method",
+ "description": "Get view mode of video player",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "viewmode": { "$ref": "Player.ViewMode", "required": true },
+ "zoom": { "type": "number", "required": true },
+ "pixelratio": { "type": "number", "required": true },
+ "verticalshift": { "type": "number", "required": true },
+ "nonlinearstretch": { "type": "boolean", "required": true }
+ }
+ }
+ },
+ "Player.Rotate": {
+ "type": "method",
+ "description": "Rotates current picture",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "value", "type": "string", "enum": [ "clockwise", "counterclockwise" ], "default": "clockwise" }
+ ],
+ "returns": "string"
+ },
+ "Player.GoTo": {
+ "type": "method",
+ "description": "Go to previous/next/specific item in the playlist",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "to", "type": [
+ { "type": "string", "enum": [ "previous", "next" ], "required": true },
+ { "$ref": "Playlist.Position", "description": "position in playlist", "required": true }
+ ],
+ "required": true }
+ ],
+ "returns": "string"
+ },
+ "Player.SetShuffle": {
+ "type": "method",
+ "description": "Shuffle/Unshuffle items in the player",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "shuffle", "$ref": "Global.Toggle", "required": true }
+ ],
+ "returns": "string"
+ },
+ "Player.SetRepeat": {
+ "type": "method",
+ "description": "Set the repeat mode of the player",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "repeat", "type": [
+ { "$ref": "Player.Repeat", "required": true },
+ { "type": "string", "enum": [ "cycle" ], "required": true }
+ ],
+ "required": true
+ }
+ ],
+ "returns": "string"
+ },
+ "Player.SetPartymode": {
+ "type": "method",
+ "description": "Turn partymode on or off",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "partymode", "$ref": "Global.Toggle", "required": true }
+ ],
+ "returns": "string"
+ },
+ "Player.SetAudioStream": {
+ "type": "method",
+ "description": "Set the audio stream played by the player",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "stream", "required": true, "type": [
+ { "type": "string", "enum": [ "previous", "next" ] },
+ { "type": "integer", "minimum": 0, "description": "Index of the audio stream to play" }
+ ]
+ }
+ ],
+ "returns": "string"
+ },
+ "Player.SetVideoStream": {
+ "type": "method",
+ "description": "Set the video stream played by the player",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "stream", "required": true, "type": [
+ { "type": "string", "enum": [ "previous", "next" ] },
+ { "type": "integer", "minimum": 0, "description": "Index of the video stream to play" }
+ ]
+ }
+ ],
+ "returns": "string"
+ },
+ "Player.AddSubtitle": {
+ "type": "method",
+ "description": "Add subtitle to the player",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "subtitle", "type": "string", "required": true, "description": "Local path or remote URL to the subtitle file to load" }
+ ],
+ "returns": "string"
+ },
+ "Player.SetSubtitle": {
+ "type": "method",
+ "description": "Set the subtitle displayed by the player",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playerid", "$ref": "Player.Id", "required": true },
+ { "name": "subtitle", "required": true, "type": [
+ { "type": "string", "enum": [ "previous", "next", "off", "on" ] },
+ { "type": "integer", "minimum": 0, "description": "Index of the subtitle to display" }
+ ]
+ },
+ { "name": "enable", "type": "boolean", "default": false, "description": "Whether to enable subtitles to be displayed after setting the new subtitle" }
+ ],
+ "returns": "string"
+ },
+ "Playlist.GetPlaylists": {
+ "type": "method",
+ "description": "Returns all existing playlists",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [],
+ "returns": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "object",
+ "properties": {
+ "playlistid": { "$ref": "Playlist.Id", "required": true },
+ "type": { "$ref": "Playlist.Type", "required": true }
+ }
+ }
+ }
+ },
+ "Playlist.GetProperties": {
+ "type": "method",
+ "description": "Retrieves the values of the given properties",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "playlistid", "$ref": "Playlist.Id", "required": true },
+ { "name": "properties", "type": "array", "uniqueItems": true, "required": true, "items": { "$ref": "Playlist.Property.Name" } }
+ ],
+ "returns": { "$ref": "Playlist.Property.Value", "required": true }
+ },
+ "Playlist.GetItems": {
+ "type": "method",
+ "description": "Get all items from playlist",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "playlistid", "$ref": "Playlist.Id", "required": true },
+ { "name": "properties", "$ref": "List.Fields.All" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "items": { "type": "array", "items": { "$ref": "List.Item.All" }, "required": true }
+ }
+ }
+ },
+ "Playlist.Add": {
+ "type": "method",
+ "description": "Add item(s) to playlist",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playlistid", "$ref": "Playlist.Id", "required": true },
+ { "name": "item",
+ "type": [
+ { "$ref": "Playlist.Item", "required": true },
+ { "type": "array", "items": { "$ref": "Playlist.Item" }, "required": true }
+ ],
+ "required": true }
+ ],
+ "returns": "string"
+ },
+ "Playlist.Insert": {
+ "type": "method",
+ "description": "Insert item(s) into playlist. Does not work for picture playlists (aka slideshows).",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playlistid", "$ref": "Playlist.Id", "required": true },
+ { "name": "position", "$ref": "Playlist.Position", "required": true },
+ { "name": "item",
+ "type": [
+ { "$ref": "Playlist.Item", "required": true },
+ { "type": "array", "items": { "$ref": "Playlist.Item" }, "required": true }
+ ],
+ "required": true }
+ ],
+ "returns": "string"
+ },
+ "Playlist.Remove": {
+ "type": "method",
+ "description": "Remove item from playlist. Does not work for picture playlists (aka slideshows).",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playlistid", "$ref": "Playlist.Id", "required": true },
+ { "name": "position", "$ref": "Playlist.Position", "required": true }
+ ],
+ "returns": "string"
+ },
+ "Playlist.Clear": {
+ "type": "method",
+ "description": "Clear playlist",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playlistid", "$ref": "Playlist.Id", "required": true }
+ ],
+ "returns": "string"
+ },
+ "Playlist.Swap": {
+ "type": "method",
+ "description": "Swap items in the playlist. Does not work for picture playlists (aka slideshows).",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "playlistid", "$ref": "Playlist.Id", "required": true },
+ { "name": "position1", "$ref": "Playlist.Position", "required": true },
+ { "name": "position2", "$ref": "Playlist.Position", "required": true }
+ ],
+ "returns": "string"
+ },
+ "Files.GetSources": {
+ "type": "method",
+ "description": "Get the sources of the media windows",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "media", "$ref": "Files.Media", "required": true },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "sources": { "$ref": "List.Items.Sources", "required": true }
+ }
+ }
+ },
+ "Files.PrepareDownload": {
+ "type": "method",
+ "description": "Provides a way to download a given file (e.g. providing an URL to the real file location)",
+ "transport": [ "Response", "FileDownloadRedirect" ],
+ "permission": "ReadData",
+ "params": [
+ { "name": "path", "type": "string", "required": true }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "protocol": { "type": "string", "enum": [ "http" ], "required": true },
+ "details": { "type": "any", "required": true, "description": "Transport specific details on how/from where to download the given file" },
+ "mode": { "type": "string", "enum": [ "redirect", "direct" ], "required": true, "description": "Direct mode allows using Files.Download whereas redirect mode requires the usage of a different protocol" }
+ }
+ }
+ },
+ "Files.Download": {
+ "type": "method",
+ "description": "Downloads the given file",
+ "transport": [ "Response", "FileDownloadDirect" ],
+ "permission": "ReadData",
+ "params": [
+ { "name": "path", "type": "string", "required": true }
+ ],
+ "returns": { "type": "any", "required": true }
+ },
+ "Files.GetDirectory": {
+ "type": "method",
+ "description": "Get the directories and files in the given directory",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "directory", "type": "string", "required": true },
+ { "name": "media", "$ref": "Files.Media", "default": "files" },
+ { "name": "properties", "$ref": "List.Fields.Files" },
+ { "name": "sort", "$ref": "List.Sort" },
+ { "name": "limits", "$ref": "List.Limits", "description": "Limits are applied after getting the directory content thus retrieval is not faster when they are applied." }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "files": { "type": "array", "items": { "$ref": "List.Item.File" }, "required": true }
+ }
+ }
+ },
+ "Files.GetFileDetails": {
+ "type": "method",
+ "description": "Get details for a specific file",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "file", "type": "string", "required": true, "description": "Full path to the file" },
+ { "name": "media", "$ref": "Files.Media", "default": "files" },
+ { "name": "properties", "$ref": "List.Fields.Files" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "filedetails": { "$ref": "List.Item.File", "required": true }
+ }
+ }
+ },
+ "Files.SetFileDetails": {
+ "type": "method",
+ "description": "Update the given specific file with the given details",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "file", "type": "string", "required": true, "description": "Full path to the file" },
+ { "name": "media", "$ref": "Files.Media", "required": true, "description": "File type to update correct database. Currently only \"video\" is supported." },
+ { "name": "playcount", "$ref": "Optional.Integer" },
+ { "name": "lastplayed", "$ref": "Optional.String", "description": "Setting a valid lastplayed without a playcount will force playcount to 1." },
+ { "name": "resume", "type": [ "null", { "$ref": "Video.Resume", "required": true } ], "default": null }
+ ],
+ "returns": "string"
+ },
+ "AudioLibrary.GetProperties": {
+ "type": "method",
+ "description": "Retrieves the values of the music library properties",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "type": "array", "uniqueItems": true, "required": true, "items": { "$ref": "Audio.Property.Name" } }
+ ],
+ "returns": { "$ref": "Audio.Property.Value", "required": true }
+ },
+ "AudioLibrary.GetArtists": {
+ "type": "method",
+ "description": "Retrieve all artists. For backward compatibility by default this implicitly does not include those that only contribute other roles, however absolutely all artists can be returned using allroles=true",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "albumartistsonly", "$ref": "Optional.Boolean", "description": "Whether or not to only include album artists rather than the artists of only individual songs as well. If the parameter is not passed or is passed as null the GUI setting will be used" },
+ { "name": "properties", "$ref": "Audio.Fields.Artist" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" },
+ { "name": "filter",
+ "type": [
+ { "type": "object", "properties": { "genreid": { "$ref": "Library.Id", "required": true, "description": "Deprecated, use songgenreid. Filter for existence of songs with this genre" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "songgenreid": { "$ref": "Library.Id", "required": true, "description": "Song genreid. Filter for existence of songs with this genre" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "songgenreid": { "$ref": "Library.Id", "required": true },
+ "roleid": { "$ref": "Library.Id", "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "songgenreid": { "$ref": "Library.Id", "required": true },
+ "role": { "type": "string", "minLength": 1, "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "genre": { "type": "string", "minLength": 1, "required": true, "description": "Deprecated, use songgenre. Filter for existence of songs with this genre" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "songgenre": { "type": "string", "minLength": 1, "required": true, "description": "Song genre. Filter for existence of songs with this genre" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "songgenre": { "type": "string", "minLength": 1, "required": true },
+ "roleid": { "$ref": "Library.Id", "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "songgenre": { "type": "string", "minLength": 1, "required": true },
+ "role": { "type": "string", "minLength": 1, "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "albumid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "album": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "songid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "songid": { "$ref": "Library.Id", "required": true },
+ "roleid": { "$ref": "Library.Id", "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "songid": { "$ref": "Library.Id", "required": true },
+ "role": { "type": "string", "minLength": 1, "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "roleid": { "$ref": "Library.Id", "required": true, "description": "Role contributed by artist. Overridden by allroles parameter" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "role": { "type": "string", "minLength": 1, "required": true, "description": "Role contributed by artist. Overridden by allroles parameter" } }, "additionalProperties": false },
+ { "$ref": "List.Filter.Artists" }
+ ]
+ },
+ { "name": "allroles", "type": "boolean", "default":false, "description": "Whether or not to include all artists irrespective of the role they contributed. When true it overrides any role filter value." }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "artists": { "type": "array",
+ "items": { "$ref": "Audio.Details.Artist" }
+ }
+ }
+ }
+ },
+ "AudioLibrary.GetArtistDetails": {
+ "type": "method",
+ "description": "Retrieve details about a specific artist",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "artistid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "Audio.Fields.Artist" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "artistdetails": { "$ref": "Audio.Details.Artist" }
+ }
+ }
+ },
+ "AudioLibrary.GetAlbums": {
+ "type": "method",
+ "description": "Retrieve all albums from specified artist (and role) or that has songs of the specified genre",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Audio.Fields.Album" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" },
+ { "name": "filter",
+ "type": [
+ { "type": "object", "properties": { "genreid": { "$ref": "Library.Id", "required": true, "description": "Song genre. Filter for existence of songs with this genre" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "genre": { "type": "string", "minLength": 1, "required": true, "description": "Song genre. Filter for existence of songs with this genre" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "artistid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "artistid": { "$ref": "Library.Id", "required": true },
+ "roleid": { "$ref": "Library.Id", "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "artistid": { "$ref": "Library.Id", "required": true },
+ "role": { "type": "string", "minLength": 1, "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "artist": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "artist": { "type": "string", "minLength": 1, "required": true },
+ "roleid": { "$ref": "Library.Id", "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "artist": { "type": "string", "minLength": 1, "required": true },
+ "role": { "type": "string", "minLength": 1, "required": true }}, "additionalProperties": false },
+ { "$ref": "List.Filter.Albums" }
+ ]
+ },
+ { "name": "includesingles", "type": "boolean", "default": false },
+ { "name": "allroles", "type": "boolean", "default":false, "description": "Whether or not to include all roles when filtering by artist, rather than the default of excluding other contributions. When true it overrides any role filter value." }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "albums": { "type": "array",
+ "items": { "$ref": "Audio.Details.Album" }
+ }
+ }
+ }
+ },
+ "AudioLibrary.GetAlbumDetails": {
+ "type": "method",
+ "description": "Retrieve details about a specific album",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "albumid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "Audio.Fields.Album" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "albumdetails": { "$ref": "Audio.Details.Album" }
+ }
+ }
+ },
+ "AudioLibrary.GetSongs": {
+ "type": "method",
+ "description": "Retrieve all songs from specified album, artist or genre",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Audio.Fields.Song" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" },
+ { "name": "filter",
+ "type": [
+ { "type": "object", "properties": { "genreid": { "$ref": "Library.Id", "required": true, "description": "Song genre. Filter for existence of songs with this genre" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "genre": { "type": "string", "minLength": 1, "required": true, "description": "Song genre. Filter for existence of songs with this genre" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "artistid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "artistid": { "$ref": "Library.Id", "required": true },
+ "roleid": { "$ref": "Library.Id", "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "artistid": { "$ref": "Library.Id", "required": true },
+ "role": { "type": "string", "minLength": 1, "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "artist": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "artist": { "type": "string", "minLength": 1, "required": true },
+ "roleid": { "$ref": "Library.Id", "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "artist": { "type": "string", "minLength": 1, "required": true },
+ "role": { "type": "string", "minLength": 1, "required": true }}, "additionalProperties": false },
+ { "type": "object", "properties": { "albumid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "album": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "$ref": "List.Filter.Songs" }
+ ]
+ },
+ { "name": "includesingles", "type": "boolean", "default": true, "description": "Only songs from albums are returned when false, but overridden when singlesonly parameter is true" },
+ { "name": "allroles", "type": "boolean", "default":false, "description": "Whether or not to include all roles when filtering by artist, rather than default of excluding other contributors. When true it overrides any role filter value." },
+ { "name": "singlesonly", "type": "boolean", "default": false, "description": "Only singles are returned when true, and overrides includesingles parameter" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "songs": { "type": "array",
+ "items": { "$ref": "Audio.Details.Song" }
+ }
+ }
+ }
+ },
+ "AudioLibrary.GetSongDetails": {
+ "type": "method",
+ "description": "Retrieve details about a specific song",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "songid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "Audio.Fields.Song" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "songdetails": { "$ref": "Audio.Details.Song" }
+ }
+ }
+ },
+ "AudioLibrary.GetRecentlyAddedAlbums": {
+ "type": "method",
+ "description": "Retrieve recently added albums",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Audio.Fields.Album" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "albums": { "type": "array",
+ "items": { "$ref": "Audio.Details.Album" }
+ }
+ }
+ }
+ },
+ "AudioLibrary.GetRecentlyAddedSongs": {
+ "type": "method",
+ "description": "Retrieve recently added songs",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "albumlimit", "$ref": "List.Amount", "description": "The amount of recently added albums from which to return the songs" },
+ { "name": "properties", "$ref": "Audio.Fields.Song" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "songs": { "type": "array",
+ "items": { "$ref": "Audio.Details.Song" }
+ }
+ }
+ }
+ },
+ "AudioLibrary.GetRecentlyPlayedAlbums": {
+ "type": "method",
+ "description": "Retrieve recently played albums",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Audio.Fields.Album" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "albums": { "type": "array",
+ "items": { "$ref": "Audio.Details.Album" }
+ }
+ }
+ }
+ },
+ "AudioLibrary.GetRecentlyPlayedSongs": {
+ "type": "method",
+ "description": "Retrieve recently played songs",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Audio.Fields.Song" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "songs": { "type": "array",
+ "items": { "$ref": "Audio.Details.Song" }
+ }
+ }
+ }
+ },
+ "AudioLibrary.GetGenres": {
+ "type": "method",
+ "description": "Retrieve all genres",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Library.Fields.Genre" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "genres": { "type": "array", "required": true,
+ "items": { "$ref": "Library.Details.Genre" }
+ }
+ }
+ }
+ },
+ "AudioLibrary.GetSources": {
+ "type": "method",
+ "description": "Get all music sources, including unique ID",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Library.Fields.Source" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "sources": { "type": "array", "required": true,
+ "items": { "$ref": "Library.Details.Source" }
+ }
+ }
+ }
+ },
+ "AudioLibrary.GetRoles": {
+ "type": "method",
+ "description": "Retrieve all contributor roles",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Audio.Fields.Role" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "roles": { "type": "array", "required": true,
+ "items": { "$ref": "Audio.Details.Role" }
+ }
+ }
+ }
+ },
+ "AudioLibrary.GetAvailableArtTypes": {
+ "type": "method",
+ "description": "Retrieve a list of potential art types for a media item",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ {
+ "name": "item", "required": true,
+ "type": [
+ { "type": "object", "properties": { "albumid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "artistid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false }
+ ]
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "availablearttypes": { "type": "array", "required": true,
+ "items": { "type": "string" }
+ }
+ }
+ }
+ },
+ "AudioLibrary.GetAvailableArt": {
+ "type": "method",
+ "description": "Retrieve all potential art URLs for a media item by art type",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ {
+ "name": "item", "required": true,
+ "type": [
+ { "type": "object", "properties": { "albumid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "artistid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false }
+ ]
+ },
+ { "name": "arttype", "type": "string" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "availableart": { "type": "array", "required": true,
+ "items": {
+ "type": "object",
+ "properties": {
+ "url": { "type": "string", "description": "URL to the original image", "required": true },
+ "arttype": { "type": "string", "required": true },
+ "previewurl": { "type": "string", "description": "URL to a preview thumbnail of the image" }
+ }
+ }
+ }
+ }
+ }
+ },
+ "AudioLibrary.SetArtistDetails": {
+ "type": "method",
+ "description": "Update the given artist with the given details",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "artistid", "$ref": "Library.Id", "required": true },
+ { "name": "artist", "$ref": "Optional.String" },
+ { "name": "instrument", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "style", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "mood", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "born", "$ref": "Optional.String" },
+ { "name": "formed", "$ref": "Optional.String" },
+ { "name": "description", "$ref": "Optional.String" },
+ { "name": "genre", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "died", "$ref": "Optional.String" },
+ { "name": "disbanded", "$ref": "Optional.String" },
+ { "name": "yearsactive", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "musicbrainzartistid", "$ref": "Optional.String" },
+ { "name": "sortname", "$ref": "Optional.String" },
+ { "name": "type", "$ref": "Optional.String" },
+ { "name": "gender", "$ref": "Optional.String" },
+ { "name": "disambiguation", "$ref": "Optional.String" },
+ { "name": "art", "type": [ "null", { "$ref": "Media.Artwork.Set", "required": true } ], "default": null }
+ ],
+ "returns": "string"
+ },
+ "AudioLibrary.SetAlbumDetails": {
+ "type": "method",
+ "description": "Update the given album with the given details",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "albumid", "$ref": "Library.Id", "required": true },
+ { "name": "title", "$ref": "Optional.String" },
+ { "name": "artist", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "description", "$ref": "Optional.String" },
+ { "name": "genre", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "theme", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "mood", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "style", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "type", "$ref": "Optional.String" },
+ { "name": "albumlabel", "$ref": "Optional.String" },
+ { "name": "rating", "$ref": "Optional.Number" },
+ { "name": "year", "$ref": "Optional.Integer" },
+ { "name": "userrating", "$ref": "Optional.Integer" },
+ { "name": "votes", "$ref": "Optional.Integer" },
+ { "name": "musicbrainzalbumid", "$ref": "Optional.String" },
+ { "name": "musicbrainzreleasegroupid", "$ref": "Optional.String" },
+ { "name": "sortartist", "$ref": "Optional.String" },
+ { "name": "displayartist", "$ref": "Optional.String" },
+ { "name": "musicbrainzalbumartistid", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "art", "type": [ "null", { "$ref": "Media.Artwork.Set", "required": true } ], "default": null },
+ { "name": "isboxset", "$ref": "Optional.Boolean" },
+ { "name": "releasedate", "$ref": "Optional.String" },
+ { "name": "originaldate", "$ref": "Optional.String" }
+ ],
+ "returns": "string"
+ },
+ "AudioLibrary.SetSongDetails": {
+ "type": "method",
+ "description": "Update the given song with the given details",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "songid", "$ref": "Library.Id", "required": true },
+ { "name": "title", "$ref": "Optional.String" },
+ { "name": "artist", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "genre", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "year", "$ref": "Optional.Integer" },
+ { "name": "rating", "$ref": "Optional.Number" },
+ { "name": "track", "$ref": "Optional.Integer" },
+ { "name": "disc", "$ref": "Optional.Integer" },
+ { "name": "duration", "$ref": "Optional.Integer" },
+ { "name": "comment", "$ref": "Optional.String" },
+ { "name": "musicbrainztrackid", "$ref": "Optional.String" },
+ { "name": "musicbrainzartistid", "$ref": "Optional.String" },
+ { "name": "playcount", "$ref": "Optional.Integer" },
+ { "name": "lastplayed", "$ref": "Optional.String" },
+ { "name": "userrating", "$ref": "Optional.Integer" },
+ { "name": "votes", "$ref": "Optional.Integer" },
+ { "name": "displayartist", "$ref": "Optional.String" },
+ { "name": "sortartist", "$ref": "Optional.String" },
+ { "name": "mood", "$ref": "Optional.String" },
+ { "name": "art", "type": [ "null", { "$ref": "Media.Artwork.Set", "required": true } ], "default": null },
+ { "name": "disctitle", "$ref": "Optional.String" },
+ { "name": "releasedate", "$ref": "Optional.String" },
+ { "name": "originaldate", "$ref": "Optional.String" },
+ { "name": "bpm", "$ref": "Optional.Integer" }
+ ],
+ "returns": "string"
+ },
+ "AudioLibrary.Scan": {
+ "type": "method",
+ "description": "Scans the audio sources for new library items",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "directory", "type": "string", "default": "" },
+ { "name": "showdialogs", "type": "boolean", "default": true, "description": "Whether or not to show the progress bar or any other GUI dialog" }
+ ],
+ "returns": "string"
+ },
+ "AudioLibrary.Export": {
+ "type": "method",
+ "description": "Exports all items from the audio library",
+ "transport": "Response",
+ "permission": "WriteFile",
+ "params": [
+ { "name": "options", "type": [
+ { "type": "object", "required": true, "additionalProperties": false,
+ "properties": {
+ "path": { "type": "string", "required": true, "minLength": 1, "description": "Path to the directory to where the data should be exported" }
+ }
+ },
+ { "type": "object", "required": true, "additionalProperties": false,
+ "properties": {
+ "overwrite": { "type": "boolean", "default": false, "description": "Whether to overwrite existing exported files" },
+ "images": { "type": "boolean", "default": false, "description": "Whether to export thumbnails and fanart images" }
+ }
+ }
+ ]
+ }
+ ],
+ "returns": "string"
+ },
+ "AudioLibrary.Clean": {
+ "type": "method",
+ "description": "Cleans the audio library from non-existent items",
+ "transport": "Response",
+ "permission": "RemoveData",
+ "params": [
+ { "name": "showdialogs", "type": "boolean", "default": true, "description": "Whether or not to show the progress bar or any other GUI dialog" }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.GetMovies": {
+ "type": "method",
+ "description": "Retrieve all movies",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Video.Fields.Movie" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" },
+ { "name": "filter",
+ "type": [
+ { "type": "object", "properties": { "genreid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "genre": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "year": { "type": "integer", "minimum": 0, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "actor": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "director": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "studio": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "country": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "setid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "set": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "$ref": "List.Filter.Movies" }
+ ]
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "movies": { "type": "array",
+ "items": { "$ref": "Video.Details.Movie" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetMovieDetails": {
+ "type": "method",
+ "description": "Retrieve details about a specific movie",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "movieid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "Video.Fields.Movie" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "moviedetails": { "$ref": "Video.Details.Movie" }
+ }
+ }
+ },
+ "VideoLibrary.GetMovieSets": {
+ "type": "method",
+ "description": "Retrieve all movie sets",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Video.Fields.MovieSet" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "sets": { "type": "array",
+ "items": { "$ref": "Video.Details.MovieSet" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetMovieSetDetails": {
+ "type": "method",
+ "description": "Retrieve details about a specific movie set",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "setid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "Video.Fields.MovieSet" },
+ { "name": "movies", "type": "object",
+ "properties": {
+ "properties": { "$ref": "Video.Fields.Movie" },
+ "limits": { "$ref": "List.Limits" },
+ "sort": { "$ref": "List.Sort" }
+ }
+ }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "setdetails": { "$ref": "Video.Details.MovieSet.Extended" }
+ }
+ }
+ },
+ "VideoLibrary.GetTVShows": {
+ "type": "method",
+ "description": "Retrieve all tv shows",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Video.Fields.TVShow" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" },
+ { "name": "filter",
+ "type": [
+ { "type": "object", "properties": { "genreid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "genre": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "year": { "type": "integer", "minimum": 0, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "actor": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "studio": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "$ref": "List.Filter.TVShows" }
+ ]
+ }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "tvshows": { "type": "array",
+ "items": { "$ref": "Video.Details.TVShow" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetTVShowDetails": {
+ "type": "method",
+ "description": "Retrieve details about a specific tv show",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "tvshowid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "Video.Fields.TVShow" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "tvshowdetails": { "$ref": "Video.Details.TVShow" }
+ }
+ }
+ },
+ "VideoLibrary.GetSeasons": {
+ "type": "method",
+ "description": "Retrieve all tv seasons",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "tvshowid", "$ref": "Library.Id" },
+ { "name": "properties", "$ref": "Video.Fields.Season" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "seasons": { "type": "array",
+ "items": { "$ref": "Video.Details.Season" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetSeasonDetails": {
+ "type": "method",
+ "description": "Retrieve details about a specific tv show season",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "seasonid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "Video.Fields.Season" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "seasondetails": { "$ref": "Video.Details.Season" }
+ }
+ }
+ },
+ "VideoLibrary.GetEpisodes": {
+ "type": "method",
+ "description": "Retrieve all tv show episodes",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "tvshowid", "$ref": "Library.Id" },
+ { "name": "season", "type": "integer", "minimum": 0, "default": -1 },
+ { "name": "properties", "$ref": "Video.Fields.Episode" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" },
+ { "name": "filter",
+ "type": [
+ { "type": "object", "properties": { "genreid": { "$ref": "Library.Id", "required": true, "description": "Requires tvshowid to be set" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "genre": { "type": "string", "minLength": 1, "required": true, "description": "Requires tvshowid to be set" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "year": { "type": "integer", "minimum": 0, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "actor": { "type": "string", "minLength": 1, "required": true, "description": "Requires tvshowid to be set" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "director": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "$ref": "List.Filter.Episodes" }
+ ]
+ }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "episodes": { "type": "array",
+ "items": { "$ref": "Video.Details.Episode" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetEpisodeDetails": {
+ "type": "method",
+ "description": "Retrieve details about a specific tv show episode",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "episodeid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "Video.Fields.Episode" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "episodedetails": { "$ref": "Video.Details.Episode" }
+ }
+ }
+ },
+ "VideoLibrary.GetMusicVideos": {
+ "type": "method",
+ "description": "Retrieve all music videos",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Video.Fields.MusicVideo" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" },
+ { "name": "filter",
+ "type": [
+ { "type": "object", "properties": { "artist": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "genreid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "genre": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "year": { "type": "integer", "minimum": 0, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "director": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "studio": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "tag": { "type": "string", "minLength": 1, "required": true } }, "additionalProperties": false },
+ { "$ref": "List.Filter.MusicVideos" }
+ ]
+ }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "musicvideos": { "type": "array",
+ "items": { "$ref": "Video.Details.MusicVideo" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetMusicVideoDetails": {
+ "type": "method",
+ "description": "Retrieve details about a specific music video",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "musicvideoid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "Video.Fields.MusicVideo" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "musicvideodetails": { "$ref": "Video.Details.MusicVideo" }
+ }
+ }
+ },
+ "VideoLibrary.GetRecentlyAddedMovies": {
+ "type": "method",
+ "description": "Retrieve all recently added movies",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Video.Fields.Movie" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "movies": { "type": "array",
+ "items": { "$ref": "Video.Details.Movie" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetRecentlyAddedEpisodes": {
+ "type": "method",
+ "description": "Retrieve all recently added tv episodes",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Video.Fields.Episode" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "episodes": { "type": "array",
+ "items": { "$ref": "Video.Details.Episode" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetRecentlyAddedMusicVideos": {
+ "type": "method",
+ "description": "Retrieve all recently added music videos",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Video.Fields.MusicVideo" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "musicvideos": { "type": "array",
+ "items": { "$ref": "Video.Details.MusicVideo" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetInProgressTVShows": {
+ "type": "method",
+ "description": "Retrieve all in progress tvshows",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Video.Fields.TVShow" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "tvshows": { "type": "array",
+ "items": { "$ref": "Video.Details.TVShow" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetGenres": {
+ "type": "method",
+ "description": "Retrieve all genres",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "type", "type": "string", "required": true, "enum": [ "movie", "tvshow", "musicvideo"] },
+ { "name": "properties", "$ref": "Library.Fields.Genre" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "genres": { "type": "array", "required": true,
+ "items": { "$ref": "Library.Details.Genre" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetTags": {
+ "type": "method",
+ "description": "Retrieve all tags",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "type", "type": "string", "required": true, "enum": [ "movie", "tvshow", "musicvideo" ] },
+ { "name": "properties", "$ref": "Library.Fields.Tag" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "tags": { "type": "array", "required": true,
+ "items": { "$ref": "Library.Details.Tag" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetAvailableArtTypes": {
+ "type": "method",
+ "description": "Retrieve a list of potential art types for a media item",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ {
+ "name": "item", "required": true,
+ "type": [
+ { "type": "object", "properties": { "episodeid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "tvshowid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "seasonid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "movieid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "setid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "musicvideoid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false }
+ ]
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "availablearttypes": { "type": "array", "required": true,
+ "items": { "type": "string" }
+ }
+ }
+ }
+ },
+ "VideoLibrary.GetAvailableArt": {
+ "type": "method",
+ "description": "Retrieve all potential art URLs for a media item by art type",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ {
+ "name": "item", "required": true,
+ "type": [
+ { "type": "object", "properties": { "episodeid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "tvshowid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "seasonid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "movieid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "setid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "musicvideoid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false }
+ ]
+ },
+ { "name": "arttype", "type": "string" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "availableart": { "type": "array", "required": true,
+ "items": {
+ "type": "object",
+ "properties": {
+ "url": { "type": "string", "description": "URL to the original image", "required": true },
+ "arttype": { "type": "string", "required": true },
+ "previewurl": { "type": "string", "description": "URL to a preview thumbnail of the image" }
+ }
+ }
+ }
+ }
+ }
+ },
+ "VideoLibrary.SetMovieDetails": {
+ "type": "method",
+ "description": "Update the given movie with the given details",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "movieid", "$ref": "Library.Id", "required": true },
+ { "name": "title", "$ref": "Optional.String" },
+ { "name": "playcount", "$ref": "Optional.Integer" },
+ { "name": "runtime", "$ref": "Optional.Integer", "description": "Runtime in seconds" },
+ { "name": "director", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "studio", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "year", "$ref": "Optional.Integer", "description": "linked with premiered. Overridden by premiered parameter" },
+ { "name": "plot", "$ref": "Optional.String" },
+ { "name": "genre", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "rating", "$ref": "Optional.Number" },
+ { "name": "mpaa", "$ref": "Optional.String" },
+ { "name": "imdbnumber", "$ref": "Optional.String" },
+ { "name": "votes", "$ref": "Optional.String" },
+ { "name": "lastplayed", "$ref": "Optional.String" },
+ { "name": "originaltitle", "$ref": "Optional.String" },
+ { "name": "trailer", "$ref": "Optional.String" },
+ { "name": "tagline", "$ref": "Optional.String" },
+ { "name": "plotoutline", "$ref": "Optional.String" },
+ { "name": "writer", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "country", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "top250", "$ref": "Optional.Integer" },
+ { "name": "sorttitle", "$ref": "Optional.String" },
+ { "name": "set", "$ref": "Optional.String" },
+ { "name": "showlink", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "thumbnail", "$ref": "Optional.String" },
+ { "name": "fanart", "$ref": "Optional.String" },
+ { "name": "tag", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "art", "type": [ "null", { "$ref": "Media.Artwork.Set", "required": true } ], "default": null },
+ { "name": "resume", "type": [ "null", { "$ref": "Video.Resume", "required": true } ], "default": null },
+ { "name": "userrating", "$ref": "Optional.Integer" },
+ { "name": "ratings", "$ref": "Video.Ratings.Set" },
+ { "name": "dateadded", "$ref": "Optional.String" },
+ { "name": "premiered", "$ref": "Optional.String", "description": "linked with year. Overrides year" },
+ { "name": "uniqueid", "type": [ "null", { "$ref": "Media.UniqueID.Set", "required": true } ], "default": null }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.SetMovieSetDetails": {
+ "type": "method",
+ "description": "Update the given movie set with the given details",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "setid", "$ref": "Library.Id", "required": true },
+ { "name": "title", "$ref": "Optional.String" },
+ { "name": "art", "type": [ "null", { "$ref": "Media.Artwork.Set", "required": true } ], "default": null },
+ { "name": "plot", "$ref": "Optional.String" }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.SetTVShowDetails": {
+ "type": "method",
+ "description": "Update the given tvshow with the given details",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "tvshowid", "$ref": "Library.Id", "required": true },
+ { "name": "title", "$ref": "Optional.String" },
+ { "name": "playcount", "$ref": "Optional.Integer" },
+ { "name": "studio", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "plot", "$ref": "Optional.String" },
+ { "name": "genre", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "rating", "$ref": "Optional.Number" },
+ { "name": "mpaa", "$ref": "Optional.String" },
+ { "name": "imdbnumber", "$ref": "Optional.String" },
+ { "name": "premiered", "$ref": "Optional.String" },
+ { "name": "votes", "$ref": "Optional.String" },
+ { "name": "lastplayed", "$ref": "Optional.String" },
+ { "name": "originaltitle", "$ref": "Optional.String" },
+ { "name": "sorttitle", "$ref": "Optional.String" },
+ { "name": "episodeguide", "$ref": "Optional.String" },
+ { "name": "thumbnail", "$ref": "Optional.String" },
+ { "name": "fanart", "$ref": "Optional.String" },
+ { "name": "tag", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "art", "type": [ "null", { "$ref": "Media.Artwork.Set", "required": true } ], "default": null },
+ { "name": "userrating", "$ref": "Optional.Integer" },
+ { "name": "ratings", "$ref": "Video.Ratings.Set" },
+ { "name": "dateadded", "$ref": "Optional.String" },
+ { "name": "runtime", "$ref": "Optional.Integer", "description": "Runtime in seconds" },
+ { "name": "status", "$ref": "Optional.String", "description": "Valid values: 'returning series', 'in production', 'planned', 'cancelled', 'ended'" },
+ { "name": "uniqueid", "type": [ "null", { "$ref": "Media.UniqueID.Set", "required": true } ], "default": null }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.SetSeasonDetails": {
+ "type": "method",
+ "description": "Update the given season with the given details",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "seasonid", "$ref": "Library.Id", "required": true },
+ { "name": "art", "type": [ "null", { "$ref": "Media.Artwork.Set", "required": true } ], "default": null },
+ { "name": "userrating", "$ref": "Optional.Integer" },
+ { "name": "title", "$ref": "Optional.String" }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.SetEpisodeDetails": {
+ "type": "method",
+ "description": "Update the given episode with the given details",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "episodeid", "$ref": "Library.Id", "required": true },
+ { "name": "title", "$ref": "Optional.String" },
+ { "name": "playcount", "$ref": "Optional.Integer" },
+ { "name": "runtime", "$ref": "Optional.Integer", "description": "Runtime in seconds" },
+ { "name": "director", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "plot", "$ref": "Optional.String" },
+ { "name": "rating", "$ref": "Optional.Number" },
+ { "name": "votes", "$ref": "Optional.String" },
+ { "name": "lastplayed", "$ref": "Optional.String" },
+ { "name": "writer", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "firstaired", "$ref": "Optional.String" },
+ { "name": "productioncode", "$ref": "Optional.String" },
+ { "name": "season", "$ref": "Optional.Integer" },
+ { "name": "episode", "$ref": "Optional.Integer" },
+ { "name": "originaltitle", "$ref": "Optional.String" },
+ { "name": "thumbnail", "$ref": "Optional.String" },
+ { "name": "fanart", "$ref": "Optional.String" },
+ { "name": "art", "type": [ "null", { "$ref": "Media.Artwork.Set", "required": true } ], "default": null },
+ { "name": "resume", "type": [ "null", { "$ref": "Video.Resume", "required": true } ], "default": null },
+ { "name": "userrating", "$ref": "Optional.Integer" },
+ { "name": "ratings", "$ref": "Video.Ratings.Set" },
+ { "name": "dateadded", "$ref": "Optional.String" },
+ { "name": "uniqueid", "type": [ "null", { "$ref": "Media.UniqueID.Set", "required": true } ], "default": null }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.SetMusicVideoDetails": {
+ "type": "method",
+ "description": "Update the given music video with the given details",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "musicvideoid", "$ref": "Library.Id", "required": true },
+ { "name": "title", "$ref": "Optional.String" },
+ { "name": "playcount", "$ref": "Optional.Integer" },
+ { "name": "runtime", "$ref": "Optional.Integer", "description": "Runtime in seconds" },
+ { "name": "director", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "studio", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "year", "$ref": "Optional.Integer", "description": "linked with premiered. Overridden by premiered parameter" },
+ { "name": "plot", "$ref": "Optional.String" },
+ { "name": "album", "$ref": "Optional.String" },
+ { "name": "artist", "type": [ "null", { "$ref": "Array.String", "required": true } ] },
+ { "name": "genre", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "track", "$ref": "Optional.Integer" },
+ { "name": "lastplayed", "$ref": "Optional.String" },
+ { "name": "thumbnail", "$ref": "Optional.String" },
+ { "name": "fanart", "$ref": "Optional.String" },
+ { "name": "tag", "type": [ "null", { "$ref": "Array.String", "required": true } ], "default": null },
+ { "name": "art", "type": [ "null", { "$ref": "Media.Artwork.Set", "required": true } ], "default": null },
+ { "name": "resume", "type": [ "null", { "$ref": "Video.Resume", "required": true } ], "default": null },
+ { "name": "rating", "$ref": "Optional.Number" },
+ { "name": "userrating", "$ref": "Optional.Integer" },
+ { "name": "dateadded", "$ref": "Optional.String" },
+ { "name": "premiered", "$ref": "Optional.String", "description": "linked with year. Overrides year" },
+ { "name": "uniqueid", "type": [ "null", { "$ref": "Media.UniqueID.Set", "required": true } ], "default": null }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.RefreshMovie": {
+ "type": "method",
+ "description": "Refresh the given movie in the library",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "movieid", "$ref": "Library.Id", "required": true },
+ { "name": "ignorenfo", "type": "boolean", "required": false, "default": false, "description": "Whether or not to ignore a local NFO if present." },
+ { "name": "title", "type": "string", "required": "false", "default": "", "description": "Title to use for searching (instead of determining it from the item's filename/path)." }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.RefreshTVShow": {
+ "type": "method",
+ "description": "Refresh the given tv show in the library",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "tvshowid", "$ref": "Library.Id", "required": true },
+ { "name": "ignorenfo", "type": "boolean", "required": false, "default": false, "description": "Whether or not to ignore a local NFO if present." },
+ { "name": "refreshepisodes", "type": "boolean", "required": false, "default": false, "description": "Whether or not to refresh all episodes belonging to the TV show." },
+ { "name": "title", "type": "string", "required": "false", "default": "", "description": "Title to use for searching (instead of determining it from the item's filename/path)." }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.RefreshEpisode": {
+ "type": "method",
+ "description": "Refresh the given episode in the library",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "episodeid", "$ref": "Library.Id", "required": true },
+ { "name": "ignorenfo", "type": "boolean", "required": false, "default": false, "description": "Whether or not to ignore a local NFO if present." },
+ { "name": "title", "type": "string", "required": "false", "default": "", "description": "Title to use for searching (instead of determining it from the item's filename/path)." }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.RefreshMusicVideo": {
+ "type": "method",
+ "description": "Refresh the given music video in the library",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "musicvideoid", "$ref": "Library.Id", "required": true },
+ { "name": "ignorenfo", "type": "boolean", "required": false, "default": false, "description": "Whether or not to ignore a local NFO if present." },
+ { "name": "title", "type": "string", "required": "false", "default": "", "description": "Title to use for searching (instead of determining it from the item's filename/path)." }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.RemoveMovie": {
+ "type": "method",
+ "description": "Removes the given movie from the library",
+ "transport": "Response",
+ "permission": "RemoveData",
+ "params": [
+ { "name": "movieid", "$ref": "Library.Id", "required": true }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.RemoveTVShow": {
+ "type": "method",
+ "description": "Removes the given tv show from the library",
+ "transport": "Response",
+ "permission": "RemoveData",
+ "params": [
+ { "name": "tvshowid", "$ref": "Library.Id", "required": true }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.RemoveEpisode": {
+ "type": "method",
+ "description": "Removes the given episode from the library",
+ "transport": "Response",
+ "permission": "RemoveData",
+ "params": [
+ { "name": "episodeid", "$ref": "Library.Id", "required": true }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.RemoveMusicVideo": {
+ "type": "method",
+ "description": "Removes the given music video from the library",
+ "transport": "Response",
+ "permission": "RemoveData",
+ "params": [
+ { "name": "musicvideoid", "$ref": "Library.Id", "required": true }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.Scan": {
+ "type": "method",
+ "description": "Scans the video sources for new library items",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "directory", "type": "string", "default": "" },
+ { "name": "showdialogs", "type": "boolean", "default": true, "description": "Whether or not to show the progress bar or any other GUI dialog" }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.Export": {
+ "type": "method",
+ "description": "Exports all items from the video library",
+ "transport": "Response",
+ "permission": "WriteFile",
+ "params": [
+ { "name": "options", "type": [
+ { "type": "object", "required": true, "additionalProperties": false,
+ "properties": {
+ "path": { "type": "string", "required": true, "minLength": 1, "description": "Path to the directory to where the data should be exported" }
+ }
+ },
+ { "type": "object", "required": true, "additionalProperties": false,
+ "properties": {
+ "overwrite": { "type": "boolean", "default": false, "description": "Whether to overwrite existing exported files" },
+ "images": { "type": "boolean", "default": false, "description": "Whether to export thumbnails and fanart images" },
+ "actorthumbs": { "type": "boolean", "default": false, "description": "Whether to export actor thumbnails" }
+ }
+ }
+ ]
+ }
+ ],
+ "returns": "string"
+ },
+ "VideoLibrary.Clean": {
+ "type": "method",
+ "description": "Cleans the video library for non-existent items",
+ "transport": "Response",
+ "permission": "RemoveData",
+ "params": [
+ { "name": "showdialogs", "type": "boolean", "default": true, "description": "Whether or not to show the progress bar or any other GUI dialog" },
+ { "name": "content", "type": "string", "default": "video", "enum": [ "video", "movies", "tvshows", "musicvideos" ], "description": "Content type to clean for" },
+ { "name": "directory", "type": "string", "default": "", "description": "Path to the directory to clean up; performs a global cleanup if not specified" }
+ ],
+ "returns": "string"
+ },
+ "GUI.ActivateWindow": {
+ "type": "method",
+ "description": "Activates the given window",
+ "transport": "Response",
+ "permission": "ControlGUI",
+ "params": [
+ { "name": "window", "$ref": "GUI.Window", "required": true },
+ { "name": "parameters", "type": "array", "items": { "type": "string", "minLength": 1, "required": true }, "minItems": 1 }
+ ],
+ "returns": "string"
+ },
+ "GUI.ShowNotification": {
+ "type": "method",
+ "description": "Shows a GUI notification",
+ "transport": "Response",
+ "permission": "ControlGUI",
+ "params": [
+ { "name": "title", "type": "string", "required": true },
+ { "name": "message", "type": "string", "required": true },
+ { "name": "image", "type": [
+ { "type": "string", "required": true, "enum": [ "info", "warning", "error" ] },
+ { "type": "string", "required": true }
+ ], "default": ""
+ },
+ { "name": "displaytime", "type": "integer", "minimum": 1500, "default": 5000, "description": "The time in milliseconds the notification will be visible" }
+ ],
+ "returns": "string"
+ },
+ "GUI.GetProperties": {
+ "type": "method",
+ "description": "Retrieves the values of the given properties",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "type": "array", "uniqueItems": true, "required": true, "items": { "$ref": "GUI.Property.Name" } }
+ ],
+ "returns": { "$ref": "GUI.Property.Value", "required": true }
+ },
+ "GUI.SetFullscreen": {
+ "type": "method",
+ "description": "Toggle fullscreen/GUI",
+ "transport": "Response",
+ "permission": "ControlGUI",
+ "params": [
+ { "name": "fullscreen", "required": true, "$ref": "Global.Toggle" }
+ ],
+ "returns": { "type": "boolean", "description": "Fullscreen state" }
+ },
+ "GUI.SetStereoscopicMode": {
+ "type": "method",
+ "description": "Sets the stereoscopic mode of the GUI to the given mode",
+ "transport": "Response",
+ "permission": "ControlGUI",
+ "params": [
+ { "name": "mode", "type": "string", "enum": [ "toggle", "tomono", "next", "previous", "select", "off", "split_vertical", "split_horizontal", "row_interleaved", "hardware_based", "anaglyph_cyan_red", "anaglyph_green_magenta", "monoscopic" ], "required": true }
+ ],
+ "returns": "string"
+ },
+ "GUI.GetStereoscopicModes": {
+ "type": "method",
+ "description": "Returns the supported stereoscopic modes of the GUI",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "stereoscopicmodes" : {
+ "type": "array",
+ "uniqueItems": true,
+ "items": { "$ref": "GUI.Stereoscopy.Mode" }
+ }
+ }
+ }
+ },
+ "Addons.GetAddons": {
+ "type": "method",
+ "description": "Gets all available addons",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "type", "$ref": "Addon.Types" },
+ { "name": "content", "$ref": "Addon.Content", "description": "Content provided by the addon. Only considered for plugins and scripts." },
+ { "name": "enabled", "type": [ { "type": "boolean" }, { "type": "string", "enum": [ "all" ] } ], "default": "all" },
+ { "name": "properties", "$ref": "Addon.Fields" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "installed", "type": [ { "type": "boolean" }, { "type": "string", "enum": [ "all" ] } ], "default": true }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "addons": { "type": "array",
+ "items": { "$ref": "Addon.Details" }
+ }
+ }
+ }
+ },
+ "Addons.GetAddonDetails": {
+ "type": "method",
+ "description": "Gets the details of a specific addon",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "addonid", "type": "string", "required": true },
+ { "name": "properties", "$ref": "Addon.Fields" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "addon": { "$ref": "Addon.Details", "required": true }
+ }
+ }
+ },
+ "Addons.SetAddonEnabled": {
+ "type": "method",
+ "description": "Enables/Disables a specific addon",
+ "transport": "Response",
+ "permission": "ManageAddon",
+ "params": [
+ { "name": "addonid", "type": "string", "required": true },
+ { "name": "enabled", "$ref": "Global.Toggle", "required": true }
+ ],
+ "returns": "string"
+ },
+ "Addons.ExecuteAddon": {
+ "type": "method",
+ "description": "Executes the given addon with the given parameters (if possible)",
+ "transport": "Response",
+ "permission": "ExecuteAddon",
+ "params": [
+ { "name": "addonid", "type": "string", "required": true },
+ { "name": "params", "type": [
+ { "type": "object", "additionalProperties": { "type": "string" } },
+ { "type": "array", "items": { "type": "string" } },
+ { "type": "string", "description": "URL path (must start with / or ?" }
+ ],
+ "default": ""
+ },
+ { "name": "wait", "type": "boolean", "default": false }
+ ],
+ "returns": "string"
+ },
+ "PVR.GetProperties": {
+ "type": "method",
+ "description": "Retrieves the values of the given properties",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "type": "array", "uniqueItems": true, "required": true, "items": { "$ref": "PVR.Property.Name" } }
+ ],
+ "returns": { "$ref": "PVR.Property.Value", "required": true }
+ },
+ "PVR.GetChannelGroups": {
+ "type": "method",
+ "description": "Retrieves the channel groups for the specified type",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "channeltype", "$ref": "PVR.Channel.Type", "required": true },
+ { "name": "limits", "$ref": "List.Limits" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "channelgroups": { "type": "array", "required": true,
+ "items": { "$ref": "PVR.Details.ChannelGroup" }
+ }
+ }
+ }
+ },
+ "PVR.GetChannelGroupDetails": {
+ "type": "method",
+ "description": "Retrieves the details of a specific channel group",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "channelgroupid", "$ref": "PVR.ChannelGroup.Id", "required": true },
+ { "name": "channels", "type": "object",
+ "properties": {
+ "properties": { "$ref": "PVR.Fields.Channel" },
+ "limits": { "$ref": "List.Limits" }
+ }
+ }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "channelgroupdetails": { "$ref": "PVR.Details.ChannelGroup.Extended" }
+ }
+ }
+ },
+ "PVR.GetChannels": {
+ "type": "method",
+ "description": "Retrieves the channel list",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "channelgroupid", "$ref": "PVR.ChannelGroup.Id", "required": true },
+ { "name": "properties", "$ref": "PVR.Fields.Channel" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "channels": { "type": "array", "required": true,
+ "items": { "$ref": "PVR.Details.Channel" }
+ }
+ }
+ }
+ },
+ "PVR.GetChannelDetails": {
+ "type": "method",
+ "description": "Retrieves the details of a specific channel",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "channelid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "PVR.Fields.Channel" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "channeldetails": { "$ref": "PVR.Details.Channel" }
+ }
+ }
+ },
+ "PVR.GetClients": {
+ "type": "method",
+ "description": "Retrieves the enabled PVR clients and their capabilities",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "limits", "$ref": "List.Limits" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "clients": { "type": "array", "required": true,
+ "items": { "$ref": "PVR.Details.Client" }
+ }
+ }
+ }
+ },
+ "PVR.GetBroadcasts": {
+ "type": "method",
+ "description": "Retrieves the program of a specific channel",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "channelid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "PVR.Fields.Broadcast" },
+ { "name": "limits", "$ref": "List.Limits" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "broadcasts": { "type": "array", "required": true,
+ "items": { "$ref": "PVR.Details.Broadcast" }
+ }
+ }
+ }
+ },
+ "PVR.GetBroadcastDetails": {
+ "type": "method",
+ "description": "Retrieves the details of a specific broadcast",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "broadcastid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "PVR.Fields.Broadcast" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "broadcastdetails": { "$ref": "PVR.Details.Broadcast" }
+ }
+ }
+ },
+ "PVR.GetBroadcastIsPlayable": {
+ "type": "method",
+ "description": "Retrieves whether or not a broadcast is playable",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "broadcastid", "$ref": "Library.Id", "required": true, "description": "the id of the broadcast to check for playability" }
+ ],
+ "returns": "boolean"
+ },
+ "PVR.GetTimers": {
+ "type": "method",
+ "description": "Retrieves the timers",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "PVR.Fields.Timer" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "timers": { "type": "array", "required": true,
+ "items": { "$ref": "PVR.Details.Timer" }
+ }
+ }
+ }
+ },
+ "PVR.GetTimerDetails": {
+ "type": "method",
+ "description": "Retrieves the details of a specific timer",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "timerid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "PVR.Fields.Timer" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "timerdetails": { "$ref": "PVR.Details.Timer" }
+ }
+ }
+ },
+ "PVR.AddTimer": {
+ "type": "method",
+ "description": "Adds a timer to record the given show one times or a timer rule to record all showings of the given show or adds a reminder timer or reminder timer rule",
+ "transport": "Response",
+ "permission": "ControlPVR",
+ "params": [
+ { "name": "broadcastid", "$ref": "Library.Id", "required": true, "description": "the broadcast id of the item to record" },
+ { "name": "timerrule", "type": "boolean", "default": false, "description": "controls whether to create a timer rule or a onetime timer" },
+ { "name": "reminder", "type": "boolean", "default": false, "description": "controls whether to create a reminder timer or a recording timer" }
+ ],
+ "returns": "string"
+ },
+ "PVR.DeleteTimer": {
+ "type": "method",
+ "description": "Deletes a onetime timer or a timer rule",
+ "transport": "Response",
+ "permission": "ControlPVR",
+ "params": [
+ { "name": "timerid", "$ref": "Library.Id", "required": true, "description": "the id of the onetime timer or timer rule to delete" }
+ ],
+ "returns": "string"
+ },
+ "PVR.ToggleTimer": {
+ "type": "method",
+ "description": "Creates or deletes a onetime timer or timer rule for a given show. If it exists, it will be deleted. If it does not exist, it will be created",
+ "transport": "Response",
+ "permission": "ControlPVR",
+ "params": [
+ { "name": "broadcastid", "$ref": "Library.Id", "required": true, "description": "the broadcast id of the item to toggle a onetime timer or time rule for" },
+ { "name": "timerrule", "type": "boolean", "default": false, "description": "controls whether to create / delete a timer rule or a onetime timer" }
+ ],
+ "returns": "string"
+ },
+ "PVR.GetRecordings": {
+ "type": "method",
+ "description": "Retrieves the recordings",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "PVR.Fields.Recording" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "recordings": { "type": "array", "required": true,
+ "items": { "$ref": "PVR.Details.Recording" }
+ }
+ }
+ }
+ },
+ "PVR.GetRecordingDetails": {
+ "type": "method",
+ "description": "Retrieves the details of a specific recording",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "recordingid", "$ref": "Library.Id", "required": true },
+ { "name": "properties", "$ref": "PVR.Fields.Recording" }
+ ],
+ "returns": { "type": "object",
+ "properties": {
+ "recordingdetails": { "$ref": "PVR.Details.Recording" }
+ }
+ }
+ },
+ "PVR.Record": {
+ "type": "method",
+ "description": "Toggle recording of a channel",
+ "transport": "Response",
+ "permission": "ControlPVR",
+ "params": [
+ { "name": "record", "$ref": "Global.Toggle", "default": "toggle" },
+ { "name": "channel", "type": [
+ { "type": "string", "enum": [ "current" ], "required": true },
+ { "$ref": "Library.Id", "required": true }
+ ],
+ "default": "current"
+ }
+ ],
+ "returns": "string"
+ },
+ "PVR.Scan": {
+ "type": "method",
+ "description": "Starts a channel scan",
+ "transport": "Response",
+ "permission": "ControlPVR",
+ "params": [
+ { "name": "clientid", "$ref": "Library.Id", "description": "Specify a PVR client id to avoid UI dialog, optional in kodi 19, required in kodi 20" }
+ ],
+ "returns": "string"
+ },
+ "Textures.GetTextures": {
+ "type": "method",
+ "description": "Retrieve all textures",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Textures.Fields.Texture" },
+ { "name": "filter", "$ref": "List.Filter.Textures" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "textures": { "type": "array", "required": true,
+ "items": { "$ref": "Textures.Details.Texture" }
+ }
+ }
+ }
+ },
+ "Textures.RemoveTexture": {
+ "type": "method",
+ "description": "Remove the specified texture",
+ "transport": "Response",
+ "permission": "RemoveData",
+ "params": [
+ { "name": "textureid", "$ref": "Library.Id", "required": true, "description": "Texture database identifier" }
+ ],
+ "returns": "string"
+ },
+ "Profiles.GetProfiles": {
+ "type": "method",
+ "description": "Retrieve all profiles",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Profiles.Fields.Profile" },
+ { "name": "limits", "$ref": "List.Limits" },
+ { "name": "sort", "$ref": "List.Sort" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "profiles": { "type": "array", "required": true,
+ "items": { "$ref": "Profiles.Details.Profile" }
+ }
+ }
+ }
+ },
+ "Profiles.GetCurrentProfile": {
+ "type": "method",
+ "description": "Retrieve the current profile",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "$ref": "Profiles.Fields.Profile" }
+ ],
+ "returns": { "$ref": "Profiles.Details.Profile", "required": true }
+ },
+ "Profiles.LoadProfile": {
+ "type": "method",
+ "description": "Load the specified profile",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [
+ { "name": "profile", "type": "string", "required": true, "description": "Profile name" },
+ { "name": "prompt", "type": "boolean", "description": "Prompt for password" },
+ { "name": "password", "$ref": "Profiles.Password" }
+ ],
+ "returns": "string"
+ },
+ "System.GetProperties": {
+ "type": "method",
+ "description": "Retrieves the values of the given properties",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "type": "array", "uniqueItems": true, "required": true, "items": { "$ref": "System.Property.Name" } }
+ ],
+ "returns": { "$ref": "System.Property.Value", "required": true }
+ },
+ "System.EjectOpticalDrive": {
+ "type": "method",
+ "description": "Ejects or closes the optical disc drive (if available)",
+ "transport": "Response",
+ "permission": "ControlSystem",
+ "params": [ ],
+ "returns": "string"
+ },
+ "System.Shutdown": {
+ "type": "method",
+ "description": "Shuts the system running Kodi down",
+ "transport": "Response",
+ "permission": "ControlPower",
+ "params": [],
+ "returns": "string"
+ },
+ "System.Suspend": {
+ "type": "method",
+ "description": "Suspends the system running Kodi",
+ "transport": "Response",
+ "permission": "ControlPower",
+ "params": [],
+ "returns": "string"
+ },
+ "System.Hibernate": {
+ "type": "method",
+ "description": "Puts the system running Kodi into hibernate mode",
+ "transport": "Response",
+ "permission": "ControlPower",
+ "params": [],
+ "returns": "string"
+ },
+ "System.Reboot": {
+ "type": "method",
+ "description": "Reboots the system running Kodi",
+ "transport": "Response",
+ "permission": "ControlPower",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.SendText": {
+ "type": "method",
+ "description": "Send a generic (unicode) text",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [
+ { "name": "text", "type": "string", "required": true, "description": "Unicode text" },
+ { "name": "done", "type": "boolean", "default": true, "description": "Whether this is the whole input or not (closes an open input dialog if true)." }
+ ],
+ "returns": "string"
+ },
+ "Input.ExecuteAction": {
+ "type": "method",
+ "description": "Execute a specific action",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [
+ { "name": "action", "$ref": "Input.Action", "required": true }
+ ],
+ "returns": "string"
+ },
+ "Input.ButtonEvent": {
+ "type": "method",
+ "description": "Send a button press event",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [
+ { "name": "button", "type": "string", "required": true, "description": "Button name" },
+ { "name": "keymap", "type": "string", "required": true, "description": "Keymap name (KB, XG, R1, or R2)", "enum": [ "KB", "XG", "R1", "R2" ] },
+ { "name": "holdtime", "type": "integer", "required": false, "minimum" : 0, "default" : 0, "description": "Number of milliseconds to simulate button hold." }
+ ],
+ "returns": "string"
+ },
+ "Input.Left": {
+ "type": "method",
+ "description": "Navigate left in GUI",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.Right": {
+ "type": "method",
+ "description": "Navigate right in GUI",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.Down": {
+ "type": "method",
+ "description": "Navigate down in GUI",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.Up": {
+ "type": "method",
+ "description": "Navigate up in GUI",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.Select": {
+ "type": "method",
+ "description": "Select current item in GUI",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.Back": {
+ "type": "method",
+ "description": "Goes back in GUI",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.ContextMenu": {
+ "type": "method",
+ "description": "Shows the context menu",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.Info": {
+ "type": "method",
+ "description": "Shows the information dialog",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.Home": {
+ "type": "method",
+ "description": "Goes to home window in GUI",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.ShowCodec": {
+ "type": "method",
+ "description": "Show codec information of the playing item",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.ShowOSD": {
+ "type": "method",
+ "description": "Show the on-screen display for the current player",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Input.ShowPlayerProcessInfo": {
+ "type": "method",
+ "description": "Show player process information of the playing item, like video decoder, pixel format, pvr signal strength, ...",
+ "transport": "Response",
+ "permission": "Navigate",
+ "params": [],
+ "returns": "string"
+ },
+ "Application.GetProperties": {
+ "type": "method",
+ "description": "Retrieves the values of the given properties",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "properties", "type": "array", "uniqueItems": true, "required": true, "items": { "$ref": "Application.Property.Name" } }
+ ],
+ "returns": { "$ref": "Application.Property.Value", "required": true }
+ },
+ "Application.SetVolume": {
+ "type": "method",
+ "description": "Set the current volume",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "volume", "type": [
+ { "type": "integer", "minimum": 0, "maximum": 100, "required": true },
+ { "$ref": "Global.IncrementDecrement", "required": true }
+ ],
+ "required": true
+ }
+ ],
+ "returns": "integer"
+ },
+ "Application.SetMute": {
+ "type": "method",
+ "description": "Toggle mute/unmute",
+ "transport": "Response",
+ "permission": "ControlPlayback",
+ "params": [
+ { "name": "mute", "required": true, "$ref": "Global.Toggle" }
+ ],
+ "returns": { "type": "boolean", "description": "Mute state" }
+ },
+ "Application.Quit": {
+ "type": "method",
+ "description": "Quit application",
+ "transport": "Response",
+ "permission": "ControlPower",
+ "params": [],
+ "returns": "string"
+ },
+ "XBMC.GetInfoLabels": {
+ "type": "method",
+ "description": "Retrieve info labels about Kodi and the system",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "labels", "type": "array", "required": true, "items": { "type": "string" }, "minItems": 1, "description": "See http://kodi.wiki/view/InfoLabels for a list of possible info labels" }
+ ],
+ "returns": {
+ "type": "object",
+ "description": "Object containing key-value pairs of the retrieved info labels",
+ "additionalProperties": { "type": "string" }
+ }
+ },
+ "XBMC.GetInfoBooleans": {
+ "type": "method",
+ "description": "Retrieve info booleans about Kodi and the system",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "booleans", "type": "array", "required": true, "items": { "type": "string" }, "minItems": 1 }
+ ],
+ "returns": {
+ "type": "object",
+ "description": "Object containing key-value pairs of the retrieved info booleans",
+ "additionalProperties": { "type": "string" }
+ }
+ },
+ "Favourites.GetFavourites": {
+ "type": "method",
+ "description": "Retrieve all favourites",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "type", "type": [ "null", { "$ref": "Favourite.Type" } ], "default": null },
+ { "name": "properties", "$ref": "Favourite.Fields.Favourite" }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "favourites": { "type": "array",
+ "items": { "$ref": "Favourite.Details.Favourite" }
+ }
+ }
+ }
+ },
+ "Favourites.AddFavourite": {
+ "type": "method",
+ "description": "Add a favourite with the given details",
+ "transport": "Response",
+ "permission": "UpdateData",
+ "params": [
+ { "name": "title", "type": "string", "required": true },
+ { "name": "type", "$ref": "Favourite.Type", "required": true },
+ { "name": "path", "$ref": "Optional.String", "description": "Required for media, script and androidapp favourites types" },
+ { "name": "window", "$ref": "Optional.String", "description": "Required for window favourite type" },
+ { "name": "windowparameter", "$ref": "Optional.String" },
+ { "name": "thumbnail", "$ref": "Optional.String" }
+ ],
+ "returns": "string"
+ },
+ "Settings.GetSections": {
+ "type": "method",
+ "description": "Retrieves all setting sections",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "level", "$ref": "Setting.Level", "default": "standard" },
+ { "name": "properties", "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "categories" ]
+ }
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "sections": { "type": "array",
+ "items": { "$ref": "Setting.Details.Section" }
+ }
+ }
+ }
+ },
+ "Settings.GetCategories": {
+ "type": "method",
+ "description": "Retrieves all setting categories",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "level", "$ref": "Setting.Level", "default": "standard" },
+ { "name": "section", "type": "string", "default": "" },
+ { "name": "properties", "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "settings" ]
+ }
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "categories": { "type": "array",
+ "items": { "$ref": "Setting.Details.Category" }
+ }
+ }
+ }
+ },
+ "Settings.GetSettings": {
+ "type": "method",
+ "description": "Retrieves all settings",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "level", "$ref": "Setting.Level", "default": "standard" },
+ { "name": "filter", "type": [
+ { "type": "object",
+ "properties": {
+ "section": { "type": "string", "required": true, "minLength": 1 },
+ "category": { "type": "string", "required": true, "minLength": 1 }
+ },
+ "additionalProperties": false,
+ "required": true
+ }
+ ],
+ "default": null
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "settings": { "type": "array",
+ "items": { "$ref": "Setting.Details.Setting" }
+ }
+ }
+ }
+ },
+ "Settings.GetSettingValue": {
+ "type": "method",
+ "description": "Retrieves the value of a setting",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "setting", "type": "string", "required": true, "minLength": 1 }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "value": { "$ref": "Setting.Value.Extended", "required": true }
+ }
+ }
+ },
+ "Settings.SetSettingValue": {
+ "type": "method",
+ "description": "Changes the value of a setting",
+ "transport": "Response",
+ "permission": "WriteSetting",
+ "params": [
+ { "name": "setting", "type": "string", "required": true, "minLength": 1 },
+ { "name": "value", "$ref": "Setting.Value.Extended", "required": true }
+ ],
+ "returns": "boolean"
+ },
+ "Settings.ResetSettingValue": {
+ "type": "method",
+ "description": "Resets the value of a setting",
+ "transport": "Response",
+ "permission": "WriteSetting",
+ "params": [
+ { "name": "setting", "type": "string", "required": true, "minLength": 1 }
+ ],
+ "returns": "string"
+ },
+ "Settings.GetSkinSettingValue": {
+ "type": "method",
+ "description": "Retrieves the value of the specified skin setting",
+ "transport": "Response",
+ "permission": "ReadData",
+ "params": [
+ { "name": "setting", "type": "string", "required": true, "minLength": 1 }
+ ],
+ "returns": {
+ "type": "object",
+ "properties": {
+ "value": { "type": [ "boolean", "string" ], "required": true }
+ }
+ }
+ },
+ "Settings.SetSkinSettingValue": {
+ "type": "method",
+ "description": "Changes the value of the specified skin setting",
+ "transport": "Response",
+ "permission": "WriteSetting",
+ "params": [
+ { "name": "setting", "type": "string", "required": true, "minLength": 1 },
+ { "name": "value", "type": [ "boolean", "string" ], "required": true }
+ ],
+ "returns": "boolean"
+ },
+ "Settings.GetSkinSettings": {
+ "type": "method",
+ "description": "Retrieves all skin settings of the currently used skin",
+ "transport": "Response",
+ "permission": "ReadData",
+ "returns": {
+ "type": "object",
+ "properties": {
+ "skin": { "type": "string", "required": true, "minLength": 1 },
+ "settings": { "type": "array",
+ "items": { "type": "object",
+ "properties": {
+ "id": { "type": "string", "required": true, "minLength": 1 },
+ "type": { "type": "string", "enum": [ "boolean", "string" ], "required": true },
+ "value": { "type": [ "boolean", "string" ], "required": true }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/xbmc/interfaces/json-rpc/schema/notifications.json b/xbmc/interfaces/json-rpc/schema/notifications.json
new file mode 100644
index 0000000..fa738cf
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/schema/notifications.json
@@ -0,0 +1,464 @@
+{
+ "Player.OnPlay": {
+ "type": "notification",
+ "description": "Playback of a media item has been started or the playback speed has changed. If there is no ID available extra information will be provided.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "$ref": "Player.Notifications.Data", "required": true }
+ ],
+ "returns": null
+ },
+ "Player.OnResume": {
+ "type": "notification",
+ "description": "Playback of a media item has been resumed. If there is no ID available extra information will be provided.",
+ "params": [
+ {
+ "name": "sender",
+ "type": "string",
+ "required": true
+ },
+ {
+ "name": "data",
+ "$ref": "Player.Notifications.Data",
+ "required": true
+ }
+ ],
+ "returns": null
+ },
+ "Player.OnAVStart": {
+ "type": "notification",
+ "description": "Playback of a media item has been started and first frame is available. If there is no ID available extra information will be provided.",
+ "params": [
+ {
+ "name": "sender",
+ "type": "string",
+ "required": true
+ },
+ {
+ "name": "data",
+ "$ref": "Player.Notifications.Data",
+ "required": true
+ }
+ ],
+ "returns": null
+ },
+ "Player.OnAVChange": {
+ "type": "notification",
+ "description": "Audio- or videostream has changed. If there is no ID available extra information will be provided.",
+ "params": [
+ {
+ "name": "sender",
+ "type": "string",
+ "required": true
+ },
+ {
+ "name": "data",
+ "$ref": "Player.Notifications.Data",
+ "required": true
+ }
+ ],
+ "returns": null
+ },
+ "Player.OnPause": {
+ "type": "notification",
+ "description": "Playback of a media item has been paused. If there is no ID available extra information will be provided.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "$ref": "Player.Notifications.Data", "required": true }
+ ],
+ "returns": null
+ },
+ "Player.OnStop": {
+ "type": "notification",
+ "description": "Playback of a media item has been stopped. If there is no ID available extra information will be provided.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "item": { "$ref": "Notifications.Item" },
+ "end": { "type": "boolean", "required": true, "description": "Whether the player has reached the end of the playable item(s) or not" }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "Player.OnSpeedChanged": {
+ "type": "notification",
+ "description": "Speed of the playback of a media item has been changed. If there is no ID available extra information will be provided.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "$ref": "Player.Notifications.Data", "required": true }
+ ],
+ "returns": null
+ },
+ "Player.OnSeek": {
+ "type": "notification",
+ "description": "The playback position has been changed. If there is no ID available extra information will be provided.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "item": { "$ref": "Notifications.Item" },
+ "player": { "$ref": "Player.Notifications.Player.Seek", "required": true }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "Player.OnPropertyChanged": {
+ "type": "notification",
+ "description": "A property of the playing items has changed.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "property": { "$ref": "Player.Property.Value" },
+ "player": { "$ref": "Player.Notifications.Player", "required": true }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "Playlist.OnAdd": {
+ "type": "notification",
+ "description": "A playlist item has been added.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "playlistid": { "$ref": "Playlist.Id", "required": true },
+ "item": { "$ref": "Notifications.Item" },
+ "position": { "$ref": "Playlist.Position" }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "Playlist.OnRemove": {
+ "type": "notification",
+ "description": "A playlist item has been removed.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "playlistid": { "$ref": "Playlist.Id", "required": true },
+ "position": { "$ref": "Playlist.Position" }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "Playlist.OnClear": {
+ "type": "notification",
+ "description": "A playlist item has been cleared.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "playlistid": { "$ref": "Playlist.Id", "required": true }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "AudioLibrary.OnUpdate": {
+ "type": "notification",
+ "description": "An audio item has been updated.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "id": { "$ref": "Library.Id", "required": true },
+ "type": { "type": "string", "id": "Notifications.Library.Audio.Type", "enum": [ "song" ], "required": true },
+ "transaction": { "$ref": "Optional.Boolean", "description": "True if the update is being performed within a transaction." },
+ "added": { "$ref": "Optional.Boolean", "description": "True if the update is for a newly added item." }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "AudioLibrary.OnRemove": {
+ "type": "notification",
+ "description": "An audio item has been removed.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "id": { "$ref": "Library.Id", "required": true },
+ "type": { "$ref": "Notifications.Library.Audio.Type", "required": true },
+ "transaction": { "$ref": "Optional.Boolean", "description": "True if the removal is being performed within a transaction." }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "AudioLibrary.OnScanStarted": {
+ "type": "notification",
+ "description": "An audio library scan has started.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "AudioLibrary.OnScanFinished": {
+ "type": "notification",
+ "description": "Scanning the audio library has been finished.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "AudioLibrary.OnCleanStarted": {
+ "type": "notification",
+ "description": "An audio library clean operation has started.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "AudioLibrary.OnCleanFinished": {
+ "type": "notification",
+ "description": "The audio library has been cleaned.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "AudioLibrary.OnExport": {
+ "type": "notification",
+ "description": "An audio library export has finished.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": false,
+ "properties": {
+ "file": { "type": "string", "required": false, "default": "" },
+ "failcount": { "type": "integer", "minimum": 0, "required": false, "default": 0 }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "VideoLibrary.OnUpdate": {
+ "type": "notification",
+ "description": "A video item has been updated.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "id": { "$ref": "Library.Id", "required": true },
+ "type": { "type": "string", "id": "Notifications.Library.Video.Type", "enum": [ "movie", "tvshow", "episode", "musicvideo" ], "required": true },
+ "playcount": { "type": "integer", "minimum": 0, "default": -1 },
+ "transaction": { "$ref": "Optional.Boolean", "description": "True if the update is being performed within a transaction." },
+ "added": { "$ref": "Optional.Boolean", "description": "True if the update is for a newly added item." }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "VideoLibrary.OnExport": {
+ "type": "notification",
+ "description": "A video library export has finished.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": false,
+ "properties": {
+ "file": { "type": "string", "required": false, "default": "" },
+ "root": { "type": "string", "required": false, "default": "" },
+ "failcount": { "type": "integer", "minimum": 0, "required": false, "default": 0 }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "VideoLibrary.OnRemove": {
+ "type": "notification",
+ "description": "A video item has been removed.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "id": { "$ref": "Library.Id", "required": true },
+ "type": { "$ref": "Notifications.Library.Video.Type", "required": true },
+ "transaction": { "$ref": "Optional.Boolean", "description": "True if the removal is being performed within a transaction." }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "VideoLibrary.OnScanStarted": {
+ "type": "notification",
+ "description": "A video library scan has started.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "VideoLibrary.OnScanFinished": {
+ "type": "notification",
+ "description": "Scanning the video library has been finished.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "VideoLibrary.OnCleanStarted": {
+ "type": "notification",
+ "description": "A video library clean operation has started.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "VideoLibrary.OnCleanFinished": {
+ "type": "notification",
+ "description": "The video library has been cleaned.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "VideoLibrary.OnRefresh": {
+ "type": "notification",
+ "description": "The video library has been refreshed and a home screen reload might be necessary.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "System.OnQuit": {
+ "type": "notification",
+ "description": "Kodi will be closed.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "exitcode": { "type": "integer", "minimum": 0, "required": true }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "System.OnRestart": {
+ "type": "notification",
+ "description": "The system will be restarted.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "System.OnSleep": {
+ "type": "notification",
+ "description": "The system will be suspended.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "System.OnWake": {
+ "type": "notification",
+ "description": "The system woke up from suspension.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "System.OnLowBattery": {
+ "type": "notification",
+ "description": "The system is on low battery.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "Application.OnVolumeChanged": {
+ "type": "notification",
+ "description": "The volume of the application has changed.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "volume": { "type": "integer", "minimum": 0, "maximum": 100, "required": true },
+ "muted": { "type": "boolean", "required": true }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "Input.OnInputRequested": {
+ "type": "notification",
+ "description": "The user is requested to provide some information.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "type": { "type": "string", "enum": [ "keyboard", "time", "date", "ip", "password", "numericpassword", "number", "seconds" ], "required": true },
+ "value": { "type": "string", "required": true },
+ "title": { "type": "string" }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "Input.OnInputFinished": {
+ "type": "notification",
+ "description": "The user has provided the requested input.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "GUI.OnScreensaverActivated": {
+ "type": "notification",
+ "description": "The screensaver has been activated.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "GUI.OnScreensaverDeactivated": {
+ "type": "notification",
+ "description": "The screensaver has been deactivated.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "object", "required": true,
+ "properties": {
+ "shuttingdown": { "type": "boolean", "required": true }
+ }
+ }
+ ],
+ "returns": null
+ },
+ "GUI.OnDPMSActivated": {
+ "type": "notification",
+ "description": "Energy saving/DPMS has been activated.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ },
+ "GUI.OnDPMSDeactivated": {
+ "type": "notification",
+ "description": "Energy saving/DPMS has been deactivated.",
+ "params": [
+ { "name": "sender", "type": "string", "required": true },
+ { "name": "data", "type": "null", "required": true }
+ ],
+ "returns": null
+ }
+}
diff --git a/xbmc/interfaces/json-rpc/schema/types.json b/xbmc/interfaces/json-rpc/schema/types.json
new file mode 100644
index 0000000..37b5140
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/schema/types.json
@@ -0,0 +1,2081 @@
+{
+ "Optional.Boolean": {
+ "type": [ "null", "boolean" ],
+ "default": null
+ },
+ "Optional.String": {
+ "type": [ "null", "string" ],
+ "default": null
+ },
+ "Optional.Integer": {
+ "type": [ "null", "integer" ],
+ "default": null
+ },
+ "Optional.Number": {
+ "type": [ "null", "number" ],
+ "default": null
+ },
+ "Array.String": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "Array.Integer": {
+ "type": "array",
+ "items": { "type": "integer" }
+ },
+ "Global.Time": {
+ "type": "object",
+ "description": "A duration.",
+ "properties": {
+ "hours": { "type": "integer", "required": true, "minimum": 0 },
+ "minutes": { "type": "integer", "required": true, "minimum": 0, "maximum": 59 },
+ "seconds": { "type": "integer", "required": true, "minimum": 0, "maximum": 59 },
+ "milliseconds": { "type": "integer", "required": true, "minimum": 0, "maximum": 999 }
+ },
+ "additionalProperties": false
+ },
+ "Global.Weekday": {
+ "type": "string",
+ "enum": [ "monday", "tuesday", "wednesday", "thursday",
+ "friday", "saturday", "sunday" ]
+ },
+ "Global.IncrementDecrement": {
+ "type": "string",
+ "enum": [ "increment", "decrement" ]
+ },
+ "Global.Toggle": {
+ "type": [
+ { "type": "boolean", "required": true },
+ { "type": "string", "enum": [ "toggle" ], "required": true }
+ ]
+ },
+ "Global.String.NotEmpty": {
+ "type": "string",
+ "minLength": 1
+ },
+ "Configuration.Notifications": {
+ "type": "object",
+ "properties": {
+ "Player": { "type": "boolean", "required": true },
+ "Playlist": { "type": "boolean", "required": true },
+ "GUI": { "type": "boolean", "required": true },
+ "System": { "type": "boolean", "required": true },
+ "VideoLibrary": { "type": "boolean", "required": true },
+ "AudioLibrary": { "type": "boolean", "required": true },
+ "Application": { "type": "boolean", "required": true },
+ "Input": { "type": "boolean", "required": true },
+ "PVR": { "type": "boolean", "required": true },
+ "Other": { "type": "boolean", "required": true }
+ },
+ "additionalProperties": false
+ },
+ "Configuration": {
+ "type": "object", "required": true,
+ "properties": {
+ "notifications": { "$ref": "Configuration.Notifications", "required": true }
+ }
+ },
+ "Files.Media": {
+ "type": "string",
+ "enum": [ "video", "music", "pictures", "files", "programs" ]
+ },
+ "List.Amount": {
+ "type": "integer",
+ "default": -1,
+ "minimum": 0
+ },
+ "List.Limits": {
+ "type": "object",
+ "properties": {
+ "start": { "type": "integer", "minimum": 0, "default": 0, "description": "Index of the first item to return" },
+ "end": { "$ref": "List.Amount", "description": "Index of the last item to return" }
+ },
+ "additionalProperties": false
+ },
+ "List.LimitsReturned": {
+ "type": "object",
+ "properties": {
+ "start": { "type": "integer", "minimum": 0, "default": 0 },
+ "end": { "$ref": "List.Amount" },
+ "total": { "type": "integer", "minimum": 0, "required": true }
+ },
+ "additionalProperties": false
+ },
+ "List.Sort": {
+ "type": "object",
+ "properties": {
+ "method": { "type": "string", "default": "none",
+ "enum": [ "none", "label", "date", "size", "file", "path", "drivetype", "title", "track", "time", "artist",
+ "album", "albumtype", "genre", "country", "year", "rating", "userrating", "votes", "top250", "programcount",
+ "playlist", "episode", "season", "totalepisodes", "watchedepisodes", "tvshowstatus", "tvshowtitle",
+ "sorttitle", "productioncode", "mpaa", "studio", "dateadded", "lastplayed", "playcount", "listeners",
+ "bitrate", "random", "totaldiscs", "originaldate", "bpm", "originaltitle" ]
+ },
+ "order": { "type": "string", "default": "ascending", "enum": [ "ascending", "descending" ] },
+ "ignorearticle": { "type": "boolean", "default": false },
+ "useartistsortname": { "type": "boolean", "default": false }
+ }
+ },
+ "Library.Id": {
+ "type": "integer",
+ "default": -1,
+ "minimum": 1
+ },
+ "PVR.Channel.Type": {
+ "type": "string",
+ "enum": [ "tv", "radio" ]
+ },
+ "Playlist.Id": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "default": -1
+ },
+ "Playlist.Type": {
+ "type": "string",
+ "enum": [ "unknown", "video", "audio", "picture", "mixed" ]
+ },
+ "Playlist.Property.Name": {
+ "type": "string",
+ "enum": [ "type", "size" ]
+ },
+ "Playlist.Property.Value": {
+ "type": "object",
+ "properties": {
+ "type": { "$ref": "Playlist.Type" },
+ "size": { "type": "integer", "minimum": 0 }
+ }
+ },
+ "Playlist.Position": {
+ "type": "integer",
+ "minimum": 0,
+ "default": -1
+ },
+ "Playlist.Item": {
+ "type": [
+ { "type": "object", "properties": { "file": { "type": "string", "description": "Path to a file (not a directory) to be added to the playlist", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "directory": { "type": "string", "required": true }, "recursive": { "type": "boolean", "default": false }, "media": { "$ref": "Files.Media", "default": "files" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "movieid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "episodeid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "musicvideoid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "artistid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "albumid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "songid": { "$ref": "Library.Id", "required": true } }, "additionalProperties": false },
+ { "type": "object", "properties": { "genreid": { "$ref": "Library.Id", "required": true, "description": "Identification of a genre from the AudioLibrary" } }, "additionalProperties": false },
+ { "type": "object", "properties": { "recordingid": { "$ref": "Library.Id", "required": true, "description": "Identification of a PVR recording" } }, "additionalProperties": false }
+ ]
+ },
+ "Player.Id": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "default": -1
+ },
+ "Player.Type": {
+ "type": "string",
+ "enum": [ "video", "audio", "picture" ]
+ },
+ "Player.Position.Percentage": {
+ "type": "number",
+ "minimum": 0.0,
+ "maximum": 100.0
+ },
+ "Player.Position.Time": {
+ "type": "object",
+ "description": "A position in duration.",
+ "additionalProperties": false,
+ "properties": {
+ "hours": { "type": "integer", "minimum": 0, "default": 0 },
+ "minutes": { "type": "integer", "minimum": 0, "maximum": 59, "default": 0 },
+ "seconds": { "type": "integer", "minimum": 0, "maximum": 59, "default": 0 },
+ "milliseconds": { "type": "integer", "minimum": 0, "maximum": 999, "default": 0 }
+ }
+ },
+ "Player.Speed": {
+ "type": "object",
+ "required": true,
+ "properties": {
+ "speed": { "type": "integer" }
+ }
+ },
+ "Player.ViewMode": {
+ "type": "string",
+ "enum": [ "normal", "zoom", "stretch4x3", "widezoom", "stretch16x9", "original",
+ "stretch16x9nonlin", "zoom120width", "zoom110width" ]
+ },
+ "Player.CustomViewMode": {
+ "type": "object",
+ "required": true,
+ "properties": {
+ "zoom": { "type": [
+ { "type": "string", "enum": [ "increase", "decrease" ], "required": true },
+ { "$ref": "Optional.Number", "minimum":0.5, "maximum": 2.0, "description": "Zoom where 1.0 means 100%", "required": true } ] },
+ "pixelratio": { "type": [
+ { "type": "string", "enum": [ "increase", "decrease" ], "required": true },
+ { "$ref": "Optional.Number", "minimum":0.5, "maximum": 2.0, "description": "Pixel aspect ratio where 1.0 means square pixel", "required": true } ] },
+ "verticalshift": { "type": [
+ { "type": "string", "enum": [ "increase", "decrease" ], "required": true },
+ { "$ref": "Optional.Number", "minimum": -2.0, "maximum": 2.0, "description": "Vertical shift 1.0 means shift to bottom", "required": true } ] },
+ "nonlinearstretch": { "type": [
+ { "type": "string", "enum": [ "increase", "decrease" ], "required": true },
+ { "$ref": "Optional.Boolean", "description": "Flag to enable nonlinear stretch", "required": true } ] }
+ }
+ },
+ "Player.Repeat": {
+ "type": "string",
+ "enum": [ "off", "one", "all" ]
+ },
+ "Player.Audio.Stream": {
+ "type": "object",
+ "properties": {
+ "index": { "type": "integer", "minimum": 0, "required": true },
+ "name": { "type": "string", "required": true },
+ "language": { "type": "string", "required": true },
+ "codec": { "type": "string", "required": true },
+ "bitrate": { "type": "integer", "required": true },
+ "channels": { "type": "integer", "required": true },
+ "isdefault": { "type": "boolean", "required": true },
+ "isoriginal": { "type": "boolean", "required": true },
+ "isimpaired": { "type": "boolean", "required": true },
+ "samplerate": { "type": "integer", "required": true }
+ }
+ },
+ "Player.Video.Stream": {
+ "type": "object",
+ "properties": {
+ "index": { "type": "integer", "minimum": 0, "required": true },
+ "name": { "type": "string", "required": true },
+ "language": { "type": "string", "required": true },
+ "codec": { "type": "string", "required": true },
+ "width": { "type": "integer", "required": true },
+ "height": { "type": "integer", "required": true }
+ }
+ },
+ "Player.Subtitle": {
+ "type": "object",
+ "properties": {
+ "index": { "type": "integer", "minimum": 0, "required": true },
+ "name": { "type": "string", "required": true },
+ "language": { "type": "string", "required": true },
+ "isdefault": { "type": "boolean", "required": true },
+ "isforced": { "type": "boolean", "required": true },
+ "isimpaired": { "type": "boolean", "required": true }
+ }
+ },
+ "Player.Property.Name": {
+ "type": "string",
+ "enum": [ "type", "partymode", "speed", "time", "percentage",
+ "totaltime", "playlistid", "position", "repeat", "shuffled",
+ "canseek", "canchangespeed", "canmove", "canzoom", "canrotate",
+ "canshuffle", "canrepeat", "currentaudiostream", "audiostreams",
+ "subtitleenabled", "currentsubtitle", "subtitles", "live",
+ "currentvideostream", "videostreams", "cachepercentage" ]
+ },
+ "Player.Property.Value": {
+ "type": "object",
+ "properties": {
+ "type": { "$ref": "Player.Type" },
+ "partymode": { "type": "boolean" },
+ "speed": { "type": "integer" },
+ "time": { "$ref": "Global.Time" },
+ "percentage": { "$ref": "Player.Position.Percentage" },
+ "totaltime": { "$ref": "Global.Time" },
+ "playlistid": { "$ref": "Playlist.Id" },
+ "position": { "$ref": "Playlist.Position" },
+ "repeat": { "$ref": "Player.Repeat" },
+ "shuffled": { "type": "boolean" },
+ "canseek": { "type": "boolean" },
+ "canchangespeed": { "type": "boolean" },
+ "canmove": { "type": "boolean" },
+ "canzoom": { "type": "boolean" },
+ "canrotate": { "type": "boolean" },
+ "canshuffle": { "type": "boolean" },
+ "canrepeat": { "type": "boolean" },
+ "currentaudiostream": { "$ref": "Player.Audio.Stream" },
+ "audiostreams": { "type": "array", "items": { "$ref": "Player.Audio.Stream" } },
+ "currentvideostream": { "$ref": "Player.Video.Stream" },
+ "videostreams": { "type": "array", "items": { "$ref": "Player.Video.Stream" } },
+ "subtitleenabled": { "type": "boolean" },
+ "currentsubtitle": { "$ref": "Player.Subtitle" },
+ "subtitles": { "type": "array", "items": { "$ref": "Player.Subtitle" } },
+ "live": { "type": "boolean" },
+ "cachepercentage": { "$ref": "Player.Position.Percentage" }
+ }
+ },
+ "Notifications.Item.Type": {
+ "type": "string",
+ "enum": [ "unknown", "movie", "episode", "musicvideo", "song", "picture", "channel" ]
+ },
+ "Notifications.Item": {
+ "type": [
+ { "type": "object", "description": "An unknown item does not have any additional information.",
+ "properties": {
+ "type": { "$ref": "Notifications.Item.Type", "required": true }
+ }
+ },
+ { "type": "object", "description": "An item known to the database has an identification.",
+ "properties": {
+ "type": { "$ref": "Notifications.Item.Type", "required": true },
+ "id": { "$ref": "Library.Id", "required": true }
+ }
+ },
+ { "type": "object", "description": "A movie item has a title and may have a release year.",
+ "properties": {
+ "type": { "$ref": "Notifications.Item.Type", "required": true },
+ "title": { "type": "string", "required": true },
+ "year": { "type": "integer" }
+ }
+ },
+ { "type": "object", "description": "A tv episode has a title and may have an episode number, season number and the title of the show it belongs to.",
+ "properties": {
+ "type": { "$ref": "Notifications.Item.Type", "required": true },
+ "title": { "type": "string", "required": true },
+ "episode": { "type": "integer" },
+ "season": { "type": "integer" },
+ "showtitle": { "type": "string" }
+ }
+ },
+ { "type": "object", "description": "A music video has a title and may have an album and an artist.",
+ "properties": {
+ "type": { "$ref": "Notifications.Item.Type", "required": true },
+ "title": { "type": "string", "required": true },
+ "album": { "type": "string" },
+ "artist": { "type": "string" }
+ }
+ },
+ { "type": "object", "description": "A song has a title and may have an album, an artist and a track number.",
+ "properties": {
+ "type": { "$ref": "Notifications.Item.Type", "required": true },
+ "title": { "type": "string", "required": true },
+ "album": { "type": "string" },
+ "artist": { "type": "string" },
+ "track": { "type": "integer" }
+ }
+ },
+ { "type": "object", "description": "A picture has a file path.",
+ "properties": {
+ "type": { "$ref": "Notifications.Item.Type", "required": true },
+ "file": { "type": "string", "required": true }
+ }
+ },
+ { "type": "object", "description": "A PVR channel is either a radio or tv channel and has a title.",
+ "properties": {
+ "type": { "$ref": "Notifications.Item.Type", "required": true },
+ "id": { "$ref": "Library.Id", "required": true },
+ "title": { "type": "string", "required": true },
+ "channeltype": { "$ref": "PVR.Channel.Type", "required": true }
+ }
+ }
+ ]
+ },
+ "Player.Notifications.Player": {
+ "type": "object",
+ "properties": {
+ "playerid": { "$ref": "Player.Id", "required": true },
+ "speed": { "type": "integer" }
+ }
+ },
+ "Player.Notifications.Player.Seek": {
+ "extends": "Player.Notifications.Player",
+ "properties": {
+ "time": { "$ref": "Global.Time" },
+ "seekoffset": { "$ref": "Global.Time" }
+ }
+ },
+ "Player.Notifications.Data": {
+ "type": "object",
+ "properties": {
+ "item": { "$ref": "Notifications.Item", "required": true },
+ "player": { "$ref": "Player.Notifications.Player", "required": true }
+ }
+ },
+ "Item.Fields.Base": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": { "type": "string" }
+ },
+ "Item.Details.Base": {
+ "type": "object",
+ "properties": {
+ "label": { "type": "string", "required": true }
+ }
+ },
+ "Item.CustomProperties": {
+ "type": "object",
+ "additionalProperties": { "$ref": "Global.String.NotEmpty" }
+ },
+ "Media.Details.Base": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "fanart": { "type": "string" },
+ "thumbnail": { "type": "string" }
+ }
+ },
+ "Media.Artwork": {
+ "type": "object",
+ "properties": {
+ "thumb": { "$ref": "Global.String.NotEmpty" },
+ "poster": { "$ref": "Global.String.NotEmpty" },
+ "banner": { "$ref": "Global.String.NotEmpty" },
+ "fanart": { "$ref": "Global.String.NotEmpty" }
+ },
+ "additionalProperties": { "$ref": "Global.String.NotEmpty" }
+ },
+ "Media.Artwork.Set": {
+ "type": "object",
+ "properties": {
+ "thumb": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ], "default": "" },
+ "poster": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ], "default": "" },
+ "banner": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ], "default": "" },
+ "fanart": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ], "default": "" }
+ },
+ "additionalProperties": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ] }
+ },
+ "Video.Rating": {
+ "type": "object",
+ "properties": {
+ "rating": { "type": "number", "required": true },
+ "votes": { "type": "integer" },
+ "default": { "type": "boolean" }
+ }
+ },
+ "Video.Ratings": {
+ "type": "object",
+ "additionalProperties": { "$ref": "Video.Rating" }
+ },
+ "Video.Ratings.Set": {
+ "type": "object",
+ "additionalProperties": { "type": [ "null", { "$ref": "Video.Rating", "required": true } ] }
+ },
+ "Media.UniqueID": {
+ "type": "object",
+ "additionalProperties": { "$ref": "Global.String.NotEmpty" }
+ },
+ "Media.UniqueID.Set": {
+ "type": "object",
+ "additionalProperties": { "type": [ "null", { "$ref": "Global.String.NotEmpty", "required": true } ] }
+ },
+ "Library.Fields.Source": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string", "enum": [ "file", "paths" ] }
+ },
+ "Library.Details.Source": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "sourceid": { "$ref": "Library.Id", "required": true },
+ "file": { "type": "string", "description": "The url encoded multipath string combining all paths of the source ", "required": true },
+ "paths": { "$ref": "Array.String", "description": "The individual paths of the media source" }
+ }
+ },
+ "Library.Fields.Genre": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string", "enum": [ "title", "thumbnail", "sourceid" ] }
+ },
+ "Library.Details.Genre": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "genreid": { "$ref": "Library.Id", "required": true },
+ "title": { "type": "string" },
+ "thumbnail": { "type": "string" },
+ "sourceid": { "$ref": "Array.Integer", "description": "The ids of sources with songs of the genre" }
+ }
+ },
+ "Library.Fields.Tag": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string", "enum": [ "title" ] }
+ },
+ "Library.Details.Tag": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "tagid": { "$ref": "Library.Id", "required": true },
+ "title": { "type": "string" }
+ }
+ },
+ "Audio.Fields.Role": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string", "enum": [ "title" ] }
+ },
+ "Audio.Details.Role": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "roleid": { "$ref": "Library.Id", "required": true },
+ "title": { "type": "string" }
+ }
+ },
+ "Audio.Fields.Artist": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "description": "Requesting the (song)genreid/genre, roleid/role or sourceid fields will result in increased response times",
+ "enum": [ "instrument", "style", "mood", "born", "formed",
+ "description", "genre", "died", "disbanded",
+ "yearsactive", "musicbrainzartistid", "fanart",
+ "thumbnail", "compilationartist", "dateadded",
+ "roles", "songgenres", "isalbumartist",
+ "sortname", "type", "gender", "disambiguation", "art", "sourceid",
+ "datemodified", "datenew" ]
+ }
+ },
+ "Audio.Fields.Album": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "description": "Requesting the songgenres, artistid and/or sourceid fields will result in increased response times",
+ "enum": [ "title", "description", "artist", "genre",
+ "theme", "mood", "style", "type", "albumlabel",
+ "rating", "votes", "userrating","year", "musicbrainzalbumid",
+ "musicbrainzalbumartistid", "fanart", "thumbnail",
+ "playcount", "artistid", "displayartist",
+ "compilation", "releasetype", "dateadded",
+ "sortartist", "musicbrainzreleasegroupid", "songgenres", "art",
+ "lastplayed", "sourceid","isboxset", "totaldiscs",
+ "releasedate", "originaldate", "albumstatus", "datemodified", "datenew",
+ "albumduration"]
+ }
+ },
+ "Audio.Fields.Song": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "description": "Requesting the genreid, artistid, albumartistid and/or sourceid fields will result in increased response times",
+ "enum": [ "title", "artist", "albumartist", "genre", "year",
+ "rating", "album", "track", "duration", "comment",
+ "lyrics", "musicbrainztrackid", "musicbrainzartistid",
+ "musicbrainzalbumid", "musicbrainzalbumartistid",
+ "playcount", "fanart", "thumbnail", "file", "albumid",
+ "lastplayed", "disc", "genreid", "artistid", "displayartist",
+ "albumartistid", "albumreleasetype", "dateadded",
+ "votes", "userrating", "mood", "contributors",
+ "displaycomposer", "displayconductor", "displayorchestra", "displaylyricist",
+ "sortartist", "art", "sourceid", "disctitle", "releasedate", "originaldate",
+ "bpm", "samplerate", "bitrate", "channels", "datemodified", "datenew" ]
+ }
+ },
+ "Audio.Album.ReleaseType": {
+ "type": "string",
+ "enum": [ "album", "single" ],
+ "default": "album"
+ },
+ "Audio.Contributors": {
+ "type": "array",
+ "items": { "type": "object",
+ "description": "The artist and the role they contribute to a song",
+ "properties": {
+ "name": { "type": "string", "required": true },
+ "role": { "type": "string", "required": true },
+ "roleid": { "$ref": "Library.Id", "required": true },
+ "artistid": { "$ref": "Library.Id", "required": true }
+ },
+ "additionalProperties": false
+ }
+ },
+ "Audio.Artist.Roles": {
+ "type": "array",
+ "items": { "type": "object",
+ "description": "The various roles contributed by an artist to one or more songs",
+ "properties": {
+ "roleid": { "$ref": "Library.Id", "required": true },
+ "role": { "type": "string", "required": true }
+ },
+ "additionalProperties": false
+ }
+ },
+ "Audio.Details.Genres": {
+ "type": "array",
+ "items": { "type": "object",
+ "properties": {
+ "genreid": { "$ref": "Library.Id", "required": true },
+ "title": { "type": "string" }
+ }
+ }
+ },
+ "Audio.Details.Base": {
+ "extends": "Media.Details.Base",
+ "properties": {
+ "genre": { "$ref": "Array.String" },
+ "dateadded": { "type": "string" },
+ "art": { "$ref": "Media.Artwork" }
+ }
+ },
+ "Audio.Details.Media": {
+ "extends": "Audio.Details.Base",
+ "properties": {
+ "title": { "type": "string" },
+ "artist": { "$ref": "Array.String" },
+ "year": { "type": "integer" },
+ "rating": { "type": "number" },
+ "musicbrainzalbumartistid": { "$ref": "Array.String" },
+ "artistid": { "$ref": "Array.Integer" },
+ "displayartist": { "type" : "string" },
+ "votes": { "type": "integer" },
+ "userrating": { "type": "integer" },
+ "sortartist": { "type" : "string" },
+ "releasedate": { "type" : "string" },
+ "originaldate": { "type" : "string" }
+ }
+ },
+ "Audio.Details.Artist": {
+ "extends": "Audio.Details.Base",
+ "properties": {
+ "artistid": { "$ref": "Library.Id", "required": true },
+ "artist": { "type": "string", "required": true },
+ "instrument": { "$ref": "Array.String" },
+ "style": { "$ref": "Array.String" },
+ "mood": { "$ref": "Array.String" },
+ "born": { "type": "string" },
+ "formed": { "type": "string" },
+ "description": { "type": "string" },
+ "died": { "type": "string" },
+ "disbanded": { "type": "string" },
+ "yearsactive": { "$ref": "Array.String" },
+ "compilationartist": { "type": "boolean" },
+ "musicbrainzartistid": { "$ref": "Array.String" },
+ "roles": {"$ref": "Audio.Artist.Roles"},
+ "songgenres": {"$ref": "Audio.Details.Genres"},
+ "isalbumartist": { "type": "boolean" },
+ "sortname": { "type": "string" },
+ "type": { "type": "string" },
+ "gender": { "type": "string" },
+ "disambiguation": { "type": "string" },
+ "sourceid": { "$ref": "Array.Integer" }
+ }
+ },
+ "Audio.Details.Album": {
+ "extends": "Audio.Details.Media",
+ "properties": {
+ "albumid": { "$ref": "Library.Id", "required": true },
+ "description": { "type": "string" },
+ "theme": { "$ref": "Array.String" },
+ "mood": { "$ref": "Array.String" },
+ "style": { "$ref": "Array.String" },
+ "type": { "type": "string" },
+ "albumlabel": { "type": "string" },
+ "playcount": { "type": "integer" },
+ "compilation": { "type": "boolean" },
+ "releasetype": { "$ref": "Audio.Album.ReleaseType" },
+ "musicbrainzreleasegroupid": { "type": "string" },
+ "musicbrainzalbumid": { "type": "string" },
+ "songgenres": {"$ref": "Audio.Details.Genres"},
+ "lastplayed": { "type": "string" },
+ "sourceid": { "$ref": "Array.Integer" },
+ "isboxset" : { "type": "boolean" },
+ "totaldiscs": { "type": "integer" },
+ "albumstatus": { "type": "string" },
+ "albumduration": { "type": "integer" }
+ }
+ },
+ "Audio.Details.Song": {
+ "extends": "Audio.Details.Media",
+ "properties": {
+ "songid": { "$ref": "Library.Id", "required": true },
+ "file": { "type": "string" },
+ "albumartist": { "$ref": "Array.String" },
+ "album": { "type": "string" },
+ "track": { "type": "integer" },
+ "duration": { "type": "integer" },
+ "comment": { "type": "string" },
+ "lyrics": { "type": "string" },
+ "playcount": { "type": "integer" },
+ "musicbrainztrackid": { "type": "string" },
+ "musicbrainzartistid": { "$ref": "Array.String" },
+ "albumid": { "$ref": "Library.Id" },
+ "lastplayed": { "type": "string" },
+ "disc": { "type": "integer" },
+ "albumartistid": { "$ref": "Array.Integer" },
+ "albumreleasetype": { "$ref": "Audio.Album.ReleaseType" },
+ "mood": { "type": "string"},
+ "contributors": { "$ref": "Audio.Contributors" },
+ "displaycomposer": { "type": "string"},
+ "displayconductor": { "type": "string"},
+ "displayorchestra": { "type": "string"},
+ "displaylyricist": { "type": "string"},
+ "genreid": { "$ref": "Array.Integer"},
+ "sourceid": { "$ref": "Array.Integer" },
+ "disctitle": { "type": "string" },
+ "bpm": { "type": "Integer" },
+ "samplerate": { "type": "Integer" },
+ "bitrate": { "type": "Integer"},
+ "channels": { "type": "Integer"}
+ }
+ },
+ "Audio.Property.Name": {
+ "type": "string",
+ "enum": [ "missingartistid", "librarylastupdated", "librarylastcleaned", "artistlinksupdated",
+ "songslastadded", "albumslastadded", "artistslastadded", "genreslastadded",
+ "songsmodified", "albumsmodified", "artistsmodified"]
+ },
+ "Audio.Property.Value": {
+ "type": "object",
+ "properties": {
+ "missingartistid": { "$ref": "Library.Id" },
+ "librarylastupdated": { "type": "string" },
+ "librarylastcleaned": { "type": "string" },
+ "artistlinksupdated": { "type": "string" },
+ "songslastadded": { "type": "string" },
+ "albumslastadded": { "type": "string" },
+ "artistslastadded": { "type": "string" },
+ "genreslastadded": { "type": "string" },
+ "songsmodified": { "type": "string" },
+ "albumsmodified": { "type": "string" },
+ "artistsmodified": { "type": "string" }
+ }
+ },
+ "Video.Fields.Movie": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "description": "Requesting the cast, ratings, showlink, streamdetails, uniqueid and/or tag field will result in increased response times",
+ "enum": [ "title", "genre", "year", "rating", "director", "trailer",
+ "tagline", "plot", "plotoutline", "originaltitle", "lastplayed",
+ "playcount", "writer", "studio", "mpaa", "cast", "country",
+ "imdbnumber", "runtime", "set", "showlink", "streamdetails",
+ "top250", "votes", "fanart", "thumbnail", "file", "sorttitle",
+ "resume", "setid", "dateadded", "tag", "art", "userrating",
+ "ratings", "premiered", "uniqueid" ]
+ }
+ },
+ "Video.Fields.MovieSet": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "title", "playcount", "fanart", "thumbnail", "art", "plot" ]
+ }
+ },
+ "Video.Fields.TVShow": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "description": "Requesting the cast, ratings, uniqueid and/or tag field will result in increased response times",
+ "enum": [ "title", "genre", "year", "rating", "plot",
+ "studio", "mpaa", "cast", "playcount", "episode",
+ "imdbnumber", "premiered", "votes", "lastplayed",
+ "fanart", "thumbnail", "file", "originaltitle",
+ "sorttitle", "episodeguide", "season", "watchedepisodes",
+ "dateadded", "tag", "art", "userrating", "ratings",
+ "runtime", "uniqueid" ]
+ }
+ },
+ "Video.Fields.Season": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "season", "showtitle", "playcount", "episode", "fanart", "thumbnail", "tvshowid",
+ "watchedepisodes", "art", "userrating", "title" ]
+ }
+ },
+ "Video.Fields.Episode": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "description": "Requesting the cast, ratings, streamdetails, uniqueid and/or tag field will result in increased response times",
+ "enum": [ "title", "plot", "votes", "rating", "writer",
+ "firstaired", "playcount", "runtime", "director",
+ "productioncode", "season", "episode", "originaltitle",
+ "showtitle", "cast", "streamdetails", "lastplayed", "fanart",
+ "thumbnail", "file", "resume", "tvshowid", "dateadded",
+ "uniqueid", "art", "specialsortseason", "specialsortepisode", "userrating",
+ "seasonid", "ratings" ]
+ }
+ },
+ "Video.Fields.MusicVideo": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "description": "Requesting the streamdetails, uniqueid and/or tag field will result in increased response times",
+ "enum": [ "title", "playcount", "runtime", "director",
+ "studio", "year", "plot", "album", "artist",
+ "genre", "track", "streamdetails", "lastplayed",
+ "fanart", "thumbnail", "file", "resume", "dateadded",
+ "tag", "art", "rating", "userrating", "premiered", "uniqueid" ]
+ }
+ },
+ "Video.Cast": {
+ "type": "array",
+ "items": { "type": "object",
+ "properties": {
+ "name": { "type": "string", "required": true },
+ "role": { "type": "string", "required": true },
+ "order": { "type": "integer", "required": true },
+ "thumbnail": { "type": "string" }
+ },
+ "additionalProperties": false
+ }
+ },
+ "Video.Streams": {
+ "type": "object",
+ "properties": {
+ "audio": { "type": "array", "minItems": 1,
+ "items": { "type": "object",
+ "properties": {
+ "codec": { "type": "string" },
+ "language": { "type": "string" },
+ "channels": { "type": "integer" }
+ },
+ "additionalProperties": false
+ }
+ },
+ "video": { "type": "array", "minItems": 1,
+ "items": { "type": "object",
+ "properties": {
+ "codec": { "type": "string" },
+ "aspect": { "type": "number" },
+ "width": { "type": "integer" },
+ "height": { "type": "integer" },
+ "duration": { "type": "integer" },
+ "hdrtype": { "type": "string"}
+ },
+ "additionalProperties": false
+ }
+ },
+ "subtitle": { "type": "array", "minItems": 1,
+ "items": { "type": "object",
+ "properties": {
+ "language": { "type": "string" }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "Video.Resume": {
+ "type": "object",
+ "properties": {
+ "position": { "type": "number", "minimum": 0.0 },
+ "total": { "type": "number", "minimum": 0.0 }
+ },
+ "additionalProperties": false
+ },
+ "Video.Details.Base": {
+ "extends": "Media.Details.Base",
+ "properties": {
+ "playcount": { "type": "integer" },
+ "art": { "$ref": "Media.Artwork" }
+ }
+ },
+ "Video.Details.Media": {
+ "extends": "Video.Details.Base",
+ "properties": {
+ "title": { "type": "string" }
+ }
+ },
+ "Video.Details.Item": {
+ "extends": "Video.Details.Media",
+ "properties": {
+ "file": { "type": "string" },
+ "plot": { "type": "string" },
+ "lastplayed": { "type": "string" },
+ "dateadded": { "type": "string" }
+ }
+ },
+ "Video.Details.File": {
+ "extends": "Video.Details.Item",
+ "properties": {
+ "runtime": { "type": "integer", "description": "Runtime in seconds" },
+ "director": { "$ref": "Array.String" },
+ "streamdetails": { "$ref": "Video.Streams" },
+ "resume": { "$ref": "Video.Resume" }
+ }
+ },
+ "Video.Details.Movie": {
+ "extends": "Video.Details.File",
+ "properties": {
+ "movieid": { "$ref": "Library.Id", "required": true },
+ "genre": { "$ref": "Array.String" },
+ "year": { "type": "integer" },
+ "rating": { "type": "number" },
+ "trailer": { "type": "string" },
+ "tagline": { "type": "string" },
+ "plotoutline": { "type": "string" },
+ "originaltitle": { "type": "string" },
+ "sorttitle": { "type": "string" },
+ "writer": { "$ref": "Array.String" },
+ "studio": { "$ref": "Array.String" },
+ "mpaa": { "type": "string" },
+ "cast": { "$ref": "Video.Cast" },
+ "country": { "$ref": "Array.String" },
+ "imdbnumber": { "type": "string" },
+ "set": { "type": "string" },
+ "showlink": { "$ref": "Array.String" },
+ "top250": { "type": "integer" },
+ "votes": { "type": "string" },
+ "setid": { "$ref": "Library.Id" },
+ "tag": { "$ref": "Array.String" },
+ "userrating": { "type": "integer" },
+ "ratings": { "type": "Video.Ratings" },
+ "premiered": { "type": "string" },
+ "uniqueid": { "$ref": "Media.UniqueID" }
+ }
+ },
+ "Video.Details.MovieSet": {
+ "extends": "Video.Details.Media",
+ "properties": {
+ "setid": { "$ref": "Library.Id", "required": true },
+ "plot": { "type": "string" }
+ }
+ },
+ "Video.Details.MovieSet.Extended": {
+ "extends": "Video.Details.MovieSet",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "movies": { "type": "array",
+ "items": { "$ref": "Video.Details.Movie" }
+ }
+ }
+ },
+ "Video.Details.TVShow": {
+ "extends": "Video.Details.Item",
+ "properties": {
+ "tvshowid": { "$ref": "Library.Id", "required": true },
+ "genre": { "$ref": "Array.String" },
+ "year": { "type": "integer" },
+ "rating": { "type": "number" },
+ "originaltitle": { "type": "string" },
+ "sorttitle": { "type": "string" },
+ "studio": { "$ref": "Array.String" },
+ "mpaa": { "type": "string" },
+ "cast": { "$ref": "Video.Cast" },
+ "episode": { "type": "integer" },
+ "watchedepisodes": { "type": "integer" },
+ "imdbnumber": { "type": "string" },
+ "premiered": { "type": "string" },
+ "votes": { "type": "string" },
+ "episodeguide": { "type": "string" },
+ "season": { "type": "integer" },
+ "tag": { "$ref": "Array.String" },
+ "userrating": { "type": "integer" },
+ "ratings": { "type": "Video.Ratings" },
+ "runtime": { "type": "integer", "description": "Runtime in seconds" },
+ "status": { "type": "string", "description": "Returns 'returning series', 'in production', 'planned', 'cancelled' or 'ended'" },
+ "uniqueid": { "$ref": "Media.UniqueID" }
+ }
+ },
+ "Video.Details.Season": {
+ "extends": "Video.Details.Base",
+ "properties": {
+ "seasonid": { "$ref": "Library.Id", "required": true },
+ "season": { "type": "integer", "required": true },
+ "showtitle": { "type": "string" },
+ "episode": { "type": "integer" },
+ "watchedepisodes": { "type": "integer" },
+ "tvshowid": { "$ref": "Library.Id" },
+ "userrating": { "type": "integer" },
+ "title": { "type": "string" }
+ }
+ },
+ "Video.Details.Episode": {
+ "extends": "Video.Details.File",
+ "properties": {
+ "episodeid": { "$ref": "Library.Id", "required": true },
+ "votes": { "type": "string" },
+ "rating": { "type": "number" },
+ "writer": { "$ref": "Array.String" },
+ "firstaired": { "type": "string" },
+ "productioncode": { "type": "string" },
+ "season": { "type": "integer" },
+ "episode": { "type": "integer" },
+ "uniqueid": { "$ref": "Media.UniqueID" },
+ "originaltitle": { "type": "string" },
+ "showtitle": { "type": "string" },
+ "cast": { "$ref": "Video.Cast" },
+ "tvshowid": { "$ref": "Library.Id" },
+ "specialsortseason": { "type": "integer" },
+ "specialsortepisode": { "type": "integer" },
+ "userrating": { "type": "integer" },
+ "seasonid": { "$ref": "Library.Id" },
+ "ratings": { "type": "Video.Ratings" }
+ }
+ },
+ "Video.Details.MusicVideo": {
+ "extends": "Video.Details.File",
+ "properties": {
+ "musicvideoid": { "$ref": "Library.Id", "required": true },
+ "studio": { "$ref": "Array.String" },
+ "year": { "type": "integer" },
+ "album": { "type": "string" },
+ "artist": { "$ref": "Array.String" },
+ "genre": { "$ref": "Array.String" },
+ "track": { "type": "integer" },
+ "tag": { "$ref": "Array.String" },
+ "rating": { "type": "number" },
+ "userrating": { "type": "integer" },
+ "premiered": { "type": "string" },
+ "uniqueid": { "$ref": "Media.UniqueID" }
+ }
+ },
+ "PVR.Property.Name": {
+ "type": "string",
+ "enum": [ "available", "recording", "scanning" ]
+ },
+ "PVR.Property.Value": {
+ "type": "object",
+ "properties": {
+ "available": { "type": "boolean" },
+ "recording": { "type": "boolean" },
+ "scanning": { "type": "boolean" }
+ }
+ },
+ "PVR.ChannelGroup.Id": {
+ "type": [
+ { "$ref": "Library.Id", "required": true },
+ { "type": "string", "enum": [ "alltv", "allradio" ], "required": true }
+ ]
+ },
+ "PVR.Fields.Broadcast": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "title", "plot", "plotoutline", "starttime",
+ "endtime", "runtime", "progress", "progresspercentage",
+ "genre", "episodename", "episodenum", "episodepart",
+ "firstaired", "hastimer", "isactive", "parentalrating",
+ "wasactive", "thumbnail", "rating", "originaltitle", "cast",
+ "director", "writer", "year", "imdbnumber", "hastimerrule",
+ "hasrecording", "recording", "isseries", "isplayable", "clientid",
+ "hasreminder", "seasonnum" ]
+ }
+ },
+ "PVR.Details.Broadcast": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "broadcastid": { "$ref": "Library.Id", "required": true },
+ "title": { "type": "string" },
+ "plot": { "type": "string" },
+ "plotoutline": { "type": "string" },
+ "starttime": { "type": "string" },
+ "endtime": { "type": "string" },
+ "runtime": { "type": "integer" },
+ "progress": { "type": "integer" },
+ "progresspercentage": { "type": "number" },
+ "genre": { "type": "string" },
+ "episodename": { "type": "string" },
+ "episodenum": { "type": "integer" },
+ "episodepart": { "type": "integer" },
+ "firstaired": { "type": "string" },
+ "hastimer": { "type": "boolean" },
+ "isactive": { "type": "boolean" },
+ "parentalrating": { "type": "integer" },
+ "wasactive": { "type": "boolean" },
+ "thumbnail": { "type": "string" },
+ "rating": { "type": "integer" },
+ "originaltitle": { "type": "string" },
+ "cast": { "type": "string" },
+ "director": { "type": "string" },
+ "writer": { "type": "string" },
+ "year": { "type": "integer" },
+ "imdbnumber": { "type": "integer" },
+ "hastimerrule": { "type": "boolean" },
+ "hasrecording": { "type": "boolean" },
+ "recording": { "type": "string" },
+ "isseries": { "type": "boolean" },
+ "isplayable": { "type": "boolean", "description": "Deprecated - Use GetBroadcastIsPlayable instead" },
+ "clientid": { "$ref": "Library.Id" },
+ "hasreminder": { "type": "boolean" },
+ "seasonnum": { "type": "integer" }
+ }
+ },
+ "PVR.Fields.Channel": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "thumbnail", "channeltype", "hidden", "locked", "channel", "lastplayed",
+ "broadcastnow", "broadcastnext", "uniqueid", "icon", "channelnumber",
+ "subchannelnumber", "isrecording", "hasarchive", "clientid" ]
+ }
+ },
+ "PVR.Details.Channel": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "channelid": { "$ref": "Library.Id", "required": true },
+ "channel": { "type": "string" },
+ "channeltype": { "$ref": "PVR.Channel.Type" },
+ "hidden": { "type": "boolean" },
+ "locked": { "type": "boolean" },
+ "thumbnail": { "type": "string" },
+ "lastplayed": { "type": "string" },
+ "broadcastnow": { "$ref": "PVR.Details.Broadcast" },
+ "broadcastnext": { "$ref": "PVR.Details.Broadcast" },
+ "uniqueid": { "type": "integer", "required": true },
+ "icon": { "type": "string" },
+ "channelnumber": { "type": "integer" },
+ "subchannelnumber": { "type": "integer" },
+ "isrecording": { "type": "boolean" },
+ "hasarchive": { "type": "boolean" },
+ "clientid": { "$ref": "Library.Id" }
+ }
+ },
+ "PVR.Details.ChannelGroup": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "channelgroupid": { "$ref": "Library.Id", "required": true },
+ "channeltype": { "$ref": "PVR.Channel.Type", "required": true }
+ }
+ },
+ "PVR.Details.ChannelGroup.Extended": {
+ "extends": "PVR.Details.ChannelGroup",
+ "properties": {
+ "limits": { "$ref": "List.LimitsReturned", "required": true },
+ "channels": { "type": "array",
+ "items": { "$ref": "PVR.Details.Channel" }
+ }
+ }
+ },
+ "PVR.Fields.Client": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "addonid", "supportstv", "supportsradio", "supportsepg",
+ "supportsrecordings", "supportstimers", "supportschannelgroups",
+ "supportschannelscan" ]
+ }
+ },
+ "PVR.Details.Client": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "clientid": { "$ref": "Library.Id", "required": true },
+ "addonid": { "type": "string" },
+ "supportstv": { "type": "boolean" },
+ "supportsradio": { "type": "boolean" },
+ "supportsepg": { "type": "boolean" },
+ "supportsrecordings": { "type": "boolean" },
+ "supportstimers": { "type": "boolean" },
+ "supportschannelgroups": { "type": "boolean" },
+ "supportschannelscan": { "type": "boolean" }
+ }
+ },
+ "PVR.TimerState": {
+ "type": "string",
+ "enum": [ "unknown", "new", "scheduled", "recording", "completed",
+ "aborted", "cancelled", "conflict_ok", "conflict_notok",
+ "error", "disabled" ]
+ },
+ "PVR.Fields.Timer": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "title", "summary", "channelid", "isradio", "istimerrule", "ismanual",
+ "starttime", "endtime", "runtime", "lifetime", "firstday",
+ "weekdays", "priority", "startmargin", "endmargin", "state",
+ "file", "directory", "preventduplicateepisodes", "startanytime",
+ "endanytime", "epgsearchstring", "fulltextepgsearch", "recordinggroup",
+ "maxrecordings", "epguid", "isreadonly", "isreminder", "clientid", "broadcastid" ]
+ }
+ },
+ "PVR.Details.Timer": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "timerid": { "$ref": "Library.Id", "required": true },
+ "title": { "type": "string" },
+ "summary": { "type": "string" },
+ "channelid": { "$ref": "Library.Id" },
+ "isradio": { "type": "boolean" },
+ "istimerrule": { "type": "boolean" },
+ "ismanual": { "type": "boolean" },
+ "starttime": { "type": "string" },
+ "endtime": { "type": "string" },
+ "runtime": { "type": "integer" },
+ "lifetime": { "type": "integer" },
+ "firstday": { "type": "string" },
+ "weekdays": { "type": "array",
+ "items": { "$ref": "Global.Weekday" },
+ "uniqueItems": true
+ },
+ "priority": { "type": "integer" },
+ "startmargin": { "type": "integer" },
+ "endmargin": { "type": "integer" },
+ "state": { "$ref": "PVR.TimerState" },
+ "file": { "type": "string" },
+ "directory": { "type": "string" },
+ "preventduplicateepisodes": { "type": "integer" },
+ "startanytime": { "type": "boolean" },
+ "endanytime": { "type": "boolean" },
+ "epgsearchstring": { "type": "string" },
+ "fulltextepgsearch": { "type": "boolean" },
+ "recordinggroup": { "type": "integer" },
+ "maxrecordings": { "type": "integer" },
+ "epguid": { "type": "integer" },
+ "isreadonly": { "type": "boolean" },
+ "isreminder": { "type": "boolean" },
+ "clientid": { "$ref": "Library.Id" },
+ "broadcastid": { "$ref": "Library.Id" }
+ }
+ },
+ "PVR.Fields.Recording": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "title", "plot", "plotoutline", "genre", "playcount",
+ "resume", "channel", "starttime","endtime", "runtime",
+ "lifetime", "icon", "art", "streamurl", "file",
+ "directory", "radio", "isdeleted", "epgeventid", "channeluid",
+ "season", "episode", "showtitle", "clientid" ]
+ }
+ },
+ "PVR.Details.Recording": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "recordingid": { "$ref": "Library.Id", "required": true },
+ "title": { "type": "string" },
+ "plot": { "type": "string" },
+ "plotoutline": { "type": "string" },
+ "genre": { "type": "string" },
+ "playcount": { "type": "integer" },
+ "resume": { "$ref": "Video.Resume" },
+ "channel": { "type": "string" },
+ "starttime": { "type": "string" },
+ "endtime": { "type": "string" },
+ "runtime": { "type": "integer" },
+ "lifetime": { "type": "integer" },
+ "icon": { "type": "string" },
+ "art": { "$ref": "Media.Artwork" },
+ "streamurl": { "type": "string" },
+ "file": { "type": "string" },
+ "directory": { "type": "string" },
+ "radio": { "type": "boolean" },
+ "isdeleted": { "type": "boolean" },
+ "epgeventid": { "type": "integer" },
+ "channeluid": { "type": "integer" },
+ "season": { "type": "integer" },
+ "episode": { "type": "integer" },
+ "showtitle": { "type": "string" },
+ "clientid": { "$ref": "Library.Id" }
+ }
+ },
+ "Textures.Details.Size": {
+ "type": "object",
+ "properties": {
+ "size": { "type": "integer", "description": "Size of the texture (1 == largest)" },
+ "width": { "type": "integer", "description": "Width of texture" },
+ "height": { "type": "integer", "description": "Height of texture" },
+ "usecount": { "type": "integer", "description": "Number of uses" },
+ "lastused": { "type": "string", "description": "Date of last use" }
+ }
+ },
+ "Textures.Fields.Texture": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "url", "cachedurl", "lasthashcheck", "imagehash", "sizes" ]
+ }
+ },
+ "Textures.Details.Texture": {
+ "type": "object",
+ "properties": {
+ "textureid": { "$ref": "Library.Id", "required": "true" },
+ "url": { "type": "string", "description": "Original source URL" },
+ "cachedurl": { "type": "string", "description": "Cached URL on disk" },
+ "lasthashcheck": { "type": "string", "description": "Last time source was checked for changes" },
+ "imagehash": { "type": "string", "description": "Hash of image" },
+ "sizes": { "type": "array", "items": { "$ref": "Textures.Details.Size" } }
+ }
+ },
+ "Profiles.Password": {
+ "type": "object",
+ "properties": {
+ "value": { "type": "string", "required": true, "description": "Password" },
+ "encryption": { "type": "string", "description": "Password Encryption", "default": "md5", "enum": [ "none", "md5" ] }
+ }
+ },
+ "Profiles.Fields.Profile": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string", "enum": [ "thumbnail", "lockmode" ] }
+ },
+ "Profiles.Details.Profile": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "thumbnail": { "type": "string" },
+ "lockmode": { "type": "integer" }
+ }
+ },
+ "List.Filter.Rule": {
+ "type": "object",
+ "properties": {
+ "operator": { "$ref": "List.Filter.Operators", "required": true },
+ "value": {
+ "type": [
+ { "type": "string", "required": true },
+ { "type": "array", "items": { "type": "string" }, "required": true }
+ ], "required": true
+ }
+ }
+ },
+ "List.Filter.Rule.Movies": {
+ "extends": "List.Filter.Rule",
+ "properties": {
+ "field": { "$ref": "List.Filter.Fields.Movies", "required": true }
+ }
+ },
+ "List.Filter.Rule.TVShows": {
+ "extends": "List.Filter.Rule",
+ "properties": {
+ "field": { "$ref": "List.Filter.Fields.TVShows", "required": true }
+ }
+ },
+ "List.Filter.Rule.Episodes": {
+ "extends": "List.Filter.Rule",
+ "properties": {
+ "field": { "$ref": "List.Filter.Fields.Episodes", "required": true }
+ }
+ },
+ "List.Filter.Rule.MusicVideos": {
+ "extends": "List.Filter.Rule",
+ "properties": {
+ "field": { "$ref": "List.Filter.Fields.MusicVideos", "required": true }
+ }
+ },
+ "List.Filter.Rule.Artists": {
+ "extends": "List.Filter.Rule",
+ "properties": {
+ "field": { "$ref": "List.Filter.Fields.Artists", "required": true }
+ }
+ },
+ "List.Filter.Rule.Albums": {
+ "extends": "List.Filter.Rule",
+ "properties": {
+ "field": { "$ref": "List.Filter.Fields.Albums", "required": true }
+ }
+ },
+ "List.Filter.Rule.Songs": {
+ "extends": "List.Filter.Rule",
+ "properties": {
+ "field": { "$ref": "List.Filter.Fields.Songs", "required": true }
+ }
+ },
+ "List.Filter.Rule.Textures": {
+ "extends": "List.Filter.Rule",
+ "properties": {
+ "field": { "$ref": "List.Filter.Fields.Textures", "required": true }
+ }
+ },
+ "List.Filter.Movies": {
+ "type": [
+ { "type": "object",
+ "properties": {
+ "and": { "type": "array",
+ "items": { "$ref": "List.Filter.Movies" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "type": "object",
+ "properties": {
+ "or": { "type": "array",
+ "items": { "$ref": "List.Filter.Movies" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "$ref": "List.Filter.Rule.Movies" }
+ ]
+ },
+ "List.Filter.TVShows": {
+ "type": [
+ { "type": "object",
+ "properties": {
+ "and": { "type": "array",
+ "items": { "$ref": "List.Filter.TVShows" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "type": "object",
+ "properties": {
+ "or": { "type": "array",
+ "items": { "$ref": "List.Filter.TVShows" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "$ref": "List.Filter.Rule.TVShows" }
+ ]
+ },
+ "List.Filter.Episodes": {
+ "type": [
+ { "type": "object",
+ "properties": {
+ "and": { "type": "array",
+ "items": { "$ref": "List.Filter.Episodes" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "type": "object",
+ "properties": {
+ "or": { "type": "array",
+ "items": { "$ref": "List.Filter.Episodes" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "$ref": "List.Filter.Rule.Episodes" }
+ ]
+ },
+ "List.Filter.MusicVideos": {
+ "type": [
+ { "type": "object",
+ "properties": {
+ "and": { "type": "array",
+ "items": { "$ref": "List.Filter.MusicVideos" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "type": "object",
+ "properties": {
+ "or": { "type": "array",
+ "items": { "$ref": "List.Filter.MusicVideos" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "$ref": "List.Filter.Rule.MusicVideos" }
+ ]
+ },
+ "List.Filter.Artists": {
+ "type": [
+ { "type": "object",
+ "properties": {
+ "and": { "type": "array",
+ "items": { "$ref": "List.Filter.Artists" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "type": "object",
+ "properties": {
+ "or": { "type": "array",
+ "items": { "$ref": "List.Filter.Artists" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "$ref": "List.Filter.Rule.Artists" }
+ ]
+ },
+ "List.Filter.Albums": {
+ "type": [
+ { "type": "object",
+ "properties": {
+ "and": { "type": "array",
+ "items": { "$ref": "List.Filter.Albums" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "type": "object",
+ "properties": {
+ "or": { "type": "array",
+ "items": { "$ref": "List.Filter.Albums" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "$ref": "List.Filter.Rule.Albums" }
+ ]
+ },
+ "List.Filter.Songs": {
+ "type": [
+ { "type": "object",
+ "properties": {
+ "and": { "type": "array",
+ "items": { "$ref": "List.Filter.Songs" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "type": "object",
+ "properties": {
+ "or": { "type": "array",
+ "items": { "$ref": "List.Filter.Songs" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "$ref": "List.Filter.Rule.Songs" }
+ ]
+ },
+ "List.Filter.Textures": {
+ "type": [
+ { "type": "object",
+ "properties": {
+ "and": { "type": "array",
+ "items": { "$ref": "List.Filter.Textures" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "type": "object",
+ "properties": {
+ "or": { "type": "array",
+ "items": { "$ref": "List.Filter.Textures" },
+ "minItems": 1, "required": true
+ }
+ }
+ },
+ { "$ref": "List.Filter.Rule.Textures" }
+ ]
+ },
+ "List.Item.Base": {
+ "extends": [ "Video.Details.File", "Audio.Details.Media" ],
+ "properties": {
+ "id": { "$ref": "Library.Id" },
+ "type": { "type": "string", "enum": [ "unknown", "movie", "episode", "musicvideo", "song", "picture", "channel", "recording" ] },
+ "albumartist": { "$ref": "Array.String" },
+ "album": { "type": "string" },
+ "track": { "type": "integer" },
+ "duration": { "type": "integer" },
+ "comment": { "type": "string" },
+ "lyrics": { "type": "string" },
+ "musicbrainztrackid": { "type": "string" },
+ "musicbrainzartistid": { "$ref": "Array.String" },
+ "trailer": { "type": "string" },
+ "tagline": { "type": "string" },
+ "plotoutline": { "type": "string" },
+ "originaltitle": { "type": "string" },
+ "writer": { "$ref": "Array.String" },
+ "studio": { "$ref": "Array.String" },
+ "mpaa": { "type": "string" },
+ "cast": { "$ref": "Video.Cast" },
+ "country": { "$ref": "Array.String" },
+ "imdbnumber": { "type": "string" },
+ "premiered": { "type": "string" },
+ "productioncode": { "type": "string" },
+ "set": { "type": "string" },
+ "showlink": { "$ref": "Array.String" },
+ "top250": { "type": "integer" },
+ "votes": { "type": "string" },
+ "firstaired": { "type": "string" },
+ "season": { "type": "integer" },
+ "episode": { "type": "integer" },
+ "showtitle": { "type": "string" },
+ "albumid": { "$ref": "Library.Id" },
+ "setid": { "$ref": "Library.Id" },
+ "tvshowid": { "$ref": "Library.Id" },
+ "watchedepisodes": { "type": "integer" },
+ "disc": { "type": "integer" },
+ "tag": { "$ref": "Array.String" },
+ "albumartistid": { "$ref": "Array.Integer" },
+ "uniqueid": { "$ref": "Media.UniqueID" },
+ "episodeguide": { "type": "string" },
+ "sorttitle": { "type": "string" },
+ "description": { "type": "string" },
+ "theme": { "$ref": "Array.String" },
+ "mood": { "$ref": "Array.String" },
+ "style": { "$ref": "Array.String" },
+ "albumlabel": { "type": "string" },
+ "specialsortseason": { "type": "integer" },
+ "specialsortepisode": { "type": "integer" },
+ "compilation": { "type": "boolean" },
+ "releasetype": { "$ref": "Audio.Album.ReleaseType" },
+ "albumreleasetype": { "$ref": "Audio.Album.ReleaseType" },
+ "contributors": { "$ref": "Audio.Contributors" },
+ "displaycomposer": { "type": "string"},
+ "displayconductor": { "type": "string"},
+ "displayorchestra": { "type": "string"},
+ "displaylyricist": { "type": "string"},
+ "mediapath": { "type": "string", "description": "Media source path that identifies the item"},
+ "dynpath": { "type": "string", "description": "An experimental property for debug purposes, often same as mediapath but when different gives the actual file playing that should also be in file property"},
+ "isboxset": { "type": "boolean" },
+ "totaldiscs": { "type": "integer" },
+ "disctitle": { "type": "string" },
+ "releasedate": { "type": "string" },
+ "originaldate": { "type": "string" },
+ "bpm": { "type": "integer" },
+ "bitrate": { "type": "integer" },
+ "samplerate": { "type": "integer" },
+ "channels": { "type": "integer"},
+ "albumstatus": { "type": "string" },
+ "customproperties": { "$ref": "Item.CustomProperties" }
+ }
+ },
+ "List.Fields.All": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "title", "artist", "albumartist", "genre", "year", "rating",
+ "album", "track", "duration", "comment", "lyrics", "musicbrainztrackid",
+ "musicbrainzartistid", "musicbrainzalbumid", "musicbrainzalbumartistid",
+ "playcount", "fanart", "director", "trailer", "tagline", "plot",
+ "plotoutline", "originaltitle", "lastplayed", "writer", "studio",
+ "mpaa", "cast", "country", "imdbnumber", "premiered", "productioncode",
+ "runtime", "set", "showlink", "streamdetails", "top250", "votes",
+ "firstaired", "season", "episode", "showtitle", "thumbnail", "file",
+ "resume", "artistid", "albumid", "tvshowid", "setid", "watchedepisodes",
+ "disc", "tag", "art", "genreid", "displayartist", "albumartistid",
+ "description", "theme", "mood", "style", "albumlabel", "sorttitle",
+ "episodeguide", "uniqueid", "dateadded", "channel", "channeltype", "hidden",
+ "locked", "channelnumber", "subchannelnumber", "starttime", "endtime",
+ "specialsortseason", "specialsortepisode", "compilation", "releasetype",
+ "albumreleasetype", "contributors", "displaycomposer", "displayconductor",
+ "displayorchestra", "displaylyricist", "userrating", "votes", "sortartist",
+ "musicbrainzreleasegroupid", "mediapath", "dynpath", "isboxset", "totaldiscs",
+ "disctitle", "releasedate", "originaldate", "bpm", "bitrate", "samplerate",
+ "channels", "albumstatus", "datemodified", "datenew", "customproperties",
+ "albumduration"]
+ }
+ },
+ "List.Item.All": {
+ "extends": "List.Item.Base",
+ "properties": {
+ "channel": { "type": "string" },
+ "channeltype": { "$ref": "PVR.Channel.Type" },
+ "hidden": { "type": "boolean" },
+ "locked": { "type": "boolean" },
+ "channelnumber": { "type": "integer" },
+ "subchannelnumber": { "type": "integer" },
+ "starttime": { "type": "string" },
+ "endtime": { "type": "string" }
+ }
+ },
+ "List.Fields.Files": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "title", "artist", "albumartist", "genre", "year", "rating",
+ "album", "track", "duration", "comment", "lyrics", "musicbrainztrackid",
+ "musicbrainzartistid", "musicbrainzalbumid", "musicbrainzalbumartistid",
+ "playcount", "fanart", "director", "trailer", "tagline", "plot",
+ "plotoutline", "originaltitle", "lastplayed", "writer", "studio",
+ "mpaa", "cast", "country", "imdbnumber", "premiered", "productioncode",
+ "runtime", "set", "showlink", "streamdetails", "top250", "votes",
+ "firstaired", "season", "episode", "showtitle", "thumbnail", "file",
+ "resume", "artistid", "albumid", "tvshowid", "setid", "watchedepisodes",
+ "disc", "tag", "art", "genreid", "displayartist", "albumartistid",
+ "description", "theme", "mood", "style", "albumlabel", "sorttitle",
+ "episodeguide", "uniqueid", "dateadded", "size", "lastmodified", "mimetype",
+ "specialsortseason", "specialsortepisode", "sortartist", "musicbrainzreleasegroupid",
+ "isboxset", "totaldiscs", "disctitle", "releasedate", "originaldate", "bpm",
+ "bitrate", "samplerate", "channels", "datemodified", "datenew", "customproperties",
+ "albumduration", "userrating"]
+ }
+ },
+ "List.Item.File": {
+ "extends": "List.Item.Base",
+ "properties": {
+ "file": { "type": "string", "required": true },
+ "filetype": { "type": "string", "enum": [ "file", "directory" ], "required": true },
+ "size": { "type": "integer", "description": "Size of the file in bytes" },
+ "lastmodified": { "type": "string" },
+ "mimetype": { "type": "string" }
+ }
+ },
+ "List.Items.Sources": {
+ "type": "array",
+ "items": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "file": { "type": "string", "required": true }
+ }
+ }
+ },
+ "Addon.Content": {
+ "type": "string",
+ "enum": [ "unknown", "video", "audio", "image", "executable" ],
+ "default": "unknown"
+ },
+ "Addon.Fields": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "name", "version", "summary", "description", "path", "author", "thumbnail", "disclaimer", "fanart",
+ "dependencies", "broken", "extrainfo", "rating", "enabled", "installed", "deprecated" ]
+ }
+ },
+ "Addon.Details": {
+ "extends": "Item.Details.Base",
+ "properties": {
+ "addonid": { "type": "string", "required": true },
+ "type": { "$ref": "Addon.Types", "required": true },
+ "name": { "type": "string" },
+ "version": { "type": "string" },
+ "summary": { "type": "string" },
+ "description": { "type": "string" },
+ "path": { "type": "string" },
+ "author": { "type": "string" },
+ "thumbnail": { "type": "string" },
+ "disclaimer": { "type": "string" },
+ "fanart": { "type": "string" },
+ "dependencies": { "type": "array",
+ "items": { "type": "object",
+ "properties": {
+ "addonid": { "type": "string", "required": true },
+ "version": { "type": "string", "required": true },
+ "optional": { "type": "boolean", "required": true }
+ }
+ }
+ },
+ "broken": { "type": [ "boolean", "string" ] },
+ "extrainfo": { "type": "array",
+ "items": { "type": "object",
+ "properties": {
+ "key": { "type": "string", "required": true },
+ "value": { "type": "string", "required": true }
+ }
+ }
+ },
+ "rating": { "type": "integer" },
+ "enabled": { "type": "boolean" },
+ "installed": { "type": "boolean" },
+ "deprecated": { "type": [ "boolean", "string" ] }
+ }
+ },
+ "GUI.Stereoscopy.Mode": {
+ "type": "object",
+ "properties": {
+ "mode": { "type": "string", "required": true, "enum": [ "off", "split_vertical", "split_horizontal", "row_interleaved", "hardware_based", "anaglyph_cyan_red", "anaglyph_green_magenta", "anaglyph_yellow_blue", "monoscopic" ] },
+ "label": { "type": "string", "required": true }
+ }
+ },
+ "GUI.Property.Name": {
+ "type": "string",
+ "enum": [ "currentwindow", "currentcontrol", "skin", "fullscreen", "stereoscopicmode" ]
+ },
+ "GUI.Property.Value": {
+ "type": "object",
+ "properties": {
+ "currentwindow": { "type": "object",
+ "properties": {
+ "id": { "type": "integer", "required": true },
+ "label": { "type": "string", "required": true }
+ }
+ },
+ "currentcontrol": { "type": "object",
+ "properties": {
+ "label": { "type": "string", "required": true }
+ }
+ },
+ "skin": { "type": "object",
+ "properties": {
+ "id": { "type": "string", "required": true, "minLength": 1 },
+ "name": { "type": "string" }
+ }
+ },
+ "fullscreen": { "type": "boolean" },
+ "stereoscopicmode": { "$ref": "GUI.Stereoscopy.Mode" }
+ }
+ },
+ "System.Property.Name": {
+ "type": "string",
+ "enum": [ "canshutdown", "cansuspend", "canhibernate", "canreboot" ]
+ },
+ "System.Property.Value": {
+ "type": "object",
+ "properties": {
+ "canshutdown": { "type": "boolean" },
+ "cansuspend": { "type": "boolean" },
+ "canhibernate": { "type": "boolean" },
+ "canreboot": { "type": "boolean" }
+ }
+ },
+ "Application.Property.Name": {
+ "type": "string",
+ "enum": [ "volume", "muted", "name", "version", "volume", "sorttokens", "language" ]
+ },
+ "Application.Property.Value": {
+ "type": "object",
+ "properties": {
+ "volume": { "type": "integer", "minimum": 0, "maximum": 100 },
+ "muted": { "type": "boolean" },
+ "name": { "type": "string", "minLength": 1 },
+ "version": { "type": "object",
+ "properties": {
+ "major": { "type": "integer", "minimum": 0, "required": true },
+ "minor": { "type": "integer", "minimum": 0, "required": true },
+ "revision": { "type": [ "string", "integer" ] },
+ "tag": { "type": "string", "enum": [ "prealpha", "alpha", "beta", "releasecandidate", "stable" ], "required": true },
+ "tagversion": { "type": "string" }
+ }
+ },
+ "sorttokens": { "$ref": "Array.String", "description": "Articles ignored during sorting when ignorearticle is enabled." },
+ "language": { "type": "string", "minLength": 1, "description": "Current language code and region e.g. en_GB" }
+ }
+ },
+ "Favourite.Fields.Favourite": {
+ "extends": "Item.Fields.Base",
+ "items": { "type": "string",
+ "enum": [ "window", "windowparameter", "thumbnail", "path" ]
+ }
+ },
+ "Favourite.Type": {
+ "type": "string",
+ "enum": [ "media", "window", "script", "androidapp", "unknown" ]
+ },
+ "Favourite.Details.Favourite": {
+ "type": "object",
+ "properties": {
+ "title": { "type": "string", "required": true },
+ "type": { "$ref": "Favourite.Type", "required": true },
+ "path": { "type": "string" },
+ "window": { "type": "string" },
+ "windowparameter": { "type": "string" },
+ "thumbnail": { "type": "string" }
+ },
+ "additionalProperties": false
+ },
+ "Setting.Type": {
+ "type": "string",
+ "enum": [
+ "boolean", "integer", "number", "string", "action", "list",
+ "path", "addon", "date", "time"
+ ]
+ },
+ "Setting.Level": {
+ "type": "string",
+ "enum": [ "basic", "standard", "advanced", "expert" ]
+ },
+ "Setting.Value": {
+ "type": [
+ { "type": "boolean", "required": true },
+ { "type": "integer", "required": true },
+ { "type": "number", "required": true },
+ { "type": "string", "required": true }
+ ]
+ },
+ "Setting.Value.List": {
+ "type": "array",
+ "items": { "$ref": "Setting.Value" }
+ },
+ "Setting.Value.Extended": {
+ "type": [
+ { "type": "boolean", "required": true },
+ { "type": "integer", "required": true },
+ { "type": "number", "required": true },
+ { "type": "string", "required": true },
+ { "$ref": "Setting.Value.List", "required": true }
+ ]
+ },
+ "Setting.Details.ControlBase": {
+ "type": "object",
+ "properties": {
+ "type": { "type": "string", "required": true },
+ "format": { "type": "string", "required": true },
+ "delayed": { "type": "boolean", "required": true }
+ }
+ },
+ "Setting.Details.ControlCheckmark": {
+ "extends": "Setting.Details.ControlBase",
+ "properties": {
+ "type": { "type": "string", "required": true, "enum": [ "toggle" ] },
+ "format": { "type": "string", "required": true, "enum": [ "boolean" ] }
+ }
+ },
+ "Setting.Details.ControlSpinner": {
+ "extends": "Setting.Details.ControlBase",
+ "properties": {
+ "type": { "type": "string", "required": true, "enum": [ "spinner" ] },
+ "formatlabel": { "type": "string" },
+ "minimumlabel": { "type": "string" }
+ }
+ },
+ "Setting.Details.ControlHeading": {
+ "extends": "Setting.Details.ControlBase",
+ "properties": {
+ "heading": { "type": "string" }
+ }
+ },
+ "Setting.Details.ControlEdit": {
+ "extends": "Setting.Details.ControlHeading",
+ "properties": {
+ "type": { "type": "string", "required": true, "enum": [ "edit" ] },
+ "hidden": { "type": "boolean", "required": true },
+ "verifynewvalue": { "type": "boolean", "required": true }
+ }
+ },
+ "Setting.Details.ControlButton": {
+ "extends": "Setting.Details.ControlHeading",
+ "properties": {
+ "type": { "type": "string", "required": true, "enum": [ "button" ] }
+ }
+ },
+ "Setting.Details.ControlList": {
+ "extends": "Setting.Details.ControlHeading",
+ "properties": {
+ "type": { "type": "string", "required": true, "enum": [ "list" ] },
+ "multiselect": { "type": "boolean", "required": true }
+ }
+ },
+ "Setting.Details.ControlSlider": {
+ "extends": "Setting.Details.ControlHeading",
+ "properties": {
+ "type": { "type": "string", "required": true, "enum": [ "slider" ] },
+ "formatlabel": { "type": "string", "required": true },
+ "popup": { "type": "boolean", "required": true }
+ }
+ },
+ "Setting.Details.ControlRange": {
+ "extends": "Setting.Details.ControlBase",
+ "properties": {
+ "type": { "type": "string", "required": true, "enum": [ "range" ] },
+ "formatlabel": { "type": "string", "required": true },
+ "formatvalue": { "type": "string", "required": true }
+ }
+ },
+ "Setting.Details.ControlLabel": {
+ "extends": "Setting.Details.ControlBase",
+ "properties": {
+ "type": { "type": "string", "required": true, "enum": [ "label" ] },
+ "format": { "type": "string", "required": true, "enum": [ "string" ] }
+ }
+ },
+ "Setting.Details.Control": {
+ "type": [
+ { "$ref": "Setting.Details.ControlCheckmark", "required": true },
+ { "$ref": "Setting.Details.ControlSpinner", "required": true },
+ { "$ref": "Setting.Details.ControlEdit", "required": true },
+ { "$ref": "Setting.Details.ControlButton", "required": true },
+ { "$ref": "Setting.Details.ControlList", "required": true },
+ { "$ref": "Setting.Details.ControlSlider", "required": true },
+ { "$ref": "Setting.Details.ControlRange", "required": true },
+ { "$ref": "Setting.Details.ControlLabel", "required": true }
+ ]
+ },
+ "Setting.Details.Base": {
+ "type": "object",
+ "properties": {
+ "id": { "type": "string", "required": true, "minLength": 1 },
+ "label": { "type": "string", "required": true },
+ "help": { "type": "string" }
+ }
+ },
+ "Setting.Details.SettingBase": {
+ "extends": "Setting.Details.Base",
+ "properties": {
+ "type": { "$ref": "Setting.Type", "required": true },
+ "enabled": { "type": "boolean", "required": true },
+ "level": { "$ref": "Setting.Level", "required": true },
+ "parent": { "type": "string" },
+ "control": { "$ref": "Setting.Details.Control" }
+ },
+ "additionalProperties": false
+ },
+ "Setting.Details.SettingBool": {
+ "extends": "Setting.Details.SettingBase",
+ "properties": {
+ "value": { "type": "boolean", "required": true },
+ "default": { "type": "boolean", "required": true }
+ },
+ "additionalProperties": false
+ },
+ "Setting.Details.SettingInt": {
+ "extends": "Setting.Details.SettingBase",
+ "properties": {
+ "value": { "type": "integer", "required": true },
+ "default": { "type": "integer", "required": true },
+ "minimum": { "type": "integer" },
+ "step": { "type": "integer" },
+ "maximum": { "type": "integer" },
+ "options": { "type": "array",
+ "items": { "type": "object",
+ "properties": {
+ "label": { "type": "string", "required": true },
+ "value": { "type": "integer", "required": true }
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "Setting.Details.SettingNumber": {
+ "extends": "Setting.Details.SettingBase",
+ "properties": {
+ "value": { "type": "number", "required": true },
+ "default": { "type": "number", "required": true },
+ "minimum": { "type": "number", "required": true },
+ "step": { "type": "number", "required": true },
+ "maximum": { "type": "number", "required": true }
+ },
+ "additionalProperties": false
+ },
+ "Setting.Details.SettingString": {
+ "extends": "Setting.Details.SettingBase",
+ "properties": {
+ "value": { "type": "string", "required": true },
+ "default": { "type": "string", "required": true },
+ "allowempty": { "type": "boolean", "required": true },
+ "options": { "type": "array",
+ "items": { "type": "object",
+ "properties": {
+ "label": { "type": "string", "required": true },
+ "value": { "type": "string", "required": true }
+ }
+ }
+ }
+ }
+ },
+ "Setting.Details.SettingAction": {
+ "extends": "Setting.Details.SettingBase",
+ "properties": {
+ "data": { "type": "string", "required": true }
+ },
+ "additionalProperties": false
+ },
+ "Setting.Details.SettingList": {
+ "extends": "Setting.Details.SettingBase",
+ "properties": {
+ "value": { "$ref": "Setting.Value.List", "required": true },
+ "default": { "$ref": "Setting.Value.List", "required": true },
+ "elementtype": { "$ref": "Setting.Type", "required": true },
+ "definition": { "$ref": "Setting.Details.Setting", "required": true },
+ "delimiter": { "type": "string", "required": true },
+ "minimumItems": { "type": "integer" },
+ "maximumItems": { "type": "integer" }
+ },
+ "additionalProperties": false
+ },
+ "Setting.Details.SettingPath": {
+ "extends": "Setting.Details.SettingString",
+ "properties": {
+ "writable": { "type": "boolean", "required": true },
+ "sources": { "type": "array", "items": { "type": "string" } }
+ },
+ "additionalProperties": false
+ },
+ "Setting.Details.SettingAddon": {
+ "extends": "Setting.Details.SettingString",
+ "properties": {
+ "addontype": { "$ref": "Addon.Types", "required": true }
+ },
+ "additionalProperties": false
+ },
+ "Setting.Details.SettingDate": {
+ "extends": "Setting.Details.SettingString",
+ "additionalProperties": false
+ },
+ "Setting.Details.SettingTime": {
+ "extends": "Setting.Details.SettingString",
+ "additionalProperties": false
+ },
+ "Setting.Details.Setting": {
+ "type": [
+ { "$ref": "Setting.Details.SettingBool", "required": true },
+ { "$ref": "Setting.Details.SettingInt", "required": true },
+ { "$ref": "Setting.Details.SettingNumber", "required": true },
+ { "$ref": "Setting.Details.SettingString", "required": true },
+ { "$ref": "Setting.Details.SettingAction", "required": true },
+ { "$ref": "Setting.Details.SettingList", "required": true },
+ { "$ref": "Setting.Details.SettingPath", "required": true },
+ { "$ref": "Setting.Details.SettingAddon", "required": true },
+ { "$ref": "Setting.Details.SettingDate", "required": true },
+ { "$ref": "Setting.Details.SettingTime", "required": true }
+ ]
+ },
+ "Setting.Details.Group": {
+ "type": "object",
+ "properties": {
+ "id": { "type": "string", "required": true, "minLength": 1 },
+ "settings": {
+ "type": "array",
+ "items": { "$ref": "Setting.Details.Setting" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ },
+ "additionalProperties": false
+ },
+ "Setting.Details.Category": {
+ "extends": "Setting.Details.Base",
+ "properties": {
+ "groups": {
+ "type": "array",
+ "items": { "$ref": "Setting.Details.Group" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ },
+ "additionalProperties": false
+ },
+ "Setting.Details.Section": {
+ "extends": "Setting.Details.Base",
+ "properties": {
+ "categories": {
+ "type": "array",
+ "items": { "$ref": "Setting.Details.Category" },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ },
+ "additionalProperties": false
+ }
+}
diff --git a/xbmc/interfaces/json-rpc/schema/version.txt b/xbmc/interfaces/json-rpc/schema/version.txt
new file mode 100644
index 0000000..40bfd87
--- /dev/null
+++ b/xbmc/interfaces/json-rpc/schema/version.txt
@@ -0,0 +1 @@
+JSONRPC_VERSION 13.0.0
diff --git a/xbmc/interfaces/legacy/Addon.cpp b/xbmc/interfaces/legacy/Addon.cpp
new file mode 100644
index 0000000..4b4db1e
--- /dev/null
+++ b/xbmc/interfaces/legacy/Addon.cpp
@@ -0,0 +1,243 @@
+/*
+ * 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 "Addon.h"
+
+#include "GUIUserMessages.h"
+#include "LanguageHook.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "addons/settings/AddonSettings.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+
+using namespace ADDON;
+
+namespace XBMCAddon
+{
+ namespace xbmcaddon
+ {
+ String Addon::getDefaultId() { return languageHook == NULL ? emptyString : languageHook->GetAddonId(); }
+
+ String Addon::getAddonVersion() { return languageHook == NULL ? emptyString : languageHook->GetAddonVersion(); }
+
+ bool Addon::UpdateSettingInActiveDialog(const char* id, const String& value)
+ {
+ ADDON::AddonPtr addon(pAddon);
+ if (!CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_ADDON_SETTINGS))
+ return false;
+
+ CGUIDialogAddonSettings* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogAddonSettings>(WINDOW_DIALOG_ADDON_SETTINGS);
+ if (dialog->GetCurrentAddonID() != addon->ID())
+ return false;
+
+ CGUIMessage message(GUI_MSG_SETTING_UPDATED, 0, 0);
+ std::vector<std::string> params;
+ params.emplace_back(id);
+ params.push_back(value);
+ message.SetStringParams(params);
+ message.SetParam1(ADDON_SETTINGS_ID);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message, WINDOW_DIALOG_ADDON_SETTINGS);
+
+ return true;
+ }
+
+ Addon::Addon(const char* cid)
+ {
+ String id(cid ? cid : emptyString);
+
+ // if the id wasn't passed then get the id from
+ // the global dictionary
+ if (id.empty())
+ id = getDefaultId();
+
+ // if we still don't have an id then bail
+ if (id.empty())
+ throw AddonException("No valid addon id could be obtained. None was passed and the script "
+ "wasn't executed in a normal Kodi manner.");
+
+ if (!CServiceBroker::GetAddonMgr().GetAddon(id, pAddon, OnlyEnabled::CHOICE_YES))
+ throw AddonException("Unknown addon id '%s'.", id.c_str());
+
+ CServiceBroker::GetAddonMgr().AddToUpdateableAddons(pAddon);
+ }
+
+ Addon::~Addon()
+ {
+ CServiceBroker::GetAddonMgr().RemoveFromUpdateableAddons(pAddon);
+ }
+
+ String Addon::getLocalizedString(int id)
+ {
+ return g_localizeStrings.GetAddonString(pAddon->ID(), id);
+ }
+
+ Settings* Addon::getSettings()
+ {
+ return new Settings(pAddon->GetSettings());
+ }
+
+ String Addon::getSetting(const char* id)
+ {
+ return pAddon->GetSetting(id);
+ }
+
+ bool Addon::getSettingBool(const char* id)
+ {
+ bool value = false;
+ if (!pAddon->GetSettingBool(id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type");
+
+ return value;
+ }
+
+ int Addon::getSettingInt(const char* id)
+ {
+ int value = 0;
+ if (!pAddon->GetSettingInt(id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type");
+
+ return value;
+ }
+
+ double Addon::getSettingNumber(const char* id)
+ {
+ double value = 0.0;
+ if (!pAddon->GetSettingNumber(id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type");
+
+ return value;
+ }
+
+ String Addon::getSettingString(const char* id)
+ {
+ std::string value;
+ if (!pAddon->GetSettingString(id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type");
+
+ return value;
+ }
+
+ void Addon::setSetting(const char* id, const String& value)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ ADDON::AddonPtr addon(pAddon);
+ if (!UpdateSettingInActiveDialog(id, value))
+ {
+ addon->UpdateSetting(id, value);
+ addon->SaveSettings();
+ }
+ }
+
+ bool Addon::setSettingBool(const char* id, bool value)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ ADDON::AddonPtr addon(pAddon);
+ if (UpdateSettingInActiveDialog(id, value ? "true" : "false"))
+ return true;
+
+ if (!addon->UpdateSettingBool(id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type");
+
+ addon->SaveSettings();
+
+ return true;
+ }
+
+ bool Addon::setSettingInt(const char* id, int value)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ ADDON::AddonPtr addon(pAddon);
+ if (UpdateSettingInActiveDialog(id, std::to_string(value)))
+ return true;
+
+ if (!addon->UpdateSettingInt(id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type");
+
+ addon->SaveSettings();
+
+ return true;
+ }
+
+ bool Addon::setSettingNumber(const char* id, double value)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ ADDON::AddonPtr addon(pAddon);
+ if (UpdateSettingInActiveDialog(id, StringUtils::Format("{:f}", value)))
+ return true;
+
+ if (!addon->UpdateSettingNumber(id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type");
+
+ addon->SaveSettings();
+
+ return true;
+ }
+
+ bool Addon::setSettingString(const char* id, const String& value)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ ADDON::AddonPtr addon(pAddon);
+ if (UpdateSettingInActiveDialog(id, value))
+ return true;
+
+ if (!addon->UpdateSettingString(id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type");
+
+ addon->SaveSettings();
+
+ return true;
+ }
+
+ void Addon::openSettings()
+ {
+ DelayedCallGuard dcguard(languageHook);
+ // show settings dialog
+ ADDON::AddonPtr addon(pAddon);
+ CGUIDialogAddonSettings::ShowForAddon(addon);
+ }
+
+ String Addon::getAddonInfo(const char* id)
+ {
+ if (StringUtils::CompareNoCase(id, "author") == 0)
+ return pAddon->Author();
+ else if (StringUtils::CompareNoCase(id, "changelog") == 0)
+ return pAddon->ChangeLog();
+ else if (StringUtils::CompareNoCase(id, "description") == 0)
+ return pAddon->Description();
+ else if (StringUtils::CompareNoCase(id, "disclaimer") == 0)
+ return pAddon->Disclaimer();
+ else if (StringUtils::CompareNoCase(id, "fanart") == 0)
+ return pAddon->FanArt();
+ else if (StringUtils::CompareNoCase(id, "icon") == 0)
+ return pAddon->Icon();
+ else if (StringUtils::CompareNoCase(id, "id") == 0)
+ return pAddon->ID();
+ else if (StringUtils::CompareNoCase(id, "name") == 0)
+ return pAddon->Name();
+ else if (StringUtils::CompareNoCase(id, "path") == 0)
+ return pAddon->Path();
+ else if (StringUtils::CompareNoCase(id, "profile") == 0)
+ return pAddon->Profile();
+ else if (StringUtils::CompareNoCase(id, "stars") == 0)
+ return StringUtils::Format("-1");
+ else if (StringUtils::CompareNoCase(id, "summary") == 0)
+ return pAddon->Summary();
+ else if (StringUtils::CompareNoCase(id, "type") == 0)
+ return ADDON::CAddonInfo::TranslateType(pAddon->Type());
+ else if (StringUtils::CompareNoCase(id, "version") == 0)
+ return pAddon->Version().asString();
+ else
+ throw AddonException("'%s' is an invalid Id", id);
+ }
+ }
+}
diff --git a/xbmc/interfaces/legacy/Addon.h b/xbmc/interfaces/legacy/Addon.h
new file mode 100644
index 0000000..97c598f
--- /dev/null
+++ b/xbmc/interfaces/legacy/Addon.h
@@ -0,0 +1,483 @@
+/*
+ * 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 "AddonClass.h"
+#include "AddonString.h"
+#include "Exception.h"
+#include "Settings.h"
+#include "addons/IAddon.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcaddon
+ {
+ XBMCCOMMONS_STANDARD_EXCEPTION(AddonException);
+
+ ///
+ /// \addtogroup python_xbmcaddon
+ /// @{
+ /// @brief **Kodi's addon class.**
+ ///
+ /// Offers classes and functions that manipulate the add-on settings,
+ /// information and localization.
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// \python_class{ xbmcaddon.Addon([id]) }
+ ///
+ /// Creates a new AddOn class.
+ ///
+ /// @param id [opt] string - id of the addon as
+ /// specified in [addon.xml](http://kodi.wiki/view/Addon.xml)
+ ///
+ /// @note Specifying the addon id is not needed.\n
+ /// Important however is that the addon folder has the same name as the AddOn
+ /// id provided in [addon.xml](http://kodi.wiki/view/Addon.xml).\n
+ /// You can optionally specify the addon id from another installed addon to
+ /// retrieve settings from it.
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ /// @python_v13
+ /// **id** is optional as it will be auto detected for this add-on instance.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.Addon = xbmcaddon.Addon()
+ /// self.Addon = xbmcaddon.Addon('script.foo.bar')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ class Addon : public AddonClass
+ {
+ ADDON::AddonPtr pAddon;
+
+ String getDefaultId();
+
+ String getAddonVersion();
+
+ bool UpdateSettingInActiveDialog(const char* id, const String& value);
+
+ public:
+ explicit Addon(const char* id = NULL);
+ ~Addon() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).getLocalizedString(id) }
+ /// Returns an addon's localized 'string'.
+ ///
+ /// @param id integer - id# for string you want to
+ /// localize.
+ /// @return Localized 'string'
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v13
+ /// **id** is optional as it will be auto detected for this add-on instance.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// locstr = self.Addon.getLocalizedString(32000)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getLocalizedString(...);
+#else
+ String getLocalizedString(int id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).getSettings() }
+ /// Returns a wrapper around the addon's settings.
+ ///
+ /// @return @ref python_settings wrapper
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// settings = self.Addon.getSettings()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getSettings(...);
+#else
+ Settings* getSettings();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).getSetting(id) }
+ /// Returns the value of a setting as string.
+ ///
+ /// @param id string - id of the setting that the module
+ /// needs to access.
+ /// @return Setting as a string
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v13
+ /// **id** is optional as it will be auto detected for this add-on instance.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// apikey = self.Addon.getSetting('apikey')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getSetting(...);
+#else
+ String getSetting(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).getSettingBool(id) }
+ /// Returns the value of a setting as a boolean.
+ ///
+ /// @param id string - id of the setting that the module
+ /// needs to access.
+ /// @return Setting as a boolean
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18
+ /// New function added.
+ /// @python_v20 Deprecated. Use **Settings.getBool()** instead.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// enabled = self.Addon.getSettingBool('enabled')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getSettingBool(...);
+#else
+ bool getSettingBool(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).getSettingInt(id) }
+ /// Returns the value of a setting as an integer.
+ ///
+ /// @param id string - id of the setting that the module
+ /// needs to access.
+ /// @return Setting as an integer
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18
+ /// New function added.
+ /// @python_v20 Deprecated. Use **Settings.getInt()** instead.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// max = self.Addon.getSettingInt('max')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getSettingInt(...);
+#else
+ int getSettingInt(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).getSettingNumber(id) }
+ /// Returns the value of a setting as a floating point number.
+ ///
+ /// @param id string - id of the setting that the module
+ /// needs to access.
+ /// @return Setting as a floating point number
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18
+ /// New function added.
+ /// @python_v20 Deprecated. Use **Settings.getNumber()** instead.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// max = self.Addon.getSettingNumber('max')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getSettingNumber(...);
+#else
+ double getSettingNumber(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).getSettingString(id) }
+ /// Returns the value of a setting as a string.
+ ///
+ /// @param id string - id of the setting that the module
+ /// needs to access.
+ /// @return Setting as a string
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18
+ /// New function added.
+ /// @python_v20 Deprecated. Use **Settings.getString()** instead.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// apikey = self.Addon.getSettingString('apikey')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getSettingString(...);
+#else
+ String getSettingString(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).setSetting(id, value) }
+ /// Sets a script setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param value string - value of the setting.
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v13
+ /// **id** is optional as it will be auto detected for this add-on instance.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.Addon.setSetting(id='username', value='teamkodi')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setSetting(...);
+#else
+ void setSetting(const char* id, const String& value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).setSettingBool(id, value) }
+ /// Sets a script setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param value boolean - value of the setting.
+ /// @return True if the value of the setting was set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18
+ /// New function added.
+ /// @python_v20 Deprecated. Use **Settings.setBool()** instead.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.Addon.setSettingBool(id='enabled', value=True)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setSettingBool(...);
+#else
+ bool setSettingBool(const char* id, bool value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).setSettingInt(id, value) }
+ /// Sets a script setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param value integer - value of the setting.
+ /// @return True if the value of the setting was set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18
+ /// New function added.
+ /// @python_v20 Deprecated. Use **Settings.setInt()** instead.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.Addon.setSettingInt(id='max', value=5)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setSettingInt(...);
+#else
+ bool setSettingInt(const char* id, int value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).setSettingNumber(id, value) }
+ /// Sets a script setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param value float - value of the setting.
+ /// @return True if the value of the setting was set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18
+ /// New function added.
+ /// @python_v20 Deprecated. Use **Settings.setNumber()** instead.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.Addon.setSettingNumber(id='max', value=5.5)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setSettingNumber(...);
+#else
+ bool setSettingNumber(const char* id, double value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).setSettingString(id, value) }
+ /// Sets a script setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param value string or unicode - value of the setting.
+ /// @return True if the value of the setting was set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18
+ /// New function added.
+ /// @python_v20 Deprecated. Use **Settings.setString()** instead.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.Addon.setSettingString(id='username', value='teamkodi')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setSettingString(...);
+#else
+ bool setSettingString(const char* id, const String& value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).openSettings() }
+ /// Opens this scripts settings dialog.
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.Addon.openSettings()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ openSettings();
+#else
+ void openSettings();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcaddon
+ /// \anchor python_xbmcaddon_Addon
+ /// @brief \python_func{ xbmcaddon.Addon([id]).getAddonInfo(id) }
+ /// Returns the value of an addon property as a string.
+ ///
+ /// @param id string - id of the property that the
+ /// module needs to access.
+ /// @par Choices for the property are
+ /// | | | | |
+ /// |:-----------:|:-----------:|:-----------:|:-----------:|
+ /// | author | changelog | description | disclaimer |
+ /// | fanart | icon | id | name |
+ /// | path | profile | stars | summary |
+ /// | type | version | | |
+ /// @return AddOn property as a string
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// version = self.Addon.getAddonInfo('version')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getAddonInfo(...);
+#else
+ String getAddonInfo(const char* id);
+#endif
+ };
+ //@}
+ }
+}
diff --git a/xbmc/interfaces/legacy/AddonCallback.cpp b/xbmc/interfaces/legacy/AddonCallback.cpp
new file mode 100644
index 0000000..c52ce80
--- /dev/null
+++ b/xbmc/interfaces/legacy/AddonCallback.cpp
@@ -0,0 +1,26 @@
+/*
+ * 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 "AddonCallback.h"
+
+namespace XBMCAddon
+{
+ // need a place to put the vtab
+ AddonCallback::~AddonCallback() = default;
+
+ void AddonCallback::invokeCallback(Callback* callback)
+ {
+ if (callback)
+ {
+ if (hasHandler())
+ handler->invokeCallback(callback);
+ else
+ callback->executeCallback();
+ }
+ }
+}
+
diff --git a/xbmc/interfaces/legacy/AddonCallback.h b/xbmc/interfaces/legacy/AddonCallback.h
new file mode 100644
index 0000000..dee5074
--- /dev/null
+++ b/xbmc/interfaces/legacy/AddonCallback.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "AddonClass.h"
+#include "CallbackFunction.h"
+#include "CallbackHandler.h"
+#include "LanguageHook.h"
+
+namespace XBMCAddon
+{
+
+ /**
+ * This class is the superclass for all API classes that are expected
+ * to be able to handle cross-language polymorphism.
+ */
+ class AddonCallback : public AddonClass
+ {
+ protected:
+ AddonClass::Ref<CallbackHandler> handler;
+
+ bool hasHandler() { return handler.isNotNull(); }
+
+ inline AddonCallback() : handler(NULL)
+ {
+ // if there is a LanguageHook, it should be set already.
+ if (languageHook != NULL)
+ setHandler(languageHook->GetCallbackHandler());
+ }
+ public:
+
+ ~AddonCallback() override;
+
+ inline void setHandler(CallbackHandler* _handler) { handler = _handler; }
+ void invokeCallback(Callback* callback);
+ };
+}
diff --git a/xbmc/interfaces/legacy/AddonClass.cpp b/xbmc/interfaces/legacy/AddonClass.cpp
new file mode 100644
index 0000000..ffdf3d4
--- /dev/null
+++ b/xbmc/interfaces/legacy/AddonClass.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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 "AddonClass.h"
+#ifdef XBMC_ADDON_DEBUG_MEMORY
+#include "utils/log.h"
+#endif
+#include "LanguageHook.h"
+#include "AddonUtils.h"
+
+using namespace XBMCAddonUtils;
+
+namespace XBMCAddon
+{
+ // need a place to put the vtab
+ AddonClass::~AddonClass()
+ {
+ m_isDeallocating= true;
+
+ if (languageHook != NULL)
+ languageHook->Release();
+
+#ifdef XBMC_ADDON_DEBUG_MEMORY
+ isDeleted = false;
+#endif
+ }
+
+ AddonClass::AddonClass() : refs(0L),
+ languageHook(NULL)
+ {
+#ifdef XBMC_ADDON_DEBUG_MEMORY
+ isDeleted = false;
+#endif
+
+ // check to see if we have a language hook that was prepared for this instantiation
+ languageHook = LanguageHook::GetLanguageHook();
+ if (languageHook != NULL)
+ {
+ languageHook->Acquire();
+
+ // here we assume the language hook was set for the single instantiation of
+ // this AddonClass (actually - its subclass - but whatever). So we
+ // will now reset the Tls. This avoids issues if the constructor of the
+ // subclass throws an exception.
+ LanguageHook::ClearLanguageHook();
+ }
+ }
+
+#ifdef XBMC_ADDON_DEBUG_MEMORY
+ void AddonClass::Release() const
+ {
+ if (isDeleted)
+ CLog::Log(LOGERROR, "NEWADDON REFCNT Releasing dead class {} 0x{:x}", GetClassname(),
+ (long)(((void*)this)));
+
+ long ct = --refs;
+#ifdef LOG_LIFECYCLE_EVENTS
+ CLog::Log(LOGDEBUG, "NEWADDON REFCNT decrementing to {} on {} 0x{:x}", refs.load(),
+ GetClassname(), (long)(((void*)this)));
+#endif
+ if(ct == 0)
+ {
+ const_cast<AddonClass*>(this)->isDeleted = true;
+ // we're faking a delete but not doing it so call the destructor explicitly
+ this->~AddonClass();
+ }
+ }
+
+ void AddonClass::Acquire() const
+ {
+ if (isDeleted)
+ CLog::Log(LOGERROR, "NEWADDON REFCNT Acquiring dead class {} 0x{:x}", GetClassname(),
+ (long)(((void*)this)));
+
+#ifdef LOG_LIFECYCLE_EVENTS
+ CLog::Log(LOGDEBUG, "NEWADDON REFCNT incrementing to {} on {} 0x{:x}", ++refs, GetClassname(),
+ (long)(((void*)this)));
+#else
+ ++refs;
+#endif
+ }
+#endif
+}
+
+
diff --git a/xbmc/interfaces/legacy/AddonClass.h b/xbmc/interfaces/legacy/AddonClass.h
new file mode 100644
index 0000000..f5de375
--- /dev/null
+++ b/xbmc/interfaces/legacy/AddonClass.h
@@ -0,0 +1,209 @@
+/*
+ * 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
+
+/**
+ * Defining LOG_LIFECYCLE_EVENTS will log all instantiations, deletions
+ * and also reference countings (increments and decrements) that take
+ * place on any Addon* class.
+ *
+ * Comment out (or uncomment out) to change the setting.
+ */
+//#define LOG_LIFECYCLE_EVENTS
+
+/**
+ * Defining XBMC_ADDON_DEBUG_MEMORY will make the Acquire and Release
+ * methods virtual allow the developer to overload them in a sub-class
+ * and set breakpoints to aid in debugging. It will also cause the
+ * reference counting mechanism to never actually delete any AddonClass
+ * instance allowing for the tracking of more references to (supposedly)
+ * deallocated classes.
+ *
+ * Comment out (or uncomment out) to change the setting.
+ */
+//#define XBMC_ADDON_DEBUG_MEMORY
+
+#include "AddonString.h"
+
+#include <mutex>
+#ifdef XBMC_ADDON_DEBUG_MEMORY
+#include "utils/log.h"
+#endif
+#include "AddonUtils.h"
+
+#include <atomic>
+#include <typeindex>
+
+namespace XBMCAddon
+{
+ class LanguageHook;
+
+ /**
+ * This class is the superclass for all reference counted classes in the api.
+ * It provides a means for the bindings to handle all api objects generically.
+ *
+ * It also provides some means for debugging "lifecycle" events (see the above
+ * description of LOG_LIFECYCLE_EVENTS).
+ *
+ * If a scripting language bindings require specific handling there is a
+ * hook to add in these language specifics that can be set here.
+ */
+ class AddonClass : public CCriticalSection
+ {
+ private:
+ mutable std::atomic<long> refs;
+ bool m_isDeallocating = false;
+
+ // no copying
+ inline AddonClass(const AddonClass&) = delete;
+
+#ifdef XBMC_ADDON_DEBUG_MEMORY
+ bool isDeleted;
+#endif
+
+ protected:
+ LanguageHook* languageHook;
+
+ /**
+ * This method is meant to be called from the destructor of the
+ * lowest level class.
+ *
+ * It's virtual because it's a convenient place to receive messages that
+ * we're about to go be deleted but prior to any real tear-down.
+ *
+ * Any overloading classes need to remember to pass the call up the chain.
+ */
+ virtual void deallocating()
+ {
+ std::unique_lock<CCriticalSection> lock(*this);
+ m_isDeallocating = true;
+ }
+
+ /**
+ * This is meant to be called during static initialization and so isn't
+ * synchronized.
+ */
+ static short getNextClassIndex();
+
+ public:
+ AddonClass();
+ virtual ~AddonClass();
+
+ inline const char* GetClassname() const { return typeid(*this).name(); }
+ inline LanguageHook* GetLanguageHook() { return languageHook; }
+
+ /**
+ * This method should be called while holding a Synchronize
+ * on the object. It will prevent the deallocation during
+ * the time it's held.
+ */
+ bool isDeallocating() { XBMC_TRACE; return m_isDeallocating; }
+
+ static short getNumAddonClasses();
+
+#ifdef XBMC_ADDON_DEBUG_MEMORY
+ virtual
+#else
+ inline
+#endif
+ void Release() const
+#ifndef XBMC_ADDON_DEBUG_MEMORY
+ {
+ long ct = --refs;
+#ifdef LOG_LIFECYCLE_EVENTS
+ CLog::Log(LOGDEBUG, "NEWADDON REFCNT decrementing to {} on {} 0x{:x}", ct, GetClassname(),
+ (long)(((void*)this)));
+#endif
+ if(ct == 0)
+ delete this;
+ }
+#else
+ ;
+#endif
+
+
+#ifdef XBMC_ADDON_DEBUG_MEMORY
+ virtual
+#else
+ inline
+#endif
+ void Acquire() const
+#ifndef XBMC_ADDON_DEBUG_MEMORY
+ {
+#ifdef LOG_LIFECYCLE_EVENTS
+ CLog::Log(LOGDEBUG, "NEWADDON REFCNT incrementing to {} on {} 0x{:x}", ++refs, GetClassname(),
+ (long)(((void*)this)));
+#else
+ ++refs;
+#endif
+ }
+#else
+ ;
+#endif
+
+#define refcheck
+ /**
+ * This class is a smart pointer for a Referenced class.
+ */
+ template <class T> class Ref
+ {
+ T * ac;
+ public:
+ inline Ref() : ac(NULL) {}
+ inline Ref(const T* _ac) : ac(const_cast<T*>(_ac)) { if (ac) ac->Acquire(); refcheck; }
+
+ // copy semantics
+ inline Ref(Ref<T> const & oref) : ac(const_cast<T*>(oref.get())) { if (ac) ac->Acquire(); refcheck; }
+ template<class O> inline Ref(Ref<O> const & oref) : ac(static_cast<T*>(oref.get())) { if (ac) ac->Acquire(); refcheck; }
+
+ /**
+ * operator= should work with either another smart pointer or a pointer since it will
+ * be able to convert a pointer to a smart pointer using one of the above constructors.
+ *
+ * Note: There is a trick here. The temporary variable is necessary because otherwise the
+ * following code will fail:
+ *
+ * Ref<T> ptr = new T;
+ * ptr = ptr;
+ *
+ * What happens without the tmp is the dereference is called first so the object ends up
+ * deleted and then the reference happens on a deleted object. The order is reversed
+ * in the following.
+ *
+ * Note: Operator= is ambiguous if you define both an operator=(Ref<T>&) and an operator=(T*). I'm
+ * opting for the route the boost took here figuring it has more history behind it.
+ */
+ inline Ref<T>& operator=(Ref<T> const & oref)
+ { T* tmp = ac; ac = const_cast<T*>(oref.get()); if (ac) ac->Acquire(); if (tmp) tmp->Release(); refcheck; return *this; }
+
+ inline T* operator->() const { refcheck; return ac; }
+
+ /**
+ * This operator doubles as the value in a boolean expression.
+ */
+ inline operator T*() const { refcheck; return ac; }
+ inline T* get() const { refcheck; return ac; }
+ inline T& getRef() const { refcheck; return *ac; }
+
+ inline ~Ref() { refcheck; if (ac) ac->Release(); }
+ inline bool isNull() const { refcheck; return ac == NULL; }
+ inline bool isNotNull() const { refcheck; return ac != NULL; }
+ inline bool isSet() const { refcheck; return ac != NULL; }
+ inline bool operator!() const { refcheck; return ac == NULL; }
+ inline bool operator==(const AddonClass::Ref<T>& oref) const { refcheck; return ac == oref.ac; }
+ inline bool operator<(const AddonClass::Ref<T>& oref) const { refcheck; return ac < oref.ac; } // std::set semantics
+
+ // This is there only for boost compatibility
+ template<class O> inline void reset(Ref<O> const & oref) { refcheck; (*this) = static_cast<T*>(oref.get()); refcheck; }
+ template<class O> inline void reset(O * oref) { refcheck; (*this) = static_cast<T*>(oref); refcheck; }
+ inline void reset() { refcheck; if (ac) ac->Release(); ac = NULL; }
+ };
+
+ };
+}
diff --git a/xbmc/interfaces/legacy/AddonString.h b/xbmc/interfaces/legacy/AddonString.h
new file mode 100644
index 0000000..7b1ef41
--- /dev/null
+++ b/xbmc/interfaces/legacy/AddonString.h
@@ -0,0 +1,21 @@
+/*
+ * 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 <string>
+
+namespace XBMCAddon
+{
+ typedef std::string String;
+
+ extern String emptyString;
+}
+
+
+
diff --git a/xbmc/interfaces/legacy/AddonUtils.cpp b/xbmc/interfaces/legacy/AddonUtils.cpp
new file mode 100644
index 0000000..ad0f0e0
--- /dev/null
+++ b/xbmc/interfaces/legacy/AddonUtils.cpp
@@ -0,0 +1,123 @@
+/*
+ * 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 "AddonUtils.h"
+
+#include "LanguageHook.h"
+#include "addons/Skin.h"
+#include "application/Application.h"
+#include "utils/XBMCTinyXML.h"
+#ifdef ENABLE_XBMC_TRACE_API
+#include "utils/log.h"
+#endif
+
+namespace XBMCAddonUtils
+{
+ GuiLock::GuiLock(XBMCAddon::LanguageHook* languageHook, bool offScreen)
+ : m_languageHook(languageHook), m_offScreen(offScreen)
+ {
+ if (!m_languageHook)
+ m_languageHook = XBMCAddon::LanguageHook::GetLanguageHook();
+ if (m_languageHook)
+ m_languageHook->DelayedCallOpen();
+
+ if (!m_offScreen)
+ g_application.LockFrameMoveGuard();
+ }
+
+ GuiLock::~GuiLock()
+ {
+ if (!m_offScreen)
+ g_application.UnlockFrameMoveGuard();
+
+ if (m_languageHook)
+ m_languageHook->DelayedCallClose();
+ }
+
+ static char defaultImage[1024];
+
+ const char *getDefaultImage(const char* cControlType, const char* cTextureType)
+ {
+ // create an xml block so that we can resolve our defaults
+ // <control type="type">
+ // <description />
+ // </control>
+ TiXmlElement control("control");
+ control.SetAttribute("type", cControlType);
+ TiXmlElement filler("description");
+ control.InsertEndChild(filler);
+ g_SkinInfo->ResolveIncludes(&control);
+
+ // ok, now check for our texture type
+ TiXmlElement *pTexture = control.FirstChildElement(cTextureType);
+ if (pTexture)
+ {
+ // found our textureType
+ TiXmlNode *pNode = pTexture->FirstChild();
+ if (pNode && pNode->Value()[0] != '-')
+ {
+ strncpy(defaultImage, pNode->Value(), sizeof(defaultImage));
+ defaultImage[sizeof(defaultImage) - 1] = '\0';
+ return defaultImage;
+ }
+ }
+ return "";
+ }
+
+#ifdef ENABLE_XBMC_TRACE_API
+ static thread_local TraceGuard* tlParent;
+
+ static char** getSpacesArray(int size)
+ {
+ char** ret = new char*[size];
+ for (int i = 0; i < size; i++)
+ {
+ ret[i] = new char[i + 1];
+
+ int j;
+ for (j = 0; j < i; j++)
+ ret[i][j] = ' ';
+ ret[i][j] = 0;
+ }
+ return ret;
+ }
+
+ static char** spaces = getSpacesArray(256);
+
+ const char* TraceGuard::getSpaces() { return spaces[depth]; }
+
+ TraceGuard::TraceGuard(const char* _function) :function(_function)
+ {
+ parent = tlParent;
+ depth = parent == NULL ? 0 : parent->depth + 1;
+
+ tlParent = this;
+
+ CLog::Log(LOGDEBUG, "{}NEWADDON Entering {}", spaces[depth], function);
+ }
+
+ TraceGuard::TraceGuard() :function(NULL)
+ {
+ parent = tlParent;
+ depth = parent == NULL ? 0 : parent->depth + 1;
+ tlParent = this;
+ // silent
+ }
+
+ TraceGuard::~TraceGuard()
+ {
+ if (function)
+ CLog::Log(LOGDEBUG, "{}NEWADDON Leaving {}", spaces[depth], function);
+
+ // need to pop the stack
+ tlParent = this->parent;
+ }
+#endif
+
+
+}
diff --git a/xbmc/interfaces/legacy/AddonUtils.h b/xbmc/interfaces/legacy/AddonUtils.h
new file mode 100644
index 0000000..2bf7de5
--- /dev/null
+++ b/xbmc/interfaces/legacy/AddonUtils.h
@@ -0,0 +1,95 @@
+/*
+ * 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
+
+/*
+ * addon.h
+ *
+ * Created on: Aug 21, 2010
+ * Author: jim
+ */
+
+//#define ENABLE_XBMC_TRACE_API
+
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <mutex>
+#include <vector>
+
+#ifdef TARGET_WINDOWS
+#define __PRETTY_FUNCTION__ __FUNCTION__
+#endif
+
+/**
+ * This file contains the public definitions for the Addon api. It's meant to be used
+ * by those writing language bindings.
+ */
+
+namespace XBMCAddon
+{
+class LanguageHook;
+}
+
+namespace XBMCAddonUtils
+{
+ class GuiLock
+ {
+ public:
+ GuiLock(XBMCAddon::LanguageHook* languageHook, bool offScreen);
+ ~GuiLock();
+
+ protected:
+ XBMCAddon::LanguageHook* m_languageHook = nullptr;
+ bool m_offScreen = false;
+ };
+
+ class InvertSingleLockGuard
+ {
+ std::unique_lock<CCriticalSection>& lock;
+
+ public:
+ explicit InvertSingleLockGuard(std::unique_lock<CCriticalSection>& _lock) : lock(_lock)
+ {
+ lock.unlock();
+ }
+ ~InvertSingleLockGuard() { lock.lock(); }
+ };
+
+
+ /*
+ * Looks in references.xml for image name
+ * If none exist return default image name
+ */
+ const char *getDefaultImage(const char* cControlType, const char* cTextureType);
+
+#ifdef ENABLE_XBMC_TRACE_API
+ class TraceGuard
+ {
+ const char* function;
+ public:
+ TraceGuard* parent;
+ int depth;
+
+ const char* getSpaces();
+
+ explicit TraceGuard(const char* _function);
+ TraceGuard();
+ ~TraceGuard();
+ };
+#endif
+}
+
+#ifdef ENABLE_XBMC_TRACE_API
+#define XBMC_TRACE XBMCAddonUtils::TraceGuard _tg(__PRETTY_FUNCTION__)
+#else
+#define XBMC_TRACE
+#endif
+
+
diff --git a/xbmc/interfaces/legacy/Alternative.h b/xbmc/interfaces/legacy/Alternative.h
new file mode 100644
index 0000000..a715688
--- /dev/null
+++ b/xbmc/interfaces/legacy/Alternative.h
@@ -0,0 +1,70 @@
+/*
+ * 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 "Exception.h"
+
+namespace XBMCAddon
+{
+ enum WhichAlternative { none, first, second };
+
+ template<typename T1, typename T2> class Alternative
+ {
+ public:
+ private:
+ WhichAlternative pos = none;
+ T1 d1;
+ T2 d2;
+
+ public:
+ Alternative() = default;
+
+ inline WhichAlternative which() const { return pos; }
+
+ inline T1& former()
+ {
+ if (pos == second)// first and none is ok
+ throw WrongTypeException("Access of XBMCAddon::Alternative as incorrect type");
+ if (pos == none)
+ d1 = T1();
+ pos = first;
+ return d1;
+ }
+
+ inline const T1& former() const
+ {
+ if (pos != first)
+ throw WrongTypeException("Access of XBMCAddon::Alternative as incorrect type");
+ return d1;
+ }
+
+ inline T2& later()
+ {
+ if (pos == first)
+ throw WrongTypeException("Access of XBMCAddon::Alternative as incorrect type");
+ if (pos == none)
+ d2 = T2();
+ pos = second;
+ return d2;
+ }
+
+ inline const T2& later() const
+ {
+ if (pos != second)
+ throw WrongTypeException("Access of XBMCAddon::Alternative as incorrect type");
+ return d2;
+ }
+
+ inline operator T1& () { return former(); }
+ inline operator const T1& () const { return former(); }
+ inline operator T2& () { return later(); }
+ inline operator const T2& () const { return later(); }
+ };
+}
+
diff --git a/xbmc/interfaces/legacy/CMakeLists.txt b/xbmc/interfaces/legacy/CMakeLists.txt
new file mode 100644
index 0000000..d92fc78
--- /dev/null
+++ b/xbmc/interfaces/legacy/CMakeLists.txt
@@ -0,0 +1,76 @@
+set(SOURCES AddonCallback.cpp
+ AddonClass.cpp
+ Addon.cpp
+ AddonUtils.cpp
+ CallbackFunction.cpp
+ CallbackHandler.cpp
+ Control.cpp
+ Dialog.cpp
+ DrmCryptoSession.cpp
+ File.cpp
+ InfoTagGame.cpp
+ InfoTagMusic.cpp
+ InfoTagPicture.cpp
+ InfoTagRadioRDS.cpp
+ InfoTagVideo.cpp
+ Keyboard.cpp
+ LanguageHook.cpp
+ ListItem.cpp
+ ModuleXbmc.cpp
+ ModuleXbmcgui.cpp
+ ModuleXbmcplugin.cpp
+ ModuleXbmcvfs.cpp
+ Monitor.cpp
+ Player.cpp
+ PlayList.cpp
+ Settings.cpp
+ String.cpp
+ Window.cpp
+ WindowDialog.cpp
+ WindowDialogMixin.cpp
+ WindowXML.cpp)
+
+set(HEADERS Addon.h
+ AddonCallback.h
+ AddonClass.h
+ AddonString.h
+ AddonUtils.h
+ Alternative.h
+ aojsonrpc.h
+ CallbackFunction.h
+ CallbackHandler.h
+ Control.h
+ Dialog.h
+ Dictionary.h
+ DrmCryptoSession.h
+ Exception.h
+ File.h
+ InfoTagGame.h
+ InfoTagMusic.h
+ InfoTagPicture.h
+ InfoTagRadioRDS.h
+ InfoTagVideo.h
+ Keyboard.h
+ LanguageHook.h
+ List.h
+ ListItem.h
+ ModuleXbmc.h
+ ModuleXbmcgui.h
+ ModuleXbmcplugin.h
+ ModuleXbmcvfs.h
+ Monitor.h
+ Player.h
+ PlayList.h
+ RenderCapture.h
+ Settings.h
+ Stat.h
+ swighelper.h
+ Tuple.h
+ Window.h
+ WindowDialog.h
+ WindowDialogMixin.h
+ WindowException.h
+ WindowInterceptor.h
+ WindowXML.h)
+
+core_add_library(legacy_interface)
diff --git a/xbmc/interfaces/legacy/CallbackFunction.cpp b/xbmc/interfaces/legacy/CallbackFunction.cpp
new file mode 100644
index 0000000..fdd6cf8
--- /dev/null
+++ b/xbmc/interfaces/legacy/CallbackFunction.cpp
@@ -0,0 +1,14 @@
+/*
+ * 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 "CallbackFunction.h"
+
+namespace XBMCAddon
+{
+ Callback::~Callback() { XBMC_TRACE; deallocating(); }
+}
diff --git a/xbmc/interfaces/legacy/CallbackFunction.h b/xbmc/interfaces/legacy/CallbackFunction.h
new file mode 100644
index 0000000..90360d5
--- /dev/null
+++ b/xbmc/interfaces/legacy/CallbackFunction.h
@@ -0,0 +1,171 @@
+/*
+ * 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 "AddonClass.h"
+
+namespace XBMCAddon
+{
+ /**
+ * <p>This is the parent class for the class templates that hold
+ * a callback. A callback is essentially a templatized
+ * functor (functoid?) for a call to a member function.</p>
+ *
+ * <p>This class combined with the attending CallbackHandlers should make
+ * sure that the AddonClass isn't in the midst of deallocating when
+ * the callback executes. In this way the Callback class acts as
+ * a weak reference.</p>
+ */
+ class Callback : public AddonClass
+ {
+ protected:
+ AddonClass* addonClassObject;
+ explicit Callback(AddonClass* _object) : addonClassObject(_object) { XBMC_TRACE; }
+
+ public:
+ virtual void executeCallback() = 0;
+ ~Callback() override;
+
+ AddonClass* getObject() { XBMC_TRACE; return addonClassObject; }
+ };
+
+ struct cb_null_type {};
+
+ // stub type template to be partial specialized
+ template<typename M = cb_null_type, typename T1 = cb_null_type,
+ typename T2 = cb_null_type, typename T3 = cb_null_type,
+ typename T4 = cb_null_type, typename Extraneous = cb_null_type>
+ class CallbackFunction {};
+
+ /**
+ * This is the template to carry a callback to a member function
+ * that returns 'void' (has no return) and takes no parameters.
+ */
+ template<class M> class CallbackFunction<M, cb_null_type, cb_null_type, cb_null_type, cb_null_type, cb_null_type> : public Callback
+ {
+ public:
+ typedef void (M::*MemberFunction)();
+
+ protected:
+ MemberFunction meth;
+ M* obj;
+
+ public:
+ CallbackFunction(M* object, MemberFunction method) :
+ Callback(object), meth(method), obj(object) { XBMC_TRACE; }
+
+ ~CallbackFunction() override { XBMC_TRACE; deallocating(); }
+
+ void executeCallback() override { XBMC_TRACE; ((*obj).*(meth))(); }
+ };
+
+ /**
+ * This is the template to carry a callback to a member function
+ * that returns 'void' (has no return) and takes one parameter.
+ */
+ template<class M, typename P1> class CallbackFunction<M,P1, cb_null_type, cb_null_type, cb_null_type, cb_null_type> : public Callback
+ {
+ public:
+ typedef void (M::*MemberFunction)(P1);
+
+ protected:
+ MemberFunction meth;
+ M* obj;
+ P1 param;
+
+ public:
+ CallbackFunction(M* object, MemberFunction method, P1 parameter) :
+ Callback(object), meth(method), obj(object),
+ param(parameter) { XBMC_TRACE; }
+
+ ~CallbackFunction() override { XBMC_TRACE; deallocating(); }
+
+ void executeCallback() override { XBMC_TRACE; ((*obj).*(meth))(param); }
+ };
+
+ /**
+ * This is the template to carry a callback to a member function
+ * that returns 'void' (has no return) and takes one parameter
+ * that can be held in an AddonClass::Ref
+ */
+ template<class M, typename P1> class CallbackFunction<M,AddonClass::Ref<P1>, cb_null_type, cb_null_type, cb_null_type, cb_null_type> : public Callback
+ {
+ public:
+ typedef void (M::*MemberFunction)(P1*);
+
+ protected:
+ MemberFunction meth;
+ M* obj;
+ AddonClass::Ref<P1> param;
+
+ public:
+ CallbackFunction(M* object, MemberFunction method, P1* parameter) :
+ Callback(object), meth(method), obj(object),
+ param(parameter) { XBMC_TRACE; }
+
+ ~CallbackFunction() override { XBMC_TRACE; deallocating(); }
+
+ void executeCallback() override { XBMC_TRACE; ((*obj).*(meth))(param); }
+ };
+
+
+ /**
+ * This is the template to carry a callback to a member function
+ * that returns 'void' (has no return) and takes two parameters.
+ */
+ template<class M, typename P1, typename P2> class CallbackFunction<M,P1,P2, cb_null_type, cb_null_type, cb_null_type> : public Callback
+ {
+ public:
+ typedef void (M::*MemberFunction)(P1,P2);
+
+ protected:
+ MemberFunction meth;
+ M* obj;
+ P1 param1;
+ P2 param2;
+
+ public:
+ CallbackFunction(M* object, MemberFunction method, P1 parameter, P2 parameter2) :
+ Callback(object), meth(method), obj(object),
+ param1(parameter), param2(parameter2) { XBMC_TRACE; }
+
+ ~CallbackFunction() override { XBMC_TRACE; deallocating(); }
+
+ void executeCallback() override { XBMC_TRACE; ((*obj).*(meth))(param1,param2); }
+ };
+
+
+ /**
+ * This is the template to carry a callback to a member function
+ * that returns 'void' (has no return) and takes three parameters.
+ */
+ template<class M, typename P1, typename P2, typename P3> class CallbackFunction<M,P1,P2,P3, cb_null_type, cb_null_type> : public Callback
+ {
+ public:
+ typedef void (M::*MemberFunction)(P1,P2,P3);
+
+ protected:
+ MemberFunction meth;
+ M* obj;
+ P1 param1;
+ P2 param2;
+ P3 param3;
+
+ public:
+ CallbackFunction(M* object, MemberFunction method, P1 parameter, P2 parameter2, P3 parameter3) :
+ Callback(object), meth(method), obj(object),
+ param1(parameter), param2(parameter2), param3(parameter3) { XBMC_TRACE; }
+
+ ~CallbackFunction() override { XBMC_TRACE; deallocating(); }
+
+ void executeCallback() override { XBMC_TRACE; ((*obj).*(meth))(param1,param2,param3); }
+ };
+}
+
+
diff --git a/xbmc/interfaces/legacy/CallbackHandler.cpp b/xbmc/interfaces/legacy/CallbackHandler.cpp
new file mode 100644
index 0000000..f567168
--- /dev/null
+++ b/xbmc/interfaces/legacy/CallbackHandler.cpp
@@ -0,0 +1,148 @@
+/*
+ * 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 "CallbackHandler.h"
+
+#include "AddonUtils.h"
+#include "commons/Exception.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <vector>
+
+namespace XBMCAddon
+{
+ class AsyncCallbackMessage : public AddonClass
+ {
+ public:
+ AddonClass::Ref<Callback> cb;
+ AddonClass::Ref<RetardedAsyncCallbackHandler> handler;
+ AsyncCallbackMessage(Callback* _cb, RetardedAsyncCallbackHandler* _handler) :
+ cb(_cb), handler(_handler) { XBMC_TRACE; }
+ };
+
+ //********************************************************************
+ // This holds the callback messages which will be executed. It doesn't
+ // seem to work correctly with the Ref object so we'll go with Ref*'s
+ typedef std::vector<AddonClass::Ref<AsyncCallbackMessage> > CallbackQueue;
+ //********************************************************************
+
+ static CCriticalSection critSection;
+ static CallbackQueue g_callQueue;
+
+ void RetardedAsyncCallbackHandler::invokeCallback(Callback* cb)
+ {
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(critSection);
+ g_callQueue.push_back(new AsyncCallbackMessage(cb,this));
+ }
+
+ RetardedAsyncCallbackHandler::~RetardedAsyncCallbackHandler()
+ {
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(critSection);
+
+ // find any messages that might be there because of me ... and remove them
+ CallbackQueue::iterator iter = g_callQueue.begin();
+ while (iter != g_callQueue.end())
+ {
+ if ((*iter)->handler.get() == this) // then this message is because of me
+ {
+ g_callQueue.erase(iter);
+ iter = g_callQueue.begin();
+ }
+ else
+ ++iter;
+ }
+ }
+
+ void RetardedAsyncCallbackHandler::makePendingCalls()
+ {
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(critSection);
+ CallbackQueue::iterator iter = g_callQueue.begin();
+ while (iter != g_callQueue.end())
+ {
+ AddonClass::Ref<AsyncCallbackMessage> p(*iter);
+
+ // only call when we are in the right thread state
+ if(p->handler->isStateOk(p->cb->getObject()))
+ {
+ // remove it from the queue. No matter what we're done with
+ // this. Even if it doesn't execute for some reason.
+ g_callQueue.erase(iter);
+
+ // we need to release the critSection lock prior to grabbing the
+ // lock on the object. Not doing so results in deadlocks. We no
+ // longer are accessing the g_callQueue so it's fine to do this now
+ {
+ XBMCAddonUtils::InvertSingleLockGuard unlock(lock);
+
+ // make sure the object is not deallocating
+
+ // we need to grab the object lock to see if the object of the call
+ // is deallocating. holding this lock should prevent it from
+ // deallocating during the execution of this call.
+#ifdef ENABLE_XBMC_TRACE_API
+ CLog::Log(LOGDEBUG, "{}NEWADDON executing callback 0x{:x}", _tg.getSpaces(),
+ (long)(p->cb.get()));
+#endif
+ AddonClass* obj = (p->cb->getObject());
+ AddonClass::Ref<AddonClass> ref(obj);
+ std::unique_lock<CCriticalSection> lock2(*obj);
+ if (!p->cb->getObject()->isDeallocating())
+ {
+ try
+ {
+ // need to make the call
+ p->cb->executeCallback();
+ }
+ catch (XbmcCommons::Exception& e) { e.LogThrowMessage(); }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Unknown exception while executing callback {:#x}",
+ reinterpret_cast<int64_t>(p->cb.get()));
+ }
+ }
+ }
+
+ // since the state of the iterator may have been corrupted by
+ // the changing state of the list from another thread during
+ // the releasing fo the lock in the immediately preceeding
+ // codeblock, we need to reset it before continuing the loop
+ iter = g_callQueue.begin();
+ }
+ else // if we're not in the right thread for this callback...
+ ++iter;
+ }
+ }
+
+ void RetardedAsyncCallbackHandler::clearPendingCalls(void* userData)
+ {
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(critSection);
+ CallbackQueue::iterator iter = g_callQueue.begin();
+ while (iter != g_callQueue.end())
+ {
+ AddonClass::Ref<AsyncCallbackMessage> p(*iter);
+
+ if(p->handler->shouldRemoveCallback(p->cb->getObject(),userData))
+ {
+#ifdef ENABLE_XBMC_TRACE_API
+ CLog::Log(LOGDEBUG,
+ "{}NEWADDON removing callback 0x{:x} for PyThreadState 0x{:x} from queue",
+ _tg.getSpaces(), (long)(p->cb.get()), (long)userData);
+#endif
+ iter = g_callQueue.erase(iter);
+ }
+ else
+ ++iter;
+ }
+ }
+}
+
diff --git a/xbmc/interfaces/legacy/CallbackHandler.h b/xbmc/interfaces/legacy/CallbackHandler.h
new file mode 100644
index 0000000..223c74a
--- /dev/null
+++ b/xbmc/interfaces/legacy/CallbackHandler.h
@@ -0,0 +1,58 @@
+/*
+ * 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 "AddonClass.h"
+#include "CallbackFunction.h"
+
+namespace XBMCAddon
+{
+ /**
+ * This is the abstraction representing different ways to handle
+ * the execution of callbacks. Different language bindings may
+ * have different requirements.
+ */
+ class CallbackHandler : public AddonClass
+ {
+ protected:
+ inline CallbackHandler() = default;
+
+ public:
+ virtual void invokeCallback(Callback* cb) = 0;
+ };
+
+ /**
+ * This class is primarily for Python support (hence the "Retarded"
+ * prefix). Python (et. al. Retarded languages) require that
+ * the context within which a callback executes is under the control
+ * of the language. Therefore, this handler is used to queue
+ * messages over to a language controlled thread for eventual
+ * execution.
+ *
+ * @todo Allow a cross thread synchronous execution.
+ * Fix the stupid means of calling the clearPendingCalls by passing
+ * userData which is specific to the handler/language type.
+ */
+ class RetardedAsyncCallbackHandler : public CallbackHandler
+ {
+ protected:
+ inline RetardedAsyncCallbackHandler() = default;
+ public:
+
+ ~RetardedAsyncCallbackHandler() override;
+
+ void invokeCallback(Callback* cb) override;
+ static void makePendingCalls();
+ static void clearPendingCalls(void* userData);
+
+ virtual bool isStateOk(AddonClass* obj) = 0;
+ virtual bool shouldRemoveCallback(AddonClass* obj, void* userData) = 0;
+ };
+
+}
diff --git a/xbmc/interfaces/legacy/Control.cpp b/xbmc/interfaces/legacy/Control.cpp
new file mode 100644
index 0000000..62dbac6
--- /dev/null
+++ b/xbmc/interfaces/legacy/Control.cpp
@@ -0,0 +1,1435 @@
+/*
+ * 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 "Control.h"
+
+#include "AddonUtils.h"
+#include "LanguageHook.h"
+#include "ServiceBroker.h"
+#include "WindowException.h"
+#include "guilib/GUIButtonControl.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIControlFactory.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIFadeLabelControl.h"
+#include "guilib/GUIFontManager.h"
+#include "guilib/GUIImage.h"
+#include "guilib/GUILabel.h"
+#include "guilib/GUILabelControl.h"
+#include "guilib/GUIListContainer.h"
+#include "guilib/GUIProgressControl.h"
+#include "guilib/GUIRadioButtonControl.h"
+#include "guilib/GUISliderControl.h"
+#include "guilib/GUITextBox.h"
+#include "guilib/GUIWindowManager.h"
+#include "listproviders/StaticProvider.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+
+using namespace KODI;
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+
+ // ============================================================
+
+ // ============================================================
+ // ============================================================
+ ControlFadeLabel::ControlFadeLabel(long x, long y, long width, long height,
+ const char* font, const char* _textColor,
+ long _alignment) :
+ strFont("font13"), textColor(0xffffffff), align(_alignment)
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+
+ if (font)
+ strFont = font;
+
+ if (_textColor)
+ sscanf(_textColor, "%x", &textColor);
+
+ pGUIControl = NULL;
+ }
+
+ void ControlFadeLabel::addLabel(const String& label)
+ {
+ CGUIMessage msg(GUI_MSG_LABEL_ADD, iParentId, iControlId);
+ msg.SetLabel(label);
+
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iParentId);
+ }
+
+ void ControlFadeLabel::reset()
+ {
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, iParentId, iControlId);
+
+ vecLabels.clear();
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iParentId);
+ }
+
+ CGUIControl* ControlFadeLabel::Create()
+ {
+ CLabelInfo label;
+ label.font = g_fontManager.GetFont(strFont);
+ label.textColor = label.focusedColor = textColor;
+ label.align = align;
+ pGUIControl = new CGUIFadeLabelControl(
+ iParentId,
+ iControlId,
+ (float)dwPosX,
+ (float)dwPosY,
+ (float)dwWidth,
+ (float)dwHeight,
+ label,
+ true,
+ 0,
+ true,
+ false);
+ pGUIControl->SetVisible(m_visible);
+
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, iParentId, iControlId);
+ pGUIControl->OnMessage(msg);
+
+ return pGUIControl;
+ }
+
+ void ControlFadeLabel::setScrolling(bool scroll)
+ {
+ static_cast<CGUIFadeLabelControl*>(pGUIControl)->SetScrolling(scroll);
+ }
+
+ // ============================================================
+
+ // ============================================================
+ // ============================================================
+ ControlTextBox::ControlTextBox(long x, long y, long width, long height,
+ const char* font, const char* _textColor) :
+ strFont("font13"), textColor(0xffffffff)
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+
+ if (font)
+ strFont = font;
+
+ if (_textColor)
+ sscanf(_textColor, "%x", &textColor);
+ }
+
+ void ControlTextBox::setText(const String& text)
+ {
+ if (pGUIControl)
+ {
+ // create message
+ CGUIMessage msg(GUI_MSG_LABEL_SET, iParentId, iControlId);
+ msg.SetLabel(text);
+
+ // send message
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iParentId);
+ }
+ else
+ {
+ m_label = text;
+ }
+ }
+
+ String ControlTextBox::getText()
+ {
+ if (pGUIControl == nullptr)
+ return m_label;
+
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ return static_cast<CGUITextBox*>(pGUIControl)->GetDescription();
+ }
+
+ void ControlTextBox::reset()
+ {
+ if (pGUIControl)
+ {
+ // create message
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, iParentId, iControlId);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iParentId);
+ }
+ m_label.clear();
+ }
+
+ void ControlTextBox::scroll(long position)
+ {
+ if (pGUIControl)
+ static_cast<CGUITextBox*>(pGUIControl)->Scroll((int)position);
+ }
+
+ void ControlTextBox::autoScroll(int delay, int time, int repeat)
+ {
+ if (pGUIControl)
+ static_cast<CGUITextBox*>(pGUIControl)->SetAutoScrolling(delay, time, repeat);
+ }
+
+ CGUIControl* ControlTextBox::Create()
+ {
+ // create textbox
+ CLabelInfo label;
+ label.font = g_fontManager.GetFont(strFont);
+ label.textColor = label.focusedColor = textColor;
+
+ pGUIControl = new CGUITextBox(iParentId, iControlId,
+ (float)dwPosX, (float)dwPosY, (float)dwWidth, (float)dwHeight,
+ label);
+ pGUIControl->SetVisible(m_visible);
+
+ // set label
+ CGUIMessage msg(GUI_MSG_LABEL_SET, iParentId, iControlId);
+ msg.SetLabel(m_label);
+ pGUIControl->OnMessage(msg);
+
+ return pGUIControl;
+ }
+
+ // ============================================================
+
+ // ============================================================
+ // ============================================================
+ ControlButton::ControlButton(long x,
+ long y,
+ long width,
+ long height,
+ const String& label,
+ const char* focusTexture,
+ const char* noFocusTexture,
+ long _textOffsetX,
+ long _textOffsetY,
+ long alignment,
+ const char* font,
+ const char* _textColor,
+ const char* _disabledColor,
+ long angle,
+ const char* _shadowColor,
+ const char* _focusedColor)
+ : textOffsetX(_textOffsetX),
+ textOffsetY(_textOffsetY),
+ align(alignment),
+ strFont("font13"),
+ textColor(0xffffffff),
+ disabledColor(0x60ffffff),
+ iAngle(angle),
+ focusedColor(0xffffffff),
+ strText(label)
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+
+ // if texture is supplied use it, else get default ones
+ strTextureFocus = focusTexture ? focusTexture :
+ XBMCAddonUtils::getDefaultImage("button", "texturefocus");
+ strTextureNoFocus = noFocusTexture ? noFocusTexture :
+ XBMCAddonUtils::getDefaultImage("button", "texturenofocus");
+
+ if (font) strFont = font;
+ if (_textColor) sscanf( _textColor, "%x", &textColor );
+ if (_disabledColor) sscanf( _disabledColor, "%x", &disabledColor );
+ if (_shadowColor) sscanf( _shadowColor, "%x", &shadowColor );
+ if (_focusedColor) sscanf( _focusedColor, "%x", &focusedColor );
+ }
+
+ void ControlButton::setLabel(const String& label,
+ const char* font,
+ const char* _textColor,
+ const char* _disabledColor,
+ const char* _shadowColor,
+ const char* _focusedColor,
+ const String& label2)
+ {
+ if (!label.empty()) strText = label;
+ if (!label2.empty()) strText2 = label2;
+ if (font) strFont = font;
+ if (_textColor) sscanf(_textColor, "%x", &textColor);
+ if (_disabledColor) sscanf( _disabledColor, "%x", &disabledColor );
+ if (_shadowColor) sscanf(_shadowColor, "%x", &shadowColor);
+ if (_focusedColor) sscanf(_focusedColor, "%x", &focusedColor);
+
+ if (pGUIControl)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ static_cast<CGUIButtonControl*>(pGUIControl)->PythonSetLabel(strFont, strText, textColor, shadowColor, focusedColor);
+ static_cast<CGUIButtonControl*>(pGUIControl)->SetLabel2(strText2);
+ static_cast<CGUIButtonControl*>(pGUIControl)->PythonSetDisabledColor(disabledColor);
+ }
+ }
+
+ void ControlButton::setDisabledColor(const char* color)
+ {
+ if (color) sscanf(color, "%x", &disabledColor);
+
+ if (pGUIControl)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ static_cast<CGUIButtonControl*>(pGUIControl)->PythonSetDisabledColor(disabledColor);
+ }
+ }
+
+ String ControlButton::getLabel()
+ {
+ if (pGUIControl == nullptr)
+ return strText;
+
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ return static_cast<CGUIButtonControl*>(pGUIControl)->GetLabel();
+ }
+
+ String ControlButton::getLabel2()
+ {
+ if (pGUIControl == nullptr)
+ return strText2;
+
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ return static_cast<CGUIButtonControl*>(pGUIControl)->GetLabel2();
+ }
+
+ CGUIControl* ControlButton::Create()
+ {
+ CLabelInfo label;
+ label.font = g_fontManager.GetFont(strFont);
+ label.textColor = textColor;
+ label.disabledColor = disabledColor;
+ label.shadowColor = shadowColor;
+ label.focusedColor = focusedColor;
+ label.align = align;
+ label.offsetX = (float)textOffsetX;
+ label.offsetY = (float)textOffsetY;
+ label.angle = (float)-iAngle;
+ pGUIControl = new CGUIButtonControl(
+ iParentId,
+ iControlId,
+ (float)dwPosX,
+ (float)dwPosY,
+ (float)dwWidth,
+ (float)dwHeight,
+ CTextureInfo(strTextureFocus),
+ CTextureInfo(strTextureNoFocus),
+ label);
+ pGUIControl->SetVisible(m_visible);
+
+ CGUIButtonControl* pGuiButtonControl =
+ static_cast<CGUIButtonControl*>(pGUIControl);
+
+ pGuiButtonControl->SetLabel(strText);
+ pGuiButtonControl->SetLabel2(strText2);
+
+ return pGUIControl;
+ }
+
+ // ============================================================
+
+ // ============================================================
+ // ============================================================
+ ControlImage::ControlImage(long x, long y, long width, long height,
+ const char* filename, long aRatio,
+ const char* _colorDiffuse):
+ aspectRatio(aRatio), colorDiffuse(0)
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+
+ // check if filename exists
+ strFileName = filename;
+ if (_colorDiffuse)
+ sscanf(_colorDiffuse, "%x", &colorDiffuse);
+ }
+
+ void ControlImage::setImage(const char* imageFilename, const bool useCache)
+ {
+ strFileName = imageFilename;
+
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ if (pGUIControl)
+ static_cast<CGUIImage*>(pGUIControl)->SetFileName(strFileName, false, useCache);
+ }
+
+ void ControlImage::setColorDiffuse(const char* cColorDiffuse)
+ {
+ if (cColorDiffuse) sscanf(cColorDiffuse, "%x", &colorDiffuse);
+ else colorDiffuse = 0;
+
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ if (pGUIControl)
+ static_cast<CGUIImage*>(pGUIControl)->SetColorDiffuse(GUILIB::GUIINFO::CGUIInfoColor(colorDiffuse));
+ }
+
+ CGUIControl* ControlImage::Create()
+ {
+ pGUIControl = new CGUIImage(iParentId, iControlId,
+ (float)dwPosX, (float)dwPosY, (float)dwWidth, (float)dwHeight,
+ CTextureInfo(strFileName));
+ pGUIControl->SetVisible(m_visible);
+
+ if (pGUIControl && aspectRatio <= CAspectRatio::AR_KEEP)
+ static_cast<CGUIImage*>(pGUIControl)->SetAspectRatio((CAspectRatio::ASPECT_RATIO)aspectRatio);
+
+ if (pGUIControl && colorDiffuse)
+ static_cast<CGUIImage*>(pGUIControl)->SetColorDiffuse(GUILIB::GUIINFO::CGUIInfoColor(colorDiffuse));
+
+ return pGUIControl;
+ }
+
+ // ============================================================
+
+ // ============================================================
+ // ============================================================
+ ControlProgress::ControlProgress(long x, long y, long width, long height,
+ const char* texturebg,
+ const char* textureleft,
+ const char* texturemid,
+ const char* textureright,
+ const char* textureoverlay)
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+
+ // if texture is supplied use it, else get default ones
+ strTextureBg = texturebg ? texturebg :
+ XBMCAddonUtils::getDefaultImage("progress", "texturebg");
+ strTextureLeft = textureleft ? textureleft :
+ XBMCAddonUtils::getDefaultImage("progress", "lefttexture");
+ strTextureMid = texturemid ? texturemid :
+ XBMCAddonUtils::getDefaultImage("progress", "midtexture");
+ strTextureRight = textureright ? textureright :
+ XBMCAddonUtils::getDefaultImage("progress", "righttexture");
+ strTextureOverlay = textureoverlay ? textureoverlay :
+ XBMCAddonUtils::getDefaultImage("progress", "overlaytexture");
+ }
+
+ void ControlProgress::setPercent(float pct)
+ {
+ if (pGUIControl)
+ static_cast<CGUIProgressControl*>(pGUIControl)->SetPercentage(pct);
+ }
+
+ float ControlProgress::getPercent()
+ {
+ return pGUIControl ? static_cast<CGUIProgressControl*>(pGUIControl)->GetPercentage() : 0.0f;
+ }
+
+ CGUIControl* ControlProgress::Create()
+ {
+ pGUIControl = new CGUIProgressControl(iParentId, iControlId,
+ (float)dwPosX, (float)dwPosY,
+ (float)dwWidth,(float)dwHeight,
+ CTextureInfo(strTextureBg), CTextureInfo(strTextureLeft),
+ CTextureInfo(strTextureMid), CTextureInfo(strTextureRight),
+ CTextureInfo(strTextureOverlay));
+ pGUIControl->SetVisible(m_visible);
+
+ if (pGUIControl && colorDiffuse)
+ static_cast<CGUIProgressControl*>(pGUIControl)->SetColorDiffuse(GUILIB::GUIINFO::CGUIInfoColor(colorDiffuse));
+
+ return pGUIControl;
+ }
+
+ // ============================================================
+
+ // ============================================================
+ // ============================================================
+ ControlSlider::ControlSlider(long x, long y, long width, long height,
+ const char* textureback,
+ const char* texture,
+ const char* texturefocus, int orientation)
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+ iOrientation = orientation;
+
+ // if texture is supplied use it, else get default ones
+ strTextureBack = textureback ? textureback :
+ XBMCAddonUtils::getDefaultImage("slider", "texturesliderbar");
+ strTexture = texture ? texture :
+ XBMCAddonUtils::getDefaultImage("slider", "textureslidernib");
+ strTextureFoc = texturefocus ? texturefocus :
+ XBMCAddonUtils::getDefaultImage("slider", "textureslidernibfocus");
+ }
+
+ float ControlSlider::getPercent()
+ {
+ return pGUIControl ? static_cast<CGUISliderControl*>(pGUIControl)->GetPercentage() : 0.0f;
+ }
+
+ void ControlSlider::setPercent(float pct)
+ {
+ if (pGUIControl)
+ static_cast<CGUISliderControl*>(pGUIControl)->SetPercentage(pct);
+ }
+
+ int ControlSlider::getInt()
+ {
+ return (pGUIControl) ? static_cast<CGUISliderControl*>(pGUIControl)->GetIntValue() : 0;
+ }
+
+ void ControlSlider::setInt(int value, int min, int delta, int max)
+ {
+ if (pGUIControl)
+ {
+ static_cast<CGUISliderControl*>(pGUIControl)->SetType(SLIDER_CONTROL_TYPE_INT);
+ static_cast<CGUISliderControl*>(pGUIControl)->SetRange(min, max);
+ static_cast<CGUISliderControl*>(pGUIControl)->SetIntInterval(delta);
+ static_cast<CGUISliderControl*>(pGUIControl)->SetIntValue(value);
+ }
+ }
+
+ float ControlSlider::getFloat()
+ {
+ return (pGUIControl) ? static_cast<CGUISliderControl*>(pGUIControl)->GetFloatValue() : 0.0f;
+ }
+
+ void ControlSlider::setFloat(float value, float min, float delta, float max)
+ {
+ if (pGUIControl)
+ {
+ static_cast<CGUISliderControl*>(pGUIControl)->SetType(SLIDER_CONTROL_TYPE_FLOAT);
+ static_cast<CGUISliderControl*>(pGUIControl)->SetFloatRange(min, max);
+ static_cast<CGUISliderControl*>(pGUIControl)->SetFloatInterval(delta);
+ static_cast<CGUISliderControl*>(pGUIControl)->SetFloatValue(value);
+ }
+ }
+
+ CGUIControl* ControlSlider::Create ()
+ {
+ pGUIControl = new CGUISliderControl(iParentId, iControlId,(float)dwPosX, (float)dwPosY,
+ (float)dwWidth,(float)dwHeight,
+ CTextureInfo(strTextureBack),CTextureInfo(strTexture),
+ CTextureInfo(strTextureFoc), 0, ORIENTATION(iOrientation));
+
+ return pGUIControl;
+ }
+
+ // ============================================================
+
+ // ============================================================
+ // ============================================================
+ ControlGroup::ControlGroup(long x, long y, long width, long height)
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+ }
+
+ CGUIControl* ControlGroup::Create()
+ {
+ pGUIControl = new CGUIControlGroup(iParentId,
+ iControlId,
+ (float) dwPosX,
+ (float) dwPosY,
+ (float) dwWidth,
+ (float) dwHeight);
+ pGUIControl->SetVisible(m_visible);
+ return pGUIControl;
+ }
+
+ // ============================================================
+
+ // ============================================================
+ // ============================================================
+ ControlRadioButton::ControlRadioButton(long x, long y, long width, long height, const String& label,
+ const char* focusOnTexture, const char* noFocusOnTexture,
+ const char* focusOffTexture, const char* noFocusOffTexture,
+ const char* focusTexture, const char* noFocusTexture,
+ long _textOffsetX, long _textOffsetY,
+ long alignment, const char* font, const char* _textColor,
+ const char* _disabledColor, long angle,
+ const char* _shadowColor, const char* _focusedColor,
+ const char* disabledOnTexture, const char* disabledOffTexture) :
+ strFont("font13"), textColor(0xffffffff), disabledColor(0x60ffffff),
+ textOffsetX(_textOffsetX), textOffsetY(_textOffsetY), align(alignment), iAngle(angle),
+ shadowColor(0), focusedColor(0xffffffff)
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+
+ strText = label;
+
+ // if texture is supplied use it, else get default ones
+ strTextureFocus = focusTexture ? focusTexture :
+ XBMCAddonUtils::getDefaultImage("button", "texturefocus");
+ strTextureNoFocus = noFocusTexture ? noFocusTexture :
+ XBMCAddonUtils::getDefaultImage("button", "texturenofocus");
+
+ if (focusOnTexture && noFocusOnTexture)
+ {
+ strTextureRadioOnFocus = focusOnTexture;
+ strTextureRadioOnNoFocus = noFocusOnTexture;
+ }
+ else
+ {
+ strTextureRadioOnFocus =
+ XBMCAddonUtils::getDefaultImage("radiobutton", "textureradioonfocus");
+ strTextureRadioOnNoFocus =
+ XBMCAddonUtils::getDefaultImage("radiobutton", "textureradioonnofocus");
+ }
+
+ if (focusOffTexture && noFocusOffTexture)
+ {
+ strTextureRadioOffFocus = focusOffTexture;
+ strTextureRadioOffNoFocus = noFocusOffTexture;
+ }
+ else
+ {
+ strTextureRadioOffFocus =
+ XBMCAddonUtils::getDefaultImage("radiobutton", "textureradioofffocus");
+ strTextureRadioOffNoFocus =
+ XBMCAddonUtils::getDefaultImage("radiobutton", "textureradiooffnofocus");
+ }
+
+ if (font) strFont = font;
+ if (_textColor) sscanf( _textColor, "%x", &textColor );
+ if (_disabledColor) sscanf( _disabledColor, "%x", &disabledColor );
+ if (_shadowColor) sscanf( _shadowColor, "%x", &shadowColor );
+ if (_focusedColor) sscanf( _focusedColor, "%x", &focusedColor );
+ }
+
+ void ControlRadioButton::setSelected(bool selected)
+ {
+ if (pGUIControl)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ static_cast<CGUIRadioButtonControl*>(pGUIControl)->SetSelected(selected);
+ }
+ }
+
+ bool ControlRadioButton::isSelected()
+ {
+ bool isSelected = false;
+
+ if (pGUIControl)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ isSelected = static_cast<CGUIRadioButtonControl*>(pGUIControl)->IsSelected();
+ }
+ return isSelected;
+ }
+
+ void ControlRadioButton::setLabel(const String& label,
+ const char* font,
+ const char* _textColor,
+ const char* _disabledColor,
+ const char* _shadowColor,
+ const char* _focusedColor,
+ const String& label2)
+ {
+ if (!label.empty()) strText = label;
+ if (font) strFont = font;
+ if (_textColor) sscanf(_textColor, "%x", &textColor);
+ if (_disabledColor) sscanf( _disabledColor, "%x", &disabledColor );
+ if (_shadowColor) sscanf(_shadowColor, "%x", &shadowColor);
+ if (_focusedColor) sscanf(_focusedColor, "%x", &focusedColor);
+
+ if (pGUIControl)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ static_cast<CGUIRadioButtonControl*>(pGUIControl)->PythonSetLabel(strFont, strText, textColor, shadowColor, focusedColor);
+ static_cast<CGUIRadioButtonControl*>(pGUIControl)->PythonSetDisabledColor(disabledColor);
+ }
+ }
+
+ void ControlRadioButton::setRadioDimension(long x, long y, long width, long height)
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+ if (pGUIControl)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ static_cast<CGUIRadioButtonControl*>(pGUIControl)->SetRadioDimensions((float)dwPosX, (float)dwPosY, (float)dwWidth, (float)dwHeight);
+ }
+ }
+
+ CGUIControl* ControlRadioButton::Create()
+ {
+ CLabelInfo label;
+ label.font = g_fontManager.GetFont(strFont);
+ label.textColor = textColor;
+ label.disabledColor = disabledColor;
+ label.shadowColor = shadowColor;
+ label.focusedColor = focusedColor;
+ label.align = align;
+ label.offsetX = (float)textOffsetX;
+ label.offsetY = (float)textOffsetY;
+ label.angle = (float)-iAngle;
+ pGUIControl = new CGUIRadioButtonControl(
+ iParentId,
+ iControlId,
+ (float)dwPosX,
+ (float)dwPosY,
+ (float)dwWidth,
+ (float)dwHeight,
+ CTextureInfo(strTextureFocus),
+ CTextureInfo(strTextureNoFocus),
+ label,
+ CTextureInfo(strTextureRadioOnFocus),
+ CTextureInfo(strTextureRadioOnNoFocus),
+ CTextureInfo(strTextureRadioOffFocus),
+ CTextureInfo(strTextureRadioOffNoFocus),
+ CTextureInfo(strTextureRadioOnDisabled),
+ CTextureInfo(strTextureRadioOffDisabled));
+ pGUIControl->SetVisible(m_visible);
+
+ CGUIRadioButtonControl* pGuiButtonControl =
+ static_cast<CGUIRadioButtonControl*>(pGUIControl);
+
+ pGuiButtonControl->SetLabel(strText);
+
+ return pGUIControl;
+ }
+
+ // ============================================================
+
+ // ============================================================
+ // ============================================================
+ Control::~Control() { deallocating(); }
+
+ CGUIControl* Control::Create()
+ {
+ throw WindowException("Object is a Control, but can't be added to a window");
+ }
+
+ std::vector<int> Control::getPosition()
+ {
+ std::vector<int> ret(2);
+ ret[0] = dwPosX;
+ ret[1] = dwPosY;
+ return ret;
+ }
+
+ void Control::setEnabled(bool enabled)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ if (pGUIControl)
+ pGUIControl->SetEnabled(enabled);
+ }
+
+ void Control::setVisible(bool visible)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ if (pGUIControl != nullptr)
+ {
+ pGUIControl->SetVisible(visible);
+ }
+ else
+ {
+ m_visible = visible;
+ }
+ }
+
+ bool Control::isVisible()
+ {
+ DelayedCallGuard dcguard(languageHook);
+ XBMCAddonUtils::GuiLock(languageHook, false);
+ if (pGUIControl)
+ return pGUIControl->IsVisible();
+ else
+ return false;
+ }
+
+ void Control::setVisibleCondition(const char* visible, bool allowHiddenFocus)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+
+ if (pGUIControl)
+ pGUIControl->SetVisibleCondition(visible, allowHiddenFocus ? "true" : "false");
+ }
+
+ void Control::setEnableCondition(const char* enable)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+
+ if (pGUIControl)
+ pGUIControl->SetEnableCondition(enable);
+ }
+
+ void Control::setAnimations(const std::vector< Tuple<String,String> >& eventAttr)
+ {
+ CXBMCTinyXML xmlDoc;
+ TiXmlElement xmlRootElement("control");
+ TiXmlNode *pRoot = xmlDoc.InsertEndChild(xmlRootElement);
+ if (!pRoot)
+ throw WindowException("TiXmlNode creation error");
+
+ std::vector<CAnimation> animations;
+
+ for (unsigned int anim = 0; anim < eventAttr.size(); anim++)
+ {
+ const Tuple<String,String>& pTuple = eventAttr[anim];
+
+ if (pTuple.GetNumValuesSet() != 2)
+ throw WindowException("Error unpacking tuple found in list");
+
+ const String& cEvent = pTuple.first();
+ const String& cAttr = pTuple.second();
+
+ TiXmlElement pNode("animation");
+ std::vector<std::string> attrs = StringUtils::Split(cAttr, " ");
+ for (const auto& i : attrs)
+ {
+ std::vector<std::string> attrs2 = StringUtils::Split(i, "=");
+ if (attrs2.size() == 2)
+ pNode.SetAttribute(attrs2[0], attrs2[1]);
+ }
+ TiXmlText value(cEvent.c_str());
+ pNode.InsertEndChild(value);
+ pRoot->InsertEndChild(pNode);
+ }
+
+ const CRect animRect((float)dwPosX, (float)dwPosY, (float)dwPosX + dwWidth, (float)dwPosY + dwHeight);
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ if (pGUIControl)
+ {
+ CGUIControlFactory::GetAnimations(pRoot, animRect, iParentId, animations);
+ pGUIControl->SetAnimations(animations);
+ }
+ }
+
+ void Control::setPosition(long x, long y)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ dwPosX = x;
+ dwPosY = y;
+ if (pGUIControl)
+ pGUIControl->SetPosition((float)dwPosX, (float)dwPosY);
+ }
+
+ void Control::setWidth(long width)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ dwWidth = width;
+ if (pGUIControl)
+ pGUIControl->SetWidth((float)dwWidth);
+ }
+
+ void Control::setHeight(long height)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ dwHeight = height;
+ if (pGUIControl)
+ pGUIControl->SetHeight((float)dwHeight);
+ }
+
+ void Control::setNavigation(const Control* up, const Control* down,
+ const Control* left, const Control* right)
+ {
+ if(iControlId == 0)
+ throw WindowException("Control has to be added to a window first");
+
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ if (pGUIControl)
+ {
+ pGUIControl->SetAction(ACTION_MOVE_UP, CGUIAction(up->iControlId));
+ pGUIControl->SetAction(ACTION_MOVE_DOWN, CGUIAction(down->iControlId));
+ pGUIControl->SetAction(ACTION_MOVE_LEFT, CGUIAction(left->iControlId));
+ pGUIControl->SetAction(ACTION_MOVE_RIGHT, CGUIAction(right->iControlId));
+ }
+ }
+ }
+
+ void Control::controlUp(const Control* control)
+ {
+ if(iControlId == 0)
+ throw WindowException("Control has to be added to a window first");
+
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ if (pGUIControl)
+ pGUIControl->SetAction(ACTION_MOVE_UP, CGUIAction(control->iControlId));
+ }
+ }
+
+ void Control::controlDown(const Control* control)
+ {
+ if(iControlId == 0)
+ throw WindowException("Control has to be added to a window first");
+
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ if (pGUIControl)
+ pGUIControl->SetAction(ACTION_MOVE_DOWN, CGUIAction(control->iControlId));
+ }
+ }
+
+ void Control::controlLeft(const Control* control)
+ {
+ if(iControlId == 0)
+ throw WindowException("Control has to be added to a window first");
+
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ if (pGUIControl)
+ pGUIControl->SetAction(ACTION_MOVE_LEFT, CGUIAction(control->iControlId));
+ }
+ }
+
+ void Control::controlRight(const Control* control)
+ {
+ if(iControlId == 0)
+ throw WindowException("Control has to be added to a window first");
+
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ if (pGUIControl)
+ pGUIControl->SetAction(ACTION_MOVE_RIGHT, CGUIAction(control->iControlId));
+ }
+ }
+
+ // ============================================================
+ // ControlSpin
+ // ============================================================
+ ControlSpin::ControlSpin()
+ {
+ // default values for spin control
+ color = 0xffffffff;
+ dwPosX = 0;
+ dwPosY = 0;
+ dwWidth = 16;
+ dwHeight = 16;
+
+ // get default images
+ strTextureUp = XBMCAddonUtils::getDefaultImage("listcontrol", "textureup");
+ strTextureDown = XBMCAddonUtils::getDefaultImage("listcontrol", "texturedown");
+ strTextureUpFocus = XBMCAddonUtils::getDefaultImage("listcontrol", "textureupfocus");
+ strTextureDownFocus = XBMCAddonUtils::getDefaultImage("listcontrol", "texturedownfocus");
+ strTextureUpDisabled = XBMCAddonUtils::getDefaultImage("listcontrol", "textureupdisabled");
+ strTextureDownDisabled = XBMCAddonUtils::getDefaultImage("listcontrol", "texturedowndisabled");
+ }
+
+ void ControlSpin::setTextures(const char* up, const char* down,
+ const char* upFocus,
+ const char* downFocus,
+ const char* upDisabled,
+ const char* downDisabled)
+ {
+ strTextureUp = up;
+ strTextureDown = down;
+ strTextureUpFocus = upFocus;
+ strTextureDownFocus = downFocus;
+ strTextureUpDisabled = upDisabled;
+ strTextureDownDisabled = downDisabled;
+ /*
+ PyXBMCGUILock();
+ if (self->pGUIControl)
+ {
+ CGUISpinControl* pControl = (CGUISpinControl*)self->pGUIControl;
+ pControl->se
+ PyXBMCGUIUnlock();
+ */
+ }
+
+ ControlSpin::~ControlSpin() = default;
+ // ============================================================
+
+ // ============================================================
+ // ControlLabel
+ // ============================================================
+ ControlLabel::ControlLabel(long x, long y, long width, long height,
+ const String& label,
+ const char* font, const char* p_textColor,
+ const char* p_disabledColor,
+ long p_alignment,
+ bool hasPath, long angle) :
+ strFont("font13"),
+ textColor(0xffffffff), disabledColor(0x60ffffff),
+ align(p_alignment), bHasPath(hasPath), iAngle(angle)
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+
+ strText = label;
+ if (font)
+ strFont = font;
+
+ if (p_textColor)
+ sscanf(p_textColor, "%x", &textColor);
+
+ if (p_disabledColor)
+ sscanf( p_disabledColor, "%x", &disabledColor );
+ }
+
+ ControlLabel::~ControlLabel() = default;
+
+ CGUIControl* ControlLabel::Create()
+ {
+ CLabelInfo label;
+ label.font = g_fontManager.GetFont(strFont);
+ label.textColor = label.focusedColor = textColor;
+ label.disabledColor = disabledColor;
+ label.align = align;
+ label.angle = (float)-iAngle;
+ pGUIControl = new CGUILabelControl(
+ iParentId,
+ iControlId,
+ (float)dwPosX,
+ (float)dwPosY,
+ (float)dwWidth,
+ (float)dwHeight,
+ label,
+ false,
+ bHasPath);
+ pGUIControl->SetVisible(m_visible);
+ static_cast<CGUILabelControl*>(pGUIControl)->SetLabel(strText);
+ return pGUIControl;
+ }
+
+ void ControlLabel::setLabel(const String& label, const char* font,
+ const char* textColor, const char* disabledColor,
+ const char* shadowColor, const char* focusedColor,
+ const String& label2)
+ {
+ strText = label;
+ CGUIMessage msg(GUI_MSG_LABEL_SET, iParentId, iControlId);
+ msg.SetLabel(strText);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iParentId);
+ }
+
+ String ControlLabel::getLabel()
+ {
+ return strText;
+ }
+ // ============================================================
+
+ // ============================================================
+ // ControlEdit
+ // ============================================================
+ ControlEdit::ControlEdit(long x, long y, long width, long height, const String& label,
+ const char* font, const char* _textColor,
+ const char* _disabledColor,
+ long _alignment, const char* focusTexture,
+ const char* noFocusTexture) :
+ strFont("font13"), textColor(0xffffffff), disabledColor(0x60ffffff),
+ align(_alignment)
+
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+
+ strTextureFocus = focusTexture ? focusTexture :
+ XBMCAddonUtils::getDefaultImage("edit", "texturefocus");
+
+ strTextureNoFocus = noFocusTexture ? noFocusTexture :
+ XBMCAddonUtils::getDefaultImage("edit", "texturenofocus");
+
+ if (!label.empty())
+ {
+ strText = label;
+ }
+ if (font) strFont = font;
+ if (_textColor) sscanf( _textColor, "%x", &textColor );
+ if (_disabledColor) sscanf( _disabledColor, "%x", &disabledColor );
+ }
+
+ CGUIControl* ControlEdit::Create()
+ {
+ CLabelInfo label;
+ label.font = g_fontManager.GetFont(strFont);
+ label.textColor = label.focusedColor = textColor;
+ label.disabledColor = disabledColor;
+ label.align = align;
+ pGUIControl = new CGUIEditControl(
+ iParentId,
+ iControlId,
+ (float)dwPosX,
+ (float)dwPosY,
+ (float)dwWidth,
+ (float)dwHeight,
+ CTextureInfo(strTextureFocus),
+ CTextureInfo(strTextureNoFocus),
+ label,
+ strText);
+ pGUIControl->SetVisible(m_visible);
+
+ // set label
+ CGUIMessage msg(GUI_MSG_LABEL_SET, iParentId, iControlId);
+ msg.SetLabel(strText);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iParentId);
+
+ return pGUIControl;
+ }
+
+ void ControlEdit::setLabel(const String& label, const char* font,
+ const char* textColor, const char* disabledColor,
+ const char* shadowColor, const char* focusedColor,
+ const String& label2)
+ {
+ strText = label;
+ CGUIMessage msg(GUI_MSG_LABEL_SET, iParentId, iControlId);
+ msg.SetLabel(strText);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iParentId);
+ }
+
+ String ControlEdit::getLabel()
+ {
+ return strText;
+ }
+
+ void ControlEdit::setText(const String& text)
+ {
+ // create message
+ CGUIMessage msg(GUI_MSG_LABEL2_SET, iParentId, iControlId);
+ msg.SetLabel(text);
+
+ // send message
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iParentId);
+ }
+
+ String ControlEdit::getText()
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, iParentId, iControlId);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg, iParentId);
+
+ return msg.GetLabel();
+ }
+
+ void ControlEdit::setType(int type, const String& heading)
+ {
+ if (pGUIControl)
+ {
+ XBMCAddonUtils::GuiLock(languageHook, false);
+ static_cast<CGUIEditControl*>(pGUIControl)->SetInputType(static_cast<CGUIEditControl::INPUT_TYPE>(type), CVariant{heading});
+ }
+ }
+
+ // ============================================================
+ // ControlList
+ // ============================================================
+ ControlList::ControlList(long x, long y, long width, long height, const char* font,
+ const char* ctextColor, const char* cbuttonTexture,
+ const char* cbuttonFocusTexture,
+ const char* cselectedColor,
+ long _imageWidth, long _imageHeight, long _itemTextXOffset,
+ long _itemTextYOffset, long _itemHeight, long _space, long _alignmentY) :
+ strFont("font13"),
+ textColor(0xe0f0f0f0), selectedColor(0xffffffff),
+ imageHeight(_imageHeight), imageWidth(_imageWidth),
+ itemHeight(_itemHeight), space(_space),
+ itemTextOffsetX(_itemTextXOffset),itemTextOffsetY(_itemTextYOffset),
+ alignmentY(_alignmentY)
+ {
+ dwPosX = x;
+ dwPosY = y;
+ dwWidth = width;
+ dwHeight = height;
+
+ // create a python spin control
+ pControlSpin = new ControlSpin();
+
+ // initialize default values
+ if (font)
+ strFont = font;
+
+ if (ctextColor)
+ sscanf( ctextColor, "%x", &textColor );
+
+ if (cselectedColor)
+ sscanf( cselectedColor, "%x", &selectedColor );
+
+ strTextureButton = cbuttonTexture ? cbuttonTexture :
+ XBMCAddonUtils::getDefaultImage("listcontrol", "texturenofocus");
+
+ strTextureButtonFocus = cbuttonFocusTexture ? cbuttonFocusTexture :
+ XBMCAddonUtils::getDefaultImage("listcontrol", "texturefocus");
+
+ // default values for spin control
+ pControlSpin->dwPosX = dwWidth - 35;
+ pControlSpin->dwPosY = dwHeight - 15;
+ }
+
+ ControlList::~ControlList() = default;
+
+ CGUIControl* ControlList::Create()
+ {
+ CLabelInfo label;
+ label.align = alignmentY;
+ label.font = g_fontManager.GetFont(strFont);
+ label.textColor = label.focusedColor = textColor;
+ //label.shadowColor = shadowColor;
+ label.selectedColor = selectedColor;
+ label.offsetX = (float)itemTextOffsetX;
+ label.offsetY = (float)itemTextOffsetY;
+ // Second label should have the same font, alignment, and colours as the first, but
+ // the offsets should be 0.
+ CLabelInfo label2 = label;
+ label2.offsetX = label2.offsetY = 0;
+ label2.align |= XBFONT_RIGHT;
+
+ pGUIControl = new CGUIListContainer(
+ iParentId,
+ iControlId,
+ (float)dwPosX,
+ (float)dwPosY,
+ (float)dwWidth,
+ (float)dwHeight - pControlSpin->dwHeight - 5,
+ label, label2,
+ CTextureInfo(strTextureButton),
+ CTextureInfo(strTextureButtonFocus),
+ (float)itemHeight,
+ (float)imageWidth, (float)imageHeight,
+ (float)space);
+ pGUIControl->SetVisible(m_visible);
+ return pGUIControl;
+ }
+
+ void ControlList::addItem(const Alternative<String, const XBMCAddon::xbmcgui::ListItem* > & item, bool sendMessage)
+ {
+ XBMC_TRACE;
+
+ if (item.which() == first)
+ internAddListItem(ListItem::fromString(item.former()),sendMessage);
+ else
+ internAddListItem(item.later(),sendMessage);
+ }
+
+ void ControlList::addItems(const std::vector<Alternative<String, const XBMCAddon::xbmcgui::ListItem* > > & items)
+ {
+ XBMC_TRACE;
+
+ for (const auto& iter : items)
+ addItem(iter, false);
+ sendLabelBind(vecItems.size());
+ }
+
+ void ControlList::internAddListItem(const AddonClass::Ref<ListItem>& pListItem,
+ bool sendMessage)
+ {
+ if (pListItem.isNull())
+ throw WindowException("NULL ListItem passed to ControlList::addListItem");
+
+ // add item to objects vector
+ vecItems.push_back(pListItem);
+
+ // send all of the items ... this is what it did before.
+ if (sendMessage)
+ sendLabelBind(vecItems.size());
+ }
+
+ void ControlList::sendLabelBind(int tail)
+ {
+ // construct a CFileItemList to pass 'em on to the list
+ CGUIListItemPtr items(new CFileItemList());
+ for (unsigned int i = vecItems.size() - tail; i < vecItems.size(); i++)
+ static_cast<CFileItemList*>(items.get())->Add(vecItems[i]->item);
+
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, iParentId, iControlId, 0, 0, items);
+ msg.SetPointer(items.get());
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iParentId);
+ }
+
+ void ControlList::selectItem(long item)
+ {
+ // create message
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, iParentId, iControlId, item);
+
+ // send message
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iParentId);
+ }
+
+ void ControlList::removeItem(int index)
+ {
+ if (index < 0 || index >= (int)vecItems.size())
+ throw WindowException("Index out of range");
+
+ vecItems.erase(vecItems.begin() + index);
+
+ sendLabelBind(vecItems.size());
+ }
+
+ void ControlList::reset()
+ {
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, iParentId, iControlId);
+
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iParentId);
+
+ // delete all items from vector
+ // delete all ListItem from vector
+ vecItems.clear(); // this should delete all of the objects
+ }
+
+ Control* ControlList::getSpinControl()
+ {
+ return pControlSpin;
+ }
+
+ long ControlList::getSelectedPosition()
+ {
+ DelayedCallGuard dcguard(languageHook);
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+
+ // create message
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, iParentId, iControlId);
+ long pos = -1;
+
+ // send message
+ if (!vecItems.empty() && pGUIControl)
+ {
+ pGUIControl->OnMessage(msg);
+ pos = msg.GetParam1();
+ }
+
+ return pos;
+ }
+
+ XBMCAddon::xbmcgui::ListItem* ControlList::getSelectedItem()
+ {
+ DelayedCallGuard dcguard(languageHook);
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+
+ // create message
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, iParentId, iControlId);
+ AddonClass::Ref<ListItem> pListItem = NULL;
+
+ // send message
+ if (!vecItems.empty() && pGUIControl)
+ {
+ pGUIControl->OnMessage(msg);
+ if (msg.GetParam1() >= 0 && (size_t)msg.GetParam1() < vecItems.size())
+ pListItem = vecItems[msg.GetParam1()];
+ }
+
+ return pListItem.get();
+ }
+
+ void ControlList::setImageDimensions(long imageWidth,long imageHeight)
+ {
+ CLog::Log(LOGWARNING,"ControlList::setImageDimensions was called but ... it currently isn't defined to do anything.");
+ /*
+ PyXBMCGUILock();
+ if (self->pGUIControl)
+ {
+ CGUIListControl* pListControl = (CGUIListControl*) self->pGUIControl;
+ pListControl->SetImageDimensions((float)self->dwImageWidth, (float)self->dwImageHeight );
+ }
+ PyXBMCGUIUnlock();
+ */
+ }
+
+ void ControlList::setItemHeight(long height)
+ {
+ CLog::Log(LOGWARNING,"ControlList::setItemHeight was called but ... it currently isn't defined to do anything.");
+ /*
+ PyXBMCGUILock();
+ if (self->pGUIControl)
+ {
+ CGUIListControl* pListControl = (CGUIListControl*) self->pGUIControl;
+ pListControl->SetItemHeight((float)self->dwItemHeight);
+ }
+ PyXBMCGUIUnlock();
+ */
+ }
+
+ void ControlList::setSpace(int space)
+ {
+ CLog::Log(LOGWARNING,"ControlList::setSpace was called but ... it currently isn't defined to do anything.");
+ /*
+ PyXBMCGUILock();
+ if (self->pGUIControl)
+ {
+ CGUIListControl* pListControl = (CGUIListControl*) self->pGUIControl;
+ pListControl->SetSpaceBetweenItems((float)self->dwSpace);
+ }
+ PyXBMCGUIUnlock();
+ */
+ }
+
+ void ControlList::setPageControlVisible(bool visible)
+ {
+ CLog::Log(LOGWARNING,"ControlList::setPageControlVisible was called but ... it currently isn't defined to do anything.");
+
+ // char isOn = true;
+
+ /*
+ PyXBMCGUILock();
+ if (self->pGUIControl)
+ {
+ ((CGUIListControl*)self->pGUIControl)->SetPageControlVisible((bool)isOn );
+ }
+ PyXBMCGUIUnlock();
+ */
+ }
+
+ long ControlList::size()
+ {
+ return (long)vecItems.size();
+ }
+
+ long ControlList::getItemHeight()
+ {
+ return (long)itemHeight;
+ }
+
+ long ControlList::getSpace()
+ {
+ return (long)space;
+ }
+
+ XBMCAddon::xbmcgui::ListItem* ControlList::getListItem(int index)
+ {
+ if (index < 0 || index >= (int)vecItems.size())
+ throw WindowException("Index out of range");
+
+ AddonClass::Ref<ListItem> pListItem = vecItems[index];
+ return pListItem.get();
+ }
+
+ void ControlList::setStaticContent(const ListItemList* pitems)
+ {
+ const ListItemList& vecItems = *pitems;
+
+ std::vector<CGUIStaticItemPtr> items;
+
+ for (unsigned int item = 0; item < vecItems.size(); item++)
+ {
+ ListItem* pItem = vecItems[item];
+
+ // NOTE: This code has likely not worked fully correctly for some time
+ // In particular, the click behaviour won't be working.
+ CGUIStaticItemPtr newItem(new CGUIStaticItem(*pItem->item));
+ items.push_back(newItem);
+ }
+
+ // set static list
+ std::unique_ptr<IListProvider> provider = std::make_unique<CStaticListProvider>(items);
+ static_cast<CGUIBaseContainer*>(pGUIControl)->SetListProvider(std::move(provider));
+ }
+
+ // ============================================================
+
+ }
+}
diff --git a/xbmc/interfaces/legacy/Control.h b/xbmc/interfaces/legacy/Control.h
new file mode 100644
index 0000000..7441310
--- /dev/null
+++ b/xbmc/interfaces/legacy/Control.h
@@ -0,0 +1,2960 @@
+/*
+ * 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 "Alternative.h"
+#include "ListItem.h"
+#include "Tuple.h"
+#include "guilib/GUIControl.h"
+#include "guilib/GUIFont.h"
+#include "input/Key.h"
+#include "swighelper.h"
+#include "utils/ColorUtils.h"
+
+#include <vector>
+
+
+// hardcoded offsets for button controls (and controls that use button controls)
+// ideally they should be dynamically read in as with all the other properties.
+#define CONTROL_TEXT_OFFSET_X 10
+#define CONTROL_TEXT_OFFSET_Y 2
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+
+ /// \defgroup python_xbmcgui_control Control
+ /// \ingroup python_xbmcgui
+ /// @{
+ /// @brief **Code based skin access.**
+ ///
+ /// Offers classes and functions that manipulate the add-on gui controls.
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// \python_class{ Control() }
+ ///
+ /// **Code based skin access.**
+ ///
+ /// Kodi is noted as having a very flexible and robust framework for its
+ /// GUI, making theme-skinning and personal customization very accessible.
+ /// Users can create their own skin (or modify an existing skin) and share
+ /// it with others.
+ ///
+ /// Kodi includes a new GUI library written from scratch. This library
+ /// allows you to skin/change everything you see in Kodi, from the images,
+ /// the sizes and positions of all controls, colours, fonts, and text,
+ /// through to altering navigation and even adding new functionality. The
+ /// skin system is quite complex, and this portion of the manual is dedicated
+ /// to providing in depth information on how it all works, along with tips
+ /// to make the experience a little more pleasant.
+ ///
+ ///-------------------------------------------------------------------------
+ //
+ class Control : public AddonClass
+ {
+ protected:
+ Control() = default;
+
+ public:
+ ~Control() override;
+
+#ifndef SWIG
+ virtual CGUIControl* Create();
+#endif
+
+ // currently we only accept messages from a button or controllist with a select action
+ virtual bool canAcceptMessages(int actionId) { return false; }
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ getId() }
+ /// Returns the control's current id as an integer.
+ ///
+ /// @return int - Current id
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// id = self.button.getId()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getId()
+#else
+ virtual int getId() { return iControlId; }
+#endif
+
+ inline bool operator==(const Control& other) const { return iControlId == other.iControlId; }
+ inline bool operator>(const Control& other) const { return iControlId > other.iControlId; }
+ inline bool operator<(const Control& other) const { return iControlId < other.iControlId; }
+
+ // hack this because it returns a tuple
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ getPosition() }
+ /// Returns the control's current position as a x,y integer tuple.
+ ///
+ /// @return Current position as a x,y integer tuple
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// pos = self.button.getPosition()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getPosition();
+#else
+ virtual std::vector<int> getPosition();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ getX() }
+ /// Returns the control's current X position.
+ ///
+ /// @return int - Current X position
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// posX = self.button.getX()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getX();
+#else
+ int getX() { return dwPosX; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ getY() }
+ /// Returns the control's current Y position.
+ ///
+ /// @return int - Current Y position
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// posY = self.button.getY()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getY();
+#else
+ int getY() { return dwPosY; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ getHeight() }
+ /// Returns the control's current height as an integer.
+ ///
+ /// @return int - Current height
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// height = self.button.getHeight()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getHeight();
+#else
+ virtual int getHeight() { return dwHeight; }
+#endif
+
+ // getWidth() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ getWidth() }
+ /// Returns the control's current width as an integer.
+ ///
+ /// @return int - Current width
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// width = self.button.getWidth()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getWidth();
+#else
+ virtual int getWidth() { return dwWidth; }
+#endif
+
+ // setEnabled() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ setEnabled(enabled) }
+ /// Sets the control's enabled/disabled state.
+ ///
+ /// @param enabled bool - True=enabled / False=disabled.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.button.setEnabled(False)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setEnabled(...);
+#else
+ virtual void setEnabled(bool enabled);
+#endif
+
+ // setVisible() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ setVisible(visible) }
+ /// Sets the control's visible/hidden state.
+ /// \anchor python_xbmcgui_control_setVisible
+ ///
+ /// @param visible bool - True=visible / False=hidden.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v19 You can now define the visible state of a control before it being
+ /// added to a window. This value will be taken into account when the control is later
+ /// added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.button.setVisible(False)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setVisible(...);
+#else
+ virtual void setVisible(bool visible);
+#endif
+
+ // isVisible() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ isVisible() }
+ /// Get the control's visible/hidden state with respect to the container/window
+ ///
+ /// @note If a given control is set visible (c.f. \ref python_xbmcgui_control_setVisible "setVisible()"
+ /// but was not yet added to a window, this method will return `False` (the control is not visible yet since
+ /// it was not added to the window).
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// if self.button.isVisible():
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ isVisible(...);
+#else
+ virtual bool isVisible();
+#endif
+
+ // setVisibleCondition() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ setVisibleCondition(visible[,allowHiddenFocus]) }
+ /// Sets the control's visible condition.
+ ///
+ /// Allows Kodi to control the visible status of the control.
+ ///
+ /// [List of Conditions](http://kodi.wiki/view/List_of_Boolean_Conditions)
+ ///
+ /// @param visible string - Visible condition
+ /// @param allowHiddenFocus [opt] bool - True=gains focus even if
+ /// hidden
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setVisibleCondition(visible[,allowHiddenFocus])
+ /// self.button.setVisibleCondition('[Control.IsVisible(41) + !Control.IsVisible(12)]', True)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setVisibleCondition(...);
+#else
+ virtual void setVisibleCondition(const char* visible, bool allowHiddenFocus = false);
+#endif
+
+ // setEnableCondition() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ setEnableCondition(enable) }
+ /// Sets the control's enabled condition.
+ ///
+ /// Allows Kodi to control the enabled status of the control.
+ ///
+ /// [List of Conditions](http://kodi.wiki/view/List_of_Boolean_Conditions)
+ ///
+ /// @param enable string - Enable condition.
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setEnableCondition(enable)
+ /// self.button.setEnableCondition('System.InternetState')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setEnableCondition(...);
+#else
+ virtual void setEnableCondition(const char* enable);
+#endif
+
+ // setAnimations() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ setAnimations([(event, attr,)*]) }
+ /// Sets the control's animations.
+ ///
+ /// <b>[(event,attr,)*]</b>: list - A list of tuples consisting of event
+ /// and attributes pairs.
+ ///
+ /// [Animating your skin](http://kodi.wiki/view/Animating_Your_Skin)
+ ///
+ /// @param event string - The event to animate.
+ /// @param attr string - The whole attribute string
+ /// separated by spaces.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setAnimations([(event, attr,)*])
+ /// self.button.setAnimations([('focus', 'effect=zoom end=90,247,220,56 time=0',)])
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setAnimations(...);
+#else
+ virtual void setAnimations(const std::vector< Tuple<String,String> >& eventAttr);
+#endif
+
+ // setPosition() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ setPosition(x, y) }
+ /// Sets the controls position.
+ ///
+ /// @param x integer - x coordinate of control.
+ /// @param y integer - y coordinate of control.
+ ///
+ /// @note You may use negative integers. (e.g sliding a control into view)
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.button.setPosition(100, 250)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setPosition(...);
+#else
+ virtual void setPosition(long x, long y);
+#endif
+
+ // setWidth() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ setWidth(width) }
+ /// Sets the controls width.
+ ///
+ /// @param width integer - width of control.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.image.setWidth(100)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setWidth(...);
+#else
+ virtual void setWidth(long width);
+#endif
+
+ // setHeight() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ setHeight(height) }
+ /// Sets the controls height.
+ ///
+ /// @param height integer - height of control.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.image.setHeight(100)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setHeight(...);
+#else
+ virtual void setHeight(long height);
+#endif
+
+ // setNavigation() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ setNavigation(up, down, left, right) }
+ /// Sets the controls navigation.
+ ///
+ /// @param up control object - control to navigate to on up.
+ /// @param down control object - control to navigate to on down.
+ /// @param left control object - control to navigate to on left.
+ /// @param right control object - control to navigate to on right.
+ /// @throw TypeError if one of the supplied arguments is not a
+ /// control type.
+ /// @throw ReferenceError if one of the controls is not added to a
+ /// window.
+ ///
+ /// @note Same as controlUp(), controlDown(), controlLeft(), controlRight().
+ /// Set to self to disable navigation for that direction.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.button.setNavigation(self.button1, self.button2, self.button3, self.button4)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ //
+ setNavigation(...);
+#else
+ virtual void setNavigation(const Control* up, const Control* down,
+ const Control* left, const Control* right);
+#endif
+
+ // controlUp() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ controlUp(control) }
+ /// Sets the controls up navigation.
+ ///
+ /// @param control control object - control to navigate to on up.
+ /// @throw TypeError if one of the supplied arguments is not a
+ /// control type.
+ /// @throw ReferenceError if one of the controls is not added to a
+ /// window.
+ ///
+ ///
+ /// @note You can also use setNavigation(). Set to self to disable navigation.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.button.controlUp(self.button1)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ controlUp(...);
+#else
+ virtual void controlUp(const Control* up);
+#endif
+
+ // controlDown() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ controlDown(control) }
+ /// Sets the controls down navigation.
+ ///
+ /// @param control control object - control to navigate to on down.
+ /// @throw TypeError if one of the supplied arguments is not a
+ /// control type.
+ /// @throw ReferenceError if one of the controls is not added to a
+ /// window.
+ ///
+ ///
+ /// @note You can also use setNavigation(). Set to self to disable navigation.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.button.controlDown(self.button1)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ controlDown(...);
+#else
+ virtual void controlDown(const Control* control);
+#endif
+
+ // controlLeft() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ controlLeft(control) }
+ /// Sets the controls left navigation.
+ ///
+ /// @param control control object - control to navigate to on left.
+ /// @throw TypeError if one of the supplied arguments is not a
+ /// control type.
+ /// @throw ReferenceError if one of the controls is not added to a
+ /// window.
+ ///
+ ///
+ /// @note You can also use setNavigation(). Set to self to disable navigation.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.button.controlLeft(self.button1)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ controlLeft(...);
+#else
+ virtual void controlLeft(const Control* control);
+#endif
+
+ // controlRight() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control
+ /// @brief \python_func{ controlRight(control) }
+ /// Sets the controls right navigation.
+ ///
+ /// @param control control object - control to navigate to on right.
+ /// @throw TypeError if one of the supplied arguments is not a
+ /// control type.
+ /// @throw ReferenceError if one of the controls is not added to a
+ /// window.
+ ///
+ ///
+ /// @note You can also use setNavigation(). Set to self to disable navigation.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.button.controlRight(self.button1)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ controlRight(...);
+#else
+ virtual void controlRight(const Control* control);
+#endif
+
+#ifndef SWIG
+ int iControlId = 0;
+ int iParentId = 0;
+ int dwPosX = 0;
+ int dwPosY = 0;
+ int dwWidth = 0;
+ int dwHeight = 0;
+ int iControlUp = 0;
+ int iControlDown = 0;
+ int iControlLeft = 0;
+ int iControlRight = 0;
+ std::string m_label{};
+ bool m_visible{true};
+ CGUIControl* pGUIControl = nullptr;
+#endif
+
+ };
+ /// @}
+
+ /// \defgroup python_xbmcgui_control_spin Subclass - ControlSpin
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// @brief **Used for cycling up/down controls.**
+ ///
+ /// Offers classes and functions that manipulate the add-on gui controls.
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// \python_class{ ControlSpin() }
+ ///
+ /// **Code based skin access.**
+ ///
+ /// The spin control is used for when a list of options can be chosen (such
+ /// as a page up/down control). You can choose the position, size, and look
+ /// of the spin control.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @warning **Not working yet**.
+ /// You can't create this object, it is returned by objects like ControlTextBox and ControlList.
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ ///
+ class ControlSpin : public Control
+ {
+ public:
+ ~ControlSpin() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_spin
+ /// @brief \python_func{ setTextures(up, down, upFocus, downFocus) }
+ /// Sets textures for this control.
+ ///
+ /// Texture are image files that are used for example in the skin
+ ///
+ /// @warning **Not working yet**.
+ ///
+ /// @param up label - for the up arrow
+ /// when it doesn't have focus.
+ /// @param down label - for the down button
+ /// when it is not focused.
+ /// @param upFocus label - for the up button
+ /// when it has focus.
+ /// @param downFocus label - for the down button
+ /// when it has focus.
+ /// @param upDisabled label - for the up arrow
+ /// when the button is disabled.
+ /// @param downDisabled label - for the up arrow
+ /// when the button is disabled.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setTextures(up, down, upFocus, downFocus, upDisabled, downDisabled)
+ ///
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setTextures(...);
+#else
+ virtual void setTextures(const char* up, const char* down,
+ const char* upFocus,
+ const char* downFocus,
+ const char* upDisabled, const char* downDisabled);
+#endif
+
+#ifndef SWIG
+ UTILS::COLOR::Color color;
+ std::string strTextureUp;
+ std::string strTextureDown;
+ std::string strTextureUpFocus;
+ std::string strTextureDownFocus;
+ std::string strTextureUpDisabled;
+ std::string strTextureDownDisabled;
+#endif
+
+ private:
+ ControlSpin();
+
+ friend class Window;
+ friend class ControlList;
+
+ };
+ /// @}
+
+ /// \defgroup python_xbmcgui_control_label Subclass - ControlLabel
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// @brief **Used to show some lines of text.**
+ ///
+ /// \python_class{ ControlLabel(x, y, width, height, label[, font, textColor,
+ /// disabledColor, alignment, hasPath, angle]) }
+ ///
+ /// The label control is used for displaying text in Kodi. You can choose
+ /// the font, size, colour, location and contents of the text to be
+ /// displayed.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @param x integer - x coordinate of control.
+ /// @param y integer - y coordinate of control.
+ /// @param width integer - width of control.
+ /// @param height integer - height of control.
+ /// @param label string or unicode - text string.
+ /// @param font [opt] string - font used for label
+ /// text. (e.g. 'font13')
+ /// @param textColor [opt] hexstring - color of enabled
+ /// label's label. (e.g. '0xFFFFFFFF')
+ /// @param disabledColor [opt] hexstring - color of disabled
+ /// label's label. (e.g. '0xFFFF3300')
+ /// @param alignment [opt] integer - alignment of label
+ /// - \ref kodi_gui_font_alignment "Flags for alignment" used as bits to have several together:
+ /// | Definition name | Bitflag | Description |
+ /// |-------------------|:----------:|:------------------------------------|
+ /// | XBFONT_LEFT | 0x00000000 | Align X left
+ /// | XBFONT_RIGHT | 0x00000001 | Align X right
+ /// | XBFONT_CENTER_X | 0x00000002 | Align X center
+ /// | XBFONT_CENTER_Y | 0x00000004 | Align Y center
+ /// | XBFONT_TRUNCATED | 0x00000008 | Truncated text
+ /// | XBFONT_JUSTIFIED | 0x00000010 | Justify text
+ /// @param hasPath [opt] bool - True=stores a
+ /// path / False=no path
+ /// @param angle [opt] integer - angle of control.
+ /// (<b>+</b> rotates CCW, <b>-</b> rotates C)
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # ControlLabel(x, y, width, height, label[, font, textColor,
+ /// # disabledColor, alignment, hasPath, angle])
+ /// self.label = xbmcgui.ControlLabel(100, 250, 125, 75, 'Status', angle=45)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class ControlLabel : public Control
+ {
+ public:
+ ControlLabel(long x, long y, long width, long height, const String& label,
+ const char* font = NULL, const char* textColor = NULL,
+ const char* disabledColor = NULL,
+ long alignment = XBFONT_LEFT,
+ bool hasPath = false, long angle = 0);
+
+ ~ControlLabel() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_label
+ /// @brief \python_func{ getLabel() }
+ /// Returns the text value for this label.
+ ///
+ /// @return This label
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// label = self.label.getLabel()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getLabel();
+#else
+ virtual String getLabel();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_label
+ /// @brief \python_func{ setLabel(label[, font, textColor, disabledColor, shadowColor, focusedColor, label2]) }
+ /// Sets text for this label.
+ ///
+ /// @param label string or unicode - text string.
+ /// @param font [opt] string - font used for label text.
+ /// (e.g. 'font13')
+ /// @param textColor [opt] hexstring - color of enabled
+ /// label's label. (e.g. '0xFFFFFFFF')
+ /// @param disabledColor [opt] hexstring - color of disabled
+ /// label's label. (e.g. '0xFFFF3300')
+ /// @param shadowColor [opt] hexstring - color of button's
+ /// label's shadow. (e.g. '0xFF000000')
+ /// @param focusedColor [opt] hexstring - color of focused
+ /// button's label. (e.g. '0xFF00FFFF')
+ /// @param label2 [opt] string or unicode - text string.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.label.setLabel('Status')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setLabel(...);
+#else
+ virtual void setLabel(const String& label = emptyString,
+ const char* font = NULL,
+ const char* textColor = NULL,
+ const char* disabledColor = NULL,
+ const char* shadowColor = NULL,
+ const char* focusedColor = NULL,
+ const String& label2 = emptyString);
+#endif
+
+#ifndef SWIG
+ ControlLabel() = default;
+
+ std::string strFont;
+ std::string strText;
+ UTILS::COLOR::Color textColor;
+ UTILS::COLOR::Color disabledColor;
+ uint32_t align;
+ bool bHasPath = false;
+ int iAngle = 0;
+
+ CGUIControl* Create() override;
+
+#endif
+ };
+ /// @}
+
+ // ControlEdit class
+ /// \defgroup python_xbmcgui_control_edit Subclass - ControlEdit
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// **Used as an input control for the osd keyboard and other input fields.**
+ ///
+ /// \python_class{ ControlEdit(x, y, width, height, label[, font, textColor,
+ /// disabledColor, alignment, focusTexture, noFocusTexture]) }
+ ///
+ /// The edit control allows a user to input text in Kodi. You can choose the
+ /// font, size, colour, location and header of the text to be displayed.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @param x integer - x coordinate of control.
+ /// @param y integer - y coordinate of control.
+ /// @param width integer - width of control.
+ /// @param height integer - height of control.
+ /// @param label string or unicode - text string.
+ /// @param font [opt] string - font used for label text.
+ /// (e.g. 'font13')
+ /// @param textColor [opt] hexstring - color of enabled
+ /// label's label. (e.g. '0xFFFFFFFF')
+ /// @param disabledColor [opt] hexstring - color of disabled
+ /// label's label. (e.g. '0xFFFF3300')
+ /// @param alignment [opt] integer - alignment of label
+ /// - \ref kodi_gui_font_alignment "Flags for alignment" used as bits to have several together:
+ /// | Definition name | Bitflag | Description |
+ /// |-------------------|:----------:|:------------------------------------|
+ /// | XBFONT_LEFT | 0x00000000 | Align X left
+ /// | XBFONT_RIGHT | 0x00000001 | Align X right
+ /// | XBFONT_CENTER_X | 0x00000002 | Align X center
+ /// | XBFONT_CENTER_Y | 0x00000004 | Align Y center
+ /// | XBFONT_TRUNCATED | 0x00000008 | Truncated text
+ /// | XBFONT_JUSTIFIED | 0x00000010 | Justify text
+ /// @param focusTexture [opt] string - filename for focus texture.
+ /// @param noFocusTexture [opt] string - filename for no focus texture.
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.\n
+ /// After you create the control, you need to add it to the window with
+ /// addControl().\n
+ ///
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ /// @python_v18 Deprecated **isPassword**
+ /// @python_v19 Removed **isPassword**
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.edit = xbmcgui.ControlEdit(100, 250, 125, 75, 'Status')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class ControlEdit : public Control
+ {
+ public:
+ ControlEdit(long x, long y, long width, long height, const String& label,
+ const char* font = NULL, const char* textColor = NULL,
+ const char* disabledColor = NULL,
+ long _alignment = XBFONT_LEFT, const char* focusTexture = NULL,
+ const char* noFocusTexture = NULL);
+
+
+ // setLabel() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_edit
+ /// @brief \python_func{ setLabel(label[, font, textColor, disabledColor, shadowColor, focusedColor, label2]) }
+ /// Sets text heading for this edit control.
+ ///
+ /// @param label string or unicode - text string.
+ /// @param font [opt] string - font used for label text.
+ /// (e.g. 'font13')
+ /// @param textColor [opt] hexstring - color of enabled
+ /// label's label. (e.g. '0xFFFFFFFF')
+ /// @param disabledColor [opt] hexstring - color of disabled
+ /// label's label. (e.g. '0xFFFF3300')
+ /// @param shadowColor [opt] hexstring - color of button's
+ /// label's shadow. (e.g. '0xFF000000')
+ /// @param focusedColor [opt] hexstring - color of focused
+ /// button's label. (e.g. '0xFF00FFFF')
+ /// @param label2 [opt] string or unicode - text string.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.edit.setLabel('Status')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setLabel(...);
+#else
+ virtual void setLabel(const String& label = emptyString,
+ const char* font = NULL,
+ const char* textColor = NULL,
+ const char* disabledColor = NULL,
+ const char* shadowColor = NULL,
+ const char* focusedColor = NULL,
+ const String& label2 = emptyString);
+#endif
+
+ // getLabel() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_edit
+ /// @brief \python_func{ getLabel() }
+ /// Returns the text heading for this edit control.
+ ///
+ /// @return Heading text
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// label = self.edit.getLabel()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getLabel();
+#else
+ virtual String getLabel();
+#endif
+
+ // setText() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_edit
+ /// @brief \python_func{ setText(value) }
+ /// Sets text value for this edit control.
+ ///
+ /// @param value string or unicode - text string.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.edit.setText('online')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setText(...);
+#else
+ virtual void setText(const String& text);
+#endif
+
+ // getText() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_edit
+ /// @brief \python_func{ getText() }
+ /// Returns the text value for this edit control.
+ ///
+ /// @return Text value of control
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v14 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// value = self.edit.getText()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getText();
+#else
+ virtual String getText();
+#endif
+
+#ifndef SWIG
+ ControlEdit() = default;
+
+ std::string strFont;
+ std::string strText;
+ std::string strTextureFocus;
+ std::string strTextureNoFocus;
+ UTILS::COLOR::Color textColor;
+ UTILS::COLOR::Color disabledColor;
+ uint32_t align;
+
+ CGUIControl* Create() override;
+#endif
+
+ // setType() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_edit
+ /// @brief \python_func{ setType(type, heading) }
+ /// Sets the type of this edit control.
+ ///
+ /// @param type integer - type of the edit control.
+ /// | Param | Definition |
+ /// |-----------------------------------------------|:--------------------------------------------|
+ /// | xbmcgui.INPUT_TYPE_TEXT | (standard keyboard)
+ /// | xbmcgui.INPUT_TYPE_NUMBER | (format: #)
+ /// | xbmcgui.INPUT_TYPE_DATE | (format: DD/MM/YYYY)
+ /// | xbmcgui.INPUT_TYPE_TIME | (format: HH:MM)
+ /// | xbmcgui.INPUT_TYPE_IPADDRESS | (format: #.#.#.#)
+ /// | xbmcgui.INPUT_TYPE_PASSWORD | (input is masked)
+ /// | xbmcgui.INPUT_TYPE_PASSWORD_MD5 | (input is masked, return md5 hash of input)
+ /// | xbmcgui.INPUT_TYPE_SECONDS | (format: SS or MM:SS or HH:MM:SS or MM min)
+ /// | xbmcgui.INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW | (numeric input is masked)
+ /// @param heading string or unicode - heading that will be used for to numeric or
+ /// keyboard dialog when the edit control is clicked.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ /// @python_v19 New option added to mask numeric input.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.edit.setType(xbmcgui.INPUT_TYPE_TIME, 'Please enter the time')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setType(...);
+#else
+ virtual void setType(int type, const String& heading);
+#endif
+ };
+ /// @}
+
+ // ControlList class
+ /// \defgroup python_xbmcgui_control_list Subclass - ControlList
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// @brief **Used for a scrolling lists of items. Replaces the list control.**
+ ///
+ /// \python_class{ ControlList(x, y, width, height[, font, textColor, buttonTexture, buttonFocusTexture,
+ /// selectedColor, imageWidth, imageHeight, itemTextXOffset, itemTextYOffset,
+ /// itemHeight, space, alignmentY, shadowColor]) }
+ ///
+ /// The list container is one of several containers used to display items
+ /// from file lists in various ways. The list container is very
+ /// flexible - it's only restriction is that it is a list - i.e. a single
+ /// column or row of items. The layout of the items is very flexible and
+ /// is up to the skinner.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @param x integer - x coordinate of control.
+ /// @param y integer - y coordinate of control.
+ /// @param width integer - width of control.
+ /// @param height integer - height of control.
+ /// @param font [opt] string - font used for items label. (e.g. 'font13')
+ /// @param textColor [opt] hexstring - color of items label. (e.g. '0xFFFFFFFF')
+ /// @param buttonTexture [opt] string - filename for focus texture.
+ /// @param buttonFocusTexture [opt] string - filename for no focus texture.
+ /// @param selectedColor [opt] integer - x offset of label.
+ /// @param imageWidth [opt] integer - width of items icon or thumbnail.
+ /// @param imageHeight [opt] integer - height of items icon or thumbnail.
+ /// @param itemTextXOffset [opt] integer - x offset of items label.
+ /// @param itemTextYOffset [opt] integer - y offset of items label.
+ /// @param itemHeight [opt] integer - height of items.
+ /// @param space [opt] integer - space between items.
+ /// @param alignmentY [opt] integer - Y-axis alignment of items label
+ /// - \ref kodi_gui_font_alignment "Flags for alignment" used as bits to have several together:
+ /// | Definition name | Bitflag | Description |
+ /// |-------------------|:----------:|:------------------------------------|
+ /// | XBFONT_LEFT | 0x00000000 | Align X left
+ /// | XBFONT_RIGHT | 0x00000001 | Align X right
+ /// | XBFONT_CENTER_X | 0x00000002 | Align X center
+ /// | XBFONT_CENTER_Y | 0x00000004 | Align Y center
+ /// | XBFONT_TRUNCATED | 0x00000008 | Truncated text
+ /// | XBFONT_JUSTIFIED | 0x00000010 | Justify text
+ /// @param shadowColor [opt] hexstring - color of items
+ /// label's shadow. (e.g. '0xFF000000')
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.\n
+ /// After you create the control, you need to add it to the window
+ /// with addControl().
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.cList = xbmcgui.ControlList(100, 250, 200, 250, 'font14', space=5)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class ControlList : public Control
+ {
+ void internAddListItem(const AddonClass::Ref<ListItem>& listitem, bool sendMessage);
+
+ public:
+ ControlList(long x, long y, long width, long height, const char* font = NULL,
+ const char* textColor = NULL, const char* buttonTexture = NULL,
+ const char* buttonFocusTexture = NULL,
+ const char* selectedColor = NULL,
+ long _imageWidth=10, long _imageHeight=10, long _itemTextXOffset = CONTROL_TEXT_OFFSET_X,
+ long _itemTextYOffset = CONTROL_TEXT_OFFSET_Y, long _itemHeight = 27, long _space = 2,
+ long _alignmentY = XBFONT_CENTER_Y);
+
+ ~ControlList() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ addItem(item) }
+ /// Add a new item to this list control.
+ ///
+ /// @param item string, unicode or ListItem - item to add.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// cList.addItem('Reboot Kodi')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ addItem(...);
+#else
+ virtual void addItem(const Alternative<String, const XBMCAddon::xbmcgui::ListItem* > & item, bool sendMessage = true);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ addItems(items) }
+ /// Adds a list of listitems or strings to this list control.
+ ///
+ /// @param items List - list of strings, unicode objects or ListItems to add.
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ /// Large lists benefit considerably, than using the standard addItem()
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// cList.addItems(items=listitems)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ addItems(...);
+#else
+ virtual void addItems(const std::vector<Alternative<String, const XBMCAddon::xbmcgui::ListItem* > > & items);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ selectItem(item) }
+ /// Select an item by index number.
+ ///
+ /// @param item integer - index number of the item to select.
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// cList.selectItem(12)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ selectItem(...);
+#else
+ virtual void selectItem(long item);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ removeItem(index) }
+ /// Remove an item by index number.
+ ///
+ /// @param index integer - index number of the item to remove.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v13 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// cList.removeItem(12)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ removeItem(...);
+#else
+ virtual void removeItem(int index);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ reset() }
+ /// Clear all ListItems in this control list.
+ ///
+ /// @warning Calling `reset()` will destroy any `ListItem` objects in the
+ /// `ControlList` if not hold by any other class. Make sure you
+ /// you don't call `addItems()` with the previous `ListItem` references
+ /// after calling `reset()`. If you need to preserve the `ListItem` objects after
+ /// `reset()` make sure you store them as members of your `WindowXML` class (see examples).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Examples:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// cList.reset()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ /// The below example shows you how you can reset the `ControlList` but this time avoiding `ListItem` object
+ /// destruction. The example assumes `self` as a `WindowXMLDialog` instance containing a `ControlList`
+ /// with id = 800. The class preserves the `ListItem` objects in a class member variable.
+ ///
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # Get all the ListItem objects in the control
+ /// self.list_control = self.getControl(800) # ControlList object
+ /// self.listitems = [self.list_control.getListItem(item) for item in range(0, self.list_control.size())]
+ /// # Reset the ControlList control
+ /// self.list_control.reset()
+ /// #
+ /// # do something with your ListItem objects here (e.g. sorting.)
+ /// # ...
+ /// #
+ /// # Add them again to the ControlList
+ /// self.list_control.addItems(self.listitems)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ reset();
+#else
+ virtual void reset();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ getSpinControl() }
+ /// Returns the associated ControlSpin object.
+ ///
+ /// @warning Not working completely yet\n
+ /// After adding this control list to a window it is not possible to change
+ /// the settings of this spin control.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// ctl = cList.getSpinControl()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getSpinControl();
+#else
+ virtual Control* getSpinControl();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ getSelectedPosition() }
+ /// Returns the position of the selected item as an integer.
+ ///
+ /// @note Returns -1 for empty lists.
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// pos = cList.getSelectedPosition()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getSelectedPosition();
+#else
+ virtual long getSelectedPosition();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ getSelectedItem() }
+ /// Returns the selected item as a ListItem object.
+ ///
+ /// @return The selected item
+ ///
+ ///
+ /// @note Same as getSelectedPosition(), but instead of an integer a ListItem object
+ /// is returned. Returns None for empty lists.\n
+ /// See windowexample.py on how to use this.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// item = cList.getSelectedItem()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getSelectedItem();
+#else
+ virtual XBMCAddon::xbmcgui::ListItem* getSelectedItem();
+#endif
+
+ // setImageDimensions() method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ setImageDimensions(imageWidth, imageHeight) }
+ /// Sets the width/height of items icon or thumbnail.
+ ///
+ /// @param imageWidth [opt] integer - width of items icon or thumbnail.
+ /// @param imageHeight [opt] integer - height of items icon or thumbnail.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// cList.setImageDimensions(18, 18)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setImageDimensions(...);
+#else
+ virtual void setImageDimensions(long imageWidth,long imageHeight);
+#endif
+
+ // setItemHeight() method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @brief \python_func{ setItemHeight(itemHeight) }
+ /// Sets the height of items.
+ ///
+ /// @param itemHeight integer - height of items.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// cList.setItemHeight(25)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setItemHeight(...);
+#else
+ virtual void setItemHeight(long height);
+#endif
+
+ // setSpace() method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ setSpace(space) }
+ /// Sets the space between items.
+ ///
+ /// @param space [opt] integer - space between items.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// cList.setSpace(5)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setSpace(...);
+#else
+ virtual void setSpace(int space);
+#endif
+
+ // setPageControlVisible() method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ setPageControlVisible(visible) }
+ /// Sets the spin control's visible/hidden state.
+ ///
+ /// @param visible boolean - True=visible / False=hidden.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// cList.setPageControlVisible(True)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setPageControlVisible(...);
+#else
+ virtual void setPageControlVisible(bool visible);
+#endif
+
+ // size() method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ size() }
+ /// Returns the total number of items in this list control as an integer.
+ ///
+ /// @return Total number of items
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// cnt = cList.size()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ size();
+#else
+ virtual long size();
+#endif
+
+ // getItemHeight() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ getItemHeight() }
+ /// Returns the control's current item height as an integer.
+ ///
+ /// @return Current item heigh
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// item_height = self.cList.getItemHeight()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getItemHeight();
+#else
+ virtual long getItemHeight();
+#endif
+
+ // getSpace() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ getSpace() }
+ /// Returns the control's space between items as an integer.
+ ///
+ /// @return Space between items
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// gap = self.cList.getSpace()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getSpace();
+#else
+ virtual long getSpace();
+#endif
+
+ // getListItem() method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ getListItem(index) }
+ /// Returns a given ListItem in this List.
+ ///
+ /// @param index integer - index number of item to return.
+ /// @return List item
+ /// @throw ValueError if index is out of range.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// listitem = cList.getListItem(6)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getListItem(...);
+#else
+ virtual XBMCAddon::xbmcgui::ListItem* getListItem(int index);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_list
+ /// @brief \python_func{ setStaticContent(items) }
+ /// Fills a static list with a list of listitems.
+ ///
+ /// @param items List - list of listitems to add.
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// cList.setStaticContent(items=listitems)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setStaticContent(...);
+#else
+ virtual void setStaticContent(const ListItemList* items);
+#endif
+
+#ifndef SWIG
+ void sendLabelBind(int tail);
+
+ bool canAcceptMessages(int actionId) override
+ { return ((actionId == ACTION_SELECT_ITEM) | (actionId == ACTION_MOUSE_LEFT_CLICK)); }
+
+ // This is called from AddonWindow.cpp but shouldn't be available
+ // to the scripting languages.
+ ControlList() = default;
+
+ std::vector<AddonClass::Ref<ListItem> > vecItems;
+ std::string strFont;
+ AddonClass::Ref<ControlSpin> pControlSpin;
+
+ UTILS::COLOR::Color textColor;
+ UTILS::COLOR::Color selectedColor;
+ std::string strTextureButton;
+ std::string strTextureButtonFocus;
+
+ int imageHeight = 0;
+ int imageWidth = 0;
+ int itemHeight = 0;
+ int space = 0;
+
+ int itemTextOffsetX = 0;
+ int itemTextOffsetY = 0;
+ uint32_t alignmentY;
+
+ CGUIControl* Create() override;
+#endif
+ };
+ /// @}
+
+ // ControlFadeLabel class
+ ///
+ /// \defgroup python_xbmcgui_control_fadelabel Subclass - ControlFadeLabel
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// @brief **Used to show multiple pieces of text in the same position, by
+ /// fading from one to the other.**
+ ///
+ /// \python_class{ ControlFadeLabel(x, y, width, height[, font, textColor, alignment]) }
+ ///
+ /// The fade label control is used for displaying multiple pieces of text
+ /// in the same space in Kodi. You can choose the font, size, colour,
+ /// location and contents of the text to be displayed. The first piece of
+ /// information to display fades in over 50 frames, then scrolls off to
+ /// the left. Once it is finished scrolling off screen, the second piece
+ /// of information fades in and the process repeats. A fade label control
+ /// is not supported in a list container.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @param x integer - x coordinate of control.
+ /// @param y integer - y coordinate of control.
+ /// @param width integer - width of control.
+ /// @param height integer - height of control.
+ /// @param font [opt] string - font used for label text. (e.g. 'font13')
+ /// @param textColor [opt] hexstring - color of fadelabel's labels. (e.g. '0xFFFFFFFF')
+ /// @param alignment [opt] integer - alignment of label
+ /// - \ref kodi_gui_font_alignment "Flags for alignment" used as bits to have several together:
+ /// | Definition name | Bitflag | Description |
+ /// |-------------------|:----------:|:------------------------------------|
+ /// | XBFONT_LEFT | 0x00000000 | Align X left
+ /// | XBFONT_RIGHT | 0x00000001 | Align X right
+ /// | XBFONT_CENTER_X | 0x00000002 | Align X center
+ /// | XBFONT_CENTER_Y | 0x00000004 | Align Y center
+ /// | XBFONT_TRUNCATED | 0x00000008 | Truncated text
+ /// | XBFONT_JUSTIFIED | 0x00000010 | Justify text
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.\n
+ /// After you create the control, you need to add it to the window
+ /// with addControl().
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.fadelabel = xbmcgui.ControlFadeLabel(100, 250, 200, 50, textColor='0xFFFFFFFF')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class ControlFadeLabel : public Control
+ {
+ public:
+ ControlFadeLabel(long x, long y, long width, long height,
+ const char* font = NULL,
+ const char* textColor = NULL,
+ long _alignment = XBFONT_LEFT);
+
+ // addLabel() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcgui_control_fadelabel
+ /// @brief \python_func{ addLabel(label) }
+ /// Add a label to this control for scrolling.
+ ///
+ /// @param label string or unicode - text string to add.
+ ///
+ /// @note To remove added text use `reset()` for them.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.fadelabel.addLabel('This is a line of text that can scroll.')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ addLabel(...);
+#else
+ virtual void addLabel(const String& label);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_fadelabel
+ /// @brief \python_func{ setScrolling(scroll) }
+ /// Set scrolling. If set to false, the labels won't scroll.
+ /// Defaults to true.
+ ///
+ /// @param scroll boolean - True = enabled / False = disabled
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.fadelabel.setScrolling(False)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setScrolling(...);
+#else
+ virtual void setScrolling(bool scroll);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_label
+ /// @brief \python_func{ reset() }
+ /// Clear this fade label.
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.fadelabel.reset()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ reset();
+#else
+ virtual void reset();
+#endif
+
+#ifndef SWIG
+ std::string strFont;
+ UTILS::COLOR::Color textColor;
+ std::vector<std::string> vecLabels;
+ uint32_t align;
+
+ CGUIControl* Create() override;
+
+ ControlFadeLabel() = default;
+#endif
+ };
+ /// @}
+
+ // ControlTextBox class
+ ///
+ /// \defgroup python_xbmcgui_control_textbox Subclass - ControlTextBox
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// @brief **Used to show a multi-page piece of text.**
+ ///
+ /// \python_class{ ControlTextBox(x, y, width, height[, font, textColor]) }
+ ///
+ /// The text box is used for showing a large multipage piece of text in Kodi.
+ /// You can choose the position, size, and look of the text.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @param x integer - x coordinate of control.
+ /// @param y integer - y coordinate of control.
+ /// @param width integer - width of control.
+ /// @param height integer - height of control.
+ /// @param font [opt] string - font used for text. (e.g. 'font13')
+ /// @param textColor [opt] hexstring - color of textbox's text. (e.g. '0xFFFFFFFF')
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.\n
+ /// After you create the control, you need to add it to the window with addControl().
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # ControlTextBox(x, y, width, height[, font, textColor])
+ /// self.textbox = xbmcgui.ControlTextBox(100, 250, 300, 300, textColor='0xFFFFFFFF')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ /// As stated above, the GUI control is only created once added to a window. The example
+ /// below shows how a ControlTextBox can be created, added to the current window and
+ /// have some of its properties changed.
+ ///
+ /// **Extended example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// textbox = xbmcgui.ControlTextBox(100, 250, 300, 300, textColor='0xFFFFFFFF')
+ /// window = xbmcgui.Window(xbmcgui.getCurrentWindowId())
+ /// window.addControl(textbox)
+ /// textbox.setText("My Text Box")
+ /// textbox.scroll()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class ControlTextBox : public Control
+ {
+ public:
+ ControlTextBox(long x, long y, long width, long height,
+ const char* font = NULL,
+ const char* textColor = NULL);
+
+ // SetText() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_textbox
+ /// @brief \python_func{ setText(text) }
+ /// Sets the text for this textbox.
+ /// \anchor python_xbmcgui_control_textbox_settext
+ ///
+ /// @param text string - text string.
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// @python_v19 setText can now be used before adding the control to the window (the defined
+ /// value is taken into consideration when the control is created)
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setText(text)
+ /// self.textbox.setText('This is a line of text that can wrap.')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setText(...);
+#else
+ virtual void setText(const String& text);
+#endif
+
+ // getText() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_textbox
+ /// @brief \python_func{ getText() }
+ /// Returns the text value for this textbox.
+ ///
+ /// @return To get text from box
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// @python_v19 getText() can now be used before adding the control to the window
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # getText()
+ /// text = self.text.getText()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getText();
+#else
+ virtual String getText();
+#endif
+
+ // reset() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_textbox
+ /// @brief \python_func{ reset() }
+ /// Clear's this textbox.
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v19 reset() will reset any text defined for this control even before you add the control to the window
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # reset()
+ /// self.textbox.reset()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ reset();
+#else
+ virtual void reset();
+#endif
+
+ // scroll() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_textbox
+ /// @brief \python_func{ scroll(id) }
+ /// Scrolls to the given position.
+ ///
+ /// @param id integer - position to scroll to.
+ ///
+ /// @note scroll() only works after the control is added to a window.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # scroll(position)
+ /// self.textbox.scroll(10)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ scroll(...);
+#else
+ virtual void scroll(long id);
+#endif
+
+ // autoScroll() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_textbox
+ /// @brief \python_func{ autoScroll(delay, time, repeat) }
+ /// Set autoscrolling times.
+ ///
+ /// @param delay integer - Scroll delay (in ms)
+ /// @param time integer - Scroll time (in ms)
+ /// @param repeat integer - Repeat time
+ ///
+ /// @note autoScroll only works after you add the control to a window.
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// @python_v15 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.textbox.autoScroll(1, 2, 1)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ autoScroll(...);
+#else
+ virtual void autoScroll(int delay, int time, int repeat);
+#endif
+
+#ifndef SWIG
+ std::string strFont;
+ UTILS::COLOR::Color textColor;
+
+ CGUIControl* Create() override;
+
+ ControlTextBox() = default;
+#endif
+ };
+ /// @}
+
+ // ControlImage class
+ ///
+ /// \defgroup python_xbmcgui_control_image Subclass - ControlImage
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// @brief **Used to show an image.**
+ ///
+ /// \python_class{ ControlImage(x, y, width, height, filename[, aspectRatio, colorDiffuse]) }
+ ///
+ /// The image control is used for displaying images in Kodi. You can choose
+ /// the position, size, transparency and contents of the image to be
+ /// displayed.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @param x integer - x coordinate of control.
+ /// @param y integer - y coordinate of control.
+ /// @param width integer - width of control.
+ /// @param height integer - height of control.
+ /// @param filename string - image filename.
+ /// @param aspectRatio [opt] integer - (values 0 = stretch
+ /// (default), 1 = scale up (crops),
+ /// 2 = scale down (black bar)
+ /// @param colorDiffuse hexString - (example, '0xC0FF0000' (red tint))
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.\n
+ /// After you create the control, you need to add it to the window with
+ /// addControl().
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # ControlImage(x, y, width, height, filename[, aspectRatio, colorDiffuse])
+ /// self.image = xbmcgui.ControlImage(100, 250, 125, 75, aspectRatio=2)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class ControlImage : public Control
+ {
+ public:
+ ControlImage(long x, long y, long width, long height,
+ const char* filename, long aspectRatio = 0,
+ const char* colorDiffuse = NULL);
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_image
+ /// @brief \python_func{ setImage(filename[, useCache]) }
+ /// Changes the image.
+ ///
+ /// @param filename string - image filename.
+ /// @param useCache [opt] bool - True=use cache (default) /
+ /// False=don't use cache.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v13 Added new option **useCache**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setImage(filename[, useCache])
+ /// self.image.setImage('special://home/scripts/test.png')
+ /// self.image.setImage('special://home/scripts/test.png', False)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setImage(...);
+#else
+ virtual void setImage(const char* imageFilename, const bool useCache = true);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_image
+ /// @brief \python_func{ setColorDiffuse(colorDiffuse) }
+ /// Changes the images color.
+ ///
+ /// @param colorDiffuse hexString - (example, '0xC0FF0000'
+ /// (red tint))
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setColorDiffuse(colorDiffuse)
+ /// self.image.setColorDiffuse('0xC0FF0000')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setColorDiffuse(...);
+#else
+ virtual void setColorDiffuse(const char* hexString);
+#endif
+
+#ifndef SWIG
+ ControlImage() = default;
+
+ std::string strFileName;
+ int aspectRatio = 0;
+ UTILS::COLOR::Color colorDiffuse;
+
+ CGUIControl* Create() override;
+#endif
+ };
+ /// @}
+
+ // ControlImage class
+ ///
+ /// \defgroup python_xbmcgui_control_progress Subclass - ControlProgress
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// @brief **Used to show the progress of a particular operation.**
+ ///
+ /// \python_class{ ControlProgress(x, y, width, height, filename[, texturebg, textureleft, texturemid, textureright, textureoverlay]) }
+ ///
+ /// The progress control is used to show the progress of an item that may
+ /// take a long time, or to show how far through a movie you are. You can
+ /// choose the position, size, and look of the progress control.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @param x integer - x coordinate of control.
+ /// @param y integer - y coordinate of control.
+ /// @param width integer - width of control.
+ /// @param height integer - height of control.
+ /// @param filename string - image filename.
+ /// @param texturebg [opt] string - specifies the image file
+ /// whichshould be displayed in the
+ /// background of the progress control.
+ /// @param textureleft [opt] string - specifies the image file
+ /// whichshould be displayed for the left
+ /// side of the progress bar. This is
+ /// rendered on the left side of the bar.
+ /// @param texturemid [opt] string - specifies the image file
+ /// which should be displayed for the middl
+ /// portion of the progress bar. This is
+ /// the `fill` texture used to fill up the
+ /// bar. It's positioned on the right of
+ /// the `<lefttexture>` texture, and fills
+ /// the gap between the `<lefttexture>` and
+ /// `<righttexture>` textures, depending on
+ /// how far progressed the item is.
+ /// @param textureright [opt] string - specifies the image file
+ /// which should be displayed for the right
+ /// side of the progress bar. This is
+ /// rendered on the right side of the bar.
+ /// @param textureoverlay [opt] string - specifies the image file
+ /// which should be displayed over the top of
+ /// all other images in the progress bar. It
+ /// is centered vertically and horizontally
+ /// within the space taken up by the
+ /// background image.
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.\n
+ /// After you create the control, you need to add it to the window
+ /// with addControl().
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # ControlProgress(x, y, width, height, filename[, texturebg, textureleft, texturemid, textureright, textureoverlay])
+ /// self.image = xbmcgui.ControlProgress(100, 250, 250, 30, 'special://home/scripts/test.png')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class ControlProgress : public Control
+ {
+ public:
+ ControlProgress(long x, long y, long width, long height,
+ const char* texturebg = NULL,
+ const char* textureleft = NULL,
+ const char* texturemid = NULL,
+ const char* textureright = NULL,
+ const char* textureoverlay = NULL);
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_progress
+ /// @brief \python_func{ setPercent(percent) }
+ /// Sets the percentage of the progressbar to show.
+ ///
+ /// @param percent float - percentage of the bar to show.
+ ///
+ ///
+ /// @note valid range for percent is 0-100
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setPercent(percent)
+ /// self.progress.setPercent(60)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setPercent(...);
+#else
+ virtual void setPercent(float pct);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_progress
+ /// @brief \python_func{ getPercent() }
+ /// Returns a float of the percent of the progress.
+ ///
+ /// @return Percent position
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # getPercent()
+ /// print(self.progress.getPercent())
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getPercent();
+#else
+ virtual float getPercent();
+#endif
+
+#ifndef SWIG
+ std::string strTextureLeft;
+ std::string strTextureMid;
+ std::string strTextureRight;
+ std::string strTextureBg;
+ std::string strTextureOverlay;
+ int aspectRatio = 0;
+ UTILS::COLOR::Color colorDiffuse;
+
+ CGUIControl* Create() override;
+ ControlProgress() = default;
+#endif
+ };
+ /// @}
+
+ // ControlButton class
+ ///
+ /// \defgroup python_xbmcgui_control_button Subclass - ControlButton
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// @brief <b>A standard push button control.</b>
+ ///
+ /// \python_class{ ControlButton(x, y, width, height, label[, focusTexture, noFocusTexture, textOffsetX, textOffsetY,
+ /// alignment, font, textColor, disabledColor, angle, shadowColor, focusedColor]) }
+ ///
+ /// The button control is used for creating push buttons in Kodi. You can
+ /// choose the position, size, and look of the button, as well as choosing
+ /// what action(s) should be performed when pushed.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @param x integer - x coordinate of control.
+ /// @param y integer - y coordinate of control.
+ /// @param width integer - width of control.
+ /// @param height integer - height of control.
+ /// @param label string or unicode - text string.
+ /// @param focusTexture [opt] string - filename for focus
+ /// texture.
+ /// @param noFocusTexture [opt] string - filename for no focus
+ /// texture.
+ /// @param textOffsetX [opt] integer - x offset of label.
+ /// @param textOffsetY [opt] integer - y offset of label.
+ /// @param alignment [opt] integer - alignment of label
+ /// - \ref kodi_gui_font_alignment "Flags for alignment" used as bits to have several together:
+ /// | Definition name | Bitflag | Description |
+ /// |-------------------|:----------:|:------------------------------------|
+ /// | XBFONT_LEFT | 0x00000000 | Align X left
+ /// | XBFONT_RIGHT | 0x00000001 | Align X right
+ /// | XBFONT_CENTER_X | 0x00000002 | Align X center
+ /// | XBFONT_CENTER_Y | 0x00000004 | Align Y center
+ /// | XBFONT_TRUNCATED | 0x00000008 | Truncated text
+ /// | XBFONT_JUSTIFIED | 0x00000010 | Justify text
+ /// @param font [opt] string - font used for label text.
+ /// (e.g. 'font13')
+ /// @param textColor [opt] hexstring - color of enabled
+ /// button's label. (e.g. '0xFFFFFFFF')
+ /// @param disabledColor [opt] hexstring - color of disabled
+ /// button's label. (e.g. '0xFFFF3300')
+ /// @param angle [opt] integer - angle of control.
+ /// (+ rotates CCW, - rotates CW)
+ /// @param shadowColor [opt] hexstring - color of button's
+ /// label's shadow. (e.g. '0xFF000000')
+ /// @param focusedColor [opt] hexstring - color of focused
+ /// button's label. (e.g. '0xFF00FFFF')
+ ///
+ /// @note You can use the above as keywords for arguments and skip
+ /// certain optional arguments.\n
+ /// Once you use a keyword, all following arguments require
+ /// the keyword.\n
+ /// After you create the control, you need to add it to the
+ /// window with addControl().
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # ControlButton(x, y, width, height, label[, focusTexture, noFocusTexture, textOffsetX, textOffsetY,
+ /// # alignment, font, textColor, disabledColor, angle, shadowColor, focusedColor])
+ /// self.button = xbmcgui.ControlButton(100, 250, 200, 50, 'Status', font='font14')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class ControlButton : public Control
+ {
+ public:
+ ControlButton(long x, long y, long width, long height, const String& label,
+ const char* focusTexture = NULL, const char* noFocusTexture = NULL,
+ long textOffsetX = CONTROL_TEXT_OFFSET_X,
+ long textOffsetY = CONTROL_TEXT_OFFSET_Y,
+ long alignment = (XBFONT_LEFT | XBFONT_CENTER_Y),
+ const char* font = NULL, const char* textColor = NULL,
+ const char* disabledColor = NULL, long angle = 0,
+ const char* shadowColor = NULL, const char* focusedColor = NULL);
+
+ // setLabel() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_button
+ /// @brief \python_func{ setLabel([label, font, textColor, disabledColor, shadowColor, focusedColor, label2]) }
+ /// Sets this buttons text attributes.
+ ///
+ /// @param label [opt] string or unicode - text string.
+ /// @param font [opt] string - font used for label text. (e.g. 'font13')
+ /// @param textColor [opt] hexstring - color of enabled button's label. (e.g. '0xFFFFFFFF')
+ /// @param disabledColor [opt] hexstring - color of disabled button's label. (e.g. '0xFFFF3300')
+ /// @param shadowColor [opt] hexstring - color of button's label's shadow. (e.g. '0xFF000000')
+ /// @param focusedColor [opt] hexstring - color of focused button's label. (e.g. '0xFFFFFF00')
+ /// @param label2 [opt] string or unicode - text string.
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setLabel([label, font, textColor, disabledColor, shadowColor, focusedColor])
+ /// self.button.setLabel('Status', 'font14', '0xFFFFFFFF', '0xFFFF3300', '0xFF000000')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setLabel(...);
+#else
+ virtual void setLabel(const String& label = emptyString,
+ const char* font = NULL,
+ const char* textColor = NULL,
+ const char* disabledColor = NULL,
+ const char* shadowColor = NULL,
+ const char* focusedColor = NULL,
+ const String& label2 = emptyString);
+#endif
+
+ // setDisabledColor() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_button
+ /// @brief \python_func{ setDisabledColor(disabledColor) }
+ /// Sets this buttons disabled color.
+ ///
+ /// @param disabledColor hexstring - color of disabled button's label. (e.g. '0xFFFF3300')
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setDisabledColor(disabledColor)
+ /// self.button.setDisabledColor('0xFFFF3300')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setDisabledColor(...);
+#else
+ virtual void setDisabledColor(const char* color);
+#endif
+
+ // getLabel() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_button
+ /// @brief \python_func{ getLabel() }
+ /// Returns the buttons label as a unicode string.
+ ///
+ /// @return Unicode string
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # getLabel()
+ /// label = self.button.getLabel()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getLabel();
+#else
+ virtual String getLabel();
+#endif
+
+ // getLabel2() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_button
+ /// @brief \python_func{ getLabel2() }
+ /// Returns the buttons label2 as a string.
+ ///
+ /// @return string of label 2
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # getLabel2()
+ /// label = self.button.getLabel2()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getLabel2();
+#else
+ virtual String getLabel2();
+#endif
+
+#ifndef SWIG
+ bool canAcceptMessages(int actionId) override { return true; }
+
+ int textOffsetX = 0;
+ int textOffsetY = 0;
+ UTILS::COLOR::Color align;
+ std::string strFont;
+ UTILS::COLOR::Color textColor;
+ UTILS::COLOR::Color disabledColor;
+ int iAngle = 0;
+ int shadowColor = 0;
+ int focusedColor = 0;
+ std::string strText;
+ std::string strText2;
+ std::string strTextureFocus;
+ std::string strTextureNoFocus;
+
+ CGUIControl* Create() override;
+
+ ControlButton() = default;
+#endif
+ };
+ /// @}
+
+ // ControlGroup class
+ ///
+ /// \defgroup python_xbmcgui_control_group Subclass - ControlGroup
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// @brief **Used to group controls together..**
+ ///
+ /// \python_class{ ControlGroup(x, y, width, height) }
+ ///
+ /// The group control is one of the most important controls. It allows you
+ /// to group controls together, applying attributes to all of them at once.
+ /// It also remembers the last navigated button in the group, so you can set
+ /// the <b>`<onup>`</b> of a control to a group of controls to have it always
+ /// go back to the one you were at before. It also allows you to position
+ /// controls more accurately relative to each other, as any controls within
+ /// a group take their coordinates from the group's top left corner (or from
+ /// elsewhere if you use the <b>"r"</b> attribute). You can have as many
+ /// groups as you like within the skin, and groups within groups are handled
+ /// with no issues.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @param x integer - x coordinate of control.
+ /// @param y integer - y coordinate of control.
+ /// @param width integer - width of control.
+ /// @param height integer - height of control.
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.group = xbmcgui.ControlGroup(100, 250, 125, 75)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class ControlGroup : public Control
+ {
+ public:
+ ControlGroup(long x, long y, long width, long height);
+
+#ifndef SWIG
+ CGUIControl* Create() override;
+
+ inline ControlGroup() = default;
+#endif
+ };
+ /// @}
+
+ // ControlRadioButton class
+ ///
+ /// \defgroup python_xbmcgui_control_radiobutton Subclass - ControlRadioButton
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// @brief **A radio button control (as used for on/off settings).**
+ ///
+ /// \python_class{ ControlRadioButton(x, y, width, height, label[, focusOnTexture, noFocusOnTexture,
+ /// focusOffTexture, noFocusOffTexture, focusTexture, noFocusTexture,
+ /// textOffsetX, textOffsetY, alignment, font, textColor, disabledColor]) }
+ ///
+ /// The radio button control is used for creating push button on/off
+ /// settings in Kodi. You can choose the position, size, and look of the
+ /// button, as well as the focused and unfocused radio textures. Used
+ /// for settings controls.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @param x integer - x coordinate of control.
+ /// @param y integer - y coordinate of control.
+ /// @param width integer - width of control.
+ /// @param height integer - height of control.
+ /// @param label string or unicode - text string.
+ /// @param focusOnTexture [opt] string - filename for radio ON
+ /// focused texture.
+ /// @param noFocusOnTexture [opt] string - filename for radio ON not
+ /// focused texture.
+ /// @param focusOfTexture [opt] string - filename for radio OFF
+ /// focused texture.
+ /// @param noFocusOffTexture [opt] string - filename for radio OFF
+ /// not focused texture.
+ /// @param focusTexture [opt] string - filename for focused button
+ /// texture.
+ /// @param noFocusTexture [opt] string - filename for not focused button
+ /// texture.
+ /// @param textOffsetX [opt] integer - horizontal text offset
+ /// @param textOffsetY [opt] integer - vertical text offset
+ /// @param alignment [opt] integer - alignment of label
+ /// - \ref kodi_gui_font_alignment "Flags for alignment" used as bits to have several together:
+ /// | Definition name | Bitflag | Description |
+ /// |-------------------|:----------:|:------------------------------------|
+ /// | XBFONT_LEFT | 0x00000000 | Align X left
+ /// | XBFONT_RIGHT | 0x00000001 | Align X right
+ /// | XBFONT_CENTER_X | 0x00000002 | Align X center
+ /// | XBFONT_CENTER_Y | 0x00000004 | Align Y center
+ /// | XBFONT_TRUNCATED | 0x00000008 | Truncated text
+ /// | XBFONT_JUSTIFIED | 0x00000010 | Justify text
+ /// @param font [opt] string - font used for label text.
+ /// (e.g. 'font13')
+ /// @param textColor [opt] hexstring - color of label when control
+ /// is enabled.
+ /// radiobutton's label. (e.g. '0xFFFFFFFF')
+ /// @param disabledColor [opt] hexstring - color of label when control
+ /// is disabled. (e.g. '0xFFFF3300')
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.\n
+ /// After you create the control, you need to add it to the window with
+ /// addControl().
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ /// @python_v13 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.radiobutton = xbmcgui.ControlRadioButton(100, 250, 200, 50, 'Enable', font='font14')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class ControlRadioButton : public Control
+ {
+ public:
+ ControlRadioButton(long x, long y, long width, long height, const String& label,
+ const char* focusOnTexture = NULL, const char* noFocusOnTexture = NULL,
+ const char* focusOffTexture = NULL, const char* noFocusOffTexture = NULL,
+ const char* focusTexture = NULL, const char* noFocusTexture = NULL,
+ long textOffsetX = CONTROL_TEXT_OFFSET_X,
+ long textOffsetY = CONTROL_TEXT_OFFSET_Y,
+ long _alignment = (XBFONT_LEFT | XBFONT_CENTER_Y),
+ const char* font = NULL, const char* textColor = NULL,
+ const char* disabledColor = NULL, long angle = 0,
+ const char* shadowColor = NULL, const char* focusedColor = NULL,
+ const char* disabledOnTexture = NULL, const char* disabledOffTexture = NULL);
+
+ // setSelected() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_radiobutton
+ /// @brief \python_func{ setSelected(selected) }
+ /// **Sets the radio buttons's selected status.**
+ ///
+ /// @param selected bool - True=selected (on) / False=not
+ /// selected (off)
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.radiobutton.setSelected(True)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setSelected(...);
+#else
+ virtual void setSelected(bool selected);
+#endif
+
+ // isSelected() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_radiobutton
+ /// @brief \python_func{ isSelected() }
+ /// Returns the radio buttons's selected status.
+ ///
+ /// @return True if selected on
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// is = self.radiobutton.isSelected()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ isSelected();
+#else
+ virtual bool isSelected();
+#endif
+
+ // setLabel() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_radiobutton
+ /// @brief \python_func{ setLabel(label[, font, textColor, disabledColor, shadowColor, focusedColor]) }
+ /// Sets the radio buttons text attributes.
+ ///
+ /// @param label string or unicode - text string.
+ /// @param font [opt] string - font used for label
+ /// text. (e.g. 'font13')
+ /// @param textColor [opt] hexstring - color of enabled radio
+ /// button's label. (e.g. '0xFFFFFFFF')
+ /// @param disabledColor [opt] hexstring - color of disabled
+ /// radio button's label. (e.g. '0xFFFF3300')
+ /// @param shadowColor [opt] hexstring - color of radio
+ /// button's label's shadow.
+ /// (e.g. '0xFF000000')
+ /// @param focusedColor [opt] hexstring - color of focused radio
+ /// button's label. (e.g. '0xFFFFFF00')
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setLabel(label[, font, textColor, disabledColor, shadowColor, focusedColor])
+ /// self.radiobutton.setLabel('Status', 'font14', '0xFFFFFFFF', '0xFFFF3300', '0xFF000000')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setLabel(...);
+#else
+ virtual void setLabel(const String& label = emptyString,
+ const char* font = NULL,
+ const char* textColor = NULL,
+ const char* disabledColor = NULL,
+ const char* shadowColor = NULL,
+ const char* focusedColor = NULL,
+ const String& label2 = emptyString);
+#endif
+
+ // setRadioDimension() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_radiobutton
+ /// @brief \python_func{ setRadioDimension(x, y, width, height) }
+ /// Sets the radio buttons's radio texture's position and size.
+ ///
+ /// @param x integer - x coordinate of radio texture.
+ /// @param y integer - y coordinate of radio texture.
+ /// @param width integer - width of radio texture.
+ /// @param height integer - height of radio texture.
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.radiobutton.setRadioDimension(x=100, y=5, width=20, height=20)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setRadioDimension(...);
+#else
+ virtual void setRadioDimension(long x, long y, long width, long height);
+#endif
+
+#ifndef SWIG
+ bool canAcceptMessages(int actionId) override { return true; }
+
+ std::string strFont;
+ std::string strText;
+ std::string strTextureFocus;
+ std::string strTextureNoFocus;
+ std::string strTextureRadioOnFocus;
+ std::string strTextureRadioOnNoFocus;
+ std::string strTextureRadioOffFocus;
+ std::string strTextureRadioOffNoFocus;
+ std::string strTextureRadioOnDisabled;
+ std::string strTextureRadioOffDisabled;
+ UTILS::COLOR::Color textColor;
+ UTILS::COLOR::Color disabledColor;
+ int textOffsetX = 0;
+ int textOffsetY = 0;
+ uint32_t align;
+ int iAngle = 0;
+ UTILS::COLOR::Color shadowColor;
+ UTILS::COLOR::Color focusedColor;
+
+ CGUIControl* Create() override;
+
+ ControlRadioButton() = default;
+#endif
+ };
+ /// @}
+
+ /// \defgroup python_xbmcgui_control_slider Subclass - ControlSlider
+ /// \ingroup python_xbmcgui_control
+ /// @{
+ /// @brief **Used for a volume slider.**
+ ///
+ /// \python_class{ ControlSlider(x, y, width, height[, textureback, texture, texturefocus, orientation]) }
+ ///
+ /// The slider control is used for things where a sliding bar best represents
+ /// the operation at hand (such as a volume control or seek control). You can
+ /// choose the position, size, and look of the slider control.
+ ///
+ /// @note This class include also all calls from \ref python_xbmcgui_control "Control"
+ ///
+ /// @param x integer - x coordinate of control
+ /// @param y integer - y coordinate of control
+ /// @param width integer - width of control
+ /// @param height integer - height of control
+ /// @param textureback [opt] string - image filename
+ /// @param texture [opt] string - image filename
+ /// @param texturefocus [opt] string - image filename
+ /// @param orientation [opt] integer - orientation of slider (xbmcgui.HORIZONTAL / xbmcgui.VERTICAL (default))
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.\n
+ /// After you create the control, you need to add it to the window
+ /// with addControl().
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ /// @python_v17 **orientation** option added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.slider = xbmcgui.ControlSlider(100, 250, 350, 40)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ class ControlSlider : public Control
+ {
+ public:
+ ControlSlider(long x, long y, long width, long height,
+ const char* textureback = NULL,
+ const char* texture = NULL,
+ const char* texturefocus = NULL, int orientation = 1);
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_slider
+ /// @brief \python_func{ getPercent() }
+ /// Returns a float of the percent of the slider.
+ ///
+ /// @return float - Percent of slider
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// print(self.slider.getPercent())
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getPercent();
+#else
+ virtual float getPercent();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_slider
+ /// @brief \python_func{ setPercent(pct) }
+ /// Sets the percent of the slider.
+ ///
+ /// @param pct float - Percent value of slider
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.slider.setPercent(50)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setPercent(...);
+#else
+ virtual void setPercent(float pct);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_slider
+ /// @brief \python_func{ getInt() }
+ /// Returns the value of the slider.
+ ///
+ /// @return int - value of slider
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// print(self.slider.getInt())
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getInt();
+#else
+ virtual int getInt();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_slider
+ /// @brief \python_func{ setInt(value, min, delta, max) }
+ /// Sets the range, value and step size of the slider.
+ ///
+ /// @param value int - value of slider
+ /// @param min int - min of slider
+ /// @param delta int - step size of slider
+ /// @param max int - max of slider
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.slider.setInt(450, 200, 10, 900)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setInt(...);
+#else
+ virtual void setInt(int value, int min, int delta, int max);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_slider
+ /// @brief \python_func{ getFloat() }
+ /// Returns the value of the slider.
+ ///
+ /// @return float - value of slider
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// print(self.slider.getFloat())
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getFloat();
+#else
+ virtual float getFloat();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_control_slider
+ /// @brief \python_func{ setFloat(value, min, delta, max) }
+ /// Sets the range, value and step size of the slider.
+ ///
+ /// @param value float - value of slider
+ /// @param min float - min of slider
+ /// @param delta float - step size of slider
+ /// @param max float - max of slider
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// self.slider.setFloat(15.0, 10.0, 1.0, 20.0)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setFloat(...);
+#else
+ virtual void setFloat(float value, float min, float delta, float max);
+#endif
+
+#ifndef SWIG
+ std::string strTextureBack;
+ std::string strTexture;
+ std::string strTextureFoc;
+ int iOrientation;
+
+ CGUIControl* Create() override;
+
+ inline ControlSlider() = default;
+#endif
+ };
+ /// @}
+ }
+}
diff --git a/xbmc/interfaces/legacy/Dialog.cpp b/xbmc/interfaces/legacy/Dialog.cpp
new file mode 100644
index 0000000..6dd00f1
--- /dev/null
+++ b/xbmc/interfaces/legacy/Dialog.cpp
@@ -0,0 +1,622 @@
+ /*
+ * 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 "Dialog.h"
+
+#include "LanguageHook.h"
+#include "ListItem.h"
+#include "ModuleXbmcgui.h"
+#include "ServiceBroker.h"
+#include "WindowException.h"
+#include "dialogs/GUIDialogColorPicker.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogTextViewer.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/dialogs/GUIDialogMusicInfo.h"
+#include "settings/MediaSourceSettings.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+
+ using namespace KODI::MESSAGING;
+
+#define ACTIVE_WINDOW CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ Dialog::~Dialog() = default;
+
+ bool Dialog::yesno(const String& heading,
+ const String& message,
+ const String& nolabel,
+ const String& yeslabel,
+ int autoclose,
+ int defaultbutton)
+ {
+ return yesNoCustomInternal(heading, message, nolabel, yeslabel, emptyString, autoclose,
+ defaultbutton) == 1;
+ }
+
+ int Dialog::yesnocustom(const String& heading,
+ const String& message,
+ const String& customlabel,
+ const String& nolabel,
+ const String& yeslabel,
+ int autoclose,
+ int defaultbutton)
+ {
+ return yesNoCustomInternal(heading, message, nolabel, yeslabel, customlabel, autoclose,
+ defaultbutton);
+ }
+
+ int Dialog::yesNoCustomInternal(const String& heading,
+ const String& message,
+ const String& nolabel,
+ const String& yeslabel,
+ const String& customlabel,
+ int autoclose,
+ int defaultbutton)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ CGUIDialogYesNo* pDialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(
+ WINDOW_DIALOG_YES_NO);
+ if (pDialog == nullptr)
+ throw WindowException("Error: Window is null");
+
+ return pDialog->ShowAndGetInput(CVariant{heading}, CVariant{message}, CVariant{nolabel},
+ CVariant{yeslabel}, CVariant{customlabel}, autoclose,
+ defaultbutton);
+ }
+
+ bool Dialog::info(const ListItem* item)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ const AddonClass::Ref<xbmcgui::ListItem> listitem(item);
+ if (listitem->item->HasVideoInfoTag())
+ {
+ CGUIDialogVideoInfo::ShowFor(*listitem->item);
+ return true;
+ }
+ else if (listitem->item->HasMusicInfoTag())
+ {
+ CGUIDialogMusicInfo::ShowFor(listitem->item.get());
+ return true;
+ }
+ return false;
+ }
+
+ int Dialog::contextmenu(const std::vector<String>& list)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ CGUIDialogContextMenu* pDialog= CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogContextMenu>(WINDOW_DIALOG_CONTEXT_MENU);
+ if (pDialog == NULL)
+ throw WindowException("Error: Window is NULL, this is not possible :-)");
+
+ CContextButtons choices;
+ for(unsigned int i = 0; i < list.size(); i++)
+ {
+ choices.Add(i, list[i]);
+ }
+ return pDialog->Show(choices);
+ }
+
+
+ int Dialog::select(const String& heading, const std::vector<Alternative<String, const ListItem* > > & list, int autoclose, int preselect, bool useDetails)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ CGUIDialogSelect* pDialog= CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (pDialog == NULL)
+ throw WindowException("Error: Window is NULL, this is not possible :-)");
+
+ pDialog->Reset();
+ if (!heading.empty())
+ pDialog->SetHeading(CVariant{heading});
+ for (const auto& item : list)
+ {
+ AddonClass::Ref<ListItem> ritem = item.which() == XBMCAddon::first ? ListItem::fromString(item.former()) : AddonClass::Ref<ListItem>(item.later());
+ CFileItemPtr& fileItem = ritem->item;
+ pDialog->Add(*fileItem);
+ }
+ if (preselect > -1)
+ pDialog->SetSelected(preselect);
+ if (autoclose > 0)
+ pDialog->SetAutoClose(autoclose);
+ pDialog->SetUseDetails(useDetails);
+ pDialog->Open();
+
+ return pDialog->GetSelectedItem();
+ }
+
+
+ std::unique_ptr<std::vector<int>> Dialog::multiselect(const String& heading,
+ const std::vector<Alternative<String, const ListItem* > > & options, int autoclose, const std::vector<int>& preselect, bool useDetails)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ CGUIDialogSelect* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (pDialog == nullptr)
+ throw WindowException("Error: Window is NULL");
+
+ pDialog->Reset();
+ pDialog->SetMultiSelection(true);
+ pDialog->SetHeading(CVariant{heading});
+
+ for (const auto& item : options)
+ {
+ AddonClass::Ref<ListItem> ritem = item.which() == XBMCAddon::first ? ListItem::fromString(item.former()) : AddonClass::Ref<ListItem>(item.later());
+ CFileItemPtr& fileItem = ritem->item;
+ pDialog->Add(*fileItem);
+ }
+ if (autoclose > 0)
+ pDialog->SetAutoClose(autoclose);
+ pDialog->SetUseDetails(useDetails);
+ pDialog->SetSelected(preselect);
+ pDialog->Open();
+
+ if (pDialog->IsConfirmed())
+ return std::unique_ptr<std::vector<int>>(new std::vector<int>(pDialog->GetSelectedItems()));
+ else
+ return std::unique_ptr<std::vector<int>>();
+ }
+
+ bool Dialog::ok(const String& heading, const String& message)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ return HELPERS::ShowOKDialogText(CVariant{heading}, CVariant{message});
+ }
+
+ void Dialog::textviewer(const String& heading, const String& text, bool usemono)
+ {
+ DelayedCallGuard dcguard(languageHook);
+
+ CGUIDialogTextViewer* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogTextViewer>(WINDOW_DIALOG_TEXT_VIEWER);
+ if (pDialog == NULL)
+ throw WindowException("Error: Window is NULL, this is not possible :-)");
+ if (!heading.empty())
+ pDialog->SetHeading(heading);
+ if (!text.empty())
+ pDialog->SetText(text);
+ pDialog->UseMonoFont(usemono);
+ pDialog->Open();
+ }
+
+
+ Alternative<String, std::vector<String> > Dialog::browse(int type, const String& heading,
+ const String& s_shares, const String& maskparam, bool useThumbs,
+ bool useFileDirectories, const String& defaultt,
+ bool enableMultiple)
+ {
+ Alternative<String, std::vector<String> > ret;
+ if (enableMultiple)
+ ret.later() = browseMultiple(type,heading,s_shares,maskparam,useThumbs,useFileDirectories,defaultt);
+ else
+ ret.former() = browseSingle(type,heading,s_shares,maskparam,useThumbs,useFileDirectories,defaultt);
+ return ret;
+ }
+
+ String Dialog::browseSingle(int type, const String& heading, const String& s_shares,
+ const String& maskparam, bool useThumbs,
+ bool useFileDirectories,
+ const String& defaultt )
+ {
+ DelayedCallGuard dcguard(languageHook);
+ std::string value;
+ std::string mask = maskparam;
+ VECSOURCES *shares = CMediaSourceSettings::GetInstance().GetSources(s_shares);
+
+ VECSOURCES localShares;
+ if (!shares)
+ {
+ CServiceBroker::GetMediaManager().GetLocalDrives(localShares);
+ if (StringUtils::CompareNoCase(s_shares, "local") != 0)
+ CServiceBroker::GetMediaManager().GetNetworkLocations(localShares);
+ }
+ else // always append local drives
+ {
+ localShares = *shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(localShares);
+ }
+
+ if (useFileDirectories && !maskparam.empty())
+ mask += "|.rar|.zip";
+
+ value = defaultt;
+ if (type == 1)
+ CGUIDialogFileBrowser::ShowAndGetFile(localShares, mask, heading, value, useThumbs, useFileDirectories);
+ else if (type == 2)
+ CGUIDialogFileBrowser::ShowAndGetImage(localShares, heading, value);
+ else
+ CGUIDialogFileBrowser::ShowAndGetDirectory(localShares, heading, value, type != 0);
+ return value;
+ }
+
+ std::vector<String> Dialog::browseMultiple(int type, const String& heading, const String& s_shares,
+ const String& mask, bool useThumbs,
+ bool useFileDirectories, const String& defaultt )
+ {
+ DelayedCallGuard dcguard(languageHook);
+ VECSOURCES *shares = CMediaSourceSettings::GetInstance().GetSources(s_shares);
+ std::vector<String> valuelist;
+ String lmask = mask;
+
+ VECSOURCES localShares;
+ if (!shares)
+ {
+ CServiceBroker::GetMediaManager().GetLocalDrives(localShares);
+ if (StringUtils::CompareNoCase(s_shares, "local") != 0)
+ CServiceBroker::GetMediaManager().GetNetworkLocations(localShares);
+ }
+ else // always append local drives
+ {
+ localShares = *shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(localShares);
+ }
+
+ if (useFileDirectories && !lmask.empty())
+ lmask += "|.rar|.zip";
+
+ if (type == 1)
+ CGUIDialogFileBrowser::ShowAndGetFileList(localShares, lmask, heading, valuelist, useThumbs, useFileDirectories);
+ else if (type == 2)
+ CGUIDialogFileBrowser::ShowAndGetImageList(localShares, heading, valuelist);
+ else
+ throw WindowException("Error: Cannot retrieve multiple directories using browse %s is NULL.",s_shares.c_str());
+
+ return valuelist;
+ }
+
+ String Dialog::numeric(int inputtype, const String& heading, const String& defaultt, bool bHiddenInput)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ std::string value;
+ KODI::TIME::SystemTime timedate;
+ KODI::TIME::GetLocalTime(&timedate);
+
+ if (!heading.empty())
+ {
+ if (inputtype == 1)
+ {
+ if (!defaultt.empty() && defaultt.size() == 10)
+ {
+ const std::string& sDefault = defaultt;
+ timedate.day = atoi(sDefault.substr(0, 2).c_str());
+ timedate.month = atoi(sDefault.substr(3, 4).c_str());
+ timedate.year = atoi(sDefault.substr(sDefault.size() - 4).c_str());
+ }
+ if (CGUIDialogNumeric::ShowAndGetDate(timedate, heading))
+ value =
+ StringUtils::Format("{:2}/{:2}/{:4}", timedate.day, timedate.month, timedate.year);
+ else
+ return emptyString;
+ }
+ else if (inputtype == 2)
+ {
+ if (!defaultt.empty() && defaultt.size() == 5)
+ {
+ const std::string& sDefault = defaultt;
+ timedate.hour = atoi(sDefault.substr(0, 2).c_str());
+ timedate.minute = atoi(sDefault.substr(3, 2).c_str());
+ }
+ if (CGUIDialogNumeric::ShowAndGetTime(timedate, heading))
+ value = StringUtils::Format("{:2}:{:02}", timedate.hour, timedate.minute);
+ else
+ return emptyString;
+ }
+ else if (inputtype == 3)
+ {
+ value = defaultt;
+ if (!CGUIDialogNumeric::ShowAndGetIPAddress(value, heading))
+ return emptyString;
+ }
+ else if (inputtype == 4)
+ {
+ value = defaultt;
+ if (!CGUIDialogNumeric::ShowAndVerifyNewPassword(value))
+ return emptyString;
+ }
+ else
+ {
+ value = defaultt;
+ if (!CGUIDialogNumeric::ShowAndGetNumber(value, heading, 0, bHiddenInput))
+ return emptyString;
+ }
+ }
+ return value;
+ }
+
+ void Dialog::notification(const String& heading, const String& message, const String& icon, int time, bool sound)
+ {
+ DelayedCallGuard dcguard(languageHook);
+
+ std::string strIcon = getNOTIFICATION_INFO();
+ int iTime = TOAST_DISPLAY_TIME;
+
+ if (time > 0)
+ iTime = time;
+ if (!icon.empty())
+ strIcon = icon;
+
+ if (strIcon == getNOTIFICATION_INFO())
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, heading, message, iTime, sound);
+ else if (strIcon == getNOTIFICATION_WARNING())
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, heading, message, iTime, sound);
+ else if (strIcon == getNOTIFICATION_ERROR())
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, heading, message, iTime, sound);
+ else
+ CGUIDialogKaiToast::QueueNotification(strIcon, heading, message, iTime, sound);
+ }
+
+ String Dialog::input(const String& heading, const String& defaultt, int type, int option, int autoclose)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ std::string value(defaultt);
+ KODI::TIME::SystemTime timedate;
+ KODI::TIME::GetLocalTime(&timedate);
+
+ switch (type)
+ {
+ case INPUT_ALPHANUM:
+ {
+ bool bHiddenInput = (option & ALPHANUM_HIDE_INPUT) == ALPHANUM_HIDE_INPUT;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(value, CVariant{heading}, true, bHiddenInput, autoclose))
+ value = emptyString;
+ }
+ break;
+ case INPUT_NUMERIC:
+ {
+ if (!CGUIDialogNumeric::ShowAndGetNumber(value, heading, autoclose))
+ value = emptyString;
+ }
+ break;
+ case INPUT_DATE:
+ {
+ if (!defaultt.empty() && defaultt.size() == 10)
+ {
+ const std::string& sDefault = defaultt;
+ timedate.day = atoi(sDefault.substr(0, 2).c_str());
+ timedate.month = atoi(sDefault.substr(3, 4).c_str());
+ timedate.year = atoi(sDefault.substr(sDefault.size() - 4).c_str());
+ }
+ if (CGUIDialogNumeric::ShowAndGetDate(timedate, heading))
+ value = StringUtils::Format("{:2}/{:2}/{:4}", timedate.day, timedate.month,
+ timedate.year);
+ else
+ value = emptyString;
+ }
+ break;
+ case INPUT_TIME:
+ {
+ if (!defaultt.empty() && defaultt.size() == 5)
+ {
+ const std::string& sDefault = defaultt;
+ timedate.hour = atoi(sDefault.substr(0, 2).c_str());
+ timedate.minute = atoi(sDefault.substr(3, 2).c_str());
+ }
+ if (CGUIDialogNumeric::ShowAndGetTime(timedate, heading))
+ value = StringUtils::Format("{:2}:{:02}", timedate.hour, timedate.minute);
+ else
+ value = emptyString;
+ }
+ break;
+ case INPUT_IPADDRESS:
+ {
+ if (!CGUIDialogNumeric::ShowAndGetIPAddress(value, heading))
+ value = emptyString;
+ }
+ break;
+ case INPUT_PASSWORD:
+ {
+ bool bResult = false;
+
+ if (option & PASSWORD_VERIFY)
+ bResult = CGUIKeyboardFactory::ShowAndVerifyPassword(value, heading, 0, autoclose) == 0 ? true : false;
+ else
+ bResult = CGUIKeyboardFactory::ShowAndVerifyNewPassword(value, heading, true, autoclose);
+
+ if (!bResult)
+ value = emptyString;
+ }
+ break;
+ default:
+ value = emptyString;
+ break;
+ }
+
+ return value;
+ }
+
+ String Dialog::colorpicker(const String& heading,
+ const String& selectedcolor,
+ const String& colorfile,
+ const std::vector<const ListItem*>& colorlist)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ std::string value = emptyString;
+ CGUIDialogColorPicker* pDialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogColorPicker>(
+ WINDOW_DIALOG_COLOR_PICKER);
+ if (pDialog == nullptr)
+ throw WindowException("Error: Window is NULL, this is not possible :-)");
+
+ pDialog->Reset();
+ if (!heading.empty())
+ pDialog->SetHeading(CVariant{heading});
+
+ if (!colorlist.empty())
+ {
+ CFileItemList items;
+ for (const auto& coloritem : colorlist)
+ {
+ items.Add(coloritem->item);
+ }
+ pDialog->SetItems(items);
+ }
+ else if (!colorfile.empty())
+ pDialog->LoadColors(colorfile);
+ else
+ pDialog->LoadColors();
+
+ if (!selectedcolor.empty())
+ pDialog->SetSelectedColor(selectedcolor);
+
+ pDialog->Open();
+
+ if (pDialog->IsConfirmed())
+ value = pDialog->GetSelectedColor();
+ return value;
+ }
+
+ DialogProgress::~DialogProgress() { XBMC_TRACE; deallocating(); }
+
+ void DialogProgress::deallocating()
+ {
+ XBMC_TRACE;
+
+ if (dlg && open)
+ {
+ DelayedCallGuard dg;
+ dlg->Close();
+ }
+ }
+
+ void DialogProgress::create(const String& heading, const String& message)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ CGUIDialogProgress* pDialog= CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+
+ if (pDialog == NULL)
+ throw WindowException("Error: Window is NULL, this is not possible :-)");
+
+ dlg = pDialog;
+ open = true;
+
+ pDialog->SetHeading(CVariant{heading});
+
+ if (!message.empty())
+ pDialog->SetText(CVariant{message});
+
+ pDialog->Open();
+ }
+
+ void DialogProgress::update(int percent, const String& message)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ CGUIDialogProgress* pDialog = dlg;
+
+ if (pDialog == NULL)
+ throw WindowException("Dialog not created.");
+
+ if (percent >= 0 && percent <= 100)
+ {
+ pDialog->SetPercentage(percent);
+ pDialog->ShowProgressBar(true);
+ }
+ else
+ {
+ pDialog->ShowProgressBar(false);
+ }
+
+ if (!message.empty())
+ pDialog->SetText(CVariant{message});
+ }
+
+ void DialogProgress::close()
+ {
+ DelayedCallGuard dcguard(languageHook);
+ if (dlg == NULL)
+ throw WindowException("Dialog not created.");
+ dlg->Close();
+ open = false;
+ }
+
+ bool DialogProgress::iscanceled()
+ {
+ if (dlg == NULL)
+ throw WindowException("Dialog not created.");
+ return dlg->IsCanceled();
+ }
+
+ DialogProgressBG::~DialogProgressBG() { XBMC_TRACE; deallocating(); }
+
+ void DialogProgressBG::deallocating()
+ {
+ XBMC_TRACE;
+
+ if (dlg && open)
+ {
+ DelayedCallGuard dg;
+ dlg->Close();
+ }
+ }
+
+ void DialogProgressBG::create(const String& heading, const String& message)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ CGUIDialogExtendedProgressBar* pDialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
+
+ if (pDialog == NULL)
+ throw WindowException("Error: Window is NULL, this is not possible :-)");
+
+ CGUIDialogProgressBarHandle* pHandle = pDialog->GetHandle(heading);
+
+ dlg = pDialog;
+ handle = pHandle;
+ open = true;
+
+ pHandle->SetTitle(heading);
+ if (!message.empty())
+ pHandle->SetText(message);
+ }
+
+ void DialogProgressBG::update(int percent, const String& heading, const String& message)
+ {
+ DelayedCallGuard dcguard(languageHook);
+ CGUIDialogProgressBarHandle* pHandle = handle;
+
+ if (pHandle == NULL)
+ throw WindowException("Dialog not created.");
+
+ if (percent >= 0 && percent <= 100)
+ pHandle->SetPercentage((float)percent);
+ if (!heading.empty())
+ pHandle->SetTitle(heading);
+ if (!message.empty())
+ pHandle->SetText(message);
+ }
+
+ void DialogProgressBG::close()
+ {
+ DelayedCallGuard dcguard(languageHook);
+ if (handle == NULL)
+ throw WindowException("Dialog not created.");
+ handle->MarkFinished();
+ open = false;
+ }
+
+ bool DialogProgressBG::isFinished()
+ {
+ if (handle == NULL)
+ throw WindowException("Dialog not created.");
+ return handle->IsFinished();
+ }
+
+ }
+}
diff --git a/xbmc/interfaces/legacy/Dialog.h b/xbmc/interfaces/legacy/Dialog.h
new file mode 100644
index 0000000..9faa733
--- /dev/null
+++ b/xbmc/interfaces/legacy/Dialog.h
@@ -0,0 +1,965 @@
+/*
+ * 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 "AddonClass.h"
+#include "AddonString.h"
+#include "Alternative.h"
+#include "ListItem.h"
+#include "dialogs/GUIDialogBoxBase.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "swighelper.h"
+
+#include <string>
+#include <vector>
+
+namespace XBMCAddon
+{
+namespace xbmcgui
+{
+constexpr int INPUT_ALPHANUM{0};
+constexpr int INPUT_NUMERIC{1};
+constexpr int INPUT_DATE{2};
+constexpr int INPUT_TIME{3};
+constexpr int INPUT_IPADDRESS{4};
+constexpr int INPUT_PASSWORD{5};
+
+constexpr int PASSWORD_VERIFY{1};
+constexpr int ALPHANUM_HIDE_INPUT{2};
+
+ ///
+ /// \defgroup python_Dialog Dialog
+ /// \ingroup python_xbmcgui
+ /// @{
+ /// @brief **Kodi's dialog class**
+ ///
+ /// The graphical control element dialog box (also called dialogue box or
+ /// just dialog) is a small window that communicates information to the user
+ /// and prompts them for a response.
+ ///
+ class Dialog : public AddonClass
+ {
+ public:
+
+ inline Dialog() = default;
+ ~Dialog() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().yesno(heading, message, [nolabel, yeslabel, autoclose]) }
+ /// **Yes / no dialog**
+ ///
+ /// The Yes / No dialog can be used to inform the user about questions and
+ /// get the answer.
+ ///
+ /// @param heading string or unicode - dialog heading.
+ /// @param message string or unicode - message text.
+ /// @param nolabel [opt] label to put on the no button.
+ /// @param yeslabel [opt] label to put on the yes button.
+ /// @param autoclose [opt] integer - milliseconds to autoclose dialog. (default=do not autoclose)
+ /// @param defaultbutton [opt] integer - specifies the default focused button.
+ /// <em>(default=DLG_YESNO_NO_BTN)</em>
+ /// | Value: | Description: |
+ /// |------------------------------:|---------------------------------------------------|
+ /// | xbmcgui.DLG_YESNO_NO_BTN | Set the "No" button as default.
+ /// | xbmcgui.DLG_YESNO_YES_BTN | Set the "Yes" button as default.
+ /// | xbmcgui.DLG_YESNO_CUSTOM_BTN | Set the "Custom" button as default.
+ /// @return Returns True if 'Yes' was pressed, else False.
+ ///
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v13 Added new option **autoclose**.
+ /// @python_v19 Renamed option **line1** to **message**.
+ /// @python_v19 Removed option **line2**.
+ /// @python_v19 Removed option **line3**.
+ /// @python_v20 Added new option **defaultbutton**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// ret = dialog.yesno('Kodi', 'Do you want to exit this script?')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ yesno(...);
+#else
+ bool yesno(const String& heading,
+ const String& message,
+ const String& nolabel = emptyString,
+ const String& yeslabel = emptyString,
+ int autoclose = 0,
+ int defaultbutton = CONTROL_NO_BUTTON);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().yesnocustom(heading, message, customlabel, [nolabel, yeslabel, autoclose]) }
+ /// **Yes / no / custom dialog**
+ ///
+ /// The YesNoCustom dialog can be used to inform the user about questions and
+ /// get the answer. The dialog provides a third button appart from yes and no.
+ /// Button labels are fully customizable.
+ ///
+ /// @param heading string or unicode - dialog heading.
+ /// @param message string or unicode - message text.
+ /// @param customlabel string or unicode - label to put on the custom button.
+ /// @param nolabel [opt] label to put on the no button.
+ /// @param yeslabel [opt] label to put on the yes button.
+ /// @param autoclose [opt] integer - milliseconds to autoclose dialog. (default=do not autoclose)
+ /// @param defaultbutton [opt] integer - specifies the default focused button.
+ /// <em>(default=DLG_YESNO_NO_BTN)</em>
+ /// | Value: | Description: |
+ /// |------------------------------:|---------------------------------------------------|
+ /// | xbmcgui.DLG_YESNO_NO_BTN | Set the "No" button as default.
+ /// | xbmcgui.DLG_YESNO_YES_BTN | Set the "Yes" button as default.
+ /// | xbmcgui.DLG_YESNO_CUSTOM_BTN | Set the "Custom" button as default.
+ /// @return Returns the integer value for the selected button (-1:cancelled, 0:no, 1:yes, 2:custom)
+ ///
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v19 New function added.
+ /// @python_v20 Added new option **defaultbutton**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// ret = dialog.yesnocustom('Kodi', 'Question?', 'Maybe')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ yesnocustom(...);
+#else
+ int yesnocustom(const String& heading,
+ const String& message,
+ const String& customlabel,
+ const String& nolabel = emptyString,
+ const String& yeslabel = emptyString,
+ int autoclose = 0,
+ int defaultbutton = CONTROL_NO_BUTTON);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().info(listitem) }
+ /// **Info dialog**
+ ///
+ /// Show the corresponding info dialog for a given listitem
+ ///
+ /// @param listitem xbmcgui.ListItem - ListItem to show info for.
+ /// @return Returns whether the dialog opened successfully.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v17 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// ret = dialog.info(listitem)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ info(...);
+#else
+ bool info(const ListItem* item);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().select(heading, list[, autoclose, preselect, useDetails]) }
+ /// **Select dialog**
+ ///
+ /// Show of a dialog to select of an entry as a key
+ ///
+ /// @param heading string or unicode - dialog heading.
+ /// @param list list of strings / xbmcgui.ListItems - list of items shown in dialog.
+ /// @param autoclose [opt] integer - milliseconds to autoclose dialog. (default=do not autoclose)
+ /// @param preselect [opt] integer - index of preselected item. (default=no preselected item)
+ /// @param useDetails [opt] bool - use detailed list instead of a compact list. (default=false)
+ /// @return Returns the position of the highlighted item as an integer.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v17 **preselect** option added.
+ /// @python_v17 Added new option **useDetails**.
+ /// @python_v17 Allow listitems for parameter **list**
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// ret = dialog.select('Choose a playlist', ['Playlist #1', 'Playlist #2, 'Playlist #3'])
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ select(...);
+#else
+ int select(const String& heading, const std::vector<Alternative<String, const ListItem* > > & list, int autoclose=0, int preselect=-1, bool useDetails=false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().contextmenu(list) }
+ /// Show a context menu.
+ ///
+ /// @param list string list - list of items.
+ /// @return the position of the highlighted item as an integer
+ /// (-1 if cancelled).
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ /// @python_v17 New function added
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// ret = dialog.contextmenu(['Option #1', 'Option #2', 'Option #3'])
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ contextmenu(...);
+#else
+ int contextmenu(const std::vector<String>& list);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().multiselect(heading, options[, autoclose, preselect, useDetails]) }
+ /// Show a multi-select dialog.
+ ///
+ /// @param heading string or unicode - dialog heading.
+ /// @param options list of strings / xbmcgui.ListItems - options to choose from.
+ /// @param autoclose [opt] integer - milliseconds to autoclose dialog.
+ /// (default=do not autoclose)
+ /// @param preselect [opt] list of int - indexes of items to preselect
+ /// in list (default: do not preselect any item)
+ /// @param useDetails [opt] bool - use detailed list instead of a compact list. (default=false)
+ /// @return Returns the selected items as a list of indices,
+ /// or None if cancelled.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v16 New function added.
+ /// @python_v17 Added new option **preselect**.
+ /// @python_v17 Added new option **useDetails**.
+ /// @python_v17 Allow listitems for parameter **options**
+ ///
+ /// **Example:**
+ /// @code{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// ret = dialog.multiselect("Choose something", ["Foo", "Bar", "Baz"], preselect=[1,2])
+ /// ..
+ /// @endcode
+ ///
+ multiselect(...);
+#else
+ std::unique_ptr<std::vector<int> > multiselect(const String& heading, const std::vector<Alternative<String, const ListItem* > > & options, int autoclose=0, const std::vector<int>& preselect = std::vector<int>(), bool useDetails=false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().ok(heading, message) }
+ /// **OK dialog**
+ ///
+ /// The functions permit the call of a dialog of information, a
+ /// confirmation of the user by press from OK required.
+ ///
+ /// @param heading string or unicode - dialog heading.
+ /// @param message string or unicode - message text.
+ /// @return Returns True if 'Ok' was pressed, else False.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v19 Renamed option **line1** to **message**.
+ /// @python_v19 Removed option **line2**.
+ /// @python_v19 Removed option **line3**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// ok = dialog.ok('Kodi', 'There was an error.')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ ok(...);
+#else
+ bool ok(const String& heading, const String& message);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().textviewer(heading, text, usemono) }
+ /// **TextViewer dialog**
+ ///
+ /// The text viewer dialog can be used to display descriptions, help texts
+ /// or other larger texts.
+ ///
+ /// @param heading string or unicode - dialog heading.
+ /// @param text string or unicode - text.
+ /// @param usemono [opt] bool - use monospace font
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v16 New function added.
+ /// @python_v18 New optional param added **usemono**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// dialog.textviewer('Plot', 'Some movie plot.')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ textviewer(...);
+#else
+ void textviewer(const String& heading, const String& text, bool usemono = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().browse(type, heading, shares[, mask, useThumbs, treatAsFolder, defaultt, enableMultiple]) }
+ /// **Browser dialog**
+ ///
+ /// The function offer the possibility to select a file by the user of
+ /// the add-on.
+ ///
+ /// It allows all the options that are possible in Kodi itself and offers
+ /// all support file types.
+ ///
+ /// @param type integer - the type of browse dialog.
+ /// | Param | Name |
+ /// |:-----:|:--------------------------------|
+ /// | 0 | ShowAndGetDirectory |
+ /// | 1 | ShowAndGetFile |
+ /// | 2 | ShowAndGetImage |
+ /// | 3 | ShowAndGetWriteableDirectory |
+ /// @param heading string or unicode - dialog heading.
+ /// @param shares string or unicode - from [sources.xml](http://kodi.wiki/view/Sources.xml)
+ /// | Param | Name |
+ /// |:--------------:|:---------------------------------------------|
+ /// | "programs" | list program addons
+ /// | "video" | list video sources
+ /// | "music" | list music sources
+ /// | "pictures" | list picture sources
+ /// | "files" | list file sources (added through filemanager)
+ /// | "games" | list game sources
+ /// | "local" | list local drives
+ /// | "" | list local drives and network shares
+ /// @param mask [opt] string or unicode - '|' separated file mask. (i.e. '.jpg|.png')
+ /// @param useThumbs [opt] boolean - if True autoswitch to Thumb view if files exist.
+ /// @param treatAsFolder [opt] boolean - if True playlists and archives act as folders.
+ /// @param defaultt [opt] string - default path or file.
+ /// @param enableMultiple [opt] boolean - if True multiple file selection is enabled.
+ ///
+ /// @return If enableMultiple is False (default): returns filename and/or path as a string
+ /// to the location of the highlighted item, if user pressed 'Ok' or a masked item
+ /// was selected. Returns the default value if dialog was canceled.
+ /// If enableMultiple is True: returns tuple of marked filenames as a string
+ /// if user pressed 'Ok' or a masked item was selected. Returns empty tuple if dialog was canceled.\n\n
+ /// If type is 0 or 3 the enableMultiple parameter is ignore
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New option added to browse network and/or local drives.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// fn = dialog.browse(3, 'Kodi', 'files', '', False, False, False, 'special://masterprofile/script_data/Kodi Lyrics')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ browse(...);
+#else
+ Alternative<String, std::vector<String> > browse(int type, const String& heading, const String& shares,
+ const String& mask = emptyString, bool useThumbs = false,
+ bool treatAsFolder = false, const String& defaultt = emptyString,
+ bool enableMultiple = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().browseSingle(type, heading, shares[, mask, useThumbs, treatAsFolder, defaultt]) }
+ /// **Browse single dialog**
+ ///
+ /// The function offer the possibility to select a file by the user of
+ /// the add-on.
+ ///
+ /// It allows all the options that are possible in Kodi itself and offers
+ /// all support file types.
+ ///
+ /// @param type integer - the type of browse dialog.
+ /// | Param | Name |
+ /// |:-----:|:--------------------------------|
+ /// | 0 | ShowAndGetDirectory
+ /// | 1 | ShowAndGetFile
+ /// | 2 | ShowAndGetImage
+ /// | 3 | ShowAndGetWriteableDirectory
+ /// @param heading string or unicode - dialog heading.
+ /// @param shares string or unicode - from [sources.xml](http://kodi.wiki/view/Sources.xml)
+ /// | Param | Name |
+ /// |:--------------:|:---------------------------------------------|
+ /// | "programs" | list program addons
+ /// | "video" | list video sources
+ /// | "music" | list music sources
+ /// | "pictures" | list picture sources
+ /// | "files" | list file sources (added through filemanager)
+ /// | "games" | list game sources
+ /// | "local" | list local drives
+ /// | "" | list local drives and network shares
+ /// @param mask [opt] string or unicode - '|' separated file mask. (i.e. '.jpg|.png')
+ /// @param useThumbs [opt] boolean - if True autoswitch to Thumb view if files exist (default=false).
+ /// @param treatAsFolder [opt] boolean - if True playlists and archives act as folders (default=false).
+ /// @param defaultt [opt] string - default path or file.
+ ///
+ /// @return Returns filename and/or path as a string to the location of the highlighted item,
+ /// if user pressed 'Ok' or a masked item was selected.
+ /// Returns the default value if dialog was canceled.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New option added to browse network and/or local drives.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// fn = dialog.browseSingle(3, 'Kodi', 'files', '', False, False, 'special://masterprofile/script_data/Kodi Lyrics')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ browseSingle(...);
+#else
+ String browseSingle(int type, const String& heading, const String& shares,
+ const String& mask = emptyString, bool useThumbs = false,
+ bool treatAsFolder = false,
+ const String& defaultt = emptyString );
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().browseMultiple(type, heading, shares[, mask, useThumbs, treatAsFolder, defaultt]) }
+ /// **Browser dialog**
+ ///
+ /// The function offer the possibility to select multiple files by the
+ /// user of the add-on.
+ ///
+ /// It allows all the options that are possible in Kodi itself and offers
+ /// all support file types.
+ ///
+ /// @param type integer - the type of browse dialog.
+ /// | Param | Name |
+ /// |:-----:|:--------------------------------|
+ /// | 1 | ShowAndGetFile
+ /// | 2 | ShowAndGetImage
+ /// @param heading string or unicode - dialog heading.
+ /// @param shares string or unicode - from [sources.xml](http://kodi.wiki/view/Sources.xml)
+ /// | Param | Name |
+ /// |:--------------:|:---------------------------------------------|
+ /// | "programs" | list program addons
+ /// | "video" | list video sources
+ /// | "music" | list music sources
+ /// | "pictures" | list picture sources
+ /// | "files" | list file sources (added through filemanager)
+ /// | "games" | list game sources
+ /// | "local" | list local drives
+ /// | "" | list local drives and network shares
+ /// @param mask [opt] string or unicode - '|' separated file mask. (i.e. '.jpg|.png')
+ /// @param useThumbs [opt] boolean - if True autoswitch to Thumb view if files exist (default=false).
+ /// @param treatAsFolder [opt] boolean - if True playlists and archives act as folders (default=false).
+ /// @param defaultt [opt] string - default path or file.
+ /// @return Returns tuple of marked filenames as a string,"
+ /// if user pressed 'Ok' or a masked item was selected. Returns empty tuple if dialog was canceled.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New option added to browse network and/or local drives.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// fn = dialog.browseMultiple(2, 'Kodi', 'files', '', False, False, 'special://masterprofile/script_data/Kodi Lyrics')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ browseMultiple(...);
+#else
+ std::vector<String> browseMultiple(int type, const String& heading, const String& shares,
+ const String& mask = emptyString, bool useThumbs = false,
+ bool treatAsFolder = false,
+ const String& defaultt = emptyString );
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().numeric(type, heading[, defaultt, bHiddenInput]) }
+ /// **Numeric dialog**
+ ///
+ /// The function have to be permitted by the user for the representation
+ /// of a numeric keyboard around an input.
+ ///
+ /// @param type integer - the type of numeric dialog.
+ /// | Param | Name | Format |
+ /// |:-----:|:-------------------------|:-----------------------------|
+ /// | 0 | ShowAndGetNumber | (default format: #)
+ /// | 1 | ShowAndGetDate | (default format: DD/MM/YYYY)
+ /// | 2 | ShowAndGetTime | (default format: HH:MM)
+ /// | 3 | ShowAndGetIPAddress | (default format: #.#.#.#)
+ /// | 4 | ShowAndVerifyNewPassword | (default format: *)
+ /// @param heading string or unicode - dialog heading (will be ignored for type 4).
+ /// @param defaultt [opt] string - default value.
+ /// @param bHiddenInput [opt] bool - mask input (available for type 0).
+ /// @return Returns the entered data as a string.
+ /// Returns the default value if dialog was canceled.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v19 New option added ShowAndVerifyNewPassword.
+ /// @python_v19 Added new option **bHiddenInput**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// d = dialog.numeric(1, 'Enter date of birth')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ numeric(...);
+#else
+ String numeric(int type, const String& heading, const String& defaultt = emptyString, bool bHiddenInput = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().notification(heading, message[, icon, time, sound]) }
+ /// Show a Notification alert.
+ ///
+ /// @param heading string - dialog heading.
+ /// @param message string - dialog message.
+ /// @param icon [opt] string - icon to use. (default xbmcgui.NOTIFICATION_INFO)
+ /// @param time [opt] integer - time in milliseconds (default 5000)
+ /// @param sound [opt] bool - play notification sound (default True)
+ ///
+ /// Builtin Icons:
+ /// - xbmcgui.NOTIFICATION_INFO
+ /// - xbmcgui.NOTIFICATION_WARNING
+ /// - xbmcgui.NOTIFICATION_ERROR
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v13 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// dialog.notification('Movie Trailers', 'Finding Nemo download finished.', xbmcgui.NOTIFICATION_INFO, 5000)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ notification(...);
+#else
+ void notification(const String& heading, const String& message, const String& icon = emptyString, int time = 0, bool sound = true);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().input(heading[, defaultt, type, option, autoclose]) }
+ /// Show an Input dialog.
+ ///
+ /// @param heading string - dialog heading.
+ /// @param defaultt [opt] string - default value. (default=empty string)
+ /// @param type [opt] integer - the type of keyboard dialog. (default=xbmcgui.INPUT_ALPHANUM)
+ /// | Parameter | Format |
+ /// |---------------------------------:|:--------------------------------|
+ /// | <tt>xbmcgui.INPUT_ALPHANUM</tt> | (standard keyboard)
+ /// | <tt>xbmcgui.INPUT_NUMERIC</tt> | (format: #)
+ /// | <tt>xbmcgui.INPUT_DATE</tt> | (format: DD/MM/YYYY)
+ /// | <tt>xbmcgui.INPUT_TIME</tt> | (format: HH:MM)
+ /// | <tt>xbmcgui.INPUT_IPADDRESS</tt> | (format: #.#.#.#)
+ /// | <tt>xbmcgui.INPUT_PASSWORD</tt> | (return md5 hash of input, input is masked)
+ /// @param option [opt] integer - option for the dialog. (see Options below)
+ /// - Password dialog:
+ /// - <tt>xbmcgui.PASSWORD_VERIFY</tt> (verifies an existing (default) md5 hashed password)
+ /// - Alphanum dialog:
+ /// - <tt>xbmcgui.ALPHANUM_HIDE_INPUT</tt> (masks input)
+ /// @param autoclose [opt] integer - milliseconds to autoclose dialog. (default=do not autoclose)
+ ///
+ /// @return Returns the entered data as a string.
+ /// Returns an empty string if dialog was canceled.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v13 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.Dialog()
+ /// d = dialog.input('Enter secret code', type=xbmcgui.INPUT_ALPHANUM, option=xbmcgui.ALPHANUM_HIDE_INPUT)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ input(...);
+#else
+ String input(const String& heading,
+ const String& defaultt = emptyString,
+ int type = INPUT_ALPHANUM,
+ int option = 0,
+ int autoclose = 0);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Dialog
+ /// \python_func{ xbmcgui.Dialog().colorpicker(heading[, colorfile, colorlist, selectedcolor]) }
+ /// Show a color selection dialog.
+ ///
+ /// @param heading string - dialog heading.
+ /// @param selectedcolor [opt] string - hex value of the preselected color.
+ /// @param colorfile [opt] string - xml file containing color definitions.\n
+ /// **XML content style:**
+ /// ~~~~~~xml
+ /// <colors>
+ /// <color name="white">ffffffff</color>
+ /// <color name="grey">7fffffff</color>
+ /// <color name="green">ff00ff7f</color>
+ /// </colors>
+ /// ~~~~~~
+ /// @param colorlist [opt] xbmcgui.ListItems - where label defines the color name and label2 is set to the hex value.
+ ///
+ /// @return Returns the hex value of the selected color as a string.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// # colorfile example
+ /// dialog = xbmcgui.Dialog()
+ /// value = dialog.colorpicker('Select color', 'ff00ff00', 'os.path.join(xbmcaddon.Addon().getAddonInfo("path"), "colors.xml")')
+ /// ..
+ /// # colorlist example
+ /// listitems = []
+ /// l1 = xbmcgui.ListItem("red", "FFFF0000")
+ /// l2 = xbmcgui.ListItem("green", "FF00FF00")
+ /// l3 = xbmcgui.ListItem("blue", "FF0000FF")
+ /// listitems.append(l1)
+ /// listitems.append(l2)
+ /// listitems.append(l3)
+ /// dialog = xbmcgui.Dialog()
+ /// value = dialog.colorpicker("Select color", "FF0000FF", colorlist=listitems)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ colorpicker(...);
+#else
+ String colorpicker(
+ const String& heading,
+ const String& selectedcolor = emptyString,
+ const String& colorfile = emptyString,
+ const std::vector<const ListItem*>& colorlist = std::vector<const ListItem*>());
+#endif
+
+ private:
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+ // used by both yesno() and yesnocustom()
+ int yesNoCustomInternal(const String& heading,
+ const String& message,
+ const String& nolabel,
+ const String& yeslabel,
+ const String& customlabel,
+ int autoclose,
+ int defaultbutton);
+#endif
+ };
+ //@}
+
+ ///
+ /// \defgroup python_DialogProgress DialogProgress
+ /// \ingroup python_xbmcgui
+ /// @{
+ /// @brief <b>Kodi's progress dialog class (Duh!)</b>
+ ///
+ ///
+ class DialogProgress : public AddonClass
+ {
+ CGUIDialogProgress* dlg = nullptr;
+ bool open = false;
+
+ protected:
+ void deallocating() override;
+
+ public:
+
+ DialogProgress() = default;
+ ~DialogProgress() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_DialogProgress
+ /// \python_func{ xbmcgui.DialogProgress().create(heading[, message]) }
+ /// Create and show a progress dialog.
+ ///
+ /// @param heading string or unicode - dialog heading.
+ /// @param message [opt] string or unicode - message text.
+ ///
+ /// @note Use update() to update lines and progressbar.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v19 Renamed option **line1** to **message**.
+ /// @python_v19 Removed option **line2**.
+ /// @python_v19 Removed option **line3**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// pDialog = xbmcgui.DialogProgress()
+ /// pDialog.create('Kodi', 'Initializing script...')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ create(...);
+#else
+ void create(const String& heading, const String& message = emptyString);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_DialogProgress
+ /// \python_func{ xbmcgui.DialogProgress().update(percent[, message]) }
+ /// Updates the progress dialog.
+ ///
+ /// @param percent integer - percent complete. (0:100)
+ /// @param message [opt] string or unicode - message text.
+ ///
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v19 Renamed option **line1** to **message**.
+ /// @python_v19 Removed option **line2**.
+ /// @python_v19 Removed option **line3**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// pDialog.update(25, 'Importing modules...')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ update(...);
+#else
+ void update(int percent, const String& message = emptyString);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_DialogProgress
+ /// \python_func{ xbmcgui.DialogProgress().close() }
+ /// Close the progress dialog.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// pDialog.close()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ close(...);
+#else
+ void close();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_DialogProgress
+ /// \python_func{ xbmcgui.DialogProgress().iscanceled() }
+ /// Checks progress is canceled.
+ ///
+ /// @return True if the user pressed cancel.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// if (pDialog.iscanceled()): return
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ iscanceled(...);
+#else
+ bool iscanceled();
+#endif
+ };
+
+ //@}
+
+ ///
+ /// \defgroup python_DialogProgressBG DialogProgressBG
+ /// \ingroup python_xbmcgui
+ /// @{
+ /// @brief <b>Kodi's background progress dialog class</b>
+ ///
+ ///
+ class DialogProgressBG : public AddonClass
+ {
+ CGUIDialogExtendedProgressBar* dlg = nullptr;
+ CGUIDialogProgressBarHandle* handle = nullptr;
+ bool open = false;
+
+ protected:
+ void deallocating() override;
+
+ public:
+
+ DialogProgressBG() = default;
+ ~DialogProgressBG() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_DialogProgressBG
+ /// \python_func{ xbmcgui.DialogProgressBG().create(heading[, message]) }
+ /// Create and show a background progress dialog.
+ ///
+ /// @param heading string or unicode - dialog heading.
+ /// @param message [opt] string or unicode - message text.
+ ///
+ /// @note 'heading' is used for the dialog's id. Use a unique heading.
+ /// Use update() to update heading, message and progressbar.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// pDialog = xbmcgui.DialogProgressBG()
+ /// pDialog.create('Movie Trailers', 'Downloading Monsters Inc... .')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ create(...);
+#else
+ void create(const String& heading, const String& message = emptyString);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_DialogProgressBG
+ /// \python_func{ xbmcgui.DialogProgressBG().update([percent, heading, message]) }
+ /// Updates the background progress dialog.
+ ///
+ /// @param percent [opt] integer - percent complete. (0:100)
+ /// @param heading [opt] string or unicode - dialog heading.
+ /// @param message [opt] string or unicode - message text.
+ ///
+ /// @note To clear heading or message, you must pass a blank character.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// pDialog.update(25, message='Downloading Finding Nemo ...')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ update(...);
+#else
+ void update(int percent = 0, const String& heading = emptyString, const String& message = emptyString);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_DialogProgressBG
+ /// \python_func{ xbmcgui.DialogProgressBG().close() }
+ /// Close the background progress dialog
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// pDialog.close()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ close(...);
+#else
+ void close();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_DialogProgressBG
+ /// \python_func{ xbmcgui.DialogProgressBG().isFinished() }
+ /// Checks progress is finished
+ ///
+ /// @return True if the background dialog is active.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// if (pDialog.isFinished()): return
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ isFinished(...);
+#else
+ bool isFinished();
+#endif
+ };
+ //@}
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+ SWIG_CONSTANT2(int, DLG_YESNO_NO_BTN, CONTROL_NO_BUTTON);
+ SWIG_CONSTANT2(int, DLG_YESNO_YES_BTN, CONTROL_YES_BUTTON);
+ SWIG_CONSTANT2(int, DLG_YESNO_CUSTOM_BTN, CONTROL_CUSTOM_BUTTON);
+#endif
+} // namespace xbmcgui
+} // namespace XBMCAddon
diff --git a/xbmc/interfaces/legacy/Dictionary.h b/xbmc/interfaces/legacy/Dictionary.h
new file mode 100644
index 0000000..65a4db8
--- /dev/null
+++ b/xbmc/interfaces/legacy/Dictionary.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "AddonString.h"
+
+#include <map>
+
+namespace XBMCAddon
+{
+ // This is a hack in order to handle int's as strings. The correct fix for
+ // this is to get rid of Alternative all together and make the codegenerator
+ // finally handle overloading correctly.
+ typedef String StringOrInt;
+
+ /**
+ * This is a bit of a hack for dynamically typed languages. In some
+ * cases python addon api calls handle dictionaries with variable
+ * value types. In this case we coerce all of these types into
+ * strings and then convert them back in the api. Yes, this is messy
+ * and maybe we should use the CVariant here. But for now the
+ * native api handles these calls by converting the string to the
+ * appropriate types.
+ */
+ template<class T> class Dictionary : public std::map<String,T> {};
+
+ typedef Dictionary<StringOrInt> Properties;
+}
diff --git a/xbmc/interfaces/legacy/DrmCryptoSession.cpp b/xbmc/interfaces/legacy/DrmCryptoSession.cpp
new file mode 100644
index 0000000..26f69be
--- /dev/null
+++ b/xbmc/interfaces/legacy/DrmCryptoSession.cpp
@@ -0,0 +1,108 @@
+/*
+ * 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 "DrmCryptoSession.h"
+
+#include "media/drm/CryptoSession.h"
+
+using namespace XbmcCommons;
+
+namespace XBMCAddon
+{
+ namespace xbmcdrm
+ {
+ CryptoSession::CryptoSession(const String& UUID,
+ const String& cipherAlgorithm,
+ const String& macAlgorithm)
+ : m_cryptoSession(DRM::CCryptoSession::GetCryptoSession(UUID, cipherAlgorithm, macAlgorithm))
+ {
+ }
+
+ CryptoSession::~CryptoSession()
+ {
+ delete m_cryptoSession;
+ }
+
+ Buffer CryptoSession::GetKeyRequest(const Buffer &init, const String &mimeType, bool offlineKey, const std::map<String, String> &parameters)
+ {
+ if (m_cryptoSession)
+ return m_cryptoSession->GetKeyRequest(init, mimeType, offlineKey, parameters);
+
+ return Buffer();
+ }
+
+ String CryptoSession::GetPropertyString(const String &name)
+ {
+ if (m_cryptoSession)
+ return m_cryptoSession->GetPropertyString(name);
+
+ return "";
+ }
+
+ String CryptoSession::ProvideKeyResponse(const Buffer &response)
+ {
+ if (m_cryptoSession)
+ return m_cryptoSession->ProvideKeyResponse(response);
+
+ return "";
+ }
+
+ void CryptoSession::RemoveKeys()
+ {
+ if (m_cryptoSession)
+ m_cryptoSession->RemoveKeys();
+ }
+
+ void CryptoSession::RestoreKeys(const String& keySetId)
+ {
+ if (m_cryptoSession)
+ m_cryptoSession->RestoreKeys(keySetId);
+ }
+
+ void CryptoSession::SetPropertyString(const String &name, const String &value)
+ {
+ if (m_cryptoSession)
+ return m_cryptoSession->SetPropertyString(name, value);
+ }
+
+ /*******************Crypto section *****************/
+
+ Buffer CryptoSession::Decrypt(const Buffer &cipherKeyId, const Buffer &input, const Buffer &iv)
+ {
+ if (m_cryptoSession)
+ return m_cryptoSession->Decrypt(cipherKeyId, input, iv);
+
+ return Buffer();
+ }
+
+ Buffer CryptoSession::Encrypt(const Buffer &cipherKeyId, const Buffer &input, const Buffer &iv)
+ {
+ if (m_cryptoSession)
+ return m_cryptoSession->Encrypt(cipherKeyId, input, iv);
+
+ return Buffer();
+ }
+
+ Buffer CryptoSession::Sign(const Buffer &macKeyId, const Buffer &message)
+ {
+ if (m_cryptoSession)
+ return m_cryptoSession->Sign(macKeyId, message);
+
+ return Buffer();
+ }
+
+ bool CryptoSession::Verify(const Buffer &macKeyId, const Buffer &message, const Buffer &signature)
+ {
+ if (m_cryptoSession)
+ return m_cryptoSession->Verify(macKeyId, message, signature);
+
+ return false;
+ }
+
+ } //namespace xbmcdrm
+} //namespace XBMCAddon
diff --git a/xbmc/interfaces/legacy/DrmCryptoSession.h b/xbmc/interfaces/legacy/DrmCryptoSession.h
new file mode 100644
index 0000000..816bec8
--- /dev/null
+++ b/xbmc/interfaces/legacy/DrmCryptoSession.h
@@ -0,0 +1,342 @@
+/*
+ * 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 "AddonClass.h"
+#include "Exception.h"
+#include "commons/Buffer.h"
+
+#include <map>
+#include <vector>
+
+namespace DRM
+{
+ class CCryptoSession;
+}
+
+namespace XBMCAddon
+{
+
+ typedef std::vector<char> charVec;
+
+ namespace xbmcdrm
+ {
+
+ XBMCCOMMONS_STANDARD_EXCEPTION(DRMException);
+
+ //
+ /// \defgroup python_xbmcdrm Library - xbmcdrm
+ ///@{
+ /// @brief **Kodi's %DRM class.**
+ ///
+ /// Offers classes and functions that allow a developer to work with
+ /// DRM-protected contents like Widevine.
+ ///
+ /// This type of functionality is closely related to the type of %DRM
+ /// used and the service to be implemented.
+ ///
+ /// Using the \ref xbmcdrm_CryptoSession "CryptoSession" constructor allow you
+ /// to have access to a %DRM session.
+ /// With a %DRM session you can read and write the %DRM properties
+ /// \ref xbmcdrm_GetPropertyString "GetPropertyString",
+ /// \ref xbmcdrm_SetPropertyString "SetPropertyString"
+ /// and establish session keys with
+ /// \ref xbmcdrm_GetKeyRequest "GetKeyRequest" and
+ /// \ref xbmcdrm_ProvideKeyResponse "ProvideKeyResponse",
+ /// or resume previous session keys with
+ /// \ref xbmcdrm_RestoreKeys "RestoreKeys".
+ ///
+ /// When the session keys are established you can use these methods
+ /// to perform various operations:
+ /// \ref xbmcdrm_Encrypt "Encrypt" /
+ /// \ref xbmcdrm_Decrypt "Decrypt" for data encryption / decryption,
+ /// \ref xbmcdrm_Sign "Sign" /
+ /// \ref xbmcdrm_Verify "Verify" for make or verify data-signature.
+ /// Useful for example to implement encrypted communication between
+ /// a client and the server.
+ ///
+ /// An example where such functionality is useful is the Message
+ /// Security Layer (MSL) transmission protocol used in some VOD applications.
+ /// This protocol (or rather framework) is used to increase the level of security
+ /// in the exchange of messages (such as licences, manifests or other data),
+ /// which defines a security extension / layer on top of the HTTP protocol.
+ ///
+ ///--------------------------------------------------------------------------
+ /// Constructor for %DRM crypto session
+ ///
+ /// \anchor xbmcdrm_CryptoSession
+ /// \python_class{ xbmcdrm.CryptoSession(UUID, cipherAlgorithm, macAlgorithm) }
+ ///
+ /// @param UUID string - 16 byte UUID of the %DRM system to use
+ /// @param cipherAlgorithm string - Algorithm used for encryption / decryption ciphers
+ /// @param macAlgorithm string - Algorithm used for sign / verify
+ ///
+ /// @throws RuntimeException If the session can not be established
+ ///
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New class added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// uuid_widevine = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'
+ /// crypto_session = xbmcdrm.CryptoSession(uuid_widevine, 'AES/CBC/NoPadding', 'HmacSHA256')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ class CryptoSession : public AddonClass
+ {
+ DRM::CCryptoSession* m_cryptoSession;
+ public:
+ CryptoSession(const String& UUID, const String& cipherAlgorithm, const String& macAlgorithm);
+ ~CryptoSession() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcdrm
+ /// @brief \python_func{ GetKeyRequest(init, mimeType, offlineKey, optionalParameters) }
+ /// Generate a key request
+ ///
+ /// Generate a key request, used for request/response exchange between the app
+ /// and a license server to obtain or release keys used to decrypt encrypted content.
+ /// After the app has received the key request response from the license server,
+ /// it should deliver to the response to the %DRM instance using
+ /// the method \ref xbmcdrm_ProvideKeyResponse "ProvideKeyResponse", to activate the keys.
+ // \anchor xbmcdrm_GetKeyRequest
+ ///
+ /// @param init byte - Initialization bytes container-specific data,
+ /// its meaning is interpreted based on the mime type provided
+ /// in the mimeType parameter. It could contain, for example,
+ /// the content ID, key ID or other data required in generating
+ /// the key request.
+ /// @param mimeType string - Type of media which is exchanged
+ /// (e.g. "application/xml", "video/mp4")
+ /// @param offlineKey bool - Specifies the type of the request.
+ /// The request may be to acquire keys for Streaming or Offline content
+ /// @param optionalParameters [opt] map - Will be included in the key request message
+ /// to allow a client application to provide additional
+ /// message parameters to the server
+ ///
+ /// @return byte - The opaque key request data (challenge) which is send to key server
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ /// @python_v19 With python 3 the init param must be a bytearray instead of byte.
+ ///
+ GetKeyRequest(...);
+#else
+ XbmcCommons::Buffer GetKeyRequest(const XbmcCommons::Buffer &init, const String &mimeType, bool offlineKey, const std::map<String, String> &optionalParameters);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcdrm
+ /// @brief \python_func{ GetPropertyString(name) }
+ /// Request a system specific property value of the %DRM system.
+ ///
+ ///\anchor xbmcdrm_GetPropertyString
+ /// @param Name string - Name of the property to query
+ ///
+ /// @return Value of the requested property
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ GetPropertyString(...);
+#else
+ String GetPropertyString(const String &name);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcdrm
+ /// @brief \python_func{ ProvideKeyResponse(response) }
+ /// Provide a key response
+ ///
+ /// \anchor xbmcdrm_ProvideKeyResponse
+ /// When a key response is received from the license server,
+ /// must be sent to the %DRM instance by using provideKeyResponse.
+ /// See also \ref xbmcdrm_GetKeyRequest "GetKeyRequest".
+ ///
+ /// @param response byte - Key data returned from the license server
+ ///
+ /// @return A keySetId if the response is for an offline key requests which
+ /// can be used later with restoreKeys,
+ /// else return empty for streaming key requests.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ /// @python_v19 With python 3 the response argument must be a bytearray instead of byte.
+ ///
+ ProvideKeyResponse(...);
+#else
+ String ProvideKeyResponse(const XbmcCommons::Buffer &response);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcdrm
+ /// @brief \python_func{ RemoveKeys() }
+ /// Removes all keys currently loaded in a session.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ RemoveKeys(...);
+#else
+ void RemoveKeys();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcdrm
+ /// @brief \python_func{ RestoreKeys(keySetId) }
+ /// Restores session keys stored during previous
+ /// \ref xbmcdrm_ProvideKeyResponse "ProvideKeyResponse" call.
+ /// \anchor xbmcdrm_RestoreKeys
+ ///
+ /// @param keySetId string - Identifies the saved key set to restore.
+ /// This value must never be null.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ RestoreKeys(...);
+#else
+ void RestoreKeys(const String& keySetId);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcdrm
+ /// @brief \python_func{ SetPropertyString(name, value) }
+ /// Set a system specific property value in the %DRM system.
+ ///
+ /// \anchor xbmcdrm_SetPropertyString
+ ///
+ /// @param name string - Name of the property. This value must never be null.
+ /// @param value string - Value of the property to set. This value must never be null.
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ SetPropertyString(...);
+#else
+ void SetPropertyString(const String &name, const String &value);
+#endif
+
+/*******************Crypto section *****************/
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcdrm
+ /// @brief \python_func{ Decrypt(cipherKeyId, input, iv) }
+ /// Decrypt an encrypted data by using session keys.
+ ///
+ /// \anchor xbmcdrm_Decrypt
+ ///
+ /// @param cipherKeyId byte - Encryption key id (provided from a service handshake)
+ /// @param input byte - Cipher text to decrypt
+ /// @param iv byte - Initialization vector of cipher text
+ ///
+ /// @return Decrypted input data
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ /// @python_v19 With python 3 all arguments need to be of type bytearray instead of byte.
+ ///
+ Decrypt(...);
+#else
+ XbmcCommons::Buffer Decrypt(const XbmcCommons::Buffer &cipherKeyId, const XbmcCommons::Buffer &input, const XbmcCommons::Buffer &iv);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcdrm
+ /// @brief \python_func{ Encrypt(cipherKeyId, input, iv) }
+ /// Encrypt data by using session keys.
+ ///
+ /// \anchor xbmcdrm_Encrypt
+ ///
+ /// @param cipherKeyId byte - Encryption key id (provided from a service handshake)
+ /// @param input byte - Encrypted text
+ /// @param iv byte - Initialization vector of encrypted text
+ ///
+ /// @return byte - Encrypted input data
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ /// @python_v19 With python 3 all arguments need to be of type bytearray instead of byte.
+ ///
+ Encrypt(...);
+#else
+ XbmcCommons::Buffer Encrypt(const XbmcCommons::Buffer &cipherKeyId, const XbmcCommons::Buffer &input, const XbmcCommons::Buffer &iv);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcdrm
+ /// @brief \python_func{ Sign(macKeyId, message) }
+ /// Generate a %DRM encrypted signature for a text message.
+ ///
+ /// \anchor xbmcdrm_Sign
+ ///
+ /// @param macKeyId byte - HMAC key id (provided from a service handshake)
+ /// @param message byte - Message text on which to base the signature
+ ///
+ /// @return byte - Signature
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ /// @python_v19 With python 3 all arguments need to be of type bytearray instead of byte.
+ ///
+ Sign(...);
+#else
+ XbmcCommons::Buffer Sign(const XbmcCommons::Buffer &macKeyId, const XbmcCommons::Buffer &message);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcdrm
+ /// @brief \python_func{ Verify(macKeyId, message, signature) }
+ /// Verify the validity of a %DRM signature of a text message.
+ ///
+ /// \anchor xbmcdrm_Verify
+ ///
+ /// @param macKeyId byte - HMAC key id (provided from a service handshake)
+ /// @param message byte - Message text on which the signature is based
+ /// @param signature byte - The signature to verify
+ ///
+ /// @return true when the signature is valid
+ ///
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ /// @python_v19 With python 3 for all arguments is needed to pass bytearray instead of byte.
+ ///
+ Verify(...);
+#else
+ bool Verify(const XbmcCommons::Buffer &macKeyId, const XbmcCommons::Buffer &message, const XbmcCommons::Buffer &signature);
+#endif
+
+ };
+ ///@}
+ }
+}
diff --git a/xbmc/interfaces/legacy/Exception.h b/xbmc/interfaces/legacy/Exception.h
new file mode 100644
index 0000000..61a32be
--- /dev/null
+++ b/xbmc/interfaces/legacy/Exception.h
@@ -0,0 +1,55 @@
+/*
+ * 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 "commons/Exception.h"
+#include "utils/log.h"
+
+#ifndef SWIG
+namespace XBMCAddon
+{
+ XBMCCOMMONS_STANDARD_EXCEPTION(WrongTypeException);
+
+ /**
+ * UnimplementedException Can be used in places like the
+ * Control hierarchy where the
+ * requirements of dynamic language usage force us to add
+ * unimplemented methods to a class hierarchy. See the
+ * detailed explanation on the class Control for more.
+ */
+ class UnimplementedException : public XbmcCommons::Exception
+ {
+ public:
+ inline UnimplementedException(const UnimplementedException& other) = default;
+ inline UnimplementedException(const char* classname, const char* methodname) :
+ Exception("UnimplementedException")
+ { SetMessage("Unimplemented method: %s::%s(...)", classname, methodname); }
+ };
+
+ /**
+ * This is what callback exceptions from the scripting language
+ * are translated to.
+ */
+ class UnhandledException : public XbmcCommons::Exception
+ {
+ public:
+ inline UnhandledException(const UnhandledException& other) = default;
+ inline UnhandledException(const char* _message,...) : Exception("UnhandledException") { XBMCCOMMONS_COPYVARARGS(_message); }
+ };
+}
+#endif
+
+/**
+ * These macros allow the easy declaration (and definition) of parent
+ * class virtual methods that are not implemented until the child class.
+ * This is to support the idosyncracies of dynamically typed scripting
+ * languages. See the comment in AddonControl.h for more details.
+ */
+#define THROW_UNIMP(classname) throw UnimplementedException(classname, __FUNCTION__)
+
diff --git a/xbmc/interfaces/legacy/File.cpp b/xbmc/interfaces/legacy/File.cpp
new file mode 100644
index 0000000..4c0ca68
--- /dev/null
+++ b/xbmc/interfaces/legacy/File.cpp
@@ -0,0 +1,61 @@
+/*
+ * 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 "File.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcvfs
+ {
+ XbmcCommons::Buffer File::readBytes(unsigned long numBytes)
+ {
+ DelayedCallGuard dg(languageHook);
+ int64_t size = file->GetLength();
+ if ((!numBytes || (((int64_t)numBytes) > size)) && (size >= 0))
+ numBytes = (unsigned long) size;
+
+
+ XbmcCommons::Buffer ret(numBytes);
+
+ if (numBytes == 0)
+ return ret;
+
+ while(ret.remaining() > 0)
+ {
+ ssize_t bytesRead = file->Read(ret.curPosition(), ret.remaining());
+ if (bytesRead <= 0) // we consider this a failure or a EOF, can't tell which,
+ { // return whatever we have already.
+ ret.flip();
+ return ret;
+ }
+ ret.forward(bytesRead);
+ }
+ ret.flip();
+ return ret;
+ }
+
+ bool File::write(XbmcCommons::Buffer& buffer)
+ {
+ DelayedCallGuard dg(languageHook);
+ while (buffer.remaining() > 0)
+ {
+ ssize_t bytesWritten = file->Write( buffer.curPosition(), buffer.remaining());
+ if (bytesWritten == 0) // this could be a failure (see HDFile, and XFileUtils) or
+ // it could mean something else when a negative number means an error
+ // (see CCurlFile). There is no consistency so we can only assume we're
+ // done when we get a 0.
+ return false;
+ else if (bytesWritten < 0) // But, if we get something less than zero, we KNOW it's an error.
+ return false;
+ buffer.forward(bytesWritten);// Otherwise, we advance the buffer by the amount written.
+ }
+ return true;
+ }
+
+ }
+}
diff --git a/xbmc/interfaces/legacy/File.h b/xbmc/interfaces/legacy/File.h
new file mode 100644
index 0000000..01a757a
--- /dev/null
+++ b/xbmc/interfaces/legacy/File.h
@@ -0,0 +1,326 @@
+/*
+ * 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 "AddonClass.h"
+#include "AddonString.h"
+#include "LanguageHook.h"
+#include "commons/Buffer.h"
+#include "filesystem/File.h"
+
+#include <algorithm>
+
+namespace XBMCAddon
+{
+
+ namespace xbmcvfs
+ {
+
+ //
+ /// \defgroup python_file File
+ /// \ingroup python_xbmcvfs
+ /// @{
+ /// @brief <b>Kodi's file class.</b>
+ ///
+ /// \python_class{ xbmcvfs.File(filepath, [mode]) }
+ ///
+ /// @param filepath string Selected file path
+ /// @param mode [opt] string Additional mode options (if no mode is supplied, the default is Open for Read).
+ /// | Mode | Description |
+ /// |:------:|:--------------------------------|
+ /// | w | Open for write |
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ /// @python_v19 Added context manager support
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// f = xbmcvfs.File(file, 'w')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ /// **Example (v19 and up):**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// with xbmcvfs.File(file, 'w') as f:
+ /// ..
+ /// ..
+ /// ~~~~~~~~~~~~~
+ //
+ class File : public AddonClass
+ {
+ XFILE::CFile* file;
+ public:
+ inline File(const String& filepath, const char* mode = NULL) : file(new XFILE::CFile())
+ {
+ DelayedCallGuard dg(languageHook);
+ if (mode && strncmp(mode, "w", 1) == 0)
+ file->OpenForWrite(filepath,true);
+ else
+ file->Open(filepath, XFILE::READ_NO_CACHE);
+ }
+
+ inline ~File() override { delete file; }
+
+#if !defined(DOXYGEN_SHOULD_USE_THIS)
+ inline File* __enter__() { return this; }
+ inline void __exit__() { close(); }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_file
+ /// @brief \python_func{ read([bytes]) }
+ /// Read file parts as string.
+ ///
+ /// @param bytes [opt] How many bytes to read - if not
+ /// set it will read the whole file
+ /// @return string
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// f = xbmcvfs.File(file)
+ /// b = f.read()
+ /// f.close()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ /// **Example (v19 and up):**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// with xbmcvfs.File(file) as file:
+ /// b = f.read()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ read(...);
+#else
+ inline String read(unsigned long numBytes = 0)
+ {
+ XbmcCommons::Buffer b = readBytes(numBytes);
+ return b.getString(numBytes == 0 ? b.remaining() : std::min((unsigned long)b.remaining(),numBytes));
+ }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_file
+ /// @brief \python_func{ readBytes(numbytes) }
+ /// Read bytes from file.
+ ///
+ /// @param numbytes How many bytes to read [opt]- if not set
+ /// it will read the whole file
+ /// @return bytearray
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// f = xbmcvfs.File(file)
+ /// b = f.readBytes()
+ /// f.close()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ /// **Example (v19 and up):**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// with xbmcvfs.File(file) as f:
+ /// b = f.readBytes()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ readBytes(...);
+#else
+ XbmcCommons::Buffer readBytes(unsigned long numBytes = 0);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_file
+ /// @brief \python_func{ write(buffer) }
+ /// To write given data in file.
+ ///
+ /// @param buffer Buffer to write to file
+ /// @return True on success.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// f = xbmcvfs.File(file, 'w')
+ /// result = f.write(buffer)
+ /// f.close()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ /// **Example (v19 and up):**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// with xbmcvfs.File(file, 'w') as f:
+ /// result = f.write(buffer)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ write(...);
+#else
+ bool write(XbmcCommons::Buffer& buffer);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_file
+ /// @brief \python_func{ size() }
+ /// Get the file size.
+ ///
+ /// @return The file size
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// f = xbmcvfs.File(file)
+ /// s = f.size()
+ /// f.close()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ /// **Example (v19 and up):**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// with xbmcvfs.File(file) as f:
+ /// s = f.size()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ size();
+#else
+ inline long long size() { DelayedCallGuard dg(languageHook); return file->GetLength(); }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_file
+ /// @brief \python_func{ seek(seekBytes, iWhence) }
+ /// Seek to position in file.
+ ///
+ /// @param seekBytes position in the file
+ /// @param iWhence [opt] where in a file to seek from[0 beginning,
+ /// 1 current , 2 end position]
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v19 Function changed. param **iWhence** is now optional.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// f = xbmcvfs.File(file)
+ /// result = f.seek(8129, 0)
+ /// f.close()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ /// **Example (v19 and up):**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// with xbmcvfs.File(file) as f:
+ /// result = f.seek(8129, 0)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ seek(...);
+#else
+ inline long long seek(long long seekBytes, int iWhence = SEEK_SET) { DelayedCallGuard dg(languageHook); return file->Seek(seekBytes,iWhence); }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_file
+ /// @brief \python_func{ tell() }
+ /// Get the current position in the file.
+ ///
+ /// @return The file position
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v19 New function added
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// f = xbmcvfs.File(file)
+ /// s = f.tell()
+ /// f.close()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ /// **Example (v19 and up):**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// with xbmcvfs.File(file) as f:
+ /// s = f.tell()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ tell();
+#else
+ inline long long tell() { DelayedCallGuard dg(languageHook); return file->GetPosition(); }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_file
+ /// @brief \python_func{ close() }
+ /// Close opened file.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// f = xbmcvfs.File(file)
+ /// f.close()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ /// **Example (v19 and up):**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// with xbmcvfs.File(file) as f:
+ /// ..
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ close();
+#else
+ inline void close() { DelayedCallGuard dg(languageHook); file->Close(); }
+#endif
+
+#ifndef SWIG
+ inline const XFILE::CFile* getFile() const { return file; }
+#endif
+
+ };
+ //@}
+ }
+}
diff --git a/xbmc/interfaces/legacy/InfoTagGame.cpp b/xbmc/interfaces/legacy/InfoTagGame.cpp
new file mode 100644
index 0000000..697247b
--- /dev/null
+++ b/xbmc/interfaces/legacy/InfoTagGame.cpp
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2021 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 "InfoTagGame.h"
+
+#include "AddonUtils.h"
+#include "games/tags/GameInfoTag.h"
+
+using namespace KODI::GAME;
+using namespace XBMCAddonUtils;
+
+namespace XBMCAddon
+{
+namespace xbmc
+{
+
+InfoTagGame::InfoTagGame(bool offscreen /* = false */)
+ : infoTag(new CGameInfoTag), offscreen(offscreen), owned(true)
+{
+}
+
+InfoTagGame::InfoTagGame(const CGameInfoTag* tag)
+ : infoTag(new CGameInfoTag(*tag)), offscreen(true), owned(true)
+{
+}
+
+InfoTagGame::InfoTagGame(CGameInfoTag* tag, bool offscreen /* = false */)
+ : infoTag(tag), offscreen(offscreen), owned(false)
+{
+}
+
+InfoTagGame::~InfoTagGame()
+{
+ if (owned)
+ delete infoTag;
+}
+
+String InfoTagGame::getTitle()
+{
+ return infoTag->GetTitle();
+}
+
+String InfoTagGame::getPlatform()
+{
+ return infoTag->GetPlatform();
+}
+
+std::vector<String> InfoTagGame::getGenres()
+{
+ return infoTag->GetGenres();
+}
+
+String InfoTagGame::getPublisher()
+{
+ return infoTag->GetPublisher();
+}
+
+String InfoTagGame::getDeveloper()
+{
+ return infoTag->GetDeveloper();
+}
+
+String InfoTagGame::getOverview()
+{
+ return infoTag->GetOverview();
+}
+
+unsigned int InfoTagGame::getYear()
+{
+ return infoTag->GetYear();
+}
+
+String InfoTagGame::getGameClient()
+{
+ return infoTag->GetGameClient();
+}
+
+void InfoTagGame::setTitle(const String& title)
+{
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setTitleRaw(infoTag, title);
+}
+
+void InfoTagGame::setPlatform(const String& platform)
+{
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setPlatformRaw(infoTag, platform);
+}
+
+void InfoTagGame::setGenres(const std::vector<String>& genres)
+{
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setGenresRaw(infoTag, genres);
+}
+
+void InfoTagGame::setPublisher(const String& publisher)
+{
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setPublisherRaw(infoTag, publisher);
+}
+
+void InfoTagGame::setDeveloper(const String& developer)
+{
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setDeveloperRaw(infoTag, developer);
+}
+
+void InfoTagGame::setOverview(const String& overview)
+{
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setOverviewRaw(infoTag, overview);
+}
+
+void InfoTagGame::setYear(unsigned int year)
+{
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setYearRaw(infoTag, year);
+}
+
+void InfoTagGame::setGameClient(const String& gameClient)
+{
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setGameClientRaw(infoTag, gameClient);
+}
+
+void InfoTagGame::setTitleRaw(KODI::GAME::CGameInfoTag* infoTag, const String& title)
+{
+ infoTag->SetTitle(title);
+}
+
+void InfoTagGame::setPlatformRaw(KODI::GAME::CGameInfoTag* infoTag, const String& platform)
+{
+ infoTag->SetPlatform(platform);
+}
+
+void InfoTagGame::setGenresRaw(KODI::GAME::CGameInfoTag* infoTag, const std::vector<String>& genres)
+{
+ infoTag->SetGenres(genres);
+}
+
+void InfoTagGame::setPublisherRaw(KODI::GAME::CGameInfoTag* infoTag, const String& publisher)
+{
+ infoTag->SetPublisher(publisher);
+}
+
+void InfoTagGame::setDeveloperRaw(KODI::GAME::CGameInfoTag* infoTag, const String& developer)
+{
+ infoTag->SetDeveloper(developer);
+}
+
+void InfoTagGame::setOverviewRaw(KODI::GAME::CGameInfoTag* infoTag, const String& overview)
+{
+ infoTag->SetOverview(overview);
+}
+
+void InfoTagGame::setYearRaw(KODI::GAME::CGameInfoTag* infoTag, unsigned int year)
+{
+ infoTag->SetYear(year);
+}
+
+void InfoTagGame::setGameClientRaw(KODI::GAME::CGameInfoTag* infoTag, const String& gameClient)
+{
+ infoTag->SetGameClient(gameClient);
+}
+
+} // namespace xbmc
+} // namespace XBMCAddon
diff --git a/xbmc/interfaces/legacy/InfoTagGame.h b/xbmc/interfaces/legacy/InfoTagGame.h
new file mode 100644
index 0000000..a6161b4
--- /dev/null
+++ b/xbmc/interfaces/legacy/InfoTagGame.h
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2021 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 "AddonClass.h"
+
+namespace KODI
+{
+namespace GAME
+{
+class CGameInfoTag;
+}
+} // namespace KODI
+
+namespace XBMCAddon
+{
+namespace xbmc
+{
+///
+/// \defgroup python_InfoTagGame InfoTagGame
+/// \ingroup python_xbmc
+/// @{
+/// @brief **Kodi's game info tag class.**
+///
+/// \python_class{ InfoTagGame() }
+///
+/// Access and / or modify the game metadata of a ListItem.
+///
+///-------------------------------------------------------------------------
+/// @python_v20 New class added.
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.py}
+/// ...
+/// tag = item.getGameInfoTag()
+///
+/// title = tag.getTitle()
+/// tag.setDeveloper('John Doe')
+/// ...
+/// ~~~~~~~~~~~~~
+///
+class InfoTagGame : public AddonClass
+{
+private:
+ KODI::GAME::CGameInfoTag* infoTag;
+ bool offscreen;
+ bool owned;
+
+public:
+#ifndef SWIG
+ explicit InfoTagGame(const KODI::GAME::CGameInfoTag* tag);
+ explicit InfoTagGame(KODI::GAME::CGameInfoTag* tag, bool offscreen = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ xbmc.InfoTagGame([offscreen]) }
+ /// Create a game info tag.
+ ///
+ /// @param offscreen [opt] bool (default `False`) - if GUI based locks should be
+ /// avoided. Most of the times listitems are created
+ /// offscreen and added later to a container
+ /// for display (e.g. plugins) or they are not
+ /// even displayed (e.g. python scrapers).
+ /// In such cases, there is no need to lock the
+ /// GUI when creating the items (increasing your addon
+ /// performance).
+ /// Note however, that if you are creating listitems
+ /// and managing the container itself (e.g using
+ /// WindowXML or WindowXMLDialog classes) subsquent
+ /// modifications to the item will require locking.
+ /// Thus, in such cases, use the default value (`False`).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// gameinfo = xbmc.InfoTagGame(offscreen=False)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ InfoTagGame(...);
+#else
+ explicit InfoTagGame(bool offscreen = false);
+#endif
+ ~InfoTagGame() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ getTitle() }
+ /// Gets the title of the game.
+ ///
+ /// @return [string] title
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getTitle();
+#else
+ String getTitle();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ getPlatform() }
+ /// Gets the platform on which the game is run.
+ ///
+ /// @return [string] platform
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getPlatform();
+#else
+ String getPlatform();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ getGenres() }
+ /// Gets the genres of the game.
+ ///
+ /// @return [list] genres
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getGenres();
+#else
+ std::vector<String> getGenres();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ getPublisher() }
+ /// Gets the publisher of the game.
+ ///
+ /// @return [string] publisher
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getPublisher();
+#else
+ String getPublisher();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ getDeveloper() }
+ /// Gets the developer of the game.
+ ///
+ /// @return [string] developer
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getDeveloper();
+#else
+ String getDeveloper();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ getOverview() }
+ /// Gets the overview of the game.
+ ///
+ /// @return [string] overview
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getOverview();
+#else
+ String getOverview();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ getYear() }
+ /// Gets the year in which the game was published.
+ ///
+ /// @return [integer] year
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getYear();
+#else
+ unsigned int getYear();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ getGameClient() }
+ /// Gets the add-on ID of the game client executing the game.
+ ///
+ /// @return [string] game client
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getGameClient();
+#else
+ String getGameClient();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ setTitle(title) }
+ /// Sets the title of the game.
+ ///
+ /// @param title string - title.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setTitle(...);
+#else
+ void setTitle(const String& title);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ setPlatform(platform) }
+ /// Sets the platform on which the game is run.
+ ///
+ /// @param platform string - platform.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setPlatform(...);
+#else
+ void setPlatform(const String& platform);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ setGenres(genres) }
+ /// Sets the genres of the game.
+ ///
+ /// @param genres list - genres.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setGenres(...);
+#else
+ void setGenres(const std::vector<String>& genres);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ setPublisher(publisher) }
+ /// Sets the publisher of the game.
+ ///
+ /// @param publisher string - publisher.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setPublisher(...);
+#else
+ void setPublisher(const String& publisher);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ setDeveloper(developer) }
+ /// Sets the developer of the game.
+ ///
+ /// @param developer string - title.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setDeveloper(...);
+#else
+ void setDeveloper(const String& developer);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ setOverview(overview) }
+ /// Sets the overview of the game.
+ ///
+ /// @param overview string - overview.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setOverview(...);
+#else
+ void setOverview(const String& overview);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ setYear(year) }
+ /// Sets the year in which the game was published.
+ ///
+ /// @param year integer - year.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setYear(...);
+#else
+ void setYear(unsigned int year);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagGame
+ /// @brief \python_func{ setGameClient(gameClient) }
+ /// Sets the add-on ID of the game client executing the game.
+ ///
+ /// @param gameClient string - game client.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setGameClient(...);
+#else
+ void setGameClient(const String& gameClient);
+#endif
+
+#ifndef SWIG
+ static void setTitleRaw(KODI::GAME::CGameInfoTag* infoTag, const String& title);
+ static void setPlatformRaw(KODI::GAME::CGameInfoTag* infoTag, const String& platform);
+ static void setGenresRaw(KODI::GAME::CGameInfoTag* infoTag, const std::vector<String>& genres);
+ static void setPublisherRaw(KODI::GAME::CGameInfoTag* infoTag, const String& publisher);
+ static void setDeveloperRaw(KODI::GAME::CGameInfoTag* infoTag, const String& developer);
+ static void setOverviewRaw(KODI::GAME::CGameInfoTag* infoTag, const String& overview);
+ static void setYearRaw(KODI::GAME::CGameInfoTag* infoTag, unsigned int year);
+ static void setGameClientRaw(KODI::GAME::CGameInfoTag* infoTag, const String& gameClient);
+#endif
+};
+
+} // namespace xbmc
+} // namespace XBMCAddon
diff --git a/xbmc/interfaces/legacy/InfoTagMusic.cpp b/xbmc/interfaces/legacy/InfoTagMusic.cpp
new file mode 100644
index 0000000..3baf7d5
--- /dev/null
+++ b/xbmc/interfaces/legacy/InfoTagMusic.cpp
@@ -0,0 +1,468 @@
+/*
+ * 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 "InfoTagMusic.h"
+
+#include "AddonUtils.h"
+#include "ServiceBroker.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ InfoTagMusic::InfoTagMusic(bool offscreen /* = false */)
+ : infoTag(new MUSIC_INFO::CMusicInfoTag()), offscreen(offscreen), owned(true)
+ {
+ }
+
+ InfoTagMusic::InfoTagMusic(const MUSIC_INFO::CMusicInfoTag* tag) : InfoTagMusic(true)
+ {
+ *infoTag = *tag;
+ }
+
+ InfoTagMusic::InfoTagMusic(MUSIC_INFO::CMusicInfoTag* tag, bool offscreen /* = false */)
+ : infoTag(tag), offscreen(offscreen), owned(false)
+ {
+ }
+
+ InfoTagMusic::~InfoTagMusic()
+ {
+ if (owned)
+ delete infoTag;
+ }
+
+ int InfoTagMusic::getDbId()
+ {
+ return infoTag->GetDatabaseId();
+ }
+
+ String InfoTagMusic::getURL()
+ {
+ return infoTag->GetURL();
+ }
+
+ String InfoTagMusic::getTitle()
+ {
+ return infoTag->GetTitle();
+ }
+
+ String InfoTagMusic::getMediaType()
+ {
+ return infoTag->GetType();
+ }
+
+ String InfoTagMusic::getArtist()
+ {
+ return infoTag->GetArtistString();
+ }
+
+ String InfoTagMusic::getAlbumArtist()
+ {
+ return infoTag->GetAlbumArtistString();
+ }
+
+ String InfoTagMusic::getAlbum()
+ {
+ return infoTag->GetAlbum();
+ }
+
+ String InfoTagMusic::getGenre()
+ {
+ return StringUtils::Join(infoTag->GetGenre(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ }
+
+ std::vector<String> InfoTagMusic::getGenres()
+ {
+ return infoTag->GetGenre();
+ }
+
+ int InfoTagMusic::getDuration()
+ {
+ return infoTag->GetDuration();
+ }
+
+ int InfoTagMusic::getYear()
+ {
+ return infoTag->GetYear();
+ }
+
+ int InfoTagMusic::getRating()
+ {
+ return infoTag->GetRating();
+ }
+
+ int InfoTagMusic::getUserRating()
+ {
+ return infoTag->GetUserrating();
+ }
+
+ int InfoTagMusic::getTrack()
+ {
+ return infoTag->GetTrackNumber();
+ }
+
+ int InfoTagMusic::getDisc()
+ {
+ return infoTag->GetDiscNumber();
+ }
+
+ String InfoTagMusic::getReleaseDate()
+ {
+ return infoTag->GetReleaseDate();
+ }
+
+ int InfoTagMusic::getListeners()
+ {
+ return infoTag->GetListeners();
+ }
+
+ int InfoTagMusic::getPlayCount()
+ {
+ return infoTag->GetPlayCount();
+ }
+
+ String InfoTagMusic::getLastPlayed()
+ {
+ CLog::Log(LOGWARNING, "InfoTagMusic.getLastPlayed() is deprecated and might be removed in "
+ "future Kodi versions. Please use InfoTagMusic.getLastPlayedAsW3C().");
+
+ return infoTag->GetLastPlayed().GetAsLocalizedDate();
+ }
+
+ String InfoTagMusic::getLastPlayedAsW3C()
+ {
+ return infoTag->GetLastPlayed().GetAsW3CDateTime();
+ }
+
+ String InfoTagMusic::getComment()
+ {
+ return infoTag->GetComment();
+ }
+
+ String InfoTagMusic::getLyrics()
+ {
+ return infoTag->GetLyrics();
+ }
+
+ String InfoTagMusic::getMusicBrainzTrackID()
+ {
+ return infoTag->GetMusicBrainzTrackID();
+ }
+
+ std::vector<String> InfoTagMusic::getMusicBrainzArtistID()
+ {
+ return infoTag->GetMusicBrainzArtistID();
+ }
+
+ String InfoTagMusic::getMusicBrainzAlbumID()
+ {
+ return infoTag->GetMusicBrainzAlbumID();
+ }
+
+ String InfoTagMusic::getMusicBrainzReleaseGroupID()
+ {
+ return infoTag->GetMusicBrainzReleaseGroupID();
+ }
+
+ std::vector<String> InfoTagMusic::getMusicBrainzAlbumArtistID()
+ {
+ return infoTag->GetMusicBrainzAlbumArtistID();
+ }
+
+ void InfoTagMusic::setDbId(int dbId, const String& type)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setDbIdRaw(infoTag, dbId, type);
+ }
+
+ void InfoTagMusic::setURL(const String& url)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setURLRaw(infoTag, url);
+ }
+
+ void InfoTagMusic::setMediaType(const String& mediaType)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setMediaTypeRaw(infoTag, mediaType);
+ }
+
+ void InfoTagMusic::setTrack(int track)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setTrackRaw(infoTag, track);
+ }
+
+ void InfoTagMusic::setDisc(int disc)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setDiscRaw(infoTag, disc);
+ }
+
+ void InfoTagMusic::setDuration(int duration)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setDurationRaw(infoTag, duration);
+ }
+
+ void InfoTagMusic::setYear(int year)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setYearRaw(infoTag, year);
+ }
+
+ void InfoTagMusic::setReleaseDate(const String& releaseDate)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setReleaseDateRaw(infoTag, releaseDate);
+ }
+
+ void InfoTagMusic::setListeners(int listeners)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setListenersRaw(infoTag, listeners);
+ }
+
+ void InfoTagMusic::setPlayCount(int playcount)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setPlayCountRaw(infoTag, playcount);
+ }
+
+ void InfoTagMusic::setGenres(const std::vector<String>& genres)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setGenresRaw(infoTag, genres);
+ }
+
+ void InfoTagMusic::setAlbum(const String& album)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setAlbumRaw(infoTag, album);
+ }
+
+ void InfoTagMusic::setArtist(const String& artist)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setArtistRaw(infoTag, artist);
+ }
+
+ void InfoTagMusic::setAlbumArtist(const String& albumArtist)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setAlbumArtistRaw(infoTag, albumArtist);
+ }
+
+ void InfoTagMusic::setTitle(const String& title)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setTitleRaw(infoTag, title);
+ }
+
+ void InfoTagMusic::setRating(float rating)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setRatingRaw(infoTag, rating);
+ }
+
+ void InfoTagMusic::setUserRating(int userrating)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setUserRatingRaw(infoTag, userrating);
+ }
+
+ void InfoTagMusic::setLyrics(const String& lyrics)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setLyricsRaw(infoTag, lyrics);
+ }
+
+ void InfoTagMusic::setLastPlayed(const String& lastPlayed)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setLastPlayedRaw(infoTag, lastPlayed);
+ }
+
+ void InfoTagMusic::setMusicBrainzTrackID(const String& musicBrainzTrackID)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setMusicBrainzTrackIDRaw(infoTag, musicBrainzTrackID);
+ }
+
+ void InfoTagMusic::setMusicBrainzArtistID(const std::vector<String>& musicBrainzArtistID)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setMusicBrainzArtistIDRaw(infoTag, musicBrainzArtistID);
+ }
+
+ void InfoTagMusic::setMusicBrainzAlbumID(const String& musicBrainzAlbumID)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setMusicBrainzAlbumIDRaw(infoTag, musicBrainzAlbumID);
+ }
+
+ void InfoTagMusic::setMusicBrainzReleaseGroupID(const String& musicBrainzReleaseGroupID)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setMusicBrainzReleaseGroupIDRaw(infoTag, musicBrainzReleaseGroupID);
+ }
+
+ void InfoTagMusic::setMusicBrainzAlbumArtistID(
+ const std::vector<String>& musicBrainzAlbumArtistID)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setMusicBrainzAlbumArtistIDRaw(infoTag, musicBrainzAlbumArtistID);
+ }
+
+ void InfoTagMusic::setComment(const String& comment)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setCommentRaw(infoTag, comment);
+ }
+
+ void InfoTagMusic::setDbIdRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int dbId, const String& type)
+ {
+ infoTag->SetDatabaseId(dbId, type);
+ }
+
+ void InfoTagMusic::setURLRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& url)
+ {
+ infoTag->SetURL(url);
+ }
+
+ void InfoTagMusic::setMediaTypeRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& mediaType)
+ {
+ infoTag->SetType(mediaType);
+ }
+
+ void InfoTagMusic::setTrackRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int track)
+ {
+ infoTag->SetTrackNumber(track);
+ }
+
+ void InfoTagMusic::setDiscRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int disc)
+ {
+ infoTag->SetDiscNumber(disc);
+ }
+
+ void InfoTagMusic::setDurationRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int duration)
+ {
+ infoTag->SetDuration(duration);
+ }
+
+ void InfoTagMusic::setYearRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int year)
+ {
+ infoTag->SetYear(year);
+ }
+
+ void InfoTagMusic::setReleaseDateRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const String& releaseDate)
+ {
+ infoTag->SetReleaseDate(releaseDate);
+ }
+
+ void InfoTagMusic::setListenersRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int listeners)
+ {
+ infoTag->SetListeners(listeners);
+ }
+
+ void InfoTagMusic::setPlayCountRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int playcount)
+ {
+ infoTag->SetPlayCount(playcount);
+ }
+
+ void InfoTagMusic::setGenresRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const std::vector<String>& genres)
+ {
+ infoTag->SetGenre(genres);
+ }
+
+ void InfoTagMusic::setAlbumRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& album)
+ {
+ infoTag->SetAlbum(album);
+ }
+
+ void InfoTagMusic::setArtistRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& artist)
+ {
+ infoTag->SetArtist(artist);
+ }
+
+ void InfoTagMusic::setAlbumArtistRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const String& albumArtist)
+ {
+ infoTag->SetAlbumArtist(albumArtist);
+ }
+
+ void InfoTagMusic::setTitleRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& title)
+ {
+ infoTag->SetTitle(title);
+ }
+
+ void InfoTagMusic::setRatingRaw(MUSIC_INFO::CMusicInfoTag* infoTag, float rating)
+ {
+ infoTag->SetRating(rating);
+ }
+
+ void InfoTagMusic::setUserRatingRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int userrating)
+ {
+ infoTag->SetUserrating(userrating);
+ }
+
+ void InfoTagMusic::setLyricsRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& lyrics)
+ {
+ infoTag->SetLyrics(lyrics);
+ }
+
+ void InfoTagMusic::setLastPlayedRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const String& lastPlayed)
+ {
+ infoTag->SetLastPlayed(lastPlayed);
+ }
+
+ void InfoTagMusic::setMusicBrainzTrackIDRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const String& musicBrainzTrackID)
+ {
+ infoTag->SetMusicBrainzTrackID(musicBrainzTrackID);
+ }
+
+ void InfoTagMusic::setMusicBrainzArtistIDRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const std::vector<String>& musicBrainzArtistID)
+ {
+ infoTag->SetMusicBrainzArtistID(musicBrainzArtistID);
+ }
+
+ void InfoTagMusic::setMusicBrainzAlbumIDRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const String& musicBrainzAlbumID)
+ {
+ infoTag->SetMusicBrainzAlbumID(musicBrainzAlbumID);
+ }
+
+ void InfoTagMusic::setMusicBrainzReleaseGroupIDRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const String& musicBrainzReleaseGroupID)
+ {
+ infoTag->SetMusicBrainzReleaseGroupID(musicBrainzReleaseGroupID);
+ }
+
+ void InfoTagMusic::setMusicBrainzAlbumArtistIDRaw(
+ MUSIC_INFO::CMusicInfoTag* infoTag, const std::vector<String>& musicBrainzAlbumArtistID)
+ {
+ infoTag->SetMusicBrainzAlbumArtistID(musicBrainzAlbumArtistID);
+ }
+
+ void InfoTagMusic::setCommentRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& comment)
+ {
+ infoTag->SetComment(comment);
+ }
+ }
+}
+
diff --git a/xbmc/interfaces/legacy/InfoTagMusic.h b/xbmc/interfaces/legacy/InfoTagMusic.h
new file mode 100644
index 0000000..835a705
--- /dev/null
+++ b/xbmc/interfaces/legacy/InfoTagMusic.h
@@ -0,0 +1,1034 @@
+/*
+ * 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 "AddonClass.h"
+
+#include <vector>
+
+namespace MUSIC_INFO
+{
+class CMusicInfoTag;
+}
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ //
+ /// \defgroup python_InfoTagMusic InfoTagMusic
+ /// \ingroup python_xbmc
+ /// @{
+ /// @brief **Kodi's music info tag class.**
+ ///
+ /// \python_class{ xbmc.InfoTagMusic([offscreen]) }
+ ///
+ /// Access and / or modify the music metadata of a ListItem.
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// tag = xbmc.Player().getMusicInfoTag()
+ ///
+ /// title = tag.getTitle()
+ /// url = tag.getURL()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ //
+ class InfoTagMusic : public AddonClass
+ {
+ private:
+ MUSIC_INFO::CMusicInfoTag* infoTag;
+ bool offscreen;
+ bool owned;
+
+ public:
+#ifndef SWIG
+ explicit InfoTagMusic(const MUSIC_INFO::CMusicInfoTag* tag);
+ explicit InfoTagMusic(MUSIC_INFO::CMusicInfoTag* tag, bool offscreen = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ xbmc.InfoTagMusic([offscreen]) }
+ /// Create a music info tag.
+ ///
+ /// @param offscreen [opt] bool (default `False`) - if GUI based locks should be
+ /// avoided. Most of the times listitems are created
+ /// offscreen and added later to a container
+ /// for display (e.g. plugins) or they are not
+ /// even displayed (e.g. python scrapers).
+ /// In such cases, there is no need to lock the
+ /// GUI when creating the items (increasing your addon
+ /// performance).
+ /// Note however, that if you are creating listitems
+ /// and managing the container itself (e.g using
+ /// WindowXML or WindowXMLDialog classes) subsquent
+ /// modifications to the item will require locking.
+ /// Thus, in such cases, use the default value (`False`).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Added **offscreen** argument.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// musicinfo = xbmc.InfoTagMusic(offscreen=False)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ InfoTagMusic(...);
+#else
+ explicit InfoTagMusic(bool offscreen = false);
+#endif
+ ~InfoTagMusic() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getDbId() }
+ /// Get identification number of tag in database.
+ ///
+ /// @return [integer] database id.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ getDbId();
+#else
+ int getDbId();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getURL() }
+ /// Returns url of source as string from music info tag.
+ ///
+ /// @return [string] Url of source
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getURL();
+#else
+ String getURL();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getTitle() }
+ /// Returns the title from music as string on info tag.
+ ///
+ /// @return [string] Music title
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getTitle();
+#else
+ String getTitle();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getMediaType() }
+ /// Get the media type of the music item.
+ ///
+ /// @return [string] media type
+ ///
+ /// Available strings about media type for music:
+ /// | String | Description |
+ /// |---------------:|:--------------------------------------------------|
+ /// | artist | If it is defined as an artist
+ /// | album | If it is defined as an album
+ /// | song | If it is defined as a song
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ getMediaType();
+#else
+ String getMediaType();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getArtist() }
+ /// Returns the artist from music as string if present.
+ ///
+ /// @return [string] Music artist
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getArtist();
+#else
+ String getArtist();
+#endif
+
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getAlbum() }
+ /// Returns the album from music tag as string if present.
+ ///
+ /// @return [string] Music album name
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getAlbum();
+#else
+ String getAlbum();
+#endif
+
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getAlbumArtist() }
+ /// Returns the album artist from music tag as string if present.
+ ///
+ /// @return [string] Music album artist name
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getAlbumArtist();
+#else
+ String getAlbumArtist();
+#endif
+
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getGenre() }
+ /// Returns the genre name from music tag as string if present.
+ ///
+ /// @return [string] Genre name
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **getGenres()** instead.
+ ///
+ getGenre();
+#else
+ String getGenre();
+#endif
+
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getGenres() }
+ /// Returns the list of genres from music tag if present.
+ ///
+ /// @return [list] List of genres
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getGenres();
+#else
+ std::vector<String> getGenres();
+#endif
+
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getDuration() }
+ /// Returns the duration of music as integer from info tag.
+ ///
+ /// @return [integer] Duration
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getDuration();
+#else
+ int getDuration();
+#endif
+
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getYear() }
+ /// Returns the year of music as integer from info tag.
+ ///
+ /// @return [integer] Year
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ ///
+ getYear();
+#else
+ int getYear();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getRating() }
+ /// Returns the scraped rating as integer.
+ ///
+ /// @return [integer] Rating
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getRating();
+#else
+ int getRating();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getUserRating() }
+ /// Returns the user rating as integer (-1 if not existing)
+ ///
+ /// @return [integer] User rating
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getUserRating();
+#else
+ int getUserRating();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getTrack() }
+ /// Returns the track number (if present) from music info tag as integer.
+ ///
+ /// @return [integer] Track number
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getTrack();
+#else
+ int getTrack();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getDisc() }
+ /// Returns the disk number (if present) from music info tag as integer.
+ ///
+ /// @return [integer] Disc number
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getDisc();
+#else
+ /**
+ * getDisc() -- returns an integer.\n
+ */
+ int getDisc();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getReleaseDate() }
+ /// Returns the release date as string from music info tag (if present).
+ ///
+ /// @return [string] Release date
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getReleaseDate();
+#else
+ String getReleaseDate();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getListeners() }
+ /// Returns the listeners as integer from music info tag.
+ ///
+ /// @return [integer] Listeners
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getListeners();
+#else
+ int getListeners();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getPlayCount() }
+ /// Returns the number of carried out playbacks.
+ ///
+ /// @return [integer] Playback count
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getPlayCount();
+#else
+ int getPlayCount();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getLastPlayed() }
+ /// Returns last played time as string from music info tag.
+ ///
+ /// @return [string] Last played date / time on tag
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **getLastPlayedAsW3C()** instead.
+ ///
+ getLastPlayed();
+#else
+ String getLastPlayed();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getLastPlayedAsW3C() }
+ /// Returns last played time as string in W3C format (YYYY-MM-DDThh:mm:ssTZD).
+ ///
+ /// @return [string] Last played datetime (W3C)
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getLastPlayedAsW3C();
+#else
+ String getLastPlayedAsW3C();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getComment() }
+ /// Returns comment as string from music info tag.
+ ///
+ /// @return [string] Comment on tag
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getComment();
+#else
+ String getComment();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getLyrics() }
+ /// Returns a string from lyrics.
+ ///
+ /// @return [string] Lyrics on tag
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getLyrics();
+#else
+ String getLyrics();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getMusicBrainzTrackID() }
+ /// Returns the MusicBrainz Recording ID from music info tag (if present).
+ ///
+ /// @return [string] MusicBrainz Recording ID
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v19 New function added.
+ ///
+ getMusicBrainzTrackID();
+#else
+ String getMusicBrainzTrackID();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getMusicBrainzArtistID() }
+ /// Returns the MusicBrainz Artist IDs from music info tag (if present).
+ ///
+ /// @return [list] MusicBrainz Artist IDs
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v19 New function added.
+ ///
+ getMusicBrainzArtistID();
+#else
+ std::vector<String> getMusicBrainzArtistID();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getMusicBrainzAlbumID() }
+ /// Returns the MusicBrainz Release ID from music info tag (if present).
+ ///
+ /// @return [string] MusicBrainz Release ID
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v19 New function added.
+ ///
+ getMusicBrainzAlbumID();
+#else
+ String getMusicBrainzAlbumID();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getMusicBrainzReleaseGroupID() }
+ /// Returns the MusicBrainz Release Group ID from music info tag (if present).
+ ///
+ /// @return [string] MusicBrainz Release Group ID
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v19 New function added.
+ ///
+ getMusicBrainzReleaseGroupID();
+#else
+ String getMusicBrainzReleaseGroupID();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ getMusicBrainzAlbumArtistID() }
+ /// Returns the MusicBrainz Release Artist IDs from music info tag (if present).
+ ///
+ /// @return [list] MusicBrainz Release Artist IDs
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v19 New function added.
+ ///
+ getMusicBrainzAlbumArtistID();
+#else
+ std::vector<String> getMusicBrainzAlbumArtistID();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setDbId(dbId, type) }
+ /// Set the database identifier of the music item.
+ ///
+ /// @param dbId integer - Database identifier.
+ /// @param type string - Media type of the item.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setDbId(...);
+#else
+ void setDbId(int dbId, const String& type);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setURL(url) }
+ /// Set the URL of the music item.
+ ///
+ /// @param url string - URL.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setURL(...);
+#else
+ void setURL(const String& url);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setMediaType(mediaType) }
+ /// Set the media type of the music item.
+ ///
+ /// @param mediaType string - Media type.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setMediaType(...);
+#else
+ void setMediaType(const String& mediaType);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setTrack(track) }
+ /// Set the track number of the song.
+ ///
+ /// @param track integer - Track number.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setTrack(...);
+#else
+ void setTrack(int track);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setDisc(disc) }
+ /// Set the disc number of the song.
+ ///
+ /// @param disc integer - Disc number.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setDisc(...);
+#else
+ void setDisc(int disc);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setDuration(duration) }
+ /// Set the duration of the song.
+ ///
+ /// @param duration integer - Duration in seconds.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setDuration(...);
+#else
+ void setDuration(int duration);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setYear(year) }
+ /// Set the year of the music item.
+ ///
+ /// @param year integer - Year.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setYear(...);
+#else
+ void setYear(int year);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setReleaseDate(releaseDate) }
+ /// Set the release date of the music item.
+ ///
+ /// @param releaseDate string - Release date in ISO8601 format (YYYY, YYYY-MM or YYYY-MM-DD).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setReleaseDate(...);
+#else
+ void setReleaseDate(const String& releaseDate);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setListeners(listeners) }
+ /// Set the number of listeners of the music item.
+ ///
+ /// @param listeners integer - Number of listeners.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setListeners(...);
+#else
+ void setListeners(int listeners);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setPlayCount(playcount) }
+ /// Set the playcount of the music item.
+ ///
+ /// @param playcount integer - Playcount.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setPlayCount(...);
+#else
+ void setPlayCount(int playcount);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setGenres(genres) }
+ /// Set the genres of the music item.
+ ///
+ /// @param genres list - Genres.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setGenres(...);
+#else
+ void setGenres(const std::vector<String>& genres);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setAlbum(album) }
+ /// Set the album of the music item.
+ ///
+ /// @param album string - Album.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setAlbum(...);
+#else
+ void setAlbum(const String& album);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setArtist(artist) }
+ /// Set the artist(s) of the music item.
+ ///
+ /// @param artist string - Artist(s).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setArtist(...);
+#else
+ void setArtist(const String& artist);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setAlbumArtist(albumArtist) }
+ /// Set the album artist(s) of the music item.
+ ///
+ /// @param albumArtist string - Album artist(s).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setAlbumArtist(...);
+#else
+ void setAlbumArtist(const String& albumArtist);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setTitle(title) }
+ /// Set the title of the music item.
+ ///
+ /// @param title string - Title.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setTitle(...);
+#else
+ void setTitle(const String& title);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setRating(rating) }
+ /// Set the rating of the music item.
+ ///
+ /// @param rating float - Rating.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setRating(...);
+#else
+ void setRating(float rating);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setUserRating(userrating) }
+ /// Set the user rating of the music item.
+ ///
+ /// @param userrating integer - User rating.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setUserRating(...);
+#else
+ void setUserRating(int userrating);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setLyrics(lyrics) }
+ /// Set the lyrics of the song.
+ ///
+ /// @param lyrics string - Lyrics.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setLyrics(...);
+#else
+ void setLyrics(const String& lyrics);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setLastPlayed(lastPlayed) }
+ /// Set the last played date of the music item.
+ ///
+ /// @param lastPlayed string - Last played date (YYYY-MM-DD HH:MM:SS).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setLastPlayed(...);
+#else
+ void setLastPlayed(const String& lastPlayed);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setMusicBrainzTrackID(musicBrainzTrackID) }
+ /// Set the MusicBrainz track ID of the song.
+ ///
+ /// @param musicBrainzTrackID string - MusicBrainz track ID.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setMusicBrainzTrackID(...);
+#else
+ void setMusicBrainzTrackID(const String& musicBrainzTrackID);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setMusicBrainzArtistID(musicBrainzArtistID) }
+ /// Set the MusicBrainz artist IDs of the music item.
+ ///
+ /// @param musicBrainzArtistID list - MusicBrainz artist IDs.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setMusicBrainzArtistID(...);
+#else
+ void setMusicBrainzArtistID(const std::vector<String>& musicBrainzArtistID);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setMusicBrainzAlbumID(musicBrainzAlbumID) }
+ /// Set the MusicBrainz album ID of the music item.
+ ///
+ /// @param musicBrainzAlbumID string - MusicBrainz album ID.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setMusicBrainzAlbumID(...);
+#else
+ void setMusicBrainzAlbumID(const String& musicBrainzAlbumID);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setMusicBrainzReleaseGroupID(musicBrainzReleaseGroupID) }
+ /// Set the MusicBrainz release group ID of the music item.
+ ///
+ /// @param musicBrainzReleaseGroupID string - MusicBrainz release group ID.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setMusicBrainzReleaseGroupID(...);
+#else
+ void setMusicBrainzReleaseGroupID(const String& musicBrainzReleaseGroupID);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setMusicBrainzAlbumArtistID(musicBrainzAlbumArtistID) }
+ /// Set the MusicBrainz album artist IDs of the music item.
+ ///
+ /// @param musicBrainzAlbumArtistID list - MusicBrainz album artist IDs.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setMusicBrainzAlbumArtistID(...);
+#else
+ void setMusicBrainzAlbumArtistID(const std::vector<String>& musicBrainzAlbumArtistID);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagMusic
+ /// @brief \python_func{ setComment(comment) }
+ /// Set the comment of the music item.
+ ///
+ /// @param comment string - Comment.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setComment(...);
+#else
+ void setComment(const String& comment);
+#endif
+
+#ifndef SWIG
+ static void setDbIdRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int dbId, const String& type);
+ static void setURLRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& url);
+ static void setMediaTypeRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& mediaType);
+ static void setTrackRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int track);
+ static void setDiscRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int disc);
+ static void setDurationRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int duration);
+ static void setYearRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int year);
+ static void setReleaseDateRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& releaseDate);
+ static void setListenersRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int listeners);
+ static void setPlayCountRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int playcount);
+ static void setGenresRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const std::vector<String>& genres);
+ static void setAlbumRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& album);
+ static void setArtistRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& artist);
+ static void setAlbumArtistRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& albumArtist);
+ static void setTitleRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& title);
+ static void setRatingRaw(MUSIC_INFO::CMusicInfoTag* infoTag, float rating);
+ static void setUserRatingRaw(MUSIC_INFO::CMusicInfoTag* infoTag, int userrating);
+ static void setLyricsRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& lyrics);
+ static void setLastPlayedRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& lastPlayed);
+ static void setMusicBrainzTrackIDRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const String& musicBrainzTrackID);
+ static void setMusicBrainzArtistIDRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const std::vector<String>& musicBrainzArtistID);
+ static void setMusicBrainzAlbumIDRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const String& musicBrainzAlbumID);
+ static void setMusicBrainzReleaseGroupIDRaw(MUSIC_INFO::CMusicInfoTag* infoTag,
+ const String& musicBrainzReleaseGroupID);
+ static void setMusicBrainzAlbumArtistIDRaw(
+ MUSIC_INFO::CMusicInfoTag* infoTag, const std::vector<String>& musicBrainzAlbumArtistID);
+ static void setCommentRaw(MUSIC_INFO::CMusicInfoTag* infoTag, const String& comment);
+#endif
+ };
+ //@}
+ }
+}
diff --git a/xbmc/interfaces/legacy/InfoTagPicture.cpp b/xbmc/interfaces/legacy/InfoTagPicture.cpp
new file mode 100644
index 0000000..892e2be
--- /dev/null
+++ b/xbmc/interfaces/legacy/InfoTagPicture.cpp
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 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 "InfoTagPicture.h"
+
+#include "AddonUtils.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "interfaces/legacy/Exception.h"
+#include "pictures/PictureInfoTag.h"
+#include "utils/StringUtils.h"
+
+using namespace XBMCAddonUtils;
+
+namespace XBMCAddon
+{
+namespace xbmc
+{
+
+InfoTagPicture::InfoTagPicture(bool offscreen /* = false */)
+ : infoTag(new CPictureInfoTag), offscreen(offscreen), owned(true)
+{
+}
+
+InfoTagPicture::InfoTagPicture(const CPictureInfoTag* tag)
+ : infoTag(new CPictureInfoTag(*tag)), offscreen(true), owned(true)
+{
+}
+
+InfoTagPicture::InfoTagPicture(CPictureInfoTag* tag, bool offscreen /* = false */)
+ : infoTag(tag), offscreen(offscreen), owned(false)
+{
+}
+
+InfoTagPicture::~InfoTagPicture()
+{
+ if (owned)
+ delete infoTag;
+}
+
+String InfoTagPicture::getResolution()
+{
+ return infoTag->GetInfo(SLIDESHOW_RESOLUTION);
+}
+
+String InfoTagPicture::getDateTimeTaken()
+{
+ return infoTag->GetDateTimeTaken().GetAsW3CDateTime();
+}
+
+void InfoTagPicture::setResolution(int width, int height)
+{
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setResolutionRaw(infoTag, width, height);
+}
+
+void InfoTagPicture::setDateTimeTaken(const String& datetimetaken)
+{
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setDateTimeTakenRaw(infoTag, datetimetaken);
+}
+
+void InfoTagPicture::setResolutionRaw(CPictureInfoTag* infoTag, const String& resolution)
+{
+ infoTag->SetInfo("resolution", resolution);
+}
+
+void InfoTagPicture::setResolutionRaw(CPictureInfoTag* infoTag, int width, int height)
+{
+ if (width <= 0)
+ throw WrongTypeException("InfoTagPicture.setResolution: width must be greater than zero (0)");
+ if (height <= 0)
+ throw WrongTypeException("InfoTagPicture.setResolution: height must be greater than zero (0)");
+
+ setResolutionRaw(infoTag, StringUtils::Format("{:d},{:d}", width, height));
+}
+
+void InfoTagPicture::setDateTimeTakenRaw(CPictureInfoTag* infoTag, String datetimetaken)
+{
+ // try to parse the datetimetaken as from W3C format and adjust it to the EXIF datetime format YYYY:MM:DD HH:MM:SS
+ CDateTime w3cDateTimeTaken;
+ if (w3cDateTimeTaken.SetFromW3CDateTime(datetimetaken))
+ {
+ datetimetaken = StringUtils::Format("{:4d}:{:2d}:{:2d} {:2d}:{:2d}:{:2d}",
+ w3cDateTimeTaken.GetYear(), w3cDateTimeTaken.GetMonth(),
+ w3cDateTimeTaken.GetDay(), w3cDateTimeTaken.GetHour(),
+ w3cDateTimeTaken.GetMinute(), w3cDateTimeTaken.GetSecond());
+ }
+
+ infoTag->SetInfo("exiftime", datetimetaken);
+}
+
+} // namespace xbmc
+} // namespace XBMCAddon
diff --git a/xbmc/interfaces/legacy/InfoTagPicture.h b/xbmc/interfaces/legacy/InfoTagPicture.h
new file mode 100644
index 0000000..a03bf0f
--- /dev/null
+++ b/xbmc/interfaces/legacy/InfoTagPicture.h
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 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 "AddonClass.h"
+
+class CPictureInfoTag;
+
+namespace XBMCAddon
+{
+namespace xbmc
+{
+
+///
+/// \defgroup python_InfoTagPicture InfoTagPicture
+/// \ingroup python_xbmc
+/// @{
+/// @brief **Kodi's picture info tag class.**
+///
+/// \python_class{ InfoTagPicture() }
+///
+/// Access and / or modify the picture metadata of a ListItem.
+///
+///-------------------------------------------------------------------------
+/// @python_v20 New class added.
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.py}
+/// ...
+/// tag = item.getPictureInfoTag()
+///
+/// datetime_taken = tag.getDateTimeTaken()
+/// tag.setResolution(1920, 1080)
+/// ...
+/// ~~~~~~~~~~~~~
+///
+class InfoTagPicture : public AddonClass
+{
+private:
+ CPictureInfoTag* infoTag;
+ bool offscreen;
+ bool owned;
+
+public:
+#ifndef SWIG
+ explicit InfoTagPicture(const CPictureInfoTag* tag);
+ explicit InfoTagPicture(CPictureInfoTag* tag, bool offscreen = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagPicture
+ /// @brief \python_func{ xbmc.InfoTagPicture([offscreen]) }
+ /// Create a picture info tag.
+ ///
+ /// @param offscreen [opt] bool (default `False`) - if GUI based locks should be
+ /// avoided. Most of the times listitems are created
+ /// offscreen and added later to a container
+ /// for display (e.g. plugins) or they are not
+ /// even displayed (e.g. python scrapers).
+ /// In such cases, there is no need to lock the
+ /// GUI when creating the items (increasing your addon
+ /// performance).
+ /// Note however, that if you are creating listitems
+ /// and managing the container itself (e.g using
+ /// WindowXML or WindowXMLDialog classes) subsquent
+ /// modifications to the item will require locking.
+ /// Thus, in such cases, use the default value (`False`).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// pictureinfo = xbmc.InfoTagPicture(offscreen=False)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ InfoTagPicture(...);
+#else
+ explicit InfoTagPicture(bool offscreen = false);
+#endif
+ ~InfoTagPicture() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagPicture
+ /// @brief \python_func{ getResolution() }
+ /// Get the resolution of the picture in the format "w x h".
+ ///
+ /// @return [string] Resolution of the picture in the format "w x h".
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getResolution();
+#else
+ String getResolution();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagPicture
+ /// @brief \python_func{ getDateTimeTaken() }
+ /// Get the date and time at which the picture was taken in W3C format.
+ ///
+ /// @return [string] Date and time at which the picture was taken in W3C format.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getDirector();
+#else
+ String getDateTimeTaken();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagPicture
+ /// @brief \python_func{ setResolution(width, height) }
+ /// Sets the resolution of the picture.
+ ///
+ /// @param width int - Width of the picture in pixels.
+ /// @param height int - Height of the picture in pixels.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setResolution(...);
+#else
+ void setResolution(int width, int height);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagPicture
+ /// @brief \python_func{ setDateTimeTaken(datetimetaken) }
+ /// Sets the date and time at which the picture was taken in W3C format.
+ /// The following formats are supported:
+ /// - YYYY
+ /// - YYYY-MM-DD
+ /// - YYYY-MM-DDThh:mm[TZD]
+ /// - YYYY-MM-DDThh:mm:ss[TZD]
+ /// where the timezone (TZD) is always optional and can be in one of the
+ /// following formats:
+ /// - Z (for UTC)
+ /// - +hh:mm
+ /// - -hh:mm
+ ///
+ /// @param datetimetaken string - Date and time at which the picture was taken in W3C format.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setDateTimeTaken(...);
+#else
+ void setDateTimeTaken(const String& datetimetaken);
+#endif
+
+#ifndef SWIG
+ static void setResolutionRaw(CPictureInfoTag* infoTag, const String& resolution);
+ static void setResolutionRaw(CPictureInfoTag* infoTag, int width, int height);
+ static void setDateTimeTakenRaw(CPictureInfoTag* infoTag, String datetimetaken);
+#endif
+};
+
+} // namespace xbmc
+} // namespace XBMCAddon
diff --git a/xbmc/interfaces/legacy/InfoTagRadioRDS.cpp b/xbmc/interfaces/legacy/InfoTagRadioRDS.cpp
new file mode 100644
index 0000000..b60ea4c
--- /dev/null
+++ b/xbmc/interfaces/legacy/InfoTagRadioRDS.cpp
@@ -0,0 +1,234 @@
+/*
+ * 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 "InfoTagRadioRDS.h"
+
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRRadioRDSInfoTag.h"
+#include "utils/StringUtils.h"
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ InfoTagRadioRDS::InfoTagRadioRDS() = default;
+
+ InfoTagRadioRDS::InfoTagRadioRDS(const std::shared_ptr<PVR::CPVRChannel>& channel)
+ {
+ if (channel)
+ infoTag = channel->GetRadioRDSInfoTag();
+ }
+
+ InfoTagRadioRDS::~InfoTagRadioRDS() = default;
+
+ String InfoTagRadioRDS::getTitle()
+ {
+ if (infoTag)
+ return infoTag->GetTitle();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getBand()
+ {
+ if (infoTag)
+ return infoTag->GetBand();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getArtist()
+ {
+ if (infoTag)
+ return infoTag->GetArtist();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getComposer()
+ {
+ if (infoTag)
+ return infoTag->GetComposer();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getConductor()
+ {
+ if (infoTag)
+ return infoTag->GetConductor();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getAlbum()
+ {
+ if (infoTag)
+ return infoTag->GetAlbum();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getComment()
+ {
+ if (infoTag)
+ return infoTag->GetComment();
+ return "";
+ }
+
+ int InfoTagRadioRDS::getAlbumTrackNumber()
+ {
+ if (infoTag)
+ return infoTag->GetAlbumTrackNumber();
+ return 0;
+ }
+
+ String InfoTagRadioRDS::getInfoNews()
+ {
+ if (infoTag)
+ return infoTag->GetInfoNews();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getInfoNewsLocal()
+ {
+ if (infoTag)
+ return infoTag->GetInfoNewsLocal();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getInfoSport()
+ {
+ if (infoTag)
+ return infoTag->GetInfoSport();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getInfoStock()
+ {
+ if (infoTag)
+ return infoTag->GetInfoStock();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getInfoWeather()
+ {
+ if (infoTag)
+ return infoTag->GetInfoWeather();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getInfoHoroscope()
+ {
+ if (infoTag)
+ return infoTag->GetInfoHoroscope();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getInfoCinema()
+ {
+ if (infoTag)
+ return infoTag->GetInfoCinema();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getInfoLottery()
+ {
+ if (infoTag)
+ return infoTag->GetInfoLottery();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getInfoOther()
+ {
+ if (infoTag)
+ return infoTag->GetInfoOther();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getEditorialStaff()
+ {
+ if (infoTag)
+ return infoTag->GetEditorialStaff();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getProgStation()
+ {
+ if (infoTag)
+ return infoTag->GetProgStation();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getProgStyle()
+ {
+ if (infoTag)
+ return infoTag->GetProgStyle();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getProgHost()
+ {
+ if (infoTag)
+ return infoTag->GetProgHost();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getProgWebsite()
+ {
+ if (infoTag)
+ return infoTag->GetProgWebsite();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getProgNow()
+ {
+ if (infoTag)
+ return infoTag->GetProgNow();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getProgNext()
+ {
+ if (infoTag)
+ return infoTag->GetProgNext();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getPhoneHotline()
+ {
+ if (infoTag)
+ return infoTag->GetPhoneHotline();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getEMailHotline()
+ {
+ if (infoTag)
+ return infoTag->GetEMailHotline();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getPhoneStudio()
+ {
+ if (infoTag)
+ return infoTag->GetPhoneStudio();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getEMailStudio()
+ {
+ if (infoTag)
+ return infoTag->GetEMailStudio();
+ return "";
+ }
+
+ String InfoTagRadioRDS::getSMSStudio()
+ {
+ if (infoTag)
+ return infoTag->GetSMSStudio();
+ return "";
+ }
+
+ }
+}
+
diff --git a/xbmc/interfaces/legacy/InfoTagRadioRDS.h b/xbmc/interfaces/legacy/InfoTagRadioRDS.h
new file mode 100644
index 0000000..6a68221
--- /dev/null
+++ b/xbmc/interfaces/legacy/InfoTagRadioRDS.h
@@ -0,0 +1,443 @@
+/*
+ * 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 "AddonClass.h"
+
+#include <memory>
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRRadioRDSInfoTag;
+}
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ //
+ /// \defgroup python_InfoTagRadioRDS InfoTagRadioRDS
+ /// \ingroup python_xbmc
+ /// @{
+ /// @brief **Kodi's radio RDS info tag class.**
+ ///
+ /// \python_class{ InfoTagRadioRDS() }
+ ///
+ /// To get radio RDS info tag data of currently played PVR radio channel source.
+ ///
+ /// @note Info tag load is only be possible from present player class.\n
+ /// Also is all the data variable from radio channels and not known on beginning
+ /// of radio receiving.
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// tag = xbmc.Player().getRadioRDSInfoTag()
+ ///
+ /// title = tag.getTitle()
+ /// artist = tag.getArtist()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ //
+ class InfoTagRadioRDS : public AddonClass
+ {
+ private:
+ std::shared_ptr<PVR::CPVRRadioRDSInfoTag> infoTag;
+
+ public:
+#ifndef SWIG
+ explicit InfoTagRadioRDS(const std::shared_ptr<PVR::CPVRChannel>& channel);
+#endif
+ InfoTagRadioRDS();
+ ~InfoTagRadioRDS() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getTitle() }
+ /// Title of the item on the air; i.e. song title.
+ ///
+ /// @return Title
+ ///
+ getTitle();
+#else
+ String getTitle();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getBand() }
+ /// Band of the item on air.
+ ///
+ /// @return Band
+ ///
+ getBand();
+#else
+ String getBand();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getArtist() }
+ /// Artist of the item on air.
+ ///
+ /// @return Artist
+ ///
+ getArtist();
+#else
+ String getArtist();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getComposer() }
+ /// Get the Composer of the music.
+ ///
+ /// @return Composer
+ ///
+ getComposer();
+#else
+ String getComposer();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getConductor() }
+ /// Get the Conductor of the Band.
+ ///
+ /// @return Conductor
+ ///
+ getConductor();
+#else
+ String getConductor();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getAlbum() }
+ /// Album of item on air.
+ ///
+ /// @return Album name
+ ///
+ getAlbum();
+#else
+ String getAlbum();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getComment() }
+ /// Get Comment text from channel.
+ ///
+ /// @return Comment
+ ///
+ getComment();
+#else
+ String getComment();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getAlbumTrackNumber() }
+ /// Get the album track number of currently sended music.
+ ///
+ /// @return Track Number
+ ///
+ getAlbumTrackNumber();
+#else
+ int getAlbumTrackNumber();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getInfoNews() }
+ /// Get News informations.
+ ///
+ /// @return News Information
+ ///
+ getInfoNews();
+#else
+ String getInfoNews();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getInfoNewsLocal() }
+ /// Get Local news informations.
+ ///
+ /// @return Local News Information
+ ///
+ getInfoNewsLocal();
+#else
+ String getInfoNewsLocal();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getInfoSport() }
+ /// Get Sport informations.
+ ///
+ /// @return Sport Information
+ ///
+ getInfoSport();
+#else
+ String getInfoSport();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getInfoStock() }
+ /// Get Stock informations.
+ ///
+ /// @return Stock Information
+ ///
+ getInfoStock();
+#else
+ String getInfoStock();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getInfoWeather() }
+ /// Get Weather informations.
+ ///
+ /// @return Weather Information
+ ///
+ getInfoWeather();
+#else
+ String getInfoWeather();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getInfoHoroscope() }
+ /// Get Horoscope informations.
+ ///
+ /// @return Horoscope Information
+ ///
+ getInfoHoroscope();
+#else
+ String getInfoHoroscope();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getInfoCinema() }
+ /// Get Cinema informations.
+ ///
+ /// @return Cinema Information
+ ///
+ getInfoCinema();
+#else
+ String getInfoCinema();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getInfoLottery() }
+ /// Get Lottery informations.
+ ///
+ /// @return Lottery Information
+ ///
+ getInfoLottery();
+#else
+ String getInfoLottery();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getInfoOther() }
+ /// Get other informations.
+ ///
+ /// @return Other Information
+ ///
+ getInfoOther();
+#else
+ String getInfoOther();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getEditorialStaff() }
+ /// Get Editorial Staff names.
+ ///
+ /// @return Editorial Staff
+ ///
+ getEditorialStaff();
+#else
+ String getEditorialStaff();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getProgStation() }
+ /// Name describing station.
+ ///
+ /// @return Program Station
+ ///
+ getProgStation();
+#else
+ String getProgStation();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getProgStyle() }
+ /// The the radio channel style currently used.
+ ///
+ /// @return Program Style
+ ///
+ getProgStyle();
+#else
+ String getProgStyle();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getProgHost() }
+ /// Host of current radio show.
+ ///
+ /// @return Program Host
+ ///
+ getProgHost();
+#else
+ String getProgHost();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getProgWebsite() }
+ /// Link to URL (web page) for radio station homepage.
+ ///
+ /// @return Program Website
+ ///
+ getProgWebsite();
+#else
+ String getProgWebsite();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getProgNow() }
+ /// Current radio program show.
+ ///
+ /// @return Program Now
+ ///
+ getProgNow();
+#else
+ String getProgNow();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getProgNext() }
+ /// Next program show.
+ ///
+ /// @return Program Next
+ ///
+ getProgNext();
+#else
+ String getProgNext();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getPhoneHotline() }
+ /// Telephone number of the radio station's hotline.
+ ///
+ /// @return Phone Hotline
+ ///
+ getPhoneHotline();
+#else
+ String getPhoneHotline();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getEMailHotline() }
+ /// Email address of the radio station's studio.
+ ///
+ /// @return EMail Hotline
+ ///
+ getEMailHotline();
+#else
+ String getEMailHotline();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getPhoneStudio() }
+ /// Telephone number of the radio station's studio.
+ ///
+ /// @return Phone Studio
+ ///
+ getPhoneStudio();
+#else
+ String getPhoneStudio();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getEMailStudio() }
+ /// Email address of radio station studio.
+ ///
+ /// @return EMail Studio
+ ///
+ getEMailStudio();
+#else
+ String getEMailStudio();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// @ingroup python_InfoTagRadioRDS
+ /// @brief \python_func{ getSMSStudio() }
+ /// SMS (Text Messaging) number for studio.
+ ///
+ /// @return SMS Studio
+ ///
+ getSMSStudio();
+#else
+ String getSMSStudio();
+#endif
+ };
+ //@}
+ }
+}
diff --git a/xbmc/interfaces/legacy/InfoTagVideo.cpp b/xbmc/interfaces/legacy/InfoTagVideo.cpp
new file mode 100644
index 0000000..8bc5fd1
--- /dev/null
+++ b/xbmc/interfaces/legacy/InfoTagVideo.cpp
@@ -0,0 +1,1063 @@
+/*
+ * 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 "InfoTagVideo.h"
+
+#include "AddonUtils.h"
+#include "ServiceBroker.h"
+#include "interfaces/legacy/Exception.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <utility>
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ Actor::Actor(const String& name /* = emptyString */,
+ const String& role /* = emptyString */,
+ int order /* = -1 */,
+ const String& thumbnail /* = emptyString */)
+ : m_name(name), m_role(role), m_order(order), m_thumbnail(thumbnail)
+ {
+ if (m_name.empty())
+ throw WrongTypeException("Actor: name property must not be empty");
+ }
+
+ SActorInfo Actor::ToActorInfo() const
+ {
+ SActorInfo actorInfo;
+ actorInfo.strName = m_name;
+ actorInfo.strRole = m_role;
+ actorInfo.order = m_order;
+ actorInfo.thumbUrl = CScraperUrl(m_thumbnail);
+ if (!actorInfo.thumbUrl.GetFirstThumbUrl().empty())
+ actorInfo.thumb = CScraperUrl::GetThumbUrl(actorInfo.thumbUrl.GetFirstUrlByType());
+
+ return actorInfo;
+ }
+
+ VideoStreamDetail::VideoStreamDetail(int width /* = 0 */,
+ int height /* = 0 */,
+ float aspect /* = 0.0f */,
+ int duration /* = 0 */,
+ const String& codec /* = emptyString */,
+ const String& stereoMode /* = emptyString */,
+ const String& language /* = emptyString */,
+ const String& hdrType /* = emptyString */)
+ : m_width(width),
+ m_height(height),
+ m_aspect(aspect),
+ m_duration(duration),
+ m_codec(codec),
+ m_stereoMode(stereoMode),
+ m_language(language),
+ m_hdrType(hdrType)
+ {
+ }
+
+ CStreamDetailVideo* VideoStreamDetail::ToStreamDetailVideo() const
+ {
+ auto streamDetail = new CStreamDetailVideo();
+ streamDetail->m_iWidth = m_width;
+ streamDetail->m_iHeight = m_height;
+ streamDetail->m_fAspect = m_aspect;
+ streamDetail->m_iDuration = m_duration;
+ streamDetail->m_strCodec = m_codec;
+ streamDetail->m_strStereoMode = m_stereoMode;
+ streamDetail->m_strLanguage = m_language;
+ streamDetail->m_strHdrType = m_hdrType;
+
+ return streamDetail;
+ }
+
+ AudioStreamDetail::AudioStreamDetail(int channels /* = -1 */,
+ const String& codec /* = emptyString */,
+ const String& language /* = emptyString */)
+ : m_channels(channels), m_codec(codec), m_language(language)
+ {
+ }
+
+ CStreamDetailAudio* AudioStreamDetail::ToStreamDetailAudio() const
+ {
+ auto streamDetail = new CStreamDetailAudio();
+ streamDetail->m_iChannels = m_channels;
+ streamDetail->m_strCodec = m_codec;
+ streamDetail->m_strLanguage = m_language;
+
+ return streamDetail;
+ }
+
+ SubtitleStreamDetail::SubtitleStreamDetail(const String& language /* = emptyString */)
+ : m_language(language)
+ {
+ }
+
+ CStreamDetailSubtitle* SubtitleStreamDetail::ToStreamDetailSubtitle() const
+ {
+ auto streamDetail = new CStreamDetailSubtitle();
+ streamDetail->m_strLanguage = m_language;
+
+ return streamDetail;
+ }
+
+ InfoTagVideo::InfoTagVideo(bool offscreen /* = false */)
+ : infoTag(new CVideoInfoTag), offscreen(offscreen), owned(true)
+ {
+ }
+
+ InfoTagVideo::InfoTagVideo(const CVideoInfoTag* tag)
+ : infoTag(new CVideoInfoTag(*tag)), offscreen(true), owned(true)
+ {
+ }
+
+ InfoTagVideo::InfoTagVideo(CVideoInfoTag* tag, bool offscreen /* = false */)
+ : infoTag(tag), offscreen(offscreen), owned(false)
+ {
+ }
+
+ InfoTagVideo::~InfoTagVideo()
+ {
+ if (owned)
+ delete infoTag;
+ }
+
+ int InfoTagVideo::getDbId()
+ {
+ return infoTag->m_iDbId;
+ }
+
+ String InfoTagVideo::getDirector()
+ {
+ return StringUtils::Join(infoTag->m_director, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+
+ std::vector<String> InfoTagVideo::getDirectors()
+ {
+ return infoTag->m_director;
+ }
+
+ String InfoTagVideo::getWritingCredits()
+ {
+ return StringUtils::Join(infoTag->m_writingCredits, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+
+ std::vector<String> InfoTagVideo::getWriters()
+ {
+ return infoTag->m_writingCredits;
+ }
+
+ String InfoTagVideo::getGenre()
+ {
+ return StringUtils::Join(infoTag->m_genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+
+ std::vector<String> InfoTagVideo::getGenres()
+ {
+ return infoTag->m_genre;
+ }
+
+ String InfoTagVideo::getTagLine()
+ {
+ return infoTag->m_strTagLine;
+ }
+
+ String InfoTagVideo::getPlotOutline()
+ {
+ return infoTag->m_strPlotOutline;
+ }
+
+ String InfoTagVideo::getPlot()
+ {
+ return infoTag->m_strPlot;
+ }
+
+ String InfoTagVideo::getPictureURL()
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ infoTag->m_strPictureURL.Parse();
+ return infoTag->m_strPictureURL.GetFirstThumbUrl();
+ }
+
+ String InfoTagVideo::getTVShowTitle()
+ {
+ return infoTag->m_strShowTitle;
+ }
+
+ String InfoTagVideo::getTitle()
+ {
+ return infoTag->m_strTitle;
+ }
+
+ String InfoTagVideo::getMediaType()
+ {
+ return infoTag->m_type;
+ }
+
+ String InfoTagVideo::getVotes()
+ {
+ CLog::Log(
+ LOGWARNING,
+ "InfoTagVideo.getVotes() is deprecated and might be removed in future Kodi versions. "
+ "Please use InfoTagVideo.getVotesAsInt().");
+
+ return std::to_string(getVotesAsInt());
+ }
+
+ int InfoTagVideo::getVotesAsInt(const String& type /* = "" */)
+ {
+ return infoTag->GetRating(type).votes;
+ }
+
+ String InfoTagVideo::getCast()
+ {
+ return infoTag->GetCast(true);
+ }
+
+ std::vector<Actor*> InfoTagVideo::getActors()
+ {
+ std::vector<Actor*> actors;
+ actors.reserve(infoTag->m_cast.size());
+
+ for (const auto& cast : infoTag->m_cast)
+ actors.push_back(new Actor(cast.strName, cast.strRole, cast.order, cast.thumbUrl.GetFirstUrlByType().m_url));
+
+ return actors;
+ }
+
+ String InfoTagVideo::getFile()
+ {
+ return infoTag->m_strFile;
+ }
+
+ String InfoTagVideo::getPath()
+ {
+ return infoTag->m_strPath;
+ }
+
+ String InfoTagVideo::getFilenameAndPath()
+ {
+ return infoTag->m_strFileNameAndPath;
+ }
+
+ String InfoTagVideo::getIMDBNumber()
+ {
+ return infoTag->GetUniqueID();
+ }
+
+ int InfoTagVideo::getSeason()
+ {
+ return infoTag->m_iSeason;
+ }
+
+ int InfoTagVideo::getEpisode()
+ {
+ return infoTag->m_iEpisode;
+ }
+
+ int InfoTagVideo::getYear()
+ {
+ return infoTag->GetYear();
+ }
+
+ double InfoTagVideo::getRating(const String& type /* = "" */)
+ {
+ return static_cast<double>(infoTag->GetRating(type).rating);
+ }
+
+ int InfoTagVideo::getUserRating()
+ {
+ return infoTag->m_iUserRating;
+ }
+
+ int InfoTagVideo::getPlayCount()
+ {
+ return infoTag->GetPlayCount();
+ }
+
+ String InfoTagVideo::getLastPlayed()
+ {
+ CLog::Log(LOGWARNING, "InfoTagVideo.getLastPlayed() is deprecated and might be removed in "
+ "future Kodi versions. Please use InfoTagVideo.getLastPlayedAsW3C().");
+
+ return infoTag->m_lastPlayed.GetAsLocalizedDateTime();
+ }
+
+ String InfoTagVideo::getLastPlayedAsW3C()
+ {
+ return infoTag->m_lastPlayed.GetAsW3CDateTime();
+ }
+
+ String InfoTagVideo::getOriginalTitle()
+ {
+ return infoTag->m_strOriginalTitle;
+ }
+
+ String InfoTagVideo::getPremiered()
+ {
+ CLog::Log(LOGWARNING, "InfoTagVideo.getPremiered() is deprecated and might be removed in "
+ "future Kodi versions. Please use InfoTagVideo.getPremieredAsW3C().");
+
+ return infoTag->GetPremiered().GetAsLocalizedDate();
+ }
+
+ String InfoTagVideo::getPremieredAsW3C()
+ {
+ return infoTag->GetPremiered().GetAsW3CDate();
+ }
+
+ String InfoTagVideo::getFirstAired()
+ {
+ CLog::Log(LOGWARNING, "InfoTagVideo.getFirstAired() is deprecated and might be removed in "
+ "future Kodi versions. Please use InfoTagVideo.getFirstAiredAsW3C().");
+
+ return infoTag->m_firstAired.GetAsLocalizedDate();
+ }
+
+ String InfoTagVideo::getFirstAiredAsW3C()
+ {
+ return infoTag->m_firstAired.GetAsW3CDate();
+ }
+
+ String InfoTagVideo::getTrailer()
+ {
+ return infoTag->m_strTrailer;
+ }
+
+ std::vector<std::string> InfoTagVideo::getArtist()
+ {
+ return infoTag->m_artist;
+ }
+
+ String InfoTagVideo::getAlbum()
+ {
+ return infoTag->m_strAlbum;
+ }
+
+ int InfoTagVideo::getTrack()
+ {
+ return infoTag->m_iTrack;
+ }
+
+ unsigned int InfoTagVideo::getDuration()
+ {
+ return infoTag->GetDuration();
+ }
+
+ double InfoTagVideo::getResumeTime()
+ {
+ return infoTag->GetResumePoint().timeInSeconds;
+ }
+
+ double InfoTagVideo::getResumeTimeTotal()
+ {
+ return infoTag->GetResumePoint().totalTimeInSeconds;
+ }
+
+ String InfoTagVideo::getUniqueID(const char* key)
+ {
+ return infoTag->GetUniqueID(key);
+ }
+
+ void InfoTagVideo::setUniqueID(const String& uniqueID,
+ const String& type /* = "" */,
+ bool isDefault /* = false */)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setUniqueIDRaw(infoTag, uniqueID, type, isDefault);
+ }
+
+ void InfoTagVideo::setUniqueIDs(const std::map<String, String>& uniqueIDs,
+ const String& defaultUniqueID /* = "" */)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setUniqueIDsRaw(infoTag, uniqueIDs, defaultUniqueID);
+ }
+
+ void InfoTagVideo::setDbId(int dbId)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setDbIdRaw(infoTag, dbId);
+ }
+
+ void InfoTagVideo::setYear(int year)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setYearRaw(infoTag, year);
+ }
+
+ void InfoTagVideo::setEpisode(int episode)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setEpisodeRaw(infoTag, episode);
+ }
+
+ void InfoTagVideo::setSeason(int season)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setSeasonRaw(infoTag, season);
+ }
+
+ void InfoTagVideo::setSortEpisode(int sortEpisode)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setSortEpisodeRaw(infoTag, sortEpisode);
+ }
+
+ void InfoTagVideo::setSortSeason(int sortSeason)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setSortSeasonRaw(infoTag, sortSeason);
+ }
+
+ void InfoTagVideo::setEpisodeGuide(const String& episodeGuide)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setEpisodeGuideRaw(infoTag, episodeGuide);
+ }
+
+ void InfoTagVideo::setTop250(int top250)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setTop250Raw(infoTag, top250);
+ }
+
+ void InfoTagVideo::setSetId(int setId)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setSetIdRaw(infoTag, setId);
+ }
+
+ void InfoTagVideo::setTrackNumber(int trackNumber)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setTrackNumberRaw(infoTag, trackNumber);
+ }
+
+ void InfoTagVideo::setRating(float rating,
+ int votes /* = 0 */,
+ const String& type /* = "" */,
+ bool isDefault /* = false */)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setRatingRaw(infoTag, rating, votes, type, isDefault);
+ }
+
+ void InfoTagVideo::setRatings(const std::map<String, Tuple<float, int>>& ratings,
+ const String& defaultRating /* = "" */)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setRatingsRaw(infoTag, ratings, defaultRating);
+ }
+
+ void InfoTagVideo::setUserRating(int userRating)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setUserRatingRaw(infoTag, userRating);
+ }
+
+ void InfoTagVideo::setPlaycount(int playcount)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setPlaycountRaw(infoTag, playcount);
+ }
+
+ void InfoTagVideo::setMpaa(const String& mpaa)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setMpaaRaw(infoTag, mpaa);
+ }
+
+ void InfoTagVideo::setPlot(const String& plot)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setPlotRaw(infoTag, plot);
+ }
+
+ void InfoTagVideo::setPlotOutline(const String& plotOutline)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setPlotOutlineRaw(infoTag, plotOutline);
+ }
+
+ void InfoTagVideo::setTitle(const String& title)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setTitleRaw(infoTag, title);
+ }
+
+ void InfoTagVideo::setOriginalTitle(const String& originalTitle)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setOriginalTitleRaw(infoTag, originalTitle);
+ }
+
+ void InfoTagVideo::setSortTitle(const String& sortTitle)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setSortTitleRaw(infoTag, sortTitle);
+ }
+
+ void InfoTagVideo::setTagLine(const String& tagLine)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setTagLineRaw(infoTag, tagLine);
+ }
+
+ void InfoTagVideo::setTvShowTitle(const String& tvshowTitle)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setTvShowTitleRaw(infoTag, tvshowTitle);
+ }
+
+ void InfoTagVideo::setTvShowStatus(const String& tvshowStatus)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setTvShowStatusRaw(infoTag, tvshowStatus);
+ }
+
+ void InfoTagVideo::setGenres(std::vector<String> genre)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setGenresRaw(infoTag, std::move(genre));
+ }
+
+ void InfoTagVideo::setCountries(std::vector<String> countries)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setCountriesRaw(infoTag, std::move(countries));
+ }
+
+ void InfoTagVideo::setDirectors(std::vector<String> directors)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setDirectorsRaw(infoTag, std::move(directors));
+ }
+
+ void InfoTagVideo::setStudios(std::vector<String> studios)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setStudiosRaw(infoTag, std::move(studios));
+ }
+
+ void InfoTagVideo::setWriters(std::vector<String> writers)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setWritersRaw(infoTag, std::move(writers));
+ }
+
+ void InfoTagVideo::setDuration(int duration)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setDurationRaw(infoTag, duration);
+ }
+
+ void InfoTagVideo::setPremiered(const String& premiered)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setPremieredRaw(infoTag, premiered);
+ }
+
+ void InfoTagVideo::setSet(const String& set)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setSetRaw(infoTag, set);
+ }
+
+ void InfoTagVideo::setSetOverview(const String& setOverview)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setSetOverviewRaw(infoTag, setOverview);
+ }
+
+ void InfoTagVideo::setTags(std::vector<String> tags)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setTagsRaw(infoTag, std::move(tags));
+ }
+
+ void InfoTagVideo::setProductionCode(const String& productionCode)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setProductionCodeRaw(infoTag, productionCode);
+ }
+
+ void InfoTagVideo::setFirstAired(const String& firstAired)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setFirstAiredRaw(infoTag, firstAired);
+ }
+
+ void InfoTagVideo::setLastPlayed(const String& lastPlayed)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setLastPlayedRaw(infoTag, lastPlayed);
+ }
+
+ void InfoTagVideo::setAlbum(const String& album)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setAlbumRaw(infoTag, album);
+ }
+
+ void InfoTagVideo::setVotes(int votes)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setVotesRaw(infoTag, votes);
+ }
+
+ void InfoTagVideo::setTrailer(const String& trailer)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setTrailerRaw(infoTag, trailer);
+ }
+
+ void InfoTagVideo::setPath(const String& path)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setPathRaw(infoTag, path);
+ }
+
+ void InfoTagVideo::setFilenameAndPath(const String& filenameAndPath)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setFilenameAndPathRaw(infoTag, filenameAndPath);
+ }
+
+ void InfoTagVideo::setIMDBNumber(const String& imdbNumber)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setIMDBNumberRaw(infoTag, imdbNumber);
+ }
+
+ void InfoTagVideo::setDateAdded(const String& dateAdded)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setDateAddedRaw(infoTag, dateAdded);
+ }
+
+ void InfoTagVideo::setMediaType(const String& mediaType)
+ {
+ setMediaTypeRaw(infoTag, mediaType);
+ }
+
+ void InfoTagVideo::setShowLinks(std::vector<String> showLinks)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setShowLinksRaw(infoTag, std::move(showLinks));
+ }
+
+ void InfoTagVideo::setArtists(std::vector<String> artists)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setArtistsRaw(infoTag, std::move(artists));
+ }
+
+ void InfoTagVideo::setCast(const std::vector<const Actor*>& actors)
+ {
+ std::vector<SActorInfo> cast;
+ cast.reserve(actors.size());
+ for (const auto& actor : actors)
+ cast.push_back(actor->ToActorInfo());
+
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setCastRaw(infoTag, std::move(cast));
+ }
+ }
+
+ void InfoTagVideo::setResumePoint(double time, double totalTime /* = 0.0 */)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ setResumePointRaw(infoTag, time, totalTime);
+ }
+
+ void InfoTagVideo::addSeason(int number, std::string name /* = "" */)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ addSeasonRaw(infoTag, number, std::move(name));
+ }
+
+ void InfoTagVideo::addSeasons(const std::vector<Tuple<int, std::string>>& namedSeasons)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ addSeasonsRaw(infoTag, namedSeasons);
+ }
+
+ void InfoTagVideo::addVideoStream(const VideoStreamDetail* stream)
+ {
+ if (stream == nullptr)
+ return;
+
+ auto streamDetail = stream->ToStreamDetailVideo();
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ addStreamRaw(infoTag, streamDetail);
+ }
+ }
+
+ void InfoTagVideo::addAudioStream(const AudioStreamDetail* stream)
+ {
+ if (stream == nullptr)
+ return;
+
+ auto streamDetail = stream->ToStreamDetailAudio();
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ addStreamRaw(infoTag, streamDetail);
+ }
+ }
+
+ void InfoTagVideo::addSubtitleStream(const SubtitleStreamDetail* stream)
+ {
+ if (stream == nullptr)
+ return;
+
+ auto streamDetail = stream->ToStreamDetailSubtitle();
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ addStreamRaw(infoTag, streamDetail);
+ }
+ }
+
+ void InfoTagVideo::addAvailableArtwork(const std::string& url,
+ const std::string& art_type,
+ const std::string& preview,
+ const std::string& referrer,
+ const std::string& cache,
+ bool post,
+ bool isgz,
+ int season)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, offscreen);
+ addAvailableArtworkRaw(infoTag, url, art_type, preview, referrer, cache, post, isgz, season);
+ }
+
+ void InfoTagVideo::setDbIdRaw(CVideoInfoTag* infoTag, int dbId)
+ {
+ infoTag->m_iDbId = dbId;
+ }
+
+ void InfoTagVideo::setUniqueIDRaw(CVideoInfoTag* infoTag,
+ const String& uniqueID,
+ const String& type /* = "" */,
+ bool isDefault /* = false */)
+ {
+ infoTag->SetUniqueID(uniqueID, type, isDefault);
+ }
+
+ void InfoTagVideo::setUniqueIDsRaw(CVideoInfoTag* infoTag,
+ std::map<String, String> uniqueIDs,
+ const String& defaultUniqueID /* = "" */)
+ {
+ infoTag->SetUniqueIDs(uniqueIDs);
+ auto defaultUniqueIDEntry = uniqueIDs.find(defaultUniqueID);
+ if (defaultUniqueIDEntry != uniqueIDs.end())
+ infoTag->SetUniqueID(defaultUniqueIDEntry->second, defaultUniqueIDEntry->first, true);
+ }
+
+ void InfoTagVideo::setYearRaw(CVideoInfoTag* infoTag, int year)
+ {
+ infoTag->SetYear(year);
+ }
+
+ void InfoTagVideo::setEpisodeRaw(CVideoInfoTag* infoTag, int episode)
+ {
+ infoTag->m_iEpisode = episode;
+ }
+
+ void InfoTagVideo::setSeasonRaw(CVideoInfoTag* infoTag, int season)
+ {
+ infoTag->m_iSeason = season;
+ }
+
+ void InfoTagVideo::setSortEpisodeRaw(CVideoInfoTag* infoTag, int sortEpisode)
+ {
+ infoTag->m_iSpecialSortEpisode = sortEpisode;
+ }
+
+ void InfoTagVideo::setSortSeasonRaw(CVideoInfoTag* infoTag, int sortSeason)
+ {
+ infoTag->m_iSpecialSortSeason = sortSeason;
+ }
+
+ void InfoTagVideo::setEpisodeGuideRaw(CVideoInfoTag* infoTag, const String& episodeGuide)
+ {
+ infoTag->SetEpisodeGuide(episodeGuide);
+ }
+
+ void InfoTagVideo::setTop250Raw(CVideoInfoTag* infoTag, int top250)
+ {
+ infoTag->m_iTop250 = top250;
+ }
+
+ void InfoTagVideo::setSetIdRaw(CVideoInfoTag* infoTag, int setId)
+ {
+ infoTag->m_set.id = setId;
+ }
+
+ void InfoTagVideo::setTrackNumberRaw(CVideoInfoTag* infoTag, int trackNumber)
+ {
+ infoTag->m_iTrack = trackNumber;
+ }
+
+ void InfoTagVideo::setRatingRaw(CVideoInfoTag* infoTag,
+ float rating,
+ int votes /* = 0 */,
+ const std::string& type /* = "" */,
+ bool isDefault /* = false */)
+ {
+ infoTag->SetRating(rating, votes, type, isDefault);
+ }
+
+ void InfoTagVideo::setRatingsRaw(CVideoInfoTag* infoTag,
+ const std::map<String, Tuple<float, int>>& ratings,
+ const String& defaultRating /* = "" */)
+ {
+ RatingMap ratingMap;
+ for (const auto& rating : ratings)
+ ratingMap.emplace(rating.first, CRating{rating.second.first(), rating.second.second()});
+
+ infoTag->SetRatings(std::move(ratingMap), defaultRating);
+ }
+
+ void InfoTagVideo::setUserRatingRaw(CVideoInfoTag* infoTag, int userRating)
+ {
+ infoTag->m_iUserRating = userRating;
+ }
+
+ void InfoTagVideo::setPlaycountRaw(CVideoInfoTag* infoTag, int playcount)
+ {
+ infoTag->SetPlayCount(playcount);
+ }
+
+ void InfoTagVideo::setMpaaRaw(CVideoInfoTag* infoTag, const String& mpaa)
+ {
+ infoTag->SetMPAARating(mpaa);
+ }
+
+ void InfoTagVideo::setPlotRaw(CVideoInfoTag* infoTag, const String& plot)
+ {
+ infoTag->SetPlot(plot);
+ }
+
+ void InfoTagVideo::setPlotOutlineRaw(CVideoInfoTag* infoTag, const String& plotOutline)
+ {
+ infoTag->SetPlotOutline(plotOutline);
+ }
+
+ void InfoTagVideo::setTitleRaw(CVideoInfoTag* infoTag, const String& title)
+ {
+ infoTag->SetTitle(title);
+ }
+
+ void InfoTagVideo::setOriginalTitleRaw(CVideoInfoTag* infoTag, const String& originalTitle)
+ {
+ infoTag->SetOriginalTitle(originalTitle);
+ }
+
+ void InfoTagVideo::setSortTitleRaw(CVideoInfoTag* infoTag, const String& sortTitle)
+ {
+ infoTag->SetSortTitle(sortTitle);
+ }
+
+ void InfoTagVideo::setTagLineRaw(CVideoInfoTag* infoTag, const String& tagLine)
+ {
+ infoTag->SetTagLine(tagLine);
+ }
+
+ void InfoTagVideo::setTvShowTitleRaw(CVideoInfoTag* infoTag, const String& tvshowTitle)
+ {
+ infoTag->SetShowTitle(tvshowTitle);
+ }
+
+ void InfoTagVideo::setTvShowStatusRaw(CVideoInfoTag* infoTag, const String& tvshowStatus)
+ {
+ infoTag->SetStatus(tvshowStatus);
+ }
+
+ void InfoTagVideo::setGenresRaw(CVideoInfoTag* infoTag, std::vector<String> genre)
+ {
+ infoTag->SetGenre(std::move(genre));
+ }
+
+ void InfoTagVideo::setCountriesRaw(CVideoInfoTag* infoTag, std::vector<String> countries)
+ {
+ infoTag->SetCountry(std::move(countries));
+ }
+
+ void InfoTagVideo::setDirectorsRaw(CVideoInfoTag* infoTag, std::vector<String> directors)
+ {
+ infoTag->SetDirector(std::move(directors));
+ }
+
+ void InfoTagVideo::setStudiosRaw(CVideoInfoTag* infoTag, std::vector<String> studios)
+ {
+ infoTag->SetStudio(std::move(studios));
+ }
+
+ void InfoTagVideo::setWritersRaw(CVideoInfoTag* infoTag, std::vector<String> writers)
+ {
+ infoTag->SetWritingCredits(std::move(writers));
+ }
+
+ void InfoTagVideo::setDurationRaw(CVideoInfoTag* infoTag, int duration)
+ {
+ infoTag->SetDuration(duration);
+ }
+
+ void InfoTagVideo::setPremieredRaw(CVideoInfoTag* infoTag, const String& premiered)
+ {
+ CDateTime premieredDate;
+ premieredDate.SetFromDateString(premiered);
+ infoTag->SetPremiered(premieredDate);
+ }
+
+ void InfoTagVideo::setSetRaw(CVideoInfoTag* infoTag, const String& set)
+ {
+ infoTag->SetSet(set);
+ }
+
+ void InfoTagVideo::setSetOverviewRaw(CVideoInfoTag* infoTag, const String& setOverview)
+ {
+ infoTag->SetSetOverview(setOverview);
+ }
+
+ void InfoTagVideo::setTagsRaw(CVideoInfoTag* infoTag, std::vector<String> tags)
+ {
+ infoTag->SetTags(std::move(tags));
+ }
+
+ void InfoTagVideo::setProductionCodeRaw(CVideoInfoTag* infoTag, const String& productionCode)
+ {
+ infoTag->SetProductionCode(productionCode);
+ }
+
+ void InfoTagVideo::setFirstAiredRaw(CVideoInfoTag* infoTag, const String& firstAired)
+ {
+ CDateTime firstAiredDate;
+ firstAiredDate.SetFromDateString(firstAired);
+ infoTag->m_firstAired = firstAiredDate;
+ }
+
+ void InfoTagVideo::setLastPlayedRaw(CVideoInfoTag* infoTag, const String& lastPlayed)
+ {
+ CDateTime lastPlayedDate;
+ lastPlayedDate.SetFromDBDateTime(lastPlayed);
+ infoTag->m_lastPlayed = lastPlayedDate;
+ }
+
+ void InfoTagVideo::setAlbumRaw(CVideoInfoTag* infoTag, const String& album)
+ {
+ infoTag->SetAlbum(album);
+ }
+
+ void InfoTagVideo::setVotesRaw(CVideoInfoTag* infoTag, int votes)
+ {
+ infoTag->SetVotes(votes);
+ }
+
+ void InfoTagVideo::setTrailerRaw(CVideoInfoTag* infoTag, const String& trailer)
+ {
+ infoTag->SetTrailer(trailer);
+ }
+
+ void InfoTagVideo::setPathRaw(CVideoInfoTag* infoTag, const String& path)
+ {
+ infoTag->SetPath(path);
+ }
+
+ void InfoTagVideo::setFilenameAndPathRaw(CVideoInfoTag* infoTag, const String& filenameAndPath)
+ {
+ infoTag->SetFileNameAndPath(filenameAndPath);
+ }
+
+ void InfoTagVideo::setIMDBNumberRaw(CVideoInfoTag* infoTag, const String& imdbNumber)
+ {
+ infoTag->SetUniqueID(imdbNumber);
+ }
+
+ void InfoTagVideo::setDateAddedRaw(CVideoInfoTag* infoTag, const String& dateAdded)
+ {
+ CDateTime dateAddedDate;
+ dateAddedDate.SetFromDBDateTime(dateAdded);
+ infoTag->m_dateAdded = dateAddedDate;
+ }
+
+ void InfoTagVideo::setMediaTypeRaw(CVideoInfoTag* infoTag, const String& mediaType)
+ {
+ if (CMediaTypes::IsValidMediaType(mediaType))
+ infoTag->m_type = mediaType;
+ }
+
+ void InfoTagVideo::setShowLinksRaw(CVideoInfoTag* infoTag, std::vector<String> showLinks)
+ {
+ infoTag->SetShowLink(std::move(showLinks));
+ }
+
+ void InfoTagVideo::setArtistsRaw(CVideoInfoTag* infoTag, std::vector<String> artists)
+ {
+ infoTag->m_artist = std::move(artists);
+ }
+
+ void InfoTagVideo::setCastRaw(CVideoInfoTag* infoTag, std::vector<SActorInfo> cast)
+ {
+ infoTag->m_cast = std::move(cast);
+ }
+
+ void InfoTagVideo::setResumePointRaw(CVideoInfoTag* infoTag,
+ double time,
+ double totalTime /* = 0.0 */)
+ {
+ auto resumePoint = infoTag->GetResumePoint();
+ resumePoint.timeInSeconds = time;
+ if (totalTime > 0.0)
+ resumePoint.totalTimeInSeconds = totalTime;
+ infoTag->SetResumePoint(resumePoint);
+ }
+
+ void InfoTagVideo::addSeasonRaw(CVideoInfoTag* infoTag, int number, std::string name /* = "" */)
+ {
+ infoTag->m_namedSeasons[number] = std::move(name);
+ }
+
+ void InfoTagVideo::addSeasonsRaw(CVideoInfoTag* infoTag,
+ const std::vector<Tuple<int, std::string>>& namedSeasons)
+ {
+ for (const auto& season : namedSeasons)
+ addSeasonRaw(infoTag, season.first(), season.second());
+ }
+
+ void InfoTagVideo::addStreamRaw(CVideoInfoTag* infoTag, CStreamDetail* stream)
+ {
+ infoTag->m_streamDetails.AddStream(stream);
+ }
+
+ void InfoTagVideo::finalizeStreamsRaw(CVideoInfoTag* infoTag)
+ {
+ infoTag->m_streamDetails.DetermineBestStreams();
+ }
+
+ void InfoTagVideo::addAvailableArtworkRaw(CVideoInfoTag* infoTag,
+ const std::string& url,
+ const std::string& art_type,
+ const std::string& preview,
+ const std::string& referrer,
+ const std::string& cache,
+ bool post,
+ bool isgz,
+ int season)
+ {
+ infoTag->m_strPictureURL.AddParsedUrl(url, art_type, preview, referrer, cache, post, isgz,
+ season);
+ }
+ }
+}
diff --git a/xbmc/interfaces/legacy/InfoTagVideo.h b/xbmc/interfaces/legacy/InfoTagVideo.h
new file mode 100644
index 0000000..91f925e
--- /dev/null
+++ b/xbmc/interfaces/legacy/InfoTagVideo.h
@@ -0,0 +1,2772 @@
+/*
+ * 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 "AddonClass.h"
+#include "Tuple.h"
+#include "utils/StreamDetails.h"
+#include "video/VideoInfoTag.h"
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ ///
+ /// \defgroup python_xbmc_actor Actor
+ /// \ingroup python_xbmc
+ /// @{
+ /// @brief **Actor class used in combination with InfoTagVideo.**
+ ///
+ /// \python_class{ xbmc.Actor([name, role, order, thumbnail]) }
+ ///
+ /// Represents a single actor in the cast of a video item wrapped by InfoTagVideo.
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ /// @python_v20 New class added.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// actor = xbmc.Actor('Sean Connery', 'James Bond', order=1)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class Actor : public AddonClass
+ {
+ public:
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_actor Actor
+ /// @brief \python_func{ xbmc.Actor([name, role, order, thumbnail]) }
+ /// Creates a single actor for the cast of a video item wrapped by InfoTagVideo.
+ ///
+ /// @param name [opt] string - Name of the actor.
+ /// @param role [opt] string - Role of the actor in the specific video item.
+ /// @param order [opt] integer - Order of the actor in the cast of the specific video item.
+ /// @param thumbnail [opt] string - Path / URL to the thumbnail of the actor.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// actor = xbmc.Actor('Sean Connery', 'James Bond', order=1)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ Actor(...);
+#else
+ explicit Actor(const String& name = emptyString,
+ const String& role = emptyString,
+ int order = -1,
+ const String& thumbnail = emptyString);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_actor
+ /// @brief \python_func{ getName() }
+ /// Get the name of the actor.
+ ///
+ /// @return [string] Name of the actor
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getName();
+#else
+ String getName() const { return m_name; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_actor
+ /// @brief \python_func{ getRole() }
+ /// Get the role of the actor in the specific video item.
+ ///
+ /// @return [string] Role of the actor in the specific video item
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getRole();
+#else
+ String getRole() const { return m_role; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_actor
+ /// @brief \python_func{ getOrder() }
+ /// Get the order of the actor in the cast of the specific video item.
+ ///
+ /// @return [integer] Order of the actor in the cast of the specific video item
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getOrder();
+#else
+ int getOrder() const { return m_order; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_actor
+ /// @brief \python_func{ getThumbnail() }
+ /// Get the path / URL to the thumbnail of the actor.
+ ///
+ /// @return [string] Path / URL to the thumbnail of the actor
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getThumbnail();
+#else
+ String getThumbnail() const { return m_thumbnail; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_actor
+ /// @brief \python_func{ setName(name) }
+ /// Set the name of the actor.
+ ///
+ /// @param name string - Name of the actor.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setName(...);
+#else
+ void setName(const String& name) { m_name = name; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_actor
+ /// @brief \python_func{ setRole(role) }
+ /// Set the role of the actor in the specific video item.
+ ///
+ /// @param role string - Role of the actor in the specific video item.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setRole(...);
+#else
+ void setRole(const String& role) { m_role = role; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_actor
+ /// @brief \python_func{ setOrder(order) }
+ /// Set the order of the actor in the cast of the specific video item.
+ ///
+ /// @param order integer - Order of the actor in the cast of the specific video item.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setOrder(...);
+#else
+ void setOrder(int order) { m_order = order; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_actor
+ /// @brief \python_func{ setThumbnail(thumbnail) }
+ /// Set the path / URL to the thumbnail of the actor.
+ ///
+ /// @param thumbnail string - Path / URL to the thumbnail of the actor.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setThumbnail(...);
+#else
+ void setThumbnail(const String& thumbnail) { m_thumbnail = thumbnail; }
+#endif
+
+#ifndef SWIG
+ SActorInfo ToActorInfo() const;
+#endif
+
+ private:
+ String m_name;
+ String m_role;
+ int m_order;
+ String m_thumbnail;
+ };
+ /// @}
+
+ ///
+ /// \defgroup python_xbmc_videostreamdetail VideoStreamDetail
+ /// \ingroup python_xbmc
+ /// @{
+ /// @brief **Video stream details class used in combination with InfoTagVideo.**
+ ///
+ /// \python_class{ xbmc.VideoStreamDetail([width, height, aspect, duration, codec, stereoMode, language, hdrType]) }
+ ///
+ /// Represents a single selectable video stream for a video item wrapped by InfoTagVideo.
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ /// @python_v20 New class added.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// videostream = xbmc.VideoStreamDetail(1920, 1080, language='English')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class VideoStreamDetail : public AddonClass
+ {
+ public:
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ xbmc.VideoStreamDetail([width, height, aspect, duration, codec, stereomode, language, hdrtype]) }
+ /// Creates a single video stream details class for a video item wrapped by InfoTagVideo.
+ ///
+ /// @param width [opt] integer - Width of the video stream in pixel.
+ /// @param height [opt] integer - Height of the video stream in pixel.
+ /// @param aspect [opt] float - Aspect ratio of the video stream.
+ /// @param duration [opt] integer - Duration of the video stream in seconds.
+ /// @param codec [opt] string - Codec of the video stream.
+ /// @param stereomode [opt] string - Stereo mode of the video stream.
+ /// @param language [opt] string - Language of the video stream.
+ /// @param hdrtype [opt] string - HDR type of the video stream.
+ /// The following types are supported:
+ /// dolbyvision, hdr10, hlg
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// videostream = xbmc.VideoStreamDetail(1920, 1080, language='English')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ VideoStreamDetail(...);
+#else
+ explicit VideoStreamDetail(int width = 0,
+ int height = 0,
+ float aspect = 0.0f,
+ int duration = 0,
+ const String& codec = emptyString,
+ const String& stereomode = emptyString,
+ const String& language = emptyString,
+ const String& hdrtype = emptyString);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ getWidth() }
+ /// Get the width of the video stream in pixel.
+ ///
+ /// @return [integer] Width of the video stream
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getWidth();
+#else
+ int getWidth() const { return m_width; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ getHeight() }
+ /// Get the height of the video stream in pixel.
+ ///
+ /// @return [integer] Height of the video stream
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getHeight();
+#else
+ int getHeight() const { return m_height; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ getAspect() }
+ /// Get the aspect ratio of the video stream.
+ ///
+ /// @return [float] Aspect ratio of the video stream
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getAspect();
+#else
+ float getAspect() const { return m_aspect; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ getDuration() }
+ /// Get the duration of the video stream in seconds.
+ ///
+ /// @return [float] Duration of the video stream in seconds
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getDuration();
+#else
+ int getDuration() const { return m_duration; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ getCodec() }
+ /// Get the codec of the stream.
+ ///
+ /// @return [string] Codec of the stream
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getCodec();
+#else
+ String getCodec() const { return m_codec; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ getStereoMode() }
+ /// Get the stereo mode of the video stream.
+ ///
+ /// @return [string] Stereo mode of the video stream
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getStereoMode();
+#else
+ String getStereoMode() const { return m_stereoMode; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ getLanguage() }
+ /// Get the language of the stream.
+ ///
+ /// @return [string] Language of the stream
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getLanguage();
+#else
+ String getLanguage() const { return m_language; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ getHDRType() }
+ /// Get the HDR type of the stream.
+ ///
+ /// @return [string] HDR type of the stream
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getHDRType();
+#else
+ String getHDRType() const { return m_hdrType; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ setWidth(width) }
+ /// Set the width of the video stream in pixel.
+ ///
+ /// @param width integer - Width of the video stream in pixel.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setWidth(...);
+#else
+ void setWidth(int width) { m_width = width; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ setHeight(height) }
+ /// Set the height of the video stream in pixel.
+ ///
+ /// @param height integer - Height of the video stream in pixel.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setHeight(...);
+#else
+ void setHeight(int height) { m_height = height; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ setAspect(aspect) }
+ /// Set the aspect ratio of the video stream.
+ ///
+ /// @param aspect float - Aspect ratio of the video stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setAspect(...);
+#else
+ void setAspect(float aspect) { m_aspect = aspect; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ setDuration(duration) }
+ /// Set the duration of the video stream in seconds.
+ ///
+ /// @param duration integer - Duration of the video stream in seconds.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setDuration(...);
+#else
+ void setDuration(int duration) { m_duration = duration; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ setCodec(codec) }
+ /// Set the codec of the stream.
+ ///
+ /// @param codec string - Codec of the stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setCodec(...);
+#else
+ void setCodec(const String& codec) { m_codec = codec; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ setStereoMode(stereomode) }
+ /// Set the stereo mode of the video stream.
+ ///
+ /// @param stereomode string - Stereo mode of the video stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setStereoMode(...);
+#else
+ void setStereoMode(const String& stereomode) { m_stereoMode = stereomode; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ setLanguage(language) }
+ /// Set the language of the stream.
+ ///
+ /// @param language string - Language of the stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setLanguage(...);
+#else
+ void setLanguage(const String& language) { m_language = language; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_videostreamdetail
+ /// @brief \python_func{ setHDRType(hdrtype) }
+ /// Set the HDR type of the stream.
+ ///
+ /// @param hdrtype string - HDR type of the stream.
+ /// The following types are supported:
+ /// dolbyvision, hdr10, hlg
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setHDRType(...);
+#else
+ void setHDRType(const String& hdrtype) { m_hdrType = hdrtype; }
+#endif
+
+#ifndef SWIG
+ CStreamDetailVideo* ToStreamDetailVideo() const;
+#endif
+
+ private:
+ int m_width;
+ int m_height;
+ float m_aspect;
+ int m_duration;
+ String m_codec;
+ String m_stereoMode;
+ String m_language;
+ String m_hdrType;
+ };
+ /// @}
+
+ ///
+ /// \defgroup python_xbmc_audiostreamdetail AudioStreamDetail
+ /// \ingroup python_xbmc
+ /// @{
+ /// @brief **Audio stream details class used in combination with InfoTagVideo.**
+ ///
+ /// \python_class{ xbmc.AudioStreamDetail([channels, codec, language]) }
+ ///
+ /// Represents a single selectable audio stream for a video item wrapped by InfoTagVideo.
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ /// @python_v20 New class added.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// audiostream = xbmc.AudioStreamDetail(6, 'DTS', 'English')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class AudioStreamDetail : public AddonClass
+ {
+ public:
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_audiostreamdetail AudioStreamDetail
+ /// @brief \python_func{ xbmc.AudioStreamDetail([channels, codec, language]) }
+ /// Creates a single audio stream details class for a video item wrapped by InfoTagVideo.
+ ///
+ /// @param channels [opt] integer - Number of channels in the audio stream.
+ /// @param codec [opt] string - Codec of the audio stream.
+ /// @param language [opt] string - Language of the audio stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// audiostream = xbmc.AudioStreamDetail(6, 'DTS', 'English')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ AudioStreamDetail(...);
+#else
+ explicit AudioStreamDetail(int channels = -1,
+ const String& codec = emptyString,
+ const String& language = emptyString);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_audiostreamdetail
+ /// @brief \python_func{ getChannels() }
+ /// Get the number of channels in the stream.
+ ///
+ /// @return [integer] Number of channels in the stream
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getChannels();
+#else
+ int getChannels() const { return m_channels; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_audiostreamdetail
+ /// @brief \python_func{ getCodec() }
+ /// Get the codec of the stream.
+ ///
+ /// @return [string] Codec of the stream
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getCodec();
+#else
+ String getCodec() const { return m_codec; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_audiostreamdetail
+ /// @brief \python_func{ getLanguage() }
+ /// Get the language of the stream.
+ ///
+ /// @return [string] Language of the stream
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getLanguage();
+#else
+ String getLanguage() const { return m_language; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_audiostreamdetail
+ /// @brief \python_func{ setChannels(channels) }
+ /// Set the number of channels in the stream.
+ ///
+ /// @param channels integer - Number of channels in the stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setChannels(...);
+#else
+ void setChannels(int channels) { m_channels = channels; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_audiostreamdetail
+ /// @brief \python_func{ setCodec(codec) }
+ /// Set the codec of the stream.
+ ///
+ /// @param codec string - Codec of the stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setCodec(...);
+#else
+ void setCodec(const String& codec) { m_codec = codec; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_audiostreamdetail
+ /// @brief \python_func{ setLanguage(language) }
+ /// Set the language of the stream.
+ ///
+ /// @param language string - Language of the stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setLanguage(...);
+#else
+ void setLanguage(const String& language) { m_language = language; }
+#endif
+
+#ifndef SWIG
+ CStreamDetailAudio* ToStreamDetailAudio() const;
+#endif
+
+ private:
+ int m_channels;
+ String m_codec;
+ String m_language;
+ };
+ /// @}
+
+ ///
+ /// \defgroup python_xbmc_subtitlestreamdetail SubtitleStreamDetail
+ /// \ingroup python_xbmc
+ /// @{
+ /// @brief **Subtitle stream details class used in combination with InfoTagVideo.**
+ ///
+ /// \python_class{ xbmc.SubtitleStreamDetail([language]) }
+ ///
+ /// Represents a single selectable subtitle stream for a video item wrapped by InfoTagVideo.
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ /// @python_v20 New class added.
+ ///
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// subtitlestream = xbmc.SubtitleStreamDetail('English')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class SubtitleStreamDetail : public AddonClass
+ {
+ public:
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_subtitlestreamdetail SubtitleStreamDetail
+ /// @brief \python_func{ xbmc.SubtitleStreamDetail([language]) }
+ /// Creates a single subtitle stream details class for a video item wrapped by InfoTagVideo.
+ ///
+ /// @param language [opt] string - Language of the subtitle.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// subtitlestream = xbmc.SubtitleStreamDetail('English')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ SubtitleStreamDetail(...);
+#else
+ explicit SubtitleStreamDetail(const String& language = emptyString);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_subtitlestreamdetail
+ /// @brief \python_func{ getLanguage() }
+ /// Get the language of the stream.
+ ///
+ /// @return [string] Language of the stream
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getLanguage();
+#else
+ String getLanguage() const { return m_language; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_subtitlestreamdetail
+ /// @brief \python_func{ setLanguage(language) }
+ /// Set the language of the stream.
+ ///
+ /// @param language string - Language of the stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setLanguage(...);
+#else
+ void setLanguage(const String& language) { m_language = language; }
+#endif
+
+#ifndef SWIG
+ CStreamDetailSubtitle* ToStreamDetailSubtitle() const;
+#endif
+
+ private:
+ String m_language;
+ };
+ /// @}
+
+ ///
+ /// \defgroup python_InfoTagVideo InfoTagVideo
+ /// \ingroup python_xbmc
+ /// @{
+ /// @brief **Kodi's video info tag class.**
+ ///
+ /// \python_class{ xbmc.InfoTagVideo([offscreen]) }
+ ///
+ /// Access and / or modify the video metadata of a ListItem.
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// tag = xbmc.Player().getVideoInfoTag()
+ ///
+ /// title = tag.getTitle()
+ /// file = tag.getFile()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ class InfoTagVideo : public AddonClass
+ {
+ private:
+ CVideoInfoTag* infoTag;
+ bool offscreen;
+ bool owned;
+
+ public:
+#ifndef SWIG
+ explicit InfoTagVideo(const CVideoInfoTag* tag);
+ explicit InfoTagVideo(CVideoInfoTag* tag, bool offscreen = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ xbmc.InfoTagVideo([offscreen]) }
+ /// Create a video info tag.
+ ///
+ /// @param offscreen [opt] bool (default `False`) - if GUI based locks should be
+ /// avoided. Most of the times listitems are created
+ /// offscreen and added later to a container
+ /// for display (e.g. plugins) or they are not
+ /// even displayed (e.g. python scrapers).
+ /// In such cases, there is no need to lock the
+ /// GUI when creating the items (increasing your addon
+ /// performance).
+ /// Note however, that if you are creating listitems
+ /// and managing the container itself (e.g using
+ /// WindowXML or WindowXMLDialog classes) subsquent
+ /// modifications to the item will require locking.
+ /// Thus, in such cases, use the default value (`False`).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Added **offscreen** argument.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// videoinfo = xbmc.InfoTagVideo(offscreen=False)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ InfoTagVideo(...);
+#else
+ explicit InfoTagVideo(bool offscreen = false);
+#endif
+ ~InfoTagVideo() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getDbId() }
+ /// Get identification number of tag in database
+ ///
+ /// @return [integer] database id
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 New function added.
+ ///
+ getDbId();
+#else
+ int getDbId();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getDirector() }
+ /// Get [film director](https://en.wikipedia.org/wiki/Film_director)
+ /// who has made the film (if present).
+ ///
+ /// @return [string] Film director name.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **getDirectors()** instead.
+ ///
+ getDirector();
+#else
+ String getDirector();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getDirectors() }
+ /// Get a list of [film directors](https://en.wikipedia.org/wiki/Film_director)
+ /// who have made the film (if present).
+ ///
+ /// @return [list] List of film director names.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getDirectors();
+#else
+ std::vector<String> getDirectors();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getWritingCredits() }
+ /// Get the writing credits if present from video info tag.
+ ///
+ /// @return [string] Writing credits
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **getWriters()** instead.
+ ///
+ getWritingCredits();
+#else
+ String getWritingCredits();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getWriters() }
+ /// Get the list of writers (if present) from video info tag.
+ ///
+ /// @return [list] List of writers
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getWriters();
+#else
+ std::vector<String> getWriters();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getGenre() }
+ /// To get the [Video Genre](https://en.wikipedia.org/wiki/Film_genre)
+ /// if available.
+ ///
+ /// @return [string] Genre name
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **getGenres()** instead.
+ ///
+ getGenre();
+#else
+ String getGenre();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getGenres() }
+ /// Get the list of [Video Genres](https://en.wikipedia.org/wiki/Film_genre)
+ /// if available.
+ ///
+ /// @return [list] List of genres
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getGenres();
+#else
+ std::vector<String> getGenres();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getTagLine() }
+ /// Get video tag line if available.
+ ///
+ /// @return [string] Video tag line
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getTagLine();
+#else
+ String getTagLine();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getPlotOutline() }
+ /// Get the outline plot of the video if present.
+ ///
+ /// @return [string] Outline plot
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getPlotOutline();
+#else
+ String getPlotOutline();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getPlot() }
+ /// Get the plot of the video if present.
+ ///
+ /// @return [string] Plot
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getPlot();
+#else
+ String getPlot();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getPictureURL() }
+ /// Get a picture URL of the video to show as screenshot.
+ ///
+ /// @return [string] Picture URL
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getPictureURL();
+#else
+ String getPictureURL();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getTitle() }
+ /// Get the video title.
+ ///
+ /// @return [string] Video title
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getTitle();
+#else
+ String getTitle();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getTVShowTitle() }
+ /// Get the video TV show title.
+ ///
+ /// @return [string] TV show title
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 New function added.
+ ///
+ getTVShowTitle();
+#else
+ String getTVShowTitle();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getMediaType() }
+ /// Get the media type of the video.
+ ///
+ /// @return [string] media type
+ ///
+ /// Available strings about media type for video:
+ /// | String | Description |
+ /// |---------------:|:--------------------------------------------------|
+ /// | video | For normal video
+ /// | set | For a selection of video
+ /// | musicvideo | To define it as music video
+ /// | movie | To define it as normal movie
+ /// | tvshow | If this is it defined as tvshow
+ /// | season | The type is used as a series season
+ /// | episode | The type is used as a series episode
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 New function added.
+ ///
+ getMediaType();
+#else
+ String getMediaType();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getVotes() }
+ /// Get the video votes if available from video info tag.
+ ///
+ /// @return [string] Votes
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **getVotesAsInt()** instead.
+ ///
+ getVotes();
+#else
+ String getVotes();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getVotesAsInt([type]) }
+ /// Get the votes of the rating (if available) as an integer.
+ ///
+ /// @param type [opt] string - the type of the rating.
+ /// - Some rating type values (any string possible):
+ /// | Label | Type |
+ /// |---------------|--------------------------------------------------|
+ /// | imdb | string - type name
+ /// | tvdb | string - type name
+ /// | tmdb | string - type name
+ /// | anidb | string - type name
+ ///
+ /// @return [integer] Votes
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getVotesAsInt(type);
+#else
+ int getVotesAsInt(const String& type = "");
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getCast() }
+ /// To get the cast of the video when available.
+ ///
+ /// @return [string] Video casts
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **getActors()** instead.
+ ///
+ getCast();
+#else
+ String getCast();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getActors() }
+ /// Get the cast of the video if available.
+ ///
+ /// @return [list] List of actors
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getActors();
+#else
+ std::vector<Actor*> getActors();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getFile() }
+ /// To get the video file name.
+ ///
+ /// @return [string] File name
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getFile();
+#else
+ String getFile();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getPath() }
+ /// To get the path where the video is stored.
+ ///
+ /// @return [string] Path
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getPath();
+#else
+ String getPath();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getFilenameAndPath() }
+ /// To get the full path with filename where the video is stored.
+ ///
+ /// @return [string] File name and Path
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v19 New function added.
+ ///
+ getFilenameAndPath();
+#else
+ String getFilenameAndPath();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getIMDBNumber() }
+ /// To get the [IMDb](https://en.wikipedia.org/wiki/Internet_Movie_Database)
+ /// number of the video (if present).
+ ///
+ /// @return [string] IMDb number
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getIMDBNumber();
+#else
+ String getIMDBNumber();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getSeason() }
+ /// To get season number of a series
+ ///
+ /// @return [integer] season number
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 New function added.
+ ///
+ getSeason();
+#else
+ int getSeason();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getEpisode() }
+ /// To get episode number of a series
+ ///
+ /// @return [integer] episode number
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 New function added.
+ ///
+ getEpisode();
+#else
+ int getEpisode();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getYear() }
+ /// Get production year of video if present.
+ ///
+ /// @return [integer] Production Year
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getYear();
+#else
+ int getYear();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getRating([type]) }
+ /// Get the video rating if present as float (double where supported).
+ ///
+ /// @param type [opt] string - the type of the rating.
+ /// - Some rating type values (any string possible):
+ /// | Label | Type |
+ /// |---------------|--------------------------------------------------|
+ /// | imdb | string - type name
+ /// | tvdb | string - type name
+ /// | tmdb | string - type name
+ /// | anidb | string - type name
+ ///
+ /// @return [float] The rating of the video
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Optional `type` parameter added.
+ ///
+ getRating(type);
+#else
+ double getRating(const String& type = "");
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getUserRating() }
+ /// Get the user rating if present as integer.
+ ///
+ /// @return [integer] The user rating of the video
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getUserRating();
+#else
+ int getUserRating();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getPlayCount() }
+ /// To get the number of plays of the video.
+ ///
+ /// @return [integer] Play Count
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getPlayCount();
+#else
+ int getPlayCount();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getLastPlayed() }
+ /// Get the last played date / time as string.
+ ///
+ /// @return [string] Last played date / time
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **getLastPlayedAsW3C()** instead.
+ ///
+ getLastPlayed();
+#else
+ String getLastPlayed();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getLastPlayedAsW3C() }
+ /// Get last played datetime as string in W3C format (YYYY-MM-DDThh:mm:ssTZD).
+ ///
+ /// @return [string] Last played datetime (W3C)
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getLastPlayedAsW3C();
+#else
+ String getLastPlayedAsW3C();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getOriginalTitle() }
+ /// To get the original title of the video.
+ ///
+ /// @return [string] Original title
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ ///
+ getOriginalTitle();
+#else
+ String getOriginalTitle();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getPremiered() }
+ /// To get [premiered](https://en.wikipedia.org/wiki/Premiere) date
+ /// of the video, if available.
+ ///
+ /// @return [string]
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **getPremieredAsW3C()** instead.
+ ///
+ getPremiered();
+#else
+ String getPremiered();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getPremieredAsW3C() }
+ /// Get [premiered](https://en.wikipedia.org/wiki/Premiere) date as string in W3C format (YYYY-MM-DD).
+ ///
+ /// @return [string] Premiered date (W3C)
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getPremieredAsW3C();
+#else
+ String getPremieredAsW3C();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getFirstAired() }
+ /// Returns first aired date as string from info tag.
+ ///
+ /// @return [string] First aired date
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **getFirstAiredAsW3C()** instead.
+ ///
+ getFirstAired();
+#else
+ String getFirstAired();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getFirstAiredAsW3C() }
+ /// Get first aired date as string in W3C format (YYYY-MM-DD).
+ ///
+ /// @return [string] First aired date (W3C)
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getFirstAiredAsW3C();
+#else
+ String getFirstAiredAsW3C();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getTrailer() }
+ /// To get the path where the trailer is stored.
+ ///
+ /// @return [string] Trailer path
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 New function added.
+ ///
+ getTrailer();
+#else
+ String getTrailer();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getArtist() }
+ /// To get the artist name (for musicvideos)
+ ///
+ /// @return [std::vector<std::string>] Artist name
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ getArtist();
+#else
+ std::vector<std::string> getArtist();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getAlbum() }
+ /// To get the album name (for musicvideos)
+ ///
+ /// @return [string] Album name
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ getAlbum();
+#else
+ String getAlbum();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getTrack() }
+ /// To get the track number (for musicvideos)
+ ///
+ /// @return [int] Track number
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ getTrack();
+#else
+ int getTrack();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getDuration() }
+ /// To get the duration
+ ///
+ /// @return [unsigned int] Duration
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ getDuration();
+#else
+ unsigned int getDuration();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getResumeTime()) }
+ /// Gets the resume time of the video item.
+ ///
+ /// @return [double] Resume time
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getResumeTime(...);
+#else
+ double getResumeTime();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getResumeTimeTotal()) }
+ /// Gets the total duration stored with the resume time of the video item.
+ ///
+ /// @return [double] Total duration stored with the resume time
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getResumeTimeTotal(...);
+#else
+ double getResumeTimeTotal();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ getUniqueID(key) }
+ /// Get the unique ID of the given key.
+ /// A unique ID is an identifier used by a (online) video database used to
+ /// identify a video in its database.
+ ///
+ /// @param key string - uniqueID name.
+ /// - Some default uniqueID values (any string possible):
+ /// | Label | Type |
+ /// |---------------|--------------------------------------------------|
+ /// | imdb | string - uniqueid name
+ /// | tvdb | string - uniqueid name
+ /// | tmdb | string - uniqueid name
+ /// | anidb | string - uniqueid name
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getUniqueID(key);
+#else
+ String getUniqueID(const char* key);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setUniqueID(uniqueid, [type], [isdefault]) }
+ /// Set the given unique ID.
+ /// A unique ID is an identifier used by a (online) video database used to
+ /// identify a video in its database.
+ ///
+ /// @param uniqueid string - value of the unique ID.
+ /// @param type [opt] string - type / label of the unique ID.
+ /// @param isdefault [opt] bool - whether the given unique ID is the default unique ID.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setUniqueID(...);
+#else
+ void setUniqueID(const String& uniqueid, const String& type = "", bool isdefault = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setUniqueIDs(values, defaultuniqueid) }
+ /// Set the given unique IDs.
+ /// A unique ID is an identifier used by a (online) video database used to
+ /// identify a video in its database.
+ ///
+ /// @param values dictionary - pairs of `{ 'label': 'value' }`.
+ /// @param defaultuniqueid [opt] string - the name of default uniqueID.
+ ///
+ /// - Some example values (any string possible):
+ /// | Label | Type |
+ /// |:-------------:|:--------------------------------------------------|
+ /// | imdb | string - uniqueid name
+ /// | tvdb | string - uniqueid name
+ /// | tmdb | string - uniqueid name
+ /// | anidb | string - uniqueid name
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setUniqueIDs(...);
+#else
+ void setUniqueIDs(const std::map<String, String>& uniqueIDs,
+ const String& defaultuniqueid = "");
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setDbId(dbid) }
+ /// Set the database identifier of the video item.
+ ///
+ /// @param dbid integer - Database identifier.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setDbId(...);
+#else
+ void setDbId(int dbid);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setYear(year) }
+ /// Set the year of the video item.
+ ///
+ /// @param year integer - Year.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setYear(...);
+#else
+ void setYear(int year);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setEpisode(episode) }
+ /// Set the episode number of the episode.
+ ///
+ /// @param episode integer - Episode number.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setEpisode(...);
+#else
+ void setEpisode(int episode);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setSeason(season) }
+ /// Set the season number of the video item.
+ ///
+ /// @param season integer - Season number.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setSeason(...);
+#else
+ void setSeason(int season);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setSortEpisode(sortepisode) }
+ /// Set the episode sort number of the episode.
+ ///
+ /// @param sortepisode integer - Episode sort number.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setSortEpisode(...);
+#else
+ void setSortEpisode(int sortepisode);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setSortSeason(sortseason) }
+ /// Set the season sort number of the season.
+ ///
+ /// @param sortseason integer - Season sort number.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setSortSeason(...);
+#else
+ void setSortSeason(int sortseason);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setEpisodeGuide(episodeguide) }
+ /// Set the episode guide of the video item.
+ ///
+ /// @param episodeguide string - Episode guide.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setEpisodeGuide(...);
+#else
+ void setEpisodeGuide(const String& episodeguide);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setTop250(top250) }
+ /// Set the top 250 number of the video item.
+ ///
+ /// @param top250 integer - Top 250 number.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setTop250(...);
+#else
+ void setTop250(int top250);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setSetId(setid) }
+ /// Set the movie set identifier of the video item.
+ ///
+ /// @param setid integer - Set identifier.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setSetId(...);
+#else
+ void setSetId(int setid);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setTrackNumber(tracknumber) }
+ /// Set the track number of the music video item.
+ ///
+ /// @param tracknumber integer - Track number.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setTrackNumber(...);
+#else
+ void setTrackNumber(int tracknumber);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setRating(rating, [votes], [type], [isdefault]) }
+ /// Set the rating of the video item.
+ ///
+ /// @param rating float - Rating number.
+ /// @param votes integer - Number of votes.
+ /// @param type string - Type of the rating.
+ /// @param isdefault bool - Whether the rating is the default or not.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setRating(...);
+#else
+ void setRating(float rating, int votes = 0, const String& type = "", bool isdefault = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setRatings(ratings, [defaultrating]) }
+ /// Set the ratings of the video item.
+ ///
+ /// @param ratings dictionary - `{ 'type': (rating, votes) }`.
+ /// @param defaultrating string - Type / Label of the default rating.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setRatings(...);
+#else
+ void setRatings(const std::map<String, Tuple<float, int>>& ratings,
+ const String& defaultrating = "");
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setUserRating(userrating) }
+ /// Set the user rating of the video item.
+ ///
+ /// @param userrating integer - User rating.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setUserRating(...);
+#else
+ void setUserRating(int userrating);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setPlaycount(playcount) }
+ /// Set the playcount of the video item.
+ ///
+ /// @param playcount integer - Playcount.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setPlaycount(...);
+#else
+ void setPlaycount(int playcount);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setMpaa(mpaa) }
+ /// Set the MPAA rating of the video item.
+ ///
+ /// @param mpaa string - MPAA rating.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setMpaa(...);
+#else
+ void setMpaa(const String& mpaa);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setPlot(plot) }
+ /// Set the plot of the video item.
+ ///
+ /// @param plot string - Plot.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setPlot(...);
+#else
+ void setPlot(const String& plot);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setPlotOutline(plotoutline) }
+ /// Set the plot outline of the video item.
+ ///
+ /// @param plotoutline string - Plot outline.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setPlotOutline(...);
+#else
+ void setPlotOutline(const String& plotoutline);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setTitle(title) }
+ /// Set the title of the video item.
+ ///
+ /// @param title string - Title.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setTitle(...);
+#else
+ void setTitle(const String& title);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setOriginalTitle(originaltitle) }
+ /// Set the original title of the video item.
+ ///
+ /// @param originaltitle string - Original title.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setOriginalTitle(...);
+#else
+ void setOriginalTitle(const String& originaltitle);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setSortTitle(sorttitle) }
+ /// Set the sort title of the video item.
+ ///
+ /// @param sorttitle string - Sort title.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setSortTitle(...);
+#else
+ void setSortTitle(const String& sorttitle);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setTagLine(tagline) }
+ /// Set the tagline of the video item.
+ ///
+ /// @param tagline string - Tagline.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setTagLine(...);
+#else
+ void setTagLine(const String& tagline);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setTvShowTitle(tvshowtitle) }
+ /// Set the TV show title of the video item.
+ ///
+ /// @param tvshowtitle string - TV show title.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setTvShowTitle(...);
+#else
+ void setTvShowTitle(const String& tvshowtitle);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setTvShowStatus(tvshowstatus) }
+ /// Set the TV show status of the video item.
+ ///
+ /// @param status string - TV show status.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setTvShowStatus(...);
+#else
+ void setTvShowStatus(const String& status);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setGenres(genre) }
+ /// Set the genres of the video item.
+ ///
+ /// @param genre list - Genres.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setGenres(...);
+#else
+ void setGenres(std::vector<String> genre);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setCountries(countries) }
+ /// Set the countries of the video item.
+ ///
+ /// @param countries list - Countries.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setCountries(...);
+#else
+ void setCountries(std::vector<String> countries);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setDirectors(directors) }
+ /// Set the directors of the video item.
+ ///
+ /// @param directors list - Directors.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setDirectors(...);
+#else
+ void setDirectors(std::vector<String> directors);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setStudios(studios) }
+ /// Set the studios of the video item.
+ ///
+ /// @param studios list - Studios.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setStudios(...);
+#else
+ void setStudios(std::vector<String> studios);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setWriters(writers) }
+ /// Set the writers of the video item.
+ ///
+ /// @param writers list - Writers.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setWriters(...);
+#else
+ void setWriters(std::vector<String> writers);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setDuration(duration) }
+ /// Set the duration of the video item.
+ ///
+ /// @param duration integer - Duration in seconds.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setDuration(...);
+#else
+ void setDuration(int duration);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setPremiered(premiered) }
+ /// Set the premiere date of the video item.
+ ///
+ /// @param premiered string - Premiere date.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setPremiered(...);
+#else
+ void setPremiered(const String& premiered);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setSet(set) }
+ /// Set the movie set (name) of the video item.
+ ///
+ /// @param set string - Movie set (name).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setSet(...);
+#else
+ void setSet(const String& set);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setSetOverview(setoverview) }
+ /// Set the movie set overview of the video item.
+ ///
+ /// @param setoverview string - Movie set overview.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setSetOverview(...);
+#else
+ void setSetOverview(const String& setoverview);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setTags(tags) }
+ /// Set the tags of the video item.
+ ///
+ /// @param tags list - Tags.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setTags(...);
+#else
+ void setTags(std::vector<String> tags);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setProductionCode(const String& productioncode) }
+ /// Set the production code of the video item.
+ ///
+ /// @param productioncode string - Production code.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setProductionCode(...);
+#else
+ void setProductionCode(const String& productioncode);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setFirstAired(firstaired) }
+ /// Set the first aired date of the video item.
+ ///
+ /// @param firstaired string - First aired date.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setFirstAired(...);
+#else
+ void setFirstAired(const String& firstaired);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setLastPlayed(lastplayed) }
+ /// Set the last played date of the video item.
+ ///
+ /// @param lastplayed string - Last played date (YYYY-MM-DD HH:MM:SS).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setLastPlayed(...);
+#else
+ void setLastPlayed(const String& lastplayed);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setAlbum(album) }
+ /// Set the album of the video item.
+ ///
+ /// @param album string - Album.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setAlbum(...);
+#else
+ void setAlbum(const String& album);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setVotes(votes) }
+ /// Set the number of votes of the video item.
+ ///
+ /// @param votes integer - Number of votes.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setVotes(...);
+#else
+ void setVotes(int votes);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setTrailer(trailer) }
+ /// Set the trailer of the video item.
+ ///
+ /// @param trailer string - Trailer.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setTrailer(...);
+#else
+ void setTrailer(const String& trailer);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setPath(path) }
+ /// Set the path of the video item.
+ ///
+ /// @param path string - Path.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setPath(...);
+#else
+ void setPath(const String& path);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setFilenameAndPath(filenameandpath) }
+ /// Set the filename and path of the video item.
+ ///
+ /// @param filenameandpath string - Filename and path.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setFilenameAndPath(...);
+#else
+ void setFilenameAndPath(const String& filenameandpath);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setIMDBNumber(imdbnumber) }
+ /// Set the IMDb number of the video item.
+ ///
+ /// @param imdbnumber string - IMDb number.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setIMDBNumber(...);
+#else
+ void setIMDBNumber(const String& imdbnumber);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setDateAdded(dateadded) }
+ /// Set the date added of the video item.
+ ///
+ /// @param dateadded string - Date added (YYYY-MM-DD HH:MM:SS).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setDateAdded(...);
+#else
+ void setDateAdded(const String& dateadded);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setMediaType(mediatype) }
+ /// Set the media type of the video item.
+ ///
+ /// @param mediatype string - Media type.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setMediaType(...);
+#else
+ void setMediaType(const String& mediatype);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setShowLinks(showlinks) }
+ /// Set the TV show links of the movie.
+ ///
+ /// @param showlinks list - TV show links.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setShowLinks(...);
+#else
+ void setShowLinks(std::vector<String> showlinks);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setArtists(artists) }
+ /// Set the artists of the music video item.
+ ///
+ /// @param artists list - Artists.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setArtists(...);
+#else
+ void setArtists(std::vector<String> artists);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setCast(actors) }
+ /// Set the cast / actors of the video item.
+ ///
+ /// @param actors list - Cast / Actors.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setCast(...);
+#else
+ void setCast(const std::vector<const Actor*>& actors);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ setResumePoint(time, [totaltime]) }
+ /// Set the resume point of the video item.
+ ///
+ /// @param time float - Resume point in seconds.
+ /// @param totaltime float - Total duration in seconds.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ setResumePoint(...);
+#else
+ void setResumePoint(double time, double totaltime = 0.0);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ addSeason(number, [name]) }
+ /// Add a season with name. It needs at least the season number.
+ ///
+ /// @param number int - the number of the season.
+ /// @param name string - the name of the season. Default "".
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # addSeason(number, name))
+ /// infotagvideo.addSeason(1, "Murder House")
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ addSeason(...);
+#else
+ void addSeason(int number, std::string name = "");
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ addSeasons(namedseasons) }
+ /// Add named seasons to the TV show.
+ ///
+ /// @param namedseasons list - `[ (season, name) ]`.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ addSeasons(...);
+#else
+ void addSeasons(const std::vector<Tuple<int, std::string>>& namedseasons);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ addVideoStream(stream) }
+ /// Add a video stream to the video item.
+ ///
+ /// @param stream VideoStreamDetail - Video stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ addVideoStream(...);
+#else
+ void addVideoStream(const VideoStreamDetail* stream);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ addAudioStream(stream) }
+ /// Add an audio stream to the video item.
+ ///
+ /// @param stream AudioStreamDetail - Audio stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ addAudioStream(...);
+#else
+ void addAudioStream(const AudioStreamDetail* stream);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ addSubtitleStream(stream) }
+ /// Add a subtitle stream to the video item.
+ ///
+ /// @param stream SubtitleStreamDetail - Subtitle stream.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ addSubtitleStream(...);
+#else
+ void addSubtitleStream(const SubtitleStreamDetail* stream);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_InfoTagVideo
+ /// @brief \python_func{ addAvailableArtwork(images) }
+ /// Add an image to available artworks (needed for video scrapers)
+ ///
+ /// @param url string - image path url
+ /// @param arttype string - image type
+ /// @param preview [opt] string - image preview path url
+ /// @param referrer [opt] string - referrer url
+ /// @param cache [opt] string - filename in cache
+ /// @param post [opt] bool - use post to retrieve the image (default false)
+ /// @param isgz [opt] bool - use gzip to retrieve the image (default false)
+ /// @param season [opt] integer - number of season in case of season thumb
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// infotagvideo.addAvailableArtwork(path_to_image_1, "thumb")
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ addAvailableArtwork(...);
+#else
+ void addAvailableArtwork(const std::string& url,
+ const std::string& arttype = "",
+ const std::string& preview = "",
+ const std::string& referrer = "",
+ const std::string& cache = "",
+ bool post = false,
+ bool isgz = false,
+ int season = -1);
+#endif
+ /// @}
+
+#ifndef SWIG
+ static void setDbIdRaw(CVideoInfoTag* infoTag, int dbId);
+ static void setUniqueIDRaw(CVideoInfoTag* infoTag,
+ const String& uniqueID,
+ const String& type = "",
+ bool isDefault = false);
+ static void setUniqueIDsRaw(CVideoInfoTag* infoTag,
+ std::map<String, String> uniqueIDs,
+ const String& defaultUniqueID = "");
+ static void setYearRaw(CVideoInfoTag* infoTag, int year);
+ static void setEpisodeRaw(CVideoInfoTag* infoTag, int episode);
+ static void setSeasonRaw(CVideoInfoTag* infoTag, int season);
+ static void setSortEpisodeRaw(CVideoInfoTag* infoTag, int sortEpisode);
+ static void setSortSeasonRaw(CVideoInfoTag* infoTag, int sortSeason);
+ static void setEpisodeGuideRaw(CVideoInfoTag* infoTag, const String& episodeGuide);
+ static void setTop250Raw(CVideoInfoTag* infoTag, int top250);
+ static void setSetIdRaw(CVideoInfoTag* infoTag, int setId);
+ static void setTrackNumberRaw(CVideoInfoTag* infoTag, int trackNumber);
+ static void setRatingRaw(CVideoInfoTag* infoTag,
+ float rating,
+ int votes = 0,
+ const std::string& type = "",
+ bool isDefault = false);
+ static void setRatingsRaw(CVideoInfoTag* infoTag,
+ const std::map<String, Tuple<float, int>>& ratings,
+ const String& defaultRating = "");
+ static void setUserRatingRaw(CVideoInfoTag* infoTag, int userRating);
+ static void setPlaycountRaw(CVideoInfoTag* infoTag, int playcount);
+ static void setMpaaRaw(CVideoInfoTag* infoTag, const String& mpaa);
+ static void setPlotRaw(CVideoInfoTag* infoTag, const String& plot);
+ static void setPlotOutlineRaw(CVideoInfoTag* infoTag, const String& plotOutline);
+ static void setTitleRaw(CVideoInfoTag* infoTag, const String& title);
+ static void setOriginalTitleRaw(CVideoInfoTag* infoTag, const String& originalTitle);
+ static void setSortTitleRaw(CVideoInfoTag* infoTag, const String& sortTitle);
+ static void setTagLineRaw(CVideoInfoTag* infoTag, const String& tagLine);
+ static void setTvShowTitleRaw(CVideoInfoTag* infoTag, const String& tvshowTitle);
+ static void setTvShowStatusRaw(CVideoInfoTag* infoTag, const String& tvshowStatus);
+ static void setGenresRaw(CVideoInfoTag* infoTag, std::vector<String> genre);
+ static void setCountriesRaw(CVideoInfoTag* infoTag, std::vector<String> countries);
+ static void setDirectorsRaw(CVideoInfoTag* infoTag, std::vector<String> directors);
+ static void setStudiosRaw(CVideoInfoTag* infoTag, std::vector<String> studios);
+ static void setWritersRaw(CVideoInfoTag* infoTag, std::vector<String> writers);
+ static void setDurationRaw(CVideoInfoTag* infoTag, int duration);
+ static void setPremieredRaw(CVideoInfoTag* infoTag, const String& premiered);
+ static void setSetRaw(CVideoInfoTag* infoTag, const String& set);
+ static void setSetOverviewRaw(CVideoInfoTag* infoTag, const String& setOverview);
+ static void setTagsRaw(CVideoInfoTag* infoTag, std::vector<String> tags);
+ static void setProductionCodeRaw(CVideoInfoTag* infoTag, const String& productionCode);
+ static void setFirstAiredRaw(CVideoInfoTag* infoTag, const String& firstAired);
+ static void setLastPlayedRaw(CVideoInfoTag* infoTag, const String& lastPlayed);
+ static void setAlbumRaw(CVideoInfoTag* infoTag, const String& album);
+ static void setVotesRaw(CVideoInfoTag* infoTag, int votes);
+ static void setTrailerRaw(CVideoInfoTag* infoTag, const String& trailer);
+ static void setPathRaw(CVideoInfoTag* infoTag, const String& path);
+ static void setFilenameAndPathRaw(CVideoInfoTag* infoTag, const String& filenameAndPath);
+ static void setIMDBNumberRaw(CVideoInfoTag* infoTag, const String& imdbNumber);
+ static void setDateAddedRaw(CVideoInfoTag* infoTag, const String& dateAdded);
+ static void setMediaTypeRaw(CVideoInfoTag* infoTag, const String& mediaType);
+ static void setShowLinksRaw(CVideoInfoTag* infoTag, std::vector<String> showLinks);
+ static void setArtistsRaw(CVideoInfoTag* infoTag, std::vector<String> artists);
+ static void setCastRaw(CVideoInfoTag* infoTag, std::vector<SActorInfo> cast);
+ static void setResumePointRaw(CVideoInfoTag* infoTag, double time, double totalTime = 0.0);
+
+ static void addSeasonRaw(CVideoInfoTag* infoTag, int number, std::string name = "");
+ static void addSeasonsRaw(CVideoInfoTag* infoTag,
+ const std::vector<Tuple<int, std::string>>& namedSeasons);
+
+ static void addStreamRaw(CVideoInfoTag* infoTag, CStreamDetail* stream);
+ static void finalizeStreamsRaw(CVideoInfoTag* infoTag);
+
+ static void addAvailableArtworkRaw(CVideoInfoTag* infoTag,
+ const std::string& url,
+ const std::string& art_type = "",
+ const std::string& preview = "",
+ const std::string& referrer = "",
+ const std::string& cache = "",
+ bool post = false,
+ bool isgz = false,
+ int season = -1);
+#endif
+ };
+ }
+}
diff --git a/xbmc/interfaces/legacy/Keyboard.cpp b/xbmc/interfaces/legacy/Keyboard.cpp
new file mode 100644
index 0000000..2b7e08c
--- /dev/null
+++ b/xbmc/interfaces/legacy/Keyboard.cpp
@@ -0,0 +1,66 @@
+/*
+ * 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 "Keyboard.h"
+
+#include "LanguageHook.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/Variant.h"
+
+using namespace KODI::MESSAGING;
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+
+ Keyboard::Keyboard(const String& line /* = nullString*/, const String& heading/* = nullString*/, bool hidden/* = false*/)
+ : strDefault(line), strHeading(heading), bHidden(hidden)
+ {
+ }
+
+ Keyboard::~Keyboard() = default;
+
+ void Keyboard::doModal(int autoclose)
+ {
+ DelayedCallGuard dg(languageHook);
+ // using keyboardfactory method to get native keyboard if there is.
+ strText = strDefault;
+ std::string text(strDefault);
+ bConfirmed = CGUIKeyboardFactory::ShowAndGetInput(text, CVariant{strHeading}, true, bHidden, autoclose * 1000);
+ strText = text;
+ }
+
+ void Keyboard::setDefault(const String& line)
+ {
+ strDefault = line;
+ }
+
+ void Keyboard::setHiddenInput(bool hidden)
+ {
+ bHidden = hidden;
+ }
+
+ void Keyboard::setHeading(const String& heading)
+ {
+ strHeading = heading;
+ }
+
+ String Keyboard::getText()
+ {
+ return strText;
+ }
+
+ bool Keyboard::isConfirmed()
+ {
+ return bConfirmed;
+ }
+ }
+}
+
diff --git a/xbmc/interfaces/legacy/Keyboard.h b/xbmc/interfaces/legacy/Keyboard.h
new file mode 100644
index 0000000..94f6421
--- /dev/null
+++ b/xbmc/interfaces/legacy/Keyboard.h
@@ -0,0 +1,215 @@
+/*
+ * 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 "AddonClass.h"
+#include "AddonString.h"
+#include "Exception.h"
+
+class CGUIDialogKeyboardGeneric;
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ XBMCCOMMONS_STANDARD_EXCEPTION(KeyboardException);
+
+ ///
+ /// \defgroup python_keyboard Keyboard
+ /// \ingroup python_xbmc
+ /// @{
+ /// @brief **Kodi's keyboard class.**
+ ///
+ /// \python_class{ xbmc.Keyboard([default, heading, hidden]) }
+ ///
+ /// Creates a new Keyboard object with default text
+ /// heading and hidden input flag if supplied.
+ ///
+ /// @param default : [opt] string - default text entry.
+ /// @param heading : [opt] string - keyboard heading.
+ /// @param hidden : [opt] boolean - True for hidden text entry.
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// kb = xbmc.Keyboard('default', 'heading', True)
+ /// kb.setDefault('password') # optional
+ /// kb.setHeading('Enter password') # optional
+ /// kb.setHiddenInput(True) # optional
+ /// kb.doModal()
+ /// if (kb.isConfirmed()):
+ /// text = kb.getText()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ class Keyboard : public AddonClass
+ {
+ public:
+#ifndef SWIG
+ String strDefault;
+ String strHeading;
+ bool bHidden;
+ String strText;
+ bool bConfirmed = false;
+#endif
+
+ Keyboard(const String& line = emptyString, const String& heading = emptyString, bool hidden = false);
+ ~Keyboard() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_keyboard
+ /// @brief \python_func{ doModal([autoclose]) }
+ /// Show keyboard and wait for user action.
+ ///
+ /// @param autoclose [opt] integer - milliseconds to autoclose
+ /// dialog. (default=do not autoclose)
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// kb.doModal(30000)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ doModal(...);
+#else
+ void doModal(int autoclose = 0);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ // setDefault() Method
+ ///
+ /// \ingroup python_keyboard
+ /// @brief \python_func{ setDefault(line) }
+ /// Set the default text entry.
+ ///
+ /// @param line string - default text entry.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// kb.setDefault('password')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setDefault(...);
+#else
+ void setDefault(const String& line = emptyString);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_keyboard
+ /// @brief \python_func{ setHiddenInput(hidden) }
+ /// Allows hidden text entry.
+ ///
+ /// @param hidden boolean - True for hidden text entry.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// kb.setHiddenInput(True)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setHiddenInput(...);
+#else
+ void setHiddenInput(bool hidden = false);
+#endif
+
+ // setHeading() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_keyboard
+ /// @brief \python_func{ setHeading(heading) }
+ /// Set the keyboard heading.
+ ///
+ /// @param heading string - keyboard heading.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// kb.setHeading('Enter password')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setHeading(...);
+#else
+ void setHeading(const String& heading);
+#endif
+
+ // getText() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_keyboard
+ /// @brief \python_func{ getText() }
+ /// Returns the user input as a string.
+ ///
+ /// @note This will always return the text entry even if you cancel the keyboard.
+ /// Use the isConfirmed() method to check if user cancelled the keyboard.
+ ///
+ /// @return get the in keyboard entered text
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// text = kb.getText()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getText();
+#else
+ String getText();
+#endif
+
+ // isConfirmed() Method
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_keyboard
+ /// @brief \python_func{ isConfirmed() }
+ /// Returns False if the user cancelled the input.
+ ///
+ /// @return true if confirmed, if cancelled false
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// if (kb.isConfirmed()):
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ isConfirmed();
+#else
+ bool isConfirmed();
+#endif
+ };
+ //@}
+ }
+}
diff --git a/xbmc/interfaces/legacy/LanguageHook.cpp b/xbmc/interfaces/legacy/LanguageHook.cpp
new file mode 100644
index 0000000..c3b70c6
--- /dev/null
+++ b/xbmc/interfaces/legacy/LanguageHook.cpp
@@ -0,0 +1,41 @@
+/*
+ * 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 "LanguageHook.h"
+
+#include "utils/GlobalsHandling.h"
+
+namespace XBMCAddon
+{
+ // just need a place for the vtab
+ LanguageHook::~LanguageHook() = default;
+
+ static thread_local LanguageHook* addonLanguageHookTls;
+ static bool threadLocalInitialized = false;
+ static xbmcutil::InitFlag initer(threadLocalInitialized);
+
+ void LanguageHook::SetLanguageHook(LanguageHook* languageHook)
+ {
+ XBMC_TRACE;
+ languageHook->Acquire();
+ addonLanguageHookTls = languageHook;
+ }
+
+ LanguageHook* LanguageHook::GetLanguageHook()
+ {
+ return threadLocalInitialized ? addonLanguageHookTls : NULL;
+ }
+
+ void LanguageHook::ClearLanguageHook()
+ {
+ LanguageHook* lh = addonLanguageHookTls;
+ addonLanguageHookTls = NULL;
+ if (lh)
+ lh->Release();
+ }
+}
diff --git a/xbmc/interfaces/legacy/LanguageHook.h b/xbmc/interfaces/legacy/LanguageHook.h
new file mode 100644
index 0000000..86abdac
--- /dev/null
+++ b/xbmc/interfaces/legacy/LanguageHook.h
@@ -0,0 +1,151 @@
+/*
+ * 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 "AddonClass.h"
+#include "CallbackHandler.h"
+#include "threads/Event.h"
+
+/**
+ * This class is an interface that can be used to define programming language
+ * specific hooks.
+ */
+
+class IPlayerCallback;
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ class Monitor;
+ }
+
+ class LanguageHook : public AddonClass
+ {
+ protected:
+ inline LanguageHook() = default;
+
+ public:
+ ~LanguageHook() override;
+
+ /**
+ * If the scripting language needs special handling for calls
+ * that block or are delayed in any other means, this should
+ * be overloaded.
+ *
+ * In Python when control is passed to a native
+ * method that blocks, you need to allow other threads in
+ * Python to run by using Py_BEGIN_ALLOW_THREADS. This is
+ * the place to put that functionality
+ */
+ virtual void DelayedCallOpen() { }
+
+ /**
+ * If the scripting language needs special handling for calls
+ * that block or are delayed in any other means, this should
+ * be overloaded.
+ *
+ * In Python when control is passed to a native
+ * method that blocks, you need to allow other threads in
+ * Python to run by using Py_BEGIN_ALLOW_THREADS. When that
+ * delayed method ends you need to restore the Python thread
+ * state using Py_END_ALLOW_THREADS. This is the place to put
+ * that functionality
+ */
+ virtual void DelayedCallClose() { }
+
+ virtual void MakePendingCalls() {}
+
+ /**
+ * For scripting languages that need a global callback handler, this
+ * method should be overloaded to supply one.
+ */
+ virtual CallbackHandler* GetCallbackHandler() { return NULL; }
+
+ /**
+ * This is a callback method that can be overridden to receive a callback
+ * when an AddonClass that has this language hook is on is being constructed.
+ * It is called from the constructor of AddonClass so the implementor
+ * cannot assume the subclasses have been built or that calling a
+ * virtual function on the AddonClass will work as expected.
+ */
+ virtual void Constructing(AddonClass* beingConstructed) { }
+
+ /**
+ * This is a callback method that can be overridden to receive a callback
+ * when an AddonClass that has this language hook is on is being destructed.
+ * It is called from the destructor of AddonClass so the implementor
+ * should assume the subclasses have been torn down and that calling a
+ * virtual function on the AddonClass will not work as expected.
+ */
+ virtual void Destructing(AddonClass* beingDestructed) { }
+
+ /**
+ * This method should be done a different way but since the only other way
+ * I can think to do it requires an InheritableThreadLocal C++ equivalent,
+ * I'm going to defer to this technique for now.
+ *
+ * Currently (for python) the scripting language has the Addon id injected
+ * into it as a global variable. Therefore the only way to retrieve it is
+ * to use scripting language specific calls. So until I figure out a
+ * better way to do this, this is how I need to retrieve it.
+ */
+ virtual String GetAddonId() { return emptyString; }
+ virtual String GetAddonVersion() { return emptyString; }
+ virtual long GetInvokerId() { return -1; }
+
+ virtual void RegisterPlayerCallback(IPlayerCallback* player) = 0;
+ virtual void UnregisterPlayerCallback(IPlayerCallback* player) = 0;
+ virtual void RegisterMonitorCallback(XBMCAddon::xbmc::Monitor* player) = 0;
+ virtual void UnregisterMonitorCallback(XBMCAddon::xbmc::Monitor* player) = 0;
+ virtual bool WaitForEvent(CEvent& hEvent, unsigned int milliseconds) = 0;
+
+ static void SetLanguageHook(LanguageHook* languageHook);
+ static LanguageHook* GetLanguageHook();
+ static void ClearLanguageHook();
+ };
+
+ /**
+ * This class can be used to access the language hook's DelayedCallOpen
+ * and DelayedCallClose. It should be used whenever an API method
+ * is written such that it can block for an indefinite amount of time
+ * since certain scripting languages (like Python) need to do extra
+ * work for delayed calls (like free the python locks and handle
+ * callbacks).
+ */
+ class DelayedCallGuard
+ {
+ LanguageHook* languageHook;
+ bool clearOnExit = false;
+
+ public:
+ inline explicit DelayedCallGuard(LanguageHook* languageHook_) : languageHook(languageHook_)
+ { if (languageHook) languageHook->DelayedCallOpen(); }
+
+ inline DelayedCallGuard() : languageHook(LanguageHook::GetLanguageHook())
+ { if (languageHook) languageHook->DelayedCallOpen(); }
+
+ inline ~DelayedCallGuard()
+ {
+ if (clearOnExit) LanguageHook::ClearLanguageHook();
+ if (languageHook) languageHook->DelayedCallClose();
+ }
+
+ inline LanguageHook* getLanguageHook() { return languageHook; }
+ };
+
+ class SetLanguageHookGuard
+ {
+ public:
+ inline explicit SetLanguageHookGuard(LanguageHook* languageHook) { LanguageHook::SetLanguageHook(languageHook); }
+ inline ~SetLanguageHookGuard() { LanguageHook::ClearLanguageHook(); }
+ };
+
+}
+
diff --git a/xbmc/interfaces/legacy/List.h b/xbmc/interfaces/legacy/List.h
new file mode 100644
index 0000000..932fa4e
--- /dev/null
+++ b/xbmc/interfaces/legacy/List.h
@@ -0,0 +1,20 @@
+/*
+ * 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 <list>
+
+namespace XBMCAddon
+{
+ template <class T> class List : public std::list<T>
+ {
+ public:
+ static List<T> nullList;
+ };
+}
diff --git a/xbmc/interfaces/legacy/ListItem.cpp b/xbmc/interfaces/legacy/ListItem.cpp
new file mode 100644
index 0000000..64410ad
--- /dev/null
+++ b/xbmc/interfaces/legacy/ListItem.cpp
@@ -0,0 +1,1056 @@
+/*
+ * 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 "ListItem.h"
+
+#include "AddonUtils.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "games/GameTypes.h"
+#include "games/tags/GameInfoTag.h"
+#include "music/tags/MusicInfoTag.h"
+#include "pictures/PictureInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <cstdlib>
+#include <sstream>
+#include <utility>
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ ListItem::ListItem(const String& label,
+ const String& label2,
+ const String& path,
+ bool offscreen) :
+ m_offscreen(offscreen)
+ {
+ item.reset();
+
+ // create CFileItem
+ item.reset(new CFileItem());
+ if (!item) // not sure if this is really possible
+ return;
+
+ if (!label.empty())
+ item->SetLabel( label );
+ if (!label2.empty())
+ item->SetLabel2( label2 );
+ if (!path.empty())
+ item->SetPath(path);
+ }
+
+ ListItem::~ListItem()
+ {
+ item.reset();
+ }
+
+ String ListItem::getLabel()
+ {
+ if (!item) return "";
+
+ String ret;
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ ret = item->GetLabel();
+ }
+
+ return ret;
+ }
+
+ String ListItem::getLabel2()
+ {
+ if (!item) return "";
+
+ String ret;
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ ret = item->GetLabel2();
+ }
+
+ return ret;
+ }
+
+ void ListItem::setLabel(const String& label)
+ {
+ if (!item) return;
+ // set label
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ item->SetLabel(label);
+ }
+ }
+
+ void ListItem::setLabel2(const String& label)
+ {
+ if (!item) return;
+ // set label
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ item->SetLabel2(label);
+ }
+ }
+
+ String ListItem::getDateTime()
+ {
+ if (!item)
+ return "";
+
+ String ret;
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ if (item->m_dateTime.IsValid())
+ ret = item->m_dateTime.GetAsW3CDateTime();
+ }
+
+ return ret;
+ }
+
+ void ListItem::setDateTime(const String& dateTime)
+ {
+ if (!item)
+ return;
+ // set datetime
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ setDateTimeRaw(dateTime);
+ }
+ }
+
+ void ListItem::setArt(const Properties& dictionary)
+ {
+ if (!item) return;
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ for (const auto& it : dictionary)
+ addArtRaw(it.first, it.second);
+ }
+ }
+
+ void ListItem::setIsFolder(bool isFolder)
+ {
+ if (!item)
+ return;
+
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ setIsFolderRaw(isFolder);
+ }
+ }
+
+ void ListItem::setUniqueIDs(const Properties& dictionary, const String& defaultrating /* = "" */)
+ {
+ CLog::Log(
+ LOGWARNING,
+ "ListItem.setUniqueIDs() is deprecated and might be removed in future Kodi versions. "
+ "Please use InfoTagVideo.setUniqueIDs().");
+
+ if (!item)
+ return;
+
+ std::map<String, String> uniqueIDs;
+ for (const auto& it : dictionary)
+ uniqueIDs.emplace(it.first, it.second);
+
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ xbmc::InfoTagVideo::setUniqueIDsRaw(GetVideoInfoTag(), uniqueIDs, defaultrating);
+ }
+ }
+
+ void ListItem::setRating(const std::string& type,
+ float rating,
+ int votes /* = 0 */,
+ bool defaultt /* = false */)
+ {
+ CLog::Log(LOGWARNING,
+ "ListItem.setRating() is deprecated and might be removed in future Kodi versions. "
+ "Please use InfoTagVideo.setRating().");
+
+ if (!item) return;
+
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ xbmc::InfoTagVideo::setRatingRaw(GetVideoInfoTag(), rating, votes, type, defaultt);
+ }
+
+ void ListItem::addSeason(int number, std::string name /* = "" */)
+ {
+ CLog::Log(LOGWARNING,
+ "ListItem.addSeason() is deprecated and might be removed in future Kodi versions. "
+ "Please use InfoTagVideo.addSeason().");
+
+ if (!item) return;
+
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ xbmc::InfoTagVideo::addSeasonRaw(GetVideoInfoTag(), number, std::move(name));
+ }
+
+ void ListItem::select(bool selected)
+ {
+ if (!item) return;
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ item->Select(selected);
+ }
+ }
+
+
+ bool ListItem::isSelected()
+ {
+ if (!item) return false;
+
+ bool ret;
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ ret = item->IsSelected();
+ }
+
+ return ret;
+ }
+
+ void ListItem::setProperty(const char * key, const String& value)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ String lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+ if (lowerKey == "startoffset")
+ { // special case for start offset - don't actually store in a property
+ setStartOffsetRaw(strtod(value.c_str(), nullptr));
+ }
+ else if (lowerKey == "mimetype")
+ { // special case for mime type - don't actually stored in a property,
+ item->SetMimeType(value);
+ }
+ else if (lowerKey == "totaltime")
+ {
+ CLog::Log(LOGWARNING,
+ "\"{}\" in ListItem.setProperty() is deprecated and might be removed in future "
+ "Kodi versions. Please use InfoTagVideo.setResumePoint().",
+ lowerKey);
+
+ CBookmark resumePoint(GetVideoInfoTag()->GetResumePoint());
+ resumePoint.totalTimeInSeconds = atof(value.c_str());
+ GetVideoInfoTag()->SetResumePoint(resumePoint);
+ }
+ else if (lowerKey == "resumetime")
+ {
+ CLog::Log(LOGWARNING,
+ "\"{}\" in ListItem.setProperty() is deprecated and might be removed in future "
+ "Kodi versions. Please use InfoTagVideo.setResumePoint().",
+ lowerKey);
+
+ xbmc::InfoTagVideo::setResumePointRaw(GetVideoInfoTag(), atof(value.c_str()));
+ }
+ else if (lowerKey == "specialsort")
+ setSpecialSortRaw(value);
+ else if (lowerKey == "fanart_image")
+ item->SetArt("fanart", value);
+ else
+ addPropertyRaw(lowerKey, value);
+ }
+
+ void ListItem::setProperties(const Properties& dictionary)
+ {
+ for (const auto& it : dictionary)
+ setProperty(it.first.c_str(), it.second);
+ }
+
+ String ListItem::getProperty(const char* key)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ String lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+ std::string value;
+ if (lowerKey == "startoffset")
+ { // special case for start offset - don't actually store in a property,
+ // we store it in item.m_lStartOffset instead
+ value = StringUtils::Format("{:f}", CUtil::ConvertMilliSecsToSecs(item->GetStartOffset()));
+ }
+ else if (lowerKey == "totaltime")
+ {
+ CLog::Log(LOGWARNING,
+ "\"{}\" in ListItem.getProperty() is deprecated and might be removed in future "
+ "Kodi versions. Please use InfoTagVideo.getResumeTimeTotal().",
+ lowerKey);
+
+ value = StringUtils::Format("{:f}", GetVideoInfoTag()->GetResumePoint().totalTimeInSeconds);
+ }
+ else if (lowerKey == "resumetime")
+ {
+ CLog::Log(LOGWARNING,
+ "\"{}\" in ListItem.getProperty() is deprecated and might be removed in future "
+ "Kodi versions. Please use InfoTagVideo.getResumeTime().",
+ lowerKey);
+
+ value = StringUtils::Format("{:f}", GetVideoInfoTag()->GetResumePoint().timeInSeconds);
+ }
+ else if (lowerKey == "fanart_image")
+ value = item->GetArt("fanart");
+ else
+ value = item->GetProperty(lowerKey).asString();
+
+ return value;
+ }
+
+ String ListItem::getArt(const char* key)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ return item->GetArt(key);
+ }
+
+ bool ListItem::isFolder() const
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ return item->m_bIsFolder;
+ }
+
+ String ListItem::getUniqueID(const char* key)
+ {
+ CLog::Log(
+ LOGWARNING,
+ "ListItem.getUniqueID() is deprecated and might be removed in future Kodi versions. "
+ "Please use InfoTagVideo.getUniqueID().");
+
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ return GetVideoInfoTag()->GetUniqueID(key);
+ }
+
+ float ListItem::getRating(const char* key)
+ {
+ CLog::Log(LOGWARNING,
+ "ListItem.getRating() is deprecated and might be removed in future Kodi versions. "
+ "Please use InfoTagVideo.getRating().");
+
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ return GetVideoInfoTag()->GetRating(key).rating;
+ }
+
+ int ListItem::getVotes(const char* key)
+ {
+ CLog::Log(LOGWARNING,
+ "ListItem.getVotes() is deprecated and might be removed in future Kodi versions. "
+ "Please use InfoTagVideo.getVotesAsInt().");
+
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ return GetVideoInfoTag()->GetRating(key).votes;
+ }
+
+ void ListItem::setPath(const String& path)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ setPathRaw(path);
+ }
+
+ void ListItem::setMimeType(const String& mimetype)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ setMimeTypeRaw(mimetype);
+ }
+
+ void ListItem::setContentLookup(bool enable)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ setContentLookupRaw(enable);
+ }
+
+ String ListItem::getPath()
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ return item->GetPath();
+ }
+
+ void ListItem::setInfo(const char* type, const InfoLabelDict& infoLabels)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+
+ bool hasDeprecatedInfoLabel = false;
+ if (StringUtils::CompareNoCase(type, "video") == 0)
+ {
+ using InfoTagVideo = xbmc::InfoTagVideo;
+ auto videotag = GetVideoInfoTag();
+ for (const auto& it : infoLabels)
+ {
+ const auto key = StringUtils::ToLower(it.first);
+ const InfoLabelValue& alt = it.second;
+ const String value(alt.which() == first ? alt.former() : emptyString);
+
+ if (key == "count")
+ setCountRaw(strtol(value.c_str(), nullptr, 10));
+ else if (key == "size")
+ setSizeRaw(static_cast<int64_t>(strtoll(value.c_str(), nullptr, 10)));
+ else if (key == "overlay")
+ {
+ long overlay = strtol(value.c_str(), nullptr, 10);
+ if (overlay >= 0 && overlay <= 8)
+ item->SetOverlayImage(static_cast<CGUIListItem::GUIIconOverlay>(overlay));
+ }
+ else if (key == "date")
+ setDateTimeRaw(value);
+ else
+ {
+ hasDeprecatedInfoLabel = true;
+
+ if (key == "dbid")
+ InfoTagVideo::setDbIdRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "year")
+ InfoTagVideo::setYearRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "episode")
+ InfoTagVideo::setEpisodeRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "season")
+ InfoTagVideo::setSeasonRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "sortepisode")
+ InfoTagVideo::setSortEpisodeRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "sortseason")
+ InfoTagVideo::setSortSeasonRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "episodeguide")
+ InfoTagVideo::setEpisodeGuideRaw(videotag, value);
+ else if (key == "showlink")
+ InfoTagVideo::setShowLinksRaw(videotag, getVideoStringArray(alt, key, value));
+ else if (key == "top250")
+ InfoTagVideo::setTop250Raw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "setid")
+ InfoTagVideo::setSetIdRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "tracknumber")
+ InfoTagVideo::setTrackNumberRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "rating")
+ InfoTagVideo::setRatingRaw(videotag,
+ static_cast<float>(strtod(value.c_str(), nullptr)));
+ else if (key == "userrating")
+ InfoTagVideo::setUserRatingRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "watched") // backward compat - do we need it?
+ InfoTagVideo::setPlaycountRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "playcount")
+ InfoTagVideo::setPlaycountRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "cast" || key == "castandrole")
+ {
+ if (alt.which() != second)
+ throw WrongTypeException("When using \"cast\" or \"castandrole\" you need to "
+ "supply a list of tuples for the value in the dictionary");
+
+ std::vector<SActorInfo> cast;
+ cast.reserve(alt.later().size());
+ for (const auto& castEntry : alt.later())
+ {
+ // castEntry can be a string meaning it's the actor or it can be a tuple meaning it's the
+ // actor and the role.
+ const String& actor =
+ castEntry.which() == first ? castEntry.former() : castEntry.later().first();
+ SActorInfo info;
+ info.strName = actor;
+ if (castEntry.which() == second)
+ info.strRole = static_cast<const String&>(castEntry.later().second());
+ cast.push_back(std::move(info));
+ }
+ InfoTagVideo::setCastRaw(videotag, std::move(cast));
+ }
+ else if (key == "artist")
+ {
+ if (alt.which() != second)
+ throw WrongTypeException("When using \"artist\" you need to supply a list of "
+ "strings for the value in the dictionary");
+
+ std::vector<String> artists;
+ artists.reserve(alt.later().size());
+ for (const auto& castEntry : alt.later())
+ {
+ auto actor =
+ castEntry.which() == first ? castEntry.former() : castEntry.later().first();
+ artists.push_back(std::move(actor));
+ }
+ InfoTagVideo::setArtistsRaw(videotag, artists);
+ }
+ else if (key == "genre")
+ InfoTagVideo::setGenresRaw(videotag, getVideoStringArray(alt, key, value));
+ else if (key == "country")
+ InfoTagVideo::setCountriesRaw(videotag, getVideoStringArray(alt, key, value));
+ else if (key == "director")
+ InfoTagVideo::setDirectorsRaw(videotag, getVideoStringArray(alt, key, value));
+ else if (key == "mpaa")
+ InfoTagVideo::setMpaaRaw(videotag, value);
+ else if (key == "plot")
+ InfoTagVideo::setPlotRaw(videotag, value);
+ else if (key == "plotoutline")
+ InfoTagVideo::setPlotOutlineRaw(videotag, value);
+ else if (key == "title")
+ InfoTagVideo::setTitleRaw(videotag, value);
+ else if (key == "originaltitle")
+ InfoTagVideo::setOriginalTitleRaw(videotag, value);
+ else if (key == "sorttitle")
+ InfoTagVideo::setSortTitleRaw(videotag, value);
+ else if (key == "duration")
+ InfoTagVideo::setDurationRaw(videotag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "studio")
+ InfoTagVideo::setStudiosRaw(videotag, getVideoStringArray(alt, key, value));
+ else if (key == "tagline")
+ InfoTagVideo::setTagLineRaw(videotag, value);
+ else if (key == "writer" || key == "credits")
+ InfoTagVideo::setWritersRaw(videotag, getVideoStringArray(alt, key, value));
+ else if (key == "tvshowtitle")
+ InfoTagVideo::setTvShowTitleRaw(videotag, value);
+ else if (key == "premiered")
+ InfoTagVideo::setPremieredRaw(videotag, value);
+ else if (key == "status")
+ InfoTagVideo::setTvShowStatusRaw(videotag, value);
+ else if (key == "set")
+ InfoTagVideo::setSetRaw(videotag, value);
+ else if (key == "setoverview")
+ InfoTagVideo::setSetOverviewRaw(videotag, value);
+ else if (key == "tag")
+ InfoTagVideo::setTagsRaw(videotag, getVideoStringArray(alt, key, value));
+ else if (key == "imdbnumber")
+ InfoTagVideo::setIMDBNumberRaw(videotag, value);
+ else if (key == "code")
+ InfoTagVideo::setProductionCodeRaw(videotag, value);
+ else if (key == "aired")
+ InfoTagVideo::setFirstAiredRaw(videotag, value);
+ else if (key == "lastplayed")
+ InfoTagVideo::setLastPlayedRaw(videotag, value);
+ else if (key == "album")
+ InfoTagVideo::setAlbumRaw(videotag, value);
+ else if (key == "votes")
+ InfoTagVideo::setVotesRaw(videotag, StringUtils::ReturnDigits(value));
+ else if (key == "trailer")
+ InfoTagVideo::setTrailerRaw(videotag, value);
+ else if (key == "path")
+ InfoTagVideo::setPathRaw(videotag, value);
+ else if (key == "filenameandpath")
+ InfoTagVideo::setFilenameAndPathRaw(videotag, value);
+ else if (key == "dateadded")
+ InfoTagVideo::setDateAddedRaw(videotag, value);
+ else if (key == "mediatype")
+ InfoTagVideo::setMediaTypeRaw(videotag, value);
+ else
+ CLog::Log(LOGERROR, "NEWADDON Unknown Video Info Key \"{}\"", key);
+ }
+ }
+
+ if (hasDeprecatedInfoLabel)
+ {
+ CLog::Log(
+ LOGWARNING,
+ "Setting most video properties through ListItem.setInfo() is deprecated and might be "
+ "removed in future Kodi versions. Please use the respective setter in InfoTagVideo.");
+ }
+ }
+ else if (StringUtils::CompareNoCase(type, "music") == 0)
+ {
+ String mediaType;
+ int dbId = -1;
+
+ using InfoTagMusic = xbmc::InfoTagMusic;
+ auto musictag = GetMusicInfoTag();
+ for (const auto& it : infoLabels)
+ {
+ const auto key = StringUtils::ToLower(it.first);
+ const auto& alt = it.second;
+ const String value(alt.which() == first ? alt.former() : emptyString);
+
+ //! @todo add the rest of the infolabels
+ if (key == "count")
+ setCountRaw(strtol(value.c_str(), nullptr, 10));
+ else if (key == "size")
+ setSizeRaw(static_cast<int64_t>(strtoll(value.c_str(), nullptr, 10)));
+ else if (key == "date")
+ setDateTimeRaw(value);
+ else
+ {
+ hasDeprecatedInfoLabel = true;
+
+ if (key == "dbid")
+ dbId = static_cast<int>(strtol(value.c_str(), NULL, 10));
+ else if (key == "mediatype")
+ mediaType = value;
+ else if (key == "tracknumber")
+ InfoTagMusic::setTrackRaw(musictag, strtol(value.c_str(), NULL, 10));
+ else if (key == "discnumber")
+ InfoTagMusic::setDiscRaw(musictag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "duration")
+ InfoTagMusic::setDurationRaw(musictag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "year")
+ InfoTagMusic::setYearRaw(musictag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "listeners")
+ InfoTagMusic::setListenersRaw(musictag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "playcount")
+ InfoTagMusic::setPlayCountRaw(musictag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "genre")
+ InfoTagMusic::setGenresRaw(musictag, getMusicStringArray(alt, key, value));
+ else if (key == "album")
+ InfoTagMusic::setAlbumRaw(musictag, value);
+ else if (key == "artist")
+ InfoTagMusic::setArtistRaw(musictag, value);
+ else if (key == "title")
+ InfoTagMusic::setTitleRaw(musictag, value);
+ else if (key == "rating")
+ InfoTagMusic::setRatingRaw(musictag,
+ static_cast<float>(strtod(value.c_str(), nullptr)));
+ else if (key == "userrating")
+ InfoTagMusic::setUserRatingRaw(musictag, strtol(value.c_str(), nullptr, 10));
+ else if (key == "lyrics")
+ InfoTagMusic::setLyricsRaw(musictag, value);
+ else if (key == "lastplayed")
+ InfoTagMusic::setLastPlayedRaw(musictag, value);
+ else if (key == "musicbrainztrackid")
+ InfoTagMusic::setMusicBrainzTrackIDRaw(musictag, value);
+ else if (key == "musicbrainzartistid")
+ InfoTagMusic::setMusicBrainzArtistIDRaw(musictag,
+ getMusicStringArray(alt, key, value));
+ else if (key == "musicbrainzalbumid")
+ InfoTagMusic::setMusicBrainzAlbumIDRaw(musictag, value);
+ else if (key == "musicbrainzalbumartistid")
+ InfoTagMusic::setMusicBrainzAlbumArtistIDRaw(musictag,
+ getMusicStringArray(alt, key, value));
+ else if (key == "comment")
+ InfoTagMusic::setCommentRaw(musictag, value);
+ else
+ CLog::Log(LOGERROR, "NEWADDON Unknown Music Info Key \"{}\"", key);
+ }
+
+ // This should probably be set outside of the loop but since the original
+ // implementation set it inside of the loop, I'll leave it that way. - Jim C.
+ musictag->SetLoaded(true);
+ }
+
+ if (dbId > 0 && !mediaType.empty())
+ InfoTagMusic::setDbIdRaw(musictag, dbId, mediaType);
+
+ if (hasDeprecatedInfoLabel)
+ {
+ CLog::Log(
+ LOGWARNING,
+ "Setting most music properties through ListItem.setInfo() is deprecated and might be "
+ "removed in future Kodi versions. Please use the respective setter in InfoTagMusic.");
+ }
+ }
+ else if (StringUtils::CompareNoCase(type, "pictures") == 0)
+ {
+ for (const auto& it : infoLabels)
+ {
+ const auto key = StringUtils::ToLower(it.first);
+ const auto& alt = it.second;
+ const String value(alt.which() == first ? alt.former() : emptyString);
+
+ if (key == "count")
+ setCountRaw(strtol(value.c_str(), nullptr, 10));
+ else if (key == "size")
+ setSizeRaw(static_cast<int64_t>(strtoll(value.c_str(), nullptr, 10)));
+ else if (key == "title")
+ setTitleRaw(value);
+ else if (key == "picturepath")
+ setPathRaw(value);
+ else if (key == "date")
+ setDateTimeRaw(value);
+ else
+ {
+ hasDeprecatedInfoLabel = true;
+
+ String exifkey = key;
+ if (!StringUtils::StartsWithNoCase(exifkey, "exif:") || exifkey.length() < 6)
+ {
+ CLog::Log(LOGWARNING, "ListItem.setInfo: unknown pictures info key \"{}\"", key);
+ continue;
+ }
+
+ exifkey = StringUtils::Mid(exifkey, 5);
+ if (exifkey == "resolution")
+ xbmc::InfoTagPicture::setResolutionRaw(item->GetPictureInfoTag(), value);
+ else if (exifkey == "exiftime")
+ xbmc::InfoTagPicture::setDateTimeTakenRaw(item->GetPictureInfoTag(), value);
+ else
+ CLog::Log(LOGWARNING, "ListItem.setInfo: unknown pictures info key \"{}\"", key);
+ }
+ }
+
+ if (hasDeprecatedInfoLabel)
+ {
+ CLog::Log(LOGWARNING, "Setting most picture properties through ListItem.setInfo() is "
+ "deprecated and might be removed in future Kodi versions. Please "
+ "use the respective setter in InfoTagPicture.");
+ }
+ }
+ else if (StringUtils::EqualsNoCase(type, "game"))
+ {
+ auto gametag = item->GetGameInfoTag();
+ for (const auto& it : infoLabels)
+ {
+ const auto key = StringUtils::ToLower(it.first);
+ const auto& alt = it.second;
+ const String value(alt.which() == first ? alt.former() : emptyString);
+
+ if (key == "title")
+ {
+ setTitleRaw(value);
+ xbmc::InfoTagGame::setTitleRaw(gametag, value);
+ }
+ else if (key == "platform")
+ xbmc::InfoTagGame::setPlatformRaw(gametag, value);
+ else if (key == "genres")
+ xbmc::InfoTagGame::setGenresRaw(gametag, getStringArray(alt, key, value, ","));
+ else if (key == "publisher")
+ xbmc::InfoTagGame::setPublisherRaw(gametag, value);
+ else if (key == "developer")
+ xbmc::InfoTagGame::setDeveloperRaw(gametag, value);
+ else if (key == "overview")
+ xbmc::InfoTagGame::setOverviewRaw(gametag, value);
+ else if (key == "year")
+ xbmc::InfoTagGame::setYearRaw(gametag, strtoul(value.c_str(), nullptr, 10));
+ else if (key == "gameclient")
+ xbmc::InfoTagGame::setGameClientRaw(gametag, value);
+ }
+
+ if (!infoLabels.empty())
+ {
+ CLog::Log(
+ LOGWARNING,
+ "Setting game properties through ListItem.setInfo() is deprecated and might be "
+ "removed in future Kodi versions. Please use the respective setter in InfoTagGame.");
+ }
+ }
+ else
+ CLog::Log(LOGWARNING, "ListItem.setInfo: unknown \"type\" parameter value: {}", type);
+ } // end ListItem::setInfo
+
+ void ListItem::setCast(const std::vector<Properties>& actors)
+ {
+ CLog::Log(LOGWARNING,
+ "ListItem.setCast() is deprecated and might be removed in future Kodi versions. "
+ "Please use InfoTagVideo.setCast().");
+
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ std::vector<SActorInfo> cast;
+ cast.reserve(actors.size());
+ for (const auto& dictionary : actors)
+ {
+ SActorInfo info;
+ for (auto it = dictionary.begin(); it != dictionary.end(); ++it)
+ {
+ const String& key = it->first;
+ const String& value = it->second;
+ if (key == "name")
+ info.strName = value;
+ else if (key == "role")
+ info.strRole = value;
+ else if (key == "thumbnail")
+ {
+ info.thumbUrl = CScraperUrl(value);
+ if (!info.thumbUrl.GetFirstThumbUrl().empty())
+ info.thumb = CScraperUrl::GetThumbUrl(info.thumbUrl.GetFirstUrlByType());
+ }
+ else if (key == "order")
+ info.order = strtol(value.c_str(), nullptr, 10);
+ }
+ cast.push_back(std::move(info));
+ }
+ xbmc::InfoTagVideo::setCastRaw(GetVideoInfoTag(), std::move(cast));
+ }
+
+ void ListItem::setAvailableFanart(const std::vector<Properties>& images)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ auto infoTag = GetVideoInfoTag();
+ infoTag->m_fanart.Clear();
+ for (const auto& dictionary : images)
+ {
+ std::string image;
+ std::string preview;
+ std::string colors;
+ for (const auto& it : dictionary)
+ {
+ const String& key = it.first;
+ const String& value = it.second;
+ if (key == "image")
+ image = value;
+ else if (key == "preview")
+ preview = value;
+ else if (key == "colors")
+ colors = value;
+ }
+ infoTag->m_fanart.AddFanart(image, preview, colors);
+ }
+ infoTag->m_fanart.Pack();
+ }
+
+ void ListItem::addAvailableArtwork(const std::string& url,
+ const std::string& art_type,
+ const std::string& preview,
+ const std::string& referrer,
+ const std::string& cache,
+ bool post,
+ bool isgz,
+ int season)
+ {
+ CLog::Log(LOGWARNING, "ListItem.addAvailableArtwork() is deprecated and might be removed in "
+ "future Kodi versions. Please use InfoTagVideo.addAvailableArtwork().");
+
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ xbmc::InfoTagVideo::addAvailableArtworkRaw(GetVideoInfoTag(), url, art_type, preview,
+ referrer, cache, post, isgz, season);
+ }
+
+ void ListItem::addStreamInfo(const char* cType, const Properties& dictionary)
+ {
+ CLog::Log(
+ LOGWARNING,
+ "ListItem.addStreamInfo() is deprecated and might be removed in future Kodi versions. "
+ "Please use InfoTagVideo.addVideoStream(), InfoTagVideo.addAudioStream() and "
+ "InfoTagVideo.addSubtitleStream().");
+
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+
+ auto infoTag = GetVideoInfoTag();
+ if (StringUtils::CompareNoCase(cType, "video") == 0)
+ {
+ CStreamDetailVideo* video = new CStreamDetailVideo;
+ for (const auto& it : dictionary)
+ {
+ const String& key = it.first;
+ const String value(it.second.c_str());
+
+ if (key == "codec")
+ video->m_strCodec = value;
+ else if (key == "aspect")
+ video->m_fAspect = static_cast<float>(atof(value.c_str()));
+ else if (key == "width")
+ video->m_iWidth = strtol(value.c_str(), nullptr, 10);
+ else if (key == "height")
+ video->m_iHeight = strtol(value.c_str(), nullptr, 10);
+ else if (key == "duration")
+ video->m_iDuration = strtol(value.c_str(), nullptr, 10);
+ else if (key == "stereomode")
+ video->m_strStereoMode = value;
+ else if (key == "language")
+ video->m_strLanguage = value;
+ }
+ xbmc::InfoTagVideo::addStreamRaw(infoTag, video);
+ }
+ else if (StringUtils::CompareNoCase(cType, "audio") == 0)
+ {
+ CStreamDetailAudio* audio = new CStreamDetailAudio;
+ for (const auto& it : dictionary)
+ {
+ const String& key = it.first;
+ const String& value = it.second;
+
+ if (key == "codec")
+ audio->m_strCodec = value;
+ else if (key == "language")
+ audio->m_strLanguage = value;
+ else if (key == "channels")
+ audio->m_iChannels = strtol(value.c_str(), nullptr, 10);
+ }
+ xbmc::InfoTagVideo::addStreamRaw(infoTag, audio);
+ }
+ else if (StringUtils::CompareNoCase(cType, "subtitle") == 0)
+ {
+ CStreamDetailSubtitle* subtitle = new CStreamDetailSubtitle;
+ for (const auto& it : dictionary)
+ {
+ const String& key = it.first;
+ const String& value = it.second;
+
+ if (key == "language")
+ subtitle->m_strLanguage = value;
+ }
+ xbmc::InfoTagVideo::addStreamRaw(infoTag, subtitle);
+ }
+ xbmc::InfoTagVideo::finalizeStreamsRaw(infoTag);
+ } // end ListItem::addStreamInfo
+
+ void ListItem::addContextMenuItems(const std::vector<Tuple<String,String> >& items, bool replaceItems /* = false */)
+ {
+ for (size_t i = 0; i < items.size(); ++i)
+ {
+ auto& tuple = items[i];
+ if (tuple.GetNumValuesSet() != 2)
+ throw ListItemException("Must pass in a list of tuples of pairs of strings. One entry in the list only has %d elements.",tuple.GetNumValuesSet());
+
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ item->SetProperty(StringUtils::Format("contextmenulabel({})", i), tuple.first());
+ item->SetProperty(StringUtils::Format("contextmenuaction({})", i), tuple.second());
+ }
+ }
+
+ void ListItem::setSubtitles(const std::vector<String>& paths)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ addSubtitlesRaw(paths);
+ }
+
+ xbmc::InfoTagVideo* ListItem::getVideoInfoTag()
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ return new xbmc::InfoTagVideo(GetVideoInfoTag(), m_offscreen);
+ }
+
+ xbmc::InfoTagMusic* ListItem::getMusicInfoTag()
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ return new xbmc::InfoTagMusic(GetMusicInfoTag(), m_offscreen);
+ }
+
+ xbmc::InfoTagPicture* ListItem::getPictureInfoTag()
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ return new xbmc::InfoTagPicture(item->GetPictureInfoTag(), m_offscreen);
+ }
+
+ xbmc::InfoTagGame* ListItem::getGameInfoTag()
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, m_offscreen);
+ return new xbmc::InfoTagGame(item->GetGameInfoTag(), m_offscreen);
+ }
+
+ std::vector<std::string> ListItem::getStringArray(const InfoLabelValue& alt,
+ const std::string& tag,
+ std::string value,
+ const std::string& separator)
+ {
+ if (alt.which() == first)
+ {
+ if (value.empty())
+ value = alt.former();
+ return StringUtils::Split(value, separator);
+ }
+
+ std::vector<std::string> els;
+ for (const auto& el : alt.later())
+ {
+ if (el.which() == second)
+ throw WrongTypeException("When using \"%s\" you need to supply a string or list of strings for the value in the dictionary", tag.c_str());
+ els.emplace_back(el.former());
+ }
+ return els;
+ }
+
+ std::vector<std::string> ListItem::getVideoStringArray(const InfoLabelValue& alt,
+ const std::string& tag,
+ std::string value /* = "" */)
+ {
+ return getStringArray(
+ alt, tag, std::move(value),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+
+ std::vector<std::string> ListItem::getMusicStringArray(const InfoLabelValue& alt,
+ const std::string& tag,
+ std::string value /* = "" */)
+ {
+ return getStringArray(
+ alt, tag, std::move(value),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ }
+
+ CVideoInfoTag* ListItem::GetVideoInfoTag()
+ {
+ return item->GetVideoInfoTag();
+ }
+
+ const CVideoInfoTag* ListItem::GetVideoInfoTag() const
+ {
+ return item->GetVideoInfoTag();
+ }
+
+ MUSIC_INFO::CMusicInfoTag* ListItem::GetMusicInfoTag()
+ {
+ return item->GetMusicInfoTag();
+ }
+
+ const MUSIC_INFO::CMusicInfoTag* ListItem::GetMusicInfoTag() const
+ {
+ return item->GetMusicInfoTag();
+ }
+
+ void ListItem::setTitleRaw(std::string title)
+ {
+ item->m_strTitle = std::move(title);
+ }
+
+ void ListItem::setPathRaw(const std::string& path)
+ {
+ item->SetPath(path);
+ }
+
+ void ListItem::setCountRaw(int count)
+ {
+ item->m_iprogramCount = count;
+ }
+
+ void ListItem::setSizeRaw(int64_t size)
+ {
+ item->m_dwSize = size;
+ }
+
+ void ListItem::setDateTimeRaw(const std::string& dateTime)
+ {
+ if (dateTime.length() == 10)
+ {
+ int year = strtol(dateTime.substr(dateTime.size() - 4).c_str(), nullptr, 10);
+ int month = strtol(dateTime.substr(3, 4).c_str(), nullptr, 10);
+ int day = strtol(dateTime.substr(0, 2).c_str(), nullptr, 10);
+ item->m_dateTime.SetDate(year, month, day);
+ }
+ else
+ item->m_dateTime.SetFromW3CDateTime(dateTime);
+ }
+
+ void ListItem::setIsFolderRaw(bool isFolder)
+ {
+ item->m_bIsFolder = isFolder;
+ }
+
+ void ListItem::setStartOffsetRaw(double startOffset)
+ {
+ // we store the offset in frames, or 1/75th of a second
+ item->SetStartOffset(CUtil::ConvertSecsToMilliSecs(startOffset));
+ }
+
+ void ListItem::setMimeTypeRaw(const std::string& mimetype)
+ {
+ item->SetMimeType(mimetype);
+ }
+
+ void ListItem::setSpecialSortRaw(std::string specialSort)
+ {
+ StringUtils::ToLower(specialSort);
+
+ if (specialSort == "bottom")
+ item->SetSpecialSort(SortSpecialOnBottom);
+ else if (specialSort == "top")
+ item->SetSpecialSort(SortSpecialOnTop);
+ }
+
+ void ListItem::setContentLookupRaw(bool enable)
+ {
+ item->SetContentLookup(enable);
+ }
+
+ void ListItem::addArtRaw(std::string type, const std::string& url)
+ {
+ StringUtils::ToLower(type);
+ item->SetArt(type, url);
+ }
+
+ void ListItem::addPropertyRaw(std::string type, const CVariant& value)
+ {
+ StringUtils::ToLower(type);
+ item->SetProperty(type, value);
+ }
+
+ void ListItem::addSubtitlesRaw(const std::vector<std::string>& subtitles)
+ {
+ for (size_t i = 0; i < subtitles.size(); ++i)
+ // subtitle:{} index starts from 1
+ addPropertyRaw(StringUtils::Format("subtitle:{}", i + 1), subtitles[i]);
+ }
+ }
+}
diff --git a/xbmc/interfaces/legacy/ListItem.h b/xbmc/interfaces/legacy/ListItem.h
new file mode 100644
index 0000000..82513e0
--- /dev/null
+++ b/xbmc/interfaces/legacy/ListItem.h
@@ -0,0 +1,1291 @@
+/*
+ * 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 "AddonClass.h"
+#include "AddonString.h"
+#include "Alternative.h"
+#include "Dictionary.h"
+#include "FileItem.h"
+#include "InfoTagGame.h"
+#include "InfoTagMusic.h"
+#include "InfoTagPicture.h"
+#include "InfoTagVideo.h"
+#include "ListItem.h"
+#include "Tuple.h"
+#include "commons/Exception.h"
+
+#include <map>
+#include <utility>
+#include <vector>
+
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ XBMCCOMMONS_STANDARD_EXCEPTION(ListItemException);
+
+ // This is a type that represents either a String or a String Tuple
+ typedef Alternative<StringOrInt,Tuple<String, StringOrInt> > InfoLabelStringOrTuple;
+
+ // This type is either a String or a list of InfoLabelStringOrTuple types
+ typedef Alternative<StringOrInt, std::vector<InfoLabelStringOrTuple> > InfoLabelValue;
+
+ // The type contains the dictionary values for the ListItem::setInfo call.
+ // The values in the dictionary can be either a String, or a list of items.
+ // If it's a list of items then the items can be either a String or a Tuple.
+ typedef Dictionary<InfoLabelValue> InfoLabelDict;
+
+ //
+ /// \defgroup python_xbmcgui_listitem ListItem
+ /// \ingroup python_xbmcgui
+ /// @{
+ /// @brief **Selectable window list item.**
+ ///
+ class ListItem : public AddonClass
+ {
+ public:
+#if !defined SWIG && !defined DOXYGEN_SHOULD_SKIP_THIS
+ CFileItemPtr item;
+ bool m_offscreen;
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief Selectable window list item.
+ ///
+ /// The list item control is used for creating item lists in Kodi
+ ///
+ /// \python_class{ ListItem([label, label2, path, offscreen]) }
+ ///
+ /// @param label [opt] string (default `""`) - the label to display on the item
+ /// @param label2 [opt] string (default `""`) - the label2 of the item
+ /// @param path [opt] string (default `""`) - the path for the item
+ /// @param offscreen [opt] bool (default `False`) - if GUI based locks should be
+ /// avoided. Most of the times listitems are created
+ /// offscreen and added later to a container
+ /// for display (e.g. plugins) or they are not
+ /// even displayed (e.g. python scrapers).
+ /// In such cases, there is no need to lock the
+ /// GUI when creating the items (increasing your addon
+ /// performance).
+ /// Note however, that if you are creating listitems
+ /// and managing the container itself (e.g using
+ /// WindowXML or WindowXMLDialog classes) subsquent
+ /// modifications to the item will require locking.
+ /// Thus, in such cases, use the default value (`False`).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v16 **iconImage** and **thumbnailImage** are deprecated. Use **setArt()**.
+ /// @python_v18 Added **offscreen** argument.
+ /// @python_v19 Removed **iconImage** and **thumbnailImage**. Use **setArt()**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// listitem = xbmcgui.ListItem('Casino Royale')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ ListItem([label, label2, path, offscreen]);
+#else
+ ListItem(const String& label = emptyString,
+ const String& label2 = emptyString,
+ const String& path = emptyString,
+ bool offscreen = false);
+#endif
+
+#if !defined SWIG && !defined DOXYGEN_SHOULD_SKIP_THIS
+ inline explicit ListItem(CFileItemPtr pitem) : item(std::move(pitem)), m_offscreen(false) {}
+
+ static inline AddonClass::Ref<ListItem> fromString(const String& str)
+ {
+ AddonClass::Ref<ListItem> ret = AddonClass::Ref<ListItem>(new ListItem());
+ ret->item.reset(new CFileItem(str));
+ return ret;
+ }
+#endif
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+ ~ListItem() override;
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getLabel() }
+ /// Returns the listitem label.
+ ///
+ /// @return Label of item
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # getLabel()
+ /// label = listitem.getLabel()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getLabel();
+#else
+ String getLabel();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getLabel2() }
+ /// Returns the second listitem label.
+ ///
+ /// @return Second label of item
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # getLabel2()
+ /// label = listitem.getLabel2()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getLabel2();
+#else
+ String getLabel2();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setLabel(label) }
+ /// Sets the listitem's label.
+ ///
+ /// @param label string or unicode - text string.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setLabel(label)
+ /// listitem.setLabel('Casino Royale')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setLabel(...);
+#else
+ void setLabel(const String& label);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setLabel2(label) }
+ /// Sets the listitem's label2.
+ ///
+ /// @param label string or unicode - text string.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setLabel2(label)
+ /// listitem.setLabel2('Casino Royale')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setLabel2(...);
+#else
+ void setLabel2(const String& label);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getDateTime() }
+ /// Returns the list item's datetime in W3C format (YYYY-MM-DDThh:mm:ssTZD).
+ ///
+ /// @return string or unicode - datetime string (W3C).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # getDateTime()
+ /// strDateTime = listitem.getDateTime()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getDateTime();
+#else
+ String getDateTime();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setDateTime(dateTime) }
+ /// Sets the list item's datetime in W3C format.
+ /// The following formats are supported:
+ /// - YYYY
+ /// - YYYY-MM-DD
+ /// - YYYY-MM-DDThh:mm[TZD]
+ /// - YYYY-MM-DDThh:mm:ss[TZD]
+ /// where the timezone (TZD) is always optional and can be in one of the
+ /// following formats:
+ /// - Z (for UTC)
+ /// - +hh:mm
+ /// - -hh:mm
+ ///
+ /// @param label string or unicode - datetime string (W3C).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setDate(dateTime)
+ /// listitem.setDateTime('2021-03-09T12:30:00Z')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setDateTime(...);
+#else
+ void setDateTime(const String& dateTime);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setArt(values) }
+ /// Sets the listitem's art
+ ///
+ /// @param values dictionary - pairs of `{ label: value }`.
+ /// - Some default art values (any string possible):
+ /// | Label | Type |
+ /// |:-------------:|:--------------------------------------------------|
+ /// | thumb | string - image filename
+ /// | poster | string - image filename
+ /// | banner | string - image filename
+ /// | fanart | string - image filename
+ /// | clearart | string - image filename
+ /// | clearlogo | string - image filename
+ /// | landscape | string - image filename
+ /// | icon | string - image filename
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v13 New function added.
+ /// @python_v16 Added new label **icon**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setArt(values)
+ /// listitem.setArt({ 'poster': 'poster.png', 'banner' : 'banner.png' })
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setArt(...);
+#else
+ void setArt(const Properties& dictionary);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setIsFolder(isFolder) }
+ /// Sets if this listitem is a folder.
+ ///
+ /// @param isFolder bool - True=folder / False=not a folder (default).
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// @python_v18 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setIsFolder(isFolder)
+ /// listitem.setIsFolder(True)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setIsFolder(...);
+#else
+ void setIsFolder(bool isFolder);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setUniqueIDs(values, defaultrating) }
+ /// Sets the listitem's uniqueID
+ ///
+ /// @param values dictionary - pairs of `{ label: value }`.
+ /// @param defaultrating [opt] string - the name of default rating.
+ ///
+ /// - Some example values (any string possible):
+ /// | Label | Type |
+ /// |:-------------:|:--------------------------------------------------|
+ /// | imdb | string - uniqueid name
+ /// | tvdb | string - uniqueid name
+ /// | tmdb | string - uniqueid name
+ /// | anidb | string - uniqueid name
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **InfoTagVideo.setUniqueIDs()** instead.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setUniqueIDs(values, defaultrating)
+ /// listitem.setUniqueIDs({ 'imdb': 'tt8938399', 'tmdb' : '9837493' }, "imdb")
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setUniqueIDs(...);
+#else
+ void setUniqueIDs(const Properties& dictionary, const String& defaultrating = "");
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setRating(type, rating, votes = 0, defaultt = False) }
+ /// Sets a listitem's rating. It needs at least type and rating param
+ ///
+ /// @param type string - the type of the rating. Any string.
+ /// @param rating float - the value of the rating.
+ /// @param votes int - the number of votes. Default 0.
+ /// @param defaultt bool - is the default rating?. Default False.
+ /// - Some example type (any string possible):
+ /// | Label | Type |
+ /// |:-------------:|:--------------------------------------------------|
+ /// | imdb | string - rating type
+ /// | tvdb | string - rating type
+ /// | tmdb | string - rating type
+ /// | anidb | string - rating type
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **InfoTagVideo.setRating()** instead.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setRating(type, rating, votes, defaultt))
+ /// listitem.setRating("imdb", 4.6, 8940, True)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setRating(...);
+#else
+ void setRating(const std::string& type, float rating, int votes = 0, bool defaultt = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ addSeason(number, name = "") }
+ /// Add a season with name to a listitem. It needs at least the season number
+ ///
+ /// @param number int - the number of the season.
+ /// @param name string - the name of the season. Default "".
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// @python_v18 New function added.
+ /// @python_v20 Deprecated. Use **InfoTagVideo.addSeason()** or **InfoTagVideo.addSeasons()** instead.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # addSeason(number, name))
+ /// listitem.addSeason(1, "Murder House")
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ addSeason(...);
+#else
+ void addSeason(int number, std::string name = "");
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getArt(key) }
+ /// Returns a listitem art path as a string, similar to an infolabel.\n
+ ///
+ /// @param key string - art name.
+ /// - Some default art values (any string possible):
+ /// | Label | Type |
+ /// |---------------|--------------------------------------------------|
+ /// | thumb | string - image path
+ /// | poster | string - image path
+ /// | banner | string - image path
+ /// | fanart | string - image path
+ /// | clearart | string - image path
+ /// | clearlogo | string - image path
+ /// | landscape | string - image path
+ /// | icon | string - image path
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// poster = listitem.getArt('poster')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getArt(key);
+#else
+ String getArt(const char* key);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ isFolder() }
+ /// Returns whether the item is a folder or not.
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// isFolder = listitem.isFolder()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ isFolder();
+#else
+ bool isFolder() const;
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getUniqueID(key) }
+ /// Returns a listitem uniqueID as a string, similar to an infolabel.\n
+ ///
+ /// @param key string - uniqueID name.
+ /// - Some default uniqueID values (any string possible):
+ /// | Label | Type |
+ /// |---------------|--------------------------------------------------|
+ /// | imdb | string - uniqueid name
+ /// | tvdb | string - uniqueid name
+ /// | tmdb | string - uniqueid name
+ /// | anidb | string - uniqueid name
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **InfoTagVideo.getUniqueID()** instead.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// uniqueID = listitem.getUniqueID('imdb')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getUniqueID(key);
+#else
+ String getUniqueID(const char* key);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getRating(key) }
+ /// Returns a listitem rating as a float.\n
+ ///
+ /// @param key string - rating type.
+ /// - Some default key values (any string possible):
+ /// | Label | Type |
+ /// |---------------|--------------------------------------------------|
+ /// | imdb | string - type name
+ /// | tvdb | string - type name
+ /// | tmdb | string - type name
+ /// | anidb | string - type name
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **InfoTagVideo.getRating()** instead.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// rating = listitem.getRating('imdb')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getRating(key);
+#else
+ float getRating(const char* key);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getVotes(key) }
+ /// Returns a listitem votes as a integer.\n
+ ///
+ /// @param key string - rating type.
+ /// - Some default key values (any string possible):
+ /// | Label | Type |
+ /// |---------------|--------------------------------------------------|
+ /// | imdb | string - type name
+ /// | tvdb | string - type name
+ /// | tmdb | string - type name
+ /// | anidb | string - type name
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **InfoTagVideo.getVotesAsInt()** instead.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// votes = listitem.getVotes('imdb')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getVotes(key);
+#else
+ int getVotes(const char* key);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ select(selected) }
+ /// Sets the listitem's selected status.
+ ///
+ /// @param selected bool - True=selected/False=not selected
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # select(selected)
+ /// listitem.select(True)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ select(...);
+#else
+ void select(bool selected);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ isSelected() }
+ /// Returns the listitem's selected status.
+ ///
+ ///
+ /// @return bool - true if selected, otherwise false
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # isSelected()
+ /// selected = listitem.isSelected()
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ isSelected();
+#else
+ bool isSelected();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setInfo(type, infoLabels) }
+ /// Sets the listitem's infoLabels.
+ ///
+ /// @param type string - type of info labels
+ /// @param infoLabels dictionary - pairs of `{ label: value }`
+ ///
+ /// __Available types__
+ /// | Command name | Description |
+ /// |:------------:|:----------------------|
+ /// | video | Video information
+ /// | music | Music information
+ /// | pictures | Pictures informanion
+ /// | game | Game information
+ ///
+ /// @note To set pictures exif info, prepend `exif:` to the label. Exif values must be passed
+ /// as strings, separate value pairs with a comma. <b>(eg. <c>{'exif:resolution': '720,480'}</c></b>
+ /// See \ref kodi_pictures_infotag for valid strings.\n
+ /// \n
+ /// You can use the above as keywords for arguments and skip certain optional arguments.
+ /// Once you use a keyword, all following arguments require the keyword.
+ ///
+ /// __General Values__ (that apply to all types):
+ /// | Info label | Description |
+ /// |--------------:|:---------------------------------------------------|
+ /// | count | integer (12) - can be used to store an id for later, or for sorting purposes
+ /// | size | long (1024) - size in bytes
+ /// | date | string (%d.%m.%Y / 01.01.2009) - file date
+ ///
+ /// __Video Values__:
+ /// | Info label | Description |
+ /// |--------------:|:---------------------------------------------------|
+ /// | genre | string (Comedy) or list of strings (["Comedy", "Animation", "Drama"])
+ /// | country | string (Germany) or list of strings (["Germany", "Italy", "France"])
+ /// | year | integer (2009)
+ /// | episode | integer (4)
+ /// | season | integer (1)
+ /// | sortepisode | integer (4)
+ /// | sortseason | integer (1)
+ /// | episodeguide | string (Episode guide)
+ /// | showlink | string (Battlestar Galactica) or list of strings (["Battlestar Galactica", "Caprica"])
+ /// | top250 | integer (192)
+ /// | setid | integer (14)
+ /// | tracknumber | integer (3)
+ /// | rating | float (6.4) - range is 0..10
+ /// | userrating | integer (9) - range is 1..10 (0 to reset)
+ /// | watched | deprecated - use playcount instead
+ /// | playcount | integer (2) - number of times this item has been played
+ /// | overlay | integer (2) - range is `0..7`. See \ref kodi_guilib_listitem_iconoverlay "Overlay icon types" for values
+ /// | cast | list (["Michal C. Hall","Jennifer Carpenter"]) - if provided a list of tuples cast will be interpreted as castandrole
+ /// | castandrole | list of tuples ([("Michael C. Hall","Dexter"),("Jennifer Carpenter","Debra")])
+ /// | director | string (Dagur Kari) or list of strings (["Dagur Kari", "Quentin Tarantino", "Chrstopher Nolan"])
+ /// | mpaa | string (PG-13)
+ /// | plot | string (Long Description)
+ /// | plotoutline | string (Short Description)
+ /// | title | string (Big Fan)
+ /// | originaltitle | string (Big Fan)
+ /// | sorttitle | string (Big Fan)
+ /// | duration | integer (245) - duration in seconds
+ /// | studio | string (Warner Bros.) or list of strings (["Warner Bros.", "Disney", "Paramount"])
+ /// | tagline | string (An awesome movie) - short description of movie
+ /// | writer | string (Robert D. Siegel) or list of strings (["Robert D. Siegel", "Jonathan Nolan", "J.K. Rowling"])
+ /// | tvshowtitle | string (Heroes)
+ /// | premiered | string (2005-03-04)
+ /// | status | string (Continuing) - status of a TVshow
+ /// | set | string (Batman Collection) - name of the collection
+ /// | setoverview | string (All Batman movies) - overview of the collection
+ /// | tag | string (cult) or list of strings (["cult", "documentary", "best movies"]) - movie tag
+ /// | imdbnumber | string (tt0110293) - IMDb code
+ /// | code | string (101) - Production code
+ /// | aired | string (2008-12-07)
+ /// | credits | string (Andy Kaufman) or list of strings (["Dagur Kari", "Quentin Tarantino", "Chrstopher Nolan"]) - writing credits
+ /// | lastplayed | string (%Y-%m-%d %h:%m:%s = 2009-04-05 23:16:04)
+ /// | album | string (The Joshua Tree)
+ /// | artist | list (['U2'])
+ /// | votes | string (12345 votes)
+ /// | path | string (/home/user/movie.avi)
+ /// | trailer | string (/home/user/trailer.avi)
+ /// | dateadded | string (%Y-%m-%d %h:%m:%s = 2009-04-05 23:16:04)
+ /// | mediatype | string - "video", "movie", "tvshow", "season", "episode" or "musicvideo"
+ /// | dbid | integer (23) - Only add this for items which are part of the local db. You also need to set the correct 'mediatype'!
+ ///
+ /// __Music Values__:
+ /// | Info label | Description |
+ /// |-------------------------:|:---------------------------------------------------|
+ /// | tracknumber | integer (8)
+ /// | discnumber | integer (2)
+ /// | duration | integer (245) - duration in seconds
+ /// | year | integer (1998)
+ /// | genre | string (Rock)
+ /// | album | string (Pulse)
+ /// | artist | string (Muse)
+ /// | title | string (American Pie)
+ /// | rating | float - range is between 0 and 10
+ /// | userrating | integer - range is 1..10
+ /// | lyrics | string (On a dark desert highway...)
+ /// | playcount | integer (2) - number of times this item has been played
+ /// | lastplayed | string (%Y-%m-%d %h:%m:%s = 2009-04-05 23:16:04)
+ /// | mediatype | string - "music", "song", "album", "artist"
+ /// | dbid | integer (23) - Only add this for items which are part of the local db. You also need to set the correct 'mediatype'!
+ /// | listeners | integer (25614)
+ /// | musicbrainztrackid | string (cd1de9af-0b71-4503-9f96-9f5efe27923c)
+ /// | musicbrainzartistid | string (d87e52c5-bb8d-4da8-b941-9f4928627dc8)
+ /// | musicbrainzalbumid | string (24944755-2f68-3778-974e-f572a9e30108)
+ /// | musicbrainzalbumartistid | string (d87e52c5-bb8d-4da8-b941-9f4928627dc8)
+ /// | comment | string (This is a great song)
+ ///
+ /// __Picture Values__:
+ /// | Info label | Description |
+ /// |--------------:|:---------------------------------------------------|
+ /// | title | string (In the last summer-1)
+ /// | picturepath | string (`/home/username/pictures/img001.jpg`)
+ /// | exif* | string (See \ref kodi_pictures_infotag for valid strings)
+ ///
+ /// __Game Values__:
+ /// | Info label | Description |
+ /// |--------------:|:---------------------------------------------------|
+ /// | title | string (Super Mario Bros.)
+ /// | platform | string (Atari 2600)
+ /// | genres | list (["Action","Strategy"])
+ /// | publisher | string (Nintendo)
+ /// | developer | string (Square)
+ /// | overview | string (Long Description)
+ /// | year | integer (1980)
+ /// | gameclient | string (game.libretro.fceumm)
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v14 Added new label **discnumber**.
+ /// @python_v15 **duration** has to be set in seconds.
+ /// @python_v16 Added new label **mediatype**.
+ /// @python_v17
+ /// Added labels **setid**, **set**, **imdbnumber**, **code**, **dbid** (video), **path** and **userrating**.
+ /// Expanded the possible infoLabels for the option **mediatype**.
+ /// @python_v18 Added new **game** type and associated infolabels.
+ /// Added labels **dbid** (music), **setoverview**, **tag**, **sortepisode**, **sortseason**, **episodeguide**, **showlink**.
+ /// Extended labels **genre**, **country**, **director**, **studio**, **writer**, **tag**, **credits** to also use a list of strings.
+ /// @python_v20 Partially deprecated. Use explicit setters in **InfoTagVideo**, **InfoTagMusic**, **InfoTagPicture** or **InfoTagGame** instead.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// listitem.setInfo('video', { 'genre': 'Comedy' })
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setInfo(...);
+#else
+ void setInfo(const char* type, const InfoLabelDict& infoLabels);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setCast(actors) }
+ /// Set cast including thumbnails
+ ///
+ /// @param actors list of dictionaries (see below for relevant keys)
+ ///
+ /// - Keys:
+ /// | Label | Description |
+ /// |--------------:|:------------------------------------------------|
+ /// | name | string (Michael C. Hall)
+ /// | role | string (Dexter)
+ /// | thumbnail | string (http://www.someurl.com/someimage.png)
+ /// | order | integer (1)
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 New function added.
+ /// @python_v20 Deprecated. Use **InfoTagVideo.setCast()** instead.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// actors = [{"name": "Actor 1", "role": "role 1"}, {"name": "Actor 2", "role": "role 2"}]
+ /// listitem.setCast(actors)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setCast(...);
+#else
+ void setCast(const std::vector<Properties>& actors);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setAvailableFanart(images) }
+ /// Set available images (needed for video scrapers)
+ ///
+ /// @param images list of dictionaries (see below for relevant keys)
+ ///
+ /// - Keys:
+ /// | Label | Description |
+ /// |--------------:|:------------------------------------------------|
+ /// | image | string (http://www.someurl.com/someimage.png)
+ /// | preview | [opt] string (http://www.someurl.com/somepreviewimage.png)
+ /// | colors | [opt] string (either comma separated Kodi hex values ("FFFFFFFF,DDDDDDDD") or TVDB RGB Int Triplets ("|68,69,59|69,70,58|78,78,68|"))
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// fanart = [{"image": path_to_image_1, "preview": path_to_preview_1}, {"image": path_to_image_2, "preview": path_to_preview_2}]
+ /// listitem.setAvailableFanart(fanart)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setAvailableFanart(...);
+#else
+ void setAvailableFanart(const std::vector<Properties>& images);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ addAvailableArtwork(images) }
+ /// Add an image to available artworks (needed for video scrapers)
+ ///
+ /// @param url string (image path url)
+ /// @param art_type string (image type)
+ /// @param preview [opt] string (image preview path url)
+ /// @param referrer [opt] string (referrer url)
+ /// @param cache [opt] string (filename in cache)
+ /// @param post [opt] bool (use post to retrieve the image, default false)
+ /// @param isgz [opt] bool (use gzip to retrieve the image, default false)
+ /// @param season [opt] integer (number of season in case of season thumb)
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ /// @python_v19 New param added (preview).
+ /// @python_v20 Deprecated. Use **InfoTagVideo.addAvailableArtwork()** instead.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// listitem.addAvailableArtwork(path_to_image_1, "thumb")
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ addAvailableArtwork(...);
+#else
+ void addAvailableArtwork(const std::string& url,
+ const std::string& art_type = "",
+ const std::string& preview = "",
+ const std::string& referrer = "",
+ const std::string& cache = "",
+ bool post = false,
+ bool isgz = false,
+ int season = -1);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ addStreamInfo(type, values) }
+ /// Add a stream with details.
+ ///
+ /// @param type string - type of stream(video/audio/subtitle).
+ /// @param values dictionary - pairs of { label: value }.
+ ///
+ /// - Video Values:
+ /// | Label | Description |
+ /// |--------------:|:------------------------------------------------|
+ /// | codec | string (h264)
+ /// | aspect | float (1.78)
+ /// | width | integer (1280)
+ /// | height | integer (720)
+ /// | duration | integer (seconds)
+ ///
+ /// - Audio Values:
+ /// | Label | Description |
+ /// |--------------:|:------------------------------------------------|
+ /// | codec | string (dts)
+ /// | language | string (en)
+ /// | channels | integer (2)
+ ///
+ /// - Subtitle Values:
+ /// | Label | Description |
+ /// |--------------:|:------------------------------------------------|
+ /// | language | string (en)
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 Deprecated. Use **InfoTagVideo.addVideoStream()**, **InfoTagVideo.addAudioStream()** or **InfoTagVideo.addSubtitleStream()** instead.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// listitem.addStreamInfo('video', { 'codec': 'h264', 'width' : 1280 })
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ addStreamInfo(...);
+#else
+ void addStreamInfo(const char* cType, const Properties& dictionary);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ addContextMenuItems([(label, action),*]) }
+ /// Adds item(s) to the context menu for media lists.
+ ///
+ /// @param items list - [(label, action),*] A list of tuples consisting of label and action pairs.
+ /// - label string or unicode - item's label.
+ /// - action string or unicode - any available \link page_List_of_built_in_functions built-in function \endlink .
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 Completely removed previously available argument **replaceItems**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// listitem.addContextMenuItems([('Theater Showtimes', 'RunScript(script.myaddon,title=Iron Man)')])
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ addContextMenuItems(...);
+#else
+ void addContextMenuItems(const std::vector<Tuple<String,String> >& items, bool replaceItems = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setProperty(key, value) }
+ /// Sets a listitem property, similar to an infolabel.
+ ///
+ /// @param key string - property name.
+ /// @param value string or unicode - value of property.
+ ///
+ /// @note Key is NOT case sensitive.\n
+ /// You can use the above as keywords for arguments and skip certain optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.\n
+ /// \n
+ /// Some of these are treated internally by Kodi, such as the 'StartOffset' property, which is
+ /// the offset in seconds at which to start playback of an item. Others may be used in the skin
+ /// to add extra information, such as 'WatchedCount' for tvshow items
+ ///
+ /// - **Internal Properties**
+ /// | Key | Description |
+ /// |--------------:|:------------------------------------------------|
+ /// | inputstream | string (inputstream.adaptive) - Set the inputstream add-on that will be used to play the item
+ /// | IsPlayable | string - "true", "false" - Mark the item as playable, **mandatory for playable items**
+ /// | MimeType | string (application/x-mpegURL) - Set the MimeType of the item before playback
+ /// | ResumeTime | float (1962.0) - Set the resume point of the item in seconds
+ /// | SpecialSort | string - "top", "bottom" - The item will remain at the top or bottom of the current list
+ /// | StartOffset | float (60.0) - Set the offset in seconds at which to start playback of the item
+ /// | StartPercent | float (15.0) - Set the percentage at which to start playback of the item
+ /// | StationName | string ("My Station Name") - Used to enforce/override MusicPlayer.StationName infolabel from addons (e.g. in radio addons)
+ /// | TotalTime | float (7848.0) - Set the total time of the item in seconds
+ /// | OverrideInfotag | string - "true", "false" - When true will override all info from previous listitem
+ /// | ForceResolvePlugin | string - "true", "false" - When true ensures that a plugin will always receive callbacks to resolve paths (useful for playlist cases)
+ /// | rtsp_transport | string - "udp", "udp_multicast" or "tcp" - Allow to force the rtsp transport mode for rtsp streams
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 OverrideInfotag property added
+ /// @python_v20 **ResumeTime** and **TotalTime** deprecated. Use **InfoTagVideo.setResumePoint()** instead.
+ /// @python_v20 ForceResolvePlugin property added
+ /// @python_v20 rtsp_transport property added
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// listitem.setProperty('AspectRatio', '1.85 : 1')
+ /// listitem.setProperty('StartOffset', '256.4')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setProperty(...);
+#else
+ void setProperty(const char * key, const String& value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setProperties(values) }
+ /// Sets multiple properties for listitem's
+ ///
+ /// @param values dictionary - pairs of `{ label: value }`.
+ ///
+ /// @python_v18 New function added.
+ ///
+ ///-----------------------------------------------------------------------
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// # setProperties(values)
+ /// listitem.setProperties({ 'AspectRatio': '1.85', 'StartOffset' : '256.4' })
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setProperties(...);
+#else
+ void setProperties(const Properties& dictionary);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getProperty(key) }
+ /// Returns a listitem property as a string, similar to an infolabel.
+ ///
+ /// @param key string - property name.
+ ///
+ /// @note Key is NOT case sensitive.\n
+ /// You can use the above as keywords for arguments and skip certain optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 **ResumeTime** and **TotalTime** deprecated. Use **InfoTagVideo.getResumeTime()** and **InfoTagVideo.getResumeTimeTotal()** instead.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// AspectRatio = listitem.getProperty('AspectRatio')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ getProperty(...);
+#else
+ String getProperty(const char* key);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setPath(path) }
+ /// Sets the listitem's path.
+ ///
+ /// @param path string or unicode - path, activated when item is clicked.
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// listitem.setPath(path='/path/to/some/file.ext')
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setPath(...);
+#else
+ void setPath(const String& path);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setMimeType(mimetype) }
+ /// Sets the listitem's mimetype if known.
+ ///
+ /// @param mimetype string or unicode - mimetype
+ ///
+ /// If known prehand, this can (but does not have to) avoid HEAD requests
+ /// being sent to HTTP servers to figure out file type.
+ ///
+ setMimeType(...);
+#else
+ void setMimeType(const String& mimetype);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setContentLookup(enable) }
+ /// Enable or disable content lookup for item.
+ ///
+ /// If disabled, HEAD requests to e.g determine mime type will not be sent.
+ ///
+ /// @param enable bool to enable content lookup
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v16 New function added.
+ ///
+ setContentLookup(...);
+#else
+ void setContentLookup(bool enable);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ setSubtitles(subtitleFiles) }
+ /// Sets subtitles for this listitem.
+ ///
+ /// @param subtitleFiles list with path to subtitle files
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// listitem.setSubtitles(['special://temp/example.srt', 'http://example.com/example.srt'])
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v14 New function added.
+ ///
+ setSubtitles(...);
+#else
+ void setSubtitles(const std::vector<String>& subtitleFiles);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getPath() }
+ /// Returns the path of this listitem.
+ ///
+ /// @return [string] filename
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 New function added.
+ ///
+ ///
+ getPath();
+#else
+ String getPath();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getVideoInfoTag() }
+ /// Returns the VideoInfoTag for this item.
+ ///
+ /// @return video info tag
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v15 New function added.
+ ///
+ getVideoInfoTag();
+#else
+ xbmc::InfoTagVideo* getVideoInfoTag();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getMusicInfoTag() }
+ /// Returns the MusicInfoTag for this item.
+ ///
+ /// @return music info tag
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v15 New function added.
+ ///
+ getMusicInfoTag();
+#else
+ xbmc::InfoTagMusic* getMusicInfoTag();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getPictureInfoTag() }
+ /// Returns the InfoTagPicture for this item.
+ ///
+ /// @return picture info tag
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getPictureInfoTag();
+#else
+ xbmc::InfoTagPicture* getPictureInfoTag();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_listitem
+ /// @brief \python_func{ getGameInfoTag() }
+ /// Returns the InfoTagGame for this item.
+ ///
+ /// @return game info tag
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getGameInfoTag();
+#else
+ xbmc::InfoTagGame* getGameInfoTag();
+#endif
+
+private:
+ std::vector<std::string> getStringArray(const InfoLabelValue& alt,
+ const std::string& tag,
+ std::string value,
+ const std::string& separator);
+ std::vector<std::string> getVideoStringArray(const InfoLabelValue& alt,
+ const std::string& tag,
+ std::string value = "");
+ std::vector<std::string> getMusicStringArray(const InfoLabelValue& alt,
+ const std::string& tag,
+ std::string value = "");
+
+ CVideoInfoTag* GetVideoInfoTag();
+ const CVideoInfoTag* GetVideoInfoTag() const;
+
+ MUSIC_INFO::CMusicInfoTag* GetMusicInfoTag();
+ const MUSIC_INFO::CMusicInfoTag* GetMusicInfoTag() const;
+
+ void setTitleRaw(std::string title);
+ void setPathRaw(const std::string& path);
+ void setCountRaw(int count);
+ void setSizeRaw(int64_t size);
+ void setDateTimeRaw(const std::string& dateTime);
+ void setIsFolderRaw(bool isFolder);
+ void setStartOffsetRaw(double startOffset);
+ void setMimeTypeRaw(const std::string& mimetype);
+ void setSpecialSortRaw(std::string specialSort);
+ void setContentLookupRaw(bool enable);
+ void addArtRaw(std::string type, const std::string& url);
+ void addPropertyRaw(std::string type, const CVariant& value);
+ void addSubtitlesRaw(const std::vector<std::string>& subtitles);
+ };
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+ typedef std::vector<ListItem*> ListItemList;
+#endif
+ }
+}
diff --git a/xbmc/interfaces/legacy/ModuleXbmc.cpp b/xbmc/interfaces/legacy/ModuleXbmc.cpp
new file mode 100644
index 0000000..b9e4c05
--- /dev/null
+++ b/xbmc/interfaces/legacy/ModuleXbmc.cpp
@@ -0,0 +1,600 @@
+/*
+ * 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.
+ */
+
+//! @todo Need a uniform way of returning an error status
+
+#include "ModuleXbmc.h"
+
+#include "AddonUtils.h"
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "LangInfo.h"
+#include "LanguageHook.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "aojsonrpc.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "guilib/GUIAudioManager.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/TextureManager.h"
+#include "input/WindowTranslator.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "network/NetworkServices.h"
+#include "playlists/PlayListTypes.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "storage/discs/IDiscDriveHandler.h"
+#include "threads/SystemClock.h"
+#include "utils/Crc32.h"
+#include "utils/ExecString.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/MemUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <vector>
+
+using namespace KODI;
+
+namespace XBMCAddon
+{
+
+ namespace xbmc
+ {
+ /*****************************************************************
+ * start of xbmc methods
+ *****************************************************************/
+ void log(const char* msg, int level)
+ {
+ // check for a valid loglevel
+ if (level < LOGDEBUG || level > LOGNONE)
+ level = LOGDEBUG;
+ CLog::Log(level, "{}", msg);
+ }
+
+ void shutdown()
+ {
+ XBMC_TRACE;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SHUTDOWN);
+ }
+
+ void restart()
+ {
+ XBMC_TRACE;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_RESTART);
+ }
+
+ void executescript(const char* script)
+ {
+ XBMC_TRACE;
+ if (! script)
+ return;
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_SCRIPT, -1, -1, nullptr, script);
+ }
+
+ void executebuiltin(const char* function, bool wait /* = false*/)
+ {
+ XBMC_TRACE;
+ if (! function)
+ return;
+
+ // builtins is no anarchy
+ // enforce some rules here
+ // DialogBusy must not be activated, it is modal dialog
+ const CExecString exec(function);
+ if (!exec.IsValid())
+ return;
+
+ const std::string execute = exec.GetFunction();
+ const std::vector<std::string> params = exec.GetParams();
+
+ if (StringUtils::EqualsNoCase(execute, "activatewindow") ||
+ StringUtils::EqualsNoCase(execute, "closedialog"))
+ {
+ int win = CWindowTranslator::TranslateWindow(params[0]);
+ if (win == WINDOW_DIALOG_BUSY)
+ {
+ CLog::Log(LOGWARNING, "addons must not activate DialogBusy");
+ return;
+ }
+ }
+
+ if (wait)
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ function);
+ else
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ function);
+ }
+
+ String executeJSONRPC(const char* jsonrpccommand)
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dg;
+ String ret;
+
+ if (! jsonrpccommand)
+ return ret;
+
+ // String method = jsonrpccommand;
+
+ CAddOnTransport transport;
+ CAddOnTransport::CAddOnClient client;
+
+ return JSONRPC::CJSONRPC::MethodCall(/*method*/ jsonrpccommand, &transport, &client);
+ }
+
+ void sleep(long timemillis)
+ {
+ XBMC_TRACE;
+
+ XbmcThreads::EndTime<> endTime{std::chrono::milliseconds(timemillis)};
+ while (!endTime.IsTimePast())
+ {
+ LanguageHook* lh = NULL;
+ {
+ DelayedCallGuard dcguard;
+ lh = dcguard.getLanguageHook(); // borrow this
+ long nextSleep = endTime.GetTimeLeft().count();
+ if (nextSleep > 100)
+ nextSleep = 100; // only sleep for 100 millis
+ KODI::TIME::Sleep(std::chrono::milliseconds(nextSleep));
+ }
+ if (lh != NULL)
+ lh->MakePendingCalls();
+ }
+ }
+
+ String getLocalizedString(int id)
+ {
+ XBMC_TRACE;
+ String label;
+ if (id >= 30000 && id <= 30999)
+ label = g_localizeStringsTemp.Get(id);
+ else if (id >= 32000 && id <= 32999)
+ label = g_localizeStringsTemp.Get(id);
+ else
+ label = g_localizeStrings.Get(id);
+
+ return label;
+ }
+
+ String getSkinDir()
+ {
+ XBMC_TRACE;
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN);
+ }
+
+ String getLanguage(int format /* = CLangCodeExpander::ENGLISH_NAME */, bool region /*= false*/)
+ {
+ XBMC_TRACE;
+ std::string lang = g_langInfo.GetEnglishLanguageName();
+
+ switch (format)
+ {
+ case CLangCodeExpander::ENGLISH_NAME:
+ {
+ if (region)
+ {
+ std::string region = "-" + g_langInfo.GetCurrentRegion();
+ return (lang += region);
+ }
+ return lang;
+ }
+ case CLangCodeExpander::ISO_639_1:
+ {
+ std::string langCode;
+ g_LangCodeExpander.ConvertToISO6391(lang, langCode);
+ if (region)
+ {
+ std::string region = g_langInfo.GetRegionLocale();
+ std::string region2Code;
+ g_LangCodeExpander.ConvertToISO6391(region, region2Code);
+ region2Code = "-" + region2Code;
+ return (langCode += region2Code);
+ }
+ return langCode;
+ }
+ case CLangCodeExpander::ISO_639_2:
+ {
+ std::string langCode;
+ g_LangCodeExpander.ConvertToISO6392B(lang, langCode);
+ if (region)
+ {
+ std::string region = g_langInfo.GetRegionLocale();
+ std::string region3Code;
+ g_LangCodeExpander.ConvertToISO6392B(region, region3Code);
+ region3Code = "-" + region3Code;
+ return (langCode += region3Code);
+ }
+
+ return langCode;
+ }
+ default:
+ return "";
+ }
+ }
+
+ String getIPAddress()
+ {
+ XBMC_TRACE;
+ char cTitleIP[32];
+ sprintf(cTitleIP, "127.0.0.1");
+ CNetworkInterface* iface = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+ if (iface)
+ return iface->GetCurrentIPAddress();
+
+ return cTitleIP;
+ }
+
+ long getDVDState()
+ {
+ XBMC_TRACE;
+ return static_cast<long>(CServiceBroker::GetMediaManager().GetDriveStatus());
+ }
+
+ long getFreeMem()
+ {
+ XBMC_TRACE;
+ KODI::MEMORY::MemoryStatus stat;
+ KODI::MEMORY::GetMemoryStatus(&stat);
+ return static_cast<long>(stat.availPhys / ( 1024 * 1024 ));
+ }
+
+ // getCpuTemp() method
+ // ## Doesn't work right, use getInfoLabel('System.CPUTemperature') instead.
+ /*PyDoc_STRVAR(getCpuTemp__doc__,
+ "getCpuTemp() -- Returns the current cpu temperature as an integer."
+ ""
+ "example:"
+ " - cputemp = xbmc.getCpuTemp()");
+
+ PyObject* XBMC_GetCpuTemp(PyObject *self, PyObject *args)
+ {
+ unsigned short cputemp;
+ unsigned short cpudec;
+
+ _outp(0xc004, (0x4c<<1)|0x01);
+ _outp(0xc008, 0x01);
+ _outpw(0xc000, _inpw(0xc000));
+ _outp(0xc002, (0) ? 0x0b : 0x0a);
+ while ((_inp(0xc000) & 8));
+ cputemp = _inpw(0xc006);
+
+ _outp(0xc004, (0x4c<<1)|0x01);
+ _outp(0xc008, 0x10);
+ _outpw(0xc000, _inpw(0xc000));
+ _outp(0xc002, (0) ? 0x0b : 0x0a);
+ while ((_inp(0xc000) & 8));
+ cpudec = _inpw(0xc006);
+
+ if (cpudec<10) cpudec = cpudec * 100;
+ if (cpudec<100) cpudec = cpudec *10;
+
+ return PyInt_FromLong((long)(cputemp + cpudec / 1000.0f));
+ }*/
+
+ String getInfoLabel(const char* cLine)
+ {
+ XBMC_TRACE;
+ if (!cLine)
+ {
+ String ret;
+ return ret;
+ }
+
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ int ret = infoMgr.TranslateString(cLine);
+ //doesn't seem to be a single InfoTag?
+ //try full blown GuiInfoLabel then
+ if (ret == 0)
+ return GUILIB::GUIINFO::CGUIInfoLabel::GetLabel(cLine, INFO::DEFAULT_CONTEXT);
+ else
+ return infoMgr.GetLabel(ret, INFO::DEFAULT_CONTEXT);
+ }
+
+ String getInfoImage(const char * infotag)
+ {
+ XBMC_TRACE;
+ if (!infotag)
+ {
+ String ret;
+ return ret;
+ }
+
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ int ret = infoMgr.TranslateString(infotag);
+ return infoMgr.GetImage(ret, WINDOW_INVALID);
+ }
+
+ void playSFX(const char* filename, bool useCached)
+ {
+ XBMC_TRACE;
+ if (!filename)
+ return;
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (CFileUtils::Exists(filename) && gui)
+ {
+ gui->GetAudioManager().PlayPythonSound(filename,useCached);
+ }
+ }
+
+ void stopSFX()
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dg;
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetAudioManager().Stop();
+ }
+
+ void enableNavSounds(bool yesNo)
+ {
+ XBMC_TRACE;
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetAudioManager().Enable(yesNo);
+ }
+
+ bool getCondVisibility(const char *condition)
+ {
+ XBMC_TRACE;
+ if (!condition)
+ return false;
+
+ bool ret;
+ {
+ XBMCAddonUtils::GuiLock lock(nullptr, false);
+
+ int id = CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog();
+ if (id == WINDOW_INVALID)
+ id = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ ret = CServiceBroker::GetGUI()->GetInfoManager().EvaluateBool(condition,id);
+ }
+
+ return ret;
+ }
+
+ int getGlobalIdleTime()
+ {
+ XBMC_TRACE;
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ return appPower->GlobalIdleTime();
+ }
+
+ String getCacheThumbName(const String& path)
+ {
+ XBMC_TRACE;
+ auto crc = Crc32::ComputeFromLowerCase(path);
+ return StringUtils::Format("{:08x}.tbn", crc);
+ }
+
+ Tuple<String,String> getCleanMovieTitle(const String& path, bool usefoldername)
+ {
+ XBMC_TRACE;
+ CFileItem item(path, false);
+ std::string strName = item.GetMovieName(usefoldername);
+
+ std::string strTitleAndYear;
+ std::string strTitle;
+ std::string strYear;
+ CUtil::CleanString(strName, strTitle, strTitleAndYear, strYear, usefoldername);
+ return Tuple<String,String>(strTitle,strYear);
+ }
+
+ String getRegion(const char* id)
+ {
+ XBMC_TRACE;
+ std::string result;
+
+ if (StringUtils::CompareNoCase(id, "datelong") == 0)
+ {
+ result = g_langInfo.GetDateFormat(true);
+ StringUtils::Replace(result, "DDDD", "%A");
+ StringUtils::Replace(result, "MMMM", "%B");
+ StringUtils::Replace(result, "D", "%d");
+ StringUtils::Replace(result, "YYYY", "%Y");
+ }
+ else if (StringUtils::CompareNoCase(id, "dateshort") == 0)
+ {
+ result = g_langInfo.GetDateFormat(false);
+ StringUtils::Replace(result, "MM", "%m");
+ StringUtils::Replace(result, "DD", "%d");
+#ifdef TARGET_WINDOWS
+ StringUtils::Replace(result, "M", "%#m");
+ StringUtils::Replace(result, "D", "%#d");
+#else
+ StringUtils::Replace(result, "M", "%-m");
+ StringUtils::Replace(result, "D", "%-d");
+#endif
+ StringUtils::Replace(result, "YYYY", "%Y");
+ }
+ else if (StringUtils::CompareNoCase(id, "tempunit") == 0)
+ result = g_langInfo.GetTemperatureUnitString();
+ else if (StringUtils::CompareNoCase(id, "speedunit") == 0)
+ result = g_langInfo.GetSpeedUnitString();
+ else if (StringUtils::CompareNoCase(id, "time") == 0)
+ {
+ result = g_langInfo.GetTimeFormat();
+ if (StringUtils::StartsWith(result, "HH"))
+ StringUtils::Replace(result, "HH", "%H");
+ else
+ StringUtils::Replace(result, "H", "%H");
+ StringUtils::Replace(result, "h", "%I");
+ StringUtils::Replace(result, "mm", "%M");
+ StringUtils::Replace(result, "ss", "%S");
+ StringUtils::Replace(result, "xx", "%p");
+ }
+ else if (StringUtils::CompareNoCase(id, "meridiem") == 0)
+ result = StringUtils::Format("{}/{}", g_langInfo.GetMeridiemSymbol(MeridiemSymbolAM),
+ g_langInfo.GetMeridiemSymbol(MeridiemSymbolPM));
+
+ return result;
+ }
+
+ //! @todo Add a mediaType enum
+ String getSupportedMedia(const char* mediaType)
+ {
+ XBMC_TRACE;
+ String result;
+ if (StringUtils::CompareNoCase(mediaType, "video") == 0)
+ result = CServiceBroker::GetFileExtensionProvider().GetVideoExtensions();
+ else if (StringUtils::CompareNoCase(mediaType, "music") == 0)
+ result = CServiceBroker::GetFileExtensionProvider().GetMusicExtensions();
+ else if (StringUtils::CompareNoCase(mediaType, "picture") == 0)
+ result = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+
+ //! @todo implement
+ // else
+ // return an error
+
+ return result;
+ }
+
+ bool skinHasImage(const char* image)
+ {
+ XBMC_TRACE;
+ return CServiceBroker::GetGUI()->GetTextureManager().HasTexture(image);
+ }
+
+
+ bool startServer(int iTyp, bool bStart)
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dg;
+ return CServiceBroker::GetNetwork().GetServices().StartServer(
+ static_cast<CNetworkServices::ESERVERS>(iTyp), bStart != 0);
+ }
+
+ void audioSuspend()
+ {
+ IAE *ae = CServiceBroker::GetActiveAE();
+ if (ae)
+ ae->Suspend();
+ }
+
+ void audioResume()
+ {
+ IAE *ae = CServiceBroker::GetActiveAE();
+ if (ae)
+ ae->Resume();
+ }
+
+ String convertLanguage(const char* language, int format)
+ {
+ std::string convertedLanguage;
+ switch (format)
+ {
+ case CLangCodeExpander::ENGLISH_NAME:
+ {
+ g_LangCodeExpander.Lookup(language, convertedLanguage);
+ // maybe it's a check whether the language exists or not
+ if (convertedLanguage.empty())
+ {
+ g_LangCodeExpander.ConvertToISO6392B(language, convertedLanguage);
+ g_LangCodeExpander.Lookup(convertedLanguage, convertedLanguage);
+ }
+ break;
+ }
+ case CLangCodeExpander::ISO_639_1:
+ g_LangCodeExpander.ConvertToISO6391(language, convertedLanguage);
+ break;
+ case CLangCodeExpander::ISO_639_2:
+ g_LangCodeExpander.ConvertToISO6392B(language, convertedLanguage);
+ break;
+ default:
+ return "";
+ }
+ return convertedLanguage;
+ }
+
+ String getUserAgent()
+ {
+ return CSysInfo::GetUserAgent();
+ }
+
+ int getSERVER_WEBSERVER()
+ {
+ return CNetworkServices::ES_WEBSERVER;
+ }
+ int getSERVER_AIRPLAYSERVER()
+ {
+ return CNetworkServices::ES_AIRPLAYSERVER;
+ }
+ int getSERVER_UPNPSERVER()
+ {
+ return CNetworkServices::ES_UPNPSERVER;
+ }
+ int getSERVER_UPNPRENDERER()
+ {
+ return CNetworkServices::ES_UPNPRENDERER;
+ }
+ int getSERVER_EVENTSERVER()
+ {
+ return CNetworkServices::ES_EVENTSERVER;
+ }
+ int getSERVER_JSONRPCSERVER()
+ {
+ return CNetworkServices::ES_JSONRPCSERVER;
+ }
+ int getSERVER_ZEROCONF()
+ {
+ return CNetworkServices::ES_ZEROCONF;
+ }
+
+ int getPLAYLIST_MUSIC()
+ {
+ return PLAYLIST::TYPE_MUSIC;
+ }
+ int getPLAYLIST_VIDEO()
+ {
+ return PLAYLIST::TYPE_VIDEO;
+ }
+ int getTRAY_OPEN()
+ {
+ return static_cast<int>(TrayState::OPEN);
+ }
+ int getDRIVE_NOT_READY()
+ {
+ return static_cast<int>(DriveState::NOT_READY);
+ }
+ int getTRAY_CLOSED_NO_MEDIA()
+ {
+ return static_cast<int>(TrayState::CLOSED_NO_MEDIA);
+ }
+ int getTRAY_CLOSED_MEDIA_PRESENT()
+ {
+ return static_cast<int>(TrayState::CLOSED_MEDIA_PRESENT);
+ }
+ int getLOGDEBUG() { return LOGDEBUG; }
+ int getLOGINFO() { return LOGINFO; }
+ int getLOGWARNING() { return LOGWARNING; }
+ int getLOGERROR() { return LOGERROR; }
+ int getLOGFATAL() { return LOGFATAL; }
+ int getLOGNONE() { return LOGNONE; }
+
+ // language string formats
+ int getISO_639_1() { return CLangCodeExpander::ISO_639_1; }
+ int getISO_639_2(){ return CLangCodeExpander::ISO_639_2; }
+ int getENGLISH_NAME() { return CLangCodeExpander::ENGLISH_NAME; }
+
+ const int lLOGDEBUG = LOGDEBUG;
+ }
+}
diff --git a/xbmc/interfaces/legacy/ModuleXbmc.h b/xbmc/interfaces/legacy/ModuleXbmc.h
new file mode 100644
index 0000000..0720d8a
--- /dev/null
+++ b/xbmc/interfaces/legacy/ModuleXbmc.h
@@ -0,0 +1,898 @@
+/*
+ * 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 "AddonString.h"
+#include "Tuple.h"
+
+#include "utils/LangCodeExpander.h"
+#include "swighelper.h"
+#include <vector>
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+#ifndef SWIG
+ // This is a bit of a hack to get around a SWIG problem
+ extern const int lLOGDEBUG;
+#endif
+#endif /* DOXYGEN_SHOULD_SKIP_THIS */
+
+ //
+ /// \defgroup python_xbmc Library - xbmc
+ /// @{
+ /// @brief **General functions on Kodi.**
+ ///
+ /// Offers classes and functions that provide information about the media
+ /// currently playing and that allow manipulation of the media player (such
+ /// as starting a new song). You can also find system information using the
+ /// functions available in this library.
+ //
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.log(msg[, level]) }
+ /// Write a string to Kodi's log file and the debug window.
+ ///
+ /// @param msg string - text to output.
+ /// @param level [opt] integer - log level to output at.
+ /// <em>(default=LOGDEBUG)</em>
+ /// | Value: | Description: |
+ /// |----------------:|---------------------------------------------------|
+ /// | xbmc.LOGDEBUG | In depth information about the status of Kodi. This information can pretty much only be deciphered by a developer or long time Kodi power user.
+ /// | xbmc.LOGINFO | Something has happened. It's not a problem, we just thought you might want to know. Fairly excessive output that most people won't care about.
+ /// | xbmc.LOGWARNING | Something potentially bad has happened. If Kodi did something you didn't expect, this is probably why. Watch for errors to follow.
+ /// | xbmc.LOGERROR | This event is bad. Something has failed. You likely noticed problems with the application be it skin artifacts, failure of playback a crash, etc.
+ /// | xbmc.LOGFATAL | We're screwed. Kodi is about to crash.
+ ///
+ /// @note Addon developers are advised to keep `LOGDEBUG` as the default
+ /// logging level and to use conservative logging (log only if needed).
+ /// Excessive logging makes it harder to debug kodi itself.
+ ///
+ /// Logging in kodi has a global configuration level that controls how text
+ /// is written to the log. This global logging behaviour can be changed in
+ /// the GUI (**Settings -> System -> Logging**) (debug toggle) or furthered
+ /// configured in advancedsettings (loglevel setting).
+ ///
+ /// Text is written to the log for the following conditions:
+ /// - loglevel == -1 (NONE, nothing at all is logged to the log)
+ /// - loglevel == 0 (NORMAL, shows `LOGINFO`, `LOGWARNING`, `LOGERROR` and `LOGFATAL`) - Default kodi behaviour
+ /// - loglevel == 1 (DEBUG, shows all) - Behaviour if you toggle debug log in the GUI
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v17 Default level changed from `LOGNOTICE` to `LOGDEBUG`
+ /// @python_v19 Removed `LOGNOTICE` (use `LOGINFO`) and `LOGSEVERE` (use `LOGFATAL`)
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.log(msg='This is a test string.', level=xbmc.LOGDEBUG);
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ log(...);
+#else
+ void log(const char* msg, int level = lLOGDEBUG);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.shutdown() }
+ /// Shutdown the htpc.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.shutdown()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ shutdown();
+#else
+ void shutdown();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.restart() }
+ /// Restart the htpc.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.restart()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ restart();
+#else
+ void restart();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.executescript(script) }
+ /// Execute a python script.
+ ///
+ /// @param script string - script filename to execute.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.executescript('special://home/scripts/update.py')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ executescript(...);
+#else
+ void executescript(const char* script);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.executebuiltin(function) }
+ /// Execute a built in Kodi function.
+ ///
+ /// @param function string - builtin function to execute.
+ /// @param wait [opt] bool - If Kodi should wait for the
+ /// builtin function execution to finish
+ /// (default False)
+ ///
+ ///
+ /// \ref page_List_of_built_in_functions "List of builtin functions"
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.executebuiltin('Skin.SetString(abc,def)')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ executebuiltin(...);
+#else
+ void executebuiltin(const char* function, bool wait = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.executeJSONRPC(jsonrpccommand) }
+ /// Execute an JSONRPC command.
+ ///
+ /// @param jsonrpccommand string - jsonrpc command to execute.
+ /// @return jsonrpc return string
+ ///
+ ///
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// response = xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "method": "JSONRPC.Introspect", "id": 1 }')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ executeJSONRPC(...);
+#else
+ String executeJSONRPC(const char* jsonrpccommand);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.sleep(time) }
+ /// Sleeps for 'time' (msec).
+ /// \anchor xbmc_Sleep
+ ///
+ /// @param time integer - number of msec to sleep.
+ ///
+ /// @throws PyExc_TypeError If time is not an integer.
+ ///
+ /// @warning This is useful if you need to sleep for a small amount of time
+ /// (milisecond range) somewhere in your addon logic. Please note that Kodi
+ /// will attempt to stop any running scripts when signaled to exit and wait for a maximum
+ /// of 5 seconds before trying to force stop your script. If your addon makes use
+ /// of \ref xbmc_Sleep "xbmc.sleep()" incorrectly (long periods of time, e.g. that exceed
+ /// the force stop waiting time) it may lead to Kodi hanging on shutdown.
+ /// In case your addon needs long sleep/idle periods use
+ /// \ref xbmc_Monitor_waitForAbort "xbmc.Monitor().waitForAbort(secs)"
+ /// instead.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.sleep(2000) # sleeps for 2 seconds
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ sleep(...);
+#else
+ void sleep(long timemillis);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getLocalizedString(id) }
+ /// Get a localized 'unicode string'.
+ ///
+ /// @param id integer - id# for string you want to
+ /// localize.
+ /// @return Localized 'unicode string'
+ ///
+ /// @note See strings.po in `\language\{yourlanguage}\` for which id
+ /// you need for a string.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// locstr = xbmc.getLocalizedString(6)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getLocalizedString(...);
+#else
+ String getLocalizedString(int id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getSkinDir() }
+ /// Get the active skin directory.
+ ///
+ /// @return The active skin directory as a string
+ ///
+ ///
+ /// @note This is not the full path like 'special://home/addons/MediaCenter',
+ /// but only 'MediaCenter'.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// skindir = xbmc.getSkinDir()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getSkinDir();
+#else
+ String getSkinDir();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getLanguage([format], [region]) }
+ /// Get the active language.
+ ///
+ /// @param format [opt] format of the returned language
+ /// string
+ /// | Value | Description
+ /// |------------------:|:-------------------------------------------------|
+ /// | xbmc.ISO_639_1 | Two letter code as defined in ISO 639-1
+ /// | xbmc.ISO_639_2 | Three letter code as defined in ISO 639-2/T or ISO 639-2/B
+ /// | xbmc.ENGLISH_NAME | Full language name in English (default)
+ /// @param region [opt] append the region delimited by "-"
+ /// of the language (setting) to the
+ /// returned language string
+ /// @return The active language as a string
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v13 Added new options **format** and **region**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// language = xbmc.getLanguage(xbmc.ENGLISH_NAME)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getLanguage(...);
+#else
+ String getLanguage(int format = CLangCodeExpander::ENGLISH_NAME, bool region = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getIPAddress() }
+ /// Get the current ip address.
+ ///
+ /// @return The current ip address as a string
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// ip = xbmc.getIPAddress()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getIPAddress();
+#else
+ String getIPAddress();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getDVDState() }
+ /// Returns the dvd state as an integer.
+ ///
+ /// @return Values for state are:
+ /// | Value | Name |
+ /// |------:|:-------------------------------|
+ /// | 1 | xbmc.DRIVE_NOT_READY
+ /// | 16 | xbmc.TRAY_OPEN
+ /// | 64 | xbmc.TRAY_CLOSED_NO_MEDIA
+ /// | 96 | xbmc.TRAY_CLOSED_MEDIA_PRESENT
+ ///
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dvdstate = xbmc.getDVDState()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getDVDState();
+#else
+ long getDVDState();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getFreeMem() }
+ /// Get amount of free memory in MB.
+ ///
+ /// @return The amount of free memory in MB as an integer
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// freemem = xbmc.getFreeMem()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getFreeMem();
+#else
+ long getFreeMem();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getInfoLabel(infotag) }
+ /// Get a info label
+ ///
+ /// @param infotag string - infoTag for value you want
+ /// returned.
+ /// @return InfoLabel as a string
+ ///
+ /// \ref modules__infolabels_boolean_conditions "List of InfoTags"
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// label = xbmc.getInfoLabel('Weather.Conditions')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getInfoLabel(...);
+#else
+ String getInfoLabel(const char* cLine);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getInfoImage(infotag) }
+ /// Get filename including path to the InfoImage's thumbnail.
+ ///
+ /// @param infotag string - infotag for value you want
+ /// returned
+ /// @return Filename including path to the
+ /// InfoImage's thumbnail as a string
+ ///
+ ///
+ /// List of InfoTags - http://kodi.wiki/view/InfoLabels
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// filename = xbmc.getInfoImage('Weather.Conditions')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getInfoImage(...);
+#else
+ String getInfoImage(const char * infotag);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.playSFX(filename,[useCached]) }
+ /// Plays a wav file by filename
+ ///
+ /// @param filename string - filename of the wav file to
+ /// play
+ /// @param useCached [opt] bool - False = Dump any
+ /// previously cached wav associated with
+ /// filename
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v14 Added new option **useCached**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.playSFX('special://xbmc/scripts/dingdong.wav')
+ /// xbmc.playSFX('special://xbmc/scripts/dingdong.wav',False)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ playSFX(...);
+#else
+ void playSFX(const char* filename, bool useCached = true);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.stopSFX() }
+ /// Stops wav file
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v14 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.stopSFX()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ stopSFX();
+#else
+ void stopSFX();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.enableNavSounds(yesNo) }
+ /// Enables/Disables nav sounds
+ ///
+ /// @param yesNo bool - enable (True) or disable
+ /// (False) nav sounds
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.enableNavSounds(True)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ enableNavSounds(...);
+#else
+ void enableNavSounds(bool yesNo);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getCondVisibility(condition) }
+ /// Get visibility conditions
+ ///
+ /// @param condition string - condition to check
+ /// @return True (if the condition is verified) or False (otherwise)
+ ///
+ /// \ref modules__infolabels_boolean_conditions "List of boolean conditions"
+ ///
+ /// @note You can combine two (or more) of the above settings by using <b>"+"</b> as an AND operator,
+ /// <b>"|"</b> as an OR operator, <b>"!"</b> as a NOT operator, and <b>"["</b> and <b>"]"</b> to bracket expressions.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// visible = xbmc.getCondVisibility('[Control.IsVisible(41) + !Control.IsVisible(12)]')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getCondVisibility(...);
+#else
+ bool getCondVisibility(const char *condition);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getGlobalIdleTime() }
+ /// Get the elapsed idle time in seconds.
+ ///
+ /// @return Elapsed idle time in seconds as an integer
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// t = xbmc.getGlobalIdleTime()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getGlobalIdleTime();
+#else
+ int getGlobalIdleTime();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getCacheThumbName(path) }
+ /// Get thumb cache filename.
+ ///
+ /// @param path string - path to file
+ /// @return Thumb cache filename
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// thumb = xbmc.getCacheThumbName('f:\\videos\\movie.avi')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getCacheThumbName(...);
+#else
+ String getCacheThumbName(const String& path);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getCleanMovieTitle(path[, usefoldername]) }
+ /// Get clean movie title and year string if available.
+ ///
+ /// @param path string - String to clean
+ /// @param usefoldername [opt] bool - use folder names (defaults
+ /// to false)
+ /// @return Clean movie title and year string if
+ /// available.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// title, year = xbmc.getCleanMovieTitle('/path/to/moviefolder/test.avi', True)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getCleanMovieTitle(...);
+#else
+ Tuple<String,String> getCleanMovieTitle(const String& path, bool usefoldername = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getRegion(id) }
+ /// Returns your regions setting as a string for the specified id.
+ ///
+ /// @param id string - id of setting to return
+ /// @return Region setting
+ ///
+ /// @note choices are (dateshort, datelong, time, meridiem, tempunit, speedunit)
+ /// You can use the above as keywords for arguments.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// date_long_format = xbmc.getRegion('datelong')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getRegion(...);
+#else
+ String getRegion(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getSupportedMedia(media) }
+ /// Get the supported file types for the specific media.
+ ///
+ /// @param media string - media type
+ /// @return Supported file types for the specific
+ /// media as a string
+ ///
+ ///
+ /// @note Media type can be (video, music, picture).
+ /// The return value is a pipe separated string of filetypes
+ /// (eg. '.mov |.avi').\n
+ /// You can use the above as keywords for arguments.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// mTypes = xbmc.getSupportedMedia('video')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getSupportedMedia(...);
+#else
+ String getSupportedMedia(const char* mediaType);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.skinHasImage(image) }
+ /// Check skin for presence of Image.
+ ///
+ /// @param image string - image filename
+ /// @return True if the image file exists in the skin
+ ///
+ ///
+ /// @note If the media resides in a subfolder include it. (eg. home-myfiles\\home-myfiles2.png).
+ /// You can use the above as keywords for arguments.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// exists = xbmc.skinHasImage('ButtonFocusedTexture.png')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ skinHasImage(...);
+#else
+ bool skinHasImage(const char* image);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmc
+ ///
+ /// @brief \python_func{ xbmc.startServer(typ, bStart, bWait) }
+ /// Start or stop a server.
+ ///
+ /// @param typ integer - use SERVER_* constants
+ /// - Used format of the returned language string
+ /// | Value | Description |
+ /// |--------------------------:|------------------------------------------------------------|
+ /// | xbmc.SERVER_WEBSERVER | [To control Kodi's builtin webserver](http://kodi.wiki/view/Webserver)
+ /// | xbmc.SERVER_AIRPLAYSERVER | [AirPlay is a proprietary protocol stack/suite developed by Apple Inc.](http://kodi.wiki/view/AirPlay)
+ /// | xbmc.SERVER_JSONRPCSERVER | [Control JSON-RPC HTTP/TCP socket-based interface](http://kodi.wiki/view/JSON-RPC_API)
+ /// | xbmc.SERVER_UPNPRENDERER | [UPnP client (aka UPnP renderer)](http://kodi.wiki/view/UPnP/Client)
+ /// | xbmc.SERVER_UPNPSERVER | [Control built-in UPnP A/V media server (UPnP-server)](http://kodi.wiki/view/UPnP/Server)
+ /// | xbmc.SERVER_EVENTSERVER | [Set eventServer part that accepts remote device input on all platforms](http://kodi.wiki/view/EventServer)
+ /// | xbmc.SERVER_ZEROCONF | [Control Kodi's Avahi Zeroconf](http://kodi.wiki/view/Zeroconf)
+ /// @param bStart bool - start (True) or stop (False) a server
+ /// @return bool - True or False
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v20 Removed option **bWait**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.startServer(xbmc.SERVER_AIRPLAYSERVER, False)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ startServer(...);
+#else
+ bool startServer(int iTyp, bool bStart);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.audioSuspend() }
+ /// Suspend Audio engine.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.audioSuspend()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ audioSuspend();
+#else
+ void audioSuspend();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.audioResume() }
+ /// Resume Audio engine.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.audioResume()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ audioResume();
+#else
+ void audioResume();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.getUserAgent() }
+ /// @brief Returns Kodi's HTTP UserAgent string
+ ///
+ /// @return HTTP user agent
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmc.getUserAgent()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ /// example output:
+ /// Kodi/17.0-ALPHA1 (X11; Linux x86_64) Ubuntu/15.10 App_Bitness/64 Version/17.0-ALPHA1-Git:2015-12-23-5770d28
+ ///
+ getUserAgent();
+#else
+ String getUserAgent();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc
+ /// @brief \python_func{ xbmc.convertLanguage(language, format) }
+ /// @brief Returns the given language converted to the given format as a
+ /// string.
+ ///
+ /// @param language string either as name in English, two
+ /// letter code (ISO 639-1), or three
+ /// letter code (ISO 639-2/T(B)
+ /// @param format format of the returned language string
+ /// | Value | Description
+ /// |------------------:|:-------------------------------------------------|
+ /// | xbmc.ISO_639_1 | Two letter code as defined in ISO 639-1
+ /// | xbmc.ISO_639_2 | Three letter code as defined in ISO 639-2/T or ISO 639-2/B
+ /// | xbmc.ENGLISH_NAME | Full language name in English (default)
+ /// @return Converted Language string
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v13 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// language = xbmc.convertLanguage(English, xbmc.ISO_639_2)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ convertLanguage(...);
+#else
+ String convertLanguage(const char* language, int format);
+#endif
+ //@}
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+ SWIG_CONSTANT_FROM_GETTER(int, SERVER_WEBSERVER);
+ SWIG_CONSTANT_FROM_GETTER(int, SERVER_AIRPLAYSERVER);
+ SWIG_CONSTANT_FROM_GETTER(int, SERVER_UPNPSERVER);
+ SWIG_CONSTANT_FROM_GETTER(int, SERVER_UPNPRENDERER);
+ SWIG_CONSTANT_FROM_GETTER(int, SERVER_EVENTSERVER);
+ SWIG_CONSTANT_FROM_GETTER(int, SERVER_JSONRPCSERVER);
+ SWIG_CONSTANT_FROM_GETTER(int, SERVER_ZEROCONF);
+
+ SWIG_CONSTANT_FROM_GETTER(int, PLAYLIST_MUSIC);
+ SWIG_CONSTANT_FROM_GETTER(int, PLAYLIST_VIDEO);
+ SWIG_CONSTANT_FROM_GETTER(int, TRAY_OPEN);
+ SWIG_CONSTANT_FROM_GETTER(int, DRIVE_NOT_READY);
+ SWIG_CONSTANT_FROM_GETTER(int, TRAY_CLOSED_NO_MEDIA);
+ SWIG_CONSTANT_FROM_GETTER(int, TRAY_CLOSED_MEDIA_PRESENT);
+ SWIG_CONSTANT_FROM_GETTER(int, LOGDEBUG);
+ SWIG_CONSTANT_FROM_GETTER(int, LOGINFO);
+ SWIG_CONSTANT_FROM_GETTER(int, LOGWARNING);
+ SWIG_CONSTANT_FROM_GETTER(int, LOGERROR);
+ SWIG_CONSTANT_FROM_GETTER(int, LOGFATAL);
+ SWIG_CONSTANT_FROM_GETTER(int, LOGNONE);
+
+ SWIG_CONSTANT_FROM_GETTER(int, ISO_639_1);
+ SWIG_CONSTANT_FROM_GETTER(int, ISO_639_2);
+ SWIG_CONSTANT_FROM_GETTER(int, ENGLISH_NAME);
+#if 0
+ void registerMonitor(Monitor* monitor);
+ void unregisterMonitor(Monitor* monitor);
+#endif
+ }
+}
+#endif /* DOXYGEN_SHOULD_SKIP_THIS */
diff --git a/xbmc/interfaces/legacy/ModuleXbmcgui.cpp b/xbmc/interfaces/legacy/ModuleXbmcgui.cpp
new file mode 100644
index 0000000..104a755
--- /dev/null
+++ b/xbmc/interfaces/legacy/ModuleXbmcgui.cpp
@@ -0,0 +1,58 @@
+/*
+ * 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 "ModuleXbmcgui.h"
+
+#include "LanguageHook.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "windowing/GraphicContext.h"
+
+#include <mutex>
+
+#define NOTIFICATION_INFO "info"
+#define NOTIFICATION_WARNING "warning"
+#define NOTIFICATION_ERROR "error"
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ long getCurrentWindowId()
+ {
+ DelayedCallGuard dg;
+ std::unique_lock<CCriticalSection> gl(CServiceBroker::GetWinSystem()->GetGfxContext());
+ return CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ }
+
+ long getCurrentWindowDialogId()
+ {
+ DelayedCallGuard dg;
+ std::unique_lock<CCriticalSection> gl(CServiceBroker::GetWinSystem()->GetGfxContext());
+ return CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog();
+ }
+
+ long getScreenHeight()
+ {
+ XBMC_TRACE;
+ return CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight();
+ }
+
+ long getScreenWidth()
+ {
+ XBMC_TRACE;
+ return CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth();
+ }
+
+ const char* getNOTIFICATION_INFO() { return NOTIFICATION_INFO; }
+ const char* getNOTIFICATION_WARNING() { return NOTIFICATION_WARNING; }
+ const char* getNOTIFICATION_ERROR() { return NOTIFICATION_ERROR; }
+
+ }
+}
diff --git a/xbmc/interfaces/legacy/ModuleXbmcgui.h b/xbmc/interfaces/legacy/ModuleXbmcgui.h
new file mode 100644
index 0000000..d2d9558
--- /dev/null
+++ b/xbmc/interfaces/legacy/ModuleXbmcgui.h
@@ -0,0 +1,149 @@
+/*
+ * 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 "guilib/GUIEditControl.h"
+#include "swighelper.h"
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+#endif /* DOXYGEN_SHOULD_SKIP_THIS */
+
+ //
+ /// \defgroup python_xbmcgui Library - xbmcgui
+ /// @{
+ /// @brief **GUI functions on Kodi.**
+ ///
+ /// Offers classes and functions that manipulate the Graphical User
+ /// Interface through windows, dialogs, and various control widgets.
+ //
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui
+ /// @brief \python_func{ xbmcgui.getCurrentWindowId() }
+ /// Returns the id for the current 'active' window as an integer.
+ ///
+ /// @return The currently active window Id
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// wid = xbmcgui.getCurrentWindowId()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getCurrentWindowId();
+#else
+ long getCurrentWindowId();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui
+ /// @brief \python_func{ xbmcgui.getCurrentWindowDialogId() }
+ /// Returns the id for the current 'active' dialog as an integer.
+ ///
+ /// @return The currently active dialog Id
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// wid = xbmcgui.getCurrentWindowDialogId()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getCurrentWindowDialogId();
+#else
+ long getCurrentWindowDialogId();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui
+ /// @brief \python_func{ getScreenHeight() }
+ /// Returns the height of this screen.
+ ///
+ /// @return Screen height
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ getScreenHeight();
+#else
+ long getScreenHeight();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui
+ /// @brief \python_func{ getScreenWidth() }
+ /// Returns the width of this screen.
+ ///
+ /// @return Screen width
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ getScreenWidth();
+#else
+ long getScreenWidth();
+#endif
+ ///@}
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+ SWIG_CONSTANT2(int, ICON_OVERLAY_NONE, CGUIListItem::ICON_OVERLAY_NONE);
+ SWIG_CONSTANT2(int, ICON_OVERLAY_RAR, CGUIListItem::ICON_OVERLAY_RAR);
+ SWIG_CONSTANT2(int, ICON_OVERLAY_ZIP, CGUIListItem::ICON_OVERLAY_ZIP);
+ SWIG_CONSTANT2(int, ICON_OVERLAY_LOCKED, CGUIListItem::ICON_OVERLAY_LOCKED);
+ SWIG_CONSTANT2(int, ICON_OVERLAY_UNWATCHED, CGUIListItem::ICON_OVERLAY_UNWATCHED);
+ SWIG_CONSTANT2(int, ICON_OVERLAY_WATCHED, CGUIListItem::ICON_OVERLAY_WATCHED);
+ SWIG_CONSTANT2(int, ICON_OVERLAY_HD, CGUIListItem::ICON_OVERLAY_HD);
+
+ SWIG_CONSTANT2(int, INPUT_TYPE_TEXT, CGUIEditControl::INPUT_TYPE_TEXT);
+ SWIG_CONSTANT2(int, INPUT_TYPE_NUMBER, CGUIEditControl::INPUT_TYPE_NUMBER);
+ SWIG_CONSTANT2(int, INPUT_TYPE_DATE, CGUIEditControl::INPUT_TYPE_DATE);
+ SWIG_CONSTANT2(int, INPUT_TYPE_TIME, CGUIEditControl::INPUT_TYPE_TIME);
+ SWIG_CONSTANT2(int, INPUT_TYPE_IPADDRESS, CGUIEditControl::INPUT_TYPE_IPADDRESS);
+ SWIG_CONSTANT2(int, INPUT_TYPE_PASSWORD, CGUIEditControl::INPUT_TYPE_PASSWORD);
+ SWIG_CONSTANT2(int, INPUT_TYPE_PASSWORD_MD5, CGUIEditControl::INPUT_TYPE_PASSWORD_MD5);
+ SWIG_CONSTANT2(int, INPUT_TYPE_SECONDS, CGUIEditControl::INPUT_TYPE_SECONDS);
+ SWIG_CONSTANT2(int, INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW, CGUIEditControl::INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW);
+
+ SWIG_CONSTANT_FROM_GETTER(const char*, NOTIFICATION_INFO);
+ SWIG_CONSTANT_FROM_GETTER(const char*, NOTIFICATION_WARNING);
+ SWIG_CONSTANT_FROM_GETTER(const char*, NOTIFICATION_ERROR);
+
+ SWIG_CONSTANT(int, INPUT_ALPHANUM);
+ SWIG_CONSTANT(int, INPUT_NUMERIC);
+ SWIG_CONSTANT(int, INPUT_DATE);
+ SWIG_CONSTANT(int, INPUT_TIME);
+ SWIG_CONSTANT(int, INPUT_IPADDRESS);
+ SWIG_CONSTANT(int, INPUT_PASSWORD);
+
+ SWIG_CONSTANT(int, HORIZONTAL);
+ SWIG_CONSTANT(int, VERTICAL);
+
+ SWIG_CONSTANT(int, PASSWORD_VERIFY);
+ SWIG_CONSTANT(int, ALPHANUM_HIDE_INPUT);
+
+ }
+}
+#endif /* DOXYGEN_SHOULD_SKIP_THIS */
diff --git a/xbmc/interfaces/legacy/ModuleXbmcplugin.cpp b/xbmc/interfaces/legacy/ModuleXbmcplugin.cpp
new file mode 100644
index 0000000..0af97b7
--- /dev/null
+++ b/xbmc/interfaces/legacy/ModuleXbmcplugin.cpp
@@ -0,0 +1,125 @@
+/*
+ * 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 "ModuleXbmcplugin.h"
+
+#include "FileItem.h"
+#include "filesystem/PluginDirectory.h"
+
+namespace XBMCAddon
+{
+
+ namespace xbmcplugin
+ {
+ bool addDirectoryItem(int handle, const String& url, const xbmcgui::ListItem* listItem,
+ bool isFolder, int totalItems)
+ {
+ if (listItem == nullptr)
+ throw new XBMCAddon::WrongTypeException("None not allowed as argument for listitem");
+ AddonClass::Ref<xbmcgui::ListItem> pListItem(listItem);
+ pListItem->item->SetPath(url);
+ pListItem->item->m_bIsFolder = isFolder;
+
+ // call the directory class to add our item
+ return XFILE::CPluginDirectory::AddItem(handle, pListItem->item.get(), totalItems);
+ }
+
+ bool addDirectoryItems(int handle,
+ const std::vector<Tuple<String,const XBMCAddon::xbmcgui::ListItem*,bool> >& items,
+ int totalItems)
+ {
+ CFileItemList fitems;
+ for (const auto& item : items)
+ {
+ const String& url = item.first();
+ const XBMCAddon::xbmcgui::ListItem* pListItem = item.second();
+ bool bIsFolder = item.GetNumValuesSet() > 2 ? item.third() : false;
+ pListItem->item->SetPath(url);
+ pListItem->item->m_bIsFolder = bIsFolder;
+ fitems.Add(pListItem->item);
+ }
+
+ // call the directory class to add our items
+ return XFILE::CPluginDirectory::AddItems(handle, &fitems, totalItems);
+ }
+
+ void endOfDirectory(int handle, bool succeeded, bool updateListing,
+ bool cacheToDisc)
+ {
+ // tell the directory class that we're done
+ XFILE::CPluginDirectory::EndOfDirectory(handle, succeeded, updateListing, cacheToDisc);
+ }
+
+ void setResolvedUrl(int handle, bool succeeded, const xbmcgui::ListItem* listItem)
+ {
+ if (listItem == nullptr)
+ throw new XBMCAddon::WrongTypeException("None not allowed as argument for listitem");
+ AddonClass::Ref<xbmcgui::ListItem> pListItem(listItem);
+ XFILE::CPluginDirectory::SetResolvedUrl(handle, succeeded, pListItem->item.get());
+ }
+
+ void addSortMethod(int handle, int sortMethod, const String& clabelMask, const String& clabel2Mask)
+ {
+ String labelMask;
+ if (sortMethod == SORT_METHOD_TRACKNUM)
+ labelMask = (clabelMask.empty() ? "[%N. ]%T" : clabelMask.c_str());
+ else if (sortMethod == SORT_METHOD_EPISODE || sortMethod == SORT_METHOD_PRODUCTIONCODE)
+ labelMask = (clabelMask.empty() ? "%H. %T" : clabelMask.c_str());
+ else
+ labelMask = (clabelMask.empty() ? "%T" : clabelMask.c_str());
+
+ String label2Mask;
+ label2Mask = (clabel2Mask.empty() ? "%D" : clabel2Mask.c_str());
+
+ // call the directory class to add the sort method.
+ if (sortMethod >= SORT_METHOD_NONE && sortMethod < SORT_METHOD_MAX)
+ XFILE::CPluginDirectory::AddSortMethod(handle, (SORT_METHOD)sortMethod, labelMask, label2Mask);
+ }
+
+ String getSetting(int handle, const char* id)
+ {
+ return XFILE::CPluginDirectory::GetSetting(handle, id);
+ }
+
+ void setSetting(int handle, const String& id, const String& value)
+ {
+ XFILE::CPluginDirectory::SetSetting(handle, id, value);
+ }
+
+ void setContent(int handle, const char* content)
+ {
+ XFILE::CPluginDirectory::SetContent(handle, content);
+ }
+
+ void setPluginCategory(int handle, const String& category)
+ {
+ XFILE::CPluginDirectory::SetProperty(handle, "plugincategory", category);
+ }
+
+ void setPluginFanart(int handle, const char* image,
+ const char* color1,
+ const char* color2,
+ const char* color3)
+ {
+ if (image)
+ XFILE::CPluginDirectory::SetProperty(handle, "fanart_image", image);
+ if (color1)
+ XFILE::CPluginDirectory::SetProperty(handle, "fanart_color1", color1);
+ if (color2)
+ XFILE::CPluginDirectory::SetProperty(handle, "fanart_color2", color2);
+ if (color3)
+ XFILE::CPluginDirectory::SetProperty(handle, "fanart_color3", color3);
+ }
+
+ void setProperty(int handle, const char* key, const String& value)
+ {
+ XFILE::CPluginDirectory::SetProperty(handle, key, value);
+ }
+
+ }
+}
diff --git a/xbmc/interfaces/legacy/ModuleXbmcplugin.h b/xbmc/interfaces/legacy/ModuleXbmcplugin.h
new file mode 100644
index 0000000..1aa458c
--- /dev/null
+++ b/xbmc/interfaces/legacy/ModuleXbmcplugin.h
@@ -0,0 +1,489 @@
+/*
+ * 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 "AddonString.h"
+#include "ListItem.h"
+#include "Tuple.h"
+#include "swighelper.h"
+
+#include <vector>
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+namespace XBMCAddon
+{
+ namespace xbmcplugin
+ {
+#endif /* DOXYGEN_SHOULD_SKIP_THIS */
+
+ //
+ /// \defgroup python_xbmcplugin Library - xbmcplugin
+ /// @{
+ /// @brief <b>Plugin functions on Kodi.</b>
+ ///
+ /// Offers classes and functions that allow a developer to present
+ /// information through Kodi's standard menu structure. While plugins don't
+ /// have the same flexibility as scripts, they boast significantly quicker
+ /// development time and a more consistent user experience.
+ //
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcplugin
+ /// @brief \python_func{ xbmcplugin.addDirectoryItem(handle, url, listitem [,isFolder, totalItems]) }
+ /// Callback function to pass directory contents back to Kodi.
+ ///
+ /// @param handle integer - handle the plugin was started
+ /// with.
+ /// @param url string - url of the entry. would be
+ /// `plugin://` for another virtual directory
+ /// @param listitem ListItem - item to add.
+ /// @param isFolder [opt] bool - True=folder / False=not a
+ /// folder(default).
+ /// @param totalItems [opt] integer - total number of items
+ /// that will be passed.(used for progressbar)
+ /// @return Returns a bool for successful completion.
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain
+ /// optional arguments. Once you use a keyword, all following arguments
+ /// require the keyword.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// if not xbmcplugin.addDirectoryItem(int(sys.argv[1]), 'F:\\Trailers\\300.mov', listitem, totalItems=50): break
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ addDirectoryItem(...);
+#else
+ bool addDirectoryItem(int handle, const String& url, const XBMCAddon::xbmcgui::ListItem* listitem,
+ bool isFolder = false, int totalItems = 0);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcplugin
+ /// @brief \python_func{ xbmcplugin.addDirectoryItems(handle, items[, totalItems]) }
+ /// Callback function to pass directory contents back to Kodi as a list.
+ ///
+ /// @param handle integer - handle the plugin was started
+ /// with.
+ /// @param items List - list of (url, listitem[, isFolder])
+ /// as a tuple to add.
+ /// @param totalItems [opt] integer - total number of items
+ /// that will be passed.(used for progressbar)
+ /// @return Returns a bool for successful completion.
+ ///
+ /// @remark Large lists benefit over using the standard addDirectoryItem().
+ /// You may call this more than once to add items in chunks.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// if not xbmcplugin.addDirectoryItems(int(sys.argv[1]), [(url, listitem, False,)]: raise
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ addDirectoryItems(...);
+#else
+ bool addDirectoryItems(int handle,
+ const std::vector<Tuple<String,const XBMCAddon::xbmcgui::ListItem*,bool> >& items,
+ int totalItems = 0);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcplugin
+ /// @brief \python_func{ xbmcplugin.endOfDirectory(handle[, succeeded, updateListing, cacheToDisc]) }
+ /// Callback function to tell Kodi that the end of the directory listing in
+ /// a virtualPythonFolder module is reached.
+ ///
+ /// @param handle integer - handle the plugin was started
+ /// with.
+ /// @param succeeded [opt] bool - True=script completed
+ /// successfully(Default)/False=Script did not.
+ /// @param updateListing [opt] bool - True=this folder should
+ /// update the current listing/False=Folder
+ /// is a subfolder(Default).
+ /// @param cacheToDisc [opt] bool - True=Folder will cache if
+ /// extended time(default)/False=this folder
+ /// will never cache to disc.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=False)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ endOfDirectory(...);
+#else
+ void endOfDirectory(int handle, bool succeeded = true, bool updateListing = false,
+ bool cacheToDisc = true);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcplugin
+ /// @brief \python_func{ xbmcplugin.setResolvedUrl(handle, succeeded, listitem) }
+ /// Callback function to tell Kodi that the file plugin has been resolved to
+ /// a url
+ ///
+ /// @param handle integer - handle the plugin was started
+ /// with.
+ /// @param succeeded bool - True=script completed
+ /// successfully/False=Script did not.
+ /// @param listitem ListItem - item the file plugin resolved
+ /// to for playback.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setResolvedUrl(...);
+#else
+ void setResolvedUrl(int handle, bool succeeded, const XBMCAddon::xbmcgui::ListItem* listitem);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcplugin
+ /// @brief \python_func{ xbmcplugin.addSortMethod(handle, sortMethod [,labelMask, label2Mask]) }
+ ///-------------------------------------------------------------------------
+ /// Adds a sorting method for the media list.
+ ///
+ /// @param handle integer - handle the plugin was started
+ /// with.
+ /// @param sortMethod integer - see available sort methods at
+ /// the bottom (or see \ref List_of_sort_methods "SortUtils").
+ /// | Value | Description |
+ /// |----------------------------------------------|-----------------------|
+ /// | xbmcplugin.SORT_METHOD_NONE | Do not sort
+ /// | xbmcplugin.SORT_METHOD_LABEL | Sort by label
+ /// | xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE | Sort by the label and ignore "The" before
+ /// | xbmcplugin.SORT_METHOD_DATE | Sort by the date
+ /// | xbmcplugin.SORT_METHOD_SIZE | Sort by the size
+ /// | xbmcplugin.SORT_METHOD_FILE | Sort by the file
+ /// | xbmcplugin.SORT_METHOD_DRIVE_TYPE | Sort by the drive type
+ /// | xbmcplugin.SORT_METHOD_TRACKNUM | Sort by the track number
+ /// | xbmcplugin.SORT_METHOD_DURATION | Sort by the duration
+ /// | xbmcplugin.SORT_METHOD_TITLE | Sort by the title
+ /// | xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE | Sort by the title and ignore "The" before
+ /// | xbmcplugin.SORT_METHOD_ARTIST | Sort by the artist
+ /// | xbmcplugin.SORT_METHOD_ARTIST_IGNORE_THE | Sort by the artist and ignore "The" before
+ /// | xbmcplugin.SORT_METHOD_ALBUM | Sort by the album
+ /// | xbmcplugin.SORT_METHOD_ALBUM_IGNORE_THE | Sort by the album and ignore "The" before
+ /// | xbmcplugin.SORT_METHOD_GENRE | Sort by the genre
+ /// | xbmcplugin.SORT_SORT_METHOD_VIDEO_YEAR, xbmcplugin.SORT_METHOD_YEAR | Sort by the year
+ /// | xbmcplugin.SORT_METHOD_VIDEO_RATING | Sort by the video rating
+ /// | xbmcplugin.SORT_METHOD_PROGRAM_COUNT | Sort by the program count
+ /// | xbmcplugin.SORT_METHOD_PLAYLIST_ORDER | Sort by the playlist order
+ /// | xbmcplugin.SORT_METHOD_EPISODE | Sort by the episode
+ /// | xbmcplugin.SORT_METHOD_VIDEO_TITLE | Sort by the video title
+ /// | xbmcplugin.SORT_METHOD_VIDEO_SORT_TITLE | Sort by the video sort title
+ /// | xbmcplugin.SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE | Sort by the video sort title and ignore "The" before
+ /// | xbmcplugin.SORT_METHOD_PRODUCTIONCODE | Sort by the production code
+ /// | xbmcplugin.SORT_METHOD_SONG_RATING | Sort by the song rating
+ /// | xbmcplugin.SORT_METHOD_MPAA_RATING | Sort by the mpaa rating
+ /// | xbmcplugin.SORT_METHOD_VIDEO_RUNTIME | Sort by video runtime
+ /// | xbmcplugin.SORT_METHOD_STUDIO | Sort by the studio
+ /// | xbmcplugin.SORT_METHOD_STUDIO_IGNORE_THE | Sort by the studio and ignore "The" before
+ /// | xbmcplugin.SORT_METHOD_UNSORTED | Use list not sorted
+ /// | xbmcplugin.SORT_METHOD_BITRATE | Sort by the bitrate
+ /// | xbmcplugin.SORT_METHOD_LISTENERS | Sort by the listeners
+ /// | xbmcplugin.SORT_METHOD_COUNTRY | Sort by the country
+ /// | xbmcplugin.SORT_METHOD_DATEADDED | Sort by the added date
+ /// | xbmcplugin.SORT_METHOD_FULLPATH | Sort by the full path name
+ /// | xbmcplugin.SORT_METHOD_LABEL_IGNORE_FOLDERS | Sort by the label names and ignore related folder names
+ /// | xbmcplugin.SORT_METHOD_LASTPLAYED | Sort by last played date
+ /// | xbmcplugin.SORT_METHOD_PLAYCOUNT | Sort by the play count
+ /// | xbmcplugin.SORT_METHOD_CHANNEL | Sort by the channel
+ /// | xbmcplugin.SORT_METHOD_DATE_TAKEN | Sort by the taken date
+ /// | xbmcplugin.SORT_METHOD_VIDEO_USER_RATING | Sort by the rating of the user of video
+ /// | xbmcplugin.SORT_METHOD_SONG_USER_RATING | Sort by the rating of the user of song
+ /// @param labelMask [opt] string - the label mask to use for
+ /// the first label.
+ /// - applies to:
+ /// | sortMethod | labelMask |
+ /// |---------------------------------------|-----------------------------|
+ /// | SORT_METHOD_TRACKNUM | Defaults to `[%%N. ]%%T` |
+ /// | SORT_METHOD_EPISODE | Defaults to `%%H. %%T` |
+ /// | SORT_METHOD_PRODUCTIONCODE | Defaults to `%%H. %%T` |
+ /// | All other sort methods | Defaults to `%%T` |
+ ///
+ ///
+ /// @param label2Mask [opt] string - the label mask to use for
+ /// the second label. Defaults to `%%D`
+ /// - applies to:
+ /// | | | |
+ /// |-----------------------------------------|-----------------------------|-----------------------------------------|
+ /// | SORT_METHOD_NONE | SORT_METHOD_UNSORTED | SORT_METHOD_VIDEO_TITLE |
+ /// | SORT_METHOD_TRACKNUM | SORT_METHOD_FILE | SORT_METHOD_TITLE |
+ /// | SORT_METHOD_TITLE_IGNORE_THE | SORT_METHOD_LABEL | SORT_METHOD_LABEL_IGNORE_THE |
+ /// | SORT_METHOD_VIDEO_SORT_TITLE | SORT_METHOD_FULLPATH | SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE |
+ /// | SORT_METHOD_LABEL_IGNORE_FOLDERS | SORT_METHOD_CHANNEL | |
+ /// @note to add multiple sort methods just call the method multiple times.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v13 Added new sort **SORT_METHOD_DATE_TAKEN**, **SORT_METHOD_COUNTRY**,
+ /// **SORT_METHOD_DATEADDED**, **SORT_METHOD_FULLPATH**, **SORT_METHOD_LABEL_IGNORE_FOLDERS**,
+ /// **SORT_METHOD_LASTPLAYED**, **SORT_METHOD_PLAYCOUNT**, **SORT_METHOD_CHANNEL**.
+ /// @python_v17 Added new sort **SORT_METHOD_VIDEO_USER_RATING**.
+ /// @python_v19 Added new option **labelMask**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORTMETHOD_DATEADDED)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ addSortMethod(...);
+#else
+ void addSortMethod(int handle, int sortMethod, const String& labelMask = emptyString, const String& label2Mask = emptyString);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcplugin
+ /// @brief \python_func{ xbmcplugin.getSetting(handle, id) }
+ /// Returns the value of a setting as a string.
+ ///
+ /// @param handle integer - handle the plugin was started
+ /// with.
+ /// @param id string - id of the setting that the
+ /// module needs to access.
+ /// @return Setting value as string
+ ///
+ /// @note You can use the above as a keyword.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// apikey = xbmcplugin.getSetting(int(sys.argv[1]), 'apikey')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getSetting(...);
+#else
+ String getSetting(int handle, const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcplugin
+ /// @brief \python_func{ xbmcplugin.setSetting(handle, id, value) }
+ /// Sets a plugin setting for the current running plugin.
+ ///
+ /// @param handle integer - handle the plugin was started with.
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param value string or unicode - value of the setting.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmcplugin.setSetting(int(sys.argv[1]), id='username', value='teamxbmc')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setSetting(...);
+#else
+ void setSetting(int handle, const String& id, const String& value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcplugin
+ /// @brief \python_func{ xbmcplugin.setContent(handle, content) }
+ /// Sets the plugins content.
+ ///
+ /// @param handle integer - handle the plugin was started with.
+ /// @param content string - content type (eg. movies)
+ ///
+ /// @par Available content strings
+ /// | | | | |
+ /// |:--------:|:--------:|:--------:|:-----------:|
+ /// | files | songs | artists | albums |
+ /// | movies | tvshows | episodes | musicvideos |
+ /// | videos | images | games | -- |
+ ///
+ /// @remark Use **videos** for all videos which do not apply to the
+ /// more specific mentioned ones like "movies", "episodes" etc.
+ /// A good example is youtube.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v18 Added new **games** content
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmcplugin.setContent(int(sys.argv[1]), 'movies')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setContent(...);
+#else
+ void setContent(int handle, const char* content);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcplugin
+ /// @brief \python_func{ xbmcplugin.setPluginCategory(handle, category) }
+ /// Sets the plugins name for skins to display.
+ ///
+ /// @param handle integer - handle the plugin was started with.
+ /// @param category string or unicode - plugins sub category.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmcplugin.setPluginCategory(int(sys.argv[1]), 'Comedy')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setPluginCategory(...);
+#else
+ void setPluginCategory(int handle, const String& category);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcplugin
+ /// @brief \python_func{ xbmcplugin.setPluginFanart(handle, image, color1, color2, color3) }
+ /// Sets the plugins fanart and color for skins to display.
+ ///
+ /// @param handle integer - handle the plugin was started with.
+ /// @param image [opt] string - path to fanart image.
+ /// @param color1 [opt] hexstring - color1. (e.g. '0xFFFFFFFF')
+ /// @param color2 [opt] hexstring - color2. (e.g. '0xFFFF3300')
+ /// @param color3 [opt] hexstring - color3. (e.g. '0xFF000000')
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmcplugin.setPluginFanart(int(sys.argv[1]), 'special://home/addons/plugins/video/Apple movie trailers II/fanart.png', color2='0xFFFF3300')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setPluginFanart(...);
+#else
+ void setPluginFanart(int handle, const char* image = NULL,
+ const char* color1 = NULL,
+ const char* color2 = NULL,
+ const char* color3 = NULL);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcplugin
+ /// @brief \python_func{ xbmcplugin.setProperty(handle, key, value) }
+ /// Sets a container property for this plugin.
+ ///
+ /// @param handle integer - handle the plugin was started with.
+ /// @param key string - property name.
+ /// @param value string or unicode - value of property.
+ ///
+ /// @note Key is NOT case sensitive.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmcplugin.setProperty(int(sys.argv[1]), 'Emulator', 'M.A.M.E.')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setProperty(...);
+ ///@}
+#else
+ void setProperty(int handle, const char* key, const String& value);
+#endif
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+ SWIG_CONSTANT(int, SORT_METHOD_NONE);
+ SWIG_CONSTANT(int, SORT_METHOD_LABEL);
+ SWIG_CONSTANT(int, SORT_METHOD_LABEL_IGNORE_THE);
+ SWIG_CONSTANT(int, SORT_METHOD_DATE);
+ SWIG_CONSTANT(int, SORT_METHOD_SIZE);
+ SWIG_CONSTANT(int, SORT_METHOD_FILE);
+ SWIG_CONSTANT(int, SORT_METHOD_DRIVE_TYPE);
+ SWIG_CONSTANT(int, SORT_METHOD_TRACKNUM);
+ SWIG_CONSTANT(int, SORT_METHOD_DURATION);
+ SWIG_CONSTANT(int, SORT_METHOD_TITLE);
+ SWIG_CONSTANT(int, SORT_METHOD_TITLE_IGNORE_THE);
+ SWIG_CONSTANT(int, SORT_METHOD_ARTIST);
+ SWIG_CONSTANT(int, SORT_METHOD_ARTIST_IGNORE_THE);
+ SWIG_CONSTANT(int, SORT_METHOD_ALBUM);
+ SWIG_CONSTANT(int, SORT_METHOD_ALBUM_IGNORE_THE);
+ SWIG_CONSTANT(int, SORT_METHOD_GENRE);
+ SWIG_CONSTANT2(int, SORT_METHOD_VIDEO_YEAR,SORT_METHOD_YEAR);
+ SWIG_CONSTANT(int, SORT_METHOD_VIDEO_RATING);
+ SWIG_CONSTANT(int, SORT_METHOD_PROGRAM_COUNT);
+ SWIG_CONSTANT(int, SORT_METHOD_PLAYLIST_ORDER);
+ SWIG_CONSTANT(int, SORT_METHOD_EPISODE);
+ SWIG_CONSTANT(int, SORT_METHOD_VIDEO_TITLE);
+ SWIG_CONSTANT(int, SORT_METHOD_VIDEO_SORT_TITLE);
+ SWIG_CONSTANT(int, SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE);
+ SWIG_CONSTANT(int, SORT_METHOD_VIDEO_ORIGINAL_TITLE);
+ SWIG_CONSTANT(int, SORT_METHOD_VIDEO_ORIGINAL_TITLE_IGNORE_THE);
+ SWIG_CONSTANT(int, SORT_METHOD_PRODUCTIONCODE);
+ SWIG_CONSTANT(int, SORT_METHOD_SONG_RATING);
+ SWIG_CONSTANT(int, SORT_METHOD_MPAA_RATING);
+ SWIG_CONSTANT(int, SORT_METHOD_VIDEO_RUNTIME);
+ SWIG_CONSTANT(int, SORT_METHOD_STUDIO);
+ SWIG_CONSTANT(int, SORT_METHOD_STUDIO_IGNORE_THE);
+ SWIG_CONSTANT(int, SORT_METHOD_UNSORTED);
+ SWIG_CONSTANT(int, SORT_METHOD_BITRATE);
+ SWIG_CONSTANT(int, SORT_METHOD_LISTENERS);
+ SWIG_CONSTANT(int, SORT_METHOD_COUNTRY);
+ SWIG_CONSTANT(int, SORT_METHOD_DATEADDED);
+ SWIG_CONSTANT(int, SORT_METHOD_FULLPATH);
+ SWIG_CONSTANT(int, SORT_METHOD_LABEL_IGNORE_FOLDERS);
+ SWIG_CONSTANT(int, SORT_METHOD_LASTPLAYED);
+ SWIG_CONSTANT(int, SORT_METHOD_PLAYCOUNT);
+ SWIG_CONSTANT(int, SORT_METHOD_CHANNEL);
+ SWIG_CONSTANT(int, SORT_METHOD_DATE_TAKEN);
+ SWIG_CONSTANT(int, SORT_METHOD_VIDEO_USER_RATING);
+ SWIG_CONSTANT(int, SORT_METHOD_SONG_USER_RATING);
+ }
+}
+#endif /* DOXYGEN_SHOULD_SKIP_THIS */
diff --git a/xbmc/interfaces/legacy/ModuleXbmcvfs.cpp b/xbmc/interfaces/legacy/ModuleXbmcvfs.cpp
new file mode 100644
index 0000000..c6858e3
--- /dev/null
+++ b/xbmc/interfaces/legacy/ModuleXbmcvfs.cpp
@@ -0,0 +1,137 @@
+/*
+ * 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 "ModuleXbmcvfs.h"
+
+#include "FileItem.h"
+#include "LanguageHook.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+
+namespace XBMCAddon
+{
+
+ namespace xbmcvfs
+ {
+ bool copy(const String& strSource, const String& strDestination)
+ {
+ DelayedCallGuard dg;
+ return XFILE::CFile::Copy(strSource, strDestination);
+ }
+
+ // delete a file
+ bool deleteFile(const String& strSource)
+ {
+ DelayedCallGuard dg;
+ return XFILE::CFile::Delete(strSource);
+ }
+
+ // rename a file
+ bool rename(const String& file, const String& newFile)
+ {
+ DelayedCallGuard dg;
+ return XFILE::CFile::Rename(file,newFile);
+ }
+
+ // check for a file or folder existence, mimics Pythons os.path.exists()
+ bool exists(const String& path)
+ {
+ DelayedCallGuard dg;
+ if (URIUtils::HasSlashAtEnd(path, true))
+ return XFILE::CDirectory::Exists(path, false);
+ return XFILE::CFile::Exists(path, false);
+ }
+
+ // make legal file name
+ String makeLegalFilename(const String& filename)
+ {
+ XBMC_TRACE;
+ return CUtil::MakeLegalPath(filename);
+ }
+
+ // translate path
+ String translatePath(const String& path)
+ {
+ XBMC_TRACE;
+ return CSpecialProtocol::TranslatePath(path);
+ }
+
+ // validate path
+ String validatePath(const String& path)
+ {
+ XBMC_TRACE;
+ return CUtil::ValidatePath(path, true);
+ }
+
+ // make a directory
+ bool mkdir(const String& path)
+ {
+ DelayedCallGuard dg;
+ return XFILE::CDirectory::Create(path);
+ }
+
+ // make all directories along the path
+ bool mkdirs(const String& path)
+ {
+ DelayedCallGuard dg;
+ return CUtil::CreateDirectoryEx(path);
+ }
+
+ bool rmdir(const String& path, bool force)
+ {
+ DelayedCallGuard dg;
+
+ if (force)
+ return CFileUtils::DeleteItem(path);
+ else
+ return XFILE::CDirectory::Remove(path);
+ }
+
+ Tuple<std::vector<String>, std::vector<String> > listdir(const String& path)
+ {
+ DelayedCallGuard dg;
+ CFileItemList items;
+ std::string strSource;
+ strSource = path;
+ XFILE::CDirectory::GetDirectory(strSource, items, "", XFILE::DIR_FLAG_NO_FILE_DIRS);
+
+ Tuple<std::vector<String>, std::vector<String> > ret;
+ // initialize the Tuple to two values
+ ret.second();
+
+ for (int i=0; i < items.Size(); i++)
+ {
+ std::string itemPath = items[i]->GetPath();
+
+ if (URIUtils::HasSlashAtEnd(itemPath)) // folder
+ {
+ URIUtils::RemoveSlashAtEnd(itemPath);
+ std::string strFileName = URIUtils::GetFileName(itemPath);
+ if (strFileName.empty())
+ {
+ CURL url(itemPath);
+ strFileName = url.GetHostName();
+ }
+ ret.first().push_back(strFileName);
+ }
+ else // file
+ {
+ std::string strFileName = URIUtils::GetFileName(itemPath);
+ ret.second().push_back(strFileName);
+ }
+ }
+
+ return ret;
+ }
+ }
+}
diff --git a/xbmc/interfaces/legacy/ModuleXbmcvfs.h b/xbmc/interfaces/legacy/ModuleXbmcvfs.h
new file mode 100644
index 0000000..30e768f
--- /dev/null
+++ b/xbmc/interfaces/legacy/ModuleXbmcvfs.h
@@ -0,0 +1,334 @@
+/*
+ * 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 "AddonString.h"
+#include "Tuple.h"
+
+#include <vector>
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+namespace XBMCAddon
+{
+ namespace xbmcvfs
+ {
+#endif /* DOXYGEN_SHOULD_SKIP_THIS */
+
+ //
+ /// \defgroup python_xbmcvfs Library - xbmcvfs
+ /// @{
+ /// @brief **Virtual file system functions on Kodi.**
+ ///
+ /// Offers classes and functions offers access to the Virtual File Server
+ /// (VFS) which you can use to manipulate files and folders.
+ //
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcvfs
+ /// @brief \python_func{ xbmcvfs.copy(source, destination) }
+ /// Copy file to destination, returns true/false.
+ ///
+ /// @param source file to copy.
+ /// @param destination destination file
+ /// @return True if successed
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// success = xbmcvfs.copy(source, destination)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ copy(...);
+#else
+ bool copy(const String& strSource, const String& strDestination);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcvfs
+ /// @brief \python_func{ xbmcvfs.delete(file) }
+ /// Delete a file
+ ///
+ /// @param file File to delete
+ /// @return True if successed
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// xbmcvfs.delete(file)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ delete(...);
+#else
+ bool deleteFile(const String& file);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcvfs
+ /// @brief \python_func{ xbmcvfs.rename(file, newFileName) }
+ /// Rename a file
+ ///
+ /// @param file File to rename
+ /// @param newFileName New filename, including the full path
+ /// @return True if successed
+ ///
+ /// @note Moving files between different filesystem (eg. local to nfs://) is not possible on
+ /// all platforms. You may have to do it manually by using the copy and deleteFile functions.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// success = xbmcvfs.rename(file,newFileName)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ rename(...);
+#else
+ bool rename(const String& file, const String& newFile);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcvfs
+ /// @brief \python_func{ xbmcvfs.exists(path) }
+ /// Check for a file or folder existence
+ ///
+ /// @param path File or folder (folder must end with
+ /// slash or backslash)
+ /// @return True if successed
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// success = xbmcvfs.exists(path)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ exists(...);
+#else
+ bool exists(const String& path);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcvfs
+ /// @brief \python_func{ xbmcvfs.makeLegalFilename(filename) }
+ /// Returns a legal filename or path as a string.
+ ///
+ /// @param filename string - filename/path to make legal
+ /// @return Legal filename or path as a string
+ ///
+ ///
+ /// @note The returned value is platform-specific. This is due to the fact that
+ /// the chars that need to be replaced to make a path legal depend on the
+ /// underlying OS filesystem. This is useful, for example, if you want to create
+ /// a file or folder based on data over which you have no control (e.g. an external API).
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ /// @python_v19 New function added (replaces old **xbmc.makeLegalFilename**)
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// # windows
+ /// >> xbmcvfs.makeLegalFilename('C://Trailers/Ice Age: The Meltdown.avi')
+ /// C:\Trailers\Ice Age_ The Meltdown.avi
+ /// # non-windows
+ /// >> xbmcvfs.makeLegalFilename("///\\jk???lj????.mpg")
+ /// /jk___lj____.mpg
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ makeLegalFilename(...);
+#else
+ String makeLegalFilename(const String& filename);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcvfs
+ /// @brief \python_func{ xbmcvfs.translatePath(path) }
+ /// Returns the translated path.
+ ///
+ /// @param path string - Path to format
+ /// @return Translated path
+ ///
+ /// @note Only useful if you are coding for both Linux and Windows.
+ /// e.g. Converts 'special://home' -> '/home/[username]/.kodi'
+ /// on Linux.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v19 New function added (replaces old **xbmc.translatePath**)
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// fpath = xbmcvfs.translatePath('special://home')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ translatePath(...);
+#else
+ String translatePath(const String& path);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcvfs
+ /// @brief \python_func{ xbmcvfs.validatePath(path) }
+ /// Returns the validated path.
+ ///
+ /// @param path string - Path to format
+ /// @return Validated path
+ ///
+ /// @note The result is platform-specific. Only useful if you are coding
+ /// for multiple platfforms for fixing slash problems
+ /// (e.g. Corrects 'Z://something' -> 'Z:\something').
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v19 New function added (replaces old **xbmc.validatePath**)
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// fpath = xbmcvfs.validatePath(somepath)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ validatePath(...);
+#else
+ String validatePath(const String& path);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcvfs
+ /// @brief \python_func{ xbmcvfs.mkdir(path) }
+ /// Create a folder.
+ ///
+ /// @param path Folder to create
+ /// @return True if successed
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// success = xbmcvfs.mkdir(path)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ mkdir(...);
+#else
+ bool mkdir(const String& path);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcvfs
+ /// @brief \python_func{ xbmcvfs.mkdirs(path) }
+ /// Make all directories along the path
+ ///
+ /// Create folder(s) - it will create all folders in the path.
+ ///
+ /// @param path Folders to create
+ /// @return True if successed
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// success = xbmcvfs.mkdirs(path)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ mkdirs(...);
+#else
+ bool mkdirs(const String& path);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcvfs
+ /// @brief \python_func{ xbmcvfs.rmdir(path, [force]) }
+ /// Remove a folder.
+ ///
+ /// @param path string - Folder to remove
+ /// @param force [opt] bool - Force directory removal
+ /// (default False). This can be useful
+ /// if the directory is not empty.
+ /// @return bool - True if successful, False
+ /// otherwise
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// success = xbmcvfs.rmdir(path)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ rmdir(...);
+#else
+ bool rmdir(const String& path, bool force = false);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcvfs
+ /// @brief \python_func{ xbmcvfs.listdir(path) }
+ /// Lists content of a folder.
+ ///
+ /// @param path Folder to get list from
+ /// @return Directory content list
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dirs, files = xbmcvfs.listdir(path)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ listdir(...);
+#else
+ Tuple<std::vector<String>, std::vector<String> > listdir(const String& path);
+#endif
+ //@}
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+ }
+}
+#endif /* DOXYGEN_SHOULD_SKIP_THIS */
diff --git a/xbmc/interfaces/legacy/Monitor.cpp b/xbmc/interfaces/legacy/Monitor.cpp
new file mode 100644
index 0000000..34d0553
--- /dev/null
+++ b/xbmc/interfaces/legacy/Monitor.cpp
@@ -0,0 +1,82 @@
+/*
+ * 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 "Monitor.h"
+
+#include "threads/SystemClock.h"
+
+#include <algorithm>
+#include <math.h>
+
+using namespace std::chrono_literals;
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ Monitor::Monitor(): abortEvent(true)
+ {
+ XBMC_TRACE;
+ if (languageHook)
+ {
+ Id = languageHook->GetAddonId();
+ invokerId = languageHook->GetInvokerId();
+ languageHook->RegisterMonitorCallback(this);
+ }
+ }
+
+ void Monitor::AbortNotify()
+ {
+ XBMC_TRACE;
+ abortEvent.Set();
+ }
+
+ bool Monitor::waitForAbort(double timeout)
+ {
+ XBMC_TRACE;
+ int timeoutMS = ceil(timeout * 1000);
+ XbmcThreads::EndTime<> endTime{std::chrono::milliseconds(timeoutMS)};
+
+ if (timeoutMS <= 0)
+ endTime.SetInfinite();
+
+ while (!endTime.IsTimePast())
+ {
+ {
+ DelayedCallGuard dg(languageHook);
+ auto timeout = std::min(endTime.GetTimeLeft(), 100ms);
+ if (abortEvent.Wait(timeout))
+ return true;
+ }
+ if (languageHook)
+ languageHook->MakePendingCalls();
+ }
+ return false;
+ }
+
+ bool Monitor::abortRequested()
+ {
+ XBMC_TRACE;
+ return abortEvent.Signaled();
+ }
+
+ Monitor::~Monitor()
+ {
+ XBMC_TRACE;
+ deallocating();
+ DelayedCallGuard dg(languageHook);
+ // we're shutting down so unregister me.
+ if (languageHook)
+ {
+ DelayedCallGuard dc;
+ languageHook->UnregisterMonitorCallback(this);
+ }
+ }
+ }
+}
+
diff --git a/xbmc/interfaces/legacy/Monitor.h b/xbmc/interfaces/legacy/Monitor.h
new file mode 100644
index 0000000..0d3d831
--- /dev/null
+++ b/xbmc/interfaces/legacy/Monitor.h
@@ -0,0 +1,311 @@
+/*
+ * 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 "AddonCallback.h"
+#include "AddonString.h"
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+
+ ///
+ /// \ingroup python_xbmc
+ /// \defgroup python_monitor Monitor
+ /// @{
+ /// @brief **Kodi's monitor class.**
+ ///
+ /// \python_class{ xbmc.Monitor() }
+ ///
+ /// Creates a new monitor to notify addon about changes.
+ ///
+ class Monitor : public AddonCallback
+ {
+ String Id;
+ long invokerId;
+ CEvent abortEvent;
+ public:
+ Monitor();
+
+#ifndef SWIG
+ inline void OnSettingsChanged() { XBMC_TRACE; invokeCallback(new CallbackFunction<Monitor>(this,&Monitor::onSettingsChanged)); }
+ inline void OnScreensaverActivated() { XBMC_TRACE; invokeCallback(new CallbackFunction<Monitor>(this,&Monitor::onScreensaverActivated)); }
+ inline void OnScreensaverDeactivated() { XBMC_TRACE; invokeCallback(new CallbackFunction<Monitor>(this,&Monitor::onScreensaverDeactivated)); }
+ inline void OnDPMSActivated() { XBMC_TRACE; invokeCallback(new CallbackFunction<Monitor>(this,&Monitor::onDPMSActivated)); }
+ inline void OnDPMSDeactivated() { XBMC_TRACE; invokeCallback(new CallbackFunction<Monitor>(this,&Monitor::onDPMSDeactivated)); }
+ inline void OnScanStarted(const String &library)
+ {
+ XBMC_TRACE;
+ invokeCallback(
+ new CallbackFunction<Monitor, const String>(this, &Monitor::onScanStarted, library));
+ }
+ inline void OnScanFinished(const String &library)
+ {
+ XBMC_TRACE;
+ invokeCallback(
+ new CallbackFunction<Monitor, const String>(this, &Monitor::onScanFinished, library));
+ }
+ inline void OnCleanStarted(const String& library)
+ {
+ XBMC_TRACE;
+ invokeCallback(
+ new CallbackFunction<Monitor, const String>(this, &Monitor::onCleanStarted, library));
+ }
+ inline void OnCleanFinished(const String& library)
+ {
+ XBMC_TRACE;
+ invokeCallback(
+ new CallbackFunction<Monitor, const String>(this, &Monitor::onCleanFinished, library));
+ }
+ inline void OnNotification(const String& sender, const String& method, const String& data)
+ {
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Monitor, const String, const String, const String>(
+ this, &Monitor::onNotification, sender, method, data));
+ }
+
+ inline const String& GetId() { return Id; }
+ inline long GetInvokerId() { return invokerId; }
+
+ /**
+ * Called from XBPython to notify registered monitors that a script is aborting/ending.
+ */
+ void AbortNotify();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_monitor
+ /// @brief \python_func{ onSettingsChanged() }
+ /// onSettingsChanged method.
+ ///
+ /// Will be called when addon settings are changed
+ ///
+ onSettingsChanged();
+#else
+ virtual void onSettingsChanged() { XBMC_TRACE; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_monitor
+ /// @brief \python_func{ onScreensaverActivated() }
+ /// onScreensaverActivated method.
+ ///
+ /// Will be called when screensaver kicks in
+ ///
+ onScreensaverActivated();
+#else
+ virtual void onScreensaverActivated() { XBMC_TRACE; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_monitor
+ /// @brief \python_func{ onScreensaverDeactivated() }
+ /// onScreensaverDeactivated method.
+ ///
+ /// Will be called when screensaver goes off
+ ///
+ onScreensaverDeactivated();
+#else
+ virtual void onScreensaverDeactivated() { XBMC_TRACE; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_monitor
+ /// @brief \python_func{ onDPMSActivated() }
+ /// onDPMSActivated method.
+ ///
+ /// Will be called when energysaving/DPMS gets active
+ ///
+ onDPMSActivated();
+#else
+ virtual void onDPMSActivated() { XBMC_TRACE; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_monitor
+ /// @brief \python_func{ onDPMSDeactivated() }
+ /// onDPMSDeactivated method.
+ ///
+ /// Will be called when energysaving/DPMS is turned off
+ ///
+ onDPMSDeactivated();
+#else
+ virtual void onDPMSDeactivated() { XBMC_TRACE; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_monitor
+ /// @brief \python_func{ onScanStarted(library) }
+ /// onScanStarted method.
+ ///
+ /// @param library Video / music as string
+ ///
+ ///
+ /// @note Will be called when library clean has ended and return video or
+ /// music to indicate which library is being scanned
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v14 New function added.
+ ///
+ onScanStarted(...);
+#else
+ virtual void onScanStarted(const String library) { XBMC_TRACE; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_monitor
+ /// @brief \python_func{ onScanFinished(library) }
+ /// onScanFinished method.
+ ///
+ /// @param library Video / music as string
+ ///
+ ///
+ /// @note Will be called when library clean has ended and return video or
+ /// music to indicate which library has been scanned
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v14 New function added.
+ ///
+ onScanFinished(...);
+#else
+ virtual void onScanFinished(const String library) { XBMC_TRACE; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_monitor
+ /// @brief \python_func{ onCleanStarted(library) }
+ /// onCleanStarted method.
+ ///
+ /// @param library Video / music as string
+ ///
+ ///
+ /// @note Will be called when library clean has ended and return video or
+ /// music to indicate which library has been cleaned
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v14 New function added.
+ ///
+ onCleanStarted(...);
+#else
+ virtual void onCleanStarted(const String library) { XBMC_TRACE; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_monitor
+ /// @brief \python_func{ onCleanFinished(library) }
+ /// onCleanFinished method.
+ ///
+ /// @param library Video / music as string
+ ///
+ ///
+ /// @note Will be called when library clean has ended and return video or
+ /// music to indicate which library has been finished
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v14 New function added.
+ ///
+ onCleanFinished(...);
+#else
+ virtual void onCleanFinished(const String library) { XBMC_TRACE; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_monitor
+ /// @brief \python_func{ onNotification(sender, method, data) }
+ /// onNotification method.
+ ///
+ /// @param sender Sender of the notification
+ /// @param method Name of the notification
+ /// @param data JSON-encoded data of the notification
+ ///
+ /// @note Will be called when Kodi receives or sends a notification
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v13 New function added.
+ ///
+ onNotification(...);
+#else
+ virtual void onNotification(const String sender, const String method, const String data)
+ {
+ XBMC_TRACE;
+ }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_monitor
+ /// @brief \python_func{ waitForAbort([timeout]) }
+ /// Wait for Abort
+ /// \anchor xbmc_Monitor_waitForAbort
+ ///
+ /// Block until abort is requested, or until timeout occurs. If an
+ /// abort requested have already been made, return immediately.
+ ///
+ /// @param timeout [opt] float - timeout in seconds.
+ /// Default: no timeout.
+ ///
+ /// @return True when abort have been requested,
+ /// False if a timeout is given and the
+ /// operation times out.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v14 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// monitor = xbmc.Monitor()
+ /// # do something
+ /// monitor.waitForAbort(10) # sleeps for 10 secs or returns early if kodi aborts
+ /// if monitor.abortRequested():
+ /// # abort was requested to Kodi (e.g. shutdown), do your cleanup logic
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ waitForAbort(...);
+#else
+ bool waitForAbort(double timeout = -1);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_monitor
+ /// @brief \python_func{ abortRequested() }
+ /// Returns True if abort has been requested.
+ ///
+ /// @return True if requested
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v14 New function added.
+ ///
+ abortRequested();
+#else
+ bool abortRequested();
+#endif
+ ~Monitor() override;
+ };
+ /** @} */
+ }
+};
diff --git a/xbmc/interfaces/legacy/PlayList.cpp b/xbmc/interfaces/legacy/PlayList.cpp
new file mode 100644
index 0000000..1117a81
--- /dev/null
+++ b/xbmc/interfaces/legacy/PlayList.cpp
@@ -0,0 +1,144 @@
+/*
+ * 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 "PlayList.h"
+
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "playlists/PlayListFactory.h"
+#include "utils/URIUtils.h"
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ //! @todo need a means to check for a valid construction
+ //! either by throwing an exception or by an "isValid" check
+ PlayList::PlayList(int playList) :
+ iPlayList(playList), pPlayList(NULL)
+ {
+ // we do not create our own playlist, just using the ones from playlistplayer
+ if (iPlayList != PLAYLIST::TYPE_MUSIC && iPlayList != PLAYLIST::TYPE_VIDEO)
+ throw PlayListException("PlayList does not exist");
+
+ pPlayList = &CServiceBroker::GetPlaylistPlayer().GetPlaylist(playList);
+ iPlayList = playList;
+ }
+
+ PlayList::~PlayList() = default;
+
+ void PlayList::add(const String& url, XBMCAddon::xbmcgui::ListItem* listitem, int index)
+ {
+ CFileItemList items;
+
+ if (listitem != NULL)
+ {
+ // an optional listitem was passed
+ // set m_strPath to the passed url
+ listitem->item->SetPath(url);
+
+ items.Add(listitem->item);
+ }
+ else
+ {
+ CFileItemPtr item(new CFileItem(url, false));
+ item->SetLabel(url);
+
+ items.Add(item);
+ }
+
+ pPlayList->Insert(items, index);
+ }
+
+ bool PlayList::load(const char* cFileName)
+ {
+ CFileItem item(cFileName);
+ item.SetPath(cFileName);
+
+ if (item.IsPlayList())
+ {
+ // load playlist and copy al items to existing playlist
+
+ // load a playlist like .m3u, .pls
+ // first get correct factory to load playlist
+ std::unique_ptr<PLAYLIST::CPlayList> pPlayList(PLAYLIST::CPlayListFactory::Create(item));
+ if (nullptr != pPlayList)
+ {
+ // load it
+ if (!pPlayList->Load(item.GetPath()))
+ //hmmm unable to load playlist?
+ return false;
+
+ // clear current playlist
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(this->iPlayList);
+
+ // add each item of the playlist to the playlistplayer
+ for (int i=0; i < pPlayList->size(); ++i)
+ {
+ CFileItemPtr playListItem =(*pPlayList)[i];
+ if (playListItem->GetLabel().empty())
+ playListItem->SetLabel(URIUtils::GetFileName(playListItem->GetPath()));
+
+ this->pPlayList->Add(playListItem);
+ }
+ }
+ }
+ else
+ // filename is not a valid playlist
+ throw PlayListException("Not a valid playlist");
+
+ return true;
+ }
+
+ void PlayList::remove(const char* filename)
+ {
+ pPlayList->Remove(filename);
+ }
+
+ void PlayList::clear()
+ {
+ pPlayList->Clear();
+ }
+
+ int PlayList::size()
+ {
+ return pPlayList->size();
+ }
+
+ void PlayList::shuffle()
+ {
+ pPlayList->Shuffle();
+ }
+
+ void PlayList::unshuffle()
+ {
+ pPlayList->UnShuffle();
+ }
+
+ int PlayList::getposition()
+ {
+ return CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ }
+
+ XBMCAddon::xbmcgui::ListItem* PlayList::operator [](long i)
+ {
+ int iPlayListSize = size();
+
+ long pos = i;
+ if (pos < 0) pos += iPlayListSize;
+
+ if (pos < 0 || pos >= iPlayListSize)
+ throw PlayListException("array out of bound");
+
+ CFileItemPtr ptr((*pPlayList)[pos]);
+
+ return new XBMCAddon::xbmcgui::ListItem(ptr);
+ }
+ }
+}
+
diff --git a/xbmc/interfaces/legacy/PlayList.h b/xbmc/interfaces/legacy/PlayList.h
new file mode 100644
index 0000000..6162945
--- /dev/null
+++ b/xbmc/interfaces/legacy/PlayList.h
@@ -0,0 +1,212 @@
+/*
+ * 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 "AddonClass.h"
+#include "Exception.h"
+#include "ListItem.h"
+#include "playlists/PlayList.h"
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ XBMCCOMMONS_STANDARD_EXCEPTION(PlayListException);
+
+ //
+ /// \defgroup python_PlayList PlayList
+ /// \ingroup python_xbmc
+ /// @{
+ /// @brief **Kodi's Play List class.**
+ ///
+ /// \python_class{ xbmc.PlayList(playList) }
+ ///
+ /// To create and edit a playlist which can be handled by the player.
+ ///
+ /// @param playList [integer] To define the stream type
+ /// | Value | Integer String | Description |
+ /// |:-----:|:--------------------|:---------------------------------------|
+ /// | 0 | xbmc.PLAYLIST_MUSIC | Playlist for music files or streams |
+ /// | 1 | xbmc.PLAYLIST_VIDEO | Playlist for video files or streams |
+ ///
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// play=xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ //
+ class PlayList : public AddonClass
+ {
+ int iPlayList;
+ PLAYLIST::CPlayList *pPlayList;
+
+ public:
+ explicit PlayList(int playList);
+ ~PlayList() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayList
+ /// @brief \python_func{ getPlayListId() }
+ /// Get the PlayList Identifier
+ ///
+ /// @return Id as an integer.
+ ///
+ getPlayListId();
+#else
+ inline int getPlayListId() const { return iPlayList; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayList
+ /// @brief \python_func{ add(url[, listitem, index]) }
+ /// Adds a new file to the playlist.
+ ///
+ /// @param url string or unicode - filename or url to add.
+ /// @param listitem [opt] listitem - used with setInfo() to set different infolabels.
+ /// @param index [opt] integer - position to add playlist item. (default=end)
+ ///
+ /// @note You can use the above as keywords for arguments and skip certain optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
+ /// video = 'F:\\movies\\Ironman.mov'
+ /// listitem = xbmcgui.ListItem('Ironman', thumbnailImage='F:\\movies\\Ironman.tbn')
+ /// listitem.setInfo('video', {'Title': 'Ironman', 'Genre': 'Science Fiction'})
+ /// playlist.add(url=video, listitem=listitem, index=7)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ add(...);
+#else
+ void add(const String& url, XBMCAddon::xbmcgui::ListItem* listitem = NULL, int index = -1);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayList
+ /// @brief \python_func{ load(filename) }
+ /// Load a playlist.
+ ///
+ /// Clear current playlist and copy items from the file to this Playlist
+ /// filename can be like .pls or .m3u ...
+ ///
+ /// @param filename File with list to play inside
+ /// @return False if unable to load playlist
+ ///
+ load(...);
+#else
+ bool load(const char* filename);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayList
+ /// @brief \python_func{ remove(filename) }
+ /// Remove an item with this filename from the playlist.
+ ///
+ /// @param filename The file to remove from list.
+ ///
+ remove(...);
+#else
+ void remove(const char* filename);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayList
+ /// @brief \python_func{ clear() }
+ /// Clear all items in the playlist.
+ ///
+ clear();
+#else
+ void clear();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayList
+ /// @brief \python_func{ size() }
+ /// Returns the total number of PlayListItems in this playlist.
+ ///
+ /// @return Amount of playlist entries.
+ ///
+ size();
+#else
+ int size();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayList
+ /// @brief \python_func{ shuffle() }
+ /// Shuffle the playlist.
+ ///
+ shuffle();
+#else
+ void shuffle();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayList
+ /// @brief \python_func{ unshuffle() }
+ /// Unshuffle the playlist.
+ ///
+ unshuffle();
+#else
+ void unshuffle();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayList
+ /// @brief \python_func{ getposition() }
+ /// Returns the position of the current song in this playlist.
+ ///
+ /// @return Position of the current song
+ ///
+ getposition();
+#else
+ int getposition();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayList
+ /// @brief \python_func{ [] }
+ /// Retrieve the item at the given position.
+ ///
+ /// @param i Pointer in list
+ /// @return The selected item on list
+ ///
+ /// @note A negative index means
+ /// from the end rather than from the start.
+ ///
+ [](...);
+#else
+ XBMCAddon::xbmcgui::ListItem* operator[](long i);
+#endif
+ };
+ /// @}
+ }
+}
+
diff --git a/xbmc/interfaces/legacy/Player.cpp b/xbmc/interfaces/legacy/Player.cpp
new file mode 100644
index 0000000..cf982ba
--- /dev/null
+++ b/xbmc/interfaces/legacy/Player.cpp
@@ -0,0 +1,607 @@
+/*
+ * 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 "Player.h"
+
+#include "AddonUtils.h"
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "ListItem.h"
+#include "PlayList.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/IPlayer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/MediaSettings.h"
+
+namespace
+{
+
+std::shared_ptr<const CApplicationPlayer> getAppPlayer()
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ auto res = components.GetComponent<CApplicationPlayer>();
+ return res;
+}
+
+std::shared_ptr<CApplicationPlayer> getAppPlayerMut()
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ auto res = components.GetComponent<CApplicationPlayer>();
+ return res;
+}
+
+} // namespace
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ PlayParameter Player::defaultPlayParameter;
+
+ Player::Player()
+ {
+ iPlayList = PLAYLIST::TYPE_MUSIC;
+
+ // now that we're done, register hook me into the system
+ if (languageHook)
+ {
+ DelayedCallGuard dc(languageHook);
+ languageHook->RegisterPlayerCallback(this);
+ }
+ }
+
+ Player::~Player()
+ {
+ deallocating();
+
+ // we're shutting down so unregister me.
+ if (languageHook)
+ {
+ DelayedCallGuard dc(languageHook);
+ languageHook->UnregisterPlayerCallback(this);
+ }
+ }
+
+ void Player::play(const Alternative<String, const PlayList* > & item,
+ const XBMCAddon::xbmcgui::ListItem* listitem, bool windowed, int startpos)
+ {
+ XBMC_TRACE;
+
+ if (&item == &defaultPlayParameter)
+ playCurrent(windowed);
+ else if (item.which() == XBMCAddon::first)
+ playStream(item.former(), listitem, windowed);
+ else // item is a PlayListItem
+ playPlaylist(item.later(),windowed,startpos);
+ }
+
+ void Player::playStream(const String& item, const xbmcgui::ListItem* plistitem, bool windowed)
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dc(languageHook);
+ if (!item.empty())
+ {
+ // set fullscreen or windowed
+ CMediaSettings::GetInstance().SetMediaStartWindowed(windowed);
+
+ const AddonClass::Ref<xbmcgui::ListItem> listitem(plistitem);
+
+ if (listitem.isSet())
+ {
+ // set m_strPath to the passed url
+ listitem->item->SetPath(item.c_str());
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_MEDIA_PLAY, 0, 0, static_cast<void*>(new CFileItem(*listitem->item)));
+ }
+ else
+ {
+ CFileItemList *l = new CFileItemList; //don't delete,
+ l->Add(std::make_shared<CFileItem>(item, false));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, -1, -1,
+ static_cast<void*>(l));
+ }
+ }
+ else
+ playCurrent(windowed);
+ }
+
+ void Player::playCurrent(bool windowed)
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dc(languageHook);
+ // set fullscreen or windowed
+ CMediaSettings::GetInstance().SetMediaStartWindowed(windowed);
+
+ // play current file in playlist
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() != iPlayList)
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(iPlayList);
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_PLAYLISTPLAYER_PLAY, CServiceBroker::GetPlaylistPlayer().GetCurrentSong());
+ }
+
+ void Player::playPlaylist(const PlayList* playlist, bool windowed, int startpos)
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dc(languageHook);
+ if (playlist != NULL)
+ {
+ // set fullscreen or windowed
+ CMediaSettings::GetInstance().SetMediaStartWindowed(windowed);
+
+ // play a python playlist (a playlist from playlistplayer.cpp)
+ iPlayList = playlist->getPlayListId();
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(iPlayList);
+ if (startpos > -1)
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(startpos);
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_PLAY, startpos);
+ }
+ else
+ playCurrent(windowed);
+ }
+
+ void Player::stop()
+ {
+ XBMC_TRACE;
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ }
+
+ void Player::pause()
+ {
+ XBMC_TRACE;
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+
+ void Player::playnext()
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dc(languageHook);
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_NEXT);
+ }
+
+ void Player::playprevious()
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dc(languageHook);
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_PREV);
+ }
+
+ void Player::playselected(int selected)
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dc(languageHook);
+
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() != iPlayList)
+ {
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(iPlayList);
+ }
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(selected);
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_PLAY, selected);
+ //CServiceBroker::GetPlaylistPlayer().Play(selected);
+ //CLog::Log(LOGINFO, "Current Song After Play: {}", CServiceBroker::GetPlaylistPlayer().GetCurrentSong());
+ }
+
+ void Player::OnPlayBackStarted(const CFileItem &file)
+ {
+ // We only have fileItem due to us having to
+ // implement the interface, we can't send it to python
+ // as we're not able to serialize it.
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player>(this, &Player::onPlayBackStarted));
+ }
+
+ void Player::OnAVStarted(const CFileItem &file)
+ {
+ // We only have fileItem due to us having to
+ // implement the interface, we can't send it to python
+ // as we're not able to serialize it.
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player>(this, &Player::onAVStarted));
+ }
+
+ void Player::OnAVChange()
+ {
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player>(this, &Player::onAVChange));
+ }
+
+ void Player::OnPlayBackEnded()
+ {
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player>(this,&Player::onPlayBackEnded));
+ }
+
+ void Player::OnPlayBackStopped()
+ {
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player>(this,&Player::onPlayBackStopped));
+ }
+
+ void Player::OnPlayBackError()
+ {
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player>(this,&Player::onPlayBackError));
+ }
+
+ void Player::OnPlayBackPaused()
+ {
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player>(this,&Player::onPlayBackPaused));
+ }
+
+ void Player::OnPlayBackResumed()
+ {
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player>(this,&Player::onPlayBackResumed));
+ }
+
+ void Player::OnQueueNextItem()
+ {
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player>(this,&Player::onQueueNextItem));
+ }
+
+ void Player::OnPlayBackSpeedChanged(int speed)
+ {
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player,int>(this,&Player::onPlayBackSpeedChanged,speed));
+ }
+
+ void Player::OnPlayBackSeek(int64_t time, int64_t seekOffset)
+ {
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player,int,int>(this,&Player::onPlayBackSeek,static_cast<int>(time),static_cast<int>(seekOffset)));
+ }
+
+ void Player::OnPlayBackSeekChapter(int chapter)
+ {
+ XBMC_TRACE;
+ invokeCallback(new CallbackFunction<Player,int>(this,&Player::onPlayBackSeekChapter,chapter));
+ }
+
+ void Player::onPlayBackStarted()
+ {
+ XBMC_TRACE;
+ }
+
+ void Player::onAVStarted()
+ {
+ XBMC_TRACE;
+ }
+
+ void Player::onAVChange()
+ {
+ XBMC_TRACE;
+ }
+
+ void Player::onPlayBackEnded()
+ {
+ XBMC_TRACE;
+ }
+
+ void Player::onPlayBackStopped()
+ {
+ XBMC_TRACE;
+ }
+
+ void Player::onPlayBackError()
+ {
+ XBMC_TRACE;
+ }
+
+ void Player::onPlayBackPaused()
+ {
+ XBMC_TRACE;
+ }
+
+ void Player::onPlayBackResumed()
+ {
+ XBMC_TRACE;
+ }
+
+ void Player::onQueueNextItem()
+ {
+ XBMC_TRACE;
+ }
+
+ void Player::onPlayBackSpeedChanged(int speed)
+ {
+ XBMC_TRACE;
+ }
+
+ void Player::onPlayBackSeek(int time, int seekOffset)
+ {
+ XBMC_TRACE;
+ }
+
+ void Player::onPlayBackSeekChapter(int chapter)
+ {
+ XBMC_TRACE;
+ }
+
+ bool Player::isPlaying()
+ {
+ XBMC_TRACE;
+ return getAppPlayer()->IsPlaying();
+ }
+
+ bool Player::isPlayingAudio()
+ {
+ XBMC_TRACE;
+ return getAppPlayer()->IsPlayingAudio();
+ }
+
+ bool Player::isPlayingVideo()
+ {
+ XBMC_TRACE;
+ return getAppPlayer()->IsPlayingVideo();
+ }
+
+ bool Player::isPlayingRDS()
+ {
+ XBMC_TRACE;
+ return getAppPlayer()->IsPlayingRDS();
+ }
+
+ bool Player::isPlayingGame()
+ {
+ XBMC_TRACE;
+ return getAppPlayer()->IsPlayingGame();
+ }
+
+ bool Player::isExternalPlayer()
+ {
+ XBMC_TRACE;
+ return getAppPlayer()->IsExternalPlaying();
+ }
+
+ String Player::getPlayingFile()
+ {
+ XBMC_TRACE;
+ if (!getAppPlayer()->IsPlaying())
+ throw PlayerException("Kodi is not playing any file");
+
+ return g_application.CurrentFileItem().GetDynPath();
+ }
+
+ XBMCAddon::xbmcgui::ListItem* Player::getPlayingItem()
+ {
+ XBMC_TRACE;
+ if (!getAppPlayer()->IsPlaying())
+ throw PlayerException("Kodi is not playing any item");
+
+ CFileItemPtr itemPtr = std::make_shared<CFileItem>(g_application.CurrentFileItem());
+ return new XBMCAddon::xbmcgui::ListItem(itemPtr);
+ }
+
+ InfoTagVideo* Player::getVideoInfoTag()
+ {
+ XBMC_TRACE;
+ if (!getAppPlayer()->IsPlayingVideo())
+ throw PlayerException("Kodi is not playing any videofile");
+
+ const CVideoInfoTag* movie = CServiceBroker::GetGUI()->GetInfoManager().GetCurrentMovieTag();
+ if (movie)
+ return new InfoTagVideo(movie);
+
+ return new InfoTagVideo(true);
+ }
+
+ InfoTagMusic* Player::getMusicInfoTag()
+ {
+ XBMC_TRACE;
+ if (getAppPlayer()->IsPlayingVideo() || !getAppPlayer()->IsPlayingAudio())
+ throw PlayerException("Kodi is not playing any music file");
+
+ const MUSIC_INFO::CMusicInfoTag* tag = CServiceBroker::GetGUI()->GetInfoManager().GetCurrentSongTag();
+ if (tag)
+ return new InfoTagMusic(tag);
+
+ return new InfoTagMusic(true);
+ }
+
+ void Player::updateInfoTag(const XBMCAddon::xbmcgui::ListItem* item)
+ {
+ XBMC_TRACE;
+ if (!getAppPlayer()->IsPlaying())
+ throw PlayerException("Kodi is not playing any file");
+
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, item->item);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+
+ InfoTagGame* Player::getGameInfoTag()
+ {
+ XBMC_TRACE;
+ if (!getAppPlayer()->IsPlayingGame())
+ throw PlayerException("Kodi is not playing any game file");
+
+ const KODI::GAME::CGameInfoTag* game =
+ CServiceBroker::GetGUI()->GetInfoManager().GetCurrentGameTag();
+ if (game)
+ return new InfoTagGame(game);
+
+ return new InfoTagGame();
+ }
+
+ InfoTagRadioRDS* Player::getRadioRDSInfoTag()
+ {
+ XBMC_TRACE;
+ if (getAppPlayer()->IsPlayingVideo() || !getAppPlayer()->IsPlayingRDS())
+ throw PlayerException("Kodi is not playing any music file with RDS");
+
+ std::shared_ptr<CFileItem> item = g_application.CurrentFileItemPtr();
+ if (item && item->HasPVRChannelInfoTag())
+ return new InfoTagRadioRDS(item->GetPVRChannelInfoTag());
+
+ return new InfoTagRadioRDS();
+ }
+
+ double Player::getTotalTime()
+ {
+ XBMC_TRACE;
+ if (!getAppPlayer()->IsPlaying())
+ throw PlayerException("Kodi is not playing any media file");
+
+ return g_application.GetTotalTime();
+ }
+
+ double Player::getTime()
+ {
+ XBMC_TRACE;
+ if (!getAppPlayer()->IsPlaying())
+ throw PlayerException("Kodi is not playing any media file");
+
+ return g_application.GetTime();
+ }
+
+ void Player::seekTime(double pTime)
+ {
+ XBMC_TRACE;
+ if (!getAppPlayer()->IsPlaying())
+ throw PlayerException("Kodi is not playing any media file");
+
+ g_application.SeekTime( pTime );
+ }
+
+ void Player::setSubtitles(const char* cLine)
+ {
+ XBMC_TRACE;
+ if (getAppPlayer()->HasPlayer())
+ {
+ getAppPlayerMut()->AddSubtitle(cLine);
+ }
+ }
+
+ void Player::showSubtitles(bool bVisible)
+ {
+ XBMC_TRACE;
+ if (getAppPlayer()->HasPlayer())
+ {
+ getAppPlayerMut()->SetSubtitleVisible(bVisible != 0);
+ }
+ }
+
+ String Player::getSubtitles()
+ {
+ XBMC_TRACE;
+ if (getAppPlayer()->HasPlayer())
+ {
+ SubtitleStreamInfo info;
+ getAppPlayerMut()->GetSubtitleStreamInfo(CURRENT_STREAM, info);
+
+ if (info.language.length() > 0)
+ return info.language;
+ else
+ return info.name;
+ }
+
+ return "";
+ }
+
+ std::vector<String> Player::getAvailableSubtitleStreams()
+ {
+ if (getAppPlayer()->HasPlayer())
+ {
+ int subtitleCount = getAppPlayer()->GetSubtitleCount();
+ std::vector<String> ret(subtitleCount);
+ for (int iStream=0; iStream < subtitleCount; iStream++)
+ {
+ SubtitleStreamInfo info;
+ getAppPlayer()->GetSubtitleStreamInfo(iStream, info);
+
+ if (info.language.length() > 0)
+ ret[iStream] = info.language;
+ else
+ ret[iStream] = info.name;
+ }
+ return ret;
+ }
+
+ return std::vector<String>();
+ }
+
+ void Player::setSubtitleStream(int iStream)
+ {
+ if (getAppPlayer()->HasPlayer())
+ {
+ int streamCount = getAppPlayer()->GetSubtitleCount();
+ if(iStream < streamCount)
+ {
+ getAppPlayerMut()->SetSubtitle(iStream);
+ getAppPlayerMut()->SetSubtitleVisible(true);
+ }
+ }
+ }
+
+ std::vector<String> Player::getAvailableAudioStreams()
+ {
+ if (getAppPlayer()->HasPlayer())
+ {
+ int streamCount = getAppPlayer()->GetAudioStreamCount();
+ std::vector<String> ret(streamCount);
+ for (int iStream=0; iStream < streamCount; iStream++)
+ {
+ AudioStreamInfo info;
+ getAppPlayerMut()->GetAudioStreamInfo(iStream, info);
+
+ if (info.language.length() > 0)
+ ret[iStream] = info.language;
+ else
+ ret[iStream] = info.name;
+ }
+ return ret;
+ }
+
+ return std::vector<String>();
+ }
+
+ void Player::setAudioStream(int iStream)
+ {
+ if (getAppPlayer()->HasPlayer())
+ {
+ int streamCount = getAppPlayer()->GetAudioStreamCount();
+ if (iStream < streamCount)
+ getAppPlayerMut()->SetAudioStream(iStream);
+ }
+ }
+
+ std::vector<String> Player::getAvailableVideoStreams()
+ {
+ int streamCount = getAppPlayer()->GetVideoStreamCount();
+ std::vector<String> ret(streamCount);
+ for (int iStream = 0; iStream < streamCount; ++iStream)
+ {
+ VideoStreamInfo info;
+ getAppPlayer()->GetVideoStreamInfo(iStream, info);
+
+ if (info.language.length() > 0)
+ ret[iStream] = info.language;
+ else
+ ret[iStream] = info.name;
+ }
+ return ret;
+ }
+
+ void Player::setVideoStream(int iStream)
+ {
+ int streamCount = getAppPlayer()->GetVideoStreamCount();
+ if (iStream < streamCount)
+ getAppPlayerMut()->SetVideoStream(iStream);
+ }
+ }
+}
+
diff --git a/xbmc/interfaces/legacy/Player.h b/xbmc/interfaces/legacy/Player.h
new file mode 100644
index 0000000..b700597
--- /dev/null
+++ b/xbmc/interfaces/legacy/Player.h
@@ -0,0 +1,824 @@
+/*
+ * 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 "AddonCallback.h"
+#include "AddonString.h"
+#include "Alternative.h"
+#include "Exception.h"
+#include "InfoTagGame.h"
+#include "InfoTagMusic.h"
+#include "InfoTagRadioRDS.h"
+#include "InfoTagVideo.h"
+#include "ListItem.h"
+#include "PlayList.h"
+#include "cores/IPlayerCallback.h"
+#include "swighelper.h"
+
+#include <vector>
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ XBMCCOMMONS_STANDARD_EXCEPTION(PlayerException);
+
+ typedef Alternative<String, const PlayList* > PlayParameter;
+
+ // This class is a merge of what was previously in xbmcmodule/player.h
+ // and xbmcmodule/PythonPlayer.h without the python references. The
+ // queuing and handling of asynchronous callbacks is done internal to
+ // this class.
+
+ //
+ /// \defgroup python_Player Player
+ /// \ingroup python_xbmc
+ /// @{
+ /// @brief **Kodi's player.**
+ ///
+ /// \python_class{ xbmc.Player() }
+ ///
+ /// To become and create the class to play something.
+ ///
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// xbmc.Player().play(url, listitem, windowed)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ //
+ class Player : public AddonCallback, public IPlayerCallback
+ {
+ private:
+ int iPlayList;
+
+ void playStream(const String& item = emptyString, const XBMCAddon::xbmcgui::ListItem* listitem = NULL, bool windowed = false);
+ void playPlaylist(const PlayList* playlist = NULL,
+ bool windowed = false, int startpos=-1);
+ void playCurrent(bool windowed = false);
+
+ public:
+#if !defined SWIG && !defined DOXYGEN_SHOULD_SKIP_THIS
+ static PlayParameter defaultPlayParameter;
+#endif
+
+#ifndef DOXYGEN_SHOULD_SKIP_THIS
+ // Construct a Player proxying the given generated binding. The
+ // construction of a Player needs to identify whether or not any
+ // callbacks will be executed asynchronously or not.
+ explicit Player();
+ ~Player(void) override;
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ play([item, listitem, windowed, startpos]) }
+ /// Play an item.
+ ///
+ /// @param item [opt] string - filename, url or playlist
+ /// @param listitem [opt] listitem - used with setInfo() to set
+ /// different infolabels.
+ /// @param windowed [opt] bool - true=play video windowed,
+ /// false=play users preference.(default)
+ /// @param startpos [opt] int - starting position when playing
+ /// a playlist. Default = -1
+ ///
+ /// @note If item is not given then the Player will try to play the
+ /// current item in the current playlist.\n
+ /// \n
+ /// You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.
+ ///
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// listitem = xbmcgui.ListItem('Ironman')
+ /// listitem.setInfo('video', {'Title': 'Ironman', 'Genre': 'Science Fiction'})
+ /// xbmc.Player().play(url, listitem, windowed)
+ /// xbmc.Player().play(playlist, listitem, windowed, startpos)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ play(...);
+#else
+ void play(const PlayParameter& item = Player::defaultPlayParameter,
+ const XBMCAddon::xbmcgui::ListItem* listitem = NULL, bool windowed = false, int startpos = -1);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ stop() }
+ /// Stop playing.
+ ///
+ stop();
+#else
+ void stop();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ pause() }
+ /// Pause or resume playing if already paused.
+ ///
+ pause();
+#else
+ void pause();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ playnext() }
+ /// Play next item in playlist.
+ ///
+ playnext();
+#else
+ void playnext();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ playprevious() }
+ /// Play previous item in playlist.
+ ///
+ playprevious();
+#else
+ void playprevious();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ playselected(selected) }
+ /// Play a certain item from the current playlist.
+ ///
+ /// @param selected Integer - Item to select
+ ///
+ playselected(...);
+#else
+ void playselected(int selected);
+#endif
+
+ //
+ /// @defgroup python_PlayerCB Callback functions from Kodi to Add-On
+ /// \ingroup python_Player
+ /// @{
+ /// @brief **Callback functions.**
+ ///
+ /// Functions to handle control callbacks from Kodi to Add-On.
+ ///
+ /// ----------------------------------------------------------------------
+ ///
+ /// @link python_Player Go back to normal functions from player@endlink
+ //
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onPlayBackStarted() }
+ /// onPlayBackStarted method.
+ ///
+ /// Will be called when Kodi player starts. Video or audio might not be available at this point.
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 Use onAVStarted() instead if you need to detect if Kodi is actually playing a media file
+ /// (i.e, if a stream is available)
+ ///
+ onPlayBackStarted();
+#else
+ virtual void onPlayBackStarted();
+#endif
+
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onAVStarted() }
+ /// onAVStarted method.
+ ///
+ /// Will be called when Kodi has a video or audiostream.
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ onAVStarted();
+#else
+ virtual void onAVStarted();
+#endif
+
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onAVChange() }
+ /// onAVChange method.
+ ///
+ /// Will be called when Kodi has a video, audio or subtitle stream. Also happens when the stream changes.
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ onAVChange();
+#else
+ virtual void onAVChange();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onPlayBackEnded() }
+ /// onPlayBackEnded method.
+ ///
+ /// Will be called when Kodi stops playing a file.
+ ///
+ onPlayBackEnded();
+#else
+ virtual void onPlayBackEnded();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onPlayBackStopped() }
+ /// onPlayBackStopped method.
+ ///
+ /// Will be called when user stops Kodi playing a file.
+ ///
+ onPlayBackStopped();
+#else
+ virtual void onPlayBackStopped();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onPlayBackError() }
+ /// onPlayBackError method.
+ ///
+ /// Will be called when playback stops due to an error.
+ ///
+ onPlayBackError();
+#else
+ virtual void onPlayBackError();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onPlayBackPaused() }
+ /// onPlayBackPaused method.
+ ///
+ /// Will be called when user pauses a playing file.
+ ///
+ onPlayBackPaused();
+#else
+ virtual void onPlayBackPaused();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onPlayBackResumed() }
+ /// onPlayBackResumed method.
+ ///
+ /// Will be called when user resumes a paused file.
+ ///
+ onPlayBackResumed();
+#else
+ virtual void onPlayBackResumed();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onQueueNextItem() }
+ /// onQueueNextItem method.
+ ///
+ /// Will be called when user queues the next item.
+ ///
+ onQueueNextItem();
+#else
+ virtual void onQueueNextItem();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onPlayBackSpeedChanged(speed) }
+ /// onPlayBackSpeedChanged method.
+ ///
+ /// Will be called when players speed changes (eg. user FF/RW).
+ ///
+ /// @param speed [integer] Current speed of player
+ ///
+ /// @note Negative speed means player is rewinding, 1 is normal playback
+ /// speed.
+ ///
+ onPlayBackSpeedChanged(int speed);
+#else
+ virtual void onPlayBackSpeedChanged(int speed);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onPlayBackSeek(time, seekOffset) }
+ /// onPlayBackSeek method.
+ ///
+ /// Will be called when user seeks to a time.
+ ///
+ /// @param time [integer] Time to seek to
+ /// @param seekOffset [integer] ?
+ ///
+ onPlayBackSeek(...);
+#else
+ virtual void onPlayBackSeek(int time, int seekOffset);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_PlayerCB
+ /// @brief \python_func{ onPlayBackSeekChapter(chapter) }
+ /// onPlayBackSeekChapter method.
+ ///
+ /// Will be called when user performs a chapter seek.
+ ///
+ /// @param chapter [integer] Chapter to seek to
+ ///
+ onPlayBackSeekChapter(...);
+#else
+ virtual void onPlayBackSeekChapter(int chapter);
+#endif
+ /// @}
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ isPlaying() }
+ /// Check Kodi is playing something.
+ ///
+ /// @return True if Kodi is playing a file.
+ ///
+ isPlaying();
+#else
+ bool isPlaying();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ isPlayingAudio() }
+ /// Check for playing audio.
+ ///
+ /// @return True if Kodi is playing an audio file.
+ ///
+ isPlayingAudio();
+#else
+ bool isPlayingAudio();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ isPlayingVideo() }
+ /// Check for playing video.
+ ///
+ /// @return True if Kodi is playing a video.
+ ///
+ isPlayingVideo();
+#else
+ bool isPlayingVideo();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ isPlayingRDS() }
+ /// Check for playing radio data system (RDS).
+ ///
+ /// @return True if kodi is playing a radio data
+ /// system (RDS).
+ ///
+ isPlayingRDS();
+#else
+ bool isPlayingRDS();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ isPlayingGame() }
+ /// Check for playing game.
+ ///
+ /// @return True if kodi is playing a game
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ isPlayingGame();
+#else
+ bool isPlayingGame();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ isExternalPlayer() }
+ /// Check for external player.
+ ///
+ /// @return True if kodi is playing using an
+ /// external player.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 New function added.
+ ///
+ isExternalPlayer();
+#else
+ bool isExternalPlayer();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getPlayingFile() }
+ /// Returns the current playing file as a string.
+ ///
+ /// @note For LiveTV, returns a __pvr://__ url which is not translatable
+ /// to an OS specific file or external url.
+ ///
+ /// @return Playing filename
+ /// @throws Exception If player is not playing a file.
+ ///
+ getPlayingFile();
+#else
+ String getPlayingFile();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getPlayingItem() }
+ /// Returns the current playing item.
+ ///
+ /// @return Playing item
+ /// @throws Exception If player is not playing a file.
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getPlayingItem();
+#else
+ XBMCAddon::xbmcgui::ListItem* getPlayingItem();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getTime() }
+ /// Get playing time.
+ ///
+ /// Returns the current time of the current playing media as fractional
+ /// seconds.
+ ///
+ /// @return Current time as fractional seconds
+ /// @throws Exception If player is not playing a file.
+ ///
+ getTime();
+#else
+ double getTime();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ seekTime(seekTime) }
+ /// Seek time.
+ ///
+ /// Seeks the specified amount of time as fractional seconds.
+ /// The time specified is relative to the beginning of the currently.
+ /// playing media file.
+ ///
+ /// @param seekTime Time to seek as fractional seconds
+ /// @throws Exception If player is not playing a file.
+ ///
+ seekTime(...);
+#else
+ void seekTime(double seekTime);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ setSubtitles(subtitleFile) }
+ /// Set subtitle file and enable subtitles.
+ ///
+ /// @param subtitleFile File to use as source ofsubtitles
+ ///
+ setSubtitles(...);
+#else
+ void setSubtitles(const char* subtitleFile);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ showSubtitles(visible) }
+ /// Enable / disable subtitles.
+ ///
+ /// @param visible [boolean] True for visible subtitles.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// xbmc.Player().showSubtitles(True)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ showSubtitles(...);
+#else
+ void showSubtitles(bool bVisible);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getSubtitles() }
+ /// Get subtitle stream name.
+ ///
+ /// @return Stream name
+ ///
+ getSubtitles();
+#else
+ String getSubtitles();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getAvailableSubtitleStreams() }
+ /// Get Subtitle stream names.
+ ///
+ /// @return List of subtitle streams as name
+ ///
+ getAvailableSubtitleStreams();
+#else
+ std::vector<String> getAvailableSubtitleStreams();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ setSubtitleStream(stream) }
+ /// Set Subtitle Stream.
+ ///
+ /// @param iStream [int] Subtitle stream to select for play
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// xbmc.Player().setSubtitleStream(1)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setSubtitleStream(...);
+#else
+ void setSubtitleStream(int iStream);
+#endif
+
+ // Player_UpdateInfoTag
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ updateInfoTag(item) }
+ /// Update info labels for currently playing item.
+ ///
+ /// @param item ListItem with new info
+ ///
+ /// @throws Exception If player is not playing a file
+ ///
+ /// @python_v18 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// item = xbmcgui.ListItem()
+ /// item.setPath(xbmc.Player().getPlayingFile())
+ /// item.setInfo('music', {'title' : 'foo', 'artist' : 'bar'})
+ /// xbmc.Player().updateInfoTag(item)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ updateInfoTag();
+#else
+ void updateInfoTag(const XBMCAddon::xbmcgui::ListItem* item);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getGameInfoTag() }
+ /// To get game info tag.
+ ///
+ /// Returns the GameInfoTag of the current playing game.
+ ///
+ /// @return Game info tag
+ /// @throws Exception If player is not playing a file or current
+ /// file is not a game file.
+ ///
+ ///------------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ getGameInfoTag();
+#else
+ InfoTagGame* getGameInfoTag();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getVideoInfoTag() }
+ /// To get video info tag.
+ ///
+ /// Returns the VideoInfoTag of the current playing Movie.
+ ///
+ /// @return Video info tag
+ /// @throws Exception If player is not playing a file or current
+ /// file is not a movie file.
+ ///
+ getVideoInfoTag();
+#else
+ InfoTagVideo* getVideoInfoTag();
+#endif
+
+ // Player_GetMusicInfoTag
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getMusicInfoTag() }
+ /// To get music info tag.
+ ///
+ /// Returns the MusicInfoTag of the current playing 'Song'.
+ ///
+ /// @return Music info tag
+ /// @throws Exception If player is not playing a file or current
+ /// file is not a music file.
+ ///
+ getMusicInfoTag();
+#else
+ InfoTagMusic* getMusicInfoTag();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getRadioRDSInfoTag() }
+ /// To get Radio RDS info tag
+ ///
+ /// Returns the RadioRDSInfoTag of the current playing 'Radio Song if.
+ /// present'.
+ ///
+ /// @return Radio RDS info tag
+ /// @throws Exception If player is not playing a file or current
+ /// file is not a rds file.
+ ///
+ getRadioRDSInfoTag();
+#else
+ InfoTagRadioRDS* getRadioRDSInfoTag();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getTotalTime() }
+ /// To get total playing time.
+ ///
+ /// Returns the total time of the current playing media in seconds.
+ /// This is only accurate to the full second.
+ ///
+ /// @return Total time of the current playing media
+ /// @throws Exception If player is not playing a file.
+ ///
+ getTotalTime();
+#else
+ double getTotalTime();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getAvailableAudioStreams() }
+ /// Get Audio stream names
+ ///
+ /// @return List of audio streams as name
+ ///
+ getAvailableAudioStreams();
+#else
+ std::vector<String> getAvailableAudioStreams();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ setAudioStream(stream) }
+ /// Set Audio Stream.
+ ///
+ /// @param iStream [int] Audio stream to select for play
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// xbmc.Player().setAudioStream(1)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setAudioStream(...);
+#else
+ void setAudioStream(int iStream);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ getAvailableVideoStreams() }
+ /// Get Video stream names
+ ///
+ /// @return List of video streams as name
+ ///
+ getAvailableVideoStreams();
+#else
+ std::vector<String> getAvailableVideoStreams();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_Player
+ /// @brief \python_func{ setVideoStream(stream) }
+ /// Set Video Stream.
+ ///
+ /// @param iStream [int] Video stream to select for play
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ...
+ /// xbmc.Player().setVideoStream(1)
+ /// ...
+ /// ~~~~~~~~~~~~~
+ ///
+ setVideoStream(...);
+#else
+ void setVideoStream(int iStream);
+#endif
+
+#if !defined SWIG && !defined DOXYGEN_SHOULD_SKIP_THIS
+ void OnPlayBackStarted(const CFileItem& file) override;
+ void OnAVStarted(const CFileItem& file) override;
+ void OnAVChange() override;
+ void OnPlayBackEnded() override;
+ void OnPlayBackStopped() override;
+ void OnPlayBackError() override;
+ void OnPlayBackPaused() override;
+ void OnPlayBackResumed() override;
+ void OnQueueNextItem() override;
+ void OnPlayBackSpeedChanged(int iSpeed) override;
+ void OnPlayBackSeek(int64_t iTime, int64_t seekOffset) override;
+ void OnPlayBackSeekChapter(int iChapter) override;
+#endif
+
+ protected:
+ };
+ }
+}
+
diff --git a/xbmc/interfaces/legacy/RenderCapture.h b/xbmc/interfaces/legacy/RenderCapture.h
new file mode 100644
index 0000000..cf1b931
--- /dev/null
+++ b/xbmc/interfaces/legacy/RenderCapture.h
@@ -0,0 +1,206 @@
+/*
+ * 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 "AddonClass.h"
+#include "Exception.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "commons/Buffer.h"
+
+#include <climits>
+
+namespace XBMCAddon
+{
+ namespace xbmc
+ {
+ XBMCCOMMONS_STANDARD_EXCEPTION(RenderCaptureException);
+
+ //
+ /// \defgroup python_xbmc_RenderCapture RenderCapture
+ /// \ingroup python_xbmc
+ /// @{
+ /// @brief **Kodi's render capture.**
+ ///
+ /// \python_class{ RenderCapture() }
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ //
+ class RenderCapture : public AddonClass
+ {
+ unsigned int m_captureId;
+ unsigned int m_width;
+ unsigned int m_height;
+ uint8_t *m_buffer;
+
+ public:
+ inline RenderCapture()
+ {
+ m_captureId = UINT_MAX;
+ m_buffer = nullptr;
+ m_width = 0;
+ m_height = 0;
+ }
+ inline ~RenderCapture() override
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->RenderCaptureRelease(m_captureId);
+ delete [] m_buffer;
+ }
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_RenderCapture
+ /// @brief \python_func{ getWidth() }
+ /// Get width
+ ///
+ /// To get width of captured image as set during RenderCapture.capture().
+ /// Returns 0 prior to calling capture.
+ ///
+ /// @return Width or 0 prior to calling capture
+ ///
+ getWidth();
+#else
+ inline int getWidth() { return m_width; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_RenderCapture
+ /// @brief \python_func{ getHeight() }
+ /// Get height
+ ///
+ /// To get height of captured image as set during RenderCapture.capture().
+ /// Returns 0 prior to calling capture.
+ ///
+ /// @return height or 0 prior to calling capture
+ getHeight();
+#else
+ inline int getHeight() { return m_height; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_RenderCapture
+ /// @brief \python_func{ getAspectRatio() }
+ /// Get aspect ratio of currently displayed video.
+ ///
+ /// @return Aspect ratio
+ /// @warning This may be called prior to calling RenderCapture.capture().
+ ///
+ getAspectRatio();
+#else
+ inline float getAspectRatio()
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ return appPlayer->GetRenderAspectRatio();
+ }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_RenderCapture
+ /// @brief \python_func{ getImageFormat() }
+ /// Get image format
+ ///
+ /// @return Format of captured image: 'BGRA'
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 Image will now always be returned in BGRA
+ ///
+ getImageFormat()
+#else
+ inline const char* getImageFormat()
+#endif
+ {
+ return "BGRA";
+ }
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_RenderCapture
+ /// @brief \python_func{ getImage([msecs]) }
+ /// Returns captured image as a bytearray.
+ ///
+ /// @param msecs [opt] Milliseconds to wait. Waits
+ /// 1000ms if not specified
+ /// @return Captured image as a bytearray
+ ///
+ /// @note The size of the image is m_width * m_height * 4
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 Added the option to specify wait time in msec.
+ ///
+ getImage(...)
+#else
+ inline XbmcCommons::Buffer getImage(unsigned int msecs = 0)
+#endif
+ {
+ if (!GetPixels(msecs))
+ return XbmcCommons::Buffer(0);
+
+ size_t size = m_width * m_height * 4;
+ return XbmcCommons::Buffer(m_buffer, size);
+ }
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmc_RenderCapture
+ /// @brief \python_func{ capture(width, height) }
+ /// Issue capture request.
+ ///
+ /// @param width Width capture image should be rendered to
+ /// @param height Height capture image should should be rendered to
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v17 Removed the option to pass **flags**
+ ///
+ capture(...)
+#else
+ inline void capture(int width, int height)
+#endif
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ if (m_buffer)
+ {
+ appPlayer->RenderCaptureRelease(m_captureId);
+ delete [] m_buffer;
+ }
+ m_captureId = appPlayer->RenderCaptureAlloc();
+ m_width = width;
+ m_height = height;
+ m_buffer = new uint8_t[m_width*m_height*4];
+ appPlayer->RenderCapture(m_captureId, m_width, m_height, CAPTUREFLAG_CONTINUOUS);
+ }
+
+// hide these from swig
+#ifndef SWIG
+ inline bool GetPixels(unsigned int msec)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ return appPlayer->RenderCaptureGetPixels(m_captureId, msec, m_buffer,
+ m_width * m_height * 4);
+ }
+#endif
+
+ };
+ //@}
+ }
+}
diff --git a/xbmc/interfaces/legacy/Settings.cpp b/xbmc/interfaces/legacy/Settings.cpp
new file mode 100644
index 0000000..0ba77c3
--- /dev/null
+++ b/xbmc/interfaces/legacy/Settings.cpp
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2017-2021 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 "Settings.h"
+
+#include "settings/SettingsBase.h"
+#include "settings/lib/Setting.h"
+
+#include <algorithm>
+#include <functional>
+#include <utility>
+
+namespace XBMCAddon
+{
+namespace xbmcaddon
+{
+
+template<class TSetting>
+bool GetSettingValue(const std::shared_ptr<CSettingsBase>& settings,
+ const std::string& key,
+ typename TSetting::Value& value)
+{
+ if (key.empty() || !settings->IsLoaded())
+ return false;
+
+ auto setting = settings->GetSetting(key);
+ if (setting == nullptr || setting->GetType() != TSetting::Type())
+ return false;
+
+ value = std::static_pointer_cast<TSetting>(setting)->GetValue();
+ return true;
+}
+
+template<class TSetting>
+bool GetSettingValueList(const std::shared_ptr<CSettingsBase>& settings,
+ const std::string& key,
+ std::function<typename TSetting::Value(CVariant)> transform,
+ std::vector<typename TSetting::Value>& values)
+{
+ if (key.empty() || !settings->IsLoaded())
+ return false;
+
+ auto setting = settings->GetSetting(key);
+ if (setting == nullptr || setting->GetType() != SettingType::List ||
+ std::static_pointer_cast<CSettingList>(setting)->GetElementType() != TSetting::Type())
+ return false;
+
+ const auto variantValues = settings->GetList(key);
+ std::transform(variantValues.begin(), variantValues.end(), std::back_inserter(values), transform);
+ return true;
+}
+
+template<class TSetting>
+bool SetSettingValue(const std::shared_ptr<CSettingsBase>& settings,
+ const std::string& key,
+ typename TSetting::Value value)
+{
+ if (key.empty() || !settings->IsLoaded())
+ return false;
+
+ // try to get the setting
+ auto setting = settings->GetSetting(key);
+ if (setting == nullptr || setting->GetType() != TSetting::Type())
+ return false;
+
+ return std::static_pointer_cast<TSetting>(setting)->SetValue(value);
+}
+
+template<class TSetting>
+bool SetSettingValueList(const std::shared_ptr<CSettingsBase>& settings,
+ const std::string& key,
+ const std::vector<typename TSetting::Value>& values)
+{
+ if (key.empty() || !settings->IsLoaded())
+ return false;
+
+ // try to get the setting
+ auto setting = settings->GetSetting(key);
+ if (setting == nullptr || setting->GetType() != SettingType::List ||
+ std::static_pointer_cast<CSettingList>(setting)->GetElementType() != TSetting::Type())
+ return false;
+
+ std::vector<CVariant> variantValues;
+ std::transform(values.begin(), values.end(), std::back_inserter(variantValues),
+ [](typename TSetting::Value value) { return CVariant(value); });
+
+ return settings->SetList(key, variantValues);
+}
+
+Settings::Settings(std::shared_ptr<CSettingsBase> settings) : settings(std::move(settings))
+{
+}
+
+bool Settings::getBool(const char* id)
+{
+ bool value = false;
+ if (!GetSettingValue<CSettingBool>(settings, id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"boolean\" for \"%s\"", id);
+
+ return value;
+}
+
+int Settings::getInt(const char* id)
+{
+ int value = 0;
+ if (!GetSettingValue<CSettingInt>(settings, id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"integer\" for \"%s\"", id);
+
+ return value;
+}
+
+double Settings::getNumber(const char* id)
+{
+ double value = 0.0;
+ if (!GetSettingValue<CSettingNumber>(settings, id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"number\" for \"%s\"", id);
+
+ return value;
+}
+
+String Settings::getString(const char* id)
+{
+ std::string value;
+ if (!GetSettingValue<CSettingString>(settings, id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"string\" for \"%s\"", id);
+
+ return value;
+}
+
+std::vector<bool> Settings::getBoolList(const char* id)
+{
+ const auto transform = [](const CVariant& value) { return value.asBoolean(); };
+ std::vector<bool> values;
+ if (!GetSettingValueList<CSettingBool>(settings, id, transform, values))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"list[boolean]\" for \"%s\"", id);
+
+ return values;
+}
+
+std::vector<int> Settings::getIntList(const char* id)
+{
+ const auto transform = [](const CVariant& value) { return value.asInteger32(); };
+ std::vector<int> values;
+ if (!GetSettingValueList<CSettingInt>(settings, id, transform, values))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"list[integer]\" for \"%s\"", id);
+
+ return values;
+}
+
+std::vector<double> Settings::getNumberList(const char* id)
+{
+ const auto transform = [](const CVariant& value) { return value.asDouble(); };
+ std::vector<double> values;
+ if (!GetSettingValueList<CSettingNumber>(settings, id, transform, values))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"list[number]\" for \"%s\"", id);
+
+ return values;
+}
+
+std::vector<String> Settings::getStringList(const char* id)
+{
+ const auto transform = [](const CVariant& value) { return value.asString(); };
+ std::vector<std::string> values;
+ if (!GetSettingValueList<CSettingString>(settings, id, transform, values))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"list[string]\" for \"%s\"", id);
+
+ return values;
+}
+
+void Settings::setBool(const char* id, bool value)
+{
+ if (!SetSettingValue<CSettingBool>(settings, id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"boolean\" for \"%s\"", id);
+ settings->Save();
+}
+
+void Settings::setInt(const char* id, int value)
+{
+ if (!SetSettingValue<CSettingInt>(settings, id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"integer\" for \"%s\"", id);
+ settings->Save();
+}
+
+void Settings::setNumber(const char* id, double value)
+{
+ if (!SetSettingValue<CSettingNumber>(settings, id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"number\" for \"%s\"", id);
+ settings->Save();
+}
+
+void Settings::setString(const char* id, const String& value)
+{
+ if (!SetSettingValue<CSettingString>(settings, id, value))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"string\" for \"%s\"", id);
+ settings->Save();
+}
+
+void Settings::setBoolList(const char* id, const std::vector<bool>& values)
+{
+ if (!SetSettingValueList<CSettingBool>(settings, id, values))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"list[boolean]\" for \"%s\"", id);
+ settings->Save();
+}
+
+void Settings::setIntList(const char* id, const std::vector<int>& values)
+{
+ if (!SetSettingValueList<CSettingInt>(settings, id, values))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"list[integer]\" for \"%s\"", id);
+ settings->Save();
+}
+
+void Settings::setNumberList(const char* id, const std::vector<double>& values)
+{
+ if (!SetSettingValueList<CSettingNumber>(settings, id, values))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"list[number]\" for \"%s\"", id);
+ settings->Save();
+}
+
+void Settings::setStringList(const char* id, const std::vector<String>& values)
+{
+ if (!SetSettingValueList<CSettingString>(settings, id, values))
+ throw XBMCAddon::WrongTypeException("Invalid setting type \"list[string]\" for \"%s\"", id);
+ settings->Save();
+}
+
+} // namespace xbmcaddon
+} // namespace XBMCAddon
diff --git a/xbmc/interfaces/legacy/Settings.h b/xbmc/interfaces/legacy/Settings.h
new file mode 100644
index 0000000..8b8bb0e
--- /dev/null
+++ b/xbmc/interfaces/legacy/Settings.h
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2017-2021 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 "commons/Exception.h"
+#include "interfaces/legacy/AddonClass.h"
+#include "interfaces/legacy/AddonString.h"
+#include "interfaces/legacy/Exception.h"
+#include "interfaces/legacy/Tuple.h"
+#include "settings/lib/SettingDefinitions.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CSettingsBase;
+
+namespace XBMCAddon
+{
+namespace xbmcaddon
+{
+
+XBMCCOMMONS_STANDARD_EXCEPTION(SettingCallbacksNotSupportedException);
+
+//
+/// \defgroup python_settings Settings
+/// \ingroup python_xbmcaddon
+/// @{
+/// @brief **Add-on settings**
+///
+/// \python_class{ Settings() }
+///
+/// This wrapper provides access to the settings specific to an add-on.
+/// It supports reading and writing specific setting values.
+///
+///-----------------------------------------------------------------------
+/// @python_v20 New class added.
+///
+/// **Example:**
+/// ~~~~~~~~~~~~~{.py}
+/// ...
+/// settings = xbmcaddon.Addon('id').getSettings()
+/// ...
+/// ~~~~~~~~~~~~~
+//
+class Settings : public AddonClass
+{
+public:
+#if !defined SWIG && !defined DOXYGEN_SHOULD_SKIP_THIS
+ std::shared_ptr<CSettingsBase> settings;
+#endif
+
+#ifndef SWIG
+ Settings(std::shared_ptr<CSettingsBase> settings);
+#endif
+
+ virtual ~Settings() = default;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ getBool(id) }
+ /// Returns the value of a setting as a boolean.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @return bool - Setting as a boolean
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// enabled = settings.getBool('enabled')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getBool(...);
+#else
+ bool getBool(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ getInt(id) }
+ /// Returns the value of a setting as an integer.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @return integer - Setting as an integer
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// max = settings.getInt('max')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getInt(...);
+#else
+ int getInt(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ getNumber(id) }
+ /// Returns the value of a setting as a floating point number.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @return float - Setting as a floating point number
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// max = settings.getNumber('max')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getNumber(...);
+#else
+ double getNumber(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ getString(id) }
+ /// Returns the value of a setting as a unicode string.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @return string - Setting as a unicode string
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// apikey = settings.getString('apikey')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getString(...);
+#else
+ String getString(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ getBoolList(id) }
+ /// Returns the value of a setting as a list of booleans.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @return list - Setting as a list of booleans
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// enabled = settings.getBoolList('enabled')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getBoolList(...);
+#else
+ std::vector<bool> getBoolList(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ getIntList(id) }
+ /// Returns the value of a setting as a list of integers.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @return list - Setting as a list of integers
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// ids = settings.getIntList('ids')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getIntList(...);
+#else
+ std::vector<int> getIntList(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ getNumberList(id) }
+ /// Returns the value of a setting as a list of floating point numbers.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @return list - Setting as a list of floating point numbers
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// max = settings.getNumberList('max')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getNumberList(...);
+#else
+ std::vector<double> getNumberList(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ getStringList(id) }
+ /// Returns the value of a setting as a list of unicode strings.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @return list - Setting as a list of unicode strings
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// views = settings.getStringList('views')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getStringList(...);
+#else
+ std::vector<String> getStringList(const char* id);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ setBool(id, value) }
+ /// Sets the value of a setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param value bool - value of the setting.
+ /// @return bool - True if the value of the setting was set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// settings.setBool(id='enabled', value=True)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setBool(...);
+#else
+ void setBool(const char* id, bool value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ setInt(id, value) }
+ /// Sets the value of a setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param value integer - value of the setting.
+ /// @return bool - True if the value of the setting was set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// settings.setInt(id='max', value=5)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setInt(...);
+#else
+ void setInt(const char* id, int value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ setNumber(id, value) }
+ /// Sets the value of a setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param value float - value of the setting.
+ /// @return bool - True if the value of the setting was set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// settings.setNumber(id='max', value=5.5)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setNumber(...);
+#else
+ void setNumber(const char* id, double value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ setString(id, value) }
+ /// Sets the value of a setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param value string or unicode - value of the setting.
+ /// @return bool - True if the value of the setting was set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// settings.setString(id='username', value='teamkodi')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setString(...);
+#else
+ void setString(const char* id, const String& value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ setBoolList(id, values) }
+ /// Sets the boolean values of a list setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param values list of boolean - values of the setting.
+ /// @return bool - True if the values of the setting were set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// settings.setBoolList(id='enabled', values=[ True, False ])
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setBoolList(...);
+#else
+ void setBoolList(const char* id, const std::vector<bool>& values);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ setIntList(id, value) }
+ /// Sets the integer values of a list setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param values list of int - values of the setting.
+ /// @return bool - True if the values of the setting were set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// settings.setIntList(id='max', values=[ 5, 23 ])
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setIntList(...);
+#else
+ void setIntList(const char* id, const std::vector<int>& values);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ setNumberList(id, value) }
+ /// Sets the floating point values of a list setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param values list of float - values of the setting.
+ /// @return bool - True if the values of the setting were set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// settings.setNumberList(id='max', values=[ 5.5, 5.8 ])
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setNumberList(...);
+#else
+ void setNumberList(const char* id, const std::vector<double>& values);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_settings
+ /// @brief \python_func{ setStringList(id, value) }
+ /// Sets the string values of a list setting.
+ ///
+ /// @param id string - id of the setting that the module needs to access.
+ /// @param values list of string or unicode - values of the setting.
+ /// @return bool - True if the values of the setting were set, false otherwise
+ ///
+ ///
+ /// @note You can use the above as keywords for arguments.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v20 New function added.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// settings.setStringList(id='username', values=[ 'team', 'kodi' ])
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setStringList(...);
+#else
+ void setStringList(const char* id, const std::vector<String>& values);
+#endif
+};
+//@}
+
+} // namespace xbmcaddon
+} // namespace XBMCAddon
diff --git a/xbmc/interfaces/legacy/Stat.h b/xbmc/interfaces/legacy/Stat.h
new file mode 100644
index 0000000..90a3843
--- /dev/null
+++ b/xbmc/interfaces/legacy/Stat.h
@@ -0,0 +1,195 @@
+/*
+ * 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 "AddonClass.h"
+#include "LanguageHook.h"
+#include "filesystem/File.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcvfs
+ {
+ //
+ /// \defgroup python_stat Stat
+ /// \ingroup python_xbmcvfs
+ /// @{
+ /// @brief **Get file or file system status.**
+ ///
+ /// \python_class{ xbmcvfs.Stat(path) }
+ ///
+ /// These class return information about a file. Execute (search) permission
+ /// is required on all of the directories in path that lead to the file.
+ ///
+ /// @param path [string] file or folder
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v12 New function added
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// st = xbmcvfs.Stat(path)
+ /// modified = st.st_mtime()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ //
+ class Stat : public AddonClass
+ {
+ struct __stat64 st;
+
+ public:
+ Stat(const String& path)
+ {
+ DelayedCallGuard dg;
+ XFILE::CFile::Stat(path, &st);
+ }
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_stat
+ /// @brief \python_func{ st_mode() }
+ /// To get file protection.
+ ///
+ /// @return st_mode
+ ///
+ st_mode();
+#else
+ inline long long st_mode() { return st.st_mode; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_stat
+ /// @brief \python_func{ st_ino() }
+ /// To get inode number.
+ ///
+ /// @return st_ino
+ ///
+ st_ino();
+#else
+ inline long long st_ino() { return st.st_ino; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_stat
+ /// @brief \python_func{ st_dev() }
+ /// To get ID of device containing file.
+ ///
+ /// The st_dev field describes the device on which this file resides.
+ ///
+ /// @return st_dev
+ ///
+ st_dev();
+#else
+ inline long long st_dev() { return st.st_dev; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_stat
+ /// @brief \python_func{ st_nlink() }
+ /// To get number of hard links.
+ ///
+ /// @return st_nlink
+ ///
+ st_nlink();
+#else
+ inline long long st_nlink() { return st.st_nlink; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_stat
+ /// @brief \python_func{ st_uid() }
+ /// To get user ID of owner.
+ ///
+ /// @return st_uid
+ ///
+ st_uid();
+#else
+ inline long long st_uid() { return st.st_uid; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_stat
+ /// @brief \python_func{ st_gid() }
+ /// To get group ID of owner.
+ ///
+ /// @return st_gid
+ ///
+ st_gid();
+#else
+ inline long long st_gid() { return st.st_gid; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_stat
+ /// @brief \python_func{ st_size() }
+ /// To get total size, in bytes.
+ ///
+ /// The st_size field gives the size of the file (if it is a regular file
+ /// or a symbolic link) in bytes. The size of a symbolic link (only on
+ /// Linux and Mac OS X) is the length of the pathname it contains, without
+ /// a terminating null byte.
+ ///
+ /// @return st_size
+ ///
+ st_size();
+#else
+ inline long long st_size() { return st.st_size; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_stat
+ /// @brief \python_func{ st_atime() }
+ /// To get time of last access.
+ ///
+ /// @return st_atime
+ ///
+ st_atime();
+#else
+ inline long long atime() { return st.st_atime; }; //names st_atime/st_mtime/st_ctime are used by sys/stat.h
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_stat
+ /// @brief \python_func{ st_mtime() }
+ /// To get time of last modification.
+ ///
+ /// @return st_mtime
+ ///
+ st_mtime();
+#else
+ inline long long mtime() { return st.st_mtime; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_stat
+ /// @brief \python_func{ st_ctime() }
+ /// To get time of last status change.
+ ///
+ /// @return st_ctime
+ ///
+ st_ctime();
+#else
+ inline long long ctime() { return st.st_ctime; }
+#endif
+ };
+ /// @}
+ }
+}
+
diff --git a/xbmc/interfaces/legacy/String.cpp b/xbmc/interfaces/legacy/String.cpp
new file mode 100644
index 0000000..cb172a2
--- /dev/null
+++ b/xbmc/interfaces/legacy/String.cpp
@@ -0,0 +1,14 @@
+/*
+ * 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 "AddonString.h"
+
+namespace XBMCAddon
+{
+ String emptyString;
+}
diff --git a/xbmc/interfaces/legacy/Tuple.h b/xbmc/interfaces/legacy/Tuple.h
new file mode 100644
index 0000000..116f307
--- /dev/null
+++ b/xbmc/interfaces/legacy/Tuple.h
@@ -0,0 +1,98 @@
+/*
+ * 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
+
+/**
+ * This file contains a few templates to define various length
+ * Tuples.
+ */
+namespace XBMCAddon
+{
+ struct tuple_null_type { };
+
+ class TupleBase
+ {
+ protected:
+ int numValuesSet;
+ explicit inline TupleBase(int pnumValuesSet) : numValuesSet(pnumValuesSet) {}
+ inline void nvs(int newSize) { if(numValuesSet < newSize) numValuesSet = newSize; }
+ public:
+ inline int GetNumValuesSet() const { return numValuesSet; }
+ };
+
+ // stub type template to be partial specialized
+ template<typename T1 = tuple_null_type, typename T2 = tuple_null_type,
+ typename T3 = tuple_null_type, typename T4 = tuple_null_type,
+ typename Extraneous = tuple_null_type> class Tuple {};
+
+ // Tuple that holds a single value
+ template<typename T1> class Tuple<T1, tuple_null_type, tuple_null_type, tuple_null_type, tuple_null_type> : public TupleBase
+ {
+ private:
+ T1 v1;
+ public:
+ explicit inline Tuple(T1 p1) : TupleBase(1), v1(p1) {}
+ inline Tuple() : TupleBase(0) {}
+ inline Tuple(const Tuple<T1>& o) : TupleBase(o), v1(o.v1) {}
+ Tuple<T1>& operator=(const Tuple<T1>& other) = default;
+
+ inline T1& first() { TupleBase::nvs(1); return v1; }
+ inline const T1& first() const { return v1; }
+ };
+
+ // Tuple that holds two values
+ template<typename T1, typename T2> class Tuple<T1, T2, tuple_null_type, tuple_null_type, tuple_null_type> : public Tuple<T1>
+ {
+ protected:
+ T2 v2;
+
+ public:
+ inline Tuple(T1 p1, T2 p2) : Tuple<T1>(p1), v2(p2) { TupleBase::nvs(2); }
+ explicit inline Tuple(T1 p1) : Tuple<T1>(p1) {}
+ inline Tuple() = default;
+ Tuple<T1, T2>& operator=(const Tuple<T1, T2>& other) = default;
+ inline Tuple(const Tuple<T1,T2>& o) : Tuple<T1>(o), v2(o.v2) {}
+
+ inline T2& second() { TupleBase::nvs(2); return v2; }
+ inline const T2& second() const { return v2; }
+ };
+
+ // Tuple that holds three values
+ template<typename T1, typename T2, typename T3> class Tuple<T1, T2, T3, tuple_null_type, tuple_null_type> : public Tuple<T1,T2>
+ {
+ private:
+ T3 v3;
+ public:
+ inline Tuple(T1 p1, T2 p2, T3 p3) : Tuple<T1,T2>(p1,p2), v3(p3) { TupleBase::nvs(3); }
+ inline Tuple(T1 p1, T2 p2) : Tuple<T1,T2>(p1,p2) {}
+ explicit inline Tuple(T1 p1) : Tuple<T1,T2>(p1) {}
+ inline Tuple() = default;
+ inline Tuple(const Tuple<T1,T2,T3>& o) : Tuple<T1,T2>(o), v3(o.v3) {}
+
+ inline T3& third() { TupleBase::nvs(3); return v3; }
+ inline const T3& third() const { return v3; }
+ };
+
+ // Tuple that holds four values
+ template<typename T1, typename T2, typename T3, typename T4> class Tuple<T1, T2, T3, T4, tuple_null_type> : public Tuple<T1,T2,T3>
+ {
+ private:
+ T4 v4;
+ public:
+ inline Tuple(T1 p1, T2 p2, T3 p3, T4 p4) : Tuple<T1,T2,T3>(p1,p2,p3), v4(p4) { TupleBase::nvs(4); }
+ inline Tuple(T1 p1, T2 p2, T3 p3) : Tuple<T1,T2,T3>(p1,p2,p3) {}
+ inline Tuple(T1 p1, T2 p2) : Tuple<T1,T2,T3>(p1,p2) {}
+ explicit inline Tuple(T1 p1) : Tuple<T1,T2,T3>(p1) {}
+ inline Tuple() = default;
+ inline Tuple(const Tuple<T1,T2,T3,T4>& o) : Tuple<T1,T2,T3>(o), v4(o.v4) {}
+
+ inline T4& fourth() { TupleBase::nvs(4); return v4; }
+ inline const T4& fourth() const { return v4; }
+ };
+}
diff --git a/xbmc/interfaces/legacy/Window.cpp b/xbmc/interfaces/legacy/Window.cpp
new file mode 100644
index 0000000..100803e
--- /dev/null
+++ b/xbmc/interfaces/legacy/Window.cpp
@@ -0,0 +1,767 @@
+/*
+ * 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 "Window.h"
+
+#include "ServiceBroker.h"
+#include "WindowException.h"
+#include "WindowInterceptor.h"
+#include "application/Application.h"
+#include "guilib/GUIButtonControl.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIRadioButtonControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#define ACTIVE_WINDOW CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ thread_local ref* InterceptorBase::upcallTls;
+
+ /**
+ * Used in add/remove control. It only locks if it's given a
+ * non-NULL CCriticalSection. It's given a NULL CCriticalSection
+ * when a function higher in the call stack already has a
+ */
+ class MaybeLock
+ {
+ CCriticalSection* lock;
+ public:
+ inline explicit MaybeLock(CCriticalSection* p_lock) : lock(p_lock) { if (lock) lock->lock(); }
+ inline ~MaybeLock() { if (lock) lock->unlock(); }
+ };
+
+ class SingleLockWithDelayGuard
+ {
+ DelayedCallGuard dcg;
+ CCriticalSection& lock;
+ public:
+ inline SingleLockWithDelayGuard(CCriticalSection& ccrit, LanguageHook* lh) : dcg(lh), lock(ccrit) { lock.lock(); }
+ inline ~SingleLockWithDelayGuard() { lock.unlock(); }
+ };
+
+ /**
+ * Explicit template instantiation
+ */
+ template class Interceptor<CGUIWindow>;
+
+ /**
+ * This interceptor is a simple, non-callbackable (is that a word?)
+ * Interceptor to satisfy the Window requirements for upcalling
+ * for the purposes of instantiating a Window instance from
+ * an already existing window.
+ */
+ class ProxyExistingWindowInterceptor : public InterceptorBase
+ {
+ CGUIWindow* cguiwindow;
+
+ public:
+ inline ProxyExistingWindowInterceptor(CGUIWindow* window) :
+ cguiwindow(window) { XBMC_TRACE; }
+
+ CGUIWindow* get() override;
+ };
+
+ CGUIWindow* ProxyExistingWindowInterceptor::get() { XBMC_TRACE; return cguiwindow; }
+
+ Window::Window(bool discrim):
+ window(NULL),
+ m_actionEvent(true),
+ canPulse(true), existingWindow(false)
+ {
+ XBMC_TRACE;
+ }
+
+ /**
+ * This just creates a default window.
+ */
+ Window::Window(int existingWindowId) :
+ window(NULL),
+ m_actionEvent(true)
+ {
+ XBMC_TRACE;
+ SingleLockWithDelayGuard gslock(CServiceBroker::GetWinSystem()->GetGfxContext(),languageHook);
+
+ if (existingWindowId == -1)
+ {
+ // in this case just do the other constructor.
+ canPulse = true;
+ existingWindow = false;
+
+ setWindow(new Interceptor<CGUIWindow>("CGUIWindow",this,getNextAvailableWindowId()));
+ }
+ else
+ {
+ // user specified window id, use this one if it exists
+ // It is not possible to capture key presses or button presses
+ CGUIWindow* pWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(existingWindowId);
+ if (!pWindow)
+ throw WindowException("Window id does not exist");
+
+ setWindow(new ProxyExistingWindowInterceptor(pWindow));
+ }
+ }
+
+ Window::~Window()
+ {
+ XBMC_TRACE;
+
+ deallocating();
+ }
+
+ void Window::deallocating()
+ {
+ AddonCallback::deallocating();
+
+ dispose();
+ }
+
+ void Window::dispose()
+ {
+ XBMC_TRACE;
+
+ //! @todo rework locking
+ // Python GIL and CServiceBroker::GetWinSystem()->GetGfxContext() are deadlock happy
+ // dispose is called from GUIWindowManager and in this case DelayGuard must not be used.
+ if (!CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ SingleLockWithDelayGuard gslock(CServiceBroker::GetWinSystem()->GetGfxContext(), languageHook);
+ }
+
+ if (!isDisposed)
+ {
+ isDisposed = true;
+
+ // no callbacks are possible any longer
+ // - this will be handled by the parent constructor
+
+ // first change to an existing window
+ if (!existingWindow)
+ {
+ if (ACTIVE_WINDOW == iWindowId && !g_application.m_bStop)
+ {
+ if(CServiceBroker::GetGUI()->GetWindowManager().GetWindow(iOldWindowId))
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(iOldWindowId);
+ }
+ // old window does not exist anymore, switch to home
+ else CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_HOME);
+ }
+
+ }
+ else
+ {
+ //! @bug
+ //! This is an existing window, so no resources are free'd. Note that
+ //! THIS WILL FAIL for any controls newly created by python - they will
+ //! remain after the script ends. Ideally this would be remedied by
+ //! a flag in Control that specifies that it was python created - any python
+ //! created controls could then be removed + free'd from the window.
+ //! how this works with controlgroups though could be a bit tricky.
+ }
+
+ // and free our list of controls
+ std::vector<AddonClass::Ref<Control> >::iterator it = vecControls.begin();
+ while (it != vecControls.end())
+ {
+ AddonClass::Ref<Control> pControl = *it;
+ // initialize control to zero
+ pControl->pGUIControl = NULL;
+ pControl->iControlId = 0;
+ pControl->iParentId = 0;
+ ++it;
+ }
+
+ if (!existingWindow)
+ {
+ if (window)
+ {
+ if (CServiceBroker::GetGUI()->GetWindowManager().IsWindowVisible(ref(window)->GetID()))
+ {
+ destroyAfterDeInit = true;
+ close();
+ }
+ else
+ CServiceBroker::GetGUI()->GetWindowManager().Delete(ref(window)->GetID());
+ }
+ }
+
+ vecControls.clear();
+ }
+ }
+
+ void Window::setWindow(InterceptorBase* _window)
+ {
+ XBMC_TRACE;
+ window = _window;
+ iWindowId = _window->get()->GetID();
+
+ if (!existingWindow)
+ CServiceBroker::GetGUI()->GetWindowManager().Add(window->get());
+ }
+
+ int Window::getNextAvailableWindowId()
+ {
+ XBMC_TRACE;
+ // window id's 13000 - 13100 are reserved for python
+ // get first window id that is not in use
+ int id = WINDOW_PYTHON_START;
+ // if window 13099 is in use it means python can't create more windows
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_PYTHON_END))
+ throw WindowException("maximum number of windows reached");
+
+ while(id < WINDOW_PYTHON_END && CServiceBroker::GetGUI()->GetWindowManager().GetWindow(id) != NULL) id++;
+ return id;
+ }
+
+ void Window::popActiveWindowId()
+ {
+ XBMC_TRACE;
+ if (iOldWindowId != iWindowId &&
+ iWindowId != ACTIVE_WINDOW)
+ iOldWindowId = ACTIVE_WINDOW;
+ }
+
+ // Internal helper method
+ /* Searches for a control in Window->vecControls
+ * If we can't find any but the window has the controlId (in case of a not python window)
+ * we create a new control with basic functionality
+ */
+ Control* Window::GetControlById(int iControlId, CCriticalSection* gc)
+ {
+ XBMC_TRACE;
+
+ // find in window vector first!!!
+ // this saves us from creating a complete new control
+ std::vector<AddonClass::Ref<Control> >::iterator it = vecControls.begin();
+ while (it != vecControls.end())
+ {
+ AddonClass::Ref<Control> control = (*it);
+ if (control->iControlId == iControlId)
+ {
+ return control.get();
+ } else ++it;
+ }
+
+ // lock xbmc GUI before accessing data from it
+ MaybeLock lock(gc);
+
+ // check if control exists
+ CGUIControl* pGUIControl = ref(window)->GetControl(iControlId);
+ if (!pGUIControl)
+ {
+ // control does not exist.
+ throw WindowException("Non-Existent Control %d",iControlId);
+ }
+
+ // allocate a new control with a new reference
+ CLabelInfo li;
+
+ Control* pControl = NULL;
+
+ //! @todo Yuck! Should probably be done with a Factory pattern
+ switch(pGUIControl->GetControlType())
+ {
+ case CGUIControl::GUICONTROL_BUTTON:
+ pControl = new ControlButton();
+
+ li = ((CGUIButtonControl *)pGUIControl)->GetLabelInfo();
+
+ // note: conversion from infocolors -> plain colors here
+ ((ControlButton*)pControl)->disabledColor = li.disabledColor;
+ ((ControlButton*)pControl)->focusedColor = li.focusedColor;
+ ((ControlButton*)pControl)->textColor = li.textColor;
+ ((ControlButton*)pControl)->shadowColor = li.shadowColor;
+ if (li.font) ((ControlButton*)pControl)->strFont = li.font->GetFontName();
+ ((ControlButton*)pControl)->align = li.align;
+ break;
+ case CGUIControl::GUICONTROL_LABEL:
+ pControl = new ControlLabel();
+ break;
+ case CGUIControl::GUICONTROL_SPIN:
+ pControl = new ControlSpin();
+ break;
+ case CGUIControl::GUICONTROL_FADELABEL:
+ pControl = new ControlFadeLabel();
+ break;
+ case CGUIControl::GUICONTROL_TEXTBOX:
+ pControl = new ControlTextBox();
+ break;
+ case CGUIControl::GUICONTROL_IMAGE:
+ case CGUIControl::GUICONTROL_BORDEREDIMAGE:
+ pControl = new ControlImage();
+ break;
+ case CGUIControl::GUICONTROL_PROGRESS:
+ pControl = new ControlProgress();
+ break;
+ case CGUIControl::GUICONTROL_SLIDER:
+ pControl = new ControlSlider();
+ break;
+ case CGUIControl::GUICONTAINER_LIST:
+ case CGUIControl::GUICONTAINER_WRAPLIST:
+ case CGUIControl::GUICONTAINER_FIXEDLIST:
+ case CGUIControl::GUICONTAINER_PANEL:
+ pControl = new ControlList();
+ // create a python spin control
+ ((ControlList*)pControl)->pControlSpin = new ControlSpin();
+ break;
+ case CGUIControl::GUICONTROL_GROUP:
+ pControl = new ControlGroup();
+ break;
+ case CGUIControl::GUICONTROL_RADIO:
+ pControl = new ControlRadioButton();
+
+ li = ((CGUIRadioButtonControl *)pGUIControl)->GetLabelInfo();
+
+ // note: conversion from infocolors -> plain colors here
+ ((ControlRadioButton*)pControl)->disabledColor = li.disabledColor;
+ ((ControlRadioButton*)pControl)->focusedColor = li.focusedColor;
+ ((ControlRadioButton*)pControl)->textColor = li.textColor;
+ ((ControlRadioButton*)pControl)->shadowColor = li.shadowColor;
+ if (li.font) ((ControlRadioButton*)pControl)->strFont = li.font->GetFontName();
+ ((ControlRadioButton*)pControl)->align = li.align;
+ break;
+ case CGUIControl::GUICONTROL_EDIT:
+ pControl = new ControlEdit();
+
+ li = ((CGUIEditControl *)pGUIControl)->GetLabelInfo();
+
+ // note: conversion from infocolors -> plain colors here
+ ((ControlEdit*)pControl)->disabledColor = li.disabledColor;
+ ((ControlEdit*)pControl)->textColor = li.textColor;
+ if (li.font) ((ControlEdit*)pControl)->strFont = li.font->GetFontName();
+ ((ControlButton*)pControl)->align = li.align;
+ break;
+ default:
+ break;
+ }
+
+ if (!pControl)
+ // throw an exception
+ throw WindowException("Unknown control type for python");
+
+ // we have a valid control here, fill in all the 'Control' data
+ pControl->pGUIControl = pGUIControl;
+ pControl->iControlId = pGUIControl->GetID();
+ pControl->iParentId = iWindowId;
+ pControl->dwHeight = (int)pGUIControl->GetHeight();
+ pControl->dwWidth = (int)pGUIControl->GetWidth();
+ pControl->dwPosX = (int)pGUIControl->GetXPosition();
+ pControl->dwPosY = (int)pGUIControl->GetYPosition();
+ pControl->iControlUp = pGUIControl->GetAction(ACTION_MOVE_UP).GetNavigation();
+ pControl->iControlDown = pGUIControl->GetAction(ACTION_MOVE_DOWN).GetNavigation();
+ pControl->iControlLeft = pGUIControl->GetAction(ACTION_MOVE_LEFT).GetNavigation();
+ pControl->iControlRight = pGUIControl->GetAction(ACTION_MOVE_RIGHT).GetNavigation();
+
+ // It got this far so means the control isn't actually in the vector of controls
+ // so lets add it to save doing all that next time
+ vecControls.emplace_back(pControl);
+
+ // return the control with increased reference (+1)
+ return pControl;
+ }
+
+ void Window::PulseActionEvent()
+ {
+ XBMC_TRACE;
+ if (canPulse)
+ m_actionEvent.Set();
+ }
+
+ bool Window::WaitForActionEvent(unsigned int milliseconds)
+ {
+ XBMC_TRACE;
+ // DO NOT MAKE THIS A DELAYED CALL!!!!
+ bool ret = languageHook == NULL ? m_actionEvent.Wait(std::chrono::milliseconds(milliseconds))
+ : languageHook->WaitForEvent(m_actionEvent, milliseconds);
+ if (ret)
+ m_actionEvent.Reset();
+ return ret;
+ }
+
+ bool Window::OnAction(const CAction &action)
+ {
+ XBMC_TRACE;
+ // do the base class window first, and the call to python after this
+ bool ret = ref(window)->OnAction(action);
+
+ // workaround - for scripts which try to access the active control (focused) when there is none.
+ // for example - the case when the mouse enters the screen.
+ CGUIControl *pControl = ref(window)->GetFocusedControl();
+ if (action.IsMouse() && !pControl)
+ return ret;
+
+ AddonClass::Ref<Action> inf(new Action(action));
+ invokeCallback(new CallbackFunction<Window,AddonClass::Ref<Action> >(this,&Window::onAction,inf.get()));
+ PulseActionEvent();
+
+ return ret;
+ }
+
+ bool Window::OnBack(int actionID)
+ {
+ // we are always a Python window ... keep that in mind when reviewing the old code
+ return true;
+ }
+
+ void Window::OnDeinitWindow(int nextWindowID /*= 0*/)
+ {
+ // NOTE!: This handle child classes correctly. XML windows will call
+ // the OnDeinitWindow from CGUIMediaWindow while non-XML classes will
+ // call the OnDeinitWindow on CGUIWindow
+ ref(window)->OnDeinitWindow(nextWindowID);
+ if (destroyAfterDeInit)
+ CServiceBroker::GetGUI()->GetWindowManager().Delete(window->get()->GetID());
+ }
+
+ void Window::onAction(Action* action)
+ {
+ XBMC_TRACE;
+ // default onAction behavior
+ if(action->id == ACTION_PREVIOUS_MENU || action->id == ACTION_NAV_BACK)
+ close();
+ }
+
+ bool Window::OnMessage(CGUIMessage& message)
+ {
+ XBMC_TRACE;
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl=message.GetSenderId();
+ AddonClass::Ref<Control> inf;
+ // find python control object with same iControl
+ std::vector<AddonClass::Ref<Control> >::iterator it = vecControls.begin();
+ while (it != vecControls.end())
+ {
+ AddonClass::Ref<Control> pControl = (*it);
+ if (pControl->iControlId == iControl)
+ {
+ inf = pControl.get();
+ break;
+ }
+ ++it;
+ }
+
+ // did we find our control?
+ if (inf.isNotNull())
+ {
+ // currently we only accept messages from a button or controllist with a select action
+ if (inf->canAcceptMessages(message.GetParam1()))
+ {
+ invokeCallback(new CallbackFunction<Window,AddonClass::Ref<Control> >(this,&Window::onControl,inf.get()));
+ PulseActionEvent();
+
+ // return true here as we are handling the event
+ return true;
+ }
+ }
+ // if we get here, we didn't add the action
+ }
+ break;
+ }
+
+ return ref(window)->OnMessage(message);
+ }
+
+ void Window::onControl(Control* action) { XBMC_TRACE; /* do nothing by default */ }
+ void Window::onClick(int controlId) { XBMC_TRACE; /* do nothing by default */ }
+ void Window::onDoubleClick(int controlId) { XBMC_TRACE; /* do nothing by default */ }
+ void Window::onFocus(int controlId) { XBMC_TRACE; /* do nothing by default */ }
+ void Window::onInit() { XBMC_TRACE; /* do nothing by default */ }
+
+ void Window::show()
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dcguard(languageHook);
+ popActiveWindowId();
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTIVATE_WINDOW, iWindowId, 0);
+ }
+
+ void Window::setFocus(Control* pControl)
+ {
+ XBMC_TRACE;
+ if(pControl == NULL)
+ throw WindowException("Object should be of type Control");
+
+ CGUIMessage msg = CGUIMessage(GUI_MSG_SETFOCUS,pControl->iParentId, pControl->iControlId);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, pControl->iParentId);
+ }
+
+ void Window::setFocusId(int iControlId)
+ {
+ XBMC_TRACE;
+ CGUIMessage msg = CGUIMessage(GUI_MSG_SETFOCUS,iWindowId,iControlId);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, iWindowId);
+ }
+
+ Control* Window::getFocus()
+ {
+ XBMC_TRACE;
+ SingleLockWithDelayGuard gslock(CServiceBroker::GetWinSystem()->GetGfxContext(),languageHook);
+
+ int iControlId = ref(window)->GetFocusedControlID();
+ if(iControlId == -1)
+ throw WindowException("No control in this window has focus");
+ // Sine I'm already holding the lock theres no reason to give it to GetFocusedControlID
+ return GetControlById(iControlId,NULL);
+ }
+
+ long Window::getFocusId()
+ {
+ XBMC_TRACE;
+ SingleLockWithDelayGuard gslock(CServiceBroker::GetWinSystem()->GetGfxContext(),languageHook);
+ int iControlId = ref(window)->GetFocusedControlID();
+ if(iControlId == -1)
+ throw WindowException("No control in this window has focus");
+ return (long)iControlId;
+ }
+
+ void Window::removeControl(Control* pControl)
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dg(languageHook);
+ doRemoveControl(pControl,&CServiceBroker::GetWinSystem()->GetGfxContext(),true);
+ }
+
+ void Window::doRemoveControl(Control* pControl, CCriticalSection* gcontext, bool wait)
+ {
+ XBMC_TRACE;
+ // type checking, object should be of type Control
+ if(pControl == NULL)
+ throw WindowException("Object should be of type Control");
+
+ {
+ MaybeLock mlock(gcontext);
+ if(!ref(window)->GetControl(pControl->iControlId))
+ throw WindowException("Control does not exist in window");
+ }
+
+ // delete control from vecControls in window object
+ std::vector<AddonClass::Ref<Control> >::iterator it = vecControls.begin();
+ while (it != vecControls.end())
+ {
+ AddonClass::Ref<Control> control = (*it);
+ if (control->iControlId == pControl->iControlId)
+ {
+ it = vecControls.erase(it);
+ } else ++it;
+ }
+
+ CGUIMessage msg(GUI_MSG_REMOVE_CONTROL, 0, 0);
+ msg.SetPointer(pControl->pGUIControl);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, iWindowId, wait);
+
+ // initialize control to zero
+ pControl->pGUIControl = NULL;
+ pControl->iControlId = 0;
+ pControl->iParentId = 0;
+ }
+
+ void Window::removeControls(std::vector<Control*> pControls)
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dg(languageHook);
+ int count = 1; int size = pControls.size();
+ for (std::vector<Control*>::iterator iter = pControls.begin(); iter != pControls.end(); count++, ++iter)
+ doRemoveControl(*iter,NULL, count == size);
+ }
+
+ long Window::getHeight()
+ {
+ XBMC_TRACE;
+ SingleLockWithDelayGuard gslock(CServiceBroker::GetWinSystem()->GetGfxContext(), languageHook);
+ RESOLUTION_INFO resInfo = ref(window)->GetCoordsRes();
+ return resInfo.iHeight;
+ }
+
+ long Window::getWidth()
+ {
+ XBMC_TRACE;
+ SingleLockWithDelayGuard gslock(CServiceBroker::GetWinSystem()->GetGfxContext(), languageHook);
+ RESOLUTION_INFO resInfo = ref(window)->GetCoordsRes();
+ return resInfo.iWidth;
+ }
+
+ void Window::setProperty(const char* key, const String& value)
+ {
+ XBMC_TRACE;
+ SingleLockWithDelayGuard gslock(CServiceBroker::GetWinSystem()->GetGfxContext(),languageHook);
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+
+ ref(window)->SetProperty(lowerKey, value);
+ }
+
+ String Window::getProperty(const char* key)
+ {
+ XBMC_TRACE;
+ SingleLockWithDelayGuard gslock(CServiceBroker::GetWinSystem()->GetGfxContext(),languageHook);
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+ std::string value = ref(window)->GetProperty(lowerKey).asString();
+ return value;
+ }
+
+ void Window::clearProperty(const char* key)
+ {
+ XBMC_TRACE;
+ if (!key) return;
+ SingleLockWithDelayGuard gslock(CServiceBroker::GetWinSystem()->GetGfxContext(),languageHook);
+
+ std::string lowerKey = key;
+ StringUtils::ToLower(lowerKey);
+ ref(window)->SetProperty(lowerKey, "");
+ }
+
+ void Window::clearProperties()
+ {
+ XBMC_TRACE;
+ SingleLockWithDelayGuard gslock(CServiceBroker::GetWinSystem()->GetGfxContext(),languageHook);
+ ref(window)->ClearProperties();
+ }
+
+ void Window::close()
+ {
+ XBMC_TRACE;
+ bModal = false;
+
+ if (!existingWindow)
+ PulseActionEvent();
+
+ {
+ DelayedCallGuard dcguard(languageHook);
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_PREVIOUS_WINDOW, iOldWindowId, 0);
+ }
+
+ iOldWindowId = 0;
+ }
+
+ void Window::doModal()
+ {
+ XBMC_TRACE;
+ if (!existingWindow)
+ {
+ bModal = true;
+
+ if(iWindowId != ACTIVE_WINDOW)
+ show();
+
+ while (bModal && !g_application.m_bStop)
+ {
+//! @todo garbear added this code to the python window.cpp class and
+//! commented in XBPyThread.cpp. I'm not sure how to handle this
+//! in this native implementation.
+// // Check if XBPyThread::stop() raised a SystemExit exception
+// if (PyThreadState_Get()->async_exc == PyExc_SystemExit)
+// {
+// CLog::Log(LOGDEBUG, "PYTHON: doModal() encountered a SystemExit exception, closing window and returning");
+// Window_Close(self, NULL);
+// break;
+// }
+ languageHook->MakePendingCalls(); // MakePendingCalls
+
+ bool stillWaiting;
+ do
+ {
+ {
+ DelayedCallGuard dcguard(languageHook);
+ stillWaiting = WaitForActionEvent(100) ? false : true;
+ }
+ languageHook->MakePendingCalls();
+ } while (stillWaiting);
+ }
+ }
+ }
+
+ void Window::addControl(Control* pControl)
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dg(languageHook);
+ doAddControl(pControl,&CServiceBroker::GetWinSystem()->GetGfxContext(),true);
+ }
+
+ void Window::doAddControl(Control* pControl, CCriticalSection* gcontext, bool wait)
+ {
+ XBMC_TRACE;
+ if(pControl == NULL)
+ throw WindowException("NULL Control passed to WindowBase::addControl");
+
+ if(pControl->iControlId != 0)
+ throw WindowException("Control is already used");
+
+ // lock kodi GUI before accessing data from it
+ pControl->iParentId = iWindowId;
+
+ {
+ MaybeLock mlock(gcontext);
+ // assign control id, if id is already in use, try next id
+ do pControl->iControlId = ++iCurrentControlId;
+ while (ref(window)->GetControl(pControl->iControlId));
+ }
+
+ pControl->Create();
+
+ // set default navigation for control
+ pControl->iControlUp = pControl->iControlId;
+ pControl->iControlDown = pControl->iControlId;
+ pControl->iControlLeft = pControl->iControlId;
+ pControl->iControlRight = pControl->iControlId;
+
+ pControl->pGUIControl->SetAction(ACTION_MOVE_UP, CGUIAction(pControl->iControlUp));
+ pControl->pGUIControl->SetAction(ACTION_MOVE_DOWN, CGUIAction(pControl->iControlDown));
+ pControl->pGUIControl->SetAction(ACTION_MOVE_LEFT, CGUIAction(pControl->iControlLeft));
+ pControl->pGUIControl->SetAction(ACTION_MOVE_RIGHT, CGUIAction(pControl->iControlRight));
+
+ // add control to list and allocate resources for the control
+ vecControls.emplace_back(pControl);
+ pControl->pGUIControl->AllocResources();
+
+ // This calls the CGUIWindow parent class to do the final add
+ CGUIMessage msg(GUI_MSG_ADD_CONTROL, 0, 0);
+ msg.SetPointer(pControl->pGUIControl);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, iWindowId, wait);
+ }
+
+ void Window::addControls(std::vector<Control*> pControls)
+ {
+ XBMC_TRACE;
+ SingleLockWithDelayGuard gslock(CServiceBroker::GetWinSystem()->GetGfxContext(),languageHook);
+ int count = 1; int size = pControls.size();
+ for (std::vector<Control*>::iterator iter = pControls.begin(); iter != pControls.end(); count++, ++iter)
+ doAddControl(*iter,NULL, count == size);
+ }
+
+ Control* Window::getControl(int iControlId)
+ {
+ XBMC_TRACE;
+ DelayedCallGuard dg(languageHook);
+ return GetControlById(iControlId,&CServiceBroker::GetWinSystem()->GetGfxContext());
+ }
+
+ void Action::setFromCAction(const CAction& action)
+ {
+ XBMC_TRACE;
+ id = action.GetID();
+ buttonCode = action.GetButtonCode();
+ fAmount1 = action.GetAmount(0);
+ fAmount2 = action.GetAmount(1);
+ fRepeat = action.GetRepeat();
+ strAction = action.GetName();
+ }
+
+ }
+}
diff --git a/xbmc/interfaces/legacy/Window.h b/xbmc/interfaces/legacy/Window.h
new file mode 100644
index 0000000..b30be7b
--- /dev/null
+++ b/xbmc/interfaces/legacy/Window.h
@@ -0,0 +1,878 @@
+/*
+ * 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 "AddonCallback.h"
+#include "AddonString.h"
+#include "Control.h"
+#include "swighelper.h"
+
+#include <limits.h>
+#include <mutex>
+#include <vector>
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ // Forward declare the interceptor as the AddonWindowInterceptor.h
+ // file needs to include the Window class because of the template
+ class InterceptorBase;
+
+ //
+ /// \defgroup python_xbmcgui_action Action
+ /// \ingroup python_xbmcgui
+ ///@{
+ /// @brief **Action class.**
+ ///
+ /// \python_class{ xbmcgui.Action(): }
+ ///
+ /// This class serves in addition to identify carried out
+ /// \ref kodi_key_action_ids of Kodi and to be able to carry out thereby own
+ /// necessary steps.
+ ///
+ /// The data of this class are always transmitted by callback
+ /// Window::onAction at a window.
+ ///
+ class Action : public AddonClass
+ {
+ public:
+ Action() = default;
+
+#ifndef SWIG
+ explicit Action(const CAction& caction) { setFromCAction(caction); }
+
+ void setFromCAction(const CAction& caction);
+
+ long id = -1;
+ float fAmount1 = 0.0f;
+ float fAmount2 = 0.0f;
+ float fRepeat = 0.0f;
+ unsigned long buttonCode = 0;
+ std::string strAction;
+
+ // Not sure if this is correct but it's here for now.
+ AddonClass::Ref<Control> control; // previously pObject
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_action
+ /// @brief \python_func{ getId() }
+ /// To get \ref kodi_key_action_ids
+ ///
+ /// This function returns the identification code used by the explained
+ /// order, it is necessary to determine the type of command from
+ /// \ref kodi_key_action_ids.
+ ///
+ /// @return The action's current id as a long or 0 if
+ /// no action is mapped in the xml's.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// def onAction(self, action):
+ /// if action.getId() == ACTION_PREVIOUS_MENU:
+ /// print('action received: previous')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getId();
+#else
+ long getId() { XBMC_TRACE; return id; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_action
+ /// @brief \python_func{ getButtonCode() }
+ /// Returns the button code for this action.
+ ///
+ /// @return [integer] button code
+ ///
+ getButtonCode();
+#else
+ long getButtonCode() { XBMC_TRACE; return buttonCode; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_action
+ /// @brief \python_func{ getAmount1() }
+ /// Returns the first amount of force applied to the thumbstick.
+ ///
+ /// @return [float] first amount
+ ///
+ getAmount1();
+#else
+ float getAmount1() { XBMC_TRACE; return fAmount1; }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_action
+ /// @brief \python_func{ getAmount2() }
+ /// Returns the second amount of force applied to the thumbstick.
+ ///
+ /// @return [float] second amount
+ ///
+ getAmount2();
+#else
+ float getAmount2() { XBMC_TRACE; return fAmount2; }
+#endif
+ };
+ ///@}
+
+ //==========================================================================
+ // This is the main class for the xbmcgui.Window functionality. It is tied
+ // into the main Kodi windowing system via the Interceptor
+ //==========================================================================
+ //
+ /// \defgroup python_xbmcgui_window Window
+ /// \ingroup python_xbmcgui
+ /// @{
+ /// @brief __GUI window class for Add-Ons.__
+ ///
+ /// This class allows over their functions to create and edit windows that
+ /// can be accessed from an Add-On.
+ ///
+ /// Likewise, all functions from here as well in the other window classes
+ /// \ref python_xbmcgui_window_dialog "WindowDialog",
+ /// \ref python_xbmcgui_window_xml "WindowXML" and
+ /// \ref python_xbmcgui_window_dialog_xml "WindowXMLDialog"
+ /// with inserted and available.
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ /// Constructor for window
+ /// ----------------------------
+ ///
+ /// \python_class{ xbmcgui.Window([existingWindowId]): }
+ ///
+ /// Creates a new from Add-On usable window class. This is to create
+ /// window for related controls by system calls.
+ ///
+ /// @param existingWindowId [opt] Specify an id to use an existing
+ /// window.
+ /// @throws ValueError if supplied window Id does not exist.
+ /// @throws Exception if more then 200 windows are created.
+ ///
+ /// Deleting this window will activate the old window that was active
+ /// and resets (not delete) all controls that are associated with this
+ /// window.
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// win = xbmcgui.Window()
+ /// width = win.getWidth()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ ///
+ //
+ class Window : public AddonCallback
+ {
+ friend class WindowDialogMixin;
+ bool isDisposed = false;
+
+ void doAddControl(Control* pControl, CCriticalSection* gcontext, bool wait);
+ void doRemoveControl(Control* pControl, CCriticalSection* gcontext, bool wait);
+
+ protected:
+#ifndef SWIG
+ InterceptorBase* window;
+ int iWindowId = -1;
+
+ std::vector<AddonClass::Ref<Control> > vecControls;
+ int iOldWindowId = 0;
+ int iCurrentControlId = 3000;
+ bool bModal = false;
+ CEvent m_actionEvent;
+
+ bool canPulse = false;
+
+ // I REALLY hate this ... but it's the simplest fix right now.
+ bool existingWindow = true;
+ bool destroyAfterDeInit = false;
+
+ /**
+ * This only takes a boolean to allow subclasses to explicitly use it. A
+ * default constructor can be used as a concrete class and we need to tell
+ * the difference.
+ * subclasses should use this constructor and not the other.
+ */
+ explicit Window(bool discrim);
+
+ void deallocating() override;
+
+ /**
+ * This helper retrieves the next available id. It is assumed that the
+ * global lock is already being held.
+ */
+ static int getNextAvailableWindowId();
+
+ /**
+ * Child classes MUST call this in their constructors. It should be an
+ * instance of Interceptor<P extends CGUIWindow>. Control of memory
+ * management for this class is then given to the Window.
+ */
+ void setWindow(InterceptorBase* _window);
+
+ /**
+ * This is a helper method since popping the previous window id is a common
+ * function.
+ */
+ void popActiveWindowId();
+
+ /**
+ * This is a helper method since getting a control by it's id is a common
+ * function.
+ */
+ Control* GetControlById(int iControlId, CCriticalSection* gc);
+
+ SWIGHIDDENVIRTUAL void PulseActionEvent();
+ SWIGHIDDENVIRTUAL bool WaitForActionEvent(unsigned int milliseconds);
+#endif
+
+ public:
+ explicit Window(int existingWindowId = -1);
+
+ ~Window() override;
+
+#ifndef SWIG
+ SWIGHIDDENVIRTUAL bool OnMessage(CGUIMessage& message);
+ SWIGHIDDENVIRTUAL bool OnAction(const CAction &action);
+ SWIGHIDDENVIRTUAL bool OnBack(int actionId);
+ SWIGHIDDENVIRTUAL void OnDeinitWindow(int nextWindowID);
+
+ SWIGHIDDENVIRTUAL bool IsDialogRunning() const
+ {
+ XBMC_TRACE;
+ return false;
+ }
+ SWIGHIDDENVIRTUAL bool IsDialog() const
+ {
+ XBMC_TRACE;
+ return false;
+ }
+ SWIGHIDDENVIRTUAL bool IsModalDialog() const
+ {
+ XBMC_TRACE;
+ return false;
+ }
+ SWIGHIDDENVIRTUAL bool IsMediaWindow() const
+ {
+ XBMC_TRACE;
+ return false;
+ }
+ SWIGHIDDENVIRTUAL void dispose();
+
+ /**
+ * This is called from the InterceptorBase destructor to prevent further
+ * use of the interceptor from the window.
+ */
+ inline void interceptorClear()
+ {
+ std::unique_lock<CCriticalSection> lock(*this);
+ window = NULL;
+ }
+#endif
+
+ //
+ /// @defgroup python_xbmcgui_window_cb Callback functions from Kodi to add-on
+ /// \ingroup python_xbmcgui_window
+ /// @{
+ /// @brief __GUI window callback functions.__
+ ///
+ /// Functions to handle control callbacks from Kodi.
+ ///
+ /// Likewise, all functions from here as well in the all window classes
+ /// (\ref python_xbmcgui_window "Window",
+ /// \ref python_xbmcgui_window_dialog "WindowDialog",
+ /// \ref python_xbmcgui_window_xml "WindowXML" and
+ /// \ref python_xbmcgui_window_dialog_xml "WindowXMLDialog") with inserted
+ /// and available.
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// @link python_xbmcgui_window Go back to normal functions from window@endlink
+ //
+
+ // callback takes a parameter
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_cb
+ /// @brief \python_func{ onAction(self, Action action) }
+ /// **onAction method.**
+ ///
+ /// This method will receive all actions that the main program will send
+ /// to this window.
+ ///
+ /// @param self Own base class pointer
+ /// @param action The action id to perform, see
+ /// \ref python_xbmcgui_action to get use
+ /// of them
+ ///
+ /// @note
+ /// - By default, only the `PREVIOUS_MENU` and `NAV_BACK actions` are
+ /// handled.
+ /// - Overwrite this method to let your script handle all actions.
+ /// - Don't forget to capture `ACTION_PREVIOUS_MENU` or `ACTION_NAV_BACK`,
+ /// else the user can't close this window.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// # Define own function where becomes called from Kodi
+ /// def onAction(self, action):
+ /// if action.getId() == ACTION_PREVIOUS_MENU:
+ /// print('action received: previous')
+ /// self.close()
+ /// if action.getId() == ACTION_SHOW_INFO:
+ /// print('action received: show info')
+ /// if action.getId() == ACTION_STOP:
+ /// print('action received: stop')
+ /// if action.getId() == ACTION_PAUSE:
+ /// print('action received: pause')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ onAction(...);
+#else
+ virtual void onAction(Action* action);
+#endif
+
+ // on control is not actually on Window in the api but is called into Python anyway.
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_cb
+ /// @brief \python_func{ onControl(self, Control) }
+ /// **onControl method.**
+ ///
+ /// This method will receive all click events on owned and selected
+ /// controls when the control itself doesn't handle the message.
+ ///
+ /// @param self Own base class pointer
+ /// @param control The \ref python_xbmcgui_control "Control" class
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// # Define own function where becomes called from Kodi
+ /// def onControl(self, control):
+ /// print("Window.onControl(control=[%s])"%control)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ void onControl(...);
+#else
+ virtual void onControl(Control* control);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_cb
+ /// @brief \python_func{ onClick(self, int controlId) }
+ /// **onClick method.**
+ ///
+ /// This method will receive all click events that the main program will
+ /// send to this window.
+ ///
+ /// @param self Own base class pointer
+ /// @param controlId The one time clicked GUI control
+ /// identifier
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// # Define own function where becomes called from Kodi
+ /// def onClick(self,controlId):
+ /// if controlId == 10:
+ /// print("The control with Id 10 is clicked")
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ onClick(...);
+#else
+ virtual void onClick(int controlId);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_cb
+ /// @brief \python_func{ onDoubleClick(self, int controlId) }
+ /// __onDoubleClick method.__
+ ///
+ /// This method will receive all double click events that the main program
+ /// will send to this window.
+ ///
+ /// @param self Own base class pointer
+ /// @param controlId The double clicked GUI control
+ /// identifier
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// # Define own function where becomes called from Kodi
+ /// def onDoubleClick(self,controlId):
+ /// if controlId == 10:
+ /// print("The control with Id 10 is double clicked")
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ onDoubleClick(...);
+#else
+ virtual void onDoubleClick(int controlId);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_cb
+ /// @brief \python_func{ onFocus(self, int controlId) }
+ /// __onFocus method.__
+ ///
+ /// This method will receive all focus events that the main program will
+ /// send to this window.
+ ///
+ /// @param self Own base class pointer
+ /// @param controlId The focused GUI control identifier
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// # Define own function where becomes called from Kodi
+ /// def onDoubleClick(self,controlId):
+ /// if controlId == 10:
+ /// print("The control with Id 10 is focused")
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ onFocus(...);
+#else
+ virtual void onFocus(int controlId);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_cb
+ /// @brief \python_func{ onInit(self) }
+ /// __onInit method.__
+ ///
+ /// This method will be called to initialize the window
+ ///
+ /// @param self Own base class pointer
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// # Define own function where becomes called from Kodi
+ /// def onInit(self):
+ /// print("Window.onInit method called from Kodi")
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ onInit(...);
+#else
+ virtual void onInit();
+#endif
+ //@}
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ show() }
+ /// Show this window.
+ ///
+ /// Shows this window by activating it, calling close() after it wil
+ /// activate the current window again.
+ ///
+ /// @note If your script ends this window will be closed to. To show it
+ /// forever, make a loop at the end of your script or use doModal()
+ /// instead.
+ ///
+ show();
+#else
+ SWIGHIDDENVIRTUAL void show();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ setFocus(Control) }
+ /// Give the supplied control focus.
+ ///
+ /// @param Control \ref python_xbmcgui_control "Control" class to focus
+ /// @throws TypeError If supplied argument is not a \ref python_xbmcgui_control "Control"
+ /// type
+ /// @throws SystemError On Internal error
+ /// @throws RuntimeError If control is not added to a window
+ ///
+ setFocus(...);
+#else
+ SWIGHIDDENVIRTUAL void setFocus(Control* pControl);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ setFocusId(ControlId) }
+ /// Gives the control with the supplied focus.
+ ///
+ /// @param ControlId [integer] On skin defined id of control
+ /// @throws SystemError On Internal error
+ /// @throws RuntimeError If control is not added to a window
+ ///
+ setFocusId(...);
+#else
+ SWIGHIDDENVIRTUAL void setFocusId(int iControlId);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ getFocus(Control) }
+ /// Returns the control which is focused.
+ ///
+ /// @return Focused control class
+ /// @throws SystemError On Internal error
+ /// @throws RuntimeError If no control has focus
+ ///
+ getFocus();
+#else
+ SWIGHIDDENVIRTUAL Control* getFocus();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ getFocusId(int) }
+ /// Returns the id of the control which is focused.
+ ///
+ /// @return Focused control id
+ /// @throws SystemError On Internal error
+ /// @throws RuntimeError If no control has focus
+ ///
+ getFocusId();
+#else
+ SWIGHIDDENVIRTUAL long getFocusId();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ removeControl(Control) }
+ /// Removes the control from this window.
+ ///
+ /// @param Control \ref python_xbmcgui_control "Control" class to remove
+ /// @throws TypeError If supplied argument is not a \ref python_xbmcgui_control
+ /// type
+ /// @throws RuntimeError If control is not added to this window
+ ///
+ /// This will not delete the control. It is only removed from the window.
+ ///
+ removeControl(...);
+#else
+ SWIGHIDDENVIRTUAL void removeControl(Control* pControl);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ removeControls(List) }
+ /// Removes a list of controls from this window.
+ ///
+ /// @param List List with controls to remove
+ /// @throws TypeError If supplied argument is not a \ref python_xbmcgui_control
+ /// type
+ /// @throws RuntimeError If control is not added to this window
+ ///
+ /// This will not delete the controls. They are only removed from the
+ /// window.
+ ///
+ removeControls(...);
+#else
+ SWIGHIDDENVIRTUAL void removeControls(std::vector<Control*> pControls);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ getHeight() }
+ /// Returns the height of this Window instance.
+ ///
+ /// @return Window height in pixels
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 Function changed
+ ///
+ getHeight();
+#else
+ SWIGHIDDENVIRTUAL long getHeight();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ getWidth() }
+ /// Returns the width of this Window instance.
+ ///
+ /// @return Window width in pixels
+ ///
+ ///-----------------------------------------------------------------------
+ /// @python_v18 Function changed
+ ///
+ getWidth();
+#else
+ SWIGHIDDENVIRTUAL long getWidth();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ setProperty(key, value) }
+ /// Sets a window property, similar to an infolabel.
+ ///
+ /// @param key string - property name.
+ /// @param value string or unicode - value of property.
+ ///
+ /// @note Key is NOT case sensitive. Setting value to an empty string is
+ /// equivalent to clearProperty(key).\n
+ /// You can use the above as keywords for arguments and skip
+ /// certain optional arguments.\n
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// win = xbmcgui.Window(xbmcgui.getCurrentWindowId())
+ /// win.setProperty('Category', 'Newest')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setProperty(...);
+#else
+ SWIGHIDDENVIRTUAL void setProperty(const char* key, const String& value);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ getProperty(key) }
+ /// Returns a window property as a string, similar to an infolabel.
+ ///
+ /// @param key string - property name.
+ ///
+ /// @note Key is NOT case sensitive.\n
+ /// You can use the above as keywords for arguments and skip
+ /// certain optional arguments.
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// win = xbmcgui.Window(xbmcgui.getCurrentWindowId())
+ /// category = win.getProperty('Category')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getProperty(...);
+#else
+ SWIGHIDDENVIRTUAL String getProperty(const char* key);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ clearProperty(key) }
+ /// Clears the specific window property.
+ ///
+ /// @param key string - property name.
+ ///
+ /// @note Key is NOT case sensitive. Equivalent to setProperty(key,'')
+ /// You can use the above as keywords for arguments and skip certain
+ /// optional arguments.
+ /// Once you use a keyword, all following arguments require the
+ /// keyword.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// win = xbmcgui.Window(xbmcgui.getCurrentWindowId())
+ /// win.clearProperty('Category')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ clearProperty(...);
+#else
+ SWIGHIDDENVIRTUAL void clearProperty(const char* key);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ clearProperties() }
+ /// Clears all window properties.
+ ///
+ ///
+ ///-----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// win = xbmcgui.Window(xbmcgui.getCurrentWindowId())
+ /// win.clearProperties()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ clearProperties();
+#else
+ SWIGHIDDENVIRTUAL void clearProperties();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ close() }
+ /// Closes this window.
+ ///
+ /// Closes this window by activating the old window.
+ ///
+ /// @note The window is not deleted with this method.
+ ///
+ close();
+#else
+ SWIGHIDDENVIRTUAL void close();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ doModal() }
+ /// Display this window until close() is called.
+ ///
+ doModal();
+#else
+
+ SWIGHIDDENVIRTUAL void doModal();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ addControl(Control) }
+ /// Add a \ref python_xbmcgui_control "Control" to this window.
+ ///
+ /// @param Control \ref python_xbmcgui_control "Control" to add
+ /// @throws TypeError If supplied argument is not a \ref python_xbmcgui_control
+ /// type
+ /// @throws ReferenceError If control is already used in another
+ /// window
+ /// @throws RuntimeError Should not happen :-)
+ ///
+ /// The next controls can be added to a window atm
+ /// | Control-class | Description |
+ /// |---------------------|------------------------------------------------------------|
+ /// | \ref python_xbmcgui_control_label "ControlLabel" | Label control to show text
+ /// | \ref python_xbmcgui_control_fadelabel "ControlFadeLabel" | The fadelabel has multiple labels which it cycles through
+ /// | \ref python_xbmcgui_control_textbox "ControlTextBox" | To show bigger text field
+ /// | \ref python_xbmcgui_control_button "ControlButton" | Brings a button to do some actions
+ /// | \ref python_xbmcgui_control_edit "ControlEdit" | The edit control allows a user to input text in Kodi
+ /// | \ref python_xbmcgui_control_fadelabel "ControlFadeLabel" | The fade label control is used for displaying multiple pieces of text in the same space in Kodi
+ /// | \ref python_xbmcgui_control_list "ControlList" | Add a list for something like files
+ /// | \ref python_xbmcgui_control_group "ControlGroup" | Is for a group which brings the others together
+ /// | \ref python_xbmcgui_control_image "ControlImage" | Controls a image on skin
+ /// | \ref python_xbmcgui_control_radiobutton "ControlRadioButton" | For a radio button which handle boolean values
+ /// | \ref python_xbmcgui_control_progress "ControlProgress" | Progress bar for a performed work or something else
+ /// | \ref python_xbmcgui_control_slider "ControlSlider" | The slider control is used for things where a sliding bar best represents the operation at hand
+ /// | \ref python_xbmcgui_control_spin "ControlSpin" | The spin control is used for when a list of options can be chosen
+ /// | \ref python_xbmcgui_control_textbox "ControlTextBox" | The text box is used for showing a large multipage piece of text in Kodi
+ ///
+ addControl(...);
+#else
+ SWIGHIDDENVIRTUAL void addControl(Control* pControl);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ addControls(List) }
+ /// Add a list of Controls to this window.
+ ///
+ /// @param List List with controls to add
+ /// @throws TypeError If supplied argument is not of List
+ /// type, or a control is not of \ref python_xbmcgui_control
+ /// type
+ /// @throws ReferenceError If control is already used in another
+ /// window
+ /// @throws RuntimeError Should not happen :-)
+ ///
+ addControls(...);
+#else
+ SWIGHIDDENVIRTUAL void addControls(std::vector<Control*> pControls);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window
+ /// @brief \python_func{ getControl(controlId) }
+ /// Gets the control from this window.
+ ///
+ /// @param controlId \ref python_xbmcgui_control id to get
+ /// @throws Exception If \ref python_xbmcgui_control doesn't exist
+ ///
+ /// @remark controlId doesn't have to be a python control, it can be a
+ /// control id from a Kodi window too (you can find id's in the xml files.
+ ///
+ /// @note Not python controls are not completely usable yet
+ /// You can only use the \ref python_xbmcgui_control functions
+ ///
+ getControl(...);
+#else
+ SWIGHIDDENVIRTUAL Control* getControl(int iControlId);
+#endif
+ };
+ ///@}
+ }
+}
diff --git a/xbmc/interfaces/legacy/WindowDialog.cpp b/xbmc/interfaces/legacy/WindowDialog.cpp
new file mode 100644
index 0000000..13ea4fd
--- /dev/null
+++ b/xbmc/interfaces/legacy/WindowDialog.cpp
@@ -0,0 +1,76 @@
+/*
+ * 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 "WindowDialog.h"
+
+#include "ServiceBroker.h"
+#include "WindowInterceptor.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindow.h"
+#include "guilib/GUIWindowManager.h"
+
+#include <mutex>
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ WindowDialog::WindowDialog() :
+ Window(true), WindowDialogMixin(this)
+ {
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ InterceptorBase* interceptor = new Interceptor<CGUIWindow>("CGUIWindow", this, getNextAvailableWindowId());
+ // set the render order to the dialog's default because this dialog is mapped to CGUIWindow instead of CGUIDialog
+ interceptor->SetRenderOrder(RENDER_ORDER_DIALOG);
+ setWindow(interceptor);
+ }
+
+ WindowDialog::~WindowDialog() { deallocating(); }
+
+ bool WindowDialog::OnMessage(CGUIMessage& message)
+ {
+#ifdef ENABLE_XBMC_TRACE_API
+ XBMC_TRACE;
+ CLog::Log(LOGDEBUG, "{}NEWADDON WindowDialog::OnMessage Message {}", _tg.getSpaces(),
+ message.GetMessage());
+#endif
+
+ switch(message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ ref(window)->OnMessage(message);
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ return Window::OnMessage(message);
+ }
+ break;
+ }
+
+ // we do not message CGUIPythonWindow here..
+ return ref(window)->OnMessage(message);
+ }
+
+ bool WindowDialog::OnAction(const CAction &action)
+ {
+ XBMC_TRACE;
+ return WindowDialogMixin::OnAction(action) ? true : Window::OnAction(action);
+ }
+
+ void WindowDialog::OnDeinitWindow(int nextWindowID)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().RemoveDialog(iWindowId);
+ Window::OnDeinitWindow(nextWindowID);
+ }
+
+ }
+}
diff --git a/xbmc/interfaces/legacy/WindowDialog.h b/xbmc/interfaces/legacy/WindowDialog.h
new file mode 100644
index 0000000..0d44c86
--- /dev/null
+++ b/xbmc/interfaces/legacy/WindowDialog.h
@@ -0,0 +1,88 @@
+/*
+ * 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 "Window.h"
+#include "WindowDialogMixin.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ //
+ /// \defgroup python_xbmcgui_window_dialog Subclass - WindowDialog
+ /// \ingroup python_xbmcgui_window
+ /// @{
+ /// @brief __GUI window dialog class for Add-Ons.__
+ ///
+ /// \python_class{ xbmcgui.WindowDialog(int windowId): }
+ ///
+ /// Creates a new window from Add-On usable dialog class. This is to create
+ /// window for related controls by system calls.
+ ///
+ /// @param windowId [opt] Specify an id to use an existing
+ /// window.
+ /// @throws ValueError if supplied window Id does not exist.
+ /// @throws Exception if more then 200 windows are created.
+ ///
+ /// Deleting this window will activate the old window that was active
+ /// and resets (not delete) all controls that are associated with this
+ /// window.
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.WindowDialog()
+ /// width = dialog.getWidth()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ ///
+ //
+ class WindowDialog : public Window, private WindowDialogMixin
+ {
+ public:
+ WindowDialog();
+ ~WindowDialog() override;
+
+#ifndef SWIG
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ bool IsDialogRunning() const override { return WindowDialogMixin::IsDialogRunning(); }
+ bool IsModalDialog() const override
+ {
+ XBMC_TRACE;
+ return true;
+ };
+ bool IsDialog() const override
+ {
+ XBMC_TRACE;
+ return true;
+ };
+
+ inline void show() override
+ {
+ XBMC_TRACE;
+ WindowDialogMixin::show();
+ }
+ inline void close() override
+ {
+ XBMC_TRACE;
+ WindowDialogMixin::close();
+ }
+#endif
+ };
+ ///@}
+ }
+}
diff --git a/xbmc/interfaces/legacy/WindowDialogMixin.cpp b/xbmc/interfaces/legacy/WindowDialogMixin.cpp
new file mode 100644
index 0000000..32d6fa9
--- /dev/null
+++ b/xbmc/interfaces/legacy/WindowDialogMixin.cpp
@@ -0,0 +1,75 @@
+/*
+ * 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 "WindowDialogMixin.h"
+
+#include "ServiceBroker.h"
+#include "WindowInterceptor.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ void WindowDialogMixin::show()
+ {
+ XBMC_TRACE;
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_PYTHON_DIALOG, HACK_CUSTOM_ACTION_OPENING,
+ 0, static_cast<void*>(w->window->get()));
+ }
+
+ void WindowDialogMixin::close()
+ {
+ XBMC_TRACE;
+ w->bModal = false;
+ w->PulseActionEvent();
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_PYTHON_DIALOG, HACK_CUSTOM_ACTION_CLOSING,
+ 0, static_cast<void*>(w->window->get()));
+
+ w->iOldWindowId = 0;
+ }
+
+ bool WindowDialogMixin::IsDialogRunning() const { XBMC_TRACE; return w->window->isActive(); }
+
+ bool WindowDialogMixin::OnAction(const CAction &action)
+ {
+ XBMC_TRACE;
+ switch (action.GetID())
+ {
+ case HACK_CUSTOM_ACTION_OPENING:
+ {
+ // This is from the CGUIPythonWindowXMLDialog::Show_Internal
+ CServiceBroker::GetGUI()->GetWindowManager().RegisterDialog(w->window->get());
+ // active this dialog...
+ CGUIMessage msg(GUI_MSG_WINDOW_INIT,0,0);
+ w->OnMessage(msg);
+ w->window->setActive(true);
+ //! @todo Figure out how to clean up the CAction
+ return true;
+ }
+ break;
+
+ case HACK_CUSTOM_ACTION_CLOSING:
+ {
+ // This is from the CGUIPythonWindowXMLDialog::Show_Internal
+ w->window->get()->Close();
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+ }
+}
+
+
+
diff --git a/xbmc/interfaces/legacy/WindowDialogMixin.h b/xbmc/interfaces/legacy/WindowDialogMixin.h
new file mode 100644
index 0000000..5057e81
--- /dev/null
+++ b/xbmc/interfaces/legacy/WindowDialogMixin.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "Window.h"
+
+// These messages are a side effect of the way dialogs work through the
+// main ApplicationMessenger. At some point it would be nice to remove
+// the messenger and have direct (or even drive) communications.
+#define HACK_CUSTOM_ACTION_CLOSING -3
+#define HACK_CUSTOM_ACTION_OPENING -4
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ class WindowDialogMixin
+ {
+ private:
+ Window* w;
+
+ protected:
+ inline explicit WindowDialogMixin(Window* window) : w(window) {}
+
+ public:
+ virtual ~WindowDialogMixin() = default;
+
+ SWIGHIDDENVIRTUAL void show();
+ SWIGHIDDENVIRTUAL void close();
+
+#ifndef SWIG
+ SWIGHIDDENVIRTUAL bool IsDialogRunning() const;
+ SWIGHIDDENVIRTUAL bool OnAction(const CAction &action);
+#endif
+ };
+ }
+}
diff --git a/xbmc/interfaces/legacy/WindowException.h b/xbmc/interfaces/legacy/WindowException.h
new file mode 100644
index 0000000..ae35b57
--- /dev/null
+++ b/xbmc/interfaces/legacy/WindowException.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "Exception.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ XBMCCOMMONS_STANDARD_EXCEPTION(WindowException);
+ }
+}
+
+
diff --git a/xbmc/interfaces/legacy/WindowInterceptor.h b/xbmc/interfaces/legacy/WindowInterceptor.h
new file mode 100644
index 0000000..4890150
--- /dev/null
+++ b/xbmc/interfaces/legacy/WindowInterceptor.h
@@ -0,0 +1,210 @@
+/*
+ * 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 "Window.h"
+#include "guilib/GUIWindow.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+
+#ifndef SWIG
+ class Window;
+ class ref;
+
+ /**
+ * These two classes are closely associated with the Interceptor template below.
+ * For more detailed explanation, see that class.
+ */
+ class InterceptorBase
+ {
+ protected:
+ AddonClass::Ref<Window> window;
+ // This instance is in Window.cpp
+ static thread_local ref* upcallTls;
+
+ InterceptorBase() : window(NULL) { upcallTls = NULL; }
+
+ /**
+ * Calling up ONCE resets the upcall to to false. The reason is that when
+ * a call is recursive we cannot assume the ref has cleared the flag.
+ * so ...
+ *
+ * ref(window)->UpCall()
+ *
+ * during the context of 'UpCall' it's possible that another call will
+ * be made back on the window from the xbmc core side (this happens in
+ * sometimes in OnMessage). In that case, if upcall is still 'true', than
+ * the call will wrongly proceed back to the xbmc core side rather than
+ * to the Addon API side.
+ */
+ static bool up() { bool ret = ((upcallTls) != NULL); upcallTls = NULL; return ret; }
+ public:
+
+ virtual ~InterceptorBase() { if (window.isNotNull()) { window->interceptorClear(); } }
+
+ virtual CGUIWindow* get() = 0;
+
+ virtual void SetRenderOrder(int renderOrder) { }
+
+ virtual void setActive(bool active) { }
+ virtual bool isActive() { return false; }
+
+ friend class ref;
+ };
+
+ /**
+ * Guard class. But instead of managing memory or thread resources,
+ * any call made using the operator-> results in an 'upcall.' That is,
+ * it expects the call about to be made to have come from the XBMCAddon
+ * xbmcgui Window API and not from either the scripting language or the
+ * XBMC core Windowing system.
+ *
+ * This class is meant to hold references to instances of Interceptor<P>.
+ * see that template definition below.
+ */
+ class ref
+ {
+ InterceptorBase* w;
+ public:
+ inline explicit ref(InterceptorBase* b) : w(b) { w->upcallTls = this; }
+ inline ~ref() { w->upcallTls = NULL; }
+ inline CGUIWindow* operator->() { return w->get(); }
+ inline CGUIWindow* get() { return w->get(); }
+ };
+
+ /**
+ * The intention of this class is to intercept calls from
+ * multiple points in the CGUIWindow class hierarchy and pass
+ * those calls over to the XBMCAddon API Window hierarchy. It
+ * is a class template that uses the type template parameter
+ * to determine the parent class.
+ *
+ * Since this class also maintains two way communication between
+ * the XBMCAddon API Window hierarchy and the core XBMC CGUIWindow
+ * hierarchy, it is used as a general bridge. For calls going
+ * TO the window hierarchy (as in callbacks) it operates in a
+ * straightforward manner. In the reverse direction (from the
+ * XBMCAddon hierarchy) it uses some hackery in a "smart pointer"
+ * overloaded -> operator.
+ */
+
+#define checkedb(methcall) ( window.isNotNull() ? window-> methcall : false )
+#define checkedv(methcall) { if (window.isNotNull()) window-> methcall ; }
+
+ template <class P /* extends CGUIWindow*/> class Interceptor :
+ public P, public InterceptorBase
+ {
+ std::string classname;
+ protected:
+ CGUIWindow* get() override { return this; }
+
+ public:
+ Interceptor(const char* specializedName,
+ Window* _window, int windowid) : P(windowid, ""),
+ classname("Interceptor<" + std::string(specializedName) + ">")
+ {
+#ifdef ENABLE_XBMC_TRACE_API
+ XBMCAddonUtils::TraceGuard tg;
+ CLog::Log(LOGDEBUG, "{}NEWADDON constructing {} 0x{:x}", tg.getSpaces(), classname,
+ (long)(((void*)this)));
+#endif
+ window.reset(_window);
+ P::SetLoadType(CGUIWindow::LOAD_ON_GUI_INIT);
+ }
+
+ Interceptor(const char* specializedName,
+ Window* _window, int windowid,
+ const char* xmlfile) : P(windowid, xmlfile),
+ classname("Interceptor<" + std::string(specializedName) + ">")
+ {
+#ifdef ENABLE_XBMC_TRACE_API
+ XBMCAddonUtils::TraceGuard tg;
+ CLog::Log(LOGDEBUG, "{}NEWADDON constructing {} 0x{:x}", tg.getSpaces(), classname,
+ (long)(((void*)this)));
+#endif
+ window.reset(_window);
+ P::SetLoadType(CGUIWindow::LOAD_ON_GUI_INIT);
+ }
+
+#ifdef ENABLE_XBMC_TRACE_API
+ ~Interceptor() override
+ {
+ XBMCAddonUtils::TraceGuard tg;
+ CLog::Log(LOGDEBUG, "{}NEWADDON LIFECYCLE destroying {} 0x{:x}", tg.getSpaces(), classname,
+ (long)(((void*)this)));
+ }
+#else
+ ~Interceptor() override = default;
+#endif
+
+ bool OnMessage(CGUIMessage& message) override
+ { XBMC_TRACE; return up() ? P::OnMessage(message) : checkedb(OnMessage(message)); }
+ bool OnAction(const CAction &action) override
+ { XBMC_TRACE; return up() ? P::OnAction(action) : checkedb(OnAction(action)); }
+
+ // NOTE!!: This ALWAYS skips up to the CGUIWindow instance.
+ bool OnBack(int actionId) override
+ { XBMC_TRACE; return up() ? CGUIWindow::OnBack(actionId) : checkedb(OnBack(actionId)); }
+
+ void OnDeinitWindow(int nextWindowID) override
+ { XBMC_TRACE; if(up()) P::OnDeinitWindow(nextWindowID); else checkedv(OnDeinitWindow(nextWindowID)); }
+
+ bool IsModalDialog() const override
+ {
+ XBMC_TRACE;
+ return checkedb(IsModalDialog());
+ }
+
+ bool IsDialogRunning() const override
+ {
+ XBMC_TRACE;
+ return checkedb(IsDialogRunning());
+ }
+ bool IsDialog() const override
+ {
+ XBMC_TRACE;
+ return checkedb(IsDialog());
+ }
+ bool IsMediaWindow() const override
+ {
+ XBMC_TRACE;
+ return checkedb(IsMediaWindow());
+ }
+
+ void SetRenderOrder(int renderOrder) override { XBMC_TRACE; P::m_renderOrder = renderOrder; }
+
+ void setActive(bool active) override { XBMC_TRACE; P::m_active = active; }
+ bool isActive() override { XBMC_TRACE; return P::m_active; }
+ };
+
+ template <class P /* extends CGUIWindow*/> class InterceptorDialog :
+ public Interceptor<P>
+ {
+ public:
+ InterceptorDialog(const char* specializedName,
+ Window* _window, int windowid) :
+ Interceptor<P>(specializedName, _window, windowid)
+ { }
+
+ InterceptorDialog(const char* specializedName,
+ Window* _window, int windowid,
+ const char* xmlfile) :
+ Interceptor<P>(specializedName, _window, windowid,xmlfile)
+ { }
+ };
+
+#undef checkedb
+#undef checkedv
+
+#endif
+ }
+}
diff --git a/xbmc/interfaces/legacy/WindowXML.cpp b/xbmc/interfaces/legacy/WindowXML.cpp
new file mode 100644
index 0000000..51a4fbf
--- /dev/null
+++ b/xbmc/interfaces/legacy/WindowXML.cpp
@@ -0,0 +1,527 @@
+/*
+ * 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 "WindowXML.h"
+
+#include "ServiceBroker.h"
+#include "WindowException.h"
+#include "WindowInterceptor.h"
+#include "addons/Addon.h"
+#include "addons/Skin.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/TextureManager.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <mutex>
+
+// These #defs are for WindowXML
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_LABELFILES 12
+
+#define A(x) interceptor->x
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ template class Interceptor<CGUIMediaWindow>;
+
+ /**
+ * This class extends the Interceptor<CGUIMediaWindow> in order to
+ * add behavior for a few more virtual functions that were unnecessary
+ * in the Window or WindowDialog.
+ */
+#define checkedb(methcall) ( window.isNotNull() ? xwin-> methcall : false )
+#define checkedv(methcall) { if (window.isNotNull()) xwin-> methcall ; }
+
+
+ //! @todo This should be done with template specialization
+ class WindowXMLInterceptor : public InterceptorDialog<CGUIMediaWindow>
+ {
+ WindowXML* xwin;
+ public:
+ WindowXMLInterceptor(WindowXML* _window, int windowid,const char* xmlfile) :
+ InterceptorDialog<CGUIMediaWindow>("CGUIMediaWindow",_window,windowid,xmlfile), xwin(_window)
+ { }
+
+ void AllocResources(bool forceLoad = false) override
+ { XBMC_TRACE; if(up()) CGUIMediaWindow::AllocResources(forceLoad); else checkedv(AllocResources(forceLoad)); }
+ void FreeResources(bool forceUnLoad = false) override
+ { XBMC_TRACE; if(up()) CGUIMediaWindow::FreeResources(forceUnLoad); else checkedv(FreeResources(forceUnLoad)); }
+ bool OnClick(int iItem, const std::string &player = "") override { XBMC_TRACE; return up() ? CGUIMediaWindow::OnClick(iItem, player) : checkedb(OnClick(iItem)); }
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override
+ { XBMC_TRACE; if(up()) CGUIMediaWindow::Process(currentTime,dirtyregions); else checkedv(Process(currentTime,dirtyregions)); }
+
+ // this is a hack to SKIP the CGUIMediaWindow
+ bool OnAction(const CAction &action) override
+ { XBMC_TRACE; return up() ? CGUIWindow::OnAction(action) : checkedb(OnAction(action)); }
+
+ protected:
+ // CGUIWindow
+ bool LoadXML(const std::string &strPath, const std::string &strPathLower) override
+ { XBMC_TRACE; return up() ? CGUIMediaWindow::LoadXML(strPath,strPathLower) : xwin->LoadXML(strPath,strPathLower); }
+
+ // CGUIMediaWindow
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override
+ { XBMC_TRACE; if (up()) CGUIMediaWindow::GetContextButtons(itemNumber,buttons); else xwin->GetContextButtons(itemNumber,buttons); }
+ bool Update(const std::string &strPath, bool) override
+ { XBMC_TRACE; return up() ? CGUIMediaWindow::Update(strPath) : xwin->Update(strPath); }
+ void SetupShares() override { XBMC_TRACE; if(up()) CGUIMediaWindow::SetupShares(); else checkedv(SetupShares()); }
+
+ friend class WindowXML;
+ friend class WindowXMLDialog;
+
+ };
+
+ WindowXML::~WindowXML() { XBMC_TRACE; deallocating(); }
+
+ WindowXML::WindowXML(const String& xmlFilename,
+ const String& scriptPath,
+ const String& defaultSkin,
+ const String& defaultRes,
+ bool isMedia) :
+ Window(true)
+ {
+ XBMC_TRACE;
+ RESOLUTION_INFO res;
+ std::string strSkinPath = g_SkinInfo->GetSkinPath(xmlFilename, &res);
+ m_isMedia = isMedia;
+
+ if (!CFileUtils::Exists(strSkinPath))
+ {
+ std::string str("none");
+ ADDON::AddonInfoPtr addonInfo =
+ std::make_shared<ADDON::CAddonInfo>(str, ADDON::AddonType::SKIN);
+ ADDON::CSkinInfo::TranslateResolution(defaultRes, res);
+
+ // Check for the matching folder for the skin in the fallback skins folder
+ std::string fallbackPath = URIUtils::AddFileToFolder(scriptPath, "resources", "skins");
+ std::string basePath = URIUtils::AddFileToFolder(fallbackPath, g_SkinInfo->ID());
+
+ strSkinPath = g_SkinInfo->GetSkinPath(xmlFilename, &res, basePath);
+
+ // Check for the matching folder for the skin in the fallback skins folder (if it exists)
+ if (CFileUtils::Exists(basePath))
+ {
+ addonInfo->SetPath(basePath);
+ std::shared_ptr<ADDON::CSkinInfo> skinInfo = std::make_shared<ADDON::CSkinInfo>(addonInfo, res);
+ skinInfo->Start();
+ strSkinPath = skinInfo->GetSkinPath(xmlFilename, &res);
+ }
+
+ if (!CFileUtils::Exists(strSkinPath))
+ {
+ // Finally fallback to the DefaultSkin as it didn't exist in either the XBMC Skin folder or the fallback skin folder
+ addonInfo->SetPath(URIUtils::AddFileToFolder(fallbackPath, defaultSkin));
+ std::shared_ptr<ADDON::CSkinInfo> skinInfo = std::make_shared<ADDON::CSkinInfo>(addonInfo, res);
+
+ skinInfo->Start();
+ strSkinPath = skinInfo->GetSkinPath(xmlFilename, &res);
+ if (!CFileUtils::Exists(strSkinPath))
+ throw WindowException("XML File for Window is missing");
+ }
+ }
+
+ m_scriptPath = scriptPath;
+// sXMLFileName = strSkinPath;
+
+ interceptor = new WindowXMLInterceptor(this, lockingGetNextAvailableWindowId(),strSkinPath.c_str());
+ setWindow(interceptor);
+ interceptor->SetCoordsRes(res);
+ }
+
+ int WindowXML::lockingGetNextAvailableWindowId()
+ {
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ return getNextAvailableWindowId();
+ }
+
+ void WindowXML::addItem(const Alternative<String, const ListItem*>& item, int position)
+ {
+ XBMC_TRACE;
+ // item could be deleted if the reference count is 0.
+ // so I MAY need to check prior to using a Ref just in
+ // case this object is managed by Python. I'm not sure
+ // though.
+ AddonClass::Ref<ListItem> ritem = item.which() == XBMCAddon::first ? ListItem::fromString(item.former()) : AddonClass::Ref<ListItem>(item.later());
+
+ // Tells the window to add the item to FileItem vector
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+
+ //----------------------------------------------------
+ // Former AddItem call
+ //AddItem(ritem->item, pos);
+ {
+ CFileItemPtr& fileItem = ritem->item;
+ if (position == INT_MAX || position > A(m_vecItems)->Size())
+ {
+ A(m_vecItems)->Add(fileItem);
+ }
+ else if (position < -1 && !(position*-1 < A(m_vecItems)->Size()))
+ {
+ A(m_vecItems)->AddFront(fileItem,0);
+ }
+ else
+ {
+ A(m_vecItems)->AddFront(fileItem,position);
+ }
+ A(m_viewControl).SetItems(*(A(m_vecItems)));
+ }
+ //----------------------------------------------------
+ }
+ }
+
+ void WindowXML::addItems(const std::vector<Alternative<String, const XBMCAddon::xbmcgui::ListItem* > > & items)
+ {
+ XBMC_TRACE;
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ for (auto item : items)
+ {
+ AddonClass::Ref<ListItem> ritem = item.which() == XBMCAddon::first ? ListItem::fromString(item.former()) : AddonClass::Ref<ListItem>(item.later());
+ CFileItemPtr& fileItem = ritem->item;
+ A(m_vecItems)->Add(fileItem);
+ }
+ A(m_viewControl).SetItems(*(A(m_vecItems)));
+ }
+
+
+ void WindowXML::removeItem(int position)
+ {
+ XBMC_TRACE;
+ // Tells the window to remove the item at the specified position from the FileItem vector
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ A(m_vecItems)->Remove(position);
+ A(m_viewControl).SetItems(*(A(m_vecItems)));
+ }
+
+ int WindowXML::getCurrentListPosition()
+ {
+ XBMC_TRACE;
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ int listPos = A(m_viewControl).GetSelectedItem();
+ return listPos;
+ }
+
+ void WindowXML::setCurrentListPosition(int position)
+ {
+ XBMC_TRACE;
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ A(m_viewControl).SetSelectedItem(position);
+ }
+
+ ListItem* WindowXML::getListItem(int position)
+ {
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ //CFileItemPtr fi = pwx->GetListItem(listPos);
+ CFileItemPtr fi;
+ {
+ if (position < 0 || position >= A(m_vecItems)->Size())
+ return new ListItem();
+ fi = A(m_vecItems)->Get(position);
+ }
+
+ if (fi == NULL)
+ {
+ throw WindowException("Index out of range (%i)",position);
+ }
+
+ ListItem* sListItem = new ListItem();
+ sListItem->item = fi;
+
+ // let's hope someone reference counts this.
+ return sListItem;
+ }
+
+ int WindowXML::getListSize()
+ {
+ XBMC_TRACE;
+ return A(m_vecItems)->Size();
+ }
+
+ void WindowXML::clearList()
+ {
+ XBMC_TRACE;
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ A(ClearFileItems());
+
+ A(m_viewControl).SetItems(*(A(m_vecItems)));
+ }
+
+ void WindowXML::setContainerProperty(const String& key, const String& value)
+ {
+ XBMC_TRACE;
+ A(m_vecItems)->SetProperty(key, value);
+ }
+
+ void WindowXML::setContent(const String& value)
+ {
+ XBMC_TRACE;
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ A(m_vecItems)->SetContent(value);
+ }
+
+ int WindowXML::getCurrentContainerId()
+ {
+ XBMC_TRACE;
+ XBMCAddonUtils::GuiLock lock(languageHook, false);
+ return A(m_viewControl.GetCurrentControl());
+ }
+
+ bool WindowXML::OnAction(const CAction &action)
+ {
+ XBMC_TRACE;
+ // do the base class window first, and the call to python after this
+ bool ret = ref(window)->OnAction(action); // we don't currently want the mediawindow actions here
+ // look at the WindowXMLInterceptor onAction, it skips
+ // the CGUIMediaWindow::OnAction and calls directly to
+ // CGUIWindow::OnAction
+ AddonClass::Ref<Action> inf(new Action(action));
+ invokeCallback(new CallbackFunction<WindowXML,AddonClass::Ref<Action> >(this,&WindowXML::onAction,inf.get()));
+ PulseActionEvent();
+ return ret;
+ }
+
+ bool WindowXML::OnMessage(CGUIMessage& message)
+ {
+#ifdef ENABLE_XBMC_TRACE_API
+ XBMC_TRACE;
+ CLog::Log(LOGDEBUG, "{}Message id:{}", _tg.getSpaces(), (int)message.GetMessage());
+#endif
+
+ //! @todo We shouldn't be dropping down to CGUIWindow in any of this ideally.
+ //! We have to make up our minds about what python should be doing and
+ //! what this side of things should be doing
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ return ref(window)->OnMessage(message);
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ ref(window)->OnMessage(message);
+ invokeCallback(new CallbackFunction<WindowXML>(this,&WindowXML::onInit));
+ PulseActionEvent();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_FOCUSED:
+ {
+ if (A(m_viewControl).HasControl(message.GetControlId()) &&
+ A(m_viewControl).GetCurrentControl() != message.GetControlId())
+ {
+ A(m_viewControl).SetFocused();
+ return true;
+ }
+ // check if our focused control is one of our category buttons
+ int iControl=message.GetControlId();
+
+ invokeCallback(new CallbackFunction<WindowXML,int>(this,&WindowXML::onFocus,iControl));
+ PulseActionEvent();
+ }
+ break;
+
+ case GUI_MSG_NOTIFY_ALL:
+ // most messages from GUI_MSG_NOTIFY_ALL break container content, whitelist working ones.
+ if (message.GetParam1() == GUI_MSG_PAGE_CHANGE || message.GetParam1() == GUI_MSG_WINDOW_RESIZE)
+ return A(CGUIMediaWindow::OnMessage(message));
+ return true;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl=message.GetSenderId();
+ // Handle Sort/View internally. Scripters shouldn't use ID 2, 3 or 4.
+ if (iControl == CONTROL_BTNSORTASC) // sort asc
+ {
+ CLog::Log(LOGINFO, "WindowXML: Internal asc/dsc button not implemented");
+ /*if (m_guiState.get())
+ m_guiState->SetNextSortOrder();
+ UpdateFileList();*/
+ return true;
+ }
+ else if (iControl == CONTROL_BTNSORTBY) // sort by
+ {
+ CLog::Log(LOGINFO, "WindowXML: Internal sort button not implemented");
+ /*if (m_guiState.get())
+ m_guiState->SetNextSortMethod();
+ UpdateFileList();*/
+ return true;
+ }
+
+ if(iControl && iControl != interceptor->GetID()) // pCallbackWindow && != this->GetID())
+ {
+ CGUIControl* controlClicked = interceptor->GetControl(iControl);
+
+ // The old python way used to check list AND SELECITEM method
+ // or if its a button, radiobutton.
+ // Its done this way for now to allow other controls without a
+ // python version like togglebutton to still raise a onAction event
+ if (controlClicked) // Will get problems if we the id is not on the window
+ // and we try to do GetControlType on it. So check to make sure it exists
+ {
+ if ((controlClicked->IsContainer() && (message.GetParam1() == ACTION_SELECT_ITEM || message.GetParam1() == ACTION_MOUSE_LEFT_CLICK)) || !controlClicked->IsContainer())
+ {
+ invokeCallback(new CallbackFunction<WindowXML,int>(this,&WindowXML::onClick,iControl));
+ PulseActionEvent();
+ return true;
+ }
+ else if (controlClicked->IsContainer() && message.GetParam1() == ACTION_MOUSE_DOUBLE_CLICK)
+ {
+ invokeCallback(new CallbackFunction<WindowXML,int>(this,&WindowXML::onDoubleClick,iControl));
+ PulseActionEvent();
+ return true;
+ }
+ else if (controlClicked->IsContainer() && message.GetParam1() == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ AddonClass::Ref<Action> inf(new Action(CAction(ACTION_CONTEXT_MENU)));
+ invokeCallback(new CallbackFunction<WindowXML,AddonClass::Ref<Action> >(this,&WindowXML::onAction,inf.get()));
+ PulseActionEvent();
+ return true;
+ }
+ // the core context menu can lead to all sort of issues right now when used with WindowXMLs, so lets intercept the corresponding message
+ else if (controlClicked->IsContainer() && message.GetParam1() == ACTION_CONTEXT_MENU)
+ return true;
+ }
+ }
+ }
+ break;
+ }
+
+ return A(CGUIMediaWindow::OnMessage(message));
+ }
+
+ void WindowXML::AllocResources(bool forceLoad /*= false */)
+ {
+ XBMC_TRACE;
+ std::string tmpDir = URIUtils::GetDirectory(ref(window)->GetProperty("xmlfile").asString());
+ std::string fallbackMediaPath;
+ URIUtils::GetParentPath(tmpDir, fallbackMediaPath);
+ URIUtils::RemoveSlashAtEnd(fallbackMediaPath);
+ m_mediaDir = fallbackMediaPath;
+
+ //CLog::Log(LOGDEBUG, "CGUIPythonWindowXML::AllocResources called: {}", fallbackMediaPath);
+ CServiceBroker::GetGUI()->GetTextureManager().AddTexturePath(m_mediaDir);
+ ref(window)->AllocResources(forceLoad);
+ CServiceBroker::GetGUI()->GetTextureManager().RemoveTexturePath(m_mediaDir);
+ }
+
+ void WindowXML::FreeResources(bool forceUnLoad /*= false */)
+ {
+ XBMC_TRACE;
+
+ ref(window)->FreeResources(forceUnLoad);
+ }
+
+ void WindowXML::Process(unsigned int currentTime, CDirtyRegionList &regions)
+ {
+ XBMC_TRACE;
+ CServiceBroker::GetGUI()->GetTextureManager().AddTexturePath(m_mediaDir);
+ ref(window)->Process(currentTime, regions);
+ CServiceBroker::GetGUI()->GetTextureManager().RemoveTexturePath(m_mediaDir);
+ }
+
+ bool WindowXML::OnClick(int iItem)
+ {
+ XBMC_TRACE;
+ // Hook Over calling CGUIMediaWindow::OnClick(iItem) results in it trying to PLAY the file item
+ // which if its not media is BAD and 99 out of 100 times undesirable.
+ return false;
+ }
+
+ bool WindowXML::OnDoubleClick(int iItem)
+ {
+ XBMC_TRACE;
+ return false;
+ }
+
+ void WindowXML::GetContextButtons(int itemNumber, CContextButtons &buttons)
+ {
+ XBMC_TRACE;
+ // maybe on day we can make an easy way to do this context menu
+ // with out this method overriding the MediaWindow version, it will display 'Add to Favorites'
+ }
+
+ bool WindowXML::LoadXML(const String &strPath, const String &strLowerPath)
+ {
+ XBMC_TRACE;
+ return A(CGUIWindow::LoadXML(strPath, strLowerPath));
+ }
+
+ void WindowXML::SetupShares()
+ {
+ XBMC_TRACE;
+ }
+
+ bool WindowXML::Update(const String &strPath)
+ {
+ XBMC_TRACE;
+ return true;
+ }
+
+ WindowXMLDialog::WindowXMLDialog(const String& xmlFilename, const String& scriptPath,
+ const String& defaultSkin,
+ const String& defaultRes) :
+ WindowXML(xmlFilename, scriptPath, defaultSkin, defaultRes),
+ WindowDialogMixin(this)
+ { XBMC_TRACE; }
+
+ WindowXMLDialog::~WindowXMLDialog() { XBMC_TRACE; deallocating(); }
+
+ bool WindowXMLDialog::OnMessage(CGUIMessage &message)
+ {
+ XBMC_TRACE;
+ if (message.GetMessage() == GUI_MSG_WINDOW_DEINIT)
+ return A(CGUIWindow::OnMessage(message));
+
+ return WindowXML::OnMessage(message);
+ }
+
+ bool WindowXMLDialog::OnAction(const CAction &action)
+ {
+ XBMC_TRACE;
+ return WindowDialogMixin::OnAction(action) ? true : WindowXML::OnAction(action);
+ }
+
+ void WindowXMLDialog::OnDeinitWindow(int nextWindowID)
+ {
+ XBMC_TRACE;
+ CServiceBroker::GetGUI()->GetWindowManager().RemoveDialog(interceptor->GetID());
+ WindowXML::OnDeinitWindow(nextWindowID);
+ }
+
+ bool WindowXMLDialog::LoadXML(const String &strPath, const String &strLowerPath)
+ {
+ XBMC_TRACE;
+ if (WindowXML::LoadXML(strPath, strLowerPath))
+ {
+ // Set the render order to the dialog's default in case it's not specified in the skin xml
+ // because this dialog is mapped to CGUIMediaWindow instead of CGUIDialog.
+ // This must be done here, because the render order will be reset before loading the skin xml.
+ if (ref(window)->GetRenderOrder() == RENDER_ORDER_WINDOW)
+ window->SetRenderOrder(RENDER_ORDER_DIALOG);
+ return true;
+ }
+ return false;
+ }
+
+ }
+
+}
diff --git a/xbmc/interfaces/legacy/WindowXML.h b/xbmc/interfaces/legacy/WindowXML.h
new file mode 100644
index 0000000..349b0a0
--- /dev/null
+++ b/xbmc/interfaces/legacy/WindowXML.h
@@ -0,0 +1,559 @@
+/*
+ * 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 "Window.h"
+#include "WindowDialogMixin.h"
+#include "swighelper.h"
+#include "windows/GUIMediaWindow.h"
+
+#include <limits.h>
+#include <vector>
+
+namespace XBMCAddon
+{
+ namespace xbmcgui
+ {
+ class ListItem;
+ class WindowXMLInterceptor;
+
+ //
+ /// \defgroup python_xbmcgui_window_xml Subclass - WindowXML
+ /// \ingroup python_xbmcgui_window
+ /// @{
+ /// @brief __GUI xml window class.__
+ ///
+ /// \python_class{ xbmcgui.WindowXML(xmlFilename, scriptPath[, defaultSkin, defaultRes]) }
+ ///
+ /// Creates a new xml file based window class.
+ ///
+ /// \note This class include also all calls from <b><c>\ref python_xbmcgui_window</c></b>.
+ ///
+ /// @param xmlFilename string - the name of the xml file to
+ /// look for.
+ /// @param scriptPath string - path to script. used to
+ /// fallback to if the xml doesn't exist in
+ /// the current skin. (eg xbmcaddon.Addon().getAddonInfo('path'))
+ /// @param defaultSkin [opt] string - name of the folder in the
+ /// skins path to look in for the xml.
+ /// (default='Default')
+ /// @param defaultRes [opt] string - default skins resolution.
+ /// (1080i, 720p, ntsc16x9, ntsc, pal16x9 or pal. default='720p')
+ /// @param isMedia [opt] bool - if False, create a regular window.
+ /// if True, create a mediawindow.
+ /// (default=False)
+ /// @throws Exception if more then 200 windows are created.
+ ///
+ /// \remark Skin folder structure is e.g. **resources/skins/Default/720p**
+ ///
+ /// Deleting this window will activate the old window that was active
+ /// and resets (not delete) all controls that are associated with this
+ /// window.
+ ///
+ ///--------------------------------------------------------------------------
+ /// @python_v18 New param added **isMedia**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// win = xbmcgui.WindowXML('script-Lyrics-main.xml', xbmcaddon.Addon().getAddonInfo('path'), 'default', '1080i', False)
+ /// win.doModal()
+ /// del win
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ ///
+ ///--------------------------------------------------------------------------
+ ///
+ /// On functions defined input variable <b><tt>controlId</tt> (GUI control identifier)</b>
+ /// is the on window.xml defined value behind type added with <tt><b>id="..."</b></tt> and
+ /// used to identify for changes there and on callbacks.
+ ///
+ /// ~~~~~~~~~~~~~{.xml}
+ /// <control type="label" id="31">
+ /// <description>Title Label</description>
+ /// ...
+ /// </control>
+ /// <control type="progress" id="32">
+ /// <description>progress control</description>
+ /// ...
+ /// </control>
+ /// ~~~~~~~~~~~~~
+ ///
+ //
+ class WindowXML : public Window
+ {
+ std::string sFallBackPath;
+
+ protected:
+#ifndef SWIG
+ /**
+ * This helper retrieves the next available id. It is doesn't
+ * assume that the global lock is already being held.
+ */
+ static int lockingGetNextAvailableWindowId();
+
+ WindowXMLInterceptor* interceptor;
+#endif
+
+ public:
+ WindowXML(const String& xmlFilename, const String& scriptPath,
+ const String& defaultSkin = "Default",
+ const String& defaultRes = "720p",
+ bool isMedia = false);
+ ~WindowXML() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_xml
+ /// @brief \python_func{ addItem(item[, position]) }
+ /// Add a new item to this Window List.
+ ///
+ /// @param item string, unicode or ListItem - item to add.
+ /// @param position [opt] integer - position of item to add. (NO Int = Adds to bottom,0 adds to top, 1 adds to one below from top,-1 adds to one above from bottom etc etc )
+ /// - If integer positions are greater than list size, negative positions will add to top of list, positive positions will add to bottom of list
+ ///
+ ///
+ ///
+ /// ----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.addItem('Reboot Kodi', 0)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ addItem(...);
+#else
+ SWIGHIDDENVIRTUAL void addItem(const Alternative<String, const ListItem*>& item, int position = INT_MAX);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_xml
+ /// @brief \python_func{ addItems(items) }
+ /// Add a list of items to to the window list.
+ ///
+ ///
+ /// @param items List - list of strings, unicode objects or ListItems to add.
+ ///
+ ///
+ /// ----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.addItems(['Reboot Kodi', 'Restart Kodi'])
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ addItems(...);
+#else
+ SWIGHIDDENVIRTUAL void addItems(const std::vector<Alternative<String, const XBMCAddon::xbmcgui::ListItem* > > & items);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_xml
+ /// @brief \python_func{ removeItem(position) }
+ /// Removes a specified item based on position, from the Window List.
+ ///
+ /// @param position integer - position of item to remove.
+ ///
+ ///
+ ///
+ /// ----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.removeItem(5)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ removeItem(...);
+#else
+ SWIGHIDDENVIRTUAL void removeItem(int position);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_xml
+ /// @brief \python_func{ getCurrentListPosition() }
+ /// Gets the current position in the Window List.
+ ///
+ ///
+ ///
+ /// ----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// pos = self.getCurrentListPosition()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getCurrentListPosition();
+#else
+ SWIGHIDDENVIRTUAL int getCurrentListPosition();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_xml
+ /// @brief \python_func{ setCurrentListPosition(position) }
+ /// Set the current position in the Window List.
+ ///
+ /// @param position integer - position of item to set.
+ ///
+ ///
+ ///
+ /// ----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.setCurrentListPosition(5)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setCurrentListPosition(...);
+#else
+ SWIGHIDDENVIRTUAL void setCurrentListPosition(int position);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_xml
+ /// @brief \python_func{ getListItem(position) }
+ /// Returns a given ListItem in this Window List.
+ ///
+ /// @param position integer - position of item to return.
+ ///
+ ///
+ ///
+ /// ----------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// listitem = self.getListItem(6)
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getListItem(...);
+#else
+ SWIGHIDDENVIRTUAL ListItem* getListItem(int position);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_xml
+ /// @brief \python_func{ getListSize() }
+ /// Returns the number of items in this Window List.
+ ///
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// listSize = self.getListSize()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getListSize();
+#else
+ SWIGHIDDENVIRTUAL int getListSize();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_xml
+ /// @brief \python_func{ clearList() }
+ /// Clear the Window List.
+ ///
+ /// ------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.clearList()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ clearList();
+#else
+ SWIGHIDDENVIRTUAL void clearList();
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_xml
+ /// @brief \python_func{ setContainerProperty(key, value) }
+ /// Sets a container property, similar to an infolabel.
+ ///
+ /// @param key string - property name.
+ /// @param value string or unicode - value of property.
+ ///
+ /// @note Key is NOT case sensitive.\n
+ /// You can use the above as keywords for arguments and skip certain
+ /// optional arguments.\n
+ /// Once you use a keyword, all following arguments require the keyword.
+ ///
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v17 Changed function from **setProperty** to **setContainerProperty**.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.setContainerProperty('Category', 'Newest')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setContainerProperty(...);
+#else
+ SWIGHIDDENVIRTUAL void setContainerProperty(const String &strProperty, const String &strValue);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_xml
+ /// @brief \python_func{ setContent(value) }
+ /// Sets the content type of the container.
+ ///
+ /// @param value string or unicode - content value.
+ ///
+ /// __Available content types__
+ /// | Name | Media |
+ /// |:-----------:|:-----------------------------------------|
+ /// | actors | Videos
+ /// | addons | Addons, Music, Pictures, Programs, Videos
+ /// | albums | Music, Videos
+ /// | artists | Music, Videos
+ /// | countries | Music, Videos
+ /// | directors | Videos
+ /// | files | Music, Videos
+ /// | games | Games
+ /// | genres | Music, Videos
+ /// | images | Pictures
+ /// | mixed | Music, Videos
+ /// | movies | Videos
+ /// | Musicvideos | Music, Videos
+ /// | playlists | Music, Videos
+ /// | seasons | Videos
+ /// | sets | Videos
+ /// | songs | Music
+ /// | studios | Music, Videos
+ /// | tags | Music, Videos
+ /// | tvshows | Videos
+ /// | videos | Videos
+ /// | years | Music, Videos
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v18 Added new function.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// self.setContent('movies')
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ setContent(...);
+#else
+ SWIGHIDDENVIRTUAL void setContent(const String &strValue);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcgui_window_xml
+ /// @brief \python_func{ getCurrentContainerId() }
+ /// Get the id of the currently visible container.
+ ///
+ /// ------------------------------------------------------------------------
+ /// @python_v17 Added new function.
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// container_id = self.getCurrentContainerId()
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ getCurrentContainerId(...);
+#else
+ SWIGHIDDENVIRTUAL int getCurrentContainerId();
+#endif
+
+#ifndef SWIG
+ // CGUIWindow
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ SWIGHIDDENVIRTUAL void AllocResources(bool forceLoad = false);
+ SWIGHIDDENVIRTUAL void FreeResources(bool forceUnLoad = false);
+ SWIGHIDDENVIRTUAL bool OnClick(int iItem);
+ SWIGHIDDENVIRTUAL bool OnDoubleClick(int iItem);
+ SWIGHIDDENVIRTUAL void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions);
+
+ bool IsMediaWindow() const override
+ {
+ XBMC_TRACE;
+ return m_isMedia;
+ };
+
+ // This method is identical to the Window::OnDeinitWindow method
+ // except it passes the message on to their respective parents.
+ // Since the respective parent differences are handled by the
+ // interceptor there's no reason to define this one here.
+// SWIGHIDDENVIRTUAL void OnDeinitWindow(int nextWindowID);
+
+
+ protected:
+ // CGUIWindow
+ SWIGHIDDENVIRTUAL bool LoadXML(const String &strPath, const String &strPathLower);
+
+ // CGUIMediaWindow
+ SWIGHIDDENVIRTUAL void GetContextButtons(int itemNumber, CContextButtons &buttons);
+ SWIGHIDDENVIRTUAL bool Update(const String &strPath);
+
+ void SetupShares();
+ String m_scriptPath;
+ String m_mediaDir;
+ bool m_isMedia;
+
+ friend class WindowXMLInterceptor;
+#endif
+ };
+ ///@}
+
+ // Ideally what we want here is a Dialog/Media Window. The problem is that these
+ // are two orthogonal discriminations of CGUIWindow and there wasn't a previous
+ // accounting for this possibility through the use of making CGUIWindow a
+ // virtual base class of the pertinent subclasses. So now we're left with
+ // no good solution.
+ //
+ // <strike>So here we're going to have the 'main' hierarchy (the one visible to SWIG)
+ // go the way intended - through WindowXML, but we're going to borrow dialog
+ // functionality from CGUIDialog by using it as a Mixin.</strike>
+ //
+ // jmarshall says that this class has no reason to inherit from CGUIMediaWindow.
+ // At some point this entire hierarchy needs to be reworked. The XML handling
+ // routines should be put in a mixin.
+ //
+ /// \defgroup python_xbmcgui_window_dialog_xml Subclass - WindowDialogXML
+ /// \ingroup python_xbmcgui_window_xml
+ /// @{
+ /// @brief __GUI xml window dialog__
+ ///
+ /// \python_class{ xbmcgui.WindowXMLDialog(xmlFilename, scriptPath[, defaultSkin, defaultRes]) }
+ ///
+ /// Creates a new xml file based window dialog class.
+ ///
+ /// @param xmlFilename string - the name of the xml file to
+ /// look for.
+ /// @param scriptPath string - path to script. used to
+ /// fallback to if the xml doesn't exist in
+ /// the current skin. (eg \ref python_xbmcaddon_Addon "xbmcaddon.Addon().getAddonInfo('path'))"
+ /// @param defaultSkin [opt] string - name of the folder in the
+ /// skins path to look in for the xml.
+ /// (default='Default')
+ /// @param defaultRes [opt] string - default skins resolution.
+ /// (1080i, 720p, ntsc16x9, ntsc, pal16x9 or pal. default='720p')
+ /// @throws Exception if more then 200 windows are created.
+ ///
+ /// @note Skin folder structure is e.g. **resources/skins/Default/720p**
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// **Example:**
+ /// ~~~~~~~~~~~~~{.py}
+ /// ..
+ /// dialog = xbmcgui.WindowXMLDialog('script-Lyrics-main.xml', xbmcaddon.Addon().getAddonInfo('path'), 'default', '1080i')
+ /// dialog.doModal()
+ /// del dialog
+ /// ..
+ /// ~~~~~~~~~~~~~
+ ///
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ /// On functions defined input variable <b><tt>controlId</tt> (GUI control identifier)</b>
+ /// is the on window.xml defined value behind type added with <tt><b>id="..."</b></tt> and
+ /// used to identify for changes there and on callbacks.
+ ///
+ /// ~~~~~~~~~~~~~{.xml}
+ /// <control type="label" id="31">
+ /// <description>Title Label</description>
+ /// ...
+ /// </control>
+ /// <control type="progress" id="32">
+ /// <description>progress control</description>
+ /// ...
+ /// </control>
+ /// ~~~~~~~~~~~~~
+ ///
+ //
+ class WindowXMLDialog : public WindowXML, private WindowDialogMixin
+ {
+ public:
+ WindowXMLDialog(const String& xmlFilename, const String& scriptPath,
+ const String& defaultSkin = "Default",
+ const String& defaultRes = "720p");
+
+ ~WindowXMLDialog() override;
+
+#ifndef SWIG
+ bool OnMessage(CGUIMessage& message) override;
+ bool IsDialogRunning() const override
+ {
+ XBMC_TRACE;
+ return WindowDialogMixin::IsDialogRunning();
+ }
+ bool IsDialog() const override
+ {
+ XBMC_TRACE;
+ return true;
+ };
+ bool IsModalDialog() const override
+ {
+ XBMC_TRACE;
+ return true;
+ };
+ bool IsMediaWindow() const override
+ {
+ XBMC_TRACE;
+ return false;
+ };
+ bool OnAction(const CAction& action) override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ bool LoadXML(const String& strPath, const String& strPathLower) override;
+
+ inline void show() override
+ {
+ XBMC_TRACE;
+ WindowDialogMixin::show();
+ }
+ inline void close() override
+ {
+ XBMC_TRACE;
+ WindowDialogMixin::close();
+ }
+
+ friend class DialogJumper;
+#endif
+ };
+ ///@}
+ }
+}
diff --git a/xbmc/interfaces/legacy/aojsonrpc.h b/xbmc/interfaces/legacy/aojsonrpc.h
new file mode 100644
index 0000000..7f1d875
--- /dev/null
+++ b/xbmc/interfaces/legacy/aojsonrpc.h
@@ -0,0 +1,30 @@
+/*
+ * 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 "interfaces/json-rpc/ITransportLayer.h"
+#include "interfaces/json-rpc/JSONRPC.h"
+
+class CVariant;
+
+class CAddOnTransport : public JSONRPC::ITransportLayer
+{
+public:
+ bool PrepareDownload(const char *path, CVariant &details, std::string &protocol) override { return false; }
+ bool Download(const char *path, CVariant& result) override { return false; }
+ int GetCapabilities() override { return JSONRPC::Response; }
+
+ class CAddOnClient : public JSONRPC::IClient
+ {
+ public:
+ int GetPermissionFlags() override { return JSONRPC::OPERATION_PERMISSION_ALL; }
+ int GetAnnouncementFlags() override { return 0; }
+ bool SetAnnouncementFlags(int flags) override { return true; }
+ };
+};
diff --git a/xbmc/interfaces/legacy/swighelper.h b/xbmc/interfaces/legacy/swighelper.h
new file mode 100644
index 0000000..15a822c
--- /dev/null
+++ b/xbmc/interfaces/legacy/swighelper.h
@@ -0,0 +1,100 @@
+/*
+ * 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
+
+/**
+ * SWIGHIDDENVIRTUAL allows the keyword 'virtual' to be there when the main
+ * Addon api is compiled, but be hidden from the SWIG code generator.
+ *
+ * This is to provide finer grain control over which methods are callbackable
+ * (is that a word? ...)
+ * into the scripting language, and which ones are not. True polymorphic
+ * behavior across the scripting language boundary will ONLY occur where
+ * the keyword 'virtual' is used. In other words, you can use the macro
+ * SWIGHIDDENVIRTUAL to 'hide' the polymorphic behavior from the scripting
+ * language using the macro instead.
+ *
+ * Note: You should not hide virtual destructors from the scripting language.
+ */
+#ifdef SWIG
+#define SWIGHIDDENVIRTUAL
+#else
+#define SWIGHIDDENVIRTUAL virtual
+#endif
+
+/**
+ * SWIG_CONSTANT_FROM_GETTER will define a constant in the scripting
+ * language from a getter in the Addon api and also provide the
+ * Addon api declaration. E.g. If you use:
+ *
+ * SWIG_CONSTANT_FROM_GETTER(int, MY_CONSTANT);
+ *
+ * ... in an Addon api header file then you need to define a function:
+ *
+ * int getMy_CONSTANT();
+ *
+ * ... in a .cpp file. That call will be made to determine the value
+ * of the constant in the scripting language.
+ */
+#ifdef SWIG
+#define SWIG_CONSTANT_FROM_GETTER(type,varname) %constant type varname = get##varname ()
+#else
+#define SWIG_CONSTANT_FROM_GETTER(type,varname) type get##varname ()
+#endif
+
+/**
+ * SWIG_CONSTANT defines a constant in SWIG from an already existing
+ * definition in the Addon api. E.g. a #define from the core window
+ * system like SORT_METHOD_PROGRAM_COUNT included in the api via
+ * a #include can be exposed to the scripting language using
+ * SWIG_CONSTANT(int,SORT_METHOD_PROGRAM_COUNT).
+ *
+ * This macro can be used when the constant name and the C++ reference
+ * look the same. When they look different see SWIG_CONSTANT2
+ *
+ * Note, this declaration is invisible to the API C++ code and can
+ * only be seen by the SWIG processor.
+ */
+#ifdef SWIG
+#define SWIG_CONSTANT(type,var) %constant type var = var
+#else
+#define SWIG_CONSTANT(type,var)
+#endif
+
+/**
+ * SWIG_CONSTANT2 defines a constant in SWIG from an already existing
+ * definition in the Addon api. E.g. a #define from the core window
+ * system like SORT_METHOD_VIDEO_YEAR included in the api via
+ * a #include can be exposed to the scripting language using
+ * SWIG_CONSTANT2(int,SORT_METHOD_VIDEO_YEAR,SORT_METHOD_YEAR).
+ *
+ * This macro can be used when the constant name and the C++ reference
+ * don't look the same. When they look the same see SWIG_CONSTANT
+ *
+ * Note, this declaration is invisible to the API C++ code and can
+ * only be seen by the SWIG processor.
+ */
+#ifdef SWIG
+#define SWIG_CONSTANT2(type,var,val) %constant type var = val
+#else
+#define SWIG_CONSTANT2(type,var,val)
+#endif
+
+/**
+* SWIG_IMMUTABLE defines a member as immutable i.e. read-only.
+*
+* Note, this declaration is invisible to the API C++ code and can
+* only be seen by the SWIG processor.
+*/
+#ifdef SWIG
+#define SWIG_IMMUTABLE(var) %feature("immutable"); var; %feature("immutable", "")
+#else
+#define SWIG_IMMUTABLE(var) var
+#endif
+
diff --git a/xbmc/interfaces/legacy/wsgi/CMakeLists.txt b/xbmc/interfaces/legacy/wsgi/CMakeLists.txt
new file mode 100644
index 0000000..cc29eb4
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/CMakeLists.txt
@@ -0,0 +1,13 @@
+if(MICROHTTPD_FOUND)
+ set(SOURCES WsgiErrorStream.cpp
+ WsgiInputStream.cpp
+ WsgiResponseBody.cpp
+ WsgiResponse.cpp)
+
+ set(HEADERS WsgiErrorStream.h
+ WsgiInputStream.h
+ WsgiResponse.h
+ WsgiResponseBody.h)
+
+ core_add_library(legacy_interface_wsgi)
+endif()
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.cpp b/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.cpp
new file mode 100644
index 0000000..b8096fe
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.cpp
@@ -0,0 +1,63 @@
+/*
+ * 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 "WsgiErrorStream.h"
+
+#include "network/httprequesthandler/python/HTTPPythonRequest.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ WsgiErrorStream::WsgiErrorStream()
+ : m_request(NULL)
+ { }
+
+ WsgiErrorStream::~WsgiErrorStream()
+ {
+ m_request = NULL;
+ }
+
+ void WsgiErrorStream::write(const String& str)
+ {
+ if (str.empty())
+ return;
+
+ String msg = str;
+ // remove a trailing \n
+ if (msg.at(msg.size() - 1) == '\n')
+ msg.erase(msg.size() - 1);
+
+ if (m_request != NULL)
+ CLog::Log(LOGERROR, "WSGI [{}]: {}", m_request->url, msg);
+ else
+ CLog::Log(LOGERROR, "WSGI: {}", msg);
+ }
+
+ void WsgiErrorStream::writelines(const std::vector<String>& seq)
+ {
+ if (seq.empty())
+ return;
+
+ String msg = StringUtils::Join(seq, "");
+ write(msg);
+ }
+
+#ifndef SWIG
+ void WsgiErrorStream::SetRequest(HTTPPythonRequest* request)
+ {
+ if (m_request != NULL)
+ return;
+
+ m_request = request;
+ }
+#endif
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.h b/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.h
new file mode 100644
index 0000000..e9e7694
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiErrorStream.h
@@ -0,0 +1,92 @@
+/*
+ * 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 "interfaces/legacy/AddonClass.h"
+
+#include <vector>
+
+struct HTTPPythonRequest;
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+
+ /// \defgroup python_xbmcwsgi_WsgiErrorStream WsgiErrorStream
+ /// \ingroup python_xbmcwsgi
+ /// @{
+ /// @brief **Represents the wsgi.errors stream to write error messages.**
+ ///
+ /// \python_class{ WsgiErrorStream() }
+ ///
+ /// This implementation writes the error messages to the application's log
+ /// file.
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ class WsgiErrorStream : public AddonClass
+ {
+ public:
+ WsgiErrorStream();
+ ~WsgiErrorStream() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcwsgi_WsgiErrorStream
+ /// \python_func{ flush() }
+ /// Since nothing is buffered this is a no-op.
+ ///
+ ///
+ flush();
+#else
+ inline void flush() { }
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcwsgi_WsgiErrorStream
+ /// \python_func{ write(str) }
+ /// Writes the given error message to the application's log file.
+ ///
+ /// @param str A string to save in log file
+ ///
+ /// @note A trailing `\n` is removed.
+ ///
+ write(...);
+#else
+ void write(const String& str);
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ ///
+ /// \ingroup python_xbmcwsgi_WsgiErrorStream
+ /// \python_func{ writelines(seq) }
+ /// Joins the given list of error messages (without any separator) into
+ /// a single error message which is written to the application's log file.
+ ///
+ /// @param seq A list of strings which will be logged.
+ ///
+ writelines(...);
+#else
+ void writelines(const std::vector<String>& seq);
+#endif
+
+#ifndef SWIG
+ /**
+ * Sets the given request.
+ */
+ void SetRequest(HTTPPythonRequest* request);
+
+ HTTPPythonRequest* m_request;
+#endif
+ };
+ /// @}
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiInputStream.cpp b/xbmc/interfaces/legacy/wsgi/WsgiInputStream.cpp
new file mode 100644
index 0000000..5146007
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiInputStream.cpp
@@ -0,0 +1,166 @@
+/*
+ * 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 "WsgiInputStream.h"
+
+#include "network/httprequesthandler/python/HTTPPythonRequest.h"
+#include "utils/StringUtils.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ WsgiInputStreamIterator::WsgiInputStreamIterator()
+ : m_data(),
+ m_line()
+ { }
+
+#ifndef SWIG
+ WsgiInputStreamIterator::WsgiInputStreamIterator(const String& data, bool end /* = false */)
+ : m_data(data),
+ m_remaining(end ? 0 : data.size()),
+ m_line()
+ { }
+#endif
+
+ WsgiInputStreamIterator::~WsgiInputStreamIterator() = default;
+
+ String WsgiInputStreamIterator::read(unsigned long size /* = 0 */) const
+ {
+ // make sure we don't try to read more data than we have
+ if (size <= 0 || size > m_remaining)
+ size = m_remaining;
+
+ // remember the current read offset
+ size_t offset = static_cast<size_t>(m_offset);
+
+ // adjust the read offset and the remaining data length
+ m_offset += size;
+ m_remaining -= size;
+
+ // return the data being requested
+ return m_data.substr(offset, size);
+ }
+
+ String WsgiInputStreamIterator::readline(unsigned long size /* = 0 */) const
+ {
+ // make sure we don't try to read more data than we have
+ if (size <= 0 || size > m_remaining)
+ size = m_remaining;
+
+ size_t offset = static_cast<size_t>(m_offset);
+ size_t pos = m_data.find('\n', offset);
+
+ // make sure pos has a valid value and includes the \n character
+ if (pos == std::string::npos)
+ pos = m_data.size();
+ else
+ pos += 1;
+
+ if (pos - offset < size)
+ size = pos - offset;
+
+ // read the next line
+ String line = read(size);
+
+ // remove any trailing \r\n
+ StringUtils::TrimRight(line, "\r\n");
+
+ return line;
+ }
+
+ std::vector<String> WsgiInputStreamIterator::readlines(unsigned long sizehint /* = 0 */) const
+ {
+ std::vector<String> lines;
+
+ // make sure we don't try to read more data than we have
+ if (sizehint <= 0 || sizehint > m_remaining)
+ sizehint = m_remaining;
+
+ do
+ {
+ // read a full line
+ String line = readline();
+
+ // adjust the sizehint by the number of bytes just read
+ sizehint -= line.length();
+
+ // add it to the list of read lines
+ lines.push_back(line);
+ } while (sizehint > 0);
+
+ return lines;
+ }
+
+#ifndef SWIG
+ WsgiInputStreamIterator& WsgiInputStreamIterator::operator++()
+ {
+ m_line.clear();
+
+ if (!end())
+ {
+ // read the next line
+ m_line = readline();
+ }
+
+ return *this;
+ }
+
+ bool WsgiInputStreamIterator::operator==(const WsgiInputStreamIterator& rhs)
+ {
+ return m_data == rhs.m_data &&
+ m_offset == rhs.m_offset &&
+ m_remaining == rhs.m_remaining;
+ }
+
+ bool WsgiInputStreamIterator::operator!=(const WsgiInputStreamIterator& rhs)
+ {
+ return !(*this == rhs);
+ }
+
+ String& WsgiInputStreamIterator::operator*()
+ {
+ return m_line;
+ }
+#endif
+
+ WsgiInputStream::WsgiInputStream()
+ : m_request(NULL)
+ { }
+
+ WsgiInputStream::~WsgiInputStream()
+ {
+ m_request = NULL;
+ }
+
+#ifndef SWIG
+ WsgiInputStreamIterator* WsgiInputStream::begin()
+ {
+ return new WsgiInputStreamIterator(m_data, false);
+ }
+
+ WsgiInputStreamIterator* WsgiInputStream::end()
+ {
+ return new WsgiInputStreamIterator(m_data, true);
+ }
+
+ void WsgiInputStream::SetRequest(HTTPPythonRequest* request)
+ {
+ if (m_request != NULL)
+ return;
+
+ m_request = request;
+
+ // set the remaining bytes to be read
+ m_data = m_request->requestContent;
+ m_offset = 0;
+ m_remaining = m_data.size();
+ }
+#endif
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiInputStream.h b/xbmc/interfaces/legacy/wsgi/WsgiInputStream.h
new file mode 100644
index 0000000..d7bf73f
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiInputStream.h
@@ -0,0 +1,120 @@
+/*
+ * 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 "interfaces/legacy/AddonClass.h"
+
+#include <vector>
+
+struct HTTPPythonRequest;
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+
+ // Iterator for the wsgi.input stream.
+ class WsgiInputStreamIterator : public AddonClass
+ {
+ public:
+ WsgiInputStreamIterator();
+ ~WsgiInputStreamIterator() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcwsgi_WsgiInputStream
+ /// \python_func{ read([size]) }
+ ///
+ /// Read a maximum of `<size>` bytes from the wsgi.input stream.
+ ///
+ /// @param size [opt] bytes to read
+ /// @return Returns the readed string
+ ///
+ read(...);
+#else
+ String read(unsigned long size = 0) const;
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcwsgi_WsgiInputStream
+ /// \python_func{ readline([size]) }
+ ///
+ /// Read a full line up to a maximum of `<size>` bytes from the wsgi.input
+ /// stream.
+ ///
+ /// @param size [opt] bytes to read
+ /// @return Returns the readed string line
+ ///
+ read(...);
+#else
+ String readline(unsigned long size = 0) const;
+#endif
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcwsgi_WsgiInputStream
+ /// \python_func{ readlines([sizehint]) }
+ ///
+ /// Read multiple full lines up to at least `<sizehint>` bytes from the
+ /// wsgi.input stream and return them as a list.
+ ///
+ /// @param sizehint [opt] bytes to read
+ /// @return Returns a list readed string lines
+ ///
+ read(...);
+#else
+ std::vector<String> readlines(unsigned long sizehint = 0) const;
+#endif
+
+#if !defined SWIG && !defined DOXYGEN_SHOULD_SKIP_THIS
+ WsgiInputStreamIterator(const String& data, bool end = false);
+
+ WsgiInputStreamIterator& operator++();
+ bool operator==(const WsgiInputStreamIterator& rhs);
+ bool operator!=(const WsgiInputStreamIterator& rhs);
+ String& operator*();
+ inline bool end() const { return m_remaining <= 0; }
+
+ protected:
+ String m_data;
+ mutable unsigned long m_offset = 0;
+ mutable unsigned long m_remaining = 0;
+
+ private:
+ String m_line;
+#endif
+ };
+
+ /// \defgroup python_xbmcwsgi_WsgiInputStream WsgiInputStream
+ /// \ingroup python_xbmcwsgi
+ /// @{
+ /// @brief **Represents the wsgi.input stream to access data from a HTTP request.**
+ ///
+ /// \python_class{ WsgiInputStream() }
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ class WsgiInputStream : public WsgiInputStreamIterator
+ {
+ public:
+ WsgiInputStream();
+ ~WsgiInputStream() override;
+
+#if !defined SWIG && !defined DOXYGEN_SHOULD_SKIP_THIS
+ WsgiInputStreamIterator* begin();
+ WsgiInputStreamIterator* end();
+
+ /**
+ * Sets the given request.
+ */
+ void SetRequest(HTTPPythonRequest* request);
+
+ HTTPPythonRequest* m_request;
+#endif
+ };
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiResponse.cpp b/xbmc/interfaces/legacy/wsgi/WsgiResponse.cpp
new file mode 100644
index 0000000..175e83d
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiResponse.cpp
@@ -0,0 +1,92 @@
+/*
+ * 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 "WsgiResponse.h"
+
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <inttypes.h>
+#include <utility>
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ WsgiResponse::WsgiResponse()
+ : m_responseHeaders(),
+ m_body()
+ { }
+
+ WsgiResponse::~WsgiResponse() = default;
+
+ WsgiResponseBody* WsgiResponse::operator()(const String& status, const std::vector<WsgiHttpHeader>& response_headers, void* exc_info /* = NULL */)
+ {
+ if (m_called)
+ {
+ CLog::Log(LOGWARNING, "WsgiResponse: callable has already been called");
+ return NULL;
+ }
+
+ m_called = true;
+
+ // parse the status
+ if (!status.empty())
+ {
+ std::vector<String> statusParts = StringUtils::Split(status, ' ', 2);
+ if (statusParts.size() == 2 && StringUtils::IsNaturalNumber(statusParts.front()))
+ {
+ int64_t parsedStatus = strtol(statusParts.front().c_str(), NULL, 0);
+ if (parsedStatus >= MHD_HTTP_OK && parsedStatus <= MHD_HTTP_NOT_EXTENDED)
+ m_status = static_cast<int>(parsedStatus);
+ else
+ CLog::Log(LOGWARNING, "WsgiResponse: invalid status number {} in \"{}\" provided",
+ parsedStatus, status);
+ }
+ else
+ CLog::Log(LOGWARNING, "WsgiResponse: invalid status \"{}\" provided", status);
+ }
+ else
+ CLog::Log(LOGWARNING, "WsgiResponse: empty status provided");
+
+ // copy the response headers
+ for (const auto& headerIt : response_headers)
+ m_responseHeaders.insert({headerIt.first(), headerIt.second()});
+
+ return &m_body;
+ }
+
+#ifndef SWIG
+ void WsgiResponse::Append(const std::string& data)
+ {
+ if (!data.empty())
+ m_body.m_data.append(data);
+ }
+
+ bool WsgiResponse::Finalize(HTTPPythonRequest* request) const
+ {
+ if (request == NULL || !m_called)
+ return false;
+
+ // copy the response status
+ request->responseStatus = m_status;
+
+ // copy the response headers
+ if (m_status >= MHD_HTTP_OK && m_status < MHD_HTTP_BAD_REQUEST)
+ request->responseHeaders.insert(m_responseHeaders.begin(), m_responseHeaders.end());
+ else
+ request->responseHeadersError.insert(m_responseHeaders.begin(), m_responseHeaders.end());
+
+ // copy the body
+ request->responseData = m_body.m_data;
+
+ return true;
+ }
+#endif
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiResponse.h b/xbmc/interfaces/legacy/wsgi/WsgiResponse.h
new file mode 100644
index 0000000..412e520
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiResponse.h
@@ -0,0 +1,77 @@
+/*
+ * 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 "interfaces/legacy/AddonClass.h"
+#include "interfaces/legacy/Tuple.h"
+#include "interfaces/legacy/wsgi/WsgiResponseBody.h"
+#include "network/httprequesthandler/python/HTTPPythonRequest.h"
+
+#include <vector>
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ typedef Tuple<String, String> WsgiHttpHeader;
+
+ /// \defgroup python_xbmcwsgi_WsgiResponse WsgiResponse
+ /// \ingroup python_xbmcwsgi
+ /// @{
+ /// @brief **Represents the start_response callable passed to a WSGI handler.**
+ ///
+ /// \python_class{ WsgiResponse() }
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ class WsgiResponse : public AddonClass
+ {
+ public:
+ WsgiResponse();
+ ~WsgiResponse() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcwsgi_WsgiInputStreamIterator
+ /// \python_func{ operator(status, response_headers[, exc_info]) }
+ ///
+ /// Callable implementation to initialize the response with the given
+ /// HTTP status and the HTTP response headers.
+ ///
+ /// @param status an HTTP status string like 200 OK or 404
+ /// Not Found.
+ /// @param response_headers a list of (header_name, header_value)
+ /// tuples. It must be a Python list. Each
+ /// header_name must be a valid HTTP header
+ /// field-name (as
+ /// @param exc_info [optional] python sys.exc_info() tuple.
+ /// This argument should be supplied by the
+ /// application only if start_response is
+ /// being called by an error
+ /// @return The write() method \ref python_xbmcwsgi_WsgiResponseBody "WsgiResponseBody"
+ ///
+ operator(...);
+#else
+ WsgiResponseBody* operator()(const String& status, const std::vector<WsgiHttpHeader>& response_headers, void* exc_info = NULL);
+#endif
+
+#ifndef SWIG
+ void Append(const std::string& data);
+
+ bool Finalize(HTTPPythonRequest* request) const;
+
+ private:
+ bool m_called = false;
+ int m_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ std::multimap<std::string, std::string> m_responseHeaders;
+
+ WsgiResponseBody m_body;
+#endif
+ };
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.cpp b/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.cpp
new file mode 100644
index 0000000..2e84319
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.cpp
@@ -0,0 +1,29 @@
+/*
+ * 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 "WsgiResponseBody.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ WsgiResponseBody::WsgiResponseBody()
+ : m_data()
+ { }
+
+ WsgiResponseBody::~WsgiResponseBody() = default;
+
+ void WsgiResponseBody::operator()(const String& data)
+ {
+ if (data.empty())
+ return;
+
+ m_data.append(data);
+ }
+ }
+}
diff --git a/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.h b/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.h
new file mode 100644
index 0000000..4f18583
--- /dev/null
+++ b/xbmc/interfaces/legacy/wsgi/WsgiResponseBody.h
@@ -0,0 +1,50 @@
+/*
+ * 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 "interfaces/legacy/AddonClass.h"
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ /// \defgroup python_xbmcwsgi_WsgiResponseBody WsgiResponseBody
+ /// \ingroup python_xbmcwsgi
+ /// @{
+ /// @brief **Represents the write callable returned by the start_response callable passed to a WSGI handler.**
+ ///
+ /// \python_class{ WsgiResponseBody() }
+ ///
+ ///-------------------------------------------------------------------------
+ ///
+ class WsgiResponseBody : public AddonClass
+ {
+ public:
+ WsgiResponseBody();
+ ~WsgiResponseBody() override;
+
+#ifdef DOXYGEN_SHOULD_USE_THIS
+ /// \ingroup python_xbmcwsgi_WsgiInputStreamIterator
+ /// \python_func{ operator(status, response_headers[, exc_info]) }
+ ///
+ /// Callable implementation to write data to the response.
+ ///
+ /// @param data string data to write
+ ///
+ operator()(...);
+#else
+ void operator()(const String& data);
+#endif
+
+#if !defined SWIG && !defined DOXYGEN_SHOULD_SKIP_THIS
+ String m_data;
+#endif
+ };
+ }
+}
diff --git a/xbmc/interfaces/python/AddonPythonInvoker.cpp b/xbmc/interfaces/python/AddonPythonInvoker.cpp
new file mode 100644
index 0000000..b6158a8
--- /dev/null
+++ b/xbmc/interfaces/python/AddonPythonInvoker.cpp
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2013-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.
+ */
+
+// python.h should always be included first before any other includes
+#include "AddonPythonInvoker.h"
+
+#include <utility>
+
+#include <Python.h>
+#include <osdefs.h>
+
+#define MODULE "xbmc"
+
+#define RUNSCRIPT_PREAMBLE \
+ "" \
+ "import " MODULE "\n" \
+ "class xbmcout:\n" \
+ " def __init__(self, loglevel=" MODULE ".LOGDEBUG):\n" \
+ " self.ll=loglevel\n" \
+ " def write(self, data):\n" \
+ " " MODULE ".log(data,self.ll)\n" \
+ " def close(self):\n" \
+ " " MODULE ".log('.')\n" \
+ " def flush(self):\n" \
+ " " MODULE ".log('.')\n" \
+ "import sys\n" \
+ "sys.stdout = xbmcout()\n" \
+ "sys.stderr = xbmcout(" MODULE ".LOGERROR)\n" \
+ ""
+
+#define RUNSCRIPT_SETUPTOOLS_HACK \
+ "" \
+ "import types,sys\n" \
+ "pkg_resources_code = \\\n" \
+ "\"\"\"\n" \
+ "def resource_filename(__name__,__path__):\n" \
+ " return __path__\n" \
+ "\"\"\"\n" \
+ "pkg_resources = types.ModuleType('pkg_resources')\n" \
+ "exec(pkg_resources_code, pkg_resources.__dict__)\n" \
+ "sys.modules['pkg_resources'] = pkg_resources\n" \
+ ""
+
+#define RUNSCRIPT_SETUP_ENVIROMENT_VARIABLES \
+ "" \
+ "from os import environ\n" \
+ "environ['SSL_CERT_FILE'] = 'system/certs/cacert.pem'\n" \
+ ""
+
+#define RUNSCRIPT_POSTSCRIPT \
+ "print('-->Python Interpreter Initialized<--')\n" \
+ ""
+
+#if defined(TARGET_ANDROID)
+
+#define RUNSCRIPT_COMPLIANT \
+ RUNSCRIPT_PREAMBLE RUNSCRIPT_SETUPTOOLS_HACK RUNSCRIPT_POSTSCRIPT
+
+#elif defined(TARGET_WINDOWS_STORE)
+
+#define RUNSCRIPT_COMPLIANT \
+ RUNSCRIPT_PREAMBLE RUNSCRIPT_SETUP_ENVIROMENT_VARIABLES RUNSCRIPT_POSTSCRIPT
+
+#else
+
+#define RUNSCRIPT_COMPLIANT \
+ RUNSCRIPT_PREAMBLE RUNSCRIPT_POSTSCRIPT
+
+#endif
+
+namespace PythonBindings {
+PyObject* PyInit_Module_xbmcdrm(void);
+PyObject* PyInit_Module_xbmcgui(void);
+PyObject* PyInit_Module_xbmc(void);
+PyObject* PyInit_Module_xbmcplugin(void);
+PyObject* PyInit_Module_xbmcaddon(void);
+PyObject* PyInit_Module_xbmcvfs(void);
+}
+
+using namespace PythonBindings;
+
+typedef struct
+{
+ const char *name;
+ CPythonInvoker::PythonModuleInitialization initialization;
+} PythonModule;
+
+static PythonModule PythonModules[] =
+ {
+ { "xbmcdrm", PyInit_Module_xbmcdrm },
+ { "xbmcgui", PyInit_Module_xbmcgui },
+ { "xbmc", PyInit_Module_xbmc },
+ { "xbmcplugin", PyInit_Module_xbmcplugin },
+ { "xbmcaddon", PyInit_Module_xbmcaddon },
+ { "xbmcvfs", PyInit_Module_xbmcvfs }
+ };
+
+CAddonPythonInvoker::CAddonPythonInvoker(ILanguageInvocationHandler *invocationHandler)
+ : CPythonInvoker(invocationHandler)
+{
+ PyImport_AppendInittab("xbmcdrm", PyInit_Module_xbmcdrm);
+ PyImport_AppendInittab("xbmcgui", PyInit_Module_xbmcgui);
+ PyImport_AppendInittab("xbmc", PyInit_Module_xbmc);
+ PyImport_AppendInittab("xbmcplugin", PyInit_Module_xbmcplugin);
+ PyImport_AppendInittab("xbmcaddon", PyInit_Module_xbmcaddon);
+ PyImport_AppendInittab("xbmcvfs", PyInit_Module_xbmcvfs);
+}
+
+CAddonPythonInvoker::~CAddonPythonInvoker() = default;
+
+std::map<std::string, CPythonInvoker::PythonModuleInitialization> CAddonPythonInvoker::getModules() const
+{
+ static std::map<std::string, PythonModuleInitialization> modules;
+ if (modules.empty())
+ {
+ for (const PythonModule& pythonModule : PythonModules)
+ modules.insert(std::make_pair(pythonModule.name, pythonModule.initialization));
+ }
+
+ return modules;
+}
+
+const char* CAddonPythonInvoker::getInitializationScript() const
+{
+ return RUNSCRIPT_COMPLIANT;
+}
diff --git a/xbmc/interfaces/python/AddonPythonInvoker.h b/xbmc/interfaces/python/AddonPythonInvoker.h
new file mode 100644
index 0000000..a846071
--- /dev/null
+++ b/xbmc/interfaces/python/AddonPythonInvoker.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2013-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 "interfaces/python/PythonInvoker.h"
+
+class CAddonPythonInvoker : public CPythonInvoker
+{
+public:
+ explicit CAddonPythonInvoker(ILanguageInvocationHandler *invocationHandler);
+ ~CAddonPythonInvoker() override;
+
+protected:
+ // overrides of CPythonInvoker
+ std::map<std::string, PythonModuleInitialization> getModules() const override;
+ const char* getInitializationScript() const override;
+};
diff --git a/xbmc/interfaces/python/CMakeLists.txt b/xbmc/interfaces/python/CMakeLists.txt
new file mode 100644
index 0000000..061cc2b
--- /dev/null
+++ b/xbmc/interfaces/python/CMakeLists.txt
@@ -0,0 +1,21 @@
+set(SOURCES AddonPythonInvoker.cpp
+ CallbackHandler.cpp
+ ContextItemAddonInvoker.cpp
+ LanguageHook.cpp
+ PythonInvoker.cpp
+ XBPython.cpp
+ swig.cpp
+ PyContext.cpp)
+
+set(HEADERS AddonPythonInvoker.h
+ CallbackHandler.h
+ ContextItemAddonInvoker.h
+ LanguageHook.h
+ preamble.h
+ PyContext.h
+ PythonInvoker.h
+ pythreadstate.h
+ swig.h
+ XBPython.h)
+
+core_add_library(python_interface)
diff --git a/xbmc/interfaces/python/CallbackHandler.cpp b/xbmc/interfaces/python/CallbackHandler.cpp
new file mode 100644
index 0000000..8fd31a7
--- /dev/null
+++ b/xbmc/interfaces/python/CallbackHandler.cpp
@@ -0,0 +1,65 @@
+/*
+ * 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 "CallbackHandler.h"
+
+#include "LanguageHook.h"
+
+namespace XBMCAddon
+{
+ namespace Python
+ {
+ /**
+ * We are ASS-U-MEing that this construction is happening
+ * within the context of a Python call. This way we can
+ * store off the PyThreadState to later verify that we're
+ * handling callbacks in the appropriate thread.
+ */
+ PythonCallbackHandler::PythonCallbackHandler()
+ {
+ XBMC_TRACE;
+ objectThreadState = PyThreadState_Get();
+ }
+
+ /**
+ * Now we are answering the question as to whether or not we are in the
+ * PyThreadState that we were in when we started.
+ */
+ bool PythonCallbackHandler::isStateOk(AddonClass* obj)
+ {
+ XBMC_TRACE;
+ PyThreadState* state = PyThreadState_Get();
+ if (objectThreadState == state)
+ {
+ // make sure the interpreter is still active.
+ AddonClass::Ref<XBMCAddon::Python::PythonLanguageHook> lh(XBMCAddon::Python::PythonLanguageHook::GetIfExists(state->interp));
+ if (lh.isNotNull() && lh->HasRegisteredAddonClassInstance(obj) && lh.get() == obj->GetLanguageHook())
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * For this method we expect the PyThreadState to be passed as the user
+ * data for the check.
+ *
+ * @todo This is a stupid way to get this information back to the handler.
+ * there should be a more language neutral means.
+ */
+ bool PythonCallbackHandler::shouldRemoveCallback(AddonClass* obj, void* threadState)
+ {
+ XBMC_TRACE;
+ if (threadState == objectThreadState)
+ return true;
+
+ // we also want to remove the callback if the language hook no longer exists.
+ // this is a belt-and-suspenders cleanup mechanism
+ return ! XBMCAddon::Python::PythonLanguageHook::IsAddonClassInstanceRegistered(obj);
+ }
+ }
+}
diff --git a/xbmc/interfaces/python/CallbackHandler.h b/xbmc/interfaces/python/CallbackHandler.h
new file mode 100644
index 0000000..b128b27
--- /dev/null
+++ b/xbmc/interfaces/python/CallbackHandler.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "interfaces/legacy/CallbackHandler.h"
+
+#include <Python.h>
+
+namespace XBMCAddon
+{
+ namespace Python
+ {
+ /**
+ * This class represents a specialization of the callback handler
+ * that specifically checks to see if we're in an OK thread state
+ * based on Python specifics.
+ */
+ class PythonCallbackHandler : public RetardedAsyncCallbackHandler
+ {
+ PyThreadState* objectThreadState;
+ public:
+
+ /**
+ * We are ASS-U-MEing that this construction is happening
+ * within the context of a Python call. This way we can
+ * store off the PyThreadState to later verify that we're
+ * handling callbacks in the appropriate thread.
+ */
+ PythonCallbackHandler();
+ bool isStateOk(AddonClass* obj) override;
+ bool shouldRemoveCallback(AddonClass* obj, void* threadState) override;
+ };
+ }
+}
diff --git a/xbmc/interfaces/python/ContextItemAddonInvoker.cpp b/xbmc/interfaces/python/ContextItemAddonInvoker.cpp
new file mode 100644
index 0000000..734193b
--- /dev/null
+++ b/xbmc/interfaces/python/ContextItemAddonInvoker.cpp
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+// python.h should always be included first before any other includes
+#include "ContextItemAddonInvoker.h"
+
+#include "interfaces/python/swig.h"
+#include "utils/log.h"
+
+#include <Python.h>
+#include <osdefs.h>
+
+
+CContextItemAddonInvoker::CContextItemAddonInvoker(
+ ILanguageInvocationHandler *invocationHandler,
+ const CFileItemPtr& item)
+ : CAddonPythonInvoker(invocationHandler), m_item(CFileItemPtr(new CFileItem(*item.get())))
+{
+}
+
+CContextItemAddonInvoker::~CContextItemAddonInvoker() = default;
+
+void CContextItemAddonInvoker::onPythonModuleInitialization(void* moduleDict)
+{
+ CAddonPythonInvoker::onPythonModuleInitialization(moduleDict);
+ if (m_item)
+ {
+ XBMCAddon::xbmcgui::ListItem* arg = new XBMCAddon::xbmcgui::ListItem(m_item);
+ PyObject* pyItem = PythonBindings::makePythonInstance(arg, true);
+ if (pyItem == Py_None || PySys_SetObject("listitem", pyItem) == -1)
+ {
+ CLog::Log(LOGERROR, "CPythonInvoker({}, {}): Failed to set sys parameter", GetId(),
+ m_sourceFile);
+ //FIXME: we should really abort execution
+ }
+ }
+}
diff --git a/xbmc/interfaces/python/ContextItemAddonInvoker.h b/xbmc/interfaces/python/ContextItemAddonInvoker.h
new file mode 100644
index 0000000..b2f4d03
--- /dev/null
+++ b/xbmc/interfaces/python/ContextItemAddonInvoker.h
@@ -0,0 +1,30 @@
+/*
+ * 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 "interfaces/python/AddonPythonInvoker.h"
+
+#include <memory>
+
+class CFileItem;
+typedef std::shared_ptr<CFileItem> CFileItemPtr;
+
+class CContextItemAddonInvoker : public CAddonPythonInvoker
+{
+public:
+ explicit CContextItemAddonInvoker(ILanguageInvocationHandler *invocationHandler,
+ const CFileItemPtr& item);
+ ~CContextItemAddonInvoker() override;
+
+protected:
+ void onPythonModuleInitialization(void* moduleDict) override;
+
+private:
+ const CFileItemPtr m_item;
+};
diff --git a/xbmc/interfaces/python/LanguageHook.cpp b/xbmc/interfaces/python/LanguageHook.cpp
new file mode 100644
index 0000000..0d4747f
--- /dev/null
+++ b/xbmc/interfaces/python/LanguageHook.cpp
@@ -0,0 +1,231 @@
+/*
+ * 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 "LanguageHook.h"
+
+#include "CallbackHandler.h"
+#include "PyContext.h"
+#include "ServiceBroker.h"
+#include "XBPython.h"
+#include "interfaces/legacy/AddonUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+namespace XBMCAddon
+{
+ namespace Python
+ {
+ static AddonClass::Ref<PythonLanguageHook> instance;
+
+ static CCriticalSection hooksMutex;
+ static std::map<PyInterpreterState*,AddonClass::Ref<PythonLanguageHook> > hooks;
+
+ // vtab instantiation
+ PythonLanguageHook::~PythonLanguageHook()
+ {
+ XBMC_TRACE;
+ XBMCAddon::LanguageHook::deallocating();
+ }
+
+ void PythonLanguageHook::MakePendingCalls()
+ {
+ XBMC_TRACE;
+ PythonCallbackHandler::makePendingCalls();
+ }
+
+ void PythonLanguageHook::DelayedCallOpen()
+ {
+ XBMC_TRACE;
+ PyGILLock::releaseGil();
+ }
+
+ void PythonLanguageHook::DelayedCallClose()
+ {
+ XBMC_TRACE;
+ PyGILLock::acquireGil();
+ }
+
+ void PythonLanguageHook::RegisterMe()
+ {
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(hooksMutex);
+ hooks[m_interp] = AddonClass::Ref<PythonLanguageHook>(this);
+ }
+
+ void PythonLanguageHook::UnregisterMe()
+ {
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(hooksMutex);
+ hooks.erase(m_interp);
+ }
+
+ static AddonClass::Ref<XBMCAddon::Python::PythonLanguageHook> g_languageHook;
+
+ // Ok ... we're going to get it even if it doesn't exist. If it doesn't exist then
+ // we're going to assume we're not in control of the interpreter. This (apparently)
+ // can be the case. E.g. Libspotify manages to call into a script using a ctypes
+ // extension but under the control of an Interpreter we know nothing about. In
+ // cases like this we're going to use a global interpreter
+ AddonClass::Ref<PythonLanguageHook> PythonLanguageHook::GetIfExists(PyInterpreterState* interp)
+ {
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(hooksMutex);
+ std::map<PyInterpreterState*,AddonClass::Ref<PythonLanguageHook> >::iterator iter = hooks.find(interp);
+ if (iter != hooks.end())
+ return iter->second;
+
+ // if we got here then we need to use the global one.
+ if (g_languageHook.isNull())
+ g_languageHook = new XBMCAddon::Python::PythonLanguageHook();
+
+ return g_languageHook;
+ }
+
+ bool PythonLanguageHook::IsAddonClassInstanceRegistered(AddonClass* obj)
+ {
+ for (const auto& iter : hooks)
+ {
+ if (iter.second->HasRegisteredAddonClassInstance(obj))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * PythonCallbackHandler expects to be instantiated PER AddonClass instance
+ * that is to be used as a callback. This is why this cannot be instantiated
+ * once.
+ *
+ * There is an expectation that this method is called from the Python thread
+ * that instantiated an AddonClass that has the potential for a callback.
+ *
+ * See RetardedAsyncCallbackHandler for more details.
+ * See PythonCallbackHandler for more details
+ * See PythonCallbackHandler::PythonCallbackHandler for more details
+ */
+ XBMCAddon::CallbackHandler* PythonLanguageHook::GetCallbackHandler()
+ {
+ XBMC_TRACE;
+ return new PythonCallbackHandler();
+ }
+
+ String PythonLanguageHook::GetAddonId()
+ {
+ XBMC_TRACE;
+
+ // Get a reference to the main module
+ // and global dictionary
+ PyObject* main_module = PyImport_AddModule("__main__");
+ if (!main_module)
+ {
+ CLog::Log(LOGDEBUG, "PythonLanguageHook::{}: __main__ returns null", __FUNCTION__);
+ return "";
+ }
+ PyObject* global_dict = PyModule_GetDict(main_module);
+ // Extract a reference to the function "func_name"
+ // from the global dictionary
+ PyObject* pyid = PyDict_GetItemString(global_dict, "__xbmcaddonid__");
+ if (pyid)
+ return PyUnicode_AsUTF8(pyid);
+ return "";
+ }
+
+ String PythonLanguageHook::GetAddonVersion()
+ {
+ XBMC_TRACE;
+ // Get a reference to the main module
+ // and global dictionary
+ PyObject* main_module = PyImport_AddModule("__main__");
+ if (!main_module)
+ {
+ CLog::Log(LOGDEBUG, "PythonLanguageHook::{}: __main__ returns null", __FUNCTION__);
+ return "";
+ }
+ PyObject* global_dict = PyModule_GetDict(main_module);
+ // Extract a reference to the function "func_name"
+ // from the global dictionary
+ PyObject* pyversion = PyDict_GetItemString(global_dict, "__xbmcapiversion__");
+ if (pyversion)
+ return PyUnicode_AsUTF8(pyversion);
+ return "";
+ }
+
+ long PythonLanguageHook::GetInvokerId()
+ {
+ XBMC_TRACE;
+
+ // Get a reference to the main module
+ // and global dictionary
+ PyObject* main_module = PyImport_AddModule("__main__");
+ if (!main_module)
+ {
+ CLog::Log(LOGDEBUG, "PythonLanguageHook::{}: __main__ returns null", __FUNCTION__);
+ return -1;
+ }
+ PyObject* global_dict = PyModule_GetDict(main_module);
+ // Extract a reference to the function "func_name"
+ // from the global dictionary
+ PyObject* pyid = PyDict_GetItemString(global_dict, "__xbmcinvokerid__");
+ if (pyid)
+ return PyLong_AsLong(pyid);
+ return -1;
+ }
+
+ void PythonLanguageHook::RegisterPlayerCallback(IPlayerCallback* player)
+ {
+ XBMC_TRACE;
+ CServiceBroker::GetXBPython().RegisterPythonPlayerCallBack(player);
+ }
+ void PythonLanguageHook::UnregisterPlayerCallback(IPlayerCallback* player)
+ {
+ XBMC_TRACE;
+ CServiceBroker::GetXBPython().UnregisterPythonPlayerCallBack(player);
+ }
+ void PythonLanguageHook::RegisterMonitorCallback(XBMCAddon::xbmc::Monitor* monitor)
+ {
+ XBMC_TRACE;
+ CServiceBroker::GetXBPython().RegisterPythonMonitorCallBack(monitor);
+ }
+ void PythonLanguageHook::UnregisterMonitorCallback(XBMCAddon::xbmc::Monitor* monitor)
+ {
+ XBMC_TRACE;
+ CServiceBroker::GetXBPython().UnregisterPythonMonitorCallBack(monitor);
+ }
+
+ bool PythonLanguageHook::WaitForEvent(CEvent& hEvent, unsigned int milliseconds)
+ {
+ XBMC_TRACE;
+ return CServiceBroker::GetXBPython().WaitForEvent(hEvent, milliseconds);
+ }
+
+ void PythonLanguageHook::RegisterAddonClassInstance(AddonClass* obj)
+ {
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> l(*this);
+ obj->Acquire();
+ currentObjects.insert(obj);
+ }
+
+ void PythonLanguageHook::UnregisterAddonClassInstance(AddonClass* obj)
+ {
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> l(*this);
+ if (currentObjects.erase(obj) > 0)
+ obj->Release();
+ }
+
+ bool PythonLanguageHook::HasRegisteredAddonClassInstance(AddonClass* obj)
+ {
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> l(*this);
+ return currentObjects.find(obj) != currentObjects.end();
+ }
+ }
+}
diff --git a/xbmc/interfaces/python/LanguageHook.h b/xbmc/interfaces/python/LanguageHook.h
new file mode 100644
index 0000000..6a4e0d0
--- /dev/null
+++ b/xbmc/interfaces/python/LanguageHook.h
@@ -0,0 +1,94 @@
+/*
+ * 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 "interfaces/legacy/LanguageHook.h"
+#include "threads/Event.h"
+
+#include <map>
+#include <mutex>
+#include <set>
+
+#include <Python.h>
+
+namespace XBMCAddon
+{
+ namespace Python
+ {
+ struct MutableInteger;
+
+ /**
+ * This class supplies the python specific functionality for
+ * plugging into the API. It's got a static only implementation
+ * and uses the singleton pattern for access.
+ */
+ class PythonLanguageHook : public XBMCAddon::LanguageHook
+ {
+ PyInterpreterState* m_interp;
+ CCriticalSection crit;
+ std::set<AddonClass*> currentObjects;
+
+ // This constructor is only used to instantiate the global LanguageHook
+ inline PythonLanguageHook() : m_interp(NULL) { }
+
+ public:
+
+ inline explicit PythonLanguageHook(PyInterpreterState* interp) : m_interp(interp) { }
+ ~PythonLanguageHook() override;
+
+ void DelayedCallOpen() override;
+ void DelayedCallClose() override;
+ void MakePendingCalls() override;
+
+ /**
+ * PythonCallbackHandler expects to be instantiated PER AddonClass instance
+ * that is to be used as a callback. This is why this cannot be instantiated
+ * once.
+ *
+ * There is an expectation that this method is called from the Python thread
+ * that instantiated an AddonClass that has the potential for a callback.
+ *
+ * See RetardedAsyncCallbackHandler for more details.
+ * See PythonCallbackHandler for more details
+ * See PythonCallbackHandler::PythonCallbackHandler for more details
+ */
+ XBMCAddon::CallbackHandler* GetCallbackHandler() override;
+
+ String GetAddonId() override;
+ String GetAddonVersion() override;
+ long GetInvokerId() override;
+
+ void RegisterPlayerCallback(IPlayerCallback* player) override;
+ void UnregisterPlayerCallback(IPlayerCallback* player) override;
+ void RegisterMonitorCallback(XBMCAddon::xbmc::Monitor* monitor) override;
+ void UnregisterMonitorCallback(XBMCAddon::xbmc::Monitor* monitor) override;
+ bool WaitForEvent(CEvent& hEvent, unsigned int milliseconds) override;
+
+ static AddonClass::Ref<PythonLanguageHook> GetIfExists(PyInterpreterState* interp);
+ static bool IsAddonClassInstanceRegistered(AddonClass* obj);
+
+ void RegisterAddonClassInstance(AddonClass* obj);
+ void UnregisterAddonClassInstance(AddonClass* obj);
+ bool HasRegisteredAddonClassInstance(AddonClass* obj);
+ inline bool HasRegisteredAddonClasses()
+ {
+ std::unique_lock<CCriticalSection> l(*this);
+ return !currentObjects.empty();
+ }
+
+ // You should hold the lock on the LanguageHook itself if you're
+ // going to do anything with the set that gets returned.
+ inline std::set<AddonClass*>& GetRegisteredAddonClasses() { return currentObjects; }
+
+ void UnregisterMe();
+ void RegisterMe();
+ };
+ }
+}
+
diff --git a/xbmc/interfaces/python/MethodType.groovy b/xbmc/interfaces/python/MethodType.groovy
new file mode 100644
index 0000000..18597fd
--- /dev/null
+++ b/xbmc/interfaces/python/MethodType.groovy
@@ -0,0 +1,14 @@
+/*
+ * 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.
+ */
+
+public enum MethodType
+{
+ constructor, destructor, method
+}
+
+
diff --git a/xbmc/interfaces/python/PyContext.cpp b/xbmc/interfaces/python/PyContext.cpp
new file mode 100644
index 0000000..3b64ac6
--- /dev/null
+++ b/xbmc/interfaces/python/PyContext.cpp
@@ -0,0 +1,117 @@
+/*
+ * 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 "PyContext.h"
+
+#include "utils/log.h"
+
+#include <Python.h>
+
+namespace XBMCAddon
+{
+ namespace Python
+ {
+ struct PyContextState
+ {
+ inline explicit PyContextState(bool pcreatedByGilRelease = false) :
+ state(NULL), createdByGilRelease(pcreatedByGilRelease) {}
+
+ int value = 0;
+ PyThreadState* state;
+ int gilReleasedDepth = 0;
+ bool createdByGilRelease;
+ };
+
+ static thread_local PyContextState* tlsPyContextState;
+
+ void* PyContext::enterContext()
+ {
+ PyContextState* cur = tlsPyContextState;
+ if (cur == NULL)
+ {
+ cur = new PyContextState();
+ tlsPyContextState = cur;
+ }
+
+ // increment the count
+ cur->value++;
+
+ return cur;
+ }
+
+ void PyContext::leaveContext()
+ {
+ // here we ASSUME that the constructor was called.
+ PyContextState* cur = tlsPyContextState;
+ cur->value--;
+ int curlevel = cur->value;
+
+ // this is a hack but ...
+ if (curlevel < 0)
+ {
+ CLog::Log(LOGERROR, "FATAL: PyContext closed more than opened");
+ curlevel = cur->value = 0;
+ }
+
+ if (curlevel == 0)
+ {
+ // clear the tlsPyContextState
+ tlsPyContextState = NULL;
+ delete cur;
+ }
+ }
+
+ void PyGILLock::releaseGil()
+ {
+ PyContextState* cur = tlsPyContextState;
+
+ // This means we're not within the python context, but
+ // because we may be in a thread spawned by python itself,
+ // we need to handle this.
+ if (!cur)
+ {
+ cur = static_cast<PyContextState*>(PyContext::enterContext());
+ cur->createdByGilRelease = true;
+ }
+
+ if (cur->gilReleasedDepth == 0) // true if we are at the outermost
+ {
+ PyThreadState* _save;
+ // this macro sets _save
+ {
+ Py_UNBLOCK_THREADS
+ }
+ cur->state = _save;
+ }
+ cur->gilReleasedDepth++; // the first time this goes to 1
+ }
+
+ void PyGILLock::acquireGil()
+ {
+ PyContextState* cur = tlsPyContextState;
+
+ // it's not possible for cur to be NULL (and if it is, we want to fail anyway).
+
+ // decrement the depth and make sure we're in the right place.
+ cur->gilReleasedDepth--;
+ if (cur->gilReleasedDepth == 0) // are we back to zero?
+ {
+ PyThreadState* _save = cur->state;
+ // This macros uses _save
+ {
+ Py_BLOCK_THREADS
+ }
+ cur->state = NULL; // clear the state to indicate we've reacquired the gil
+
+ // we clear it only if we created it on this level.
+ if (cur->createdByGilRelease)
+ PyContext::leaveContext();
+ }
+ }
+ }
+}
diff --git a/xbmc/interfaces/python/PyContext.h b/xbmc/interfaces/python/PyContext.h
new file mode 100644
index 0000000..216a45f
--- /dev/null
+++ b/xbmc/interfaces/python/PyContext.h
@@ -0,0 +1,49 @@
+/*
+ * 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
+
+namespace XBMCAddon
+{
+ namespace Python
+ {
+ class PyGILLock;
+
+ /**
+ * These classes should NOT be used with 'new'. They are expected to reside
+ * as stack instances and they act as "Guard" classes that track the
+ * current context.
+ */
+ class PyContext
+ {
+ protected:
+ friend class PyGILLock;
+ static void* enterContext();
+ static void leaveContext();
+ public:
+
+ inline PyContext() { enterContext(); }
+ inline ~PyContext() { leaveContext(); }
+ };
+
+ /**
+ * This class supports recursive locking of the GIL. It assumes that
+ * all Python GIL manipulation is done through this class so that it
+ * can monitor the current owner.
+ */
+ class PyGILLock
+ {
+ public:
+ static void releaseGil();
+ static void acquireGil();
+
+ inline PyGILLock() { releaseGil(); }
+ inline ~PyGILLock() { acquireGil(); }
+ };
+ }
+}
diff --git a/xbmc/interfaces/python/PythonInvoker.cpp b/xbmc/interfaces/python/PythonInvoker.cpp
new file mode 100644
index 0000000..1e9d344
--- /dev/null
+++ b/xbmc/interfaces/python/PythonInvoker.cpp
@@ -0,0 +1,724 @@
+/*
+ * Copyright (C) 2013-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.
+ */
+
+// clang-format off
+// python.h should always be included first before any other includes
+#include <mutex>
+#include <Python.h>
+// clang-format on
+
+#include "PythonInvoker.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/python/PyContext.h"
+#include "interfaces/python/pythreadstate.h"
+#include "interfaces/python/swig.h"
+#include "messaging/ApplicationMessenger.h"
+#include "threads/SingleLock.h"
+#include "threads/SystemClock.h"
+#include "utils/CharsetConverter.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+// clang-format off
+// This breaks fmt because of SEP define, don't include
+// before anything that includes logging
+#include <osdefs.h>
+// clang-format on
+
+#include <cassert>
+#include <iterator>
+
+#ifdef TARGET_WINDOWS
+extern "C" FILE* fopen_utf8(const char* _Filename, const char* _Mode);
+#else
+#define fopen_utf8 fopen
+#endif
+
+#define GC_SCRIPT \
+ "import gc\n" \
+ "gc.collect(2)\n"
+
+#define PY_PATH_SEP DELIM
+
+// Time before ill-behaved scripts are terminated
+#define PYTHON_SCRIPT_TIMEOUT 5000ms // ms
+
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+#define PythonModulesSize sizeof(PythonModules) / sizeof(PythonModule)
+
+CCriticalSection CPythonInvoker::s_critical;
+
+static const std::string getListOfAddonClassesAsString(
+ XBMCAddon::AddonClass::Ref<XBMCAddon::Python::PythonLanguageHook>& languageHook)
+{
+ std::string message;
+ std::unique_lock<CCriticalSection> l(*(languageHook.get()));
+ const std::set<XBMCAddon::AddonClass*>& acs = languageHook->GetRegisteredAddonClasses();
+ bool firstTime = true;
+ for (const auto& iter : acs)
+ {
+ if (!firstTime)
+ message += ",";
+ else
+ firstTime = false;
+ message += iter->GetClassname();
+ }
+
+ return message;
+}
+
+CPythonInvoker::CPythonInvoker(ILanguageInvocationHandler* invocationHandler)
+ : ILanguageInvoker(invocationHandler), m_threadState(NULL), m_stop(false)
+{
+}
+
+CPythonInvoker::~CPythonInvoker()
+{
+ // nothing to do for the default invoker used for registration with the
+ // CScriptInvocationManager
+ if (GetId() < 0)
+ return;
+
+ if (GetState() < InvokerStateExecutionDone)
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}): waiting for python thread \"{}\" to stop", GetId(),
+ (!m_sourceFile.empty() ? m_sourceFile : "unknown script"));
+ Stop(true);
+ pulseGlobalEvent();
+
+ onExecutionFinalized();
+}
+
+bool CPythonInvoker::Execute(
+ const std::string& script,
+ const std::vector<std::string>& arguments /* = std::vector<std::string>() */)
+{
+ if (script.empty())
+ return false;
+
+ if (!CFileUtils::Exists(script))
+ {
+ CLog::Log(LOGERROR, "CPythonInvoker({}): python script \"{}\" does not exist", GetId(),
+ CSpecialProtocol::TranslatePath(script));
+ return false;
+ }
+
+ if (!onExecutionInitialized())
+ return false;
+
+ return ILanguageInvoker::Execute(script, arguments);
+}
+
+bool CPythonInvoker::execute(const std::string& script, const std::vector<std::string>& arguments)
+{
+ std::vector<std::wstring> w_arguments;
+ for (const auto& argument : arguments)
+ {
+ std::wstring w_argument;
+ g_charsetConverter.utf8ToW(argument, w_argument);
+ w_arguments.push_back(w_argument);
+ }
+ return execute(script, w_arguments);
+}
+
+bool CPythonInvoker::execute(const std::string& script, std::vector<std::wstring>& arguments)
+{
+ // copy the code/script into a local string buffer
+ m_sourceFile = script;
+ std::set<std::string> pythonPath;
+
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): start processing", GetId(), m_sourceFile);
+
+ std::string realFilename(CSpecialProtocol::TranslatePath(m_sourceFile));
+ std::string scriptDir = URIUtils::GetDirectory(realFilename);
+ URIUtils::RemoveSlashAtEnd(scriptDir);
+
+ // set m_threadState if it's not set.
+ PyThreadState* l_threadState = nullptr;
+ bool newInterp = false;
+ {
+ if (!m_threadState)
+ {
+#if PY_VERSION_HEX < 0x03070000
+ // this is a TOTAL hack. We need the GIL but we need to borrow a PyThreadState in order to get it
+ // as of Python 3.2 since PyEval_AcquireLock is deprecated
+ extern PyThreadState* savestate;
+ PyEval_RestoreThread(savestate);
+#else
+ PyThreadState* ts = PyInterpreterState_ThreadHead(PyInterpreterState_Main());
+ PyEval_RestoreThread(ts);
+#endif
+ l_threadState = Py_NewInterpreter();
+ PyEval_ReleaseThread(l_threadState);
+ if (l_threadState == NULL)
+ {
+ CLog::Log(LOGERROR, "CPythonInvoker({}, {}): FAILED to get thread m_threadState!", GetId(),
+ m_sourceFile);
+ return false;
+ }
+ newInterp = true;
+ }
+ else
+ l_threadState = m_threadState;
+ }
+
+ // get the GIL
+ PyEval_RestoreThread(l_threadState);
+ if (newInterp)
+ {
+ m_languageHook = new XBMCAddon::Python::PythonLanguageHook(l_threadState->interp);
+ m_languageHook->RegisterMe();
+
+ onInitialization();
+ setState(InvokerStateInitialized);
+
+ if (realFilename == m_sourceFile)
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): the source file to load is \"{}\"", GetId(),
+ m_sourceFile, m_sourceFile);
+ else
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): the source file to load is \"{}\" (\"{}\")",
+ GetId(), m_sourceFile, m_sourceFile, realFilename);
+
+ // get path from script file name and add python path's
+ // this is used for python so it will search modules from script path first
+ pythonPath.emplace(scriptDir);
+
+ // add all addon module dependencies to path
+ if (m_addon)
+ {
+ std::set<std::string> paths;
+ getAddonModuleDeps(m_addon, paths);
+ for (const auto& it : paths)
+ pythonPath.emplace(it);
+ }
+ else
+ { // for backwards compatibility.
+ // we don't have any addon so just add all addon modules installed
+ CLog::Log(
+ LOGWARNING,
+ "CPythonInvoker({}): Script invoked without an addon. Adding all addon "
+ "modules installed to python path as fallback. This behaviour will be removed in future "
+ "version.",
+ GetId());
+ ADDON::VECADDONS addons;
+ CServiceBroker::GetAddonMgr().GetAddons(addons, ADDON::AddonType::SCRIPT_MODULE);
+ for (unsigned int i = 0; i < addons.size(); ++i)
+ pythonPath.emplace(CSpecialProtocol::TranslatePath(addons[i]->LibPath()));
+ }
+
+ PyObject* sysPath = PySys_GetObject("path");
+
+ std::for_each(pythonPath.crbegin(), pythonPath.crend(),
+ [&sysPath](const auto& path)
+ {
+ PyObject* pyPath = PyUnicode_FromString(path.c_str());
+ PyList_Insert(sysPath, 0, pyPath);
+
+ Py_DECREF(pyPath);
+ });
+
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}): full python path:", GetId());
+
+ Py_ssize_t pathListSize = PyList_Size(sysPath);
+
+ for (Py_ssize_t index = 0; index < pathListSize; index++)
+ {
+ if (index == 0 && !pythonPath.empty())
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}): custom python path:", GetId());
+
+ if (index == static_cast<ssize_t>(pythonPath.size()))
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}): default python path:", GetId());
+
+ PyObject* pyPath = PyList_GetItem(sysPath, index);
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}): {}", GetId(), PyUnicode_AsUTF8(pyPath));
+ }
+
+ { // set the m_threadState to this new interp
+ std::unique_lock<CCriticalSection> lockMe(m_critical);
+ m_threadState = l_threadState;
+ }
+ }
+ else
+ // swap in my thread m_threadState
+ PyThreadState_Swap(m_threadState);
+
+ PyObject* sysArgv = PyList_New(0);
+
+ if (arguments.empty())
+ arguments.emplace_back(L"");
+
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}): adding args:", GetId());
+
+ for (const auto& arg : arguments)
+ {
+ PyObject* pyArg = PyUnicode_FromWideChar(arg.c_str(), arg.length());
+ PyList_Append(sysArgv, pyArg);
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}): {}", GetId(), PyUnicode_AsUTF8(pyArg));
+
+ Py_DECREF(pyArg);
+ }
+
+ PySys_SetObject("argv", sysArgv);
+
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): entering source directory {}", GetId(), m_sourceFile,
+ scriptDir);
+ PyObject* module = PyImport_AddModule("__main__");
+ PyObject* moduleDict = PyModule_GetDict(module);
+
+ // we need to check if we was asked to abort before we had inited
+ bool stopping = false;
+ {
+ GilSafeSingleLock lock(m_critical);
+ stopping = m_stop;
+ }
+
+ bool failed = false;
+ std::string exceptionType, exceptionValue, exceptionTraceback;
+ if (!stopping)
+ {
+ try
+ {
+ // run script from file
+ // We need to have python open the file because on Windows the DLL that python
+ // is linked against may not be the DLL that xbmc is linked against so
+ // passing a FILE* to python from an fopen has the potential to crash.
+
+ PyObject* pyRealFilename = Py_BuildValue("s", realFilename.c_str());
+ FILE* fp = _Py_fopen_obj(pyRealFilename, "rb");
+ Py_DECREF(pyRealFilename);
+
+ if (fp != NULL)
+ {
+ PyObject* f = PyUnicode_FromString(realFilename.c_str());
+ PyDict_SetItemString(moduleDict, "__file__", f);
+
+ onPythonModuleInitialization(moduleDict);
+
+ Py_DECREF(f);
+ setState(InvokerStateRunning);
+ XBMCAddon::Python::PyContext
+ pycontext; // this is a guard class that marks this callstack as being in a python context
+ executeScript(fp, realFilename, moduleDict);
+ }
+ else
+ CLog::Log(LOGERROR, "CPythonInvoker({}, {}): {} not found!", GetId(), m_sourceFile,
+ m_sourceFile);
+ }
+ catch (const XbmcCommons::Exception& e)
+ {
+ setState(InvokerStateFailed);
+ e.LogThrowMessage();
+ failed = true;
+ }
+ catch (...)
+ {
+ setState(InvokerStateFailed);
+ CLog::Log(LOGERROR, "CPythonInvoker({}, {}): failure in script", GetId(), m_sourceFile);
+ failed = true;
+ }
+ }
+
+ m_systemExitThrown = false;
+ InvokerState stateToSet;
+ if (!failed && !PyErr_Occurred())
+ {
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): script successfully run", GetId(), m_sourceFile);
+ stateToSet = InvokerStateScriptDone;
+ onSuccess();
+ }
+ else if (PyErr_ExceptionMatches(PyExc_SystemExit))
+ {
+ m_systemExitThrown = true;
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): script aborted", GetId(), m_sourceFile);
+ stateToSet = InvokerStateFailed;
+ onAbort();
+ }
+ else
+ {
+ stateToSet = InvokerStateFailed;
+
+ // if it failed with an exception we already logged the details
+ if (!failed)
+ {
+ PythonBindings::PythonToCppException* e = NULL;
+ if (PythonBindings::PythonToCppException::ParsePythonException(exceptionType, exceptionValue,
+ exceptionTraceback))
+ e = new PythonBindings::PythonToCppException(exceptionType, exceptionValue,
+ exceptionTraceback);
+ else
+ e = new PythonBindings::PythonToCppException();
+
+ e->LogThrowMessage();
+ delete e;
+ }
+
+ onError(exceptionType, exceptionValue, exceptionTraceback);
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // no need to do anything else because the script has already stopped
+ if (failed)
+ {
+ setState(stateToSet);
+ return true;
+ }
+
+ if (m_threadState)
+ {
+ // make sure all sub threads have finished
+ for (PyThreadState* old = nullptr; m_threadState != nullptr;)
+ {
+ PyThreadState* s = PyInterpreterState_ThreadHead(m_threadState->interp);
+ for (; s && s == m_threadState;)
+ s = PyThreadState_Next(s);
+
+ if (!s)
+ break;
+
+ if (old != s)
+ {
+ CLog::Log(LOGINFO, "CPythonInvoker({}, {}): waiting on thread {}", GetId(), m_sourceFile,
+ (uint64_t)s->thread_id);
+ old = s;
+ }
+
+ lock.unlock();
+ CPyThreadState pyState;
+ KODI::TIME::Sleep(100ms);
+ pyState.Restore();
+ lock.lock();
+ }
+ }
+
+ // pending calls must be cleared out
+ XBMCAddon::RetardedAsyncCallbackHandler::clearPendingCalls(m_threadState);
+
+ assert(m_threadState != nullptr);
+ PyEval_ReleaseThread(m_threadState);
+
+ setState(stateToSet);
+
+ return true;
+}
+
+void CPythonInvoker::executeScript(FILE* fp, const std::string& script, PyObject* moduleDict)
+{
+ if (fp == NULL || script.empty() || moduleDict == NULL)
+ return;
+
+ int m_Py_file_input = Py_file_input;
+ PyRun_FileExFlags(fp, script.c_str(), m_Py_file_input, moduleDict, moduleDict, 1, NULL);
+}
+
+FILE* CPythonInvoker::PyFile_AsFileWithMode(PyObject* py_file, const char* mode)
+{
+ PyObject* ret = PyObject_CallMethod(py_file, "flush", "");
+ if (ret == NULL)
+ return NULL;
+ Py_DECREF(ret);
+
+ int fd = PyObject_AsFileDescriptor(py_file);
+ if (fd == -1)
+ return NULL;
+
+ FILE* f = fdopen(fd, mode);
+ if (f == NULL)
+ {
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+ }
+
+ return f;
+}
+
+bool CPythonInvoker::stop(bool abort)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_stop = true;
+
+ if (!IsRunning() && !m_threadState)
+ return false;
+
+ if (m_threadState != NULL)
+ {
+ if (IsRunning())
+ {
+ setState(InvokerStateStopping);
+ lock.unlock();
+
+ PyEval_RestoreThread((PyThreadState*)m_threadState);
+
+ //tell xbmc.Monitor to call onAbortRequested()
+ if (m_addon)
+ {
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): trigger Monitor abort request", GetId(),
+ m_sourceFile);
+ AbortNotification();
+ }
+
+ PyEval_ReleaseThread(m_threadState);
+ }
+ else
+ //Release the lock while waiting for threads to finish
+ lock.unlock();
+
+ XbmcThreads::EndTime<> timeout(PYTHON_SCRIPT_TIMEOUT);
+ while (!m_stoppedEvent.Wait(15ms))
+ {
+ if (timeout.IsTimePast())
+ {
+ CLog::Log(LOGERROR,
+ "CPythonInvoker({}, {}): script didn't stop in {} seconds - let's kill it",
+ GetId(), m_sourceFile,
+ std::chrono::duration_cast<std::chrono::seconds>(PYTHON_SCRIPT_TIMEOUT).count());
+ break;
+ }
+
+ // We can't empty-spin in the main thread and expect scripts to be able to
+ // dismantle themselves. Python dialogs aren't normal XBMC dialogs, they rely
+ // on TMSG_GUI_PYTHON_DIALOG messages, so pump the message loop.
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ CServiceBroker::GetAppMessenger()->ProcessMessages();
+ }
+ }
+
+ lock.lock();
+
+ setState(InvokerStateExecutionDone);
+
+ // Useful for add-on performance metrics
+ if (!timeout.IsTimePast())
+ CLog::Log(LOGDEBUG, "CPythonInvoker({}, {}): script termination took {}ms", GetId(),
+ m_sourceFile, (PYTHON_SCRIPT_TIMEOUT - timeout.GetTimeLeft()).count());
+
+ // Since we released the m_critical it's possible that the state is cleaned up
+ // so we need to recheck for m_threadState == NULL
+ if (m_threadState != NULL)
+ {
+ {
+ // grabbing the PyLock while holding the m_critical is asking for a deadlock
+ CSingleExit ex2(m_critical);
+ PyEval_RestoreThread((PyThreadState*)m_threadState);
+ }
+
+
+ PyThreadState* state = PyInterpreterState_ThreadHead(m_threadState->interp);
+ while (state)
+ {
+ // Raise a SystemExit exception in python threads
+ Py_XDECREF(state->async_exc);
+ state->async_exc = PyExc_SystemExit;
+ Py_XINCREF(state->async_exc);
+ state = PyThreadState_Next(state);
+ }
+
+ // If a dialog entered its doModal(), we need to wake it to see the exception
+ pulseGlobalEvent();
+
+ PyEval_ReleaseThread(m_threadState);
+ }
+ lock.unlock();
+
+ setState(InvokerStateFailed);
+ }
+
+ return true;
+}
+
+// Always called from Invoker thread
+void CPythonInvoker::onExecutionDone()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (m_threadState != NULL)
+ {
+ CLog::Log(LOGDEBUG, "{}({}, {})", __FUNCTION__, GetId(), m_sourceFile);
+
+ PyEval_RestoreThread(m_threadState);
+
+ onDeinitialization();
+
+ // run the gc before finishing
+ //
+ // if the script exited by throwing a SystemExit exception then going back
+ // into the interpreter causes this python bug to get hit:
+ // http://bugs.python.org/issue10582
+ // and that causes major failures. So we are not going to go back in
+ // to run the GC if that's the case.
+ if (!m_stop && m_languageHook->HasRegisteredAddonClasses() && !m_systemExitThrown &&
+ PyRun_SimpleString(GC_SCRIPT) == -1)
+ CLog::Log(LOGERROR,
+ "CPythonInvoker({}, {}): failed to run the gc to clean up after running prior to "
+ "shutting down the Interpreter",
+ GetId(), m_sourceFile);
+
+ Py_EndInterpreter(m_threadState);
+
+ // If we still have objects left around, produce an error message detailing what's been left behind
+ if (m_languageHook->HasRegisteredAddonClasses())
+ CLog::Log(LOGWARNING,
+ "CPythonInvoker({}, {}): the python script \"{}\" has left several "
+ "classes in memory that we couldn't clean up. The classes include: {}",
+ GetId(), m_sourceFile, m_sourceFile, getListOfAddonClassesAsString(m_languageHook));
+
+ // unregister the language hook
+ m_languageHook->UnregisterMe();
+
+#if PY_VERSION_HEX < 0x03070000
+ PyEval_ReleaseLock();
+#else
+ PyThreadState_Swap(PyInterpreterState_ThreadHead(PyInterpreterState_Main()));
+ PyEval_SaveThread();
+#endif
+
+ // set stopped event - this allows ::stop to run and kill remaining threads
+ // this event has to be fired without holding m_critical
+ // also the GIL (PyEval_AcquireLock) must not be held
+ // if not obeyed there is still no deadlock because ::stop waits with timeout (smart one!)
+ m_stoppedEvent.Set();
+
+ m_threadState = nullptr;
+
+ setState(InvokerStateExecutionDone);
+ }
+ ILanguageInvoker::onExecutionDone();
+}
+
+void CPythonInvoker::onExecutionFailed()
+{
+ PyEval_SaveThread();
+
+ setState(InvokerStateFailed);
+ CLog::Log(LOGERROR, "CPythonInvoker({}, {}): abnormally terminating python thread", GetId(),
+ m_sourceFile);
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_threadState = NULL;
+
+ ILanguageInvoker::onExecutionFailed();
+}
+
+void CPythonInvoker::onInitialization()
+{
+ XBMC_TRACE;
+ {
+ GilSafeSingleLock lock(s_critical);
+ initializeModules(getModules());
+ }
+
+ // get a possible initialization script
+ const char* runscript = getInitializationScript();
+ if (runscript != NULL && strlen(runscript) > 0)
+ {
+ // redirecting default output to debug console
+ if (PyRun_SimpleString(runscript) == -1)
+ CLog::Log(LOGFATAL, "CPythonInvoker({}, {}): initialize error", GetId(), m_sourceFile);
+ }
+}
+
+void CPythonInvoker::onPythonModuleInitialization(void* moduleDict)
+{
+ if (m_addon.get() == NULL || moduleDict == NULL)
+ return;
+
+ PyObject* moduleDictionary = (PyObject*)moduleDict;
+
+ PyObject* pyaddonid = PyUnicode_FromString(m_addon->ID().c_str());
+ PyDict_SetItemString(moduleDictionary, "__xbmcaddonid__", pyaddonid);
+
+ ADDON::CAddonVersion version = m_addon->GetDependencyVersion("xbmc.python");
+ PyObject* pyxbmcapiversion = PyUnicode_FromString(version.asString().c_str());
+ PyDict_SetItemString(moduleDictionary, "__xbmcapiversion__", pyxbmcapiversion);
+
+ PyObject* pyinvokerid = PyLong_FromLong(GetId());
+ PyDict_SetItemString(moduleDictionary, "__xbmcinvokerid__", pyinvokerid);
+
+ CLog::Log(LOGDEBUG,
+ "CPythonInvoker({}, {}): instantiating addon using automatically obtained id of \"{}\" "
+ "dependent on version {} of the xbmc.python api",
+ GetId(), m_sourceFile, m_addon->ID(), version.asString());
+}
+
+void CPythonInvoker::onDeinitialization()
+{
+ XBMC_TRACE;
+}
+
+void CPythonInvoker::onError(const std::string& exceptionType /* = "" */,
+ const std::string& exceptionValue /* = "" */,
+ const std::string& exceptionTraceback /* = "" */)
+{
+ CPyThreadState releaseGil;
+ std::unique_lock<CCriticalSection> gc(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ CGUIDialogKaiToast* pDlgToast =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogKaiToast>(
+ WINDOW_DIALOG_KAI_TOAST);
+ if (pDlgToast != NULL)
+ {
+ std::string message;
+ if (m_addon && !m_addon->Name().empty())
+ message = StringUtils::Format(g_localizeStrings.Get(2102), m_addon->Name());
+ else
+ message = g_localizeStrings.Get(2103);
+ pDlgToast->QueueNotification(CGUIDialogKaiToast::Error, message, g_localizeStrings.Get(2104));
+ }
+}
+
+void CPythonInvoker::initializeModules(
+ const std::map<std::string, PythonModuleInitialization>& modules)
+{
+ for (const auto& module : modules)
+ {
+ if (!initializeModule(module.second))
+ CLog::Log(LOGWARNING, "CPythonInvoker({}, {}): unable to initialize python module \"{}\"",
+ GetId(), m_sourceFile, module.first);
+ }
+}
+
+bool CPythonInvoker::initializeModule(PythonModuleInitialization module)
+{
+ if (module == NULL)
+ return false;
+
+ return module() != nullptr;
+}
+
+void CPythonInvoker::getAddonModuleDeps(const ADDON::AddonPtr& addon, std::set<std::string>& paths)
+{
+ for (const auto& it : addon->GetDependencies())
+ {
+ //Check if dependency is a module addon
+ ADDON::AddonPtr dependency;
+ if (CServiceBroker::GetAddonMgr().GetAddon(it.id, dependency, ADDON::AddonType::SCRIPT_MODULE,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ std::string path = CSpecialProtocol::TranslatePath(dependency->LibPath());
+ if (paths.find(path) == paths.end())
+ {
+ // add it and its dependencies
+ paths.insert(path);
+ getAddonModuleDeps(dependency, paths);
+ }
+ }
+ }
+}
diff --git a/xbmc/interfaces/python/PythonInvoker.h b/xbmc/interfaces/python/PythonInvoker.h
new file mode 100644
index 0000000..dd093ed
--- /dev/null
+++ b/xbmc/interfaces/python/PythonInvoker.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2013-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 "interfaces/generic/ILanguageInvoker.h"
+#include "interfaces/legacy/Addon.h"
+#include "interfaces/python/LanguageHook.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+typedef struct _object PyObject;
+
+class CPythonInvoker : public ILanguageInvoker
+{
+public:
+ explicit CPythonInvoker(ILanguageInvocationHandler* invocationHandler);
+ ~CPythonInvoker() override;
+
+ bool Execute(const std::string& script,
+ const std::vector<std::string>& arguments = std::vector<std::string>()) override;
+
+ bool IsStopping() const override { return m_stop || ILanguageInvoker::IsStopping(); }
+
+ typedef PyObject* (*PythonModuleInitialization)();
+
+protected:
+ // implementation of ILanguageInvoker
+ bool execute(const std::string& script, const std::vector<std::string>& arguments) override;
+ virtual void executeScript(FILE* fp, const std::string& script, PyObject* moduleDict);
+ bool stop(bool abort) override;
+ void onExecutionDone() override;
+ void onExecutionFailed() override;
+
+ // custom virtual methods
+ virtual std::map<std::string, PythonModuleInitialization> getModules() const = 0;
+ virtual const char* getInitializationScript() const = 0;
+ virtual void onInitialization();
+ // actually a PyObject* but don't wanna draw Python.h include into the header
+ virtual void onPythonModuleInitialization(void* moduleDict);
+ virtual void onDeinitialization();
+
+ virtual void onSuccess() {}
+ virtual void onAbort() {}
+ virtual void onError(const std::string& exceptionType = "",
+ const std::string& exceptionValue = "",
+ const std::string& exceptionTraceback = "");
+
+ std::string m_sourceFile;
+ CCriticalSection m_critical;
+
+private:
+ void initializeModules(const std::map<std::string, PythonModuleInitialization>& modules);
+ bool initializeModule(PythonModuleInitialization module);
+ void getAddonModuleDeps(const ADDON::AddonPtr& addon, std::set<std::string>& paths);
+ bool execute(const std::string& script, std::vector<std::wstring>& arguments);
+ FILE* PyFile_AsFileWithMode(PyObject* py_file, const char* mode);
+
+ PyThreadState* m_threadState;
+ bool m_stop;
+ CEvent m_stoppedEvent;
+
+ XBMCAddon::AddonClass::Ref<XBMCAddon::Python::PythonLanguageHook> m_languageHook;
+ bool m_systemExitThrown = false;
+
+ static CCriticalSection s_critical;
+};
diff --git a/xbmc/interfaces/python/PythonSwig.cpp.template b/xbmc/interfaces/python/PythonSwig.cpp.template
new file mode 100644
index 0000000..24756ea
--- /dev/null
+++ b/xbmc/interfaces/python/PythonSwig.cpp.template
@@ -0,0 +1,942 @@
+<%
+/*
+ * 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.
+ */
+%>
+<%
+import Helper
+import SwigTypeParser
+import PythonTools
+
+import groovy.xml.XmlUtil
+import groovy.text.SimpleTemplateEngine
+import java.util.regex.Pattern
+
+/**
+ * All of the method nodes and all of the class nodes are used several
+ * times over, so they are pulled out once here.
+ */
+
+// ---------------------------------------------------------
+// initialize the SwigTypeParser with the module's typetables
+module.findAll( { it.name() == 'typetab' } ).each { SwigTypeParser.appendTypeTable(it) }
+// ---------------------------------------------------------
+
+// ---------------------------------------------------------
+// Flatten out all of the method/function nodes, whether inside
+// classes or not, into 'methods'
+List methods = module.depthFirst().findAll { it.name() == 'function' || it.name() == 'constructor' || it.name() == 'destructor' }
+// ---------------------------------------------------------
+
+// ---------------------------------------------------------
+// Flatten out all of the class nodes into 'classes'
+List classes = module.depthFirst().findAll { it.name() == 'class' }
+// ---------------------------------------------------------
+
+// ---------------------------------------------------------
+// Initialize the Helper with the type conversions
+Helper.setup(this,classes,
+ /**
+ * This is meant to contain mini-templates for converting the return type
+ * of the native call to be returned to the python caller.
+ */
+ [ 'void' : 'Py_INCREF(Py_None);\n ${result} = Py_None;',
+ 'long': '${result} = PyLong_FromLong(${api});',
+ 'unsigned long': '${result} = PyLong_FromLong(${api});',
+ 'bool': '${result} = ${api} ? Py_True : Py_False; Py_INCREF(${result});',
+ 'long long': '${result} = Py_BuildValue("L", ${api});',
+ 'int': '${result} = Py_BuildValue("i", ${api});',
+ 'unsigned int': '${result} = Py_BuildValue("I", ${api});',
+ 'double': '${result} = PyFloat_FromDouble(${api});',
+ 'float': '${result} = Py_BuildValue("f", static_cast<double>(${api}));',
+ 'std::string' : new File('typemaps/python.string.outtm'),
+ 'p.q(const).char' : '${result} = PyUnicode_FromString(${api});',
+ (Pattern.compile('''(p.){0,1}XbmcCommons::Buffer''')) : new File('typemaps/python.buffer.outtm'),
+ (Pattern.compile('''std::shared_ptr<\\(.*\\)>''')) : new File('typemaps/python.smart_ptr.outtm'),
+ (Pattern.compile('''std::unique_ptr<\\(.*\\)>''')) : new File('typemaps/python.smart_ptr.outtm'),
+ (Pattern.compile('''(p.){0,1}std::vector<\\(.*\\)>''')) : new File('typemaps/python.vector.outtm'),
+ (Pattern.compile('''(p.){0,1}Tuple<\\(.*\\)>''')) : new File('typemaps/python.Tuple.outtm'),
+ (Pattern.compile('''(p.){0,1}Alternative<\\(.*\\)>''')) : new File('typemaps/python.Alternative.outtm')
+ ], '${result} = makePythonInstance(${api},true);',
+ /**
+ * This is meant to contain mini-templates for converting the parameter types
+ * of the native call to be converted from the python types provided by the caller.
+ *
+ * Note: if the type can be handled by PythonTools.ltypeToFormatChar then it wont
+ * appear here as it gets converted directly within the PyArg_ParseTupleAndKeywords
+ * call.
+ */
+ [
+ 'std::string' : 'if (${slarg}) PyXBMCGetUnicodeString(${api},${slarg},false,"${api}","${method.@name}");',
+ (Pattern.compile('''(p.){0,1}std::vector<\\(.*\\)>''')) : new File('typemaps/python.vector.intm'),
+ (Pattern.compile('''(p.){0,1}Tuple(3){0,1}<\\(.*\\)>''')) : new File('typemaps/python.Tuple.intm'),
+ (Pattern.compile('''(p.){0,1}Alternative<\\(.*\\)>''')) : new File('typemaps/python.Alternative.intm'),
+ (Pattern.compile('''(r.){0,1}XbmcCommons::Buffer''')) : new File('typemaps/python.buffer.intm'),
+ (Pattern.compile('''(p.){0,1}std::map<\\(.*\\)>''')) : new File('typemaps/python.map.intm'),
+ (Pattern.compile('''(r.){0,1}XBMCAddon::Dictionary<\\(.*\\)>''')) : new File('typemaps/python.dict.intm'),
+ (Pattern.compile('''p.void''')) : '${api} = (void*)${slarg};',
+ 'bool' : '${api} = (PyLong_AsLong(${slarg}) == 0L ? false : true);',
+ 'long' : '${api} = PyLong_AsLong(${slarg});',
+ 'unsigned long' : '${api} = PyLong_AsUnsignedLong(${slarg});',
+ 'long long' : '${api} = PyLong_AsLongLong(${slarg});',
+ 'unsigned long long' : '${api} = PyLong_AsUnsignedLongLong(${slarg});',
+ 'int' : '${api} = (int)PyLong_AsLong(${slarg});',
+ 'double' : '${api} = PyFloat_AsDouble(${slarg});',
+ 'float' : '${api} = (float)PyFloat_AsDouble(${slarg});',
+ 'XBMCAddon::StringOrInt' : 'if (${slarg}) PyXBMCGetUnicodeString(${api},${slarg},PyLong_Check(${slarg}) || PyFloat_Check(${slarg}),"${api}","${method.@name}");'
+ ], '${api} = (${swigTypeParser.SwigType_str(ltype)})retrieveApiInstance(${slarg},"${ltype}","${helper.findNamespace(method)}","${helper.callingName(method)}");')
+// ---------------------------------------------------------
+
+/*******************************************************************************/
+/**
+ * The doMethod will actually write out the CPython method call for
+ * the method/function represented by the provided Node ('method').
+ */
+void doMethod(Node method, MethodType methodType)
+{
+ boolean isOperator = method.@name.startsWith("operator ")
+ boolean doAsMappingIndex = false
+ boolean doAsCallable = false
+
+ if (isOperator)
+ {
+ if("[]" == method.@name.substring(9))
+ doAsMappingIndex = true
+ else if("()" == method.@name.substring(9))
+ doAsCallable = true
+ else
+ return;
+ }
+
+ boolean constructor = methodType == MethodType.constructor
+
+ // if we're a constructor, but we're private, then we're outta here
+ if (constructor && method.@access != null && method.@access != "public")
+ return
+
+ boolean destructor = methodType == MethodType.destructor
+ List params = method?.parm
+ int numParams = params?.size()
+ String clazz = Helper.findFullClassName(method)
+ String returns = constructor ? 'p.' + clazz : (destructor ? 'void' : Helper.getReturnSwigType(method))
+ Node classnode = Helper.findClassNode(method)
+ String classNameAsVariable = clazz == null ? null : PythonTools.getClassNameAsVariable(classnode)
+ boolean useKeywordParsing = !('true' == classnode?.@feature_python_nokwds || 'true' == method?.@feature_python_nokwds)
+
+ // do the docs
+ if (!constructor && !destructor)
+ {
+ if (Helper.hasDoc(method))
+ {
+%>
+ PyDoc_STRVAR(${PythonTools.getPyMethodName(method,methodType)}__doc__,
+ ${PythonTools.makeDocString(method.doc[0])});
+<% }
+ }
+%>
+ static <% if(methodType == MethodType.destructor) { %>void<% } else { %>PyObject*<% } %> ${module.@name}_${PythonTools.getPyMethodName(method,methodType)} (<%= ((clazz == null) ? "PyObject" :
+ (constructor ? "PyTypeObject" : 'PyHolder')) %>* ${constructor ? 'pytype' : 'self'} <%
+ if (doAsMappingIndex) { %>, PyObject* py${params[0].@name}<% }
+ else if (methodType != MethodType.destructor) { %> , PyObject *args, PyObject *kwds <%} %> )
+ {
+ XBMC_TRACE;
+<% if (numParams > 0)
+ {
+ if (useKeywordParsing && !doAsMappingIndex)
+ { %>
+ static const char *keywords[] = {<%
+ params.each { %>
+ "${it.@name}",<% } %>
+ NULL};
+<% }
+ params.each {
+%>
+ ${SwigTypeParser.SwigType_str(SwigTypeParser.convertTypeToLTypeForParam(it.@type))} ${it.@name} ${it.@value != null ? ' = ' + it.@value : SwigTypeParser.SwigType_ispointer(it.@type) ? ' = nullptr' : ''};<%
+ if (!PythonTools.parameterCanBeUsedDirectly(it) && !doAsMappingIndex)
+ { %>
+ PyObject* py${it.@name} = NULL;<%
+ }
+ }
+ if (!doAsMappingIndex)
+ { %>
+ if (!${useKeywordParsing ? 'PyArg_ParseTupleAndKeywords' : 'PyArg_ParseTuple'}(
+ args,
+ <% if (useKeywordParsing) { %>kwds,<% } %>
+ "<%= PythonTools.makeFormatStringFromParameters(method) %>",
+ <% if (useKeywordParsing) { %>const_cast<char**>(keywords),<% } %><% params.eachWithIndex { param,i -> %>
+ &${PythonTools.parameterCanBeUsedDirectly(param) ? '' : 'py'}${param.@name}${i < params.size() - 1 ? "," : ""}<% } %>
+ ))
+ {
+ return NULL;
+ }
+
+<% }
+ }
+ // now actually invoke the method
+ if (returns != "void") { %> ${SwigTypeParser.SwigType_str(returns)} apiResult;<% }
+%>
+ try
+ {
+<%
+ // now do the input conversion if any are necessary
+ params.findAll({ !PythonTools.parameterCanBeUsedDirectly(it) || doAsMappingIndex }).each { %> ${Helper.getInConversion(it.@type, it.@name, 'py' + it.@name, method)} <% println() }
+%>
+<%
+ // check to see if this method is a call to a virtual function on a director class.
+ boolean isDirectorCall = Helper.isDirector(method)
+ if (isDirectorCall)
+ {
+%> // This is a director call coming from python so it explicitly calls the base class method.
+<%
+ }
+ // now do the method call itself
+ if (!destructor) {
+ if (constructor || !clazz) { %> XBMCAddon::SetLanguageHookGuard slhg(XBMCAddon::Python::PythonLanguageHook::GetIfExists(PyThreadState_Get()->interp).get());<% println() }
+%> <%
+ if (returns != "void") { %>apiResult = <% }
+ if (clazz && !constructor) {
+ %>((${clazz}*)retrieveApiInstance((PyObject*)self,&Ty${classNameAsVariable}_Type,"${Helper.callingName(method)}","${clazz}"))-> <%
+ }
+ if (constructor && classnode.@feature_director) {
+ %>(&(Ty${classNameAsVariable}_Type.pythonType) != pytype) ? new ${classNameAsVariable}_Director(<% params.eachWithIndex { param, i -> %> ${param.@name}${i < params.size() - 1 ? "," : ""} <% } %>) : <% }
+
+ // Here is the actual call ... if this is a Director we need to do an upCall (from Python)
+ if (isDirectorCall){ %>${clazz}::<% }
+ %>${Helper.callingName(method)}( <% params.eachWithIndex { param, i -> %> ${param.@name}${i < params.size() - 1 ? "," : ""} <% } %> );
+<%
+ if (constructor) { %> prepareForReturn(apiResult);<% }
+ } // close the 'if method is not a destructor'
+ else { // it is a destructor
+%>
+ ${clazz}* theObj = (${clazz}*)retrieveApiInstance((PyObject*)self,&Ty${classNameAsVariable}_Type,"~${Helper.callingName(method)}","${clazz}");
+ cleanForDealloc(theObj);
+<%
+ }
+%>
+ }
+ catch (const XBMCAddon::WrongTypeException& e)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage());
+ PyErr_SetString(PyExc_TypeError, e.GetExMessage()); <%
+ if (!destructor) { %>
+ return NULL; <%
+ } %>
+ }
+ catch (const XbmcCommons::Exception& e)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage());
+ PyErr_SetString(PyExc_RuntimeError, e.GetExMessage()); <%
+ if (!destructor) { %>
+ return NULL; <%
+ } %>
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: Unknown exception thrown from the call \"${Helper.callingName(method)}\"");
+ PyErr_SetString(PyExc_RuntimeError, "Unknown exception thrown from the call \"${Helper.callingName(method)}\""); <%
+ if (!destructor) { %>
+ return NULL; <%
+ } %>
+ }
+<%
+ if (!destructor) { %>
+ PyObject* result = Py_None;
+
+ // transform the result
+<%
+ if (constructor) {
+ %> result = makePythonInstance(apiResult,pytype,false);<%
+ }
+ else {
+%> ${Helper.getOutConversion(returns,'result',method)}<%
+ }
+ if (constructor && method.@feature_director) { %>
+ if (&(Ty${classNameAsVariable}_Type.pythonType) != pytype)
+ ((${classNameAsVariable}_Director*)apiResult)->setPyObjectForDirector(result);<%
+ }
+ %>
+
+ return result; <% }
+ else { %>
+ (((PyObject*)(self))->ob_type)->tp_free((PyObject*)self);
+ <%
+ }
+ %>
+ } <%
+}
+/*******************************************************************************/
+
+/**
+ * This method writes out the instance of a TypeInfo (which includes
+ * The PyTypeObject as a member) for the class node provided.
+ *
+ * If classNameAsVariable is not null then the class name as a
+ * variable will be appended to it.
+ */
+void doClassTypeInfo(Node clazz, List classNameAsVariables = null)
+{
+ String classNameAsVariable = PythonTools.getClassNameAsVariable(clazz)
+ String fullClassName = Helper.findFullClassName(clazz)
+ classNameAsVariables?.add(classNameAsVariable)
+%>
+ //=========================================================================
+ // These variables will hold the Python Type information for ${fullClassName}
+ TypeInfo Ty${classNameAsVariable}_Type(typeid(${fullClassName}));<%
+%>
+ //=========================================================================
+<%
+}
+
+/**
+ * This method will take the name of an API class from another module and
+ * create an external reference to its TypeInfo instance.
+ */
+void doExternClassTypeInfo(String knownType)
+{
+ String classNameAsVariable = knownType.replaceAll('::','_')
+%>
+ //=========================================================================
+ // These variables define the type ${knownType} from another module
+ extern TypeInfo Ty${classNameAsVariable}_Type;
+ //=========================================================================
+<%
+}
+
+/*******************************************************************************/
+/**
+ * This method takes the class node and outputs all of the python meta-data
+ * and class oddities (like comparators, as_mapping, etc.). These include:
+ *
+ * 1) comparator *_cmp python method as long as there's an operator==, an
+ * operator>, AND an operator<.
+ * 2) it will create a python "as_mapping" method as long as there's both
+ * an operator[], AND a .size() method on the class.
+ * 3) it will handle the explicitly defined rich compare (_rcmp) if the
+ * feature is included in the .i file using %feature("python:rcmp")
+ * 4) The array of PyMethodDefs for the class definition
+ * 5) It will handle public fields as if the were python properties by:
+ * a) Creating a get/set_member if there are read/write properties.
+ * b) Creating only a get if there are only read-only properties.
+ * 6) It will write the init[Classname] method for the class which will
+ * initialize the TypeInfo and PyTypeObject structs.
+ *
+ * If initTypeCalls is not null then the method name for the generated init
+ * method (see #6 above) will be appended to it.
+ */
+void doClassMethodInfo(Node clazz, List initTypeCalls)
+{
+ String classNameAsVariable = PythonTools.getClassNameAsVariable(clazz)
+ String fullClassName = Helper.findFullClassName(clazz)
+ String initTypeCall = "initPy${classNameAsVariable}_Type"
+ initTypeCalls?.add(initTypeCall)
+
+ // see if we have any valid (or invalid) operators
+ boolean doComparator = false
+ boolean doAsMapping = false
+ boolean hasEquivalenceOp = false
+ boolean hasLtOp = false
+ boolean hasGtOp = false
+ Node indexOp = null
+ Node callableOp = null
+ Node sizeNode = null
+
+ List normalMethods = clazz.function.findAll { !it.@name.startsWith("operator ") }
+ List operators = clazz.function.findAll { it.@name.startsWith("operator ") }
+ List properties = clazz.variable.findAll { it.@access != null && it.@access == "public" }
+ List properties_set = properties.findAll { it.@feature_immutable == null || it.@feature_immutable == 0 }
+
+ operators.each {
+ // we have an operator. The only one we can handle is ==
+ if (it.@name.substring(9).startsWith("=="))
+ hasEquivalenceOp = true
+ else if (it.@name.substring(9) == "<")
+ hasLtOp = true
+ else if (it.@name.substring(9) == ">")
+ hasGtOp = true
+ else if (it.@name.substring(9) == "[]")
+ indexOp = it
+ else if (it.@name.substring(9) == "()")
+ callableOp = it
+ else
+ System.err.println ("Warning: class ${fullClassName} has an operator \"${it.@name}\" that is being ignored.");
+ }
+
+ if (hasGtOp || hasLtOp || hasEquivalenceOp)
+ {
+ if (!(hasLtOp && hasGtOp && hasEquivalenceOp))
+ System.err.println ("Warning: class ${fullClassName} has an inconsistent operator set. To get a comparator you must implement all 3 operators >,<,==.")
+ else
+ doComparator = true
+ }
+
+ if (indexOp)
+ {
+ sizeNode = clazz.function.find { it.@name == "size" }
+ if (sizeNode)
+ doAsMapping = true
+ else
+ System.err.println ("Warning: class ${fullClassName} has an inconsistent operator set. To get a as_mapping you must implement 'size' as well as operator[]")
+ }
+
+ if (doAsMapping)
+ {
+%>
+ static Py_ssize_t ${module.@name}_${classNameAsVariable}_size_(PyObject* self)
+ {
+ return (Py_ssize_t)((${fullClassName}*)retrieveApiInstance(self,&Ty${classNameAsVariable}_Type,"${Helper.callingName(indexOp)}","${fullClassName}"))-> size();
+ }
+
+ //=========================================================================
+ // tp_as_mapping struct for ${fullClassName}
+ //=========================================================================
+ PyMappingMethods ${module.@name}_${classNameAsVariable}_as_mapping = {
+ ${module.@name}_${classNameAsVariable}_size_, /* inquiry mp_length; __len__ */
+ (PyCFunction)${module.@name}_${PythonTools.getPyMethodName(indexOp,MethodType.method)}, /* binaryfunc mp_subscript __getitem__ */
+ 0, /* objargproc mp_ass_subscript; __setitem__ */
+ };
+<%
+ }
+
+ if (clazz.@feature_python_rcmp)
+ { %>
+ static PyObject* ${module.@name}_${classNameAsVariable}_rcmp(PyObject* obj1, PyObject *obj2, int method)
+ ${Helper.unescape(clazz.@feature_python_rcmp)}
+<%
+ }
+%>
+ //=========================================================================
+ // This section contains the initialization for the
+ // Python extension for the Api class ${fullClassName}
+ //=========================================================================
+ // All of the methods on this class
+ static PyMethodDef ${classNameAsVariable}_methods[] = { <%
+ normalMethods.each { %>
+ {"${it.@sym_name}", (PyCFunction)${module.@name}_${PythonTools.getPyMethodName(it,MethodType.method)}, METH_VARARGS|METH_KEYWORDS, ${Helper.hasDoc(it) ? PythonTools.getPyMethodName(it,MethodType.method) + '__doc__' : 'NULL'} }, <% }
+
+ // now do all of the explicit feature:python:method's that may be in this class
+ List tmpl = []
+ tmpl.addAll(clazz.attributes().keySet())
+ List newMethodKeys = tmpl.findAll { it.startsWith('feature_python_method_') }
+ newMethodKeys.each { key ->
+ String featureEntry = clazz.attribute(key)
+ String methodName = key.substring('feature_python_method_'.length()) %>
+ {"${methodName}", (PyCFunction)${module.@name}_${PythonTools.getClassNameAsVariable(clazz)}_${methodName}, METH_VARARGS|METH_KEYWORDS, NULL},
+<%
+ }
+%>
+ {NULL, NULL, 0, NULL}
+ };
+
+<%
+ if (properties.size() > 0) {
+%> static PyObject* ${classNameAsVariable}_getMember(PyHolder *self, void *name)
+ {
+ if (self == NULL)
+ return NULL;
+<%
+ String clazzName = Helper.findFullClassName(properties[0])
+%>
+ try
+ {
+ ${clazzName}* theObj = (${clazzName}*)retrieveApiInstance((PyObject*)self, &Ty${classNameAsVariable}_Type, "${classNameAsVariable}_getMember()", "${clazzName}");
+
+ PyObject* result = NULL;
+ <%
+ properties.each {
+ String returns = Helper.getPropertyReturnSwigType(it);
+%> if (strcmp((char*)name, "${it.@sym_name}") == 0)
+ {
+ ${SwigTypeParser.SwigType_lstr(returns)} apiResult = theObj->${it.@sym_name};
+ ${Helper.getOutConversion(returns, 'result', it)}
+ }
+ else<%
+ } %>
+ {
+ Py_INCREF(Py_None);
+ return Py_None;
+ }
+
+ return result;
+ }
+ catch (const XBMCAddon::WrongTypeException& e)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage());
+ PyErr_SetString(PyExc_TypeError, e.GetExMessage());
+ return NULL;
+ }
+ catch (const XbmcCommons::Exception& e)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage());
+ PyErr_SetString(PyExc_RuntimeError, e.GetExMessage());
+ return NULL;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: Unknown exception thrown from the call \"${classNameAsVariable}_getMember()\"");
+ PyErr_SetString(PyExc_RuntimeError, "Unknown exception thrown from the call \"${classNameAsVariable}_getMember()\"");
+ return NULL;
+ }
+
+ return NULL;
+ }
+
+<%
+ if (properties_set.size() > 0) {
+%> int ${classNameAsVariable}_setMember(PyHolder *self, PyObject *value, void *name)
+ {
+ if (self == NULL)
+ return -1;
+
+ ${clazzName}* theObj = NULL;
+ try
+ {
+ theObj = (${clazzName}*)retrieveApiInstance((PyObject*)self, &Ty${classNameAsVariable}_Type, "${classNameAsVariable}_getMember()", "${clazzName}");
+ }
+ catch (const XBMCAddon::WrongTypeException& e)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage());
+ PyErr_SetString(PyExc_TypeError, e.GetExMessage());
+ return -1;
+ }
+ catch (const XbmcCommons::Exception& e)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage());
+ PyErr_SetString(PyExc_RuntimeError, e.GetExMessage());
+ return -1;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: Unknown exception thrown from the call \"${classNameAsVariable}_getMember()\"");
+ PyErr_SetString(PyExc_RuntimeError, "Unknown exception thrown from the call \"${classNameAsVariable}_getMember()\"");
+ return -1;
+ }
+
+<%
+ properties_set.each {
+ String returns = Helper.getPropertyReturnSwigType(it);
+%> if (strcmp((char*)name, "${it.@sym_name}") == 0)
+ {
+ ${SwigTypeParser.SwigType_lstr(returns)} tmp;
+ ${Helper.getInConversion(returns, 'tmp', 'value', it)}
+ if (PyErr_Occurred())
+ throw PythonBindings::PythonToCppException();
+
+ theObj->${it.@sym_name} = tmp;
+ }
+ else<%
+ } %>
+ return -1;
+
+ return 0;
+ } <%
+ }
+%>
+
+ // All of the methods on this class
+ static PyGetSetDef ${classNameAsVariable}_getsets[] = { <%
+ properties.each { %>
+ {(char*)"${it.@sym_name}", (getter)${classNameAsVariable}_getMember, ${(it.@feature_immutable == null || it.@feature_immutable == 0) ? '(setter)' + classNameAsVariable + '_setMember' : 'NULL'}, (char*)${Helper.hasDoc(it) ? PythonTools.makeDocString(it.doc[0]) : 'NULL'}, (char*)"${it.@sym_name}" }, <% }
+%>
+ {NULL}
+ };
+<%
+ }
+
+ if ((clazz.@feature_iterator && clazz.@feature_iterator != '') ||
+ (clazz.@feature_iterable && clazz.@feature_iterable != '')) { %>
+ static PyObject* ${module.@name}_${classNameAsVariable}_iter(PyObject* self)
+ { <%
+ if (clazz.@feature_iterator) { %>
+ return self; <%
+ }
+ else { %>
+ PyObject* result = NULL;
+ try
+ {
+ ${clazz.@feature_iterable}* apiResult = ((${fullClassName}*)retrieveApiInstance(self,&Ty${classNameAsVariable}_Type,"${module.@name}_${classNameAsVariable}_iternext","${fullClassName}"))->begin();
+
+ ${Helper.getOutConversion('p.' + clazz.@feature_iterable,'result',clazz)}
+ }
+ catch (const XBMCAddon::WrongTypeException& e)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage());
+ PyErr_SetString(PyExc_TypeError, e.GetExMessage());
+ return NULL;
+ }
+ catch (const XbmcCommons::Exception& e)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage());
+ PyErr_SetString(PyExc_RuntimeError, e.GetExMessage());
+ return NULL;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: Unknown exception thrown from the call \"${module.@name}_${classNameAsVariable}_iternext\"");
+ PyErr_SetString(PyExc_RuntimeError, "Unknown exception thrown from the call \"${module.@name}_${classNameAsVariable}_iternext\"");
+ return NULL;
+ }
+
+ return result; <%
+ } %>
+ }
+<%
+
+ if (clazz.@feature_iterator) { %>
+ static PyObject* ${module.@name}_${classNameAsVariable}_iternext(PyObject* self)
+ {
+ PyObject* result = NULL;
+ try
+ {
+ ${fullClassName}* iter = (${fullClassName}*)retrieveApiInstance(self,&Ty${classNameAsVariable}_Type,"${module.@name}_${classNameAsVariable}_iternext","${fullClassName}");
+
+ // check if we have reached the end
+ if (!iter->end())
+ {
+ ++(*iter);
+
+ ${clazz.@feature_iterator} apiResult = **iter;
+ ${Helper.getOutConversion(clazz.@feature_iterator,'result',clazz)}
+ }
+ }
+ catch (const XBMCAddon::WrongTypeException& e)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage());
+ PyErr_SetString(PyExc_TypeError, e.GetExMessage());
+ return NULL;
+ }
+ catch (const XbmcCommons::Exception& e)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: {}",e.GetExMessage());
+ PyErr_SetString(PyExc_RuntimeError, e.GetExMessage());
+ return NULL;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,"EXCEPTION: Unknown exception thrown from the call \"${module.@name}_${classNameAsVariable}_iternext\"");
+ PyErr_SetString(PyExc_RuntimeError, "Unknown exception thrown from the call \"${module.@name}_${classNameAsVariable}_iternext\"");
+ return NULL;
+ }
+
+ return result;
+ }
+<%
+ }
+ }
+%>
+
+ // This method initializes the above mentioned Python Type structure
+ static void ${initTypeCall}()
+ {
+<%
+ if (Helper.hasDoc(clazz))
+ {
+%>
+ PyDoc_STRVAR(${classNameAsVariable}__doc__,
+ ${PythonTools.makeDocString(clazz.doc[0])}
+ );
+<% } %>
+
+ PyTypeObject& pythonType = Ty${classNameAsVariable}_Type.pythonType;
+ pythonType.tp_name = "${module.@name}.${clazz.@sym_name}";
+ pythonType.tp_basicsize = sizeof(PyHolder);
+ pythonType.tp_dealloc = (destructor)${module.@name}_${classNameAsVariable}_Dealloc; <%
+
+ if (clazz.@feature_python_rcmp) { %>
+ pythonType.tp_richcompare=(richcmpfunc)${module.@name}_${classNameAsVariable}_rcmp;<%
+ } %>
+
+ pythonType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
+
+ pythonType.tp_doc = ${Helper.hasDoc(clazz) ? (classNameAsVariable + '__doc__') : 'NULL' };
+ pythonType.tp_methods = ${classNameAsVariable}_methods; <%
+ if (properties.size() > 0) { %>
+ pythonType.tp_getset = ${classNameAsVariable}_getsets;
+<%
+ }
+ if (callableOp) { %>
+ pythonType.tp_call = (ternaryfunc)${module.@name}_${PythonTools.getPyMethodName(callableOp,MethodType.method)};
+<%
+ }
+ if (doAsMapping) { %>
+ pythonType.tp_as_mapping = &${module.@name}_${classNameAsVariable}_as_mapping;
+<%
+ }
+
+ if (clazz.@feature_iterator) { %>
+ pythonType.tp_iter = (getiterfunc)${module.@name}_${classNameAsVariable}_iter;
+ pythonType.tp_iternext = (iternextfunc)${module.@name}_${classNameAsVariable}_iternext;
+<%
+ }
+ else if (clazz.@feature_iterable && clazz.@feature_iterable != '') { %>
+ pythonType.tp_iter = (getiterfunc)${module.@name}_${classNameAsVariable}_iter;
+<%
+ }
+
+ Node baseclass = PythonTools.findValidBaseClass(clazz, module)
+%>
+
+ pythonType.tp_base = ${baseclass ? ('&(Ty' + PythonTools.getClassNameAsVariable(baseclass) + '_Type.pythonType)') : "NULL"};
+ pythonType.tp_new = <% !Helper.hasDefinedConstructor(clazz) || Helper.hasHiddenConstructor(clazz) ? print('NULL') : print("${module.@name}_${classNameAsVariable}_New") %>;
+ pythonType.tp_init = dummy_tp_init;
+
+ Ty${classNameAsVariable}_Type.swigType="p.${fullClassName}";<%
+ if (baseclass) { %>
+ Ty${classNameAsVariable}_Type.parentType=&Ty${PythonTools.getClassNameAsVariable(baseclass)}_Type;
+<%}
+
+ if (!Helper.hasHiddenConstructor(clazz)) { %>
+ registerAddonClassTypeInformation(&Ty${classNameAsVariable}_Type);
+<%} %>
+ }
+ //=========================================================================
+<%
+}
+/*******************************************************************************/
+
+
+List getAllVirtualMethods(Node clazz)
+{
+ List ret = []
+ ret.addAll(clazz.findAll({ it.name() == 'function' && it.@storage && it.@storage == 'virtual' }))
+ if (clazz.baselist) {
+ if (clazz.baselist[0].base) clazz.baselist[0].base.each {
+ Node baseclassnode = Helper.findClassNodeByName(module,it.@name,clazz)
+ if (baseclassnode && baseclassnode.@feature_director) ret.addAll(getAllVirtualMethods(baseclassnode))
+ }
+ }
+ return ret;
+}
+
+%>
+/*
+ * 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.
+ */
+
+// ************************************************************************
+// This file was generated by xbmc compile process. DO NOT EDIT!!
+// It was created by running the code generator on the spec file for
+// the module "${module.@name}" on the template file PythonSwig.template.cpp
+// ************************************************************************
+
+<%
+Helper.getInsertNodes(module, 'begin').each { %>${Helper.unescape(it)}<% }
+%>
+
+#include <Python.h>
+#include <string>
+#include "CompileInfo.h"
+#include "interfaces/python/LanguageHook.h"
+#include "interfaces/python/swig.h"
+#include "interfaces/python/PyContext.h"
+
+<%
+Helper.getInsertNodes(module, 'header').each { %>${Helper.unescape(it)}<% }
+%>
+
+namespace PythonBindings
+{
+<%
+ // initTypeCalls is the
+ List initTypeCalls = []
+ List classNameAsVariables = []
+
+ classes.each { clazz -> doClassTypeInfo(clazz, classNameAsVariables) }
+
+ // make sure known api types are declared as externs
+
+ // first, find all of the declared known api types
+ Set<String> knownApiTypes = new HashSet<String>()
+ module.depthFirst().each
+ {
+ String attr = it.attribute('feature_knownapitypes')
+ if (attr != null)
+ {
+ attr.trim().split(',').each { knownApiTypes.add(it) }
+ }
+ }
+
+ // now declare an extern for each one
+ knownApiTypes.each { doExternClassTypeInfo(it) }
+
+%>
+
+<%
+//=========================================================================
+// Do the directors. For every class that can be extended in python, we
+// need to create a Director instance with bridging calls. This chunk of
+// code will generate those classes.
+ classes.findAll({ it.@feature_director != null }).each { clazz ->
+ // find the constructor for this class
+ constructor = clazz.constructor[0]
+%>
+ //=========================================================================
+ // This class is the Director for ${Helper.findFullClassName(clazz)}.
+ // It provides the "reverse bridge" from C++ to Python to support
+ // cross-language polymorphism.
+ //=========================================================================
+ class ${PythonTools.getClassNameAsVariable(clazz)}_Director : public Director, public ${clazz.@name}
+ {
+ public:
+<%
+ if (constructor)
+ {%>
+ inline ${PythonTools.getClassNameAsVariable(clazz)}_Director(<%
+ List params = constructor?.parm
+ params.eachWithIndex { param, i -> %>${SwigTypeParser.SwigType_str(param.@type)} ${param.@name}${i < params.size() - 1 ? "," : ""} <% }
+ %>) : ${Helper.findFullClassName(constructor)}(<%
+ params.eachWithIndex { param, i -> %> ${param.@name}${i < params.size() - 1 ? "," : ""} <% } %>) { } <%
+ }
+%>
+<%
+ getAllVirtualMethods(clazz).each
+ { %>
+ virtual ${SwigTypeParser.SwigType_str(Helper.getReturnSwigType(it))} ${Helper.callingName(it)}( <%
+ List params = it?.parm
+ String paramFormatStr = ''
+ params.each { paramFormatStr += 'O' }
+ params.eachWithIndex { param, i -> %> ${SwigTypeParser.SwigType_str(param.@type)} ${param.@name}${i < params.size() - 1 ? "," : ""} <% }
+ %> )
+ { <%
+ params.each
+ { param ->
+ %>
+ PyObject* py${param.@name} = NULL;
+ ${Helper.getOutConversion(param.@type,'result',it,['result' : 'py' + param.@name, 'api' : param.@name])}<%
+ }
+%>
+ XBMCAddon::Python::PyContext pyContext;
+ PyObject_CallMethod(self,"${Helper.callingName(it)}","(${paramFormatStr})"<%
+ params.each {
+ %>, py${it.@name} <%
+ }
+ %>);
+ if (PyErr_Occurred())
+ throw PythonBindings::PythonToCppException();
+ }
+<% }
+
+%>
+ };
+<%
+ }
+//=========================================================================
+
+ // types used as method parameter or return values need to be declared
+ // as extern if they are unknown types.
+ methods.each { if (it.name() != 'destructor') { doMethod(it, (it.name() == 'constructor' ? MethodType.constructor : MethodType.method)); println(); } }
+ classes.each { clazz -> doMethod(clazz, MethodType.destructor) }
+
+ // now find any methods that have been added explicitly
+ classes.each { node ->
+ List tmpl = []
+ tmpl.addAll(node.attributes().keySet())
+ List newMethodKeys = tmpl.findAll { it.startsWith('feature_python_method_') }
+ newMethodKeys.each { key ->
+ String featureEntry = node.attribute(key)
+ String methodName = key.substring('feature_python_method_'.length()) %>
+ static PyObject* ${module.@name}_${PythonTools.getClassNameAsVariable(node)}_${methodName}(PyObject* self, PyObject *args, PyObject *kwds)
+ ${Helper.unescape(featureEntry)}
+<%
+ }
+ }
+
+ classes.each { clazz -> doClassMethodInfo(clazz, initTypeCalls) }
+
+%>
+
+ static PyMethodDef ${module.@name}_methods[] = { <%
+ module.depthFirst().findAll({ it.name() == 'function' && Helper.parents(it, { Node lnode -> lnode.name() == 'class'}).size() == 0 }).each { %>
+ {"${it.@sym_name}", (PyCFunction)${module.@name}_${PythonTools.getPyMethodName(it,MethodType.method)}, METH_VARARGS|METH_KEYWORDS, ${Helper.hasDoc(it) ? PythonTools.getPyMethodName(it,MethodType.method) + '__doc__' : 'NULL'} }, <% }
+%>
+ {NULL, NULL, 0, NULL}
+ };
+
+ // This is the call that will call all of the other initializes
+ // for all of the classes in this module
+ static void initTypes()
+ {
+ static bool typesAlreadyInitialized = false;
+ if (!typesAlreadyInitialized)
+ {
+ typesAlreadyInitialized = true;
+<%
+ initTypeCalls.each { %>
+ ${it}();<%
+ }
+
+ classNameAsVariables.each { %>
+ if (PyType_Ready(&(Ty${it}_Type.pythonType)) < 0)
+ return;<%
+ }%>
+ }
+ }
+
+ static struct PyModuleDef createModule
+ {
+ PyModuleDef_HEAD_INIT,
+ "${module.@name}",
+ "",
+ -1,
+ ${module.@name}_methods,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ };
+
+ PyObject *PyInit_Module_${module.@name}()
+ {
+ initTypes();
+
+ // init general ${module.@name} modules
+ PyObject* module;
+
+<% classNameAsVariables.each { %>
+ Py_INCREF(&(Ty${it}_Type.pythonType));<%
+ }%>
+
+ module = PyModule_Create(&createModule);
+ if (module == NULL) return NULL;
+
+<% classes.each { clazz -> %>
+ PyModule_AddObject(module, "${clazz.@sym_name}", (PyObject*)(&(Ty${PythonTools.getClassNameAsVariable(clazz)}_Type.pythonType)));<%
+ }%>
+
+ // constants
+ PyModule_AddStringConstant(module, "__author__", "Team Kodi <http://kodi.tv>");
+ PyModule_AddStringConstant(module, "__date__", CCompileInfo::GetBuildDate().c_str());
+ PyModule_AddStringConstant(module, "__version__", "3.0.1");
+ PyModule_AddStringConstant(module, "__credits__", "Team Kodi");
+ PyModule_AddStringConstant(module, "__platform__", "ALL");
+
+ // need to handle constants
+ // #define constants
+<% module.depthFirst().findAll( { it.name() == 'constant'} ).each {
+ String pyCall =
+ (it.@type == 'int' || it.@type == 'long' || it.@type == 'unsigned int' || it.@type == 'unsigned long' || it.@type == 'bool') ?
+ 'PyModule_AddIntConstant' : 'PyModule_AddStringConstant' %>
+ ${pyCall}(module,"${it.@sym_name}",${it.@value}); <%
+ } %>
+ // constexpr constants
+<% module.depthFirst().findAll( { it.name() == 'variable' && it.@storage && it.@storage == "constexpr" && !it.@error } ).each {
+ String pyCall =
+ ( it.@type == 'q(const).int' || it.@type == 'q(const).long' || it.@type == 'q(const).unsigned int' || it.@type == 'q(const).unsigned long' || it.@type == 'q(const).bool' ) ?
+ 'PyModule_AddIntConstant' : 'PyModule_AddStringConstant' %>
+ ${pyCall}(module,"${it.@sym_name}",${it.@value}); <%
+ } %>
+ return module;
+ }
+
+} // end PythonBindings namespace for python type definitions
+
+<%
+Helper.getInsertNodes(module, 'footer').each { %>${Helper.unescape(it)}<% }
+%>
diff --git a/xbmc/interfaces/python/PythonTools.groovy b/xbmc/interfaces/python/PythonTools.groovy
new file mode 100644
index 0000000..e41db6c
--- /dev/null
+++ b/xbmc/interfaces/python/PythonTools.groovy
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ */
+
+import Helper
+import SwigTypeParser
+
+public class PythonTools
+{
+ /**
+ * This array defines a mapping of the api spec type to the python parse format character.
+ * By default, if a lookup here results in 'null' then the format char is 'O'
+ */
+ private static Map ltypeToFormatChar = [
+ 'p.char':"s", bool:"b",
+ int:"i", 'unsigned int' : 'I',
+ long:"l", 'unsigned long' : 'k',
+ 'double':"d", 'float':"f",
+ 'long long' : "L"
+ ]
+
+ /**
+ * if the parameter can be directly read from python then its type should be in the ltypeToFormatChar
+ * otherwise we need an intermediate pyobject
+ */
+ public static boolean parameterCanBeUsedDirectly(Node param) { return ltypeToFormatChar[SwigTypeParser.convertTypeToLTypeForParam(param.@type)] != null }
+
+ /**
+ * This method will take the parameter list from the method node passed
+ * and will convert it to a Python argument string for PyArg_ParseTupleAndKeywords
+ */
+ public static String makeFormatStringFromParameters(Node method)
+ {
+ if (!method)
+ return ''
+ List params = method.parm
+ String format = ""
+ boolean previousDefaulted = false
+ params.eachWithIndex { param, i ->
+ String defaultValue = param.@value
+ String paramtype = SwigTypeParser.convertTypeToLTypeForParam(param.@type)
+ String curFormat = ltypeToFormatChar[paramtype];
+ if (curFormat == null) // then we will assume it's an object
+ curFormat = "O";
+
+ if (defaultValue != null && !previousDefaulted)
+ {
+ format +="|"
+ previousDefaulted = true
+ }
+ format += curFormat
+ }
+ return format;
+ }
+
+ /**
+ * This method gets the FULL class name as a variable including the
+ * namespace. If converts all of the '::' references to '_' so
+ * that the result can be used in part, or in whole, as a variable name
+ */
+ public static String getClassNameAsVariable(Node clazz) { return Helper.findFullClassName(clazz).replaceAll('::','_') }
+
+ public static String getPyMethodName(Node method, MethodType methodType)
+ {
+ String clazz = Helper.findFullClassName(method)?.replaceAll('::','_')
+
+ // if we're not in a class then this must be a method node
+ assert (clazz != null || methodType == MethodType.method), 'Cannot use a non-class function as a constructor or destructor ' + method
+
+ // it's ok to pass a 'class' node if the methodType is either constructor or destructor
+ assert (method.name() != 'class' || (methodType == MethodType.constructor || methodType == MethodType.destructor))
+
+ // if this is a constructor node then the methodtype best reflect that
+ assert (method.name() != 'constructor' || methodType == MethodType.constructor), 'Cannot use a constructor node and not identify the type as a constructor' + method
+
+ // if this is a destructor node then the methodtype best reflect that
+ assert (method.name() != 'destructor' || methodType == MethodType.destructor), 'Cannot use a destructor node and not identify the type as a destructor' + method
+
+ if (clazz == null)
+ return method.@sym_name
+
+ if (methodType == MethodType.constructor)
+ return clazz + "_New"
+
+ if (methodType == MethodType.destructor)
+ return clazz + "_Dealloc"
+
+ if (method.@name.startsWith("operator "))
+ {
+ if ("[]" == method.@name.substring(9))
+ return clazz + "_operatorIndex_"
+
+ if ("()" == method.@name.substring(9))
+ return clazz + "_callable_"
+ }
+
+ return clazz + "_" + method.@sym_name;
+ }
+
+ public static String makeDocString(Node docnode)
+ {
+ if (docnode?.name() != 'doc')
+ throw new RuntimeException("Invalid doc Node passed to PythonTools.makeDocString (" + docnode + ")")
+
+ String[] lines = (docnode.@value).split(Helper.newline)
+ def ret = ''
+ lines.eachWithIndex { val, index ->
+ val = ((val =~ /\\n/).replaceAll('')) // remove extraneous \n's
+ val = val.replaceAll("\\\\","\\\\\\\\") // escape backslash
+ val = ((val =~ /\"/).replaceAll("\\\\\"")) // escape quotes
+ ret += ('"' + val + '\\n"' + (index != lines.length - 1 ? Helper.newline : ''))
+ }
+
+ return ret
+ }
+
+ public static Node findValidBaseClass(Node clazz, Node module, boolean warn = false)
+ {
+ // I need to find the base type if there is a known class with it
+ assert clazz.baselist.size() < 2, "${clazz} has multiple baselists - need to write code to separate out the public one."
+ String baseclass = 'NULL'
+ List knownbases = []
+ if (clazz.baselist)
+ {
+ if (clazz.baselist[0].base) clazz.baselist[0].base.each {
+ Node baseclassnode = Helper.findClassNodeByName(module,it.@name,clazz)
+ if (baseclassnode) knownbases.add(baseclassnode)
+ else if (warn && !Helper.isKnownBaseType(it.@name,clazz))
+ System.out.println("WARNING: the base class ${it.@name} for ${Helper.findFullClassName(clazz)} is unrecognized within ${module.@name}.")
+ }
+ }
+ assert knownbases.size() < 2,
+ "The class ${Helper.findFullClassName(clazz)} has too many known base classes. Multiple inheritance isn't supported in the code generator. Please \"#ifdef SWIG\" out all but one."
+ return knownbases.size() > 0 ? knownbases[0] : null
+ }
+}
diff --git a/xbmc/interfaces/python/XBPython.cpp b/xbmc/interfaces/python/XBPython.cpp
new file mode 100644
index 0000000..ee8ed93
--- /dev/null
+++ b/xbmc/interfaces/python/XBPython.cpp
@@ -0,0 +1,627 @@
+/*
+ * 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.
+ */
+
+// clang-format off
+// python.h should always be included first before any other includes
+#include <mutex>
+#include <Python.h>
+// clang-format on
+
+#include "XBPython.h"
+
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "cores/DllLoader/DllLoaderContainer.h"
+#include "filesystem/SpecialProtocol.h"
+#include "interfaces/AnnouncementManager.h"
+#include "interfaces/legacy/AddonUtils.h"
+#include "interfaces/legacy/Monitor.h"
+#include "interfaces/python/AddonPythonInvoker.h"
+#include "interfaces/python/PythonInvoker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/JSONVariantWriter.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "utils/CharsetConverter.h"
+
+#ifdef TARGET_WINDOWS
+#include "platform/Environment.h"
+#endif
+
+#include <algorithm>
+
+// Only required for Py3 < 3.7
+PyThreadState* savestate;
+
+bool XBPython::m_bInitialized = false;
+
+XBPython::XBPython()
+{
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+XBPython::~XBPython()
+{
+ XBMC_TRACE;
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+#define LOCK_AND_COPY(type, dest, src) \
+ if (!m_bInitialized) \
+ return; \
+ std::unique_lock<CCriticalSection> lock(src); \
+ src.hadSomethingRemoved = false; \
+ type dest; \
+ dest = src
+
+#define CHECK_FOR_ENTRY(l, v) \
+ (l.hadSomethingRemoved ? (std::find(l.begin(), l.end(), v) != l.end()) : true)
+
+void XBPython::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (flag & ANNOUNCEMENT::VideoLibrary)
+ {
+ if (message == "OnScanFinished")
+ OnScanFinished("video");
+ else if (message == "OnScanStarted")
+ OnScanStarted("video");
+ else if (message == "OnCleanStarted")
+ OnCleanStarted("video");
+ else if (message == "OnCleanFinished")
+ OnCleanFinished("video");
+ }
+ else if (flag & ANNOUNCEMENT::AudioLibrary)
+ {
+ if (message == "OnScanFinished")
+ OnScanFinished("music");
+ else if (message == "OnScanStarted")
+ OnScanStarted("music");
+ else if (message == "OnCleanStarted")
+ OnCleanStarted("music");
+ else if (message == "OnCleanFinished")
+ OnCleanFinished("music");
+ }
+ else if (flag & ANNOUNCEMENT::GUI)
+ {
+ if (message == "OnScreensaverDeactivated")
+ OnScreensaverDeactivated();
+ else if (message == "OnScreensaverActivated")
+ OnScreensaverActivated();
+ else if (message == "OnDPMSDeactivated")
+ OnDPMSDeactivated();
+ else if (message == "OnDPMSActivated")
+ OnDPMSActivated();
+ }
+
+ std::string jsonData;
+ if (CJSONVariantWriter::Write(
+ data, jsonData,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_jsonOutputCompact))
+ OnNotification(sender,
+ std::string(ANNOUNCEMENT::AnnouncementFlagToString(flag)) + "." +
+ std::string(message),
+ jsonData);
+}
+
+// message all registered callbacks that we started playing
+void XBPython::OnPlayBackStarted(const CFileItem& file)
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnPlayBackStarted(file);
+ }
+}
+
+// message all registered callbacks that we changed stream
+void XBPython::OnAVStarted(const CFileItem& file)
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnAVStarted(file);
+ }
+}
+
+// message all registered callbacks that we changed stream
+void XBPython::OnAVChange()
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnAVChange();
+ }
+}
+
+// message all registered callbacks that we paused playing
+void XBPython::OnPlayBackPaused()
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnPlayBackPaused();
+ }
+}
+
+// message all registered callbacks that we resumed playing
+void XBPython::OnPlayBackResumed()
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnPlayBackResumed();
+ }
+}
+
+// message all registered callbacks that xbmc stopped playing
+void XBPython::OnPlayBackEnded()
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnPlayBackEnded();
+ }
+}
+
+// message all registered callbacks that user stopped playing
+void XBPython::OnPlayBackStopped()
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnPlayBackStopped();
+ }
+}
+
+// message all registered callbacks that playback stopped due to error
+void XBPython::OnPlayBackError()
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnPlayBackError();
+ }
+}
+
+// message all registered callbacks that playback speed changed (FF/RW)
+void XBPython::OnPlayBackSpeedChanged(int iSpeed)
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnPlayBackSpeedChanged(iSpeed);
+ }
+}
+
+// message all registered callbacks that player is seeking
+void XBPython::OnPlayBackSeek(int64_t iTime, int64_t seekOffset)
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnPlayBackSeek(iTime, seekOffset);
+ }
+}
+
+// message all registered callbacks that player chapter seeked
+void XBPython::OnPlayBackSeekChapter(int iChapter)
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnPlayBackSeekChapter(iChapter);
+ }
+}
+
+// message all registered callbacks that next item has been queued
+void XBPython::OnQueueNextItem()
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<void*>, tmp, m_vecPlayerCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecPlayerCallbackList, it))
+ ((IPlayerCallback*)it)->OnQueueNextItem();
+ }
+}
+
+void XBPython::RegisterPythonPlayerCallBack(IPlayerCallback* pCallback)
+{
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(m_vecPlayerCallbackList);
+ m_vecPlayerCallbackList.push_back(pCallback);
+}
+
+void XBPython::UnregisterPythonPlayerCallBack(IPlayerCallback* pCallback)
+{
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(m_vecPlayerCallbackList);
+ PlayerCallbackList::iterator it = m_vecPlayerCallbackList.begin();
+ while (it != m_vecPlayerCallbackList.end())
+ {
+ if (*it == pCallback)
+ {
+ it = m_vecPlayerCallbackList.erase(it);
+ m_vecPlayerCallbackList.hadSomethingRemoved = true;
+ }
+ else
+ ++it;
+ }
+}
+
+void XBPython::RegisterPythonMonitorCallBack(XBMCAddon::xbmc::Monitor* pCallback)
+{
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(m_vecMonitorCallbackList);
+ m_vecMonitorCallbackList.push_back(pCallback);
+}
+
+void XBPython::UnregisterPythonMonitorCallBack(XBMCAddon::xbmc::Monitor* pCallback)
+{
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(m_vecMonitorCallbackList);
+ MonitorCallbackList::iterator it = m_vecMonitorCallbackList.begin();
+ while (it != m_vecMonitorCallbackList.end())
+ {
+ if (*it == pCallback)
+ {
+ it = m_vecMonitorCallbackList.erase(it);
+ m_vecMonitorCallbackList.hadSomethingRemoved = true;
+ }
+ else
+ ++it;
+ }
+}
+
+void XBPython::OnSettingsChanged(const std::string& ID)
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it) && (it->GetId() == ID))
+ it->OnSettingsChanged();
+ }
+}
+
+void XBPython::OnScreensaverActivated()
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it))
+ it->OnScreensaverActivated();
+ }
+}
+
+void XBPython::OnScreensaverDeactivated()
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it))
+ it->OnScreensaverDeactivated();
+ }
+}
+
+void XBPython::OnDPMSActivated()
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it))
+ it->OnDPMSActivated();
+ }
+}
+
+void XBPython::OnDPMSDeactivated()
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it))
+ it->OnDPMSDeactivated();
+ }
+}
+
+void XBPython::OnScanStarted(const std::string& library)
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it))
+ it->OnScanStarted(library);
+ }
+}
+
+void XBPython::OnScanFinished(const std::string& library)
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it))
+ it->OnScanFinished(library);
+ }
+}
+
+void XBPython::OnCleanStarted(const std::string& library)
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it))
+ it->OnCleanStarted(library);
+ }
+}
+
+void XBPython::OnCleanFinished(const std::string& library)
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it))
+ it->OnCleanFinished(library);
+ }
+}
+
+void XBPython::OnNotification(const std::string& sender,
+ const std::string& method,
+ const std::string& data)
+{
+ XBMC_TRACE;
+ LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it))
+ it->OnNotification(sender, method, data);
+ }
+}
+
+void XBPython::Uninitialize()
+{
+ // don't handle any more announcements as most scripts are probably already
+ // stopped and executing a callback on one of their already destroyed classes
+ // would lead to a crash
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+
+ LOCK_AND_COPY(std::vector<PyElem>, tmpvec, m_vecPyList);
+ m_vecPyList.clear();
+ m_vecPyList.hadSomethingRemoved = true;
+
+ lock.unlock(); //unlock here because the python thread might lock when it exits
+
+ // cleanup threads that are still running
+ tmpvec.clear();
+}
+
+void XBPython::Process()
+{
+ if (m_bInitialized)
+ {
+ PyList tmpvec;
+ std::unique_lock<CCriticalSection> lock(m_vecPyList);
+ for (PyList::iterator it = m_vecPyList.begin(); it != m_vecPyList.end();)
+ {
+ if (it->bDone)
+ {
+ tmpvec.push_back(*it);
+ it = m_vecPyList.erase(it);
+ m_vecPyList.hadSomethingRemoved = true;
+ }
+ else
+ ++it;
+ }
+ lock.unlock();
+
+ //delete scripts which are done
+ tmpvec.clear();
+ }
+}
+
+bool XBPython::OnScriptInitialized(ILanguageInvoker* invoker)
+{
+ if (invoker == NULL)
+ return false;
+
+ XBMC_TRACE;
+ CLog::Log(LOGDEBUG, "initializing python engine.");
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iDllScriptCounter++;
+ if (!m_bInitialized)
+ {
+ // Darwin packs .pyo files, we need PYTHONOPTIMIZE on in order to load them.
+ // linux built with unified builds only packages the pyo files so need it
+#if defined(TARGET_DARWIN) || defined(TARGET_LINUX)
+ setenv("PYTHONOPTIMIZE", "1", 1);
+#endif
+ // Info about interesting python envvars available
+ // at http://docs.python.org/using/cmdline.html#environment-variables
+
+#if !defined(TARGET_WINDOWS) && !defined(TARGET_ANDROID)
+ // check if we are running as real xbmc.app or just binary
+ if (!CUtil::GetFrameworksPath(true).empty())
+ {
+ // using external python, it's build looking for xxx/lib/python3.8
+ // so point it to frameworks which is where python3.8 is located
+ setenv("PYTHONHOME", CSpecialProtocol::TranslatePath("special://frameworks").c_str(), 1);
+ setenv("PYTHONPATH", CSpecialProtocol::TranslatePath("special://frameworks").c_str(), 1);
+ CLog::Log(LOGDEBUG, "PYTHONHOME -> {}",
+ CSpecialProtocol::TranslatePath("special://frameworks"));
+ CLog::Log(LOGDEBUG, "PYTHONPATH -> {}",
+ CSpecialProtocol::TranslatePath("special://frameworks"));
+ }
+#elif defined(TARGET_WINDOWS)
+
+#ifdef TARGET_WINDOWS_STORE
+#ifdef _DEBUG
+ CEnvironment::putenv("PYTHONCASEOK=1");
+#endif
+ CEnvironment::putenv("OS=win10");
+#else // TARGET_WINDOWS_DESKTOP
+ CEnvironment::putenv("OS=win32");
+#endif
+
+ std::wstring pythonHomeW;
+ CCharsetConverter::utf8ToW(CSpecialProtocol::TranslatePath("special://xbmc/system/python"),
+ pythonHomeW);
+ Py_SetPythonHome(pythonHomeW.c_str());
+
+ std::string pythonPath = CSpecialProtocol::TranslatePath("special://xbmc/system/python/DLLs");
+ pythonPath += ";";
+ pythonPath += CSpecialProtocol::TranslatePath("special://xbmc/system/python/Lib");
+ pythonPath += ";";
+ pythonPath += CSpecialProtocol::TranslatePath("special://xbmc/system/python/Lib/site-packages");
+ std::wstring pythonPathW;
+ CCharsetConverter::utf8ToW(pythonPath, pythonPathW);
+
+ Py_SetPath(pythonPathW.c_str());
+
+ Py_OptimizeFlag = 1;
+#endif
+
+ Py_Initialize();
+
+#if PY_VERSION_HEX < 0x03070000
+ // Python >= 3.7 Py_Initialize implicitly calls PyEval_InitThreads
+ // Python < 3.7 we have to manually call initthreads.
+ // PyEval_InitThreads is a no-op on subsequent calls, No need to wrap in
+ // PyEval_ThreadsInitialized() check
+ PyEval_InitThreads();
+#endif
+
+ // Acquire GIL if thread doesn't currently hold.
+ if (!PyGILState_Check())
+ PyEval_RestoreThread((PyThreadState*)m_mainThreadState);
+
+ if (!(m_mainThreadState = PyThreadState_Get()))
+ CLog::Log(LOGERROR, "Python threadstate is NULL.");
+ savestate = PyEval_SaveThread();
+
+ m_bInitialized = true;
+ }
+
+ return m_bInitialized;
+}
+
+void XBPython::OnScriptStarted(ILanguageInvoker* invoker)
+{
+ if (invoker == NULL)
+ return;
+
+ if (!m_bInitialized)
+ return;
+
+ PyElem inf;
+ inf.id = invoker->GetId();
+ inf.bDone = false;
+ inf.pyThread = static_cast<CPythonInvoker*>(invoker);
+ std::unique_lock<CCriticalSection> lock(m_vecPyList);
+ m_vecPyList.push_back(inf);
+}
+
+void XBPython::NotifyScriptAborting(ILanguageInvoker* invoker)
+{
+ XBMC_TRACE;
+
+ long invokerId(-1);
+ if (invoker != NULL)
+ invokerId = invoker->GetId();
+
+ LOCK_AND_COPY(std::vector<XBMCAddon::xbmc::Monitor*>, tmp, m_vecMonitorCallbackList);
+ for (auto& it : tmp)
+ {
+ if (CHECK_FOR_ENTRY(m_vecMonitorCallbackList, it))
+ {
+ if (invokerId < 0 || it->GetInvokerId() == invokerId)
+ it->AbortNotify();
+ }
+ }
+}
+
+void XBPython::OnExecutionEnded(ILanguageInvoker* invoker)
+{
+ std::unique_lock<CCriticalSection> lock(m_vecPyList);
+ PyList::iterator it = m_vecPyList.begin();
+ while (it != m_vecPyList.end())
+ {
+ if (it->id == invoker->GetId())
+ {
+ if (it->pyThread->IsStopping())
+ CLog::Log(LOGDEBUG, "Python interpreter interrupted by user");
+ else
+ CLog::Log(LOGDEBUG, "Python interpreter stopped");
+ it->bDone = true;
+ }
+ ++it;
+ }
+}
+
+void XBPython::OnScriptFinalized(ILanguageInvoker* invoker)
+{
+ XBMC_TRACE;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ // for linux - we never release the library. its loaded and stays in memory.
+ if (m_iDllScriptCounter)
+ m_iDllScriptCounter--;
+ else
+ CLog::Log(LOGERROR, "Python script counter attempted to become negative");
+}
+
+ILanguageInvoker* XBPython::CreateInvoker()
+{
+ return new CAddonPythonInvoker(this);
+}
+
+void XBPython::PulseGlobalEvent()
+{
+ m_globalEvent.Set();
+}
+
+bool XBPython::WaitForEvent(CEvent& hEvent, unsigned int milliseconds)
+{
+ // wait for either this event our our global event
+ XbmcThreads::CEventGroup eventGroup{&hEvent, &m_globalEvent};
+ CEvent* ret = eventGroup.wait(std::chrono::milliseconds(milliseconds));
+ if (ret)
+ m_globalEvent.Reset();
+ return ret != NULL;
+}
diff --git a/xbmc/interfaces/python/XBPython.h b/xbmc/interfaces/python/XBPython.h
new file mode 100644
index 0000000..e54b4a2
--- /dev/null
+++ b/xbmc/interfaces/python/XBPython.h
@@ -0,0 +1,123 @@
+/*
+ * 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/IPlayerCallback.h"
+#include "interfaces/IAnnouncer.h"
+#include "interfaces/generic/ILanguageInvocationHandler.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+
+#include <memory>
+#include <vector>
+
+class CPythonInvoker;
+class CVariant;
+
+typedef struct
+{
+ int id;
+ bool bDone;
+ CPythonInvoker* pyThread;
+} PyElem;
+
+class LibraryLoader;
+
+namespace XBMCAddon
+{
+namespace xbmc
+{
+class Monitor;
+}
+} // namespace XBMCAddon
+
+template<class T>
+struct LockableType : public T, public CCriticalSection
+{
+ bool hadSomethingRemoved;
+};
+
+typedef LockableType<std::vector<void*>> PlayerCallbackList;
+typedef LockableType<std::vector<XBMCAddon::xbmc::Monitor*>> MonitorCallbackList;
+typedef LockableType<std::vector<PyElem>> PyList;
+typedef std::vector<LibraryLoader*> PythonExtensionLibraries;
+
+class XBPython : public IPlayerCallback,
+ public ANNOUNCEMENT::IAnnouncer,
+ public ILanguageInvocationHandler
+{
+public:
+ XBPython();
+ ~XBPython() override;
+ void OnPlayBackEnded() override;
+ void OnPlayBackStarted(const CFileItem& file) override;
+ void OnAVStarted(const CFileItem& file) override;
+ void OnAVChange() override;
+ void OnPlayBackPaused() override;
+ void OnPlayBackResumed() override;
+ void OnPlayBackStopped() override;
+ void OnPlayBackError() override;
+ void OnPlayBackSpeedChanged(int iSpeed) override;
+ void OnPlayBackSeek(int64_t iTime, int64_t seekOffset) override;
+ void OnPlayBackSeekChapter(int iChapter) override;
+ void OnQueueNextItem() override;
+
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+ void RegisterPythonPlayerCallBack(IPlayerCallback* pCallback);
+ void UnregisterPythonPlayerCallBack(IPlayerCallback* pCallback);
+ void RegisterPythonMonitorCallBack(XBMCAddon::xbmc::Monitor* pCallback);
+ void UnregisterPythonMonitorCallBack(XBMCAddon::xbmc::Monitor* pCallback);
+ void OnSettingsChanged(const std::string& strings);
+ void OnScreensaverActivated();
+ void OnScreensaverDeactivated();
+ void OnDPMSActivated();
+ void OnDPMSDeactivated();
+ void OnScanStarted(const std::string& library);
+ void OnScanFinished(const std::string& library);
+ void OnCleanStarted(const std::string& library);
+ void OnCleanFinished(const std::string& library);
+ void OnNotification(const std::string& sender,
+ const std::string& method,
+ const std::string& data);
+
+ void Process() override;
+ void PulseGlobalEvent() override;
+ void Uninitialize() override;
+ bool OnScriptInitialized(ILanguageInvoker* invoker) override;
+ void OnScriptStarted(ILanguageInvoker* invoker) override;
+ void NotifyScriptAborting(ILanguageInvoker* invoker) override;
+ void OnExecutionEnded(ILanguageInvoker* invoker) override;
+ void OnScriptFinalized(ILanguageInvoker* invoker) override;
+ ILanguageInvoker* CreateInvoker() override;
+
+ bool WaitForEvent(CEvent& hEvent, unsigned int milliseconds);
+
+private:
+ static bool m_bInitialized; // whether global python runtime was already initialized
+
+ CCriticalSection m_critSection;
+ void* m_mainThreadState{nullptr};
+ int m_iDllScriptCounter{0}; // to keep track of the total scripts running that need the dll
+
+ //Vector with list of threads used for running scripts
+ PyList m_vecPyList;
+ PlayerCallbackList m_vecPlayerCallbackList;
+ MonitorCallbackList m_vecMonitorCallbackList;
+
+ // any global events that scripts should be using
+ CEvent m_globalEvent;
+
+ // in order to finalize and unload the python library, need to save all the extension libraries that are
+ // loaded by it and unload them first (not done by finalize)
+ PythonExtensionLibraries m_extensions;
+};
diff --git a/xbmc/interfaces/python/preamble.h b/xbmc/interfaces/python/preamble.h
new file mode 100644
index 0000000..080a4fb
--- /dev/null
+++ b/xbmc/interfaces/python/preamble.h
@@ -0,0 +1,15 @@
+/*
+ * 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
+
+#ifdef SWIGPYTHON
+
+#include <Python.h>
+
+#endif
diff --git a/xbmc/interfaces/python/pythreadstate.h b/xbmc/interfaces/python/pythreadstate.h
new file mode 100644
index 0000000..2cae5a7
--- /dev/null
+++ b/xbmc/interfaces/python/pythreadstate.h
@@ -0,0 +1,64 @@
+/*
+ * 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
+
+
+//WARNING: since this will unlock/lock the python global interpreter lock,
+// it will not work recursively
+
+//this is basically a scoped version of a Py_BEGIN_ALLOW_THREADS .. Py_END_ALLOW_THREADS block
+class CPyThreadState
+{
+ public:
+ explicit CPyThreadState(bool save = true)
+ {
+ m_threadState = NULL;
+
+ if (save)
+ Save();
+ }
+
+ ~CPyThreadState()
+ {
+ Restore();
+ }
+
+ void Save()
+ {
+ if (!m_threadState)
+ m_threadState = PyEval_SaveThread(); //same as Py_BEGIN_ALLOW_THREADS
+ }
+
+ void Restore()
+ {
+ if (m_threadState)
+ {
+ PyEval_RestoreThread(m_threadState); //same as Py_END_ALLOW_THREADS
+ m_threadState = NULL;
+ }
+ }
+
+ private:
+ PyThreadState* m_threadState;
+};
+
+/**
+ * A std::unique_lock<CCriticalSection> that will relinquish the GIL during the time
+ * it takes to obtain the CriticalSection
+ */
+class GilSafeSingleLock : public CPyThreadState, public std::unique_lock<CCriticalSection>
+{
+public:
+ explicit GilSafeSingleLock(CCriticalSection& critSec)
+ : CPyThreadState(true), std::unique_lock<CCriticalSection>(critSec)
+ {
+ CPyThreadState::Restore();
+ }
+};
+
diff --git a/xbmc/interfaces/python/swig.cpp b/xbmc/interfaces/python/swig.cpp
new file mode 100644
index 0000000..0c49f87
--- /dev/null
+++ b/xbmc/interfaces/python/swig.cpp
@@ -0,0 +1,443 @@
+/*
+ * 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 "swig.h"
+
+#include "LanguageHook.h"
+#include "interfaces/legacy/AddonString.h"
+#include "utils/StringUtils.h"
+
+#include <string>
+
+namespace PythonBindings
+{
+ TypeInfo::TypeInfo(const std::type_info& ti) : swigType(NULL), parentType(NULL), typeIndex(ti)
+ {
+ static PyTypeObject py_type_object_header = {
+ PyVarObject_HEAD_INIT(nullptr, 0) 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+#if PY_VERSION_HEX > 0x03080000
+ 0,
+ 0,
+#endif
+#if PY_VERSION_HEX < 0x03090000
+ 0,
+#endif
+#if PY_VERSION_HEX >= 0x030C00A1
+ 0,
+#endif
+ };
+
+ static int size = (long*)&(py_type_object_header.tp_name) - (long*)&py_type_object_header;
+ memcpy(&(this->pythonType), &py_type_object_header, size);
+ }
+
+ class PyObjectDecrementor
+ {
+ PyObject* obj;
+ public:
+ inline explicit PyObjectDecrementor(PyObject* pyobj) : obj(pyobj) {}
+ inline ~PyObjectDecrementor() { Py_XDECREF(obj); }
+
+ inline PyObject* get() { return obj; }
+ };
+
+ void PyXBMCGetUnicodeString(std::string& buf, PyObject* pObject, bool coerceToString,
+ const char* argumentName, const char* methodname)
+ {
+ // It's okay for a string to be "None". In this case the buf returned
+ // will be the emptyString.
+ if (pObject == Py_None)
+ {
+ buf = XBMCAddon::emptyString;
+ return;
+ }
+
+ //! @todo UTF-8: Does python use UTF-16?
+ //! Do we need to convert from the string charset to UTF-8
+ //! for non-unicode data?
+ if (PyUnicode_Check(pObject))
+ {
+ // Python unicode objects are UCS2 or UCS4 depending on compilation
+ // options, wchar_t is 16-bit or 32-bit depending on platform.
+ // Avoid the complexity by just letting python convert the string.
+
+ buf = PyUnicode_AsUTF8(pObject);
+ return;
+ }
+
+ if (PyBytes_Check(pObject)) // If pobject is of type Bytes
+ {
+ buf = PyBytes_AsString(pObject);
+ return;
+ }
+
+ // if we got here then we need to coerce the value to a string
+ if (coerceToString)
+ {
+ PyObjectDecrementor dec(PyObject_Str(pObject));
+ PyObject* pyStrCast = dec.get();
+ if (pyStrCast)
+ {
+ PyXBMCGetUnicodeString(buf,pyStrCast,false,argumentName,methodname);
+ return;
+ }
+ }
+
+ // Object is not a unicode or a normal string.
+ buf = "";
+ throw XBMCAddon::WrongTypeException("argument \"%s\" for method \"%s\" must be unicode or str", argumentName, methodname);
+ }
+
+ // need to compare the typestring
+ bool isParameterRightType(const char* passedType, const char* expectedType, const char* methodNamespacePrefix, bool tryReverse)
+ {
+ if (strcmp(expectedType,passedType) == 0)
+ return true;
+
+ // well now things are a bit more complicated. We need to see if the passed type
+ // is a subset of the overall type
+ std::string et(expectedType);
+ bool isPointer = (et[0] == 'p' && et[1] == '.');
+ std::string baseType(et,(isPointer ? 2 : 0)); // this may contain a namespace
+
+ std::string ns(methodNamespacePrefix);
+ // cut off trailing '::'
+ if (ns.size() > 2 && ns[ns.size() - 1] == ':' && ns[ns.size() - 2] == ':')
+ ns = ns.substr(0,ns.size()-2);
+
+ bool done = false;
+ while(! done)
+ {
+ done = true;
+
+ // now we need to see if the expected type can be munged
+ // into the passed type by tacking on the namespace of
+ // of the method.
+ std::string check(isPointer ? "p." : "");
+ check += ns;
+ check += "::";
+ check += baseType;
+
+ if (strcmp(check.c_str(),passedType) == 0)
+ return true;
+
+ // see if the namespace is nested.
+ int posOfScopeOp = ns.find("::");
+ if (posOfScopeOp >= 0)
+ {
+ done = false;
+ // cur off the outermost namespace
+ ns = ns.substr(posOfScopeOp + 2);
+ }
+ }
+
+ // so far we applied the namespace to the expected type. Now lets try
+ // the reverse if we haven't already.
+ if (tryReverse)
+ return isParameterRightType(expectedType, passedType, methodNamespacePrefix, false);
+
+ return false;
+ }
+
+ PythonToCppException::PythonToCppException() : XbmcCommons::UncheckedException(" ")
+ {
+ setClassname("PythonToCppException");
+
+ std::string msg;
+ std::string type, value, traceback;
+ if (!ParsePythonException(type, value, traceback))
+ UncheckedException::SetMessage("Strange: No Python exception occurred");
+ else
+ SetMessage(type, value, traceback);
+ }
+
+ PythonToCppException::PythonToCppException(const std::string &exceptionType, const std::string &exceptionValue, const std::string &exceptionTraceback) : XbmcCommons::UncheckedException(" ")
+ {
+ setClassname("PythonToCppException");
+
+ SetMessage(exceptionType, exceptionValue, exceptionTraceback);
+ }
+
+ bool PythonToCppException::ParsePythonException(std::string &exceptionType, std::string &exceptionValue, std::string &exceptionTraceback)
+ {
+ PyObject* exc_type;
+ PyObject* exc_value;
+ PyObject* exc_traceback;
+ PyObject* pystring = NULL;
+
+ PyErr_Fetch(&exc_type, &exc_value, &exc_traceback);
+ if (exc_type == NULL && exc_value == NULL && exc_traceback == NULL)
+ return false;
+
+ // See https://docs.python.org/3/c-api/exceptions.html#c.PyErr_NormalizeException
+ PyErr_NormalizeException(&exc_type, &exc_value, &exc_traceback);
+ if (exc_traceback != NULL) {
+ PyException_SetTraceback(exc_value, exc_traceback);
+ }
+
+ exceptionType.clear();
+ exceptionValue.clear();
+ exceptionTraceback.clear();
+
+ if (exc_type != NULL && (pystring = PyObject_Str(exc_type)) != NULL && PyUnicode_Check(pystring))
+ {
+ const char* str = PyUnicode_AsUTF8(pystring);
+ if (str != NULL)
+ exceptionType = str;
+
+ pystring = PyObject_Str(exc_value);
+ if (pystring != NULL)
+ {
+ str = PyUnicode_AsUTF8(pystring);
+ exceptionValue = str;
+ }
+
+ PyObject *tracebackModule = PyImport_ImportModule("traceback");
+ if (tracebackModule != NULL)
+ {
+ char method[] = "format_exception";
+ char format[] = "OOO";
+ PyObject *tbList = PyObject_CallMethod(tracebackModule, method, format, exc_type, exc_value == NULL ? Py_None : exc_value, exc_traceback == NULL ? Py_None : exc_traceback);
+
+ if (tbList)
+ {
+ PyObject* emptyString = PyUnicode_FromString("");
+ char method[] = "join";
+ char format[] = "O";
+ PyObject *strRetval = PyObject_CallMethod(emptyString, method, format, tbList);
+ Py_DECREF(emptyString);
+
+ if (strRetval)
+ {
+ str = PyUnicode_AsUTF8(strRetval);
+ if (str != NULL)
+ exceptionTraceback = str;
+ Py_DECREF(strRetval);
+ }
+ Py_DECREF(tbList);
+ }
+ Py_DECREF(tracebackModule);
+
+ }
+ }
+
+ Py_XDECREF(exc_type);
+ Py_XDECREF(exc_value);
+ Py_XDECREF(exc_traceback);
+ Py_XDECREF(pystring);
+
+ return true;
+ }
+
+ void PythonToCppException::SetMessage(const std::string &exceptionType, const std::string &exceptionValue, const std::string &exceptionTraceback)
+ {
+ std::string msg = "-->Python callback/script returned the following error<--\n";
+ msg += " - NOTE: IGNORING THIS CAN LEAD TO MEMORY LEAKS!\n";
+
+ if (!exceptionType.empty())
+ {
+ msg += StringUtils::Format("Error Type: {}\n", exceptionType);
+
+ if (!exceptionValue.empty())
+ msg += StringUtils::Format("Error Contents: {}\n", exceptionValue);
+
+ if (!exceptionTraceback.empty())
+ msg += exceptionTraceback;
+
+ msg += "-->End of Python script error report<--\n";
+ }
+ else
+ msg += "<unknown exception type>";
+
+ UncheckedException::SetMessage("%s", msg.c_str());
+ }
+
+ XBMCAddon::AddonClass* doretrieveApiInstance(const PyHolder* pythonObj, const TypeInfo* typeInfo, const char* expectedType,
+ const char* methodNamespacePrefix, const char* methodNameForErrorString)
+ {
+ if (pythonObj->magicNumber != XBMC_PYTHON_TYPE_MAGIC_NUMBER)
+ throw XBMCAddon::WrongTypeException("Non api type passed to \"%s\" in place of the expected type \"%s.\"",
+ methodNameForErrorString, expectedType);
+ if (!isParameterRightType(typeInfo->swigType,expectedType,methodNamespacePrefix))
+ {
+ // maybe it's a child class
+ if (typeInfo->parentType)
+ return doretrieveApiInstance(pythonObj, typeInfo->parentType,expectedType,
+ methodNamespacePrefix, methodNameForErrorString);
+ else
+ throw XBMCAddon::WrongTypeException("Incorrect type passed to \"%s\", was expecting a \"%s\" but received a \"%s\"",
+ methodNameForErrorString,expectedType,typeInfo->swigType);
+ }
+ return const_cast<XBMCAddon::AddonClass*>(pythonObj->pSelf);
+ }
+
+ /**
+ * This method is a helper for the generated API. It's called prior to any API
+ * class constructor being returned from the generated code to Python
+ */
+ void prepareForReturn(XBMCAddon::AddonClass* c)
+ {
+ XBMC_TRACE;
+ if(c) {
+ c->Acquire();
+ PyThreadState* state = PyThreadState_Get();
+ XBMCAddon::Python::PythonLanguageHook::GetIfExists(state->interp)->RegisterAddonClassInstance(c);
+ }
+ }
+
+ static bool handleInterpRegistrationForClean(XBMCAddon::AddonClass* c)
+ {
+ XBMC_TRACE;
+ if(c){
+ XBMCAddon::AddonClass::Ref<XBMCAddon::Python::PythonLanguageHook> lh =
+ XBMCAddon::AddonClass::Ref<XBMCAddon::AddonClass>(c->GetLanguageHook());
+
+ if (lh.isNotNull())
+ {
+ lh->UnregisterAddonClassInstance(c);
+ return true;
+ }
+ else
+ {
+ PyThreadState* state = PyThreadState_Get();
+ lh = XBMCAddon::Python::PythonLanguageHook::GetIfExists(state->interp);
+ if (lh.isNotNull()) lh->UnregisterAddonClassInstance(c);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This method is a helper for the generated API. It's called prior to any API
+ * class destructor being dealloc-ed from the generated code from Python
+ */
+ void cleanForDealloc(XBMCAddon::AddonClass* c)
+ {
+ XBMC_TRACE;
+ if (handleInterpRegistrationForClean(c))
+ c->Release();
+ }
+
+ /**
+ * This method is a helper for the generated API. It's called prior to any API
+ * class destructor being dealloc-ed from the generated code from Python
+ *
+ * There is a Catch-22 in the destruction of a Window. 'dispose' needs to be
+ * called on destruction but cannot be called from the destructor.
+ * This overrides the default cleanForDealloc to resolve that.
+ */
+ void cleanForDealloc(XBMCAddon::xbmcgui::Window* c)
+ {
+ XBMC_TRACE;
+ if (handleInterpRegistrationForClean(c))
+ {
+ c->dispose();
+ c->Release();
+ }
+ }
+
+ /**
+ * This method allows for conversion of the native api Type to the Python type.
+ *
+ * When this form of the call is used (and pytype isn't NULL) then the
+ * passed type is used in the instance. This is for classes that extend API
+ * classes in python. The type passed may not be the same type that's stored
+ * in the class metadata of the AddonClass of which 'api' is an instance,
+ * it can be a subclass in python.
+ *
+ * if pytype is NULL then the type is inferred using the class metadata
+ * stored in the AddonClass instance 'api'.
+ */
+ PyObject* makePythonInstance(XBMCAddon::AddonClass* api, PyTypeObject* pytype, bool incrementRefCount)
+ {
+ // null api types result in Py_None
+ if (!api)
+ {
+ Py_INCREF(Py_None);
+ return Py_None;
+ }
+
+ // retrieve the TypeInfo from the api class
+ const TypeInfo* typeInfo = getTypeInfoForInstance(api);
+ PyTypeObject* typeObj = pytype == NULL ? const_cast<PyTypeObject*>(&(typeInfo->pythonType)) : pytype;
+
+ PyHolder* self = reinterpret_cast<PyHolder*>(typeObj->tp_alloc(typeObj,0));
+ if (!self) return NULL;
+ self->magicNumber = XBMC_PYTHON_TYPE_MAGIC_NUMBER;
+ self->typeInfo = typeInfo;
+ self->pSelf = api;
+ if (incrementRefCount)
+ Py_INCREF((PyObject*)self);
+ return (PyObject*)self;
+ }
+
+ std::map<std::type_index, const TypeInfo*> typeInfoLookup;
+
+ void registerAddonClassTypeInformation(const TypeInfo* classInfo)
+ {
+ typeInfoLookup[classInfo->typeIndex] = classInfo;
+ }
+
+ const TypeInfo* getTypeInfoForInstance(XBMCAddon::AddonClass* obj)
+ {
+ std::type_index ti(typeid(*obj));
+ return typeInfoLookup[ti];
+ }
+
+ int dummy_tp_init(PyObject* self, PyObject* args, PyObject* kwds)
+ {
+ return 0;
+ }
+}
+
diff --git a/xbmc/interfaces/python/swig.h b/xbmc/interfaces/python/swig.h
new file mode 100644
index 0000000..353d968
--- /dev/null
+++ b/xbmc/interfaces/python/swig.h
@@ -0,0 +1,202 @@
+/*
+ * 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 "interfaces/legacy/AddonClass.h"
+#include "interfaces/legacy/Exception.h"
+#include "interfaces/legacy/Window.h"
+
+#include <stdint.h>
+#include <string>
+#include <typeindex>
+
+#include <Python.h>
+
+namespace PythonBindings
+{
+ /**
+ * This call will convert the python object passed to a string. The object
+ * passed must be a python str or unicode object unless coerceToString is
+ * true. If coerceToString is true then the type must be castable to a string
+ * using the python call str(pObject).
+ *
+ * This method will handle a 'None' that's passed in. If 'None' is passed then
+ * the resulting buf will contain the value of XBMCAddon::emptyString (which
+ * is simply a std::string instantiated with the default constructor.
+ */
+ void PyXBMCGetUnicodeString(std::string& buf, PyObject* pObject, bool coerceToString = false,
+ const char* pos = "unknown",
+ const char* methodname = "unknown");
+
+ struct TypeInfo
+ {
+ const char* swigType;
+ TypeInfo* parentType;
+ PyTypeObject pythonType;
+ const std::type_index typeIndex;
+
+ explicit TypeInfo(const std::type_info& ti);
+ };
+
+ // This will hold the pointer to the api type, whether known or unknown
+ struct PyHolder
+ {
+ PyObject_HEAD
+ int32_t magicNumber;
+ const TypeInfo* typeInfo;
+ XBMCAddon::AddonClass* pSelf;
+ };
+
+#define XBMC_PYTHON_TYPE_MAGIC_NUMBER 0x58626D63
+
+ /**
+ * This method retrieves the pointer from the PyHolder. The return value should
+ * be cast to the appropriate type.
+ *
+ * Since the calls to this are generated there's no NULL pointer checks
+ */
+ inline XBMCAddon::AddonClass* retrieveApiInstance(PyObject* pythonObj, const TypeInfo* typeToCheck,
+ const char* methodNameForErrorString,
+ const char* typenameForErrorString)
+ {
+ if (pythonObj == NULL || pythonObj == Py_None)
+ return NULL;
+ if (reinterpret_cast<PyHolder*>(pythonObj)->magicNumber != XBMC_PYTHON_TYPE_MAGIC_NUMBER || !PyObject_TypeCheck(pythonObj, const_cast<PyTypeObject*>((&(typeToCheck->pythonType)))))
+ throw XBMCAddon::WrongTypeException("Incorrect type passed to \"%s\", was expecting a \"%s\".",methodNameForErrorString,typenameForErrorString);
+ return reinterpret_cast<PyHolder*>(pythonObj)->pSelf;
+ }
+
+ bool isParameterRightType(const char* passedType, const char* expectedType, const char* methodNamespacePrefix, bool tryReverse = true);
+
+ XBMCAddon::AddonClass* doretrieveApiInstance(const PyHolder* pythonObj, const TypeInfo* typeInfo, const char* expectedType,
+ const char* methodNamespacePrefix, const char* methodNameForErrorString);
+
+ /**
+ * This method retrieves the pointer from the PyHolder. The return value should
+ * be cast to the appropriate type.
+ *
+ * Since the calls to this are generated there's no NULL pointer checks
+ *
+ * This method will return NULL if either the pythonObj is NULL or the
+ * pythonObj is Py_None.
+ */
+ inline XBMCAddon::AddonClass* retrieveApiInstance(const PyObject* pythonObj, const char* expectedType, const char* methodNamespacePrefix,
+ const char* methodNameForErrorString)
+ {
+ return (pythonObj == NULL || pythonObj == Py_None) ? NULL :
+ doretrieveApiInstance(reinterpret_cast<const PyHolder*>(pythonObj),reinterpret_cast<const PyHolder*>(pythonObj)->typeInfo, expectedType, methodNamespacePrefix, methodNameForErrorString);
+ }
+
+ /**
+ * This method is a helper for the generated API. It's called prior to any API
+ * class constructor being returned from the generated code to Python
+ */
+ void prepareForReturn(XBMCAddon::AddonClass* c);
+
+ /**
+ * This method is a helper for the generated API. It's called prior to any API
+ * class destructor being dealloc-ed from the generated code from Python
+ */
+ void cleanForDealloc(XBMCAddon::AddonClass* c);
+
+ /**
+ * This method is a helper for the generated API. It's called prior to any API
+ * class destructor being dealloc-ed from the generated code from Python
+ *
+ * There is a Catch-22 in the destruction of a Window. 'dispose' needs to be
+ * called on destruction but cannot be called from the destructor.
+ * This overrides the default cleanForDealloc to resolve that.
+ */
+ void cleanForDealloc(XBMCAddon::xbmcgui::Window* c);
+
+ /**
+ * This method allows for conversion of the native api Type to the Python type.
+ *
+ * When this form of the call is used (and pythonType isn't NULL) then the
+ * passed type is used in the instance. This is for classes that extend API
+ * classes in python. The type passed may not be the same type that's stored
+ * in the class metadata of the AddonClass of which 'api' is an instance,
+ * it can be a subclass in python.
+ *
+ * if pythonType is NULL then the type is inferred using the class metadata
+ * stored in the AddonClass instance 'api'.
+ */
+ PyObject* makePythonInstance(XBMCAddon::AddonClass* api, PyTypeObject* pythonType, bool incrementRefCount);
+
+ /**
+ * This method allows for conversion of the native api Type to the Python type.
+ *
+ * When this form of the call is used then the python type constructed will be the
+ * type given by the class metadata in the AddonClass instance 'api'.
+ *
+ * This is just a helper inline to call the other makePythonInstance with NULL as
+ * the pythonType.
+ */
+ inline PyObject* makePythonInstance(XBMCAddon::AddonClass* api, bool incrementRefCount)
+ {
+ return makePythonInstance(api,NULL,incrementRefCount);
+ }
+
+ void registerAddonClassTypeInformation(const TypeInfo* classInfo);
+ const TypeInfo* getTypeInfoForInstance(XBMCAddon::AddonClass* obj);
+
+ int dummy_tp_init(PyObject* self, PyObject* args, PyObject* kwds);
+
+ class Director
+ {
+ protected:
+ PyObject* self;
+ public:
+ inline Director() : self(NULL) {}
+ inline void setPyObjectForDirector(PyObject* pyargself) { self = pyargself; }
+ };
+
+ /**
+ * This exception is thrown from Director calls that call into python when the
+ * Python error is
+ */
+ class PythonToCppException : public XbmcCommons::UncheckedException
+ {
+ public:
+ /**
+ * Assuming a PyErr_Occurred, this will fill the exception message with all
+ * of the appropriate information including the traceback if it can be
+ * obtained. It will also clear the python message.
+ */
+ PythonToCppException();
+ PythonToCppException(const std::string &exceptionType, const std::string &exceptionValue, const std::string &exceptionTraceback);
+
+ static bool ParsePythonException(std::string &exceptionType, std::string &exceptionValue, std::string &exceptionTraceback);
+
+ protected:
+ void SetMessage(const std::string &exceptionType, const std::string &exceptionValue, const std::string &exceptionTraceback);
+ };
+
+ template<class T> struct PythonCompare
+ {
+ static inline int compare(PyObject* obj1, PyObject* obj2, const char* swigType, const char* methodNamespacePrefix, const char* methodNameForErrorString)
+ {
+ XBMC_TRACE;
+ try
+ {
+ T* o1 = (T*)retrieveApiInstance(obj1, swigType, methodNamespacePrefix, methodNameForErrorString);
+ T* o2 = (T*)retrieveApiInstance(obj2, swigType, methodNamespacePrefix, methodNameForErrorString);
+
+ return ((*o1) < (*o2) ? -1 :
+ ((*o1) > (*o2) ? 1 : 0));
+ }
+ catch (const XBMCAddon::WrongTypeException& e)
+ {
+ CLog::Log(LOGERROR, "EXCEPTION: {}", e.GetExMessage());
+ PyErr_SetString(PyExc_RuntimeError, e.GetExMessage());
+ }
+ return -1;
+ }
+ };
+}
diff --git a/xbmc/interfaces/python/test/CMakeLists.txt b/xbmc/interfaces/python/test/CMakeLists.txt
new file mode 100644
index 0000000..ec38a51
--- /dev/null
+++ b/xbmc/interfaces/python/test/CMakeLists.txt
@@ -0,0 +1,5 @@
+if(PYTHON_FOUND)
+ set(SOURCES TestSwig.cpp)
+
+ core_add_test_library(python_test)
+endif()
diff --git a/xbmc/interfaces/python/test/TestSwig.cpp b/xbmc/interfaces/python/test/TestSwig.cpp
new file mode 100644
index 0000000..463bb98
--- /dev/null
+++ b/xbmc/interfaces/python/test/TestSwig.cpp
@@ -0,0 +1,21 @@
+/*
+ * 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 "../swig.h"
+
+#include <gtest/gtest.h>
+
+using namespace PythonBindings;
+
+TEST(TestSwig, TypeConversion)
+{
+ EXPECT_TRUE(isParameterRightType("p.XBMCAddon::xbmcgui::ListItem","p.XBMCAddon::xbmcgui::ListItem","XBMCAddon::xbmc::"));
+ EXPECT_TRUE(isParameterRightType("p.XBMCAddon::xbmc::PlayList","p.PlayList","XBMCAddon::xbmc::"));
+ EXPECT_TRUE(isParameterRightType("p.PlayList","p.XBMCAddon::xbmc::PlayList","XBMCAddon::xbmc::"));
+}
+
diff --git a/xbmc/interfaces/python/typemaps/python.Alternative.intm b/xbmc/interfaces/python/typemaps/python.Alternative.intm
new file mode 100644
index 0000000..12a5e65
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.Alternative.intm
@@ -0,0 +1,43 @@
+<%
+/*
+ * 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.
+ */
+ boolean ispointer = swigTypeParser.SwigType_ispointer(ltype)
+ String accessor = ispointer ? '->' : '.'
+ int seq = sequence.increment()
+ altAccess = [ 'former', 'later' ]
+ altSwitch = [ 'first', 'second' ]
+
+ List types = swigTypeParser.SwigType_templateparmlist(ltype)
+%>
+ {
+ // we need to check the parameter type and see if it matches
+ PyObject *pyentry_${seq} = ${slarg};
+ try
+ {
+ ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(types[0]))} entry0_${seq};
+ ${helper.getInConversion(types[0], 'entry0' + '_' + seq, 'pyentry' + '_' + seq, method,
+ [ 'sequence' : sequence ])}
+ ${api}${accessor}${altAccess[0]}() = entry0_${seq};
+ }
+ catch (const XBMCAddon::WrongTypeException&)
+ {
+ try
+ {
+ ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(types[1]))} entry1_${seq};
+ ${helper.getInConversion(types[1], 'entry1' + '_' + seq, 'pyentry' + '_' + seq, method,
+ [ 'sequence' : sequence ])}
+ ${api}${accessor}${altAccess[1]}() = entry1_${seq};
+ }
+ catch (const XBMCAddon::WrongTypeException&)
+ {
+ throw XBMCAddon::WrongTypeException("Failed to convert to input type to either a "
+ "${swigTypeParser.SwigType_ltype(types[0])} or a "
+ "${swigTypeParser.SwigType_ltype(types[1])}" );
+ }
+ }
+ } \ No newline at end of file
diff --git a/xbmc/interfaces/python/typemaps/python.Alternative.outtm b/xbmc/interfaces/python/typemaps/python.Alternative.outtm
new file mode 100644
index 0000000..31f4205
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.Alternative.outtm
@@ -0,0 +1,34 @@
+<%
+/*
+ * 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.
+ */
+ List types = swigTypeParser.SwigType_templateparmlist(type)
+ boolean ispointer = swigTypeParser.SwigType_ispointer(type)
+ int seq = sequence.increment()
+ String accessor = ispointer ? '->' : '.'
+ altAccess = [ 'former', 'later' ]
+ altSwitch = [ 'first', 'second' ]
+%>
+ WhichAlternative pos = ${api}${accessor}which();
+
+ if (<%if (ispointer) { %>${api} != NULL && <%}%>pos != XBMCAddon::none)
+ { <%
+ types.eachWithIndex { curType, entryIndex ->
+%>
+ if (pos == XBMCAddon::${altSwitch[entryIndex]})
+ {
+ ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_lrtype(curType))} entry${seq} = ${api}${accessor}${altAccess[entryIndex]}();
+ {
+ ${helper.getOutConversion(curType,result,method,[ 'api' : 'entry' + seq, 'sequence' : sequence ])}
+ }
+ }
+<%
+ }
+%>
+ }
+ else
+ ${result} = Py_None; \ No newline at end of file
diff --git a/xbmc/interfaces/python/typemaps/python.Tuple.intm b/xbmc/interfaces/python/typemaps/python.Tuple.intm
new file mode 100644
index 0000000..c426856
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.Tuple.intm
@@ -0,0 +1,35 @@
+<%
+/*
+ * 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.
+ */
+ List types = swigTypeParser.SwigType_templateparmlist(ltype)
+ boolean ispointer = swigTypeParser.SwigType_ispointer(type)
+ String accessor = ispointer ? '->' : '.'
+ int seq = sequence.increment()
+ tupleAccess = [ 'first', 'second', 'third', 'fourth' ]
+%>
+ if(${slarg})
+ {
+ bool isTuple = PyObject_TypeCheck(${slarg},&PyTuple_Type);
+ if (!isTuple && !PyObject_TypeCheck(${slarg},&PyList_Type))
+ throw WrongTypeException("The parameter \"${api}\" must be either a Tuple or a List.");
+ Py_ssize_t vecSize = (isTuple ? PyTuple_Size(${slarg}) : PyList_Size(${slarg}));
+<%
+ types.eachWithIndex { curType, entryIndex ->
+%>
+ if (vecSize > ${entryIndex})
+ {
+ PyObject *pyentry${entryIndex}_${seq} = NULL;
+ pyentry${entryIndex}_${seq} = (isTuple ? PyTuple_GetItem(${slarg}, ${entryIndex}) : PyList_GetItem(${slarg}, ${entryIndex}));
+ ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(curType))} entry${entryIndex}_${seq};
+ ${helper.getInConversion(curType, 'entry' + entryIndex + '_' + seq, 'pyentry' + entryIndex + '_' + seq, method,[ 'sequence' : sequence ])}
+ ${api}${accessor}${tupleAccess[entryIndex]}() = entry${entryIndex}_${seq};
+ }
+<%
+ }
+%>
+ }
diff --git a/xbmc/interfaces/python/typemaps/python.Tuple.outtm b/xbmc/interfaces/python/typemaps/python.Tuple.outtm
new file mode 100644
index 0000000..18655fe
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.Tuple.outtm
@@ -0,0 +1,39 @@
+<%
+/*
+ * 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.
+ */
+ List types = swigTypeParser.SwigType_templateparmlist(type)
+ boolean ispointer = swigTypeParser.SwigType_ispointer(type)
+ int seq = sequence.increment()
+ String accessor = ispointer ? '->' : '.'
+ tupleAccess = [ 'first', 'second', 'third', 'fourth' ]
+%>
+ int vecSize = ${api}${accessor}GetNumValuesSet();
+ ${result} = PyTuple_New(vecSize);
+<%
+ if (ispointer)
+ {
+%>
+ if (${api} != NULL)
+<% } // this ends the if (ispointer)
+%> {
+ PyObject* pyentry${seq}; <%
+ types.eachWithIndex { curType, entryIndex ->
+%>
+
+ if (vecSize > ${entryIndex})
+ {
+ ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_lrtype(curType))} entry${seq} = ${api}${accessor}${tupleAccess[entryIndex]}();
+ {
+ ${helper.getOutConversion(curType,'result',method,[ 'result' : 'pyentry' + seq, 'api' : 'entry' + seq, 'sequence' : sequence ])}
+ }
+ PyTuple_SetItem(${result}, ${entryIndex}, pyentry${seq});
+ }
+<%
+ }
+%>
+ } \ No newline at end of file
diff --git a/xbmc/interfaces/python/typemaps/python.buffer.intm b/xbmc/interfaces/python/typemaps/python.buffer.intm
new file mode 100644
index 0000000..f074b2b
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.buffer.intm
@@ -0,0 +1,36 @@
+<%
+/*
+ * 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.
+ */
+%>
+ if (PyUnicode_Check(${slarg}))
+ {
+ Py_ssize_t pysize;
+ const char* str = PyUnicode_AsUTF8AndSize(${slarg}, &pysize);
+ size_t size = static_cast<size_t>(pysize);
+ ${api}.allocate(size);
+ ${api}.put(str, size);
+ ${api}.flip(); // prepare the buffer for reading from
+ }
+ else if (PyBytes_Check(${slarg}))
+ {
+ Py_ssize_t pysize = PyBytes_GET_SIZE(${slarg});
+ const char* str = PyBytes_AS_STRING(${slarg});
+ size_t size = static_cast<size_t>(pysize);
+ ${api}.allocate(size);
+ ${api}.put(str, size);
+ ${api}.flip(); // prepare the buffer for reading from
+ }
+ else if (PyByteArray_Check(${slarg}))
+ {
+ size_t size = PyByteArray_Size(${slarg});
+ ${api}.allocate(size);
+ ${api}.put(PyByteArray_AsString(${slarg}),size);
+ ${api}.flip(); // prepare the buffer for reading from
+ }
+ else
+ throw XBMCAddon::WrongTypeException("argument \"%s\" for \"%s\" must be a string, bytes or a bytearray", "${api}", "${method.@name}"); \ No newline at end of file
diff --git a/xbmc/interfaces/python/typemaps/python.buffer.outtm b/xbmc/interfaces/python/typemaps/python.buffer.outtm
new file mode 100644
index 0000000..8e38d81
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.buffer.outtm
@@ -0,0 +1,11 @@
+<%
+/*
+ * 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.
+ */
+ boolean ispointer = swigTypeParser.SwigType_ispointer(type)
+ String accessor = ispointer ? '->' : '.'
+%>${result} = PyByteArray_FromStringAndSize((char*)${api}${accessor}curPosition(),${api}${accessor}remaining()); \ No newline at end of file
diff --git a/xbmc/interfaces/python/typemaps/python.dict.intm b/xbmc/interfaces/python/typemaps/python.dict.intm
new file mode 100644
index 0000000..ea3c78c
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.dict.intm
@@ -0,0 +1,23 @@
+<%
+/*
+ * 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.
+ */
+ List templateArgs = swigTypeParser.SwigType_templateparmlist(ltype)
+ valtype = templateArgs[0]
+%>
+ {
+ PyObject *pykey, *pyvalue;
+ Py_ssize_t pos = 0;
+ while(PyDict_Next(${slarg}, &pos, &pykey, &pyvalue))
+ {
+ std::string key;
+ PyXBMCGetUnicodeString(key,pykey,false,"${api}","${method.@name}");
+ ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(valtype))} value;
+ ${helper.getInConversion(valtype, 'value', 'pyvalue' ,method)}
+ ${api}.emplace(std::move(key), std::move(value));
+ }
+ } \ No newline at end of file
diff --git a/xbmc/interfaces/python/typemaps/python.map.intm b/xbmc/interfaces/python/typemaps/python.map.intm
new file mode 100644
index 0000000..095d23b
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.map.intm
@@ -0,0 +1,24 @@
+<%
+/*
+ * 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.
+ */
+ List templateArgs = swigTypeParser.SwigType_templateparmlist(ltype)
+ keytype = templateArgs[0]
+ valtype = templateArgs[1]
+%>
+ {
+ PyObject *pykey, *pyvalue;
+ Py_ssize_t pos = 0;
+ while(PyDict_Next(${slarg}, &pos, &pykey, &pyvalue))
+ {
+ ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(keytype))} key;
+ ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(valtype))} value;
+ ${helper.getInConversion(keytype, 'key', 'pykey', method)}
+ ${helper.getInConversion(valtype, 'value', 'pyvalue' ,method)}
+ ${api}.emplace(std::move(key), std::move(value));
+ }
+ } \ No newline at end of file
diff --git a/xbmc/interfaces/python/typemaps/python.smart_ptr.outtm b/xbmc/interfaces/python/typemaps/python.smart_ptr.outtm
new file mode 100644
index 0000000..0d7fa31
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.smart_ptr.outtm
@@ -0,0 +1,14 @@
+<%
+/*
+ * 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.
+ */
+ itype = swigTypeParser.SwigType_templateparmlist(type)[0]
+ pointertype = swigTypeParser.SwigType_makepointer(itype)
+ int seq = sequence.increment()
+%>
+ ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(pointertype))} entry${seq} = ${api}.get();
+ ${helper.getOutConversion(pointertype,'result',method,[ 'api' : 'entry' + seq, 'sequence' : sequence ])}
diff --git a/xbmc/interfaces/python/typemaps/python.string.outtm b/xbmc/interfaces/python/typemaps/python.string.outtm
new file mode 100644
index 0000000..f9eb068
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.string.outtm
@@ -0,0 +1,11 @@
+<%
+/*
+ * 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.
+ */
+%>${result} = <%
+ if(method.@feature_python_strictUnicode) { %>PyUnicode_DecodeUTF8(${api}.c_str(),${api}.size(),"strict");<% }
+ else { %>PyUnicode_DecodeUTF8(${api}.c_str(),${api}.size(),"surrogateescape");<% } %>
diff --git a/xbmc/interfaces/python/typemaps/python.vector.intm b/xbmc/interfaces/python/typemaps/python.vector.intm
new file mode 100644
index 0000000..6bc71ec
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.vector.intm
@@ -0,0 +1,36 @@
+<%
+/*
+ * 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.
+ */
+ List templateArgs = swigTypeParser.SwigType_templateparmlist(ltype)
+ vectype = templateArgs[0]
+ boolean ispointer = swigTypeParser.SwigType_ispointer(type)
+ String accessor = ispointer ? '->' : '.'
+ int seq = sequence.increment()
+%>
+ if (${slarg})
+ {
+ bool isTuple = PyObject_TypeCheck(${slarg},&PyTuple_Type);
+ if (!isTuple && !PyObject_TypeCheck(${slarg},&PyList_Type))
+ throw WrongTypeException("The parameter \"${api}\" must be either a Tuple or a List.");
+
+ <% if (ispointer) print("${api} = new std::vector<${swigTypeParser.SwigType_str(vectype)}>();") %>
+ PyObject *pyentry${seq} = NULL;
+ Py_ssize_t vecSize = (isTuple ? PyTuple_Size(${slarg}) : PyList_Size(${slarg}));
+ ${api}${accessor}reserve(vecSize);
+ for(Py_ssize_t i = 0; i < vecSize; i++)
+ {
+ pyentry${seq} = (isTuple ? PyTuple_GetItem(${slarg}, i) : PyList_GetItem(${slarg}, i));
+ ${swigTypeParser.SwigType_str(swigTypeParser.SwigType_ltype(vectype))} entry${seq};
+ ${helper.getInConversion(vectype, 'entry' + seq, 'pyentry' + seq, method,
+ [ 'type' : vectype,
+ 'ltype' : swigTypeParser.SwigType_ltype(vectype),
+ 'sequence' : sequence
+ ])}
+ ${api}${accessor}push_back(<% if (swigTypeParser.SwigType_ispointer(vectype) || vectype in ["bool", "double", "int"]) { %>entry${seq}<% } else { %>std::move(entry${seq})<% } %>);
+ }
+ }
diff --git a/xbmc/interfaces/python/typemaps/python.vector.outtm b/xbmc/interfaces/python/typemaps/python.vector.outtm
new file mode 100644
index 0000000..c1c4c79
--- /dev/null
+++ b/xbmc/interfaces/python/typemaps/python.vector.outtm
@@ -0,0 +1,35 @@
+<%
+/*
+ * 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.
+ */
+ List templateArgs = swigTypeParser.SwigType_templateparmlist(type)
+ vectype = templateArgs[0]
+ boolean ispointer = swigTypeParser.SwigType_ispointer(type)
+ String accessor = ispointer ? '->' : '.'
+ seq = sequence.increment()
+
+ if (ispointer)
+ {
+%>
+ if (${api} != NULL)
+ {
+<% } %>
+ ${result} = PyList_New(0);
+
+ for (std::vector<${swigTypeParser.SwigType_str(vectype)}>::iterator iter = ${api}${accessor}begin(); iter != ${api}${accessor}end(); ++iter)
+ {
+ PyObject* pyentry${seq};
+ ${helper.getOutConversion(vectype,'result',method,[ 'result' : 'pyentry' + seq, 'api' : '(*iter)', 'sequence' : sequence ])}
+ PyList_Append(${result}, pyentry${seq});
+ Py_DECREF(pyentry${seq});
+ }
+<%
+ if (ispointer)
+ {
+%>
+ }
+<% } %>
diff --git a/xbmc/interfaces/swig/AddonModuleXbmc.i b/xbmc/interfaces/swig/AddonModuleXbmc.i
new file mode 100644
index 0000000..0b54435
--- /dev/null
+++ b/xbmc/interfaces/swig/AddonModuleXbmc.i
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+%module(directors="1") xbmc
+
+%{
+#if defined(TARGET_WINDOWS)
+# include <windows.h>
+#endif
+
+#include "interfaces/legacy/Player.h"
+#include "interfaces/legacy/RenderCapture.h"
+#include "interfaces/legacy/Keyboard.h"
+#include "interfaces/legacy/ModuleXbmc.h"
+#include "interfaces/legacy/Monitor.h"
+
+using namespace XBMCAddon;
+using namespace xbmc;
+
+#if defined(__GNUG__)
+#pragma GCC diagnostic ignored "-Wstrict-aliasing"
+#endif
+%}
+
+// This is all about warning suppression. It's OK that these base classes are
+// not part of what swig parses.
+%feature("knownbasetypes") XBMCAddon::xbmc "AddonClass,IPlayerCallback,AddonCallback"
+%feature("knownapitypes") XBMCAddon::xbmc "XBMCAddon::xbmcgui::ListItem,XBMCAddon::xbmc::PlayListItem"
+
+%include "interfaces/legacy/swighelper.h"
+
+%include "interfaces/legacy/AddonString.h"
+%include "interfaces/legacy/ModuleXbmc.h"
+%include "interfaces/legacy/Dictionary.h"
+
+%feature("director") Player;
+
+%feature("python:nokwds") XBMCAddon::xbmc::Keyboard::Keyboard "true"
+%feature("python:nokwds") XBMCAddon::xbmc::Player::Player "true"
+%feature("python:nokwds") XBMCAddon::xbmc::PlayList::PlayList "true"
+
+%include "interfaces/legacy/Player.h"
+
+%include "interfaces/legacy/RenderCapture.h"
+
+%include "interfaces/legacy/InfoTagGame.h"
+%include "interfaces/legacy/InfoTagMusic.h"
+%include "interfaces/legacy/InfoTagPicture.h"
+%include "interfaces/legacy/InfoTagRadioRDS.h"
+%include "interfaces/legacy/InfoTagVideo.h"
+%include "interfaces/legacy/Keyboard.h"
+%include "interfaces/legacy/PlayList.h"
+
+%feature("director") Monitor;
+
+%include "interfaces/legacy/Monitor.h"
+
+
diff --git a/xbmc/interfaces/swig/AddonModuleXbmcaddon.i b/xbmc/interfaces/swig/AddonModuleXbmcaddon.i
new file mode 100644
index 0000000..6c00a1c
--- /dev/null
+++ b/xbmc/interfaces/swig/AddonModuleXbmcaddon.i
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+%module xbmcaddon
+
+%{
+#if defined(TARGET_WINDOWS)
+# include <windows.h>
+#endif
+
+#include "interfaces/legacy/Addon.h"
+#include "interfaces/legacy/Settings.h"
+
+using namespace XBMCAddon;
+using namespace xbmcaddon;
+
+#if defined(__GNUG__)
+#pragma GCC diagnostic ignored "-Wstrict-aliasing"
+#endif
+
+%}
+
+// This is all about warning suppression. It's OK that these base classes are
+// not part of what swig parses.
+%feature("knownbasetypes") XBMCAddon::xbmcaddon "AddonClass"
+
+%include "interfaces/legacy/swighelper.h"
+%include "interfaces/legacy/AddonString.h"
+
+%include "interfaces/legacy/Addon.h"
+%include "interfaces/legacy/Settings.h"
+
diff --git a/xbmc/interfaces/swig/AddonModuleXbmcdrm.i b/xbmc/interfaces/swig/AddonModuleXbmcdrm.i
new file mode 100644
index 0000000..055652a
--- /dev/null
+++ b/xbmc/interfaces/swig/AddonModuleXbmcdrm.i
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+%module xbmcdrm
+
+%{
+#if defined(TARGET_WINDOWS)
+# include <windows.h>
+#endif
+
+#include "interfaces/legacy/DrmCryptoSession.h"
+#include "utils/log.h"
+
+using namespace XBMCAddon;
+using namespace xbmcdrm;
+
+#if defined(__GNUG__)
+#pragma GCC diagnostic ignored "-Wstrict-aliasing"
+#endif
+
+%}
+
+%include "interfaces/legacy/swighelper.h"
+%include "interfaces/legacy/AddonString.h"
+%include "interfaces/legacy/DrmCryptoSession.h"
diff --git a/xbmc/interfaces/swig/AddonModuleXbmcgui.i b/xbmc/interfaces/swig/AddonModuleXbmcgui.i
new file mode 100644
index 0000000..b869401
--- /dev/null
+++ b/xbmc/interfaces/swig/AddonModuleXbmcgui.i
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+%module(directors="1") xbmcgui
+
+%{
+#if defined(TARGET_WINDOWS)
+# include <windows.h>
+#endif
+
+#include "interfaces/legacy/Dialog.h"
+#include "interfaces/legacy/ModuleXbmcgui.h"
+#include "interfaces/legacy/Control.h"
+#include "interfaces/legacy/Window.h"
+#include "interfaces/legacy/WindowDialog.h"
+#include "interfaces/legacy/Dialog.h"
+#include "interfaces/legacy/WindowXML.h"
+#include "input/actions/ActionIDs.h"
+#include "input/Key.h"
+
+using namespace XBMCAddon;
+using namespace xbmcgui;
+
+#if defined(__GNUG__)
+#pragma GCC diagnostic ignored "-Wstrict-aliasing"
+#endif
+
+%}
+
+// This is all about warning suppression. It's OK that these base classes are
+// not part of what swig parses.
+%feature("knownbasetypes") XBMCAddon::xbmcgui "AddonClass,AddonCallback"
+
+%feature("knownapitypes") XBMCAddon::xbmcgui "XBMCAddon::xbmc::InfoTagVideo,xbmc::InfoTagMusic,xbmc::InfoTagPicture,xbmc::InfoTagGame"
+
+%include "interfaces/legacy/swighelper.h"
+%include "interfaces/legacy/AddonString.h"
+
+%include "interfaces/legacy/ModuleXbmcgui.h"
+
+%include "interfaces/legacy/Exception.h"
+
+%include "interfaces/legacy/Dictionary.h"
+
+%include "interfaces/legacy/ListItem.h"
+
+%include "interfaces/legacy/Control.h"
+
+%include "interfaces/legacy/Dialog.h"
+
+%feature("python:nokwds") XBMCAddon::xbmcgui::Dialog::Dialog "true"
+%feature("python:nokwds") XBMCAddon::xbmcgui::Window::Window "true"
+%feature("python:nokwds") XBMCAddon::xbmcgui::WindowXML::WindowXML "true"
+%feature("python:nokwds") XBMCAddon::xbmcgui::WindowXMLDialog::WindowXMLDialog "true"
+%feature("python:nokwds") XBMCAddon::xbmcgui::WindowDialog::WindowDialog "true"
+
+%feature("director") Window;
+%feature("director") WindowDialog;
+%feature("director") WindowXML;
+%feature("director") WindowXMLDialog;
+
+// This is such a damn hack it makes me nauseous
+%feature("python:rcmp") XBMCAddon::xbmcgui::Action
+ { XBMC_TRACE;
+ if (method == Py_EQ)
+ {
+ XBMCAddon::xbmcgui::Action* a1 = (Action*)retrieveApiInstance(obj1,&TyXBMCAddon_xbmcgui_Action_Type,"rcmp","XBMCAddon::xbmcgui::Action");
+ if (PyObject_TypeCheck(obj2, &(TyXBMCAddon_xbmcgui_Action_Type.pythonType)))
+ {
+ // both are Action objects
+ XBMCAddon::xbmcgui::Action* a2 = (Action*)retrieveApiInstance(obj2,&TyXBMCAddon_xbmcgui_Action_Type,"rcmp","XBMCAddon::xbmcgui::Action");
+
+ if (a1->id == a2->id &&
+ a1->buttonCode == a2->buttonCode &&
+ a1->fAmount1 == a2->fAmount1 &&
+ a1->fAmount2 == a2->fAmount2 &&
+ a1->fRepeat == a2->fRepeat &&
+ a1->strAction == a2->strAction)
+ {
+ Py_RETURN_TRUE;
+ }
+ else
+ {
+ Py_RETURN_FALSE;
+ }
+ }
+ else
+ {
+ // for backwards compatibility in python scripts
+ PyObject* o1 = PyLong_FromLong(a1->id);
+ return PyObject_RichCompare(o1, obj2, method);
+ }
+ }
+ Py_INCREF(Py_NotImplemented);
+ return Py_NotImplemented;
+ }
+
+
+%include "interfaces/legacy/Window.h"
+%include "interfaces/legacy/WindowDialog.h"
+%include "interfaces/legacy/Dialog.h"
+
+%include "interfaces/legacy/WindowXML.h"
+
+%include "input/actions/ActionIDs.h"
+%include "input/Key.h"
diff --git a/xbmc/interfaces/swig/AddonModuleXbmcplugin.i b/xbmc/interfaces/swig/AddonModuleXbmcplugin.i
new file mode 100644
index 0000000..24e42b1
--- /dev/null
+++ b/xbmc/interfaces/swig/AddonModuleXbmcplugin.i
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+%module xbmcplugin
+
+%{
+#if defined(TARGET_WINDOWS)
+# include <windows.h>
+#endif
+
+#include "interfaces/legacy/ModuleXbmcplugin.h"
+
+using namespace XBMCAddon;
+using namespace xbmcplugin;
+
+#if defined(__GNUG__)
+#pragma GCC diagnostic ignored "-Wstrict-aliasing"
+#endif
+
+%}
+
+%feature("knownapitypes") XBMCAddon::xbmcplugin "XBMCAddon::xbmcgui::ListItem"
+
+%include "interfaces/legacy/swighelper.h"
+%include "interfaces/legacy/AddonString.h"
+%include "interfaces/legacy/ModuleXbmcplugin.h"
+
diff --git a/xbmc/interfaces/swig/AddonModuleXbmcvfs.i b/xbmc/interfaces/swig/AddonModuleXbmcvfs.i
new file mode 100644
index 0000000..bf18f38
--- /dev/null
+++ b/xbmc/interfaces/swig/AddonModuleXbmcvfs.i
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+%module xbmcvfs
+
+%{
+#if defined(TARGET_WINDOWS)
+# include <windows.h>
+#endif
+
+#include "interfaces/legacy/ModuleXbmcvfs.h"
+#include "interfaces/legacy/File.h"
+#include "interfaces/legacy/Stat.h"
+#include "utils/log.h"
+
+using namespace XBMCAddon;
+using namespace xbmcvfs;
+
+#if defined(__GNUG__)
+#pragma GCC diagnostic ignored "-Wstrict-aliasing"
+#endif
+
+%}
+
+%include "interfaces/legacy/swighelper.h"
+%include "interfaces/legacy/AddonString.h"
+
+%feature("python:strictUnicode") XBMCAddon::xbmcvfs::File::read "true"
+
+%include "interfaces/legacy/File.h"
+
+%rename ("st_atime") XBMCAddon::xbmcvfs::Stat::atime;
+%rename ("st_mtime") XBMCAddon::xbmcvfs::Stat::mtime;
+%rename ("st_ctime") XBMCAddon::xbmcvfs::Stat::ctime;
+%include "interfaces/legacy/Stat.h"
+
+%rename ("delete") XBMCAddon::xbmcvfs::deleteFile;
+%include "interfaces/legacy/ModuleXbmcvfs.h"
+
diff --git a/xbmc/interfaces/swig/AddonModuleXbmcwsgi.i b/xbmc/interfaces/swig/AddonModuleXbmcwsgi.i
new file mode 100644
index 0000000..6df84e0
--- /dev/null
+++ b/xbmc/interfaces/swig/AddonModuleXbmcwsgi.i
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+%begin %{
+#if defined(TARGET_WINDOWS)
+# include <windows.h>
+#endif
+
+#ifdef HAS_WEB_SERVER
+%}
+
+%module xbmcwsgi
+
+%{
+#include "interfaces/legacy/wsgi/WsgiErrorStream.h"
+#include "interfaces/legacy/wsgi/WsgiInputStream.h"
+#include "interfaces/legacy/wsgi/WsgiResponse.h"
+#include "interfaces/legacy/wsgi/WsgiResponseBody.h"
+
+using namespace XBMCAddon;
+using namespace xbmcwsgi;
+
+#if defined(__GNUG__)
+#pragma GCC diagnostic ignored "-Wstrict-aliasing"
+#endif
+
+%}
+
+// This is all about warning suppression. It's OK that these base classes are
+// not part of what swig parses.
+%feature("knownbasetypes") XBMCAddon::xbmcaddon "AddonClass"
+
+%feature("iterator") WsgiInputStreamIterator "std::string"
+%feature("iterable") WsgiInputStream "XBMCAddon::xbmcwsgi::WsgiInputStreamIterator"
+
+%include "interfaces/legacy/swighelper.h"
+%include "interfaces/legacy/AddonString.h"
+
+%include "interfaces/legacy/wsgi/WsgiErrorStream.h"
+%include "interfaces/legacy/wsgi/WsgiInputStream.h"
+%include "interfaces/legacy/wsgi/WsgiResponse.h"
+%include "interfaces/legacy/wsgi/WsgiResponseBody.h"
+
+%insert("footer") %{
+#endif
+%}
+
diff --git a/xbmc/interfaces/swig/CMakeLists.txt b/xbmc/interfaces/swig/CMakeLists.txt
new file mode 100644
index 0000000..f89eb98
--- /dev/null
+++ b/xbmc/interfaces/swig/CMakeLists.txt
@@ -0,0 +1,63 @@
+function(generate_file file)
+ set(classpath ${GROOVY_DIR}/groovy-${GROOVY_VER}.jar
+ ${GROOVY_DIR}/groovy-xml-${GROOVY_VER}.jar
+ ${GROOVY_DIR}/groovy-templates-${GROOVY_VER}.jar
+ ${GROOVY_DIR}/commons-lang-${COMMONS_VER}.jar
+ ${CMAKE_SOURCE_DIR}/tools/codegenerator
+ ${CMAKE_CURRENT_SOURCE_DIR}/../python)
+ if(NOT CORE_SYSTEM_NAME STREQUAL windows AND NOT CORE_SYSTEM_NAME STREQUAL windowsstore)
+ set(devnull "/dev/null")
+ string(REPLACE ";" ":" classpath "${classpath}")
+ else()
+ set(devnull "nul")
+ endif()
+
+ set(CPP_FILE ${file}.cpp)
+ if(CLANGFORMAT_FOUND)
+ set(CLANG_FORMAT_COMMAND COMMAND ${CLANG_FORMAT_EXECUTABLE} ARGS -i ${CPP_FILE})
+ endif()
+
+ if(Java_VERSION_MAJOR GREATER 8)
+ set(JAVA_OPEN_OPTS --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.regex=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED)
+ endif()
+
+ add_custom_command(OUTPUT ${CPP_FILE}
+ COMMAND ${SWIG_EXECUTABLE}
+ ARGS -w401 -c++ -o ${file}.xml -xml -I${CMAKE_SOURCE_DIR}/xbmc -xmllang python ${CMAKE_CURRENT_SOURCE_DIR}/../swig/${file}
+ COMMAND ${Java_JAVA_EXECUTABLE}
+ ARGS ${JAVA_OPEN_OPTS} -cp "${classpath}" groovy.ui.GroovyMain ${CMAKE_SOURCE_DIR}/tools/codegenerator/Generator.groovy ${file}.xml ${CMAKE_CURRENT_SOURCE_DIR}/../python/PythonSwig.cpp.template ${file}.cpp > ${devnull}
+ ${CLANG_FORMAT_COMMAND}
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/../swig/${file} ${CMAKE_CURRENT_SOURCE_DIR}/../python/PythonSwig.cpp.template)
+ set(SOURCES ${SOURCES} "${CPP_FILE}" PARENT_SCOPE)
+endfunction()
+
+find_package(Java COMPONENTS Runtime REQUIRED)
+find_package(SWIG REQUIRED)
+
+# The generated bindings
+set(INPUTS AddonModuleXbmcaddon.i
+ AddonModuleXbmcdrm.i
+ AddonModuleXbmcgui.i
+ AddonModuleXbmc.i
+ AddonModuleXbmcplugin.i
+ AddonModuleXbmcvfs.i
+ AddonModuleXbmcwsgi.i)
+
+set(GROOVY_DIR ${CMAKE_SOURCE_DIR}/tools/codegenerator/groovy)
+set(GROOVY_VER 4.0.6)
+set(COMMONS_VER 2.6)
+
+foreach(INPUT IN LISTS INPUTS)
+ generate_file(${INPUT})
+ list(APPEND GEN_SRCS ${CMAKE_CURRENT_BINARY_DIR}/${INPUT}.cpp)
+endforeach()
+
+add_library(python_binding STATIC ${SOURCES})
+set_target_properties(python_binding PROPERTIES POSITION_INDEPENDENT_CODE TRUE
+ FOLDER "Build Utilities")
+set(core_DEPENDS python_binding ${core_DEPENDS} CACHE STRING "" FORCE)
+add_dependencies(python_binding ${GLOBAL_TARGET_DEPS})
+
+if(CORE_SYSTEM_NAME STREQUAL windowsstore)
+ set_target_properties(python_binding PROPERTIES STATIC_LIBRARY_FLAGS "/ignore:4264")
+endif()
diff --git a/xbmc/listproviders/CMakeLists.txt b/xbmc/listproviders/CMakeLists.txt
new file mode 100644
index 0000000..e9c0ef1
--- /dev/null
+++ b/xbmc/listproviders/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES DirectoryProvider.cpp
+ IListProvider.cpp
+ MultiProvider.cpp
+ StaticProvider.cpp)
+
+set(HEADERS DirectoryProvider.h
+ IListProvider.h
+ MultiProvider.h
+ StaticProvider.h)
+
+core_add_library(listproviders)
diff --git a/xbmc/listproviders/DirectoryProvider.cpp b/xbmc/listproviders/DirectoryProvider.cpp
new file mode 100644
index 0000000..6a9395f
--- /dev/null
+++ b/xbmc/listproviders/DirectoryProvider.cpp
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2013-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 "DirectoryProvider.h"
+
+#include "ContextMenuManager.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/gui/GUIDialogAddonInfo.h"
+#include "favourites/FavouritesService.h"
+#include "favourites/FavouritesURL.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "interfaces/AnnouncementManager.h"
+#include "music/MusicThumbLoader.h"
+#include "music/dialogs/GUIDialogMusicInfo.h"
+#include "pictures/PictureThumbLoader.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRThumbLoader.h"
+#include "pvr/guilib/PVRGUIActionsUtils.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/ExecString.h"
+#include "utils/JobManager.h"
+#include "utils/PlayerUtils.h"
+#include "utils/SortUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+#include "video/VideoThumbLoader.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+#include "video/windows/GUIWindowVideoBase.h"
+
+#include <memory>
+#include <mutex>
+#include <utility>
+
+using namespace XFILE;
+using namespace KODI::MESSAGING;
+using namespace PVR;
+
+class CDirectoryJob : public CJob
+{
+public:
+ CDirectoryJob(const std::string &url, SortDescription sort, int limit, int parentID)
+ : m_url(url),
+ m_sort(sort),
+ m_limit(limit),
+ m_parentID(parentID)
+ { }
+ ~CDirectoryJob() override = default;
+
+ const char* GetType() const override { return "directory"; }
+ bool operator==(const CJob *job) const override
+ {
+ if (strcmp(job->GetType(),GetType()) == 0)
+ {
+ const CDirectoryJob* dirJob = dynamic_cast<const CDirectoryJob*>(job);
+ if (dirJob && dirJob->m_url == m_url)
+ return true;
+ }
+ return false;
+ }
+
+ bool DoWork() override
+ {
+ CFileItemList items;
+ if (CDirectory::GetDirectory(m_url, items, "", DIR_FLAG_DEFAULTS))
+ {
+ // sort the items if necessary
+ if (m_sort.sortBy != SortByNone)
+ items.Sort(m_sort);
+
+ // limit must not exceed the number of items
+ int limit = (m_limit == 0) ? items.Size() : std::min((int) m_limit, items.Size());
+ // convert to CGUIStaticItem's and set visibility and targets
+ m_items.reserve(limit);
+ for (int i = 0; i < limit; i++)
+ {
+ CGUIStaticItemPtr item(new CGUIStaticItem(*items[i]));
+ if (item->HasProperty("node.visible"))
+ item->SetVisibleCondition(item->GetProperty("node.visible").asString(), m_parentID);
+
+ getThumbLoader(item)->LoadItem(item.get());
+
+ m_items.push_back(item);
+ }
+ m_target = items.GetProperty("node.target").asString();
+ }
+ return true;
+ }
+
+ std::shared_ptr<CThumbLoader> getThumbLoader(const CGUIStaticItemPtr& item)
+ {
+ if (item->IsVideo())
+ {
+ initThumbLoader<CVideoThumbLoader>(InfoTagType::VIDEO);
+ return m_thumbloaders[InfoTagType::VIDEO];
+ }
+ if (item->IsAudio())
+ {
+ initThumbLoader<CMusicThumbLoader>(InfoTagType::AUDIO);
+ return m_thumbloaders[InfoTagType::AUDIO];
+ }
+ if (item->IsPicture())
+ {
+ initThumbLoader<CPictureThumbLoader>(InfoTagType::PICTURE);
+ return m_thumbloaders[InfoTagType::PICTURE];
+ }
+ if (item->IsPVRChannelGroup())
+ {
+ initThumbLoader<CPVRThumbLoader>(InfoTagType::PVR);
+ return m_thumbloaders[InfoTagType::PVR];
+ }
+ initThumbLoader<CProgramThumbLoader>(InfoTagType::PROGRAM);
+ return m_thumbloaders[InfoTagType::PROGRAM];
+ }
+
+ template<class CThumbLoaderClass>
+ void initThumbLoader(InfoTagType type)
+ {
+ if (!m_thumbloaders.count(type))
+ {
+ std::shared_ptr<CThumbLoader> thumbLoader = std::make_shared<CThumbLoaderClass>();
+ thumbLoader->OnLoaderStart();
+ m_thumbloaders.insert(make_pair(type, thumbLoader));
+ }
+ }
+
+ const std::vector<CGUIStaticItemPtr> &GetItems() const { return m_items; }
+ const std::string &GetTarget() const { return m_target; }
+ std::vector<InfoTagType> GetItemTypes(std::vector<InfoTagType> &itemTypes) const
+ {
+ itemTypes.clear();
+ for (const auto& i : m_thumbloaders)
+ itemTypes.push_back(i.first);
+ return itemTypes;
+ }
+private:
+ std::string m_url;
+ std::string m_target;
+ SortDescription m_sort;
+ unsigned int m_limit;
+ int m_parentID;
+ std::vector<CGUIStaticItemPtr> m_items;
+ std::map<InfoTagType, std::shared_ptr<CThumbLoader> > m_thumbloaders;
+};
+
+CDirectoryProvider::CDirectoryProvider(const TiXmlElement *element, int parentID)
+ : IListProvider(parentID),
+ m_updateState(OK),
+ m_jobID(0),
+ m_currentLimit(0)
+{
+ assert(element);
+ if (!element->NoChildren())
+ {
+ const char *target = element->Attribute("target");
+ if (target)
+ m_target.SetLabel(target, "", parentID);
+
+ const char *sortMethod = element->Attribute("sortby");
+ if (sortMethod)
+ m_sortMethod.SetLabel(sortMethod, "", parentID);
+
+ const char *sortOrder = element->Attribute("sortorder");
+ if (sortOrder)
+ m_sortOrder.SetLabel(sortOrder, "", parentID);
+
+ const char *limit = element->Attribute("limit");
+ if (limit)
+ m_limit.SetLabel(limit, "", parentID);
+
+ m_url.SetLabel(element->FirstChild()->ValueStr(), "", parentID);
+ }
+}
+
+CDirectoryProvider::CDirectoryProvider(const CDirectoryProvider& other)
+ : IListProvider(other.m_parentID),
+ m_updateState(INVALIDATED),
+ m_jobID(0),
+ m_url(other.m_url),
+ m_target(other.m_target),
+ m_sortMethod(other.m_sortMethod),
+ m_sortOrder(other.m_sortOrder),
+ m_limit(other.m_limit),
+ m_currentUrl(other.m_currentUrl),
+ m_currentTarget(other.m_currentTarget),
+ m_currentSort(other.m_currentSort),
+ m_currentLimit(other.m_currentLimit)
+{
+}
+
+CDirectoryProvider::~CDirectoryProvider()
+{
+ Reset();
+}
+
+std::unique_ptr<IListProvider> CDirectoryProvider::Clone()
+{
+ return std::make_unique<CDirectoryProvider>(*this);
+}
+
+bool CDirectoryProvider::Update(bool forceRefresh)
+{
+ // we never need to force refresh here
+ bool changed = false;
+ bool fireJob = false;
+
+ // update the URL & limit and fire off a new job if needed
+ fireJob |= UpdateURL();
+ fireJob |= UpdateSort();
+ fireJob |= UpdateLimit();
+ fireJob &= !m_currentUrl.empty();
+
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_updateState == INVALIDATED)
+ fireJob = true;
+ else if (m_updateState == DONE)
+ changed = true;
+
+ m_updateState = OK;
+
+ if (fireJob)
+ {
+ CLog::Log(LOGDEBUG, "CDirectoryProvider[{}]: refreshing..", m_currentUrl);
+ if (m_jobID)
+ CServiceBroker::GetJobManager()->CancelJob(m_jobID);
+ m_jobID = CServiceBroker::GetJobManager()->AddJob(
+ new CDirectoryJob(m_currentUrl, m_currentSort, m_currentLimit, m_parentID), this);
+ }
+
+ if (!changed)
+ {
+ for (auto& i : m_items)
+ changed |= i->UpdateVisibility(m_parentID);
+ }
+ return changed; //! @todo Also returned changed if properties are changed (if so, need to update scroll to letter).
+}
+
+void CDirectoryProvider::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ // we are only interested in library, player and GUI changes
+ if ((flag & (ANNOUNCEMENT::VideoLibrary | ANNOUNCEMENT::AudioLibrary | ANNOUNCEMENT::Player | ANNOUNCEMENT::GUI)) == 0)
+ return;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ // we don't need to refresh anything if there are no fitting
+ // items in this list provider for the announcement flag
+ if (((flag & ANNOUNCEMENT::VideoLibrary) &&
+ (std::find(m_itemTypes.begin(), m_itemTypes.end(), InfoTagType::VIDEO) == m_itemTypes.end())) ||
+ ((flag & ANNOUNCEMENT::AudioLibrary) &&
+ (std::find(m_itemTypes.begin(), m_itemTypes.end(), InfoTagType::AUDIO) == m_itemTypes.end())))
+ return;
+
+ if (flag & ANNOUNCEMENT::Player)
+ {
+ if (message == "OnPlay" || message == "OnResume" || message == "OnStop")
+ {
+ if (m_currentSort.sortBy == SortByNone || // not nice, but many directories that need to be refreshed on start/stop have no special sort order (e.g. in progress movies)
+ m_currentSort.sortBy == SortByLastPlayed ||
+ m_currentSort.sortBy == SortByPlaycount ||
+ m_currentSort.sortBy == SortByLastUsed)
+ m_updateState = INVALIDATED;
+ }
+ }
+ else
+ {
+ // if we're in a database transaction, don't bother doing anything just yet
+ if (data.isMember("transaction") && data["transaction"].asBoolean())
+ return;
+
+ // if there was a database update, we set the update state
+ // to PENDING to fire off a new job in the next update
+ if (message == "OnScanFinished" || message == "OnCleanFinished" || message == "OnUpdate" ||
+ message == "OnRemove" || message == "OnRefresh")
+ m_updateState = INVALIDATED;
+ }
+ }
+}
+
+void CDirectoryProvider::Fetch(std::vector<CGUIListItemPtr> &items)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ items.clear();
+ for (const auto& i : m_items)
+ {
+ if (i->IsVisible())
+ items.push_back(i);
+ }
+}
+
+void CDirectoryProvider::OnAddonEvent(const ADDON::AddonEvent& event)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (URIUtils::IsProtocol(m_currentUrl, "addons"))
+ {
+ if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) ||
+ typeid(event) == typeid(ADDON::AddonEvents::Disabled) ||
+ typeid(event) == typeid(ADDON::AddonEvents::ReInstalled) ||
+ typeid(event) == typeid(ADDON::AddonEvents::UnInstalled) ||
+ typeid(event) == typeid(ADDON::AddonEvents::MetadataChanged) ||
+ typeid(event) == typeid(ADDON::AddonEvents::AutoUpdateStateChanged))
+ m_updateState = INVALIDATED;
+ }
+}
+
+void CDirectoryProvider::OnAddonRepositoryEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (URIUtils::IsProtocol(m_currentUrl, "addons"))
+ {
+ m_updateState = INVALIDATED;
+ }
+}
+
+void CDirectoryProvider::OnPVRManagerEvent(const PVR::PVREvent& event)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (URIUtils::IsProtocol(m_currentUrl, "pvr"))
+ {
+ if (event == PVR::PVREvent::ManagerStarted || event == PVR::PVREvent::ManagerStopped ||
+ event == PVR::PVREvent::ManagerError || event == PVR::PVREvent::ManagerInterrupted ||
+ event == PVR::PVREvent::RecordingsInvalidated ||
+ event == PVR::PVREvent::TimersInvalidated ||
+ event == PVR::PVREvent::ChannelGroupsInvalidated ||
+ event == PVR::PVREvent::SavedSearchesInvalidated)
+ m_updateState = INVALIDATED;
+ }
+}
+
+void CDirectoryProvider::OnFavouritesEvent(const CFavouritesService::FavouritesUpdated& event)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (URIUtils::IsProtocol(m_currentUrl, "favourites"))
+ m_updateState = INVALIDATED;
+}
+
+void CDirectoryProvider::Reset()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (m_jobID)
+ CServiceBroker::GetJobManager()->CancelJob(m_jobID);
+ m_jobID = 0;
+ m_items.clear();
+ m_currentTarget.clear();
+ m_currentUrl.clear();
+ m_itemTypes.clear();
+ m_currentSort.sortBy = SortByNone;
+ m_currentSort.sortOrder = SortOrderAscending;
+ m_currentLimit = 0;
+ m_updateState = OK;
+ }
+
+ std::unique_lock<CCriticalSection> subscriptionLock(m_subscriptionSection);
+ if (m_isSubscribed)
+ {
+ m_isSubscribed = false;
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+ CServiceBroker::GetFavouritesService().Events().Unsubscribe(this);
+ CServiceBroker::GetRepositoryUpdater().Events().Unsubscribe(this);
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+ CServiceBroker::GetPVRManager().Events().Unsubscribe(this);
+ }
+}
+
+void CDirectoryProvider::FreeResources(bool immediately)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for (const auto& item : m_items)
+ item->FreeMemory(immediately);
+}
+
+void CDirectoryProvider::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (success)
+ {
+ m_items = static_cast<CDirectoryJob*>(job)->GetItems();
+ m_currentTarget = static_cast<CDirectoryJob*>(job)->GetTarget();
+ static_cast<CDirectoryJob*>(job)->GetItemTypes(m_itemTypes);
+ if (m_updateState == OK)
+ m_updateState = DONE;
+ }
+ m_jobID = 0;
+}
+
+std::string CDirectoryProvider::GetTarget(const CFileItem& item) const
+{
+ std::string target = item.GetProperty("node.target").asString();
+
+ std::unique_lock<CCriticalSection> lock(m_section);
+ if (target.empty())
+ target = m_currentTarget;
+ if (target.empty())
+ target = m_target.GetLabel(m_parentID, false);
+
+ return target;
+}
+
+namespace
+{
+bool ExecuteAction(const std::string& execute)
+{
+ if (!execute.empty())
+ {
+ CGUIMessage message(GUI_MSG_EXECUTE, 0, 0);
+ message.SetStringParam(execute);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+ return true;
+ }
+ return false;
+}
+} // namespace
+
+bool CDirectoryProvider::OnClick(const CGUIListItemPtr &item)
+{
+ CFileItem fileItem(*std::static_pointer_cast<CFileItem>(item));
+
+ if (fileItem.HasVideoInfoTag()
+ && CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MYVIDEOS_SELECTACTION) == SELECT_ACTION_INFO
+ && OnInfo(item))
+ return true;
+
+ if (fileItem.HasProperty("node.target_url"))
+ fileItem.SetPath(fileItem.GetProperty("node.target_url").asString());
+
+ // grab and execute the execute string
+ return ExecuteAction(CExecString(fileItem, GetTarget(fileItem)).GetExecString());
+}
+
+bool CDirectoryProvider::OnPlay(const CGUIListItemPtr& item)
+{
+ CFileItem fileItem(*std::static_pointer_cast<CFileItem>(item));
+
+ if (fileItem.IsFavourite())
+ {
+ // Resolve the favourite
+ const CFavouritesURL url(fileItem.GetPath());
+ if (url.IsValid())
+ {
+ // If action is playmedia, just play it
+ if (url.GetAction() == CFavouritesURL::Action::PLAY_MEDIA)
+ return ExecuteAction(url.GetExecString());
+
+ CFileItem targetItem(url.GetTarget(), url.IsDir());
+ fileItem = targetItem;
+ }
+ }
+
+ if (CPlayerUtils::IsItemPlayable(fileItem))
+ {
+ CExecString exec(fileItem, {});
+ if (exec.GetFunction() == "playmedia")
+ {
+ return ExecuteAction(exec.GetExecString());
+ }
+ else
+ {
+ // build and execute a playmedia execute string
+ exec = CExecString("PlayMedia", {StringUtils::Paramify(fileItem.GetPath())});
+ return ExecuteAction(exec.GetExecString());
+ }
+ }
+ return true;
+}
+
+bool CDirectoryProvider::OnInfo(const CGUIListItemPtr& item)
+{
+ auto fileItem = std::static_pointer_cast<CFileItem>(item);
+
+ if (fileItem->HasAddonInfo())
+ {
+ return CGUIDialogAddonInfo::ShowForItem(fileItem);
+ }
+ else if (fileItem->IsPVR())
+ {
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Utils>().OnInfo(*fileItem);
+ }
+ else if (fileItem->HasVideoInfoTag())
+ {
+ auto mediaType = fileItem->GetVideoInfoTag()->m_type;
+ if (mediaType == MediaTypeMovie ||
+ mediaType == MediaTypeTvShow ||
+ mediaType == MediaTypeEpisode ||
+ mediaType == MediaTypeVideo ||
+ mediaType == MediaTypeMusicVideo)
+ {
+ CGUIDialogVideoInfo::ShowFor(*fileItem);
+ return true;
+ }
+ }
+ else if (fileItem->HasMusicInfoTag())
+ {
+ CGUIDialogMusicInfo::ShowFor(fileItem.get());
+ return true;
+ }
+ return false;
+}
+
+bool CDirectoryProvider::OnContextMenu(const CGUIListItemPtr& item)
+{
+ auto fileItem = std::static_pointer_cast<CFileItem>(item);
+
+ const std::string target = GetTarget(*fileItem);
+ if (!target.empty())
+ fileItem->SetProperty("targetwindow", target);
+
+ return CONTEXTMENU::ShowFor(fileItem);
+}
+
+bool CDirectoryProvider::IsUpdating() const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ return m_jobID || m_updateState == DONE || m_updateState == INVALIDATED;
+}
+
+bool CDirectoryProvider::UpdateURL()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ std::string value(m_url.GetLabel(m_parentID, false));
+ if (value == m_currentUrl)
+ return false;
+
+ m_currentUrl = value;
+ }
+
+ std::unique_lock<CCriticalSection> subscriptionLock(m_subscriptionSection);
+ if (!m_isSubscribed)
+ {
+ m_isSubscribed = true;
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CDirectoryProvider::OnAddonEvent);
+ CServiceBroker::GetRepositoryUpdater().Events().Subscribe(this, &CDirectoryProvider::OnAddonRepositoryEvent);
+ CServiceBroker::GetPVRManager().Events().Subscribe(this, &CDirectoryProvider::OnPVRManagerEvent);
+ CServiceBroker::GetFavouritesService().Events().Subscribe(this, &CDirectoryProvider::OnFavouritesEvent);
+ }
+ return true;
+}
+
+bool CDirectoryProvider::UpdateLimit()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ unsigned int value = m_limit.GetIntValue(m_parentID);
+ if (value == m_currentLimit)
+ return false;
+
+ m_currentLimit = value;
+
+ return true;
+}
+
+bool CDirectoryProvider::UpdateSort()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ SortBy sortMethod(SortUtils::SortMethodFromString(m_sortMethod.GetLabel(m_parentID, false)));
+ SortOrder sortOrder(SortUtils::SortOrderFromString(m_sortOrder.GetLabel(m_parentID, false)));
+ if (sortOrder == SortOrderNone)
+ sortOrder = SortOrderAscending;
+
+ if (sortMethod == m_currentSort.sortBy && sortOrder == m_currentSort.sortOrder)
+ return false;
+
+ m_currentSort.sortBy = sortMethod;
+ m_currentSort.sortOrder = sortOrder;
+ m_currentSort.sortAttributes = SortAttributeIgnoreFolders;
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ m_currentSort.sortAttributes = static_cast<SortAttribute>(m_currentSort.sortAttributes | SortAttributeIgnoreArticle);
+
+ return true;
+}
diff --git a/xbmc/listproviders/DirectoryProvider.h b/xbmc/listproviders/DirectoryProvider.h
new file mode 100644
index 0000000..8e39dc7
--- /dev/null
+++ b/xbmc/listproviders/DirectoryProvider.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2013-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 "IListProvider.h"
+#include "addons/AddonEvents.h"
+#include "addons/RepositoryUpdater.h"
+#include "favourites/FavouritesService.h"
+#include "guilib/GUIStaticItem.h"
+#include "interfaces/IAnnouncer.h"
+#include "threads/CriticalSection.h"
+#include "utils/Job.h"
+
+#include <string>
+#include <vector>
+
+class TiXmlElement;
+class CVariant;
+
+namespace PVR
+{
+ enum class PVREvent;
+}
+
+enum class InfoTagType
+{
+ VIDEO,
+ AUDIO,
+ PICTURE,
+ PROGRAM,
+ PVR,
+};
+
+class CDirectoryProvider :
+ public IListProvider,
+ public IJobCallback,
+ public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ typedef enum
+ {
+ OK,
+ INVALIDATED,
+ DONE
+ } UpdateState;
+
+ CDirectoryProvider(const TiXmlElement *element, int parentID);
+ explicit CDirectoryProvider(const CDirectoryProvider& other);
+ ~CDirectoryProvider() override;
+
+ // Implementation of IListProvider
+ std::unique_ptr<IListProvider> Clone() override;
+ bool Update(bool forceRefresh) override;
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+ void Fetch(std::vector<CGUIListItemPtr> &items) override;
+ void Reset() override;
+ bool OnClick(const CGUIListItemPtr &item) override;
+ bool OnPlay(const CGUIListItemPtr& item) override;
+ bool OnInfo(const CGUIListItemPtr &item) override;
+ bool OnContextMenu(const CGUIListItemPtr &item) override;
+ bool IsUpdating() const override;
+ void FreeResources(bool immediately) override;
+
+ // callback from directory job
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+private:
+ UpdateState m_updateState;
+ unsigned int m_jobID;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_url;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_target;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_sortMethod;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_sortOrder;
+ KODI::GUILIB::GUIINFO::CGUIInfoLabel m_limit;
+ std::string m_currentUrl;
+ std::string m_currentTarget; ///< \brief node.target property on the list as a whole
+ SortDescription m_currentSort;
+ unsigned int m_currentLimit;
+ std::vector<CGUIStaticItemPtr> m_items;
+ std::vector<InfoTagType> m_itemTypes;
+ mutable CCriticalSection m_section;
+
+ bool UpdateURL();
+ bool UpdateLimit();
+ bool UpdateSort();
+ void OnAddonEvent(const ADDON::AddonEvent& event);
+ void OnAddonRepositoryEvent(const ADDON::CRepositoryUpdater::RepositoryUpdated& event);
+ void OnPVRManagerEvent(const PVR::PVREvent& event);
+ void OnFavouritesEvent(const CFavouritesService::FavouritesUpdated& event);
+ std::string GetTarget(const CFileItem& item) const;
+
+ CCriticalSection m_subscriptionSection;
+ bool m_isSubscribed{false};
+};
diff --git a/xbmc/listproviders/IListProvider.cpp b/xbmc/listproviders/IListProvider.cpp
new file mode 100644
index 0000000..3979319
--- /dev/null
+++ b/xbmc/listproviders/IListProvider.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2013-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 "IListProvider.h"
+
+#include "DirectoryProvider.h"
+#include "MultiProvider.h"
+#include "StaticProvider.h"
+#include "utils/XBMCTinyXML.h"
+
+std::unique_ptr<IListProvider> IListProvider::Create(const TiXmlNode* node, int parentID)
+{
+ const TiXmlNode *root = node->FirstChild("content");
+ if (root)
+ {
+ const TiXmlNode *next = root->NextSibling("content");
+ if (next)
+ return std::make_unique<CMultiProvider>(root, parentID);
+
+ return CreateSingle(root, parentID);
+ }
+ return std::unique_ptr<IListProvider>{};
+}
+
+std::unique_ptr<IListProvider> IListProvider::CreateSingle(const TiXmlNode* content, int parentID)
+{
+ const TiXmlElement *item = content->FirstChildElement("item");
+ if (item)
+ return std::make_unique<CStaticListProvider>(content->ToElement(), parentID);
+
+ if (!content->NoChildren())
+ return std::make_unique<CDirectoryProvider>(content->ToElement(), parentID);
+
+ return std::unique_ptr<IListProvider>{};
+}
diff --git a/xbmc/listproviders/IListProvider.h b/xbmc/listproviders/IListProvider.h
new file mode 100644
index 0000000..fea2860
--- /dev/null
+++ b/xbmc/listproviders/IListProvider.h
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2013-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 <memory>
+#include <vector>
+
+class TiXmlNode;
+class CGUIListItem;
+typedef std::shared_ptr<CGUIListItem> CGUIListItemPtr;
+
+/*!
+ \ingroup listproviders
+ \brief An interface for providing lists to UI containers.
+ */
+class IListProvider
+{
+public:
+ explicit IListProvider(int parentID) : m_parentID(parentID) {}
+ explicit IListProvider(const IListProvider& other) = default;
+ virtual ~IListProvider() = default;
+
+ /*! \brief Factory to create list providers.
+ \param parent a parent TiXmlNode for the container.
+ \param parentID id of parent window for context.
+ \return the list provider, empty pointer if none.
+ */
+ static std::unique_ptr<IListProvider> Create(const TiXmlNode* parent, int parentID);
+
+ /*! \brief Factory to create list providers. Cannot create a multi-provider.
+ \param content the TiXmlNode for the content to create.
+ \param parentID id of parent window for context.
+ \return the list provider, empty pointer if none.
+ */
+ static std::unique_ptr<IListProvider> CreateSingle(const TiXmlNode* content, int parentID);
+
+ /*! \brief Create an instance of the derived class. Allows for polymorphic copies.
+ */
+ virtual std::unique_ptr<IListProvider> Clone() = 0;
+
+ /*! \brief Update the list content
+ \return true if the content has changed, false otherwise.
+ */
+ virtual bool Update(bool forceRefresh)=0;
+
+ /*! \brief Fetch the current list of items.
+ \param items [out] the list to be filled.
+ */
+ virtual void Fetch(std::vector<CGUIListItemPtr> &items)=0;
+
+ /*! \brief Check whether the list provider is updating content.
+ \return true if in the processing of updating, false otherwise.
+ */
+ virtual bool IsUpdating() const { return false; }
+
+ /*! \brief Reset the current list of items.
+ Derived classes may choose to ignore this.
+ */
+ virtual void Reset() {}
+
+ /*! \brief Free all GUI resources allocated by the items.
+ \param immediately true to free resources immediately, free resources async later otherwise.
+ */
+ virtual void FreeResources(bool immediately) {}
+
+ /*! \brief Click event on an item.
+ \param item the item that was clicked.
+ \return true if the click was handled, false otherwise.
+ */
+ virtual bool OnClick(const CGUIListItemPtr &item)=0;
+
+ /*! \brief Play event on an item.
+ \param item the item to play.
+ \return true if the event was handled, false otherwise.
+ */
+ virtual bool OnPlay(const CGUIListItemPtr& item) { return false; }
+
+ /*! \brief Open the info dialog for an item provided by this IListProvider.
+ \param item the item that was clicked.
+ \return true if the dialog was shown, false otherwise.
+ */
+ virtual bool OnInfo(const CGUIListItemPtr &item)=0;
+
+ /*! \brief Open the context menu for an item provided by this IListProvider.
+ \param item the item that was clicked.
+ \return true if the click was handled, false otherwise.
+ */
+ virtual bool OnContextMenu(const CGUIListItemPtr &item)=0;
+
+ /*! \brief Set the default item to focus. For backwards compatibility.
+ \param item the item to focus.
+ \param always whether this item should always be used on first focus.
+ \sa GetDefaultItem, AlwaysFocusDefaultItem
+ */
+ virtual void SetDefaultItem(int item, bool always) {}
+
+ /*! \brief The default item to focus.
+ \return the item to focus by default. -1 for none.
+ \sa SetDefaultItem, AlwaysFocusDefaultItem
+ */
+ virtual int GetDefaultItem() const { return -1; }
+
+ /*! \brief Whether to always focus the default item.
+ \return true if the default item should always be the one to receive focus.
+ \sa GetDefaultItem, SetDefaultItem
+ */
+ virtual bool AlwaysFocusDefaultItem() const { return false; }
+protected:
+ int m_parentID;
+};
diff --git a/xbmc/listproviders/MultiProvider.cpp b/xbmc/listproviders/MultiProvider.cpp
new file mode 100644
index 0000000..e05a037
--- /dev/null
+++ b/xbmc/listproviders/MultiProvider.cpp
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2013-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 "MultiProvider.h"
+
+#include "utils/XBMCTinyXML.h"
+
+#include <mutex>
+
+CMultiProvider::CMultiProvider(const TiXmlNode *first, int parentID)
+ : IListProvider(parentID)
+{
+ for (const TiXmlNode *content = first; content; content = content->NextSiblingElement("content"))
+ {
+ IListProviderPtr sub(IListProvider::CreateSingle(content, parentID));
+ if (sub)
+ m_providers.push_back(std::move(sub));
+ }
+}
+
+CMultiProvider::CMultiProvider(const CMultiProvider& other) : IListProvider(other.m_parentID)
+{
+ for (const auto& provider : other.m_providers)
+ {
+ std::unique_ptr<IListProvider> newProvider = provider->Clone();
+ if (newProvider)
+ m_providers.emplace_back(std::move(newProvider));
+ }
+}
+
+std::unique_ptr<IListProvider> CMultiProvider::Clone()
+{
+ return std::make_unique<CMultiProvider>(*this);
+}
+
+bool CMultiProvider::Update(bool forceRefresh)
+{
+ bool result = false;
+ for (auto& provider : m_providers)
+ result |= provider->Update(forceRefresh);
+ return result;
+}
+
+void CMultiProvider::Fetch(std::vector<CGUIListItemPtr> &items)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ std::vector<CGUIListItemPtr> subItems;
+ items.clear();
+ m_itemMap.clear();
+ for (auto const& provider : m_providers)
+ {
+ provider->Fetch(subItems);
+ for (auto& item : subItems)
+ {
+ auto key = GetItemKey(item);
+ m_itemMap[key] = provider.get();
+ items.push_back(item);
+ }
+ subItems.clear();
+ }
+}
+
+bool CMultiProvider::IsUpdating() const
+{
+ bool result = false;
+ for (auto const& provider : m_providers)
+ result |= provider->IsUpdating();
+ return result;
+}
+
+void CMultiProvider::Reset()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_itemMap.clear();
+ }
+
+ for (auto const& provider : m_providers)
+ provider->Reset();
+}
+
+bool CMultiProvider::OnClick(const CGUIListItemPtr &item)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ auto key = GetItemKey(item);
+ auto it = m_itemMap.find(key);
+ if (it != m_itemMap.end())
+ return it->second->OnClick(item);
+ else
+ return false;
+}
+
+bool CMultiProvider::OnInfo(const CGUIListItemPtr &item)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ auto key = GetItemKey(item);
+ auto it = m_itemMap.find(key);
+ if (it != m_itemMap.end())
+ return it->second->OnInfo(item);
+ else
+ return false;
+}
+
+bool CMultiProvider::OnContextMenu(const CGUIListItemPtr &item)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ auto key = GetItemKey(item);
+ auto it = m_itemMap.find(key);
+ if (it != m_itemMap.end())
+ return it->second->OnContextMenu(item);
+ else
+ return false;
+}
+
+CMultiProvider::item_key_type CMultiProvider::GetItemKey(CGUIListItemPtr const &item)
+{
+ return reinterpret_cast<item_key_type>(item.get());
+}
diff --git a/xbmc/listproviders/MultiProvider.h b/xbmc/listproviders/MultiProvider.h
new file mode 100644
index 0000000..9eeddc0
--- /dev/null
+++ b/xbmc/listproviders/MultiProvider.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013-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 "IListProvider.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <vector>
+
+typedef std::unique_ptr<IListProvider> IListProviderPtr;
+
+/*!
+ \ingroup listproviders
+ \brief A listprovider that handles multiple individual providers.
+ */
+class CMultiProvider : public IListProvider
+{
+public:
+ CMultiProvider(const TiXmlNode *first, int parentID);
+ explicit CMultiProvider(const CMultiProvider& other);
+
+ // Implementation of IListProvider
+ std::unique_ptr<IListProvider> Clone() override;
+ bool Update(bool forceRefresh) override;
+ void Fetch(std::vector<CGUIListItemPtr> &items) override;
+ bool IsUpdating() const override;
+ void Reset() override;
+ bool OnClick(const CGUIListItemPtr &item) override;
+ bool OnInfo(const CGUIListItemPtr &item) override;
+ bool OnContextMenu(const CGUIListItemPtr &item) override;
+
+protected:
+ typedef size_t item_key_type;
+ static item_key_type GetItemKey(CGUIListItemPtr const &item);
+ std::vector<IListProviderPtr> m_providers;
+ std::map<item_key_type, IListProvider*> m_itemMap;
+ CCriticalSection m_section; // protects m_itemMap
+};
diff --git a/xbmc/listproviders/StaticProvider.cpp b/xbmc/listproviders/StaticProvider.cpp
new file mode 100644
index 0000000..a5750e6
--- /dev/null
+++ b/xbmc/listproviders/StaticProvider.cpp
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2013-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 "StaticProvider.h"
+
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/XMLUtils.h"
+
+CStaticListProvider::CStaticListProvider(const TiXmlElement *element, int parentID)
+: IListProvider(parentID),
+ m_defaultItem(-1),
+ m_defaultAlways(false),
+ m_updateTime(0)
+{
+ assert(element);
+
+ const TiXmlElement *item = element->FirstChildElement("item");
+ while (item)
+ {
+ if (item->FirstChild())
+ {
+ CGUIStaticItemPtr newItem(new CGUIStaticItem(item, parentID));
+ m_items.push_back(newItem);
+ }
+ item = item->NextSiblingElement("item");
+ }
+
+ if (XMLUtils::GetInt(element, "default", m_defaultItem))
+ {
+ const char *always = element->FirstChildElement("default")->Attribute("always");
+ if (always && StringUtils::CompareNoCase(always, "true", 4) == 0)
+ m_defaultAlways = true;
+ }
+}
+
+CStaticListProvider::CStaticListProvider(const std::vector<CGUIStaticItemPtr> &items)
+: IListProvider(0),
+ m_defaultItem(-1),
+ m_defaultAlways(false),
+ m_updateTime(0),
+ m_items(items)
+{
+}
+
+CStaticListProvider::CStaticListProvider(const CStaticListProvider& other)
+ : IListProvider(other.m_parentID),
+ m_defaultItem(other.m_defaultItem),
+ m_defaultAlways(other.m_defaultAlways),
+ m_updateTime(other.m_updateTime)
+{
+ for (const auto& item : other.m_items)
+ {
+ std::shared_ptr<CGUIListItem> control(item->Clone());
+ if (!control)
+ continue;
+
+ std::shared_ptr<CGUIStaticItem> newItem = std::dynamic_pointer_cast<CGUIStaticItem>(control);
+ if (!newItem)
+ continue;
+
+ m_items.emplace_back(std::move(newItem));
+ }
+}
+
+CStaticListProvider::~CStaticListProvider() = default;
+
+std::unique_ptr<IListProvider> CStaticListProvider::Clone()
+{
+ return std::make_unique<CStaticListProvider>(*this);
+}
+
+bool CStaticListProvider::Update(bool forceRefresh)
+{
+ bool changed = forceRefresh;
+ if (!m_updateTime)
+ m_updateTime = CTimeUtils::GetFrameTime();
+ else if (CTimeUtils::GetFrameTime() - m_updateTime > 1000)
+ {
+ m_updateTime = CTimeUtils::GetFrameTime();
+ for (auto& i : m_items)
+ i->UpdateProperties(m_parentID);
+ }
+ for (auto& i : m_items)
+ changed |= i->UpdateVisibility(m_parentID);
+ return changed; //! @todo Also returned changed if properties are changed (if so, need to update scroll to letter).
+}
+
+void CStaticListProvider::Fetch(std::vector<CGUIListItemPtr> &items)
+{
+ items.clear();
+ for (const auto& i : m_items)
+ {
+ if (i->IsVisible())
+ items.push_back(i);
+ }
+}
+
+void CStaticListProvider::SetDefaultItem(int item, bool always)
+{
+ m_defaultItem = item;
+ m_defaultAlways = always;
+}
+
+int CStaticListProvider::GetDefaultItem() const
+{
+ if (m_defaultItem >= 0)
+ {
+ unsigned int offset = 0;
+ for (const auto& i : m_items)
+ {
+ if (i->IsVisible())
+ {
+ if (i->m_iprogramCount == m_defaultItem)
+ return offset;
+ offset++;
+ }
+ }
+ }
+ return -1;
+}
+
+bool CStaticListProvider::AlwaysFocusDefaultItem() const
+{
+ return m_defaultAlways;
+}
+
+bool CStaticListProvider::OnClick(const CGUIListItemPtr &item)
+{
+ CGUIStaticItem *staticItem = static_cast<CGUIStaticItem*>(item.get());
+ return staticItem->GetClickActions().ExecuteActions(0, m_parentID);
+}
diff --git a/xbmc/listproviders/StaticProvider.h b/xbmc/listproviders/StaticProvider.h
new file mode 100644
index 0000000..6aea6b1
--- /dev/null
+++ b/xbmc/listproviders/StaticProvider.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2013-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 "IListProvider.h"
+#include "guilib/GUIStaticItem.h"
+
+#include <vector>
+
+class CStaticListProvider : public IListProvider
+{
+public:
+ CStaticListProvider(const TiXmlElement *element, int parentID);
+ explicit CStaticListProvider(const std::vector<CGUIStaticItemPtr> &items); // for python
+ explicit CStaticListProvider(const CStaticListProvider& other);
+ ~CStaticListProvider() override;
+
+ // Implementation of IListProvider
+ std::unique_ptr<IListProvider> Clone() override;
+ bool Update(bool forceRefresh) override;
+ void Fetch(std::vector<CGUIListItemPtr> &items) override;
+ bool OnClick(const CGUIListItemPtr &item) override;
+ bool OnInfo(const CGUIListItemPtr &item) override { return false; }
+ bool OnContextMenu(const CGUIListItemPtr &item) override { return false; }
+ void SetDefaultItem(int item, bool always) override;
+ int GetDefaultItem() const override;
+ bool AlwaysFocusDefaultItem() const override;
+private:
+ int m_defaultItem;
+ bool m_defaultAlways;
+ unsigned int m_updateTime;
+ std::vector<CGUIStaticItemPtr> m_items;
+};
diff --git a/xbmc/media/CMakeLists.txt b/xbmc/media/CMakeLists.txt
new file mode 100644
index 0000000..76b0737
--- /dev/null
+++ b/xbmc/media/CMakeLists.txt
@@ -0,0 +1,6 @@
+set(SOURCES MediaType.cpp)
+
+set(HEADERS MediaLockState.h
+ MediaType.h)
+
+core_add_library(media)
diff --git a/xbmc/media/MediaLockState.h b/xbmc/media/MediaLockState.h
new file mode 100644
index 0000000..48c1183
--- /dev/null
+++ b/xbmc/media/MediaLockState.h
@@ -0,0 +1,16 @@
+/*
+ * Copyright (C) 2005-2020 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
+
+typedef enum
+{
+ LOCK_STATE_NO_LOCK = 0,
+ LOCK_STATE_LOCK_BUT_UNLOCKED = 1,
+ LOCK_STATE_LOCKED = 2,
+} MediaLockState;
diff --git a/xbmc/media/MediaType.cpp b/xbmc/media/MediaType.cpp
new file mode 100644
index 0000000..86484fe
--- /dev/null
+++ b/xbmc/media/MediaType.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2013-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 "MediaType.h"
+
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+
+#include <utility>
+
+static std::map<std::string, CMediaTypes::MediaTypeInfo> fillDefaultMediaTypes()
+{
+ std::map<std::string, CMediaTypes::MediaTypeInfo> mediaTypes;
+
+ // clang-format off
+ mediaTypes.insert(std::make_pair(MediaTypeMusic, CMediaTypes::MediaTypeInfo(MediaTypeMusic, MediaTypeMusic, true, 36914, 36915, 249, 249)));
+ mediaTypes.insert(std::make_pair(MediaTypeArtist, CMediaTypes::MediaTypeInfo(MediaTypeArtist, MediaTypeArtist "s", true, 36916, 36917, 557, 133)));
+ mediaTypes.insert(std::make_pair(MediaTypeAlbum, CMediaTypes::MediaTypeInfo(MediaTypeAlbum, MediaTypeAlbum "s", true, 36918, 36919, 558, 132)));
+ mediaTypes.insert(std::make_pair(MediaTypeSong, CMediaTypes::MediaTypeInfo(MediaTypeSong, MediaTypeSong "s", false, 36920, 36921, 179, 134)));
+ mediaTypes.insert(std::make_pair(MediaTypeVideo, CMediaTypes::MediaTypeInfo(MediaTypeVideo, MediaTypeVideo "s", true, 36912, 36913, 291, 3)));
+ mediaTypes.insert(std::make_pair(MediaTypeVideoCollection, CMediaTypes::MediaTypeInfo(MediaTypeVideoCollection, MediaTypeVideoCollection "s", true, 36910, 36911, 20141, 20434)));
+ mediaTypes.insert(std::make_pair(MediaTypeMusicVideo, CMediaTypes::MediaTypeInfo(MediaTypeMusicVideo, MediaTypeMusicVideo "s", false, 36908, 36909, 20391, 20389)));
+ mediaTypes.insert(std::make_pair(MediaTypeMovie, CMediaTypes::MediaTypeInfo(MediaTypeMovie, MediaTypeMovie "s", false, 36900, 36901, 20338, 20342)));
+ mediaTypes.insert(std::make_pair(MediaTypeTvShow, CMediaTypes::MediaTypeInfo(MediaTypeTvShow, MediaTypeTvShow "s", true, 36902, 36903, 36902, 36903)));
+ mediaTypes.insert(std::make_pair(MediaTypeSeason, CMediaTypes::MediaTypeInfo(MediaTypeSeason, MediaTypeSeason "s", true, 36904, 36905, 20373, 33054)));
+ mediaTypes.insert(std::make_pair(MediaTypeEpisode, CMediaTypes::MediaTypeInfo(MediaTypeEpisode, MediaTypeEpisode "s", false, 36906, 36907, 20359, 20360)));
+ // clang-format on
+
+ return mediaTypes;
+}
+
+std::map<std::string, CMediaTypes::MediaTypeInfo> CMediaTypes::m_mediaTypes = fillDefaultMediaTypes();
+
+bool CMediaTypes::IsValidMediaType(const MediaType &mediaType)
+{
+ return findMediaType(mediaType) != m_mediaTypes.end();
+}
+
+bool CMediaTypes::IsMediaType(const std::string &strMediaType, const MediaType &mediaType)
+{
+ std::map<std::string, MediaTypeInfo>::const_iterator strMediaTypeIt = findMediaType(strMediaType);
+ std::map<std::string, MediaTypeInfo>::const_iterator mediaTypeIt = findMediaType(mediaType);
+
+ return strMediaTypeIt != m_mediaTypes.end() && mediaTypeIt != m_mediaTypes.end() &&
+ strMediaTypeIt->first.compare(mediaTypeIt->first) == 0;
+}
+
+MediaType CMediaTypes::FromString(const std::string &strMediaType)
+{
+ std::map<std::string, MediaTypeInfo>::const_iterator mediaTypeIt = findMediaType(strMediaType);
+ if (mediaTypeIt == m_mediaTypes.end())
+ return MediaTypeNone;
+
+ return mediaTypeIt->first;
+}
+
+MediaType CMediaTypes::ToPlural(const MediaType &mediaType)
+{
+ std::map<std::string, MediaTypeInfo>::const_iterator mediaTypeIt = findMediaType(mediaType);
+ if (mediaTypeIt == m_mediaTypes.end())
+ return MediaTypeNone;
+
+ return mediaTypeIt->second.plural;
+}
+
+bool CMediaTypes::IsContainer(const MediaType &mediaType)
+{
+ std::map<std::string, MediaTypeInfo>::const_iterator mediaTypeIt = findMediaType(mediaType);
+ if (mediaTypeIt == m_mediaTypes.end())
+ return false;
+
+ return mediaTypeIt->second.container;
+}
+
+std::map<std::string, CMediaTypes::MediaTypeInfo>::const_iterator CMediaTypes::findMediaType(const std::string &mediaType)
+{
+ std::string strMediaType = mediaType;
+ StringUtils::ToLower(strMediaType);
+
+ std::map<std::string, MediaTypeInfo>::const_iterator it = m_mediaTypes.find(strMediaType);
+ if (it != m_mediaTypes.end())
+ return it;
+
+ for (it = m_mediaTypes.begin(); it != m_mediaTypes.end(); ++it)
+ {
+ if (strMediaType.compare(it->second.plural) == 0)
+ return it;
+ }
+
+ return m_mediaTypes.end();
+}
+
+std::string CMediaTypes::GetLocalization(const MediaType &mediaType)
+{
+ std::map<std::string, MediaTypeInfo>::const_iterator mediaTypeIt = findMediaType(mediaType);
+ if (mediaTypeIt == m_mediaTypes.end() ||
+ mediaTypeIt->second.localizationSingular <= 0)
+ return "";
+
+ return g_localizeStrings.Get(mediaTypeIt->second.localizationSingular);
+}
+
+std::string CMediaTypes::GetPluralLocalization(const MediaType &mediaType)
+{
+ std::map<std::string, MediaTypeInfo>::const_iterator mediaTypeIt = findMediaType(mediaType);
+ if (mediaTypeIt == m_mediaTypes.end() ||
+ mediaTypeIt->second.localizationPlural <= 0)
+ return "";
+
+ return g_localizeStrings.Get(mediaTypeIt->second.localizationPlural);
+}
+
+std::string CMediaTypes::GetCapitalLocalization(const MediaType &mediaType)
+{
+ std::map<std::string, MediaTypeInfo>::const_iterator mediaTypeIt = findMediaType(mediaType);
+ if (mediaTypeIt == m_mediaTypes.end() ||
+ mediaTypeIt->second.localizationSingular <= 0)
+ return "";
+
+ return g_localizeStrings.Get(mediaTypeIt->second.localizationSingularCapital);
+}
+
+std::string CMediaTypes::GetCapitalPluralLocalization(const MediaType &mediaType)
+{
+ std::map<std::string, MediaTypeInfo>::const_iterator mediaTypeIt = findMediaType(mediaType);
+ if (mediaTypeIt == m_mediaTypes.end() ||
+ mediaTypeIt->second.localizationPlural <= 0)
+ return "";
+
+ return g_localizeStrings.Get(mediaTypeIt->second.localizationPluralCapital);
+}
diff --git a/xbmc/media/MediaType.h b/xbmc/media/MediaType.h
new file mode 100644
index 0000000..a237720
--- /dev/null
+++ b/xbmc/media/MediaType.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2013-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 <map>
+#include <string>
+
+using MediaType = std::string;
+
+#define MediaTypeNone ""
+#define MediaTypeMusic "music"
+#define MediaTypeArtist "artist"
+#define MediaTypeAlbum "album"
+#define MediaTypeSong "song"
+#define MediaTypeVideo "video"
+#define MediaTypeVideoCollection "set"
+#define MediaTypeMusicVideo "musicvideo"
+#define MediaTypeMovie "movie"
+#define MediaTypeTvShow "tvshow"
+#define MediaTypeSeason "season"
+#define MediaTypeEpisode "episode"
+
+class CMediaTypes
+{
+public:
+ static bool IsValidMediaType(const MediaType &mediaType);
+ static bool IsMediaType(const std::string &strMediaType, const MediaType &mediaType);
+ static MediaType FromString(const std::string &strMediaType);
+ static MediaType ToPlural(const MediaType &mediaType);
+
+ static bool IsContainer(const MediaType &mediaType);
+
+ static std::string GetLocalization(const MediaType &mediaType);
+ static std::string GetPluralLocalization(const MediaType &mediaType);
+ static std::string GetCapitalLocalization(const MediaType &mediaType);
+ static std::string GetCapitalPluralLocalization(const MediaType &mediaType);
+
+ struct MediaTypeInfo
+ {
+ MediaTypeInfo(const MediaType &mediaType, const std::string &plural, bool container,
+ int localizationSingular, int localizationPlural,
+ int localizationSingularCapital, int localizationPluralCapital)
+ : mediaType(mediaType),
+ plural(plural),
+ container(container),
+ localizationSingular(localizationSingular),
+ localizationPlural(localizationPlural),
+ localizationSingularCapital(localizationSingularCapital),
+ localizationPluralCapital(localizationPluralCapital)
+ { }
+
+ MediaType mediaType;
+ std::string plural;
+ bool container;
+ int localizationSingular;
+ int localizationPlural;
+ int localizationSingularCapital;
+ int localizationPluralCapital;
+ };
+
+private:
+ static std::map<std::string, MediaTypeInfo>::const_iterator findMediaType(const std::string &mediaType);
+
+ static std::map<std::string, MediaTypeInfo> m_mediaTypes;
+};
diff --git a/xbmc/media/decoderfilter/CMakeLists.txt b/xbmc/media/decoderfilter/CMakeLists.txt
new file mode 100644
index 0000000..48febb7
--- /dev/null
+++ b/xbmc/media/decoderfilter/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES DecoderFilterManager.cpp)
+
+set(HEADERS DecoderFilterManager.h)
+
+core_add_library(decoderfilter)
diff --git a/xbmc/media/decoderfilter/DecoderFilterManager.cpp b/xbmc/media/decoderfilter/DecoderFilterManager.cpp
new file mode 100644
index 0000000..0917768
--- /dev/null
+++ b/xbmc/media/decoderfilter/DecoderFilterManager.cpp
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2013-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.
+ */
+
+
+/**
+ * \file media\hwdecoder\DecoderFilterManager.cpp
+ * \brief Implements CDecoderFilterManager class.
+ *
+ */
+
+#include "DecoderFilterManager.h"
+
+#include "Util.h"
+#include "cores/VideoPlayer/DVDStreamInfo.h"
+#include "filesystem/File.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+static const char* TAG_ROOT = "decoderfilter";
+static const char* TAG_FILTER = "filter";
+static const char* TAG_NAME = "name";
+static const char* TAG_GENERAL = "allowed";
+static const char* TAG_STILLS = "stills-allowed";
+static const char* TAG_DVD = "dvd-allowed";
+static const char* TAG_MINHEIGHT = "min-height";
+static const char* HWDFFileName = "special://masterprofile/decoderfilter.xml";
+static const char* CLASSNAME = "CDecoderFilter";
+
+CDecoderFilter::CDecoderFilter(const std::string& name, uint32_t flags, int minHeight)
+ : m_name(name)
+ , m_flags(flags)
+ , m_minHeight(minHeight)
+{
+}
+
+bool CDecoderFilter::isValid(const CDVDStreamInfo& streamInfo) const
+{
+ uint32_t flags = FLAG_GENERAL_ALLOWED;
+
+ if (streamInfo.stills)
+ flags |= FLAG_STILLS_ALLOWED;
+
+ if (streamInfo.dvd)
+ flags |= FLAG_DVD_ALLOWED;
+
+ if ((flags & m_flags) != flags)
+ return false;
+
+ // remove codec pitch for comparison
+ if (m_minHeight && (streamInfo.height & ~31) <= m_minHeight)
+ return false;
+
+ return true;
+}
+
+bool CDecoderFilter::Load(const TiXmlNode *node)
+{
+ bool flagBool = false;
+
+ XMLUtils::GetString(node, TAG_NAME, m_name);
+ XMLUtils::GetBoolean(node, TAG_GENERAL, flagBool);
+ if (flagBool)
+ m_flags |= FLAG_GENERAL_ALLOWED;
+ flagBool = false;
+
+ XMLUtils::GetBoolean(node, TAG_STILLS, flagBool);
+ if (flagBool)
+ m_flags |= FLAG_STILLS_ALLOWED;
+ flagBool = false;
+
+ XMLUtils::GetBoolean(node, TAG_DVD, flagBool);
+ if (flagBool)
+ m_flags |= FLAG_DVD_ALLOWED;
+
+ XMLUtils::GetInt(node, TAG_MINHEIGHT, m_minHeight);
+
+ return true;
+}
+
+bool CDecoderFilter::Save(TiXmlNode *node) const
+{
+ // Now write each of the pieces of information we need...
+ XMLUtils::SetString(node, TAG_NAME, m_name);
+ XMLUtils::SetBoolean(node, TAG_GENERAL, (m_flags & FLAG_GENERAL_ALLOWED) != 0);
+ XMLUtils::SetBoolean(node, TAG_STILLS, (m_flags & FLAG_STILLS_ALLOWED) != 0);
+ XMLUtils::SetBoolean(node, TAG_DVD, (m_flags & FLAG_DVD_ALLOWED) != 0);
+ XMLUtils::SetInt(node, TAG_MINHEIGHT, m_minHeight);
+ return true;
+}
+
+
+/****************************************/
+
+bool CDecoderFilterManager::isValid(const std::string& name, const CDVDStreamInfo& streamInfo)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ std::set<CDecoderFilter>::const_iterator filter(m_filters.find(name));
+ return filter != m_filters.end() ? filter->isValid(streamInfo) : m_filters.empty();
+}
+
+void CDecoderFilterManager::add(const CDecoderFilter& filter)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ std::pair<std::set<CDecoderFilter>::iterator, bool> res = m_filters.insert(filter);
+ m_dirty = m_dirty || res.second;
+}
+
+bool CDecoderFilterManager::Load()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ m_filters.clear();
+
+ std::string fileName = CUtil::TranslateSpecialSource(HWDFFileName);
+ if (!XFILE::CFile::Exists(fileName))
+ return true;
+
+ CLog::Log(LOGINFO, "{}: loading filters from {}", CLASSNAME, fileName);
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(fileName))
+ {
+ CLog::Log(LOGERROR, "{}: error loading: line {}, {}", CLASSNAME, xmlDoc.ErrorRow(),
+ xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ const TiXmlElement *pRootElement = xmlDoc.RootElement();
+ if (!pRootElement || !StringUtils::EqualsNoCase(pRootElement->ValueStr(), TAG_ROOT))
+ {
+ CLog::Log(LOGERROR, "{}: invalid root element ({})", CLASSNAME, pRootElement->ValueStr());
+ return false;
+ }
+
+ const TiXmlElement *pFilter = pRootElement->FirstChildElement(TAG_FILTER);
+ while (pFilter)
+ {
+ CDecoderFilter filter("");
+ if (filter.Load(pFilter))
+ m_filters.insert(filter);
+ pFilter = pFilter->NextSiblingElement(TAG_FILTER);
+ }
+ return true;
+}
+
+bool CDecoderFilterManager::Save() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (!m_dirty || m_filters.empty())
+ return true;
+
+ CXBMCTinyXML doc;
+ TiXmlElement xmlRootElement(TAG_ROOT);
+ TiXmlNode *pRoot = doc.InsertEndChild(xmlRootElement);
+ if (pRoot == NULL)
+ return false;
+
+ for (const CDecoderFilter& filter : m_filters)
+ {
+ // Write the resolution tag
+ TiXmlElement filterElem(TAG_FILTER);
+ TiXmlNode *pNode = pRoot->InsertEndChild(filterElem);
+ if (pNode == NULL)
+ return false;
+
+ filter.Save(pNode);
+ }
+ std::string fileName = CUtil::TranslateSpecialSource(HWDFFileName);
+ return doc.SaveFile(fileName);
+}
diff --git a/xbmc/media/decoderfilter/DecoderFilterManager.h b/xbmc/media/decoderfilter/DecoderFilterManager.h
new file mode 100644
index 0000000..146538b
--- /dev/null
+++ b/xbmc/media/decoderfilter/DecoderFilterManager.h
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2013-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
+
+/**
+ * \file platform\DecoderFilter.h
+ * \brief Declares CDecoderFilterManager which gives control about how / when to use platform decoder.
+ *
+ */
+#include "threads/CriticalSection.h"
+
+#include <cinttypes>
+#include <set>
+#include <string>
+
+class TiXmlNode;
+class CDVDStreamInfo;
+
+/**
+* @class CDecoderFilter
+*
+* @brief Declaration of CDecoderFilter.
+*
+*/
+class CDecoderFilter
+{
+public:
+ /**
+ * @brief Flags to control decoder validity.
+ */
+ enum : uint32_t
+ {
+ FLAG_GENERAL_ALLOWED = 1, ///< early false exit if set
+ FLAG_STILLS_ALLOWED = 2, ///< early false exit if set and stream is marked as "has stillframes"
+ FLAG_DVD_ALLOWED = 4 ///< early false exit if set and stream is marked as dvd
+ };
+
+ /**
+ * \fn CDecoderFilter::CDecoderFilter(const std::string& name);
+ * \brief constructs a CDecoderFilter
+ * \param name decodername
+ * \return nothing.
+ */
+ CDecoderFilter(const std::string& name) : m_name(name) {}
+
+ /**
+ * \fn CDecoderFilter::CDecoderFilter(const std::string& name, uint32_t flags, uint32_t maxWidth, uint32_t maxHeight);
+ * \brief constructs a CDecoderFilter
+ * \param name decodername
+ * \param flags collection of FLAG_ values, bitwise OR
+ * \param minHeight minimum height of stream allowed by this decoder
+ * \return nothing.
+ */
+ CDecoderFilter(const std::string& name, uint32_t flags, int minHeight);
+
+ virtual ~CDecoderFilter() = default;
+
+ /**
+ * \fn CDecoderFilter::operator < (const CDecoderFilter& other);
+ * \brief used for sorting / replacing / find
+ */
+ bool operator<(const CDecoderFilter& other) const { return m_name < other.m_name; }
+
+ /**
+ * \fn CDecoderFilter::isValid(const CDVDStreamInfo& streamInfo);
+ * \brief test if stream is allowed by filter.
+ * \return true if valid, false otherwise
+ */
+ virtual bool isValid(const CDVDStreamInfo& streamInfo) const;
+
+ /**
+ * \fn CDecoderFilter::Load(const TiXmlNode *settings);
+ * \brief load all members from XML node
+ * \param node filter node from where to get the values
+ * \return true if operation was successful, false on error
+ */
+ virtual bool Load(const TiXmlNode *node);
+
+ /**
+ * \fn CDecoderFilter::Save(TiXmlNode *settings);
+ * \brief store all members in XML node
+ * \param node a ready to use filter setting node
+ * \return true if operation was successful, false on error
+ */
+ virtual bool Save(TiXmlNode *node) const;
+
+
+private:
+ std::string m_name;
+
+ uint32_t m_flags = 0;
+ int m_minHeight = 0;
+};
+
+
+/**
+* @class CDecoderFilterManager
+*
+* @brief Class which handles multiple CDecoderFilter elements.
+*
+*/
+
+class CDecoderFilterManager
+{
+public:
+ CDecoderFilterManager() { Load(); }
+ virtual ~CDecoderFilterManager() { Save(); }
+
+ /**
+ * \fn bool CDecoderFilterManager::add(const CDecoderFilter& filter);
+ * \brief adds an CDecoderFilter if key [filter.name] is not yet existing.
+ * \param filter the decoder filter to add / replace.
+ * \return nothing.
+ */
+ void add(const CDecoderFilter& filter);
+
+
+ /**
+ * \fn bool CDecoderFilterManager::validate(const std::string& name, const CDVDStreamInfo& streamInfo);
+ * \brief Validates if decoder with name [name] is allowed to be used.
+ * \param streamInfo Stream information used to validate().
+ * \return true if HardwarDecoder could be used, false otherwise.
+ */
+ bool isValid(const std::string& name, const CDVDStreamInfo& streamInfo);
+
+protected:
+ bool Load();
+ bool Save() const;
+
+private:
+ bool m_dirty = false;
+ std::set<CDecoderFilter> m_filters;
+ mutable CCriticalSection m_critical;
+};
diff --git a/xbmc/media/drm/CMakeLists.txt b/xbmc/media/drm/CMakeLists.txt
new file mode 100644
index 0000000..15b2a84
--- /dev/null
+++ b/xbmc/media/drm/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES CryptoSession.cpp)
+
+set(HEADERS CryptoSession.h)
+
+core_add_library(drm)
diff --git a/xbmc/media/drm/CryptoSession.cpp b/xbmc/media/drm/CryptoSession.cpp
new file mode 100644
index 0000000..9e5e888
--- /dev/null
+++ b/xbmc/media/drm/CryptoSession.cpp
@@ -0,0 +1,27 @@
+/*
+ * 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 "CryptoSession.h"
+
+using namespace DRM;
+
+std::vector<GET_CRYPTO_SESSION_INTERFACE_FN> CCryptoSession::s_registeredInterfaces;
+
+void CCryptoSession::RegisterInterface(GET_CRYPTO_SESSION_INTERFACE_FN fn)
+{
+ s_registeredInterfaces.push_back(fn);
+}
+
+CCryptoSession* CCryptoSession::GetCryptoSession(const std::string& UUID, const std::string& cipherAlgo, const std::string& macAlgo)
+{
+ CCryptoSession* retVal = nullptr;
+ for (auto fn : s_registeredInterfaces)
+ if ((retVal = fn(UUID, cipherAlgo, macAlgo)))
+ break;
+ return retVal;
+}
diff --git a/xbmc/media/drm/CryptoSession.h b/xbmc/media/drm/CryptoSession.h
new file mode 100644
index 0000000..fdfc00b
--- /dev/null
+++ b/xbmc/media/drm/CryptoSession.h
@@ -0,0 +1,51 @@
+/*
+ * 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 "commons/Buffer.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+namespace DRM
+{
+ class CCryptoSession;
+
+ typedef CCryptoSession* (*GET_CRYPTO_SESSION_INTERFACE_FN)(const std::string& UUID, const std::string& cipherAlgo, const std::string& hmacAlgo);
+
+ class CCryptoSession
+ {
+ public:
+ // Interface registration
+ static CCryptoSession* GetCryptoSession(const std::string& UUID, const std::string& cipherAlgo, const std::string& macAlgo);
+ virtual ~CCryptoSession() = default;
+
+ // Interface methods
+ virtual XbmcCommons::Buffer GetKeyRequest(const XbmcCommons::Buffer& init, const std::string& mimeType, bool offlineKey, const std::map<std::string, std::string>& parameters) = 0;
+ virtual std::string GetPropertyString(const std::string& name) = 0;
+ virtual std::string ProvideKeyResponse(const XbmcCommons::Buffer& response) = 0;
+ virtual void RemoveKeys() = 0;
+ virtual void RestoreKeys(const std::string& keySetId) = 0;
+ virtual void SetPropertyString(const std::string& name, const std::string& value) = 0;
+
+ // Crypto methods
+ virtual XbmcCommons::Buffer Decrypt(const XbmcCommons::Buffer& cipherKeyId, const XbmcCommons::Buffer& input, const XbmcCommons::Buffer& iv) = 0;
+ virtual XbmcCommons::Buffer Encrypt(const XbmcCommons::Buffer& cipherKeyId, const XbmcCommons::Buffer& input, const XbmcCommons::Buffer& iv) = 0;
+ virtual XbmcCommons::Buffer Sign(const XbmcCommons::Buffer& macKeyId, const XbmcCommons::Buffer& message) = 0;
+ virtual bool Verify(const XbmcCommons::Buffer& macKeyId, const XbmcCommons::Buffer& message, const XbmcCommons::Buffer& signature ) = 0;
+
+ protected:
+ static void RegisterInterface(GET_CRYPTO_SESSION_INTERFACE_FN fn);
+
+ private:
+ static std::vector<GET_CRYPTO_SESSION_INTERFACE_FN> s_registeredInterfaces;
+ };
+
+} //namespace
diff --git a/xbmc/messaging/ApplicationMessenger.cpp b/xbmc/messaging/ApplicationMessenger.cpp
new file mode 100644
index 0000000..dfacac9
--- /dev/null
+++ b/xbmc/messaging/ApplicationMessenger.cpp
@@ -0,0 +1,293 @@
+/*
+ * 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 "ApplicationMessenger.h"
+
+#include "guilib/GUIMessage.h"
+#include "messaging/IMessageTarget.h"
+#include "threads/SingleLock.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <memory>
+#include <mutex>
+#include <utility>
+
+namespace KODI
+{
+namespace MESSAGING
+{
+
+class CDelayedMessage : public CThread
+{
+ public:
+ CDelayedMessage(const ThreadMessage& msg, unsigned int delay);
+ void Process() override;
+
+ private:
+ unsigned int m_delay;
+ ThreadMessage m_msg;
+};
+
+CDelayedMessage::CDelayedMessage(const ThreadMessage& msg, unsigned int delay)
+ : CThread("DelayedMessage"), m_msg(msg)
+{
+ m_delay = delay;
+}
+
+void CDelayedMessage::Process()
+{
+ CThread::Sleep(std::chrono::milliseconds(m_delay));
+
+ if (!m_bStop)
+ CServiceBroker::GetAppMessenger()->PostMsg(m_msg.dwMessage, m_msg.param1, m_msg.param1,
+ m_msg.lpVoid, m_msg.strParam, m_msg.params);
+}
+
+CApplicationMessenger::CApplicationMessenger() = default;
+
+CApplicationMessenger::~CApplicationMessenger()
+{
+ Cleanup();
+}
+
+void CApplicationMessenger::Cleanup()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ while (!m_vecMessages.empty())
+ {
+ ThreadMessage* pMsg = m_vecMessages.front();
+
+ if (pMsg->waitEvent)
+ pMsg->waitEvent->Set();
+
+ delete pMsg;
+ m_vecMessages.pop();
+ }
+
+ while (!m_vecWindowMessages.empty())
+ {
+ ThreadMessage* pMsg = m_vecWindowMessages.front();
+
+ if (pMsg->waitEvent)
+ pMsg->waitEvent->Set();
+
+ delete pMsg;
+ m_vecWindowMessages.pop();
+ }
+}
+
+int CApplicationMessenger::SendMsg(ThreadMessage&& message, bool wait)
+{
+ std::shared_ptr<CEvent> waitEvent;
+ std::shared_ptr<int> result;
+
+ if (wait)
+ {
+ //Initialize result here as it's not needed for posted messages
+ message.result = std::make_shared<int>(-1);
+ // check that we're not being called from our application thread, else we'll be waiting
+ // forever!
+ if (m_guiThreadId != CThread::GetCurrentThreadId())
+ {
+ message.waitEvent.reset(new CEvent(true));
+ waitEvent = message.waitEvent;
+ result = message.result;
+ }
+ else
+ {
+ //OutputDebugString("Attempting to wait on a SendMessage() from our application thread will cause lockup!\n");
+ //OutputDebugString("Sending immediately\n");
+ ProcessMessage(&message);
+ return *message.result;
+ }
+ }
+
+
+ if (m_bStop)
+ return -1;
+
+ ThreadMessage* msg = new ThreadMessage(std::move(message));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (msg->dwMessage == TMSG_GUI_MESSAGE)
+ m_vecWindowMessages.push(msg);
+ else
+ m_vecMessages.push(msg);
+ lock.unlock(); // this releases the lock on the vec of messages and
+ // allows the ProcessMessage to execute and therefore
+ // delete the message itself. Therefore any access
+ // of the message itself after this point constitutes
+ // a race condition (yarc - "yet another race condition")
+ //
+ if (waitEvent) // ... it just so happens we have a spare reference to the
+ // waitEvent ... just for such contingencies :)
+ {
+ // ensure the thread doesn't hold the graphics lock
+ CWinSystemBase* winSystem = CServiceBroker::GetWinSystem();
+ //! @todo This won't really help as winSystem can die every single
+ // moment on shutdown. A shared ptr would be a more valid solution
+ // depending on the design dependencies.
+ if (winSystem)
+ {
+ CSingleExit exit(winSystem->GetGfxContext());
+ waitEvent->Wait();
+ }
+ return *result;
+ }
+
+ return -1;
+}
+
+int CApplicationMessenger::SendMsg(uint32_t messageId)
+{
+ return SendMsg(ThreadMessage{ messageId }, true);
+}
+
+int CApplicationMessenger::SendMsg(uint32_t messageId, int param1, int param2, void* payload)
+{
+ return SendMsg(ThreadMessage{ messageId, param1, param2, payload }, true);
+}
+
+int CApplicationMessenger::SendMsg(uint32_t messageId, int param1, int param2, void* payload, std::string strParam)
+{
+ return SendMsg(ThreadMessage{messageId, param1, param2, payload, std::move(strParam),
+ std::vector<std::string>{}},
+ true);
+}
+
+int CApplicationMessenger::SendMsg(uint32_t messageId, int param1, int param2, void* payload, std::string strParam, std::vector<std::string> params)
+{
+ return SendMsg(
+ ThreadMessage{messageId, param1, param2, payload, std::move(strParam), std::move(params)},
+ true);
+}
+
+void CApplicationMessenger::PostMsg(uint32_t messageId)
+{
+ SendMsg(ThreadMessage{ messageId }, false);
+}
+
+void CApplicationMessenger::PostMsg(uint32_t messageId, int64_t param3)
+{
+ SendMsg(ThreadMessage{ messageId, param3 }, false);
+}
+
+void CApplicationMessenger::PostMsg(uint32_t messageId, int param1, int param2, void* payload)
+{
+ SendMsg(ThreadMessage{ messageId, param1, param2, payload }, false);
+}
+
+void CApplicationMessenger::PostMsg(uint32_t messageId, int param1, int param2, void* payload, std::string strParam)
+{
+ SendMsg(ThreadMessage{messageId, param1, param2, payload, std::move(strParam),
+ std::vector<std::string>{}},
+ false);
+}
+
+void CApplicationMessenger::PostMsg(uint32_t messageId, int param1, int param2, void* payload, std::string strParam, std::vector<std::string> params)
+{
+ SendMsg(ThreadMessage{messageId, param1, param2, payload, std::move(strParam), std::move(params)},
+ false);
+}
+
+void CApplicationMessenger::ProcessMessages()
+{
+ // process threadmessages
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ while (!m_vecMessages.empty())
+ {
+ ThreadMessage* pMsg = m_vecMessages.front();
+ //first remove the message from the queue, else the message could be processed more then once
+ m_vecMessages.pop();
+
+ //Leave here as the message might make another
+ //thread call processmessages or sendmessage
+
+ std::shared_ptr<CEvent> waitEvent = pMsg->waitEvent;
+ lock.unlock(); // <- see the large comment in SendMessage ^
+
+ ProcessMessage(pMsg);
+
+ if (waitEvent)
+ waitEvent->Set();
+ delete pMsg;
+
+ lock.lock();
+ }
+}
+
+void CApplicationMessenger::ProcessMessage(ThreadMessage *pMsg)
+{
+ //special case for this that we handle ourselves
+ if (pMsg->dwMessage == TMSG_CALLBACK)
+ {
+ ThreadMessageCallback *callback = static_cast<ThreadMessageCallback*>(pMsg->lpVoid);
+ callback->callback(callback->userptr);
+ return;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ int mask = pMsg->dwMessage & TMSG_MASK_MESSAGE;
+
+ const auto it = m_mapTargets.find(mask);
+ if (it != m_mapTargets.end())
+ {
+ CSingleExit exit(m_critSection);
+ it->second->OnApplicationMessage(pMsg);
+ }
+ else
+ CLog::LogF(LOGERROR, "receiver {} is not defined", mask);
+}
+
+void CApplicationMessenger::ProcessWindowMessages()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ //message type is window, process window messages
+ while (!m_vecWindowMessages.empty())
+ {
+ ThreadMessage* pMsg = m_vecWindowMessages.front();
+ //first remove the message from the queue, else the message could be processed more then once
+ m_vecWindowMessages.pop();
+
+ // leave here in case we make more thread messages from this one
+
+ std::shared_ptr<CEvent> waitEvent = pMsg->waitEvent;
+ lock.unlock(); // <- see the large comment in SendMessage ^
+
+ ProcessMessage(pMsg);
+ if (waitEvent)
+ waitEvent->Set();
+ delete pMsg;
+
+ lock.lock();
+ }
+}
+
+void CApplicationMessenger::SendGUIMessage(const CGUIMessage &message, int windowID, bool waitResult)
+{
+ ThreadMessage tMsg(TMSG_GUI_MESSAGE);
+ tMsg.param1 = windowID == WINDOW_INVALID ? 0 : windowID;
+ tMsg.lpVoid = new CGUIMessage(message);
+ SendMsg(std::move(tMsg), waitResult);
+}
+
+void CApplicationMessenger::RegisterReceiver(IMessageTarget* target)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_mapTargets.insert(std::make_pair(target->GetMessageMask(), target));
+}
+
+bool CApplicationMessenger::IsProcessThread() const
+{
+ return m_processThreadId == CThread::GetCurrentThreadId();
+}
+}
+}
diff --git a/xbmc/messaging/ApplicationMessenger.h b/xbmc/messaging/ApplicationMessenger.h
new file mode 100644
index 0000000..8f15ef9
--- /dev/null
+++ b/xbmc/messaging/ApplicationMessenger.h
@@ -0,0 +1,438 @@
+/*
+ * 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 "guilib/WindowIDs.h"
+#include "messaging/ThreadMessage.h"
+#include "threads/Thread.h"
+
+#include <map>
+#include <memory>
+#include <queue>
+#include <string>
+#include <vector>
+
+#define TMSG_MASK_MESSAGE 0xFFFF0000 // only keep the high bits to route messages
+#define TMSG_MASK_APPLICATION (1<<30) //Don't use bit 31 as it'll fail to build, using unsigned variable to hold the message.
+#define TMSG_MASK_PLAYLISTPLAYER (1<<29)
+#define TMSG_MASK_GUIINFOMANAGER (1<<28)
+#define TMSG_MASK_WINDOWMANAGER (1<<27)
+#define TMSG_MASK_PERIPHERALS (1<<26)
+
+// defines here
+#define TMSG_PLAYLISTPLAYER_PLAY TMSG_MASK_PLAYLISTPLAYER + 0
+#define TMSG_PLAYLISTPLAYER_NEXT TMSG_MASK_PLAYLISTPLAYER + 1
+#define TMSG_PLAYLISTPLAYER_PREV TMSG_MASK_PLAYLISTPLAYER + 2
+#define TMSG_PLAYLISTPLAYER_ADD TMSG_MASK_PLAYLISTPLAYER + 3
+#define TMSG_PLAYLISTPLAYER_CLEAR TMSG_MASK_PLAYLISTPLAYER + 4
+#define TMSG_PLAYLISTPLAYER_SHUFFLE TMSG_MASK_PLAYLISTPLAYER + 5
+#define TMSG_PLAYLISTPLAYER_GET_ITEMS TMSG_MASK_PLAYLISTPLAYER + 6
+#define TMSG_PLAYLISTPLAYER_PLAY_SONG_ID TMSG_MASK_PLAYLISTPLAYER + 7
+#define TMSG_PLAYLISTPLAYER_INSERT TMSG_MASK_PLAYLISTPLAYER + 8
+#define TMSG_PLAYLISTPLAYER_REMOVE TMSG_MASK_PLAYLISTPLAYER + 9
+#define TMSG_PLAYLISTPLAYER_SWAP TMSG_MASK_PLAYLISTPLAYER + 10
+#define TMSG_PLAYLISTPLAYER_REPEAT TMSG_MASK_PLAYLISTPLAYER + 11
+#define TMSG_MEDIA_PLAY TMSG_MASK_PLAYLISTPLAYER + 12
+#define TMSG_MEDIA_STOP TMSG_MASK_PLAYLISTPLAYER + 13
+// the PAUSE is indeed a PLAYPAUSE
+#define TMSG_MEDIA_PAUSE TMSG_MASK_PLAYLISTPLAYER + 14
+#define TMSG_MEDIA_RESTART TMSG_MASK_PLAYLISTPLAYER + 15
+#define TMSG_MEDIA_UNPAUSE TMSG_MASK_PLAYLISTPLAYER + 16
+#define TMSG_MEDIA_PAUSE_IF_PLAYING TMSG_MASK_PLAYLISTPLAYER + 17
+#define TMSG_MEDIA_SEEK_TIME TMSG_MASK_PLAYLISTPLAYER + 18
+
+#define TMSG_SHUTDOWN TMSG_MASK_APPLICATION + 0
+#define TMSG_POWERDOWN TMSG_MASK_APPLICATION + 1
+#define TMSG_QUIT TMSG_MASK_APPLICATION + 2
+#define TMSG_HIBERNATE TMSG_MASK_APPLICATION + 3
+#define TMSG_SUSPEND TMSG_MASK_APPLICATION + 4
+#define TMSG_RESTART TMSG_MASK_APPLICATION + 5
+#define TMSG_RESET TMSG_MASK_APPLICATION + 6
+#define TMSG_RESTARTAPP TMSG_MASK_APPLICATION + 7
+#define TMSG_ACTIVATESCREENSAVER TMSG_MASK_APPLICATION + 8
+#define TMSG_NETWORKMESSAGE TMSG_MASK_APPLICATION + 9
+#define TMSG_RESETSCREENSAVER TMSG_MASK_APPLICATION + 10
+#define TMSG_VOLUME_SHOW TMSG_MASK_APPLICATION + 11
+#define TMSG_DISPLAY_SETUP TMSG_MASK_APPLICATION + 12
+#define TMSG_DISPLAY_DESTROY TMSG_MASK_APPLICATION + 13
+#define TMSG_SETVIDEORESOLUTION TMSG_MASK_APPLICATION + 14
+#define TMSG_SWITCHTOFULLSCREEN TMSG_MASK_APPLICATION + 15
+#define TMSG_MINIMIZE TMSG_MASK_APPLICATION + 16
+#define TMSG_TOGGLEFULLSCREEN TMSG_MASK_APPLICATION + 17
+#define TMSG_SETLANGUAGE TMSG_MASK_APPLICATION + 18
+#define TMSG_RENDERER_FLUSH TMSG_MASK_APPLICATION + 19
+#define TMSG_INHIBITIDLESHUTDOWN TMSG_MASK_APPLICATION + 20
+#define TMSG_START_ANDROID_ACTIVITY TMSG_MASK_APPLICATION + 21
+#define TMSG_EXECUTE_SCRIPT TMSG_MASK_APPLICATION + 22
+#define TMSG_EXECUTE_BUILT_IN TMSG_MASK_APPLICATION + 23
+#define TMSG_EXECUTE_OS TMSG_MASK_APPLICATION + 24
+#define TMSG_PICTURE_SHOW TMSG_MASK_APPLICATION + 25
+#define TMSG_PICTURE_SLIDESHOW TMSG_MASK_APPLICATION + 26
+#define TMSG_LOADPROFILE TMSG_MASK_APPLICATION + 27
+#define TMSG_VIDEORESIZE TMSG_MASK_APPLICATION + 28
+#define TMSG_INHIBITSCREENSAVER TMSG_MASK_APPLICATION + 29
+
+#define TMSG_SYSTEM_POWERDOWN TMSG_MASK_APPLICATION + 30
+#define TMSG_RENDERER_PREINIT TMSG_MASK_APPLICATION + 31
+#define TMSG_RENDERER_UNINIT TMSG_MASK_APPLICATION + 32
+#define TMSG_EVENT TMSG_MASK_APPLICATION + 33
+
+/// @brief Called from the player when its current item is updated
+#define TMSG_UPDATE_PLAYER_ITEM TMSG_MASK_APPLICATION + 35
+
+#define TMSG_GUI_INFOLABEL TMSG_MASK_GUIINFOMANAGER + 0
+#define TMSG_GUI_INFOBOOL TMSG_MASK_GUIINFOMANAGER + 1
+#define TMSG_UPDATE_CURRENT_ITEM TMSG_MASK_GUIINFOMANAGER + 2
+
+#define TMSG_CECTOGGLESTATE TMSG_MASK_PERIPHERALS + 1
+#define TMSG_CECACTIVATESOURCE TMSG_MASK_PERIPHERALS + 2
+#define TMSG_CECSTANDBY TMSG_MASK_PERIPHERALS + 3
+
+#define TMSG_GUI_DIALOG_OPEN TMSG_MASK_WINDOWMANAGER + 1
+#define TMSG_GUI_ACTIVATE_WINDOW TMSG_MASK_WINDOWMANAGER + 2
+#define TMSG_GUI_PYTHON_DIALOG TMSG_MASK_WINDOWMANAGER + 3
+#define TMSG_GUI_WINDOW_CLOSE TMSG_MASK_WINDOWMANAGER + 4
+#define TMSG_GUI_ACTION TMSG_MASK_WINDOWMANAGER + 5
+#define TMSG_GUI_ADDON_DIALOG TMSG_MASK_WINDOWMANAGER + 6
+#define TMSG_GUI_MESSAGE TMSG_MASK_WINDOWMANAGER + 7
+
+/*!
+ \def TMSG_GUI_DIALOG_YESNO
+ \brief Message sent through CApplicationMessenger to open a yes/no dialog box
+
+ There's two ways to send this message, a short and concise way and a more
+ flexible way allowing more customization.
+
+ Option 1:
+ CApplicationMessenger::Get().SendMsg(TMSG_GUI_DIALOG_YESNO, 123, 456);
+ 123: This is the string id for the heading
+ 456: This is the string id for the text
+
+ Option 2:
+ \a HELPERS::DialogYesNoMessage options.
+ Fill in options
+ CApplicationMessenger::Get().SendMsg(TMSG_GUI_DIALOG_YESNO, -1, -1, static_cast<void*>(&options));
+
+ \returns -1 for cancelled, 0 for No and 1 for Yes
+ \sa HELPERS::DialogYesNoMessage
+*/
+#define TMSG_GUI_DIALOG_YESNO TMSG_MASK_WINDOWMANAGER + 8
+#define TMSG_GUI_DIALOG_OK TMSG_MASK_WINDOWMANAGER + 9
+
+/*!
+ \def TMSG_GUI_PREVIOUS_WINDOW
+ \brief Message sent through CApplicationMessenger to go back to the previous window
+
+ This is an alternative to TMSG_GUI_ACTIVATE_WINDOW, but it keeps
+ all configured parameters, like startup directory.
+*/
+#define TMSG_GUI_PREVIOUS_WINDOW TMSG_MASK_WINDOWMANAGER + 10
+
+
+#define TMSG_CALLBACK 800
+
+
+
+class CGUIMessage;
+
+namespace KODI
+{
+namespace MESSAGING
+{
+class IMessageTarget;
+
+struct ThreadMessageCallback
+{
+ void (*callback)(void *userptr);
+ void *userptr;
+};
+
+/*!
+ * \class CApplicationMessenger ApplicationMessenger.h "messaging/ApplicationMessenger.h"
+ * \brief This implements a simple message dispatcher/router for Kodi
+ *
+ * For most users that wants to send message go to the documentation for these
+ * \sa CApplicationMessenger::SendMsg
+ * \sa CApplicationMessenger::PostMsg
+ *
+ * For anyone wanting to implement a message receiver, go to the documentation for
+ * \sa IMessageTarget
+ *
+ * IMPLEMENTATION SPECIFIC NOTES - DOCUMENTED HERE FOR THE SOLE PURPOSE OF IMPLEMENTERS OF THIS CLASS
+ * On a high level this implements two methods for dispatching messages, SendMsg and PostMsg.
+ * These are roughly modeled on the implementation of SendMessage and PostMessage in Windows.
+ *
+ * PostMsg is the preferred method to use as it's non-blocking and does not wait for any response before
+ * returning to the caller. Messages will be stored in a queue and processed in order.
+ *
+ * SendMsg is a blocking version and has a bit more subtleties to it regarding how inter-process
+ * dispatching is handled.
+ *
+ * Calling SendMsg with a message type that doesn't require marshalling will bypass the message queue
+ * and call the receiver directly
+ *
+ * Calling SendMsg with a message type that require marshalling to a specific thread when not on that thread
+ * will add a message to the queue with a an event, it will then block the calling thread waiting on this event
+ * to be signaled.
+ * The message will be processed by the correct thread in it's message pump and the event will be signaled, unblocking
+ * the calling thread
+ *
+ * Calling SendMsg with a message type that require marshalling to a specific thread when already on that thread
+ * will behave as scenario one, it will bypass the queue and call the receiver directly.
+ *
+ * Currently there is a hack implemented in the message dispatcher that releases the graphicslock before dispatching
+ * a message. This was here before the redesign and removing it will require careful inspection of every call site.
+ * TODO: add logging if the graphicslock is held during message dispatch
+ *
+ * Current design has three different message types
+ * 1. Normal messages that can be processed on any thread
+ * 2. GUI messages that require marshalling to the UI thread
+ * 3. A thread message that will spin up a background thread and wait a specified amount of time before posting the message
+ * This should probably be removed, it's left for compatibility
+ *
+ * Heavy emphasis on current design, the idea is that we can easily add more message types to route messages
+ * to more threads or other scenarios.
+ *
+ * \sa CApplicationMessenger::ProcessMessages()
+ * handles regular messages that require no marshalling, this can be called from any thread to drive the message
+ * pump
+ *
+ * \sa CApplicationMessenger::ProcessWindowMessages()
+ * handles GUI messages and currently should only be called on the UI thread
+ *
+ * If/When this is expanded upon ProcessMessage() and ProcessWindowMessages() should be combined into a single method
+ * taking an enum or similar to indicate which message it's interested in.
+ *
+ * The above methods are backed by two messages queues, one for each type of message. If more types are added
+ * this might need to be redesigned to simplify the lookup of the correct message queue but currently they're implemented
+ * as two member variables
+ *
+ * The design is meant to be very encapsulated and easy to extend without altering the public interface.
+ * e.g. If GUI messages should be handled on another thread, call \sa CApplicationMessenger::ProcessWindowMessage() on that
+ * thread and nothing else has to change. The callers have no knowledge of how this is implemented.
+ *
+ * The design is also meant to be very dependency free to work as a bridge between lower layer functionality without
+ * having to have knowledge of the GUI or having a dependency on the GUI in any way. This is not the reality currently as
+ * this depends on \sa CApplication and the graphicslock but should be fixed soon enough.
+ *
+ * To keep things simple the current implementation routes messages based on a mask that the receiver provides.
+ * Any message fitting that mask will be routed to that specific receiver.
+ * This will likely need to change if many different receivers are added but it should be possible to do it without
+ * any of the callers being changed.
+ */
+class CApplicationMessenger
+{
+public:
+ CApplicationMessenger();
+ ~CApplicationMessenger();
+
+ void Cleanup();
+ // if a message has to be send to the gui, use MSG_TYPE_WINDOW instead
+ /*!
+ * \brief Send a blocking message and wait for a response
+ *
+ * If and what the response is depends entirely on the message being sent and
+ * should be documented on the message.
+ *
+ * Under no circumestances shall the caller hold a lock when calling SendMsg as there's
+ * no guarantee what the receiver will do to answer the request.
+ *
+ * \param [in] messageId defined further up in this file
+ * \return meaning of the return varies based on the message
+ */
+ int SendMsg(uint32_t messageId);
+
+ /*!
+ * \brief Send a blocking message and wait for a response
+ *
+ * If and what the response is depends entirely on the message being sent and
+ * should be documented on the message.
+ *
+ * Under no circumestances shall the caller hold a lock when calling SendMsg as there's
+ * no guarantee what the receiver will do to answer the request.
+ *
+ * \param [in] messageId defined further up in this file
+ * \param [in] param1 value depends on the message being sent
+ * \param [in] param2 value depends on the message being sent, defaults to -1
+ * \param [in] payload this is a void pointer that is meant to send larger objects to the receiver
+ * what to send depends on the message
+ * \return meaning of the return varies based on the message
+ */
+ int SendMsg(uint32_t messageId, int param1, int param2 = -1, void* payload = nullptr);
+
+ /*!
+ * \brief Send a blocking message and wait for a response
+ *
+ * If and what the response is depends entirely on the message being sent and
+ * should be documented on the message.
+ *
+ * Under no circumestances shall the caller hold a lock when calling SendMsg as there's
+ * no guarantee what the receiver will do to answer the request.
+ *
+ * \param [in] messageId defined further up in this file
+ * \param [in] param1 value depends on the message being sent
+ * \param [in] param2 value depends on the message being sent
+ * \param [in,out] payload this is a void pointer that is meant to send larger objects to the receiver
+ * what to send depends on the message
+ * \param [in] strParam value depends on the message being sent, remains for backward compat
+ * \return meaning of the return varies based on the message
+ */
+ int SendMsg(uint32_t messageId, int param1, int param2, void* payload, std::string strParam);
+
+ /*!
+ * \brief Send a blocking message and wait for a response
+ *
+ * If and what the response is depends entirely on the message being sent and
+ * should be documented on the message.
+ *
+ * Under no circumestances shall the caller hold a lock when calling SendMsg as there's
+ * no guarantee what the receiver will do to answer the request.
+ *
+ * \param [in] messageId defined further up in this file
+ * \param [in] param1 value depends on the message being sent
+ * \param [in] param2 value depends on the message being sent
+ * \param [in,out] payload this is a void pointer that is meant to send larger objects to the receiver
+ * what to send depends on the message
+ * \param [in] strParam value depends on the message being sent, remains for backward compat
+ * \param [in] params value depends on the message being sent, kept for backward compatibility
+ * \return meaning of the return varies based on the message
+ */
+ int SendMsg(uint32_t messageId, int param1, int param2, void* payload, std::string strParam, std::vector<std::string> params);
+
+ /*!
+ * \brief Send a non-blocking message and return immediately
+ *
+ * If and what the response is depends entirely on the message being sent and
+ * should be documented on the message.
+ *
+ * \param [in] messageId defined further up in this file
+ */
+ void PostMsg(uint32_t messageId);
+
+ /*!
+ * \brief Send a non-blocking message and return immediately
+ *
+ * If and what the response is depends entirely on the message being sent and
+ * should be documented on the message.
+ *
+ * \param [in] messageId defined further up in this file
+ * \param [in] param3 value depends on the message being sent
+ */
+ void PostMsg(uint32_t messageId, int64_t param3);
+
+ /*!
+ * \brief Send a non-blocking message and return immediately
+ *
+ * If and what the response is depends entirely on the message being sent and
+ * should be documented on the message.
+ *
+ * \param [in] messageId defined further up in this file
+ * \param [in] param1 value depends on the message being sent
+ * \param [in] param2 value depends on the message being sent
+ * \param [in,out] payload this is a void pointer that is meant to send larger objects to the receiver
+ * what to send depends on the message
+ */
+ void PostMsg(uint32_t messageId, int param1, int param2 = -1, void* payload = nullptr);
+
+ /*!
+ * \brief Send a non-blocking message and return immediately
+ *
+ * If and what the response is depends entirely on the message being sent and
+ * should be documented on the message.
+ *
+ * \param [in] messageId defined further up in this file
+ * \param [in] param1 value depends on the message being sent
+ * \param [in] param2 value depends on the message being sent
+ * \param [in,out] payload this is a void pointer that is meant to send larger objects to the receiver
+ * what to send depends on the message
+ * \param [in] strParam value depends on the message being sent, remains for backward compat
+ */
+ void PostMsg(uint32_t messageId, int param1, int param2, void* payload, std::string strParam);
+ /*!
+ * \brief Send a non-blocking message and return immediately
+ *
+ * If and what the response is depends entirely on the message being sent and
+ * should be documented on the message.
+ *
+ * \param [in] messageId defined further up in this file
+ * \param [in] param1 value depends on the message being sent
+ * \param [in] param2 value depends on the message being sent
+ * \param [in,out] payload this is a void pointer that is meant to send larger objects to the receiver
+ * what to send depends on the message
+ * \param [in] strParam value depends on the message being sent, remains for backward compat
+ * \param [in] params value depends on the message being sent, kept for backward compatibility
+ */
+ void PostMsg(uint32_t messageId, int param1, int param2, void* payload, std::string strParam, std::vector<std::string> params);
+
+ /*!
+ * \brief Called from any thread to dispatch messages
+ */
+ void ProcessMessages();
+
+ /*!
+ * \brief Called from the UI thread to dispatch UI messages
+ * This is only of value to implementers of the message pump, do not rely on a specific thread
+ * being used other than that it's appropriate for UI messages
+ */
+ void ProcessWindowMessages();
+
+ /*! \brief Send a GUIMessage, optionally waiting before it's processed to return.
+ * This is kept for backward compat and is just a convenience wrapper for for SendMsg and PostMsg
+ * specifically for UI messages
+ * \param msg the GUIMessage to send.
+ * \param windowID optional window to send the message to (defaults to no specified window).
+ * \param waitResult whether to wait for the result (defaults to false).
+ */
+ void SendGUIMessage(const CGUIMessage &msg, int windowID = WINDOW_INVALID, bool waitResult=false);
+
+ /*!
+ * \brief This should be called any class implementing \sa IMessageTarget before it
+ * can receive any messages
+ */
+ void RegisterReceiver(IMessageTarget* target);
+
+ /*!
+ * \brief Set the UI thread id to avoid messenger being dependent on
+ * CApplication to determine if marshaling is required
+ * \param thread The UI thread ID
+ */
+ void SetGUIThread(const std::thread::id thread) { m_guiThreadId = thread; }
+
+ /*!
+ * \brief Set the processing thread id to avoid messenger being dependent on
+ * CApplication to determine if marshaling is required
+ * \param thread The processing thread ID
+ */
+ void SetProcessThread(const std::thread::id thread) { m_processThreadId = thread; }
+
+ /*
+ * \brief Signals the shutdown of the application and message processing
+ */
+ void Stop() { m_bStop = true; }
+
+ //! \brief Returns true if this is the process / app loop thread.
+ bool IsProcessThread() const;
+
+private:
+ CApplicationMessenger(const CApplicationMessenger&) = delete;
+ CApplicationMessenger const& operator=(CApplicationMessenger const&) = delete;
+
+ int SendMsg(ThreadMessage&& msg, bool wait);
+ void ProcessMessage(ThreadMessage *pMsg);
+
+ std::queue<ThreadMessage*> m_vecMessages; /*!< queue for regular messages */
+ std::queue<ThreadMessage*> m_vecWindowMessages; /*!< queue for UI messages */
+ std::map<int, IMessageTarget*> m_mapTargets; /*!< a map of registered receivers indexed on the message mask*/
+ CCriticalSection m_critSection;
+ std::thread::id m_guiThreadId;
+ std::thread::id m_processThreadId;
+ bool m_bStop{ false };
+};
+}
+}
diff --git a/xbmc/messaging/CMakeLists.txt b/xbmc/messaging/CMakeLists.txt
new file mode 100644
index 0000000..86c6b35
--- /dev/null
+++ b/xbmc/messaging/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES ApplicationMessenger.cpp)
+
+set(HEADERS ApplicationMessenger.h
+ IMessageTarget.h
+ ThreadMessage.h)
+
+core_add_library(messaging)
diff --git a/xbmc/messaging/IMessageTarget.h b/xbmc/messaging/IMessageTarget.h
new file mode 100644
index 0000000..17adc83
--- /dev/null
+++ b/xbmc/messaging/IMessageTarget.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
+
+namespace KODI
+{
+namespace MESSAGING
+{
+class ThreadMessage;
+
+/*!
+ * \class IMessageTarget IMessageTarget.h "messaging/IMessageTarget.h"
+ * \brief A class wishing to receive messages should implement this
+ * and call \sa CApplicationMessenger::RegisterReceiver
+ * to start receiving messages
+ */
+class IMessageTarget
+{
+public:
+ virtual ~IMessageTarget() = default;
+ /*!
+ * \brief Should return the message mask that it wishes to receive
+ * messages for
+ *
+ * The message mask is defined in "messaging/ApplicationMessenger.h"
+ * pick the next one available when creating a new
+ */
+ virtual int GetMessageMask() = 0;
+
+ /*!
+ * \brief This gets called whenever a message matching the registered
+ * message mask is processed.
+ *
+ * There are no ordering guarantees here so implementations should never
+ * rely on a certain ordering of messages.
+ *
+ * Cleaning up any pointers stored in the message payload is not specified
+ * and is decided by the implementer of the message.
+ * In general prefer to delete any data in this method to keep the callsites cleaner
+ * and simpler but if data is to be passed back it's perfectly valid to handle it any way
+ * that fits the situation as long as it's documented along with the message.
+ *
+ * To return a simple value the result parameter of \sa ThreadMessage can be used
+ * as it will be used as the return value for \sa CApplicationMessenger::SendMsg.
+ * It is up to the implementer to decide if this is to be used and it should be documented
+ * along with any new message implemented.
+ */
+ virtual void OnApplicationMessage(ThreadMessage* msg) = 0;
+};
+}
+}
diff --git a/xbmc/messaging/ThreadMessage.h b/xbmc/messaging/ThreadMessage.h
new file mode 100644
index 0000000..1a277ff
--- /dev/null
+++ b/xbmc/messaging/ThreadMessage.h
@@ -0,0 +1,142 @@
+/*
+ * 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 <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CEvent;
+
+namespace KODI
+{
+namespace MESSAGING
+{
+
+class CApplicationMessenger;
+
+class ThreadMessage
+{
+ friend CApplicationMessenger;
+public:
+ ThreadMessage()
+ : ThreadMessage{ 0, -1, -1, nullptr }
+ {
+ }
+
+ explicit ThreadMessage(uint32_t messageId)
+ : ThreadMessage{ messageId, -1, -1, nullptr }
+ {
+ }
+
+ ThreadMessage(uint32_t messageId, int64_t p3)
+ : ThreadMessage{ messageId, -1, -1, nullptr, p3 }
+ {
+ }
+
+ ThreadMessage(uint32_t messageId, int p1, int p2, void* payload, int64_t p3 = 0)
+ : dwMessage{ messageId }
+ , param1{ p1 }
+ , param2{ p2 }
+ , param3{ p3 }
+ , lpVoid{ payload }
+ {
+ }
+
+ ThreadMessage(uint32_t messageId,
+ int p1,
+ int p2,
+ void* payload,
+ std::string param,
+ std::vector<std::string> vecParams)
+ : dwMessage{messageId},
+ param1{p1},
+ param2{p2},
+ param3{0},
+ lpVoid{payload},
+ strParam(std::move(param)),
+ params(std::move(vecParams))
+ {
+ }
+
+ ThreadMessage(const ThreadMessage& other) = default;
+
+ ThreadMessage(ThreadMessage&& other) noexcept
+ : dwMessage(other.dwMessage),
+ param1(other.param1),
+ param2(other.param2),
+ param3(other.param3),
+ lpVoid(other.lpVoid),
+ strParam(std::move(other.strParam)),
+ params(std::move(other.params)),
+ waitEvent(std::move(other.waitEvent)),
+ result(std::move(other.result))
+ {
+ }
+
+ ThreadMessage& operator=(const ThreadMessage& other)
+ {
+ if (this == &other)
+ return *this;
+ dwMessage = other.dwMessage;
+ param1 = other.param1;
+ param2 = other.param2;
+ param3 = other.param3;
+ lpVoid = other.lpVoid;
+ strParam = other.strParam;
+ params = other.params;
+ waitEvent = other.waitEvent;
+ result = other.result;
+ return *this;
+ }
+
+ ThreadMessage& operator=(ThreadMessage&& other) noexcept
+ {
+ if (this == &other)
+ return *this;
+ dwMessage = other.dwMessage;
+ param1 = other.param1;
+ param2 = other.param2;
+ param3 = other.param3;
+ lpVoid = other.lpVoid;
+ strParam = std::move(other.strParam);
+ params = std::move(other.params);
+ waitEvent = std::move(other.waitEvent);
+ result = std::move(other.result);
+ return *this;
+ }
+
+ uint32_t dwMessage;
+ int param1;
+ int param2;
+ int64_t param3;
+ void* lpVoid;
+ std::string strParam;
+ std::vector<std::string> params;
+
+ /*!
+ * \brief set the message return value, will only be returned when
+ * the message is sent using SendMsg
+ * \param [in] res the return value or a result status code that is returned to the caller
+ */
+ void SetResult(int res) const
+ {
+ //On posted messages result will be zero, since they can't
+ //retrieve the response we silently ignore this to let message
+ //handlers not have to worry about it
+ if (result)
+ *result = res;
+ }
+protected:
+ std::shared_ptr<CEvent> waitEvent;
+ std::shared_ptr<int> result;
+};
+}
+}
diff --git a/xbmc/messaging/helpers/CMakeLists.txt b/xbmc/messaging/helpers/CMakeLists.txt
new file mode 100644
index 0000000..288ba7c
--- /dev/null
+++ b/xbmc/messaging/helpers/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES DialogHelper.cpp
+ DialogOKHelper.cpp)
+
+set(HEADERS DialogHelper.h
+ DialogOKHelper.h)
+
+core_add_library(messagingHelpers)
diff --git a/xbmc/messaging/helpers/DialogHelper.cpp b/xbmc/messaging/helpers/DialogHelper.cpp
new file mode 100644
index 0000000..e269c09
--- /dev/null
+++ b/xbmc/messaging/helpers/DialogHelper.cpp
@@ -0,0 +1,94 @@
+/*
+ * 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 "DialogHelper.h"
+
+#include "ServiceBroker.h"
+#include "messaging/ApplicationMessenger.h"
+
+#include <cassert>
+#include <utility>
+
+namespace KODI
+{
+namespace MESSAGING
+{
+namespace HELPERS
+{
+DialogResponse ShowYesNoDialogText(CVariant heading, CVariant text, CVariant noLabel, CVariant yesLabel, uint32_t autoCloseTimeout)
+{
+ return ShowYesNoCustomDialog(std::move(heading), std::move(text), std::move(noLabel),
+ std::move(yesLabel), "", autoCloseTimeout);
+}
+
+DialogResponse ShowYesNoCustomDialog(CVariant heading, CVariant text, CVariant noLabel, CVariant yesLabel, CVariant customLabel, uint32_t autoCloseTimeout)
+{
+ DialogYesNoMessage options;
+ options.heading = std::move(heading);
+ options.text = std::move(text);
+ options.noLabel = std::move(noLabel);
+ options.yesLabel = std::move(yesLabel);
+ options.customLabel = std::move(customLabel);
+ options.autoclose = autoCloseTimeout;
+
+ switch (CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_DIALOG_YESNO, -1, -1,
+ static_cast<void*>(&options)))
+ {
+ case -1:
+ return DialogResponse::CHOICE_CANCELLED;
+ case 0:
+ return DialogResponse::CHOICE_NO;
+ case 1:
+ return DialogResponse::CHOICE_YES;
+ case 2:
+ return DialogResponse::CHOICE_CUSTOM;
+ default:
+ //If we get here someone changed the return values without updating this code
+ assert(false);
+ }
+ //This is unreachable code but we need to return something to suppress warnings about
+ //no return
+ return DialogResponse::CHOICE_CANCELLED;
+}
+
+DialogResponse ShowYesNoDialogLines(CVariant heading, CVariant line0, CVariant line1, CVariant line2,
+ CVariant noLabel, CVariant yesLabel, uint32_t autoCloseTimeout)
+{
+ DialogYesNoMessage options;
+ options.heading = std::move(heading);
+ options.lines[0] = std::move(line0);
+ options.lines[1] = std::move(line1);
+ options.lines[2] = std::move(line2);
+ options.noLabel = std::move(noLabel);
+ options.yesLabel = std::move(yesLabel);
+ options.customLabel = "";
+ options.autoclose = autoCloseTimeout;
+
+ switch (CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_DIALOG_YESNO, -1, -1,
+ static_cast<void*>(&options)))
+ {
+ case -1:
+ return DialogResponse::CHOICE_CANCELLED;
+ case 0:
+ return DialogResponse::CHOICE_NO;
+ case 1:
+ return DialogResponse::CHOICE_YES;
+ case 2:
+ return DialogResponse::CHOICE_CUSTOM;
+ default:
+ //If we get here someone changed the return values without updating this code
+ assert(false);
+ }
+ //This is unreachable code but we need to return something to suppress warnings about
+ //no return
+ return DialogResponse::CHOICE_CANCELLED;
+}
+
+}
+}
+}
diff --git a/xbmc/messaging/helpers/DialogHelper.h b/xbmc/messaging/helpers/DialogHelper.h
new file mode 100644
index 0000000..d36fad3
--- /dev/null
+++ b/xbmc/messaging/helpers/DialogHelper.h
@@ -0,0 +1,112 @@
+/*
+ * 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 "utils/Variant.h"
+
+#include <array>
+#include <string>
+
+namespace KODI
+{
+namespace MESSAGING
+{
+namespace HELPERS
+{
+
+enum class DialogResponse : int
+{
+ CHOICE_CANCELLED,
+ CHOICE_YES,
+ CHOICE_NO,
+ CHOICE_CUSTOM,
+};
+
+/*! \struct DialogYesNoMessage DialogHelper.h "messaging/helpers/DialogHelper.h"
+ \brief Payload sent for message TMSG_GUI_DIALOG_YESNO
+
+ \sa ShowDialogText
+ \sa ShowDialogLines
+*/
+struct DialogYesNoMessage
+{
+ CVariant heading; //!< Heading to be displayed in the dialog box
+ CVariant text; //!< Body text to be displayed, this is mutually exclusive with lines below
+ std::array<CVariant, 3> lines; //!< Body text to be displayed, specified as three lines. This is mutually exclusive with the text above
+ CVariant yesLabel; //!< Text to show on the yes button
+ CVariant noLabel; //!< Text to show on the no button
+ CVariant customLabel; //!< Text to show on the 3rd custom button
+ uint32_t autoclose{0}; //!< Time in milliseconds before autoclosing the dialog, 0 means don't autoclose
+};
+
+/*!
+ \brief This is a helper method to send a threadmessage to open a Yes/No dialog box
+
+ \param[in] heading The text to display as the dialog box header
+ \param[in] text The text to display in the dialog body
+ \param[in] noLabel The text to display on the No button
+ defaults to No
+ \param[in] yesLabel The text to display on the Yes button
+ defaults to Yes
+ \param[in] autoCloseTimeout The time before the dialog closes
+ defaults to 0 show indefinitely
+ \return -1 on cancelled, 0 on no and 1 on yes
+ \sa ShowYesNoDialogLines
+ \sa CGUIDialogYesNo::ShowAndGetInput
+ \sa DialogYesNoMessage
+*/
+DialogResponse ShowYesNoDialogText(CVariant heading, CVariant text, CVariant noLabel = CVariant(),
+ CVariant yesLabel = CVariant(), uint32_t autoCloseTimeout = 0);
+
+/*!
+\brief This is a helper method to send a threadmessage to open a Yes/No dialog box with a custom button
+
+\param[in] heading The text to display as the dialog box header
+\param[in] text The text to display in the dialog body
+\param[in] noLabel The text to display on the No button
+ defaults to No
+\param[in] yesLabel The text to display on the Yes button
+ defaults to Yes
+\param[in] customLabel The text to display on the optional 3rd custom button
+ defaults to empty and button not shown
+\param[in] autoCloseTimeout The time before the dialog closes
+ defaults to 0 show indefinitely
+\return -1 on cancelled, 0 on no, 1 on yes and 2 on 3rd custom response
+\sa ShowYesNoDialogLines
+\sa CGUIDialogYesNo::ShowAndGetInput
+\sa DialogYesNoMessage
+*/
+DialogResponse ShowYesNoCustomDialog(CVariant heading, CVariant text, CVariant noLabel = CVariant(), CVariant yesLabel = CVariant(),
+ CVariant customLabel = CVariant(), uint32_t autoCloseTimeout = 0);
+
+/*!
+ \brief This is a helper method to send a threadmessage to open a Yes/No dialog box
+
+ \param[in] heading The text to display as the dialog box header
+ \param[in] line0 The text to display on the first line
+ \param[in] line1 The text to display on the second line
+ \param[in] line2 The text to display on the third line
+ \param[in] noLabel The text to display on the No button
+ defaults to No
+ \param[in] yesLabel The text to display on the Yes button
+ defaults to Yes
+ \param[in] autoCloseTimeout The time before the dialog closes
+ defaults to 0 show indefinitely
+ \return -1 on cancelled, 0 on no and 1 on yes
+ \sa ShowYesNoDialogText
+ \sa CGUIDialogYesNo::ShowAndGetInput
+ \sa DialogYesNoMessage
+*/
+DialogResponse ShowYesNoDialogLines(CVariant heading, CVariant line0, CVariant line1 = CVariant(),
+ CVariant line2 = CVariant(), CVariant noLabel = CVariant(),
+ CVariant yesLabel = CVariant(), uint32_t autoCloseTimeout = 0);
+
+}
+}
+}
diff --git a/xbmc/messaging/helpers/DialogOKHelper.cpp b/xbmc/messaging/helpers/DialogOKHelper.cpp
new file mode 100644
index 0000000..71e8a79
--- /dev/null
+++ b/xbmc/messaging/helpers/DialogOKHelper.cpp
@@ -0,0 +1,59 @@
+/*
+ * 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 "DialogOKHelper.h"
+
+#include "ServiceBroker.h"
+#include "messaging/ApplicationMessenger.h"
+
+namespace KODI
+{
+namespace MESSAGING
+{
+namespace HELPERS
+{
+bool ShowOKDialogText(CVariant heading, CVariant text)
+{
+ DialogOKMessage options;
+ options.heading = std::move(heading);
+ options.text = std::move(text);
+
+ if (CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_DIALOG_OK, -1, -1,
+ static_cast<void*>(&options)) > 0)
+ return true;
+ return false;
+}
+
+void UpdateOKDialogText(CVariant heading, CVariant text)
+{
+ DialogOKMessage options;
+ options.heading = std::move(heading);
+ options.text = std::move(text);
+ options.show = false;
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_DIALOG_OK, -1, -1,
+ static_cast<void*>(&options));
+}
+
+bool ShowOKDialogLines(CVariant heading, CVariant line0, CVariant line1, CVariant line2)
+{
+ DialogOKMessage options;
+ options.heading = std::move(heading);
+ options.lines[0] = std::move(line0);
+ options.lines[1] = std::move(line1);
+ options.lines[2] = std::move(line2);
+
+ if (CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_DIALOG_OK, -1, -1,
+ static_cast<void*>(&options)) > 0)
+ return true;
+ return false;
+}
+
+}
+}
+}
diff --git a/xbmc/messaging/helpers/DialogOKHelper.h b/xbmc/messaging/helpers/DialogOKHelper.h
new file mode 100644
index 0000000..574bed6
--- /dev/null
+++ b/xbmc/messaging/helpers/DialogOKHelper.h
@@ -0,0 +1,77 @@
+/*
+ * 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 "utils/Variant.h"
+
+#include <array>
+#include <string>
+
+namespace KODI
+{
+namespace MESSAGING
+{
+namespace HELPERS
+{
+
+/*! \struct DialogOkMessage DialogHelper.h "messaging/helpers/DialogHelper.h"
+ \brief Payload sent for message TMSG_GUI_DIALOG_OK
+
+ \sa ShowOKDialogText
+ \sa ShowOKDialogLines
+*/
+struct DialogOKMessage
+{
+ CVariant heading; //!< Heading to be displayed in the dialog box
+ CVariant text; //!< Body text to be displayed, this is mutually exclusive with lines below
+ std::array<CVariant, 3> lines; //!< Body text to be displayed, specified as three lines. This is mutually exclusive with the text above
+ bool show = true; //!< bool to see if the dialog needs to be shown
+};
+
+/*!
+ \brief This is a helper method to send a threadmessage to update a Ok dialog text
+
+ \param[in] heading The text to display as the dialog box header
+ \param[in] text The text to display in the dialog body
+ \sa ShowOKDialogLines
+ \sa CGUIDialogOK::ShowAndGetInput
+ \sa DialogOKMessage
+*/
+void UpdateOKDialogText(CVariant heading, CVariant text);
+
+/*!
+ \brief This is a helper method to send a threadmessage to open a Ok dialog box
+
+ \param[in] heading The text to display as the dialog box header
+ \param[in] text The text to display in the dialog body
+ \return if it's confirmed
+ \sa UpdateOKDialogLines
+ \sa CGUIDialogOK::ShowAndGetInput
+ \sa DialogOKMessage
+*/
+bool ShowOKDialogText(CVariant heading, CVariant text);
+
+/*!
+ \brief This is a helper method to send a threadmessage to open a OK dialog box
+
+ \param[in] heading The text to display as the dialog box header
+ \param[in] line0 The text to display on the first line
+ \param[in] line1 The text to display on the second line
+ \param[in] line2 The text to display on the third line
+ \return if it's confirmed
+ \sa ShowOKDialogText
+ \sa CGUIDialogOK::ShowAndGetInput
+ \sa DialogOKMessage
+*/
+bool ShowOKDialogLines(CVariant heading, CVariant line0, CVariant line1 = CVariant(),
+ CVariant line2 = CVariant());
+
+}
+}
+}
diff --git a/xbmc/music/Album.cpp b/xbmc/music/Album.cpp
new file mode 100644
index 0000000..efaa7ac
--- /dev/null
+++ b/xbmc/music/Album.cpp
@@ -0,0 +1,671 @@
+/*
+ * 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 "Album.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/MathUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace MUSIC_INFO;
+
+typedef struct ReleaseTypeInfo {
+ CAlbum::ReleaseType type;
+ std::string name;
+} ReleaseTypeInfo;
+
+ReleaseTypeInfo releaseTypes[] = {
+ { CAlbum::Album, "album" },
+ { CAlbum::Single, "single" }
+};
+
+CAlbum::CAlbum(const CFileItem& item)
+{
+ Reset();
+ const CMusicInfoTag& tag = *item.GetMusicInfoTag();
+ strAlbum = tag.GetAlbum();
+ strMusicBrainzAlbumID = tag.GetMusicBrainzAlbumID();
+ strReleaseGroupMBID = tag.GetMusicBrainzReleaseGroupID();
+ genre = tag.GetGenre();
+ strArtistDesc = tag.GetAlbumArtistString();
+ //Set sort string before processing artist credits
+ strArtistSort = tag.GetAlbumArtistSort();
+ // Determine artist credits from various tag arrays, inc fallback to song artist names
+ SetArtistCredits(tag.GetAlbumArtist(), tag.GetMusicBrainzAlbumArtistHints(), tag.GetMusicBrainzAlbumArtistID(),
+ tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID());
+
+ strOrigReleaseDate = tag.GetOriginalDate();
+ strReleaseDate = tag.GetReleaseDate();
+ strLabel = tag.GetRecordLabel();
+ strType = tag.GetMusicBrainzReleaseType();
+ bCompilation = tag.GetCompilation();
+ iTimesPlayed = 0;
+ bBoxedSet = tag.GetBoxset();
+ dateAdded.Reset();
+ dateUpdated.Reset();
+ lastPlayed.Reset();
+ releaseType = tag.GetAlbumReleaseType();
+ strReleaseStatus = tag.GetAlbumReleaseStatus();
+}
+
+void CAlbum::SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints,
+ const std::vector<std::string>& mbids,
+ const std::vector<std::string>& artistnames, const std::vector<std::string>& artisthints,
+ const std::vector<std::string>& artistmbids)
+{
+ std::vector<std::string> albumartistHints = hints;
+ //Split the artist sort string to try and get sort names for individual artists
+ auto artistSort = StringUtils::Split(strArtistSort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ artistCredits.clear();
+
+ if (!mbids.empty())
+ { // Have musicbrainz artist info, so use it
+
+ // Vector of possible separators in the order least likely to be part of artist name
+ const std::vector<std::string> separators{ " feat. ", ";", ":", "|", "#", "/", ",", "&" };
+
+ // Establish tag consistency
+ // Do the number of musicbrainz ids and *both* number of names and number of hints mismatch?
+ if (albumartistHints.size() != mbids.size() && names.size() != mbids.size())
+ {
+ // Tags mismatch - report it and then try to fix
+ CLog::Log(LOGDEBUG, "Mismatch in song file albumartist tags: {} mbid {} name album: {} {}",
+ (int)mbids.size(), (int)names.size(), strAlbum, strArtistDesc);
+ /*
+ Most likely we have no hints and a single artist name like "Artist1 feat. Artist2"
+ or "Composer; Conductor, Orchestra, Soloist" or "Artist1/Artist2" where the
+ expected single item separator (default = space-slash-space) as not been used.
+ Comma and slash (no spaces) are poor delimiters as could be in name e.g. AC/DC,
+ but here treat them as such in attempt to find artist names.
+ When there are hints they could be poorly formatted using unexpected separators,
+ so attempt to split them. Or we could have more hints or artist names than
+ musicbrainz so ignore them but raise warning.
+ */
+
+ // Do hints exist yet mismatch
+ if (!albumartistHints.empty() && albumartistHints.size() != mbids.size())
+ {
+ if (names.size() == mbids.size())
+ // Album artist name count matches, use that as hints
+ albumartistHints = names;
+ else if (albumartistHints.size() < mbids.size())
+ { // Try splitting the hints until have matching number
+ albumartistHints = StringUtils::SplitMulti(albumartistHints, separators, mbids.size());
+ }
+ else
+ // Extra hints, discard them.
+ albumartistHints.resize(mbids.size());
+ }
+ // Do hints not exist or still mismatch, try album artists
+ if (albumartistHints.size() != mbids.size())
+ albumartistHints = names;
+ // Still mismatch, try splitting the hints (now artists) until have matching number
+ if (albumartistHints.size() < mbids.size())
+ albumartistHints = StringUtils::SplitMulti(albumartistHints, separators, mbids.size());
+ // Try matching on artists or artist hints field, if it is reliable
+ if (albumartistHints.size() != mbids.size())
+ {
+ if (!artistmbids.empty() &&
+ (artistmbids.size() == artistnames.size() ||
+ artistmbids.size() == artisthints.size()))
+ {
+ for (size_t i = 0; i < mbids.size(); i++)
+ {
+ for (size_t j = 0; j < artistmbids.size(); j++)
+ {
+ if (mbids[i] == artistmbids[j])
+ {
+ if (albumartistHints.size() < i + 1)
+ albumartistHints.resize(i + 1);
+ if (artistmbids.size() == artisthints.size())
+ albumartistHints[i] = artisthints[j];
+ else
+ albumartistHints[i] = artistnames[j];
+ }
+ }
+ }
+ }
+ }
+ }
+ else
+ { // Either hints or album artists (or both) name matches number of musicbrainz id
+ // If hints mismatch, use album artists
+ if (albumartistHints.size() != mbids.size())
+ albumartistHints = names;
+ }
+
+ // Try to get number of artist sort names and musicbrainz ids to match. Split sort names
+ // further using multiple possible delimiters, over single separator applied in Tag loader
+ if (artistSort.size() != mbids.size())
+ artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" });
+
+ for (size_t i = 0; i < mbids.size(); i++)
+ {
+ std::string artistId = mbids[i];
+ std::string artistName;
+ /*
+ We try and get the musicbrainz id <-> name matching from the hints and match on the same index.
+ Some album artist hints could be blank (if populated from artist or artist hints).
+ If not found, use the musicbrainz id and hope we later on can update that entry.
+ If we have more names than musicbrainz id they are ignored, but raise a warning.
+ */
+ if (i < albumartistHints.size())
+ artistName = albumartistHints[i];
+ if (artistName.empty())
+ artistName = artistId;
+
+ // Use artist sort name providing we have as many as we have mbid,
+ // otherwise something is wrong with them so ignore and leave blank
+ if (artistSort.size() == mbids.size())
+ artistCredits.emplace_back(StringUtils::Trim(artistName), StringUtils::Trim(artistSort[i]), artistId);
+ else
+ artistCredits.emplace_back(StringUtils::Trim(artistName), "", artistId);
+ }
+ }
+ else
+ {
+ /*
+ No musicbrainz album artist ids so fill artist names directly.
+ This method only called during scanning when there is a musicbrainz album id, so
+ means mbid tags are incomplete. But could also be called by JSON to SetAlbumDetails
+ Try to separate album artist names further, and trim blank space.
+ */
+ std::vector<std::string> albumArtists = names;
+ if (albumartistHints.size() > albumArtists.size())
+ // Make use of hints (ALBUMARTISTS tag), when present, to separate artist names
+ albumArtists = albumartistHints;
+ else
+ // Split album artist names further using multiple possible delimiters, over single separator applied in Tag loader
+ albumArtists = StringUtils::SplitMulti(albumArtists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicArtistSeparators);
+
+ if (artistSort.size() != albumArtists.size())
+ // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader
+ artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" });
+
+ for (size_t i = 0; i < albumArtists.size(); i++)
+ {
+ artistCredits.emplace_back(StringUtils::Trim(albumArtists[i]));
+ // Set artist sort name providing we have as many as we have artists,
+ // otherwise something is wrong with them so ignore rather than guess.
+ if (artistSort.size() == albumArtists.size())
+ artistCredits.back().SetSortName(StringUtils::Trim(artistSort[i]));
+ }
+ }
+}
+
+void CAlbum::MergeScrapedAlbum(const CAlbum& source, bool override /* = true */)
+{
+ /*
+ Initial scraping of album information when there is a Musicbrainz album ID derived from
+ tags is done directly using that ID, otherwise the lookup is based on album and artist names
+ but this can sometimes mis-identify the album (i.e. classical music has many "Symphony No. 5").
+ It is useful to store the scraped mbid, but we need to be able to correct any mistakes. Hence
+ a manual refresh of album information uses either the mbid as derived from tags or the album
+ and artist names, not any previously scraped mbid.
+
+ When overwriting the data derived from tags, AND the original and scraped album have the same
+ Musicbrainz album ID, then merging is used to keep Kodi up to date with changes in the Musicbrainz
+ database including album artist credits, song artist credits and song titles. However it is only
+ appropriate when the music files are tagged with mbids, these are taken as definative, scraped
+ mbids can not be depended on in this way.
+
+ When the album is megerd in this deep way it is flagged so that the database album update is aware
+ artist credits and songs need to be updated too.
+ */
+
+ bArtistSongMerge = override && !bScrapedMBID
+ && !source.strMusicBrainzAlbumID.empty() && !strMusicBrainzAlbumID.empty()
+ && (strMusicBrainzAlbumID.compare(source.strMusicBrainzAlbumID) == 0);
+
+ /*
+ Musicbrainz album (release) ID and release group ID values derived from music file tags are
+ always taken as accurate and so can not be overwritten by a scraped value. When the album does
+ not already have an mbid or has a previously scraped mbid, merge the new scraped value,
+ flagging it as being from the scraper rather than derived from music file tags.
+ */
+ if (!source.strMusicBrainzAlbumID.empty() && (strMusicBrainzAlbumID.empty() || bScrapedMBID))
+ {
+ strMusicBrainzAlbumID = source.strMusicBrainzAlbumID;
+ bScrapedMBID = true;
+ }
+ if (!source.strReleaseGroupMBID.empty() && (strReleaseGroupMBID.empty() || bScrapedMBID))
+ {
+ strReleaseGroupMBID = source.strReleaseGroupMBID;
+ }
+
+ /*
+ Scraping can return different album artists from the originals derived from tags, even when
+ doing a lookup on artist name.
+
+ When overwriting the data derived from tags, AND the original and scraped album have the same
+ Musicbrainz album ID, then merging an album replaces both the album artsts and the song artists
+ with those scraped (providing they are not empty).
+
+ When not doing that kind of merge, for any matching artist names the Musicbrainz artist id
+ returned by the scraper can be used to populate any previously missing Musicbrainz artist id values.
+ */
+ if (bArtistSongMerge && !source.artistCredits.empty())
+ {
+ artistCredits = source.artistCredits; // Replace artists and store mbid returned by scraper
+ strArtistDesc.clear(); // @todo: set artist display string e.g. "artist1 & artist2" when scraped
+ }
+ else
+ {
+ // Compare original album artists with those scraped (ignoring order), and set any missing mbid
+ for (auto &artistCredit : artistCredits)
+ {
+ if (artistCredit.GetMusicBrainzArtistID().empty())
+ {
+ for (const auto& sourceartistCredit : source.artistCredits)
+ {
+ if (StringUtils::EqualsNoCase(artistCredit.GetArtist(), sourceartistCredit.GetArtist()))
+ {
+ artistCredit.SetMusicBrainzArtistID(sourceartistCredit.GetMusicBrainzArtistID());
+ artistCredit.SetScrapedMBID(true);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ //@todo: scraped album genre needs adding to genre and album_genre tables, this just changes the string
+ if ((override && !source.genre.empty()) || genre.empty())
+ genre = source.genre;
+ if ((override && !source.strAlbum.empty()) || strAlbum.empty())
+ strAlbum = source.strAlbum;
+ //@todo: validate ISO8601 format YYYY, YYYY-MM, or YYYY-MM-DD
+ if ((override && !source.strReleaseDate.empty()) || strReleaseDate.empty())
+ strReleaseDate = source.strReleaseDate;
+ if ((override && !source.strOrigReleaseDate.empty()) || strOrigReleaseDate.empty())
+ strOrigReleaseDate = source.strOrigReleaseDate;
+
+ if (override)
+ bCompilation = source.bCompilation;
+ // iTimesPlayed = source.iTimesPlayed; // times played is derived from songs
+
+ if ((override && !source.strArtistSort.empty()) || strArtistSort.empty())
+ strArtistSort = source.strArtistSort;
+ for (const auto& i : source.art)
+ {
+ if (override || art.find(i.first) == art.end())
+ art[i.first] = i.second;
+ }
+ if((override && !source.strLabel.empty()) || strLabel.empty())
+ strLabel = source.strLabel;
+ thumbURL = source.thumbURL;
+ moods = source.moods;
+ styles = source.styles;
+ themes = source.themes;
+ strReview = source.strReview;
+ if ((override && !source.strType.empty()) || strType.empty())
+ strType = source.strType;
+// strPath = source.strPath; // don't merge the path
+ if ((override && !source.strReleaseStatus.empty()) || strReleaseStatus.empty())
+ strReleaseStatus = source.strReleaseStatus;
+ fRating = source.fRating;
+ iUserrating = source.iUserrating;
+ iVotes = source.iVotes;
+
+ /*
+ When overwriting the data derived from tags, AND the original and scraped album have the same
+ Musicbrainz album ID, update the local songs with scaped Musicbrainz information including the
+ artist credits.
+ */
+ if (bArtistSongMerge)
+ {
+ for (auto &song : songs)
+ {
+ if (!song.strMusicBrainzTrackID.empty())
+ for (const auto& sourceSong : source.songs)
+ if ((sourceSong.strMusicBrainzTrackID == song.strMusicBrainzTrackID) && (sourceSong.iTrack == song.iTrack))
+ song.MergeScrapedSong(sourceSong, override);
+ }
+ }
+}
+
+std::string CAlbum::GetGenreString() const
+{
+ return StringUtils::Join(genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+}
+
+const std::vector<std::string> CAlbum::GetAlbumArtist() const
+{
+ //Get artist names as vector from artist credits
+ std::vector<std::string> albumartists;
+ for (const auto& artistCredit : artistCredits)
+ {
+ albumartists.push_back(artistCredit.GetArtist());
+ }
+ return albumartists;
+}
+
+const std::vector<std::string> CAlbum::GetMusicBrainzAlbumArtistID() const
+{
+ //Get artist MusicBrainz IDs as vector from artist credits
+ std::vector<std::string> musicBrainzID;
+ for (const auto& artistCredit : artistCredits)
+ {
+ musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID());
+ }
+ return musicBrainzID;
+}
+
+const std::string CAlbum::GetAlbumArtistString() const
+{
+ //Artist description may be different from the artists in artistcredits (see ALBUMARTISTS tag processing)
+ //but is takes precedence as a string because artistcredits is not always filled during processing
+ if (!strArtistDesc.empty())
+ return strArtistDesc;
+ std::vector<std::string> artistvector;
+ for (const auto& i : artistCredits)
+ artistvector.emplace_back(i.GetArtist());
+ std::string artistString;
+ if (!artistvector.empty())
+ artistString = StringUtils::Join(artistvector, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ return artistString;
+}
+
+const std::string CAlbum::GetAlbumArtistSort() const
+{
+ //The stored artist sort name string takes precedence but a
+ //value could be created from individual sort names held in artistcredits
+ if (!strArtistSort.empty())
+ return strArtistSort;
+ std::vector<std::string> artistvector;
+ for (const auto& artistcredit : artistCredits)
+ if (!artistcredit.GetSortName().empty())
+ artistvector.emplace_back(artistcredit.GetSortName());
+ std::string artistString;
+ if (!artistvector.empty())
+ artistString = StringUtils::Join(artistvector, "; ");
+ return artistString;
+}
+
+const std::vector<int> CAlbum::GetArtistIDArray() const
+{
+ // Get album artist IDs for json rpc
+ std::vector<int> artistids;
+ for (const auto& artistCredit : artistCredits)
+ artistids.push_back(artistCredit.GetArtistId());
+ return artistids;
+}
+
+
+std::string CAlbum::GetReleaseType() const
+{
+ return ReleaseTypeToString(releaseType);
+}
+
+void CAlbum::SetReleaseType(const std::string& strReleaseType)
+{
+ releaseType = ReleaseTypeFromString(strReleaseType);
+}
+
+void CAlbum::SetDateAdded(const std::string& strDateAdded)
+{
+ dateAdded.SetFromDBDateTime(strDateAdded);
+}
+
+void CAlbum::SetDateUpdated(const std::string& strDateUpdated)
+{
+ dateUpdated.SetFromDBDateTime(strDateUpdated);
+}
+
+void CAlbum::SetDateNew(const std::string& strDateNew)
+{
+ dateNew.SetFromDBDateTime(strDateNew);
+}
+
+void CAlbum::SetLastPlayed(const std::string& strLastPlayed)
+{
+ lastPlayed.SetFromDBDateTime(strLastPlayed);
+}
+
+std::string CAlbum::ReleaseTypeToString(CAlbum::ReleaseType releaseType)
+{
+ for (const ReleaseTypeInfo& releaseTypeInfo : releaseTypes)
+ {
+ if (releaseTypeInfo.type == releaseType)
+ return releaseTypeInfo.name;
+ }
+
+ return "album";
+}
+
+CAlbum::ReleaseType CAlbum::ReleaseTypeFromString(const std::string& strReleaseType)
+{
+ for (const ReleaseTypeInfo& releaseTypeInfo : releaseTypes)
+ {
+ if (releaseTypeInfo.name == strReleaseType)
+ return releaseTypeInfo.type;
+ }
+
+ return Album;
+}
+
+bool CAlbum::operator<(const CAlbum &a) const
+{
+ if (strMusicBrainzAlbumID.empty() && a.strMusicBrainzAlbumID.empty())
+ {
+ if (strAlbum < a.strAlbum) return true;
+ if (strAlbum > a.strAlbum) return false;
+
+ // This will do an std::vector compare (i.e. item by item)
+ if (GetAlbumArtist() < a.GetAlbumArtist()) return true;
+ if (GetAlbumArtist() > a.GetAlbumArtist()) return false;
+ return false;
+ }
+
+ if (strMusicBrainzAlbumID < a.strMusicBrainzAlbumID) return true;
+ if (strMusicBrainzAlbumID > a.strMusicBrainzAlbumID) return false;
+ return false;
+}
+
+bool CAlbum::Load(const TiXmlElement *album, bool append, bool prioritise)
+{
+ if (!album) return false;
+ if (!append)
+ Reset();
+
+ const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ XMLUtils::GetString(album, "title", strAlbum);
+ XMLUtils::GetString(album, "musicbrainzalbumid", strMusicBrainzAlbumID);
+ XMLUtils::GetString(album, "musicbrainzreleasegroupid", strReleaseGroupMBID);
+ XMLUtils::GetBoolean(album, "scrapedmbid", bScrapedMBID);
+ XMLUtils::GetString(album, "artistdesc", strArtistDesc);
+ std::vector<std::string> artist; // Support old style <artist></artist> for backwards compatibility
+ XMLUtils::GetStringArray(album, "artist", artist, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(album, "genre", genre, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(album, "style", styles, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(album, "mood", moods, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(album, "theme", themes, prioritise, itemSeparator);
+ XMLUtils::GetBoolean(album, "compilation", bCompilation);
+ XMLUtils::GetBoolean(album, "boxset", bBoxedSet);
+
+ XMLUtils::GetString(album,"review",strReview);
+ XMLUtils::GetString(album,"label",strLabel);
+ XMLUtils::GetInt(album, "duration", iAlbumDuration);
+ XMLUtils::GetString(album,"type",strType);
+ XMLUtils::GetString(album, "releasestatus", strReleaseStatus);
+
+ XMLUtils::GetString(album, "releasedate", strReleaseDate);
+ StringUtils::Trim(strReleaseDate); // @todo: validate ISO8601 format
+ // Support old style <year></year> for backwards compatibility
+ if (strReleaseDate.empty())
+ {
+ int year;
+ XMLUtils::GetInt(album, "year", year);
+ if (year > 0)
+ strReleaseDate = StringUtils::Format("{:04}", year);
+ }
+ XMLUtils::GetString(album, "originalreleasedate", strOrigReleaseDate);
+
+ const TiXmlElement* rElement = album->FirstChildElement("rating");
+ if (rElement)
+ {
+ float rating = 0;
+ float max_rating = 10;
+ XMLUtils::GetFloat(album, "rating", rating);
+ if (rElement->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating>=1)
+ rating *= (10.f / max_rating); // Normalise the Rating to between 0 and 10
+ if (rating > 10.f)
+ rating = 10.f;
+ fRating = rating;
+ }
+ const TiXmlElement* userrating = album->FirstChildElement("userrating");
+ if (userrating)
+ {
+ float rating = 0;
+ float max_rating = 10;
+ XMLUtils::GetFloat(album, "userrating", rating);
+ if (userrating->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1)
+ rating *= (10.f / max_rating); // Normalise the Rating to between 0 and 10
+ if (rating > 10.f)
+ rating = 10.f;
+ iUserrating = MathUtils::round_int(static_cast<double>(rating));
+ }
+ XMLUtils::GetInt(album, "votes", iVotes);
+
+ size_t iThumbCount = thumbURL.GetUrls().size();
+ std::string xmlAdd = thumbURL.GetData();
+ const TiXmlElement* thumb = album->FirstChildElement("thumb");
+ while (thumb)
+ {
+ thumbURL.ParseAndAppendUrl(thumb);
+ if (prioritise)
+ {
+ std::string temp;
+ temp << *thumb;
+ xmlAdd = temp+xmlAdd;
+ }
+ thumb = thumb->NextSiblingElement("thumb");
+ }
+ // prioritise thumbs from nfos
+ if (prioritise && iThumbCount && iThumbCount != thumbURL.GetUrls().size())
+ {
+ auto thumbUrls = thumbURL.GetUrls();
+ rotate(thumbUrls.begin(), thumbUrls.begin() + iThumbCount, thumbUrls.end());
+ thumbURL.SetUrls(thumbUrls);
+ thumbURL.SetData(xmlAdd);
+ }
+
+ const TiXmlElement* albumArtistCreditsNode = album->FirstChildElement("albumArtistCredits");
+ if (albumArtistCreditsNode)
+ artistCredits.clear();
+
+ while (albumArtistCreditsNode)
+ {
+ if (albumArtistCreditsNode->FirstChild())
+ {
+ CArtistCredit artistCredit;
+ XMLUtils::GetString(albumArtistCreditsNode, "artist", artistCredit.m_strArtist);
+ XMLUtils::GetString(albumArtistCreditsNode, "musicBrainzArtistID", artistCredit.m_strMusicBrainzArtistID);
+ artistCredits.push_back(artistCredit);
+ }
+
+ albumArtistCreditsNode = albumArtistCreditsNode->NextSiblingElement("albumArtistCredits");
+ }
+
+ // Support old style <artist></artist> for backwards compatibility
+ // .nfo files should ideally be updated to use the artist credits structure above
+ // or removed entirely in preference for better tags (MusicBrainz?)
+ if (artistCredits.empty() && !artist.empty())
+ {
+ for (const auto& it : artist)
+ {
+ CArtistCredit artistCredit(it);
+ artistCredits.push_back(artistCredit);
+ }
+ }
+
+ std::string strReleaseType;
+ if (XMLUtils::GetString(album, "releasetype", strReleaseType))
+ SetReleaseType(strReleaseType);
+ else
+ releaseType = Album;
+
+ return true;
+}
+
+bool CAlbum::Save(TiXmlNode *node, const std::string &tag, const std::string& strPath)
+{
+ if (!node) return false;
+
+ // we start with a <tag> tag
+ TiXmlElement albumElement(tag.c_str());
+ TiXmlNode *album = node->InsertEndChild(albumElement);
+
+ if (!album) return false;
+
+ XMLUtils::SetString(album, "title", strAlbum);
+ XMLUtils::SetString(album, "musicbrainzalbumid", strMusicBrainzAlbumID);
+ XMLUtils::SetString(album, "musicbrainzreleasegroupid", strReleaseGroupMBID);
+ XMLUtils::SetBoolean(album, "scrapedmbid", bScrapedMBID);
+ XMLUtils::SetString(album, "artistdesc", strArtistDesc); //Can be different from artist credits
+ XMLUtils::SetStringArray(album, "genre", genre);
+ XMLUtils::SetStringArray(album, "style", styles);
+ XMLUtils::SetStringArray(album, "mood", moods);
+ XMLUtils::SetStringArray(album, "theme", themes);
+ XMLUtils::SetBoolean(album, "compilation", bCompilation);
+ XMLUtils::SetBoolean(album, "boxset", bBoxedSet);
+
+ XMLUtils::SetString(album, "review", strReview);
+ XMLUtils::SetString(album, "type", strType);
+ XMLUtils::SetString(album, "releasestatus", strReleaseStatus);
+ XMLUtils::SetString(album, "releasedate", strReleaseDate);
+ XMLUtils::SetString(album, "originalreleasedate", strOrigReleaseDate);
+ XMLUtils::SetString(album, "label", strLabel);
+ XMLUtils::SetInt(album, "duration", iAlbumDuration);
+ if (thumbURL.HasData())
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(thumbURL.GetData());
+ const TiXmlNode* thumb = doc.FirstChild("thumb");
+ while (thumb)
+ {
+ album->InsertEndChild(*thumb);
+ thumb = thumb->NextSibling("thumb");
+ }
+ }
+ XMLUtils::SetString(album, "path", strPath);
+
+ auto* rating = XMLUtils::SetFloat(album, "rating", fRating);
+ if (rating)
+ rating->ToElement()->SetAttribute("max", 10);
+
+ auto* userrating = XMLUtils::SetInt(album, "userrating", iUserrating);
+ if (userrating)
+ userrating->ToElement()->SetAttribute("max", 10);
+
+ XMLUtils::SetInt(album, "votes", iVotes);
+
+ for (const auto& artistCredit : artistCredits)
+ {
+ // add an <albumArtistCredits> tag
+ TiXmlElement albumArtistCreditsElement("albumArtistCredits");
+ TiXmlNode *albumArtistCreditsNode = album->InsertEndChild(albumArtistCreditsElement);
+ XMLUtils::SetString(albumArtistCreditsNode, "artist", artistCredit.m_strArtist);
+ XMLUtils::SetString(albumArtistCreditsNode, "musicBrainzArtistID",
+ artistCredit.m_strMusicBrainzArtistID);
+ }
+
+ XMLUtils::SetString(album, "releasetype", GetReleaseType());
+
+ return true;
+}
+
diff --git a/xbmc/music/Album.h b/xbmc/music/Album.h
new file mode 100644
index 0000000..18a08b7
--- /dev/null
+++ b/xbmc/music/Album.h
@@ -0,0 +1,183 @@
+/*
+ * 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
+
+/*!
+ \file Album.h
+\brief
+*/
+
+#include "Artist.h"
+#include "Song.h"
+#include "XBDateTime.h"
+#include "utils/ScraperUrl.h"
+
+#include <map>
+#include <vector>
+
+class TiXmlNode;
+class CFileItem;
+class CAlbum
+{
+public:
+ explicit CAlbum(const CFileItem& item);
+ CAlbum() = default;
+ bool operator<(const CAlbum &a) const;
+ void MergeScrapedAlbum(const CAlbum& album, bool override = true);
+
+ void Reset()
+ {
+ idAlbum = -1;
+ strAlbum.clear();
+ strMusicBrainzAlbumID.clear();
+ strReleaseGroupMBID.clear();
+ artistCredits.clear();
+ strArtistDesc.clear();
+ strArtistSort.clear();
+ genre.clear();
+ thumbURL.Clear();
+ moods.clear();
+ styles.clear();
+ themes.clear();
+ art.clear();
+ strReview.clear();
+ strLabel.clear();
+ strType.clear();
+ strReleaseStatus.clear();
+ strPath.clear();
+ fRating = -1;
+ iUserrating = -1;
+ iVotes = -1;
+ strOrigReleaseDate.clear();
+ strReleaseDate.clear();
+ bCompilation = false;
+ bBoxedSet = false;
+ iTimesPlayed = 0;
+ dateAdded.Reset();
+ dateUpdated.Reset();
+ dateNew.Reset();
+ lastPlayed.Reset();
+ iTotalDiscs = -1;
+ songs.clear();
+ releaseType = Album;
+ strLastScraped.clear();
+ bScrapedMBID = false;
+ bArtistSongMerge = false;
+ iAlbumDuration = 0;
+ }
+
+ /*! \brief Get album artist names from the vector of artistcredits objects
+ \return album artist names as a vector of strings
+ */
+ const std::vector<std::string> GetAlbumArtist() const;
+
+ /*! \brief Get album artist MusicBrainz IDs from the vector of artistcredits objects
+ \return album artist MusicBrainz IDs as a vector of strings
+ */
+ const std::vector<std::string> GetMusicBrainzAlbumArtistID() const;
+ std::string GetGenreString() const;
+
+ /*! \brief Get album artist names from the artist description string (if it exists)
+ or concatenated from the vector of artistcredits objects
+ \return album artist names as a single string
+ */
+ const std::string GetAlbumArtistString() const;
+
+ /*! \brief Get album artist sort name from the artist sort string (if it exists)
+ or concatenated from the vector of artistcredits objects
+ \return album artist sort names as a single string
+ */
+ const std::string GetAlbumArtistSort() const;
+
+ /*! \brief Get album artist IDs (for json rpc) from the vector of artistcredits objects
+ \return album artist IDs as a vector of integers
+ */
+ const std::vector<int> GetArtistIDArray() const;
+
+ typedef enum ReleaseType {
+ Album = 0,
+ Single
+ } ReleaseType;
+
+ std::string GetReleaseType() const;
+ void SetReleaseType(const std::string& strReleaseType);
+ void SetDateAdded(const std::string& strDateAdded);
+ void SetDateUpdated(const std::string& strDateUpdated);
+ void SetDateNew(const std::string& strDateNew);
+ void SetLastPlayed(const std::string& strLastPlayed);
+
+ static std::string ReleaseTypeToString(ReleaseType releaseType);
+ static ReleaseType ReleaseTypeFromString(const std::string& strReleaseType);
+
+ /*! \brief Set album artist credits using the arrays of tag values.
+ If strArtistSort (as from ALBUMARTISTSORT tag) is already set then individual
+ artist sort names are also processed.
+ \param names String vector of albumartist names (as from ALBUMARTIST tag)
+ \param hints String vector of albumartist name hints (as from ALBUMARTISTS tag)
+ \param mbids String vector of albumartist Musicbrainz IDs (as from MUSICBRAINZABUMARTISTID tag)
+ \param artistnames String vector of artist names (as from ARTIST tag)
+ \param artisthints String vector of artist name hints (as from ARTISTS tag)
+ \param artistmbids String vector of artist Musicbrainz IDs (as from MUSICBRAINZARTISTID tag)
+ */
+ void SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints,
+ const std::vector<std::string>& mbids,
+ const std::vector<std::string>& artistnames = std::vector<std::string>(),
+ const std::vector<std::string>& artisthints = std::vector<std::string>(),
+ const std::vector<std::string>& artistmbids = std::vector<std::string>());
+
+ /*! \brief Load album information from an XML file.
+ See CVideoInfoTag::Load for a description of the types of elements we load.
+ \param element the root XML element to parse.
+ \param append whether information should be added to the existing tag, or whether it should be reset first.
+ \param prioritise if appending, whether additive tags should be prioritised (i.e. replace or prepend) over existing values. Defaults to false.
+ \sa CVideoInfoTag::Load
+ */
+ bool Load(const TiXmlElement *element, bool append = false, bool prioritise = false);
+ bool Save(TiXmlNode *node, const std::string &tag, const std::string& strPath);
+
+ int idAlbum = -1;
+ std::string strAlbum;
+ std::string strMusicBrainzAlbumID;
+ std::string strReleaseGroupMBID;
+ std::string strArtistDesc;
+ std::string strArtistSort;
+ VECARTISTCREDITS artistCredits;
+ std::vector<std::string> genre;
+ CScraperUrl thumbURL;
+ std::vector<std::string> moods;
+ std::vector<std::string> styles;
+ std::vector<std::string> themes;
+ std::map<std::string, std::string> art;
+ std::string strReview;
+ std::string strLabel;
+ std::string strType;
+ std::string strReleaseStatus;
+ std::string strPath;
+ float fRating = -1;
+ int iUserrating = -1;
+ int iVotes = -1;
+ std::string strReleaseDate;
+ std::string strOrigReleaseDate;
+ bool bBoxedSet = false;
+ bool bCompilation = false;
+ int iTimesPlayed = 0;
+ CDateTime dateAdded; // From related file creation or modification times, or when (re-)scanned
+ CDateTime dateUpdated; // Time db record Last modified
+ CDateTime dateNew; // Time db record created
+ CDateTime lastPlayed;
+ int iTotalDiscs = -1;
+ VECSONGS songs; ///< Local songs
+ ReleaseType releaseType = Album;
+ std::string strLastScraped;
+ bool bScrapedMBID = false;
+ bool bArtistSongMerge = false;
+ int iAlbumDuration = 0;
+};
+
+typedef std::vector<CAlbum> VECALBUMS;
diff --git a/xbmc/music/Artist.cpp b/xbmc/music/Artist.cpp
new file mode 100644
index 0000000..786f7a7
--- /dev/null
+++ b/xbmc/music/Artist.cpp
@@ -0,0 +1,239 @@
+/*
+ * 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 "Artist.h"
+
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Fanart.h"
+#include "utils/XMLUtils.h"
+
+#include <algorithm>
+
+void CArtist::MergeScrapedArtist(const CArtist& source, bool override /* = true */)
+{
+ /*
+ Initial scraping of artist information when the mbid is derived from tags is done directly
+ using that ID, otherwise the lookup is based on name and can mis-identify the artist
+ (many have same name). It is useful to store the scraped mbid, but we need to be
+ able to correct any mistakes. Hence a manual refresh of artist information uses either
+ the mbid is derived from tags or the artist name, not any previously scraped mbid.
+
+ A Musicbrainz artist ID derived from music file tags is always taken as accurate and so can
+ not be overwritten by a scraped value. When the artist does not already have an mbid or has
+ a previously scraped mbid, merge the new scraped value, flagging it as being from the
+ scraper rather than derived from music file tags.
+ */
+ if (!source.strMusicBrainzArtistID.empty() && (strMusicBrainzArtistID.empty() || bScrapedMBID))
+ {
+ strMusicBrainzArtistID = source.strMusicBrainzArtistID;
+ bScrapedMBID = true;
+ }
+
+ if ((override && !source.strArtist.empty()) || strArtist.empty())
+ strArtist = source.strArtist;
+
+ if ((override && !source.strSortName.empty()) || strSortName.empty())
+ strSortName = source.strSortName;
+
+ strType = source.strType;
+ strGender = source.strGender;
+ strDisambiguation = source.strDisambiguation;
+ genre = source.genre;
+ strBiography = source.strBiography;
+ styles = source.styles;
+ moods = source.moods;
+ instruments = source.instruments;
+ strBorn = source.strBorn;
+ strFormed = source.strFormed;
+ strDied = source.strDied;
+ strDisbanded = source.strDisbanded;
+ yearsActive = source.yearsActive;
+
+ thumbURL = source.thumbURL; // Available remote art
+ // Current artwork - thumb, fanart etc., to be stored in art table
+ if (!source.art.empty())
+ art = source.art;
+
+ discography = source.discography;
+}
+
+
+bool CArtist::Load(const TiXmlElement *artist, bool append, bool prioritise)
+{
+ if (!artist) return false;
+ if (!append)
+ Reset();
+
+ const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ XMLUtils::GetString(artist, "name", strArtist);
+ XMLUtils::GetString(artist, "musicBrainzArtistID", strMusicBrainzArtistID);
+ XMLUtils::GetString(artist, "sortname", strSortName);
+ XMLUtils::GetString(artist, "type", strType);
+ XMLUtils::GetString(artist, "gender", strGender);
+ XMLUtils::GetString(artist, "disambiguation", strDisambiguation);
+ XMLUtils::GetStringArray(artist, "genre", genre, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(artist, "style", styles, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(artist, "mood", moods, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(artist, "yearsactive", yearsActive, prioritise, itemSeparator);
+ XMLUtils::GetStringArray(artist, "instruments", instruments, prioritise, itemSeparator);
+
+ XMLUtils::GetString(artist, "born", strBorn);
+ XMLUtils::GetString(artist, "formed", strFormed);
+ XMLUtils::GetString(artist, "biography", strBiography);
+ XMLUtils::GetString(artist, "died", strDied);
+ XMLUtils::GetString(artist, "disbanded", strDisbanded);
+
+ size_t iThumbCount = thumbURL.GetUrls().size();
+ std::string xmlAdd = thumbURL.GetData();
+
+ // Available artist thumbs
+ const TiXmlElement* thumb = artist->FirstChildElement("thumb");
+ while (thumb)
+ {
+ thumbURL.ParseAndAppendUrl(thumb);
+ if (prioritise)
+ {
+ std::string temp;
+ temp << *thumb;
+ xmlAdd = temp+xmlAdd;
+ }
+ thumb = thumb->NextSiblingElement("thumb");
+ }
+ // prefix thumbs from nfos
+ if (prioritise && iThumbCount && iThumbCount != thumbURL.GetUrls().size())
+ {
+ auto thumbUrls = thumbURL.GetUrls();
+ rotate(thumbUrls.begin(), thumbUrls.begin() + iThumbCount, thumbUrls.end());
+ thumbURL.SetUrls(thumbUrls);
+ thumbURL.SetData(xmlAdd);
+ }
+
+ // Discography
+ const TiXmlElement* node = artist->FirstChildElement("album");
+ if (node)
+ discography.clear();
+ while (node)
+ {
+ if (node->FirstChild())
+ {
+ CDiscoAlbum album;
+ XMLUtils::GetString(node, "title", album.strAlbum);
+ XMLUtils::GetString(node, "year", album.strYear);
+ XMLUtils::GetString(node, "musicbrainzreleasegroupid", album.strReleaseGroupMBID);
+ discography.push_back(album);
+ }
+ node = node->NextSiblingElement("album");
+ }
+
+ // Support old style <fanart></fanart> for backwards compatibility of old nfo files and scrapers
+ const TiXmlElement *fanart2 = artist->FirstChildElement("fanart");
+ if (fanart2)
+ {
+ CFanart fanart;
+ // we prefix to handle mixed-mode nfo's with fanart set
+ if (prioritise)
+ {
+ std::string temp;
+ temp << *fanart2;
+ fanart.m_xml = temp+fanart.m_xml;
+ }
+ else
+ fanart.m_xml << *fanart2;
+ fanart.Unpack();
+ // Append fanart to other image URLs
+ for (unsigned int i = 0; i < fanart.GetNumFanarts(); i++)
+ thumbURL.AddParsedUrl(fanart.GetImageURL(i), "fanart", fanart.GetPreviewURL(i));
+ }
+
+ // Current artwork - thumb, fanart etc. (the chosen art, not the lists of those available)
+ node = artist->FirstChildElement("art");
+ if (node)
+ {
+ const TiXmlNode *artdetailNode = node->FirstChild();
+ while (artdetailNode && artdetailNode->FirstChild())
+ {
+ art.insert(make_pair(artdetailNode->ValueStr(), artdetailNode->FirstChild()->ValueStr()));
+ artdetailNode = artdetailNode->NextSibling();
+ }
+ }
+
+ return true;
+}
+
+bool CArtist::Save(TiXmlNode *node, const std::string &tag, const std::string& strPath)
+{
+ if (!node) return false;
+
+ // we start with a <tag> tag
+ TiXmlElement artistElement(tag.c_str());
+ TiXmlNode *artist = node->InsertEndChild(artistElement);
+
+ if (!artist) return false;
+
+ XMLUtils::SetString(artist, "name", strArtist);
+ XMLUtils::SetString(artist, "musicBrainzArtistID", strMusicBrainzArtistID);
+ XMLUtils::SetString(artist, "sortname", strSortName);
+ XMLUtils::SetString(artist, "type", strType);
+ XMLUtils::SetString(artist, "gender", strGender);
+ XMLUtils::SetString(artist, "disambiguation", strDisambiguation);
+ XMLUtils::SetStringArray(artist, "genre", genre);
+ XMLUtils::SetStringArray(artist, "style", styles);
+ XMLUtils::SetStringArray(artist, "mood", moods);
+ XMLUtils::SetStringArray(artist, "yearsactive", yearsActive);
+ XMLUtils::SetStringArray(artist, "instruments", instruments);
+ XMLUtils::SetString(artist, "born", strBorn);
+ XMLUtils::SetString(artist, "formed", strFormed);
+ XMLUtils::SetString(artist, "biography", strBiography);
+ XMLUtils::SetString(artist, "died", strDied);
+ XMLUtils::SetString(artist, "disbanded", strDisbanded);
+ // Available remote art
+ if (thumbURL.HasData())
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(thumbURL.GetData());
+ const TiXmlNode* thumb = doc.FirstChild("thumb");
+ while (thumb)
+ {
+ artist->InsertEndChild(*thumb);
+ thumb = thumb->NextSibling("thumb");
+ }
+ }
+ XMLUtils::SetString(artist, "path", strPath);
+
+ // Discography
+ for (const auto& it : discography)
+ {
+ // add a <album> tag
+ TiXmlElement discoElement("album");
+ TiXmlNode* node = artist->InsertEndChild(discoElement);
+ XMLUtils::SetString(node, "title", it.strAlbum);
+ XMLUtils::SetString(node, "year", it.strYear);
+ XMLUtils::SetString(node, "musicbrainzreleasegroupid", it.strReleaseGroupMBID);
+ }
+
+ return true;
+}
+
+void CArtist::SetDateAdded(const std::string& strDateAdded)
+{
+ dateAdded.SetFromDBDateTime(strDateAdded);
+}
+
+void CArtist::SetDateUpdated(const std::string& strDateUpdated)
+{
+ dateUpdated.SetFromDBDateTime(strDateUpdated);
+}
+
+void CArtist::SetDateNew(const std::string& strDateNew)
+{
+ dateNew.SetFromDBDateTime(strDateNew);
+}
+
diff --git a/xbmc/music/Artist.h b/xbmc/music/Artist.h
new file mode 100644
index 0000000..2ec77a8
--- /dev/null
+++ b/xbmc/music/Artist.h
@@ -0,0 +1,222 @@
+/*
+ * 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 "XBDateTime.h"
+#include "utils/ScraperUrl.h"
+#include "utils/StringUtils.h"
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+class TiXmlNode;
+class CAlbum;
+class CMusicDatabase;
+
+class CDiscoAlbum
+{
+public:
+ std::string strAlbum;
+ std::string strYear;
+ std::string strReleaseGroupMBID;
+};
+
+class CArtist
+{
+public:
+ int idArtist = -1;
+ bool operator<(const CArtist& a) const
+ {
+ if (strMusicBrainzArtistID.empty() && a.strMusicBrainzArtistID.empty())
+ {
+ if (strArtist < a.strArtist) return true;
+ if (strArtist > a.strArtist) return false;
+ return false;
+ }
+
+ if (strMusicBrainzArtistID < a.strMusicBrainzArtistID) return true;
+ if (strMusicBrainzArtistID > a.strMusicBrainzArtistID) return false;
+ return false;
+ }
+
+ void MergeScrapedArtist(const CArtist& source, bool override = true);
+
+ void Reset()
+ {
+ strArtist.clear();
+ strSortName.clear();
+ strType.clear();
+ strGender.clear();
+ strDisambiguation.clear();
+ genre.clear();
+ strBiography.clear();
+ styles.clear();
+ moods.clear();
+ instruments.clear();
+ strBorn.clear();
+ strFormed.clear();
+ strDied.clear();
+ strDisbanded.clear();
+ yearsActive.clear();
+ thumbURL.Clear();
+ art.clear();
+ discography.clear();
+ idArtist = -1;
+ strPath.clear();
+ dateAdded.Reset();
+ dateUpdated.Reset();
+ dateNew.Reset();
+ bScrapedMBID = false;
+ strLastScraped.clear();
+ }
+
+ /*! \brief Load artist information from an XML file.
+ See CVideoInfoTag::Load for a description of the types of elements we load.
+ \param element the root XML element to parse.
+ \param append whether information should be added to the existing tag, or whether it should be reset first.
+ \param prioritise if appending, whether additive tags should be prioritised (i.e. replace or prepend) over existing values. Defaults to false.
+ \sa CVideoInfoTag::Load
+ */
+ bool Load(const TiXmlElement *element, bool append = false, bool prioritise = false);
+ bool Save(TiXmlNode *node, const std::string &tag, const std::string& strPath);
+
+ void SetDateAdded(const std::string& strDateAdded);
+ void SetDateUpdated(const std::string& strDateUpdated);
+ void SetDateNew(const std::string& strDateNew);
+
+ std::string strArtist;
+ std::string strSortName;
+ std::string strMusicBrainzArtistID;
+ std::string strType;
+ std::string strGender;
+ std::string strDisambiguation;
+ std::vector<std::string> genre;
+ std::string strBiography;
+ std::vector<std::string> styles;
+ std::vector<std::string> moods;
+ std::vector<std::string> instruments;
+ std::string strBorn;
+ std::string strFormed;
+ std::string strDied;
+ std::string strDisbanded;
+ std::vector<std::string> yearsActive;
+ std::string strPath;
+ CScraperUrl thumbURL; // Data for available remote art
+ std::map<std::string, std::string> art; // Current artwork - thumb, fanart etc.
+ std::vector<CDiscoAlbum> discography;
+ CDateTime dateAdded; // From related file creation or modification times, or when (re-)scanned
+ CDateTime dateUpdated; // Time db record Last modified
+ CDateTime dateNew; // Time db record created
+ bool bScrapedMBID = false;
+ std::string strLastScraped;
+};
+
+class CArtistCredit
+{
+ friend class CAlbum;
+ friend class CMusicDatabase;
+
+public:
+ CArtistCredit() = default;
+ explicit CArtistCredit(std::string strArtist) : m_strArtist(std::move(strArtist)) {}
+ CArtistCredit(std::string strArtist, std::string strMusicBrainzArtistID)
+ : m_strArtist(std::move(strArtist)), m_strMusicBrainzArtistID(std::move(strMusicBrainzArtistID))
+ {
+ }
+ CArtistCredit(std::string strArtist, std::string strSortName, std::string strMusicBrainzArtistID)
+ : m_strArtist(std::move(strArtist)),
+ m_strSortName(std::move(strSortName)),
+ m_strMusicBrainzArtistID(std::move(strMusicBrainzArtistID))
+ {
+ }
+
+ bool operator<(const CArtistCredit& a) const
+ {
+ if (m_strMusicBrainzArtistID.empty() && a.m_strMusicBrainzArtistID.empty())
+ {
+ if (m_strArtist < a.m_strArtist) return true;
+ if (m_strArtist > a.m_strArtist) return false;
+ return false;
+ }
+
+ if (m_strMusicBrainzArtistID < a.m_strMusicBrainzArtistID) return true;
+ if (m_strMusicBrainzArtistID > a.m_strMusicBrainzArtistID) return false;
+ return false;
+ }
+
+ std::string GetArtist() const { return m_strArtist; }
+ std::string GetSortName() const { return m_strSortName; }
+ std::string GetMusicBrainzArtistID() const { return m_strMusicBrainzArtistID; }
+ int GetArtistId() const { return idArtist; }
+ bool HasScrapedMBID() const { return m_bScrapedMBID; }
+ void SetArtist(const std::string &strArtist) { m_strArtist = strArtist; }
+ void SetSortName(const std::string &strSortName) { m_strSortName = strSortName; }
+ void SetMusicBrainzArtistID(const std::string &strMusicBrainzArtistID) { m_strMusicBrainzArtistID = strMusicBrainzArtistID; }
+ void SetArtistId(int idArtist) { this->idArtist = idArtist; }
+ void SetScrapedMBID(bool scrapedMBID) { this->m_bScrapedMBID = scrapedMBID; }
+
+private:
+ int idArtist = -1;
+ std::string m_strArtist;
+ std::string m_strSortName;
+ std::string m_strMusicBrainzArtistID;
+ bool m_bScrapedMBID = false; // Flag that mbid is from album merge of scarper results not derived from tags
+};
+
+typedef std::vector<CArtist> VECARTISTS;
+typedef std::vector<CArtistCredit> VECARTISTCREDITS;
+
+const std::string BLANKARTIST_FAKEMUSICBRAINZID = "Artist Tag Missing";
+const std::string BLANKARTIST_NAME = "[Missing Tag]";
+const int BLANKARTIST_ID = 1;
+const std::string VARIOUSARTISTS_MBID = "89ad4ac3-39f7-470e-963a-56509c546377";
+
+#define ROLE_ARTIST 1 //Default role
+
+class CMusicRole
+{
+public:
+ CMusicRole() = default;
+ CMusicRole(std::string strRole, std::string strArtist)
+ : idRole(-1), m_strRole(std::move(strRole)), m_strArtist(std::move(strArtist)), idArtist(-1)
+ {
+ }
+ CMusicRole(int role, std::string strRole, std::string strArtist, int ArtistId)
+ : idRole(role),
+ m_strRole(std::move(strRole)),
+ m_strArtist(std::move(strArtist)),
+ idArtist(ArtistId)
+ {
+ }
+ std::string GetArtist() const { return m_strArtist; }
+ std::string GetRoleDesc() const { return m_strRole; }
+ int GetRoleId() const { return idRole; }
+ int GetArtistId() const { return idArtist; }
+ void SetArtistId(int iArtistId) { idArtist = iArtistId; }
+
+ bool operator==(const CMusicRole& a) const
+ {
+ if (StringUtils::EqualsNoCase(m_strRole, a.m_strRole))
+ return StringUtils::EqualsNoCase(m_strArtist, a.m_strArtist);
+ else
+ return false;
+ }
+private:
+ int idRole;
+ std::string m_strRole;
+ std::string m_strArtist;
+ int idArtist;
+};
+
+typedef std::vector<CMusicRole> VECMUSICROLES;
+
+
+
diff --git a/xbmc/music/CMakeLists.txt b/xbmc/music/CMakeLists.txt
new file mode 100644
index 0000000..5e1fd42
--- /dev/null
+++ b/xbmc/music/CMakeLists.txt
@@ -0,0 +1,25 @@
+set(SOURCES Album.cpp
+ Artist.cpp
+ ContextMenus.cpp
+ GUIViewStateMusic.cpp
+ MusicDatabase.cpp
+ MusicDbUrl.cpp
+ MusicInfoLoader.cpp
+ MusicLibraryQueue.cpp
+ MusicThumbLoader.cpp
+ MusicUtils.cpp
+ Song.cpp)
+
+set(HEADERS Album.h
+ Artist.h
+ ContextMenus.h
+ GUIViewStateMusic.h
+ MusicDatabase.h
+ MusicDbUrl.h
+ MusicInfoLoader.h
+ MusicLibraryQueue.h
+ MusicThumbLoader.h
+ MusicUtils.h
+ Song.h)
+
+core_add_library(music)
diff --git a/xbmc/music/ContextMenus.cpp b/xbmc/music/ContextMenus.cpp
new file mode 100644
index 0000000..bf7dd47
--- /dev/null
+++ b/xbmc/music/ContextMenus.cpp
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenus.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "music/MusicUtils.h"
+#include "music/dialogs/GUIDialogMusicInfo.h"
+#include "tags/MusicInfoTag.h"
+
+#include <utility>
+
+using namespace CONTEXTMENU;
+
+CMusicInfo::CMusicInfo(MediaType mediaType)
+ : CStaticContextMenuAction(19033), m_mediaType(std::move(mediaType))
+{
+}
+
+bool CMusicInfo::IsVisible(const CFileItem& item) const
+{
+ return (item.HasMusicInfoTag() && item.GetMusicInfoTag()->GetType() == m_mediaType) ||
+ (m_mediaType == MediaTypeArtist && item.IsVideoDb() && item.HasProperty("artist_musicid")) ||
+ (m_mediaType == MediaTypeAlbum && item.IsVideoDb() && item.HasProperty("album_musicid"));
+}
+
+bool CMusicInfo::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ CGUIDialogMusicInfo::ShowFor(item.get());
+ return true;
+}
+
+bool CMusicBrowse::IsVisible(const CFileItem& item) const
+{
+ if (item.IsFileFolder(EFILEFOLDER_MASK_ONBROWSE))
+ return false; // handled by CMediaWindow
+
+ return item.m_bIsFolder && MUSIC_UTILS::IsItemPlayable(item);
+}
+
+bool CMusicBrowse::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ auto& windowMgr = CServiceBroker::GetGUI()->GetWindowManager();
+ if (windowMgr.GetActiveWindow() == WINDOW_MUSIC_NAV)
+ {
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, WINDOW_MUSIC_NAV, 0, GUI_MSG_UPDATE);
+ msg.SetStringParam(item->GetPath());
+ windowMgr.SendMessage(msg);
+ }
+ else
+ {
+ windowMgr.ActivateWindow(WINDOW_MUSIC_NAV, {item->GetPath(), "return"});
+ }
+ return true;
+}
+
+bool CMusicPlay::IsVisible(const CFileItem& item) const
+{
+ return MUSIC_UTILS::IsItemPlayable(item);
+}
+
+bool CMusicPlay::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ MUSIC_UTILS::PlayItem(item);
+ return true;
+}
+
+bool CMusicPlayNext::IsVisible(const CFileItem& item) const
+{
+ if (!item.CanQueue())
+ return false;
+
+ return MUSIC_UTILS::IsItemPlayable(item);
+}
+
+bool CMusicPlayNext::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ MUSIC_UTILS::QueueItem(item, MUSIC_UTILS::QueuePosition::POSITION_BEGIN);
+ return true;
+}
+
+bool CMusicQueue::IsVisible(const CFileItem& item) const
+{
+ if (!item.CanQueue())
+ return false;
+
+ return MUSIC_UTILS::IsItemPlayable(item);
+}
+
+namespace
+{
+void SelectNextItem(int windowID)
+{
+ auto& windowMgr = CServiceBroker::GetGUI()->GetWindowManager();
+ CGUIWindow* window = windowMgr.GetWindow(windowID);
+ if (window)
+ {
+ const int viewContainerID = window->GetViewContainerID();
+ if (viewContainerID > 0)
+ {
+ CGUIMessage msg1(GUI_MSG_ITEM_SELECTED, windowID, viewContainerID);
+ windowMgr.SendMessage(msg1, windowID);
+
+ CGUIMessage msg2(GUI_MSG_ITEM_SELECT, windowID, viewContainerID, msg1.GetParam1() + 1);
+ windowMgr.SendMessage(msg2, windowID);
+ }
+ }
+}
+} // unnamed namespace
+
+bool CMusicQueue::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ MUSIC_UTILS::QueueItem(item, MUSIC_UTILS::QueuePosition::POSITION_END);
+
+ // Set selection to next item in active window's view.
+ const int windowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ SelectNextItem(windowID);
+
+ return true;
+}
diff --git a/xbmc/music/ContextMenus.h b/xbmc/music/ContextMenus.h
new file mode 100644
index 0000000..7327592
--- /dev/null
+++ b/xbmc/music/ContextMenus.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenuItem.h"
+#include "media/MediaType.h"
+
+#include <memory>
+
+class CFileItem;
+
+namespace CONTEXTMENU
+{
+
+struct CMusicInfo : CStaticContextMenuAction
+{
+ explicit CMusicInfo(MediaType mediaType);
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+
+private:
+ const MediaType m_mediaType;
+};
+
+struct CAlbumInfo : CMusicInfo
+{
+ CAlbumInfo() : CMusicInfo(MediaTypeAlbum) {}
+};
+
+struct CArtistInfo : CMusicInfo
+{
+ CArtistInfo() : CMusicInfo(MediaTypeArtist) {}
+};
+
+struct CSongInfo : CMusicInfo
+{
+ CSongInfo() : CMusicInfo(MediaTypeSong) {}
+};
+
+struct CMusicBrowse : CStaticContextMenuAction
+{
+ CMusicBrowse() : CStaticContextMenuAction(37015) {} // Browse into
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CMusicPlay : CStaticContextMenuAction
+{
+ CMusicPlay() : CStaticContextMenuAction(208) {} // Play
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CMusicPlayNext : CStaticContextMenuAction
+{
+ CMusicPlayNext() : CStaticContextMenuAction(10008) {} // Play next
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CMusicQueue : CStaticContextMenuAction
+{
+ CMusicQueue() : CStaticContextMenuAction(13347) {} // Queue item
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+} // namespace CONTEXTMENU
diff --git a/xbmc/music/GUIViewStateMusic.cpp b/xbmc/music/GUIViewStateMusic.cpp
new file mode 100644
index 0000000..f14f501
--- /dev/null
+++ b/xbmc/music/GUIViewStateMusic.cpp
@@ -0,0 +1,648 @@
+/*
+ * 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 "GUIViewStateMusic.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "playlists/PlayListTypes.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/SortUtils.h"
+#include "utils/log.h"
+#include "view/ViewStateSettings.h"
+
+using namespace XFILE;
+using namespace MUSICDATABASEDIRECTORY;
+
+PLAYLIST::Id CGUIViewStateWindowMusic::GetPlaylist() const
+{
+ return PLAYLIST::TYPE_MUSIC;
+}
+
+bool CGUIViewStateWindowMusic::AutoPlayNextItem()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ return settings->GetBool(CSettings::SETTING_MUSICPLAYER_AUTOPLAYNEXTITEM) &&
+ !settings->GetBool(CSettings::SETTING_MUSICPLAYER_QUEUEBYDEFAULT);
+}
+
+std::string CGUIViewStateWindowMusic::GetLockType()
+{
+ return "music";
+}
+
+std::string CGUIViewStateWindowMusic::GetExtensions()
+{
+ return CServiceBroker::GetFileExtensionProvider().GetMusicExtensions();
+}
+
+VECSOURCES& CGUIViewStateWindowMusic::GetSources()
+{
+ return CGUIViewState::GetSources();
+}
+
+CGUIViewStateMusicSearch::CGUIViewStateMusicSearch(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ SortAttribute sortAttribute = SortAttributeNone;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttribute = SortAttributeIgnoreArticle;
+
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D", "%L", "%A")); // Title - Artist, Duration | Label, Artist
+ SetSortMethod(SortByTitle);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateMusicSearch::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavsongs"));
+}
+
+CGUIViewStateMusicDatabase::CGUIViewStateMusicDatabase(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ CMusicDatabaseDirectory dir;
+ NODE_TYPE NodeType=dir.GetDirectoryChildType(items.GetPath());
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_LIBRARYTRACKFORMAT);
+ if (strTrack.empty())
+ strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ std::string strAlbum = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_strMusicLibraryAlbumFormat;
+ if (strAlbum.empty())
+ strAlbum = "%B"; // album
+ CLog::Log(LOGDEBUG, "Custom album format = [{}]", strAlbum);
+ SortAttribute sortAttribute = SortAttributeNone;
+ if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttribute = SortAttributeIgnoreArticle;
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName);
+
+ switch (NodeType)
+ {
+ case NODE_TYPE_OVERVIEW:
+ {
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "", "%L", "")); // Filename, empty | Foldername, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_TOP100:
+ {
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "", "%L", "")); // Filename, empty | Foldername, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_GENRE:
+ {
+ AddSortMethod(SortByGenre, 515, LABEL_MASKS("%F", "", "%G", "")); // Filename, empty | Genre, empty
+ SetSortMethod(SortByGenre);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderAscending);
+ }
+ break;
+ case NODE_TYPE_ROLE:
+ {
+ AddSortMethod(SortByNone, 576, LABEL_MASKS("%F", "", "%G", "")); // Filename, empty | Genre, empty
+ SetSortMethod(SortByPlaycount);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_YEAR:
+ {
+ AddSortMethod(SortByLabel, 562, LABEL_MASKS("%F", "", "%Y", "")); // Filename, empty | Year, empty
+ SetSortMethod(SortByLabel);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderAscending);
+ }
+ break;
+ case NODE_TYPE_ARTIST:
+ {
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%F", "", "%A", "")); // Filename, empty | Artist, empty
+ AddSortMethod(SortByDateAdded, sortAttribute, 570, LABEL_MASKS("%F", "", "%A", "%a")); // Filename, empty | Artist, dateAdded
+ SetSortMethod(SortByArtist);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavartists");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_ALBUM:
+ {
+ // album
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined (default=%B), Artist
+ // artist
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined, Artist
+ // artist / year
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%F", "", strAlbum, "%A / %Y")); // Filename, empty | Userdefined, Artist / Year
+ // discs
+ AddSortMethod(
+ SortByTotalDiscs, sortAttribute, 38077,
+ LABEL_MASKS("%F", "", strAlbum, "%b")); // Filename, empty | Userdefined, Total discs
+ // year
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%F", "", strAlbum, "%Y")); // Filename, empty | Userdefined, Year
+ // original release year
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ AddSortMethod(
+ SortByOrigDate, sortAttribute, 38079,
+ LABEL_MASKS("%F", "", strAlbum, "%e")); // Filename, empty | Userdefined, Original date
+ // album date added
+ AddSortMethod(SortByDateAdded, sortAttribute, 570, LABEL_MASKS("%F", "", strAlbum, "%a")); // Filename, empty | Userdefined, dateAdded
+ // play count
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%F", "", strAlbum, "%V")); // Filename, empty | Userdefined, Play count
+ // last played
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%F", "", strAlbum, "%p")); // Filename, empty | Userdefined, last played
+ // rating
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%F", "", strAlbum, "%R")); // Filename, empty | Userdefined, Rating
+ // userrating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%F", "", strAlbum, "%r")); // Filename, empty | Userdefined, UserRating
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavalbums");
+ SetSortMethod(viewState->m_sortDescription);
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ {
+ AddSortMethod(SortByNone, 552, LABEL_MASKS("%F", "", strAlbum, "%a")); // Filename, empty | Userdefined, dateAdded
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS:
+ {
+ AddSortMethod(SortByNone, 552, LABEL_MASKS(strTrack, "%a")); // Userdefined, dateAdded | empty, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavsongs")->m_viewMode);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ {
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%F", "", strAlbum, "%p")); // Filename, empty | Userdefined, last played
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode);
+ }
+ break;
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS:
+ {
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS(strTrack, "%p")); // Userdefined, last played | empty, empty
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode);
+ }
+ break;
+ case NODE_TYPE_ALBUM_TOP100:
+ {
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "", strAlbum, "%V")); // Filename, empty | Userdefined, Play count
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_SINGLES:
+ {
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D"));
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title - Artist, Rating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year
+ // original release date (singles can be re-released)
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ AddSortMethod(SortByOrigDate, 38079,
+ LABEL_MASKS("%T - %A", "%e")); // Title, Artist, Original Date
+ AddSortMethod(SortByDateAdded, 570,
+ LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS(strTrack, "%p")); // Userdefined, last played | empty, empty
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs");
+ SetSortMethod(viewState->m_sortDescription);
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_ALBUM_TOP100_SONGS:
+ case NODE_TYPE_SONG:
+ {
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty
+ AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D"));
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title - Artist, Rating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year
+ // original release date
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ AddSortMethod(SortByOrigDate, 38079,
+ LABEL_MASKS("%T - %A", "%e")); // Title, Artist, Original Date
+ AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS(strTrack, "%p")); // Userdefined, last played | empty, empty
+ AddSortMethod(SortByBPM, 38080, LABEL_MASKS(strTrack, "%f")); // Userdefined, bpm, empty,empty
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs");
+ // the "All Albums" entries always default to SortByAlbum as this is most logical - user can always
+ // change it and the change will be saved for this particular path
+ if (dir.IsAllItem(items.GetPath()))
+ SetSortMethod(SortByAlbum);
+ else
+ SetSortMethod(viewState->m_sortDescription);
+
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_SONG_TOP100:
+ {
+ AddSortMethod(SortByNone, 576, LABEL_MASKS("%T - %A", "%V"));
+ SetSortMethod(SortByPlaycount);
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavsongs")->m_viewMode);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_DISC:
+ {
+ AddSortMethod(SortByNone, 427, LABEL_MASKS("%L")); // Use the existing label
+ SetSortMethod(SortByNone);
+ }
+ break;
+ default:
+ break;
+ }
+
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateMusicDatabase::SaveViewState()
+{
+ CMusicDatabaseDirectory dir;
+ NODE_TYPE NodeType=dir.GetDirectoryChildType(m_items.GetPath());
+
+ switch (NodeType)
+ {
+ case NODE_TYPE_ARTIST:
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavartists"));
+ break;
+ case NODE_TYPE_ALBUM:
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavalbums"));
+ break;
+ case NODE_TYPE_SINGLES:
+ case NODE_TYPE_SONG:
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavsongs"));
+ break;
+ default:
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV);
+ break;
+ }
+}
+
+CGUIViewStateMusicSmartPlaylist::CGUIViewStateMusicSmartPlaylist(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ SortAttribute sortAttribute = SortAttributeNone;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttribute = SortAttributeIgnoreArticle;
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName);
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs");
+
+ if (items.GetContent() == "songs" || items.GetContent() == "mixed")
+ {
+ std::string strTrack=settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty
+ AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D"));
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title, Artist, Rating| empty, empty
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year
+ AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ AddSortMethod(SortByOrigDate, 38079,
+ LABEL_MASKS("%T - %A", "%e")); // Title - Artist, original date, empty, empty
+ AddSortMethod(SortByBPM, 38080,
+ LABEL_MASKS("%T - %A", "%f")); // Title - Artist, bpm, empty, empty
+
+ if (items.IsSmartPlayList() || items.IsLibraryFolder())
+ AddPlaylistOrder(items, LABEL_MASKS(strTrack, "%D"));
+ else
+ {
+ SetSortMethod(viewState->m_sortDescription);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavsongs")->m_viewMode);
+ }
+ else if (items.GetContent() == "albums")
+ {
+ std::string strAlbum = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_strMusicLibraryAlbumFormat;
+ if (strAlbum.empty())
+ strAlbum = "%B"; // album
+ // album
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined (default=%B), Artist
+ // artist
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%F", "", strAlbum, "%A")); // Filename, empty | Userdefined, Artist
+ // artist / year
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%F", "", strAlbum, "%A / %Y")); // Filename, empty | Userdefined, Artist / Year
+ // discs
+ AddSortMethod(
+ SortByTotalDiscs, sortAttribute, 38077,
+ LABEL_MASKS("%F", "", strAlbum, "%b")); // Filename, empty | Userdefined, Total discs
+ // year
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%F", "", strAlbum, "%Y"));
+ // original release date
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ AddSortMethod(
+ SortByOrigDate, 38079,
+ LABEL_MASKS("%F", "", strAlbum, "%e")); // Filename, empty | Userdefined, Original date
+ // album date added
+ AddSortMethod(SortByDateAdded, sortAttribute, 570, LABEL_MASKS("%F", "", strAlbum, "%a")); // Filename, empty | Userdefined, dateAdded
+ // play count
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%F", "", strAlbum, "%V")); // Filename, empty | Userdefined, Play count
+ // last played
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%F", "", strAlbum, "%p")); // Filename, empty | Userdefined, last played
+ // rating
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%F", "", strAlbum, "%R")); // Filename, empty | Userdefined, Rating
+ // userrating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%F", "", strAlbum, "%r")); // Filename, empty | Userdefined, UserRating
+
+ if (items.IsSmartPlayList() || items.IsLibraryFolder())
+ AddPlaylistOrder(items, LABEL_MASKS("%F", "", strAlbum, "%D"));
+ else
+ {
+ SetSortMethod(viewState->m_sortDescription);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("musicnavalbums")->m_viewMode);
+ }
+ else
+ {
+ CLog::Log(LOGERROR,"Music Smart Playlist must be one of songs, mixed or albums");
+ }
+
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateMusicSmartPlaylist::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV, CViewStateSettings::GetInstance().Get("musicnavsongs"));
+}
+
+CGUIViewStateMusicPlaylist::CGUIViewStateMusicPlaylist(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ SortAttribute sortAttribute = SortAttributeNone;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttribute = SortAttributeIgnoreArticle;
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName);
+
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ AddSortMethod(SortByPlaylistOrder, 559, LABEL_MASKS(strTrack, "%D"));
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty
+ AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS(strTrack, "%D"));
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title - Artist, Duration| empty, empty
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%T - %A", "%R")); // Title - Artist, Rating| empty, empty
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating
+ SetSortMethod(SortByPlaylistOrder);
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicfiles");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateMusicPlaylist::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+CGUIViewStateWindowMusicNav::CGUIViewStateWindowMusicNav(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ SortAttribute sortAttribute = SortAttributeNone;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttribute = SortAttributeIgnoreArticle;
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sortAttribute = static_cast<SortAttribute>(sortAttribute | SortAttributeUseArtistSortName);
+
+ if (items.IsVirtualDirectoryRoot())
+ {
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Foldername, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+ }
+ else if (items.GetPath() == "special://musicplaylists/")
+ { // playlists list sorts by label only, ignoring folders
+ AddSortMethod(SortByLabel, SortAttributeIgnoreFolders, 551,
+ LABEL_MASKS("%F", "%D", "%L", "")); // Filename, Duration | Foldername, empty
+ SetSortMethod(SortByLabel);
+ }
+ else
+ {
+ if (items.IsVideoDb() && items.Size() > (settings->GetBool(CSettings::SETTING_FILELISTS_SHOWPARENTDIRITEMS)?1:0))
+ {
+ XFILE::VIDEODATABASEDIRECTORY::CQueryParams params;
+ XFILE::CVideoDatabaseDirectory::GetQueryParams(items[settings->GetBool(CSettings::SETTING_FILELISTS_SHOWPARENTDIRITEMS) ? 1 : 0]->GetPath(), params);
+ if (params.GetMVideoId() != -1)
+ {
+ AddSortMethod(SortByLabel, sortAttribute, 551, LABEL_MASKS("%T", "%Y")); // Filename, Duration | Foldername, empty
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y"));
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%Y"));
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y"));
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T", "%Y"));
+
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty
+ }
+ else
+ {
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS("%F", "%D", "%L", "")); // Filename, Duration | Foldername, empty
+ SetSortMethod(SortByLabel);
+ }
+ }
+ else
+ {
+ //In navigation of music files tag data is scanned whenever present and can be used as sort criteria
+ //hence sort methods available are similar to song node (not the same as only tag data)
+ //Unfortunately anything here appears at all levels of file navigation even if no song files there.
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_LIBRARYTRACKFORMAT);
+ if (strTrack.empty())
+ strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS(strTrack, "%D", "%L", ""), // Userdefined, Duration | FolderName, empty
+ settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ AddSortMethod(SortBySize, 553, LABEL_MASKS("%F", "%I", "%L", "%I")); // Filename, Size | Foldername, Size
+ AddSortMethod(SortByDate, 552, LABEL_MASKS("%F", "%J", "%L", "%J")); // Filename, Date | Foldername, Date
+ AddSortMethod(SortByFile, 561, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Label, empty
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%D")); // Userdefined, Duration| empty, empty
+ AddSortMethod(SortByTitle, sortAttribute, 556, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByAlbum, sortAttribute, 558, LABEL_MASKS("%B - %T - %A", "%D")); // Album, Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByArtist, sortAttribute, 557, LABEL_MASKS("%A - %T", "%D")); // Artist, Title, Duration| empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttribute, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist(year), Title, Year| empty, empty
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year
+
+ SetSortMethod(SortByLabel);
+ }
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("musicnavsongs");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+
+ SetSortOrder(SortOrderAscending);
+ }
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateWindowMusicNav::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_NAV);
+}
+
+void CGUIViewStateWindowMusicNav::AddOnlineShares()
+{
+ if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVirtualShares)
+ return;
+
+ VECSOURCES *musicSources = CMediaSourceSettings::GetInstance().GetSources("music");
+
+ for (int i = 0; i < (int)musicSources->size(); ++i)
+ {
+ CMediaSource share = musicSources->at(i);
+ }
+}
+
+VECSOURCES& CGUIViewStateWindowMusicNav::GetSources()
+{
+ // Setup shares we want to have
+ m_sources.clear();
+ CFileItemList items;
+
+ CDirectory::GetDirectory("library://music/", items, "", DIR_FLAG_DEFAULTS);
+ for (int i=0; i<items.Size(); ++i)
+ {
+ CFileItemPtr item=items[i];
+ CMediaSource share;
+ share.strName = item->GetLabel();
+ share.strPath = item->GetPath();
+ share.m_strThumbnailImage = item->GetArt("icon");
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ m_sources.push_back(share);
+ }
+
+ AddOnlineShares();
+
+ return CGUIViewStateWindowMusic::GetSources();
+}
+
+CGUIViewStateWindowMusicPlaylist::CGUIViewStateWindowMusicPlaylist(const CFileItemList& items) : CGUIViewStateWindowMusic(items)
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_NOWPLAYINGTRACKFORMAT);
+ if (strTrack.empty())
+ strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+
+ AddSortMethod(SortByNone, 551, LABEL_MASKS(strTrack, "%D", "%L", "")); // Userdefined, Duration | FolderName, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+
+ LoadViewState(items.GetPath(), WINDOW_MUSIC_PLAYLIST);
+}
+
+void CGUIViewStateWindowMusicPlaylist::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_MUSIC_PLAYLIST);
+}
+
+PLAYLIST::Id CGUIViewStateWindowMusicPlaylist::GetPlaylist() const
+{
+ return PLAYLIST::TYPE_MUSIC;
+}
+
+bool CGUIViewStateWindowMusicPlaylist::AutoPlayNextItem()
+{
+ return false;
+}
+
+bool CGUIViewStateWindowMusicPlaylist::HideParentDirItems()
+{
+ return true;
+}
+
+VECSOURCES& CGUIViewStateWindowMusicPlaylist::GetSources()
+{
+ m_sources.clear();
+ // Playlist share
+ CMediaSource share;
+ share.strPath = "playlistmusic://";
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ m_sources.push_back(share);
+
+ // CGUIViewState::GetSources would add music plugins
+ return m_sources;
+}
diff --git a/xbmc/music/GUIViewStateMusic.h b/xbmc/music/GUIViewStateMusic.h
new file mode 100644
index 0000000..7f7311a
--- /dev/null
+++ b/xbmc/music/GUIViewStateMusic.h
@@ -0,0 +1,85 @@
+/*
+ * 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 "view/GUIViewState.h"
+
+class CGUIViewStateWindowMusic : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateWindowMusic(const CFileItemList& items) : CGUIViewState(items) {}
+protected:
+ VECSOURCES& GetSources() override;
+ PLAYLIST::Id GetPlaylist() const override;
+ bool AutoPlayNextItem() override;
+ std::string GetLockType() override;
+ std::string GetExtensions() override;
+};
+
+class CGUIViewStateMusicSearch : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateMusicSearch(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateMusicDatabase : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateMusicDatabase(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateMusicSmartPlaylist : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateMusicSmartPlaylist(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateMusicPlaylist : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateMusicPlaylist(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateWindowMusicNav : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateWindowMusicNav(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ VECSOURCES& GetSources() override;
+
+private:
+ void AddOnlineShares();
+};
+
+class CGUIViewStateWindowMusicPlaylist : public CGUIViewStateWindowMusic
+{
+public:
+ explicit CGUIViewStateWindowMusicPlaylist(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ PLAYLIST::Id GetPlaylist() const override;
+ bool AutoPlayNextItem() override;
+ bool HideParentDirItems() override;
+ VECSOURCES& GetSources() override;
+};
diff --git a/xbmc/music/MusicDatabase.cpp b/xbmc/music/MusicDatabase.cpp
new file mode 100644
index 0000000..91ff208
--- /dev/null
+++ b/xbmc/music/MusicDatabase.cpp
@@ -0,0 +1,13822 @@
+/*
+ * 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 "MusicDatabase.h"
+
+#include "Album.h"
+#include "Artist.h"
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "Song.h"
+#include "TextureCache.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/Scraper.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/audiodecoder.h"
+#include "dbwrappers/dataset.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+#include "filesystem/Directory.h"
+#include "filesystem/DirectoryCache.h"
+#include "filesystem/File.h"
+#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicDbUrl.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/tags/MusicInfoTag.h"
+#include "network/Network.h"
+#include "network/cddb.h"
+#include "playlists/SmartPlayList.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/LegacyPathTranslation.h"
+#include "utils/MathUtils.h"
+#include "utils/Random.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <inttypes.h>
+
+using namespace XFILE;
+using namespace MUSICDATABASEDIRECTORY;
+using namespace KODI::MESSAGING;
+using namespace MUSIC_INFO;
+
+using ADDON::AddonPtr;
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+#define RECENTLY_PLAYED_LIMIT 25
+#define MIN_FULL_SEARCH_LENGTH 3
+
+#ifdef HAS_DVD_DRIVE
+using namespace CDDB;
+using namespace MEDIA_DETECT;
+#endif
+
+static void AnnounceRemove(const std::string& content, int id)
+{
+ CVariant data;
+ data["type"] = content;
+ data["id"] = id;
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ data["transaction"] = true;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnRemove", data);
+}
+
+static void AnnounceUpdate(const std::string& content, int id, bool added = false)
+{
+ CVariant data;
+ data["type"] = content;
+ data["id"] = id;
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ data["transaction"] = true;
+ if (added)
+ data["added"] = true;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnUpdate", data);
+}
+
+CMusicDatabase::CMusicDatabase(void)
+{
+ m_translateBlankArtist = true;
+}
+
+CMusicDatabase::~CMusicDatabase(void)
+{
+ EmptyCache();
+}
+
+bool CMusicDatabase::Open()
+{
+ return CDatabase::Open(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic);
+}
+
+void CMusicDatabase::CreateTables()
+{
+ CLog::Log(LOGINFO, "create artist table");
+ m_pDS->exec("CREATE TABLE artist ( idArtist integer primary key, "
+ " strArtist varchar(256), strMusicBrainzArtistID text, "
+ " strSortName text, "
+ " strType text, strGender text, strDisambiguation text, "
+ " strBorn text, strFormed text, strGenres text, strMoods text, "
+ " strStyles text, strInstruments text, strBiography text, "
+ " strDied text, strDisbanded text, strYearsActive text, "
+ " strImage text, "
+ " lastScraped varchar(20) default NULL, "
+ " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
+ " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
+ " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
+ // Create missing artist tag artist [Missing].
+ std::string strSQL =
+ PrepareSQL("INSERT INTO artist (idArtist, strArtist, strSortName, strMusicBrainzArtistID) "
+ "VALUES( %i, '%s', '%s', '%s' )",
+ BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_NAME.c_str(),
+ BLANKARTIST_FAKEMUSICBRAINZID.c_str());
+ m_pDS->exec(strSQL);
+
+ CLog::Log(LOGINFO, "create album table");
+ m_pDS->exec("CREATE TABLE album (idAlbum integer primary key, "
+ " strAlbum varchar(256), strMusicBrainzAlbumID text, "
+ " strReleaseGroupMBID text, "
+ " strArtistDisp text, strArtistSort text, strGenres text, "
+ " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
+ " bBoxedSet INTEGER NOT NULL DEFAULT 0, "
+ " bCompilation integer not null default '0', "
+ " strMoods text, strStyles text, strThemes text, "
+ " strReview text, strImage text, strLabel text, "
+ " strType text, "
+ " strReleaseStatus TEXT, "
+ " fRating FLOAT NOT NULL DEFAULT 0, "
+ " iVotes INTEGER NOT NULL DEFAULT 0, "
+ " iUserrating INTEGER NOT NULL DEFAULT 0, "
+ " lastScraped varchar(20) default NULL, "
+ " bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
+ " strReleaseType text, "
+ " iDiscTotal INTEGER NOT NULL DEFAULT 0, "
+ " iAlbumDuration INTEGER NOT NULL DEFAULT 0, "
+ " idInfoSetting INTEGER NOT NULL DEFAULT 0, "
+ " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
+
+ CLog::Log(LOGINFO, "create audiobook table");
+ m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
+ " strBook varchar(256), strAuthor text,"
+ " bookmark integer, file text,"
+ " dateAdded varchar (20) default NULL)");
+
+ CLog::Log(LOGINFO, "create album_artist table");
+ m_pDS->exec("CREATE TABLE album_artist (idArtist integer, idAlbum integer, iOrder integer, "
+ "strArtist text)");
+
+ CLog::Log(LOGINFO, "create album_source table");
+ m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
+
+ CLog::Log(LOGINFO, "create genre table");
+ m_pDS->exec("CREATE TABLE genre (idGenre integer primary key, strGenre varchar(256))");
+
+ CLog::Log(LOGINFO, "create path table");
+ m_pDS->exec("CREATE TABLE path (idPath integer primary key, strPath varchar(512), strHash text)");
+
+ CLog::Log(LOGINFO, "create source table");
+ m_pDS->exec(
+ "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
+
+ CLog::Log(LOGINFO, "create source_path table");
+ m_pDS->exec("CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
+
+ CLog::Log(LOGINFO, "create song table");
+ m_pDS->exec("CREATE TABLE song (idSong integer primary key, "
+ " idAlbum integer, idPath integer, "
+ " strArtistDisp text, strArtistSort text, strGenres text, strTitle varchar(512), "
+ " iTrack integer, iDuration integer, "
+ " strReleaseDate TEXT, strOrigReleaseDate TEXT, "
+ " strDiscSubtitle text, strFileName text, strMusicBrainzTrackID text, "
+ " iTimesPlayed integer, iStartOffset integer, iEndOffset integer, "
+ " lastplayed varchar(20) default NULL, "
+ " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
+ " userrating INTEGER NOT NULL DEFAULT 0, "
+ " comment text, mood text, iBPM INTEGER NOT NULL DEFAULT 0, "
+ " iBitRate INTEGER NOT NULL DEFAULT 0, "
+ " iSampleRate INTEGER NOT NULL DEFAULT 0, iChannels INTEGER NOT NULL DEFAULT 0, "
+ " strReplayGain text, "
+ " dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
+ CLog::Log(LOGINFO, "create song_artist table");
+ m_pDS->exec("CREATE TABLE song_artist (idArtist integer, idSong integer, idRole integer, iOrder "
+ "integer, strArtist text)");
+ CLog::Log(LOGINFO, "create song_genre table");
+ m_pDS->exec("CREATE TABLE song_genre (idGenre integer, idSong integer, iOrder integer)");
+
+ CLog::Log(LOGINFO, "create role table");
+ m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
+ m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default role
+
+ CLog::Log(LOGINFO, "create infosetting table");
+ m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, "
+ "strScraperPath TEXT, strSettings TEXT)");
+
+ CLog::Log(LOGINFO, "create discography table");
+ m_pDS->exec("CREATE TABLE discography (idArtist integer, strAlbum text, strYear text, "
+ "strReleaseGroupMBID TEXT)");
+
+ CLog::Log(LOGINFO, "create art table");
+ m_pDS->exec("CREATE TABLE art(art_id INTEGER PRIMARY KEY, "
+ "media_id INTEGER, media_type TEXT, type TEXT, url TEXT)");
+
+ CLog::Log(LOGINFO, "create versiontagscan table");
+ m_pDS->exec("CREATE TABLE versiontagscan "
+ "(idVersion INTEGER, iNeedsScan INTEGER, "
+ "lastscanned VARCHAR(20), "
+ "lastcleaned VARCHAR(20), "
+ "artistlinksupdated VARCHAR(20), "
+ "genresupdated VARCHAR(20))");
+ m_pDS->exec(PrepareSQL("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(%i, 0)",
+ GetSchemaVersion()));
+
+ CLog::Log(LOGINFO, "create removed_link table");
+ m_pDS->exec("CREATE TABLE removed_link (idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
+}
+
+void CMusicDatabase::CreateAnalytics()
+{
+ CLog::Log(LOGINFO, "{} - creating indices", __FUNCTION__);
+ m_pDS->exec("CREATE INDEX idxAlbum ON album(strAlbum(255))");
+ m_pDS->exec("CREATE INDEX idxAlbum_1 ON album(bCompilation)");
+ m_pDS->exec("CREATE UNIQUE INDEX idxAlbum_2 ON album(strMusicBrainzAlbumID(36))");
+ m_pDS->exec("CREATE INDEX idxAlbum_3 ON album(idInfoSetting)");
+
+ m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_1 ON album_artist ( idAlbum, idArtist )");
+ m_pDS->exec("CREATE UNIQUE INDEX idxAlbumArtist_2 ON album_artist ( idArtist, idAlbum )");
+
+ m_pDS->exec("CREATE INDEX idxGenre ON genre(strGenre(255))");
+
+ m_pDS->exec("CREATE INDEX idxArtist ON artist(strArtist(255))");
+ m_pDS->exec("CREATE UNIQUE INDEX idxArtist1 ON artist(strMusicBrainzArtistID(36))");
+ m_pDS->exec("CREATE INDEX idxArtist_2 ON artist(idInfoSetting)");
+
+ m_pDS->exec("CREATE INDEX idxPath ON path(strPath(255))");
+
+ m_pDS->exec("CREATE INDEX idxSource_1 ON source(strName(255))");
+ m_pDS->exec("CREATE INDEX idxSource_2 ON source(strMultipath(255))");
+
+ m_pDS->exec("CREATE UNIQUE INDEX idxSourcePath_1 ON source_path ( idSource, idPath)");
+
+ m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_1 ON album_source ( idSource, idAlbum )");
+ m_pDS->exec("CREATE UNIQUE INDEX idxAlbumSource_2 ON album_source ( idAlbum, idSource )");
+
+ m_pDS->exec("CREATE INDEX idxSong ON song(strTitle(255))");
+ m_pDS->exec("CREATE INDEX idxSong1 ON song(iTimesPlayed)");
+ m_pDS->exec("CREATE INDEX idxSong2 ON song(lastplayed)");
+ m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
+ m_pDS->exec("CREATE INDEX idxSong6 ON song( idPath, strFileName(255) )");
+ //Musicbrainz Track ID is not unique on an album, recordings are sometimes repeated e.g. "[silence]" or on a disc set
+ m_pDS->exec("CREATE UNIQUE INDEX idxSong7 ON song( idAlbum, iTrack, strMusicBrainzTrackID(36) )");
+
+ m_pDS->exec("CREATE UNIQUE INDEX idxSongArtist_1 ON song_artist ( idSong, idArtist, idRole )");
+ m_pDS->exec("CREATE INDEX idxSongArtist_2 ON song_artist ( idSong, idRole )");
+ m_pDS->exec("CREATE INDEX idxSongArtist_3 ON song_artist ( idArtist, idRole )");
+ m_pDS->exec("CREATE INDEX idxSongArtist_4 ON song_artist ( idRole )");
+
+ m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_1 ON song_genre ( idSong, idGenre )");
+ m_pDS->exec("CREATE UNIQUE INDEX idxSongGenre_2 ON song_genre ( idGenre, idSong )");
+
+ m_pDS->exec("CREATE INDEX idxRole on role(strRole(255))");
+
+ m_pDS->exec("CREATE INDEX idxDiscography_1 ON discography ( idArtist )");
+
+ m_pDS->exec("CREATE INDEX ix_art ON art(media_id, media_type(20), type(20))");
+
+ CLog::Log(LOGINFO, "create triggers");
+ m_pDS->exec("CREATE TRIGGER tgrDeleteAlbum AFTER delete ON album FOR EACH ROW BEGIN"
+ " DELETE FROM song WHERE song.idAlbum = old.idAlbum;"
+ " DELETE FROM album_artist WHERE album_artist.idAlbum = old.idAlbum;"
+ " DELETE FROM album_source WHERE album_source.idAlbum = old.idAlbum;"
+ " DELETE FROM art WHERE media_id=old.idAlbum AND media_type='album';"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrDeleteArtist AFTER delete ON artist FOR EACH ROW BEGIN"
+ " DELETE FROM album_artist WHERE album_artist.idArtist = old.idArtist;"
+ " DELETE FROM song_artist WHERE song_artist.idArtist = old.idArtist;"
+ " DELETE FROM discography WHERE discography.idArtist = old.idArtist;"
+ " DELETE FROM art WHERE media_id=old.idArtist AND media_type='artist';"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrDeleteSong AFTER delete ON song FOR EACH ROW BEGIN"
+ " DELETE FROM song_artist WHERE song_artist.idSong = old.idSong;"
+ " DELETE FROM song_genre WHERE song_genre.idSong = old.idSong;"
+ " DELETE FROM art WHERE media_id=old.idSong AND media_type='song';"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrDeleteSource AFTER delete ON source FOR EACH ROW BEGIN"
+ " DELETE FROM source_path WHERE source_path.idSource = old.idSource;"
+ " DELETE FROM album_source WHERE album_source.idSource = old.idSource;"
+ " END");
+
+ /* Maintain date new and last modified for songs, albums and artists using triggers
+ MySQL triggers cannot modify a table that is already being used by the statement that invoked
+ the trigger (to avoid recursion), but can set NEW column values before insert or update.
+ Meanwhile SQLite triggers cannot set NEW column values in that way, but can update same table.
+ Recursion avoided using WHEN but SQLite has PRAGMA recursive-triggers off by default anyway.
+ // ! @todo: once on SQLite v3.31 we could use a generated column for dateModified as real
+ */
+ bool bisMySQL = StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
+
+ if (!bisMySQL)
+ { // SQLite trigger syntax - AFTER INSERT/UPDATE
+ m_pDS->exec("CREATE TRIGGER tgrInsertSong AFTER INSERT ON song FOR EACH ROW BEGIN"
+ " UPDATE song SET dateNew = DATETIME('now') WHERE idSong = NEW.idSong"
+ " AND NEW.dateNew IS NULL;"
+ " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = NEW.idSong;"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateSong AFTER UPDATE ON song FOR EACH ROW"
+ " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
+ " UPDATE song SET dateModified = DATETIME('now') WHERE idSong = OLD.idSong;"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrInsertAlbum AFTER INSERT ON album FOR EACH ROW BEGIN"
+ " UPDATE album SET dateNew = DATETIME('now') WHERE idAlbum = NEW.idAlbum"
+ " AND NEW.dateNew IS NULL;"
+ " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = NEW.idAlbum;"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum AFTER UPDATE ON album FOR EACH ROW"
+ " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
+ " UPDATE album SET dateModified = DATETIME('now') WHERE idAlbum = OLD.idAlbum;"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrInsertArtist AFTER INSERT ON artist FOR EACH ROW BEGIN"
+ " UPDATE artist SET dateNew = DATETIME('now') WHERE idArtist = NEW.idArtist"
+ " AND NEW.dateNew IS NULL;"
+ " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = NEW.idArtist;"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateArtist AFTER UPDATE ON artist FOR EACH ROW"
+ " WHEN NEW.dateModified <= OLD.dateModified BEGIN"
+ " UPDATE artist SET dateModified = DATETIME('now') WHERE idArtist = OLD.idArtist;"
+ " END");
+
+ m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre"
+ " BEGIN UPDATE versiontagscan SET genresupdated = DATETIME('now');"
+ " END");
+ }
+ else
+ { // MySQL trigger syntax - BEFORE INSERT/UPDATE
+ m_pDS->exec("CREATE TRIGGER tgrInsertSong BEFORE INSERT ON song FOR EACH ROW BEGIN"
+ " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
+ " SET NEW.dateModified = now();"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateSong BEFORE UPDATE ON song FOR EACH ROW"
+ " SET NEW.dateModified = now()");
+
+ m_pDS->exec("CREATE TRIGGER tgrInsertAlbum BEFORE INSERT ON album FOR EACH ROW BEGIN"
+ " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
+ " SET NEW.dateModified = now();"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateAlbum BEFORE UPDATE ON album FOR EACH ROW"
+ " SET NEW.dateModified = now()");
+
+ m_pDS->exec("CREATE TRIGGER tgrInsertArtist BEFORE INSERT ON artist FOR EACH ROW BEGIN"
+ " IF NEW.dateNew IS NULL THEN SET NEW.dateNew = now(); END IF;"
+ " SET NEW.dateModified = now();"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrUpdateArtist BEFORE UPDATE ON artist FOR EACH ROW"
+ " SET NEW.dateModified = now()");
+
+ m_pDS->exec("CREATE TRIGGER tgrInsertGenre AFTER INSERT ON genre FOR EACH ROW"
+ " UPDATE versiontagscan SET genresupdated = now()");
+ }
+
+ // Triggers to maintain recent changes to album and song artist links in removed_link table
+ m_pDS->exec("CREATE TRIGGER tgrInsertSongArtist AFTER INSERT ON song_artist FOR EACH ROW BEGIN "
+ "DELETE FROM removed_link "
+ "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idSong AND idRole = NEW.idRole; "
+ "END");
+ m_pDS->exec("CREATE TRIGGER tgrInsertAlbumArtist AFTER INSERT ON album_artist FOR EACH ROW BEGIN "
+ "DELETE FROM removed_link "
+ "WHERE idArtist = NEW.idArtist AND idMedia = NEW.idAlbum AND idRole = -1; "
+ "END");
+ CreateRemovedLinkTriggers(); // DELETE ON song_artist and album_artist tables
+
+ // Create native functions stored in DB (MySQL/MariaDB only)
+ CreateNativeDBFunctions();
+
+ // we create views last to ensure all indexes are rolled in
+ CreateViews();
+}
+
+void CMusicDatabase::CreateRemovedLinkTriggers()
+{
+ // DELETE ON song_artist and album_artist tables need to be recreated after cleanup
+ m_pDS->exec("CREATE TRIGGER tgrDeleteSongArtist AFTER DELETE ON song_artist FOR EACH ROW BEGIN"
+ " INSERT INTO removed_link (idArtist, idMedia, idRole)"
+ " VALUES(OLD.idArtist, OLD.idSong, OLD.idRole);"
+ " END");
+ m_pDS->exec("CREATE TRIGGER tgrDeleteAlbumArtist AFTER DELETE ON album_artist FOR EACH ROW BEGIN"
+ " INSERT INTO removed_link (idArtist, idMedia, idRole)"
+ " VALUES(OLD.idArtist, OLD.idAlbum, -1);"
+ " END");
+}
+
+
+void CMusicDatabase::CreateViews()
+{
+ CLog::Log(LOGINFO, "create song view");
+ m_pDS->exec("CREATE VIEW songview AS SELECT "
+ " song.idSong AS idSong, "
+ " song.strArtistDisp AS strArtists,"
+ " song.strArtistSort AS strArtistSort,"
+ " song.strGenres AS strGenres,"
+ " strTitle, "
+ " iTrack, iDuration, "
+ " song.strReleaseDate as strReleaseDate, "
+ " song.strOrigReleaseDate as strOrigReleaseDate, "
+ " song.strDiscSubtitle as strDiscSubtitle, "
+ " strFileName, "
+ " strMusicBrainzTrackID, "
+ " iTimesPlayed, iStartOffset, iEndOffset, "
+ " lastplayed, "
+ " song.rating, "
+ " song.userrating, "
+ " song.votes, "
+ " comment, "
+ " song.idAlbum AS idAlbum, "
+ " strAlbum, "
+ " strPath, "
+ " album.strReleaseStatus as strReleaseStatus,"
+ " album.bCompilation AS bCompilation,"
+ " album.bBoxedSet AS bBoxedSet, "
+ " album.strArtistDisp AS strAlbumArtists,"
+ " album.strArtistSort AS strAlbumArtistSort,"
+ " album.strReleaseType AS strAlbumReleaseType,"
+ " song.mood as mood,"
+ " song.strReplayGain, "
+ " iBPM, "
+ " iBitRate, "
+ " iSampleRate, "
+ " iChannels, "
+ " album.iAlbumDuration AS iAlbumDuration, "
+ " album.iDiscTotal as iDiscTotal, "
+ " song.dateAdded as dateAdded, "
+ " song.dateNew AS dateNew, "
+ " song.dateModified AS dateModified "
+ "FROM song"
+ " JOIN album ON"
+ " song.idAlbum=album.idAlbum"
+ " JOIN path ON"
+ " song.idPath=path.idPath");
+
+ CLog::Log(LOGINFO, "create album view");
+ m_pDS->exec("CREATE VIEW albumview AS SELECT "
+ "album.idAlbum AS idAlbum, "
+ "strAlbum, "
+ "strMusicBrainzAlbumID, "
+ "strReleaseGroupMBID, "
+ "album.strArtistDisp AS strArtists, "
+ "album.strArtistSort AS strArtistSort, "
+ "album.strGenres AS strGenres, "
+ "album.strReleaseDate as strReleaseDate, "
+ "album.strOrigReleaseDate as strOrigReleaseDate, "
+ "album.bBoxedSet AS bBoxedSet, "
+ "album.strMoods AS strMoods, "
+ "album.strStyles AS strStyles, "
+ "strThemes, "
+ "strReview, "
+ "strLabel, "
+ "strType, "
+ "strReleaseStatus, "
+ "album.strImage as strImage, "
+ "album.fRating, "
+ "album.iUserrating, "
+ "album.iVotes, "
+ "bCompilation, "
+ "bScrapedMBID,"
+ "lastScraped,"
+ "dateAdded, dateNew, dateModified, "
+ "(SELECT ROUND(AVG(song.iTimesPlayed)) FROM song "
+ "WHERE song.idAlbum = album.idAlbum) AS iTimesPlayed, "
+ "strReleaseType, "
+ "iDiscTotal, "
+ "(SELECT MAX(song.lastplayed) FROM song "
+ "WHERE song.idAlbum = album.idAlbum) AS lastplayed, "
+ "iAlbumDuration "
+ "FROM album");
+
+ CLog::Log(LOGINFO, "create artist view");
+ m_pDS->exec("CREATE VIEW artistview AS SELECT"
+ " idArtist, strArtist, strSortName, "
+ " strMusicBrainzArtistID, "
+ " strType, strGender, strDisambiguation, "
+ " strBorn, strFormed, strGenres,"
+ " strMoods, strStyles, strInstruments, "
+ " strBiography, strDied, strDisbanded, "
+ " strYearsActive, strImage, "
+ " bScrapedMBID, lastScraped, "
+ " dateAdded, dateNew, dateModified "
+ "FROM artist");
+
+ CLog::Log(LOGINFO, "create albumartist view");
+ m_pDS->exec("CREATE VIEW albumartistview AS SELECT"
+ " album_artist.idAlbum AS idAlbum, "
+ " album_artist.idArtist AS idArtist, "
+ " 0 AS idRole, "
+ " 'AlbumArtist' AS strRole, "
+ " artist.strArtist AS strArtist, "
+ " artist.strSortName AS strSortName,"
+ " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
+ " album_artist.iOrder AS iOrder "
+ "FROM album_artist "
+ "JOIN artist ON "
+ " album_artist.idArtist = artist.idArtist");
+
+ CLog::Log(LOGINFO, "create songartist view");
+ m_pDS->exec("CREATE VIEW songartistview AS SELECT"
+ " song_artist.idSong AS idSong, "
+ " song_artist.idArtist AS idArtist, "
+ " song_artist.idRole AS idRole, "
+ " role.strRole AS strRole, "
+ " artist.strArtist AS strArtist, "
+ " artist.strSortName AS strSortName,"
+ " artist.strMusicBrainzArtistID AS strMusicBrainzArtistID, "
+ " song_artist.iOrder AS iOrder "
+ "FROM song_artist "
+ "JOIN artist ON "
+ " song_artist.idArtist = artist.idArtist "
+ "JOIN role ON "
+ " song_artist.idRole = role.idRole");
+}
+
+void CMusicDatabase::CreateNativeDBFunctions()
+{
+ // Create native functions in MySQL/MariaDB database only
+ if (!StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
+ "mysql"))
+ return;
+ CLog::Log(LOGINFO, "Create native MySQL/MariaDB functions");
+ /* Functions to do the natural number sorting and all ascii symbol char at top adjustments to
+ default utf8_general_ci collation that SQLite does via a collation sequence callback
+ function to StringUtils::AlphaNumericCompare
+ !@todo: the video needs these defined too for sorting in DB, then creation can be made common
+ */
+ // clang-format off
+ // udfFirstNumberPos finds the position of the first digit in a string
+ m_pDS->exec("DROP FUNCTION IF EXISTS udfFirstNumberPos");
+ m_pDS->exec("CREATE FUNCTION udfFirstNumberPos (instring VARCHAR(512))\n"
+ "RETURNS int \n"
+ "LANGUAGE SQL \n"
+ "DETERMINISTIC \n"
+ "NO SQL \n"
+ "SQL SECURITY INVOKER \n"
+ "BEGIN \n"
+ " DECLARE position int; \n"
+ " DECLARE tmppos int; \n"
+ " SET position = 5000; \n"
+ " SET tmppos = LOCATE('0', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('1', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('2', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('3', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('4', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('5', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('6', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('7', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('8', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " SET tmppos = LOCATE('9', instring); IF(tmppos > 0 AND tmppos < position) THEN SET position = tmppos; END IF;\n"
+ " IF(position = 5000) THEN RETURN 0; END IF;\n"
+ " RETURN position; \n"
+ "END\n");
+
+ // udfSymbolShift adds "/" (the last symbol before "0"), in front any of the chars input
+ m_pDS->exec("DROP FUNCTION IF EXISTS udfSymbolShift");
+ m_pDS->exec("CREATE FUNCTION udfSymbolShift(instring varchar(512), symbolChars char(25))\n"
+ "RETURNS varchar(1024)\n"
+ "LANGUAGE SQL\n"
+ "DETERMINISTIC\n"
+ "NO SQL\n"
+ "SQL SECURITY INVOKER\n"
+ "BEGIN\n"
+ " DECLARE sortString varchar(1024); -- Allow for every char to be symbol\n"
+ " DECLARE i int;\n"
+ " DECLARE symbolCharsLen int;\n"
+ " DECLARE symbol char(1);\n"
+ " SET sortString = instring;\n"
+ " SET i = 1;\n"
+ " SET symbolCharsLen = CHAR_LENGTH(symbolChars);\n"
+ " WHILE(i <= symbolCharsLen) DO\n"
+ " SET symbol = SUBSTRING(symbolChars, i, 1);\n"
+ " SET sortString = REPLACE(sortString, symbol, CONCAT('/', symbol));\n"
+ " SET i = i + 1;\n"
+ " END WHILE;\n"
+ " RETURN sortString;\n"
+ "END\n");
+
+ // udfNaturalSortFormat - provide natural number sorting and ascii symbols above numbers
+ m_pDS->exec("DROP FUNCTION IF EXISTS udfNaturalSortFormat");
+ m_pDS->exec("CREATE FUNCTION udfNaturalSortFormat(instring varchar(512), numberLength int, "
+ "sameOrderChars char(25))\n"
+ "RETURNS varchar(1024)\n"
+ "LANGUAGE SQL\n"
+ "DETERMINISTIC\n"
+ "NO SQL\n"
+ "SQL SECURITY INVOKER\n"
+ "BEGIN\n"
+ " DECLARE sortString varchar(1024);\n"
+ " DECLARE shiftedString varchar(1024);\n"
+ " DECLARE inLength int;\n"
+ " DECLARE shiftedLength int;\n"
+ " DECLARE totalSympadLength int;\n"
+ " DECLARE symbolshifted512 varchar(1024);\n"
+ " DECLARE numStartIndex int; \n"
+ " DECLARE numEndIndex int; \n"
+ " DECLARE padLength int; \n"
+ " DECLARE totalPadLength int; \n"
+ " DECLARE i int; \n"
+ " DECLARE sameOrderCharsLen int;\n"
+ " SET totalPadLength = 0; \n"
+ " SET instring = TRIM(instring);\n"
+ " SET inLength = CHAR_LENGTH(inString);\n"
+ " SET sortString = instring; \n"
+ " SET numStartIndex = udfFirstNumberPos(instring); \n"
+ " SET numEndIndex = 0; \n"
+ " SET i = 1; \n"
+ " SET sameOrderCharsLen = CHAR_LENGTH(sameOrderChars); \n"
+ " WHILE(i <= sameOrderCharsLen) DO \n"
+ " SET sortString = REPLACE(sortString, SUBSTRING(sameOrderChars, i, 1), ' '); \n"
+ " SET i = i + 1; \n"
+ " END WHILE; \n"
+ " WHILE(numStartIndex <> 0) DO \n"
+ " SET numStartIndex = numStartIndex + numEndIndex; \n"
+ " SET numEndIndex = numStartIndex; \n"
+ " WHILE(udfFirstNumberPos(SUBSTRING(instring, numEndIndex, 1)) = 1) DO \n"
+ " SET numEndIndex = numEndIndex + 1; \n"
+ " END WHILE; \n"
+ " SET numEndIndex = numEndIndex - 1; \n"
+ " SET padLength = numberLength - (numEndIndex + 1 - numStartIndex); \n"
+ " IF padLength < 0 THEN \n"
+ " SET padLength = 0; \n"
+ " END IF; \n"
+ " IF inLength + totalPadLength + padlength > 1024 THEN \n"
+ " -- Padding more digits would be too long, pad this one just enough \n"
+ " SET padLength = 1024 - inLength - totalPadLength; \n"
+ " SET numStartIndex = 0; \n"
+ " END IF; \n"
+ " SET sortString = INSERT(sortString, numStartIndex + totalPadLength, 0, REPEAT('0', padLength)); \n"
+ " SET totalPadLength = totalPadLength + padLength; \n"
+ " IF numStartIndex <> 0 THEN \n"
+ " SET numStartIndex = udfFirstNumberPos(RIGHT(instring, inLength - numEndIndex)); \n"
+ " END IF; \n"
+ " END WHILE; \n"
+ " -- Handle symbol order inserting '/' to shift ascii symbols :;<=>?@[\\]^_ `{|}~ above 0 \n"
+ " -- when there is space as this could double string length. Note '\\' needs escaping \n"
+ " SET numStartIndex = 1; \n"
+ " SET numEndIndex = inLength + totalPadLength; \n"
+ " IF numEndIndex < 1024 THEN \n"
+ " SET shiftedLength = 0; \n"
+ " SET totalSympadLength = 0; \n"
+ " WHILE numStartIndex < numEndIndex AND totalSympadLength < 1024 DO \n"
+ " SET symbolshifted512 = udfSymbolShift(SUBSTRING(sortString, numStartIndex, 512), ':;<=>?@[\\\\]^_`{|}~'); \n"
+ " SET numStartIndex = numStartIndex + 512; \n"
+ " SET shiftedLength = CHAR_LENGTH(symbolshifted512); \n"
+ " IF totalSympadLength = 0 THEN \n"
+ " SET shiftedString = symbolshifted512; \n"
+ " ELSE \n"
+ " IF totalSympadLength + shiftedLength > 1024 THEN \n"
+ " SET shiftedLength = 1024 - totalSympadLength; \n"
+ " SET symbolshifted512 = LEFT(symbolshifted512, shiftedLength); \n"
+ " END IF; \n"
+ " SET shiftedString = CONCAT(shiftedString, symbolshifted512); \n"
+ " END IF; \n"
+ " SET totalSympadLength = totalSympadLength + shiftedLength; \n"
+ " END WHILE; \n"
+ " SET sortString = shiftedString; \n"
+ " END IF; \n"
+ " RETURN sortString; \n"
+ "END\n");
+ // clang-format on
+}
+
+void CMusicDatabase::SplitPath(const std::string& strFileNameAndPath,
+ std::string& strPath,
+ std::string& strFileName)
+{
+ URIUtils::Split(strFileNameAndPath, strPath, strFileName);
+ // Keep protocol options as part of the path
+ if (URIUtils::IsURL(strFileNameAndPath))
+ {
+ CURL url(strFileNameAndPath);
+ if (!url.GetProtocolOptions().empty())
+ strPath += "|" + url.GetProtocolOptions();
+ }
+}
+
+bool CMusicDatabase::AddAlbum(CAlbum& album, int idSource)
+{
+ BeginTransaction();
+ SetLibraryLastUpdated();
+
+ album.idAlbum = AddAlbum(album.strAlbum, //
+ album.strMusicBrainzAlbumID, //
+ album.strReleaseGroupMBID, //
+ album.GetAlbumArtistString(), //
+ album.GetAlbumArtistSort(), //
+ album.GetGenreString(), //
+ album.strReleaseDate, //
+ album.strOrigReleaseDate, //
+ album.bBoxedSet, //
+ album.strLabel, //
+ album.strType, //
+ album.strReleaseStatus, //
+ album.bCompilation, //
+ album.releaseType);
+
+ // Add the album artists
+ // Album must have at least one artist so set artist to [Missing]
+ if (album.artistCredits.empty())
+ AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0);
+ for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end();
+ ++artistCredit)
+ {
+ artistCredit->idArtist =
+ AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
+ artistCredit->GetSortName());
+ AddAlbumArtist(artistCredit->idArtist, album.idAlbum, artistCredit->GetArtist(),
+ static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
+ }
+
+ // Add songs
+ for (auto song = album.songs.begin(); song != album.songs.end(); ++song)
+ {
+ song->idAlbum = album.idAlbum;
+
+ song->idSong = AddSong(song->idSong, //
+ song->dateNew, //
+ song->idAlbum, //
+ song->strTitle, //
+ song->strMusicBrainzTrackID, //
+ song->strFileName, //
+ song->strComment, //
+ song->strMood, //
+ song->strThumb, //
+ song->GetArtistString(), //
+ song->GetArtistSort(), //
+ song->genre, //
+ song->iTrack, //
+ song->iDuration, //
+ song->strReleaseDate, //
+ song->strOrigReleaseDate, //
+ song->strDiscSubtitle, //
+ song->iTimesPlayed, //
+ song->iStartOffset, song->iEndOffset, //
+ song->lastPlayed, //
+ song->rating, //
+ song->userrating, //
+ song->votes, //
+ song->iBPM, song->iBitRate, song->iSampleRate, song->iChannels, //
+ song->replayGain);
+
+ // Song must have at least one artist so set artist to [Missing]
+ if (song->artistCredits.empty())
+ AddSongArtist(BLANKARTIST_ID, song->idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0);
+
+ for (auto artistCredit = song->artistCredits.begin(); artistCredit != song->artistCredits.end();
+ ++artistCredit)
+ {
+ artistCredit->idArtist =
+ AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
+ artistCredit->GetSortName());
+ AddSongArtist(
+ artistCredit->idArtist, song->idSong, ROLE_ARTIST,
+ artistCredit->GetArtist(), // we don't have song artist breakdowns from scrapers, yet
+ static_cast<int>(std::distance(song->artistCredits.begin(), artistCredit)));
+ }
+ // Having added artist credits (maybe with MBID) add the other contributing artists (no MBID)
+ // and use COMPOSERSORT tag data to provide sort names for artists that are composers
+ AddSongContributors(song->idSong, song->GetContributors(), song->GetComposerSort());
+ }
+
+ // Set album duration as total of all songs on album.
+ // Folder layout may mean AddAlbum call has added more songs to an existing album
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT SUM(iDuration) FROM song WHERE idAlbum = %i", album.idAlbum);
+ int albumDuration = GetSingleValueInt(strSQL);
+ m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE idAlbum = %i", albumDuration,
+ album.idAlbum));
+
+ // Add album sources
+ if (idSource > 0)
+ AddAlbumSource(album.idAlbum, idSource);
+ else
+ {
+ // Use album path, or failing that song paths to determine sources for the album
+ AddAlbumSources(album.idAlbum, album.strPath);
+ }
+
+ for (const auto& albumArt : album.art)
+ SetArtForItem(album.idAlbum, MediaTypeAlbum, albumArt.first, albumArt.second);
+
+ // Set album disc total
+ m_pDS->exec(
+ PrepareSQL("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT iTrack >> 16) FROM song "
+ "WHERE song.idAlbum = album.idAlbum) WHERE idAlbum = %i",
+ album.idAlbum));
+ // Set a non-compilation album as a boxset if it has three or more distinct disc titles
+ if (!album.bBoxedSet && !album.bCompilation)
+ {
+ strSQL = PrepareSQL("SELECT COUNT(DISTINCT strDiscSubtitle) FROM song WHERE song.idAlbum = %i",
+ album.idAlbum);
+ int numTitles = GetSingleValueInt(strSQL);
+ if (numTitles >= 3)
+ {
+ strSQL = PrepareSQL("UPDATE album SET bBoxedSet=1 WHERE album.idAlbum=%i", album.idAlbum);
+ m_pDS->exec(strSQL);
+ }
+ }
+ m_pDS->exec(PrepareSQL("UPDATE album SET strReleaseDate = (SELECT DISTINCT strReleaseDate "
+ "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
+ album.idAlbum));
+ m_pDS->exec(
+ PrepareSQL("UPDATE album SET strOrigReleaseDate = (SELECT DISTINCT strOrigReleaseDate "
+ "FROM song WHERE song.idAlbum = album.idAlbum LIMIT 1) WHERE idAlbum = %i",
+ album.idAlbum));
+
+ std::string albumdateadded =
+ GetSingleValue("song", "MAX(dateAdded)", PrepareSQL("idAlbum = %i", album.idAlbum));
+ m_pDS->exec(PrepareSQL("UPDATE album SET dateAdded = '%s' WHERE idAlbum = %i",
+ albumdateadded.c_str(), album.idAlbum));
+
+ /* Update artist dateAdded values for artists involved in album as song or album artists.
+ Dateadded does NOT hold when the artist was added to the library (that is dateNew), but is
+ derived from song dateadded values which are usually file dates (or the last scan).
+ It is used to indicate those artists with recent media.
+ For artists that are neither album nor song artists (other roles only) dateadded will be null.
+ */
+ std::vector<std::string> artistIDs;
+ // Get distinct song and album artist IDs for this album
+ GetArtistsByAlbum(album.idAlbum, artistIDs);
+ std::string strIDs = "(" + StringUtils::Join(artistIDs, ",") + ")";
+ strSQL = PrepareSQL("UPDATE artist SET dateAdded = '%s' "
+ "WHERE idArtist IN %s AND (dateAdded < '%s' OR dateAdded IS NULL)",
+ albumdateadded.c_str(), strIDs.c_str(), albumdateadded.c_str());
+ m_pDS->exec(strSQL);
+
+ CommitTransaction();
+ return true;
+}
+
+bool CMusicDatabase::UpdateAlbum(CAlbum& album)
+{
+ BeginTransaction();
+ SetLibraryLastUpdated();
+
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ if (album.bBoxedSet)
+ {
+ bool isBoxset = IsAlbumBoxset(album.idAlbum);
+ if (!isBoxset)
+ { // not a boxset already so make sure we have enough discs & they all have titles
+ bool canBeBoxset = false;
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE idAlbum = %i", album.idAlbum);
+ int numDiscs = GetSingleValueInt(strSQL);
+ if (numDiscs >= 2)
+ {
+ canBeBoxset = true;
+ int discValue;
+ for (discValue = 1; discValue <= numDiscs; discValue++)
+ {
+ strSQL =
+ PrepareSQL("SELECT DISTINCT strDiscSubtitle FROM song WHERE song.idAlbum = %i AND "
+ "song.iTrack >> 16 = %i",
+ album.idAlbum, discValue);
+ std::string currentTitle = GetSingleValue(strSQL);
+ if (currentTitle.empty())
+ {
+ currentTitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discValue);
+ strSQL =
+ PrepareSQL("UPDATE song SET strDiscSubtitle = '%s' WHERE song.idAlbum = %i AND "
+ "song.iTrack >> 16 = %i",
+ currentTitle.c_str(), album.idAlbum, discValue);
+ ExecuteQuery(strSQL);
+ }
+ }
+ }
+ if (!canBeBoxset && album.bBoxedSet)
+ {
+ CLog::Log(LOGINFO, "{} : Album with id [{}] does not meet the requirements for a boxset.",
+ __FUNCTION__, album.idAlbum);
+ album.bBoxedSet = false;
+ }
+ }
+ }
+ UpdateAlbum(album.idAlbum, album.strAlbum, album.strMusicBrainzAlbumID, //
+ album.strReleaseGroupMBID, //
+ album.GetAlbumArtistString(), album.GetAlbumArtistSort(), //
+ album.GetGenreString(), //
+ StringUtils::Join(album.moods, itemSeparator), //
+ StringUtils::Join(album.styles, itemSeparator), //
+ StringUtils::Join(album.themes, itemSeparator), //
+ album.strReview, //
+ album.thumbURL.GetData(), //
+ album.strLabel, //
+ album.strType, //
+ album.strReleaseStatus, //
+ album.fRating, album.iUserrating, album.iVotes, //
+ album.strReleaseDate, //
+ album.strOrigReleaseDate, //
+ album.bBoxedSet, //
+ album.bCompilation, //
+ album.releaseType, //
+ album.bScrapedMBID);
+
+ if (!album.bArtistSongMerge)
+ {
+ // Album artist(s) already exist and names are not changing, but may have scraped Musicbrainz ids to add
+ for (const auto& artistCredit : album.artistCredits)
+ UpdateArtistScrapedMBID(artistCredit.GetArtistId(), artistCredit.GetMusicBrainzArtistID());
+ }
+ else
+ {
+ // Replace the album artists with those scraped or set by JSON
+ DeleteAlbumArtistsByAlbum(album.idAlbum);
+ // Album must have at least one artist so set artist to [Missing]
+ if (album.artistCredits.empty())
+ AddAlbumArtist(BLANKARTIST_ID, album.idAlbum, BLANKARTIST_NAME, 0);
+ for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end();
+ ++artistCredit)
+ {
+ artistCredit->idArtist =
+ AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
+ artistCredit->GetSortName(), true);
+ AddAlbumArtist(artistCredit->idArtist, album.idAlbum, artistCredit->GetArtist(),
+ static_cast<int>(std::distance(album.artistCredits.begin(), artistCredit)));
+ }
+ /* Replace the songs with those scraped or imported, but if new songs is empty
+ (such as when called from JSON) do not remove the original ones
+ Also updates nested data e.g. song artists, song genres and contributors
+ Do not check for artist link changes, that is done later for all songs and album
+ */
+ int albumDuration = 0;
+ for (auto& song : album.songs)
+ {
+ UpdateSong(song);
+ albumDuration += song.iDuration;
+ }
+ if (albumDuration > 0)
+ m_pDS->exec(PrepareSQL("UPDATE album SET iAlbumDuration = %i WHERE album.idAlbum = %i",
+ albumDuration, album.idAlbum));
+ }
+
+ if (!album.art.empty())
+ SetArtForItem(album.idAlbum, MediaTypeAlbum, album.art);
+
+ CheckArtistLinksChanged();
+
+ CommitTransaction();
+ return true;
+}
+
+void CMusicDatabase::NormaliseSongDates(std::string& strRelease, std::string& strOriginal)
+{
+ // Validate we have ISO8601 format date strings YYYY, YYYY-MM, or YYYY-MM-DD
+ int iDate;
+ iDate = StringUtils::DateStringToYYYYMMDD(strRelease);
+ if (iDate < 0)
+ strRelease.clear();
+ iDate = StringUtils::DateStringToYYYYMMDD(strOriginal);
+ if (iDate < 0)
+ strOriginal.clear();
+ // Avoid missing release or original values unless both invalid or empty
+ if (!strRelease.empty() && strOriginal.empty())
+ strOriginal = strRelease;
+ else if (strRelease.empty() && !strOriginal.empty())
+ strRelease = strOriginal;
+}
+
+int CMusicDatabase::AddSong(const int idSong,
+ const CDateTime& dtDateNew,
+ const int idAlbum,
+ const std::string& strTitle,
+ const std::string& strMusicBrainzTrackID,
+ const std::string& strPathAndFileName,
+ const std::string& strComment,
+ const std::string& strMood,
+ const std::string& strThumb,
+ const std::string& artistDisp,
+ const std::string& artistSort,
+ const std::vector<std::string>& genres,
+ int iTrack,
+ int iDuration,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ std::string& strDiscSubtitle,
+ const int iTimesPlayed,
+ int iStartOffset,
+ int iEndOffset,
+ const CDateTime& dtLastPlayed,
+ float rating,
+ int userrating,
+ int votes,
+ int iBPM,
+ int iBitRate,
+ int iSampleRate,
+ int iChannels,
+ const ReplayGain& replayGain)
+{
+ int idNew = -1;
+ std::string strSQL;
+ try
+ {
+ // We need at least the title
+ if (strTitle.empty())
+ return -1;
+
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string strPath, strFileName;
+ SplitPath(strPathAndFileName, strPath, strFileName);
+ int idPath = AddPath(strPath);
+
+ if (idSong <= 1)
+ {
+ if (!strMusicBrainzTrackID.empty())
+ strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
+ "idAlbum = %i AND iTrack=%i AND strMusicBrainzTrackID = '%s'",
+ idAlbum, iTrack, strMusicBrainzTrackID.c_str());
+ else
+ strSQL = PrepareSQL("SELECT idSong FROM song WHERE "
+ "idAlbum=%i AND strFileName='%s' AND strTitle='%s' AND iTrack=%i "
+ "AND strMusicBrainzTrackID IS NULL",
+ idAlbum, strFileName.c_str(), strTitle.c_str(), iTrack);
+
+ if (!m_pDS->query(strSQL))
+ return -1;
+ }
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+
+ // As all discs in a boxset have to have a title, generate one in the form of 'Disc N'
+ bool isBoxset = IsAlbumBoxset(idAlbum);
+ if (isBoxset && strDiscSubtitle.empty())
+ {
+ int discno = iTrack >> 16;
+ strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discno);
+ }
+
+ // Validate ISO8601 dates and ensure none missing
+ std::string strRelease = strReleaseDate;
+ std::string strOriginal = strOrigReleaseDate;
+ NormaliseSongDates(strRelease, strOriginal);
+
+ // Get dateAdded from music file timestamp
+ std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
+
+ strSQL = "INSERT INTO song ("
+ "idSong, dateNew, idAlbum, idPath, strArtistDisp, "
+ "strTitle, iTrack, iDuration, "
+ "strReleaseDate, strOrigReleaseDate, iBPM, "
+ "iBitrate, iSampleRate, iChannels, "
+ "strDiscSubtitle, strFileName, dateAdded, "
+ "strMusicBrainzTrackID, strArtistSort, "
+ "iTimesPlayed, iStartOffset, iEndOffset, "
+ "lastplayed, rating, userrating, votes, comment, mood, strReplayGain) ";
+
+ if (idSong <= 0)
+ // Song ID is autoincremented and dateNew set by trigger
+ strSQL += PrepareSQL("VALUES (NULL, NULL, ");
+ else
+ //Reuse song Id and original date when the Id added
+ strSQL += PrepareSQL("VALUES (%i, '%s', ", idSong, dtDateNew.GetAsDBDateTime().c_str());
+
+ strSQL +=
+ PrepareSQL("%i, %i, '%s', '%s', %i, %i, '%s', '%s', %i, %i, %i, %i,'%s', '%s', '%s' ",
+ idAlbum, idPath, artistDisp.c_str(), strTitle.c_str(), iTrack, iDuration,
+ strRelease.c_str(), strOriginal.c_str(), iBPM, iBitRate, iSampleRate,
+ iChannels, strDiscSubtitle.c_str(), strFileName.c_str(), strDateMedia.c_str());
+
+ if (strMusicBrainzTrackID.empty())
+ strSQL += PrepareSQL(",NULL");
+ else
+ strSQL += PrepareSQL(",'%s'", strMusicBrainzTrackID.c_str());
+ if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
+ strSQL += PrepareSQL(",NULL");
+ else
+ strSQL += PrepareSQL(",'%s'", artistSort.c_str());
+
+ if (dtLastPlayed.IsValid())
+ strSQL += PrepareSQL(",%i,%i,%i,'%s', %.1f, %i, %i, '%s','%s', '%s')", //
+ iTimesPlayed, iStartOffset, iEndOffset,
+ dtLastPlayed.GetAsDBDateTime().c_str(), //
+ static_cast<double>(rating), userrating, votes, //
+ strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
+ else
+ strSQL += PrepareSQL(",%i,%i,%i,NULL, %.1f, %i, %i,'%s', '%s', '%s')", //
+ iTimesPlayed, iStartOffset, iEndOffset, //
+ static_cast<double>(rating), userrating, votes, //
+ strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
+ m_pDS->exec(strSQL);
+ if (idSong <= 0)
+ idNew = (int)m_pDS->lastinsertid();
+ else
+ idNew = idSong;
+ }
+ else
+ {
+ idNew = m_pDS->fv("idSong").get_asInt();
+ m_pDS->close();
+ UpdateSong(idNew, //
+ strTitle, //
+ strMusicBrainzTrackID, //
+ strPathAndFileName, //
+ strComment, //
+ strMood, //
+ strThumb, //
+ artistDisp, //
+ artistSort, //
+ genres, //
+ iTrack, //
+ iDuration, //
+ strReleaseDate, //
+ strOrigReleaseDate, //
+ strDiscSubtitle, //
+ iTimesPlayed, //
+ iStartOffset, iEndOffset, //
+ dtLastPlayed, //
+ rating, userrating, votes, //
+ replayGain, //
+ iBPM, iBitRate, iSampleRate, iChannels);
+ }
+ if (!strThumb.empty())
+ SetArtForItem(idNew, MediaTypeSong, "thumb", strThumb);
+
+ // Song genres added, and genre string updated to use the standardised genre names
+ AddSongGenres(idNew, genres);
+
+ AnnounceUpdate(MediaTypeSong, idNew, true);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to addsong ({})", strSQL);
+ }
+ return idNew;
+}
+
+bool CMusicDatabase::GetSong(int idSong, CSong& song)
+{
+ try
+ {
+ song.Clear();
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL =
+ PrepareSQL("SELECT songview.*,songartistview.* FROM songview "
+ " JOIN songartistview ON songview.idSong = songartistview.idSong "
+ " WHERE songview.idSong = %i "
+ " ORDER BY songartistview.idRole, songartistview.iOrder",
+ idSong);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ int songArtistOffset = song_enumCount;
+
+ song = GetSongFromDataset(m_pDS->get_sql_record());
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
+ if (idSongArtistRole == ROLE_ARTIST)
+ song.artistCredits.emplace_back(GetArtistCreditFromDataset(record, songArtistOffset));
+ else
+ song.AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::UpdateSong(CSong& song, bool bArtists /*= true*/, bool bArtistLinks /*= true*/)
+{
+ int result = UpdateSong(song.idSong,
+ song.strTitle, //
+ song.strMusicBrainzTrackID, //
+ song.strFileName, //
+ song.strComment, //
+ song.strMood, //
+ song.strThumb, //
+ song.GetArtistString(), //
+ song.GetArtistSort(), //
+ song.genre, //
+ song.iTrack, //
+ song.iDuration, //
+ song.strReleaseDate, //
+ song.strOrigReleaseDate, //
+ song.strDiscSubtitle, //
+ song.iTimesPlayed, //
+ song.iStartOffset, song.iEndOffset, //
+ song.lastPlayed, //
+ song.rating, song.userrating, song.votes, //
+ song.replayGain, //
+ song.iBPM, song.iBitRate, song.iSampleRate, song.iChannels);
+ if (result < 0)
+ return false;
+
+ // Replace Song genres and update genre string using the standardised genre names
+ AddSongGenres(song.idSong, song.genre);
+ if (bArtists)
+ {
+ //Replace song artists and contributors
+ DeleteSongArtistsBySong(song.idSong);
+ // Song must have at least one artist so set artist to [Missing]
+ if (song.artistCredits.empty())
+ AddSongArtist(BLANKARTIST_ID, song.idSong, ROLE_ARTIST, BLANKARTIST_NAME, 0);
+ for (auto artistCredit = song.artistCredits.begin(); artistCredit != song.artistCredits.end();
+ ++artistCredit)
+ {
+ artistCredit->idArtist =
+ AddArtist(artistCredit->GetArtist(), artistCredit->GetMusicBrainzArtistID(),
+ artistCredit->GetSortName());
+ AddSongArtist(artistCredit->idArtist, song.idSong, ROLE_ARTIST, artistCredit->GetArtist(),
+ static_cast<int>(std::distance(song.artistCredits.begin(), artistCredit)));
+ }
+ // Having added artist credits (maybe with MBID) add the other contributing artists (MBID unknown)
+ // and use COMPOSERSORT tag data to provide sort names for artists that are composers
+ AddSongContributors(song.idSong, song.GetContributors(), song.GetComposerSort());
+
+ if (bArtistLinks)
+ CheckArtistLinksChanged();
+ }
+
+ return true;
+}
+
+int CMusicDatabase::UpdateSong(int idSong,
+ const std::string& strTitle,
+ const std::string& strMusicBrainzTrackID,
+ const std::string& strPathAndFileName,
+ const std::string& strComment,
+ const std::string& strMood,
+ const std::string& strThumb,
+ const std::string& artistDisp,
+ const std::string& artistSort,
+ const std::vector<std::string>& genres,
+ int iTrack,
+ int iDuration,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ const std::string& strDiscSubtitle,
+ int iTimesPlayed,
+ int iStartOffset,
+ int iEndOffset,
+ const CDateTime& dtLastPlayed,
+ float rating,
+ int userrating,
+ int votes,
+ const ReplayGain& replayGain,
+ int iBPM,
+ int iBitRate,
+ int iSampleRate,
+ int iChannels)
+{
+ if (idSong < 0)
+ return -1;
+
+ std::string strSQL;
+ std::string strPath, strFileName;
+ SplitPath(strPathAndFileName, strPath, strFileName);
+ int idPath = AddPath(strPath);
+
+ // Validate ISO8601 dates and ensure none missing
+ std::string strRelease = strReleaseDate;
+ std::string strOriginal = strOrigReleaseDate;
+ NormaliseSongDates(strRelease, strOriginal);
+
+ std::string strDateMedia = GetMediaDateFromFile(strPathAndFileName);
+
+ strSQL = PrepareSQL(
+ "UPDATE song SET idPath = %i, strArtistDisp = '%s', strGenres = '%s', "
+ " strTitle = '%s', iTrack = %i, iDuration = %i, "
+ "strReleaseDate = '%s', strOrigReleaseDate = '%s', strDiscSubtitle = '%s', "
+ "strFileName = '%s', iBPM = %i, iBitrate = %i, iSampleRate = %i, iChannels = %i, "
+ "dateAdded = '%s'",
+ idPath, artistDisp.c_str(),
+ StringUtils::Join(
+ genres,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator)
+ .c_str(),
+ strTitle.c_str(), iTrack, iDuration, strRelease.c_str(), strOriginal.c_str(),
+ strDiscSubtitle.c_str(), strFileName.c_str(), iBPM, iBitRate, iSampleRate, iChannels,
+ strDateMedia.c_str());
+ if (strMusicBrainzTrackID.empty())
+ strSQL += PrepareSQL(", strMusicBrainzTrackID = NULL");
+ else
+ strSQL += PrepareSQL(", strMusicBrainzTrackID = '%s'", strMusicBrainzTrackID.c_str());
+ if (artistSort.empty() || artistSort.compare(artistDisp) == 0)
+ strSQL += PrepareSQL(", strArtistSort = NULL");
+ else
+ strSQL += PrepareSQL(", strArtistSort = '%s'", artistSort.c_str());
+
+ strSQL += PrepareSQL(", iStartOffset = %i, iEndOffset = %i, rating = %.1f, userrating = %i, "
+ "votes = %i, comment = '%s', mood = '%s', strReplayGain = '%s' ",
+ iStartOffset, iEndOffset, static_cast<double>(rating), userrating, votes,
+ strComment.c_str(), strMood.c_str(), replayGain.Get().c_str());
+
+ if (dtLastPlayed.IsValid())
+ strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
+ dtLastPlayed.GetAsDBDateTime().c_str());
+ else if (iTimesPlayed > 0)
+ strSQL += PrepareSQL(", iTimesPlayed = %i, lastplayed = '%s' ", iTimesPlayed,
+ CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
+ else
+ strSQL += ", iTimesPlayed = 0, lastplayed = NULL ";
+ strSQL += PrepareSQL("WHERE idSong = %i", idSong);
+
+ bool status = ExecuteQuery(strSQL);
+
+ if (status)
+ AnnounceUpdate(MediaTypeSong, idSong);
+ return idSong;
+}
+
+int CMusicDatabase::AddAlbum(const std::string& strAlbum,
+ const std::string& strMusicBrainzAlbumID,
+ const std::string& strReleaseGroupMBID,
+ const std::string& strArtist,
+ const std::string& strArtistSort,
+ const std::string& strGenre,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ bool bBoxedSet,
+ const std::string& strRecordLabel,
+ const std::string& strType,
+ const std::string& strReleaseStatus,
+ bool bCompilation,
+ CAlbum::ReleaseType releaseType)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ if (!strMusicBrainzAlbumID.empty())
+ strSQL = PrepareSQL("SELECT * FROM album WHERE strMusicBrainzAlbumID = '%s'",
+ strMusicBrainzAlbumID.c_str());
+ else
+ strSQL = PrepareSQL("SELECT * FROM album "
+ "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' "
+ "AND strMusicBrainzAlbumID IS NULL",
+ strArtist.c_str(), strAlbum.c_str());
+ m_pDS->query(strSQL);
+ std::string strCheckFlag = strType;
+ StringUtils::ToLower(strCheckFlag);
+ if (strCheckFlag.find("boxset") != std::string::npos) //boxset flagged in album type
+ bBoxedSet = true;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ // Does not exist, add it
+ strSQL =
+ PrepareSQL("INSERT INTO album (idAlbum, strAlbum, strArtistDisp, strGenres, "
+ "strReleaseDate, strOrigReleaseDate, bBoxedSet, "
+ "strLabel, strType, strReleaseStatus, bCompilation, strReleaseType, "
+ "strMusicBrainzAlbumID, "
+ "strReleaseGroupMBID, strArtistSort) "
+ "values(NULL, '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', '%s', %i, '%s'",
+ strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(), //
+ strReleaseDate.c_str(), strOrigReleaseDate.c_str(), bBoxedSet, //
+ strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(), //
+ bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str());
+
+ if (strMusicBrainzAlbumID.empty())
+ strSQL += PrepareSQL(", NULL");
+ else
+ strSQL += PrepareSQL(",'%s'", strMusicBrainzAlbumID.c_str());
+ if (strReleaseGroupMBID.empty())
+ strSQL += PrepareSQL(", NULL");
+ else
+ strSQL += PrepareSQL(",'%s'", strReleaseGroupMBID.c_str());
+ if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
+ strSQL += PrepareSQL(", NULL");
+ else
+ strSQL += PrepareSQL(", '%s'", strArtistSort.c_str());
+ strSQL += ")";
+ m_pDS->exec(strSQL);
+
+ return (int)m_pDS->lastinsertid();
+ }
+ else
+ {
+ /* Exists in our database and being re-scanned from tags, so we should update it as the details
+ may have changed.
+
+ Note that for multi-folder albums this will mean the last folder scanned will have the information
+ stored for it. Most values here should be the same across all songs anyway, but it does mean
+ that if there's any inconsistencies then only the last folders information will be taken.
+
+ We make sure we clear out the link tables (album artists, album sources) and we reset
+ the last scraped time to make sure that online metadata is re-fetched. */
+ int idAlbum = m_pDS->fv("idAlbum").get_asInt();
+ m_pDS->close();
+
+ strSQL = "UPDATE album SET ";
+ if (!strMusicBrainzAlbumID.empty())
+ strSQL += PrepareSQL("strAlbum = '%s', strArtistDisp = '%s', ", //
+ strAlbum.c_str(), strArtist.c_str());
+ if (strReleaseGroupMBID.empty())
+ strSQL += PrepareSQL(" strReleaseGroupMBID = NULL,");
+ else
+ strSQL += PrepareSQL(" strReleaseGroupMBID ='%s', ", strReleaseGroupMBID.c_str());
+ if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
+ strSQL += PrepareSQL(" strArtistSort = NULL");
+ else
+ strSQL += PrepareSQL(" strArtistSort = '%s'", strArtistSort.c_str());
+
+ strSQL +=
+ PrepareSQL(", strGenres = '%s', strReleaseDate= '%s', strOrigReleaseDate= '%s', "
+ "bBoxedSet=%i, strLabel = '%s', strType = '%s', strReleaseStatus = '%s', "
+ "bCompilation=%i, strReleaseType = '%s', "
+ "lastScraped = NULL "
+ "WHERE idAlbum=%i",
+ strGenre.c_str(), strReleaseDate.c_str(), strOrigReleaseDate.c_str(), //
+ bBoxedSet, strRecordLabel.c_str(), strType.c_str(), strReleaseStatus.c_str(),
+ bCompilation, CAlbum::ReleaseTypeToString(releaseType).c_str(), //
+ idAlbum);
+ m_pDS->exec(strSQL);
+ DeleteAlbumArtistsByAlbum(idAlbum);
+ DeleteAlbumSources(idAlbum);
+ return idAlbum;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
+ }
+
+ return -1;
+}
+
+int CMusicDatabase::UpdateAlbum(int idAlbum,
+ const std::string& strAlbum,
+ const std::string& strMusicBrainzAlbumID,
+ const std::string& strReleaseGroupMBID,
+ const std::string& strArtist,
+ const std::string& strArtistSort,
+ const std::string& strGenre,
+ const std::string& strMoods,
+ const std::string& strStyles,
+ const std::string& strThemes,
+ const std::string& strReview,
+ const std::string& strImage,
+ const std::string& strLabel,
+ const std::string& strType,
+ const std::string& strReleaseStatus,
+ float fRating,
+ int iUserrating,
+ int iVotes,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ bool bBoxedSet,
+ bool bCompilation,
+ CAlbum::ReleaseType releaseType,
+ bool bScrapedMBID)
+{
+ if (idAlbum < 0)
+ return -1;
+
+ // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
+ // Truncate value cleaning up xml when URLs exceeds this
+ std::string strImageURLs = strImage;
+ if (StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
+ "mysql"))
+ TrimImageURLs(strImageURLs, 65535);
+
+ std::string strSQL;
+ strSQL = PrepareSQL("UPDATE album SET "
+ " strAlbum = '%s', strArtistDisp = '%s', strGenres = '%s', "
+ " strMoods = '%s', strStyles = '%s', strThemes = '%s', "
+ " strReview = '%s', strImage = '%s', strLabel = '%s', "
+ " strType = '%s', fRating = %f, iUserrating = %i, iVotes = %i,"
+ " strReleaseDate= '%s', strOrigReleaseDate= '%s', "
+ " bBoxedSet = %i, bCompilation = %i,"
+ " strReleaseType = '%s', strReleaseStatus = '%s', "
+ " lastScraped = '%s', bScrapedMBID = %i",
+ strAlbum.c_str(), strArtist.c_str(), strGenre.c_str(), //
+ strMoods.c_str(), strStyles.c_str(), strThemes.c_str(), //
+ strReview.c_str(), strImageURLs.c_str(), strLabel.c_str(), //
+ strType.c_str(), static_cast<double>(fRating), iUserrating, iVotes, //
+ strReleaseDate.c_str(), strOrigReleaseDate.c_str(), //
+ bBoxedSet, bCompilation, //
+ CAlbum::ReleaseTypeToString(releaseType).c_str(), strReleaseStatus.c_str(), //
+ CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), bScrapedMBID);
+ if (strMusicBrainzAlbumID.empty())
+ strSQL += PrepareSQL(", strMusicBrainzAlbumID = NULL");
+ else
+ strSQL += PrepareSQL(", strMusicBrainzAlbumID = '%s'", strMusicBrainzAlbumID.c_str());
+ if (strReleaseGroupMBID.empty())
+ strSQL += PrepareSQL(", strReleaseGroupMBID = NULL");
+ else
+ strSQL += PrepareSQL(", strReleaseGroupMBID = '%s'", strReleaseGroupMBID.c_str());
+ if (strArtistSort.empty() || strArtistSort.compare(strArtist) == 0)
+ strSQL += PrepareSQL(", strArtistSort = NULL");
+ else
+ strSQL += PrepareSQL(", strArtistSort = '%s'", strArtistSort.c_str());
+
+ strSQL += PrepareSQL(" WHERE idAlbum = %i", idAlbum);
+
+ bool status = ExecuteQuery(strSQL);
+ if (status)
+ AnnounceUpdate(MediaTypeAlbum, idAlbum);
+ return idAlbum;
+}
+
+bool CMusicDatabase::GetAlbum(int idAlbum, CAlbum& album, bool getSongs /* = true */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (idAlbum == -1)
+ return false; // not in the database
+
+ //Get album, song and album song info data using separate queries/datasets because we can have
+ //multiple roles per artist for songs and that makes a single combined join impractical
+ //Get album data
+ std::string sql;
+ sql = PrepareSQL("SELECT albumview.*,albumartistview.* "
+ " FROM albumview "
+ " JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
+ " WHERE albumview.idAlbum = %ld "
+ " ORDER BY albumartistview.iOrder",
+ idAlbum);
+
+ CLog::Log(LOGDEBUG, "{}", sql);
+ if (!m_pDS->query(sql))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ int albumArtistOffset = album_enumCount;
+
+ album = GetAlbumFromDataset(m_pDS->get_sql_record(), 0,
+ true); // true to grab and parse the imageURL
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ // Album artists always have role = 0 (idRole and strRole columns are in albumartistview to match columns of songartistview)
+ // so there is only one row in the result set for each artist credit.
+ album.artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ //Get song data
+ if (getSongs)
+ {
+ sql = PrepareSQL("SELECT songview.*, songartistview.*"
+ " FROM songview "
+ " JOIN songartistview ON songview.idSong = songartistview.idSong "
+ " WHERE songview.idAlbum = %ld "
+ " ORDER BY songview.iTrack, songartistview.idRole, songartistview.iOrder",
+ idAlbum);
+
+ CLog::Log(LOGDEBUG, "{}", sql);
+ if (!m_pDS->query(sql))
+ return false;
+ if (m_pDS->num_rows() == 0) //Album with no songs
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ int songArtistOffset = song_enumCount;
+ std::set<int> songs;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ int idSong = record->at(song_idSong).get_asInt(); //Same as songartist.idSong by join
+ if (songs.find(idSong) == songs.end())
+ {
+ album.songs.emplace_back(GetSongFromDataset(record));
+ songs.insert(idSong);
+ }
+
+ int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
+ //By query order song is the last one appended to the album song vector.
+ if (idSongArtistRole == ROLE_ARTIST)
+ album.songs.back().artistCredits.emplace_back(
+ GetArtistCreditFromDataset(record, songArtistOffset));
+ else
+ album.songs.back().AppendArtistRole(GetArtistRoleFromDataset(record, songArtistOffset));
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ }
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::ClearAlbumLastScrapedTime(int idAlbum)
+{
+ std::string strSQL =
+ PrepareSQL("UPDATE album SET lastScraped = NULL WHERE idAlbum = %i", idAlbum);
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::HasAlbumBeenScraped(int idAlbum)
+{
+ std::string strSQL =
+ PrepareSQL("SELECT idAlbum FROM album WHERE idAlbum = %i AND lastScraped IS NULL", idAlbum);
+ return GetSingleValue(strSQL).empty();
+}
+
+int CMusicDatabase::AddGenre(std::string& strGenre)
+{
+ std::string strSQL;
+ try
+ {
+ StringUtils::Trim(strGenre);
+
+ if (strGenre.empty())
+ strGenre = g_localizeStrings.Get(13205); // Unknown
+
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ auto it = m_genreCache.find(strGenre);
+ if (it != m_genreCache.end())
+ return it->second;
+
+
+ strSQL = PrepareSQL("SELECT idGenre, strGenre FROM genre WHERE strGenre LIKE '%s'",
+ strGenre.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ // doesn't exists, add it
+ strSQL = PrepareSQL("INSERT INTO genre (idGenre, strGenre) values( NULL, '%s' )",
+ strGenre.c_str());
+ m_pDS->exec(strSQL);
+
+ int idGenre = (int)m_pDS->lastinsertid();
+ m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
+ return idGenre;
+ }
+ else
+ {
+ int idGenre = m_pDS->fv("idGenre").get_asInt();
+ strGenre = m_pDS->fv("strGenre").get_asString();
+ m_genreCache.insert(std::pair<std::string, int>(strGenre, idGenre));
+ m_pDS->close();
+ return idGenre;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to addgenre ({})", strSQL);
+ }
+
+ return -1;
+}
+
+bool CMusicDatabase::UpdateArtist(const CArtist& artist)
+{
+ SetLibraryLastUpdated();
+
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ UpdateArtist(artist.idArtist, //
+ artist.strArtist, //
+ artist.strSortName, //
+ artist.strMusicBrainzArtistID, //
+ artist.bScrapedMBID, //
+ artist.strType, //
+ artist.strGender, //
+ artist.strDisambiguation, //
+ artist.strBorn, //
+ artist.strFormed, //
+ StringUtils::Join(artist.genre, itemSeparator), //
+ StringUtils::Join(artist.moods, itemSeparator), //
+ StringUtils::Join(artist.styles, itemSeparator), //
+ StringUtils::Join(artist.instruments, itemSeparator), //
+ artist.strBiography, //
+ artist.strDied, //
+ artist.strDisbanded, //
+ StringUtils::Join(artist.yearsActive, itemSeparator).c_str(), //
+ artist.thumbURL.GetData());
+
+ DeleteArtistDiscography(artist.idArtist);
+ for (const auto& disc : artist.discography)
+ {
+ AddArtistDiscography(artist.idArtist, disc);
+ }
+
+ // Set current artwork (held in art table)
+ if (!artist.art.empty())
+ SetArtForItem(artist.idArtist, MediaTypeArtist, artist.art);
+
+ return true;
+}
+
+int CMusicDatabase::AddArtist(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ const std::string& strSortName,
+ bool bScrapedMBID /* = false*/)
+{
+ std::string strSQL;
+ int idArtist = AddArtist(strArtist, strMusicBrainzArtistID, bScrapedMBID);
+ if (idArtist < 0 || strSortName.empty())
+ return idArtist;
+
+ /* Artist sort name always taken as the first value provided that is different from name, so only
+ update when current sort name is blank. If a new sortname the same as name is provided then
+ clear any sortname currently held.
+ */
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ strSQL = PrepareSQL("SELECT strArtist, strSortName FROM artist WHERE idArtist = %i", idArtist);
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ std::string strArtistName, strArtistSort;
+ strArtistName = m_pDS->fv("strArtist").get_asString();
+ strArtistSort = m_pDS->fv("strSortName").get_asString();
+ m_pDS->close();
+
+ if (!strArtistSort.empty())
+ {
+ if (strSortName.compare(strArtistName) == 0)
+ m_pDS->exec(
+ PrepareSQL("UPDATE artist SET strSortName = NULL WHERE idArtist = %i", idArtist));
+ }
+ else if (strSortName.compare(strArtistName) != 0)
+ m_pDS->exec(PrepareSQL("UPDATE artist SET strSortName = '%s' WHERE idArtist = %i",
+ strSortName.c_str(), idArtist));
+
+ return idArtist;
+ }
+
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to addartist with sortname ({})", strSQL);
+ }
+
+ return -1;
+}
+
+int CMusicDatabase::AddArtist(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ bool bScrapedMBID /* = false*/)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ // 1) MusicBrainz
+ if (!strMusicBrainzArtistID.empty())
+ {
+ // 1.a) Match on a MusicBrainz ID
+ strSQL =
+ PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
+ strMusicBrainzArtistID.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ {
+ int idArtist = m_pDS->fv("idArtist").get_asInt();
+ bool update = m_pDS->fv("strArtist").get_asString().compare(strMusicBrainzArtistID) == 0;
+ m_pDS->close();
+ if (update)
+ {
+ strSQL = PrepareSQL("UPDATE artist SET strArtist = '%s' "
+ "WHERE idArtist = %i",
+ strArtist.c_str(), idArtist);
+ m_pDS->exec(strSQL);
+ m_pDS->close();
+ }
+ return idArtist;
+ }
+ m_pDS->close();
+
+
+ // 1.b) No match on MusicBrainz ID. Look for a previously added artist with no MusicBrainz ID
+ // and update that if it exists.
+ strSQL = PrepareSQL("SELECT idArtist FROM artist "
+ "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
+ strArtist.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ {
+ int idArtist = m_pDS->fv("idArtist").get_asInt();
+ m_pDS->close();
+ // 1.b.a) We found an artist by name but with no MusicBrainz ID set, update it and assume it is our artist, flag when mbid scraped
+ strSQL =
+ PrepareSQL("UPDATE artist SET strArtist = '%s', strMusicBrainzArtistID = '%s', "
+ "bScrapedMBID = %i WHERE idArtist = %i",
+ strArtist.c_str(), strMusicBrainzArtistID.c_str(), bScrapedMBID, idArtist);
+ m_pDS->exec(strSQL);
+ return idArtist;
+ }
+
+ // 2) No MusicBrainz - search for any artist (MB ID or non) with the same name.
+ // With MusicBrainz IDs this could return multiple artists and is non-determinstic
+ // Always pick the first artist ID returned by the DB to return.
+ }
+ else
+ {
+ strSQL =
+ PrepareSQL("SELECT idArtist FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str());
+
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ {
+ int idArtist = m_pDS->fv("idArtist").get_asInt();
+ m_pDS->close();
+ return idArtist;
+ }
+ m_pDS->close();
+ }
+
+ // 3) No artist exists at all - add it, flagging when has scraped mbid
+ if (strMusicBrainzArtistID.empty())
+ strSQL = PrepareSQL("INSERT INTO artist "
+ "(idArtist, strArtist, strMusicBrainzArtistID) "
+ "VALUES( NULL, '%s', NULL)",
+ strArtist.c_str());
+ else
+ strSQL = PrepareSQL("INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID, "
+ "bScrapedMBID) "
+ "VALUES( NULL, '%s', '%s', %i )",
+ strArtist.c_str(), strMusicBrainzArtistID.c_str(), bScrapedMBID);
+
+ m_pDS->exec(strSQL);
+ int idArtist = (int)m_pDS->lastinsertid();
+ return idArtist;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to addartist ({})", strSQL);
+ }
+
+ return -1;
+}
+
+int CMusicDatabase::UpdateArtist(int idArtist,
+ const std::string& strArtist,
+ const std::string& strSortName,
+ const std::string& strMusicBrainzArtistID,
+ const bool bScrapedMBID,
+ const std::string& strType,
+ const std::string& strGender,
+ const std::string& strDisambiguation,
+ const std::string& strBorn,
+ const std::string& strFormed,
+ const std::string& strGenres,
+ const std::string& strMoods,
+ const std::string& strStyles,
+ const std::string& strInstruments,
+ const std::string& strBiography,
+ const std::string& strDied,
+ const std::string& strDisbanded,
+ const std::string& strYearsActive,
+ const std::string& strImage)
+{
+ if (idArtist < 0)
+ return -1;
+
+ // Check another artist with this mbid not already exist (an alias for example)
+ bool useMBIDNull = strMusicBrainzArtistID.empty();
+ bool isScrapedMBID = bScrapedMBID;
+ std::string artistname;
+ int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
+ if (idArtistMbid > 0 && idArtistMbid != idArtist)
+ {
+ CLog::Log(LOGDEBUG, "{0}: Updating {4} (Id: {5}) mbid {1} already assigned to {2} (Id: {3})",
+ __FUNCTION__, strMusicBrainzArtistID, artistname, idArtistMbid, strArtist, idArtist);
+ useMBIDNull = true;
+ isScrapedMBID = false;
+ }
+
+ // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
+ // Truncate value cleaning up xml when URLs exceeds this
+ std::string strImageURLs = strImage;
+ if (StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
+ "mysql"))
+ TrimImageURLs(strImageURLs, 65535);
+
+ std::string strSQL;
+ strSQL = PrepareSQL("UPDATE artist SET "
+ " strArtist = '%s', "
+ " strType = '%s', strGender = '%s', strDisambiguation = '%s', "
+ " strBorn = '%s', strFormed = '%s', strGenres = '%s', "
+ " strMoods = '%s', strStyles = '%s', strInstruments = '%s', "
+ " strBiography = '%s', strDied = '%s', strDisbanded = '%s', "
+ " strYearsActive = '%s', strImage = '%s', "
+ " lastScraped = '%s', bScrapedMBID = %i",
+ strArtist.c_str(),
+ /* strSortName.c_str(),*/
+ /* strMusicBrainzArtistID.c_str(), */
+ strType.c_str(), strGender.c_str(), strDisambiguation.c_str(), //
+ strBorn.c_str(), strFormed.c_str(), strGenres.c_str(), //
+ strMoods.c_str(), strStyles.c_str(), strInstruments.c_str(), //
+ strBiography.c_str(), strDied.c_str(), strDisbanded.c_str(), //
+ strYearsActive.c_str(), strImageURLs.c_str(), //
+ CDateTime::GetUTCDateTime().GetAsDBDateTime().c_str(), isScrapedMBID);
+ if (useMBIDNull)
+ strSQL += PrepareSQL(", strMusicBrainzArtistID = NULL");
+ else
+ strSQL += PrepareSQL(", strMusicBrainzArtistID = '%s'", strMusicBrainzArtistID.c_str());
+ if (strSortName.empty())
+ strSQL += PrepareSQL(", strSortName = NULL");
+ else
+ strSQL += PrepareSQL(", strSortName = '%s'", strSortName.c_str());
+
+ strSQL += PrepareSQL(" WHERE idArtist = %i", idArtist);
+
+ bool status = ExecuteQuery(strSQL);
+ if (status)
+ AnnounceUpdate(MediaTypeArtist, idArtist);
+ return idArtist;
+}
+
+bool CMusicDatabase::UpdateArtistScrapedMBID(int idArtist,
+ const std::string& strMusicBrainzArtistID)
+{
+ if (strMusicBrainzArtistID.empty() || idArtist < 0)
+ return false;
+
+ // Check another artist with this mbid not already exist (an alias for example)
+ std::string artistname;
+ int idArtistMbid = GetArtistFromMBID(strMusicBrainzArtistID, artistname);
+ if (idArtistMbid > 0 && idArtistMbid != idArtist)
+ {
+ CLog::Log(LOGDEBUG, "{0}: Artist mbid {1} already assigned to {2} (Id: {3})", __FUNCTION__,
+ strMusicBrainzArtistID, artistname, idArtistMbid);
+ return false;
+ }
+
+ // Set scraped artist Musicbrainz ID for a previously added artist with no MusicBrainz ID
+ std::string strSQL;
+ strSQL = PrepareSQL("UPDATE artist SET strMusicBrainzArtistID = '%s', bScrapedMBID = 1 "
+ "WHERE idArtist = %i AND strMusicBrainzArtistID IS NULL",
+ strMusicBrainzArtistID.c_str(), idArtist);
+
+ bool status = ExecuteQuery(strSQL);
+ if (status)
+ {
+ AnnounceUpdate(MediaTypeArtist, idArtist);
+ return true;
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetArtist(int idArtist, CArtist& artist, bool fetchAll /* = false */)
+{
+ try
+ {
+ auto start = std::chrono::steady_clock::now();
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (idArtist == -1)
+ return false; // not in the database
+
+ std::string strSQL;
+ if (fetchAll)
+ strSQL = PrepareSQL("SELECT * FROM artistview "
+ "LEFT JOIN discography ON artistview.idArtist = discography.idArtist "
+ "WHERE artistview.idArtist = %i",
+ idArtist);
+ else
+ strSQL = PrepareSQL("SELECT * FROM artistview WHERE artistview.idArtist = %i", idArtist);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ int discographyOffset = artist_enumCount;
+
+ artist.discography.clear();
+ artist = GetArtistFromDataset(m_pDS->get_sql_record(), 0, true); // inc scraped art URLs
+ if (fetchAll)
+ {
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+ CDiscoAlbum discoAlbum;
+ discoAlbum.strAlbum = record->at(discographyOffset + 1).get_asString();
+ discoAlbum.strYear = record->at(discographyOffset + 2).get_asString();
+ discoAlbum.strReleaseGroupMBID = record->at(discographyOffset + 3).get_asString();
+ artist.discography.emplace_back(discoAlbum);
+ m_pDS->next();
+ }
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{0}({1}) - took {2} ms", __FUNCTION__, strSQL,
+ duration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetArtistExists(int idArtist)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL =
+ PrepareSQL("SELECT 1 FROM artist WHERE artist.idArtist = %i LIMIT 1", idArtist);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+
+ return false;
+}
+
+int CMusicDatabase::GetLastArtist()
+{
+ std::string strSQL = "SELECT MAX(idArtist) FROM artist";
+ std::string lastArtist = GetSingleValue(strSQL);
+ if (lastArtist.empty())
+ return -1;
+
+ return static_cast<int>(strtol(lastArtist.c_str(), NULL, 10));
+}
+
+int CMusicDatabase::GetArtistFromMBID(const std::string& strMusicBrainzArtistID,
+ std::string& artistname)
+{
+ if (strMusicBrainzArtistID.empty())
+ return -1;
+
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB || nullptr == m_pDS2)
+ return -1;
+ // Match on MusicBrainz ID, definitively unique
+ strSQL =
+ PrepareSQL("SELECT idArtist, strArtist FROM artist WHERE strMusicBrainzArtistID = '%s'",
+ strMusicBrainzArtistID.c_str());
+ if (!m_pDS2->query(strSQL))
+ return -1;
+ int idArtist = -1;
+ if (m_pDS2->num_rows() > 0)
+ {
+ idArtist = m_pDS2->fv("idArtist").get_asInt();
+ artistname = m_pDS2->fv("strArtist").get_asString();
+ }
+ m_pDS2->close();
+ return idArtist;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{0} - failed to execute {1}", __FUNCTION__, strSQL);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::HasArtistBeenScraped(int idArtist)
+{
+ std::string strSQL = PrepareSQL(
+ "SELECT idArtist FROM artist WHERE idArtist = %i AND lastScraped IS NULL", idArtist);
+ return GetSingleValue(strSQL).empty();
+}
+
+bool CMusicDatabase::ClearArtistLastScrapedTime(int idArtist)
+{
+ std::string strSQL =
+ PrepareSQL("UPDATE artist SET lastScraped = NULL WHERE idArtist = %i", idArtist);
+ return ExecuteQuery(strSQL);
+}
+
+int CMusicDatabase::AddArtistDiscography(int idArtist, const CDiscoAlbum& discoAlbum)
+{
+ std::string strSQL = PrepareSQL("INSERT INTO discography "
+ "(idArtist, strAlbum, strYear, strReleaseGroupMBID) "
+ "VALUES(%i, '%s', '%s', '%s')",
+ idArtist, discoAlbum.strAlbum.c_str(), discoAlbum.strYear.c_str(),
+ discoAlbum.strReleaseGroupMBID.c_str());
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::DeleteArtistDiscography(int idArtist)
+{
+ std::string strSQL = PrepareSQL("DELETE FROM discography WHERE idArtist = %i", idArtist);
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::GetArtistDiscography(int idArtist, CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ /* Combine entries from discography and album tables
+ Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of table using
+ correlated subqueries to a temp table. An updatable join to temp table would work in MySQL
+ but SQLite not support updatable joins.
+ */
+ m_pDS->exec("CREATE TABLE tempDisco "
+ "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
+ m_pDS->exec("CREATE TABLE tempAlbum "
+ "(strAlbum TEXT, strYear VARCHAR(4), mbid TEXT, idAlbum INTEGER)");
+
+ std::string strSQL;
+ strSQL = PrepareSQL("INSERT INTO tempDisco(strAlbum, strYear, mbid, idAlbum) "
+ "SELECT strAlbum, SUBSTR(discography.strYear, 1, 4) AS strYear, "
+ "strReleaseGroupMBID, NULL "
+ "FROM discography WHERE idArtist = %i",
+ idArtist);
+ m_pDS->exec(strSQL);
+
+ strSQL = PrepareSQL("INSERT INTO tempAlbum(strAlbum, strYear, mbid, idAlbum) "
+ "SELECT strAlbum, SUBSTR(strOrigReleaseDate, 1, 4) AS strYear, "
+ "strReleaseGroupMBID, album.idAlbum "
+ "FROM album JOIN album_artist ON album_artist.idAlbum = album.idAlbum "
+ "WHERE idArtist = %i",
+ idArtist);
+ m_pDS->exec(strSQL);
+
+ // Match albums on release group mbid, if multi-releases then first used
+ // Only use albums credited to this artist
+ strSQL = "UPDATE tempDisco SET idAlbum = (SELECT tempAlbum.idAlbum FROM tempAlbum "
+ "WHERE tempAlbum.mbid = tempDisco.mbid AND tempAlbum.mbid IS NOT NULL)";
+ m_pDS->exec(strSQL);
+ //Delete matched albums
+ strSQL = "DELETE FROM tempAlbum "
+ "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
+ m_pDS->exec(strSQL);
+
+ // Match remaining to albums by artist on title and year
+ strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
+ "WHERE tempAlbum.strAlbum = tempDisco.strAlbum "
+ "AND tempAlbum.strYear = tempDisco.strYear) "
+ "WHERE tempDisco.idAlbum is NULL";
+ m_pDS->exec(strSQL);
+ //Delete matched albums
+ strSQL = "DELETE FROM tempAlbum "
+ "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
+ m_pDS->exec(strSQL);
+
+ // Match remaining to albums by artist on title only
+ strSQL = "UPDATE tempDisco SET idAlbum = (SELECT idAlbum FROM tempAlbum "
+ "WHERE tempAlbum.strAlbum = tempDisco.strAlbum) "
+ "WHERE tempDisco.idAlbum is NULL";
+ m_pDS->exec(strSQL);
+ // Use year from album table, when matched by title only as it could be different
+ strSQL = "UPDATE tempDisco SET strYear = (SELECT strYear FROM tempAlbum "
+ "WHERE tempAlbum.idAlbum = tempDisco.idAlbum) "
+ "WHERE EXISTS(SELECT 1 FROM tempAlbum WHERE tempAlbum.idAlbum = tempDisco.idAlbum)";
+ m_pDS->exec(strSQL);
+ //Delete matched albums
+ strSQL = "DELETE FROM tempAlbum "
+ "WHERE EXISTS(SELECT 1 FROM tempDisco WHERE tempDisco.idAlbum = tempAlbum.idAlbum)";
+ m_pDS->exec(strSQL);
+
+ // Combine distinctly with any remaining unmatched albums by artist
+ strSQL = "SELECT strAlbum, strYear, idAlbum FROM tempDisco "
+ "UNION "
+ "SELECT strAlbum, strYear, idAlbum FROM tempAlbum "
+ "ORDER BY strYear, strAlbum, idAlbum";
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ while (!m_pDS->eof())
+ {
+ int idAlbum = m_pDS->fv("idAlbum").get_asInt();
+ if (idAlbum == 0)
+ idAlbum = -1;
+ std::string strAlbum = m_pDS->fv("strAlbum").get_asString();
+ if (!strAlbum.empty())
+ {
+ CFileItemPtr pItem(new CFileItem(strAlbum));
+ pItem->SetLabel2(m_pDS->fv("strYear").get_asString());
+ pItem->GetMusicInfoTag()->SetDatabaseId(idAlbum, MediaTypeAlbum);
+ items.Add(pItem);
+ }
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+ m_pDS->exec("DROP TABLE tempDisco");
+ m_pDS->exec("DROP TABLE tempAlbum");
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->exec("DROP TABLE tempDisco");
+ m_pDS->exec("DROP TABLE tempAlbum");
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+int CMusicDatabase::AddRole(const std::string& strRole)
+{
+ int idRole = -1;
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+ strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole LIKE '%s'", strRole.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ idRole = m_pDS->fv("idRole").get_asInt();
+ m_pDS->close();
+
+ if (idRole < 0)
+ {
+ strSQL = PrepareSQL("INSERT INTO role (strRole) VALUES ('%s')", strRole.c_str());
+ m_pDS->exec(strSQL);
+ idRole = static_cast<int>(m_pDS->lastinsertid());
+ m_pDS->close();
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to AddRole ({})", strSQL);
+ }
+ return idRole;
+}
+
+bool CMusicDatabase::AddSongArtist(
+ int idArtist, int idSong, const std::string& strRole, const std::string& strArtist, int iOrder)
+{
+ int idRole = AddRole(strRole);
+ return AddSongArtist(idArtist, idSong, idRole, strArtist, iOrder);
+}
+
+bool CMusicDatabase::AddSongArtist(
+ int idArtist, int idSong, int idRole, const std::string& strArtist, int iOrder)
+{
+ std::string strSQL;
+ strSQL = PrepareSQL("REPLACE INTO song_artist (idArtist, idSong, idRole, strArtist, iOrder) "
+ "VALUES(%i, %i, %i,'%s', %i)",
+ idArtist, idSong, idRole, strArtist.c_str(), iOrder);
+ return ExecuteQuery(strSQL);
+}
+
+int CMusicDatabase::AddSongContributor(int idSong,
+ const std::string& strRole,
+ const std::string& strArtist,
+ const std::string& strSort)
+{
+ if (strArtist.empty())
+ return -1;
+
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ int idArtist = -1;
+ // Add artist. As we only have name (no MBID) first try to identify artist from song
+ // as they may have already been added with a different role (including MBID).
+ strSQL =
+ PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND strArtist LIKE '%s' ",
+ idSong, strArtist.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ idArtist = m_pDS->fv("idArtist").get_asInt();
+ m_pDS->close();
+
+ if (idArtist < 0)
+ idArtist = AddArtist(strArtist, "", strSort);
+
+ // Add to song_artist table
+ AddSongArtist(idArtist, idSong, strRole, strArtist, 0);
+
+ return idArtist;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to AddSongContributor ({})", strSQL);
+ }
+
+ return -1;
+}
+
+void CMusicDatabase::AddSongContributors(int idSong,
+ const VECMUSICROLES& contributors,
+ const std::string& strSort)
+{
+ std::vector<std::string> composerSort;
+ size_t countComposer = 0;
+ if (!strSort.empty())
+ {
+ composerSort = StringUtils::Split(
+ strSort,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ }
+
+ for (const auto& credit : contributors)
+ {
+ std::string strSortName;
+ //Identify composer sort name if we have it
+ if (countComposer < composerSort.size())
+ {
+ if (credit.GetRoleDesc().compare("Composer") == 0)
+ {
+ strSortName = composerSort[countComposer];
+ countComposer++;
+ }
+ }
+ AddSongContributor(idSong, credit.GetRoleDesc(), credit.GetArtist(), strSortName);
+ }
+}
+
+int CMusicDatabase::GetRoleByName(const std::string& strRole)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idRole FROM role WHERE strRole like '%s'", strRole.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ return m_pDS->fv("idRole").get_asInt();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::GetRolesByArtist(int idArtist, CFileItem* item)
+{
+ try
+ {
+ std::string strSQL =
+ PrepareSQL("SELECT DISTINCT song_artist.idRole, Role.strRole "
+ "FROM song_artist JOIN role ON song_artist.idRole = role.idRole "
+ "WHERE idArtist = %i ORDER BY song_artist.idRole ASC",
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ CVariant artistRoles(CVariant::VariantTypeArray);
+
+ while (!m_pDS->eof())
+ {
+ CVariant roleObj;
+ roleObj["role"] = m_pDS->fv("strRole").get_asString();
+ roleObj["roleid"] = m_pDS->fv("idrole").get_asInt();
+ artistRoles.push_back(roleObj);
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ item->SetProperty("roles", artistRoles);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+}
+
+bool CMusicDatabase::DeleteSongArtistsBySong(int idSong)
+{
+ return ExecuteQuery(PrepareSQL("DELETE FROM song_artist WHERE idSong = %i", idSong));
+}
+
+bool CMusicDatabase::AddAlbumArtist(int idArtist,
+ int idAlbum,
+ const std::string& strArtist,
+ int iOrder)
+{
+ std::string strSQL;
+ strSQL = PrepareSQL("REPLACE INTO album_artist (idArtist, idAlbum, strArtist, iOrder) "
+ "VALUES(%i,%i,'%s',%i)",
+ idArtist, idAlbum, strArtist.c_str(), iOrder);
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::DeleteAlbumArtistsByAlbum(int idAlbum)
+{
+ return ExecuteQuery(PrepareSQL("DELETE FROM album_artist WHERE idAlbum = %i", idAlbum));
+}
+
+bool CMusicDatabase::AddSongGenres(int idSong, const std::vector<std::string>& genres)
+{
+ if (idSong == -1)
+ return true;
+
+ std::string strSQL;
+ try
+ {
+ // Clear current entries for song
+ strSQL = PrepareSQL("DELETE FROM song_genre WHERE idSong = %i", idSong);
+ if (!ExecuteQuery(strSQL))
+ return false;
+ unsigned int index = 0;
+ std::vector<std::string> modgenres = genres;
+ for (auto& strGenre : modgenres)
+ {
+ int idGenre = AddGenre(strGenre); // Genre string trimmed and matched case-insensitively
+ strSQL = PrepareSQL("INSERT INTO song_genre (idGenre, idSong, iOrder) VALUES(%i,%i,%i)",
+ idGenre, idSong, index++);
+ if (!ExecuteQuery(strSQL))
+ return false;
+ }
+ // Update concatenated genre string from the standardised genre values
+ std::string strGenres = StringUtils::Join(
+ modgenres,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ strSQL = PrepareSQL("UPDATE song SET strGenres = '%s' WHERE idSong = %i", //
+ strGenres.c_str(), idSong);
+ if (!ExecuteQuery(strSQL))
+ return false;
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) {} failed", __FUNCTION__, idSong, strSQL);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumsByArtist(int idArtist, std::vector<int>& albums)
+{
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idAlbum FROM album_artist WHERE idArtist = %i", idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ while (!m_pDS->eof())
+ {
+ albums.push_back(m_pDS->fv("idAlbum").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, CFileItem* item)
+{
+ try
+ {
+ std::string strSQL;
+
+ strSQL = PrepareSQL("SELECT * FROM albumartistview WHERE idAlbum = %i", idAlbum);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ // Get album artist credits
+ VECARTISTCREDITS artistCredits;
+ while (!m_pDS->eof())
+ {
+ artistCredits.emplace_back(GetArtistCreditFromDataset(m_pDS->get_sql_record(), 0));
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ // Populate item with song albumartist credits
+ std::vector<std::string> musicBrainzID;
+ std::vector<std::string> albumartists;
+ CVariant artistidObj(CVariant::VariantTypeArray);
+ for (const auto& artistCredit : artistCredits)
+ {
+ artistidObj.push_back(artistCredit.GetArtistId());
+ albumartists.emplace_back(artistCredit.GetArtist());
+ if (!artistCredit.GetMusicBrainzArtistID().empty())
+ musicBrainzID.emplace_back(artistCredit.GetMusicBrainzArtistID());
+ }
+ item->GetMusicInfoTag()->SetAlbumArtist(albumartists);
+ item->GetMusicInfoTag()->SetMusicBrainzAlbumArtistID(musicBrainzID);
+ // Add song albumartistIds as separate property as not part of CMusicInfoTag
+ item->SetProperty("albumartistid", artistidObj);
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetArtistsByAlbum(int idAlbum, std::vector<std::string>& artistIDs)
+{
+ try
+ {
+ std::string strSQL;
+ // Get distinct song and album artist IDs for this album, no other roles
+ // Allow for artists that are only album artists and not song artists
+ strSQL = PrepareSQL(
+ "SELECT DISTINCT idArtist FROM album_artist WHERE album_artist.idAlbum = %i \n"
+ "UNION \n"
+ "SELECT DISTINCT idArtist FROM song_artist JOIN song ON song.idSong = song_artist.idSong "
+ "WHERE song_artist.idRole = 1 AND song.idAlbum = %i ",
+ idAlbum, idAlbum);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+ while (!m_pDS->eof())
+ {
+ // Get ID as string so can easily join to make "IN" clause
+ artistIDs.push_back(m_pDS->fv("idArtist").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
+ }
+ return false;
+};
+
+bool CMusicDatabase::GetSongsByArtist(int idArtist, std::vector<int>& songs)
+{
+ try
+ {
+ std::string strSQL;
+ //Restrict to Artists only, no other roles
+ strSQL = PrepareSQL("SELECT idSong FROM song_artist WHERE idArtist = %i AND idRole = 1", //
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ while (!m_pDS->eof())
+ {
+ songs.push_back(m_pDS->fv("idSong").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+};
+
+bool CMusicDatabase::GetArtistsBySong(int idSong, std::vector<int>& artists)
+{
+ try
+ {
+ std::string strSQL;
+ //Restrict to Artists only, no other roles
+ strSQL = PrepareSQL("SELECT idArtist FROM song_artist WHERE idSong = %i AND idRole = 1", //
+ idSong);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ while (!m_pDS->eof())
+ {
+ artists.push_back(m_pDS->fv("idArtist").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetGenresByArtist(int idArtist, CFileItem* item)
+{
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre "
+ "FROM album_artist "
+ "JOIN song ON album_artist.idAlbum = song.idAlbum "
+ "JOIN song_genre ON song.idSong = song_genre.idSong "
+ "JOIN genre ON song_genre.idGenre = genre.idGenre "
+ "WHERE album_artist.idArtist = %i "
+ "ORDER BY song_genre.idGenre",
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ // Artist does have any song genres via albums may not be an album artist.
+ // Check via songs artist to fetch song genres from compilations or where they are guest artist
+ m_pDS->close();
+ strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre "
+ "FROM song_artist "
+ "JOIN song_genre ON song_artist.idSong = song_genre.idSong "
+ "JOIN genre ON song_genre.idGenre = genre.idGenre "
+ "WHERE song_artist.idArtist = %i "
+ "ORDER BY song_genre.idGenre",
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ //No song genres, but query successful
+ m_pDS->close();
+ return true;
+ }
+ }
+
+ CVariant artistSongGenres(CVariant::VariantTypeArray);
+
+ while (!m_pDS->eof())
+ {
+ CVariant genreObj;
+ genreObj["title"] = m_pDS->fv("strGenre").get_asString();
+ genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
+ artistSongGenres.push_back(genreObj);
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ item->SetProperty("songgenres", artistSongGenres);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetGenresByAlbum(int idAlbum, CFileItem* item)
+{
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT DISTINCT song_genre.idGenre, genre.strGenre FROM "
+ "song JOIN song_genre ON song.idSong = song_genre.idSong "
+ "JOIN genre ON song_genre.idGenre = genre.idGenre "
+ "WHERE song.idAlbum = %i "
+ "ORDER BY song_genre.idSong, song_genre.iOrder",
+ idAlbum);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ //No song genres, but query successful
+ m_pDS->close();
+ return true;
+ }
+
+ CVariant albumSongGenres(CVariant::VariantTypeArray);
+
+ while (!m_pDS->eof())
+ {
+ CVariant genreObj;
+ genreObj["title"] = m_pDS->fv("strGenre").get_asString();
+ genreObj["genreid"] = m_pDS->fv("idGenre").get_asInt();
+ albumSongGenres.push_back(genreObj);
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ item->SetProperty("songgenres", albumSongGenres);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetGenresBySong(int idSong, std::vector<int>& genres)
+{
+ try
+ {
+ std::string strSQL = PrepareSQL("SELECT idGenre FROM song_genre "
+ "WHERE idSong = %i ORDER BY iOrder ASC",
+ idSong);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ while (!m_pDS->eof())
+ {
+ genres.push_back(m_pDS->fv("idGenre").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetIsAlbumArtist(int idArtist, CFileItem* item)
+{
+ try
+ {
+ int countalbum =
+ GetSingleValueInt("album_artist", "count(idArtist)", PrepareSQL("idArtist=%i", idArtist));
+ CVariant IsAlbumArtistObj(CVariant::VariantTypeBoolean);
+ IsAlbumArtistObj = (countalbum > 0);
+ item->SetProperty("isalbumartist", IsAlbumArtistObj);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+}
+
+
+int CMusicDatabase::AddPath(const std::string& strPath1)
+{
+ std::string strSQL;
+ try
+ {
+ std::string strPath(strPath1);
+ if (!URIUtils::HasSlashAtEnd(strPath))
+ URIUtils::AddSlashAtEnd(strPath);
+
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ auto it = m_pathCache.find(strPath);
+ if (it != m_pathCache.end())
+ return it->second;
+
+ strSQL = PrepareSQL("SELECT * FROM path WHERE strPath='%s'", strPath.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ // doesn't exists, add it
+ strSQL = PrepareSQL("INSERT INTO path (idPath, strPath) "
+ "VALUES(NULL, '%s')",
+ strPath.c_str());
+ m_pDS->exec(strSQL);
+
+ int idPath = (int)m_pDS->lastinsertid();
+ m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
+ return idPath;
+ }
+ else
+ {
+ int idPath = m_pDS->fv("idPath").get_asInt();
+ m_pathCache.insert(std::pair<std::string, int>(strPath, idPath));
+ m_pDS->close();
+ return idPath;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "musicdatabase:unable to addpath ({})", strSQL);
+ }
+
+ return -1;
+}
+
+CSong CMusicDatabase::GetSongFromDataset()
+{
+ return GetSongFromDataset(m_pDS->get_sql_record());
+}
+
+CSong CMusicDatabase::GetSongFromDataset(const dbiplus::sql_record* const record,
+ int offset /* = 0 */)
+{
+ CSong song;
+ song.idSong = record->at(offset + song_idSong).get_asInt();
+ // Note this function does not populate artist credits, this must be done separately.
+ // However artist names are held as a descriptive string
+ song.strArtistDesc = record->at(offset + song_strArtists).get_asString();
+ song.strArtistSort = record->at(offset + song_strArtistSort).get_asString();
+ // Get the full genre string
+ song.genre = StringUtils::Split(
+ record->at(offset + song_strGenres).get_asString(),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ // and the rest...
+ song.strAlbum = record->at(offset + song_strAlbum).get_asString();
+ song.idAlbum = record->at(offset + song_idAlbum).get_asInt();
+ song.iTrack = record->at(offset + song_iTrack).get_asInt();
+ song.iDuration = record->at(offset + song_iDuration).get_asInt();
+ song.strReleaseDate = record->at(offset + song_strReleaseDate).get_asString();
+ song.strOrigReleaseDate = record->at(offset + song_strOrigReleaseDate).get_asString();
+ song.strTitle = record->at(offset + song_strTitle).get_asString();
+ song.iTimesPlayed = record->at(offset + song_iTimesPlayed).get_asInt();
+ song.lastPlayed.SetFromDBDateTime(record->at(offset + song_lastplayed).get_asString());
+ song.dateAdded.SetFromDBDateTime(record->at(offset + song_dateAdded).get_asString());
+ song.dateNew.SetFromDBDateTime(record->at(offset + song_dateNew).get_asString());
+ song.dateUpdated.SetFromDBDateTime(record->at(offset + song_dateModified).get_asString());
+ song.iStartOffset = record->at(offset + song_iStartOffset).get_asInt();
+ song.iEndOffset = record->at(offset + song_iEndOffset).get_asInt();
+ song.strMusicBrainzTrackID = record->at(offset + song_strMusicBrainzTrackID).get_asString();
+ song.rating = record->at(offset + song_rating).get_asFloat();
+ song.userrating = record->at(offset + song_userrating).get_asInt();
+ song.votes = record->at(offset + song_votes).get_asInt();
+ song.strComment = record->at(offset + song_comment).get_asString();
+ song.strMood = record->at(offset + song_mood).get_asString();
+ song.bCompilation = record->at(offset + song_bCompilation).get_asInt() == 1;
+ song.strDiscSubtitle = record->at(offset + song_strDiscSubtitle).get_asString();
+ // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
+ song.replayGain.Set(record->at(offset + song_strReplayGain).get_asString());
+ // Get filename with full path
+ song.strFileName =
+ URIUtils::AddFileToFolder(record->at(offset + song_strPath).get_asString(),
+ record->at(offset + song_strFileName).get_asString());
+ song.iBPM = record->at(offset + song_iBPM).get_asInt();
+ song.iBitRate = record->at(offset + song_iBitRate).get_asInt();
+ song.iSampleRate = record->at(offset + song_iSampleRate).get_asInt();
+ song.iChannels = record->at(offset + song_iChannels).get_asInt();
+ return song;
+}
+
+void CMusicDatabase::GetFileItemFromDataset(CFileItem* item, const CMusicDbUrl& baseUrl)
+{
+ GetFileItemFromDataset(m_pDS->get_sql_record(), item, baseUrl);
+}
+
+void CMusicDatabase::GetFileItemFromDataset(const dbiplus::sql_record* const record,
+ CFileItem* item,
+ const CMusicDbUrl& baseUrl)
+{
+ // get the artist string from songview (not the song_artist and artist tables)
+ item->GetMusicInfoTag()->SetArtistDesc(record->at(song_strArtists).get_asString());
+ // get the artist sort name string from songview (not the song_artist and artist tables)
+ item->GetMusicInfoTag()->SetArtistSort(record->at(song_strArtistSort).get_asString());
+ // and the full genre string
+ item->GetMusicInfoTag()->SetGenre(record->at(song_strGenres).get_asString());
+ // and the rest...
+ item->GetMusicInfoTag()->SetAlbum(record->at(song_strAlbum).get_asString());
+ item->GetMusicInfoTag()->SetAlbumId(record->at(song_idAlbum).get_asInt());
+ item->GetMusicInfoTag()->SetTrackAndDiscNumber(record->at(song_iTrack).get_asInt());
+ item->GetMusicInfoTag()->SetDuration(record->at(song_iDuration).get_asInt());
+ item->GetMusicInfoTag()->SetDatabaseId(record->at(song_idSong).get_asInt(), MediaTypeSong);
+ item->GetMusicInfoTag()->SetOriginalDate(record->at(song_strOrigReleaseDate).get_asString());
+ item->GetMusicInfoTag()->SetReleaseDate(record->at(song_strReleaseDate).get_asString());
+ item->GetMusicInfoTag()->SetTitle(record->at(song_strTitle).get_asString());
+ item->GetMusicInfoTag()->SetDiscSubtitle(record->at(song_strDiscSubtitle).get_asString());
+ item->SetLabel(record->at(song_strTitle).get_asString());
+ item->SetStartOffset(record->at(song_iStartOffset).get_asInt64());
+ item->SetProperty("item_start", item->GetStartOffset());
+ item->SetEndOffset(record->at(song_iEndOffset).get_asInt64());
+ item->GetMusicInfoTag()->SetMusicBrainzTrackID(
+ record->at(song_strMusicBrainzTrackID).get_asString());
+ item->GetMusicInfoTag()->SetRating(record->at(song_rating).get_asFloat());
+ item->GetMusicInfoTag()->SetUserrating(record->at(song_userrating).get_asInt());
+ item->GetMusicInfoTag()->SetVotes(record->at(song_votes).get_asInt());
+ item->GetMusicInfoTag()->SetComment(record->at(song_comment).get_asString());
+ item->GetMusicInfoTag()->SetMood(record->at(song_mood).get_asString());
+ item->GetMusicInfoTag()->SetPlayCount(record->at(song_iTimesPlayed).get_asInt());
+ item->GetMusicInfoTag()->SetLastPlayed(record->at(song_lastplayed).get_asString());
+ item->GetMusicInfoTag()->SetDateAdded(record->at(song_dateAdded).get_asString());
+ item->GetMusicInfoTag()->SetDateNew(record->at(song_dateNew).get_asString());
+ item->GetMusicInfoTag()->SetDateUpdated(record->at(song_dateModified).get_asString());
+ std::string strRealPath = URIUtils::AddFileToFolder(record->at(song_strPath).get_asString(),
+ record->at(song_strFileName).get_asString());
+ item->GetMusicInfoTag()->SetURL(strRealPath);
+ item->GetMusicInfoTag()->SetCompilation(record->at(song_bCompilation).get_asInt() == 1);
+ item->GetMusicInfoTag()->SetBoxset(record->at(song_bBoxedSet).get_asInt() == 1);
+ // get the album artist string from songview (not the album_artist and artist tables)
+ item->GetMusicInfoTag()->SetAlbumArtist(record->at(song_strAlbumArtists).get_asString());
+ item->GetMusicInfoTag()->SetAlbumReleaseType(
+ CAlbum::ReleaseTypeFromString(record->at(song_strAlbumReleaseType).get_asString()));
+ item->GetMusicInfoTag()->SetBPM(record->at(song_iBPM).get_asInt());
+ item->GetMusicInfoTag()->SetBitRate(record->at(song_iBitRate).get_asInt());
+ item->GetMusicInfoTag()->SetSampleRate(record->at(song_iSampleRate).get_asInt());
+ item->GetMusicInfoTag()->SetNoOfChannels(record->at(song_iChannels).get_asInt());
+ // Replay gain data (needed for songs from cuesheets, both separate .cue files and embedded metadata)
+ ReplayGain replaygain;
+ replaygain.Set(record->at(song_strReplayGain).get_asString());
+ item->GetMusicInfoTag()->SetReplayGain(replaygain);
+ item->GetMusicInfoTag()->SetTotalDiscs(record->at(song_iDiscTotal).get_asInt());
+
+ item->GetMusicInfoTag()->SetLoaded(true);
+ // Get filename with full path
+ if (!baseUrl.IsValid())
+ item->SetPath(strRealPath);
+ else
+ {
+ CMusicDbUrl itemUrl = baseUrl;
+ std::string strFileName = record->at(song_strFileName).get_asString();
+ std::string strExt = URIUtils::GetExtension(strFileName);
+ std::string path = StringUtils::Format("{}{}", record->at(song_idSong).get_asInt(), strExt);
+ itemUrl.AppendPath(path);
+ item->SetPath(itemUrl.ToString());
+ item->SetDynPath(strRealPath);
+ }
+}
+
+void CMusicDatabase::GetFileItemFromArtistCredits(VECARTISTCREDITS& artistCredits, CFileItem* item)
+{
+ // Populate fileitem with artists from vector of artist credits
+ std::vector<std::string> musicBrainzID;
+ std::vector<std::string> songartists;
+ CVariant artistidObj(CVariant::VariantTypeArray);
+
+ // When "missing tag" artist, it is the only artist when present.
+ if (artistCredits.begin()->GetArtistId() == BLANKARTIST_ID)
+ {
+ artistidObj.push_back((int)BLANKARTIST_ID);
+ songartists.push_back(StringUtils::Empty);
+ }
+ else
+ {
+ for (const auto& artistCredit : artistCredits)
+ {
+ artistidObj.push_back(artistCredit.GetArtistId());
+ songartists.push_back(artistCredit.GetArtist());
+ if (!artistCredit.GetMusicBrainzArtistID().empty())
+ musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID());
+ }
+ }
+ // Also sets ArtistDesc if empty from song.strArtist field
+ item->GetMusicInfoTag()->SetArtist(songartists);
+ item->GetMusicInfoTag()->SetMusicBrainzArtistID(musicBrainzID);
+ // Add album artistIds as separate property as not part of CMusicInfoTag
+ item->SetProperty("artistid", artistidObj);
+}
+
+CAlbum CMusicDatabase::GetAlbumFromDataset(dbiplus::Dataset* pDS,
+ int offset /* = 0 */,
+ bool imageURL /* = false*/)
+{
+ return GetAlbumFromDataset(pDS->get_sql_record(), offset, imageURL);
+}
+
+CAlbum CMusicDatabase::GetAlbumFromDataset(const dbiplus::sql_record* const record,
+ int offset /* = 0 */,
+ bool imageURL /* = false*/)
+{
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ CAlbum album;
+ album.idAlbum = record->at(offset + album_idAlbum).get_asInt();
+ album.strAlbum = record->at(offset + album_strAlbum).get_asString();
+ if (album.strAlbum.empty())
+ album.strAlbum = g_localizeStrings.Get(1050);
+ album.strMusicBrainzAlbumID = record->at(offset + album_strMusicBrainzAlbumID).get_asString();
+ album.strReleaseGroupMBID = record->at(offset + album_strReleaseGroupMBID).get_asString();
+ album.strArtistDesc = record->at(offset + album_strArtists).get_asString();
+ album.strArtistSort = record->at(offset + album_strArtistSort).get_asString();
+ album.genre =
+ StringUtils::Split(record->at(offset + album_strGenres).get_asString(), itemSeparator);
+ album.strReleaseDate = record->at(offset + album_strReleaseDate).get_asString();
+ album.strOrigReleaseDate = record->at(offset + album_strOrigReleaseDate).get_asString();
+ album.bBoxedSet = record->at(offset + album_bBoxedSet).get_asInt() == 1;
+ if (imageURL)
+ album.thumbURL.ParseFromData(record->at(offset + album_strThumbURL).get_asString());
+ album.fRating = record->at(offset + album_fRating).get_asFloat();
+ album.iUserrating = record->at(offset + album_iUserrating).get_asInt();
+ album.iVotes = record->at(offset + album_iVotes).get_asInt();
+ album.strReview = record->at(offset + album_strReview).get_asString();
+ album.styles =
+ StringUtils::Split(record->at(offset + album_strStyles).get_asString(), itemSeparator);
+ album.moods =
+ StringUtils::Split(record->at(offset + album_strMoods).get_asString(), itemSeparator);
+ album.themes =
+ StringUtils::Split(record->at(offset + album_strThemes).get_asString(), itemSeparator);
+ album.strLabel = record->at(offset + album_strLabel).get_asString();
+ album.strType = record->at(offset + album_strType).get_asString();
+ album.strReleaseStatus = record->at(offset + album_strReleaseStatus).get_asString();
+ album.bCompilation = record->at(offset + album_bCompilation).get_asInt() == 1;
+ album.bScrapedMBID = record->at(offset + album_bScrapedMBID).get_asInt() == 1;
+ album.strLastScraped = record->at(offset + album_lastScraped).get_asString();
+ album.iTimesPlayed = record->at(offset + album_iTimesPlayed).get_asInt();
+ album.SetReleaseType(record->at(offset + album_strReleaseType).get_asString());
+ album.iTotalDiscs = record->at(offset + album_iTotalDiscs).get_asInt();
+ album.SetDateAdded(record->at(offset + album_dateAdded).get_asString());
+ album.SetDateNew(record->at(offset + album_dateNew).get_asString());
+ album.SetDateUpdated(record->at(offset + album_dateModified).get_asString());
+ album.SetLastPlayed(record->at(offset + album_dtLastPlayed).get_asString());
+ album.iAlbumDuration = record->at(offset + album_iAlbumDuration).get_asInt();
+ return album;
+}
+
+CArtistCredit CMusicDatabase::GetArtistCreditFromDataset(const dbiplus::sql_record* const record,
+ int offset /* = 0 */)
+{
+ CArtistCredit artistCredit;
+ artistCredit.idArtist = record->at(offset + artistCredit_idArtist).get_asInt();
+ if (artistCredit.idArtist == BLANKARTIST_ID)
+ artistCredit.m_strArtist = StringUtils::Empty;
+ else
+ {
+ artistCredit.m_strArtist = record->at(offset + artistCredit_strArtist).get_asString();
+ artistCredit.m_strMusicBrainzArtistID =
+ record->at(offset + artistCredit_strMusicBrainzArtistID).get_asString();
+ }
+ return artistCredit;
+}
+
+CMusicRole CMusicDatabase::GetArtistRoleFromDataset(const dbiplus::sql_record* const record,
+ int offset /* = 0 */)
+{
+ CMusicRole ArtistRole(record->at(offset + artistCredit_idRole).get_asInt(),
+ record->at(offset + artistCredit_strRole).get_asString(),
+ record->at(offset + artistCredit_strArtist).get_asString(),
+ record->at(offset + artistCredit_idArtist).get_asInt());
+ return ArtistRole;
+}
+
+CArtist CMusicDatabase::GetArtistFromDataset(dbiplus::Dataset* pDS,
+ int offset /* = 0 */,
+ bool needThumb /* = true */)
+{
+ return GetArtistFromDataset(pDS->get_sql_record(), offset, needThumb);
+}
+
+CArtist CMusicDatabase::GetArtistFromDataset(const dbiplus::sql_record* const record,
+ int offset /* = 0 */,
+ bool needThumb /* = true */)
+{
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ CArtist artist;
+ artist.idArtist = record->at(offset + artist_idArtist).get_asInt();
+ if (artist.idArtist == BLANKARTIST_ID && m_translateBlankArtist)
+ artist.strArtist = g_localizeStrings.Get(38042); //Missing artist tag in current language
+ else
+ artist.strArtist = record->at(offset + artist_strArtist).get_asString();
+ artist.strSortName = record->at(offset + artist_strSortName).get_asString();
+ artist.strMusicBrainzArtistID = record->at(offset + artist_strMusicBrainzArtistID).get_asString();
+ artist.strType = record->at(offset + artist_strType).get_asString();
+ artist.strGender = record->at(offset + artist_strGender).get_asString();
+ artist.strDisambiguation = record->at(offset + artist_strDisambiguation).get_asString();
+ artist.genre =
+ StringUtils::Split(record->at(offset + artist_strGenres).get_asString(), itemSeparator);
+ artist.strBiography = record->at(offset + artist_strBiography).get_asString();
+ artist.styles =
+ StringUtils::Split(record->at(offset + artist_strStyles).get_asString(), itemSeparator);
+ artist.moods =
+ StringUtils::Split(record->at(offset + artist_strMoods).get_asString(), itemSeparator);
+ artist.strBorn = record->at(offset + artist_strBorn).get_asString();
+ artist.strFormed = record->at(offset + artist_strFormed).get_asString();
+ artist.strDied = record->at(offset + artist_strDied).get_asString();
+ artist.strDisbanded = record->at(offset + artist_strDisbanded).get_asString();
+ artist.yearsActive =
+ StringUtils::Split(record->at(offset + artist_strYearsActive).get_asString(), itemSeparator);
+ artist.instruments =
+ StringUtils::Split(record->at(offset + artist_strInstruments).get_asString(), itemSeparator);
+ artist.bScrapedMBID = record->at(offset + artist_bScrapedMBID).get_asInt() == 1;
+ artist.strLastScraped = record->at(offset + artist_lastScraped).get_asString();
+ artist.SetDateAdded(record->at(offset + artist_dateAdded).get_asString());
+ artist.SetDateNew(record->at(offset + artist_dateNew).get_asString());
+ artist.SetDateUpdated(record->at(offset + artist_dateModified).get_asString());
+
+ if (needThumb)
+ {
+ artist.thumbURL.ParseFromData(record->at(artist_strImage).get_asString());
+ }
+
+ return artist;
+}
+
+bool CMusicDatabase::GetSongByFileName(const std::string& strFileNameAndPath,
+ CSong& song,
+ int64_t startOffset)
+{
+ song.Clear();
+ CURL url(strFileNameAndPath);
+
+ if (url.IsProtocol("musicdb"))
+ {
+ std::string strFile = URIUtils::GetFileName(strFileNameAndPath);
+ URIUtils::RemoveExtension(strFile);
+ return GetSong(atoi(strFile.c_str()), song);
+ }
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strPath, strFileName;
+ SplitPath(strFileNameAndPath, strPath, strFileName);
+ URIUtils::AddSlashAtEnd(strPath);
+
+ std::string strSQL = PrepareSQL("SELECT idSong FROM songview "
+ "WHERE strFileName='%s' AND strPath='%s'",
+ strFileName.c_str(), strPath.c_str());
+ if (startOffset)
+ strSQL += PrepareSQL(" AND iStartOffset=%" PRIi64, startOffset);
+
+ int idSong = GetSingleValueInt(strSQL);
+ if (idSong > 0)
+ return GetSong(idSong, song);
+
+ return false;
+}
+
+int CMusicDatabase::GetAlbumIdByPath(const std::string& strPath)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL = PrepareSQL("SELECT DISTINCT idAlbum FROM song "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE path.strPath='%s'",
+ strPath.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+
+ int idAlbum = -1; // If no album is found, or more than one album is found then -1 is returned
+ if (iRowsFound == 1)
+ idAlbum = m_pDS->fv(0).get_asInt();
+
+ m_pDS->close();
+
+ return idAlbum;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strPath);
+ }
+
+ return -1;
+}
+
+int CMusicDatabase::GetSongByArtistAndAlbumAndTitle(const std::string& strArtist,
+ const std::string& strAlbum,
+ const std::string& strTitle)
+{
+ try
+ {
+ std::string strSQL =
+ PrepareSQL("SELECT idSong FROM songview "
+ "WHERE strArtists LIKE '%s' AND strAlbum LIKE '%s' AND strTitle LIKE '%s'",
+ strArtist.c_str(), strAlbum.c_str(), strTitle.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ int lResult = m_pDS->fv(0).get_asInt();
+ m_pDS->close(); // cleanup recordset data
+ return lResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({},{},{}) failed", __FUNCTION__, strArtist, strAlbum, strTitle);
+ }
+
+ return -1;
+}
+
+bool CMusicDatabase::SearchArtists(const std::string& search, CFileItemList& artists)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strVariousArtists = g_localizeStrings.Get(340).c_str();
+ std::string strSQL;
+ if (search.size() >= MIN_FULL_SEARCH_LENGTH)
+ strSQL = PrepareSQL("SELECT * FROM artist "
+ "WHERE (strArtist LIKE '%s%%' OR strArtist LIKE '%% %s%%') "
+ "AND strArtist <> '%s' ",
+ search.c_str(), search.c_str(), strVariousArtists.c_str());
+ else
+ strSQL = PrepareSQL("SELECT * FROM artist "
+ "WHERE strArtist LIKE '%s%%' AND strArtist <> '%s' ",
+ search.c_str(), strVariousArtists.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ const std::string& artistLabel(g_localizeStrings.Get(557)); // Artist
+ while (!m_pDS->eof())
+ {
+ std::string path = StringUtils::Format("musicdb://artists/{}/", m_pDS->fv(0).get_asInt());
+ CFileItemPtr pItem(new CFileItem(path, true));
+ std::string label = StringUtils::Format("[{}] {}", artistLabel, m_pDS->fv(1).get_asString());
+ pItem->SetLabel(label);
+ // sort label is stored in the title tag
+ label = StringUtils::Format("A {}", m_pDS->fv(1).get_asString());
+ pItem->GetMusicInfoTag()->SetTitle(label);
+ pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv(0).get_asInt(), MediaTypeArtist);
+ artists.Add(pItem);
+ m_pDS->next();
+ }
+
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetTop100(const std::string& strBaseDir, CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CMusicDbUrl baseUrl;
+ if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
+ return false;
+
+ std::string strSQL = "SELECT * FROM songview "
+ "WHERE iTimesPlayed>0 "
+ "ORDER BY iTimesPlayed DESC "
+ "LIMIT 100";
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+ items.Reserve(iRowsFound);
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(item.get(), baseUrl);
+ items.Add(item);
+ m_pDS->next();
+ }
+
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetTop100Albums(VECALBUMS& albums)
+{
+ try
+ {
+ albums.erase(albums.begin(), albums.end());
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Get data from album and album_artist tables to fully populate albums
+ std::string strSQL = "SELECT albumview.*, albumartistview.* FROM albumview "
+ "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
+ "WHERE albumartistview.idAlbum IN "
+ "(SELECT albumview.idAlbum FROM albumview "
+ "WHERE albumview.strAlbum != '' AND albumview.iTimesPlayed>0 "
+ "ORDER BY albumview.iTimesPlayed DESC LIMIT 100) "
+ "ORDER BY albumview.iTimesPlayed DESC, albumartistview.iOrder";
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ int albumArtistOffset = album_enumCount;
+ int albumId = -1;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (albumId != record->at(album_idAlbum).get_asInt())
+ { // New album
+ albumId = record->at(album_idAlbum).get_asInt();
+ albums.push_back(GetAlbumFromDataset(record));
+ }
+ // Get album artists
+ albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
+
+ m_pDS->next();
+ }
+
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetTop100AlbumSongs(const std::string& strBaseDir, CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CMusicDbUrl baseUrl;
+ if (!strBaseDir.empty() && baseUrl.FromString(strBaseDir))
+ return false;
+
+ std::string strSQL = StringUtils::Format(
+ "SELECT songview.*, albumview.* FROM songview"
+ "JOIN albumview ON (songview.idAlbum = albumview.idAlbum) "
+ "JOIN (SELECT song.idAlbum, SUM(song.iTimesPlayed) AS iTimesPlayedSum FROM song "
+ "WHERE song.iTimesPlayed > 0 "
+ "GROUP BY idAlbum "
+ "ORDER BY iTimesPlayedSum DESC LIMIT 100) AS _albumlimit "
+ "ON (songview.idAlbum = _albumlimit.idAlbum) "
+ "ORDER BY _albumlimit.iTimesPlayedSum DESC");
+ CLog::Log(LOGDEBUG, "GetTop100AlbumSongs() query: {}", strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ items.Reserve(iRowsFound);
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(item.get(), baseUrl);
+ items.Add(item);
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetRecentlyPlayedAlbums(VECALBUMS& albums)
+{
+ try
+ {
+ albums.erase(albums.begin(), albums.end());
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ auto start = std::chrono::steady_clock::now();
+
+ // Get data from album and album_artist tables to fully populate albums
+ std::string strSQL =
+ PrepareSQL("SELECT albumview.*, albumartistview.* "
+ "FROM (SELECT idAlbum FROM albumview WHERE albumview.lastplayed IS NOT NULL "
+ "AND albumview.strReleaseType = '%s' "
+ "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
+ "JOIN albumview ON albumview.idAlbum = playedalbums.idAlbum "
+ "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
+ "ORDER BY albumview.lastplayed DESC, albumartistview.iorder ",
+ CAlbum::ReleaseTypeToString(CAlbum::Album).c_str(), RECENTLY_PLAYED_LIMIT);
+
+ auto queryStart = std::chrono::steady_clock::now();
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ auto queryEnd = std::chrono::steady_clock::now();
+ auto queryDuration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ int albumArtistOffset = album_enumCount;
+ int albumId = -1;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (albumId != record->at(album_idAlbum).get_asInt())
+ { // New album
+ albumId = record->at(album_idAlbum).get_asInt();
+ albums.push_back(GetAlbumFromDataset(record));
+ }
+ // Get album artists
+ albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__,
+ duration.count(), queryDuration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetRecentlyPlayedAlbumSongs(const std::string& strBaseDir,
+ CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CMusicDbUrl baseUrl;
+ if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
+ return false;
+
+ std::string strSQL =
+ PrepareSQL("SELECT songview.*, songartistview.* "
+ "FROM (SELECT idAlbum, lastPlayed FROM albumview "
+ "WHERE albumview.lastplayed IS NOT NULL "
+ "ORDER BY albumview.lastplayed DESC LIMIT %u) as playedalbums "
+ "JOIN songview ON songview.idAlbum = playedalbums.idAlbum "
+ "JOIN songartistview ON songview.idSong = songartistview.idSong "
+ "ORDER BY playedalbums.lastplayed DESC, "
+ "songartistview.idsong, songartistview.idRole, songartistview.iOrder",
+ CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iMusicLibraryRecentlyAddedItems);
+ CLog::Log(LOGDEBUG, "GetRecentlyPlayedAlbumSongs() query: {}", strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Needs a separate query to determine number of songs to set items size.
+ // Get songs from returned rows. Join means there is a row for every song artist
+ // Gather artist credits, rather than append to item as go along, so can return array of artistIDs too
+ int songArtistOffset = song_enumCount;
+ int songId = -1;
+ VECARTISTCREDITS artistCredits;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
+ if (songId != record->at(song_idSong).get_asInt())
+ { //New song
+ if (songId > 0 && !artistCredits.empty())
+ {
+ //Store artist credits for previous song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+ songId = record->at(song_idSong).get_asInt();
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(record, item.get(), baseUrl);
+ items.Add(item);
+ }
+ // Get song artist credits and contributors
+ if (idSongArtistRole == ROLE_ARTIST)
+ artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
+ else
+ items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
+ GetArtistRoleFromDataset(record, songArtistOffset));
+
+ m_pDS->next();
+ }
+ if (!artistCredits.empty())
+ {
+ //Store artist credits for final song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+
+ // cleanup
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetRecentlyAddedAlbums(VECALBUMS& albums, unsigned int limit)
+{
+ try
+ {
+ albums.erase(albums.begin(), albums.end());
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Get data from album and album_artist tables to fully populate albums
+ // Determine the recently added albums from dateAdded (usually derived from music file
+ // timestamps, nothing to do with when albums added to library)
+ std::string strSQL =
+ PrepareSQL("SELECT albumview.*, albumartistview.* "
+ "FROM (SELECT idAlbum FROM album WHERE strAlbum != '' "
+ "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
+ "JOIN albumview ON albumview.idAlbum = recentalbums.idAlbum "
+ "JOIN albumartistview ON albumview.idAlbum = albumartistview.idAlbum "
+ "ORDER BY dateAdded DESC, albumview.idAlbum desc, albumartistview.iOrder ",
+ limit ? limit
+ : CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iMusicLibraryRecentlyAddedItems);
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ int albumArtistOffset = album_enumCount;
+ int albumId = -1;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (albumId != record->at(album_idAlbum).get_asInt())
+ { // New album
+ albumId = record->at(album_idAlbum).get_asInt();
+ albums.push_back(GetAlbumFromDataset(record));
+ }
+ // Get album artists
+ albums.back().artistCredits.push_back(GetArtistCreditFromDataset(record, albumArtistOffset));
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetRecentlyAddedAlbumSongs(const std::string& strBaseDir,
+ CFileItemList& items,
+ unsigned int limit)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CMusicDbUrl baseUrl;
+ if (!strBaseDir.empty() && !baseUrl.FromString(strBaseDir))
+ return false;
+
+ // Get data from song and song_artist tables to fully populate songs
+ // Determine the recently added albums from dateAdded (usually derived from music file
+ // timestamps, nothing to do with when albums added to library)
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT songview.*, songartistview.* "
+ "FROM (SELECT idAlbum, dateAdded FROM album "
+ "ORDER BY dateAdded DESC LIMIT %u) AS recentalbums "
+ "JOIN songview ON songview.idAlbum = recentalbums.idAlbum "
+ "JOIN songartistview ON songview.idSong = songartistview.idSong "
+ "ORDER BY recentalbums.dateAdded DESC, songview.idAlbum DESC, "
+ "songview.idSong, songartistview.idRole, songartistview.iOrder ",
+ limit ? limit
+ : CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iMusicLibraryRecentlyAddedItems);
+ CLog::Log(LOGDEBUG, "GetRecentlyAddedAlbumSongs() query: {}", strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Needs a separate query to determine number of songs to set items size.
+ // Get songs from returned rows. Join means there is a row for every song artist
+ int songArtistOffset = song_enumCount;
+ int songId = -1;
+ VECARTISTCREDITS artistCredits;
+ while (!m_pDS->eof())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
+ if (songId != record->at(song_idSong).get_asInt())
+ { //New song
+ if (songId > 0 && !artistCredits.empty())
+ {
+ //Store artist credits for previous song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+ songId = record->at(song_idSong).get_asInt();
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(record, item.get(), baseUrl);
+ items.Add(item);
+ }
+ // Get song artist credits and contributors
+ if (idSongArtistRole == ROLE_ARTIST)
+ artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
+ else
+ items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
+ GetArtistRoleFromDataset(record, songArtistOffset));
+
+ m_pDS->next();
+ }
+ if (!artistCredits.empty())
+ {
+ //Store artist credits for final song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+
+ // cleanup
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+void CMusicDatabase::IncrementPlayCount(const CFileItem& item)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ int idSong = GetSongIDFromPath(item.GetPath());
+ std::string strDateNow = CDateTime::GetCurrentDateTime().GetAsDBDateTime();
+ std::string sql = PrepareSQL("UPDATE song SET iTimesPlayed = iTimesPlayed+1, lastplayed ='%s' "
+ "WHERE idSong=%i",
+ strDateNow.c_str(), idSong);
+ m_pDS->exec(sql);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, item.GetPath());
+ }
+}
+
+bool CMusicDatabase::GetSongsByPath(const std::string& strPath1,
+ MAPSONGS& songmap,
+ bool bAppendToMap)
+{
+ std::string strPath(strPath1);
+ try
+ {
+ if (!URIUtils::HasSlashAtEnd(strPath))
+ URIUtils::AddSlashAtEnd(strPath);
+
+ if (!bAppendToMap)
+ songmap.clear();
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Filename is not unique for a path as songs from a cuesheet have same filename.
+ // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
+ // in a folder and some edited and rescanned.
+ // Hence order by filename so these songs can be gathered together.
+ std::string strSQL = PrepareSQL("SELECT * FROM songview "
+ "WHERE strPath='%s' ORDER BY strFileName",
+ strPath.c_str());
+ if (!m_pDS->query(strSQL))
+ return false;
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ // Each file is potentially mapped to a list of songs, gather these and save as list
+ VECSONGS songs;
+ std::string filename;
+ while (!m_pDS->eof())
+ {
+ CSong song = GetSongFromDataset();
+ if (!filename.empty() && filename != song.strFileName)
+ {
+ // Save songs for previous filename
+ songmap.insert(std::make_pair(filename, songs));
+ songs.clear();
+ }
+ filename = song.strFileName;
+ songs.emplace_back(song);
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strPath);
+ }
+
+ return false;
+}
+
+void CMusicDatabase::EmptyCache()
+{
+ m_genreCache.erase(m_genreCache.begin(), m_genreCache.end());
+ m_pathCache.erase(m_pathCache.begin(), m_pathCache.end());
+}
+
+bool CMusicDatabase::Search(const std::string& search, CFileItemList& items)
+{
+ auto start = std::chrono::steady_clock::now();
+ // first grab all the artists that match
+ SearchArtists(search, items);
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "{} Artist search in {} ms", __FUNCTION__, duration.count());
+
+ start = std::chrono::steady_clock::now();
+ // then albums that match
+ SearchAlbums(search, items);
+ end = std::chrono::steady_clock::now();
+ duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "{} Album search in {} ms", __FUNCTION__, duration.count());
+
+ start = std::chrono::steady_clock::now();
+ // and finally songs
+ SearchSongs(search, items);
+ end = std::chrono::steady_clock::now();
+ duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ CLog::Log(LOGDEBUG, "{} Songs search in {} ms", __FUNCTION__, duration.count());
+
+ return true;
+}
+
+bool CMusicDatabase::SearchSongs(const std::string& search, CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CMusicDbUrl baseUrl;
+ if (!baseUrl.FromString("musicdb://songs/"))
+ return false;
+
+ std::string strSQL;
+ if (search.size() >= MIN_FULL_SEARCH_LENGTH)
+ strSQL = PrepareSQL("SELECT * FROM songview "
+ "WHERE strTitle LIKE '%s%%' or strTitle LIKE '%% %s%%' LIMIT 1000",
+ search.c_str(), search.c_str());
+ else
+ strSQL = PrepareSQL("SELECT * FROM songview "
+ "WHERE strTitle LIKE '%s%%' LIMIT 1000",
+ search.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ return false;
+
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(item.get(), baseUrl);
+ items.Add(item);
+ m_pDS->next();
+ }
+
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::SearchAlbums(const std::string& search, CFileItemList& albums)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ if (search.size() >= MIN_FULL_SEARCH_LENGTH)
+ strSQL = PrepareSQL("SELECT * FROM albumview "
+ "WHERE strAlbum LIKE '%s%%' OR strAlbum LIKE '%% %s%%'",
+ search.c_str(), search.c_str());
+ else
+ strSQL = PrepareSQL("SELECT * FROM albumview "
+ "WHERE strAlbum LIKE '%s%%'",
+ search.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ const std::string& albumLabel(g_localizeStrings.Get(558)); // Album
+ while (!m_pDS->eof())
+ {
+ CAlbum album = GetAlbumFromDataset(m_pDS.get());
+ std::string path = StringUtils::Format("musicdb://albums/{}/", album.idAlbum);
+ CFileItemPtr pItem(new CFileItem(path, album));
+ std::string label = StringUtils::Format("[{}] {}", albumLabel, album.strAlbum);
+ pItem->SetLabel(label);
+ // sort label is stored in the title tag
+ label = StringUtils::Format("B {}", album.strAlbum);
+ pItem->GetMusicInfoTag()->SetTitle(label);
+ albums.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupSongsByIds(const std::string& strSongIds)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ // ok, now find all idSong's
+ std::string strSQL = PrepareSQL("SELECT * FROM song JOIN path ON song.idPath = path.idPath "
+ "WHERE song.idSong IN %s",
+ strSongIds.c_str());
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+ std::vector<std::string> songsToDelete;
+ while (!m_pDS->eof())
+ { // get the full song path
+ std::string strFileName = URIUtils::AddFileToFolder(
+ m_pDS->fv("path.strPath").get_asString(), m_pDS->fv("song.strFileName").get_asString());
+
+ // Special case for streams inside an audio decoder package file.
+ // The last dir in the path is the audio file that
+ // contains the stream, so test if its there
+ if (StringUtils::EndsWith(URIUtils::GetExtension(strFileName),
+ KODI_ADDON_AUDIODECODER_TRACK_EXT))
+ {
+ strFileName = URIUtils::GetDirectory(strFileName);
+ // we are dropping back to a file, so remove the slash at end
+ URIUtils::RemoveSlashAtEnd(strFileName);
+ }
+
+ if (!CFile::Exists(strFileName, false))
+ { // file no longer exists, so add to deletion list
+ songsToDelete.push_back(m_pDS->fv("song.idSong").get_asString());
+ }
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ if (!songsToDelete.empty())
+ {
+ std::string strSongsToDelete = "(" + StringUtils::Join(songsToDelete, ",") + ")";
+ // ok, now delete these songs + all references to them from the linked tables
+ strSQL = "delete from song where idSong in " + strSongsToDelete;
+ m_pDS->exec(strSQL);
+ m_pDS->close();
+ }
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongsFromPaths()");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupSongs(CGUIDialogProgress* progressDialog /*= nullptr*/)
+{
+ try
+ {
+ int total;
+ // Count total number of songs
+ total = GetSingleValueInt("SELECT COUNT(1) FROM song", m_pDS);
+ // No songs to clean
+ if (total == 0)
+ return true;
+
+ // run through all songs and get all unique path ids
+ int iLIMIT = 1000;
+ for (int i = 0;; i += iLIMIT)
+ {
+ std::string strSQL = PrepareSQL("SELECT song.idSong FROM song "
+ "ORDER BY song.idSong LIMIT %i OFFSET %i",
+ iLIMIT, i);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ // keep going until no rows are left!
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ std::vector<std::string> songIds;
+ while (!m_pDS->eof())
+ {
+ songIds.push_back(m_pDS->fv("song.idSong").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ std::string strSongIds = "(" + StringUtils::Join(songIds, ",") + ")";
+ CLog::Log(LOGDEBUG, "Checking songs from song ID list: {}", strSongIds);
+ if (progressDialog)
+ {
+ int percentage = i * 100 / total;
+ if (percentage > progressDialog->GetPercentage())
+ {
+ progressDialog->SetPercentage(percentage);
+ progressDialog->Progress();
+ }
+ if (progressDialog->IsCanceled())
+ {
+ m_pDS->close();
+ return false;
+ }
+ }
+ if (!CleanupSongsByIds(strSongIds))
+ return false;
+ }
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupSongs()");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupAlbums()
+{
+ try
+ {
+ // This must be run AFTER songs have been cleaned up
+ // delete albums with no reference to songs
+ std::string strSQL = "SELECT * FROM album "
+ "WHERE album.idAlbum NOT IN (SELECT idAlbum FROM song)";
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ std::vector<std::string> albumIds;
+ while (!m_pDS->eof())
+ {
+ albumIds.push_back(m_pDS->fv("album.idAlbum").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ std::string strAlbumIds = "(" + StringUtils::Join(albumIds, ",") + ")";
+ // ok, now we can delete them and the references in the linked tables
+ strSQL = "delete from album where idAlbum in " + strAlbumIds;
+ m_pDS->exec(strSQL);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupAlbums()");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupPaths()
+{
+ try
+ {
+ // needs to be done AFTER the songs and albums have been cleaned up.
+ // we can happily delete any path that has no reference to a song
+ // but we must keep all paths that have been scanned that may contain songs in subpaths
+
+ // first create a temporary table of song paths
+ m_pDS->exec("CREATE TEMPORARY TABLE songpaths (idPath integer, strPath varchar(512))\n");
+ m_pDS->exec("INSERT INTO songpaths "
+ "SELECT idPath, strPath FROM path "
+ "WHERE idPath IN (SELECT idPath FROM song)\n");
+
+ // grab all paths that aren't immediately connected with a song
+ std::string sql = "SELECT * FROM path WHERE idPath NOT IN (SELECT idPath FROM song)";
+ if (!m_pDS->query(sql))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+ // and construct a list to delete
+ std::vector<std::string> pathIds;
+ while (!m_pDS->eof())
+ {
+ // anything that isn't a parent path of a song path is to be deleted
+ std::string path = m_pDS->fv("strPath").get_asString();
+ sql = PrepareSQL("SELECT COUNT(idPath) FROM songpaths WHERE SUBSTR(strPath,1,%i)='%s'",
+ StringUtils::utf8_strlen(path.c_str()), path.c_str());
+ if (m_pDS2->query(sql) && m_pDS2->num_rows() == 1 && m_pDS2->fv(0).get_asInt() == 0)
+ pathIds.push_back(m_pDS->fv("idPath").get_asString()); // nothing found, so delete
+ m_pDS2->close();
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ if (!pathIds.empty())
+ {
+ // do the deletion, and drop our temp table
+ std::string deleteSQL =
+ "DELETE FROM path WHERE idPath IN (" + StringUtils::Join(pathIds, ",") + ")";
+ m_pDS->exec(deleteSQL);
+ }
+ m_pDS->exec("drop table songpaths");
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupPaths() or was aborted");
+ }
+ return false;
+}
+
+bool CMusicDatabase::InsideScannedPath(const std::string& path)
+{
+ std::string sql = PrepareSQL("SELECT idPath FROM path WHERE SUBSTR(strPath,1,%i)='%s' LIMIT 1",
+ path.size(), path.c_str());
+ return !GetSingleValue(sql).empty();
+}
+
+bool CMusicDatabase::CleanupArtists()
+{
+ try
+ {
+ // (nested queries by Bobbin007)
+ // must be executed AFTER the song, album and their artist link tables are cleaned.
+ // Don't delete [Missing] the missing artist tag artist
+
+ // Create temp table to avoid 1442 trigger hell on mysql
+ m_pDS->exec("CREATE TEMPORARY TABLE tmp_delartists (idArtist integer)");
+ m_pDS->exec("INSERT INTO tmp_delartists select idArtist from song_artist");
+ m_pDS->exec("INSERT INTO tmp_delartists select idArtist from album_artist");
+ m_pDS->exec(PrepareSQL("INSERT INTO tmp_delartists VALUES(%i)", BLANKARTIST_ID));
+ // tmp_delartists contains duplicate ids, and on a large library with small changes can be very large.
+ // To avoid MySQL hanging or timeout create a table of unique ids with primary key
+ m_pDS->exec("CREATE TEMPORARY TABLE tmp_keep (idArtist INTEGER PRIMARY KEY)");
+ m_pDS->exec("INSERT INTO tmp_keep SELECT DISTINCT idArtist from tmp_delartists");
+ m_pDS->exec("DELETE FROM artist WHERE idArtist NOT IN (SELECT idArtist FROM tmp_keep)");
+ // Tidy up temp tables
+ m_pDS->exec("DROP TABLE tmp_delartists");
+ m_pDS->exec("DROP TABLE tmp_keep");
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupArtists() or was aborted");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupGenres()
+{
+ try
+ {
+ // Cleanup orphaned song genres (ie those that don't belong to a song entry)
+ // (nested queries by Bobbin007)
+ // Must be executed AFTER the song, and song_genre have been cleaned.
+ std::string strSQL = "DELETE FROM genre WHERE idGenre NOT IN (SELECT idGenre FROM song_genre)";
+ m_pDS->exec(strSQL);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupGenres() or was aborted");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupInfoSettings()
+{
+ try
+ {
+ // Cleanup orphaned info settings (ie those that don't belong to an album or artist entry)
+ // Must be executed AFTER the album and artist tables have been cleaned.
+ std::string strSQL = "DELETE FROM infosetting "
+ "WHERE idSetting NOT IN (SELECT idInfoSetting FROM artist) "
+ "AND idSetting NOT IN (SELECT idInfoSetting FROM album)";
+ m_pDS->exec(strSQL);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupInfoSettings() or was aborted");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupRoles()
+{
+ try
+ {
+ // Cleanup orphaned roles (ie those that don't belong to a song entry)
+ // Must be executed AFTER the song, and song_artist tables have been cleaned.
+ // Do not remove default role (ROLE_ARTIST)
+ std::string strSQL = "DELETE FROM role "
+ "WHERE idRole > 1 AND idRole NOT IN (SELECT idRole FROM song_artist)";
+ m_pDS->exec(strSQL);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::CleanupRoles() or was aborted");
+ }
+ return false;
+}
+
+bool CMusicDatabase::DeleteRemovedLinks()
+{
+ try
+ {
+ std::string strSQL = "DELETE FROM removed_link";
+ m_pDS->exec(strSQL);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicDatabase::DeleteRemovedLinks");
+ }
+ return false;
+}
+
+bool CMusicDatabase::CleanupOrphanedItems()
+{
+ // paths aren't cleaned up here - they're cleaned up in RemoveSongsFromPath()
+ // remove_links not cleared here - done in CheckArtistLinksChanged()
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ SetLibraryLastUpdated();
+ if (!CleanupAlbums())
+ return false;
+ if (!CleanupArtists())
+ return false;
+ if (!CleanupGenres())
+ return false;
+ if (!CleanupRoles())
+ return false;
+ if (!CleanupInfoSettings())
+ return false;
+ return true;
+}
+
+int CMusicDatabase::Cleanup(CGUIDialogProgress* progressDialog /*= nullptr*/)
+{
+ if (nullptr == m_pDB)
+ return ERROR_DATABASE;
+ if (nullptr == m_pDS)
+ return ERROR_DATABASE;
+
+ int ret;
+ std::chrono::seconds duration;
+ auto time = std::chrono::steady_clock::now();
+ CLog::Log(LOGINFO, "{}: Starting musicdatabase cleanup ..", __FUNCTION__);
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanStarted");
+
+ SetLibraryLastCleaned();
+
+ // Drop triggers song_artist and album_artist to avoid creation of entries in removed_link
+ // Check that triggers actually exist first as interrupting the clean causes them to not be
+ // re-created
+
+ m_pDS->exec("DROP TRIGGER IF EXISTS tgrDeleteSongArtist");
+ m_pDS->exec("DROP TRIGGER IF EXISTS tgrDeleteAlbumArtist");
+
+ // first cleanup any songs with invalid paths
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{318});
+ progressDialog->SetLine(2, CVariant{330});
+ progressDialog->SetPercentage(0);
+ progressDialog->Progress();
+ }
+ if (!CleanupSongs(progressDialog))
+ {
+ ret = ERROR_REORG_SONGS;
+ goto error;
+ }
+ // then the albums that are not linked to a song or to album, or whose path is removed
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{326});
+ progressDialog->SetPercentage(20);
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ ret = ERROR_CANCEL;
+ goto error;
+ }
+ }
+ if (!CleanupAlbums())
+ {
+ ret = ERROR_REORG_ALBUM;
+ goto error;
+ }
+ // now the paths
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{324});
+ progressDialog->SetPercentage(40);
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ ret = ERROR_CANCEL;
+ goto error;
+ }
+ }
+ if (!CleanupPaths())
+ {
+ ret = ERROR_REORG_PATH;
+ goto error;
+ }
+ // and finally artists + genres
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{320});
+ progressDialog->SetPercentage(60);
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ ret = ERROR_CANCEL;
+ goto error;
+ }
+ }
+ if (!CleanupArtists())
+ {
+ ret = ERROR_REORG_ARTIST;
+ goto error;
+ }
+ //Genres, roles and info settings progress in one step
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{322});
+ progressDialog->SetPercentage(80);
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ ret = ERROR_CANCEL;
+ goto error;
+ }
+ }
+ if (!CleanupGenres())
+ {
+ ret = ERROR_REORG_OTHER;
+ goto error;
+ }
+ if (!CleanupRoles())
+ {
+ ret = ERROR_REORG_OTHER;
+ goto error;
+ }
+ if (!CleanupInfoSettings())
+ {
+ ret = ERROR_REORG_OTHER;
+ goto error;
+ }
+ if (!DeleteRemovedLinks())
+ {
+ ret = ERROR_REORG_OTHER;
+ goto error;
+ }
+
+ // commit transaction
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{328});
+ progressDialog->SetPercentage(90);
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ ret = ERROR_CANCEL;
+ goto error;
+ }
+ }
+ if (!CommitTransaction())
+ {
+ ret = ERROR_WRITING_CHANGES;
+ goto error;
+ }
+
+ // Recreate DELETE triggers on song_artist and album_artist
+ CreateRemovedLinkTriggers();
+
+ // and compress the database
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{331});
+ progressDialog->SetPercentage(100);
+ progressDialog->Close();
+ }
+
+ duration =
+ std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - time);
+ CLog::Log(LOGINFO, "{}: Cleaning musicdatabase done. Operation took {}s", __FUNCTION__,
+ duration.count());
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
+
+ if (!Compress(false))
+ {
+ return ERROR_COMPRESSING;
+ }
+ return ERROR_OK;
+
+error:
+ RollbackTransaction();
+ // Recreate DELETE triggers on song_artist and album_artist
+ CreateRemovedLinkTriggers();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnCleanFinished");
+ return ret;
+}
+
+bool CMusicDatabase::TrimImageURLs(std::string& strImage, const size_t space)
+{
+ if (strImage.length() > space)
+ {
+ strImage = strImage.substr(0, space);
+ // Tidy to last </thumb> tag
+ size_t iPos = strImage.rfind("</thumb>");
+ if (iPos == std::string::npos)
+ return false;
+ strImage = strImage.substr(0, iPos + 8);
+ }
+ return true;
+}
+
+bool CMusicDatabase::LookupCDDBInfo(bool bRequery /*=false*/)
+{
+#ifdef HAS_DVD_DRIVE
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_AUDIOCDS_USECDDB))
+ return false;
+
+ // check network connectivity
+ if (!CServiceBroker::GetNetwork().IsAvailable())
+ return false;
+
+ // Get information for the inserted disc
+ CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
+ if (pCdInfo == NULL)
+ return false;
+
+ // If the disc has no tracks, we are finished here.
+ int nTracks = pCdInfo->GetTrackCount();
+ if (nTracks <= 0)
+ return false;
+
+ // Delete old info if any
+ if (bRequery)
+ {
+ std::string strFile = StringUtils::Format("{:x}.cddb", pCdInfo->GetCddbDiscId());
+ CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
+ }
+
+ // Prepare cddb
+ Xcddb cddb;
+ cddb.setCacheDir(m_profileManager.GetCDDBFolder());
+
+ // Do we have to look for cddb information
+ if (pCdInfo->HasCDDBInfo() && !cddb.isCDCached(pCdInfo))
+ {
+ CGUIDialogProgress* pDialogProgress =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
+ WINDOW_DIALOG_PROGRESS);
+ CGUIDialogSelect* pDlgSelect =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+
+ if (!pDialogProgress)
+ return false;
+ if (!pDlgSelect)
+ return false;
+
+ // Show progress dialog if we have to connect to freedb.org
+ pDialogProgress->SetHeading(CVariant{255}); //CDDB
+ pDialogProgress->SetLine(0, CVariant{""}); // Querying freedb for CDDB info
+ pDialogProgress->SetLine(1, CVariant{256});
+ pDialogProgress->SetLine(2, CVariant{""});
+ pDialogProgress->ShowProgressBar(false);
+ pDialogProgress->Open();
+
+ // get cddb information
+ if (!cddb.queryCDinfo(pCdInfo))
+ {
+ pDialogProgress->Close();
+ int lasterror = cddb.getLastError();
+
+ // Have we found more then on match in cddb for this disc,...
+ if (lasterror == E_WAIT_FOR_INPUT)
+ {
+ // ...yes, show the matches found in a select dialog
+ // and let the user choose an entry.
+ pDlgSelect->Reset();
+ pDlgSelect->SetHeading(CVariant{255});
+ int i = 1;
+ while (true)
+ {
+ std::string strTitle = cddb.getInexactTitle(i);
+ if (strTitle == "")
+ break;
+
+ const std::string& strArtist = cddb.getInexactArtist(i);
+ if (!strArtist.empty())
+ strTitle += " - " + strArtist;
+
+ pDlgSelect->Add(strTitle);
+ i++;
+ }
+ pDlgSelect->Open();
+
+ // Has the user selected a match...
+ int iSelectedCD = pDlgSelect->GetSelectedItem();
+ if (iSelectedCD >= 0)
+ {
+ // ...query cddb for the inexact match
+ if (!cddb.queryCDinfo(pCdInfo, 1 + iSelectedCD))
+ pCdInfo->SetNoCDDBInfo();
+ }
+ else
+ pCdInfo->SetNoCDDBInfo();
+ }
+ else if (lasterror == E_NO_MATCH_FOUND)
+ {
+ pCdInfo->SetNoCDDBInfo();
+ }
+ else
+ {
+ pCdInfo->SetNoCDDBInfo();
+ // ..no, an error occurred, display it to the user
+ std::string strErrorText =
+ StringUtils::Format("[{}] {}", cddb.getLastError(), cddb.getLastErrorText());
+ HELPERS::ShowOKDialogLines(CVariant{255}, CVariant{257}, CVariant{std::move(strErrorText)},
+ CVariant{0});
+ }
+ } // if ( !cddb.queryCDinfo( pCdInfo ) )
+ else
+ pDialogProgress->Close();
+ }
+
+ // Filling the file items with cddb info happens in CMusicInfoTagLoaderCDDA
+
+ return pCdInfo->HasCDDBInfo();
+#else
+ return false;
+#endif
+}
+
+void CMusicDatabase::DeleteCDDBInfo()
+{
+#ifdef HAS_DVD_DRIVE
+ CFileItemList items;
+ if (!CDirectory::GetDirectory(m_profileManager.GetCDDBFolder(), items, ".cddb",
+ DIR_FLAG_NO_FILE_DIRS))
+ {
+ HELPERS::ShowOKDialogText(CVariant{313}, CVariant{426});
+ return;
+ }
+ // Show a selectdialog that the user can select the album to delete
+ CGUIDialogSelect* pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (pDlg)
+ {
+ pDlg->SetHeading(CVariant{g_localizeStrings.Get(181)});
+ pDlg->Reset();
+
+ std::map<uint32_t, std::string> mapCDDBIds;
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ if (items[i]->m_bIsFolder)
+ continue;
+
+ std::string strFile = URIUtils::GetFileName(items[i]->GetPath());
+ strFile.erase(strFile.size() - 5, 5);
+ uint32_t lDiscId = strtoul(strFile.c_str(), NULL, 16);
+ Xcddb cddb;
+ cddb.setCacheDir(m_profileManager.GetCDDBFolder());
+
+ if (!cddb.queryCache(lDiscId))
+ continue;
+
+ std::string strDiskTitle, strDiskArtist;
+ cddb.getDiskTitle(strDiskTitle);
+ cddb.getDiskArtist(strDiskArtist);
+
+ std::string str;
+ if (strDiskArtist.empty())
+ str = strDiskTitle;
+ else
+ str = strDiskTitle + " - " + strDiskArtist;
+
+ pDlg->Add(str);
+ mapCDDBIds.insert(std::pair<uint32_t, std::string>(lDiscId, str));
+ }
+
+ pDlg->Sort();
+ pDlg->Open();
+
+ // and wait till user selects one
+ int iSelectedAlbum = pDlg->GetSelectedItem();
+ if (iSelectedAlbum < 0)
+ {
+ mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
+ return;
+ }
+
+ std::string strSelectedAlbum = pDlg->GetSelectedFileItem()->GetLabel();
+ for (const auto& i : mapCDDBIds)
+ {
+ if (i.second == strSelectedAlbum)
+ {
+ std::string strFile = StringUtils::Format("{:x}.cddb", (unsigned int)i.first);
+ CFile::Delete(URIUtils::AddFileToFolder(m_profileManager.GetCDDBFolder(), strFile));
+ break;
+ }
+ }
+ mapCDDBIds.erase(mapCDDBIds.begin(), mapCDDBIds.end());
+ }
+#endif
+}
+
+void CMusicDatabase::Clean()
+{
+ // If we are scanning for music info in the background,
+ // other writing access to the database is prohibited.
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ HELPERS::ShowOKDialogText(CVariant{189}, CVariant{14057});
+ return;
+ }
+
+ if (HELPERS::ShowYesNoDialogText(CVariant{313}, CVariant{333}) == DialogResponse::CHOICE_YES)
+ {
+ CMusicDatabase musicdatabase;
+ if (musicdatabase.Open())
+ {
+ int iReturnString = musicdatabase.Cleanup();
+ musicdatabase.Close();
+
+ if (iReturnString != ERROR_OK)
+ {
+ HELPERS::ShowOKDialogText(CVariant{313}, CVariant{iReturnString});
+ }
+ }
+ }
+}
+
+bool CMusicDatabase::GetGenresNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // get primary genres for songs - could be simplified to just SELECT * FROM genre?
+ std::string strSQL = "SELECT %s FROM genre ";
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // if there are extra WHERE conditions we might need access
+ // to songview or albumview for these conditions
+ if (!extFilter.where.empty())
+ {
+ if (extFilter.where.find("artistview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
+ extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
+ extFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = songview.idSong");
+ extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = song_artist.idArtist");
+ }
+ else if (extFilter.where.find("songview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
+ extFilter.AppendJoin("JOIN songview ON songview.idSong = song_genre.idSong");
+ }
+ else if (extFilter.where.find("albumview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
+ extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
+ extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = song.idAlbum");
+ }
+ extFilter.AppendGroup("genre.idGenre");
+ }
+ extFilter.AppendWhere("genre.strGenre != ''");
+
+ if (countOnly)
+ {
+ extFilter.fields = "COUNT(DISTINCT genre.idGenre)";
+ extFilter.group.clear();
+ extFilter.order.clear();
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() && extFilter.fields.compare("*") != 0
+ ? extFilter.fields.c_str()
+ : "genre.*") +
+ strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv("genre.strGenre").get_asString()));
+ pItem->GetMusicInfoTag()->SetGenre(m_pDS->fv("genre.strGenre").get_asString());
+ pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("genre.idGenre").get_asInt(), "genre");
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv("genre.idGenre").get_asInt());
+ itemUrl.AppendPath(strDir);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSourcesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /*= Filter()*/,
+ bool countOnly /*= false*/)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Get sources for selection list when add/edit filter or smartplaylist rule
+ std::string strSQL = "SELECT %s FROM source ";
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // if there are extra WHERE conditions we might need access
+ // to songview or albumview for these conditions
+ if (!extFilter.where.empty())
+ {
+ if (extFilter.where.find("artistview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
+ extFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = album_source.idAlbum");
+ extFilter.AppendJoin("JOIN artistview ON artistview.idArtist = album_artist.idArtist");
+ }
+ else if (extFilter.where.find("songview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
+ extFilter.AppendJoin("JOIN songview ON songview.idAlbum = album_source .idAlbum");
+ }
+ else if (extFilter.where.find("albumview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
+ extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = album_source .idAlbum");
+ }
+ extFilter.AppendGroup("source.idSource");
+ }
+ else
+ { // Get only sources that have been scanned into music library
+ extFilter.AppendJoin("JOIN album_source ON album_source.idSource = source.idSource");
+ extFilter.AppendGroup("source.idSource");
+ }
+
+ if (countOnly)
+ {
+ extFilter.fields = "COUNT(DISTINCT source.idSource)";
+ extFilter.group.clear();
+ extFilter.order.clear();
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() && extFilter.fields.compare("*") != 0
+ ? extFilter.fields.c_str()
+ : "source.*") +
+ strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
+ pItem->GetMusicInfoTag()->SetTitle(m_pDS->fv("source.strName").get_asString());
+ pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("source.idSource").get_asInt(), "source");
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv("source.idSource").get_asInt());
+ itemUrl.AppendPath(strDir);
+ itemUrl.AddOption("sourceid", m_pDS->fv("source.idSource").get_asInt());
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetYearsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting;
+ std::string strSQL;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ bool useOriginalYears = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
+
+ useOriginalYears =
+ useOriginalYears || StringUtils::StartsWith(strBaseDir, "musicdb://originalyears/");
+
+ if (!useOriginalYears)
+ { // Get years from year part of release date
+ strSQL = "SELECT DISTINCT CAST(strReleaseDate AS INTEGER) AS year FROM albumview ";
+ extFilter.AppendWhere("(TRIM(strReleaseDate) <> '' AND strReleaseDate IS NOT NULL)");
+ }
+ else
+ { // Get years from year part of original date
+ strSQL = "SELECT DISTINCT CAST(strOrigReleaseDate AS INTEGER) AS year FROM albumview ";
+ extFilter.AppendWhere("(TRIM(strOrigReleaseDate) <> '' AND strOrigReleaseDate IS NOT NULL)");
+ }
+ if (!BuildSQL(strSQL, extFilter, strSQL))
+ return false;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(0).get_asString()));
+ pItem->GetMusicInfoTag()->SetYear(m_pDS->fv(0).get_asInt());
+ if (useOriginalYears)
+ pItem->GetMusicInfoTag()->SetDatabaseId(-1, "originalyear");
+ else
+ pItem->GetMusicInfoTag()->SetDatabaseId(-1, "year");
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ itemUrl.AppendPath(strDir);
+ if (useOriginalYears)
+ itemUrl.AddOption("useoriginalyear", true);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetRolesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // get roles with artists having that role
+ std::string strSQL = "SELECT DISTINCT role.idRole, role.strRole FROM role "
+ "JOIN song_artist ON song_artist.idRole = role.idRole ";
+
+ if (!BuildSQL(strSQL, extFilter, strSQL))
+ return false;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ while (!m_pDS->eof())
+ {
+ std::string labelValue = m_pDS->fv("role.strRole").get_asString();
+ CFileItemPtr pItem(new CFileItem(labelValue));
+ pItem->GetMusicInfoTag()->SetTitle(labelValue);
+ pItem->GetMusicInfoTag()->SetDatabaseId(m_pDS->fv("role.idRole").get_asInt(), "role");
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv("role.idRole").get_asInt());
+ itemUrl.AppendPath(strDir);
+ itemUrl.AddOption("roleid", m_pDS->fv("role.idRole").get_asInt());
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumsByYear(const std::string& strBaseDir, CFileItemList& items, int year)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ musicUrl.AddOption("year", year);
+ musicUrl.AddOption("show_singles", true); // allow singles to be listed
+
+ Filter filter;
+ return GetAlbumsByWhere(musicUrl.ToString(), filter, items);
+}
+
+bool CMusicDatabase::GetCommonNav(const std::string& strBaseDir,
+ const std::string& table,
+ const std::string& labelField,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (table.empty() || labelField.empty())
+ return false;
+
+ try
+ {
+ Filter extFilter = filter;
+ std::string strSQL = "SELECT %s FROM " + table + " ";
+ extFilter.AppendGroup(labelField);
+ extFilter.AppendWhere(labelField + " != ''");
+
+ if (countOnly)
+ {
+ extFilter.fields = "COUNT(DISTINCT " + labelField + ")";
+ extFilter.group.clear();
+ extFilter.order.clear();
+ }
+
+ // Do prepare before add where as it could contain a LIKE statement with wild card that upsets format
+ // e.g. LIKE '%symphony%' would be taken as a %s format argument
+ strSQL = PrepareSQL(strSQL,
+ !extFilter.fields.empty() ? extFilter.fields.c_str() : labelField.c_str());
+
+ CMusicDbUrl musicUrl;
+ if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, musicUrl))
+ return false;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound <= 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ // get data from returned rows
+ while (!m_pDS->eof())
+ {
+ std::string labelValue = m_pDS->fv(labelField.c_str()).get_asString();
+ CFileItemPtr pItem(new CFileItem(labelValue));
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string strDir = StringUtils::Format("{}/", labelValue);
+ itemUrl.AppendPath(strDir);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+
+ m_pDS->next();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumTypesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ return GetCommonNav(strBaseDir, "albumview", "albumview.strType", items, filter, countOnly);
+}
+
+bool CMusicDatabase::GetMusicLabelsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ return GetCommonNav(strBaseDir, "albumview", "albumview.strLabel", items, filter, countOnly);
+}
+
+bool CMusicDatabase::GetArtistsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ bool albumArtistsOnly /* = false */,
+ int idGenre /* = -1 */,
+ int idAlbum /* = -1 */,
+ int idSong /* = -1 */,
+ const Filter& filter /* = Filter() */,
+ const SortDescription& sortDescription /* = SortDescription() */,
+ bool countOnly /* = false */)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ try
+ {
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ if (idGenre > 0)
+ musicUrl.AddOption("genreid", idGenre);
+ else if (idAlbum > 0)
+ musicUrl.AddOption("albumid", idAlbum);
+ else if (idSong > 0)
+ musicUrl.AddOption("songid", idSong);
+
+ // Override albumArtistsOnly parameter (usually externally set to SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS)
+ // when local option already present in music URL thus allowing it to be an option in custom nodes
+ if (!musicUrl.HasOption("albumartistsonly"))
+ musicUrl.AddOption("albumartistsonly", albumArtistsOnly);
+
+ bool result = GetArtistsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
+
+ return result;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetArtistsByWhere(
+ const std::string& strBaseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription /* = SortDescription() */,
+ bool countOnly /* = false */)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ auto start = std::chrono::steady_clock::now();
+ int total = -1;
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ bool extended = false;
+ bool limitedInSQL = extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
+
+ // if there are extra WHERE conditions (from media filter dialog) we might
+ // need access to songview or albumview for these conditions
+ if (!extFilter.where.empty())
+ {
+ if (extFilter.where.find("songview") != std::string::npos)
+ {
+ extended = true;
+ extFilter.AppendJoin("JOIN song_artist ON song_artist.idArtist = artistview.idArtist "
+ "JOIN songview ON songview.idSong = song_artist.idSong");
+ }
+ else if (extFilter.where.find("albumview") != std::string::npos)
+ {
+ extended = true;
+ extFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = artistview.idArtist "
+ "JOIN albumview ON albumview.idAlbum = album_artist.idAlbum");
+ }
+ if (extended)
+ extFilter.AppendGroup("artistview.idArtist"); // Only one row per artist despite joins
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count number of artsits that satisfy selection criteria (no limit built)
+ // Count done in full query fetch when unlimited
+ if (countOnly || limitedInSQL)
+ {
+ if (extended)
+ {
+ // Count distinct without group by
+ Filter countFilter = extFilter;
+ countFilter.group.clear();
+ std::string strSQLWhere;
+ if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
+ return false;
+ total = GetSingleValueInt(
+ "SELECT COUNT(DISTINCT artistview.idArtist) FROM artistview " + strSQLWhere, m_pDS);
+ }
+ else
+ total = GetSingleValueInt("SELECT COUNT(1) FROM artistview " + strSQLExtra, m_pDS);
+ }
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", total);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ // Apply any limiting directly in SQL and so sort as well
+ if (limitedInSQL)
+ {
+ extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
+ }
+
+ // Apply sort in SQL
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sorting.sortAttributes =
+ static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
+ // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
+ GetOrderFilter(MediaTypeArtist, sorting, extFilter);
+
+ strSQLExtra.clear();
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ std::string strSQL;
+ std::string strFields = "artistview.*";
+ if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
+ strFields = "artistview.*, " + extFilter.fields;
+ strSQL = "SELECT " + strFields + " FROM artistview " + strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ auto queryStart = std::chrono::steady_clock::now();
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ auto queryEnd = std::chrono::steady_clock::now();
+ auto queryDuration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
+
+ // Store the total number of artists as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+ // Populate results field vector from dataset
+ FieldList fields;
+ if (!DatabaseUtils::GetDatabaseResults(MediaTypeArtist, fields, m_pDS, results))
+ return false;
+ // Store item list sort order
+ items.SetSortMethod(sortDescription.sortBy);
+ items.SetSortOrder(sortDescription.sortOrder);
+
+ // Get Artists from returned rows
+ items.Reserve(results.size());
+ const dbiplus::query_data& data = m_pDS->get_result_set().records;
+ for (const auto& i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ try
+ {
+ CArtist artist = GetArtistFromDataset(record, false);
+ CFileItemPtr pItem(new CFileItem(artist));
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string path = StringUtils::Format("{}/", artist.idArtist);
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->GetMusicInfoTag()->SetDatabaseId(artist.idArtist, MediaTypeArtist);
+ // Set icon now to avoid slow per item processing in FillInDefaultIcon later
+ pItem->SetProperty("icon_never_overlay", true);
+ pItem->SetArt("icon", "DefaultArtist.png");
+
+ SetPropertiesFromArtist(*pItem, artist);
+ items.Add(pItem);
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
+ items.Size());
+ }
+ }
+ // cleanup
+ m_pDS->close();
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{0}: Time to fill list with artists {1} ms query took {2} ms",
+ __FUNCTION__, duration.count(), queryDuration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumFromSong(int idSong, CAlbum& album)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL = PrepareSQL("SELECT albumview.* FROM song "
+ "JOIN albumview on song.idAlbum = albumview.idAlbum "
+ "WHERE song.idSong='%i'",
+ idSong);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ album = GetAlbumFromDataset(m_pDS.get());
+
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idGenre /* = -1 */,
+ int idArtist /* = -1 */,
+ const Filter& filter /* = Filter() */,
+ const SortDescription& sortDescription /* = SortDescription() */,
+ bool countOnly /* = false */)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ // where clause
+ if (idGenre > 0)
+ musicUrl.AddOption("genreid", idGenre);
+
+ if (idArtist > 0)
+ musicUrl.AddOption("artistid", idArtist);
+
+ return GetAlbumsByWhere(musicUrl.ToString(), filter, items, sortDescription, countOnly);
+}
+
+bool CMusicDatabase::GetAlbumsByWhere(
+ const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription /* = SortDescription() */,
+ bool countOnly /* = false */)
+{
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ try
+ {
+ auto start = std::chrono::steady_clock::now();
+ int total = -1;
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ bool extended = false;
+ bool limitedInSQL = extFilter.limit.empty() && (sorting.limitStart > 0 || sorting.limitEnd > 0);
+
+ // If there are extra WHERE conditions (from media filter dialog) we might
+ // need access to songview for these conditions
+ if (extFilter.where.find("songview") != std::string::npos)
+ {
+ extended = true;
+ extFilter.AppendJoin("JOIN songview ON songview.idAlbum = albumview.idAlbum");
+ extFilter.AppendGroup("albumview.idAlbum");
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count number of albums that satisfy selection criteria (no limit built)
+ // Count done in full query fetch when unlimited
+ if (countOnly || limitedInSQL)
+ {
+ if (extended)
+ {
+ // Count distinct without group by
+ Filter countFilter = extFilter;
+ countFilter.group.clear();
+ std::string strSQLWhere;
+ if (!BuildSQL(strSQLWhere, countFilter, strSQLWhere))
+ return false;
+ total = GetSingleValueInt(
+ "SELECT COUNT(DISTINCT albumview.idAlbum) FROM albumview " + strSQLWhere, m_pDS);
+ }
+ else
+ total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
+ }
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", total);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ // Apply any limiting directly in SQL
+ if (limitedInSQL)
+ {
+ extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
+ }
+
+ // Apply sort in SQL
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sorting.sortAttributes =
+ static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
+ // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
+ GetOrderFilter(MediaTypeAlbum, sorting, extFilter);
+ // Modify order to use correct calculated year field
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
+ else
+ StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
+
+ strSQLExtra.clear();
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ std::string strSQL;
+ std::string strFields = "albumview.*";
+ if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
+ strFields = "albumview.*, " + extFilter.fields;
+ strSQL = "SELECT " + strFields + " FROM albumview " + strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ auto querytime = std::chrono::steady_clock::now();
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ auto queryEnd = std::chrono::steady_clock::now();
+ auto queryDuration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - querytime);
+
+ // Store the total number of albums as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+ // Populate results field vector from dataset
+ FieldList fields;
+ if (!DatabaseUtils::GetDatabaseResults(MediaTypeAlbum, fields, m_pDS, results))
+ return false;
+ // Store item list sort order
+ items.SetSortMethod(sorting.sortBy);
+ items.SetSortOrder(sorting.sortOrder);
+
+ // Get albums from returned rows
+ items.Reserve(results.size());
+ const dbiplus::query_data& data = m_pDS->get_result_set().records;
+ for (const auto& i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ try
+ {
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string path = StringUtils::Format("{}/", record->at(album_idAlbum).get_asInt());
+ itemUrl.AppendPath(path);
+
+ CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), GetAlbumFromDataset(record)));
+ // Set icon now to avoid slow per item processing in FillInDefaultIcon later
+ pItem->SetProperty("icon_never_overlay", true);
+ pItem->SetArt("icon", "DefaultAlbumCover.png");
+ items.Add(pItem);
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
+ items.Size());
+ }
+ }
+ // cleanup
+ m_pDS->close();
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{0}: Time to fill list with albums {1}ms query took {2}ms", __FUNCTION__,
+ duration.count(), queryDuration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filter.where);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetDiscsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idAlbum,
+ const Filter& filter,
+ const SortDescription& sortDescription,
+ bool countOnly)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ if (idAlbum > 0)
+ musicUrl.AddOption("albumid", idAlbum);
+
+ return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
+}
+
+bool CMusicDatabase::GetDiscsByWhere(const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription,
+ bool countOnly)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(baseDir))
+ return false;
+ return GetDiscsByWhere(musicUrl, filter, items, sortDescription, countOnly);
+}
+
+bool CMusicDatabase::GetDiscsByWhere(CMusicDbUrl& musicUrl,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription,
+ bool countOnly)
+{
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ try
+ {
+ auto start = std::chrono::steady_clock::now();
+ int total = -1;
+ std::string strSQL;
+
+ Filter extFilter = filter;
+ SortDescription sorting = sortDescription;
+
+ if (!GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ extFilter.AppendGroup("albumview.idAlbum, iDisc");
+
+ // If there are extra songview WHERE conditions adjust to song or albumview
+ // fields, and join Path table for strPath
+ // ! @todo: convert songview fields into to song or albumview fields
+ // But not sure we ever get songview fields in filter - REMOVE??
+ if (extFilter.where.find("songview.strPath") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Apply any limiting directly in SQL if there is either no special sorting or random sort
+ // When limited, random sort is also applied in SQL
+ bool limitedInSQL = extFilter.limit.empty() &&
+ (sorting.sortBy == SortByNone || sorting.sortBy == SortByRandom) &&
+ (sorting.limitStart > 0 || sorting.limitEnd > 0);
+
+ if (countOnly || limitedInSQL)
+ {
+ // Count number of discs that satisfy selection criteria
+ // (when fetching all records get total from row count of results dataset)
+ // Count not allow for same non-null title discs to be grouped together
+ strSQL = "SELECT iTrack >> 16 AS iDisc FROM albumview JOIN song on song.idAlbum = "
+ "albumview.idAlbum " +
+ strSQLExtra;
+ strSQL = "SELECT COUNT(1) FROM (" + strSQL + ") AS albumdisc ";
+ total = GetSingleValueInt(strSQL, m_pDS);
+ }
+ if (countOnly)
+ {
+ items.SetProperty("total", total);
+ return true;
+ }
+ // Apply limits and random sort order directly in SQL
+ if (limitedInSQL)
+ {
+ if (sorting.sortBy == SortByRandom)
+ strSQLExtra += PrepareSQL(" ORDER BY RANDOM()");
+ strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
+ }
+ else
+ strSQLExtra += PrepareSQL(" ORDER BY albumview.idAlbum, iDisc");
+
+ strSQL = "SELECT iTrack >> 16 AS iDisc, strDiscSubtitle, albumview.* "
+ "FROM albumview JOIN song on song.idAlbum = albumview.idAlbum " +
+ strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ auto queryStart = std::chrono::steady_clock::now();
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ auto queryEnd = std::chrono::steady_clock::now();
+ auto queryDuration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
+
+ // store the total value of items as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+
+ // Avoid sorting with limits, just fetch results from dataset
+ // Limit when SortByNone already applied in SQL,
+ // Need guaranteed ordering for dataset processing to group by disc title
+ // so apply sort later to fileitems list rather than dataset
+ sorting.sortBy = SortByNone;
+ if (!SortUtils::SortFromDataset(sorting, MediaTypeAlbum, m_pDS, results))
+ return false;
+
+ // Get data from returned rows, note possibly multiple albums although usually only one
+ items.Reserve(total);
+ int albumOffset = 2;
+ CAlbum album;
+ bool useTitle = true; // Assume we want to match by disc title later unless we have no titles
+ std::string oldDiscTitle;
+ const dbiplus::query_data& data = m_pDS->get_result_set().records;
+ for (const auto& i : results)
+ {
+ unsigned int targetRow = static_cast<unsigned int>(i.at(FieldRow).asInteger());
+ const dbiplus::sql_record* const record = data.at(targetRow);
+ try
+ {
+ if (album.idAlbum != record->at(albumOffset + album_idAlbum).get_asInt())
+ { // New album
+ useTitle = true;
+ album = GetAlbumFromDataset(record, albumOffset);
+ }
+
+ int discnum = record->at(0).get_asInt();
+ std::string strDiscSubtitle = record->at(1).get_asString();
+ if (strDiscSubtitle.empty())
+ { // Make (fake) disc title from disc number, group by disc number as no real title to match
+ strDiscSubtitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), discnum);
+ useTitle = false;
+ }
+ else if (oldDiscTitle == strDiscSubtitle)
+ { // disc title already added to list, fetch the next disc
+ continue;
+ }
+ oldDiscTitle = strDiscSubtitle;
+
+ CMusicDbUrl itemUrl = musicUrl;
+ std::string path = StringUtils::Format("{}/", discnum);
+ itemUrl.AppendPath(path);
+
+ // When disc titles are provided group discs together by title not number.
+ // For monster sets like https://musicbrainz.org/release/cc967f36-7e4e-4a5b-ae0d-f1a1ab2c9c5a
+ if (useTitle)
+ itemUrl.AddOption("disctitle", strDiscSubtitle.c_str());
+ else
+ itemUrl.AddOption("discid", discnum);
+ CFileItemPtr pItem(new CFileItem(itemUrl.ToString(), album));
+ pItem->SetLabel2(record->at(0).get_asString()); // GUI show label2 for disc sort order??
+ pItem->GetMusicInfoTag()->SetDiscNumber(discnum);
+ pItem->GetMusicInfoTag()->SetTitle(strDiscSubtitle);
+ pItem->SetLabel(strDiscSubtitle);
+ // Set icon now to avoid slow per item processing in FillInDefaultIcon later
+ pItem->SetProperty("icon_never_overlay", true);
+ pItem->SetArt("icon", "DefaultAlbumCover.png");
+ items.Add(pItem);
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} - out of memory getting listing (got {})", __FUNCTION__,
+ items.Size());
+ }
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ // Finally do any sorting in items list we have not been able to do before in SQL or dataset,
+ // that is when have join with songartistview and sorting other than random with limit
+ if (sorting.sortBy != SortByNone && !(limitedInSQL && sorting.sortBy == SortByRandom))
+ items.Sort(sorting);
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{0}: Time to fill list with discs {1}ms query took {2}ms", __FUNCTION__,
+ duration.count(), queryDuration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filter.where);
+ }
+
+ return false;
+}
+int CMusicDatabase::GetDiscsCount(const std::string& baseDir, const Filter& filter /* = Filter() */)
+{
+ int iDiscTotal = -1;
+ CFileItemList itemscount;
+ if (GetDiscsByWhere(baseDir, filter, itemscount, SortDescription(), true))
+ iDiscTotal = itemscount.GetProperty("total").asInteger32();
+ return iDiscTotal;
+}
+
+bool CMusicDatabase::GetSongsFullByWhere(
+ const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription /* = SortDescription() */,
+ bool artistData /* = false*/)
+{
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ try
+ {
+ auto start = std::chrono::steady_clock::now();
+ int total = -1;
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ bool extended = false;
+ bool limitedInSQL =
+ extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0);
+
+ // If there are extra WHERE conditions (from media filter dialog) we might
+ // need access to albumview for these conditions
+ if (extFilter.where.find("albumview") != std::string::npos)
+ {
+ extended = true;
+ extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
+ }
+
+ // Build songview <where> for count
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count (without group by) number of songs that satisfy selection criteria
+ // Much quicker to use song table, not songview, when filtering only on song fields
+ if (extended ||
+ (!extFilter.where.empty() && (extFilter.where.find("strAlbum") != std::string::npos ||
+ extFilter.where.find("strPath") != std::string::npos ||
+ extFilter.where.find("bCompilation") != std::string::npos ||
+ extFilter.where.find("bBoxedset") != std::string::npos)))
+ total = GetSingleValueInt("SELECT COUNT(1) FROM songview " + strSQLExtra, m_pDS);
+ else
+ {
+ std::string strSQLsong = strSQLExtra;
+ StringUtils::Replace(strSQLsong, "songview", "song");
+ total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLsong, m_pDS);
+ }
+
+ if (extended)
+ extFilter.AppendGroup("songview.idSong");
+
+ // Apply any limiting directly in SQL
+ if (limitedInSQL)
+ {
+ extFilter.limit = DatabaseUtils::BuildLimitClauseOnly(sorting.limitEnd, sorting.limitStart);
+ }
+
+ // Apply sort in SQL
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME))
+ sorting.sortAttributes =
+ static_cast<SortAttribute>(sorting.sortAttributes | SortAttributeUseArtistSortName);
+ // Set Orderby and add any extra fields needed for sort e.g. "artistname" scalar query
+ GetOrderFilter(MediaTypeSong, sorting, extFilter);
+ // Modify order to use correct calculated year field
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ StringUtils::Replace(extFilter.order, "iYear", "CAST(strReleaseDate AS INTEGER)");
+ else
+ StringUtils::Replace(extFilter.order, "iYear", "CAST(strOrigReleaseDate AS INTEGER)");
+
+ std::string strFields = "songview.*";
+ if (!artistData || limitedInSQL)
+ {
+ // Build songview <where> + <order by> + <limits>
+ strSQLExtra.clear();
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+ }
+ else
+ strFields = "songview.*, songartistview.*";
+ if (!extFilter.fields.empty() && extFilter.fields.compare("*") != 0)
+ strFields = strFields + ", " + extFilter.fields;
+
+ std::string strSQL;
+ if (artistData)
+ { // Get data from song and song_artist tables to fully populate songs with artists
+ // All songs now have at least one artist so inner join sufficient
+ // Build songartistview JOIN part of query
+ Filter joinFilter;
+ std::string strSQLJoin;
+ joinFilter.AppendJoin("JOIN songartistview ON songartistview.idSong = songview.idSong");
+ if (sortDescription.sortBy == SortByRandom)
+ joinFilter.AppendOrder("songartistview.idSong");
+ else
+ joinFilter.order = extFilter.order;
+ if (limitedInSQL)
+ {
+ StringUtils::Replace(joinFilter.join, "songview.idSong", "sv.idSong");
+ StringUtils::Replace(joinFilter.order, "songview.", "sv.");
+ }
+ else
+ joinFilter.where = extFilter.where;
+ joinFilter.AppendOrder("songartistview.idRole");
+ joinFilter.AppendOrder("songartistview.iOrder");
+ if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
+ return false;
+
+ if (limitedInSQL)
+ {
+ // When have artist data (all roles) and LIMIT on songs use inline view
+ // SELECT sv.*, songartistview.* FROM
+ // (SELECT songview.* FROM songview <where> + <order by> + <limits> ) AS sv
+ // <order by sv fields>, songartistview.idRole, songartistview.iOrder
+ // Apply where clause, limits and order to songview, then join to songartistview this gives
+ // multiple records per song in result set
+ strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
+ strSQL = "(" + strSQL + ") AS sv ";
+ strSQL = "SELECT sv.*, songartistview.* FROM " + strSQL + strSQLJoin;
+ }
+ else
+ strSQL = "SELECT " + strFields + " FROM songview " + strSQLJoin;
+ }
+ else
+ strSQL = "SELECT " + strFields + " FROM songview " + strSQLExtra;
+
+ CLog::Log(LOGDEBUG, "{} query = {}", __FUNCTION__, strSQL);
+ auto queryStart = std::chrono::steady_clock::now();
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ auto queryEnd = std::chrono::steady_clock::now();
+ auto queryDuration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(queryEnd - queryStart);
+
+ // Store the total number of songs as a property
+ items.SetProperty("total", total);
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+ // Populate results field vector from dataset
+ FieldList fields;
+ if (!DatabaseUtils::GetDatabaseResults(MediaTypeSong, fields, m_pDS, results))
+ return false;
+ // Store item list sort order
+ items.SetSortMethod(sorting.sortBy);
+ items.SetSortOrder(sorting.sortOrder);
+
+ // Get songs from returned rows. If join songartistview then there is a row for every artist
+ items.Reserve(total);
+ int songArtistOffset = song_enumCount;
+ int songId = -1;
+ VECARTISTCREDITS artistCredits;
+ const dbiplus::query_data& data = m_pDS->get_result_set().records;
+ int count = 0;
+ for (const auto& i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ try
+ {
+ if (songId != record->at(song_idSong).get_asInt())
+ { //New song
+ if (songId > 0 && !artistCredits.empty())
+ {
+ //Store artist credits for previous song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+ songId = record->at(song_idSong).get_asInt();
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(record, item.get(), musicUrl);
+ // HACK for sorting by database returned order
+ item->m_iprogramCount = ++count;
+ // Set icon now to avoid slow per item processing in FillInDefaultIcon later
+ item->SetProperty("icon_never_overlay", true);
+ item->SetArt("icon", "DefaultAudio.png");
+ items.Add(item);
+ }
+ // Get song artist credits and contributors
+ if (artistData)
+ {
+ int idSongArtistRole = record->at(songArtistOffset + artistCredit_idRole).get_asInt();
+ if (idSongArtistRole == ROLE_ARTIST)
+ artistCredits.push_back(GetArtistCreditFromDataset(record, songArtistOffset));
+ else
+ items[items.Size() - 1]->GetMusicInfoTag()->AppendArtistRole(
+ GetArtistRoleFromDataset(record, songArtistOffset));
+ }
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{}: out of memory loading query: {}", __FUNCTION__, filter.where);
+ return (items.Size() > 0);
+ }
+ }
+ if (!artistCredits.empty())
+ {
+ //Store artist credits for final song
+ GetFileItemFromArtistCredits(artistCredits, items[items.Size() - 1].get());
+ artistCredits.clear();
+ }
+ // cleanup
+ m_pDS->close();
+
+ // Ensure random order of item list when results set sorted by idSong for artist processing
+ // Note while smartplaylists and xml nodes provide sort order, sort is not passed in from node
+ // navigation. Order is read later from view state and list sorting is then triggered by
+ // CGUIMediaWindow::Update in both cases.
+ // So sorting here is currently redundant, but the consistent place to do it.
+ // !@ todo: do sorting once, preferably in SQL
+ if (sorting.sortBy == SortByRandom && artistData)
+ items.Sort(sorting);
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{0}: Time to fill list with songs {1}ms query took {2}ms", __FUNCTION__,
+ duration.count(), queryDuration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ // cleanup
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSongsByWhere(
+ const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ try
+ {
+ int total = -1;
+
+ std::string strSQL = "SELECT %s FROM songview ";
+
+ Filter extFilter = filter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // if there are extra WHERE conditions we might need access
+ // to songview for these conditions
+ if (extFilter.where.find("albumview") != std::string::npos)
+ {
+ extFilter.AppendJoin("JOIN albumview ON albumview.idAlbum = songview.idAlbum");
+ extFilter.AppendGroup("songview.idSong");
+ }
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Apply the limiting directly here if there's no special sorting but limiting
+ if (extFilter.limit.empty() && sorting.sortBy == SortByNone &&
+ (sorting.limitStart > 0 || sorting.limitEnd > 0))
+ {
+ total = GetSingleValueInt(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS);
+ strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
+ }
+
+ strSQL = PrepareSQL(strSQL, !filter.fields.empty() && filter.fields.compare("*") != 0
+ ? filter.fields.c_str()
+ : "songview.*") +
+ strSQLExtra;
+
+ CLog::Log(LOGDEBUG, "{} query = {}", __FUNCTION__, strSQL);
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // store the total value of items as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+ if (!SortUtils::SortFromDataset(sorting, MediaTypeSong, m_pDS, results))
+ return false;
+
+ // get data from returned rows
+ items.Reserve(results.size());
+ const dbiplus::query_data& data = m_pDS->get_result_set().records;
+ int count = 0;
+ for (const auto& i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ try
+ {
+ CFileItemPtr item(new CFileItem);
+ GetFileItemFromDataset(record, item.get(), musicUrl);
+ // HACK for sorting by database returned order
+ item->m_iprogramCount = ++count;
+ items.Add(item);
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{}: out of memory loading query: {}", __FUNCTION__, filter.where);
+ return (items.Size() > 0);
+ }
+ }
+
+ // cleanup
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ // cleanup
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSongsByYear(const std::string& baseDir, CFileItemList& items, int year)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(baseDir))
+ return false;
+
+ musicUrl.AddOption("year", year);
+
+ Filter filter;
+ return GetSongsFullByWhere(baseDir, filter, items, SortDescription(), true);
+}
+
+bool CMusicDatabase::GetSongsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idGenre,
+ int idArtist,
+ int idAlbum,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ if (idAlbum > 0)
+ musicUrl.AddOption("albumid", idAlbum);
+
+ if (idGenre > 0)
+ musicUrl.AddOption("genreid", idGenre);
+
+ if (idArtist > 0)
+ musicUrl.AddOption("artistid", idArtist);
+
+ Filter filter;
+ return GetSongsFullByWhere(musicUrl.ToString(), filter, items, sortDescription, true);
+}
+
+// clang-format off
+typedef struct
+{
+ std::string fieldJSON; // Field name in JSON schema
+ std::string formatJSON; // Format in JSON schema
+ bool bSimple; // Fetch field directly to JSON output
+ std::string fieldDB; // Name of field in db query
+ std::string SQL; // SQL for scalar subqueries or field alias
+} translateJSONField;
+
+static const translateJSONField JSONtoDBArtist[] = {
+ // Table and single value join fields
+ { "artist", "string", true, "strArtist", "" }, // Label field at top
+ { "sortname", "string", true, "strSortname", "" },
+ { "instrument", "array", true, "strInstruments", "" },
+ { "description", "string", true, "strBiography", "" },
+ { "genre", "array", true, "strGenres", "" },
+ { "mood", "array", true, "strMoods", "" },
+ { "style", "array", true, "strStyles", "" },
+ { "yearsactive", "array", true, "strYearsActive", "" },
+ { "born", "string", true, "strBorn", "" },
+ { "formed", "string", true, "strFormed", "" },
+ { "died", "string", true, "strDied", "" },
+ { "disbanded", "string", true, "strDisbanded", "" },
+ { "type", "string", true, "strType", "" },
+ { "gender", "string", true, "strGender", "" },
+ { "disambiguation", "string", true, "strDisambiguation", "" },
+ { "musicbrainzartistid", "array", true, "strMusicBrainzArtistId", "" }, // Array in schema, but only ever one element
+ { "dateadded", "string", true, "dateAdded", "" },
+ { "datenew", "string", true, "dateNew", "" },
+ { "datemodified", "string", true, "dateModified", "" },
+
+ // JOIN fields (multivalue), same order as _JoinToArtistFields
+ { "", "", false, "isSong", "" },
+ { "sourceid", "string", false, "idSourceAlbum", "album_source.idSource AS idSourceAlbum" },
+ { "", "string", false, "idSourceSong", "album_source.idSource AS idSourceSong" },
+ { "songgenres", "array", false, "idSongGenreAlbum", "song_genre.idGenre AS idSongGenreAlbum" },
+ { "", "array", false, "idSongGenreSong", "song_genre.idGenre AS idSongGenreSong" },
+ { "", "", false, "strSongGenreAlbum", "genre.strGenre AS strSongGenreAlbum" },
+ { "", "", false, "strSongGenreSong", "genre.strGenre AS strSongGenreSong" },
+ { "art", "", false, "idArt", "art.art_id AS idArt" },
+ { "", "", false, "artType", "art.type AS artType" },
+ { "", "", false, "artURL", "art.url AS artURL" },
+ { "", "", false, "idRole", "song_artist.idRole" },
+ { "roles", "", false, "strRole", "role.strRole" },
+ { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
+ // Derived from joined tables
+ { "isalbumartist", "bool", false, "", "" },
+ { "thumbnail", "string", false, "", "" },
+ { "fanart", "string", false, "", "" }
+ /*
+ Sources and genre are related via album, and so the dataset only contains source and genre
+ pairs that exist, rather than all the genres being repeated for every source. We can not only
+ look at genres for the first source, and genre can be out of order.
+ */
+};
+// clang-format on
+
+static const size_t NUM_ARTIST_FIELDS = sizeof(JSONtoDBArtist) / sizeof(translateJSONField);
+
+bool CMusicDatabase::GetArtistsByWhereJSON(
+ const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ total = -1;
+
+ size_t resultcount = 0;
+ Filter extFilter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting = sortDescription;
+ //! @todo: replace GetFilter to avoid exists as well as JOIn to albm_artist and song_artist tables
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // Replace view names in filter with table names
+ StringUtils::Replace(extFilter.where, "artistview", "artist");
+ StringUtils::Replace(extFilter.where, "albumview", "album");
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count number of artists that satisfy selection criteria
+ //(includes xsp limits from filter, but not sort limits)
+ total = GetSingleValueInt("SELECT COUNT(1) FROM artist " + strSQLExtra, m_pDS);
+ resultcount = static_cast<size_t>(total);
+
+ // Process albumartistsonly option
+ const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
+ bool albumArtistsOnly(false);
+ auto option = options.find("albumartistsonly");
+ if (option != options.end())
+ albumArtistsOnly = option->second.asBoolean();
+ // Process role options
+ int roleidfilter = 1; // Default restrict song_artist to "artists" only, no other roles.
+ option = options.find("roleid");
+ if (option != options.end())
+ roleidfilter = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("role");
+ if (option != options.end())
+ {
+ if (option->second.asString() == "all" || option->second.asString() == "%")
+ roleidfilter = -1000; //All roles
+ else
+ roleidfilter = GetRoleByName(option->second.asString());
+ }
+ }
+
+ // Get order by (and any scalar query artist fields)
+ int iAddedFields = GetOrderFilter(MediaTypeArtist, sortDescription, extFilter);
+ // Replace artistview field names in order by artist table field names
+ StringUtils::Replace(extFilter.order, "artistview", "artist");
+ StringUtils::Replace(extFilter.fields, "artistview", "artist");
+
+ // Grab and adjust artist sort field that may have been added to filter
+ // These need to be added to the end of the artist table field list
+ std::string artistsortSQL = extFilter.fields;
+ extFilter.fields.clear();
+
+ std::string strSQL;
+
+ // Setup fields to query, and album field number mapping
+ // Find first join field (isSong) in JSONtoDBArtist for offset
+ int index_firstjoin = -1;
+ for (unsigned int i = 0; i < NUM_ARTIST_FIELDS; i++)
+ {
+ if (JSONtoDBArtist[i].fieldDB == "isSong")
+ {
+ index_firstjoin = i;
+ break;
+ }
+ }
+ Filter joinFilter;
+ Filter albumArtistFilter;
+ Filter songArtistFilter;
+ DatasetLayout joinLayout(static_cast<size_t>(joinToArtist_enumCount));
+ extFilter.AppendField("artist.idArtist"); // ID "artistid" in JSON
+ std::vector<int> dbfieldindex;
+ // JSON "label" field is strArtist which is also output as "artist", query field once output twice
+ extFilter.AppendField(JSONtoDBArtist[0].fieldDB);
+ dbfieldindex.emplace_back(0); // Output "artist"
+
+ // Check each optional artist db field that could be retrieved (not "artist")
+ for (unsigned int i = 1; i < NUM_ARTIST_FIELDS; i++)
+ {
+ bool foundJSON = fields.find(JSONtoDBArtist[i].fieldJSON) != fields.end();
+ if (JSONtoDBArtist[i].bSimple)
+ {
+ // Check for non-join fields in order too.
+ // Query these in inline view (but not output) so can ref in outer order
+ bool foundOrderby(false);
+ if (!foundJSON)
+ foundOrderby = extFilter.order.find(JSONtoDBArtist[i].fieldDB) != std::string::npos;
+ if (foundOrderby || foundJSON)
+ {
+ // Store indexes of requested artist table and scalar subquery fields
+ // to be output, and -1 when not output to JSON
+ if (!foundJSON)
+ dbfieldindex.emplace_back(-1);
+ else
+ dbfieldindex.emplace_back(i);
+ // Field from scaler subquery
+ if (!JSONtoDBArtist[i].SQL.empty())
+ extFilter.AppendField(PrepareSQL(JSONtoDBArtist[i].SQL));
+ else
+ // Field from artist table
+ extFilter.AppendField(JSONtoDBArtist[i].fieldDB);
+ }
+ }
+ else if (foundJSON)
+ // Field from join or derived from joined fields
+ joinLayout.SetField(i - index_firstjoin, JSONtoDBArtist[i].fieldDB, true);
+ }
+
+ // Append calculated artistsort field that may have been added to filter
+ // Field used only for ORDER BY, not output to JSON
+ extFilter.AppendField(artistsortSQL);
+ for (int i = 0; i < iAddedFields; i++)
+ dbfieldindex.emplace_back(-2); // columns in dataset
+
+ // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
+ strSQLExtra = "";
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Add any LIMIT clause to strSQLExtra
+ if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
+ {
+ strSQLExtra +=
+ DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
+ resultcount = std::min(
+ DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
+ resultcount);
+ }
+
+ // Setup multivalue JOINs, GROUP BY and ORDER BY
+ bool bJoinAlbumArtist(false);
+ bool bJoinSongArtist(false);
+ if (sortDescription.sortBy != SortByRandom)
+ {
+ // Repeat inline view order (that always includes idArtist) on join query
+ std::string order = extFilter.order;
+ StringUtils::Replace(order, "artist.", "a1.");
+ joinFilter.AppendOrder(order);
+ }
+ else
+ joinFilter.AppendOrder("a1.idArtist");
+ joinFilter.AppendGroup("a1.idArtist");
+ // Album artists and song artists
+ if ((joinLayout.GetFetch(joinToArtist_isalbumartist) && !albumArtistsOnly) ||
+ joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
+ joinLayout.GetFetch(joinToArtist_idSongGenreAlbum) ||
+ joinLayout.GetFetch(joinToArtist_strRole))
+ {
+ bJoinAlbumArtist = true;
+ albumArtistFilter.AppendField("album_artist.idArtist AS id");
+ if (!albumArtistsOnly || joinLayout.GetFetch(joinToArtist_strRole))
+ {
+ bJoinSongArtist = true;
+ songArtistFilter.AppendField("song_artist.idArtist AS id");
+ songArtistFilter.AppendField("1 AS isSong");
+ albumArtistFilter.AppendField("0 AS isSong");
+ joinLayout.SetField(joinToArtist_isSong,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
+ joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
+ joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_isSong].fieldDB);
+ }
+ }
+ else if (joinLayout.GetFetch(joinToArtist_isalbumartist))
+ {
+ // Filtering album artists only and isalbumartist requested but not source, songgenres or roles,
+ // so no need for join to album_artist table. Set fetching fetch false so that
+ // joinLayout.HasFilterFields() is false
+ joinLayout.SetFetch(joinToArtist_isalbumartist, false);
+ }
+
+ // Sources
+ if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
+ { // Left join as source may have been removed but leaving lib entries
+ albumArtistFilter.AppendJoin(
+ "LEFT JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
+ albumArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL);
+ joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
+ joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
+ if (bJoinSongArtist)
+ {
+ songArtistFilter.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
+ songArtistFilter.AppendJoin(
+ "LEFT JOIN album_source ON album_source.idAlbum = song.idAlbum");
+ songArtistFilter.AppendField(
+ "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].fieldDB);
+ songArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].SQL);
+ albumArtistFilter.AppendField(
+ "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
+ joinLayout.SetField(joinToArtist_idSourceSong,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
+ joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
+ joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceSong].fieldDB);
+ }
+ else
+ {
+ joinLayout.SetField(joinToArtist_idSourceAlbum,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSourceAlbum].SQL, true);
+ }
+ }
+
+ // Songgenres - id and genres always both
+ if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
+ { // All albums have songs, but left join genre as songs may not have genre
+ albumArtistFilter.AppendJoin("JOIN song ON song.idAlbum = album_artist.idAlbum");
+ albumArtistFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = song.idSong");
+ albumArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
+ albumArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL);
+ albumArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
+ joinLayout.SetField(joinToArtist_strSongGenreAlbum,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
+ joinFilter.AppendGroup(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
+ joinFilter.AppendOrder(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
+ if (bJoinSongArtist)
+ { // Left join genre as songs may not have genre
+ songArtistFilter.AppendJoin(
+ "LEFT JOIN song_genre ON song_genre.idSong = song_artist.idSong");
+ songArtistFilter.AppendJoin("LEFT JOIN genre ON genre.idGenre = song_genre.idGenre");
+ songArtistFilter.AppendField(
+ "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].fieldDB);
+ songArtistFilter.AppendField(
+ "'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].fieldDB);
+ songArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].SQL);
+ songArtistFilter.AppendField(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].SQL);
+ albumArtistFilter.AppendField(
+ "-1 AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
+ albumArtistFilter.AppendField(
+ "'' AS " + JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
+ joinLayout.SetField(joinToArtist_idSongGenreSong,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
+ joinLayout.SetField(
+ joinToArtist_strSongGenreSong,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreSong].fieldDB);
+ joinFilter.AppendGroup(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
+ joinFilter.AppendOrder(
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreSong].fieldDB);
+ }
+ else
+ { // Define field alias names in join layout
+ joinLayout.SetField(joinToArtist_idSongGenreAlbum,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idSongGenreAlbum].SQL,
+ true);
+ joinLayout.SetField(joinToArtist_strSongGenreAlbum,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strSongGenreAlbum].SQL);
+ }
+ }
+
+ // Roles
+ if (roleidfilter == 1 && !joinLayout.GetFetch(joinToArtist_strRole))
+ // Only looking at album and song artists not other roles (default),
+ // so filter dataset rows likewise.
+ songArtistFilter.AppendWhere("song_artist.idRole = 1");
+ else if (joinLayout.GetFetch(joinToArtist_strRole) || // "roles" field
+ (bJoinSongArtist && (joinLayout.GetFetch(joinToArtist_idSourceAlbum) ||
+ joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))))
+ { // Rows from many roles so fetch roleid for "roles", source and genre processing
+ songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].SQL);
+ // Add fake column to album_artist query
+ albumArtistFilter.AppendField("-1 AS " +
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
+ joinLayout.SetField(joinToArtist_idRole,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
+ joinFilter.AppendGroup(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
+ joinFilter.AppendOrder(JSONtoDBArtist[index_firstjoin + joinToArtist_idRole].fieldDB);
+ }
+ if (joinLayout.GetFetch(joinToArtist_strRole))
+ { // Fetch role desc
+ songArtistFilter.AppendJoin("JOIN role ON role.idRole = song_artist.idRole");
+ songArtistFilter.AppendField(JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].SQL);
+ // Add fake column to album_artist query
+ albumArtistFilter.AppendField("'albumartist' AS " +
+ JSONtoDBArtist[index_firstjoin + joinToArtist_strRole].fieldDB);
+ }
+
+ // Build source, genre and roles part of query
+ if (bJoinAlbumArtist)
+ {
+ if (bJoinSongArtist)
+ {
+ // Combine song and album artist filter as UNION and add to join filter as an inline view
+ std::string strAlbumSQL;
+ if (!BuildSQL(strAlbumSQL, albumArtistFilter, strAlbumSQL))
+ return false;
+ strAlbumSQL = "SELECT " + albumArtistFilter.fields + " FROM album_artist " + strAlbumSQL;
+ std::string strSongSQL;
+ if (!BuildSQL(strSongSQL, songArtistFilter, strSongSQL))
+ return false;
+ strSongSQL = "SELECT " + songArtistFilter.fields + " FROM song_artist " + strSongSQL;
+
+ joinFilter.AppendJoin("JOIN (" + strAlbumSQL + " UNION " + strSongSQL +
+ ") AS albumSong ON id = a1.idArtist");
+ }
+ else
+ { //Only join album_artist, so move filter elements to join filter
+ joinFilter.AppendJoin("JOIN album_artist ON album_artist.idArtist = a1.idArtist");
+ joinFilter.AppendJoin(albumArtistFilter.join);
+ }
+ }
+
+ //Art
+ bool bJoinArt(false);
+ bJoinArt = joinLayout.GetOutput(joinToArtist_idArt) ||
+ joinLayout.GetOutput(joinToArtist_thumbnail) ||
+ joinLayout.GetOutput(joinToArtist_fanart);
+ if (bJoinArt)
+ { // Left join as artist may not have any art
+ joinFilter.AppendJoin(
+ "LEFT JOIN art ON art.media_id = a1.idArtist AND art.media_type = 'artist'");
+ joinLayout.SetField(joinToArtist_idArt,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_idArt].SQL,
+ joinLayout.GetOutput(joinToArtist_idArt));
+ joinLayout.SetField(joinToArtist_artType,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_artType].SQL);
+ joinLayout.SetField(joinToArtist_artURL,
+ JSONtoDBArtist[index_firstjoin + joinToArtist_artURL].SQL);
+ joinFilter.AppendGroup("art.art_id");
+ joinFilter.AppendOrder("arttype");
+ if (!joinLayout.GetOutput(joinToArtist_idArt))
+ {
+ if (!joinLayout.GetOutput(joinToArtist_thumbnail))
+ // Fanart only
+ joinFilter.AppendJoin("AND art.type = 'fanart'");
+ else if (!joinLayout.GetOutput(joinToArtist_fanart))
+ // Thumb only
+ joinFilter.AppendJoin("AND art.type = 'thumb'");
+ }
+ }
+ else if (bJoinSongArtist)
+ joinFilter.group.clear(); // UNION only so no GROUP BY needed
+
+ // Build JOIN part of query (if we have one)
+ std::string strSQLJoin;
+ if (joinLayout.HasFilterFields())
+ if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
+ return false;
+
+ // Adjust where in the results record the join fields are allowing for the
+ // inline view fields (Quicker than finding field by name every time)
+ // idArtist + other artist fields
+ joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
+
+ // Build full query
+ // When have multiple value joins e.g. song genres, use inline view
+ // SELECT a1.*, <join fields> FROM
+ // (SELECT <artist fields> FROM artist <where> + <order by> + <limits> ) AS a1
+ // <joins> <group by> <order by> + <joins order by>
+ // Don't use prepareSQL - confuses arttype = 'thumb' filter
+
+ strSQL = "SELECT " + extFilter.fields + " FROM artist " + strSQLExtra;
+ if (joinLayout.HasFilterFields())
+ {
+ strSQL = "(" + strSQL + ") AS a1 ";
+ strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
+ }
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ // run query
+ auto start = std::chrono::steady_clock::now();
+
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound <= 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Get artists from returned rows. Joins means there can be many rows per artist
+ int artistId = -1;
+ int sourceId = -1;
+ int genreId = -1;
+ int roleId = -1;
+ int artId = -1;
+ std::vector<int> genreidlist;
+ std::vector<int> sourceidlist;
+ std::vector<int> roleidlist;
+ bool bArtDone(false);
+ bool bHaveArtist(false);
+ bool bIsAlbumArtist(true);
+ bool bGenreFoundViaAlbum(false);
+ CVariant artistObj;
+ result["artists"].reserve(resultcount);
+ while (!m_pDS->eof() || bHaveArtist)
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (m_pDS->eof() || artistId != record->at(0).get_asInt())
+ {
+ // Store previous or last artist
+ if (bHaveArtist)
+ {
+ // Convert any empty MBid array into an array with one empty element [""]
+ // to match the number of artist ID (way other mbid arrays handled)
+ if (artistObj.isMember("musicbrainzartistid") && artistObj["musicbrainzartistid"].empty())
+ artistObj["musicbrainzartistid"].append("");
+
+ result["artists"].append(artistObj);
+ bHaveArtist = false;
+ artistObj.clear();
+ }
+ if (artistObj.empty())
+ {
+ // Initialise fields, ensure those with possible null values are set to correct empty variant type
+ if (joinLayout.GetOutput(joinToArtist_idSourceAlbum))
+ artistObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
+ if (joinLayout.GetOutput(joinToArtist_idSongGenreAlbum))
+ artistObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
+ if (joinLayout.GetOutput(joinToArtist_idArt))
+ artistObj["art"] = CVariant(CVariant::VariantTypeObject);
+ if (joinLayout.GetOutput(joinToArtist_thumbnail))
+ artistObj["thumbnail"] = "";
+ if (joinLayout.GetOutput(joinToArtist_fanart))
+ artistObj["fanart"] = "";
+
+ sourceId = -1;
+ roleId = -1;
+ genreId = -1;
+ artId = -1;
+ genreidlist.clear();
+ bGenreFoundViaAlbum = false;
+ sourceidlist.clear();
+ roleidlist.clear();
+ bArtDone = false;
+ }
+ if (m_pDS->eof())
+ continue; // Having saved the last artist stop
+
+ // New artist
+ artistId = record->at(0).get_asInt();
+ bHaveArtist = true;
+ artistObj["artistid"] = artistId;
+ artistObj["label"] = record->at(1).get_asString();
+ artistObj["artist"] = record->at(1).get_asString(); // Always have "artist"
+ bIsAlbumArtist = true; //Album artist by default
+ if (joinLayout.GetOutput(joinToArtist_isalbumartist))
+ {
+ // Not album artist when fetching song artists too and first row for artist isSong=true
+ if (bJoinSongArtist)
+ bIsAlbumArtist = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
+ artistObj["isalbumartist"] = bIsAlbumArtist;
+ }
+ for (size_t i = 0; i < dbfieldindex.size(); i++)
+ if (dbfieldindex[i] > -1)
+ {
+ if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "integer")
+ artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
+ else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "float")
+ artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] =
+ record->at(1 + i).get_asFloat();
+ else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "array")
+ artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
+ record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_musicItemSeparator);
+ else if (JSONtoDBArtist[dbfieldindex[i]].formatJSON == "boolean")
+ artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
+ else
+ artistObj[JSONtoDBArtist[dbfieldindex[i]].fieldJSON] =
+ record->at(1 + i).get_asString();
+ }
+ }
+ if (bJoinAlbumArtist)
+ {
+ bool bAlbumArtistRow(true);
+ int idRoleRow = -1;
+ if (bJoinSongArtist)
+ {
+ bAlbumArtistRow = !record->at(joinLayout.GetRecNo(joinToArtist_isSong)).get_asBool();
+ if (joinLayout.GetRecNo(joinToArtist_idRole) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_isNull())
+ {
+ idRoleRow = record->at(joinLayout.GetRecNo(joinToArtist_idRole)).get_asInt();
+ }
+ }
+
+ // Sources - gathered via both album_artist and song_artist (with role = 1)
+ if (joinLayout.GetFetch(joinToArtist_idSourceAlbum))
+ {
+ if ((bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceAlbum) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_isNull() &&
+ sourceId !=
+ record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt()) ||
+ (!bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSourceSong) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_isNull() &&
+ sourceId != record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt()))
+ {
+ bArtDone = bArtDone || (sourceId > 0); // Not first source, skip art repeats
+ bool found(false);
+ sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceAlbum)).get_asInt();
+ if (!bAlbumArtistRow)
+ {
+ // Skip other roles (when fetching them)
+ if (idRoleRow > 1)
+ {
+ found = true;
+ }
+ else
+ {
+ sourceId = record->at(joinLayout.GetRecNo(joinToArtist_idSourceSong)).get_asInt();
+ // Song artist row may repeat sources found via album artist
+ // Already have that source?
+ for (const auto& i : sourceidlist)
+ if (i == sourceId)
+ {
+ found = true;
+ break;
+ }
+ }
+ }
+ if (!found)
+ {
+ sourceidlist.emplace_back(sourceId);
+ artistObj["sourceid"].append(sourceId);
+ }
+ }
+ }
+ // Songgenres - via album artist takes precedence
+ /*
+ Sources and genre are related via album, and so the dataset only contains source
+ and genre pairs that exist, rather than all the genres being repeated for every
+ source. We can not only look at genres for the first source, and genre can be
+ found out of order.
+ Also song artist row may repeat genres found via album artist
+ */
+ if (joinLayout.GetFetch(joinToArtist_idSongGenreAlbum))
+ {
+ std::string strGenre;
+ bool newgenre(false);
+ if (bAlbumArtistRow && joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_isNull() &&
+ genreId != record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt())
+ {
+ bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats
+ newgenre = true;
+ genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreAlbum)).get_asInt();
+ strGenre =
+ record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreAlbum)).get_asString();
+ }
+ else if (!bAlbumArtistRow && !bGenreFoundViaAlbum &&
+ joinLayout.GetRecNo(joinToArtist_idSongGenreSong) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_isNull() &&
+ genreId !=
+ record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt())
+ {
+ bArtDone = bArtDone || (genreId > 0); // Not first genre, skip art repeats
+ newgenre = idRoleRow <= 1; // Skip other roles (when fetching them)
+ genreId = record->at(joinLayout.GetRecNo(joinToArtist_idSongGenreSong)).get_asInt();
+ strGenre =
+ record->at(joinLayout.GetRecNo(joinToArtist_strSongGenreSong)).get_asString();
+ }
+ if (newgenre)
+ {
+ // Already have that genre?
+ bool found(false);
+ for (const auto& i : genreidlist)
+ if (i == genreId)
+ {
+ found = true;
+ break;
+ }
+ if (!found)
+ {
+ bGenreFoundViaAlbum = bGenreFoundViaAlbum || bAlbumArtistRow;
+ genreidlist.emplace_back(genreId);
+ CVariant genreObj;
+ genreObj["genreid"] = genreId;
+ genreObj["title"] = strGenre;
+ artistObj["songgenres"].append(genreObj);
+ }
+ }
+ }
+ // Roles - gathered via song_artist roleid rows
+ if (joinLayout.GetFetch(joinToArtist_idRole))
+ {
+ if (!bAlbumArtistRow && roleId != idRoleRow)
+ {
+ bArtDone = bArtDone || (roleId > 0); // Not first role, skip art repeats
+ roleId = idRoleRow;
+ if (joinLayout.GetOutput(joinToArtist_strRole))
+ {
+ // Already have that role?
+ bool found(false);
+ for (const auto& i : roleidlist)
+ if (i == roleId)
+ {
+ found = true;
+ break;
+ }
+ if (!found)
+ {
+ roleidlist.emplace_back(roleId);
+ CVariant roleObj;
+ roleObj["roleid"] = roleId;
+ roleObj["role"] =
+ record->at(joinLayout.GetRecNo(joinToArtist_strRole)).get_asString();
+ artistObj["roles"].append(roleObj);
+ }
+ }
+ }
+ }
+ }
+ // Art
+ if (bJoinArt && !bArtDone &&
+ !record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_isNull() &&
+ record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt() > 0 &&
+ artId != record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt())
+ {
+ artId = record->at(joinLayout.GetRecNo(joinToArtist_idArt)).get_asInt();
+ if (joinLayout.GetOutput(joinToArtist_idArt))
+ {
+ artistObj["art"][record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString()] =
+ CTextureUtils::GetWrappedImageURL(
+ record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
+ }
+ if (joinLayout.GetOutput(joinToArtist_thumbnail) &&
+ record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "thumb")
+ {
+ artistObj["thumbnail"] = CTextureUtils::GetWrappedImageURL(
+ record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
+ }
+ if (joinLayout.GetOutput(joinToArtist_fanart) &&
+ record->at(joinLayout.GetRecNo(joinToArtist_artType)).get_asString() == "fanart")
+ {
+ artistObj["fanart"] = CTextureUtils::GetWrappedImageURL(
+ record->at(joinLayout.GetRecNo(joinToArtist_artURL)).get_asString());
+ }
+ }
+
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ // Ensure random order of output when results set is sorted to process multi-value joins
+ if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
+ KODI::UTILS::RandomShuffle(result["artists"].begin_array(), result["artists"].end_array());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+// clang-format off
+static const translateJSONField JSONtoDBAlbum[] = {
+ // albumview (inc scalar subquery fields use in filter rules)
+ { "title", "string", true, "strAlbum", "" }, // Label field at top
+ { "description", "string", true, "strReview", "" },
+ { "genre", "array", true, "strGenres", "" },
+ { "theme", "array", true, "strThemes", "" },
+ { "mood", "array", true, "strMoods", "" },
+ { "style", "array", true, "strStyles", "" },
+ { "type", "string", true, "strType", "" },
+ { "albumlabel", "string", true, "strLabel", "" },
+ { "rating", "float", true, "fRating", "" },
+ { "votes", "integer", true, "iVotes", "" },
+ { "userrating", "unsigned", true, "iUserrating", "" },
+ { "isboxset", "boolean", true, "bBoxedSet", "" },
+ { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "" },
+ { "displayartist", "string", true, "strArtists", "" }, //strArtistDisp in album table
+ { "compilation", "boolean", true, "bCompilation", "" },
+ { "releasetype", "string", true, "strReleaseType", "" },
+ { "totaldiscs", "integer", true, "iDiscTotal", "" },
+ { "sortartist", "string", true, "strArtistSort", "" },
+ { "musicbrainzreleasegroupid", "string", true, "strReleaseGroupMBID", "" },
+ { "playcount", "integer", true, "iTimesPlayed", "" }, // Scalar subquery in view
+ { "dateadded", "string", true, "dateAdded", "" },
+ { "datenew", "string", true, "dateNew", "" },
+ { "datemodified", "string", true, "dateModified", "" },
+ { "lastplayed", "string", true, "lastPlayed", "" }, // Scalar subquery in view
+ { "originaldate", "string", true, "strOrigReleaseDate", "" },
+ { "releasedate", "string", true, "strReleaseDate", "" },
+ { "albumstatus", "string", true, "strReleaseStatus", "" },
+ { "albumduration", "integer", true, "iAlbumDuration", "" },
+ // Scalar subquery fields
+ { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
+ { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = albumview.idAlbum) AS sources" },
+ { "songgenres", "array", true, "songgenres", "(SELECT GROUP_CONCAT(DISTINCT CONCAT(genre.idGenre, ',', REPLACE(genre.strGenre, ',', '-'))) FROM song "
+ "JOIN song_genre ON song.idSong = song_genre.idSong JOIN genre ON song_genre.idGenre = genre.idGenre WHERE song.idAlbum = albumview.idAlbum) AS songgenres" } ,
+ // Single value JOIN fields
+ { "thumbnail", "image", true, "thumbnail", "art.url AS thumbnail" }, // or (SELECT art.url FROM art WHERE art.media_id = album.idAlbum AND art.media_type = "album" AND art.type = "thumb") as url
+ // JOIN fields (multivalue), same order as _JoinToAlbumFields
+ { "artistid", "array", false, "idArtist", "album_artist.idArtist AS idArtist" },
+ { "artist", "array", false, "strArtist", "artist.strArtist AS strArtist" },
+ { "musicbrainzalbumartistid", "array", false, "strArtistMBID", "artist.strMusicBrainzArtistID AS strArtistMBID" },
+ /*
+ Album "fanart" and "art" fields of JSON schema are fetched using thumbloader
+ and separate queries to allow for fallback strategy.
+
+ Using albmview, rather than album table, as view has scalar subqueries for
+ playcount and lastplayed already defined. Needed as MySQL does
+ not support use of scalar subquery field alias names in where clauses (they
+ have to be repeated) and these fields can be used by filter rules.
+ Using this view is no slower than the album table as these scalar fields are
+ only calculated (slowing query) when field is in field list.
+ */
+};
+// clang-format on
+
+static const size_t NUM_ALBUM_FIELDS = sizeof(JSONtoDBAlbum) / sizeof(translateJSONField);
+
+bool CMusicDatabase::GetAlbumsByWhereJSON(
+ const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ total = -1;
+
+ size_t resultcount = 0;
+ Filter extFilter;
+ CMusicDbUrl musicUrl;
+ // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
+ // passed in at the start of the function
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // Replace view names in filter with table names
+ StringUtils::Replace(extFilter.where, "artistview", "artist");
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count number of albums that satisfy selection criteria
+ // (includes xsp limits from filter, but not sort limits)
+ // Use albumview as filter rules in where clause may use scalar query fields
+ total = GetSingleValueInt("SELECT COUNT(1) FROM albumview " + strSQLExtra, m_pDS);
+ resultcount = static_cast<size_t>(total);
+
+ // Get order by (and any scalar query artist fields
+ int iAddedFields = GetOrderFilter(MediaTypeAlbum, sortDescription, extFilter);
+
+ // Grab calculated artist/title sort fields that may have been added to filter
+ // These need to be added to the end of the album table field list
+ std::string calcsortfieldsSQL = extFilter.fields;
+ extFilter.fields.clear();
+
+ std::string strSQL;
+
+ // Setup fields to query, and album field number mapping
+ // Find idArtist in JSONtoDBAlbum, offset of first join field
+ int index_idArtist = -1;
+ for (unsigned int i = 0; i < NUM_ALBUM_FIELDS; i++)
+ {
+ if (JSONtoDBAlbum[i].fieldDB == "idArtist")
+ {
+ index_idArtist = i;
+ break;
+ }
+ }
+ Filter joinFilter;
+ DatasetLayout joinLayout(static_cast<size_t>(joinToAlbum_enumCount));
+ extFilter.AppendField("albumview.idAlbum"); // ID "albumid" in JSON
+ std::vector<int> dbfieldindex;
+ // JSON "label" field is strAlbum which may also be requested as "title", query field once output twice
+ extFilter.AppendField(JSONtoDBAlbum[0].fieldDB);
+ if (fields.find(JSONtoDBAlbum[0].fieldJSON) != fields.end())
+ dbfieldindex.emplace_back(0); // Output "title"
+ else
+ dbfieldindex.emplace_back(-1); // fetch but not output
+
+ // Check each optional album db field that could be retrieved (not label)
+ for (unsigned int i = 1; i < NUM_ALBUM_FIELDS; i++)
+ {
+ bool foundJSON = fields.find(JSONtoDBAlbum[i].fieldJSON) != fields.end();
+ if (JSONtoDBAlbum[i].bSimple)
+ {
+ // Check for non-join fields in order too.
+ // Query these in inline view (but not output) so can ref in outer order
+ bool foundOrderby(false);
+ if (!foundJSON)
+ foundOrderby = extFilter.order.find(JSONtoDBAlbum[i].fieldDB) != std::string::npos;
+ if (foundOrderby || foundJSON)
+ {
+ // Store indexes of requested album table and scalar subquery fields
+ // to be output, and -1 when not output to JSON
+ if (!foundJSON)
+ dbfieldindex.emplace_back(-1);
+ else
+ dbfieldindex.emplace_back(i);
+ if (!JSONtoDBAlbum[i].SQL.empty())
+ // Field from scaler subquery
+ extFilter.AppendField(PrepareSQL(JSONtoDBAlbum[i].SQL));
+ else
+ // Field from album table
+ extFilter.AppendField(JSONtoDBAlbum[i].fieldDB);
+ }
+ }
+ else if (foundJSON)
+ // Field from join found in JSON request
+ joinLayout.SetField(i - index_idArtist, JSONtoDBAlbum[i].SQL, true);
+ }
+
+ // Append calculated artist/title sort fields that may have been added to filter
+ // Field used only for ORDER BY, not output to JSON
+ extFilter.AppendField(calcsortfieldsSQL);
+ for (int i = 0; i < iAddedFields; i++)
+ dbfieldindex.emplace_back(-1); // columns in dataset
+
+ // JOIN art tables if needed (fields output and/or in sort)
+ if (extFilter.fields.find("art.") != std::string::npos)
+ { // Left join as not all albums have art, but only have one thumb at most
+ extFilter.AppendJoin("LEFT JOIN art ON art.media_id = idAlbum "
+ "AND art.media_type = 'album' AND art.type = 'thumb'");
+ }
+
+ // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
+ strSQLExtra = "";
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Add any LIMIT clause to strSQLExtra
+ if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
+ {
+ strSQLExtra +=
+ DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
+ resultcount = std::min(
+ DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
+ resultcount);
+ }
+
+ // Setup multivalue JOINs, GROUP BY and ORDER BY
+ bool bJoinAlbumArtist(false);
+ if (sortDescription.sortBy != SortByRandom)
+ {
+ // Repeat inline view order (that always includes idAlbum) on join query
+ std::string order = extFilter.order;
+ StringUtils::Replace(order, "albumview.", "a1.");
+ joinFilter.AppendOrder(order);
+ }
+ else
+ joinFilter.AppendOrder("a1.idAlbum");
+ joinFilter.AppendGroup("a1.idAlbum");
+ // Album artists
+ if (joinLayout.GetFetch(joinToAlbum_idArtist) || joinLayout.GetFetch(joinToAlbum_strArtist) ||
+ joinLayout.GetFetch(joinToAlbum_strArtistMBID))
+ { // All albums have at least one artist so inner join sufficient
+ bJoinAlbumArtist = true;
+ joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = a1.idAlbum");
+ joinFilter.AppendGroup("album_artist.idArtist");
+ joinFilter.AppendOrder("album_artist.iOrder");
+ // Ensure idArtist is queried
+ if (!joinLayout.GetFetch(joinToAlbum_idArtist))
+ joinLayout.SetField(joinToAlbum_idArtist,
+ JSONtoDBAlbum[index_idArtist + joinToAlbum_idArtist].SQL);
+ }
+ // artist table needed for strArtist or MBID
+ // (album_artist.strArtist can be an alias or spelling variation)
+ if (joinLayout.GetFetch(joinToAlbum_strArtist) ||
+ joinLayout.GetFetch(joinToAlbum_strArtistMBID))
+ joinFilter.AppendJoin("JOIN artist ON artist.idArtist = album_artist.idArtist");
+
+ // Build JOIN part of query (if we have one)
+ std::string strSQLJoin;
+ if (joinLayout.HasFilterFields())
+ if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
+ return false;
+
+ // Adjust where in the results record the join fields are allowing for the
+ // inline view fields (Quicker than finding field by name every time)
+ // idAlbum + other album fields
+ joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
+
+ // Build full query
+ // When have multiple value joins (artists or song genres) use inline view
+ // SELECT a1.*, <join fields> FROM
+ // (SELECT <album fields> FROM albumview <where> + <order by> + <limits> ) AS a1
+ // <joins> <group by> <order by> <joins order by>
+ // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
+
+ strSQL = "SELECT " + extFilter.fields + " FROM albumview " + strSQLExtra;
+ if (joinLayout.HasFilterFields())
+ {
+ strSQL = "(" + strSQL + ") AS a1 ";
+ strSQL = "SELECT a1.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
+ }
+
+ // Modify query to use correct year field
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ StringUtils::Replace(strSQL, "<datefield>", "strReleaseDate");
+ else
+ StringUtils::Replace(strSQL, "<datefield>", "strOrigReleaseDate");
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ // run query
+ auto start = std::chrono::steady_clock::now();
+
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound <= 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Get albums from returned rows. Joins means there can be many rows per album
+ int albumId = -1;
+ int artistId = -1;
+ CVariant albumObj;
+ result["albums"].reserve(resultcount);
+ while (!m_pDS->eof() || !albumObj.empty())
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (m_pDS->eof() || albumId != record->at(0).get_asInt())
+ {
+ // Store previous or last album
+ if (!albumObj.empty())
+ {
+ // Split sources string into int array
+ if (albumObj.isMember("sourceid"))
+ {
+ std::vector<std::string> sources =
+ StringUtils::Split(albumObj["sourceid"].asString(), ";");
+ albumObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
+ for (size_t i = 0; i < sources.size(); i++)
+ albumObj["sourceid"].append(atoi(sources[i].c_str()));
+ }
+ result["albums"].append(albumObj);
+ albumObj.clear();
+ artistId = -1;
+ }
+ if (m_pDS->eof())
+ continue; // Having saved last album stop
+
+ // New album
+ albumId = record->at(0).get_asInt();
+ albumObj["albumid"] = albumId;
+ albumObj["label"] = record->at(1).get_asString();
+ for (size_t i = 0; i < dbfieldindex.size(); i++)
+ if (dbfieldindex[i] > -1)
+ {
+ if (JSONtoDBAlbum[dbfieldindex[i]].fieldDB == "songgenres")
+ {
+ // Convert "20,Jazz,54,New Age,65,Rock" into array of objects
+ std::vector<std::string> values =
+ StringUtils::Split(record->at(1 + i).get_asString(), ",");
+ if (values.size() % 2 == 0) // Must contain an even number of entries
+ {
+ for (size_t j = 0; j + 1 < values.size(); j += 2)
+ {
+ int idGenre = atoi(values[j].c_str());
+ if (idGenre > 0)
+ {
+ CVariant genreObj;
+ genreObj["genreid"] = idGenre;
+ genreObj["title"] = values[j + 1];
+ albumObj["songgenres"].append(genreObj);
+ }
+ }
+ }
+ // Ensure albums with null songgenres get empty array
+ if (!albumObj.isMember("songgenres"))
+ albumObj["songgenres"] = CVariant(CVariant::VariantTypeArray);
+ }
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "integer")
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "unsigned")
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] =
+ std::max(record->at(1 + i).get_asInt(), 0);
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "float")
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] =
+ std::max(record->at(1 + i).get_asFloat(), 0.f);
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "array")
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
+ record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_musicItemSeparator);
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "boolean")
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
+ else if (JSONtoDBAlbum[dbfieldindex[i]].formatJSON == "image")
+ {
+ std::string url = record->at(1 + i).get_asString();
+ if (!url.empty())
+ url = CTextureUtils::GetWrappedImageURL(url);
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = url;
+ }
+ else
+ albumObj[JSONtoDBAlbum[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
+ }
+ }
+ if (bJoinAlbumArtist && joinLayout.GetRecNo(joinToAlbum_idArtist) > -1)
+ {
+ if (artistId != record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt())
+ {
+ artistId = record->at(joinLayout.GetRecNo(joinToAlbum_idArtist)).get_asInt();
+ if (joinLayout.GetOutput(joinToAlbum_idArtist))
+ albumObj["artistid"].append(artistId);
+ if (artistId == BLANKARTIST_ID)
+ {
+ if (joinLayout.GetOutput(joinToAlbum_strArtist))
+ albumObj["artist"].append(StringUtils::Empty);
+ if (joinLayout.GetOutput(joinToAlbum_strArtistMBID))
+ albumObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
+ }
+ else
+ {
+ if (joinLayout.GetOutput(joinToAlbum_strArtist) &&
+ joinLayout.GetRecNo(joinToAlbum_strArtist) > -1)
+ albumObj["artist"].append(
+ record->at(joinLayout.GetRecNo(joinToAlbum_strArtist)).get_asString());
+ if (joinLayout.GetOutput(joinToAlbum_strArtistMBID) &&
+ joinLayout.GetRecNo(joinToAlbum_strArtistMBID) > -1)
+ albumObj["musicbrainzalbumartistid"].append(
+ record->at(joinLayout.GetRecNo(joinToAlbum_strArtistMBID)).get_asString());
+ }
+ }
+ }
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ // Ensure random order of output when results set is sorted to process multi-value joins
+ if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
+ KODI::UTILS::RandomShuffle(result["albums"].begin_array(), result["albums"].end_array());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+// clang-format off
+static const translateJSONField JSONtoDBSong[] = {
+ // table and single value join fields
+ { "title", "string", true, "strTitle", "" }, // Label field at top
+ { "albumid", "integer", true, "song.idAlbum", "" },
+ { "", "", true, "song.iTrack", "" },
+ { "displayartist", "string", true, "song.strArtistDisp", "" },
+ { "sortartist", "string", true, "song.strArtistSort", "" },
+ { "genre", "array", true, "song.strGenres", "" },
+ { "duration", "integer", true, "iDuration", "" },
+ { "comment", "string", true, "comment", "" },
+ { "", "string", true, "strFileName", "" },
+ { "musicbrainztrackid", "string", true, "strMusicBrainzTrackID", "" },
+ { "playcount", "integer", true, "iTimesPlayed", "" },
+ { "lastplayed", "string", true, "lastPlayed", "" },
+ { "rating", "float", true, "rating", "" },
+ { "votes", "integer", true, "votes", "" },
+ { "userrating", "unsigned", true, "song.userrating", "" },
+ { "mood", "array", true, "mood", "" },
+ { "dateadded", "string", true, "song.dateAdded", "" },
+ { "datenew", "string", true, "song.dateNew", "" },
+ { "datemodified", "string", true, "song.dateModified", "" },
+ { "file", "string", true, "strPathFile", "CONCAT(path.strPath, strFilename) AS strPathFile" },
+ { "", "string", true, "strPath", "path.strPath AS strPath" },
+ { "album", "string", true, "strAlbum", "album.strAlbum AS strAlbum" },
+ { "albumreleasetype", "string", true, "strAlbumReleaseType", "album.strReleaseType AS strAlbumReleaseType" },
+ { "musicbrainzalbumid", "string", true, "strMusicBrainzAlbumID", "album.strMusicBrainzAlbumID AS strMusicBrainzAlbumID" },
+ { "disctitle", "string", true, "song.strDiscSubtitle", "" },
+ { "bpm", "integer", true, "iBPM", "" },
+ { "originaldate", "string" , true, "song.strOrigReleaseDate","" },
+ { "releasedate", "string" , true, "song.strReleaseDate", "" },
+ { "bitrate", "integer", true, "iBitRate", "" },
+ { "samplerate", "integer", true, "iSampleRate", "" },
+ { "channels", "integer", true, "iChannels", "" },
+
+ // JOIN fields (multivalue), same order as _JoinToSongFields
+ { "albumartistid", "array", false, "idAlbumArtist", "album_artist.idArtist AS idAlbumArtist" },
+ { "albumartist", "array", false, "strAlbumArtist", "albumartist.strArtist AS strAlbumArtist" },
+ { "musicbrainzalbumartistid", "array", false, "strAlbumArtistMBID", "albumartist.strMusicBrainzArtistID AS strAlbumArtistMBID" },
+ { "", "", false, "iOrderAlbumArtist", "album_artist.iOrder AS iOrderAlbumArtist" },
+ { "artistid", "array", false, "idArtist", "song_artist.idArtist AS idArtist" },
+ { "artist", "array", false, "strArtist", "songartist.strArtist AS strArtist" },
+ { "musicbrainzartistid", "array", false, "strArtistMBID", "songartist.strMusicBrainzArtistID AS strArtistMBID" },
+ { "", "", false, "iOrderArtist", "song_artist.iOrder AS iOrderArtist" },
+ { "", "", false, "idRole", "song_artist.idRole" },
+ { "", "", false, "strRole", "role.strRole" },
+ { "", "", false, "iOrderRole", "song_artist.iOrder AS iOrderRole" },
+ { "genreid", "array", false, "idGenre", "song_genre.idGenre AS idGenre" }, // Not GROUP_CONCAT as can't control order
+ { "", "", false, "iOrderGenre", "song_genre.idOrder AS iOrderGenre" },
+
+ { "contributors", "array", false, "Role_All", "song_artist.idRole AS Role_All" },
+ { "displaycomposer", "string", false, "Role_Composer", "song_artist.idRole AS Role_Composer" },
+ { "displayconductor", "string", false, "Role_Conductor", "song_artist.idRole AS Role_Conductor" },
+ { "displayorchestra", "string", false, "Role_Orchestra", "song_artist.idRole AS Role_Orchestra" },
+ { "displaylyricist", "string", false, "Role_Lyricist", "song_artist.idRole AS Role_Lyricist" },
+
+ // Scalar subquery fields
+ { "year", "integer", true, "iYear", "CAST(<datefield> AS INTEGER) AS iYear" }, //From strReleaseDate or strOrigReleaseDate
+ { "track", "integer", true, "track", "(iTrack & 0xffff) AS track" },
+ { "disc", "integer", true, "disc", "(iTrack >> 16) AS disc" },
+ { "sourceid", "string", true, "sourceid", "(SELECT GROUP_CONCAT(album_source.idSource SEPARATOR '; ') FROM album_source WHERE album_source.idAlbum = song.idAlbum) AS sources" },
+ /*
+ Song "thumbnail", "fanart" and "art" fields of JSON schema are fetched using
+ thumbloader and separate queries to allow for fallback strategy
+ "lyrics"?? Can be set for an item (by addons) but not held in db so
+ AudioLibrary.GetSongs() never fills this field despite being in schema
+
+ FROM ( SELECT * FROM song
+ JOIN album ON album.idAlbum = song.idAlbum
+ JOIN path ON path.idPath = song.idPath) AS sv
+ JOIN album_artist ON album_artist.idAlbum = song.idAlbum
+ JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist
+ JOIN song_artist ON song_artist.idSong = song.idSong
+ JOIN artist AS artistsong ON artistsong.idArtist = song_artist.idArtist
+ JOIN role ON song_artist.idRole = role.idRole
+ LEFT JOIN song_genre ON song.idSong = song_genre.idSong
+
+ */
+};
+// clang-format on
+
+static const size_t NUM_SONG_FIELDS = sizeof(JSONtoDBSong) / sizeof(translateJSONField);
+
+bool CMusicDatabase::GetSongsByWhereJSON(
+ const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ total = -1;
+
+ size_t resultcount = 0;
+ Filter extFilter;
+ CMusicDbUrl musicUrl;
+ // sorting passed into GetFilter() but not used as we only want to use the Const sortDescription
+ // passed into the function
+ SortDescription sorting = sortDescription;
+ if (!musicUrl.FromString(baseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ // Replace view names in filter with table names
+ StringUtils::Replace(extFilter.where, "artistview", "artist");
+ StringUtils::Replace(extFilter.where, "albumview", "album");
+ StringUtils::Replace(extFilter.where, "songview.strPath", "strPath");
+ StringUtils::Replace(extFilter.where, "songview.strAlbum", "strAlbum");
+ StringUtils::Replace(extFilter.where, "songview", "song");
+ StringUtils::Replace(extFilter.where, "songartistview", "song_artist");
+
+ // JOIN album and path tables needed by filter rules in where clause
+ if (extFilter.where.find("album.") != std::string::npos ||
+ extFilter.where.find("strAlbum") != std::string::npos)
+ { // All songs have one album so inner join sufficient
+ extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
+ }
+ if (extFilter.where.find("strPath") != std::string::npos)
+ { // All songs have one path so inner join sufficient
+ extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
+ }
+
+ // Build JOINs and WHERE needed by filter for counting songs
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Count number of songs that satisfy selection criteria
+ // (includes xsp limits from filter, but not sort limits)
+ total = GetSingleValueInt("SELECT COUNT(1) FROM song " + strSQLExtra, m_pDS);
+ resultcount = static_cast<size_t>(total);
+
+ int iAddedFields = GetOrderFilter(MediaTypeSong, sortDescription, extFilter);
+ // Replace songview field names in order by with song, album path table field names
+ // Field names in album same as song:
+ // idAlbum, strArtistDisp, strArtistSort, strGenres, iYear, bCompilation
+ StringUtils::Replace(extFilter.order, "songview.strPath", "strPath");
+ StringUtils::Replace(extFilter.order, "songview.strAlbum", "strAlbum");
+ StringUtils::Replace(extFilter.order, "songview.bCompilation", "album.bCompilation");
+ StringUtils::Replace(extFilter.order, "songview.strArtists", "song.strArtistDisp");
+ StringUtils::Replace(extFilter.order, "songview.strAlbumArtists", "album.strArtistDisp");
+ StringUtils::Replace(extFilter.order, "songview.strAlbumArtistSort", "album.strArtistSort");
+ StringUtils::Replace(extFilter.order, "songview.strAlbumReleaseType", "strReleaseType");
+ StringUtils::Replace(extFilter.order, "songview", "song");
+ StringUtils::Replace(extFilter.fields, " strArtistSort", " song.strArtistSort");
+ StringUtils::Replace(extFilter.fields, "songview.strArtists", "song.strArtistDisp");
+ StringUtils::Replace(extFilter.fields, "songview.strAlbum", "strAlbum");
+ StringUtils::Replace(extFilter.fields, "songview.strTitle", "strTitle");
+
+ // Grab calculated artist/title sort fields that may have been added to filter
+ // These need to be added to the end of the song table field list
+ std::string calcsortfieldsSQL = extFilter.fields;
+ extFilter.fields.clear();
+
+ std::string strSQL;
+
+ // Setup fields to query, and song field number mapping
+ // Find idAlbumArtist in JSONtoDBSong, offset of first join field
+ int index_idAlbumArtist = -1;
+ for (unsigned int i = 0; i < NUM_SONG_FIELDS; i++)
+ {
+ if (JSONtoDBSong[i].fieldDB == "idAlbumArtist")
+ {
+ index_idAlbumArtist = i;
+ break;
+ }
+ }
+ Filter joinFilter;
+ DatasetLayout joinLayout(static_cast<size_t>(joinToSongs_enumCount));
+ extFilter.AppendField("song.idSong"); // ID "songid" in JSON
+ std::vector<int> dbfieldindex;
+ // JSON "label" field is strTitle which may also be requested as "title", query field once output twice
+ extFilter.AppendField(JSONtoDBSong[0].fieldDB);
+ if (fields.find(JSONtoDBSong[0].fieldJSON) != fields.end())
+ dbfieldindex.emplace_back(0); // Output "title"
+ else
+ dbfieldindex.emplace_back(-1); // Fetch but not output
+ std::vector<std::string> rolefieldlist;
+ std::vector<int> roleidlist;
+ // Check each optional db field that could be retrieved (not label)
+ for (unsigned int i = 1; i < NUM_SONG_FIELDS; i++)
+ {
+ bool foundJSON = fields.find(JSONtoDBSong[i].fieldJSON) != fields.end();
+ if (JSONtoDBSong[i].bSimple)
+ {
+ // Check for non-join fields in order too.
+ // Query these in inline view (but not output) so can ref in outer order
+ bool foundOrderby(false);
+ if (!foundJSON)
+ foundOrderby = extFilter.order.find(JSONtoDBSong[i].fieldDB) != std::string::npos;
+ if (foundOrderby || foundJSON)
+ {
+ // Store indexes of requested album table and scalar subquery fields
+ // to be output, and -1 when not output to JSON
+ if (!foundJSON)
+ dbfieldindex.emplace_back(-1);
+ else
+ dbfieldindex.emplace_back(i);
+ if (!JSONtoDBSong[i].SQL.empty())
+ // Field from scaler subquery
+ extFilter.AppendField(PrepareSQL(JSONtoDBSong[i].SQL));
+ else
+ // Field from song table
+ extFilter.AppendField(JSONtoDBSong[i].fieldDB);
+ }
+ }
+ else if (foundJSON)
+ { // Field from join found in JSON request
+ if (!StringUtils::StartsWith(JSONtoDBSong[i].fieldDB, "Role_"))
+ {
+ joinLayout.SetField(i - index_idAlbumArtist, JSONtoDBSong[i].SQL, true);
+ }
+ else
+ { // "contributors", "displaycomposer" etc.
+ rolefieldlist.emplace_back(JSONtoDBSong[i].fieldJSON);
+ }
+ }
+ }
+ // Append calculated artist/title sort fields that may have been added to filter
+ // Field used only for ORDER BY, not output to JSON
+ extFilter.AppendField(calcsortfieldsSQL);
+ for (int i = 0; i < iAddedFields; i++)
+ dbfieldindex.emplace_back(-1); // columns in dataset
+
+ // Build matching list of role id for "displaycomposer", "displayconductor",
+ // "displayorchestra", "displaylyricist"
+ if (!rolefieldlist.empty())
+ {
+ for (const auto& name : rolefieldlist)
+ {
+ int idRole = -1;
+ if (StringUtils::StartsWith(name, "display"))
+ idRole = GetRoleByName(name.substr(7));
+ roleidlist.emplace_back(idRole);
+ }
+ }
+
+ // JOIN album and path tables needed for field output and/or in sort
+ // if not already there for filter
+ if ((extFilter.fields.find("album.") != std::string::npos ||
+ extFilter.fields.find("strAlbum") != std::string::npos) &&
+ extFilter.join.find("JOIN album") == std::string::npos)
+ { // All songs have one album so inner join sufficient
+ extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
+ }
+ if (extFilter.fields.find("path.") != std::string::npos &&
+ extFilter.join.find("JOIN path") == std::string::npos)
+ { // All songs have one path so inner join sufficient
+ extFilter.AppendJoin("JOIN path ON path.idPath = song.idPath");
+ }
+
+ // Build JOIN, WHERE, ORDER BY and LIMIT for inline view
+ strSQLExtra = "";
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Add any LIMIT clause to strSQLExtra
+ if (extFilter.limit.empty() && (sortDescription.limitStart > 0 || sortDescription.limitEnd > 0))
+ {
+ strSQLExtra +=
+ DatabaseUtils::BuildLimitClause(sortDescription.limitEnd, sortDescription.limitStart);
+ resultcount = std::min(
+ DatabaseUtils::GetLimitCount(sortDescription.limitEnd, sortDescription.limitStart),
+ resultcount);
+ }
+
+ // Setup multivalue JOINs, GROUP BY and ORDER BY
+ bool bJoinSongArtist(false);
+ bool bJoinAlbumArtist(false);
+ bool bJoinRole(false);
+ if (sortDescription.sortBy != SortByRandom)
+ {
+ // Repeat inline view order (that always includes idSong) on join query
+ std::string order = extFilter.order;
+ order = extFilter.order;
+ StringUtils::Replace(order, "album.", "sv.");
+ StringUtils::Replace(order, "song.", "sv.");
+ joinFilter.AppendOrder(order);
+ }
+ else
+ joinFilter.AppendOrder("sv.idSong");
+ joinFilter.AppendGroup("sv.idSong");
+
+ // Album artists
+ if (joinLayout.GetFetch(joinToSongs_idAlbumArtist) ||
+ joinLayout.GetFetch(joinToSongs_strAlbumArtist) ||
+ joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID))
+ { // All songs have at least one album artist so inner join sufficient
+ bJoinAlbumArtist = true;
+ joinFilter.AppendJoin("JOIN album_artist ON album_artist.idAlbum = sv.idAlbum");
+ joinFilter.AppendGroup("album_artist.idArtist");
+ joinFilter.AppendOrder("album_artist.iOrder");
+ // Ensure idAlbumArtist is queried for processing repeats
+ if (!joinLayout.GetFetch(joinToSongs_idAlbumArtist))
+ {
+ joinLayout.SetField(joinToSongs_idAlbumArtist,
+ JSONtoDBSong[index_idAlbumArtist + joinToSongs_idAlbumArtist].SQL);
+ }
+ // Ensure song.IdAlbum is field of the inline view for join
+ if (fields.find("albumid") == fields.end())
+ {
+ extFilter.AppendField("song.idAlbum"); //Prefer lookup JSONtoDBSong[XXX].dbField);
+ dbfieldindex.emplace_back(-1);
+ }
+ // artist table needed for strArtist or MBID
+ // (album_artist.strArtist can be an alias or spelling variation)
+ if (joinLayout.GetFetch(joinToSongs_strAlbumArtistMBID) ||
+ joinLayout.GetFetch(joinToSongs_strAlbumArtist))
+ joinFilter.AppendJoin(
+ "JOIN artist AS albumartist ON albumartist.idArtist = album_artist.idArtist");
+ }
+
+ /*
+ Song artists
+ JSON schema "artist", "artistid", "musicbrainzartistid", "contributors",
+ "displaycomposer", "displayconductor", "displayorchestra", "displaylyricist",
+ */
+ if (joinLayout.GetFetch(joinToSongs_idArtist) || joinLayout.GetFetch(joinToSongs_strArtist) ||
+ joinLayout.GetFetch(joinToSongs_strArtistMBID) || !rolefieldlist.empty())
+ { // All songs have at least one artist (idRole = 1) so inner join sufficient
+ bJoinSongArtist = true;
+ if (rolefieldlist.empty())
+ { // song artists only, no other roles needed
+ joinFilter.AppendJoin(
+ "JOIN song_artist ON song_artist.idSong = sv.idSong AND song_artist.idRole = 1");
+ joinFilter.AppendGroup("song_artist.idArtist");
+ joinFilter.AppendOrder("song_artist.iOrder");
+ }
+ else
+ {
+ // Ensure idRole is queried
+ if (!joinLayout.GetFetch(joinToSongs_idRole))
+ {
+ joinLayout.SetField(joinToSongs_idRole,
+ JSONtoDBSong[index_idAlbumArtist + joinToSongs_idRole].SQL);
+ }
+ // Ensure strArtist is queried
+ if (!joinLayout.GetFetch(joinToSongs_strArtist))
+ {
+ joinLayout.SetField(joinToSongs_strArtist,
+ JSONtoDBSong[index_idAlbumArtist + joinToSongs_strArtist].SQL);
+ }
+ if (fields.find("contributors") != fields.end())
+ { // all roles
+ bJoinRole = true;
+ // Ensure strRole is queried from role table
+ joinLayout.SetField(joinToSongs_strRole, "role.strRole");
+ joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong");
+ joinFilter.AppendJoin("JOIN role ON song_artist.idRole = role.idRole");
+ joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
+ joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
+ }
+ else
+ { // Get just roles for "displaycomposer", "displayconductor" etc.
+ std::string where;
+ for (size_t i = 0; i < roleidlist.size(); i++)
+ {
+ int idRole = roleidlist[i];
+ if (idRole <= 1)
+ continue;
+ if (where.empty())
+ // Always get song artists too (role = 1) so can do inner join
+ where = PrepareSQL("song_artist.idRole = 1 OR song_artist.idRole = %i", idRole);
+ else
+ where += PrepareSQL(" OR song_artist.idRole = %i", idRole);
+ }
+ where = " (" + where + ")";
+ joinFilter.AppendJoin("JOIN song_artist ON song_artist.idSong = sv.idSong AND " + where);
+ joinFilter.AppendGroup("song_artist.idArtist, song_artist.idRole");
+ joinFilter.AppendOrder("song_artist.idRole, song_artist.iOrder, song_artist.idArtist");
+ }
+ }
+ // Ensure idArtist is queried for processing repeats
+ if (!joinLayout.GetFetch(joinToSongs_idArtist))
+ {
+ joinLayout.SetField(joinToSongs_idArtist,
+ JSONtoDBSong[index_idAlbumArtist + joinToSongs_idArtist].SQL);
+ }
+ // artist table needed for strArtist or MBID
+ // (song_artist.strArtist can be an alias or spelling variation)
+ if (joinLayout.GetFetch(joinToSongs_strArtistMBID) ||
+ joinLayout.GetFetch(joinToSongs_strArtist))
+ joinFilter.AppendJoin(
+ "JOIN artist AS songartist ON songartist.idArtist = song_artist.idArtist");
+ }
+
+ // Genre ids
+ if (joinLayout.GetFetch(joinToSongs_idGenre))
+ { // song genre ids (strGenre demormalised in song table)
+ // Left join as songs may not have genre
+ joinFilter.AppendJoin("LEFT JOIN song_genre ON song_genre.idSong = sv.idSong");
+ joinFilter.AppendGroup("song_genre.idGenre");
+ joinFilter.AppendOrder("song_genre.iOrder");
+ }
+
+ // Build JOIN part of query (if we have one)
+ std::string strSQLJoin;
+ if (joinLayout.HasFilterFields())
+ if (!BuildSQL(strSQLJoin, joinFilter, strSQLJoin))
+ return false;
+
+ // Adjust where in the results record the join fields are allowing for the
+ // inline view fields (Quicker than finding field by name every time)
+ // idSong + other song fields
+ joinLayout.AdjustRecordNumbers(static_cast<int>(1 + dbfieldindex.size()));
+
+ // Build full query
+ // When have multiple value joins use inline view
+ // SELECT sv.*, <join fields> FROM
+ // (SELECT <song fields> FROM song <JOIN album> <where> + <order by> + <limits> ) AS sv
+ // <joins> <group by>
+ // <order by> + <joins order by>
+ // Don't use prepareSQL - confuses releasetype = 'album' filter and group_concat separator
+ strSQL = "SELECT " + extFilter.fields + " FROM song " + strSQLExtra;
+ if (joinLayout.HasFilterFields())
+ {
+ strSQL = "(" + strSQL + ") AS sv ";
+ strSQL = "SELECT sv.*, " + joinLayout.GetFields() + " FROM " + strSQL + strSQLJoin;
+ }
+
+ // Modify query to use correct year field
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ StringUtils::Replace(strSQL, "<datefield>", "song.strReleaseDate");
+ else
+ StringUtils::Replace(strSQL, "<datefield>", "song.strOrigReleaseDate");
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ // Run query
+ auto start = std::chrono::steady_clock::now();
+
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "{} - query took {} ms", __FUNCTION__, duration.count());
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound <= 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Get song from returned rows. Joins mean there can be many rows per song
+ int songId = -1;
+ int albumartistId = -1;
+ int artistId = -1;
+ int roleId = -1;
+ bool bSongGenreDone(false);
+ bool bSongArtistDone(false);
+ bool bHaveSong(false);
+ CVariant songObj;
+ result["songs"].reserve(resultcount);
+ while (!m_pDS->eof() || bHaveSong)
+ {
+ const dbiplus::sql_record* const record = m_pDS->get_sql_record();
+
+ if (m_pDS->eof() || songId != record->at(0).get_asInt())
+ {
+ // Store previous or last song
+ if (bHaveSong)
+ {
+ // Check empty role fields get returned, and format
+ if (!rolefieldlist.empty())
+ {
+ for (const auto& displayXXX : rolefieldlist)
+ {
+ if (!StringUtils::StartsWith(displayXXX, "display"))
+ {
+ // "contributors"
+ if (!songObj.isMember(displayXXX))
+ songObj[displayXXX] = CVariant(CVariant::VariantTypeArray);
+ }
+ else if (songObj.isMember(displayXXX) && songObj[displayXXX].isArray())
+ {
+ // Convert "displaycomposer", "displayconductor", "displayorchestra",
+ // and "displaylyricist" arrays into strings
+ std::vector<std::string> names;
+ for (CVariant::const_iterator_array field = songObj[displayXXX].begin_array();
+ field != songObj[displayXXX].end_array(); ++field)
+ names.emplace_back(field->asString());
+
+ std::string role = StringUtils::Join(names, CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_musicItemSeparator);
+ songObj[displayXXX] = role;
+ }
+ else
+ songObj[displayXXX] = "";
+ }
+ }
+ result["songs"].append(songObj);
+ bHaveSong = false;
+ songObj.clear();
+ }
+ if (songObj.empty())
+ {
+ // Initialise fields, ensure those with possible null values are set to correct empty variant type
+ if (joinLayout.GetOutput(joinToSongs_idGenre))
+ songObj["genreid"] =
+ CVariant(CVariant::VariantTypeArray); //"genre" set [] by split of array
+
+ albumartistId = -1;
+ artistId = -1;
+ roleId = -1;
+ bSongGenreDone = false;
+ bSongArtistDone = false;
+ }
+ if (m_pDS->eof())
+ continue; // Having saved the last song stop
+
+ // New song
+ songId = record->at(0).get_asInt();
+ bHaveSong = true;
+ songObj["songid"] = songId;
+ songObj["label"] = record->at(1).get_asString();
+ for (size_t i = 0; i < dbfieldindex.size(); i++)
+ if (dbfieldindex[i] > -1)
+ {
+ if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "integer")
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asInt();
+ else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "unsigned")
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] =
+ std::max(record->at(1 + i).get_asInt(), 0);
+ else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "float")
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] =
+ std::max(record->at(1 + i).get_asFloat(), 0.f);
+ else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "array")
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = StringUtils::Split(
+ record->at(1 + i).get_asString(), CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_musicItemSeparator);
+ else if (JSONtoDBSong[dbfieldindex[i]].formatJSON == "boolean")
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asBool();
+ else
+ songObj[JSONtoDBSong[dbfieldindex[i]].fieldJSON] = record->at(1 + i).get_asString();
+ }
+
+ // Split sources string into int array
+ if (songObj.isMember("sourceid"))
+ {
+ std::vector<std::string> sources =
+ StringUtils::Split(songObj["sourceid"].asString(), ";");
+ songObj["sourceid"] = CVariant(CVariant::VariantTypeArray);
+ for (size_t i = 0; i < sources.size(); i++)
+ songObj["sourceid"].append(atoi(sources[i].c_str()));
+ }
+ }
+
+ if (bJoinAlbumArtist)
+ {
+ if (albumartistId != record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt())
+ {
+ bSongGenreDone =
+ bSongGenreDone || (albumartistId > 0); // Not first album artist, skip genre
+ bSongArtistDone =
+ bSongArtistDone || (albumartistId > 0); // Not first album artist, skip song artists
+ albumartistId = record->at(joinLayout.GetRecNo(joinToSongs_idAlbumArtist)).get_asInt();
+ if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
+ songObj["albumartistid"].append(albumartistId);
+ if (albumartistId == BLANKARTIST_ID)
+ {
+ if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
+ songObj["albumartist"].append(StringUtils::Empty);
+ if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
+ songObj["musicbrainzalbumartistid"].append(StringUtils::Empty);
+ }
+ else
+ {
+ if (joinLayout.GetOutput(joinToSongs_idAlbumArtist))
+ songObj["albumartistid"].append(albumartistId);
+ if (joinLayout.GetOutput(joinToSongs_strAlbumArtist))
+ songObj["albumartist"].append(
+ record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtist)).get_asString());
+ if (joinLayout.GetOutput(joinToSongs_strAlbumArtistMBID))
+ songObj["musicbrainzalbumartistid"].append(
+ record->at(joinLayout.GetRecNo(joinToSongs_strAlbumArtistMBID)).get_asString());
+ }
+ }
+ }
+ if (bJoinSongArtist && !bSongArtistDone)
+ {
+ if (artistId != record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt())
+ {
+ bSongGenreDone = bSongGenreDone || (artistId > 0); // Not first artist, skip genre
+ roleId = -1; // Allow for many artists same role
+ artistId = record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
+ if (joinLayout.GetRecNo(joinToSongs_idRole) < 0 ||
+ record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt() == 1)
+ {
+ if (joinLayout.GetOutput(joinToSongs_idArtist))
+ songObj["artistid"].append(artistId);
+ if (artistId == BLANKARTIST_ID)
+ {
+ if (joinLayout.GetOutput(joinToSongs_strArtist))
+ songObj["artist"].append(StringUtils::Empty);
+ if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
+ songObj["musicbrainzartistid"].append(StringUtils::Empty);
+ }
+ else
+ {
+ if (joinLayout.GetOutput(joinToSongs_strArtist))
+ songObj["artist"].append(
+ record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
+ if (joinLayout.GetOutput(joinToSongs_strArtistMBID))
+ songObj["musicbrainzartistid"].append(
+ record->at(joinLayout.GetRecNo(joinToSongs_strArtistMBID)).get_asString());
+ }
+ }
+ }
+ if (joinLayout.GetRecNo(joinToSongs_idRole) > 0 &&
+ roleId != record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt())
+ {
+ bSongGenreDone = bSongGenreDone || (roleId > 0); // Not first role, skip genre
+ roleId = record->at(joinLayout.GetRecNo(joinToSongs_idRole)).get_asInt();
+ if (roleId > 1)
+ {
+ if (bJoinRole)
+ { //Contributors
+ CVariant contributor;
+ contributor["name"] =
+ record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString();
+ contributor["role"] =
+ record->at(joinLayout.GetRecNo(joinToSongs_strRole)).get_asString();
+ contributor["roleid"] = roleId;
+ contributor["artistid"] =
+ record->at(joinLayout.GetRecNo(joinToSongs_idArtist)).get_asInt();
+ songObj["contributors"].append(contributor);
+ }
+ // "displaycomposer", "displayconductor" etc.
+ for (size_t i = 0; i < roleidlist.size(); i++)
+ {
+ if (roleidlist[i] == roleId)
+ {
+ songObj[rolefieldlist[i]].append(
+ record->at(joinLayout.GetRecNo(joinToSongs_strArtist)).get_asString());
+ continue;
+ }
+ }
+ }
+ }
+ }
+ if (!bSongGenreDone && joinLayout.GetRecNo(joinToSongs_idGenre) > -1 &&
+ !record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_isNull())
+ {
+ songObj["genreid"].append(record->at(joinLayout.GetRecNo(joinToSongs_idGenre)).get_asInt());
+ }
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+
+ // Ensure random order of output when results set is sorted to process multi-value joins
+ if (sortDescription.sortBy == SortByRandom && joinLayout.HasFilterFields())
+ KODI::UTILS::RandomShuffle(result["songs"].begin_array(), result["songs"].end_array());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+std::string CMusicDatabase::GetIgnoreArticleSQL(const std::string& strField)
+{
+ /*
+ Make SQL clause from ignore article list.
+ Group tokens the same length together, for example :
+ WHEN strArtist LIKE 'the ' OR strArtist LIKE 'the.' strArtist LIKE 'the_' ESCAPE '_'
+ THEN SUBSTR(strArtist, 5)
+ WHEN strArtist LIKE 'an ' OR strArtist LIKE 'an.' strArtist LIKE 'an_' ESCAPE '_'
+ THEN SUBSTR(strArtist, 4)
+ */
+ std::set<std::string> sortTokens = g_langInfo.GetSortTokens();
+ std::string sortclause;
+ size_t tokenlength = 0;
+ std::string strWhen;
+ for (const auto& token : sortTokens)
+ {
+ if (token.length() != tokenlength)
+ {
+ if (!strWhen.empty())
+ {
+ if (!sortclause.empty())
+ sortclause += " ";
+ std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
+ sortclause += "WHEN " + strWhen + strThen;
+ strWhen.clear();
+ }
+ tokenlength = token.length();
+ }
+ std::string tokenclause = token;
+ //Escape any ' or % in the token
+ StringUtils::Replace(tokenclause, "'", "''");
+ StringUtils::Replace(tokenclause, "%", "%%");
+ // Single %, _ and ' so avoid using PrepareSQL
+ tokenclause = strField + " LIKE '" + tokenclause + "%'";
+ if (token.find('_') != std::string::npos)
+ tokenclause += " ESCAPE '_'";
+ if (!strWhen.empty())
+ strWhen += " OR ";
+ strWhen += tokenclause;
+ }
+ if (!strWhen.empty())
+ {
+ if (!sortclause.empty())
+ sortclause += " ";
+ std::string strThen = PrepareSQL(" THEN SUBSTR(%s, %i)", strField.c_str(), tokenlength + 1);
+ sortclause += "WHEN " + strWhen + strThen;
+ }
+ return sortclause;
+}
+
+std::string CMusicDatabase::SortnameBuildSQL(const std::string& strAlias,
+ const SortAttribute& sortAttributes,
+ const std::string& strField,
+ const std::string& strSortField)
+{
+ /*
+ Build SQL for sort name scalar subquery from sort attributes and ignore article list.
+ For example :
+ CASE WHEN strArtistSort IS NOT NULL THEN strArtistSort
+ WHEN strField LIKE 'the ' OR strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5)
+ WHEN strField LIKE 'LIKE 'an.' strField LIKE 'an_' ESCAPE '_' THEN SUBSTR(strArtist, 4)
+ ELSE strField
+ END AS strAlias
+ */
+
+ std::string sortSQL;
+ if (!strSortField.empty() && sortAttributes & SortAttributeUseArtistSortName)
+ sortSQL =
+ PrepareSQL("WHEN %s IS NOT NULL THEN %s ", strSortField.c_str(), strSortField.c_str());
+ if (sortAttributes & SortAttributeIgnoreArticle)
+ {
+ if (!sortSQL.empty())
+ sortSQL += " ";
+ // Make SQL from ignore article list, grouping tokens the same length together
+ sortSQL += GetIgnoreArticleSQL(strField);
+ }
+ if (!sortSQL.empty())
+ {
+ sortSQL = "CASE " + sortSQL; // Not prepare as may contain ' and % etc.
+ sortSQL += PrepareSQL(" ELSE %s END AS %s", strField.c_str(), strAlias.c_str());
+ }
+
+ return sortSQL;
+}
+
+std::string CMusicDatabase::AlphanumericSortSQL(const std::string& strField,
+ const SortOrder& sortOrder)
+{
+ /*
+ Use custom collation ALPHANUM in SQLite. This handles natural number order, case sensitivity
+ and locale UFT-8 order for accents using the same functionality as fileitem list sorting.
+ Natural number order is not significant for where clause comparison and use of calculated fields
+ means there is no advantage in defining as column default in table create than per query (which
+ also makes looking at the db with other tools difficult).
+
+ MySQL does not have callback collation, but all tables are defined with utf8_general_ci an
+ "ascii folding" case insensitive collation. Natural sorting is provided via native functions
+ stored in the db.
+ */
+ std::string DESC;
+ if (sortOrder == SortOrderDescending)
+ DESC = " DESC";
+ std::string strSort;
+
+ if (StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
+ "mysql"))
+ strSort = PrepareSQL("udfNaturalSortFormat(%s, 8, '.')%s", strField.c_str(), DESC.c_str());
+ else
+ strSort = PrepareSQL("%s COLLATE ALPHANUM%s", strField.c_str(), DESC.c_str());
+ return strSort;
+}
+
+void CMusicDatabase::UpdateTables(int version)
+{
+ CLog::Log(LOGINFO, "{} - updating tables", __FUNCTION__);
+ if (version < 34)
+ {
+ m_pDS->exec("ALTER TABLE artist ADD strMusicBrainzArtistID text\n");
+ m_pDS->exec("ALTER TABLE album ADD strMusicBrainzAlbumID text\n");
+ m_pDS->exec(
+ "CREATE TABLE song_new ( idSong integer primary key, idAlbum integer, idPath integer, "
+ "strArtists text, strGenres text, strTitle varchar(512), iTrack integer, iDuration "
+ "integer, iYear integer, dwFileNameCRC text, strFileName text, strMusicBrainzTrackID text, "
+ "iTimesPlayed integer, iStartOffset integer, iEndOffset integer, idThumb integer, "
+ "lastplayed varchar(20) default NULL, rating char default '0', comment text)\n");
+ m_pDS->exec("INSERT INTO song_new ( idSong, idAlbum, idPath, strArtists, strTitle, iTrack, "
+ "iDuration, iYear, dwFileNameCRC, strFileName, strMusicBrainzTrackID, "
+ "iTimesPlayed, iStartOffset, iEndOffset, idThumb, lastplayed, rating, comment) "
+ "SELECT idSong, idAlbum, idPath, strArtists, strTitle, iTrack, iDuration, iYear, "
+ "dwFileNameCRC, strFileName, strMusicBrainzTrackID, iTimesPlayed, iStartOffset, "
+ "iEndOffset, idThumb, lastplayed, rating, comment FROM song");
+
+ m_pDS->exec("DROP TABLE song");
+ m_pDS->exec("ALTER TABLE song_new RENAME TO song");
+
+ m_pDS->exec("UPDATE song SET strMusicBrainzTrackID = NULL");
+ }
+
+ if (version < 36)
+ {
+ // translate legacy musicdb:// paths
+ if (m_pDS->query("SELECT strPath FROM content"))
+ {
+ std::vector<std::string> contentPaths;
+ while (!m_pDS->eof())
+ {
+ contentPaths.push_back(m_pDS->fv(0).get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ for (const auto& originalPath : contentPaths)
+ {
+ std::string path = CLegacyPathTranslation::TranslateMusicDbPath(originalPath);
+ m_pDS->exec(PrepareSQL("UPDATE content SET strPath='%s' WHERE strPath='%s'", path.c_str(),
+ originalPath.c_str()));
+ }
+ }
+ }
+
+ if (version < 39)
+ {
+ m_pDS->exec("CREATE TABLE album_new "
+ "(idAlbum integer primary key, "
+ " strAlbum varchar(256), strMusicBrainzAlbumID text, "
+ " strArtists text, strGenres text, "
+ " iYear integer, idThumb integer, "
+ " bCompilation integer not null default '0', "
+ " strMoods text, strStyles text, strThemes text, "
+ " strReview text, strImage text, strLabel text, "
+ " strType text, "
+ " iRating integer, "
+ " lastScraped varchar(20) default NULL, "
+ " dateAdded varchar (20) default NULL)");
+ m_pDS->exec("INSERT INTO album_new "
+ "(idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, "
+ " iRating) "
+ " SELECT "
+ " album.idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " album.iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, iRating "
+ " FROM album LEFT JOIN albuminfo ON album.idAlbum = albuminfo.idAlbum");
+ m_pDS->exec("UPDATE albuminfosong SET idAlbumInfo = (SELECT idAlbum FROM albuminfo WHERE "
+ "albuminfo.idAlbumInfo = albuminfosong.idAlbumInfo)");
+ m_pDS->exec(PrepareSQL(
+ "UPDATE album_new SET lastScraped='%s' WHERE idAlbum IN (SELECT idAlbum FROM albuminfo)",
+ CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
+ m_pDS->exec("DROP TABLE album");
+ m_pDS->exec("DROP TABLE albuminfo");
+ m_pDS->exec("ALTER TABLE album_new RENAME TO album");
+ }
+ if (version < 40)
+ {
+ m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
+ " strArtist varchar(256), strMusicBrainzArtistID text, "
+ " strBorn text, strFormed text, strGenres text, strMoods text, "
+ " strStyles text, strInstruments text, strBiography text, "
+ " strDied text, strDisbanded text, strYearsActive text, "
+ " strImage text, strFanart text, "
+ " lastScraped varchar(20) default NULL, "
+ " dateAdded varchar (20) default NULL)");
+ m_pDS->exec("INSERT INTO artist_new "
+ "(idArtist, strArtist, strMusicBrainzArtistID, "
+ " strBorn, strFormed, strGenres, strMoods, "
+ " strStyles , strInstruments , strBiography , "
+ " strDied, strDisbanded, strYearsActive, "
+ " strImage, strFanart) "
+ " SELECT "
+ " artist.idArtist, "
+ " strArtist, strMusicBrainzArtistID, "
+ " strBorn, strFormed, strGenres, strMoods, "
+ " strStyles, strInstruments, strBiography, "
+ " strDied, strDisbanded, strYearsActive, "
+ " strImage, strFanart "
+ " FROM artist "
+ " LEFT JOIN artistinfo ON artist.idArtist = artistinfo.idArtist");
+ m_pDS->exec(PrepareSQL("UPDATE artist_new SET lastScraped='%s' WHERE idArtist IN (SELECT "
+ "idArtist FROM artistinfo)",
+ CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str()));
+ m_pDS->exec("DROP TABLE artist");
+ m_pDS->exec("DROP TABLE artistinfo");
+ m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
+ }
+ if (version < 42)
+ {
+ m_pDS->exec("ALTER TABLE album_artist ADD strArtist text\n");
+ m_pDS->exec("ALTER TABLE song_artist ADD strArtist text\n");
+ // populate these
+ std::string sql = "select idArtist,strArtist from artist";
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ m_pDS2->exec(PrepareSQL("UPDATE song_artist SET strArtist='%s' where idArtist=%i",
+ m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt()));
+ m_pDS2->exec(PrepareSQL("UPDATE album_artist SET strArtist='%s' where idArtist=%i",
+ m_pDS->fv(1).get_asString().c_str(), m_pDS->fv(0).get_asInt()));
+ m_pDS->next();
+ }
+ }
+ if (version < 48)
+ { // null out columns that are no longer used
+ m_pDS->exec("UPDATE song SET dwFileNameCRC=NULL, idThumb=NULL");
+ m_pDS->exec("UPDATE album SET idThumb=NULL");
+ }
+ if (version < 49)
+ {
+ m_pDS->exec("CREATE TABLE cue (idPath integer, strFileName text, strCuesheet text)");
+ }
+ if (version < 50)
+ {
+ // add a new column strReleaseType for albums
+ m_pDS->exec("ALTER TABLE album ADD strReleaseType text\n");
+
+ // set strReleaseType based on album name
+ m_pDS->exec(PrepareSQL(
+ "UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NOT NULL AND strAlbum <> ''",
+ CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
+ m_pDS->exec(
+ PrepareSQL("UPDATE album SET strReleaseType = '%s' WHERE strAlbum IS NULL OR strAlbum = ''",
+ CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
+ }
+ if (version < 51)
+ {
+ m_pDS->exec("ALTER TABLE song ADD mood text\n");
+ }
+ if (version < 53)
+ {
+ m_pDS->exec("ALTER TABLE song ADD dateAdded text");
+ }
+ if (version < 54)
+ {
+ //Remove dateAdded from artist table
+ m_pDS->exec("CREATE TABLE artist_new ( idArtist integer primary key, "
+ " strArtist varchar(256), strMusicBrainzArtistID text, "
+ " strBorn text, strFormed text, strGenres text, strMoods text, "
+ " strStyles text, strInstruments text, strBiography text, "
+ " strDied text, strDisbanded text, strYearsActive text, "
+ " strImage text, strFanart text, "
+ " lastScraped varchar(20) default NULL)");
+ m_pDS->exec("INSERT INTO artist_new "
+ "(idArtist, strArtist, strMusicBrainzArtistID, "
+ " strBorn, strFormed, strGenres, strMoods, "
+ " strStyles , strInstruments , strBiography , "
+ " strDied, strDisbanded, strYearsActive, "
+ " strImage, strFanart, lastScraped) "
+ " SELECT "
+ " idArtist, "
+ " strArtist, strMusicBrainzArtistID, "
+ " strBorn, strFormed, strGenres, strMoods, "
+ " strStyles, strInstruments, strBiography, "
+ " strDied, strDisbanded, strYearsActive, "
+ " strImage, strFanart, lastScraped "
+ " FROM artist");
+ m_pDS->exec("DROP TABLE artist");
+ m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
+
+ //Remove dateAdded from album table
+ m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
+ " strAlbum varchar(256), strMusicBrainzAlbumID text, "
+ " strArtists text, strGenres text, "
+ " iYear integer, idThumb integer, "
+ " bCompilation integer not null default '0', "
+ " strMoods text, strStyles text, strThemes text, "
+ " strReview text, strImage text, strLabel text, "
+ " strType text, "
+ " iRating integer, "
+ " lastScraped varchar(20) default NULL, "
+ " strReleaseType text)");
+ m_pDS->exec("INSERT INTO album_new "
+ "(idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, iRating, lastScraped, "
+ " strReleaseType) "
+ " SELECT "
+ " album.idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, iRating, lastScraped, "
+ " strReleaseType"
+ " FROM album");
+ m_pDS->exec("DROP TABLE album");
+ m_pDS->exec("ALTER TABLE album_new RENAME TO album");
+ }
+ if (version < 55)
+ {
+ m_pDS->exec("DROP TABLE karaokedata");
+ }
+ if (version < 57)
+ {
+ m_pDS->exec("ALTER TABLE song ADD userrating INTEGER NOT NULL DEFAULT 0");
+ m_pDS->exec("UPDATE song SET rating = 0 WHERE rating < 0 or rating IS NULL");
+ m_pDS->exec("UPDATE song SET userrating = rating * 2");
+ m_pDS->exec("UPDATE song SET rating = 0");
+ m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
+ " idAlbum INTEGER, idPath INTEGER, "
+ " strArtists TEXT, strGenres TEXT, strTitle VARCHAR(512), "
+ " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
+ " dwFileNameCRC TEXT, "
+ " strFileName TEXT, strMusicBrainzTrackID TEXT, "
+ " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
+ " idThumb INTEGER, "
+ " lastplayed VARCHAR(20) DEFAULT NULL, "
+ " rating FLOAT DEFAULT 0, "
+ " userrating INTEGER DEFAULT 0, "
+ " comment TEXT, mood TEXT, dateAdded TEXT)");
+ m_pDS->exec("INSERT INTO song_new "
+ "(idSong, "
+ " idAlbum, idPath, "
+ " strArtists, strGenres, strTitle, "
+ " iTrack, iDuration, iYear, "
+ " dwFileNameCRC, "
+ " strFileName, strMusicBrainzTrackID, "
+ " iTimesPlayed, iStartOffset, iEndOffset, "
+ " idThumb, "
+ " lastplayed,"
+ " rating, userrating, "
+ " comment, mood, dateAdded)"
+ " SELECT "
+ " idSong, "
+ " idAlbum, idPath, "
+ " strArtists, strGenres, strTitle, "
+ " iTrack, iDuration, iYear, "
+ " dwFileNameCRC, "
+ " strFileName, strMusicBrainzTrackID, "
+ " iTimesPlayed, iStartOffset, iEndOffset, "
+ " idThumb, "
+ " lastplayed,"
+ " rating, "
+ " userrating, "
+ " comment, mood, dateAdded"
+ " FROM song");
+ m_pDS->exec("DROP TABLE song");
+ m_pDS->exec("ALTER TABLE song_new RENAME TO song");
+
+ m_pDS->exec("ALTER TABLE album ADD iUserrating INTEGER NOT NULL DEFAULT 0");
+ m_pDS->exec("UPDATE album SET iRating = 0 WHERE iRating < 0 or iRating IS NULL");
+ m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
+ " strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
+ " strArtists TEXT, strGenres TEXT, "
+ " iYear INTEGER, idThumb INTEGER, "
+ " bCompilation INTEGER NOT NULL DEFAULT '0', "
+ " strMoods TEXT, strStyles TEXT, strThemes TEXT, "
+ " strReview TEXT, strImage TEXT, strLabel TEXT, "
+ " strType TEXT, "
+ " fRating FLOAT NOT NULL DEFAULT 0, "
+ " iUserrating INTEGER NOT NULL DEFAULT 0, "
+ " lastScraped VARCHAR(20) DEFAULT NULL, "
+ " strReleaseType TEXT)");
+ m_pDS->exec("INSERT INTO album_new "
+ "(idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, "
+ " fRating, "
+ " iUserrating, "
+ " lastScraped, "
+ " strReleaseType)"
+ " SELECT "
+ " idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, strGenres, "
+ " iYear, idThumb, "
+ " bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, "
+ " iRating, "
+ " iUserrating, "
+ " lastScraped, "
+ " strReleaseType"
+ " FROM album");
+ m_pDS->exec("DROP TABLE album");
+ m_pDS->exec("ALTER TABLE album_new RENAME TO album");
+
+ m_pDS->exec("ALTER TABLE album ADD iVotes INTEGER NOT NULL DEFAULT 0");
+ m_pDS->exec("ALTER TABLE song ADD votes INTEGER NOT NULL DEFAULT 0");
+ }
+ if (version < 58)
+ {
+ m_pDS->exec("UPDATE album SET fRating = fRating * 2");
+ }
+ if (version < 59)
+ {
+ m_pDS->exec("CREATE TABLE role (idRole integer primary key, strRole text)");
+ m_pDS->exec("INSERT INTO role(idRole, strRole) VALUES (1, 'Artist')"); //Default Role
+
+ //Remove strJoinPhrase, boolFeatured from song_artist table and add idRole
+ m_pDS->exec("CREATE TABLE song_artist_new (idArtist integer, idSong integer, idRole integer, "
+ "iOrder integer, strArtist text)");
+ m_pDS->exec("INSERT INTO song_artist_new (idArtist, idSong, idRole, iOrder, strArtist) "
+ "SELECT idArtist, idSong, 1 as idRole, iOrder, strArtist FROM song_artist");
+ m_pDS->exec("DROP TABLE song_artist");
+ m_pDS->exec("ALTER TABLE song_artist_new RENAME TO song_artist");
+
+ //Remove strJoinPhrase, boolFeatured from album_artist table
+ m_pDS->exec("CREATE TABLE album_artist_new (idArtist integer, idAlbum integer, iOrder integer, "
+ "strArtist text)");
+ m_pDS->exec("INSERT INTO album_artist_new (idArtist, idAlbum, iOrder, strArtist) "
+ "SELECT idArtist, idAlbum, iOrder, strArtist FROM album_artist");
+ m_pDS->exec("DROP TABLE album_artist");
+ m_pDS->exec("ALTER TABLE album_artist_new RENAME TO album_artist");
+ }
+ if (version < 60)
+ {
+ // From now on artist ID = 1 will be an artificial artist [Missing] use for songs that
+ // do not have an artist tag to ensure all songs in the library have at least one artist.
+ std::string strSQL;
+ if (GetArtistExists(BLANKARTIST_ID))
+ {
+ // When BLANKARTIST_ID (=1) is already in use, move the record
+ try
+ { //No mbid index yet, so can have record for artist twice even with mbid
+ strSQL = PrepareSQL("INSERT INTO artist SELECT null, "
+ "strArtist, strMusicBrainzArtistID, "
+ "strBorn, strFormed, strGenres, strMoods, "
+ "strStyles, strInstruments, strBiography, "
+ "strDied, strDisbanded, strYearsActive, "
+ "strImage, strFanart, lastScraped "
+ "FROM artist WHERE artist.idArtist = %i",
+ BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+ int idArtist = (int)m_pDS->lastinsertid();
+ //No triggers, so can delete artist without effecting other tables.
+ strSQL = PrepareSQL("DELETE FROM artist WHERE artist.idArtist = %i", BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+
+ // Update related tables with the new artist ID
+ // Indices have been dropped making transactions very slow, so create appropriate temp indices
+ m_pDS->exec("CREATE INDEX idxSongArtist2 ON song_artist ( idArtist )");
+ m_pDS->exec("CREATE INDEX idxAlbumArtist2 ON album_artist ( idArtist )");
+ m_pDS->exec("CREATE INDEX idxDiscography ON discography ( idArtist )");
+ m_pDS->exec("CREATE INDEX ix_art ON art ( media_id, media_type(20) )");
+ strSQL = PrepareSQL("UPDATE song_artist SET idArtist = %i WHERE idArtist = %i", idArtist,
+ BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+ strSQL = PrepareSQL("UPDATE album_artist SET idArtist = %i WHERE idArtist = %i", idArtist,
+ BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+ strSQL =
+ PrepareSQL("UPDATE art SET media_id = %i WHERE media_id = %i AND media_type='artist'",
+ idArtist, BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+ strSQL = PrepareSQL("UPDATE discography SET idArtist = %i WHERE idArtist = %i", idArtist,
+ BLANKARTIST_ID);
+ m_pDS->exec(strSQL);
+ // Drop temp indices
+ m_pDS->exec("DROP INDEX idxSongArtist2 ON song_artist");
+ m_pDS->exec("DROP INDEX idxAlbumArtist2 ON album_artist");
+ m_pDS->exec("DROP INDEX idxDiscography ON discography");
+ m_pDS->exec("DROP INDEX ix_art ON art");
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Moving existing artist to add missing tag artist has failed");
+ }
+ }
+
+ // Create missing artist tag artist [Missing].
+ // Fake MusicbrainzId assures uniqueness and avoids updates from scanned songs
+ strSQL = PrepareSQL(
+ "INSERT INTO artist (idArtist, strArtist, strMusicBrainzArtistID) VALUES( %i, '%s', '%s' )",
+ BLANKARTIST_ID, BLANKARTIST_NAME.c_str(), BLANKARTIST_FAKEMUSICBRAINZID.c_str());
+ m_pDS->exec(strSQL);
+
+ // Indices have been dropped making transactions very slow, so create temp index
+ m_pDS->exec("CREATE INDEX idxSongArtist1 ON song_artist ( idSong, idRole )");
+ m_pDS->exec("CREATE INDEX idxAlbumArtist1 ON album_artist ( idAlbum )");
+
+ // Ensure all songs have at least one artist, set those without to [Missing]
+ strSQL = "SELECT count(idSong) FROM song "
+ "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
+ "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = 1)";
+ int numsongs = GetSingleValueInt(strSQL);
+ if (numsongs > 0)
+ {
+ CLog::Log(LOGDEBUG, "{} songs have no artist, setting artist to [Missing]", numsongs);
+ // Insert song_artist records for songs that don't have any
+ try
+ {
+ strSQL = PrepareSQL("INSERT INTO song_artist(idArtist, idSong, idRole, strArtist, iOrder) "
+ "SELECT %i, idSong, %i, '%s', 0 FROM song "
+ "WHERE NOT EXISTS(SELECT idSong FROM song_artist "
+ "WHERE song_artist.idsong = song.idsong AND song_artist.idRole = %i)",
+ BLANKARTIST_ID, ROLE_ARTIST, BLANKARTIST_NAME.c_str(), ROLE_ARTIST);
+ ExecuteQuery(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Setting missing artist for songs without an artist has failed");
+ }
+ }
+
+ // Ensure all albums have at least one artist, set those without to [Missing]
+ strSQL = "SELECT count(idAlbum) FROM album "
+ "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
+ "WHERE album_artist.idAlbum = album.idAlbum)";
+ int numalbums = GetSingleValueInt(strSQL);
+ if (numalbums > 0)
+ {
+ CLog::Log(LOGDEBUG, "{} albums have no artist, setting artist to [Missing]", numalbums);
+ // Insert album_artist records for albums that don't have any
+ try
+ {
+ strSQL = PrepareSQL("INSERT INTO album_artist(idArtist, idAlbum, strArtist, iOrder) "
+ "SELECT %i, idAlbum, '%s', 0 FROM album "
+ "WHERE NOT EXISTS(SELECT idAlbum FROM album_artist "
+ "WHERE album_artist.idAlbum = album.idAlbum)",
+ BLANKARTIST_ID, BLANKARTIST_NAME.c_str());
+ ExecuteQuery(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Setting artist missing for albums without an artist has failed");
+ }
+ }
+ //Remove temp indices, full analytics for database created later
+ m_pDS->exec("DROP INDEX idxSongArtist1 ON song_artist");
+ m_pDS->exec("DROP INDEX idxAlbumArtist1 ON album_artist");
+ }
+ if (version < 61)
+ {
+ // Create versiontagscan table
+ m_pDS->exec("CREATE TABLE versiontagscan (idVersion integer, iNeedsScan integer)");
+ m_pDS->exec("INSERT INTO versiontagscan (idVersion, iNeedsScan) values(0, 0)");
+ }
+ if (version < 62)
+ {
+ CLog::Log(LOGINFO, "create audiobook table");
+ m_pDS->exec("CREATE TABLE audiobook (idBook integer primary key, "
+ " strBook varchar(256), strAuthor text,"
+ " bookmark integer, file text,"
+ " dateAdded varchar (20) default NULL)");
+ }
+ if (version < 63)
+ {
+ // Add strSortName to Artist table
+ m_pDS->exec("ALTER TABLE artist ADD strSortName text\n");
+
+ //Remove idThumb (column unused since v47), rename strArtists and add strArtistSort to album table
+ m_pDS->exec("CREATE TABLE album_new (idAlbum integer primary key, "
+ " strAlbum varchar(256), strMusicBrainzAlbumID text, "
+ " strArtistDisp text, strArtistSort text, strGenres text, "
+ " iYear integer, bCompilation integer not null default '0', "
+ " strMoods text, strStyles text, strThemes text, "
+ " strReview text, strImage text, strLabel text, "
+ " strType text, "
+ " fRating FLOAT NOT NULL DEFAULT 0, "
+ " iUserrating INTEGER NOT NULL DEFAULT 0, "
+ " lastScraped varchar(20) default NULL, "
+ " strReleaseType text, "
+ " iVotes INTEGER NOT NULL DEFAULT 0)");
+ m_pDS->exec("INSERT INTO album_new "
+ "(idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtistDisp, strArtistSort, strGenres, "
+ " iYear, bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, "
+ " fRating, iUserrating, iVotes, "
+ " lastScraped, "
+ " strReleaseType)"
+ " SELECT "
+ " idAlbum, "
+ " strAlbum, strMusicBrainzAlbumID, "
+ " strArtists, NULL, strGenres, "
+ " iYear, bCompilation, "
+ " strMoods, strStyles, strThemes, "
+ " strReview, strImage, strLabel, "
+ " strType, "
+ " fRating, iUserrating, iVotes, "
+ " lastScraped, "
+ " strReleaseType"
+ " FROM album");
+ m_pDS->exec("DROP TABLE album");
+ m_pDS->exec("ALTER TABLE album_new RENAME TO album");
+
+ //Remove dwFileNameCRC, idThumb (columns unused since v47), rename strArtists and add strArtistSort to song table
+ m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
+ " idAlbum INTEGER, idPath INTEGER, "
+ " strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
+ " iTrack INTEGER, iDuration INTEGER, iYear INTEGER, "
+ " strFileName TEXT, strMusicBrainzTrackID TEXT, "
+ " iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
+ " lastplayed VARCHAR(20) DEFAULT NULL, "
+ " rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
+ " userrating INTEGER NOT NULL DEFAULT 0, "
+ " comment TEXT, mood TEXT, dateAdded TEXT)");
+ m_pDS->exec("INSERT INTO song_new "
+ "(idSong, "
+ " idAlbum, idPath, "
+ " strArtistDisp, strArtistSort, strGenres, strTitle, "
+ " iTrack, iDuration, iYear, "
+ " strFileName, strMusicBrainzTrackID, "
+ " iTimesPlayed, iStartOffset, iEndOffset, "
+ " lastplayed,"
+ " rating, userrating, votes, "
+ " comment, mood, dateAdded)"
+ " SELECT "
+ " idSong, "
+ " idAlbum, idPath, "
+ " strArtists, NULL, strGenres, strTitle, "
+ " iTrack, iDuration, iYear, "
+ " strFileName, strMusicBrainzTrackID, "
+ " iTimesPlayed, iStartOffset, iEndOffset, "
+ " lastplayed,"
+ " rating, userrating, votes, "
+ " comment, mood, dateAdded"
+ " FROM song");
+ m_pDS->exec("DROP TABLE song");
+ m_pDS->exec("ALTER TABLE song_new RENAME TO song");
+ }
+ if (version < 65)
+ {
+ // Remove cue table
+ m_pDS->exec("DROP TABLE cue");
+ // Add strReplayGain to song table
+ m_pDS->exec("ALTER TABLE song ADD strReplayGain TEXT\n");
+ }
+ if (version < 66)
+ {
+ // Add a new columns strReleaseGroupMBID, bScrapedMBID for albums
+ m_pDS->exec("ALTER TABLE album ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
+ m_pDS->exec("ALTER TABLE album ADD strReleaseGroupMBID TEXT \n");
+ // Add a new column bScrapedMBID for artists
+ m_pDS->exec("ALTER TABLE artist ADD bScrapedMBID INTEGER NOT NULL DEFAULT 0\n");
+ }
+ if (version < 67)
+ {
+ // Add infosetting table
+ m_pDS->exec("CREATE TABLE infosetting (idSetting INTEGER PRIMARY KEY, strScraperPath TEXT, "
+ "strSettings TEXT)");
+ // Add a new column for setting to album and artist tables
+ m_pDS->exec("ALTER TABLE artist ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
+ m_pDS->exec("ALTER TABLE album ADD idInfoSetting INTEGER NOT NULL DEFAULT 0\n");
+
+ // Attempt to get album and artist specific scraper settings from the content table, extracting ids from path
+ m_pDS->exec(
+ "CREATE TABLE content_temp(id INTEGER PRIMARY KEY, idItem INTEGER, strContent text, "
+ "strScraperPath text, strSettings text)");
+ try
+ {
+ m_pDS->exec("INSERT INTO content_temp(idItem, strContent, strScraperPath, strSettings) "
+ "SELECT SUBSTR(strPath, 19, LENGTH(strPath) - 19) + 0 AS idItem, strContent, "
+ "strScraperPath, strSettings "
+ "FROM content WHERE strContent = 'artists' AND strPath LIKE "
+ "'musicdb://artists/_%/' ORDER BY idItem");
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,
+ "Migrating specific artist scraper settings has failed, settings not transferred");
+ }
+ try
+ {
+ m_pDS->exec("INSERT INTO content_temp (idItem, strContent, strScraperPath, strSettings ) "
+ "SELECT SUBSTR(strPath, 18, LENGTH(strPath) - 18) + 0 AS idItem, strContent, "
+ "strScraperPath, strSettings "
+ "FROM content WHERE strContent = 'albums' AND strPath LIKE "
+ "'musicdb://albums/_%/' ORDER BY idItem");
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,
+ "Migrating specific album scraper settings has failed, settings not transferred");
+ }
+ try
+ {
+ m_pDS->exec("INSERT INTO infosetting(idSetting, strScraperPath, strSettings) "
+ "SELECT id, strScraperPath, strSettings FROM content_temp");
+ m_pDS->exec(
+ "UPDATE artist SET idInfoSetting = "
+ "(SELECT id FROM content_temp WHERE strContent = 'artists' AND idItem = idArtist) "
+ "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'artists' AND idItem = "
+ "idArtist) ");
+ m_pDS->exec("UPDATE album SET idInfoSetting = "
+ "(SELECT id FROM content_temp WHERE strContent = 'albums' AND idItem = idAlbum) "
+ "WHERE EXISTS(SELECT 1 FROM content_temp WHERE strContent = 'albums' AND idItem "
+ "= idAlbum) ");
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,
+ "Migrating album and artist scraper settings has failed, settings not transferred");
+ }
+ m_pDS->exec("DROP TABLE content_temp");
+
+ // Remove content table
+ m_pDS->exec("DROP TABLE content");
+ // Remove albuminfosong table
+ m_pDS->exec("DROP TABLE albuminfosong");
+ }
+ if (version < 68)
+ {
+ // Add a new columns strType, strGender, strDisambiguation for artists
+ m_pDS->exec("ALTER TABLE artist ADD strType TEXT \n");
+ m_pDS->exec("ALTER TABLE artist ADD strGender TEXT \n");
+ m_pDS->exec("ALTER TABLE artist ADD strDisambiguation TEXT \n");
+ }
+ if (version < 69)
+ {
+ // Remove album_genre table
+ m_pDS->exec("DROP TABLE album_genre");
+ }
+ if (version < 70)
+ {
+ // Update all songs iStartOffset and iEndOffset to milliseconds instead of frames (* 1000 / 75)
+ m_pDS->exec("UPDATE song SET iStartOffset = iStartOffset * 40 / 3, iEndOffset = iEndOffset * "
+ "40 / 3 \n");
+ }
+ if (version < 71)
+ {
+ // Add lastscanned to versiontagscan table
+ m_pDS->exec("ALTER TABLE versiontagscan ADD lastscanned VARCHAR(20)\n");
+ CDateTime dateAdded = CDateTime::GetCurrentDateTime();
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
+ dateAdded.GetAsDBDateTime().c_str()));
+ }
+ if (version < 72)
+ {
+ // Create source table
+ m_pDS->exec(
+ "CREATE TABLE source (idSource INTEGER PRIMARY KEY, strName TEXT, strMultipath TEXT)");
+ // Create source_path table
+ m_pDS->exec(
+ "CREATE TABLE source_path (idSource INTEGER, idPath INTEGER, strPath varchar(512))");
+ // Create album_source table
+ m_pDS->exec("CREATE TABLE album_source (idSource INTEGER, idAlbum INTEGER)");
+ // Populate source and source_path tables from sources.xml
+ // Filling album_source needs to be done after indexes are created or it is
+ // very slow. It could be populated during CreateAnalytics but it is checked
+ // and filled as part of scanning anyway so simply force full rescan.
+ MigrateSources();
+ }
+ if (version < 73)
+ {
+ // add bBoxedSet to album table
+ m_pDS->exec("ALTER TABLE album ADD bBoxedSet INTEGER NOT NULL DEFAULT 0 \n");
+ // add iDiscTotal to album table
+ m_pDS->exec("ALTER TABLE album ADD iDiscTotal INTEGER NOT NULL DEFAULT 0 \n");
+ // populate iDiscTotal from the data already in the song table
+ m_pDS->exec("UPDATE album SET iDisctotal = (SELECT COUNT(DISTINCT (iTrack >> 16)) "
+ "FROM song WHERE song.idAlbum = album.idAlbum GROUP BY idAlbum ) "
+ "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
+ // add strDiscSubtitles to song table
+ m_pDS->exec("ALTER TABLE song ADD strDiscSubtitle TEXT \n");
+ }
+ if (version < 74)
+ {
+ //Remove iYear, add stReleaseDate and strOrigReleaseDate columns to album table
+ m_pDS->exec("CREATE TABLE album_new (idAlbum INTEGER PRIMARY KEY, "
+ "strAlbum VARCHAR(256), strMusicBrainzAlbumID TEXT, "
+ "strReleaseGroupMBID TEXT, "
+ "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, "
+ "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
+ "bBoxedSet INTEGER NOT NULL DEFAULT 0, "
+ "bCompilation INTEGER NOT NULL DEFAULT '0', "
+ "strMoods TEXT, strStyles TEXT, strThemes TEXT, "
+ "strReview TEXT, strImage TEXT, strLabel TEXT, "
+ "strType TEXT, "
+ "fRating FLOAT NOT NULL DEFAULT 0, "
+ "iVotes INTEGER NOT NULL DEFAULT 0, "
+ "iUserrating INTEGER NOT NULL DEFAULT 0, "
+ "lastScraped VARCHAR(20) DEFAULT NULL, "
+ "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
+ "strReleaseType TEXT, "
+ "iDiscTotal INTEGER NOT NULL DEFAULT 0, "
+ "idInfoSetting INTEGER NOT NULL DEFAULT 0)");
+ // Prepare as MySQL has different CAST datatypes
+ m_pDS->exec(
+ PrepareSQL("INSERT INTO album_new "
+ "(idalbum, strAlbum, "
+ "strMusicBrainzAlbumID, strReleaseGroupMBID, "
+ "strArtistDisp, strArtistSort, strGenres, "
+ "strReleaseDate, strOrigReleaseDate, "
+ "bBoxedSet, bCompilation, strMoods, strStyles, strThemes, "
+ "strReview, strImage, strLabel, strType, "
+ "fRating, iVotes, iUserrating, "
+ "lastScraped, bScrapedMBID, strReleaseType, "
+ "iDiscTotal, idInfoSetting) "
+ "SELECT "
+ "idAlbum, strAlbum, "
+ "strMusicBrainzAlbumID, strReleaseGroupMBID, "
+ "strArtistDisp, strArtistSort, strGenres, "
+ "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
+ "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
+ // bBoxedSet could be null if v72 not rescanned and that is invalid, tidy up now
+ "CASE WHEN bBoxedSet IS NULL THEN 0 ELSE bBoxedSet END, "
+ "bCompilation, strMoods, strStyles, strThemes, "
+ "strReview, strImage, strLabel, strType, "
+ "fRating, iVotes, iUserrating, "
+ "lastScraped, bScrapedMBID, strReleaseType, "
+ "iDiscTotal, idInfoSetting "
+ "FROM album"));
+ m_pDS->exec("DROP TABLE album");
+ m_pDS->exec("ALTER TABLE album_new RENAME TO album");
+
+ //Remove iYear and add stReleaseDate, strOrigReleaseDate and iBPM columns to song table
+ m_pDS->exec("CREATE TABLE song_new (idSong INTEGER PRIMARY KEY, "
+ "idAlbum INTEGER, idPath INTEGER, "
+ "strArtistDisp TEXT, strArtistSort TEXT, strGenres TEXT, strTitle VARCHAR(512), "
+ "iTrack INTEGER, iDuration INTEGER, "
+ "strReleaseDate TEXT, strOrigReleaseDate TEXT, "
+ "strDiscSubtitle TEXT, strFileName TEXT, strMusicBrainzTrackID TEXT, "
+ "iTimesPlayed INTEGER, iStartOffset INTEGER, iEndOffset INTEGER, "
+ "lastplayed VARCHAR(20) DEFAULT NULL, "
+ "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
+ "userrating INTEGER NOT NULL DEFAULT 0, "
+ "comment TEXT, mood TEXT, iBPM INTEGER NOT NULL DEFAULT 0, strReplayGain TEXT, "
+ "dateAdded TEXT)");
+ // Prepare as MySQL has different CAST datatypes
+ m_pDS->exec(PrepareSQL("INSERT INTO song_new "
+ "(idSong, "
+ "idAlbum, idPath, "
+ "strArtistDisp, strArtistSort, strGenres, strTitle, "
+ "iTrack, iDuration, "
+ "strReleaseDate, strOrigReleaseDate, "
+ "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
+ "iTimesPlayed, iStartOffset, iEndOffset, "
+ "lastplayed, "
+ "rating, userrating, votes, "
+ "comment, mood, strReplayGain, dateAdded) "
+ "SELECT "
+ "idSong, "
+ "idAlbum, idPath, "
+ "strArtistDisp, strArtistSort, strGenres, strTitle, "
+ "iTrack, iDuration, "
+ "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
+ "CASE WHEN iYear > 0 THEN CAST(iYear AS TEXT) ELSE NULL END, "
+ "strDiscSubtitle, strFileName, strMusicBrainzTrackID, "
+ "iTimesPlayed, iStartOffset, iEndOffset, "
+ "lastplayed, "
+ "rating, userrating, votes, "
+ "comment, mood, strReplayGain, dateAdded "
+ "FROM song"));
+ m_pDS->exec("DROP TABLE song");
+ m_pDS->exec("ALTER TABLE song_new RENAME TO song");
+ }
+ if (version < 75)
+ {
+ m_pDS->exec("ALTER TABLE song ADD iBitRate INTEGER NOT NULL DEFAULT 0");
+ m_pDS->exec("ALTER TABLE song ADD iSampleRate INTEGER NOT NULL DEFAULT 0");
+ m_pDS->exec("ALTER TABLE song ADD iChannels INTEGER NOT NULL DEFAULT 0");
+ }
+ if (version < 77)
+ {
+ m_pDS->exec("ALTER TABLE album ADD strReleaseStatus TEXT");
+ }
+ if (version < 78)
+ {
+ std::string strUTCNow = CDateTime::GetUTCDateTime().GetAsDBDateTime();
+
+ // Add removed_link table
+ m_pDS->exec("CREATE TABLE removed_link(idArtist INTEGER, idMedia INTEGER, idRole INTEGER)");
+ // Add lastcleaned and artistlinksupdated to versiontagscan table
+ m_pDS->exec("ALTER TABLE versiontagscan ADD lastcleaned VARCHAR(20)");
+ m_pDS->exec("ALTER TABLE versiontagscan ADD artistlinksupdated VARCHAR(20)");
+ m_pDS->exec("ALTER TABLE versiontagscan ADD genresupdated VARCHAR(20)");
+ // Adjust lastscanned if original local time value is after current UTC
+ if (GetLibraryLastUpdated() > strUTCNow)
+ SetLibraryLastUpdated();
+ m_pDS->exec("UPDATE versiontagscan SET lastcleaned = lastscanned, "
+ "genresupdated = lastscanned, "
+ "artistlinksupdated = lastscanned");
+
+ // Add dateNew, dateModified to song table
+ m_pDS->exec("ALTER TABLE song ADD dateNew TEXT");
+ m_pDS->exec("ALTER TABLE song ADD dateModified TEXT");
+ // Set new to dateAdded and modified to lastplayed as estimates
+ // Limit those local time values to now UTC, and modified is after new
+ m_pDS->exec("UPDATE song SET dateNew = dateAdded, dateModified = lastplayed");
+ m_pDS->exec(PrepareSQL("UPDATE song SET dateNew = '%s' WHERE dateNew > '%s'", strUTCNow.c_str(),
+ strUTCNow.c_str()));
+ m_pDS->exec("UPDATE song SET dateModified = dateNew WHERE dateModified IS NULL");
+ m_pDS->exec(PrepareSQL("UPDATE song SET dateModified = '%s' WHERE dateModified > '%s'",
+ strUTCNow.c_str(), strUTCNow.c_str()));
+ m_pDS->exec("UPDATE song SET dateAdded = dateModified WHERE dateAdded > dateModified");
+
+ // Add dateAdded, dateNew, dateModified to album table
+ m_pDS->exec("ALTER TABLE album ADD dateAdded TEXT");
+ m_pDS->exec("ALTER TABLE album ADD dateNew TEXT");
+ m_pDS->exec("ALTER TABLE album ADD dateModified TEXT");
+ // Set dateAdded and new values from song dates, and modified to lastscraped as estimates
+ // Limit modified value to now UTC and after new
+ // Indices have been dropped making subquery very slow, so create temp index
+ m_pDS->exec("CREATE INDEX idxSong3 ON song(idAlbum)");
+ m_pDS->exec("UPDATE album SET dateAdded = "
+ "(SELECT MAX(song.dateAdded) FROM song WHERE song.idAlbum = album.idAlbum)");
+ m_pDS->exec("UPDATE album SET dateNew = "
+ "(SELECT MIN(song.dateNew) FROM song WHERE song.idAlbum = album.idAlbum)");
+ m_pDS->exec("UPDATE album SET dateModified = dateNew");
+ m_pDS->exec("UPDATE album SET dateModified = lastscraped WHERE lastscraped > dateModified");
+ m_pDS->exec(PrepareSQL("UPDATE album SET dateModified = '%s' WHERE dateModified > '%s'",
+ strUTCNow.c_str(), strUTCNow.c_str()));
+ //Remove temp index, full analytics for database created later
+ m_pDS->exec("DROP INDEX idxSong3 ON song");
+
+ // Add dateAdded, dateNew, dateModified to artist table
+ m_pDS->exec("ALTER TABLE artist ADD dateAdded TEXT");
+ m_pDS->exec("ALTER TABLE artist ADD dateNew TEXT");
+ m_pDS->exec("ALTER TABLE artist ADD dateModified TEXT");
+ // dateAdded has NULL values until files rescanned by user
+ // Set new and modified to now UTC as not worth complexity of estimating from song dates
+ m_pDS->exec(PrepareSQL("UPDATE artist SET dateNew = '%s'", strUTCNow.c_str()));
+ m_pDS->exec("UPDATE artist SET dateModified = dateNew");
+ }
+ if (version < 79)
+ {
+ m_pDS->exec("ALTER TABLE discography ADD strReleaseGroupMBID TEXT");
+ }
+ if (version < 80)
+ {
+ m_pDS->exec("ALTER TABLE album ADD iAlbumDuration INTEGER NOT NULL DEFAULT 0");
+ // update duration for all current albums
+ m_pDS->exec("UPDATE album SET iAlbumDuration = (SELECT SUM(song.iDuration) FROM song "
+ "WHERE song.idAlbum = album.idAlbum) "
+ "WHERE EXISTS (SELECT 1 FROM song WHERE song.idAlbum = album.idAlbum)");
+ }
+ if (version < 82)
+ {
+ // Update artist table combining fanart URL data into strImage field
+ // Clear empty URL data <fanart /> and <thumb />
+ m_pDS->exec("UPDATE artist SET strFanart = '' WHERE strFanart = '<fanart />'");
+ m_pDS->exec("UPDATE artist SET strImage = '' WHERE strImage = '<thumb />'");
+ //Prepare strFanart - strip <fanart>...</fanart>, add aspect to the URLs
+ m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '<fanart>', '')");
+ m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, '</fanart>', '')");
+ m_pDS->exec("UPDATE artist SET strFanart = REPLACE(strFanart, 'thumb preview', 'thumb "
+ "aspect=\"fanart\" preview')");
+ // Art URLs limited on MySQL databases to 65535 characters (TEXT field)
+ // Truncate the fanart when total URLs exceeds this
+ bool bisMySQL = StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type,
+ "mysql");
+ if (bisMySQL)
+ {
+ std::string strSQL = "SELECT idArtist, strFanart, strImage FROM artist "
+ "WHERE LENGTH(strImage) + LENGTH(strFanart) > 65535";
+ if (m_pDS->query(strSQL))
+ {
+ while (!m_pDS->eof())
+ {
+ int idArtist = m_pDS->fv("idArtist").get_asInt();
+ std::string strFanart = m_pDS->fv("strFanart").get_asString();
+ std::string strImage = m_pDS->fv("strImage").get_asString();
+ size_t space = 65535;
+ // Trim strImage to allow arbitrary half space for fanart
+ if (!TrimImageURLs(strImage, space / 2))
+ strImage.clear(); // </thumb> not found, empty field
+ space = space - strImage.length();
+ // Trim fanart to fit remaining space
+ if (!TrimImageURLs(strFanart, space))
+ strFanart.clear(); // </thumb> not found, empty field
+
+ strSQL = PrepareSQL("UPDATE artist SET strFanart = '%s', strImage = '%s' "
+ "WHERE idArtist = %i",
+ strFanart.c_str(), strImage.c_str(), idArtist);
+ m_pDS2->exec(strSQL); // Use other dataset to update while looping result set
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ }
+
+ // Remove strFanart column from artist table
+ m_pDS->exec("CREATE TABLE artist_new (idArtist INTEGER PRIMARY KEY, "
+ "strArtist varchar(256), strMusicBrainzArtistID text, "
+ "strSortName text, "
+ "strType text, strGender text, strDisambiguation text, "
+ "strBorn text, strFormed text, strGenres text, strMoods text, "
+ "strStyles text, strInstruments text, strBiography text, "
+ "strDied text, strDisbanded text, strYearsActive text, "
+ "strImage text, "
+ "lastScraped varchar(20) default NULL, "
+ "bScrapedMBID INTEGER NOT NULL DEFAULT 0, "
+ "idInfoSetting INTEGER NOT NULL DEFAULT 0, "
+ "dateAdded TEXT, dateNew TEXT, dateModified TEXT)");
+ // Concatenate fanart URLs into strImage field
+ // Prepare SQL to convert CONCAT to || in SQLite
+ m_pDS->exec(PrepareSQL("INSERT INTO artist_new "
+ "(idArtist, strArtist, strMusicBrainzArtistID, "
+ "strSortName, strType, strGender, strDisambiguation, "
+ "strBorn, strFormed, strGenres, strMoods, "
+ "strStyles , strInstruments , strBiography , "
+ "strDied, strDisbanded, strYearsActive, "
+ "strImage, "
+ "lastScraped, bScrapedMBID, idInfoSetting, "
+ "dateAdded, dateNew, dateModified) "
+ "SELECT "
+ "artist.idArtist, "
+ "strArtist, strMusicBrainzArtistID, "
+ "strSortName, strType, strGender, strDisambiguation, "
+ "strBorn, strFormed, strGenres, strMoods, "
+ "strStyles, strInstruments, strBiography, "
+ "strDied, strDisbanded, strYearsActive, "
+ "CONCAT(strImage, strFanart), "
+ "lastScraped, bScrapedMBID, idInfoSetting, "
+ "dateAdded, dateNew, dateModified "
+ "FROM artist"));
+ m_pDS->exec("DROP TABLE artist");
+ m_pDS->exec("ALTER TABLE artist_new RENAME TO artist");
+ }
+ // Set the version of tag scanning required.
+ // Not every schema change requires the tags to be rescanned, set to the highest schema version
+ // that needs this. Forced rescanning (of music files that have not changed since they were
+ // previously scanned) also accommodates any changes to the way tags are processed
+ // e.g. read tags that were not processed by previous versions.
+ // The original db version when the tags were scanned, and the minimal db version needed are
+ // later used to determine if a forced rescan should be prompted
+
+ // The last schema change needing forced rescanning was 73.
+ // This is because Kodi can now read and process extra tags involved in the creation of box sets
+
+ SetMusicNeedsTagScan(73);
+
+ // After all updates, store the original db version.
+ // This indicates the version of tag processing that was used to populate db
+ SetMusicTagScanVersion(version);
+}
+
+int CMusicDatabase::GetSchemaVersion() const
+{
+ return 82;
+}
+
+int CMusicDatabase::GetMusicNeedsTagScan()
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string sql = "SELECT * FROM versiontagscan";
+ if (!m_pDS->query(sql))
+ return -1;
+
+ if (m_pDS->num_rows() != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+
+ int idVersion = m_pDS->fv("idVersion").get_asInt();
+ int iNeedsScan = m_pDS->fv("iNeedsScan").get_asInt();
+ m_pDS->close();
+ if (idVersion < iNeedsScan)
+ return idVersion;
+ else
+ return 0;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+void CMusicDatabase::SetMusicNeedsTagScan(int version)
+{
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET iNeedsScan=%i", version));
+}
+
+void CMusicDatabase::SetMusicTagScanVersion(int version /* = 0 */)
+{
+ if (version == 0)
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", GetSchemaVersion()));
+ else
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET idVersion=%i", version));
+}
+
+std::string CMusicDatabase::GetLibraryLastUpdated()
+{
+ return GetSingleValue("SELECT lastscanned FROM versiontagscan LIMIT 1");
+}
+
+void CMusicDatabase::SetLibraryLastUpdated()
+{
+ CDateTime dateUpdated = CDateTime::GetUTCDateTime();
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastscanned = '%s'",
+ dateUpdated.GetAsDBDateTime().c_str()));
+}
+
+std::string CMusicDatabase::GetLibraryLastCleaned()
+{
+ return GetSingleValue("SELECT lastcleaned FROM versiontagscan LIMIT 1");
+}
+
+void CMusicDatabase::SetLibraryLastCleaned()
+{
+ std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
+ m_pDS->exec(PrepareSQL("UPDATE versiontagscan SET lastcleaned = '%s'", strUpdated.c_str()));
+}
+
+std::string CMusicDatabase::GetArtistLinksUpdated()
+{
+ return GetSingleValue("SELECT artistlinksupdated FROM versiontagscan LIMIT 1");
+}
+
+void CMusicDatabase::SetArtistLinksUpdated()
+{
+ std::string strUpdated = CDateTime::GetUTCDateTime().GetAsDBDateTime();
+ m_pDS->exec(
+ PrepareSQL("UPDATE versiontagscan SET artistlinksupdated = '%s'", strUpdated.c_str()));
+}
+
+std::string CMusicDatabase::GetGenresLastAdded()
+{
+ return GetSingleValue("SELECT genresupdated FROM versiontagscan LIMIT 1");
+}
+
+std::string CMusicDatabase::GetSongsLastAdded()
+{
+ return GetSingleValue("SELECT MAX(dateNew) FROM song");
+}
+
+std::string CMusicDatabase::GetAlbumsLastAdded()
+{
+ return GetSingleValue("SELECT MAX(dateNew) FROM album");
+}
+
+std::string CMusicDatabase::GetArtistsLastAdded()
+{
+ return GetSingleValue("SELECT MAX(dateNew) FROM artist");
+}
+
+std::string CMusicDatabase::GetSongsLastModified()
+{
+ return GetSingleValue("SELECT MAX(dateModified) FROM song");
+}
+
+std::string CMusicDatabase::GetAlbumsLastModified()
+{
+ return GetSingleValue("SELECT MAX(dateModified) FROM album");
+}
+
+std::string CMusicDatabase::GetArtistsLastModified()
+{
+ return GetSingleValue("SELECT MAX(dateModified) FROM artist");
+}
+
+unsigned int CMusicDatabase::GetRandomSongIDs(const Filter& filter,
+ std::vector<std::pair<int, int>>& songIDs)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return 0;
+ if (nullptr == m_pDS)
+ return 0;
+
+ std::string strSQL = "SELECT idSong FROM songview ";
+ if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
+ return false;
+ strSQL += PrepareSQL(" ORDER BY RANDOM()");
+
+ if (!m_pDS->query(strSQL))
+ return 0;
+ songIDs.clear();
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return 0;
+ }
+ songIDs.reserve(m_pDS->num_rows());
+ while (!m_pDS->eof())
+ {
+ songIDs.push_back(std::make_pair<int, int>(1, m_pDS->fv(song_idSong).get_asInt()));
+ m_pDS->next();
+ } // cleanup
+ m_pDS->close();
+ return static_cast<unsigned int>(songIDs.size());
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
+ }
+ return 0;
+}
+
+int CMusicDatabase::GetSongsCount(const Filter& filter)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return 0;
+ if (nullptr == m_pDS)
+ return 0;
+
+ std::string strSQL = "select count(idSong) as NumSongs from songview ";
+ if (!CDatabase::BuildSQL(strSQL, filter, strSQL))
+ return false;
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return 0;
+ }
+
+ int iNumSongs = m_pDS->fv("NumSongs").get_asInt();
+ // cleanup
+ m_pDS->close();
+ return iNumSongs;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, filter.where);
+ }
+ return 0;
+}
+
+bool CMusicDatabase::GetAlbumPath(int idAlbum, std::string& basePath)
+{
+ basePath.clear();
+ std::vector<std::pair<std::string, int>> paths;
+ if (!GetAlbumPaths(idAlbum, paths))
+ return false;
+
+ for (const auto& pathpair : paths)
+ {
+ if (basePath.empty())
+ basePath = pathpair.first.c_str();
+ else
+ URIUtils::GetCommonPath(basePath, pathpair.first.c_str());
+ }
+ return true;
+}
+
+bool CMusicDatabase::GetAlbumPaths(int idAlbum, std::vector<std::pair<std::string, int>>& paths)
+{
+ paths.clear();
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false;
+
+ // Get the unique paths of songs on the album, providing there are no songs from
+ // other albums with the same path. This returns
+ // a) <album> if is contains all the songs and no others, or
+ // b) <album>/cd1, <album>/cd2 etc. for disc sets
+ // but does *not* return any path when albums are mixed together. That could be because of
+ // deliberate file organisation, or (more likely) because of a tagging error in album name
+ // or Musicbrainzalbumid. Thus it avoids finding some generic music path.
+ strSQL = PrepareSQL("SELECT DISTINCT strPath, song.idPath FROM song "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE song.idAlbum = %ld "
+ "AND (SELECT COUNT(DISTINCT(idAlbum)) FROM song AS song2 "
+ "WHERE idPath = song.idPath) = 1",
+ idAlbum);
+
+ if (!m_pDS2->query(strSQL))
+ return false;
+ if (m_pDS2->num_rows() == 0)
+ {
+ // Album does not have a unique path, files are mixed
+ m_pDS2->close();
+ return false;
+ }
+
+ while (!m_pDS2->eof())
+ {
+ paths.emplace_back(m_pDS2->fv("strPath").get_asString(),
+ m_pDS2->fv("song.idPath").get_asInt());
+ m_pDS2->next();
+ }
+ // Cleanup recordset data
+ m_pDS2->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
+ }
+
+ return false;
+}
+
+int CMusicDatabase::GetDiscnumberForPathID(int idPath)
+{
+ std::string strSQL;
+ int result = -1;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS2)
+ return -1;
+
+ strSQL = PrepareSQL("SELECT DISTINCT(song.iTrack >> 16) AS discnum FROM song "
+ "WHERE idPath = %i",
+ idPath);
+
+ if (!m_pDS2->query(strSQL))
+ return -1;
+ if (m_pDS2->num_rows() == 1)
+ { // Songs with this path have a unique disc number
+ result = m_pDS2->fv("discnum").get_asInt();
+ }
+ // Cleanup recordset data
+ m_pDS2->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
+ }
+ return result;
+}
+
+// Get old "artist path" - where artist.nfo and art was located v17 and below.
+// It is the path common to all albums by an (album) artist, but ensure it is unique
+// to that artist and not shared with other artists. Previously this caused incorrect nfo
+// and art to be applied to multiple artists.
+bool CMusicDatabase::GetOldArtistPath(int idArtist, std::string& basePath)
+{
+ basePath.clear();
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false;
+
+ // find all albums from this artist, and all the paths to the songs from those albums
+ std::string strSQL = PrepareSQL("SELECT strPath FROM album_artist "
+ "JOIN song ON album_artist.idAlbum = song.idAlbum "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE album_artist.idArtist = %ld "
+ "GROUP BY song.idPath",
+ idArtist);
+
+ // run query
+ if (!m_pDS2->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS2->num_rows();
+ if (iRowsFound == 0)
+ {
+ // Artist is not an album artist, no path to find
+ m_pDS2->close();
+ return false;
+ }
+ else if (iRowsFound == 1)
+ {
+ // Special case for single path - assume that we're in an artist/album/songs filesystem
+ URIUtils::GetParentPath(m_pDS2->fv("strPath").get_asString(), basePath);
+ m_pDS2->close();
+ }
+ else
+ {
+ // find the common path (if any) to these albums
+ while (!m_pDS2->eof())
+ {
+ std::string path = m_pDS2->fv("strPath").get_asString();
+ if (basePath.empty())
+ basePath = path;
+ else
+ URIUtils::GetCommonPath(basePath, path);
+
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ }
+
+ // Check any path found is unique to that album artist, and do *not* return any path
+ // that is shared with other album artists. That could be because of collaborations
+ // i.e. albums with more than one album artist, or because there are albums by the
+ // artist on multiple music sources, or elsewhere in the folder hierarchy.
+ // Avoid returning some generic music path.
+ if (!basePath.empty())
+ {
+ strSQL = PrepareSQL("SELECT COUNT(album_artist.idArtist) FROM album_artist "
+ "JOIN song ON album_artist.idAlbum = song.idAlbum "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE album_artist.idArtist <> %ld "
+ "AND strPath LIKE '%s%%'",
+ idArtist, basePath.c_str());
+ std::string strValue = GetSingleValue(strSQL, m_pDS2);
+ if (!strValue.empty())
+ {
+ int countartists = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
+ if (countartists == 0)
+ return true;
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ basePath.clear();
+ return false;
+}
+
+bool CMusicDatabase::GetArtistPath(const CArtist& artist, std::string& path)
+{
+ // Get path for artist in the artists folder
+ path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ if (path.empty())
+ return false; // No Artists folder not set;
+ // Get unique artist folder name
+ std::string strFolder;
+ if (GetArtistFolderName(artist, strFolder))
+ {
+ path = URIUtils::AddFileToFolder(path, strFolder);
+ return true;
+ }
+ path.clear();
+ return false;
+}
+
+bool CMusicDatabase::GetAlbumFolder(const CAlbum& album,
+ const std::string& strAlbumPath,
+ std::string& strFolder)
+{
+ strFolder.clear();
+ // Get a name for the album folder that is unique for the artist to use when
+ // exporting albums to separate nfo files in a folder under an artist folder
+
+ // When given an album path (common to all the music files containing *only*
+ // that album) check if that folder name is *unique* looking at folders on
+ // all levels of the music file paths for the artist
+ if (!strAlbumPath.empty())
+ {
+ // Get last folder from full path
+ std::vector<std::string> folders = URIUtils::SplitPath(strAlbumPath);
+ if (!folders.empty())
+ {
+ strFolder = folders.back();
+ // The same folder name could be used on different paths for albums by the
+ // same first artist. The albums could be totally different or also have
+ // the same name (but different mbid). Be over cautious and look for the
+ // name any where in the music file paths
+ std::string strSQL = PrepareSQL("SELECT DISTINCT album_artist.idAlbum FROM album_artist "
+ "JOIN song ON album_artist.idAlbum = song.idAlbum "
+ "JOIN path on path.idPath = song.idPath "
+ "WHERE album_artist.iOrder = 0 "
+ "AND album_artist.idArtist = %ld "
+ "AND path.strPath LIKE '%%\\%s\\%%'",
+ album.artistCredits[0].GetArtistId(), strFolder.c_str());
+
+ if (!m_pDS2->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS2->num_rows();
+ m_pDS2->close();
+ if (iRowsFound == 1)
+ return true;
+ }
+ }
+ // Create a valid unique folder name from album title
+ // @todo: Does UFT8 matter or need normalizing?
+ // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
+ strFolder = CUtil::MakeLegalFileName(album.strAlbum, LEGAL_WIN32_COMPAT);
+ StringUtils::Replace(strFolder, " _ ", "_");
+
+ // Check <first albumartist name>/<albumname> is unique e.g. 2 x Bruckner Symphony No. 3
+ // To have duplicate albumartist/album names at least one will have mbid, so append start of mbid to folder.
+ // This will not handle names that only differ by reserved chars e.g. "a>album" and "a?name"
+ // will be unique in db, but produce same folder name "a_name", but that kind of album and artist naming is very unlikely
+ std::string strSQL = PrepareSQL("SELECT COUNT(album_artist.idAlbum) FROM album_artist "
+ "JOIN album ON album_artist.idAlbum = album.idAlbum "
+ "WHERE album_artist.iOrder = 0 "
+ "AND album_artist.idArtist = %ld "
+ "AND album.strAlbum LIKE '%s' ",
+ album.artistCredits[0].GetArtistId(), album.strAlbum.c_str());
+ std::string strValue = GetSingleValue(strSQL, m_pDS2);
+ if (strValue.empty())
+ return false;
+ int countalbum = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
+ if (countalbum > 1 && !album.strMusicBrainzAlbumID.empty())
+ { // Only one of the duplicate albums can be without mbid
+ strFolder += "_" + album.strMusicBrainzAlbumID.substr(0, 4);
+ }
+ return !strFolder.empty();
+}
+
+bool CMusicDatabase::GetArtistFolderName(const CArtist& artist, std::string& strFolder)
+{
+ return GetArtistFolderName(artist.strArtist, artist.strMusicBrainzArtistID, strFolder);
+}
+
+bool CMusicDatabase::GetArtistFolderName(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ std::string& strFolder)
+{
+ // Create a valid unique folder name for artist
+ // @todo: Does UFT8 matter or need normalizing?
+ // @todo: Simplify punctuation removing unicode appostraphes, "..." etc.?
+ strFolder = CUtil::MakeLegalFileName(strArtist, LEGAL_WIN32_COMPAT);
+ StringUtils::Replace(strFolder, " _ ", "_");
+
+ // Ensure <artist name> is unique e.g. 2 x John Williams.
+ // To have duplicate artist names there must both have mbids, so append start of mbid to folder.
+ // This will not handle names that only differ by reserved chars e.g. "a>name" and "a?name"
+ // will be unique in db, but produce same folder name "a_name", but that kind of artist naming is very unlikely
+ std::string strSQL =
+ PrepareSQL("SELECT COUNT(1) FROM artist WHERE strArtist LIKE '%s'", strArtist.c_str());
+ std::string strValue = GetSingleValue(strSQL, m_pDS2);
+ if (strValue.empty())
+ return false;
+ int countartist = static_cast<int>(strtol(strValue.c_str(), NULL, 10));
+ if (countartist > 1)
+ strFolder += "_" + strMusicBrainzArtistID.substr(0, 4);
+ return !strFolder.empty();
+}
+
+int CMusicDatabase::AddSource(const std::string& strName,
+ const std::string& strMultipath,
+ const std::vector<std::string>& vecPaths,
+ int id /*= -1*/)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ // Check if source name already exists
+ int idSource = GetSourceByName(strName);
+ if (idSource < 0)
+ {
+ BeginTransaction();
+ // Add new source and source paths
+ if (id > 0)
+ strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
+ "VALUES(%i, '%s', '%s')",
+ id, strName.c_str(), strMultipath.c_str());
+ else
+ strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
+ "VALUES(NULL, '%s', '%s')",
+ strName.c_str(), strMultipath.c_str());
+ m_pDS->exec(strSQL);
+
+ idSource = static_cast<int>(m_pDS->lastinsertid());
+
+ int idPath = 1;
+ for (const auto& path : vecPaths)
+ {
+ strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) "
+ "VALUES(%i,%i,'%s')",
+ idSource, idPath, path.c_str());
+ m_pDS->exec(strSQL);
+ ++idPath;
+ }
+
+ // Find albums by song path, building WHERE for multiple source paths
+ // (providing source has a path)
+ if (vecPaths.size() > 0)
+ {
+ std::vector<int> albumIds;
+ Filter extFilter;
+ strSQL = "SELECT DISTINCT idAlbum FROM song ";
+ extFilter.AppendJoin("JOIN path ON song.idPath = path.idPath");
+ for (const auto& path : vecPaths)
+ extFilter.AppendWhere(PrepareSQL("path.strPath LIKE '%s%%%%'", path.c_str()), false);
+ if (!BuildSQL(strSQL, extFilter, strSQL))
+ return -1;
+
+ if (!m_pDS->query(strSQL))
+ return -1;
+
+ while (!m_pDS->eof())
+ {
+ albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ // Add album_source for related albums
+ for (auto idAlbum : albumIds)
+ {
+ strSQL = PrepareSQL("INSERT INTO album_source (idSource, idAlbum) "
+ "VALUES('%i', '%i')",
+ idSource, idAlbum);
+ m_pDS->exec(strSQL);
+ }
+ }
+ CommitTransaction();
+ }
+ return idSource;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
+ RollbackTransaction();
+ }
+
+ return -1;
+}
+
+int CMusicDatabase::UpdateSource(const std::string& strOldName,
+ const std::string& strName,
+ const std::string& strMultipath,
+ const std::vector<std::string>& vecPaths)
+{
+ int idSource = -1;
+ std::string strSourceMultipath;
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ // Get details of named old source
+ if (!strOldName.empty())
+ {
+ strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source WHERE strName LIKE '%s'",
+ strOldName.c_str());
+ if (!m_pDS->query(strSQL))
+ return -1;
+ if (m_pDS->num_rows() > 0)
+ {
+ idSource = m_pDS->fv("idSource").get_asInt();
+ strSourceMultipath = m_pDS->fv("strMultipath").get_asString();
+ }
+ m_pDS->close();
+ }
+ if (idSource < 0)
+ {
+ // Source not found, add new one
+ return AddSource(strName, strMultipath, vecPaths);
+ }
+
+ // Nothing changed? (that we hold in db, other source details could be modified)
+ bool pathschanged = strMultipath.compare(strSourceMultipath) != 0;
+ if (!pathschanged && strOldName.compare(strName) == 0)
+ return idSource;
+
+ if (!pathschanged)
+ {
+ // Name changed? Could be that none of the values held in db changed
+ if (strOldName.compare(strName) != 0)
+ {
+ strSQL = PrepareSQL("UPDATE source SET strName = '%s' "
+ "WHERE idSource = %i",
+ strName.c_str(), idSource);
+ m_pDS->exec(strSQL);
+ }
+ return idSource;
+ }
+ else
+ {
+ // Change paths (and name) by deleting and re-adding, but keep same ID
+ strSQL = PrepareSQL("DELETE FROM source WHERE idSource = %i", idSource);
+ m_pDS->exec(strSQL);
+ return AddSource(strName, strMultipath, vecPaths, idSource);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed with query ({})", __FUNCTION__, strSQL);
+ RollbackTransaction();
+ }
+
+ return -1;
+}
+
+bool CMusicDatabase::RemoveSource(const std::string& strName)
+{
+ // Related album_source and source_path rows removed by trigger
+ SetLibraryLastCleaned();
+ return ExecuteQuery(PrepareSQL("DELETE FROM source WHERE strName ='%s'", strName.c_str()));
+}
+
+int CMusicDatabase::GetSourceFromPath(const std::string& strPath1)
+{
+ std::string strSQL;
+ int idSource = -1;
+ try
+ {
+ std::string strPath(strPath1);
+ if (!URIUtils::HasSlashAtEnd(strPath))
+ URIUtils::AddSlashAtEnd(strPath);
+
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ // Check if path is a source matching on multipath
+ strSQL = PrepareSQL("SELECT idSource FROM source WHERE strMultipath = '%s'", strPath.c_str());
+ if (!m_pDS->query(strSQL))
+ return -1;
+ if (m_pDS->num_rows() > 0)
+ idSource = m_pDS->fv("idSource").get_asInt();
+ m_pDS->close();
+ if (idSource > 0)
+ return idSource;
+
+ // Check if path is a source path (of many) or a subfolder of a single source
+ strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
+ "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
+ strPath.c_str());
+ if (!m_pDS->query(strSQL))
+ return -1;
+ if (m_pDS->num_rows() == 1)
+ idSource = m_pDS->fv("idSource").get_asInt();
+ m_pDS->close();
+ return idSource;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} path: {} ({}) failed", __FUNCTION__, strSQL, strPath1);
+ }
+
+ return -1;
+}
+
+bool CMusicDatabase::AddAlbumSource(int idAlbum, int idSource)
+{
+ std::string strSQL;
+ strSQL = PrepareSQL("INSERT INTO album_source (idAlbum, idSource) "
+ "VALUES(%i, %i)",
+ idAlbum, idSource);
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::AddAlbumSources(int idAlbum, const std::string& strPath)
+{
+ std::string strSQL;
+ std::vector<int> sourceIds;
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (!strPath.empty())
+ {
+ // Find sources related to album using album path
+ strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
+ "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
+ strPath.c_str());
+ if (!m_pDS->query(strSQL))
+ return false;
+ while (!m_pDS->eof())
+ {
+ sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ else
+ {
+ // Find sources using song paths, check each source path individually
+ if (nullptr == m_pDS2)
+ return false;
+ strSQL = "SELECT idSource, strPath FROM source_path";
+ if (!m_pDS->query(strSQL))
+ return false;
+ while (!m_pDS->eof())
+ {
+ std::string sourcepath = m_pDS->fv("strPath").get_asString();
+ strSQL = PrepareSQL("SELECT 1 FROM song "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'",
+ sourcepath.c_str());
+ if (!m_pDS2->query(strSQL))
+ return false;
+ if (m_pDS2->num_rows() > 0)
+ sourceIds.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS2->close();
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+
+ //Add album sources
+ for (auto idSource : sourceIds)
+ {
+ AddAlbumSource(idAlbum, idSource);
+ }
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} path: {} ({}) failed", __FUNCTION__, strSQL, strPath);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::DeleteAlbumSources(int idAlbum)
+{
+ return ExecuteQuery(PrepareSQL("DELETE FROM album_source WHERE idAlbum = %i", idAlbum));
+}
+
+bool CMusicDatabase::CheckSources(VECSOURCES& sources)
+{
+ if (sources.empty())
+ {
+ // Source table empty too?
+ return GetSingleValue("SELECT 1 FROM source LIMIT 1").empty();
+ }
+
+ // Check number of entries matches
+ size_t total = static_cast<size_t>(GetSingleValueInt("SELECT COUNT(1) FROM source"));
+ if (total != sources.size())
+ return false;
+
+ // Check individual sources match
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ for (const auto& source : sources)
+ {
+ // Check each source by name
+ strSQL = PrepareSQL("SELECT idSource, strMultipath FROM source "
+ "WHERE strName LIKE '%s'",
+ source.strName.c_str());
+ m_pDS->query(strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() != 1)
+ {
+ // Missing source, or name duplication
+ m_pDS->close();
+ return false;
+ }
+ else
+ {
+ // Check details. Encoded URLs of source.strPath matched to strMultipath
+ // field, no need to look at individual paths of source_path table
+ if (source.strPath.compare(m_pDS->fv("strMultipath").get_asString()) != 0)
+ {
+ // Paths not match
+ m_pDS->close();
+ return false;
+ }
+ m_pDS->close();
+ }
+ }
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::MigrateSources()
+{
+ //Fetch music sources from xml
+ VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
+
+ std::string strSQL;
+ try
+ {
+ // Fill source and source paths tables
+ for (const auto& source : sources)
+ {
+ // AddSource(source.strName, source.strPath, source.vecPaths);
+ // Add new source
+ strSQL = PrepareSQL("INSERT INTO source (idSource, strName, strMultipath) "
+ "VALUES(NULL, '%s', '%s')",
+ source.strName.c_str(), source.strPath.c_str());
+ m_pDS->exec(strSQL);
+ int idSource = static_cast<int>(m_pDS->lastinsertid());
+
+ // Add new source paths
+ int idPath = 1;
+ for (const auto& path : source.vecPaths)
+ {
+ strSQL = PrepareSQL("INSERT INTO source_path (idSource, idPath, strPath) "
+ "VALUES(%i,%i,'%s')",
+ idSource, idPath, path.c_str());
+ m_pDS->exec(strSQL);
+ ++idPath;
+ }
+ }
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+ return false;
+}
+
+bool CMusicDatabase::UpdateSources()
+{
+ //Check library and xml sources match
+ VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
+ if (CheckSources(sources))
+ return true;
+
+ try
+ {
+ // Empty sources table (related link tables removed by trigger);
+ ExecuteQuery("DELETE FROM source");
+
+ // Fill source table, and album sources
+ for (const auto& source : sources)
+ AddSource(source.strName, source.strPath, source.vecPaths);
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSources(CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Get music sources and individual source paths (may not be scanned or have albums etc.)
+ std::string strSQL =
+ "SELECT source.idSource, source.strName, source.strMultipath, source_path.strPath "
+ "FROM source JOIN source_path ON source.idSource = source_path.idSource "
+ "ORDER BY source.idSource, source_path.idPath";
+
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ // Get data from returned rows
+ // Item has source ID in MusicInfotag, multipath in path, and individual paths in property
+ CVariant sourcePaths(CVariant::VariantTypeArray);
+ int idSource = -1;
+ while (!m_pDS->eof())
+ {
+ if (idSource != m_pDS->fv("source.idSource").get_asInt())
+ { // New source
+ if (idSource > 0 && !sourcePaths.empty())
+ {
+ //Store paths for previous source in item list
+ items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
+ sourcePaths.clear();
+ }
+ idSource = m_pDS->fv("source.idSource").get_asInt();
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv("source.strName").get_asString()));
+ pItem->GetMusicInfoTag()->SetDatabaseId(idSource, "source");
+ // Set tag URL for "file" property in AudioLibary processing
+ pItem->GetMusicInfoTag()->SetURL(m_pDS->fv("source.strMultipath").get_asString());
+ // Set item path as source URL encoded multipath too
+ pItem->SetPath(m_pDS->fv("source.strMultiPath").get_asString());
+
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+ }
+ // Get path data
+ sourcePaths.push_back(m_pDS->fv("source_path.strPath").get_asString());
+
+ m_pDS->next();
+ }
+ if (!sourcePaths.empty())
+ {
+ //Store paths for final source
+ items[items.Size() - 1].get()->SetProperty("paths", sourcePaths);
+ sourcePaths.clear();
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSourcesByArtist(int idArtist, CFileItem* item)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource FROM artist "
+ "JOIN album_artist ON album_artist.idArtist = artist.idArtist "
+ "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum "
+ "WHERE artist.idArtist = %i "
+ "ORDER BY album_source.idSource",
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ // Artist does have any source via albums may not be an album artist.
+ // Check via songs fetch sources from compilations or where they are guest artist
+ m_pDS->close();
+ strSQL = PrepareSQL("SELECT DISTINCT album_source.idSource, FROM song_artist "
+ "JOIN song ON song_artist.idSong = song.idSong "
+ "JOIN album_source ON album_source.idAlbum = song.idAlbum "
+ "WHERE song_artist.idArtist = %i AND song_artist.idRole = 1 "
+ "ORDER BY album_source.idSource",
+ idArtist);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0)
+ {
+ //No sources, but query successful
+ m_pDS->close();
+ return true;
+ }
+ }
+
+ CVariant artistSources(CVariant::VariantTypeArray);
+ while (!m_pDS->eof())
+ {
+ artistSources.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ item->SetProperty("sourceid", artistSources);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idArtist);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSourcesByAlbum(int idAlbum, CFileItem* item)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idSource FROM album_source "
+ "WHERE album_source.idAlbum = %i "
+ "ORDER BY idSource",
+ idAlbum);
+ if (!m_pDS->query(strSQL))
+ return false;
+ CVariant albumSources(CVariant::VariantTypeArray);
+ if (m_pDS->num_rows() > 0)
+ {
+ while (!m_pDS->eof())
+ {
+ albumSources.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ else
+ {
+ //! @todo: handle singles, or don't waste time checking songs
+ // Album does have any sources, may be a single??
+ // Check via song paths, check each source path individually
+ // usually fewer source paths than songs
+ m_pDS->close();
+
+ if (nullptr == m_pDS2)
+ return false;
+ strSQL = "SELECT idSource, strPath FROM source_path";
+ if (!m_pDS->query(strSQL))
+ return false;
+ while (!m_pDS->eof())
+ {
+ std::string sourcepath = m_pDS->fv("strPath").get_asString();
+ strSQL = PrepareSQL("SELECT 1 FROM song "
+ "JOIN path ON song.idPath = path.idPath "
+ "WHERE song.idAlbum = %i AND path.strPath LIKE '%s%%%%'",
+ idAlbum, sourcepath.c_str());
+ if (!m_pDS2->query(strSQL))
+ return false;
+ if (m_pDS2->num_rows() > 0)
+ albumSources.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS2->close();
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+
+
+ item->SetProperty("sourceid", albumSources);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idAlbum);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetSourcesBySong(int idSong, const std::string& strPath1, CFileItem* item)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idSource FROM song "
+ "JOIN album_source ON album_source.idAlbum = song.idAlbum "
+ "WHERE song.idSong = %i "
+ "ORDER BY idSource",
+ idSong);
+ if (!m_pDS->query(strSQL))
+ return false;
+ if (m_pDS->num_rows() == 0 && !strPath1.empty())
+ {
+ // Check via song path instead
+ m_pDS->close();
+ std::string strPath(strPath1);
+ if (!URIUtils::HasSlashAtEnd(strPath))
+ URIUtils::AddSlashAtEnd(strPath);
+
+ strSQL = PrepareSQL("SELECT DISTINCT idSource FROM source_path "
+ "WHERE SUBSTR('%s', 1, LENGTH(strPath)) = strPath",
+ strPath.c_str());
+ if (!m_pDS->query(strSQL))
+ return false;
+ }
+ CVariant songSources(CVariant::VariantTypeArray);
+ while (!m_pDS->eof())
+ {
+ songSources.push_back(m_pDS->fv("idSource").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ item->SetProperty("sourceid", songSources);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, idSong);
+ }
+ return false;
+}
+
+int CMusicDatabase::GetSourceByName(const std::string& strSource)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idSource FROM source WHERE strName LIKE '%s'", strSource.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ return m_pDS->fv("idSource").get_asInt();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+std::string CMusicDatabase::GetSourceById(int id)
+{
+ return GetSingleValue("source", "strName", PrepareSQL("idSource = %i", id));
+}
+
+int CMusicDatabase::GetArtistByName(const std::string& strArtist)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL = PrepareSQL("SELECT idArtist FROM artist WHERE artist.strArtist LIKE '%s'",
+ strArtist.c_str());
+
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ int lResult = m_pDS->fv("artist.idArtist").get_asInt();
+ m_pDS->close();
+ return lResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+int CMusicDatabase::GetArtistByMatch(const CArtist& artist)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB || nullptr == m_pDS)
+ return false;
+ // Match on MusicBrainz ID, definitively unique
+ if (!artist.strMusicBrainzArtistID.empty())
+ strSQL = PrepareSQL("SELECT idArtist FROM artist "
+ "WHERE strMusicBrainzArtistID = '%s'",
+ artist.strMusicBrainzArtistID.c_str());
+ else
+ // No MusicBrainz ID, artist by name with no mbid
+ strSQL = PrepareSQL("SELECT idArtist FROM artist "
+ "WHERE strArtist LIKE '%s' AND strMusicBrainzArtistID IS NULL",
+ artist.strArtist.c_str());
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ // Match on artist name, relax mbid restriction
+ return GetArtistByName(artist.strArtist);
+ }
+ int lResult = m_pDS->fv("idArtist").get_asInt();
+ m_pDS->close();
+ return lResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::GetArtistFromSong(int idSong, CArtist& artist)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL = PrepareSQL(
+ "SELECT artistview.* FROM song_artist "
+ "JOIN artistview ON song_artist.idArtist = artistview.idArtist "
+ "WHERE song_artist.idSong= %i AND song_artist.idRole = 1 AND song_artist.iOrder = 0",
+ idSong);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ artist = GetArtistFromDataset(m_pDS.get());
+
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::IsSongArtist(int idSong, int idArtist)
+{
+ std::string strSQL = PrepareSQL("SELECT 1 FROM song_artist "
+ "WHERE song_artist.idSong= %i AND "
+ "song_artist.idArtist = %i AND song_artist.idRole = 1",
+ idSong, idArtist);
+ return GetSingleValue(strSQL).empty();
+}
+
+bool CMusicDatabase::IsSongAlbumArtist(int idSong, int idArtist)
+{
+ std::string strSQL =
+ PrepareSQL("SELECT 1 FROM song JOIN album_artist ON song.idAlbum = album_artist.idAlbum "
+ "WHERE song.idSong = %i AND album_artist.idArtist = %i",
+ idSong, idArtist);
+ return GetSingleValue(strSQL).empty();
+}
+
+bool CMusicDatabase::IsAlbumBoxset(int idAlbum)
+{
+ std::string strSQL = PrepareSQL("SELECT bBoxedSet FROM album WHERE idAlbum = %i", idAlbum);
+ int isBoxSet = GetSingleValueInt(strSQL);
+ return (isBoxSet == 1 ? true : false);
+}
+
+int CMusicDatabase::GetAlbumByName(const std::string& strAlbum, const std::string& strArtist)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ if (strArtist.empty())
+ strSQL =
+ PrepareSQL("SELECT idAlbum FROM album WHERE album.strAlbum LIKE '%s'", strAlbum.c_str());
+ else
+ strSQL = PrepareSQL("SELECT idAlbum FROM album "
+ "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'",
+ strAlbum.c_str(), strArtist.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ return m_pDS->fv("idAlbum").get_asInt();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::GetMatchingMusicVideoAlbum(const std::string& strAlbum,
+ const std::string& strArtist,
+ int& idAlbum,
+ std::string& strReview)
+{
+ /*
+ Get the first album that matches with the title and artist display name.
+ Artist(s) and album title may not be sufficient to uniquely identify a match since library can
+ store multiple releases, and occasionally artists even have different albums with same name.
+ Taking the first album that matches and ignoring re-releases etc. is acceptable for musicvideo
+ */
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ if (strArtist.empty())
+ strSQL = PrepareSQL("SELECT idAlbum, strReview FROM album WHERE album.strAlbum LIKE '%s'",
+ strAlbum.c_str());
+ else
+ strSQL = PrepareSQL("SELECT idAlbum, strReview FROM album "
+ "WHERE album.strAlbum LIKE '%s' AND album.strArtistDisp LIKE '%s'",
+ strAlbum.c_str(), strArtist.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound > 0)
+ {
+ idAlbum = m_pDS->fv("idAlbum").get_asInt();
+ strReview = m_pDS->fv("strReview").get_asString();
+ return true;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SearchAlbumsByArtistName(const std::string& strArtist, CFileItemList& items)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT albumview.* FROM albumview "
+ "JOIN album_artist ON album_artist.idAlbum = albumview.idAlbum "
+ "WHERE album_artist.strArtist LIKE '%s'",
+ strArtist.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+
+ while (!m_pDS->eof())
+ {
+ CAlbum album = GetAlbumFromDataset(m_pDS.get());
+ std::string path = StringUtils::Format("musicdb://albums/{}/", album.idAlbum);
+ CFileItemPtr pItem(new CFileItem(path, album));
+ std::string label =
+ StringUtils::Format("{} ({})", album.strAlbum, pItem->GetMusicInfoTag()->GetYear());
+ pItem->SetLabel(label);
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close(); // cleanup recordset data
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+int CMusicDatabase::GetAlbumByName(const std::string& strAlbum,
+ const std::vector<std::string>& artist)
+{
+ return GetAlbumByName(
+ strAlbum,
+ StringUtils::Join(
+ artist,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+int CMusicDatabase::GetAlbumByMatch(const CAlbum& album)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB || nullptr == m_pDS)
+ return false;
+ // Match on MusicBrainz ID, definitively unique
+ if (!album.strMusicBrainzAlbumID.empty())
+ strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = '%s'",
+ album.strMusicBrainzAlbumID.c_str());
+ else
+ // No mbid, match on album title and album artist descriptive string, ignore those with mbid
+ strSQL = PrepareSQL("SELECT idAlbum FROM album "
+ "WHERE strArtistDisp LIKE '%s' AND strAlbum LIKE '%s' "
+ "AND strMusicBrainzAlbumID IS NULL",
+ album.GetAlbumArtistString().c_str(), album.strAlbum.c_str());
+ m_pDS->query(strSQL);
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ // Match on album title and album artist descriptive string, relax mbid restriction
+ return GetAlbumByName(album.strAlbum, album.GetAlbumArtistString());
+ }
+ int lResult = m_pDS->fv("idAlbum").get_asInt();
+ m_pDS->close();
+ return lResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{} - failed to execute {}", __FUNCTION__, strSQL);
+ }
+ return -1;
+}
+
+std::string CMusicDatabase::GetGenreById(int id)
+{
+ return GetSingleValue("genre", "strGenre", PrepareSQL("idGenre=%i", id));
+}
+
+std::string CMusicDatabase::GetArtistById(int id)
+{
+ return GetSingleValue("artist", "strArtist", PrepareSQL("idArtist=%i", id));
+}
+
+std::string CMusicDatabase::GetRoleById(int id)
+{
+ return GetSingleValue("role", "strRole", PrepareSQL("idRole=%i", id));
+}
+
+bool CMusicDatabase::UpdateArtistSortNames(int idArtist /*=-1*/)
+{
+ // Propagate artist sort names into concatenated artist sort name string for songs and albums
+ // Avoid updating records where sort same as strArtistDisp
+ std::string strSQL;
+
+ // MySQL syntax for GROUP_CONCAT with order is different from that in SQLite
+ // (not handled by PrepareSQL)
+ bool bisMySQL = StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql");
+
+ BeginMultipleExecute();
+ if (bisMySQL)
+ strSQL = "(SELECT GROUP_CONCAT("
+ "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
+ "ELSE artist.strSortName END "
+ "ORDER BY album_artist.idAlbum, album_artist.iOrder "
+ "SEPARATOR '; ') as val "
+ "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
+ "WHERE album_artist.idAlbum = album.idAlbum GROUP BY idAlbum) ";
+ else
+ strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
+ "FROM(SELECT album_artist.idAlbum, "
+ "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
+ "ELSE artist.strSortName END as val "
+ "FROM album_artist JOIN artist on artist.idArtist = album_artist.idArtist "
+ "WHERE album_artist.idAlbum = album.idAlbum "
+ "ORDER BY album_artist.idAlbum, album_artist.iOrder) GROUP BY idAlbum) ";
+
+ strSQL = "UPDATE album SET strArtistSort = " + strSQL +
+ "WHERE (album.strArtistSort = '' OR album.strArtistSort IS NULL) "
+ "AND strArtistDisp <> " +
+ strSQL;
+ if (idArtist > 0)
+ strSQL +=
+ PrepareSQL(" AND EXISTS (SELECT 1 FROM album_artist WHERE album_artist.idArtist = %ld "
+ "AND album_artist.idAlbum = album.idAlbum)",
+ idArtist);
+ ExecuteQuery(strSQL);
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (bisMySQL)
+ strSQL = "(SELECT GROUP_CONCAT("
+ "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
+ "ELSE artist.strSortName END "
+ "ORDER BY song_artist.idSong, song_artist.iOrder "
+ "SEPARATOR '; ') as val "
+ "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
+ "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 GROUP BY idSong) ";
+ else
+ strSQL = "(SELECT GROUP_CONCAT(val, '; ') "
+ "FROM(SELECT song_artist.idSong, "
+ "CASE WHEN artist.strSortName IS NULL THEN artist.strArtist "
+ "ELSE artist.strSortName END as val "
+ "FROM song_artist JOIN artist on artist.idArtist = song_artist.idArtist "
+ "WHERE song_artist.idSong = song.idSong AND song_artist.idRole = 1 "
+ "ORDER BY song_artist.idSong, song_artist.iOrder) GROUP BY idSong) ";
+
+ strSQL = "UPDATE song SET strArtistSort = " + strSQL +
+ "WHERE (song.strArtistSort = '' OR song.strArtistSort IS NULL) "
+ "AND strArtistDisp <> " +
+ strSQL;
+ if (idArtist > 0)
+ strSQL += PrepareSQL(" AND EXISTS (SELECT 1 FROM song_artist WHERE song_artist.idArtist = %ld "
+ "AND song_artist.idSong = song.idSong AND song_artist.idRole = 1)",
+ idArtist);
+ ExecuteQuery(strSQL);
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (CommitMultipleExecute())
+ return true;
+ else
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ return false;
+}
+
+std::string CMusicDatabase::GetAlbumById(int id)
+{
+ return GetSingleValue("album", "strAlbum", PrepareSQL("idAlbum=%i", id));
+}
+
+int CMusicDatabase::GetGenreByName(const std::string& strGenre)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = PrepareSQL("SELECT idGenre FROM genre "
+ "WHERE genre.strGenre LIKE '%s'",
+ strGenre.c_str());
+ // run query
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound != 1)
+ {
+ m_pDS->close();
+ return -1;
+ }
+ return m_pDS->fv("genre.idGenre").get_asInt();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::GetGenresJSON(CFileItemList& items, bool bSources)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ strSQL = "SELECT %s FROM genre ";
+ Filter extFilter;
+ extFilter.AppendField("genre.idGenre");
+ extFilter.AppendField("genre.strGenre");
+ if (bSources)
+ {
+ strSQL = "SELECT DISTINCT %s FROM genre ";
+ extFilter.AppendField("album_source.idSource");
+ extFilter.AppendJoin("JOIN song_genre ON song_genre.idGenre = genre.idGenre");
+ extFilter.AppendJoin("JOIN song ON song.idSong = song_genre.idSong");
+ extFilter.AppendJoin("JOIN album ON album.idAlbum = song.idAlbum");
+ extFilter.AppendJoin("LEFT JOIN album_source on album_source.idAlbum = album.idAlbum");
+ extFilter.AppendOrder("genre.strGenre");
+ extFilter.AppendOrder("album_source.idSource");
+ }
+ extFilter.AppendWhere("genre.strGenre != ''");
+
+ std::string strSQLExtra;
+ if (!BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ strSQL = PrepareSQL(strSQL, extFilter.fields.c_str()) + strSQLExtra;
+
+ // run query
+ CLog::Log(LOGDEBUG, "{} query: {}", __FUNCTION__, strSQL);
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ if (!bSources)
+ items.Reserve(iRowsFound);
+
+ // Get data from returned rows
+ // Item has genre name and ID in MusicInfotag, VFS path, and sources in property
+ CVariant genreSources(CVariant::VariantTypeArray);
+ int idGenre = -1;
+ while (!m_pDS->eof())
+ {
+ if (idGenre != m_pDS->fv("genre.idGenre").get_asInt())
+ { // New genre
+ if (idGenre > 0 && bSources)
+ {
+ //Store sources for previous genre in item list
+ items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
+ genreSources.clear();
+ }
+ idGenre = m_pDS->fv("genre.idGenre").get_asInt();
+ std::string strGenre = m_pDS->fv("genre.strGenre").get_asString();
+ CFileItemPtr pItem(new CFileItem(strGenre));
+ pItem->GetMusicInfoTag()->SetTitle(strGenre);
+ pItem->GetMusicInfoTag()->SetGenre(strGenre);
+ pItem->GetMusicInfoTag()->SetDatabaseId(idGenre, "genre");
+ pItem->SetPath(StringUtils::Format("musicdb://genres/{}/", idGenre));
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+ }
+ // Get source data
+ if (bSources)
+ {
+ int sourceid = m_pDS->fv("album_source.idSource").get_asInt();
+ if (sourceid > 0)
+ genreSources.push_back(sourceid);
+ }
+ m_pDS->next();
+ }
+ if (bSources)
+ {
+ //Store sources for final genre
+ items[items.Size() - 1].get()->SetProperty("sourceid", genreSources);
+ }
+
+ // cleanup
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+ return false;
+}
+
+std::string CMusicDatabase::GetAlbumDiscTitle(int idAlbum, int idDisc)
+{
+ // Get disc node title from ids allowing for "*all"
+ std::string disctitle;
+ std::string albumtitle;
+ if (idAlbum > 0)
+ albumtitle = GetAlbumById(idAlbum);
+ if (idDisc > 0)
+ {
+ disctitle = GetSingleValue("song", "strDiscSubtitle",
+ PrepareSQL("idAlbum = %i AND iTrack >> 16 = %i", idAlbum, idDisc));
+ if (disctitle.empty())
+ disctitle = StringUtils::Format("{} {}", g_localizeStrings.Get(427), idDisc); // "Disc 1" etc.
+ if (albumtitle.empty())
+ albumtitle = disctitle;
+ else
+ albumtitle = albumtitle + " - " + disctitle;
+ }
+ return albumtitle;
+}
+
+int CMusicDatabase::GetBoxsetsCount()
+{
+ return GetSingleValueInt("album", "count(idAlbum)", "bBoxedSet = 1");
+}
+
+int CMusicDatabase::GetAlbumDiscsCount(int idAlbum)
+{
+ std::string strSQL = PrepareSQL("SELECT iDiscTotal FROM album WHERE album.idAlbum = %i", idAlbum);
+ return GetSingleValueInt(strSQL);
+}
+
+int CMusicDatabase::GetCompilationAlbumsCount()
+{
+ return GetSingleValueInt("album", "count(idAlbum)", "bCompilation = 1");
+}
+
+int CMusicDatabase::GetSinglesCount()
+{
+ CDatabase::Filter filter(
+ PrepareSQL("songview.idAlbum IN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
+ CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
+ return GetSongsCount(filter);
+}
+
+int CMusicDatabase::GetArtistCountForRole(int role)
+{
+ std::string strSQL = PrepareSQL(
+ "SELECT COUNT(DISTINCT idartist) FROM song_artist WHERE song_artist.idRole = %i", role);
+ return GetSingleValueInt(strSQL);
+}
+
+int CMusicDatabase::GetArtistCountForRole(const std::string& strRole)
+{
+ std::string strSQL = PrepareSQL("SELECT COUNT(DISTINCT idartist) FROM song_artist "
+ "JOIN role ON song_artist.idRole = role.idRole "
+ "WHERE role.strRole LIKE '%s'",
+ strRole.c_str());
+ return GetSingleValueInt(strSQL);
+}
+
+bool CMusicDatabase::SetPathHash(const std::string& path, const std::string& hash)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (hash.empty())
+ { // this is an empty folder - we need only add it to the path table
+ // if the path actually exists
+ if (!CDirectory::Exists(path))
+ return false;
+ }
+ int idPath = AddPath(path);
+ if (idPath < 0)
+ return false;
+
+ std::string strSQL =
+ PrepareSQL("UPDATE path SET strHash='%s' WHERE idPath=%ld", hash.c_str(), idPath);
+ m_pDS->exec(strSQL);
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}, {}) failed", __FUNCTION__, path, hash);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::GetPathHash(const std::string& path, std::string& hash)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL = PrepareSQL("select strHash from path where strPath='%s'", path.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ return false;
+ hash = m_pDS->fv("strHash").get_asString();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path);
+ }
+
+ return false;
+}
+
+bool CMusicDatabase::RemoveSongsFromPath(const std::string& path1, MAPSONGS& songmap, bool exact)
+{
+ // We need to remove all songs from this path, as their tags are going
+ // to be re-read. We need to remove all songs from the song table + all links to them
+ // from the song link tables (as otherwise if a song is added back
+ // to the table with the same idSong, these tables can't be cleaned up properly later)
+
+ //! @todo SQLite probably doesn't allow this, but can we rely on that??
+
+ // We don't need to remove orphaned albums at this point as in AddAlbum() we check
+ // first whether the album has already been read during this scan, and if it hasn't
+ // we check whether it's in the table and update accordingly at that point, removing the entries from
+ // the album link tables. The only failure point for this is albums
+ // that span multiple folders, where just the files in one folder have been changed. In this case
+ // any linked fields that are only in the files that haven't changed will be removed. Clearly
+ // the primary albumartist still matches (as that's what we looked up based on) so is this really
+ // an issue? I don't think it is, as those artists will still have links to the album via the songs
+ // which is generally what we rely on, so the only failure point is albumartist lookup. In this
+ // case, it will return only things in the album_artist table from the newly updated songs (and
+ // only if they have additional artists). I think the effect of this is minimal at best, as ALL
+ // songs in the album should have the same albumartist!
+
+ // we also remove the path at this point as it will be added later on if the
+ // path still exists.
+ // After scanning we then remove the orphaned artists, genres and thumbs.
+
+ // Note: when used to remove all songs from a path and its subpath (exact=false), this
+ // does miss archived songs.
+ std::string path(path1);
+ SetLibraryLastUpdated();
+ try
+ {
+ if (!URIUtils::HasSlashAtEnd(path))
+ URIUtils::AddSlashAtEnd(path);
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // Filename is not unique for a path as songs from a cuesheet have same filename.
+ // Songs from cuesheets often have consecutive ID but not always e.g. more than one cuesheet
+ // in a folder and some edited and rescanned.
+ // Hence order by filename so these songs can be gathered together.
+ std::string where;
+ if (exact)
+ where = PrepareSQL(" WHERE strPath='%s'", path.c_str());
+ else
+ where = PrepareSQL(" WHERE SUBSTR(strPath,1,%i)='%s'", StringUtils::utf8_strlen(path.c_str()),
+ path.c_str());
+ std::string sql = "SELECT * FROM songview" + where + " ORDER BY strFileName";
+ if (!m_pDS->query(sql))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound > 0)
+ {
+ // Each file is potentially mapped to a list of songs, gather these and save as list
+ VECSONGS songs;
+ std::string filename;
+ std::vector<std::string> songIds;
+ while (!m_pDS->eof())
+ {
+ CSong song = GetSongFromDataset();
+ if (!filename.empty() && filename != song.strFileName)
+ {
+ // Save songs for previous filename
+ songmap.insert(std::make_pair(filename, songs));
+ songs.clear();
+ }
+ song.strThumb = GetArtForItem(song.idSong, MediaTypeSong, "thumb");
+ songs.emplace_back(song);
+ songIds.push_back(PrepareSQL("%i", song.idSong));
+ filename = song.strFileName;
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ songmap.insert(std::make_pair(filename, songs)); // Save songs for last filename
+
+ //! @todo move this below the m_pDS->exec block, once UPnP doesn't rely on this anymore
+ for (const auto& id : songIds)
+ AnnounceRemove(MediaTypeSong, atoi(id.c_str()));
+
+ // Delete all songs, and anything linked to them via triggers
+ std::string strIDs = StringUtils::Join(songIds, ",");
+ sql = "DELETE FROM song WHERE idSong in (" + strIDs + ")";
+ m_pDS->exec(sql);
+ }
+ // and remove the path as well (it'll be re-added later on with the new hash if it's non-empty)
+ sql = "delete from path" + where;
+ m_pDS->exec(sql);
+ return iRowsFound > 0;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path);
+ }
+ return false;
+}
+
+void CMusicDatabase::CheckArtistLinksChanged()
+{
+ std::string strSQL = "SELECT COUNT(1) FROM removed_link ";
+ int iLinks = GetSingleValueInt(strSQL, m_pDS);
+ if (iLinks > 0)
+ {
+ SetArtistLinksUpdated(); // Store datetime artist links last updated
+ DeleteRemovedLinks(); // Clean-up artist links
+ }
+}
+
+bool CMusicDatabase::GetPaths(std::set<std::string>& paths)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ paths.clear();
+
+ // find all paths
+ if (!m_pDS->query("SELECT strPath FROM path"))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+ while (!m_pDS->eof())
+ {
+ paths.insert(m_pDS->fv("strPath").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetSongUserrating(const std::string& filePath, int userrating)
+{
+ try
+ {
+ if (filePath.empty())
+ return false;
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ int songID = GetSongIDFromPath(filePath);
+ if (-1 == songID)
+ return false;
+
+ return SetSongUserrating(songID, userrating);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, filePath, userrating);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetSongUserrating(int idSong, int userrating)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string sql =
+ PrepareSQL("UPDATE song SET userrating ='%i' WHERE idSong = %i", userrating, idSong);
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, idSong, userrating);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetAlbumUserrating(const int idAlbum, int userrating)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (-1 == idAlbum)
+ return false;
+ std::string sql =
+ PrepareSQL("UPDATE album SET iUserrating='%i' WHERE idAlbum = %i", userrating, idAlbum);
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, idAlbum, userrating);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetSongVotes(const std::string& filePath, int votes)
+{
+ try
+ {
+ if (filePath.empty())
+ return false;
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ int songID = GetSongIDFromPath(filePath);
+ if (-1 == songID)
+ return false;
+
+ std::string sql = PrepareSQL("UPDATE song SET votes ='%i' WHERE idSong = %i", votes, songID);
+
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({},{}) failed", __FUNCTION__, filePath, votes);
+ }
+ return false;
+}
+
+int CMusicDatabase::GetSongIDFromPath(const std::string& filePath)
+{
+ // grab the where string to identify the song id
+ CURL url(filePath);
+ if (url.IsProtocol("musicdb"))
+ {
+ std::string strFile = URIUtils::GetFileName(filePath);
+ URIUtils::RemoveExtension(strFile);
+ return atoi(strFile.c_str());
+ }
+ // hit the db
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string strPath, strFileName;
+ SplitPath(filePath, strPath, strFileName);
+ URIUtils::AddSlashAtEnd(strPath);
+
+ std::string sql = PrepareSQL("SELECT idSong FROM song JOIN path ON song.idPath = path.idPath "
+ "WHERE song.strFileName='%s' AND path.strPath='%s'",
+ strFileName.c_str(), strPath.c_str());
+ if (!m_pDS->query(sql))
+ return -1;
+
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return -1;
+ }
+
+ int songID = m_pDS->fv("idSong").get_asInt();
+ m_pDS->close();
+ return songID;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
+ }
+ return -1;
+}
+
+bool CMusicDatabase::CommitTransaction()
+{
+ if (CDatabase::CommitTransaction())
+ { // number of items in the db has likely changed, so reset the infomanager cache
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ {
+ gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().SetLibraryBool(
+ LIBRARY_HAS_MUSIC, GetSongsCount() > 0);
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetScraperAll(const std::string& strBaseDir, const ADDON::ScraperPtr& scraper)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ std::string strSQL;
+ int idSetting = -1;
+ try
+ {
+ CONTENT_TYPE content = CONTENT_NONE;
+
+ // Build where clause from virtual path
+ Filter extFilter;
+ CMusicDbUrl musicUrl;
+ SortDescription sorting;
+ if (!musicUrl.FromString(strBaseDir) || !GetFilter(musicUrl, extFilter, sorting))
+ return false;
+
+ std::string itemType = musicUrl.GetType();
+ if (StringUtils::EqualsNoCase(itemType, "artists"))
+ {
+ content = CONTENT_ARTISTS;
+ }
+ else if (StringUtils::EqualsNoCase(itemType, "albums"))
+ {
+ content = CONTENT_ALBUMS;
+ }
+ else
+ return false; //Only artists and albums have info settings
+
+ std::string strSQLWhere;
+ if (!BuildSQL(strSQLWhere, extFilter, strSQLWhere))
+ return false;
+
+ // Replace view names with table names
+ StringUtils::Replace(strSQLWhere, "artistview", "artist");
+ StringUtils::Replace(strSQLWhere, "albumview", "album");
+
+ BeginTransaction();
+ // Clear current scraper settings (0 => default scraper used)
+ if (content == CONTENT_ARTISTS)
+ strSQL = "UPDATE artist SET idInfoSetting = %i ";
+ else
+ strSQL = "UPDATE album SET idInfoSetting = %i ";
+ strSQL = PrepareSQL(strSQL, 0) + strSQLWhere;
+ m_pDS->exec(strSQL);
+
+ //Remove orphaned settings
+ CleanupInfoSettings();
+
+ if (scraper)
+ {
+ // Add new info setting
+ strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
+ strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
+ m_pDS->exec(strSQL);
+ idSetting = static_cast<int>(m_pDS->lastinsertid());
+
+ if (content == CONTENT_ARTISTS)
+ strSQL = "UPDATE artist SET idInfoSetting = %i ";
+ else
+ strSQL = "UPDATE album SET idInfoSetting = %i ";
+ strSQL = PrepareSQL(strSQL, idSetting) + strSQLWhere;
+ m_pDS->exec(strSQL);
+ }
+ CommitTransaction();
+ return true;
+ }
+ catch (...)
+ {
+ RollbackTransaction();
+ CLog::Log(LOGERROR, "{} - ({}, {}) failed", __FUNCTION__, strBaseDir, strSQL);
+ }
+ return false;
+}
+
+bool CMusicDatabase::SetScraper(int id,
+ const CONTENT_TYPE& content,
+ const ADDON::ScraperPtr& scraper)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ std::string strSQL;
+ int idSetting = -1;
+ try
+ {
+ BeginTransaction();
+ // Fetch current info settings for item, 0 => default is used
+ if (content == CONTENT_ARTISTS)
+ strSQL = "SELECT idInfoSetting FROM artist WHERE idArtist = %i";
+ else
+ strSQL = "SELECT idInfoSetting FROM album WHERE idAlbum = %i";
+ strSQL = PrepareSQL(strSQL, id);
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ idSetting = m_pDS->fv("idInfoSetting").get_asInt();
+ m_pDS->close();
+
+ if (idSetting < 1)
+ { // Add new info setting
+ strSQL = "INSERT INTO infosetting (strScraperPath, strSettings) values ('%s','%s')";
+ strSQL = PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str());
+ m_pDS->exec(strSQL);
+ idSetting = static_cast<int>(m_pDS->lastinsertid());
+
+ if (content == CONTENT_ARTISTS)
+ strSQL = "UPDATE artist SET idInfoSetting = %i WHERE idArtist = %i";
+ else
+ strSQL = "UPDATE album SET idInfoSetting = %i WHERE idAlbum = %i";
+ strSQL = PrepareSQL(strSQL, idSetting, id);
+ m_pDS->exec(strSQL);
+ }
+ else
+ { // Update info setting
+ strSQL = "UPDATE infosetting SET strScraperPath = '%s', strSettings = '%s' "
+ "WHERE idSetting = %i";
+ strSQL =
+ PrepareSQL(strSQL, scraper->ID().c_str(), scraper->GetPathSettings().c_str(), idSetting);
+ m_pDS->exec(strSQL);
+ }
+ CommitTransaction();
+ return true;
+ }
+ catch (...)
+ {
+ RollbackTransaction();
+ CLog::Log(LOGERROR, "{} - ({}, {}) failed", __FUNCTION__, id, strSQL);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetScraper(int id, const CONTENT_TYPE& content, ADDON::ScraperPtr& scraper)
+{
+ std::string scraperUUID;
+ std::string strSettings;
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ strSQL = "SELECT strScraperPath, strSettings FROM infosetting JOIN ";
+ if (content == CONTENT_ARTISTS)
+ strSQL = strSQL + "artist ON artist.idInfoSetting = infosetting.idSetting "
+ "WHERE artist.idArtist = %i";
+ else
+ strSQL = strSQL + "album ON album.idInfoSetting = infosetting.idSetting "
+ "WHERE album.idAlbum = %i";
+ strSQL = PrepareSQL(strSQL, id);
+ m_pDS->query(strSQL);
+ if (!m_pDS->eof())
+ { // try and ascertain scraper
+ scraperUUID = m_pDS->fv("strScraperPath").get_asString();
+ strSettings = m_pDS->fv("strSettings").get_asString();
+
+ // Use pre configured or default scraper
+ ADDON::AddonPtr addon;
+ if (!scraperUUID.empty() &&
+ CServiceBroker::GetAddonMgr().GetAddon(scraperUUID, addon,
+ ADDON::OnlyEnabled::CHOICE_YES) &&
+ addon)
+ {
+ scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
+ if (scraper)
+ // Set settings
+ scraper->SetPathSettings(content, strSettings);
+ }
+ }
+ m_pDS->close();
+
+ if (!scraper)
+ { // use default music scraper instead
+ ADDON::AddonPtr addon;
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(
+ ADDON::ScraperTypeFromContent(content), addon))
+ {
+ scraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
+ return scraper != NULL;
+ }
+ else
+ return false;
+ }
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} -({}, {} {}) failed", __FUNCTION__, id, scraperUUID, strSettings);
+ }
+ return false;
+}
+
+bool CMusicDatabase::ScraperInUse(const std::string& scraperID) const
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string sql =
+ PrepareSQL("SELECT COUNT(1) FROM infosetting WHERE strScraperPath='%s'", scraperID.c_str());
+ if (!m_pDS->query(sql) || m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+ bool found = m_pDS->fv(0).get_asInt() > 0;
+ m_pDS->close();
+ return found;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, scraperID);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetItems(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(strBaseDir))
+ return false;
+
+ return GetItems(strBaseDir, musicUrl.GetType(), items, filter, sortDescription);
+}
+
+bool CMusicDatabase::GetItems(const std::string& strBaseDir,
+ const std::string& itemType,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+ if (StringUtils::EqualsNoCase(itemType, "genres"))
+ return GetGenresNav(strBaseDir, items, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "sources"))
+ return GetSourcesNav(strBaseDir, items, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "years"))
+ return GetYearsNav(strBaseDir, items, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "roles"))
+ return GetRolesNav(strBaseDir, items, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "artists"))
+ return GetArtistsNav(strBaseDir, items,
+ !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS),
+ -1, -1, -1, filter, sortDescription);
+ else if (StringUtils::EqualsNoCase(itemType, "albums"))
+ return GetAlbumsByWhere(strBaseDir, filter, items, sortDescription);
+ else if (StringUtils::EqualsNoCase(itemType, "discs"))
+ return GetDiscsByWhere(strBaseDir, filter, items, sortDescription);
+ else if (StringUtils::EqualsNoCase(itemType, "songs"))
+ return GetSongsFullByWhere(strBaseDir, filter, items, sortDescription, true);
+
+ return false;
+}
+
+std::string CMusicDatabase::GetItemById(const std::string& itemType, int id)
+{
+ if (StringUtils::EqualsNoCase(itemType, "genres"))
+ return GetGenreById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "sources"))
+ return GetSourceById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "years"))
+ return std::to_string(id);
+ else if (StringUtils::EqualsNoCase(itemType, "artists"))
+ return GetArtistById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "albums"))
+ return GetAlbumById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "roles"))
+ return GetRoleById(id);
+
+ return "";
+}
+
+void CMusicDatabase::ExportToXML(const CLibExportSettings& settings,
+ CGUIDialogProgress* progressDialog /*= nullptr*/)
+{
+ if (!settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS) &&
+ !settings.IsItemExported(ELIBEXPORT_SONGARTISTS) &&
+ !settings.IsItemExported(ELIBEXPORT_OTHERARTISTS) &&
+ !settings.IsItemExported(ELIBEXPORT_ALBUMS) && !settings.IsItemExported(ELIBEXPORT_SONGS))
+ return;
+
+ // Exporting albums either art or NFO (or both) selected
+ if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && settings.m_skipnfo &&
+ !settings.m_artwork && settings.IsItemExported(ELIBEXPORT_ALBUMS))
+ return;
+
+ std::string strFolder;
+ if (settings.IsSingleFile() || settings.IsSeparateFiles())
+ {
+ // Exporting to single file or separate files in a specified location
+ if (settings.m_strPath.empty())
+ return;
+
+ strFolder = settings.m_strPath;
+ if (!URIUtils::HasSlashAtEnd(strFolder))
+ URIUtils::AddSlashAtEnd(strFolder);
+ strFolder = URIUtils::GetDirectory(strFolder);
+ if (strFolder.empty())
+ return;
+ }
+ else if (settings.IsArtistFoldersOnly() || (settings.IsToLibFolders() && settings.IsArtists()))
+ {
+ // Exporting artist folders only, or artist NFO or art to library folders
+ // need Artist Information Folder defined.
+ // (Album NFO and art goes to music folders)
+ strFolder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ if (strFolder.empty())
+ return;
+ }
+
+ //
+ bool artistfoldersonly;
+ artistfoldersonly = settings.IsArtistFoldersOnly() ||
+ ((settings.IsToLibFolders() || settings.IsSeparateFiles()) &&
+ settings.m_skipnfo && !settings.m_artwork);
+
+ int iFailCount = 0;
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+ if (nullptr == m_pDS2)
+ return;
+
+ // Create our xml document
+ CXBMCTinyXML xmlDoc;
+ TiXmlDeclaration decl("1.0", "UTF-8", "yes");
+ xmlDoc.InsertEndChild(decl);
+ TiXmlNode* pMain = NULL;
+ if ((settings.IsToLibFolders() || settings.IsSeparateFiles()) && !artistfoldersonly)
+ pMain = &xmlDoc;
+ else if (settings.IsSingleFile())
+ {
+ TiXmlElement xmlMainElement("musicdb");
+ pMain = xmlDoc.InsertEndChild(xmlMainElement);
+ }
+
+ if (settings.IsItemExported(ELIBEXPORT_ALBUMS) && !artistfoldersonly)
+ {
+ // Find albums to export
+ std::vector<int> albumIds;
+ std::string strSQL = PrepareSQL("SELECT idAlbum FROM album WHERE strReleaseType = '%s' ",
+ CAlbum::ReleaseTypeToString(CAlbum::Album).c_str());
+ if (!settings.m_unscraped)
+ strSQL += "AND lastScraped IS NOT NULL";
+ CLog::Log(LOGDEBUG, "CMusicDatabase::{} - {}", __FUNCTION__, strSQL);
+ m_pDS->query(strSQL);
+
+ int total = m_pDS->num_rows();
+ int current = 0;
+
+ albumIds.reserve(total);
+ while (!m_pDS->eof())
+ {
+ albumIds.push_back(m_pDS->fv("idAlbum").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ for (const auto& albumId : albumIds)
+ {
+ CAlbum album;
+ GetAlbum(albumId, album);
+ std::string strAlbumPath;
+ std::string strPath;
+ // Get album path, empty unless all album songs are under a unique folder, and
+ // there are no songs from another album in the same folder.
+ if (!GetAlbumPath(albumId, strAlbumPath))
+ strAlbumPath.clear();
+ if (settings.IsSingleFile())
+ {
+ // Save album to xml, including album path
+ album.Save(pMain, "album", strAlbumPath);
+ }
+ else
+ { // Separate files and artwork
+ bool pathfound = false;
+ if (settings.IsToLibFolders())
+ { // Save album.nfo and artwork with music files.
+ // Most albums are under a unique folder, but if songs from various albums are mixed then
+ // avoid overwriting by not allow NFO and art to be exported
+ if (strAlbumPath.empty())
+ CLog::Log(LOGDEBUG,
+ "CMusicDatabase::{} - Not exporting album {} as unique path not found",
+ __FUNCTION__, album.strAlbum);
+ else if (!CDirectory::Exists(strAlbumPath))
+ CLog::Log(
+ LOGDEBUG,
+ "CMusicDatabase::{} - Not exporting album {} as found path {} does not exist",
+ __FUNCTION__, album.strAlbum, strAlbumPath);
+ else
+ {
+ strPath = strAlbumPath;
+ pathfound = true;
+ }
+ }
+ else
+ { // Save album.nfo and artwork to subfolder on export path
+ // strPath = strFolder/<albumartist name>/<albumname>
+ // where <albumname> is either the same name as the album folder
+ // containing the music files (if unique) or is created using the album name
+ std::string strAlbumArtist;
+ pathfound = GetArtistFolderName(album.GetAlbumArtist()[0],
+ album.GetMusicBrainzAlbumArtistID()[0], strAlbumArtist);
+ if (pathfound)
+ {
+ strPath = URIUtils::AddFileToFolder(strFolder, strAlbumArtist);
+ pathfound = CDirectory::Exists(strPath);
+ if (!pathfound)
+ pathfound = CDirectory::Create(strPath);
+ }
+ if (!pathfound)
+ CLog::Log(LOGDEBUG,
+ "CMusicDatabase::{} - Not exporting album {} as could not create {}",
+ __FUNCTION__, album.strAlbum, strPath);
+ else
+ {
+ std::string strAlbumFolder;
+ pathfound = GetAlbumFolder(album, strAlbumPath, strAlbumFolder);
+ if (pathfound)
+ {
+ strPath = URIUtils::AddFileToFolder(strPath, strAlbumFolder);
+ pathfound = CDirectory::Exists(strPath);
+ if (!pathfound)
+ pathfound = CDirectory::Create(strPath);
+ }
+ if (!pathfound)
+ CLog::Log(LOGDEBUG,
+ "CMusicDatabase::{} - Not exporting album {} as could not create {}",
+ __FUNCTION__, album.strAlbum, strPath);
+ }
+ }
+ if (pathfound)
+ {
+ if (!settings.m_skipnfo)
+ {
+ // Save album to NFO, including album path
+ album.Save(pMain, "album", strAlbumPath);
+ std::string nfoFile = URIUtils::AddFileToFolder(strPath, "album.nfo");
+ if (settings.m_overwrite || !CFile::Exists(nfoFile))
+ {
+ if (!xmlDoc.SaveFile(nfoFile))
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{}: Album nfo export failed! ('{}')",
+ __FUNCTION__, nfoFile);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(20302),
+ CURL::GetRedacted(nfoFile));
+ iFailCount++;
+ }
+ }
+ }
+ if (settings.m_artwork)
+ {
+ // Save art in album folder
+ // Note thumb resolution may be lower than original when overwriting
+ std::map<std::string, std::string> artwork;
+ std::string savedArtfile;
+ if (GetArtForItem(album.idAlbum, MediaTypeAlbum, artwork))
+ {
+ for (const auto& art : artwork)
+ {
+ if (art.first == "thumb")
+ savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
+ else
+ savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
+ CServiceBroker::GetTextureCache()->Export(art.second, savedArtfile,
+ settings.m_overwrite);
+ }
+ }
+ }
+ xmlDoc.Clear();
+ xmlDoc.InsertEndChild(decl); // TiXmlDeclaration ("1.0", "UTF-8", "yes")
+ }
+ }
+
+ if ((current % 50) == 0 && progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{album.strAlbum});
+ progressDialog->SetPercentage(current * 100 / total);
+ if (progressDialog->IsCanceled())
+ return;
+ }
+ current++;
+ }
+ }
+
+ // Export song playback history to single file only
+ if (settings.IsSingleFile() && settings.IsItemExported(ELIBEXPORT_SONGS))
+ {
+ if (!ExportSongHistory(pMain, progressDialog))
+ return;
+ }
+
+ if ((settings.IsArtists() || artistfoldersonly) && !strFolder.empty())
+ {
+ // Find artists to export
+ std::vector<int> artistIds;
+ Filter filter;
+
+ if (settings.IsItemExported(ELIBEXPORT_ALBUMARTISTS))
+ filter.AppendWhere("EXISTS(SELECT 1 FROM album_artist "
+ "WHERE album_artist.idArtist = artist.idArtist)",
+ false);
+ if (settings.IsItemExported(ELIBEXPORT_SONGARTISTS))
+ {
+ if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
+ filter.AppendWhere("EXISTS (SELECT 1 FROM song_artist "
+ "WHERE song_artist.idArtist = artist.idArtist )",
+ false);
+ else
+ filter.AppendWhere(
+ "EXISTS (SELECT 1 FROM song_artist "
+ "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole = 1)",
+ false);
+ }
+ else if (settings.IsItemExported(ELIBEXPORT_OTHERARTISTS))
+ filter.AppendWhere(
+ "EXISTS (SELECT 1 FROM song_artist "
+ "WHERE song_artist.idArtist = artist.idArtist AND song_artist.idRole > 1)",
+ false);
+
+ if (!settings.m_unscraped && !artistfoldersonly)
+ filter.AppendWhere("lastScraped IS NOT NULL", true);
+
+ std::string strSQL = "SELECT idArtist FROM artist";
+ BuildSQL(strSQL, filter, strSQL);
+ CLog::Log(LOGDEBUG, "CMusicDatabase::{} - {}", __FUNCTION__, strSQL);
+
+ m_pDS->query(strSQL);
+ int total = m_pDS->num_rows();
+ int current = 0;
+ artistIds.reserve(total);
+ while (!m_pDS->eof())
+ {
+ artistIds.push_back(m_pDS->fv("idArtist").get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ for (const auto& artistId : artistIds)
+ {
+ CArtist artist;
+ // Include discography when not folders only
+ GetArtist(artistId, artist, !artistfoldersonly);
+ std::string strPath;
+ std::map<std::string, std::string> artwork;
+ if (settings.IsSingleFile())
+ {
+ // Save artist to xml, and old path (common to music files) if it has one
+ GetOldArtistPath(artist.idArtist, strPath);
+ artist.Save(pMain, "artist", strPath);
+
+ if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
+ { // append to the XML
+ TiXmlElement additionalNode("art");
+ for (const auto& i : artwork)
+ XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second);
+ pMain->LastChild()->InsertEndChild(additionalNode);
+ }
+ }
+ else
+ { // Separate files: artist.nfo and artwork in strFolder/<artist name>
+ // Get unique folder allowing for duplicate names e.g. 2 x John Williams
+ bool pathfound = GetArtistFolderName(artist, strPath);
+ if (pathfound)
+ {
+ strPath = URIUtils::AddFileToFolder(strFolder, strPath);
+ pathfound = CDirectory::Exists(strPath);
+ if (!pathfound)
+ pathfound = CDirectory::Create(strPath);
+ }
+ if (!pathfound)
+ CLog::Log(LOGDEBUG,
+ "CMusicDatabase::{} - Not exporting artist {} as could not create {}",
+ __FUNCTION__, artist.strArtist, strPath);
+ else
+ {
+ if (!artistfoldersonly)
+ {
+ if (!settings.m_skipnfo)
+ {
+ artist.Save(pMain, "artist", strPath);
+ std::string nfoFile = URIUtils::AddFileToFolder(strPath, "artist.nfo");
+ if (settings.m_overwrite || !CFile::Exists(nfoFile))
+ {
+ if (!xmlDoc.SaveFile(nfoFile))
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{}: Artist nfo export failed! ('{}')",
+ __FUNCTION__, nfoFile);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(20302),
+ CURL::GetRedacted(nfoFile));
+ iFailCount++;
+ }
+ }
+ }
+ if (settings.m_artwork)
+ {
+ std::string savedArtfile;
+ if (GetArtForItem(artist.idArtist, MediaTypeArtist, artwork))
+ {
+ for (const auto& art : artwork)
+ {
+ if (art.first == "thumb")
+ savedArtfile = URIUtils::AddFileToFolder(strPath, "folder");
+ else
+ savedArtfile = URIUtils::AddFileToFolder(strPath, art.first);
+ CServiceBroker::GetTextureCache()->Export(art.second, savedArtfile,
+ settings.m_overwrite);
+ }
+ }
+ }
+ xmlDoc.Clear();
+ xmlDoc.InsertEndChild(decl); // TiXmlDeclaration ("1.0", "UTF-8", "yes")
+ }
+ }
+ }
+ if ((current % 50) == 0 && progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{artist.strArtist});
+ progressDialog->SetPercentage(current * 100 / total);
+ if (progressDialog->IsCanceled())
+ return;
+ }
+ current++;
+ }
+ }
+
+ if (settings.IsSingleFile())
+ {
+ std::string xmlFile = URIUtils::AddFileToFolder(
+ strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsDBDate() + ".xml");
+ if (CFile::Exists(xmlFile))
+ xmlFile = URIUtils::AddFileToFolder(
+ strFolder, "kodi_musicdb" + CDateTime::GetCurrentDateTime().GetAsSaveString() + ".xml");
+ xmlDoc.SaveFile(xmlFile);
+
+ CVariant data;
+ data["file"] = xmlFile;
+ if (iFailCount > 0)
+ data["failcount"] = iFailCount;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnExport",
+ data);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{} failed", __FUNCTION__);
+ iFailCount++;
+ }
+
+ if (progressDialog)
+ progressDialog->Close();
+
+ if (iFailCount > 0 && progressDialog)
+ HELPERS::ShowOKDialogLines(
+ CVariant{20196}, CVariant{StringUtils::Format(g_localizeStrings.Get(15011), iFailCount)});
+}
+
+bool CMusicDatabase::ExportSongHistory(TiXmlNode* pNode, CGUIDialogProgress* progressDialog)
+{
+ try
+ {
+ // Export songs with some playback history
+ std::string strSQL =
+ "SELECT idSong, song.idAlbum, "
+ "strAlbum, strMusicBrainzAlbumID, album.strArtistDisp AS strAlbumArtistDisp, "
+ "song.strArtistDisp, strTitle, iTrack, strFileName, strMusicBrainzTrackID, "
+ "iTimesPlayed, lastplayed, song.rating, song.votes, song.userrating "
+ "FROM song JOIN album on album.idAlbum = song.idAlbum "
+ "WHERE iTimesPlayed > 0 OR rating > 0 or userrating > 0";
+
+ CLog::Log(LOGDEBUG, "{0} - {1}", __FUNCTION__, strSQL);
+ m_pDS->query(strSQL);
+
+ int total = m_pDS->num_rows();
+ int current = 0;
+ while (!m_pDS->eof())
+ {
+ TiXmlElement songElement("song");
+ TiXmlNode* song = pNode->InsertEndChild(songElement);
+
+ XMLUtils::SetInt(song, "idsong", m_pDS->fv("idSong").get_asInt());
+ XMLUtils::SetString(song, "artistdesc", m_pDS->fv("strArtistDisp").get_asString());
+ XMLUtils::SetString(song, "title", m_pDS->fv("strTitle").get_asString());
+ XMLUtils::SetInt(song, "track", m_pDS->fv("iTrack").get_asInt());
+ XMLUtils::SetString(song, "filename", m_pDS->fv("strFilename").get_asString());
+ XMLUtils::SetString(song, "musicbrainztrackid",
+ m_pDS->fv("strMusicBrainzTrackID").get_asString());
+ XMLUtils::SetInt(song, "idalbum", m_pDS->fv("idAlbum").get_asInt());
+ XMLUtils::SetString(song, "albumtitle", m_pDS->fv("strAlbum").get_asString());
+ XMLUtils::SetString(song, "musicbrainzalbumid",
+ m_pDS->fv("strMusicBrainzAlbumID").get_asString());
+ XMLUtils::SetString(song, "albumartistdesc", m_pDS->fv("strAlbumArtistDisp").get_asString());
+ XMLUtils::SetInt(song, "timesplayed", m_pDS->fv("iTimesplayed").get_asInt());
+ XMLUtils::SetString(song, "lastplayed", m_pDS->fv("lastplayed").get_asString());
+ auto* rating = XMLUtils::SetString(
+ song, "rating", StringUtils::FormatNumber(m_pDS->fv("rating").get_asFloat()));
+ if (rating)
+ rating->ToElement()->SetAttribute("max", 10);
+ XMLUtils::SetInt(song, "votes", m_pDS->fv("votes").get_asInt());
+ auto* userrating = XMLUtils::SetInt(song, "userrating", m_pDS->fv("userrating").get_asInt());
+ if (userrating)
+ userrating->ToElement()->SetAttribute("max", 10);
+
+ if ((current % 100) == 0 && progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{m_pDS->fv("strAlbum").get_asString()});
+ progressDialog->SetPercentage(current * 100 / total);
+ if (progressDialog->IsCanceled())
+ {
+ m_pDS->close();
+ return false;
+ }
+ }
+ current++;
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{0} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+void CMusicDatabase::ImportFromXML(const std::string& xmlFile, CGUIDialogProgress* progressDialog)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(xmlFile) && progressDialog)
+ {
+ HELPERS::ShowOKDialogLines(CVariant{20197}, CVariant{38354}); //"Unable to read xml file"
+ return;
+ }
+
+ TiXmlElement* root = xmlDoc.RootElement();
+ if (!root)
+ return;
+
+ TiXmlElement* entry = root->FirstChildElement();
+ int current = 0;
+ int total = 0;
+ int songtotal = 0;
+ // Count the number of artists, albums and songs
+ while (entry)
+ {
+ if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0 ||
+ StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
+ total++;
+ else if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0)
+ songtotal++;
+
+ entry = entry->NextSiblingElement();
+ }
+
+ BeginTransaction();
+ entry = root->FirstChildElement();
+ while (entry)
+ {
+ std::string strTitle;
+ if (StringUtils::CompareNoCase(entry->Value(), "artist", 6) == 0)
+ {
+ CArtist importedArtist;
+ importedArtist.Load(entry);
+ strTitle = importedArtist.strArtist;
+
+ // Match by mbid first (that is definatively unique), then name (no mbid), finally by just name
+ int idArtist = GetArtistByMatch(importedArtist);
+ if (idArtist > -1)
+ {
+ CArtist artist;
+ GetArtist(idArtist, artist, true); // include discography
+ artist.MergeScrapedArtist(importedArtist, true);
+ UpdateArtist(artist);
+ }
+ else
+ CLog::Log(LOGDEBUG, "{} - Not import additional artist data as {} not found",
+ __FUNCTION__, importedArtist.strArtist);
+ current++;
+ }
+ else if (StringUtils::CompareNoCase(entry->Value(), "album", 5) == 0)
+ {
+ CAlbum importedAlbum;
+ importedAlbum.Load(entry);
+ strTitle = importedAlbum.strAlbum;
+ // Match by mbid first (that is definatively unique), then title and artist desc (no mbid), finally by just name and artist
+ int idAlbum = GetAlbumByMatch(importedAlbum);
+ if (idAlbum > -1)
+ {
+ CAlbum album;
+ GetAlbum(idAlbum, album, true);
+ album.MergeScrapedAlbum(importedAlbum, true);
+ UpdateAlbum(album); //Will replace song artists if present in xml
+ }
+ else
+ CLog::Log(LOGDEBUG, "{} - Not import additional album data as {} not found", __FUNCTION__,
+ importedAlbum.strAlbum);
+
+ current++;
+ }
+ entry = entry->NextSiblingElement();
+ if (progressDialog && total)
+ {
+ progressDialog->SetPercentage(current * 100 / total);
+ progressDialog->SetLine(2, CVariant{std::move(strTitle)});
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ RollbackTransaction();
+ return;
+ }
+ }
+ }
+ CommitTransaction();
+
+ // Import song playback history <song> entries found
+ if (songtotal > 0)
+ if (!ImportSongHistory(xmlFile, songtotal, progressDialog))
+ return;
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ RollbackTransaction();
+ }
+ if (progressDialog)
+ progressDialog->Close();
+}
+
+bool CMusicDatabase::ImportSongHistory(const std::string& xmlFile,
+ const int total,
+ CGUIDialogProgress* progressDialog)
+{
+ bool bHistSongExists = false;
+ try
+ {
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(xmlFile))
+ return false;
+
+ TiXmlElement* root = xmlDoc.RootElement();
+ if (!root)
+ return false;
+
+ TiXmlElement* entry = root->FirstChildElement();
+ int current = 0;
+
+ if (progressDialog)
+ {
+ progressDialog->SetLine(1, CVariant{38350}); //"Importing song playback history"
+ progressDialog->SetLine(2, CVariant{""});
+ }
+
+ // As can be many songs do in db, not song at a time which would be slow
+ // Convert xml entries into a SQL bulk insert statement
+ std::string strSQL;
+ entry = root->FirstChildElement();
+ while (entry)
+ {
+ std::string strArtistDisp;
+ std::string strTitle;
+ int iTrack;
+ std::string strFilename;
+ std::string strMusicBrainzTrackID;
+ std::string strAlbum;
+ std::string strMusicBrainzAlbumID;
+ std::string strAlbumArtistDisp;
+ int iTimesplayed;
+ std::string lastplayed;
+ int iUserrating = 0;
+ float fRating = 0.0;
+ int iVotes;
+ std::string strSQLSong;
+ if (StringUtils::CompareNoCase(entry->Value(), "song", 4) == 0)
+ {
+ XMLUtils::GetString(entry, "artistdesc", strArtistDisp);
+ XMLUtils::GetString(entry, "title", strTitle);
+ XMLUtils::GetInt(entry, "track", iTrack);
+ XMLUtils::GetString(entry, "filename", strFilename);
+ XMLUtils::GetString(entry, "musicbrainztrackid", strMusicBrainzTrackID);
+ XMLUtils::GetString(entry, "albumtitle", strAlbum);
+ XMLUtils::GetString(entry, "musicbrainzalbumid", strMusicBrainzAlbumID);
+ XMLUtils::GetString(entry, "albumartistdesc", strAlbumArtistDisp);
+ XMLUtils::GetInt(entry, "timesplayed", iTimesplayed);
+ XMLUtils::GetString(entry, "lastplayed", lastplayed);
+ const TiXmlElement* rElement = entry->FirstChildElement("rating");
+ if (rElement)
+ {
+ float rating = 0;
+ float max_rating = 10;
+ XMLUtils::GetFloat(entry, "rating", rating);
+ if (rElement->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS && max_rating >= 1)
+ rating *= (10.f / max_rating); // Normalise the value to between 0 and 10
+ if (rating > 10.f)
+ rating = 10.f;
+ fRating = rating;
+ }
+ XMLUtils::GetInt(entry, "votes", iVotes);
+ const TiXmlElement* userrating = entry->FirstChildElement("userrating");
+ if (userrating)
+ {
+ float rating = 0;
+ float max_rating = 10;
+ XMLUtils::GetFloat(entry, "userrating", rating);
+ if (userrating->QueryFloatAttribute("max", &max_rating) == TIXML_SUCCESS &&
+ max_rating >= 1)
+ rating *= (10.f / max_rating); // Normalise the value to between 0 and 10
+ if (rating > 10.f)
+ rating = 10.f;
+ iUserrating = MathUtils::round_int(static_cast<double>(rating));
+ }
+
+ strSQLSong = PrepareSQL("(%d, %d, ", current + 1, iTrack);
+ strSQLSong += PrepareSQL("'%s', '%s', '%s', ", strArtistDisp.c_str(), strTitle.c_str(),
+ strFilename.c_str());
+ if (strMusicBrainzTrackID.empty())
+ strSQLSong += PrepareSQL("NULL, ");
+ else
+ strSQLSong += PrepareSQL("'%s', ", strMusicBrainzTrackID.c_str());
+ strSQLSong += PrepareSQL("'%s', '%s', ", strAlbum.c_str(), strAlbumArtistDisp.c_str());
+ if (strMusicBrainzAlbumID.empty())
+ strSQLSong += PrepareSQL("NULL, ");
+ else
+ strSQLSong += PrepareSQL("'%s', ", strMusicBrainzAlbumID.c_str());
+ strSQLSong += PrepareSQL("%d, ", iTimesplayed);
+ if (lastplayed.empty())
+ strSQLSong += PrepareSQL("NULL, ");
+ else
+ strSQLSong += PrepareSQL("'%s', ", lastplayed.c_str());
+ strSQLSong +=
+ PrepareSQL("%.1f, %d, %d, -1, -1)", static_cast<double>(fRating), iVotes, iUserrating);
+
+ if (current > 0)
+ strSQLSong = ", " + strSQLSong;
+ strSQL += strSQLSong;
+ current++;
+ }
+
+ entry = entry->NextSiblingElement();
+
+ if ((current % 100) == 0 && progressDialog)
+ {
+ progressDialog->SetPercentage(current * 100 / total);
+ progressDialog->SetLine(3, CVariant{std::move(strTitle)});
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ return false;
+ }
+ }
+
+ CLog::Log(LOGINFO, "{0}: Create temporary HistSong table and insert {1} records", __FUNCTION__,
+ total);
+ /* Can not use CREATE TEMPORARY TABLE as MySQL does not support updates of
+ song table using correlated subqueries to a temp table. An updatable join
+ to temp table would work in MySQL but SQLite not support updatable joins.
+ */
+ m_pDS->exec("CREATE TABLE HistSong ("
+ "idSongSrc INTEGER primary key, "
+ "strAlbum varchar(256), "
+ "strMusicBrainzAlbumID text, "
+ "strAlbumArtistDisp text, "
+ "strArtistDisp text, strTitle varchar(512), "
+ "iTrack INTEGER, strFileName text, strMusicBrainzTrackID text, "
+ "iTimesPlayed INTEGER, lastplayed varchar(20) default NULL, "
+ "rating FLOAT NOT NULL DEFAULT 0, votes INTEGER NOT NULL DEFAULT 0, "
+ "userrating INTEGER NOT NULL DEFAULT 0, "
+ "idAlbum INTEGER, idSong INTEGER)");
+ bHistSongExists = true;
+
+ strSQL = "INSERT INTO HistSong (idSongSrc, iTrack, strArtistDisp, strTitle, "
+ "strFileName, strMusicBrainzTrackID, "
+ "strAlbum, strAlbumArtistDisp, strMusicBrainzAlbumID, "
+ " iTimesPlayed, lastplayed, rating, votes, userrating, idAlbum, idSong) VALUES " +
+ strSQL;
+ m_pDS->exec(strSQL);
+
+ if (progressDialog)
+ {
+ progressDialog->SetLine(2, CVariant{38351}); //"Matching data"
+ progressDialog->SetLine(3, CVariant{""});
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ m_pDS->exec("DROP TABLE HistSong");
+ return false;
+ }
+ }
+
+ BeginTransaction();
+ // Match albums first on mbid then artist string and album title, setting idAlbum
+ // mbid is unique so subquery can only return one result at most
+ strSQL = "UPDATE HistSong "
+ "SET idAlbum = (SELECT album.idAlbum FROM album "
+ "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) "
+ "WHERE EXISTS(SELECT 1 FROM album "
+ "WHERE album.strMusicBrainzAlbumID = HistSong.strMusicBrainzAlbumID) AND idAlbum < 0";
+ m_pDS->exec(strSQL);
+
+ // Can only be one album with same title and artist(s) and no mbid.
+ // But could have 2 releases one with and one without mbid, match up those without mbid
+ strSQL = "UPDATE HistSong "
+ "SET idAlbum = (SELECT album.idAlbum FROM album "
+ "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
+ "AND HistSong.strAlbum = album.strAlbum "
+ "AND album.strMusicBrainzAlbumID IS NULL "
+ "AND HistSong.strMusicBrainzAlbumID IS NULL) "
+ "WHERE EXISTS(SELECT 1 FROM album "
+ "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
+ "AND HistSong.strAlbum = album.strAlbum "
+ "AND album.strMusicBrainzAlbumID IS NULL "
+ "AND HistSong.strMusicBrainzAlbumID IS NULL) "
+ "AND idAlbum < 0";
+ m_pDS->exec(strSQL);
+
+ // Try match rest by title and artist(s), prioritise one without mbid
+ // Target could have multiple releases - with mbid (non-matching) or one without mbid
+ strSQL = "UPDATE HistSong "
+ "SET idAlbum = (SELECT album.idAlbum FROM album "
+ "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
+ "AND HistSong.strAlbum = album.strAlbum "
+ "ORDER BY album.strMusicBrainzAlbumID LIMIT 1) "
+ "WHERE EXISTS(SELECT 1 FROM album "
+ "WHERE HistSong.strAlbumArtistDisp = album.strArtistDisp "
+ "AND HistSong.strAlbum = album.strAlbum) "
+ "AND idAlbum < 0";
+ m_pDS->exec(strSQL);
+ if (progressDialog)
+ {
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ RollbackTransaction();
+ m_pDS->exec("DROP TABLE HistSong");
+ return false;
+ }
+ }
+
+ // Match songs on first on idAlbum, track and mbid, then idAlbum, track and title, setting idSong
+ strSQL = "UPDATE HistSong "
+ "SET idSong = (SELECT idsong FROM song "
+ "WHERE HistSong.idAlbum = song.idAlbum AND "
+ "HistSong.iTrack = song.iTrack AND "
+ "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) "
+ "WHERE EXISTS(SELECT 1 FROM song "
+ "WHERE HistSong.idAlbum = song.idAlbum AND "
+ "HistSong.iTrack = song.iTrack AND "
+ "HistSong.strMusicBrainzTrackID = song.strMusicBrainzTrackID) AND idSong < 0";
+ m_pDS->exec(strSQL);
+
+ // An album can have more than one song with same track and title (although idAlbum, track and
+ // title is often unique), but not using filename as an identifier to allow for import of song
+ // history for renamed files. It is about song playback not file playback.
+ // Pick the first
+ strSQL = "UPDATE HistSong "
+ "SET idSong = (SELECT idsong FROM song "
+ "WHERE HistSong.idAlbum = song.idAlbum AND "
+ "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle LIMIT 1) "
+ "WHERE EXISTS(SELECT 1 FROM song "
+ "WHERE HistSong.idAlbum = song.idAlbum AND "
+ "HistSong.iTrack = song.iTrack AND HistSong.strTitle = song.strTitle) AND idSong < 0";
+ m_pDS->exec(strSQL);
+
+ CommitTransaction();
+ if (progressDialog)
+ {
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ m_pDS->exec("DROP TABLE HistSong");
+ return false;
+ }
+ }
+
+ // Create an index to speed up the updates
+ m_pDS->exec("CREATE INDEX idxHistSong ON HistSong(idSong)");
+
+ // Log how many songs matched
+ int unmatched = GetSingleValueInt("SELECT COUNT(1) FROM HistSong WHERE idSong < 0", m_pDS);
+ CLog::Log(LOGINFO, "{0}: Importing song history {1} of {2} songs matched", __FUNCTION__,
+ total - unmatched, total);
+
+ if (progressDialog)
+ {
+ progressDialog->SetLine(2, CVariant{38352}); //"Updating song playback history"
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ m_pDS->exec("DROP TABLE HistSong"); // Drops index too
+ return false;
+ }
+ }
+
+ /* Update song table using the song ids we have matched.
+ Use correlated subqueries as SQLite does not support updatable joins.
+ MySQL requires HistSong table not to be defined temporary for this.
+ */
+
+ BeginTransaction();
+ // Times played and last played date(when count is greater)
+ strSQL = "UPDATE song SET iTimesPlayed = "
+ "(SELECT iTimesPlayed FROM HistSong WHERE HistSong.idSong = song.idSong), "
+ "lastplayed = "
+ "(SELECT lastplayed FROM HistSong WHERE HistSong.idSong = song.idSong) "
+ "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
+ "HistSong.idSong = song.idSong AND HistSong.iTimesPlayed > song.iTimesPlayed)";
+ m_pDS->exec(strSQL);
+
+ // User rating
+ strSQL = "UPDATE song SET userrating = "
+ "(SELECT userrating FROM HistSong WHERE HistSong.idSong = song.idSong) "
+ "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
+ "HistSong.idSong = song.idSong AND HistSong.userrating > 0)";
+ m_pDS->exec(strSQL);
+
+ // Rating and votes
+ strSQL = "UPDATE song SET rating = "
+ "(SELECT rating FROM HistSong WHERE HistSong.idSong = song.idSong), "
+ "votes = "
+ "(SELECT votes FROM HistSong WHERE HistSong.idSong = song.idSong) "
+ "WHERE EXISTS(SELECT 1 FROM HistSong WHERE "
+ "HistSong.idSong = song.idSong AND HistSong.rating > 0)";
+ m_pDS->exec(strSQL);
+
+ if (progressDialog)
+ {
+ progressDialog->Progress();
+ if (progressDialog->IsCanceled())
+ {
+ RollbackTransaction();
+ m_pDS->exec("DROP TABLE HistSong");
+ return false;
+ }
+ }
+ CommitTransaction();
+
+ // Tidy up temp table (index also removed)
+ m_pDS->exec("DROP TABLE HistSong");
+ // Compact db to recover space as had to add/drop actual table
+ if (progressDialog)
+ {
+ progressDialog->SetLine(2, CVariant{331});
+ progressDialog->Progress();
+ }
+ Compress(false);
+
+ // Write event log entry
+ // "Importing song history {1} of {2} songs matched", total - unmatched, total)
+ std::string strLine =
+ StringUtils::Format(g_localizeStrings.Get(38353), total - unmatched, total);
+
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(new CNotificationEvent(20197, strLine, EventLevel::Information)));
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ RollbackTransaction();
+ if (bHistSongExists)
+ m_pDS->exec("DROP TABLE HistSong");
+ }
+ return false;
+}
+
+void CMusicDatabase::SetPropertiesFromArtist(CFileItem& item, const CArtist& artist)
+{
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ item.SetProperty("artist_sortname", artist.strSortName);
+ item.SetProperty("artist_type", artist.strType);
+ item.SetProperty("artist_gender", artist.strGender);
+ item.SetProperty("artist_disambiguation", artist.strDisambiguation);
+ item.SetProperty("artist_instrument", StringUtils::Join(artist.instruments, itemSeparator));
+ item.SetProperty("artist_instrument_array", artist.instruments);
+ item.SetProperty("artist_style", StringUtils::Join(artist.styles, itemSeparator));
+ item.SetProperty("artist_style_array", artist.styles);
+ item.SetProperty("artist_mood", StringUtils::Join(artist.moods, itemSeparator));
+ item.SetProperty("artist_mood_array", artist.moods);
+ item.SetProperty("artist_born", artist.strBorn);
+ item.SetProperty("artist_formed", artist.strFormed);
+ item.SetProperty("artist_description", artist.strBiography);
+ item.SetProperty("artist_genre", StringUtils::Join(artist.genre, itemSeparator));
+ item.SetProperty("artist_genre_array", artist.genre);
+ item.SetProperty("artist_died", artist.strDied);
+ item.SetProperty("artist_disbanded", artist.strDisbanded);
+ item.SetProperty("artist_yearsactive", StringUtils::Join(artist.yearsActive, itemSeparator));
+ item.SetProperty("artist_yearsactive_array", artist.yearsActive);
+}
+
+void CMusicDatabase::SetPropertiesFromAlbum(CFileItem& item, const CAlbum& album)
+{
+ const std::string itemSeparator =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator;
+
+ item.SetProperty("album_description", album.strReview);
+ item.SetProperty("album_theme", StringUtils::Join(album.themes, itemSeparator));
+ item.SetProperty("album_theme_array", album.themes);
+ item.SetProperty("album_mood", StringUtils::Join(album.moods, itemSeparator));
+ item.SetProperty("album_mood_array", album.moods);
+ item.SetProperty("album_style", StringUtils::Join(album.styles, itemSeparator));
+ item.SetProperty("album_style_array", album.styles);
+ item.SetProperty("album_type", album.strType);
+ item.SetProperty("album_label", album.strLabel);
+ item.SetProperty("album_artist", album.GetAlbumArtistString());
+ item.SetProperty("album_artist_array", album.GetAlbumArtist());
+ item.SetProperty("album_genre", StringUtils::Join(album.genre, itemSeparator));
+ item.SetProperty("album_genre_array", album.genre);
+ item.SetProperty("album_title", album.strAlbum);
+ if (album.fRating > 0)
+ item.SetProperty("album_rating", StringUtils::FormatNumber(album.fRating));
+ if (album.iUserrating > 0)
+ item.SetProperty("album_userrating", album.iUserrating);
+ if (album.iVotes > 0)
+ item.SetProperty("album_votes", album.iVotes);
+
+ item.SetProperty("album_isboxset", album.bBoxedSet);
+ item.SetProperty("album_totaldiscs", album.iTotalDiscs);
+ item.SetProperty("album_releasetype", CAlbum::ReleaseTypeToString(album.releaseType));
+ item.SetProperty("album_duration",
+ StringUtils::SecondsToTimeString(album.iAlbumDuration,
+ static_cast<TIME_FORMAT>(TIME_FORMAT_GUESS)));
+}
+
+void CMusicDatabase::SetPropertiesForFileItem(CFileItem& item)
+{
+ if (!item.HasMusicInfoTag())
+ return;
+ // May already have song artist ids as item property set when data read from
+ // db, but check property is valid array (scripts could set item properties
+ // incorrectly), otherwise try to fetch artist by name.
+ int idArtist = -1;
+ if (item.HasProperty("artistid") && item.GetProperty("artistid").isArray())
+ {
+ CVariant::const_iterator_array varid = item.GetProperty("artistid").begin_array();
+ idArtist = static_cast<int>(varid->asInteger());
+ }
+ else
+ idArtist = GetArtistByName(item.GetMusicInfoTag()->GetArtistString());
+ if (idArtist > -1)
+ {
+ CArtist artist;
+ if (GetArtist(idArtist, artist))
+ SetPropertiesFromArtist(item, artist);
+ }
+ int idAlbum = item.GetMusicInfoTag()->GetAlbumId();
+ if (idAlbum <= 0)
+ idAlbum = GetAlbumByName(item.GetMusicInfoTag()->GetAlbum(),
+ item.GetMusicInfoTag()->GetArtistString());
+ if (idAlbum > -1)
+ {
+ CAlbum album;
+ if (GetAlbum(idAlbum, album, false))
+ SetPropertiesFromAlbum(item, album);
+ }
+}
+
+void CMusicDatabase::SetItemUpdated(int mediaId, const std::string& mediaType)
+{
+ std::string strSQL;
+ try
+ {
+ if (mediaType != MediaTypeArtist && mediaType != MediaTypeAlbum && mediaType != MediaTypeSong)
+ return;
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ // Fire AFTER UPDATE db trigger on artist, album or song table to set datemodified field
+ // e.g. when artwork for item is changed from info dialog but not item details.
+ // Use SQL UPDATE that does not change record data.
+ if (mediaType == MediaTypeArtist)
+ strSQL = PrepareSQL("UPDATE artist SET strArtist = strArtist WHERE idArtist = %i", mediaId);
+ else if (mediaType == MediaTypeAlbum)
+ strSQL = PrepareSQL("UPDATE album SET strAlbum = strAlbum WHERE idAlbum = %i", mediaId);
+ else // MediaTypeSong
+ strSQL = PrepareSQL("UPDATE song SET strTitle = strTitle WHERE idSong = %i", mediaId);
+ m_pDS->exec(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "CMusicDatabase::{0} ({1}, {2}) - failed to execute {3}", __FUNCTION__,
+ mediaId, mediaType, strSQL);
+ }
+}
+
+void CMusicDatabase::SetArtForItem(int mediaId,
+ const std::string& mediaType,
+ const std::map<std::string, std::string>& art)
+{
+ for (const auto& i : art)
+ SetArtForItem(mediaId, mediaType, i.first, i.second);
+}
+
+void CMusicDatabase::SetArtForItem(int mediaId,
+ const std::string& mediaType,
+ const std::string& artType,
+ const std::string& url)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ // don't set <foo>.<bar> art types - these are derivative types from parent items
+ if (artType.find('.') != std::string::npos)
+ return;
+
+ std::string sql = PrepareSQL("SELECT art_id FROM art "
+ "WHERE media_id=%i AND media_type='%s' AND type='%s'",
+ mediaId, mediaType.c_str(), artType.c_str());
+ m_pDS->query(sql);
+ if (!m_pDS->eof())
+ { // update
+ int artId = m_pDS->fv(0).get_asInt();
+ m_pDS->close();
+ sql = PrepareSQL("UPDATE art SET url='%s' where art_id=%d", url.c_str(), artId);
+ m_pDS->exec(sql);
+ }
+ else
+ { // insert
+ m_pDS->close();
+ sql = PrepareSQL("INSERT INTO art(media_id, media_type, type, url) "
+ "VALUES (%d, '%s', '%s', '%s')",
+ mediaId, mediaType.c_str(), artType.c_str(), url.c_str());
+ m_pDS->exec(sql);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}, '{}', '{}', '{}') failed", __FUNCTION__, mediaId, mediaType,
+ artType, url);
+ }
+}
+
+bool CMusicDatabase::GetArtForItem(
+ int songId, int albumId, int artistId, bool bPrimaryArtist, std::vector<ArtForThumbLoader>& art)
+{
+ std::string strSQL;
+ try
+ {
+ if (!(songId > 0 || albumId > 0 || artistId > 0))
+ return false;
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false; // using dataset 2 as we're likely called in loops on dataset 1
+
+ Filter filter;
+ if (songId > 0)
+ filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", songId, MediaTypeSong));
+ if (albumId > 0)
+ filter.AppendWhere(PrepareSQL("media_id = %i AND media_type ='%s'", albumId, MediaTypeAlbum),
+ false);
+ if (artistId > 0)
+ filter.AppendWhere(
+ PrepareSQL("media_id = %i AND media_type ='%s'", artistId, MediaTypeArtist), false);
+
+ strSQL = "SELECT DISTINCT art_id, media_id, media_type, type, '' as prefix, url, 0 as iorder "
+ "FROM art";
+ if (!BuildSQL(strSQL, filter, strSQL))
+ return false;
+
+ if (!(artistId > 0))
+ {
+ // Artist ID unknown, so lookup album artist for albums and songs
+ std::string strSQL2;
+ if (albumId > 0)
+ {
+ //Album ID known, so use it to look up album artist(s)
+ strSQL2 = PrepareSQL(
+ "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
+ "url, album_artist.iOrder as iorder FROM art "
+ "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
+ "WHERE album_artist.idAlbum = %i ",
+ MediaTypeArtist, albumId);
+ if (bPrimaryArtist)
+ strSQL2 += "AND album_artist.iOrder = 0";
+
+ strSQL = strSQL + " UNION " + strSQL2;
+ }
+ if (songId > 0)
+ {
+ if (albumId < 0)
+ {
+ //Album ID unknown, so get from song to look up album artist(s)
+ strSQL2 = PrepareSQL(
+ "SELECT art_id, media_id, media_type, type, 'albumartist' as prefix, "
+ "url, album_artist.iOrder as iorder FROM art "
+ "JOIN album_artist ON art.media_id = album_artist.idArtist AND art.media_type ='%s' "
+ "JOIN song ON song.idAlbum = album_artist.idAlbum "
+ "WHERE song.idSong = %i ",
+ MediaTypeArtist, songId);
+ if (bPrimaryArtist)
+ strSQL2 += "AND album_artist.iOrder = 0";
+
+ strSQL = strSQL + " UNION " + strSQL2;
+ }
+
+ // Artist ID unknown, so lookup artist for songs (could be different from album artist)
+ strSQL2 = PrepareSQL(
+ "SELECT art_id, media_id, media_type, type, 'artist' as prefix, "
+ "url, song_artist.iOrder as iorder FROM art "
+ "JOIN song_artist on art.media_id = song_artist.idArtist AND art.media_type = '%s' "
+ "WHERE song_artist.idsong = %i AND song_artist.idRole = %i ",
+ MediaTypeArtist, songId, ROLE_ARTIST);
+ if (bPrimaryArtist)
+ strSQL2 += "AND song_artist.iOrder = 0";
+
+ strSQL = strSQL + " UNION " + strSQL2;
+ }
+ }
+ if (songId > 0 && albumId < 0)
+ {
+ //Album ID unknown, so get from song to look up album art
+ std::string strSQL2;
+ strSQL2 = PrepareSQL("SELECT art_id, media_id, media_type, type, '' as prefix, "
+ "url, 0 as iorder FROM art "
+ "JOIN song ON art.media_id = song.idAlbum AND art.media_type ='%s' "
+ "WHERE song.idSong = %i ",
+ MediaTypeAlbum, songId);
+ strSQL = strSQL + " UNION " + strSQL2;
+ }
+
+ m_pDS2->query(strSQL);
+ while (!m_pDS2->eof())
+ {
+ ArtForThumbLoader artitem;
+ artitem.artType = m_pDS2->fv("type").get_asString();
+ artitem.mediaType = m_pDS2->fv("media_type").get_asString();
+ artitem.prefix = m_pDS2->fv("prefix").get_asString();
+ artitem.url = m_pDS2->fv("url").get_asString();
+ int iOrder = m_pDS2->fv("iorder").get_asInt();
+ // Add order to prefix for multiple artist art for songs and albums e.g. "albumartist2"
+ if (iOrder > 0)
+ artitem.prefix += m_pDS2->fv("iorder").get_asString();
+
+ art.emplace_back(artitem);
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ return !art.empty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, strSQL);
+ }
+ return false;
+}
+
+bool CMusicDatabase::GetArtForItem(int mediaId,
+ const std::string& mediaType,
+ std::map<std::string, std::string>& art)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false; // using dataset 2 as we're likely called in loops on dataset 1
+
+ std::string sql = PrepareSQL("SELECT type,url FROM art WHERE media_id=%i AND media_type='%s'",
+ mediaId, mediaType.c_str());
+ m_pDS2->query(sql);
+ while (!m_pDS2->eof())
+ {
+ art.insert(std::make_pair(m_pDS2->fv(0).get_asString(), m_pDS2->fv(1).get_asString()));
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ return !art.empty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaId);
+ }
+ return false;
+}
+
+std::string CMusicDatabase::GetArtForItem(int mediaId,
+ const std::string& mediaType,
+ const std::string& artType)
+{
+ std::string query = PrepareSQL("SELECT url FROM art "
+ "WHERE media_id=%i AND media_type='%s' AND type='%s'",
+ mediaId, mediaType.c_str(), artType.c_str());
+ return GetSingleValue(query, m_pDS2);
+}
+
+bool CMusicDatabase::RemoveArtForItem(int mediaId,
+ const MediaType& mediaType,
+ const std::string& artType)
+{
+ return ExecuteQuery(PrepareSQL("DELETE FROM art "
+ "WHERE media_id=%i AND media_type='%s' AND type='%s'",
+ mediaId, mediaType.c_str(), artType.c_str()));
+}
+
+bool CMusicDatabase::RemoveArtForItem(int mediaId,
+ const MediaType& mediaType,
+ const std::set<std::string>& artTypes)
+{
+ bool result = true;
+ for (const auto& i : artTypes)
+ result &= RemoveArtForItem(mediaId, mediaType, i);
+
+ return result;
+}
+
+bool CMusicDatabase::GetArtTypes(const MediaType& mediaType, std::vector<std::string>& artTypes)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL =
+ PrepareSQL("SELECT DISTINCT type FROM art WHERE media_type='%s'", mediaType.c_str());
+
+ if (!m_pDS->query(strSQL))
+ return false;
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ while (!m_pDS->eof())
+ {
+ artTypes.emplace_back(m_pDS->fv(0).get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaType);
+ }
+ return false;
+}
+
+std::vector<std::string> CMusicDatabase::GetAvailableArtTypesForItem(int mediaId,
+ const MediaType& mediaType)
+{
+ CScraperUrl thumbURL;
+ if (mediaType == MediaTypeArtist)
+ {
+ CArtist artist;
+ if (GetArtist(mediaId, artist))
+ thumbURL = artist.thumbURL;
+ }
+ else if (mediaType == MediaTypeAlbum)
+ {
+ CAlbum album;
+ if (GetAlbum(mediaId, album))
+ thumbURL = album.thumbURL;
+ }
+
+ std::vector<std::string> result;
+ for (const auto& urlEntry : thumbURL.GetUrls())
+ {
+ std::string artType = urlEntry.m_aspect;
+ if (artType.empty())
+ artType = "thumb";
+ if (std::find(result.begin(), result.end(), artType) == result.end())
+ result.push_back(artType);
+ }
+ return result;
+}
+
+std::vector<CScraperUrl::SUrlEntry> CMusicDatabase::GetAvailableArtForItem(
+ int mediaId, const MediaType& mediaType, const std::string& artType)
+{
+ CScraperUrl thumbURL;
+ if (mediaType == MediaTypeArtist)
+ {
+ CArtist artist;
+ if (GetArtist(mediaId, artist))
+ thumbURL = artist.thumbURL;
+ }
+ else if (mediaType == MediaTypeAlbum)
+ {
+ CAlbum album;
+ if (GetAlbum(mediaId, album))
+ thumbURL = album.thumbURL;
+ }
+
+ std::vector<CScraperUrl::SUrlEntry> result;
+ for (auto urlEntry : thumbURL.GetUrls())
+ {
+ if (urlEntry.m_aspect.empty())
+ urlEntry.m_aspect = "thumb";
+ if (artType.empty() || urlEntry.m_aspect == artType)
+ result.push_back(urlEntry);
+ }
+ return result;
+}
+
+int CMusicDatabase::GetOrderFilter(const std::string& type,
+ const SortDescription& sorting,
+ Filter& filter)
+{
+ // Populate filter with ORDER BY clause and any extra scalar query fields needed for sort
+ int iFieldsAdded = 0;
+ filter.fields.clear(); // remove "*"
+ std::vector<std::string> orderfields;
+ std::string DESC;
+
+ if (sorting.sortOrder == SortOrderDescending)
+ DESC = " DESC";
+
+ if (sorting.sortBy == SortByRandom)
+ orderfields.emplace_back(PrepareSQL("RANDOM()")); //Adjusts styntax for MySQL
+ else
+ {
+ FieldList fields;
+ SortUtils::GetFieldsForSQLSort(type, sorting.sortBy, fields);
+ for (const auto& it : fields)
+ {
+ std::string strField;
+ if (it == FieldYear)
+ strField = "iYear";
+ else
+ strField = DatabaseUtils::GetField(it, type, DatabaseQueryPartSelect);
+ if (!strField.empty())
+ orderfields.emplace_back(strField);
+ }
+ }
+
+ // Convert field names into order by statement elements
+ for (auto& name : orderfields)
+ {
+ //Add field for adjusted name sorting using sort name and ignoring articles
+ std::string sortSQL;
+ if (StringUtils::EndsWith(name, "strArtists") || StringUtils::EndsWith(name, "strArtist"))
+ {
+ if (StringUtils::EndsWith(name, "strArtists"))
+ sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strArtistSort");
+ else
+ sortSQL = SortnameBuildSQL("artistsortname", sorting.sortAttributes, name, "strSortName");
+ if (!sortSQL.empty())
+ {
+ name = "artistsortname";
+ filter.AppendField(sortSQL); // Add artistsortname as scalar query field
+ iFieldsAdded++;
+ }
+ // Natural number case-insensitive sort
+ filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
+ }
+ else if (StringUtils::EndsWith(name, "strAlbum") || StringUtils::EndsWith(name, "strTitle"))
+ {
+ sortSQL = SortnameBuildSQL("titlesortname", sorting.sortAttributes, name, "");
+ if (!sortSQL.empty())
+ {
+ name = "titlesortname";
+ filter.AppendField(sortSQL); // Add sortname as scalar query field
+ iFieldsAdded++;
+ }
+ // Natural number case-insensitive sort
+ filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
+ }
+ else if (StringUtils::EndsWith(name, "strGenres"))
+ // Natural number case-insensitive sort
+ filter.AppendOrder(AlphanumericSortSQL(name, sorting.sortOrder));
+ else
+ filter.AppendOrder(name + DESC);
+ }
+ return iFieldsAdded;
+}
+
+bool CMusicDatabase::GetFilter(CDbUrl& musicUrl, Filter& filter, SortDescription& sorting)
+{
+ if (!musicUrl.IsValid())
+ return false;
+
+ std::string type = musicUrl.GetType();
+ const CUrlOptions::UrlOptions& options = musicUrl.GetOptions();
+
+ // Check for playlist rules first, they may contain role criteria
+ bool hasRoleRules = false;
+
+ auto option = options.find("xsp");
+ if (option != options.end())
+ {
+ CSmartPlaylist xsp;
+ if (!xsp.LoadFromJson(option->second.asString()))
+ return false;
+
+ std::set<std::string> playlists;
+ std::string xspWhere;
+ xspWhere = xsp.GetWhereClause(*this, playlists);
+ hasRoleRules = xsp.GetType() == "artists" &&
+ xspWhere.find("song_artist.idRole = role.idRole") != xspWhere.npos;
+
+ // Check if the filter playlist matches the item type
+ // Allow for grouping name like "originalyears" and type "years"
+ if (xsp.GetType() == type ||
+ (xsp.GetGroup().find(type) != std::string::npos && !xsp.IsGroupMixed()))
+ {
+ filter.AppendWhere(xspWhere);
+
+ if (xsp.GetLimit() > 0)
+ sorting.limitEnd = xsp.GetLimit();
+ if (xsp.GetOrder() != SortByNone)
+ sorting.sortBy = xsp.GetOrder();
+ sorting.sortOrder = xsp.GetOrderAscending() ? SortOrderAscending : SortOrderDescending;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sorting.sortAttributes = SortAttributeIgnoreArticle;
+ }
+ }
+
+ //Process role options, common to artist and album type filtering
+ int idRole = 1; // Default restrict song_artist to "artists" only, no other roles.
+ option = options.find("roleid");
+ if (option != options.end())
+ idRole = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("role");
+ if (option != options.end())
+ {
+ if (option->second.asString() == "all" || option->second.asString() == "%")
+ idRole = -1000; //All roles
+ else
+ idRole = GetRoleByName(option->second.asString());
+ }
+ }
+ if (hasRoleRules)
+ {
+ // Get Role from role rule(s) here.
+ // But that requires much change, so for now get all roles as better than none
+ idRole = -1000; //All roles
+ }
+
+ std::string strRoleSQL; //Role < 0 means all roles, otherwise filter by role
+ if (idRole > 0)
+ strRoleSQL = PrepareSQL(" AND song_artist.idRole = %i ", idRole);
+
+ int idArtist = -1, idGenre = -1, idAlbum = -1, idSong = -1;
+ int idDisc = -1;
+ int idSource = -1;
+ bool albumArtistsOnly = false;
+ bool useOriginalYear = false;
+ std::string artistname;
+
+ // Process useoriginalyear option, setting overridden by option
+ useOriginalYear = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE);
+ option = options.find("useoriginalyear");
+ if (option != options.end())
+ useOriginalYear = option->second.asBoolean();
+
+ // Process albumartistsonly option
+ option = options.find("albumartistsonly");
+ if (option != options.end())
+ albumArtistsOnly = option->second.asBoolean();
+
+ // Process genre option
+ option = options.find("genreid");
+ if (option != options.end())
+ idGenre = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("genre");
+ if (option != options.end())
+ idGenre = GetGenreByName(option->second.asString());
+ }
+
+ // Process source option
+ option = options.find("sourceid");
+ if (option != options.end())
+ idSource = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("source");
+ if (option != options.end())
+ idSource = GetSourceByName(option->second.asString());
+ }
+
+ // Process album option
+ option = options.find("albumid");
+ if (option != options.end())
+ idAlbum = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("album");
+ if (option != options.end())
+ idAlbum = GetAlbumByName(option->second.asString());
+ }
+
+ // Process artist option
+ option = options.find("artistid");
+ if (option != options.end())
+ idArtist = static_cast<int>(option->second.asInteger());
+ else
+ {
+ option = options.find("artist");
+ if (option != options.end())
+ {
+ idArtist = GetArtistByName(option->second.asString());
+ if (idArtist == -1)
+ { // not found with that name, or more than one found as artist name is not unique
+ artistname = option->second.asString();
+ }
+ }
+ }
+
+ // Process song option
+ option = options.find("songid");
+ if (option != options.end())
+ idSong = static_cast<int>(option->second.asInteger());
+
+ if (type == "artists")
+ {
+ if (!hasRoleRules)
+ { // Not an "artists" smart playlist with roles rules, so get filter from options
+ if (idArtist > 0)
+ filter.AppendWhere(PrepareSQL("artistview.idArtist = %d", idArtist));
+ else if (idAlbum > 0)
+ filter.AppendWhere(
+ PrepareSQL("artistview.idArtist IN (SELECT album_artist.idArtist FROM album_artist "
+ "WHERE album_artist.idAlbum = %i)",
+ idAlbum));
+ else if (idSong > 0)
+ {
+ filter.AppendWhere(
+ PrepareSQL("artistview.idArtist IN (SELECT song_artist.idArtist FROM song_artist "
+ "WHERE song_artist.idSong = %i %s)",
+ idSong, strRoleSQL.c_str()));
+ }
+ else
+ { /*
+ Process idRole, idGenre, idSource and albumArtistsOnly options
+
+ For artists these rules are combined because they apply via album and song
+ and so we need to ensure all criteria are met via the same album or song.
+ 1) Some artists may be only album artists, so for all artists (with linked
+ albums or songs) we need to check both album_artist and song_artist tables.
+ 2) Role is determined from song_artist table, so even if looking for album artists
+ only we find those that also have a specific role e.g. which album artist is a
+ composer of songs in that album, from entries in the song_artist table.
+ a) Role < -1 is used to indicate that all roles are wanted.
+ b) When not album artists only and a specific role wanted then only the song_artist
+ table is checked.
+ c) When album artists only and role = 1 (an "artist") then only the album_artist
+ table is checked.
+ */
+ std::string albumArtistSQL, songArtistSQL;
+ ExistsSubQuery albumArtistSub("album_artist",
+ "album_artist.idArtist = artistview.idArtist");
+ // Prepare album artist subquery SQL
+ if (idSource > 0)
+ {
+ if (idRole == 1 && idGenre < 0)
+ {
+ albumArtistSub.AppendJoin(
+ "JOIN album_source ON album_source.idAlbum = album_artist.idAlbum");
+ albumArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
+ }
+ else
+ {
+ albumArtistSub.AppendWhere(
+ PrepareSQL("EXISTS(SELECT 1 FROM album_source "
+ "WHERE album_source.idSource = %i "
+ "AND album_source.idAlbum = album_artist.idAlbum)",
+ idSource));
+ }
+ }
+ if (idRole <= 1 && idGenre > 0)
+ { // Check genre of songs of album using nested subquery
+ std::string strGenre =
+ PrepareSQL("EXISTS(SELECT 1 FROM song "
+ "JOIN song_genre ON song_genre.idSong = song.idSong "
+ "WHERE song.idAlbum = album_artist.idAlbum AND song_genre.idGenre = %i)",
+ idGenre);
+ albumArtistSub.AppendWhere(strGenre);
+ }
+
+ // Prepare song artist subquery SQL
+ ExistsSubQuery songArtistSub("song_artist", "song_artist.idArtist = artistview.idArtist");
+ if (idRole > 0)
+ songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
+ if (idSource > 0 && idGenre > 0 && !albumArtistsOnly && idRole >= 1)
+ {
+ songArtistSub.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song "
+ "JOIN song_genre ON song_genre.idSong = song.idSong "
+ "WHERE song.idSong = song_artist.idSong "
+ "AND song_genre.idGenre = %i "
+ "AND EXISTS(SELECT 1 FROM album_source "
+ "WHERE album_source.idSource = %i "
+ "AND album_source.idAlbum = song.idAlbum))",
+ idGenre, idSource));
+ }
+ else
+ {
+ if (idGenre > 0)
+ {
+ songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song_artist.idSong");
+ songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
+ }
+ if (idSource > 0 && !albumArtistsOnly)
+ {
+ songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
+ songArtistSub.AppendJoin("JOIN album_source ON album_source.idAlbum = song.idAlbum");
+ songArtistSub.AppendWhere(PrepareSQL("album_source.idSource = %i", idSource));
+ }
+ if (idRole > 1 && albumArtistsOnly)
+ { // Album artists only with role, check AND in album_artist for album of song
+ // using nested subquery correlated with album_artist
+ songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
+ songArtistSub.param = "song_artist.idArtist = album_artist.idArtist";
+ songArtistSub.AppendWhere("song.idAlbum = album_artist.idAlbum");
+ }
+ }
+
+ // Build filter clause from subqueries
+ if (idRole > 1 && albumArtistsOnly)
+ { // Album artists only with role, check AND in album_artist for album of song
+ // using nested subquery correlated with album_artist
+ songArtistSub.BuildSQL(songArtistSQL);
+ albumArtistSub.AppendWhere(songArtistSQL);
+ albumArtistSub.BuildSQL(albumArtistSQL);
+ filter.AppendWhere(albumArtistSQL);
+ }
+ else
+ {
+ songArtistSub.BuildSQL(songArtistSQL);
+ albumArtistSub.BuildSQL(albumArtistSQL);
+ if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
+ { // Artist contributing to songs, any role, check OR album artist too
+ // as artists can be just album artists but not song artists
+ filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
+ }
+ else if (idRole > 1)
+ {
+ // Artist contributes that role (not albmartistsonly as already handled)
+ filter.AppendWhere(songArtistSQL);
+ }
+ else // idRole = 1 and albumArtistsOnly
+ { // Only look at album artists, not albums where artist features on songs
+ filter.AppendWhere(albumArtistSQL);
+ }
+ }
+ }
+ }
+ // remove the null string
+ filter.AppendWhere("artistview.strArtist != ''");
+ }
+ else if (type == "albums")
+ {
+ option = options.find("year");
+ if (option != options.end())
+ {
+ if (!useOriginalYear)
+ filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ else
+ filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ }
+ option = options.find("compilation");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
+
+ option = options.find("boxset");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
+
+ if (idSource > 0)
+ filter.AppendWhere(PrepareSQL(
+ "EXISTS(SELECT 1 FROM album_source "
+ "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
+ idSource));
+
+ // Process artist, role and genre options together as song subquery to filter those
+ // albums that have songs with both that artist and genre
+ std::string albumArtistSQL, songArtistSQL, genreSQL;
+ ExistsSubQuery genreSub("song", "song.idAlbum = album_artist.idAlbum");
+ genreSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
+ genreSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
+ ExistsSubQuery albumArtistSub("album_artist", "album_artist.idAlbum = albumview.idAlbum");
+ ExistsSubQuery songArtistSub("song_artist", "song.idAlbum = albumview.idAlbum");
+ songArtistSub.AppendJoin("JOIN song ON song.idSong = song_artist.idSong");
+
+ if (idArtist > 0)
+ {
+ songArtistSub.AppendWhere(PrepareSQL("song_artist.idArtist = %i", idArtist));
+ albumArtistSub.AppendWhere(PrepareSQL("album_artist.idArtist = %i", idArtist));
+ }
+ else if (!artistname.empty())
+ { // Artist name is not unique, so could get albums or songs from more than one.
+ songArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
+ songArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
+
+ albumArtistSub.AppendJoin("JOIN artist ON artist.idArtist = song_artist.idArtist");
+ albumArtistSub.AppendWhere(PrepareSQL("artist.strArtist like '%s'", artistname.c_str()));
+ }
+ if (idRole > 0)
+ songArtistSub.AppendWhere(PrepareSQL("song_artist.idRole = %i", idRole));
+ if (idGenre > 0)
+ {
+ songArtistSub.AppendJoin("JOIN song_genre ON song_genre.idSong = song.idSong");
+ songArtistSub.AppendWhere(PrepareSQL("song_genre.idGenre = %i", idGenre));
+ }
+
+ if (idArtist > 0 || !artistname.empty())
+ {
+ if (idRole <= 1 && idGenre > 0)
+ { // Check genre of songs of album using nested subquery
+ genreSub.BuildSQL(genreSQL);
+ albumArtistSub.AppendWhere(genreSQL);
+ }
+ if (idRole > 1 && albumArtistsOnly)
+ { // Album artists only with role, check AND in album_artist for same song
+ // using nested subquery correlated with album_artist
+ songArtistSub.param = "song.idAlbum = album_artist.idAlbum";
+ songArtistSub.BuildSQL(songArtistSQL);
+ albumArtistSub.AppendWhere(songArtistSQL);
+ albumArtistSub.BuildSQL(albumArtistSQL);
+ filter.AppendWhere(albumArtistSQL);
+ }
+ else
+ {
+ songArtistSub.BuildSQL(songArtistSQL);
+ albumArtistSub.BuildSQL(albumArtistSQL);
+ if (idRole < 0 || (idRole == 1 && !albumArtistsOnly))
+ { // Artist contributing to songs, any role, check OR album artist too
+ // as artists can be just album artists but not song artists
+ filter.AppendWhere(songArtistSQL + " OR " + albumArtistSQL);
+ }
+ else if (idRole > 1)
+ { // Albums with songs where artist contributes that role (not albmartistsonly as already handled)
+ filter.AppendWhere(songArtistSQL);
+ }
+ else // idRole = 1 and albumArtistsOnly
+ { // Only look at album artists, not albums where artist features on songs
+ // This may want to be a separate option so you can choose to see all the albums where that artist
+ // appears on one or more songs without having to list all song artists in the artists node.
+ filter.AppendWhere(albumArtistSQL);
+ }
+ }
+ }
+ else
+ { // No artist given
+ if (idGenre > 0)
+ { // Have genre option but not artist
+ genreSub.param = "song.idAlbum = albumview.idAlbum";
+ genreSub.BuildSQL(genreSQL);
+ filter.AppendWhere(genreSQL);
+ }
+ // Exclude any single albums (aka empty tagged albums)
+ // This causes "albums" media filter artist selection to only offer album artists
+ option = options.find("show_singles");
+ if (option == options.end() || !option->second.asBoolean())
+ filter.AppendWhere(PrepareSQL("albumview.strReleaseType = '%s'",
+ CAlbum::ReleaseTypeToString(CAlbum::Album).c_str()));
+ }
+ }
+ else if (type == "discs")
+ {
+ if (idAlbum > 0)
+ filter.AppendWhere(PrepareSQL("albumview.idAlbum = %i", idAlbum));
+ else
+ {
+ option = options.find("year");
+ if (option != options.end())
+ {
+ if (!useOriginalYear)
+ filter.AppendWhere(PrepareSQL("albumview.strReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ else
+ filter.AppendWhere(PrepareSQL("albumview.strOrigReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ }
+
+ option = options.find("compilation");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("albumview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
+
+ option = options.find("boxset");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("albumview.bBoxedSet = %i", option->second.asBoolean() ? 1 : 0));
+
+ if (idSource > 0)
+ filter.AppendWhere(PrepareSQL(
+ "EXISTS(SELECT 1 FROM album_source "
+ "WHERE album_source.idAlbum = albumview.idAlbum AND album_source.idSource = %i)",
+ idSource));
+ }
+ option = options.find("discid");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("iDisc = %i", option->second.asInteger()));
+
+ option = options.find("disctitle");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("strDiscSubtitle = '%s'", option->second.asString().c_str()));
+
+ if (idGenre > 0)
+ filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM song_genre WHERE song_genre.idSong = "
+ "song.idSong AND song_genre.idGenre = %i)",
+ idGenre));
+
+ std::string songArtistClause, albumArtistClause;
+ if (idArtist > 0)
+ {
+ songArtistClause =
+ PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
+ "WHERE song_artist.idSong = song.idSong AND song_artist.idArtist = %i %s)",
+ idArtist, strRoleSQL.c_str());
+ albumArtistClause =
+ PrepareSQL("EXISTS (SELECT 1 FROM album_artist "
+ "WHERE album_artist.idAlbum = song.idAlbum AND album_artist.idArtist = %i)",
+ idArtist);
+ }
+ else if (!artistname.empty())
+ { // Artist name is not unique, so could get songs from more than one.
+ songArtistClause = PrepareSQL(
+ "EXISTS (SELECT 1 FROM song_artist JOIN artist ON artist.idArtist = song_artist.idArtist "
+ "WHERE song_artist.idSong = song.idSong AND artist.strArtist like '%s' %s)",
+ artistname.c_str(), strRoleSQL.c_str());
+ albumArtistClause =
+ PrepareSQL("EXISTS (SELECT 1 FROM album_artist JOIN artist ON artist.idArtist = "
+ "album_artist.idArtist "
+ "WHERE album_artist.idAlbum = song.idAlbum AND artist.strArtist like '%s')",
+ artistname.c_str());
+ }
+
+ // Process artist name or id option
+ if (!songArtistClause.empty())
+ {
+ if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
+ filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
+ else if (idRole > 1)
+ {
+ if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song
+ filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
+ else // songs where artist contributes that role.
+ filter.AppendWhere(songArtistClause);
+ }
+ else
+ {
+ if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
+ filter.AppendWhere(albumArtistClause);
+ else // Artist is song artist or album artist
+ filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
+ }
+ }
+ }
+ else if (type == "songs" || type == "singles")
+ {
+ option = options.find("singles");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL(
+ "songview.idAlbum %sIN (SELECT idAlbum FROM album WHERE strReleaseType = '%s')",
+ option->second.asBoolean() ? "" : "NOT ",
+ CAlbum::ReleaseTypeToString(CAlbum::Single).c_str()));
+
+ // When have idAlbum skip year, compilation, boxset criteria as already applied via album
+ if (idAlbum < 0)
+ {
+ option = options.find("year");
+ if (option != options.end())
+ {
+ if (!useOriginalYear)
+ filter.AppendWhere(PrepareSQL("songview.strReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ else
+ filter.AppendWhere(PrepareSQL("songview.strOrigReleaseDate LIKE '%s%%%%'",
+ option->second.asString().c_str()));
+ }
+ option = options.find("compilation");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("songview.bCompilation = %i", option->second.asBoolean() ? 1 : 0));
+
+ option = options.find("boxset");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("EXISTS(SELECT 1 FROM album WHERE album.idAlbum = "
+ "songview.idAlbum AND bBoxedSet = %i)",
+ option->second.asBoolean() ? 1 : 0));
+ }
+
+ option = options.find("discid");
+ if (option != options.end())
+ idDisc = static_cast<int>(option->second.asInteger());
+
+ option = options.find("disctitle");
+ if (option != options.end())
+ filter.AppendWhere(
+ PrepareSQL("songview.strDiscSubtitle = '%s'", option->second.asString().c_str()));
+
+ if (idSong > 0)
+ filter.AppendWhere(PrepareSQL("songview.idSong = %i", idSong));
+
+ if (idAlbum > 0)
+ filter.AppendWhere(PrepareSQL("songview.idAlbum = %i", idAlbum));
+
+ if (idDisc > 0)
+ filter.AppendWhere(PrepareSQL("songview.iTrack >> 16 = %i", idDisc));
+
+ if (idGenre > 0)
+ filter.AppendWhere(PrepareSQL("songview.idSong IN (SELECT song_genre.idSong FROM song_genre "
+ "WHERE song_genre.idGenre = %i)",
+ idGenre));
+
+ if (idSource > 0)
+ filter.AppendWhere(PrepareSQL(
+ "EXISTS(SELECT 1 FROM album_source "
+ "WHERE album_source.idAlbum = songview.idAlbum AND album_source.idSource = %i)",
+ idSource));
+
+ std::string songArtistClause, albumArtistClause;
+ if (idArtist > 0)
+ {
+ songArtistClause =
+ PrepareSQL("EXISTS (SELECT 1 FROM song_artist "
+ "WHERE song_artist.idSong = songview.idSong AND song_artist.idArtist = %i %s)",
+ idArtist, strRoleSQL.c_str());
+ albumArtistClause = PrepareSQL(
+ "EXISTS (SELECT 1 FROM album_artist "
+ "WHERE album_artist.idAlbum = songview.idAlbum AND album_artist.idArtist = %i)",
+ idArtist);
+ }
+ else if (!artistname.empty())
+ { // Artist name is not unique, so could get songs from more than one.
+ songArtistClause = PrepareSQL(
+ "EXISTS (SELECT 1 FROM song_artist "
+ "JOIN artist ON artist.idArtist = song_artist.idArtist "
+ "WHERE song_artist.idSong = songview.idSong AND artist.strArtist like '%s' %s)",
+ artistname.c_str(), strRoleSQL.c_str());
+ albumArtistClause = PrepareSQL(
+ "EXISTS (SELECT 1 FROM album_artist "
+ "JOIN artist ON artist.idArtist = album_artist.idArtist "
+ "WHERE album_artist.idAlbum = songview.idAlbum AND artist.strArtist like '%s')",
+ artistname.c_str());
+ }
+
+ // Process artist name or id option
+ if (!songArtistClause.empty())
+ {
+ if (idRole < 0) // Artist contributes to songs, any roles OR is album artist
+ filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
+ else if (idRole > 1)
+ {
+ if (albumArtistsOnly) //Album artists only with role, check AND in album_artist for same song
+ filter.AppendWhere("(" + songArtistClause + " AND " + albumArtistClause + ")");
+ else // songs where artist contributes that role.
+ filter.AppendWhere(songArtistClause);
+ }
+ else
+ {
+ if (albumArtistsOnly) // Only look at album artists, not where artist features on songs
+ filter.AppendWhere(albumArtistClause);
+ else // Artist is song artist or album artist
+ filter.AppendWhere("(" + songArtistClause + " OR " + albumArtistClause + ")");
+ }
+ }
+ }
+
+ option = options.find("filter");
+ if (option != options.end())
+ {
+ CSmartPlaylist xspFilter;
+ if (!xspFilter.LoadFromJson(option->second.asString()))
+ return false;
+
+ // check if the filter playlist matches the item type
+ if (xspFilter.GetType() == type)
+ {
+ std::set<std::string> playlists;
+ filter.AppendWhere(xspFilter.GetWhereClause(*this, playlists));
+ }
+ // remove the filter if it doesn't match the item type
+ else
+ musicUrl.RemoveOption("filter");
+ }
+
+ return true;
+}
+
+std::string CMusicDatabase::GetMediaDateFromFile(const std::string& strFileNameAndPath)
+{
+ if (strFileNameAndPath.empty())
+ return std::string();
+
+ CDateTime dateMedia;
+ int code;
+ code = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iMusicLibraryDateAdded;
+ // 1 using the files mtime (if valid) and only using the ctime if the mtime isn't valid
+ if (code == 1)
+ dateMedia = CFileUtils::GetModificationDate(0, strFileNameAndPath);
+ //2 using the newer datetime of the file's mtime and ctime
+ else if (code == 2)
+ dateMedia = CFileUtils::GetModificationDate(1, strFileNameAndPath);
+ //3 using the older datetime of the file's mtime and ctime
+ else if (code == 3)
+ dateMedia = CFileUtils::GetModificationDate(2, strFileNameAndPath);
+ //0 using the current datetime if none of the above matches or one returns an invalid datetime
+ if (!dateMedia.IsValid())
+ dateMedia = CDateTime::GetCurrentDateTime();
+
+ return dateMedia.GetAsDBDateTime();
+}
+
+bool CMusicDatabase::AddAudioBook(const CFileItem& item)
+{
+ auto const& artists = item.GetMusicInfoTag()->GetArtist();
+ std::string strSQL = PrepareSQL(
+ "INSERT INTO audiobook (idBook,strBook,strAuthor,bookmark,file,dateAdded) "
+ "VALUES (NULL,'%s','%s',%i,'%s','%s')",
+ item.GetMusicInfoTag()->GetAlbum().c_str(), artists.empty() ? "" : artists[0].c_str(), 0,
+ item.GetDynPath().c_str(), CDateTime::GetCurrentDateTime().GetAsDBDateTime().c_str());
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::SetResumeBookmarkForAudioBook(const CFileItem& item, int bookmark)
+{
+ std::string strSQL = PrepareSQL("SELECT bookmark FROM audiobook "
+ "WHERE file='%s'",
+ item.GetDynPath().c_str());
+ if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
+ {
+ if (!AddAudioBook(item))
+ return false;
+ }
+
+ strSQL = PrepareSQL("UPDATE audiobook SET bookmark=%i "
+ "WHERE file='%s'",
+ bookmark, item.GetDynPath().c_str());
+
+ return ExecuteQuery(strSQL);
+}
+
+bool CMusicDatabase::GetResumeBookmarkForAudioBook(const CFileItem& item, int& bookmark)
+{
+ std::string strSQL =
+ PrepareSQL("SELECT bookmark FROM audiobook WHERE file='%s'", item.GetDynPath().c_str());
+ if (!m_pDS->query(strSQL.c_str()) || m_pDS->num_rows() == 0)
+ return false;
+
+ bookmark = m_pDS->fv(0).get_asInt();
+ return true;
+}
diff --git a/xbmc/music/MusicDatabase.h b/xbmc/music/MusicDatabase.h
new file mode 100644
index 0000000..8544eb2
--- /dev/null
+++ b/xbmc/music/MusicDatabase.h
@@ -0,0 +1,1157 @@
+/*
+ * 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
+
+/*!
+ \file MusicDatabase.h
+\brief
+*/
+
+#include "Album.h"
+#include "MediaSource.h"
+#include "addons/Scraper.h"
+#include "dbwrappers/Database.h"
+#include "settings/LibExportSettings.h"
+#include "utils/SortUtils.h"
+
+#include <utility>
+#include <vector>
+
+class CArtist;
+class CFileItem;
+class CMusicDbUrl;
+
+namespace dbiplus
+{
+class field_value;
+typedef std::vector<field_value> sql_record;
+} // namespace dbiplus
+
+#include <set>
+#include <string>
+
+// return codes of Cleaning up the Database
+// numbers are strings from strings.po
+#define ERROR_OK 317
+#define ERROR_CANCEL 0
+#define ERROR_DATABASE 315
+#define ERROR_REORG_SONGS 319
+#define ERROR_REORG_ARTIST 321
+#define ERROR_REORG_OTHER 323
+#define ERROR_REORG_PATH 325
+#define ERROR_REORG_ALBUM 327
+#define ERROR_WRITING_CHANGES 329
+#define ERROR_COMPRESSING 332
+
+#define NUM_SONGS_BEFORE_COMMIT 500
+
+/*!
+ \ingroup music
+ \brief A set of std::string objects, used for CMusicDatabase
+ \sa ISETPATHS, CMusicDatabase
+ */
+typedef std::set<std::string> SETPATHS;
+
+/*!
+ \ingroup music
+ \brief The SETPATHS iterator
+ \sa SETPATHS, CMusicDatabase
+ */
+typedef std::set<std::string>::iterator ISETPATHS;
+
+/*!
+\ingroup music
+\brief A structure used for fetching music art data
+\sa CMusicDatabase::GetArtForItem()
+*/
+
+typedef struct
+{
+ std::string mediaType;
+ std::string artType;
+ std::string prefix;
+ std::string url;
+} ArtForThumbLoader;
+
+class CGUIDialogProgress;
+class CFileItemList;
+
+/*!
+ \ingroup music
+ \brief Class to store and read tag information
+
+ CMusicDatabase can be used to read and store
+ tag information for faster access. It is based on
+ sqlite (http://www.sqlite.org).
+
+ Here is the database layout:
+ \image html musicdatabase.png
+
+ \sa CAlbum, CSong, VECSONGS, CMapSong, VECARTISTS, VECALBUMS, VECGENRES
+ */
+class CMusicDatabase : public CDatabase
+{
+ friend class DatabaseUtils;
+ friend class TestDatabaseUtilsHelper;
+
+public:
+ CMusicDatabase(void);
+ ~CMusicDatabase(void) override;
+
+ bool Open() override;
+ bool CommitTransaction() override;
+ void EmptyCache();
+ void Clean();
+ int Cleanup(CGUIDialogProgress* progressDialog = nullptr);
+ bool LookupCDDBInfo(bool bRequery = false);
+ void DeleteCDDBInfo();
+
+ /////////////////////////////////////////////////
+ // Song CRUD
+ /////////////////////////////////////////////////
+ /*! \brief Add a song to the database
+ \param idSong [in] the original database ID of the song to reuse (-1 when new)
+ \param dtDateNew [in] the datetime the original ID was new
+ \param idAlbum [in] the database ID of the album for the song
+ \param strTitle [in] the title of the song (required to be non-empty)
+ \param strMusicBrainzTrackID [in] the MusicBrainz track ID of the song
+ \param strPathAndFileName [in] the path and filename to the song
+ \param strComment [in] the ids of the added songs
+ \param strMood [in] the mood of the added song
+ \param strThumb [in] the ids of the added songs
+ \param artistDisp [in] the assembled artist name(s) display string
+ \param artistSort [in] the artist name(s) sort string
+ \param genres [in] a vector of genres to which this song belongs
+ \param iTrack [in] the track number and disc number of the song
+ \param iDuration [in] the duration of the song
+ \param strReleaseDate [in] the release date of the song ISO8601 format
+ \param strOrigReleaseDate [in] the original release date of the song ISO8601 format
+ \param strDiscSubtitle [in] subtitle of a disc
+ \param iTimesPlayed [in] the number of times the song has been played
+ \param iStartOffset [in] the start offset of the song (when using a single audio file with a .cue)
+ \param iEndOffset [in] the end offset of the song (when using a single audio file with .cue)
+ \param dtLastPlayed [in] the time the song was last played
+ \param rating [in] a rating for the song
+ \param userrating [in] a userrating (my rating) for the song
+ \param votes [in] a vote counter for the song rating
+ \param replayGain [in] album and track replaygain and peak values
+ \return the id of the song
+ */
+ int AddSong(const int idSong,
+ const CDateTime& dtDateNew,
+ const int idAlbum,
+ const std::string& strTitle,
+ const std::string& strMusicBrainzTrackID,
+ const std::string& strPathAndFileName,
+ const std::string& strComment,
+ const std::string& strMood,
+ const std::string& strThumb,
+ const std::string& artistDisp,
+ const std::string& artistSort,
+ const std::vector<std::string>& genres,
+ int iTrack,
+ int iDuration,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ std::string& strDiscSubtitle,
+ const int iTimesPlayed,
+ int iStartOffset,
+ int iEndOffset,
+ const CDateTime& dtLastPlayed,
+ float rating,
+ int userrating,
+ int votes,
+ int iBPM,
+ int iBitRate,
+ int iSampleRate,
+ int iChannels,
+ const ReplayGain& replayGain);
+ bool GetSong(int idSong, CSong& song);
+
+ /*! \brief Update a song and all its nested entities (genres, artists, contributors)
+ \param song [in/out] the song to update, artist ids are returned in artist credits
+ \param bArtists to update artist credits and contributors, default is true
+ \param bArtists to check and log if artist links have changed, default is true
+ \return true if successful
+ */
+ bool UpdateSong(CSong& song, bool bArtists = true, bool bArtistLinks = true);
+
+ /*! \brief Update a song in the database
+ \param idSong [in] the database ID of the song to update
+ \param strTitle [in] the title of the song (required to be non-empty)
+ \param strMusicBrainzTrackID [in] the MusicBrainz track ID of the song
+ \param strPathAndFileName [in] the path and filename to the song
+ \param strComment [in] the ids of the added songs
+ \param strMood [in] the mood of the added song
+ \param strThumb [in] the ids of the added songs
+ \param artistDisp [in] the artist name(s) display string
+ \param artistSort [in] the artist name(s) sort string
+ \param genres [in] a vector of genres to which this song belongs
+ \param iTrack [in] the track number and disc number of the song
+ \param iDuration [in] the duration of the song
+ \param strReleaseDate [in] the release date of the song ISO8601 format
+ \param strOrigReleaseDate [in] the original release date of the song ISO8601 format
+ \param strDiscSubtitle [in] subtitle of a disc
+ \param iTimesPlayed [in] the number of times the song has been played
+ \param iStartOffset [in] the start offset of the song (when using a single audio file with a .cue)
+ \param iEndOffset [in] the end offset of the song (when using a single audio file with .cue)
+ \param dtLastPlayed [in] the time the song was last played
+ \param rating [in] a rating for the song
+ \param userrating [in] a userrating (my rating) for the song
+ \param votes [in] a vote counter for the song rating
+ \param replayGain [in] album and track replaygain and peak values
+ \param iBPM [in] the beats per minute of a song
+ \param iBitRate [in] the bitrate of the song file
+ \param iSampleRate [in] the sample rate of the song file
+ \param iChannels [in] the number of audio channels in the song file
+ \return the id of the song
+ */
+ int UpdateSong(int idSong,
+ const std::string& strTitle,
+ const std::string& strMusicBrainzTrackID,
+ const std::string& strPathAndFileName,
+ const std::string& strComment,
+ const std::string& strMood,
+ const std::string& strThumb,
+ const std::string& artistDisp,
+ const std::string& artistSort,
+ const std::vector<std::string>& genres,
+ int iTrack,
+ int iDuration,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ const std::string& strDiscSubtitle,
+ int iTimesPlayed,
+ int iStartOffset,
+ int iEndOffset,
+ const CDateTime& dtLastPlayed,
+ float rating,
+ int userrating,
+ int votes,
+ const ReplayGain& replayGain,
+ int iBPM,
+ int iBitRate,
+ int iSampleRate,
+ int iChannels);
+
+ //// Misc Song
+ bool GetSongByFileName(const std::string& strFileName, CSong& song, int64_t startOffset = 0);
+ bool GetSongsByPath(const std::string& strPath, MAPSONGS& songmap, bool bAppendToMap = false);
+ bool Search(const std::string& search, CFileItemList& items);
+ bool RemoveSongsFromPath(const std::string& path, MAPSONGS& songmap, bool exact = true);
+ void CheckArtistLinksChanged();
+ bool SetSongUserrating(const std::string& filePath, int userrating);
+ bool SetSongUserrating(int idSong, int userrating);
+ bool SetSongVotes(const std::string& filePath, int votes);
+ int GetSongByArtistAndAlbumAndTitle(const std::string& strArtist,
+ const std::string& strAlbum,
+ const std::string& strTitle);
+
+ /////////////////////////////////////////////////
+ // Album
+ /////////////////////////////////////////////////
+ /*! \brief Add an album and all its songs to the database
+ \param album the album to add
+ \param idSource the music source id
+ \return the id of the album
+ */
+ bool AddAlbum(CAlbum& album, int idSource);
+
+ /*! \brief Update an album and all its nested entities (artists, songs etc)
+ \param album the album to update
+ \return true or false
+ */
+ bool UpdateAlbum(CAlbum& album);
+
+ /*! \brief Add an album to the database
+ \param strAlbum the album title
+ \param strMusicBrainzAlbumID the Musicbrainz Id
+ \param strArtist the album artist name(s) display string
+ \param strArtistSort the album artist name(s) sort string
+ \param strGenre the album genre(s)
+ \param strReleaseDate [in] the release date of the album ISO8601 format
+ \param strOrigReleaseDate [in] the original release date of the album ISO8601 format
+ \param bBoxedSet if the album is a boxset
+ \param strRecordLabel the recording label
+ \param strType album type (Musicbrainz release type e.g. "Broadcast, Soundtrack, live"),
+ \param strReleaseStatus (see https://musicbrainz.org/doc/Release#Status)
+ \param bCompilation if the album is a compilation
+ \param releaseType "album" or "single"
+ \return the id of the album
+ */
+ int AddAlbum(const std::string& strAlbum,
+ const std::string& strMusicBrainzAlbumID,
+ const std::string& strReleaseGroupMBID,
+ const std::string& strArtist,
+ const std::string& strArtistSort,
+ const std::string& strGenre,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ bool bBoxedSet,
+ const std::string& strRecordLabel,
+ const std::string& strType,
+ const std::string& strReleaseStatus,
+ bool bCompilation,
+ CAlbum::ReleaseType releaseType);
+
+ /*! \brief retrieve an album, optionally with all songs.
+ \param idAlbum the database id of the album.
+ \param album [out] the album to fill.
+ \param getSongs whether or not to retrieve songs, defaults to true.
+ \return true if the album is retrieved, false otherwise.
+ */
+ bool GetAlbum(int idAlbum, CAlbum& album, bool getSongs = true);
+ int UpdateAlbum(int idAlbum,
+ const std::string& strAlbum,
+ const std::string& strMusicBrainzAlbumID,
+ const std::string& strReleaseGroupMBID,
+ const std::string& strArtist,
+ const std::string& strArtistSort,
+ const std::string& strGenre,
+ const std::string& strMoods,
+ const std::string& strStyles,
+ const std::string& strThemes,
+ const std::string& strReview,
+ const std::string& strImage,
+ const std::string& strLabel,
+ const std::string& strType,
+ const std::string& strReleaseStatus,
+ float fRating,
+ int iUserrating,
+ int iVotes,
+ const std::string& strReleaseDate,
+ const std::string& strOrigReleaseDate,
+ bool bBoxedSet,
+ bool bCompilation,
+ CAlbum::ReleaseType releaseType,
+ bool bScrapedMBID);
+ bool ClearAlbumLastScrapedTime(int idAlbum);
+ bool HasAlbumBeenScraped(int idAlbum);
+
+ /////////////////////////////////////////////////
+ // Audiobook
+ /////////////////////////////////////////////////
+ bool AddAudioBook(const CFileItem& item);
+ bool SetResumeBookmarkForAudioBook(const CFileItem& item, int bookmark);
+ bool GetResumeBookmarkForAudioBook(const CFileItem& item, int& bookmark);
+
+ /*! \brief Checks if the given path is inside a folder that has already been scanned into the library
+ \param path the path we want to check
+ */
+ bool InsideScannedPath(const std::string& path);
+
+ //// Misc Album
+ int GetAlbumIdByPath(const std::string& path);
+ bool GetAlbumFromSong(int idSong, CAlbum& album);
+ int GetAlbumByName(const std::string& strAlbum, const std::string& strArtist = "");
+ int GetAlbumByName(const std::string& strAlbum, const std::vector<std::string>& artist);
+ bool GetMatchingMusicVideoAlbum(const std::string& strAlbum,
+ const std::string& strArtist,
+ int& idAlbum,
+ std::string& strReview);
+ bool SearchAlbumsByArtistName(const std::string& strArtist, CFileItemList& items);
+ int GetAlbumByMatch(const CAlbum& album);
+ std::string GetAlbumById(int id);
+ std::string GetAlbumDiscTitle(int idAlbum, int idDisc);
+ bool SetAlbumUserrating(const int idAlbum, int userrating);
+ int GetAlbumDiscsCount(int idAlbum);
+
+ /////////////////////////////////////////////////
+ // Artist CRUD
+ /////////////////////////////////////////////////
+ bool UpdateArtist(const CArtist& artist);
+
+ int AddArtist(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ const std::string& strSortName,
+ bool bScrapedMBID = false);
+ int AddArtist(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ bool bScrapedMBID = false);
+ bool GetArtist(int idArtist, CArtist& artist, bool fetchAll = false);
+ bool GetArtistExists(int idArtist);
+ int GetLastArtist();
+ int GetArtistFromMBID(const std::string& strMusicBrainzArtistID, std::string& artistname);
+ int UpdateArtist(int idArtist,
+ const std::string& strArtist,
+ const std::string& strSortName,
+ const std::string& strMusicBrainzArtistID,
+ bool bScrapedMBID,
+ const std::string& strType,
+ const std::string& strGender,
+ const std::string& strDisambiguation,
+ const std::string& strBorn,
+ const std::string& strFormed,
+ const std::string& strGenres,
+ const std::string& strMoods,
+ const std::string& strStyles,
+ const std::string& strInstruments,
+ const std::string& strBiography,
+ const std::string& strDied,
+ const std::string& strDisbanded,
+ const std::string& strYearsActive,
+ const std::string& strImage);
+ bool UpdateArtistScrapedMBID(int idArtist, const std::string& strMusicBrainzArtistID);
+ bool GetTranslateBlankArtist() { return m_translateBlankArtist; }
+ void SetTranslateBlankArtist(bool translate) { m_translateBlankArtist = translate; }
+ bool HasArtistBeenScraped(int idArtist);
+ bool ClearArtistLastScrapedTime(int idArtist);
+ int AddArtistDiscography(int idArtist, const CDiscoAlbum& discoAlbum);
+ bool DeleteArtistDiscography(int idArtist);
+ bool GetArtistDiscography(int idArtist, CFileItemList& items);
+
+ std::string GetArtistById(int id);
+ int GetArtistByName(const std::string& strArtist);
+ int GetArtistByMatch(const CArtist& artist);
+ bool GetArtistFromSong(int idSong, CArtist& artist);
+ bool IsSongArtist(int idSong, int idArtist);
+ bool IsSongAlbumArtist(int idSong, int idArtist);
+ std::string GetRoleById(int id);
+
+ /*! \brief Propagate artist sort name into the concatenated artist sort name strings
+ held for songs and albums
+ \param int idArtist to propagate sort name for, -1 means all artists
+ */
+ bool UpdateArtistSortNames(int idArtist = -1);
+
+ /////////////////////////////////////////////////
+ // Paths
+ /////////////////////////////////////////////////
+ int AddPath(const std::string& strPath);
+
+ bool GetPaths(std::set<std::string>& paths);
+ bool SetPathHash(const std::string& path, const std::string& hash);
+ bool GetPathHash(const std::string& path, std::string& hash);
+ bool GetAlbumPaths(int idAlbum, std::vector<std::pair<std::string, int>>& paths);
+ bool GetAlbumPath(int idAlbum, std::string& basePath);
+ int GetDiscnumberForPathID(int idPath);
+ bool GetOldArtistPath(int idArtist, std::string& path);
+ bool GetArtistPath(const CArtist& artist, std::string& path);
+ bool GetAlbumFolder(const CAlbum& album, const std::string& strAlbumPath, std::string& strFolder);
+ bool GetArtistFolderName(const CArtist& artist, std::string& strFolder);
+ bool GetArtistFolderName(const std::string& strArtist,
+ const std::string& strMusicBrainzArtistID,
+ std::string& strFolder);
+
+ /////////////////////////////////////////////////
+ // Sources
+ /////////////////////////////////////////////////
+ bool UpdateSources();
+ int AddSource(const std::string& strName,
+ const std::string& strMultipath,
+ const std::vector<std::string>& vecPaths,
+ int id = -1);
+ int UpdateSource(const std::string& strOldName,
+ const std::string& strName,
+ const std::string& strMultipath,
+ const std::vector<std::string>& vecPaths);
+ bool RemoveSource(const std::string& strName);
+ int GetSourceFromPath(const std::string& strPath);
+ bool AddAlbumSource(int idAlbum, int idSource);
+ bool AddAlbumSources(int idAlbum, const std::string& strPath);
+ bool DeleteAlbumSources(int idAlbum);
+ bool GetSources(CFileItemList& items);
+
+ bool GetSourcesByArtist(int idArtist, CFileItem* item);
+ bool GetSourcesByAlbum(int idAlbum, CFileItem* item);
+ bool GetSourcesBySong(int idSong, const std::string& strPath, CFileItem* item);
+ int GetSourceByName(const std::string& strSource);
+ std::string GetSourceById(int id);
+
+ /////////////////////////////////////////////////
+ // Genres
+ /////////////////////////////////////////////////
+ int AddGenre(std::string& strGenre);
+ std::string GetGenreById(int id);
+ int GetGenreByName(const std::string& strGenre);
+
+ /////////////////////////////////////////////////
+ // Link tables
+ /////////////////////////////////////////////////
+ bool AddAlbumArtist(int idArtist, int idAlbum, const std::string& strArtist, int iOrder);
+ bool GetAlbumsByArtist(int idArtist, std::vector<int>& albums);
+ bool GetArtistsByAlbum(int idAlbum, CFileItem* item);
+ bool GetArtistsByAlbum(int idAlbum, std::vector<std::string>& artistIDs);
+ bool DeleteAlbumArtistsByAlbum(int idAlbum);
+
+ int AddRole(const std::string& strRole);
+ bool AddSongArtist(int idArtist,
+ int idSong,
+ const std::string& strRole,
+ const std::string& strArtist,
+ int iOrder);
+ bool AddSongArtist(
+ int idArtist, int idSong, int idRole, const std::string& strArtist, int iOrder);
+ int AddSongContributor(int idSong,
+ const std::string& strRole,
+ const std::string& strArtist,
+ const std::string& strSort);
+ void AddSongContributors(int idSong,
+ const VECMUSICROLES& contributors,
+ const std::string& strSort);
+ int GetRoleByName(const std::string& strRole);
+ bool GetRolesByArtist(int idArtist, CFileItem* item);
+ bool GetSongsByArtist(int idArtist, std::vector<int>& songs);
+ bool GetArtistsBySong(int idSong, std::vector<int>& artists);
+ bool DeleteSongArtistsBySong(int idSong);
+
+ bool AddSongGenres(int idSong, const std::vector<std::string>& genres);
+ bool GetGenresBySong(int idSong, std::vector<int>& genres);
+
+ bool GetGenresByAlbum(int idAlbum, CFileItem* item);
+
+ bool GetGenresByArtist(int idArtist, CFileItem* item);
+ bool GetIsAlbumArtist(int idArtist, CFileItem* item);
+
+ /////////////////////////////////////////////////
+ // Top 100
+ /////////////////////////////////////////////////
+ bool GetTop100(const std::string& strBaseDir, CFileItemList& items);
+ bool GetTop100Albums(VECALBUMS& albums);
+ bool GetTop100AlbumSongs(const std::string& strBaseDir, CFileItemList& item);
+
+ /////////////////////////////////////////////////
+ // Recently added
+ /////////////////////////////////////////////////
+ bool GetRecentlyAddedAlbums(VECALBUMS& albums, unsigned int limit = 0);
+ bool GetRecentlyAddedAlbumSongs(const std::string& strBaseDir,
+ CFileItemList& item,
+ unsigned int limit = 0);
+ bool GetRecentlyPlayedAlbums(VECALBUMS& albums);
+ bool GetRecentlyPlayedAlbumSongs(const std::string& strBaseDir, CFileItemList& item);
+
+ /////////////////////////////////////////////////
+ // Compilations
+ /////////////////////////////////////////////////
+ int GetCompilationAlbumsCount();
+
+ ////////////////////////////////////////////////
+ // Boxsets
+ ////////////////////////////////////////////////
+ bool IsAlbumBoxset(int idAlbum);
+ int GetBoxsetsCount();
+
+ int GetSinglesCount();
+
+ int GetArtistCountForRole(int role);
+ int GetArtistCountForRole(const std::string& strRole);
+
+ /*! \brief Increment the playcount of an item
+ Increments the playcount and updates the last played date
+ \param item CFileItem to increment the playcount for
+ */
+ void IncrementPlayCount(const CFileItem& item);
+ bool CleanupOrphanedItems();
+
+ /////////////////////////////////////////////////
+ // VIEWS
+ /////////////////////////////////////////////////
+ bool GetGenresNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetSourcesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetYearsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter());
+ bool GetRolesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter());
+ bool GetArtistsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ bool albumArtistsOnly = false,
+ int idGenre = -1,
+ int idAlbum = -1,
+ int idSong = -1,
+ const Filter& filter = Filter(),
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetCommonNav(const std::string& strBaseDir,
+ const std::string& table,
+ const std::string& labelField,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */);
+ bool GetAlbumTypesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetMusicLabelsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetAlbumsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idGenre = -1,
+ int idArtist = -1,
+ const Filter& filter = Filter(),
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetDiscsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idAlbum,
+ const Filter& filter = Filter(),
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetAlbumsByYear(const std::string& strBaseDir, CFileItemList& items, int year);
+ bool GetSongsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ int idGenre,
+ int idArtist,
+ int idAlbum,
+ const SortDescription& sortDescription = SortDescription());
+ bool GetSongsByYear(const std::string& baseDir, CFileItemList& items, int year);
+ bool GetSongsByWhere(const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription());
+ bool GetSongsFullByWhere(const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription(),
+ bool artistData = false);
+ bool GetAlbumsByWhere(const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetDiscsByWhere(const std::string& baseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetDiscsByWhere(CMusicDbUrl& musicUrl,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ bool GetArtistsByWhere(const std::string& strBaseDir,
+ const Filter& filter,
+ CFileItemList& items,
+ const SortDescription& sortDescription = SortDescription(),
+ bool countOnly = false);
+ int GetDiscsCount(const std::string& baseDir, const Filter& filter = Filter());
+ int GetSongsCount(const Filter& filter = Filter());
+ bool GetFilter(CDbUrl& musicUrl, Filter& filter, SortDescription& sorting) override;
+ int GetOrderFilter(const std::string& type, const SortDescription& sorting, Filter& filter);
+
+ /////////////////////////////////////////////////
+ // Party Mode
+ /////////////////////////////////////////////////
+ /*! \brief Gets song IDs in random order that match the filter criteria
+ \param filter the criteria to apply in the query
+ \param songIDs a vector of <1, id> pairs suited to party mode use
+ \return count of song ids found.
+ */
+ unsigned int GetRandomSongIDs(const Filter& filter, std::vector<std::pair<int, int>>& songIDs);
+
+ /////////////////////////////////////////////////
+ // JSON-RPC
+ /////////////////////////////////////////////////
+ bool GetGenresJSON(CFileItemList& items, bool bSources = false);
+ bool GetArtistsByWhereJSON(const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription = SortDescription());
+ bool GetAlbumsByWhereJSON(const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription = SortDescription());
+ bool GetSongsByWhereJSON(const std::set<std::string>& fields,
+ const std::string& baseDir,
+ CVariant& result,
+ int& total,
+ const SortDescription& sortDescription = SortDescription());
+
+ /////////////////////////////////////////////////
+ // Scraper
+ /////////////////////////////////////////////////
+ bool SetScraper(int id, const CONTENT_TYPE& content, const ADDON::ScraperPtr& scraper);
+ bool SetScraperAll(const std::string& strBaseDir, const ADDON::ScraperPtr& scraper);
+ bool GetScraper(int id, const CONTENT_TYPE& content, ADDON::ScraperPtr& scraper);
+
+ /*! \brief Check whether a given scraper is in use.
+ \param scraperID the scraper to check for.
+ \return true if the scraper is in use, false otherwise.
+ */
+ bool ScraperInUse(const std::string& scraperID) const;
+
+ /////////////////////////////////////////////////
+ // Filters
+ /////////////////////////////////////////////////
+ bool GetItems(const std::string& strBaseDir,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ const SortDescription& sortDescription = SortDescription());
+ bool GetItems(const std::string& strBaseDir,
+ const std::string& itemType,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ const SortDescription& sortDescription = SortDescription());
+ std::string GetItemById(const std::string& itemType, int id);
+
+ /////////////////////////////////////////////////
+ // XML
+ /////////////////////////////////////////////////
+ void ExportToXML(const CLibExportSettings& settings,
+ CGUIDialogProgress* progressDialog = nullptr);
+ bool ExportSongHistory(TiXmlNode* pNode, CGUIDialogProgress* progressDialog = nullptr);
+ void ImportFromXML(const std::string& xmlFile, CGUIDialogProgress* progressDialog = nullptr);
+ bool ImportSongHistory(const std::string& xmlFile,
+ const int total,
+ CGUIDialogProgress* progressDialog = nullptr);
+
+ /////////////////////////////////////////////////
+ // Properties
+ /////////////////////////////////////////////////
+ void SetPropertiesForFileItem(CFileItem& item);
+ static void SetPropertiesFromArtist(CFileItem& item, const CArtist& artist);
+ static void SetPropertiesFromAlbum(CFileItem& item, const CAlbum& album);
+ void SetItemUpdated(int mediaId, const std::string& mediaType);
+
+ /////////////////////////////////////////////////
+ // Art
+ /////////////////////////////////////////////////
+ /*! \brief Sets art for a database item.
+ Sets a single piece of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param artType the type of art to set, e.g. "thumb", "fanart"
+ \param url the url to the art (this is the original url, not a cached url).
+ \sa GetArtForItem
+ */
+ void SetArtForItem(int mediaId,
+ const std::string& mediaType,
+ const std::string& artType,
+ const std::string& url);
+
+ /*! \brief Sets art for a database item.
+ Sets multiple pieces of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param art a map of <type, url> where type is "thumb", "fanart", etc. and url is the original url of the art.
+ \sa GetArtForItem
+ */
+ void SetArtForItem(int mediaId,
+ const std::string& mediaType,
+ const std::map<std::string, std::string>& art);
+
+
+ /*! \brief Fetch all related art for a database item.
+ Fetches multiple pieces of art for a database item including that for related media types
+ Given song id art for the related album, artist(s) and albumartist(s) will also be fetched, looking up the
+ album and artist when ids are not provided.
+ Given album id (and not song id) art for the related artist(s) will also be fetched, looking up the
+ artist(s) when id are not provided.
+ \param songId the id in the song table, -1 when song art not being fetched
+ \param albumId the id in the album table, -1 when album art not being fetched
+ \param artistId the id in the artist table, -1 when artist not known
+ \param bPrimaryArtist true if art from only the first song artist or album artist is to be fetched
+ \param art [out] a vector, each element having media type e.g. "artist", "album" or "song",
+ artType e.g. "thumb", "fanart", etc., prefix of "", "artist" or "albumartist" etc. giving the kind of artist
+ relationship, and the original url of the art.
+
+ \return true if art is retrieved, false if no art is found.
+ \sa SetArtForItem
+ */
+ bool GetArtForItem(int songId,
+ int albumId,
+ int artistId,
+ bool bPrimaryArtist,
+ std::vector<ArtForThumbLoader>& art);
+
+ /*! \brief Fetch art for a database item.
+ Fetches multiple pieces of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param art [out] a map of <type, url> where type is "thumb", "fanart", etc. and url is the original url of the art.
+ \return true if art is retrieved, false if no art is found.
+ \sa SetArtForItem
+ */
+ bool GetArtForItem(int mediaId,
+ const std::string& mediaType,
+ std::map<std::string, std::string>& art);
+
+ /*! \brief Fetch art for a database item.
+ Fetches a single piece of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param artType the type of art to retrieve, eg "thumb", "fanart".
+ \return the original URL to the piece of art, if available.
+ \sa SetArtForItem
+ */
+ std::string GetArtForItem(int mediaId, const std::string& mediaType, const std::string& artType);
+
+ /*! \brief Remove art for a database item.
+ Removes a single piece of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param artType the type of art to remove, eg "thumb", "fanart".
+ \return true if art is removed, false if no art is found.
+ \sa RemoveArtForItem
+ */
+ bool RemoveArtForItem(int mediaId, const MediaType& mediaType, const std::string& artType);
+
+ /*! \brief Remove art for a database item.
+ Removes multiple pieces of art for a database item.
+ \param mediaId the id in the media (song/artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param arttypes a set of types, e.g. "thumb", "fanart", etc. to be removed.
+ \return true if art is removed, false if no art is found.
+ \sa RemoveArtForItem
+ */
+ bool RemoveArtForItem(int mediaId,
+ const MediaType& mediaType,
+ const std::set<std::string>& artTypes);
+
+ /*! \brief Fetch the distinct types of art held in the database for a type of media.
+ \param mediaType the type of media, which corresponds to the table the item resides in (song/artist/album).
+ \param artTypes [out] the types of art e.g. "thumb", "fanart", etc.
+ \return true if art is found, false if no art is found.
+ */
+ bool GetArtTypes(const MediaType& mediaType, std::vector<std::string>& artTypes);
+
+ /*! \brief Fetch the distinct types of available-but-unassigned art held in the
+ database for a specific media item.
+ \param mediaId the id in the media (artist/album) table.
+ \param mediaType the type of media, which corresponds to the table the item resides in (artist/album).
+ \return the types of art e.g. "thumb", "fanart", etc.
+ */
+ std::vector<std::string> GetAvailableArtTypesForItem(int mediaId, const MediaType& mediaType);
+
+ /*! \brief Fetch the list of available-but-unassigned art URLs held in the
+ database for a specific media item and art type.
+ \param mediaId the id in the media (artist/album) table.
+ \param mediaType corresponds to the table the item resides in (artist/album).
+ \param artType e.g. "thumb", "fanart", etc.
+ \return list of URLs
+ */
+ std::vector<CScraperUrl::SUrlEntry> GetAvailableArtForItem(int mediaId,
+ const MediaType& mediaType,
+ const std::string& artType);
+
+ /////////////////////////////////////////////////
+ // Tag Scan Version
+ /////////////////////////////////////////////////
+ /*! \brief Check if music files need all tags rescanning regardless of file being unchanged
+ because the tag processing has changed (which may happen without db version changes) since they
+ where last scanned.
+ \return -1 if an error occurred, 0 if no scan is needed, or the version number of tags if not the same as current.
+ */
+ virtual int GetMusicNeedsTagScan();
+
+ /*! \brief Set minimum version number of db needed when tag data scanned from music files
+ \param version the version number of db
+ */
+ void SetMusicNeedsTagScan(int version);
+
+ /*! \brief Set the version number of tag data
+ \param version the version number of db when tags last scanned, 0 (default) means current db version
+ */
+ void SetMusicTagScanVersion(int version = 0);
+
+ std::string GetLibraryLastUpdated();
+ void SetLibraryLastUpdated();
+ std::string GetLibraryLastCleaned();
+ void SetLibraryLastCleaned();
+ std::string GetArtistLinksUpdated();
+ void SetArtistLinksUpdated();
+ std::string GetGenresLastAdded();
+ std::string GetSongsLastAdded();
+ std::string GetAlbumsLastAdded();
+ std::string GetArtistsLastAdded();
+ std::string GetSongsLastModified();
+ std::string GetAlbumsLastModified();
+ std::string GetArtistsLastModified();
+
+
+protected:
+ std::map<std::string, int> m_genreCache;
+ std::map<std::string, int> m_pathCache;
+
+ void CreateTables() override;
+ void CreateAnalytics() override;
+ int GetMinSchemaVersion() const override { return 32; }
+ int GetSchemaVersion() const override;
+
+ const char* GetBaseDBName() const override { return "MyMusic"; }
+
+private:
+ /*! \brief (Re)Create the generic database views for songs and albums
+ */
+ virtual void CreateViews();
+ void CreateNativeDBFunctions();
+ void CreateRemovedLinkTriggers();
+
+ void SplitPath(const std::string& strFileNameAndPath,
+ std::string& strPath,
+ std::string& strFileName);
+
+ CSong GetSongFromDataset();
+ CSong GetSongFromDataset(const dbiplus::sql_record* const record, int offset = 0);
+ CArtist GetArtistFromDataset(dbiplus::Dataset* pDS, int offset = 0, bool needThumb = true);
+ CArtist GetArtistFromDataset(const dbiplus::sql_record* const record,
+ int offset = 0,
+ bool needThumb = true);
+ CAlbum GetAlbumFromDataset(dbiplus::Dataset* pDS, int offset = 0, bool imageURL = false);
+ CAlbum GetAlbumFromDataset(const dbiplus::sql_record* const record,
+ int offset = 0,
+ bool imageURL = false);
+ CArtistCredit GetArtistCreditFromDataset(const dbiplus::sql_record* const record, int offset = 0);
+ CMusicRole GetArtistRoleFromDataset(const dbiplus::sql_record* const record, int offset = 0);
+ std::string GetMediaDateFromFile(const std::string& strFileNameAndPath);
+ void GetFileItemFromDataset(CFileItem* item, const CMusicDbUrl& baseUrl);
+ void GetFileItemFromDataset(const dbiplus::sql_record* const record,
+ CFileItem* item,
+ const CMusicDbUrl& baseUrl);
+ void GetFileItemFromArtistCredits(VECARTISTCREDITS& artistCredits, CFileItem* item);
+
+ bool DeleteRemovedLinks();
+
+ bool CleanupSongs(CGUIDialogProgress* progressDialog = nullptr);
+ bool CleanupSongsByIds(const std::string& strSongIds);
+ bool CleanupPaths();
+ bool CleanupAlbums();
+ bool CleanupArtists();
+ bool CleanupGenres();
+ bool CleanupInfoSettings();
+ bool CleanupRoles();
+ void UpdateTables(int version) override;
+ bool SearchArtists(const std::string& search, CFileItemList& artists);
+ bool SearchAlbums(const std::string& search, CFileItemList& albums);
+ bool SearchSongs(const std::string& strSearch, CFileItemList& songs);
+ int GetSongIDFromPath(const std::string& filePath);
+ void NormaliseSongDates(std::string& strRelease, std::string& strOriginal);
+ bool TrimImageURLs(std::string& strImage, const size_t space);
+
+ /*! \brief Build SQL for sort subquery from ignore article token list
+ \param strField original name or title field that articles could be removed from
+ \return SQL string e.g. WHEN strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5)
+ */
+ std::string GetIgnoreArticleSQL(const std::string& strField);
+
+ /*! \brief Build SQL for sort name scalar subquery from sort attributes and ignore article list.
+ \param strAlias alias name of scalar subquery field
+ \param sortAttributes the sort attributes e.g. SortAttributeIgnoreArticle
+ \param strField original name or title field that articles could be removed from
+ \param strSortField sort name or title field to be used instead of original (when data not null)
+ \return SQL string e.g.
+ CASE WHEN strArtistSort IS NOT NULL THEN strArtistSort
+ WHEN strField LIKE 'the ' OR strField LIKE 'the_' ESCAPE '_' THEN SUBSTR(strArtist, 5)
+ ELSE strField
+ END AS strAlias
+ */
+ std::string SortnameBuildSQL(const std::string& strAlias,
+ const SortAttribute& sortAttributes,
+ const std::string& strField,
+ const std::string& strSortField);
+
+ /*! \brief Build SQL for sorting field naturally and case-insensitively (in SQLite).
+ \param strField field name
+ \param sortOrder the sort order
+ \return SQL string e.g.
+ CASE WHEN CAST(strTitle AS INTEGER) = 0 THEN 100000000
+ ELSE CAST(strTitle AS INTEGER) END DESC, strTitle COLLATE NOCASE DESC
+ */
+ std::string AlphanumericSortSQL(const std::string& strField, const SortOrder& sortOrder);
+
+ /*! \brief Checks that source table matches sources.xml
+ returns true when they do
+ */
+ bool CheckSources(VECSOURCES& sources);
+
+ /*! \brief Initially fills source table from sources.xml for use only at
+ migration of db from an earlier version than 72
+ returns true when successfully done
+ */
+ bool MigrateSources();
+
+ bool m_translateBlankArtist;
+
+ // Fields should be ordered as they
+ // appear in the songview
+ static enum _SongFields {
+ song_idSong = 0,
+ song_strArtists,
+ song_strArtistSort,
+ song_strGenres,
+ song_strTitle,
+ song_iTrack,
+ song_iDuration,
+ song_strReleaseDate,
+ song_strOrigReleaseDate,
+ song_strDiscSubtitle,
+ song_strFileName,
+ song_strMusicBrainzTrackID,
+ song_iTimesPlayed,
+ song_iStartOffset,
+ song_iEndOffset,
+ song_lastplayed,
+ song_rating,
+ song_userrating,
+ song_votes,
+ song_comment,
+ song_idAlbum,
+ song_strAlbum,
+ song_strPath,
+ song_strReleaseStatus,
+ song_bCompilation,
+ song_bBoxedSet,
+ song_strAlbumArtists,
+ song_strAlbumArtistSort,
+ song_strAlbumReleaseType,
+ song_mood,
+ song_strReplayGain,
+ song_iBPM,
+ song_iBitRate,
+ song_iSampleRate,
+ song_iChannels,
+ song_iAlbumDuration,
+ song_iDiscTotal,
+ song_dateAdded,
+ song_dateNew,
+ song_dateModified,
+ song_enumCount // end of the enum, do not add past here
+ } SongFields;
+
+ // Fields should be ordered as they
+ // appear in the albumview
+ static enum _AlbumFields {
+ album_idAlbum = 0,
+ album_strAlbum,
+ album_strMusicBrainzAlbumID,
+ album_strReleaseGroupMBID,
+ album_strArtists,
+ album_strArtistSort,
+ album_strGenres,
+ album_strReleaseDate,
+ album_strOrigReleaseDate,
+ album_bBoxedSet,
+ album_strMoods,
+ album_strStyles,
+ album_strThemes,
+ album_strReview,
+ album_strLabel,
+ album_strType,
+ album_strReleaseStatus,
+ album_strThumbURL,
+ album_fRating,
+ album_iUserrating,
+ album_iVotes,
+ album_bCompilation,
+ album_bScrapedMBID,
+ album_lastScraped,
+ album_dateAdded,
+ album_dateNew,
+ album_dateModified,
+ album_iTimesPlayed,
+ album_strReleaseType,
+ album_iTotalDiscs,
+ album_dtLastPlayed,
+ album_iAlbumDuration,
+ album_enumCount // end of the enum, do not add past here
+ } AlbumFields;
+
+ // Fields should be ordered as they
+ // appear in the songartistview/albumartistview
+ static enum _ArtistCreditFields {
+ // used for GetAlbum to get the cascaded album/song artist credits
+ artistCredit_idEntity = 0, // can be idSong or idAlbum depending on context
+ artistCredit_idArtist,
+ artistCredit_idRole,
+ artistCredit_strRole,
+ artistCredit_strArtist,
+ artistCredit_strSortName,
+ artistCredit_strMusicBrainzArtistID,
+ artistCredit_iOrder,
+ artistCredit_enumCount
+ } ArtistCreditFields;
+
+ // Fields should be ordered as they
+ // appear in the artistview
+ static enum _ArtistFields {
+ artist_idArtist = 0,
+ artist_strArtist,
+ artist_strSortName,
+ artist_strMusicBrainzArtistID,
+ artist_strType,
+ artist_strGender,
+ artist_strDisambiguation,
+ artist_strBorn,
+ artist_strFormed,
+ artist_strGenres,
+ artist_strMoods,
+ artist_strStyles,
+ artist_strInstruments,
+ artist_strBiography,
+ artist_strDied,
+ artist_strDisbanded,
+ artist_strYearsActive,
+ artist_strImage,
+ artist_bScrapedMBID,
+ artist_lastScraped,
+ artist_dateAdded,
+ artist_dateNew,
+ artist_dateModified,
+ artist_enumCount // end of the enum, do not add past here
+ } ArtistFields;
+
+ // Fields fetched by GetArtistsByWhereJSON, order same as in JSONtoDBArtist
+ static enum _JoinToArtistFields {
+ joinToArtist_isSong = 0,
+ joinToArtist_idSourceAlbum,
+ joinToArtist_idSourceSong,
+ joinToArtist_idSongGenreAlbum,
+ joinToArtist_idSongGenreSong,
+ joinToArtist_strSongGenreAlbum,
+ joinToArtist_strSongGenreSong,
+ joinToArtist_idArt,
+ joinToArtist_artType,
+ joinToArtist_artURL,
+ joinToArtist_idRole,
+ joinToArtist_strRole,
+ joinToArtist_iOrderRole,
+ joinToArtist_isalbumartist,
+ joinToArtist_thumbnail,
+ joinToArtist_fanart,
+ joinToArtist_enumCount // end of the enum, do not add past here
+ } JoinToArtistFields;
+
+ // Fields fetched by GetAlbumsByWhereJSON, order same as in JSONtoDBAlbum
+ static enum _JoinToAlbumFields {
+ joinToAlbum_idArtist = 0,
+ joinToAlbum_strArtist,
+ joinToAlbum_strArtistMBID,
+ joinToAlbum_enumCount // end of the enum, do not add past here
+ } JoinToAlbumFields;
+
+ // Fields fetched by GetSongsByWhereJSON, order same as in JSONtoDBSong
+ static enum _JoinToSongFields {
+ // Used by GetSongsByWhereJSON
+ joinToSongs_idAlbumArtist = 0,
+ joinToSongs_strAlbumArtist,
+ joinToSongs_strAlbumArtistMBID,
+ joinToSongs_iOrderAlbumArtist,
+ joinToSongs_idArtist,
+ joinToSongs_strArtist,
+ joinToSongs_strArtistMBID,
+ joinToSongs_iOrderArtist,
+ joinToSongs_idRole,
+ joinToSongs_strRole,
+ joinToSongs_iOrderRole,
+ joinToSongs_idGenre,
+ joinToSongs_iOrderGenre,
+ joinToSongs_enumCount // end of the enum, do not add past here
+ } JoinToSongFields;
+};
diff --git a/xbmc/music/MusicDbUrl.cpp b/xbmc/music/MusicDbUrl.cpp
new file mode 100644
index 0000000..3b4893b
--- /dev/null
+++ b/xbmc/music/MusicDbUrl.cpp
@@ -0,0 +1,170 @@
+/*
+ * 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 "MusicDbUrl.h"
+
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "playlists/SmartPlayList.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+using namespace XFILE;
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+CMusicDbUrl::CMusicDbUrl()
+ : CDbUrl()
+{ }
+
+CMusicDbUrl::~CMusicDbUrl() = default;
+
+bool CMusicDbUrl::parse()
+{
+ // the URL must start with musicdb://
+ if (!m_url.IsProtocol("musicdb") || m_url.GetFileName().empty())
+ return false;
+
+ std::string path = m_url.Get();
+
+ // Parse path for directory node types and query params
+ NODE_TYPE dirType;
+ NODE_TYPE childType;
+ CQueryParams queryParams;
+ if (!CMusicDatabaseDirectory::GetDirectoryNodeInfo(path, dirType, childType, queryParams))
+ return false;
+
+ switch (dirType)
+ {
+ case NODE_TYPE_ARTIST:
+ m_type = "artists";
+ break;
+
+ case NODE_TYPE_ALBUM:
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ case NODE_TYPE_ALBUM_TOP100:
+ m_type = "albums";
+ break;
+
+ case NODE_TYPE_DISC:
+ m_type = "discs";
+ break;
+
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS:
+ case NODE_TYPE_ALBUM_TOP100_SONGS:
+ case NODE_TYPE_SONG:
+ case NODE_TYPE_SONG_TOP100:
+ case NODE_TYPE_SINGLES:
+ m_type = "songs";
+ break;
+
+ default:
+ break;
+ }
+
+ switch (childType)
+ {
+ case NODE_TYPE_ARTIST:
+ m_type = "artists";
+ break;
+
+ case NODE_TYPE_ALBUM:
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ case NODE_TYPE_ALBUM_TOP100:
+ m_type = "albums";
+ break;
+
+ case NODE_TYPE_DISC:
+ m_type = "discs";
+ break;
+
+ case NODE_TYPE_SONG:
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS:
+ case NODE_TYPE_ALBUM_TOP100_SONGS:
+ case NODE_TYPE_SONG_TOP100:
+ case NODE_TYPE_SINGLES:
+ m_type = "songs";
+ break;
+
+ case NODE_TYPE_GENRE:
+ m_type = "genres";
+ break;
+
+ case NODE_TYPE_SOURCE:
+ m_type = "sources";
+ break;
+
+ case NODE_TYPE_ROLE:
+ m_type = "roles";
+ break;
+
+ case NODE_TYPE_YEAR:
+ m_type = "years";
+ break;
+
+ case NODE_TYPE_TOP100:
+ m_type = "top100";
+ break;
+
+ case NODE_TYPE_ROOT:
+ case NODE_TYPE_OVERVIEW:
+ default:
+ return false;
+ }
+
+ if (m_type.empty())
+ return false;
+
+ // retrieve and parse all options
+ AddOptions(m_url.GetOptions());
+
+ // add options based on the node type
+ if (dirType == NODE_TYPE_SINGLES || childType == NODE_TYPE_SINGLES)
+ AddOption("singles", true);
+
+ // add options based on the QueryParams
+ if (queryParams.GetArtistId() != -1)
+ AddOption("artistid", (int)queryParams.GetArtistId());
+ if (queryParams.GetAlbumId() != -1)
+ AddOption("albumid", (int)queryParams.GetAlbumId());
+ if (queryParams.GetGenreId() != -1)
+ AddOption("genreid", (int)queryParams.GetGenreId());
+ if (queryParams.GetSongId() != -1)
+ AddOption("songid", (int)queryParams.GetSongId());
+ if (queryParams.GetYear() != -1)
+ AddOption("year", (int)queryParams.GetYear());
+
+ // Decode legacy use of "musicdb://compilations/" path for filtered albums
+ if (m_url.GetFileName() == "compilations/")
+ AddOption("compilation", true);
+
+ return true;
+}
+
+bool CMusicDbUrl::validateOption(const std::string &key, const CVariant &value)
+{
+ if (!CDbUrl::validateOption(key, value))
+ return false;
+
+ // if the value is empty it will remove the option which is ok
+ // otherwise we only care about the "filter" option here
+ if (value.empty() || !StringUtils::EqualsNoCase(key, "filter"))
+ return true;
+
+ if (!value.isString())
+ return false;
+
+ CSmartPlaylist xspFilter;
+ if (!xspFilter.LoadFromJson(value.asString()))
+ return false;
+
+ // check if the filter playlist matches the item type
+ return xspFilter.GetType() == m_type;
+}
diff --git a/xbmc/music/MusicDbUrl.h b/xbmc/music/MusicDbUrl.h
new file mode 100644
index 0000000..06f3efe
--- /dev/null
+++ b/xbmc/music/MusicDbUrl.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "DbUrl.h"
+
+#include <string>
+
+class CVariant;
+
+class CMusicDbUrl : public CDbUrl
+{
+public:
+ CMusicDbUrl();
+ ~CMusicDbUrl() override;
+
+protected:
+ bool parse() override;
+ bool validateOption(const std::string &key, const CVariant &value) override;
+};
diff --git a/xbmc/music/MusicInfoLoader.cpp b/xbmc/music/MusicInfoLoader.cpp
new file mode 100644
index 0000000..2732bd4
--- /dev/null
+++ b/xbmc/music/MusicInfoLoader.cpp
@@ -0,0 +1,314 @@
+/*
+ * 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 "MusicInfoLoader.h"
+
+#include "Album.h"
+#include "Artist.h"
+#include "FileItem.h"
+#include "MusicDatabase.h"
+#include "MusicThumbLoader.h"
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
+#include "filesystem/MusicDatabaseDirectory/QueryParams.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/tags/MusicInfoTagLoaderFactory.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Archive.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+using namespace MUSIC_INFO;
+
+// HACK until we make this threadable - specify 1 thread only for now
+CMusicInfoLoader::CMusicInfoLoader()
+ : CBackgroundInfoLoader()
+ , m_databaseHits{0}
+ , m_tagReads{0}
+{
+ m_mapFileItems = new CFileItemList;
+
+ m_thumbLoader = new CMusicThumbLoader();
+}
+
+CMusicInfoLoader::~CMusicInfoLoader()
+{
+ StopThread();
+ delete m_mapFileItems;
+ delete m_thumbLoader;
+}
+
+void CMusicInfoLoader::OnLoaderStart()
+{
+ // Load previously cached items from HD
+ if (!m_strCacheFileName.empty())
+ LoadCache(m_strCacheFileName, *m_mapFileItems);
+ else
+ {
+ m_mapFileItems->SetPath(m_pVecItems->GetPath());
+ m_mapFileItems->Load();
+ m_mapFileItems->SetFastLookup(true);
+ }
+
+ m_strPrevPath.clear();
+
+ m_databaseHits = m_tagReads = 0;
+
+ if (m_pProgressCallback)
+ m_pProgressCallback->SetProgressMax(m_pVecItems->GetFileCount());
+
+ m_musicDatabase.Open();
+
+ if (m_thumbLoader)
+ m_thumbLoader->OnLoaderStart();
+}
+
+bool CMusicInfoLoader::LoadAdditionalTagInfo(CFileItem* pItem)
+{
+ if (!pItem || (pItem->m_bIsFolder && !pItem->IsAudio()) ||
+ pItem->IsPlayList() || pItem->IsNFO() || pItem->IsInternetStream())
+ return false;
+
+ if (pItem->GetProperty("hasfullmusictag") == "true")
+ return false; // already have the information
+
+ std::string path(pItem->GetPath());
+ // For songs in library set the (primary) song artist and album properties
+ // Use song Id (not path) as called for items from either library or file view,
+ // but could also be listitem with tag loaded by a script
+ if (pItem->HasMusicInfoTag() &&
+ pItem->GetMusicInfoTag()->GetType() == MediaTypeSong &&
+ pItem->GetMusicInfoTag()->GetDatabaseId() > 0)
+ {
+ CMusicDatabase database;
+ database.Open();
+ // May already have song artist ids as item property set when data read from
+ // db, but check property is valid array (scripts could set item properties
+ // incorrectly), otherwise fetch artist using song id.
+ CArtist artist;
+ bool artistfound = false;
+ if (pItem->HasProperty("artistid") && pItem->GetProperty("artistid").isArray())
+ {
+ CVariant::const_iterator_array varid = pItem->GetProperty("artistid").begin_array();
+ int idArtist = static_cast<int>(varid->asInteger());
+ artistfound = database.GetArtist(idArtist, artist, false);
+ }
+ else
+ artistfound = database.GetArtistFromSong(pItem->GetMusicInfoTag()->GetDatabaseId(), artist);
+ if (artistfound)
+ CMusicDatabase::SetPropertiesFromArtist(*pItem, artist);
+
+ // May already have album id, otherwise fetch album from song id
+ CAlbum album;
+ bool albumfound = false;
+ int idAlbum = pItem->GetMusicInfoTag()->GetAlbumId();
+ if (idAlbum > 0)
+ albumfound = database.GetAlbum(idAlbum, album, false);
+ else
+ albumfound = database.GetAlbumFromSong(pItem->GetMusicInfoTag()->GetDatabaseId(), album);
+ if (albumfound)
+ CMusicDatabase::SetPropertiesFromAlbum(*pItem, album);
+
+ path = pItem->GetMusicInfoTag()->GetURL();
+ }
+
+ CLog::Log(LOGDEBUG, "Loading additional tag info for file {}", path);
+
+ // we load up the actual tag for this file in order to
+ // fetch the lyrics and add it to the current music info tag
+ CFileItem tempItem(path, false);
+ std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(tempItem));
+ if (nullptr != pLoader)
+ {
+ CMusicInfoTag tag;
+ pLoader->Load(path, tag);
+ pItem->GetMusicInfoTag()->SetLyrics(tag.GetLyrics());
+ pItem->SetProperty("hasfullmusictag", "true");
+ return true;
+ }
+ return false;
+}
+
+bool CMusicInfoLoader::LoadItem(CFileItem* pItem)
+{
+ bool result = LoadItemCached(pItem);
+ result |= LoadItemLookup(pItem);
+
+ return result;
+}
+
+bool CMusicInfoLoader::LoadItemCached(CFileItem* pItem)
+{
+ if ((pItem->m_bIsFolder && !pItem->IsAudio()) ||
+ pItem->IsPlayList() || pItem->IsSmartPlayList() ||
+ StringUtils::StartsWithNoCase(pItem->GetPath(), "newplaylist://") ||
+ StringUtils::StartsWithNoCase(pItem->GetPath(), "newsmartplaylist://") ||
+ pItem->IsNFO() || (pItem->IsInternetStream() && !pItem->IsMusicDb()))
+ return false;
+
+ // Get thumb for item
+ m_thumbLoader->LoadItem(pItem);
+
+ return true;
+}
+
+bool CMusicInfoLoader::LoadItemLookup(CFileItem* pItem)
+{
+ if (m_pProgressCallback && !pItem->m_bIsFolder)
+ m_pProgressCallback->SetProgressAdvance();
+
+ if ((pItem->m_bIsFolder && !pItem->IsAudio()) || //
+ pItem->IsPlayList() || pItem->IsSmartPlayList() || //
+ StringUtils::StartsWithNoCase(pItem->GetPath(), "newplaylist://") || //
+ StringUtils::StartsWithNoCase(pItem->GetPath(), "newsmartplaylist://") || //
+ pItem->IsNFO() || (pItem->IsInternetStream() && !pItem->IsMusicDb()))
+ return false;
+
+ if ((!pItem->HasMusicInfoTag() || !pItem->GetMusicInfoTag()->Loaded()) && pItem->IsAudio())
+ {
+ // first check the cached item
+ CFileItemPtr mapItem = (*m_mapFileItems)[pItem->GetPath()];
+ if (mapItem && mapItem->m_dateTime==pItem->m_dateTime && mapItem->HasMusicInfoTag() && mapItem->GetMusicInfoTag()->Loaded())
+ { // Query map if we previously cached the file on HD
+ *pItem->GetMusicInfoTag() = *mapItem->GetMusicInfoTag();
+ if (mapItem->HasArt("thumb"))
+ pItem->SetArt("thumb", mapItem->GetArt("thumb"));
+ }
+ else
+ {
+ std::string strPath = URIUtils::GetDirectory(pItem->GetPath());
+ URIUtils::AddSlashAtEnd(strPath);
+ if (strPath!=m_strPrevPath)
+ {
+ // The item is from another directory as the last one,
+ // query the database for the new directory...
+ m_musicDatabase.GetSongsByPath(strPath, m_songsMap);
+ m_databaseHits++;
+ }
+
+ /*
+ This only loads the item with the song from the database when it maps to a single song,
+ it can not load song data for items with cuesheets that expand to multiple songs.
+ For songs from embedded or separate cuesheets strFileName is not unique, so the song map for
+ the path will have the list of songs from that file. But items with cuesheets are expanded
+ (replacing each item with items for every track) elsewhere. When the item we are looking up
+ has a cuesheet document or is a music file with a cuesheet embedded in the tags, and it maps
+ to more than one song then we can not fill the tag data and thumb from the database.
+ */
+ MAPSONGS::iterator it = m_songsMap.find(pItem->GetPath()); // Find file in song map
+ if (it != m_songsMap.end() && it->second.size() == 1)
+ {
+ // Have we loaded this item from database before,
+ // and even if it has a cuesheet it has only one song
+ pItem->GetMusicInfoTag()->SetSong(it->second[0]);
+ if (!it->second[0].strThumb.empty())
+ pItem->SetArt("thumb", it->second[0].strThumb);
+ }
+ else if (pItem->IsMusicDb())
+ { // a music db item that doesn't have tag loaded - grab details from the database
+ XFILE::MUSICDATABASEDIRECTORY::CQueryParams param;
+ XFILE::MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(pItem->GetPath(),param);
+ CSong song;
+ if (m_musicDatabase.GetSong(param.GetSongId(), song))
+ {
+ pItem->GetMusicInfoTag()->SetSong(song);
+ if (!song.strThumb.empty())
+ pItem->SetArt("thumb", song.strThumb);
+ }
+ }
+ else if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICFILES_USETAGS) || pItem->IsCDDA())
+ { // Nothing found, load tag from file,
+ // always try to load cddb info
+ // get correct tag parser
+ std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(*pItem));
+ if (nullptr != pLoader)
+ // get tag
+ pLoader->Load(pItem->GetPath(), *pItem->GetMusicInfoTag());
+ m_tagReads++;
+ }
+
+ m_strPrevPath = strPath;
+ }
+ }
+
+ return true;
+}
+
+void CMusicInfoLoader::OnLoaderFinish()
+{
+ // cleanup last loaded songs from database
+ m_songsMap.clear();
+
+ // cleanup cache loaded from HD
+ m_mapFileItems->Clear();
+
+ // Save loaded items to HD
+ if (!m_strCacheFileName.empty())
+ SaveCache(m_strCacheFileName, *m_pVecItems);
+ else if (!m_bStop && (m_databaseHits > 1 || m_tagReads > 0))
+ m_pVecItems->Save();
+
+ m_musicDatabase.Close();
+
+ if (m_thumbLoader)
+ m_thumbLoader->OnLoaderFinish();
+}
+
+void CMusicInfoLoader::UseCacheOnHD(const std::string& strFileName)
+{
+ m_strCacheFileName = strFileName;
+}
+
+void CMusicInfoLoader::LoadCache(const std::string& strFileName, CFileItemList& items)
+{
+ CFile file;
+
+ if (file.Open(strFileName))
+ {
+ CArchive ar(&file, CArchive::load);
+ int iSize = 0;
+ ar >> iSize;
+ for (int i = 0; i < iSize; i++)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ ar >> *pItem;
+ items.Add(pItem);
+ }
+ ar.Close();
+ file.Close();
+ items.SetFastLookup(true);
+ }
+}
+
+void CMusicInfoLoader::SaveCache(const std::string& strFileName, CFileItemList& items)
+{
+ int iSize = items.Size();
+
+ if (iSize <= 0)
+ return ;
+
+ CFile file;
+
+ if (file.OpenForWrite(strFileName))
+ {
+ CArchive ar(&file, CArchive::store);
+ ar << items.Size();
+ for (int i = 0; i < iSize; i++)
+ {
+ CFileItemPtr pItem = items[i];
+ ar << *pItem;
+ }
+ ar.Close();
+ file.Close();
+ }
+
+}
diff --git a/xbmc/music/MusicInfoLoader.h b/xbmc/music/MusicInfoLoader.h
new file mode 100644
index 0000000..d6cab41
--- /dev/null
+++ b/xbmc/music/MusicInfoLoader.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "BackgroundInfoLoader.h"
+#include "MusicDatabase.h"
+
+class CFileItemList;
+class CMusicThumbLoader;
+
+namespace MUSIC_INFO
+{
+class CMusicInfoLoader : public CBackgroundInfoLoader
+{
+public:
+ CMusicInfoLoader();
+ ~CMusicInfoLoader() override;
+
+ void UseCacheOnHD(const std::string& strFileName);
+ bool LoadItem(CFileItem* pItem) override;
+ bool LoadItemCached(CFileItem* pItem) override;
+ bool LoadItemLookup(CFileItem* pItem) override;
+ static bool LoadAdditionalTagInfo(CFileItem* pItem);
+
+protected:
+ void OnLoaderStart() override;
+ void OnLoaderFinish() override;
+ void LoadCache(const std::string& strFileName, CFileItemList& items);
+ void SaveCache(const std::string& strFileName, CFileItemList& items);
+protected:
+ std::string m_strCacheFileName;
+ CFileItemList* m_mapFileItems;
+ MAPSONGS m_songsMap;
+ std::string m_strPrevPath;
+ CMusicDatabase m_musicDatabase;
+ unsigned int m_databaseHits;
+ unsigned int m_tagReads;
+ CMusicThumbLoader *m_thumbLoader;
+};
+}
diff --git a/xbmc/music/MusicLibraryQueue.cpp b/xbmc/music/MusicLibraryQueue.cpp
new file mode 100644
index 0000000..f16272f
--- /dev/null
+++ b/xbmc/music/MusicLibraryQueue.cpp
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2017-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 "MusicLibraryQueue.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "music/infoscanner/MusicInfoScanner.h"
+#include "music/jobs/MusicLibraryCleaningJob.h"
+#include "music/jobs/MusicLibraryExportJob.h"
+#include "music/jobs/MusicLibraryImportJob.h"
+#include "music/jobs/MusicLibraryJob.h"
+#include "music/jobs/MusicLibraryScanningJob.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Variant.h"
+
+#include <mutex>
+#include <utility>
+
+CMusicLibraryQueue::CMusicLibraryQueue()
+ : CJobQueue(false, 1, CJob::PRIORITY_LOW),
+ m_jobs()
+{ }
+
+CMusicLibraryQueue::~CMusicLibraryQueue()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_jobs.clear();
+}
+
+CMusicLibraryQueue& CMusicLibraryQueue::GetInstance()
+{
+ static CMusicLibraryQueue s_instance;
+ return s_instance;
+}
+
+void CMusicLibraryQueue::ExportLibrary(const CLibExportSettings& settings, bool showDialog /* = false */)
+{
+ CGUIDialogProgress* progress = NULL;
+ if (showDialog)
+ {
+ progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (progress)
+ {
+ progress->SetHeading(CVariant{ 20196 }); //"Export music library"
+ progress->SetText(CVariant{ 650 }); //"Exporting"
+ progress->SetPercentage(0);
+ progress->Open();
+ progress->ShowProgressBar(true);
+ }
+ }
+
+ CMusicLibraryExportJob* exportJob = new CMusicLibraryExportJob(settings, progress);
+ if (showDialog)
+ {
+ AddJob(exportJob);
+
+ // Wait for export to complete or be canceled, but render every 10ms so that the
+ // pointer movements work on dialog even when export is reporting progress infrequently
+ if (progress)
+ progress->Wait();
+ }
+ else
+ {
+ m_modal = true;
+ exportJob->DoWork();
+
+ delete exportJob;
+ m_modal = false;
+ Refresh();
+ }
+}
+
+void CMusicLibraryQueue::ImportLibrary(const std::string& xmlFile, bool showDialog /* = false */)
+{
+ CGUIDialogProgress* progress = nullptr;
+ if (showDialog)
+ {
+ progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (progress)
+ {
+ progress->SetHeading(CVariant{ 20197 }); //"Import music library"
+ progress->SetText(CVariant{ 649 }); //"Importing"
+ progress->SetLine(1, CVariant{ 330 }); //"This could take some time"
+ progress->SetLine(2, CVariant{ "" });
+ progress->SetPercentage(0);
+ progress->Open();
+ progress->ShowProgressBar(true);
+ }
+ }
+
+ CMusicLibraryImportJob* importJob = new CMusicLibraryImportJob(xmlFile, progress);
+ if (showDialog)
+ {
+ AddJob(importJob);
+
+ // Wait for import to complete or be canceled, but render every 10ms so that the
+ // pointer movements work on dialog even when import is reporting progress infrequently
+ if (progress)
+ progress->Wait();
+ }
+ else
+ {
+ m_modal = true;
+ importJob->DoWork();
+
+ delete importJob;
+ m_modal = false;
+ Refresh();
+ }
+}
+
+void CMusicLibraryQueue::ScanLibrary(const std::string& strDirectory,
+ int flags /* = 0 */,
+ bool showProgress /* = true */)
+{
+ if (flags == MUSIC_INFO::CMusicInfoScanner::SCAN_NORMAL)
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO))
+ flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_ONLINE;
+ }
+
+ if (!showProgress || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_BACKGROUNDUPDATE))
+ flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_BACKGROUND;
+
+ AddJob(new CMusicLibraryScanningJob(strDirectory, flags, showProgress));
+}
+
+void CMusicLibraryQueue::StartAlbumScan(const std::string & strDirectory, bool refresh)
+{
+ int flags = MUSIC_INFO::CMusicInfoScanner::SCAN_ALBUMS;
+ if (refresh)
+ flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN;
+ AddJob(new CMusicLibraryScanningJob(strDirectory, flags, true));
+}
+
+void CMusicLibraryQueue::StartArtistScan(const std::string& strDirectory, bool refresh)
+{
+ int flags = MUSIC_INFO::CMusicInfoScanner::SCAN_ARTISTS;
+ if (refresh)
+ flags |= MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN;
+ AddJob(new CMusicLibraryScanningJob(strDirectory, flags, true));
+}
+
+bool CMusicLibraryQueue::IsScanningLibrary() const
+{
+ // check if the library is being cleaned synchronously
+ if (m_cleaning)
+ return true;
+
+ // check if the library is being scanned asynchronously
+ MusicLibraryJobMap::const_iterator scanningJobs = m_jobs.find("MusicLibraryScanningJob");
+ if (scanningJobs != m_jobs.end() && !scanningJobs->second.empty())
+ return true;
+
+ // check if the library is being cleaned asynchronously
+ MusicLibraryJobMap::const_iterator cleaningJobs = m_jobs.find("MusicLibraryCleaningJob");
+ if (cleaningJobs != m_jobs.end() && !cleaningJobs->second.empty())
+ return true;
+
+ return false;
+}
+
+void CMusicLibraryQueue::StopLibraryScanning()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ MusicLibraryJobMap::const_iterator scanningJobs = m_jobs.find("MusicLibraryScanningJob");
+ if (scanningJobs == m_jobs.end())
+ return;
+
+ // get a copy of the scanning jobs because CancelJob() will modify m_scanningJobs
+ MusicLibraryJobs tmpScanningJobs(scanningJobs->second.begin(), scanningJobs->second.end());
+
+ // cancel all scanning jobs
+ for (const auto& job : tmpScanningJobs)
+ CancelJob(job);
+ Refresh();
+}
+
+void CMusicLibraryQueue::CleanLibrary(bool showDialog /* = false */)
+{
+ CGUIDialogProgress* progress = NULL;
+ if (showDialog)
+ {
+ progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (progress)
+ {
+ progress->SetHeading(CVariant{ 700 });
+ progress->SetPercentage(0);
+ progress->Open();
+ progress->ShowProgressBar(true);
+ }
+ }
+
+ CMusicLibraryCleaningJob* cleaningJob = new CMusicLibraryCleaningJob(progress);
+ AddJob(cleaningJob);
+
+ // Wait for cleaning to complete or be canceled, but render every 20ms so that the
+ // pointer movements work on dialog even when cleaning is reporting progress infrequently
+ if (progress)
+ progress->Wait(20);
+}
+
+void CMusicLibraryQueue::AddJob(CMusicLibraryJob *job)
+{
+ if (job == NULL)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (!CJobQueue::AddJob(job))
+ return;
+
+ // add the job to our list of queued/running jobs
+ std::string jobType = job->GetType();
+ MusicLibraryJobMap::iterator jobsIt = m_jobs.find(jobType);
+ if (jobsIt == m_jobs.end())
+ {
+ MusicLibraryJobs jobs;
+ jobs.insert(job);
+ m_jobs.insert(std::make_pair(jobType, jobs));
+ }
+ else
+ jobsIt->second.insert(job);
+}
+
+void CMusicLibraryQueue::CancelJob(CMusicLibraryJob *job)
+{
+ if (job == NULL)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // remember the job type needed later because the job might be deleted
+ // in the call to CJobQueue::CancelJob()
+ std::string jobType;
+ if (job->GetType() != NULL)
+ jobType = job->GetType();
+
+ // check if the job supports cancellation and cancel it
+ if (job->CanBeCancelled())
+ job->Cancel();
+
+ // remove the job from the job queue
+ CJobQueue::CancelJob(job);
+
+ // remove the job from our list of queued/running jobs
+ MusicLibraryJobMap::iterator jobsIt = m_jobs.find(jobType);
+ if (jobsIt != m_jobs.end())
+ jobsIt->second.erase(job);
+}
+
+void CMusicLibraryQueue::CancelAllJobs()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ CJobQueue::CancelJobs();
+
+ // remove all scanning jobs
+ m_jobs.clear();
+}
+
+bool CMusicLibraryQueue::IsRunning() const
+{
+ return CJobQueue::IsProcessing() || m_modal;
+}
+
+void CMusicLibraryQueue::Refresh()
+{
+ CUtil::DeleteMusicDatabaseDirectoryCache();
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CMusicLibraryQueue::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ if (success)
+ {
+ if (QueueEmpty())
+ Refresh();
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // remove the job from our list of queued/running jobs
+ MusicLibraryJobMap::iterator jobsIt = m_jobs.find(job->GetType());
+ if (jobsIt != m_jobs.end())
+ jobsIt->second.erase(static_cast<CMusicLibraryJob*>(job));
+ }
+
+ return CJobQueue::OnJobComplete(jobID, success, job);
+}
diff --git a/xbmc/music/MusicLibraryQueue.h b/xbmc/music/MusicLibraryQueue.h
new file mode 100644
index 0000000..f7948f7
--- /dev/null
+++ b/xbmc/music/MusicLibraryQueue.h
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2017-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/LibExportSettings.h"
+#include "threads/CriticalSection.h"
+#include "utils/JobManager.h"
+
+#include <map>
+#include <set>
+
+class CGUIDialogProgressBarHandle;
+class CMusicLibraryJob;
+class CGUIDialogProgress;
+
+/*!
+ \brief Queue for music library jobs.
+
+ The queue can only process a single job at any time and every job will be
+ executed at the lowest priority.
+ */
+class CMusicLibraryQueue : protected CJobQueue
+{
+public:
+ ~CMusicLibraryQueue() override;
+
+ /*!
+ \brief Gets the singleton instance of the music library queue.
+ */
+ static CMusicLibraryQueue& GetInstance();
+
+ /*!
+ \brief Enqueue a music library export job.
+ \param[in] settings Library export settings
+ \param[in] showDialog Show a progress dialog while (asynchronously) exporting, otherwise export in synchronous
+ */
+ void ExportLibrary(const CLibExportSettings& settings, bool showDialog = false);
+
+ /*!
+ \brief Enqueue a music library import job.
+ \param[in] xmlFile xml file to import
+ \param[in] showDialog Show a progress dialog while (asynchronously) exporting, otherwise export in synchronous
+ */
+ void ImportLibrary(const std::string& xmlFile, bool showDialog = false);
+
+ /*!
+ \brief Enqueue a music library update job, scanning tags embedded in music files and optionally scraping additional data.
+ \param[in] strDirectory Directory to scan or "" (empty string) for a global scan.
+ \param[in] flags Flags for controlling the scanning process. See xbmc/music/infoscanner/MusicInfoScanner.h for possible values.
+ \param[in] showProgress Whether or not to show a progress dialog. Defaults to true
+ */
+ void ScanLibrary(const std::string& strDirectory, int flags = 0, bool showProgress = true);
+
+ /*!
+ \brief Enqueue an album scraping job fetching additional album data.
+ \param[in] strDirectory Virtual path that identifies which albums to process or "" (empty string) for all albums
+ \param[in] refresh Whether or not to refresh data for albums that have previously been scraped
+ */
+ void StartAlbumScan(const std::string& strDirectory, bool refresh = false);
+
+ /*!
+ \brief Enqueue an artist scraping job fetching additional artist data.
+ \param[in] strDirectory Virtual path that identifies which artists to process or "" (empty string) for all artists
+ \param[in] refresh Whether or not to refresh data for artists that have previously been scraped
+ */
+ void StartArtistScan(const std::string& strDirectory, bool refresh = false);
+
+ /*!
+ \brief Check if a library scan or cleaning is in progress.
+ \return True if a scan or clean is in progress, false otherwise
+ */
+ bool IsScanningLibrary() const;
+
+ /*!
+ \brief Stop and dequeue all scanning jobs.
+ */
+ void StopLibraryScanning();
+
+ /*!
+ \brief Enqueue an asynchronous library cleaning job.
+ \param[in] showDialog Show a model progress dialog while cleaning. Default is false.
+ */
+ void CleanLibrary(bool showDialog = false);
+
+ /*!
+ \brief Adds the given job to the queue.
+ \param[in] job Music library job to be queued.
+ */
+ void AddJob(CMusicLibraryJob *job);
+
+ /*!
+ \brief Cancels the given job and removes it from the queue.
+ \param[in] job Music library job to be canceled and removed from the queue.
+ */
+ void CancelJob(CMusicLibraryJob *job);
+
+ /*!
+ \brief Cancels all running and queued jobs.
+ */
+ void CancelAllJobs();
+
+ /*!
+ \brief Whether any jobs are running or not.
+ */
+ bool IsRunning() const;
+
+protected:
+ // implementation of IJobCallback
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ /*!
+ \brief Notifies all to refresh the current listings.
+ */
+ void Refresh();
+
+private:
+ CMusicLibraryQueue();
+ CMusicLibraryQueue(const CMusicLibraryQueue&);
+ CMusicLibraryQueue const& operator=(CMusicLibraryQueue const&);
+
+ typedef std::set<CMusicLibraryJob*> MusicLibraryJobs;
+ typedef std::map<std::string, MusicLibraryJobs> MusicLibraryJobMap;
+ MusicLibraryJobMap m_jobs;
+ CCriticalSection m_critical;
+
+ bool m_modal = false;
+ bool m_exporting = false;
+ bool m_cleaning = false;
+};
diff --git a/xbmc/music/MusicThumbLoader.cpp b/xbmc/music/MusicThumbLoader.cpp
new file mode 100644
index 0000000..98d369a
--- /dev/null
+++ b/xbmc/music/MusicThumbLoader.cpp
@@ -0,0 +1,385 @@
+/*
+ * 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 "MusicThumbLoader.h"
+
+#include "FileItem.h"
+#include "TextureDatabase.h"
+#include "music/infoscanner/MusicInfoScanner.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/tags/MusicInfoTagLoaderFactory.h"
+#include "utils/StringUtils.h"
+#include "video/VideoThumbLoader.h"
+
+#include <utility>
+
+using namespace MUSIC_INFO;
+
+CMusicThumbLoader::CMusicThumbLoader() : CThumbLoader()
+{
+ m_musicDatabase = new CMusicDatabase;
+}
+
+CMusicThumbLoader::~CMusicThumbLoader()
+{
+ delete m_musicDatabase;
+}
+
+void CMusicThumbLoader::OnLoaderStart()
+{
+ m_musicDatabase->Open();
+ m_albumArt.clear();
+ CThumbLoader::OnLoaderStart();
+}
+
+void CMusicThumbLoader::OnLoaderFinish()
+{
+ m_musicDatabase->Close();
+ m_albumArt.clear();
+ CThumbLoader::OnLoaderFinish();
+}
+
+bool CMusicThumbLoader::LoadItem(CFileItem* pItem)
+{
+ bool result = LoadItemCached(pItem);
+ result |= LoadItemLookup(pItem);
+
+ return result;
+}
+
+bool CMusicThumbLoader::LoadItemCached(CFileItem* pItem)
+{
+ if (pItem->m_bIsShareOrDrive)
+ return false;
+
+ if (pItem->HasMusicInfoTag() && !pItem->GetProperty("libraryartfilled").asBoolean())
+ {
+ if (FillLibraryArt(*pItem))
+ return true;
+
+ if (pItem->GetMusicInfoTag()->GetType() == MediaTypeArtist)
+ return false; // No fallback
+ }
+
+ if (pItem->HasVideoInfoTag() && !pItem->HasArt("thumb"))
+ { // music video
+ CVideoThumbLoader loader;
+ if (loader.LoadItemCached(pItem))
+ return true;
+ }
+
+ // Fallback to folder thumb when path has one cached
+ if (!pItem->HasArt("thumb"))
+ {
+ std::string art = GetCachedImage(*pItem, "thumb");
+ if (!art.empty())
+ pItem->SetArt("thumb", art);
+ }
+
+ // Fallback to folder fanart when path has one cached
+ //! @todo Remove as "fanart" is never been cached for music folders (only for
+ // artists) or start caching fanart for folders?
+ if (!pItem->HasArt("fanart"))
+ {
+ std::string art = GetCachedImage(*pItem, "fanart");
+ if (!art.empty())
+ {
+ pItem->SetArt("fanart", art);
+ }
+ }
+
+ return false;
+}
+
+bool CMusicThumbLoader::LoadItemLookup(CFileItem* pItem)
+{
+ if (pItem->m_bIsShareOrDrive)
+ return false;
+
+ if (pItem->HasMusicInfoTag() && pItem->GetMusicInfoTag()->GetType() == MediaTypeArtist) // No fallback for artist
+ return false;
+
+ if (pItem->HasVideoInfoTag())
+ { // music video
+ CVideoThumbLoader loader;
+ if (loader.LoadItemLookup(pItem))
+ return true;
+ }
+
+ if (!pItem->HasArt("thumb"))
+ {
+ // Look for embedded art
+ if (pItem->HasMusicInfoTag() && !pItem->GetMusicInfoTag()->GetCoverArtInfo().Empty())
+ {
+ // The item has got embedded art but user thumbs overrule, so check for those first
+ if (!FillThumb(*pItem, false)) // Check for user thumbs but ignore folder thumbs
+ {
+ // No user thumb, use embedded art
+ std::string thumb = CTextureUtils::GetWrappedImageURL(pItem->GetPath(), "music");
+ pItem->SetArt("thumb", thumb);
+ }
+ }
+ else
+ {
+ // Check for user thumbs
+ FillThumb(*pItem, true);
+ }
+ }
+
+ return true;
+}
+
+bool CMusicThumbLoader::FillThumb(CFileItem &item, bool folderThumbs /* = true */)
+{
+ if (item.HasArt("thumb"))
+ return true;
+ std::string thumb = GetCachedImage(item, "thumb");
+ if (thumb.empty())
+ {
+ thumb = item.GetUserMusicThumb(false, folderThumbs);
+ if (!thumb.empty())
+ SetCachedImage(item, "thumb", thumb);
+ }
+ if (!thumb.empty())
+ item.SetArt("thumb", thumb);
+ return !thumb.empty();
+}
+
+bool CMusicThumbLoader::FillLibraryArt(CFileItem &item)
+{
+ /* Called for any item with MusicInfoTag and no art.
+ Items on Genres, Sources and Roles nodes have ID (although items on Years
+ node do not) so check for song/album/artist specifically.
+ Non-library songs (file view) can also have MusicInfoTag but no ID or type
+ */
+ bool artfound(false);
+ std::vector<ArtForThumbLoader> art;
+ CMusicInfoTag &tag = *item.GetMusicInfoTag();
+ if (tag.GetDatabaseId() > -1 &&
+ (tag.GetType() == MediaTypeSong || tag.GetType() == MediaTypeAlbum ||
+ tag.GetType() == MediaTypeArtist))
+ {
+ // Item in music library, fetch the art
+ m_musicDatabase->Open();
+ if (tag.GetType() == MediaTypeSong)
+ artfound = m_musicDatabase->GetArtForItem(tag.GetDatabaseId(), tag.GetAlbumId(), -1, false, art);
+ else if (tag.GetType() == MediaTypeAlbum)
+ artfound = m_musicDatabase->GetArtForItem(-1, tag.GetDatabaseId(), -1, false, art);
+ else //Artist
+ artfound = m_musicDatabase->GetArtForItem(-1, -1, tag.GetDatabaseId(), true, art);
+
+ m_musicDatabase->Close();
+ }
+ else if (!tag.GetArtist().empty() &&
+ (tag.GetType() == MediaTypeNone || tag.GetType() == MediaTypeSong))
+ {
+ /*
+ Could be non-library song - has musictag but no ID or type (may have
+ thumb already). Try to fetch both song artist(s) and album artist(s) art by
+ artist name, e.g. "artist.thumb", "artist.fanart", "artist.clearlogo",
+ "artist.banner", "artist1.thumb", "artist1.fanart", "artist1.clearlogo",
+ "artist1.banner", "albumartist.thumb", "albumartist.fanart" etc.
+ Set fanart as fallback.
+ */
+ CSong song;
+ // Try to split song artist names (various tags) into artist credits
+ song.SetArtistCredits(tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID());
+ if (!song.artistCredits.empty())
+ {
+ tag.SetType(MediaTypeSong); // Makes "Information" context menu visible
+ m_musicDatabase->Open();
+ int iOrder = 0;
+ // Song artist art
+ for (const auto& artistCredit : song.artistCredits)
+ {
+ int idArtist = m_musicDatabase->GetArtistByName(artistCredit.GetArtist());
+ if (idArtist > 0)
+ {
+ std::vector<ArtForThumbLoader> artistart;
+ if (m_musicDatabase->GetArtForItem(-1, -1, idArtist, true, artistart))
+ {
+ for (auto& artitem : artistart)
+ {
+ if (iOrder > 0)
+ artitem.prefix = StringUtils::Format("artist{}", iOrder);
+ else
+ artitem.prefix = "artist";
+ }
+ art.insert(art.end(), artistart.begin(), artistart.end());
+ }
+ }
+ ++iOrder;
+ }
+ // Album artist art
+ if (!tag.GetAlbumArtist().empty() && tag.GetArtistString().compare(tag.GetAlbumArtistString()) != 0)
+ {
+ // Split song artist names correctly into artist credits from various tag
+ // arrays, inc. fallback to song artist names
+ CAlbum album;
+ album.SetArtistCredits(tag.GetAlbumArtist(), tag.GetMusicBrainzAlbumArtistHints(), tag.GetMusicBrainzAlbumArtistID(),
+ tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID());
+
+ iOrder = 0;
+ for (const auto& artistCredit : album.artistCredits)
+ {
+ int idArtist = m_musicDatabase->GetArtistByName(artistCredit.GetArtist());
+ if (idArtist > 0)
+ {
+ std::vector<ArtForThumbLoader> artistart;
+ if (m_musicDatabase->GetArtForItem(-1, -1, idArtist, true, artistart))
+ {
+ for (auto& artitem : artistart)
+ {
+ if (iOrder > 0)
+ artitem.prefix = StringUtils::Format("albumartist{}", iOrder);
+ else
+ artitem.prefix = "albumartist";
+ }
+ art.insert(art.end(), artistart.begin(), artistart.end());
+ }
+ }
+ ++iOrder;
+ }
+ }
+ else
+ {
+ // Replicate the artist art as album artist art
+ std::vector<ArtForThumbLoader> artistart;
+ for (const auto& artitem : art)
+ {
+ ArtForThumbLoader newart;
+ newart.artType = artitem.artType;
+ newart.mediaType = artitem.mediaType;
+ newart.prefix = "album" + artitem.prefix;
+ newart.url = artitem.url;
+ artistart.emplace_back(newart);
+ }
+ art.insert(art.end(), artistart.begin(), artistart.end());
+ }
+ artfound = !art.empty();
+ m_musicDatabase->Close();
+ }
+ }
+
+ if (artfound)
+ {
+ std::string fanartfallback;
+ std::string artname;
+ std::map<std::string, std::string> artmap;
+ std::map<std::string, std::string> discartmap;
+ for (auto artitem : art)
+ {
+ /* Add art to artmap, naming according to media type.
+ For example: artists have "thumb", "fanart", "poster" etc.,
+ albums have "thumb", "artist.thumb", "artist.fanart",... "artist1.thumb", "artist1.fanart" etc.,
+ songs have "thumb", "album.thumb", "artist.thumb", "albumartist.thumb", "albumartist1.thumb" etc.
+ */
+ if (tag.GetType() == artitem.mediaType)
+ artname = artitem.artType;
+ else if (artitem.prefix.empty())
+ artname = artitem.mediaType + "." + artitem.artType;
+ else
+ {
+ if (tag.GetType() == MediaTypeAlbum)
+ StringUtils::Replace(artitem.prefix, "albumartist", "artist");
+ artname = artitem.prefix + "." + artitem.artType;
+ }
+
+ // Pull out album art for this specific disc e.g. "thumb2", skip art for other discs
+ if (artitem.mediaType == MediaTypeAlbum && tag.GetDiscNumber() > 0)
+ {
+ // Find any trailing digits
+ size_t startnum = artitem.artType.find_last_not_of("0123456789");
+ std::string digits = artitem.artType.substr(startnum + 1);
+ int num = atoi(digits.c_str());
+ if (num > 0 && startnum < artitem.artType.size())
+ {
+ if (num == tag.GetDiscNumber())
+ discartmap.insert(std::make_pair(artitem.artType.substr(0, startnum + 1), artitem.url));
+ continue;
+ }
+ }
+
+ artmap.insert(std::make_pair(artname, artitem.url));
+
+ // Add fallback art for "thumb" and "fanart" art types only
+ // Set album thumb as the fallback used when song thumb is missing
+ if (tag.GetType() == MediaTypeSong && artitem.mediaType == MediaTypeAlbum &&
+ artitem.artType == "thumb")
+ {
+ item.SetArtFallback(artitem.artType, artname);
+ }
+
+ // For albums and songs set fallback fanart from the artist.
+ // For songs prefer primary song artist over primary albumartist fanart as fallback fanart
+ if (artitem.prefix == "artist" && artitem.artType == "fanart")
+ fanartfallback = artname;
+ if (artitem.prefix == "albumartist" && artitem.artType == "fanart" && fanartfallback.empty())
+ fanartfallback = artname;
+ }
+ if (!fanartfallback.empty())
+ item.SetArtFallback("fanart", fanartfallback);
+
+ // Process specific disc art when we have some
+ for (const auto& discart : discartmap)
+ {
+ std::map<std::string, std::string>::iterator it;
+ if (tag.GetType() == MediaTypeAlbum)
+ {
+ // Insert or replace album art with specific disc art
+ it = artmap.find(discart.first);
+ if (it != artmap.end())
+ it->second = discart.second;
+ else
+ artmap.insert(discart);
+ }
+ else if (tag.GetType() == MediaTypeSong)
+ {
+ // Use disc thumb rather than album as fallback for song thumb
+ // (Fallback approach is used to fill missing thumbs).
+ if (discart.first == "thumb")
+ {
+ it = artmap.find("album.thumb");
+ if (it != artmap.end())
+ // Replace "album.thumb" already set as fallback
+ it->second = discart.second;
+ else
+ {
+ // Insert thumb for album and set as fallback
+ artmap.insert(std::make_pair("album.thumb", discart.second));
+ item.SetArtFallback("thumb", "album.thumb");
+ }
+ }
+ else
+ {
+ // Apply disc art as song art when not have that type (fallback does not apply).
+ // Art of other types could been set via JSON, or in future read from metadata
+ it = artmap.find(discart.first);
+ if (it == artmap.end())
+ artmap.insert(discart);
+ }
+ }
+ }
+
+ item.AppendArt(artmap);
+ }
+
+ item.SetProperty("libraryartfilled", true);
+ return artfound;
+}
+
+bool CMusicThumbLoader::GetEmbeddedThumb(const std::string &path, EmbeddedArt &art)
+{
+ CFileItem item(path, false);
+ std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(item));
+ CMusicInfoTag tag;
+ if (nullptr != pLoader)
+ pLoader->Load(path, tag, &art);
+
+ return !art.Empty();
+}
diff --git a/xbmc/music/MusicThumbLoader.h b/xbmc/music/MusicThumbLoader.h
new file mode 100644
index 0000000..fa02ad5
--- /dev/null
+++ b/xbmc/music/MusicThumbLoader.h
@@ -0,0 +1,60 @@
+/*
+ * 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 "ThumbLoader.h"
+
+#include <map>
+
+class CFileItem;
+class CMusicDatabase;
+class EmbeddedArt;
+
+class CMusicThumbLoader : public CThumbLoader
+{
+public:
+ CMusicThumbLoader();
+ ~CMusicThumbLoader() override;
+
+ void OnLoaderStart() override;
+ void OnLoaderFinish() override;
+
+ bool LoadItem(CFileItem* pItem) override;
+ bool LoadItemCached(CFileItem* pItem) override;
+ bool LoadItemLookup(CFileItem* pItem) override;
+
+ /*! \brief Helper function to fill all the art for a music library item
+ This fetches the original url for each type of art, and sets fallback thumb and fanart.
+ For songs the art for the related album and artist(s) is also set, and for albums that
+ of the related artist(s). Art type is named according to media type of the item,
+ for example:
+ artists may have "thumb", "fanart", "logo", "poster" etc.,
+ albums may have "thumb", "spine" etc. and "artist.thumb", "artist.fanart" etc.,
+ songs may have "thumb", "album.thumb", "artist.thumb", "artist.fanart", "artist.logo",...
+ "artist1.thumb", "artist1.fanart",... "albumartist.thumb", "albumartist1.thumb" etc.
+ \param item a music CFileItem
+ \return true if we fill art, false if there is no art found
+ */
+ bool FillLibraryArt(CFileItem &item) override;
+
+ /*! \brief Fill the thumb of a music file/folder item
+ First uses a cached thumb from a previous run, then checks for a local thumb
+ and caches it for the next run
+ \param item the CFileItem object to fill
+ \return true if we fill the thumb, false otherwise
+ */
+ virtual bool FillThumb(CFileItem &item, bool folderThumbs = true);
+
+ static bool GetEmbeddedThumb(const std::string &path, EmbeddedArt &art);
+
+protected:
+ CMusicDatabase *m_musicDatabase;
+ typedef std::map<int, std::map<std::string, std::string> > ArtCache;
+ ArtCache m_albumArt;
+};
diff --git a/xbmc/music/MusicUtils.cpp b/xbmc/music/MusicUtils.cpp
new file mode 100644
index 0000000..ac66aa3
--- /dev/null
+++ b/xbmc/music/MusicUtils.cpp
@@ -0,0 +1,841 @@
+/*
+ * Copyright (C) 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 "MusicUtils.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "PartyModeManager.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "media/MediaType.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicDbUrl.h"
+#include "music/tags/MusicInfoTag.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+#include "profiles/ProfileManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/IRunnable.h"
+#include "utils/FileUtils.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "view/GUIViewState.h"
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+namespace MUSIC_UTILS
+{
+class CSetArtJob : public CJob
+{
+ CFileItemPtr pItem;
+ std::string m_artType;
+ std::string m_newArt;
+
+public:
+ CSetArtJob(const CFileItemPtr& item, const std::string& type, const std::string& newArt)
+ : pItem(item), m_artType(type), m_newArt(newArt)
+ {
+ }
+
+ ~CSetArtJob(void) override = default;
+
+ bool HasSongExtraArtChanged(const CFileItemPtr& pSongItem,
+ const std::string& type,
+ const int itemID,
+ CMusicDatabase& db)
+ {
+ if (!pSongItem->HasMusicInfoTag())
+ return false;
+ int idSong = pSongItem->GetMusicInfoTag()->GetDatabaseId();
+ if (idSong <= 0)
+ return false;
+ bool result = false;
+ if (type == MediaTypeAlbum)
+ // Update art when song is from album
+ result = (itemID == pSongItem->GetMusicInfoTag()->GetAlbumId());
+ else if (type == MediaTypeArtist)
+ {
+ // Update art when artist is song or album artist of the song
+ if (pSongItem->HasProperty("artistid"))
+ {
+ // Check artistid property when we have it
+ for (CVariant::const_iterator_array varid =
+ pSongItem->GetProperty("artistid").begin_array();
+ varid != pSongItem->GetProperty("artistid").end_array(); ++varid)
+ {
+ int idArtist = static_cast<int>(varid->asInteger());
+ result = (itemID == idArtist);
+ if (result)
+ break;
+ }
+ }
+ else
+ { // Check song artists in database
+ result = db.IsSongArtist(idSong, itemID);
+ }
+ if (!result)
+ {
+ // Check song album artists
+ result = db.IsSongAlbumArtist(idSong, itemID);
+ }
+ }
+ return result;
+ }
+
+ // Asynchronously update song, album or artist art in library
+ // and trigger update to album & artist art of the currently playing song
+ // and songs queued in the current playlist
+ bool DoWork(void) override
+ {
+ int itemID = pItem->GetMusicInfoTag()->GetDatabaseId();
+ if (itemID <= 0)
+ return false;
+ std::string type = pItem->GetMusicInfoTag()->GetType();
+ CMusicDatabase db;
+ if (!db.Open())
+ return false;
+ if (!m_newArt.empty())
+ db.SetArtForItem(itemID, type, m_artType, m_newArt);
+ else
+ db.RemoveArtForItem(itemID, type, m_artType);
+ // Artwork changed so set datemodified field for artist, album or song
+ db.SetItemUpdated(itemID, type);
+
+ /* Update the art of the songs of the current music playlist.
+ Song thumb is often a fallback from the album and fanart is from the artist(s).
+ Clear the art if it is a song from the album or by the artist
+ (as song or album artist) that has modified artwork. The new artwork gets
+ loaded when the playlist is shown.
+ */
+ bool clearcache(false);
+ const PLAYLIST::CPlayList& playlist =
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC);
+
+ for (int i = 0; i < playlist.size(); ++i)
+ {
+ CFileItemPtr songitem = playlist[i];
+ if (HasSongExtraArtChanged(songitem, type, itemID, db))
+ {
+ songitem->ClearArt(); // Art gets reloaded when the current playist is shown
+ clearcache = true;
+ }
+ }
+ if (clearcache)
+ {
+ // Clear the music playlist from cache
+ CFileItemList items("playlistmusic://");
+ items.RemoveDiscCache(WINDOW_MUSIC_PLAYLIST);
+ }
+
+ // Similarly update the art of the currently playing song so it shows on OSD
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingAudio() && g_application.CurrentFileItem().HasMusicInfoTag())
+ {
+ CFileItemPtr songitem = CFileItemPtr(new CFileItem(g_application.CurrentFileItem()));
+ if (HasSongExtraArtChanged(songitem, type, itemID, db))
+ g_application.UpdateCurrentPlayArt();
+ }
+
+ db.Close();
+ return true;
+ }
+};
+
+class CSetSongRatingJob : public CJob
+{
+ std::string strPath;
+ int idSong;
+ int iUserrating;
+
+public:
+ CSetSongRatingJob(const std::string& filePath, int userrating)
+ : strPath(filePath), idSong(-1), iUserrating(userrating)
+ {
+ }
+
+ CSetSongRatingJob(int songId, int userrating) : strPath(), idSong(songId), iUserrating(userrating)
+ {
+ }
+
+ ~CSetSongRatingJob(void) override = default;
+
+ bool DoWork(void) override
+ {
+ // Asynchronously update song userrating in library
+ CMusicDatabase db;
+ if (db.Open())
+ {
+ if (idSong > 0)
+ db.SetSongUserrating(idSong, iUserrating);
+ else
+ db.SetSongUserrating(strPath, iUserrating);
+ db.Close();
+ }
+
+ return true;
+ }
+};
+
+void UpdateArtJob(const std::shared_ptr<CFileItem>& pItem,
+ const std::string& strType,
+ const std::string& strArt)
+{
+ // Asynchronously update that type of art in the database
+ CSetArtJob* job = new CSetArtJob(pItem, strType, strArt);
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+}
+
+// Add art types required in Kodi and configured by the user
+void AddHardCodedAndExtendedArtTypes(std::vector<std::string>& artTypes, const CMusicInfoTag& tag)
+{
+ for (const auto& artType : GetArtTypesToScan(tag.GetType()))
+ {
+ if (find(artTypes.begin(), artTypes.end(), artType) == artTypes.end())
+ artTypes.push_back(artType);
+ }
+}
+
+// Add art types currently assigned to the media item
+void AddCurrentArtTypes(std::vector<std::string>& artTypes,
+ const CMusicInfoTag& tag,
+ CMusicDatabase& db)
+{
+ std::map<std::string, std::string> currentArt;
+ db.GetArtForItem(tag.GetDatabaseId(), tag.GetType(), currentArt);
+ for (const auto& art : currentArt)
+ {
+ if (!art.second.empty() && find(artTypes.begin(), artTypes.end(), art.first) == artTypes.end())
+ artTypes.push_back(art.first);
+ }
+}
+
+// Add art types that exist for other media items of the same type
+void AddMediaTypeArtTypes(std::vector<std::string>& artTypes,
+ const CMusicInfoTag& tag,
+ CMusicDatabase& db)
+{
+ std::vector<std::string> dbArtTypes;
+ db.GetArtTypes(tag.GetType(), dbArtTypes);
+ for (const auto& artType : dbArtTypes)
+ {
+ if (find(artTypes.begin(), artTypes.end(), artType) == artTypes.end())
+ artTypes.push_back(artType);
+ }
+}
+
+// Add art types from available but unassigned artwork for this media item
+void AddAvailableArtTypes(std::vector<std::string>& artTypes,
+ const CMusicInfoTag& tag,
+ CMusicDatabase& db)
+{
+ for (const auto& artType : db.GetAvailableArtTypesForItem(tag.GetDatabaseId(), tag.GetType()))
+ {
+ if (find(artTypes.begin(), artTypes.end(), artType) == artTypes.end())
+ artTypes.push_back(artType);
+ }
+}
+
+bool FillArtTypesList(CFileItem& musicitem, CFileItemList& artlist)
+{
+ const CMusicInfoTag& tag = *musicitem.GetMusicInfoTag();
+ if (tag.GetDatabaseId() < 1 || tag.GetType().empty())
+ return false;
+ if (tag.GetType() != MediaTypeArtist && tag.GetType() != MediaTypeAlbum &&
+ tag.GetType() != MediaTypeSong)
+ return false;
+
+ artlist.Clear();
+
+ CMusicDatabase db;
+ db.Open();
+
+ std::vector<std::string> artTypes;
+
+ AddHardCodedAndExtendedArtTypes(artTypes, tag);
+ AddCurrentArtTypes(artTypes, tag, db);
+ AddMediaTypeArtTypes(artTypes, tag, db);
+ AddAvailableArtTypes(artTypes, tag, db);
+
+ db.Close();
+
+ for (const auto& type : artTypes)
+ {
+ CFileItemPtr artitem(new CFileItem(type, false));
+ // Localise the names of common types of art
+ if (type == "banner")
+ artitem->SetLabel(g_localizeStrings.Get(20020));
+ else if (type == "fanart")
+ artitem->SetLabel(g_localizeStrings.Get(20445));
+ else if (type == "poster")
+ artitem->SetLabel(g_localizeStrings.Get(20021));
+ else if (type == "thumb")
+ artitem->SetLabel(g_localizeStrings.Get(21371));
+ else
+ artitem->SetLabel(type);
+ // Set art type as art item property
+ artitem->SetProperty("arttype", type);
+ // Set current art as art item thumb
+ if (musicitem.HasArt(type))
+ artitem->SetArt("thumb", musicitem.GetArt(type));
+ artlist.Add(artitem);
+ }
+
+ return !artlist.IsEmpty();
+}
+
+std::string ShowSelectArtTypeDialog(CFileItemList& artitems)
+{
+ // Prompt for choice
+ CGUIDialogSelect* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!dialog)
+ return "";
+
+ dialog->SetHeading(CVariant{13521});
+ dialog->Reset();
+ dialog->SetUseDetails(true);
+ dialog->EnableButton(true, 13516);
+
+ dialog->SetItems(artitems);
+ dialog->Open();
+
+ if (dialog->IsButtonPressed())
+ {
+ // Get the new art type name
+ std::string strArtTypeName;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strArtTypeName,
+ CVariant{g_localizeStrings.Get(13516)}, false))
+ return "";
+ // Add new type to the list of art types
+ CFileItemPtr artitem(new CFileItem(strArtTypeName, false));
+ artitem->SetLabel(strArtTypeName);
+ artitem->SetProperty("arttype", strArtTypeName);
+ artitems.Add(artitem);
+
+ return strArtTypeName;
+ }
+
+ return dialog->GetSelectedFileItem()->GetProperty("arttype").asString();
+}
+
+int ShowSelectRatingDialog(int iSelected)
+{
+ CGUIDialogSelect* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (dialog)
+ {
+ dialog->SetHeading(CVariant{38023});
+ dialog->Add(g_localizeStrings.Get(38022));
+ for (int i = 1; i <= 10; i++)
+ dialog->Add(StringUtils::Format("{}: {}", g_localizeStrings.Get(563), i));
+ dialog->SetSelected(iSelected);
+ dialog->Open();
+
+ int userrating = dialog->GetSelectedItem();
+ userrating = std::max(userrating, -1);
+ userrating = std::min(userrating, 10);
+ return userrating;
+ }
+ return -1;
+}
+
+void UpdateSongRatingJob(const std::shared_ptr<CFileItem>& pItem, int userrating)
+{
+ // Asynchronously update the song user rating in music library
+ const CMusicInfoTag* tag = pItem->GetMusicInfoTag();
+ CSetSongRatingJob* job;
+ if (tag && tag->GetType() == MediaTypeSong && tag->GetDatabaseId() > 0)
+ // Use song ID when known
+ job = new CSetSongRatingJob(tag->GetDatabaseId(), userrating);
+ else
+ job = new CSetSongRatingJob(pItem->GetPath(), userrating);
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+}
+
+std::vector<std::string> GetArtTypesToScan(const MediaType& mediaType)
+{
+ std::vector<std::string> arttypes;
+ // Get default types of art that are to be automatically fetched during scanning
+ if (mediaType == MediaTypeArtist)
+ {
+ arttypes = {"thumb", "fanart"};
+ for (auto& artType : CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_MUSICLIBRARY_ARTISTART_WHITELIST))
+ {
+ if (find(arttypes.begin(), arttypes.end(), artType.asString()) == arttypes.end())
+ arttypes.emplace_back(artType.asString());
+ }
+ }
+ else if (mediaType == MediaTypeAlbum)
+ {
+ arttypes = {"thumb"};
+ for (auto& artType : CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_MUSICLIBRARY_ALBUMART_WHITELIST))
+ {
+ if (find(arttypes.begin(), arttypes.end(), artType.asString()) == arttypes.end())
+ arttypes.emplace_back(artType.asString());
+ }
+ }
+ return arttypes;
+}
+
+bool IsValidArtType(const std::string& potentialArtType)
+{
+ // Check length and is ascii
+ return potentialArtType.length() <= 25 &&
+ std::find_if_not(potentialArtType.begin(), potentialArtType.end(),
+ StringUtils::isasciialphanum) == potentialArtType.end();
+}
+
+} // namespace MUSIC_UTILS
+
+namespace
+{
+class CAsyncGetItemsForPlaylist : public IRunnable
+{
+public:
+ CAsyncGetItemsForPlaylist(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems)
+ : m_item(item), m_queuedItems(queuedItems)
+ {
+ }
+
+ ~CAsyncGetItemsForPlaylist() override = default;
+
+ void Run() override
+ {
+ // fast lookup is needed here
+ m_queuedItems.SetFastLookup(true);
+
+ m_musicDatabase.Open();
+ GetItemsForPlaylist(m_item);
+ m_musicDatabase.Close();
+ }
+
+private:
+ void GetItemsForPlaylist(const std::shared_ptr<CFileItem>& item);
+
+ const std::shared_ptr<CFileItem> m_item;
+ CFileItemList& m_queuedItems;
+ CMusicDatabase m_musicDatabase;
+};
+
+SortDescription GetSortDescription(const CGUIViewState& state, const CFileItemList& items)
+{
+ SortDescription sortDescTrackNumber;
+
+ auto sortDescriptions = state.GetSortDescriptions();
+ for (auto& sortDescription : sortDescriptions)
+ {
+ if (sortDescription.sortBy == SortByTrackNumber)
+ {
+ // check whether at least one item has actually a track number set
+ for (const auto& item : items)
+ {
+ if (item->HasMusicInfoTag() && item->GetMusicInfoTag()->GetTrackNumber() > 0)
+ {
+ // First choice for folders containing a single album
+ sortDescTrackNumber = sortDescription;
+ sortDescTrackNumber.sortOrder = SortOrderAscending;
+ break; // leave items loop. we can still find ByArtistThenYear. so, no return here.
+ }
+ }
+ }
+ else if (sortDescription.sortBy == SortByArtistThenYear)
+ {
+ // check whether songs from at least two different albums are in the list
+ int lastAlbumId = -1;
+ for (const auto& item : items)
+ {
+ if (item->HasMusicInfoTag())
+ {
+ const auto tag = item->GetMusicInfoTag();
+ if (lastAlbumId != -1 && tag->GetAlbumId() != lastAlbumId)
+ {
+ // First choice for folders containing multiple albums
+ sortDescription.sortOrder = SortOrderAscending;
+ return sortDescription;
+ }
+ lastAlbumId = tag->GetAlbumId();
+ }
+ }
+ }
+ }
+
+ if (sortDescTrackNumber.sortBy != SortByNone)
+ return sortDescTrackNumber;
+ else
+ return state.GetSortMethod(); // last resort
+}
+
+void CAsyncGetItemsForPlaylist::GetItemsForPlaylist(const std::shared_ptr<CFileItem>& item)
+{
+ if (item->IsParentFolder() || !item->CanQueue() || item->IsRAR() || item->IsZIP())
+ return;
+
+ if (item->IsMusicDb() && item->m_bIsFolder && !item->IsParentFolder())
+ {
+ // we have a music database folder, just grab the "all" item underneath it
+ XFILE::CMusicDatabaseDirectory dir;
+
+ if (!dir.ContainsSongs(item->GetPath()))
+ {
+ // grab the ALL item in this category
+ // Genres will still require 2 lookups, and queuing the entire Genre folder
+ // will require 3 lookups (genre, artist, album)
+ CMusicDbUrl musicUrl;
+ if (musicUrl.FromString(item->GetPath()))
+ {
+ musicUrl.AppendPath("-1/");
+
+ const auto allItem = std::make_shared<CFileItem>(musicUrl.ToString(), true);
+ allItem->SetCanQueue(true); // workaround for CanQueue() check above
+ GetItemsForPlaylist(allItem);
+ }
+ return;
+ }
+ }
+
+ if (item->m_bIsFolder)
+ {
+ // Check if we add a locked share
+ if (item->m_bIsShareOrDrive)
+ {
+ if (!g_passwordManager.IsItemUnlocked(item.get(), "music"))
+ return;
+ }
+
+ CFileItemList items;
+ XFILE::CDirectory::GetDirectory(item->GetPath(), items, "", XFILE::DIR_FLAG_DEFAULTS);
+
+ const std::unique_ptr<CGUIViewState> state(
+ CGUIViewState::GetViewState(WINDOW_MUSIC_NAV, items));
+ if (state)
+ {
+ LABEL_MASKS labelMasks;
+ state->GetSortMethodLabelMasks(labelMasks);
+
+ const CLabelFormatter fileFormatter(labelMasks.m_strLabelFile, labelMasks.m_strLabel2File);
+ const CLabelFormatter folderFormatter(labelMasks.m_strLabelFolder,
+ labelMasks.m_strLabel2Folder);
+ for (const auto& i : items)
+ {
+ if (i->IsLabelPreformatted())
+ continue;
+
+ if (i->m_bIsFolder)
+ folderFormatter.FormatLabels(i.get());
+ else
+ fileFormatter.FormatLabels(i.get());
+ }
+
+ SortDescription sortDesc;
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_NAV)
+ sortDesc = state->GetSortMethod();
+ else
+ sortDesc = GetSortDescription(*state, items);
+
+ if (sortDesc.sortBy == SortByLabel)
+ items.ClearSortState();
+
+ items.Sort(sortDesc);
+ }
+
+ for (const auto& i : items)
+ {
+ GetItemsForPlaylist(i);
+ }
+ }
+ else
+ {
+ if (item->IsPlayList())
+ {
+ const std::unique_ptr<PLAYLIST::CPlayList> playList(
+ PLAYLIST::CPlayListFactory::Create(*item));
+ if (!playList)
+ {
+ CLog::Log(LOGERROR, "{} failed to create playlist {}", __FUNCTION__, item->GetPath());
+ return;
+ }
+
+ if (!playList->Load(item->GetPath()))
+ {
+ CLog::Log(LOGERROR, "{} failed to load playlist {}", __FUNCTION__, item->GetPath());
+ return;
+ }
+
+ for (int i = 0; i < playList->size(); ++i)
+ {
+ GetItemsForPlaylist((*playList)[i]);
+ }
+ }
+ else if (item->IsInternetStream() && !item->IsMusicDb())
+ {
+ // just queue the internet stream, it will be expanded on play
+ m_queuedItems.Add(item);
+ }
+ else if (item->IsPlugin() && item->GetProperty("isplayable").asBoolean())
+ {
+ // python files can be played
+ m_queuedItems.Add(item);
+ }
+ else if (!item->IsNFO() && (item->IsAudio() || item->IsVideo()))
+ {
+ const auto itemCheck = m_queuedItems.Get(item->GetPath());
+ if (!itemCheck || itemCheck->GetStartOffset() != item->GetStartOffset())
+ {
+ // add item
+ m_musicDatabase.SetPropertiesForFileItem(*item);
+ m_queuedItems.Add(item);
+ }
+ }
+ }
+}
+
+void ShowToastNotification(const CFileItem& item, int titleId)
+{
+ std::string localizedMediaType;
+ std::string title;
+
+ if (item.HasMusicInfoTag())
+ {
+ localizedMediaType = CMediaTypes::GetCapitalLocalization(item.GetMusicInfoTag()->GetType());
+ title = item.GetMusicInfoTag()->GetTitle();
+ }
+
+ if (title.empty())
+ title = item.GetLabel();
+ if (title.empty())
+ return; // no meaningful toast possible.
+
+ const std::string message =
+ localizedMediaType.empty() ? title : localizedMediaType + ": " + title;
+
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(titleId),
+ message);
+}
+} // unnamed namespace
+
+namespace MUSIC_UTILS
+{
+void PlayItem(const std::shared_ptr<CFileItem>& itemIn)
+{
+ auto item = itemIn;
+
+ // Allow queuing of unqueueable items
+ // when we try to queue them directly
+ if (!itemIn->CanQueue())
+ {
+ // make a copy to not alter the original item
+ item = std::make_shared<CFileItem>(*itemIn);
+ item->SetCanQueue(true);
+ }
+
+ if (item->m_bIsFolder)
+ {
+ // build a playlist and play it
+ CFileItemList queuedItems;
+ GetItemsForPlayList(item, queuedItems);
+
+ auto& player = CServiceBroker::GetPlaylistPlayer();
+ player.ClearPlaylist(PLAYLIST::TYPE_MUSIC);
+ player.Reset();
+ player.Add(PLAYLIST::TYPE_MUSIC, queuedItems);
+ player.SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+ player.Play();
+ }
+ else if (item->HasMusicInfoTag())
+ {
+ // song, so just play it
+ CServiceBroker::GetPlaylistPlayer().Play(item, "");
+ }
+}
+
+void QueueItem(const std::shared_ptr<CFileItem>& itemIn, QueuePosition pos)
+{
+ auto item = itemIn;
+
+ // Allow queuing of unqueueable items
+ // when we try to queue them directly
+ if (!itemIn->CanQueue())
+ {
+ // make a copy to not alter the original item
+ item = std::make_shared<CFileItem>(*itemIn);
+ item->SetCanQueue(true);
+ }
+
+ auto& player = CServiceBroker::GetPlaylistPlayer();
+
+ PLAYLIST::Id playlistId = player.GetCurrentPlaylist();
+ if (playlistId == PLAYLIST::TYPE_NONE)
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ playlistId = components.GetComponent<CApplicationPlayer>()->GetPreferredPlaylist();
+ }
+
+ if (playlistId == PLAYLIST::TYPE_NONE)
+ playlistId = PLAYLIST::TYPE_MUSIC;
+
+ // Check for the partymode playlist item, do nothing when "PartyMode.xsp" not exists
+ if (item->IsSmartPlayList() && !CFileUtils::Exists(item->GetPath()))
+ {
+ const auto profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ if (item->GetPath() == profileManager->GetUserDataItem("PartyMode.xsp"))
+ return;
+ }
+
+ const int oldSize = player.GetPlaylist(playlistId).size();
+
+ CFileItemList queuedItems;
+ GetItemsForPlayList(item, queuedItems);
+
+ // if party mode, add items but DONT start playing
+ if (g_partyModeManager.IsEnabled())
+ {
+ g_partyModeManager.AddUserSongs(queuedItems, false);
+ return;
+ }
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ if (pos == QueuePosition::POSITION_BEGIN && appPlayer->IsPlaying())
+ player.Insert(playlistId, queuedItems,
+ CServiceBroker::GetPlaylistPlayer().GetCurrentSong() + 1);
+ else
+ player.Add(playlistId, queuedItems);
+
+ bool playbackStarted = false;
+
+ if (!appPlayer->IsPlaying() && player.GetPlaylist(playlistId).size())
+ {
+ const int winID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ if (winID == WINDOW_MUSIC_NAV)
+ {
+ CGUIViewState* viewState = CGUIViewState::GetViewState(winID, queuedItems);
+ if (viewState)
+ viewState->SetPlaylistDirectory("playlistmusic://");
+ }
+
+ player.Reset();
+ player.SetCurrentPlaylist(playlistId);
+ player.Play(oldSize, ""); // start playing at the first new item
+
+ playbackStarted = true;
+ }
+
+ if (!playbackStarted)
+ {
+ if (pos == QueuePosition::POSITION_END)
+ ShowToastNotification(*item, 38082); // Added to end of playlist
+ else
+ ShowToastNotification(*item, 38083); // Added to playlist to play next
+ }
+}
+
+bool GetItemsForPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems)
+{
+ CAsyncGetItemsForPlaylist getItems(item, queuedItems);
+ return CGUIDialogBusy::Wait(&getItems,
+ 500, // 500ms before busy dialog appears
+ true); // can be cancelled
+}
+
+bool IsItemPlayable(const CFileItem& item)
+{
+ // Exclude all parent folders
+ if (item.IsParentFolder())
+ return false;
+
+ // Exclude all video library items
+ if (item.IsVideoDb() || StringUtils::StartsWithNoCase(item.GetPath(), "library://video/"))
+ return false;
+
+ // Exclude other components
+ if (item.IsPVR() || item.IsPlugin() || item.IsScript() || item.IsAddonsPath())
+ return false;
+
+ // Exclude special items
+ if (StringUtils::StartsWithNoCase(item.GetPath(), "newsmartplaylist://") ||
+ StringUtils::StartsWithNoCase(item.GetPath(), "newplaylist://"))
+ return false;
+
+ // Exclude unwanted windows
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_PLAYLIST)
+ return false;
+
+ // Include playlists located at one of the possible music playlist locations
+ if (item.IsPlayList())
+ {
+ if (StringUtils::StartsWithNoCase(item.GetMimeType(), "audio/"))
+ return true;
+
+ if (StringUtils::StartsWithNoCase(item.GetPath(), "special://musicplaylists/") ||
+ StringUtils::StartsWithNoCase(item.GetPath(), "special://profile/playlists/music/"))
+ return true;
+
+ // Has user changed default playlists location and the list is located there?
+ const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::string path = settings->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH);
+ StringUtils::TrimRight(path, "/");
+ if (StringUtils::StartsWith(item.GetPath(), StringUtils::Format("{}/music/", path)))
+ return true;
+
+ if (!item.m_bIsFolder)
+ {
+ // Unknown location. Type cannot be determined for non-folder items.
+ return false;
+ }
+ }
+
+ if (item.m_bIsFolder &&
+ (item.IsMusicDb() || StringUtils::StartsWithNoCase(item.GetPath(), "library://music/")))
+ {
+ // Exclude top level nodes - eg can't play 'genres' just a specific genre etc
+ const XFILE::MUSICDATABASEDIRECTORY::NODE_TYPE node =
+ XFILE::CMusicDatabaseDirectory::GetDirectoryParentType(item.GetPath());
+ if (node == XFILE::MUSICDATABASEDIRECTORY::NODE_TYPE_OVERVIEW)
+ return false;
+
+ return true;
+ }
+
+ if (item.HasMusicInfoTag() && item.CanQueue())
+ return true;
+ else if (!item.m_bIsFolder && item.IsAudio())
+ return true;
+ else if (item.m_bIsFolder)
+ {
+ // Not a music-specific folder (just file:// or nfs://). Allow play if context is Music window.
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_NAV &&
+ item.GetPath() != "add") // Exclude "Add music source" item
+ return true;
+ }
+ return false;
+}
+
+} // namespace MUSIC_UTILS
diff --git a/xbmc/music/MusicUtils.h b/xbmc/music/MusicUtils.h
new file mode 100644
index 0000000..c9b6f94
--- /dev/null
+++ b/xbmc/music/MusicUtils.h
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 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 "media/MediaType.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CFileItemList;
+
+namespace MUSIC_UTILS
+{
+/*! \brief Show a dialog to allow the selection of type of art from a list.
+ Input is a fileitem list, with each item having an "arttype" property
+ e.g. "thumb", current art URL (if art exists), and label. One of these art types
+ can be selected, or a new art type added. The new art type is added as a new item
+ in the list, as well as returned as the selected art type.
+ \param artitems [in/out] a fileitem list to display
+ \return the selected art type e.g. "fanart" or empty string when cancelled.
+ \sa FillArtTypesList
+ */
+std::string ShowSelectArtTypeDialog(CFileItemList& artitems);
+
+/*! \brief Helper function to build a list of art types for a music library item.
+ This fetches the possible types of art for a song, album or artist, and the
+ current art URL (if the item has art of that type), for display on a dialog.
+ \param musicitem a music CFileItem (song, album or artist)
+ \param artitems [out] a fileitem list, each item having "arttype" property
+ e.g. "thumb", current art URL (if art exists), and localized label (for common arttypes)
+ \return true if art types are retrieved, false if none is found.
+ \sa ShowSelectArtTypeDialog
+ */
+bool FillArtTypesList(CFileItem& musicitem, CFileItemList& artlist);
+
+/*! \brief Helper function to asynchronously update art in the music database
+ and then refresh the album & artist art of the currently playing song.
+ For the song, album or artist this adds a job to the queue to update the art table
+ modifying, adding or deleting that type of art. Changes to album or artist art are
+ then passed to the currently playing song (if there is one).
+ \param item a shared pointer to a music CFileItem (song, album or artist)
+ \param strType the type of art e.g. "fanart" or "thumb" etc.
+ \param strArt art URL, when empty the entry for that type of art is deleted.
+ */
+void UpdateArtJob(const std::shared_ptr<CFileItem>& pItem,
+ const std::string& strType,
+ const std::string& strArt);
+
+/*! \brief Show a dialog to allow the selection of user rating.
+ \param iSelected the rating to show initially
+ \return the selected rating, 0 (no rating), 1 to 10 or -1 no rating selected
+ */
+int ShowSelectRatingDialog(int iSelected);
+
+/*! \brief Helper function to asynchronously update the user rating of a song
+ \param pItem pointer to song item being rated
+ \param userrating the userrating 0 = no rating, 1 to 10
+ */
+void UpdateSongRatingJob(const std::shared_ptr<CFileItem>& pItem, int userrating);
+
+/*! \brief Get the types of art for an artist or album that are to be
+ automatically fetched from local files during scanning
+ \param mediaType [in] artist or album
+ \return vector of art types that are to be fetched during scanning
+ */
+std::vector<std::string> GetArtTypesToScan(const MediaType& mediaType);
+
+/*! \brief Validate string is acceptable as the name of an additional art type
+ - limited length, and ascii alphanumberic characters only
+ \param potentialArtType [in] potential art type name
+ \return true if the art type is valid
+ */
+bool IsValidArtType(const std::string& potentialArtType);
+
+/*! \brief Start playback of the given item. If the item is a folder, build a playlist with
+ all items contained in the folder and start playback of the playlist. If item is a single music
+ item, start playback directly, without adding it to the music playlist first.
+ \param item [in] the item to play
+ */
+void PlayItem(const std::shared_ptr<CFileItem>& item);
+
+enum class QueuePosition
+{
+ POSITION_BEGIN, // place at begin of queue, before other items
+ POSITION_END, // place at end of queue, after other items
+};
+
+/*! \brief Queue the given item in the currently active playlist. If none is active, put the
+ item into the music playlist. Start playback of the playlist, if player is not already playing.
+ \param item [in] the item to queue
+ \param pos [in] whether to place the item and the begin or the end of the queue
+ */
+void QueueItem(const std::shared_ptr<CFileItem>& item, QueuePosition pos);
+
+/*! \brief For a given item, get the items to put in a playlist. If the item is a folder, all
+ subitems will be added recursively to the returned item list. If the item is a playlist, the
+ playlist will be loaded and contained items will be added to the returned item list. Shows a
+ busy dialog if action takes certain amount of time to give the user visual feedback.
+ \param item [in] the item to add to the playlist
+ \param queuedItems [out] the items that can be put in a play list
+ \return true on success, false otherwise
+ */
+bool GetItemsForPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems);
+
+/*!
+ \brief Check whether the given item can be played by the app playlist player as one or more songs.
+ \param item The item to check
+ \return True if playable, false otherwise.
+ */
+bool IsItemPlayable(const CFileItem& item);
+
+} // namespace MUSIC_UTILS
diff --git a/xbmc/music/Song.cpp b/xbmc/music/Song.cpp
new file mode 100644
index 0000000..165889c
--- /dev/null
+++ b/xbmc/music/Song.cpp
@@ -0,0 +1,373 @@
+/*
+ * 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 "Song.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace MUSIC_INFO;
+
+CSong::CSong(CFileItem& item)
+{
+ CMusicInfoTag& tag = *item.GetMusicInfoTag();
+ strTitle = tag.GetTitle();
+ genre = tag.GetGenre();
+ strArtistDesc = tag.GetArtistString();
+ //Set sort string before processing artist credits
+ strArtistSort = tag.GetArtistSort();
+ m_strComposerSort = tag.GetComposerSort();
+
+ // Determine artist credits from various tag arrays
+ SetArtistCredits(tag.GetArtist(), tag.GetMusicBrainzArtistHints(), tag.GetMusicBrainzArtistID());
+
+ strAlbum = tag.GetAlbum();
+ m_albumArtist = tag.GetAlbumArtist();
+ // Separate album artist names further, if possible, and trim blank space.
+ if (tag.GetMusicBrainzAlbumArtistHints().size() > m_albumArtist.size())
+ // Make use of hints (ALBUMARTISTS tag), when present, to separate artist names
+ m_albumArtist = tag.GetMusicBrainzAlbumArtistHints();
+ else
+ // Split album artist names further using multiple possible delimiters, over single separator applied in Tag loader
+ m_albumArtist = StringUtils::SplitMulti(m_albumArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicArtistSeparators);
+ for (auto artistname : m_albumArtist)
+ StringUtils::Trim(artistname);
+ m_strAlbumArtistSort = tag.GetAlbumArtistSort();
+
+ strMusicBrainzTrackID = tag.GetMusicBrainzTrackID();
+ m_musicRoles = tag.GetContributors();
+ strComment = tag.GetComment();
+ strCueSheet = tag.GetCueSheet();
+ strMood = tag.GetMood();
+ rating = tag.GetRating();
+ userrating = tag.GetUserrating();
+ votes = tag.GetVotes();
+ strOrigReleaseDate = tag.GetOriginalDate();
+ strReleaseDate = tag.GetReleaseDate();
+ strDiscSubtitle = tag.GetDiscSubtitle();
+ iTrack = tag.GetTrackAndDiscNumber();
+ iDuration = tag.GetDuration();
+ strRecordLabel = tag.GetRecordLabel();
+ strAlbumType = tag.GetMusicBrainzReleaseType();
+ bCompilation = tag.GetCompilation();
+ embeddedArt = tag.GetCoverArtInfo();
+ strFileName = tag.GetURL().empty() ? item.GetPath() : tag.GetURL();
+ dateAdded = tag.GetDateAdded();
+ replayGain = tag.GetReplayGain();
+ strThumb = item.GetUserMusicThumb(true);
+ iStartOffset = static_cast<int>(item.GetStartOffset());
+ iEndOffset = static_cast<int>(item.GetEndOffset());
+ idSong = -1;
+ iTimesPlayed = 0;
+ idAlbum = -1;
+ iBPM = tag.GetBPM();
+ iSampleRate = tag.GetSampleRate();
+ iBitRate = tag.GetBitRate();
+ iChannels = tag.GetNoOfChannels();
+}
+
+CSong::CSong()
+{
+ Clear();
+}
+
+void CSong::SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints,
+ const std::vector<std::string>& mbids)
+{
+ artistCredits.clear();
+ std::vector<std::string> artistHints = hints;
+ //Split the artist sort string to try and get sort names for individual artists
+ std::vector<std::string> artistSort = StringUtils::Split(strArtistSort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+
+ if (!mbids.empty())
+ { // Have musicbrainz artist info, so use it
+
+ // Vector of possible separators in the order least likely to be part of artist name
+ const std::vector<std::string> separators{ " feat. ", " ft. ", " Feat. "," Ft. ", ";", ":", "|", "#", "/", " with ", ",", "&" };
+
+ // Establish tag consistency - do the number of musicbrainz ids and number of names in hints or artist match
+ if (mbids.size() != artistHints.size() && mbids.size() != names.size())
+ {
+ // Tags mismatch - report it and then try to fix
+ CLog::Log(LOGDEBUG, "Mismatch in song file tags: {} mbid {} names {} {}", (int)mbids.size(),
+ (int)names.size(), strTitle, strArtistDesc);
+ /*
+ Most likely we have no hints and a single artist name like "Artist1 feat. Artist2"
+ or "Composer; Conductor, Orchestra, Soloist" or "Artist1/Artist2" where the
+ expected single item separator (default = space-slash-space) as not been used.
+ Ampersand (&), comma and slash (no spaces) are poor delimiters as could be in name
+ e.g. "AC/DC", "Earth, Wind & Fire", but here treat them as such in attempt to find artist names.
+ When there are hints but count not match mbid they could be poorly formatted using unexpected
+ separators so attempt to split them. Or we could have more hints or artist names than
+ musicbrainz id so ignore them but raise warning.
+ */
+ // Do hints exist yet mismatch
+ if (artistHints.size() > 0 &&
+ artistHints.size() != mbids.size())
+ {
+ if (names.size() == mbids.size())
+ // Artist name count matches, use that as hints
+ artistHints = names;
+ else if (artistHints.size() < mbids.size())
+ { // Try splitting the hints until have matching number
+ artistHints = StringUtils::SplitMulti(artistHints, separators, mbids.size());
+ }
+ else
+ // Extra hints, discard them.
+ artistHints.resize(mbids.size());
+ }
+ // Do hints not exist or still mismatch, try artists
+ if (artistHints.size() != mbids.size())
+ artistHints = names;
+ // Still mismatch, try splitting the hints (now artists) until have matching number
+ if (artistHints.size() < mbids.size())
+ {
+ artistHints = StringUtils::SplitMulti(artistHints, separators, mbids.size());
+ }
+ }
+ else
+ { // Either hints or artist names (or both) matches number of musicbrainz id
+ // If hints mismatch, use artists
+ if (artistHints.size() != mbids.size())
+ artistHints = names;
+ }
+
+ // Try to get number of artist sort names and musicbrainz ids to match. Split sort names
+ // further using multiple possible delimiters, over single separator applied in Tag loader
+ if (artistSort.size() != mbids.size())
+ artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" });
+
+ for (size_t i = 0; i < mbids.size(); i++)
+ {
+ std::string artistId = mbids[i];
+ std::string artistName;
+ /*
+ We try and get the corresponding artist name from the hints list.
+ Having already attempted to make the number of hints match, if they
+ still don't then use musicbrainz id as the name and hope later on we
+ can update that entry.
+ */
+ if (i < artistHints.size())
+ artistName = artistHints[i];
+ else
+ artistName = artistId;
+
+ // Use artist sort name providing we have as many as we have mbid,
+ // otherwise something is wrong with them so ignore and leave blank
+ if (artistSort.size() == mbids.size())
+ artistCredits.emplace_back(StringUtils::Trim(artistName), StringUtils::Trim(artistSort[i]), artistId);
+ else
+ artistCredits.emplace_back(StringUtils::Trim(artistName), "", artistId);
+ }
+ }
+ else
+ { // No musicbrainz artist ids, so fill in directly
+ // Separate artist names further, if possible, and trim blank space.
+ std::vector<std::string> artists = names;
+ if (artistHints.size() > names.size())
+ // Make use of hints (ARTISTS tag), when present, to separate artist names
+ artists = artistHints;
+ else
+ // Split artist names further using multiple possible delimiters, over single separator applied in Tag loader
+ artists = StringUtils::SplitMulti(artists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicArtistSeparators);
+
+ if (artistSort.size() != artists.size())
+ // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader
+ artistSort = StringUtils::SplitMulti(artistSort, { ";", ":", "|", "#" });
+
+ for (size_t i = 0; i < artists.size(); i++)
+ {
+ artistCredits.emplace_back(StringUtils::Trim(artists[i]));
+ // Set artist sort name providing we have as many as we have artists,
+ // otherwise something is wrong with them so ignore rather than guess.
+ if (artistSort.size() == artists.size())
+ artistCredits.back().SetSortName(StringUtils::Trim(artistSort[i]));
+ }
+ }
+
+}
+
+void CSong::MergeScrapedSong(const CSong& source, bool override)
+{
+ // Merge when MusicBrainz Track ID match (checked in CAlbum::MergeScrapedAlbum)
+ if ((override && !source.strTitle.empty()) || strTitle.empty())
+ strTitle = source.strTitle;
+ if ((override && source.iTrack != 0) || iTrack == 0)
+ iTrack = source.iTrack;
+ if (override)
+ {
+ artistCredits = source.artistCredits; // Replace artists and store mbid returned by scraper
+ strArtistDesc.clear(); // @todo: set artist display string e.g. "artist1 feat. artist2" when scraped
+ }
+}
+
+void CSong::Serialize(CVariant& value) const
+{
+ value["filename"] = strFileName;
+ value["title"] = strTitle;
+ value["artist"] = GetArtist();
+ value["artistsort"] = GetArtistSort(); // a string for the song not vector of values for each artist
+ value["album"] = strAlbum;
+ value["albumartist"] = GetAlbumArtist();
+ value["genre"] = genre;
+ value["duration"] = iDuration;
+ value["track"] = iTrack;
+ value["year"] = atoi(strReleaseDate.c_str());;
+ value["musicbrainztrackid"] = strMusicBrainzTrackID;
+ value["comment"] = strComment;
+ value["mood"] = strMood;
+ value["rating"] = rating;
+ value["userrating"] = userrating;
+ value["votes"] = votes;
+ value["timesplayed"] = iTimesPlayed;
+ value["lastplayed"] = lastPlayed.IsValid() ? lastPlayed.GetAsDBDateTime() : "";
+ value["dateadded"] = dateAdded.IsValid() ? dateAdded.GetAsDBDateTime() : "";
+ value["albumid"] = idAlbum;
+ value["albumreleasedate"] = strReleaseDate;
+ value["bpm"] = iBPM;
+ value["bitrate"] = iBitRate;
+ value["samplerate"] = iSampleRate;
+ value["channels"] = iChannels;
+}
+
+void CSong::Clear()
+{
+ strFileName.clear();
+ strTitle.clear();
+ strAlbum.clear();
+ strArtistSort.clear();
+ strArtistDesc.clear();
+ m_albumArtist.clear();
+ m_strAlbumArtistSort.clear();
+ genre.clear();
+ strThumb.clear();
+ strMusicBrainzTrackID.clear();
+ m_musicRoles.clear();
+ strComment.clear();
+ strMood.clear();
+ rating = 0;
+ userrating = 0;
+ votes = 0;
+ iTrack = 0;
+ iDuration = 0;
+ strOrigReleaseDate.clear();
+ strReleaseDate.clear();
+ strDiscSubtitle.clear();
+ iStartOffset = 0;
+ iEndOffset = 0;
+ idSong = -1;
+ iTimesPlayed = 0;
+ lastPlayed.Reset();
+ dateAdded.Reset();
+ dateUpdated.Reset();
+ dateNew.Reset();
+ idAlbum = -1;
+ bCompilation = false;
+ embeddedArt.Clear();
+ iBPM = 0;
+ iBitRate = 0;
+ iSampleRate = 0;
+ iChannels = 0;
+
+ replayGain = ReplayGain();
+}
+const std::vector<std::string> CSong::GetArtist() const
+{
+ //Get artist names as vector from artist credits
+ std::vector<std::string> songartists;
+ for (const auto& artistCredit : artistCredits)
+ {
+ songartists.push_back(artistCredit.GetArtist());
+ }
+ //When artist credits have not been populated attempt to build an artist vector from the description string
+ //This is a temporary fix, in the longer term other areas should query the song_artist table and populate
+ //artist credits. Note that splitting the string may not give the same artists as held in the song_artist table
+ if (songartists.empty() && !strArtistDesc.empty())
+ songartists = StringUtils::Split(strArtistDesc, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ return songartists;
+}
+
+const std::string CSong::GetArtistSort() const
+{
+ //The stored artist sort name string takes precedence but a
+ //value could be created from individual sort names held in artistcredits
+ if (!strArtistSort.empty())
+ return strArtistSort;
+ std::vector<std::string> artistvector;
+ for (const auto& artistcredit : artistCredits)
+ if (!artistcredit.GetSortName().empty())
+ artistvector.emplace_back(artistcredit.GetSortName());
+ std::string artistString;
+ if (!artistvector.empty())
+ artistString = StringUtils::Join(artistvector, "; ");
+ return artistString;
+}
+
+const std::vector<std::string> CSong::GetMusicBrainzArtistID() const
+{
+ //Get artist MusicBrainz IDs as vector from artist credits
+ std::vector<std::string> musicBrainzID;
+ for (const auto& artistCredit : artistCredits)
+ {
+ musicBrainzID.push_back(artistCredit.GetMusicBrainzArtistID());
+ }
+ return musicBrainzID;
+}
+
+const std::string CSong::GetArtistString() const
+{
+ //Artist description may be different from the artists in artistcredits (see ARTISTS tag processing)
+ //but is takes precedence as a string because artistcredits is not always filled during processing
+ if (!strArtistDesc.empty())
+ return strArtistDesc;
+ std::vector<std::string> artistvector;
+ for (const auto& i : artistCredits)
+ artistvector.push_back(i.GetArtist());
+ std::string artistString;
+ if (!artistvector.empty())
+ artistString = StringUtils::Join(artistvector, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ return artistString;
+}
+
+const std::vector<int> CSong::GetArtistIDArray() const
+{
+ // Get song artist IDs for json rpc
+ std::vector<int> artistids;
+ for (const auto& artistCredit : artistCredits)
+ artistids.push_back(artistCredit.GetArtistId());
+ return artistids;
+}
+
+void CSong::AppendArtistRole(const CMusicRole& musicRole)
+{
+ m_musicRoles.push_back(musicRole);
+}
+
+bool CSong::HasArt() const
+{
+ if (!strThumb.empty()) return true;
+ if (!embeddedArt.Empty()) return true;
+ return false;
+}
+
+bool CSong::ArtMatches(const CSong &right) const
+{
+ return (right.strThumb == strThumb &&
+ embeddedArt.Matches(right.embeddedArt));
+}
+
+const std::string CSong::GetDiscSubtitle() const
+{
+ return strDiscSubtitle;
+}
diff --git a/xbmc/music/Song.h b/xbmc/music/Song.h
new file mode 100644
index 0000000..3fc127a
--- /dev/null
+++ b/xbmc/music/Song.h
@@ -0,0 +1,224 @@
+/*
+ * 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
+
+/*!
+ \file Song.h
+\brief
+*/
+
+#include "Artist.h"
+#include "XBDateTime.h"
+#include "music/tags/ReplayGain.h"
+#include "utils/EmbeddedArt.h"
+#include "utils/ISerializable.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+class CVariant;
+
+/*!
+ \ingroup music
+ \brief Class to store and read album information from CMusicDatabase
+ \sa CSong, CMusicDatabase
+ */
+
+class CGenre
+{
+public:
+ int idGenre;
+ std::string strGenre;
+};
+
+class CFileItem;
+
+/*!
+ \ingroup music
+ \brief Class to store and read song information from CMusicDatabase
+ \sa CAlbum, CMusicDatabase
+ */
+class CSong final : public ISerializable
+{
+public:
+ CSong() ;
+ explicit CSong(CFileItem& item);
+ void Clear() ;
+ void MergeScrapedSong(const CSong& source, bool override);
+ void Serialize(CVariant& value) const override;
+
+ bool operator<(const CSong &song) const
+ {
+ if (strFileName < song.strFileName) return true;
+ if (strFileName > song.strFileName) return false;
+ if (iTrack < song.iTrack) return true;
+ return false;
+ }
+
+ /*! \brief Get artist names from the vector of artistcredits objects
+ \return artist names as a vector of strings
+ */
+ const std::vector<std::string> GetArtist() const;
+
+ /*! \brief Get artist sort name string
+ \return artist sort name as a single string
+ */
+ const std::string GetArtistSort() const;
+
+ /*! \brief Get artist MusicBrainz IDs from the vector of artistcredits objects
+ \return artist MusicBrainz IDs as a vector of strings
+ */
+ const std::vector<std::string> GetMusicBrainzArtistID() const;
+
+ /*! \brief Get artist names from the artist description string (if it exists)
+ or concatenated from the vector of artistcredits objects
+ \return artist names as a single string
+ */
+ const std::string GetArtistString() const;
+
+ /*! \brief Get song artist IDs (for json rpc) from the vector of artistcredits objects
+ \return album artist IDs as a vector of integers
+ */
+ const std::vector<int> GetArtistIDArray() const;
+
+ /*! \brief Get album artist names associated with song from tag data
+ Note for initial album processing only, normalised album artist data belongs to album
+ and is stored in album artist credits
+ \return album artist names as a vector of strings
+ */
+ const std::vector<std::string> GetAlbumArtist() const { return m_albumArtist; }
+
+ /*! \brief Get album artist sort name string
+ \return album artist sort name as a single string
+ */
+ const std::string GetAlbumArtistSort() const { return m_strAlbumArtistSort; }
+
+ /*! \brief Get disc subtitle string where one exists
+ \return disc subtitle as a single string
+ */
+ const std::string GetDiscSubtitle() const;
+
+ /*! \brief Get composer sort name string
+ \return composer sort name as a single string
+ */
+ const std::string GetComposerSort() const { return m_strComposerSort; }
+
+ /*! \brief Get the full list of artist names and the role each played for those
+ that contributed to the recording. Given in music file tags other than ARTIST
+ or ALBUMARTIST, e.g. COMPOSER or CONDUCTOR etc.
+ \return a vector of all contributing artist names and their roles
+ */
+ const VECMUSICROLES& GetContributors() const { return m_musicRoles; }
+ //void AddArtistRole(const int &role, const std::string &artist);
+ void AppendArtistRole(const CMusicRole& musicRole);
+
+ /*! \brief Set album artist vector.
+ Album artist is held local to song until album created for initial processing only.
+ Normalised album artist data belongs to album and is stored in album artist credits
+ \param album artist names as a vector of strings
+ */
+ void SetAlbumArtist(const std::vector<std::string>& albumartists) { m_albumArtist = albumartists; }
+
+ /*! \brief Whether this song has any artists in artist credits vector
+ Tests if artist credits has been populated yet, during processing there can be
+ artists in the artist description but not yet in the credits
+ */
+ bool HasArtistCredits() const { return !artistCredits.empty(); }
+
+ /*! \brief Whether this song has any artists in music roles (contributors) vector
+ Tests if contributors has been populated yet, there may be none.
+ */
+ bool HasContributors() const { return !m_musicRoles.empty(); }
+
+ /*! \brief whether this song has art associated with it
+ Tests both the strThumb and embeddedArt members.
+ */
+ bool HasArt() const;
+
+ /*! \brief whether the art from this song matches the art from another
+ Tests both the strThumb and embeddedArt members.
+ */
+ bool ArtMatches(const CSong &right) const;
+
+ /*! \brief Set artist credits using the arrays of tag values.
+ If strArtistSort (as from ARTISTSORT tag) is already set then individual
+ artist sort names are also processed.
+ \param names String vector of artist names (as from ARTIST tag)
+ \param hints String vector of artist name hints (as from ARTISTS tag)
+ \param mbids String vector of artist Musicbrainz IDs (as from MUSICBRAINZARTISTID tag)
+ */
+ void SetArtistCredits(const std::vector<std::string>& names, const std::vector<std::string>& hints,
+ const std::vector<std::string>& mbids);
+
+ int idSong;
+ int idAlbum;
+ std::string strFileName;
+ std::string strTitle;
+ std::string strArtistSort;
+ std::string strArtistDesc;
+ VECARTISTCREDITS artistCredits;
+ std::string strAlbum;
+ std::vector<std::string> genre;
+ std::string strThumb;
+ EmbeddedArtInfo embeddedArt;
+ std::string strMusicBrainzTrackID;
+ std::string strComment;
+ std::string strMood;
+ std::string strCueSheet;
+ float rating;
+ int userrating;
+ int votes;
+ int iTrack;
+ int iDuration;
+ std::string strOrigReleaseDate;
+ std::string strReleaseDate;
+ std::string strDiscSubtitle;
+ int iTimesPlayed;
+ CDateTime lastPlayed;
+ CDateTime dateAdded; // File creation or modification time, or when tags (re-)scanned
+ CDateTime dateUpdated; // Time db record Last modified
+ CDateTime dateNew; // Time db record created
+ int iStartOffset;
+ int iEndOffset;
+ bool bCompilation;
+ int iBPM;
+ int iSampleRate;
+ int iBitRate;
+ int iChannels;
+ std::string strRecordLabel; // Record label from tag for album processing by CMusicInfoScanner::FileItemsToAlbums
+ std::string strAlbumType; // (Musicbrainz release type) album type from tag for album processing by CMusicInfoScanner::FileItemsToAlbums
+
+ ReplayGain replayGain;
+private:
+ std::vector<std::string> m_albumArtist; // Album artist from tag for album processing, no desc or MBID
+ std::string m_strAlbumArtistSort; // Albumartist sort string from tag for album processing by CMusicInfoScanner::FileItemsToAlbums
+ std::string m_strComposerSort;
+ VECMUSICROLES m_musicRoles;
+};
+
+/*!
+ \ingroup music
+ \brief A vector of CSong objects, used for CMusicDatabase
+ \sa CMusicDatabase
+ */
+typedef std::vector<CSong> VECSONGS;
+
+/*!
+ \ingroup music
+ \brief A map of a vector of CSong objects key by filename, used for CMusicDatabase
+ */
+typedef std::map<std::string, VECSONGS> MAPSONGS;
+
+/*!
+ \ingroup music
+ \brief A vector of std::string objects, used for CMusicDatabase
+ \sa CMusicDatabase
+ */
+typedef std::vector<CGenre> VECGENRES;
diff --git a/xbmc/music/dialogs/CMakeLists.txt b/xbmc/music/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..40b6e90
--- /dev/null
+++ b/xbmc/music/dialogs/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES GUIDialogInfoProviderSettings.cpp
+ GUIDialogMusicInfo.cpp
+ GUIDialogMusicOSD.cpp
+ GUIDialogSongInfo.cpp
+ GUIDialogVisualisationPresetList.cpp)
+
+set(HEADERS GUIDialogInfoProviderSettings.h
+ GUIDialogMusicInfo.h
+ GUIDialogMusicOSD.h
+ GUIDialogSongInfo.h
+ GUIDialogVisualisationPresetList.h)
+
+core_add_library(music_dialogs)
diff --git a/xbmc/music/dialogs/GUIDialogInfoProviderSettings.cpp b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.cpp
new file mode 100644
index 0000000..a9e2232
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.cpp
@@ -0,0 +1,479 @@
+/*
+ * Copyright (C) 2017-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 "GUIDialogInfoProviderSettings.h"
+
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "filesystem/AddonsDirectory.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/builtins/Builtins.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "storage/MediaManager.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <limits.h>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace ADDON;
+
+const std::string SETTING_ALBUMSCRAPER_SETTINGS = "albumscrapersettings";
+const std::string SETTING_ARTISTSCRAPER_SETTINGS = "artistscrapersettings";
+const std::string SETTING_APPLYTOITEMS = "applysettingstoitems";
+
+CGUIDialogInfoProviderSettings::CGUIDialogInfoProviderSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_INFOPROVIDER_SETTINGS, "DialogSettings.xml")
+{ }
+
+bool CGUIDialogInfoProviderSettings::Show()
+{
+ CGUIDialogInfoProviderSettings *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogInfoProviderSettings>(WINDOW_DIALOG_INFOPROVIDER_SETTINGS);
+ if (!dialog)
+ return false;
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ dialog->m_showSingleScraper = false;
+
+ // Get current default info provider settings from service broker
+ dialog->m_fetchInfo = settings->GetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO);
+
+ ADDON::AddonPtr defaultScraper;
+ // Get default album scraper (when enabled - can default scraper be disabled??)
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ALBUMS,
+ defaultScraper))
+ {
+ ADDON::ScraperPtr scraper = std::dynamic_pointer_cast<ADDON::CScraper>(defaultScraper);
+ dialog->SetAlbumScraper(scraper);
+ }
+
+ // Get default artist scraper
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ARTISTS,
+ defaultScraper))
+ {
+ ADDON::ScraperPtr scraper = std::dynamic_pointer_cast<ADDON::CScraper>(defaultScraper);
+ dialog->SetArtistScraper(scraper);
+ }
+
+ dialog->m_strArtistInfoPath = settings->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+
+ dialog->Open();
+
+ dialog->ResetDefaults();
+ return dialog->IsConfirmed();
+}
+
+int CGUIDialogInfoProviderSettings::Show(ADDON::ScraperPtr& scraper)
+{
+ CGUIDialogInfoProviderSettings *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogInfoProviderSettings>(WINDOW_DIALOG_INFOPROVIDER_SETTINGS);
+ if (!dialog || !scraper)
+ return -1;
+ if (scraper->Content() != CONTENT_ARTISTS && scraper->Content() != CONTENT_ALBUMS)
+ return -1;
+
+ dialog->m_showSingleScraper = true;
+ dialog->m_singleScraperType = scraper->Content();
+
+ if (dialog->m_singleScraperType == CONTENT_ALBUMS)
+ dialog->SetAlbumScraper(scraper);
+ else
+ dialog->SetArtistScraper(scraper);
+ // toast selected but disabled scrapers
+ if (CServiceBroker::GetAddonMgr().IsAddonDisabled(scraper->ID()))
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(24024), scraper->Name(), 2000, true);
+
+ dialog->Open();
+
+ bool confirmed = dialog->IsConfirmed();
+ unsigned int applyToItems = dialog->m_applyToItems;
+ if (confirmed)
+ {
+ if (dialog->m_singleScraperType == CONTENT_ALBUMS)
+ scraper = dialog->GetAlbumScraper();
+ else
+ {
+ scraper = dialog->GetArtistScraper();
+ // Save artist information folder (here not in the caller) when applying setting as default for all artists
+ if (applyToItems == INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_DEFAULT)
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, dialog->m_strArtistInfoPath);
+ }
+ if (scraper)
+ scraper->SetPathSettings(dialog->m_singleScraperType, "");
+ }
+
+ dialog->ResetDefaults();
+
+ if (confirmed)
+ return applyToItems;
+ else
+ return -1;
+}
+
+void CGUIDialogInfoProviderSettings::OnInitWindow()
+{
+ CGUIDialogSettingsManualBase::OnInitWindow();
+}
+
+void CGUIDialogInfoProviderSettings::OnSettingChanged(
+ const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO)
+ {
+ m_fetchInfo = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ SetupView();
+ SetFocus(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO);
+ }
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER)
+ m_strArtistInfoPath = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_APPLYTOITEMS)
+ {
+ m_applyToItems = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ SetupView();
+ SetFocus(SETTING_APPLYTOITEMS);
+ }
+}
+
+void CGUIDialogInfoProviderSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER)
+ {
+ std::string currentScraperId;
+ if (m_albumscraper)
+ currentScraperId = m_albumscraper->ID();
+ std::string selectedAddonId = currentScraperId;
+
+ if (CGUIWindowAddonBrowser::SelectAddonID(AddonType::SCRAPER_ALBUMS, selectedAddonId, false) ==
+ 1 &&
+ selectedAddonId != currentScraperId)
+ {
+ AddonPtr scraperAddon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(selectedAddonId, scraperAddon,
+ OnlyEnabled::CHOICE_YES))
+ {
+ m_albumscraper = std::dynamic_pointer_cast<CScraper>(scraperAddon);
+ SetupView();
+ SetFocus(settingId);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{} - Could not get settings for addon: {}", __FUNCTION__,
+ selectedAddonId);
+ }
+ }
+ }
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER)
+ {
+ std::string currentScraperId;
+ if (m_artistscraper)
+ currentScraperId = m_artistscraper->ID();
+ std::string selectedAddonId = currentScraperId;
+
+ if (CGUIWindowAddonBrowser::SelectAddonID(AddonType::SCRAPER_ARTISTS, selectedAddonId, false) ==
+ 1 &&
+ selectedAddonId != currentScraperId)
+ {
+ AddonPtr scraperAddon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(selectedAddonId, scraperAddon,
+ OnlyEnabled::CHOICE_YES))
+ {
+ m_artistscraper = std::dynamic_pointer_cast<CScraper>(scraperAddon);
+ SetupView();
+ SetFocus(settingId);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{} - Could not get addon: {}", __FUNCTION__, selectedAddonId);
+ }
+ }
+ }
+ else if (settingId == SETTING_ALBUMSCRAPER_SETTINGS)
+ CGUIDialogAddonSettings::ShowForAddon(m_albumscraper, false);
+ else if (settingId == SETTING_ARTISTSCRAPER_SETTINGS)
+ CGUIDialogAddonSettings::ShowForAddon(m_artistscraper, false);
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER)
+ {
+ VECSOURCES shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ CServiceBroker::GetMediaManager().GetNetworkLocations(shares);
+ CServiceBroker::GetMediaManager().GetRemovableDrives(shares);
+ std::string strDirectory = m_strArtistInfoPath;
+ if (!strDirectory.empty())
+ {
+ URIUtils::AddSlashAtEnd(strDirectory);
+ bool bIsSource;
+ if (CUtil::GetMatchingSource(strDirectory, shares, bIsSource) < 0) // path is outside shares - add it as a separate one
+ {
+ CMediaSource share;
+ share.strName = g_localizeStrings.Get(13278);
+ share.strPath = strDirectory;
+ shares.push_back(share);
+ }
+ }
+ else
+ strDirectory = "default location";
+
+ if (CGUIDialogFileBrowser::ShowAndGetDirectory(shares, g_localizeStrings.Get(20223), strDirectory, true))
+ {
+ if (!strDirectory.empty())
+ {
+ m_strArtistInfoPath = strDirectory;
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, strDirectory);
+ SetFocus(CSettings::CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ }
+ }
+ }
+}
+
+bool CGUIDialogInfoProviderSettings::Save()
+{
+ if (m_showSingleScraper)
+ return true; //Save done by caller of ::Show
+
+ // Save default settings for fetching additional information and art
+ CLog::Log(LOGINFO, "{} called", __FUNCTION__);
+ // Save Fetch addiitional info during update
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO, m_fetchInfo);
+ // Save default scrapers and addon setting values
+ settings->SetString(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, m_albumscraper->ID());
+ m_albumscraper->SaveSettings();
+ settings->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, m_artistscraper->ID());
+ m_artistscraper->SaveSettings();
+ // Save artist information folder
+ settings->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, m_strArtistInfoPath);
+ settings->Save();
+
+ return true;
+}
+
+void CGUIDialogInfoProviderSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, m_strArtistInfoPath);
+
+ if (!m_showSingleScraper)
+ {
+ SetHeading(38330);
+ if (!m_fetchInfo)
+ {
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, false);
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false);
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, false);
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false);
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, false);
+ }
+ else
+ { // Album scraper
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, true);
+ if (m_albumscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_albumscraper->ID()))
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, m_albumscraper->Name());
+ if (m_albumscraper && m_albumscraper->HasSettings())
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, true);
+ else
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false);
+ }
+ else
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None"
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false);
+ }
+ // Artist scraper
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, true);
+ if (m_artistscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_artistscraper->ID()))
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, m_artistscraper->Name());
+ if (m_artistscraper && m_artistscraper->HasSettings())
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, true);
+ else
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false);
+ }
+ else
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None"
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false);
+ }
+ // Artist Information Folder
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, true);
+ }
+ }
+ else if (m_singleScraperType == CONTENT_ALBUMS)
+ {
+ SetHeading(38331);
+ // Album scraper
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, true);
+ if (m_albumscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_albumscraper->ID()))
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, m_albumscraper->Name());
+ if (m_albumscraper && m_albumscraper->HasSettings())
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, true);
+ else
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false);
+ }
+ else
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None"
+ ToggleState(SETTING_ALBUMSCRAPER_SETTINGS, false);
+ }
+ }
+ else
+ {
+ SetHeading(38332);
+ // Artist scraper
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, true);
+ if (m_artistscraper && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_artistscraper->ID()))
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, m_artistscraper->Name());
+ if (m_artistscraper && m_artistscraper->HasSettings())
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, true);
+ else
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false);
+ }
+ else
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, g_localizeStrings.Get(231)); //Set label2 to "None"
+ ToggleState(SETTING_ARTISTSCRAPER_SETTINGS, false);
+ }
+ // Artist Information Folder when default settings
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, m_applyToItems == INFOPROVIDER_DEFAULT);
+ }
+}
+
+void CGUIDialogInfoProviderSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ std::shared_ptr<CSettingCategory> category = AddCategory("infoprovidersettings", -1);
+ if (category == nullptr)
+ {
+ CLog::Log(LOGERROR, "{}: unable to setup settings", __FUNCTION__);
+ return;
+ }
+ std::shared_ptr<CSettingGroup> group1 = AddGroup(category);
+ if (group1 == nullptr)
+ {
+ CLog::Log(LOGERROR, "{}: unable to setup settings", __FUNCTION__);
+ return;
+ }
+
+ if (!m_showSingleScraper)
+ {
+ AddToggle(group1, CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO, 38333, SettingLevel::Basic, m_fetchInfo); // "Fetch additional information during scan"
+ }
+ else
+ {
+ TranslatableIntegerSettingOptions entries;
+ entries.clear();
+ if (m_singleScraperType == CONTENT_ALBUMS)
+ {
+ entries.push_back(TranslatableIntegerSettingOption(38066, INFOPROVIDER_THISITEM));
+ entries.push_back(TranslatableIntegerSettingOption(38067, INFOPROVIDER_ALLVIEW));
+ }
+ else
+ {
+ entries.push_back(TranslatableIntegerSettingOption(38064, INFOPROVIDER_THISITEM));
+ entries.push_back(TranslatableIntegerSettingOption(38065, INFOPROVIDER_ALLVIEW));
+ }
+ entries.push_back(TranslatableIntegerSettingOption(38063, INFOPROVIDER_DEFAULT));
+ AddList(group1, SETTING_APPLYTOITEMS, 38338, SettingLevel::Basic, m_applyToItems, entries, 38339); // "Apply settings to"
+ }
+
+ std::shared_ptr<CSettingGroup> group = AddGroup(category, 38337);
+ if (group == nullptr)
+ {
+ CLog::Log(LOGERROR, "{}: unable to setup settings", __FUNCTION__);
+ return;
+ }
+ std::shared_ptr<CSettingAction> subsetting;
+ if (!m_showSingleScraper || m_singleScraperType == CONTENT_ALBUMS)
+ {
+ AddButton(group, CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, 38334, SettingLevel::Basic); //Provider for album information
+ subsetting = AddButton(group, SETTING_ALBUMSCRAPER_SETTINGS, 10004, SettingLevel::Basic); //"settings"
+ if (subsetting)
+ subsetting->SetParent(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER);
+ }
+ if (!m_showSingleScraper || m_singleScraperType == CONTENT_ARTISTS)
+ {
+ AddButton(group, CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, 38335, SettingLevel::Basic); //Provider for artist information
+ subsetting = AddButton(group, SETTING_ARTISTSCRAPER_SETTINGS, 10004, SettingLevel::Basic); //"settings"
+ if (subsetting)
+ subsetting->SetParent(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER);
+
+ AddButton(group, CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER, 38336, SettingLevel::Basic);
+ }
+}
+
+void CGUIDialogInfoProviderSettings::SetLabel2(const std::string &settingid, const std::string &label)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ SET_CONTROL_LABEL2(settingControl->GetID(), label);
+}
+
+void CGUIDialogInfoProviderSettings::ToggleState(const std::string &settingid, bool enabled)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ {
+ if (enabled)
+ CONTROL_ENABLE(settingControl->GetID());
+ else
+ CONTROL_DISABLE(settingControl->GetID());
+ }
+}
+
+void CGUIDialogInfoProviderSettings::SetFocus(const std::string &settingid)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ SET_CONTROL_FOCUS(settingControl->GetID(), 0);
+}
+
+void CGUIDialogInfoProviderSettings::ResetDefaults()
+{
+ m_showSingleScraper = false;
+ m_singleScraperType = CONTENT_NONE;
+ m_applyToItems = INFOPROVIDER_THISITEM;
+}
diff --git a/xbmc/music/dialogs/GUIDialogInfoProviderSettings.h b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.h
new file mode 100644
index 0000000..56a33b5
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogInfoProviderSettings.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017-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 "addons/Addon.h"
+#include "addons/Scraper.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <map>
+#include <utility>
+
+class CFileItemList;
+
+// Enumeration of what combination of items to apply the scraper settings
+enum INFOPROVIDERAPPLYOPTIONS
+{
+ INFOPROVIDER_DEFAULT = 0x0000,
+ INFOPROVIDER_ALLVIEW = 0x0001,
+ INFOPROVIDER_THISITEM = 0x0002
+};
+
+class CGUIDialogInfoProviderSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogInfoProviderSettings();
+
+ // specialization of CGUIWindow
+ bool HasListItems() const override { return true; }
+
+ const ADDON::ScraperPtr& GetAlbumScraper() const { return m_albumscraper; }
+ void SetAlbumScraper(ADDON::ScraperPtr scraper) { m_albumscraper = std::move(scraper); }
+ const ADDON::ScraperPtr& GetArtistScraper() const { return m_artistscraper; }
+ void SetArtistScraper(ADDON::ScraperPtr scraper) { m_artistscraper = std::move(scraper); }
+
+ /*! \brief Show dialog to change information provider for either artists or albums (not both).
+ Has a list to select how settings are to be applied - as system default, to just current item or to all the filtered items on the node.
+ This does not save the settings itself, that is left to the caller
+ \param scraper [in/out] the selected scraper addon and settings. Scraper content must be artists or albums.
+ \return 0 settings apply as system default, 1 to all items on node, 2 to just the selected item or -1 if dialog cancelled or error occurs
+ */
+ static int Show(ADDON::ScraperPtr& scraper);
+
+ /*! \brief Show dialog to change the music scraping settings including default information providers for both artists or albums.
+ This saves the settings when the dialog is confirmed.
+ \return true if the dialog is confirmed, false otherwise
+ */
+ static bool Show();
+
+protected:
+ // specializations of CGUIWindow
+ void OnInitWindow() override;
+
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+private:
+ void SetLabel2(const std::string &settingid, const std::string &label);
+ void ToggleState(const std::string &settingid, bool enabled);
+ using CGUIDialogSettingsManualBase::SetFocus;
+ void SetFocus(const std::string &settingid);
+ void ResetDefaults();
+
+ /*!
+ * @brief The currently selected album scraper
+ */
+ ADDON::ScraperPtr m_albumscraper;
+ /*!
+ * @brief The currently selected artist scraper
+ */
+ ADDON::ScraperPtr m_artistscraper;
+
+ std::string m_strArtistInfoPath;
+ bool m_showSingleScraper = false;
+ CONTENT_TYPE m_singleScraperType = CONTENT_NONE;
+ bool m_fetchInfo;
+ unsigned int m_applyToItems = INFOPROVIDER_THISITEM;
+};
diff --git a/xbmc/music/dialogs/GUIDialogMusicInfo.cpp b/xbmc/music/dialogs/GUIDialogMusicInfo.cpp
new file mode 100644
index 0000000..a4b282a
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogMusicInfo.cpp
@@ -0,0 +1,1043 @@
+/*
+ * 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 "GUIDialogMusicInfo.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "URL.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory/QueryParams.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/MusicThumbLoader.h"
+#include "music/MusicUtils.h"
+#include "music/dialogs/GUIDialogSongInfo.h"
+#include "music/infoscanner/MusicInfoScanner.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/windows/GUIWindowMusicBase.h"
+#include "profiles/ProfileManager.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/ProgressJob.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace XFILE;
+using namespace MUSIC_INFO;
+using namespace MUSICDATABASEDIRECTORY;
+using namespace KODI::MESSAGING;
+
+#define CONTROL_BTN_REFRESH 6
+#define CONTROL_USERRATING 7
+#define CONTROL_BTN_PLAY 8
+#define CONTROL_BTN_GET_THUMB 10
+#define CONTROL_ARTISTINFO 12
+
+#define CONTROL_LIST 50
+
+#define TIME_TO_BUSY_DIALOG 500
+
+class CGetInfoJob : public CJob
+{
+public:
+ ~CGetInfoJob(void) override = default;
+
+ // Fetch full album/artist information including art types list
+ bool DoWork() override
+ {
+ CGUIDialogMusicInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager().
+ GetWindow<CGUIDialogMusicInfo>(WINDOW_DIALOG_MUSIC_INFO);
+ if (!dialog)
+ return false;
+ if (dialog->IsCancelled())
+ return false;
+ CFileItemPtr m_item = dialog->GetCurrentListItem();
+ CMusicInfoTag& tag = *m_item->GetMusicInfoTag();
+
+ CMusicDatabase database;
+ database.Open();
+ // May only have partially populated music item, so fetch all artist or album data from db
+ if (tag.GetType() == MediaTypeArtist)
+ {
+ int artistId = tag.GetDatabaseId();
+ CArtist artist;
+ if (!database.GetArtist(artistId, artist))
+ return false;
+ tag.SetArtist(artist);
+ CMusicDatabase::SetPropertiesFromArtist(*m_item, artist);
+ m_item->SetLabel(artist.strArtist);
+
+ // Get artist folder where local art could be found
+ // Get the *name* of the folder for this artist within the Artist Info folder (may not exist).
+ // If there is no Artist Info folder specified in settings this will be blank
+ database.GetArtistPath(artist, artist.strPath);
+ // Get the old location for those album artists with a unique folder (local to music files)
+ // If there is no folder for the artist and *only* the artist this will be blank
+ std::string oldartistpath;
+ bool oldpathfound = database.GetOldArtistPath(artist.idArtist, oldartistpath);
+
+ // Set up path for *item folder when browsing for art, by default this is
+ // in the Artist Info Folder (when it exists), but could end up blank
+ std::string artistItemPath = artist.strPath;
+ if (!CDirectory::Exists(artistItemPath))
+ {
+ // Fall back local to music files (historic location for those album artists with a unique folder)
+ // although there may not be such a unique folder for the arist
+ if (oldpathfound)
+ artistItemPath = oldartistpath;
+ else
+ // Fall back further to browse the Artist Info Folder itself
+ artistItemPath = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ }
+ m_item->SetPath(artistItemPath);
+
+ // Store info as CArtist as well as item properties
+ dialog->SetArtist(artist, oldartistpath);
+
+ // Fetch artist discography as scraped from online sources, but always
+ // include all the albums in the music library
+ dialog->SetDiscography(database);
+ }
+ else
+ {
+ // tag.GetType == MediaTypeAlbum
+ int albumId = tag.GetDatabaseId();
+ CAlbum album;
+ if (!database.GetAlbum(albumId, album))
+ return false;
+ tag.SetAlbum(album);
+ CMusicDatabase::SetPropertiesFromAlbum(*m_item, album);
+
+ // Get album folder where local art could be found
+ database.GetAlbumPath(albumId, album.strPath);
+ // Set up path for *item folder when browsing for art
+ m_item->SetPath(album.strPath);
+ // Store info as CAlbum as well as item properties
+ dialog->SetAlbum(album, album.strPath);
+
+ // Set the list of songs and related art
+ dialog->SetSongs(album.songs);
+ }
+ database.Close();
+
+ /*
+ Load current art (to CGUIListItem.m_art)
+ For albums this includes related artist(s) art and artist fanart set as
+ fallback album fanart.
+ Clear item art first to ensure fresh not cached/partial art
+ */
+ m_item->ClearArt();
+ CMusicThumbLoader loader;
+ loader.LoadItem(m_item.get());
+
+ // Fill vector of possible art types with current art, when it exists,
+ // for display on the art type selection dialog
+ CFileItemList artlist;
+ MUSIC_UTILS::FillArtTypesList(*m_item, artlist);
+ dialog->SetArtTypeList(artlist);
+ if (dialog->IsCancelled())
+ return false;
+
+ // Tell waiting MusicDialog that job is complete
+ dialog->FetchComplete();
+
+ return true;
+ }
+};
+
+class CSetUserratingJob : public CJob
+{
+ int idAlbum;
+ int iUserrating;
+public:
+ CSetUserratingJob(int albumId, int userrating) :
+ idAlbum(albumId),
+ iUserrating(userrating)
+ { }
+
+ ~CSetUserratingJob(void) override = default;
+
+ bool DoWork(void) override
+ {
+ // Asynchronously update userrating in library
+ CMusicDatabase db;
+ if (db.Open())
+ {
+ db.SetAlbumUserrating(idAlbum, iUserrating);
+ db.Close();
+ }
+
+ return true;
+ }
+};
+
+class CRefreshInfoJob : public CProgressJob
+{
+public:
+ CRefreshInfoJob(CGUIDialogProgress* progressDialog)
+ : CProgressJob(nullptr)
+ {
+ if (progressDialog)
+ SetProgressIndicators(nullptr, progressDialog);
+ SetAutoClose(true);
+ }
+
+ ~CRefreshInfoJob(void) override = default;
+
+ // Refresh album/artist information including art types list
+ bool DoWork() override
+ {
+ CGUIDialogMusicInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager().
+ GetWindow<CGUIDialogMusicInfo>(WINDOW_DIALOG_MUSIC_INFO);
+ if (!dialog)
+ return false;
+ if (dialog->IsCancelled())
+ return false;
+ CFileItemPtr m_item = dialog->GetCurrentListItem();
+ CMusicInfoTag& tag = *m_item->GetMusicInfoTag();
+ CArtist& m_artist = dialog->GetArtist();
+ CAlbum& m_album = dialog->GetAlbum();
+
+ CGUIDialogProgress* dlgProgress = GetProgressDialog();
+ CMusicDatabase database;
+ database.Open();
+ if (tag.GetType() == MediaTypeArtist)
+ {
+ ADDON::ScraperPtr scraper;
+ if (!database.GetScraper(m_artist.idArtist, CONTENT_ARTISTS, scraper))
+ return false;
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ database.ClearArtistLastScrapedTime(m_artist.idArtist);
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ CMusicInfoScanner scanner;
+ if (scanner.UpdateArtistInfo(m_artist, scraper, true, dlgProgress) != CInfoScanner::INFO_ADDED)
+ return false;
+ else
+ // Tell info dialog, so can show message
+ dialog->SetScrapedInfo(true);
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ //That changed DB and m_artist, now update dialog item with new info and art
+ tag.SetArtist(m_artist);
+ CMusicDatabase::SetPropertiesFromArtist(*m_item, m_artist);
+
+ // Fetch artist discography as scraped from online sources, but always
+ // include all the albums in the music library
+ dialog->SetDiscography(database);
+ }
+ else
+ {
+ // tag.GetType == MediaTypeAlbum
+ ADDON::ScraperPtr scraper;
+ if (!database.GetScraper(m_album.idAlbum, CONTENT_ALBUMS, scraper))
+ return false;
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ database.ClearAlbumLastScrapedTime(m_album.idAlbum);
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ CMusicInfoScanner scanner;
+ if (scanner.UpdateAlbumInfo(m_album, scraper, true, GetProgressDialog()) != CInfoScanner::INFO_ADDED)
+ return false;
+ else
+ // Tell info dialog, so can show message
+ dialog->SetScrapedInfo(true);
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ //That changed DB and m_album, now update dialog item with new info and art
+ // Album songs are unchanged by refresh (even with Musicbrainz sync?)
+ tag.SetAlbum(m_album);
+ CMusicDatabase::SetPropertiesFromAlbum(*m_item, m_album);
+
+ // Set the list of songs and related art
+ dialog->SetSongs(m_album.songs);
+ }
+ database.Close();
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ /*
+ Load current art (to CGUIListItem.m_art)
+ For albums this includes related artist(s) art and artist fanart set as
+ fallback album fanart.
+ Clear item art first to ensure fresh not cached/partial art
+ */
+ m_item->ClearArt();
+ CMusicThumbLoader loader;
+ loader.LoadItem(m_item.get());
+
+ if (dlgProgress->IsCanceled())
+ return false;
+ // Fill vector of possible art types with current art, when it exists,
+ // for display on the art type selection dialog
+ CFileItemList artlist;
+ MUSIC_UTILS::FillArtTypesList(*m_item, artlist);
+ dialog->SetArtTypeList(artlist);
+ if (dialog->IsCancelled())
+ return false;
+
+ // Tell waiting MusicDialog that job is complete
+ MarkFinished();
+ return true;
+ }
+};
+
+CGUIDialogMusicInfo::CGUIDialogMusicInfo(void)
+ : CGUIDialog(WINDOW_DIALOG_MUSIC_INFO, "DialogMusicInfo.xml"),
+ m_albumSongs(new CFileItemList),
+ m_item(new CFileItem),
+ m_artTypeList(new CFileItemList)
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogMusicInfo::~CGUIDialogMusicInfo(void)
+{
+}
+
+bool CGUIDialogMusicInfo::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ m_artTypeList->Clear();
+ // For albums update user rating if it has changed
+ if (!m_bArtistInfo && m_startUserrating != m_item->GetMusicInfoTag()->GetUserrating())
+ {
+ m_hasUpdatedUserrating = true;
+
+ // Asynchronously update song userrating in library
+ CSetUserratingJob *job = new CSetUserratingJob(m_item->GetMusicInfoTag()->GetAlbumId(),
+ m_item->GetMusicInfoTag()->GetUserrating());
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+ }
+ if (m_hasRefreshed || m_hasUpdatedUserrating)
+ {
+ // Send a message to all windows to tell them to update the item.
+ // This communicates changes to the music lib window.
+ // The music lib window item is updated to but changes to the rating when it is the sort
+ // do not show on screen until refresh() that fetches the list from scratch, sorts etc.
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_item);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_LIST);
+ OnMessage(msg);
+ m_albumSongs->Clear();
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CGUIDialog::OnMessage(message);
+ Update();
+ m_cancelled = false;
+ return true;
+ }
+ break;
+
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_USERRATING)
+ {
+ OnSetUserrating();
+ }
+ else if (iControl == CONTROL_BTN_REFRESH)
+ {
+ RefreshInfo();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_GET_THUMB)
+ {
+ OnGetArt();
+ return true;
+ }
+ else if (iControl == CONTROL_ARTISTINFO)
+ {
+ if (!m_bArtistInfo)
+ OnArtistInfo(m_album.artistCredits[0].GetArtistId());
+ return true;
+ }
+ else if (iControl == CONTROL_LIST)
+ {
+ int iAction = message.GetParam1();
+ if (m_bArtistInfo && (ACTION_SELECT_ITEM == iAction || ACTION_MOUSE_LEFT_CLICK == iAction))
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ int iItem = msg.GetParam1();
+ int id = -1;
+ if (iItem >= 0 && iItem < m_albumSongs->Size())
+ id = m_albumSongs->Get(iItem)->GetMusicInfoTag()->GetDatabaseId();
+ if (id > 0)
+ {
+ OnAlbumInfo(id);
+ return true;
+ }
+ }
+ }
+ else if (iControl == CONTROL_BTN_PLAY)
+ {
+ if (m_album.idAlbum >= 0)
+ {
+ // Play album
+ const std::string path = StringUtils::Format("musicdb://albums/{}", m_album.idAlbum);
+ OnPlayItem(std::make_shared<CFileItem>(path, m_album));
+ return true;
+ }
+ else
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ const int iItem = msg.GetParam1();
+ if (iItem >= 0 && iItem < m_albumSongs->Size())
+ {
+ // Play selected song
+ OnPlayItem(m_albumSongs->Get(iItem));
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogMusicInfo::OnAction(const CAction &action)
+{
+ int userrating = m_item->GetMusicInfoTag()->GetUserrating();
+ if (action.GetID() == ACTION_INCREASE_RATING)
+ {
+ SetUserrating(userrating + 1);
+ return true;
+ }
+ else if (action.GetID() == ACTION_DECREASE_RATING)
+ {
+ SetUserrating(userrating - 1);
+ return true;
+ }
+ else if (action.GetID() == ACTION_SHOW_INFO)
+ {
+ Close();
+ return true;
+ }
+ return CGUIDialog::OnAction(action);
+}
+
+bool CGUIDialogMusicInfo::SetItem(CFileItem* item)
+{
+ *m_item = *item;
+ m_event.Reset();
+ m_cancelled = false; // Happens before win_init
+
+ // In a separate job fetch info and fill list of art types.
+ int jobid =
+ CServiceBroker::GetJobManager()->AddJob(new CGetInfoJob(), nullptr, CJob::PRIORITY_LOW);
+
+ // Wait to get all data before show, allowing user to cancel if fetch is slow
+ if (!CGUIDialogBusy::WaitOnEvent(m_event, TIME_TO_BUSY_DIALOG))
+ {
+ // Cancel job still waiting in queue (unlikely)
+ CServiceBroker::GetJobManager()->CancelJob(jobid);
+ // Flag to stop job already in progress
+ m_cancelled = true;
+ return false;
+ }
+
+ return true;
+}
+
+void CGUIDialogMusicInfo::SetAlbum(const CAlbum& album, const std::string &path)
+{
+ m_album = album;
+ m_item->SetPath(album.strPath);
+
+ m_startUserrating = m_album.iUserrating;
+ m_fallbackartpath.clear();
+ m_bArtistInfo = false;
+ m_hasUpdatedUserrating = false;
+ m_hasRefreshed = false;
+}
+
+void CGUIDialogMusicInfo::SetArtist(const CArtist& artist, const std::string &path)
+{
+ m_artist = artist;
+ m_fallbackartpath = path;
+ m_bArtistInfo = true;
+ m_hasRefreshed = false;
+}
+
+void CGUIDialogMusicInfo::SetSongs(const VECSONGS &songs) const
+{
+ m_albumSongs->Clear();
+ CMusicThumbLoader loader;
+ for (unsigned int i = 0; i < songs.size(); i++)
+ {
+ const CSong& song = songs[i];
+ CFileItemPtr item(new CFileItem(song));
+ // Load the song art and related artist(s) (that may be different from album artist) art
+ loader.LoadItem(item.get());
+ m_albumSongs->Add(item);
+ }
+}
+
+void CGUIDialogMusicInfo::SetDiscography(CMusicDatabase& database) const
+{
+ m_albumSongs->Clear();
+ database.GetArtistDiscography(m_artist.idArtist, *m_albumSongs);
+ CMusicThumbLoader loader;
+ for (const auto& item : *m_albumSongs)
+ {
+ // Load all the album art and related artist(s) art (could be other collaborating artists)
+ loader.LoadItem(item.get());
+ if (item->GetMusicInfoTag()->GetDatabaseId() == -1)
+ item->SetArt("thumb", "DefaultAlbumCover.png");
+ }
+}
+
+void CGUIDialogMusicInfo::Update()
+{
+ if (m_bArtistInfo)
+ {
+ SET_CONTROL_HIDDEN(CONTROL_ARTISTINFO);
+ SET_CONTROL_HIDDEN(CONTROL_USERRATING);
+
+ CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST, 0, 0, m_albumSongs.get());
+ OnMessage(message);
+
+ }
+ else
+ {
+ SET_CONTROL_VISIBLE(CONTROL_ARTISTINFO);
+ SET_CONTROL_VISIBLE(CONTROL_USERRATING);
+
+ CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST, 0, 0, m_albumSongs.get());
+ OnMessage(message);
+
+ }
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // Disable the Choose Art button if the user isn't allowed it
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_GET_THUMB,
+ profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser);
+}
+
+void CGUIDialogMusicInfo::SetLabel(int iControl, const std::string& strLabel)
+{
+ if (strLabel.empty())
+ {
+ SET_CONTROL_LABEL(iControl, 416);
+ }
+ else
+ {
+ SET_CONTROL_LABEL(iControl, strLabel);
+ }
+}
+
+void CGUIDialogMusicInfo::OnInitWindow()
+{
+ SET_CONTROL_LABEL(CONTROL_BTN_REFRESH, 184);
+ SET_CONTROL_LABEL(CONTROL_USERRATING, 38023);
+ SET_CONTROL_LABEL(CONTROL_BTN_GET_THUMB, 13511);
+ SET_CONTROL_LABEL(CONTROL_ARTISTINFO, 21891);
+ SET_CONTROL_LABEL(CONTROL_BTN_PLAY, 208);
+
+ if (m_bArtistInfo)
+ {
+ SET_CONTROL_HIDDEN(CONTROL_ARTISTINFO);
+ SET_CONTROL_HIDDEN(CONTROL_USERRATING);
+ SET_CONTROL_HIDDEN(CONTROL_BTN_PLAY);
+ }
+ CGUIDialog::OnInitWindow();
+}
+
+void CGUIDialogMusicInfo::FetchComplete()
+{
+ //Trigger the event to indicate data has been fetched
+ m_event.Set();
+}
+
+
+void CGUIDialogMusicInfo::RefreshInfo()
+{
+ // Double check we have permission (button should be hidden when not)
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ if (!profileManager->GetCurrentProfile().canWriteDatabases() && !g_passwordManager.bMasterUser)
+ return;
+
+ // Check if scanning
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ HELPERS::ShowOKDialogText(CVariant{ 189 }, CVariant{ 14057 });
+ return;
+ }
+
+ CGUIDialogProgress* dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().
+ GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (!dlgProgress)
+ return;
+
+ if (m_bArtistInfo)
+ { // Show dialog box indicating we're searching for the artist
+ dlgProgress->SetHeading(CVariant{ 21889 });
+ dlgProgress->SetLine(0, CVariant{ m_artist.strArtist });
+ dlgProgress->SetLine(1, CVariant{ "" });
+ dlgProgress->SetLine(2, CVariant{ "" });
+ }
+ else
+ { // Show dialog box indicating we're searching for the album
+ dlgProgress->SetHeading(CVariant{ 185 });
+ dlgProgress->SetLine(0, CVariant{ m_album.strAlbum });
+ dlgProgress->SetLine(1, CVariant{ m_album.strArtistDesc });
+ dlgProgress->SetLine(2, CVariant{ "" });
+ }
+ dlgProgress->Open();
+
+ SetScrapedInfo(false);
+ // Start separate job to scrape info and fill list of art types.
+ CServiceBroker::GetJobManager()->AddJob(new CRefreshInfoJob(dlgProgress), nullptr,
+ CJob::PRIORITY_HIGH);
+
+ // Wait for refresh to complete or be canceled, but render every 10ms so that the
+ // pointer movements works on dialog even when job is reporting progress infrequently
+ if (dlgProgress)
+ dlgProgress->Wait(10);
+
+ if (dlgProgress->IsCanceled())
+ {
+ return;
+ }
+
+ // Show message when scraper was unsuccessful
+ if (!HasScrapedInfo())
+ {
+ if (m_bArtistInfo)
+ HELPERS::ShowOKDialogText(CVariant{ 21889 }, CVariant{ 20199 });
+ else
+ HELPERS::ShowOKDialogText(CVariant{ 185 }, CVariant{ 500 });
+ return;
+ }
+
+ // Show new values on screen
+ Update();
+ m_hasRefreshed = true;
+
+ if (dlgProgress)
+ dlgProgress->Close();
+}
+
+void CGUIDialogMusicInfo::SetUserrating(int userrating) const
+{
+ userrating = std::max(userrating, 0);
+ userrating = std::min(userrating, 10);
+ if (userrating != m_item->GetMusicInfoTag()->GetUserrating())
+ {
+ m_item->GetMusicInfoTag()->SetUserrating(userrating);
+ }
+}
+
+void CGUIDialogMusicInfo::OnAlbumInfo(int id)
+{
+ // Switch to show album info for given album ID
+ // Close current (artist) dialog to save art changes
+ Close(true);
+ ShowForAlbum(id);
+}
+
+void CGUIDialogMusicInfo::OnArtistInfo(int id)
+{
+ // Switch to show artist info for given artist ID
+ // Close current (album) dialog to save art and rating changes
+ Close(true);
+ ShowForArtist(id);
+}
+
+CFileItemPtr CGUIDialogMusicInfo::GetCurrentListItem(int offset)
+{
+ return m_item;
+}
+
+std::string CGUIDialogMusicInfo::GetContent()
+{
+ if (m_item->GetMusicInfoTag()->GetType() == MediaTypeArtist)
+ return "artists";
+ else
+ return "albums";
+}
+
+void CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(VECSOURCES &sources, const CFileItem &item)
+{
+ std::string itemDir;
+ std::string artistFolder;
+
+ itemDir = item.GetPath();
+ if (item.HasMusicInfoTag())
+ {
+ if (item.GetMusicInfoTag()->GetType() == MediaTypeSong)
+ itemDir = URIUtils::GetParentPath(item.GetMusicInfoTag()->GetURL());
+
+ // For artist add Artist Info Folder path to browser sources
+ if (item.GetMusicInfoTag()->GetType() == MediaTypeArtist)
+ {
+ artistFolder = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ if (!artistFolder.empty() && artistFolder.compare(itemDir) == 0)
+ itemDir.clear(); // skip *item when artist not have a unique path
+ }
+ }
+ // Add "*Item folder" path to file browser sources
+ if (!itemDir.empty() && CDirectory::Exists(itemDir))
+ {
+ CMediaSource itemSource;
+ itemSource.strName = g_localizeStrings.Get(36041);
+ itemSource.strPath = itemDir;
+ sources.push_back(itemSource);
+ }
+
+ // For artist add Artist Info Folder path to browser sources
+ if (!artistFolder.empty() && CDirectory::Exists(artistFolder))
+ {
+ CMediaSource itemSource;
+ itemSource.strName = "* " + g_localizeStrings.Get(20223);
+ itemSource.strPath = artistFolder;
+ sources.push_back(itemSource);
+ }
+}
+
+void CGUIDialogMusicInfo::SetArtTypeList(CFileItemList& artlist)
+{
+ m_artTypeList->Clear();
+ m_artTypeList->Copy(artlist);
+}
+
+/*
+Allow user to choose artwork for the artist or album
+For each type of art the options are:
+1. Current art
+2. Local art (thumb found by filename)
+3. Remote art (scraped list of urls from online sources e.g. fanart.tv)
+5. Embedded art (@todo)
+6. None
+*/
+void CGUIDialogMusicInfo::OnGetArt()
+{
+ std::string type = MUSIC_UTILS::ShowSelectArtTypeDialog(*m_artTypeList);
+ if (type.empty())
+ return; // Cancelled
+
+ CFileItemList items;
+ CGUIListItem::ArtMap primeArt = m_item->GetArt(); // art without fallbacks
+ bool bHasArt = m_item->HasArt(type);
+ bool bFallback(false);
+ if (bHasArt)
+ {
+ // Check if that type of art is actually a fallback, e.g. artist fanart
+ CGUIListItem::ArtMap::const_iterator i = primeArt.find(type);
+ bFallback = (i == primeArt.end());
+ }
+
+ // Build list of possible images of that art type
+ if (bHasArt)
+ {
+ // Add item for current artwork
+ // For album it could be a fallback from artist
+ CFileItemPtr item(new CFileItem("thumb://Current", false));
+ item->SetArt("thumb", m_item->GetArt(type));
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13512));
+ items.Add(item);
+ }
+
+ // Grab the thumbnails of this art type scraped from the web
+ std::vector<std::string> remotethumbs;
+ // Art type is encoded into the scraper XML as optional "aspect=" field
+ // Type "thumb" returns URLs for all types of art including those without aspect.
+ // Those URL without aspect are also returned for all other type values.
+ if (m_bArtistInfo)
+ m_artist.thumbURL.GetThumbUrls(remotethumbs, type);
+ else
+ m_album.thumbURL.GetThumbUrls(remotethumbs, type);
+
+ for (unsigned int i = 0; i < remotethumbs.size(); ++i)
+ {
+ std::string strItemPath;
+ strItemPath = StringUtils::Format("thumb://Remote{}", i);
+ CFileItemPtr item(new CFileItem(strItemPath, false));
+ item->SetArt("thumb", remotethumbs[i]);
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13513));
+
+ items.Add(item);
+ }
+
+ // Local art
+ std::string localArt;
+ std::vector<std::string> paths;
+ if (m_bArtistInfo)
+ {
+ // Individual artist subfolder within the Artist Information Folder
+ paths.emplace_back(m_artist.strPath);
+ // Fallback local to music files (when there is a unique folder)
+ paths.emplace_back(m_fallbackartpath);
+ }
+ else
+ // Album folder, when a unique one exists, no fallback
+ paths.emplace_back(m_album.strPath);
+ for (const auto& path : paths)
+ {
+ if (!localArt.empty() && CFileUtils::Exists(localArt))
+ break;
+ if (!path.empty())
+ {
+ CFileItem item(path, true);
+ if (type == "thumb")
+ // Local music thumbnail images named by <musicthumbs>
+ localArt = item.GetUserMusicThumb(true);
+ else
+ { // Check case and ext insenitively for local images with type as name
+ // e.g. <arttype>.jpg
+ CFileItemList items;
+ CDirectory::GetDirectory(path, items,
+ CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
+ DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
+
+ for (int j = 0; j < items.Size(); j++)
+ {
+ std::string strCandidate = URIUtils::GetFileName(items[j]->GetPath());
+ URIUtils::RemoveExtension(strCandidate);
+ if (StringUtils::EqualsNoCase(strCandidate, type))
+ {
+ localArt = items[j]->GetPath();
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (!localArt.empty() && CFileUtils::Exists(localArt))
+ {
+ CFileItemPtr item(new CFileItem("Local Art: " + localArt, false));
+ item->SetArt("thumb", localArt);
+ item->SetLabel(g_localizeStrings.Get(13514)); // "Local art"
+ items.Add(item);
+ }
+
+ // No art
+ if (bHasArt && !bFallback)
+ { // Actually has this type of art (not a fallback) so
+ // allow the user to delete it by selecting "no art".
+ CFileItemPtr item(new CFileItem("thumb://None", false));
+ if (m_bArtistInfo)
+ item->SetArt("icon", "DefaultArtist.png");
+ else
+ item->SetArt("icon", "DefaultAlbumCover.png");
+ item->SetLabel(g_localizeStrings.Get(13515));
+ items.Add(item);
+ }
+
+ //! @todo: Add support for extracting embedded art from song files to use for album
+
+ // Clear local images of this type from cache so user will see any recent
+ // local file changes immediately
+ for (auto& item : items)
+ {
+ // Skip images from remote sources, recache done by refresh (could be slow)
+ if (StringUtils::StartsWith(item->GetPath(), "thumb://Remote"))
+ continue;
+ std::string thumb(item->GetArt("thumb"));
+ if (thumb.empty())
+ continue;
+ CURL url(CTextureUtils::UnwrapImageURL(thumb));
+ // Skip images from remote sources (current thumb could be remote)
+ if (url.IsProtocol("http") || url.IsProtocol("https"))
+ continue;
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumb);
+ // Remove any thumbnail of local image too (created when browsing files)
+ std::string thumbthumb(CTextureUtils::GetWrappedThumbURL(thumb));
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumbthumb);
+ }
+
+ // Show list of possible art for user selection
+ // Note that during selection thumbs of *all* images shown are cached. When
+ // browsing folders there could be many irrelevant thumbs cached that are
+ // never used by Kodi again, but there is no obvious way to clear these
+ // thumbs from the cache automatically.
+ std::string result;
+ VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
+ CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(sources, *m_item);
+ CServiceBroker::GetMediaManager().GetLocalDrives(sources);
+ if (CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(13511), result) &&
+ result != "thumb://Current")
+ {
+ // User didn't choose the one they have.
+ // Overwrite with the new art or clear it
+ std::string newArt;
+ if (StringUtils::StartsWith(result, "thumb://Remote"))
+ {
+ int number = atoi(result.substr(14).c_str());
+ newArt = remotethumbs[number];
+ }
+ else if (result == "thumb://Thumb")
+ newArt = m_item->GetArt("thumb");
+ else if (StringUtils::StartsWith(result, "Local Art: "))
+ newArt = localArt;
+ else if (CFileUtils::Exists(result))
+ newArt = result;
+ else // none
+ newArt.clear();
+
+ // Asynchronously update that type of art in the database and then
+ // refresh artist, album and fallback art of currently playing song
+ MUSIC_UTILS::UpdateArtJob(m_item, type, newArt);
+
+ // Update local item and art list with current art
+ m_item->SetArt(type, newArt);
+ for (const auto& artitem : *m_artTypeList)
+ {
+ if (artitem->GetProperty("artType") == type)
+ {
+ artitem->SetArt("thumb", newArt);
+ break;
+ }
+ }
+
+ // Get new artwork to show in other places e.g. on music lib window, but this does
+ // not update artist, album or fallback art for the currently playing song as it
+ // is a different item with different ID and media type
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_item);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+
+ // Re-open the art type selection dialog as we come back from
+ // the image selection dialog
+ OnGetArt();
+}
+
+void CGUIDialogMusicInfo::OnSetUserrating() const
+{
+ int userrating = MUSIC_UTILS::ShowSelectRatingDialog(m_item->GetMusicInfoTag()->GetUserrating());
+ if (userrating < 0) // Nothing selected, so rating unchanged
+ return;
+
+ SetUserrating(userrating);
+}
+
+
+void CGUIDialogMusicInfo::ShowForAlbum(int idAlbum)
+{
+ std::string path = StringUtils::Format("musicdb://albums/{}", idAlbum);
+ CFileItem item(path, true); // An album, but IsAlbum() not set as didn't use SetAlbum()
+ ShowFor(&item);
+}
+
+void CGUIDialogMusicInfo::ShowForArtist(int idArtist)
+{
+ std::string path = StringUtils::Format("musicdb://artists/{}", idArtist);
+ CFileItem item(path, true);
+ ShowFor(&item);
+}
+
+void CGUIDialogMusicInfo::ShowFor(CFileItem* pItem)
+{
+ if (pItem->IsParentFolder() || URIUtils::IsSpecial(pItem->GetPath()) ||
+ StringUtils::StartsWithNoCase(pItem->GetPath(), "musicsearch://"))
+ return; // nothing to do
+
+ if (!pItem->m_bIsFolder)
+ { // Show Song information dialog
+ CGUIDialogSongInfo::ShowFor(pItem);
+ return;
+ }
+
+ CFileItem musicitem("musicdb://", true);
+
+ // We have a folder album/artist info dialog only shown for db items
+ // or for music video with artist/album in music library
+ if (pItem->IsMusicDb())
+ {
+ if (!pItem->HasMusicInfoTag() || pItem->GetMusicInfoTag()->GetDatabaseId() < 1)
+ {
+ // Maybe only path is set, then set MusicInfoTag
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(pItem->GetPath(), params);
+ if (params.GetArtistId() > 0)
+ pItem->GetMusicInfoTag()->SetDatabaseId(params.GetArtistId(), MediaTypeArtist);
+ else if (params.GetAlbumId() > 0)
+ pItem->GetMusicInfoTag()->SetDatabaseId(params.GetAlbumId(), MediaTypeAlbum);
+ else
+ return; // nothing to do
+ }
+ musicitem.SetFromMusicInfoTag(*pItem->GetMusicInfoTag());
+ }
+ else if (pItem->HasProperty("artist_musicid"))
+ {
+ musicitem.GetMusicInfoTag()->SetDatabaseId(pItem->GetProperty("artist_musicid").asInteger32(),
+ MediaTypeArtist);
+ }
+ else if (pItem->HasProperty("album_musicid"))
+ {
+ musicitem.GetMusicInfoTag()->SetDatabaseId(pItem->GetProperty("album_musicid").asInteger32(),
+ MediaTypeAlbum);
+ }
+ else
+ return; // nothing to do
+
+
+ CGUIDialogMusicInfo *pDlgMusicInfo = CServiceBroker::GetGUI()->GetWindowManager().
+ GetWindow<CGUIDialogMusicInfo>(WINDOW_DIALOG_MUSIC_INFO);
+ if (pDlgMusicInfo)
+ {
+ if (pDlgMusicInfo->SetItem(&musicitem))
+ {
+ pDlgMusicInfo->Open();
+ if (pItem->GetMusicInfoTag()->GetType() == MediaTypeAlbum &&
+ pDlgMusicInfo->HasUpdatedUserrating())
+ {
+ auto window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowMusicBase>(WINDOW_MUSIC_NAV);
+ if (window)
+ window->RefreshContent("albums");
+ }
+ }
+ }
+}
+
+void CGUIDialogMusicInfo::OnPlayItem(const std::shared_ptr<CFileItem>& item)
+{
+ Close(true);
+ MUSIC_UTILS::PlayItem(item);
+}
diff --git a/xbmc/music/dialogs/GUIDialogMusicInfo.h b/xbmc/music/dialogs/GUIDialogMusicInfo.h
new file mode 100644
index 0000000..781a7d1
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogMusicInfo.h
@@ -0,0 +1,80 @@
+/*
+ * 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 "MediaSource.h"
+#include "guilib/GUIDialog.h"
+#include "music/Album.h"
+#include "music/Artist.h"
+#include "music/Song.h"
+#include "threads/Event.h"
+
+#include <memory>
+
+class CFileItem;
+class CFileItemList;
+
+class CGUIDialogMusicInfo :
+ public CGUIDialog
+{
+public:
+ CGUIDialogMusicInfo(void);
+ ~CGUIDialogMusicInfo(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool SetItem(CFileItem* item);
+ void SetAlbum(const CAlbum& album, const std::string &path);
+ void SetArtist(const CArtist& artist, const std::string &path);
+ bool HasUpdatedUserrating() const { return m_hasUpdatedUserrating; }
+ bool HasRefreshed() const { return m_hasRefreshed; }
+
+ bool HasListItems() const override { return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+ std::string GetContent();
+ static void AddItemPathToFileBrowserSources(VECSOURCES &sources, const CFileItem &item);
+ void SetDiscography(CMusicDatabase& database) const;
+ void SetSongs(const VECSONGS &songs) const;
+ void SetArtTypeList(CFileItemList& artlist);
+ void SetScrapedInfo(bool bScraped) { m_scraperAddInfo = bScraped; }
+ CArtist& GetArtist() { return m_artist; }
+ CAlbum& GetAlbum() { return m_album; }
+ bool IsArtistInfo() const { return m_bArtistInfo; }
+ bool IsCancelled() const { return m_cancelled; }
+ bool HasScrapedInfo() const { return m_scraperAddInfo; }
+ void FetchComplete();
+ void RefreshInfo();
+
+ static void ShowForAlbum(int idAlbum);
+ static void ShowForArtist(int idArtist);
+ static void ShowFor(CFileItem* pItem);
+protected:
+ void OnInitWindow() override;
+ void Update();
+ void SetLabel(int iControl, const std::string& strLabel);
+ void OnGetArt();
+ void OnAlbumInfo(int id);
+ void OnArtistInfo(int id);
+ void OnSetUserrating() const;
+ void SetUserrating(int userrating) const;
+ void OnPlayItem(const std::shared_ptr<CFileItem>& item);
+
+ CAlbum m_album;
+ CArtist m_artist;
+ int m_startUserrating = -1;
+ bool m_hasUpdatedUserrating = false;
+ bool m_hasRefreshed = false;
+ bool m_bArtistInfo = false;
+ bool m_cancelled = false;
+ bool m_scraperAddInfo = false;
+ std::unique_ptr<CFileItemList> m_albumSongs;
+ std::shared_ptr<CFileItem> m_item;
+ std::unique_ptr<CFileItemList> m_artTypeList;
+ CEvent m_event;
+ std::string m_fallbackartpath;
+};
diff --git a/xbmc/music/dialogs/GUIDialogMusicOSD.cpp b/xbmc/music/dialogs/GUIDialogMusicOSD.cpp
new file mode 100644
index 0000000..6cd110b
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogMusicOSD.cpp
@@ -0,0 +1,91 @@
+/*
+ * 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 "GUIDialogMusicOSD.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/InputManager.h"
+#include "input/Key.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+#define CONTROL_VIS_BUTTON 500
+#define CONTROL_LOCK_BUTTON 501
+
+CGUIDialogMusicOSD::CGUIDialogMusicOSD(void)
+ : CGUIDialog(WINDOW_DIALOG_MUSIC_OSD, "MusicOSD.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogMusicOSD::~CGUIDialogMusicOSD(void) = default;
+
+bool CGUIDialogMusicOSD::OnMessage(CGUIMessage &message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ unsigned int iControl = message.GetSenderId();
+ if (iControl == CONTROL_VIS_BUTTON)
+ {
+ std::string addonID;
+ if (CGUIWindowAddonBrowser::SelectAddonID(ADDON::AddonType::VISUALIZATION, addonID, true) ==
+ 1)
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetString(CSettings::SETTING_MUSICPLAYER_VISUALISATION, addonID);
+ settings->Save();
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_VISUALISATION_RELOAD, 0, 0);
+ }
+ }
+ else if (iControl == CONTROL_LOCK_BUTTON)
+ {
+ CGUIMessage msg(GUI_MSG_VISUALISATION_ACTION, 0, 0, ACTION_VIS_PRESET_LOCK);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+ return true;
+ }
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogMusicOSD::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_SHOW_OSD:
+ Close();
+ return true;
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnAction(action);
+}
+
+void CGUIDialogMusicOSD::FrameMove()
+{
+ if (m_autoClosing)
+ {
+ // check for movement of mouse or a submenu open
+ if (CServiceBroker::GetInputManager().IsMouseActive() ||
+ CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_VIS_SETTINGS) ||
+ CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_VIS_PRESET_LIST) ||
+ CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_PVR_RADIO_RDS_INFO))
+ // extend show time by original value
+ SetAutoClose(m_showDuration);
+ }
+ CGUIDialog::FrameMove();
+}
diff --git a/xbmc/music/dialogs/GUIDialogMusicOSD.h b/xbmc/music/dialogs/GUIDialogMusicOSD.h
new file mode 100644
index 0000000..99a208c
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogMusicOSD.h
@@ -0,0 +1,22 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIDialogMusicOSD :
+ public CGUIDialog
+{
+public:
+ CGUIDialogMusicOSD(void);
+ ~CGUIDialogMusicOSD(void) override;
+ bool OnMessage(CGUIMessage &message) override;
+ bool OnAction(const CAction &action) override;
+ void FrameMove() override;
+};
diff --git a/xbmc/music/dialogs/GUIDialogSongInfo.cpp b/xbmc/music/dialogs/GUIDialogSongInfo.cpp
new file mode 100644
index 0000000..8efca94
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogSongInfo.cpp
@@ -0,0 +1,523 @@
+/*
+ * 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 "GUIDialogSongInfo.h"
+
+#include "GUIDialogMusicInfo.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "Util.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicUtils.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/windows/GUIWindowMusicBase.h"
+#include "profiles/ProfileManager.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+
+#define CONTROL_BTN_REFRESH 6
+#define CONTROL_USERRATING 7
+#define CONTROL_BTN_PLAY 8
+#define CONTROL_BTN_GET_THUMB 10
+#define CONTROL_ALBUMINFO 12
+
+#define CONTROL_LIST 50
+
+#define TIME_TO_BUSY_DIALOG 500
+
+
+
+class CGetSongInfoJob : public CJob
+{
+public:
+ ~CGetSongInfoJob(void) override = default;
+
+ // Fetch full song information including art types list
+ bool DoWork() override
+ {
+ CGUIDialogSongInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSongInfo>(WINDOW_DIALOG_SONG_INFO);
+ if (!dialog)
+ return false;
+ if (dialog->IsCancelled())
+ return false;
+ CFileItemPtr m_song = dialog->GetCurrentListItem();
+
+ // Fetch tag data from library using filename of item path, or scanning file
+ // (if item does not already have this loaded)
+ if (!m_song->LoadMusicTag())
+ {
+ // Stop SongInfoDialog waiting
+ dialog->FetchComplete();
+ return false;
+ }
+ if (dialog->IsCancelled())
+ return false;
+ // Fetch album and primary song artist data from library as properties
+ // and lyrics by scanning tags from file
+ MUSIC_INFO::CMusicInfoLoader::LoadAdditionalTagInfo(m_song.get());
+ if (dialog->IsCancelled())
+ return false;
+
+ // Get album path (for use in browsing art selection)
+ std::string albumpath;
+ CMusicDatabase db;
+ db.Open();
+ db.GetAlbumPath(m_song->GetMusicInfoTag()->GetAlbumId(), albumpath);
+ m_song->SetProperty("album_path", albumpath);
+ db.Close();
+ if (dialog->IsCancelled())
+ return false;
+
+ // Load song art.
+ // For songs in library this includes related album and artist(s) art.
+ // Also fetches artist art for non library songs when artist can be found
+ // uniquely by name, otherwise just embedded or cached thumb is fetched.
+ CMusicThumbLoader loader;
+ loader.LoadItem(m_song.get());
+ if (dialog->IsCancelled())
+ return false;
+
+ // For songs in library fill vector of possible art types, with current art when it exists
+ // for display on the art type selection dialog
+ CFileItemList artlist;
+ MUSIC_UTILS::FillArtTypesList(*m_song, artlist);
+ dialog->SetArtTypeList(artlist);
+ if (dialog->IsCancelled())
+ return false;
+
+ // Tell waiting SongInfoDialog that job is complete
+ dialog->FetchComplete();
+
+ return true;
+ }
+};
+
+CGUIDialogSongInfo::CGUIDialogSongInfo(void)
+ : CGUIDialog(WINDOW_DIALOG_SONG_INFO, "DialogMusicInfo.xml")
+ , m_song(new CFileItem)
+{
+ m_cancelled = false;
+ m_hasUpdatedUserrating = false;
+ m_startUserrating = -1;
+ m_artTypeList.Clear();
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogSongInfo::~CGUIDialogSongInfo(void) = default;
+
+bool CGUIDialogSongInfo::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ m_artTypeList.Clear();
+ if (m_startUserrating != m_song->GetMusicInfoTag()->GetUserrating())
+ {
+ m_hasUpdatedUserrating = true;
+
+ // Asynchronously update song userrating in library
+ MUSIC_UTILS::UpdateSongRatingJob(m_song, m_song->GetMusicInfoTag()->GetUserrating());
+
+ // Send a message to all windows to tell them to update the fileitem
+ // This communicates the rating change to the music lib window, current playlist and OSD.
+ // The music lib window item is updated to but changes to the rating when it is the sort
+ // do not show on screen until refresh() that fetches the list from scratch, sorts etc.
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_song);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_LIST);
+ OnMessage(msg);
+ break;
+ }
+ case GUI_MSG_WINDOW_INIT:
+ CGUIDialog::OnMessage(message);
+ Update();
+ m_cancelled = false;
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_USERRATING)
+ {
+ OnSetUserrating();
+ }
+ else if (iControl == CONTROL_ALBUMINFO)
+ {
+ CGUIDialogMusicInfo::ShowForAlbum(m_albumId);
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_GET_THUMB)
+ {
+ OnGetArt();
+ return true;
+ }
+ else if (iControl == CONTROL_LIST)
+ {
+ int iAction = message.GetParam1();
+ if ((ACTION_SELECT_ITEM == iAction || ACTION_MOUSE_LEFT_CLICK == iAction))
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ int iItem = msg.GetParam1();
+ if (iItem < 0 || iItem >= static_cast<int>(m_song->GetMusicInfoTag()->GetContributors().size()))
+ break;
+ int idArtist = m_song->GetMusicInfoTag()->GetContributors()[iItem].GetArtistId();
+ if (idArtist > 0)
+ CGUIDialogMusicInfo::ShowForArtist(idArtist);
+ return true;
+ }
+ }
+ else if (iControl == CONTROL_BTN_PLAY)
+ {
+ OnPlaySong(m_song);
+ return true;
+ }
+ return false;
+ }
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogSongInfo::OnAction(const CAction& action)
+{
+ int userrating = m_song->GetMusicInfoTag()->GetUserrating();
+ if (action.GetID() == ACTION_INCREASE_RATING)
+ {
+ SetUserrating(userrating + 1);
+ return true;
+ }
+ else if (action.GetID() == ACTION_DECREASE_RATING)
+ {
+ SetUserrating(userrating - 1);
+ return true;
+ }
+ else if (action.GetID() == ACTION_SHOW_INFO)
+ {
+ Close();
+ return true;
+ }
+ return CGUIDialog::OnAction(action);
+}
+
+bool CGUIDialogSongInfo::OnBack(int actionID)
+{
+ m_cancelled = true;
+ return CGUIDialog::OnBack(actionID);
+}
+
+void CGUIDialogSongInfo::FetchComplete()
+{
+ //Trigger the event to indicate data has been fetched
+ m_event.Set();
+}
+
+void CGUIDialogSongInfo::OnInitWindow()
+{
+ // Enable album info button when we know album
+ m_albumId = m_song->GetMusicInfoTag()->GetAlbumId();
+
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_ALBUMINFO, m_albumId > 0);
+
+ // Disable music user rating button for plugins as they don't have tables to save this
+ if (m_song->IsPlugin())
+ CONTROL_DISABLE(CONTROL_USERRATING);
+ else
+ CONTROL_ENABLE(CONTROL_USERRATING);
+
+ // Disable the Choose Art button if the user isn't allowed it
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_GET_THUMB,
+ profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser);
+
+ SET_CONTROL_HIDDEN(CONTROL_BTN_REFRESH);
+ SET_CONTROL_LABEL(CONTROL_USERRATING, 38023);
+ SET_CONTROL_LABEL(CONTROL_BTN_GET_THUMB, 13511);
+ SET_CONTROL_LABEL(CONTROL_ALBUMINFO, 10523);
+ SET_CONTROL_LABEL(CONTROL_BTN_PLAY, 208);
+
+ CGUIDialog::OnInitWindow();
+}
+
+void CGUIDialogSongInfo::Update()
+{
+ CFileItemList items;
+ for (const auto& contributor : m_song->GetMusicInfoTag()->GetContributors())
+ {
+ auto item = std::make_shared<CFileItem>(contributor.GetRoleDesc());
+ item->SetLabel2(contributor.GetArtist());
+ item->GetMusicInfoTag()->SetDatabaseId(contributor.GetArtistId(), MediaTypeArtist);
+ items.Add(std::move(item));
+ }
+ CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST, 0, 0, &items);
+ OnMessage(message);
+}
+
+void CGUIDialogSongInfo::SetUserrating(int userrating)
+{
+ userrating = std::max(userrating, 0);
+ userrating = std::min(userrating, 10);
+ if (userrating != m_song->GetMusicInfoTag()->GetUserrating())
+ {
+ m_song->GetMusicInfoTag()->SetUserrating(userrating);
+ }
+}
+
+bool CGUIDialogSongInfo::SetSong(CFileItem* item)
+{
+ *m_song = *item;
+ m_event.Reset();
+ m_cancelled = false; // SetSong happens before win_init
+ // In a separate job fetch song info and fill list of art types.
+ int jobid =
+ CServiceBroker::GetJobManager()->AddJob(new CGetSongInfoJob(), nullptr, CJob::PRIORITY_LOW);
+
+ // Wait to get all data before show, allowing user to cancel if fetch is slow
+ if (!CGUIDialogBusy::WaitOnEvent(m_event, TIME_TO_BUSY_DIALOG))
+ {
+ // Cancel job still waiting in queue (unlikely)
+ CServiceBroker::GetJobManager()->CancelJob(jobid);
+ // Flag to stop job already in progress
+ m_cancelled = true;
+ return false;
+ }
+
+ // Store initial userrating
+ m_startUserrating = m_song->GetMusicInfoTag()->GetUserrating();
+ m_hasUpdatedUserrating = false;
+ return true;
+}
+
+void CGUIDialogSongInfo::SetArtTypeList(CFileItemList& artlist)
+{
+ m_artTypeList.Copy(artlist);
+}
+
+CFileItemPtr CGUIDialogSongInfo::GetCurrentListItem(int offset)
+{
+ return m_song;
+}
+
+std::string CGUIDialogSongInfo::GetContent()
+{
+ return "songs";
+}
+
+/*
+ Allow user to choose artwork for the song
+ For each type of art the options are:
+ 1. Current art
+ 2. Local art (thumb found by filename)
+ 3. Embedded art (@todo)
+ 4. None
+ Note that songs are not scraped, hence there is no list of urls for possible remote art
+*/
+void CGUIDialogSongInfo::OnGetArt()
+{
+ std::string type = MUSIC_UTILS::ShowSelectArtTypeDialog(m_artTypeList);
+ if (type.empty())
+ return; // Cancelled
+
+ CFileItemList items;
+ CGUIListItem::ArtMap primeArt = m_song->GetArt(); // Song art without fallbacks
+ bool bHasArt = m_song->HasArt(type);
+ bool bFallback(false);
+ if (bHasArt)
+ {
+ // Check if that type of art is actually a fallback, e.g. album thumb or artist fanart
+ CGUIListItem::ArtMap::const_iterator i = primeArt.find(type);
+ bFallback = (i == primeArt.end());
+ }
+
+ // Build list of possible images of that art type
+ if (bHasArt)
+ {
+ // Add item for current artwork, could a fallback from album/artist
+ CFileItemPtr item(new CFileItem("thumb://Current", false));
+ item->SetArt("thumb", m_song->GetArt(type));
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13512)); //! @todo: label fallback art so user knows?
+ items.Add(item);
+ }
+ else if (m_song->HasArt("thumb"))
+ { // For missing art of that type add the thumb (when it exists and not a fallback)
+ CGUIListItem::ArtMap::const_iterator i = primeArt.find("thumb");
+ if (i != primeArt.end())
+ {
+ CFileItemPtr item(new CFileItem("thumb://Thumb", false));
+ item->SetArt("thumb", m_song->GetArt("thumb"));
+ item->SetArt("icon", "DefaultAlbumCover.png");
+ item->SetLabel(g_localizeStrings.Get(21371));
+ items.Add(item);
+ }
+ }
+
+ std::string localThumb;
+ if (type == "thumb")
+ { // Local thumb type art held in <filename>.tbn (for non-library items)
+ localThumb = m_song->GetUserMusicThumb(true);
+ if (m_song->IsMusicDb())
+ {
+ CFileItem item(m_song->GetMusicInfoTag()->GetURL(), false);
+ localThumb = item.GetUserMusicThumb(true);
+ }
+ if (CFileUtils::Exists(localThumb))
+ {
+ CFileItemPtr item(new CFileItem("thumb://Local", false));
+ item->SetArt("thumb", localThumb);
+ item->SetLabel(g_localizeStrings.Get(20017));
+ items.Add(item);
+ }
+ }
+
+ // Clear these local images from cache so user will see any recent
+ // local file changes immediately
+ for (auto& item : items)
+ {
+ std::string thumb(item->GetArt("thumb"));
+ if (thumb.empty())
+ continue;
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumb);
+ // Remove any thumbnail of local image too (created when browsing files)
+ std::string thumbthumb(CTextureUtils::GetWrappedThumbURL(thumb));
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumbthumb);
+ }
+
+ if (bHasArt && !bFallback)
+ { // Actually has this type of art (not a fallback) so
+ // allow the user to delete it by selecting "no art".
+ CFileItemPtr item(new CFileItem("thumb://None", false));
+ item->SetArt("thumb", "DefaultAlbumCover.png");
+ item->SetLabel(g_localizeStrings.Get(13515));
+ items.Add(item);
+ }
+
+ //! @todo: Add support for extracting embedded art
+
+ // Show list of possible art for user selection
+ std::string result;
+ VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("music"));
+ // Add album folder as source (could be disc set)
+ std::string albumpath = m_song->GetProperty("album_path").asString();
+ if (!albumpath.empty())
+ {
+ CFileItem pathItem(albumpath, true);
+ CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(sources, pathItem);
+ }
+ else // Add parent folder of song
+ CGUIDialogMusicInfo::AddItemPathToFileBrowserSources(sources, *m_song);
+ CServiceBroker::GetMediaManager().GetLocalDrives(sources);
+ if (CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(13511), result) &&
+ result != "thumb://Current")
+ {
+ // User didn't choose the one they have, or the fallback image.
+ // Overwrite with the new art or clear it
+ std::string newArt;
+ if (result == "thumb://Thumb")
+ newArt = m_song->GetArt("thumb");
+ else if (result == "thumb://Local")
+ newArt = localThumb;
+// else if (result == "thumb://Embedded")
+// newArt = embeddedArt;
+ else if (CFileUtils::Exists(result))
+ newArt = result;
+ else // none
+ newArt.clear();
+
+ // Asynchronously update that type of art in the database
+ MUSIC_UTILS::UpdateArtJob(m_song, type, newArt);
+
+ // Update local song with current art
+ if (newArt.empty())
+ {
+ // Remove that type of art from the song
+ primeArt.erase(type);
+ m_song->SetArt(primeArt);
+ }
+ else
+ // Add or modify the type of art
+ m_song->SetArt(type, newArt);
+
+ // Update local art list with current art
+ // Show any fallback art when song art removed
+ if (newArt.empty() && m_song->HasArt(type))
+ newArt = m_song->GetArt(type);
+ for (const auto& artitem : m_artTypeList)
+ {
+ if (artitem->GetProperty("artType") == type)
+ {
+ artitem->SetArt("thumb", newArt);
+ break;
+ }
+ }
+
+ // Get new artwork to show in other places e.g. on music lib window,
+ // current playlist and player OSD.
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_song);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ }
+
+ // Re-open the art type selection dialog as we come back from
+ // the image selection dialog
+ OnGetArt();
+}
+
+void CGUIDialogSongInfo::OnSetUserrating()
+{
+ int userrating = MUSIC_UTILS::ShowSelectRatingDialog(m_song->GetMusicInfoTag()->GetUserrating());
+ if (userrating < 0) // Nothing selected, so rating unchanged
+ return;
+
+ SetUserrating(userrating);
+}
+
+void CGUIDialogSongInfo::ShowFor(CFileItem* pItem)
+{
+ if (pItem->m_bIsFolder)
+ return;
+ if (!pItem->IsMusicDb())
+ pItem->LoadMusicTag();
+ if (!pItem->HasMusicInfoTag())
+ return;
+
+ CGUIDialogSongInfo *dialog = CServiceBroker::GetGUI()->GetWindowManager().
+ GetWindow<CGUIDialogSongInfo>(WINDOW_DIALOG_SONG_INFO);
+ if (dialog)
+ {
+ if (dialog->SetSong(pItem)) // Fetch full song info asynchronously
+ {
+ dialog->Open();
+ if (dialog->HasUpdatedUserrating())
+ {
+ auto window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowMusicBase>(WINDOW_MUSIC_NAV);
+ if (window)
+ window->RefreshContent("songs");
+ }
+ }
+ }
+}
+
+void CGUIDialogSongInfo::OnPlaySong(const std::shared_ptr<CFileItem>& item)
+{
+ Close(true);
+ MUSIC_UTILS::PlayItem(item);
+}
diff --git a/xbmc/music/dialogs/GUIDialogSongInfo.h b/xbmc/music/dialogs/GUIDialogSongInfo.h
new file mode 100644
index 0000000..274f1ac
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogSongInfo.h
@@ -0,0 +1,54 @@
+/*
+ * 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 "FileItem.h"
+#include "guilib/GUIDialog.h"
+#include "threads/Event.h"
+
+#include <memory>
+
+class CGUIDialogSongInfo :
+ public CGUIDialog
+{
+public:
+ CGUIDialogSongInfo(void);
+ ~CGUIDialogSongInfo(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool SetSong(CFileItem* item);
+ void SetArtTypeList(CFileItemList& artlist);
+ bool OnAction(const CAction& action) override;
+ bool OnBack(int actionID) override;
+ bool HasUpdatedUserrating() const { return m_hasUpdatedUserrating; }
+
+ bool HasListItems() const override { return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+ std::string GetContent();
+ //const CFileItemList& CurrentDirectory() const { return m_artTypeList; }
+ bool IsCancelled() const { return m_cancelled; }
+ void FetchComplete();
+
+ static void ShowFor(CFileItem* pItem);
+protected:
+ void OnInitWindow() override;
+ void Update();
+ void OnGetArt();
+ void SetUserrating(int userrating);
+ void OnSetUserrating();
+ void OnPlaySong(const std::shared_ptr<CFileItem>& item);
+
+ CFileItemPtr m_song;
+ CFileItemList m_artTypeList;
+ CEvent m_event;
+ int m_startUserrating;
+ bool m_cancelled;
+ bool m_hasUpdatedUserrating;
+ long m_albumId = -1;
+
+};
diff --git a/xbmc/music/dialogs/GUIDialogVisualisationPresetList.cpp b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.cpp
new file mode 100644
index 0000000..b5f2607
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.cpp
@@ -0,0 +1,98 @@
+/*
+ * 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 "GUIDialogVisualisationPresetList.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIVisualisationControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+CGUIDialogVisualisationPresetList::CGUIDialogVisualisationPresetList()
+ : CGUIDialogSelect(WINDOW_DIALOG_VIS_PRESET_LIST)
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+bool CGUIDialogVisualisationPresetList::OnMessage(CGUIMessage &message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_VISUALISATION_UNLOADING:
+ ClearVisualisation();
+ break;
+ }
+ return CGUIDialogSelect::OnMessage(message);
+}
+
+void CGUIDialogVisualisationPresetList::OnSelect(int idx)
+{
+ if (m_viz)
+ m_viz->SetPreset(idx);
+}
+
+void CGUIDialogVisualisationPresetList::ClearVisualisation()
+{
+ m_viz = nullptr;
+ Reset();
+}
+
+void CGUIDialogVisualisationPresetList::SetVisualisation(CGUIVisualisationControl* vis)
+{
+ m_viz = vis;
+ Reset();
+ if (!m_viz)
+ { // No viz, but show something if this dialog activated
+ SetHeading(CVariant{ 10122 });
+ CFileItem item(g_localizeStrings.Get(13389));
+ Add(item);
+ }
+ else
+ {
+ SetUseDetails(false);
+ SetMultiSelection(false);
+ SetHeading(CVariant{StringUtils::Format(g_localizeStrings.Get(13407), m_viz->Name())});
+ std::vector<std::string> presets;
+ if (m_viz->GetPresetList(presets))
+ {
+ for (const auto& preset : presets)
+ {
+ CFileItem item(preset);
+ item.RemoveExtension();
+ Add(item);
+ }
+ SetSelected(m_viz->GetActivePreset());
+ }
+ else
+ { // Viz does not have any presets
+ // "There are no presets available for this visualisation"
+ CFileItem item(g_localizeStrings.Get(13389));
+ Add(item);
+ }
+ }
+}
+
+void CGUIDialogVisualisationPresetList::OnInitWindow()
+{
+ CGUIMessage msg(GUI_MSG_GET_VISUALISATION, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ SetVisualisation(static_cast<CGUIVisualisationControl*>(msg.GetPointer()));
+ CGUIDialogSelect::OnInitWindow();
+}
+
+void CGUIDialogVisualisationPresetList::OnDeinitWindow(int nextWindowID)
+{
+ ClearVisualisation();
+ CGUIDialogSelect::OnDeinitWindow(nextWindowID);
+}
diff --git a/xbmc/music/dialogs/GUIDialogVisualisationPresetList.h b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.h
new file mode 100644
index 0000000..c832163
--- /dev/null
+++ b/xbmc/music/dialogs/GUIDialogVisualisationPresetList.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIDialog.h"
+
+class CGUIVisualisationControl;
+class CFileItemList;
+
+class CGUIDialogVisualisationPresetList : public CGUIDialogSelect
+{
+public:
+ CGUIDialogVisualisationPresetList();
+ bool OnMessage(CGUIMessage &message) override;
+
+protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ void OnSelect(int idx) override;
+
+private:
+ void ClearVisualisation();
+ void SetVisualisation(CGUIVisualisationControl *addon);
+ CGUIVisualisationControl* m_viz = nullptr;
+};
diff --git a/xbmc/music/infoscanner/CMakeLists.txt b/xbmc/music/infoscanner/CMakeLists.txt
new file mode 100644
index 0000000..b8c24a0
--- /dev/null
+++ b/xbmc/music/infoscanner/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES MusicAlbumInfo.cpp
+ MusicArtistInfo.cpp
+ MusicInfoScanner.cpp
+ MusicInfoScraper.cpp)
+
+set(HEADERS MusicAlbumInfo.h
+ MusicArtistInfo.h
+ MusicInfoScanner.h
+ MusicInfoScraper.h)
+
+core_add_library(music_infoscanner)
diff --git a/xbmc/music/infoscanner/MusicAlbumInfo.cpp b/xbmc/music/infoscanner/MusicAlbumInfo.cpp
new file mode 100644
index 0000000..90aedd3
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicAlbumInfo.cpp
@@ -0,0 +1,52 @@
+/*
+ * 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 "MusicAlbumInfo.h"
+
+#include "addons/Scraper.h"
+#include "settings/AdvancedSettings.h"
+#include "utils/StringUtils.h"
+
+using namespace MUSIC_GRABBER;
+
+CMusicAlbumInfo::CMusicAlbumInfo(const std::string& strAlbumInfo, const CScraperUrl& strAlbumURL)
+ : m_strTitle2(strAlbumInfo), m_albumURL(strAlbumURL)
+{
+ m_relevance = -1;
+ m_bLoaded = false;
+}
+
+CMusicAlbumInfo::CMusicAlbumInfo(const std::string& strAlbum,
+ const std::string& strArtist,
+ const std::string& strAlbumInfo,
+ const CScraperUrl& strAlbumURL)
+ : m_strTitle2(strAlbumInfo), m_albumURL(strAlbumURL)
+{
+ m_album.strAlbum = strAlbum;
+ //Just setting artist desc, not populating album artist credits.
+ m_album.strArtistDesc = strArtist;
+ m_relevance = -1;
+ m_bLoaded = false;
+}
+
+void CMusicAlbumInfo::SetAlbum(CAlbum& album)
+{
+ m_album = album;
+ m_strTitle2 = "";
+ m_bLoaded = true;
+}
+
+bool CMusicAlbumInfo::Load(XFILE::CCurlFile& http, const ADDON::ScraperPtr& scraper)
+{
+ bool fSuccess = scraper->GetAlbumDetails(http, m_albumURL, m_album);
+ if (fSuccess && m_strTitle2.empty())
+ m_strTitle2 = m_album.strAlbum;
+ SetLoaded(fSuccess);
+ return fSuccess;
+}
+
diff --git a/xbmc/music/infoscanner/MusicAlbumInfo.h b/xbmc/music/infoscanner/MusicAlbumInfo.h
new file mode 100644
index 0000000..91b8d5d
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicAlbumInfo.h
@@ -0,0 +1,50 @@
+/*
+ * 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 "addons/Scraper.h"
+#include "music/Album.h"
+#include "utils/ScraperUrl.h"
+
+class CXBMCTinyXML;
+
+namespace XFILE { class CCurlFile; }
+
+namespace MUSIC_GRABBER
+{
+class CMusicAlbumInfo
+{
+public:
+ CMusicAlbumInfo() = default;
+ CMusicAlbumInfo(const std::string& strAlbumInfo, const CScraperUrl& strAlbumURL);
+ CMusicAlbumInfo(const std::string& strAlbum, const std::string& strArtist, const std::string& strAlbumInfo, const CScraperUrl& strAlbumURL);
+ virtual ~CMusicAlbumInfo() = default;
+
+ bool Loaded() const { return m_bLoaded; }
+ void SetLoaded(bool bLoaded) { m_bLoaded = bLoaded; }
+ const CAlbum &GetAlbum() const { return m_album; }
+ CAlbum& GetAlbum() { return m_album; }
+ void SetAlbum(CAlbum& album);
+ const std::string& GetTitle2() const { return m_strTitle2; }
+ void SetTitle(const std::string& strTitle) { m_album.strAlbum = strTitle; }
+ const CScraperUrl& GetAlbumURL() const { return m_albumURL; }
+ float GetRelevance() const { return m_relevance; }
+ void SetRelevance(float relevance) { m_relevance = relevance; }
+
+ bool Load(XFILE::CCurlFile& http, const ADDON::ScraperPtr& scraper);
+
+protected:
+ bool m_bLoaded = false;
+ CAlbum m_album;
+ float m_relevance = -1;
+ std::string m_strTitle2;
+ CScraperUrl m_albumURL;
+};
+
+}
diff --git a/xbmc/music/infoscanner/MusicArtistInfo.cpp b/xbmc/music/infoscanner/MusicArtistInfo.cpp
new file mode 100644
index 0000000..3a3f64a
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicArtistInfo.cpp
@@ -0,0 +1,34 @@
+/*
+ * 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 "MusicArtistInfo.h"
+
+#include "addons/Scraper.h"
+
+using namespace XFILE;
+using namespace MUSIC_GRABBER;
+
+CMusicArtistInfo::CMusicArtistInfo(const std::string& strArtist, const CScraperUrl& strArtistURL):
+ m_artistURL(strArtistURL)
+{
+ m_artist.strArtist = strArtist;
+ m_bLoaded = false;
+}
+
+void CMusicArtistInfo::SetArtist(const CArtist& artist)
+{
+ m_artist = artist;
+ m_bLoaded = true;
+}
+
+bool CMusicArtistInfo::Load(CCurlFile& http, const ADDON::ScraperPtr& scraper,
+ const std::string &strSearch)
+{
+ return m_bLoaded = scraper->GetArtistDetails(http, m_artistURL, strSearch, m_artist);
+}
+
diff --git a/xbmc/music/infoscanner/MusicArtistInfo.h b/xbmc/music/infoscanner/MusicArtistInfo.h
new file mode 100644
index 0000000..1817589
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicArtistInfo.h
@@ -0,0 +1,39 @@
+/*
+ * 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 "addons/Scraper.h"
+#include "music/Artist.h"
+
+class CXBMCTinyXML;
+class CScraperUrl;
+
+namespace MUSIC_GRABBER
+{
+class CMusicArtistInfo
+{
+public:
+ CMusicArtistInfo() = default;
+ CMusicArtistInfo(const std::string& strArtist, const CScraperUrl& strArtistURL);
+ virtual ~CMusicArtistInfo() = default;
+ bool Loaded() const { return m_bLoaded; }
+ void SetLoaded() { m_bLoaded = true; }
+ void SetArtist(const CArtist& artist);
+ const CArtist& GetArtist() const { return m_artist; }
+ CArtist& GetArtist() { return m_artist; }
+ const CScraperUrl& GetArtistURL() const { return m_artistURL; }
+ bool Load(XFILE::CCurlFile& http, const ADDON::ScraperPtr& scraper,
+ const std::string &strSearch);
+
+protected:
+ CArtist m_artist;
+ CScraperUrl m_artistURL;
+ bool m_bLoaded = false;
+};
+}
diff --git a/xbmc/music/infoscanner/MusicInfoScanner.cpp b/xbmc/music/infoscanner/MusicInfoScanner.cpp
new file mode 100644
index 0000000..f828b44
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicInfoScanner.cpp
@@ -0,0 +1,2338 @@
+/*
+ * 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 "MusicInfoScanner.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "MusicAlbumInfo.h"
+#include "MusicInfoScraper.h"
+#include "NfoFile.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/Scraper.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "events/EventLog.h"
+#include "events/MediaLibraryEvent.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
+#include "filesystem/SmartPlaylistDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/AnnouncementManager.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/MusicThumbLoader.h"
+#include "music/MusicUtils.h"
+#include "music/tags/MusicInfoTag.h"
+#include "music/tags/MusicInfoTagLoaderFactory.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+using namespace MUSICDATABASEDIRECTORY;
+using namespace MUSIC_GRABBER;
+using namespace ADDON;
+using KODI::UTILITY::CDigest;
+
+CMusicInfoScanner::CMusicInfoScanner()
+: m_fileCountReader(this, "MusicFileCounter")
+{
+ m_bStop = false;
+ m_currentItem=0;
+ m_itemCount=0;
+ m_flags = 0;
+}
+
+CMusicInfoScanner::~CMusicInfoScanner() = default;
+
+void CMusicInfoScanner::Process()
+{
+ m_bStop = false;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnScanStarted");
+ try
+ {
+ if (m_showDialog && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_BACKGROUNDUPDATE))
+ {
+ CGUIDialogExtendedProgressBar* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
+ if (dialog)
+ m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
+ }
+
+ // check if we only need to perform a cleaning
+ if (m_bClean && m_pathsToScan.empty())
+ {
+ CMusicLibraryQueue::GetInstance().CleanLibrary(false);
+ m_handle = NULL;
+ m_bRunning = false;
+
+ return;
+ }
+
+ auto tick = std::chrono::steady_clock::now();
+ m_musicDatabase.Open();
+ m_bCanInterrupt = true;
+
+ if (m_scanType == 0) // load info from files
+ {
+ CLog::Log(LOGDEBUG, "{} - Starting scan", __FUNCTION__);
+
+ if (m_handle)
+ m_handle->SetTitle(g_localizeStrings.Get(505));
+
+ // Reset progress vars
+ m_currentItem=0;
+ m_itemCount=-1;
+
+ // Create the thread to count all files to be scanned
+ if (m_handle)
+ m_fileCountReader.Create();
+
+ // Database operations should not be canceled
+ // using Interrupt() while scanning as it could
+ // result in unexpected behaviour.
+ m_bCanInterrupt = false;
+ m_needsCleanup = false;
+
+ bool commit = true;
+ for (const auto& it : m_pathsToScan)
+ {
+ if (!CDirectory::Exists(it) && !m_bClean)
+ {
+ /*
+ * Note that this will skip scanning (if m_bClean is disabled) if the directory really
+ * doesn't exist. Since the music scanner is fed with a list of existing paths from the DB
+ * and cleans out all songs under that path as its first step before re-adding files, if
+ * the entire source is offline we totally empty the music database in one go.
+ */
+ CLog::Log(LOGWARNING, "{} directory '{}' does not exist - skipping scan.", __FUNCTION__,
+ it);
+ m_seenPaths.insert(it);
+ continue;
+ }
+
+ // Clear list of albums added by this scan
+ m_albumsAdded.clear();
+ bool scancomplete = DoScan(it);
+ if (scancomplete)
+ {
+ if (m_albumsAdded.size() > 0)
+ {
+ // Set local art for added album disc sets and primary album artists
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) !=
+ CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE)
+ RetrieveLocalArt();
+
+ if (m_flags & SCAN_ONLINE)
+ // Download additional album and artist information for the recently added albums.
+ // This also identifies any local artist art if it exists, and gives it priority,
+ // otherwise it is set to the first available from the remote art that was scraped.
+ ScrapeInfoAddedAlbums();
+ }
+ }
+ else
+ {
+ commit = false;
+ break;
+ }
+ }
+
+ if (commit)
+ {
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
+
+ if (m_needsCleanup)
+ {
+ if (m_handle)
+ {
+ m_handle->SetTitle(g_localizeStrings.Get(700));
+ m_handle->SetText("");
+ }
+
+ m_musicDatabase.CleanupOrphanedItems();
+ m_musicDatabase.CheckArtistLinksChanged();
+
+ if (m_handle)
+ m_handle->SetTitle(g_localizeStrings.Get(331));
+
+ m_musicDatabase.Compress(false);
+ }
+ }
+
+ m_fileCountReader.StopThread();
+
+ m_musicDatabase.EmptyCache();
+
+ auto elapsed =
+ std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - tick);
+ CLog::Log(LOGINFO,
+ "My Music: Scanning for music info using worker thread, operation took {}s",
+ elapsed.count());
+ }
+ if (m_scanType == 1) // load album info
+ {
+ for (std::set<std::string>::const_iterator it = m_pathsToScan.begin(); it != m_pathsToScan.end(); ++it)
+ {
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(*it, params);
+ // Only scrape information for albums that have not been scraped before
+ // For refresh of information the lastscraped date is optionally clearered elsewhere
+ if (m_musicDatabase.HasAlbumBeenScraped(params.GetAlbumId()))
+ continue;
+
+ CAlbum album;
+ m_musicDatabase.GetAlbum(params.GetAlbumId(), album);
+ if (m_handle)
+ {
+ float percentage = static_cast<float>(std::distance(m_pathsToScan.begin(), it) * 100) / static_cast<float>(m_pathsToScan.size());
+ m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
+ m_handle->SetPercentage(percentage);
+ }
+
+ // find album info
+ ADDON::ScraperPtr scraper;
+ if (!m_musicDatabase.GetScraper(album.idAlbum, CONTENT_ALBUMS, scraper))
+ continue;
+
+ UpdateDatabaseAlbumInfo(album, scraper, false);
+
+ if (m_bStop)
+ break;
+ }
+ }
+ if (m_scanType == 2) // load artist info
+ {
+ for (std::set<std::string>::const_iterator it = m_pathsToScan.begin(); it != m_pathsToScan.end(); ++it)
+ {
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(*it, params);
+ // Only scrape information for artists that have not been scraped before
+ // For refresh of information the lastscraped date is optionally clearered elsewhere
+ if (m_musicDatabase.HasArtistBeenScraped(params.GetArtistId()))
+ continue;
+
+ CArtist artist;
+ m_musicDatabase.GetArtist(params.GetArtistId(), artist);
+ m_musicDatabase.GetArtistPath(artist, artist.strPath);
+
+ if (m_handle)
+ {
+ float percentage = static_cast<float>(std::distance(m_pathsToScan.begin(), it) * 100) / static_cast<float>(m_pathsToScan.size());
+ m_handle->SetText(artist.strArtist);
+ m_handle->SetPercentage(percentage);
+ }
+
+ // find album info
+ ADDON::ScraperPtr scraper;
+ if (!m_musicDatabase.GetScraper(artist.idArtist, CONTENT_ARTISTS, scraper) || !scraper)
+ continue;
+
+ UpdateDatabaseArtistInfo(artist, scraper, false);
+
+ if (m_bStop)
+ break;
+ }
+ }
+ //propagate artist sort names to albums and songs
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryArtistSortOnUpdate)
+ m_musicDatabase.UpdateArtistSortNames();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "MusicInfoScanner: Exception while scanning.");
+ }
+ m_musicDatabase.Close();
+ CLog::Log(LOGDEBUG, "{} - Finished scan", __FUNCTION__);
+
+ m_bRunning = false;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary, "OnScanFinished");
+
+ // we need to clear the musicdb cache and update any active lists
+ CUtil::DeleteMusicDatabaseDirectoryCache();
+ CGUIMessage msg(GUI_MSG_SCAN_FINISHED, 0, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+
+ if (m_handle)
+ m_handle->MarkFinished();
+ m_handle = NULL;
+}
+
+void CMusicInfoScanner::Start(const std::string& strDirectory, int flags)
+{
+ m_fileCountReader.StopThread();
+
+ m_pathsToScan.clear();
+ m_seenPaths.clear();
+ m_albumsAdded.clear();
+ m_flags = flags;
+
+ m_musicDatabase.Open();
+ // Check db sources match xml file and update if they don't
+ m_musicDatabase.UpdateSources();
+
+ if (strDirectory.empty())
+ { // Scan all paths in the database. We do this by scanning all paths in the
+ // db, and crossing them off the list as we go.
+ m_musicDatabase.GetPaths(m_pathsToScan);
+ m_idSourcePath = -1;
+ }
+ else
+ {
+ m_pathsToScan.insert(strDirectory);
+ m_idSourcePath = m_musicDatabase.GetSourceFromPath(strDirectory);
+ }
+ m_musicDatabase.Close();
+
+ m_bClean = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryCleanOnUpdate;
+
+ m_scanType = 0;
+ m_bRunning = true;
+ Process();
+}
+
+void CMusicInfoScanner::FetchAlbumInfo(const std::string& strDirectory,
+ bool refresh)
+{
+ m_fileCountReader.StopThread();
+ m_pathsToScan.clear();
+
+ CFileItemList items;
+ if (strDirectory.empty())
+ {
+ m_musicDatabase.Open();
+ m_musicDatabase.GetAlbumsNav("musicdb://albums/", items);
+ m_musicDatabase.Close();
+ }
+ else
+ {
+ CURL pathToUrl(strDirectory);
+
+ if (pathToUrl.IsProtocol("musicdb"))
+ {
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(strDirectory, params);
+ if (params.GetAlbumId() != -1)
+ {
+ //Add single album (id and path) as item to scan
+ CFileItemPtr item(new CFileItem(strDirectory, false));
+ item->GetMusicInfoTag()->SetDatabaseId(params.GetAlbumId(), MediaTypeAlbum);
+ items.Add(item);
+ }
+ else
+ {
+ CMusicDatabaseDirectory dir;
+ NODE_TYPE childtype = dir.GetDirectoryChildType(strDirectory);
+ if (childtype == NODE_TYPE_ALBUM)
+ dir.GetDirectory(pathToUrl, items);
+ }
+ }
+ else if (StringUtils::EndsWith(strDirectory, ".xsp"))
+ {
+ CSmartPlaylistDirectory dir;
+ dir.GetDirectory(pathToUrl, items);
+ }
+ }
+
+ m_musicDatabase.Open();
+ for (int i=0;i<items.Size();++i)
+ {
+ if (CMusicDatabaseDirectory::IsAllItem(items[i]->GetPath()) || items[i]->IsParentFolder())
+ continue;
+
+ m_pathsToScan.insert(items[i]->GetPath());
+ if (refresh)
+ {
+ m_musicDatabase.ClearAlbumLastScrapedTime(items[i]->GetMusicInfoTag()->GetDatabaseId());
+ }
+ }
+ m_musicDatabase.Close();
+
+ m_scanType = 1;
+ m_bRunning = true;
+ Process();
+}
+
+void CMusicInfoScanner::FetchArtistInfo(const std::string& strDirectory,
+ bool refresh)
+{
+ m_fileCountReader.StopThread();
+ m_pathsToScan.clear();
+ CFileItemList items;
+
+ if (strDirectory.empty())
+ {
+ m_musicDatabase.Open();
+ m_musicDatabase.GetArtistsNav("musicdb://artists/", items, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS), -1);
+ m_musicDatabase.Close();
+ }
+ else
+ {
+ CURL pathToUrl(strDirectory);
+
+ if (pathToUrl.IsProtocol("musicdb"))
+ {
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(strDirectory, params);
+ if (params.GetArtistId() != -1)
+ {
+ //Add single artist (id and path) as item to scan
+ CFileItemPtr item(new CFileItem(strDirectory, false));
+ item->GetMusicInfoTag()->SetDatabaseId(params.GetAlbumId(), MediaTypeArtist);
+ items.Add(item);
+ }
+ else
+ {
+ CMusicDatabaseDirectory dir;
+ NODE_TYPE childtype = dir.GetDirectoryChildType(strDirectory);
+ if (childtype == NODE_TYPE_ARTIST)
+ dir.GetDirectory(pathToUrl, items);
+ }
+ }
+ else if (StringUtils::EndsWith(strDirectory, ".xsp"))
+ {
+ CSmartPlaylistDirectory dir;
+ dir.GetDirectory(pathToUrl, items);
+ }
+ }
+
+ m_musicDatabase.Open();
+ for (int i=0;i<items.Size();++i)
+ {
+ if (CMusicDatabaseDirectory::IsAllItem(items[i]->GetPath()) || items[i]->IsParentFolder())
+ continue;
+
+ m_pathsToScan.insert(items[i]->GetPath());
+ if (refresh)
+ {
+ m_musicDatabase.ClearArtistLastScrapedTime(items[i]->GetMusicInfoTag()->GetDatabaseId());
+ }
+ }
+ m_musicDatabase.Close();
+
+ m_scanType = 2;
+ m_bRunning = true;
+ Process();
+}
+
+void CMusicInfoScanner::Stop()
+{
+ if (m_bCanInterrupt)
+ m_musicDatabase.Interrupt();
+
+ m_bStop = true;
+}
+
+static void OnDirectoryScanned(const std::string& strDirectory)
+{
+ CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
+ msg.SetStringParam(strDirectory);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+static std::string Prettify(const std::string& strDirectory)
+{
+ CURL url(strDirectory);
+
+ return CURL::Decode(url.GetWithoutUserDetails());
+}
+
+bool CMusicInfoScanner::DoScan(const std::string& strDirectory)
+{
+ if (m_handle)
+ {
+ m_handle->SetTitle(g_localizeStrings.Get(506)); //"Checking media files..."
+ m_handle->SetText(Prettify(strDirectory));
+ }
+
+ std::set<std::string>::const_iterator it = m_seenPaths.find(strDirectory);
+ if (it != m_seenPaths.end())
+ return true;
+
+ m_seenPaths.insert(strDirectory);
+
+ // Discard all excluded files defined by m_musicExcludeRegExps
+ const std::vector<std::string> &regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromScanRegExps;
+
+ if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
+ return true;
+
+ if (HasNoMedia(strDirectory))
+ return true;
+
+ // load subfolder
+ CFileItemList items;
+ CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions() + "|.jpg|.tbn|.lrc|.cdg", DIR_FLAG_DEFAULTS);
+
+ // sort and get the path hash. Note that we don't filter .cue sheet items here as we want
+ // to detect changes in the .cue sheet as well. The .cue sheet items only need filtering
+ // if we have a changed hash.
+ items.Sort(SortByLabel, SortOrderAscending);
+ std::string hash;
+ GetPathHash(items, hash);
+
+ // check whether we need to rescan or not
+ std::string dbHash;
+ if ((m_flags & SCAN_RESCAN) || !m_musicDatabase.GetPathHash(strDirectory, dbHash) || !StringUtils::EqualsNoCase(dbHash, hash))
+ { // path has changed - rescan
+ if (dbHash.empty())
+ CLog::Log(LOGDEBUG, "{} Scanning dir '{}' as not in the database", __FUNCTION__,
+ CURL::GetRedacted(strDirectory));
+ else
+ CLog::Log(LOGDEBUG, "{} Rescanning dir '{}' due to change", __FUNCTION__,
+ CURL::GetRedacted(strDirectory));
+
+ if (m_handle)
+ m_handle->SetTitle(g_localizeStrings.Get(505)); //"Loading media information from files..."
+
+ // filter items in the sub dir (for .cue sheet support)
+ items.FilterCueItems();
+ items.Sort(SortByLabel, SortOrderAscending);
+
+ // and then scan in the new information from tags
+ if (RetrieveMusicInfo(strDirectory, items) > 0)
+ {
+ if (m_handle)
+ OnDirectoryScanned(strDirectory);
+ }
+
+ // save information about this folder
+ m_musicDatabase.SetPathHash(strDirectory, hash);
+ }
+ else
+ { // path is the same - no need to rescan
+ CLog::Log(LOGDEBUG, "{} Skipping dir '{}' due to no change", __FUNCTION__,
+ CURL::GetRedacted(strDirectory));
+ m_currentItem += CountFiles(items, false); // false for non-recursive
+
+ // updated the dialog with our progress
+ if (m_handle)
+ {
+ if (m_itemCount>0)
+ m_handle->SetPercentage(static_cast<float>(m_currentItem * 100) / static_cast<float>(m_itemCount));
+ OnDirectoryScanned(strDirectory);
+ }
+ }
+
+ // now scan the subfolders
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr pItem = items[i];
+
+ if (m_bStop)
+ break;
+ // if we have a directory item (non-playlist) we then recurse into that folder
+ if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList())
+ {
+ std::string strPath=pItem->GetPath();
+ if (!DoScan(strPath))
+ {
+ m_bStop = true;
+ }
+ }
+ }
+ return !m_bStop;
+}
+
+CInfoScanner::INFO_RET CMusicInfoScanner::ScanTags(const CFileItemList& items,
+ CFileItemList& scannedItems)
+{
+ std::vector<std::string> regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromScanRegExps;
+
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ if (m_bStop)
+ return INFO_CANCELLED;
+
+ CFileItemPtr pItem = items[i];
+
+ if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), regexps))
+ continue;
+
+ if (pItem->m_bIsFolder || pItem->IsPlayList() || pItem->IsPicture() || pItem->IsLyrics())
+ continue;
+
+ m_currentItem++;
+
+ CMusicInfoTag& tag = *pItem->GetMusicInfoTag();
+ if (!tag.Loaded())
+ {
+ std::unique_ptr<IMusicInfoTagLoader> pLoader (CMusicInfoTagLoaderFactory::CreateLoader(*pItem));
+ if (nullptr != pLoader)
+ pLoader->Load(pItem->GetPath(), tag);
+ }
+
+ if (m_handle && m_itemCount>0)
+ m_handle->SetPercentage(static_cast<float>(m_currentItem * 100) / static_cast<float>(m_itemCount));
+
+ if (!tag.Loaded() && !pItem->HasCueDocument())
+ {
+ CLog::Log(LOGDEBUG, "{} - No tag found for: {}", __FUNCTION__, pItem->GetPath());
+ continue;
+ }
+ else
+ {
+ if (!tag.GetCueSheet().empty())
+ pItem->LoadEmbeddedCue();
+ }
+
+ if (pItem->HasCueDocument())
+ pItem->LoadTracksFromCueDocument(scannedItems);
+ else
+ scannedItems.Add(pItem);
+ }
+ return INFO_ADDED;
+}
+
+static bool SortSongsByTrack(const CSong& song, const CSong& song2)
+{
+ return song.iTrack < song2.iTrack;
+}
+
+void CMusicInfoScanner::FileItemsToAlbums(CFileItemList& items, VECALBUMS& albums, MAPSONGS* songsMap /* = NULL */)
+{
+ /*
+ * Step 1: Convert the FileItems into Songs.
+ * If they're MB tagged, create albums directly from the FileItems.
+ * If they're non-MB tagged, index them by album name ready for step 2.
+ */
+ std::map<std::string, VECSONGS> songsByAlbumNames;
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CMusicInfoTag& tag = *items[i]->GetMusicInfoTag();
+ CSong song(*items[i]);
+
+ // keep the db-only fields intact on rescan...
+ if (songsMap != NULL)
+ {
+ // Match up item to songs in library previously scanned with this path
+ MAPSONGS::iterator songlist = songsMap->find(items[i]->GetPath());
+ if (songlist != songsMap->end())
+ {
+ VECSONGS::iterator foundsong;
+ if (songlist->second.size() == 1)
+ foundsong = songlist->second.begin();
+ else
+ {
+ // When filename mapped to multiple songs it is from cuesheet, match on disc/track number
+ int disctrack = tag.GetTrackAndDiscNumber();
+ foundsong = std::find_if(songlist->second.begin(), songlist->second.end(),
+ [&](const CSong& song) { return disctrack == song.iTrack; });
+ }
+ if (foundsong != songlist->second.end())
+ {
+ song.idSong = foundsong->idSong; // Reuse ID
+ song.dateNew = foundsong->dateNew; // Keep date originally created
+ song.iTimesPlayed = foundsong->iTimesPlayed;
+ song.lastPlayed = foundsong->lastPlayed;
+ if (song.rating == 0)
+ song.rating = foundsong->rating;
+ if (song.userrating == 0)
+ song.userrating = foundsong->userrating;
+ if (song.strThumb.empty())
+ song.strThumb = foundsong->strThumb;
+ }
+ }
+ }
+
+ if (!tag.GetMusicBrainzAlbumID().empty())
+ {
+ VECALBUMS::iterator it;
+ for (it = albums.begin(); it != albums.end(); ++it)
+ if (it->strMusicBrainzAlbumID == tag.GetMusicBrainzAlbumID())
+ break;
+
+ if (it == albums.end())
+ {
+ CAlbum album(*items[i]);
+ album.songs.push_back(song);
+ albums.push_back(album);
+ }
+ else
+ it->songs.push_back(song);
+ }
+ else
+ songsByAlbumNames[tag.GetAlbum()].push_back(song);
+ }
+
+ /*
+ Step 2: Split into unique albums based on album name and album artist
+ In the case where the album artist is unknown, we use the primary artist
+ (i.e. first artist from each song).
+ */
+ for (auto& songsByAlbumName : songsByAlbumNames)
+ {
+ VECSONGS& songs = songsByAlbumName.second;
+ // sort the songs by tracknumber to identify duplicate track numbers
+ sort(songs.begin(), songs.end(), SortSongsByTrack);
+
+ // map the songs to their primary artists
+ bool tracksOverlap = false;
+ bool hasAlbumArtist = false;
+ bool isCompilation = true;
+ std::string old_DiscSubtitle;
+
+ std::map<std::string, std::vector<CSong *> > artists;
+ for (VECSONGS::iterator song = songs.begin(); song != songs.end(); ++song)
+ {
+ // test for song overlap
+ if (song != songs.begin() && song->iTrack == (song - 1)->iTrack)
+ tracksOverlap = true;
+
+ if (!song->bCompilation)
+ isCompilation = false;
+
+ if (song->strDiscSubtitle != old_DiscSubtitle)
+ old_DiscSubtitle = song->strDiscSubtitle;
+
+ // get primary artist
+ std::string primary;
+ if (!song->GetAlbumArtist().empty())
+ {
+ primary = song->GetAlbumArtist()[0];
+ hasAlbumArtist = true;
+ }
+ else if (!song->artistCredits.empty())
+ primary = song->artistCredits.begin()->GetArtist();
+
+ // add to the artist map
+ artists[primary].push_back(&(*song));
+ }
+
+ /*
+ We have a Various Artists compilation if
+ 1. album name is non-empty AND
+ 2a. no tracks overlap OR
+ 2b. all tracks are marked as part of compilation AND
+ 3a. a unique primary artist is specified as "various", "various artists" or the localized value
+ OR
+ 3b. we have at least two primary artists and no album artist specified.
+ */
+ std::string various = g_localizeStrings.Get(340); // Various Artists
+ bool compilation =
+ !songsByAlbumName.first.empty() && (isCompilation || !tracksOverlap); // 1+2b+2a
+ if (artists.size() == 1)
+ {
+ std::string artist = artists.begin()->first; StringUtils::ToLower(artist);
+ if (!StringUtils::EqualsNoCase(artist, "various") &&
+ !StringUtils::EqualsNoCase(artist, "various artists") &&
+ !StringUtils::EqualsNoCase(artist, various)) // 3a
+ compilation = false;
+ else
+ // Grab name for use in "various artist" artist
+ various = artists.begin()->first;
+ }
+ else if (hasAlbumArtist) // 3b
+ compilation = false;
+
+ // Such a compilation album is stored under a unique artist that matches on Musicbrainz ID
+ // the "various artists" artist for music tagged with mbids.
+ if (compilation)
+ {
+ CLog::Log(LOGDEBUG, "Album '{}' is a compilation as there's no overlapping tracks and {}",
+ songsByAlbumName.first,
+ hasAlbumArtist ? "the album artist is 'Various'"
+ : "there is more than one unique artist");
+ // Clear song artists from artists map, put songs under "various artists" mbid entry
+ artists.clear();
+ for (auto& song : songs)
+ artists[VARIOUSARTISTS_MBID].push_back(&song);
+ }
+
+ /*
+ We also have a compilation album when album name is non-empty and ALL tracks are marked as part of
+ a compilation even if an album artist is given, or all songs have the same primary artist. For
+ example an anthology - a collection of recordings from various old sources
+ combined together such as a "best of", retrospective or rarities type release.
+
+ Such an anthology compilation will not have been caught by the previous tests as it fails 3a and 3b.
+ The album artist can be determined just like any normal album.
+ */
+ if (!compilation && !songsByAlbumName.first.empty() && isCompilation)
+ {
+ compilation = true;
+ CLog::Log(LOGDEBUG,
+ "Album '{}' is a compilation as all songs are marked as part of a compilation",
+ songsByAlbumName.first);
+ }
+
+ /*
+ Step 3: Find the common albumartist for each song and assign
+ albumartist to those tracks that don't have it set.
+ */
+ for (auto& j : artists)
+ {
+ /* Find the common artist(s) for these songs (grouped under primary artist).
+ Various artist compilations already under the unique "various artists" mbid.
+ Take from albumartist tag when present, or use artist tag.
+ When from albumartist tag also check albumartistsort tag and take first non-empty value
+ */
+ std::vector<CSong*>& artistSongs = j.second;
+ std::vector<std::string> common;
+ std::string albumartistsort;
+ if (artistSongs.front()->GetAlbumArtist().empty())
+ common = artistSongs.front()->GetArtist();
+ else
+ {
+ common = artistSongs.front()->GetAlbumArtist();
+ albumartistsort = artistSongs.front()->GetAlbumArtistSort();
+ }
+ for (std::vector<CSong *>::iterator k = artistSongs.begin() + 1; k != artistSongs.end(); ++k)
+ {
+ unsigned int match = 0;
+ std::vector<std::string> compare;
+ if ((*k)->GetAlbumArtist().empty())
+ compare = (*k)->GetArtist();
+ else
+ {
+ compare = (*k)->GetAlbumArtist();
+ if (albumartistsort.empty())
+ albumartistsort = (*k)->GetAlbumArtistSort();
+ }
+ for (; match < common.size() && match < compare.size(); match++)
+ {
+ if (compare[match] != common[match])
+ break;
+ }
+ common.erase(common.begin() + match, common.end());
+ }
+ if (j.first == VARIOUSARTISTS_MBID)
+ {
+ common.clear();
+ common.emplace_back(VARIOUSARTISTS_MBID);
+ }
+
+ /*
+ Step 4: Assign the album artist for each song that doesn't have it set
+ and add to the album vector
+ */
+ CAlbum album;
+ album.strAlbum = songsByAlbumName.first;
+
+ //Split the albumartist sort string to try and get sort names for individual artists
+ std::vector<std::string> sortnames = StringUtils::Split(albumartistsort, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ if (sortnames.size() != common.size())
+ // Split artist sort names further using multiple possible delimiters, over single separator applied in Tag loader
+ sortnames = StringUtils::SplitMulti(sortnames, { ";", ":", "|", "#" });
+
+ for (size_t i = 0; i < common.size(); i++)
+ {
+ if (common[i] == VARIOUSARTISTS_MBID)
+ /* Treat "various", "various artists" and the localized equivalent name as the same
+ album artist as the artist with Musicbrainz ID 89ad4ac3-39f7-470e-963a-56509c546377.
+ If adding this artist for the first time then the name will be set to either the primary
+ artist read from tags when 3a, or the localized value for "various artists" when not 3a.
+ This means that tag values are no longer translated into the current language.
+ */
+ album.artistCredits.emplace_back(various, VARIOUSARTISTS_MBID);
+ else
+ {
+ album.artistCredits.emplace_back(StringUtils::Trim(common[i]));
+ // Set artist sort name providing we have as many as we have artists,
+ // otherwise something is wrong with them so ignore rather than guess.
+ if (sortnames.size() == common.size())
+ album.artistCredits.back().SetSortName(StringUtils::Trim(sortnames[i]));
+ }
+ }
+ album.bCompilation = compilation;
+ for (auto& k : artistSongs)
+ {
+ if (k->GetAlbumArtist().empty())
+ k->SetAlbumArtist(common);
+ //! @todo in future we may wish to union up the genres, for now we assume they're the same
+ album.genre = k->genre;
+ album.strArtistSort = k->GetAlbumArtistSort();
+ // in addition, we may want to use release date as discriminating between albums
+ album.strReleaseDate = k->strReleaseDate,
+ album.strLabel = k->strRecordLabel;
+ album.strType = k->strAlbumType;
+ album.songs.push_back(*k);
+ }
+ albums.push_back(album);
+ }
+ }
+}
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::UpdateAlbumInfo(CAlbum& album,
+ const ADDON::ScraperPtr& scraper,
+ bool bAllowSelection,
+ CGUIDialogProgress* pDialog)
+{
+ m_musicDatabase.Open();
+ INFO_RET result = UpdateDatabaseAlbumInfo(album, scraper, bAllowSelection, pDialog);
+ m_musicDatabase.Close();
+ return result;
+}
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::UpdateArtistInfo(CArtist& artist,
+ const ADDON::ScraperPtr& scraper,
+ bool bAllowSelection,
+ CGUIDialogProgress* pDialog)
+{
+ m_musicDatabase.Open();
+ INFO_RET result = UpdateDatabaseArtistInfo(artist, scraper, bAllowSelection, pDialog);
+ m_musicDatabase.Close();
+ return result;
+}
+
+int CMusicInfoScanner::RetrieveMusicInfo(const std::string& strDirectory, CFileItemList& items)
+{
+ MAPSONGS songsMap;
+
+ // get all information for all files in current directory from database, and remove them
+ if (m_musicDatabase.RemoveSongsFromPath(strDirectory, songsMap))
+ m_needsCleanup = true;
+
+ CFileItemList scannedItems;
+ if (ScanTags(items, scannedItems) == INFO_CANCELLED || scannedItems.Size() == 0)
+ return 0;
+
+ VECALBUMS albums;
+ FileItemsToAlbums(scannedItems, albums, &songsMap);
+
+ /*
+ Set thumb for songs and, if only one album in folder, store the thumb for
+ the album (music db) and the folder path (in Textures db) too.
+ The album and path thumb is either set to the folder art, or failing that to
+ the art embedded in the first music file.
+ Song thumb is only set when it varies, otherwise it is cleared so that it will
+ fallback to the album art (that may be from the first file, or that of the
+ folder or set later by scraping from NFO files or remote sources). Clearing
+ saves caching repeats of the same image.
+
+ However even if all songs are from one album this may not be the album
+ folder. It could be just a subfolder containing some of the songs from a disc
+ set e.g. CD1, CD2 etc., or the album could spread across many folders. In
+ this case the album art gets reset every time a folder with songs from just
+ that album is processed, and needs to be corrected later once all the parts
+ of the album have been scanned.
+ */
+ FindArtForAlbums(albums, items.GetPath());
+
+ /* Strategy: Having scanned tags and made a list of albums, add them to the library. Only then try
+ to scrape additional album and artist information. Music is often tagged to a mixed standard
+ - some albums have mbid tags, some don't. Once all the music files have been added to the library,
+ the mbid for an artist will be known even if it was only tagged on one song. The artist is best
+ scraped with an mbid, so scrape after all the files that may provide that tag have been scanned.
+ That artist mbid can then be used to improve the accuracy of scraping other albums by that artist
+ even when it was not in the tagging for that album.
+
+ Doing scraping, generally the slower activity, in the background after scanning has fully populated
+ the library also means that the user can use their library to select music to play sooner.
+ */
+
+ int numAdded = 0;
+
+ // Add all albums to the library, and hence any new song or album artists or other contributors
+ for (auto& album : albums)
+ {
+ if (m_bStop)
+ break;
+
+ // mark albums without a title as singles
+ if (album.strAlbum.empty())
+ album.releaseType = CAlbum::Single;
+
+ album.strPath = strDirectory;
+ m_musicDatabase.AddAlbum(album, m_idSourcePath);
+ m_albumsAdded.insert(album.idAlbum);
+
+ numAdded += static_cast<int>(album.songs.size());
+ }
+ return numAdded;
+}
+
+void MUSIC_INFO::CMusicInfoScanner::ScrapeInfoAddedAlbums()
+{
+ /* Strategy: Having scanned tags, make a list of albums and add them to the library, only then try
+ to scrape additional album and artist information. Music is often tagged to a mixed standard
+ - some albums have mbid tags, some don't. Once all the music files have been added to the library,
+ the mbid for an artist will be known even if it was only tagged on one song. The artist is best
+ scraped with an mbid, so scrape after all the files that may provide that tag have been scanned.
+ That artist mbid can then be used to improve the accuracy of scraping other albums by that artist
+ even when it was not in the tagging for that album.
+
+ Doing scraping, generally the slower activity, in the background after scanning has fully populated
+ the library also means that the user can use their library to select music to play sooner.
+ */
+
+ /* Scrape additional album and artist data.
+ For albums and artists without mbids, matching on album-artist pair can
+ be used to identify artist with greater accuracy than artist name alone.
+ Artist mbid returned by album scraper is used if we do not already have it.
+ Hence scrape album then related artists.
+ */
+ ADDON::AddonPtr addon;
+
+ ADDON::ScraperPtr albumScraper;
+ ADDON::ScraperPtr artistScraper;
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ALBUMS, addon))
+ albumScraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
+
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::SCRAPER_ARTISTS,
+ addon))
+ artistScraper = std::dynamic_pointer_cast<ADDON::CScraper>(addon);
+
+ bool albumartistsonly = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS);
+
+ if (!albumScraper || !artistScraper)
+ return;
+
+ int i = 0;
+ std::set<int> artists;
+ for (auto albumId : m_albumsAdded)
+ {
+ i++;
+ if (m_bStop)
+ break;
+ // Scrape album data
+ CAlbum album;
+ if (!m_musicDatabase.HasAlbumBeenScraped(albumId))
+ {
+ if (m_handle)
+ {
+ m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
+ m_handle->SetProgress(i, static_cast<int>(m_albumsAdded.size()));
+ }
+
+ // Fetch any artist mbids for album artist(s) and song artists when scraping those too.
+ m_musicDatabase.GetAlbum(albumId, album, !albumartistsonly);
+ UpdateDatabaseAlbumInfo(album, albumScraper, false);
+
+ // Scrape information for artists that have not been scraped before, avoiding repeating
+ // unsuccessful attempts for every album and song.
+ for (const auto &artistCredit : album.artistCredits)
+ {
+ if (m_bStop)
+ break;
+
+ if (!m_musicDatabase.HasArtistBeenScraped(artistCredit.GetArtistId()) &&
+ artists.find(artistCredit.GetArtistId()) == artists.end())
+ {
+ artists.insert(artistCredit.GetArtistId()); // Artist scraping attempted
+ CArtist artist;
+ m_musicDatabase.GetArtist(artistCredit.GetArtistId(), artist);
+ UpdateDatabaseArtistInfo(artist, artistScraper, false);
+ }
+ }
+ // Only scrape song artists if they are being displayed in artists node by default
+ if (!albumartistsonly)
+ {
+ for (auto &song : album.songs)
+ {
+ if (m_bStop)
+ break;
+ for (const auto &artistCredit : song.artistCredits)
+ {
+ if (m_bStop)
+ break;
+
+ CMusicArtistInfo musicArtistInfo;
+ if (!m_musicDatabase.HasArtistBeenScraped(artistCredit.GetArtistId()) &&
+ artists.find(artistCredit.GetArtistId()) == artists.end())
+ {
+ artists.insert(artistCredit.GetArtistId()); // Artist scraping attempted
+ CArtist artist;
+ m_musicDatabase.GetArtist(artistCredit.GetArtistId(), artist);
+ UpdateDatabaseArtistInfo(artist, artistScraper, false);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/*
+ Set thumb for songs and the album(if only one album in folder).
+ The album thumb is either set to the folder art, or failing that to the art
+ embedded in the first music file. However this does not allow for there being
+ other folders with more songs from the album e.g. this was a subfolder CD1
+ and there is CD2 etc. yet to be processed
+ Song thumb is only set when it varies, otherwise it is cleared so that it will
+ fallback to the album art(that may be from the first file, or that of the
+ folder or set later by scraping from NFO files or remote sources).Clearing
+ saves caching repeats of the same image.
+*/
+void CMusicInfoScanner::FindArtForAlbums(VECALBUMS &albums, const std::string &path)
+{
+ /*
+ If there's a single album in the folder, then art can be taken from
+ the folder art.
+ */
+ std::string albumArt;
+ if (albums.size() == 1)
+ {
+ CFileItem album(path, true);
+ /*
+ If we are scanning a directory served over http(s) the root directory for an album will set
+ IsInternetStream to true which prevents scanning it for art. As we can't reach this point
+ without having read some tags (and tags are not read from streams) we can safely check for
+ that case and set the IsHTTPDirectory property to enable scanning for art.
+ */
+ if (StringUtils::StartsWithNoCase(path, "http") && StringUtils::EndsWith(path, "/"))
+ album.SetProperty("IsHTTPDirectory", true);
+ albumArt = album.GetUserMusicThumb(true);
+ if (!albumArt.empty())
+ albums[0].art["thumb"] = albumArt;
+ }
+ for (auto& album : albums)
+ {
+ if (albums.size() != 1)
+ albumArt = "";
+
+ /*
+ Find art that is common across these items
+ If we find a single art image we treat it as the album art
+ and discard song art else we use first as album art and
+ keep everything as song art.
+ */
+ bool singleArt = true;
+ CSong *art = NULL;
+ for (auto& song : album.songs)
+ {
+ if (song.HasArt())
+ {
+ if (art && !art->ArtMatches(song))
+ {
+ singleArt = false;
+ break;
+ }
+ if (!art)
+ art = &song;
+ }
+ }
+
+ /*
+ assign the first art found to the album - better than no art at all
+ */
+
+ if (art && albumArt.empty())
+ {
+ if (!art->strThumb.empty())
+ albumArt = art->strThumb;
+ else
+ albumArt = CTextureUtils::GetWrappedImageURL(art->strFileName, "music");
+ }
+
+ if (!albumArt.empty())
+ album.art["thumb"] = albumArt;
+
+ if (singleArt)
+ { //if singleArt then we can clear the artwork for all songs
+ for (auto& k : album.songs)
+ k.strThumb.clear();
+ }
+ else
+ { // more than one piece of art was found for these songs, so cache per song
+ for (auto& k : album.songs)
+ {
+ if (k.strThumb.empty() && !k.embeddedArt.Empty())
+ k.strThumb = CTextureUtils::GetWrappedImageURL(k.strFileName, "music");
+ }
+ }
+ }
+ if (albums.size() == 1 && !albumArt.empty())
+ {
+ // assign to folder thumb as well
+ CFileItem albumItem(path, true);
+ CMusicThumbLoader loader;
+ loader.SetCachedImage(albumItem, "thumb", albumArt);
+ }
+}
+
+void MUSIC_INFO::CMusicInfoScanner::RetrieveLocalArt()
+{
+ if (m_handle)
+ {
+ m_handle->SetTitle(g_localizeStrings.Get(506)); //"Checking media files..."
+ //!@todo: title = Checking for local art
+ }
+
+ std::set<int> artistsArtDone; // artists processed to avoid unsuccessful repeats
+ int count = 0;
+ for (auto albumId : m_albumsAdded)
+ {
+ count++;
+ if (m_bStop)
+ break;
+ CAlbum album;
+ m_musicDatabase.GetAlbum(albumId, album, false);
+ if (m_handle)
+ {
+ m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
+ m_handle->SetProgress(count, static_cast<int>(m_albumsAdded.size()));
+ }
+
+ /*
+ Automatically fetch local art from album folder and any disc sets subfolders
+
+ Providing all songs from an album are are under a unique common album
+ folder (no songs from other albums) then thumb has been set to local art,
+ or failing that to embedded art, during scanning by FindArtForAlbums().
+ But when songs are also spread over multiple subfolders within it e.g. disc
+ sets, it will have been set to either the art of the last subfolder that was
+ processed (if there is any), or from the first song in that subfolder with
+ embedded art (if there is any). To correct this and find any thumb in the
+ (common) album folder add "thumb" to those missing.
+ */
+ AddAlbumArtwork(album);
+
+ /*
+ Local album artist art
+
+ Look in the nominated "Artist Information Folder" for thumbs and fanart.
+ Failing that, for backward compatibility, fallback to the folder immediately
+ above the album folder.
+ It can only fallback if the album has a unique folder, and can only do so
+ for the first album artist if the album is a collaboration e.g. composer,
+ conductor, orchestra, or by several pop artists in their own right.
+ Avoids repeatedly processing the same artist by maintaining a set.
+
+ Adding the album may have added new artists, or provide art for an existing
+ (song) artist, but does not replace any artwork already set. Hence once art
+ has been found for an album artist, art is not searched for in other folders.
+
+ It will find art for "various artists", if artwork is located above the
+ folder containing compilatons.
+ */
+ for (auto artistCredit = album.artistCredits.begin(); artistCredit != album.artistCredits.end(); ++artistCredit)
+ {
+ if (m_bStop)
+ break;
+ int idArtist = artistCredit->GetArtistId();
+ if (artistsArtDone.find(idArtist) == artistsArtDone.end())
+ {
+ artistsArtDone.insert(idArtist); // Artist processed
+
+ // Get artist and subfolder within the Artist Information Folder
+ CArtist artist;
+ m_musicDatabase.GetArtist(idArtist, artist);
+ m_musicDatabase.GetArtistPath(artist, artist.strPath);
+ // Location of local art
+ std::string artfolder;
+ if (CDirectory::Exists(artist.strPath))
+ // When subfolder exists that is only place we look for local art
+ artfolder = artist.strPath;
+ else if (!album.strPath.empty() && artistCredit == album.artistCredits.begin())
+ {
+ // If no individual artist subfolder has been found, for primary
+ // album artist only look in the folder immediately above the album
+ // folder. Not using GetOldArtistPath here because may not have not
+ // have scanned all the albums yet.
+ artfolder = URIUtils::GetParentPath(album.strPath);
+ }
+ AddArtistArtwork(artist, artfolder);
+ }
+ }
+ }
+}
+
+int CMusicInfoScanner::GetPathHash(const CFileItemList &items, std::string &hash)
+{
+ // Create a hash based on the filenames, filesize and filedate. Also count the number of files
+ if (0 == items.Size()) return 0;
+ CDigest digest{CDigest::Type::MD5};
+ int count = 0;
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ const CFileItemPtr pItem = items[i];
+ digest.Update(pItem->GetPath());
+ digest.Update((unsigned char *)&pItem->m_dwSize, sizeof(pItem->m_dwSize));
+ KODI::TIME::FileTime time = pItem->m_dateTime;
+ digest.Update((unsigned char*)&time, sizeof(KODI::TIME::FileTime));
+ if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO())
+ count++;
+ }
+ hash = digest.Finalize();
+ return count;
+}
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::UpdateDatabaseAlbumInfo(CAlbum& album,
+ const ADDON::ScraperPtr& scraper,
+ bool bAllowSelection,
+ CGUIDialogProgress* pDialog /* = NULL */)
+{
+ if (!scraper)
+ return INFO_ERROR;
+
+ CMusicAlbumInfo albumInfo;
+ INFO_RET albumDownloadStatus(INFO_CANCELLED);
+ std::string origArtist(album.GetAlbumArtistString());
+ std::string origAlbum(album.strAlbum);
+
+ bool stop(false);
+ while (!stop)
+ {
+ stop = true;
+ CLog::Log(LOGDEBUG, "{} downloading info for: {}", __FUNCTION__, album.strAlbum);
+ albumDownloadStatus = DownloadAlbumInfo(album, scraper, albumInfo, !bAllowSelection, pDialog);
+ if (albumDownloadStatus == INFO_NOT_FOUND)
+ {
+ if (pDialog && bAllowSelection)
+ {
+ std::string strTempAlbum(album.strAlbum);
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strTempAlbum, CVariant{ g_localizeStrings.Get(16011) }, false))
+ albumDownloadStatus = INFO_CANCELLED;
+ else
+ {
+ std::string strTempArtist(album.GetAlbumArtistString());
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strTempArtist, CVariant{ g_localizeStrings.Get(16025) }, false))
+ albumDownloadStatus = INFO_CANCELLED;
+ else
+ {
+ album.strAlbum = strTempAlbum;
+ album.strArtistDesc = strTempArtist;
+ stop = false;
+ }
+ }
+ }
+ else
+ {
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(new CMediaLibraryEvent(
+ MediaTypeAlbum, album.strPath, 24146,
+ StringUtils::Format(g_localizeStrings.Get(24147), MediaTypeAlbum, album.strAlbum),
+ CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType()),
+ CURL::GetRedacted(album.strPath), EventLevel::Warning)));
+ }
+ }
+ }
+
+ // Restore original album and artist name, possibly changed by manual entry
+ // to get info but should not change outside merge
+ album.strAlbum = origAlbum;
+ album.strArtistDesc = origArtist;
+
+ if (albumDownloadStatus == INFO_ADDED)
+ {
+ bool overridetags = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_OVERRIDETAGS);
+ // Remove art accidentally set by the Python scraper, it only provides URLs of possible artwork
+ // Art is selected later applying whitelist and other art preferences
+ albumInfo.GetAlbum().art.clear();
+ album.MergeScrapedAlbum(albumInfo.GetAlbum(), overridetags);
+ m_musicDatabase.UpdateAlbum(album);
+ albumInfo.SetLoaded(true);
+ }
+
+ // Check album art.
+ // Fill any gaps with local art files or use first available from scraped URL list (when it has
+ // been successfully scraped) as controlled by whitelist. Do this even when no info added
+ // (cancelled, not found or error), there may be new local art files.
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) !=
+ CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE &&
+ AddAlbumArtwork(album))
+ albumDownloadStatus = INFO_ADDED; // Local art added
+
+ return albumDownloadStatus;
+}
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::UpdateDatabaseArtistInfo(CArtist& artist,
+ const ADDON::ScraperPtr& scraper,
+ bool bAllowSelection,
+ CGUIDialogProgress* pDialog /* = NULL */)
+{
+ if (!scraper)
+ return INFO_ERROR;
+
+ CMusicArtistInfo artistInfo;
+ INFO_RET artistDownloadStatus(INFO_CANCELLED);
+ std::string origArtist(artist.strArtist);
+
+ bool stop(false);
+ while (!stop)
+ {
+ stop = true;
+ CLog::Log(LOGDEBUG, "{} downloading info for: {}", __FUNCTION__, artist.strArtist);
+ artistDownloadStatus = DownloadArtistInfo(artist, scraper, artistInfo, !bAllowSelection, pDialog);
+ if (artistDownloadStatus == INFO_NOT_FOUND)
+ {
+ if (pDialog && bAllowSelection)
+ {
+ if (!CGUIKeyboardFactory::ShowAndGetInput(artist.strArtist, CVariant{ g_localizeStrings.Get(16025) }, false))
+ artistDownloadStatus = INFO_CANCELLED;
+ else
+ stop = false;
+ }
+ else
+ {
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(new CMediaLibraryEvent(
+ MediaTypeArtist, artist.strPath, 24146,
+ StringUtils::Format(g_localizeStrings.Get(24147), MediaTypeArtist, artist.strArtist),
+ CScraperUrl::GetThumbUrl(artist.thumbURL.GetFirstUrlByType()),
+ CURL::GetRedacted(artist.strPath), EventLevel::Warning)));
+ }
+ }
+ }
+
+ // Restore original artist name, possibly changed by manual entry to get info
+ // but should not change outside merge
+ artist.strArtist = origArtist;
+
+ if (artistDownloadStatus == INFO_ADDED)
+ {
+ artist.MergeScrapedArtist(artistInfo.GetArtist(), CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_OVERRIDETAGS));
+ m_musicDatabase.UpdateArtist(artist);
+ artistInfo.SetLoaded();
+ }
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL) ==
+ CSettings::MUSICLIBRARY_ARTWORK_LEVEL_NONE)
+ return artistDownloadStatus;
+
+ // Check artist art.
+ // Fill any gaps with local art files, or use first available from scraped
+ // list (when it has been successfully scraped). Do this even when no info
+ // added (cancelled, not found or error), there may be new local art files.
+ // Get individual artist subfolder within the Artist Information Folder
+ m_musicDatabase.GetArtistPath(artist, artist.strPath);
+ // Location of local art
+ std::string artfolder;
+ if (CDirectory::Exists(artist.strPath))
+ // When subfolder exists that is only place we look for art
+ artfolder = artist.strPath;
+ else
+ {
+ // Fallback to the old location local to music files (when there is a
+ // unique folder). If there is no folder for the artist, and *only* the
+ // artist, this will be blank
+ m_musicDatabase.GetOldArtistPath(artist.idArtist, artfolder);
+ }
+ if (AddArtistArtwork(artist, artfolder))
+ artistDownloadStatus = INFO_ADDED; // Local art added
+
+ return artistDownloadStatus; // Added, cancelled or not found
+}
+
+#define THRESHOLD .95f
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::DownloadAlbumInfo(const CAlbum& album,
+ const ADDON::ScraperPtr& info,
+ CMusicAlbumInfo& albumInfo,
+ bool bUseScrapedMBID,
+ CGUIDialogProgress* pDialog)
+{
+ if (m_handle)
+ {
+ m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20321), info->Name()));
+ m_handle->SetText(album.GetAlbumArtistString() + " - " + album.strAlbum);
+ }
+
+ // clear our scraper cache
+ info->ClearCache();
+
+ CMusicInfoScraper scraper(info);
+ bool bMusicBrainz = false;
+ /*
+ When the mbid is derived from tags scraping of album information is done directly
+ using that ID, otherwise the lookup is based on album and artist names and can mis-identify the
+ album (i.e. classical music has many "Symphony No. 5"). To be able to correct any mistakes a
+ manual refresh of artist information uses either the mbid if derived from tags or the album
+ and artist names, not any previously scraped mbid.
+ */
+ if (!album.strMusicBrainzAlbumID.empty() && (!album.bScrapedMBID || bUseScrapedMBID))
+ {
+ CScraperUrl musicBrainzURL;
+ if (ResolveMusicBrainz(album.strMusicBrainzAlbumID, info, musicBrainzURL))
+ {
+ CMusicAlbumInfo albumNfo("nfo", musicBrainzURL);
+ scraper.GetAlbums().clear();
+ scraper.GetAlbums().push_back(albumNfo);
+ bMusicBrainz = true;
+ }
+ }
+
+ // handle nfo files
+ bool existsNFO = false;
+ std::string path = album.strPath;
+ if (path.empty())
+ m_musicDatabase.GetAlbumPath(album.idAlbum, path);
+
+ std::string strNfo = URIUtils::AddFileToFolder(path, "album.nfo");
+ CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
+ CNfoFile nfoReader;
+ existsNFO = CFileUtils::Exists(strNfo);
+ // When on GUI ask user if they want to ignore nfo and refresh from Internet
+ if (existsNFO && pDialog && CGUIDialogYesNo::ShowAndGetInput(10523, 20446))
+ {
+ existsNFO = false;
+ CLog::Log(LOGDEBUG, "Ignoring nfo file: {}", CURL::GetRedacted(strNfo));
+ }
+ if (existsNFO)
+ {
+ CLog::Log(LOGDEBUG, "Found matching nfo file: {}", CURL::GetRedacted(strNfo));
+ result = nfoReader.Create(strNfo, info);
+ if (result == CInfoScanner::FULL_NFO)
+ {
+ CLog::Log(LOGDEBUG, "{} Got details from nfo", __FUNCTION__);
+ nfoReader.GetDetails(albumInfo.GetAlbum());
+ return INFO_ADDED;
+ }
+ else if (result == CInfoScanner::URL_NFO ||
+ result == CInfoScanner::COMBINED_NFO)
+ {
+ CScraperUrl scrUrl(nfoReader.ScraperUrl());
+ CMusicAlbumInfo albumNfo("nfo",scrUrl);
+ ADDON::ScraperPtr nfoReaderScraper = nfoReader.GetScraperInfo();
+ CLog::Log(LOGDEBUG, "-- nfo-scraper: {}", nfoReaderScraper->Name());
+ CLog::Log(LOGDEBUG, "-- nfo url: {}", scrUrl.GetFirstThumbUrl());
+ scraper.SetScraperInfo(nfoReaderScraper);
+ scraper.GetAlbums().clear();
+ scraper.GetAlbums().push_back(albumNfo);
+ }
+ else if (result != CInfoScanner::OVERRIDE_NFO)
+ CLog::Log(LOGERROR, "Unable to find an url in nfo file: {}", strNfo);
+ }
+
+ if (!scraper.CheckValidOrFallback(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER)))
+ { // the current scraper is invalid, as is the default - bail
+ CLog::Log(LOGERROR, "{} - current and default scrapers are invalid. Pick another one",
+ __FUNCTION__);
+ return INFO_ERROR;
+ }
+
+ if (!scraper.GetAlbumCount())
+ {
+ scraper.FindAlbumInfo(album.strAlbum, album.GetAlbumArtistString());
+
+ while (!scraper.Completed())
+ {
+ if (m_bStop)
+ {
+ scraper.Cancel();
+ return INFO_CANCELLED;
+ }
+ ScannerWait(1);
+ }
+ /*
+ Finding album using xml scraper may request data from Musicbrainz.
+ MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
+ returns 503 errors for all calls from that IP address.
+ To stay below the rate-limit threshold wait 1s before proceeding
+ */
+ if (!info->IsPython())
+ ScannerWait(1000);
+ }
+
+ CGUIDialogSelect *pDlg = NULL;
+ int iSelectedAlbum=0;
+ if ((result == CInfoScanner::NO_NFO || result == CInfoScanner::OVERRIDE_NFO)
+ && !bMusicBrainz)
+ {
+ iSelectedAlbum = -1; // set negative so that we can detect a failure
+ if (scraper.Succeeded() && scraper.GetAlbumCount() >= 1)
+ {
+ double bestRelevance = 0;
+ double minRelevance = static_cast<double>(THRESHOLD);
+ if (pDialog || scraper.GetAlbumCount() > 1) // score the matches
+ {
+ //show dialog with all albums found
+ if (pDialog)
+ {
+ pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ pDlg->SetHeading(CVariant{g_localizeStrings.Get(181)});
+ pDlg->Reset();
+ pDlg->EnableButton(true, 413); // manual
+ pDlg->SetUseDetails(true);
+ }
+
+ CFileItemList items;
+ for (int i = 0; i < scraper.GetAlbumCount(); ++i)
+ {
+ CMusicAlbumInfo& info = scraper.GetAlbum(i);
+ double relevance = static_cast<double>(info.GetRelevance());
+ if (relevance < 0)
+ relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum, album.strAlbum,
+ info.GetAlbum().GetAlbumArtistString(),
+ album.GetAlbumArtistString());
+
+ // if we're doing auto-selection (ie querying all albums at once, then allow 95->100% for perfect matches)
+ // otherwise, perfect matches only
+ if (relevance >= std::max(minRelevance, bestRelevance))
+ { // we auto-select the best of these
+ bestRelevance = relevance;
+ iSelectedAlbum = i;
+ }
+ if (pDialog)
+ {
+ // set the label to [relevance] album - artist
+ std::string strTemp = StringUtils::Format("[{:0.2f}] {}", relevance, info.GetTitle2());
+ CFileItemPtr item(new CFileItem("", false));
+ item->SetLabel(strTemp);
+
+ std::string strTemp2;
+ if (!scraper.GetAlbum(i).GetAlbum().strType.empty())
+ strTemp2 += scraper.GetAlbum(i).GetAlbum().strType;
+ if (!scraper.GetAlbum(i).GetAlbum().strReleaseDate.empty())
+ strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strReleaseDate;
+ if (!scraper.GetAlbum(i).GetAlbum().strReleaseStatus.empty())
+ strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strReleaseStatus;
+ if (!scraper.GetAlbum(i).GetAlbum().strLabel.empty())
+ strTemp2 += " - " + scraper.GetAlbum(i).GetAlbum().strLabel;
+ item->SetLabel2(strTemp2);
+
+ item->SetArt(scraper.GetAlbum(i).GetAlbum().art);
+
+ items.Add(item);
+ }
+ if (!pDialog && relevance > 0.999) // we're so close, no reason to search further
+ break;
+ }
+
+ if (pDialog)
+ {
+ pDlg->Sort(false);
+ pDlg->SetItems(items);
+ pDlg->Open();
+
+ // and wait till user selects one
+ if (pDlg->GetSelectedItem() < 0)
+ { // none chosen
+ if (!pDlg->IsButtonPressed())
+ return INFO_CANCELLED;
+
+ // manual button pressed
+ std::string strNewAlbum = album.strAlbum;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strNewAlbum, CVariant{g_localizeStrings.Get(16011)}, false))
+ return INFO_CANCELLED;
+ if (strNewAlbum == "")
+ return INFO_CANCELLED;
+
+ std::string strNewArtist = album.GetAlbumArtistString();
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist, CVariant{g_localizeStrings.Get(16025)}, false))
+ return INFO_CANCELLED;
+
+ pDialog->SetLine(0, CVariant{strNewAlbum});
+ pDialog->SetLine(1, CVariant{strNewArtist});
+ pDialog->Progress();
+
+ CAlbum newAlbum = album;
+ newAlbum.strAlbum = strNewAlbum;
+ newAlbum.strArtistDesc = strNewArtist;
+
+ return DownloadAlbumInfo(newAlbum, info, albumInfo, bUseScrapedMBID, pDialog);
+ }
+ iSelectedAlbum = pDlg->GetSelectedItem();
+ }
+ }
+ else
+ {
+ CMusicAlbumInfo& info = scraper.GetAlbum(0);
+ double relevance = static_cast<double>(info.GetRelevance());
+ if (relevance < 0)
+ relevance = CUtil::AlbumRelevance(info.GetAlbum().strAlbum,
+ album.strAlbum,
+ info.GetAlbum().GetAlbumArtistString(),
+ album.GetAlbumArtistString());
+ if (relevance < static_cast<double>(THRESHOLD))
+ return INFO_NOT_FOUND;
+
+ iSelectedAlbum = 0;
+ }
+ }
+
+ if (iSelectedAlbum < 0)
+ return INFO_NOT_FOUND;
+
+ }
+
+ scraper.LoadAlbumInfo(iSelectedAlbum);
+ while (!scraper.Completed())
+ {
+ if (m_bStop)
+ {
+ scraper.Cancel();
+ return INFO_CANCELLED;
+ }
+ ScannerWait(1);
+ }
+ if (!scraper.Succeeded())
+ return INFO_ERROR;
+ /*
+ Fetching album details using xml scraper may makes requests for data from Musicbrainz.
+ MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
+ returns 503 errors for all calls from that IP address.
+ To stay below the rate-limit threshold wait 1s before proceeding incase next action is
+ to scrape another album or artist
+ */
+ if (!info->IsPython())
+ ScannerWait(1000);
+
+ albumInfo = scraper.GetAlbum(iSelectedAlbum);
+
+ if (result == CInfoScanner::COMBINED_NFO || result == CInfoScanner::OVERRIDE_NFO)
+ nfoReader.GetDetails(albumInfo.GetAlbum(), NULL, true);
+
+ return INFO_ADDED;
+}
+
+CInfoScanner::INFO_RET
+CMusicInfoScanner::DownloadArtistInfo(const CArtist& artist,
+ const ADDON::ScraperPtr& info,
+ MUSIC_GRABBER::CMusicArtistInfo& artistInfo,
+ bool bUseScrapedMBID,
+ CGUIDialogProgress* pDialog)
+{
+ if (m_handle)
+ {
+ m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20320), info->Name()));
+ m_handle->SetText(artist.strArtist);
+ }
+
+ // clear our scraper cache
+ info->ClearCache();
+
+ CMusicInfoScraper scraper(info);
+ bool bMusicBrainz = false;
+ /*
+ When the mbid is derived from tags scraping of artist information is done directly
+ using that ID, otherwise the lookup is based on name and can mis-identify the artist
+ (many have same name). To be able to correct any mistakes a manual refresh of artist
+ information uses either the mbid if derived from tags or the artist name, not any previously
+ scraped mbid.
+ */
+ if (!artist.strMusicBrainzArtistID.empty() && (!artist.bScrapedMBID || bUseScrapedMBID))
+ {
+ CScraperUrl musicBrainzURL;
+ if (ResolveMusicBrainz(artist.strMusicBrainzArtistID, info, musicBrainzURL))
+ {
+ CMusicArtistInfo artistNfo("nfo", musicBrainzURL);
+ scraper.GetArtists().clear();
+ scraper.GetArtists().push_back(artistNfo);
+ bMusicBrainz = true;
+ }
+ }
+
+ // Handle nfo files
+ CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
+ CNfoFile nfoReader;
+ std::string strNfo;
+ std::string path;
+ bool existsNFO = false;
+ // First look for nfo in the artists folder, the primary location
+ path = artist.strPath;
+ // Get path when don't already have it.
+ bool artistpathfound = !path.empty();
+ if (!artistpathfound)
+ artistpathfound = m_musicDatabase.GetArtistPath(artist, path);
+ if (artistpathfound)
+ {
+ strNfo = URIUtils::AddFileToFolder(path, "artist.nfo");
+ existsNFO = CFileUtils::Exists(strNfo);
+ }
+
+ // If not there fall back local to music files (historic location for those album artists with a unique folder)
+ if (!existsNFO)
+ {
+ artistpathfound = m_musicDatabase.GetOldArtistPath(artist.idArtist, path);
+ if (artistpathfound)
+ {
+ strNfo = URIUtils::AddFileToFolder(path, "artist.nfo");
+ existsNFO = CFileUtils::Exists(strNfo);
+ }
+ else
+ CLog::Log(LOGDEBUG, "{} not have path, nfo file not possible", artist.strArtist);
+ }
+
+ // When on GUI ask user if they want to ignore nfo and refresh from Internet
+ if (existsNFO && pDialog && CGUIDialogYesNo::ShowAndGetInput(21891, 20446))
+ {
+ existsNFO = false;
+ CLog::Log(LOGDEBUG, "Ignoring nfo file: {}", CURL::GetRedacted(strNfo));
+ }
+
+ if (existsNFO)
+ {
+ CLog::Log(LOGDEBUG, "Found matching nfo file: {}", CURL::GetRedacted(strNfo));
+ result = nfoReader.Create(strNfo, info);
+ if (result == CInfoScanner::FULL_NFO)
+ {
+ CLog::Log(LOGDEBUG, "{} Got details from nfo", __FUNCTION__);
+ nfoReader.GetDetails(artistInfo.GetArtist());
+ return INFO_ADDED;
+ }
+ else if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
+ {
+ CScraperUrl scrUrl(nfoReader.ScraperUrl());
+ CMusicArtistInfo artistNfo("nfo", scrUrl);
+ ADDON::ScraperPtr nfoReaderScraper = nfoReader.GetScraperInfo();
+ CLog::Log(LOGDEBUG, "-- nfo-scraper: {}", nfoReaderScraper->Name());
+ CLog::Log(LOGDEBUG, "-- nfo url: {}", scrUrl.GetFirstThumbUrl());
+ scraper.SetScraperInfo(nfoReaderScraper);
+ scraper.GetArtists().push_back(artistNfo);
+ }
+ else
+ CLog::Log(LOGERROR, "Unable to find an url in nfo file: {}", strNfo);
+ }
+
+ if (!scraper.GetArtistCount())
+ {
+ scraper.FindArtistInfo(artist.strArtist);
+
+ while (!scraper.Completed())
+ {
+ if (m_bStop)
+ {
+ scraper.Cancel();
+ return INFO_CANCELLED;
+ }
+ ScannerWait(1);
+ }
+ /*
+ Finding artist using xml scraper makes a request for data from Musicbrainz.
+ MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter
+ the server returns 503 errors for all calls from that IP address. To stay
+ below the rate-limit threshold wait 1s before proceeding
+ */
+ if (!info->IsPython())
+ ScannerWait(1000);
+ }
+
+ int iSelectedArtist = 0;
+ if (result == CInfoScanner::NO_NFO && !bMusicBrainz)
+ {
+ if (scraper.GetArtistCount() >= 1)
+ {
+ // now load the first match
+ if (pDialog && scraper.GetArtistCount() > 1)
+ {
+ // if we found more then 1 album, let user choose one
+ CGUIDialogSelect *pDlg = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (pDlg)
+ {
+ pDlg->SetHeading(CVariant{g_localizeStrings.Get(21890)});
+ pDlg->Reset();
+ pDlg->EnableButton(true, 413); // manual
+
+ for (int i = 0; i < scraper.GetArtistCount(); ++i)
+ {
+ // set the label to artist
+ CFileItem item(scraper.GetArtist(i).GetArtist());
+ std::string strTemp = scraper.GetArtist(i).GetArtist().strArtist;
+ if (!scraper.GetArtist(i).GetArtist().strBorn.empty())
+ strTemp += " ("+scraper.GetArtist(i).GetArtist().strBorn+")";
+ if (!scraper.GetArtist(i).GetArtist().strDisambiguation.empty())
+ strTemp += " - " + scraper.GetArtist(i).GetArtist().strDisambiguation;
+ if (!scraper.GetArtist(i).GetArtist().genre.empty())
+ {
+ std::string genres = StringUtils::Join(scraper.GetArtist(i).GetArtist().genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ if (!genres.empty())
+ strTemp = StringUtils::Format("[{}] {}", genres, strTemp);
+ }
+ item.SetLabel(strTemp);
+ item.m_idepth = i; // use this to hold the index of the album in the scraper
+ pDlg->Add(item);
+ }
+ pDlg->Open();
+
+ // and wait till user selects one
+ if (pDlg->GetSelectedItem() < 0)
+ { // none chosen
+ if (!pDlg->IsButtonPressed())
+ return INFO_CANCELLED;
+
+ // manual button pressed
+ std::string strNewArtist = artist.strArtist;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strNewArtist, CVariant{g_localizeStrings.Get(16025)}, false))
+ return INFO_CANCELLED;
+
+ if (pDialog)
+ {
+ pDialog->SetLine(0, CVariant{strNewArtist});
+ pDialog->Progress();
+ }
+
+ CArtist newArtist;
+ newArtist.strArtist = strNewArtist;
+ return DownloadArtistInfo(newArtist, info, artistInfo, bUseScrapedMBID, pDialog);
+ }
+ iSelectedArtist = pDlg->GetSelectedFileItem()->m_idepth;
+ }
+ }
+ }
+ else
+ return INFO_NOT_FOUND;
+ }
+ /*
+ Fetching artist details using xml scraper makes requests for data from Musicbrainz.
+ MusicBrainz rate-limits queries to 1 per sec, once we hit the rate-limiter the server
+ returns 503 errors for all calls from that IP address.
+ To stay below the rate-limit threshold wait 1s before proceeding incase next action is
+ to scrape another album or artist
+ */
+ if (!info->IsPython())
+ ScannerWait(1000);
+
+ scraper.LoadArtistInfo(iSelectedArtist, artist.strArtist);
+ while (!scraper.Completed())
+ {
+ if (m_bStop)
+ {
+ scraper.Cancel();
+ return INFO_CANCELLED;
+ }
+ ScannerWait(1);
+ }
+
+ if (!scraper.Succeeded())
+ return INFO_ERROR;
+
+ artistInfo = scraper.GetArtist(iSelectedArtist);
+
+ if (result == CInfoScanner::COMBINED_NFO)
+ nfoReader.GetDetails(artistInfo.GetArtist(), NULL, true);
+
+ return INFO_ADDED;
+}
+
+bool CMusicInfoScanner::ResolveMusicBrainz(const std::string &strMusicBrainzID, const ScraperPtr &preferredScraper, CScraperUrl &musicBrainzURL)
+{
+ // We have a MusicBrainz ID
+ // Get a scraper that can resolve it to a MusicBrainz URL & force our
+ // search directly to the specific album.
+ bool bMusicBrainz = false;
+ try
+ {
+ musicBrainzURL = preferredScraper->ResolveIDToUrl(strMusicBrainzID);
+ }
+ catch (const ADDON::CScraperError &sce)
+ {
+ if (sce.FAborted())
+ return false;
+ }
+
+ if (musicBrainzURL.HasUrls())
+ {
+ CLog::Log(LOGDEBUG, "-- nfo-scraper: {}", preferredScraper->Name());
+ CLog::Log(LOGDEBUG, "-- nfo url: {}", musicBrainzURL.GetFirstThumbUrl());
+ bMusicBrainz = true;
+ }
+
+ return bMusicBrainz;
+}
+
+void CMusicInfoScanner::ScannerWait(unsigned int milliseconds)
+{
+ if (milliseconds > 10)
+ {
+ CEvent m_StopEvent;
+ m_StopEvent.Wait(std::chrono::milliseconds(milliseconds));
+ }
+ else
+ std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
+}
+
+bool CMusicInfoScanner::AddArtistArtwork(CArtist& artist, const std::string& artfolder)
+{
+ if (!artist.thumbURL.HasUrls() && artfolder.empty())
+ return false; // No local or scraped possible art to process
+
+ if (artist.art.empty())
+ m_musicDatabase.GetArtForItem(artist.idArtist, MediaTypeArtist, artist.art);
+
+ std::map<std::string, std::string> addedart;
+ std::string strArt;
+
+ // Handle thumb separately, can be from multiple confgurable file names
+ if (artist.art.find("thumb") == artist.art.end())
+ {
+ if (!artfolder.empty())
+ { // Local music thumbnail images named by "musiclibrary.musicthumbs"
+ CFileItem item(artfolder, true);
+ strArt = item.GetUserMusicThumb(true);
+ }
+ if (strArt.empty())
+ strArt = CScraperUrl::GetThumbUrl(artist.thumbURL.GetFirstUrlByType("thumb"));
+ if (!strArt.empty())
+ addedart.insert(std::make_pair("thumb", strArt));
+ }
+
+ // Process additional art types in artist folder
+ AddLocalArtwork(addedart, MediaTypeArtist, artist.strArtist, artfolder);
+
+ // Process remote artist art filling gaps with first of scraped art URLs
+ AddRemoteArtwork(addedart, MediaTypeArtist, artist.thumbURL);
+
+ int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
+
+ for (const auto& it : addedart)
+ {
+ // Cache thumb, fanart and other whitelisted artwork immediately
+ // (other art types will be cached when first displayed)
+ if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL || it.first == "thumb" ||
+ it.first == "fanart")
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(it.second);
+ auto ret = artist.art.insert(it);
+ if (ret.second)
+ m_musicDatabase.SetArtForItem(artist.idArtist, MediaTypeArtist, it.first, it.second);
+ }
+ return addedart.size() > 0;
+}
+
+bool CMusicInfoScanner::AddAlbumArtwork(CAlbum& album)
+{
+ // Fetch album path and any subfolders (disc sets).
+ // No paths found when songs from different albums are in one folder
+ std::vector<std::pair<std::string, int>> paths;
+ m_musicDatabase.GetAlbumPaths(album.idAlbum, paths);
+ for (const auto& pathpair : paths)
+ {
+ if (album.strPath.empty())
+ album.strPath = pathpair.first.c_str();
+ else
+ // When more than one album path is the common path
+ URIUtils::GetCommonPath(album.strPath, pathpair.first.c_str());
+ }
+
+ if (!album.thumbURL.HasUrls() && album.strPath.empty())
+ return false; // No local or scraped possible art to process
+
+ if (album.art.empty())
+ m_musicDatabase.GetArtForItem(album.idAlbum, MediaTypeAlbum, album.art);
+ auto thumb = album.art.find("thumb"); // Find "thumb", may want to replace it
+
+ bool replaceThumb = paths.size() > 1;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_PREFERONLINEALBUMART))
+ {
+ // When "prefer online album art" enabled and we have a thumb as embedded art
+ // then replace it if we find a scraped cover
+ if (thumb != album.art.end() && StringUtils::StartsWith(thumb->second, "image://"))
+ replaceThumb = true;
+ }
+
+ std::map<std::string, std::string> addedart;
+ std::string strArt;
+
+ // Fetch local art from album folder
+ // Handle thumbs separately, can be from multiple confgurable file names
+ if (replaceThumb || thumb == album.art.end())
+ {
+ if (!album.strPath.empty())
+ { // Local music thumbnail images named by "musiclibrary.musicthumbs"
+ CFileItem item(album.strPath, true);
+ strArt = item.GetUserMusicThumb(true);
+ }
+ if (strArt.empty())
+ strArt = CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType("thumb"));
+ if (!strArt.empty())
+ {
+ if (thumb != album.art.end())
+ album.art.erase(thumb);
+ addedart.insert(std::make_pair("thumb", strArt));
+ }
+ }
+ // Process additional art types in album folder
+ AddLocalArtwork(addedart, MediaTypeAlbum, album.strAlbum, album.strPath);
+
+ // Fetch local art from disc subfolders
+ if (paths.size() > 1)
+ {
+ CMusicThumbLoader loader;
+ std::string firstDiscThumb;
+ int iDiscThumb = 10000;
+ for (const auto& pathpair : paths)
+ {
+ strArt.clear();
+
+ int discnum = m_musicDatabase.GetDiscnumberForPathID(pathpair.second);
+ if (discnum > 0)
+ {
+ // Handle thumbs separately. Get thumb for path from textures db cached during scan
+ // (could be embedded or local file from multiple confgurable file names)
+ CFileItem item(pathpair.first.c_str(), true);
+ std::string strArtType = StringUtils::Format("{}{}", "thumb", discnum);
+ strArt = loader.GetCachedImage(item, "thumb");
+ if (strArt.empty())
+ strArt = CScraperUrl::GetThumbUrl(album.thumbURL.GetFirstUrlByType(strArtType));
+ if (!strArt.empty())
+ {
+ addedart.insert(std::make_pair(strArtType, strArt));
+ // Store thumb of first disc with a thumb
+ if (discnum < iDiscThumb)
+ {
+ iDiscThumb = discnum;
+ firstDiscThumb = strArt;
+ }
+ }
+ }
+ // Process additional art types in disc subfolder
+ AddLocalArtwork(addedart, MediaTypeAlbum, album.strAlbum, pathpair.first, discnum);
+ }
+ // Finally if we still don't have album thumb then use the art from the
+ // first disc in the set with a thumb
+ if (!firstDiscThumb.empty() && album.art.find("thumb") == album.art.end())
+ {
+ m_musicDatabase.SetArtForItem(album.idAlbum, MediaTypeAlbum, "thumb", firstDiscThumb);
+ // Assign art as folder thumb (in textures db) as well
+
+ CFileItem albumItem(album.strPath, true);
+ loader.SetCachedImage(albumItem, "thumb", firstDiscThumb);
+ }
+ }
+
+ // Process remote album art filling gaps with first of scraped art URLs
+ AddRemoteArtwork(addedart, MediaTypeAlbum, album.thumbURL);
+
+ int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
+ for (const auto& it : addedart)
+ {
+ // Cache thumb, fanart and whitelisted artwork immediately
+ // (other art types will be cached when first displayed)
+ if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL || it.first == "thumb" ||
+ it.first == "fanart")
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(it.second);
+
+ auto ret = album.art.insert(it);
+ if (ret.second)
+ m_musicDatabase.SetArtForItem(album.idAlbum, MediaTypeAlbum, it.first, it.second);
+ }
+ return addedart.size() > 0;
+}
+
+std::vector<CVariant> CMusicInfoScanner::GetArtWhitelist(const MediaType& mediaType, int iArtLevel)
+{
+ std::vector<CVariant> whitelistarttypes;
+ if (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_BASIC)
+ {
+ // Basic artist artwork = thumb + fanart (but not "family" fanart1, fanart2 etc.)
+ // Basic album artwork = thumb only, thumb handled separately not in whitelist
+ if (mediaType == MediaTypeArtist)
+ whitelistarttypes.emplace_back("fanart");
+ }
+ else
+ {
+ if (mediaType == MediaTypeArtist)
+ whitelistarttypes = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_MUSICLIBRARY_ARTISTART_WHITELIST);
+ else
+ whitelistarttypes = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_MUSICLIBRARY_ALBUMART_WHITELIST);
+ }
+
+ return whitelistarttypes;
+}
+
+bool CMusicInfoScanner::AddLocalArtwork(std::map<std::string, std::string>& art,
+ const std::string& mediaType,
+ const std::string& mediaName,
+ const std::string& artfolder,
+ int discnum)
+{
+ if (artfolder.empty())
+ return false;
+
+ int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
+
+ std::vector<CVariant> whitelistarttypes = GetArtWhitelist(mediaType, iArtLevel);
+ bool bUseAll = (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL) ||
+ ((iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM) &&
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEALLLOCALART));
+
+ // Not useall and empty whitelist means no extra art is picked up from either place
+ if (!bUseAll && whitelistarttypes.empty())
+ return false;
+
+ // Image files used as thumbs
+ std::vector<CVariant> thumbs = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_MUSICLIBRARY_MUSICTHUMBS);
+
+ // Find local art
+ CFileItemList availableArtFiles;
+ CDirectory::GetDirectory(artfolder, availableArtFiles,
+ CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
+ DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
+
+ for (const auto& artFile : availableArtFiles)
+ {
+ if (artFile->m_bIsFolder)
+ continue;
+ std::string strCandidate = URIUtils::GetFileName(artFile->GetPath());
+ // Strip media name
+ if (!mediaName.empty() && StringUtils::StartsWith(strCandidate, mediaName))
+ strCandidate.erase(0, mediaName.length());
+ StringUtils::ToLower(strCandidate);
+ // Skip files already used as "thumb"
+ // Typically folder.jpg but can be from multiple confgurable file names
+ if (std::find(thumbs.begin(), thumbs.end(), strCandidate) != thumbs.end())
+ continue;
+ // Grab and strip file extension
+ std::string strExt;
+ size_t period = strCandidate.find_last_of("./\\");
+ if (period != std::string::npos && strCandidate[period] == '.')
+ {
+ strExt = strCandidate.substr(period); // ".jpg", ".png" etc.
+ strCandidate.erase(period); // "abc16" for file Abc16.jpg
+ }
+ if (strCandidate.empty())
+ continue;
+ // Validate art type name
+ size_t last_index = strCandidate.find_last_not_of("0123456789");
+ std::string strDigits = strCandidate.substr(last_index + 1);
+ std::string strFamily = strCandidate.substr(0, last_index + 1); // "abc" of "abc16"
+ if (strFamily.empty())
+ continue;
+ if (!MUSIC_UTILS::IsValidArtType(strCandidate))
+ continue;
+ // Disc specific art from disc subfolder
+ // Skip art where digits of filename do not match disc number
+ if (discnum > 0 && !strDigits.empty() && (atoi(strDigits.c_str()) != discnum))
+ continue;
+
+ // Use all art, or check for basic level art in whitelist exactly allowing for disc number,
+ // or for custom art check whitelist contains art type family (strip trailing digits)
+ // e.g. 'fanart', 'fanart1', 'fanart2' etc. all match whitelist entry 'fanart'
+ std::string strCheck = strCandidate;
+ if (discnum > 0 || iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM)
+ strCheck = strFamily;
+ if (bUseAll || std::find(whitelistarttypes.begin(), whitelistarttypes.end(), strCheck) !=
+ whitelistarttypes.end())
+ {
+ if (!strDigits.empty())
+ {
+ // Catch any variants of music thumbs e.g. folder2.jpg as "thumb2"
+ // Used for disc sets when files all in one album folder
+ if (std::find(thumbs.begin(), thumbs.end(), strFamily + strExt) != thumbs.end())
+ strCandidate = "thumb" + strDigits;
+ }
+ else if (discnum > 0)
+ // Append disc number when candidate art type (and file) not have it
+ strCandidate += std::to_string(discnum);
+
+ if (art.find(strCandidate) == art.end())
+ art.insert(std::make_pair(strCandidate, artFile->GetPath()));
+ }
+ }
+
+ return art.size() > 0;
+}
+
+bool CMusicInfoScanner::AddRemoteArtwork(std::map<std::string, std::string>& art,
+ const std::string& mediaType,
+ const CScraperUrl& thumbURL)
+{
+ int iArtLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL);
+
+ std::vector<CVariant> whitelistarttypes = GetArtWhitelist(mediaType, iArtLevel);
+ bool bUseAll = (iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_ALL) ||
+ ((iArtLevel == CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM) &&
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEALLREMOTEART));
+
+ // not useall and empty whitelist means no extra art is picked up from either place
+ if (!bUseAll && whitelistarttypes.empty())
+ return false;
+
+ // Add online art
+ // Done for artists and albums, so not need repeating at disc level
+ for (const auto& url : thumbURL.GetUrls())
+ {
+ // Art type is encoded into the scraper XML held in thumbURL as optional "aspect=" field.
+ // Those URL without aspect are also returned for all other type values.
+ // Loop through all the first URLS of each type except "thumb" and add if art missing
+ if (url.m_aspect.empty() || url.m_aspect == "thumb")
+ continue;
+ if (!bUseAll)
+ { // Check whitelist for art type family e.g. "discart" for aspect="discart2"
+ std::string strName = url.m_aspect;
+ if (iArtLevel != CSettings::MUSICLIBRARY_ARTWORK_LEVEL_BASIC)
+ StringUtils::TrimRight(strName, "0123456789");
+ if (std::find(whitelistarttypes.begin(), whitelistarttypes.end(), strName) ==
+ whitelistarttypes.end())
+ continue;
+ }
+ if (art.find(url.m_aspect) == art.end())
+ {
+ std::string strArt = CScraperUrl::GetThumbUrl(url);
+ if (!strArt.empty())
+ art.insert(std::make_pair(url.m_aspect, strArt));
+ }
+ }
+
+ return art.size() > 0;
+}
+
+// This function is the Run() function of the IRunnable
+// CFileCountReader and runs in a separate thread.
+void CMusicInfoScanner::Run()
+{
+ int count = 0;
+ for (auto& it : m_pathsToScan)
+ {
+ count += CountFilesRecursively(it);
+ }
+ m_itemCount = count;
+}
+
+// Recurse through all folders we scan and count files
+int CMusicInfoScanner::CountFilesRecursively(const std::string& strPath)
+{
+ // load subfolder
+ CFileItemList items;
+ CDirectory::GetDirectory(strPath, items, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions(), DIR_FLAG_NO_FILE_DIRS);
+
+ if (m_bStop)
+ return 0;
+
+ // true for recursive counting
+ int count = CountFiles(items, true);
+ return count;
+}
+
+int CMusicInfoScanner::CountFiles(const CFileItemList &items, bool recursive)
+{
+ int count = 0;
+ for (int i=0; i<items.Size(); ++i)
+ {
+ const CFileItemPtr pItem=items[i];
+
+ if (recursive && pItem->m_bIsFolder)
+ count+=CountFilesRecursively(pItem->GetPath());
+ else if (pItem->IsAudio() && !pItem->IsPlayList() && !pItem->IsNFO())
+ count++;
+ }
+ return count;
+}
diff --git a/xbmc/music/infoscanner/MusicInfoScanner.h b/xbmc/music/infoscanner/MusicInfoScanner.h
new file mode 100644
index 0000000..d5d5470
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicInfoScanner.h
@@ -0,0 +1,265 @@
+/*
+ * 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 "InfoScanner.h"
+#include "MusicAlbumInfo.h"
+#include "MusicInfoScraper.h"
+#include "music/MusicDatabase.h"
+#include "threads/IRunnable.h"
+#include "threads/Thread.h"
+#include "utils/ScraperUrl.h"
+
+class CAlbum;
+class CArtist;
+class CGUIDialogProgressBarHandle;
+
+namespace MUSIC_INFO
+{
+
+class CMusicInfoScanner : public IRunnable, public CInfoScanner
+{
+public:
+ /*! \brief Flags for controlling the scanning process
+ */
+ enum SCAN_FLAGS { SCAN_NORMAL = 0,
+ SCAN_ONLINE = 1 << 0,
+ SCAN_BACKGROUND = 1 << 1,
+ SCAN_RESCAN = 1 << 2,
+ SCAN_ARTISTS = 1 << 3,
+ SCAN_ALBUMS = 1 << 4 };
+
+ CMusicInfoScanner();
+ ~CMusicInfoScanner() override;
+
+ void Start(const std::string& strDirectory, int flags);
+ void FetchAlbumInfo(const std::string& strDirectory, bool refresh = false);
+ void FetchArtistInfo(const std::string& strDirectory, bool refresh = false);
+ void Stop();
+
+ /*! \brief Categorize FileItems into Albums, Songs, and Artists
+ This takes a list of FileItems and turns it into a tree of Albums,
+ Artists, and Songs.
+ Albums are defined uniquely by the album name and album artist.
+
+ \param songs [in/out] list of songs to categorise - albumartist field may be altered.
+ \param albums [out] albums found within these songs.
+ */
+ static void FileItemsToAlbums(CFileItemList& items, VECALBUMS& albums, MAPSONGS* songsMap = NULL);
+
+ /*! \brief Scrape additional album information and update the music database with it.
+ Given an album, search for it using the given scraper.
+ If info is found, update the database and artwork with the new
+ information.
+ \param album [in/out] the album to update
+ \param scraper [in] the album scraper to use
+ \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET UpdateAlbumInfo(CAlbum& album, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL);
+
+ /*! \brief Scrape additional artist information and update the music database with it.
+ Given an artist, search for it using the given scraper.
+ If info is found, update the database and artwork with the new
+ information.
+ \param artist [in/out] the artist to update
+ \param scraper [in] the artist scraper to use
+ \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET UpdateArtistInfo(CArtist& artist, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL);
+
+protected:
+ virtual void Process();
+ bool DoScan(const std::string& strDirectory) override;
+
+
+ /*! \brief Find art for albums
+ Based on the albums in the folder, finds whether we have unique album art
+ and assigns to the album if we do.
+
+ In order of priority:
+ 1. If there is a single album in the folder, then the folder art is assigned to the album.
+ 2. We find the art for each song. A .tbn file takes priority over embedded art.
+ 3. If we have a unique piece of art for all songs in the album, we assign that to the album
+ and remove that art from each song so that they inherit from the album.
+ 4. If there is not a unique piece of art for each song, then no art is assigned
+ to the album.
+
+ \param albums [in/out] list of albums to categorise - art field may be altered.
+ \param path [in] path containing albums.
+ */
+ static void FindArtForAlbums(VECALBUMS &albums, const std::string &path);
+
+ /*! \brief Scrape additional album information and update the database.
+ Search for the given album using the given scraper.
+ If info is found, update the database and artwork with the new
+ information.
+ \param album [in/out] the album to update
+ \param scraper [in] the album scraper to use
+ \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET UpdateDatabaseAlbumInfo(CAlbum& album, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL);
+
+ /*! \brief Scrape additional artist information and update the database.
+ Search for the given artist using the given scraper.
+ If info is found, update the database and artwork with the new
+ information.
+ \param artist [in/out] the artist to update
+ \param scraper [in] the artist scraper to use
+ \param bAllowSelection [in] should we allow the user to manually override the info with a GUI if the album is not found?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET UpdateDatabaseArtistInfo(CArtist& artist, const ADDON::ScraperPtr& scraper, bool bAllowSelection, CGUIDialogProgress* pDialog = NULL);
+
+ /*! \brief Using the scrapers download metadata for an album
+ Given a CAlbum style struct containing some data about an album, query
+ the scrapers to try and get more information about the album. The responsibility
+ is with the caller to do something with that information. It will be passed back
+ in a MusicInfo struct, which you can save, display to the user or throw away.
+ \param album [in] a partially or fully filled out album structure containing the search query
+ \param scraper [in] the scraper to query, usually the default or the relevant scraper for the musicdb path
+ \param albumInfo [in/out] a CMusicAlbumInfo struct which will be populated with the output of the scraper
+ \param bUseScrapedMBID [in] should scraper use any previously scraped mbid to identify the artist, or use artist name?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET DownloadAlbumInfo(const CAlbum& album, const ADDON::ScraperPtr& scraper, MUSIC_GRABBER::CMusicAlbumInfo& albumInfo, bool bUseScrapedMBID, CGUIDialogProgress* pDialog = NULL);
+
+ /*! \brief Using the scrapers download metadata for an artist
+ Given a CAlbum style struct containing some data about an artist, query
+ the scrapers to try and get more information about the artist. The responsibility
+ is with the caller to do something with that information. It will be passed back
+ in a MusicInfo struct, which you can save, display to the user or throw away.
+ \param artist [in] a partially or fully filled out artist structure containing the search query
+ \param scraper [in] the scraper to query, usually the default or the relevant scraper for the musicdb path
+ \param artistInfo [in/out] a CMusicAlbumInfo struct which will be populated with the output of the scraper
+ \param bUseScrapedMBID [in] should scraper use any previously scraped mbid to identify the album, or use album and artist name?
+ \param pDialog [in] a progress dialog which this and downstream functions can update with status, if required
+ */
+ INFO_RET DownloadArtistInfo(const CArtist& artist, const ADDON::ScraperPtr& scraper, MUSIC_GRABBER::CMusicArtistInfo& artistInfo, bool bUseScrapedMBID, CGUIDialogProgress* pDialog = NULL);
+
+ /*! \brief Get the types of art for an artist or album that are to be
+ automatically fetched from local files during scanning
+ \param mediaType [in] artist or album
+ \param iArtLevel [in] art level
+ \return vector of art types that are to be fetched during scanning
+ */
+ std::vector<CVariant> GetArtWhitelist(const MediaType& mediaType, int iArtLevel);
+
+ /*! \brief Add extra local artwork for albums and artists
+ This common utility scans the given folder for local (non-thumb) art.
+ The art types checked are determined by whitelist and usealllocalart settings.
+ \param art [in/out] map of art type and file location (URL or path) pairs
+ \param mediaType [in] artist or album
+ \param mediaName [in] artist or album name that may be stripped from image file names
+ \param artfolder [in] path of folder containing local image files
+ \return true when art is added
+ */
+ bool AddLocalArtwork(std::map<std::string, std::string>& art,
+ const std::string& mediaType,
+ const std::string& mediaName,
+ const std::string& artfolder,
+ int discnum = 0);
+
+ /*! \brief Add extra remote artwork for albums and artists
+ This common utility fills the gaps in artwork using the first available art of each type from the
+ possible art URL results of previous scraping.
+ The art types applied are determined by whitelist and usealllocalart settings.
+ \param art [in/out] map of art type and file location (URL or path) pairs
+ \param mediaType [in] artist or album
+ \param thumbURL [in] URLs for potential remote artwork (provided by scrapers)
+ \return true when art is added
+ */
+ bool AddRemoteArtwork(std::map<std::string, std::string>& art,
+ const std::string& mediaType,
+ const CScraperUrl& thumbURL);
+
+ /*! \brief Add art for an artist
+ This scans the given folder for local art and/or applies the first available art of each type
+ from the possible art URLs previously scraped. Art is added to any already stored by previous
+ scanning etc.The art types processed are determined by whitelist and other art settings.
+ When usealllocalart is enabled then all local image files are applied as art (providing name is
+ valid for an art type), and then the URL list of remote art is checked adding the first available
+ image of each art type not yet in the art map.
+ Art found is saved in the album structure and the music database. The images found are cached.
+ \param artist [in/out] an artist, the art is set
+ \param artfolder [in] path of the location to search for local art files
+ \return true when art is added
+ */
+ bool AddArtistArtwork(CArtist& artist, const std::string& artfolder);
+
+ /*! \brief Add art for an album
+ This scans the album folder, and any disc set subfolders, for local art and/or applies the first
+ available art of each type from the possible art URLs previously scraped. Only those subfolders
+ beneath the album folder containing music files tagged with same unique disc number are scanned.
+ Art is added to any already stored by previous scanning, only "thumb" is optionally replaced.
+ The art types processed are determined by whitelist and other art settings. When usealllocalart is
+ enabled then all local image files are applied as art (providing name is valid for an art type),
+ and then the URL list of remote art is checked adding the first available image of each art type
+ not yet in the art map.
+ Art found is saved in the album structure and the music database. The images found are cached.
+ \param artist [in/out] an album, the art is set
+ \return true when art is added
+ */
+ bool AddAlbumArtwork(CAlbum& album);
+
+ /*! \brief Scan in the ID3/Ogg/FLAC tags for a bunch of FileItems
+ Given a list of FileItems, scan in the tags for those FileItems
+ and populate a new FileItemList with the files that were successfully scanned.
+ Add album to library, populate a list of album ids added for possible scraping later.
+ Any files which couldn't be scanned (no/bad tags) are discarded in the process.
+ \param items [in] list of FileItems to scan
+ \param scannedItems [in] list to populate with the scannedItems
+ */
+ int RetrieveMusicInfo(const std::string& strDirectory, CFileItemList& items);
+
+ void RetrieveLocalArt();
+ void ScrapeInfoAddedAlbums();
+
+ /*! \brief Scan in the ID3/Ogg/FLAC tags for a bunch of FileItems
+ Given a list of FileItems, scan in the tags for those FileItems
+ and populate a new FileItemList with the files that were successfully scanned.
+ Any files which couldn't be scanned (no/bad tags) are discarded in the process.
+ \param items [in] list of FileItems to scan
+ \param scannedItems [in] list to populate with the scannedItems
+ */
+ INFO_RET ScanTags(const CFileItemList& items, CFileItemList& scannedItems);
+ int GetPathHash(const CFileItemList &items, std::string &hash);
+
+ void Run() override;
+ int CountFiles(const CFileItemList& items, bool recursive);
+ int CountFilesRecursively(const std::string& strPath);
+
+ /*! \brief Resolve a MusicBrainzID to a URL
+ If we have a MusicBrainz ID for an artist or album,
+ resolve it to an MB URL and set up the scrapers accordingly.
+
+ \param preferredScraper [in] A ScraperPtr to the preferred album/artist scraper.
+ \param musicBrainzURL [out] will be populated with the MB URL for the artist/album.
+ */
+ bool ResolveMusicBrainz(const std::string &strMusicBrainzID, const ADDON::ScraperPtr &preferredScraper, CScraperUrl &musicBrainzURL);
+
+ void ScannerWait(unsigned int milliseconds);
+
+ int m_currentItem;
+ int m_itemCount;
+ bool m_bStop;
+ bool m_needsCleanup = false;
+ int m_scanType = 0; // 0 - load from files, 1 - albums, 2 - artists
+ int m_idSourcePath;
+ CMusicDatabase m_musicDatabase;
+
+ std::set<int> m_albumsAdded;
+
+ std::set<std::string> m_seenPaths;
+ int m_flags;
+ CThread m_fileCountReader;
+};
+}
diff --git a/xbmc/music/infoscanner/MusicInfoScraper.cpp b/xbmc/music/infoscanner/MusicInfoScraper.cpp
new file mode 100644
index 0000000..630c2a7
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicInfoScraper.cpp
@@ -0,0 +1,202 @@
+/*
+ * 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 "MusicInfoScraper.h"
+
+#include "filesystem/CurlFile.h"
+#include "utils/log.h"
+
+using namespace MUSIC_GRABBER;
+using namespace ADDON;
+using namespace std::chrono_literals;
+
+CMusicInfoScraper::CMusicInfoScraper(const ADDON::ScraperPtr& scraper)
+ : CThread("MusicInfoScraper"), m_scraper(scraper)
+{
+ m_bSucceeded=false;
+ m_bCanceled=false;
+ m_iAlbum=-1;
+ m_iArtist=-1;
+ m_http = new XFILE::CCurlFile;
+}
+
+CMusicInfoScraper::~CMusicInfoScraper(void)
+{
+ StopThread();
+ delete m_http;
+}
+
+int CMusicInfoScraper::GetAlbumCount() const
+{
+ return (int)m_vecAlbums.size();
+}
+
+int CMusicInfoScraper::GetArtistCount() const
+{
+ return (int)m_vecArtists.size();
+}
+
+CMusicAlbumInfo& CMusicInfoScraper::GetAlbum(int iAlbum)
+{
+ return m_vecAlbums[iAlbum];
+}
+
+CMusicArtistInfo& CMusicInfoScraper::GetArtist(int iArtist)
+{
+ return m_vecArtists[iArtist];
+}
+
+void CMusicInfoScraper::FindAlbumInfo(const std::string& strAlbum, const std::string& strArtist /* = "" */)
+{
+ m_strAlbum=strAlbum;
+ m_strArtist=strArtist;
+ m_bSucceeded=false;
+ StopThread();
+ Create();
+}
+
+void CMusicInfoScraper::FindArtistInfo(const std::string& strArtist)
+{
+ m_strArtist=strArtist;
+ m_bSucceeded=false;
+ StopThread();
+ Create();
+}
+
+void CMusicInfoScraper::FindAlbumInfo()
+{
+ m_vecAlbums = m_scraper->FindAlbum(*m_http, m_strAlbum, m_strArtist);
+ m_bSucceeded = !m_vecAlbums.empty();
+}
+
+void CMusicInfoScraper::FindArtistInfo()
+{
+ m_vecArtists = m_scraper->FindArtist(*m_http, m_strArtist);
+ m_bSucceeded = !m_vecArtists.empty();
+}
+
+void CMusicInfoScraper::LoadAlbumInfo(int iAlbum)
+{
+ m_iAlbum=iAlbum;
+ m_iArtist=-1;
+ StopThread();
+ Create();
+}
+
+void CMusicInfoScraper::LoadArtistInfo(int iArtist, const std::string &strSearch)
+{
+ m_iAlbum=-1;
+ m_iArtist=iArtist;
+ m_strSearch=strSearch;
+ StopThread();
+ Create();
+}
+
+void CMusicInfoScraper::LoadAlbumInfo()
+{
+ if (m_iAlbum<0 || m_iAlbum>=(int)m_vecAlbums.size())
+ return;
+
+ CMusicAlbumInfo& album=m_vecAlbums[m_iAlbum];
+ // Clear album artist credits
+ album.GetAlbum().artistCredits.clear();
+ if (album.Load(*m_http,m_scraper))
+ m_bSucceeded=true;
+}
+
+void CMusicInfoScraper::LoadArtistInfo()
+{
+ if (m_iArtist<0 || m_iArtist>=(int)m_vecArtists.size())
+ return;
+
+ CMusicArtistInfo& artist=m_vecArtists[m_iArtist];
+ artist.GetArtist().strArtist.clear();
+ if (artist.Load(*m_http,m_scraper,m_strSearch))
+ m_bSucceeded=true;
+}
+
+bool CMusicInfoScraper::Completed()
+{
+ return Join(10ms);
+}
+
+bool CMusicInfoScraper::Succeeded()
+{
+ return !m_bCanceled && m_bSucceeded;
+}
+
+void CMusicInfoScraper::Cancel()
+{
+ m_http->Cancel();
+ m_bCanceled=true;
+ m_http->Reset();
+}
+
+bool CMusicInfoScraper::IsCanceled()
+{
+ return m_bCanceled;
+}
+
+void CMusicInfoScraper::OnStartup()
+{
+ m_bSucceeded=false;
+ m_bCanceled=false;
+}
+
+void CMusicInfoScraper::Process()
+{
+ try
+ {
+ if (m_strAlbum.size())
+ {
+ FindAlbumInfo();
+ m_strAlbum.clear();
+ m_strArtist.clear();
+ }
+ else if (m_strArtist.size())
+ {
+ FindArtistInfo();
+ m_strArtist.clear();
+ }
+ if (m_iAlbum>-1)
+ {
+ LoadAlbumInfo();
+ m_iAlbum=-1;
+ }
+ if (m_iArtist>-1)
+ {
+ LoadArtistInfo();
+ m_iArtist=-1;
+ }
+ }
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "Exception in CMusicInfoScraper::Process()");
+ }
+}
+
+bool CMusicInfoScraper::CheckValidOrFallback(const std::string &fallbackScraper)
+{
+ return true;
+//! @todo Handle fallback mechanism
+ /*
+ if (m_scraper->Path() != fallbackScraper &&
+ parser.Load("special://xbmc/system/scrapers/music/" + fallbackScraper))
+ {
+ CLog::Log(LOGWARNING, "{} - scraper {} fails to load, falling back to {}", __FUNCTION__, m_info.strPath, fallbackScraper);
+ m_info.strPath = fallbackScraper;
+ m_info.strContent = "albums";
+ m_info.strTitle = parser.GetName();
+ m_info.strDate = parser.GetDate();
+ m_info.strFramework = parser.GetFramework();
+ m_info.strLanguage = parser.GetLanguage();
+ m_info.settings.LoadSettingsXML("special://xbmc/system/scrapers/music/" + m_info.strPath);
+ return true;
+ }
+ return false; */
+}
diff --git a/xbmc/music/infoscanner/MusicInfoScraper.h b/xbmc/music/infoscanner/MusicInfoScraper.h
new file mode 100644
index 0000000..fcab576
--- /dev/null
+++ b/xbmc/music/infoscanner/MusicInfoScraper.h
@@ -0,0 +1,82 @@
+/*
+ * 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 "MusicAlbumInfo.h"
+#include "MusicArtistInfo.h"
+#include "addons/Scraper.h"
+#include "threads/Thread.h"
+
+#include <vector>
+
+namespace XFILE
+{
+class CurlFile;
+}
+
+namespace MUSIC_GRABBER
+{
+class CMusicInfoScraper : public CThread
+{
+public:
+ explicit CMusicInfoScraper(const ADDON::ScraperPtr &scraper);
+ ~CMusicInfoScraper(void) override;
+ void FindAlbumInfo(const std::string& strAlbum, const std::string& strArtist = "");
+ void LoadAlbumInfo(int iAlbum);
+ void FindArtistInfo(const std::string& strArtist);
+ void LoadArtistInfo(int iArtist, const std::string &strSearch);
+ bool Completed();
+ bool Succeeded();
+ void Cancel();
+ bool IsCanceled();
+ int GetAlbumCount() const;
+ int GetArtistCount() const;
+ CMusicAlbumInfo& GetAlbum(int iAlbum);
+ CMusicArtistInfo& GetArtist(int iArtist);
+ std::vector<CMusicArtistInfo>& GetArtists()
+ {
+ return m_vecArtists;
+ }
+ std::vector<CMusicAlbumInfo>& GetAlbums()
+ {
+ return m_vecAlbums;
+ }
+ void SetScraperInfo(const ADDON::ScraperPtr& scraper)
+ {
+ m_scraper = scraper;
+ }
+ /*!
+ \brief Checks whether we have a valid scraper. If not, we try the fallbackScraper
+ First tests the current scraper for validity by loading it. If it is not valid we
+ attempt to load the fallback scraper. If this is also invalid we return false.
+ \param fallbackScraper name of scraper to use as a fallback
+ \return true if we have a valid scraper (or the default is valid).
+ */
+ bool CheckValidOrFallback(const std::string &fallbackScraper);
+protected:
+ void FindAlbumInfo();
+ void LoadAlbumInfo();
+ void FindArtistInfo();
+ void LoadArtistInfo();
+ void OnStartup() override;
+ void Process() override;
+ std::vector<CMusicAlbumInfo> m_vecAlbums;
+ std::vector<CMusicArtistInfo> m_vecArtists;
+ std::string m_strAlbum;
+ std::string m_strArtist;
+ std::string m_strSearch;
+ int m_iAlbum;
+ int m_iArtist;
+ bool m_bSucceeded;
+ bool m_bCanceled;
+ XFILE::CCurlFile* m_http;
+ ADDON::ScraperPtr m_scraper;
+};
+
+}
diff --git a/xbmc/music/jobs/CMakeLists.txt b/xbmc/music/jobs/CMakeLists.txt
new file mode 100644
index 0000000..8ee0f5d
--- /dev/null
+++ b/xbmc/music/jobs/CMakeLists.txt
@@ -0,0 +1,15 @@
+set(SOURCES MusicLibraryJob.cpp
+ MusicLibraryProgressJob.cpp
+ MusicLibraryCleaningJob.cpp
+ MusicLibraryExportJob.cpp
+ MusicLibraryImportJob.cpp
+ MusicLibraryScanningJob.cpp)
+
+set(HEADERS MusicLibraryJob.h
+ MusicLibraryProgressJob.h
+ MusicLibraryCleaningJob.h
+ MusicLibraryExportJob.h
+ MusicLibraryImportJob.h
+ MusicLibraryScanningJob.h)
+
+core_add_library(music_jobs)
diff --git a/xbmc/music/jobs/MusicLibraryCleaningJob.cpp b/xbmc/music/jobs/MusicLibraryCleaningJob.cpp
new file mode 100644
index 0000000..4361566
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryCleaningJob.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017-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 "MusicLibraryCleaningJob.h"
+
+#include "dialogs/GUIDialogProgress.h"
+#include "music/MusicDatabase.h"
+
+CMusicLibraryCleaningJob::CMusicLibraryCleaningJob(CGUIDialogProgress* progressDialog)
+ : CMusicLibraryProgressJob(nullptr)
+{
+ if (progressDialog)
+ SetProgressIndicators(nullptr, progressDialog);
+ SetAutoClose(true);
+}
+
+CMusicLibraryCleaningJob::~CMusicLibraryCleaningJob() = default;
+
+bool CMusicLibraryCleaningJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CMusicLibraryCleaningJob* cleaningJob = dynamic_cast<const CMusicLibraryCleaningJob*>(job);
+ if (cleaningJob == nullptr)
+ return false;
+
+ return true;
+}
+
+bool CMusicLibraryCleaningJob::Work(CMusicDatabase &db)
+{
+ db.Cleanup(GetProgressDialog());
+ return true;
+}
diff --git a/xbmc/music/jobs/MusicLibraryCleaningJob.h b/xbmc/music/jobs/MusicLibraryCleaningJob.h
new file mode 100644
index 0000000..6e893c6
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryCleaningJob.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017-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 "music/jobs/MusicLibraryProgressJob.h"
+
+#include <set>
+
+/*!
+ \brief Music library job implementation for cleaning the video library.
+*/
+class CMusicLibraryCleaningJob : public CMusicLibraryProgressJob
+{
+public:
+ /*!
+ \brief Creates a new music library cleaning job.
+ \param[in] progressDialog Progress dialog to be used to display the cleaning progress
+ */
+ CMusicLibraryCleaningJob(CGUIDialogProgress* progressDialog);
+ ~CMusicLibraryCleaningJob() override;
+
+ // specialization of CJob
+ const char *GetType() const override { return "MusicLibraryCleaningJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ // implementation of CMusicLibraryJob
+ bool Work(CMusicDatabase &db) override;
+
+private:
+
+};
diff --git a/xbmc/music/jobs/MusicLibraryExportJob.cpp b/xbmc/music/jobs/MusicLibraryExportJob.cpp
new file mode 100644
index 0000000..ca63d13
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryExportJob.cpp
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017-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 "MusicLibraryExportJob.h"
+
+#include "dialogs/GUIDialogProgress.h"
+#include "music/MusicDatabase.h"
+#include "settings/LibExportSettings.h"
+
+CMusicLibraryExportJob::CMusicLibraryExportJob(const CLibExportSettings& settings, CGUIDialogProgress* progressDialog)
+ : CMusicLibraryProgressJob(NULL),
+ m_settings(settings)
+{
+ if (progressDialog)
+ SetProgressIndicators(NULL, progressDialog);
+ SetAutoClose(true);
+}
+
+CMusicLibraryExportJob::~CMusicLibraryExportJob() = default;
+
+bool CMusicLibraryExportJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CMusicLibraryExportJob* exportJob = dynamic_cast<const CMusicLibraryExportJob*>(job);
+ if (exportJob == NULL)
+ return false;
+
+ return !(m_settings != exportJob->m_settings);
+}
+
+bool CMusicLibraryExportJob::Work(CMusicDatabase &db)
+{
+ db.ExportToXML(m_settings, GetProgressDialog());
+
+ return true;
+}
diff --git a/xbmc/music/jobs/MusicLibraryExportJob.h b/xbmc/music/jobs/MusicLibraryExportJob.h
new file mode 100644
index 0000000..4dc759e
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryExportJob.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017-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 "MusicLibraryProgressJob.h"
+#include "settings/LibExportSettings.h"
+
+class CGUIDialogProgress;
+
+/*!
+ \brief Music library job implementation for exporting the music library.
+*/
+class CMusicLibraryExportJob : public CMusicLibraryProgressJob
+{
+public:
+ /*!
+ \brief Creates a new music library export job for the given paths.
+
+ \param[in] settings Library export settings
+ \param[in] progressDialog Progress dialog to be used to display the export progress
+ */
+ CMusicLibraryExportJob(const CLibExportSettings& settings, CGUIDialogProgress* progressDialog);
+
+ ~CMusicLibraryExportJob() override;
+
+ // specialization of CJob
+ const char *GetType() const override { return "MusicLibraryExportJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ // implementation of CMusicLibraryJob
+ bool Work(CMusicDatabase &db) override;
+
+private:
+ CLibExportSettings m_settings;
+};
diff --git a/xbmc/music/jobs/MusicLibraryImportJob.cpp b/xbmc/music/jobs/MusicLibraryImportJob.cpp
new file mode 100644
index 0000000..6fcae05
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryImportJob.cpp
@@ -0,0 +1,42 @@
+/*
+* Copyright (C) 2017-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 "MusicLibraryImportJob.h"
+
+#include "dialogs/GUIDialogProgress.h"
+#include "music/MusicDatabase.h"
+
+CMusicLibraryImportJob::CMusicLibraryImportJob(const std::string& xmlFile, CGUIDialogProgress* progressDialog)
+ : CMusicLibraryProgressJob(nullptr)
+ , m_xmlFile(xmlFile)
+{
+ if (progressDialog)
+ SetProgressIndicators(nullptr, progressDialog);
+ SetAutoClose(true);
+}
+
+CMusicLibraryImportJob::~CMusicLibraryImportJob() = default;
+
+bool CMusicLibraryImportJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CMusicLibraryImportJob* importJob = dynamic_cast<const CMusicLibraryImportJob*>(job);
+ if (importJob == nullptr)
+ return false;
+
+ return !(m_xmlFile != importJob->m_xmlFile);
+}
+
+bool CMusicLibraryImportJob::Work(CMusicDatabase &db)
+{
+ db.ImportFromXML(m_xmlFile, GetProgressDialog());
+
+ return true;
+}
diff --git a/xbmc/music/jobs/MusicLibraryImportJob.h b/xbmc/music/jobs/MusicLibraryImportJob.h
new file mode 100644
index 0000000..bcca44f
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryImportJob.h
@@ -0,0 +1,42 @@
+/*
+* Copyright (C) 2017-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 "MusicLibraryProgressJob.h"
+
+class CGUIDialogProgress;
+
+/*!
+\brief Music library job implementation for importing data to the music library.
+*/
+class CMusicLibraryImportJob : public CMusicLibraryProgressJob
+{
+public:
+ /*!
+ \brief Creates a new music library import job for the given xml file.
+
+ \param[in] xmlFile xml file to import
+ \param[in] progressDialog Progress dialog to be used to display the import progress
+ */
+ CMusicLibraryImportJob(const std::string &xmlFile, CGUIDialogProgress* progressDialog);
+
+ ~CMusicLibraryImportJob() override;
+
+ // specialization of CJob
+ const char *GetType() const override { return "MusicLibraryImportJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ // implementation of CMusicLibraryJob
+ bool Work(CMusicDatabase &db) override;
+
+private:
+ std::string m_xmlFile;
+};
+
diff --git a/xbmc/music/jobs/MusicLibraryJob.cpp b/xbmc/music/jobs/MusicLibraryJob.cpp
new file mode 100644
index 0000000..281d7f9
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryJob.cpp
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2017-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 "MusicLibraryJob.h"
+
+#include "music/MusicDatabase.h"
+
+CMusicLibraryJob::CMusicLibraryJob() = default;
+
+CMusicLibraryJob::~CMusicLibraryJob() = default;
+
+bool CMusicLibraryJob::DoWork()
+{
+ CMusicDatabase db;
+ if (!db.Open())
+ return false;
+
+ return Work(db);
+}
diff --git a/xbmc/music/jobs/MusicLibraryJob.h b/xbmc/music/jobs/MusicLibraryJob.h
new file mode 100644
index 0000000..81f68b7
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryJob.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017-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 "utils/Job.h"
+
+class CMusicDatabase;
+
+/*!
+ \brief Basic implementation/interface of a CJob which interacts with the
+ music database.
+ */
+class CMusicLibraryJob : public CJob
+{
+public:
+ ~CMusicLibraryJob() override;
+
+ /*!
+ \brief Whether the job can be cancelled or not.
+ */
+ virtual bool CanBeCancelled() const { return false; }
+
+ /*!
+ \brief Tries to cancel the running job.
+ \return True if the job was cancelled, false otherwise
+ */
+ virtual bool Cancel() { return false; }
+
+ // implementation of CJob
+ bool DoWork() override;
+ const char *GetType() const override { return "MusicLibraryJob"; }
+ bool operator==(const CJob* job) const override { return false; }
+
+protected:
+ CMusicLibraryJob();
+
+ /*!
+ \brief Worker method to be implemented by an actual implementation.
+
+ \param[in] db Already open music database to be used for interaction
+ \return True if the process succeeded, false otherwise
+ */
+ virtual bool Work(CMusicDatabase &db) = 0;
+};
diff --git a/xbmc/music/jobs/MusicLibraryProgressJob.cpp b/xbmc/music/jobs/MusicLibraryProgressJob.cpp
new file mode 100644
index 0000000..d07e85b
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryProgressJob.cpp
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2017-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 "MusicLibraryProgressJob.h"
+
+CMusicLibraryProgressJob::CMusicLibraryProgressJob(CGUIDialogProgressBarHandle* progressBar)
+ : CProgressJob(progressBar)
+{ }
+
+CMusicLibraryProgressJob::~CMusicLibraryProgressJob() = default;
+
+bool CMusicLibraryProgressJob::DoWork()
+{
+ bool result = CMusicLibraryJob::DoWork();
+
+ MarkFinished();
+
+ return result;
+}
diff --git a/xbmc/music/jobs/MusicLibraryProgressJob.h b/xbmc/music/jobs/MusicLibraryProgressJob.h
new file mode 100644
index 0000000..ae843da
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryProgressJob.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017-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 "music/jobs/MusicLibraryJob.h"
+#include "utils/ProgressJob.h"
+
+/*!
+ \brief Combined base implementation of a music library job with a progress bar.
+ */
+class CMusicLibraryProgressJob : public CProgressJob, public CMusicLibraryJob
+{
+public:
+ ~CMusicLibraryProgressJob() override;
+
+ // implementation of CJob
+ bool DoWork() override;
+ const char *GetType() const override { return "CMusicLibraryProgressJob"; }
+ bool operator==(const CJob* job) const override { return false; }
+
+protected:
+ explicit CMusicLibraryProgressJob(CGUIDialogProgressBarHandle* progressBar);
+};
diff --git a/xbmc/music/jobs/MusicLibraryScanningJob.cpp b/xbmc/music/jobs/MusicLibraryScanningJob.cpp
new file mode 100644
index 0000000..d2626a1
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryScanningJob.cpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017-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 "MusicLibraryScanningJob.h"
+
+#include "music/MusicDatabase.h"
+
+CMusicLibraryScanningJob::CMusicLibraryScanningJob(const std::string& directory, int flags, bool showProgress /* = true */)
+ : m_scanner(),
+ m_directory(directory),
+ m_showProgress(showProgress),
+ m_flags(flags)
+{ }
+
+CMusicLibraryScanningJob::~CMusicLibraryScanningJob() = default;
+
+bool CMusicLibraryScanningJob::Cancel()
+{
+ if (!m_scanner.IsScanning())
+ return true;
+
+ m_scanner.Stop();
+ return true;
+}
+
+bool CMusicLibraryScanningJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CMusicLibraryScanningJob* scanningJob = dynamic_cast<const CMusicLibraryScanningJob*>(job);
+ if (scanningJob == nullptr)
+ return false;
+
+ return m_directory == scanningJob->m_directory &&
+ m_flags == scanningJob->m_flags;
+}
+
+bool CMusicLibraryScanningJob::Work(CMusicDatabase &db)
+{
+ m_scanner.ShowDialog(m_showProgress);
+ if (m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_ALBUMS)
+ // Scrape additional album information
+ m_scanner.FetchAlbumInfo(m_directory, m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN);
+ else if (m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_ARTISTS)
+ // Scrape additional artist information
+ m_scanner.FetchArtistInfo(m_directory, m_flags & MUSIC_INFO::CMusicInfoScanner::SCAN_RESCAN);
+ else
+ // Scan tags from music files, and optionally scrape artist and album info
+ m_scanner.Start(m_directory, m_flags);
+
+ return true;
+}
diff --git a/xbmc/music/jobs/MusicLibraryScanningJob.h b/xbmc/music/jobs/MusicLibraryScanningJob.h
new file mode 100644
index 0000000..6c5ea13
--- /dev/null
+++ b/xbmc/music/jobs/MusicLibraryScanningJob.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017-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 "music/infoscanner/MusicInfoScanner.h"
+#include "music/jobs/MusicLibraryJob.h"
+
+#include <string>
+
+/*!
+ \brief Music library job implementation for scanning items.
+ Uses CMusicInfoScanner for scanning and can be run with or
+ without a visible progress bar.
+ */
+class CMusicLibraryScanningJob : public CMusicLibraryJob
+{
+public:
+ /*!
+ \brief Creates a new music library tag scanning and data scraping job.
+ \param[in] directory Directory to be scanned for new items
+ \param[in] flags What kind of scan to do
+ \param[in] showProgress Whether to show a progress bar or not
+ */
+ CMusicLibraryScanningJob(const std::string& directory, int flags, bool showProgress = true);
+ ~CMusicLibraryScanningJob() override;
+
+ // specialization of CMusicLibraryJob
+ bool CanBeCancelled() const override { return true; }
+ bool Cancel() override;
+
+ // specialization of CJob
+ const char *GetType() const override { return "MusicLibraryScanningJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ // implementation of CMusicLibraryJob
+ bool Work(CMusicDatabase &db) override;
+
+private:
+ MUSIC_INFO::CMusicInfoScanner m_scanner;
+ std::string m_directory;
+ bool m_showProgress;
+ int m_flags;
+};
diff --git a/xbmc/music/tags/CMakeLists.txt b/xbmc/music/tags/CMakeLists.txt
new file mode 100644
index 0000000..bbd3462
--- /dev/null
+++ b/xbmc/music/tags/CMakeLists.txt
@@ -0,0 +1,22 @@
+set(SOURCES MusicInfoTag.cpp
+ MusicInfoTagLoaderCDDA.cpp
+ MusicInfoTagLoaderDatabase.cpp
+ MusicInfoTagLoaderFactory.cpp
+ MusicInfoTagLoaderFFmpeg.cpp
+ MusicInfoTagLoaderShn.cpp
+ ReplayGain.cpp
+ TagLibVFSStream.cpp
+ TagLoaderTagLib.cpp)
+
+set(HEADERS ImusicInfoTagLoader.h
+ MusicInfoTag.h
+ MusicInfoTagLoaderCDDA.h
+ MusicInfoTagLoaderDatabase.h
+ MusicInfoTagLoaderFactory.h
+ MusicInfoTagLoaderFFmpeg.h
+ MusicInfoTagLoaderShn.h
+ ReplayGain.h
+ TagLibVFSStream.h
+ TagLoaderTagLib.h)
+
+core_add_library(music_tags)
diff --git a/xbmc/music/tags/ImusicInfoTagLoader.h b/xbmc/music/tags/ImusicInfoTagLoader.h
new file mode 100644
index 0000000..c3e07fe
--- /dev/null
+++ b/xbmc/music/tags/ImusicInfoTagLoader.h
@@ -0,0 +1,26 @@
+/*
+ * 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 <string>
+
+class EmbeddedArt;
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTag;
+ class IMusicInfoTagLoader
+ {
+ public:
+ IMusicInfoTagLoader(void) = default;
+ virtual ~IMusicInfoTagLoader() = default;
+
+ virtual bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) = 0;
+ };
+}
diff --git a/xbmc/music/tags/MusicInfoTag.cpp b/xbmc/music/tags/MusicInfoTag.cpp
new file mode 100644
index 0000000..62548bf
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTag.cpp
@@ -0,0 +1,1303 @@
+/*
+ * 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 "MusicInfoTag.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/Artist.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Archive.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+
+using namespace MUSIC_INFO;
+
+CMusicInfoTag::CMusicInfoTag(void)
+{
+ Clear();
+}
+
+bool CMusicInfoTag::operator !=(const CMusicInfoTag& tag) const
+{
+ if (this == &tag) return false;
+ if (m_strURL != tag.m_strURL) return true;
+ if (m_strTitle != tag.m_strTitle) return true;
+ if (m_bCompilation != tag.m_bCompilation) return true;
+ if (m_artist != tag.m_artist) return true;
+ if (m_albumArtist != tag.m_albumArtist) return true;
+ if (m_strAlbum != tag.m_strAlbum) return true;
+ if (m_iDuration != tag.m_iDuration) return true;
+ if (m_strDiscSubtitle != tag.m_strDiscSubtitle)
+ return true;
+ if (m_iTrack != tag.m_iTrack)
+ return true;
+ if (m_albumReleaseType != tag.m_albumReleaseType) return true;
+ return false;
+}
+
+int CMusicInfoTag::GetTrackNumber() const
+{
+ return (m_iTrack & 0xffff);
+}
+
+int CMusicInfoTag::GetDiscNumber() const
+{
+ return (m_iTrack >> 16);
+}
+
+int CMusicInfoTag::GetTrackAndDiscNumber() const
+{
+ return m_iTrack;
+}
+
+int CMusicInfoTag::GetDuration() const
+{
+ return m_iDuration;
+}
+
+const std::string& CMusicInfoTag::GetTitle() const
+{
+ return m_strTitle;
+}
+
+const std::string& CMusicInfoTag::GetURL() const
+{
+ return m_strURL;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetArtist() const
+{
+ return m_artist;
+}
+
+const std::string CMusicInfoTag::GetArtistString() const
+{
+ if (!m_strArtistDesc.empty())
+ return m_strArtistDesc;
+ else if (!m_artist.empty())
+ return StringUtils::Join(m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ else
+ return StringUtils::Empty;
+}
+
+const std::string& CMusicInfoTag::GetArtistSort() const
+{
+ return m_strArtistSort;
+}
+
+const std::string& CMusicInfoTag::GetComposerSort() const
+{
+ return m_strComposerSort;
+}
+
+const std::string& CMusicInfoTag::GetAlbum() const
+{
+ return m_strAlbum;
+}
+
+const std::string& CMusicInfoTag::GetDiscSubtitle() const
+{
+ return m_strDiscSubtitle;
+}
+
+const std::string& CMusicInfoTag::GetOriginalDate() const
+{
+ return m_strOriginalDate;
+}
+
+const std::string MUSIC_INFO::CMusicInfoTag::GetOriginalYear() const
+{
+ return StringUtils::Left(m_strOriginalDate, 4);
+}
+
+int CMusicInfoTag::GetAlbumId() const
+{
+ return m_iAlbumId;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetAlbumArtist() const
+{
+ return m_albumArtist;
+}
+
+const std::string CMusicInfoTag::GetAlbumArtistString() const
+{
+ if (!m_strAlbumArtistDesc.empty())
+ return m_strAlbumArtistDesc;
+ if (!m_albumArtist.empty())
+ return StringUtils::Join(m_albumArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ else
+ return StringUtils::Empty;
+}
+
+const std::string& CMusicInfoTag::GetAlbumArtistSort() const
+{
+ return m_strAlbumArtistSort;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetGenre() const
+{
+ return m_genre;
+}
+
+int CMusicInfoTag::GetDatabaseId() const
+{
+ return m_iDbId;
+}
+
+const std::string &CMusicInfoTag::GetType() const
+{
+ return m_type;
+}
+
+int CMusicInfoTag::GetYear() const
+{
+ return atoi(GetYearString().c_str());
+}
+
+std::string CMusicInfoTag::GetYearString() const
+{
+ /* Get year as YYYY from release or original dates depending on setting
+ This is how GUI and by year sorting swiches to using original year.
+ For ripper and non-library items (library entries have both values):
+ when release date missing try to fallback to original date
+ when original date missing use release date
+ */
+ std::string value;
+ value = GetReleaseYear();
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE) ||
+ value.empty())
+ {
+ std::string origvalue = GetOriginalYear();
+ if (!origvalue.empty())
+ return origvalue;
+ }
+ return value;
+}
+
+const std::string &CMusicInfoTag::GetComment() const
+{
+ return m_strComment;
+}
+
+const std::string &CMusicInfoTag::GetMood() const
+{
+ return m_strMood;
+}
+
+const std::string &CMusicInfoTag::GetRecordLabel() const
+{
+ return m_strRecordLabel;
+}
+
+const std::string &CMusicInfoTag::GetLyrics() const
+{
+ return m_strLyrics;
+}
+
+const std::string &CMusicInfoTag::GetCueSheet() const
+{
+ return m_cuesheet;
+}
+
+float CMusicInfoTag::GetRating() const
+{
+ return m_Rating;
+}
+
+int CMusicInfoTag::GetUserrating() const
+{
+ return m_Userrating;
+}
+
+int CMusicInfoTag::GetVotes() const
+{
+ return m_Votes;
+}
+
+int CMusicInfoTag::GetListeners() const
+{
+ return m_listeners;
+}
+
+int CMusicInfoTag::GetPlayCount() const
+{
+ return m_iTimesPlayed;
+}
+
+const CDateTime &CMusicInfoTag::GetLastPlayed() const
+{
+ return m_lastPlayed;
+}
+
+const CDateTime &CMusicInfoTag::GetDateAdded() const
+{
+ return m_dateAdded;
+}
+
+bool CMusicInfoTag::GetCompilation() const
+{
+ return m_bCompilation;
+}
+
+bool CMusicInfoTag::GetBoxset() const
+{
+ return m_bBoxset;
+}
+
+int CMusicInfoTag::GetTotalDiscs() const
+{
+ return m_iDiscTotal;
+}
+
+const EmbeddedArtInfo &CMusicInfoTag::GetCoverArtInfo() const
+{
+ return m_coverArt;
+}
+
+const ReplayGain& CMusicInfoTag::GetReplayGain() const
+{
+ return m_replayGain;
+}
+
+CAlbum::ReleaseType CMusicInfoTag::GetAlbumReleaseType() const
+{
+ return m_albumReleaseType;
+}
+
+int CMusicInfoTag::GetBPM() const
+{
+ return m_iBPM;
+}
+
+int CMusicInfoTag::GetBitRate() const
+{
+ return m_bitrate;
+}
+
+int CMusicInfoTag::GetSampleRate() const
+{
+ return m_samplerate;
+}
+
+int CMusicInfoTag::GetNoOfChannels() const
+{
+ return m_channels;
+}
+
+const std::string& CMusicInfoTag::GetReleaseDate() const
+{
+ return m_strReleaseDate;
+}
+
+const std::string MUSIC_INFO::CMusicInfoTag::GetReleaseYear() const
+{
+ return StringUtils::Left(m_strReleaseDate, 4);
+}
+
+// This is the Musicbrainz release status tag. See https://musicbrainz.org/doc/Release#Status
+
+const std::string& CMusicInfoTag::GetAlbumReleaseStatus() const
+{
+ return m_strReleaseStatus;
+}
+
+const std::string& CMusicInfoTag::GetStationName() const
+{
+ return m_stationName;
+}
+
+const std::string& CMusicInfoTag::GetStationArt() const
+{
+ return m_stationArt;
+}
+
+void CMusicInfoTag::SetURL(const std::string& strURL)
+{
+ m_strURL = strURL;
+}
+
+void CMusicInfoTag::SetTitle(const std::string& strTitle)
+{
+ m_strTitle = Trim(strTitle);
+}
+
+void CMusicInfoTag::SetArtist(const std::string& strArtist)
+{
+ if (!strArtist.empty())
+ {
+ SetArtistDesc(strArtist);
+ SetArtist(StringUtils::Split(strArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ }
+ else
+ {
+ m_strArtistDesc.clear();
+ m_artist.clear();
+ }
+}
+
+void CMusicInfoTag::SetArtist(const std::vector<std::string>& artists, bool FillDesc /* = false*/)
+{
+ m_artist = artists;
+ if (m_strArtistDesc.empty() || FillDesc)
+ {
+ SetArtistDesc(StringUtils::Join(artists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ }
+}
+
+void CMusicInfoTag::SetArtistDesc(const std::string& strArtistDesc)
+{
+ m_strArtistDesc = strArtistDesc;
+}
+
+void CMusicInfoTag::SetArtistSort(const std::string& strArtistsort)
+{
+ m_strArtistSort = strArtistsort;
+}
+
+void CMusicInfoTag::SetComposerSort(const std::string& strComposerSort)
+{
+ m_strComposerSort = strComposerSort;
+}
+
+void CMusicInfoTag::SetAlbum(const std::string& strAlbum)
+{
+ m_strAlbum = Trim(strAlbum);
+}
+
+void CMusicInfoTag::SetAlbumId(const int iAlbumId)
+{
+ m_iAlbumId = iAlbumId;
+}
+
+void CMusicInfoTag::SetAlbumArtist(const std::string& strAlbumArtist)
+{
+ if (!strAlbumArtist.empty())
+ {
+ SetAlbumArtistDesc(strAlbumArtist);
+ SetAlbumArtist(StringUtils::Split(strAlbumArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ }
+ else
+ {
+ m_strAlbumArtistDesc.clear();
+ m_albumArtist.clear();
+ }
+}
+
+void CMusicInfoTag::SetAlbumArtist(const std::vector<std::string>& albumArtists, bool FillDesc /* = false*/)
+{
+ m_albumArtist = albumArtists;
+ if (m_strAlbumArtistDesc.empty() || FillDesc)
+ SetAlbumArtistDesc(StringUtils::Join(albumArtists, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CMusicInfoTag::SetAlbumArtistDesc(const std::string& strAlbumArtistDesc)
+{
+ m_strAlbumArtistDesc = strAlbumArtistDesc;
+}
+
+void CMusicInfoTag::SetAlbumArtistSort(const std::string& strAlbumArtistSort)
+{
+ m_strAlbumArtistSort = strAlbumArtistSort;
+}
+
+void CMusicInfoTag::SetGenre(const std::string& strGenre, bool bTrim /* = false*/)
+{
+ if (!strGenre.empty())
+ SetGenre(StringUtils::Split(strGenre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator), bTrim);
+ else
+ m_genre.clear();
+}
+
+void CMusicInfoTag::SetGenre(const std::vector<std::string>& genres, bool bTrim /* = false*/)
+{
+ m_genre = genres;
+ if (bTrim)
+ for (auto genre : m_genre)
+ StringUtils::Trim(genre);
+}
+
+void CMusicInfoTag::SetYear(int year)
+{
+ // Parse integer year value into YYYY ISO8601 format (partial) date string
+ // Add century for to 2 digit numbers, 41 -> 1941, 40 -> 2040
+ if (year > 99)
+ SetReleaseDate(StringUtils::Format("{:04}", year));
+ else if (year > 40)
+ SetReleaseDate(StringUtils::Format("{:04}", 19 + year));
+ else if (year > 0)
+ SetReleaseDate(StringUtils::Format("{:04}", 20 + year));
+ else
+ m_strReleaseDate.clear();
+}
+
+void CMusicInfoTag::SetDatabaseId(int id, const std::string &type)
+{
+ m_iDbId = id;
+ m_type = type;
+}
+
+void CMusicInfoTag::SetTrackNumber(int iTrack)
+{
+ m_iTrack = (m_iTrack & 0xffff0000) | (iTrack & 0xffff);
+}
+
+void CMusicInfoTag::SetDiscNumber(int iDiscNumber)
+{
+ m_iTrack = (m_iTrack & 0xffff) | (iDiscNumber << 16);
+}
+
+void CMusicInfoTag::SetDiscSubtitle(const std::string& strDiscSubtitle)
+{
+ m_strDiscSubtitle = strDiscSubtitle;
+}
+
+void CMusicInfoTag::SetTotalDiscs(int iDiscTotal)
+{
+ m_iDiscTotal = iDiscTotal;
+}
+
+void CMusicInfoTag::SetReleaseDate(const std::string& strReleaseDate)
+{
+ // Date in ISO8601 YYYY, YYYY-MM or YYYY-MM-DD
+ m_strReleaseDate = strReleaseDate;
+}
+
+void CMusicInfoTag::SetOriginalDate(const std::string& strOriginalDate)
+{
+ // Date in ISO8601 YYYY, YYYY-MM or YYYY-MM-DD
+ m_strOriginalDate = strOriginalDate;
+}
+
+void CMusicInfoTag::AddOriginalDate(const std::string& strDateYear)
+{
+ // Avoid overwriting YYYY-MM or YYYY-MM-DD (from DATE tag) with just YYYY (from YEAR tag)
+ if (strDateYear.size() > m_strOriginalDate.size())
+ m_strOriginalDate = strDateYear;
+}
+
+void CMusicInfoTag::AddReleaseDate(const std::string& strDateYear, bool isMonth /*= false*/)
+{
+ // Given MMDD (from ID3 v2.3 TDAT tag) set MM-DD part of ISO8601 string
+ if (isMonth && !strDateYear.empty())
+ {
+ std::string strYYYY = GetReleaseYear();
+ if (strYYYY.empty())
+ strYYYY = "0000"; // Fake year when TYER not read yet
+ m_strReleaseDate = StringUtils::Format("{}-{}-{}", strYYYY, StringUtils::Left(strDateYear, 2),
+ StringUtils::Right(strDateYear, 2));
+ }
+ // Given YYYY only (from YEAR tag) and already have YYYY-MM or YYYY-MM-DD (from DATE tag)
+ else if (strDateYear.size() == 4 && (m_strReleaseDate.size() > 4))
+ {
+ // Have 0000-MM-DD where ID3 v2.3 TDAT tag read first, fill YYYY part from TYER
+ if (GetReleaseYear() == "0000")
+ StringUtils::Replace(m_strReleaseDate, "0000", strDateYear);
+ }
+ else
+ m_strReleaseDate = strDateYear; // Could be YYYY, YYYY-MM or YYYY-MM-DD
+}
+
+void CMusicInfoTag::SetTrackAndDiscNumber(int iTrackAndDisc)
+{
+ m_iTrack = iTrackAndDisc;
+}
+
+void CMusicInfoTag::SetDuration(int iSec)
+{
+ m_iDuration = iSec;
+}
+
+void CMusicInfoTag::SetBitRate(int bitrate)
+{
+ m_bitrate = bitrate;
+}
+
+void CMusicInfoTag::SetNoOfChannels(int channels)
+{
+ m_channels = channels;
+}
+
+void CMusicInfoTag::SetSampleRate(int samplerate)
+{
+ m_samplerate = samplerate;
+}
+
+void CMusicInfoTag::SetComment(const std::string& comment)
+{
+ m_strComment = comment;
+}
+
+void CMusicInfoTag::SetMood(const std::string& mood)
+{
+ m_strMood = mood;
+}
+
+void CMusicInfoTag::SetRecordLabel(const std::string& publisher)
+{
+ m_strRecordLabel = publisher;
+}
+
+void CMusicInfoTag::SetCueSheet(const std::string& cueSheet)
+{
+ m_cuesheet = cueSheet;
+}
+
+void CMusicInfoTag::SetLyrics(const std::string& lyrics)
+{
+ m_strLyrics = lyrics;
+}
+
+void CMusicInfoTag::SetRating(float rating)
+{
+ //This value needs to be between 0-10 - 0 will unset the rating
+ rating = std::max(rating, 0.f);
+ rating = std::min(rating, 10.f);
+
+ m_Rating = rating;
+}
+
+void CMusicInfoTag::SetVotes(int votes)
+{
+ m_Votes = votes;
+}
+
+void CMusicInfoTag::SetUserrating(int rating)
+{
+ //This value needs to be between 0-10 - 0 will unset the userrating
+ rating = std::max(rating, 0);
+ rating = std::min(rating, 10);
+
+ m_Userrating = rating;
+}
+
+void CMusicInfoTag::SetListeners(int listeners)
+{
+ m_listeners = std::max(listeners, 0);
+}
+
+void CMusicInfoTag::SetPlayCount(int playcount)
+{
+ m_iTimesPlayed = playcount;
+}
+
+void CMusicInfoTag::SetLastPlayed(const std::string& lastplayed)
+{
+ m_lastPlayed.SetFromDBDateTime(lastplayed);
+}
+
+void CMusicInfoTag::SetLastPlayed(const CDateTime& lastplayed)
+{
+ m_lastPlayed = lastplayed;
+}
+
+void CMusicInfoTag::SetDateAdded(const std::string& strDateAdded)
+{
+ m_dateAdded.SetFromDBDateTime(strDateAdded);
+}
+
+void CMusicInfoTag::SetDateAdded(const CDateTime& dateAdded)
+{
+ m_dateAdded = dateAdded;
+}
+
+void MUSIC_INFO::CMusicInfoTag::SetDateUpdated(const std::string& strDateUpdated)
+{
+ m_dateUpdated.SetFromDBDateTime(strDateUpdated);
+}
+
+void MUSIC_INFO::CMusicInfoTag::SetDateUpdated(const CDateTime& dateUpdated)
+{
+ m_dateUpdated = dateUpdated;
+}
+
+void MUSIC_INFO::CMusicInfoTag::SetDateNew(const std::string& strDateNew)
+{
+ m_dateNew.SetFromDBDateTime(strDateNew);
+}
+
+void MUSIC_INFO::CMusicInfoTag::SetDateNew(const CDateTime& dateNew)
+{
+ m_dateNew = dateNew;
+}
+
+void CMusicInfoTag::SetCompilation(bool compilation)
+{
+ m_bCompilation = compilation;
+}
+
+void CMusicInfoTag::SetBoxset(bool boxset)
+{
+ m_bBoxset = boxset;
+}
+
+void CMusicInfoTag::SetLoaded(bool bOnOff)
+{
+ m_bLoaded = bOnOff;
+}
+
+bool CMusicInfoTag::Loaded() const
+{
+ return m_bLoaded;
+}
+
+void CMusicInfoTag::SetBPM(int bpm)
+{
+ m_iBPM = bpm;
+}
+
+void CMusicInfoTag::SetStationName(const std::string& strStationName)
+{
+ m_stationName = strStationName;
+}
+
+const std::string& CMusicInfoTag::GetMusicBrainzTrackID() const
+{
+ return m_strMusicBrainzTrackID;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzArtistID() const
+{
+ return m_musicBrainzArtistID;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzArtistHints() const
+{
+ return m_musicBrainzArtistHints;
+}
+
+const std::string& CMusicInfoTag::GetMusicBrainzAlbumID() const
+{
+ return m_strMusicBrainzAlbumID;
+}
+
+const std::string & MUSIC_INFO::CMusicInfoTag::GetMusicBrainzReleaseGroupID() const
+{
+ return m_strMusicBrainzReleaseGroupID;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzAlbumArtistID() const
+{
+ return m_musicBrainzAlbumArtistID;
+}
+
+const std::vector<std::string>& CMusicInfoTag::GetMusicBrainzAlbumArtistHints() const
+{
+ return m_musicBrainzAlbumArtistHints;
+}
+
+const std::string &CMusicInfoTag::GetMusicBrainzReleaseType() const
+{
+ return m_strMusicBrainzReleaseType;
+}
+
+void CMusicInfoTag::SetMusicBrainzTrackID(const std::string& strTrackID)
+{
+ m_strMusicBrainzTrackID = strTrackID;
+}
+
+void CMusicInfoTag::SetMusicBrainzArtistID(const std::vector<std::string>& musicBrainzArtistId)
+{
+ m_musicBrainzArtistID = musicBrainzArtistId;
+}
+
+void CMusicInfoTag::SetMusicBrainzArtistHints(const std::vector<std::string>& musicBrainzArtistHints)
+{
+ m_musicBrainzArtistHints = musicBrainzArtistHints;
+}
+
+void CMusicInfoTag::SetMusicBrainzAlbumID(const std::string& strAlbumID)
+{
+ m_strMusicBrainzAlbumID = strAlbumID;
+}
+
+void CMusicInfoTag::SetMusicBrainzAlbumArtistID(const std::vector<std::string>& musicBrainzAlbumArtistId)
+{
+ m_musicBrainzAlbumArtistID = musicBrainzAlbumArtistId;
+}
+
+void CMusicInfoTag::SetMusicBrainzAlbumArtistHints(const std::vector<std::string>& musicBrainzAlbumArtistHints)
+{
+ m_musicBrainzAlbumArtistHints = musicBrainzAlbumArtistHints;
+}
+
+void MUSIC_INFO::CMusicInfoTag::SetMusicBrainzReleaseGroupID(const std::string & strReleaseGroupID)
+{
+ m_strMusicBrainzReleaseGroupID = strReleaseGroupID;
+}
+
+void CMusicInfoTag::SetMusicBrainzReleaseType(const std::string& ReleaseType)
+{
+ m_strMusicBrainzReleaseType = ReleaseType;
+}
+
+void CMusicInfoTag::SetCoverArtInfo(size_t size, const std::string &mimeType)
+{
+ m_coverArt.Set(size, mimeType);
+}
+
+void CMusicInfoTag::SetReplayGain(const ReplayGain& aGain)
+{
+ m_replayGain = aGain;
+}
+
+void CMusicInfoTag::SetAlbumReleaseType(CAlbum::ReleaseType releaseType)
+{
+ m_albumReleaseType = releaseType;
+}
+
+void CMusicInfoTag::SetType(const MediaType& mediaType)
+{
+ m_type = mediaType;
+}
+
+// This is the Musicbrainz release status tag. See https://musicbrainz.org/doc/Release#Status
+
+void CMusicInfoTag::SetAlbumReleaseStatus(const std::string& ReleaseStatus)
+{
+ m_strReleaseStatus = ReleaseStatus;
+}
+
+void CMusicInfoTag::SetStationArt(const std::string& strStationArt)
+{
+ m_stationArt = strStationArt;
+}
+
+void CMusicInfoTag::SetArtist(const CArtist& artist)
+{
+ SetArtist(artist.strArtist);
+ SetArtistSort(artist.strSortName);
+ SetAlbumArtist(artist.strArtist);
+ SetAlbumArtistSort(artist.strSortName);
+ SetMusicBrainzArtistID({ artist.strMusicBrainzArtistID });
+ SetMusicBrainzAlbumArtistID({ artist.strMusicBrainzArtistID });
+ SetGenre(artist.genre);
+ SetMood(StringUtils::Join(artist.moods, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ SetDateAdded(artist.dateAdded);
+ SetDateUpdated(artist.dateUpdated);
+ SetDateNew(artist.dateNew);
+ SetDatabaseId(artist.idArtist, MediaTypeArtist);
+
+ SetLoaded();
+}
+
+void CMusicInfoTag::SetAlbum(const CAlbum& album)
+{
+ Clear();
+ //Set all artist information from album artist credits and artist description
+ SetArtistDesc(album.GetAlbumArtistString());
+ SetArtist(album.GetAlbumArtist());
+ SetArtistSort(album.GetAlbumArtistSort());
+ SetMusicBrainzArtistID(album.GetMusicBrainzAlbumArtistID());
+ SetAlbumArtistDesc(album.GetAlbumArtistString());
+ SetAlbumArtist(album.GetAlbumArtist());
+ SetAlbumArtistSort(album.GetAlbumArtistSort());
+ SetMusicBrainzAlbumArtistID(album.GetMusicBrainzAlbumArtistID());
+ SetAlbumId(album.idAlbum);
+ SetAlbum(album.strAlbum);
+ SetTitle(album.strAlbum);
+ SetMusicBrainzAlbumID(album.strMusicBrainzAlbumID);
+ SetMusicBrainzReleaseGroupID(album.strReleaseGroupMBID);
+ SetMusicBrainzReleaseType(album.strType);
+ SetAlbumReleaseStatus(album.strReleaseStatus);
+ SetGenre(album.genre);
+ SetMood(StringUtils::Join(album.moods, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ SetRecordLabel(album.strLabel);
+ SetRating(album.fRating);
+ SetUserrating(album.iUserrating);
+ SetVotes(album.iVotes);
+ SetCompilation(album.bCompilation);
+ SetOriginalDate(album.strOrigReleaseDate);
+ SetReleaseDate(album.strReleaseDate);
+ SetBoxset(album.bBoxedSet);
+ SetAlbumReleaseType(album.releaseType);
+ SetDateAdded(album.dateAdded);
+ SetDateUpdated(album.dateUpdated);
+ SetDateNew(album.dateNew);
+ SetPlayCount(album.iTimesPlayed);
+ SetDatabaseId(album.idAlbum, MediaTypeAlbum);
+ SetLastPlayed(album.lastPlayed);
+ SetTotalDiscs(album.iTotalDiscs);
+ SetDuration(album.iAlbumDuration);
+
+ SetLoaded();
+}
+
+void CMusicInfoTag::SetSong(const CSong& song)
+{
+ Clear();
+ SetTitle(song.strTitle);
+ SetGenre(song.genre);
+ /* Set all artist information from song artist credits and artist description.
+ During processing e.g. Cue Sheets, song may only have artist description string
+ rather than a fully populated artist credits vector.
+ */
+ if (!song.HasArtistCredits())
+ SetArtist(song.GetArtistString()); //Sets both artist description string and artist vector from string
+ else
+ {
+ SetArtistDesc(song.GetArtistString());
+ SetArtist(song.GetArtist());
+ SetMusicBrainzArtistID(song.GetMusicBrainzArtistID());
+ }
+ SetArtistSort(song.GetArtistSort());
+ SetAlbum(song.strAlbum);
+ SetAlbumArtist(song.GetAlbumArtist()); //Only have album artist in song as vector, no desc or MBID
+ SetAlbumArtistSort(song.GetAlbumArtistSort());
+ SetMusicBrainzTrackID(song.strMusicBrainzTrackID);
+ SetContributors(song.GetContributors());
+ SetComment(song.strComment);
+ SetCueSheet(song.strCueSheet);
+ SetPlayCount(song.iTimesPlayed);
+ SetLastPlayed(song.lastPlayed);
+ SetDateAdded(song.dateAdded);
+ SetDateUpdated(song.dateUpdated);
+ SetDateNew(song.dateNew);
+ SetCoverArtInfo(song.embeddedArt.m_size, song.embeddedArt.m_mime);
+ SetRating(song.rating);
+ SetUserrating(song.userrating);
+ SetVotes(song.votes);
+ SetURL(song.strFileName);
+ SetReleaseDate(song.strReleaseDate);
+ SetOriginalDate(song.strOrigReleaseDate);
+ SetTrackAndDiscNumber(song.iTrack);
+ SetDiscSubtitle(song.strDiscSubtitle);
+ SetDuration(song.iDuration);
+ SetMood(song.strMood);
+ SetCompilation(song.bCompilation);
+ SetAlbumId(song.idAlbum);
+ SetDatabaseId(song.idSong, MediaTypeSong);
+ SetBPM(song.iBPM);
+ SetBitRate(song.iBitRate);
+ SetSampleRate(song.iSampleRate);
+ SetNoOfChannels(song.iChannels);
+
+ if (song.replayGain.Get(ReplayGain::TRACK).Valid())
+ m_replayGain.Set(ReplayGain::TRACK, song.replayGain.Get(ReplayGain::TRACK));
+ if (song.replayGain.Get(ReplayGain::ALBUM).Valid())
+ m_replayGain.Set(ReplayGain::ALBUM, song.replayGain.Get(ReplayGain::ALBUM));
+
+ SetLoaded();
+}
+
+void CMusicInfoTag::Serialize(CVariant& value) const
+{
+ value["url"] = m_strURL;
+ value["title"] = m_strTitle;
+ if (m_type.compare(MediaTypeArtist) == 0 && m_artist.size() == 1)
+ value["artist"] = m_artist[0];
+ else
+ value["artist"] = m_artist;
+ // There are situations where the individual artist(s) are not queried from the song_artist and artist tables e.g. playlist,
+ // only artist description from song table. Since processing of the ARTISTS tag was added the individual artists may not always
+ // be accurately derived by simply splitting the artist desc. Hence m_artist is only populated when the individual artists are
+ // queried, whereas GetArtistString() will always return the artist description.
+ // To avoid empty artist array in JSON, when m_artist is empty then an attempt is made to split the artist desc into artists.
+ // A longer term solution would be to ensure that when individual artists are to be returned then the song_artist and artist tables
+ // are queried.
+ if (m_artist.empty())
+ value["artist"] = StringUtils::Split(GetArtistString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+
+ value["displayartist"] = GetArtistString();
+ value["displayalbumartist"] = GetAlbumArtistString();
+ value["sortartist"] = GetArtistSort();
+ value["album"] = m_strAlbum;
+ value["albumartist"] = m_albumArtist;
+ value["sortalbumartist"] = m_strAlbumArtistSort;
+ value["genre"] = m_genre;
+ value["duration"] = m_iDuration;
+ value["track"] = GetTrackNumber();
+ value["disc"] = GetDiscNumber();
+ value["loaded"] = m_bLoaded;
+ value["year"] = GetYear(); // Optionally from m_strOriginalDate
+ value["musicbrainztrackid"] = m_strMusicBrainzTrackID;
+ value["musicbrainzartistid"] = m_musicBrainzArtistID;
+ value["musicbrainzalbumid"] = m_strMusicBrainzAlbumID;
+ value["musicbrainzreleasegroupid"] = m_strMusicBrainzReleaseGroupID;
+ value["musicbrainzalbumartistid"] = m_musicBrainzAlbumArtistID;
+ value["comment"] = m_strComment;
+ value["contributors"] = CVariant(CVariant::VariantTypeArray);
+ for (const auto& role : m_musicRoles)
+ {
+ CVariant contributor;
+ contributor["name"] = role.GetArtist();
+ contributor["role"] = role.GetRoleDesc();
+ contributor["roleid"] = role.GetRoleId();
+ contributor["artistid"] = (int)(role.GetArtistId());
+ value["contributors"].push_back(contributor);
+ }
+ value["displaycomposer"] = GetArtistStringForRole("composer"); //TCOM
+ value["displayconductor"] = GetArtistStringForRole("conductor"); //TPE3
+ value["displayorchestra"] = GetArtistStringForRole("orchestra");
+ value["displaylyricist"] = GetArtistStringForRole("lyricist"); //TEXT
+ value["mood"] = StringUtils::Split(m_strMood, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ value["recordlabel"] = m_strRecordLabel;
+ value["rating"] = m_Rating;
+ value["userrating"] = m_Userrating;
+ value["votes"] = m_Votes;
+ value["playcount"] = m_iTimesPlayed;
+ value["lastplayed"] = m_lastPlayed.IsValid() ? m_lastPlayed.GetAsDBDateTime() : StringUtils::Empty;
+ value["dateadded"] = m_dateAdded.IsValid() ? m_dateAdded.GetAsDBDateTime() : StringUtils::Empty;
+ value["datenew"] = m_dateNew.IsValid() ? m_dateNew.GetAsDBDateTime() : StringUtils::Empty;
+ value["datemodified"] =
+ m_dateUpdated.IsValid() ? m_dateUpdated.GetAsDBDateTime() : StringUtils::Empty;
+ value["lyrics"] = m_strLyrics;
+ value["albumid"] = m_iAlbumId;
+ value["compilationartist"] = m_bCompilation;
+ value["compilation"] = m_bCompilation;
+ if (m_type.compare(MediaTypeAlbum) == 0)
+ value["releasetype"] = CAlbum::ReleaseTypeToString(m_albumReleaseType);
+ else if (m_type.compare(MediaTypeSong) == 0)
+ value["albumreleasetype"] = CAlbum::ReleaseTypeToString(m_albumReleaseType);
+ value["isboxset"] = m_bBoxset;
+ value["totaldiscs"] = m_iDiscTotal;
+ value["disctitle"] = m_strDiscSubtitle;
+ value["releasedate"] = m_strReleaseDate;
+ value["originaldate"] = m_strOriginalDate;
+ value["albumstatus"] = m_strReleaseStatus;
+ value["bpm"] = m_iBPM;
+ value["bitrate"] = m_bitrate;
+ value["samplerate"] = m_samplerate;
+ value["channels"] = m_channels;
+}
+
+void CMusicInfoTag::ToSortable(SortItem& sortable, Field field) const
+{
+ switch (field)
+ {
+ case FieldTitle:
+ {
+ // make sure not to overwrite an existing path with an empty one
+ std::string title = m_strTitle;
+ if (!title.empty() || sortable.find(FieldTitle) == sortable.end())
+ sortable[FieldTitle] = title;
+ break;
+ }
+ case FieldArtist: sortable[FieldArtist] = m_strArtistDesc; break;
+ case FieldArtistSort: sortable[FieldArtistSort] = m_strArtistSort; break;
+ case FieldAlbum: sortable[FieldAlbum] = m_strAlbum; break;
+ case FieldAlbumArtist: sortable[FieldAlbumArtist] = m_strAlbumArtistDesc; break;
+ case FieldGenre: sortable[FieldGenre] = m_genre; break;
+ case FieldTime: sortable[FieldTime] = m_iDuration; break;
+ case FieldTrackNumber: sortable[FieldTrackNumber] = m_iTrack; break;
+ case FieldTotalDiscs:
+ sortable[FieldTotalDiscs] = m_iDiscTotal;
+ break;
+ case FieldYear:
+ sortable[FieldYear] = GetYear(); // Optionally from m_strOriginalDate
+ break;
+ case FieldComment: sortable[FieldComment] = m_strComment; break;
+ case FieldMoods: sortable[FieldMoods] = m_strMood; break;
+ case FieldRating: sortable[FieldRating] = m_Rating; break;
+ case FieldUserRating: sortable[FieldUserRating] = m_Userrating; break;
+ case FieldVotes: sortable[FieldVotes] = m_Votes; break;
+ case FieldPlaycount: sortable[FieldPlaycount] = m_iTimesPlayed; break;
+ case FieldLastPlayed: sortable[FieldLastPlayed] = m_lastPlayed.IsValid() ? m_lastPlayed.GetAsDBDateTime() : StringUtils::Empty; break;
+ case FieldDateAdded: sortable[FieldDateAdded] = m_dateAdded.IsValid() ? m_dateAdded.GetAsDBDateTime() : StringUtils::Empty; break;
+ case FieldListeners: sortable[FieldListeners] = m_listeners; break;
+ case FieldId: sortable[FieldId] = (int64_t)m_iDbId; break;
+ case FieldOrigDate: sortable[FieldOrigDate] = m_strOriginalDate; break;
+ case FieldBPM: sortable[FieldBPM] = m_iBPM; break;
+ default: break;
+ }
+}
+
+void CMusicInfoTag::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_strURL;
+ ar << m_strTitle;
+ ar << m_artist;
+ ar << m_strArtistSort;
+ ar << m_strArtistDesc;
+ ar << m_strAlbum;
+ ar << m_albumArtist;
+ ar << m_strAlbumArtistDesc;
+ ar << m_genre;
+ ar << m_iDuration;
+ ar << m_iTrack;
+ ar << m_bLoaded;
+ ar << m_strReleaseDate;
+ ar << m_strOriginalDate;
+ ar << m_strMusicBrainzTrackID;
+ ar << m_musicBrainzArtistID;
+ ar << m_strMusicBrainzAlbumID;
+ ar << m_strMusicBrainzReleaseGroupID;
+ ar << m_musicBrainzAlbumArtistID;
+ ar << m_strDiscSubtitle;
+ ar << m_bBoxset;
+ ar << m_iDiscTotal;
+ ar << m_strMusicBrainzReleaseType;
+ ar << m_lastPlayed;
+ ar << m_dateAdded;
+ ar << m_strComment;
+ ar << (int)m_musicRoles.size();
+ for (const auto& credit : m_musicRoles)
+ {
+ ar << credit.GetRoleId();
+ ar << credit.GetRoleDesc();
+ ar << credit.GetArtist();
+ ar << credit.GetArtistId();
+ }
+ ar << m_strMood;
+ ar << m_strRecordLabel;
+ ar << m_Rating;
+ ar << m_Userrating;
+ ar << m_Votes;
+ ar << m_iTimesPlayed;
+ ar << m_iAlbumId;
+ ar << m_iDbId;
+ ar << m_type;
+ ar << m_strReleaseStatus;
+ ar << m_strLyrics;
+ ar << m_bCompilation;
+ ar << m_listeners;
+ ar << m_coverArt;
+ ar << m_cuesheet;
+ ar << static_cast<int>(m_albumReleaseType);
+ ar << m_iBPM;
+ ar << m_samplerate;
+ ar << m_bitrate;
+ ar << m_channels;
+ }
+ else
+ {
+ ar >> m_strURL;
+ ar >> m_strTitle;
+ ar >> m_artist;
+ ar >> m_strArtistSort;
+ ar >> m_strArtistDesc;
+ ar >> m_strAlbum;
+ ar >> m_albumArtist;
+ ar >> m_strAlbumArtistDesc;
+ ar >> m_genre;
+ ar >> m_iDuration;
+ ar >> m_iTrack;
+ ar >> m_bLoaded;
+ ar >> m_strReleaseDate;
+ ar >> m_strOriginalDate;
+ ar >> m_strMusicBrainzTrackID;
+ ar >> m_musicBrainzArtistID;
+ ar >> m_strMusicBrainzAlbumID;
+ ar >> m_strMusicBrainzReleaseGroupID;
+ ar >> m_musicBrainzAlbumArtistID;
+ ar >> m_strDiscSubtitle;
+ ar >> m_bBoxset;
+ ar >> m_iDiscTotal;
+ ar >> m_strMusicBrainzReleaseType;
+ ar >> m_lastPlayed;
+ ar >> m_dateAdded;
+ ar >> m_strComment;
+ int iMusicRolesSize;
+ ar >> iMusicRolesSize;
+ m_musicRoles.reserve(iMusicRolesSize);
+ for (int i = 0; i < iMusicRolesSize; ++i)
+ {
+ int idRole;
+ int idArtist;
+ std::string strArtist;
+ std::string strRole;
+ ar >> idRole;
+ ar >> strRole;
+ ar >> strArtist;
+ ar >> idArtist;
+ m_musicRoles.emplace_back(idRole, strRole, strArtist, idArtist);
+ }
+ ar >> m_strMood;
+ ar >> m_strRecordLabel;
+ ar >> m_Rating;
+ ar >> m_Userrating;
+ ar >> m_Votes;
+ ar >> m_iTimesPlayed;
+ ar >> m_iAlbumId;
+ ar >> m_iDbId;
+ ar >> m_type;
+ ar >> m_strReleaseStatus;
+ ar >> m_strLyrics;
+ ar >> m_bCompilation;
+ ar >> m_listeners;
+ ar >> m_coverArt;
+ ar >> m_cuesheet;
+
+ int albumReleaseType;
+ ar >> albumReleaseType;
+ m_albumReleaseType = static_cast<CAlbum::ReleaseType>(albumReleaseType);
+ ar >> m_iBPM;
+ ar >> m_samplerate;
+ ar >> m_bitrate;
+ ar >> m_channels;
+ }
+}
+
+void CMusicInfoTag::Clear()
+{
+ m_strURL.clear();
+ m_artist.clear();
+ m_strArtistSort.clear();
+ m_strComposerSort.clear();
+ m_strAlbum.clear();
+ m_albumArtist.clear();
+ m_genre.clear();
+ m_strTitle.clear();
+ m_strMusicBrainzTrackID.clear();
+ m_musicBrainzArtistID.clear();
+ m_strMusicBrainzAlbumID.clear();
+ m_strMusicBrainzReleaseGroupID.clear();
+ m_musicBrainzAlbumArtistID.clear();
+ m_strMusicBrainzReleaseType.clear();
+ m_musicRoles.clear();
+ m_iDuration = 0;
+ m_iTrack = 0;
+ m_bLoaded = false;
+ m_lastPlayed.Reset();
+ m_dateAdded.Reset();
+ m_dateNew.Reset();
+ m_dateUpdated.Reset();
+ m_bCompilation = false;
+ m_bBoxset = false;
+ m_strDiscSubtitle.clear();
+ m_strComment.clear();
+ m_strMood.clear();
+ m_strRecordLabel.clear();
+ m_cuesheet.clear();
+ m_iDbId = -1;
+ m_type.clear();
+ m_strReleaseStatus.clear();
+ m_iTimesPlayed = 0;
+ m_strReleaseDate.clear();
+ m_strOriginalDate.clear();
+ m_iAlbumId = -1;
+ m_coverArt.Clear();
+ m_replayGain = ReplayGain();
+ m_albumReleaseType = CAlbum::Album;
+ m_listeners = 0;
+ m_Rating = 0;
+ m_Userrating = 0;
+ m_Votes = 0;
+ m_iDiscTotal = 0;
+ m_iBPM = 0;
+ m_samplerate = 0;
+ m_bitrate = 0;
+ m_channels = 0;
+ m_stationName.clear();
+ m_stationArt.clear();
+}
+
+void CMusicInfoTag::AppendArtist(const std::string &artist)
+{
+ for (unsigned int index = 0; index < m_artist.size(); index++)
+ {
+ if (StringUtils::EqualsNoCase(artist, m_artist.at(index)))
+ return;
+ }
+
+ m_artist.push_back(artist);
+}
+
+void CMusicInfoTag::AppendAlbumArtist(const std::string &albumArtist)
+{
+ for (unsigned int index = 0; index < m_albumArtist.size(); index++)
+ {
+ if (StringUtils::EqualsNoCase(albumArtist, m_albumArtist.at(index)))
+ return;
+ }
+
+ m_albumArtist.push_back(albumArtist);
+}
+
+void CMusicInfoTag::AppendGenre(const std::string &genre)
+{
+ for (unsigned int index = 0; index < m_genre.size(); index++)
+ {
+ if (StringUtils::EqualsNoCase(genre, m_genre.at(index)))
+ return;
+ }
+
+ m_genre.push_back(genre);
+}
+
+void CMusicInfoTag::AddArtistRole(const std::string& Role, const std::string& strArtist)
+{
+ if (!strArtist.empty() && !Role.empty())
+ AddArtistRole(Role, StringUtils::Split(strArtist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CMusicInfoTag::AddArtistRole(const std::string& Role, const std::vector<std::string>& artists)
+{
+ for (unsigned int index = 0; index < artists.size(); index++)
+ {
+ CMusicRole ArtistCredit(Role, Trim(artists.at(index)));
+ //Prevent duplicate entries
+ auto credit = find(m_musicRoles.begin(), m_musicRoles.end(), ArtistCredit);
+ if (credit == m_musicRoles.end())
+ m_musicRoles.push_back(ArtistCredit);
+ }
+}
+
+void CMusicInfoTag::AppendArtistRole(const CMusicRole& ArtistRole)
+{
+ //Append contributor, no check for duplicates as from database
+ m_musicRoles.push_back(ArtistRole);
+}
+
+const std::string CMusicInfoTag::GetArtistStringForRole(const std::string& strRole) const
+{
+ std::vector<std::string> artistvector;
+ for (const auto& credit : m_musicRoles)
+ {
+ if (StringUtils::EqualsNoCase(credit.GetRoleDesc(), strRole))
+ artistvector.push_back(credit.GetArtist());
+ }
+ return StringUtils::Join(artistvector, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+}
+
+const std::string CMusicInfoTag::GetContributorsText() const
+{
+ std::string strLabel;
+ for (const auto& credit : m_musicRoles)
+ {
+ strLabel += StringUtils::Format("{}\n", credit.GetArtist());
+ }
+ return StringUtils::TrimRight(strLabel, "\n");
+}
+
+const std::string CMusicInfoTag::GetContributorsAndRolesText() const
+{
+ std::string strLabel;
+ for (const auto& credit : m_musicRoles)
+ {
+ strLabel += StringUtils::Format("{} - {}\n", credit.GetRoleDesc(), credit.GetArtist());
+ }
+ return StringUtils::TrimRight(strLabel, "\n");
+}
+
+
+const VECMUSICROLES &CMusicInfoTag::GetContributors() const
+{
+ return m_musicRoles;
+}
+
+void CMusicInfoTag::SetContributors(const VECMUSICROLES& contributors)
+{
+ m_musicRoles = contributors;
+}
+
+std::string CMusicInfoTag::Trim(const std::string &value) const
+{
+ std::string trimmedValue(value);
+ StringUtils::TrimLeft(trimmedValue, " ");
+ StringUtils::TrimRight(trimmedValue, " \n\r");
+ return trimmedValue;
+}
diff --git a/xbmc/music/tags/MusicInfoTag.h b/xbmc/music/tags/MusicInfoTag.h
new file mode 100644
index 0000000..3c1b994
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTag.h
@@ -0,0 +1,263 @@
+/*
+ * 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
+
+class CSong;
+class CArtist;
+class CVariant;
+
+#include "ReplayGain.h"
+#include "XBDateTime.h"
+#include "music/Album.h"
+#include "utils/IArchivable.h"
+#include "utils/ISerializable.h"
+#include "utils/ISortable.h"
+
+#include <string>
+#include <vector>
+
+namespace MUSIC_INFO
+{
+class CMusicInfoTag final : public IArchivable, public ISerializable, public ISortable
+{
+public:
+ CMusicInfoTag(void);
+ bool operator !=(const CMusicInfoTag& tag) const;
+ bool Loaded() const;
+ const std::string& GetTitle() const;
+ const std::string& GetURL() const;
+ const std::vector<std::string>& GetArtist() const;
+ const std::string& GetArtistSort() const;
+ const std::string GetArtistString() const;
+ const std::string& GetComposerSort() const;
+ const std::string& GetAlbum() const;
+ int GetAlbumId() const;
+ const std::vector<std::string>& GetAlbumArtist() const;
+ const std::string GetAlbumArtistString() const;
+ const std::string& GetAlbumArtistSort() const;
+ const std::vector<std::string>& GetGenre() const;
+ int GetTrackNumber() const;
+ int GetDiscNumber() const;
+ int GetTrackAndDiscNumber() const;
+ int GetTotalDiscs() const;
+ int GetDuration() const; // may be set even if Loaded() returns false
+ int GetYear() const;
+ const std::string& GetReleaseDate() const;
+ const std::string GetReleaseYear() const;
+ const std::string& GetOriginalDate() const;
+ const std::string GetOriginalYear() const;
+ int GetDatabaseId() const;
+ const std::string &GetType() const;
+ const std::string& GetDiscSubtitle() const;
+ int GetBPM() const;
+ std::string GetYearString() const;
+ const std::string& GetMusicBrainzTrackID() const;
+ const std::vector<std::string>& GetMusicBrainzArtistID() const;
+ const std::vector<std::string>& GetMusicBrainzArtistHints() const;
+ const std::string& GetMusicBrainzAlbumID() const;
+ const std::string& GetMusicBrainzReleaseGroupID() const;
+ const std::vector<std::string>& GetMusicBrainzAlbumArtistID() const;
+ const std::vector<std::string>& GetMusicBrainzAlbumArtistHints() const;
+ const std::string& GetMusicBrainzReleaseType() const;
+ const std::string& GetComment() const;
+ const std::string& GetMood() const;
+ const std::string& GetRecordLabel() const;
+ const std::string& GetLyrics() const;
+ const std::string& GetCueSheet() const;
+ const CDateTime& GetLastPlayed() const;
+ const CDateTime& GetDateAdded() const;
+ bool GetCompilation() const;
+ bool GetBoxset() const;
+ float GetRating() const;
+ int GetUserrating() const;
+ int GetVotes() const;
+ int GetListeners() const;
+ int GetPlayCount() const;
+ int GetBitRate() const;
+ int GetNoOfChannels() const;
+ int GetSampleRate() const;
+ const std::string& GetAlbumReleaseStatus() const;
+ const std::string& GetStationName() const;
+ const std::string& GetStationArt() const;
+ const EmbeddedArtInfo &GetCoverArtInfo() const;
+ const ReplayGain& GetReplayGain() const;
+ CAlbum::ReleaseType GetAlbumReleaseType() const;
+
+ void SetURL(const std::string& strURL);
+ void SetTitle(const std::string& strTitle);
+ void SetArtist(const std::string& strArtist);
+ void SetArtist(const std::vector<std::string>& artists, bool FillDesc = false);
+ void SetArtistDesc(const std::string& strArtistDesc);
+ void SetArtistSort(const std::string& strArtistsort);
+ void SetComposerSort(const std::string& strComposerSort);
+ void SetAlbum(const std::string& strAlbum);
+ void SetAlbumId(const int iAlbumId);
+ void SetAlbumArtist(const std::string& strAlbumArtist);
+ void SetAlbumArtist(const std::vector<std::string>& albumArtists, bool FillDesc = false);
+ void SetAlbumArtistDesc(const std::string& strAlbumArtistDesc);
+ void SetAlbumArtistSort(const std::string& strAlbumArtistSort);
+ void SetGenre(const std::string& strGenre, bool bTrim = false);
+ void SetGenre(const std::vector<std::string>& genres, bool bTrim = false);
+ void SetYear(int year);
+ void SetOriginalDate(const std::string& strOriginalDate);
+ void SetReleaseDate(const std::string& strReleaseDate);
+ void SetDatabaseId(int id, const std::string &type);
+ void SetTrackNumber(int iTrack);
+ void SetDiscNumber(int iDiscNumber);
+ void SetTrackAndDiscNumber(int iTrackAndDisc);
+ void SetDuration(int iSec);
+ void SetLoaded(bool bOnOff = true);
+ void SetArtist(const CArtist& artist);
+ void SetAlbum(const CAlbum& album);
+ void SetSong(const CSong& song);
+ void SetMusicBrainzTrackID(const std::string& strTrackID);
+ void SetMusicBrainzArtistID(const std::vector<std::string>& musicBrainzArtistId);
+ void SetMusicBrainzArtistHints(const std::vector<std::string>& musicBrainzArtistHints);
+ void SetMusicBrainzAlbumID(const std::string& strAlbumID);
+ void SetMusicBrainzAlbumArtistID(const std::vector<std::string>& musicBrainzAlbumArtistId);
+ void SetMusicBrainzAlbumArtistHints(const std::vector<std::string>& musicBrainzAlbumArtistHints);
+ void SetMusicBrainzReleaseGroupID(const std::string& strReleaseGroupID);
+ void SetMusicBrainzReleaseType(const std::string& ReleaseType);
+ void SetComment(const std::string& comment);
+ void SetMood(const std::string& mood);
+ void SetRecordLabel(const std::string& publisher);
+ void SetLyrics(const std::string& lyrics);
+ void SetCueSheet(const std::string& cueSheet);
+ void SetRating(float rating);
+ void SetUserrating(int rating);
+ void SetVotes(int votes);
+ void SetListeners(int listeners);
+ void SetPlayCount(int playcount);
+ void SetLastPlayed(const std::string& strLastPlayed);
+ void SetLastPlayed(const CDateTime& strLastPlayed);
+ void SetDateAdded(const std::string& strDateAdded);
+ void SetDateAdded(const CDateTime& dateAdded);
+ void SetDateUpdated(const std::string& strDateUpdated);
+ void SetDateUpdated(const CDateTime& dateUpdated);
+ void SetDateNew(const std::string& strDateNew);
+ void SetDateNew(const CDateTime& dateNew);
+ void SetCompilation(bool compilation);
+ void SetBoxset(bool boxset);
+ void SetCoverArtInfo(size_t size, const std::string &mimeType);
+ void SetReplayGain(const ReplayGain& aGain);
+ void SetAlbumReleaseType(CAlbum::ReleaseType releaseType);
+ void SetType(const MediaType& mediaType);
+ void SetDiscSubtitle(const std::string& strDiscSubtitle);
+ void SetTotalDiscs(int iDiscTotal);
+ void SetBPM(int iBPM);
+ void SetBitRate(int bitrate);
+ void SetNoOfChannels(int channels);
+ void SetSampleRate(int samplerate);
+ void SetAlbumReleaseStatus(const std::string& strReleaseStatus);
+ void SetStationName(const std::string& strStationName); // name of online radio station
+ void SetStationArt(const std::string& strStationArt);
+
+ /*! \brief Append a unique artist to the artist list
+ Checks if we have this artist already added, and if not adds it to the songs artist list.
+ \param value artist to add.
+ */
+ void AppendArtist(const std::string &artist);
+
+ /*! \brief Append a unique album artist to the artist list
+ Checks if we have this album artist already added, and if not adds it to the songs album artist list.
+ \param albumArtist album artist to add.
+ */
+ void AppendAlbumArtist(const std::string &albumArtist);
+
+ /*! \brief Append a unique genre to the genre list
+ Checks if we have this genre already added, and if not adds it to the songs genre list.
+ \param genre genre to add.
+ */
+ void AppendGenre(const std::string &genre);
+ void AddOriginalDate(const std::string& strDateYear);
+ void AddReleaseDate(const std::string& strDateYear, bool isMonth = false);
+
+ void AddArtistRole(const std::string& Role, const std::string& strArtist);
+ void AddArtistRole(const std::string& Role, const std::vector<std::string>& artists);
+ void AppendArtistRole(const CMusicRole& ArtistRole);
+ const std::string GetArtistStringForRole(const std::string& strRole) const;
+ const std::string GetContributorsText() const;
+ const std::string GetContributorsAndRolesText() const;
+ const VECMUSICROLES &GetContributors() const;
+ void SetContributors(const VECMUSICROLES& contributors);
+ bool HasContributors() const { return !m_musicRoles.empty(); }
+
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& ar) const override;
+ void ToSortable(SortItem& sortable, Field field) const override;
+
+ void Clear();
+
+protected:
+ /*! \brief Trim whitespace off the given string
+ \param value string to trim
+ \return trimmed value, with spaces removed from left and right, as well as carriage returns from the right.
+ */
+ std::string Trim(const std::string &value) const;
+
+ std::string m_strURL;
+ std::string m_strTitle;
+ std::vector<std::string> m_artist;
+ std::string m_strArtistSort;
+ std::string m_strArtistDesc;
+ std::string m_strComposerSort;
+ std::string m_strAlbum;
+ std::vector<std::string> m_albumArtist;
+ std::string m_strAlbumArtistDesc;
+ std::string m_strAlbumArtistSort;
+ std::vector<std::string> m_genre;
+ std::string m_strMusicBrainzTrackID;
+ std::vector<std::string> m_musicBrainzArtistID;
+ std::vector<std::string> m_musicBrainzArtistHints;
+ std::string m_strMusicBrainzAlbumID;
+ std::vector<std::string> m_musicBrainzAlbumArtistID;
+ std::vector<std::string> m_musicBrainzAlbumArtistHints;
+ std::string m_strMusicBrainzReleaseGroupID;
+ std::string m_strMusicBrainzReleaseType;
+ VECMUSICROLES m_musicRoles; //Artists contributing to the recording and role (from tags other than ARTIST or ALBUMARTIST)
+ std::string m_strComment;
+ std::string m_strMood;
+ std::string m_strRecordLabel;
+ std::string m_strLyrics;
+ std::string m_cuesheet;
+ std::string m_strDiscSubtitle;
+ std::string m_strReleaseDate; //ISO8601 date YYYY, YYYY-MM or YYYY-MM-DD
+ std::string m_strOriginalDate; //ISO8601 date YYYY, YYYY-MM or YYYY-MM-DD
+ CDateTime m_lastPlayed;
+ CDateTime m_dateNew;
+ CDateTime m_dateAdded;
+ CDateTime m_dateUpdated;
+ bool m_bCompilation;
+ int m_iDuration;
+ int m_iTrack; // consists of the disk number in the high 16 bits, the track number in the low 16bits
+ int m_iDbId;
+ MediaType m_type; ///< item type "music", "song", "album", "artist"
+ bool m_bLoaded;
+ float m_Rating;
+ int m_Userrating;
+ int m_Votes;
+ int m_listeners;
+ int m_iTimesPlayed;
+ int m_iAlbumId;
+ int m_iDiscTotal;
+ bool m_bBoxset;
+ int m_iBPM;
+ CAlbum::ReleaseType m_albumReleaseType;
+ std::string m_strReleaseStatus;
+ int m_samplerate;
+ int m_channels;
+ int m_bitrate;
+ std::string m_stationName;
+ std::string m_stationArt; // Used to fetch thumb URL for Shoutcasts
+
+ EmbeddedArtInfo m_coverArt; ///< art information
+
+ ReplayGain m_replayGain; ///< ReplayGain information
+};
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderCDDA.cpp b/xbmc/music/tags/MusicInfoTagLoaderCDDA.cpp
new file mode 100644
index 0000000..66938d3
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderCDDA.cpp
@@ -0,0 +1,156 @@
+/*
+ * 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 "MusicInfoTagLoaderCDDA.h"
+
+#include "MusicInfoTag.h"
+#include "ServiceBroker.h"
+#include "network/cddb.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/log.h"
+
+using namespace MUSIC_INFO;
+
+#ifdef HAS_DVD_DRIVE
+using namespace MEDIA_DETECT;
+using namespace CDDB;
+#endif
+
+//! @todo - remove after Ubuntu 16.04 (Xenial) is EOL
+#if !defined(LIBCDIO_VERSION_NUM) || (LIBCDIO_VERSION_NUM <= 83)
+#define CDTEXT_FIELD_TITLE CDTEXT_TITLE
+#define CDTEXT_FIELD_PERFORMER CDTEXT_PERFORMER
+#define CDTEXT_FIELD_GENRE CDTEXT_GENRE
+#endif
+
+CMusicInfoTagLoaderCDDA::CMusicInfoTagLoaderCDDA(void) = default;
+
+CMusicInfoTagLoaderCDDA::~CMusicInfoTagLoaderCDDA() = default;
+
+bool CMusicInfoTagLoaderCDDA::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art)
+{
+#ifdef HAS_DVD_DRIVE
+ try
+ {
+ tag.SetURL(strFileName);
+ bool bResult = false;
+
+ // Get information for the inserted disc
+ CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
+ if (pCdInfo == NULL)
+ return bResult;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // Prepare cddb
+ Xcddb cddb;
+ cddb.setCacheDir(profileManager->GetCDDBFolder());
+
+ int iTrack = atoi(strFileName.substr(13, strFileName.size() - 13 - 5).c_str());
+
+ // duration is always available
+ tag.SetDuration( ( pCdInfo->GetTrackInformation(iTrack).nMins * 60 )
+ + pCdInfo->GetTrackInformation(iTrack).nSecs );
+
+ // Only load cached cddb info in this tag loader, the internet database query is made in CCDDADirectory
+ if (pCdInfo->HasCDDBInfo() && cddb.isCDCached(pCdInfo))
+ {
+ // get cddb information
+ if (cddb.queryCDinfo(pCdInfo))
+ {
+ // Fill the fileitems music tag with cddb information, if available
+ const std::string& strTitle = cddb.getTrackTitle(iTrack);
+ if (!strTitle.empty())
+ {
+ // Tracknumber
+ tag.SetTrackNumber(iTrack);
+
+ // Title
+ tag.SetTitle(strTitle);
+
+ // Artist: Use track artist or disc artist
+ std::string strArtist = cddb.getTrackArtist(iTrack);
+ if (strArtist.empty())
+ cddb.getDiskArtist(strArtist);
+ tag.SetArtist(strArtist);
+
+ // Album
+ std::string strAlbum;
+ cddb.getDiskTitle( strAlbum );
+ tag.SetAlbum(strAlbum);
+
+ // Album Artist
+ std::string strAlbumArtist;
+ cddb.getDiskArtist(strAlbumArtist);
+ tag.SetAlbumArtist(strAlbumArtist);
+
+ // Year
+ tag.SetReleaseDate(cddb.getYear());
+
+ // Genre
+ tag.SetGenre( cddb.getGenre() );
+
+ tag.SetLoaded(true);
+ bResult = true;
+ }
+ }
+ }
+ else
+ {
+ // No cddb info, maybe we have CD-Text
+ trackinfo ti = pCdInfo->GetTrackInformation(iTrack);
+
+ // Fill the fileitems music tag with CD-Text information, if available
+ std::string strTitle = ti.cdtext[CDTEXT_FIELD_TITLE];
+ if (!strTitle.empty())
+ {
+ // Tracknumber
+ tag.SetTrackNumber(iTrack);
+
+ // Title
+ tag.SetTitle(strTitle);
+
+ // Get info for track zero, as we may have and need CD-Text Album info
+ xbmc_cdtext_t discCDText = pCdInfo->GetDiscCDTextInformation();
+
+ // Artist: Use track artist or disc artist
+ std::string strArtist = ti.cdtext[CDTEXT_FIELD_PERFORMER];
+ if (strArtist.empty())
+ strArtist = discCDText[CDTEXT_FIELD_PERFORMER];
+ tag.SetArtist(strArtist);
+
+ // Album
+ std::string strAlbum;
+ strAlbum = discCDText[CDTEXT_FIELD_TITLE];
+ tag.SetAlbum(strAlbum);
+
+ // Genre: use track or disc genre
+ std::string strGenre = ti.cdtext[CDTEXT_FIELD_GENRE];
+ if (strGenre.empty())
+ strGenre = discCDText[CDTEXT_FIELD_GENRE];
+ tag.SetGenre( strGenre );
+
+ tag.SetLoaded(true);
+ bResult = true;
+ }
+ }
+ return bResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Tag loader CDDB: exception in file {}", strFileName);
+ }
+
+#endif
+
+ tag.SetLoaded(false);
+
+ return false;
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderCDDA.h b/xbmc/music/tags/MusicInfoTagLoaderCDDA.h
new file mode 100644
index 0000000..95fc35f
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderCDDA.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "ImusicInfoTagLoader.h"
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTagLoaderCDDA: public IMusicInfoTagLoader
+ {
+ public:
+ CMusicInfoTagLoaderCDDA(void);
+ ~CMusicInfoTagLoaderCDDA() override;
+
+ bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override;
+ };
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderDatabase.cpp b/xbmc/music/tags/MusicInfoTagLoaderDatabase.cpp
new file mode 100644
index 0000000..1409809
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderDatabase.cpp
@@ -0,0 +1,38 @@
+/*
+ * 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 "MusicInfoTagLoaderDatabase.h"
+
+#include "MusicInfoTag.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
+#include "music/MusicDatabase.h"
+
+using namespace MUSIC_INFO;
+
+CMusicInfoTagLoaderDatabase::CMusicInfoTagLoaderDatabase(void) = default;
+
+CMusicInfoTagLoaderDatabase::~CMusicInfoTagLoaderDatabase() = default;
+
+bool CMusicInfoTagLoaderDatabase::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art)
+{
+ tag.SetLoaded(false);
+ CMusicDatabase database;
+ database.Open();
+ XFILE::MUSICDATABASEDIRECTORY::CQueryParams param;
+ XFILE::MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(strFileName,param);
+
+ CSong song;
+ if (database.GetSong(param.GetSongId(),song))
+ tag.SetSong(song);
+
+ database.Close();
+
+ return tag.Loaded();
+}
+
diff --git a/xbmc/music/tags/MusicInfoTagLoaderDatabase.h b/xbmc/music/tags/MusicInfoTagLoaderDatabase.h
new file mode 100644
index 0000000..85272e5
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderDatabase.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "ImusicInfoTagLoader.h"
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTagLoaderDatabase: public IMusicInfoTagLoader
+ {
+ public:
+ CMusicInfoTagLoaderDatabase(void);
+ ~CMusicInfoTagLoaderDatabase() override;
+
+ bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override;
+ };
+}
+
diff --git a/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.cpp b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.cpp
new file mode 100644
index 0000000..9bb2cdc
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.cpp
@@ -0,0 +1,168 @@
+/*
+ * 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 "MusicInfoTagLoaderFFmpeg.h"
+
+#include "MusicInfoTag.h"
+#include "cores/FFmpeg.h"
+#include "filesystem/File.h"
+#include "utils/StringUtils.h"
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+
+static int vfs_file_read(void *h, uint8_t* buf, int size)
+{
+ CFile* pFile = static_cast<CFile*>(h);
+ return pFile->Read(buf, size);
+}
+
+static int64_t vfs_file_seek(void *h, int64_t pos, int whence)
+{
+ CFile* pFile = static_cast<CFile*>(h);
+ if (whence == AVSEEK_SIZE)
+ return pFile->GetLength();
+ else
+ return pFile->Seek(pos, whence & ~AVSEEK_FORCE);
+}
+
+CMusicInfoTagLoaderFFmpeg::CMusicInfoTagLoaderFFmpeg(void) = default;
+
+CMusicInfoTagLoaderFFmpeg::~CMusicInfoTagLoaderFFmpeg() = default;
+
+bool CMusicInfoTagLoaderFFmpeg::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art)
+{
+ tag.SetLoaded(false);
+
+ CFile file;
+ if (!file.Open(strFileName))
+ return false;
+
+ int bufferSize = 4096;
+ int blockSize = file.GetChunkSize();
+ if (blockSize > 1)
+ bufferSize = blockSize;
+ uint8_t* buffer = (uint8_t*)av_malloc(bufferSize);
+ AVIOContext* ioctx = avio_alloc_context(buffer, bufferSize, 0,
+ &file, vfs_file_read, NULL,
+ vfs_file_seek);
+
+ AVFormatContext* fctx = avformat_alloc_context();
+ fctx->pb = ioctx;
+
+ if (file.IoControl(IOCTRL_SEEK_POSSIBLE, NULL) != 1)
+ ioctx->seekable = 0;
+
+ AVInputFormat* iformat=NULL;
+ av_probe_input_buffer(ioctx, &iformat, strFileName.c_str(), NULL, 0, 0);
+
+ if (avformat_open_input(&fctx, strFileName.c_str(), iformat, NULL) < 0)
+ {
+ if (fctx)
+ avformat_close_input(&fctx);
+ av_free(ioctx->buffer);
+ av_free(ioctx);
+ return false;
+ }
+
+ /* ffmpeg supports the return of ID3v2 metadata but has its own naming system
+ for some, but not all, of the keys. In particular the key for the conductor
+ tag TPE3 is called "performer".
+ See https://github.com/xbmc/FFmpeg/blob/master/libavformat/id3v2.c#L43
+ Other keys are retuened using their 4 char name.
+ Only single frame values are returned even for v2.4 fomart tags e.g. while
+ tagged with multiple TPE1 frames "artist1", "artist2", "artist3" only
+ "artist1" is returned by ffmpeg.
+ Hence, like with v2.3 format tags, multiple values for artist, genre etc.
+ need to be combined when tagging into a single value using a known item
+ separator e.g. "artist1 / artist2 / artist3"
+
+ Any changes to ID3v2 tag processing in CTagLoaderTagLib need to be
+ repeated here
+ */
+ auto&& ParseTag = [&tag](AVDictionaryEntry* avtag)
+ {
+ if (StringUtils::CompareNoCase(avtag->key, "album") == 0)
+ tag.SetAlbum(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "artist") == 0)
+ tag.SetArtist(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "album_artist") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "album artist") == 0)
+ tag.SetAlbumArtist(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "title") == 0)
+ tag.SetTitle(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "genre") == 0)
+ tag.SetGenre(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "part_number") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "track") == 0)
+ tag.SetTrackNumber(
+ static_cast<int>(strtol(avtag->value, nullptr, 10)));
+ else if (StringUtils::CompareNoCase(avtag->key, "disc") == 0)
+ tag.SetDiscNumber(
+ static_cast<int>(strtol(avtag->value, nullptr, 10)));
+ else if (StringUtils::CompareNoCase(avtag->key, "date") == 0)
+ tag.SetReleaseDate(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "compilation") == 0)
+ tag.SetCompilation((strtol(avtag->value, nullptr, 10) == 0) ? false : true);
+ else if (StringUtils::CompareNoCase(avtag->key, "encoded_by") == 0) {}
+ else if (StringUtils::CompareNoCase(avtag->key, "composer") == 0)
+ tag.AddArtistRole("Composer", avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "performer") == 0) // Conductor or TPE3 tag
+ tag.AddArtistRole("Conductor", avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "TEXT") == 0)
+ tag.AddArtistRole("Lyricist", avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "TPE4") == 0)
+ tag.AddArtistRole("Remixer", avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "LABEL") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TPUB") == 0)
+ tag.SetRecordLabel(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "copyright") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TCOP") == 0) {} // Copyright message
+ else if (StringUtils::CompareNoCase(avtag->key, "TDRC") == 0)
+ tag.SetReleaseDate(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "TDOR") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TORY") == 0)
+ tag.SetOriginalDate(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key , "TDAT") == 0)
+ tag.AddReleaseDate(avtag->value, true); // MMDD part
+ else if (StringUtils::CompareNoCase(avtag->key, "TYER") == 0)
+ tag.AddReleaseDate(avtag->value); // YYYY part
+ else if (StringUtils::CompareNoCase(avtag->key, "TBPM") == 0)
+ tag.SetBPM(static_cast<int>(strtol(avtag->value, nullptr, 10)));
+ else if (StringUtils::CompareNoCase(avtag->key, "TDTG") == 0) {} // Tagging time
+ else if (StringUtils::CompareNoCase(avtag->key, "language") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TLAN") == 0) {} // Languages
+ else if (StringUtils::CompareNoCase(avtag->key, "mood") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TMOO") == 0)
+ tag.SetMood(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "artist-sort") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "TSOP") == 0) {}
+ else if (StringUtils::CompareNoCase(avtag->key, "TSO2") == 0) {} // Album artist sort
+ else if (StringUtils::CompareNoCase(avtag->key, "TSOC") == 0) {} // composer sort
+ else if (StringUtils::CompareNoCase(avtag->key, "TSST") == 0)
+ tag.SetDiscSubtitle(avtag->value);
+ };
+
+ AVDictionaryEntry* avtag=nullptr;
+ while ((avtag = av_dict_get(fctx->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX)))
+ ParseTag(avtag);
+
+ const AVStream* st = fctx->streams[0];
+ if (st)
+ while ((avtag = av_dict_get(st->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX)))
+ ParseTag(avtag);
+
+ if (!tag.GetTitle().empty())
+ tag.SetLoaded(true);
+
+ avformat_close_input(&fctx);
+ av_free(ioctx->buffer);
+ av_free(ioctx);
+
+ return true;
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.h b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.h
new file mode 100644
index 0000000..272c065
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderFFmpeg.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "ImusicInfoTagLoader.h"
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTagLoaderFFmpeg: public IMusicInfoTagLoader
+ {
+ public:
+ CMusicInfoTagLoaderFFmpeg(void);
+ ~CMusicInfoTagLoaderFFmpeg() override;
+
+ bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override;
+ };
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderFactory.cpp b/xbmc/music/tags/MusicInfoTagLoaderFactory.cpp
new file mode 100644
index 0000000..56b2c17
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderFactory.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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 "MusicInfoTagLoaderFactory.h"
+
+#include "FileItem.h"
+#include "MusicInfoTagLoaderCDDA.h"
+#include "MusicInfoTagLoaderDatabase.h"
+#include "MusicInfoTagLoaderFFmpeg.h"
+#include "MusicInfoTagLoaderShn.h"
+#include "ServiceBroker.h"
+#include "TagLoaderTagLib.h"
+#include "addons/AudioDecoder.h"
+#include "addons/ExtsMimeSupportList.h"
+#include "addons/addoninfo/AddonType.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace KODI::ADDONS;
+using namespace MUSIC_INFO;
+
+CMusicInfoTagLoaderFactory::CMusicInfoTagLoaderFactory() = default;
+
+CMusicInfoTagLoaderFactory::~CMusicInfoTagLoaderFactory() = default;
+
+IMusicInfoTagLoader* CMusicInfoTagLoaderFactory::CreateLoader(const CFileItem& item)
+{
+ // dont try to read the tags for streams & shoutcast
+ if (item.IsInternetStream())
+ return NULL;
+
+ if (item.IsMusicDb())
+ return new CMusicInfoTagLoaderDatabase();
+
+ std::string strExtension = URIUtils::GetExtension(item.GetPath());
+ StringUtils::ToLower(strExtension);
+ StringUtils::TrimLeft(strExtension, ".");
+
+ if (strExtension.empty())
+ return NULL;
+
+ const auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetExtensionSupportedAddonInfos(
+ "." + strExtension, CExtsMimeSupportList::FilterSelect::hasTags);
+ for (const auto& addonInfo : addonInfos)
+ {
+ if (addonInfo.first == ADDON::AddonType::AUDIODECODER)
+ {
+ std::unique_ptr<CAudioDecoder> result = std::make_unique<CAudioDecoder>(addonInfo.second);
+ if (!result->CreateDecoder() && result->SupportsFile(item.GetPath()))
+ continue;
+
+ return result.release();
+ }
+ }
+
+ if (strExtension == "aac" || strExtension == "ape" || strExtension == "mac" ||
+ strExtension == "mp3" || strExtension == "wma" || strExtension == "flac" ||
+ strExtension == "m4a" || strExtension == "mp4" || strExtension == "m4b" ||
+ strExtension == "m4v" || strExtension == "mpc" || strExtension == "mpp" ||
+ strExtension == "mp+" || strExtension == "ogg" || strExtension == "oga" ||
+ strExtension == "opus" || strExtension == "aif" || strExtension == "aiff" ||
+ strExtension == "wav" || strExtension == "mod" || strExtension == "s3m" ||
+ strExtension == "it" || strExtension == "xm" || strExtension == "wv")
+ {
+ CTagLoaderTagLib *pTagLoader = new CTagLoaderTagLib();
+ return pTagLoader;
+ }
+#ifdef HAS_DVD_DRIVE
+ else if (strExtension == "cdda")
+ {
+ CMusicInfoTagLoaderCDDA *pTagLoader = new CMusicInfoTagLoaderCDDA();
+ return pTagLoader;
+ }
+#endif
+ else if (strExtension == "shn")
+ {
+ CMusicInfoTagLoaderSHN *pTagLoader = new CMusicInfoTagLoaderSHN();
+ return pTagLoader;
+ }
+ else if (strExtension == "mka" || strExtension == "dsf" ||
+ strExtension == "dff")
+ return new CMusicInfoTagLoaderFFmpeg();
+
+ return NULL;
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderFactory.h b/xbmc/music/tags/MusicInfoTagLoaderFactory.h
new file mode 100644
index 0000000..ba2aba2
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderFactory.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "ImusicInfoTagLoader.h"
+
+class CFileItem; // forward
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTagLoaderFactory
+ {
+ public:
+ CMusicInfoTagLoaderFactory(void);
+ virtual ~CMusicInfoTagLoaderFactory();
+
+ static IMusicInfoTagLoader* CreateLoader(const CFileItem& item);
+ };
+}
+
diff --git a/xbmc/music/tags/MusicInfoTagLoaderShn.cpp b/xbmc/music/tags/MusicInfoTagLoaderShn.cpp
new file mode 100644
index 0000000..2b9bd18
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderShn.cpp
@@ -0,0 +1,38 @@
+/*
+ * 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 "MusicInfoTagLoaderShn.h"
+
+#include "MusicInfoTag.h"
+#include "utils/log.h"
+
+using namespace MUSIC_INFO;
+
+CMusicInfoTagLoaderSHN::CMusicInfoTagLoaderSHN(void) = default;
+
+CMusicInfoTagLoaderSHN::~CMusicInfoTagLoaderSHN() = default;
+
+bool CMusicInfoTagLoaderSHN::Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art)
+{
+ try
+ {
+
+ tag.SetURL(strFileName);
+ tag.SetDuration((long)0); //! @todo Use libavformat to calculate duration.
+ tag.SetLoaded(false);
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "Tag loader shn: exception in file {}", strFileName);
+ }
+
+ tag.SetLoaded(false);
+ return false;
+}
diff --git a/xbmc/music/tags/MusicInfoTagLoaderShn.h b/xbmc/music/tags/MusicInfoTagLoaderShn.h
new file mode 100644
index 0000000..d932320
--- /dev/null
+++ b/xbmc/music/tags/MusicInfoTagLoaderShn.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "ImusicInfoTagLoader.h"
+
+namespace MUSIC_INFO
+{
+
+class CMusicInfoTagLoaderSHN: public IMusicInfoTagLoader
+{
+public:
+ CMusicInfoTagLoaderSHN(void);
+ ~CMusicInfoTagLoaderSHN() override;
+
+ bool Load(const std::string& strFileName, CMusicInfoTag& tag, EmbeddedArt *art = NULL) override;
+};
+}
diff --git a/xbmc/music/tags/ReplayGain.cpp b/xbmc/music/tags/ReplayGain.cpp
new file mode 100644
index 0000000..c9947f4
--- /dev/null
+++ b/xbmc/music/tags/ReplayGain.cpp
@@ -0,0 +1,144 @@
+/*
+ * 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 "ReplayGain.h"
+
+#include "utils/StringUtils.h"
+
+#include <stdlib.h>
+
+static bool TypeIsValid(ReplayGain::Type aType)
+{
+ return (aType > ReplayGain::NONE && aType <= ReplayGain::TRACK);
+}
+
+static int index(ReplayGain::Type aType)
+{
+ return static_cast<int>(aType) - 1;
+}
+
+///////////////////////////////////////////////////////////////
+// class ReplayGain
+///////////////////////////////////////////////////////////////
+
+const ReplayGain::Info& ReplayGain::Get(Type aType) const
+{
+ if (TypeIsValid(aType))
+ return m_data[index(aType)];
+
+ static Info invalid;
+ return invalid;
+}
+
+void ReplayGain::Set(Type aType, const Info& aInfo)
+{
+ if (TypeIsValid(aType))
+ m_data[index(aType)] = aInfo;
+}
+
+void ReplayGain::ParseGain(Type aType, const std::string& aStrGain)
+{
+ if (TypeIsValid(aType))
+ m_data[index(aType)].SetGain(aStrGain);
+}
+
+void ReplayGain::SetGain(Type aType, float aGain)
+{
+ if (TypeIsValid(aType))
+ m_data[index(aType)].SetGain(aGain);
+}
+
+void ReplayGain::ParsePeak(Type aType, const std::string& aStrPeak)
+{
+ if (TypeIsValid(aType))
+ m_data[index(aType)].SetPeak(aStrPeak);
+}
+
+void ReplayGain::SetPeak(Type aType, float aPeak)
+{
+ if (TypeIsValid(aType))
+ m_data[index(aType)].SetPeak(aPeak);
+}
+
+std::string ReplayGain::Get() const
+{
+ if (!Get(ALBUM).Valid() && !Get(TRACK).Valid())
+ return std::string();
+
+ std::string rg;
+ if (Get(ALBUM).Valid())
+ rg = StringUtils::Format("{:.3f},{:.3f},", Get(ALBUM).Gain(), Get(ALBUM).Peak());
+ else
+ rg = "-1000, -1,";
+ if (Get(TRACK).Valid())
+ rg += StringUtils::Format("{:.3f},{:.3f}", Get(TRACK).Gain(), Get(TRACK).Peak());
+ else
+ rg += "-1000, -1";
+ return rg;
+}
+
+void ReplayGain::Set(const std::string& strReplayGain)
+{
+ std::vector<std::string> values = StringUtils::Split(strReplayGain, ",");
+ if (values.size() == 4)
+ {
+ ParseGain(ALBUM, values[0]);
+ ParsePeak(ALBUM, values[1]);
+ ParseGain(TRACK, values[2]);
+ ParsePeak(TRACK, values[3]);
+ }
+}
+
+///////////////////////////////////////////////////////////////
+// class ReplayGain::Info
+///////////////////////////////////////////////////////////////
+
+void ReplayGain::Info::SetGain(float aGain)
+{
+ m_gain = aGain;
+}
+
+void ReplayGain::Info::SetGain(const std::string& aStrGain)
+{
+ SetGain(static_cast<float>(atof(aStrGain.c_str())));
+}
+
+float ReplayGain::Info::Gain() const
+{
+ return m_gain;
+}
+
+void ReplayGain::Info::SetPeak(const std::string& aStrPeak)
+{
+ SetPeak(static_cast<float>(atof(aStrPeak.c_str())));
+}
+
+void ReplayGain::Info::SetPeak(float aPeak)
+{
+ m_peak = aPeak;
+}
+
+float ReplayGain::Info::Peak() const
+{
+ return m_peak;
+}
+
+bool ReplayGain::Info::HasGain() const
+{
+ return m_gain != REPLAY_GAIN_NO_GAIN;
+}
+
+bool ReplayGain::Info::HasPeak() const
+{
+ return m_peak != REPLAY_GAIN_NO_PEAK;
+}
+
+bool ReplayGain::Info::Valid() const
+{
+ return HasPeak() && HasGain();
+}
diff --git a/xbmc/music/tags/ReplayGain.h b/xbmc/music/tags/ReplayGain.h
new file mode 100644
index 0000000..efcd784
--- /dev/null
+++ b/xbmc/music/tags/ReplayGain.h
@@ -0,0 +1,51 @@
+/*
+ * 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 <string>
+
+#define REPLAY_GAIN_NO_PEAK -1.0f
+#define REPLAY_GAIN_NO_GAIN -1000.0f
+
+class ReplayGain
+{
+public:
+ enum Type {
+ NONE = 0,
+ ALBUM,
+ TRACK
+ };
+public:
+ class Info
+ {
+ public:
+ void SetGain(float aGain);
+ void SetGain(const std::string& aStrGain);
+ float Gain() const;
+ void SetPeak(const std::string& aStrPeak);
+ void SetPeak(float aPeak);
+ float Peak() const;
+ bool HasGain() const;
+ bool HasPeak() const;
+ bool Valid() const;
+ private:
+ float m_gain = REPLAY_GAIN_NO_GAIN; // measured in milliBels
+ float m_peak = REPLAY_GAIN_NO_PEAK; // 1.0 == full digital scale
+ };
+ const Info& Get(Type aType) const;
+ void Set(Type aType, const Info& aInfo);
+ void ParseGain(Type aType, const std::string& aStrGain);
+ void SetGain(Type aType, float aGain);
+ void ParsePeak(Type aType, const std::string& aStrPeak);
+ void SetPeak(Type aType, float aPeak);
+ std::string Get() const;
+ void Set(const std::string& strReplayGain);
+private:
+ Info m_data[TRACK];
+};
diff --git a/xbmc/music/tags/TagLibVFSStream.cpp b/xbmc/music/tags/TagLibVFSStream.cpp
new file mode 100644
index 0000000..1d2b454
--- /dev/null
+++ b/xbmc/music/tags/TagLibVFSStream.cpp
@@ -0,0 +1,321 @@
+/*
+ * 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 "TagLibVFSStream.h"
+
+#include "filesystem/File.h"
+
+#include <limits.h>
+
+#include <taglib/tiostream.h>
+
+using namespace XFILE;
+using namespace TagLib;
+using namespace MUSIC_INFO;
+
+/*!
+ * Construct a File object and opens the \a file. \a file should be a
+ * be an XBMC Vfile.
+ */
+TagLibVFSStream::TagLibVFSStream(const std::string& strFileName, bool readOnly)
+{
+ m_bIsOpen = true;
+ if (readOnly)
+ {
+ if (!m_file.Open(strFileName))
+ m_bIsOpen = false;
+ }
+ else
+ {
+ if (!m_file.OpenForWrite(strFileName))
+ m_bIsOpen = false;
+ }
+ m_strFileName = strFileName;
+ m_bIsReadOnly = readOnly || !m_bIsOpen;
+}
+
+/*!
+ * Destroys this ByteVectorStream instance.
+ */
+TagLibVFSStream::~TagLibVFSStream()
+{
+ m_file.Close();
+}
+
+/*!
+ * Returns the file name in the local file system encoding.
+ */
+FileName TagLibVFSStream::name() const
+{
+ return m_strFileName.c_str();
+}
+
+/*!
+ * Reads a block of size \a length at the current get pointer.
+ */
+ByteVector TagLibVFSStream::readBlock(TagLib::ulong length)
+{
+ ByteVector byteVector(static_cast<TagLib::uint>(length));
+ ssize_t read = m_file.Read(byteVector.data(), length);
+ if (read > 0)
+ byteVector.resize(read);
+ else
+ byteVector.clear();
+
+ return byteVector;
+}
+
+/*!
+ * Attempts to write the block \a data at the current get pointer. If the
+ * file is currently only opened read only -- i.e. readOnly() returns true --
+ * this attempts to reopen the file in read/write mode.
+ *
+ * \note This should be used instead of using the streaming output operator
+ * for a ByteVector. And even this function is significantly slower than
+ * doing output with a char[].
+ */
+void TagLibVFSStream::writeBlock(const ByteVector &data)
+{
+ m_file.Write(data.data(), data.size());
+}
+
+/*!
+ * Insert \a data at position \a start in the file overwriting \a replace
+ * bytes of the original content.
+ *
+ * \note This method is slow since it requires rewriting all of the file
+ * after the insertion point.
+ */
+void TagLibVFSStream::insert(const ByteVector &data, TagLib::ulong start, TagLib::ulong replace)
+{
+ if (data.size() == replace)
+ {
+ seek(start);
+ writeBlock(data);
+ return;
+ }
+ else if (data.size() < replace)
+ {
+ seek(start);
+ writeBlock(data);
+ removeBlock(start + data.size(), replace - data.size());
+ }
+
+ // Woohoo! Faster (about 20%) than id3lib at last. I had to get hardcore
+ // and avoid TagLib's high level API for rendering just copying parts of
+ // the file that don't contain tag data.
+ //
+ // Now I'll explain the steps in this ugliness:
+
+ // First, make sure that we're working with a buffer that is longer than
+ // the *difference* in the tag sizes. We want to avoid overwriting parts
+ // that aren't yet in memory, so this is necessary.
+ TagLib::ulong bufferLength = bufferSize();
+
+ while (data.size() - replace > bufferLength)
+ bufferLength += bufferSize();
+
+ // Set where to start the reading and writing.
+ long readPosition = start + replace;
+ long writePosition = start;
+ ByteVector buffer;
+ ByteVector aboutToOverwrite(static_cast<TagLib::uint>(bufferLength));
+
+ // This is basically a special case of the loop below. Here we're just
+ // doing the same steps as below, but since we aren't using the same buffer
+ // size -- instead we're using the tag size -- this has to be handled as a
+ // special case. We're also using File::writeBlock() just for the tag.
+ // That's a bit slower than using char *'s so, we're only doing it here.
+ seek(readPosition);
+ ssize_t bytesRead = m_file.Read(aboutToOverwrite.data(), bufferLength);
+ if (bytesRead <= 0)
+ return; // error
+ readPosition += bufferLength;
+
+ seek(writePosition);
+ writeBlock(data);
+ writePosition += data.size();
+
+ buffer = aboutToOverwrite;
+ buffer.resize(bytesRead);
+
+ // Ok, here's the main loop. We want to loop until the read fails, which
+ // means that we hit the end of the file.
+ while (!buffer.isEmpty())
+ {
+ // Seek to the current read position and read the data that we're about
+ // to overwrite. Appropriately increment the readPosition.
+ seek(readPosition);
+ bytesRead = m_file.Read(aboutToOverwrite.data(), bufferLength);
+ if (bytesRead <= 0)
+ return; // error
+ aboutToOverwrite.resize(bytesRead);
+ readPosition += bufferLength;
+
+ // Check to see if we just read the last block. We need to call clear()
+ // if we did so that the last write succeeds.
+ if (TagLib::ulong(bytesRead) < bufferLength)
+ clear();
+
+ // Seek to the write position and write our buffer. Increment the
+ // writePosition.
+ seek(writePosition);
+ if (m_file.Write(buffer.data(), buffer.size()) < static_cast<ssize_t>(buffer.size()))
+ return; // error
+ writePosition += buffer.size();
+
+ buffer = aboutToOverwrite;
+ bufferLength = bytesRead;
+ }
+}
+
+/*!
+ * Removes a block of the file starting a \a start and continuing for
+ * \a length bytes.
+ *
+ * \note This method is slow since it involves rewriting all of the file
+ * after the removed portion.
+ */
+void TagLibVFSStream::removeBlock(TagLib::ulong start, TagLib::ulong length)
+{
+ TagLib::ulong bufferLength = bufferSize();
+
+ long readPosition = start + length;
+ long writePosition = start;
+
+ ByteVector buffer(static_cast<TagLib::uint>(bufferLength));
+
+ TagLib::ulong bytesRead = 1;
+
+ while(bytesRead != 0)
+ {
+ seek(readPosition);
+ ssize_t read = m_file.Read(buffer.data(), bufferLength);
+ if (read < 0)
+ return;// explicit error
+
+ bytesRead = static_cast<TagLib::ulong>(read);
+ readPosition += bytesRead;
+
+ // Check to see if we just read the last block. We need to call clear()
+ // if we did so that the last write succeeds.
+ if(bytesRead < bufferLength)
+ clear();
+
+ seek(writePosition);
+ if (m_file.Write(buffer.data(), bytesRead) != static_cast<ssize_t>(bytesRead))
+ return; // error
+ writePosition += bytesRead;
+ }
+ truncate(writePosition);
+}
+
+/*!
+ * Returns true if the file is read only (or if the file can not be opened).
+ */
+bool TagLibVFSStream::readOnly() const
+{
+ return m_bIsReadOnly;
+}
+
+/*!
+ * Since the file can currently only be opened as an argument to the
+ * constructor (sort-of by design), this returns if that open succeeded.
+ */
+bool TagLibVFSStream::isOpen() const
+{
+ return m_bIsOpen;
+}
+
+/*!
+ * Move the I/O pointer to \a offset in the file from position \a p. This
+ * defaults to seeking from the beginning of the file.
+ *
+ * \see Position
+ */
+void TagLibVFSStream::seek(long offset, Position p)
+{
+ const long fileLen = length();
+ if (m_bIsReadOnly && fileLen > 0)
+ {
+ long startPos;
+ if (p == Beginning)
+ startPos = 0;
+ else if (p == Current)
+ startPos = tell();
+ else if (p == End)
+ startPos = fileLen;
+ else
+ return; // wrong Position value
+
+ // When parsing some broken files, taglib may try to seek above end of file.
+ // If underlying VFS does not move I/O pointer in this case, taglib will parse
+ // same part of file several times and ends with error. To prevent this
+ // situation, force seek to last valid position so VFS move I/O pointer.
+ if (startPos >= 0)
+ {
+ if (offset < 0 && startPos + offset < 0)
+ {
+ m_file.Seek(0, SEEK_SET);
+ return;
+ }
+ if (offset > 0 && startPos + offset > fileLen)
+ {
+ m_file.Seek(fileLen, SEEK_SET);
+ return;
+ }
+ }
+ }
+
+ switch(p)
+ {
+ case Beginning:
+ m_file.Seek(offset, SEEK_SET);
+ break;
+ case Current:
+ m_file.Seek(offset, SEEK_CUR);
+ break;
+ case End:
+ m_file.Seek(offset, SEEK_END);
+ break;
+ }
+}
+
+/*!
+ * Reset the end-of-file and error flags on the file.
+ */
+void TagLibVFSStream::clear()
+{
+}
+
+/*!
+ * Returns the current offset within the file.
+ */
+long TagLibVFSStream::tell() const
+{
+ int64_t pos = m_file.GetPosition();
+ if(pos > LONG_MAX)
+ return -1;
+ else
+ return (long)pos;
+}
+
+/*!
+ * Returns the length of the file.
+ */
+long TagLibVFSStream::length()
+{
+ return (long)m_file.GetLength();
+}
+
+/*!
+ * Truncates the file to a \a length.
+ */
+void TagLibVFSStream::truncate(long length)
+{
+ m_file.Truncate(length);
+}
diff --git a/xbmc/music/tags/TagLibVFSStream.h b/xbmc/music/tags/TagLibVFSStream.h
new file mode 100644
index 0000000..d567055
--- /dev/null
+++ b/xbmc/music/tags/TagLibVFSStream.h
@@ -0,0 +1,122 @@
+/*
+ * 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 "filesystem/File.h"
+
+#include <taglib/tiostream.h>
+
+namespace MUSIC_INFO
+{
+ class TagLibVFSStream : public TagLib::IOStream
+ {
+ public:
+ /*!
+ * Construct a File object and opens the \a file. \a file should be a
+ * be an XBMC Vfile.
+ */
+ TagLibVFSStream(const std::string& strFileName, bool readOnly);
+
+ /*!
+ * Destroys this ByteVectorStream instance.
+ */
+ ~TagLibVFSStream() override;
+
+ /*!
+ * Returns the file name in the local file system encoding.
+ */
+ TagLib::FileName name() const override;
+
+ /*!
+ * Reads a block of size \a length at the current get pointer.
+ */
+ TagLib::ByteVector readBlock(TagLib::ulong length) override;
+
+ /*!
+ * Attempts to write the block \a data at the current get pointer. If the
+ * file is currently only opened read only -- i.e. readOnly() returns true --
+ * this attempts to reopen the file in read/write mode.
+ *
+ * \note This should be used instead of using the streaming output operator
+ * for a ByteVector. And even this function is significantly slower than
+ * doing output with a char[].
+ */
+ void writeBlock(const TagLib::ByteVector &data) override;
+
+ /*!
+ * Insert \a data at position \a start in the file overwriting \a replace
+ * bytes of the original content.
+ *
+ * \note This method is slow since it requires rewriting all of the file
+ * after the insertion point.
+ */
+ void insert(const TagLib::ByteVector &data, TagLib::ulong start = 0, TagLib::ulong replace = 0) override;
+
+ /*!
+ * Removes a block of the file starting a \a start and continuing for
+ * \a length bytes.
+ *
+ * \note This method is slow since it involves rewriting all of the file
+ * after the removed portion.
+ */
+ void removeBlock(TagLib::ulong start = 0, TagLib::ulong length = 0) override;
+
+ /*!
+ * Returns true if the file is read only (or if the file can not be opened).
+ */
+ bool readOnly() const override;
+
+ /*!
+ * Since the file can currently only be opened as an argument to the
+ * constructor (sort-of by design), this returns if that open succeeded.
+ */
+ bool isOpen() const override;
+
+ /*!
+ * Move the I/O pointer to \a offset in the file from position \a p. This
+ * defaults to seeking from the beginning of the file.
+ *
+ * \see Position
+ */
+ void seek(long offset, TagLib::IOStream::Position p = Beginning) override;
+
+ /*!
+ * Reset the end-of-file and error flags on the file.
+ */
+ void clear() override;
+
+ /*!
+ * Returns the current offset within the file.
+ */
+ long tell() const override;
+
+ /*!
+ * Returns the length of the file.
+ */
+ long length() override;
+
+ /*!
+ * Truncates the file to a \a length.
+ */
+ void truncate(long length) override;
+
+ protected:
+ /*!
+ * Returns the buffer size that is used for internal buffering.
+ */
+ static TagLib::uint bufferSize() { return 1024; }
+
+ private:
+ std::string m_strFileName;
+ XFILE::CFile m_file;
+ bool m_bIsReadOnly;
+ bool m_bIsOpen;
+ };
+}
+
diff --git a/xbmc/music/tags/TagLoaderTagLib.cpp b/xbmc/music/tags/TagLoaderTagLib.cpp
new file mode 100644
index 0000000..6eb591a
--- /dev/null
+++ b/xbmc/music/tags/TagLoaderTagLib.cpp
@@ -0,0 +1,1371 @@
+/*
+ * 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 "TagLoaderTagLib.h"
+
+#include <vector>
+
+#include <taglib/id3v1tag.h>
+#include <taglib/apetag.h>
+#include <taglib/asftag.h>
+#include <taglib/id3v1genres.h>
+#include <taglib/aifffile.h>
+#include <taglib/apefile.h>
+#include <taglib/asffile.h>
+#include <taglib/modfile.h>
+#include <taglib/mp4file.h>
+#include <taglib/mpegfile.h>
+#include <taglib/oggfile.h>
+#include <taglib/oggflacfile.h>
+#include <taglib/opusfile.h>
+#include <taglib/rifffile.h>
+#include <taglib/speexfile.h>
+#include <taglib/s3mfile.h>
+#include <taglib/trueaudiofile.h>
+#include <taglib/vorbisfile.h>
+#include <taglib/wavfile.h>
+#include <taglib/wavpackfile.h>
+#include <taglib/xmfile.h>
+#include <taglib/flacfile.h>
+#include <taglib/itfile.h>
+#include <taglib/mpcfile.h>
+#include <taglib/id3v2tag.h>
+#include <taglib/xiphcomment.h>
+#include <taglib/mp4tag.h>
+
+#include <taglib/textidentificationframe.h>
+#include <taglib/uniquefileidentifierframe.h>
+#include <taglib/popularimeterframe.h>
+#include <taglib/commentsframe.h>
+#include <taglib/unsynchronizedlyricsframe.h>
+#include <taglib/attachedpictureframe.h>
+
+#include <taglib/tstring.h>
+#include <taglib/tpropertymap.h>
+
+#include "TagLibVFSStream.h"
+#include "MusicInfoTag.h"
+#include "ReplayGain.h"
+#include "utils/RegExp.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "utils/StringUtils.h"
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+
+#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11
+#include "utils/Base64.h"
+#endif
+
+using namespace TagLib;
+using namespace MUSIC_INFO;
+
+namespace
+{
+std::vector<std::string> StringListToVectorString(const StringList& stringList)
+{
+ std::vector<std::string> values;
+ for (const auto& it: stringList)
+ values.push_back(it.to8Bit(true));
+ return values;
+}
+
+std::vector<std::string> GetASFStringList(const List<ASF::Attribute>& list)
+{
+ std::vector<std::string> values;
+ for (const auto& at: list)
+ values.push_back(at.toString().to8Bit(true));
+ return values;
+}
+
+std::vector<std::string> GetID3v2StringList(const ID3v2::FrameList& frameList)
+{
+ auto frame = dynamic_cast<const ID3v2::TextIdentificationFrame *>(frameList.front());
+ if (frame)
+ return StringListToVectorString(frame->fieldList());
+ return std::vector<std::string>();
+}
+
+void SetFlacArt(FLAC::File *flacFile, EmbeddedArt *art, CMusicInfoTag &tag)
+{
+ FLAC::Picture *cover[2] = {};
+ auto pictures = flacFile->pictureList();
+ for (List<FLAC::Picture *>::ConstIterator i = pictures.begin(); i != pictures.end(); ++i)
+ {
+ FLAC::Picture *picture = *i;
+ if (picture->type() == FLAC::Picture::FrontCover)
+ cover[0] = picture;
+ else // anything else is taken as second priority
+ cover[1] = picture;
+ }
+ for (const FLAC::Picture* const c : cover)
+ {
+ if (c)
+ {
+ tag.SetCoverArtInfo(c->data().size(), c->mimeType().to8Bit(true));
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t*>(c->data().data()), c->data().size(), c->mimeType().to8Bit(true));
+ return; // one is enough
+ }
+ }
+}
+}
+
+bool CTagLoaderTagLib::Load(const std::string& strFileName, MUSIC_INFO::CMusicInfoTag& tag, EmbeddedArt *art /* = NULL */)
+{
+ return Load(strFileName, tag, "", art);
+}
+
+
+template<>
+bool CTagLoaderTagLib::ParseTag(ASF::Tag *asf, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!asf)
+ return false;
+
+ ReplayGain replayGainInfo;
+ tag.SetTitle(asf->title().to8Bit(true));
+ const ASF::AttributeListMap& attributeListMap = asf->attributeListMap();
+ for (ASF::AttributeListMap::ConstIterator it = attributeListMap.begin(); it != attributeListMap.end(); ++it)
+ {
+ if (it->first == "Author")
+ SetArtist(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/ArtistSortOrder")
+ SetArtistSort(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/AlbumArtist")
+ SetAlbumArtist(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/AlbumArtistSortOrder")
+ SetAlbumArtistSort(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/ComposerSortOrder")
+ SetComposerSort(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/AlbumTitle")
+ tag.SetAlbum(it->second.front().toString().to8Bit(true));
+ else if (it->first == "WM/TrackNumber" ||
+ it->first == "WM/Track")
+ {
+ if (it->second.front().type() == ASF::Attribute::DWordType)
+ tag.SetTrackNumber(it->second.front().toUInt());
+ else
+ tag.SetTrackNumber(atoi(it->second.front().toString().toCString(true)));
+ }
+ else if (it->first == "WM/PartOfSet")
+ tag.SetDiscNumber(atoi(it->second.front().toString().toCString(true)));
+ else if (it->first == "WM/Genre")
+ SetGenre(tag, GetASFStringList(it->second));
+ else if (it->first == "WM/Mood")
+ tag.SetMood(it->second.front().toString().to8Bit(true));
+ else if (it->first == "WM/Composer")
+ AddArtistRole(tag, "Composer", GetASFStringList(it->second));
+ else if (it->first == "WM/Conductor")
+ AddArtistRole(tag, "Conductor", GetASFStringList(it->second));
+ //No ASF/WMA tag from Taglib for "ensemble"
+ else if (it->first == "WM/Writer")
+ AddArtistRole(tag, "Lyricist", GetASFStringList(it->second));
+ else if (it->first == "WM/ModifiedBy")
+ AddArtistRole(tag, "Remixer", GetASFStringList(it->second));
+ else if (it->first == "WM/Engineer")
+ AddArtistRole(tag, "Engineer", GetASFStringList(it->second));
+ else if (it->first == "WM/Producer")
+ AddArtistRole(tag, "Producer", GetASFStringList(it->second));
+ else if (it->first == "WM/DJMixer")
+ AddArtistRole(tag, "DJMixer", GetASFStringList(it->second));
+ else if (it->first == "WM/Mixer")
+ AddArtistRole(tag, "mixer", GetASFStringList(it->second));
+ else if (it->first == "WM/Publisher")
+ tag.SetRecordLabel(it->second.front().toString().to8Bit(true));
+ else if (it->first == "WM/Script")
+ {} // Known unsupported, suppress warnings
+ else if (it->first == "WM/Year")
+ tag.SetReleaseDate(it->second.front().toString().to8Bit(true));
+ else if (it->first == "WM/OriginalReleaseYear")
+ tag.SetOriginalDate(it->second.front().toString().to8Bit(true));
+ else if (it->first == "WM/SetSubTitle")
+ tag.SetDiscSubtitle(it->second.front().toString().to8Bit(true));
+ else if (it->first == "MusicBrainz/Artist Id")
+ tag.SetMusicBrainzArtistID(SplitMBID(GetASFStringList(it->second)));
+ else if (it->first == "MusicBrainz/Album Id")
+ tag.SetMusicBrainzAlbumID(it->second.front().toString().to8Bit(true));
+ else if (it->first == "MusicBrainz/Release Group Id")
+ tag.SetMusicBrainzReleaseGroupID(it->second.front().toString().to8Bit(true));
+ else if (it->first == "MusicBrainz/Album Artist")
+ SetAlbumArtist(tag, GetASFStringList(it->second));
+ else if (it->first == "MusicBrainz/Album Artist Id")
+ tag.SetMusicBrainzAlbumArtistID(SplitMBID(GetASFStringList(it->second)));
+ else if (it->first == "MusicBrainz/Track Id")
+ tag.SetMusicBrainzTrackID(it->second.front().toString().to8Bit(true));
+ else if (it->first == "MusicBrainz/Album Status")
+ tag.SetAlbumReleaseStatus(it->second.front().toString().toCString(true));
+ else if (it->first == "MusicBrainz/Album Type")
+ SetReleaseType(tag, GetASFStringList(it->second));
+ else if (it->first == "MusicIP/PUID")
+ {}
+ else if (it->first == "WM/BeatsPerMinute")
+ tag.SetBPM(atoi(it->second.front().toString().toCString(true)));
+ else if (it->first == "replaygain_track_gain" || it->first == "REPLAYGAIN_TRACK_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.front().toString().toCString(true));
+ else if (it->first == "replaygain_album_gain" || it->first == "REPLAYGAIN_ALBUM_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.front().toString().toCString(true));
+ else if (it->first == "replaygain_track_peak" || it->first == "REPLAYGAIN_TRACK_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.front().toString().toCString(true));
+ else if (it->first == "replaygain_album_peak" || it->first == "REPLAYGAIN_ALBUM_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.front().toString().toCString(true));
+ else if (it->first == "WM/Picture")
+ { // picture
+ ASF::Picture pic = it->second.front().toPicture();
+ tag.SetCoverArtInfo(pic.picture().size(), pic.mimeType().toCString());
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t *>(pic.picture().data()), pic.picture().size(), pic.mimeType().toCString());
+ }
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX)
+ CLog::Log(LOGDEBUG, "unrecognized ASF tag name: {}", it->first.toCString(true));
+ }
+ // artist may be specified in the ContentDescription block rather than using the 'Author' attribute.
+ if (tag.GetArtist().empty())
+ tag.SetArtist(asf->artist().toCString(true));
+
+ if (!asf->comment().isEmpty())
+ tag.SetComment(asf->comment().toCString(true));
+ tag.SetReplayGain(replayGainInfo);
+ tag.SetLoaded(true);
+ return true;
+}
+
+int CTagLoaderTagLib::POPMtoXBMC(int popm)
+{
+ // Ratings:
+ // FROM: http://www.mediamonkey.com/forum/viewtopic.php?f=7&t=40532&start=30#p391067
+ // The following schemes are used by the other POPM-compatible players:
+ // WMP/Vista: "Windows Media Player 9 Series" ratings:
+ // 1 = 1, 2 = 64, 3=128, 4=196 (not 192), 5=255
+ // MediaMonkey (v4.2.1): "no@email" ratings:
+ // 0.5=13, 1=1, 1.5=54, 2=64, 2.5=118,
+ // 3=128, 3.5=186, 4=196, 4.5=242, 5=255
+ // Note 1 star written as 1 while half a star is 13, a higher value
+ // Accommodate these mapped values in a scale from 0-255
+ if (popm == 0) return 0;
+ if (popm == 1) return 2;
+ if (popm < 23) return 1;
+ if (popm < 32) return 2;
+ if (popm < 64) return 3;
+ if (popm < 96) return 4;
+ if (popm < 128) return 5;
+ if (popm < 160) return 6;
+ if (popm < 196) return 7;
+ if (popm < 224) return 8;
+ if (popm < 255) return 9;
+ else return 10;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(ID3v1::Tag *id3v1, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!id3v1) return false;
+ tag.SetTitle(id3v1->title().to8Bit(true));
+ tag.SetArtist(id3v1->artist().to8Bit(true));
+ tag.SetAlbum(id3v1->album().to8Bit(true));
+ tag.SetComment(id3v1->comment().to8Bit(true));
+ tag.SetGenre(id3v1->genre().to8Bit(true), true);
+ tag.SetYear(id3v1->year());
+ tag.SetTrackNumber(id3v1->track());
+ return true;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(ID3v2::Tag *id3v2, EmbeddedArt *art, MUSIC_INFO::CMusicInfoTag& tag)
+{
+ if (!id3v2) return false;
+ ReplayGain replayGainInfo;
+
+ ID3v2::AttachedPictureFrame *pictures[3] = {};
+ const ID3v2::FrameListMap& frameListMap = id3v2->frameListMap();
+ for (ID3v2::FrameListMap::ConstIterator it = frameListMap.begin(); it != frameListMap.end(); ++it)
+ {
+ // It is possible that the taglist is empty. In that case no useable values can be extracted.
+ // and we should skip the tag.
+ if (it->second.isEmpty()) continue;
+
+ if (it->first == "TPE1") SetArtist(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TSOP") SetArtistSort(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TALB") tag.SetAlbum(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TPE2") SetAlbumArtist(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TSO2") SetAlbumArtistSort(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TSOC") SetComposerSort(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TIT2") tag.SetTitle(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TCON") SetGenre(tag, GetID3v2StringList(it->second));
+ else if (it->first == "TRCK")
+ tag.SetTrackNumber(
+ static_cast<int>(strtol(it->second.front()->toString().toCString(true), nullptr, 10)));
+ else if (it->first == "TPOS")
+ tag.SetDiscNumber(
+ static_cast<int>(strtol(it->second.front()->toString().toCString(true), nullptr, 10)));
+ else if (it->first == "TDOR" || it->first == "TORY") // TDOR - ID3v2.4, TORY - ID3v2.3
+ tag.SetOriginalDate(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TDAT") {} // empty as taglib has moved the value to TDRC
+ else if (it->first == "TCMP") tag.SetCompilation((strtol(it->second.front()->toString().toCString(true), nullptr, 10) == 0) ? false : true);
+ else if (it->first == "TENC") {} // EncodedBy
+ else if (it->first == "TCOM") AddArtistRole(tag, "Composer", GetID3v2StringList(it->second));
+ else if (it->first == "TPE3") AddArtistRole(tag, "Conductor", GetID3v2StringList(it->second));
+ else if (it->first == "TEXT") AddArtistRole(tag, "Lyricist", GetID3v2StringList(it->second));
+ else if (it->first == "TPE4") AddArtistRole(tag, "Remixer", GetID3v2StringList(it->second));
+ else if (it->first == "TPUB") tag.SetRecordLabel(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TCOP") {} // Copyright message
+ else if (it->first == "TDRC") // taglib concatenates TYER & TDAT into this field if v2.3
+ tag.SetReleaseDate(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TDRL") {} // Not set by Picard or used in community generally
+ else if (it->first == "TDTG") {} // Tagging time
+ else if (it->first == "TLAN") {} // Languages
+ else if (it->first == "TMOO") tag.SetMood(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TSST")
+ tag.SetDiscSubtitle(it->second.front()->toString().to8Bit(true));
+ else if (it->first == "TBPM")
+ tag.SetBPM(
+ static_cast<int>(strtol(it->second.front()->toString().toCString(true), nullptr, 10)));
+ else if (it->first == "USLT")
+ // Loop through any lyrics frames. Could there be multiple frames, how to choose?
+ for (ID3v2::FrameList::ConstIterator lt = it->second.begin(); lt != it->second.end(); ++lt)
+ {
+ auto lyricsFrame = dynamic_cast<ID3v2::UnsynchronizedLyricsFrame *> (*lt);
+ if (lyricsFrame)
+ tag.SetLyrics(lyricsFrame->text().to8Bit(true));
+ }
+ else if (it->first == "COMM")
+ // Loop through and look for the main (no description) comment
+ for (ID3v2::FrameList::ConstIterator ct = it->second.begin(); ct != it->second.end(); ++ct)
+ {
+ ID3v2::CommentsFrame *commentsFrame = dynamic_cast<ID3v2::CommentsFrame *> (*ct);
+ if (commentsFrame && commentsFrame->description().isEmpty())
+ tag.SetComment(commentsFrame->text().to8Bit(true));
+ }
+ else if (it->first == "TXXX")
+ // Loop through and process the UserTextIdentificationFrames
+ for (ID3v2::FrameList::ConstIterator ut = it->second.begin(); ut != it->second.end(); ++ut)
+ {
+ ID3v2::UserTextIdentificationFrame *frame = dynamic_cast<ID3v2::UserTextIdentificationFrame *> (*ut);
+ if (!frame) continue;
+
+ // First field is the same as the description
+ StringList stringList = frame->fieldList();
+ if (stringList.size() == 1) continue;
+ stringList.erase(stringList.begin());
+ String desc = frame->description().upper();
+ if (desc == "MUSICBRAINZ ARTIST ID")
+ tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(stringList)));
+ else if (desc == "MUSICBRAINZ ALBUM ID")
+ tag.SetMusicBrainzAlbumID(stringList.front().to8Bit(true));
+ else if (desc == "MUSICBRAINZ RELEASE GROUP ID")
+ tag.SetMusicBrainzReleaseGroupID(stringList.front().to8Bit(true));
+ else if (desc == "MUSICBRAINZ ALBUM ARTIST ID")
+ tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(stringList)));
+ else if (desc == "MUSICBRAINZ ALBUM ARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(stringList));
+ else if (desc == "MUSICBRAINZ ALBUM TYPE")
+ SetReleaseType(tag, StringListToVectorString(stringList));
+ else if (desc == "MUSICBRAINZ ALBUM STATUS")
+ tag.SetAlbumReleaseStatus(stringList.front().to8Bit(true));
+ else if (desc == "REPLAYGAIN_TRACK_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::TRACK, stringList.front().toCString(true));
+ else if (desc == "REPLAYGAIN_ALBUM_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::ALBUM, stringList.front().toCString(true));
+ else if (desc == "REPLAYGAIN_TRACK_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::TRACK, stringList.front().toCString(true));
+ else if (desc == "REPLAYGAIN_ALBUM_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::ALBUM, stringList.front().toCString(true));
+ else if (desc == "ALBUMARTIST" || desc == "ALBUM ARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(stringList));
+ else if (desc == "ALBUMARTISTSORT" || desc == "ALBUM ARTIST SORT")
+ SetAlbumArtistSort(tag, StringListToVectorString(stringList));
+ else if (desc == "ARTISTS")
+ SetArtistHints(tag, StringListToVectorString(stringList));
+ else if (desc == "ALBUMARTISTS" || desc == "ALBUM ARTISTS")
+ SetAlbumArtistHints(tag, StringListToVectorString(stringList));
+ else if (desc == "WRITER") // How Picard >1.3 tags writer in ID3
+ AddArtistRole(tag, "Writer", StringListToVectorString(stringList));
+ else if (desc == "COMPOSERSORT" || desc == "COMPOSER SORT")
+ SetComposerSort(tag, StringListToVectorString(stringList));
+ else if (desc == "MOOD")
+ tag.SetMood(stringList.front().to8Bit(true));
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX)
+ CLog::Log(LOGDEBUG, "unrecognized user text tag detected: TXXX:{}",
+ frame->description().toCString(true));
+ }
+ else if (it->first == "TIPL")
+ // Loop through and process the involved people list
+ // For example Arranger, Engineer, Producer, DJMixer or Mixer
+ // In fieldlist every odd field is a function, and every even is an artist or a comma delimited list of artists.
+ for (ID3v2::FrameList::ConstIterator ip = it->second.begin(); ip != it->second.end(); ++ip)
+ {
+ auto tiplframe = dynamic_cast<ID3v2::TextIdentificationFrame*> (*ip);
+ if (tiplframe)
+ AddArtistRole(tag, StringListToVectorString(tiplframe->fieldList()));
+ }
+ else if (it->first == "TMCL")
+ // Loop through and process the musician credits list
+ // It is a mapping between the instrument and the person that played it, but also includes "orchestra" or "soloist".
+ // In fieldlist every odd field is an instrument, and every even is an artist or a comma delimited list of artists.
+ for (ID3v2::FrameList::ConstIterator ip = it->second.begin(); ip != it->second.end(); ++ip)
+ {
+ auto tiplframe = dynamic_cast<ID3v2::TextIdentificationFrame*> (*ip);
+ if (tiplframe)
+ AddArtistRole(tag, StringListToVectorString(tiplframe->fieldList()));
+ }
+ else if (it->first == "UFID")
+ // Loop through any UFID frames and set them
+ for (ID3v2::FrameList::ConstIterator ut = it->second.begin(); ut != it->second.end(); ++ut)
+ {
+ auto ufid = dynamic_cast<ID3v2::UniqueFileIdentifierFrame*> (*ut);
+ if (ufid && ufid->owner() == "http://musicbrainz.org")
+ {
+ // MusicBrainz pads with a \0, but the spec requires binary, be cautious
+ char cUfid[64];
+ int max_size = std::min(static_cast<int>(ufid->identifier().size()), 63);
+ strncpy(cUfid, ufid->identifier().data(), max_size);
+ cUfid[max_size] = '\0';
+ tag.SetMusicBrainzTrackID(cUfid);
+ }
+ }
+ else if (it->first == "APIC")
+ // Loop through all pictures and store the frame pointers for the picture types we want
+ for (ID3v2::FrameList::ConstIterator pi = it->second.begin(); pi != it->second.end(); ++pi)
+ {
+ auto pictureFrame = dynamic_cast<ID3v2::AttachedPictureFrame *> (*pi);
+ if (!pictureFrame) continue;
+
+ if (pictureFrame->type() == ID3v2::AttachedPictureFrame::FrontCover) pictures[0] = pictureFrame;
+ else if (pictureFrame->type() == ID3v2::AttachedPictureFrame::Other) pictures[1] = pictureFrame;
+ else if (pi == it->second.begin()) pictures[2] = pictureFrame;
+ }
+ else if (it->first == "POPM")
+ // Loop through and process ratings
+ for (ID3v2::FrameList::ConstIterator ct = it->second.begin(); ct != it->second.end(); ++ct)
+ {
+ auto popFrame = dynamic_cast<ID3v2::PopularimeterFrame *> (*ct);
+ if (!popFrame) continue;
+
+ // @xbmc.org ratings trump others (of course)
+ if (popFrame->email() == "ratings@xbmc.org")
+ tag.SetUserrating(popFrame->rating() / 51); //! @todo wtf? Why 51 find some explanation, somewhere...
+ else if (tag.GetUserrating() == 0)
+ {
+ if (popFrame->email() != "Windows Media Player 9 Series" &&
+ popFrame->email() != "Banshee" &&
+ popFrame->email() != "no@email" &&
+ popFrame->email() != "quodlibet@lists.sacredchao.net" &&
+ popFrame->email() != "rating@winamp.com")
+ CLog::Log(LOGDEBUG, "unrecognized ratings schema detected: {}",
+ popFrame->email().toCString(true));
+ tag.SetUserrating(POPMtoXBMC(popFrame->rating()));
+ }
+ }
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX)
+ CLog::Log(LOGDEBUG, "unrecognized ID3 frame detected: {}{}{}{}", it->first[0], it->first[1],
+ it->first[2], it->first[3]);
+ } // for
+
+ // Process the extracted picture frames; 0 = CoverArt, 1 = Other, 2 = First Found picture
+ for (const ID3v2::AttachedPictureFrame* const picture : pictures)
+ if (picture)
+ {
+ std::string mime = picture->mimeType().to8Bit(true);
+ TagLib::uint size = picture->picture().size();
+ tag.SetCoverArtInfo(size, mime);
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t*>(picture->picture().data()), size, mime);
+
+ // Stop after we find the first picture for now.
+ break;
+ }
+
+
+ if (!id3v2->comment().isEmpty())
+ tag.SetComment(id3v2->comment().toCString(true));
+
+ tag.SetReplayGain(replayGainInfo);
+ return true;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(APE::Tag *ape, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!ape)
+ return false;
+
+ ReplayGain replayGainInfo;
+ const APE::ItemListMap itemListMap = ape->itemListMap();
+ for (APE::ItemListMap::ConstIterator it = itemListMap.begin(); it != itemListMap.end(); ++it)
+ {
+ if (it->first == "ARTIST")
+ SetArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ARTISTSORT")
+ SetArtistSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ARTISTS")
+ SetArtistHints(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ALBUMARTIST" || it->first == "ALBUM ARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ALBUMARTISTSORT")
+ SetAlbumArtistSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ALBUMARTISTS" || it->first == "ALBUM ARTISTS")
+ SetAlbumArtistHints(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "COMPOSERSORT")
+ SetComposerSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ALBUM")
+ tag.SetAlbum(it->second.toString().to8Bit(true));
+ else if (it->first == "TITLE")
+ tag.SetTitle(it->second.toString().to8Bit(true));
+ else if (it->first == "TRACKNUMBER" || it->first == "TRACK")
+ tag.SetTrackNumber(it->second.toString().toInt());
+ else if (it->first == "DISCNUMBER" || it->first == "DISC")
+ tag.SetDiscNumber(it->second.toString().toInt());
+ else if (it->first == "YEAR")
+ tag.SetReleaseDate(it->second.toString().to8Bit(true));
+ else if (it->first == "DISCSUBTITLE")
+ tag.SetDiscSubtitle(it->second.toString().to8Bit(true));
+ else if (it->first == "ORIGINALYEAR")
+ tag.SetOriginalDate(it->second.toString().to8Bit(true));
+ else if (it->first == "GENRE")
+ SetGenre(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "MOOD")
+ tag.SetMood(it->second.toString().to8Bit(true));
+ else if (it->first == "COMMENT")
+ tag.SetComment(it->second.toString().to8Bit(true));
+ else if (it->first == "CUESHEET")
+ tag.SetCueSheet(it->second.toString().to8Bit(true));
+ else if (it->first == "ENCODEDBY")
+ {}
+ else if (it->first == "COMPOSER")
+ AddArtistRole(tag, "Composer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "CONDUCTOR")
+ AddArtistRole(tag, "Conductor", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "BAND")
+ AddArtistRole(tag, "Band", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ENSEMBLE")
+ AddArtistRole(tag, "Ensemble", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "LYRICIST")
+ AddArtistRole(tag, "Lyricist", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "WRITER")
+ AddArtistRole(tag, "Writer", StringListToVectorString(it->second.toStringList()));
+ else if ((it->first == "MIXARTIST") || (it->first == "REMIXER"))
+ AddArtistRole(tag, "Remixer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ARRANGER")
+ AddArtistRole(tag, "Arranger", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "ENGINEER")
+ AddArtistRole(tag, "Engineer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "PRODUCER")
+ AddArtistRole(tag, "Producer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "DJMIXER")
+ AddArtistRole(tag, "DJMixer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "MIXER")
+ AddArtistRole(tag, "Mixer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "PERFORMER")
+ // Picard uses PERFORMER tag as musician credits list formatted "name (instrument)"
+ AddArtistInstrument(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "LABEL")
+ tag.SetRecordLabel(it->second.toString().to8Bit(true));
+ else if (it->first == "COMPILATION")
+ tag.SetCompilation(it->second.toString().toInt() == 1);
+ else if (it->first == "LYRICS")
+ tag.SetLyrics(it->second.toString().to8Bit(true));
+ else if (it->first == "REPLAYGAIN_TRACK_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.toString().toCString(true));
+ else if (it->first == "REPLAYGAIN_ALBUM_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.toString().toCString(true));
+ else if (it->first == "REPLAYGAIN_TRACK_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.toString().toCString(true));
+ else if (it->first == "REPLAYGAIN_ALBUM_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.toString().toCString(true));
+ else if (it->first == "MUSICBRAINZ_ARTISTID")
+ tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(it->second.toStringList())));
+ else if (it->first == "MUSICBRAINZ_ALBUMARTISTID")
+ tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(it->second.toStringList())));
+ else if (it->first == "MUSICBRAINZ_ALBUMARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "MUSICBRAINZ_ALBUMID")
+ tag.SetMusicBrainzAlbumID(it->second.toString().to8Bit(true));
+ else if (it->first == "MUSICBRAINZ_RELEASEGROUPID")
+ tag.SetMusicBrainzReleaseGroupID(it->second.toString().to8Bit(true));
+ else if (it->first == "MUSICBRAINZ_TRACKID")
+ tag.SetMusicBrainzTrackID(it->second.toString().to8Bit(true));
+ else if (it->first == "MUSICBRAINZ_ALBUMTYPE")
+ SetReleaseType(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "BPM")
+ tag.SetBPM(it->second.toString().toInt());
+ else if (it->first == "MUSICBRAINZ_ALBUMSTATUS")
+ tag.SetAlbumReleaseStatus(it->second.toString().to8Bit(true));
+ else if (it->first == "COVER ART (FRONT)")
+ {
+ TagLib::ByteVector tdata = it->second.binaryData();
+ // The image data follows a null byte, which can optionally be preceded by a filename
+ const uint offset = tdata.find('\0') + 1;
+ ByteVector bv(tdata.data() + offset, tdata.size() - offset);
+ // Infer the mimetype
+ std::string mime{};
+ if (bv.startsWith("\xFF\xD8\xFF"))
+ mime = "image/jpeg";
+ else if (bv.startsWith("\x89\x50\x4E\x47"))
+ mime = "image/png";
+ else if (bv.startsWith("\x47\x49\x46\x38"))
+ mime = "image/gif";
+ else if (bv.startsWith("\x42\x4D"))
+ mime = "image/bmp";
+ if ((offset > 0) && (offset <= tdata.size()) && (mime.size() > 0))
+ {
+ tag.SetCoverArtInfo(bv.size(), mime);
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t*>(bv.data()), bv.size(), mime);
+ }
+ }
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX)
+ CLog::Log(LOGDEBUG, "unrecognized APE tag: {}", it->first.toCString(true));
+ }
+
+ tag.SetReplayGain(replayGainInfo);
+ return true;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(Ogg::XiphComment *xiph, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!xiph)
+ return false;
+
+#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11
+ FLAC::Picture pictures[3];
+#endif
+ ReplayGain replayGainInfo;
+
+ const Ogg::FieldListMap& fieldListMap = xiph->fieldListMap();
+ for (Ogg::FieldListMap::ConstIterator it = fieldListMap.begin(); it != fieldListMap.end(); ++it)
+ {
+ if (it->first == "ARTIST")
+ SetArtist(tag, StringListToVectorString(it->second));
+ else if (it->first == "ARTISTSORT")
+ SetArtistSort(tag, StringListToVectorString(it->second));
+ else if (it->first == "ARTISTS")
+ SetArtistHints(tag, StringListToVectorString(it->second));
+ else if (it->first == "ALBUMARTIST" || it->first == "ALBUM ARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(it->second));
+ else if (it->first == "ALBUMARTISTSORT" || it->first == "ALBUM ARTIST SORT")
+ SetAlbumArtistSort(tag, StringListToVectorString(it->second));
+ else if (it->first == "ALBUMARTISTS" || it->first == "ALBUM ARTISTS")
+ SetAlbumArtistHints(tag, StringListToVectorString(it->second));
+ else if (it->first == "COMPOSERSORT")
+ SetComposerSort(tag, StringListToVectorString(it->second));
+ else if (it->first == "ALBUM")
+ tag.SetAlbum(it->second.front().to8Bit(true));
+ else if (it->first == "TITLE")
+ tag.SetTitle(it->second.front().to8Bit(true));
+ else if (it->first == "TRACKNUMBER")
+ tag.SetTrackNumber(it->second.front().toInt());
+ else if (it->first == "DISCNUMBER")
+ tag.SetDiscNumber(it->second.front().toInt());
+ else if (it->first == "YEAR" || it->first == "DATE")
+ tag.AddReleaseDate(it->second.front().to8Bit(true));
+ else if (it->first == "GENRE")
+ SetGenre(tag, StringListToVectorString(it->second));
+ else if (it->first == "MOOD")
+ tag.SetMood(it->second.front().to8Bit(true));
+ else if (it->first == "COMMENT")
+ tag.SetComment(it->second.front().to8Bit(true));
+ else if (it->first == "ORIGINALYEAR" || it->first == "ORIGINALDATE")
+ tag.AddOriginalDate(it->second.front().to8Bit(true));
+ else if (it->first == "CUESHEET")
+ tag.SetCueSheet(it->second.front().to8Bit(true));
+ else if (it->first == "DISCSUBTITLE")
+ tag.SetDiscSubtitle(it->second.front().to8Bit(true));
+ else if (it->first == "ENCODEDBY")
+ {} // Known but unsupported, suppress warnings
+ else if (it->first == "COMPOSER")
+ AddArtistRole(tag, "Composer", StringListToVectorString(it->second));
+ else if (it->first == "CONDUCTOR")
+ AddArtistRole(tag, "Conductor", StringListToVectorString(it->second));
+ else if (it->first == "BAND")
+ AddArtistRole(tag, "Band", StringListToVectorString(it->second));
+ else if (it->first == "ENSEMBLE")
+ AddArtistRole(tag, "Ensemble", StringListToVectorString(it->second));
+ else if (it->first == "LYRICIST")
+ AddArtistRole(tag, "Lyricist", StringListToVectorString(it->second));
+ else if (it->first == "WRITER")
+ AddArtistRole(tag, "Writer", StringListToVectorString(it->second));
+ else if ((it->first == "MIXARTIST") || (it->first == "REMIXER"))
+ AddArtistRole(tag, "Remixer", StringListToVectorString(it->second));
+ else if (it->first == "ARRANGER")
+ AddArtistRole(tag, "Arranger", StringListToVectorString(it->second));
+ else if (it->first == "ENGINEER")
+ AddArtistRole(tag, "Engineer", StringListToVectorString(it->second));
+ else if (it->first == "PRODUCER")
+ AddArtistRole(tag, "Producer", StringListToVectorString(it->second));
+ else if (it->first == "DJMIXER")
+ AddArtistRole(tag, "DJMixer", StringListToVectorString(it->second));
+ else if (it->first == "MIXER")
+ AddArtistRole(tag, "Mixer", StringListToVectorString(it->second));
+ else if (it->first == "PERFORMER")
+ // Picard uses PERFORMER tag as musician credits list formatted "name (instrument)"
+ AddArtistInstrument(tag, StringListToVectorString(it->second));
+ else if (it->first == "LABEL")
+ tag.SetRecordLabel(it->second.front().to8Bit(true));
+ else if (it->first == "COMPILATION")
+ tag.SetCompilation(it->second.front().toInt() == 1);
+ else if (it->first == "LYRICS")
+ tag.SetLyrics(it->second.front().to8Bit(true));
+ else if (it->first == "REPLAYGAIN_TRACK_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.front().toCString(true));
+ else if (it->first == "REPLAYGAIN_ALBUM_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.front().toCString(true));
+ else if (it->first == "REPLAYGAIN_TRACK_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.front().toCString(true));
+ else if (it->first == "REPLAYGAIN_ALBUM_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.front().toCString(true));
+ else if (it->first == "MUSICBRAINZ_ARTISTID")
+ tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(it->second)));
+ else if (it->first == "MUSICBRAINZ_ALBUMARTISTID")
+ tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(it->second)));
+ else if (it->first == "MUSICBRAINZ_ALBUMARTIST")
+ SetAlbumArtist(tag, StringListToVectorString(it->second));
+ else if (it->first == "MUSICBRAINZ_ALBUMID")
+ tag.SetMusicBrainzAlbumID(it->second.front().to8Bit(true));
+ else if (it->first == "MUSICBRAINZ_RELEASEGROUPID")
+ tag.SetMusicBrainzReleaseGroupID(it->second.front().to8Bit(true));
+ else if (it->first == "MUSICBRAINZ_TRACKID")
+ tag.SetMusicBrainzTrackID(it->second.front().to8Bit(true));
+ else if (it->first == "RELEASETYPE")
+ SetReleaseType(tag, StringListToVectorString(it->second));
+ else if (it->first == "BPM")
+ tag.SetBPM(strtol(it->second.front().toCString(true), nullptr, 10));
+ else if (it->first == "RELEASESTATUS")
+ tag.SetAlbumReleaseStatus(it->second.front().toCString(true));
+ else if (it->first == "RATING")
+ {
+ // Vorbis ratings are a mess because the standard forgot to mention anything about them.
+ // If you want to see how emotive the issue is and the varying standards, check here:
+ // http://forums.winamp.com/showthread.php?t=324512
+ // The most common standard in that thread seems to be a 0-100 scale for 1-5 stars.
+ // So, that's what we'll support for now.
+ int iUserrating = it->second.front().toInt();
+ if (iUserrating > 0 && iUserrating <= 100)
+ tag.SetUserrating((iUserrating / 10));
+ }
+#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11
+ else if (it->first == "METADATA_BLOCK_PICTURE")
+ {
+ const char* b64 = it->second.front().toCString();
+ std::string decoded_block = Base64::Decode(b64, it->second.front().size());
+ ByteVector bv(decoded_block.data(), decoded_block.size());
+ TagLib::FLAC::Picture* pictureFrame = new TagLib::FLAC::Picture(bv);
+
+ if (pictureFrame->type() == FLAC::Picture::FrontCover) pictures[0].parse(bv);
+ else if (pictureFrame->type() == FLAC::Picture::Other) pictures[1].parse(bv);
+
+ delete pictureFrame;
+ }
+ else if (it->first == "COVERART")
+ {
+ const char* b64 = it->second.front().toCString();
+ std::string decoded_block = Base64::Decode(b64, it->second.front().size());
+ ByteVector bv(decoded_block.data(), decoded_block.size());
+ pictures[2].setData(bv);
+ // Assume jpeg
+ if (pictures[2].mimeType().isEmpty())
+ pictures[2].setMimeType("image/jpeg");
+ }
+ else if (it->first == "COVERARTMIME")
+ {
+ pictures[2].setMimeType(it->second.front());
+ }
+#endif
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel == LOG_LEVEL_MAX)
+ CLog::Log(LOGDEBUG, "unrecognized XipComment name: {}", it->first.toCString(true));
+ }
+
+#if TAGLIB_MAJOR_VERSION <= 1 && TAGLIB_MINOR_VERSION < 11
+ // Process the extracted picture frames; 0 = CoverArt, 1 = Other, 2 = COVERART/COVERARTMIME
+ for (int i = 0; i < 3; ++i)
+ if (pictures[i].data().size())
+ {
+ std::string mime = pictures[i].mimeType().toCString();
+ if (mime.compare(0, 6, "image/") != 0)
+ continue;
+ TagLib::uint size = pictures[i].data().size();
+ tag.SetCoverArtInfo(size, mime);
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t*>(pictures[i].data().data()), size, mime);
+
+ break;
+ }
+#else
+ auto pictureList = xiph->pictureList();
+ FLAC::Picture *cover[2] = {};
+
+ for (auto i: pictureList)
+ {
+ FLAC::Picture *picture = i;
+ if (picture->type() == FLAC::Picture::FrontCover)
+ cover[0] = picture;
+ else // anything else is taken as second priority
+ cover[1] = picture;
+ }
+ for (const FLAC::Picture* const c : cover)
+ {
+ if (c)
+ {
+ tag.SetCoverArtInfo(c->data().size(), c->mimeType().to8Bit(true));
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t*>(c->data().data()), c->data().size(), c->mimeType().to8Bit(true));
+ break; // one is enough
+ }
+ }
+#endif
+
+ if (!xiph->comment().isEmpty())
+ tag.SetComment(xiph->comment().toCString(true));
+
+ tag.SetReplayGain(replayGainInfo);
+ return true;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(MP4::Tag *mp4, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!mp4)
+ return false;
+
+ ReplayGain replayGainInfo;
+ const MP4::ItemMap itemMap = mp4->itemMap();
+ for (auto it = itemMap.begin(); it != itemMap.end(); ++it)
+ {
+ if (it->first == "\251nam")
+ tag.SetTitle(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "\251ART")
+ SetArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "soar")
+ SetArtistSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:ARTISTS")
+ SetArtistHints(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "\251alb")
+ tag.SetAlbum(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "aART")
+ SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "soaa")
+ SetAlbumArtistSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:albumartists" ||
+ it->first == "----:com.apple.iTunes:ALBUMARTISTS")
+ SetAlbumArtistHints(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "soco")
+ SetComposerSort(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "\251gen")
+ SetGenre(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:MOOD")
+ tag.SetMood(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "\251cmt")
+ tag.SetComment(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "\251wrt")
+ AddArtistRole(tag, "Composer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:CONDUCTOR")
+ AddArtistRole(tag, "Conductor", StringListToVectorString(it->second.toStringList()));
+ //No MP4 standard tag for "ensemble"
+ else if (it->first == "----:com.apple.iTunes:LYRICIST")
+ AddArtistRole(tag, "Lyricist", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:REMIXER")
+ AddArtistRole(tag, "Remixer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:ENGINEER")
+ AddArtistRole(tag, "Engineer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:PRODUCER")
+ AddArtistRole(tag, "Producer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:DJMIXER")
+ AddArtistRole(tag, "DJMixer", StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:MIXER")
+ AddArtistRole(tag, "Mixer", StringListToVectorString(it->second.toStringList()));
+ //No MP4 standard tag for musician credits
+ else if (it->first == "----:com.apple.iTunes:LABEL")
+ tag.SetRecordLabel(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:DISCSUBTITLE")
+ tag.SetDiscSubtitle(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "cpil")
+ tag.SetCompilation(it->second.toBool());
+ else if (it->first == "trkn")
+ tag.SetTrackNumber(it->second.toIntPair().first);
+ else if (it->first == "disk")
+ tag.SetDiscNumber(it->second.toIntPair().first);
+ else if (it->first == "\251day")
+ tag.SetReleaseDate(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:originaldate")
+ tag.SetOriginalDate(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:replaygain_track_gain" ||
+ it->first == "----:com.apple.iTunes:REPLAYGAIN_TRACK_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::TRACK, it->second.toStringList().front().toCString());
+ else if (it->first == "----:com.apple.iTunes:replaygain_album_gain" ||
+ it->first == "----:com.apple.iTunes:REPLAYGAIN_ALBUM_GAIN")
+ replayGainInfo.ParseGain(ReplayGain::ALBUM, it->second.toStringList().front().toCString());
+ else if (it->first == "----:com.apple.iTunes:replaygain_track_peak" ||
+ it->first == "----:com.apple.iTunes:REPLAYGAIN_TRACK_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::TRACK, it->second.toStringList().front().toCString());
+ else if (it->first == "----:com.apple.iTunes:replaygain_album_peak" ||
+ it->first == "----:com.apple.iTunes:REPLAYGAIN_ALBUM_PEAK")
+ replayGainInfo.ParsePeak(ReplayGain::ALBUM, it->second.toStringList().front().toCString());
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Artist Id")
+ tag.SetMusicBrainzArtistID(SplitMBID(StringListToVectorString(it->second.toStringList())));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Artist Id")
+ tag.SetMusicBrainzAlbumArtistID(SplitMBID(StringListToVectorString(it->second.toStringList())));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Artist")
+ SetAlbumArtist(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Id")
+ tag.SetMusicBrainzAlbumID(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Release Group Id")
+ tag.SetMusicBrainzReleaseGroupID(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Track Id")
+ tag.SetMusicBrainzTrackID(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Type")
+ SetReleaseType(tag, StringListToVectorString(it->second.toStringList()));
+ else if (it->first == "----:com.apple.iTunes:MusicBrainz Album Status")
+ tag.SetAlbumReleaseStatus(it->second.toStringList().front().to8Bit(true));
+ else if (it->first == "tmpo")
+ tag.SetBPM(it->second.toIntPair().first);
+ else if (it->first == "covr")
+ {
+ MP4::CoverArtList coverArtList = it->second.toCoverArtList();
+ for (MP4::CoverArtList::ConstIterator pt = coverArtList.begin(); pt != coverArtList.end(); ++pt)
+ {
+ std::string mime;
+ switch (pt->format())
+ {
+ case MP4::CoverArt::PNG:
+ mime = "image/png";
+ break;
+ case MP4::CoverArt::JPEG:
+ mime = "image/jpeg";
+ break;
+ default:
+ break;
+ }
+ if (mime.empty())
+ continue;
+ tag.SetCoverArtInfo(pt->data().size(), mime);
+ if (art)
+ art->Set(reinterpret_cast<const uint8_t *>(pt->data().data()), pt->data().size(), mime);
+ break; // one is enough
+ }
+ }
+ }
+
+ if (!mp4->comment().isEmpty())
+ tag.SetComment(mp4->comment().toCString(true));
+
+ tag.SetReplayGain(replayGainInfo);
+ return true;
+}
+
+template<>
+bool CTagLoaderTagLib::ParseTag(Tag *genericTag, EmbeddedArt *art, CMusicInfoTag& tag)
+{
+ if (!genericTag)
+ return false;
+
+ PropertyMap properties = genericTag->properties();
+ for (PropertyMap::ConstIterator it = properties.begin(); it != properties.end(); ++it)
+ {
+ if (it->first == "ARTIST")
+ SetArtist(tag, StringListToVectorString(it->second));
+ else if (it->first == "ALBUM")
+ tag.SetAlbum(it->second.front().to8Bit(true));
+ else if (it->first == "TITLE")
+ tag.SetTitle(it->second.front().to8Bit(true));
+ else if (it->first == "TRACKNUMBER")
+ tag.SetTrackNumber(it->second.front().toInt());
+ else if (it->first == "YEAR")
+ tag.SetYear(it->second.front().toInt());
+ else if (it->first == "GENRE")
+ SetGenre(tag, StringListToVectorString(it->second));
+ else if (it->first == "COMMENT")
+ tag.SetComment(it->second.front().to8Bit(true));
+ }
+
+ return true;
+}
+
+
+
+
+void CTagLoaderTagLib::SetArtist(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.SetArtist(values[0]);
+ else
+ // Fill both artist vector and artist desc from tag value.
+ // Note desc may not be empty as it could have been set by previous parsing of ID3v2 before APE
+ tag.SetArtist(values, true);
+}
+
+void CTagLoaderTagLib::SetArtistSort(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ // ARTISTSORT/TSOP tag is often a single string, when not take union of values
+ if (values.size() == 1)
+ tag.SetArtistSort(values[0]);
+ else
+ tag.SetArtistSort(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CTagLoaderTagLib::SetArtistHints(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.SetMusicBrainzArtistHints(StringUtils::Split(values[0], CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ else
+ tag.SetMusicBrainzArtistHints(values);
+}
+
+std::vector<std::string> CTagLoaderTagLib::SplitMBID(const std::vector<std::string> &values)
+{
+ if (values.empty() || values.size() > 1)
+ return values;
+
+ // Picard, and other taggers use a heap of different separators. We use a regexp to detect
+ // MBIDs to make sure we hit them all...
+ std::vector<std::string> ret;
+ std::string value = values[0];
+ StringUtils::ToLower(value);
+ CRegExp reg;
+ if (reg.RegComp("([[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12})"))
+ {
+ int pos = -1;
+ while ((pos = reg.RegFind(value, pos+1)) > -1)
+ ret.push_back(reg.GetMatch(1));
+ }
+ return ret;
+}
+
+void CTagLoaderTagLib::SetAlbumArtist(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.SetAlbumArtist(values[0]);
+ else
+ // Fill both artist vector and artist desc from tag value.
+ // Note desc may not be empty as it could have been set by previous parsing of ID3v2 before APE
+ tag.SetAlbumArtist(values, true);
+}
+
+void CTagLoaderTagLib::SetAlbumArtistSort(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ // ALBUMARTISTSORT/TSOP tag is often a single string, when not take union of values
+ if (values.size() == 1)
+ tag.SetAlbumArtistSort(values[0]);
+ else
+ tag.SetAlbumArtistSort(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CTagLoaderTagLib::SetAlbumArtistHints(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.SetMusicBrainzAlbumArtistHints(StringUtils::Split(values[0], CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+ else
+ tag.SetMusicBrainzAlbumArtistHints(values);
+}
+
+void CTagLoaderTagLib::SetComposerSort(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ // COMPOSRSORT/TSOC tag is often a single string, when not take union of values
+ if (values.size() == 1)
+ tag.SetComposerSort(values[0]);
+ else
+ tag.SetComposerSort(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CTagLoaderTagLib::SetGenre(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ /*
+ TagLib doesn't resolve ID3v1 genre numbers in the case were only
+ a number is specified, thus this workaround.
+ */
+ std::vector<std::string> genres;
+ for (const std::string& i : values)
+ {
+ std::string genre = i;
+ if (StringUtils::IsNaturalNumber(genre))
+ {
+ int number = strtol(i.c_str(), nullptr, 10);
+ if (number >= 0 && number < 256)
+ genre = ID3v1::genre(number).to8Bit(true);
+ }
+ genres.push_back(genre);
+ }
+ if (genres.size() == 1)
+ tag.SetGenre(genres[0], true);
+ else
+ tag.SetGenre(genres, true);
+}
+
+void CTagLoaderTagLib::SetReleaseType(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.SetMusicBrainzReleaseType(values[0]);
+ else
+ tag.SetMusicBrainzReleaseType(StringUtils::Join(values, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator));
+}
+
+void CTagLoaderTagLib::AddArtistRole(CMusicInfoTag &tag, const std::string& strRole, const std::vector<std::string> &values)
+{
+ if (values.size() == 1)
+ tag.AddArtistRole(strRole, values[0]);
+ else
+ tag.AddArtistRole(strRole, values);
+}
+
+void CTagLoaderTagLib::SetDiscSubtitle(CMusicInfoTag& tag, const std::vector<std::string>& values)
+{
+ if (values.size() == 1)
+ tag.SetDiscSubtitle(values[0]);
+ else
+ tag.SetDiscSubtitle(std::string());
+}
+
+void CTagLoaderTagLib::AddArtistRole(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ // Values contains role, name pairs (as in ID3 standard for TIPL or TMCL tags)
+ // Every odd entry is a function (e.g. Producer, Arranger etc.) or instrument (e.g. Orchestra, Vocal, Piano)
+ // and every even is an artist or a comma delimited list of artists.
+
+ if (values.size() % 2 != 0) // Must contain an even number of entries
+ return;
+
+ // Vector of possible separators
+ const std::vector<std::string> separators{ ";", "/", ",", "&", " and " };
+
+ for (size_t i = 0; i + 1 < values.size(); i += 2)
+ {
+ std::vector<std::string> roles;
+ //Split into individual roles
+ roles = StringUtils::Split(values[i], separators);
+ for (auto role : roles)
+ {
+ StringUtils::Trim(role);
+ StringUtils::ToCapitalize(role);
+ tag.AddArtistRole(role, StringUtils::Split(values[i + 1], ","));
+ }
+ }
+}
+
+void CTagLoaderTagLib::AddArtistInstrument(CMusicInfoTag &tag, const std::vector<std::string> &values)
+{
+ /* Values is a musician credits list, each entry is artist name followed by instrument (or function)
+ e.g. violin, drums, background vocals, solo, orchestra etc. in brackets. This is how Picard uses
+ the PERFORMER tag. Multiple instruments may be in one tag
+ e.g "Pierre Marchand (bass, drum machine and hammond organ)",
+ these will be separated into individual roles.
+ If there is not a pair of brackets then role is "performer" by default, and the whole entry is
+ taken as artist name.
+ */
+ // Vector of possible separators
+ const std::vector<std::string> separators{";", "/", ",", "&", " and "};
+
+ for (size_t i = 0; i < values.size(); ++i)
+ {
+ std::vector<std::string> roles;
+ std::string strArtist = values[i];
+ size_t firstLim = values[i].find_first_of('(');
+ size_t lastLim = values[i].find_last_of(')');
+ if (lastLim != std::string::npos && firstLim != std::string::npos && firstLim < lastLim - 1)
+ {
+ //Pair of brackets with something between them
+ strArtist.erase(firstLim, lastLim - firstLim + 1);
+ std::string strRole = values[i].substr(firstLim + 1, lastLim - firstLim - 1);
+ //Split into individual roles
+ roles = StringUtils::Split(strRole, separators);
+ }
+ StringUtils::Trim(strArtist);
+ if (roles.empty())
+ tag.AddArtistRole("Performer", strArtist);
+ else
+ for (auto role : roles)
+ {
+ StringUtils::Trim(role);
+ StringUtils::ToCapitalize(role);
+ tag.AddArtistRole(role, strArtist);
+ }
+ }
+}
+
+bool CTagLoaderTagLib::Load(const std::string& strFileName, CMusicInfoTag& tag, const std::string& fallbackFileExtension, EmbeddedArt *art /* = NULL */)
+{
+ std::string strExtension = URIUtils::GetExtension(strFileName);
+ StringUtils::TrimLeft(strExtension, ".");
+
+ if (strExtension.empty())
+ {
+ strExtension = fallbackFileExtension;
+ if (strExtension.empty())
+ return false;
+ }
+
+ StringUtils::ToLower(strExtension);
+ TagLibVFSStream* stream = new TagLibVFSStream(strFileName, true);
+ if (!stream)
+ {
+ CLog::Log(LOGERROR, "could not create TagLib VFS stream for: {}", strFileName);
+ return false;
+ }
+
+ long file_length = stream->length();
+
+ if (file_length == 0) // a stream returns zero as the length
+ {
+ delete stream; // scrap this instance
+ return false; // and quit without attempting to read non-existent tags
+ }
+
+ TagLib::File* file = nullptr;
+ TagLib::APE::File* apeFile = nullptr;
+ TagLib::ASF::File* asfFile = nullptr;
+ TagLib::FLAC::File* flacFile = nullptr;
+ TagLib::MP4::File* mp4File = nullptr;
+ TagLib::MPC::File* mpcFile = nullptr;
+ TagLib::MPEG::File* mpegFile = nullptr;
+ TagLib::Ogg::Vorbis::File* oggVorbisFile = nullptr;
+ TagLib::Ogg::FLAC::File* oggFlacFile = nullptr;
+ TagLib::Ogg::Opus::File* oggOpusFile = nullptr;
+ TagLib::TrueAudio::File* ttaFile = nullptr;
+ TagLib::WavPack::File* wvFile = nullptr;
+ TagLib::RIFF::WAV::File * wavFile = nullptr;
+ TagLib::RIFF::AIFF::File * aiffFile = nullptr;
+
+ try
+ {
+ if (strExtension == "ape")
+ file = apeFile = new APE::File(stream);
+ else if (strExtension == "asf" || strExtension == "wmv" || strExtension == "wma")
+ file = asfFile = new ASF::File(stream);
+ else if (strExtension == "flac")
+ file = flacFile = new FLAC::File(stream, ID3v2::FrameFactory::instance());
+ else if (strExtension == "it")
+ file = new IT::File(stream);
+ else if (strExtension == "mod" || strExtension == "module" || strExtension == "nst" || strExtension == "wow")
+ file = new Mod::File(stream);
+ else if (strExtension == "mp4" || strExtension == "m4a" || strExtension == "m4v" ||
+ strExtension == "m4r" || strExtension == "m4b" ||
+ strExtension == "m4p" || strExtension == "3g2")
+ file = mp4File = new MP4::File(stream);
+ else if (strExtension == "mpc")
+ file = mpcFile = new MPC::File(stream);
+ else if (strExtension == "mp3" || strExtension == "aac")
+ file = mpegFile = new MPEG::File(stream, ID3v2::FrameFactory::instance());
+ else if (strExtension == "s3m")
+ file = new S3M::File(stream);
+ else if (strExtension == "tta")
+ file = ttaFile = new TrueAudio::File(stream, ID3v2::FrameFactory::instance());
+ else if (strExtension == "wv")
+ file = wvFile = new WavPack::File(stream);
+ else if (strExtension == "aif" || strExtension == "aiff")
+ file = aiffFile = new RIFF::AIFF::File(stream);
+ else if (strExtension == "wav")
+ file = wavFile = new RIFF::WAV::File(stream);
+ else if (strExtension == "xm")
+ file = new XM::File(stream);
+ else if (strExtension == "ogg")
+ file = oggVorbisFile = new Ogg::Vorbis::File(stream);
+ else if (strExtension == "opus")
+ file = oggOpusFile = new Ogg::Opus::File(stream);
+ else if (strExtension == "oga") // Leave this madness until last - oga container can have Vorbis or FLAC
+ {
+ file = oggFlacFile = new Ogg::FLAC::File(stream);
+ if (!file || !file->isValid())
+ {
+ delete file;
+ oggFlacFile = nullptr;
+ file = oggVorbisFile = new Ogg::Vorbis::File(stream);
+ }
+ }
+ }
+ catch (const std::exception& ex)
+ {
+ CLog::Log(LOGERROR, "Taglib exception: {}", ex.what());
+ }
+
+ if (!file || !file->isOpen())
+ {
+ delete file;
+ delete stream;
+ CLog::Log(LOGDEBUG, "file {} could not be opened for tag reading", strFileName);
+ return false;
+ }
+
+ APE::Tag *ape = nullptr;
+ ASF::Tag *asf = nullptr;
+ MP4::Tag *mp4 = nullptr;
+ ID3v1::Tag *id3v1 = nullptr;
+ ID3v2::Tag *id3v2 = nullptr;
+ Ogg::XiphComment *xiph = nullptr;
+ Tag *genericTag = nullptr;
+
+ if (apeFile)
+ ape = apeFile->APETag(false);
+ else if (asfFile)
+ asf = asfFile->tag();
+ else if (flacFile)
+ {
+ xiph = flacFile->xiphComment(false);
+ id3v2 = flacFile->ID3v2Tag(false);
+ }
+ else if (mp4File)
+ mp4 = mp4File->tag();
+ else if (mpegFile)
+ {
+ id3v1 = mpegFile->ID3v1Tag(false);
+ id3v2 = mpegFile->ID3v2Tag(false);
+ ape = mpegFile->APETag(false);
+ }
+ else if (oggFlacFile)
+ xiph = oggFlacFile->tag();
+ else if (oggVorbisFile)
+ xiph = oggVorbisFile->tag();
+ else if (oggOpusFile)
+ xiph = oggOpusFile->tag();
+ else if (ttaFile)
+ id3v2 = ttaFile->ID3v2Tag(false);
+ else if (aiffFile)
+ id3v2 = aiffFile->tag();
+ else if (wavFile)
+ id3v2 = wavFile->ID3v2Tag();
+ else if (wvFile)
+ ape = wvFile->APETag(false);
+ else if (mpcFile)
+ ape = mpcFile->APETag(false);
+ else // This is a catch all to get generic information for other files types (s3m, xm, it, mod, etc)
+ genericTag = file->tag();
+
+ if (file->audioProperties())
+ {
+ tag.SetDuration(file->audioProperties()->length());
+ tag.SetBitRate(file->audioProperties()->bitrate());
+ tag.SetNoOfChannels(file->audioProperties()->channels());
+ tag.SetSampleRate(file->audioProperties()->sampleRate());
+ }
+
+ if (asf)
+ ParseTag(asf, art, tag);
+ if (id3v1)
+ ParseTag(id3v1, art, tag);
+ if (id3v2)
+ ParseTag(id3v2, art, tag);
+ if (genericTag)
+ ParseTag(genericTag, art, tag);
+ if (mp4)
+ ParseTag(mp4, art, tag);
+ if (xiph) // xiph tags override id3v2 tags in badly tagged FLACs
+ ParseTag(xiph, art, tag);
+ if (ape && (!id3v2 || CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_prioritiseAPEv2tags)) // ape tags override id3v2 if we're prioritising them
+ ParseTag(ape, art, tag);
+
+ // art for flac files is outside the tag
+ if (flacFile)
+ SetFlacArt(flacFile, art, tag);
+
+ if (!tag.GetTitle().empty() || !tag.GetArtist().empty() || !tag.GetAlbum().empty())
+ tag.SetLoaded();
+ tag.SetURL(strFileName);
+
+ delete file;
+ delete stream;
+
+ return true;
+}
diff --git a/xbmc/music/tags/TagLoaderTagLib.h b/xbmc/music/tags/TagLoaderTagLib.h
new file mode 100644
index 0000000..a4b82ad
--- /dev/null
+++ b/xbmc/music/tags/TagLoaderTagLib.h
@@ -0,0 +1,54 @@
+/*
+ * 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 "ImusicInfoTagLoader.h"
+
+#include <string>
+#include <vector>
+
+class EmbeddedArt;
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTag;
+};
+
+class CTagLoaderTagLib : public MUSIC_INFO::IMusicInfoTagLoader
+{
+public:
+ CTagLoaderTagLib() = default;
+ ~CTagLoaderTagLib() override = default;
+ bool Load(const std::string& strFileName, MUSIC_INFO::CMusicInfoTag& tag,
+ EmbeddedArt *art = nullptr) override;
+ bool Load(const std::string& strFileName, MUSIC_INFO::CMusicInfoTag& tag,
+ const std::string& fallbackFileExtension, EmbeddedArt *art = nullptr);
+
+ static std::vector<std::string> SplitMBID(const std::vector<std::string> &values);
+protected:
+ static void SetArtist(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetArtistSort(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetArtistHints(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetAlbumArtist(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetAlbumArtistSort(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetAlbumArtistHints(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetComposerSort(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void SetDiscSubtitle(MUSIC_INFO::CMusicInfoTag& tag,
+ const std::vector<std::string>& values);
+ static void SetGenre(MUSIC_INFO::CMusicInfoTag& tag, const std::vector<std::string>& values);
+ static void SetReleaseType(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void AddArtistRole(MUSIC_INFO::CMusicInfoTag &tag, const std::string& strRole, const std::vector<std::string> &values);
+ static void AddArtistRole(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static void AddArtistInstrument(MUSIC_INFO::CMusicInfoTag &tag, const std::vector<std::string> &values);
+ static int POPMtoXBMC(int popm);
+
+template<typename T>
+ static bool ParseTag(T *tag, EmbeddedArt *art, MUSIC_INFO::CMusicInfoTag& infoTag);
+};
+
diff --git a/xbmc/music/tags/test/CMakeLists.txt b/xbmc/music/tags/test/CMakeLists.txt
new file mode 100644
index 0000000..e228a9c
--- /dev/null
+++ b/xbmc/music/tags/test/CMakeLists.txt
@@ -0,0 +1,3 @@
+set(SOURCES TestTagLoaderTagLib.cpp)
+
+core_add_test_library(musictags_test)
diff --git a/xbmc/music/tags/test/TestTagLoaderTagLib.cpp b/xbmc/music/tags/test/TestTagLoaderTagLib.cpp
new file mode 100644
index 0000000..0818f04
--- /dev/null
+++ b/xbmc/music/tags/test/TestTagLoaderTagLib.cpp
@@ -0,0 +1,230 @@
+/*
+ * 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 "music/tags/MusicInfoTag.h"
+#include "music/tags/TagLoaderTagLib.h"
+
+#include <gtest/gtest.h>
+#include <taglib/apetag.h>
+#include <taglib/asftag.h>
+#include <taglib/id3v1genres.h>
+#include <taglib/id3v1tag.h>
+#include <taglib/id3v2tag.h>
+#include <taglib/mp4tag.h>
+#include <taglib/tpropertymap.h>
+#include <taglib/xiphcomment.h>
+
+using namespace TagLib;
+using namespace MUSIC_INFO;
+
+template <typename T>
+class TestTagParser : public ::testing::Test, public CTagLoaderTagLib {
+ public:
+ T value_;
+};
+
+
+typedef ::testing::Types<ID3v2::Tag, ID3v1::Tag, ASF::Tag, APE::Tag, Ogg::XiphComment, MP4::Tag> TagTypes;
+TYPED_TEST_SUITE(TestTagParser, TagTypes);
+
+TYPED_TEST(TestTagParser, ParsesBasicTag) {
+ // Create a basic tag
+ TypeParam *tg = &this->value_;
+ // Configure a basic tag..
+ tg->setTitle ("title");
+ tg->setArtist ("artist");
+ tg->setAlbum ("album");
+ tg->setComment("comment");
+ tg->setGenre("Jazz");
+ tg->setYear (1985);
+ tg->setTrack (2);
+
+ CMusicInfoTag tag;
+ EXPECT_TRUE(CTagLoaderTagLib::ParseTag<TypeParam>(tg, NULL, tag));
+
+ EXPECT_EQ(1985, tag.GetYear());
+ EXPECT_EQ(2, tag.GetTrackNumber());
+ EXPECT_EQ(1u, tag.GetArtist().size());
+ if (!tag.GetArtist().empty())
+ {
+ EXPECT_EQ("artist", tag.GetArtist().front());
+ }
+ EXPECT_EQ("album", tag.GetAlbum());
+ EXPECT_EQ("comment", tag.GetComment());
+ EXPECT_EQ(1u, tag.GetGenre().size());
+ if (!tag.GetGenre().empty())
+ {
+ EXPECT_EQ("Jazz", tag.GetGenre().front());
+ }
+ EXPECT_EQ("title", tag.GetTitle());
+}
+
+
+TYPED_TEST(TestTagParser, HandleNullTag) {
+ // A Null tag should not parse, and not break us either
+ CMusicInfoTag tag;
+ EXPECT_FALSE(CTagLoaderTagLib::ParseTag<TypeParam>(NULL, NULL, tag));
+}
+
+template<typename T, size_t N>
+T * end(T (&ra)[N]) {
+ return ra + N;
+}
+
+const char *tags[] = { "APIC", "ASPI", "COMM", "COMR", "ENCR", "EQU2",
+ "ETCO", "GEOB", "GRID", "LINK", "MCDI", "MLLT", "OWNE", "PRIV", "PCNT",
+ "POPM", "POSS", "RBUF", "RVA2", "RVRB", "SEEK", "SIGN", "SYLT",
+ "SYTC", "TALB", "TBPM", "TCOM", "TCON", "TCOP", "TDEN", "TDLY", "TDOR",
+ "TDRC", "TDRL", "TDTG", "TENC", "TEXT", "TFLT", "TIPL", "TIT1", "TIT2",
+ "TIT3", "TKEY", "TLAN", "TLEN", "TMCL", "TMED", "TMOO", "TOAL", "TOFN",
+ "TOLY", "TOPE", "TOWN", "TPE1", "TPE2", "TPE3", "TPE4", "TPOS", "TPRO",
+ "TPUB", "TRCK", "TRSN", "TRSO", "TSOA", "TSOP", "TSOT", "TSRC", "TSSE",
+ "TSST", "TXXX", "UFID", "USER", "USLT", "WCOM", "WCOP", "WOAF", "WOAR",
+ "WOAS", "WORS", "WPAY", "WPUB", "WXXX", "ARTIST", "ARTISTS",
+ "ALBUMARTIST" , "ALBUM ARTIST", "ALBUMARTISTS" , "ALBUM ARTISTS", "ALBUM",
+ "TITLE", "TRACKNUMBER" "TRACK", "DISCNUMBER" "DISC", "YEAR", "GENRE",
+ "COMMENT", "CUESHEET", "ENCODEDBY", "COMPILATION", "LYRICS",
+ "REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN", "REPLAYGAIN_TRACK_PEAK",
+ "REPLAYGAIN_ALBUM_PEAK", "MUSICBRAINZ_ARTISTID",
+ "MUSICBRAINZ_ALBUMARTISTID", "RATING", "MUSICBRAINZ_ALBUMARTIST",
+ "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ_TRACKID", "METADATA_BLOCK_PICTURE",
+ "COVERART"
+};
+
+
+// This test exposes a bug in taglib library (#670) so for now we will not run it for all tag types
+// See https://github.com/taglib/taglib/issues/670 for details.
+typedef ::testing::Types<ID3v2::Tag, ID3v1::Tag, ASF::Tag, APE::Tag, Ogg::XiphComment> EmptyPropertiesTagTypes;
+template <typename T>
+class EmptyTagParser : public ::testing::Test, public CTagLoaderTagLib {
+ public:
+ T value_;
+};
+TYPED_TEST_SUITE(EmptyTagParser, EmptyPropertiesTagTypes);
+
+TYPED_TEST(EmptyTagParser, EmptyProperties) {
+ TypeParam *tg = &this->value_;
+ CMusicInfoTag tag;
+ PropertyMap props;
+ int tagcount = end(tags) - tags;
+ for(int i = 0; i < tagcount; i++) {
+ props.insert(tags[i], StringList());
+ }
+
+ // Even though all the properties are empty, we shouldn't
+ // crash
+ EXPECT_TRUE(CTagLoaderTagLib::ParseTag<TypeParam>(tg, NULL, tag));
+}
+
+
+
+TYPED_TEST(TestTagParser, FooProperties) {
+ TypeParam *tg = &this->value_;
+ CMusicInfoTag tag;
+ PropertyMap props;
+ int tagcount = end(tags) - tags;
+ for(int i = 0; i < tagcount; i++) {
+ props.insert(tags[i], String("foo"));
+ }
+ tg->setProperties(props);
+
+ EXPECT_TRUE(CTagLoaderTagLib::ParseTag<TypeParam>(tg, NULL, tag));
+ EXPECT_EQ(0, tag.GetYear());
+ EXPECT_EQ(0, tag.GetTrackNumber());
+ EXPECT_EQ(1u, tag.GetArtist().size());
+ if (!tag.GetArtist().empty())
+ {
+ EXPECT_EQ("foo", tag.GetArtist().front());
+ }
+ EXPECT_EQ("foo", tag.GetAlbum());
+ EXPECT_EQ("foo", tag.GetComment());
+ if (!tag.GetGenre().empty())
+ {
+ EXPECT_EQ("foo", tag.GetGenre().front());
+ }
+ EXPECT_EQ("foo", tag.GetTitle());
+}
+
+class TestTagLoaderTagLib : public ::testing::Test, public CTagLoaderTagLib {};
+TEST_F(TestTagLoaderTagLib, SetGenre)
+{
+ CMusicInfoTag tag, tag2;
+ const char *genre_nr[] = {"0", "2", "4"};
+ const char *names[] = { "Jazz", "Funk", "Ska" };
+ std::vector<std::string> genres(genre_nr, end(genre_nr));
+ std::vector<std::string> named_genre(names, end(names));
+
+ CTagLoaderTagLib::SetGenre(tag, genres);
+ EXPECT_EQ(3u, tag.GetGenre().size());
+ EXPECT_EQ("Blues", tag.GetGenre()[0]);
+ EXPECT_EQ("Country", tag.GetGenre()[1]);
+ EXPECT_EQ("Disco", tag.GetGenre()[2]);
+
+ CTagLoaderTagLib::SetGenre(tag2, named_genre);
+ EXPECT_EQ(3u, tag2.GetGenre().size());
+ for(int i = 0; i < 3; i++)
+ EXPECT_EQ(names[i], tag2.GetGenre()[i]);
+
+}
+
+TEST_F(TestTagLoaderTagLib, SplitMBID)
+{
+ CTagLoaderTagLib lib;
+
+ // SplitMBID should return the vector if it's empty or longer than 1
+ std::vector<std::string> values;
+ EXPECT_TRUE(lib.SplitMBID(values).empty());
+
+ values.emplace_back("1");
+ values.emplace_back("2");
+ EXPECT_EQ(values, lib.SplitMBID(values));
+
+ // length 1 and invalid should return empty
+ values.clear();
+ values.emplace_back("invalid");
+ EXPECT_TRUE(lib.SplitMBID(values).empty());
+
+ // length 1 and valid should return the valid id
+ values.clear();
+ values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+ EXPECT_EQ(lib.SplitMBID(values), values);
+
+ // case shouldn't matter
+ values.clear();
+ values.emplace_back("0383DaDf-2A4e-4d10-a46a-e9e041da8eb3");
+ EXPECT_EQ(lib.SplitMBID(values).size(), 1u);
+ EXPECT_STREQ(lib.SplitMBID(values)[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+
+ // valid with some stuff off the end or start should return valid
+ values.clear();
+ values.emplace_back("foo0383dadf-2a4e-4d10-a46a-e9e041da8eb3 blah");
+ EXPECT_EQ(lib.SplitMBID(values).size(), 1u);
+ EXPECT_STREQ(lib.SplitMBID(values)[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+
+ // two valid with various separators
+ values.clear();
+ values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3;53b106e7-0cc6-42cc-ac95-ed8d30a3a98e");
+ std::vector<std::string> result = lib.SplitMBID(values);
+ EXPECT_EQ(result.size(), 2u);
+ EXPECT_STREQ(result[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+ EXPECT_STREQ(result[1].c_str(), "53b106e7-0cc6-42cc-ac95-ed8d30a3a98e");
+
+ values.clear();
+ values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3/53b106e7-0cc6-42cc-ac95-ed8d30a3a98e");
+ result = lib.SplitMBID(values);
+ EXPECT_EQ(result.size(), 2u);
+ EXPECT_STREQ(result[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+ EXPECT_STREQ(result[1].c_str(), "53b106e7-0cc6-42cc-ac95-ed8d30a3a98e");
+
+ values.clear();
+ values.emplace_back("0383dadf-2a4e-4d10-a46a-e9e041da8eb3 / 53b106e7-0cc6-42cc-ac95-ed8d30a3a98e; ");
+ result = lib.SplitMBID(values);
+ EXPECT_EQ(result.size(), 2u);
+ EXPECT_STREQ(result[0].c_str(), "0383dadf-2a4e-4d10-a46a-e9e041da8eb3");
+ EXPECT_STREQ(result[1].c_str(), "53b106e7-0cc6-42cc-ac95-ed8d30a3a98e");
+}
diff --git a/xbmc/music/windows/CMakeLists.txt b/xbmc/music/windows/CMakeLists.txt
new file mode 100644
index 0000000..031d735
--- /dev/null
+++ b/xbmc/music/windows/CMakeLists.txt
@@ -0,0 +1,15 @@
+set(SOURCES GUIWindowMusicBase.cpp
+ GUIWindowMusicNav.cpp
+ GUIWindowMusicPlaylist.cpp
+ GUIWindowMusicPlaylistEditor.cpp
+ GUIWindowVisualisation.cpp
+ MusicFileItemListModifier.cpp)
+
+set(HEADERS GUIWindowMusicBase.h
+ GUIWindowMusicNav.h
+ GUIWindowMusicPlaylist.h
+ GUIWindowMusicPlaylistEditor.h
+ GUIWindowVisualisation.h
+ MusicFileItemListModifier.h)
+
+core_add_library(music_windows)
diff --git a/xbmc/music/windows/GUIWindowMusicBase.cpp b/xbmc/music/windows/GUIWindowMusicBase.cpp
new file mode 100644
index 0000000..df45141
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicBase.cpp
@@ -0,0 +1,1097 @@
+/*
+ * 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 "GUIWindowMusicBase.h"
+
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogMediaSource.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "music/MusicDbUrl.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/MusicUtils.h"
+#include "music/dialogs/GUIDialogInfoProviderSettings.h"
+#include "music/dialogs/GUIDialogMusicInfo.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+#ifdef HAS_CDDA_RIPPER
+#include "cdrip/CDDARipper.h"
+#endif
+#include "Autorun.h"
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIPassword.h"
+#include "PartyModeManager.h"
+#include "URL.h"
+#include "addons/gui/GUIDialogAddonInfo.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSmartPlaylistEditor.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/infoscanner/MusicInfoScanner.h"
+#include "music/tags/MusicInfoTag.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+#include "view/GUIViewState.h"
+
+#include <algorithm>
+
+using namespace XFILE;
+using namespace MUSICDATABASEDIRECTORY;
+using namespace MUSIC_GRABBER;
+using namespace MUSIC_INFO;
+using namespace KODI::MESSAGING;
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+using namespace std::chrono_literals;
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_BTNPLAYLISTS 7
+#define CONTROL_BTNSCAN 9
+#define CONTROL_BTNRIP 11
+
+CGUIWindowMusicBase::CGUIWindowMusicBase(int id, const std::string &xmlFile)
+ : CGUIMediaWindow(id, xmlFile.c_str())
+{
+ m_dlgProgress = NULL;
+ m_thumbLoader.SetObserver(this);
+}
+
+CGUIWindowMusicBase::~CGUIWindowMusicBase () = default;
+
+bool CGUIWindowMusicBase::OnBack(int actionID)
+{
+ if (!CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ CUtil::RemoveTempFiles();
+ }
+ return CGUIMediaWindow::OnBack(actionID);
+}
+
+/*!
+ \brief Handle messages on window.
+ \param message GUI Message that can be reacted on.
+ \return if a message can't be processed, return \e false
+
+ On these messages this class reacts.\n
+ When retrieving...
+ - #GUI_MSG_WINDOW_DEINIT\n
+ ...the last focused control is saved to m_iLastControl.
+ - #GUI_MSG_WINDOW_INIT\n
+ ...the musicdatabase is opend and the music extensions and shares are set.
+ The last focused control is set.
+ - #GUI_MSG_CLICKED\n
+ ... the base class reacts on the following controls:\n
+ Buttons:\n
+ - #CONTROL_BTNVIEWASICONS - switch between list, thumb and with large items
+ - #CONTROL_BTNSEARCH - Search for items\n
+ Other Controls:
+ - The container controls\n
+ Have the following actions in message them clicking on them.
+ - #ACTION_QUEUE_ITEM - add selected item to end of playlist
+ - #ACTION_QUEUE_ITEM_NEXT - add selected item to next pos in playlist
+ - #ACTION_SHOW_INFO - retrieve album info from the internet
+ - #ACTION_SELECT_ITEM - Item has been selected. Overwrite OnClick() to react on it
+ */
+bool CGUIWindowMusicBase::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+ m_musicdatabase.Close();
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+
+ m_musicdatabase.Open();
+
+ if (!CGUIMediaWindow::OnMessage(message))
+ return false;
+
+ return true;
+ }
+ break;
+ case GUI_MSG_DIRECTORY_SCANNED:
+ {
+ CFileItem directory(message.GetStringParam(), true);
+
+ // Only update thumb on a local drive
+ if (directory.IsHD())
+ {
+ std::string strParent;
+ URIUtils::GetParentPath(directory.GetPath(), strParent);
+ if (directory.GetPath() == m_vecItems->GetPath() || strParent == m_vecItems->GetPath())
+ Refresh();
+ }
+ }
+ break;
+
+ // update the display
+ case GUI_MSG_SCAN_FINISHED:
+ case GUI_MSG_REFRESH_THUMBS: // Never called as is secondary msg sent as GUI_MSG_NOTIFY_ALL
+ Refresh();
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTNRIP)
+ {
+ OnRipCD();
+ }
+ else if (iControl == CONTROL_BTNPLAYLISTS)
+ {
+ if (!m_vecItems->IsPath("special://musicplaylists/"))
+ Update("special://musicplaylists/");
+ }
+ else if (iControl == CONTROL_BTNSCAN)
+ {
+ OnScan(-1);
+ }
+ else if (m_viewControl.HasControl(iControl)) // list/thumb control
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ int iAction = message.GetParam1();
+
+ // iItem is checked for validity inside these routines
+ if (iAction == ACTION_QUEUE_ITEM || iAction == ACTION_MOUSE_MIDDLE_CLICK)
+ {
+ OnQueueItem(iItem);
+ }
+ else if (iAction == ACTION_QUEUE_ITEM_NEXT)
+ {
+ OnQueueItem(iItem, true);
+ }
+ else if (iAction == ACTION_SHOW_INFO)
+ {
+ OnItemInfo(iItem);
+ }
+ else if (iAction == ACTION_DELETE_ITEM)
+ {
+ // is delete allowed?
+ // must be at the playlists directory
+ if (m_vecItems->IsPath("special://musicplaylists/"))
+ OnDeleteItem(iItem);
+
+ else
+ return false;
+ }
+ // use play button to add folders of items to temp playlist
+ else if (iAction == ACTION_PLAYER_PLAY)
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ // if playback is paused or playback speed != 1, return
+ if (appPlayer->IsPlayingAudio())
+ {
+ if (appPlayer->IsPausedPlayback())
+ return false;
+ if (appPlayer->GetPlaySpeed() != 1)
+ return false;
+ }
+
+ // not playing audio, or playback speed == 1
+ PlayItem(iItem);
+
+ return true;
+ }
+ }
+ }
+ break;
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (message.GetParam1()==GUI_MSG_REMOVED_MEDIA)
+ CUtil::DeleteDirectoryCache("r-");
+ }
+ break;
+ }
+ return CGUIMediaWindow::OnMessage(message);
+}
+
+bool CGUIWindowMusicBase::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_SHOW_PLAYLIST)
+ {
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC ||
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC).size() > 0)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST);
+ return true;
+ }
+ }
+
+ if (action.GetID() == ACTION_SCAN_ITEM)
+ {
+ int item = m_viewControl.GetSelectedItem();
+ if (item > -1 && m_vecItems->Get(item)->m_bIsFolder)
+ OnScan(item);
+
+ return true;
+ }
+
+ return CGUIMediaWindow::OnAction(action);
+}
+
+void CGUIWindowMusicBase::OnItemInfoAll(const std::string& strPath, bool refresh)
+{
+ if (StringUtils::EqualsNoCase(m_vecItems->GetContent(), "albums"))
+ {
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ return;
+
+ CMusicLibraryQueue::GetInstance().StartAlbumScan(strPath, refresh);
+ }
+ else if (StringUtils::EqualsNoCase(m_vecItems->GetContent(), "artists"))
+ {
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ return;
+
+ CMusicLibraryQueue::GetInstance().StartArtistScan(strPath, refresh);
+ }
+}
+
+void CGUIWindowMusicBase::OnItemInfo(int iItem)
+{
+ if ( iItem < 0 || iItem >= m_vecItems->Size() )
+ return;
+
+ CFileItemPtr item = m_vecItems->Get(iItem);
+
+ // Match visibility test of CMusicInfo::IsVisible
+ if (item->IsVideoDb() && item->HasVideoInfoTag() &&
+ (item->HasProperty("artist_musicid") || item->HasProperty("album_musicid")))
+ {
+ // Music video artist or album (navigation by music > music video > artist))
+ CGUIDialogMusicInfo::ShowFor(item.get());
+ return;
+ }
+
+ if (item->IsVideo() && item->HasVideoInfoTag() &&
+ item->GetVideoInfoTag()->m_type == MediaTypeMusicVideo)
+ { // Music video on a mixed current playlist or navigation by music > music video > artist > video
+ CGUIDialogVideoInfo::ShowFor(*item);
+ return;
+ }
+
+ if (!m_vecItems->IsPlugin() && (item->IsPlugin() || item->IsScript()))
+ {
+ CGUIDialogAddonInfo::ShowForItem(item);
+ return;
+ }
+
+ // Match visibility test of CMusicInfo::IsVisible
+ if (item->HasMusicInfoTag() && (item->GetMusicInfoTag()->GetType() == MediaTypeSong ||
+ item->GetMusicInfoTag()->GetType() == MediaTypeAlbum ||
+ item->GetMusicInfoTag()->GetType() == MediaTypeArtist))
+ CGUIDialogMusicInfo::ShowFor(item.get());
+}
+
+void CGUIWindowMusicBase::RefreshContent(const std::string& strContent)
+{
+ if ( CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_MUSIC_NAV &&
+ m_vecItems->GetContent() == strContent &&
+ m_vecItems->GetSortMethod() == SortByUserRating)
+ // When music library window is active and showing songs or albums sorted
+ // by userrating refresh the list to resort items and show new userrating
+ Refresh(true);
+}
+
+/// \brief Retrieve tag information for \e m_vecItems
+void CGUIWindowMusicBase::RetrieveMusicInfo()
+{
+ auto start = std::chrono::steady_clock::now();
+
+ OnRetrieveMusicInfo(*m_vecItems);
+
+ //! @todo Scan for multitrack items here...
+ std::vector<std::string> itemsForRemove;
+ CFileItemList itemsForAdd;
+ for (int i = 0; i < m_vecItems->Size(); ++i)
+ {
+ CFileItemPtr pItem = (*m_vecItems)[i];
+ if (pItem->m_bIsFolder || pItem->IsPlayList() || pItem->IsPicture() || pItem->IsLyrics() || pItem->IsVideo())
+ continue;
+
+ CMusicInfoTag& tag = *pItem->GetMusicInfoTag();
+ if (tag.Loaded() && !tag.GetCueSheet().empty())
+ pItem->LoadEmbeddedCue();
+
+ if (pItem->HasCueDocument()
+ && pItem->LoadTracksFromCueDocument(itemsForAdd))
+ {
+ itemsForRemove.push_back(pItem->GetPath());
+ }
+ }
+ for (size_t i = 0; i < itemsForRemove.size(); ++i)
+ {
+ for (int j = 0; j < m_vecItems->Size(); ++j)
+ {
+ if ((*m_vecItems)[j]->GetPath() == itemsForRemove[i])
+ {
+ m_vecItems->Remove(j);
+ break;
+ }
+ }
+ }
+ m_vecItems->Append(itemsForAdd);
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, "RetrieveMusicInfo() took {} ms", duration.count());
+}
+
+/// \brief Add selected list/thumb control item to playlist and start playing
+/// \param iItem Selected Item in list/thumb control
+void CGUIWindowMusicBase::OnQueueItem(int iItem, bool first)
+{
+ // don't re-queue items from playlist window
+ if (iItem < 0 || iItem >= m_vecItems->Size() || GetID() == WINDOW_MUSIC_PLAYLIST)
+ return;
+
+ // add item 2 playlist
+ const auto item = m_vecItems->Get(iItem);
+
+ if (item->IsRAR() || item->IsZIP())
+ return;
+
+ MUSIC_UTILS::QueueItem(item, first ? MUSIC_UTILS::QueuePosition::POSITION_BEGIN
+ : MUSIC_UTILS::QueuePosition::POSITION_END);
+
+ // select next item
+ m_viewControl.SetSelectedItem(iItem + 1);
+}
+
+void CGUIWindowMusicBase::UpdateButtons()
+{
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTNRIP, CServiceBroker::GetMediaManager().IsAudio());
+
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTNSCAN,
+ !(m_vecItems->IsVirtualDirectoryRoot() ||
+ m_vecItems->IsMusicDb()));
+
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ SET_CONTROL_LABEL(CONTROL_BTNSCAN, 14056); // Stop Scan
+ else
+ SET_CONTROL_LABEL(CONTROL_BTNSCAN, 102); // Scan
+
+ CGUIMediaWindow::UpdateButtons();
+}
+
+void CGUIWindowMusicBase::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+
+ if (item)
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // Check for the partymode playlist item.
+ // When "PartyMode.xsp" not exist, only context menu button is edit
+ if (item->IsSmartPlayList() &&
+ (item->GetPath() == profileManager->GetUserDataItem("PartyMode.xsp")) &&
+ !CFileUtils::Exists(item->GetPath()))
+ {
+ buttons.Add(CONTEXT_BUTTON_EDIT_SMART_PLAYLIST, 586);
+ return;
+ }
+
+ if (!item->IsParentFolder())
+ {
+ //! @todo get rid of IsAddonsPath and IsScript check. CanQueue should be enough!
+ if (item->CanQueue() && !item->IsAddonsPath() && !item->IsScript())
+ {
+ if (!item->m_bIsFolder &&
+ (!item->IsPlayList() ||
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_playlistAsFolders))
+ {
+ const CPlayerCoreFactory& playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ // check what players we have, if we have multiple display play with option
+ std::vector<std::string> players;
+ playerCoreFactory.GetPlayers(*item, players);
+ if (players.size() >= 1)
+ buttons.Add(CONTEXT_BUTTON_PLAY_WITH, 15213); // Play With...
+ }
+
+ if (item->IsSmartPlayList())
+ buttons.Add(CONTEXT_BUTTON_PLAY_PARTYMODE, 15216); // Play in Partymode
+
+ if (item->IsSmartPlayList() || m_vecItems->IsSmartPlayList())
+ buttons.Add(CONTEXT_BUTTON_EDIT_SMART_PLAYLIST, 586);
+ else if (item->IsPlayList() || m_vecItems->IsPlayList())
+ buttons.Add(CONTEXT_BUTTON_EDIT, 586);
+ }
+#ifdef HAS_DVD_DRIVE
+ // enable Rip CD Audio or Track button if we have an audio disc
+ if (CServiceBroker::GetMediaManager().IsDiscInDrive() && m_vecItems->IsCDDA())
+ {
+ // those cds can also include Audio Tracks: CDExtra and MixedMode!
+ MEDIA_DETECT::CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
+ if (pCdInfo->IsAudio(1) || pCdInfo->IsCDExtra(1) || pCdInfo->IsMixedMode(1))
+ buttons.Add(CONTEXT_BUTTON_RIP_TRACK, 610);
+ }
+#endif
+ }
+
+ // enable CDDB lookup if the current dir is CDDA
+ if (CServiceBroker::GetMediaManager().IsDiscInDrive() && m_vecItems->IsCDDA() &&
+ (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser))
+ {
+ buttons.Add(CONTEXT_BUTTON_CDDB, 16002);
+ }
+ }
+ CGUIMediaWindow::GetContextButtons(itemNumber, buttons);
+}
+
+void CGUIWindowMusicBase::GetNonContextButtons(CContextButtons &buttons)
+{
+}
+
+bool CGUIWindowMusicBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+
+ if (CGUIDialogContextMenu::OnContextButton("music", item, button))
+ {
+ if (button == CONTEXT_BUTTON_REMOVE_SOURCE)
+ OnRemoveSource(itemNumber);
+
+ Update(m_vecItems->GetPath());
+ return true;
+ }
+
+ switch (button)
+ {
+ case CONTEXT_BUTTON_INFO:
+ OnItemInfo(itemNumber);
+ return true;
+
+ case CONTEXT_BUTTON_EDIT:
+ {
+ std::string playlist = item->IsPlayList() ? item->GetPath() : m_vecItems->GetPath(); // save path as activatewindow will destroy our items
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST_EDITOR, playlist);
+ // need to update
+ m_vecItems->RemoveDiscCache(GetID());
+ return true;
+ }
+
+ case CONTEXT_BUTTON_EDIT_SMART_PLAYLIST:
+ {
+ std::string playlist = item->IsSmartPlayList() ? item->GetPath() : m_vecItems->GetPath(); // save path as activatewindow will destroy our items
+ if (CGUIDialogSmartPlaylistEditor::EditPlaylist(playlist, "music"))
+ Refresh(true); // need to update
+ return true;
+ }
+
+ case CONTEXT_BUTTON_PLAY_WITH:
+ {
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ std::vector<std::string> players;
+ playerCoreFactory.GetPlayers(*item, players);
+ std::string player = playerCoreFactory.SelectPlayerDialog(players);
+ if (!player.empty())
+ OnClick(itemNumber, player);
+ return true;
+ }
+
+ case CONTEXT_BUTTON_PLAY_PARTYMODE:
+ g_partyModeManager.Enable(PARTYMODECONTEXT_MUSIC, item->GetPath());
+ return true;
+
+ case CONTEXT_BUTTON_RIP_CD:
+ OnRipCD();
+ return true;
+
+#ifdef HAS_CDDA_RIPPER
+ case CONTEXT_BUTTON_CANCEL_RIP_CD:
+ KODI::CDRIP::CCDDARipper::GetInstance().CancelJobs();
+ return true;
+#endif
+
+ case CONTEXT_BUTTON_RIP_TRACK:
+ OnRipTrack(itemNumber);
+ return true;
+
+ case CONTEXT_BUTTON_SCAN:
+ // Check if scanning already and inform user
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ HELPERS::ShowOKDialogText(CVariant{ 189 }, CVariant{ 14057 });
+ else
+ OnScan(itemNumber, true);
+ return true;
+
+ case CONTEXT_BUTTON_CDDB:
+ if (m_musicdatabase.LookupCDDBInfo(true))
+ Refresh();
+ return true;
+
+ default:
+ break;
+ }
+
+ return CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowMusicBase::OnAddMediaSource()
+{
+ return CGUIDialogMediaSource::ShowAndAddMediaSource("music");
+}
+
+void CGUIWindowMusicBase::OnRipCD()
+{
+ if (CServiceBroker::GetMediaManager().IsAudio())
+ {
+ if (!g_application.CurrentFileItem().IsCDDA())
+ {
+#ifdef HAS_CDDA_RIPPER
+ KODI::CDRIP::CCDDARipper::GetInstance().RipCD();
+#endif
+ }
+ else
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{20099});
+ }
+}
+
+void CGUIWindowMusicBase::OnRipTrack(int iItem)
+{
+ if (CServiceBroker::GetMediaManager().IsAudio())
+ {
+ if (!g_application.CurrentFileItem().IsCDDA())
+ {
+#ifdef HAS_CDDA_RIPPER
+ CFileItemPtr item = m_vecItems->Get(iItem);
+ KODI::CDRIP::CCDDARipper::GetInstance().RipTrack(item.get());
+#endif
+ }
+ else
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{20099});
+ }
+}
+
+void CGUIWindowMusicBase::PlayItem(int iItem)
+{
+ // restrictions should be placed in the appropriate window code
+ // only call the base code if the item passes since this clears
+ // the current playlist
+
+ const CFileItemPtr pItem = m_vecItems->Get(iItem);
+#ifdef HAS_DVD_DRIVE
+ if (pItem->IsDVD())
+ {
+ MEDIA_DETECT::CAutorun::PlayDiscAskResume(pItem->GetPath());
+ return;
+ }
+#endif
+
+ // Check for the partymode playlist item, do nothing when "PartyMode.xsp" not exist
+ if (pItem->IsSmartPlayList())
+ {
+ const std::shared_ptr<CProfileManager> profileManager =
+ CServiceBroker::GetSettingsComponent()->GetProfileManager();
+ if ((pItem->GetPath() == profileManager->GetUserDataItem("PartyMode.xsp")) &&
+ !CFileUtils::Exists(pItem->GetPath()))
+ return;
+ }
+
+ // if its a folder, build a playlist
+ if (pItem->m_bIsFolder && !pItem->IsPlugin())
+ {
+ // make a copy so that we can alter the queue state
+ CFileItemPtr item(new CFileItem(*m_vecItems->Get(iItem)));
+
+ // Allow queuing of unqueueable items
+ // when we try to queue them directly
+ if (!item->CanQueue())
+ item->SetCanQueue(true);
+
+ // skip ".."
+ if (item->IsParentFolder())
+ return;
+
+ CFileItemList queuedItems;
+ MUSIC_UTILS::GetItemsForPlayList(item, queuedItems);
+ if (g_partyModeManager.IsEnabled())
+ {
+ g_partyModeManager.AddUserSongs(queuedItems, true);
+ return;
+ }
+
+ /*
+ std::string strPlayListDirectory = m_vecItems->GetPath();
+ URIUtils::RemoveSlashAtEnd(strPlayListDirectory);
+ */
+
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_MUSIC);
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_MUSIC, queuedItems);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+
+ // play!
+ CServiceBroker::GetPlaylistPlayer().Play();
+ }
+ else if (pItem->IsPlayList())
+ {
+ // load the playlist the old way
+ LoadPlayList(pItem->GetPath());
+ }
+ else
+ {
+ // just a single item, play it
+ //! @todo Add music-specific code for single playback of an item here (See OnClick in MediaWindow, and OnPlayMedia below)
+ OnClick(iItem);
+ }
+}
+
+void CGUIWindowMusicBase::LoadPlayList(const std::string& strPlayList)
+{
+ // if partymode is active, we disable it
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Disable();
+
+ // load a playlist like .m3u, .pls
+ // first get correct factory to load playlist
+ std::unique_ptr<PLAYLIST::CPlayList> pPlayList(PLAYLIST::CPlayListFactory::Create(strPlayList));
+ if (pPlayList)
+ {
+ // load it
+ if (!pPlayList->Load(strPlayList))
+ {
+ HELPERS::ShowOKDialogText(CVariant{6}, CVariant{477});
+ return; //hmmm unable to load playlist?
+ }
+ }
+
+ int iSize = pPlayList->size();
+ if (g_application.ProcessAndStartPlaylist(strPlayList, *pPlayList, PLAYLIST::TYPE_MUSIC))
+ {
+ if (m_guiState)
+ m_guiState->SetPlaylistDirectory("playlistmusic://");
+ // activate the playlist window if its not activated yet
+ if (GetID() == CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() && iSize > 1)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST);
+ }
+ }
+}
+
+bool CGUIWindowMusicBase::OnPlayMedia(int iItem, const std::string &player)
+{
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+
+ // party mode
+ if (g_partyModeManager.IsEnabled())
+ {
+ PLAYLIST::CPlayList playlistTemp;
+ playlistTemp.Add(pItem);
+ g_partyModeManager.AddUserSongs(playlistTemp, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICPLAYER_QUEUEBYDEFAULT));
+ return true;
+ }
+ else if (!pItem->IsPlayList() && !pItem->IsInternetStream())
+ { // single music file - if we get here then we have autoplaynextitem turned off or queuebydefault
+ // turned on, but we still want to use the playlist player in order to handle more queued items
+ // following etc.
+ if ( (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICPLAYER_QUEUEBYDEFAULT) && CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_MUSIC_PLAYLIST_EDITOR) )
+ {
+ //! @todo Should the playlist be cleared if nothing is already playing?
+ OnQueueItem(iItem);
+ return true;
+ }
+ pItem->SetProperty("playlist_type_hint", m_guiState->GetPlaylist());
+ CServiceBroker::GetPlaylistPlayer().Play(pItem, player);
+ return true;
+ }
+ return CGUIMediaWindow::OnPlayMedia(iItem, player);
+}
+
+/// \brief Can be overwritten to implement an own tag filling function.
+/// \param items File items to fill
+void CGUIWindowMusicBase::OnRetrieveMusicInfo(CFileItemList& items)
+{
+ // No need to attempt to read music file tags for music videos
+ if (items.IsVideoDb())
+ return;
+ if (items.GetFolderCount()==items.Size() || items.IsMusicDb() ||
+ (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICFILES_USETAGS) && !items.IsCDDA()))
+ {
+ return;
+ }
+ // Start the music info loader thread
+ m_musicInfoLoader.SetProgressCallback(m_dlgProgress);
+ m_musicInfoLoader.Load(items);
+
+ bool bShowProgress = !CServiceBroker::GetGUI()->GetWindowManager().HasModalDialog(true);
+ bool bProgressVisible = false;
+
+ auto start = std::chrono::steady_clock::now();
+
+ while (m_musicInfoLoader.IsLoading())
+ {
+ if (bShowProgress)
+ { // Do we have to init a progress dialog?
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ if (!bProgressVisible && duration.count() > 1500 && m_dlgProgress)
+ { // tag loading takes more then 1.5 secs, show a progress dialog
+ CURL url(items.GetPath());
+ m_dlgProgress->SetHeading(CVariant{189});
+ m_dlgProgress->SetLine(0, CVariant{505});
+ m_dlgProgress->SetLine(1, CVariant{""});
+ m_dlgProgress->SetLine(2, CVariant{url.GetWithoutUserDetails()});
+ m_dlgProgress->Open();
+ m_dlgProgress->ShowProgressBar(true);
+ bProgressVisible = true;
+ }
+
+ if (bProgressVisible && m_dlgProgress && !m_dlgProgress->IsCanceled())
+ { // keep GUI alive
+ m_dlgProgress->Progress();
+ }
+ } // if (bShowProgress)
+ KODI::TIME::Sleep(1ms);
+ } // while (m_musicInfoLoader.IsLoading())
+
+ if (bProgressVisible && m_dlgProgress)
+ m_dlgProgress->Close();
+}
+
+bool CGUIWindowMusicBase::GetDirectory(const std::string &strDirectory, CFileItemList &items)
+{
+ items.ClearArt();
+ bool bResult = CGUIMediaWindow::GetDirectory(strDirectory, items);
+ if (bResult)
+ {
+ // We want to expand disc images when browsing in file view but not on library, smartplaylist
+ // or node menu music windows
+ if (!items.GetPath().empty() && !StringUtils::StartsWithNoCase(items.GetPath(), "musicdb://") &&
+ !StringUtils::StartsWithNoCase(items.GetPath(), "special://") &&
+ !StringUtils::StartsWithNoCase(items.GetPath(), "library://"))
+ CDirectory::FilterFileDirectories(items, ".iso", true);
+
+ CMusicThumbLoader loader;
+ loader.FillThumb(items);
+
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(items.GetPath(), params);
+
+ // Get art for directory when album or artist
+ bool artfound = false;
+ std::vector<ArtForThumbLoader> art;
+ if (params.GetAlbumId() > 0)
+ { // Get album and related artist(s) art
+ artfound = m_musicdatabase.GetArtForItem(-1, params.GetAlbumId(), -1, false, art);
+ }
+ else if (params.GetArtistId() > 0)
+ { // get artist art
+ artfound = m_musicdatabase.GetArtForItem(-1, -1, params.GetArtistId(), true, art);
+ }
+ if (artfound)
+ {
+ std::string dirType = MediaTypeArtist;
+ if (params.GetAlbumId() > 0)
+ dirType = MediaTypeAlbum;
+ std::map<std::string, std::string> artmap;
+ for (auto artitem : art)
+ {
+ std::string artname;
+ if (dirType == artitem.mediaType)
+ artname = artitem.artType;
+ else if (artitem.prefix.empty())
+ artname = artitem.mediaType + "." + artitem.artType;
+ else
+ {
+ if (dirType == MediaTypeAlbum)
+ StringUtils::Replace(artitem.prefix, "albumartist", "artist");
+ artname = artitem.prefix + "." + artitem.artType;
+ }
+ artmap.insert(std::make_pair(artname, artitem.url));
+ }
+ items.SetArt(artmap);
+ }
+
+ int iWindow = GetID();
+ // Add "New Playlist" items when in the playlists folder, except on playlist editor screen
+ if ((iWindow != WINDOW_MUSIC_PLAYLIST_EDITOR) &&
+ (items.GetPath() == "special://musicplaylists/") && !items.Contains("newplaylist://"))
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ CFileItemPtr newPlaylist(new CFileItem(profileManager->GetUserDataItem("PartyMode.xsp"),false));
+ newPlaylist->SetLabel(g_localizeStrings.Get(16035));
+ newPlaylist->SetLabelPreformatted(true);
+ newPlaylist->SetArt("icon", "DefaultPartyMode.png");
+ newPlaylist->m_bIsFolder = true;
+ items.Add(newPlaylist);
+
+ newPlaylist.reset(new CFileItem("newplaylist://", false));
+ newPlaylist->SetLabel(g_localizeStrings.Get(525));
+ newPlaylist->SetArt("icon", "DefaultAddSource.png");
+ newPlaylist->SetLabelPreformatted(true);
+ newPlaylist->SetSpecialSort(SortSpecialOnBottom);
+ newPlaylist->SetCanQueue(false);
+ items.Add(newPlaylist);
+
+ newPlaylist.reset(new CFileItem("newsmartplaylist://music", false));
+ newPlaylist->SetLabel(g_localizeStrings.Get(21437));
+ newPlaylist->SetArt("icon", "DefaultAddSource.png");
+ newPlaylist->SetLabelPreformatted(true);
+ newPlaylist->SetSpecialSort(SortSpecialOnBottom);
+ newPlaylist->SetCanQueue(false);
+ items.Add(newPlaylist);
+ }
+
+ // check for .CUE files here.
+ items.FilterCueItems();
+
+ std::string label;
+ if (items.GetLabel().empty() && m_rootDir.IsSource(items.GetPath(), CMediaSourceSettings::GetInstance().GetSources("music"), &label))
+ items.SetLabel(label);
+ }
+
+ return bResult;
+}
+
+bool CGUIWindowMusicBase::CheckFilterAdvanced(CFileItemList &items) const
+{
+ const std::string& content = items.GetContent();
+ if ((items.IsMusicDb() || CanContainFilter(m_strFilterPath)) &&
+ (StringUtils::EqualsNoCase(content, "artists") ||
+ StringUtils::EqualsNoCase(content, "albums") ||
+ StringUtils::EqualsNoCase(content, "songs")))
+ return true;
+
+ return false;
+}
+
+bool CGUIWindowMusicBase::CanContainFilter(const std::string &strDirectory) const
+{
+ return URIUtils::IsProtocol(strDirectory, "musicdb");
+}
+
+bool CGUIWindowMusicBase::OnSelect(int iItem)
+{
+ auto item = m_vecItems->Get(iItem);
+ if (item->IsAudioBook())
+ {
+ int bookmark;
+ if (m_musicdatabase.GetResumeBookmarkForAudioBook(*item, bookmark) && bookmark > 0)
+ {
+ // find which chapter the bookmark belongs to
+ auto itemIt =
+ std::find_if(m_vecItems->cbegin(), m_vecItems->cend(),
+ [&](const CFileItemPtr& item) { return bookmark < item->GetEndOffset(); });
+
+ if (itemIt != m_vecItems->cend())
+ {
+ // ask the user if they want to play or resume
+ CContextButtons choices;
+ choices.Add(MUSIC_SELECT_ACTION_PLAY, 208); // 208 = Play
+ choices.Add(MUSIC_SELECT_ACTION_RESUME,
+ StringUtils::Format(g_localizeStrings.Get(12022), // 12022 = Resume from ...
+ (*itemIt)->GetMusicInfoTag()->GetTitle()));
+
+ auto choice = CGUIDialogContextMenu::Show(choices);
+ if (choice == MUSIC_SELECT_ACTION_RESUME)
+ {
+ (*itemIt)->SetProperty("audiobook_bookmark", bookmark);
+ return CGUIMediaWindow::OnSelect(static_cast<int>(itemIt - m_vecItems->cbegin()));
+ }
+ else if (choice < 0)
+ return true;
+ }
+ }
+ }
+
+ return CGUIMediaWindow::OnSelect(iItem);
+}
+
+void CGUIWindowMusicBase::OnInitWindow()
+{
+ CGUIMediaWindow::OnInitWindow();
+ // Prompt for rescan of library to read music file tags that were not processed by previous versions
+ // and accommodate any changes to the way some tags are processed
+ if (m_musicdatabase.GetMusicNeedsTagScan() != 0)
+ {
+ if (CServiceBroker::GetGUI()
+ ->GetInfoManager()
+ .GetInfoProviders()
+ .GetLibraryInfoProvider()
+ .GetLibraryBool(LIBRARY_HAS_MUSIC) &&
+ !CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ // rescan of music library required
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{799}, CVariant{38060}))
+ {
+ int flags = CMusicInfoScanner::SCAN_RESCAN;
+ // When set to fetch information on update enquire about scraping that as well
+ // It may take some time, so the user may want to do it later by "Query Info For All"
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO))
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{799}, CVariant{38061}))
+ flags |= CMusicInfoScanner::SCAN_ONLINE;
+
+ CMusicLibraryQueue::GetInstance().ScanLibrary("", flags, true);
+
+ m_musicdatabase.SetMusicTagScanVersion(); // once is enough (user may interrupt, but that's up to them)
+ }
+ }
+ else
+ {
+ // no need to force a rescan if there's no music in the library or if a library scan is already active
+ m_musicdatabase.SetMusicTagScanVersion();
+ }
+ }
+}
+
+std::string CGUIWindowMusicBase::GetStartFolder(const std::string &dir)
+{
+ std::string lower(dir); StringUtils::ToLower(lower);
+ if (lower == "plugins" || lower == "addons")
+ return "addons://sources/audio/";
+ else if (lower == "$playlists" || lower == "playlists")
+ return "special://musicplaylists/";
+ return CGUIMediaWindow::GetStartFolder(dir);
+}
+
+void CGUIWindowMusicBase::OnScan(int iItem, bool bPromptRescan /*= false*/)
+{
+ std::string strPath;
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ strPath = m_vecItems->GetPath();
+ else if (m_vecItems->Get(iItem)->m_bIsFolder)
+ strPath = m_vecItems->Get(iItem)->GetPath();
+ else
+ { //! @todo MUSICDB - should we allow scanning a single item into the database?
+ //! This will require changes to the info scanner, which assumes we're running on a folder
+ strPath = m_vecItems->GetPath();
+ }
+ // Ask for full rescan of music files when scan item from file view context menu
+ bool doRescan = false;
+ if (bPromptRescan)
+ doRescan = CGUIDialogYesNo::ShowAndGetInput(CVariant{ 799 }, CVariant{ 38062 });
+
+ DoScan(strPath, doRescan);
+}
+
+void CGUIWindowMusicBase::DoScan(const std::string &strPath, bool bRescan /*= false*/)
+{
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ CMusicLibraryQueue::GetInstance().StopLibraryScanning();
+ return;
+ }
+
+ // Start background loader
+ int iControl=GetFocusedControlID();
+ int flags = 0;
+ if (bRescan)
+ flags = CMusicInfoScanner::SCAN_RESCAN;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO))
+ flags |= CMusicInfoScanner::SCAN_ONLINE;
+
+ CMusicLibraryQueue::GetInstance().ScanLibrary(strPath, flags, true);
+
+ SET_CONTROL_FOCUS(iControl, 0);
+ UpdateButtons();
+}
+
+void CGUIWindowMusicBase::OnRemoveSource(int iItem)
+{
+
+ //Remove music source from library, even when leaving songs
+ CMusicDatabase database;
+ database.Open();
+ database.RemoveSource(m_vecItems->Get(iItem)->GetLabel());
+
+ bool bCanceled;
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{522}, CVariant{20340}, bCanceled, CVariant{""}, CVariant{""}, CGUIDialogYesNo::NO_TIMEOUT))
+ {
+ MAPSONGS songs;
+ database.RemoveSongsFromPath(m_vecItems->Get(iItem)->GetPath(), songs, false);
+ database.CleanupOrphanedItems();
+ database.CheckArtistLinksChanged();
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
+ m_vecItems->RemoveDiscCache(GetID());
+ }
+ database.Close();
+}
+
+void CGUIWindowMusicBase::OnPrepareFileItems(CFileItemList &items)
+{
+ CGUIMediaWindow::OnPrepareFileItems(items);
+
+ if (!items.IsMusicDb() && !items.IsSmartPlayList())
+ RetrieveMusicInfo();
+}
+
+void CGUIWindowMusicBase::OnAssignContent(const std::string& oldName, const CMediaSource& source)
+{
+ // Music scrapers are not source specific, so unlike video there is no content selection logic here.
+ // Called on having added or edited a music source, this starts scanning items into library when required
+
+ //! @todo: do async as updating sources for all albums could be slow??
+ //Store music source in the music library, even those not scanned
+ CMusicDatabase database;
+ database.Open();
+ database.UpdateSource(oldName, source.strName, source.strPath, source.vecPaths);
+ database.Close();
+
+ // "Add to library" yes/no dialog with additional "settings" custom button
+ // "Do you want to add the media from this source to your library?"
+ DialogResponse rep = DialogResponse::CHOICE_CUSTOM;
+ while (rep == DialogResponse::CHOICE_CUSTOM)
+ {
+ rep = HELPERS::ShowYesNoCustomDialog(CVariant{20444}, CVariant{20447}, CVariant{106}, CVariant{107}, CVariant{10004});
+ if (rep == DialogResponse::CHOICE_CUSTOM)
+ // Edit default info provider settings so can be applied during scan
+ CGUIDialogInfoProviderSettings::Show();
+ }
+ if (rep == DialogResponse::CHOICE_YES)
+ CMusicLibraryQueue::GetInstance().ScanLibrary(source.strPath,
+ MUSIC_INFO::CMusicInfoScanner::SCAN_NORMAL, true);
+}
+
diff --git a/xbmc/music/windows/GUIWindowMusicBase.h b/xbmc/music/windows/GUIWindowMusicBase.h
new file mode 100644
index 0000000..85f4931
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicBase.h
@@ -0,0 +1,106 @@
+/*
+ * 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
+
+/*!
+\file GUIWindowMusicBase.h
+\brief
+*/
+
+#include "music/MusicDatabase.h"
+#include "music/MusicInfoLoader.h"
+#include "music/MusicThumbLoader.h"
+#include "music/infoscanner/MusicInfoScraper.h"
+#include "windows/GUIMediaWindow.h"
+
+#include <vector>
+
+enum MusicSelectAction
+{
+ MUSIC_SELECT_ACTION_PLAY,
+ MUSIC_SELECT_ACTION_RESUME,
+};
+
+/*!
+ \ingroup windows
+ \brief The base class for music windows
+
+ CGUIWindowMusicBase is the base class for
+ all music windows.
+ */
+class CGUIWindowMusicBase : public CGUIMediaWindow, public IBackgroundLoaderObserver
+{
+public:
+ CGUIWindowMusicBase(int id, const std::string &xmlFile);
+ ~CGUIWindowMusicBase(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnBack(int actionID) override;
+
+ void DoScan(const std::string &strPath, bool bRescan = false);
+ void RefreshContent(const std::string& strContent);
+
+ /*! \brief Once a music source is added, store source in library, and prompt
+ the user to scan this folder into the library
+ \param oldName the original music source name
+ \param source details of the music source (just added or edited)
+ */
+ static void OnAssignContent(const std::string& oldName, const CMediaSource& source);
+
+protected:
+ void OnInitWindow() override;
+ /*!
+ \brief Will be called when an popup context menu has been asked for
+ \param itemNumber List/thumb control item that has been clicked on
+ */
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ void GetNonContextButtons(CContextButtons &buttons);
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool OnAddMediaSource() override;
+ /*!
+ \brief Overwrite to update your gui buttons (visible, enable,...)
+ */
+ void UpdateButtons() override;
+
+ bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override;
+ virtual void OnRetrieveMusicInfo(CFileItemList& items);
+ void OnPrepareFileItems(CFileItemList& items) override;
+ void OnRipCD();
+ std::string GetStartFolder(const std::string &dir) override;
+ void OnItemLoaded(CFileItem* pItem) override {}
+
+ virtual void OnScan(int iItem, bool bPromptRescan = false);
+
+ bool CheckFilterAdvanced(CFileItemList &items) const override;
+ bool CanContainFilter(const std::string &strDirectory) const override;
+
+ bool OnSelect(int iItem) override;
+
+ // new methods
+ virtual void PlayItem(int iItem);
+ bool OnPlayMedia(int iItem, const std::string &player = "") override;
+
+ void RetrieveMusicInfo();
+ void OnItemInfo(int iItem);
+ void OnItemInfoAll(const std::string& strPath, bool refresh = false);
+ virtual void OnQueueItem(int iItem, bool first = false);
+ enum ALLOW_SELECTION { SELECTION_ALLOWED = 0, SELECTION_AUTO, SELECTION_FORCED };
+
+ void OnRipTrack(int iItem);
+ void LoadPlayList(const std::string& strPlayList) override;
+ virtual void OnRemoveSource(int iItem);
+
+ typedef std::vector <CFileItem*>::iterator ivecItems; ///< CFileItem* vector Iterator
+ CGUIDialogProgress* m_dlgProgress; ///< Progress dialog
+
+ CMusicDatabase m_musicdatabase;
+ MUSIC_INFO::CMusicInfoLoader m_musicInfoLoader;
+
+ CMusicThumbLoader m_thumbLoader;
+};
diff --git a/xbmc/music/windows/GUIWindowMusicNav.cpp b/xbmc/music/windows/GUIWindowMusicNav.cpp
new file mode 100644
index 0000000..f8ba5a4
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicNav.cpp
@@ -0,0 +1,944 @@
+/*
+ * 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 "GUIWindowMusicNav.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "PartyModeManager.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/AddonSystemSettings.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/dialogs/GUIDialogInfoProviderSettings.h"
+#include "music/tags/MusicInfoTag.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/LegacyPathTranslation.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+#include "video/windows/GUIWindowVideoNav.h"
+#include "view/GUIViewState.h"
+
+using namespace XFILE;
+using namespace PLAYLIST;
+using namespace MUSICDATABASEDIRECTORY;
+using namespace KODI::MESSAGING;
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_BTNTYPE 5
+#define CONTROL_LABELFILES 12
+
+#define CONTROL_SEARCH 8
+#define CONTROL_FILTER 15
+#define CONTROL_BTNPARTYMODE 16
+#define CONTROL_BTNMANUALINFO 17
+#define CONTROL_BTN_FILTER 19
+
+#define CONTROL_UPDATE_LIBRARY 20
+
+CGUIWindowMusicNav::CGUIWindowMusicNav(void)
+ : CGUIWindowMusicBase(WINDOW_MUSIC_NAV, "MyMusicNav.xml")
+{
+ m_vecItems->SetPath("?");
+ m_searchWithEdit = false;
+}
+
+CGUIWindowMusicNav::~CGUIWindowMusicNav(void) = default;
+
+bool CGUIWindowMusicNav::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_RESET:
+ m_vecItems->SetPath("?");
+ break;
+ case GUI_MSG_WINDOW_INIT:
+ {
+ /* We don't want to show Autosourced items (ie removable pendrives, memorycards) in Library mode */
+ m_rootDir.AllowNonLocalSources(false);
+
+ // is this the first time the window is opened?
+ if (m_vecItems->GetPath() == "?" && message.GetStringParam().empty())
+ message.SetStringParam(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW));
+
+ if (!CGUIWindowMusicBase::OnMessage(message))
+ return false;
+
+ if (message.GetStringParam(0) != "")
+ {
+ CURL url(message.GetStringParam(0));
+
+ int i = 0;
+ for (; i < m_vecItems->Size(); i++)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+
+ // skip ".."
+ if (pItem->IsParentFolder())
+ continue;
+
+ if (URIUtils::PathEquals(pItem->GetPath(), message.GetStringParam(0), true, true))
+ {
+ m_viewControl.SetSelectedItem(i);
+ i = -1;
+ if (url.GetOption("showinfo") == "true")
+ OnItemInfo(i);
+ break;
+ }
+ }
+ }
+
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTNPARTYMODE)
+ {
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Disable();
+ else
+ {
+ if (!g_partyModeManager.Enable())
+ {
+ SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE,false);
+ return false;
+ }
+
+ // Playlist directory is the root of the playlist window
+ if (m_guiState)
+ m_guiState->SetPlaylistDirectory("playlistmusic://");
+
+ return true;
+ }
+ UpdateButtons();
+ }
+ else if (iControl == CONTROL_SEARCH)
+ {
+ if (m_searchWithEdit)
+ {
+ // search updated - reset timer
+ m_searchTimer.StartZero();
+ // grab our search string
+ CGUIMessage selected(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_SEARCH);
+ OnMessage(selected);
+ SetProperty("search", selected.GetLabel());
+ return true;
+ }
+ std::string search(GetProperty("search").asString());
+ CGUIKeyboardFactory::ShowAndGetFilter(search, true);
+ SetProperty("search", search);
+ return true;
+ }
+ else if (iControl == CONTROL_UPDATE_LIBRARY)
+ {
+ if (!CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ CMusicLibraryQueue::GetInstance().ScanLibrary("");
+ else
+ CMusicLibraryQueue::GetInstance().StopLibraryScanning();
+ return true;
+ }
+ }
+ break;
+ case GUI_MSG_PLAYBACK_STOPPED:
+ case GUI_MSG_PLAYBACK_ENDED:
+ case GUI_MSG_PLAYLISTPLAYER_STOPPED:
+ case GUI_MSG_PLAYBACK_STARTED:
+ {
+ SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE, g_partyModeManager.IsEnabled());
+ }
+ break;
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (message.GetParam1() == GUI_MSG_SEARCH_UPDATE && IsActive())
+ {
+ // search updated - reset timer
+ m_searchTimer.StartZero();
+ SetProperty("search", message.GetStringParam());
+ }
+ }
+ }
+ return CGUIWindowMusicBase::OnMessage(message);
+}
+
+bool CGUIWindowMusicNav::OnAction(const CAction& action)
+{
+ if (action.GetID() == ACTION_SCAN_ITEM)
+ {
+ int item = m_viewControl.GetSelectedItem();
+ CMusicDatabaseDirectory dir;
+ if (item > -1 && m_vecItems->Get(item)->m_bIsFolder
+ && (m_vecItems->Get(item)->IsAlbum()||
+ dir.IsArtistDir(m_vecItems->Get(item)->GetPath())))
+ {
+ OnContextButton(item,CONTEXT_BUTTON_INFO);
+ return true;
+ }
+ }
+
+ return CGUIWindowMusicBase::OnAction(action);
+}
+
+bool CGUIWindowMusicNav::ManageInfoProvider(const CFileItemPtr& item)
+{
+ CQueryParams params;
+ CDirectoryNode::GetDatabaseInfo(item->GetPath(), params);
+ // Management of Info provider only valid for specific artist or album items
+ if (params.GetAlbumId() == -1 && params.GetArtistId() == -1)
+ return false;
+
+ // Set things up for processing artist or albums
+ CONTENT_TYPE content = CONTENT_ALBUMS;
+ int id = params.GetAlbumId();
+ if (id == -1)
+ {
+ content = CONTENT_ARTISTS;
+ id = params.GetArtistId();
+ }
+
+ ADDON::ScraperPtr scraper;
+ // Get specific scraper and settings for current item or use default
+ if (!m_musicdatabase.GetScraper(id, content, scraper))
+ {
+ ADDON::AddonPtr defaultScraper;
+ if (ADDON::CAddonSystemSettings::GetInstance().GetActive(
+ ADDON::ScraperTypeFromContent(content), defaultScraper))
+ {
+ scraper = std::dynamic_pointer_cast<ADDON::CScraper>(defaultScraper);
+ }
+ }
+
+ // Set Information provider and settings
+ int applyto = CGUIDialogInfoProviderSettings::Show(scraper);
+ if (applyto >= 0)
+ {
+ bool result = false;
+ CVariant msgctxt;
+ switch (applyto)
+ {
+ case INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_THISITEM: // Change information provider for specific item
+ result = m_musicdatabase.SetScraper(id, content, scraper);
+ break;
+ case INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_ALLVIEW: // Change information provider for the filtered items shown on this node
+ {
+ msgctxt = 38069;
+ if (content == CONTENT_ARTISTS)
+ msgctxt = 38068;
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{ 20195 }, msgctxt)) // Change information provider, confirm for all shown
+ {
+ // Set scraper for all items on current view.
+ std::string strPath = "musicdb://";
+ if (content == CONTENT_ARTISTS)
+ strPath += "artists";
+ else
+ strPath += "albums";
+ URIUtils::AddSlashAtEnd(strPath);
+ // Items on view could be limited by navigation criteria, smart playlist rules or a filter.
+ // Get these options, except ID, from item path
+ CURL musicUrl(item->GetPath()); //Use CURL, as CMusicDbUrl removes "filter" option
+ if (content == CONTENT_ARTISTS)
+ musicUrl.RemoveOption("artistid");
+ else
+ musicUrl.RemoveOption("albumid");
+ strPath += musicUrl.GetOptions();
+ result = m_musicdatabase.SetScraperAll(strPath, scraper);
+ }
+ }
+ break;
+ case INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_DEFAULT: // Change information provider for all items
+ {
+ msgctxt = 38071;
+ if (content == CONTENT_ARTISTS)
+ msgctxt = 38070;
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20195}, msgctxt)) // Change information provider, confirm default and clear
+ {
+ // Save scraper addon default setting values
+ scraper->SaveSettings();
+ // Set default scraper
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (content == CONTENT_ARTISTS)
+ settings->SetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER, scraper->ID());
+ else
+ settings->SetString(CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER, scraper->ID());
+ settings->Save();
+ // Clear all item specific settings
+ if (content == CONTENT_ARTISTS)
+ result = m_musicdatabase.SetScraperAll("musicdb://artists/", nullptr);
+ else
+ result = m_musicdatabase.SetScraperAll("musicdb://albums/", nullptr);
+ }
+ }
+ default:
+ break;
+ }
+ if (!result)
+ return false;
+
+ // Refresh additional information using the new settings
+ if (applyto == INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_ALLVIEW || applyto == INFOPROVIDERAPPLYOPTIONS::INFOPROVIDER_DEFAULT)
+ {
+ // Change information provider, all artists or albums
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20195}, CVariant{38072}))
+ OnItemInfoAll(m_vecItems->GetPath(), true);
+ }
+ else
+ {
+ // Change information provider, selected artist or album
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20195}, CVariant{38073}))
+ {
+ std::string itempath = StringUtils::Format("musicdb://albums/{}/", id);
+ if (content == CONTENT_ARTISTS)
+ itempath = StringUtils::Format("musicdb://artists/{}/", id);
+ OnItemInfoAll(itempath, true);
+ }
+ }
+ }
+ return true;
+}
+
+bool CGUIWindowMusicNav::OnClick(int iItem, const std::string &player /* = "" */)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size()) return false;
+
+ CFileItemPtr item = m_vecItems->Get(iItem);
+ if (StringUtils::StartsWith(item->GetPath(), "musicsearch://"))
+ {
+ if (m_searchWithEdit)
+ OnSearchUpdate();
+ else
+ {
+ std::string search(GetProperty("search").asString());
+ CGUIKeyboardFactory::ShowAndGetFilter(search, true);
+ SetProperty("search", search);
+ }
+ return true;
+ }
+ if (item->IsMusicDb() && !item->m_bIsFolder)
+ m_musicdatabase.SetPropertiesForFileItem(*item);
+
+ if (item->IsPlayList() &&
+ !CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_playlistAsFolders)
+ {
+ PlayItem(iItem);
+ return true;
+ }
+ return CGUIWindowMusicBase::OnClick(iItem, player);
+}
+
+bool CGUIWindowMusicNav::Update(const std::string &strDirectory, bool updateFilterPath /* = true */)
+{
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ if (CGUIWindowMusicBase::Update(strDirectory, updateFilterPath))
+ {
+ m_thumbLoader.Load(*m_unfilteredItems);
+ return true;
+ }
+
+ return false;
+}
+
+bool CGUIWindowMusicNav::GetDirectory(const std::string &strDirectory, CFileItemList &items)
+{
+ if (strDirectory.empty())
+ AddSearchFolder();
+
+ bool bResult = CGUIWindowMusicBase::GetDirectory(strDirectory, items);
+ if (bResult)
+ {
+ if (items.IsPlayList())
+ OnRetrieveMusicInfo(items);
+ }
+
+ // update our content in the info manager
+ if (StringUtils::StartsWithNoCase(strDirectory, "videodb://") || items.IsVideoDb())
+ {
+ CVideoDatabaseDirectory dir;
+ VIDEODATABASEDIRECTORY::NODE_TYPE node = dir.GetDirectoryChildType(items.GetPath());
+ if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MUSICVIDEOS ||
+ node == VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS)
+ items.SetContent("musicvideos");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_GENRE)
+ items.SetContent("genres");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_COUNTRY)
+ items.SetContent("countries");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_ACTOR)
+ items.SetContent("artists");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_DIRECTOR)
+ items.SetContent("directors");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_STUDIO)
+ items.SetContent("studios");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_YEAR)
+ items.SetContent("years");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_ALBUM)
+ items.SetContent("albums");
+ else if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_TAGS)
+ items.SetContent("tags");
+ else
+ items.SetContent("");
+ }
+ else if (StringUtils::StartsWithNoCase(strDirectory, "musicdb://") || items.IsMusicDb())
+ {
+ CMusicDatabaseDirectory dir;
+ NODE_TYPE node = dir.GetDirectoryChildType(items.GetPath());
+ if (node == NODE_TYPE_ALBUM ||
+ node == NODE_TYPE_ALBUM_RECENTLY_ADDED ||
+ node == NODE_TYPE_ALBUM_RECENTLY_PLAYED ||
+ node == NODE_TYPE_ALBUM_TOP100 ||
+ node == NODE_TYPE_DISC) // ! @todo: own content type "discs"??
+ items.SetContent("albums");
+ else if (node == NODE_TYPE_ARTIST)
+ items.SetContent("artists");
+ else if (node == NODE_TYPE_SONG ||
+ node == NODE_TYPE_SONG_TOP100 ||
+ node == NODE_TYPE_SINGLES ||
+ node == NODE_TYPE_ALBUM_RECENTLY_ADDED_SONGS ||
+ node == NODE_TYPE_ALBUM_RECENTLY_PLAYED_SONGS ||
+ node == NODE_TYPE_ALBUM_TOP100_SONGS)
+ items.SetContent("songs");
+ else if (node == NODE_TYPE_GENRE)
+ items.SetContent("genres");
+ else if (node == NODE_TYPE_SOURCE)
+ items.SetContent("sources");
+ else if (node == NODE_TYPE_ROLE)
+ items.SetContent("roles");
+ else if (node == NODE_TYPE_YEAR)
+ items.SetContent("years");
+ else
+ items.SetContent("");
+ }
+ else if (items.IsPlayList())
+ items.SetContent("songs");
+ else if (URIUtils::PathEquals(strDirectory, "special://musicplaylists/") ||
+ URIUtils::PathEquals(strDirectory, "library://music/playlists.xml/"))
+ items.SetContent("playlists");
+ else if (URIUtils::PathEquals(strDirectory, "plugin://music/"))
+ items.SetContent("plugins");
+ else if (items.IsAddonsPath())
+ items.SetContent("addons");
+ else if (!items.IsSourcesPath() && !items.IsVirtualDirectoryRoot() &&
+ !items.IsLibraryFolder() && !items.IsPlugin() && !items.IsSmartPlayList())
+ items.SetContent("files");
+
+ return bResult;
+}
+
+void CGUIWindowMusicNav::UpdateButtons()
+{
+ CGUIWindowMusicBase::UpdateButtons();
+
+ // Update object count
+ int iItems = m_vecItems->Size();
+ if (iItems)
+ {
+ // check for parent dir and "all" items
+ // should always be the first two items
+ for (int i = 0; i <= (iItems>=2 ? 1 : 0); i++)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+ if (pItem->IsParentFolder()) iItems--;
+ if (StringUtils::StartsWith(pItem->GetPath(), "/-1/")) iItems--;
+ }
+ // or the last item
+ if (m_vecItems->Size() > 2 &&
+ StringUtils::StartsWith(m_vecItems->Get(m_vecItems->Size()-1)->GetPath(), "/-1/"))
+ iItems--;
+ }
+ std::string items = StringUtils::Format("{} {}", iItems, g_localizeStrings.Get(127));
+ SET_CONTROL_LABEL(CONTROL_LABELFILES, items);
+
+ // set the filter label
+ std::string strLabel;
+
+ // "Playlists"
+ if (m_vecItems->IsPath("special://musicplaylists/"))
+ strLabel = g_localizeStrings.Get(136);
+ // "{Playlist Name}"
+ else if (m_vecItems->IsPlayList())
+ {
+ // get playlist name from path
+ std::string strDummy;
+ URIUtils::Split(m_vecItems->GetPath(), strDummy, strLabel);
+ }
+ // everything else is from a musicdb:// path
+ else
+ {
+ CMusicDatabaseDirectory dir;
+ dir.GetLabel(m_vecItems->GetPath(), strLabel);
+ }
+
+ SET_CONTROL_LABEL(CONTROL_FILTER, strLabel);
+
+ SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE, g_partyModeManager.IsEnabled());
+
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_UPDATE_LIBRARY, !m_vecItems->IsAddonsPath() && !m_vecItems->IsPlugin() && !m_vecItems->IsScript());
+}
+
+void CGUIWindowMusicNav::PlayItem(int iItem)
+{
+ // unlike additemtoplaylist, we need to check the items here
+ // before calling it since the current playlist will be stopped
+ // and cleared!
+
+ // root is not allowed
+ if (m_vecItems->IsVirtualDirectoryRoot() && !m_vecItems->Get(iItem)->IsDVD())
+ return;
+
+ CGUIWindowMusicBase::PlayItem(iItem);
+}
+
+void CGUIWindowMusicNav::OnWindowLoaded()
+{
+ const CGUIControl *control = GetControl(CONTROL_SEARCH);
+ m_searchWithEdit = (control && control->GetControlType() == CGUIControl::GUICONTROL_EDIT);
+
+ CGUIWindowMusicBase::OnWindowLoaded();
+
+ if (m_searchWithEdit)
+ {
+ SendMessage(GUI_MSG_SET_TYPE, CONTROL_SEARCH, CGUIEditControl::INPUT_TYPE_SEARCH);
+ SET_CONTROL_LABEL2(CONTROL_SEARCH, GetProperty("search").asString());
+ }
+}
+
+void CGUIWindowMusicNav::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+ if (item)
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // are we in the playlists location?
+ bool inPlaylists = m_vecItems->IsPath(CUtil::MusicPlaylistsLocation()) ||
+ m_vecItems->IsPath("special://musicplaylists/");
+
+ if (m_vecItems->IsPath("sources://music/"))
+ {
+ // get the usual music shares, and anything for all media windows
+ CGUIDialogContextMenu::GetContextButtons("music", item, buttons);
+#ifdef HAS_DVD_DRIVE
+ // enable Rip CD an audio disc
+ if (CServiceBroker::GetMediaManager().IsDiscInDrive() && item->IsCDDA())
+ {
+ // those cds can also include Audio Tracks: CDExtra and MixedMode!
+ MEDIA_DETECT::CCdInfo* pCdInfo = CServiceBroker::GetMediaManager().GetCdInfo();
+ if (pCdInfo->IsAudio(1) || pCdInfo->IsCDExtra(1) || pCdInfo->IsMixedMode(1))
+ {
+ if (CServiceBroker::GetJobManager()->IsProcessing("cdrip"))
+ buttons.Add(CONTEXT_BUTTON_CANCEL_RIP_CD, 14100);
+ else
+ buttons.Add(CONTEXT_BUTTON_RIP_CD, 600);
+ }
+ }
+#endif
+ // Scan button for music sources except ".." and "Add music source" items
+ if (!item->IsPath("add") && !item->IsParentFolder() &&
+ (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser))
+ {
+ buttons.Add(CONTEXT_BUTTON_SCAN, 13352);
+ }
+ CGUIMediaWindow::GetContextButtons(itemNumber, buttons);
+ }
+ else
+ {
+ CGUIWindowMusicBase::GetContextButtons(itemNumber, buttons);
+
+ // Scan button for real folders containing files when navigating within music sources.
+ // Blacklist the bespoke Kodi protocols as to many valid external protocols to whitelist
+ if (m_vecItems->GetContent() == "files" && // Other content not scanned to library
+ !inPlaylists && !m_vecItems->IsInternetStream() && // Not playlists locations or streams
+ !item->IsPath("add") && !item->IsParentFolder() && // Not ".." and "Add items
+ item->m_bIsFolder && // Folders only, but playlists can be folders too
+ !URIUtils::IsLibraryContent(item->GetPath()) && // database folder or .xsp files
+ !URIUtils::IsSpecial(item->GetPath()) && !item->IsPlugin() && !item->IsScript() &&
+ !item->IsPlayList() && // .m3u etc. that as flagged as folders when playlistasfolders
+ !StringUtils::StartsWithNoCase(item->GetPath(), "addons://") &&
+ (profileManager->GetCurrentProfile().canWriteDatabases() ||
+ g_passwordManager.bMasterUser))
+ {
+ buttons.Add(CONTEXT_BUTTON_SCAN, 13352);
+ }
+
+ CMusicDatabaseDirectory dir;
+
+ if (!item->IsParentFolder() && !dir.IsAllItem(item->GetPath()))
+ {
+ if (item->m_bIsFolder && !item->IsVideoDb() &&
+ !item->IsPlugin() && !StringUtils::StartsWithNoCase(item->GetPath(), "musicsearch://"))
+ {
+ if (item->IsAlbum())
+ // enable query all albums button only in album view
+ buttons.Add(CONTEXT_BUTTON_INFO_ALL, 20059);
+ else if (dir.IsArtistDir(item->GetPath()))
+ // enable query all artist button only in artist view
+ buttons.Add(CONTEXT_BUTTON_INFO_ALL, 21884);
+
+ //Set default or clear default
+ NODE_TYPE nodetype = dir.GetDirectoryType(item->GetPath());
+ if (!inPlaylists &&
+ (nodetype == NODE_TYPE_ROOT ||
+ nodetype == NODE_TYPE_OVERVIEW ||
+ nodetype == NODE_TYPE_TOP100))
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (!item->IsPath(settings->GetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW)))
+ buttons.Add(CONTEXT_BUTTON_SET_DEFAULT, 13335); // set default
+ if (!settings->GetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW).empty())
+ buttons.Add(CONTEXT_BUTTON_CLEAR_DEFAULT, 13403); // clear default
+ }
+
+ //Change information provider
+ if (StringUtils::EqualsNoCase(m_vecItems->GetContent(), "albums") ||
+ StringUtils::EqualsNoCase(m_vecItems->GetContent(), "artists"))
+ {
+ // we allow the user to set information provider for albums and artists
+ buttons.Add(CONTEXT_BUTTON_SET_CONTENT, 20195);
+ }
+ }
+ if (item->HasMusicInfoTag() && !item->GetMusicInfoTag()->GetArtistString().empty())
+ {
+ CVideoDatabase database;
+ database.Open();
+ if (database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString()) > -1)
+ buttons.Add(CONTEXT_BUTTON_GO_TO_ARTIST, 20400);
+ }
+ if (item->HasMusicInfoTag() && !item->GetMusicInfoTag()->GetArtistString().empty() &&
+ !item->GetMusicInfoTag()->GetAlbum().empty() &&
+ !item->GetMusicInfoTag()->GetTitle().empty())
+ {
+ CVideoDatabase database;
+ database.Open();
+ if (database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString(), item->GetMusicInfoTag()->GetAlbum(), item->GetMusicInfoTag()->GetTitle()) > -1)
+ buttons.Add(CONTEXT_BUTTON_PLAY_OTHER, 20401);
+ }
+ if (item->HasVideoInfoTag() && !item->m_bIsFolder)
+ {
+ if ((profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser) && !item->IsPlugin())
+ {
+ buttons.Add(CONTEXT_BUTTON_RENAME, 16105);
+ buttons.Add(CONTEXT_BUTTON_DELETE, 646);
+ }
+ }
+ if (inPlaylists && URIUtils::GetFileName(item->GetPath()) != "PartyMode.xsp"
+ && (item->IsPlayList() || item->IsSmartPlayList()))
+ buttons.Add(CONTEXT_BUTTON_DELETE, 117);
+
+ if (!item->IsReadOnly() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool("filelists.allowfiledeletion"))
+ {
+ buttons.Add(CONTEXT_BUTTON_DELETE, 117);
+ buttons.Add(CONTEXT_BUTTON_RENAME, 118);
+ }
+ }
+ }
+ }
+ // noncontextual buttons
+
+ CGUIWindowMusicBase::GetNonContextButtons(buttons);
+}
+
+bool CGUIWindowMusicNav::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+
+ switch (button)
+ {
+ case CONTEXT_BUTTON_INFO:
+ {
+ if (!item->IsVideoDb())
+ return CGUIWindowMusicBase::OnContextButton(itemNumber,button);
+
+ // music videos - artists
+ if (StringUtils::StartsWithNoCase(item->GetPath(), "videodb://musicvideos/artists/"))
+ {
+ int idArtist = m_musicdatabase.GetArtistByName(item->GetLabel());
+ if (idArtist == -1)
+ return false;
+ std::string path = StringUtils::Format("musicdb://artists/{}/", idArtist);
+ CArtist artist;
+ m_musicdatabase.GetArtist(idArtist, artist, false);
+ *item = CFileItem(artist);
+ item->SetPath(path);
+ CGUIWindowMusicBase::OnContextButton(itemNumber,button);
+ Refresh();
+ m_viewControl.SetSelectedItem(itemNumber);
+ return true;
+ }
+
+ // music videos - albums
+ if (StringUtils::StartsWithNoCase(item->GetPath(), "videodb://musicvideos/albums/"))
+ {
+ int idAlbum = m_musicdatabase.GetAlbumByName(item->GetLabel());
+ if (idAlbum == -1)
+ return false;
+ std::string path = StringUtils::Format("musicdb://albums/{}/", idAlbum);
+ CAlbum album;
+ m_musicdatabase.GetAlbum(idAlbum, album, false);
+ *item = CFileItem(path,album);
+ item->SetPath(path);
+ CGUIWindowMusicBase::OnContextButton(itemNumber,button);
+ Refresh();
+ m_viewControl.SetSelectedItem(itemNumber);
+ return true;
+ }
+
+ if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strTitle.empty())
+ {
+ CGUIDialogVideoInfo::ShowFor(*item);
+ Refresh();
+ }
+ return true;
+ }
+
+ case CONTEXT_BUTTON_INFO_ALL:
+ OnItemInfoAll(m_vecItems->GetPath());
+ return true;
+
+ case CONTEXT_BUTTON_SET_DEFAULT:
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW, item->GetPath());
+ settings->Save();
+ return true;
+ }
+
+ case CONTEXT_BUTTON_CLEAR_DEFAULT:
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetString(CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW, "");
+ settings->Save();
+ return true;
+ }
+
+ case CONTEXT_BUTTON_GO_TO_ARTIST:
+ {
+ std::string strPath;
+ CVideoDatabase database;
+ database.Open();
+ strPath = StringUtils::Format(
+ "videodb://musicvideos/artists/{}/",
+ database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString()));
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_NAV,strPath);
+ return true;
+ }
+
+ case CONTEXT_BUTTON_PLAY_OTHER:
+ {
+ CVideoDatabase database;
+ database.Open();
+ CVideoInfoTag details;
+ database.GetMusicVideoInfo("", details, database.GetMatchingMusicVideo(item->GetMusicInfoTag()->GetArtistString(), item->GetMusicInfoTag()->GetAlbum(), item->GetMusicInfoTag()->GetTitle()));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0,
+ static_cast<void*>(new CFileItem(details)));
+ return true;
+ }
+
+ case CONTEXT_BUTTON_RENAME:
+ if (!item->IsVideoDb() && !item->IsReadOnly())
+ OnRenameItem(itemNumber);
+
+ CGUIDialogVideoInfo::UpdateVideoItemTitle(item);
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ Refresh();
+ return true;
+
+ case CONTEXT_BUTTON_DELETE:
+ if (item->IsPlayList() || item->IsSmartPlayList())
+ {
+ item->m_bIsFolder = false;
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui && gui->ConfirmDelete(item->GetPath()))
+ CFileUtils::DeleteItem(item);
+ }
+ else if (!item->IsVideoDb())
+ OnDeleteItem(itemNumber);
+ else
+ {
+ CGUIDialogVideoInfo::DeleteVideoItemFromDatabase(item);
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ }
+ Refresh();
+ return true;
+
+ case CONTEXT_BUTTON_SET_CONTENT:
+ return ManageInfoProvider(item);
+
+ default:
+ break;
+ }
+
+ return CGUIWindowMusicBase::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowMusicNav::GetSongsFromPlayList(const std::string& strPlayList, CFileItemList &items)
+{
+ std::string strParentPath=m_history.GetParentPath();
+
+ if (m_guiState.get() && !m_guiState->HideParentDirItems())
+ {
+ CFileItemPtr pItem(new CFileItem(".."));
+ pItem->SetPath(strParentPath);
+ items.Add(pItem);
+ }
+
+ items.SetPath(strPlayList);
+ CLog::Log(LOGDEBUG, "CGUIWindowMusicNav, opening playlist [{}]", strPlayList);
+
+ std::unique_ptr<CPlayList> pPlayList (CPlayListFactory::Create(strPlayList));
+ if (nullptr != pPlayList)
+ {
+ // load it
+ if (!pPlayList->Load(strPlayList))
+ {
+ HELPERS::ShowOKDialogText(CVariant{6}, CVariant{477});
+ return false; //hmmm unable to load playlist?
+ }
+ CPlayList playlist = *pPlayList;
+ // convert playlist items to songs
+ for (int i = 0; i < playlist.size(); ++i)
+ {
+ items.Add(playlist[i]);
+ }
+ }
+
+ return true;
+}
+
+void CGUIWindowMusicNav::OnSearchUpdate()
+{
+ std::string search(CURL::Encode(GetProperty("search").asString()));
+ if (!search.empty())
+ {
+ std::string path = "musicsearch://" + search + "/";
+ m_history.ClearSearchHistory();
+ Update(path);
+ }
+ else if (m_vecItems->IsVirtualDirectoryRoot())
+ {
+ Update("");
+ }
+}
+
+void CGUIWindowMusicNav::FrameMove()
+{
+ static const int search_timeout = 2000;
+ // update our searching
+ if (m_searchTimer.IsRunning() && m_searchTimer.GetElapsedMilliseconds() > search_timeout)
+ {
+ m_searchTimer.Stop();
+ OnSearchUpdate();
+ }
+ CGUIWindowMusicBase::FrameMove();
+}
+
+void CGUIWindowMusicNav::AddSearchFolder()
+{
+ // we use a general viewstate (and not our member) here as our
+ // current viewstate may be specific to some other folder, and
+ // we know we're in the root here
+ CFileItemList items;
+ CGUIViewState* viewState = CGUIViewState::GetViewState(GetID(), items);
+ if (viewState)
+ {
+ // add our remove the musicsearch source
+ VECSOURCES &sources = viewState->GetSources();
+ bool haveSearchSource = false;
+ bool needSearchSource = !GetProperty("search").empty() || !m_searchWithEdit; // we always need it if we don't have the edit control
+ for (IVECSOURCES it = sources.begin(); it != sources.end(); ++it)
+ {
+ CMediaSource& share = *it;
+ if (share.strPath == "musicsearch://")
+ {
+ haveSearchSource = true;
+ if (!needSearchSource)
+ { // remove it
+ sources.erase(it);
+ break;
+ }
+ }
+ }
+ if (!haveSearchSource && needSearchSource)
+ {
+ // add search share
+ CMediaSource share;
+ share.strName=g_localizeStrings.Get(137); // Search
+ share.strPath = "musicsearch://";
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ sources.push_back(share);
+ }
+ m_rootDir.SetSources(sources);
+ delete viewState;
+ }
+}
+
+std::string CGUIWindowMusicNav::GetStartFolder(const std::string &dir)
+{
+ std::string lower(dir); StringUtils::ToLower(lower);
+ if (lower == "genres")
+ return "musicdb://genres/";
+ else if (lower == "artists")
+ return "musicdb://artists/";
+ else if (lower == "albums")
+ return "musicdb://albums/";
+ else if (lower == "singles")
+ return "musicdb://singles/";
+ else if (lower == "songs")
+ return "musicdb://songs/";
+ else if (lower == "top100")
+ return "musicdb://top100/";
+ else if (lower == "top100songs")
+ return "musicdb://top100/songs/";
+ else if (lower == "top100albums")
+ return "musicdb://top100/albums/";
+ else if (lower == "recentlyaddedalbums")
+ return "musicdb://recentlyaddedalbums/";
+ else if (lower == "recentlyplayedalbums")
+ return "musicdb://recentlyplayedalbums/";
+ else if (lower == "compilations")
+ return "musicdb://compilations/";
+ else if (lower == "years")
+ return "musicdb://years/";
+ else if (lower == "files")
+ return "sources://music/";
+ else if (lower == "boxsets")
+ return "musicdb://boxsets/";
+
+ return CGUIWindowMusicBase::GetStartFolder(dir);
+}
diff --git a/xbmc/music/windows/GUIWindowMusicNav.h b/xbmc/music/windows/GUIWindowMusicNav.h
new file mode 100644
index 0000000..021eaad
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicNav.h
@@ -0,0 +1,50 @@
+/*
+ * 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 "GUIWindowMusicBase.h"
+#include "utils/Stopwatch.h"
+
+class CFileItemList;
+
+class CGUIWindowMusicNav : public CGUIWindowMusicBase
+{
+public:
+
+ CGUIWindowMusicNav(void);
+ ~CGUIWindowMusicNav(void) override;
+
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void FrameMove() override;
+
+protected:
+ void OnItemLoaded(CFileItem* pItem) override {};
+ // override base class methods
+ bool Update(const std::string &strDirectory, bool updateFilterPath = true) override;
+ bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override;
+ void UpdateButtons() override;
+ void PlayItem(int iItem) override;
+ void OnWindowLoaded() override;
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool OnClick(int iItem, const std::string &player = "") override;
+ std::string GetStartFolder(const std::string &url) override;
+
+ bool GetSongsFromPlayList(const std::string& strPlayList, CFileItemList &items);
+ bool ManageInfoProvider(const CFileItemPtr& item);
+
+ VECSOURCES m_shares;
+
+ // searching
+ void OnSearchUpdate();
+ void AddSearchFolder();
+ CStopWatch m_searchTimer; ///< Timer to delay a search while more characters are entered
+ bool m_searchWithEdit; ///< Whether the skin supports the new edit control searching
+};
diff --git a/xbmc/music/windows/GUIWindowMusicPlaylist.cpp b/xbmc/music/windows/GUIWindowMusicPlaylist.cpp
new file mode 100644
index 0000000..acc46f6
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicPlaylist.cpp
@@ -0,0 +1,715 @@
+/*
+ * 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 "GUIWindowMusicPlaylist.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "PartyModeManager.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "dialogs/GUIDialogSmartPlaylistEditor.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "music/tags/MusicInfoTag.h"
+#include "playlists/PlayListM3U.h"
+#include "profiles/ProfileManager.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/LabelFormatter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "view/GUIViewState.h"
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_LABELFILES 12
+
+#define CONTROL_BTNSHUFFLE 20
+#define CONTROL_BTNSAVE 21
+#define CONTROL_BTNCLEAR 22
+
+#define CONTROL_BTNPLAY 23
+#define CONTROL_BTNNEXT 24
+#define CONTROL_BTNPREVIOUS 25
+#define CONTROL_BTNREPEAT 26
+
+CGUIWindowMusicPlayList::CGUIWindowMusicPlayList(void)
+ : CGUIWindowMusicBase(WINDOW_MUSIC_PLAYLIST, "MyPlaylist.xml")
+{
+ m_musicInfoLoader.SetObserver(this);
+ m_movingFrom = -1;
+}
+
+CGUIWindowMusicPlayList::~CGUIWindowMusicPlayList(void) = default;
+
+bool CGUIWindowMusicPlayList::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_PLAYLISTPLAYER_REPEAT:
+ {
+ UpdateButtons();
+ }
+ break;
+
+ case GUI_MSG_PLAYLISTPLAYER_RANDOM:
+ case GUI_MSG_PLAYLIST_CHANGED:
+ {
+ // global playlist changed outside playlist window
+ UpdateButtons();
+
+ if (m_vecItemsUpdating)
+ {
+ CLog::Log(LOGWARNING, "CGUIWindowMusicPlayList::OnMessage - updating in progress");
+ return true;
+ }
+ CUpdateGuard ug(m_vecItemsUpdating);
+
+ Refresh(true);
+
+ if (m_viewControl.HasControl(m_iLastControl) && m_vecItems->Size() <= 0)
+ {
+ m_iLastControl = CONTROL_BTNVIEWASICONS;
+ SET_CONTROL_FOCUS(m_iLastControl, 0);
+ }
+
+ }
+ break;
+
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ if (m_musicInfoLoader.IsLoading())
+ m_musicInfoLoader.StopThread();
+
+ m_movingFrom = -1;
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ // Setup item cache for tagloader
+ m_musicInfoLoader.UseCacheOnHD("special://temp/archive_cache/MusicPlaylist.fi");
+
+ m_vecItems->SetPath("playlistmusic://");
+
+ // updatebuttons is called in here
+ if (!CGUIWindowMusicBase::OnMessage(message))
+ return false;
+
+ if (m_vecItems->Size() <= 0)
+ {
+ m_iLastControl = CONTROL_BTNVIEWASICONS;
+ SET_CONTROL_FOCUS(m_iLastControl, 0);
+ }
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingAudio() &&
+ CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC)
+ {
+ int iSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ if (iSong >= 0 && iSong <= m_vecItems->Size())
+ m_viewControl.SetSelectedItem(iSong);
+ }
+
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTNSHUFFLE)
+ {
+ if (!g_partyModeManager.IsEnabled())
+ {
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(
+ PLAYLIST::TYPE_MUSIC,
+ !(CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_MUSIC)));
+ CMediaSettings::GetInstance().SetMusicPlaylistShuffled(
+ CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_MUSIC));
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ UpdateButtons();
+ Refresh();
+ }
+ }
+ else if (iControl == CONTROL_BTNSAVE)
+ {
+ if (m_musicInfoLoader.IsLoading()) // needed since we destroy m_vecitems to save memory
+ m_musicInfoLoader.StopThread();
+
+ SavePlayList();
+ }
+ else if (iControl == CONTROL_BTNCLEAR)
+ {
+ if (m_musicInfoLoader.IsLoading())
+ m_musicInfoLoader.StopThread();
+
+ ClearPlayList();
+ }
+ else if (iControl == CONTROL_BTNPLAY)
+ {
+ m_guiState->SetPlaylistDirectory("playlistmusic://");
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().Play(m_viewControl.GetSelectedItem(), "");
+ UpdateButtons();
+ }
+ else if (iControl == CONTROL_BTNNEXT)
+ {
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+ CServiceBroker::GetPlaylistPlayer().PlayNext();
+ }
+ else if (iControl == CONTROL_BTNPREVIOUS)
+ {
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_MUSIC);
+ CServiceBroker::GetPlaylistPlayer().PlayPrevious();
+ }
+ else if (iControl == CONTROL_BTNREPEAT)
+ {
+ // increment repeat state
+ PLAYLIST::RepeatState state =
+ CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_MUSIC);
+ if (state == PLAYLIST::RepeatState::NONE)
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_MUSIC,
+ PLAYLIST::RepeatState::ALL);
+ else if (state == PLAYLIST::RepeatState::ALL)
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_MUSIC,
+ PLAYLIST::RepeatState::ONE);
+ else
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_MUSIC,
+ PLAYLIST::RepeatState::NONE);
+
+ // save settings
+ CMediaSettings::GetInstance().SetMusicPlaylistRepeat(
+ CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_MUSIC) ==
+ PLAYLIST::RepeatState::ALL);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ UpdateButtons();
+ }
+ else if (m_viewControl.HasControl(iControl))
+ {
+ int iAction = message.GetParam1();
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iAction == ACTION_DELETE_ITEM || iAction == ACTION_MOUSE_MIDDLE_CLICK)
+ {
+ RemovePlayListItem(iItem);
+ MarkPlaying();
+ }
+ }
+ }
+ break;
+
+ }
+ return CGUIWindowMusicBase::OnMessage(message);
+}
+
+bool CGUIWindowMusicPlayList::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_PARENT_DIR)
+ {
+ // Playlist has no parent dirs
+ return true;
+ }
+ if (action.GetID() == ACTION_SHOW_PLAYLIST)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return true;
+ }
+ if ((action.GetID() == ACTION_MOVE_ITEM_UP) || (action.GetID() == ACTION_MOVE_ITEM_DOWN))
+ {
+ int iItem = -1;
+ int iFocusedControl = GetFocusedControlID();
+ if (m_viewControl.HasControl(iFocusedControl))
+ iItem = m_viewControl.GetSelectedItem();
+ OnMove(iItem, action.GetID());
+ return true;
+ }
+ return CGUIWindowMusicBase::OnAction(action);
+}
+
+bool CGUIWindowMusicPlayList::OnBack(int actionID)
+{
+ CancelUpdateItems();
+
+ if (actionID == ACTION_NAV_BACK)
+ return CGUIWindow::OnBack(actionID); // base class goes up a folder, but none to go up
+ return CGUIWindowMusicBase::OnBack(actionID);
+}
+
+bool CGUIWindowMusicPlayList::MoveCurrentPlayListItem(int iItem, int iAction, bool bUpdate /* = true */)
+{
+ int iSelected = iItem;
+ int iNew = iSelected;
+ if (iAction == ACTION_MOVE_ITEM_UP)
+ iNew--;
+ else
+ iNew++;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ // is the currently playing item affected?
+ bool bFixCurrentSong = false;
+ if ((CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC) &&
+ appPlayer->IsPlayingAudio() &&
+ ((CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iSelected) ||
+ (CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iNew)))
+ bFixCurrentSong = true;
+
+ PLAYLIST::CPlayList& playlist =
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_MUSIC);
+ if (playlist.Swap(iSelected, iNew))
+ {
+ // Correct the current playing song in playlistplayer
+ if (bFixCurrentSong)
+ {
+ int iCurrentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ if (iSelected == iCurrentSong)
+ iCurrentSong = iNew;
+ else if (iNew == iCurrentSong)
+ iCurrentSong = iSelected;
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(iCurrentSong);
+ }
+
+ if (bUpdate)
+ Refresh();
+ return true;
+ }
+
+ return false;
+}
+
+void CGUIWindowMusicPlayList::SavePlayList()
+{
+ std::string strNewFileName;
+ if (CGUIKeyboardFactory::ShowAndGetInput(strNewFileName, CVariant{g_localizeStrings.Get(16012)}, false))
+ {
+ // need 2 rename it
+ strNewFileName = CUtil::MakeLegalFileName(strNewFileName);
+ strNewFileName += ".m3u";
+ std::string strPath = URIUtils::AddFileToFolder(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH),
+ "music",
+ strNewFileName);
+
+ // get selected item
+ int iItem = m_viewControl.GetSelectedItem();
+ std::string strSelectedItem = "";
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+ if (!pItem->IsParentFolder())
+ {
+ GetDirectoryHistoryString(pItem.get(), strSelectedItem);
+ }
+ }
+
+ std::string strOldDirectory = m_vecItems->GetPath();
+ m_history.SetSelectedItem(strSelectedItem, strOldDirectory);
+
+ PLAYLIST::CPlayListM3U playlist;
+ for (int i = 0; i < m_vecItems->Size(); ++i)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+
+ // Musicdatabase items should contain the real path instead of a musicdb url
+ // otherwise the user can't save and reuse the playlist when the musicdb gets deleted
+ if (pItem->IsMusicDb())
+ pItem->SetPath(pItem->GetMusicInfoTag()->GetURL());
+
+ playlist.Add(pItem);
+ }
+ CLog::Log(LOGDEBUG, "Saving music playlist: [{}]", strPath);
+ playlist.Save(strPath);
+ Refresh(); // need to update
+ }
+}
+
+void CGUIWindowMusicPlayList::ClearPlayList()
+{
+ ClearFileItems();
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_MUSIC);
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC)
+ {
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE);
+ }
+ Refresh();
+ SET_CONTROL_FOCUS(CONTROL_BTNVIEWASICONS, 0);
+}
+
+void CGUIWindowMusicPlayList::RemovePlayListItem(int iItem)
+{
+ if (iItem < 0 || iItem > m_vecItems->Size()) return;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ // The current playing song can't be removed
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC &&
+ appPlayer->IsPlayingAudio() && CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iItem)
+ return ;
+
+ CServiceBroker::GetPlaylistPlayer().Remove(PLAYLIST::TYPE_MUSIC, iItem);
+
+ Refresh();
+
+ if (m_vecItems->Size() <= 0)
+ {
+ SET_CONTROL_FOCUS(CONTROL_BTNVIEWASICONS, 0);
+ }
+ else
+ {
+ m_viewControl.SetSelectedItem(iItem);
+ }
+
+ g_partyModeManager.OnSongChange();
+}
+
+void CGUIWindowMusicPlayList::UpdateButtons()
+{
+ CGUIWindowMusicBase::UpdateButtons();
+
+ // Update playlist buttons
+ if (m_vecItems->Size() && !g_partyModeManager.IsEnabled())
+ {
+ CONTROL_ENABLE(CONTROL_BTNSHUFFLE);
+ CONTROL_ENABLE(CONTROL_BTNSAVE);
+ CONTROL_ENABLE(CONTROL_BTNCLEAR);
+ CONTROL_ENABLE(CONTROL_BTNREPEAT);
+ CONTROL_ENABLE(CONTROL_BTNPLAY);
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingAudio() &&
+ CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_MUSIC)
+ {
+ CONTROL_ENABLE(CONTROL_BTNNEXT);
+ CONTROL_ENABLE(CONTROL_BTNPREVIOUS);
+ }
+ else
+ {
+ CONTROL_DISABLE(CONTROL_BTNNEXT);
+ CONTROL_DISABLE(CONTROL_BTNPREVIOUS);
+ }
+ }
+ else
+ {
+ // disable buttons if party mode is enabled too
+ CONTROL_DISABLE(CONTROL_BTNSHUFFLE);
+ CONTROL_DISABLE(CONTROL_BTNSAVE);
+ CONTROL_DISABLE(CONTROL_BTNCLEAR);
+ CONTROL_DISABLE(CONTROL_BTNREPEAT);
+ CONTROL_DISABLE(CONTROL_BTNPLAY);
+ CONTROL_DISABLE(CONTROL_BTNNEXT);
+ CONTROL_DISABLE(CONTROL_BTNPREVIOUS);
+ }
+
+ // update buttons
+ CONTROL_DESELECT(CONTROL_BTNSHUFFLE);
+ if (CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_MUSIC))
+ CONTROL_SELECT(CONTROL_BTNSHUFFLE);
+
+ // update repeat button
+ int iLocalizedString;
+ PLAYLIST::RepeatState repState =
+ CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_MUSIC);
+ if (repState == PLAYLIST::RepeatState::NONE)
+ iLocalizedString = 595; // Repeat: Off
+ else if (repState == PLAYLIST::RepeatState::ONE)
+ iLocalizedString = 596; // Repeat: One
+ else
+ iLocalizedString = 597; // Repeat: All
+
+ SET_CONTROL_LABEL(CONTROL_BTNREPEAT, g_localizeStrings.Get(iLocalizedString));
+
+ // Update object count label
+ std::string items =
+ StringUtils::Format("{} {}", m_vecItems->GetObjectCount(), g_localizeStrings.Get(127));
+ SET_CONTROL_LABEL(CONTROL_LABELFILES, items);
+
+ MarkPlaying();
+}
+
+bool CGUIWindowMusicPlayList::OnPlayMedia(int iItem, const std::string &player)
+{
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Play(iItem);
+ else
+ {
+ PLAYLIST::Id playlistId = m_guiState->GetPlaylist();
+ if (playlistId != PLAYLIST::TYPE_NONE)
+ {
+ if (m_guiState)
+ m_guiState->SetPlaylistDirectory(m_vecItems->GetPath());
+
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(playlistId);
+ CServiceBroker::GetPlaylistPlayer().Play(iItem, player);
+ }
+ else
+ {
+ // Reset Playlistplayer, playback started now does
+ // not use the playlistplayer.
+ CFileItemPtr pItem=m_vecItems->Get(iItem);
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE);
+ g_application.PlayFile(*pItem, player);
+ }
+ }
+
+ return true;
+}
+
+void CGUIWindowMusicPlayList::OnItemLoaded(CFileItem* pItem)
+{
+ if (pItem->HasMusicInfoTag() && pItem->GetMusicInfoTag()->Loaded())
+ { // set label 1+2 from tags
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_NOWPLAYINGTRACKFORMAT);
+ if (strTrack.empty())
+ strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ CLabelFormatter formatter(strTrack, "%D");
+ formatter.FormatLabels(pItem);
+ } // if (pItem->m_musicInfoTag.Loaded())
+ else
+ {
+ // Our tag may have a duration even if its not loaded
+ if (pItem->HasMusicInfoTag() && pItem->GetMusicInfoTag()->GetDuration())
+ {
+ int nDuration = pItem->GetMusicInfoTag()->GetDuration();
+ if (nDuration > 0)
+ pItem->SetLabel2(StringUtils::SecondsToTimeString(nDuration));
+ }
+ else if (pItem->GetLabel() == "") // pls labels come in preformatted
+ {
+ // FIXME: get the position of the item in the playlist
+ // currently it is hacked into m_iprogramCount
+
+ // No music info and it's not CDDA so we'll just show the filename
+ std::string str;
+ str = CUtil::GetTitleFromPath(pItem->GetPath());
+ str = StringUtils::Format("{:02}. {} ", pItem->m_iprogramCount, str);
+ pItem->SetLabel(str);
+ }
+ }
+}
+
+bool CGUIWindowMusicPlayList::Update(const std::string& strDirectory, bool updateFilterPath /* = true */)
+{
+ if (m_musicInfoLoader.IsLoading())
+ m_musicInfoLoader.StopThread();
+
+ if (!CGUIWindowMusicBase::Update(strDirectory, updateFilterPath))
+ return false;
+
+ if (m_vecItems->GetContent().empty())
+ m_vecItems->SetContent("songs");
+
+ m_musicInfoLoader.Load(*m_vecItems);
+ return true;
+}
+
+void CGUIWindowMusicPlayList::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ // is this playlist playing?
+ int itemPlaying = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ {
+ CFileItemPtr item;
+ item = m_vecItems->Get(itemNumber);
+
+ if (m_movingFrom >= 0)
+ {
+ // we can move the item to any position not where we are, and any position not above currently
+ // playing item in party mode
+ if (itemNumber != m_movingFrom && (!g_partyModeManager.IsEnabled() || itemNumber > itemPlaying))
+ buttons.Add(CONTEXT_BUTTON_MOVE_HERE, 13252); // move item here
+ buttons.Add(CONTEXT_BUTTON_CANCEL_MOVE, 13253);
+ }
+ else
+ {
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ // aren't in a move
+ // check what players we have, if we have multiple display play with option
+ std::vector<std::string> players;
+ playerCoreFactory.GetPlayers(*item, players);
+ if (players.size() > 1)
+ buttons.Add(CONTEXT_BUTTON_PLAY_WITH, 15213); // Play With...
+
+ if (itemNumber > (g_partyModeManager.IsEnabled() ? 1 : 0))
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_UP, 13332);
+ if (itemNumber + 1 < m_vecItems->Size())
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_DOWN, 13333);
+ if (!g_partyModeManager.IsEnabled() || itemNumber != itemPlaying)
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM, 13251);
+ if (itemNumber != itemPlaying)
+ buttons.Add(CONTEXT_BUTTON_DELETE, 1210); // Remove
+ }
+ }
+
+ if (g_partyModeManager.IsEnabled())
+ {
+ buttons.Add(CONTEXT_BUTTON_EDIT_PARTYMODE, 21439);
+ buttons.Add(CONTEXT_BUTTON_CANCEL_PARTYMODE, 588); // cancel party mode
+ }
+}
+
+bool CGUIWindowMusicPlayList::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ switch (button)
+ {
+ case CONTEXT_BUTTON_PLAY_WITH:
+ {
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+ if (!item)
+ break;
+
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ std::vector<std::string> players;
+ playerCoreFactory.GetPlayers(*item, players);
+ std::string player = playerCoreFactory.SelectPlayerDialog(players);
+ if (!player.empty())
+ OnClick(itemNumber, player);
+ return true;
+ }
+ case CONTEXT_BUTTON_MOVE_ITEM:
+ m_movingFrom = itemNumber;
+ return true;
+
+ case CONTEXT_BUTTON_MOVE_HERE:
+ MoveItem(m_movingFrom, itemNumber);
+ m_movingFrom = -1;
+ return true;
+
+ case CONTEXT_BUTTON_CANCEL_MOVE:
+ m_movingFrom = -1;
+ return true;
+
+ case CONTEXT_BUTTON_MOVE_ITEM_UP:
+ OnMove(itemNumber, ACTION_MOVE_ITEM_UP);
+ return true;
+
+ case CONTEXT_BUTTON_MOVE_ITEM_DOWN:
+ OnMove(itemNumber, ACTION_MOVE_ITEM_DOWN);
+ return true;
+
+ case CONTEXT_BUTTON_DELETE:
+ RemovePlayListItem(itemNumber);
+ return true;
+
+ case CONTEXT_BUTTON_CANCEL_PARTYMODE:
+ g_partyModeManager.Disable();
+ return true;
+
+ case CONTEXT_BUTTON_EDIT_PARTYMODE:
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::string playlist = profileManager->GetUserDataItem("PartyMode.xsp");
+ if (CGUIDialogSmartPlaylistEditor::EditPlaylist(playlist))
+ {
+ // apply new rules
+ g_partyModeManager.Disable();
+ g_partyModeManager.Enable();
+ }
+ return true;
+ }
+
+ default:
+ break;
+ }
+ return CGUIWindowMusicBase::OnContextButton(itemNumber, button);
+}
+
+
+void CGUIWindowMusicPlayList::OnMove(int iItem, int iAction)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size()) return;
+
+ bool bRestart = m_musicInfoLoader.IsLoading();
+ if (bRestart)
+ m_musicInfoLoader.StopThread();
+
+ MoveCurrentPlayListItem(iItem, iAction);
+
+ if (bRestart)
+ m_musicInfoLoader.Load(*m_vecItems);
+}
+
+void CGUIWindowMusicPlayList::MoveItem(int iStart, int iDest)
+{
+ if (iStart < 0 || iStart >= m_vecItems->Size()) return;
+ if (iDest < 0 || iDest >= m_vecItems->Size()) return;
+
+ // default to move up
+ int iAction = ACTION_MOVE_ITEM_UP;
+ int iDirection = -1;
+ // are we moving down?
+ if (iStart < iDest)
+ {
+ iAction = ACTION_MOVE_ITEM_DOWN;
+ iDirection = 1;
+ }
+
+ bool bRestart = m_musicInfoLoader.IsLoading();
+ if (bRestart)
+ m_musicInfoLoader.StopThread();
+
+ // keep swapping until you get to the destination or you
+ // hit the currently playing song
+ int i = iStart;
+ while (i != iDest)
+ {
+ // try to swap adjacent items
+ if (MoveCurrentPlayListItem(i, iAction, false))
+ i = i + (1 * iDirection);
+ // we hit currently playing song, so abort
+ else
+ break;
+ }
+ Refresh();
+
+ if (bRestart)
+ m_musicInfoLoader.Load(*m_vecItems);
+}
+
+void CGUIWindowMusicPlayList::MarkPlaying()
+{
+ /* // clear markings
+ for (int i = 0; i < m_vecItems->Size(); i++)
+ m_vecItems->Get(i)->Select(false);
+
+ // mark the currently playing item
+ if ((CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == TYPE_MUSIC) && (g_application.GetAppPlayer().IsPlayingAudio()))
+ {
+ int iSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ if (iSong >= 0 && iSong <= m_vecItems->Size())
+ m_vecItems->Get(iSong)->Select(true);
+ }*/
+}
+
diff --git a/xbmc/music/windows/GUIWindowMusicPlaylist.h b/xbmc/music/windows/GUIWindowMusicPlaylist.h
new file mode 100644
index 0000000..019a1c7
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicPlaylist.h
@@ -0,0 +1,44 @@
+/*
+ * 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 "GUIWindowMusicBase.h"
+
+class CGUIWindowMusicPlayList : public CGUIWindowMusicBase
+{
+public:
+ CGUIWindowMusicPlayList(void);
+ ~CGUIWindowMusicPlayList(void) override;
+
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnBack(int actionID) override;
+
+ void RemovePlayListItem(int iItem);
+ void MoveItem(int iStart, int iDest);
+
+protected:
+ bool GoParentFolder() override { return false; }
+ void UpdateButtons() override;
+ void OnItemLoaded(CFileItem* pItem) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ void OnMove(int iItem, int iAction);
+ bool OnPlayMedia(int iItem, const std::string &player = "") override;
+
+ void SavePlayList();
+ void ClearPlayList();
+ void MarkPlaying();
+
+ bool MoveCurrentPlayListItem(int iItem, int iAction, bool bUpdate = true);
+
+ int m_movingFrom;
+ VECSOURCES m_shares;
+};
diff --git a/xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp
new file mode 100644
index 0000000..393923d
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.cpp
@@ -0,0 +1,450 @@
+/*
+ * 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 "GUIWindowMusicPlaylistEditor.h"
+
+#include "Autorun.h"
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "filesystem/PlaylistFileDirectory.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "music/MusicUtils.h"
+#include "playlists/PlayListM3U.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+#define CONTROL_LABELFILES 12
+
+#define CONTROL_LOAD_PLAYLIST 6
+#define CONTROL_SAVE_PLAYLIST 7
+#define CONTROL_CLEAR_PLAYLIST 8
+
+#define CONTROL_LIST 50
+#define CONTROL_PLAYLIST 100
+#define CONTROL_LABEL_PLAYLIST 101
+
+CGUIWindowMusicPlaylistEditor::CGUIWindowMusicPlaylistEditor(void)
+ : CGUIWindowMusicBase(WINDOW_MUSIC_PLAYLIST_EDITOR, "MyMusicPlaylistEditor.xml")
+{
+ m_playlistThumbLoader.SetObserver(this);
+ m_playlist = new CFileItemList;
+}
+
+CGUIWindowMusicPlaylistEditor::~CGUIWindowMusicPlaylistEditor(void)
+{
+ delete m_playlist;
+}
+
+bool CGUIWindowMusicPlaylistEditor::OnBack(int actionID)
+{
+ if (actionID == ACTION_NAV_BACK && !m_viewControl.HasControl(GetFocusedControlID()))
+ return CGUIWindow::OnBack(actionID); // base class goes up a folder, but none to go up
+ return CGUIWindowMusicBase::OnBack(actionID);
+}
+
+bool CGUIWindowMusicPlaylistEditor::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_CONTEXT_MENU)
+ {
+ int iControl = GetFocusedControlID();
+ if (iControl == CONTROL_PLAYLIST)
+ {
+ OnPlaylistContext();
+ return true;
+ }
+ else if (iControl == CONTROL_LIST)
+ {
+ OnSourcesContext();
+ return true;
+ }
+ }
+ return CGUIWindow::OnAction(action);
+}
+
+bool CGUIWindowMusicPlaylistEditor::OnClick(int iItem, const std::string& player /* = "" */)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size()) return false;
+ CFileItemPtr item = m_vecItems->Get(iItem);
+
+ // Expand .m3u files in sources list when clicked on regardless of <playlistasfolders>
+ if (item->IsFileFolder(EFILEFOLDER_MASK_ONBROWSE))
+ return Update(item->GetPath());
+ // Avoid playback (default click behaviour) of media files
+ if (!item->m_bIsFolder)
+ return false;
+
+ return CGUIWindowMusicBase::OnClick(iItem, player);
+}
+
+bool CGUIWindowMusicPlaylistEditor::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+ if (m_playlistThumbLoader.IsLoading())
+ m_playlistThumbLoader.StopThread();
+ CGUIWindowMusicBase::OnMessage(message);
+ return true;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ if (m_vecItems->GetPath() == "?")
+ m_vecItems->SetPath("");
+ CGUIWindowMusicBase::OnMessage(message);
+
+ if (message.GetNumStringParams())
+ LoadPlaylist(message.GetStringParam());
+
+ return true;
+ }
+ break;
+
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (message.GetParam1()==GUI_MSG_REMOVED_MEDIA)
+ DeleteRemoveableMediaDirectoryCache();
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int control = message.GetSenderId();
+ if (control == CONTROL_PLAYLIST)
+ {
+ int item = GetCurrentPlaylistItem();
+ int action = message.GetParam1();
+ if (action == ACTION_CONTEXT_MENU || action == ACTION_MOUSE_RIGHT_CLICK)
+ OnPlaylistContext();
+ else if (action == ACTION_QUEUE_ITEM || action == ACTION_DELETE_ITEM ||
+ action == ACTION_MOUSE_MIDDLE_CLICK)
+ OnDeletePlaylistItem(item);
+ else if (action == ACTION_MOVE_ITEM_UP)
+ OnMovePlaylistItem(item, -1);
+ else if (action == ACTION_MOVE_ITEM_DOWN)
+ OnMovePlaylistItem(item, 1);
+ return true;
+ }
+ else if (control == CONTROL_LIST)
+ {
+ int action = message.GetParam1();
+ if (action == ACTION_CONTEXT_MENU || action == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ OnSourcesContext();
+ return true;
+ }
+ }
+ else if (control == CONTROL_LOAD_PLAYLIST)
+ { // load a playlist
+ OnLoadPlaylist();
+ return true;
+ }
+ else if (control == CONTROL_SAVE_PLAYLIST)
+ { // save the playlist
+ OnSavePlaylist();
+ return true;
+ }
+ else if (control == CONTROL_CLEAR_PLAYLIST)
+ { // clear the playlist
+ ClearPlaylist();
+ return true;
+ }
+ }
+ break;
+ }
+
+ return CGUIWindowMusicBase::OnMessage(message);
+}
+
+bool CGUIWindowMusicPlaylistEditor::GetDirectory(const std::string &strDirectory, CFileItemList &items)
+{
+ items.Clear();
+ if (strDirectory.empty())
+ { // root listing - list files:// and musicdb://
+ CFileItemPtr files(new CFileItem("sources://music/", true));
+ files->SetLabel(g_localizeStrings.Get(744));
+ files->SetLabelPreformatted(true);
+ files->m_bIsShareOrDrive = true;
+ items.Add(files);
+
+ CFileItemPtr mdb(new CFileItem("library://music/", true));
+ mdb->SetLabel(g_localizeStrings.Get(14022));
+ mdb->SetLabelPreformatted(true);
+ mdb->m_bIsShareOrDrive = true;
+ items.SetPath("");
+ items.Add(mdb);
+
+ CFileItemPtr vdb(new CFileItem("videodb://musicvideos/", true));
+ vdb->SetLabel(g_localizeStrings.Get(20389));
+ vdb->SetLabelPreformatted(true);
+ vdb->m_bIsShareOrDrive = true;
+ items.SetPath("");
+ items.Add(vdb);
+
+ return true;
+ }
+
+ if (!CGUIWindowMusicBase::GetDirectory(strDirectory, items))
+ return false;
+
+ // check for .CUE files here.
+ items.FilterCueItems();
+
+ return true;
+}
+
+void CGUIWindowMusicPlaylistEditor::OnPrepareFileItems(CFileItemList &items)
+{
+ CGUIWindowMusicBase::OnPrepareFileItems(items);
+
+ RetrieveMusicInfo();
+}
+
+void CGUIWindowMusicPlaylistEditor::UpdateButtons()
+{
+ CGUIWindowMusicBase::UpdateButtons();
+
+ // Update object count label
+ std::string items = StringUtils::Format("{} {}", m_vecItems->GetObjectCount(),
+ g_localizeStrings.Get(127)); // " 14 Objects"
+ SET_CONTROL_LABEL(CONTROL_LABELFILES, items);
+}
+
+void CGUIWindowMusicPlaylistEditor::DeleteRemoveableMediaDirectoryCache()
+{
+ CUtil::DeleteDirectoryCache("r-");
+}
+
+void CGUIWindowMusicPlaylistEditor::PlayItem(int iItem)
+{
+ // unlike additemtoplaylist, we need to check the items here
+ // before calling it since the current playlist will be stopped
+ // and cleared!
+
+ // we're at the root source listing
+ if (m_vecItems->IsVirtualDirectoryRoot() && !m_vecItems->Get(iItem)->IsDVD())
+ return;
+
+#ifdef HAS_DVD_DRIVE
+ if (m_vecItems->Get(iItem)->IsDVD())
+ MEDIA_DETECT::CAutorun::PlayDiscAskResume(m_vecItems->Get(iItem)->GetPath());
+ else
+#endif
+ CGUIWindowMusicBase::PlayItem(iItem);
+}
+
+void CGUIWindowMusicPlaylistEditor::OnQueueItem(int iItem, bool)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return;
+
+ // add this item to our playlist. We make a new copy here as we may be rendering them side by side,
+ // and thus want a different layout for each item
+ CFileItemPtr item(new CFileItem(*m_vecItems->Get(iItem)));
+ CFileItemList newItems;
+ MUSIC_UTILS::GetItemsForPlayList(item, newItems);
+ AppendToPlaylist(newItems);
+}
+
+bool CGUIWindowMusicPlaylistEditor::Update(const std::string &strDirectory, bool updateFilterPath /* = true */)
+{
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ if (!CGUIMediaWindow::Update(strDirectory, updateFilterPath))
+ return false;
+
+ m_vecItems->SetContent("files");
+ m_thumbLoader.Load(*m_vecItems);
+
+ // update our playlist control
+ UpdatePlaylist();
+ return true;
+}
+
+void CGUIWindowMusicPlaylistEditor::ClearPlaylist()
+{
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_PLAYLIST);
+ OnMessage(msg);
+
+ m_playlist->Clear();
+}
+
+void CGUIWindowMusicPlaylistEditor::UpdatePlaylist()
+{
+ if (m_playlistThumbLoader.IsLoading())
+ m_playlistThumbLoader.StopThread();
+
+ // deselect all items
+ for (int i = 0; i < m_playlist->Size(); i++)
+ m_playlist->Get(i)->Select(false);
+
+ // bind them to the list
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_PLAYLIST, 0, 0, m_playlist);
+ OnMessage(msg);
+
+ // indicate how many songs we have
+ std::string items = StringUtils::Format("{} {}", m_playlist->Size(),
+ g_localizeStrings.Get(134)); // "123 Songs"
+ SET_CONTROL_LABEL(CONTROL_LABEL_PLAYLIST, items);
+
+ m_playlistThumbLoader.Load(*m_playlist);
+}
+
+int CGUIWindowMusicPlaylistEditor::GetCurrentPlaylistItem()
+{
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_PLAYLIST);
+ OnMessage(msg);
+ int item = msg.GetParam1();
+ if (item > m_playlist->Size())
+ return -1;
+ return item;
+}
+
+void CGUIWindowMusicPlaylistEditor::OnDeletePlaylistItem(int item)
+{
+ if (item < 0) return;
+ m_playlist->Remove(item);
+ UpdatePlaylist();
+ // select the next item
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), CONTROL_PLAYLIST, item);
+ OnMessage(msg);
+}
+
+void CGUIWindowMusicPlaylistEditor::OnMovePlaylistItem(int item, int direction)
+{
+ if (item < 0) return;
+ if (item + direction >= m_playlist->Size() || item + direction < 0)
+ return;
+ m_playlist->Swap(item, item + direction);
+ UpdatePlaylist();
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), CONTROL_PLAYLIST, item + direction);
+ OnMessage(msg);
+}
+
+void CGUIWindowMusicPlaylistEditor::OnLoadPlaylist()
+{
+ // Prompt user for file to load from music playlists folder
+ std::string playlist;
+ if (CGUIDialogFileBrowser::ShowAndGetFile("special://musicplaylists/",
+ ".m3u|.pls|.b4s|.wpl|.xspf", g_localizeStrings.Get(656),
+ playlist))
+ LoadPlaylist(playlist);
+}
+
+void CGUIWindowMusicPlaylistEditor::LoadPlaylist(const std::string &playlist)
+{
+ const CURL pathToUrl(playlist);
+ if (pathToUrl.IsProtocol("newplaylist"))
+ {
+ ClearPlaylist();
+ m_strLoadedPlaylist.clear();
+ return;
+ }
+
+ XFILE::CPlaylistFileDirectory dir;
+ CFileItemList items;
+ if (dir.GetDirectory(pathToUrl, items))
+ {
+ ClearPlaylist();
+ AppendToPlaylist(items);
+ m_strLoadedPlaylist = playlist;
+ }
+}
+
+void CGUIWindowMusicPlaylistEditor::OnSavePlaylist()
+{
+ // saves playlist to the playlist folder
+ std::string name = URIUtils::GetFileName(m_strLoadedPlaylist);
+ URIUtils::RemoveExtension(name);
+
+ if (CGUIKeyboardFactory::ShowAndGetInput(name, CVariant{g_localizeStrings.Get(16012)}, false))
+ { // save playlist as an .m3u
+ PLAYLIST::CPlayListM3U playlist;
+ playlist.Add(*m_playlist);
+ std::string path = URIUtils::AddFileToFolder(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH),
+ "music",
+ name + ".m3u");
+
+ playlist.Save(path);
+ m_strLoadedPlaylist = name;
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info,
+ g_localizeStrings.Get(559), // "Playlist"
+ g_localizeStrings.Get(35259)); // "Saved"
+ }
+}
+
+void CGUIWindowMusicPlaylistEditor::AppendToPlaylist(CFileItemList &newItems)
+{
+ OnRetrieveMusicInfo(newItems);
+ FormatItemLabels(newItems, LABEL_MASKS(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT), "%D", "%L", ""));
+ m_playlist->Append(newItems);
+ UpdatePlaylist();
+}
+
+void CGUIWindowMusicPlaylistEditor::OnSourcesContext()
+{
+ CFileItemPtr item = GetCurrentListItem();
+ CContextButtons buttons;
+ if (item->IsFileFolder(EFILEFOLDER_MASK_ONBROWSE))
+ buttons.Add(CONTEXT_BUTTON_BROWSE_INTO, 37015); //Browse into
+ if (item && !item->IsParentFolder() && !m_vecItems->IsVirtualDirectoryRoot())
+ buttons.Add(CONTEXT_BUTTON_QUEUE_ITEM, 15019); // Add (to playlist)
+
+ int btnid = CGUIDialogContextMenu::ShowAndGetChoice(buttons);
+ if (btnid == CONTEXT_BUTTON_QUEUE_ITEM)
+ OnQueueItem(m_viewControl.GetSelectedItem(), false);
+ else if (btnid == CONTEXT_BUTTON_BROWSE_INTO)
+ Update(item->GetPath());
+}
+
+void CGUIWindowMusicPlaylistEditor::OnPlaylistContext()
+{
+ int item = GetCurrentPlaylistItem();
+ CContextButtons buttons;
+ if (item > 0)
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_UP, 13332);
+ if (item >= 0 && item < m_playlist->Size() - 1)
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_DOWN, 13333);
+ if (item >= 0)
+ buttons.Add(CONTEXT_BUTTON_DELETE, 1210);
+
+ int btnid = CGUIDialogContextMenu::ShowAndGetChoice(buttons);
+ if (btnid == CONTEXT_BUTTON_MOVE_ITEM_UP)
+ OnMovePlaylistItem(item, -1);
+ else if (btnid == CONTEXT_BUTTON_MOVE_ITEM_DOWN)
+ OnMovePlaylistItem(item, 1);
+ else if (btnid == CONTEXT_BUTTON_DELETE)
+ OnDeletePlaylistItem(item);
+}
+
+bool CGUIWindowMusicPlaylistEditor::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ switch (button)
+ {
+ case CONTEXT_BUTTON_QUEUE_ITEM:
+ OnQueueItem(itemNumber);
+ return true;
+
+ default:
+ break;
+ }
+
+ return CGUIWindowMusicBase::OnContextButton(itemNumber, button);
+}
diff --git a/xbmc/music/windows/GUIWindowMusicPlaylistEditor.h b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.h
new file mode 100644
index 0000000..3859b55
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowMusicPlaylistEditor.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 "GUIWindowMusicBase.h"
+
+class CFileItemList;
+
+class CGUIWindowMusicPlaylistEditor : public CGUIWindowMusicBase
+{
+public:
+ CGUIWindowMusicPlaylistEditor(void);
+ ~CGUIWindowMusicPlaylistEditor(void) override;
+
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnClick(int iItem, const std::string &player = "") override;
+ bool OnBack(int actionID) override;
+
+protected:
+ bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override;
+ void UpdateButtons() override;
+ bool Update(const std::string &strDirectory, bool updateFilterPath = true) override;
+ void OnPrepareFileItems(CFileItemList &items) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ void OnQueueItem(int iItem, bool first = false) override;
+ std::string GetStartFolder(const std::string& dir) override { return ""; }
+
+ void OnSourcesContext();
+ void OnPlaylistContext();
+ int GetCurrentPlaylistItem();
+ void OnDeletePlaylistItem(int item);
+ void UpdatePlaylist();
+ void ClearPlaylist();
+ void OnSavePlaylist();
+ void OnLoadPlaylist();
+ void AppendToPlaylist(CFileItemList &newItems);
+ void OnMovePlaylistItem(int item, int direction);
+
+ void LoadPlaylist(const std::string &playlist);
+
+ // new method
+ void PlayItem(int iItem) override;
+
+ void DeleteRemoveableMediaDirectoryCache();
+
+ CMusicThumbLoader m_playlistThumbLoader;
+
+ CFileItemList* m_playlist;
+ std::string m_strLoadedPlaylist;
+};
diff --git a/xbmc/music/windows/GUIWindowVisualisation.cpp b/xbmc/music/windows/GUIWindowVisualisation.cpp
new file mode 100644
index 0000000..805c2df
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowVisualisation.cpp
@@ -0,0 +1,233 @@
+/*
+ * 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 "GUIWindowVisualisation.h"
+
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIDialog.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/Key.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+using namespace MUSIC_INFO;
+
+#define START_FADE_LENGTH 2.0f // 2 seconds on startup
+
+#define CONTROL_VIS 2
+
+CGUIWindowVisualisation::CGUIWindowVisualisation(void)
+ : CGUIWindow(WINDOW_VISUALISATION, "MusicVisualisation.xml")
+{
+ m_bShowPreset = false;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+bool CGUIWindowVisualisation::OnAction(const CAction &action)
+{
+ bool passToVis = false;
+ switch (action.GetID())
+ {
+ case ACTION_VIS_PRESET_NEXT:
+ case ACTION_VIS_PRESET_PREV:
+ case ACTION_VIS_PRESET_RANDOM:
+ case ACTION_VIS_RATE_PRESET_PLUS:
+ case ACTION_VIS_RATE_PRESET_MINUS:
+ passToVis = true;
+ break;
+
+ case ACTION_SHOW_INFO:
+ {
+ m_initTimer.Stop();
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetBool(CSettings::SETTING_MYMUSIC_SONGTHUMBINVIS,
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().ToggleShowInfo());
+ return true;
+ }
+ break;
+
+ case ACTION_SHOW_OSD:
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_DIALOG_MUSIC_OSD);
+ return true;
+
+ case ACTION_SHOW_GUI:
+ // save the settings
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return true;
+ break;
+
+ case ACTION_VIS_PRESET_LOCK:
+ { // show the locked icon + fall through so that the vis handles the locking
+ if (!m_bShowPreset)
+ {
+ m_lockedTimer.StartZero();
+ }
+ passToVis = true;
+ }
+ break;
+ case ACTION_VIS_PRESET_SHOW:
+ {
+ if (!m_lockedTimer.IsRunning() || m_bShowPreset)
+ m_bShowPreset = !m_bShowPreset;
+ return true;
+ }
+ break;
+
+ case ACTION_DECREASE_RATING:
+ case ACTION_INCREASE_RATING:
+ {
+ // actual action is taken care of in CApplication::OnAction()
+ m_initTimer.StartZero();
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(true);
+ }
+ break;
+ //! @todo These should be mapped to its own function - at the moment it's overriding
+ //! the global action of fastforward/rewind and OSD.
+/* case KEY_BUTTON_Y:
+ g_application.m_CdgParser.Pause();
+ return true;
+ break;
+
+ case ACTION_ANALOG_FORWARD:
+ // calculate the speed based on the amount the button is held down
+ if (action.GetAmount())
+ {
+ float AVDelay = g_application.m_CdgParser.GetAVDelay();
+ g_application.m_CdgParser.SetAVDelay(AVDelay - action.GetAmount() / 4.0f);
+ return true;
+ }
+ break;*/
+ }
+
+ if (passToVis)
+ {
+ CGUIControl *control = GetControl(CONTROL_VIS);
+ if (control)
+ return control->OnAction(action);
+ }
+
+ return CGUIWindow::OnAction(action);
+}
+
+bool CGUIWindowVisualisation::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_GET_VISUALISATION:
+ case GUI_MSG_VISUALISATION_RELOAD:
+ case GUI_MSG_PLAYBACK_STARTED:
+ {
+ CGUIControl *control = GetControl(CONTROL_VIS);
+ if (control)
+ return control->OnMessage(message);
+ }
+ break;
+ case GUI_MSG_VISUALISATION_ACTION:
+ {
+ CAction action(message.GetParam1());
+ return OnAction(action);
+ }
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ if (IsActive()) // save any changed settings from the OSD
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ // close all active modal dialogs
+ CServiceBroker::GetGUI()->GetWindowManager().CloseInternalModalDialogs(true);
+ }
+ break;
+ case GUI_MSG_WINDOW_INIT:
+ {
+ // check whether we've come back here from a window during which time we've actually
+ // stopped playing music
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (message.GetParam1() == WINDOW_INVALID && !appPlayer->IsPlayingAudio())
+ { // why are we here if nothing is playing???
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return true;
+ }
+
+ // hide or show the preset button(s)
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ infoMgr.GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(true); // always show the info initially.
+ CGUIWindow::OnMessage(message);
+ if (infoMgr.GetCurrentSongTag())
+ m_tag = *infoMgr.GetCurrentSongTag();
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYMUSIC_SONGTHUMBINVIS))
+ { // always on
+ m_initTimer.Stop();
+ }
+ else
+ {
+ // start display init timer (fade out after 3 secs...)
+ m_initTimer.StartZero();
+ }
+ return true;
+ }
+ }
+ return CGUIWindow::OnMessage(message);
+}
+
+EVENT_RESULT CGUIWindowVisualisation::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_RIGHT_CLICK)
+ { // no control found to absorb this click - go back to GUI
+ OnAction(CAction(ACTION_SHOW_GUI));
+ return EVENT_RESULT_HANDLED;
+ }
+ if (event.m_id == ACTION_GESTURE_NOTIFY)
+ return EVENT_RESULT_UNHANDLED;
+ if (event.m_id != ACTION_MOUSE_MOVE || event.m_offsetX || event.m_offsetY)
+ { // some other mouse action has occurred - bring up the OSD
+ CGUIDialog *pOSD = CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_MUSIC_OSD);
+ if (pOSD)
+ {
+ pOSD->SetAutoClose(3000);
+ pOSD->Open();
+ }
+ return EVENT_RESULT_HANDLED;
+ }
+ return EVENT_RESULT_UNHANDLED;
+}
+
+void CGUIWindowVisualisation::FrameMove()
+{
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+
+ // check for a tag change
+ const CMusicInfoTag* tag = infoMgr.GetCurrentSongTag();
+ if (tag && *tag != m_tag)
+ { // need to fade in then out again
+ m_tag = *tag;
+ // fade in
+ m_initTimer.StartZero();
+ infoMgr.GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(true);
+ }
+ if (m_initTimer.IsRunning() && m_initTimer.GetElapsedSeconds() > (float)CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_songInfoDuration)
+ {
+ m_initTimer.Stop();
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYMUSIC_SONGTHUMBINVIS))
+ { // reached end of fade in, fade out again
+ infoMgr.GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(false);
+ }
+ }
+ // show or hide the locked texture
+ if (m_lockedTimer.IsRunning() && m_lockedTimer.GetElapsedSeconds() > START_FADE_LENGTH)
+ {
+ m_lockedTimer.Stop();
+ }
+ CGUIWindow::FrameMove();
+}
diff --git a/xbmc/music/windows/GUIWindowVisualisation.h b/xbmc/music/windows/GUIWindowVisualisation.h
new file mode 100644
index 0000000..ce9a198
--- /dev/null
+++ b/xbmc/music/windows/GUIWindowVisualisation.h
@@ -0,0 +1,31 @@
+/*
+ * 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 "guilib/GUIWindow.h"
+#include "music/tags/MusicInfoTag.h"
+#include "utils/Stopwatch.h"
+
+class CGUIWindowVisualisation :
+ public CGUIWindow
+{
+public:
+ CGUIWindowVisualisation(void);
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ void FrameMove() override;
+protected:
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+
+ CStopWatch m_initTimer;
+ CStopWatch m_lockedTimer;
+ bool m_bShowPreset;
+ MUSIC_INFO::CMusicInfoTag m_tag; // current tag info, for finding when the info manager updates
+};
+
diff --git a/xbmc/music/windows/MusicFileItemListModifier.cpp b/xbmc/music/windows/MusicFileItemListModifier.cpp
new file mode 100644
index 0000000..c78de79
--- /dev/null
+++ b/xbmc/music/windows/MusicFileItemListModifier.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016-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 "MusicFileItemListModifier.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "filesystem/MusicDatabaseDirectory/DirectoryNode.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/MusicDbUrl.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+using namespace XFILE::MUSICDATABASEDIRECTORY;
+
+bool CMusicFileItemListModifier::CanModify(const CFileItemList &items) const
+{
+ if (items.IsMusicDb())
+ return true;
+
+ return false;
+}
+
+bool CMusicFileItemListModifier::Modify(CFileItemList &items) const
+{
+ AddQueuingFolder(items);
+ return true;
+}
+
+// Add an "* All ..." folder to the CFileItemList
+// depending on the child node
+void CMusicFileItemListModifier::AddQueuingFolder(CFileItemList& items)
+{
+ if (!items.IsMusicDb())
+ return;
+
+ auto directoryNode = CDirectoryNode::ParseURL(items.GetPath());
+
+ CFileItemPtr pItem;
+
+ CMusicDbUrl musicUrl;
+ if (!musicUrl.FromString(directoryNode->BuildPath()))
+ return;
+
+ // always show "all" items by default
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWALLITEMS))
+ return;
+
+ // no need for "all" item when only one item
+ if (items.GetObjectCount() <= 1)
+ return;
+
+ auto nodeChildType = directoryNode->GetChildType();
+
+ // No need for "*all" when overview node and child node is "albums" or "artists"
+ // without options (hence all albums or artists unfiltered).
+ if (directoryNode->GetType() == NODE_TYPE_OVERVIEW &&
+ (nodeChildType == NODE_TYPE_ARTIST || nodeChildType == NODE_TYPE_ALBUM) &&
+ musicUrl.GetOptions().empty())
+ return;
+ // Smart playlist rules on parent node do not get applied to child nodes so no "*all"
+ // ! @Todo: Remove this allowing "*all" once rules do get applied to child nodes.
+ if (directoryNode->GetType() == NODE_TYPE_OVERVIEW &&
+ (nodeChildType == NODE_TYPE_ARTIST || nodeChildType == NODE_TYPE_ALBUM) &&
+ musicUrl.HasOption("xsp"))
+ return;
+
+ switch (nodeChildType)
+ {
+ case NODE_TYPE_ARTIST:
+ pItem.reset(new CFileItem(g_localizeStrings.Get(15103))); // "All Artists"
+ musicUrl.AppendPath("-1/");
+ pItem->SetPath(musicUrl.ToString());
+ break;
+
+ // All album related nodes
+ case NODE_TYPE_ALBUM:
+ case NODE_TYPE_ALBUM_RECENTLY_PLAYED:
+ case NODE_TYPE_ALBUM_RECENTLY_ADDED:
+ case NODE_TYPE_ALBUM_TOP100:
+ pItem.reset(new CFileItem(g_localizeStrings.Get(15102))); // "All Albums"
+ musicUrl.AppendPath("-1/");
+ pItem->SetPath(musicUrl.ToString());
+ break;
+
+ // Disc node
+ case NODE_TYPE_DISC:
+ pItem.reset(new CFileItem(g_localizeStrings.Get(38075))); // "All Discs"
+ musicUrl.AppendPath("-1/");
+ pItem->SetPath(musicUrl.ToString());
+ break;
+
+ default:
+ break;
+ }
+
+ if (pItem)
+ {
+ pItem->m_bIsFolder = true;
+ pItem->SetSpecialSort(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryAllItemsOnBottom ? SortSpecialOnBottom : SortSpecialOnTop);
+ pItem->SetCanQueue(false);
+ pItem->SetLabelPreformatted(true);
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryAllItemsOnBottom)
+ items.Add(pItem);
+ else
+ items.AddFront(pItem, (items.Size() > 0 && items[0]->IsParentFolder()) ? 1 : 0);
+ }
+}
diff --git a/xbmc/music/windows/MusicFileItemListModifier.h b/xbmc/music/windows/MusicFileItemListModifier.h
new file mode 100644
index 0000000..9a2190b
--- /dev/null
+++ b/xbmc/music/windows/MusicFileItemListModifier.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016-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 "IFileItemListModifier.h"
+
+class CMusicFileItemListModifier : public IFileItemListModifier
+{
+public:
+ CMusicFileItemListModifier() = default;
+ ~CMusicFileItemListModifier() override = default;
+
+ bool CanModify(const CFileItemList &items) const override;
+ bool Modify(CFileItemList &items) const override;
+
+private:
+ static void AddQueuingFolder(CFileItemList & items);
+};
diff --git a/xbmc/network/AirPlayServer.cpp b/xbmc/network/AirPlayServer.cpp
new file mode 100644
index 0000000..74251d3
--- /dev/null
+++ b/xbmc/network/AirPlayServer.cpp
@@ -0,0 +1,1225 @@
+/*
+ * Copyright (C) 2011-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.
+ *
+ * Many concepts and protocol specification in this code are taken
+ * from Airplayer. https://github.com/PascalW/Airplayer
+ */
+
+#include "AirPlayServer.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "playlists/PlayListTypes.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#ifdef HAS_ZEROCONF
+#include "network/Zeroconf.h"
+#endif // HAS_ZEROCONF
+
+#include <inttypes.h>
+
+#include <plist/plist.h>
+
+using KODI::UTILITY::CDigest;
+using namespace std::chrono_literals;
+
+#ifdef TARGET_WINDOWS
+#define close closesocket
+#endif
+
+#define RECEIVEBUFFER 1024
+
+#define AIRPLAY_STATUS_OK 200
+#define AIRPLAY_STATUS_SWITCHING_PROTOCOLS 101
+#define AIRPLAY_STATUS_NEED_AUTH 401
+#define AIRPLAY_STATUS_NOT_FOUND 404
+#define AIRPLAY_STATUS_METHOD_NOT_ALLOWED 405
+#define AIRPLAY_STATUS_PRECONDITION_FAILED 412
+#define AIRPLAY_STATUS_NOT_IMPLEMENTED 501
+#define AIRPLAY_STATUS_NO_RESPONSE_NEEDED 1000
+
+CCriticalSection CAirPlayServer::ServerInstanceLock;
+CAirPlayServer *CAirPlayServer::ServerInstance = NULL;
+int CAirPlayServer::m_isPlaying = 0;
+
+#define EVENT_NONE -1
+#define EVENT_PLAYING 0
+#define EVENT_PAUSED 1
+#define EVENT_LOADING 2
+#define EVENT_STOPPED 3
+const char *eventStrings[] = {"playing", "paused", "loading", "stopped"};
+
+#define PLAYBACK_INFO \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " \
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n" \
+ "<plist version=\"1.0\">\r\n" \
+ "<dict>\r\n" \
+ "<key>duration</key>\r\n" \
+ "<real>{:f}</real>\r\n" \
+ "<key>loadedTimeRanges</key>\r\n" \
+ "<array>\r\n" \
+ "\t\t<dict>\r\n" \
+ "\t\t\t<key>duration</key>\r\n" \
+ "\t\t\t<real>{:f}</real>\r\n" \
+ "\t\t\t<key>start</key>\r\n" \
+ "\t\t\t<real>0.0</real>\r\n" \
+ "\t\t</dict>\r\n" \
+ "</array>\r\n" \
+ "<key>playbackBufferEmpty</key>\r\n" \
+ "<true/>\r\n" \
+ "<key>playbackBufferFull</key>\r\n" \
+ "<false/>\r\n" \
+ "<key>playbackLikelyToKeepUp</key>\r\n" \
+ "<true/>\r\n" \
+ "<key>position</key>\r\n" \
+ "<real>{:f}</real>\r\n" \
+ "<key>rate</key>\r\n" \
+ "<real>{:d}</real>\r\n" \
+ "<key>readyToPlay</key>\r\n" \
+ "<true/>\r\n" \
+ "<key>seekableTimeRanges</key>\r\n" \
+ "<array>\r\n" \
+ "\t\t<dict>\r\n" \
+ "\t\t\t<key>duration</key>\r\n" \
+ "\t\t\t<real>{:f}</real>\r\n" \
+ "\t\t\t<key>start</key>\r\n" \
+ "\t\t\t<real>0.0</real>\r\n" \
+ "\t\t</dict>\r\n" \
+ "</array>\r\n" \
+ "</dict>\r\n" \
+ "</plist>\r\n"
+
+#define PLAYBACK_INFO_NOT_READY "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
+"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n"\
+"<plist version=\"1.0\">\r\n"\
+"<dict>\r\n"\
+"<key>readyToPlay</key>\r\n"\
+"<false/>\r\n"\
+"</dict>\r\n"\
+"</plist>\r\n"
+
+#define SERVER_INFO \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" \
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " \
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n" \
+ "<plist version=\"1.0\">\r\n" \
+ "<dict>\r\n" \
+ "<key>deviceid</key>\r\n" \
+ "<string>{:s}</string>\r\n" \
+ "<key>features</key>\r\n" \
+ "<integer>119</integer>\r\n" \
+ "<key>model</key>\r\n" \
+ "<string>Kodi,1</string>\r\n" \
+ "<key>protovers</key>\r\n" \
+ "<string>1.0</string>\r\n" \
+ "<key>srcvers</key>\r\n" \
+ "<string>" AIRPLAY_SERVER_VERSION_STR "</string>\r\n" \
+ "</dict>\r\n" \
+ "</plist>\r\n"
+
+#define EVENT_INFO \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\r\n" \
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " \
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\r\n" \
+ "<plist version=\"1.0\">\r\n" \
+ "<dict>\r\n" \
+ "<key>category</key>\r\n" \
+ "<string>video</string>\r\n" \
+ "<key>sessionID</key>\r\n" \
+ "<integer>{:d}</integer>\r\n" \
+ "<key>state</key>\r\n" \
+ "<string>{:s}</string>\r\n" \
+ "</dict>\r\n" \
+ "</plist>\r\n"
+
+#define AUTH_REALM "AirPlay"
+#define AUTH_REQUIRED "WWW-Authenticate: Digest realm=\"" AUTH_REALM "\", nonce=\"{:s}\"\r\n"
+
+void CAirPlayServer::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ // We are only interested in player changes
+ if ((flag & ANNOUNCEMENT::Player) == 0)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ if (sender == ANNOUNCEMENT::CAnnouncementManager::ANNOUNCEMENT_SENDER && ServerInstance)
+ {
+ if (message == "OnStop")
+ {
+ bool shouldRestoreVolume = true;
+ if (data.isMember("player") && data["player"].isMember("playerid"))
+ shouldRestoreVolume = (data["player"]["playerid"] != PLAYLIST::TYPE_PICTURE);
+
+ if (shouldRestoreVolume)
+ restoreVolume();
+
+ ServerInstance->AnnounceToClients(EVENT_STOPPED);
+ }
+ else if (message == "OnPlay" || message == "OnResume")
+ {
+ ServerInstance->AnnounceToClients(EVENT_PLAYING);
+ }
+ else if (message == "OnPause")
+ {
+ ServerInstance->AnnounceToClients(EVENT_PAUSED);
+ }
+ }
+}
+
+bool CAirPlayServer::StartServer(int port, bool nonlocal)
+{
+ StopServer(true);
+
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ ServerInstance = new CAirPlayServer(port, nonlocal);
+ if (ServerInstance->Initialize())
+ {
+ ServerInstance->Create();
+ return true;
+ }
+ else
+ return false;
+}
+
+bool CAirPlayServer::SetCredentials(bool usePassword, const std::string& password)
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+ bool ret = false;
+
+ if (ServerInstance)
+ {
+ ret = ServerInstance->SetInternalCredentials(usePassword, password);
+ }
+ return ret;
+}
+
+bool CAirPlayServer::SetInternalCredentials(bool usePassword, const std::string& password)
+{
+ m_usePassword = usePassword;
+ m_password = password;
+ return true;
+}
+
+void ClearPhotoAssetCache()
+{
+ CLog::Log(LOGINFO, "AIRPLAY: Cleaning up photoassetcache");
+ // remove all cached photos
+ CFileItemList items;
+ XFILE::CDirectory::GetDirectory("special://temp/", items, "", XFILE::DIR_FLAG_DEFAULTS);
+
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr pItem = items[i];
+ if (!pItem->m_bIsFolder)
+ {
+ if (StringUtils::StartsWithNoCase(pItem->GetLabel(), "airplayasset") &&
+ (StringUtils::EndsWithNoCase(pItem->GetLabel(), ".jpg") ||
+ StringUtils::EndsWithNoCase(pItem->GetLabel(), ".png") ))
+ {
+ XFILE::CFile::Delete(pItem->GetPath());
+ }
+ }
+ }
+}
+
+void CAirPlayServer::StopServer(bool bWait)
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+ //clean up the photo cache temp folder
+ ClearPhotoAssetCache();
+
+ if (ServerInstance)
+ {
+ ServerInstance->StopThread(bWait);
+ if (bWait)
+ {
+ delete ServerInstance;
+ ServerInstance = NULL;
+ }
+ }
+}
+
+bool CAirPlayServer::IsRunning()
+{
+ if (ServerInstance == NULL)
+ return false;
+
+ return static_cast<CThread*>(ServerInstance)->IsRunning();
+}
+
+void CAirPlayServer::AnnounceToClients(int state)
+{
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+
+ for (auto& it : m_connections)
+ {
+ std::string reverseHeader;
+ std::string reverseBody;
+ std::string response;
+ int reverseSocket = INVALID_SOCKET;
+ it.ComposeReverseEvent(reverseHeader, reverseBody, state);
+
+ // Send event status per reverse http socket (play, loading, paused)
+ // if we have a reverse header and a reverse socket
+ if (!reverseHeader.empty() && m_reverseSockets.find(it.m_sessionId) != m_reverseSockets.end())
+ {
+ //search the reverse socket to this sessionid
+ response = StringUtils::Format("POST /event HTTP/1.1\r\n");
+ reverseSocket = m_reverseSockets[it.m_sessionId]; //that is our reverse socket
+ response += reverseHeader;
+ }
+ response += "\r\n";
+
+ if (!reverseBody.empty())
+ {
+ response += reverseBody;
+ }
+
+ // don't send it to the connection object
+ // the reverse socket itself belongs to
+ if (reverseSocket != INVALID_SOCKET && reverseSocket != it.m_socket)
+ {
+ send(reverseSocket, response.c_str(), response.size(), 0);//send the event status on the eventSocket
+ }
+ }
+}
+
+CAirPlayServer::CAirPlayServer(int port, bool nonlocal) : CThread("AirPlayServer")
+{
+ m_port = port;
+ m_nonlocal = nonlocal;
+ m_ServerSockets = std::vector<SOCKET>();
+ m_usePassword = false;
+ m_origVolume = -1;
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+CAirPlayServer::~CAirPlayServer()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+void handleZeroconfAnnouncement()
+{
+#if defined(HAS_ZEROCONF)
+ static XbmcThreads::EndTime<> timeout(10s);
+ if(timeout.IsTimePast())
+ {
+ CZeroconf::GetInstance()->ForceReAnnounceService("servers.airplay");
+ timeout.Set(10s);
+ }
+#endif
+}
+
+void CAirPlayServer::Process()
+{
+ m_bStop = false;
+ static int sessionCounter = 0;
+
+ while (!m_bStop)
+ {
+ int max_fd = 0;
+ fd_set rfds;
+ struct timeval to = {1, 0};
+ FD_ZERO(&rfds);
+
+ for (SOCKET socket : m_ServerSockets)
+ {
+ FD_SET(socket, &rfds);
+ if ((intptr_t)socket > (intptr_t)max_fd)
+ max_fd = socket;
+ }
+
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ {
+ FD_SET(m_connections[i].m_socket, &rfds);
+ if (m_connections[i].m_socket > max_fd)
+ max_fd = m_connections[i].m_socket;
+ }
+
+ int res = select(max_fd+1, &rfds, NULL, NULL, &to);
+ if (res < 0)
+ {
+ CLog::Log(LOGERROR, "AIRPLAY Server: Select failed");
+ CThread::Sleep(1000ms);
+ Initialize();
+ }
+ else if (res > 0)
+ {
+ for (int i = m_connections.size() - 1; i >= 0; i--)
+ {
+ int socket = m_connections[i].m_socket;
+ if (FD_ISSET(socket, &rfds))
+ {
+ char buffer[RECEIVEBUFFER] = {};
+ int nread = 0;
+ nread = recv(socket, (char*)&buffer, RECEIVEBUFFER, 0);
+ if (nread > 0)
+ {
+ std::string sessionId;
+ m_connections[i].PushBuffer(this, buffer, nread, sessionId, m_reverseSockets);
+ }
+ if (nread <= 0)
+ {
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+ CLog::Log(LOGINFO, "AIRPLAY Server: Disconnection detected");
+ m_connections[i].Disconnect();
+ m_connections.erase(m_connections.begin() + i);
+ }
+ }
+ }
+
+ for (SOCKET socket : m_ServerSockets)
+ {
+ if (FD_ISSET(socket, &rfds))
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY Server: New connection detected");
+ CTCPClient newconnection;
+ newconnection.m_socket = accept(socket, (struct sockaddr*) &newconnection.m_cliaddr, &newconnection.m_addrlen);
+ sessionCounter++;
+ newconnection.m_sessionCounter = sessionCounter;
+
+ if (newconnection.m_socket == INVALID_SOCKET)
+ {
+ CLog::Log(LOGERROR, "AIRPLAY Server: Accept of new connection failed: {}", errno);
+ if (EBADF == errno)
+ {
+ CThread::Sleep(1000ms);
+ Initialize();
+ break;
+ }
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+ CLog::Log(LOGINFO, "AIRPLAY Server: New connection added");
+ m_connections.push_back(newconnection);
+ }
+ }
+ }
+ }
+
+ // by reannouncing the zeroconf service
+ // we fix issues where xbmc is detected
+ // as audio-only target on devices with
+ // ios7 and later
+ handleZeroconfAnnouncement();
+ }
+
+ Deinitialize();
+}
+
+bool CAirPlayServer::Initialize()
+{
+ Deinitialize();
+
+ m_ServerSockets = CreateTCPServerSocket(m_port, !m_nonlocal, 10, "AIRPLAY");
+ if (m_ServerSockets.empty())
+ return false;
+
+ CLog::Log(LOGINFO, "AIRPLAY Server: Successfully initialized");
+ return true;
+}
+
+void CAirPlayServer::Deinitialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_connectionLock);
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ m_connections[i].Disconnect();
+
+ m_connections.clear();
+ m_reverseSockets.clear();
+
+ for (SOCKET socket : m_ServerSockets)
+ {
+ shutdown(socket, SHUT_RDWR);
+ close(socket);
+ }
+ m_ServerSockets.clear();
+}
+
+CAirPlayServer::CTCPClient::CTCPClient()
+{
+ m_socket = INVALID_SOCKET;
+ m_httpParser = new HttpParser();
+
+ m_addrlen = sizeof(struct sockaddr_storage);
+
+ m_bAuthenticated = false;
+ m_lastEvent = EVENT_NONE;
+}
+
+CAirPlayServer::CTCPClient::CTCPClient(const CTCPClient& client)
+: m_lastEvent(EVENT_NONE)
+{
+ Copy(client);
+ m_httpParser = new HttpParser();
+}
+
+CAirPlayServer::CTCPClient::~CTCPClient()
+{
+ delete m_httpParser;
+}
+
+CAirPlayServer::CTCPClient& CAirPlayServer::CTCPClient::operator=(const CTCPClient& client)
+{
+ Copy(client);
+ m_httpParser = new HttpParser();
+ return *this;
+}
+
+void CAirPlayServer::CTCPClient::PushBuffer(CAirPlayServer *host, const char *buffer,
+ int length, std::string &sessionId, std::map<std::string,
+ int> &reverseSockets)
+{
+ HttpParser::status_t status = m_httpParser->addBytes(buffer, length);
+
+ if (status == HttpParser::Done)
+ {
+ // Parse the request
+ std::string responseHeader;
+ std::string responseBody;
+ int status = ProcessRequest(responseHeader, responseBody);
+ sessionId = m_sessionId;
+ std::string statusMsg = "OK";
+
+ switch(status)
+ {
+ case AIRPLAY_STATUS_NOT_IMPLEMENTED:
+ statusMsg = "Not Implemented";
+ break;
+ case AIRPLAY_STATUS_SWITCHING_PROTOCOLS:
+ statusMsg = "Switching Protocols";
+ reverseSockets[sessionId] = m_socket;//save this socket as reverse http socket for this sessionid
+ break;
+ case AIRPLAY_STATUS_NEED_AUTH:
+ statusMsg = "Unauthorized";
+ break;
+ case AIRPLAY_STATUS_NOT_FOUND:
+ statusMsg = "Not Found";
+ break;
+ case AIRPLAY_STATUS_METHOD_NOT_ALLOWED:
+ statusMsg = "Method Not Allowed";
+ break;
+ case AIRPLAY_STATUS_PRECONDITION_FAILED:
+ statusMsg = "Precondition Failed";
+ break;
+ }
+
+ // Prepare the response
+ std::string response;
+ const time_t ltime = time(NULL);
+ char *date = asctime(gmtime(&ltime)); //Fri, 17 Dec 2010 11:18:01 GMT;
+ date[strlen(date) - 1] = '\0'; // remove \n
+ response = StringUtils::Format("HTTP/1.1 {} {}\nDate: {}\r\n", status, statusMsg, date);
+ if (!responseHeader.empty())
+ {
+ response += responseHeader;
+ }
+
+ response = StringUtils::Format("{}Content-Length: {}\r\n\r\n", response, responseBody.size());
+
+ if (!responseBody.empty())
+ {
+ response += responseBody;
+ }
+
+ // Send the response
+ //don't send response on AIRPLAY_STATUS_NO_RESPONSE_NEEDED
+ if (status != AIRPLAY_STATUS_NO_RESPONSE_NEEDED)
+ {
+ send(m_socket, response.c_str(), response.size(), 0);
+ }
+ // We need a new parser...
+ delete m_httpParser;
+ m_httpParser = new HttpParser;
+ }
+}
+
+void CAirPlayServer::CTCPClient::Disconnect()
+{
+ if (m_socket != INVALID_SOCKET)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ shutdown(m_socket, SHUT_RDWR);
+ close(m_socket);
+ m_socket = INVALID_SOCKET;
+ delete m_httpParser;
+ m_httpParser = NULL;
+ }
+}
+
+void CAirPlayServer::CTCPClient::Copy(const CTCPClient& client)
+{
+ m_socket = client.m_socket;
+ m_cliaddr = client.m_cliaddr;
+ m_addrlen = client.m_addrlen;
+ m_httpParser = client.m_httpParser;
+ m_authNonce = client.m_authNonce;
+ m_bAuthenticated = client.m_bAuthenticated;
+ m_sessionCounter = client.m_sessionCounter;
+}
+
+
+void CAirPlayServer::CTCPClient::ComposeReverseEvent( std::string& reverseHeader,
+ std::string& reverseBody,
+ int state)
+{
+
+ if ( m_lastEvent != state )
+ {
+ switch(state)
+ {
+ case EVENT_PLAYING:
+ case EVENT_LOADING:
+ case EVENT_PAUSED:
+ case EVENT_STOPPED:
+ reverseBody = StringUtils::Format(EVENT_INFO, m_sessionCounter, eventStrings[state]);
+ CLog::Log(LOGDEBUG, "AIRPLAY: sending event: {}", eventStrings[state]);
+ break;
+ }
+ reverseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+ reverseHeader =
+ StringUtils::Format("{}Content-Length: {}\r\n", reverseHeader, reverseBody.size());
+ reverseHeader = StringUtils::Format("{}x-apple-session-id: {}\r\n", reverseHeader.c_str(),
+ m_sessionId.c_str());
+ m_lastEvent = state;
+ }
+}
+
+void CAirPlayServer::CTCPClient::ComposeAuthRequestAnswer(std::string& responseHeader, std::string& responseBody)
+{
+ int16_t random=rand();
+ std::string randomStr = std::to_string(random);
+ m_authNonce=CDigest::Calculate(CDigest::Type::MD5, randomStr);
+ responseHeader = StringUtils::Format(AUTH_REQUIRED, m_authNonce);
+ responseBody.clear();
+}
+
+
+//as of rfc 2617
+std::string calcResponse(const std::string& username,
+ const std::string& password,
+ const std::string& realm,
+ const std::string& method,
+ const std::string& digestUri,
+ const std::string& nonce)
+{
+ std::string response;
+ std::string HA1;
+ std::string HA2;
+
+ HA1 = CDigest::Calculate(CDigest::Type::MD5, username + ":" + realm + ":" + password);
+ HA2 = CDigest::Calculate(CDigest::Type::MD5, method + ":" + digestUri);
+ response = CDigest::Calculate(CDigest::Type::MD5, HA1 + ":" + nonce + ":" + HA2);
+ return response;
+}
+
+//helper function
+//from a string field1="value1", field2="value2" it parses the value to a field
+std::string getFieldFromString(const std::string &str, const char* field)
+{
+ std::vector<std::string> tmpAr1 = StringUtils::Split(str, ",");
+ for (const auto& i : tmpAr1)
+ {
+ if (i.find(field) != std::string::npos)
+ {
+ std::vector<std::string> tmpAr2 = StringUtils::Split(i, "=");
+ if (tmpAr2.size() == 2)
+ {
+ StringUtils::Replace(tmpAr2[1], "\"", "");//remove quotes
+ return tmpAr2[1];
+ }
+ }
+ }
+ return "";
+}
+
+bool CAirPlayServer::CTCPClient::checkAuthorization(const std::string& authStr,
+ const std::string& method,
+ const std::string& uri)
+{
+ bool authValid = true;
+
+ std::string username;
+
+ if (authStr.empty())
+ return false;
+
+ //first get username - we allow all usernames for airplay (usually it is AirPlay)
+ username = getFieldFromString(authStr, "username");
+ if (username.empty())
+ {
+ authValid = false;
+ }
+
+ //second check realm
+ if (authValid)
+ {
+ if (getFieldFromString(authStr, "realm") != AUTH_REALM)
+ {
+ authValid = false;
+ }
+ }
+
+ //third check nonce
+ if (authValid)
+ {
+ if (getFieldFromString(authStr, "nonce") != m_authNonce)
+ {
+ authValid = false;
+ }
+ }
+
+ //forth check uri
+ if (authValid)
+ {
+ if (getFieldFromString(authStr, "uri") != uri)
+ {
+ authValid = false;
+ }
+ }
+
+ //last check response
+ if (authValid)
+ {
+ std::string realm = AUTH_REALM;
+ std::string ourResponse = calcResponse(username, ServerInstance->m_password, realm, method, uri, m_authNonce);
+ std::string theirResponse = getFieldFromString(authStr, "response");
+ if (!StringUtils::EqualsNoCase(theirResponse, ourResponse))
+ {
+ authValid = false;
+ CLog::Log(LOGDEBUG, "AirAuth: response mismatch - our: {} theirs: {}", ourResponse,
+ theirResponse);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "AirAuth: successful authentication from AirPlay client");
+ }
+ }
+ m_bAuthenticated = authValid;
+ return m_bAuthenticated;
+}
+
+void CAirPlayServer::backupVolume()
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ if (ServerInstance && ServerInstance->m_origVolume == -1)
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ ServerInstance->m_origVolume = static_cast<int>(appVolume->GetVolumePercent());
+ }
+}
+
+void CAirPlayServer::restoreVolume()
+{
+ std::unique_lock<CCriticalSection> lock(ServerInstanceLock);
+
+ const auto& settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (ServerInstance && ServerInstance->m_origVolume != -1 &&
+ settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL))
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume(static_cast<float>(ServerInstance->m_origVolume));
+ ServerInstance->m_origVolume = -1;
+ }
+}
+
+std::string getStringFromPlist(plist_t node)
+{
+ std::string ret;
+ char *tmpStr = nullptr;
+ plist_get_string_val(node, &tmpStr);
+ ret = tmpStr;
+ free(tmpStr);
+ return ret;
+}
+
+int CAirPlayServer::CTCPClient::ProcessRequest( std::string& responseHeader,
+ std::string& responseBody)
+{
+ std::string method = m_httpParser->getMethod() ? m_httpParser->getMethod() : "";
+ std::string uri = m_httpParser->getUri() ? m_httpParser->getUri() : "";
+ std::string queryString = m_httpParser->getQueryString() ? m_httpParser->getQueryString() : "";
+ std::string body = m_httpParser->getBody() ? m_httpParser->getBody() : "";
+ std::string contentType = m_httpParser->getValue("content-type") ? m_httpParser->getValue("content-type") : "";
+ m_sessionId = m_httpParser->getValue("x-apple-session-id") ? m_httpParser->getValue("x-apple-session-id") : "";
+ std::string authorization = m_httpParser->getValue("authorization") ? m_httpParser->getValue("authorization") : "";
+ std::string photoAction = m_httpParser->getValue("x-apple-assetaction") ? m_httpParser->getValue("x-apple-assetaction") : "";
+ std::string photoCacheId = m_httpParser->getValue("x-apple-assetkey") ? m_httpParser->getValue("x-apple-assetkey") : "";
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ int status = AIRPLAY_STATUS_OK;
+ bool needAuth = false;
+
+ if (m_sessionId.empty())
+ m_sessionId = "00000000-0000-0000-0000-000000000000";
+
+ if (ServerInstance->m_usePassword && !m_bAuthenticated)
+ {
+ needAuth = true;
+ }
+
+ size_t startQs = uri.find('?');
+ if (startQs != std::string::npos)
+ {
+ uri.erase(startQs);
+ }
+
+ // This is the socket which will be used for reverse HTTP
+ // negotiate reverse HTTP via upgrade
+ if (uri == "/reverse")
+ {
+ status = AIRPLAY_STATUS_SWITCHING_PROTOCOLS;
+ responseHeader = "Upgrade: PTTH/1.0\r\nConnection: Upgrade\r\n";
+ }
+
+ // The rate command is used to play/pause media.
+ // A value argument should be supplied which indicates media should be played or paused.
+ // 0.000000 => pause
+ // 1.000000 => play
+ else if (uri == "/rate")
+ {
+ const char* found = strstr(queryString.c_str(), "value=");
+ int rate = found ? (int)(atof(found + strlen("value=")) + 0.5) : 0;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {} with rate {}", uri, rate);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (rate == 0)
+ {
+ if (appPlayer->IsPlaying() && !appPlayer->IsPaused())
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ }
+ else
+ {
+ if (appPlayer->IsPausedPlayback())
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ }
+ }
+
+ // The volume command is used to change playback volume.
+ // A value argument should be supplied which indicates how loud we should get.
+ // 0.000000 => silent
+ // 1.000000 => loud
+ else if (uri == "/volume")
+ {
+ const char* found = strstr(queryString.c_str(), "volume=");
+ float volume = found ? (float)strtod(found + strlen("volume="), NULL) : 0;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {} with volume {:f}", uri, volume);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (volume >= 0 && volume <= 1)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ float oldVolume = appVolume->GetVolumePercent();
+ volume *= 100;
+ const auto& settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (oldVolume != volume &&
+ settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL))
+ {
+ backupVolume();
+ appVolume->SetVolume(volume);
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_VOLUME_SHOW, oldVolume < volume ? ACTION_VOLUME_UP : ACTION_VOLUME_DOWN);
+ }
+ }
+ }
+
+
+ // Contains a header like format in the request body which should contain a
+ // Content-Location and optionally a Start-Position
+ else if (uri == "/play")
+ {
+ std::string location;
+ float position = 0.0;
+ bool startPlayback = true;
+ m_lastEvent = EVENT_NONE;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (contentType == "application/x-apple-binary-plist")
+ {
+ CAirPlayServer::m_isPlaying++;
+
+ const char* bodyChr = m_httpParser->getBody();
+
+ plist_t dict = NULL;
+ plist_from_bin(bodyChr, m_httpParser->getContentLength(), &dict);
+
+ if (plist_dict_get_size(dict))
+ {
+ plist_t tmpNode = plist_dict_get_item(dict, "Start-Position");
+ if (tmpNode)
+ {
+ double tmpDouble = 0;
+ plist_get_real_val(tmpNode, &tmpDouble);
+ position = (float)tmpDouble;
+ }
+
+ tmpNode = plist_dict_get_item(dict, "Content-Location");
+ if (tmpNode)
+ {
+ location = getStringFromPlist(tmpNode);
+ tmpNode = NULL;
+ }
+
+ tmpNode = plist_dict_get_item(dict, "rate");
+ if (tmpNode)
+ {
+ double rate = 0;
+ plist_get_real_val(tmpNode, &rate);
+ if (rate == 0.0)
+ {
+ startPlayback = false;
+ }
+ tmpNode = NULL;
+ }
+
+ // in newer protocol versions the location is given
+ // via host and path where host is ip:port and path is /path/file.mov
+ if (location.empty())
+ tmpNode = plist_dict_get_item(dict, "host");
+ if (tmpNode)
+ {
+ location = "http://";
+ location += getStringFromPlist(tmpNode);
+
+ tmpNode = plist_dict_get_item(dict, "path");
+ if (tmpNode)
+ {
+ location += getStringFromPlist(tmpNode);
+ }
+ }
+
+ if (dict)
+ {
+ plist_free(dict);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Error parsing plist");
+ }
+ }
+ else
+ {
+ CAirPlayServer::m_isPlaying++;
+ // Get URL to play
+ std::string contentLocation = "Content-Location: ";
+ size_t start = body.find(contentLocation);
+ if (start == std::string::npos)
+ return AIRPLAY_STATUS_NOT_IMPLEMENTED;
+ start += contentLocation.size();
+ int end = body.find('\n', start);
+ location = body.substr(start, end - start);
+
+ std::string startPosition = "Start-Position: ";
+ start = body.find(startPosition);
+ if (start != std::string::npos)
+ {
+ start += startPosition.size();
+ int end = body.find('\n', start);
+ std::string positionStr = body.substr(start, end - start);
+ position = (float)atof(positionStr.c_str());
+ }
+ }
+
+ if (status != AIRPLAY_STATUS_NEED_AUTH)
+ {
+ std::string userAgent(CURL::Encode("AppleCoreMedia/1.0.0.8F455 (AppleTV; U; CPU OS 4_3 like Mac OS X; de_de)"));
+ location += "|User-Agent=" + userAgent;
+
+ CFileItem fileToPlay(location, false);
+ fileToPlay.SetProperty("StartPercent", position*100.0f);
+ ServerInstance->AnnounceToClients(EVENT_LOADING);
+
+ CFileItemList *l = new CFileItemList; //don't delete,
+ l->Add(std::make_shared<CFileItem>(fileToPlay));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, -1, -1, static_cast<void*>(l));
+
+ // allow starting the player paused in ios8 mode (needed by camera roll app)
+ if (!startPlayback)
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ appPlayer->SeekPercentage(position * 100.0f);
+ }
+ }
+ }
+
+ // Used to perform seeking (POST request) and to retrieve current player position (GET request).
+ // GET scrub seems to also set rate 1 - strange but true
+ else if (uri == "/scrub")
+ {
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (method == "GET")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got GET request {}", uri);
+
+ if (appPlayer->GetTotalTime())
+ {
+ float position = static_cast<float>(appPlayer->GetTime()) / 1000;
+ responseBody =
+ StringUtils::Format("duration: {:.6f}\r\nposition: {:.6f}\r\n",
+ static_cast<float>(appPlayer->GetTotalTime()) / 1000, position);
+ }
+ else
+ {
+ status = AIRPLAY_STATUS_METHOD_NOT_ALLOWED;
+ }
+ }
+ else
+ {
+ const char* found = strstr(queryString.c_str(), "position=");
+
+ if (found && appPlayer->HasPlayer())
+ {
+ int64_t position = (int64_t) (atof(found + strlen("position=")) * 1000.0);
+ appPlayer->SeekTime(position);
+ CLog::Log(LOGDEBUG, "AIRPLAY: got POST request {} with pos {}", uri, position);
+ }
+ }
+ }
+
+ // Sent when media playback should be stopped
+ else if (uri == "/stop")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else
+ {
+ if (IsPlaying()) //only stop player if we started him
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ CAirPlayServer::m_isPlaying--;
+ }
+ else //if we are not playing and get the stop request - we just wanna stop picture streaming
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_STOP)));
+ }
+ }
+ ClearPhotoAssetCache();
+ }
+
+ // RAW JPEG data is contained in the request body
+ else if (uri == "/photo")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (m_httpParser->getContentLength() > 0 || photoAction == "displayCached")
+ {
+ XFILE::CFile tmpFile;
+ std::string tmpFileName = "special://temp/airplayasset";
+ bool showPhoto = true;
+ bool receivePhoto = true;
+
+
+ if (photoAction == "cacheOnly")
+ showPhoto = false;
+ else if (photoAction == "displayCached")
+ {
+ receivePhoto = false;
+ if (photoCacheId.length())
+ CLog::Log(LOGDEBUG, "AIRPLAY: Trying to show from cache asset: {}", photoCacheId);
+ }
+
+ if (photoCacheId.length())
+ tmpFileName += photoCacheId;
+ else
+ tmpFileName += "airplay_photo";
+
+ if( receivePhoto && m_httpParser->getContentLength() > 3 &&
+ m_httpParser->getBody()[1] == 'P' &&
+ m_httpParser->getBody()[2] == 'N' &&
+ m_httpParser->getBody()[3] == 'G')
+ {
+ tmpFileName += ".png";
+ }
+ else
+ {
+ tmpFileName += ".jpg";
+ }
+
+ int writtenBytes=0;
+ if (receivePhoto)
+ {
+ if (tmpFile.OpenForWrite(tmpFileName, true))
+ {
+ writtenBytes = tmpFile.Write(m_httpParser->getBody(), m_httpParser->getContentLength());
+ tmpFile.Close();
+ }
+ if (photoCacheId.length())
+ CLog::Log(LOGDEBUG, "AIRPLAY: Cached asset: {}", photoCacheId);
+ }
+
+ if (showPhoto)
+ {
+ if ((writtenBytes > 0 && (unsigned int)writtenBytes == m_httpParser->getContentLength()) || !receivePhoto)
+ {
+ if (!receivePhoto && !XFILE::CFile::Exists(tmpFileName))
+ {
+ status = AIRPLAY_STATUS_PRECONDITION_FAILED; //image not found in the cache
+ if (photoCacheId.length())
+ CLog::Log(LOGWARNING, "AIRPLAY: Asset {} not found in our cache.", photoCacheId);
+ }
+ else
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_PICTURE_SHOW, -1, -1, nullptr,
+ tmpFileName);
+ }
+ else
+ {
+ CLog::Log(LOGERROR,"AirPlayServer: Error writing tmpFile.");
+ }
+ }
+ }
+ }
+
+ else if (uri == "/playback-info")
+ {
+ float position = 0.0f;
+ float duration = 0.0f;
+ float cachePosition = 0.0f;
+ bool playing = false;
+
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+
+ if (needAuth && !checkAuthorization(authorization, method, uri))
+ {
+ status = AIRPLAY_STATUS_NEED_AUTH;
+ }
+ else if (appPlayer->HasPlayer())
+ {
+ if (appPlayer->GetTotalTime())
+ {
+ position = static_cast<float>(appPlayer->GetTime()) / 1000;
+ duration = static_cast<float>(appPlayer->GetTotalTime()) / 1000;
+ playing = !appPlayer->IsPaused();
+ cachePosition = position + (duration * appPlayer->GetCachePercentage() / 100.0f);
+ }
+
+ responseBody = StringUtils::Format(PLAYBACK_INFO, duration, cachePosition, position, (playing ? 1 : 0), duration);
+ responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+
+ if (appPlayer->IsCaching())
+ {
+ CAirPlayServer::ServerInstance->AnnounceToClients(EVENT_LOADING);
+ }
+ }
+ else
+ {
+ responseBody = StringUtils::Format(PLAYBACK_INFO_NOT_READY);
+ responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+ }
+ }
+
+ else if (uri == "/server-info")
+ {
+ CLog::Log(LOGDEBUG, "AIRPLAY: got request {}", uri);
+ responseBody = StringUtils::Format(
+ SERVER_INFO, CServiceBroker::GetNetwork().GetFirstConnectedInterface()->GetMacAddress());
+ responseHeader = "Content-Type: text/x-apple-plist+xml\r\n";
+ }
+
+ else if (uri == "/slideshow-features")
+ {
+ // Ignore for now.
+ }
+
+ else if (uri == "/authorize")
+ {
+ // DRM, ignore for now.
+ }
+
+ else if (uri == "/setProperty")
+ {
+ status = AIRPLAY_STATUS_NOT_FOUND;
+ }
+
+ else if (uri == "/getProperty")
+ {
+ status = AIRPLAY_STATUS_NOT_FOUND;
+ }
+
+ else if (uri == "/fp-setup")
+ {
+ status = AIRPLAY_STATUS_PRECONDITION_FAILED;
+ }
+
+ else if (uri == "200") //response OK from the event reverse message
+ {
+ status = AIRPLAY_STATUS_NO_RESPONSE_NEEDED;
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "AIRPLAY Server: unhandled request [{}]", uri);
+ status = AIRPLAY_STATUS_NOT_IMPLEMENTED;
+ }
+
+ if (status == AIRPLAY_STATUS_NEED_AUTH)
+ {
+ ComposeAuthRequestAnswer(responseHeader, responseBody);
+ }
+
+ return status;
+}
diff --git a/xbmc/network/AirPlayServer.h b/xbmc/network/AirPlayServer.h
new file mode 100644
index 0000000..986616b
--- /dev/null
+++ b/xbmc/network/AirPlayServer.h
@@ -0,0 +1,108 @@
+/*
+ * Many concepts and protocol specification in this code are taken from
+ * the Boxee project. http://www.boxee.tv
+ *
+ * Copyright (C) 2011-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 "interfaces/IAnnouncer.h"
+#include "network/Network.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+#include "utils/HttpParser.h"
+
+#include <map>
+#include <vector>
+
+#include <sys/socket.h>
+
+class CVariant;
+
+#define AIRPLAY_SERVER_VERSION_STR "101.28"
+
+class CAirPlayServer : public CThread, public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ // IAnnouncer IF
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ //AirPlayServer impl.
+ static bool StartServer(int port, bool nonlocal);
+ static void StopServer(bool bWait);
+ static bool IsRunning();
+ static bool SetCredentials(bool usePassword, const std::string& password);
+ static bool IsPlaying(){ return m_isPlaying > 0;}
+ static void backupVolume();
+ static void restoreVolume();
+ static int m_isPlaying;
+
+protected:
+ void Process() override;
+
+private:
+ CAirPlayServer(int port, bool nonlocal);
+ ~CAirPlayServer() override;
+ bool SetInternalCredentials(bool usePassword, const std::string& password);
+ bool Initialize();
+ void Deinitialize();
+ void AnnounceToClients(int state);
+
+ class CTCPClient
+ {
+ public:
+ CTCPClient();
+ ~CTCPClient();
+ //Copying a CCriticalSection is not allowed, so copy everything but that
+ //when adding a member variable, make sure to copy it in CTCPClient::Copy
+ CTCPClient(const CTCPClient& client);
+ CTCPClient& operator=(const CTCPClient& client);
+ void PushBuffer(CAirPlayServer *host, const char *buffer,
+ int length, std::string &sessionId,
+ std::map<std::string, int> &reverseSockets);
+ void ComposeReverseEvent(std::string& reverseHeader, std::string& reverseBody, int state);
+
+ void Disconnect();
+
+ int m_socket;
+ struct sockaddr_storage m_cliaddr;
+ socklen_t m_addrlen;
+ CCriticalSection m_critSection;
+ int m_sessionCounter;
+ std::string m_sessionId;
+
+ private:
+ int ProcessRequest( std::string& responseHeader,
+ std::string& response);
+
+ void ComposeAuthRequestAnswer(std::string& responseHeader, std::string& responseBody);
+ bool checkAuthorization(const std::string& authStr, const std::string& method, const std::string& uri);
+ void Copy(const CTCPClient& client);
+
+ HttpParser* m_httpParser;
+ bool m_bAuthenticated;
+ int m_lastEvent;
+ std::string m_authNonce;
+ };
+
+ CCriticalSection m_connectionLock;
+ std::vector<CTCPClient> m_connections;
+ std::map<std::string, int> m_reverseSockets;
+ std::vector<SOCKET> m_ServerSockets;
+ int m_port;
+ bool m_nonlocal;
+ bool m_usePassword;
+ std::string m_password;
+ int m_origVolume;
+
+ static CCriticalSection ServerInstanceLock;
+ static CAirPlayServer *ServerInstance;
+};
diff --git a/xbmc/network/AirTunesServer.cpp b/xbmc/network/AirTunesServer.cpp
new file mode 100644
index 0000000..0add358
--- /dev/null
+++ b/xbmc/network/AirTunesServer.cpp
@@ -0,0 +1,753 @@
+/*
+ * Many concepts and protocol specification in this code are taken
+ * from Shairport, by James Laird.
+ *
+ * Copyright (C) 2011-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 "AirTunesServer.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "application/ApplicationActionListeners.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "cores/VideoPlayer/DVDDemuxers/DVDDemuxBXA.h"
+#include "filesystem/File.h"
+#include "filesystem/PipeFile.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "music/tags/MusicInfoTag.h"
+#include "network/Network.h"
+#include "network/Zeroconf.h"
+#include "network/ZeroconfBrowser.h"
+#include "network/dacp/dacp.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/EndianSwap.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <cstring>
+#include <map>
+#include <mutex>
+#include <string>
+#include <utility>
+
+#if !defined(TARGET_WINDOWS)
+#pragma GCC diagnostic ignored "-Wwrite-strings"
+#endif
+
+#ifdef HAS_AIRPLAY
+#include "network/AirPlayServer.h"
+#endif
+
+#define TMP_COVERART_PATH_JPG "special://temp/airtunes_album_thumb.jpg"
+#define TMP_COVERART_PATH_PNG "special://temp/airtunes_album_thumb.png"
+#define ZEROCONF_DACP_SERVICE "_dacp._tcp"
+
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+CAirTunesServer *CAirTunesServer::ServerInstance = NULL;
+std::string CAirTunesServer::m_macAddress;
+std::string CAirTunesServer::m_metadata[3];
+CCriticalSection CAirTunesServer::m_metadataLock;
+bool CAirTunesServer::m_streamStarted = false;
+CCriticalSection CAirTunesServer::m_dacpLock;
+CDACP *CAirTunesServer::m_pDACP = NULL;
+std::string CAirTunesServer::m_dacp_id;
+std::string CAirTunesServer::m_active_remote_header;
+CCriticalSection CAirTunesServer::m_actionQueueLock;
+std::list<CAction> CAirTunesServer::m_actionQueue;
+CEvent CAirTunesServer::m_processActions;
+int CAirTunesServer::m_sampleRate = 44100;
+
+unsigned int CAirTunesServer::m_cachedStartTime = 0;
+unsigned int CAirTunesServer::m_cachedEndTime = 0;
+unsigned int CAirTunesServer::m_cachedCurrentTime = 0;
+
+
+//parse daap metadata - thx to project MythTV
+std::map<std::string, std::string> decodeDMAP(const char *buffer, unsigned int size)
+{
+ std::map<std::string, std::string> result;
+ unsigned int offset = 8;
+ while (offset < size)
+ {
+ std::string tag;
+ tag.append(buffer + offset, 4);
+ offset += 4;
+ uint32_t length = Endian_SwapBE32(*(const uint32_t *)(buffer + offset));
+ offset += sizeof(uint32_t);
+ std::string content;
+ content.append(buffer + offset, length);//possible fixme - utf8?
+ offset += length;
+ result[tag] = content;
+ }
+ return result;
+}
+
+void CAirTunesServer::ResetMetadata()
+{
+ std::unique_lock<CCriticalSection> lock(m_metadataLock);
+
+ XFILE::CFile::Delete(TMP_COVERART_PATH_JPG);
+ XFILE::CFile::Delete(TMP_COVERART_PATH_PNG);
+ RefreshCoverArt();
+
+ m_metadata[0] = "";
+ m_metadata[1] = "AirPlay";
+ m_metadata[2] = "";
+ RefreshMetadata();
+}
+
+void CAirTunesServer::RefreshMetadata()
+{
+ std::unique_lock<CCriticalSection> lock(m_metadataLock);
+ MUSIC_INFO::CMusicInfoTag tag;
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ if (infoMgr.GetCurrentSongTag())
+ tag = *infoMgr.GetCurrentSongTag();
+ if (m_metadata[0].length())
+ tag.SetAlbum(m_metadata[0]);//album
+ if (m_metadata[1].length())
+ tag.SetTitle(m_metadata[1]);//title
+ if (m_metadata[2].length())
+ tag.SetArtist(m_metadata[2]);//artist
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_CURRENT_ITEM, 1, -1,
+ static_cast<void*>(new CFileItem(tag)));
+}
+
+void CAirTunesServer::RefreshCoverArt(const char *outputFilename/* = NULL*/)
+{
+ static std::string coverArtFile = TMP_COVERART_PATH_JPG;
+
+ if (outputFilename != NULL)
+ coverArtFile = std::string(outputFilename);
+
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ std::unique_lock<CCriticalSection> lock(m_metadataLock);
+ //reset to empty before setting the new one
+ //else it won't get refreshed because the name didn't change
+ infoMgr.SetCurrentAlbumThumb("");
+ //update the ui
+ infoMgr.SetCurrentAlbumThumb(coverArtFile);
+ //update the ui
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_REFRESH_THUMBS);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CAirTunesServer::SetMetadataFromBuffer(const char *buffer, unsigned int size)
+{
+
+ std::map<std::string, std::string> metadata = decodeDMAP(buffer, size);
+ std::unique_lock<CCriticalSection> lock(m_metadataLock);
+
+ if(metadata["asal"].length())
+ m_metadata[0] = metadata["asal"];//album
+ if(metadata["minm"].length())
+ m_metadata[1] = metadata["minm"];//title
+ if(metadata["asar"].length())
+ m_metadata[2] = metadata["asar"];//artist
+
+ RefreshMetadata();
+}
+
+void CAirTunesServer::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if ((flag & ANNOUNCEMENT::Player) &&
+ sender == ANNOUNCEMENT::CAnnouncementManager::ANNOUNCEMENT_SENDER)
+ {
+ if ((message == "OnPlay" || message == "OnResume") && m_streamStarted)
+ {
+ RefreshMetadata();
+ RefreshCoverArt();
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ if (m_pDACP)
+ m_pDACP->Play();
+ }
+
+ if (message == "OnStop" && m_streamStarted)
+ {
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ if (m_pDACP)
+ m_pDACP->Stop();
+ }
+
+ if (message == "OnPause" && m_streamStarted)
+ {
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ if (m_pDACP)
+ m_pDACP->Pause();
+ }
+ }
+}
+
+void CAirTunesServer::EnableActionProcessing(bool enable)
+{
+ ServerInstance->RegisterActionListener(enable);
+}
+
+bool CAirTunesServer::OnAction(const CAction &action)
+{
+ switch(action.GetID())
+ {
+ case ACTION_NEXT_ITEM:
+ case ACTION_PREV_ITEM:
+ case ACTION_VOLUME_UP:
+ case ACTION_VOLUME_DOWN:
+ case ACTION_MUTE:
+ {
+ std::unique_lock<CCriticalSection> lock(m_actionQueueLock);
+ m_actionQueue.push_back(action);
+ m_processActions.Set();
+ }
+ }
+ return false;
+}
+
+void CAirTunesServer::Process()
+{
+ m_bStop = false;
+ while(!m_bStop)
+ {
+ if (m_streamStarted)
+ SetupRemoteControl();// check for remote controls
+
+ m_processActions.Wait(1000ms); // timeout for being able to stop
+ std::list<CAction> currentActions;
+ {
+ std::unique_lock<CCriticalSection> lock(m_actionQueueLock); // copy and clear the source queue
+ currentActions.insert(currentActions.begin(), m_actionQueue.begin(), m_actionQueue.end());
+ m_actionQueue.clear();
+ }
+
+ for (const auto& currentAction : currentActions)
+ {
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ if (m_pDACP)
+ {
+ switch(currentAction.GetID())
+ {
+ case ACTION_NEXT_ITEM:
+ m_pDACP->NextItem();
+ break;
+ case ACTION_PREV_ITEM:
+ m_pDACP->PrevItem();
+ break;
+ case ACTION_VOLUME_UP:
+ m_pDACP->VolumeUp();
+ break;
+ case ACTION_VOLUME_DOWN:
+ m_pDACP->VolumeDown();
+ break;
+ case ACTION_MUTE:
+ m_pDACP->ToggleMute();
+ break;
+ }
+ }
+ }
+ }
+}
+
+bool IsJPEG(const char *buffer, unsigned int size)
+{
+ bool ret = false;
+ if (size < 2)
+ return false;
+
+ //JPEG image files begin with FF D8 and end with FF D9.
+ // check for FF D8 big + little endian on start
+ if ((buffer[0] == (char)0xd8 && buffer[1] == (char)0xff) ||
+ (buffer[1] == (char)0xd8 && buffer[0] == (char)0xff))
+ ret = true;
+
+ if (ret)
+ {
+ ret = false;
+ //check on FF D9 big + little endian on end
+ if ((buffer[size - 2] == (char)0xd9 && buffer[size - 1] == (char)0xff) ||
+ (buffer[size - 1] == (char)0xd9 && buffer[size - 2] == (char)0xff))
+ ret = true;
+ }
+
+ return ret;
+}
+
+void CAirTunesServer::SetCoverArtFromBuffer(const char *buffer, unsigned int size)
+{
+ XFILE::CFile tmpFile;
+ std::string tmpFilename = TMP_COVERART_PATH_PNG;
+
+ if(!size)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_metadataLock);
+
+ if (IsJPEG(buffer, size))
+ tmpFilename = TMP_COVERART_PATH_JPG;
+
+ if (tmpFile.OpenForWrite(tmpFilename, true))
+ {
+ int writtenBytes=0;
+ writtenBytes = tmpFile.Write(buffer, size);
+ tmpFile.Close();
+
+ if (writtenBytes > 0)
+ RefreshCoverArt(tmpFilename.c_str());
+ }
+}
+
+void CAirTunesServer::FreeDACPRemote()
+{
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ if (m_pDACP)
+ delete m_pDACP;
+ m_pDACP = NULL;
+}
+
+#define RSA_KEY " \
+-----BEGIN RSA PRIVATE KEY-----\
+MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\
+wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U\
+wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf\
+/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/\
+UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW\
+BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa\
+LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5\
+NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm\
+lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz\
+aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu\
+a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM\
+oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z\
+oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+\
+k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL\
+AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA\
+cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf\
+54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov\
+17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc\
+1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI\
+LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ\
+2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=\
+-----END RSA PRIVATE KEY-----"
+
+void CAirTunesServer::AudioOutputFunctions::audio_set_metadata(void *cls, void *session, const void *buffer, int buflen)
+{
+ CAirTunesServer::SetMetadataFromBuffer((const char *)buffer, buflen);
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_set_coverart(void *cls, void *session, const void *buffer, int buflen)
+{
+ CAirTunesServer::SetCoverArtFromBuffer((const char *)buffer, buflen);
+}
+
+char session[]="Kodi-AirTunes";
+
+void* CAirTunesServer::AudioOutputFunctions::audio_init(void *cls, int bits, int channels, int samplerate)
+{
+ XFILE::CPipeFile *pipe=(XFILE::CPipeFile *)cls;
+ const CURL pathToUrl(XFILE::PipesManager::GetInstance().GetUniquePipeName());
+ pipe->OpenForWrite(pathToUrl);
+ pipe->SetOpenThreshold(300);
+
+ Demux_BXA_FmtHeader header;
+ std::memcpy(header.fourcc, "BXA ", 4);
+ header.type = BXA_PACKET_TYPE_FMT_DEMUX;
+ header.bitsPerSample = bits;
+ header.channels = channels;
+ header.sampleRate = samplerate;
+ header.durationMs = 0;
+
+ if (pipe->Write(&header, sizeof(header)) == 0)
+ return 0;
+
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+
+ CFileItem *item = new CFileItem();
+ item->SetPath(pipe->GetName());
+ item->SetMimeType("audio/x-xbmc-pcm");
+ m_streamStarted = true;
+ m_sampleRate = samplerate;
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0, static_cast<void*>(item));
+
+ // Not all airplay streams will provide metadata (e.g. if using mirroring,
+ // no metadata will be sent). If there *is* metadata, it will be received
+ // in a later call to audio_set_metadata/audio_set_coverart.
+ ResetMetadata();
+
+ // browse for dacp services protocol which gives us the remote control service
+ CZeroconfBrowser::GetInstance()->Start();
+ CZeroconfBrowser::GetInstance()->AddServiceType(ZEROCONF_DACP_SERVICE);
+ CAirTunesServer::EnableActionProcessing(true);
+
+ return session;//session
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_remote_control_id(void *cls, const char *dacp_id, const char *active_remote_header)
+{
+ if (dacp_id && active_remote_header)
+ {
+ m_dacp_id = dacp_id;
+ m_active_remote_header = active_remote_header;
+ }
+}
+
+void CAirTunesServer::InformPlayerAboutPlayTimes()
+{
+ if (m_cachedEndTime > 0)
+ {
+ unsigned int duration = m_cachedEndTime - m_cachedStartTime;
+ unsigned int position = m_cachedCurrentTime - m_cachedStartTime;
+ duration /= m_sampleRate;
+ position /= m_sampleRate;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ {
+ appPlayer->SetTime(position * 1000);
+ appPlayer->SetTotalTime(duration * 1000);
+
+ // reset play times now that we have informed the player
+ m_cachedEndTime = 0;
+ m_cachedCurrentTime = 0;
+ m_cachedStartTime = 0;
+
+ }
+ }
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_set_progress(void *cls, void *session, unsigned int start, unsigned int curr, unsigned int end)
+{
+ m_cachedStartTime = start;
+ m_cachedCurrentTime = curr;
+ m_cachedEndTime = end;
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ {
+ // player is there - directly inform him about play times
+ InformPlayerAboutPlayTimes();
+ }
+}
+
+void CAirTunesServer::SetupRemoteControl()
+{
+ // check if we found the remote control service via zeroconf already or
+ // if no valid id and headers was received yet
+ if (m_dacp_id.empty() || m_active_remote_header.empty() || m_pDACP != NULL)
+ return;
+
+ // check for the service matching m_dacp_id
+ std::vector<CZeroconfBrowser::ZeroconfService> services = CZeroconfBrowser::GetInstance()->GetFoundServices();
+ for (auto service : services )
+ {
+ if (StringUtils::EqualsNoCase(service.GetType(), std::string(ZEROCONF_DACP_SERVICE) + "."))
+ {
+#define DACP_NAME_PREFIX "iTunes_Ctrl_"
+ // name has the form "iTunes_Ctrl_56B29BB6CB904862"
+ // were we are interested in the 56B29BB6CB904862 identifier
+ if (StringUtils::StartsWithNoCase(service.GetName(), DACP_NAME_PREFIX))
+ {
+ std::vector<std::string> tokens = StringUtils::Split(service.GetName(), DACP_NAME_PREFIX);
+ // if we found the service matching the given identifier
+ if (tokens.size() > 1 && tokens[1] == m_dacp_id)
+ {
+ // resolve the service and save it
+ CZeroconfBrowser::GetInstance()->ResolveService(service);
+ std::unique_lock<CCriticalSection> lock(m_dacpLock);
+ // recheck with lock hold
+ if (m_pDACP == NULL)
+ {
+ // we can control the client with this object now
+ m_pDACP = new CDACP(m_active_remote_header, service.GetIP(), service.GetPort());
+ }
+ break;
+ }
+ }
+ }
+ }
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_set_volume(void *cls, void *session, float volume)
+{
+ //volume from -30 - 0 - -144 means mute
+ float volPercent = volume < -30.0f ? 0 : 1 - volume/-30;
+#ifdef HAS_AIRPLAY
+ CAirPlayServer::backupVolume();
+#endif
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL))
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume(volPercent, false); //non-percent volume 0.0-1.0
+ }
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_process(void *cls, void *session, const void *buffer, int buflen)
+{
+ XFILE::CPipeFile *pipe=(XFILE::CPipeFile *)cls;
+ pipe->Write(buffer, buflen);
+
+ // in case there are some play times cached that are not yet sent to the player - do it here
+ InformPlayerAboutPlayTimes();
+}
+
+void CAirTunesServer::AudioOutputFunctions::audio_destroy(void *cls, void *session)
+{
+ XFILE::CPipeFile *pipe=(XFILE::CPipeFile *)cls;
+ pipe->SetEof();
+ pipe->Close();
+
+ CAirTunesServer::FreeDACPRemote();
+ m_dacp_id.clear();
+ m_active_remote_header.clear();
+
+ //fix airplay video for ios5 devices
+ //on ios5 when airplaying video
+ //the client first opens an airtunes stream
+ //while the movie is loading
+ //in that case we don't want to stop the player here
+ //because this would stop the airplaying video
+#ifdef HAS_AIRPLAY
+ if (!CAirPlayServer::IsPlaying())
+#endif
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ CLog::Log(LOGDEBUG, "AIRTUNES: AirPlay not running - stopping player");
+ }
+
+ m_streamStarted = false;
+
+ // no need to browse for dacp services while we don't receive
+ // any airtunes streams...
+ CZeroconfBrowser::GetInstance()->RemoveServiceType(ZEROCONF_DACP_SERVICE);
+ CZeroconfBrowser::GetInstance()->Stop();
+ CAirTunesServer::EnableActionProcessing(false);
+}
+
+void shairplay_log(void *cls, int level, const char *msg)
+{
+ int xbmcLevel = LOGINFO;
+ if (!CServiceBroker::GetLogging().CanLogComponent(LOGAIRTUNES))
+ return;
+
+ switch(level)
+ {
+ case RAOP_LOG_EMERG: // system is unusable
+ case RAOP_LOG_ALERT: // action must be taken immediately
+ case RAOP_LOG_CRIT: // critical conditions
+ xbmcLevel = LOGFATAL;
+ break;
+ case RAOP_LOG_ERR: // error conditions
+ xbmcLevel = LOGERROR;
+ break;
+ case RAOP_LOG_WARNING: // warning conditions
+ xbmcLevel = LOGWARNING;
+ break;
+ case RAOP_LOG_NOTICE: // normal but significant condition
+ case RAOP_LOG_INFO: // informational
+ xbmcLevel = LOGINFO;
+ break;
+ case RAOP_LOG_DEBUG: // debug-level messages
+ xbmcLevel = LOGDEBUG;
+ break;
+ default:
+ break;
+ }
+ CLog::Log(xbmcLevel, "AIRTUNES: {}", msg);
+}
+
+bool CAirTunesServer::StartServer(int port, bool nonlocal, bool usePassword, const std::string &password/*=""*/)
+{
+ bool success = false;
+ std::string pw = password;
+ CNetworkInterface *net = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+ StopServer(true);
+
+ if (net)
+ {
+ m_macAddress = net->GetMacAddress();
+ StringUtils::Replace(m_macAddress, ":","");
+ while (m_macAddress.size() < 12)
+ {
+ m_macAddress = '0' + m_macAddress;
+ }
+ }
+ else
+ {
+ m_macAddress = "000102030405";
+ }
+
+ if (!usePassword)
+ {
+ pw.clear();
+ }
+
+ ServerInstance = new CAirTunesServer(port, nonlocal);
+ if (ServerInstance->Initialize(pw))
+ {
+ success = true;
+ std::string appName = StringUtils::Format("{}@{}", m_macAddress, CSysInfo::GetDeviceName());
+
+ std::vector<std::pair<std::string, std::string> > txt;
+ txt.emplace_back("txtvers", "1");
+ txt.emplace_back("cn", "0,1");
+ txt.emplace_back("ch", "2");
+ txt.emplace_back("ek", "1");
+ txt.emplace_back("et", "0,1");
+ txt.emplace_back("sv", "false");
+ txt.emplace_back("tp", "UDP");
+ txt.emplace_back("sm", "false");
+ txt.emplace_back("ss", "16");
+ txt.emplace_back("sr", "44100");
+ txt.emplace_back("pw", usePassword ? "true" : "false");
+ txt.emplace_back("vn", "3");
+ txt.emplace_back("da", "true");
+ txt.emplace_back("md", "0,1,2");
+ txt.emplace_back("am", "Kodi,1");
+ txt.emplace_back("vs", "130.14");
+
+ CZeroconf::GetInstance()->PublishService("servers.airtunes", "_raop._tcp", appName, port, txt);
+ }
+
+ return success;
+}
+
+void CAirTunesServer::StopServer(bool bWait)
+{
+ if (ServerInstance)
+ {
+ ServerInstance->Deinitialize();
+ if (bWait)
+ {
+ delete ServerInstance;
+ ServerInstance = NULL;
+ }
+
+ CZeroconf::GetInstance()->RemoveService("servers.airtunes");
+ }
+}
+
+bool CAirTunesServer::IsRunning()
+{
+ if (ServerInstance == NULL)
+ return false;
+
+ return ServerInstance->IsRAOPRunningInternal();
+}
+
+bool CAirTunesServer::IsRAOPRunningInternal()
+{
+ if (m_pRaop)
+ {
+ return raop_is_running(m_pRaop) != 0;
+ }
+
+ return false;
+}
+
+CAirTunesServer::CAirTunesServer(int port, bool nonlocal)
+ : CThread("AirTunesActionThread")
+{
+ m_port = port;
+ m_pPipe = new XFILE::CPipeFile;
+}
+
+CAirTunesServer::~CAirTunesServer()
+{
+ delete m_pPipe;
+}
+
+void CAirTunesServer::RegisterActionListener(bool doRegister)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appListener = components.GetComponent<CApplicationActionListeners>();
+
+ if (doRegister)
+ {
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+ appListener->RegisterActionListener(this);
+ ServerInstance->Create();
+ }
+ else
+ {
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+ appListener->UnregisterActionListener(this);
+ ServerInstance->StopThread(true);
+ }
+}
+
+bool CAirTunesServer::Initialize(const std::string &password)
+{
+ bool ret = false;
+
+ Deinitialize();
+
+ raop_callbacks_t ao = {};
+ ao.cls = m_pPipe;
+ ao.audio_init = AudioOutputFunctions::audio_init;
+ ao.audio_set_volume = AudioOutputFunctions::audio_set_volume;
+ ao.audio_set_metadata = AudioOutputFunctions::audio_set_metadata;
+ ao.audio_set_coverart = AudioOutputFunctions::audio_set_coverart;
+ ao.audio_process = AudioOutputFunctions::audio_process;
+ ao.audio_destroy = AudioOutputFunctions::audio_destroy;
+ ao.audio_remote_control_id = AudioOutputFunctions::audio_remote_control_id;
+ ao.audio_set_progress = AudioOutputFunctions::audio_set_progress;
+ m_pRaop = raop_init(1, &ao, RSA_KEY, nullptr); //1 - we handle one client at a time max
+
+ if (m_pRaop)
+ {
+ char macAdr[6];
+ unsigned short port = (unsigned short)m_port;
+
+ raop_set_log_level(m_pRaop, RAOP_LOG_WARNING);
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGAIRTUNES))
+ {
+ raop_set_log_level(m_pRaop, RAOP_LOG_DEBUG);
+ }
+
+ raop_set_log_callback(m_pRaop, shairplay_log, NULL);
+
+ CNetworkInterface* net = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+
+ if (net)
+ {
+ net->GetMacAddressRaw(macAdr);
+ }
+
+ ret = raop_start(m_pRaop, &port, macAdr, 6, password.c_str()) >= 0;
+ }
+ return ret;
+}
+
+void CAirTunesServer::Deinitialize()
+{
+ RegisterActionListener(false);
+
+ if (m_pRaop)
+ {
+ raop_stop(m_pRaop);
+ raop_destroy(m_pRaop);
+ m_pRaop = nullptr;
+ }
+}
diff --git a/xbmc/network/AirTunesServer.h b/xbmc/network/AirTunesServer.h
new file mode 100644
index 0000000..a739ca7
--- /dev/null
+++ b/xbmc/network/AirTunesServer.h
@@ -0,0 +1,101 @@
+/*
+ * Many concepts and protocol specification in this code are taken from
+ * the Boxee project. http://www.boxee.tv
+ *
+ * Copyright (C) 2011-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 "filesystem/PipeFile.h"
+#include "interfaces/IActionListener.h"
+#include "interfaces/IAnnouncer.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <list>
+#include <string>
+#include <vector>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <shairplay/raop.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+class CDACP;
+class CVariant;
+
+class CAirTunesServer : public ANNOUNCEMENT::IAnnouncer, public IActionListener, public CThread
+{
+public:
+ // ANNOUNCEMENT::IAnnouncer
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ void RegisterActionListener(bool doRegister);
+ static void EnableActionProcessing(bool enable);
+ // IACtionListener
+ bool OnAction(const CAction &action) override;
+
+ //CThread
+ void Process() override;
+
+ static bool StartServer(int port, bool nonlocal, bool usePassword, const std::string &password="");
+ static void StopServer(bool bWait);
+ static bool IsRunning();
+ bool IsRAOPRunningInternal();
+ static void SetMetadataFromBuffer(const char *buffer, unsigned int size);
+ static void SetCoverArtFromBuffer(const char *buffer, unsigned int size);
+ static void SetupRemoteControl();
+ static void FreeDACPRemote();
+
+private:
+ CAirTunesServer(int port, bool nonlocal);
+ ~CAirTunesServer() override;
+ bool Initialize(const std::string &password);
+ void Deinitialize();
+ static void RefreshCoverArt(const char *outputFilename = NULL);
+ static void RefreshMetadata();
+ static void ResetMetadata();
+ static void InformPlayerAboutPlayTimes();
+
+ int m_port;
+ raop_t* m_pRaop = nullptr;
+ XFILE::CPipeFile *m_pPipe;
+ static CAirTunesServer *ServerInstance;
+ static std::string m_macAddress;
+ static CCriticalSection m_metadataLock;
+ static std::string m_metadata[3];//0 - album, 1 - title, 2 - artist
+ static bool m_streamStarted;
+ static CCriticalSection m_dacpLock;
+ static CDACP *m_pDACP;
+ static std::string m_dacp_id;
+ static std::string m_active_remote_header;
+ static CCriticalSection m_actionQueueLock;
+ static std::list<CAction> m_actionQueue;
+ static CEvent m_processActions;
+ static int m_sampleRate;
+ static unsigned int m_cachedStartTime;
+ static unsigned int m_cachedEndTime;
+ static unsigned int m_cachedCurrentTime;
+
+ class AudioOutputFunctions
+ {
+ public:
+ static void* audio_init(void *cls, int bits, int channels, int samplerate);
+ static void audio_set_volume(void *cls, void *session, float volume);
+ static void audio_set_metadata(void *cls, void *session, const void *buffer, int buflen);
+ static void audio_set_coverart(void *cls, void *session, const void *buffer, int buflen);
+ static void audio_process(void *cls, void *session, const void *buffer, int buflen);
+ static void audio_destroy(void *cls, void *session);
+ static void audio_remote_control_id(void *cls, const char *identifier, const char *active_remote_header);
+ static void audio_set_progress(void *cls, void *session, unsigned int start, unsigned int curr, unsigned int end);
+ };
+};
diff --git a/xbmc/network/CMakeLists.txt b/xbmc/network/CMakeLists.txt
new file mode 100644
index 0000000..400e151
--- /dev/null
+++ b/xbmc/network/CMakeLists.txt
@@ -0,0 +1,60 @@
+set(SOURCES DNSNameCache.cpp
+ EventClient.cpp
+ EventPacket.cpp
+ EventServer.cpp
+ GUIDialogNetworkSetup.cpp
+ Network.cpp
+ NetworkServices.cpp
+ Socket.cpp
+ TCPServer.cpp
+ UdpClient.cpp
+ WakeOnAccess.cpp
+ ZeroconfBrowser.cpp
+ Zeroconf.cpp)
+
+set(HEADERS DNSNameCache.h
+ EventClient.h
+ EventPacket.h
+ EventServer.h
+ GUIDialogNetworkSetup.h
+ Network.h
+ NetworkServices.h
+ Socket.h
+ TCPServer.h
+ UdpClient.h
+ WakeOnAccess.h
+ Zeroconf.h
+ ZeroconfBrowser.h)
+
+if(ENABLE_OPTICAL)
+ list(APPEND SOURCES cddb.cpp)
+ list(APPEND HEADERS cddb.h)
+endif()
+
+if(PLIST_FOUND)
+ list(APPEND SOURCES AirPlayServer.cpp)
+ list(APPEND HEADERS AirPlayServer.h)
+endif()
+
+if(SHAIRPLAY_FOUND)
+ list(APPEND SOURCES AirTunesServer.cpp)
+ list(APPEND HEADERS AirTunesServer.h)
+endif()
+
+if(SMBCLIENT_FOUND)
+ list(APPEND HEADERS IWSDiscovery.h)
+endif()
+
+if(MICROHTTPD_FOUND)
+ list(APPEND SOURCES WebServer.cpp)
+ list(APPEND HEADERS WebServer.h)
+endif()
+
+core_add_library(network)
+if(BLUETOOTH_FOUND)
+ target_compile_definitions(${CORE_LIBRARY} PRIVATE -DHAVE_LIBBLUETOOTH=1)
+endif()
+
+if(ENABLE_STATIC_LIBS AND ENABLE_UPNP)
+ target_link_libraries(${CORE_LIBRARY} PRIVATE upnp)
+endif()
diff --git a/xbmc/network/DNSNameCache.cpp b/xbmc/network/DNSNameCache.cpp
new file mode 100644
index 0000000..af2ac1a
--- /dev/null
+++ b/xbmc/network/DNSNameCache.cpp
@@ -0,0 +1,114 @@
+/*
+ * 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 "DNSNameCache.h"
+
+#include "threads/CriticalSection.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#if !defined(TARGET_WINDOWS) && defined(HAS_FILESYSTEM_SMB)
+#include "ServiceBroker.h"
+
+#include "platform/posix/filesystem/SMBWSDiscovery.h"
+#endif
+
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <netinet/in.h>
+
+CDNSNameCache g_DNSCache;
+
+CCriticalSection CDNSNameCache::m_critical;
+
+CDNSNameCache::CDNSNameCache(void) = default;
+
+CDNSNameCache::~CDNSNameCache(void) = default;
+
+bool CDNSNameCache::Lookup(const std::string& strHostName, std::string& strIpAddress)
+{
+ if (strHostName.empty() && strIpAddress.empty())
+ return false;
+
+ // first see if this is already an ip address
+ unsigned long address = inet_addr(strHostName.c_str());
+ strIpAddress.clear();
+
+ if (address != INADDR_NONE)
+ {
+ strIpAddress = StringUtils::Format("{}.{}.{}.{}", (address & 0xFF), (address & 0xFF00) >> 8,
+ (address & 0xFF0000) >> 16, (address & 0xFF000000) >> 24);
+ return true;
+ }
+
+ // check if there's a custom entry or if it's already cached
+ if(g_DNSCache.GetCached(strHostName, strIpAddress))
+ return true;
+
+ // perform dns lookup
+ struct hostent *host = gethostbyname(strHostName.c_str());
+ if (host && host->h_addr_list[0])
+ {
+ strIpAddress = StringUtils::Format("{}.{}.{}.{}", (unsigned char)host->h_addr_list[0][0],
+ (unsigned char)host->h_addr_list[0][1],
+ (unsigned char)host->h_addr_list[0][2],
+ (unsigned char)host->h_addr_list[0][3]);
+ g_DNSCache.Add(strHostName, strIpAddress);
+ return true;
+ }
+
+ CLog::Log(LOGERROR, "Unable to lookup host: '{}'", strHostName);
+ return false;
+}
+
+bool CDNSNameCache::GetCached(const std::string& strHostName, std::string& strIpAddress)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ // loop through all DNSname entries and see if strHostName is cached
+ for (const auto& DNSname : g_DNSCache.m_vecDNSNames)
+ {
+ if (DNSname.m_strHostName == strHostName)
+ {
+ strIpAddress = DNSname.m_strIpAddress;
+ return true;
+ }
+ }
+ }
+
+#if !defined(TARGET_WINDOWS) && defined(HAS_FILESYSTEM_SMB)
+ if (WSDiscovery::CWSDiscoveryPosix::IsInitialized())
+ {
+ WSDiscovery::CWSDiscoveryPosix& WSInstance =
+ dynamic_cast<WSDiscovery::CWSDiscoveryPosix&>(CServiceBroker::GetWSDiscovery());
+ if (WSInstance.GetCached(strHostName, strIpAddress))
+ return true;
+ }
+ else
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CDNSNameCache::GetCached: CWSDiscoveryPosix not initialized");
+#endif
+
+ // not cached
+ return false;
+}
+
+void CDNSNameCache::Add(const std::string& strHostName, const std::string& strIpAddress)
+{
+ CDNSName dnsName;
+
+ dnsName.m_strHostName = strHostName;
+ dnsName.m_strIpAddress = strIpAddress;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ g_DNSCache.m_vecDNSNames.push_back(dnsName);
+}
+
diff --git a/xbmc/network/DNSNameCache.h b/xbmc/network/DNSNameCache.h
new file mode 100644
index 0000000..25c2943
--- /dev/null
+++ b/xbmc/network/DNSNameCache.h
@@ -0,0 +1,34 @@
+/*
+ * 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 <string>
+#include <vector>
+
+class CCriticalSection;
+
+class CDNSNameCache
+{
+public:
+ class CDNSName
+ {
+ public:
+ std::string m_strHostName;
+ std::string m_strIpAddress;
+ };
+ CDNSNameCache(void);
+ virtual ~CDNSNameCache(void);
+ static void Add(const std::string& strHostName, const std::string& strIpAddress);
+ static bool GetCached(const std::string& strHostName, std::string& strIpAddress);
+ static bool Lookup(const std::string& strHostName, std::string& strIpAddress);
+
+protected:
+ static CCriticalSection m_critical;
+ std::vector<CDNSName> m_vecDNSNames;
+};
diff --git a/xbmc/network/EventClient.cpp b/xbmc/network/EventClient.cpp
new file mode 100644
index 0000000..d3b67dc
--- /dev/null
+++ b/xbmc/network/EventClient.cpp
@@ -0,0 +1,788 @@
+/*
+ * 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 "EventClient.h"
+
+#include "EventPacket.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "filesystem/File.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/ButtonTranslator.h"
+#include "input/GamepadTranslator.h"
+#include "input/IRTranslator.h"
+#include "input/Key.h"
+#include "input/KeyboardTranslator.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <map>
+#include <mutex>
+#include <queue>
+
+using namespace EVENTCLIENT;
+using namespace EVENTPACKET;
+
+struct ButtonStateFinder
+{
+ explicit ButtonStateFinder(const CEventButtonState& state)
+ : m_keycode(state.m_iKeyCode)
+ , m_map(state.m_mapName)
+ , m_button(state.m_buttonName)
+ {}
+
+ bool operator()(const CEventButtonState& state)
+ {
+ return state.m_mapName == m_map
+ && state.m_iKeyCode == m_keycode
+ && state.m_buttonName == m_button;
+ }
+ private:
+ unsigned short m_keycode;
+ std::string m_map;
+ std::string m_button;
+};
+
+/************************************************************************/
+/* CEventButtonState */
+/************************************************************************/
+void CEventButtonState::Load()
+{
+ if ( m_iKeyCode == 0 )
+ {
+ if ( (m_mapName.length() > 0) && (m_buttonName.length() > 0) )
+ {
+ m_iKeyCode = CButtonTranslator::TranslateString(m_mapName, m_buttonName);
+ if (m_iKeyCode == 0)
+ {
+ Reset();
+ CLog::Log(LOGERROR, "ES: Could not map {} : {} to a key", m_mapName, m_buttonName);
+ }
+ }
+ }
+ else
+ {
+ if (m_mapName.length() > 3 &&
+ (StringUtils::StartsWith(m_mapName, "JS")) )
+ {
+ m_joystickName = m_mapName.substr(2); // <num>:joyname
+ m_iControllerNumber = (unsigned char)(*(m_joystickName.c_str()))
+ - (unsigned char)'0'; // convert <num> to int
+ m_joystickName = m_joystickName.substr(2); // extract joyname
+ }
+
+ if (m_mapName.length() > 3 &&
+ (StringUtils::StartsWith(m_mapName, "CC")) ) // custom map - CC:<controllerName>
+ {
+ m_customControllerName = m_mapName.substr(3);
+ }
+ }
+}
+
+/************************************************************************/
+/* CEventClient */
+/************************************************************************/
+bool CEventClient::AddPacket(std::unique_ptr<CEventPacket> packet)
+{
+ if (!packet)
+ return false;
+
+ ResetTimeout();
+ if ( packet->Size() > 1 )
+ {
+ //! @todo limit payload size
+ if (m_seqPackets[packet->Sequence()])
+ {
+ if(!m_bSequenceError)
+ CLog::Log(LOGWARNING,
+ "CEventClient::AddPacket - received packet with same sequence number ({}) as "
+ "previous packet from eventclient {}",
+ packet->Sequence(), m_deviceName);
+ m_bSequenceError = true;
+ m_seqPackets.erase(packet->Sequence());
+ }
+
+ unsigned int sequence = packet->Sequence();
+
+ m_seqPackets[sequence] = std::move(packet);
+ if (m_seqPackets.size() == m_seqPackets[sequence]->Size())
+ {
+ unsigned int iSeqPayloadSize = 0;
+ for (unsigned int i = 1; i <= m_seqPackets[sequence]->Size(); i++)
+ {
+ iSeqPayloadSize += m_seqPackets[i]->PayloadSize();
+ }
+
+ std::vector<uint8_t> newPayload(iSeqPayloadSize);
+ auto newPayloadIter = newPayload.begin();
+
+ unsigned int packets = m_seqPackets[sequence]->Size(); // packet can be deleted in this loop
+ for (unsigned int i = 1; i <= packets; i++)
+ {
+ newPayloadIter =
+ std::copy(m_seqPackets[i]->Payload(),
+ m_seqPackets[i]->Payload() + m_seqPackets[i]->PayloadSize(), newPayloadIter);
+
+ if (i > 1)
+ m_seqPackets.erase(i);
+ }
+ m_seqPackets[1]->SetPayload(newPayload);
+ m_readyPackets.push(std::move(m_seqPackets[1]));
+ m_seqPackets.clear();
+ }
+ }
+ else
+ {
+ m_readyPackets.push(std::move(packet));
+ }
+ return true;
+}
+
+void CEventClient::ProcessEvents()
+{
+ if (!m_readyPackets.empty())
+ {
+ while ( ! m_readyPackets.empty() )
+ {
+ ProcessPacket(m_readyPackets.front().get());
+ if ( ! m_readyPackets.empty() ) // in case the BYE packet cleared the queues
+ m_readyPackets.pop();
+ }
+ }
+}
+
+bool CEventClient::GetNextAction(CEventAction &action)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_actionQueue.empty())
+ {
+ // grab the next action in line
+ action = m_actionQueue.front();
+ m_actionQueue.pop();
+ return true;
+ }
+ else
+ {
+ // we got nothing
+ return false;
+ }
+}
+
+bool CEventClient::ProcessPacket(CEventPacket *packet)
+{
+ if (!packet)
+ return false;
+
+ bool valid = false;
+
+ switch (packet->Type())
+ {
+ case PT_HELO:
+ valid = OnPacketHELO(packet);
+ break;
+
+ case PT_BYE:
+ valid = OnPacketBYE(packet);
+ break;
+
+ case PT_BUTTON:
+ valid = OnPacketBUTTON(packet);
+ break;
+
+ case EVENTPACKET::PT_MOUSE:
+ valid = OnPacketMOUSE(packet);
+ break;
+
+ case PT_NOTIFICATION:
+ valid = OnPacketNOTIFICATION(packet);
+ break;
+
+ case PT_PING:
+ valid = true;
+ break;
+
+ case PT_LOG:
+ valid = OnPacketLOG(packet);
+ break;
+
+ case PT_ACTION:
+ valid = OnPacketACTION(packet);
+ break;
+
+ default:
+ CLog::Log(LOGDEBUG, "ES: Got Unknown Packet");
+ break;
+ }
+
+ if (valid)
+ ResetTimeout();
+
+ return valid;
+}
+
+bool CEventClient::OnPacketHELO(CEventPacket *packet)
+{
+ //! @todo check it last HELO packet was received less than 5 minutes back
+ //! if so, do not show notification of connection.
+ if (Greeted())
+ return false;
+
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+
+ // parse device name
+ if (!ParseString(payload, psize, m_deviceName))
+ return false;
+
+ CLog::Log(LOGINFO, "ES: Incoming connection from {}", m_deviceName);
+
+ // icon type
+ unsigned char ltype;
+ if (!ParseByte(payload, psize, ltype))
+ return false;
+ m_eLogoType = (LogoType)ltype;
+
+ // client's port (if any)
+ unsigned short dport;
+ if (!ParseUInt16(payload, psize, dport))
+ return false;
+ m_iRemotePort = (unsigned int)dport;
+
+ // 2 x reserved uint32 (8 bytes)
+ unsigned int reserved;
+ ParseUInt32(payload, psize, reserved);
+ ParseUInt32(payload, psize, reserved);
+
+ // image data if any
+ std::string iconfile = "special://temp/helo";
+ if (m_eLogoType != LT_NONE && psize>0)
+ {
+ switch (m_eLogoType)
+ {
+ case LT_JPEG:
+ iconfile += ".jpg";
+ break;
+
+ case LT_GIF:
+ iconfile += ".gif";
+ break;
+
+ default:
+ iconfile += ".png";
+ break;
+ }
+ XFILE::CFile file;
+ if (!file.OpenForWrite(iconfile, true) || file.Write((const void *)payload, psize) != psize)
+ {
+ CLog::Log(LOGERROR, "ES: Could not write icon file");
+ m_eLogoType = LT_NONE;
+ }
+ }
+
+ m_bGreeted = true;
+ if (m_eLogoType == LT_NONE)
+ {
+ CGUIDialogKaiToast::QueueNotification(g_localizeStrings.Get(33200), m_deviceName);
+ }
+ else
+ {
+ CGUIDialogKaiToast::QueueNotification(iconfile, g_localizeStrings.Get(33200), m_deviceName);
+ }
+ return true;
+}
+
+bool CEventClient::OnPacketBYE(CEventPacket *packet)
+{
+ if (!Greeted())
+ return false;
+
+ m_bGreeted = false;
+ FreePacketQueues();
+ m_currentButton.Reset();
+
+ return true;
+}
+
+bool CEventClient::OnPacketBUTTON(CEventPacket *packet)
+{
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+
+ std::string map, button;
+ unsigned short flags;
+ unsigned short bcode;
+ unsigned short amount;
+
+ // parse the button code
+ if (!ParseUInt16(payload, psize, bcode))
+ return false;
+
+ // parse flags
+ if (!ParseUInt16(payload, psize, flags))
+ return false;
+
+ // parse amount
+ if (!ParseUInt16(payload, psize, amount))
+ return false;
+
+ // parse the map to use
+ if (!ParseString(payload, psize, map))
+ return false;
+
+ // parse button name
+ if (flags & PTB_USE_NAME)
+ {
+ if (!ParseString(payload, psize, button))
+ return false;
+ }
+
+ unsigned int keycode;
+ if(flags & PTB_USE_NAME)
+ keycode = 0;
+ else if(flags & PTB_VKEY)
+ keycode = bcode|KEY_VKEY;
+ else if(flags & PTB_UNICODE)
+ keycode = bcode|ES_FLAG_UNICODE;
+ else
+ keycode = bcode;
+
+ float famount = 0;
+ bool active = (flags & PTB_DOWN) ? true : false;
+
+ if (flags & PTB_USE_NAME)
+ CLog::Log(LOGDEBUG, "EventClient: button name \"{}\" map \"{}\" {}", button, map,
+ active ? "pressed" : "released");
+ else
+ CLog::Log(LOGDEBUG, "EventClient: button code {} {}", bcode, active ? "pressed" : "released");
+
+ if(flags & PTB_USE_AMOUNT)
+ {
+ if(flags & PTB_AXIS)
+ famount = (float)amount/65535.0f*2.0f-1.0f;
+ else
+ famount = (float)amount/65535.0f;
+ }
+ else
+ famount = (active ? 1.0f : 0.0f);
+
+ if(flags & PTB_QUEUE)
+ {
+ /* find the last queued item of this type */
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CEventButtonState state( keycode,
+ map,
+ button,
+ famount,
+ (flags & (PTB_AXIS|PTB_AXISSINGLE)) ? true : false,
+ (flags & PTB_NO_REPEAT) ? false : true,
+ (flags & PTB_USE_AMOUNT) ? true : false );
+
+ /* correct non active events so they work with rest of code */
+ if(!active)
+ {
+ state.m_bActive = false;
+ state.m_bRepeat = false;
+ state.m_fAmount = 0.0;
+ }
+
+ std::list<CEventButtonState>::reverse_iterator it;
+ it = find_if( m_buttonQueue.rbegin() , m_buttonQueue.rend(), ButtonStateFinder(state));
+
+ if(it == m_buttonQueue.rend())
+ {
+ if(active)
+ m_buttonQueue.push_back(state);
+ }
+ else
+ {
+ if(!active && it->m_bActive)
+ {
+ /* since modifying the list invalidates the reverse iterator */
+ std::list<CEventButtonState>::iterator it2 = (++it).base();
+
+ /* if last event had an amount, we must resend without amount */
+ if (it2->m_bUseAmount && it2->m_fAmount != 0.0f)
+ {
+ m_buttonQueue.push_back(state);
+ }
+
+ /* if the last event was waiting for a repeat interval, it has executed already.*/
+ if(it2->m_bRepeat)
+ {
+ if (it2->m_iNextRepeat.time_since_epoch().count() > 0)
+ {
+ m_buttonQueue.erase(it2);
+ }
+ else
+ {
+ it2->m_bRepeat = false;
+ it2->m_bActive = false;
+ }
+ }
+
+ }
+ else if(active && !it->m_bActive)
+ {
+ m_buttonQueue.push_back(state);
+ if (!state.m_bRepeat && state.m_bAxis && state.m_fAmount != 0.0f)
+ {
+ state.m_bActive = false;
+ state.m_bRepeat = false;
+ state.m_fAmount = 0.0;
+ m_buttonQueue.push_back(state);
+ }
+ }
+ else
+ it->m_fAmount = state.m_fAmount;
+ }
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if ( flags & PTB_DOWN )
+ {
+ m_currentButton.m_iKeyCode = keycode;
+ m_currentButton.m_mapName = map;
+ m_currentButton.m_buttonName = button;
+ m_currentButton.m_fAmount = famount;
+ m_currentButton.m_bRepeat = (flags & PTB_NO_REPEAT) ? false : true;
+ m_currentButton.m_bAxis = (flags & PTB_AXIS) ? true : false;
+ m_currentButton.m_iNextRepeat = {};
+ m_currentButton.SetActive();
+ m_currentButton.Load();
+ }
+ else
+ {
+ /* when a button is released that had amount, make sure *
+ * to resend the keypress with an amount of 0 */
+ if ((flags & PTB_USE_AMOUNT) && m_currentButton.m_fAmount > 0.0f)
+ {
+ CEventButtonState state( m_currentButton.m_iKeyCode,
+ m_currentButton.m_mapName,
+ m_currentButton.m_buttonName,
+ 0.0,
+ m_currentButton.m_bAxis,
+ false,
+ true );
+
+ m_buttonQueue.push_back (state);
+ }
+ m_currentButton.Reset();
+ }
+ }
+
+ return true;
+}
+
+bool CEventClient::OnPacketMOUSE(CEventPacket *packet)
+{
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+ unsigned char flags;
+ unsigned short mx, my;
+
+ // parse flags
+ if (!ParseByte(payload, psize, flags))
+ return false;
+
+ // parse x position
+ if (!ParseUInt16(payload, psize, mx))
+ return false;
+
+ // parse x position
+ if (!ParseUInt16(payload, psize, my))
+ return false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if ( flags & PTM_ABSOLUTE )
+ {
+ m_iMouseX = mx;
+ m_iMouseY = my;
+ m_bMouseMoved = true;
+ }
+ }
+
+ return true;
+}
+
+bool CEventClient::OnPacketNOTIFICATION(CEventPacket *packet)
+{
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+ std::string title, message;
+
+ // parse caption
+ if (!ParseString(payload, psize, title))
+ return false;
+
+ // parse message
+ if (!ParseString(payload, psize, message))
+ return false;
+
+ // icon type
+ unsigned char ltype;
+ if (!ParseByte(payload, psize, ltype))
+ return false;
+ m_eLogoType = (LogoType)ltype;
+
+ // reserved uint32
+ unsigned int reserved;
+ ParseUInt32(payload, psize, reserved);
+
+ // image data if any
+ std::string iconfile = "special://temp/notification";
+ if (m_eLogoType != LT_NONE && psize>0)
+ {
+ switch (m_eLogoType)
+ {
+ case LT_JPEG:
+ iconfile += ".jpg";
+ break;
+
+ case LT_GIF:
+ iconfile += ".gif";
+ break;
+
+ default:
+ iconfile += ".png";
+ break;
+ }
+
+ XFILE::CFile file;
+ if (!file.OpenForWrite(iconfile, true) || file.Write((const void *)payload, psize) != psize)
+ {
+ CLog::Log(LOGERROR, "ES: Could not write icon file");
+ m_eLogoType = LT_NONE;
+ }
+ }
+
+ if (m_eLogoType == LT_NONE)
+ {
+ CGUIDialogKaiToast::QueueNotification(title, message);
+ }
+ else
+ {
+ CGUIDialogKaiToast::QueueNotification(iconfile, title, message);
+ }
+ return true;
+}
+
+bool CEventClient::OnPacketLOG(CEventPacket *packet)
+{
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+ std::string logmsg;
+ unsigned char ltype;
+
+ if (!ParseByte(payload, psize, ltype))
+ return false;
+ if (!ParseString(payload, psize, logmsg))
+ return false;
+
+ CLog::Log((int)ltype, "{}", logmsg);
+ return true;
+}
+
+bool CEventClient::OnPacketACTION(CEventPacket *packet)
+{
+ unsigned char *payload = (unsigned char *)packet->Payload();
+ int psize = (int)packet->PayloadSize();
+ std::string actionString;
+ unsigned char actionType;
+
+ if (!ParseByte(payload, psize, actionType))
+ return false;
+ if (!ParseString(payload, psize, actionString))
+ return false;
+
+ switch(actionType)
+ {
+ case AT_EXEC_BUILTIN:
+ case AT_BUTTON:
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_actionQueue.push(CEventAction(actionString.c_str(), actionType));
+ }
+ break;
+
+ default:
+ CLog::Log(LOGDEBUG, "ES: Failed - ActionType: {} ActionString: {}", actionType, actionString);
+ return false;
+ break;
+ }
+ return true;
+}
+
+bool CEventClient::ParseString(unsigned char* &payload, int &psize, std::string& parsedVal)
+{
+ if (psize <= 0)
+ return false;
+
+ unsigned char *pos = (unsigned char *)memchr((void*)payload, (int)'\0', psize);
+ if (!pos)
+ return false;
+
+ parsedVal = (char*)payload;
+ psize -= ((pos - payload) + 1);
+ payload = pos+1;
+ return true;
+}
+
+bool CEventClient::ParseByte(unsigned char* &payload, int &psize, unsigned char& parsedVal)
+{
+ if (psize <= 0)
+ return false;
+
+ parsedVal = *payload;
+ payload++;
+ psize--;
+ return true;
+}
+
+bool CEventClient::ParseUInt32(unsigned char* &payload, int &psize, unsigned int& parsedVal)
+{
+ if (psize < 4)
+ return false;
+
+ parsedVal = ntohl(*((unsigned int *)payload));
+ payload+=4;
+ psize-=4;
+ return true;
+}
+
+bool CEventClient::ParseUInt16(unsigned char* &payload, int &psize, unsigned short& parsedVal)
+{
+ if (psize < 2)
+ return false;
+
+ parsedVal = ntohs(*((unsigned short *)payload));
+ payload+=2;
+ psize-=2;
+ return true;
+}
+
+void CEventClient::FreePacketQueues()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ while ( ! m_readyPackets.empty() )
+ m_readyPackets.pop();
+
+ m_seqPackets.clear();
+}
+
+unsigned int CEventClient::GetButtonCode(std::string& strMapName, bool& isAxis, float& amount, bool &isJoystick)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ unsigned int bcode = 0;
+
+ if ( m_currentButton.Active() )
+ {
+ bcode = m_currentButton.KeyCode();
+ strMapName = m_currentButton.JoystickName();
+ isJoystick = true;
+ if (strMapName.length() == 0)
+ {
+ strMapName = m_currentButton.CustomControllerName();
+ isJoystick = false;
+ }
+
+ isAxis = m_currentButton.Axis();
+ amount = m_currentButton.Amount();
+
+ if ( ! m_currentButton.Repeat() )
+ m_currentButton.Reset();
+ else
+ {
+ if ( ! CheckButtonRepeat(m_currentButton.m_iNextRepeat) )
+ bcode = 0;
+ }
+ return bcode;
+ }
+
+ if(m_buttonQueue.empty())
+ return 0;
+
+
+ std::list<CEventButtonState> repeat;
+ std::list<CEventButtonState>::iterator it;
+ for(it = m_buttonQueue.begin(); bcode == 0 && it != m_buttonQueue.end(); ++it)
+ {
+ bcode = it->KeyCode();
+ strMapName = it->JoystickName();
+ isJoystick = true;
+
+ if (strMapName.length() == 0)
+ {
+ strMapName = it->CustomControllerName();
+ isJoystick = false;
+ }
+
+ isAxis = it->Axis();
+ amount = it->Amount();
+
+ if(it->Repeat())
+ {
+ /* MUST update m_iNextRepeat before resend */
+ bool skip = !it->Axis() && !CheckButtonRepeat(it->m_iNextRepeat);
+
+ repeat.push_back(*it);
+ if(skip)
+ {
+ bcode = 0;
+ continue;
+ }
+ }
+ }
+
+ m_buttonQueue.erase(m_buttonQueue.begin(), it);
+ m_buttonQueue.insert(m_buttonQueue.end(), repeat.begin(), repeat.end());
+ return bcode;
+}
+
+bool CEventClient::GetMousePos(float& x, float& y)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bMouseMoved)
+ {
+ x = (m_iMouseX / 65535.0f) * CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth();
+ y = (m_iMouseY / 65535.0f) * CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight();
+ m_bMouseMoved = false;
+ return true;
+ }
+ return false;
+}
+
+bool CEventClient::CheckButtonRepeat(std::chrono::time_point<std::chrono::steady_clock>& next)
+{
+ auto now = std::chrono::steady_clock::now();
+
+ if (next.time_since_epoch().count() == 0)
+ {
+ next = now + m_iRepeatDelay;
+ return true;
+ }
+ else if ( now > next )
+ {
+ next = now + m_iRepeatSpeed;
+ return true;
+ }
+ return false;
+}
+
+bool CEventClient::Alive() const
+{
+ // 60 seconds timeout
+ if ( (time(NULL) - m_lastPing) > 60 )
+ return false;
+ return true;
+}
diff --git a/xbmc/network/EventClient.h b/xbmc/network/EventClient.h
new file mode 100644
index 0000000..b5fe5e1
--- /dev/null
+++ b/xbmc/network/EventClient.h
@@ -0,0 +1,260 @@
+/*
+ * 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 "EventPacket.h"
+#include "ServiceBroker.h"
+#include "Socket.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <chrono>
+#include <list>
+#include <map>
+#include <queue>
+#include <utility>
+
+namespace EVENTCLIENT
+{
+
+ #define ES_FLAG_UNICODE 0x80000000 // new 16bit key flag to support real unicode over EventServer
+
+ class CEventAction
+ {
+ public:
+ CEventAction()
+ {
+ actionType = 0;
+ }
+ CEventAction(const char* action, unsigned char type):
+ actionName(action)
+ {
+ actionType = type;
+ }
+
+ std::string actionName;
+ unsigned char actionType;
+ };
+
+ class CEventButtonState
+ {
+ public:
+ CEventButtonState()
+ {
+ m_iKeyCode = 0;
+ m_fAmount = 0.0f;
+ m_bUseAmount = false;
+ m_bRepeat = false;
+ m_bActive = false;
+ m_bAxis = false;
+ m_iControllerNumber = 0;
+ m_iNextRepeat = {};
+ }
+
+ CEventButtonState(unsigned int iKeyCode,
+ std::string mapName,
+ std::string buttonName,
+ float fAmount,
+ bool isAxis,
+ bool bRepeat,
+ bool bUseAmount)
+ : m_buttonName(std::move(buttonName)), m_mapName(std::move(mapName))
+ {
+ m_iKeyCode = iKeyCode;
+ m_fAmount = fAmount;
+ m_bUseAmount = bUseAmount;
+ m_bRepeat = bRepeat;
+ m_bActive = true;
+ m_bAxis = isAxis;
+ m_iControllerNumber = 0;
+ m_iNextRepeat = {};
+ Load();
+ }
+
+ void Reset() { m_bActive = false; }
+ void SetActive() { m_bActive = true; }
+ bool Active() const { return m_bActive; }
+ bool Repeat() const { return m_bRepeat; }
+ int ControllerNumber() const { return m_iControllerNumber; }
+ bool Axis() const { return m_bAxis; }
+ unsigned int KeyCode() const { return m_iKeyCode; }
+ float Amount() const { return m_fAmount; }
+ void Load();
+ const std::string& JoystickName() const { return m_joystickName; }
+ const std::string& CustomControllerName() const { return m_customControllerName; }
+
+ // data
+ unsigned int m_iKeyCode;
+ unsigned short m_iControllerNumber;
+ std::string m_buttonName;
+ std::string m_mapName;
+ std::string m_joystickName;
+ std::string m_customControllerName;
+ float m_fAmount;
+ bool m_bUseAmount;
+ bool m_bRepeat;
+ bool m_bActive;
+ bool m_bAxis;
+ std::chrono::time_point<std::chrono::steady_clock> m_iNextRepeat;
+ };
+
+
+ /**********************************************************************/
+ /* UDP EventClient Class */
+ /**********************************************************************/
+ // - clients timeout if they don't receive at least 1 ping in 1 minute
+ // - sequence packets timeout after 5 seconds
+ class CEventClient
+ {
+ public:
+ CEventClient()
+ {
+ Initialize();
+ }
+
+ explicit CEventClient(SOCKETS::CAddress& addr):
+ m_remoteAddr(addr)
+ {
+ Initialize();
+ }
+
+ void Initialize()
+ {
+ m_bGreeted = false;
+ m_iMouseX = 0;
+ m_iMouseY = 0;
+ m_iCurrentSeqLen = 0;
+ m_lastPing = 0;
+ m_lastSeq = 0;
+ m_iRemotePort = 0;
+ m_bMouseMoved = false;
+ m_bSequenceError = false;
+ RefreshSettings();
+ }
+
+ const std::string& Name() const
+ {
+ return m_deviceName;
+ }
+
+ void RefreshSettings()
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ m_iRepeatDelay =
+ std::chrono::milliseconds(settings->GetInt(CSettings::SETTING_SERVICES_ESINITIALDELAY));
+ m_iRepeatSpeed = std::chrono::milliseconds(
+ settings->GetInt(CSettings::SETTING_SERVICES_ESCONTINUOUSDELAY));
+ }
+
+ SOCKETS::CAddress& Address()
+ {
+ return m_remoteAddr;
+ }
+
+ virtual ~CEventClient()
+ {
+ FreePacketQueues();
+ }
+
+ // add packet to queue
+ bool AddPacket(std::unique_ptr<EVENTPACKET::CEventPacket> packet);
+
+ // return true if client received ping with the last 1 minute
+ bool Alive() const;
+
+ // process the packet queue
+ bool ProcessQueue();
+
+ // process the queued up events (packets)
+ void ProcessEvents();
+
+ // gets the next action in the action queue
+ bool GetNextAction(CEventAction& action);
+
+ // deallocate all packets in the queues
+ void FreePacketQueues();
+
+ // return event states
+ unsigned int GetButtonCode(std::string& strMapName, bool& isAxis, float& amount, bool &isJoystick);
+
+ // update mouse position
+ bool GetMousePos(float& x, float& y);
+
+ protected:
+ bool ProcessPacket(EVENTPACKET::CEventPacket *packet);
+
+ // packet handlers
+ virtual bool OnPacketHELO(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketBYE(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketBUTTON(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketMOUSE(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketNOTIFICATION(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketLOG(EVENTPACKET::CEventPacket *packet);
+ virtual bool OnPacketACTION(EVENTPACKET::CEventPacket *packet);
+ bool CheckButtonRepeat(std::chrono::time_point<std::chrono::steady_clock>& next);
+
+ // returns true if the client has received the HELO packet
+ bool Greeted() { return m_bGreeted; }
+
+ // reset the timeout counter
+ void ResetTimeout()
+ {
+ m_lastPing = time(NULL);
+ }
+
+ // helper functions
+
+ // Parses a null terminated string from payload.
+ // After parsing successfully:
+ // 1. payload is incremented to end of string
+ // 2. psize is decremented by length of string
+ // 3. parsedVal contains the parsed string
+ // 4. true is returned
+ bool ParseString(unsigned char* &payload, int &psize, std::string& parsedVal);
+
+ // Parses a single byte (same behavior as ParseString)
+ bool ParseByte(unsigned char* &payload, int &psize, unsigned char& parsedVal);
+
+ // Parse a single 32-bit integer (converts from network order to host order)
+ bool ParseUInt32(unsigned char* &payload, int &psize, unsigned int& parsedVal);
+
+ // Parse a single 16-bit integer (converts from network order to host order)
+ bool ParseUInt16(unsigned char* &payload, int &psize, unsigned short& parsedVal);
+
+ std::string m_deviceName;
+ int m_iCurrentSeqLen;
+ time_t m_lastPing;
+ time_t m_lastSeq;
+ int m_iRemotePort;
+ bool m_bGreeted;
+ std::chrono::milliseconds m_iRepeatDelay;
+ std::chrono::milliseconds m_iRepeatSpeed;
+ unsigned int m_iMouseX;
+ unsigned int m_iMouseY;
+ bool m_bMouseMoved;
+ bool m_bSequenceError;
+
+ SOCKETS::CAddress m_remoteAddr;
+
+ EVENTPACKET::LogoType m_eLogoType;
+ CCriticalSection m_critSection;
+
+ std::map<unsigned int, std::unique_ptr<EVENTPACKET::CEventPacket>> m_seqPackets;
+ std::queue<std::unique_ptr<EVENTPACKET::CEventPacket>> m_readyPackets;
+
+ // button and mouse state
+ std::list<CEventButtonState> m_buttonQueue;
+ std::queue<CEventAction> m_actionQueue;
+ CEventButtonState m_currentButton;
+ };
+
+}
+
diff --git a/xbmc/network/EventPacket.cpp b/xbmc/network/EventPacket.cpp
new file mode 100644
index 0000000..17389f1
--- /dev/null
+++ b/xbmc/network/EventPacket.cpp
@@ -0,0 +1,80 @@
+/*
+ * 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 "EventPacket.h"
+
+#include "Socket.h"
+#include "utils/log.h"
+
+using namespace EVENTPACKET;
+
+/************************************************************************/
+/* CEventPacket */
+/************************************************************************/
+bool CEventPacket::Parse(int datasize, const void *data)
+{
+ unsigned char* buf = const_cast<unsigned char*>((const unsigned char *)data);
+ if (datasize < HEADER_SIZE || datasize > PACKET_SIZE)
+ return false;
+
+ // check signature
+ if (memcmp(data, (const void*)HEADER_SIG, HEADER_SIG_LENGTH) != 0)
+ return false;
+
+ buf += HEADER_SIG_LENGTH;
+
+ // extract protocol version
+ m_cMajVer = (*buf++);
+ m_cMinVer = (*buf++);
+
+ if (m_cMajVer != 2 && m_cMinVer != 0)
+ return false;
+
+ // get packet type
+ m_eType = (PacketType)ntohs(*((uint16_t*)buf));
+
+ if (m_eType < (unsigned short)PT_HELO || m_eType >= (unsigned short)PT_LAST)
+ return false;
+
+ // get packet sequence id
+ buf += 2;
+ m_iSeq = ntohl(*((uint32_t*)buf));
+
+ // get total message length
+ buf += 4;
+ m_iTotalPackets = ntohl(*((uint32_t*)buf));
+
+ // get payload size
+ buf += 4;
+ uint16_t payloadSize = ntohs(*(reinterpret_cast<uint16_t*>(buf)));
+
+ if ((payloadSize + HEADER_SIZE) != static_cast<uint16_t>(datasize))
+ return false;
+
+ // get the client's token
+ buf += 2;
+ m_iClientToken = ntohl(*((uint32_t*)buf));
+
+ buf += 4;
+
+ // get payload
+ if (payloadSize > 0)
+ {
+ // forward past reserved bytes
+ buf += 10;
+
+ m_pPayload = std::vector<uint8_t>(buf, buf + payloadSize);
+ }
+ m_bValid = true;
+ return true;
+}
+
+void CEventPacket::SetPayload(std::vector<uint8_t> payload)
+{
+ m_pPayload = std::move(payload);
+}
diff --git a/xbmc/network/EventPacket.h b/xbmc/network/EventPacket.h
new file mode 100644
index 0000000..40074e8
--- /dev/null
+++ b/xbmc/network/EventPacket.h
@@ -0,0 +1,227 @@
+/*
+ * 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 <cstdint>
+#include <stdlib.h>
+#include <vector>
+
+namespace EVENTPACKET
+{
+ const int PACKET_SIZE = 1024;
+ const int HEADER_SIZE = 32;
+ const char HEADER_SIG[] = "XBMC";
+ const int HEADER_SIG_LENGTH = 4;
+
+ /************************************************************************/
+ /* */
+ /* - Generic packet structure (maximum 1024 bytes per packet) */
+ /* - Header is 32 bytes long, so 992 bytes available for payload */
+ /* - large payloads can be split into multiple packets using H4 and H5 */
+ /* H5 should contain total no. of packets in such a case */
+ /* - H6 contains length of P1, which is limited to 992 bytes */
+ /* - if H5 is 0 or 1, then H4 will be ignored (single packet msg) */
+ /* - H7 must be set to zeros for now */
+ /* */
+ /* ----------------------------- */
+ /* | -H1 Signature ("XBMC") | - 4 x CHAR 4B */
+ /* | -H2 Version (eg. 2.0) | - 2 x UNSIGNED CHAR 2B */
+ /* | -H3 PacketType | - 1 x UNSIGNED SHORT 2B */
+ /* | -H4 Sequence number | - 1 x UNSIGNED LONG 4B */
+ /* | -H5 No. of packets in msg | - 1 x UNSIGNED LONG 4B */
+ /* | -H6 Payload size | - 1 x UNSIGNED SHORT 2B */
+ /* | -H7 Client's unique token | - 1 x UNSIGNED LONG 4B */
+ /* | -H8 Reserved | - 10 x UNSIGNED CHAR 10B */
+ /* |---------------------------| */
+ /* | -P1 payload | - */
+ /* ----------------------------- */
+ /************************************************************************/
+
+ /************************************************************************
+ The payload format for each packet type is described below each
+ packet type.
+
+ Legend:
+ %s - null terminated ASCII string (strlen + '\0' bytes)
+ (empty string is represented as a single byte NULL '\0')
+ %c - single byte
+ %i - network byte ordered short unsigned integer (2 bytes)
+ %d - network byte ordered long unsigned integer (4 bytes)
+ XX - binary data prefixed with %d size
+ (can span multiple packets with <raw>)
+ raw - raw binary data
+ ************************************************************************/
+
+ enum LogoType
+ {
+ LT_NONE = 0x00,
+ LT_JPEG = 0x01,
+ LT_PNG = 0x02,
+ LT_GIF = 0x03
+ };
+
+ enum ButtonFlags
+ {
+ PTB_USE_NAME = 0x01,
+ PTB_DOWN = 0x02,
+ PTB_UP = 0x04,
+ PTB_USE_AMOUNT = 0x08,
+ PTB_QUEUE = 0x10,
+ PTB_NO_REPEAT = 0x20,
+ PTB_VKEY = 0x40,
+ PTB_AXIS = 0x80,
+ PTB_AXISSINGLE = 0x100,
+ PTB_UNICODE = 0x200
+ };
+
+ enum MouseFlags
+ {
+ PTM_ABSOLUTE = 0x01
+ /* PTM_RELATIVE = 0x02 */
+ };
+
+ enum ActionType
+ {
+ AT_EXEC_BUILTIN = 0x01,
+ AT_BUTTON = 0x02
+ };
+
+ enum PacketType
+ {
+ PT_HELO = 0x01,
+ /************************************************************************/
+ /* Payload format */
+ /* %s - device name (max 128 chars) */
+ /* %c - icontype ( 0=>NOICON, 1=>JPEG , 2=>PNG , 3=>GIF ) */
+ /* %s - my port ( 0=>not listening ) */
+ /* %d - reserved1 ( 0 ) */
+ /* %d - reserved2 ( 0 ) */
+ /* XX - imagedata ( can span multiple packets ) */
+ /************************************************************************/
+
+ PT_BYE = 0x02,
+ /************************************************************************/
+ /* no payload */
+ /************************************************************************/
+
+ PT_BUTTON = 0x03,
+ /************************************************************************/
+ /* Payload format */
+ /* %i - button code */
+ /* %i - flags 0x01 => use button map/name instead of code */
+ /* 0x02 => btn down */
+ /* 0x04 => btn up */
+ /* 0x08 => use amount */
+ /* 0x10 => queue event */
+ /* 0x20 => do not repeat */
+ /* 0x40 => virtual key */
+ /* 0x80 => axis key */
+ /* %i - amount ( 0 => 65k maps to -1 => 1 ) */
+ /* %s - device map (case sensitive and required if flags & 0x01) */
+ /* "KB" - Standard keyboard map */
+ /* "XG" - Xbox Gamepad */
+ /* "R1" - Xbox Remote */
+ /* "R2" - Xbox Universal Remote */
+ /* "LI:devicename" - valid LIRC device map where 'devicename' */
+ /* is the actual name of the LIRC device */
+ /* "JS<num>:joyname" - valid Joystick device map where */
+ /* 'joyname' is the name specified in */
+ /* the keymap. JS only supports button code */
+ /* and not button name currently (!0x01). */
+ /* %s - button name (required if flags & 0x01) */
+ /************************************************************************/
+
+ PT_MOUSE = 0x04,
+ /************************************************************************/
+ /* Payload format */
+ /* %c - flags */
+ /* - 0x01 absolute position */
+ /* %i - mousex (0-65535 => maps to screen width) */
+ /* %i - mousey (0-65535 => maps to screen height) */
+ /************************************************************************/
+
+ PT_PING = 0x05,
+ /************************************************************************/
+ /* no payload */
+ /************************************************************************/
+
+ PT_BROADCAST = 0x06,
+ /************************************************************************/
+ /* @todo implement */
+ /* Payload format: TODO */
+ /************************************************************************/
+
+ PT_NOTIFICATION = 0x07,
+ /************************************************************************/
+ /* Payload format: */
+ /* %s - caption */
+ /* %s - message */
+ /* %c - icontype ( 0=>NOICON, 1=>JPEG , 2=>PNG , 3=>GIF ) */
+ /* %d - reserved ( 0 ) */
+ /* XX - imagedata ( can span multiple packets ) */
+ /************************************************************************/
+
+ PT_BLOB = 0x08,
+ /************************************************************************/
+ /* Payload format */
+ /* raw - raw binary data */
+ /************************************************************************/
+
+ PT_LOG = 0x09,
+ /************************************************************************/
+ /* Payload format */
+ /* %c - log type */
+ /* %s - message */
+ /************************************************************************/
+ PT_ACTION = 0x0A,
+ /************************************************************************/
+ /* Payload format */
+ /* %c - action type */
+ /* %s - action message */
+ /************************************************************************/
+ PT_DEBUG = 0xFF,
+ /************************************************************************/
+ /* Payload format: */
+ /************************************************************************/
+
+ PT_LAST // NO-OP
+ };
+
+ class CEventPacket
+ {
+ public:
+ CEventPacket() = default;
+
+ explicit CEventPacket(int datasize, const void* data) { Parse(datasize, data); }
+
+ virtual ~CEventPacket() = default;
+ virtual bool Parse(int datasize, const void *data);
+ bool IsValid() const { return m_bValid; }
+ PacketType Type() const { return m_eType; }
+ unsigned int Size() const { return m_iTotalPackets; }
+ unsigned int Sequence() const { return m_iSeq; }
+ const uint8_t* Payload() const { return m_pPayload.data(); }
+ unsigned int PayloadSize() const { return m_pPayload.size(); }
+ unsigned int ClientToken() const { return m_iClientToken; }
+ void SetPayload(std::vector<uint8_t> payload);
+
+ protected:
+ bool m_bValid{false};
+ unsigned int m_iSeq{0};
+ unsigned int m_iTotalPackets{0};
+ unsigned char m_header[32];
+ std::vector<uint8_t> m_pPayload;
+ unsigned int m_iClientToken{0};
+ unsigned char m_cMajVer{'0'};
+ unsigned char m_cMinVer{'0'};
+ PacketType m_eType{PT_LAST};
+ };
+
+}
+
diff --git a/xbmc/network/EventServer.cpp b/xbmc/network/EventServer.cpp
new file mode 100644
index 0000000..3e25b59
--- /dev/null
+++ b/xbmc/network/EventServer.cpp
@@ -0,0 +1,352 @@
+/*
+ * 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 "EventServer.h"
+
+#include "EventClient.h"
+#include "EventPacket.h"
+#include "ServiceBroker.h"
+#include "Socket.h"
+#include "Zeroconf.h"
+#include "application/Application.h"
+#include "guilib/GUIAudioManager.h"
+#include "input/Key.h"
+#include "input/actions/ActionTranslator.h"
+#include "interfaces/builtins/Builtins.h"
+#include "utils/SystemInfo.h"
+#include "utils/log.h"
+
+#include <cassert>
+#include <map>
+#include <mutex>
+#include <queue>
+
+using namespace EVENTSERVER;
+using namespace EVENTPACKET;
+using namespace EVENTCLIENT;
+using namespace SOCKETS;
+using namespace std::chrono_literals;
+
+/************************************************************************/
+/* CEventServer */
+/************************************************************************/
+std::unique_ptr<CEventServer> CEventServer::m_pInstance;
+
+CEventServer::CEventServer() : CThread("EventServer")
+{
+ m_bStop = false;
+ m_bRefreshSettings = false;
+
+ // default timeout in ms for receiving a single packet
+ m_iListenTimeout = 1000;
+}
+
+void CEventServer::RemoveInstance()
+{
+ m_pInstance.reset();
+}
+
+CEventServer* CEventServer::GetInstance()
+{
+ if (!m_pInstance)
+ m_pInstance = std::make_unique<CEventServer>();
+
+ return m_pInstance.get();
+}
+
+void CEventServer::StartServer()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bRunning)
+ return;
+
+ // set default port
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ m_iPort = settings->GetInt(CSettings::SETTING_SERVICES_ESPORT);
+ assert(m_iPort <= 65535 && m_iPort >= 1);
+
+ // max clients
+ m_iMaxClients = settings->GetInt(CSettings::SETTING_SERVICES_ESMAXCLIENTS);
+ if (m_iMaxClients < 0)
+ {
+ CLog::Log(LOGERROR, "ES: Invalid maximum number of clients specified {}", m_iMaxClients);
+ m_iMaxClients = 20;
+ }
+
+ CThread::Create();
+}
+
+void CEventServer::StopServer(bool bWait)
+{
+ CZeroconf::GetInstance()->RemoveService("services.eventserver");
+ StopThread(bWait);
+}
+
+void CEventServer::Cleanup()
+{
+ if (m_pSocket)
+ m_pSocket->Close();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_clients.clear();
+}
+
+int CEventServer::GetNumberOfClients()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_clients.size();
+}
+
+void CEventServer::Process()
+{
+ while(!m_bStop)
+ {
+ Run();
+ if (!m_bStop)
+ CThread::Sleep(1000ms);
+ }
+}
+
+void CEventServer::Run()
+{
+ CSocketListener listener;
+ int packetSize = 0;
+
+ CLog::Log(LOGINFO, "ES: Starting UDP Event server on port {}", m_iPort);
+
+ Cleanup();
+
+ // create socket and initialize buffer
+ m_pSocket = CSocketFactory::CreateUDPSocket();
+ if (!m_pSocket)
+ {
+ CLog::Log(LOGERROR, "ES: Could not create socket, aborting!");
+ return;
+ }
+
+ m_pPacketBuffer.resize(PACKET_SIZE);
+
+ // bind to IP and start listening on port
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ int port_range = settings->GetInt(CSettings::SETTING_SERVICES_ESPORTRANGE);
+ if (port_range < 1 || port_range > 100)
+ {
+ CLog::Log(LOGERROR, "ES: Invalid port range specified {}, defaulting to 10", port_range);
+ port_range = 10;
+ }
+ if (!m_pSocket->Bind(!settings->GetBool(CSettings::SETTING_SERVICES_ESALLINTERFACES), m_iPort, port_range))
+ {
+ CLog::Log(LOGERROR, "ES: Could not listen on port {}", m_iPort);
+ return;
+ }
+
+ // publish service
+ std::vector<std::pair<std::string, std::string> > txt;
+ CZeroconf::GetInstance()->PublishService("servers.eventserver",
+ "_xbmc-events._udp",
+ CSysInfo::GetDeviceName(),
+ m_iPort,
+ txt);
+
+ // add our socket to the 'select' listener
+ listener.AddSocket(m_pSocket.get());
+
+ m_bRunning = true;
+
+ while (!m_bStop)
+ {
+ try
+ {
+ // start listening until we timeout
+ if (listener.Listen(m_iListenTimeout))
+ {
+ CAddress addr;
+ if ((packetSize = m_pSocket->Read(addr, PACKET_SIZE, m_pPacketBuffer.data())) > -1)
+ {
+ ProcessPacket(addr, packetSize);
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "ES: Exception caught while listening for socket");
+ break;
+ }
+
+ // process events and queue the necessary actions and button codes
+ ProcessEvents();
+
+ // refresh client list
+ RefreshClients();
+
+ // broadcast
+ // BroadcastBeacon();
+ }
+
+ CLog::Log(LOGINFO, "ES: UDP Event server stopped");
+ m_bRunning = false;
+ Cleanup();
+}
+
+void CEventServer::ProcessPacket(CAddress& addr, int pSize)
+{
+ // check packet validity
+ std::unique_ptr<CEventPacket> packet =
+ std::make_unique<CEventPacket>(pSize, m_pPacketBuffer.data());
+ if (!packet)
+ {
+ CLog::Log(LOGERROR, "ES: Out of memory, cannot accept packet");
+ return;
+ }
+
+ unsigned int clientToken;
+
+ if (!packet->IsValid())
+ {
+ CLog::Log(LOGDEBUG, "ES: Received invalid packet");
+ return;
+ }
+
+ clientToken = packet->ClientToken();
+ if (!clientToken)
+ clientToken = addr.ULong(); // use IP if packet doesn't have a token
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // first check if we have a client for this address
+ auto iter = m_clients.find(clientToken);
+
+ if ( iter == m_clients.end() )
+ {
+ if ( m_clients.size() >= (unsigned int)m_iMaxClients)
+ {
+ CLog::Log(LOGWARNING, "ES: Cannot accept any more clients, maximum client count reached");
+ return;
+ }
+
+ // new client
+ auto client = std::make_unique<CEventClient>(addr);
+ if (!client)
+ {
+ CLog::Log(LOGERROR, "ES: Out of memory, cannot accept new client connection");
+ return;
+ }
+
+ m_clients[clientToken] = std::move(client);
+ }
+ m_clients[clientToken]->AddPacket(std::move(packet));
+}
+
+void CEventServer::RefreshClients()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto iter = m_clients.begin();
+
+ while ( iter != m_clients.end() )
+ {
+ if (! (iter->second->Alive()))
+ {
+ CLog::Log(LOGINFO, "ES: Client {} from {} timed out", iter->second->Name(),
+ iter->second->Address().Address());
+ m_clients.erase(iter);
+ iter = m_clients.begin();
+ }
+ else
+ {
+ if (m_bRefreshSettings)
+ {
+ iter->second->RefreshSettings();
+ }
+ ++iter;
+ }
+ }
+ m_bRefreshSettings = false;
+}
+
+void CEventServer::ProcessEvents()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto iter = m_clients.begin();
+
+ while (iter != m_clients.end())
+ {
+ iter->second->ProcessEvents();
+ ++iter;
+ }
+}
+
+bool CEventServer::ExecuteNextAction()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CEventAction actionEvent;
+ auto iter = m_clients.begin();
+
+ while (iter != m_clients.end())
+ {
+ if (iter->second->GetNextAction(actionEvent))
+ {
+ // Leave critical section before processing action
+ lock.unlock();
+ switch(actionEvent.actionType)
+ {
+ case AT_EXEC_BUILTIN:
+ CBuiltins::GetInstance().Execute(actionEvent.actionName);
+ break;
+
+ case AT_BUTTON:
+ {
+ unsigned int actionID;
+ CActionTranslator::TranslateString(actionEvent.actionName, actionID);
+ CAction action(actionID, 1.0f, 0.0f, actionEvent.actionName);
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetAudioManager().PlayActionSound(action);
+
+ g_application.OnAction(action);
+ }
+ break;
+ }
+ return true;
+ }
+ ++iter;
+ }
+
+ return false;
+}
+
+unsigned int CEventServer::GetButtonCode(std::string& strMapName, bool& isAxis, float& fAmount, bool &isJoystick)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto iter = m_clients.begin();
+ unsigned int bcode = 0;
+
+ while (iter != m_clients.end())
+ {
+ bcode = iter->second->GetButtonCode(strMapName, isAxis, fAmount, isJoystick);
+ if (bcode)
+ return bcode;
+ ++iter;
+ }
+ return bcode;
+}
+
+bool CEventServer::GetMousePos(float &x, float &y)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto iter = m_clients.begin();
+
+ while (iter != m_clients.end())
+ {
+ if (iter->second->GetMousePos(x, y))
+ return true;
+ ++iter;
+ }
+ return false;
+}
diff --git a/xbmc/network/EventServer.h b/xbmc/network/EventServer.h
new file mode 100644
index 0000000..8e529af
--- /dev/null
+++ b/xbmc/network/EventServer.h
@@ -0,0 +1,81 @@
+/*
+ * 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 "EventClient.h"
+#include "Socket.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <atomic>
+#include <map>
+#include <mutex>
+#include <queue>
+#include <vector>
+
+namespace EVENTSERVER
+{
+
+ /**********************************************************************/
+ /* UDP Event Server Class */
+ /**********************************************************************/
+ class CEventServer : private CThread
+ {
+ public:
+ static void RemoveInstance();
+ static CEventServer* GetInstance();
+
+ CEventServer();
+ ~CEventServer() override = default;
+
+ // IRunnable entry point for thread
+ void Process() override;
+
+ bool Running()
+ {
+ return m_bRunning;
+ }
+
+ void RefreshSettings()
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bRefreshSettings = true;
+ }
+
+ // start / stop server
+ void StartServer();
+ void StopServer(bool bWait);
+
+ // get events
+ unsigned int GetButtonCode(std::string& strMapName, bool& isAxis, float& amount, bool &isJoystick);
+ bool ExecuteNextAction();
+ bool GetMousePos(float &x, float &y);
+ int GetNumberOfClients();
+
+ protected:
+ void Cleanup();
+ void Run();
+ void ProcessPacket(SOCKETS::CAddress& addr, int packetSize);
+ void ProcessEvents();
+ void RefreshClients();
+
+ std::map<unsigned long, std::unique_ptr<EVENTCLIENT::CEventClient>> m_clients;
+ static std::unique_ptr<CEventServer> m_pInstance;
+ std::unique_ptr<SOCKETS::CUDPSocket> m_pSocket;
+ int m_iPort;
+ int m_iListenTimeout;
+ int m_iMaxClients;
+ std::vector<uint8_t> m_pPacketBuffer;
+ std::atomic<bool> m_bRunning = false;
+ CCriticalSection m_critSection;
+ bool m_bRefreshSettings;
+ };
+
+}
+
diff --git a/xbmc/network/GUIDialogNetworkSetup.cpp b/xbmc/network/GUIDialogNetworkSetup.cpp
new file mode 100644
index 0000000..e2fd165
--- /dev/null
+++ b/xbmc/network/GUIDialogNetworkSetup.cpp
@@ -0,0 +1,471 @@
+/*
+ * 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 "GUIDialogNetworkSetup.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/VFSEntry.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "settings/lib/Setting.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <utility>
+
+
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+
+
+#define CONTROL_OK 28
+#define CONTROL_CANCEL 29
+
+#define SETTING_PROTOCOL "protocol"
+#define SETTING_SERVER_ADDRESS "serveraddress"
+#define SETTING_SERVER_BROWSE "serverbrowse"
+#define SETTING_PORT_NUMBER "portnumber"
+#define SETTING_USERNAME "username"
+#define SETTING_PASSWORD "password"
+#define SETTING_REMOTE_PATH "remotepath"
+
+CGUIDialogNetworkSetup::CGUIDialogNetworkSetup(void)
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_NETWORK_SETUP, "DialogSettings.xml")
+{
+ m_protocol = 0;
+ m_confirmed = false;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogNetworkSetup::~CGUIDialogNetworkSetup() = default;
+
+bool CGUIDialogNetworkSetup::OnBack(int actionID)
+{
+ m_confirmed = false;
+ return CGUIDialogSettingsManualBase::OnBack(actionID);
+}
+
+bool CGUIDialogNetworkSetup::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_OK)
+ {
+ OnOK();
+ return true;
+ }
+ else if (iControl == CONTROL_CANCEL)
+ {
+ OnCancel();
+ return true;
+ }
+ }
+ break;
+ }
+ return CGUIDialogSettingsManualBase::OnMessage(message);
+}
+
+void CGUIDialogNetworkSetup::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == SETTING_PROTOCOL)
+ {
+ m_server.clear();
+ m_path.clear();
+ m_username.clear();
+ m_password.clear();
+ OnProtocolChange();
+ }
+ else if (settingId == SETTING_SERVER_ADDRESS)
+ m_server = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_REMOTE_PATH)
+ m_path = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_PORT_NUMBER)
+ m_port = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_USERNAME)
+ m_username = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_PASSWORD)
+ m_password = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+}
+
+void CGUIDialogNetworkSetup::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == SETTING_SERVER_BROWSE)
+ OnServerBrowse();
+}
+
+// \brief Show CGUIDialogNetworkSetup dialog and prompt for a new network address.
+// \return True if the network address is valid, false otherwise.
+bool CGUIDialogNetworkSetup::ShowAndGetNetworkAddress(std::string &path)
+{
+ CGUIDialogNetworkSetup *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogNetworkSetup>(WINDOW_DIALOG_NETWORK_SETUP);
+ if (!dialog) return false;
+ dialog->Initialize();
+ if (!dialog->SetPath(path))
+ {
+ HELPERS::ShowOKDialogText(CVariant{ 10218 }, CVariant{ 39103 });
+ return false;
+ }
+
+ dialog->Open();
+ path = dialog->ConstructPath();
+ return dialog->IsConfirmed();
+}
+
+void CGUIDialogNetworkSetup::OnInitWindow()
+{
+ // start as unconfirmed
+ m_confirmed = false;
+
+ CGUIDialogSettingsManualBase::OnInitWindow();
+
+ UpdateButtons();
+}
+
+void CGUIDialogNetworkSetup::OnDeinitWindow(int nextWindowID)
+{
+ // clear protocol spinner
+ BaseSettingControlPtr settingControl = GetSettingControl(SETTING_PROTOCOL);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ {
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), settingControl->GetID());
+ OnMessage(msg);
+ }
+
+ CGUIDialogSettingsManualBase::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIDialogNetworkSetup::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+ SetHeading(1007);
+
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+}
+
+void CGUIDialogNetworkSetup::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("networksetupsettings", -1);
+ if (category == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogNetworkSetup: unable to setup settings");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogNetworkSetup: unable to setup settings");
+ return;
+ }
+
+ // Add our protocols
+ TranslatableIntegerSettingOptions labels;
+ for (size_t idx = 0; idx < m_protocols.size(); ++idx)
+ labels.push_back(
+ TranslatableIntegerSettingOption(m_protocols[idx].label, idx, m_protocols[idx].addonId));
+
+ AddSpinner(group, SETTING_PROTOCOL, 1008, SettingLevel::Basic, m_protocol, labels);
+ AddEdit(group, SETTING_SERVER_ADDRESS, 1010, SettingLevel::Basic, m_server, true);
+ std::shared_ptr<CSettingAction> subsetting = AddButton(group, SETTING_SERVER_BROWSE, 1024, SettingLevel::Basic, "", false);
+ if (subsetting != NULL)
+ subsetting->SetParent(SETTING_SERVER_ADDRESS);
+
+ AddEdit(group, SETTING_REMOTE_PATH, 1012, SettingLevel::Basic, m_path, true);
+ AddEdit(group, SETTING_PORT_NUMBER, 1013, SettingLevel::Basic, m_port, true);
+ AddEdit(group, SETTING_USERNAME, 1014, SettingLevel::Basic, m_username, true);
+ AddEdit(group, SETTING_PASSWORD, 15052, SettingLevel::Basic, m_password, true, true);
+}
+
+void CGUIDialogNetworkSetup::OnServerBrowse()
+{
+ // open a filebrowser dialog with the current address
+ VECSOURCES shares;
+ std::string path = ConstructPath();
+ // get the share as the base path
+ CMediaSource share;
+ std::string basePath = path;
+ std::string tempPath;
+ while (URIUtils::GetParentPath(basePath, tempPath))
+ basePath = tempPath;
+ share.strPath = basePath;
+ // don't include the user details in the share name
+ CURL url(share.strPath);
+ share.strName = url.GetWithoutUserDetails();
+ shares.push_back(share);
+ if (CGUIDialogFileBrowser::ShowAndGetDirectory(shares, g_localizeStrings.Get(1015), path))
+ {
+ SetPath(path);
+ UpdateButtons();
+ }
+}
+
+void CGUIDialogNetworkSetup::OnOK()
+{
+ m_confirmed = true;
+ Close();
+}
+
+void CGUIDialogNetworkSetup::OnCancel()
+{
+ m_confirmed = false;
+ Close();
+}
+
+void CGUIDialogNetworkSetup::OnProtocolChange()
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(SETTING_PROTOCOL);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), settingControl->GetID());
+ if (!OnMessage(msg))
+ return;
+ m_protocol = msg.GetParam1();
+ // set defaults for the port
+ m_port = std::to_string(m_protocols[m_protocol].defaultPort);
+
+ UpdateButtons();
+ }
+}
+
+void CGUIDialogNetworkSetup::UpdateButtons()
+{
+ // Address label
+ BaseSettingControlPtr addressControl = GetSettingControl(SETTING_SERVER_ADDRESS);
+ if (addressControl != NULL && addressControl->GetControl() != NULL)
+ {
+ int addressControlID = addressControl->GetID();
+ SET_CONTROL_LABEL2(addressControlID, m_server);
+ if (m_protocols[m_protocol].type == "smb")
+ {
+ SET_CONTROL_LABEL(addressControlID, 1010); // Server name
+ }
+ else
+ {
+ SET_CONTROL_LABEL(addressControlID, 1009); // Server Address
+ }
+ SendMessage(GUI_MSG_SET_TYPE, addressControlID, CGUIEditControl::INPUT_TYPE_TEXT, 1016);
+ }
+
+ // remote path
+ BaseSettingControlPtr pathControl = GetSettingControl(SETTING_REMOTE_PATH);
+ if (pathControl != NULL && pathControl->GetControl() != NULL)
+ {
+ int pathControlID = pathControl->GetID();
+ SET_CONTROL_LABEL2(pathControlID, m_path);
+ CONTROL_ENABLE_ON_CONDITION(pathControlID, m_protocols[m_protocol].supportPath);
+ if (m_protocols[m_protocol].type != "smb")
+ {
+ SET_CONTROL_LABEL(pathControlID, 1011); // Remote Path
+ }
+ else
+ {
+ SET_CONTROL_LABEL(pathControlID, 1012); // Shared Folder
+ }
+ SendMessage(GUI_MSG_SET_TYPE, pathControlID, CGUIEditControl::INPUT_TYPE_TEXT, 1017);
+ }
+
+ // username
+ BaseSettingControlPtr userControl = GetSettingControl(SETTING_USERNAME);
+ if (userControl != NULL && userControl->GetControl() != NULL)
+ {
+ int userControlID = userControl->GetID();
+ SET_CONTROL_LABEL2(userControlID, m_username);
+ CONTROL_ENABLE_ON_CONDITION(userControlID,
+ m_protocols[m_protocol].supportUsername);
+
+ SendMessage(GUI_MSG_SET_TYPE, userControlID, CGUIEditControl::INPUT_TYPE_TEXT, 1019);
+ }
+
+ // port
+ BaseSettingControlPtr portControl = GetSettingControl(SETTING_PORT_NUMBER);
+ if (portControl != NULL && portControl->GetControl() != NULL)
+ {
+ int portControlID = portControl->GetID();
+ SET_CONTROL_LABEL2(portControlID, m_port);
+ CONTROL_ENABLE_ON_CONDITION(portControlID, m_protocols[m_protocol].supportPort);
+
+ SendMessage(GUI_MSG_SET_TYPE, portControlID, CGUIEditControl::INPUT_TYPE_NUMBER, 1018);
+ }
+
+ // password
+ BaseSettingControlPtr passControl = GetSettingControl(SETTING_PASSWORD);
+ if (passControl != NULL && passControl->GetControl() != NULL)
+ {
+ int passControlID = passControl->GetID();
+ SET_CONTROL_LABEL2(passControlID, m_password);
+ CONTROL_ENABLE_ON_CONDITION(passControlID,
+ m_protocols[m_protocol].supportPassword);
+
+ SendMessage(GUI_MSG_SET_TYPE, passControlID, CGUIEditControl::INPUT_TYPE_PASSWORD, 12326);
+ }
+
+ // server browse should be disabled if we are in FTP, FTPS, HTTP, HTTPS, RSS, RSSS, DAV or DAVS
+ BaseSettingControlPtr browseControl = GetSettingControl(SETTING_SERVER_BROWSE);
+ if (browseControl != NULL && browseControl->GetControl() != NULL)
+ {
+ int browseControlID = browseControl->GetID();
+ CONTROL_ENABLE_ON_CONDITION(browseControlID,
+ m_protocols[m_protocol].supportBrowsing);
+ }
+}
+
+std::string CGUIDialogNetworkSetup::ConstructPath() const
+{
+ CURL url;
+ url.SetProtocol(m_protocols[m_protocol].type);
+
+ if (!m_username.empty())
+ {
+ // domain/name to domain\name
+ std::string username = m_username;
+ std::replace(username.begin(), username.end(), '/', '\\');
+
+ if (url.IsProtocol("smb") && username.find('\\') != std::string::npos)
+ {
+ auto pair = StringUtils::Split(username, "\\", 2);
+ url.SetDomain(pair[0]);
+ url.SetUserName(pair[1]);
+ }
+ else
+ url.SetUserName(m_username);
+ if (!m_password.empty())
+ url.SetPassword(m_password);
+ }
+
+ if (!m_server.empty())
+ url.SetHostName(m_server);
+
+ if (m_protocols[m_protocol].supportPort &&
+ !m_port.empty() && atoi(m_port.c_str()) > 0)
+ {
+ url.SetPort(atoi(m_port.c_str()));
+ }
+
+ if (!m_path.empty())
+ url.SetFileName(m_path);
+
+ return url.Get();
+}
+
+bool CGUIDialogNetworkSetup::SetPath(const std::string &path)
+{
+ UpdateAvailableProtocols();
+
+ if (path.empty())
+ {
+ Reset();
+ return true;
+ }
+
+ CURL url(path);
+ m_protocol = -1;
+ for (size_t i = 0; i < m_protocols.size(); ++i)
+ {
+ if (m_protocols[i].type == url.GetProtocol())
+ {
+ m_protocol = i;
+ break;
+ }
+ }
+ if (m_protocol == -1)
+ {
+ CLog::LogF(LOGERROR, "Asked to initialize for unknown path {}", path);
+ Reset();
+ return false;
+ }
+
+ if (!url.GetDomain().empty())
+ m_username = url.GetDomain() + "\\" + url.GetUserName();
+ else
+ m_username = url.GetUserName();
+ m_password = url.GetPassWord();
+ m_port = std::to_string(url.GetPort());
+ m_server = url.GetHostName();
+ m_path = url.GetFileName();
+ URIUtils::RemoveSlashAtEnd(m_path);
+
+ return true;
+}
+
+void CGUIDialogNetworkSetup::Reset()
+{
+ m_username.clear();
+ m_password.clear();
+ m_port.clear();
+ m_server.clear();
+ m_path.clear();
+ m_protocol = 0;
+}
+
+void CGUIDialogNetworkSetup::UpdateAvailableProtocols()
+{
+ m_protocols.clear();
+#ifdef HAS_FILESYSTEM_SMB
+ // most popular protocol at the first place
+ m_protocols.emplace_back(Protocol{true, true, true, false, true, 0, "smb", 20171, ""});
+#endif
+ // protocols from vfs addon next
+ if (CServiceBroker::IsAddonInterfaceUp())
+ {
+ for (const auto& addon : CServiceBroker::GetVFSAddonCache().GetAddonInstances())
+ {
+ const auto& info = addon->GetProtocolInfo();
+ if (!addon->GetProtocolInfo().type.empty())
+ {
+ // only use first protocol
+ auto prots = StringUtils::Split(info.type, "|");
+ m_protocols.emplace_back(Protocol{
+ info.supportPath, info.supportUsername, info.supportPassword, info.supportPort,
+ info.supportBrowsing, info.defaultPort, prots.front(), info.label, addon->ID()});
+ }
+ }
+ }
+ // internals
+ const std::vector<Protocol> defaults = {{true, true, true, true, false, 443, "https", 20301, ""},
+ {true, true, true, true, false, 80, "http", 20300, ""},
+ {true, true, true, true, false, 443, "davs", 20254, ""},
+ {true, true, true, true, false, 80, "dav", 20253, ""},
+ {true, true, true, true, false, 21, "ftp", 20173, ""},
+ {true, true, true, true, false, 990, "ftps", 20174, ""},
+ {false, false, false, false, true, 0, "upnp", 20175, ""},
+ {true, true, true, true, false, 80, "rss", 20304, ""},
+ {true, true, true, true, false, 443, "rsss", 20305, ""}};
+
+ m_protocols.insert(m_protocols.end(), defaults.begin(), defaults.end());
+#ifdef HAS_FILESYSTEM_NFS
+ m_protocols.emplace_back(Protocol{true, false, false, false, true, 0, "nfs", 20259, ""});
+#endif
+}
diff --git a/xbmc/network/GUIDialogNetworkSetup.h b/xbmc/network/GUIDialogNetworkSetup.h
new file mode 100644
index 0000000..68de748
--- /dev/null
+++ b/xbmc/network/GUIDialogNetworkSetup.h
@@ -0,0 +1,74 @@
+/*
+ * 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 "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+class CGUIDialogNetworkSetup : public CGUIDialogSettingsManualBase
+{
+public:
+ //! \brief A structure encapsulating properties of a supported protocol.
+ struct Protocol
+ {
+ bool supportPath; //!< Protocol has path in addition to server name
+ bool supportUsername; //!< Protocol uses logins
+ bool supportPassword; //!< Protocol supports passwords
+ bool supportPort; //!< Protocol supports port customization
+ bool supportBrowsing; //!< Protocol supports server browsing
+ int defaultPort; //!< Default port to use for protocol
+ std::string type; //!< URL type for protocol
+ int label; //!< String ID to use as label in dialog
+ std::string addonId; //!< Addon identifier, leaved empty if inside Kodi
+ };
+
+ CGUIDialogNetworkSetup(void);
+ ~CGUIDialogNetworkSetup(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnBack(int actionID) override;
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ static bool ShowAndGetNetworkAddress(std::string &path);
+
+ std::string ConstructPath() const;
+ bool SetPath(const std::string &path);
+ bool IsConfirmed() const override { return m_confirmed; }
+
+protected:
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override { return true; }
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+ void OnProtocolChange();
+ void OnServerBrowse();
+ void OnOK();
+ void OnCancel() override;
+ void UpdateButtons();
+ void Reset();
+
+ void UpdateAvailableProtocols();
+
+ int m_protocol; //!< Currently selected protocol
+ std::vector<Protocol> m_protocols; //!< List of available protocols
+ std::string m_server;
+ std::string m_path;
+ std::string m_username;
+ std::string m_password;
+ std::string m_port;
+
+ bool m_confirmed;
+};
diff --git a/xbmc/network/IWSDiscovery.h b/xbmc/network/IWSDiscovery.h
new file mode 100644
index 0000000..47142dc
--- /dev/null
+++ b/xbmc/network/IWSDiscovery.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021- 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 <memory>
+
+namespace WSDiscovery
+{
+class IWSDiscovery
+{
+public:
+ virtual ~IWSDiscovery() = default;
+ virtual bool StartServices() = 0;
+ virtual bool StopServices() = 0;
+ virtual bool IsRunning() = 0;
+
+ static std::unique_ptr<IWSDiscovery> GetInstance();
+};
+} // namespace WSDiscovery
diff --git a/xbmc/network/Network.cpp b/xbmc/network/Network.cpp
new file mode 100644
index 0000000..69a1513
--- /dev/null
+++ b/xbmc/network/Network.cpp
@@ -0,0 +1,525 @@
+/*
+ * 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 <netdb.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+
+#include "Network.h"
+#include "ServiceBroker.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/NetworkServices.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+#ifdef TARGET_WINDOWS
+#include "platform/win32/WIN32Util.h"
+#include "utils/CharsetConverter.h"
+#endif
+#include "utils/StringUtils.h"
+#include "utils/XTimeUtils.h"
+
+/* slightly modified in_ether taken from the etherboot project (http://sourceforge.net/projects/etherboot) */
+bool in_ether (const char *bufp, unsigned char *addr)
+{
+ if (strlen(bufp) != 17)
+ return false;
+
+ char c;
+ const char *orig;
+ unsigned char *ptr = addr;
+ unsigned val;
+
+ int i = 0;
+ orig = bufp;
+
+ while ((*bufp != '\0') && (i < 6))
+ {
+ val = 0;
+ c = *bufp++;
+
+ if (isdigit(c))
+ val = c - '0';
+ else if (c >= 'a' && c <= 'f')
+ val = c - 'a' + 10;
+ else if (c >= 'A' && c <= 'F')
+ val = c - 'A' + 10;
+ else
+ return false;
+
+ val <<= 4;
+ c = *bufp;
+ if (isdigit(c))
+ val |= c - '0';
+ else if (c >= 'a' && c <= 'f')
+ val |= c - 'a' + 10;
+ else if (c >= 'A' && c <= 'F')
+ val |= c - 'A' + 10;
+ else if (c == ':' || c == '-' || c == 0)
+ val >>= 4;
+ else
+ return false;
+
+ if (c != 0)
+ bufp++;
+
+ *ptr++ = (unsigned char) (val & 0377);
+ i++;
+
+ if (*bufp == ':' || *bufp == '-')
+ bufp++;
+ }
+
+ if (bufp - orig != 17)
+ return false;
+
+ return true;
+}
+
+CNetworkBase::CNetworkBase() :
+ m_services(new CNetworkServices())
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_NETWORKMESSAGE, SERVICES_UP, 0);
+}
+
+CNetworkBase::~CNetworkBase()
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_NETWORKMESSAGE, SERVICES_DOWN, 0);
+}
+
+int CNetworkBase::ParseHex(char *str, unsigned char *addr)
+{
+ int len = 0;
+
+ while (*str)
+ {
+ int tmp;
+ if (str[1] == 0)
+ return -1;
+ if (sscanf(str, "%02x", (unsigned int *)&tmp) != 1)
+ return -1;
+ addr[len] = tmp;
+ len++;
+ str += 2;
+ }
+
+ return len;
+}
+
+bool CNetworkBase::GetHostName(std::string& hostname)
+{
+ char hostName[128];
+ if (gethostname(hostName, sizeof(hostName)))
+ return false;
+
+#ifdef TARGET_WINDOWS
+ std::string hostStr;
+ g_charsetConverter.systemToUtf8(hostName, hostStr);
+ hostname = hostStr;
+#else
+ hostname = hostName;
+#endif
+ return true;
+}
+
+bool CNetworkBase::IsLocalHost(const std::string& hostname)
+{
+ if (hostname.empty())
+ return false;
+
+ if (StringUtils::StartsWith(hostname, "127.")
+ || (hostname == "::1")
+ || StringUtils::EqualsNoCase(hostname, "localhost"))
+ return true;
+
+ std::string myhostname;
+ if (GetHostName(myhostname)
+ && StringUtils::EqualsNoCase(hostname, myhostname))
+ return true;
+
+ std::vector<CNetworkInterface*>& ifaces = GetInterfaceList();
+ std::vector<CNetworkInterface*>::const_iterator iter = ifaces.begin();
+ while (iter != ifaces.end())
+ {
+ CNetworkInterface* iface = *iter;
+ if (iface && iface->GetCurrentIPAddress() == hostname)
+ return true;
+
+ ++iter;
+ }
+
+ return false;
+}
+
+CNetworkInterface* CNetworkBase::GetFirstConnectedInterface()
+{
+ CNetworkInterface* fallbackInterface = nullptr;
+ for (CNetworkInterface* iface : GetInterfaceList())
+ {
+ if (iface && iface->IsConnected())
+ {
+ if (!iface->GetCurrentDefaultGateway().empty())
+ return iface;
+ else if (fallbackInterface == nullptr)
+ fallbackInterface = iface;
+ }
+ }
+
+ return fallbackInterface;
+}
+
+bool CNetworkBase::HasInterfaceForIP(unsigned long address)
+{
+ unsigned long subnet;
+ unsigned long local;
+ std::vector<CNetworkInterface*>& ifaces = GetInterfaceList();
+ std::vector<CNetworkInterface*>::const_iterator iter = ifaces.begin();
+ while (iter != ifaces.end())
+ {
+ CNetworkInterface* iface = *iter;
+ if (iface && iface->IsConnected())
+ {
+ subnet = ntohl(inet_addr(iface->GetCurrentNetmask().c_str()));
+ local = ntohl(inet_addr(iface->GetCurrentIPAddress().c_str()));
+ if( (address & subnet) == (local & subnet) )
+ return true;
+ }
+ ++iter;
+ }
+
+ return false;
+}
+
+bool CNetworkBase::IsAvailable(void)
+{
+ const std::vector<CNetworkInterface*>& ifaces = GetInterfaceList();
+ return (ifaces.size() != 0);
+}
+
+bool CNetworkBase::IsConnected()
+{
+ return GetFirstConnectedInterface() != NULL;
+}
+
+void CNetworkBase::NetworkMessage(EMESSAGE message, int param)
+{
+ switch( message )
+ {
+ case SERVICES_UP:
+ CLog::Log(LOGDEBUG, "{} - Starting network services", __FUNCTION__);
+ m_services->Start();
+ break;
+
+ case SERVICES_DOWN:
+ CLog::Log(LOGDEBUG, "{} - Signaling network services to stop", __FUNCTION__);
+ m_services->Stop(false); // tell network services to stop, but don't wait for them yet
+ CLog::Log(LOGDEBUG, "{} - Waiting for network services to stop", __FUNCTION__);
+ m_services->Stop(true); // wait for network services to stop
+ break;
+ }
+}
+
+bool CNetworkBase::WakeOnLan(const char* mac)
+{
+ int i, j, packet;
+ unsigned char ethaddr[8];
+ unsigned char buf [128];
+ unsigned char *ptr;
+
+ // Fetch the hardware address
+ if (!in_ether(mac, ethaddr))
+ {
+ CLog::Log(LOGERROR, "{} - Invalid hardware address specified ({})", __FUNCTION__, mac);
+ return false;
+ }
+
+ // Setup the socket
+ if ((packet = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0)
+ {
+ CLog::Log(LOGERROR, "{} - Unable to create socket ({})", __FUNCTION__, strerror(errno));
+ return false;
+ }
+
+ // Set socket options
+ struct sockaddr_in saddr;
+ saddr.sin_family = AF_INET;
+ saddr.sin_addr.s_addr = htonl(INADDR_BROADCAST);
+ saddr.sin_port = htons(9);
+
+ unsigned int value = 1;
+ if (setsockopt (packet, SOL_SOCKET, SO_BROADCAST, (char*) &value, sizeof( unsigned int ) ) == SOCKET_ERROR)
+ {
+ CLog::Log(LOGERROR, "{} - Unable to set socket options ({})", __FUNCTION__, strerror(errno));
+ closesocket(packet);
+ return false;
+ }
+
+ // Build the magic packet (6 x 0xff + 16 x MAC address)
+ ptr = buf;
+ for (i = 0; i < 6; i++)
+ *ptr++ = 0xff;
+
+ for (j = 0; j < 16; j++)
+ for (i = 0; i < 6; i++)
+ *ptr++ = ethaddr[i];
+
+ // Send the magic packet
+ if (sendto (packet, (char *)buf, 102, 0, (struct sockaddr *)&saddr, sizeof (saddr)) < 0)
+ {
+ CLog::Log(LOGERROR, "{} - Unable to send magic packet ({})", __FUNCTION__, strerror(errno));
+ closesocket(packet);
+ return false;
+ }
+
+ closesocket(packet);
+ CLog::Log(LOGINFO, "{} - Magic packet send to '{}'", __FUNCTION__, mac);
+ return true;
+}
+
+// ping helper
+static const char* ConnectHostPort(SOCKET soc, const struct sockaddr_in& addr, struct timeval& timeOut, bool tryRead)
+{
+ // set non-blocking
+#ifdef TARGET_WINDOWS
+ u_long nonblocking = 1;
+ int result = ioctlsocket(soc, FIONBIO, &nonblocking);
+#else
+ int result = fcntl(soc, F_SETFL, fcntl(soc, F_GETFL) | O_NONBLOCK);
+#endif
+
+ if (result != 0)
+ return "set non-blocking option failed";
+
+ result = connect(soc, (const struct sockaddr *)&addr, sizeof(addr)); // non-blocking connect, will fail ..
+
+ if (result < 0)
+ {
+#ifdef TARGET_WINDOWS
+ if (WSAGetLastError() != WSAEWOULDBLOCK)
+#else
+ if (errno != EINPROGRESS)
+#endif
+ return "unexpected connect fail";
+
+ { // wait for connect to complete
+ fd_set wset;
+ FD_ZERO(&wset);
+ FD_SET(soc, &wset);
+
+ result = select(FD_SETSIZE, 0, &wset, 0, &timeOut);
+ }
+
+ if (result < 0)
+ return "select fail";
+
+ if (result == 0) // timeout
+ return ""; // no error
+
+ { // verify socket connection state
+ int err_code = -1;
+ socklen_t code_len = sizeof (err_code);
+
+ result = getsockopt(soc, SOL_SOCKET, SO_ERROR, (char*) &err_code, &code_len);
+
+ if (result != 0)
+ return "getsockopt fail";
+
+ if (err_code != 0)
+ return ""; // no error, just not connected
+ }
+ }
+
+ if (tryRead)
+ {
+ fd_set rset;
+ FD_ZERO(&rset);
+ FD_SET(soc, &rset);
+
+ result = select(FD_SETSIZE, &rset, 0, 0, &timeOut);
+
+ if (result > 0)
+ {
+ char message [32];
+
+ result = recv(soc, message, sizeof(message), 0);
+ }
+
+ if (result == 0)
+ return ""; // no reply yet
+
+ if (result < 0)
+ return "recv fail";
+ }
+
+ return 0; // success
+}
+
+bool CNetworkBase::PingHost(unsigned long ipaddr, unsigned short port, unsigned int timeOutMs, bool readability_check)
+{
+ if (port == 0) // use icmp ping
+ return PingHost (ipaddr, timeOutMs);
+
+ struct sockaddr_in addr;
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(port);
+ addr.sin_addr.s_addr = ipaddr;
+
+ SOCKET soc = socket(AF_INET, SOCK_STREAM, 0);
+
+ const char* err_msg = "invalid socket";
+
+ if (soc != INVALID_SOCKET)
+ {
+ struct timeval tmout;
+ tmout.tv_sec = timeOutMs / 1000;
+ tmout.tv_usec = (timeOutMs % 1000) * 1000;
+
+ err_msg = ConnectHostPort (soc, addr, tmout, readability_check);
+
+ (void) closesocket (soc);
+ }
+
+ if (err_msg && *err_msg)
+ {
+#ifdef TARGET_WINDOWS
+ std::string sock_err = CWIN32Util::WUSysMsg(WSAGetLastError());
+#else
+ std::string sock_err = strerror(errno);
+#endif
+
+ CLog::Log(LOGERROR, "{}({}:{}) - {} ({})", __FUNCTION__, inet_ntoa(addr.sin_addr), port,
+ err_msg, sock_err);
+ }
+
+ return err_msg == 0;
+}
+
+//creates, binds and listens tcp sockets on the desired port. Set bindLocal to
+//true to bind to localhost only.
+std::vector<SOCKET> CreateTCPServerSocket(const int port, const bool bindLocal, const int backlog, const char *callerName)
+{
+#ifdef WINSOCK_VERSION
+ int yes = 1;
+#else
+ unsigned int yes = 1;
+#endif
+
+ std::vector<SOCKET> sockets;
+ struct addrinfo* results = nullptr;
+
+ std::string sPort = std::to_string(port);
+ struct addrinfo hints = {};
+ hints.ai_family = PF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_PASSIVE;
+ hints.ai_protocol = 0;
+
+ int error = getaddrinfo(bindLocal ? "localhost" : nullptr, sPort.c_str(), &hints, &results);
+ if (error != 0)
+ return sockets;
+
+ for (struct addrinfo* result = results; result != nullptr; result = result->ai_next)
+ {
+ SOCKET sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
+ if (sock == INVALID_SOCKET)
+ continue;
+
+ setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&yes), sizeof(yes));
+ setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char*>(&yes), sizeof(yes));
+
+ if (bind(sock, result->ai_addr, result->ai_addrlen) != 0)
+ {
+ closesocket(sock);
+ CLog::Log(LOGDEBUG, "{} Server: Failed to bind {} serversocket", callerName,
+ result->ai_family == AF_INET6 ? "IPv6" : "IPv4");
+ continue;
+ }
+
+ if (listen(sock, backlog) == 0)
+ sockets.push_back(sock);
+ else
+ {
+ closesocket(sock);
+ CLog::Log(LOGERROR, "{} Server: Failed to set listen", callerName);
+ }
+ }
+ freeaddrinfo(results);
+
+ if (sockets.empty())
+ CLog::Log(LOGERROR, "{} Server: Failed to create serversocket(s)", callerName);
+
+ return sockets;
+}
+
+void CNetworkBase::WaitForNet()
+{
+ const int timeout = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_POWERMANAGEMENT_WAITFORNETWORK);
+ if (timeout <= 0)
+ return; // wait for network is disabled
+
+ // check if we have at least one network interface to wait for
+ if (!IsAvailable())
+ return;
+
+ CLog::Log(LOGINFO, "{}: Waiting for a network interface to come up (Timeout: {} s)", __FUNCTION__,
+ timeout);
+
+ const static int intervalMs = 200;
+ const int numMaxTries = (timeout * 1000) / intervalMs;
+
+ for(int i=0; i < numMaxTries; ++i)
+ {
+ if (i > 0)
+ KODI::TIME::Sleep(std::chrono::milliseconds(intervalMs));
+
+ if (IsConnected())
+ {
+ CLog::Log(LOGINFO, "{}: A network interface is up after waiting {} ms", __FUNCTION__,
+ i * intervalMs);
+ return;
+ }
+ }
+
+ CLog::Log(LOGINFO, "{}: No network interface did come up within {} s... Giving up...",
+ __FUNCTION__, timeout);
+}
+
+std::string CNetworkBase::GetIpStr(const struct sockaddr* sa)
+{
+ std::string result;
+ if (!sa)
+ return result;
+
+ char buffer[INET6_ADDRSTRLEN] = {};
+ switch (sa->sa_family)
+ {
+ case AF_INET:
+ inet_ntop(AF_INET, &reinterpret_cast<const struct sockaddr_in *>(sa)->sin_addr, buffer, INET_ADDRSTRLEN);
+ break;
+ case AF_INET6:
+ inet_ntop(AF_INET6, &reinterpret_cast<const struct sockaddr_in6 *>(sa)->sin6_addr, buffer, INET6_ADDRSTRLEN);
+ break;
+ default:
+ return result;
+ }
+
+ result = buffer;
+ return result;
+}
+
+std::string CNetworkBase::GetMaskByPrefixLength(uint8_t prefixLength)
+{
+ if (prefixLength > 32)
+ return "";
+
+ struct sockaddr_in sa;
+ sa.sin_family = AF_INET;
+ sa.sin_addr.s_addr = htonl(~((1 << (32u - prefixLength)) - 1));;
+ return CNetworkBase::GetIpStr(reinterpret_cast<struct sockaddr*>(&sa));
+}
diff --git a/xbmc/network/Network.h b/xbmc/network/Network.h
new file mode 100644
index 0000000..e996e12
--- /dev/null
+++ b/xbmc/network/Network.h
@@ -0,0 +1,123 @@
+/*
+ * 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 <string>
+#include <vector>
+
+#include "settings/lib/ISettingCallback.h"
+
+#include "PlatformDefs.h"
+
+class CNetworkInterface
+{
+public:
+ virtual ~CNetworkInterface() = default;
+
+ virtual bool IsEnabled(void) const = 0;
+ virtual bool IsConnected(void) const = 0;
+
+ virtual std::string GetMacAddress(void) const = 0;
+ virtual void GetMacAddressRaw(char rawMac[6]) const = 0;
+
+ virtual bool GetHostMacAddress(unsigned long host, std::string& mac) const = 0;
+
+ virtual std::string GetCurrentIPAddress() const = 0;
+ virtual std::string GetCurrentNetmask() const = 0;
+ virtual std::string GetCurrentDefaultGateway(void) const = 0;
+};
+
+class CSettings;
+class CNetworkServices;
+struct sockaddr;
+
+class CNetworkBase
+{
+public:
+ enum EMESSAGE
+ {
+ SERVICES_UP,
+ SERVICES_DOWN
+ };
+
+ static std::unique_ptr<CNetworkBase> GetNetwork();
+
+ CNetworkBase();
+ virtual ~CNetworkBase();
+
+ // Get network services
+ CNetworkServices& GetServices() { return *m_services; }
+
+ // Return our hostname
+ virtual bool GetHostName(std::string& hostname);
+
+ // Return the list of interfaces
+ virtual std::vector<CNetworkInterface*>& GetInterfaceList(void) = 0;
+
+ // Return the first interface which is active
+ virtual CNetworkInterface* GetFirstConnectedInterface(void);
+
+ // Return true if there is a interface for the same network as address
+ bool HasInterfaceForIP(unsigned long address);
+
+ // Return true if there's at least one defined network interface
+ bool IsAvailable(void);
+
+ // Return true if there's at least one interface which is connected
+ bool IsConnected(void);
+
+ // Return true if the magic packet was send
+ bool WakeOnLan(const char* mac);
+
+ // Return true if host replies to ping
+ bool PingHost(unsigned long host,
+ unsigned short port,
+ unsigned int timeout_ms = 2000,
+ bool readability_check = false);
+ virtual bool PingHost(unsigned long host, unsigned int timeout_ms = 2000) = 0;
+
+ // Get/set the nameserver(s)
+ virtual std::vector<std::string> GetNameServers(void) = 0;
+
+ // callback from application controlled thread to handle any setup
+ void NetworkMessage(EMESSAGE message, int param);
+
+ static int ParseHex(char* str, unsigned char* addr);
+
+ // Return true if given name or ip address corresponds to localhost
+ bool IsLocalHost(const std::string& hostname);
+
+ // Waits for the first network interface to become available
+ void WaitForNet();
+
+ /*!
+ \brief IPv6/IPv4 compatible conversion of host IP address
+ \param struct sockaddr
+ \return Function converts binary structure sockaddr to std::string.
+ It can read sockaddr_in and sockaddr_in6, cast as (sockaddr*).
+ IPv4 address is returned in the format x.x.x.x (where x is 0-255),
+ IPv6 address is returned in it's canonised form.
+ On error (or no IPv6/v4 valid input) empty string is returned.
+ */
+ static std::string GetIpStr(const sockaddr* sa);
+
+ /*!
+ \brief convert prefix length of IPv4 address to IP mask representation
+ \param prefix length
+ \return
+ */
+ static std::string GetMaskByPrefixLength(uint8_t prefixLength);
+
+ std::unique_ptr<CNetworkServices> m_services;
+};
+
+//creates, binds and listens tcp sockets on the desired port. Set bindLocal to
+//true to bind to localhost only.
+std::vector<SOCKET> CreateTCPServerSocket(const int port, const bool bindLocal, const int backlog, const char *callerName);
+
diff --git a/xbmc/network/NetworkServices.cpp b/xbmc/network/NetworkServices.cpp
new file mode 100644
index 0000000..4009433
--- /dev/null
+++ b/xbmc/network/NetworkServices.cpp
@@ -0,0 +1,1267 @@
+/*
+ * 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 "NetworkServices.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/json-rpc/JSONRPC.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "network/EventServer.h"
+#include "network/Network.h"
+#include "network/TCPServer.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/RssManager.h"
+#include "utils/SystemInfo.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <utility>
+
+#ifdef TARGET_LINUX
+#include "Util.h"
+#endif
+#ifdef HAS_AIRPLAY
+#include "network/AirPlayServer.h"
+#endif // HAS_AIRPLAY
+
+#ifdef HAS_AIRTUNES
+#include "network/AirTunesServer.h"
+#endif // HAS_AIRTUNES
+
+#ifdef HAS_ZEROCONF
+#include "network/Zeroconf.h"
+#endif // HAS_ZEROCONF
+
+#ifdef HAS_UPNP
+#include "network/upnp/UPnP.h"
+#endif // HAS_UPNP
+
+#ifdef HAS_WEB_SERVER
+#include "network/WebServer.h"
+#include "network/httprequesthandler/HTTPImageHandler.h"
+#include "network/httprequesthandler/HTTPImageTransformationHandler.h"
+#include "network/httprequesthandler/HTTPVfsHandler.h"
+#include "network/httprequesthandler/HTTPJsonRpcHandler.h"
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+#include "network/httprequesthandler/HTTPPythonHandler.h"
+#endif
+#include "network/httprequesthandler/HTTPWebinterfaceHandler.h"
+#include "network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h"
+#endif // HAS_WEB_INTERFACE
+#endif // HAS_WEB_SERVER
+
+#if defined(HAS_FILESYSTEM_SMB)
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/network/WSDiscoveryWin32.h"
+#else // defined(TARGET_POSIX)
+#include "platform/posix/filesystem/SMBWSDiscovery.h"
+#endif
+#endif
+
+#if defined(TARGET_DARWIN_OSX)
+#include "platform/darwin/osx/XBMCHelper.h"
+#endif
+
+using namespace KODI::MESSAGING;
+using namespace JSONRPC;
+using namespace EVENTSERVER;
+#ifdef HAS_UPNP
+using namespace UPNP;
+#endif // HAS_UPNP
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+CNetworkServices::CNetworkServices()
+#ifdef HAS_WEB_SERVER
+ : m_webserver(*new CWebServer),
+ m_httpImageHandler(*new CHTTPImageHandler),
+ m_httpImageTransformationHandler(*new CHTTPImageTransformationHandler),
+ m_httpVfsHandler(*new CHTTPVfsHandler),
+ m_httpJsonRpcHandler(*new CHTTPJsonRpcHandler)
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+ , m_httpPythonHandler(*new CHTTPPythonHandler)
+#endif
+ , m_httpWebinterfaceHandler(*new CHTTPWebinterfaceHandler)
+ , m_httpWebinterfaceAddonsHandler(*new CHTTPWebinterfaceAddonsHandler)
+#endif // HAS_WEB_INTERFACE
+#endif // HAS_WEB_SERVER
+{
+#ifdef HAS_WEB_SERVER
+ m_webserver.RegisterRequestHandler(&m_httpImageHandler);
+ m_webserver.RegisterRequestHandler(&m_httpImageTransformationHandler);
+ m_webserver.RegisterRequestHandler(&m_httpVfsHandler);
+ m_webserver.RegisterRequestHandler(&m_httpJsonRpcHandler);
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+ m_webserver.RegisterRequestHandler(&m_httpPythonHandler);
+#endif
+ m_webserver.RegisterRequestHandler(&m_httpWebinterfaceAddonsHandler);
+ m_webserver.RegisterRequestHandler(&m_httpWebinterfaceHandler);
+#endif // HAS_WEB_INTERFACE
+#endif // HAS_WEB_SERVER
+ std::set<std::string> settingSet{
+ CSettings::SETTING_SERVICES_WEBSERVER,
+ CSettings::SETTING_SERVICES_WEBSERVERPORT,
+ CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION,
+ CSettings::SETTING_SERVICES_WEBSERVERUSERNAME,
+ CSettings::SETTING_SERVICES_WEBSERVERPASSWORD,
+ CSettings::SETTING_SERVICES_WEBSERVERSSL,
+ CSettings::SETTING_SERVICES_ZEROCONF,
+ CSettings::SETTING_SERVICES_AIRPLAY,
+ CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL,
+ CSettings::SETTING_SERVICES_AIRPLAYVIDEOSUPPORT,
+ CSettings::SETTING_SERVICES_USEAIRPLAYPASSWORD,
+ CSettings::SETTING_SERVICES_AIRPLAYPASSWORD,
+ CSettings::SETTING_SERVICES_UPNP,
+ CSettings::SETTING_SERVICES_UPNPSERVER,
+ CSettings::SETTING_SERVICES_UPNPRENDERER,
+ CSettings::SETTING_SERVICES_UPNPCONTROLLER,
+ CSettings::SETTING_SERVICES_ESENABLED,
+ CSettings::SETTING_SERVICES_ESPORT,
+ CSettings::SETTING_SERVICES_ESALLINTERFACES,
+ CSettings::SETTING_SERVICES_ESINITIALDELAY,
+ CSettings::SETTING_SERVICES_ESCONTINUOUSDELAY,
+ CSettings::SETTING_SMB_WINSSERVER,
+ CSettings::SETTING_SMB_WORKGROUP,
+ CSettings::SETTING_SMB_MINPROTOCOL,
+ CSettings::SETTING_SMB_MAXPROTOCOL,
+ CSettings::SETTING_SMB_LEGACYSECURITY,
+ CSettings::SETTING_SERVICES_WSDISCOVERY,
+ };
+ m_settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ m_settings->GetSettingsManager()->RegisterCallback(this, settingSet);
+}
+
+CNetworkServices::~CNetworkServices()
+{
+ m_settings->GetSettingsManager()->UnregisterCallback(this);
+#ifdef HAS_WEB_SERVER
+ m_webserver.UnregisterRequestHandler(&m_httpImageHandler);
+ delete &m_httpImageHandler;
+ m_webserver.UnregisterRequestHandler(&m_httpImageTransformationHandler);
+ delete &m_httpImageTransformationHandler;
+ m_webserver.UnregisterRequestHandler(&m_httpVfsHandler);
+ delete &m_httpVfsHandler;
+ m_webserver.UnregisterRequestHandler(&m_httpJsonRpcHandler);
+ delete &m_httpJsonRpcHandler;
+ CJSONRPC::Cleanup();
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+ m_webserver.UnregisterRequestHandler(&m_httpPythonHandler);
+ delete &m_httpPythonHandler;
+#endif
+ m_webserver.UnregisterRequestHandler(&m_httpWebinterfaceAddonsHandler);
+ delete &m_httpWebinterfaceAddonsHandler;
+ m_webserver.UnregisterRequestHandler(&m_httpWebinterfaceHandler);
+ delete &m_httpWebinterfaceHandler;
+#endif // HAS_WEB_INTERFACE
+ delete &m_webserver;
+#endif // HAS_WEB_SERVER
+}
+
+bool CNetworkServices::OnSettingChanging(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return false;
+
+ const std::string &settingId = setting->GetId();
+#ifdef HAS_WEB_SERVER
+ // Ask user to confirm disabling the authentication requirement, but not when the configuration
+ // would be invalid when authentication was enabled (meaning that the change was triggered
+ // automatically)
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION &&
+ !std::static_pointer_cast<const CSettingBool>(setting)->GetValue() &&
+ (!m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER) ||
+ (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER) &&
+ !m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD).empty())) &&
+ HELPERS::ShowYesNoDialogText(19098, 36634) != DialogResponse::CHOICE_YES)
+ {
+ // Leave it as-is
+ return false;
+ }
+
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVER ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERPORT ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERSSL ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERUSERNAME ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERPASSWORD)
+ {
+ if (IsWebserverRunning() && !StopWebserver())
+ return false;
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER))
+ {
+ // Prevent changing to an invalid configuration
+ if ((settingId == CSettings::SETTING_SERVICES_WEBSERVER ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERPASSWORD) &&
+ m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION) &&
+ m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD).empty())
+ {
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION)
+ {
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{36636});
+ }
+ else
+ {
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{36635});
+ }
+ return false;
+ }
+
+ // Ask for confirmation when enabling the web server
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVER &&
+ HELPERS::ShowYesNoDialogText(19098, 36632) != DialogResponse::CHOICE_YES)
+ {
+ // Revert change, do not start server
+ return false;
+ }
+
+ if (!StartWebserver())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33101}, CVariant{33100});
+ return false;
+ }
+ }
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_ESPORT ||
+ settingId == CSettings::SETTING_SERVICES_WEBSERVERPORT)
+ return ValidatePort(std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ else
+#endif // HAS_WEB_SERVER
+
+#ifdef HAS_ZEROCONF
+ if (settingId == CSettings::SETTING_SERVICES_ZEROCONF)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ return StartZeroconf();
+#ifdef HAS_AIRPLAY
+ else
+ {
+ // cannot disable
+ if (IsAirPlayServerRunning() || IsAirTunesServerRunning())
+ {
+ HELPERS::ShowOKDialogText(CVariant{1259}, CVariant{34303});
+ return false;
+ }
+
+ return StopZeroconf();
+ }
+#endif // HAS_AIRPLAY
+ }
+ else
+#endif // HAS_ZEROCONF
+
+#ifdef HAS_AIRPLAY
+ if (settingId == CSettings::SETTING_SERVICES_AIRPLAY)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+#ifdef HAS_ZEROCONF
+ // AirPlay needs zeroconf
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_ZEROCONF))
+ {
+ HELPERS::ShowOKDialogText(CVariant{1273}, CVariant{34302});
+ return false;
+ }
+#endif //HAS_ZEROCONF
+
+ // note - airtunesserver has to start before airplay server (ios7 client detection bug)
+#ifdef HAS_AIRTUNES
+ if (!StartAirTunesServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{1274}, CVariant{33100});
+ return false;
+ }
+#endif //HAS_AIRTUNES
+
+ if (!StartAirPlayServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{1273}, CVariant{33100});
+ return false;
+ }
+ }
+ else
+ {
+ bool ret = true;
+#ifdef HAS_AIRTUNES
+ if (!StopAirTunesServer(true))
+ ret = false;
+#endif //HAS_AIRTUNES
+
+ if (!StopAirPlayServer(true))
+ ret = false;
+
+ if (!ret)
+ return false;
+ }
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_AIRPLAYVIDEOSUPPORT)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ if (!StartAirPlayServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{1273}, CVariant{33100});
+ return false;
+ }
+ }
+ else
+ {
+ if (!StopAirPlayServer(true))
+ return false;
+ }
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_AIRPLAYPASSWORD ||
+ settingId == CSettings::SETTING_SERVICES_USEAIRPLAYPASSWORD)
+ {
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAY))
+ return false;
+
+ if (!CAirPlayServer::SetCredentials(m_settings->GetBool(CSettings::SETTING_SERVICES_USEAIRPLAYPASSWORD),
+ m_settings->GetString(CSettings::SETTING_SERVICES_AIRPLAYPASSWORD)))
+ return false;
+ }
+ else
+#endif //HAS_AIRPLAY
+
+#ifdef HAS_UPNP
+ if (settingId == CSettings::SETTING_SERVICES_UPNP)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ StartUPnPClient();
+ StartUPnPController();
+ StartUPnPServer();
+ StartUPnPRenderer();
+ }
+ else
+ {
+ StopUPnPRenderer();
+ StopUPnPServer();
+ StopUPnPController();
+ StopUPnPClient();
+ }
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_UPNPSERVER)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ if (!StartUPnPServer())
+ return false;
+
+ // always stop and restart the client and controller if necessary
+ StopUPnPClient();
+ StopUPnPController();
+ StartUPnPClient();
+ StartUPnPController();
+ }
+ else
+ return StopUPnPServer();
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_UPNPRENDERER)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ return StartUPnPRenderer();
+ else
+ return StopUPnPRenderer();
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_UPNPCONTROLLER)
+ {
+ // always stop and restart
+ StopUPnPController();
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ return StartUPnPController();
+ }
+ else
+#endif // HAS_UPNP
+
+ if (settingId == CSettings::SETTING_SERVICES_ESENABLED)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ bool result = true;
+ if (!StartEventServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33102}, CVariant{33100});
+ result = false;
+ }
+
+ if (!StartJSONRPCServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33103}, CVariant{33100});
+ result = false;
+ }
+ return result;
+ }
+ else
+ {
+ bool result = true;
+ result = StopEventServer(true, true);
+ result &= StopJSONRPCServer(false);
+ return result;
+ }
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_ESPORT)
+ {
+ // restart eventserver without asking user
+ if (!StopEventServer(true, false))
+ return false;
+
+ if (!StartEventServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33102}, CVariant{33100});
+ return false;
+ }
+
+#if defined(TARGET_DARWIN_OSX)
+ // reconfigure XBMCHelper for port changes
+ XBMCHelper::GetInstance().Configure();
+#endif // TARGET_DARWIN_OSX
+ }
+ else if (settingId == CSettings::SETTING_SERVICES_ESALLINTERFACES)
+ {
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESALLINTERFACES) &&
+ HELPERS::ShowYesNoDialogText(19098, 36633) != DialogResponse::CHOICE_YES)
+ {
+ // Revert change, do not start server
+ return false;
+ }
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ {
+ if (!StopEventServer(true, true))
+ return false;
+
+ if (!StartEventServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33102}, CVariant{33100});
+ return false;
+ }
+ }
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ {
+ if (!StopJSONRPCServer(true))
+ return false;
+
+ if (!StartJSONRPCServer())
+ {
+ HELPERS::ShowOKDialogText(CVariant{33103}, CVariant{33100});
+ return false;
+ }
+ }
+ }
+
+ else if (settingId == CSettings::SETTING_SERVICES_ESINITIALDELAY ||
+ settingId == CSettings::SETTING_SERVICES_ESCONTINUOUSDELAY)
+ {
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ return RefreshEventServer();
+ }
+
+#if defined(HAS_FILESYSTEM_SMB)
+ else if (settingId == CSettings::SETTING_SERVICES_WSDISCOVERY)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue())
+ {
+ if (!StartWSDiscovery())
+ return false;
+ }
+ else
+ return StopWSDiscovery();
+ }
+#endif // HAS_FILESYSTEM_SMB
+
+ return true;
+}
+
+void CNetworkServices::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_SMB_WINSSERVER ||
+ settingId == CSettings::SETTING_SMB_WORKGROUP ||
+ settingId == CSettings::SETTING_SMB_MINPROTOCOL ||
+ settingId == CSettings::SETTING_SMB_MAXPROTOCOL ||
+ settingId == CSettings::SETTING_SMB_LEGACYSECURITY)
+ {
+ // okey we really don't need to restart, only deinit samba, but that could be damn hard if something is playing
+ //! @todo - General way of handling setting changes that require restart
+ if (HELPERS::ShowYesNoDialogText(CVariant{14038}, CVariant{14039}) ==
+ DialogResponse::CHOICE_YES)
+ {
+ m_settings->Save();
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_RESTARTAPP);
+ }
+ }
+}
+
+bool CNetworkServices::OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode)
+{
+ if (setting == NULL)
+ return false;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVERUSERNAME)
+ {
+ // if webserverusername is xbmc and pw is not empty we treat it as altered
+ // and don't change the username to kodi - part of rebrand
+ if (m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERUSERNAME) == "xbmc" &&
+ !m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD).empty())
+ return true;
+ }
+ if (settingId == CSettings::SETTING_SERVICES_WEBSERVERPORT)
+ {
+ // if webserverport is default but webserver is activated then treat it as altered
+ // and don't change the port to new value
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER))
+ return true;
+ }
+ return false;
+}
+
+void CNetworkServices::Start()
+{
+ StartZeroconf();
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ StartUPnP();
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED) && !StartEventServer())
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, g_localizeStrings.Get(33102), g_localizeStrings.Get(33100));
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED) && !StartJSONRPCServer())
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, g_localizeStrings.Get(33103), g_localizeStrings.Get(33100));
+
+#ifdef HAS_WEB_SERVER
+ // Start web server after eventserver and JSON-RPC server, so users can use these interfaces
+ // to confirm the warning message below if it is shown
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER))
+ {
+ // services.webserverauthentication setting was added in Kodi v18 and requires a valid password
+ // to be set, but on upgrade the setting will be activated automatically regardless of whether
+ // a password was set before -> this can lead to an invalid configuration
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION) &&
+ m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD).empty())
+ {
+ // Alert user to new default security settings in new Kodi version
+ HELPERS::ShowOKDialogText(33101, 33104);
+ // Fix settings: Disable web server
+ m_settings->SetBool(CSettings::SETTING_SERVICES_WEBSERVER, false);
+ // Bring user to settings screen where authentication can be configured properly
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(
+ WINDOW_SETTINGS_SERVICE, std::vector<std::string>{"services.webserverauthentication"});
+ }
+ // Only try to start server if configuration is OK
+ else if (!StartWebserver())
+ CGUIDialogKaiToast::QueueNotification(
+ CGUIDialogKaiToast::Warning, g_localizeStrings.Get(33101), g_localizeStrings.Get(33100));
+ }
+#endif // HAS_WEB_SERVER
+
+ // note - airtunesserver has to start before airplay server (ios7 client detection bug)
+ StartAirTunesServer();
+ StartAirPlayServer();
+ StartRss();
+ StartWSDiscovery();
+}
+
+void CNetworkServices::Stop(bool bWait)
+{
+ if (bWait)
+ {
+ StopUPnP(bWait);
+ StopZeroconf();
+ StopWebserver();
+ StopRss();
+ }
+
+ StopEventServer(bWait, false);
+ StopJSONRPCServer(bWait);
+ StopAirPlayServer(bWait);
+ StopAirTunesServer(bWait);
+ StopWSDiscovery();
+}
+
+bool CNetworkServices::StartServer(enum ESERVERS server, bool start)
+{
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return false;
+
+ auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (!settings)
+ return false;
+
+ bool ret = false;
+ switch (server)
+ {
+ case ES_WEBSERVER:
+ // the callback will take care of starting/stopping webserver
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_WEBSERVER, start);
+ break;
+
+ case ES_AIRPLAYSERVER:
+ // the callback will take care of starting/stopping airplay
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_AIRPLAY, start);
+ break;
+
+ case ES_JSONRPCSERVER:
+ // the callback will take care of starting/stopping jsonrpc server
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_ESENABLED, start);
+ break;
+
+ case ES_UPNPSERVER:
+ // the callback will take care of starting/stopping upnp server
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_UPNPSERVER, start);
+ break;
+
+ case ES_UPNPRENDERER:
+ // the callback will take care of starting/stopping upnp renderer
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_UPNPRENDERER, start);
+ break;
+
+ case ES_EVENTSERVER:
+ // the callback will take care of starting/stopping event server
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_ESENABLED, start);
+ break;
+
+ case ES_ZEROCONF:
+ // the callback will take care of starting/stopping zeroconf
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_ZEROCONF, start);
+ break;
+
+ case ES_WSDISCOVERY:
+ // the callback will take care of starting/stopping WS-Discovery
+ ret = settings->SetBool(CSettings::SETTING_SERVICES_WSDISCOVERY, start);
+ break;
+
+ default:
+ ret = false;
+ break;
+ }
+ settings->Save();
+
+ return ret;
+}
+
+bool CNetworkServices::StartWebserver()
+{
+#ifdef HAS_WEB_SERVER
+ if (!CServiceBroker::GetNetwork().IsAvailable())
+ return false;
+
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVER))
+ return false;
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION) &&
+ m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD).empty())
+ {
+ CLog::Log(LOGERROR, "Tried to start webserver with invalid configuration (authentication "
+ "enabled, but no password set");
+ return false;
+ }
+
+ int webPort = m_settings->GetInt(CSettings::SETTING_SERVICES_WEBSERVERPORT);
+ if (!ValidatePort(webPort))
+ {
+ CLog::Log(LOGERROR, "Cannot start Web Server on port {}", webPort);
+ return false;
+ }
+
+ if (IsWebserverRunning())
+ return true;
+
+ std::string username;
+ std::string password;
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION))
+ {
+ username = m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERUSERNAME);
+ password = m_settings->GetString(CSettings::SETTING_SERVICES_WEBSERVERPASSWORD);
+ }
+
+ if (!m_webserver.Start(webPort, username, password))
+ return false;
+
+#ifdef HAS_ZEROCONF
+ std::vector<std::pair<std::string, std::string> > txt;
+ txt.emplace_back("txtvers", "1");
+ txt.emplace_back("uuid", CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_SERVICES_DEVICEUUID));
+
+ // publish web frontend and API services
+#ifdef HAS_WEB_INTERFACE
+ CZeroconf::GetInstance()->PublishService("servers.webserver", "_http._tcp", CSysInfo::GetDeviceName(), webPort, txt);
+#endif // HAS_WEB_INTERFACE
+ CZeroconf::GetInstance()->PublishService("servers.jsonrpc-http", "_xbmc-jsonrpc-h._tcp", CSysInfo::GetDeviceName(), webPort, txt);
+#endif // HAS_ZEROCONF
+
+ return true;
+#endif // HAS_WEB_SERVER
+ return false;
+}
+
+bool CNetworkServices::IsWebserverRunning()
+{
+#ifdef HAS_WEB_SERVER
+ return m_webserver.IsStarted();
+#endif // HAS_WEB_SERVER
+ return false;
+}
+
+bool CNetworkServices::StopWebserver()
+{
+#ifdef HAS_WEB_SERVER
+ if (!IsWebserverRunning())
+ return true;
+
+ if (!m_webserver.Stop() || m_webserver.IsStarted())
+ {
+ CLog::Log(LOGWARNING, "Webserver: Failed to stop.");
+ return false;
+ }
+
+#ifdef HAS_ZEROCONF
+#ifdef HAS_WEB_INTERFACE
+ CZeroconf::GetInstance()->RemoveService("servers.webserver");
+#endif // HAS_WEB_INTERFACE
+ CZeroconf::GetInstance()->RemoveService("servers.jsonrpc-http");
+#endif // HAS_ZEROCONF
+
+ return true;
+#endif // HAS_WEB_SERVER
+ return false;
+}
+
+bool CNetworkServices::StartAirPlayServer()
+{
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAYVIDEOSUPPORT))
+ return true;
+
+#ifdef HAS_AIRPLAY
+ if (!CServiceBroker::GetNetwork().IsAvailable() || !m_settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAY))
+ return false;
+
+ if (IsAirPlayServerRunning())
+ return true;
+
+ if (!CAirPlayServer::StartServer(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_airPlayPort, true))
+ return false;
+
+ if (!CAirPlayServer::SetCredentials(m_settings->GetBool(CSettings::SETTING_SERVICES_USEAIRPLAYPASSWORD),
+ m_settings->GetString(CSettings::SETTING_SERVICES_AIRPLAYPASSWORD)))
+ return false;
+
+#ifdef HAS_ZEROCONF
+ std::vector<std::pair<std::string, std::string> > txt;
+ CNetworkInterface* iface = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+ txt.emplace_back("deviceid", iface != nullptr ? iface->GetMacAddress() : "FF:FF:FF:FF:FF:F2");
+ txt.emplace_back("model", "Xbmc,1");
+ txt.emplace_back("srcvers", AIRPLAY_SERVER_VERSION_STR);
+
+ // for ios8 clients we need to announce mirroring support
+ // else we won't get video urls anymore.
+ // We also announce photo caching support (as it seems faster and
+ // we have implemented it anyways).
+ txt.emplace_back("features", "0x20F7");
+
+ CZeroconf::GetInstance()->PublishService("servers.airplay", "_airplay._tcp", CSysInfo::GetDeviceName(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_airPlayPort, txt);
+#endif // HAS_ZEROCONF
+
+ return true;
+#endif // HAS_AIRPLAY
+ return false;
+}
+
+bool CNetworkServices::IsAirPlayServerRunning()
+{
+#ifdef HAS_AIRPLAY
+ return CAirPlayServer::IsRunning();
+#endif // HAS_AIRPLAY
+ return false;
+}
+
+bool CNetworkServices::StopAirPlayServer(bool bWait)
+{
+#ifdef HAS_AIRPLAY
+ if (!IsAirPlayServerRunning())
+ return true;
+
+ CAirPlayServer::StopServer(bWait);
+
+#ifdef HAS_ZEROCONF
+ CZeroconf::GetInstance()->RemoveService("servers.airplay");
+#endif // HAS_ZEROCONF
+
+ return true;
+#endif // HAS_AIRPLAY
+ return false;
+}
+
+bool CNetworkServices::StartAirTunesServer()
+{
+#ifdef HAS_AIRTUNES
+ if (!CServiceBroker::GetNetwork().IsAvailable() || !m_settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAY))
+ return false;
+
+ if (IsAirTunesServerRunning())
+ return true;
+
+ if (!CAirTunesServer::StartServer(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_airTunesPort, true,
+ m_settings->GetBool(CSettings::SETTING_SERVICES_USEAIRPLAYPASSWORD),
+ m_settings->GetString(CSettings::SETTING_SERVICES_AIRPLAYPASSWORD)))
+ {
+ CLog::Log(LOGERROR, "Failed to start AirTunes Server");
+ return false;
+ }
+
+ return true;
+#endif // HAS_AIRTUNES
+ return false;
+}
+
+bool CNetworkServices::IsAirTunesServerRunning()
+{
+#ifdef HAS_AIRTUNES
+ return CAirTunesServer::IsRunning();
+#endif // HAS_AIRTUNES
+ return false;
+}
+
+bool CNetworkServices::StopAirTunesServer(bool bWait)
+{
+#ifdef HAS_AIRTUNES
+ if (!IsAirTunesServerRunning())
+ return true;
+
+ CAirTunesServer::StopServer(bWait);
+ return true;
+#endif // HAS_AIRTUNES
+ return false;
+}
+
+bool CNetworkServices::StartJSONRPCServer()
+{
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ return false;
+
+ if (IsJSONRPCServerRunning())
+ return true;
+
+ if (!CTCPServer::StartServer(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_jsonTcpPort, m_settings->GetBool(CSettings::SETTING_SERVICES_ESALLINTERFACES)))
+ return false;
+
+#ifdef HAS_ZEROCONF
+ std::vector<std::pair<std::string, std::string> > txt;
+ txt.emplace_back("txtvers", "1");
+ txt.emplace_back("uuid", CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_SERVICES_DEVICEUUID));
+
+ CZeroconf::GetInstance()->PublishService("servers.jsonrpc-tpc", "_xbmc-jsonrpc._tcp", CSysInfo::GetDeviceName(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_jsonTcpPort, txt);
+#endif // HAS_ZEROCONF
+
+ return true;
+}
+
+bool CNetworkServices::IsJSONRPCServerRunning()
+{
+ return CTCPServer::IsRunning();
+}
+
+bool CNetworkServices::StopJSONRPCServer(bool bWait)
+{
+ if (!IsJSONRPCServerRunning())
+ return true;
+
+ CTCPServer::StopServer(bWait);
+
+#ifdef HAS_ZEROCONF
+ CZeroconf::GetInstance()->RemoveService("servers.jsonrpc-tcp");
+#endif // HAS_ZEROCONF
+
+ return true;
+}
+
+bool CNetworkServices::StartEventServer()
+{
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ return false;
+
+ if (IsEventServerRunning())
+ return true;
+
+ CEventServer* server = CEventServer::GetInstance();
+ if (!server)
+ {
+ CLog::Log(LOGERROR, "ES: Out of memory");
+ return false;
+ }
+
+ server->StartServer();
+
+ return true;
+}
+
+bool CNetworkServices::IsEventServerRunning()
+{
+ return CEventServer::GetInstance()->Running();
+}
+
+bool CNetworkServices::StopEventServer(bool bWait, bool promptuser)
+{
+ if (!IsEventServerRunning())
+ return true;
+
+ CEventServer* server = CEventServer::GetInstance();
+ if (!server)
+ {
+ CLog::Log(LOGERROR, "ES: Out of memory");
+ return false;
+ }
+
+ if (promptuser)
+ {
+ if (server->GetNumberOfClients() > 0)
+ {
+ if (HELPERS::ShowYesNoDialogText(CVariant{13140}, CVariant{13141}, CVariant{""}, CVariant{""},
+ 10000) != DialogResponse::CHOICE_YES)
+ {
+ CLog::Log(LOGINFO, "ES: Not stopping event server");
+ return false;
+ }
+ }
+ CLog::Log(LOGINFO, "ES: Stopping event server with confirmation");
+
+ CEventServer::GetInstance()->StopServer(true);
+ }
+ else
+ {
+ if (!bWait)
+ CLog::Log(LOGINFO, "ES: Stopping event server");
+
+ CEventServer::GetInstance()->StopServer(bWait);
+ }
+
+ return true;
+}
+
+bool CNetworkServices::RefreshEventServer()
+{
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_ESENABLED))
+ return false;
+
+ if (!IsEventServerRunning())
+ return false;
+
+ CEventServer::GetInstance()->RefreshSettings();
+ return true;
+}
+
+bool CNetworkServices::StartUPnP()
+{
+ bool ret = false;
+#ifdef HAS_UPNP
+ ret |= StartUPnPClient();
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPSERVER))
+ {
+ ret |= StartUPnPServer();
+ }
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPCONTROLLER))
+ {
+ ret |= StartUPnPController();
+ }
+
+ if (m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPRENDERER))
+ {
+ ret |= StartUPnPRenderer();
+ }
+#endif // HAS_UPNP
+ return ret;
+}
+
+bool CNetworkServices::StopUPnP(bool bWait)
+{
+#ifdef HAS_UPNP
+ if (!CUPnP::IsInstantiated())
+ return true;
+
+ CLog::Log(LOGINFO, "stopping upnp");
+ CUPnP::ReleaseInstance(bWait);
+
+ return true;
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StartUPnPClient()
+{
+#ifdef HAS_UPNP
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ return false;
+
+ CLog::Log(LOGINFO, "starting upnp client");
+ CUPnP::GetInstance()->StartClient();
+ return IsUPnPClientRunning();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::IsUPnPClientRunning()
+{
+#ifdef HAS_UPNP
+ return CUPnP::GetInstance()->IsClientStarted();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StopUPnPClient()
+{
+#ifdef HAS_UPNP
+ if (!IsUPnPClientRunning())
+ return true;
+
+ CLog::Log(LOGINFO, "stopping upnp client");
+ CUPnP::GetInstance()->StopClient();
+
+ return true;
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StartUPnPController()
+{
+#ifdef HAS_UPNP
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPCONTROLLER) ||
+ !m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPSERVER) ||
+ !m_settings->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ return false;
+
+ CLog::Log(LOGINFO, "starting upnp controller");
+ CUPnP::GetInstance()->StartController();
+ return IsUPnPControllerRunning();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::IsUPnPControllerRunning()
+{
+#ifdef HAS_UPNP
+ return CUPnP::GetInstance()->IsControllerStarted();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StopUPnPController()
+{
+#ifdef HAS_UPNP
+ if (!IsUPnPControllerRunning())
+ return true;
+
+ CLog::Log(LOGINFO, "stopping upnp controller");
+ CUPnP::GetInstance()->StopController();
+
+ return true;
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StartUPnPRenderer()
+{
+#ifdef HAS_UPNP
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPRENDERER) ||
+ !m_settings->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ return false;
+
+ CLog::Log(LOGINFO, "starting upnp renderer");
+ return CUPnP::GetInstance()->StartRenderer();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::IsUPnPRendererRunning()
+{
+#ifdef HAS_UPNP
+ return CUPnP::GetInstance()->IsInstantiated();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StopUPnPRenderer()
+{
+#ifdef HAS_UPNP
+ if (!IsUPnPRendererRunning())
+ return true;
+
+ CLog::Log(LOGINFO, "stopping upnp renderer");
+ CUPnP::GetInstance()->StopRenderer();
+
+ return true;
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StartUPnPServer()
+{
+#ifdef HAS_UPNP
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_UPNPSERVER) ||
+ !m_settings->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ return false;
+
+ CLog::Log(LOGINFO, "starting upnp server");
+ return CUPnP::GetInstance()->StartServer();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::IsUPnPServerRunning()
+{
+#ifdef HAS_UPNP
+ return CUPnP::GetInstance()->IsInstantiated();
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StopUPnPServer()
+{
+#ifdef HAS_UPNP
+ if (!IsUPnPServerRunning())
+ return true;
+
+ StopUPnPController();
+
+ CLog::Log(LOGINFO, "stopping upnp server");
+ CUPnP::GetInstance()->StopServer();
+
+ return true;
+#endif // HAS_UPNP
+ return false;
+}
+
+bool CNetworkServices::StartRss()
+{
+ if (IsRssRunning())
+ return true;
+
+ CRssManager::GetInstance().Start();
+ return true;
+}
+
+bool CNetworkServices::IsRssRunning()
+{
+ return CRssManager::GetInstance().IsActive();
+}
+
+bool CNetworkServices::StopRss()
+{
+ if (!IsRssRunning())
+ return true;
+
+ CRssManager::GetInstance().Stop();
+ return true;
+}
+
+bool CNetworkServices::StartZeroconf()
+{
+#ifdef HAS_ZEROCONF
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_ZEROCONF))
+ return false;
+
+ if (IsZeroconfRunning())
+ return true;
+
+ CLog::Log(LOGINFO, "starting zeroconf publishing");
+ return CZeroconf::GetInstance()->Start();
+#endif // HAS_ZEROCONF
+ return false;
+}
+
+bool CNetworkServices::IsZeroconfRunning()
+{
+#ifdef HAS_ZEROCONF
+ return CZeroconf::GetInstance()->IsStarted();
+#endif // HAS_ZEROCONF
+ return false;
+}
+
+bool CNetworkServices::StopZeroconf()
+{
+#ifdef HAS_ZEROCONF
+ if (!IsZeroconfRunning())
+ return true;
+
+ CLog::Log(LOGINFO, "stopping zeroconf publishing");
+ CZeroconf::GetInstance()->Stop();
+
+ return true;
+#endif // HAS_ZEROCONF
+ return false;
+}
+
+bool CNetworkServices::StartWSDiscovery()
+{
+#if defined(HAS_FILESYSTEM_SMB)
+ if (!m_settings->GetBool(CSettings::SETTING_SERVICES_WSDISCOVERY))
+ return false;
+
+ if (IsWSDiscoveryRunning())
+ return true;
+
+ return CServiceBroker::GetWSDiscovery().StartServices();
+#endif // HAS_FILESYSTEM_SMB
+ return false;
+}
+
+bool CNetworkServices::IsWSDiscoveryRunning()
+{
+#if defined(HAS_FILESYSTEM_SMB)
+ return CServiceBroker::GetWSDiscovery().IsRunning();
+#endif // HAS_FILESYSTEM_SMB
+ return false;
+}
+
+bool CNetworkServices::StopWSDiscovery()
+{
+#if defined(HAS_FILESYSTEM_SMB)
+ if (!IsWSDiscoveryRunning())
+ return true;
+
+ CServiceBroker::GetWSDiscovery().StopServices();
+
+ return true;
+#endif // HAS_FILESYSTEM_SMB
+ return false;
+}
+
+bool CNetworkServices::ValidatePort(int port)
+{
+ if (port <= 0 || port > 65535)
+ return false;
+
+#ifdef TARGET_LINUX
+ if (!CUtil::CanBindPrivileged() && (port < 1024))
+ return false;
+#endif
+
+ return true;
+}
diff --git a/xbmc/network/NetworkServices.h b/xbmc/network/NetworkServices.h
new file mode 100644
index 0000000..bf8ca23
--- /dev/null
+++ b/xbmc/network/NetworkServices.h
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2013-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 CSettings;
+#ifdef HAS_WEB_SERVER
+class CWebServer;
+class CHTTPImageHandler;
+class CHTTPImageTransformationHandler;
+class CHTTPVfsHandler;
+class CHTTPJsonRpcHandler;
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+class CHTTPPythonHandler;
+#endif
+class CHTTPWebinterfaceHandler;
+class CHTTPWebinterfaceAddonsHandler;
+#endif // HAS_WEB_INTERFACE
+#endif // HAS_WEB_SERVER
+
+class CNetworkServices : public ISettingCallback
+{
+public:
+ CNetworkServices();
+ ~CNetworkServices() override;
+
+ bool OnSettingChanging(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ bool OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode) override;
+
+ void Start();
+ void Stop(bool bWait);
+
+ enum ESERVERS
+ {
+ ES_WEBSERVER = 1,
+ ES_AIRPLAYSERVER,
+ ES_JSONRPCSERVER,
+ ES_UPNPRENDERER,
+ ES_UPNPSERVER,
+ ES_EVENTSERVER,
+ ES_ZEROCONF,
+ ES_WSDISCOVERY,
+ };
+
+ bool StartServer(enum ESERVERS server, bool start);
+
+ bool StartWebserver();
+ bool IsWebserverRunning();
+ bool StopWebserver();
+
+ bool StartAirPlayServer();
+ bool IsAirPlayServerRunning();
+ bool StopAirPlayServer(bool bWait);
+ bool StartAirTunesServer();
+ bool IsAirTunesServerRunning();
+ bool StopAirTunesServer(bool bWait);
+
+ bool StartJSONRPCServer();
+ bool IsJSONRPCServerRunning();
+ bool StopJSONRPCServer(bool bWait);
+
+ bool StartEventServer();
+ bool IsEventServerRunning();
+ bool StopEventServer(bool bWait, bool promptuser);
+ bool RefreshEventServer();
+
+ bool StartUPnP();
+ bool StopUPnP(bool bWait);
+ bool StartUPnPClient();
+ bool IsUPnPClientRunning();
+ bool StopUPnPClient();
+ bool StartUPnPController();
+ bool IsUPnPControllerRunning();
+ bool StopUPnPController();
+ bool StartUPnPRenderer();
+ bool IsUPnPRendererRunning();
+ bool StopUPnPRenderer();
+ bool StartUPnPServer();
+ bool IsUPnPServerRunning();
+ bool StopUPnPServer();
+
+ bool StartRss();
+ bool IsRssRunning();
+ bool StopRss();
+
+ bool StartZeroconf();
+ bool IsZeroconfRunning();
+ bool StopZeroconf();
+
+ bool StartWSDiscovery();
+ bool IsWSDiscoveryRunning();
+ bool StopWSDiscovery();
+
+private:
+ CNetworkServices(const CNetworkServices&);
+ CNetworkServices const& operator=(CNetworkServices const&);
+
+ bool ValidatePort(int port);
+
+ // Construction parameters
+ std::shared_ptr<CSettings> m_settings;
+
+ // Network services
+#ifdef HAS_WEB_SERVER
+ CWebServer& m_webserver;
+ // Handlers
+ CHTTPImageHandler& m_httpImageHandler;
+ CHTTPImageTransformationHandler& m_httpImageTransformationHandler;
+ CHTTPVfsHandler& m_httpVfsHandler;
+ CHTTPJsonRpcHandler& m_httpJsonRpcHandler;
+#ifdef HAS_WEB_INTERFACE
+#ifdef HAS_PYTHON
+ CHTTPPythonHandler& m_httpPythonHandler;
+#endif
+ CHTTPWebinterfaceHandler& m_httpWebinterfaceHandler;
+ CHTTPWebinterfaceAddonsHandler& m_httpWebinterfaceAddonsHandler;
+#endif
+#endif
+};
diff --git a/xbmc/network/Socket.cpp b/xbmc/network/Socket.cpp
new file mode 100644
index 0000000..f25e776
--- /dev/null
+++ b/xbmc/network/Socket.cpp
@@ -0,0 +1,329 @@
+/*
+ * Socket classes
+ * Copyright (c) 2008 d4rk
+ * Copyright (C) 2008-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 "Socket.h"
+
+#include "utils/ScopeGuard.h"
+#include "utils/log.h"
+
+#include <vector>
+
+using namespace SOCKETS;
+
+#ifdef WINSOCK_VERSION
+#define close closesocket
+#endif
+
+/**********************************************************************/
+/* CPosixUDPSocket */
+/**********************************************************************/
+
+bool CPosixUDPSocket::Bind(bool localOnly, int port, int range)
+{
+ // close any existing sockets
+ Close();
+
+ // If we can, create a socket that works with IPv6 and IPv4.
+ // If not, try an IPv4-only socket (we don't want to end up
+ // with an IPv6-only socket).
+ if (!localOnly) // Only bind loopback to ipv4. TODO : Implement dual bindinds.
+ {
+ m_ipv6Socket = CheckIPv6(port, range);
+
+ if (m_ipv6Socket)
+ {
+ m_iSock = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
+ if (m_iSock != INVALID_SOCKET)
+ {
+#ifdef WINSOCK_VERSION
+ const char zero = 0;
+#else
+ int zero = 0;
+#endif
+ if (setsockopt(m_iSock, IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof(zero)) == -1)
+ {
+ closesocket(m_iSock);
+ m_iSock = INVALID_SOCKET;
+ }
+ }
+ }
+ }
+
+ if (m_iSock == INVALID_SOCKET)
+ m_iSock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+
+ if (m_iSock == INVALID_SOCKET)
+ {
+#ifdef TARGET_WINDOWS
+ int ierr = WSAGetLastError();
+ CLog::Log(LOGERROR, "UDP: Could not create socket {}", ierr);
+ // hack for broken third party libs
+ if (ierr == WSANOTINITIALISED)
+ {
+ WSADATA wd;
+ if (WSAStartup(MAKEWORD(2,2), &wd) != 0)
+ CLog::Log(LOGERROR, "UDP: WSAStartup failed");
+ }
+#else
+ CLog::Log(LOGERROR, "UDP: Could not create socket");
+#endif
+ CLog::Log(LOGERROR, "UDP: {}", strerror(errno));
+ return false;
+ }
+
+ // make sure we can reuse the address
+#ifdef WINSOCK_VERSION
+ const char yes=1;
+#else
+ int yes = 1;
+#endif
+ if (setsockopt(m_iSock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) == -1)
+ {
+ CLog::Log(LOGWARNING, "UDP: Could not enable the address reuse options");
+ CLog::Log(LOGWARNING, "UDP: {}", strerror(errno));
+ }
+
+ // bind to any address or localhost
+ if (m_ipv6Socket)
+ {
+ if (localOnly)
+ m_addr = CAddress("::1");
+ else
+ m_addr = CAddress("::");
+ }
+ else
+ {
+ if (localOnly)
+ m_addr = CAddress("127.0.0.1");
+ else
+ m_addr = CAddress("0.0.0.0");
+ }
+
+ // bind the socket ( try from port to port+range )
+ for (m_iPort = port; m_iPort <= port + range; ++m_iPort)
+ {
+ if (m_ipv6Socket)
+ m_addr.saddr.saddr6.sin6_port = htons(m_iPort);
+ else
+ m_addr.saddr.saddr4.sin_port = htons(m_iPort);
+
+ if (bind(m_iSock, (struct sockaddr*)&m_addr.saddr, m_addr.size) != 0)
+ {
+ CLog::Log(LOGWARNING, "UDP: Error binding socket on port {} (ipv6 : {})", m_iPort,
+ m_ipv6Socket ? "true" : "false");
+ CLog::Log(LOGWARNING, "UDP: {}", strerror(errno));
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "UDP: Listening on port {} (ipv6 : {})", m_iPort,
+ m_ipv6Socket ? "true" : "false");
+ SetBound();
+ SetReady();
+ break;
+ }
+ }
+
+ // check for errors
+ if (!Bound())
+ {
+ CLog::Log(LOGERROR, "UDP: No suitable port found");
+ Close();
+ return false;
+ }
+
+ return true;
+}
+
+bool CPosixUDPSocket::CheckIPv6(int port, int range)
+{
+ CAddress testaddr("::");
+#if defined(TARGET_WINDOWS)
+ using CAutoPtrSocket = KODI::UTILS::CScopeGuard<SOCKET, INVALID_SOCKET, decltype(closesocket)>;
+ CAutoPtrSocket testSocket(closesocket, socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP));
+#else
+ using CAutoPtrSocket = KODI::UTILS::CScopeGuard<int, -1, decltype(close)>;
+ CAutoPtrSocket testSocket(close, socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP));
+#endif
+
+ if (static_cast<SOCKET>(testSocket) == INVALID_SOCKET)
+ {
+ CLog::LogF(LOGDEBUG, "Could not create IPv6 socket: {}", strerror(errno));
+ return false;
+ }
+
+#ifdef WINSOCK_VERSION
+ const char zero = 0;
+#else
+ int zero = 0;
+#endif
+
+ if (setsockopt(static_cast<SOCKET>(testSocket), IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof(zero)) ==
+ -1)
+ {
+ CLog::LogF(LOGDEBUG, "Could not disable IPV6_V6ONLY for socket: {}", strerror(errno));
+ return false;
+ }
+
+ // Try to bind a socket to validate ipv6 status
+ for (; port <= port + range; port++)
+ {
+ testaddr.saddr.saddr6.sin6_port = htons(port);
+ if (bind(static_cast<SOCKET>(testSocket), reinterpret_cast<struct sockaddr*>(&testaddr.saddr),
+ testaddr.size) == 0)
+ {
+ CLog::LogF(LOGDEBUG, "IPv6 socket bound successfully");
+ return true;
+ }
+ else
+ {
+ CLog::LogF(LOGDEBUG, "Could not bind IPv6 socket: {}", strerror(errno));
+ }
+ }
+
+ return false;
+}
+
+void CPosixUDPSocket::Close()
+{
+ if (m_iSock>=0)
+ {
+ close(m_iSock);
+ m_iSock = INVALID_SOCKET;
+ }
+ SetBound(false);
+ SetReady(false);
+}
+
+int CPosixUDPSocket::Read(CAddress& addr, const int buffersize, void *buffer)
+{
+ if (m_ipv6Socket)
+ addr.SetAddress("::");
+ return (int)recvfrom(m_iSock, (char*)buffer, (size_t)buffersize, 0,
+ (struct sockaddr*)&addr.saddr, &addr.size);
+}
+
+int CPosixUDPSocket::SendTo(const CAddress& addr, const int buffersize,
+ const void *buffer)
+{
+ return (int)sendto(m_iSock, (const char *)buffer, (size_t)buffersize, 0,
+ (const struct sockaddr*)&addr.saddr, addr.size);
+}
+
+/**********************************************************************/
+/* CSocketFactory */
+/**********************************************************************/
+
+std::unique_ptr<CUDPSocket> CSocketFactory::CreateUDPSocket()
+{
+ return std::make_unique<CPosixUDPSocket>();
+}
+
+/**********************************************************************/
+/* CSocketListener */
+/**********************************************************************/
+
+CSocketListener::CSocketListener()
+{
+ Clear();
+}
+
+void CSocketListener::AddSocket(CBaseSocket *sock)
+{
+ // WARNING: not threadsafe (which is ok for now)
+
+ if (sock && sock->Ready())
+ {
+ m_sockets.push_back(sock);
+ FD_SET(sock->Socket(), &m_fdset);
+#ifndef WINSOCK_VERSION
+ if (sock->Socket() > m_iMaxSockets)
+ m_iMaxSockets = sock->Socket();
+#endif
+ }
+}
+
+bool CSocketListener::Listen(int timeout)
+{
+ if (m_sockets.size()==0)
+ {
+ CLog::Log(LOGERROR, "SOCK: No sockets to listen for");
+ throw LISTENEMPTY;
+ }
+
+ m_iReadyCount = 0;
+ m_iCurrentSocket = 0;
+
+ FD_ZERO(&m_fdset);
+ for (unsigned int i = 0 ; i<m_sockets.size() ; i++)
+ {
+ FD_SET(m_sockets[i]->Socket(), &m_fdset);
+ }
+
+ // set our timeout
+ struct timeval tv;
+ int rem = timeout % 1000;
+ tv.tv_usec = rem * 1000;
+ tv.tv_sec = timeout / 1000;
+
+ m_iReadyCount = select(m_iMaxSockets+1, &m_fdset, NULL, NULL, (timeout < 0 ? NULL : &tv));
+
+ if (m_iReadyCount<0)
+ {
+ CLog::Log(LOGERROR, "SOCK: Error selecting socket(s)");
+ Clear();
+ throw LISTENERROR;
+ }
+ else
+ {
+ m_iCurrentSocket = 0;
+ return (m_iReadyCount>0);
+ }
+}
+
+void CSocketListener::Clear()
+{
+ m_sockets.clear();
+ FD_ZERO(&m_fdset);
+ m_iReadyCount = 0;
+ m_iMaxSockets = 0;
+ m_iCurrentSocket = 0;
+}
+
+CBaseSocket* CSocketListener::GetFirstReadySocket()
+{
+ if (m_iReadyCount<=0)
+ return NULL;
+
+ for (int i = 0 ; i < (int)m_sockets.size() ; i++)
+ {
+ if (FD_ISSET((m_sockets[i])->Socket(), &m_fdset))
+ {
+ m_iCurrentSocket = i;
+ return m_sockets[i];
+ }
+ }
+ return NULL;
+}
+
+CBaseSocket* CSocketListener::GetNextReadySocket()
+{
+ if (m_iReadyCount<=0)
+ return NULL;
+
+ for (int i = m_iCurrentSocket+1 ; i<(int)m_sockets.size() ; i++)
+ {
+ if (FD_ISSET(m_sockets[i]->Socket(), &m_fdset))
+ {
+ m_iCurrentSocket = i;
+ return m_sockets[i];
+ }
+ }
+ return NULL;
+}
diff --git a/xbmc/network/Socket.h b/xbmc/network/Socket.h
new file mode 100644
index 0000000..b945533
--- /dev/null
+++ b/xbmc/network/Socket.h
@@ -0,0 +1,253 @@
+/*
+ * Socket classes
+ * Copyright (c) 2008 d4rk
+ * Copyright (C) 2008-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 <map>
+#include <memory>
+#include <string.h>
+#include <vector>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "PlatformDefs.h"
+#ifdef TARGET_POSIX
+typedef int SOCKET;
+#endif
+
+namespace SOCKETS
+{
+ // types of sockets
+ enum SocketType
+ {
+ ST_TCP,
+ ST_UDP,
+ ST_UNIX
+ };
+
+ /**********************************************************************/
+ /* IP address abstraction class */
+ /**********************************************************************/
+ class CAddress
+ {
+ public:
+ union
+ {
+ sockaddr_in saddr4;
+ sockaddr_in6 saddr6;
+ sockaddr saddr_generic;
+ } saddr;
+ socklen_t size;
+
+ public:
+ CAddress()
+ {
+ memset(&saddr, 0, sizeof(saddr));
+ saddr.saddr4.sin_family = AF_INET;
+ saddr.saddr4.sin_addr.s_addr = htonl(INADDR_ANY);
+ size = sizeof(saddr.saddr4);
+ }
+
+ explicit CAddress(const char *address)
+ {
+ SetAddress(address);
+ }
+
+ void SetAddress(const char *address)
+ {
+ in6_addr addr6;
+ memset(&saddr, 0, sizeof(saddr));
+ if (inet_pton(AF_INET6, address, &addr6) == 1)
+ {
+ saddr.saddr6.sin6_family = AF_INET6;
+ saddr.saddr6.sin6_addr = addr6;
+ size = sizeof(saddr.saddr6);
+ }
+ else
+ {
+ saddr.saddr4.sin_family = AF_INET;
+ saddr.saddr4.sin_addr.s_addr = inet_addr(address);
+ size = sizeof(saddr.saddr4);
+ }
+ }
+
+ // returns statically alloced buffer, do not free
+ const char *Address()
+ {
+ if (saddr.saddr_generic.sa_family == AF_INET6)
+ {
+ static char buf[INET6_ADDRSTRLEN];
+ return inet_ntop(AF_INET6, &saddr.saddr6.sin6_addr, buf, size);
+ }
+ else
+ return inet_ntoa(saddr.saddr4.sin_addr);
+ }
+
+ unsigned long ULong()
+ {
+ if (saddr.saddr_generic.sa_family == AF_INET6)
+ {
+ // IPv4 coercion (see http://home.samfundet.no/~sesse/ipv6-porting.pdf).
+ // We hash the entire IPv6 address because XBMC might conceivably need to
+ // distinguish between different hosts in the same subnet.
+ // This hash function (djbhash) is not too strong, but good enough.
+ uint32_t hash = 5381;
+ for (int i = 0; i < 16; ++i)
+ {
+ hash = hash * 33 + saddr.saddr6.sin6_addr.s6_addr[i];
+ }
+ // Move into 224.0.0.0/3. As a special safeguard, make sure we don't
+ // end up with the the special broadcast address 255.255.255.255.
+ hash |= 0xe0000000u;
+ if (hash == 0xffffffffu)
+ hash = 0xfffffffeu;
+ return (unsigned long)htonl(hash);
+ }
+ else
+ return (unsigned long)saddr.saddr4.sin_addr.s_addr;
+ }
+ };
+
+ /**********************************************************************/
+ /* Base class for all sockets */
+ /**********************************************************************/
+ class CBaseSocket
+ {
+ public:
+ CBaseSocket()
+ {
+ m_Type = ST_TCP;
+ m_bReady = false;
+ m_bBound = false;
+ m_iPort = 0;
+ }
+ virtual ~CBaseSocket() { Close(); }
+
+ // socket functions
+ virtual bool Bind(bool localOnly, int port, int range=0) = 0;
+ virtual bool Connect() = 0;
+ virtual void Close() {}
+
+ // state functions
+ bool Ready() { return m_bReady; }
+ bool Bound() { return m_bBound; }
+ SocketType Type() { return m_Type; }
+ int Port() { return m_iPort; }
+ virtual SOCKET Socket() = 0;
+
+ protected:
+ virtual void SetBound(bool set=true) { m_bBound = set; }
+ virtual void SetReady(bool set=true) { m_bReady = set; }
+
+ protected:
+ SocketType m_Type;
+ bool m_bReady;
+ bool m_bBound;
+ int m_iPort;
+ };
+
+ /**********************************************************************/
+ /* Base class for UDP socket implementations */
+ /**********************************************************************/
+ class CUDPSocket : public CBaseSocket
+ {
+ public:
+ CUDPSocket()
+ {
+ m_Type = ST_UDP;
+ }
+ // I/O functions
+ virtual int SendTo(const CAddress& addr, const int bufferlength,
+ const void* buffer) = 0;
+
+ // read datagrams, return no. of bytes read or -1 or error
+ virtual int Read(CAddress& addr, const int buffersize, void *buffer) = 0;
+ virtual bool Broadcast(const CAddress& addr, const int datasize,
+ const void* data) = 0;
+ };
+
+ // Implementation specific classes
+
+ /**********************************************************************/
+ /* POSIX based UDP socket implementation */
+ /**********************************************************************/
+ class CPosixUDPSocket : public CUDPSocket
+ {
+ public:
+ CPosixUDPSocket()
+ {
+ m_iSock = INVALID_SOCKET;
+ m_ipv6Socket = false;
+ }
+
+ bool Bind(bool localOnly, int port, int range=0) override;
+ bool Connect() override { return false; }
+ bool Listen(int timeout);
+ int SendTo(const CAddress& addr, const int datasize, const void* data) override;
+ int Read(CAddress& addr, const int buffersize, void *buffer) override;
+ bool Broadcast(const CAddress& addr, const int datasize, const void* data) override
+ {
+ //! @todo implement
+ return false;
+ }
+ SOCKET Socket() override { return m_iSock; }
+ void Close() override;
+
+ protected:
+ SOCKET m_iSock;
+ CAddress m_addr;
+
+ private:
+ bool CheckIPv6(int port, int range);
+
+ bool m_ipv6Socket;
+ };
+
+ /**********************************************************************/
+ /* Create and return platform dependent sockets */
+ /**********************************************************************/
+ class CSocketFactory
+ {
+ public:
+ static std::unique_ptr<CUDPSocket> CreateUDPSocket();
+ };
+
+ /**********************************************************************/
+ /* Listens on multiple sockets for reads */
+ /**********************************************************************/
+
+#define LISTENERROR 1
+#define LISTENEMPTY 2
+
+ class CSocketListener
+ {
+ public:
+ CSocketListener();
+ void AddSocket(CBaseSocket *);
+ bool Listen(int timeoutMs); // in ms, -1=>never timeout, 0=>poll
+ void Clear();
+ CBaseSocket* GetFirstReadySocket();
+ CBaseSocket* GetNextReadySocket();
+
+ protected:
+ std::vector<CBaseSocket*> m_sockets;
+ int m_iReadyCount;
+ int m_iMaxSockets;
+ int m_iCurrentSocket;
+ fd_set m_fdset;
+ };
+
+}
+
diff --git a/xbmc/network/TCPServer.cpp b/xbmc/network/TCPServer.cpp
new file mode 100644
index 0000000..cd055c4
--- /dev/null
+++ b/xbmc/network/TCPServer.cpp
@@ -0,0 +1,756 @@
+/*
+ * 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 "TCPServer.h"
+
+#include "ServiceBroker.h"
+#include "interfaces/AnnouncementManager.h"
+#include "interfaces/json-rpc/JSONRPC.h"
+#include "network/Network.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "websocket/WebSocketManager.h"
+
+#include <mutex>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <arpa/inet.h>
+#include <memory.h>
+#include <netinet/in.h>
+
+using namespace std::chrono_literals;
+
+#if defined(TARGET_WINDOWS) || defined(HAVE_LIBBLUETOOTH)
+static const char bt_service_name[] = "XBMC JSON-RPC";
+static const char bt_service_desc[] = "Interface for XBMC remote control over bluetooth";
+static const char bt_service_prov[] = "XBMC JSON-RPC Provider";
+static const uint32_t bt_service_guid[] = {0x65AE4CC0, 0x775D11E0, 0xBE16CE28, 0x4824019B};
+#endif
+
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/CharsetConverter.h"
+#endif
+
+#ifdef HAVE_LIBBLUETOOTH
+#include <bluetooth/bluetooth.h>
+#include <bluetooth/rfcomm.h>
+#include <bluetooth/sdp.h>
+#include <bluetooth/sdp_lib.h>
+
+ /* The defines BDADDR_ANY and BDADDR_LOCAL are broken so use our own structs */
+static const bdaddr_t bt_bdaddr_any = {{0, 0, 0, 0, 0, 0}};
+static const bdaddr_t bt_bdaddr_local = {{0, 0, 0, 0xff, 0xff, 0xff}};
+
+#endif
+
+using namespace JSONRPC;
+
+#define RECEIVEBUFFER 4096
+
+namespace
+{
+constexpr size_t maxBufferLength = 64 * 1024;
+}
+
+CTCPServer *CTCPServer::ServerInstance = NULL;
+
+bool CTCPServer::StartServer(int port, bool nonlocal)
+{
+ StopServer(true);
+
+ ServerInstance = new CTCPServer(port, nonlocal);
+ if (ServerInstance->Initialize())
+ {
+ ServerInstance->Create(false);
+ return true;
+ }
+ else
+ return false;
+}
+
+void CTCPServer::StopServer(bool bWait)
+{
+ if (ServerInstance)
+ {
+ ServerInstance->StopThread(bWait);
+ if (bWait)
+ {
+ delete ServerInstance;
+ ServerInstance = NULL;
+ }
+ }
+}
+
+bool CTCPServer::IsRunning()
+{
+ if (ServerInstance == NULL)
+ return false;
+
+ return ((CThread*)ServerInstance)->IsRunning();
+}
+
+CTCPServer::CTCPServer(int port, bool nonlocal) : CThread("TCPServer")
+{
+ m_port = port;
+ m_nonlocal = nonlocal;
+ m_sdpd = NULL;
+}
+
+void CTCPServer::Process()
+{
+ m_bStop = false;
+
+ while (!m_bStop)
+ {
+ SOCKET max_fd = 0;
+ fd_set rfds;
+ struct timeval to = {1, 0};
+ FD_ZERO(&rfds);
+
+ for (auto& it : m_servers)
+ {
+ FD_SET(it, &rfds);
+ if ((intptr_t)it > (intptr_t)max_fd)
+ max_fd = it;
+ }
+
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ {
+ FD_SET(m_connections[i]->m_socket, &rfds);
+ if ((intptr_t)m_connections[i]->m_socket > (intptr_t)max_fd)
+ max_fd = m_connections[i]->m_socket;
+ }
+
+ int res = select((intptr_t)max_fd+1, &rfds, NULL, NULL, &to);
+ if (res < 0)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Select failed");
+ CThread::Sleep(1000ms);
+ Initialize();
+ }
+ else if (res > 0)
+ {
+ for (int i = m_connections.size() - 1; i >= 0; i--)
+ {
+ int socket = m_connections[i]->m_socket;
+ if (FD_ISSET(socket, &rfds))
+ {
+ char buffer[RECEIVEBUFFER] = {};
+ int nread = 0;
+ nread = recv(socket, (char*)&buffer, RECEIVEBUFFER, 0);
+ bool close = false;
+ if (nread > 0)
+ {
+ std::string response;
+ if (m_connections[i]->IsNew())
+ {
+ CWebSocket *websocket = CWebSocketManager::Handle(buffer, nread, response);
+
+ if (!response.empty())
+ m_connections[i]->Send(response.c_str(), response.size());
+
+ if (websocket != NULL)
+ {
+ // Replace the CTCPClient with a CWebSocketClient
+ CWebSocketClient *websocketClient = new CWebSocketClient(websocket, *(m_connections[i]));
+ delete m_connections[i];
+ m_connections.erase(m_connections.begin() + i);
+ m_connections.insert(m_connections.begin() + i, websocketClient);
+ }
+ }
+
+ if (response.size() <= 0)
+ m_connections[i]->PushBuffer(this, buffer, nread);
+
+ close = m_connections[i]->Closing();
+ }
+ else
+ close = true;
+
+ if (close)
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: Disconnection detected");
+ m_connections[i]->Disconnect();
+ delete m_connections[i];
+ m_connections.erase(m_connections.begin() + i);
+ }
+ }
+ }
+
+ for (auto& it : m_servers)
+ {
+ if (FD_ISSET(it, &rfds))
+ {
+ CLog::Log(LOGDEBUG, "JSONRPC Server: New connection detected");
+ CTCPClient *newconnection = new CTCPClient();
+ newconnection->m_socket =
+ accept(it, (sockaddr*)&newconnection->m_cliaddr, &newconnection->m_addrlen);
+
+ if (newconnection->m_socket == INVALID_SOCKET)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Accept of new connection failed: {}", errno);
+ if (EBADF == errno)
+ {
+ CThread::Sleep(1000ms);
+ Initialize();
+ break;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: New connection added");
+ m_connections.push_back(newconnection);
+ }
+ }
+ }
+ }
+ }
+
+ Deinitialize();
+}
+
+bool CTCPServer::PrepareDownload(const char *path, CVariant &details, std::string &protocol)
+{
+ return false;
+}
+
+bool CTCPServer::Download(const char *path, CVariant &result)
+{
+ return false;
+}
+
+int CTCPServer::GetCapabilities()
+{
+ return Response | Announcing;
+}
+
+void CTCPServer::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (m_connections.empty())
+ return;
+
+ std::string str = IJSONRPCAnnouncer::AnnouncementToJSONRPC(flag, sender, message, data, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_jsonOutputCompact);
+
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ {
+ {
+ std::unique_lock<CCriticalSection> lock(m_connections[i]->m_critSection);
+ if ((m_connections[i]->GetAnnouncementFlags() & flag) == 0)
+ continue;
+ }
+
+ m_connections[i]->Send(str.c_str(), str.size());
+ }
+}
+
+bool CTCPServer::Initialize()
+{
+ Deinitialize();
+
+ bool started = false;
+
+ started |= InitializeBlue();
+ started |= InitializeTCP();
+
+ if (started)
+ {
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+ CLog::Log(LOGINFO, "JSONRPC Server: Successfully initialized");
+ return true;
+ }
+ return false;
+}
+
+#ifdef TARGET_WINDOWS_STORE
+bool CTCPServer::InitializeBlue()
+{
+ CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__);
+ return true; // need to fake it for now
+}
+
+#else
+bool CTCPServer::InitializeBlue()
+{
+ if (!m_nonlocal)
+ return false;
+
+#ifdef TARGET_WINDOWS
+
+ SOCKET fd = socket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM);
+ if (fd == INVALID_SOCKET)
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: Unable to get bluetooth socket");
+ return false;
+ }
+ SOCKADDR_BTH sa = {};
+ sa.addressFamily = AF_BTH;
+ sa.port = BT_PORT_ANY;
+
+ if (bind(fd, (SOCKADDR*)&sa, sizeof(sa)) < 0)
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: Unable to bind to bluetooth socket");
+ closesocket(fd);
+ return false;
+ }
+
+ ULONG optval = TRUE;
+ if (setsockopt(fd, SOL_RFCOMM, SO_BTH_AUTHENTICATE, (const char*)&optval, sizeof(optval)) == SOCKET_ERROR)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to force authentication for bluetooth socket");
+ closesocket(fd);
+ return false;
+ }
+
+ int len = sizeof(sa);
+ if (getsockname(fd, (SOCKADDR*)&sa, &len) < 0)
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to get bluetooth port");
+
+ if (listen(fd, 10) < 0)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to listen to bluetooth port");
+ closesocket(fd);
+ return false;
+ }
+
+ m_servers.push_back(fd);
+
+ CSADDR_INFO addrinfo;
+ addrinfo.iProtocol = BTHPROTO_RFCOMM;
+ addrinfo.iSocketType = SOCK_STREAM;
+ addrinfo.LocalAddr.lpSockaddr = (SOCKADDR*)&sa;
+ addrinfo.LocalAddr.iSockaddrLength = sizeof(sa);
+ addrinfo.RemoteAddr.lpSockaddr = (SOCKADDR*)&sa;
+ addrinfo.RemoteAddr.iSockaddrLength = sizeof(sa);
+
+ using KODI::PLATFORM::WINDOWS::ToW;
+
+ WSAQUERYSET service = {};
+ service.dwSize = sizeof(service);
+ service.lpszServiceInstanceName = const_cast<LPWSTR>(ToW(bt_service_name).c_str());
+ service.lpServiceClassId = (LPGUID)&bt_service_guid;
+ service.lpszComment = const_cast<LPWSTR>(ToW(bt_service_desc).c_str());
+ service.dwNameSpace = NS_BTH;
+ service.lpNSProviderId = NULL; /* RFCOMM? */
+ service.lpcsaBuffer = &addrinfo;
+ service.dwNumberOfCsAddrs = 1;
+
+ if (WSASetService(&service, RNRSERVICE_REGISTER, 0) == SOCKET_ERROR)
+ CLog::Log(LOGERROR, "JSONRPC Server: failed to register bluetooth service error {}",
+ WSAGetLastError());
+
+ return true;
+#endif
+
+#ifdef HAVE_LIBBLUETOOTH
+
+ SOCKET fd = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);
+ if (fd == INVALID_SOCKET)
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: Unable to get bluetooth socket");
+ return false;
+ }
+ struct sockaddr_rc sa = {};
+ sa.rc_family = AF_BLUETOOTH;
+ sa.rc_bdaddr = bt_bdaddr_any;
+ sa.rc_channel = 0;
+
+ if (bind(fd, (struct sockaddr*)&sa, sizeof(sa)) < 0)
+ {
+ CLog::Log(LOGINFO, "JSONRPC Server: Unable to bind to bluetooth socket");
+ closesocket(fd);
+ return false;
+ }
+
+ socklen_t len = sizeof(sa);
+ if (getsockname(fd, (struct sockaddr*)&sa, &len) < 0)
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to get bluetooth port");
+
+ if (listen(fd, 10) < 0)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to listen to bluetooth port {}", sa.rc_channel);
+ closesocket(fd);
+ return false;
+ }
+
+ uint8_t rfcomm_channel = sa.rc_channel;
+
+ uuid_t root_uuid, l2cap_uuid, rfcomm_uuid, svc_uuid;
+ sdp_list_t *l2cap_list = 0,
+ *rfcomm_list = 0,
+ *root_list = 0,
+ *proto_list = 0,
+ *access_proto_list = 0,
+ *service_class = 0;
+
+ sdp_data_t *channel = 0;
+
+ sdp_record_t *record = sdp_record_alloc();
+
+ // set the general service ID
+ sdp_uuid128_create(&svc_uuid, &bt_service_guid);
+ sdp_set_service_id(record, svc_uuid);
+
+ // make the service record publicly browseable
+ sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP);
+ root_list = sdp_list_append(0, &root_uuid);
+ sdp_set_browse_groups(record, root_list);
+
+ // set l2cap information
+ sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID);
+ l2cap_list = sdp_list_append(0, &l2cap_uuid);
+ proto_list = sdp_list_append(0, l2cap_list);
+
+ // set rfcomm information
+ sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID);
+ channel = sdp_data_alloc(SDP_UINT8, &rfcomm_channel);
+ rfcomm_list = sdp_list_append(0, &rfcomm_uuid);
+ sdp_list_append(rfcomm_list, channel);
+ sdp_list_append(proto_list, rfcomm_list);
+
+ // attach protocol information to service record
+ access_proto_list = sdp_list_append(0, proto_list);
+ sdp_set_access_protos(record, access_proto_list);
+
+ // set the name, provider, and description
+ sdp_set_info_attr(record, bt_service_name, bt_service_prov, bt_service_desc);
+
+ // set the Service class ID
+ service_class = sdp_list_append(0, &svc_uuid);
+ sdp_set_service_classes(record, service_class);
+
+ // cleanup
+ sdp_data_free(channel);
+ sdp_list_free(l2cap_list, 0);
+ sdp_list_free(rfcomm_list, 0);
+ sdp_list_free(root_list, 0);
+ sdp_list_free(access_proto_list, 0);
+ sdp_list_free(service_class, 0);
+
+ // connect to the local SDP server, register the service record
+ sdp_session_t *session = sdp_connect(&bt_bdaddr_any, &bt_bdaddr_local, SDP_RETRY_IF_BUSY);
+ if (session == NULL)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to connect to sdpd");
+ closesocket(fd);
+ sdp_record_free(record);
+ return false;
+ }
+
+ if (sdp_record_register(session, record, 0) < 0)
+ {
+ CLog::Log(LOGERROR, "JSONRPC Server: Failed to register record with error {}", errno);
+ closesocket(fd);
+ sdp_close(session);
+ sdp_record_free(record);
+ return false;
+ }
+
+ m_sdpd = session;
+ m_servers.push_back(fd);
+
+ return true;
+#endif
+ return false;
+}
+#endif
+
+bool CTCPServer::InitializeTCP()
+{
+ Deinitialize();
+
+ std::vector<SOCKET> sockets = CreateTCPServerSocket(m_port, !m_nonlocal, 10, "JSONRPC");
+ if (sockets.empty())
+ return false;
+
+ m_servers.insert(m_servers.end(), sockets.begin(), sockets.end());
+ return true;
+}
+
+void CTCPServer::Deinitialize()
+{
+ for (unsigned int i = 0; i < m_connections.size(); i++)
+ {
+ m_connections[i]->Disconnect();
+ delete m_connections[i];
+ }
+
+ m_connections.clear();
+
+ for (unsigned int i = 0; i < m_servers.size(); i++)
+ closesocket(m_servers[i]);
+
+ m_servers.clear();
+
+#ifdef HAVE_LIBBLUETOOTH
+ if (m_sdpd)
+ sdp_close((sdp_session_t*)m_sdpd);
+ m_sdpd = NULL;
+#endif
+
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+CTCPServer::CTCPClient::CTCPClient()
+{
+ m_new = true;
+ m_announcementflags = ANNOUNCEMENT::ANNOUNCE_ALL;
+ m_socket = INVALID_SOCKET;
+ m_beginBrackets = 0;
+ m_endBrackets = 0;
+ m_beginChar = 0;
+ m_endChar = 0;
+
+ m_addrlen = sizeof(m_cliaddr);
+}
+
+CTCPServer::CTCPClient::CTCPClient(const CTCPClient& client)
+{
+ Copy(client);
+}
+
+CTCPServer::CTCPClient& CTCPServer::CTCPClient::operator=(const CTCPClient& client)
+{
+ Copy(client);
+ return *this;
+}
+
+int CTCPServer::CTCPClient::GetPermissionFlags()
+{
+ return OPERATION_PERMISSION_ALL;
+}
+
+int CTCPServer::CTCPClient::GetAnnouncementFlags()
+{
+ return m_announcementflags;
+}
+
+bool CTCPServer::CTCPClient::SetAnnouncementFlags(int flags)
+{
+ m_announcementflags = flags;
+ return true;
+}
+
+void CTCPServer::CTCPClient::Send(const char *data, unsigned int size)
+{
+ unsigned int sent = 0;
+ do
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ sent += send(m_socket, data + sent, size - sent, 0);
+ } while (sent < size);
+}
+
+void CTCPServer::CTCPClient::PushBuffer(CTCPServer *host, const char *buffer, int length)
+{
+ m_new = false;
+ bool inObject = false;
+ bool inString = false;
+ bool escapeNext = false;
+
+ for (int i = 0; i < length; i++)
+ {
+ char c = buffer[i];
+
+ if (m_beginChar == 0 && c == '{')
+ {
+ m_beginChar = '{';
+ m_endChar = '}';
+ }
+ else if (m_beginChar == 0 && c == '[')
+ {
+ m_beginChar = '[';
+ m_endChar = ']';
+ }
+
+ if (m_beginChar != 0)
+ {
+ m_buffer.push_back(c);
+ if (inObject)
+ {
+ if (!inString)
+ {
+ if (c == '"')
+ inString = true;
+ }
+ else
+ {
+ if (escapeNext)
+ {
+ escapeNext = false;
+ }
+ else
+ {
+ if (c == '\\')
+ escapeNext = true;
+ else if (c == '"')
+ inString = false;
+ }
+ }
+ }
+ if (!inString)
+ {
+ if (c == m_beginChar)
+ {
+ m_beginBrackets++;
+ inObject = true;
+ }
+ else if (c == m_endChar)
+ {
+ m_endBrackets++;
+ if (m_beginBrackets == m_endBrackets)
+ inObject = false;
+ }
+ }
+ if (m_beginBrackets > 0 && m_endBrackets > 0 && m_beginBrackets == m_endBrackets)
+ {
+ std::string line = CJSONRPC::MethodCall(m_buffer, host, this);
+ Send(line.c_str(), line.size());
+ m_beginChar = m_beginBrackets = m_endBrackets = 0;
+ m_buffer.clear();
+ }
+ }
+ }
+}
+
+void CTCPServer::CTCPClient::Disconnect()
+{
+ if (m_socket > 0)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ shutdown(m_socket, SHUT_RDWR);
+ closesocket(m_socket);
+ m_socket = INVALID_SOCKET;
+ }
+}
+
+void CTCPServer::CTCPClient::Copy(const CTCPClient& client)
+{
+ m_new = client.m_new;
+ m_socket = client.m_socket;
+ m_cliaddr = client.m_cliaddr;
+ m_addrlen = client.m_addrlen;
+ m_announcementflags = client.m_announcementflags;
+ m_beginBrackets = client.m_beginBrackets;
+ m_endBrackets = client.m_endBrackets;
+ m_beginChar = client.m_beginChar;
+ m_endChar = client.m_endChar;
+ m_buffer = client.m_buffer;
+}
+
+CTCPServer::CWebSocketClient::CWebSocketClient(CWebSocket *websocket)
+{
+ m_websocket = websocket;
+ m_buffer.reserve(maxBufferLength);
+}
+
+CTCPServer::CWebSocketClient::CWebSocketClient(const CWebSocketClient& client)
+ : CTCPServer::CTCPClient(client)
+{
+ *this = client;
+ m_buffer.reserve(maxBufferLength);
+}
+
+CTCPServer::CWebSocketClient::CWebSocketClient(CWebSocket *websocket, const CTCPClient& client)
+{
+ Copy(client);
+
+ m_websocket = websocket;
+ m_buffer.reserve(maxBufferLength);
+}
+
+CTCPServer::CWebSocketClient::~CWebSocketClient()
+{
+ delete m_websocket;
+}
+
+CTCPServer::CWebSocketClient& CTCPServer::CWebSocketClient::operator=(const CWebSocketClient& client)
+{
+ Copy(client);
+
+ m_websocket = client.m_websocket;
+ m_buffer = client.m_buffer;
+
+ return *this;
+}
+
+void CTCPServer::CWebSocketClient::Send(const char *data, unsigned int size)
+{
+ const CWebSocketMessage *msg = m_websocket->Send(WebSocketTextFrame, data, size);
+ if (msg == NULL || !msg->IsComplete())
+ return;
+
+ std::vector<const CWebSocketFrame *> frames = msg->GetFrames();
+ for (unsigned int index = 0; index < frames.size(); index++)
+ CTCPClient::Send(frames.at(index)->GetFrameData(), (unsigned int)frames.at(index)->GetFrameLength());
+}
+
+void CTCPServer::CWebSocketClient::PushBuffer(CTCPServer *host, const char *buffer, int length)
+{
+ bool send;
+ const CWebSocketMessage *msg = NULL;
+
+ if (m_buffer.size() + length > maxBufferLength)
+ {
+ CLog::Log(LOGINFO, "WebSocket: client buffer size {} exceeded", maxBufferLength);
+ return Disconnect();
+ }
+
+ m_buffer.append(buffer, length);
+
+ const char* buf = m_buffer.data();
+ size_t len = m_buffer.size();
+
+ do
+ {
+ if ((msg = m_websocket->Handle(buf, len, send)) != NULL && msg->IsComplete())
+ {
+ std::vector<const CWebSocketFrame *> frames = msg->GetFrames();
+ if (send)
+ {
+ for (unsigned int index = 0; index < frames.size(); index++)
+ Send(frames.at(index)->GetFrameData(), (unsigned int)frames.at(index)->GetFrameLength());
+ }
+ else
+ {
+ for (unsigned int index = 0; index < frames.size(); index++)
+ CTCPClient::PushBuffer(host, frames.at(index)->GetApplicationData(), (int)frames.at(index)->GetLength());
+ }
+
+ delete msg;
+ }
+ }
+ while (len > 0 && msg != NULL);
+
+ if (len < m_buffer.size())
+ m_buffer = m_buffer.substr(m_buffer.size() - len);
+
+ if (m_websocket->GetState() == WebSocketStateClosed)
+ Disconnect();
+}
+
+void CTCPServer::CWebSocketClient::Disconnect()
+{
+ if (m_socket > 0)
+ {
+ if (m_websocket->GetState() != WebSocketStateClosed && m_websocket->GetState() != WebSocketStateNotConnected)
+ {
+ const CWebSocketFrame *closeFrame = m_websocket->Close();
+ if (closeFrame)
+ Send(closeFrame->GetFrameData(), (unsigned int)closeFrame->GetFrameLength());
+ }
+
+ if (m_websocket->GetState() == WebSocketStateClosed)
+ CTCPClient::Disconnect();
+ }
+}
diff --git a/xbmc/network/TCPServer.h b/xbmc/network/TCPServer.h
new file mode 100644
index 0000000..cf98194
--- /dev/null
+++ b/xbmc/network/TCPServer.h
@@ -0,0 +1,118 @@
+/*
+ * 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 "interfaces/json-rpc/IClient.h"
+#include "interfaces/json-rpc/IJSONRPCAnnouncer.h"
+#include "interfaces/json-rpc/ITransportLayer.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+#include "websocket/WebSocket.h"
+
+#include <vector>
+
+#include <sys/socket.h>
+
+#include "PlatformDefs.h"
+
+class CVariant;
+
+namespace JSONRPC
+{
+ class CTCPServer : public ITransportLayer, public JSONRPC::IJSONRPCAnnouncer, public CThread
+ {
+ public:
+ static bool StartServer(int port, bool nonlocal);
+ static void StopServer(bool bWait);
+ static bool IsRunning();
+
+ bool PrepareDownload(const char *path, CVariant &details, std::string &protocol) override;
+ bool Download(const char *path, CVariant &result) override;
+ int GetCapabilities() override;
+
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ protected:
+ void Process() override;
+ private:
+ CTCPServer(int port, bool nonlocal);
+ bool Initialize();
+ bool InitializeBlue();
+ bool InitializeTCP();
+ void Deinitialize();
+
+ class CTCPClient : public IClient
+ {
+ public:
+ CTCPClient();
+ //Copying a CCriticalSection is not allowed, so copy everything but that
+ //when adding a member variable, make sure to copy it in CTCPClient::Copy
+ CTCPClient(const CTCPClient& client);
+ CTCPClient& operator=(const CTCPClient& client);
+ ~CTCPClient() override = default;
+
+ int GetPermissionFlags() override;
+ int GetAnnouncementFlags() override;
+ bool SetAnnouncementFlags(int flags) override;
+
+ virtual void Send(const char *data, unsigned int size);
+ virtual void PushBuffer(CTCPServer *host, const char *buffer, int length);
+ virtual void Disconnect();
+
+ virtual bool IsNew() const { return m_new; }
+ virtual bool Closing() const { return false; }
+
+ SOCKET m_socket;
+ sockaddr_storage m_cliaddr;
+ socklen_t m_addrlen;
+ CCriticalSection m_critSection;
+
+ protected:
+ void Copy(const CTCPClient& client);
+ private:
+ bool m_new;
+ int m_announcementflags;
+ int m_beginBrackets, m_endBrackets;
+ char m_beginChar, m_endChar;
+ std::string m_buffer;
+ };
+
+ class CWebSocketClient : public CTCPClient
+ {
+ public:
+ explicit CWebSocketClient(CWebSocket *websocket);
+ CWebSocketClient(const CWebSocketClient& client);
+ CWebSocketClient(CWebSocket *websocket, const CTCPClient& client);
+ CWebSocketClient& operator=(const CWebSocketClient& client);
+ ~CWebSocketClient() override;
+
+ void Send(const char *data, unsigned int size) override;
+ void PushBuffer(CTCPServer *host, const char *buffer, int length) override;
+ void Disconnect() override;
+
+ bool IsNew() const override { return m_websocket == NULL; }
+ bool Closing() const override { return m_websocket != NULL && m_websocket->GetState() == WebSocketStateClosed; }
+
+ private:
+ CWebSocket *m_websocket;
+ std::string m_buffer;
+ };
+
+ std::vector<CTCPClient*> m_connections;
+ std::vector<SOCKET> m_servers;
+ int m_port;
+ bool m_nonlocal;
+ void* m_sdpd;
+
+ static CTCPServer *ServerInstance;
+ };
+}
diff --git a/xbmc/network/UdpClient.cpp b/xbmc/network/UdpClient.cpp
new file mode 100644
index 0000000..97ac4ed
--- /dev/null
+++ b/xbmc/network/UdpClient.cpp
@@ -0,0 +1,262 @@
+/*
+ * 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 "UdpClient.h"
+
+#include <mutex>
+#ifdef TARGET_POSIX
+#include <sys/ioctl.h>
+#endif
+#include "Network.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <chrono>
+
+#include <arpa/inet.h>
+
+using namespace std::chrono_literals;
+
+#define UDPCLIENT_DEBUG_LEVEL LOGDEBUG
+
+CUdpClient::CUdpClient(void) : CThread("UDPClient")
+{}
+
+CUdpClient::~CUdpClient(void) = default;
+
+bool CUdpClient::Create(void)
+{
+ m_bStop = false;
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Creating UDP socket...");
+
+ // Create a UDP socket
+ client_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+ if (client_socket == SOCKET_ERROR)
+ {
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Unable to create socket.");
+ return false;
+ }
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Setting broadcast socket option...");
+
+ unsigned int value = 1;
+ if ( setsockopt( client_socket, SOL_SOCKET, SO_BROADCAST, (char*) &value, sizeof( unsigned int ) ) == SOCKET_ERROR)
+ {
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Unable to set socket option.");
+ return false;
+ }
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Setting non-blocking socket options...");
+
+ unsigned long nonblocking = 1;
+ ioctlsocket(client_socket, FIONBIO, &nonblocking);
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Spawning listener thread...");
+ CThread::Create(false);
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Ready.");
+
+ return true;
+}
+
+void CUdpClient::Destroy()
+{
+ StopThread();
+ closesocket(client_socket);
+}
+
+void CUdpClient::OnStartup()
+{
+ SetPriority(ThreadPriority::LOWEST);
+}
+
+bool CUdpClient::Broadcast(int aPort, const std::string& aMessage)
+{
+ std::unique_lock<CCriticalSection> lock(critical_section);
+
+ struct sockaddr_in addr;
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(aPort);
+ addr.sin_addr.s_addr = INADDR_BROADCAST;
+ memset(&addr.sin_zero, 0, sizeof(addr.sin_zero));
+
+ UdpCommand broadcast = {addr, aMessage, NULL, 0};
+ commands.push_back(broadcast);
+
+ return true;
+}
+
+
+bool CUdpClient::Send(const std::string& aIpAddress, int aPort, const std::string& aMessage)
+{
+ std::unique_lock<CCriticalSection> lock(critical_section);
+
+ struct sockaddr_in addr;
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(aPort);
+ addr.sin_addr.s_addr = inet_addr(aIpAddress.c_str());
+ memset(&addr.sin_zero, 0, sizeof(addr.sin_zero));
+
+ UdpCommand transmit = {addr, aMessage, NULL, 0};
+ commands.push_back(transmit);
+
+ return true;
+}
+
+bool CUdpClient::Send(struct sockaddr_in aAddress, const std::string& aMessage)
+{
+ std::unique_lock<CCriticalSection> lock(critical_section);
+
+ UdpCommand transmit = {aAddress, aMessage, NULL, 0};
+ commands.push_back(transmit);
+
+ return true;
+}
+
+bool CUdpClient::Send(struct sockaddr_in aAddress, unsigned char* pMessage, DWORD dwSize)
+{
+ std::unique_lock<CCriticalSection> lock(critical_section);
+
+ UdpCommand transmit = {aAddress, "", pMessage, dwSize};
+ commands.push_back(transmit);
+
+ return true;
+}
+
+
+void CUdpClient::Process()
+{
+ CThread::Sleep(2000ms);
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Listening.");
+
+ struct sockaddr_in remoteAddress;
+ char messageBuffer[1024];
+ DWORD dataAvailable;
+
+ while ( !m_bStop )
+ {
+ fd_set readset, exceptset;
+ FD_ZERO(&readset); FD_SET(client_socket, &readset);
+ FD_ZERO(&exceptset); FD_SET(client_socket, &exceptset);
+
+ int nfds = (int)(client_socket);
+ timeval tv = { 0, 100000 };
+ if (select(nfds, &readset, NULL, &exceptset, &tv) < 0)
+ {
+ CLog::Log(LOGERROR, "UDPCLIENT: failed to select on socket");
+ break;
+ }
+
+ // is there any data to read
+ dataAvailable = 0;
+ ioctlsocket(client_socket, FIONREAD, &dataAvailable);
+
+ // while there is data to read
+ while (dataAvailable > 0)
+ {
+ // read data
+ int messageLength = sizeof(messageBuffer) - 1 ;
+#ifndef TARGET_POSIX
+ int remoteAddressSize;
+#else
+ socklen_t remoteAddressSize;
+#endif
+ remoteAddressSize = sizeof(remoteAddress);
+
+ int ret = recvfrom(client_socket, messageBuffer, messageLength, 0, (struct sockaddr *) & remoteAddress, &remoteAddressSize);
+ if (ret != SOCKET_ERROR)
+ {
+ // Packet received
+ messageLength = ret;
+ messageBuffer[messageLength] = '\0';
+
+ std::string message = messageBuffer;
+
+ auto now = std::chrono::steady_clock::now();
+ auto timestamp =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT RX: {}\t\t<- '{}'", timestamp.count(), message);
+
+ OnMessage(remoteAddress, message, reinterpret_cast<unsigned char*>(messageBuffer), messageLength);
+ }
+ else
+ {
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Socket error {}", WSAGetLastError());
+ }
+
+ // is there any more data to read?
+ dataAvailable = 0;
+ ioctlsocket(client_socket, FIONREAD, &dataAvailable);
+ }
+
+ // dispatch a single command if any pending
+ while(DispatchNextCommand()) {}
+ }
+
+ closesocket(client_socket);
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT: Stopped listening.");
+}
+
+
+bool CUdpClient::DispatchNextCommand()
+{
+ UdpCommand command;
+ {
+ std::unique_lock<CCriticalSection> lock(critical_section);
+
+ if (commands.size() <= 0)
+ return false;
+
+ COMMANDITERATOR it = commands.begin();
+ command = *it;
+ commands.erase(it);
+ }
+
+ int ret;
+ if (command.binarySize > 0)
+ {
+ // only perform the following if logging level at debug
+
+ auto now = std::chrono::steady_clock::now();
+ auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL,
+ "UDPCLIENT TX: {}\t\t-> "
+ "<binary payload {} bytes>",
+ timestamp.count(), command.binarySize);
+
+ do
+ {
+ ret = sendto(client_socket, (const char*) command.binary, command.binarySize, 0, (struct sockaddr *) & command.address, sizeof(command.address));
+ }
+ while (ret == -1);
+
+ delete[] command.binary;
+ }
+ else
+ {
+ // only perform the following if logging level at debug
+ auto now = std::chrono::steady_clock::now();
+ auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
+
+ CLog::Log(UDPCLIENT_DEBUG_LEVEL, "UDPCLIENT TX: {}\t\t-> '{}'", timestamp.count(),
+ command.message);
+
+ do
+ {
+ ret = sendto(client_socket, command.message.c_str(), command.message.size(), 0, (struct sockaddr *) & command.address, sizeof(command.address));
+ }
+ while (ret == -1 && !m_bStop);
+ }
+ return true;
+}
diff --git a/xbmc/network/UdpClient.h b/xbmc/network/UdpClient.h
new file mode 100644
index 0000000..060cb71
--- /dev/null
+++ b/xbmc/network/UdpClient.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2002 Frodo
+ * Portions Copyright (c) by the authors of ffmpeg and xvid
+ *
+ * Copyright (C) 2002-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/Thread.h"
+
+#include <string>
+#include <vector>
+
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#include "PlatformDefs.h"
+
+class CUdpClient : CThread
+{
+public:
+ CUdpClient();
+ ~CUdpClient(void) override;
+
+protected:
+
+ bool Create();
+ void Destroy();
+
+ void OnStartup() override;
+ void Process() override;
+
+ bool Broadcast(int aPort, const std::string& aMessage);
+ bool Send(const std::string& aIpAddress, int aPort, const std::string& aMessage);
+ bool Send(struct sockaddr_in aAddress, const std::string& aMessage);
+ bool Send(struct sockaddr_in aAddress, unsigned char* pMessage, DWORD dwSize);
+
+ virtual void OnMessage(struct sockaddr_in& aRemoteAddress,
+ const std::string& aMessage,
+ unsigned char* pMessage,
+ DWORD dwMessageLength)
+ {
+ }
+
+protected:
+
+ struct UdpCommand
+ {
+ struct sockaddr_in address;
+ std::string message;
+ unsigned char* binary;
+ DWORD binarySize;
+ };
+
+ bool DispatchNextCommand();
+
+ SOCKET client_socket;
+
+ std::vector<UdpCommand> commands;
+ typedef std::vector<UdpCommand> ::iterator COMMANDITERATOR;
+
+ CCriticalSection critical_section;
+};
diff --git a/xbmc/network/WakeOnAccess.cpp b/xbmc/network/WakeOnAccess.cpp
new file mode 100644
index 0000000..929c68b
--- /dev/null
+++ b/xbmc/network/WakeOnAccess.cpp
@@ -0,0 +1,956 @@
+/*
+ * Copyright (C) 2013-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 "WakeOnAccess.h"
+
+#include "DNSNameCache.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <limits.h>
+#include <mutex>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#ifdef HAS_UPNP
+#include "network/upnp/UPnP.h"
+#include <Platinum/Source/Platinum/Platinum.h>
+#endif
+
+#define DEFAULT_NETWORK_INIT_SEC (20) // wait 20 sec for network after startup or resume
+#define DEFAULT_NETWORK_SETTLE_MS (500) // require 500ms of consistent network availability before trusting it
+
+#define DEFAULT_TIMEOUT_SEC (5*60) // at least 5 minutes between each magic packets
+#define DEFAULT_WAIT_FOR_ONLINE_SEC_1 (40) // wait at 40 seconds after sending magic packet
+#define DEFAULT_WAIT_FOR_ONLINE_SEC_2 (40) // same for extended wait
+#define DEFAULT_WAIT_FOR_SERVICES_SEC (5) // wait 5 seconds after host go online to launch file sharing daemons
+
+using namespace std::chrono_literals;
+
+static CDateTime upnpInitReady;
+
+static int GetTotalSeconds(const CDateTimeSpan& ts)
+{
+ int hours = ts.GetHours() + ts.GetDays() * 24;
+ int minutes = ts.GetMinutes() + hours * 60;
+ return ts.GetSeconds() + minutes * 60;
+}
+
+static unsigned long HostToIP(const std::string& host)
+{
+ std::string ip;
+ CDNSNameCache::Lookup(host, ip);
+ return inet_addr(ip.c_str());
+}
+
+#define LOCALIZED(id) g_localizeStrings.Get(id)
+
+static void ShowDiscoveryMessage(const char* function, const char* server_name, bool new_entry)
+{
+ std::string message;
+
+ if (new_entry)
+ {
+ CLog::Log(LOGINFO, "{} - Create new entry for host '{}'", function, server_name);
+ message = StringUtils::Format(LOCALIZED(13035), server_name);
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "{} - Update existing entry for host '{}'", function, server_name);
+ message = StringUtils::Format(LOCALIZED(13034), server_name);
+ }
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, LOCALIZED(13033), message, 4000, true, 3000);
+}
+
+struct UPnPServer
+{
+ UPnPServer() : m_nextWake(CDateTime::GetCurrentDateTime()) {}
+ bool operator == (const UPnPServer& server) const { return server.m_uuid == m_uuid; }
+ bool operator != (const UPnPServer& server) const { return !(*this == server); }
+ bool operator == (const std::string& server_uuid) const { return server_uuid == m_uuid; }
+ bool operator != (const std::string& server_uuid) const { return !(*this == server_uuid); }
+ std::string m_name;
+ std::string m_uuid;
+ std::string m_mac;
+ CDateTime m_nextWake;
+};
+
+static UPnPServer* LookupUPnPServer(std::vector<UPnPServer>& list, const std::string& uuid)
+{
+ auto serverIt = find(list.begin(), list.end(), uuid);
+
+ return serverIt != list.end() ? &(*serverIt) : nullptr;
+}
+
+static void AddOrUpdateUPnPServer(std::vector<UPnPServer>& list, const UPnPServer& server)
+{
+ auto serverIt = find(list.begin(), list.end(), server);
+
+ bool addNewEntry = serverIt == list.end();
+
+ if (addNewEntry)
+ list.push_back(server); // add server
+ else
+ *serverIt = server; // update existing server
+
+ ShowDiscoveryMessage(__FUNCTION__, server.m_name.c_str(), addNewEntry);
+}
+
+static void AddMatchingUPnPServers(std::vector<UPnPServer>& list, const std::string& host, const std::string& mac, const CDateTimeSpan& wakeupDelay)
+{
+#ifdef HAS_UPNP
+ while (CDateTime::GetCurrentDateTime() < upnpInitReady)
+ KODI::TIME::Sleep(1s);
+
+ PLT_SyncMediaBrowser* browser = UPNP::CUPnP::GetInstance()->m_MediaBrowser;
+
+ if (browser)
+ {
+ UPnPServer server;
+ server.m_nextWake += wakeupDelay;
+
+ for (NPT_List<PLT_DeviceDataReference>::Iterator device = browser->GetMediaServers().GetFirstItem(); device; ++device)
+ {
+ if (host == (const char*) (*device)->GetURLBase().GetHost())
+ {
+ server.m_name = (*device)->GetFriendlyName();
+ server.m_uuid = (*device)->GetUUID();
+ server.m_mac = mac;
+
+ AddOrUpdateUPnPServer(list, server);
+ }
+ }
+ }
+#endif
+}
+
+static std::string LookupUPnPHost(const std::string& uuid)
+{
+#ifdef HAS_UPNP
+ UPNP::CUPnP* upnp = UPNP::CUPnP::GetInstance();
+
+ if (!upnp->IsClientStarted())
+ {
+ upnp->StartClient();
+
+ upnpInitReady = CDateTime::GetCurrentDateTime() + CDateTimeSpan(0, 0, 0, 10);
+ }
+
+ PLT_SyncMediaBrowser* browser = upnp->m_MediaBrowser;
+
+ PLT_DeviceDataReference device;
+
+ if (browser && NPT_SUCCEEDED(browser->FindServer(uuid.c_str(), device)) && !device.IsNull())
+ return (const char*)device->GetURLBase().GetHost();
+#endif
+
+ return "";
+}
+
+CWakeOnAccess::WakeUpEntry::WakeUpEntry (bool isAwake)
+ : timeout (0, 0, 0, DEFAULT_TIMEOUT_SEC)
+ , wait_online1_sec(DEFAULT_WAIT_FOR_ONLINE_SEC_1)
+ , wait_online2_sec(DEFAULT_WAIT_FOR_ONLINE_SEC_2)
+ , wait_services_sec(DEFAULT_WAIT_FOR_SERVICES_SEC)
+{
+ nextWake = CDateTime::GetCurrentDateTime();
+
+ if (isAwake)
+ nextWake += timeout;
+}
+
+//**
+
+class CMACDiscoveryJob : public CJob
+{
+public:
+ explicit CMACDiscoveryJob(const std::string& host) : m_host(host) {}
+
+ bool DoWork() override;
+
+ const std::string& GetMAC() const { return m_macAddress; }
+ const std::string& GetHost() const { return m_host; }
+
+private:
+ std::string m_macAddress;
+ std::string m_host;
+};
+
+bool CMACDiscoveryJob::DoWork()
+{
+ unsigned long ipAddress = HostToIP(m_host);
+
+ if (ipAddress == INADDR_NONE)
+ {
+ CLog::Log(LOGERROR, "{} - can't determine ip of '{}'", __FUNCTION__, m_host);
+ return false;
+ }
+
+ const std::vector<CNetworkInterface*>& ifaces = CServiceBroker::GetNetwork().GetInterfaceList();
+ for (const auto& it : ifaces)
+ {
+ if (it->GetHostMacAddress(ipAddress, m_macAddress))
+ return true;
+ }
+
+ return false;
+}
+
+//**
+
+class WaitCondition
+{
+public:
+ virtual ~WaitCondition() = default;
+ virtual bool SuccessWaiting () const { return false; }
+};
+
+//
+
+class NestDetect
+{
+public:
+ NestDetect() : m_gui_thread(CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ if (m_gui_thread)
+ ++m_nest;
+ }
+ ~NestDetect()
+ {
+ if (m_gui_thread)
+ m_nest--;
+ }
+ static int Level()
+ {
+ return m_nest;
+ }
+ bool IsNested() const
+ {
+ return m_gui_thread && m_nest > 1;
+ }
+
+private:
+ static int m_nest;
+ const bool m_gui_thread;
+};
+int NestDetect::m_nest = 0;
+
+//
+
+class ProgressDialogHelper
+{
+public:
+ explicit ProgressDialogHelper (const std::string& heading) : m_dialog(0)
+ {
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui)
+ m_dialog = gui->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ }
+
+ if (m_dialog)
+ {
+ m_dialog->SetHeading(CVariant{heading});
+ m_dialog->SetLine(0, CVariant{""});
+ m_dialog->SetLine(1, CVariant{""});
+ m_dialog->SetLine(2, CVariant{""});
+ }
+ }
+ ~ProgressDialogHelper ()
+ {
+ if (m_dialog)
+ m_dialog->Close();
+ }
+
+ bool HasDialog() const { return m_dialog != 0; }
+
+ enum wait_result { TimedOut, Canceled, Success };
+
+ wait_result ShowAndWait (const WaitCondition& waitObj, unsigned timeOutSec, const std::string& line1)
+ {
+ auto timeOutMs = std::chrono::milliseconds(timeOutSec * 1000);
+
+ if (m_dialog)
+ {
+ m_dialog->SetLine(0, CVariant{line1});
+
+ m_dialog->SetPercentage(1); // avoid flickering by starting at 1% ..
+ }
+
+ XbmcThreads::EndTime<> end_time(timeOutMs);
+
+ while (!end_time.IsTimePast())
+ {
+ if (waitObj.SuccessWaiting())
+ return Success;
+
+ if (m_dialog)
+ {
+ if (!m_dialog->IsActive())
+ m_dialog->Open();
+
+ if (m_dialog->IsCanceled())
+ return Canceled;
+
+ m_dialog->Progress();
+
+ auto ms_passed = timeOutMs - end_time.GetTimeLeft();
+
+ int percentage = (ms_passed.count() * 100) / timeOutMs.count();
+ m_dialog->SetPercentage(std::max(percentage, 1)); // avoid flickering , keep minimum 1%
+ }
+
+ KODI::TIME::Sleep(m_dialog ? 20ms : 200ms);
+ }
+
+ return TimedOut;
+ }
+
+private:
+ CGUIDialogProgress* m_dialog;
+};
+
+class NetworkStartWaiter : public WaitCondition
+{
+public:
+ NetworkStartWaiter (unsigned settle_time_ms, const std::string& host) : m_settle_time_ms (settle_time_ms), m_host(host)
+ {
+ }
+ bool SuccessWaiting () const override
+ {
+ unsigned long address = ntohl(HostToIP(m_host));
+ bool online = CServiceBroker::GetNetwork().HasInterfaceForIP(address);
+
+ if (!online) // setup endtime so we dont return true until network is consistently connected
+ m_end.Set(std::chrono::milliseconds(m_settle_time_ms));
+
+ return online && m_end.IsTimePast();
+ }
+private:
+ mutable XbmcThreads::EndTime<> m_end;
+ unsigned m_settle_time_ms;
+ const std::string m_host;
+};
+
+class PingResponseWaiter : public WaitCondition, private IJobCallback
+{
+public:
+ PingResponseWaiter (bool async, const CWakeOnAccess::WakeUpEntry& server)
+ : m_server(server), m_jobId(0), m_hostOnline(false)
+ {
+ if (async)
+ {
+ CJob* job = new CHostProberJob(server);
+ m_jobId = CServiceBroker::GetJobManager()->AddJob(job, this);
+ }
+ }
+ ~PingResponseWaiter() override { CServiceBroker::GetJobManager()->CancelJob(m_jobId); }
+ bool SuccessWaiting () const override
+ {
+ return m_jobId ? m_hostOnline : Ping(m_server);
+ }
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override
+ {
+ m_hostOnline = success;
+ }
+
+ static bool Ping(const CWakeOnAccess::WakeUpEntry& server, unsigned timeOutMs = 2000)
+ {
+ if (server.upnpUuid.empty())
+ {
+ unsigned long dst_ip = HostToIP(server.host);
+
+ return CServiceBroker::GetNetwork().PingHost(dst_ip, server.ping_port, timeOutMs, server.ping_mode & 1);
+ }
+ else // upnp mode
+ {
+ std::string host = LookupUPnPHost(server.upnpUuid);
+
+ if (host.empty())
+ {
+ KODI::TIME::Sleep(std::chrono::milliseconds(timeOutMs));
+
+ host = LookupUPnPHost(server.upnpUuid);
+ }
+
+ return !host.empty();
+ }
+ }
+
+private:
+ class CHostProberJob : public CJob
+ {
+ public:
+ explicit CHostProberJob(const CWakeOnAccess::WakeUpEntry& server) : m_server (server) {}
+
+ bool DoWork() override
+ {
+ while (!ShouldCancel(0,0))
+ {
+ if (PingResponseWaiter::Ping(m_server))
+ return true;
+ }
+ return false;
+ }
+
+ private:
+ const CWakeOnAccess::WakeUpEntry& m_server;
+ };
+
+ const CWakeOnAccess::WakeUpEntry& m_server;
+ unsigned int m_jobId;
+ bool m_hostOnline;
+};
+
+//
+
+CWakeOnAccess::CWakeOnAccess()
+ : m_netinit_sec(DEFAULT_NETWORK_INIT_SEC) // wait for network to connect
+ , m_netsettle_ms(DEFAULT_NETWORK_SETTLE_MS) // wait for network to settle
+{
+}
+
+CWakeOnAccess &CWakeOnAccess::GetInstance()
+{
+ static CWakeOnAccess sWakeOnAccess;
+ return sWakeOnAccess;
+}
+
+bool CWakeOnAccess::WakeUpHost(const CURL& url)
+{
+ const std::string& hostName = url.GetHostName();
+
+ if (!hostName.empty())
+ return WakeUpHost(hostName, url.Get(), url.IsProtocol("upnp"));
+
+ return true;
+}
+
+bool CWakeOnAccess::WakeUpHost(const std::string& hostName, const std::string& customMessage)
+{
+ return WakeUpHost(hostName, customMessage, false);
+}
+
+bool CWakeOnAccess::WakeUpHost(const std::string& hostName, const std::string& customMessage, bool upnpMode)
+{
+ if (!IsEnabled())
+ return true; // bail if feature is turned off
+
+ WakeUpEntry server;
+
+ if (FindOrTouchHostEntry(hostName, upnpMode, server))
+ {
+ CLog::Log(LOGINFO, "WakeOnAccess [{}] triggered by accessing : {}", server.friendlyName,
+ customMessage);
+
+ NestDetect nesting ; // detect recursive calls on gui thread..
+
+ if (nesting.IsNested()) // we might get in trouble if it gets called back in loop
+ CLog::Log(LOGWARNING, "WakeOnAccess recursively called on gui-thread [{}]",
+ NestDetect::Level());
+
+ bool ret = WakeUpHost(server);
+
+ if (!ret) // extra log if we fail for some reason
+ CLog::Log(LOGWARNING, "WakeOnAccess failed to bring up [{}] - there may be trouble ahead !",
+ server.friendlyName);
+
+ TouchHostEntry(hostName, upnpMode);
+
+ return ret;
+ }
+ return true;
+}
+
+bool CWakeOnAccess::WakeUpHost(const WakeUpEntry& server)
+{
+ std::string heading = StringUtils::Format(LOCALIZED(13027), server.friendlyName);
+
+ ProgressDialogHelper dlg (heading);
+
+ {
+ NetworkStartWaiter waitObj (m_netsettle_ms, server.host); // wait until network connected before sending wake-on-lan
+
+ if (dlg.ShowAndWait (waitObj, m_netinit_sec, LOCALIZED(13028)) != ProgressDialogHelper::Success)
+ {
+ if (CServiceBroker::GetNetwork().IsConnected() && HostToIP(server.host) == INADDR_NONE)
+ {
+ // network connected (at least one interface) but dns-lookup failed (host by name, not ip-address), so dont abort yet
+ CLog::Log(LOGWARNING, "WakeOnAccess timeout/cancel while waiting for network (proceeding anyway)");
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "WakeOnAccess timeout/cancel while waiting for network");
+ return false; // timedout or canceled ; give up
+ }
+ }
+ }
+
+ if (PingResponseWaiter::Ping(server, 500)) // quick ping with short timeout to not block too long
+ {
+ CLog::Log(LOGINFO, "WakeOnAccess success exit, server already running");
+ return true;
+ }
+
+ if (!CServiceBroker::GetNetwork().WakeOnLan(server.mac.c_str()))
+ {
+ CLog::Log(LOGERROR,"WakeOnAccess failed to send. (Is it blocked by firewall?)");
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread() || !appPlayer->IsPlaying())
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, heading, LOCALIZED(13029));
+ return false;
+ }
+
+ {
+ PingResponseWaiter waitObj (dlg.HasDialog(), server); // wait for ping response ..
+
+ ProgressDialogHelper::wait_result
+ result = dlg.ShowAndWait (waitObj, server.wait_online1_sec, LOCALIZED(13030));
+
+ if (result == ProgressDialogHelper::TimedOut)
+ result = dlg.ShowAndWait (waitObj, server.wait_online2_sec, LOCALIZED(13031));
+
+ if (result != ProgressDialogHelper::Success)
+ {
+ CLog::Log(LOGINFO, "WakeOnAccess timeout/cancel while waiting for response");
+ return false; // timedout or canceled
+ }
+ }
+
+ // we have ping response ; just add extra wait-for-services before returning if requested
+
+ {
+ WaitCondition waitObj ; // wait uninterruptible fixed time for services ..
+
+ dlg.ShowAndWait (waitObj, server.wait_services_sec, LOCALIZED(13032));
+
+ CLog::Log(LOGINFO, "WakeOnAccess sequence completed, server started");
+ }
+ return true;
+}
+
+bool CWakeOnAccess::FindOrTouchHostEntry(const std::string& hostName, bool upnpMode, WakeUpEntry& result)
+{
+ std::unique_lock<CCriticalSection> lock(m_entrylist_protect);
+
+ bool need_wakeup = false;
+
+ UPnPServer* upnp = upnpMode ? LookupUPnPServer(m_UPnPServers, hostName) : nullptr;
+
+ for (auto& server : m_entries)
+ {
+ if (upnp ? StringUtils::EqualsNoCase(upnp->m_mac, server.mac) : StringUtils::EqualsNoCase(hostName, server.host))
+ {
+ CDateTime now = CDateTime::GetCurrentDateTime();
+
+ if (now >= (upnp ? upnp->m_nextWake : server.nextWake))
+ {
+ result = server;
+
+ result.friendlyName = upnp ? upnp->m_name : server.host;
+
+ if (upnp)
+ result.upnpUuid = upnp->m_uuid;
+
+ need_wakeup = true;
+ }
+ else // 'touch' next wakeup time
+ {
+ server.nextWake = now + server.timeout;
+
+ if (upnp)
+ upnp->m_nextWake = server.nextWake;
+ }
+
+ break;
+ }
+ }
+
+ return need_wakeup;
+}
+
+void CWakeOnAccess::TouchHostEntry(const std::string& hostName, bool upnpMode)
+{
+ std::unique_lock<CCriticalSection> lock(m_entrylist_protect);
+
+ UPnPServer* upnp = upnpMode ? LookupUPnPServer(m_UPnPServers, hostName) : nullptr;
+
+ for (auto& server : m_entries)
+ {
+ if (upnp ? StringUtils::EqualsNoCase(upnp->m_mac, server.mac) : StringUtils::EqualsNoCase(hostName, server.host))
+ {
+ server.nextWake = CDateTime::GetCurrentDateTime() + server.timeout;
+
+ if (upnp)
+ upnp->m_nextWake = server.nextWake;
+
+ return;
+ }
+ }
+}
+
+static void AddHost (const std::string& host, std::vector<std::string>& hosts)
+{
+ for (const auto& it : hosts)
+ if (StringUtils::EqualsNoCase(host, it))
+ return; // already there ..
+
+ if (!host.empty())
+ hosts.push_back(host);
+}
+
+static void AddHostFromDatabase(const DatabaseSettings& setting, std::vector<std::string>& hosts)
+{
+ if (StringUtils::EqualsNoCase(setting.type, "mysql"))
+ AddHost(setting.host, hosts);
+}
+
+void CWakeOnAccess::QueueMACDiscoveryForHost(const std::string& host)
+{
+ if (IsEnabled())
+ {
+ if (URIUtils::IsHostOnLAN(host, true))
+ CServiceBroker::GetJobManager()->AddJob(new CMACDiscoveryJob(host), this);
+ else
+ CLog::Log(LOGINFO, "{} - skip Mac discovery for non-local host '{}'", __FUNCTION__, host);
+ }
+}
+
+static void AddHostsFromMediaSource(const CMediaSource& source, std::vector<std::string>& hosts)
+{
+ for (const auto& it : source.vecPaths)
+ {
+ CURL url(it);
+
+ std::string host_name = url.GetHostName();
+
+ if (url.IsProtocol("upnp"))
+ host_name = LookupUPnPHost(host_name);
+
+ AddHost(host_name, hosts);
+ }
+}
+
+static void AddHostsFromVecSource(const VECSOURCES& sources, std::vector<std::string>& hosts)
+{
+ for (const auto& it : sources)
+ AddHostsFromMediaSource(it, hosts);
+}
+
+static void AddHostsFromVecSource(const VECSOURCES* sources, std::vector<std::string>& hosts)
+{
+ if (sources)
+ AddHostsFromVecSource(*sources, hosts);
+}
+
+void CWakeOnAccess::QueueMACDiscoveryForAllRemotes()
+{
+ std::vector<std::string> hosts;
+
+ // add media sources
+ CMediaSourceSettings& ms = CMediaSourceSettings::GetInstance();
+
+ AddHostsFromVecSource(ms.GetSources("video"), hosts);
+ AddHostsFromVecSource(ms.GetSources("music"), hosts);
+ AddHostsFromVecSource(ms.GetSources("files"), hosts);
+ AddHostsFromVecSource(ms.GetSources("pictures"), hosts);
+ AddHostsFromVecSource(ms.GetSources("programs"), hosts);
+
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ // add mysql servers
+ AddHostFromDatabase(advancedSettings->m_databaseVideo, hosts);
+ AddHostFromDatabase(advancedSettings->m_databaseMusic, hosts);
+ AddHostFromDatabase(advancedSettings->m_databaseEpg, hosts);
+ AddHostFromDatabase(advancedSettings->m_databaseTV, hosts);
+
+ // add from path substitutions ..
+ for (const auto& pathPair : advancedSettings->m_pathSubstitutions)
+ {
+ CURL url(pathPair.second);
+ AddHost (url.GetHostName(), hosts);
+ }
+
+ for (const std::string& host : hosts)
+ QueueMACDiscoveryForHost(host);
+}
+
+void CWakeOnAccess::SaveMACDiscoveryResult(const std::string& host, const std::string& mac)
+{
+ CLog::Log(LOGINFO, "{} - Mac discovered for host '{}' -> '{}'", __FUNCTION__, host, mac);
+
+ for (auto& i : m_entries)
+ {
+ if (StringUtils::EqualsNoCase(host, i.host))
+ {
+ i.mac = mac;
+ ShowDiscoveryMessage(__FUNCTION__, host.c_str(), false);
+
+ AddMatchingUPnPServers(m_UPnPServers, host, mac, i.timeout);
+ SaveToXML();
+ return;
+ }
+ }
+
+ // not found entry to update - create using default values
+ WakeUpEntry entry (true);
+ entry.host = host;
+ entry.mac = mac;
+ m_entries.push_back(entry);
+ ShowDiscoveryMessage(__FUNCTION__, host.c_str(), true);
+
+ AddMatchingUPnPServers(m_UPnPServers, host, mac, entry.timeout);
+ SaveToXML();
+}
+
+void CWakeOnAccess::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ CMACDiscoveryJob* discoverJob = static_cast<CMACDiscoveryJob*>(job);
+
+ const std::string& host = discoverJob->GetHost();
+ const std::string& mac = discoverJob->GetMAC();
+
+ if (success)
+ {
+ std::unique_lock<CCriticalSection> lock(m_entrylist_protect);
+
+ SaveMACDiscoveryResult(host, mac);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{} - Mac discovery failed for host '{}'", __FUNCTION__, host);
+
+ if (IsEnabled())
+ {
+ const std::string& heading = LOCALIZED(13033);
+ std::string message = StringUtils::Format(LOCALIZED(13036), host);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, heading, message, 4000, true, 3000);
+ }
+ }
+}
+
+void CWakeOnAccess::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_POWERMANAGEMENT_WAKEONACCESS)
+ {
+ bool enabled = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+
+ SetEnabled(enabled);
+
+ if (enabled)
+ QueueMACDiscoveryForAllRemotes();
+ }
+}
+
+std::string CWakeOnAccess::GetSettingFile()
+{
+ return CSpecialProtocol::TranslatePath("special://profile/wakeonlan.xml");
+}
+
+void CWakeOnAccess::OnSettingsLoaded()
+{
+ std::unique_lock<CCriticalSection> lock(m_entrylist_protect);
+
+ LoadFromXML();
+}
+
+void CWakeOnAccess::SetEnabled(bool enabled)
+{
+ m_enabled = enabled;
+
+ CLog::Log(LOGINFO, "WakeOnAccess - Enabled:{}", m_enabled ? "TRUE" : "FALSE");
+}
+
+void CWakeOnAccess::LoadFromXML()
+{
+ bool enabled = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_POWERMANAGEMENT_WAKEONACCESS);
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(GetSettingFile()))
+ {
+ if (enabled)
+ CLog::Log(LOGINFO, "{} - unable to load:{}", __FUNCTION__, GetSettingFile());
+ return;
+ }
+
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (StringUtils::CompareNoCase(pRootElement->Value(), "onaccesswakeup"))
+ {
+ CLog::Log(LOGERROR, "{} - XML file {} doesn't contain <onaccesswakeup>", __FUNCTION__,
+ GetSettingFile());
+ return;
+ }
+
+ m_entries.clear();
+
+ CLog::Log(LOGINFO, "WakeOnAccess - Load settings :");
+
+ SetEnabled(enabled);
+
+ int tmp;
+ if (XMLUtils::GetInt(pRootElement, "netinittimeout", tmp, 0, 5 * 60))
+ m_netinit_sec = tmp;
+ CLog::Log(LOGINFO, " -Network init timeout : [{}] sec", m_netinit_sec);
+
+ if (XMLUtils::GetInt(pRootElement, "netsettletime", tmp, 0, 5 * 1000))
+ m_netsettle_ms = tmp;
+ CLog::Log(LOGINFO, " -Network settle time : [{}] ms", m_netsettle_ms);
+
+ const TiXmlNode* pWakeUp = pRootElement->FirstChildElement("wakeup");
+ while (pWakeUp)
+ {
+ WakeUpEntry entry;
+
+ std::string strtmp;
+ if (XMLUtils::GetString(pWakeUp, "host", strtmp))
+ entry.host = strtmp;
+
+ if (XMLUtils::GetString(pWakeUp, "mac", strtmp))
+ entry.mac = strtmp;
+
+ if (entry.host.empty())
+ CLog::Log(LOGERROR, "{} - Missing <host> tag or it's empty", __FUNCTION__);
+ else if (entry.mac.empty())
+ CLog::Log(LOGERROR, "{} - Missing <mac> tag or it's empty", __FUNCTION__);
+ else
+ {
+ if (XMLUtils::GetInt(pWakeUp, "pingport", tmp, 0, USHRT_MAX))
+ entry.ping_port = (unsigned short) tmp;
+
+ if (XMLUtils::GetInt(pWakeUp, "pingmode", tmp, 0, USHRT_MAX))
+ entry.ping_mode = (unsigned short) tmp;
+
+ if (XMLUtils::GetInt(pWakeUp, "timeout", tmp, 10, 12 * 60 * 60))
+ entry.timeout.SetDateTimeSpan (0, 0, 0, tmp);
+
+ if (XMLUtils::GetInt(pWakeUp, "waitonline", tmp, 0, 10 * 60)) // max 10 minutes
+ entry.wait_online1_sec = tmp;
+
+ if (XMLUtils::GetInt(pWakeUp, "waitonline2", tmp, 0, 10 * 60)) // max 10 minutes
+ entry.wait_online2_sec = tmp;
+
+ if (XMLUtils::GetInt(pWakeUp, "waitservices", tmp, 0, 5 * 60)) // max 5 minutes
+ entry.wait_services_sec = tmp;
+
+ CLog::Log(LOGINFO, " Registering wakeup entry:");
+ CLog::Log(LOGINFO, " HostName : {}", entry.host);
+ CLog::Log(LOGINFO, " MacAddress : {}", entry.mac);
+ CLog::Log(LOGINFO, " PingPort : {}", entry.ping_port);
+ CLog::Log(LOGINFO, " PingMode : {}", entry.ping_mode);
+ CLog::Log(LOGINFO, " Timeout : {} (sec)", GetTotalSeconds(entry.timeout));
+ CLog::Log(LOGINFO, " WaitForOnline : {} (sec)", entry.wait_online1_sec);
+ CLog::Log(LOGINFO, " WaitForOnlineEx : {} (sec)", entry.wait_online2_sec);
+ CLog::Log(LOGINFO, " WaitForServices : {} (sec)", entry.wait_services_sec);
+
+ m_entries.push_back(entry);
+ }
+
+ pWakeUp = pWakeUp->NextSiblingElement("wakeup"); // get next one
+ }
+
+ // load upnp server map
+ m_UPnPServers.clear();
+
+ const TiXmlNode* pUPnPNode = pRootElement->FirstChildElement("upnp_map");
+ while (pUPnPNode)
+ {
+ UPnPServer server;
+
+ XMLUtils::GetString(pUPnPNode, "name", server.m_name);
+ XMLUtils::GetString(pUPnPNode, "uuid", server.m_uuid);
+ XMLUtils::GetString(pUPnPNode, "mac", server.m_mac);
+
+ if (server.m_name.empty())
+ server.m_name = server.m_uuid;
+
+ if (server.m_uuid.empty() || server.m_mac.empty())
+ CLog::Log(LOGERROR, "{} - Missing or empty <upnp_map> entry", __FUNCTION__);
+ else
+ {
+ CLog::Log(LOGINFO, " Registering upnp_map entry [{} : {}] -> [{}]", server.m_name,
+ server.m_uuid, server.m_mac);
+
+ m_UPnPServers.push_back(server);
+ }
+
+ pUPnPNode = pUPnPNode->NextSiblingElement("upnp_map"); // get next one
+ }
+}
+
+void CWakeOnAccess::SaveToXML()
+{
+ CXBMCTinyXML xmlDoc;
+ TiXmlElement xmlRootElement("onaccesswakeup");
+ TiXmlNode *pRoot = xmlDoc.InsertEndChild(xmlRootElement);
+ if (!pRoot) return;
+
+ XMLUtils::SetInt(pRoot, "netinittimeout", m_netinit_sec);
+ XMLUtils::SetInt(pRoot, "netsettletime", m_netsettle_ms);
+
+ for (const auto& i : m_entries)
+ {
+ TiXmlElement xmlSetting("wakeup");
+ TiXmlNode* pWakeUpNode = pRoot->InsertEndChild(xmlSetting);
+ if (pWakeUpNode)
+ {
+ XMLUtils::SetString(pWakeUpNode, "host", i.host);
+ XMLUtils::SetString(pWakeUpNode, "mac", i.mac);
+ XMLUtils::SetInt(pWakeUpNode, "pingport", i.ping_port);
+ XMLUtils::SetInt(pWakeUpNode, "pingmode", i.ping_mode);
+ XMLUtils::SetInt(pWakeUpNode, "timeout", GetTotalSeconds(i.timeout));
+ XMLUtils::SetInt(pWakeUpNode, "waitonline", i.wait_online1_sec);
+ XMLUtils::SetInt(pWakeUpNode, "waitonline2", i.wait_online2_sec);
+ XMLUtils::SetInt(pWakeUpNode, "waitservices", i.wait_services_sec);
+ }
+ }
+
+ for (const auto& upnp : m_UPnPServers)
+ {
+ TiXmlElement xmlSetting("upnp_map");
+ TiXmlNode* pUPnPNode = pRoot->InsertEndChild(xmlSetting);
+ if (pUPnPNode)
+ {
+ XMLUtils::SetString(pUPnPNode, "name", upnp.m_name);
+ XMLUtils::SetString(pUPnPNode, "uuid", upnp.m_uuid);
+ XMLUtils::SetString(pUPnPNode, "mac", upnp.m_mac);
+ }
+ }
+
+ xmlDoc.SaveFile(GetSettingFile());
+}
diff --git a/xbmc/network/WakeOnAccess.h b/xbmc/network/WakeOnAccess.h
new file mode 100644
index 0000000..797f0c1
--- /dev/null
+++ b/xbmc/network/WakeOnAccess.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013-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 "URL.h"
+#include "XBDateTime.h"
+#include "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+#include "threads/CriticalSection.h"
+#include "utils/Job.h"
+
+#include <string>
+#include <vector>
+
+class CWakeOnAccess : private IJobCallback, public ISettingCallback, public ISettingsHandler
+{
+public:
+ static CWakeOnAccess &GetInstance();
+
+ bool WakeUpHost (const CURL& fileUrl);
+ bool WakeUpHost (const std::string& hostName, const std::string& customMessage);
+
+ void QueueMACDiscoveryForAllRemotes();
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingsLoaded() override;
+
+ // struct to keep per host settings
+ struct WakeUpEntry
+ {
+ explicit WakeUpEntry (bool isAwake = false);
+
+ std::string host;
+ std::string mac;
+ CDateTimeSpan timeout;
+ unsigned int wait_online1_sec; // initial wait
+ unsigned int wait_online2_sec; // extended wait
+ unsigned int wait_services_sec;
+
+ unsigned short ping_port = 0; // where to ping
+ unsigned short ping_mode = 0; // how to ping
+
+ CDateTime nextWake;
+ std::string upnpUuid; // empty unless upnpmode
+ std::string friendlyName;
+ };
+
+private:
+ CWakeOnAccess();
+ std::string GetSettingFile();
+ void LoadFromXML();
+ void SaveToXML();
+
+ void SetEnabled(bool enabled);
+ bool IsEnabled() const { return m_enabled; }
+
+ void QueueMACDiscoveryForHost(const std::string& host);
+ void SaveMACDiscoveryResult(const std::string& host, const std::string& mac);
+
+ typedef std::vector<WakeUpEntry> EntriesVector;
+ EntriesVector m_entries;
+ CCriticalSection m_entrylist_protect;
+ bool FindOrTouchHostEntry(const std::string& hostName, bool upnpMode, WakeUpEntry& server);
+ void TouchHostEntry(const std::string& hostName, bool upnpMode);
+
+ unsigned int m_netinit_sec, m_netsettle_ms; //time to wait for network connection
+
+ bool m_enabled = false;
+
+ bool WakeUpHost(const std::string& hostName, const std::string& customMessage, bool upnpMode);
+ bool WakeUpHost(const WakeUpEntry& server);
+
+ std::vector<struct UPnPServer> m_UPnPServers; // list of wakeable upnp servers
+};
diff --git a/xbmc/network/WebServer.cpp b/xbmc/network/WebServer.cpp
new file mode 100644
index 0000000..0ee1696
--- /dev/null
+++ b/xbmc/network/WebServer.cpp
@@ -0,0 +1,1409 @@
+/*
+ * 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 "WebServer.h"
+
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "filesystem/File.h"
+#include "network/httprequesthandler/HTTPRequestHandlerUtils.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+#include "utils/Mime.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <stdexcept>
+#include <utility>
+
+#if defined(TARGET_POSIX)
+#include <pthread.h>
+#endif
+
+#include <inttypes.h>
+
+#define MAX_POST_BUFFER_SIZE 2048
+
+#define PAGE_FILE_NOT_FOUND \
+ "<html><head><title>File not found</title></head><body>File not found</body></html>"
+#define NOT_SUPPORTED \
+ "<html><head><title>Not Supported</title></head><body>The method you are trying to use is not " \
+ "supported by this server</body></html>"
+
+#define HEADER_VALUE_NO_CACHE "no-cache"
+
+#define HEADER_NEWLINE "\r\n"
+
+typedef struct
+{
+ std::shared_ptr<XFILE::CFile> file;
+ CHttpRanges ranges;
+ size_t rangeCountTotal;
+ std::string boundary;
+ std::string boundaryWithHeader;
+ std::string boundaryEnd;
+ bool boundaryWritten;
+ std::string contentType;
+ uint64_t writePosition;
+} HttpFileDownloadContext;
+
+CWebServer::CWebServer()
+ : m_authenticationUsername("kodi"),
+ m_authenticationPassword(""),
+ m_key(),
+ m_cert(),
+ m_logger(CServiceBroker::GetLogging().GetLogger("CWebServer"))
+{
+#if defined(TARGET_DARWIN)
+ void* stack_addr;
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_getstack(&attr, &stack_addr, &m_thread_stacksize);
+ pthread_attr_destroy(&attr);
+ // double the stack size under darwin, not sure why yet
+ // but it stopped crashing using Kodi iOS remote -> play video.
+ // non-darwin will pass a value of zero which means 'system default'
+ m_thread_stacksize *= 2;
+ m_logger->debug("increasing thread stack to {}", m_thread_stacksize);
+#endif
+}
+
+static MHD_Response* create_response(size_t size, const void* data, int free, int copy)
+{
+ MHD_ResponseMemoryMode mode = MHD_RESPMEM_PERSISTENT;
+ if (copy)
+ mode = MHD_RESPMEM_MUST_COPY;
+ else if (free)
+ mode = MHD_RESPMEM_MUST_FREE;
+ //! @bug libmicrohttpd isn't const correct
+ return MHD_create_response_from_buffer(size, const_cast<void*>(data), mode);
+}
+
+MHD_RESULT CWebServer::AskForAuthentication(const HTTPRequest& request) const
+{
+ struct MHD_Response* response = create_response(0, nullptr, MHD_NO, MHD_NO);
+ if (!response)
+ {
+ m_logger->error("unable to create HTTP Unauthorized response");
+ return MHD_NO;
+ }
+
+ MHD_RESULT ret = AddHeader(response, MHD_HTTP_HEADER_CONNECTION, "close");
+ if (!ret)
+ {
+ m_logger->error("unable to prepare HTTP Unauthorized response");
+ MHD_destroy_response(response);
+ return MHD_NO;
+ }
+
+ LogResponse(request, MHD_HTTP_UNAUTHORIZED);
+
+ // This MHD_RESULT cast is only necessary for libmicrohttpd 0.9.71
+ // The return type of MHD_queue_basic_auth_fail_response was fixed for future versions
+ // See
+ // https://git.gnunet.org/libmicrohttpd.git/commit/?id=860b42e9180da4dcd7e8690a3fcdb4e37e5772c5
+ ret = static_cast<MHD_RESULT>(
+ MHD_queue_basic_auth_fail_response(request.connection, CCompileInfo::GetAppName(), response));
+ MHD_destroy_response(response);
+
+ return ret;
+}
+
+bool CWebServer::IsAuthenticated(const HTTPRequest& request) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_authenticationRequired)
+ return true;
+
+ // try to retrieve username and password for basic authentication
+ char* password = nullptr;
+ char* username = MHD_basic_auth_get_username_password(request.connection, &password);
+
+ if (username == nullptr || password == nullptr)
+ return false;
+
+ // compare the received username and password
+ bool authenticated = m_authenticationUsername.compare(username) == 0 &&
+ m_authenticationPassword.compare(password) == 0;
+
+ free(username);
+ free(password);
+
+ return authenticated;
+}
+
+MHD_RESULT CWebServer::AnswerToConnection(void* cls,
+ struct MHD_Connection* connection,
+ const char* url,
+ const char* method,
+ const char* version,
+ const char* upload_data,
+ size_t* upload_data_size,
+ void** con_cls)
+{
+ if (cls == nullptr || con_cls == nullptr || *con_cls == nullptr)
+ {
+ GetLogger()->error("invalid request received");
+ return MHD_NO;
+ }
+
+ CWebServer* webServer = reinterpret_cast<CWebServer*>(cls);
+
+ ConnectionHandler* connectionHandler = reinterpret_cast<ConnectionHandler*>(*con_cls);
+ HTTPMethod methodType = GetHTTPMethod(method);
+ HTTPRequest request = {webServer, connection, connectionHandler->fullUri, url, methodType,
+ version, {}};
+
+ if (connectionHandler->isNew)
+ webServer->LogRequest(request);
+
+ return webServer->HandlePartialRequest(connection, connectionHandler, request, upload_data,
+ upload_data_size, con_cls);
+}
+
+MHD_RESULT CWebServer::HandlePartialRequest(struct MHD_Connection* connection,
+ ConnectionHandler* connectionHandler,
+ const HTTPRequest& request,
+ const char* upload_data,
+ size_t* upload_data_size,
+ void** con_cls)
+{
+ std::unique_ptr<ConnectionHandler> conHandler(connectionHandler);
+
+ // remember if the request was new
+ bool isNewRequest = conHandler->isNew;
+ // because now it isn't anymore
+ conHandler->isNew = false;
+
+ // reset con_cls and set it if still necessary
+ *con_cls = nullptr;
+
+ if (!IsAuthenticated(request))
+ return AskForAuthentication(request);
+
+ // check if this is the first call to AnswerToConnection for this request
+ if (isNewRequest)
+ {
+ // look for a IHTTPRequestHandler which can take care of the current request
+ auto handler = FindRequestHandler(request);
+ if (handler != nullptr)
+ {
+ // if we got a GET request we need to check if it should be cached
+ if (request.method == GET || request.method == HEAD)
+ {
+ if (handler->CanBeCached())
+ {
+ bool cacheable = IsRequestCacheable(request);
+
+ CDateTime lastModified;
+ if (handler->GetLastModifiedDate(lastModified) && lastModified.IsValid())
+ {
+ // handle If-Modified-Since or If-Unmodified-Since
+ std::string ifModifiedSince = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_MODIFIED_SINCE);
+ std::string ifUnmodifiedSince = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE);
+
+ CDateTime ifModifiedSinceDate;
+ CDateTime ifUnmodifiedSinceDate;
+ // handle If-Modified-Since (but only if the response is cacheable)
+ if (cacheable && ifModifiedSinceDate.SetFromRFC1123DateTime(ifModifiedSince) &&
+ lastModified.GetAsUTCDateTime() <= ifModifiedSinceDate)
+ {
+ struct MHD_Response* response = create_response(0, nullptr, MHD_NO, MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP 304 response");
+ return MHD_NO;
+ }
+
+ return FinalizeRequest(handler, MHD_HTTP_NOT_MODIFIED, response);
+ }
+ // handle If-Unmodified-Since
+ else if (ifUnmodifiedSinceDate.SetFromRFC1123DateTime(ifUnmodifiedSince) &&
+ lastModified.GetAsUTCDateTime() > ifUnmodifiedSinceDate)
+ return SendErrorResponse(request, MHD_HTTP_PRECONDITION_FAILED, request.method);
+ }
+
+ // pass the requested ranges on to the request handler
+ handler->SetRequestRanged(IsRequestRanged(request, lastModified));
+ }
+ }
+ // if we got a POST request we need to take care of the POST data
+ else if (request.method == POST)
+ {
+ // as ownership of the connection handler is passed to libmicrohttpd we must not destroy it
+ SetupPostDataProcessing(request, conHandler.get(), handler, con_cls);
+
+ // as ownership of the connection handler has been passed to libmicrohttpd we must not
+ // destroy it
+ conHandler.release();
+
+ return MHD_YES;
+ }
+
+ return HandleRequest(handler);
+ }
+ }
+ // this is a subsequent call to AnswerToConnection for this request
+ else
+ {
+ // again we need to take special care of the POST data
+ if (request.method == POST)
+ {
+ // process additional / remaining POST data
+ if (ProcessPostData(request, conHandler.get(), upload_data, upload_data_size, con_cls))
+ {
+ // as ownership of the connection handler has been passed to libmicrohttpd we must not
+ // destroy it
+ conHandler.release();
+
+ return MHD_YES;
+ }
+
+ // finalize POST data processing
+ FinalizePostDataProcessing(conHandler.get());
+
+ // check if something went wrong while handling the POST data
+ if (conHandler->errorStatus != MHD_HTTP_OK)
+ return SendErrorResponse(request, conHandler->errorStatus, request.method);
+
+ // we have handled all POST data so it's time to invoke the IHTTPRequestHandler
+ return HandleRequest(conHandler->requestHandler);
+ }
+
+ // it's unusual to get more than one call to AnswerToConnection for none-POST requests, but
+ // let's handle it anyway
+ auto requestHandler = FindRequestHandler(request);
+ if (requestHandler != nullptr)
+ return HandleRequest(requestHandler);
+ }
+
+ m_logger->error("couldn't find any request handler for {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_NOT_FOUND, request.method);
+}
+
+MHD_RESULT CWebServer::HandlePostField(void* cls,
+ enum MHD_ValueKind kind,
+ const char* key,
+ const char* filename,
+ const char* content_type,
+ const char* transfer_encoding,
+ const char* data,
+ uint64_t off,
+ size_t size)
+{
+ ConnectionHandler* conHandler = (ConnectionHandler*)cls;
+
+ if (conHandler == nullptr || conHandler->requestHandler == nullptr || key == nullptr ||
+ data == nullptr || size == 0)
+ {
+ GetLogger()->error("unable to handle HTTP POST field");
+ return MHD_NO;
+ }
+
+ conHandler->requestHandler->AddPostField(key, std::string(data, size));
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::HandleRequest(const std::shared_ptr<IHTTPRequestHandler>& handler)
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ HTTPRequest request = handler->GetRequest();
+ MHD_RESULT ret = handler->HandleRequest();
+ if (ret == MHD_NO)
+ {
+ m_logger->error("failed to handle HTTP request for {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ struct MHD_Response* response = nullptr;
+ switch (responseDetails.type)
+ {
+ case HTTPNone:
+ m_logger->error("HTTP request handler didn't process {}", request.pathUrl);
+ return MHD_NO;
+
+ case HTTPRedirect:
+ ret = CreateRedirect(request.connection, handler->GetRedirectUrl(), response);
+ break;
+
+ case HTTPFileDownload:
+ ret = CreateFileDownloadResponse(handler, response);
+ break;
+
+ case HTTPMemoryDownloadNoFreeNoCopy:
+ case HTTPMemoryDownloadNoFreeCopy:
+ case HTTPMemoryDownloadFreeNoCopy:
+ case HTTPMemoryDownloadFreeCopy:
+ ret = CreateMemoryDownloadResponse(handler, response);
+ break;
+
+ case HTTPError:
+ ret =
+ CreateErrorResponse(request.connection, responseDetails.status, request.method, response);
+ break;
+
+ default:
+ m_logger->error("internal error while HTTP request handler processed {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ if (ret == MHD_NO)
+ {
+ m_logger->error("failed to create HTTP response for {}", request.pathUrl);
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ return FinalizeRequest(handler, responseDetails.status, response);
+}
+
+MHD_RESULT CWebServer::FinalizeRequest(const std::shared_ptr<IHTTPRequestHandler>& handler,
+ int responseStatus,
+ struct MHD_Response* response)
+{
+ if (handler == nullptr || response == nullptr)
+ return MHD_NO;
+
+ const HTTPRequest& request = handler->GetRequest();
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+
+ // if the request handler has set a content type and it hasn't been set as a header, add it
+ if (!responseDetails.contentType.empty())
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_TYPE, responseDetails.contentType);
+
+ // if the request handler has set a last modified date and it hasn't been set as a header, add it
+ CDateTime lastModified;
+ if (handler->GetLastModifiedDate(lastModified) && lastModified.IsValid())
+ handler->AddResponseHeader(MHD_HTTP_HEADER_LAST_MODIFIED, lastModified.GetAsRFC1123DateTime());
+
+ // check if the request handler has set Cache-Control and add it if not
+ if (!handler->HasResponseHeader(MHD_HTTP_HEADER_CACHE_CONTROL))
+ {
+ int maxAge = handler->GetMaximumAgeForCaching();
+ if (handler->CanBeCached() && maxAge == 0 && !responseDetails.contentType.empty())
+ {
+ // don't cache HTML, CSS and JavaScript files
+ if (!StringUtils::EqualsNoCase(responseDetails.contentType, "text/html") &&
+ !StringUtils::EqualsNoCase(responseDetails.contentType, "text/css") &&
+ !StringUtils::EqualsNoCase(responseDetails.contentType, "application/javascript"))
+ maxAge = CDateTimeSpan(365, 0, 0, 0).GetSecondsTotal();
+ }
+
+ // if the response can't be cached or the maximum age is 0 force the client not to cache
+ if (!handler->CanBeCached() || maxAge == 0)
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CACHE_CONTROL,
+ "private, max-age=0, " HEADER_VALUE_NO_CACHE);
+ else
+ {
+ // create the value of the Cache-Control header
+ std::string cacheControl = StringUtils::Format("public, max-age={}", maxAge);
+
+ // check if the response contains a Set-Cookie header because they must not be cached
+ if (handler->HasResponseHeader(MHD_HTTP_HEADER_SET_COOKIE))
+ cacheControl += ", no-cache=\"set-cookie\"";
+
+ // set the Cache-Control header
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CACHE_CONTROL, cacheControl);
+
+ // set the Expires header
+ CDateTime expiryTime = CDateTime::GetCurrentDateTime() + CDateTimeSpan(0, 0, 0, maxAge);
+ handler->AddResponseHeader(MHD_HTTP_HEADER_EXPIRES, expiryTime.GetAsRFC1123DateTime());
+ }
+ }
+
+ // if the request handler can handle ranges and it hasn't been set as a header, add it
+ if (handler->CanHandleRanges())
+ handler->AddResponseHeader(MHD_HTTP_HEADER_ACCEPT_RANGES, "bytes");
+ else
+ handler->AddResponseHeader(MHD_HTTP_HEADER_ACCEPT_RANGES, "none");
+
+ // add all headers set by the request handler
+ for (const auto& it : responseDetails.headers)
+ AddHeader(response, it.first, it.second);
+
+ return SendResponse(request, responseStatus, response);
+}
+
+std::shared_ptr<IHTTPRequestHandler> CWebServer::FindRequestHandler(
+ const HTTPRequest& request) const
+{
+ // look for a IHTTPRequestHandler which can take care of the current request
+ auto requestHandlerIt = std::find_if(m_requestHandlers.cbegin(), m_requestHandlers.cend(),
+ [&request](const IHTTPRequestHandler* requestHandler) {
+ return requestHandler->CanHandleRequest(request);
+ });
+
+ // we found a matching IHTTPRequestHandler so let's get a new instance for this request
+ if (requestHandlerIt != m_requestHandlers.cend())
+ return std::shared_ptr<IHTTPRequestHandler>((*requestHandlerIt)->Create(request));
+
+ return nullptr;
+}
+
+bool CWebServer::IsRequestCacheable(const HTTPRequest& request) const
+{
+ // handle Cache-Control
+ std::string cacheControl = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CACHE_CONTROL);
+ if (!cacheControl.empty())
+ {
+ std::vector<std::string> cacheControls = StringUtils::Split(cacheControl, ",");
+ for (auto control : cacheControls)
+ {
+ control = StringUtils::Trim(control);
+
+ // handle no-cache
+ if (control.compare(HEADER_VALUE_NO_CACHE) == 0)
+ return false;
+ }
+ }
+
+ // handle Pragma
+ std::string pragma = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_PRAGMA);
+ if (pragma.compare(HEADER_VALUE_NO_CACHE) == 0)
+ return false;
+
+ return true;
+}
+
+bool CWebServer::IsRequestRanged(const HTTPRequest& request, const CDateTime& lastModified) const
+{
+ // parse the Range header and store it in the request object
+ CHttpRanges ranges;
+ bool ranged = ranges.Parse(HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_RANGE));
+
+ // handle If-Range header but only if the Range header is present
+ if (ranged && lastModified.IsValid())
+ {
+ std::string ifRange = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_IF_RANGE);
+ if (!ifRange.empty() && lastModified.IsValid())
+ {
+ CDateTime ifRangeDate;
+ ifRangeDate.SetFromRFC1123DateTime(ifRange);
+
+ // check if the last modification is newer than the If-Range date
+ // if so we have to server the whole file instead
+ if (lastModified.GetAsUTCDateTime() > ifRangeDate)
+ ranges.Clear();
+ }
+ }
+
+ return !ranges.IsEmpty();
+}
+
+void CWebServer::SetupPostDataProcessing(const HTTPRequest& request,
+ ConnectionHandler* connectionHandler,
+ std::shared_ptr<IHTTPRequestHandler> handler,
+ void** con_cls) const
+{
+ connectionHandler->requestHandler = std::move(handler);
+
+ // we might need to handle the POST data ourselves which is done in the next call to
+ // AnswerToConnection
+ *con_cls = connectionHandler;
+
+ // get the content-type of the POST data
+ const auto contentType = HTTPRequestHandlerUtils::GetRequestHeaderValue(
+ request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_TYPE);
+ if (contentType.empty())
+ return;
+
+ // if the content-type is neither application/x-ww-form-urlencoded nor multipart/form-data we need
+ // to handle it ourselves
+ if (!StringUtils::EqualsNoCase(contentType, MHD_HTTP_POST_ENCODING_FORM_URLENCODED) &&
+ !StringUtils::EqualsNoCase(contentType, MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA))
+ return;
+
+ // otherwise we can use MHD's POST processor
+ connectionHandler->postprocessor = MHD_create_post_processor(
+ request.connection, MAX_POST_BUFFER_SIZE, &CWebServer::HandlePostField,
+ static_cast<void*>(connectionHandler));
+
+ // MHD doesn't seem to be able to handle this post request
+ if (connectionHandler->postprocessor == nullptr)
+ {
+ m_logger->error("unable to create HTTP POST processor for {}", request.pathUrl);
+ connectionHandler->errorStatus = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+}
+
+bool CWebServer::ProcessPostData(const HTTPRequest& request,
+ ConnectionHandler* connectionHandler,
+ const char* upload_data,
+ size_t* upload_data_size,
+ void** con_cls) const
+{
+ if (connectionHandler->requestHandler == nullptr)
+ {
+ m_logger->error("cannot handle partial HTTP POST for {} request because there is no valid "
+ "request handler available",
+ request.pathUrl);
+ connectionHandler->errorStatus = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ // we only need to handle POST data if there actually is data left to handle
+ if (*upload_data_size == 0)
+ return false;
+
+ // we may need to handle more POST data which is done in the next call to AnswerToConnection
+ *con_cls = connectionHandler;
+
+ // if nothing has gone wrong so far, process the given POST data
+ if (connectionHandler->errorStatus == MHD_HTTP_OK)
+ {
+ bool postDataHandled = false;
+ // either use MHD's POST processor
+ if (connectionHandler->postprocessor != nullptr)
+ postDataHandled = MHD_post_process(connectionHandler->postprocessor, upload_data,
+ *upload_data_size) == MHD_YES;
+ // or simply copy the data to the handler
+ else if (connectionHandler->requestHandler != nullptr)
+ postDataHandled =
+ connectionHandler->requestHandler->AddPostData(upload_data, *upload_data_size);
+
+ // abort if the received POST data couldn't be handled
+ if (!postDataHandled)
+ {
+ m_logger->error("failed to handle HTTP POST data for {}", request.pathUrl);
+#if (MHD_VERSION >= 0x00097400)
+ connectionHandler->errorStatus = MHD_HTTP_CONTENT_TOO_LARGE;
+#elif (MHD_VERSION >= 0x00095213)
+ connectionHandler->errorStatus = MHD_HTTP_PAYLOAD_TOO_LARGE;
+#else
+ connectionHandler->errorStatus = MHD_HTTP_REQUEST_ENTITY_TOO_LARGE;
+#endif
+ }
+ }
+
+ // signal that we have handled the data
+ *upload_data_size = 0;
+
+ return true;
+}
+
+void CWebServer::FinalizePostDataProcessing(ConnectionHandler* connectionHandler) const
+{
+ if (connectionHandler->postprocessor == nullptr)
+ return;
+
+ MHD_destroy_post_processor(connectionHandler->postprocessor);
+}
+
+MHD_RESULT CWebServer::CreateMemoryDownloadResponse(
+ const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response*& response) const
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ const HTTPRequest& request = handler->GetRequest();
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ HttpResponseRanges responseRanges = handler->GetResponseData();
+
+ // check if the response is completely empty
+ if (responseRanges.empty())
+ return CreateMemoryDownloadResponse(request.connection, nullptr, 0, false, false, response);
+
+ // check if the response contains more ranges than the request asked for
+ if ((request.ranges.IsEmpty() && responseRanges.size() > 1) ||
+ (!request.ranges.IsEmpty() && responseRanges.size() > request.ranges.Size()))
+ {
+ m_logger->warn("response contains more ranges ({}) than the request asked for ({})",
+ static_cast<int>(responseRanges.size()),
+ static_cast<int>(request.ranges.Size()));
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ // if the request asked for no or only one range we can simply use MHDs memory download handler
+ // we MUST NOT send a multipart response
+ if (request.ranges.Size() <= 1)
+ {
+ CHttpResponseRange responseRange = responseRanges.front();
+ // check if the range is valid
+ if (!responseRange.IsValid())
+ {
+ m_logger->warn("invalid response data with range start at {} and end at {}",
+ responseRange.GetFirstPosition(), responseRange.GetLastPosition());
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+
+ const void* responseData = responseRange.GetData();
+ size_t responseDataLength = static_cast<size_t>(responseRange.GetLength());
+
+ switch (responseDetails.type)
+ {
+ case HTTPMemoryDownloadNoFreeNoCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ false, false, response);
+
+ case HTTPMemoryDownloadNoFreeCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ false, true, response);
+
+ case HTTPMemoryDownloadFreeNoCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ true, false, response);
+
+ case HTTPMemoryDownloadFreeCopy:
+ return CreateMemoryDownloadResponse(request.connection, responseData, responseDataLength,
+ true, true, response);
+
+ default:
+ return SendErrorResponse(request, MHD_HTTP_INTERNAL_SERVER_ERROR, request.method);
+ }
+ }
+
+ return CreateRangedMemoryDownloadResponse(handler, response);
+}
+
+MHD_RESULT CWebServer::CreateRangedMemoryDownloadResponse(
+ const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response*& response) const
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ const HTTPRequest& request = handler->GetRequest();
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ HttpResponseRanges responseRanges = handler->GetResponseData();
+
+ // if there's no or only one range this is not the right place
+ if (responseRanges.size() <= 1)
+ return CreateMemoryDownloadResponse(handler, response);
+
+ // extract all the valid ranges and calculate their total length
+ uint64_t firstRangePosition = 0;
+ HttpResponseRanges ranges;
+ for (const auto& range : responseRanges)
+ {
+ // ignore invalid ranges
+ if (!range.IsValid())
+ continue;
+
+ // determine the first range position
+ if (ranges.empty())
+ firstRangePosition = range.GetFirstPosition();
+
+ ranges.push_back(range);
+ }
+
+ if (ranges.empty())
+ return CreateMemoryDownloadResponse(request.connection, nullptr, 0, false, false, response);
+
+ // determine the last range position
+ uint64_t lastRangePosition = ranges.back().GetLastPosition();
+
+ // adjust the HTTP status of the response
+ handler->SetResponseStatus(MHD_HTTP_PARTIAL_CONTENT);
+ // add Content-Range header
+ handler->AddResponseHeader(
+ MHD_HTTP_HEADER_CONTENT_RANGE,
+ HttpRangeUtils::GenerateContentRangeHeaderValue(firstRangePosition, lastRangePosition,
+ responseDetails.totalLength));
+
+ // generate a multipart boundary
+ std::string multipartBoundary = HttpRangeUtils::GenerateMultipartBoundary();
+ // and the content-type
+ std::string contentType = HttpRangeUtils::GenerateMultipartBoundaryContentType(multipartBoundary);
+
+ // add Content-Type header
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_TYPE, contentType);
+
+ // generate the multipart boundary with the Content-Type header field
+ std::string multipartBoundaryWithHeader =
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(multipartBoundary, contentType);
+
+ std::string result;
+ // add all the ranges to the result
+ for (HttpResponseRanges::const_iterator range = ranges.begin(); range != ranges.end(); ++range)
+ {
+ // add a newline before any new multipart boundary
+ if (range != ranges.begin())
+ result += HEADER_NEWLINE;
+
+ // generate and append the multipart boundary with the full header (Content-Type and
+ // Content-Length)
+ result +=
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(multipartBoundaryWithHeader, &*range);
+
+ // and append the data of the range
+ result.append(static_cast<const char*>(range->GetData()),
+ static_cast<size_t>(range->GetLength()));
+
+ // check if we need to free the range data
+ if (responseDetails.type == HTTPMemoryDownloadFreeNoCopy ||
+ responseDetails.type == HTTPMemoryDownloadFreeCopy)
+ free(const_cast<void*>(range->GetData()));
+ }
+
+ result += HttpRangeUtils::GenerateMultipartBoundaryEnd(multipartBoundary);
+
+ // add Content-Length header
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_LENGTH,
+ std::to_string(static_cast<uint64_t>(result.size())));
+
+ // finally create the response
+ return CreateMemoryDownloadResponse(request.connection, result.c_str(), result.size(), false,
+ true, response);
+}
+
+MHD_RESULT CWebServer::CreateRedirect(struct MHD_Connection* connection,
+ const std::string& strURL,
+ struct MHD_Response*& response) const
+{
+ response = create_response(0, nullptr, MHD_NO, MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create HTTP redirect response to {}", strURL);
+ return MHD_NO;
+ }
+
+ AddHeader(response, MHD_HTTP_HEADER_LOCATION, strURL);
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::CreateFileDownloadResponse(
+ const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response*& response) const
+{
+ if (handler == nullptr)
+ return MHD_NO;
+
+ const HTTPRequest& request = handler->GetRequest();
+ const HTTPResponseDetails& responseDetails = handler->GetResponseDetails();
+ HttpResponseRanges responseRanges = handler->GetResponseData();
+
+ std::shared_ptr<XFILE::CFile> file = std::make_shared<XFILE::CFile>();
+ std::string filePath = handler->GetResponseFile();
+
+ // access check
+ if (!CFileUtils::CheckFileAccessAllowed(filePath))
+ return SendErrorResponse(request, MHD_HTTP_NOT_FOUND, request.method);
+
+ if (!file->Open(filePath, XFILE::READ_NO_CACHE))
+ {
+ m_logger->error("Failed to open {}", filePath);
+ return SendErrorResponse(request, MHD_HTTP_NOT_FOUND, request.method);
+ }
+
+ bool ranged = false;
+ uint64_t fileLength = static_cast<uint64_t>(file->GetLength());
+
+ // get the MIME type for the Content-Type header
+ std::string mimeType = responseDetails.contentType;
+ if (mimeType.empty())
+ {
+ std::string ext = URIUtils::GetExtension(filePath);
+ StringUtils::ToLower(ext);
+ mimeType = CreateMimeTypeFromExtension(ext.c_str());
+ }
+
+ uint64_t totalLength = 0;
+ std::unique_ptr<HttpFileDownloadContext> context = std::make_unique<HttpFileDownloadContext>();
+ context->file = file;
+ context->contentType = mimeType;
+ context->boundaryWritten = false;
+ context->writePosition = 0;
+
+ if (handler->IsRequestRanged())
+ {
+ if (!request.ranges.IsEmpty())
+ context->ranges = request.ranges;
+ else
+ HTTPRequestHandlerUtils::GetRequestedRanges(request.connection, fileLength, context->ranges);
+ }
+
+ uint64_t firstPosition = 0;
+ uint64_t lastPosition = 0;
+ // if there are no ranges, add the whole range
+ if (context->ranges.IsEmpty())
+ context->ranges.Add(CHttpRange(0, fileLength - 1));
+ else
+ {
+ handler->SetResponseStatus(MHD_HTTP_PARTIAL_CONTENT);
+
+ // we need to remember that we are ranged because the range length might change and won't be
+ // reliable anymore for length comparisons
+ ranged = true;
+
+ context->ranges.GetFirstPosition(firstPosition);
+ context->ranges.GetLastPosition(lastPosition);
+ }
+
+ // remember the total number of ranges
+ context->rangeCountTotal = context->ranges.Size();
+ // remember the total length
+ totalLength = context->ranges.GetLength();
+
+ // adjust the MIME type and range length in case of multiple ranges which requires multipart
+ // boundaries
+ if (context->rangeCountTotal > 1)
+ {
+ context->boundary = HttpRangeUtils::GenerateMultipartBoundary();
+ mimeType = HttpRangeUtils::GenerateMultipartBoundaryContentType(context->boundary);
+
+ // build part of the boundary with the optional Content-Type header
+ // "--<boundary>\r\nContent-Type: <content-type>\r\n
+ context->boundaryWithHeader = HttpRangeUtils::GenerateMultipartBoundaryWithHeader(
+ context->boundary, context->contentType);
+ context->boundaryEnd = HttpRangeUtils::GenerateMultipartBoundaryEnd(context->boundary);
+
+ // for every range, we need to add a boundary with header
+ for (HttpRanges::const_iterator range = context->ranges.Begin(); range != context->ranges.End();
+ ++range)
+ {
+ // we need to temporarily add the Content-Range header to the boundary to be able to
+ // determine the length
+ std::string completeBoundaryWithHeader =
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(context->boundaryWithHeader, &*range);
+ totalLength += completeBoundaryWithHeader.size();
+
+ // add a newline before any new multipart boundary
+ if (range != context->ranges.Begin())
+ totalLength += strlen(HEADER_NEWLINE);
+ }
+ // and at the very end a special end-boundary "\r\n--<boundary>--"
+ totalLength += context->boundaryEnd.size();
+ }
+
+ // set the initial write position
+ context->ranges.GetFirstPosition(context->writePosition);
+
+ // create the response object
+ response =
+ MHD_create_response_from_callback(totalLength, 2048, &CWebServer::ContentReaderCallback,
+ context.get(), &CWebServer::ContentReaderFreeCallback);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP response for {} to be filled from{}", request.pathUrl,
+ filePath);
+ return MHD_NO;
+ }
+
+ context.release(); // ownership was passed to mhd
+
+ // add Content-Range header
+ if (ranged)
+ handler->AddResponseHeader(
+ MHD_HTTP_HEADER_CONTENT_RANGE,
+ HttpRangeUtils::GenerateContentRangeHeaderValue(firstPosition, lastPosition, fileLength));
+
+ // set the Content-Type header
+ if (!mimeType.empty())
+ handler->AddResponseHeader(MHD_HTTP_HEADER_CONTENT_TYPE, mimeType);
+
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::CreateErrorResponse(struct MHD_Connection* connection,
+ int responseType,
+ HTTPMethod method,
+ struct MHD_Response*& response) const
+{
+ size_t payloadSize = 0;
+ const void* payload = nullptr;
+
+ switch (responseType)
+ {
+ case MHD_HTTP_NOT_FOUND:
+ payloadSize = strlen(PAGE_FILE_NOT_FOUND);
+ payload = (const void*)PAGE_FILE_NOT_FOUND;
+ break;
+
+ case MHD_HTTP_NOT_IMPLEMENTED:
+ payloadSize = strlen(NOT_SUPPORTED);
+ payload = (const void*)NOT_SUPPORTED;
+ break;
+ }
+
+ response = create_response(payloadSize, payload, MHD_NO, MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP {} error response", responseType);
+ return MHD_NO;
+ }
+
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::CreateMemoryDownloadResponse(struct MHD_Connection* connection,
+ const void* data,
+ size_t size,
+ bool free,
+ bool copy,
+ struct MHD_Response*& response) const
+{
+ response = create_response(size, const_cast<void*>(data), free ? MHD_YES : MHD_NO,
+ copy ? MHD_YES : MHD_NO);
+ if (response == nullptr)
+ {
+ m_logger->error("failed to create a HTTP download response");
+ return MHD_NO;
+ }
+
+ return MHD_YES;
+}
+
+MHD_RESULT CWebServer::SendResponse(const HTTPRequest& request,
+ int responseStatus,
+ MHD_Response* response) const
+{
+ LogResponse(request, responseStatus);
+
+ MHD_RESULT ret = MHD_queue_response(request.connection, responseStatus, response);
+ MHD_destroy_response(response);
+
+ return ret;
+}
+
+MHD_RESULT CWebServer::SendErrorResponse(const HTTPRequest& request,
+ int errorType,
+ HTTPMethod method) const
+{
+ struct MHD_Response* response = nullptr;
+ MHD_RESULT ret = CreateErrorResponse(request.connection, errorType, method, response);
+ if (ret == MHD_NO)
+ return MHD_NO;
+
+ return SendResponse(request, errorType, response);
+}
+
+void* CWebServer::UriRequestLogger(void* cls, const char* uri)
+{
+ CWebServer* webServer = reinterpret_cast<CWebServer*>(cls);
+
+ // log the full URI
+ if (webServer == nullptr)
+ GetLogger()->debug("request received for {}", uri);
+ else
+ webServer->LogRequest(uri);
+
+ // create and return a new connection handler
+ return new ConnectionHandler(uri);
+}
+
+void CWebServer::LogRequest(const char* uri) const
+{
+ if (uri == nullptr)
+ return;
+
+ m_logger->debug("request received for {}", uri);
+}
+
+ssize_t CWebServer::ContentReaderCallback(void* cls, uint64_t pos, char* buf, size_t max)
+{
+ HttpFileDownloadContext* context = (HttpFileDownloadContext*)cls;
+ if (context == nullptr || context->file == nullptr)
+ return -1;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ GetLogger()->debug("[OUT] write maximum {} bytes from {} ({})", max, context->writePosition,
+ pos);
+
+ // check if we need to add the end-boundary
+ if (context->rangeCountTotal > 1 && context->ranges.IsEmpty())
+ {
+ // put together the end-boundary
+ std::string endBoundary = HttpRangeUtils::GenerateMultipartBoundaryEnd(context->boundary);
+ if ((unsigned int)max != endBoundary.size())
+ return -1;
+
+ // copy the boundary into the buffer
+ memcpy(buf, endBoundary.c_str(), endBoundary.size());
+ return endBoundary.size();
+ }
+
+ CHttpRange range;
+ if (context->ranges.IsEmpty() || !context->ranges.GetFirst(range))
+ return -1;
+
+ uint64_t start = range.GetFirstPosition();
+ uint64_t end = range.GetLastPosition();
+ uint64_t maximum = (uint64_t)max;
+ int written = 0;
+
+ if (context->rangeCountTotal > 1 && !context->boundaryWritten)
+ {
+ // add a newline before any new multipart boundary
+ if (context->rangeCountTotal > context->ranges.Size())
+ {
+ size_t newlineLength = strlen(HEADER_NEWLINE);
+ memcpy(buf, HEADER_NEWLINE, newlineLength);
+ buf += newlineLength;
+ written += newlineLength;
+ maximum -= newlineLength;
+ }
+
+ // put together the boundary for the current range
+ std::string boundary =
+ HttpRangeUtils::GenerateMultipartBoundaryWithHeader(context->boundaryWithHeader, &range);
+
+ // copy the boundary into the buffer
+ memcpy(buf, boundary.c_str(), boundary.size());
+ // advance the buffer position
+ buf += boundary.size();
+ // update the number of written byte
+ written += boundary.size();
+ // update the maximum number of bytes
+ maximum -= boundary.size();
+ context->boundaryWritten = true;
+ }
+
+ // check if the current position is within this range
+ // if not, set it to the start position
+ if (context->writePosition < start || context->writePosition > end)
+ context->writePosition = start;
+ // adjust the maximum number of read bytes
+ maximum = std::min(maximum, end - context->writePosition + 1);
+
+ // seek to the position if necessary
+ if (context->file->GetPosition() < 0 ||
+ context->writePosition != static_cast<uint64_t>(context->file->GetPosition()))
+ context->file->Seek(context->writePosition);
+
+ // read data from the file
+ ssize_t res = context->file->Read(buf, static_cast<size_t>(maximum));
+ if (res <= 0)
+ return -1;
+
+ // add the number of read bytes to the number of written bytes
+ written += res;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ GetLogger()->debug("[OUT] wrote {} bytes from {} in range ({} - {})", written,
+ context->writePosition, start, end);
+
+ // update the current write position
+ context->writePosition += res;
+
+ // if we have read all the data from the current range
+ // remove it from the list
+ if (context->writePosition >= end + 1)
+ {
+ context->ranges.Remove(0);
+ context->boundaryWritten = false;
+ }
+
+ return written;
+}
+
+void CWebServer::ContentReaderFreeCallback(void* cls)
+{
+ HttpFileDownloadContext* context = (HttpFileDownloadContext*)cls;
+ delete context;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ GetLogger()->debug("[OUT] done");
+}
+
+static Logger GetMhdLogger()
+{
+ return CServiceBroker::GetLogging().GetLogger("libmicrohttpd");
+}
+
+// local helper
+static void panicHandlerForMHD(void* unused,
+ const char* file,
+ unsigned int line,
+ const char* reason)
+{
+ GetMhdLogger()->critical("serious error: reason \"{}\" in file \"{}\" at line {}",
+ reason ? reason : "", file ? file : "", line);
+ throw std::runtime_error("MHD serious error"); // FIXME: better solution?
+}
+
+// local helper
+static void logFromMHD(void* unused, const char* fmt, va_list ap)
+{
+ Logger logger = GetMhdLogger();
+ if (fmt == nullptr || fmt[0] == 0)
+ GetMhdLogger()->error("reported error with empty string");
+ else
+ {
+ std::string errDsc = StringUtils::FormatV(fmt, ap);
+ if (errDsc.empty())
+ GetMhdLogger()->error("reported error with unprintable string \"{}\"", fmt);
+ else
+ {
+ if (errDsc.at(errDsc.length() - 1) == '\n')
+ errDsc.erase(errDsc.length() - 1);
+
+ // Most common error is "aborted connection", so log it at LOGDEBUG level
+ GetMhdLogger()->debug(errDsc);
+ }
+ }
+}
+
+bool CWebServer::LoadCert(std::string& skey, std::string& scert)
+{
+ XFILE::CFile file;
+ std::vector<uint8_t> buf;
+ const char* keyFile = "special://userdata/server.key";
+ const char* certFile = "special://userdata/server.pem";
+
+ if (!file.Exists(keyFile) || !file.Exists(certFile))
+ return false;
+
+ if (file.LoadFile(keyFile, buf) > 0)
+ {
+ skey.resize(buf.size());
+ skey.assign(reinterpret_cast<char*>(buf.data()));
+ file.Close();
+ }
+ else
+ m_logger->error("{}: Error loading: {}", __FUNCTION__, keyFile);
+
+ if (file.LoadFile(certFile, buf) > 0)
+ {
+ scert.resize(buf.size());
+ scert.assign(reinterpret_cast<char*>(buf.data()));
+ file.Close();
+ }
+ else
+ m_logger->error("{}: Error loading: {}", __FUNCTION__, certFile);
+
+ if (!skey.empty() && !scert.empty())
+ {
+ m_logger->info("{}: found server key: {}, certificate: {}, HTTPS support enabled", __FUNCTION__,
+ keyFile, certFile);
+ return true;
+ }
+ return false;
+}
+
+struct MHD_Daemon* CWebServer::StartMHD(unsigned int flags, int port)
+{
+ unsigned int timeout = 60 * 60 * 24;
+ const char* ciphers = "NORMAL:-VERS-TLS1.0";
+
+ MHD_set_panic_func(&panicHandlerForMHD, nullptr);
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_SERVICES_WEBSERVERSSL) &&
+ MHD_is_feature_supported(MHD_FEATURE_SSL) == MHD_YES && LoadCert(m_key, m_cert))
+ // SSL enabled
+ return MHD_start_daemon(
+ flags |
+ // one thread per connection
+ // WARNING: set MHD_OPTION_CONNECTION_TIMEOUT to something higher than 1
+ // otherwise on libmicrohttpd 0.4.4-1 it spins a busy loop
+ MHD_USE_THREAD_PER_CONNECTION
+#if (MHD_VERSION >= 0x00095207)
+ |
+ MHD_USE_INTERNAL_POLLING_THREAD /* MHD_USE_THREAD_PER_CONNECTION must be used only with
+ MHD_USE_INTERNAL_POLLING_THREAD since 0.9.54 */
+#endif
+ | MHD_USE_DEBUG /* Print MHD error messages to log */
+ | MHD_USE_SSL,
+ port, 0, 0, &CWebServer::AnswerToConnection, this,
+
+ MHD_OPTION_EXTERNAL_LOGGER, &logFromMHD, 0, MHD_OPTION_CONNECTION_LIMIT, 512,
+ MHD_OPTION_CONNECTION_TIMEOUT, timeout, MHD_OPTION_URI_LOG_CALLBACK,
+ &CWebServer::UriRequestLogger, this, MHD_OPTION_THREAD_STACK_SIZE, m_thread_stacksize,
+ MHD_OPTION_HTTPS_MEM_KEY, m_key.c_str(), MHD_OPTION_HTTPS_MEM_CERT, m_cert.c_str(),
+ MHD_OPTION_HTTPS_PRIORITIES, ciphers, MHD_OPTION_END);
+
+ // No SSL
+ return MHD_start_daemon(
+ flags |
+ // one thread per connection
+ // WARNING: set MHD_OPTION_CONNECTION_TIMEOUT to something higher than 1
+ // otherwise on libmicrohttpd 0.4.4-1 it spins a busy loop
+ MHD_USE_THREAD_PER_CONNECTION
+#if (MHD_VERSION >= 0x00095207)
+ | MHD_USE_INTERNAL_POLLING_THREAD /* MHD_USE_THREAD_PER_CONNECTION must be used only with
+ MHD_USE_INTERNAL_POLLING_THREAD since 0.9.54 */
+#endif
+ | MHD_USE_DEBUG /* Print MHD error messages to log */
+ ,
+ port, 0, 0, &CWebServer::AnswerToConnection, this,
+
+ MHD_OPTION_EXTERNAL_LOGGER, &logFromMHD, 0, MHD_OPTION_CONNECTION_LIMIT, 512,
+ MHD_OPTION_CONNECTION_TIMEOUT, timeout, MHD_OPTION_URI_LOG_CALLBACK,
+ &CWebServer::UriRequestLogger, this, MHD_OPTION_THREAD_STACK_SIZE, m_thread_stacksize,
+ MHD_OPTION_END);
+}
+
+bool CWebServer::Start(uint16_t port, const std::string& username, const std::string& password)
+{
+ SetCredentials(username, password);
+ if (!m_running)
+ {
+ // use a new logger containing the port in the name
+ m_logger = CServiceBroker::GetLogging().GetLogger(StringUtils::Format("CWebserver[{}]", port));
+
+ int v6testSock;
+ if ((v6testSock = socket(AF_INET6, SOCK_STREAM, 0)) >= 0)
+ {
+ closesocket(v6testSock);
+ m_daemon_ip6 = StartMHD(MHD_USE_IPv6, port);
+ }
+ m_daemon_ip4 = StartMHD(0, port);
+
+ m_running = (m_daemon_ip6 != nullptr) || (m_daemon_ip4 != nullptr);
+ if (m_running)
+ {
+ m_port = port;
+ m_logger->info("Started");
+ }
+ else
+ m_logger->error("Failed to start");
+ }
+
+ return m_running;
+}
+
+bool CWebServer::Stop()
+{
+ if (!m_running)
+ return true;
+
+ if (m_daemon_ip6 != nullptr)
+ MHD_stop_daemon(m_daemon_ip6);
+
+ if (m_daemon_ip4 != nullptr)
+ MHD_stop_daemon(m_daemon_ip4);
+
+ m_running = false;
+ m_logger->info("Stopped");
+ m_port = 0;
+
+ return true;
+}
+
+bool CWebServer::IsStarted()
+{
+ return m_running;
+}
+
+bool CWebServer::WebServerSupportsSSL()
+{
+ return MHD_is_feature_supported(MHD_FEATURE_SSL) == MHD_YES;
+}
+
+void CWebServer::SetCredentials(const std::string& username, const std::string& password)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_authenticationUsername = username;
+ m_authenticationPassword = password;
+ m_authenticationRequired = !m_authenticationPassword.empty();
+}
+
+void CWebServer::RegisterRequestHandler(IHTTPRequestHandler* handler)
+{
+ if (handler == nullptr)
+ return;
+
+ const auto& it = std::find(m_requestHandlers.cbegin(), m_requestHandlers.cend(), handler);
+ if (it != m_requestHandlers.cend())
+ return;
+
+ m_requestHandlers.push_back(handler);
+ std::sort(m_requestHandlers.begin(), m_requestHandlers.end(),
+ [](IHTTPRequestHandler* lhs, IHTTPRequestHandler* rhs) {
+ return rhs->GetPriority() < lhs->GetPriority();
+ });
+}
+
+void CWebServer::UnregisterRequestHandler(IHTTPRequestHandler* handler)
+{
+ if (handler == nullptr)
+ return;
+
+ m_requestHandlers.erase(std::remove(m_requestHandlers.begin(), m_requestHandlers.end(), handler),
+ m_requestHandlers.end());
+}
+
+void CWebServer::LogRequest(const HTTPRequest& request) const
+{
+ if (!CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ return;
+
+ std::multimap<std::string, std::string> headerValues;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_HEADER_KIND,
+ headerValues);
+ std::multimap<std::string, std::string> getValues;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_GET_ARGUMENT_KIND,
+ getValues);
+
+ m_logger->debug(" [IN] {} {} {}", request.version, GetHTTPMethod(request.method),
+ request.pathUrlFull);
+
+ if (!getValues.empty())
+ {
+ std::vector<std::string> values;
+ for (const auto& get : getValues)
+ values.push_back(get.first + " = " + get.second);
+
+ m_logger->debug(" [IN] Query arguments: {}", StringUtils::Join(values, "; "));
+ }
+
+ for (const auto& header : headerValues)
+ m_logger->debug(" [IN] {}: {}", header.first, header.second);
+}
+
+void CWebServer::LogResponse(const HTTPRequest& request, int responseStatus) const
+{
+ if (!CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ return;
+
+ std::multimap<std::string, std::string> headerValues;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_HEADER_KIND,
+ headerValues);
+
+ m_logger->debug("[OUT] {} {} {}", request.version, responseStatus, request.pathUrlFull);
+
+ for (const auto& header : headerValues)
+ m_logger->debug("[OUT] {}: {}", header.first, header.second);
+}
+
+std::string CWebServer::CreateMimeTypeFromExtension(const char* ext)
+{
+ if (strcmp(ext, ".kar") == 0)
+ return "audio/midi";
+ if (strcmp(ext, ".tbn") == 0)
+ return "image/jpeg";
+
+ return CMime::GetMimeType(ext);
+}
+
+MHD_RESULT CWebServer::AddHeader(struct MHD_Response* response,
+ const std::string& name,
+ const std::string& value) const
+{
+ if (response == nullptr || name.empty())
+ return MHD_NO;
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWEBSERVER))
+ m_logger->debug("[OUT] {}: {}", name, value);
+
+ if (name == MHD_HTTP_HEADER_CONTENT_LENGTH)
+ m_logger->warn("Attempt to override MHD automatic \"Content-Length\" header");
+
+ return MHD_add_response_header(response, name.c_str(), value.c_str());
+}
+
+Logger CWebServer::GetLogger()
+{
+ static Logger s_logger = CServiceBroker::GetLogging().GetLogger("CWebServer");
+ return s_logger;
+}
diff --git a/xbmc/network/WebServer.h b/xbmc/network/WebServer.h
new file mode 100644
index 0000000..4127524
--- /dev/null
+++ b/xbmc/network/WebServer.h
@@ -0,0 +1,131 @@
+/*
+ * 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 "network/httprequesthandler/IHTTPRequestHandler.h"
+#include "threads/CriticalSection.h"
+#include "utils/logtypes.h"
+
+#include <memory>
+#include <vector>
+
+namespace XFILE
+{
+ class CFile;
+}
+class CDateTime;
+class CVariant;
+
+class CWebServer
+{
+public:
+ CWebServer();
+ virtual ~CWebServer() = default;
+
+ bool Start(uint16_t port, const std::string &username, const std::string &password);
+ bool Stop();
+ bool IsStarted();
+ static bool WebServerSupportsSSL();
+ void SetCredentials(const std::string &username, const std::string &password);
+
+ void RegisterRequestHandler(IHTTPRequestHandler *handler);
+ void UnregisterRequestHandler(IHTTPRequestHandler *handler);
+
+protected:
+ typedef struct ConnectionHandler
+ {
+ std::string fullUri;
+ bool isNew;
+ std::shared_ptr<IHTTPRequestHandler> requestHandler;
+ struct MHD_PostProcessor *postprocessor;
+ int errorStatus;
+
+ explicit ConnectionHandler(const std::string& uri)
+ : fullUri(uri)
+ , isNew(true)
+ , requestHandler(nullptr)
+ , postprocessor(nullptr)
+ , errorStatus(MHD_HTTP_OK)
+ { }
+ } ConnectionHandler;
+
+ virtual void LogRequest(const char* uri) const;
+
+ virtual MHD_RESULT HandlePartialRequest(struct MHD_Connection *connection, ConnectionHandler* connectionHandler, const HTTPRequest& request,
+ const char *upload_data, size_t *upload_data_size, void **con_cls);
+ virtual MHD_RESULT HandleRequest(const std::shared_ptr<IHTTPRequestHandler>& handler);
+ virtual MHD_RESULT FinalizeRequest(const std::shared_ptr<IHTTPRequestHandler>& handler, int responseStatus, struct MHD_Response *response);
+
+private:
+ struct MHD_Daemon* StartMHD(unsigned int flags, int port);
+
+ std::shared_ptr<IHTTPRequestHandler> FindRequestHandler(const HTTPRequest& request) const;
+
+ MHD_RESULT AskForAuthentication(const HTTPRequest& request) const;
+ bool IsAuthenticated(const HTTPRequest& request) const;
+
+ bool IsRequestCacheable(const HTTPRequest& request) const;
+ bool IsRequestRanged(const HTTPRequest& request, const CDateTime &lastModified) const;
+
+ void SetupPostDataProcessing(const HTTPRequest& request, ConnectionHandler *connectionHandler, std::shared_ptr<IHTTPRequestHandler> handler, void **con_cls) const;
+ bool ProcessPostData(const HTTPRequest& request, ConnectionHandler *connectionHandler, const char *upload_data, size_t *upload_data_size, void **con_cls) const;
+ void FinalizePostDataProcessing(ConnectionHandler *connectionHandler) const;
+
+ MHD_RESULT CreateMemoryDownloadResponse(const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response *&response) const;
+ MHD_RESULT CreateRangedMemoryDownloadResponse(const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response *&response) const;
+
+ MHD_RESULT CreateRedirect(struct MHD_Connection *connection, const std::string &strURL, struct MHD_Response *&response) const;
+ MHD_RESULT CreateFileDownloadResponse(const std::shared_ptr<IHTTPRequestHandler>& handler, struct MHD_Response *&response) const;
+ MHD_RESULT CreateErrorResponse(struct MHD_Connection *connection, int responseType, HTTPMethod method, struct MHD_Response *&response) const;
+ MHD_RESULT CreateMemoryDownloadResponse(struct MHD_Connection *connection, const void *data, size_t size, bool free, bool copy, struct MHD_Response *&response) const;
+
+ MHD_RESULT SendResponse(const HTTPRequest& request, int responseStatus, MHD_Response *response) const;
+ MHD_RESULT SendErrorResponse(const HTTPRequest& request, int errorType, HTTPMethod method) const;
+
+ MHD_RESULT AddHeader(struct MHD_Response *response, const std::string &name, const std::string &value) const;
+
+ void LogRequest(const HTTPRequest& request) const;
+ void LogResponse(const HTTPRequest& request, int responseStatus) const;
+
+ static std::string CreateMimeTypeFromExtension(const char *ext);
+
+ // MHD callback implementations
+ static void* UriRequestLogger(void *cls, const char *uri);
+
+ static ssize_t ContentReaderCallback (void *cls, uint64_t pos, char *buf, size_t max);
+ static void ContentReaderFreeCallback(void *cls);
+
+ static MHD_RESULT AnswerToConnection (void *cls, struct MHD_Connection *connection,
+ const char *url, const char *method,
+ const char *version, const char *upload_data,
+ size_t *upload_data_size, void **con_cls);
+ static MHD_RESULT HandlePostField(void *cls, enum MHD_ValueKind kind, const char *key,
+ const char *filename, const char *content_type,
+ const char *transfer_encoding, const char *data, uint64_t off,
+ size_t size);
+
+ bool LoadCert(std::string &skey, std::string &scert);
+
+ static Logger GetLogger();
+
+ uint16_t m_port = 0;
+ struct MHD_Daemon *m_daemon_ip6 = nullptr;
+ struct MHD_Daemon *m_daemon_ip4 = nullptr;
+ bool m_running = false;
+ size_t m_thread_stacksize = 0;
+ bool m_authenticationRequired = false;
+ std::string m_authenticationUsername;
+ std::string m_authenticationPassword;
+ std::string m_key;
+ std::string m_cert;
+ mutable CCriticalSection m_critSection;
+ std::vector<IHTTPRequestHandler *> m_requestHandlers;
+
+ Logger m_logger;
+};
diff --git a/xbmc/network/Zeroconf.cpp b/xbmc/network/Zeroconf.cpp
new file mode 100644
index 0000000..311ecfb
--- /dev/null
+++ b/xbmc/network/Zeroconf.cpp
@@ -0,0 +1,187 @@
+/*
+ * 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 "Zeroconf.h"
+
+#include "ServiceBroker.h"
+
+#include <mutex>
+#if defined(HAS_MDNS)
+#include "mdns/ZeroconfMDNS.h"
+#endif
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/CriticalSection.h"
+#include "utils/JobManager.h"
+
+#if defined(TARGET_ANDROID)
+#include "platform/android/network/ZeroconfAndroid.h"
+#elif defined(TARGET_DARWIN)
+//on osx use the native implementation
+#include "platform/darwin/network/ZeroconfDarwin.h"
+#elif defined(HAS_AVAHI)
+#include "platform/linux/network/zeroconf/ZeroconfAvahi.h"
+#endif
+
+#include <cassert>
+#include <utility>
+
+namespace
+{
+
+std::mutex singletonMutex;
+
+}
+
+#ifndef HAS_ZEROCONF
+//dummy implementation used if no zeroconf is present
+//should be optimized away
+class CZeroconfDummy : public CZeroconf
+{
+ virtual bool doPublishService(const std::string&, const std::string&, const std::string&, unsigned int, const std::vector<std::pair<std::string, std::string> >&)
+ {
+ return false;
+ }
+
+ virtual bool doForceReAnnounceService(const std::string&){return false;}
+ virtual bool doRemoveService(const std::string& fcr_ident){return false;}
+ virtual void doStop(){}
+};
+#endif
+
+CZeroconf* CZeroconf::smp_instance = 0;
+
+CZeroconf::CZeroconf():mp_crit_sec(new CCriticalSection)
+{
+}
+
+CZeroconf::~CZeroconf()
+{
+ delete mp_crit_sec;
+}
+
+bool CZeroconf::PublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ std::vector<std::pair<std::string, std::string> > txt /* = std::vector<std::pair<std::string, std::string> >() */)
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ CZeroconf::PublishInfo info = {fcr_type, fcr_name, f_port, std::move(txt)};
+ std::pair<tServiceMap::const_iterator, bool> ret = m_service_map.insert(std::make_pair(fcr_identifier, info));
+ if(!ret.second) //identifier exists
+ return false;
+ if(m_started)
+ CServiceBroker::GetJobManager()->AddJob(new CPublish(fcr_identifier, info), nullptr);
+
+ //not yet started, so its just queued
+ return true;
+}
+
+bool CZeroconf::RemoveService(const std::string& fcr_identifier)
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ tServiceMap::iterator it = m_service_map.find(fcr_identifier);
+ if(it == m_service_map.end())
+ return false;
+ m_service_map.erase(it);
+ if(m_started)
+ return doRemoveService(fcr_identifier);
+ else
+ return true;
+}
+
+bool CZeroconf::ForceReAnnounceService(const std::string& fcr_identifier)
+{
+ if (HasService(fcr_identifier) && m_started)
+ {
+ return doForceReAnnounceService(fcr_identifier);
+ }
+ return false;
+}
+
+bool CZeroconf::HasService(const std::string& fcr_identifier) const
+{
+ return (m_service_map.find(fcr_identifier) != m_service_map.end());
+}
+
+bool CZeroconf::Start()
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(!IsZCdaemonRunning())
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetBool(CSettings::SETTING_SERVICES_ZEROCONF, false);
+ if (settings->GetBool(CSettings::SETTING_SERVICES_AIRPLAY))
+ settings->SetBool(CSettings::SETTING_SERVICES_AIRPLAY, false);
+ return false;
+ }
+ if(m_started)
+ return true;
+ m_started = true;
+
+ CServiceBroker::GetJobManager()->AddJob(new CPublish(m_service_map), nullptr);
+ return true;
+}
+
+void CZeroconf::Stop()
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(!m_started)
+ return;
+ doStop();
+ m_started = false;
+}
+
+CZeroconf* CZeroconf::GetInstance()
+{
+ std::lock_guard<std::mutex> lock(singletonMutex);
+ if(!smp_instance)
+ {
+#ifndef HAS_ZEROCONF
+ smp_instance = new CZeroconfDummy;
+#else
+#if defined(TARGET_DARWIN)
+ smp_instance = new CZeroconfDarwin;
+#elif defined(HAS_AVAHI)
+ smp_instance = new CZeroconfAvahi;
+#elif defined(TARGET_ANDROID)
+ smp_instance = new CZeroconfAndroid;
+#elif defined(HAS_MDNS)
+ smp_instance = new CZeroconfMDNS;
+#endif
+#endif
+ }
+ assert(smp_instance);
+ return smp_instance;
+}
+
+void CZeroconf::ReleaseInstance()
+{
+ std::lock_guard<std::mutex> lock(singletonMutex);
+ delete smp_instance;
+ smp_instance = 0;
+}
+
+CZeroconf::CPublish::CPublish(const std::string& fcr_identifier, const PublishInfo& pubinfo)
+{
+ m_servmap.insert(std::make_pair(fcr_identifier, pubinfo));
+}
+
+CZeroconf::CPublish::CPublish(const tServiceMap& servmap)
+ : m_servmap(servmap)
+{
+}
+
+bool CZeroconf::CPublish::DoWork()
+{
+ for (const auto& it : m_servmap)
+ CZeroconf::GetInstance()->doPublishService(it.first, it.second.type, it.second.name,
+ it.second.port, it.second.txt);
+
+ return true;
+}
diff --git a/xbmc/network/Zeroconf.h b/xbmc/network/Zeroconf.h
new file mode 100644
index 0000000..bed66c9
--- /dev/null
+++ b/xbmc/network/Zeroconf.h
@@ -0,0 +1,138 @@
+/*
+ * 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 "utils/Job.h"
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CCriticalSection;
+/// this class provides support for zeroconf
+/// while the different zeroconf implementations have asynchronous APIs
+/// this class hides it and provides only few ways to interact
+/// with the services. If more control is needed, feel
+/// free to add it. The main purpose currently is to provide an easy
+/// way to publish services in the different StartXXX/StopXXX methods
+/// in CApplication
+//! @todo Make me safe for use in static initialization. CritSec is a static member :/
+//! use e.g. loki's singleton implementation to make do it properly
+class CZeroconf
+{
+public:
+
+ //tries to publish this service via zeroconf
+ //fcr_identifier can be used to stop or reannounce this service later
+ //fcr_type is the zeroconf service type to publish (e.g. _http._tcp for webserver)
+ //fcr_name is the name of the service to publish. The hostname is currently automatically appended
+ // and used for name collisions. e.g. XBMC would get published as fcr_name@Martn or, after collision fcr_name@Martn-2
+ //f_port port of the service to publish
+ // returns false if fcr_identifier was already present
+ bool PublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ std::vector<std::pair<std::string, std::string> > txt /*= std::vector<std::pair<std::string, std::string> >()*/);
+
+ //tries to rebroadcast that service on the network without removing/readding
+ //this can be achieved by changing a fake txt record. Implementations should
+ //implement it by doing so.
+ //
+ //fcr_identifier - the identifier of the already published service which should be reannounced
+ // returns true on successful reannonuce - false if this service isn't published yet
+ bool ForceReAnnounceService(const std::string& fcr_identifier);
+
+ ///removes the specified service
+ ///returns false if fcr_identifier does not exist
+ bool RemoveService(const std::string& fcr_identifier);
+
+ ///returns true if fcr_identifier exists
+ bool HasService(const std::string& fcr_identifier) const;
+
+ //starts publishing
+ //services that were added with PublishService(...) while Zeroconf wasn't
+ //started, get published now.
+ bool Start();
+
+ // unpublishes all services (but keeps them stored in this class)
+ // a call to Start() will republish them
+ void Stop();
+
+ // class methods
+ // access to singleton; singleton gets created on call if not existent
+ // if zeroconf is disabled (!HAS_ZEROCONF), this will return a dummy implementation that
+ // just does nothings, otherwise the platform specific one
+ static CZeroconf* GetInstance();
+ // release the singleton; (save to call multiple times)
+ static void ReleaseInstance();
+ // returns false if ReleaseInstance() was called before
+ static bool IsInstantiated() { return smp_instance != 0; }
+ // win32: process results from the bonjour daemon
+ virtual void ProcessResults() {}
+ // returns if the service is started and services are announced
+ bool IsStarted() { return m_started; }
+
+protected:
+ //methods to implement for concrete implementations
+ //publishs this service
+ virtual bool doPublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ const std::vector<std::pair<std::string, std::string> >& txt) = 0;
+
+ //methods to implement for concrete implementations
+ //update this service
+ virtual bool doForceReAnnounceService(const std::string& fcr_identifier) = 0;
+
+ //removes the service if published
+ virtual bool doRemoveService(const std::string& fcr_ident) = 0;
+
+ //removes all services (short hand for "for i in m_service_map doRemoveService(i)")
+ virtual void doStop() = 0;
+
+ // return true if the zeroconf daemon is running
+ virtual bool IsZCdaemonRunning() { return true; }
+
+protected:
+ //singleton: we don't want to get instantiated nor copied or deleted from outside
+ CZeroconf();
+ CZeroconf(const CZeroconf&);
+ virtual ~CZeroconf();
+
+private:
+ struct PublishInfo{
+ std::string type;
+ std::string name;
+ unsigned int port;
+ std::vector<std::pair<std::string, std::string> > txt;
+ };
+
+ //protects data
+ CCriticalSection* mp_crit_sec;
+ typedef std::map<std::string, PublishInfo> tServiceMap;
+ tServiceMap m_service_map;
+ bool m_started = false;
+
+ static CZeroconf* smp_instance;
+
+ class CPublish : public CJob
+ {
+ public:
+ CPublish(const std::string& fcr_identifier, const PublishInfo& pubinfo);
+ explicit CPublish(const tServiceMap& servmap);
+
+ bool DoWork() override;
+
+ private:
+ tServiceMap m_servmap;
+ };
+};
diff --git a/xbmc/network/ZeroconfBrowser.cpp b/xbmc/network/ZeroconfBrowser.cpp
new file mode 100644
index 0000000..2d1b8c6
--- /dev/null
+++ b/xbmc/network/ZeroconfBrowser.cpp
@@ -0,0 +1,245 @@
+/*
+ * 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 "ZeroconfBrowser.h"
+
+#include "utils/log.h"
+
+#include <cassert>
+#include <mutex>
+#include <stdexcept>
+
+#if defined (HAS_AVAHI)
+#include "platform/linux/network/zeroconf/ZeroconfBrowserAvahi.h"
+#elif defined(TARGET_DARWIN)
+//on osx use the native implementation
+#include "platform/darwin/network/ZeroconfBrowserDarwin.h"
+#elif defined(TARGET_ANDROID)
+#include "platform/android/network/ZeroconfBrowserAndroid.h"
+#elif defined(HAS_MDNS)
+#include "mdns/ZeroconfBrowserMDNS.h"
+#endif
+
+#include "threads/CriticalSection.h"
+
+namespace
+{
+
+std::mutex singletonMutex;
+
+}
+
+#if !defined(HAS_ZEROCONF)
+//dummy implementation used if no zeroconf is present
+//should be optimized away
+class CZeroconfBrowserDummy : public CZeroconfBrowser
+{
+ virtual bool doAddServiceType(const std::string&){return false;}
+ virtual bool doRemoveServiceType(const std::string&){return false;}
+ virtual std::vector<ZeroconfService> doGetFoundServices(){return std::vector<ZeroconfService>();}
+ virtual bool doResolveService(ZeroconfService&, double){return false;}
+};
+#endif
+
+CZeroconfBrowser* CZeroconfBrowser::smp_instance = 0;
+
+CZeroconfBrowser::CZeroconfBrowser():mp_crit_sec(new CCriticalSection)
+{
+#ifdef HAS_FILESYSTEM_SMB
+ AddServiceType("_smb._tcp.");
+#endif
+ AddServiceType("_ftp._tcp.");
+ AddServiceType("_webdav._tcp.");
+#ifdef HAS_FILESYSTEM_NFS
+ AddServiceType("_nfs._tcp.");
+#endif// HAS_FILESYSTEM_NFS
+}
+
+CZeroconfBrowser::~CZeroconfBrowser()
+{
+ delete mp_crit_sec;
+}
+
+void CZeroconfBrowser::Start()
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(m_started)
+ return;
+ m_started = true;
+ for (const auto& it : m_services)
+ doAddServiceType(it);
+}
+
+void CZeroconfBrowser::Stop()
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(!m_started)
+ return;
+ for (const auto& it : m_services)
+ RemoveServiceType(it);
+ m_started = false;
+}
+
+bool CZeroconfBrowser::AddServiceType(const std::string& fcr_service_type /*const std::string& domain*/ )
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ std::pair<tServices::iterator, bool> ret = m_services.insert(fcr_service_type);
+ if(!ret.second)
+ {
+ //service already in list
+ return false;
+ }
+ if(m_started)
+ return doAddServiceType(*ret.first);
+ //not yet started, so its just queued
+ return true;
+}
+
+bool CZeroconfBrowser::RemoveServiceType(const std::string& fcr_service_type)
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ tServices::iterator ret = m_services.find(fcr_service_type);
+ if(ret == m_services.end())
+ return false;
+ if(m_started)
+ return doRemoveServiceType(fcr_service_type);
+ //not yet started, so its just queued
+ return true;
+}
+
+std::vector<CZeroconfBrowser::ZeroconfService> CZeroconfBrowser::GetFoundServices()
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(m_started)
+ return doGetFoundServices();
+ else
+ {
+ CLog::Log(LOGDEBUG, "CZeroconfBrowser::GetFoundServices asked for services without browser running");
+ return std::vector<ZeroconfService>();
+ }
+}
+
+bool CZeroconfBrowser::ResolveService(ZeroconfService& fr_service, double f_timeout)
+{
+ std::unique_lock<CCriticalSection> lock(*mp_crit_sec);
+ if(m_started)
+ {
+ return doResolveService(fr_service, f_timeout);
+ }
+ CLog::Log(LOGDEBUG, "CZeroconfBrowser::GetFoundServices asked for services without browser running");
+ return false;
+}
+
+CZeroconfBrowser* CZeroconfBrowser::GetInstance()
+{
+ std::lock_guard<std::mutex> lock(singletonMutex);
+
+ if(!smp_instance)
+ {
+#if !defined(HAS_ZEROCONF)
+ smp_instance = new CZeroconfBrowserDummy;
+#else
+#if defined(TARGET_DARWIN)
+ smp_instance = new CZeroconfBrowserDarwin;
+#elif defined(HAS_AVAHI)
+ smp_instance = new CZeroconfBrowserAvahi;
+#elif defined(TARGET_ANDROID)
+ // WIP
+ smp_instance = new CZeroconfBrowserAndroid;
+#elif defined(HAS_MDNS)
+ smp_instance = new CZeroconfBrowserMDNS;
+#endif
+#endif
+ }
+ assert(smp_instance);
+ return smp_instance;
+}
+
+void CZeroconfBrowser::ReleaseInstance()
+{
+ std::lock_guard<std::mutex> lock(singletonMutex);
+
+ delete smp_instance;
+ smp_instance = 0;
+}
+
+
+CZeroconfBrowser::ZeroconfService::ZeroconfService(const std::string& fcr_name, const std::string& fcr_type, const std::string& fcr_domain):
+ m_name(fcr_name),
+ m_domain(fcr_domain)
+{
+ SetType(fcr_type);
+}
+void CZeroconfBrowser::ZeroconfService::SetName(const std::string& fcr_name)
+{
+ m_name = fcr_name;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetType(const std::string& fcr_type)
+{
+ if(fcr_type.empty())
+ throw std::runtime_error("CZeroconfBrowser::ZeroconfService::SetType invalid type: "+ fcr_type);
+ //make sure there's a "." as last char (differs for avahi and osx implementation of browsers)
+ if(fcr_type[fcr_type.length() - 1] != '.')
+ m_type = fcr_type + ".";
+ else
+ m_type = fcr_type;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetDomain(const std::string& fcr_domain)
+{
+ m_domain = fcr_domain;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetHostname(const std::string& fcr_hostname)
+{
+ m_hostname = fcr_hostname;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetIP(const std::string& fcr_ip)
+{
+ m_ip = fcr_ip;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetPort(int f_port)
+{
+ m_port = f_port;
+}
+
+void CZeroconfBrowser::ZeroconfService::SetTxtRecords(const tTxtRecordMap& txt_records)
+{
+ m_txtrecords_map = txt_records;
+
+ CLog::Log(LOGDEBUG,"CZeroconfBrowser: dump txt-records");
+ for (const auto& it : m_txtrecords_map)
+ {
+ CLog::Log(LOGDEBUG, "CZeroconfBrowser: key: {} value: {}", it.first, it.second);
+ }
+}
+
+std::string CZeroconfBrowser::ZeroconfService::toPath(const ZeroconfService& fcr_service)
+{
+ return fcr_service.m_type + '@' + fcr_service.m_domain + '@' + fcr_service.m_name;
+}
+
+CZeroconfBrowser::ZeroconfService CZeroconfBrowser::ZeroconfService::fromPath(const std::string& fcr_path)
+{
+ if( fcr_path.empty() )
+ throw std::runtime_error("CZeroconfBrowser::ZeroconfService::fromPath input string empty!");
+
+ size_t pos1 = fcr_path.find('@'); //first @
+ size_t pos2 = fcr_path.find('@', pos1 + 1); //second
+
+ if(pos1 == std::string::npos || pos2 == std::string::npos)
+ throw std::runtime_error("CZeroconfBrowser::ZeroconfService::fromPath invalid input path");
+
+ return ZeroconfService(
+ fcr_path.substr(pos2 + 1, fcr_path.length()), //name
+ fcr_path.substr(0, pos1), //type
+ fcr_path.substr(pos1 + 1, pos2-(pos1+1)) //domain
+ );
+}
diff --git a/xbmc/network/ZeroconfBrowser.h b/xbmc/network/ZeroconfBrowser.h
new file mode 100644
index 0000000..76a4439
--- /dev/null
+++ b/xbmc/network/ZeroconfBrowser.h
@@ -0,0 +1,174 @@
+/*
+ * 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 <string>
+#include <set>
+#include <vector>
+#include <map>
+
+#ifdef TARGET_WINDOWS
+#undef SetPort // WIN32INCLUDES this is defined as SetPortA in WinSpool.h which is being included _somewhere_
+#endif
+
+//forwards
+class CCriticalSection;
+
+/// this class provides support for zeroconf browsing
+class CZeroconfBrowser
+{
+public:
+ class ZeroconfService
+ {
+ public:
+ typedef std::map<std::string, std::string> tTxtRecordMap;
+
+ ZeroconfService() = default;
+ ZeroconfService(const std::string& fcr_name, const std::string& fcr_type, const std::string& fcr_domain);
+
+ /// easy conversion to string and back (used in czeronfdiretory to store this service)
+ ///@{
+ static std::string toPath(const ZeroconfService& fcr_service);
+ static ZeroconfService fromPath(const std::string& fcr_path); //throws std::runtime_error on failure
+ ///@}
+
+ /// general access methods
+ ///@{
+ void SetName(const std::string& fcr_name);
+ const std::string& GetName() const {return m_name;}
+
+ void SetType(const std::string& fcr_type);
+ const std::string& GetType() const {return m_type;}
+
+ void SetDomain(const std::string& fcr_domain);
+ const std::string& GetDomain() const {return m_domain;}
+ ///@}
+
+ /// access methods needed during resolve
+ ///@{
+ void SetIP(const std::string& fcr_ip);
+ const std::string& GetIP() const {return m_ip;}
+
+ void SetHostname(const std::string& fcr_hostname);
+ const std::string& GetHostname() const {return m_hostname;}
+
+ void SetPort(int f_port);
+ int GetPort() const {return m_port;}
+
+ void SetTxtRecords(const tTxtRecordMap& txt_records);
+ const tTxtRecordMap& GetTxtRecords() const { return m_txtrecords_map;}
+ ///@}
+ private:
+ //3 entries below identify a service
+ std::string m_name;
+ std::string m_type;
+ std::string m_domain;
+
+ //2 entries below store 1 ip:port pair for this service
+ std::string m_ip;
+ int m_port = 0;
+
+ //used for mdns in case dns resolution fails
+ //we store the hostname and resolve with mdns functions again
+ std::string m_hostname;
+
+ //1 entry below stores the txt-record as a key value map for this service
+ tTxtRecordMap m_txtrecords_map;
+ };
+
+ // starts browsing
+ void Start();
+
+ // stops browsing
+ void Stop();
+
+ ///returns the list of found services
+ /// if this is updated, the following message with "zeroconf://" as path is sent:
+ /// CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ std::vector<ZeroconfService> GetFoundServices();
+ ///@}
+
+ // resolves a ZeroconfService to ip + port
+ // @param fcr_service the service to resolve
+ // @param f_timeout timeout in seconds for resolving
+ // the protocol part of CURL is the raw zeroconf service type
+ // added with AddServiceType (== needs further processing! e.g. _smb._tcp -> smb)
+ // @return true if it was successfully resolved (or scheduled), false if resolve
+ // failed (async or not)
+ bool ResolveService(ZeroconfService& fr_service, double f_timeout = 1.0);
+
+ // class methods
+ // access to singleton; singleton gets created on call if not existent
+ // if zeroconf is disabled (!HAS_ZEROCONF), this will return a dummy implementation that
+ // just does nothings, otherwise the platform specific one
+ static CZeroconfBrowser* GetInstance();
+ // release the singleton; (save to call multiple times)
+ static void ReleaseInstance();
+ // returns false if ReleaseInstance() was called before
+ static bool IsInstantiated() { return smp_instance != 0; }
+
+ virtual void ProcessResults() {}
+
+ /// methods for browsing and getting results of it
+ ///@{
+ /// adds a service type for browsing
+ /// @param fcr_service_type the service type as string, e.g. _smb._tcp.
+ /// @return false if it was already there
+ bool AddServiceType(const std::string& fcr_service_type);
+
+ /// remove the specified service from discovery
+ /// @param fcr_service_type the service type as string, e.g. _smb._tcp.
+ /// @return if it was not found
+ bool RemoveServiceType(const std::string& fcr_service_type);
+
+protected:
+ //singleton: we don't want to get instantiated nor copied or deleted from outside
+ CZeroconfBrowser();
+ CZeroconfBrowser(const CZeroconfBrowser&) = delete;
+ CZeroconfBrowser& operator=(const CZeroconfBrowser&) = delete;
+ virtual ~CZeroconfBrowser();
+
+ // pure virtual methods to implement for OS specific implementations
+ virtual bool doAddServiceType(const std::string& fcr_service_type) = 0;
+ virtual bool doRemoveServiceType(const std::string& fcr_service_type) = 0;
+ virtual std::vector<ZeroconfService> doGetFoundServices() = 0;
+ virtual bool doResolveService(ZeroconfService& fr_service, double f_timeout) = 0;
+
+private:
+ struct ServiceInfo
+ {
+ std::string type;
+ };
+
+ //protects data
+ CCriticalSection* mp_crit_sec;
+ typedef std::set<std::string> tServices;
+ tServices m_services;
+ bool m_started = false;
+
+ static CZeroconfBrowser* smp_instance;
+};
+#include <iostream>
+
+//debugging helper
+inline std::ostream& operator<<(std::ostream& o, const CZeroconfBrowser::ZeroconfService& service){
+ o << "(" << service.GetName() << "|" << service.GetType() << "|" << service.GetDomain() << ")";
+ return o;
+}
+
+//inline methods
+inline bool operator<(CZeroconfBrowser::ZeroconfService const& fcr_lhs, CZeroconfBrowser::ZeroconfService const& fcr_rhs)
+{
+ return (fcr_lhs.GetName() + fcr_lhs.GetType() + fcr_lhs.GetDomain() < fcr_rhs.GetName() + fcr_rhs.GetType() + fcr_rhs.GetDomain());
+}
+
+inline bool operator==(CZeroconfBrowser::ZeroconfService const& fcr_lhs, CZeroconfBrowser::ZeroconfService const& fcr_rhs)
+{
+ return (fcr_lhs.GetName() == fcr_rhs.GetName() && fcr_lhs.GetType() == fcr_rhs.GetType() && fcr_lhs.GetDomain() == fcr_rhs.GetDomain() );
+}
diff --git a/xbmc/network/cddb.cpp b/xbmc/network/cddb.cpp
new file mode 100644
index 0000000..acbfc9f
--- /dev/null
+++ b/xbmc/network/cddb.cpp
@@ -0,0 +1,1083 @@
+/*
+ * 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 "cddb.h"
+
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "network/DNSNameCache.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+
+#include <netdb.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <taglib/id3v1genres.h>
+
+using namespace MEDIA_DETECT;
+using namespace CDDB;
+
+//-------------------------------------------------------------------------------------------------------------------
+Xcddb::Xcddb()
+#if defined(TARGET_WINDOWS)
+ : m_cddb_socket(closesocket, INVALID_SOCKET)
+#else
+ : m_cddb_socket(close, -1)
+#endif
+ , m_cddb_ip_address(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cddbAddress)
+{
+ m_lastError = 0;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+Xcddb::~Xcddb()
+{
+ closeSocket();
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::openSocket()
+{
+ char namebuf[NI_MAXHOST], portbuf[NI_MAXSERV];
+ struct addrinfo hints;
+ struct addrinfo *result, *addr;
+ char service[33];
+ int res;
+ SOCKET fd = INVALID_SOCKET;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = IPPROTO_TCP;
+ sprintf(service, "%d", CDDB_PORT);
+
+ res = getaddrinfo(m_cddb_ip_address.c_str(), service, &hints, &result);
+ if(res)
+ {
+ std::string err;
+#if defined(TARGET_WINDOWS)
+ g_charsetConverter.wToUTF8(gai_strerror(res), err);
+#else
+ err = gai_strerror(res);
+#endif
+ CLog::Log(LOGERROR, "Xcddb::openSocket - failed to lookup {} with error {}", m_cddb_ip_address,
+ err);
+ res = getaddrinfo("130.179.31.49", service, &hints, &result);
+ if(res)
+ return false;
+ }
+
+ for(addr = result; addr; addr = addr->ai_next)
+ {
+ if(getnameinfo(addr->ai_addr, addr->ai_addrlen, namebuf, sizeof(namebuf), portbuf, sizeof(portbuf),NI_NUMERICHOST))
+ {
+ strcpy(namebuf, "[unknown]");
+ strcpy(portbuf, "[unknown]");
+ }
+ CLog::Log(LOGDEBUG, "Xcddb::openSocket - connecting to: {}:{} ...", namebuf, portbuf);
+
+ fd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
+ if (fd == INVALID_SOCKET)
+ continue;
+
+ if (connect(fd, addr->ai_addr, addr->ai_addrlen) != SOCKET_ERROR)
+ break;
+
+ closesocket(fd);
+ fd = INVALID_SOCKET;
+ }
+
+ freeaddrinfo(result);
+ if(fd == INVALID_SOCKET)
+ {
+ CLog::Log(LOGERROR, "Xcddb::openSocket - failed to connect to cddb");
+ return false;
+ }
+
+ m_cddb_socket.attach(fd);
+ return true;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::closeSocket()
+{
+ if (m_cddb_socket)
+ {
+ m_cddb_socket.reset();
+ }
+ return true;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::Send( const void *buffer, int bytes )
+{
+ std::unique_ptr<char[]> tmp_buffer(new char[bytes + 10]);
+ strcpy(tmp_buffer.get(), (const char*)buffer);
+ tmp_buffer.get()[bytes] = '.';
+ tmp_buffer.get()[bytes + 1] = 0x0d;
+ tmp_buffer.get()[bytes + 2] = 0x0a;
+ tmp_buffer.get()[bytes + 3] = 0x00;
+ int iErr = send((SOCKET)m_cddb_socket, (const char*)tmp_buffer.get(), bytes + 3, 0);
+ if (iErr <= 0)
+ {
+ return false;
+ }
+ return true;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::Send( const char *buffer)
+{
+ int iErr = Send(buffer, strlen(buffer));
+ if (iErr <= 0)
+ {
+ return false;
+ }
+ return true;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+std::string Xcddb::Recv(bool wait4point)
+{
+ char tmpbuffer[1];
+ char prevChar;
+ int counter = 0;
+ std::string str_buffer;
+
+
+ //##########################################################
+ // Read the buffer. Character by character
+ tmpbuffer[0]=0;
+ do
+ {
+ int lenRead;
+
+ prevChar=tmpbuffer[0];
+ lenRead = recv((SOCKET)m_cddb_socket, (char*) & tmpbuffer, 1, 0);
+
+ //Check if there was any error reading the buffer
+ if(lenRead == 0 || lenRead == SOCKET_ERROR || WSAGetLastError() == WSAECONNRESET)
+ {
+ CLog::Log(LOGERROR,
+ "Xcddb::Recv Error reading buffer. lenRead = [{}] and WSAGetLastError = [{}]",
+ lenRead, WSAGetLastError());
+ break;
+ }
+
+ //Write received data to the return string
+ str_buffer.push_back(tmpbuffer[0]);
+ counter++;
+ }while(wait4point ? prevChar != '\n' || tmpbuffer[0] != '.' : tmpbuffer[0] != '\n');
+
+
+ //##########################################################
+ // Write captured data information to the xbmc log file
+ CLog::Log(LOGDEBUG,
+ "Xcddb::Recv Captured {0} bytes // Buffer= {1} bytes. Captured data follows on next "
+ "line\n{2}",
+ counter, str_buffer.size(), str_buffer);
+
+
+ return str_buffer;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::queryCDinfo(CCdInfo* pInfo, int inexact_list_select)
+{
+ if ( pInfo == NULL )
+ {
+ m_lastError = E_PARAMETER_WRONG;
+ return false;
+ }
+
+ uint32_t discid = pInfo->GetCddbDiscId();
+
+
+ //##########################################################
+ // Compose the cddb query string
+ std::string read_buffer = getInexactCommand(inexact_list_select);
+ if (read_buffer.empty())
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Size of inexact_list_select are 0");
+ m_lastError = E_PARAMETER_WRONG;
+ return false;
+ }
+
+
+ //##########################################################
+ // Read the data from cddb
+ Recv(false); // Clear pending data on our connection
+ if (!Send(read_buffer.c_str()))
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Error sending \"{}\"", read_buffer);
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select pInfo == NULL");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ std::string recv_buffer = Recv(true);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 210: //OK, CDDB database entry follows (until terminating marker)
+ // Cool, I got it ;-)
+ writeCacheFile( recv_buffer.c_str(), discid );
+ parseData(recv_buffer.c_str());
+ break;
+
+ case 401: //Specified CDDB entry not found.
+ case 402: //Server error.
+ case 403: //Database entry is corrupt.
+ case 409: //No handshake.
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Quit
+ if ( ! Send("quit") )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Error sending \"{}\"", "quit");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 0: //By some reason, also 0 is a valid value. This is not documented, and might depend on that no string was found and atoi then returns 0
+ case 230: //Closing connection. Goodbye.
+ break;
+
+ case 530: //error, closing connection.
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Close connection
+ if ( !closeSocket() )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo_inexact_list_select Error closing socket");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ return true;
+}
+
+
+//-------------------------------------------------------------------------------------------------------------------
+int Xcddb::getLastError() const
+{
+ return m_lastError;
+}
+
+
+//-------------------------------------------------------------------------------------------------------------------
+const char *Xcddb::getLastErrorText() const
+{
+ switch (getLastError())
+ {
+ case E_TOC_INCORRECT:
+ return "TOC Incorrect";
+ break;
+ case E_NETWORK_ERROR_OPEN_SOCKET:
+ return "Error open Socket";
+ break;
+ case E_NETWORK_ERROR_SEND:
+ return "Error send PDU";
+ break;
+ case E_WAIT_FOR_INPUT:
+ return "Wait for Input";
+ break;
+ case E_PARAMETER_WRONG:
+ return "Error Parameter Wrong";
+ break;
+ case 202: return "No match found";
+ case 210: return "Found exact matches, list follows (until terminating marker)";
+ case 211: return "Found inexact matches, list follows (until terminating marker)";
+ case 401: return "Specified CDDB entry not found";
+ case 402: return "Server error";
+ case 403: return "Database entry is corrupt";
+ case 408: return "CGI environment error";
+ case 409: return "No handshake";
+ case 431: return "Handshake not successful, closing connection";
+ case 432: return "No connections allowed: permission denied";
+ case 433: return "No connections allowed: X users allowed, Y currently active";
+ case 434: return "No connections allowed: system load too high";
+ case 500: return "Command syntax error, command unknown, command unimplemented";
+ case 501: return "Illegal protocol level";
+ case 530: return "error, closing connection, Server error, server timeout";
+ default: return "Unknown Error";
+ }
+}
+
+
+//-------------------------------------------------------------------------------------------------------------------
+int Xcddb::cddb_sum(int n)
+{
+ int ret;
+
+ /* For backward compatibility this algorithm must not change */
+
+ ret = 0;
+
+ while (n > 0)
+ {
+ ret = ret + (n % 10);
+ n = n / 10;
+ }
+
+ return (ret);
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+uint32_t Xcddb::calc_disc_id(int tot_trks, toc cdtoc[])
+{
+ int i = 0, t = 0, n = 0;
+
+ while (i < tot_trks)
+ {
+
+ n = n + cddb_sum((cdtoc[i].min * 60) + cdtoc[i].sec);
+ i++;
+ }
+
+ t = ((cdtoc[tot_trks].min * 60) + cdtoc[tot_trks].sec) - ((cdtoc[0].min * 60) + cdtoc[0].sec);
+
+ return ((n % 0xff) << 24 | t << 8 | tot_trks);
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::addTitle(const char *buffer)
+{
+ char value[2048];
+ int trk_nr = 0;
+ //TTITLEN
+ if (buffer[7] == '=')
+ { //Einstellig
+ trk_nr = buffer[6] - 47;
+ strncpy(value, buffer + 8, sizeof(value) - 1);
+ }
+ else if (buffer[8] == '=')
+ { //Zweistellig
+ trk_nr = ((buffer[6] - 48) * 10) + buffer[7] - 47;
+ strncpy(value, buffer + 9, sizeof(value) - 1);
+ }
+ else if (buffer[9] == '=')
+ { //Dreistellig
+ trk_nr = ((buffer[6] - 48) * 100) + ((buffer[7] - 48) * 10) + buffer[8] - 47;
+ strncpy(value, buffer + 10, sizeof(value) - 1);
+ }
+ else
+ {
+ return ;
+ }
+ value[sizeof(value) - 1] = '\0';
+
+ // track artist" / "track title
+ std::vector<std::string> values = StringUtils::Split(value, " / ");
+ if (values.size() > 1)
+ {
+ g_charsetConverter.unknownToUTF8(values[0]);
+ m_mapArtists[trk_nr] += values[0];
+ g_charsetConverter.unknownToUTF8(values[1]);
+ m_mapTitles[trk_nr] += values[1];
+ }
+ else if (!values.empty())
+ {
+ g_charsetConverter.unknownToUTF8(values[0]);
+ m_mapTitles[trk_nr] += values[0];
+ }
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getInexactCommand(int select) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapInexact_cddb_command_list.find(select);
+ if (i == m_mapInexact_cddb_command_list.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getInexactArtist(int select) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapInexact_artist_list.find(select);
+ if (i == m_mapInexact_artist_list.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getInexactTitle(int select) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapInexact_title_list.find(select);
+ if (i == m_mapInexact_title_list.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getTrackArtist(int track) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapArtists.find(track);
+ if (i == m_mapArtists.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getTrackTitle(int track) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapTitles.find(track);
+ if (i == m_mapTitles.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::getDiskTitle(std::string& strdisk_title) const
+{
+ strdisk_title = m_strDisk_title;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::getDiskArtist(std::string& strdisk_artist) const
+{
+ strdisk_artist = m_strDisk_artist;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::parseData(const char *buffer)
+{
+ //writeLog("parseData Start");
+
+ std::map<std::string, std::string> keywords;
+ std::list<std::string> keywordsOrder; // remember order of keywords as it appears in data received from CDDB
+
+ // Collect all the keywords and put them in map.
+ // Multiple occurrences of the same keyword indicate that
+ // the data contained on those lines should be concatenated
+ char *line;
+ const char trenner[3] = {'\n', '\r', '\0'};
+ strtok(const_cast<char*>(buffer), trenner); // skip first line
+ while ((line = strtok(0, trenner)))
+ {
+ // Lines that begin with # are comments, should be ignored
+ if (line[0] != '#')
+ {
+ char *s = strstr(line, "=");
+ if (s != NULL)
+ {
+ std::string strKeyword(line, s - line);
+ StringUtils::TrimRight(strKeyword);
+
+ std::string strValue(s+1);
+ StringUtils::Replace(strValue, "\\n", "\n");
+ StringUtils::Replace(strValue, "\\t", "\t");
+ StringUtils::Replace(strValue, "\\\\", "\\");
+
+ std::map<std::string, std::string>::const_iterator it = keywords.find(strKeyword);
+ if (it != keywords.end())
+ strValue = it->second + strValue; // keyword occurred before, concatenate
+ else
+ keywordsOrder.push_back(strKeyword);
+
+ keywords[strKeyword] = strValue;
+ }
+ }
+ }
+
+ // parse keywords
+ for (const std::string& strKeyword : keywordsOrder)
+ {
+ std::string strValue = keywords[strKeyword];
+
+ //! @todo STRING_CLEANUP
+ if (strKeyword == "DTITLE")
+ {
+ // DTITLE may contain artist and disc title, separated with " / ",
+ // for example: DTITLE=Modern Talking / Album: Victory (The 11th Album)
+ bool found = false;
+ for (int i = 0; i < (int)strValue.size() - 2; i++)
+ {
+ if (strValue[i] == ' ' && strValue[i + 1] == '/' && strValue[i + 2] == ' ')
+ {
+ m_strDisk_artist = TrimToUTF8(strValue.substr(0, i));
+ m_strDisk_title = TrimToUTF8(strValue.substr(i+3));
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ m_strDisk_title = TrimToUTF8(strValue);
+ }
+ else if (strKeyword == "DYEAR")
+ m_strYear = TrimToUTF8(strValue);
+ else if (strKeyword== "DGENRE")
+ m_strGenre = TrimToUTF8(strValue);
+ else if (StringUtils::StartsWith(strKeyword, "TTITLE"))
+ addTitle((strKeyword + "=" + strValue).c_str());
+ else if (strKeyword == "EXTD")
+ {
+ const std::string& strExtd(strValue);
+
+ if (m_strYear.empty())
+ {
+ // Extract Year from extended info
+ // as a fallback
+ size_t iPos = strExtd.find("YEAR: ");
+ if (iPos != std::string::npos) // You never know if you really get UTF-8 strings from cddb
+ g_charsetConverter.unknownToUTF8(strExtd.substr(iPos + 6, 4), m_strYear);
+ }
+
+ if (m_strGenre.empty())
+ {
+ // Extract ID3 Genre
+ // as a fallback
+ size_t iPos = strExtd.find("ID3G: ");
+ if (iPos != std::string::npos)
+ {
+ std::string strGenre = strExtd.substr(iPos + 5, 4);
+ StringUtils::TrimLeft(strGenre);
+ if (StringUtils::IsNaturalNumber(strGenre))
+ {
+ int iGenre = strtol(strGenre.c_str(), NULL, 10);
+ m_strGenre = TagLib::ID3v1::genre(iGenre).to8Bit(true);
+ }
+ }
+ }
+ }
+ else if (StringUtils::StartsWith(strKeyword, "EXTT"))
+ addExtended((strKeyword + "=" + strValue).c_str());
+ }
+
+ //writeLog("parseData Ende");
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::addExtended(const char *buffer)
+{
+ char value[2048];
+ int trk_nr = 0;
+ //TTITLEN
+ if (buffer[5] == '=')
+ { //Einstellig
+ trk_nr = buffer[4] - 47;
+ strncpy(value, buffer + 6, sizeof(value) - 1);
+ }
+ else if (buffer[6] == '=')
+ { //Zweistellig
+ trk_nr = ((buffer[4] - 48) * 10) + buffer[5] - 47;
+ strncpy(value, buffer + 7, sizeof(value) - 1);
+ }
+ else if (buffer[7] == '=')
+ { //Dreistellig
+ trk_nr = ((buffer[4] - 48) * 100) + ((buffer[5] - 48) * 10) + buffer[6] - 47;
+ strncpy(value, buffer + 8, sizeof(value) - 1);
+ }
+ else
+ {
+ return ;
+ }
+ value[sizeof(value) - 1] = '\0';
+
+ std::string strValue;
+ std::string strValueUtf8=value;
+ // You never know if you really get UTF-8 strings from cddb
+ g_charsetConverter.unknownToUTF8(strValueUtf8, strValue);
+ m_mapExtended_track[trk_nr] = strValue;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getTrackExtended(int track) const
+{
+ typedef std::map<int, std::string>::const_iterator iter;
+ iter i = m_mapExtended_track.find(track);
+ if (i == m_mapExtended_track.end())
+ return m_strNull;
+ return i->second;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::addInexactList(const char *list)
+{
+ /*
+ 211 Found inexact matches, list follows (until terminating `.')
+ soundtrack bf0cf90f Modern Talking / Victory - The 11th Album
+ rock c90cf90f Modern Talking / Album: Victory (The 11th Album)
+ misc de0d020f Modern Talking / Ready for the victory
+ rock e00d080f Modern Talking / Album: Victory (The 11th Album)
+ rock c10d150f Modern Talking / Victory (The 11th Album)
+ .
+ */
+
+ /*
+ m_mapInexact_cddb_command_list;
+ m_mapInexact_artist_list;
+ m_mapInexact_title_list;
+ */
+ int start = 0;
+ int end = 0;
+ bool found = false;
+ int line_counter = 0;
+ // //writeLog("addInexactList Start");
+ for (unsigned int i = 0;i < strlen(list);i++)
+ {
+ if (list[i] == '\n')
+ {
+ end = i;
+ found = true;
+ }
+ if (found)
+ {
+ if (line_counter > 0)
+ {
+ addInexactListLine(line_counter, list + start, end - start - 1);
+ }
+ start = i + 1;
+ line_counter++;
+ found = false;
+ }
+ }
+ // //writeLog("addInexactList End");
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::addInexactListLine(int line_cnt, const char *line, int len)
+{
+ // rock c90cf90f Modern Talking / Album: Victory (The 11th Album)
+ int search4 = 0;
+ char genre[100]; // 0
+ char discid[10]; // 1
+ char artist[1024]; // 2
+ char title[1024];
+ char cddb_command[1024];
+ int start = 0;
+ // //writeLog("addInexactListLine Start");
+ for (int i = 0;i < len;i++)
+ {
+ switch (search4)
+ {
+ case 0:
+ if (line[i] == ' ')
+ {
+ strncpy(genre, line, i);
+ genre[i] = 0x00;
+ search4 = 1;
+ start = i + 1;
+ }
+ break;
+ case 1:
+ if (line[i] == ' ')
+ {
+ strncpy(discid, line + start, i - start);
+ discid[i - start] = 0x00;
+ search4 = 2;
+ start = i + 1;
+ }
+ break;
+ case 2:
+ if (i + 2 <= len && line[i] == ' ' && line[i + 1] == '/' && line[i + 2] == ' ')
+ {
+ strncpy(artist, line + start, i - start);
+ artist[i - start] = 0x00;
+ strncpy(title, line + (i + 3), len - (i + 3));
+ title[len - (i + 3)] = 0x00;
+ }
+ break;
+ }
+ }
+ sprintf(cddb_command, "cddb read %s %s", genre, discid);
+
+ m_mapInexact_cddb_command_list[line_cnt] = cddb_command;
+
+ std::string strArtist=artist;
+ // You never know if you really get UTF-8 strings from cddb
+ g_charsetConverter.unknownToUTF8(artist, strArtist);
+ m_mapInexact_artist_list[line_cnt] = strArtist;
+
+ std::string strTitle=title;
+ // You never know if you really get UTF-8 strings from cddb
+ g_charsetConverter.unknownToUTF8(title, strTitle);
+ m_mapInexact_title_list[line_cnt] = strTitle;
+ // char log_string[1024];
+ // sprintf(log_string,"%u: %s - %s",line_cnt,artist,title);
+ // //writeLog(log_string);
+ // //writeLog("addInexactListLine End");
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::setCDDBIpAddress(const std::string& ip_address)
+{
+ m_cddb_ip_address = ip_address;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+void Xcddb::setCacheDir(const std::string& pCacheDir )
+{
+ cCacheDir = pCacheDir;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::queryCache( uint32_t discid )
+{
+ if (cCacheDir.empty())
+ return false;
+
+ XFILE::CFile file;
+ if (file.Open(GetCacheFile(discid)))
+ {
+ // Got a cachehit
+ char buffer[4096];
+ file.Read(buffer, 4096);
+ file.Close();
+ parseData( buffer );
+ return true;
+ }
+
+ return false;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::writeCacheFile( const char* pBuffer, uint32_t discid )
+{
+ if (cCacheDir.empty())
+ return false;
+
+ XFILE::CFile file;
+ if (file.OpenForWrite(GetCacheFile(discid), true))
+ {
+ const bool ret = ( (size_t) file.Write((const void*)pBuffer, strlen(pBuffer) + 1) == strlen(pBuffer) + 1);
+ file.Close();
+ return ret;
+ }
+
+ return false;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::isCDCached( int nr_of_tracks, toc cdtoc[] )
+{
+ if (cCacheDir.empty())
+ return false;
+
+ return XFILE::CFile::Exists(GetCacheFile(calc_disc_id(nr_of_tracks, cdtoc)));
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getYear() const
+{
+ return m_strYear;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+const std::string& Xcddb::getGenre() const
+{
+ return m_strGenre;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::queryCDinfo(CCdInfo* pInfo)
+{
+ if ( pInfo == NULL )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo pInfo == NULL");
+ m_lastError = E_PARAMETER_WRONG;
+ return false;
+ }
+
+ int lead_out = pInfo->GetTrackCount();
+ int real_track_count = pInfo->GetTrackCount();
+ uint32_t discid = pInfo->GetCddbDiscId();
+ unsigned long frames[100];
+
+
+ //##########################################################
+ //
+ if ( queryCache(discid) )
+ {
+ CLog::Log(LOGDEBUG, "Xcddb::queryCDinfo discid [{:08x}] already cached", discid);
+ return true;
+ }
+
+ //##########################################################
+ //
+ for (int i = 0;i < lead_out;i++)
+ {
+ frames[i] = pInfo->GetTrackInformation( i + 1 ).nFrames;
+ if (i > 0 && frames[i] < frames[i - 1])
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo E_TOC_INCORRECT");
+ m_lastError = E_TOC_INCORRECT;
+ return false;
+ }
+ }
+ unsigned long complete_length = pInfo->GetDiscLength();
+
+
+ //##########################################################
+ // Open socket to cddb database
+ if ( !openSocket() )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error opening socket");
+ m_lastError = E_NETWORK_ERROR_OPEN_SOCKET;
+ return false;
+ }
+ std::string recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 200: //OK, read/write allowed
+ case 201: //OK, read only
+ break;
+
+ case 432: //No connections allowed: permission denied
+ case 433: //No connections allowed: X users allowed, Y currently active
+ case 434: //No connections allowed: system load too high
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Send the Hello message
+ std::string version = CSysInfo::GetVersion();
+ std::string lcAppName = CCompileInfo::GetAppName();
+ StringUtils::ToLower(lcAppName);
+ if (version.find(' ') != std::string::npos)
+ version = version.substr(0, version.find(' '));
+ std::string strGreeting = "cddb hello " + lcAppName + " kodi.tv " + CCompileInfo::GetAppName() + " " + version;
+ if ( ! Send(strGreeting.c_str()) )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error sending \"{}\"", strGreeting);
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 200: //Handshake successful
+ case 402: //Already shook hands
+ break;
+
+ case 431: //Handshake not successful, closing connection
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Set CDDB protocol-level to 5
+ if ( ! Send("proto 5"))
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error sending \"{}\"", "proto 5");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 200: //CDDB protocol level: current cur_level, supported supp_level
+ case 201: //OK, protocol version now: cur_level
+ case 502: //Protocol level already cur_level
+ break;
+
+ case 501: //Illegal protocol level.
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Compose the cddb query string
+ char query_buffer[1024];
+ strcpy(query_buffer, "");
+ strcat(query_buffer, "cddb query");
+ {
+ char tmp_buffer[256];
+ sprintf(tmp_buffer, " %08x", discid);
+ strcat(query_buffer, tmp_buffer);
+ }
+ {
+ char tmp_buffer[256];
+ sprintf(tmp_buffer, " %i", real_track_count);
+ strcat(query_buffer, tmp_buffer);
+ }
+ for (int i = 0;i < lead_out;i++)
+ {
+ char tmp_buffer[256];
+ sprintf(tmp_buffer, " %lu", frames[i]);
+ strcat(query_buffer, tmp_buffer);
+ }
+ {
+ char tmp_buffer[256];
+ sprintf(tmp_buffer, " %lu", complete_length);
+ strcat(query_buffer, tmp_buffer);
+ }
+
+
+ //##########################################################
+ // Query for matches
+ if ( ! Send(query_buffer))
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error sending \"{}\"", query_buffer);
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ // 200 rock d012180e Soundtrack / Hackers
+ std::string read_buffer;
+ recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 200: //Found exact match
+ strtok(const_cast<char *>(recv_buffer.c_str()), " ");
+ read_buffer = StringUtils::Format("cddb read {} {:08x}", strtok(NULL, " "), discid);
+ break;
+
+ case 210: //Found exact matches, list follows (until terminating marker)
+ case 211: //Found inexact matches, list follows (until terminating marker)
+ /*
+ soundtrack bf0cf90f Modern Talking / Victory - The 11th Album
+ rock c90cf90f Modern Talking / Album: Victory (The 11th Album)
+ misc de0d020f Modern Talking / Ready for the victory
+ rock e00d080f Modern Talking / Album: Victory (The 11th Album)
+ rock c10d150f Modern Talking / Victory (The 11th Album)
+ .
+ */
+ recv_buffer += Recv(true);
+ addInexactList(recv_buffer.c_str());
+ m_lastError=E_WAIT_FOR_INPUT;
+ return false; //This is actually good. The calling method will handle this
+
+ case 202: //No match found
+ CLog::Log(
+ LOGINFO,
+ "Xcddb::queryCDinfo No match found in CDDB database when doing the query shown below:\n{}",
+ query_buffer);
+ [[fallthrough]];
+ case 403: //Database entry is corrupt
+ case 409: //No handshake
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Read the data from cddb
+ if ( !Send(read_buffer.c_str()) )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error sending \"{}\"", read_buffer);
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ recv_buffer = Recv(true);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 210: //OK, CDDB database entry follows (until terminating marker)
+ // Cool, I got it ;-)
+ writeCacheFile( recv_buffer.c_str(), discid );
+ parseData(recv_buffer.c_str());
+ break;
+
+ case 401: //Specified CDDB entry not found.
+ case 402: //Server error.
+ case 403: //Database entry is corrupt.
+ case 409: //No handshake.
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Quit
+ if ( ! Send("quit") )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error sending \"{}\"", "quit");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ recv_buffer = Recv(false);
+ m_lastError = atoi(recv_buffer.c_str());
+ switch(m_lastError)
+ {
+ case 0: //By some reason, also 0 is a valid value. This is not documented, and might depend on that no string was found and atoi then returns 0
+ case 230: //Closing connection. Goodbye.
+ break;
+
+ case 530: //error, closing connection.
+ default:
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error: \"{}\"", recv_buffer);
+ return false;
+ }
+
+
+ //##########################################################
+ // Close connection
+ if ( !closeSocket() )
+ {
+ CLog::Log(LOGERROR, "Xcddb::queryCDinfo Error closing socket");
+ m_lastError = E_NETWORK_ERROR_SEND;
+ return false;
+ }
+ return true;
+}
+
+//-------------------------------------------------------------------------------------------------------------------
+bool Xcddb::isCDCached( CCdInfo* pInfo )
+{
+ if (cCacheDir.empty())
+ return false;
+ if ( pInfo == NULL )
+ return false;
+
+ return XFILE::CFile::Exists(GetCacheFile(pInfo->GetCddbDiscId()));
+}
+
+std::string Xcddb::GetCacheFile(uint32_t disc_id) const
+{
+ std::string strFileName;
+ strFileName = StringUtils::Format("{:x}.cddb", disc_id);
+ return URIUtils::AddFileToFolder(cCacheDir, strFileName);
+}
+
+std::string Xcddb::TrimToUTF8(const std::string &untrimmedText)
+{
+ std::string text(untrimmedText);
+ StringUtils::Trim(text);
+ // You never know if you really get UTF-8 strings from cddb
+ g_charsetConverter.unknownToUTF8(text);
+ return text;
+}
diff --git a/xbmc/network/cddb.h b/xbmc/network/cddb.h
new file mode 100644
index 0000000..5217aa2
--- /dev/null
+++ b/xbmc/network/cddb.h
@@ -0,0 +1,125 @@
+/*
+ * 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 <string>
+#include <sstream>
+#include <iostream>
+#include <map>
+#ifndef TARGET_POSIX
+#include <strstream>
+#endif
+#include "storage/cdioSupport.h"
+
+#include "utils/ScopeGuard.h"
+
+namespace CDDB
+{
+
+//Can be removed if/when removing Xcddb::queryCDinfo(int real_track_count, toc cdtoc[])
+//#define IN_PROGRESS -1
+//#define QUERY_OK 7
+//#define E_INEXACT_MATCH_FOUND 211
+//#define W_CDDB_already_shook_hands 402
+//#define E_CDDB_Handshake_not_successful 431
+
+#define E_TOC_INCORRECT 2
+#define E_NETWORK_ERROR_OPEN_SOCKET 3
+#define E_NETWORK_ERROR_SEND 4
+#define E_WAIT_FOR_INPUT 5
+#define E_PARAMETER_WRONG 6
+#define E_NO_MATCH_FOUND 202
+
+#define CDDB_PORT 8880
+
+
+struct toc
+{
+ int min;
+ int sec;
+ int frame;
+};
+
+
+class Xcddb
+{
+public:
+ Xcddb();
+ virtual ~Xcddb();
+ void setCDDBIpAddress(const std::string& ip_address);
+ void setCacheDir(const std::string& pCacheDir );
+
+// int queryCDinfo(int real_track_count, toc cdtoc[]);
+ bool queryCDinfo(MEDIA_DETECT::CCdInfo* pInfo, int inexact_list_select);
+ bool queryCDinfo(MEDIA_DETECT::CCdInfo* pInfo);
+ int getLastError() const;
+ const char * getLastErrorText() const;
+ const std::string& getYear() const;
+ const std::string& getGenre() const;
+ const std::string& getTrackArtist(int track) const;
+ const std::string& getTrackTitle(int track) const;
+ void getDiskArtist(std::string& strdisk_artist) const;
+ void getDiskTitle(std::string& strdisk_title) const;
+ const std::string& getTrackExtended(int track) const;
+ uint32_t calc_disc_id(int nr_of_tracks, toc cdtoc[]);
+ const std::string& getInexactArtist(int select) const;
+ const std::string& getInexactTitle(int select) const;
+ bool queryCache( uint32_t discid );
+ bool writeCacheFile( const char* pBuffer, uint32_t discid );
+ bool isCDCached( int nr_of_tracks, toc cdtoc[] );
+ bool isCDCached( MEDIA_DETECT::CCdInfo* pInfo );
+
+protected:
+ std::string m_strNull;
+#if defined(TARGET_WINDOWS)
+ using CAutoPtrSocket = KODI::UTILS::CScopeGuard<SOCKET, INVALID_SOCKET, decltype(closesocket)>;
+#else
+ using CAutoPtrSocket = KODI::UTILS::CScopeGuard<int, -1, decltype(close)>;
+#endif
+ CAutoPtrSocket m_cddb_socket;
+ const static int recv_buffer = 4096;
+ int m_lastError;
+ std::map<int, std::string> m_mapTitles;
+ std::map<int, std::string> m_mapArtists;
+ std::map<int, std::string> m_mapExtended_track;
+
+ std::map<int, std::string> m_mapInexact_cddb_command_list;
+ std::map<int, std::string> m_mapInexact_artist_list;
+ std::map<int, std::string> m_mapInexact_title_list;
+
+
+ std::string m_strDisk_artist;
+ std::string m_strDisk_title;
+ std::string m_strYear;
+ std::string m_strGenre;
+
+ void addTitle(const char *buffer);
+ void addExtended(const char *buffer);
+ void parseData(const char *buffer);
+ bool Send( const void *buffer, int bytes );
+ bool Send( const char *buffer);
+ std::string Recv(bool wait4point);
+ bool openSocket();
+ bool closeSocket();
+ struct toc cdtoc[100];
+ int cddb_sum(int n);
+ void addInexactList(const char *list);
+ void addInexactListLine(int line_cnt, const char *line, int len);
+ const std::string& getInexactCommand(int select) const;
+ std::string GetCacheFile(uint32_t disc_id) const;
+ /*! \brief Trim and convert some text to UTF8
+ \param untrimmedText original text to trim and convert
+ \return a utf8 version of the trimmed text
+ */
+ std::string TrimToUTF8(const std::string &untrimmed);
+
+ std::string m_cddb_ip_address;
+ std::string cCacheDir;
+};
+}
diff --git a/xbmc/network/dacp/CMakeLists.txt b/xbmc/network/dacp/CMakeLists.txt
new file mode 100644
index 0000000..7eb2df5
--- /dev/null
+++ b/xbmc/network/dacp/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES dacp.cpp)
+
+set(HEADERS dacp.h)
+
+core_add_library(network_dacp)
diff --git a/xbmc/network/dacp/dacp.cpp b/xbmc/network/dacp/dacp.cpp
new file mode 100644
index 0000000..890d1f2
--- /dev/null
+++ b/xbmc/network/dacp/dacp.cpp
@@ -0,0 +1,97 @@
+/*
+ * 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 "dacp.h"
+
+#include "filesystem/File.h"
+
+#define AIRTUNES_DACP_CMD_URI "ctrl-int/1/"
+
+// AirTunes related DACP implementation taken from http://nto.github.io/AirPlay.html#audio-remotecontrol
+
+CDACP::CDACP(const std::string &active_remote_header, const std::string &hostname, int port)
+{
+ m_dacpUrl.SetHostName(hostname);
+ m_dacpUrl.SetPort(port);
+ m_dacpUrl.SetProtocol("http");
+ m_dacpUrl.SetProtocolOption("Active-Remote", active_remote_header);
+}
+
+void CDACP::SendCmd(const std::string &cmd)
+{
+ m_dacpUrl.SetFileName(AIRTUNES_DACP_CMD_URI + cmd);
+ // issue the command
+ XFILE::CFile file;
+ file.Open(m_dacpUrl);
+ file.Write(NULL, 0);
+}
+
+void CDACP::BeginFwd()
+{
+ SendCmd("beginff");
+}
+
+void CDACP::BeginRewnd()
+{
+ SendCmd("beginrew");
+}
+
+void CDACP::ToggleMute()
+{
+ SendCmd("mutetoggle");
+}
+
+void CDACP::NextItem()
+{
+ SendCmd("nextitem");
+}
+
+void CDACP::PrevItem()
+{
+ SendCmd("previtem");
+}
+
+void CDACP::Pause()
+{
+ SendCmd("pause");
+}
+
+void CDACP::PlayPause()
+{
+ SendCmd("playpause");
+}
+
+void CDACP::Play()
+{
+ SendCmd("play");
+}
+
+void CDACP::Stop()
+{
+ SendCmd("stop");
+}
+
+void CDACP::PlayResume()
+{
+ SendCmd("playresume");
+}
+
+void CDACP::ShuffleSongs()
+{
+ SendCmd("shuffle_songs");
+}
+
+void CDACP::VolumeDown()
+{
+ SendCmd("volumedown");
+}
+
+void CDACP::VolumeUp()
+{
+ SendCmd("volumeup");
+}
diff --git a/xbmc/network/dacp/dacp.h b/xbmc/network/dacp/dacp.h
new file mode 100644
index 0000000..074b477
--- /dev/null
+++ b/xbmc/network/dacp/dacp.h
@@ -0,0 +1,38 @@
+/*
+ * 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 "URL.h"
+
+#include <string>
+
+class CDACP
+{
+ public:
+ CDACP(const std::string &active_remote_header, const std::string &hostname, int port);
+
+ void BeginFwd();
+ void BeginRewnd();
+ void ToggleMute();
+ void NextItem();
+ void PrevItem();
+ void Pause();
+ void PlayPause();
+ void Play();
+ void Stop();
+ void PlayResume();
+ void ShuffleSongs();
+ void VolumeDown();
+ void VolumeUp();
+
+ private:
+ void SendCmd(const std::string &cmd);
+
+ CURL m_dacpUrl;
+};
diff --git a/xbmc/network/httprequesthandler/CMakeLists.txt b/xbmc/network/httprequesthandler/CMakeLists.txt
new file mode 100644
index 0000000..ea514c5
--- /dev/null
+++ b/xbmc/network/httprequesthandler/CMakeLists.txt
@@ -0,0 +1,30 @@
+if(MICROHTTPD_FOUND)
+ set(SOURCES HTTPFileHandler.cpp
+ HTTPImageHandler.cpp
+ HTTPImageTransformationHandler.cpp
+ HTTPJsonRpcHandler.cpp
+ HTTPRequestHandlerUtils.cpp
+ HTTPVfsHandler.cpp
+ HTTPWebinterfaceAddonsHandler.cpp
+ HTTPWebinterfaceHandler.cpp
+ IHTTPRequestHandler.cpp)
+
+ if(PYTHON_FOUND)
+ list(APPEND SOURCES HTTPPythonHandler.cpp)
+ endif()
+
+ set(HEADERS HTTPFileHandler.h
+ HTTPImageHandler.h
+ HTTPImageTransformationHandler.h
+ HTTPJsonRpcHandler.h
+ HTTPRequestHandlerUtils.h
+ HTTPVfsHandler.h
+ HTTPWebinterfaceAddonsHandler.h
+ HTTPWebinterfaceHandler.h
+ IHTTPRequestHandler.h)
+ if(PYTHON_FOUND)
+ list(APPEND HEADERS HTTPPythonHandler.h)
+ endif()
+
+ core_add_library(network_httprequesthandlers)
+endif()
diff --git a/xbmc/network/httprequesthandler/HTTPFileHandler.cpp b/xbmc/network/httprequesthandler/HTTPFileHandler.cpp
new file mode 100644
index 0000000..466ab9f
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPFileHandler.cpp
@@ -0,0 +1,103 @@
+/*
+ * 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 "HTTPFileHandler.h"
+
+#include "filesystem/File.h"
+#include "utils/Mime.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+CHTTPFileHandler::CHTTPFileHandler()
+ : IHTTPRequestHandler(),
+ m_url(),
+ m_lastModified()
+{ }
+
+CHTTPFileHandler::CHTTPFileHandler(const HTTPRequest &request)
+ : IHTTPRequestHandler(request),
+ m_url(),
+ m_lastModified()
+{ }
+
+MHD_RESULT CHTTPFileHandler::HandleRequest()
+{
+ return !m_url.empty() ? MHD_YES : MHD_NO;
+}
+
+bool CHTTPFileHandler::GetLastModifiedDate(CDateTime &lastModified) const
+{
+ if (!m_lastModified.IsValid())
+ return false;
+
+ lastModified = m_lastModified;
+ return true;
+}
+
+void CHTTPFileHandler::SetFile(const std::string& file, int responseStatus)
+{
+ m_url = file;
+ m_response.status = responseStatus;
+ if (m_url.empty())
+ return;
+
+ // translate the response status into the response type
+ if (m_response.status == MHD_HTTP_OK)
+ m_response.type = HTTPFileDownload;
+ else if (m_response.status == MHD_HTTP_FOUND)
+ m_response.type = HTTPRedirect;
+ else
+ m_response.type = HTTPError;
+
+ // try to determine some additional information if the file can be downloaded
+ if (m_response.type == HTTPFileDownload)
+ {
+ // determine the content type
+ std::string ext = URIUtils::GetExtension(m_url);
+ StringUtils::ToLower(ext);
+ m_response.contentType = CMime::GetMimeType(ext);
+
+ // determine the last modified date
+ XFILE::CFile fileObj;
+ if (!fileObj.Open(m_url, XFILE::READ_NO_CACHE))
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+ else
+ {
+ struct __stat64 statBuffer;
+ if (fileObj.Stat(&statBuffer) == 0)
+ SetLastModifiedDate(&statBuffer);
+ }
+ }
+
+ // disable ranges and caching if the file can't be downloaded
+ if (m_response.type != HTTPFileDownload)
+ {
+ m_canHandleRanges = false;
+ m_canBeCached = false;
+ }
+
+ // disable caching if the last modified date couldn't be read
+ if (!m_lastModified.IsValid())
+ m_canBeCached = false;
+}
+
+void CHTTPFileHandler::SetLastModifiedDate(const struct __stat64 *statBuffer)
+{
+ struct tm *time;
+#ifdef HAVE_LOCALTIME_R
+ struct tm result = {};
+ time = localtime_r((const time_t*)&statBuffer->st_mtime, &result);
+#else
+ time = localtime((time_t *)&statBuffer->st_mtime);
+#endif
+ if (time != NULL)
+ m_lastModified = *time;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPFileHandler.h b/xbmc/network/httprequesthandler/HTTPFileHandler.h
new file mode 100644
index 0000000..1562977
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPFileHandler.h
@@ -0,0 +1,48 @@
+/*
+ * 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 "XBDateTime.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <string>
+
+class CHTTPFileHandler : public IHTTPRequestHandler
+{
+public:
+ ~CHTTPFileHandler() override = default;
+
+ MHD_RESULT HandleRequest() override;
+
+ bool CanHandleRanges() const override { return m_canHandleRanges; }
+ bool CanBeCached() const override { return m_canBeCached; }
+ bool GetLastModifiedDate(CDateTime &lastModified) const override;
+
+ std::string GetRedirectUrl() const override { return m_url; }
+ std::string GetResponseFile() const override { return m_url; }
+
+protected:
+ CHTTPFileHandler();
+ explicit CHTTPFileHandler(const HTTPRequest &request);
+
+ void SetFile(const std::string& file, int responseStatus);
+
+ void SetCanHandleRanges(bool canHandleRanges) { m_canHandleRanges = canHandleRanges; }
+ void SetCanBeCached(bool canBeCached) { m_canBeCached = canBeCached; }
+ void SetLastModifiedDate(const struct __stat64 *buffer);
+
+private:
+ std::string m_url;
+
+ bool m_canHandleRanges = true;
+ bool m_canBeCached = true;
+
+ CDateTime m_lastModified;
+
+};
diff --git a/xbmc/network/httprequesthandler/HTTPImageHandler.cpp b/xbmc/network/httprequesthandler/HTTPImageHandler.cpp
new file mode 100644
index 0000000..6cde13b
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPImageHandler.cpp
@@ -0,0 +1,51 @@
+/*
+ * 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 "HTTPImageHandler.h"
+
+#include "URL.h"
+#include "filesystem/ImageFile.h"
+#include "network/WebServer.h"
+#include "utils/FileUtils.h"
+
+
+CHTTPImageHandler::CHTTPImageHandler(const HTTPRequest &request)
+ : CHTTPFileHandler(request)
+{
+ std::string file;
+ int responseStatus = MHD_HTTP_BAD_REQUEST;
+
+ // resolve the URL into a file path and a HTTP response status
+ if (m_request.pathUrl.size() > 7)
+ {
+ file = m_request.pathUrl.substr(7);
+
+ XFILE::CImageFile imageFile;
+ const CURL pathToUrl(file);
+ if (imageFile.Exists(pathToUrl) && CFileUtils::CheckFileAccessAllowed(file))
+ {
+ responseStatus = MHD_HTTP_OK;
+ struct __stat64 statBuffer;
+ if (imageFile.Stat(pathToUrl, &statBuffer) == 0)
+ {
+ SetLastModifiedDate(&statBuffer);
+ SetCanBeCached(true);
+ }
+ }
+ else
+ responseStatus = MHD_HTTP_NOT_FOUND;
+ }
+
+ // set the file and the HTTP response status
+ SetFile(file, responseStatus);
+}
+
+bool CHTTPImageHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ return request.pathUrl.find("/image/") == 0;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPImageHandler.h b/xbmc/network/httprequesthandler/HTTPImageHandler.h
new file mode 100644
index 0000000..97df097
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPImageHandler.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "network/httprequesthandler/HTTPFileHandler.h"
+
+#include <string>
+
+class CHTTPImageHandler : public CHTTPFileHandler
+{
+public:
+ CHTTPImageHandler() = default;
+ ~CHTTPImageHandler() override = default;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPImageHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+
+ int GetPriority() const override { return 5; }
+ int GetMaximumAgeForCaching() const override { return 60 * 60 * 24 * 7; }
+
+protected:
+ explicit CHTTPImageHandler(const HTTPRequest &request);
+};
diff --git a/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.cpp b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.cpp
new file mode 100644
index 0000000..3d2dccb
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.cpp
@@ -0,0 +1,170 @@
+/*
+ * 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 "HTTPImageTransformationHandler.h"
+
+#include "TextureCacheJob.h"
+#include "URL.h"
+#include "filesystem/ImageFile.h"
+#include "network/WebServer.h"
+#include "network/httprequesthandler/HTTPRequestHandlerUtils.h"
+#include "utils/Mime.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <map>
+
+#define TRANSFORMATION_OPTION_WIDTH "width"
+#define TRANSFORMATION_OPTION_HEIGHT "height"
+#define TRANSFORMATION_OPTION_SCALING_ALGORITHM "scaling_algorithm"
+
+static const std::string ImageBasePath = "/image/";
+
+CHTTPImageTransformationHandler::CHTTPImageTransformationHandler()
+ : m_url(),
+ m_lastModified(),
+ m_buffer(NULL),
+ m_responseData()
+{ }
+
+CHTTPImageTransformationHandler::CHTTPImageTransformationHandler(const HTTPRequest &request)
+ : IHTTPRequestHandler(request),
+ m_url(),
+ m_lastModified(),
+ m_buffer(NULL),
+ m_responseData()
+{
+ m_url = m_request.pathUrl.substr(ImageBasePath.size());
+ if (m_url.empty())
+ {
+ m_response.status = MHD_HTTP_BAD_REQUEST;
+ m_response.type = HTTPError;
+ return;
+ }
+
+ XFILE::CImageFile imageFile;
+ const CURL pathToUrl(m_url);
+ if (!imageFile.Exists(pathToUrl))
+ {
+ m_response.status = MHD_HTTP_NOT_FOUND;
+ m_response.type = HTTPError;
+ return;
+ }
+
+ m_response.type = HTTPMemoryDownloadNoFreeCopy;
+ m_response.status = MHD_HTTP_OK;
+
+ // determine the content type
+ std::string ext = URIUtils::GetExtension(pathToUrl.GetHostName());
+ StringUtils::ToLower(ext);
+ m_response.contentType = CMime::GetMimeType(ext);
+
+ //! @todo determine the maximum age
+
+ // determine the last modified date
+ struct __stat64 statBuffer;
+ if (imageFile.Stat(pathToUrl, &statBuffer) != 0)
+ return;
+
+ struct tm *time;
+#ifdef HAVE_LOCALTIME_R
+ struct tm result = {};
+ time = localtime_r((time_t*)&statBuffer.st_mtime, &result);
+#else
+ time = localtime((time_t *)&statBuffer.st_mtime);
+#endif
+ if (time == NULL)
+ return;
+
+ m_lastModified = *time;
+}
+
+CHTTPImageTransformationHandler::~CHTTPImageTransformationHandler()
+{
+ m_responseData.clear();
+ delete m_buffer;
+ m_buffer = NULL;
+}
+
+bool CHTTPImageTransformationHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ if ((request.method != GET && request.method != HEAD) ||
+ request.pathUrl.find(ImageBasePath) != 0 || request.pathUrl.size() <= ImageBasePath.size())
+ return false;
+
+ // get the transformation options
+ std::map<std::string, std::string> options;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(request.connection, MHD_GET_ARGUMENT_KIND, options);
+
+ return (options.find(TRANSFORMATION_OPTION_WIDTH) != options.end() ||
+ options.find(TRANSFORMATION_OPTION_HEIGHT) != options.end());
+}
+
+MHD_RESULT CHTTPImageTransformationHandler::HandleRequest()
+{
+ if (m_response.type == HTTPError)
+ return MHD_YES;
+
+ // get the transformation options
+ std::map<std::string, std::string> options;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_GET_ARGUMENT_KIND, options);
+
+ std::vector<std::string> urlOptions;
+ std::map<std::string, std::string>::const_iterator option = options.find(TRANSFORMATION_OPTION_WIDTH);
+ if (option != options.end())
+ urlOptions.push_back(TRANSFORMATION_OPTION_WIDTH "=" + option->second);
+
+ option = options.find(TRANSFORMATION_OPTION_HEIGHT);
+ if (option != options.end())
+ urlOptions.push_back(TRANSFORMATION_OPTION_HEIGHT "=" + option->second);
+
+ option = options.find(TRANSFORMATION_OPTION_SCALING_ALGORITHM);
+ if (option != options.end())
+ urlOptions.push_back(TRANSFORMATION_OPTION_SCALING_ALGORITHM "=" + option->second);
+
+ std::string imagePath = m_url;
+ if (!urlOptions.empty())
+ {
+ imagePath += "?";
+ imagePath += StringUtils::Join(urlOptions, "&");
+ }
+
+ // resize the image into the local buffer
+ size_t bufferSize;
+ if (!CTextureCacheJob::ResizeTexture(imagePath, m_buffer, bufferSize))
+ {
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ m_response.type = HTTPError;
+
+ return MHD_YES;
+ }
+
+ // store the size of the image
+ m_response.totalLength = bufferSize;
+
+ // nothing else to do if the request is not ranged
+ if (!GetRequestedRanges(m_response.totalLength))
+ {
+ m_responseData.push_back(CHttpResponseRange(m_buffer, 0, m_response.totalLength - 1));
+ return MHD_YES;
+ }
+
+ for (HttpRanges::const_iterator range = m_request.ranges.Begin(); range != m_request.ranges.End(); ++range)
+ m_responseData.push_back(CHttpResponseRange(m_buffer + range->GetFirstPosition(), range->GetFirstPosition(), range->GetLastPosition()));
+
+ return MHD_YES;
+}
+
+bool CHTTPImageTransformationHandler::GetLastModifiedDate(CDateTime &lastModified) const
+{
+ if (!m_lastModified.IsValid())
+ return false;
+
+ lastModified = m_lastModified;
+ return true;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.h b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.h
new file mode 100644
index 0000000..6d8732d
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPImageTransformationHandler.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "XBDateTime.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <stdint.h>
+#include <string>
+
+class CHTTPImageTransformationHandler : public IHTTPRequestHandler
+{
+public:
+ CHTTPImageTransformationHandler();
+ ~CHTTPImageTransformationHandler() override;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPImageTransformationHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request)const override;
+
+ MHD_RESULT HandleRequest() override;
+
+ bool CanHandleRanges() const override { return true; }
+ bool CanBeCached() const override { return true; }
+ bool GetLastModifiedDate(CDateTime &lastModified) const override;
+
+ HttpResponseRanges GetResponseData() const override { return m_responseData; }
+
+ // priority must be higher than the one of CHTTPImageHandler
+ int GetPriority() const override { return 6; }
+
+protected:
+ explicit CHTTPImageTransformationHandler(const HTTPRequest &request);
+
+private:
+ std::string m_url;
+ CDateTime m_lastModified;
+
+ uint8_t* m_buffer;
+ HttpResponseRanges m_responseData;
+};
diff --git a/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.cpp b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.cpp
new file mode 100644
index 0000000..9cfdbc5
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.cpp
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2011-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 "HTTPJsonRpcHandler.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "interfaces/json-rpc/JSONRPC.h"
+#include "interfaces/json-rpc/JSONServiceDescription.h"
+#include "network/httprequesthandler/HTTPRequestHandlerUtils.h"
+#include "utils/FileUtils.h"
+#include "utils/JSONVariantWriter.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#define MAX_HTTP_POST_SIZE 65536
+
+bool CHTTPJsonRpcHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ return (request.pathUrl.compare("/jsonrpc") == 0);
+}
+
+MHD_RESULT CHTTPJsonRpcHandler::HandleRequest()
+{
+ CHTTPClient client(m_request.method);
+ bool isRequest = false;
+ std::string jsonpCallback;
+
+ // get all query arguments
+ std::map<std::string, std::string> arguments;
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_GET_ARGUMENT_KIND, arguments);
+
+ if (m_request.method == POST)
+ {
+ std::string contentType = HTTPRequestHandlerUtils::GetRequestHeaderValue(m_request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_CONTENT_TYPE);
+ // If the content-type of the m_request was specified, it must be application/json-rpc, application/json, or application/jsonrequest
+ // http://www.jsonrpc.org/historical/json-rpc-over-http.html
+ if (!contentType.empty() && contentType.compare("application/json-rpc") != 0 &&
+ contentType.compare("application/json") != 0 && contentType.compare("application/jsonrequest") != 0)
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_UNSUPPORTED_MEDIA_TYPE;
+ return MHD_YES;
+ }
+
+ isRequest = true;
+ }
+ else if (m_request.method == GET || m_request.method == HEAD)
+ {
+ std::map<std::string, std::string>::const_iterator argument = arguments.find("request");
+ if (argument != arguments.end() && !argument->second.empty())
+ {
+ m_requestData = argument->second;
+ isRequest = true;
+ }
+ }
+
+ std::map<std::string, std::string>::const_iterator argument = arguments.find("jsonp");
+ if (argument != arguments.end() && !argument->second.empty())
+ jsonpCallback = argument->second;
+ else
+ {
+ argument = arguments.find("callback");
+ if (argument != arguments.end() && !argument->second.empty())
+ jsonpCallback = argument->second;
+ }
+
+ if (isRequest)
+ {
+ m_responseData = JSONRPC::CJSONRPC::MethodCall(m_requestData, &m_transportLayer, &client);
+
+ if (!jsonpCallback.empty())
+ m_responseData = jsonpCallback + "(" + m_responseData + ");";
+ }
+ else if (jsonpCallback.empty())
+ {
+ // get the whole output of JSONRPC.Introspect
+ CVariant result;
+ JSONRPC::CJSONServiceDescription::Print(result, &m_transportLayer, &client);
+ if (!CJSONVariantWriter::Write(result, m_responseData, false))
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+
+ return MHD_YES;
+ }
+ }
+ else
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_BAD_REQUEST;
+
+ return MHD_YES;
+ }
+
+ m_requestData.clear();
+
+ m_responseRange.SetData(m_responseData.c_str(), m_responseData.size());
+
+ m_response.type = HTTPMemoryDownloadNoFreeCopy;
+ m_response.status = MHD_HTTP_OK;
+ m_response.contentType = "application/json";
+ m_response.totalLength = m_responseData.size();
+
+ return MHD_YES;
+}
+
+HttpResponseRanges CHTTPJsonRpcHandler::GetResponseData() const
+{
+ HttpResponseRanges ranges;
+ ranges.push_back(m_responseRange);
+
+ return ranges;
+}
+
+bool CHTTPJsonRpcHandler::appendPostData(const char *data, size_t size)
+{
+ if (m_requestData.size() + size > MAX_HTTP_POST_SIZE)
+ {
+ CServiceBroker::GetLogging()
+ .GetLogger("CHTTPJsonRpcHandler")
+ ->error("Stopped uploading POST data since it exceeded size limitations ({})",
+ MAX_HTTP_POST_SIZE);
+ return false;
+ }
+
+ m_requestData.append(data, size);
+
+ return true;
+}
+
+bool CHTTPJsonRpcHandler::CHTTPTransportLayer::PrepareDownload(const char *path, CVariant &details, std::string &protocol)
+{
+ if (!CFileUtils::Exists(path))
+ return false;
+
+ protocol = "http";
+ std::string url;
+ std::string strPath = path;
+ if (StringUtils::StartsWith(strPath, "image://") ||
+ (StringUtils::StartsWith(strPath, "special://") && StringUtils::EndsWith(strPath, ".tbn")))
+ url = "image/";
+ else
+ url = "vfs/";
+ url += CURL::Encode(strPath);
+ details["path"] = url;
+
+ return true;
+}
+
+bool CHTTPJsonRpcHandler::CHTTPTransportLayer::Download(const char *path, CVariant &result)
+{
+ return false;
+}
+
+int CHTTPJsonRpcHandler::CHTTPTransportLayer::GetCapabilities()
+{
+ return JSONRPC::Response | JSONRPC::FileDownloadRedirect;
+}
+
+CHTTPJsonRpcHandler::CHTTPClient::CHTTPClient(HTTPMethod method)
+ : m_permissionFlags(JSONRPC::ReadData)
+{
+ // with a HTTP POST request everything is allowed
+ if (method == POST)
+ m_permissionFlags = JSONRPC::OPERATION_PERMISSION_ALL;
+}
+
+int CHTTPJsonRpcHandler::CHTTPClient::GetAnnouncementFlags()
+{
+ // Does not support broadcast
+ return 0;
+}
+
+bool CHTTPJsonRpcHandler::CHTTPClient::SetAnnouncementFlags(int flags)
+{
+ return false;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.h b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.h
new file mode 100644
index 0000000..88d4496
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPJsonRpcHandler.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011-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 "interfaces/json-rpc/IClient.h"
+#include "interfaces/json-rpc/ITransportLayer.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <string>
+
+class CHTTPJsonRpcHandler : public IHTTPRequestHandler
+{
+public:
+ CHTTPJsonRpcHandler() = default;
+ ~CHTTPJsonRpcHandler() override = default;
+
+ // implementations of IHTTPRequestHandler
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPJsonRpcHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+
+ MHD_RESULT HandleRequest() override;
+
+ HttpResponseRanges GetResponseData() const override;
+
+ int GetPriority() const override { return 5; }
+
+protected:
+ explicit CHTTPJsonRpcHandler(const HTTPRequest &request)
+ : IHTTPRequestHandler(request)
+ { }
+
+ bool appendPostData(const char *data, size_t size) override;
+
+private:
+ std::string m_requestData;
+ std::string m_responseData;
+ CHttpResponseRange m_responseRange;
+
+ class CHTTPTransportLayer : public JSONRPC::ITransportLayer
+ {
+ public:
+ CHTTPTransportLayer() = default;
+ ~CHTTPTransportLayer() override = default;
+
+ // implementations of JSONRPC::ITransportLayer
+ bool PrepareDownload(const char *path, CVariant &details, std::string &protocol) override;
+ bool Download(const char *path, CVariant &result) override;
+ int GetCapabilities() override;
+ };
+ CHTTPTransportLayer m_transportLayer;
+
+ class CHTTPClient : public JSONRPC::IClient
+ {
+ public:
+ explicit CHTTPClient(HTTPMethod method);
+ ~CHTTPClient() override = default;
+
+ int GetPermissionFlags() override { return m_permissionFlags; }
+ int GetAnnouncementFlags() override;
+ bool SetAnnouncementFlags(int flags) override;
+
+ private:
+ int m_permissionFlags;
+ };
+};
diff --git a/xbmc/network/httprequesthandler/HTTPPythonHandler.cpp b/xbmc/network/httprequesthandler/HTTPPythonHandler.cpp
new file mode 100644
index 0000000..8633744
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPPythonHandler.cpp
@@ -0,0 +1,250 @@
+/*
+ * 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 "HTTPPythonHandler.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/Webinterface.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/File.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "interfaces/python/XBPython.h"
+#include "network/WebServer.h"
+#include "network/httprequesthandler/HTTPRequestHandlerUtils.h"
+#include "network/httprequesthandler/HTTPWebinterfaceHandler.h"
+#include "network/httprequesthandler/python/HTTPPythonInvoker.h"
+#include "network/httprequesthandler/python/HTTPPythonWsgiInvoker.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#define MAX_STRING_POST_SIZE 20000
+
+CHTTPPythonHandler::CHTTPPythonHandler()
+ : IHTTPRequestHandler(),
+ m_scriptPath(),
+ m_addon(),
+ m_lastModified(),
+ m_requestData(),
+ m_responseData(),
+ m_responseRanges(),
+ m_redirectUrl()
+{ }
+
+CHTTPPythonHandler::CHTTPPythonHandler(const HTTPRequest &request)
+ : IHTTPRequestHandler(request),
+ m_scriptPath(),
+ m_addon(),
+ m_lastModified(),
+ m_requestData(),
+ m_responseData(),
+ m_responseRanges(),
+ m_redirectUrl()
+{
+ m_response.type = HTTPMemoryDownloadNoFreeCopy;
+
+ // get the real path of the script and check if it actually exists
+ m_response.status = CHTTPWebinterfaceHandler::ResolveUrl(m_request.pathUrl, m_scriptPath, m_addon);
+ // only allow requests to a non-static webinterface addon
+ if (m_addon == NULL || m_addon->Type() != ADDON::AddonType::WEB_INTERFACE ||
+ std::dynamic_pointer_cast<ADDON::CWebinterface>(m_addon)->GetType() ==
+ ADDON::WebinterfaceTypeStatic)
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+
+ return;
+ }
+
+ std::shared_ptr<ADDON::CWebinterface> webinterface = std::dynamic_pointer_cast<ADDON::CWebinterface>(m_addon);
+
+ // forward every request to the default entry point
+ m_scriptPath = webinterface->LibPath();
+
+ // we need to map any requests to a specific WSGI webinterface to the root path
+ std::string baseLocation = webinterface->GetBaseLocation();
+ if (!URIUtils::PathHasParent(m_request.pathUrl, baseLocation))
+ {
+ m_response.type = HTTPRedirect;
+ m_response.status = MHD_HTTP_MOVED_PERMANENTLY;
+ m_redirectUrl = baseLocation + m_request.pathUrl;
+ }
+
+ // no need to try to read the last modified date from a non-existing file
+ if (m_response.status != MHD_HTTP_OK)
+ return;
+
+ // determine the last modified date
+ const CURL pathToUrl(m_scriptPath);
+ struct __stat64 statBuffer;
+ if (XFILE::CFile::Stat(pathToUrl, &statBuffer) != 0)
+ return;
+
+ struct tm* time;
+#ifdef HAVE_LOCALTIME_R
+ struct tm result = {};
+ time = localtime_r((time_t*)&statBuffer.st_mtime, &result);
+#else
+ time = localtime((time_t *)&statBuffer.st_mtime);
+#endif
+ if (time == NULL)
+ return;
+
+ m_lastModified = *time;
+}
+
+bool CHTTPPythonHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ ADDON::AddonPtr addon;
+ std::string path;
+ // try to resolve the addon as any python script must be part of a webinterface
+ if (!CHTTPWebinterfaceHandler::ResolveAddon(request.pathUrl, addon, path) || addon == NULL ||
+ addon->Type() != ADDON::AddonType::WEB_INTERFACE)
+ return false;
+
+ // static webinterfaces aren't allowed to run python scripts
+ ADDON::CWebinterface* webinterface = static_cast<ADDON::CWebinterface*>(addon.get());
+ if (webinterface->GetType() != ADDON::WebinterfaceTypeWsgi)
+ return false;
+
+ return true;
+}
+
+MHD_RESULT CHTTPPythonHandler::HandleRequest()
+{
+ if (m_response.type == HTTPError || m_response.type == HTTPRedirect)
+ return MHD_YES;
+
+ std::vector<std::string> args;
+ args.push_back(m_scriptPath);
+
+ try
+ {
+ HTTPPythonRequest* pythonRequest = new HTTPPythonRequest();
+ pythonRequest->connection = m_request.connection;
+ pythonRequest->file = URIUtils::GetFileName(m_request.pathUrl);
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_GET_ARGUMENT_KIND, pythonRequest->getValues);
+ HTTPRequestHandlerUtils::GetRequestHeaderValues(m_request.connection, MHD_HEADER_KIND, pythonRequest->headerValues);
+ pythonRequest->method = m_request.method;
+ pythonRequest->postValues = m_postFields;
+ pythonRequest->requestContent = m_requestData;
+ pythonRequest->responseType = HTTPNone;
+ pythonRequest->responseLength = 0;
+ pythonRequest->responseStatus = MHD_HTTP_OK;
+ pythonRequest->url = m_request.pathUrlFull;
+ pythonRequest->path = m_request.pathUrl;
+ pythonRequest->version = m_request.version;
+ pythonRequest->requestTime = CDateTime::GetCurrentDateTime();
+ pythonRequest->lastModifiedTime = m_lastModified;
+
+ std::string hostname;
+ uint16_t port;
+ if (GetHostnameAndPort(hostname, port))
+ {
+ pythonRequest->hostname = hostname;
+ pythonRequest->port = port;
+ }
+
+ CHTTPPythonInvoker* pythonInvoker =
+ new CHTTPPythonWsgiInvoker(&CServiceBroker::GetXBPython(), pythonRequest);
+ LanguageInvokerPtr languageInvokerPtr(pythonInvoker);
+ int result = CScriptInvocationManager::GetInstance().ExecuteSync(m_scriptPath, languageInvokerPtr, m_addon, args, 30000, false);
+
+ // check if the script couldn't be started
+ if (result < 0)
+ {
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ m_response.type = HTTPError;
+
+ return MHD_YES;
+ }
+ // check if the script exited with an error
+ if (result > 0)
+ {
+ // check if the script didn't finish in time
+ if (result == ETIMEDOUT)
+ m_response.status = MHD_HTTP_REQUEST_TIMEOUT;
+ else
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ m_response.type = HTTPError;
+
+ return MHD_YES;
+ }
+
+ HTTPPythonRequest* pythonFinalizedRequest = pythonInvoker->GetRequest();
+ if (pythonFinalizedRequest == NULL)
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ return MHD_YES;
+ }
+
+ m_response.type = pythonFinalizedRequest->responseType;
+ m_response.status = pythonFinalizedRequest->responseStatus;
+ if (m_response.status < MHD_HTTP_BAD_REQUEST)
+ {
+ if (m_response.type == HTTPNone)
+ m_response.type = HTTPMemoryDownloadNoFreeCopy;
+ m_response.headers = pythonFinalizedRequest->responseHeaders;
+
+ if (pythonFinalizedRequest->lastModifiedTime.IsValid())
+ m_lastModified = pythonFinalizedRequest->lastModifiedTime;
+ }
+ else
+ {
+ if (m_response.type == HTTPNone)
+ m_response.type = HTTPError;
+ m_response.headers = pythonFinalizedRequest->responseHeadersError;
+ }
+
+ m_responseData = pythonFinalizedRequest->responseData;
+ if (pythonFinalizedRequest->responseLength > 0 && pythonFinalizedRequest->responseLength <= m_responseData.size())
+ m_response.totalLength = pythonFinalizedRequest->responseLength;
+ else
+ m_response.totalLength = m_responseData.size();
+
+ CHttpResponseRange responseRange(m_responseData.c_str(), m_responseData.size());
+ m_responseRanges.push_back(responseRange);
+
+ if (!pythonFinalizedRequest->responseContentType.empty())
+ m_response.contentType = pythonFinalizedRequest->responseContentType;
+ }
+ catch (...)
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ return MHD_YES;
+}
+
+bool CHTTPPythonHandler::GetLastModifiedDate(CDateTime &lastModified) const
+{
+ if (!m_lastModified.IsValid())
+ return false;
+
+ lastModified = m_lastModified;
+ return true;
+}
+
+bool CHTTPPythonHandler::appendPostData(const char *data, size_t size)
+{
+ if (m_requestData.size() + size > MAX_STRING_POST_SIZE)
+ {
+ CServiceBroker::GetLogging()
+ .GetLogger("CHTTPPythonHandler")
+ ->error("Stopped uploading post since it exceeded size limitations ({})",
+ MAX_STRING_POST_SIZE);
+ return false;
+ }
+
+ m_requestData.append(data, size);
+
+ return true;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPPythonHandler.h b/xbmc/network/httprequesthandler/HTTPPythonHandler.h
new file mode 100644
index 0000000..166430e
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPPythonHandler.h
@@ -0,0 +1,51 @@
+/*
+ * 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 "XBDateTime.h"
+#include "addons/IAddon.h"
+#include "addons/Webinterface.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+class CHTTPPythonHandler : public IHTTPRequestHandler
+{
+public:
+ CHTTPPythonHandler();
+ ~CHTTPPythonHandler() override = default;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPPythonHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+ bool CanHandleRanges() const override { return false; }
+ bool CanBeCached() const override { return false; }
+ bool GetLastModifiedDate(CDateTime &lastModified) const override;
+
+ MHD_RESULT HandleRequest() override;
+
+ HttpResponseRanges GetResponseData() const override { return m_responseRanges; }
+
+ std::string GetRedirectUrl() const override { return m_redirectUrl; }
+
+ int GetPriority() const override { return 3; }
+
+protected:
+ explicit CHTTPPythonHandler(const HTTPRequest &request);
+
+ bool appendPostData(const char *data, size_t size) override;
+
+private:
+ std::string m_scriptPath;
+ ADDON::AddonPtr m_addon;
+ CDateTime m_lastModified;
+
+ std::string m_requestData;
+ std::string m_responseData;
+ HttpResponseRanges m_responseRanges;
+
+ std::string m_redirectUrl;
+};
diff --git a/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.cpp b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.cpp
new file mode 100644
index 0000000..240449a
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016-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 "HTTPRequestHandlerUtils.h"
+
+#include "utils/StringUtils.h"
+
+#include <map>
+
+std::string HTTPRequestHandlerUtils::GetRequestHeaderValue(struct MHD_Connection *connection, enum MHD_ValueKind kind, const std::string &key)
+{
+ if (connection == nullptr)
+ return "";
+
+ const char* value = MHD_lookup_connection_value(connection, kind, key.c_str());
+ if (value == nullptr)
+ return "";
+
+ if (StringUtils::EqualsNoCase(key, MHD_HTTP_HEADER_CONTENT_TYPE))
+ {
+ // Work around a bug in firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=416178)
+ // by cutting of anything that follows a ";" in a "Content-Type" header field
+ std::string strValue(value);
+ size_t pos = strValue.find(';');
+ if (pos != std::string::npos)
+ strValue = strValue.substr(0, pos);
+
+ return strValue;
+ }
+
+ return value;
+}
+
+int HTTPRequestHandlerUtils::GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::map<std::string, std::string> &headerValues)
+{
+ if (connection == nullptr)
+ return -1;
+
+ return MHD_get_connection_values(connection, kind, FillArgumentMap, &headerValues);
+}
+
+int HTTPRequestHandlerUtils::GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::multimap<std::string, std::string> &headerValues)
+{
+ if (connection == nullptr)
+ return -1;
+
+ return MHD_get_connection_values(connection, kind, FillArgumentMultiMap, &headerValues);
+}
+
+bool HTTPRequestHandlerUtils::GetRequestedRanges(struct MHD_Connection *connection, uint64_t totalLength, CHttpRanges &ranges)
+{
+ ranges.Clear();
+
+ if (connection == nullptr)
+ return false;
+
+ return ranges.Parse(GetRequestHeaderValue(connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_RANGE), totalLength);
+}
+
+MHD_RESULT HTTPRequestHandlerUtils::FillArgumentMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value)
+{
+ if (cls == nullptr || key == nullptr)
+ return MHD_NO;
+
+ std::map<std::string, std::string> *arguments = reinterpret_cast<std::map<std::string, std::string>*>(cls);
+ arguments->insert(std::make_pair(key, value != nullptr ? value : ""));
+
+ return MHD_YES;
+}
+
+MHD_RESULT HTTPRequestHandlerUtils::FillArgumentMultiMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value)
+{
+ if (cls == nullptr || key == nullptr)
+ return MHD_NO;
+
+ std::multimap<std::string, std::string> *arguments = reinterpret_cast<std::multimap<std::string, std::string>*>(cls);
+ arguments->insert(std::make_pair(key, value != nullptr ? value : ""));
+
+ return MHD_YES;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.h b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.h
new file mode 100644
index 0000000..d02b5c1
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPRequestHandlerUtils.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016-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 "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <stdint.h>
+#include <string>
+
+class HTTPRequestHandlerUtils
+{
+public:
+ static std::string GetRequestHeaderValue(struct MHD_Connection *connection, enum MHD_ValueKind kind, const std::string &key);
+ static int GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::map<std::string, std::string> &headerValues);
+ static int GetRequestHeaderValues(struct MHD_Connection *connection, enum MHD_ValueKind kind, std::multimap<std::string, std::string> &headerValues);
+
+ static bool GetRequestedRanges(struct MHD_Connection *connection, uint64_t totalLength, CHttpRanges &ranges);
+
+private:
+ HTTPRequestHandlerUtils() = delete;
+
+ static MHD_RESULT FillArgumentMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value);
+ static MHD_RESULT FillArgumentMultiMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value);
+};
diff --git a/xbmc/network/httprequesthandler/HTTPVfsHandler.cpp b/xbmc/network/httprequesthandler/HTTPVfsHandler.cpp
new file mode 100644
index 0000000..ef2f7a6
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPVfsHandler.cpp
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2011-2020 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 "HTTPVfsHandler.h"
+
+#include "MediaSource.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "media/MediaLockState.h"
+#include "settings/MediaSourceSettings.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+
+CHTTPVfsHandler::CHTTPVfsHandler(const HTTPRequest &request)
+ : CHTTPFileHandler(request)
+{
+ std::string file;
+ int responseStatus = MHD_HTTP_BAD_REQUEST;
+
+ if (m_request.pathUrl.size() > 5)
+ {
+ file = m_request.pathUrl.substr(5);
+
+ if (CFileUtils::Exists(file))
+ {
+ bool accessible = false;
+ if (file.substr(0, 8) == "image://")
+ accessible = true;
+ else
+ {
+ std::string sourceTypes[] = { "video", "music", "pictures" };
+ unsigned int size = sizeof(sourceTypes) / sizeof(std::string);
+
+ std::string realPath = URIUtils::GetRealPath(file);
+ // for rar:// and zip:// paths we need to extract the path to the archive instead of using the VFS path
+ while (URIUtils::IsInArchive(realPath))
+ realPath = CURL(realPath).GetHostName();
+
+ // Check manually configured sources
+ VECSOURCES *sources = NULL;
+ for (unsigned int index = 0; index < size && !accessible; index++)
+ {
+ sources = CMediaSourceSettings::GetInstance().GetSources(sourceTypes[index]);
+ if (sources == NULL)
+ continue;
+
+ for (const auto& source : *sources)
+ {
+ if (accessible)
+ break;
+
+ // don't allow access to locked / disabled sharing sources
+ if (source.m_iHasLock == LOCK_STATE_LOCKED || !source.m_allowSharing)
+ continue;
+
+ for (const auto& path : source.vecPaths)
+ {
+ std::string realSourcePath = URIUtils::GetRealPath(path);
+ if (URIUtils::PathHasParent(realPath, realSourcePath, true))
+ {
+ accessible = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // Check auto-mounted sources
+ if (!accessible)
+ {
+ bool isSource;
+ VECSOURCES removableSources;
+ CServiceBroker::GetMediaManager().GetRemovableDrives(removableSources);
+ int sourceIndex = CUtil::GetMatchingSource(realPath, removableSources, isSource);
+ if (sourceIndex >= 0 && sourceIndex < static_cast<int>(removableSources.size()) &&
+ removableSources.at(sourceIndex).m_iHasLock != LOCK_STATE_LOCKED &&
+ removableSources.at(sourceIndex).m_allowSharing)
+ accessible = true;
+ }
+ }
+
+ if (accessible)
+ responseStatus = MHD_HTTP_OK;
+ // the file exists but not in one of the defined sources so we deny access to it
+ else
+ responseStatus = MHD_HTTP_UNAUTHORIZED;
+ }
+ else
+ responseStatus = MHD_HTTP_NOT_FOUND;
+ }
+
+ // set the file and the HTTP response status
+ SetFile(file, responseStatus);
+}
+
+bool CHTTPVfsHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ return request.pathUrl.find("/vfs") == 0;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPVfsHandler.h b/xbmc/network/httprequesthandler/HTTPVfsHandler.h
new file mode 100644
index 0000000..af66bad
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPVfsHandler.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2011-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 "network/httprequesthandler/HTTPFileHandler.h"
+
+#include <string>
+
+class CHTTPVfsHandler : public CHTTPFileHandler
+{
+public:
+ CHTTPVfsHandler() = default;
+ ~CHTTPVfsHandler() override = default;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPVfsHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+
+ int GetPriority() const override { return 5; }
+
+protected:
+ explicit CHTTPVfsHandler(const HTTPRequest &request);
+};
diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.cpp b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.cpp
new file mode 100644
index 0000000..fa47144
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2011-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 "HTTPWebinterfaceAddonsHandler.h"
+
+#include "ServiceBroker.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "network/WebServer.h"
+
+#define ADDON_HEADER "<html><head><title>Add-on List</title></head><body>\n<h1>Available web interfaces:</h1>\n<ul>\n"
+
+bool CHTTPWebinterfaceAddonsHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ return (request.pathUrl.compare("/addons") == 0 || request.pathUrl.compare("/addons/") == 0);
+}
+
+MHD_RESULT CHTTPWebinterfaceAddonsHandler::HandleRequest()
+{
+ m_responseData = ADDON_HEADER;
+ ADDON::VECADDONS addons;
+ if (!CServiceBroker::GetAddonMgr().GetAddons(addons, ADDON::AddonType::WEB_INTERFACE) ||
+ addons.empty())
+ {
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+
+ return MHD_YES;
+ }
+
+ for (const auto& addon : addons)
+ m_responseData += "<li><a href=/addons/" + addon->ID() + "/>" + addon->Name() + "</a></li>\n";
+
+ m_responseData += "</ul>\n</body></html>";
+
+ m_responseRange.SetData(m_responseData.c_str(), m_responseData.size());
+
+ m_response.type = HTTPMemoryDownloadNoFreeCopy;
+ m_response.status = MHD_HTTP_OK;
+ m_response.contentType = "text/html";
+ m_response.totalLength = m_responseData.size();
+
+ return MHD_YES;
+}
+
+HttpResponseRanges CHTTPWebinterfaceAddonsHandler::GetResponseData() const
+{
+ HttpResponseRanges ranges;
+ ranges.push_back(m_responseRange);
+
+ return ranges;
+}
+
+
diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h
new file mode 100644
index 0000000..20a44ec
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceAddonsHandler.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011-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 "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <string>
+
+class CHTTPWebinterfaceAddonsHandler : public IHTTPRequestHandler
+{
+public:
+ CHTTPWebinterfaceAddonsHandler() = default;
+ ~CHTTPWebinterfaceAddonsHandler() override = default;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPWebinterfaceAddonsHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+
+ MHD_RESULT HandleRequest() override;
+
+ HttpResponseRanges GetResponseData() const override;
+
+ int GetPriority() const override { return 4; }
+
+protected:
+ explicit CHTTPWebinterfaceAddonsHandler(const HTTPRequest &request)
+ : IHTTPRequestHandler(request)
+ { }
+
+private:
+ std::string m_responseData;
+ CHttpResponseRange m_responseRange;
+};
diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.cpp b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.cpp
new file mode 100644
index 0000000..fe6e760
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.cpp
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011-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 "HTTPWebinterfaceHandler.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/Webinterface.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/Directory.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#define WEBSERVER_DIRECTORY_SEPARATOR "/"
+
+CHTTPWebinterfaceHandler::CHTTPWebinterfaceHandler(const HTTPRequest &request)
+ : CHTTPFileHandler(request)
+{
+ // resolve the URL into a file path and a HTTP response status
+ std::string file;
+ int responseStatus = ResolveUrl(request.pathUrl, file);
+
+ // set the file and the HTTP response status
+ SetFile(file, responseStatus);
+}
+
+bool CHTTPWebinterfaceHandler::CanHandleRequest(const HTTPRequest &request) const
+{
+ return true;
+}
+
+int CHTTPWebinterfaceHandler::ResolveUrl(const std::string &url, std::string &path)
+{
+ ADDON::AddonPtr dummyAddon;
+ return ResolveUrl(url, path, dummyAddon);
+}
+
+int CHTTPWebinterfaceHandler::ResolveUrl(const std::string &url, std::string &path, ADDON::AddonPtr &addon)
+{
+ // determine the addon and addon's path
+ if (!ResolveAddon(url, addon, path))
+ return MHD_HTTP_NOT_FOUND;
+
+ if (XFILE::CDirectory::Exists(path))
+ {
+ if (URIUtils::GetFileName(path).empty())
+ {
+ // determine the actual file path using the default entry point
+ if (addon != NULL && addon->Type() == ADDON::AddonType::WEB_INTERFACE)
+ path = std::dynamic_pointer_cast<ADDON::CWebinterface>(addon)->GetEntryPoint(path);
+ }
+ else
+ {
+ URIUtils::AddSlashAtEnd(path);
+ return MHD_HTTP_FOUND;
+ }
+ }
+
+ if (!CFileUtils::CheckFileAccessAllowed(path))
+ return MHD_HTTP_NOT_FOUND;
+
+ if (!CFileUtils::Exists(path))
+ return MHD_HTTP_NOT_FOUND;
+
+ return MHD_HTTP_OK;
+}
+
+bool CHTTPWebinterfaceHandler::ResolveAddon(const std::string &url, ADDON::AddonPtr &addon)
+{
+ std::string addonPath;
+ return ResolveAddon(url, addon, addonPath);
+}
+
+bool CHTTPWebinterfaceHandler::ResolveAddon(const std::string &url, ADDON::AddonPtr &addon, std::string &addonPath)
+{
+ std::string path = url;
+
+ // check if the URL references a specific addon
+ if (url.find("/addons/") == 0 && url.size() > 8)
+ {
+ std::vector<std::string> components;
+ StringUtils::Tokenize(path, components, WEBSERVER_DIRECTORY_SEPARATOR);
+ if (components.size() <= 1)
+ return false;
+
+ if (!CServiceBroker::GetAddonMgr().GetAddon(components.at(1), addon,
+ ADDON::OnlyEnabled::CHOICE_YES) ||
+ addon == NULL)
+ return false;
+
+ addonPath = addon->Path();
+ if (addon->Type() !=
+ ADDON::AddonType::WEB_INTERFACE) // No need to append /htdocs for web interfaces
+ addonPath = URIUtils::AddFileToFolder(addonPath, "/htdocs/");
+
+ // remove /addons/<addon-id> from the path
+ components.erase(components.begin(), components.begin() + 2);
+
+ // determine the path within the addon
+ path = StringUtils::Join(components, WEBSERVER_DIRECTORY_SEPARATOR);
+ }
+ else if (!ADDON::CAddonSystemSettings::GetInstance().GetActive(ADDON::AddonType::WEB_INTERFACE,
+ addon) ||
+ addon == NULL)
+ return false;
+
+ // get the path of the addon
+ addonPath = addon->Path();
+
+ // add /htdocs/ to the addon's path if it's not a webinterface
+ if (addon->Type() != ADDON::AddonType::WEB_INTERFACE)
+ addonPath = URIUtils::AddFileToFolder(addonPath, "/htdocs/");
+
+ // append the path within the addon to the path of the addon
+ addonPath = URIUtils::AddFileToFolder(addonPath, path);
+
+ // ensure that we don't have a directory traversal hack here
+ // by checking if the resolved absolute path is inside the addon path
+ std::string realPath = URIUtils::GetRealPath(addonPath);
+ std::string realAddonPath = URIUtils::GetRealPath(addon->Path());
+ if (!URIUtils::PathHasParent(realPath, realAddonPath, true))
+ return false;
+
+ return true;
+}
diff --git a/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.h b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.h
new file mode 100644
index 0000000..5618c75
--- /dev/null
+++ b/xbmc/network/httprequesthandler/HTTPWebinterfaceHandler.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011-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 "addons/IAddon.h"
+#include "network/httprequesthandler/HTTPFileHandler.h"
+
+#include <string>
+
+class CHTTPWebinterfaceHandler : public CHTTPFileHandler
+{
+public:
+ CHTTPWebinterfaceHandler() = default;
+ ~CHTTPWebinterfaceHandler() override = default;
+
+ IHTTPRequestHandler* Create(const HTTPRequest &request) const override { return new CHTTPWebinterfaceHandler(request); }
+ bool CanHandleRequest(const HTTPRequest &request) const override;
+
+ static int ResolveUrl(const std::string &url, std::string &path);
+ static int ResolveUrl(const std::string &url, std::string &path, ADDON::AddonPtr &addon);
+ static bool ResolveAddon(const std::string &url, ADDON::AddonPtr &addon);
+ static bool ResolveAddon(const std::string &url, ADDON::AddonPtr &addon, std::string &addonPath);
+
+protected:
+ explicit CHTTPWebinterfaceHandler(const HTTPRequest &request);
+};
diff --git a/xbmc/network/httprequesthandler/IHTTPRequestHandler.cpp b/xbmc/network/httprequesthandler/IHTTPRequestHandler.cpp
new file mode 100644
index 0000000..3a06604
--- /dev/null
+++ b/xbmc/network/httprequesthandler/IHTTPRequestHandler.cpp
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2011-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 "IHTTPRequestHandler.h"
+
+#include "network/WebServer.h"
+#include "network/httprequesthandler/HTTPRequestHandlerUtils.h"
+#include "utils/StringUtils.h"
+
+#include <limits>
+#include <utility>
+
+static const std::string HTTPMethodHead = "HEAD";
+static const std::string HTTPMethodGet = "GET";
+static const std::string HTTPMethodPost = "POST";
+
+HTTPMethod GetHTTPMethod(const char *method)
+{
+ if (HTTPMethodGet.compare(method) == 0)
+ return GET;
+ if (HTTPMethodPost.compare(method) == 0)
+ return POST;
+ if (HTTPMethodHead.compare(method) == 0)
+ return HEAD;
+
+ return UNKNOWN;
+}
+
+std::string GetHTTPMethod(HTTPMethod method)
+{
+ switch (method)
+ {
+ case HEAD:
+ return HTTPMethodHead;
+
+ case GET:
+ return HTTPMethodGet;
+
+ case POST:
+ return HTTPMethodPost;
+
+ case UNKNOWN:
+ break;
+ }
+
+ return "";
+}
+
+IHTTPRequestHandler::IHTTPRequestHandler()
+ : m_request(),
+ m_response(),
+ m_postFields()
+{ }
+
+IHTTPRequestHandler::IHTTPRequestHandler(const HTTPRequest &request)
+ : m_request(request),
+ m_response(),
+ m_postFields()
+{
+ m_response.type = HTTPError;
+ m_response.status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ m_response.totalLength = 0;
+}
+
+bool IHTTPRequestHandler::HasResponseHeader(const std::string &field) const
+{
+ if (field.empty())
+ return false;
+
+ return m_response.headers.find(field) != m_response.headers.end();
+}
+
+bool IHTTPRequestHandler::AddResponseHeader(const std::string &field, const std::string &value, bool allowMultiple /* = false */)
+{
+ if (field.empty() || value.empty())
+ return false;
+
+ if (!allowMultiple && HasResponseHeader(field))
+ return false;
+
+ m_response.headers.insert(std::make_pair(field, value));
+ return true;
+}
+
+void IHTTPRequestHandler::AddPostField(const std::string &key, const std::string &value)
+{
+ if (key.empty())
+ return;
+
+ std::map<std::string, std::string>::iterator field = m_postFields.find(key);
+ if (field == m_postFields.end())
+ m_postFields[key] = value;
+ else
+ m_postFields[key].append(value);
+}
+
+bool IHTTPRequestHandler::AddPostData(const char *data, size_t size)
+{
+ if (size > 0)
+ return appendPostData(data, size);
+
+ return true;
+}
+
+bool IHTTPRequestHandler::GetRequestedRanges(uint64_t totalLength)
+{
+ if (!m_ranged || m_request.webserver == NULL || m_request.connection == NULL)
+ return false;
+
+ m_request.ranges.Clear();
+ if (totalLength == 0)
+ return true;
+
+ return HTTPRequestHandlerUtils::GetRequestedRanges(m_request.connection, totalLength, m_request.ranges);
+}
+
+bool IHTTPRequestHandler::GetHostnameAndPort(std::string& hostname, uint16_t &port)
+{
+ if (m_request.webserver == NULL || m_request.connection == NULL)
+ return false;
+
+ std::string hostnameAndPort = HTTPRequestHandlerUtils::GetRequestHeaderValue(m_request.connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_HOST);
+ if (hostnameAndPort.empty())
+ return false;
+
+ size_t pos = hostnameAndPort.find(':');
+ hostname = hostnameAndPort.substr(0, pos);
+ if (hostname.empty())
+ return false;
+
+ if (pos != std::string::npos)
+ {
+ std::string strPort = hostnameAndPort.substr(pos + 1);
+ if (!StringUtils::IsNaturalNumber(strPort))
+ return false;
+
+ unsigned long portL = strtoul(strPort.c_str(), NULL, 0);
+ if (portL > std::numeric_limits<uint16_t>::max())
+ return false;
+
+ port = static_cast<uint16_t>(portL);
+ }
+ else
+ port = 80;
+
+ return true;
+}
diff --git a/xbmc/network/httprequesthandler/IHTTPRequestHandler.h b/xbmc/network/httprequesthandler/IHTTPRequestHandler.h
new file mode 100644
index 0000000..13c170f
--- /dev/null
+++ b/xbmc/network/httprequesthandler/IHTTPRequestHandler.h
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2011-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 "utils/HttpRangeUtils.h"
+
+#include <map>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <string>
+
+#include <microhttpd.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#if MHD_VERSION >= 0x00097002
+using MHD_RESULT = MHD_Result;
+#else
+using MHD_RESULT = int;
+#endif
+
+class CDateTime;
+class CWebServer;
+
+enum HTTPMethod
+{
+ UNKNOWN,
+ POST,
+ GET,
+ HEAD
+};
+
+HTTPMethod GetHTTPMethod(const char *method);
+std::string GetHTTPMethod(HTTPMethod method);
+
+typedef enum HTTPResponseType
+{
+ HTTPNone,
+ // creates and returns a HTTP error
+ HTTPError,
+ // creates and returns a HTTP redirect response
+ HTTPRedirect,
+ // creates a HTTP response with the content from a file
+ HTTPFileDownload,
+ // creates a HTTP response from a buffer without copying or freeing the buffer
+ HTTPMemoryDownloadNoFreeNoCopy,
+ // creates a HTTP response from a buffer by copying but not freeing the buffer
+ HTTPMemoryDownloadNoFreeCopy,
+ // creates a HTTP response from a buffer without copying followed by freeing the buffer
+ // the buffer must have been malloc'ed and not new'ed
+ HTTPMemoryDownloadFreeNoCopy,
+ // creates a HTTP response from a buffer by copying followed by freeing the buffer
+ // the buffer must have been malloc'ed and not new'ed
+ HTTPMemoryDownloadFreeCopy
+} HTTPResponseType;
+
+typedef struct HTTPRequest
+{
+ CWebServer *webserver;
+ struct MHD_Connection *connection;
+ std::string pathUrlFull;
+ std::string pathUrl;
+ HTTPMethod method;
+ std::string version;
+ CHttpRanges ranges;
+} HTTPRequest;
+
+typedef struct HTTPResponseDetails {
+ HTTPResponseType type;
+ int status;
+ std::multimap<std::string, std::string> headers;
+ std::string contentType;
+ uint64_t totalLength;
+} HTTPResponseDetails;
+
+class IHTTPRequestHandler
+{
+public:
+ virtual ~IHTTPRequestHandler() = default;
+
+ /*!
+ * \brief Creates a new HTTP request handler for the given request.
+ *
+ * \details This call is responsible for doing some preparation work like -
+ * depending on the supported features - determining whether the requested
+ * entity supports ranges, whether it can be cached and what it's last
+ * modified date is.
+ *
+ * \param request HTTP request to be handled
+ */
+ virtual IHTTPRequestHandler* Create(const HTTPRequest &request) const = 0;
+
+ /*!
+ * \brief Returns the priority of the HTTP request handler.
+ *
+ * \details The higher the priority the more important is the HTTP request
+ * handler.
+ */
+ virtual int GetPriority() const { return 0; }
+
+ /*!
+ * \brief Checks if the HTTP request handler can handle the given request.
+ *
+ * \param request HTTP request to be handled
+ * \return True if the given HTTP request can be handled otherwise false.
+ */
+ virtual bool CanHandleRequest(const HTTPRequest &request) const = 0;
+
+ /*!
+ * \brief Handles the HTTP request.
+ *
+ * \return MHD_NO if a severe error has occurred otherwise MHD_YES.
+ */
+ virtual MHD_RESULT HandleRequest() = 0;
+
+ /*!
+ * \brief Whether the HTTP response could also be provided in ranges.
+ */
+ virtual bool CanHandleRanges() const { return false; }
+
+ /*!
+ * \brief Whether the HTTP response can be cached.
+ */
+ virtual bool CanBeCached() const { return false; }
+
+ /*!
+ * \brief Returns the maximum age (in seconds) for which the response can be cached.
+ *
+ * \details This is only used if the response can be cached.
+ */
+ virtual int GetMaximumAgeForCaching() const { return 0; }
+
+ /*!
+ * \brief Returns the last modification date of the response data.
+ *
+ * \details This is only used if the response can be cached.
+ */
+ virtual bool GetLastModifiedDate(CDateTime &lastModified) const { return false; }
+
+ /*!
+ * \brief Returns the ranges with raw data belonging to the response.
+ *
+ * \details This is only used if the response type is one of the HTTPMemoryDownload types.
+ */
+ virtual HttpResponseRanges GetResponseData() const { return HttpResponseRanges(); }
+
+ /*!
+ * \brief Returns the URL to which the request should be redirected.
+ *
+ * \details This is only used if the response type is HTTPRedirect.
+ */
+ virtual std::string GetRedirectUrl() const { return ""; }
+
+ /*!
+ * \brief Returns the path to the file that should be returned as the response.
+ *
+ * \details This is only used if the response type is HTTPFileDownload.
+ */
+ virtual std::string GetResponseFile() const { return ""; }
+
+ /*!
+ * \brief Returns the HTTP request handled by the HTTP request handler.
+ */
+ const HTTPRequest& GetRequest() const { return m_request; }
+
+ /*!
+ * \brief Returns true if the HTTP request is ranged, otherwise false.
+ */
+ bool IsRequestRanged() const { return m_ranged; }
+
+ /*!
+ * \brief Sets whether the HTTP request contains ranges or not
+ */
+ void SetRequestRanged(bool ranged) { m_ranged = ranged; }
+
+ /*!
+ * \brief Sets the response status of the HTTP response.
+ *
+ * \param status HTTP status of the response
+ */
+ void SetResponseStatus(int status) { m_response.status = status; }
+
+ /*!
+ * \brief Checks if the given HTTP header field is part of the response details.
+ *
+ * \param field HTTP header field name
+ * \return True if the header field is set, otherwise false.
+ */
+ bool HasResponseHeader(const std::string &field) const;
+
+ /*!
+ * \brief Adds the given HTTP header field and value to the response details.
+ *
+ * \param field HTTP header field name
+ * \param value HTTP header field value
+ * \param allowMultiple Whether the same header is allowed multiple times
+ * \return True if the header field was added, otherwise false.
+ */
+ bool AddResponseHeader(const std::string &field, const std::string &value, bool allowMultiple = false);
+
+ /*!
+ * \brief Returns the HTTP response header details.
+ */
+ const HTTPResponseDetails& GetResponseDetails() const { return m_response; }
+
+ /*!
+ * \brief Adds the given key-value pair extracted from the HTTP POST data.
+ *
+ * \param key Key of the HTTP POST field
+ * \param value Value of the HTTP POST field
+ */
+ void AddPostField(const std::string &key, const std::string &value);
+ /*!
+ * \brief Adds the given raw HTTP POST data.
+ *
+ * \param data Raw HTTP POST data
+ * \param size Size of the raw HTTP POST data
+ */
+ bool AddPostData(const char *data, size_t size);
+
+protected:
+ IHTTPRequestHandler();
+ explicit IHTTPRequestHandler(const HTTPRequest &request);
+
+ virtual bool appendPostData(const char *data, size_t size)
+ { return true; }
+
+ bool GetRequestedRanges(uint64_t totalLength);
+ bool GetHostnameAndPort(std::string& hostname, uint16_t &port);
+
+ HTTPRequest m_request;
+ HTTPResponseDetails m_response;
+
+ std::map<std::string, std::string> m_postFields;
+
+private:
+ bool m_ranged = false;
+};
diff --git a/xbmc/network/httprequesthandler/python/CMakeLists.txt b/xbmc/network/httprequesthandler/python/CMakeLists.txt
new file mode 100644
index 0000000..7bbbad9
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/CMakeLists.txt
@@ -0,0 +1,10 @@
+if(MICROHTTPD_FOUND AND PYTHON_FOUND)
+ set(SOURCES HTTPPythonInvoker.cpp
+ HTTPPythonWsgiInvoker.cpp)
+
+ set(HEADERS HTTPPythonInvoker.h
+ HTTPPythonRequest.h
+ HTTPPythonWsgiInvoker.h)
+
+ core_add_library(network_httprequesthandlers_python)
+endif()
diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.cpp b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.cpp
new file mode 100644
index 0000000..696c039
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.cpp
@@ -0,0 +1,73 @@
+/*
+ * 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 "HTTPPythonInvoker.h"
+
+#include "CompileInfo.h"
+#include "utils/StringUtils.h"
+
+CHTTPPythonInvoker::CHTTPPythonInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request)
+ : CPythonInvoker(invocationHandler),
+ m_request(request),
+ m_internalError(false)
+{ }
+
+CHTTPPythonInvoker::~CHTTPPythonInvoker()
+{
+ delete m_request;
+ m_request = NULL;
+}
+
+void CHTTPPythonInvoker::onAbort()
+{
+ if (m_request == NULL)
+ return;
+
+ m_internalError = true;
+ m_request->responseType = HTTPError;
+ m_request->responseStatus = MHD_HTTP_INTERNAL_SERVER_ERROR;
+}
+
+void CHTTPPythonInvoker::onError(const std::string& exceptionType /* = "" */, const std::string& exceptionValue /* = "" */, const std::string& exceptionTraceback /* = "" */)
+{
+ if (m_request == NULL)
+ return;
+
+ m_internalError = true;
+ m_request->responseType = HTTPMemoryDownloadNoFreeCopy;
+ m_request->responseStatus = MHD_HTTP_INTERNAL_SERVER_ERROR;
+
+ std::string output;
+ if (!exceptionType.empty())
+ {
+ output += exceptionType;
+
+ if (!exceptionValue.empty())
+ output += ": " + exceptionValue;
+ output += "\n";
+ }
+
+ if (!exceptionTraceback .empty())
+ output += exceptionTraceback;
+
+ // replace all special characters
+
+ StringUtils::Replace(output, "<", "&lt;");
+ StringUtils::Replace(output, ">", "&gt;");
+ StringUtils::Replace(output, " ", "&nbsp;");
+ StringUtils::Replace(output, "\n", "\n<br />");
+
+ if (!exceptionType.empty())
+ {
+ // now make the type and value bold (needs to be done here because otherwise the < and > would have been replaced
+ output = "<b>" + output;
+ output.insert(output.find('\n'), "</b>");
+ }
+
+ m_request->responseData = "<html><head><title>" + std::string(CCompileInfo::GetAppName()) + ": python error</title></head><body>" + output + "</body></html>";
+}
diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.h b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.h
new file mode 100644
index 0000000..83f1f34
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/HTTPPythonInvoker.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "interfaces/python/PythonInvoker.h"
+#include "network/httprequesthandler/python/HTTPPythonRequest.h"
+
+#include <string>
+
+class CHTTPPythonInvoker : public CPythonInvoker
+{
+public:
+ ~CHTTPPythonInvoker() override;
+
+ virtual HTTPPythonRequest* GetRequest() = 0;
+
+protected:
+ CHTTPPythonInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request);
+
+ // overrides of CPythonInvoker
+ void onAbort() override;
+ void onError(const std::string& exceptionType = "", const std::string& exceptionValue = "", const std::string& exceptionTraceback = "") override;
+
+ HTTPPythonRequest* m_request;
+ bool m_internalError;
+};
diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonRequest.h b/xbmc/network/httprequesthandler/python/HTTPPythonRequest.h
new file mode 100644
index 0000000..7874203
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/HTTPPythonRequest.h
@@ -0,0 +1,42 @@
+/*
+ * 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 "XBDateTime.h"
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+
+#include <map>
+#include <stdint.h>
+#include <string>
+
+typedef struct HTTPPythonRequest
+{
+ struct MHD_Connection *connection;
+ std::string hostname;
+ uint16_t port;
+ std::string url;
+ std::string path;
+ std::string file;
+ HTTPMethod method;
+ std::string version;
+ std::multimap<std::string, std::string> headerValues;
+ std::map<std::string, std::string> getValues;
+ std::map<std::string, std::string> postValues;
+ std::string requestContent;
+ CDateTime requestTime;
+ CDateTime lastModifiedTime;
+
+ HTTPResponseType responseType;
+ int responseStatus;
+ std::string responseContentType;
+ std::string responseData;
+ size_t responseLength;
+ std::multimap<std::string, std::string> responseHeaders;
+ std::multimap<std::string, std::string> responseHeadersError;
+} HTTPPythonRequest;
diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.cpp b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.cpp
new file mode 100644
index 0000000..ac047a7
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.cpp
@@ -0,0 +1,458 @@
+/*
+ * 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 "HTTPPythonWsgiInvoker.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/Webinterface.h"
+#include "addons/addoninfo/AddonType.h"
+#include "interfaces/legacy/wsgi/WsgiErrorStream.h"
+#include "interfaces/legacy/wsgi/WsgiInputStream.h"
+#include "interfaces/legacy/wsgi/WsgiResponse.h"
+#include "interfaces/python/swig.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <utility>
+
+#include <Python.h>
+
+#define MODULE "xbmc"
+
+#define RUNSCRIPT_PREAMBLE \
+ "" \
+ "import " MODULE "\n" \
+ "class xbmcout:\n" \
+ " def __init__(self, loglevel=" MODULE ".LOGINFO):\n" \
+ " self.ll=loglevel\n" \
+ " def write(self, data):\n" \
+ " " MODULE ".log(data,self.ll)\n" \
+ " def close(self):\n" \
+ " " MODULE ".log('.')\n" \
+ " def flush(self):\n" \
+ " " MODULE ".log('.')\n" \
+ "import sys\n" \
+ "sys.stdout = xbmcout()\n" \
+ "sys.stderr = xbmcout(" MODULE ".LOGERROR)\n" \
+ ""
+
+#define RUNSCRIPT_SETUPTOOLS_HACK \
+ "" \
+ "import types,sys\n" \
+ "pkg_resources_code = \\\n" \
+ "\"\"\"\n" \
+ "def resource_filename(__name__,__path__):\n" \
+ " return __path__\n" \
+ "\"\"\"\n" \
+ "pkg_resources = types.ModuleType('pkg_resources')\n" \
+ "exec(pkg_resources_code, pkg_resources.__dict__)\n" \
+ "sys.modules['pkg_resources'] = pkg_resources\n" \
+ ""
+
+#define RUNSCRIPT_POSTSCRIPT \
+ MODULE ".log('-->HTTP Python WSGI Interpreter Initialized<--', " MODULE ".LOGINFO)\n" \
+ ""
+
+#if defined(TARGET_ANDROID)
+#define RUNSCRIPT \
+ RUNSCRIPT_PREAMBLE RUNSCRIPT_SETUPTOOLS_HACK RUNSCRIPT_POSTSCRIPT
+#else
+#define RUNSCRIPT \
+ RUNSCRIPT_PREAMBLE RUNSCRIPT_POSTSCRIPT
+#endif
+
+namespace PythonBindings {
+PyObject* PyInit_Module_xbmc(void);
+PyObject* PyInit_Module_xbmcaddon(void);
+PyObject* PyInit_Module_xbmcwsgi(void);
+}
+
+using namespace PythonBindings;
+
+typedef struct
+{
+ const char *name;
+ CPythonInvoker::PythonModuleInitialization initialization;
+} PythonModule;
+
+static PythonModule PythonModules[] =
+{
+ { "xbmc", PyInit_Module_xbmc },
+ { "xbmcaddon", PyInit_Module_xbmcaddon },
+ { "xbmcwsgi", PyInit_Module_xbmcwsgi }
+};
+
+CHTTPPythonWsgiInvoker::CHTTPPythonWsgiInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request)
+ : CHTTPPythonInvoker(invocationHandler, request),
+ m_wsgiResponse(NULL)
+{
+ PyImport_AppendInittab("xbmc", PyInit_Module_xbmc);
+ PyImport_AppendInittab("xbmcaddon", PyInit_Module_xbmcaddon);
+ PyImport_AppendInittab("xbmcwsgi", PyInit_Module_xbmcwsgi);
+}
+
+CHTTPPythonWsgiInvoker::~CHTTPPythonWsgiInvoker()
+{
+ delete m_wsgiResponse;
+ m_wsgiResponse = NULL;
+}
+
+HTTPPythonRequest* CHTTPPythonWsgiInvoker::GetRequest()
+{
+ if (m_request == NULL || m_wsgiResponse == NULL)
+ return NULL;
+
+ if (m_internalError)
+ return m_request;
+
+ m_wsgiResponse->Finalize(m_request);
+ return m_request;
+}
+
+void CHTTPPythonWsgiInvoker::executeScript(FILE* fp, const std::string& script, PyObject* moduleDict)
+{
+ if (m_request == NULL || m_addon == NULL || m_addon->Type() != ADDON::AddonType::WEB_INTERFACE ||
+ fp == NULL || script.empty() || moduleDict == NULL)
+ return;
+
+ auto logger = CServiceBroker::GetLogging().GetLogger(
+ StringUtils::Format("CHTTPPythonWsgiInvoker[{}]", script));
+
+ ADDON::CWebinterface* webinterface = static_cast<ADDON::CWebinterface*>(m_addon.get());
+ if (webinterface->GetType() != ADDON::WebinterfaceTypeWsgi)
+ {
+ logger->error("trying to execute a non-WSGI script");
+ return;
+ }
+
+ PyObject* pyScript = NULL;
+ PyObject* pyModule = NULL;
+ PyObject* pyEntryPoint = NULL;
+ std::map<std::string, std::string> cgiEnvironment;
+ PyObject* pyEnviron = NULL;
+ PyObject* pyStart_response = NULL;
+ PyObject* pyArgs = NULL;
+ PyObject* pyResult = NULL;
+ PyObject* pyResultIterator = NULL;
+ PyObject* pyIterResult = NULL;
+
+ // get the script
+ std::string scriptName = URIUtils::GetFileName(script);
+ URIUtils::RemoveExtension(scriptName);
+ pyScript = PyUnicode_FromStringAndSize(scriptName.c_str(), scriptName.size());
+ if (pyScript == NULL)
+ {
+ logger->error("failed to convert script to python string");
+ return;
+ }
+
+ // load the script
+ logger->debug("loading script");
+ pyModule = PyImport_Import(pyScript);
+ Py_DECREF(pyScript);
+ if (pyModule == NULL)
+ {
+ logger->error("failed to load WSGI script");
+ return;
+ }
+
+ // get the entry point
+ const std::string& entryPoint = webinterface->EntryPoint();
+ logger->debug(R"(loading entry point "{}")", entryPoint);
+ pyEntryPoint = PyObject_GetAttrString(pyModule, entryPoint.c_str());
+ if (pyEntryPoint == NULL)
+ {
+ logger->error(R"(failed to load entry point "{}")", entryPoint);
+ goto cleanup;
+ }
+
+ // check if the loaded entry point is a callable function
+ if (!PyCallable_Check(pyEntryPoint))
+ {
+ logger->error(R"(defined entry point "{}" is not callable)", entryPoint);
+ goto cleanup;
+ }
+
+ // prepare the WsgiResponse object
+ m_wsgiResponse = new XBMCAddon::xbmcwsgi::WsgiResponse();
+ if (m_wsgiResponse == NULL)
+ {
+ logger->error("failed to create WsgiResponse object");
+ goto cleanup;
+ }
+
+ try
+ {
+ // prepare the start_response callable
+ pyStart_response = PythonBindings::makePythonInstance(m_wsgiResponse, true);
+
+ // create the (CGI) environment dictionary
+ cgiEnvironment = createCgiEnvironment(m_request, m_addon);
+ // and turn it into a python dictionary
+ pyEnviron = PyDict_New();
+ for (const auto& cgiEnv : cgiEnvironment)
+ {
+ PyObject* pyEnvEntry = PyUnicode_FromStringAndSize(cgiEnv.second.c_str(), cgiEnv.second.size());
+ PyDict_SetItemString(pyEnviron, cgiEnv.first.c_str(), pyEnvEntry);
+ Py_DECREF(pyEnvEntry);
+ }
+
+ // add the WSGI-specific environment variables
+ addWsgiEnvironment(m_request, pyEnviron);
+ }
+ catch (const XBMCAddon::WrongTypeException& e)
+ {
+ logger->error("failed to prepare WsgiResponse object with wrong type exception: {}",
+ e.GetExMessage());
+ goto cleanup;
+ }
+ catch (const XbmcCommons::Exception& e)
+ {
+ logger->error("failed to prepare WsgiResponse object with exception: {}", e.GetExMessage());
+ goto cleanup;
+ }
+ catch (...)
+ {
+ logger->error("failed to prepare WsgiResponse object with unknown exception");
+ goto cleanup;
+ }
+
+ // put together the arguments
+ pyArgs = PyTuple_Pack(2, pyEnviron, pyStart_response);
+ Py_DECREF(pyEnviron);
+ Py_DECREF(pyStart_response);
+
+ // call the given handler with the prepared arguments
+ pyResult = PyObject_CallObject(pyEntryPoint, pyArgs);
+ Py_DECREF(pyArgs);
+ if (pyResult == NULL)
+ {
+ logger->error("no result");
+ goto cleanup;
+ }
+
+ // try to get an iterator from the result object
+ pyResultIterator = PyObject_GetIter(pyResult);
+ if (pyResultIterator == NULL || !PyIter_Check(pyResultIterator))
+ {
+ logger->error("result is not iterable");
+ goto cleanup;
+ }
+
+ // go through all the iterables in the result and turn them into strings
+ while ((pyIterResult = PyIter_Next(pyResultIterator)) != NULL)
+ {
+ std::string result;
+ try
+ {
+ PythonBindings::PyXBMCGetUnicodeString(result, pyIterResult, false, "result", "handle_request");
+ }
+ catch (const XBMCAddon::WrongTypeException& e)
+ {
+ logger->error("failed to parse result iterable object with wrong type exception: {}",
+ e.GetExMessage());
+ goto cleanup;
+ }
+ catch (const XbmcCommons::Exception& e)
+ {
+ logger->error("failed to parse result iterable object with exception: {}", e.GetExMessage());
+ goto cleanup;
+ }
+ catch (...)
+ {
+ logger->error("failed to parse result iterable object with unknown exception");
+ goto cleanup;
+ }
+
+ // append the result string to the response
+ m_wsgiResponse->Append(result);
+ }
+
+cleanup:
+ if (pyIterResult != NULL)
+ {
+ Py_DECREF(pyIterResult);
+ }
+ if (pyResultIterator != NULL)
+ {
+ // Call optional close method on iterator
+ if (PyObject_HasAttrString(pyResultIterator, "close") == 1)
+ {
+ if (PyObject_CallMethod(pyResultIterator, "close", NULL) == NULL)
+ logger->error("failed to close iterator object");
+ }
+ Py_DECREF(pyResultIterator);
+ }
+ if (pyResult != NULL)
+ {
+ Py_DECREF(pyResult);
+ }
+ if (pyEntryPoint != NULL)
+ {
+ Py_DECREF(pyEntryPoint);
+ }
+ if (pyModule != NULL)
+ {
+ Py_DECREF(pyModule);
+ }
+}
+
+std::map<std::string, CPythonInvoker::PythonModuleInitialization> CHTTPPythonWsgiInvoker::getModules() const
+{
+ static std::map<std::string, PythonModuleInitialization> modules;
+ if (modules.empty())
+ {
+ for (const PythonModule& pythonModule : PythonModules)
+ modules.insert(std::make_pair(pythonModule.name, pythonModule.initialization));
+ }
+
+ return modules;
+}
+
+const char* CHTTPPythonWsgiInvoker::getInitializationScript() const
+{
+ return RUNSCRIPT;
+}
+
+std::map<std::string, std::string> CHTTPPythonWsgiInvoker::createCgiEnvironment(
+ const HTTPPythonRequest* httpRequest, const ADDON::AddonPtr& addon)
+{
+ std::map<std::string, std::string> environment;
+
+ // REQUEST_METHOD
+ std::string requestMethod;
+ switch (httpRequest->method)
+ {
+ case HEAD:
+ requestMethod = "HEAD";
+ break;
+
+ case POST:
+ requestMethod = "POST";
+ break;
+
+ case GET:
+ default:
+ requestMethod = "GET";
+ break;
+ }
+ environment.insert(std::make_pair("REQUEST_METHOD", requestMethod));
+
+ // SCRIPT_NAME
+ std::string scriptName = std::dynamic_pointer_cast<ADDON::CWebinterface>(addon)->GetBaseLocation();
+ environment.insert(std::make_pair("SCRIPT_NAME", scriptName));
+
+ // PATH_INFO
+ std::string pathInfo = httpRequest->path.substr(scriptName.size());
+ environment.insert(std::make_pair("PATH_INFO", pathInfo));
+
+ // QUERY_STRING
+ size_t iOptions = httpRequest->url.find_first_of('?');
+ if (iOptions != std::string::npos)
+ environment.insert(std::make_pair("QUERY_STRING", httpRequest->url.substr(iOptions+1)));
+ else
+ environment.insert(std::make_pair("QUERY_STRING", ""));
+
+ // CONTENT_TYPE
+ std::string headerValue;
+ std::multimap<std::string, std::string>::const_iterator headerIt = httpRequest->headerValues.find(MHD_HTTP_HEADER_CONTENT_TYPE);
+ if (headerIt != httpRequest->headerValues.end())
+ headerValue = headerIt->second;
+ environment.insert(std::make_pair("CONTENT_TYPE", headerValue));
+
+ // CONTENT_LENGTH
+ headerValue.clear();
+ headerIt = httpRequest->headerValues.find(MHD_HTTP_HEADER_CONTENT_LENGTH);
+ if (headerIt != httpRequest->headerValues.end())
+ headerValue = headerIt->second;
+ environment.insert(std::make_pair("CONTENT_LENGTH", headerValue));
+
+ // SERVER_NAME
+ environment.insert(std::make_pair("SERVER_NAME", httpRequest->hostname));
+
+ // SERVER_PORT
+ environment.insert(std::make_pair("SERVER_PORT", std::to_string(httpRequest->port)));
+
+ // SERVER_PROTOCOL
+ environment.insert(std::make_pair("SERVER_PROTOCOL", httpRequest->version));
+
+ // HTTP_<HEADER_NAME>
+ for (headerIt = httpRequest->headerValues.begin(); headerIt != httpRequest->headerValues.end(); ++headerIt)
+ {
+ std::string headerName = headerIt->first;
+ StringUtils::ToUpper(headerName);
+ environment.insert(std::make_pair("HTTP_" + headerName, headerIt->second));
+ }
+
+ return environment;
+}
+
+void CHTTPPythonWsgiInvoker::addWsgiEnvironment(HTTPPythonRequest* request, void* environment)
+{
+ if (environment == nullptr)
+ return;
+
+ PyObject* pyEnviron = reinterpret_cast<PyObject*>(environment);
+ if (pyEnviron == nullptr)
+ return;
+
+ // WSGI-defined variables
+ {
+ // wsgi.version
+ PyObject* pyValue = Py_BuildValue("(ii)", 1, 0);
+ PyDict_SetItemString(pyEnviron, "wsgi.version", pyValue);
+ Py_DECREF(pyValue);
+ }
+ {
+ // wsgi.url_scheme
+ PyObject* pyValue = PyUnicode_FromStringAndSize("http", 4);
+ PyDict_SetItemString(pyEnviron, "wsgi.url_scheme", pyValue);
+ Py_DECREF(pyValue);
+ }
+ {
+ // wsgi.input
+ XBMCAddon::xbmcwsgi::WsgiInputStream* wsgiInputStream = new XBMCAddon::xbmcwsgi::WsgiInputStream();
+ if (request != NULL)
+ wsgiInputStream->SetRequest(request);
+
+ PythonBindings::prepareForReturn(wsgiInputStream);
+ PyObject* pyWsgiInputStream = PythonBindings::makePythonInstance(wsgiInputStream, false);
+ PyDict_SetItemString(pyEnviron, "wsgi.input", pyWsgiInputStream);
+ Py_DECREF(pyWsgiInputStream);
+ }
+ {
+ // wsgi.errors
+ XBMCAddon::xbmcwsgi::WsgiErrorStream* wsgiErrorStream = new XBMCAddon::xbmcwsgi::WsgiErrorStream();
+ if (request != NULL)
+ wsgiErrorStream->SetRequest(request);
+
+ PythonBindings::prepareForReturn(wsgiErrorStream);
+ PyObject* pyWsgiErrorStream = PythonBindings::makePythonInstance(wsgiErrorStream, false);
+ PyDict_SetItemString(pyEnviron, "wsgi.errors", pyWsgiErrorStream);
+ Py_DECREF(pyWsgiErrorStream);
+ }
+ {
+ // wsgi.multithread
+ PyObject* pyValue = Py_BuildValue("b", false);
+ PyDict_SetItemString(pyEnviron, "wsgi.multithread", pyValue);
+ Py_DECREF(pyValue);
+ }
+ {
+ // wsgi.multiprocess
+ PyObject* pyValue = Py_BuildValue("b", false);
+ PyDict_SetItemString(pyEnviron, "wsgi.multiprocess", pyValue);
+ Py_DECREF(pyValue);
+ }
+ {
+ // wsgi.run_once
+ PyObject* pyValue = Py_BuildValue("b", true);
+ PyDict_SetItemString(pyEnviron, "wsgi.run_once", pyValue);
+ Py_DECREF(pyValue);
+ }
+}
diff --git a/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.h b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.h
new file mode 100644
index 0000000..3ec34c0
--- /dev/null
+++ b/xbmc/network/httprequesthandler/python/HTTPPythonWsgiInvoker.h
@@ -0,0 +1,47 @@
+/*
+ * 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 "interfaces/python/PythonInvoker.h"
+#include "network/httprequesthandler/python/HTTPPythonInvoker.h"
+#include "network/httprequesthandler/python/HTTPPythonRequest.h"
+
+#include <map>
+#include <string>
+
+namespace XBMCAddon
+{
+ namespace xbmcwsgi
+ {
+ class WsgiResponse;
+ }
+}
+
+class CHTTPPythonWsgiInvoker : public CHTTPPythonInvoker
+{
+public:
+ CHTTPPythonWsgiInvoker(ILanguageInvocationHandler* invocationHandler, HTTPPythonRequest* request);
+ ~CHTTPPythonWsgiInvoker() override;
+
+ // implementations of CHTTPPythonInvoker
+ HTTPPythonRequest* GetRequest() override;
+
+protected:
+ // overrides of CPythonInvoker
+ void executeScript(FILE* fp, const std::string& script, PyObject* moduleDict) override;
+ std::map<std::string, PythonModuleInitialization> getModules() const override;
+ const char* getInitializationScript() const override;
+
+private:
+ static std::map<std::string, std::string> createCgiEnvironment(
+ const HTTPPythonRequest* httpRequest, const ADDON::AddonPtr& addon);
+ static void addWsgiEnvironment(HTTPPythonRequest* request, void* environment);
+
+ XBMCAddon::xbmcwsgi::WsgiResponse* m_wsgiResponse;
+};
diff --git a/xbmc/network/mdns/CMakeLists.txt b/xbmc/network/mdns/CMakeLists.txt
new file mode 100644
index 0000000..e0b084a
--- /dev/null
+++ b/xbmc/network/mdns/CMakeLists.txt
@@ -0,0 +1,9 @@
+if(MDNS_FOUND)
+ set(SOURCES ZeroconfBrowserMDNS.cpp
+ ZeroconfMDNS.cpp)
+
+ set(HEADERS ZeroconfBrowserMDNS.h
+ ZeroconfMDNS.h)
+
+ core_add_library(network_mdns)
+endif()
diff --git a/xbmc/network/mdns/ZeroconfBrowserMDNS.cpp b/xbmc/network/mdns/ZeroconfBrowserMDNS.cpp
new file mode 100644
index 0000000..c4a1c1e
--- /dev/null
+++ b/xbmc/network/mdns/ZeroconfBrowserMDNS.cpp
@@ -0,0 +1,420 @@
+/*
+ * 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 "ZeroconfBrowserMDNS.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "network/DNSNameCache.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/WIN32Util.h"
+#endif //TARGET_WINDOWS
+
+using namespace std::chrono_literals;
+
+extern HWND g_hWnd;
+
+CZeroconfBrowserMDNS::CZeroconfBrowserMDNS()
+{
+ m_browser = NULL;
+}
+
+CZeroconfBrowserMDNS::~CZeroconfBrowserMDNS()
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ //make sure there are no browsers anymore
+ for (const auto& it : m_service_browsers)
+ doRemoveServiceType(it.first);
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+ WSAAsyncSelect( (SOCKET) DNSServiceRefSockFD( m_browser ), g_hWnd, BONJOUR_BROWSER_EVENT, 0 );
+#elif defined(TARGET_WINDOWS_STORE)
+ // need to modify this code to use WSAEventSelect since WSAAsyncSelect is not supported
+ CLog::Log(LOGDEBUG, "{} is not implemented for TARGET_WINDOWS_STORE", __FUNCTION__);
+#endif //TARGET_WINDOWS
+
+ if (m_browser)
+ DNSServiceRefDeallocate(m_browser);
+ m_browser = NULL;
+}
+
+void DNSSD_API CZeroconfBrowserMDNS::BrowserCallback(DNSServiceRef browser,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *serviceName,
+ const char *regtype,
+ const char *replyDomain,
+ void *context)
+{
+
+ if (errorCode == kDNSServiceErr_NoError)
+ {
+ //get our instance
+ CZeroconfBrowserMDNS* p_this = reinterpret_cast<CZeroconfBrowserMDNS*>(context);
+ //store the service
+ ZeroconfService s(serviceName, regtype, replyDomain);
+
+ if (flags & kDNSServiceFlagsAdd)
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "ZeroconfBrowserMDNS::BrowserCallback found service named: {}, type: {}, domain: {}",
+ s.GetName(), s.GetType(), s.GetDomain());
+ p_this->addDiscoveredService(browser, s);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG,
+ "ZeroconfBrowserMDNS::BrowserCallback service named: {}, type: {}, domain: {} "
+ "disappeared",
+ s.GetName(), s.GetType(), s.GetDomain());
+ p_this->removeDiscoveredService(browser, s);
+ }
+ if(! (flags & kDNSServiceFlagsMoreComing) )
+ {
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ message.SetStringParam("zeroconf://");
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+ CLog::Log(LOGDEBUG, "ZeroconfBrowserMDNS::BrowserCallback sent gui update for path zeroconf://");
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS::BrowserCallback returned (error = {})",
+ (int)errorCode);
+ }
+}
+
+void DNSSD_API CZeroconfBrowserMDNS::GetAddrInfoCallback(DNSServiceRef sdRef,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *hostname,
+ const struct sockaddr *address,
+ uint32_t ttl,
+ void *context
+ )
+{
+
+ if (errorCode)
+ {
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: GetAddrInfoCallback failed with error = {}",
+ (int)errorCode);
+ return;
+ }
+
+ std::string strIP;
+ CZeroconfBrowserMDNS* p_instance = static_cast<CZeroconfBrowserMDNS*> ( context );
+
+ if (address->sa_family == AF_INET)
+ strIP = inet_ntoa(((const struct sockaddr_in *)address)->sin_addr);
+
+ p_instance->m_resolving_service.SetIP(strIP);
+ p_instance->m_addrinfo_event.Set();
+}
+
+void DNSSD_API CZeroconfBrowserMDNS::ResolveCallback(DNSServiceRef sdRef,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *fullname,
+ const char *hosttarget,
+ uint16_t port, /* In network byte order */
+ uint16_t txtLen,
+ const unsigned char *txtRecord,
+ void *context
+ )
+{
+
+ if (errorCode)
+ {
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: ResolveCallback failed with error = {}",
+ (int)errorCode);
+ return;
+ }
+
+ DNSServiceErrorType err;
+ CZeroconfBrowser::ZeroconfService::tTxtRecordMap recordMap;
+ std::string strIP;
+ CZeroconfBrowserMDNS* p_instance = static_cast<CZeroconfBrowserMDNS*> ( context );
+
+ p_instance->m_resolving_service.SetHostname(hosttarget);
+
+ for(uint16_t i = 0; i < TXTRecordGetCount(txtLen, txtRecord); ++i)
+ {
+ char key[256];
+ uint8_t valueLen;
+ const void *value;
+ std::string strvalue;
+ err = TXTRecordGetItemAtIndex(txtLen, txtRecord,i ,sizeof(key) , key, &valueLen, &value);
+ if(err != kDNSServiceErr_NoError)
+ continue;
+
+ if(value != NULL && valueLen > 0)
+ strvalue.append((const char *)value, valueLen);
+
+ recordMap.insert(std::make_pair(key, strvalue));
+ }
+ p_instance->m_resolving_service.SetTxtRecords(recordMap);
+ p_instance->m_resolving_service.SetPort(ntohs(port));
+ p_instance->m_resolved_event.Set();
+}
+
+/// adds the service to list of found services
+void CZeroconfBrowserMDNS::addDiscoveredService(DNSServiceRef browser, CZeroconfBrowser::ZeroconfService const& fcr_service)
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tDiscoveredServicesMap::iterator browserIt = m_discovered_services.find(browser);
+ if(browserIt == m_discovered_services.end())
+ {
+ //first service by this browser
+ browserIt = m_discovered_services.insert(make_pair(browser, std::vector<std::pair<ZeroconfService, unsigned int> >())).first;
+ }
+ //search this service
+ std::vector<std::pair<ZeroconfService, unsigned int> >& services = browserIt->second;
+ std::vector<std::pair<ZeroconfService, unsigned int> >::iterator serviceIt = services.begin();
+ for( ; serviceIt != services.end(); ++serviceIt)
+ {
+ if(serviceIt->first == fcr_service)
+ break;
+ }
+ if(serviceIt == services.end())
+ services.push_back(std::make_pair(fcr_service, 1));
+ else
+ ++serviceIt->second;
+}
+
+void CZeroconfBrowserMDNS::removeDiscoveredService(DNSServiceRef browser, CZeroconfBrowser::ZeroconfService const& fcr_service)
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tDiscoveredServicesMap::iterator browserIt = m_discovered_services.find(browser);
+ //search this service
+ std::vector<std::pair<ZeroconfService, unsigned int> >& services = browserIt->second;
+ std::vector<std::pair<ZeroconfService, unsigned int> >::iterator serviceIt = services.begin();
+ for( ; serviceIt != services.end(); ++serviceIt)
+ if(serviceIt->first == fcr_service)
+ break;
+ if(serviceIt != services.end())
+ {
+ //decrease refCount
+ --serviceIt->second;
+ if(!serviceIt->second)
+ {
+ //eventually remove the service
+ services.erase(serviceIt);
+ }
+ } else
+ {
+ //looks like we missed the announce, no problem though..
+ }
+}
+
+
+bool CZeroconfBrowserMDNS::doAddServiceType(const std::string& fcr_service_type)
+{
+ DNSServiceErrorType err;
+ DNSServiceRef browser = NULL;
+
+#if !defined(HAS_MDNS_EMBEDDED)
+ if(m_browser == NULL)
+ {
+ err = DNSServiceCreateConnection(&m_browser);
+ if (err != kDNSServiceErr_NoError)
+ {
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: DNSServiceCreateConnection failed with error = {}",
+ (int)err);
+ return false;
+ }
+#if defined(TARGET_WINDOWS_DESKTOP)
+ err = WSAAsyncSelect( (SOCKET) DNSServiceRefSockFD( m_browser ), g_hWnd, BONJOUR_BROWSER_EVENT, FD_READ | FD_CLOSE );
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: WSAAsyncSelect failed with error = {}", (int)err);
+#elif defined(TARGET_WINDOWS_STORE)
+ // need to modify this code to use WSAEventSelect since WSAAsyncSelect is not supported
+ CLog::Log(LOGERROR, "{} is not implemented for TARGET_WINDOWS_STORE", __FUNCTION__);
+#endif // TARGET_WINDOWS_STORE
+ }
+#endif //!HAS_MDNS_EMBEDDED
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ browser = m_browser;
+ err = DNSServiceBrowse(&browser, kDNSServiceFlagsShareConnection, kDNSServiceInterfaceIndexAny, fcr_service_type.c_str(), NULL, BrowserCallback, this);
+ }
+
+ if( err != kDNSServiceErr_NoError )
+ {
+ if (browser)
+ DNSServiceRefDeallocate(browser);
+
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: DNSServiceBrowse returned (error = {})", (int)err);
+ return false;
+ }
+
+ //store the browser
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ m_service_browsers.insert(std::make_pair(fcr_service_type, browser));
+ }
+
+ return true;
+}
+
+bool CZeroconfBrowserMDNS::doRemoveServiceType(const std::string& fcr_service_type)
+{
+ //search for this browser and remove it from the map
+ DNSServiceRef browser = 0;
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tBrowserMap::iterator it = m_service_browsers.find(fcr_service_type);
+ if(it == m_service_browsers.end())
+ {
+ return false;
+ }
+ browser = it->second;
+ m_service_browsers.erase(it);
+ }
+
+ //remove the services of this browser
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tDiscoveredServicesMap::iterator it = m_discovered_services.find(browser);
+ if(it != m_discovered_services.end())
+ m_discovered_services.erase(it);
+ }
+
+ if (browser)
+ DNSServiceRefDeallocate(browser);
+
+ return true;
+}
+
+std::vector<CZeroconfBrowser::ZeroconfService> CZeroconfBrowserMDNS::doGetFoundServices()
+{
+ std::vector<CZeroconfBrowser::ZeroconfService> ret;
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ for (const auto& it : m_discovered_services)
+ {
+ auto& services = it.second;
+ for(unsigned int i = 0; i < services.size(); ++i)
+ {
+ ret.push_back(services[i].first);
+ }
+ }
+ return ret;
+}
+
+bool CZeroconfBrowserMDNS::doResolveService(CZeroconfBrowser::ZeroconfService& fr_service, double f_timeout)
+{
+ DNSServiceErrorType err;
+ DNSServiceRef sdRef = NULL;
+
+ //start resolving
+ m_resolving_service = fr_service;
+ m_resolved_event.Reset();
+
+ err = DNSServiceResolve(&sdRef, 0, kDNSServiceInterfaceIndexAny, fr_service.GetName().c_str(), fr_service.GetType().c_str(), fr_service.GetDomain().c_str(), ResolveCallback, this);
+
+ if( err != kDNSServiceErr_NoError )
+ {
+ if (sdRef)
+ DNSServiceRefDeallocate(sdRef);
+
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: DNSServiceResolve returned (error = {})", (int)err);
+ return false;
+ }
+
+ err = DNSServiceProcessResult(sdRef);
+
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR,
+ "ZeroconfBrowserMDNS::doResolveService DNSServiceProcessResult returned (error = {})",
+ (int)err);
+
+#if defined(HAS_MDNS_EMBEDDED)
+ // when using the embedded mdns service the call to DNSServiceProcessResult
+ // above will not block until the resolving was finished - instead we have to
+ // wait for resolve to return or timeout
+ m_resolved_event.Wait(std::chrono::duration<double, std::milli>(f_timeout * 1000));
+#endif //HAS_MDNS_EMBEDDED
+ fr_service = m_resolving_service;
+
+ if (sdRef)
+ DNSServiceRefDeallocate(sdRef);
+
+ // resolve the hostname
+ if (!fr_service.GetHostname().empty())
+ {
+ std::string strIP;
+
+ // use mdns resolving
+ m_addrinfo_event.Reset();
+ sdRef = NULL;
+
+ err = DNSServiceGetAddrInfo(&sdRef, 0, kDNSServiceInterfaceIndexAny, kDNSServiceProtocol_IPv4, fr_service.GetHostname().c_str(), GetAddrInfoCallback, this);
+
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: DNSServiceGetAddrInfo returned (error = {})",
+ (int)err);
+
+ err = DNSServiceProcessResult(sdRef);
+
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(
+ LOGERROR,
+ "ZeroconfBrowserMDNS::doResolveService DNSServiceProcessResult returned (error = {})",
+ (int)err);
+
+#if defined(HAS_MDNS_EMBEDDED)
+ // when using the embedded mdns service the call to DNSServiceProcessResult
+ // above will not block until the resolving was finished - instead we have to
+ // wait for resolve to return or timeout
+ // give it 2 secs for resolving (resolving in mdns is cached and queued
+ // in timeslices off 1 sec
+ m_addrinfo_event.Wait(2000ms);
+#endif //HAS_MDNS_EMBEDDED
+ fr_service = m_resolving_service;
+
+ if (sdRef)
+ DNSServiceRefDeallocate(sdRef);
+
+ // fall back to our resolver
+ if (fr_service.GetIP().empty())
+ {
+ CLog::Log(LOGWARNING,
+ "ZeroconfBrowserMDNS: Could not resolve hostname {} falling back to CDNSNameCache",
+ fr_service.GetHostname());
+ if (CDNSNameCache::Lookup(fr_service.GetHostname(), strIP))
+ fr_service.SetIP(strIP);
+ else
+ CLog::Log(LOGERROR, "ZeroconfBrowserMDNS: Could not resolve hostname {}",
+ fr_service.GetHostname());
+ }
+ }
+
+ return (!fr_service.GetIP().empty());
+}
+
+void CZeroconfBrowserMDNS::ProcessResults()
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ DNSServiceErrorType err = DNSServiceProcessResult(m_browser);
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR, "ZeroconfWIN: DNSServiceProcessResult returned (error = {})", (int)err);
+}
diff --git a/xbmc/network/mdns/ZeroconfBrowserMDNS.h b/xbmc/network/mdns/ZeroconfBrowserMDNS.h
new file mode 100644
index 0000000..4ef3aac
--- /dev/null
+++ b/xbmc/network/mdns/ZeroconfBrowserMDNS.h
@@ -0,0 +1,92 @@
+/*
+ * 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 "network/ZeroconfBrowser.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <map>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include <dns_sd.h>
+
+//platform specific implementation of zeroconfbrowser interface using native os x APIs
+class CZeroconfBrowserMDNS : public CZeroconfBrowser
+{
+public:
+ CZeroconfBrowserMDNS();
+ ~CZeroconfBrowserMDNS();
+
+private:
+ ///implementation if CZeroconfBrowser interface
+ ///@{
+ virtual bool doAddServiceType(const std::string& fcr_service_type);
+ virtual bool doRemoveServiceType(const std::string& fcr_service_type);
+
+ virtual std::vector<CZeroconfBrowser::ZeroconfService> doGetFoundServices();
+ virtual bool doResolveService(CZeroconfBrowser::ZeroconfService& fr_service, double f_timeout);
+ ///@}
+
+ /// browser callback
+ static void DNSSD_API BrowserCallback(DNSServiceRef browser,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *serviceName,
+ const char *regtype,
+ const char *replyDomain,
+ void *context);
+ /// GetAddrInfo callback
+ static void DNSSD_API GetAddrInfoCallback(DNSServiceRef sdRef,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *hostname,
+ const struct sockaddr *address,
+ uint32_t ttl,
+ void *context
+ );
+
+ /// resolve callback
+ static void DNSSD_API ResolveCallback(DNSServiceRef sdRef,
+ DNSServiceFlags flags,
+ uint32_t interfaceIndex,
+ DNSServiceErrorType errorCode,
+ const char *fullname,
+ const char *hosttarget,
+ uint16_t port, /* In network byte order */
+ uint16_t txtLen,
+ const unsigned char *txtRecord,
+ void *context
+ );
+
+ /// adds the service to list of found services
+ void addDiscoveredService(DNSServiceRef browser, CZeroconfBrowser::ZeroconfService const& fcr_service);
+ /// removes the service from list of found services
+ void removeDiscoveredService(DNSServiceRef browser, CZeroconfBrowser::ZeroconfService const& fcr_service);
+ // win32: process replies from the bonjour daemon
+ void ProcessResults();
+
+ //shared variables (with guard)
+ CCriticalSection m_data_guard;
+ // tBrowserMap maps service types the corresponding browser
+ typedef std::map<std::string, DNSServiceRef> tBrowserMap;
+ tBrowserMap m_service_browsers;
+ //tDiscoveredServicesMap maps browsers to their discovered services + a ref-count for each service
+ //ref-count is needed, because a service might pop up more than once, if there's more than one network-iface
+ typedef std::map<DNSServiceRef, std::vector<std::pair<ZeroconfService, unsigned int> > > tDiscoveredServicesMap;
+ tDiscoveredServicesMap m_discovered_services;
+ DNSServiceRef m_browser;
+ CZeroconfBrowser::ZeroconfService m_resolving_service;
+ CEvent m_resolved_event;
+ CEvent m_addrinfo_event;
+};
diff --git a/xbmc/network/mdns/ZeroconfMDNS.cpp b/xbmc/network/mdns/ZeroconfMDNS.cpp
new file mode 100644
index 0000000..5c65d9f
--- /dev/null
+++ b/xbmc/network/mdns/ZeroconfMDNS.cpp
@@ -0,0 +1,245 @@
+/*
+ * 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 "ZeroconfMDNS.h"
+
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <sstream>
+#include <string>
+
+#include <arpa/inet.h>
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/WIN32Util.h"
+#endif //TARGET_WINDOWS
+
+#if defined(HAS_MDNS_EMBEDDED)
+#include <mDnsEmbedded.h>
+#endif //HAS_MDNS_EMBEDDED
+
+extern HWND g_hWnd;
+
+void CZeroconfMDNS::Process()
+{
+#if defined(HAS_MDNS_EMBEDDED)
+ CLog::Log(LOGDEBUG, "ZeroconfEmbedded - processing...");
+ struct timeval timeout;
+ timeout.tv_sec = 1;
+ while (( !m_bStop ))
+ embedded_mDNSmainLoop(timeout);
+#endif //HAS_MDNS_EMBEDDED
+
+}
+
+
+CZeroconfMDNS::CZeroconfMDNS() : CThread("ZeroconfEmbedded")
+{
+ m_service = NULL;
+#if defined(HAS_MDNS_EMBEDDED)
+ embedded_mDNSInit();
+ Create();
+#endif //HAS_MDNS_EMBEDDED
+}
+
+CZeroconfMDNS::~CZeroconfMDNS()
+{
+ doStop();
+#if defined(HAS_MDNS_EMBEDDED)
+ StopThread();
+ embedded_mDNSExit();
+#endif //HAS_MDNS_EMBEDDED
+}
+
+bool CZeroconfMDNS::IsZCdaemonRunning()
+{
+#if !defined(HAS_MDNS_EMBEDDED)
+ uint32_t version;
+ uint32_t size = sizeof(version);
+ DNSServiceErrorType err = DNSServiceGetProperty(kDNSServiceProperty_DaemonVersion, &version, &size);
+ if(err != kDNSServiceErr_NoError)
+ {
+ CLog::Log(LOGERROR, "ZeroconfMDNS: Zeroconf can't be started probably because Apple's Bonjour Service isn't installed. You can get it by either installing Itunes or Apple's Bonjour Print Service for Windows (http://support.apple.com/kb/DL999)");
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(34300), g_localizeStrings.Get(34301), 10000, true);
+ return false;
+ }
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS:Bonjour version is {}.{}", version / 10000,
+ version / 100 % 100);
+#endif //!HAS_MDNS_EMBEDDED
+ return true;
+}
+
+//methods to implement for concrete implementations
+bool CZeroconfMDNS::doPublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ const std::vector<std::pair<std::string, std::string> >& txt)
+{
+ DNSServiceRef netService = NULL;
+ TXTRecordRef txtRecord;
+ DNSServiceErrorType err;
+ TXTRecordCreate(&txtRecord, 0, NULL);
+
+#if !defined(HAS_MDNS_EMBEDDED)
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ if(m_service == NULL)
+ {
+ err = DNSServiceCreateConnection(&m_service);
+ if (err != kDNSServiceErr_NoError)
+ {
+ CLog::Log(LOGERROR, "ZeroconfMDNS: DNSServiceCreateConnection failed with error = {}",
+ (int)err);
+ return false;
+ }
+#ifdef TARGET_WINDOWS_STORE
+ CLog::Log(LOGERROR, "ZeroconfMDNS: WSAAsyncSelect not yet supported for TARGET_WINDOWS_STORE");
+#else
+ err = WSAAsyncSelect( (SOCKET) DNSServiceRefSockFD( m_service ), g_hWnd, BONJOUR_EVENT, FD_READ | FD_CLOSE );
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR, "ZeroconfMDNS: WSAAsyncSelect failed with error = {}", (int)err);
+#endif
+ }
+#endif //!HAS_MDNS_EMBEDDED
+
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: identifier: {} type: {} name:{} port:{}", fcr_identifier,
+ fcr_type, fcr_name, f_port);
+
+ //add txt records
+ if(!txt.empty())
+ {
+ for (const auto& it : txt)
+ {
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: key:{}, value:{}", it.first, it.second);
+ uint8_t txtLen = (uint8_t)strlen(it.second.c_str());
+ TXTRecordSetValue(&txtRecord, it.first.c_str(), txtLen, it.second.c_str());
+ }
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ netService = m_service;
+ err = DNSServiceRegister(&netService, kDNSServiceFlagsShareConnection, 0, fcr_name.c_str(), fcr_type.c_str(), NULL, NULL, htons(f_port), TXTRecordGetLength(&txtRecord), TXTRecordGetBytesPtr(&txtRecord), registerCallback, NULL);
+ }
+
+ if (err != kDNSServiceErr_NoError)
+ {
+ // Something went wrong so lets clean up.
+ if (netService)
+ DNSServiceRefDeallocate(netService);
+
+ CLog::Log(LOGERROR, "ZeroconfMDNS: DNSServiceRegister returned (error = {})", (int)err);
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ struct tServiceRef newService;
+ newService.serviceRef = netService;
+ newService.txtRecordRef = txtRecord;
+ newService.updateNumber = 0;
+ m_services.insert(make_pair(fcr_identifier, newService));
+ }
+
+ return err == kDNSServiceErr_NoError;
+}
+
+bool CZeroconfMDNS::doForceReAnnounceService(const std::string& fcr_identifier)
+{
+ bool ret = false;
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tServiceMap::iterator it = m_services.find(fcr_identifier);
+ if(it != m_services.end())
+ {
+ // for force announcing a service with mdns we need
+ // to change a txt record - so we diddle between
+ // even and odd dummy records here
+ if ( (it->second.updateNumber % 2) == 0)
+ TXTRecordSetValue(&it->second.txtRecordRef, "xbmcdummy", strlen("evendummy"), "evendummy");
+ else
+ TXTRecordSetValue(&it->second.txtRecordRef, "xbmcdummy", strlen("odddummy"), "odddummy");
+ it->second.updateNumber++;
+
+ if (DNSServiceUpdateRecord(it->second.serviceRef, NULL, 0, TXTRecordGetLength(&it->second.txtRecordRef), TXTRecordGetBytesPtr(&it->second.txtRecordRef), 0) == kDNSServiceErr_NoError)
+ ret = true;
+ }
+ return ret;
+}
+
+bool CZeroconfMDNS::doRemoveService(const std::string& fcr_ident)
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ tServiceMap::iterator it = m_services.find(fcr_ident);
+ if(it != m_services.end())
+ {
+ DNSServiceRefDeallocate(it->second.serviceRef);
+ TXTRecordDeallocate(&it->second.txtRecordRef);
+ m_services.erase(it);
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: Removed service {}", fcr_ident);
+ return true;
+ }
+ else
+ return false;
+}
+
+void CZeroconfMDNS::doStop()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: Shutdown services");
+ for (auto& it : m_services)
+ {
+ DNSServiceRefDeallocate(it.second.serviceRef);
+ TXTRecordDeallocate(&it.second.txtRecordRef);
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: Removed service {}", it.first);
+ }
+ m_services.clear();
+ }
+ {
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+#if defined(TARGET_WINDOWS_STORE)
+ CLog::Log(LOGERROR, "ZeroconfMDNS: WSAAsyncSelect not yet supported for TARGET_WINDOWS_STORE");
+#else
+ WSAAsyncSelect( (SOCKET) DNSServiceRefSockFD( m_service ), g_hWnd, BONJOUR_EVENT, 0 );
+#endif //TARGET_WINDOWS
+
+ if (m_service)
+ DNSServiceRefDeallocate(m_service);
+ m_service = NULL;
+ }
+}
+
+void DNSSD_API CZeroconfMDNS::registerCallback(DNSServiceRef sdref, const DNSServiceFlags flags, DNSServiceErrorType errorCode, const char *name, const char *regtype, const char *domain, void *context)
+{
+ (void)sdref; // Unused
+ (void)flags; // Unused
+ (void)context; // Unused
+
+ if (errorCode == kDNSServiceErr_NoError)
+ {
+ if (flags & kDNSServiceFlagsAdd)
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: {}.{}{} now registered and active", name, regtype, domain);
+ else
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: {}.{}{} registration removed", name, regtype, domain);
+ }
+ else if (errorCode == kDNSServiceErr_NameConflict)
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: {}.{}{} Name in use, please choose another", name, regtype,
+ domain);
+ else
+ CLog::Log(LOGDEBUG, "ZeroconfMDNS: {}.{}{} error code {}", name, regtype, domain, errorCode);
+}
+
+void CZeroconfMDNS::ProcessResults()
+{
+ std::unique_lock<CCriticalSection> lock(m_data_guard);
+ DNSServiceErrorType err = DNSServiceProcessResult(m_service);
+ if (err != kDNSServiceErr_NoError)
+ CLog::Log(LOGERROR, "ZeroconfMDNS: DNSServiceProcessResult returned (error = {})", (int)err);
+}
+
diff --git a/xbmc/network/mdns/ZeroconfMDNS.h b/xbmc/network/mdns/ZeroconfMDNS.h
new file mode 100644
index 0000000..95959a1
--- /dev/null
+++ b/xbmc/network/mdns/ZeroconfMDNS.h
@@ -0,0 +1,71 @@
+/*
+ * 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 "network/Zeroconf.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <dns_sd.h>
+
+class CZeroconfMDNS : public CZeroconf,public CThread
+{
+public:
+ CZeroconfMDNS();
+ ~CZeroconfMDNS();
+
+protected:
+
+ //CThread interface
+ void Process();
+
+ //implement base CZeroConf interface
+ bool doPublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ const std::vector<std::pair<std::string, std::string> >& txt);
+
+ bool doForceReAnnounceService(const std::string& fcr_identifier);
+ bool doRemoveService(const std::string& fcr_ident);
+
+ virtual void doStop();
+
+ bool IsZCdaemonRunning();
+
+ void ProcessResults();
+
+private:
+
+ static void DNSSD_API registerCallback(DNSServiceRef sdref,
+ const DNSServiceFlags flags,
+ DNSServiceErrorType errorCode,
+ const char *name,
+ const char *regtype,
+ const char *domain,
+ void *context);
+
+
+ //lock + data (accessed from runloop(main thread) + the rest)
+ CCriticalSection m_data_guard;
+ struct tServiceRef
+ {
+ DNSServiceRef serviceRef;
+ TXTRecordRef txtRecordRef;
+ int updateNumber;
+ };
+ typedef std::map<std::string, struct tServiceRef> tServiceMap;
+ tServiceMap m_services;
+ DNSServiceRef m_service;
+};
diff --git a/xbmc/network/test/CMakeLists.txt b/xbmc/network/test/CMakeLists.txt
new file mode 100644
index 0000000..a323d18
--- /dev/null
+++ b/xbmc/network/test/CMakeLists.txt
@@ -0,0 +1,5 @@
+if(MICROHTTPD_FOUND)
+ set(SOURCES TestWebServer.cpp)
+
+ core_add_test_library(network_test)
+endif()
diff --git a/xbmc/network/test/TestWebServer.cpp b/xbmc/network/test/TestWebServer.cpp
new file mode 100644
index 0000000..aa728ec
--- /dev/null
+++ b/xbmc/network/test/TestWebServer.cpp
@@ -0,0 +1,926 @@
+/*
+ * 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.
+ */
+
+#if defined(TARGET_WINDOWS)
+# include <windows.h>
+#endif
+
+#include <errno.h>
+#include <stdlib.h>
+
+#include <gtest/gtest.h>
+#include "URL.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/File.h"
+#include "interfaces/json-rpc/JSONRPC.h"
+#include "network/WebServer.h"
+#include "network/httprequesthandler/HTTPVfsHandler.h"
+#include "network/httprequesthandler/HTTPJsonRpcHandler.h"
+#include "settings/MediaSourceSettings.h"
+#include "test/TestUtils.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+#include <random>
+
+using namespace XFILE;
+
+#define WEBSERVER_HOST "localhost"
+
+#define TEST_URL_JSONRPC "jsonrpc"
+
+#define TEST_FILES_DATA "test"
+#define TEST_FILES_DATA_RANGES "range1;range2;range3"
+#define TEST_FILES_HTML TEST_FILES_DATA ".html"
+#define TEST_FILES_RANGES TEST_FILES_DATA "-ranges.txt"
+
+class TestWebServer : public testing::Test
+{
+protected:
+ TestWebServer()
+ : webserver(),
+ sourcePath(XBMC_REF_FILE_PATH("xbmc/network/test/data/webserver/"))
+ {
+ static uint16_t port;
+ if (port == 0)
+ {
+ std::random_device rd;
+ std::mt19937 mt(rd());
+ std::uniform_int_distribution<uint16_t> dist(49152, 65535);
+ port = dist(mt);
+ }
+ webserverPort = port;
+ baseUrl = StringUtils::Format("http://" WEBSERVER_HOST ":{}", webserverPort);
+ }
+ ~TestWebServer() override = default;
+
+protected:
+ void SetUp() override
+ {
+ SetupMediaSources();
+
+ webserver.Start(webserverPort, "", "");
+ webserver.RegisterRequestHandler(&m_jsonRpcHandler);
+ webserver.RegisterRequestHandler(&m_vfsHandler);
+ }
+
+ void TearDown() override
+ {
+ if (webserver.IsStarted())
+ webserver.Stop();
+
+ webserver.UnregisterRequestHandler(&m_vfsHandler);
+ webserver.UnregisterRequestHandler(&m_jsonRpcHandler);
+
+ TearDownMediaSources();
+ }
+
+ void SetupMediaSources()
+ {
+ CMediaSource source;
+ source.strName = "WebServer Share";
+ source.strPath = sourcePath;
+ source.vecPaths.push_back(sourcePath);
+ source.m_allowSharing = true;
+ source.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ source.m_iLockMode = LOCK_MODE_EVERYONE;
+ source.m_ignore = true;
+
+ CMediaSourceSettings::GetInstance().AddShare("videos", source);
+ }
+
+ void TearDownMediaSources()
+ {
+ CMediaSourceSettings::GetInstance().Clear();
+ }
+
+ std::string GetUrl(const std::string& path)
+ {
+ if (path.empty())
+ return baseUrl;
+
+ return URIUtils::AddFileToFolder(baseUrl, path);
+ }
+
+ std::string GetUrlOfTestFile(const std::string& testFile)
+ {
+ if (testFile.empty())
+ return "";
+
+ std::string path = URIUtils::AddFileToFolder(sourcePath, testFile);
+ path = CURL::Encode(path);
+ path = URIUtils::AddFileToFolder("vfs", path);
+
+ return GetUrl(path);
+ }
+
+ bool GetLastModifiedOfTestFile(const std::string& testFile, CDateTime& lastModified)
+ {
+ CFile file;
+ if (!file.Open(URIUtils::AddFileToFolder(sourcePath, testFile), READ_NO_CACHE))
+ return false;
+
+ struct __stat64 statBuffer;
+ if (file.Stat(&statBuffer) != 0)
+ return false;
+
+ struct tm *time;
+#ifdef HAVE_LOCALTIME_R
+ struct tm result = {};
+ time = localtime_r((time_t*)&statBuffer.st_mtime, &result);
+#else
+ time = localtime((time_t *)&statBuffer.st_mtime);
+#endif
+ if (time == NULL)
+ return false;
+
+ lastModified = *time;
+ return lastModified.IsValid();
+ }
+
+ void CheckHtmlTestFileResponse(const CCurlFile& curl)
+ {
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Content-Type must be "text/html"
+ EXPECT_STREQ("text/html", httpHeader.GetMimeType().c_str());
+ // Must be only one "Content-Length" header
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // Content-Length must be "4"
+ EXPECT_STREQ("4", httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str());
+ // Accept-Ranges must be "bytes"
+ EXPECT_STREQ("bytes", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());
+
+ // check Last-Modified
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_HTML, lastModified));
+ ASSERT_STREQ(lastModified.GetAsRFC1123DateTime().c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_LAST_MODIFIED).c_str());
+
+ // Cache-Control must contain "mag-age=0" and "no-cache"
+ std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
+ EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
+ EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);
+ }
+
+ void CheckRangesTestFileResponse(const CCurlFile& curl, int httpStatus = MHD_HTTP_OK, bool empty = false)
+ {
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Only zero or one "Content-Length" headers
+ ASSERT_GE(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+
+ // check the protocol line for the expected HTTP status
+ std::string httpStatusString = StringUtils::Format(" {} ", httpStatus);
+ std::string protocolLine = httpHeader.GetProtoLine();
+ ASSERT_TRUE(protocolLine.find(httpStatusString) != std::string::npos);
+
+ // Content-Type must be "text/html"
+ EXPECT_STREQ("text/plain", httpHeader.GetMimeType().c_str());
+ // check Content-Length
+ if (!empty)
+ {
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ EXPECT_STREQ("20", httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str());
+ }
+ // Accept-Ranges must be "bytes"
+ EXPECT_STREQ("bytes", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());
+
+ // check Last-Modified
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+ ASSERT_STREQ(lastModified.GetAsRFC1123DateTime().c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_LAST_MODIFIED).c_str());
+
+ // Cache-Control must contain "mag-age=0" and "no-cache"
+ std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
+ EXPECT_TRUE(cacheControl.find("max-age=31536000") != std::string::npos);
+ EXPECT_TRUE(cacheControl.find("public") != std::string::npos);
+ }
+
+ void CheckRangesTestFileResponse(const CCurlFile& curl, const std::string& result, const CHttpRanges& ranges)
+ {
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Only zero or one "Content-Length" headers
+ ASSERT_GE(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+
+ // check the protocol line for the expected HTTP status
+ std::string httpStatusString = StringUtils::Format(" {} ", MHD_HTTP_PARTIAL_CONTENT);
+ std::string protocolLine = httpHeader.GetProtoLine();
+ ASSERT_TRUE(protocolLine.find(httpStatusString) != std::string::npos);
+
+ // Accept-Ranges must be "bytes"
+ EXPECT_STREQ("bytes", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());
+
+ // check Last-Modified
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+ ASSERT_STREQ(lastModified.GetAsRFC1123DateTime().c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_LAST_MODIFIED).c_str());
+
+ // Cache-Control must contain "mag-age=0" and "no-cache"
+ std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
+ EXPECT_TRUE(cacheControl.find("max-age=31536000") != std::string::npos);
+ EXPECT_TRUE(cacheControl.find("public") != std::string::npos);
+
+ // If there's no range Content-Length must be "20"
+ if (ranges.IsEmpty())
+ {
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ EXPECT_STREQ("20", httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str());
+ EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
+ return;
+ }
+
+ // check Content-Range
+ uint64_t firstPosition, lastPosition;
+ ASSERT_TRUE(ranges.GetFirstPosition(firstPosition));
+ ASSERT_TRUE(ranges.GetLastPosition(lastPosition));
+ EXPECT_STREQ(HttpRangeUtils::GenerateContentRangeHeaderValue(firstPosition, lastPosition, 20).c_str(), httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_RANGE).c_str());
+
+ std::string expectedContent = TEST_FILES_DATA_RANGES;
+ const std::string expectedContentType = "text/plain";
+ if (ranges.Size() == 1)
+ {
+ // Content-Type must be "text/html"
+ EXPECT_STREQ(expectedContentType.c_str(), httpHeader.GetMimeType().c_str());
+
+ // check the content
+ CHttpRange firstRange;
+ ASSERT_TRUE(ranges.GetFirst(firstRange));
+ expectedContent = expectedContent.substr(static_cast<size_t>(firstRange.GetFirstPosition()), static_cast<size_t>(firstRange.GetLength()));
+ EXPECT_STREQ(expectedContent.c_str(), result.c_str());
+
+ // and Content-Length
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ EXPECT_STREQ(std::to_string(static_cast<unsigned int>(expectedContent.size())).c_str(),
+ httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_LENGTH).c_str());
+
+ return;
+ }
+
+ // Content-Type contains the multipart boundary
+ const std::string expectedMimeType = "multipart/byteranges";
+ std::string mimeType = httpHeader.GetMimeType();
+ ASSERT_STREQ(expectedMimeType.c_str(), mimeType.c_str());
+
+ std::string contentType = httpHeader.GetValue(MHD_HTTP_HEADER_CONTENT_TYPE);
+ std::string contentTypeStart = expectedMimeType + "; boundary=";
+ // it must start with "multipart/byteranges; boundary=" followed by the boundary
+ ASSERT_EQ(0U, contentType.find(contentTypeStart));
+ ASSERT_GT(contentType.size(), contentTypeStart.size());
+ // extract the boundary
+ std::string multipartBoundary = contentType.substr(contentTypeStart.size());
+ ASSERT_FALSE(multipartBoundary.empty());
+ multipartBoundary = "--" + multipartBoundary;
+
+ ASSERT_EQ(0U, result.find(multipartBoundary));
+ std::vector<std::string> rangeParts = StringUtils::Split(result, multipartBoundary);
+ // the first part is not really a part and is therefore empty (the place before the first boundary)
+ ASSERT_TRUE(rangeParts.front().empty());
+ rangeParts.erase(rangeParts.begin());
+ // the last part is the end of the end multipart boundary
+ ASSERT_STREQ("--", rangeParts.back().c_str());
+ rangeParts.erase(rangeParts.begin() + rangeParts.size() - 1);
+ ASSERT_EQ(ranges.Size(), rangeParts.size());
+
+ for (size_t i = 0; i < rangeParts.size(); ++i)
+ {
+ std::string data = rangeParts.at(i);
+ StringUtils::Trim(data, " \r\n");
+
+ // find the separator between header and data
+ size_t pos = data.find("\r\n\r\n");
+ ASSERT_NE(std::string::npos, pos);
+
+ std::string header = data.substr(0, pos + 4);
+ data = data.substr(pos + 4);
+
+ // get the expected range
+ CHttpRange range;
+ ASSERT_TRUE(ranges.Get(i, range));
+
+ // parse the header of the range part
+ CHttpHeader rangeHeader;
+ rangeHeader.Parse(header);
+
+ // check Content-Type
+ EXPECT_STREQ(expectedContentType.c_str(), rangeHeader.GetMimeType().c_str());
+
+ // parse and check Content-Range
+ std::string contentRangeHeader = rangeHeader.GetValue(MHD_HTTP_HEADER_CONTENT_RANGE);
+ std::vector<std::string> contentRangeHeaderParts = StringUtils::Split(contentRangeHeader, "/");
+ ASSERT_EQ(2U, contentRangeHeaderParts.size());
+
+ // check the length of the range
+ EXPECT_TRUE(StringUtils::IsNaturalNumber(contentRangeHeaderParts.back()));
+ uint64_t contentRangeLength = str2uint64(contentRangeHeaderParts.back());
+ EXPECT_EQ(range.GetLength(), contentRangeLength);
+
+ // remove the leading "bytes " string from the range definition
+ std::string contentRangeDefinition = contentRangeHeaderParts.front();
+ ASSERT_EQ(0U, contentRangeDefinition.find("bytes "));
+ contentRangeDefinition = contentRangeDefinition.substr(6);
+
+ // check the start and end positions of the range
+ std::vector<std::string> contentRangeParts = StringUtils::Split(contentRangeDefinition, "-");
+ ASSERT_EQ(2U, contentRangeParts.size());
+ EXPECT_TRUE(StringUtils::IsNaturalNumber(contentRangeParts.front()));
+ uint64_t contentRangeStart = str2uint64(contentRangeParts.front());
+ EXPECT_EQ(range.GetFirstPosition(), contentRangeStart);
+ EXPECT_TRUE(StringUtils::IsNaturalNumber(contentRangeParts.back()));
+ uint64_t contentRangeEnd = str2uint64(contentRangeParts.back());
+ EXPECT_EQ(range.GetLastPosition(), contentRangeEnd);
+
+ // make sure the length of the content matches the one of the expected range
+ EXPECT_EQ(range.GetLength(), data.size());
+ EXPECT_STREQ(expectedContent.substr(static_cast<size_t>(range.GetFirstPosition()), static_cast<size_t>(range.GetLength())).c_str(), data.c_str());
+ }
+ }
+
+ std::string GenerateRangeHeaderValue(unsigned int start, unsigned int end)
+ {
+ return StringUtils::Format("bytes={}-{}", start, end);
+ }
+
+ CWebServer webserver;
+ CHTTPJsonRpcHandler m_jsonRpcHandler;
+ CHTTPVfsHandler m_vfsHandler;
+ std::string baseUrl;
+ std::string sourcePath;
+ uint16_t webserverPort;
+};
+
+TEST_F(TestWebServer, IsStarted)
+{
+ ASSERT_TRUE(webserver.IsStarted());
+}
+
+TEST_F(TestWebServer, CanGetJsonRpcApiDescriptionWithHttpGet)
+{
+ std::string result;
+ CCurlFile curl;
+ ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC), result));
+ ASSERT_FALSE(result.empty());
+
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Content-Length header must be present
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // Content-Type must be "application/json"
+ EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
+ // Accept-Ranges must be "none"
+ EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());
+
+ // Cache-Control must contain "mag-age=0" and "no-cache"
+ std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
+ EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
+ EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);
+}
+
+TEST_F(TestWebServer, CanReadDataOverJsonRpcWithHttpGet)
+{
+ // initialized JSON-RPC
+ JSONRPC::CJSONRPC::Initialize();
+
+ std::string result;
+ CCurlFile curl;
+ ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC "?request=" + CURL::Encode("{ \"jsonrpc\": \"2.0\", \"method\": \"JSONRPC.Version\", \"id\": 1 }")), result));
+ ASSERT_FALSE(result.empty());
+
+ // parse the JSON-RPC response
+ CVariant resultObj;
+ ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
+ // make sure it's an object
+ ASSERT_TRUE(resultObj.isObject());
+
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Content-Length header must be present
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // Content-Type must be "application/json"
+ EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
+ // Accept-Ranges must be "none"
+ EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());
+
+ // Cache-Control must contain "mag-age=0" and "no-cache"
+ std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
+ EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
+ EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);
+
+ // uninitialize JSON-RPC
+ JSONRPC::CJSONRPC::Cleanup();
+}
+
+TEST_F(TestWebServer, CannotModifyOverJsonRpcWithHttpGet)
+{
+ // initialized JSON-RPC
+ JSONRPC::CJSONRPC::Initialize();
+
+ std::string result;
+ CCurlFile curl;
+ ASSERT_TRUE(curl.Get(GetUrl(TEST_URL_JSONRPC "?request=" + CURL::Encode("{ \"jsonrpc\": \"2.0\", \"method\": \"Input.Left\", \"id\": 1 }")), result));
+ ASSERT_FALSE(result.empty());
+
+ // parse the JSON-RPC response
+ CVariant resultObj;
+ ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
+ // make sure it's an object
+ ASSERT_TRUE(resultObj.isObject());
+ // it must contain the "error" property with the "Bad client permission" error code
+ ASSERT_TRUE(resultObj.isMember("error") && resultObj["error"].isObject());
+ ASSERT_TRUE(resultObj["error"].isMember("code") && resultObj["error"]["code"].isInteger());
+ ASSERT_EQ(JSONRPC::BadPermission, resultObj["error"]["code"].asInteger());
+
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Content-Length header must be present
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // Content-Type must be "application/json"
+ EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
+ // Accept-Ranges must be "none"
+ EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());
+
+ // Cache-Control must contain "mag-age=0" and "no-cache"
+ std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
+ EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
+ EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);
+
+ // uninitialize JSON-RPC
+ JSONRPC::CJSONRPC::Cleanup();
+}
+
+TEST_F(TestWebServer, CanReadDataOverJsonRpcWithHttpPost)
+{
+ // initialized JSON-RPC
+ JSONRPC::CJSONRPC::Initialize();
+
+ std::string result;
+ CCurlFile curl;
+ curl.SetMimeType("application/json");
+ ASSERT_TRUE(curl.Post(GetUrl(TEST_URL_JSONRPC), "{ \"jsonrpc\": \"2.0\", \"method\": \"JSONRPC.Version\", \"id\": 1 }", result));
+ ASSERT_FALSE(result.empty());
+
+ // parse the JSON-RPC response
+ CVariant resultObj;
+ ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
+ // make sure it's an object
+ ASSERT_TRUE(resultObj.isObject());
+
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Content-Length header must be present
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // Content-Type must be "application/json"
+ EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
+ // Accept-Ranges must be "none"
+ EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());
+
+ // Cache-Control must contain "mag-age=0" and "no-cache"
+ std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
+ EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
+ EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);
+
+ // uninitialize JSON-RPC
+ JSONRPC::CJSONRPC::Cleanup();
+}
+
+TEST_F(TestWebServer, CanModifyOverJsonRpcWithHttpPost)
+{
+ // initialized JSON-RPC
+ JSONRPC::CJSONRPC::Initialize();
+
+ std::string result;
+ CCurlFile curl;
+ curl.SetMimeType("application/json");
+ ASSERT_TRUE(curl.Post(GetUrl(TEST_URL_JSONRPC), "{ \"jsonrpc\": \"2.0\", \"method\": \"Input.Left\", \"id\": 1 }", result));
+ ASSERT_FALSE(result.empty());
+
+ // parse the JSON-RPC response
+ CVariant resultObj;
+ ASSERT_TRUE(CJSONVariantParser::Parse(result, resultObj));
+ // make sure it's an object
+ ASSERT_TRUE(resultObj.isObject());
+ // it must contain the "result" property with the "OK" value
+ ASSERT_TRUE(resultObj.isMember("result") && resultObj["result"].isString());
+ EXPECT_STREQ("OK", resultObj["result"].asString().c_str());
+
+ // get the HTTP header details
+ const CHttpHeader& httpHeader = curl.GetHttpHeader();
+
+ // Content-Length header must be present
+ ASSERT_EQ(1U, httpHeader.GetValues(MHD_HTTP_HEADER_CONTENT_LENGTH).size());
+ // Content-Type must be "application/json"
+ EXPECT_STREQ("application/json", httpHeader.GetMimeType().c_str());
+ // Accept-Ranges must be "none"
+ EXPECT_STREQ("none", httpHeader.GetValue(MHD_HTTP_HEADER_ACCEPT_RANGES).c_str());
+
+ // Cache-Control must contain "mag-age=0" and "no-cache"
+ std::string cacheControl = httpHeader.GetValue(MHD_HTTP_HEADER_CACHE_CONTROL);
+ EXPECT_TRUE(cacheControl.find("max-age=0") != std::string::npos);
+ EXPECT_TRUE(cacheControl.find("no-cache") != std::string::npos);
+
+ // uninitialize JSON-RPC
+ JSONRPC::CJSONRPC::Cleanup();
+}
+
+TEST_F(TestWebServer, CanNotHeadNonExistingFile)
+{
+ CCurlFile curl;
+ ASSERT_FALSE(curl.Exists(CURL(GetUrlOfTestFile("file_does_not_exist"))));
+}
+
+TEST_F(TestWebServer, CanHeadFile)
+{
+ CCurlFile curl;
+ ASSERT_TRUE(curl.Exists(CURL(GetUrlOfTestFile(TEST_FILES_HTML))));
+
+ CheckHtmlTestFileResponse(curl);
+}
+
+TEST_F(TestWebServer, CanNotGetNonExistingFile)
+{
+ std::string result;
+ CCurlFile curl;
+ ASSERT_FALSE(curl.Get(GetUrlOfTestFile("file_does_not_exist"), result));
+ ASSERT_TRUE(result.empty());
+}
+
+TEST_F(TestWebServer, CanGetFile)
+{
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_HTML), result));
+ ASSERT_STREQ(TEST_FILES_DATA, result.c_str());
+
+ CheckHtmlTestFileResponse(curl);
+}
+
+TEST_F(TestWebServer, CanGetFileForcingNoCache)
+{
+ // check non-cacheable HTML with Control-Cache: no-cache
+ std::string result;
+ CCurlFile curl_html;
+ curl_html.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl_html.SetRequestHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache");
+ ASSERT_TRUE(curl_html.Get(GetUrlOfTestFile(TEST_FILES_HTML), result));
+ EXPECT_STREQ(TEST_FILES_DATA, result.c_str());
+ CheckHtmlTestFileResponse(curl_html);
+
+ // check cacheable text file with Control-Cache: no-cache
+ result.clear();
+ CCurlFile curl_txt;
+ curl_txt.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl_txt.SetRequestHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache");
+ ASSERT_TRUE(curl_txt.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
+ CheckRangesTestFileResponse(curl_txt);
+
+ // check cacheable text file with deprecated Pragma: no-cache
+ result.clear();
+ CCurlFile curl_txt_pragma;
+ curl_txt_pragma.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl_txt_pragma.SetRequestHeader(MHD_HTTP_HEADER_PRAGMA, "no-cache");
+ ASSERT_TRUE(curl_txt_pragma.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
+ CheckRangesTestFileResponse(curl_txt_pragma);
+}
+
+TEST_F(TestWebServer, CanGetCachedFileWithOlderIfModifiedSince)
+{
+ // get the last modified date of the file
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+ CDateTime lastModifiedOlder = lastModified - CDateTimeSpan(1, 0, 0, 0);
+
+ // get the file with an older If-Modified-Since value
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE, lastModifiedOlder.GetAsRFC1123DateTime());
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
+ CheckRangesTestFileResponse(curl);
+}
+
+TEST_F(TestWebServer, CanGetCachedFileWithExactIfModifiedSince)
+{
+ // get the last modified date of the file
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+
+ // get the file with the exact If-Modified-Since value
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE, lastModified.GetAsRFC1123DateTime());
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ ASSERT_TRUE(result.empty());
+ CheckRangesTestFileResponse(curl, MHD_HTTP_NOT_MODIFIED, true);
+}
+
+TEST_F(TestWebServer, CanGetCachedFileWithNewerIfModifiedSince)
+{
+ // get the last modified date of the file
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+ CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0);
+
+ // get the file with a newer If-Modified-Since value
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE,
+ lastModifiedNewer.GetAsRFC1123DateTime());
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ ASSERT_TRUE(result.empty());
+ CheckRangesTestFileResponse(curl, MHD_HTTP_NOT_MODIFIED, true);
+}
+
+TEST_F(TestWebServer, CanGetCachedFileWithNewerIfModifiedSinceForcingNoCache)
+{
+ // get the last modified date of the file
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+ CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0);
+
+ // get the file with a newer If-Modified-Since value but forcing no caching
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_MODIFIED_SINCE, lastModifiedNewer.GetAsRFC1123DateTime());
+ curl.SetRequestHeader(MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache");
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
+ CheckRangesTestFileResponse(curl);
+}
+
+TEST_F(TestWebServer, CanGetCachedFileWithOlderIfUnmodifiedSince)
+{
+ // get the last modified date of the file
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+ CDateTime lastModifiedOlder = lastModified - CDateTimeSpan(1, 0, 0, 0);
+
+ // get the file with an older If-Unmodified-Since value
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE, lastModifiedOlder.GetAsRFC1123DateTime());
+ ASSERT_FALSE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+}
+
+TEST_F(TestWebServer, CanGetCachedFileWithExactIfUnmodifiedSince)
+{
+ // get the last modified date of the file
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+
+ // get the file with an older If-Unmodified-Since value
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE, lastModified.GetAsRFC1123DateTime());
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
+ CheckRangesTestFileResponse(curl);
+}
+
+TEST_F(TestWebServer, CanGetCachedFileWithNewerIfUnmodifiedSince)
+{
+ // get the last modified date of the file
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+ CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0);
+
+ // get the file with a newer If-Unmodified-Since value
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, "");
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE, lastModifiedNewer.GetAsRFC1123DateTime());
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
+ CheckRangesTestFileResponse(curl);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRange0_)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ const std::string range = "bytes=0-";
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify the beginning of the range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRange0_End)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ const std::string range = GenerateRangeHeaderValue(0, rangedFileContent.size());
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify the whole range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRange0_2xEnd)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ const std::string range = GenerateRangeHeaderValue(0, rangedFileContent.size() * 2);
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRange0_First)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
+ const std::string range = GenerateRangeHeaderValue(0, rangedContent.front().size() - 1);
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRangeFirst_Second)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
+ const std::string range = GenerateRangeHeaderValue(rangedContent.front().size() + 1, rangedContent.front().size() + 1 + rangedContent.at(2).size() - 1);
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRange_Last)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
+ const std::string range =
+ StringUtils::Format("bytes=-{}", static_cast<unsigned int>(rangedContent.back().size()));
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRangeFirstSecond)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
+ const std::string range = StringUtils::Format(
+ "bytes=0-{},{}-{}", static_cast<unsigned int>(rangedContent.front().size() - 1),
+ static_cast<unsigned int>(rangedContent.front().size() + 1),
+ static_cast<unsigned int>(rangedContent.front().size() + 1) +
+ static_cast<unsigned int>(rangedContent.at(1).size() - 1));
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetRangedFileRangeFirstSecondLast)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ std::vector<std::string> rangedContent = StringUtils::Split(TEST_FILES_DATA_RANGES, ";");
+ const std::string range = StringUtils::Format(
+ "bytes=0-{},{}-{},-{}", static_cast<unsigned int>(rangedContent.front().size() - 1),
+ static_cast<unsigned int>(rangedContent.front().size() + 1),
+ static_cast<unsigned int>(rangedContent.front().size() + 1) +
+ static_cast<unsigned int>(rangedContent.at(1).size() - 1),
+ static_cast<unsigned int>(rangedContent.back().size()));
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the whole file but specify a larger range
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetCachedRangedFileWithOlderIfRange)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ const std::string range = "bytes=0-";
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the last modified date of the file
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+ CDateTime lastModifiedOlder = lastModified - CDateTimeSpan(1, 0, 0, 0);
+
+ // get the whole file (but ranged) with an older If-Range value
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_RANGE, lastModifiedOlder.GetAsRFC1123DateTime());
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ EXPECT_STREQ(TEST_FILES_DATA_RANGES, result.c_str());
+ CheckRangesTestFileResponse(curl);
+}
+
+TEST_F(TestWebServer, CanGetCachedRangedFileWithExactIfRange)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ const std::string range = "bytes=0-";
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the last modified date of the file
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+
+ // get the whole file (but ranged) with an older If-Range value
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_RANGE, lastModified.GetAsRFC1123DateTime());
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
+
+TEST_F(TestWebServer, CanGetCachedRangedFileWithNewerIfRange)
+{
+ const std::string rangedFileContent = TEST_FILES_DATA_RANGES;
+ const std::string range = "bytes=0-";
+
+ CHttpRanges ranges;
+ ASSERT_TRUE(ranges.Parse(range, rangedFileContent.size()));
+
+ // get the last modified date of the file
+ CDateTime lastModified;
+ ASSERT_TRUE(GetLastModifiedOfTestFile(TEST_FILES_RANGES, lastModified));
+ CDateTime lastModifiedNewer = lastModified + CDateTimeSpan(1, 0, 0, 0);
+
+ // get the whole file (but ranged) with an older If-Range value
+ std::string result;
+ CCurlFile curl;
+ curl.SetRequestHeader(MHD_HTTP_HEADER_RANGE, range);
+ curl.SetRequestHeader(MHD_HTTP_HEADER_IF_RANGE, lastModifiedNewer.GetAsRFC1123DateTime());
+ ASSERT_TRUE(curl.Get(GetUrlOfTestFile(TEST_FILES_RANGES), result));
+ CheckRangesTestFileResponse(curl, result, ranges);
+}
diff --git a/xbmc/network/test/data/webserver/test-ranges.txt b/xbmc/network/test/data/webserver/test-ranges.txt
new file mode 100644
index 0000000..6c0a04b
--- /dev/null
+++ b/xbmc/network/test/data/webserver/test-ranges.txt
@@ -0,0 +1 @@
+range1;range2;range3 \ No newline at end of file
diff --git a/xbmc/network/test/data/webserver/test.html b/xbmc/network/test/data/webserver/test.html
new file mode 100644
index 0000000..30d74d2
--- /dev/null
+++ b/xbmc/network/test/data/webserver/test.html
@@ -0,0 +1 @@
+test \ No newline at end of file
diff --git a/xbmc/network/test/data/webserver/test.png b/xbmc/network/test/data/webserver/test.png
new file mode 100644
index 0000000..f792601
--- /dev/null
+++ b/xbmc/network/test/data/webserver/test.png
Binary files differ
diff --git a/xbmc/network/upnp/CMakeLists.txt b/xbmc/network/upnp/CMakeLists.txt
new file mode 100644
index 0000000..e558cfc
--- /dev/null
+++ b/xbmc/network/upnp/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(SOURCES UPnP.cpp
+ UPnPInternal.cpp
+ UPnPPlayer.cpp
+ UPnPRenderer.cpp
+ UPnPServer.cpp
+ UPnPSettings.cpp)
+
+set(HEADERS UPnP.h
+ UPnPInternal.h
+ UPnPPlayer.h
+ UPnPRenderer.h
+ UPnPServer.h
+ UPnPSettings.h)
+
+core_add_library(network_upnp)
+if(ENABLE_STATIC_LIBS)
+ target_link_libraries(network_upnp PRIVATE upnp)
+endif()
diff --git a/xbmc/network/upnp/UPnP.cpp b/xbmc/network/upnp/UPnP.cpp
new file mode 100644
index 0000000..0e6b689
--- /dev/null
+++ b/xbmc/network/upnp/UPnP.cpp
@@ -0,0 +1,891 @@
+/*
+ * UPnP Support for XBMC
+ * Copyright (c) 2006 c0diq (Sylvain Rebaud)
+ * Portions Copyright (c) by the authors of libPlatinum
+ * http://www.plutinosoft.com/blog/category/platinum/
+ * Copyright (C) 2006-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 "UPnP.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "UPnPInternal.h"
+#include "UPnPRenderer.h"
+#include "UPnPServer.h"
+#include "UPnPSettings.h"
+#include "URL.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "profiles/ProfileManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SystemInfo.h"
+#include "utils/TimeUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <memory>
+#include <mutex>
+#include <set>
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+using namespace UPNP;
+using namespace KODI::MESSAGING;
+
+#define UPNP_DEFAULT_MAX_RETURNED_ITEMS 200
+#define UPNP_DEFAULT_MIN_RETURNED_ITEMS 30
+
+/*
+# Play speed
+# 1 normal
+# 0 invalid
+DLNA_ORG_PS = 'DLNA.ORG_PS'
+DLNA_ORG_PS_VAL = '1'
+
+# Conversion Indicator
+# 1 transcoded
+# 0 not transcoded
+DLNA_ORG_CI = 'DLNA.ORG_CI'
+DLNA_ORG_CI_VAL = '0'
+
+# Operations
+# 00 not time seek range, not range
+# 01 range supported
+# 10 time seek range supported
+# 11 both supported
+DLNA_ORG_OP = 'DLNA.ORG_OP'
+DLNA_ORG_OP_VAL = '01'
+
+# Flags
+# senderPaced 80000000 31
+# lsopTimeBasedSeekSupported 40000000 30
+# lsopByteBasedSeekSupported 20000000 29
+# playcontainerSupported 10000000 28
+# s0IncreasingSupported 08000000 27
+# sNIncreasingSupported 04000000 26
+# rtspPauseSupported 02000000 25
+# streamingTransferModeSupported 01000000 24
+# interactiveTransferModeSupported 00800000 23
+# backgroundTransferModeSupported 00400000 22
+# connectionStallingSupported 00200000 21
+# dlnaVersion15Supported 00100000 20
+DLNA_ORG_FLAGS = 'DLNA.ORG_FLAGS'
+DLNA_ORG_FLAGS_VAL = '01500000000000000000000000000000'
+*/
+
+/*----------------------------------------------------------------------
+| NPT_Console::Output
++---------------------------------------------------------------------*/
+void
+NPT_Console::Output(const char* msg) { }
+
+spdlog::level::level_enum ConvertLogLevel(int nptLogLevel)
+{
+ if (nptLogLevel >= NPT_LOG_LEVEL_FATAL)
+ return spdlog::level::critical;
+ if (nptLogLevel >= NPT_LOG_LEVEL_SEVERE)
+ return spdlog::level::err;
+ if (nptLogLevel >= NPT_LOG_LEVEL_WARNING)
+ return spdlog::level::warn;
+ if (nptLogLevel >= NPT_LOG_LEVEL_FINE)
+ return spdlog::level::info;
+ if (nptLogLevel >= NPT_LOG_LEVEL_FINER)
+ return spdlog::level::debug;
+
+ return spdlog::level::trace;
+}
+
+void
+UPnPLogger(const NPT_LogRecord* record)
+{
+ static Logger logger = CServiceBroker::GetLogging().GetLogger("Platinum");
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGUPNP))
+ logger->log(ConvertLogLevel(record->m_Level), "[{}]: {}", record->m_LoggerName,
+ record->m_Message);
+}
+
+namespace UPNP
+{
+
+/*----------------------------------------------------------------------
+| static
++---------------------------------------------------------------------*/
+CUPnP* CUPnP::upnp = NULL;
+static NPT_List<void*> g_UserData;
+static NPT_Mutex g_UserDataLock;
+
+/*----------------------------------------------------------------------
+| CDeviceHostReferenceHolder class
++---------------------------------------------------------------------*/
+class CDeviceHostReferenceHolder
+{
+public:
+ PLT_DeviceHostReference m_Device;
+};
+
+/*----------------------------------------------------------------------
+| CCtrlPointReferenceHolder class
++---------------------------------------------------------------------*/
+class CCtrlPointReferenceHolder
+{
+public:
+ PLT_CtrlPointReference m_CtrlPoint;
+};
+
+/*----------------------------------------------------------------------
+| CUPnPCleaner class
++---------------------------------------------------------------------*/
+class CUPnPCleaner : public NPT_Thread
+{
+public:
+ explicit CUPnPCleaner(CUPnP* upnp) : NPT_Thread(true), m_UPnP(upnp) {}
+ void Run() override {
+ delete m_UPnP;
+ }
+
+ CUPnP* m_UPnP;
+};
+
+/*----------------------------------------------------------------------
+| CMediaBrowser class
++---------------------------------------------------------------------*/
+class CMediaBrowser : public PLT_SyncMediaBrowser, public PLT_MediaContainerChangesListener
+{
+public:
+ explicit CMediaBrowser(PLT_CtrlPointReference& ctrlPoint)
+ : PLT_SyncMediaBrowser(ctrlPoint, true),
+ m_logger(CServiceBroker::GetLogging().GetLogger("UPNP::CMediaBrowser"))
+ {
+ SetContainerListener(this);
+ }
+
+ // PLT_MediaBrowser methods
+ bool OnMSAdded(PLT_DeviceDataReference& device) override
+ {
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ message.SetStringParam("upnp://");
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+
+ return PLT_SyncMediaBrowser::OnMSAdded(device);
+ }
+ void OnMSRemoved(PLT_DeviceDataReference& device) override
+ {
+ PLT_SyncMediaBrowser::OnMSRemoved(device);
+
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ message.SetStringParam("upnp://");
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+
+ PLT_SyncMediaBrowser::OnMSRemoved(device);
+ }
+
+ // PLT_MediaContainerChangesListener methods
+ void OnContainerChanged(PLT_DeviceDataReference& device,
+ const char* item_id,
+ const char* update_id) override
+ {
+ NPT_String path = "upnp://"+device->GetUUID()+"/";
+ if (!NPT_StringsEqual(item_id, "0")) {
+ std::string id(CURL::Encode(item_id));
+ URIUtils::AddSlashAtEnd(id);
+ path += id.c_str();
+ }
+
+ m_logger->debug("notified container update {}", (const char*)path);
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH);
+ message.SetStringParam(path.GetChars());
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+ }
+
+ bool MarkWatched(const CFileItem& item, const bool watched)
+ {
+ if (watched) {
+ CFileItem temp(item);
+ temp.SetProperty("original_listitem_url", item.GetPath());
+ return SaveFileState(temp, CBookmark(), watched);
+ }
+ else {
+ m_logger->debug("Marking video item {} as watched", item.GetPath());
+
+ std::set<std::pair<NPT_String, NPT_String> > values;
+ values.insert(std::make_pair("<upnp:playCount>1</upnp:playCount>",
+ "<upnp:playCount>0</upnp:playCount>"));
+ return InvokeUpdateObject(item.GetPath().c_str(), values);
+ }
+ }
+
+ bool SaveFileState(const CFileItem& item, const CBookmark& bookmark, const bool updatePlayCount)
+ {
+ std::string path = item.GetProperty("original_listitem_url").asString();
+ if (!item.HasVideoInfoTag() || path.empty()) {
+ return false;
+ }
+
+ std::set<std::pair<NPT_String, NPT_String> > values;
+ if (item.GetVideoInfoTag()->GetResumePoint().timeInSeconds != bookmark.timeInSeconds) {
+ m_logger->debug("Updating resume point for item {}", path);
+ long time = (long)bookmark.timeInSeconds;
+ if (time < 0)
+ time = 0;
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ (long)item.GetVideoInfoTag()->GetResumePoint().timeInSeconds),
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ time)));
+
+ NPT_String curr_value = "<xbmc:lastPlayerState>";
+ PLT_Didl::AppendXmlEscape(curr_value, item.GetVideoInfoTag()->GetResumePoint().playerState.c_str());
+ curr_value += "</xbmc:lastPlayerState>";
+ NPT_String new_value = "<xbmc:lastPlayerState>";
+ PLT_Didl::AppendXmlEscape(new_value, bookmark.playerState.c_str());
+ new_value += "</xbmc:lastPlayerState>";
+ values.insert(std::make_pair(curr_value, new_value));
+ }
+ if (updatePlayCount) {
+ m_logger->debug("Marking video item {} as watched", path);
+ values.insert(std::make_pair("<upnp:playCount>0</upnp:playCount>",
+ "<upnp:playCount>1</upnp:playCount>"));
+ }
+
+ return InvokeUpdateObject(path.c_str(), values);
+ }
+
+ bool UpdateItem(const std::string& path, const CFileItem& item)
+ {
+ if (path.empty())
+ return false;
+
+ std::set<std::pair<NPT_String, NPT_String> > values;
+ if (item.HasVideoInfoTag())
+ {
+ // handle playcount
+ const CVideoInfoTag *details = item.GetVideoInfoTag();
+ int playcountOld = 0, playcountNew = 0;
+ if (details->GetPlayCount() <= 0)
+ playcountOld = 1;
+ else
+ playcountNew = details->GetPlayCount();
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:playCount>%d</upnp:playCount>", playcountOld),
+ NPT_String::Format("<upnp:playCount>%d</upnp:playCount>", playcountNew)));
+
+ // handle lastplayed
+ CDateTime lastPlayedOld, lastPlayedNew;
+ if (!details->m_lastPlayed.IsValid())
+ lastPlayedOld = CDateTime::GetCurrentDateTime();
+ else
+ lastPlayedNew = details->m_lastPlayed;
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:lastPlaybackTime>%s</upnp:lastPlaybackTime>",
+ lastPlayedOld.GetAsW3CDateTime().c_str()),
+ NPT_String::Format("<upnp:lastPlaybackTime>%s</upnp:lastPlaybackTime>",
+ lastPlayedNew.GetAsW3CDateTime().c_str())));
+
+ // handle resume point
+ long resumePointOld = 0L, resumePointNew = 0L;
+ if (details->GetResumePoint().timeInSeconds <= 0)
+ resumePointOld = 1;
+ else
+ resumePointNew = static_cast<long>(details->GetResumePoint().timeInSeconds);
+
+ values.insert(std::make_pair(
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ resumePointOld),
+ NPT_String::Format("<upnp:lastPlaybackPosition>%ld</upnp:lastPlaybackPosition>",
+ resumePointNew)));
+ }
+
+ return InvokeUpdateObject(path.c_str(), values);
+ }
+
+ bool InvokeUpdateObject(const char *id, const std::set<std::pair<NPT_String, NPT_String> >& values)
+ {
+ CURL url(id);
+ PLT_DeviceDataReference device;
+ PLT_Service* cds;
+ PLT_ActionReference action;
+ NPT_String curr_value, new_value;
+
+ m_logger->debug("attempting to invoke UpdateObject for {}", id);
+
+ // check this server supports UpdateObject action
+ NPT_CHECK_LABEL(FindServer(url.GetHostName().c_str(), device),failed);
+ NPT_CHECK_LABEL(device->FindServiceById("urn:upnp-org:serviceId:ContentDirectory", cds),failed);
+
+ NPT_CHECK_LABEL(m_CtrlPoint->CreateAction(
+ device,
+ "urn:schemas-upnp-org:service:ContentDirectory:1",
+ "UpdateObject",
+ action), failed);
+
+ NPT_CHECK_LABEL(action->SetArgumentValue("ObjectID", url.GetFileName().c_str()), failed);
+
+ // put together the current and the new value string
+ for (std::set<std::pair<NPT_String, NPT_String> >::const_iterator value = values.begin(); value != values.end(); ++value)
+ {
+ if (!curr_value.IsEmpty())
+ curr_value.Append(",");
+ if (!new_value.IsEmpty())
+ new_value.Append(",");
+
+ curr_value.Append(value->first);
+ new_value.Append(value->second);
+ }
+ NPT_CHECK_LABEL(action->SetArgumentValue("CurrentTagValue", curr_value), failed);
+ NPT_CHECK_LABEL(action->SetArgumentValue("NewTagValue", new_value), failed);
+
+ NPT_CHECK_LABEL(m_CtrlPoint->InvokeAction(action, NULL),failed);
+
+ m_logger->debug("invoked UpdateObject successfully");
+ return true;
+
+ failed:
+ m_logger->info("invoking UpdateObject failed");
+ return false;
+ }
+
+ private:
+ Logger m_logger;
+};
+
+
+/*----------------------------------------------------------------------
+| CMediaController class
++---------------------------------------------------------------------*/
+class CMediaController
+ : public PLT_MediaControllerDelegate
+ , public PLT_MediaController
+{
+public:
+ explicit CMediaController(PLT_CtrlPointReference& ctrl_point)
+ : PLT_MediaController(ctrl_point)
+ {
+ PLT_MediaController::SetDelegate(this);
+ }
+
+ ~CMediaController() override
+ {
+ for (const auto& itRenderer : m_registeredRenderers)
+ unregisterRenderer(itRenderer);
+ m_registeredRenderers.clear();
+ }
+
+#define CHECK_USERDATA_RETURN(userdata) do { \
+ if (!g_UserData.Contains(userdata)) \
+ return; \
+ } while(0)
+
+ void OnStopResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnStopResult(res, device, userdata);
+ }
+
+ void OnSetPlayModeResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSetPlayModeResult(res, device, userdata);
+ }
+
+ void OnSetAVTransportURIResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSetAVTransportURIResult(res, device, userdata);
+ }
+
+ void OnSeekResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnSeekResult(res, device, userdata);
+ }
+
+ void OnPreviousResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPreviousResult(res, device, userdata);
+ }
+
+ void OnPlayResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPlayResult(res, device, userdata);
+ }
+
+ void OnPauseResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnPauseResult(res, device, userdata);
+ }
+
+ void OnNextResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnNextResult(res, device, userdata);
+ }
+
+ void OnGetMediaInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_MediaInfo* info, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetMediaInfoResult(res, device, info, userdata);
+ }
+
+ void OnGetPositionInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_PositionInfo* info, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetPositionInfoResult(res, device, info, userdata);
+ }
+
+ void OnGetTransportInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_TransportInfo* info, void* userdata) override
+ { CHECK_USERDATA_RETURN(userdata);
+ static_cast<PLT_MediaControllerDelegate*>(userdata)->OnGetTransportInfoResult(res, device, info, userdata);
+ }
+
+ bool OnMRAdded(PLT_DeviceDataReference& device ) override
+ {
+ if (device->GetUUID().IsEmpty() || device->GetUUID().GetChars() == NULL)
+ return false;
+
+ CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ playerCoreFactory.OnPlayerDiscovered((const char*)device->GetUUID()
+ ,(const char*)device->GetFriendlyName());
+
+ m_registeredRenderers.insert(std::string(device->GetUUID().GetChars()));
+ return true;
+ }
+
+ void OnMRRemoved(PLT_DeviceDataReference& device ) override
+ {
+ if (device->GetUUID().IsEmpty() || device->GetUUID().GetChars() == NULL)
+ return;
+
+ std::string uuid(device->GetUUID().GetChars());
+ unregisterRenderer(uuid);
+ m_registeredRenderers.erase(uuid);
+ }
+
+private:
+ void unregisterRenderer(const std::string &deviceUUID)
+ {
+ CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ playerCoreFactory.OnPlayerRemoved(deviceUUID);
+ }
+
+ std::set<std::string> m_registeredRenderers;
+};
+
+/*----------------------------------------------------------------------
+| CUPnP::CUPnP
++---------------------------------------------------------------------*/
+CUPnP::CUPnP() :
+ m_MediaBrowser(NULL),
+ m_MediaController(NULL),
+ m_LogHandler(NULL),
+ m_ServerHolder(new CDeviceHostReferenceHolder()),
+ m_RendererHolder(new CRendererReferenceHolder()),
+ m_CtrlPointHolder(new CCtrlPointReferenceHolder())
+{
+ NPT_LogManager::GetDefault().Configure("plist:.level=FINE;.handlers=CustomHandler;");
+ NPT_LogHandler::Create("xbmc", "CustomHandler", m_LogHandler);
+ m_LogHandler->SetCustomHandlerFunction(&UPnPLogger);
+
+ // initialize upnp context
+ m_UPnP = new PLT_UPnP();
+
+ // keep main IP around
+ if (CServiceBroker::GetNetwork().GetFirstConnectedInterface()) {
+ m_IP = CServiceBroker::GetNetwork().GetFirstConnectedInterface()->GetCurrentIPAddress().c_str();
+ }
+ NPT_List<NPT_IpAddress> list;
+ if (NPT_SUCCEEDED(PLT_UPnPMessageHelper::GetIPAddresses(list)) && list.GetItemCount()) {
+ m_IP = (*(list.GetFirstItem())).ToString();
+ }
+ else if(m_IP.empty())
+ m_IP = "localhost";
+
+ // start upnp monitoring
+ m_UPnP->Start();
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::~CUPnP
++---------------------------------------------------------------------*/
+CUPnP::~CUPnP()
+{
+ m_UPnP->Stop();
+ StopClient();
+ StopController();
+ StopServer();
+
+ delete m_UPnP;
+ delete m_LogHandler;
+ delete m_ServerHolder;
+ delete m_RendererHolder;
+ delete m_CtrlPointHolder;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::GetInstance
++---------------------------------------------------------------------*/
+CUPnP*
+CUPnP::GetInstance()
+{
+ if (!upnp) {
+ upnp = new CUPnP();
+ }
+
+ return upnp;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::ReleaseInstance
++---------------------------------------------------------------------*/
+void
+CUPnP::ReleaseInstance(bool bWait)
+{
+ if (upnp) {
+ CUPnP* _upnp = upnp;
+ upnp = NULL;
+
+ if (bWait) {
+ delete _upnp;
+ } else {
+ // since it takes a while to clean up
+ // starts a detached thread to do this
+ CUPnPCleaner* cleaner = new CUPnPCleaner(_upnp);
+ cleaner->Start();
+ }
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::GetServer
++---------------------------------------------------------------------*/
+CUPnPServer* CUPnP::GetServer()
+{
+ if(upnp)
+ return static_cast<CUPnPServer*>(upnp->m_ServerHolder->m_Device.AsPointer());
+ return NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::MarkWatched
++---------------------------------------------------------------------*/
+bool
+CUPnP::MarkWatched(const CFileItem& item, const bool watched)
+{
+ if (upnp && upnp->m_MediaBrowser) {
+ // dynamic_cast is safe here, avoids polluting CUPnP.h header file
+ CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
+ if (browser)
+ return browser->MarkWatched(item, watched);
+ }
+ return false;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::SaveFileState
++---------------------------------------------------------------------*/
+bool
+CUPnP::SaveFileState(const CFileItem& item, const CBookmark& bookmark, const bool updatePlayCount)
+{
+ if (upnp && upnp->m_MediaBrowser) {
+ // dynamic_cast is safe here, avoids polluting CUPnP.h header file
+ CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
+ if (browser)
+ return browser->SaveFileState(item, bookmark, updatePlayCount);
+ }
+ return false;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::CreateControlPoint
++---------------------------------------------------------------------*/
+void
+CUPnP::CreateControlPoint()
+{
+ if (!m_CtrlPointHolder->m_CtrlPoint.IsNull())
+ return;
+
+ // create controlpoint
+ m_CtrlPointHolder->m_CtrlPoint = new PLT_CtrlPoint();
+
+ // start it
+ m_UPnP->AddCtrlPoint(m_CtrlPointHolder->m_CtrlPoint);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::DestroyControlPoint
++---------------------------------------------------------------------*/
+void
+CUPnP::DestroyControlPoint()
+{
+ if (m_CtrlPointHolder->m_CtrlPoint.IsNull())
+ return;
+
+ m_UPnP->RemoveCtrlPoint(m_CtrlPointHolder->m_CtrlPoint);
+ m_CtrlPointHolder->m_CtrlPoint = NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::UpdateItem
++---------------------------------------------------------------------*/
+bool
+CUPnP::UpdateItem(const std::string& path, const CFileItem& item)
+{
+ if (upnp && upnp->m_MediaBrowser) {
+ // dynamic_cast is safe here, avoids polluting CUPnP.h header file
+ CMediaBrowser* browser = dynamic_cast<CMediaBrowser*>(upnp->m_MediaBrowser);
+ if (browser)
+ return browser->UpdateItem(path, item);
+ }
+ return false;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartClient
++---------------------------------------------------------------------*/
+void
+CUPnP::StartClient()
+{
+ std::unique_lock<CCriticalSection> lock(m_lockMediaBrowser);
+ if (m_MediaBrowser != NULL)
+ return;
+
+ CreateControlPoint();
+
+ // start browser
+ m_MediaBrowser = new CMediaBrowser(m_CtrlPointHolder->m_CtrlPoint);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopClient
++---------------------------------------------------------------------*/
+void
+CUPnP::StopClient()
+{
+ std::unique_lock<CCriticalSection> lock(m_lockMediaBrowser);
+ if (m_MediaBrowser == NULL)
+ return;
+
+ delete m_MediaBrowser;
+ m_MediaBrowser = NULL;
+
+ if (!IsControllerStarted())
+ DestroyControlPoint();
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartController
++---------------------------------------------------------------------*/
+void
+CUPnP::StartController()
+{
+ if (m_MediaController != NULL)
+ return;
+
+ CreateControlPoint();
+
+ m_MediaController = new CMediaController(m_CtrlPointHolder->m_CtrlPoint);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopController
++---------------------------------------------------------------------*/
+void
+CUPnP::StopController()
+{
+ if (m_MediaController == NULL)
+ return;
+
+ delete m_MediaController;
+ m_MediaController = NULL;
+
+ if (!IsClientStarted())
+ DestroyControlPoint();
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::CreateServer
++---------------------------------------------------------------------*/
+CUPnPServer*
+CUPnP::CreateServer(int port /* = 0 */)
+{
+ CUPnPServer* device =
+ new CUPnPServer(CSysInfo::GetDeviceName().c_str(),
+ CUPnPSettings::GetInstance().GetServerUUID().length() ? CUPnPSettings::GetInstance().GetServerUUID().c_str() : NULL,
+ port);
+
+ // trying to set optional upnp values for XP UPnP UI Icons to detect us
+ // but it doesn't work anyways as it requires multicast for XP to detect us
+ device->m_PresentationURL =
+ NPT_HttpUrl(m_IP.c_str(),
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SERVICES_WEBSERVERPORT),
+ "/").ToString();
+
+ device->m_ModelName = "Kodi";
+ device->m_ModelNumber = CSysInfo::GetVersion().c_str();
+ device->m_ModelDescription = "Kodi - Media Server";
+ device->m_ModelURL = "http://kodi.tv/";
+ device->m_Manufacturer = "XBMC Foundation";
+ device->m_ManufacturerURL = "http://kodi.tv/";
+
+ device->SetDelegate(device);
+ return device;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartServer
++---------------------------------------------------------------------*/
+bool
+CUPnP::StartServer()
+{
+ if (!m_ServerHolder->m_Device.IsNull()) return false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // load upnpserver.xml
+ std::string filename = URIUtils::AddFileToFolder(profileManager->GetUserDataFolder(), "upnpserver.xml");
+ CUPnPSettings::GetInstance().Load(filename);
+
+ // create the server with a XBox compatible friendlyname and UUID from upnpserver.xml if found
+ m_ServerHolder->m_Device = CreateServer(CUPnPSettings::GetInstance().GetServerPort());
+
+ // start server
+ NPT_Result res = m_UPnP->AddDevice(m_ServerHolder->m_Device);
+ if (NPT_FAILED(res)) {
+ // if the upnp device port was not 0, it could have failed because
+ // of port being in used, so restart with a random port
+ if (CUPnPSettings::GetInstance().GetServerPort() > 0) m_ServerHolder->m_Device = CreateServer(0);
+
+ res = m_UPnP->AddDevice(m_ServerHolder->m_Device);
+ }
+
+ // save port but don't overwrite saved settings if port was random
+ if (NPT_SUCCEEDED(res)) {
+ if (CUPnPSettings::GetInstance().GetServerPort() == 0) {
+ CUPnPSettings::GetInstance().SetServerPort(m_ServerHolder->m_Device->GetPort());
+ }
+ CUPnPServer::m_MaxReturnedItems = UPNP_DEFAULT_MAX_RETURNED_ITEMS;
+ if (CUPnPSettings::GetInstance().GetMaximumReturnedItems() > 0) {
+ // must be > UPNP_DEFAULT_MIN_RETURNED_ITEMS
+ CUPnPServer::m_MaxReturnedItems = std::max(UPNP_DEFAULT_MIN_RETURNED_ITEMS, CUPnPSettings::GetInstance().GetMaximumReturnedItems());
+ }
+ CUPnPSettings::GetInstance().SetMaximumReturnedItems(CUPnPServer::m_MaxReturnedItems);
+ }
+
+ // save UUID
+ CUPnPSettings::GetInstance().SetServerUUID(m_ServerHolder->m_Device->GetUUID().GetChars());
+ return CUPnPSettings::GetInstance().Save(filename);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopServer
++---------------------------------------------------------------------*/
+void
+CUPnP::StopServer()
+{
+ if (m_ServerHolder->m_Device.IsNull()) return;
+
+ m_UPnP->RemoveDevice(m_ServerHolder->m_Device);
+ m_ServerHolder->m_Device = NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::CreateRenderer
++---------------------------------------------------------------------*/
+CUPnPRenderer*
+CUPnP::CreateRenderer(int port /* = 0 */)
+{
+ CUPnPRenderer* device =
+ new CUPnPRenderer(CSysInfo::GetDeviceName().c_str(),
+ false,
+ (CUPnPSettings::GetInstance().GetRendererUUID().length() ? CUPnPSettings::GetInstance().GetRendererUUID().c_str() : NULL),
+ port);
+
+ device->m_PresentationURL =
+ NPT_HttpUrl(m_IP.c_str(),
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SERVICES_WEBSERVERPORT),
+ "/").ToString();
+ device->m_ModelName = "Kodi";
+ device->m_ModelNumber = CSysInfo::GetVersion().c_str();
+ device->m_ModelDescription = "Kodi - Media Renderer";
+ device->m_ModelURL = "http://kodi.tv/";
+ device->m_Manufacturer = "XBMC Foundation";
+ device->m_ManufacturerURL = "http://kodi.tv/";
+
+ return device;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StartRenderer
++---------------------------------------------------------------------*/
+bool CUPnP::StartRenderer()
+{
+ if (!m_RendererHolder->m_Device.IsNull())
+ return false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::string filename = URIUtils::AddFileToFolder(profileManager->GetUserDataFolder(), "upnpserver.xml");
+ CUPnPSettings::GetInstance().Load(filename);
+
+ m_RendererHolder->m_Device = CreateRenderer(CUPnPSettings::GetInstance().GetRendererPort());
+
+ NPT_Result res = m_UPnP->AddDevice(m_RendererHolder->m_Device);
+
+ // failed most likely because port is in use, try again with random port now
+ if (NPT_FAILED(res) && CUPnPSettings::GetInstance().GetRendererPort() != 0) {
+ m_RendererHolder->m_Device = CreateRenderer(0);
+
+ res = m_UPnP->AddDevice(m_RendererHolder->m_Device);
+ }
+
+ // save port but don't overwrite saved settings if random
+ if (NPT_SUCCEEDED(res) && CUPnPSettings::GetInstance().GetRendererPort() == 0) {
+ CUPnPSettings::GetInstance().SetRendererPort(m_RendererHolder->m_Device->GetPort());
+ }
+
+ // save UUID
+ CUPnPSettings::GetInstance().SetRendererUUID(m_RendererHolder->m_Device->GetUUID().GetChars());
+ return CUPnPSettings::GetInstance().Save(filename);
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::StopRenderer
++---------------------------------------------------------------------*/
+void CUPnP::StopRenderer()
+{
+ if (m_RendererHolder->m_Device.IsNull()) return;
+
+ m_UPnP->RemoveDevice(m_RendererHolder->m_Device);
+ m_RendererHolder->m_Device = NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnP::UpdateState
++---------------------------------------------------------------------*/
+void CUPnP::UpdateState()
+{
+ if (!m_RendererHolder->m_Device.IsNull())
+ static_cast<CUPnPRenderer*>(m_RendererHolder->m_Device.AsPointer())->UpdateState();
+}
+
+void CUPnP::RegisterUserdata(void* ptr)
+{
+ NPT_AutoLock lock(g_UserDataLock);
+ g_UserData.Add(ptr);
+}
+
+void CUPnP::UnregisterUserdata(void* ptr)
+{
+ NPT_AutoLock lock(g_UserDataLock);
+ g_UserData.Remove(ptr);
+}
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnP.h b/xbmc/network/upnp/UPnP.h
new file mode 100644
index 0000000..311aa6c
--- /dev/null
+++ b/xbmc/network/upnp/UPnP.h
@@ -0,0 +1,108 @@
+/*
+ * UPnP Support for XBMC
+ * Copyright (c) 2006 c0diq (Sylvain Rebaud)
+ * Portions Copyright (c) by the authors of libPlatinum
+ * http://www.plutinosoft.com/blog/category/platinum/
+ * Copyright (C) 2006-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 <string>
+
+class NPT_LogHandler;
+class PLT_UPnP;
+class PLT_SyncMediaBrowser;
+class PLT_MediaController;
+class PLT_MediaObject;
+class PLT_MediaItemResource;
+class CFileItem;
+class CBookmark;
+
+namespace UPNP
+{
+
+class CDeviceHostReferenceHolder;
+class CCtrlPointReferenceHolder;
+class CRendererReferenceHolder;
+class CUPnPRenderer;
+class CUPnPServer;
+
+class CUPnP
+{
+public:
+ CUPnP();
+ ~CUPnP();
+
+ // server
+ bool StartServer();
+ void StopServer();
+
+ // client
+ void StartClient();
+ void StopClient();
+ bool IsClientStarted() { return (m_MediaBrowser != NULL); }
+
+ // controller
+ void StartController();
+ void StopController();
+ bool IsControllerStarted() { return (m_MediaController != NULL); }
+
+ // renderer
+ bool StartRenderer();
+ void StopRenderer();
+ void UpdateState();
+
+ // class methods
+ static CUPnP* GetInstance();
+ static CUPnPServer* GetServer();
+ static void ReleaseInstance(bool bWait);
+ static bool IsInstantiated() { return upnp != NULL; }
+
+ static bool MarkWatched(const CFileItem& item,
+ const bool watched);
+
+ static bool SaveFileState(const CFileItem& item,
+ const CBookmark& bookmark,
+ const bool updatePlayCount);
+ static bool UpdateItem(const std::string& path,
+ const CFileItem& item);
+
+ static void RegisterUserdata(void* ptr);
+ static void UnregisterUserdata(void* ptr);
+private:
+ CUPnP(const CUPnP&) = delete;
+ CUPnP& operator=(const CUPnP&) = delete;
+
+ void CreateControlPoint();
+ void DestroyControlPoint();
+
+ // methods
+ CUPnPRenderer* CreateRenderer(int port = 0);
+ CUPnPServer* CreateServer(int port = 0);
+
+ CCriticalSection m_lockMediaBrowser;
+
+ public:
+ PLT_SyncMediaBrowser* m_MediaBrowser;
+ PLT_MediaController* m_MediaController;
+
+private:
+ std::string m_IP;
+ PLT_UPnP* m_UPnP;
+ NPT_LogHandler* m_LogHandler;
+ CDeviceHostReferenceHolder* m_ServerHolder;
+ CRendererReferenceHolder* m_RendererHolder;
+ CCtrlPointReferenceHolder* m_CtrlPointHolder;
+
+
+ static CUPnP* upnp;
+};
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnPInternal.cpp b/xbmc/network/upnp/UPnPInternal.cpp
new file mode 100644
index 0000000..4066157
--- /dev/null
+++ b/xbmc/network/upnp/UPnPInternal.cpp
@@ -0,0 +1,1245 @@
+/*
+ * 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 "UPnPInternal.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "ThumbLoader.h"
+#include "UPnPServer.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/StackDirectory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/ContentUtils.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <algorithm>
+#include <array>
+#include <string_view>
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+
+namespace UPNP
+{
+
+// the original version of content type here,eg: text/srt,which was defined 10 years ago (year 2013,commit 56519bec #L1158-L1161 )
+// is not a standard mime type. according to the specs of UPNP
+// http://upnp.org/specs/av/UPnP-av-ConnectionManager-v3-Service.pdf chapter "A.1.1 ProtocolInfo Definition"
+// "The <contentFormat> part for HTTP GET is described by a MIME type RFC https://www.ietf.org/rfc/rfc1341.txt"
+// all the pre-defined "text/*" MIME by IANA is here https://www.iana.org/assignments/media-types/media-types.xhtml#text
+// there is not any subtitle MIME for now (year 2022), we used to use text/srt|ssa|sub|idx, but,
+// kodi support SUP subtitle now, and SUP subtitle is not really a text(see below), to keep it
+// compatible, we suggest only to match the extension
+//
+// main purpose of this array is to share supported real subtitle formats when kodi act as a UPNP
+// server or UPNP/DLNA media render
+constexpr std::array<std::string_view, 9> SupportedSubFormats = {
+ "txt", "srt", "ssa", "ass", "sub", "smi", "vtt",
+ // "sup" subtitle is not a real TEXT,
+ // and there is no real STD subtitle RFC of DLNA,
+ // so we only match the extension of the "fake" content type
+ "sup", "idx"};
+
+// Map defining extensions for mimetypes not available in Platinum mimetype map
+// or that the application wants to override. These definitions take precedence
+// over all other possible mime type definitions.
+constexpr NPT_HttpFileRequestHandler_DefaultFileTypeMapEntry kodiPlatinumMimeTypeExtensions[] = {
+ {"m2ts", "video/vnd.dlna.mpeg-tts"}};
+
+/*----------------------------------------------------------------------
+| GetClientQuirks
++---------------------------------------------------------------------*/
+EClientQuirks GetClientQuirks(const PLT_HttpRequestContext* context)
+{
+ if(context == NULL)
+ return ECLIENTQUIRKS_NONE;
+
+ unsigned int quirks = 0;
+ const NPT_String* user_agent = context->GetRequest().GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_USER_AGENT);
+ const NPT_String* server = context->GetRequest().GetHeaders().GetHeaderValue(NPT_HTTP_HEADER_SERVER);
+
+ if (user_agent) {
+ if (user_agent->Find("XBox", 0, true) >= 0 ||
+ user_agent->Find("Xenon", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_ONLYSTORAGEFOLDER | ECLIENTQUIRKS_BASICVIDEOCLASS;
+
+ if (user_agent->Find("Windows-Media-Player", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_UNKNOWNSERIES;
+
+ }
+ if (server) {
+ if (server->Find("Xbox", 0, true) >= 0)
+ quirks |= ECLIENTQUIRKS_ONLYSTORAGEFOLDER | ECLIENTQUIRKS_BASICVIDEOCLASS;
+ }
+
+ return (EClientQuirks)quirks;
+}
+
+/*----------------------------------------------------------------------
+| GetMediaControllerQuirks
++---------------------------------------------------------------------*/
+EMediaControllerQuirks GetMediaControllerQuirks(const PLT_DeviceData *device)
+{
+ if (device == NULL)
+ return EMEDIACONTROLLERQUIRKS_NONE;
+
+ unsigned int quirks = 0;
+
+ if (device->m_Manufacturer.Find("Samsung Electronics") >= 0)
+ quirks |= EMEDIACONTROLLERQUIRKS_X_MKV;
+
+ return (EMediaControllerQuirks)quirks;
+}
+
+/*----------------------------------------------------------------------
+| GetMimeType
++---------------------------------------------------------------------*/
+NPT_String
+GetMimeType(const char* filename,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ NPT_String ext = URIUtils::GetExtension(filename).c_str();
+ ext.TrimLeft('.');
+ ext = ext.ToLowercase();
+
+ return PLT_MimeType::GetMimeTypeFromExtension(ext, context);
+}
+
+/*----------------------------------------------------------------------
+| GetMimeType
++---------------------------------------------------------------------*/
+NPT_String
+GetMimeType(const CFileItem& item,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ std::string path = item.GetPath();
+ if (item.HasVideoInfoTag() && !item.GetVideoInfoTag()->GetPath().empty()) {
+ path = item.GetVideoInfoTag()->GetPath();
+ } else if (item.HasMusicInfoTag() && !item.GetMusicInfoTag()->GetURL().empty()) {
+ path = item.GetMusicInfoTag()->GetURL();
+ }
+
+ if (URIUtils::IsStack(path))
+ path = XFILE::CStackDirectory::GetFirstStackedFile(path);
+
+ NPT_String ext = URIUtils::GetExtension(path).c_str();
+ ext.TrimLeft('.');
+ ext = ext.ToLowercase();
+
+ NPT_String mime;
+
+ if (!ext.IsEmpty())
+ {
+ /* We look first to our extensions/overrides of libplatinum mimetypes. If not found, fallback to
+ Platinum definitions.
+ */
+ const auto kodiOverrideMimeType = std::find_if(
+ std::begin(kodiPlatinumMimeTypeExtensions), std::end(kodiPlatinumMimeTypeExtensions),
+ [&](const auto& mimeTypeEntry) { return mimeTypeEntry.extension == ext; });
+ if (kodiOverrideMimeType != std::end(kodiPlatinumMimeTypeExtensions))
+ {
+ mime = kodiOverrideMimeType->mime_type;
+ }
+ else
+ {
+ /* Give priority to Platinum mime types as they are defined to map extension to DLNA compliant mime types
+ or custom types according to context (who asked for it)
+ */
+ mime = PLT_MimeType::GetMimeTypeFromExtension(ext, context);
+ if (mime == "application/octet-stream")
+ {
+ mime = "";
+ }
+ }
+ }
+
+ /* if Platinum couldn't map it, default to Kodi internal mapping */
+ if (mime.IsEmpty()) {
+ NPT_String mime = item.GetMimeType().c_str();
+ if (mime == "application/octet-stream") mime = "";
+ }
+
+ /* fallback to generic mime type if not found */
+ if (mime.IsEmpty()) {
+ if (item.IsVideo() || item.IsVideoDb() )
+ mime = "video/" + ext;
+ else if (item.IsAudio() || item.IsMusicDb() )
+ mime = "audio/" + ext;
+ else if (item.IsPicture() )
+ mime = "image/" + ext;
+ else if (item.IsSubtitle())
+ mime = "text/" + ext;
+ }
+
+ /* nothing we can figure out */
+ if (mime.IsEmpty()) {
+ mime = "application/octet-stream";
+ }
+
+ return mime;
+}
+
+/*----------------------------------------------------------------------
+| GetProtocolInfo
++---------------------------------------------------------------------*/
+const NPT_String
+GetProtocolInfo(const CFileItem& item,
+ const char* protocol,
+ const PLT_HttpRequestContext* context /* = NULL */)
+{
+ NPT_String proto = protocol;
+
+ //! @todo fixup the protocol just in case nothing was passed
+ if (proto.IsEmpty()) {
+ proto = item.GetURL().GetProtocol().c_str();
+ }
+
+ /**
+ * map protocol to right prefix and use xbmc-get for
+ * unsupported UPnP protocols for other xbmc clients
+ * @todo add rtsp ?
+ */
+ if (proto == "http") {
+ proto = "http-get";
+ } else {
+ proto = "xbmc-get";
+ }
+
+ /* we need a valid extension to retrieve the mimetype for the protocol info */
+ NPT_String mime = GetMimeType(item, context);
+ proto += ":*:" + mime + ":" + PLT_ProtocolInfo::GetDlnaExtension(mime, context);
+ return proto;
+}
+
+ /*----------------------------------------------------------------------
+ | CResourceFinder
+ +---------------------------------------------------------------------*/
+CResourceFinder::CResourceFinder(const char* protocol, const char* content)
+ : m_Protocol(protocol)
+ , m_Content(content)
+{
+}
+
+bool CResourceFinder::operator()(const PLT_MediaItemResource& resource) const {
+ if (m_Content.IsEmpty())
+ return (resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0);
+ else
+ return ((resource.m_ProtocolInfo.GetProtocol().Compare(m_Protocol, true) == 0)
+ && resource.m_ProtocolInfo.GetContentType().StartsWith(m_Content, true));
+}
+
+/*----------------------------------------------------------------------
+| PopulateObjectFromTag
++---------------------------------------------------------------------*/
+NPT_Result
+PopulateObjectFromTag(CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ if (!tag.GetURL().empty() && file_path)
+ *file_path = tag.GetURL().c_str();
+
+ std::vector<std::string> genres = tag.GetGenre();
+ for (unsigned int index = 0; index < genres.size(); index++)
+ object.m_Affiliation.genres.Add(genres.at(index).c_str());
+ object.m_Title = tag.GetTitle().c_str();
+ object.m_Affiliation.album = tag.GetAlbum().c_str();
+ for (unsigned int index = 0; index < tag.GetArtist().size(); index++)
+ {
+ object.m_People.artists.Add(tag.GetArtist().at(index).c_str());
+ object.m_People.artists.Add(tag.GetArtist().at(index).c_str(), "Performer");
+ }
+ object.m_People.artists.Add((!tag.GetAlbumArtistString().empty() ? tag.GetAlbumArtistString() : tag.GetArtistString()).c_str(), "AlbumArtist");
+ if(tag.GetAlbumArtistString().empty())
+ object.m_Creator = tag.GetArtistString().c_str();
+ else
+ object.m_Creator = tag.GetAlbumArtistString().c_str();
+ object.m_MiscInfo.original_track_number = tag.GetTrackNumber();
+ if(tag.GetDatabaseId() >= 0) {
+ object.m_ReferenceID = NPT_String::Format("musicdb://songs/%i%s", tag.GetDatabaseId(), URIUtils::GetExtension(tag.GetURL()).c_str());
+ }
+ if (object.m_ReferenceID == object.m_ObjectID)
+ object.m_ReferenceID = "";
+
+ object.m_MiscInfo.last_time = tag.GetLastPlayed().GetAsW3CDateTime().c_str();
+ object.m_MiscInfo.play_count = tag.GetPlayCount();
+
+ if (resource) resource->m_Duration = tag.GetDuration();
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| PopulateObjectFromTag
++---------------------------------------------------------------------*/
+NPT_Result
+PopulateObjectFromTag(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ if (!tag.m_strFileNameAndPath.empty() && file_path)
+ *file_path = tag.m_strFileNameAndPath.c_str();
+
+ if (tag.m_iDbId != -1 ) {
+ if (tag.m_type == MediaTypeMusicVideo) {
+ object.m_ObjectClass.type = "object.item.videoItem.musicVideoClip";
+ object.m_Creator = StringUtils::Join(tag.m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator).c_str();
+ for (const auto& itArtist : tag.m_artist)
+ object.m_People.artists.Add(itArtist.c_str());
+ object.m_Affiliation.album = tag.m_strAlbum.c_str();
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Date = tag.GetPremiered().GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://musicvideos/titles/%i", tag.m_iDbId);
+ } else if (tag.m_type == MediaTypeMovie) {
+ object.m_ObjectClass.type = "object.item.videoItem.movie";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Date = tag.GetPremiered().GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://movies/titles/%i", tag.m_iDbId);
+ } else {
+ object.m_Recorded.series_title = tag.m_strShowTitle.c_str();
+
+ if (tag.m_type == MediaTypeTvShow) {
+ object.m_ObjectClass.type = "object.container.album.videoAlbum.videoBroadcastShow";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Recorded.episode_number = tag.m_iEpisode;
+ object.m_Recorded.episode_count = tag.m_iEpisode;
+ if (!tag.m_premiered.IsValid() && tag.GetYear() > 0)
+ object.m_Date = CDateTime(tag.GetYear(), 1, 1, 0, 0, 0).GetAsW3CDate().c_str();
+ else
+ object.m_Date = tag.m_premiered.GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i", tag.m_iDbId);
+ } else if (tag.m_type == MediaTypeSeason) {
+ object.m_ObjectClass.type = "object.container.album.videoAlbum.videoBroadcastSeason";
+ object.m_Title = tag.m_strTitle.c_str();
+ object.m_Recorded.episode_season = tag.m_iSeason;
+ object.m_Recorded.episode_count = tag.m_iEpisode;
+ if (!tag.m_premiered.IsValid() && tag.GetYear() > 0)
+ object.m_Date = CDateTime(tag.GetYear(), 1, 1, 0, 0, 0).GetAsW3CDate().c_str();
+ else
+ object.m_Date = tag.m_premiered.GetAsW3CDate().c_str();
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i/%i", tag.m_iIdShow, tag.m_iSeason);
+ } else {
+ object.m_ObjectClass.type = "object.item.videoItem.videoBroadcast";
+ object.m_Recorded.program_title = "S" + ("0" + NPT_String::FromInteger(tag.m_iSeason)).Right(2);
+ object.m_Recorded.program_title += "E" + ("0" + NPT_String::FromInteger(tag.m_iEpisode)).Right(2);
+ object.m_Recorded.program_title += (" : " + tag.m_strTitle).c_str();
+ object.m_Recorded.episode_number = tag.m_iEpisode;
+ object.m_Recorded.episode_season = tag.m_iSeason;
+ object.m_Title = object.m_Recorded.series_title + " - " + object.m_Recorded.program_title;
+ object.m_ReferenceID = NPT_String::Format("videodb://tvshows/titles/%i/%i/%i", tag.m_iIdShow, tag.m_iSeason, tag.m_iDbId);
+ object.m_Date = tag.m_firstAired.GetAsW3CDate().c_str();
+ }
+ }
+ }
+
+ if(quirks & ECLIENTQUIRKS_BASICVIDEOCLASS)
+ object.m_ObjectClass.type = "object.item.videoItem";
+
+ if(object.m_ReferenceID == object.m_ObjectID)
+ object.m_ReferenceID = "";
+
+ for (unsigned int index = 0; index < tag.m_studio.size(); index++)
+ object.m_People.publisher.Add(tag.m_studio[index].c_str());
+
+ object.m_XbmcInfo.date_added = tag.m_dateAdded.GetAsW3CDate().c_str();
+ object.m_XbmcInfo.rating = tag.GetRating().rating;
+ object.m_XbmcInfo.votes = tag.GetRating().votes;
+ object.m_XbmcInfo.unique_identifier = tag.GetUniqueID().c_str();
+ for (const auto& country : tag.m_country)
+ object.m_XbmcInfo.countries.Add(country.c_str());
+ object.m_XbmcInfo.user_rating = tag.m_iUserRating;
+
+ for (unsigned int index = 0; index < tag.m_genre.size(); index++)
+ object.m_Affiliation.genres.Add(tag.m_genre.at(index).c_str());
+
+ for (CVideoInfoTag::iCast it = tag.m_cast.begin(); it != tag.m_cast.end(); ++it)
+ {
+ object.m_People.actors.Add(it->strName.c_str(), it->strRole.c_str());
+ }
+
+ for (unsigned int index = 0; index < tag.m_director.size(); index++)
+ object.m_People.directors.Add(tag.m_director[index].c_str());
+
+ for (unsigned int index = 0; index < tag.m_writingCredits.size(); index++)
+ object.m_People.authors.Add(tag.m_writingCredits[index].c_str());
+
+ object.m_Description.description = tag.m_strTagLine.c_str();
+ object.m_Description.long_description = tag.m_strPlot.c_str();
+ object.m_Description.rating = tag.m_strMPAARating.c_str();
+ object.m_MiscInfo.last_position = (NPT_UInt32)tag.GetResumePoint().timeInSeconds;
+ object.m_XbmcInfo.last_playerstate = tag.GetResumePoint().playerState.c_str();
+ object.m_MiscInfo.last_time = tag.m_lastPlayed.GetAsW3CDateTime().c_str();
+ object.m_MiscInfo.play_count = tag.GetPlayCount();
+ if (resource) {
+ resource->m_Duration = tag.GetDuration();
+ if (tag.HasStreamDetails()) {
+ const CStreamDetails &details = tag.m_streamDetails;
+ resource->m_Resolution = NPT_String::FromInteger(details.GetVideoWidth()) + "x" + NPT_String::FromInteger(details.GetVideoHeight());
+ resource->m_NbAudioChannels = details.GetAudioChannels();
+ }
+ }
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| BuildObject
++---------------------------------------------------------------------*/
+PLT_MediaObject*
+BuildObject(CFileItem& item,
+ NPT_String& file_path,
+ bool with_count,
+ NPT_Reference<CThumbLoader>& thumb_loader,
+ const PLT_HttpRequestContext* context /* = NULL */,
+ CUPnPServer* upnp_server /* = NULL */,
+ UPnPService upnp_service /* = UPnPServiceNone */)
+{
+ static Logger logger = CServiceBroker::GetLogging().GetLogger("UPNP::BuildObject");
+
+ PLT_MediaItemResource resource;
+ PLT_MediaObject* object = NULL;
+ std::string thumb;
+
+ logger->debug("Building didl for object '{}'", item.GetPath());
+
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return nullptr;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return nullptr;
+
+ EClientQuirks quirks = GetClientQuirks(context);
+
+ // get list of ip addresses
+ NPT_List<NPT_IpAddress> ips;
+ NPT_HttpUrl rooturi;
+ NPT_CHECK_LABEL(PLT_UPnPMessageHelper::GetIPAddresses(ips), failure);
+
+ // if we're passed an interface where we received the request from
+ // move the ip to the top
+ if (context && context->GetLocalAddress().GetIpAddress().ToString() != "0.0.0.0")
+ {
+ rooturi = NPT_HttpUrl(context->GetLocalAddress().GetIpAddress().ToString(),
+ context->GetLocalAddress().GetPort(), "/");
+ ips.Remove(context->GetLocalAddress().GetIpAddress());
+ ips.Insert(ips.GetFirstItem(), context->GetLocalAddress().GetIpAddress());
+ } else if(upnp_server) {
+ rooturi = NPT_HttpUrl("localhost", upnp_server->GetPort(), "/");
+ }
+
+ if (!item.m_bIsFolder) {
+ object = new PLT_MediaItem();
+ object->m_ObjectID = item.GetPath().c_str();
+
+ /* Setup object type */
+ if (item.IsMusicDb() || item.IsAudio()) {
+ object->m_ObjectClass.type = "object.item.audioItem.musicTrack";
+
+ if (item.HasMusicInfoTag()) {
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ PopulateObjectFromTag(*tag, *object, &file_path, &resource, quirks, upnp_service);
+ }
+ } else if (item.IsVideoDb() || item.IsVideo()) {
+ object->m_ObjectClass.type = "object.item.videoItem";
+
+ if(quirks & ECLIENTQUIRKS_UNKNOWNSERIES)
+ object->m_Affiliation.album = "[Unknown Series]";
+
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *object, &file_path, &resource, quirks, upnp_service);
+ }
+ } else if (item.IsPicture()) {
+ object->m_ObjectClass.type = "object.item.imageItem.photo";
+ } else {
+ object->m_ObjectClass.type = "object.item";
+ }
+
+ // duration of zero is invalid
+ if (resource.m_Duration == 0) resource.m_Duration = -1;
+
+ // Set the resource file size
+ resource.m_Size = item.m_dwSize;
+ if(resource.m_Size == 0)
+ resource.m_Size = (NPT_LargeSize)-1;
+
+ // set date
+ if (object->m_Date.IsEmpty() && item.m_dateTime.IsValid()) {
+ object->m_Date = item.m_dateTime.GetAsW3CDate().c_str();
+ }
+
+ if (upnp_server) {
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, file_path, GetProtocolInfo(item, "http", context));
+ }
+
+ // if the item is remote, add a direct link to the item
+ if (URIUtils::IsRemote((const char*)file_path)) {
+ resource.m_ProtocolInfo = PLT_ProtocolInfo(GetProtocolInfo(item, item.GetURL().GetProtocol().c_str(), context));
+ resource.m_Uri = file_path;
+
+ // if the direct link can be served directly using http, then push it in front
+ // otherwise keep the xbmc-get resource last and let a compatible client look for it
+ if (resource.m_ProtocolInfo.ToString().StartsWith("xbmc", true)) {
+ object->m_Resources.Add(resource);
+ } else {
+ object->m_Resources.Insert(object->m_Resources.GetFirstItem(), resource);
+ }
+ }
+
+ // copy across the known metadata
+ for(unsigned i=0; i<object->m_Resources.GetItemCount(); i++) {
+ object->m_Resources[i].m_Size = resource.m_Size;
+ object->m_Resources[i].m_Duration = resource.m_Duration;
+ object->m_Resources[i].m_Resolution = resource.m_Resolution;
+ }
+
+ // Some upnp clients expect all audio items to have parent root id 4
+#ifdef WMP_ID_MAPPING
+ object->m_ParentID = "4";
+#endif
+ } else {
+ PLT_MediaContainer* container = new PLT_MediaContainer;
+ object = container;
+
+ /* Assign a title and id for this container */
+ container->m_ObjectID = item.GetPath().c_str();
+ container->m_ObjectClass.type = "object.container";
+ container->m_ChildrenCount = -1;
+
+ /* this might be overkill, but hey */
+ if (item.IsMusicDb()) {
+ MUSICDATABASEDIRECTORY::NODE_TYPE node = CMusicDatabaseDirectory::GetDirectoryType(item.GetPath());
+ switch(node) {
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ARTIST: {
+ container->m_ObjectClass.type += ".person.musicArtist";
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ if (tag) {
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(tag->GetArtistString()).c_str(), "Performer");
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack((!tag->GetAlbumArtistString().empty() ? tag->GetAlbumArtistString() : tag->GetArtistString())).c_str(), "AlbumArtist");
+ }
+#ifdef WMP_ID_MAPPING
+ // Some upnp clients expect all artists to have parent root id 107
+ container->m_ParentID = "107";
+#endif
+ }
+ break;
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM:
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_ALBUM_RECENTLY_ADDED: {
+ container->m_ObjectClass.type += ".album.musicAlbum";
+ // for Sonos to be happy
+ CMusicInfoTag *tag = item.GetMusicInfoTag();
+ if (tag) {
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(tag->GetArtistString()).c_str(), "Performer");
+ container->m_People.artists.Add(
+ CorrectAllItemsSortHack(!tag->GetAlbumArtistString().empty() ? tag->GetAlbumArtistString() : tag->GetArtistString()).c_str(), "AlbumArtist");
+ container->m_Affiliation.album = CorrectAllItemsSortHack(tag->GetAlbum()).c_str();
+ }
+#ifdef WMP_ID_MAPPING
+ // Some upnp clients expect all albums to have parent root id 7
+ container->m_ParentID = "7";
+#endif
+ }
+ break;
+ case MUSICDATABASEDIRECTORY::NODE_TYPE_GENRE:
+ container->m_ObjectClass.type += ".genre.musicGenre";
+ break;
+ default:
+ break;
+ }
+ } else if (item.IsVideoDb()) {
+ VIDEODATABASEDIRECTORY::NODE_TYPE node = CVideoDatabaseDirectory::GetDirectoryType(item.GetPath());
+ CVideoInfoTag &tag = *item.GetVideoInfoTag();
+ switch(node) {
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_GENRE:
+ container->m_ObjectClass.type += ".genre.movieGenre";
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_ACTOR:
+ container->m_ObjectClass.type += ".person.videoArtist";
+ container->m_Creator =
+ StringUtils::Join(
+ tag.m_artist,
+ settingsComponent->GetAdvancedSettings()->m_videoItemSeparator)
+ .c_str();
+ container->m_Title = tag.m_strTitle.c_str();
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_SEASONS:
+ container->m_ObjectClass.type += ".album.videoAlbum.videoBroadcastSeason";
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = (CVideoInfoTag*)item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *container, &file_path, &resource, quirks);
+ }
+ break;
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_TVSHOWS:
+ container->m_ObjectClass.type += ".album.videoAlbum.videoBroadcastShow";
+ if (item.HasVideoInfoTag()) {
+ CVideoInfoTag *tag = (CVideoInfoTag*)item.GetVideoInfoTag();
+ PopulateObjectFromTag(*tag, *container, &file_path, &resource, quirks);
+ }
+ break;
+ default:
+ container->m_ObjectClass.type += ".storageFolder";
+ break;
+ }
+ } else if (item.IsPlayList() || item.IsSmartPlayList()) {
+ container->m_ObjectClass.type += ".playlistContainer";
+ }
+
+ if(quirks & ECLIENTQUIRKS_ONLYSTORAGEFOLDER) {
+ container->m_ObjectClass.type = "object.container.storageFolder";
+ }
+
+ /* Get the number of children for this container */
+ if (with_count && upnp_server) {
+ if (object->m_ObjectID.StartsWith("virtualpath://")) {
+ NPT_LargeSize count = 0;
+ NPT_CHECK_LABEL(NPT_File::GetSize(file_path, count), failure);
+ container->m_ChildrenCount = (NPT_Int32)count;
+ } else {
+ /* this should be a standard path */
+ //! @todo - get file count of this directory
+ }
+ }
+ }
+
+ // set a title for the object
+ if (object->m_Title.IsEmpty()) {
+ if (!item.GetLabel().empty()) {
+ std::string title = item.GetLabel();
+ if (item.IsPlayList() || !item.m_bIsFolder) URIUtils::RemoveExtension(title);
+ object->m_Title = title.c_str();
+ }
+ }
+
+ if (upnp_server) {
+ // determine the correct artwork for this item
+ if (!thumb_loader.IsNull())
+ thumb_loader->LoadItem(&item);
+
+ // we have to decide the best art type to serve to the client - use ContentUtils
+ // to get it since it depends on the mediatype of the item being served
+ thumb = ContentUtils::GetPreferredArtImage(item);
+
+ if (!thumb.empty()) {
+ PLT_AlbumArtInfo art;
+ // Set DLNA profileID by extension, defaulting to JPEG.
+ if (URIUtils::HasExtension(thumb, ".png"))
+ {
+ art.dlna_profile = "PNG_TN";
+ }
+ else
+ {
+ art.dlna_profile = "JPEG_TN";
+ }
+ // append /thumb to the safe resource uri to avoid clients flagging the item with
+ // the incorrect mimetype (derived from the file extension)
+ art.uri = upnp_server->BuildSafeResourceUri(
+ rooturi, (*ips.GetFirstItem()).ToString(),
+ std::string(CTextureUtils::GetWrappedImageURL(thumb) + "/thumb").c_str());
+ object->m_ExtraInfo.album_arts.Add(art);
+ }
+
+ for (const auto& itArtwork : item.GetArt())
+ {
+ if (!itArtwork.first.empty() && !itArtwork.second.empty())
+ {
+ std::string wrappedUrl = CTextureUtils::GetWrappedImageURL(itArtwork.second);
+ object->m_XbmcInfo.artwork.Add(
+ itArtwork.first.c_str(),
+ upnp_server->BuildSafeResourceUri(rooturi, (*ips.GetFirstItem()).ToString(),
+ wrappedUrl.c_str()));
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, wrappedUrl.c_str(),
+ ("xbmc.org:*:" + itArtwork.first + ":*").c_str());
+ }
+ }
+ }
+
+ // look for and add external subtitle if we are processing a video file and
+ // we are being called by a UPnP player or renderer or the user has chosen
+ // to look for external subtitles
+ if (upnp_server != NULL && item.IsVideo() &&
+ (upnp_service == UPnPPlayer || upnp_service == UPnPRenderer ||
+ settings->GetBool(CSettings::SETTING_SERVICES_UPNPLOOKFOREXTERNALSUBTITLES)))
+ {
+ // find any available external subtitles
+ std::vector<std::string> filenames;
+ std::vector<std::string> subtitles;
+ CUtil::ScanForExternalSubtitles(file_path.GetChars(), filenames);
+
+ std::string ext;
+ for (unsigned int i = 0; i < filenames.size(); i++)
+ {
+ ext = URIUtils::GetExtension(filenames[i]).c_str();
+ ext = ext.substr(1);
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+ /* Hardcoded check for extension is not the best way, but it can't be allowed to pass all
+ subtitle extension (ex. rar or zip). There are the most popular extensions support by UPnP devices.*/
+ for (std::string_view type : SupportedSubFormats)
+ {
+ if (type == ext)
+ {
+ subtitles.push_back(filenames[i]);
+ }
+ }
+ }
+
+ std::string subtitlePath;
+
+ if (subtitles.size() == 1)
+ {
+ subtitlePath = subtitles[0];
+ }
+ else if (!subtitles.empty())
+ {
+ std::string preferredLanguage{"en"};
+
+ /* trying to find subtitle with preferred language settings */
+ auto setting = settings->GetSetting("locale.subtitlelanguage");
+ if (!setting)
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", "locale.subtitlelanguage");
+ else
+ preferredLanguage = setting->ToString();
+
+ std::string preferredLanguageCode;
+ g_LangCodeExpander.ConvertToISO6392B(preferredLanguage, preferredLanguageCode);
+
+ for (unsigned int i = 0; i < subtitles.size(); i++)
+ {
+ ExternalStreamInfo info =
+ CUtil::GetExternalStreamDetailsFromFilename(file_path.GetChars(), subtitles[i]);
+
+ if (preferredLanguageCode == info.language)
+ {
+ subtitlePath = subtitles[i];
+ break;
+ }
+ }
+ /* if not found subtitle with preferred language, get the first one */
+ if (subtitlePath.empty())
+ {
+ subtitlePath = subtitles[0];
+ }
+ }
+
+ if (!subtitlePath.empty())
+ {
+ /* subtitles are added as 2 resources, 2 sec resources and 1 addon to video resource, to be compatible with
+ the most of the devices; all UPnP devices take the last one it could handle,
+ and skip ones it doesn't "understand" */
+ // add subtitle resource with standard protocolInfo
+ NPT_String protocolInfo = GetProtocolInfo(CFileItem(subtitlePath, false), "http", context);
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, NPT_String(subtitlePath.c_str()), protocolInfo);
+ // add subtitle resource with smi/caption protocol info (some devices)
+ PLT_ProtocolInfo protInfo = PLT_ProtocolInfo(protocolInfo);
+ protocolInfo = protInfo.GetProtocol() + ":" + protInfo.GetMask() + ":smi/caption:" + protInfo.GetExtra();
+ upnp_server->AddSafeResourceUri(object, rooturi, ips, NPT_String(subtitlePath.c_str()), protocolInfo);
+
+ ext = URIUtils::GetExtension(subtitlePath).c_str();
+ ext = ext.substr(1);
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+
+ NPT_String subtitle_uri = object->m_Resources[object->m_Resources.GetItemCount() - 1].m_Uri;
+
+ // add subtitle to video resource (the first one) (for some devices)
+ object->m_Resources[0].m_CustomData["xmlns:pv"] = "http://www.pv.com/pvns/";
+ object->m_Resources[0].m_CustomData["pv:subtitleFileUri"] = subtitle_uri;
+ object->m_Resources[0].m_CustomData["pv:subtitleFileType"] = ext.c_str();
+
+ // for samsung devices
+ PLT_SecResource sec_res;
+ sec_res.name = "CaptionInfoEx";
+ sec_res.value = subtitle_uri;
+ sec_res.attributes["type"] = ext.c_str();
+ object->m_SecResources.Add(sec_res);
+ sec_res.name = "CaptionInfo";
+ object->m_SecResources.Add(sec_res);
+
+ // adding subtitle uri for movie md5, for later use in http response
+ NPT_String movie_md5 = object->m_Resources[0].m_Uri;
+ movie_md5 = movie_md5.Right(movie_md5.GetLength() - movie_md5.Find("/%25/") - 5);
+ upnp_server->AddSubtitleUriForSecResponse(movie_md5, subtitle_uri);
+ }
+ }
+
+ return object;
+
+failure:
+ delete object;
+ return NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::CorrectAllItemsSortHack
++---------------------------------------------------------------------*/
+const std::string&
+CorrectAllItemsSortHack(const std::string &item)
+{
+ // This is required as in order for the "* All Albums" etc. items to sort
+ // correctly, they must have fake artist/album etc. information generated.
+ // This looks nasty if we attempt to render it to the GUI, thus this (further)
+ // workaround
+ if ((item.size() == 1 && item[0] == 0x01) || (item.size() > 1 && ((unsigned char) item[1]) == 0xff))
+ return StringUtils::Empty;
+
+ return item;
+}
+
+int
+PopulateTagFromObject(CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource /* = NULL */,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ tag.SetTitle((const char*)object.m_Title);
+ tag.SetArtist((const char*)object.m_Creator);
+ for(PLT_PersonRoles::Iterator it = object.m_People.artists.GetFirstItem(); it; it++) {
+ if (it->role == "") tag.SetArtist((const char*)it->name);
+ else if(it->role == "Performer") tag.SetArtist((const char*)it->name);
+ else if(it->role == "AlbumArtist") tag.SetAlbumArtist((const char*)it->name);
+ }
+ tag.SetTrackNumber(object.m_MiscInfo.original_track_number);
+
+ for (NPT_List<NPT_String>::Iterator it = object.m_Affiliation.genres.GetFirstItem(); it; it++) {
+ // ignore single "Unknown" genre inserted by Platinum
+ if (it == object.m_Affiliation.genres.GetFirstItem() && object.m_Affiliation.genres.GetItemCount() == 1 &&
+ *it == "Unknown")
+ break;
+
+ tag.SetGenre((const char*) *it);
+ }
+
+ tag.SetAlbum((const char*)object.m_Affiliation.album);
+ CDateTime last;
+ last.SetFromW3CDateTime((const char*)object.m_MiscInfo.last_time);
+ tag.SetLastPlayed(last);
+ tag.SetPlayCount(object.m_MiscInfo.play_count);
+ if(resource)
+ tag.SetDuration(resource->m_Duration);
+ tag.SetLoaded();
+ return NPT_SUCCESS;
+}
+
+int
+PopulateTagFromObject(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource /* = NULL */,
+ UPnPService service /* = UPnPServiceNone */)
+{
+ CDateTime date;
+ date.SetFromW3CDate((const char*)object.m_Date);
+
+ if(!object.m_Recorded.program_title.IsEmpty() || object.m_ObjectClass.type == "object.item.videoItem.videoBroadcast")
+ {
+ tag.m_type = MediaTypeEpisode;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ if (date.IsValid())
+ tag.m_firstAired = date;
+
+ int title = object.m_Recorded.program_title.Find(" : ");
+ if (title >= 0)
+ tag.m_strTitle = object.m_Recorded.program_title.SubString(title + 3);
+ else
+ tag.m_strTitle = object.m_Recorded.program_title;
+
+ int episode;
+ int season;
+ if (object.m_Recorded.episode_number > 0 && object.m_Recorded.episode_season < (NPT_UInt32)-1) {
+ tag.m_iEpisode = object.m_Recorded.episode_number;
+ tag.m_iSeason = object.m_Recorded.episode_season;
+ } else if(sscanf(object.m_Recorded.program_title, "S%2dE%2d", &season, &episode) == 2 && title >= 0) {
+ tag.m_iEpisode = episode;
+ tag.m_iSeason = season;
+ } else {
+ tag.m_iSeason = object.m_Recorded.episode_number / 100;
+ tag.m_iEpisode = object.m_Recorded.episode_number % 100;
+ }
+ }
+ else {
+ tag.m_strTitle = object.m_Title;
+ if (date.IsValid())
+ tag.m_premiered = date;
+
+ if (!object.m_Recorded.series_title.IsEmpty()) {
+ if (object.m_ObjectClass.type == "object.container.album.videoAlbum.videoBroadcastSeason") {
+ tag.m_type = MediaTypeSeason;
+ tag.m_iSeason = object.m_Recorded.episode_season;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ }
+ else {
+ tag.m_type = MediaTypeTvShow;
+ tag.m_strShowTitle = object.m_Title;
+ }
+
+ if (object.m_Recorded.episode_count > 0)
+ tag.m_iEpisode = object.m_Recorded.episode_count;
+ else
+ tag.m_iEpisode = object.m_Recorded.episode_number;
+ }
+ else if(object.m_ObjectClass.type == "object.item.videoItem.musicVideoClip") {
+ tag.m_type = MediaTypeMusicVideo;
+
+ if (object.m_People.artists.GetItemCount() > 0) {
+ for (unsigned int index = 0; index < object.m_People.artists.GetItemCount(); index++)
+ tag.m_artist.emplace_back(object.m_People.artists.GetItem(index)->name.GetChars());
+ }
+ else if (!object.m_Creator.IsEmpty() && object.m_Creator != "Unknown")
+ tag.m_artist = StringUtils::Split(object.m_Creator.GetChars(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ tag.m_strAlbum = object.m_Affiliation.album;
+ }
+ else
+ tag.m_type = MediaTypeMovie;
+
+ tag.m_strTitle = object.m_Title;
+ if (date.IsValid())
+ tag.SetPremiered(date);
+ }
+
+ for (unsigned int index = 0; index < object.m_People.publisher.GetItemCount(); index++)
+ tag.m_studio.emplace_back(object.m_People.publisher.GetItem(index)->GetChars());
+
+ tag.m_dateAdded.SetFromW3CDate((const char*)object.m_XbmcInfo.date_added);
+ tag.SetRating(object.m_XbmcInfo.rating, object.m_XbmcInfo.votes);
+ tag.SetUniqueID(object.m_XbmcInfo.unique_identifier.GetChars());
+ for (unsigned int index = 0; index < object.m_XbmcInfo.countries.GetItemCount(); index++)
+ tag.m_country.emplace_back(object.m_XbmcInfo.countries.GetItem(index)->GetChars());
+ tag.m_iUserRating = object.m_XbmcInfo.user_rating;
+
+ for (unsigned int index = 0; index < object.m_Affiliation.genres.GetItemCount(); index++)
+ {
+ // ignore single "Unknown" genre inserted by Platinum
+ if (index == 0 && object.m_Affiliation.genres.GetItemCount() == 1 &&
+ *object.m_Affiliation.genres.GetItem(index) == "Unknown")
+ break;
+
+ tag.m_genre.emplace_back(object.m_Affiliation.genres.GetItem(index)->GetChars());
+ }
+ for (unsigned int index = 0; index < object.m_People.directors.GetItemCount(); index++)
+ tag.m_director.emplace_back(object.m_People.directors.GetItem(index)->name.GetChars());
+ for (unsigned int index = 0; index < object.m_People.authors.GetItemCount(); index++)
+ tag.m_writingCredits.emplace_back(object.m_People.authors.GetItem(index)->name.GetChars());
+ for (unsigned int index = 0; index < object.m_People.actors.GetItemCount(); index++)
+ {
+ SActorInfo info;
+ info.strName = object.m_People.actors.GetItem(index)->name;
+ info.strRole = object.m_People.actors.GetItem(index)->role;
+ tag.m_cast.push_back(info);
+ }
+ tag.m_strTagLine = object.m_Description.description;
+ tag.m_strPlot = object.m_Description.long_description;
+ tag.m_strMPAARating = object.m_Description.rating;
+ tag.m_strShowTitle = object.m_Recorded.series_title;
+ tag.m_lastPlayed.SetFromW3CDateTime((const char*)object.m_MiscInfo.last_time);
+ tag.SetPlayCount(object.m_MiscInfo.play_count);
+
+ if(resource)
+ {
+ if (resource->m_Duration)
+ tag.SetDuration(resource->m_Duration);
+ if (object.m_MiscInfo.last_position > 0 )
+ {
+ tag.SetResumePoint(object.m_MiscInfo.last_position,
+ resource->m_Duration,
+ object.m_XbmcInfo.last_playerstate.GetChars());
+ }
+ if (!resource->m_Resolution.IsEmpty())
+ {
+ int width, height;
+ if (sscanf(resource->m_Resolution, "%dx%d", &width, &height) == 2)
+ {
+ CStreamDetailVideo* detail = new CStreamDetailVideo;
+ detail->m_iWidth = width;
+ detail->m_iHeight = height;
+ detail->m_iDuration = tag.GetDuration();
+ tag.m_streamDetails.AddStream(detail);
+ }
+ }
+ if (resource->m_NbAudioChannels > 0)
+ {
+ CStreamDetailAudio* detail = new CStreamDetailAudio;
+ detail->m_iChannels = resource->m_NbAudioChannels;
+ tag.m_streamDetails.AddStream(detail);
+ }
+ }
+ return NPT_SUCCESS;
+}
+
+std::shared_ptr<CFileItem> BuildObject(PLT_MediaObject* entry,
+ UPnPService upnp_service /* = UPnPServiceNone */)
+{
+ NPT_String ObjectClass = entry->m_ObjectClass.type.ToLowercase();
+
+ CFileItemPtr pItem(new CFileItem((const char*)entry->m_Title));
+ pItem->SetLabelPreformatted(true);
+ pItem->m_strTitle = (const char*)entry->m_Title;
+ pItem->m_bIsFolder = entry->IsContainer();
+
+ // if it's a container, format a string as upnp://uuid/object_id
+ if (pItem->m_bIsFolder) {
+
+ // look for metadata
+ if( ObjectClass.StartsWith("object.container.album.videoalbum") ) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *entry, NULL, upnp_service);
+
+ } else if( ObjectClass.StartsWith("object.container.album.photoalbum")) {
+ //CPictureInfoTag* tag = pItem->GetPictureInfoTag();
+
+ } else if( ObjectClass.StartsWith("object.container.album") ) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *entry, NULL, upnp_service);
+ }
+
+ } else {
+ bool audio = false
+ , image = false
+ , video = false;
+ // set a general content type
+ const char* content = NULL;
+ if (ObjectClass.StartsWith("object.item.videoitem")) {
+ pItem->SetMimeType("video/octet-stream");
+ content = "video";
+ video = true;
+ }
+ else if(ObjectClass.StartsWith("object.item.audioitem")) {
+ pItem->SetMimeType("audio/octet-stream");
+ content = "audio";
+ audio = true;
+ }
+ else if(ObjectClass.StartsWith("object.item.imageitem")) {
+ pItem->SetMimeType("image/octet-stream");
+ content = "image";
+ image = true;
+ }
+
+ // attempt to find a valid resource (may be multiple)
+ PLT_MediaItemResource resource, *res = NULL;
+ if(NPT_SUCCEEDED(NPT_ContainerFind(entry->m_Resources,
+ CResourceFinder("http-get", content), resource))) {
+
+ // set metadata
+ if (resource.m_Size != (NPT_LargeSize)-1) {
+ pItem->m_dwSize = resource.m_Size;
+ }
+ res = &resource;
+ }
+ // look for metadata
+ if(video) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetVideoInfoTag(), *entry, res, upnp_service);
+
+ } else if(audio) {
+ pItem->SetLabelPreformatted(false);
+ UPNP::PopulateTagFromObject(*pItem->GetMusicInfoTag(), *entry, res, upnp_service);
+
+ } else if(image) {
+ //! @todo fill pictureinfotag?
+ GetResource(entry, *pItem);
+ }
+ }
+
+ // look for date?
+ if(entry->m_Description.date.GetLength()) {
+ KODI::TIME::SystemTime time = {};
+ sscanf(entry->m_Description.date, "%hu-%hu-%huT%hu:%hu:%hu", &time.year, &time.month, &time.day,
+ &time.hour, &time.minute, &time.second);
+ pItem->m_dateTime = time;
+ }
+
+ // if there is a thumbnail available set it here
+ if(entry->m_ExtraInfo.album_arts.GetItem(0))
+ // only considers first album art
+ pItem->SetArt("thumb", (const char*) entry->m_ExtraInfo.album_arts.GetItem(0)->uri);
+ else if(entry->m_Description.icon_uri.GetLength())
+ pItem->SetArt("thumb", (const char*) entry->m_Description.icon_uri);
+
+ for (unsigned int index = 0; index < entry->m_XbmcInfo.artwork.GetItemCount(); index++)
+ pItem->SetArt(entry->m_XbmcInfo.artwork.GetItem(index)->type.GetChars(),
+ entry->m_XbmcInfo.artwork.GetItem(index)->url.GetChars());
+
+ // set the watched overlay, as this will not be set later due to
+ // content set on file item list
+ if (pItem->HasVideoInfoTag()) {
+ int episodes = pItem->GetVideoInfoTag()->m_iEpisode;
+ int played = pItem->GetVideoInfoTag()->GetPlayCount();
+ const std::string& type = pItem->GetVideoInfoTag()->m_type;
+ bool watched(false);
+ if (type == MediaTypeTvShow || type == MediaTypeSeason) {
+ pItem->SetProperty("totalepisodes", episodes);
+ pItem->SetProperty("numepisodes", episodes);
+ pItem->SetProperty("watchedepisodes", played);
+ pItem->SetProperty("unwatchedepisodes", episodes - played);
+ pItem->SetProperty("watchedepisodepercent", episodes > 0 ? played * 100 / episodes : 0);
+ watched = (episodes && played >= episodes);
+ pItem->GetVideoInfoTag()->SetPlayCount(watched ? 1 : 0);
+ }
+ else if (type == MediaTypeEpisode || type == MediaTypeMovie)
+ watched = (played > 0);
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, watched);
+ }
+ return pItem;
+}
+
+struct ResourcePrioritySort
+{
+ explicit ResourcePrioritySort(const PLT_MediaObject* entry)
+ {
+ if (entry->m_ObjectClass.type.StartsWith("object.item.audioItem"))
+ m_content = "audio";
+ else if (entry->m_ObjectClass.type.StartsWith("object.item.imageItem"))
+ m_content = "image";
+ else if (entry->m_ObjectClass.type.StartsWith("object.item.videoItem"))
+ m_content = "video";
+ }
+
+ int GetPriority(const PLT_MediaItemResource& res) const
+ {
+ int prio = 0;
+
+ if (m_content != "" && res.m_ProtocolInfo.GetContentType().StartsWith(m_content))
+ prio += 400;
+
+ NPT_Url url(res.m_Uri);
+ if (URIUtils::IsHostOnLAN((const char*)url.GetHost(), false))
+ prio += 300;
+
+ if (res.m_ProtocolInfo.GetProtocol() == "xbmc-get")
+ prio += 200;
+ else if (res.m_ProtocolInfo.GetProtocol() == "http-get")
+ prio += 100;
+
+ return prio;
+ }
+
+ int operator()(const PLT_MediaItemResource& lh, const PLT_MediaItemResource& rh) const
+ {
+ if(GetPriority(lh) < GetPriority(rh))
+ return 1;
+ else
+ return 0;
+ }
+
+ NPT_String m_content;
+};
+
+bool GetResource(const PLT_MediaObject* entry, CFileItem& item)
+{
+ static Logger logger = CServiceBroker::GetLogging().GetLogger("CUPnPDirectory::GetResource");
+
+ PLT_MediaItemResource resource;
+
+ // store original path so we remember it
+ item.SetProperty("original_listitem_url", item.GetPath());
+ item.SetProperty("original_listitem_mime", item.GetMimeType());
+
+ // get a sorted list based on our preference
+ NPT_List<PLT_MediaItemResource> sorted;
+ for (NPT_Cardinal i = 0; i < entry->m_Resources.GetItemCount(); ++i) {
+ sorted.Add(entry->m_Resources[i]);
+ }
+ sorted.Sort(ResourcePrioritySort(entry));
+
+ if(sorted.GetItemCount() == 0)
+ return false;
+
+ resource = *sorted.GetFirstItem();
+
+ // if it's an item, path is the first url to the item
+ // we hope the server made the first one reachable for us
+ // (it could be a format we dont know how to play however)
+ item.SetDynPath((const char*) resource.m_Uri);
+
+ // look for content type in protocol info
+ if (resource.m_ProtocolInfo.IsValid()) {
+ logger->debug("resource protocol info '{}'", (const char*)(resource.m_ProtocolInfo.ToString()));
+
+ if (resource.m_ProtocolInfo.GetContentType().Compare("application/octet-stream") != 0) {
+ item.SetMimeType((const char*)resource.m_ProtocolInfo.GetContentType());
+ }
+
+ // if this is an image fill the thumb of the item
+ if (StringUtils::StartsWithNoCase(resource.m_ProtocolInfo.GetContentType(), "image"))
+ {
+ item.SetArt("thumb", std::string(resource.m_Uri));
+ }
+ } else {
+ logger->error("invalid protocol info '{}'", (const char*)(resource.m_ProtocolInfo.ToString()));
+ }
+
+ // look for subtitles
+ unsigned subIdx = 0;
+
+ for(unsigned r = 0; r < entry->m_Resources.GetItemCount(); r++)
+ {
+ const PLT_MediaItemResource& res = entry->m_Resources[r];
+ const PLT_ProtocolInfo& info = res.m_ProtocolInfo;
+
+ for (std::string_view type : SupportedSubFormats)
+ {
+ if (type == info.GetContentType().Split("/").GetLastItem()->GetChars())
+ {
+ ++subIdx;
+ logger->info("adding subtitle: #{}, type '{}', URI '{}'", subIdx, type,
+ res.m_Uri.GetChars());
+
+ std::string prop = StringUtils::Format("subtitle:{}", subIdx);
+ item.SetProperty(prop, (const char*)res.m_Uri);
+ }
+ }
+ }
+ return true;
+}
+
+std::shared_ptr<CFileItem> GetFileItem(const NPT_String& uri, const NPT_String& meta)
+{
+ PLT_MediaObjectListReference list;
+ PLT_MediaObject* object = NULL;
+ CFileItemPtr item;
+
+ if (NPT_SUCCEEDED(PLT_Didl::FromDidl(meta, list))) {
+ list->Get(0, object);
+ }
+
+ if (object) {
+ item = BuildObject(object);
+ }
+
+ if (item) {
+ item->SetPath((const char*)uri);
+ GetResource(object, *item);
+ } else {
+ item.reset(new CFileItem((const char*)uri, false));
+ }
+ return item;
+}
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPInternal.h b/xbmc/network/upnp/UPnPInternal.h
new file mode 100644
index 0000000..5723e22
--- /dev/null
+++ b/xbmc/network/upnp/UPnPInternal.h
@@ -0,0 +1,122 @@
+/*
+ * 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 <memory>
+#include <string>
+
+#include <Neptune/Source/Core/NptReferences.h>
+#include <Neptune/Source/Core/NptStrings.h>
+#include <Neptune/Source/Core/NptTypes.h>
+
+class CUPnPServer;
+class CFileItem;
+class CThumbLoader;
+class PLT_DeviceData;
+class PLT_HttpRequestContext;
+class PLT_MediaItemResource;
+class PLT_MediaObject;
+class NPT_String;
+namespace MUSIC_INFO {
+ class CMusicInfoTag;
+}
+class CVideoInfoTag;
+
+namespace UPNP
+{
+ enum UPnPService {
+ UPnPServiceNone = 0,
+ UPnPClient,
+ UPnPContentDirectory,
+ UPnPPlayer,
+ UPnPRenderer
+ };
+
+ class CResourceFinder {
+ public:
+ CResourceFinder(const char* protocol, const char* content = NULL);
+ bool operator()(const PLT_MediaItemResource& resource) const;
+ private:
+ NPT_String m_Protocol;
+ NPT_String m_Content;
+ };
+
+ enum EClientQuirks
+ {
+ ECLIENTQUIRKS_NONE = 0x0
+
+ /* Client requires folder's to be marked as storageFolders as vendor type (360)*/
+ , ECLIENTQUIRKS_ONLYSTORAGEFOLDER = 0x01
+
+ /* Client can't handle subtypes for videoItems (360) */
+ , ECLIENTQUIRKS_BASICVIDEOCLASS = 0x02
+
+ /* Client requires album to be set to [Unknown Series] to show title (WMP) */
+ , ECLIENTQUIRKS_UNKNOWNSERIES = 0x04
+ };
+
+ EClientQuirks GetClientQuirks(const PLT_HttpRequestContext* context);
+
+ enum EMediaControllerQuirks
+ {
+ EMEDIACONTROLLERQUIRKS_NONE = 0x00
+
+ /* Media Controller expects MIME type video/x-mkv instead of video/x-matroska (Samsung) */
+ , EMEDIACONTROLLERQUIRKS_X_MKV = 0x01
+ };
+
+ EMediaControllerQuirks GetMediaControllerQuirks(const PLT_DeviceData *device);
+
+ const char* GetMimeTypeFromExtension(const char* extension, const PLT_HttpRequestContext* context = NULL);
+ NPT_String GetMimeType(const CFileItem& item, const PLT_HttpRequestContext* context = NULL);
+ NPT_String GetMimeType(const char* filename, const PLT_HttpRequestContext* context = NULL);
+ const NPT_String GetProtocolInfo(const CFileItem& item, const char* protocol, const PLT_HttpRequestContext* context = NULL);
+
+
+ const std::string& CorrectAllItemsSortHack(const std::string &item);
+
+ NPT_Result PopulateTagFromObject(MUSIC_INFO::CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource = NULL,
+ UPnPService service = UPnPServiceNone);
+
+ NPT_Result PopulateTagFromObject(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ PLT_MediaItemResource* resource = NULL,
+ UPnPService service = UPnPServiceNone);
+
+ NPT_Result PopulateObjectFromTag(MUSIC_INFO::CMusicInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service = UPnPServiceNone);
+
+ NPT_Result PopulateObjectFromTag(CVideoInfoTag& tag,
+ PLT_MediaObject& object,
+ NPT_String* file_path,
+ PLT_MediaItemResource* resource,
+ EClientQuirks quirks,
+ UPnPService service = UPnPServiceNone);
+
+ PLT_MediaObject* BuildObject(CFileItem& item,
+ NPT_String& file_path,
+ bool with_count,
+ NPT_Reference<CThumbLoader>& thumb_loader,
+ const PLT_HttpRequestContext* context = NULL,
+ CUPnPServer* upnp_server = NULL,
+ UPnPService upnp_service = UPnPServiceNone);
+
+ std::shared_ptr<CFileItem> BuildObject(PLT_MediaObject* entry,
+ UPnPService upnp_service = UPnPServiceNone);
+
+ bool GetResource(const PLT_MediaObject* entry, CFileItem& item);
+ std::shared_ptr<CFileItem> GetFileItem(const NPT_String& uri, const NPT_String& meta);
+}
+
diff --git a/xbmc/network/upnp/UPnPPlayer.cpp b/xbmc/network/upnp/UPnPPlayer.cpp
new file mode 100644
index 0000000..0f0d52a
--- /dev/null
+++ b/xbmc/network/upnp/UPnPPlayer.cpp
@@ -0,0 +1,625 @@
+/*
+ * Copyright (c) 2006 elupus (Joakim Plate)
+ * Copyright (C) 2006-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 "UPnPPlayer.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "ThumbLoader.h"
+#include "UPnP.h"
+#include "UPnPInternal.h"
+#include "application/Application.h"
+#include "cores/DataCacheCore.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "music/MusicThumbLoader.h"
+#include "threads/Event.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoThumbLoader.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+
+#include <Platinum/Source/Devices/MediaRenderer/PltMediaController.h>
+#include <Platinum/Source/Devices/MediaServer/PltDidl.h>
+#include <Platinum/Source/Platinum/Platinum.h>
+
+using namespace KODI::MESSAGING;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+using namespace std::chrono_literals;
+
+NPT_SET_LOCAL_LOGGER("xbmc.upnp.player")
+
+namespace UPNP
+{
+
+class CUPnPPlayerController : public PLT_MediaControllerDelegate
+{
+public:
+ CUPnPPlayerController(PLT_MediaController* control,
+ PLT_DeviceDataReference& device,
+ IPlayerCallback& callback)
+ : m_control(control),
+ m_transport(NULL),
+ m_device(device),
+ m_instance(0),
+ m_callback(callback),
+ m_postime(0),
+ m_logger(CServiceBroker::GetLogging().GetLogger("CUPnPPlayerController"))
+ {
+ m_posinfo = {};
+ m_device->FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", m_transport);
+ }
+
+ void OnSetAVTransportURIResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ {
+ if(NPT_FAILED(res))
+ m_logger->error("OnSetAVTransportURIResult failed");
+ m_resstatus = res;
+ m_resevent.Set();
+ }
+
+ void OnPlayResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ {
+ if(NPT_FAILED(res))
+ m_logger->error("OnPlayResult failed");
+ m_resstatus = res;
+ m_resevent.Set();
+ }
+
+ void OnStopResult(NPT_Result res, PLT_DeviceDataReference& device, void* userdata) override
+ {
+ if(NPT_FAILED(res))
+ m_logger->error("OnStopResult failed");
+ m_resstatus = res;
+ m_resevent.Set();
+ }
+
+ void OnGetMediaInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_MediaInfo* info, void* userdata) override
+ {
+ if(NPT_FAILED(res) || info == NULL)
+ m_logger->error("OnGetMediaInfoResult failed");
+ }
+
+ void OnGetTransportInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_TransportInfo* info, void* userdata) override
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if(NPT_FAILED(res))
+ {
+ m_logger->error("OnGetTransportInfoResult failed");
+ m_trainfo.cur_speed = "0";
+ m_trainfo.cur_transport_state = "STOPPED";
+ m_trainfo.cur_transport_status = "ERROR_OCCURED";
+ }
+ else
+ m_trainfo = *info;
+ m_traevnt.Set();
+ }
+
+ void UpdatePositionInfo()
+ {
+ if(m_postime == 0
+ || m_postime > CTimeUtils::GetFrameTime())
+ return;
+
+ m_control->GetTransportInfo(m_device, m_instance, this);
+ m_control->GetPositionInfo(m_device, m_instance, this);
+ m_postime = 0;
+ }
+
+ void OnGetPositionInfoResult(NPT_Result res, PLT_DeviceDataReference& device, PLT_PositionInfo* info, void* userdata) override
+ {
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if(NPT_FAILED(res) || info == NULL)
+ {
+ m_logger->error("OnGetMediaInfoResult failed");
+ m_posinfo = PLT_PositionInfo();
+ }
+ else
+ m_posinfo = *info;
+ m_postime = CTimeUtils::GetFrameTime() + 500;
+ m_posevnt.Set();
+ }
+
+
+ ~CUPnPPlayerController() override = default;
+
+ PLT_MediaController* m_control;
+ PLT_Service * m_transport;
+ PLT_DeviceDataReference m_device;
+ NPT_UInt32 m_instance;
+ IPlayerCallback& m_callback;
+
+ NPT_Result m_resstatus;
+ CEvent m_resevent;
+
+ CCriticalSection m_section;
+ unsigned int m_postime;
+
+ CEvent m_posevnt;
+ PLT_PositionInfo m_posinfo;
+
+ CEvent m_traevnt;
+ PLT_TransportInfo m_trainfo;
+
+ Logger m_logger;
+};
+
+CUPnPPlayer::CUPnPPlayer(IPlayerCallback& callback, const char* uuid)
+ : IPlayer(callback),
+ m_control(NULL),
+ m_delegate(NULL),
+ m_started(false),
+ m_stopremote(false),
+ m_logger(CServiceBroker::GetLogging().GetLogger(StringUtils::Format("CUPnPPlayer[{}]", uuid)))
+{
+ m_control = CUPnP::GetInstance()->m_MediaController;
+
+ PLT_DeviceDataReference device;
+ if(NPT_SUCCEEDED(m_control->FindRenderer(uuid, device)))
+ {
+ m_delegate = new CUPnPPlayerController(m_control, device, callback);
+ CUPnP::RegisterUserdata(m_delegate);
+ }
+ else
+ m_logger->error("couldn't find device as {}", uuid);
+
+ CServiceBroker::GetWinSystem()->RegisterRenderLoop(this);
+}
+
+CUPnPPlayer::~CUPnPPlayer()
+{
+ CServiceBroker::GetWinSystem()->UnregisterRenderLoop(this);
+ CloseFile();
+ CUPnP::UnregisterUserdata(m_delegate);
+ delete m_delegate;
+}
+
+static NPT_Result WaitOnEvent(CEvent& event, XbmcThreads::EndTime<>& timeout)
+{
+ if (event.Wait(0ms))
+ return NPT_SUCCESS;
+
+ if (!CGUIDialogBusy::WaitOnEvent(event))
+ return NPT_FAILURE;
+
+ return NPT_SUCCESS;
+}
+
+int CUPnPPlayer::PlayFile(const CFileItem& file,
+ const CPlayerOptions& options,
+ XbmcThreads::EndTime<>& timeout)
+{
+ CFileItem item(file);
+ NPT_Reference<CThumbLoader> thumb_loader;
+ NPT_Reference<PLT_MediaObject> obj;
+ NPT_String path(file.GetPath().c_str());
+ NPT_String tmp, resource;
+ EMediaControllerQuirks quirks = EMEDIACONTROLLERQUIRKS_NONE;
+
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+
+ if (file.IsVideoDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ else if (item.IsMusicDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+
+ obj = BuildObject(item, path, false, thumb_loader, NULL, CUPnP::GetServer(), UPnPPlayer);
+ if(obj.IsNull()) goto failed;
+
+ NPT_CHECK_LABEL_SEVERE(PLT_Didl::ToDidl(*obj, "", tmp), failed_todidl);
+ tmp.Insert(didl_header, 0);
+ tmp.Append(didl_footer);
+
+ quirks = GetMediaControllerQuirks(m_delegate->m_device.AsPointer());
+ if (quirks & EMEDIACONTROLLERQUIRKS_X_MKV)
+ {
+ for (NPT_Cardinal i=0; i< obj->m_Resources.GetItemCount(); i++) {
+ if (obj->m_Resources[i].m_ProtocolInfo.GetContentType().Compare("video/x-matroska") == 0) {
+ m_logger->debug("PlayFile({}): applying video/x-mkv quirk", file.GetPath());
+ NPT_String protocolInfo = obj->m_Resources[i].m_ProtocolInfo.ToString();
+ protocolInfo.Replace(":video/x-matroska:", ":video/x-mkv:");
+ obj->m_Resources[i].m_ProtocolInfo = PLT_ProtocolInfo(protocolInfo);
+ }
+ }
+ }
+
+ /* The resource uri's are stored in the Didl. We must choose the best resource
+ * for the playback device */
+ NPT_Cardinal res_index;
+ NPT_CHECK_LABEL_SEVERE(m_control->FindBestResource(m_delegate->m_device, *obj, res_index), failed_findbestresource);
+
+ // get the transport info to evaluate the TransportState to be able to
+ // determine whether we first need to call Stop()
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed_gettransportinfo);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed_gettransportinfo);
+
+ if (m_delegate->m_trainfo.cur_transport_state != "NO_MEDIA_PRESENT" &&
+ m_delegate->m_trainfo.cur_transport_state != "STOPPED")
+ {
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->Stop(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed_stop);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_stop);
+ NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_stop);
+ }
+
+
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->SetAVTransportURI(m_delegate->m_device
+ , m_delegate->m_instance
+ , obj->m_Resources[res_index].m_Uri
+ , (const char*)tmp
+ , m_delegate), failed_setavtransporturi);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_setavtransporturi);
+ NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_setavtransporturi);
+
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ NPT_CHECK_LABEL_SEVERE(m_control->Play(m_delegate->m_device
+ , m_delegate->m_instance
+ , "1"
+ , m_delegate), failed_play);
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_resevent, timeout), failed_play);
+ NPT_CHECK_LABEL_SEVERE(m_delegate->m_resstatus, failed_play);
+
+
+ /* wait for PLAYING state */
+ timeout.Set(timeout.GetInitialTimeoutValue());
+ do {
+ NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed_waitplaying);
+
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_delegate->m_section);
+ if(m_delegate->m_trainfo.cur_transport_state == "PLAYING"
+ || m_delegate->m_trainfo.cur_transport_state == "PAUSED_PLAYBACK")
+ break;
+
+ if(m_delegate->m_trainfo.cur_transport_state == "STOPPED"
+ && m_delegate->m_trainfo.cur_transport_status != "OK")
+ {
+ m_logger->error("OpenFile({}): remote player signalled error", file.GetPath());
+ return NPT_FAILURE;
+ }
+ }
+
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed_waitplaying);
+
+ } while(!timeout.IsTimePast());
+
+ if(options.starttime > 0)
+ {
+ /* many upnp units won't load file properly until after play (including xbmc) */
+ NPT_CHECK_LABEL(m_control->Seek(m_delegate->m_device
+ , m_delegate->m_instance
+ , "REL_TIME"
+ , PLT_Didl::FormatTimeStamp((NPT_UInt32)options.starttime)
+ , m_delegate), failed_seek);
+ }
+
+ return NPT_SUCCESS;
+failed_todidl:
+ m_logger->error("PlayFile({}) failed to serialize item into DIDL-Lite", file.GetPath());
+ return NPT_FAILURE;
+failed_findbestresource:
+ m_logger->error("PlayFile({}) failed to find a matching resource", file.GetPath());
+ return NPT_FAILURE;
+failed_gettransportinfo:
+ m_logger->error("PlayFile({}): call to GetTransportInfo failed", file.GetPath());
+ return NPT_FAILURE;
+failed_stop:
+ m_logger->error("PlayFile({}) failed to stop current playback", file.GetPath());
+ return NPT_FAILURE;
+failed_setavtransporturi:
+ m_logger->error("PlayFile({}) failed to set the playback URI", file.GetPath());
+ return NPT_FAILURE;
+failed_play:
+ m_logger->error("PlayFile({}) failed to start playback", file.GetPath());
+ return NPT_FAILURE;
+failed_waitplaying:
+ m_logger->error("PlayFile({}) failed to wait for PLAYING state", file.GetPath());
+ return NPT_FAILURE;
+failed_seek:
+ m_logger->error("PlayFile({}) failed to seek to start offset", file.GetPath());
+ return NPT_FAILURE;
+failed:
+ m_logger->error("PlayFile({}) failed", file.GetPath());
+ return NPT_FAILURE;
+}
+
+bool CUPnPPlayer::OpenFile(const CFileItem& file, const CPlayerOptions& options)
+{
+ XbmcThreads::EndTime<> timeout(10s);
+
+ /* if no path we want to attach to a already playing player */
+ if(file.GetPath() == "")
+ {
+ NPT_CHECK_LABEL_SEVERE(m_control->GetTransportInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+
+ NPT_CHECK_LABEL_SEVERE(WaitOnEvent(m_delegate->m_traevnt, timeout), failed);
+
+ /* make sure the attached player is actually playing */
+ {
+ std::unique_lock<CCriticalSection> lock(m_delegate->m_section);
+ if(m_delegate->m_trainfo.cur_transport_state != "PLAYING"
+ && m_delegate->m_trainfo.cur_transport_state != "PAUSED_PLAYBACK")
+ goto failed;
+ }
+ }
+ else
+ NPT_CHECK_LABEL_SEVERE(PlayFile(file, options, timeout), failed);
+
+ m_stopremote = true;
+ m_started = true;
+ m_callback.OnPlayBackStarted(file);
+ m_callback.OnAVStarted(file);
+ NPT_CHECK_LABEL_SEVERE(m_control->GetPositionInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+ NPT_CHECK_LABEL_SEVERE(m_control->GetMediaInfo(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+
+ m_updateTimer.Set(0ms);
+
+ return true;
+failed:
+ m_logger->error("OpenFile({}) failed to open file", file.GetPath());
+ return false;
+}
+
+bool CUPnPPlayer::QueueNextFile(const CFileItem& file)
+{
+ CFileItem item(file);
+ NPT_Reference<CThumbLoader> thumb_loader;
+ NPT_Reference<PLT_MediaObject> obj;
+ NPT_String path(file.GetPath().c_str());
+ NPT_String tmp;
+
+ if (file.IsVideoDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ else if (item.IsMusicDb())
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+
+
+ obj = BuildObject(item, path, false, thumb_loader, NULL, CUPnP::GetServer(), UPnPPlayer);
+ if(!obj.IsNull())
+ {
+ NPT_CHECK_LABEL_SEVERE(PLT_Didl::ToDidl(*obj, "", tmp), failed);
+ tmp.Insert(didl_header, 0);
+ tmp.Append(didl_footer);
+ }
+
+ NPT_CHECK_LABEL_WARNING(m_control->SetNextAVTransportURI(m_delegate->m_device
+ , m_delegate->m_instance
+ , file.GetPath().c_str()
+ , (const char*)tmp
+ , m_delegate), failed);
+ if (!m_delegate->m_resevent.Wait(10000ms))
+ goto failed;
+ NPT_CHECK_LABEL_WARNING(m_delegate->m_resstatus, failed);
+ return true;
+
+failed:
+ m_logger->error("QueueNextFile({}) failed to queue file", file.GetPath());
+ return false;
+}
+
+bool CUPnPPlayer::CloseFile(bool reopen)
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ if(m_stopremote)
+ {
+ NPT_CHECK_LABEL(m_control->Stop(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+ if (!m_delegate->m_resevent.Wait(10000ms))
+ goto failed;
+ NPT_CHECK_LABEL(m_delegate->m_resstatus, failed);
+ }
+
+ if(m_started)
+ {
+ m_started = false;
+ m_callback.OnPlayBackStopped();
+ }
+
+ return true;
+failed:
+ m_logger->error("CloseFile - unable to stop playback");
+ return false;
+}
+
+void CUPnPPlayer::Pause()
+{
+ if(IsPaused())
+ {
+ NPT_CHECK_LABEL(m_control->Play(m_delegate->m_device
+ , m_delegate->m_instance
+ , "1"
+ , m_delegate), failed);
+ CDataCacheCore::GetInstance().SetSpeed(1.0, 1.0);
+ }
+ else
+ {
+ NPT_CHECK_LABEL(m_control->Pause(m_delegate->m_device
+ , m_delegate->m_instance
+ , m_delegate), failed);
+ CDataCacheCore::GetInstance().SetSpeed(1.0, 0.0);
+ }
+
+ return;
+failed:
+ m_logger->error("CloseFile - unable to pause/unpause playback");
+}
+
+void CUPnPPlayer::SeekTime(int64_t ms)
+{
+ NPT_CHECK_LABEL(m_control->Seek(m_delegate->m_device
+ , m_delegate->m_instance
+ , "REL_TIME", PLT_Didl::FormatTimeStamp((NPT_UInt32)(ms / 1000))
+ , m_delegate), failed);
+
+ CDataCacheCore::GetInstance().SeekFinished(0);
+ return;
+failed:
+ m_logger->error("SeekTime - unable to seek playback");
+}
+
+float CUPnPPlayer::GetPercentage()
+{
+ int64_t tot = GetTotalTime();
+ if(tot)
+ return 100.0f * GetTime() / tot;
+ else
+ return 0.0f;
+}
+
+void CUPnPPlayer::SeekPercentage(float percent)
+{
+ int64_t tot = GetTotalTime();
+ if (tot)
+ SeekTime((int64_t)(tot * percent / 100));
+}
+
+void CUPnPPlayer::Seek(bool bPlus, bool bLargeStep, bool bChapterOverride)
+{
+}
+
+void CUPnPPlayer::DoAudioWork()
+{
+ NPT_String data;
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ m_delegate->UpdatePositionInfo();
+
+ if(m_started) {
+ NPT_String uri, meta;
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("CurrentTrackURI", uri), failed);
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("CurrentTrackMetadata", meta), failed);
+
+ if(m_current_uri != (const char*)uri
+ || m_current_meta != (const char*)meta) {
+ m_current_uri = (const char*)uri;
+ m_current_meta = (const char*)meta;
+ CFileItemPtr item = GetFileItem(uri, meta);
+ g_application.CurrentFileItem() = *item;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_UPDATE_CURRENT_ITEM, 0, -1,
+ static_cast<void*>(new CFileItem(*item)));
+ }
+
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed);
+ if(data == "STOPPED")
+ {
+ m_started = false;
+ m_callback.OnPlayBackEnded();
+ }
+ }
+ return;
+failed:
+ return;
+}
+
+bool CUPnPPlayer::IsPlaying() const
+{
+ NPT_String data;
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed);
+ return data != "STOPPED";
+failed:
+ return false;
+}
+
+bool CUPnPPlayer::IsPaused() const
+{
+ NPT_String data;
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ NPT_CHECK_LABEL(m_delegate->m_transport->GetStateVariableValue("TransportState", data), failed);
+ return data == "PAUSED_PLAYBACK";
+failed:
+ return false;
+}
+
+void CUPnPPlayer::SetVolume(float volume)
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ NPT_CHECK_LABEL(m_control->SetVolume(m_delegate->m_device
+ , m_delegate->m_instance
+ , "Master", (int)(volume * 100)
+ , m_delegate), failed);
+ return;
+failed:
+ m_logger->error("- unable to set volume");
+}
+
+int64_t CUPnPPlayer::GetTime()
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ return m_delegate->m_posinfo.rel_time.ToMillis();
+failed:
+ return 0;
+}
+
+int64_t CUPnPPlayer::GetTotalTime()
+{
+ NPT_CHECK_POINTER_LABEL_SEVERE(m_delegate, failed);
+ return m_delegate->m_posinfo.track_duration.ToMillis();
+failed:
+ return 0;
+};
+
+bool CUPnPPlayer::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_STOP:
+ if(IsPlaying())
+ {
+ //stop on remote system
+ m_stopremote = HELPERS::ShowYesNoDialogText(CVariant{37022}, CVariant{37023}) ==
+ DialogResponse::CHOICE_YES;
+
+ return false; /* let normal code handle the action */
+ }
+ [[fallthrough]];
+ default:
+ return false;
+ }
+}
+
+void CUPnPPlayer::SetSpeed(float speed)
+{
+
+}
+
+void CUPnPPlayer::FrameMove()
+{
+ if (m_updateTimer.IsTimePast())
+ {
+ CDataCacheCore::GetInstance().SetPlayTimes(0, GetTime(), 0, GetTotalTime());
+ m_updateTimer.Set(500ms);
+ }
+}
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnPPlayer.h b/xbmc/network/upnp/UPnPPlayer.h
new file mode 100644
index 0000000..aafd8c4
--- /dev/null
+++ b/xbmc/network/upnp/UPnPPlayer.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2006 elupus (Joakim Plate)
+ * Copyright (C) 2006-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/IPlayer.h"
+#include "guilib/DispResource.h"
+#include "threads/SystemClock.h"
+#include "utils/logtypes.h"
+
+#include <string>
+
+class PLT_MediaController;
+
+namespace UPNP
+{
+
+class CUPnPPlayerController;
+
+class CUPnPPlayer
+ : public IPlayer, public IRenderLoop
+{
+public:
+ CUPnPPlayer(IPlayerCallback& callback, const char* uuid);
+ ~CUPnPPlayer() override;
+
+ bool OpenFile(const CFileItem& file, const CPlayerOptions& options) override;
+ bool QueueNextFile(const CFileItem &file) override;
+ bool CloseFile(bool reopen = false) override;
+ bool IsPlaying() const override;
+ void Pause() override;
+ bool HasVideo() const override { return false; }
+ bool HasAudio() const override { return false; }
+ void Seek(bool bPlus, bool bLargeStep, bool bChapterOverride) override;
+ void SeekPercentage(float fPercent = 0) override;
+ void SetVolume(float volume) override;
+
+ int SeekChapter(int iChapter) override { return -1; }
+
+ void SeekTime(int64_t iTime = 0) override;
+ void SetSpeed(float speed = 0) override;
+
+ bool IsCaching() const override { return false; }
+ int GetCacheLevel() const override { return -1; }
+ void DoAudioWork() override;
+ bool OnAction(const CAction &action) override;
+
+ void FrameMove() override;
+
+ int PlayFile(const CFileItem& file,
+ const CPlayerOptions& options,
+ XbmcThreads::EndTime<>& timeout);
+
+private:
+ bool IsPaused() const;
+ int64_t GetTime();
+ int64_t GetTotalTime();
+ float GetPercentage();
+
+ PLT_MediaController* m_control;
+ CUPnPPlayerController* m_delegate;
+ std::string m_current_uri;
+ std::string m_current_meta;
+ bool m_started;
+ bool m_stopremote;
+ XbmcThreads::EndTime<> m_updateTimer;
+
+ Logger m_logger;
+};
+
+} /* namespace UPNP */
diff --git a/xbmc/network/upnp/UPnPRenderer.cpp b/xbmc/network/upnp/UPnPRenderer.cpp
new file mode 100644
index 0000000..d296471
--- /dev/null
+++ b/xbmc/network/upnp/UPnPRenderer.cpp
@@ -0,0 +1,758 @@
+/*
+ * 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 "UPnPRenderer.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "ThumbLoader.h"
+#include "UPnP.h"
+#include "UPnPInternal.h"
+#include "URL.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Network.h"
+#include "pictures/GUIWindowSlideShow.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "xbmc/interfaces/AnnouncementManager.h"
+
+#include <inttypes.h>
+#include <mutex>
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+NPT_SET_LOCAL_LOGGER("xbmc.upnp.renderer")
+
+namespace UPNP
+{
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::CUPnPRenderer
++---------------------------------------------------------------------*/
+CUPnPRenderer::CUPnPRenderer(const char* friendly_name, bool show_ip /*= false*/,
+ const char* uuid /*= NULL*/, unsigned int port /*= 0*/)
+ : PLT_MediaRenderer(friendly_name, show_ip, uuid, port)
+{
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::~CUPnPRenderer
++---------------------------------------------------------------------*/
+CUPnPRenderer::~CUPnPRenderer()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::SetupServices
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::SetupServices()
+{
+ NPT_CHECK(PLT_MediaRenderer::SetupServices());
+
+ // update what we can play
+ PLT_Service* service = NULL;
+ NPT_CHECK_FATAL(FindServiceByType("urn:schemas-upnp-org:service:ConnectionManager:1", service));
+ service->SetStateVariable("SinkProtocolInfo"
+ ,"http-get:*:*:*"
+ ",xbmc-get:*:*:*"
+ ",http-get:*:audio/mkv:*"
+ ",http-get:*:audio/mpegurl:*"
+ ",http-get:*:audio/mpeg:*"
+ ",http-get:*:audio/mpeg3:*"
+ ",http-get:*:audio/mp3:*"
+ ",http-get:*:audio/mp4:*"
+ ",http-get:*:audio/basic:*"
+ ",http-get:*:audio/midi:*"
+ ",http-get:*:audio/ulaw:*"
+ ",http-get:*:audio/ogg:*"
+ ",http-get:*:audio/DVI4:*"
+ ",http-get:*:audio/G722:*"
+ ",http-get:*:audio/G723:*"
+ ",http-get:*:audio/G726-16:*"
+ ",http-get:*:audio/G726-24:*"
+ ",http-get:*:audio/G726-32:*"
+ ",http-get:*:audio/G726-40:*"
+ ",http-get:*:audio/G728:*"
+ ",http-get:*:audio/G729:*"
+ ",http-get:*:audio/G729D:*"
+ ",http-get:*:audio/G729E:*"
+ ",http-get:*:audio/GSM:*"
+ ",http-get:*:audio/GSM-EFR:*"
+ ",http-get:*:audio/L8:*"
+ ",http-get:*:audio/L16:*"
+ ",http-get:*:audio/LPC:*"
+ ",http-get:*:audio/MPA:*"
+ ",http-get:*:audio/PCMA:*"
+ ",http-get:*:audio/PCMU:*"
+ ",http-get:*:audio/QCELP:*"
+ ",http-get:*:audio/RED:*"
+ ",http-get:*:audio/VDVI:*"
+ ",http-get:*:audio/ac3:*"
+ ",http-get:*:audio/webm:*"
+ ",http-get:*:audio/vorbis:*"
+ ",http-get:*:audio/speex:*"
+ ",http-get:*:audio/flac:*"
+ ",http-get:*:audio/x-flac:*"
+ ",http-get:*:audio/x-aiff:*"
+ ",http-get:*:audio/x-pn-realaudio:*"
+ ",http-get:*:audio/x-realaudio:*"
+ ",http-get:*:audio/x-wav:*"
+ ",http-get:*:audio/x-matroska:*"
+ ",http-get:*:audio/x-ms-wma:*"
+ ",http-get:*:audio/x-mpegurl:*"
+ ",http-get:*:application/x-shockwave-flash:*"
+ ",http-get:*:application/ogg:*"
+ ",http-get:*:application/sdp:*"
+ ",http-get:*:image/gif:*"
+ ",http-get:*:image/jpeg:*"
+ ",http-get:*:image/ief:*"
+ ",http-get:*:image/png:*"
+ ",http-get:*:image/tiff:*"
+ ",http-get:*:image/webp:*"
+ ",http-get:*:video/avi:*"
+ ",http-get:*:video/divx:*"
+ ",http-get:*:video/mpeg:*"
+ ",http-get:*:video/fli:*"
+ ",http-get:*:video/flv:*"
+ ",http-get:*:video/quicktime:*"
+ ",http-get:*:video/vnd.vivo:*"
+ ",http-get:*:video/vc1:*"
+ ",http-get:*:video/ogg:*"
+ ",http-get:*:video/mp4:*"
+ ",http-get:*:video/mkv:*"
+ ",http-get:*:video/BT656:*"
+ ",http-get:*:video/CelB:*"
+ ",http-get:*:video/JPEG:*"
+ ",http-get:*:video/H261:*"
+ ",http-get:*:video/H263:*"
+ ",http-get:*:video/H263-1998:*"
+ ",http-get:*:video/H263-2000:*"
+ ",http-get:*:video/MPV:*"
+ ",http-get:*:video/MP2T:*"
+ ",http-get:*:video/MP1S:*"
+ ",http-get:*:video/MP2P:*"
+ ",http-get:*:video/BMPEG:*"
+ ",http-get:*:video/webm:*"
+ ",http-get:*:video/xvid:*"
+ ",http-get:*:video/x-divx:*"
+ ",http-get:*:video/x-matroska:*"
+ ",http-get:*:video/x-mkv:*"
+ ",http-get:*:video/x-ms-wmv:*"
+ ",http-get:*:video/x-ms-avi:*"
+ ",http-get:*:video/x-flv:*"
+ ",http-get:*:video/x-fli:*"
+ ",http-get:*:video/x-ms-asf:*"
+ ",http-get:*:video/x-ms-asx:*"
+ ",http-get:*:video/x-ms-wmx:*"
+ ",http-get:*:video/x-ms-wvx:*"
+ ",http-get:*:video/x-msvideo:*"
+ ",http-get:*:video/x-xvid:*"
+ );
+
+ NPT_CHECK_FATAL(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+ service->SetStateVariable("NextAVTransportURI", "");
+ service->SetStateVariable("NextAVTransportURIMetadata", "");
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::ProcessHttpRequest
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::ProcessHttpGetRequest(NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response)
+{
+ // get the address of who sent us some data back
+ NPT_String ip_address = context.GetRemoteAddress().GetIpAddress().ToString();
+ NPT_String method = request.GetMethod();
+ NPT_String protocol = request.GetProtocol();
+ NPT_HttpUrl url = request.GetUrl();
+
+ if (url.GetPath() == "/thumb") {
+ NPT_HttpUrlQuery query(url.GetQuery());
+ NPT_String filepath = query.GetField("path");
+ if (!filepath.IsEmpty()) {
+ NPT_HttpEntity* entity = response.GetEntity();
+ if (entity == NULL) return NPT_ERROR_INVALID_STATE;
+
+ // check the method
+ if (request.GetMethod() != NPT_HTTP_METHOD_GET &&
+ request.GetMethod() != NPT_HTTP_METHOD_HEAD) {
+ response.SetStatus(405, "Method Not Allowed");
+ return NPT_SUCCESS;
+ }
+
+ // prevent hackers from accessing files outside of our root
+ if ((filepath.Find("/..") >= 0) || (filepath.Find("\\..") >=0)) {
+ return NPT_FAILURE;
+ }
+
+ // open the file
+ std::string path (CURL::Decode((const char*) filepath));
+ NPT_File file(path.c_str());
+ NPT_Result result = file.Open(NPT_FILE_OPEN_MODE_READ);
+ if (NPT_FAILED(result)) {
+ response.SetStatus(404, "Not Found");
+ return NPT_SUCCESS;
+ }
+ NPT_InputStreamReference stream;
+ file.GetInputStream(stream);
+ entity->SetContentType(GetMimeType(filepath));
+ entity->SetInputStream(stream, true);
+
+ return NPT_SUCCESS;
+ }
+ }
+
+ return PLT_MediaRenderer::ProcessHttpGetRequest(request, context, response);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::Announce
++---------------------------------------------------------------------*/
+void CUPnPRenderer::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (sender != ANNOUNCEMENT::CAnnouncementManager::ANNOUNCEMENT_SENDER)
+ return;
+
+ NPT_AutoLock lock(m_state);
+ PLT_Service *avt, *rct;
+
+ if (flag == ANNOUNCEMENT::Player)
+ {
+ if (NPT_FAILED(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", avt)))
+ return;
+
+ if (message == "OnPlay" || message == "OnResume")
+ {
+ avt->SetStateVariable("AVTransportURI", g_application.CurrentFile().c_str());
+ avt->SetStateVariable("CurrentTrackURI", g_application.CurrentFile().c_str());
+
+ NPT_String meta;
+ if (NPT_SUCCEEDED(GetMetadata(meta)))
+ {
+ avt->SetStateVariable("CurrentTrackMetadata", meta);
+ avt->SetStateVariable("AVTransportURIMetaData", meta);
+ }
+
+ avt->SetStateVariable("TransportPlaySpeed",
+ NPT_String::FromInteger(data["player"]["speed"].asInteger()));
+ avt->SetStateVariable("TransportState", "PLAYING");
+
+ /* this could be a transition to next track, so clear next */
+ avt->SetStateVariable("NextAVTransportURI", "");
+ avt->SetStateVariable("NextAVTransportURIMetaData", "");
+ }
+ else if (message == "OnPause")
+ {
+ int64_t speed = data["player"]["speed"].asInteger();
+ avt->SetStateVariable("TransportPlaySpeed", NPT_String::FromInteger(speed != 0 ? speed : 1));
+ avt->SetStateVariable("TransportState", "PAUSED_PLAYBACK");
+ }
+ else if (message == "OnSpeedChanged")
+ {
+ avt->SetStateVariable("TransportPlaySpeed",
+ NPT_String::FromInteger(data["player"]["speed"].asInteger()));
+ }
+ }
+ else if (flag == ANNOUNCEMENT::Application && message == "OnVolumeChanged")
+ {
+ if (NPT_FAILED(FindServiceByType("urn:schemas-upnp-org:service:RenderingControl:1", rct)))
+ return;
+
+ std::string buffer;
+
+ buffer = std::to_string(data["volume"].asInteger());
+ rct->SetStateVariable("Volume", buffer.c_str());
+
+ buffer = std::to_string(256 * (data["volume"].asInteger() * 60 - 60) / 100);
+ rct->SetStateVariable("VolumeDb", buffer.c_str());
+
+ rct->SetStateVariable("Mute", data["muted"].asBoolean() ? "1" : "0");
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::UpdateState
++---------------------------------------------------------------------*/
+void
+CUPnPRenderer::UpdateState()
+{
+ NPT_AutoLock lock(m_state);
+
+ PLT_Service *avt;
+
+ if (NPT_FAILED(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", avt)))
+ return;
+
+ /* don't update state while transitioning */
+ NPT_String state;
+ avt->GetStateVariableValue("TransportState", state);
+ if(state == "TRANSITIONING")
+ return;
+
+ avt->SetStateVariable("TransportStatus", "OK");
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying() || appPlayer->IsPausedPlayback())
+ {
+ avt->SetStateVariable("NumberOfTracks", "1");
+ avt->SetStateVariable("CurrentTrack", "1");
+
+ // get elapsed time
+ std::string buffer = StringUtils::SecondsToTimeString(std::lrint(g_application.GetTime()),
+ TIME_FORMAT_HH_MM_SS);
+ avt->SetStateVariable("RelativeTimePosition", buffer.c_str());
+ avt->SetStateVariable("AbsoluteTimePosition", buffer.c_str());
+
+ // get duration
+ buffer = StringUtils::SecondsToTimeString(std::lrint(g_application.GetTotalTime()),
+ TIME_FORMAT_HH_MM_SS);
+ if (buffer.length() > 0)
+ {
+ avt->SetStateVariable("CurrentTrackDuration", buffer.c_str());
+ avt->SetStateVariable("CurrentMediaDuration", buffer.c_str());
+ }
+ else
+ {
+ avt->SetStateVariable("CurrentTrackDuration", "00:00:00");
+ avt->SetStateVariable("CurrentMediaDuration", "00:00:00");
+ }
+ }
+ else if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ {
+ avt->SetStateVariable("TransportState", "PLAYING");
+
+ const std::string filePath = CServiceBroker::GetGUI()->GetInfoManager().GetLabel(
+ SLIDESHOW_FILE_PATH, INFO::DEFAULT_CONTEXT);
+ avt->SetStateVariable("AVTransportURI", filePath.c_str());
+ avt->SetStateVariable("CurrentTrackURI", filePath.c_str());
+ avt->SetStateVariable("TransportPlaySpeed", "1");
+
+ CGUIWindowSlideShow* slideshow =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(
+ WINDOW_SLIDESHOW);
+ if (slideshow)
+ {
+ std::string index;
+ index = std::to_string(slideshow->NumSlides());
+ avt->SetStateVariable("NumberOfTracks", index.c_str());
+ index = std::to_string(slideshow->CurrentSlide());
+ avt->SetStateVariable("CurrentTrack", index.c_str());
+ }
+
+ avt->SetStateVariable("CurrentTrackMetadata", "");
+ avt->SetStateVariable("AVTransportURIMetaData", "");
+ }
+ else
+ {
+ avt->SetStateVariable("TransportState", "STOPPED");
+ avt->SetStateVariable("TransportPlaySpeed", "1");
+ avt->SetStateVariable("NumberOfTracks", "0");
+ avt->SetStateVariable("CurrentTrack", "0");
+ avt->SetStateVariable("RelativeTimePosition", "00:00:00");
+ avt->SetStateVariable("AbsoluteTimePosition", "00:00:00");
+ avt->SetStateVariable("CurrentTrackDuration", "00:00:00");
+ avt->SetStateVariable("CurrentMediaDuration", "00:00:00");
+ avt->SetStateVariable("NextAVTransportURI", "");
+ avt->SetStateVariable("NextAVTransportURIMetaData", "");
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::SetupIcons
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::SetupIcons()
+{
+ NPT_String file_root = CSpecialProtocol::TranslatePath("special://xbmc/media/").c_str();
+ AddIcon(
+ PLT_DeviceIcon("image/png", 256, 256, 8, "/icon256x256.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 120, 120, 8, "/icon120x120.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 48, 48, 8, "/icon48x48.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 32, 32, 8, "/icon32x32.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 16, 16, 8, "/icon16x16.png"),
+ file_root);
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::GetMetadata
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::GetMetadata(NPT_String& meta)
+{
+ NPT_Result res = NPT_FAILURE;
+ CFileItem item(g_application.CurrentFileItem());
+ NPT_String file_path, tmp;
+
+ // we pass an empty CThumbLoader reference, as it can't be used
+ // without CUPnPServer enabled
+ NPT_Reference<CThumbLoader> thumb_loader;
+ PLT_MediaObject* object = BuildObject(item, file_path, false, thumb_loader, NULL, NULL, UPnPRenderer);
+ if (object) {
+ // fetch the item's artwork
+ std::string thumb;
+ if (object->m_ObjectClass.type == "object.item.audioItem.musicTrack")
+ thumb = CServiceBroker::GetGUI()->GetInfoManager().GetImage(MUSICPLAYER_COVER, -1);
+ else
+ thumb = CServiceBroker::GetGUI()->GetInfoManager().GetImage(VIDEOPLAYER_COVER, -1);
+
+ thumb = CTextureUtils::GetWrappedImageURL(thumb);
+
+ NPT_String ip;
+ if (CServiceBroker::GetNetwork().GetFirstConnectedInterface()) {
+ ip = CServiceBroker::GetNetwork().GetFirstConnectedInterface()->GetCurrentIPAddress().c_str();
+ }
+ // build url, use the internal device http server to serv the image
+ NPT_HttpUrlQuery query;
+ query.AddField("path", thumb.c_str());
+ PLT_AlbumArtInfo art;
+ art.uri = NPT_HttpUrl(
+ ip,
+ m_URLDescription.GetPort(),
+ "/thumb",
+ query.ToString()).ToString();
+ // Set DLNA profileID by extension, defaulting to JPEG.
+ if (URIUtils::HasExtension(item.GetArt("thumb"), ".png")) {
+ art.dlna_profile = "PNG_TN";
+ } else {
+ art.dlna_profile = "JPEG_TN";
+ }
+ object->m_ExtraInfo.album_arts.Add(art);
+
+ res = PLT_Didl::ToDidl(*object, "*", tmp);
+ meta = didl_header + tmp + didl_footer;
+ delete object;
+ }
+ return res;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnNext
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnNext(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_NEXT_PICTURE)));
+ } else {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_NEXT);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnPause
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnPause(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_NEXT_PICTURE)));
+ }
+ else
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPausedPlayback())
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnPlay
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnPlay(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ return NPT_SUCCESS;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPausedPlayback())
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ else if (appPlayer && !appPlayer->IsPlaying())
+ {
+ NPT_String uri, meta;
+ PLT_Service* service;
+ // look for value set previously by SetAVTransportURI
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+ NPT_CHECK_SEVERE(service->GetStateVariableValue("AVTransportURI", uri));
+ NPT_CHECK_SEVERE(service->GetStateVariableValue("AVTransportURIMetaData", meta));
+
+ // if not set, use the current file being played
+ PlayMedia(uri, meta);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnPrevious
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnPrevious(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_PREV_PICTURE)));
+ } else {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_PLAYLISTPLAYER_PREV);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnStop
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnStop(PLT_ActionReference& action)
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) {
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_SLIDESHOW, -1,
+ static_cast<void*>(new CAction(ACTION_NEXT_PICTURE)));
+ } else {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSetAVTransportURI
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetAVTransportURI(PLT_ActionReference& action)
+{
+ NPT_String uri, meta;
+ PLT_Service* service;
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+
+ NPT_CHECK_SEVERE(action->GetArgumentValue("CurrentURI", uri));
+ NPT_CHECK_SEVERE(action->GetArgumentValue("CurrentURIMetaData", meta));
+
+ // if not playing already, just keep around uri & metadata
+ // and wait for play command
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlaying() &&
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() != WINDOW_SLIDESHOW)
+ {
+ service->SetStateVariable("TransportState", "STOPPED");
+ service->SetStateVariable("TransportStatus", "OK");
+ service->SetStateVariable("TransportPlaySpeed", "1");
+ service->SetStateVariable("AVTransportURI", uri);
+ service->SetStateVariable("AVTransportURIMetaData", meta);
+ service->SetStateVariable("NextAVTransportURI", "");
+ service->SetStateVariable("NextAVTransportURIMetaData", "");
+
+ NPT_CHECK_SEVERE(action->SetArgumentsOutFromStateVariable());
+ return NPT_SUCCESS;
+ }
+
+ return PlayMedia(uri, meta, action.AsPointer());
+}
+
+/*----------------------------------------------------------------------
+ | CUPnPRenderer::OnSetAVTransportURI
+ +---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetNextAVTransportURI(PLT_ActionReference& action)
+{
+ NPT_String uri, meta;
+ PLT_Service* service;
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+
+ NPT_CHECK_SEVERE(action->GetArgumentValue("NextURI", uri));
+ NPT_CHECK_SEVERE(action->GetArgumentValue("NextURIMetaData", meta));
+
+ CFileItemPtr item = GetFileItem(uri, meta);
+ if (!item) {
+ return NPT_FAILURE;
+ }
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ {
+
+ PLAYLIST::Id playlistId = PLAYLIST::TYPE_MUSIC;
+ if (item->IsVideo())
+ playlistId = PLAYLIST::TYPE_VIDEO;
+
+ {
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(playlistId);
+ CServiceBroker::GetPlaylistPlayer().Add(playlistId, item);
+
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(-1);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(playlistId);
+ }
+
+ CGUIMessage msg(GUI_MSG_PLAYLIST_CHANGED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+
+
+ service->SetStateVariable("NextAVTransportURI", uri);
+ service->SetStateVariable("NextAVTransportURIMetaData", meta);
+
+ NPT_CHECK_SEVERE(action->SetArgumentsOutFromStateVariable());
+
+ return NPT_SUCCESS;
+ }
+ else if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ {
+ return NPT_FAILURE;
+ }
+ else
+ {
+ return NPT_FAILURE;
+ }
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::PlayMedia
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::PlayMedia(const NPT_String& uri, const NPT_String& meta, PLT_Action* action)
+{
+ PLT_Service* service;
+ NPT_CHECK_SEVERE(FindServiceByType("urn:schemas-upnp-org:service:AVTransport:1", service));
+
+ { NPT_AutoLock lock(m_state);
+ service->SetStateVariable("TransportState", "TRANSITIONING");
+ service->SetStateVariable("TransportStatus", "OK");
+ }
+
+ CFileItemPtr item = GetFileItem(uri, meta);
+ if (!item) {
+ return NPT_FAILURE;
+ }
+
+ if (item->IsPicture()) {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_PICTURE_SHOW, -1, -1, nullptr,
+ item->GetPath());
+ } else {
+ CFileItemList *l = new CFileItemList; //don't delete,
+ l->Add(std::make_shared<CFileItem>(*item));
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, -1, -1, static_cast<void*>(l));
+ }
+
+ // just return success because the play actions are asynchronous
+ NPT_AutoLock lock(m_state);
+ service->SetStateVariable("TransportState", "PLAYING");
+ service->SetStateVariable("TransportStatus", "OK");
+ service->SetStateVariable("AVTransportURI", uri);
+ service->SetStateVariable("AVTransportURIMetaData", meta);
+
+ service->SetStateVariable("NextAVTransportURI", "");
+ service->SetStateVariable("NextAVTransportURIMetaData", "");
+
+ if (action) {
+ NPT_CHECK_SEVERE(action->SetArgumentsOutFromStateVariable());
+ }
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSetVolume
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetVolume(PLT_ActionReference& action)
+{
+ NPT_String volume;
+ NPT_CHECK_SEVERE(action->GetArgumentValue("DesiredVolume", volume));
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume((float)strtod((const char*)volume, NULL));
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSetMute
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSetMute(PLT_ActionReference& action)
+{
+ NPT_String mute;
+ NPT_CHECK_SEVERE(action->GetArgumentValue("DesiredMute",mute));
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ if ((mute == "1") ^ appVolume->IsMuted())
+ appVolume->ToggleMute();
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPRenderer::OnSeek
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPRenderer::OnSeek(PLT_ActionReference& action)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlaying())
+ return NPT_ERROR_INVALID_STATE;
+
+ NPT_String unit, target;
+ NPT_CHECK_SEVERE(action->GetArgumentValue("Unit", unit));
+ NPT_CHECK_SEVERE(action->GetArgumentValue("Target", target));
+
+ if (!unit.Compare("REL_TIME"))
+ {
+ // converts target to seconds
+ NPT_UInt32 seconds;
+ NPT_CHECK_SEVERE(PLT_Didl::ParseTimeStamp(target, seconds));
+ g_application.SeekTime(seconds);
+ }
+
+ return NPT_SUCCESS;
+}
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPRenderer.h b/xbmc/network/upnp/UPnPRenderer.h
new file mode 100644
index 0000000..e2ebe4b
--- /dev/null
+++ b/xbmc/network/upnp/UPnPRenderer.h
@@ -0,0 +1,73 @@
+/*
+ * 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 "interfaces/IAnnouncer.h"
+
+#include <Platinum/Source/Devices/MediaRenderer/PltMediaRenderer.h>
+
+class CVariant;
+
+namespace UPNP
+{
+
+class CRendererReferenceHolder
+{
+public:
+ PLT_DeviceHostReference m_Device;
+};
+
+class CUPnPRenderer : public PLT_MediaRenderer
+ , public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ CUPnPRenderer(const char* friendly_name,
+ bool show_ip = false,
+ const char* uuid = NULL,
+ unsigned int port = 0);
+
+ ~CUPnPRenderer() override;
+
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+ void UpdateState();
+
+ // Http server handler
+ NPT_Result ProcessHttpGetRequest(NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response) override;
+
+ // AVTransport methods
+ NPT_Result OnNext(PLT_ActionReference& action) override;
+ NPT_Result OnPause(PLT_ActionReference& action) override;
+ NPT_Result OnPlay(PLT_ActionReference& action) override;
+ NPT_Result OnPrevious(PLT_ActionReference& action) override;
+ NPT_Result OnStop(PLT_ActionReference& action) override;
+ NPT_Result OnSeek(PLT_ActionReference& action) override;
+ NPT_Result OnSetAVTransportURI(PLT_ActionReference& action) override;
+ NPT_Result OnSetNextAVTransportURI(PLT_ActionReference& action) override;
+
+ // RenderingControl methods
+ NPT_Result OnSetVolume(PLT_ActionReference& action) override;
+ NPT_Result OnSetMute(PLT_ActionReference& action) override;
+
+private:
+ NPT_Result SetupServices() override;
+ NPT_Result SetupIcons() override;
+ NPT_Result GetMetadata(NPT_String& meta);
+ NPT_Result PlayMedia(const NPT_String& uri,
+ const NPT_String& meta,
+ PLT_Action* action = NULL);
+ NPT_Mutex m_state;
+};
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPServer.cpp b/xbmc/network/upnp/UPnPServer.cpp
new file mode 100644
index 0000000..8e8432e
--- /dev/null
+++ b/xbmc/network/upnp/UPnPServer.cpp
@@ -0,0 +1,1388 @@
+/*
+ * 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 "UPnPServer.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "UPnPInternal.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MusicDatabaseDirectory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "music/Artist.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicLibraryQueue.h"
+#include "music/MusicThumbLoader.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Digest.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoLibraryQueue.h"
+#include "video/VideoThumbLoader.h"
+#include "view/GUIViewState.h"
+#include "xbmc/interfaces/AnnouncementManager.h"
+
+#include <Platinum/Source/Platinum/Platinum.h>
+
+NPT_SET_LOCAL_LOGGER("xbmc.upnp.server")
+
+using namespace ANNOUNCEMENT;
+using namespace XFILE;
+using KODI::UTILITY::CDigest;
+
+namespace UPNP
+{
+
+NPT_UInt32 CUPnPServer::m_MaxReturnedItems = 0;
+
+const char* audio_containers[] = { "musicdb://genres/", "musicdb://artists/", "musicdb://albums/",
+ "musicdb://songs/", "musicdb://recentlyaddedalbums/", "musicdb://years/",
+ "musicdb://singles/" };
+
+const char* video_containers[] = { "library://video/movies/titles.xml/", "library://video/tvshows/titles.xml/",
+ "videodb://recentlyaddedmovies/", "videodb://recentlyaddedepisodes/" };
+
+/*----------------------------------------------------------------------
+| CUPnPServer::CUPnPServer
++---------------------------------------------------------------------*/
+CUPnPServer::CUPnPServer(const char* friendly_name, const char* uuid /*= NULL*/, int port /*= 0*/)
+ : PLT_MediaConnect(friendly_name, false, uuid, port),
+ PLT_FileMediaConnectDelegate("/", "/"),
+ m_scanning(CMusicLibraryQueue::GetInstance().IsScanningLibrary() ||
+ CVideoLibraryQueue::GetInstance().IsScanningLibrary()),
+ m_logger(CServiceBroker::GetLogging().GetLogger(
+ StringUtils::Format("CUPnPServer[{}]", friendly_name)))
+{
+}
+
+CUPnPServer::~CUPnPServer()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::ProcessGetSCPD
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::ProcessGetSCPD(PLT_Service* service,
+ NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response)
+{
+ // needed because PLT_MediaConnect only allows Xbox360 & WMP to search
+ return PLT_MediaServer::ProcessGetSCPD(service, request, context, response);
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::SetupServices
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::SetupServices()
+{
+ PLT_MediaConnect::SetupServices();
+ PLT_Service* service = NULL;
+ NPT_Result result = FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service);
+ if (service)
+ {
+ service->SetStateVariable("SearchCapabilities", "upnp:class");
+ service->SetStateVariable("SortCapabilities", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating,upnp:episodeCount,upnp:episodeSeason,xbmc:rating,xbmc:dateadded,xbmc:votes");
+ }
+
+ m_scanning = true;
+ OnScanCompleted(AudioLibrary);
+ m_scanning = true;
+ OnScanCompleted(VideoLibrary);
+
+ // now safe to start passing on new notifications
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+
+ return result;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnScanCompleted
++---------------------------------------------------------------------*/
+void
+CUPnPServer::OnScanCompleted(int type)
+{
+ if (type == AudioLibrary) {
+ for (const char* const audio_container : audio_containers)
+ UpdateContainer(audio_container);
+ }
+ else if (type == VideoLibrary) {
+ for (const char* const video_container : video_containers)
+ UpdateContainer(video_container);
+ }
+ else
+ return;
+ m_scanning = false;
+ PropagateUpdates();
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::UpdateContainer
++---------------------------------------------------------------------*/
+void
+CUPnPServer::UpdateContainer(const std::string& id)
+{
+ std::map<std::string, std::pair<bool, unsigned long> >::iterator itr = m_UpdateIDs.find(id);
+ unsigned long count = 0;
+ if (itr != m_UpdateIDs.end())
+ count = ++itr->second.second;
+ m_UpdateIDs[id] = std::make_pair(true, count);
+ PropagateUpdates();
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::PropagateUpdates
++---------------------------------------------------------------------*/
+void
+CUPnPServer::PropagateUpdates()
+{
+ PLT_Service* service = NULL;
+ NPT_String current_ids;
+ std::string buffer;
+ std::map<std::string, std::pair<bool, unsigned long> >::iterator itr;
+
+ if (m_scanning || !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_UPNPANNOUNCE))
+ return;
+
+ NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), failed);
+
+ // we pause, and we must retain any changes which have not been
+ // broadcast yet
+ NPT_CHECK_LABEL(service->PauseEventing(), failed);
+ NPT_CHECK_LABEL(service->GetStateVariableValue("ContainerUpdateIDs", current_ids), failed);
+ buffer = (const char*)current_ids;
+ if (!buffer.empty())
+ buffer.append(",");
+
+ // only broadcast ids with modified bit set
+ for (itr = m_UpdateIDs.begin(); itr != m_UpdateIDs.end(); ++itr) {
+ if (itr->second.first) {
+ buffer.append(StringUtils::Format("{},{},", itr->first, itr->second.second));
+ itr->second.first = false;
+ }
+ }
+
+ // set the value, Platinum will clear ContainerUpdateIDs after sending
+ NPT_CHECK_LABEL(service->SetStateVariable("ContainerUpdateIDs", buffer.substr(0,buffer.size()-1).c_str(), true), failed);
+ NPT_CHECK_LABEL(service->IncStateVariable("SystemUpdateID"), failed);
+
+ service->PauseEventing(false);
+ return;
+
+failed:
+ // should attempt to start eventing on a failure
+ if (service) service->PauseEventing(false);
+ m_logger->error("Unable to propagate updates");
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::SetupIcons
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::SetupIcons()
+{
+ NPT_String file_root = CSpecialProtocol::TranslatePath("special://xbmc/media/").c_str();
+ AddIcon(
+ PLT_DeviceIcon("image/png", 256, 256, 8, "/icon256x256.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 120, 120, 8, "/icon120x120.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 48, 48, 8, "/icon48x48.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 32, 32, 8, "/icon32x32.png"),
+ file_root);
+ AddIcon(
+ PLT_DeviceIcon("image/png", 16, 16, 8, "/icon16x16.png"),
+ file_root);
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::BuildSafeResourceUri
++---------------------------------------------------------------------*/
+NPT_String CUPnPServer::BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
+ const char* host,
+ const char* file_path)
+{
+ CURL url(file_path);
+ std::string md5;
+ std::string mapped_file_path(file_path);
+
+ // determine the filename to provide context to md5'd urls
+ std::string filename;
+ if (url.IsProtocol("image"))
+ {
+ filename = URIUtils::GetFileName(url.GetHostName());
+ // Remove trailing / for Platinum/Neptune to recognize the file extension and use the correct mime type when serving the image
+ URIUtils::RemoveSlashAtEnd(mapped_file_path);
+ }
+ else
+ filename = URIUtils::GetFileName(mapped_file_path);
+
+ filename = CURL::Encode(filename);
+ md5 = CDigest::Calculate(CDigest::Type::MD5, mapped_file_path);
+ md5 += "/" + filename;
+ { NPT_AutoLock lock(m_FileMutex);
+ NPT_CHECK(m_FileMap.Put(md5.c_str(), mapped_file_path.c_str()));
+ }
+ return PLT_FileMediaServer::BuildSafeResourceUri(rooturi, host, md5.c_str());
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::Build
++---------------------------------------------------------------------*/
+PLT_MediaObject* CUPnPServer::Build(const std::shared_ptr<CFileItem>& item,
+ bool with_count,
+ const PLT_HttpRequestContext& context,
+ NPT_Reference<CThumbLoader>& thumb_loader,
+ const char* parent_id /* = NULL */)
+{
+ PLT_MediaObject* object = NULL;
+ NPT_String path = item->GetPath().c_str();
+
+ //HACK: temporary disabling count as it thrashes HDD
+ with_count = false;
+
+ m_logger->debug("Preparing upnp object for item '{}'", (const char*)path);
+
+ if (path.StartsWith("virtualpath://upnproot")) {
+ path.TrimRight("/");
+ item->m_bIsFolder = true;
+ if (path.StartsWith("virtualpath://")) {
+ object = new PLT_MediaContainer;
+ object->m_Title = item->GetLabel().c_str();
+ object->m_ObjectClass.type = "object.container";
+ object->m_ObjectID = path;
+
+ // root
+ object->m_ObjectID = "0";
+ object->m_ParentID = "-1";
+ // root has 5 children
+
+ //This is dead code because of the HACK a few lines up setting with_count to false
+ //if (with_count) {
+ // ((PLT_MediaContainer*)object)->m_ChildrenCount = 5;
+ //}
+ } else {
+ goto failure;
+ }
+
+ } else {
+ // db path handling
+ NPT_String file_path, share_name;
+ file_path = item->GetPath().c_str();
+ share_name = "";
+
+ if (path.StartsWith("musicdb://")) {
+ if (path == "musicdb://" ) {
+ item->SetLabel("Music Library");
+ item->SetLabelPreformatted(true);
+ item->m_bIsFolder = true;
+ } else {
+ if (!item->HasMusicInfoTag()) {
+ MUSICDATABASEDIRECTORY::CQueryParams params;
+ MUSICDATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
+
+ CMusicDatabase db;
+ if (!db.Open() ) return NULL;
+
+ if (params.GetSongId() >= 0 ) {
+ CSong song;
+ if (db.GetSong(params.GetSongId(), song))
+ item->GetMusicInfoTag()->SetSong(song);
+ }
+ else if (params.GetAlbumId() >= 0 ) {
+ item->m_bIsFolder = true;
+ CAlbum album;
+ if (db.GetAlbum(params.GetAlbumId(), album, false))
+ item->GetMusicInfoTag()->SetAlbum(album);
+ }
+ else if (params.GetArtistId() >= 0 ) {
+ item->m_bIsFolder = true;
+ CArtist artist;
+ if (db.GetArtist(params.GetArtistId(), artist, false))
+ item->GetMusicInfoTag()->SetArtist(artist);
+ }
+ }
+
+ // all items appart from songs (artists, albums, etc) are folders
+ if (!item->HasMusicInfoTag() || item->GetMusicInfoTag()->GetType() != MediaTypeSong)
+ {
+ item->m_bIsFolder = true;
+ }
+
+ if (item->GetLabel().empty()) {
+ /* if no label try to grab it from node type */
+ std::string label;
+ if (CMusicDatabaseDirectory::GetLabel((const char*)path, label)) {
+ item->SetLabel(label);
+ item->SetLabelPreformatted(true);
+ }
+ }
+ }
+ } else if (file_path.StartsWith("library://") || file_path.StartsWith("videodb://")) {
+ if (path == "library://video/" ) {
+ item->SetLabel("Video Library");
+ item->SetLabelPreformatted(true);
+ item->m_bIsFolder = true;
+ } else {
+ if (!item->HasVideoInfoTag()) {
+ VIDEODATABASEDIRECTORY::CQueryParams params;
+ VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo((const char*)path, params);
+
+ CVideoDatabase db;
+ if (!db.Open() ) return NULL;
+
+ if (params.GetMovieId() >= 0 )
+ db.GetMovieInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMovieId());
+ else if (params.GetMVideoId() >= 0 )
+ db.GetMusicVideoInfo((const char*)path, *item->GetVideoInfoTag(), params.GetMVideoId());
+ else if (params.GetEpisodeId() >= 0 )
+ db.GetEpisodeInfo((const char*)path, *item->GetVideoInfoTag(), params.GetEpisodeId());
+ else if (params.GetTvShowId() >= 0)
+ {
+ if (params.GetSeason() >= 0)
+ {
+ int idSeason = db.GetSeasonId(params.GetTvShowId(), params.GetSeason());
+ if (idSeason >= 0)
+ db.GetSeasonInfo(idSeason, *item->GetVideoInfoTag());
+ }
+ else
+ db.GetTvShowInfo((const char*)path, *item->GetVideoInfoTag(), params.GetTvShowId());
+ }
+ }
+
+ if (item->GetVideoInfoTag()->m_type == MediaTypeTvShow || item->GetVideoInfoTag()->m_type == MediaTypeSeason) {
+ // for tvshows and seasons, iEpisode and playCount are
+ // invalid
+ item->m_bIsFolder = true;
+ item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("totalepisodes").asInteger();
+ item->GetVideoInfoTag()->SetPlayCount(static_cast<int>(item->GetProperty("watchedepisodes").asInteger()));
+ }
+ // if this is an item in the library without a playable path it most be a folder
+ else if (item->GetVideoInfoTag()->m_strFileNameAndPath.empty())
+ {
+ item->m_bIsFolder = true;
+ }
+
+ // try to grab title from tag
+ if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strTitle.empty()) {
+ item->SetLabel(item->GetVideoInfoTag()->m_strTitle);
+ item->SetLabelPreformatted(true);
+ }
+
+ // try to grab it from the folder
+ if (item->GetLabel().empty()) {
+ std::string label;
+ if (CVideoDatabaseDirectory::GetLabel((const char*)path, label)) {
+ item->SetLabel(label);
+ item->SetLabelPreformatted(true);
+ }
+ }
+ }
+ }
+ // playlists are folders
+ else if (item->IsPlayList())
+ {
+ item->m_bIsFolder = true;
+ }
+ // audio and not a playlist -> song, so it's not a folder
+ else if (item->IsAudio())
+ {
+ item->m_bIsFolder = false;
+ }
+ // any other type of item -> delegate to CDirectory
+ else
+ {
+ item->m_bIsFolder = CDirectory::Exists(item->GetPath());
+ }
+
+ // not a virtual path directory, new system
+ object = BuildObject(*item.get(), file_path, with_count, thumb_loader, &context, this, UPnPContentDirectory);
+
+ // set parent id if passed, otherwise it should have been determined
+ if (object && parent_id) {
+ object->m_ParentID = parent_id;
+ }
+ }
+
+ if (object) {
+ // remap Root virtualpath://upnproot/ to id "0"
+ if (object->m_ObjectID == "virtualpath://upnproot/")
+ object->m_ObjectID = "0";
+
+ // remap Parent Root virtualpath://upnproot/ to id "0"
+ if (object->m_ParentID == "virtualpath://upnproot/")
+ object->m_ParentID = "0";
+ }
+
+ return object;
+
+failure:
+ delete object;
+ return NULL;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::Announce
++---------------------------------------------------------------------*/
+void CUPnPServer::Announce(AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ NPT_String path;
+ int item_id;
+ std::string item_type;
+
+ if (sender != CAnnouncementManager::ANNOUNCEMENT_SENDER)
+ return;
+
+ if (message != "OnUpdate" && message != "OnRemove" && message != "OnScanStarted" &&
+ message != "OnScanFinished")
+ return;
+
+ if (data.isNull()) {
+ if (message == "OnScanStarted" || message == "OnCleanStarted")
+ {
+ m_scanning = true;
+ }
+ else if (message == "OnScanFinished" || message == "OnCleanFinished")
+ {
+ OnScanCompleted(flag);
+ }
+ }
+ else {
+ // handle both updates & removals
+ if (!data["item"].isNull()) {
+ item_id = (int)data["item"]["id"].asInteger();
+ item_type = data["item"]["type"].asString();
+ }
+ else {
+ item_id = (int)data["id"].asInteger();
+ item_type = data["type"].asString();
+ }
+
+ // we always update 'recently added' nodes along with the specific container,
+ // as we don't differentiate 'updates' from 'adds' in RPC interface
+ if (flag == VideoLibrary) {
+ if(item_type == MediaTypeEpisode) {
+ CVideoDatabase db;
+ if (!db.Open()) return;
+ int show_id = db.GetTvShowForEpisode(item_id);
+ int season_id = db.GetSeasonForEpisode(item_id);
+ UpdateContainer(StringUtils::Format("videodb://tvshows/titles/{}/", show_id));
+ UpdateContainer(StringUtils::Format("videodb://tvshows/titles/{}/{}/?tvshowid={}",
+ show_id, season_id, show_id));
+ UpdateContainer("videodb://recentlyaddedepisodes/");
+ }
+ else if(item_type == MediaTypeTvShow) {
+ UpdateContainer("library://video/tvshows/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedepisodes/");
+ }
+ else if(item_type == MediaTypeMovie) {
+ UpdateContainer("library://video/movies/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedmovies/");
+ }
+ else if(item_type == MediaTypeMusicVideo) {
+ UpdateContainer("library://video/musicvideos/titles.xml/");
+ UpdateContainer("videodb://recentlyaddedmusicvideos/");
+ }
+ }
+ else if (flag == AudioLibrary && item_type == MediaTypeSong) {
+ // we also update the 'songs' container is maybe a performance drop too
+ // high? would need to check if slow clients even cache at all anyway
+ CMusicDatabase db;
+ CAlbum album;
+ if (!db.Open()) return;
+ if (db.GetAlbumFromSong(item_id, album)) {
+ UpdateContainer(StringUtils::Format("musicdb://albums/{}", album.idAlbum));
+ UpdateContainer("musicdb://songs/");
+ UpdateContainer("musicdb://recentlyaddedalbums/");
+ }
+ }
+ }
+}
+
+/*----------------------------------------------------------------------
+| TranslateWMPObjectId
++---------------------------------------------------------------------*/
+static NPT_String TranslateWMPObjectId(NPT_String id, const Logger& logger)
+{
+ if (id == "0") {
+ id = "virtualpath://upnproot/";
+ } else if (id == "15") {
+ // Xbox 360 asking for videos
+ id = "library://video/";
+ } else if (id == "16") {
+ // Xbox 360 asking for photos
+ } else if (id == "107") {
+ // Sonos uses 107 for artists root container id
+ id = "musicdb://artists/";
+ } else if (id == "7") {
+ // Sonos uses 7 for albums root container id
+ id = "musicdb://albums/";
+ } else if (id == "4") {
+ // Sonos uses 4 for tracks root container id
+ id = "musicdb://songs/";
+ }
+
+ logger->debug("Translated id to '{}'", (const char*)id);
+ return id;
+}
+
+NPT_Result
+ObjectIDValidate(const NPT_String& id)
+{
+ if (CFileUtils::RemoteAccessAllowed(id.GetChars()))
+ return NPT_SUCCESS;
+ return NPT_ERROR_NO_SUCH_FILE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnBrowseMetadata
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnBrowseMetadata(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ NPT_COMPILER_UNUSED(sort_criteria);
+ NPT_COMPILER_UNUSED(requested_count);
+ NPT_COMPILER_UNUSED(starting_index);
+
+ NPT_String didl;
+ NPT_Reference<PLT_MediaObject> object;
+ NPT_String id = TranslateWMPObjectId(object_id, m_logger);
+ CFileItemPtr item;
+ NPT_Reference<CThumbLoader> thumb_loader;
+
+ m_logger->info("Received UPnP Browse Metadata request for object '{}'", object_id);
+
+ if(NPT_FAILED(ObjectIDValidate(id))) {
+ action->SetError(701, "Incorrect ObjectID.");
+ return NPT_FAILURE;
+ }
+
+ if (id.StartsWith("virtualpath://")) {
+ id.TrimRight("/");
+ if (id == "virtualpath://upnproot") {
+ id += "/";
+ item.reset(new CFileItem((const char*)id, true));
+ item->SetLabel("Root");
+ item->SetLabelPreformatted(true);
+ object = Build(item, true, context, thumb_loader);
+ object->m_ParentID = "-1";
+ } else {
+ return NPT_FAILURE;
+ }
+ } else {
+ item.reset(new CFileItem((const char*)id, false));
+
+ // attempt to determine the parent of this item
+ std::string parent;
+ if (URIUtils::IsVideoDb((const char*)id) || URIUtils::IsMusicDb((const char*)id) || StringUtils::StartsWithNoCase((const char*)id, "library://video/")) {
+ if (!URIUtils::GetParentPath((const char*)id, parent)) {
+ parent = "0";
+ }
+ }
+ else {
+ // non-library objects - playlists / sources
+ //
+ // we could instead store the parents in a hash during every browse
+ // or could handle this in URIUtils::GetParentPath() possibly,
+ // however this is quicker to implement and subsequently purge when a
+ // better solution presents itself
+ std::string child_id((const char*)id);
+ if (StringUtils::StartsWithNoCase(child_id, "special://musicplaylists/")) parent = "musicdb://";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://videoplaylists/")) parent = "library://video/";
+ else if (StringUtils::StartsWithNoCase(child_id, "sources://video/")) parent = "library://video/";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://profile/playlists/music/")) parent = "special://musicplaylists/";
+ else if (StringUtils::StartsWithNoCase(child_id, "special://profile/playlists/video/")) parent = "special://videoplaylists/";
+ else parent = "sources://video/"; // this can only match video sources
+ }
+
+ if (item->IsVideoDb()) {
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ }
+ else if (item->IsMusicDb()) {
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+ }
+ if (!thumb_loader.IsNull()) {
+ thumb_loader->OnLoaderStart();
+ }
+ object = Build(item, true, context, thumb_loader, parent.empty()?NULL:parent.c_str());
+ }
+
+ if (object.IsNull()) {
+ /* error */
+ NPT_LOG_WARNING_1("CUPnPServer::OnBrowseMetadata - Object null (%s)", object_id);
+ action->SetError(701, "No Such Object.");
+ return NPT_FAILURE;
+ }
+
+ NPT_String tmp;
+ NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
+
+ /* add didl header and footer */
+ didl = didl_header + tmp + didl_footer;
+
+ NPT_CHECK(action->SetArgumentValue("Result", didl));
+ NPT_CHECK(action->SetArgumentValue("NumberReturned", "1"));
+ NPT_CHECK(action->SetArgumentValue("TotalMatches", "1"));
+
+ // update ID may be wrong here, it should be the one of the container?
+ NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
+
+ //! @todo We need to keep track of the overall SystemUpdateID of the CDS
+
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnBrowseDirectChildren
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnBrowseDirectChildren(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ CFileItemList items;
+ NPT_String parent_id = TranslateWMPObjectId(object_id, m_logger);
+
+ m_logger->info("Received Browse DirectChildren request for object '{}', with sort criteria {}",
+ object_id, sort_criteria);
+
+ if(NPT_FAILED(ObjectIDValidate(parent_id))) {
+ action->SetError(701, "Incorrect ObjectID.");
+ return NPT_FAILURE;
+ }
+
+ items.SetPath(std::string(parent_id));
+
+ // guard against loading while saving to the same cache file
+ // as CArchive currently performs no locking itself
+ bool load;
+ { NPT_AutoLock lock(m_CacheMutex);
+ load = items.Load();
+ }
+
+ if (!load) {
+ // cache anything that takes more than a second to retrieve
+ auto start = std::chrono::steady_clock::now();
+
+ if (parent_id.StartsWith("virtualpath://upnproot")) {
+ CFileItemPtr item;
+
+ // music library
+ item.reset(new CFileItem("musicdb://", true));
+ item->SetLabel("Music Library");
+ item->SetLabelPreformatted(true);
+ items.Add(item);
+
+ // video library
+ item.reset(new CFileItem("library://video/", true));
+ item->SetLabel("Video Library");
+ item->SetLabelPreformatted(true);
+ items.Add(item);
+
+ items.Sort(SortByLabel, SortOrderAscending);
+ } else {
+ // this is the only way to hide unplayable items in the 'files'
+ // view as we cannot tell what context (eg music vs video) the
+ // request came from
+ std::string supported = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetVideoExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetMusicExtensions() + "|"
+ + CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ CDirectory::GetDirectory((const char*)parent_id, items, supported, DIR_FLAG_DEFAULTS);
+ DefaultSortItems(items);
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ if (items.CacheToDiscAlways() || (items.CacheToDiscIfSlow() && duration.count() > 1000))
+ {
+ NPT_AutoLock lock(m_CacheMutex);
+ items.Save();
+ }
+ }
+
+ // as there's no library://music support, manually add playlists and music
+ // video nodes
+ if (items.GetPath() == "musicdb://") {
+ CFileItemPtr playlists(new CFileItem("special://musicplaylists/", true));
+ playlists->SetLabel(g_localizeStrings.Get(136));
+ items.Add(playlists);
+
+ CVideoDatabase database;
+ database.Open();
+ if (database.HasContent(VideoDbContentType::MUSICVIDEOS))
+ {
+ CFileItemPtr mvideos(new CFileItem("library://video/musicvideos/", true));
+ mvideos->SetLabel(g_localizeStrings.Get(20389));
+ items.Add(mvideos);
+ }
+ }
+
+ // Don't pass parent_id if action is Search not BrowseDirectChildren, as
+ // we want the engine to determine the best parent id, not necessarily the one
+ // passed
+ NPT_String action_name = action->GetActionDesc().GetName();
+ return BuildResponse(
+ action,
+ items,
+ filter,
+ starting_index,
+ requested_count,
+ sort_criteria,
+ context,
+ (action_name.Compare("Search", true)==0)?NULL:parent_id.GetChars());
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::BuildResponse
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::BuildResponse(PLT_ActionReference& action,
+ CFileItemList& items,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context,
+ const char* parent_id /* = NULL */)
+{
+ NPT_COMPILER_UNUSED(sort_criteria);
+
+ m_logger->debug("Building UPnP response with filter '{}', starting @ {} with {} requested",
+ filter, starting_index, requested_count);
+
+ // we will reuse this ThumbLoader for all items
+ NPT_Reference<CThumbLoader> thumb_loader;
+
+ if (URIUtils::IsVideoDb(items.GetPath()) ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "library://video/") ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/video/")) {
+
+ thumb_loader = NPT_Reference<CThumbLoader>(new CVideoThumbLoader());
+ }
+ else if (URIUtils::IsMusicDb(items.GetPath()) ||
+ StringUtils::StartsWithNoCase(items.GetPath(), "special://profile/playlists/music/")) {
+
+ thumb_loader = NPT_Reference<CThumbLoader>(new CMusicThumbLoader());
+ }
+ if (!thumb_loader.IsNull()) {
+ thumb_loader->OnLoaderStart();
+ }
+
+ // this isn't pretty but needed to properly hide the addons node from clients
+ if (StringUtils::StartsWith(items.GetPath(), "library")) {
+ for (int i=0; i<items.Size(); i++) {
+ if (StringUtils::StartsWith(items[i]->GetPath(), "addons") ||
+ StringUtils::EndsWith(items[i]->GetPath(), "/addons.xml/"))
+ items.Remove(i);
+ }
+ }
+
+ // won't return more than UPNP_MAX_RETURNED_ITEMS items at a time to keep things smooth
+ // 0 requested means as many as possible
+ NPT_UInt32 max_count = (requested_count == 0)?m_MaxReturnedItems:std::min((unsigned long)requested_count, (unsigned long)m_MaxReturnedItems);
+ NPT_UInt32 stop_index = std::min((unsigned long)(starting_index + max_count), (unsigned long)items.Size()); // don't return more than we can
+
+ NPT_Cardinal count = 0;
+ NPT_Cardinal total = items.Size();
+ NPT_String didl = didl_header;
+ PLT_MediaObjectReference object;
+ for (unsigned long i=starting_index; i<stop_index; ++i) {
+ object = Build(items[i], true, context, thumb_loader, parent_id);
+ if (object.IsNull()) {
+ // don't tell the client this item ever existed
+ --total;
+ continue;
+ }
+
+ NPT_String tmp;
+ NPT_CHECK(PLT_Didl::ToDidl(*object.AsPointer(), filter, tmp));
+
+ // Neptunes string growing is dead slow for small additions
+ if (didl.GetCapacity() < tmp.GetLength() + didl.GetLength()) {
+ didl.Reserve((tmp.GetLength() + didl.GetLength())*2);
+ }
+ didl += tmp;
+ ++count;
+ }
+
+ didl += didl_footer;
+
+ m_logger->debug("Returning UPnP response with {} items out of {} total matches", count, total);
+
+ NPT_CHECK(action->SetArgumentValue("Result", didl));
+ NPT_CHECK(action->SetArgumentValue("NumberReturned", NPT_String::FromInteger(count)));
+ NPT_CHECK(action->SetArgumentValue("TotalMatches", NPT_String::FromInteger(total)));
+ NPT_CHECK(action->SetArgumentValue("UpdateId", "0"));
+ return NPT_SUCCESS;
+}
+
+/*----------------------------------------------------------------------
+| FindSubCriteria
++---------------------------------------------------------------------*/
+static
+NPT_String
+FindSubCriteria(NPT_String criteria, const char* name)
+{
+ NPT_String result;
+ int search = criteria.Find(name);
+ if (search >= 0) {
+ criteria = criteria.Right(criteria.GetLength() - search - NPT_StringLength(name));
+ criteria.TrimLeft(" ");
+ if (criteria.GetLength()>0 && criteria[0] == '=') {
+ criteria.TrimLeft("= ");
+ if (criteria.GetLength()>0 && criteria[0] == '\"') {
+ search = criteria.Find("\"", 1);
+ if (search > 0) result = criteria.SubString(1, search-1);
+ }
+ }
+ }
+ return result;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnSearchContainer
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnSearchContainer(PLT_ActionReference& action,
+ const char* object_id,
+ const char* search_criteria,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context)
+{
+ m_logger->debug("Received Search request for object '{}' with search '{}'", object_id,
+ search_criteria);
+
+ NPT_String id = object_id;
+ NPT_String searchClass = NPT_String(search_criteria);
+ if (id.StartsWith("musicdb://")) {
+ // we browse for all tracks given a genre, artist or album
+ if (searchClass.Find("object.item.audioItem") >= 0) {
+ if (!id.EndsWith("/")) id += "/";
+ NPT_Cardinal count = id.SubString(10).Split("/").GetItemCount();
+ // remove extra empty node count
+ count = count?count-1:0;
+
+ // genre
+ if (id.StartsWith("musicdb://genres/")) {
+ // all tracks of all genres
+ if (count == 1)
+ id += "-1/-1/-1/";
+ // all tracks of a specific genre
+ else if (count == 2)
+ id += "-1/-1/";
+ // all tracks of a specific genre of a specific artist
+ else if (count == 3)
+ id += "-1/";
+ }
+ else if (id.StartsWith("musicdb://artists/")) {
+ // all tracks by all artists
+ if (count == 1)
+ id += "-1/-1/";
+ // all tracks of a specific artist
+ else if (count == 2)
+ id += "-1/";
+ }
+ else if (id.StartsWith("musicdb://albums/")) {
+ // all albums ?
+ if (count == 1) id += "-1/";
+ }
+ }
+ return OnBrowseDirectChildren(action, id, filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.item.audioItem") >= 0) {
+ // look for artist, album & genre filters
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+ NPT_String album = FindSubCriteria(searchClass, "upnp:album");
+ NPT_String artist = FindSubCriteria(searchClass, "upnp:artist");
+ // sonos looks for microsoft specific stuff
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistPerformer");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistAlbumArtist");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:authorComposer");
+
+ CMusicDatabase database;
+ database.Open();
+
+ if (genre.GetLength() > 0) {
+ // all tracks by genre filtered by artist and/or album
+ std::string strPath = StringUtils::Format(
+ "musicdb://genres/{}/{}/{}/", database.GetGenreByName((const char*)genre),
+ database.GetArtistByName((const char*)artist), // will return -1 if no artist
+ database.GetAlbumByName((const char*)album)); // will return -1 if no album
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ } else if (artist.GetLength() > 0) {
+ // all tracks by artist name filtered by album if passed
+ std::string strPath = StringUtils::Format(
+ "musicdb://artists/{}/{}/", database.GetArtistByName((const char*)artist),
+ database.GetAlbumByName((const char*)album)); // will return -1 if no album
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ } else if (album.GetLength() > 0) {
+ // all tracks by album name
+ std::string strPath = StringUtils::Format("musicdb://albums/{}/",
+ database.GetAlbumByName((const char*)album));
+
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ }
+
+ // browse all songs
+ return OnBrowseDirectChildren(action, "musicdb://songs/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.album.musicAlbum") >= 0) {
+ // sonos filters by genre
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+
+ // 360 hack: artist/albums using search
+ NPT_String artist = FindSubCriteria(searchClass, "upnp:artist");
+ // sonos looks for microsoft specific stuff
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistPerformer");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:artistAlbumArtist");
+ artist = artist.GetLength()?artist:FindSubCriteria(searchClass, "microsoft:authorComposer");
+
+ CMusicDatabase database;
+ database.Open();
+
+ if (genre.GetLength() > 0) {
+ std::string strPath = StringUtils::Format(
+ "musicdb://genres/{}/{}/", database.GetGenreByName((const char*)genre),
+ database.GetArtistByName((const char*)artist)); // no artist should return -1
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index,
+ requested_count, sort_criteria, context);
+ } else if (artist.GetLength() > 0) {
+ std::string strPath = StringUtils::Format("musicdb://artists/{}/",
+ database.GetArtistByName((const char*)artist));
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index,
+ requested_count, sort_criteria, context);
+ }
+
+ // all albums
+ return OnBrowseDirectChildren(action, "musicdb://albums/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.person.musicArtist") >= 0) {
+ // Sonos filters by genre
+ NPT_String genre = FindSubCriteria(searchClass, "upnp:genre");
+ if (genre.GetLength() > 0) {
+ CMusicDatabase database;
+ database.Open();
+ std::string strPath = StringUtils::Format("musicdb://genres/{}/",
+ database.GetGenreByName((const char*)genre));
+ return OnBrowseDirectChildren(action, strPath.c_str(), filter, starting_index, requested_count, sort_criteria, context);
+ }
+ return OnBrowseDirectChildren(action, "musicdb://artists/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.genre.musicGenre") >= 0) {
+ return OnBrowseDirectChildren(action, "musicdb://genres/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.playlistContainer") >= 0) {
+ return OnBrowseDirectChildren(action, "special://musicplaylists/", filter, starting_index, requested_count, sort_criteria, context);
+ } else if (searchClass.Find("object.container.album.videoAlbum.videoBroadcastShow") >= 0) {
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ CFileItemList items;
+ if (!database.GetTvShowsByWhere("videodb://tvshows/titles/?local", CDatabase::Filter(),
+ items, SortDescription(), GetRequiredVideoDbDetails(NPT_String(filter)))) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ items.SetPath("videodb://tvshows/titles/");
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.container.album.videoAlbum.videoBroadcastSeason") >= 0) {
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ CFileItemList items;
+ if (!database.GetSeasonsByWhere("videodb://tvshows/titles/-1/?local", CDatabase::Filter(), items, true)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ items.SetPath("videodb://tvshows/titles/-1/");
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.item.videoItem") >= 0) {
+ CFileItemList items, allItems;
+
+ CVideoDatabase database;
+ if (!database.Open()) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ bool allVideoItems = searchClass.Compare("object.item.videoItem") == 0;
+
+ // determine the required videodb details to be retrieved
+ int requiredVideoDbDetails = GetRequiredVideoDbDetails(NPT_String(filter));
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.movie") >= 0)
+ {
+ if (!database.GetMoviesByWhere("videodb://movies/titles/?local", CDatabase::Filter(), items, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://movies/titles/");
+ }
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.videoBroadcast") >= 0)
+ {
+ if (!database.GetEpisodesByWhere("videodb://tvshows/titles/?local", CDatabase::Filter(), items, true, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://tvshows/titles/");
+ }
+
+ if (allVideoItems || searchClass.Find("object.item.videoItem.musicVideoClip") >= 0)
+ {
+ if (!database.GetMusicVideosByWhere("videodb://musicvideos/titles/?local", CDatabase::Filter(), items, true, SortDescription(), requiredVideoDbDetails)) {
+ action->SetError(800, "Internal Error");
+ return NPT_SUCCESS;
+ }
+
+ allItems.Append(items);
+ items.Clear();
+
+ if (!allVideoItems)
+ allItems.SetPath("videodb://musicvideos/titles/");
+ }
+
+ if (allVideoItems)
+ allItems.SetPath("videodb://movies/titles/");
+
+ return BuildResponse(action, allItems, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ } else if (searchClass.Find("object.item.imageItem") >= 0) {
+ CFileItemList items;
+ return BuildResponse(action, items, filter, starting_index, requested_count, sort_criteria, context, NULL);
+ }
+
+ return NPT_FAILURE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::OnUpdateObject
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::OnUpdateObject(PLT_ActionReference& action,
+ const char* object_id,
+ NPT_Map<NPT_String,NPT_String>& current_vals,
+ NPT_Map<NPT_String,NPT_String>& new_vals,
+ const PLT_HttpRequestContext& context)
+{
+ std::string path(CURL::Decode(object_id));
+ CFileItem updated;
+ updated.SetPath(path);
+ m_logger->info("OnUpdateObject: {} from {}", path,
+ (const char*)context.GetRemoteAddress().GetIpAddress().ToString());
+
+ NPT_String playCount, position, lastPlayed;
+ int err;
+ const char* msg = NULL;
+ bool updatelisting(false);
+
+ // we pause eventing as multiple announces may happen in this operation
+ PLT_Service* service = NULL;
+ NPT_CHECK_LABEL(FindServiceById("urn:upnp-org:serviceId:ContentDirectory", service), error);
+ NPT_CHECK_LABEL(service->PauseEventing(), error);
+
+ if (updated.IsVideoDb()) {
+ CVideoDatabase db;
+ NPT_CHECK_LABEL(!db.Open(), error);
+
+ // must first determine type of file from object id
+ VIDEODATABASEDIRECTORY::CQueryParams params;
+ VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(path.c_str(), params);
+
+ int id = -1;
+ VideoDbContentType content_type;
+ if ((id = params.GetMovieId()) >= 0 )
+ content_type = VideoDbContentType::MOVIES;
+ else if ((id = params.GetEpisodeId()) >= 0 )
+ content_type = VideoDbContentType::EPISODES;
+ else if ((id = params.GetMVideoId()) >= 0 )
+ content_type = VideoDbContentType::MUSICVIDEOS;
+ else {
+ err = 701;
+ msg = "No such object";
+ goto failure;
+ }
+
+ std::string file_path;
+ db.GetFilePathById(id, file_path, content_type);
+ CVideoInfoTag tag;
+ db.LoadVideoInfo(file_path, tag);
+ updated.SetFromVideoInfoTag(tag);
+ m_logger->info("Translated to {}", file_path);
+
+ position = new_vals["lastPlaybackPosition"];
+ playCount = new_vals["playCount"];
+ lastPlayed = new_vals["lastPlaybackTime"];
+
+
+ if (!position.IsEmpty()
+ && position.Compare(current_vals["lastPlaybackPosition"]) != 0) {
+ NPT_UInt32 resume;
+ NPT_CHECK_LABEL(position.ToInteger32(resume), args);
+
+ if (resume <= 0)
+ db.ClearBookMarksOfFile(file_path, CBookmark::RESUME);
+ else {
+ CBookmark bookmark;
+ bookmark.timeInSeconds = resume;
+ bookmark.totalTimeInSeconds = resume + 100; // not required to be correct
+ bookmark.playerState = new_vals["lastPlayerState"];
+
+ db.AddBookMarkToFile(file_path, bookmark, CBookmark::RESUME);
+ }
+ if (playCount.IsEmpty()) {
+ CVariant data;
+ data["id"] = updated.GetVideoInfoTag()->m_iDbId;
+ data["type"] = updated.GetVideoInfoTag()->m_type;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
+ "OnUpdate", data);
+ }
+ updatelisting = true;
+ }
+
+ if (!playCount.IsEmpty()
+ && playCount.Compare(current_vals["playCount"]) != 0) {
+
+ NPT_UInt32 count;
+ NPT_CHECK_LABEL(playCount.ToInteger32(count), args);
+
+ CDateTime lastPlayedObj;
+ if (!lastPlayed.IsEmpty() &&
+ lastPlayed.Compare(current_vals["lastPlaybackTime"]) != 0)
+ lastPlayedObj.SetFromW3CDateTime(lastPlayed.GetChars());
+
+ db.SetPlayCount(updated, count, lastPlayedObj);
+ updatelisting = true;
+ }
+
+ // we must load the changed settings before propagating to local UI
+ if (updatelisting) {
+ db.LoadVideoInfo(file_path, tag);
+ updated.SetFromVideoInfoTag(tag);
+ //! TODO: we should find a way to avoid obtaining the artwork just to
+ // update the playcount or similar properties. Maybe a flag in the GUI
+ // update message to inform we should only update the playback properties
+ // without touching other parts of the item.
+ CVideoThumbLoader().FillLibraryArt(updated);
+ }
+
+ } else if (updated.IsMusicDb()) {
+ //! @todo implement this
+
+ } else {
+ err = 701;
+ msg = "No such object";
+ goto failure;
+ }
+
+ if (updatelisting) {
+ updated.SetPath(path);
+ if (updated.IsVideoDb())
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ else if (updated.IsMusicDb())
+ CUtil::DeleteMusicDatabaseDirectoryCache();
+
+ CFileItemPtr msgItem(new CFileItem(updated));
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, GUI_MSG_UPDATE_ITEM, GUI_MSG_FLAG_UPDATE_LIST, msgItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+ }
+
+ NPT_CHECK_LABEL(service->PauseEventing(false), error);
+ return NPT_SUCCESS;
+
+args:
+ err = 402;
+ msg = "Invalid args";
+ goto failure;
+
+error:
+ err = 501;
+ msg = "Internal error";
+
+failure:
+ m_logger->error("OnUpdateObject failed with err {}: {}", err, msg);
+ action->SetError(err, msg);
+ service->PauseEventing(false);
+ return NPT_FAILURE;
+}
+
+/*----------------------------------------------------------------------
+| CUPnPServer::ServeFile
++---------------------------------------------------------------------*/
+NPT_Result
+CUPnPServer::ServeFile(const NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response,
+ const NPT_String& md5)
+{
+ // Translate hash to filename
+ NPT_String file_path(md5), *file_path2;
+ { NPT_AutoLock lock(m_FileMutex);
+ if(NPT_SUCCEEDED(m_FileMap.Get(md5, file_path2))) {
+ file_path = *file_path2;
+ m_logger->debug("Received request to serve '{}' = '{}'", (const char*)md5,
+ (const char*)file_path);
+ } else {
+ m_logger->debug("Received request to serve unknown md5 '{}'", (const char*)md5);
+ response.SetStatus(404, "File Not Found");
+ return NPT_SUCCESS;
+ }
+ }
+
+ // File requested
+ NPT_HttpUrl rooturi(context.GetLocalAddress().GetIpAddress().ToString(), context.GetLocalAddress().GetPort(), "/");
+
+ if (file_path.Left(8).Compare("stack://", true) == 0) {
+
+ NPT_List<NPT_String> files = file_path.SubString(8).Split(" , ");
+ if (files.GetItemCount() == 0) {
+ response.SetStatus(404, "File Not Found");
+ return NPT_SUCCESS;
+ }
+
+ NPT_String output;
+ output.Reserve(file_path.GetLength()*2);
+ output += "#EXTM3U\r\n";
+
+ NPT_List<NPT_String>::Iterator url = files.GetFirstItem();
+ for (;url;url++) {
+ output += ("#EXTINF:-1," + URIUtils::GetFileName((const char*)*url)).c_str();
+ output += "\r\n";
+ output += BuildSafeResourceUri(
+ rooturi,
+ context.GetLocalAddress().GetIpAddress().ToString(),
+ *url);
+ output += "\r\n";
+ }
+
+ PLT_HttpHelper::SetBody(response, (const char*)output, output.GetLength());
+ response.GetHeaders().SetHeader("Content-Disposition", "inline; filename=\"stack.m3u\"");
+ return NPT_SUCCESS;
+ }
+
+ if (URIUtils::IsURL(static_cast<const char*>(file_path)))
+ {
+ CURL url(CTextureUtils::UnwrapImageURL(static_cast<const char*>(file_path)));
+ std::string disp = "inline; filename=\"" + URIUtils::GetFileName(url) + "\"";
+ response.GetHeaders().SetHeader("Content-Disposition", disp.c_str());
+ }
+
+ // set getCaptionInfo.sec - sets subtitle uri for Samsung devices
+ const NPT_String* captionInfoHeader = request.GetHeaders().GetHeaderValue("getCaptionInfo.sec");
+ if (captionInfoHeader)
+ {
+ NPT_String *sub_uri, movie;
+ movie = "subtitle://" + md5;
+
+ NPT_AutoLock lock(m_FileMutex);
+ if (NPT_SUCCEEDED(m_FileMap.Get(movie, sub_uri)))
+ {
+ response.GetHeaders().SetHeader("CaptionInfo.sec", sub_uri->GetChars(), false);
+ }
+ }
+
+ return PLT_HttpServer::ServeFile(request,
+ context,
+ response,
+ file_path);
+}
+
+void
+CUPnPServer::DefaultSortItems(CFileItemList& items)
+{
+ CGUIViewState* viewState = CGUIViewState::GetViewState(items.IsVideoDb() ? WINDOW_VIDEO_NAV : -1, items);
+ if (viewState)
+ {
+ SortDescription sorting = viewState->GetSortMethod();
+ items.Sort(sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
+ delete viewState;
+ }
+}
+
+NPT_Result CUPnPServer::AddSubtitleUriForSecResponse(const NPT_String& movie_md5,
+ const NPT_String& subtitle_uri)
+{
+ /* using existing m_FileMap to store subtitle uri for movie,
+ adding subtitle:// prefix, because there is already entry for movie md5 with movie path */
+ NPT_String movie = "subtitle://" + movie_md5;
+
+ NPT_AutoLock lock(m_FileMutex);
+ NPT_CHECK(m_FileMap.Put(movie, subtitle_uri));
+
+ return NPT_SUCCESS;
+}
+
+int CUPnPServer::GetRequiredVideoDbDetails(const NPT_String& filter)
+{
+ int details = VideoDbDetailsRating;
+ if (filter.Find("res@resolution") >= 0 || filter.Find("res@nrAudioChannels") >= 0)
+ details |= VideoDbDetailsStream;
+ if (filter.Find("upnp:actor") >= 0)
+ details |= VideoDbDetailsCast;
+
+ return details;
+}
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPServer.h b/xbmc/network/upnp/UPnPServer.h
new file mode 100644
index 0000000..1c92e8b
--- /dev/null
+++ b/xbmc/network/upnp/UPnPServer.h
@@ -0,0 +1,159 @@
+/*
+ * 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 "interfaces/IAnnouncer.h"
+#include "utils/logtypes.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include <Platinum/Source/Devices/MediaConnect/PltMediaConnect.h>
+
+class CFileItem;
+class CFileItemList;
+class CThumbLoader;
+class CVariant;
+class PLT_MediaObject;
+class PLT_HttpRequestContext;
+
+namespace UPNP
+{
+
+class CUPnPServer : public PLT_MediaConnect,
+ public PLT_FileMediaConnectDelegate,
+ public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ CUPnPServer(const char* friendly_name, const char* uuid = NULL, int port = 0);
+ ~CUPnPServer() override;
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ // PLT_MediaServer methods
+ NPT_Result OnBrowseMetadata(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context) override;
+ NPT_Result OnBrowseDirectChildren(PLT_ActionReference& action,
+ const char* object_id,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context) override;
+ NPT_Result OnSearchContainer(PLT_ActionReference& action,
+ const char* container_id,
+ const char* search_criteria,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context) override;
+
+ NPT_Result OnUpdateObject(PLT_ActionReference& action,
+ const char* object_id,
+ NPT_Map<NPT_String,NPT_String>& current_vals,
+ NPT_Map<NPT_String,NPT_String>& new_vals,
+ const PLT_HttpRequestContext& context) override;
+
+ // PLT_FileMediaServer methods
+ NPT_Result ServeFile(const NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response,
+ const NPT_String& file_path) override;
+
+ // PLT_DeviceHost methods
+ NPT_Result ProcessGetSCPD(PLT_Service* service,
+ NPT_HttpRequest& request,
+ const NPT_HttpRequestContext& context,
+ NPT_HttpResponse& response) override;
+
+ NPT_Result SetupServices() override;
+ NPT_Result SetupIcons() override;
+ NPT_String BuildSafeResourceUri(const NPT_HttpUrl &rooturi,
+ const char* host,
+ const char* file_path);
+
+ void AddSafeResourceUri(PLT_MediaObject* object,
+ const NPT_HttpUrl& rooturi,
+ const NPT_List<NPT_IpAddress>& ips,
+ const char* file_path,
+ const NPT_String& info)
+ {
+ PLT_MediaItemResource res;
+ for(NPT_List<NPT_IpAddress>::Iterator ip = ips.GetFirstItem(); ip; ++ip) {
+ res.m_ProtocolInfo = PLT_ProtocolInfo(info);
+ res.m_Uri = BuildSafeResourceUri(rooturi, (*ip).ToString(), file_path);
+ object->m_Resources.Add(res);
+ }
+ }
+
+ /* Samsung's devices get subtitles from header in response (for movie file), not from didl.
+ It's a way to store subtitle uri generated when building didl, to use later in http response*/
+ NPT_Result AddSubtitleUriForSecResponse(const NPT_String& movie_md5,
+ const NPT_String& subtitle_uri);
+
+
+ private:
+ void OnScanCompleted(int type);
+ void UpdateContainer(const std::string& id);
+ void PropagateUpdates();
+
+ PLT_MediaObject* Build(const std::shared_ptr<CFileItem>& item,
+ bool with_count,
+ const PLT_HttpRequestContext& context,
+ NPT_Reference<CThumbLoader>& thumbLoader,
+ const char* parent_id = NULL);
+ NPT_Result BuildResponse(PLT_ActionReference& action,
+ CFileItemList& items,
+ const char* filter,
+ NPT_UInt32 starting_index,
+ NPT_UInt32 requested_count,
+ const char* sort_criteria,
+ const PLT_HttpRequestContext& context,
+ const char* parent_id /* = NULL */);
+
+ // class methods
+ static void DefaultSortItems(CFileItemList& items);
+ static NPT_String GetParentFolder(const NPT_String& file_path)
+ {
+ int index = file_path.ReverseFind("\\");
+ if (index == -1)
+ return "";
+
+ return file_path.Left(index);
+ }
+
+ static int GetRequiredVideoDbDetails(const NPT_String& filter);
+
+ NPT_Mutex m_CacheMutex;
+
+ NPT_Mutex m_FileMutex;
+ NPT_Map<NPT_String, NPT_String> m_FileMap;
+
+ std::map<std::string, std::pair<bool, unsigned long> > m_UpdateIDs;
+ bool m_scanning;
+
+ Logger m_logger;
+
+ public:
+ // class members
+ static NPT_UInt32 m_MaxReturnedItems;
+};
+
+} /* namespace UPNP */
+
diff --git a/xbmc/network/upnp/UPnPSettings.cpp b/xbmc/network/upnp/UPnPSettings.cpp
new file mode 100644
index 0000000..30ad1fd
--- /dev/null
+++ b/xbmc/network/upnp/UPnPSettings.cpp
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2013-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 "UPnPSettings.h"
+
+#include "ServiceBroker.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#define XML_UPNP "upnpserver"
+#define XML_SERVER_UUID "UUID"
+#define XML_SERVER_PORT "Port"
+#define XML_MAX_ITEMS "MaxReturnedItems"
+#define XML_RENDERER_UUID "UUIDRenderer"
+#define XML_RENDERER_PORT "PortRenderer"
+
+CUPnPSettings::CUPnPSettings() : m_logger(CServiceBroker::GetLogging().GetLogger("CUPnPSettings"))
+{
+ Clear();
+}
+
+CUPnPSettings::~CUPnPSettings()
+{
+ Clear();
+}
+
+CUPnPSettings& CUPnPSettings::GetInstance()
+{
+ static CUPnPSettings sUPnPSettings;
+ return sUPnPSettings;
+}
+
+void CUPnPSettings::OnSettingsUnloaded()
+{
+ Clear();
+}
+
+bool CUPnPSettings::Load(const std::string &file)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ Clear();
+
+ if (!CFileUtils::Exists(file))
+ return false;
+
+ CXBMCTinyXML doc;
+ if (!doc.LoadFile(file))
+ {
+ m_logger->error("error loading {}, Line {}\n{}", file, doc.ErrorRow(), doc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement *pRootElement = doc.RootElement();
+ if (pRootElement == NULL || !StringUtils::EqualsNoCase(pRootElement->Value(), XML_UPNP))
+ {
+ m_logger->error("error loading {}, no <upnpserver> node", file);
+ return false;
+ }
+
+ // load settings
+ XMLUtils::GetString(pRootElement, XML_SERVER_UUID, m_serverUUID);
+ XMLUtils::GetInt(pRootElement, XML_SERVER_PORT, m_serverPort);
+ XMLUtils::GetInt(pRootElement, XML_MAX_ITEMS, m_maxReturnedItems);
+ XMLUtils::GetString(pRootElement, XML_RENDERER_UUID, m_rendererUUID);
+ XMLUtils::GetInt(pRootElement, XML_RENDERER_PORT, m_rendererPort);
+
+ return true;
+}
+
+bool CUPnPSettings::Save(const std::string &file) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ CXBMCTinyXML doc;
+ TiXmlElement xmlRootElement(XML_UPNP);
+ TiXmlNode *pRoot = doc.InsertEndChild(xmlRootElement);
+ if (pRoot == NULL)
+ return false;
+
+ XMLUtils::SetString(pRoot, XML_SERVER_UUID, m_serverUUID);
+ XMLUtils::SetInt(pRoot, XML_SERVER_PORT, m_serverPort);
+ XMLUtils::SetInt(pRoot, XML_MAX_ITEMS, m_maxReturnedItems);
+ XMLUtils::SetString(pRoot, XML_RENDERER_UUID, m_rendererUUID);
+ XMLUtils::SetInt(pRoot, XML_RENDERER_PORT, m_rendererPort);
+
+ // save the file
+ return doc.SaveFile(file);
+}
+
+void CUPnPSettings::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ m_serverUUID.clear();
+ m_serverPort = 0;
+ m_maxReturnedItems = 0;
+ m_rendererUUID.clear();
+ m_rendererPort = 0;
+}
diff --git a/xbmc/network/upnp/UPnPSettings.h b/xbmc/network/upnp/UPnPSettings.h
new file mode 100644
index 0000000..7469b06
--- /dev/null
+++ b/xbmc/network/upnp/UPnPSettings.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013-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/ISettingsHandler.h"
+#include "threads/CriticalSection.h"
+#include "utils/logtypes.h"
+
+#include <string>
+
+class CUPnPSettings : public ISettingsHandler
+{
+public:
+ static CUPnPSettings& GetInstance();
+
+ void OnSettingsUnloaded() override;
+
+ bool Load(const std::string &file);
+ bool Save(const std::string &file) const;
+ void Clear();
+
+ const std::string& GetServerUUID() const { return m_serverUUID; }
+ int GetServerPort() const { return m_serverPort; }
+ int GetMaximumReturnedItems() const { return m_maxReturnedItems; }
+ const std::string& GetRendererUUID() const { return m_rendererUUID; }
+ int GetRendererPort() const { return m_rendererPort; }
+
+ void SetServerUUID(const std::string &uuid) { m_serverUUID = uuid; }
+ void SetServerPort(int port) { m_serverPort = port; }
+ void SetMaximumReturnedItems(int maximumReturnedItems) { m_maxReturnedItems = maximumReturnedItems; }
+ void SetRendererUUID(const std::string &uuid) { m_rendererUUID = uuid; }
+ void SetRendererPort(int port) { m_rendererPort = port; }
+
+protected:
+ CUPnPSettings();
+ CUPnPSettings(const CUPnPSettings&) = delete;
+ CUPnPSettings& operator=(CUPnPSettings const&) = delete;
+ ~CUPnPSettings() override;
+
+private:
+ std::string m_serverUUID;
+ int m_serverPort;
+ int m_maxReturnedItems;
+ std::string m_rendererUUID;
+ int m_rendererPort;
+
+ mutable CCriticalSection m_critical;
+
+ Logger m_logger;
+};
diff --git a/xbmc/network/websocket/CMakeLists.txt b/xbmc/network/websocket/CMakeLists.txt
new file mode 100644
index 0000000..306cd6c
--- /dev/null
+++ b/xbmc/network/websocket/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES WebSocket.cpp
+ WebSocketManager.cpp
+ WebSocketV13.cpp
+ WebSocketV8.cpp)
+
+set(HEADERS WebSocket.h
+ WebSocketManager.h
+ WebSocketV13.h
+ WebSocketV8.h)
+
+core_add_library(network_websockets)
diff --git a/xbmc/network/websocket/WebSocket.cpp b/xbmc/network/websocket/WebSocket.cpp
new file mode 100644
index 0000000..bbf4a01
--- /dev/null
+++ b/xbmc/network/websocket/WebSocket.cpp
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2011-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 "WebSocket.h"
+
+#include "utils/EndianSwap.h"
+#include "utils/HttpParser.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <sstream>
+#include <string>
+
+#define MASK_FIN 0x80
+#define MASK_RSV1 0x40
+#define MASK_RSV2 0x20
+#define MASK_RSV3 0x10
+#define MASK_RSV (MASK_RSV1 | MASK_RSV2 | MASK_RSV3)
+#define MASK_OPCODE 0x0F
+#define MASK_MASK 0x80
+#define MASK_LENGTH 0x7F
+
+#define CONTROL_FRAME 0x08
+
+#define LENGTH_MIN 0x2
+
+CWebSocketFrame::CWebSocketFrame(const char* data, uint64_t length)
+{
+ reset();
+
+ if (data == NULL || length < LENGTH_MIN)
+ return;
+
+ m_free = false;
+ m_data = data;
+ m_lengthFrame = length;
+
+ // Get the FIN flag
+ m_final = ((m_data[0] & MASK_FIN) == MASK_FIN);
+ // Get the RSV1 - RSV3 flags
+ m_extension |= m_data[0] & MASK_RSV1;
+ m_extension |= (m_data[0] & MASK_RSV2) << 1;
+ m_extension |= (m_data[0] & MASK_RSV3) << 2;
+ // Get the opcode
+ m_opcode = (WebSocketFrameOpcode)(m_data[0] & MASK_OPCODE);
+ if (m_opcode >= WebSocketUnknownFrame)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Frame with invalid opcode {:2X} received", m_opcode);
+ reset();
+ return;
+ }
+ if ((m_opcode & CONTROL_FRAME) == CONTROL_FRAME && !m_final)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Fragmented control frame (opcode {:2X}) received", m_opcode);
+ reset();
+ return;
+ }
+
+ // Get the MASK flag
+ m_masked = ((m_data[1] & MASK_MASK) == MASK_MASK);
+
+ // Get the payload length
+ m_length = (uint64_t)(m_data[1] & MASK_LENGTH);
+ if ((m_length <= 125 && m_lengthFrame < m_length + LENGTH_MIN) ||
+ (m_length == 126 && m_lengthFrame < LENGTH_MIN + 2) ||
+ (m_length == 127 && m_lengthFrame < LENGTH_MIN + 8))
+ {
+ CLog::Log(LOGINFO, "WebSocket: Frame with invalid length received");
+ reset();
+ return;
+ }
+
+ if (IsControlFrame() && (m_length > 125 || !m_final))
+ {
+ CLog::Log(LOGWARNING, "WebSocket: Invalid control frame received");
+ reset();
+ return;
+ }
+
+ int offset = 0;
+ if (m_length == 126)
+ {
+ m_length = (uint64_t)Endian_SwapBE16(*(const uint16_t *)(m_data + 2));
+ offset = 2;
+ }
+ else if (m_length == 127)
+ {
+ m_length = Endian_SwapBE64(*(const uint64_t *)(m_data + 2));
+ offset = 8;
+ }
+
+ if (m_lengthFrame < LENGTH_MIN + offset + m_length)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Frame with invalid length received");
+ reset();
+ return;
+ }
+
+ // Get the mask
+ if (m_masked)
+ {
+ m_mask = *(const uint32_t *)(m_data + LENGTH_MIN + offset);
+ offset += 4;
+ }
+
+ if (m_lengthFrame != LENGTH_MIN + offset + m_length)
+ m_lengthFrame = LENGTH_MIN + offset + m_length;
+
+ // Get application data
+ if (m_length > 0)
+ m_applicationData = const_cast<char *>(m_data + LENGTH_MIN + offset);
+ else
+ m_applicationData = NULL;
+
+ // Unmask the application data if necessary
+ if (m_masked)
+ {
+ for (uint64_t index = 0; index < m_length; index++)
+ m_applicationData[index] = m_applicationData[index] ^ ((char *)(&m_mask))[index % 4];
+ }
+
+ m_valid = true;
+}
+
+CWebSocketFrame::CWebSocketFrame(WebSocketFrameOpcode opcode, const char* data /* = NULL */, uint32_t length /* = 0 */,
+ bool final /* = true */, bool masked /* = false */, int32_t mask /* = 0 */, int8_t extension /* = 0 */)
+{
+ reset();
+
+ if (opcode >= WebSocketUnknownFrame)
+ return;
+
+ m_free = true;
+ m_opcode = opcode;
+
+ m_length = length;
+
+ m_masked = masked;
+ m_mask = mask;
+ m_final = final;
+ m_extension = extension;
+
+ std::string buffer;
+ char dataByte = 0;
+
+ // Set the FIN flag
+ if (m_final)
+ dataByte |= MASK_FIN;
+
+ // Set RSV1 - RSV3 flags
+ if (m_extension != 0)
+ dataByte |= (m_extension << 4) & MASK_RSV;
+
+ // Set opcode flag
+ dataByte |= opcode & MASK_OPCODE;
+
+ buffer.push_back(dataByte);
+ dataByte = 0;
+
+ // Set MASK flag
+ if (m_masked)
+ dataByte |= MASK_MASK;
+
+ // Set payload length
+ if (m_length < 126)
+ {
+ dataByte |= m_length & MASK_LENGTH;
+ buffer.push_back(dataByte);
+ }
+ else if (m_length <= 65535)
+ {
+ dataByte |= 126 & MASK_LENGTH;
+ buffer.push_back(dataByte);
+
+ uint16_t dataLength = Endian_SwapBE16((uint16_t)m_length);
+ buffer.append((const char*)&dataLength, 2);
+ }
+ else
+ {
+ dataByte |= 127 & MASK_LENGTH;
+ buffer.push_back(dataByte);
+
+ uint64_t dataLength = Endian_SwapBE64(m_length);
+ buffer.append((const char*)&dataLength, 8);
+ }
+
+ uint64_t applicationDataOffset = 0;
+ if (data)
+ {
+ // Set masking key
+ if (m_masked)
+ {
+ buffer.append((char *)&m_mask, sizeof(m_mask));
+ applicationDataOffset = buffer.size();
+
+ for (uint64_t index = 0; index < m_length; index++)
+ buffer.push_back(data[index] ^ ((char *)(&m_mask))[index % 4]);
+ }
+ else
+ {
+ applicationDataOffset = buffer.size();
+ buffer.append(data, (unsigned int)length);
+ }
+ }
+
+ // Get the whole data
+ m_lengthFrame = buffer.size();
+ m_data = new char[(uint32_t)m_lengthFrame];
+ memcpy(const_cast<char *>(m_data), buffer.c_str(), (uint32_t)m_lengthFrame);
+
+ if (data)
+ {
+ m_applicationData = const_cast<char *>(m_data);
+ m_applicationData += applicationDataOffset;
+ }
+
+ m_valid = true;
+}
+
+CWebSocketFrame::~CWebSocketFrame()
+{
+ if (!m_valid)
+ return;
+
+ if (m_free && m_data != NULL)
+ {
+ delete[] m_data;
+ m_data = NULL;
+ }
+}
+
+void CWebSocketFrame::reset()
+{
+ m_free = false;
+ m_data = NULL;
+ m_lengthFrame = 0;
+ m_length = 0;
+ m_valid = false;
+ m_final = false;
+ m_extension = 0;
+ m_opcode = WebSocketUnknownFrame;
+ m_masked = false;
+ m_mask = 0;
+ m_applicationData = NULL;
+}
+
+CWebSocketMessage::CWebSocketMessage()
+{
+ Clear();
+}
+
+CWebSocketMessage::~CWebSocketMessage()
+{
+ for (unsigned int index = 0; index < m_frames.size(); index++)
+ delete m_frames[index];
+
+ m_frames.clear();
+}
+
+bool CWebSocketMessage::AddFrame(const CWebSocketFrame *frame)
+{
+ if (!frame->IsValid() || m_complete)
+ return false;
+
+ if (frame->IsFinal())
+ m_complete = true;
+ else
+ m_fragmented = true;
+
+ m_frames.push_back(frame);
+
+ return true;
+}
+
+void CWebSocketMessage::Clear()
+{
+ m_fragmented = false;
+ m_complete = false;
+
+ m_frames.clear();
+}
+
+const CWebSocketMessage* CWebSocket::Handle(const char* &buffer, size_t &length, bool &send)
+{
+ send = false;
+
+ while (length > 0)
+ {
+ switch (m_state)
+ {
+ case WebSocketStateConnected:
+ {
+ CWebSocketFrame *frame = GetFrame(buffer, length);
+ if (!frame->IsValid())
+ {
+ CLog::Log(LOGINFO, "WebSocket: Invalid frame received");
+ delete frame;
+ return NULL;
+ }
+
+ // adjust the length and the buffer values
+ length -= (size_t)frame->GetFrameLength();
+ buffer += frame->GetFrameLength();
+
+ if (frame->IsControlFrame())
+ {
+ if (!frame->IsFinal())
+ {
+ delete frame;
+ return NULL;
+ }
+
+ CWebSocketMessage *msg = NULL;
+ switch (frame->GetOpcode())
+ {
+ case WebSocketPing:
+ msg = GetMessage();
+ if (msg != NULL)
+ msg->AddFrame(Pong(frame->GetApplicationData()));
+ break;
+
+ case WebSocketConnectionClose:
+ CLog::Log(LOGINFO, "WebSocket: connection closed by client");
+
+ msg = GetMessage();
+ if (msg != NULL)
+ msg->AddFrame(Close());
+
+ m_state = WebSocketStateClosed;
+ break;
+
+ case WebSocketContinuationFrame:
+ case WebSocketTextFrame:
+ case WebSocketBinaryFrame:
+ case WebSocketPong:
+ case WebSocketUnknownFrame:
+ default:
+ break;
+ }
+
+ delete frame;
+
+ if (msg != NULL)
+ send = true;
+
+ return msg;
+ }
+
+ if (m_message == NULL && (m_message = GetMessage()) == NULL)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Could not allocate a new websocket message");
+ delete frame;
+ return NULL;
+ }
+
+ m_message->AddFrame(frame);
+ if (!m_message->IsComplete())
+ {
+ if (length > 0)
+ continue;
+ else
+ return NULL;
+ }
+
+ CWebSocketMessage *msg = m_message;
+ m_message = NULL;
+ return msg;
+ }
+
+ case WebSocketStateClosing:
+ {
+ CWebSocketFrame *frame = GetFrame(buffer, length);
+
+ if (frame->IsValid())
+ {
+ // adjust the length and the buffer values
+ length -= (size_t)frame->GetFrameLength();
+ buffer += frame->GetFrameLength();
+ }
+
+ if (!frame->IsValid() || frame->GetOpcode() == WebSocketConnectionClose)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Invalid or unexpected frame received (only closing handshake expected)");
+ delete frame;
+ return NULL;
+ }
+
+ m_state = WebSocketStateClosed;
+ return NULL;
+ }
+
+ case WebSocketStateNotConnected:
+ case WebSocketStateClosed:
+ case WebSocketStateHandshaking:
+ default:
+ CLog::Log(LOGINFO, "WebSocket: No frame expected in the current state");
+ return NULL;
+ }
+ }
+
+ return NULL;
+}
+
+const CWebSocketMessage* CWebSocket::Send(WebSocketFrameOpcode opcode, const char* data /* = NULL */, uint32_t length /* = 0 */)
+{
+ CWebSocketFrame *frame = GetFrame(opcode, data, length);
+ if (frame == NULL || !frame->IsValid())
+ {
+ CLog::Log(LOGINFO, "WebSocket: Trying to send an invalid frame");
+ return NULL;
+ }
+
+ CWebSocketMessage *msg = GetMessage();
+ if (msg == NULL)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Could not allocate a message");
+ return NULL;
+ }
+
+ msg->AddFrame(frame);
+ if (msg->IsComplete())
+ return msg;
+
+ return NULL;
+}
diff --git a/xbmc/network/websocket/WebSocket.h b/xbmc/network/websocket/WebSocket.h
new file mode 100644
index 0000000..8901ede
--- /dev/null
+++ b/xbmc/network/websocket/WebSocket.h
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2011-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 <stdint.h>
+#include <string>
+#include <vector>
+
+enum WebSocketFrameOpcode
+{
+ WebSocketContinuationFrame = 0x00,
+ WebSocketTextFrame = 0x01,
+ WebSocketBinaryFrame = 0x02,
+ //0x3 - 0x7 are reserved for non-control frames
+ WebSocketConnectionClose = 0x08,
+ WebSocketPing = 0x09,
+ WebSocketPong = 0x0A,
+ //0xB - 0xF are reserved for control frames
+ WebSocketUnknownFrame = 0x10
+};
+
+enum WebSocketState
+{
+ WebSocketStateNotConnected = 0,
+ WebSocketStateHandshaking = 1,
+ WebSocketStateConnected = 2,
+ WebSocketStateClosing = 3,
+ WebSocketStateClosed = 4
+};
+
+enum WebSocketCloseReason
+{
+ WebSocketCloseNormal = 1000,
+ WebSocketCloseLeaving = 1001,
+ WebSocketCloseProtocolError = 1002,
+ WebSocketCloseInvalidData = 1003,
+ WebSocketCloseFrameTooLarge = 1004,
+ // Reserved status code = 1005,
+ // Reserved status code = 1006,
+ WebSocketCloseInvalidUtf8 = 1007
+};
+
+class CWebSocketFrame
+{
+public:
+ CWebSocketFrame(const char* data, uint64_t length);
+ CWebSocketFrame(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0, bool final = true, bool masked = false, int32_t mask = 0, int8_t extension = 0);
+ virtual ~CWebSocketFrame();
+
+ virtual bool IsValid() const { return m_valid; }
+ virtual uint64_t GetFrameLength() const { return m_lengthFrame; }
+ virtual bool IsFinal() const { return m_final; }
+ virtual int8_t GetExtension() const { return m_extension; }
+ virtual WebSocketFrameOpcode GetOpcode() const { return m_opcode; }
+ virtual bool IsControlFrame() const { return (m_valid && (m_opcode & 0x8) == 0x8); }
+ virtual bool IsMasked() const { return m_masked; }
+ virtual uint64_t GetLength() const { return m_length; }
+ virtual int32_t GetMask() const { return m_mask; }
+ virtual const char* GetFrameData() const { return m_data; }
+ virtual const char* GetApplicationData() const { return m_applicationData; }
+
+protected:
+ bool m_free;
+ const char *m_data;
+ uint64_t m_lengthFrame;
+ uint64_t m_length;
+ bool m_valid;
+ bool m_final;
+ int8_t m_extension;
+ WebSocketFrameOpcode m_opcode;
+ bool m_masked;
+ int32_t m_mask;
+ char *m_applicationData;
+
+private:
+ void reset();
+ CWebSocketFrame(const CWebSocketFrame&) = delete;
+ CWebSocketFrame& operator=(const CWebSocketFrame&) = delete;
+};
+
+class CWebSocketMessage
+{
+public:
+ CWebSocketMessage();
+ virtual ~CWebSocketMessage();
+
+ virtual bool IsFragmented() const { return m_fragmented; }
+ virtual bool IsComplete() const { return m_complete; }
+
+ virtual bool AddFrame(const CWebSocketFrame* frame);
+ virtual const std::vector<const CWebSocketFrame *>& GetFrames() const { return m_frames; }
+
+ virtual void Clear();
+
+protected:
+ std::vector<const CWebSocketFrame *> m_frames;
+ bool m_fragmented;
+ bool m_complete;
+};
+
+class CWebSocket
+{
+public:
+ CWebSocket() { m_state = WebSocketStateNotConnected; m_message = NULL; }
+ virtual ~CWebSocket()
+ {
+ if (m_message)
+ delete m_message;
+ }
+
+ int GetVersion() { return m_version; }
+ WebSocketState GetState() { return m_state; }
+
+ virtual bool Handshake(const char* data, size_t length, std::string &response) = 0;
+ virtual const CWebSocketMessage* Handle(const char* &buffer, size_t &length, bool &send);
+ virtual const CWebSocketMessage* Send(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0);
+ virtual const CWebSocketFrame* Ping(const char* data = NULL) const = 0;
+ virtual const CWebSocketFrame* Pong(const char* data = NULL) const = 0;
+ virtual const CWebSocketFrame* Close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = "") = 0;
+ virtual void Fail() = 0;
+
+protected:
+ int m_version;
+ WebSocketState m_state;
+ CWebSocketMessage *m_message;
+
+ virtual CWebSocketFrame* GetFrame(const char* data, uint64_t length) = 0;
+ virtual CWebSocketFrame* GetFrame(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0, bool final = true, bool masked = false, int32_t mask = 0, int8_t extension = 0) = 0;
+ virtual CWebSocketMessage* GetMessage() = 0;
+};
diff --git a/xbmc/network/websocket/WebSocketManager.cpp b/xbmc/network/websocket/WebSocketManager.cpp
new file mode 100644
index 0000000..6fef69a
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketManager.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2011-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 "WebSocketManager.h"
+
+#include "WebSocket.h"
+#include "WebSocketV13.h"
+#include "WebSocketV8.h"
+#include "utils/HttpParser.h"
+#include "utils/HttpResponse.h"
+#include "utils/log.h"
+
+#include <string>
+
+#define WS_HTTP_METHOD "GET"
+#define WS_HTTP_TAG "HTTP/"
+#define WS_SUPPORTED_VERSIONS "8, 13"
+
+#define WS_HEADER_VERSION "Sec-WebSocket-Version"
+#define WS_HEADER_VERSION_LC "sec-websocket-version" // "Sec-WebSocket-Version"
+
+CWebSocket* CWebSocketManager::Handle(const char* data, unsigned int length, std::string &response)
+{
+ if (data == NULL || length <= 0)
+ return NULL;
+
+ HttpParser header;
+ HttpParser::status_t status = header.addBytes(data, length);
+ switch (status)
+ {
+ case HttpParser::Error:
+ case HttpParser::Incomplete:
+ response.clear();
+ return NULL;
+
+ case HttpParser::Done:
+ default:
+ break;
+ }
+
+ // There must be a "Sec-WebSocket-Version" header
+ const char* value = header.getValue(WS_HEADER_VERSION_LC);
+ if (value == NULL)
+ {
+ CLog::Log(LOGINFO, "WebSocket: missing Sec-WebSocket-Version");
+ CHttpResponse httpResponse(HTTP::Get, HTTP::BadRequest, HTTP::Version1_1);
+ response = httpResponse.Create();
+
+ return NULL;
+ }
+
+ CWebSocket *websocket = NULL;
+ if (strncmp(value, "8", 1) == 0)
+ websocket = new CWebSocketV8();
+ else if (strncmp(value, "13", 2) == 0)
+ websocket = new CWebSocketV13();
+
+ if (websocket == NULL)
+ {
+ CLog::Log(LOGINFO, "WebSocket: Unsupported Sec-WebSocket-Version {}", value);
+ CHttpResponse httpResponse(HTTP::Get, HTTP::UpgradeRequired, HTTP::Version1_1);
+ httpResponse.AddHeader(WS_HEADER_VERSION, WS_SUPPORTED_VERSIONS);
+ response = httpResponse.Create();
+
+ return NULL;
+ }
+
+ if (websocket->Handshake(data, length, response))
+ return websocket;
+
+ return NULL;
+}
diff --git a/xbmc/network/websocket/WebSocketManager.h b/xbmc/network/websocket/WebSocketManager.h
new file mode 100644
index 0000000..7cbe8e9
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketManager.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2011-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 <string>
+
+class CWebSocket;
+
+class CWebSocketManager
+{
+public:
+ static CWebSocket* Handle(const char* data, unsigned int length, std::string &response);
+};
diff --git a/xbmc/network/websocket/WebSocketV13.cpp b/xbmc/network/websocket/WebSocketV13.cpp
new file mode 100644
index 0000000..e0ef8f0
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketV13.cpp
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2011-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 "WebSocketV13.h"
+
+#include "WebSocket.h"
+#include "utils/HttpParser.h"
+#include "utils/HttpResponse.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <sstream>
+#include <string>
+
+#define WS_HTTP_METHOD "GET"
+#define WS_HTTP_TAG "HTTP/"
+
+#define WS_HEADER_UPGRADE "Upgrade"
+#define WS_HEADER_UPGRADE_LC "upgrade"
+#define WS_HEADER_CONNECTION "Connection"
+#define WS_HEADER_CONNECTION_LC "connection"
+
+#define WS_HEADER_KEY_LC "sec-websocket-key" // "Sec-WebSocket-Key"
+#define WS_HEADER_ACCEPT "Sec-WebSocket-Accept"
+#define WS_HEADER_PROTOCOL "Sec-WebSocket-Protocol"
+#define WS_HEADER_PROTOCOL_LC "sec-websocket-protocol" // "Sec-WebSocket-Protocol"
+
+#define WS_PROTOCOL_JSONRPC "jsonrpc.xbmc.org"
+#define WS_HEADER_UPGRADE_VALUE "websocket"
+
+bool CWebSocketV13::Handshake(const char* data, size_t length, std::string &response)
+{
+ std::string strHeader(data, length);
+ const char *value;
+ HttpParser header;
+ if (header.addBytes(data, length) != HttpParser::Done)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: incomplete handshake received");
+ return false;
+ }
+
+ // The request must be GET
+ value = header.getMethod();
+ if (value == NULL ||
+ StringUtils::CompareNoCase(value, WS_HTTP_METHOD, strlen(WS_HTTP_METHOD)) != 0)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid HTTP method received (GET expected)");
+ return false;
+ }
+
+ // The request must be HTTP/1.1 or higher
+ size_t pos;
+ if ((pos = strHeader.find(WS_HTTP_TAG)) == std::string::npos)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid handshake received");
+ return false;
+ }
+
+ pos += strlen(WS_HTTP_TAG);
+ std::istringstream converter(strHeader.substr(pos, strHeader.find_first_of(" \r\n\t", pos) - pos));
+ float fVersion;
+ converter >> fVersion;
+
+ if (fVersion < 1.1f)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid HTTP version {:f} (1.1 or higher expected)",
+ fVersion);
+ return false;
+ }
+
+ std::string websocketKey, websocketProtocol;
+ // There must be a "Host" header
+ value = header.getValue("host");
+ if (value == NULL || strlen(value) == 0)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: \"Host\" header missing");
+ return true;
+ }
+
+ // There must be a "Upgrade" header with the value "websocket"
+ value = header.getValue(WS_HEADER_UPGRADE_LC);
+ if (value == NULL || StringUtils::CompareNoCase(value, WS_HEADER_UPGRADE_VALUE,
+ strlen(WS_HEADER_UPGRADE_VALUE)) != 0)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid \"{}\" received", WS_HEADER_UPGRADE);
+ return true;
+ }
+
+ // There must be a "Connection" header with the value "Upgrade"
+ value = header.getValue(WS_HEADER_CONNECTION_LC);
+ std::vector<std::string> elements;
+ if (value != nullptr)
+ elements = StringUtils::Split(value, ",");
+ if (elements.empty() || !std::any_of(elements.begin(), elements.end(), [](std::string& elem) { return StringUtils::EqualsNoCase(StringUtils::Trim(elem), WS_HEADER_UPGRADE); }))
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid \"{}\" received", WS_HEADER_CONNECTION_LC);
+ return true;
+ }
+
+ // There must be a base64 encoded 16 byte (=> 24 byte as base62) "Sec-WebSocket-Key" header
+ value = header.getValue(WS_HEADER_KEY_LC);
+ if (value == NULL || (websocketKey = value).size() != 24)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: invalid \"Sec-WebSocket-Key\" received");
+ return true;
+ }
+
+ // There might be a "Sec-WebSocket-Protocol" header
+ value = header.getValue(WS_HEADER_PROTOCOL_LC);
+ if (value && strlen(value) > 0)
+ {
+ std::vector<std::string> protocols = StringUtils::Split(value, ",");
+ for (auto& protocol : protocols)
+ {
+ StringUtils::Trim(protocol);
+ if (protocol == WS_PROTOCOL_JSONRPC)
+ {
+ websocketProtocol = WS_PROTOCOL_JSONRPC;
+ break;
+ }
+ }
+ }
+
+ CHttpResponse httpResponse(HTTP::Get, HTTP::SwitchingProtocols, HTTP::Version1_1);
+ httpResponse.AddHeader(WS_HEADER_UPGRADE, WS_HEADER_UPGRADE_VALUE);
+ httpResponse.AddHeader(WS_HEADER_CONNECTION, WS_HEADER_UPGRADE);
+ std::string responseKey = calculateKey(websocketKey);
+ httpResponse.AddHeader(WS_HEADER_ACCEPT, responseKey);
+ if (!websocketProtocol.empty())
+ httpResponse.AddHeader(WS_HEADER_PROTOCOL, websocketProtocol);
+
+ response = httpResponse.Create();
+
+ m_state = WebSocketStateConnected;
+
+ return true;
+}
+
+const CWebSocketFrame* CWebSocketV13::Close(WebSocketCloseReason reason /* = WebSocketCloseNormal */, const std::string &message /* = "" */)
+{
+ if (m_state == WebSocketStateNotConnected || m_state == WebSocketStateHandshaking || m_state == WebSocketStateClosed)
+ {
+ CLog::Log(LOGINFO, "WebSocket [RFC6455]: Cannot send a closing handshake if no connection has been established");
+ return NULL;
+ }
+
+ return close(reason, message);
+}
diff --git a/xbmc/network/websocket/WebSocketV13.h b/xbmc/network/websocket/WebSocketV13.h
new file mode 100644
index 0000000..80c5a73
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketV13.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2011-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 "WebSocketV8.h"
+
+#include <string>
+
+class CWebSocketV13 : public CWebSocketV8
+{
+public:
+ CWebSocketV13() { m_version = 13; }
+
+ bool Handshake(const char* data, size_t length, std::string &response) override;
+ const CWebSocketFrame* Close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = "") override;
+};
diff --git a/xbmc/network/websocket/WebSocketV8.cpp b/xbmc/network/websocket/WebSocketV8.cpp
new file mode 100644
index 0000000..e0a187b
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketV8.cpp
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2011-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 "WebSocketV8.h"
+
+#include "WebSocket.h"
+#include "utils/Base64.h"
+#include "utils/Digest.h"
+#include "utils/EndianSwap.h"
+#include "utils/HttpParser.h"
+#include "utils/HttpResponse.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <sstream>
+#include <string>
+
+using KODI::UTILITY::CDigest;
+
+#define WS_HTTP_METHOD "GET"
+#define WS_HTTP_TAG "HTTP/"
+
+#define WS_HEADER_UPGRADE "Upgrade"
+#define WS_HEADER_CONNECTION "Connection"
+
+#define WS_HEADER_KEY_LC "sec-websocket-key" // "Sec-WebSocket-Key"
+#define WS_HEADER_ACCEPT "Sec-WebSocket-Accept"
+#define WS_HEADER_PROTOCOL "Sec-WebSocket-Protocol"
+#define WS_HEADER_PROTOCOL_LC "sec-websocket-protocol" // "Sec-WebSocket-Protocol"
+
+#define WS_PROTOCOL_JSONRPC "jsonrpc.xbmc.org"
+#define WS_HEADER_UPGRADE_VALUE "websocket"
+#define WS_KEY_MAGICSTRING "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+
+bool CWebSocketV8::Handshake(const char* data, size_t length, std::string &response)
+{
+ std::string strHeader(data, length);
+ const char *value;
+ HttpParser header;
+ if (header.addBytes(data, length) != HttpParser::Done)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: incomplete handshake received");
+ return false;
+ }
+
+ // The request must be GET
+ value = header.getMethod();
+ if (value == NULL ||
+ StringUtils::CompareNoCase(value, WS_HTTP_METHOD, strlen(WS_HTTP_METHOD)) != 0)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid HTTP method received (GET expected)");
+ return false;
+ }
+
+ // The request must be HTTP/1.1 or higher
+ size_t pos;
+ if ((pos = strHeader.find(WS_HTTP_TAG)) == std::string::npos)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid handshake received");
+ return false;
+ }
+
+ pos += strlen(WS_HTTP_TAG);
+ std::istringstream converter(strHeader.substr(pos, strHeader.find_first_of(" \r\n\t", pos) - pos));
+ float fVersion;
+ converter >> fVersion;
+
+ if (fVersion < 1.1f)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid HTTP version {:f} (1.1 or higher expected)",
+ fVersion);
+ return false;
+ }
+
+ std::string websocketKey, websocketProtocol;
+ // There must be a "Host" header
+ value = header.getValue("host");
+ if (value == NULL || strlen(value) == 0)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: \"Host\" header missing");
+ return true;
+ }
+
+ // There must be a base64 encoded 16 byte (=> 24 byte as base64) "Sec-WebSocket-Key" header
+ value = header.getValue(WS_HEADER_KEY_LC);
+ if (value == NULL || (websocketKey = value).size() != 24)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: invalid \"Sec-WebSocket-Key\" received");
+ return true;
+ }
+
+ // There might be a "Sec-WebSocket-Protocol" header
+ value = header.getValue(WS_HEADER_PROTOCOL_LC);
+ if (value && strlen(value) > 0)
+ {
+ std::vector<std::string> protocols = StringUtils::Split(value, ",");
+ for (auto& protocol : protocols)
+ {
+ StringUtils::Trim(protocol);
+ if (protocol == WS_PROTOCOL_JSONRPC)
+ {
+ websocketProtocol = WS_PROTOCOL_JSONRPC;
+ break;
+ }
+ }
+ }
+
+ CHttpResponse httpResponse(HTTP::Get, HTTP::SwitchingProtocols, HTTP::Version1_1);
+ httpResponse.AddHeader(WS_HEADER_UPGRADE, WS_HEADER_UPGRADE_VALUE);
+ httpResponse.AddHeader(WS_HEADER_CONNECTION, WS_HEADER_UPGRADE);
+ httpResponse.AddHeader(WS_HEADER_ACCEPT, calculateKey(websocketKey));
+ if (!websocketProtocol.empty())
+ httpResponse.AddHeader(WS_HEADER_PROTOCOL, websocketProtocol);
+
+ response = httpResponse.Create();
+
+ m_state = WebSocketStateConnected;
+
+ return true;
+}
+
+const CWebSocketFrame* CWebSocketV8::Close(WebSocketCloseReason reason /* = WebSocketCloseNormal */, const std::string &message /* = "" */)
+{
+ if (m_state == WebSocketStateNotConnected || m_state == WebSocketStateHandshaking || m_state == WebSocketStateClosed)
+ {
+ CLog::Log(LOGINFO, "WebSocket [hybi-10]: Cannot send a closing handshake if no connection has been established");
+ return NULL;
+ }
+
+ return close(reason, message);
+}
+
+void CWebSocketV8::Fail()
+{
+ m_state = WebSocketStateClosed;
+}
+
+CWebSocketFrame* CWebSocketV8::GetFrame(const char* data, uint64_t length)
+{
+ return new CWebSocketFrame(data, length);
+}
+
+CWebSocketFrame* CWebSocketV8::GetFrame(WebSocketFrameOpcode opcode, const char* data /* = NULL */, uint32_t length /* = 0 */,
+ bool final /* = true */, bool masked /* = false */, int32_t mask /* = 0 */, int8_t extension /* = 0 */)
+{
+ return new CWebSocketFrame(opcode, data, length, final, masked, mask, extension);
+}
+
+CWebSocketMessage* CWebSocketV8::GetMessage()
+{
+ return new CWebSocketMessage();
+}
+
+const CWebSocketFrame* CWebSocketV8::close(WebSocketCloseReason reason /* = WebSocketCloseNormal */, const std::string &message /* = "" */)
+{
+ size_t length = 2 + message.size();
+
+ char* data = new char[length + 1];
+ memset(data, 0, length + 1);
+ uint16_t iReason = Endian_SwapBE16((uint16_t)reason);
+ memcpy(data, &iReason, 2);
+ message.copy(data + 2, message.size());
+
+ if (m_state == WebSocketStateConnected)
+ m_state = WebSocketStateClosing;
+ else
+ m_state = WebSocketStateClosed;
+
+ CWebSocketFrame* frame = new CWebSocketFrame(WebSocketConnectionClose, data, length);
+ delete[] data;
+
+ return frame;
+}
+
+std::string CWebSocketV8::calculateKey(const std::string &key)
+{
+ std::string acceptKey = key;
+ acceptKey.append(WS_KEY_MAGICSTRING);
+
+ CDigest digest{CDigest::Type::SHA1};
+ digest.Update(acceptKey);
+
+ return Base64::Encode(digest.FinalizeRaw());
+}
diff --git a/xbmc/network/websocket/WebSocketV8.h b/xbmc/network/websocket/WebSocketV8.h
new file mode 100644
index 0000000..d86688c
--- /dev/null
+++ b/xbmc/network/websocket/WebSocketV8.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011-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 "WebSocket.h"
+
+#include <string>
+
+class CWebSocketV8 : public CWebSocket
+{
+public:
+ CWebSocketV8() { m_version = 8; }
+
+ bool Handshake(const char* data, size_t length, std::string &response) override;
+ const CWebSocketFrame* Ping(const char* data = NULL) const override { return new CWebSocketFrame(WebSocketPing, data); }
+ const CWebSocketFrame* Pong(const char* data = NULL) const override { return new CWebSocketFrame(WebSocketPong, data); }
+ const CWebSocketFrame* Close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = "") override;
+ void Fail() override;
+
+protected:
+ CWebSocketFrame* GetFrame(const char* data, uint64_t length) override;
+ CWebSocketFrame* GetFrame(WebSocketFrameOpcode opcode, const char* data = NULL, uint32_t length = 0, bool final = true, bool masked = false, int32_t mask = 0, int8_t extension = 0) override;
+ CWebSocketMessage* GetMessage() override;
+ virtual const CWebSocketFrame* close(WebSocketCloseReason reason = WebSocketCloseNormal, const std::string &message = "");
+
+ std::string calculateKey(const std::string &key);
+};
diff --git a/xbmc/peripherals/CMakeLists.txt b/xbmc/peripherals/CMakeLists.txt
new file mode 100644
index 0000000..9de62b9
--- /dev/null
+++ b/xbmc/peripherals/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES EventLockHandle.cpp
+ EventPollHandle.cpp
+ EventScanner.cpp
+ Peripherals.cpp)
+
+set(HEADERS EventLockHandle.h
+ EventPollHandle.h
+ EventScanner.h
+ IEventScannerCallback.h
+ Peripherals.h
+ PeripheralTypes.h)
+
+core_add_library(peripherals)
diff --git a/xbmc/peripherals/EventLockHandle.cpp b/xbmc/peripherals/EventLockHandle.cpp
new file mode 100644
index 0000000..b6d2a6d
--- /dev/null
+++ b/xbmc/peripherals/EventLockHandle.cpp
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 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 "EventLockHandle.h"
+
+using namespace PERIPHERALS;
+
+CEventLockHandle::CEventLockHandle(IEventLockCallback& callback) : m_callback(callback)
+{
+}
+
+CEventLockHandle::~CEventLockHandle(void)
+{
+ m_callback.ReleaseLock(*this);
+}
diff --git a/xbmc/peripherals/EventLockHandle.h b/xbmc/peripherals/EventLockHandle.h
new file mode 100644
index 0000000..7012ea8
--- /dev/null
+++ b/xbmc/peripherals/EventLockHandle.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 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
+
+namespace PERIPHERALS
+{
+class CEventLockHandle;
+
+/*!
+ * \brief Callback implemented by event scanner
+ */
+class IEventLockCallback
+{
+public:
+ virtual ~IEventLockCallback(void) = default;
+
+ virtual void ReleaseLock(CEventLockHandle& handle) = 0;
+};
+
+/*!
+ * \brief Handle returned by the event scanner to disable event processing
+ *
+ * When held, this disables event processing.
+ */
+class CEventLockHandle
+{
+public:
+ /*!
+ * \brief Create an event lock handle
+ */
+ CEventLockHandle(IEventLockCallback& callback);
+
+ /*!
+ * \brief Handle is automatically released when this class is destructed
+ */
+ ~CEventLockHandle(void);
+
+private:
+ // Construction parameters
+ IEventLockCallback& m_callback;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/EventPollHandle.cpp b/xbmc/peripherals/EventPollHandle.cpp
new file mode 100644
index 0000000..39a2e1a
--- /dev/null
+++ b/xbmc/peripherals/EventPollHandle.cpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016-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 "EventPollHandle.h"
+
+using namespace PERIPHERALS;
+
+CEventPollHandle::CEventPollHandle(IEventPollCallback& callback) : m_callback(callback)
+{
+}
+
+CEventPollHandle::~CEventPollHandle(void)
+{
+ m_callback.Release(*this);
+}
+
+void CEventPollHandle::Activate()
+{
+ m_callback.Activate(*this);
+}
+
+void CEventPollHandle::Deactivate()
+{
+ m_callback.Deactivate(*this);
+}
+
+void CEventPollHandle::HandleEvents(bool bWait)
+{
+ m_callback.HandleEvents(bWait);
+}
diff --git a/xbmc/peripherals/EventPollHandle.h b/xbmc/peripherals/EventPollHandle.h
new file mode 100644
index 0000000..5d99e1e
--- /dev/null
+++ b/xbmc/peripherals/EventPollHandle.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016-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
+
+namespace PERIPHERALS
+{
+class CEventPollHandle;
+
+/*!
+ * \brief Callback implemented by event scanner
+ */
+class IEventPollCallback
+{
+public:
+ virtual ~IEventPollCallback(void) = default;
+
+ virtual void Activate(CEventPollHandle& handle) = 0;
+ virtual void Deactivate(CEventPollHandle& handle) = 0;
+ virtual void HandleEvents(bool bWait) = 0;
+ virtual void Release(CEventPollHandle& handle) = 0;
+};
+
+/*!
+ * \brief Handle returned by the event scanner to control scan timing
+ *
+ * When held, this allows one to control the timing of when events are
+ * handled.
+ */
+class CEventPollHandle
+{
+public:
+ /*!
+ * \brief Create an active polling handle
+ */
+ CEventPollHandle(IEventPollCallback& callback);
+
+ /*!
+ * \brief Handle is automatically released when this class is destructed
+ */
+ ~CEventPollHandle();
+
+ /*!
+ * \brief Activate handle
+ */
+ void Activate();
+
+ /*!
+ * \brief Deactivate handle
+ */
+ void Deactivate();
+
+ /*!
+ * \brief Trigger a scan for events
+ *
+ * \param bWait If true, this blocks until all events are handled
+ */
+ void HandleEvents(bool bWait);
+
+private:
+ IEventPollCallback& m_callback;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/EventScanner.cpp b/xbmc/peripherals/EventScanner.cpp
new file mode 100644
index 0000000..0462a17
--- /dev/null
+++ b/xbmc/peripherals/EventScanner.cpp
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2016-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 "EventScanner.h"
+
+#include "IEventScannerCallback.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+
+using namespace PERIPHERALS;
+
+// Default event scan rate when no polling handles are held
+#define DEFAULT_SCAN_RATE_HZ 60
+
+// Timeout when a polling handle is held but doesn't trigger scan. This reduces
+// input latency when the game is running at < 1/4 speed.
+#define WATCHDOG_TIMEOUT_MS 80
+
+CEventScanner::CEventScanner(IEventScannerCallback& callback)
+ : CThread("PeripEventScan"), m_callback(callback)
+{
+}
+
+void CEventScanner::Start()
+{
+ Create();
+}
+
+void CEventScanner::Stop()
+{
+ StopThread(false);
+ m_scanEvent.Set();
+ StopThread(true);
+}
+
+EventPollHandlePtr CEventScanner::RegisterPollHandle()
+{
+ EventPollHandlePtr handle(new CEventPollHandle(*this));
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_handleMutex);
+ m_activeHandles.insert(handle.get());
+ }
+
+ CLog::Log(LOGDEBUG, "PERIPHERALS: Event poll handle registered");
+
+ return handle;
+}
+
+void CEventScanner::Activate(CEventPollHandle& handle)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_handleMutex);
+ m_activeHandles.insert(&handle);
+ }
+
+ CLog::Log(LOGDEBUG, "PERIPHERALS: Event poll handle activated");
+}
+
+void CEventScanner::Deactivate(CEventPollHandle& handle)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_handleMutex);
+ m_activeHandles.erase(&handle);
+ }
+
+ CLog::Log(LOGDEBUG, "PERIPHERALS: Event poll handle deactivated");
+}
+
+void CEventScanner::HandleEvents(bool bWait)
+{
+ if (bWait)
+ {
+ std::unique_lock<CCriticalSection> lock(m_pollMutex);
+
+ m_scanFinishedEvent.Reset();
+ m_scanEvent.Set();
+ m_scanFinishedEvent.Wait();
+ }
+ else
+ {
+ m_scanEvent.Set();
+ }
+}
+
+void CEventScanner::Release(CEventPollHandle& handle)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_handleMutex);
+ m_activeHandles.erase(&handle);
+ }
+
+ CLog::Log(LOGDEBUG, "PERIPHERALS: Event poll handle released");
+}
+
+EventLockHandlePtr CEventScanner::RegisterLock()
+{
+ EventLockHandlePtr handle(new CEventLockHandle(*this));
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_lockMutex);
+ m_activeLocks.insert(handle.get());
+ }
+
+ CLog::Log(LOGDEBUG, "PERIPHERALS: Event lock handle registered");
+
+ return handle;
+}
+
+void CEventScanner::ReleaseLock(CEventLockHandle& handle)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_lockMutex);
+ m_activeLocks.erase(&handle);
+ }
+
+ CLog::Log(LOGDEBUG, "PERIPHERALS: Event lock handle released");
+}
+
+void CEventScanner::Process()
+{
+ auto nextScan = std::chrono::steady_clock::now();
+
+ while (!m_bStop)
+ {
+ {
+ std::unique_lock<CCriticalSection> lock(m_lockMutex);
+ if (m_activeLocks.empty())
+ m_callback.ProcessEvents();
+ }
+
+ m_scanFinishedEvent.Set();
+
+ auto now = std::chrono::steady_clock::now();
+ auto scanIntervalMs = GetScanIntervalMs();
+
+ // Handle wrap-around
+ if (now < nextScan)
+ nextScan = now;
+
+ while (nextScan <= now)
+ nextScan += scanIntervalMs;
+
+ auto waitTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(nextScan - now);
+
+ if (!m_bStop && waitTimeMs.count() > 0)
+ m_scanEvent.Wait(waitTimeMs);
+ }
+}
+
+std::chrono::milliseconds CEventScanner::GetScanIntervalMs() const
+{
+ bool bHasActiveHandle;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_handleMutex);
+ bHasActiveHandle = !m_activeHandles.empty();
+ }
+
+ if (!bHasActiveHandle)
+ {
+ // this truncates to 16 (from 16.666) should it round up to 17 using std::nearbyint or should we use nanoseconds?
+ return std::chrono::milliseconds(static_cast<uint32_t>(1000.0 / DEFAULT_SCAN_RATE_HZ));
+ }
+ else
+ return std::chrono::milliseconds(WATCHDOG_TIMEOUT_MS);
+}
diff --git a/xbmc/peripherals/EventScanner.h b/xbmc/peripherals/EventScanner.h
new file mode 100644
index 0000000..0092278
--- /dev/null
+++ b/xbmc/peripherals/EventScanner.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016-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 "EventLockHandle.h"
+#include "EventPollHandle.h"
+#include "PeripheralTypes.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+
+#include <chrono>
+#include <set>
+
+namespace PERIPHERALS
+{
+class IEventScannerCallback;
+
+/*!
+ * \brief Class to scan for peripheral events
+ *
+ * By default, a rate of 60 Hz is used. A client can obtain control over when
+ * input is handled by registering for a polling handle.
+ */
+class CEventScanner : public IEventPollCallback, public IEventLockCallback, protected CThread
+{
+public:
+ explicit CEventScanner(IEventScannerCallback& callback);
+
+ ~CEventScanner() override = default;
+
+ void Start();
+ void Stop();
+
+ EventPollHandlePtr RegisterPollHandle();
+
+ /*!
+ * \brief Acquire a lock that prevents event processing while held
+ */
+ EventLockHandlePtr RegisterLock();
+
+ // implementation of IEventPollCallback
+ void Activate(CEventPollHandle& handle) override;
+ void Deactivate(CEventPollHandle& handle) override;
+ void HandleEvents(bool bWait) override;
+ void Release(CEventPollHandle& handle) override;
+
+ // implementation of IEventLockCallback
+ void ReleaseLock(CEventLockHandle& handle) override;
+
+protected:
+ // implementation of CThread
+ void Process() override;
+
+private:
+ std::chrono::milliseconds GetScanIntervalMs() const;
+
+ // Construction parameters
+ IEventScannerCallback& m_callback;
+
+ // Event parameters
+ std::set<void*> m_activeHandles;
+ std::set<void*> m_activeLocks;
+ CEvent m_scanEvent;
+ CEvent m_scanFinishedEvent;
+ mutable CCriticalSection m_handleMutex;
+ CCriticalSection m_lockMutex;
+ CCriticalSection m_pollMutex; // Prevent two poll handles from polling at once
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/IEventScannerCallback.h b/xbmc/peripherals/IEventScannerCallback.h
new file mode 100644
index 0000000..d017273
--- /dev/null
+++ b/xbmc/peripherals/IEventScannerCallback.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 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
+
+namespace PERIPHERALS
+{
+class IEventScannerCallback
+{
+public:
+ virtual ~IEventScannerCallback(void) = default;
+
+ virtual void ProcessEvents(void) = 0;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/PeripheralTypes.h b/xbmc/peripherals/PeripheralTypes.h
new file mode 100644
index 0000000..425ac0f
--- /dev/null
+++ b/xbmc/peripherals/PeripheralTypes.h
@@ -0,0 +1,370 @@
+/*
+ * 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 <algorithm>
+#include <map>
+#include <memory>
+#include <stdio.h>
+#include <string>
+#include <vector>
+#ifdef TARGET_WINDOWS
+#include "PlatformDefs.h"
+#endif
+#include "utils/StringUtils.h"
+
+class CSetting;
+
+namespace PERIPHERALS
+{
+enum PeripheralBusType
+{
+ PERIPHERAL_BUS_UNKNOWN = 0,
+ PERIPHERAL_BUS_USB,
+ PERIPHERAL_BUS_PCI,
+ PERIPHERAL_BUS_CEC,
+ PERIPHERAL_BUS_ADDON,
+#ifdef TARGET_ANDROID
+ PERIPHERAL_BUS_ANDROID,
+#endif
+#if defined(TARGET_DARWIN)
+ PERIPHERAL_BUS_GCCONTROLLER,
+#endif
+ PERIPHERAL_BUS_APPLICATION,
+};
+
+enum PeripheralFeature
+{
+ FEATURE_UNKNOWN = 0,
+ FEATURE_HID,
+ FEATURE_NIC,
+ FEATURE_DISK,
+ FEATURE_NYXBOARD,
+ FEATURE_CEC,
+ FEATURE_BLUETOOTH,
+ FEATURE_TUNER,
+ FEATURE_IMON,
+ FEATURE_JOYSTICK,
+ FEATURE_RUMBLE,
+ FEATURE_POWER_OFF,
+ FEATURE_KEYBOARD,
+ FEATURE_MOUSE,
+};
+
+enum PeripheralType
+{
+ PERIPHERAL_UNKNOWN = 0,
+ PERIPHERAL_HID,
+ PERIPHERAL_NIC,
+ PERIPHERAL_DISK,
+ PERIPHERAL_NYXBOARD,
+ PERIPHERAL_CEC,
+ PERIPHERAL_BLUETOOTH,
+ PERIPHERAL_TUNER,
+ PERIPHERAL_IMON,
+ PERIPHERAL_JOYSTICK,
+ PERIPHERAL_KEYBOARD,
+ PERIPHERAL_MOUSE,
+};
+
+class CPeripheral;
+using PeripheralPtr = std::shared_ptr<CPeripheral>;
+using PeripheralVector = std::vector<PeripheralPtr>;
+
+class CPeripheralAddon;
+using PeripheralAddonPtr = std::shared_ptr<CPeripheralAddon>;
+using PeripheralAddonVector = std::vector<PeripheralAddonPtr>;
+
+class CEventPollHandle;
+using EventPollHandlePtr = std::unique_ptr<CEventPollHandle>;
+
+class CEventLockHandle;
+using EventLockHandlePtr = std::unique_ptr<CEventLockHandle>;
+
+struct PeripheralID
+{
+ int m_iVendorId;
+ int m_iProductId;
+};
+
+struct PeripheralDeviceSetting
+{
+ std::shared_ptr<CSetting> m_setting;
+ int m_order;
+};
+
+struct PeripheralDeviceMapping
+{
+ std::vector<PeripheralID> m_PeripheralID;
+ PeripheralBusType m_busType;
+ PeripheralType m_class;
+ std::string m_strDeviceName;
+ PeripheralType m_mappedTo;
+ std::map<std::string, PeripheralDeviceSetting> m_settings;
+};
+
+class PeripheralTypeTranslator
+{
+public:
+ static const char* TypeToString(const PeripheralType type)
+ {
+ switch (type)
+ {
+ case PERIPHERAL_BLUETOOTH:
+ return "bluetooth";
+ case PERIPHERAL_CEC:
+ return "cec";
+ case PERIPHERAL_DISK:
+ return "disk";
+ case PERIPHERAL_HID:
+ return "hid";
+ case PERIPHERAL_NIC:
+ return "nic";
+ case PERIPHERAL_NYXBOARD:
+ return "nyxboard";
+ case PERIPHERAL_TUNER:
+ return "tuner";
+ case PERIPHERAL_IMON:
+ return "imon";
+ case PERIPHERAL_JOYSTICK:
+ return "joystick";
+ case PERIPHERAL_KEYBOARD:
+ return "keyboard";
+ case PERIPHERAL_MOUSE:
+ return "mouse";
+ default:
+ return "unknown";
+ }
+ };
+
+ static PeripheralType GetTypeFromString(const std::string& strType)
+ {
+ std::string strTypeLowerCase(strType);
+ StringUtils::ToLower(strTypeLowerCase);
+
+ if (strTypeLowerCase == "bluetooth")
+ return PERIPHERAL_BLUETOOTH;
+ else if (strTypeLowerCase == "cec")
+ return PERIPHERAL_CEC;
+ else if (strTypeLowerCase == "disk")
+ return PERIPHERAL_DISK;
+ else if (strTypeLowerCase == "hid")
+ return PERIPHERAL_HID;
+ else if (strTypeLowerCase == "nic")
+ return PERIPHERAL_NIC;
+ else if (strTypeLowerCase == "nyxboard")
+ return PERIPHERAL_NYXBOARD;
+ else if (strTypeLowerCase == "tuner")
+ return PERIPHERAL_TUNER;
+ else if (strTypeLowerCase == "imon")
+ return PERIPHERAL_IMON;
+ else if (strTypeLowerCase == "joystick")
+ return PERIPHERAL_JOYSTICK;
+ else if (strTypeLowerCase == "keyboard")
+ return PERIPHERAL_KEYBOARD;
+ else if (strTypeLowerCase == "mouse")
+ return PERIPHERAL_MOUSE;
+
+ return PERIPHERAL_UNKNOWN;
+ };
+
+ static const char* BusTypeToString(const PeripheralBusType type)
+ {
+ switch (type)
+ {
+ case PERIPHERAL_BUS_USB:
+ return "usb";
+ case PERIPHERAL_BUS_PCI:
+ return "pci";
+ case PERIPHERAL_BUS_CEC:
+ return "cec";
+ case PERIPHERAL_BUS_ADDON:
+ return "addon";
+#ifdef TARGET_ANDROID
+ case PERIPHERAL_BUS_ANDROID:
+ return "android";
+#endif
+#if defined(TARGET_DARWIN)
+ case PERIPHERAL_BUS_GCCONTROLLER:
+ return "darwin_gccontroller";
+#endif
+ case PERIPHERAL_BUS_APPLICATION:
+ return "application";
+ default:
+ return "unknown";
+ }
+ };
+
+ static PeripheralBusType GetBusTypeFromString(const std::string& strType)
+ {
+ std::string strTypeLowerCase(strType);
+ StringUtils::ToLower(strTypeLowerCase);
+
+ if (strTypeLowerCase == "usb")
+ return PERIPHERAL_BUS_USB;
+ else if (strTypeLowerCase == "pci")
+ return PERIPHERAL_BUS_PCI;
+ else if (strTypeLowerCase == "cec")
+ return PERIPHERAL_BUS_CEC;
+ else if (strTypeLowerCase == "addon")
+ return PERIPHERAL_BUS_ADDON;
+#ifdef TARGET_ANDROID
+ else if (strTypeLowerCase == "android")
+ return PERIPHERAL_BUS_ANDROID;
+#endif
+#if defined(TARGET_DARWIN)
+ else if (strTypeLowerCase == "darwin_gccontroller")
+ return PERIPHERAL_BUS_GCCONTROLLER;
+#endif
+ else if (strTypeLowerCase == "application")
+ return PERIPHERAL_BUS_APPLICATION;
+
+ return PERIPHERAL_BUS_UNKNOWN;
+ };
+
+ static const char* FeatureToString(const PeripheralFeature type)
+ {
+ switch (type)
+ {
+ case FEATURE_HID:
+ return "HID";
+ case FEATURE_NIC:
+ return "NIC";
+ case FEATURE_DISK:
+ return "disk";
+ case FEATURE_NYXBOARD:
+ return "nyxboard";
+ case FEATURE_CEC:
+ return "CEC";
+ case FEATURE_BLUETOOTH:
+ return "bluetooth";
+ case FEATURE_TUNER:
+ return "tuner";
+ case FEATURE_IMON:
+ return "imon";
+ case FEATURE_JOYSTICK:
+ return "joystick";
+ case FEATURE_RUMBLE:
+ return "rumble";
+ case FEATURE_POWER_OFF:
+ return "poweroff";
+ case FEATURE_KEYBOARD:
+ return "keyboard";
+ case FEATURE_MOUSE:
+ return "mouse";
+ case FEATURE_UNKNOWN:
+ default:
+ return "unknown";
+ }
+ };
+
+ static PeripheralFeature GetFeatureTypeFromString(const std::string& strType)
+ {
+ std::string strTypeLowerCase(strType);
+ StringUtils::ToLower(strTypeLowerCase);
+
+ if (strTypeLowerCase == "hid")
+ return FEATURE_HID;
+ else if (strTypeLowerCase == "cec")
+ return FEATURE_CEC;
+ else if (strTypeLowerCase == "disk")
+ return FEATURE_DISK;
+ else if (strTypeLowerCase == "nyxboard")
+ return FEATURE_NYXBOARD;
+ else if (strTypeLowerCase == "bluetooth")
+ return FEATURE_BLUETOOTH;
+ else if (strTypeLowerCase == "tuner")
+ return FEATURE_TUNER;
+ else if (strTypeLowerCase == "imon")
+ return FEATURE_IMON;
+ else if (strTypeLowerCase == "joystick")
+ return FEATURE_JOYSTICK;
+ else if (strTypeLowerCase == "rumble")
+ return FEATURE_RUMBLE;
+ else if (strTypeLowerCase == "poweroff")
+ return FEATURE_POWER_OFF;
+ else if (strTypeLowerCase == "keyboard")
+ return FEATURE_KEYBOARD;
+ else if (strTypeLowerCase == "mouse")
+ return FEATURE_MOUSE;
+
+ return FEATURE_UNKNOWN;
+ };
+
+ static int HexStringToInt(const char* strHex)
+ {
+ int iVal;
+ sscanf(strHex, "%x", &iVal);
+ return iVal;
+ };
+
+ static void FormatHexString(int iVal, std::string& strHexString)
+ {
+ if (iVal < 0)
+ iVal = 0;
+ if (iVal > 65536)
+ iVal = 65536;
+
+ strHexString = StringUtils::Format("{:04X}", iVal);
+ };
+};
+
+class PeripheralScanResult
+{
+public:
+ explicit PeripheralScanResult(const PeripheralBusType busType)
+ : m_busType(busType), m_mappedBusType(busType)
+ {
+ }
+
+ PeripheralScanResult(void) = default;
+
+ bool operator==(const PeripheralScanResult& right) const
+ {
+ return m_iVendorId == right.m_iVendorId && m_iProductId == right.m_iProductId &&
+ m_type == right.m_type && m_busType == right.m_busType &&
+ StringUtils::EqualsNoCase(m_strLocation, right.m_strLocation);
+ }
+
+ bool operator!=(const PeripheralScanResult& right) const { return !(*this == right); }
+
+ PeripheralType m_type = PERIPHERAL_UNKNOWN;
+ std::string m_strLocation;
+ int m_iVendorId = 0;
+ int m_iProductId = 0;
+ PeripheralType m_mappedType = PERIPHERAL_UNKNOWN;
+ std::string m_strDeviceName;
+ PeripheralBusType m_busType = PERIPHERAL_BUS_UNKNOWN;
+ PeripheralBusType m_mappedBusType = PERIPHERAL_BUS_UNKNOWN;
+ unsigned int m_iSequence = 0; // when more than one adapter of the same type is found
+};
+
+struct PeripheralScanResults
+{
+ bool GetDeviceOnLocation(const std::string& strLocation, PeripheralScanResult* result) const
+ {
+ for (const auto& it : m_results)
+ {
+ if (it.m_strLocation == strLocation)
+ {
+ *result = it;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool ContainsResult(const PeripheralScanResult& result) const
+ {
+ return std::find(m_results.begin(), m_results.end(), result) != m_results.end();
+ }
+
+ std::vector<PeripheralScanResult> m_results;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/Peripherals.cpp b/xbmc/peripherals/Peripherals.cpp
new file mode 100644
index 0000000..c178158
--- /dev/null
+++ b/xbmc/peripherals/Peripherals.cpp
@@ -0,0 +1,1030 @@
+/*
+ * 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 "Peripherals.h"
+
+#include "CompileInfo.h"
+#include "EventScanner.h"
+#include "addons/AddonButtonMap.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "bus/PeripheralBus.h"
+#include "bus/PeripheralBusUSB.h"
+
+#include <mutex>
+#include <utility>
+#if defined(TARGET_ANDROID)
+#include "platform/android/peripherals/PeripheralBusAndroid.h"
+#elif defined(TARGET_DARWIN)
+#include "platform/darwin/peripherals/PeripheralBusGCController.h"
+#endif
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "bus/virtual/PeripheralBusAddon.h"
+#include "bus/virtual/PeripheralBusApplication.h"
+#include "devices/PeripheralBluetooth.h"
+#include "devices/PeripheralCecAdapter.h"
+#include "devices/PeripheralDisk.h"
+#include "devices/PeripheralHID.h"
+#include "devices/PeripheralImon.h"
+#include "devices/PeripheralJoystick.h"
+#include "devices/PeripheralKeyboard.h"
+#include "devices/PeripheralMouse.h"
+#include "devices/PeripheralNIC.h"
+#include "devices/PeripheralNyxboard.h"
+#include "devices/PeripheralTuner.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "input/Key.h"
+#include "input/joysticks/interfaces/IButtonMapper.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/ThreadMessage.h"
+#include "peripherals/dialogs/GUIDialogPeripherals.h"
+#include "settings/SettingAddon.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#if defined(HAVE_LIBCEC)
+#include "bus/virtual/PeripheralBusCEC.h"
+#else
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/LocalizeStrings.h"
+#endif
+
+using namespace KODI;
+using namespace JOYSTICK;
+using namespace PERIPHERALS;
+using namespace XFILE;
+
+CPeripherals::CPeripherals(CInputManager& inputManager,
+ GAME::CControllerManager& controllerProfiles)
+ : m_inputManager(inputManager),
+ m_controllerProfiles(controllerProfiles),
+ m_eventScanner(new CEventScanner(*this))
+{
+ // Register settings
+ std::set<std::string> settingSet;
+ settingSet.insert(CSettings::SETTING_INPUT_PERIPHERALS);
+ settingSet.insert(CSettings::SETTING_INPUT_PERIPHERALLIBRARIES);
+ settingSet.insert(CSettings::SETTING_INPUT_CONTROLLERCONFIG);
+ settingSet.insert(CSettings::SETTING_INPUT_TESTRUMBLE);
+ settingSet.insert(CSettings::SETTING_LOCALE_LANGUAGE);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->RegisterCallback(this, settingSet);
+}
+
+CPeripherals::~CPeripherals()
+{
+ // Unregister settings
+ CServiceBroker::GetSettingsComponent()->GetSettings()->UnregisterCallback(this);
+
+ Clear();
+}
+
+void CPeripherals::Initialise()
+{
+ Clear();
+
+ CDirectory::Create("special://profile/peripheral_data");
+
+ /* load mappings from peripherals.xml */
+ LoadMappings();
+
+ std::vector<PeripheralBusPtr> busses;
+
+#if defined(HAVE_PERIPHERAL_BUS_USB)
+ busses.push_back(std::make_shared<CPeripheralBusUSB>(*this));
+#endif
+#if defined(HAVE_LIBCEC)
+ busses.push_back(std::make_shared<CPeripheralBusCEC>(*this));
+#endif
+ busses.push_back(std::make_shared<CPeripheralBusAddon>(*this));
+#if defined(TARGET_ANDROID)
+ busses.push_back(std::make_shared<CPeripheralBusAndroid>(*this));
+#elif defined(TARGET_DARWIN)
+ busses.push_back(std::make_shared<CPeripheralBusGCController>(*this));
+#endif
+ busses.push_back(std::make_shared<CPeripheralBusApplication>(*this));
+
+ {
+ std::unique_lock<CCriticalSection> bussesLock(m_critSectionBusses);
+ m_busses = busses;
+ }
+
+ /* initialise all known busses and run an initial scan for devices */
+ for (auto& bus : busses)
+ bus->Initialise();
+
+ m_eventScanner->Start();
+
+ CServiceBroker::GetAppMessenger()->RegisterReceiver(this);
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+void CPeripherals::Clear()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+
+ m_eventScanner->Stop();
+
+ // avoid deadlocks by copying all busses into a temporary variable and destroying them from there
+ std::vector<PeripheralBusPtr> busses;
+ {
+ std::unique_lock<CCriticalSection> bussesLock(m_critSectionBusses);
+ /* delete busses and devices */
+ busses = m_busses;
+ m_busses.clear();
+ }
+
+ for (const auto& bus : busses)
+ bus->Clear();
+ busses.clear();
+
+ {
+ std::unique_lock<CCriticalSection> mappingsLock(m_critSectionMappings);
+ /* delete mappings */
+ for (auto& mapping : m_mappings)
+ mapping.m_settings.clear();
+ m_mappings.clear();
+ }
+
+#if !defined(HAVE_LIBCEC)
+ m_bMissingLibCecWarningDisplayed = false;
+#endif
+}
+
+void CPeripherals::TriggerDeviceScan(const PeripheralBusType type /* = PERIPHERAL_BUS_UNKNOWN */)
+{
+ std::vector<PeripheralBusPtr> busses;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSectionBusses);
+ busses = m_busses;
+ }
+
+ for (auto& bus : busses)
+ {
+ bool bScan = false;
+
+ if (type == PERIPHERAL_BUS_UNKNOWN)
+ bScan = true;
+ else if (bus->Type() == PERIPHERAL_BUS_ADDON)
+ bScan = true;
+ else if (bus->Type() == type)
+ bScan = true;
+
+ if (bScan)
+ bus->TriggerDeviceScan();
+ }
+}
+
+PeripheralBusPtr CPeripherals::GetBusByType(const PeripheralBusType type) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSectionBusses);
+
+ const auto& bus =
+ std::find_if(m_busses.cbegin(), m_busses.cend(),
+ [type](const PeripheralBusPtr& bus) { return bus->Type() == type; });
+ if (bus != m_busses.cend())
+ return *bus;
+
+ return nullptr;
+}
+
+PeripheralPtr CPeripherals::GetPeripheralAtLocation(
+ const std::string& strLocation, PeripheralBusType busType /* = PERIPHERAL_BUS_UNKNOWN */) const
+{
+ PeripheralPtr result;
+
+ std::unique_lock<CCriticalSection> lock(m_critSectionBusses);
+ for (const auto& bus : m_busses)
+ {
+ /* check whether the bus matches if a bus type other than unknown was passed */
+ if (busType != PERIPHERAL_BUS_UNKNOWN && bus->Type() != busType)
+ continue;
+
+ /* return the first device that matches */
+ PeripheralPtr peripheral = bus->GetPeripheral(strLocation);
+ if (peripheral)
+ {
+ result = peripheral;
+ break;
+ }
+ }
+
+ return result;
+}
+
+bool CPeripherals::HasPeripheralAtLocation(
+ const std::string& strLocation, PeripheralBusType busType /* = PERIPHERAL_BUS_UNKNOWN */) const
+{
+ return (GetPeripheralAtLocation(strLocation, busType) != nullptr);
+}
+
+PeripheralBusPtr CPeripherals::GetBusWithDevice(const std::string& strLocation) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSectionBusses);
+
+ const auto& bus =
+ std::find_if(m_busses.cbegin(), m_busses.cend(), [&strLocation](const PeripheralBusPtr& bus) {
+ return bus->HasPeripheral(strLocation);
+ });
+ if (bus != m_busses.cend())
+ return *bus;
+
+ return nullptr;
+}
+
+bool CPeripherals::SupportsFeature(PeripheralFeature feature) const
+{
+ bool bSupportsFeature = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSectionBusses);
+ for (const auto& bus : m_busses)
+ bSupportsFeature |= bus->SupportsFeature(feature);
+
+ return bSupportsFeature;
+}
+
+int CPeripherals::GetPeripheralsWithFeature(
+ PeripheralVector& results,
+ const PeripheralFeature feature,
+ PeripheralBusType busType /* = PERIPHERAL_BUS_UNKNOWN */) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSectionBusses);
+ int iReturn(0);
+ for (const auto& bus : m_busses)
+ {
+ /* check whether the bus matches if a bus type other than unknown was passed */
+ if (busType != PERIPHERAL_BUS_UNKNOWN && bus->Type() != busType)
+ continue;
+
+ iReturn += bus->GetPeripheralsWithFeature(results, feature);
+ }
+
+ return iReturn;
+}
+
+size_t CPeripherals::GetNumberOfPeripherals() const
+{
+ size_t iReturn(0);
+ std::unique_lock<CCriticalSection> lock(m_critSectionBusses);
+ for (const auto& bus : m_busses)
+ iReturn += bus->GetNumberOfPeripherals();
+
+ return iReturn;
+}
+
+bool CPeripherals::HasPeripheralWithFeature(
+ const PeripheralFeature feature, PeripheralBusType busType /* = PERIPHERAL_BUS_UNKNOWN */) const
+{
+ PeripheralVector dummy;
+ return (GetPeripheralsWithFeature(dummy, feature, busType) > 0);
+}
+
+void CPeripherals::CreatePeripheral(CPeripheralBus& bus, const PeripheralScanResult& result)
+{
+ PeripheralPtr peripheral;
+ PeripheralScanResult mappedResult = result;
+ if (mappedResult.m_busType == PERIPHERAL_BUS_UNKNOWN)
+ mappedResult.m_busType = bus.Type();
+
+ /* check whether there's something mapped in peripherals.xml */
+ GetMappingForDevice(bus, mappedResult);
+
+ switch (mappedResult.m_mappedType)
+ {
+ case PERIPHERAL_HID:
+ peripheral = PeripheralPtr(new CPeripheralHID(*this, mappedResult, &bus));
+ break;
+
+ case PERIPHERAL_NIC:
+ peripheral = PeripheralPtr(new CPeripheralNIC(*this, mappedResult, &bus));
+ break;
+
+ case PERIPHERAL_DISK:
+ peripheral = PeripheralPtr(new CPeripheralDisk(*this, mappedResult, &bus));
+ break;
+
+ case PERIPHERAL_NYXBOARD:
+ peripheral = PeripheralPtr(new CPeripheralNyxboard(*this, mappedResult, &bus));
+ break;
+
+ case PERIPHERAL_TUNER:
+ peripheral = PeripheralPtr(new CPeripheralTuner(*this, mappedResult, &bus));
+ break;
+
+ case PERIPHERAL_BLUETOOTH:
+ peripheral = PeripheralPtr(new CPeripheralBluetooth(*this, mappedResult, &bus));
+ break;
+
+ case PERIPHERAL_CEC:
+#if defined(HAVE_LIBCEC)
+ if (bus.Type() == PERIPHERAL_BUS_CEC)
+ peripheral = PeripheralPtr(new CPeripheralCecAdapter(*this, mappedResult, &bus));
+#else
+ if (!m_bMissingLibCecWarningDisplayed)
+ {
+ m_bMissingLibCecWarningDisplayed = true;
+ CLog::Log(
+ LOGWARNING,
+ "{} - libCEC support has not been compiled in, so the CEC adapter cannot be used.",
+ __FUNCTION__);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning,
+ g_localizeStrings.Get(36000),
+ g_localizeStrings.Get(36017));
+ }
+#endif
+ break;
+
+ case PERIPHERAL_IMON:
+ peripheral = PeripheralPtr(new CPeripheralImon(*this, mappedResult, &bus));
+ break;
+
+ case PERIPHERAL_JOYSTICK:
+ peripheral = PeripheralPtr(new CPeripheralJoystick(*this, mappedResult, &bus));
+ break;
+
+ case PERIPHERAL_KEYBOARD:
+ peripheral = PeripheralPtr(new CPeripheralKeyboard(*this, mappedResult, &bus));
+ break;
+
+ case PERIPHERAL_MOUSE:
+ peripheral = PeripheralPtr(new CPeripheralMouse(*this, mappedResult, &bus));
+ break;
+
+ default:
+ break;
+ }
+
+ if (peripheral)
+ {
+ /* try to initialise the new peripheral
+ * Initialise() will make sure that each device is only initialised once */
+ if (peripheral->Initialise())
+ bus.Register(peripheral);
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - failed to initialise peripheral on '{}'", __FUNCTION__,
+ mappedResult.m_strLocation);
+ }
+ }
+}
+
+void CPeripherals::OnDeviceAdded(const CPeripheralBus& bus, const CPeripheral& peripheral)
+{
+ OnDeviceChanged();
+
+ //! @todo Improve device notifications in v18
+#if 0
+ bool bNotify = true;
+
+ // don't show a notification for devices detected during the initial scan
+ if (!bus.IsInitialised())
+ bNotify = false;
+
+ if (bNotify)
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(35005), peripheral.DeviceName());
+#endif
+}
+
+void CPeripherals::OnDeviceDeleted(const CPeripheralBus& bus, const CPeripheral& peripheral)
+{
+ OnDeviceChanged();
+
+ //! @todo Improve device notifications in v18
+#if 0
+ bool bNotify = true;
+
+ if (bNotify)
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(35006), peripheral.DeviceName());
+#endif
+}
+
+void CPeripherals::OnDeviceChanged()
+{
+ // refresh settings (peripherals manager could be enabled/disabled now)
+ CGUIMessage msgSettings(GUI_MSG_UPDATE, WINDOW_SETTINGS_SYSTEM, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msgSettings,
+ WINDOW_SETTINGS_SYSTEM);
+
+ SetChanged();
+}
+
+bool CPeripherals::GetMappingForDevice(const CPeripheralBus& bus,
+ PeripheralScanResult& result) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSectionMappings);
+
+ /* check all mappings in the order in which they are defined in peripherals.xml */
+ for (const auto& mapping : m_mappings)
+ {
+ bool bProductMatch = false;
+ if (mapping.m_PeripheralID.empty())
+ bProductMatch = true;
+ else
+ {
+ for (const auto& peripheralID : mapping.m_PeripheralID)
+ if (peripheralID.m_iVendorId == result.m_iVendorId &&
+ peripheralID.m_iProductId == result.m_iProductId)
+ bProductMatch = true;
+ }
+
+ bool bBusMatch =
+ (mapping.m_busType == PERIPHERAL_BUS_UNKNOWN || mapping.m_busType == bus.Type());
+ bool bClassMatch = (mapping.m_class == PERIPHERAL_UNKNOWN || mapping.m_class == result.m_type);
+
+ if (bProductMatch && bBusMatch && bClassMatch)
+ {
+ std::string strVendorId, strProductId;
+ PeripheralTypeTranslator::FormatHexString(result.m_iVendorId, strVendorId);
+ PeripheralTypeTranslator::FormatHexString(result.m_iProductId, strProductId);
+ CLog::Log(LOGDEBUG, "{} - device ({}:{}) mapped to {} (type = {})", __FUNCTION__, strVendorId,
+ strProductId, mapping.m_strDeviceName,
+ PeripheralTypeTranslator::TypeToString(mapping.m_mappedTo));
+ result.m_mappedType = mapping.m_mappedTo;
+ if (result.m_strDeviceName.empty() && !mapping.m_strDeviceName.empty())
+ result.m_strDeviceName = mapping.m_strDeviceName;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CPeripherals::GetSettingsFromMapping(CPeripheral& peripheral) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSectionMappings);
+
+ /* check all mappings in the order in which they are defined in peripherals.xml */
+ for (const auto& mapping : m_mappings)
+ {
+ bool bProductMatch = false;
+ if (mapping.m_PeripheralID.empty())
+ bProductMatch = true;
+ else
+ {
+ for (const auto& peripheralID : mapping.m_PeripheralID)
+ if (peripheralID.m_iVendorId == peripheral.VendorId() &&
+ peripheralID.m_iProductId == peripheral.ProductId())
+ bProductMatch = true;
+ }
+
+ bool bBusMatch = (mapping.m_busType == PERIPHERAL_BUS_UNKNOWN ||
+ mapping.m_busType == peripheral.GetBusType());
+ bool bClassMatch =
+ (mapping.m_class == PERIPHERAL_UNKNOWN || mapping.m_class == peripheral.Type());
+
+ if (bBusMatch && bProductMatch && bClassMatch)
+ {
+ for (auto itr = mapping.m_settings.begin(); itr != mapping.m_settings.end(); ++itr)
+ peripheral.AddSetting((*itr).first, (*itr).second.m_setting, (*itr).second.m_order);
+ }
+ }
+}
+
+#define SS(x) ((x) ? x : "")
+bool CPeripherals::LoadMappings()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSectionMappings);
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile("special://xbmc/system/peripherals.xml"))
+ {
+ CLog::Log(LOGWARNING, "{} - peripherals.xml does not exist", __FUNCTION__);
+ return true;
+ }
+
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (!pRootElement || StringUtils::CompareNoCase(pRootElement->Value(), "peripherals") != 0)
+ {
+ CLog::Log(LOGERROR, "{} - peripherals.xml does not contain <peripherals>", __FUNCTION__);
+ return false;
+ }
+
+ for (TiXmlElement* currentNode = pRootElement->FirstChildElement("peripheral"); currentNode;
+ currentNode = currentNode->NextSiblingElement("peripheral"))
+ {
+ PeripheralID id;
+ PeripheralDeviceMapping mapping;
+
+ mapping.m_strDeviceName = XMLUtils::GetAttribute(currentNode, "name");
+
+ // If there is no vendor_product attribute ignore this entry
+ if (currentNode->Attribute("vendor_product"))
+ {
+ // The vendor_product attribute is a list of comma separated vendor:product pairs
+ std::vector<std::string> vpArray =
+ StringUtils::Split(currentNode->Attribute("vendor_product"), ",");
+ for (const auto& i : vpArray)
+ {
+ std::vector<std::string> idArray = StringUtils::Split(i, ":");
+ if (idArray.size() != 2)
+ {
+ CLog::Log(LOGERROR, "{} - ignoring node \"{}\" with invalid vendor_product attribute",
+ __FUNCTION__, mapping.m_strDeviceName);
+ continue;
+ }
+
+ id.m_iVendorId = PeripheralTypeTranslator::HexStringToInt(idArray[0].c_str());
+ id.m_iProductId = PeripheralTypeTranslator::HexStringToInt(idArray[1].c_str());
+ mapping.m_PeripheralID.push_back(id);
+ }
+ }
+
+ mapping.m_busType =
+ PeripheralTypeTranslator::GetBusTypeFromString(XMLUtils::GetAttribute(currentNode, "bus"));
+ mapping.m_class =
+ PeripheralTypeTranslator::GetTypeFromString(XMLUtils::GetAttribute(currentNode, "class"));
+ mapping.m_mappedTo =
+ PeripheralTypeTranslator::GetTypeFromString(XMLUtils::GetAttribute(currentNode, "mapTo"));
+ GetSettingsFromMappingsFile(currentNode, mapping.m_settings);
+
+ m_mappings.push_back(mapping);
+ CLog::Log(LOGDEBUG, "{} - loaded node \"{}\"", __FUNCTION__, mapping.m_strDeviceName);
+ }
+
+ return true;
+}
+
+void CPeripherals::GetSettingsFromMappingsFile(
+ TiXmlElement* xmlNode, std::map<std::string, PeripheralDeviceSetting>& settings)
+{
+ TiXmlElement* currentNode = xmlNode->FirstChildElement("setting");
+ int iMaxOrder = 0;
+
+ while (currentNode)
+ {
+ SettingPtr setting;
+ std::string strKey = XMLUtils::GetAttribute(currentNode, "key");
+ if (strKey.empty())
+ continue;
+
+ std::string strSettingsType = XMLUtils::GetAttribute(currentNode, "type");
+ int iLabelId = currentNode->Attribute("label") ? atoi(currentNode->Attribute("label")) : -1;
+ const std::string config = XMLUtils::GetAttribute(currentNode, "configurable");
+ bool bConfigurable = (config.empty() || (config != "no" && config != "false" && config != "0"));
+ if (strSettingsType == "bool")
+ {
+ const std::string value = XMLUtils::GetAttribute(currentNode, "value");
+ bool bValue = (value != "no" && value != "false" && value != "0");
+ setting = std::make_shared<CSettingBool>(strKey, iLabelId, bValue);
+ }
+ else if (strSettingsType == "int")
+ {
+ int iValue = currentNode->Attribute("value") ? atoi(currentNode->Attribute("value")) : 0;
+ int iMin = currentNode->Attribute("min") ? atoi(currentNode->Attribute("min")) : 0;
+ int iStep = currentNode->Attribute("step") ? atoi(currentNode->Attribute("step")) : 1;
+ int iMax = currentNode->Attribute("max") ? atoi(currentNode->Attribute("max")) : 255;
+ setting = std::make_shared<CSettingInt>(strKey, iLabelId, iValue, iMin, iStep, iMax);
+ }
+ else if (strSettingsType == "float")
+ {
+ float fValue =
+ currentNode->Attribute("value") ? (float)atof(currentNode->Attribute("value")) : 0;
+ float fMin = currentNode->Attribute("min") ? (float)atof(currentNode->Attribute("min")) : 0;
+ float fStep =
+ currentNode->Attribute("step") ? (float)atof(currentNode->Attribute("step")) : 0;
+ float fMax = currentNode->Attribute("max") ? (float)atof(currentNode->Attribute("max")) : 0;
+ setting = std::make_shared<CSettingNumber>(strKey, iLabelId, fValue, fMin, fStep, fMax);
+ }
+ else if (StringUtils::EqualsNoCase(strSettingsType, "enum"))
+ {
+ std::string strEnums = XMLUtils::GetAttribute(currentNode, "lvalues");
+ if (!strEnums.empty())
+ {
+ TranslatableIntegerSettingOptions enums;
+ std::vector<std::string> valuesVec;
+ StringUtils::Tokenize(strEnums, valuesVec, "|");
+ for (unsigned int i = 0; i < valuesVec.size(); i++)
+ enums.emplace_back(atoi(valuesVec[i].c_str()), atoi(valuesVec[i].c_str()));
+ int iValue = currentNode->Attribute("value") ? atoi(currentNode->Attribute("value")) : 0;
+ setting = std::make_shared<CSettingInt>(strKey, iLabelId, iValue, enums);
+ }
+ }
+ else if (StringUtils::EqualsNoCase(strSettingsType, "addon"))
+ {
+ std::string addonFilter = XMLUtils::GetAttribute(currentNode, "addontype");
+ ADDON::AddonType addonType = ADDON::CAddonInfo::TranslateType(addonFilter);
+ std::string strValue = XMLUtils::GetAttribute(currentNode, "value");
+ setting = std::make_shared<CSettingAddon>(strKey, iLabelId, strValue);
+ static_cast<CSettingAddon&>(*setting).SetAddonType(addonType);
+ }
+ else
+ {
+ std::string strValue = XMLUtils::GetAttribute(currentNode, "value");
+ setting = std::make_shared<CSettingString>(strKey, iLabelId, strValue);
+ }
+
+ if (setting)
+ {
+ //! @todo add more types if needed
+
+ /* set the visibility */
+ setting->SetVisible(bConfigurable);
+
+ /* set the order */
+ int iOrder = 0;
+ currentNode->Attribute("order", &iOrder);
+ /* if the order attribute is invalid or 0, then the setting will be added at the end */
+ if (iOrder < 0)
+ iOrder = 0;
+ if (iOrder > iMaxOrder)
+ iMaxOrder = iOrder;
+
+ /* and add this new setting */
+ PeripheralDeviceSetting deviceSetting = {setting, iOrder};
+ settings[strKey] = deviceSetting;
+ }
+
+ currentNode = currentNode->NextSiblingElement("setting");
+ }
+
+ /* add the settings without an order attribute or an invalid order attribute set at the end */
+ for (auto& it : settings)
+ {
+ if (it.second.m_order == 0)
+ it.second.m_order = ++iMaxOrder;
+ }
+}
+
+void CPeripherals::GetDirectory(const std::string& strPath, CFileItemList& items) const
+{
+ if (!StringUtils::StartsWithNoCase(strPath, "peripherals://"))
+ return;
+
+ std::string strPathCut = strPath.substr(14);
+ std::string strBus = strPathCut.substr(0, strPathCut.find('/'));
+
+ std::unique_lock<CCriticalSection> lock(m_critSectionBusses);
+ for (const auto& bus : m_busses)
+ {
+ if (StringUtils::EqualsNoCase(strBus, "all") ||
+ StringUtils::EqualsNoCase(strBus, PeripheralTypeTranslator::BusTypeToString(bus->Type())))
+ bus->GetDirectory(strPath, items);
+ }
+}
+
+PeripheralPtr CPeripherals::GetByPath(const std::string& strPath) const
+{
+ PeripheralPtr result;
+
+ if (!StringUtils::StartsWithNoCase(strPath, "peripherals://"))
+ return result;
+
+ std::string strPathCut = strPath.substr(14);
+ std::string strBus = strPathCut.substr(0, strPathCut.find('/'));
+
+ std::unique_lock<CCriticalSection> lock(m_critSectionBusses);
+ for (const auto& bus : m_busses)
+ {
+ if (StringUtils::EqualsNoCase(strBus, PeripheralTypeTranslator::BusTypeToString(bus->Type())))
+ {
+ result = bus->GetByPath(strPath);
+ break;
+ }
+ }
+
+ return result;
+}
+
+bool CPeripherals::OnAction(const CAction& action)
+{
+ if (action.GetID() == ACTION_MUTE)
+ {
+ return ToggleMute();
+ }
+
+ if (SupportsCEC() && action.GetAmount() &&
+ (action.GetID() == ACTION_VOLUME_UP || action.GetID() == ACTION_VOLUME_DOWN))
+ {
+ PeripheralVector peripherals;
+ if (GetPeripheralsWithFeature(peripherals, FEATURE_CEC))
+ {
+ for (auto& peripheral : peripherals)
+ {
+ std::shared_ptr<CPeripheralCecAdapter> cecDevice =
+ std::static_pointer_cast<CPeripheralCecAdapter>(peripheral);
+ if (cecDevice->HasAudioControl())
+ {
+ if (action.GetID() == ACTION_VOLUME_UP)
+ cecDevice->VolumeUp();
+ else
+ cecDevice->VolumeDown();
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+bool CPeripherals::IsMuted()
+{
+ PeripheralVector peripherals;
+ if (SupportsCEC() && GetPeripheralsWithFeature(peripherals, FEATURE_CEC))
+ {
+ for (const auto& peripheral : peripherals)
+ {
+ std::shared_ptr<CPeripheralCecAdapter> cecDevice =
+ std::static_pointer_cast<CPeripheralCecAdapter>(peripheral);
+ if (cecDevice->IsMuted())
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CPeripherals::ToggleMute()
+{
+ PeripheralVector peripherals;
+ if (SupportsCEC() && GetPeripheralsWithFeature(peripherals, FEATURE_CEC))
+ {
+ for (auto& peripheral : peripherals)
+ {
+ std::shared_ptr<CPeripheralCecAdapter> cecDevice =
+ std::static_pointer_cast<CPeripheralCecAdapter>(peripheral);
+ if (cecDevice->HasAudioControl())
+ {
+ cecDevice->ToggleMute();
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool CPeripherals::ToggleDeviceState(CecStateChange mode /*= STATE_SWITCH_TOGGLE */)
+{
+ bool ret(false);
+ PeripheralVector peripherals;
+
+ if (SupportsCEC() && GetPeripheralsWithFeature(peripherals, FEATURE_CEC))
+ {
+ for (auto& peripheral : peripherals)
+ {
+ std::shared_ptr<CPeripheralCecAdapter> cecDevice =
+ std::static_pointer_cast<CPeripheralCecAdapter>(peripheral);
+ ret |= cecDevice->ToggleDeviceState(mode);
+ }
+ }
+
+ return ret;
+}
+
+bool CPeripherals::GetNextKeypress(float frameTime, CKey& key)
+{
+ PeripheralVector peripherals;
+ if (SupportsCEC() && GetPeripheralsWithFeature(peripherals, FEATURE_CEC))
+ {
+ for (auto& peripheral : peripherals)
+ {
+ std::shared_ptr<CPeripheralCecAdapter> cecDevice =
+ std::static_pointer_cast<CPeripheralCecAdapter>(peripheral);
+ if (cecDevice->GetButton())
+ {
+ CKey newKey(cecDevice->GetButton(), cecDevice->GetHoldTime());
+ cecDevice->ResetButton();
+ key = newKey;
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+EventPollHandlePtr CPeripherals::RegisterEventPoller()
+{
+ return m_eventScanner->RegisterPollHandle();
+}
+
+EventLockHandlePtr CPeripherals::RegisterEventLock()
+{
+ return m_eventScanner->RegisterLock();
+}
+
+void CPeripherals::OnUserNotification()
+{
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_INPUT_RUMBLENOTIFY))
+ return;
+
+ PeripheralVector peripherals;
+ GetPeripheralsWithFeature(peripherals, FEATURE_RUMBLE);
+
+ for (auto& peripheral : peripherals)
+ peripheral->OnUserNotification();
+}
+
+void CPeripherals::TestFeature(PeripheralFeature feature)
+{
+ PeripheralVector peripherals;
+ GetPeripheralsWithFeature(peripherals, feature);
+
+ for (auto& peripheral : peripherals)
+ {
+ if (peripheral->TestFeature(feature))
+ {
+ CLog::Log(LOGDEBUG, "PERIPHERALS: Device \"{}\" tested {} feature", peripheral->DeviceName(),
+ PeripheralTypeTranslator::FeatureToString(feature));
+ }
+ else
+ {
+ if (peripheral->HasFeature(feature))
+ CLog::Log(LOGDEBUG, "PERIPHERALS: Device \"{}\" failed to test {} feature",
+ peripheral->DeviceName(), PeripheralTypeTranslator::FeatureToString(feature));
+ else
+ CLog::Log(LOGDEBUG, "PERIPHERALS: Device \"{}\" doesn't support {} feature",
+ peripheral->DeviceName(), PeripheralTypeTranslator::FeatureToString(feature));
+ }
+ }
+}
+
+void CPeripherals::PowerOffDevices()
+{
+ TestFeature(FEATURE_POWER_OFF);
+}
+
+void CPeripherals::ProcessEvents(void)
+{
+ std::vector<PeripheralBusPtr> busses;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSectionBusses);
+ busses = m_busses;
+ }
+
+ for (PeripheralBusPtr& bus : busses)
+ bus->ProcessEvents();
+}
+
+void CPeripherals::EnableButtonMapping()
+{
+ std::vector<PeripheralBusPtr> busses;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSectionBusses);
+ busses = m_busses;
+ }
+
+ for (PeripheralBusPtr& bus : busses)
+ bus->EnableButtonMapping();
+}
+
+PeripheralAddonPtr CPeripherals::GetAddonWithButtonMap(const CPeripheral* device)
+{
+ PeripheralBusAddonPtr addonBus =
+ std::static_pointer_cast<CPeripheralBusAddon>(GetBusByType(PERIPHERAL_BUS_ADDON));
+
+ PeripheralAddonPtr addon;
+
+ PeripheralAddonPtr addonWithButtonMap;
+ if (addonBus && addonBus->GetAddonWithButtonMap(device, addonWithButtonMap))
+ addon = std::move(addonWithButtonMap);
+
+ return addon;
+}
+
+void CPeripherals::ResetButtonMaps(const std::string& controllerId)
+{
+ PeripheralBusAddonPtr addonBus =
+ std::static_pointer_cast<CPeripheralBusAddon>(GetBusByType(PERIPHERAL_BUS_ADDON));
+
+ PeripheralVector peripherals;
+ GetPeripheralsWithFeature(peripherals, FEATURE_JOYSTICK);
+
+ for (auto& peripheral : peripherals)
+ {
+ PeripheralAddonPtr addon;
+ if (addonBus->GetAddonWithButtonMap(peripheral.get(), addon))
+ {
+ CAddonButtonMap buttonMap(peripheral.get(), addon, controllerId, *this);
+ buttonMap.Reset();
+ }
+ }
+}
+
+void CPeripherals::RegisterJoystickButtonMapper(IButtonMapper* mapper)
+{
+ PeripheralVector peripherals;
+ GetPeripheralsWithFeature(peripherals, FEATURE_JOYSTICK);
+ GetPeripheralsWithFeature(peripherals, FEATURE_KEYBOARD);
+ GetPeripheralsWithFeature(peripherals, FEATURE_MOUSE);
+
+ for (auto& peripheral : peripherals)
+ peripheral->RegisterJoystickButtonMapper(mapper);
+}
+
+void CPeripherals::UnregisterJoystickButtonMapper(IButtonMapper* mapper)
+{
+ mapper->ResetButtonMapCallbacks();
+
+ PeripheralVector peripherals;
+ GetPeripheralsWithFeature(peripherals, FEATURE_JOYSTICK);
+ GetPeripheralsWithFeature(peripherals, FEATURE_KEYBOARD);
+ GetPeripheralsWithFeature(peripherals, FEATURE_MOUSE);
+
+ for (auto& peripheral : peripherals)
+ peripheral->UnregisterJoystickButtonMapper(mapper);
+}
+
+void CPeripherals::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_LOCALE_LANGUAGE)
+ {
+ // user set language, no longer use the TV's language
+ PeripheralVector cecDevices;
+ if (GetPeripheralsWithFeature(cecDevices, FEATURE_CEC) > 0)
+ {
+ for (auto& cecDevice : cecDevices)
+ cecDevice->SetSetting("use_tv_menu_language", false);
+ }
+ }
+}
+
+void CPeripherals::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_INPUT_PERIPHERALS)
+ CGUIDialogPeripherals::Show(*this);
+ else if (settingId == CSettings::SETTING_INPUT_CONTROLLERCONFIG)
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_DIALOG_GAME_CONTROLLERS);
+ else if (settingId == CSettings::SETTING_INPUT_TESTRUMBLE)
+ TestFeature(FEATURE_RUMBLE);
+ else if (settingId == CSettings::SETTING_INPUT_PERIPHERALLIBRARIES)
+ {
+ std::string strAddonId;
+ if (CGUIWindowAddonBrowser::SelectAddonID(ADDON::AddonType::PERIPHERALDLL, strAddonId, false,
+ true, true, false, true) == 1 &&
+ !strAddonId.empty())
+ {
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(strAddonId, addon, ADDON::OnlyEnabled::CHOICE_YES))
+ CGUIDialogAddonSettings::ShowForAddon(addon);
+ }
+ }
+}
+
+void CPeripherals::OnApplicationMessage(MESSAGING::ThreadMessage* pMsg)
+{
+ switch (pMsg->dwMessage)
+ {
+ case TMSG_CECTOGGLESTATE:
+ *static_cast<bool*>(pMsg->lpVoid) = ToggleDeviceState(STATE_SWITCH_TOGGLE);
+ break;
+
+ case TMSG_CECACTIVATESOURCE:
+ ToggleDeviceState(STATE_ACTIVATE_SOURCE);
+ break;
+
+ case TMSG_CECSTANDBY:
+ ToggleDeviceState(STATE_STANDBY);
+ break;
+ }
+}
+
+int CPeripherals::GetMessageMask()
+{
+ return TMSG_MASK_PERIPHERALS;
+}
+
+void CPeripherals::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (flag == ANNOUNCEMENT::Player &&
+ sender == ANNOUNCEMENT::CAnnouncementManager::ANNOUNCEMENT_SENDER)
+ {
+ if (message == "OnQuit")
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_INPUT_CONTROLLERPOWEROFF))
+ PowerOffDevices();
+ }
+ }
+}
diff --git a/xbmc/peripherals/Peripherals.h b/xbmc/peripherals/Peripherals.h
new file mode 100644
index 0000000..d9931d7
--- /dev/null
+++ b/xbmc/peripherals/Peripherals.h
@@ -0,0 +1,371 @@
+/*
+ * 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 "IEventScannerCallback.h"
+#include "bus/PeripheralBus.h"
+#include "devices/Peripheral.h"
+#include "interfaces/IAnnouncer.h"
+#include "messaging/IMessageTarget.h"
+#include "settings/lib/ISettingCallback.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+#include "utils/Observer.h"
+
+#include <memory>
+#include <vector>
+
+class CFileItemList;
+class CInputManager;
+class CSetting;
+class CSettingsCategory;
+class TiXmlElement;
+class CAction;
+class CKey;
+
+namespace KODI
+{
+namespace GAME
+{
+class CControllerManager;
+}
+
+namespace JOYSTICK
+{
+class IButtonMapper;
+}
+} // namespace KODI
+
+namespace PERIPHERALS
+{
+class CEventScanner;
+
+class CPeripherals : public ISettingCallback,
+ public Observable,
+ public KODI::MESSAGING::IMessageTarget,
+ public IEventScannerCallback,
+ public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ explicit CPeripherals(CInputManager& inputManager,
+ KODI::GAME::CControllerManager& controllerProfiles);
+
+ ~CPeripherals() override;
+
+ /*!
+ * @brief Initialise the peripherals manager.
+ */
+ void Initialise();
+
+ /*!
+ * @brief Clear all data known by the peripherals manager.
+ */
+ void Clear();
+
+ /*!
+ * @brief Get the instance of the peripheral at the given location.
+ * @param strLocation The location.
+ * @param busType The bus to query. Default (PERIPHERAL_BUS_UNKNOWN) searches all busses.
+ * @return The peripheral or NULL if it wasn't found.
+ */
+ PeripheralPtr GetPeripheralAtLocation(const std::string& strLocation,
+ PeripheralBusType busType = PERIPHERAL_BUS_UNKNOWN) const;
+
+ /*!
+ * @brief Check whether a peripheral is present at the given location.
+ * @param strLocation The location.
+ * @param busType The bus to query. Default (PERIPHERAL_BUS_UNKNOWN) searches all busses.
+ * @return True when a peripheral was found, false otherwise.
+ */
+ bool HasPeripheralAtLocation(const std::string& strLocation,
+ PeripheralBusType busType = PERIPHERAL_BUS_UNKNOWN) const;
+
+ /*!
+ * @brief Get the bus that holds the device with the given location.
+ * @param strLocation The location.
+ * @return The bus or NULL if no device was found.
+ */
+ PeripheralBusPtr GetBusWithDevice(const std::string& strLocation) const;
+
+ /*!
+ * @brief Check if any busses support the given feature
+ * @param feature The feature to check for
+ * @return True if a bus supports the feature, false otherwise
+ */
+ bool SupportsFeature(PeripheralFeature feature) const;
+
+ /*!
+ * @brief Get all peripheral instances that have the given feature.
+ * @param results The list of results.
+ * @param feature The feature to search for.
+ * @param busType The bus to query. Default (PERIPHERAL_BUS_UNKNOWN) searches all busses.
+ * @return The number of devices that have been found.
+ */
+ int GetPeripheralsWithFeature(PeripheralVector& results,
+ const PeripheralFeature feature,
+ PeripheralBusType busType = PERIPHERAL_BUS_UNKNOWN) const;
+
+ size_t GetNumberOfPeripherals() const;
+
+ /*!
+ * @brief Check whether there is at least one device present with the given feature.
+ * @param feature The feature to check for.
+ * @param busType The bus to query. Default (PERIPHERAL_BUS_UNKNOWN) searches all busses.
+ * @return True when at least one device was found with this feature, false otherwise.
+ */
+ bool HasPeripheralWithFeature(const PeripheralFeature feature,
+ PeripheralBusType busType = PERIPHERAL_BUS_UNKNOWN) const;
+
+ /*!
+ * @brief Called when a device has been added to a bus.
+ * @param bus The bus the device was added to.
+ * @param peripheral The peripheral that has been added.
+ */
+ void OnDeviceAdded(const CPeripheralBus& bus, const CPeripheral& peripheral);
+
+ /*!
+ * @brief Called when a device has been deleted from a bus.
+ * @param bus The bus from which the device removed.
+ * @param peripheral The peripheral that has been removed.
+ */
+ void OnDeviceDeleted(const CPeripheralBus& bus, const CPeripheral& peripheral);
+
+ /*!
+ * @brief Creates a new instance of a peripheral.
+ * @param bus The bus on which this peripheral is present.
+ * @param result The scan result from the device scanning code.
+ * @return The new peripheral or NULL if it could not be created.
+ */
+ void CreatePeripheral(CPeripheralBus& bus, const PeripheralScanResult& result);
+
+ /*!
+ * @brief Add the settings that are defined in the mappings file to the peripheral (if there is
+ * anything defined).
+ * @param peripheral The peripheral to get the settings for.
+ */
+ void GetSettingsFromMapping(CPeripheral& peripheral) const;
+
+ /*!
+ * @brief Trigger a device scan on all known busses
+ */
+ void TriggerDeviceScan(const PeripheralBusType type = PERIPHERAL_BUS_UNKNOWN);
+
+ /*!
+ * @brief Get the instance of a bus given it's type.
+ * @param type The bus type.
+ * @return The bus or NULL if it wasn't found.
+ */
+ PeripheralBusPtr GetBusByType(const PeripheralBusType type) const;
+
+ /*!
+ * @brief Get all fileitems for a path.
+ * @param strPath The path to the directory to get the items from.
+ * @param items The item list.
+ */
+ void GetDirectory(const std::string& strPath, CFileItemList& items) const;
+
+ /*!
+ * @brief Get the instance of a peripheral given it's path.
+ * @param strPath The path to the peripheral.
+ * @return The peripheral or NULL if it wasn't found.
+ */
+ PeripheralPtr GetByPath(const std::string& strPath) const;
+
+ /*!
+ * @brief Try to let one of the peripherals handle an action.
+ * @param action The change to handle.
+ * @return True when this change was handled by a peripheral (and should not be handled by
+ * anything else), false otherwise.
+ */
+ bool OnAction(const CAction& action);
+
+ /*!
+ * @brief Check whether there's a peripheral that reports to be muted.
+ * @return True when at least one peripheral reports to be muted, false otherwise.
+ */
+ bool IsMuted();
+
+ /*!
+ * @brief Try to toggle the mute status via a peripheral.
+ * @return True when this change was handled by a peripheral (and should not be handled by
+ * anything else), false otherwise.
+ */
+ bool ToggleMute();
+
+ /*!
+ * @brief Try to toggle the playing device state via a peripheral.
+ * @param mode Whether to activate, put on standby or toggle the source.
+ * @return True when the playing device has been switched on, false otherwise.
+ */
+ bool ToggleDeviceState(const CecStateChange mode = STATE_SWITCH_TOGGLE);
+
+ /*!
+ * @brief Try to mute the audio via a peripheral.
+ * @return True when this change was handled by a peripheral (and should not be handled by
+ * anything else), false otherwise.
+ */
+ bool Mute()
+ {
+ return ToggleMute();
+ } //! @todo CEC only supports toggling the mute status at this time
+
+ /*!
+ * @brief Try to unmute the audio via a peripheral.
+ * @return True when this change was handled by a peripheral (and should not be handled by
+ * anything else), false otherwise.
+ */
+ bool UnMute()
+ {
+ return ToggleMute();
+ } //! @todo CEC only supports toggling the mute status at this time
+
+ /*!
+ * @brief Try to get a keypress from a peripheral.
+ * @param frameTime The current frametime.
+ * @param key The fetched key.
+ * @return True when a keypress was fetched, false otherwise.
+ */
+ bool GetNextKeypress(float frameTime, CKey& key);
+
+ /*!
+ * @brief Register with the event scanner to control scan timing
+ * @return A handle that unregisters itself when expired
+ */
+ EventPollHandlePtr RegisterEventPoller();
+
+ /*!
+ * @brief Register with the event scanner to disable event processing
+ * @return A handle that unregisters itself when expired
+ */
+ EventLockHandlePtr RegisterEventLock();
+
+ /*!
+ *
+ */
+ void OnUserNotification();
+
+ /*!
+ * @brief Request peripherals with the specified feature to perform a quick test
+ * @return true if any peripherals support the feature, false otherwise
+ */
+ void TestFeature(PeripheralFeature feature);
+
+ /*!
+ * \brief Request all devices with power-off support to power down
+ */
+ void PowerOffDevices();
+
+ bool SupportsCEC() const
+ {
+#if defined(HAVE_LIBCEC)
+ return true;
+#else
+ return false;
+#endif
+ }
+
+ // implementation of IEventScannerCallback
+ void ProcessEvents(void) override;
+
+ /*!
+ * \brief Initialize button mapping
+ *
+ * This command enables button mapping on all busses. Button maps allow
+ * connect events from the driver to the higher-level features used by
+ * controller profiles.
+ *
+ * If user input is required, a blocking dialog may be shown.
+ */
+ void EnableButtonMapping();
+
+ /*!
+ * \brief Get an add-on that can provide button maps for a device
+ * \return An add-on that provides button maps, or empty if no add-on is found
+ */
+ PeripheralAddonPtr GetAddonWithButtonMap(const CPeripheral* device);
+
+ /*!
+ * \brief Reset all button maps to the defaults for all devices and the given controller
+ * \param controllerId The controller profile to reset
+ * @todo Add a device parameter to allow resetting button maps per-device
+ */
+ void ResetButtonMaps(const std::string& controllerId);
+
+ /*!
+ * \brief Register a button mapper interface
+ * \param mapper The button mapper
+ *
+ * Clients implementing the IButtonMapper interface call
+ * \ref CPeripherals::RegisterJoystickButtonMapper to register themselves
+ * as eligible for button mapping commands.
+ *
+ * When registering the mapper is forwarded to all peripherals. See
+ * \ref CPeripheral::RegisterJoystickButtonMapper for what is done to the
+ * mapper after being given to the peripheral.
+ */
+ void RegisterJoystickButtonMapper(KODI::JOYSTICK::IButtonMapper* mapper);
+
+ /*!
+ * \brief Unregister a button mapper interface
+ * \param mapper The button mapper
+ */
+ void UnregisterJoystickButtonMapper(KODI::JOYSTICK::IButtonMapper* mapper);
+
+ // implementation of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // implementation of IMessageTarget
+ void OnApplicationMessage(KODI::MESSAGING::ThreadMessage* pMsg) override;
+ int GetMessageMask() override;
+
+ // implementation of IAnnouncer
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ /*!
+ * \brief Access the input manager passed to the constructor
+ */
+ CInputManager& GetInputManager() { return m_inputManager; }
+
+ /*!
+ * \brief Access controller profiles through the construction parameter
+ */
+ KODI::GAME::CControllerManager& GetControllerProfiles() { return m_controllerProfiles; }
+
+ /*!
+ * \brief Get a mutex that allows for add-on install tasks to block on each other
+ */
+ CCriticalSection& GetAddonInstallMutex() { return m_addonInstallMutex; }
+
+private:
+ bool LoadMappings();
+ bool GetMappingForDevice(const CPeripheralBus& bus, PeripheralScanResult& result) const;
+ static void GetSettingsFromMappingsFile(
+ TiXmlElement* xmlNode, std::map<std::string, PeripheralDeviceSetting>& m_settings);
+
+ void OnDeviceChanged();
+
+ // Construction parameters
+ CInputManager& m_inputManager;
+ KODI::GAME::CControllerManager& m_controllerProfiles;
+
+#if !defined(HAVE_LIBCEC)
+ bool m_bMissingLibCecWarningDisplayed = false;
+#endif
+ std::vector<PeripheralBusPtr> m_busses;
+ std::vector<PeripheralDeviceMapping> m_mappings;
+ std::unique_ptr<CEventScanner> m_eventScanner;
+ mutable CCriticalSection m_critSectionBusses;
+ mutable CCriticalSection m_critSectionMappings;
+ CCriticalSection m_addonInstallMutex;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/addons/AddonButtonMap.cpp b/xbmc/peripherals/addons/AddonButtonMap.cpp
new file mode 100644
index 0000000..0d76ec9
--- /dev/null
+++ b/xbmc/peripherals/addons/AddonButtonMap.cpp
@@ -0,0 +1,734 @@
+/*
+ * Copyright (C) 2014-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 "AddonButtonMap.h"
+
+#include "PeripheralAddonTranslator.h"
+#include "input/joysticks/JoystickUtils.h"
+#include "peripherals/Peripherals.h"
+#include "peripherals/devices/Peripheral.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <assert.h>
+#include <mutex>
+#include <vector>
+
+using namespace KODI;
+using namespace JOYSTICK;
+using namespace PERIPHERALS;
+
+CAddonButtonMap::CAddonButtonMap(CPeripheral* device,
+ const std::weak_ptr<CPeripheralAddon>& addon,
+ const std::string& strControllerId,
+ CPeripherals& manager)
+ : m_device(device), m_addon(addon), m_strControllerId(strControllerId), m_manager(manager)
+{
+ auto peripheralAddon = m_addon.lock();
+ assert(peripheralAddon != nullptr);
+
+ peripheralAddon->RegisterButtonMap(device, this);
+}
+
+CAddonButtonMap::~CAddonButtonMap(void)
+{
+ if (auto addon = m_addon.lock())
+ addon->UnregisterButtonMap(this);
+}
+
+std::string CAddonButtonMap::Location(void) const
+{
+ return m_device->Location();
+}
+
+bool CAddonButtonMap::Load(void)
+{
+ std::string controllerAppearance;
+ FeatureMap features;
+ DriverMap driverMap;
+ PrimitiveVector ignoredPrimitives;
+
+ bool bSuccess = false;
+ if (auto addon = m_addon.lock())
+ {
+ bSuccess |= addon->GetFeatures(m_device, m_strControllerId, features);
+ bSuccess |= addon->GetIgnoredPrimitives(m_device, ignoredPrimitives);
+ }
+
+ if (features.empty())
+ {
+ // Check if we can initialize a buttonmap from the peripheral bus
+ PeripheralBusPtr peripheralBus = m_manager.GetBusByType(m_device->GetBusType());
+ if (peripheralBus)
+ {
+ CLog::Log(LOGDEBUG,
+ "Buttonmap not found for {}, attempting to initialize from peripheral bus",
+ m_device->Location());
+ if (peripheralBus->InitializeButtonMap(*m_device, *this))
+ {
+ bSuccess = true;
+
+ if (auto addon = m_addon.lock())
+ {
+ addon->GetFeatures(m_device, m_strControllerId, features);
+ addon->GetIgnoredPrimitives(m_device, ignoredPrimitives);
+ }
+ }
+ }
+ }
+
+ // GetFeatures() was changed to always return false if no features were
+ // retrieved. Check here, just in case its contract is changed or violated in
+ // the future.
+ if (bSuccess && features.empty())
+ bSuccess = false;
+
+ if (bSuccess)
+ driverMap = CreateLookupTable(features);
+ else
+ CLog::Log(LOGDEBUG, "Failed to load button map for \"{}\"", m_device->Location());
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ m_controllerAppearance = std::move(controllerAppearance);
+ m_features = std::move(features);
+ m_driverMap = std::move(driverMap);
+ m_ignoredPrimitives = CPeripheralAddonTranslator::TranslatePrimitives(ignoredPrimitives);
+ }
+
+ return true;
+}
+
+void CAddonButtonMap::Reset(void)
+{
+ if (auto addon = m_addon.lock())
+ addon->ResetButtonMap(m_device, m_strControllerId);
+}
+
+bool CAddonButtonMap::IsEmpty(void) const
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ return m_driverMap.empty();
+}
+
+std::string CAddonButtonMap::GetAppearance() const
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ return m_controllerAppearance;
+}
+
+bool CAddonButtonMap::SetAppearance(const std::string& controllerId) const
+{
+ return false;
+}
+
+bool CAddonButtonMap::GetFeature(const CDriverPrimitive& primitive, FeatureName& feature)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ DriverMap::const_iterator it = m_driverMap.find(primitive);
+ if (it != m_driverMap.end())
+ {
+ feature = it->second;
+ return true;
+ }
+
+ return false;
+}
+
+FEATURE_TYPE CAddonButtonMap::GetFeatureType(const FeatureName& feature)
+{
+ FEATURE_TYPE type = FEATURE_TYPE::UNKNOWN;
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ FeatureMap::const_iterator it = m_features.find(feature);
+ if (it != m_features.end())
+ type = CPeripheralAddonTranslator::TranslateFeatureType(it->second.Type());
+
+ return type;
+}
+
+bool CAddonButtonMap::GetScalar(const FeatureName& feature, CDriverPrimitive& primitive)
+{
+ bool retVal(false);
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ FeatureMap::const_iterator it = m_features.find(feature);
+ if (it != m_features.end())
+ {
+ const kodi::addon::JoystickFeature& addonFeature = it->second;
+
+ if (addonFeature.Type() == JOYSTICK_FEATURE_TYPE_SCALAR ||
+ addonFeature.Type() == JOYSTICK_FEATURE_TYPE_MOTOR)
+ {
+ primitive = CPeripheralAddonTranslator::TranslatePrimitive(
+ addonFeature.Primitive(JOYSTICK_SCALAR_PRIMITIVE));
+ retVal = true;
+ }
+ }
+
+ return retVal;
+}
+
+void CAddonButtonMap::AddScalar(const FeatureName& feature, const CDriverPrimitive& primitive)
+{
+ const bool bMotor = (primitive.Type() == PRIMITIVE_TYPE::MOTOR);
+
+ kodi::addon::JoystickFeature scalar(feature, bMotor ? JOYSTICK_FEATURE_TYPE_MOTOR
+ : JOYSTICK_FEATURE_TYPE_SCALAR);
+ scalar.SetPrimitive(JOYSTICK_SCALAR_PRIMITIVE,
+ CPeripheralAddonTranslator::TranslatePrimitive(primitive));
+
+ if (auto addon = m_addon.lock())
+ addon->MapFeature(m_device, m_strControllerId, scalar);
+}
+
+bool CAddonButtonMap::GetAnalogStick(const FeatureName& feature,
+ JOYSTICK::ANALOG_STICK_DIRECTION direction,
+ JOYSTICK::CDriverPrimitive& primitive)
+{
+ bool retVal(false);
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ FeatureMap::const_iterator it = m_features.find(feature);
+ if (it != m_features.end())
+ {
+ const kodi::addon::JoystickFeature& addonFeature = it->second;
+
+ if (addonFeature.Type() == JOYSTICK_FEATURE_TYPE_ANALOG_STICK)
+ {
+ primitive = CPeripheralAddonTranslator::TranslatePrimitive(
+ addonFeature.Primitive(GetAnalogStickIndex(direction)));
+ retVal = primitive.IsValid();
+ }
+ }
+
+ return retVal;
+}
+
+void CAddonButtonMap::AddAnalogStick(const FeatureName& feature,
+ JOYSTICK::ANALOG_STICK_DIRECTION direction,
+ const JOYSTICK::CDriverPrimitive& primitive)
+{
+ using namespace JOYSTICK;
+
+ JOYSTICK_FEATURE_PRIMITIVE primitiveIndex = GetAnalogStickIndex(direction);
+ kodi::addon::DriverPrimitive addonPrimitive =
+ CPeripheralAddonTranslator::TranslatePrimitive(primitive);
+
+ kodi::addon::JoystickFeature analogStick(feature, JOYSTICK_FEATURE_TYPE_ANALOG_STICK);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ auto it = m_features.find(feature);
+ if (it != m_features.end())
+ analogStick = it->second;
+ }
+
+ const bool bModified = (primitive != CPeripheralAddonTranslator::TranslatePrimitive(
+ analogStick.Primitive(primitiveIndex)));
+ if (bModified)
+ analogStick.SetPrimitive(primitiveIndex, addonPrimitive);
+
+ if (auto addon = m_addon.lock())
+ addon->MapFeature(m_device, m_strControllerId, analogStick);
+
+ // Because each direction is mapped individually, we need to refresh the
+ // feature each time a new direction is mapped.
+ if (bModified)
+ Load();
+}
+
+bool CAddonButtonMap::GetRelativePointer(const FeatureName& feature,
+ JOYSTICK::RELATIVE_POINTER_DIRECTION direction,
+ JOYSTICK::CDriverPrimitive& primitive)
+{
+ bool retVal(false);
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ FeatureMap::const_iterator it = m_features.find(feature);
+ if (it != m_features.end())
+ {
+ const kodi::addon::JoystickFeature& addonFeature = it->second;
+
+ if (addonFeature.Type() == JOYSTICK_FEATURE_TYPE_RELPOINTER)
+ {
+ primitive = CPeripheralAddonTranslator::TranslatePrimitive(
+ addonFeature.Primitive(GetRelativePointerIndex(direction)));
+ retVal = primitive.IsValid();
+ }
+ }
+
+ return retVal;
+}
+
+void CAddonButtonMap::AddRelativePointer(const FeatureName& feature,
+ JOYSTICK::RELATIVE_POINTER_DIRECTION direction,
+ const JOYSTICK::CDriverPrimitive& primitive)
+{
+ using namespace JOYSTICK;
+
+ JOYSTICK_FEATURE_PRIMITIVE primitiveIndex = GetRelativePointerIndex(direction);
+ kodi::addon::DriverPrimitive addonPrimitive =
+ CPeripheralAddonTranslator::TranslatePrimitive(primitive);
+
+ kodi::addon::JoystickFeature relPointer(feature, JOYSTICK_FEATURE_TYPE_RELPOINTER);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ auto it = m_features.find(feature);
+ if (it != m_features.end())
+ relPointer = it->second;
+ }
+
+ const bool bModified = (primitive != CPeripheralAddonTranslator::TranslatePrimitive(
+ relPointer.Primitive(primitiveIndex)));
+ if (bModified)
+ relPointer.SetPrimitive(primitiveIndex, addonPrimitive);
+
+ if (auto addon = m_addon.lock())
+ addon->MapFeature(m_device, m_strControllerId, relPointer);
+
+ // Because each direction is mapped individually, we need to refresh the
+ // feature each time a new direction is mapped.
+ if (bModified)
+ Load();
+}
+
+bool CAddonButtonMap::GetAccelerometer(const FeatureName& feature,
+ CDriverPrimitive& positiveX,
+ CDriverPrimitive& positiveY,
+ CDriverPrimitive& positiveZ)
+{
+ bool retVal(false);
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ FeatureMap::const_iterator it = m_features.find(feature);
+ if (it != m_features.end())
+ {
+ const kodi::addon::JoystickFeature& addonFeature = it->second;
+
+ if (addonFeature.Type() == JOYSTICK_FEATURE_TYPE_ACCELEROMETER)
+ {
+ positiveX = CPeripheralAddonTranslator::TranslatePrimitive(
+ addonFeature.Primitive(JOYSTICK_ACCELEROMETER_POSITIVE_X));
+ positiveY = CPeripheralAddonTranslator::TranslatePrimitive(
+ addonFeature.Primitive(JOYSTICK_ACCELEROMETER_POSITIVE_Y));
+ positiveZ = CPeripheralAddonTranslator::TranslatePrimitive(
+ addonFeature.Primitive(JOYSTICK_ACCELEROMETER_POSITIVE_Z));
+ retVal = true;
+ }
+ }
+
+ return retVal;
+}
+
+void CAddonButtonMap::AddAccelerometer(const FeatureName& feature,
+ const CDriverPrimitive& positiveX,
+ const CDriverPrimitive& positiveY,
+ const CDriverPrimitive& positiveZ)
+{
+ using namespace JOYSTICK;
+
+ kodi::addon::JoystickFeature accelerometer(feature, JOYSTICK_FEATURE_TYPE_ACCELEROMETER);
+
+ accelerometer.SetPrimitive(JOYSTICK_ACCELEROMETER_POSITIVE_X,
+ CPeripheralAddonTranslator::TranslatePrimitive(positiveX));
+ accelerometer.SetPrimitive(JOYSTICK_ACCELEROMETER_POSITIVE_Y,
+ CPeripheralAddonTranslator::TranslatePrimitive(positiveY));
+ accelerometer.SetPrimitive(JOYSTICK_ACCELEROMETER_POSITIVE_Z,
+ CPeripheralAddonTranslator::TranslatePrimitive(positiveZ));
+
+ if (auto addon = m_addon.lock())
+ addon->MapFeature(m_device, m_strControllerId, accelerometer);
+}
+
+bool CAddonButtonMap::GetWheel(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::WHEEL_DIRECTION direction,
+ KODI::JOYSTICK::CDriverPrimitive& primitive)
+{
+ bool retVal(false);
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ FeatureMap::const_iterator it = m_features.find(feature);
+ if (it != m_features.end())
+ {
+ const kodi::addon::JoystickFeature& addonFeature = it->second;
+
+ if (addonFeature.Type() == JOYSTICK_FEATURE_TYPE_WHEEL)
+ {
+ primitive = CPeripheralAddonTranslator::TranslatePrimitive(
+ addonFeature.Primitive(GetPrimitiveIndex(direction)));
+ retVal = primitive.IsValid();
+ }
+ }
+
+ return retVal;
+}
+
+void CAddonButtonMap::AddWheel(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::WHEEL_DIRECTION direction,
+ const KODI::JOYSTICK::CDriverPrimitive& primitive)
+{
+ using namespace JOYSTICK;
+
+ JOYSTICK_FEATURE_PRIMITIVE primitiveIndex = GetPrimitiveIndex(direction);
+ kodi::addon::DriverPrimitive addonPrimitive =
+ CPeripheralAddonTranslator::TranslatePrimitive(primitive);
+
+ kodi::addon::JoystickFeature joystickFeature(feature, JOYSTICK_FEATURE_TYPE_WHEEL);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ auto it = m_features.find(feature);
+ if (it != m_features.end())
+ joystickFeature = it->second;
+ }
+
+ const bool bModified = (primitive != CPeripheralAddonTranslator::TranslatePrimitive(
+ joystickFeature.Primitive(primitiveIndex)));
+ if (bModified)
+ joystickFeature.SetPrimitive(primitiveIndex, addonPrimitive);
+
+ if (auto addon = m_addon.lock())
+ addon->MapFeature(m_device, m_strControllerId, joystickFeature);
+
+ // Because each direction is mapped individually, we need to refresh the
+ // feature each time a new direction is mapped.
+ if (bModified)
+ Load();
+}
+
+bool CAddonButtonMap::GetThrottle(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::THROTTLE_DIRECTION direction,
+ KODI::JOYSTICK::CDriverPrimitive& primitive)
+{
+ bool retVal(false);
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ FeatureMap::const_iterator it = m_features.find(feature);
+ if (it != m_features.end())
+ {
+ const kodi::addon::JoystickFeature& addonFeature = it->second;
+
+ if (addonFeature.Type() == JOYSTICK_FEATURE_TYPE_THROTTLE)
+ {
+ primitive = CPeripheralAddonTranslator::TranslatePrimitive(
+ addonFeature.Primitive(GetPrimitiveIndex(direction)));
+ retVal = primitive.IsValid();
+ }
+ }
+
+ return retVal;
+}
+
+void CAddonButtonMap::AddThrottle(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::THROTTLE_DIRECTION direction,
+ const KODI::JOYSTICK::CDriverPrimitive& primitive)
+{
+ using namespace JOYSTICK;
+
+ JOYSTICK_FEATURE_PRIMITIVE primitiveIndex = GetPrimitiveIndex(direction);
+ kodi::addon::DriverPrimitive addonPrimitive =
+ CPeripheralAddonTranslator::TranslatePrimitive(primitive);
+
+ kodi::addon::JoystickFeature joystickFeature(feature, JOYSTICK_FEATURE_TYPE_THROTTLE);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ auto it = m_features.find(feature);
+ if (it != m_features.end())
+ joystickFeature = it->second;
+ }
+
+ const bool bModified = (primitive != CPeripheralAddonTranslator::TranslatePrimitive(
+ joystickFeature.Primitive(primitiveIndex)));
+ if (bModified)
+ joystickFeature.SetPrimitive(primitiveIndex, addonPrimitive);
+
+ if (auto addon = m_addon.lock())
+ addon->MapFeature(m_device, m_strControllerId, joystickFeature);
+
+ // Because each direction is mapped individually, we need to refresh the
+ // feature each time a new direction is mapped.
+ if (bModified)
+ Load();
+}
+
+bool CAddonButtonMap::GetKey(const FeatureName& feature, CDriverPrimitive& primitive)
+{
+ bool retVal(false);
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ FeatureMap::const_iterator it = m_features.find(feature);
+ if (it != m_features.end())
+ {
+ const kodi::addon::JoystickFeature& addonFeature = it->second;
+
+ if (addonFeature.Type() == JOYSTICK_FEATURE_TYPE_KEY)
+ {
+ primitive = CPeripheralAddonTranslator::TranslatePrimitive(
+ addonFeature.Primitive(JOYSTICK_SCALAR_PRIMITIVE));
+ retVal = true;
+ }
+ }
+
+ return retVal;
+}
+
+void CAddonButtonMap::AddKey(const FeatureName& feature, const CDriverPrimitive& primitive)
+{
+ kodi::addon::JoystickFeature scalar(feature, JOYSTICK_FEATURE_TYPE_KEY);
+ scalar.SetPrimitive(JOYSTICK_SCALAR_PRIMITIVE,
+ CPeripheralAddonTranslator::TranslatePrimitive(primitive));
+
+ if (auto addon = m_addon.lock())
+ addon->MapFeature(m_device, m_strControllerId, scalar);
+}
+
+void CAddonButtonMap::SetIgnoredPrimitives(
+ const std::vector<JOYSTICK::CDriverPrimitive>& primitives)
+{
+ if (auto addon = m_addon.lock())
+ addon->SetIgnoredPrimitives(m_device,
+ CPeripheralAddonTranslator::TranslatePrimitives(primitives));
+}
+
+bool CAddonButtonMap::IsIgnored(const JOYSTICK::CDriverPrimitive& primitive)
+{
+ return std::find(m_ignoredPrimitives.begin(), m_ignoredPrimitives.end(), primitive) !=
+ m_ignoredPrimitives.end();
+}
+
+bool CAddonButtonMap::GetAxisProperties(unsigned int axisIndex, int& center, unsigned int& range)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ for (const auto& it : m_driverMap)
+ {
+ const CDriverPrimitive& primitive = it.first;
+
+ if (primitive.Type() != PRIMITIVE_TYPE::SEMIAXIS)
+ continue;
+
+ if (primitive.Index() != axisIndex)
+ continue;
+
+ center = primitive.Center();
+ range = primitive.Range();
+ return true;
+ }
+
+ return false;
+}
+
+void CAddonButtonMap::SaveButtonMap()
+{
+ if (auto addon = m_addon.lock())
+ addon->SaveButtonMap(m_device);
+}
+
+void CAddonButtonMap::RevertButtonMap()
+{
+ if (auto addon = m_addon.lock())
+ addon->RevertButtonMap(m_device);
+}
+
+CAddonButtonMap::DriverMap CAddonButtonMap::CreateLookupTable(const FeatureMap& features)
+{
+ using namespace JOYSTICK;
+
+ DriverMap driverMap;
+
+ for (const auto& it : features)
+ {
+ const kodi::addon::JoystickFeature& feature = it.second;
+
+ switch (feature.Type())
+ {
+ case JOYSTICK_FEATURE_TYPE_SCALAR:
+ case JOYSTICK_FEATURE_TYPE_KEY:
+ {
+ driverMap[CPeripheralAddonTranslator::TranslatePrimitive(
+ feature.Primitive(JOYSTICK_SCALAR_PRIMITIVE))] = it.first;
+ break;
+ }
+
+ case JOYSTICK_FEATURE_TYPE_ANALOG_STICK:
+ {
+ std::vector<JOYSTICK_FEATURE_PRIMITIVE> primitives = {
+ JOYSTICK_ANALOG_STICK_UP,
+ JOYSTICK_ANALOG_STICK_DOWN,
+ JOYSTICK_ANALOG_STICK_RIGHT,
+ JOYSTICK_ANALOG_STICK_LEFT,
+ };
+
+ for (auto primitive : primitives)
+ driverMap[CPeripheralAddonTranslator::TranslatePrimitive(feature.Primitive(primitive))] =
+ it.first;
+ break;
+ }
+
+ case JOYSTICK_FEATURE_TYPE_ACCELEROMETER:
+ {
+ std::vector<JOYSTICK_FEATURE_PRIMITIVE> primitives = {
+ JOYSTICK_ACCELEROMETER_POSITIVE_X,
+ JOYSTICK_ACCELEROMETER_POSITIVE_Y,
+ JOYSTICK_ACCELEROMETER_POSITIVE_Z,
+ };
+
+ for (auto primitive : primitives)
+ {
+ CDriverPrimitive translatedPrimitive =
+ CPeripheralAddonTranslator::TranslatePrimitive(feature.Primitive(primitive));
+ driverMap[translatedPrimitive] = it.first;
+
+ // Map opposite semiaxis
+ CDriverPrimitive oppositePrimitive = CDriverPrimitive(
+ translatedPrimitive.Index(), 0, translatedPrimitive.SemiAxisDirection() * -1, 1);
+ driverMap[oppositePrimitive] = it.first;
+ }
+ break;
+ }
+
+ case JOYSTICK_FEATURE_TYPE_WHEEL:
+ {
+ std::vector<JOYSTICK_FEATURE_PRIMITIVE> primitives = {
+ JOYSTICK_WHEEL_LEFT,
+ JOYSTICK_WHEEL_RIGHT,
+ };
+
+ for (auto primitive : primitives)
+ driverMap[CPeripheralAddonTranslator::TranslatePrimitive(feature.Primitive(primitive))] =
+ it.first;
+ break;
+ }
+
+ case JOYSTICK_FEATURE_TYPE_THROTTLE:
+ {
+ std::vector<JOYSTICK_FEATURE_PRIMITIVE> primitives = {
+ JOYSTICK_THROTTLE_UP,
+ JOYSTICK_THROTTLE_DOWN,
+ };
+
+ for (auto primitive : primitives)
+ driverMap[CPeripheralAddonTranslator::TranslatePrimitive(feature.Primitive(primitive))] =
+ it.first;
+ break;
+ }
+
+ case JOYSTICK_FEATURE_TYPE_RELPOINTER:
+ {
+ std::vector<JOYSTICK_FEATURE_PRIMITIVE> primitives = {
+ JOYSTICK_RELPOINTER_UP,
+ JOYSTICK_RELPOINTER_DOWN,
+ JOYSTICK_RELPOINTER_RIGHT,
+ JOYSTICK_RELPOINTER_LEFT,
+ };
+
+ for (auto primitive : primitives)
+ driverMap[CPeripheralAddonTranslator::TranslatePrimitive(feature.Primitive(primitive))] =
+ it.first;
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+
+ return driverMap;
+}
+
+JOYSTICK_FEATURE_PRIMITIVE CAddonButtonMap::GetAnalogStickIndex(
+ JOYSTICK::ANALOG_STICK_DIRECTION dir)
+{
+ using namespace JOYSTICK;
+
+ switch (dir)
+ {
+ case ANALOG_STICK_DIRECTION::UP:
+ return JOYSTICK_ANALOG_STICK_UP;
+ case ANALOG_STICK_DIRECTION::DOWN:
+ return JOYSTICK_ANALOG_STICK_DOWN;
+ case ANALOG_STICK_DIRECTION::RIGHT:
+ return JOYSTICK_ANALOG_STICK_RIGHT;
+ case ANALOG_STICK_DIRECTION::LEFT:
+ return JOYSTICK_ANALOG_STICK_LEFT;
+ default:
+ break;
+ }
+
+ return static_cast<JOYSTICK_FEATURE_PRIMITIVE>(0);
+}
+
+JOYSTICK_FEATURE_PRIMITIVE CAddonButtonMap::GetRelativePointerIndex(
+ JOYSTICK::RELATIVE_POINTER_DIRECTION dir)
+{
+ using namespace JOYSTICK;
+
+ switch (dir)
+ {
+ case RELATIVE_POINTER_DIRECTION::UP:
+ return JOYSTICK_RELPOINTER_UP;
+ case RELATIVE_POINTER_DIRECTION::DOWN:
+ return JOYSTICK_RELPOINTER_DOWN;
+ case RELATIVE_POINTER_DIRECTION::RIGHT:
+ return JOYSTICK_RELPOINTER_RIGHT;
+ case RELATIVE_POINTER_DIRECTION::LEFT:
+ return JOYSTICK_RELPOINTER_LEFT;
+ default:
+ break;
+ }
+
+ return static_cast<JOYSTICK_FEATURE_PRIMITIVE>(0);
+}
+
+JOYSTICK_FEATURE_PRIMITIVE CAddonButtonMap::GetPrimitiveIndex(JOYSTICK::WHEEL_DIRECTION dir)
+{
+ using namespace JOYSTICK;
+
+ switch (dir)
+ {
+ case WHEEL_DIRECTION::RIGHT:
+ return JOYSTICK_WHEEL_RIGHT;
+ case WHEEL_DIRECTION::LEFT:
+ return JOYSTICK_WHEEL_LEFT;
+ default:
+ break;
+ }
+
+ return static_cast<JOYSTICK_FEATURE_PRIMITIVE>(0);
+}
+
+JOYSTICK_FEATURE_PRIMITIVE CAddonButtonMap::GetPrimitiveIndex(JOYSTICK::THROTTLE_DIRECTION dir)
+{
+ using namespace JOYSTICK;
+
+ switch (dir)
+ {
+ case THROTTLE_DIRECTION::UP:
+ return JOYSTICK_THROTTLE_UP;
+ case THROTTLE_DIRECTION::DOWN:
+ return JOYSTICK_THROTTLE_DOWN;
+ default:
+ break;
+ }
+
+ return static_cast<JOYSTICK_FEATURE_PRIMITIVE>(0);
+}
diff --git a/xbmc/peripherals/addons/AddonButtonMap.h b/xbmc/peripherals/addons/AddonButtonMap.h
new file mode 100644
index 0000000..cade54b
--- /dev/null
+++ b/xbmc/peripherals/addons/AddonButtonMap.h
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2014-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 "PeripheralAddon.h" // for FeatureMap
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "peripherals/PeripheralTypes.h"
+#include "threads/CriticalSection.h"
+
+namespace PERIPHERALS
+{
+class CPeripheral;
+class CPeripherals;
+
+class CAddonButtonMap : public KODI::JOYSTICK::IButtonMap
+{
+public:
+ CAddonButtonMap(CPeripheral* device,
+ const std::weak_ptr<CPeripheralAddon>& addon,
+ const std::string& strControllerId,
+ CPeripherals& manager);
+
+ ~CAddonButtonMap(void) override;
+
+ // Implementation of IButtonMap
+ std::string ControllerID(void) const override { return m_strControllerId; }
+
+ std::string Location(void) const override;
+
+ bool Load(void) override;
+
+ void Reset(void) override;
+
+ bool IsEmpty(void) const override;
+
+ std::string GetAppearance() const override;
+
+ bool SetAppearance(const std::string& controllerId) const override;
+
+ bool GetFeature(const KODI::JOYSTICK::CDriverPrimitive& primitive,
+ KODI::JOYSTICK::FeatureName& feature) override;
+
+ KODI::JOYSTICK::FEATURE_TYPE GetFeatureType(const KODI::JOYSTICK::FeatureName& feature) override;
+
+ bool GetScalar(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ void AddScalar(const KODI::JOYSTICK::FeatureName& feature,
+ const KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ bool GetAnalogStick(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::ANALOG_STICK_DIRECTION direction,
+ KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ void AddAnalogStick(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::ANALOG_STICK_DIRECTION direction,
+ const KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ bool GetRelativePointer(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::RELATIVE_POINTER_DIRECTION direction,
+ KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ void AddRelativePointer(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::RELATIVE_POINTER_DIRECTION direction,
+ const KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ bool GetAccelerometer(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::CDriverPrimitive& positiveX,
+ KODI::JOYSTICK::CDriverPrimitive& positiveY,
+ KODI::JOYSTICK::CDriverPrimitive& positiveZ) override;
+
+ void AddAccelerometer(const KODI::JOYSTICK::FeatureName& feature,
+ const KODI::JOYSTICK::CDriverPrimitive& positiveX,
+ const KODI::JOYSTICK::CDriverPrimitive& positiveY,
+ const KODI::JOYSTICK::CDriverPrimitive& positiveZ) override;
+
+ bool GetWheel(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::WHEEL_DIRECTION direction,
+ KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ void AddWheel(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::WHEEL_DIRECTION direction,
+ const KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ bool GetThrottle(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::THROTTLE_DIRECTION direction,
+ KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ void AddThrottle(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::THROTTLE_DIRECTION direction,
+ const KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ bool GetKey(const KODI::JOYSTICK::FeatureName& feature,
+ KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ void AddKey(const KODI::JOYSTICK::FeatureName& feature,
+ const KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ void SetIgnoredPrimitives(
+ const std::vector<KODI::JOYSTICK::CDriverPrimitive>& primitives) override;
+
+ bool IsIgnored(const KODI::JOYSTICK::CDriverPrimitive& primitive) override;
+
+ bool GetAxisProperties(unsigned int axisIndex, int& center, unsigned int& range) override;
+
+ void SaveButtonMap() override;
+
+ void RevertButtonMap() override;
+
+private:
+ typedef std::map<KODI::JOYSTICK::CDriverPrimitive, KODI::JOYSTICK::FeatureName> DriverMap;
+ typedef std::vector<KODI::JOYSTICK::CDriverPrimitive> JoystickPrimitiveVector;
+
+ // Utility functions
+ static DriverMap CreateLookupTable(const FeatureMap& features);
+
+ static JOYSTICK_FEATURE_PRIMITIVE GetAnalogStickIndex(KODI::JOYSTICK::ANALOG_STICK_DIRECTION dir);
+ static JOYSTICK_FEATURE_PRIMITIVE GetRelativePointerIndex(
+ KODI::JOYSTICK::RELATIVE_POINTER_DIRECTION dir);
+ static JOYSTICK_FEATURE_PRIMITIVE GetPrimitiveIndex(KODI::JOYSTICK::WHEEL_DIRECTION dir);
+ static JOYSTICK_FEATURE_PRIMITIVE GetPrimitiveIndex(KODI::JOYSTICK::THROTTLE_DIRECTION dir);
+
+ // Construction parameters
+ CPeripheral* const m_device;
+ const std::weak_ptr<CPeripheralAddon> m_addon;
+ const std::string m_strControllerId;
+ CPeripherals& m_manager;
+
+ // Button map state
+ std::string m_controllerAppearance;
+ FeatureMap m_features;
+ DriverMap m_driverMap;
+ JoystickPrimitiveVector m_ignoredPrimitives;
+
+ // Synchronization parameters
+ mutable CCriticalSection m_mutex;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/addons/AddonButtonMapping.cpp b/xbmc/peripherals/addons/AddonButtonMapping.cpp
new file mode 100644
index 0000000..abf981f
--- /dev/null
+++ b/xbmc/peripherals/addons/AddonButtonMapping.cpp
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2014-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 "AddonButtonMapping.h"
+
+#include "input/joysticks/generic/ButtonMapping.h"
+#include "input/joysticks/interfaces/IButtonMapper.h"
+#include "peripherals/Peripherals.h"
+#include "peripherals/addons/AddonButtonMap.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace JOYSTICK;
+using namespace PERIPHERALS;
+
+CAddonButtonMapping::CAddonButtonMapping(CPeripherals& manager,
+ CPeripheral* peripheral,
+ IButtonMapper* mapper)
+{
+ PeripheralAddonPtr addon = manager.GetAddonWithButtonMap(peripheral);
+
+ if (!addon)
+ {
+ CLog::Log(LOGDEBUG, "Failed to locate add-on for \"{}\"", peripheral->DeviceName());
+ }
+ else
+ {
+ const std::string controllerId = mapper->ControllerID();
+ m_buttonMap = std::make_unique<CAddonButtonMap>(peripheral, addon, controllerId, manager);
+ if (m_buttonMap->Load())
+ {
+ IKeymap* keymap = peripheral->GetKeymap(controllerId);
+ m_buttonMapping.reset(new CButtonMapping(mapper, m_buttonMap.get(), keymap));
+
+ // Allow the mapper to save our button map
+ mapper->SetButtonMapCallback(peripheral->Location(), this);
+ }
+ else
+ m_buttonMap.reset();
+ }
+}
+
+CAddonButtonMapping::~CAddonButtonMapping(void)
+{
+ m_buttonMapping.reset();
+ m_buttonMap.reset();
+}
+
+bool CAddonButtonMapping::OnButtonMotion(unsigned int buttonIndex, bool bPressed)
+{
+ if (m_buttonMapping)
+ return m_buttonMapping->OnButtonMotion(buttonIndex, bPressed);
+
+ return false;
+}
+
+bool CAddonButtonMapping::OnHatMotion(unsigned int hatIndex, HAT_STATE state)
+{
+ if (m_buttonMapping)
+ return m_buttonMapping->OnHatMotion(hatIndex, state);
+
+ return false;
+}
+
+bool CAddonButtonMapping::OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range)
+{
+ if (m_buttonMapping)
+ return m_buttonMapping->OnAxisMotion(axisIndex, position, center, range);
+
+ return false;
+}
+
+void CAddonButtonMapping::OnInputFrame(void)
+{
+ if (m_buttonMapping)
+ m_buttonMapping->OnInputFrame();
+}
+
+bool CAddonButtonMapping::OnKeyPress(const CKey& key)
+{
+ if (m_buttonMapping)
+ return m_buttonMapping->OnKeyPress(key);
+
+ return false;
+}
+
+void CAddonButtonMapping::OnKeyRelease(const CKey& key)
+{
+ if (m_buttonMapping)
+ m_buttonMapping->OnKeyRelease(key);
+}
+
+bool CAddonButtonMapping::OnPosition(int x, int y)
+{
+ if (m_buttonMapping)
+ return m_buttonMapping->OnPosition(x, y);
+
+ return false;
+}
+
+bool CAddonButtonMapping::OnButtonPress(MOUSE::BUTTON_ID button)
+{
+ if (m_buttonMapping)
+ return m_buttonMapping->OnButtonPress(button);
+
+ return false;
+}
+
+void CAddonButtonMapping::OnButtonRelease(MOUSE::BUTTON_ID button)
+{
+ if (m_buttonMapping)
+ m_buttonMapping->OnButtonRelease(button);
+}
+
+void CAddonButtonMapping::SaveButtonMap()
+{
+ if (m_buttonMapping)
+ m_buttonMapping->SaveButtonMap();
+}
+
+void CAddonButtonMapping::ResetIgnoredPrimitives()
+{
+ if (m_buttonMapping)
+ m_buttonMapping->ResetIgnoredPrimitives();
+}
+
+void CAddonButtonMapping::RevertButtonMap()
+{
+ if (m_buttonMapping)
+ m_buttonMapping->RevertButtonMap();
+}
diff --git a/xbmc/peripherals/addons/AddonButtonMapping.h b/xbmc/peripherals/addons/AddonButtonMapping.h
new file mode 100644
index 0000000..245a588
--- /dev/null
+++ b/xbmc/peripherals/addons/AddonButtonMapping.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2014-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 "input/joysticks/interfaces/IButtonMapCallback.h"
+#include "input/joysticks/interfaces/IDriverHandler.h"
+#include "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+#include "input/mouse/interfaces/IMouseDriverHandler.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CButtonMapping;
+class IButtonMap;
+class IButtonMapper;
+} // namespace JOYSTICK
+} // namespace KODI
+
+namespace PERIPHERALS
+{
+class CPeripheral;
+class CPeripherals;
+
+class CAddonButtonMapping : public KODI::JOYSTICK::IDriverHandler,
+ public KODI::KEYBOARD::IKeyboardDriverHandler,
+ public KODI::MOUSE::IMouseDriverHandler,
+ public KODI::JOYSTICK::IButtonMapCallback
+{
+public:
+ CAddonButtonMapping(CPeripherals& manager,
+ CPeripheral* peripheral,
+ KODI::JOYSTICK::IButtonMapper* mapper);
+
+ ~CAddonButtonMapping(void) override;
+
+ // implementation of IDriverHandler
+ bool OnButtonMotion(unsigned int buttonIndex, bool bPressed) override;
+ bool OnHatMotion(unsigned int hatIndex, KODI::JOYSTICK::HAT_STATE state) override;
+ bool OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range) override;
+ void OnInputFrame(void) override;
+
+ // implementation of IKeyboardDriverHandler
+ bool OnKeyPress(const CKey& key) override;
+ void OnKeyRelease(const CKey& key) override;
+
+ // implementation of IMouseDriverHandler
+ bool OnPosition(int x, int y) override;
+ bool OnButtonPress(KODI::MOUSE::BUTTON_ID button) override;
+ void OnButtonRelease(KODI::MOUSE::BUTTON_ID button) override;
+
+ // implementation of IButtonMapCallback
+ void SaveButtonMap() override;
+ void ResetIgnoredPrimitives() override;
+ void RevertButtonMap() override;
+
+private:
+ std::unique_ptr<KODI::JOYSTICK::CButtonMapping> m_buttonMapping;
+ std::unique_ptr<KODI::JOYSTICK::IButtonMap> m_buttonMap;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/addons/AddonInputHandling.cpp b/xbmc/peripherals/addons/AddonInputHandling.cpp
new file mode 100644
index 0000000..7490b96
--- /dev/null
+++ b/xbmc/peripherals/addons/AddonInputHandling.cpp
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2014-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 "AddonInputHandling.h"
+
+#include "input/joysticks/generic/DriverReceiving.h"
+#include "input/joysticks/generic/InputHandling.h"
+#include "input/joysticks/interfaces/IDriverReceiver.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+#include "input/keyboard/generic/KeyboardInputHandling.h"
+#include "input/keyboard/interfaces/IKeyboardInputHandler.h"
+#include "input/mouse/generic/MouseInputHandling.h"
+#include "input/mouse/interfaces/IMouseInputHandler.h"
+#include "peripherals/addons/AddonButtonMap.h"
+#include "peripherals/devices/Peripheral.h"
+#include "utils/log.h"
+
+using namespace KODI;
+using namespace JOYSTICK;
+using namespace PERIPHERALS;
+
+CAddonInputHandling::CAddonInputHandling(CPeripherals& manager,
+ CPeripheral* peripheral,
+ std::shared_ptr<CPeripheralAddon> addon,
+ IInputHandler* handler,
+ IDriverReceiver* receiver)
+ : m_manager(manager),
+ m_peripheral(peripheral),
+ m_addon(std::move(addon)),
+ m_joystickInputHandler(handler),
+ m_joystickDriverReceiver(receiver)
+{
+}
+
+CAddonInputHandling::CAddonInputHandling(CPeripherals& manager,
+ CPeripheral* peripheral,
+ std::shared_ptr<CPeripheralAddon> addon,
+ KEYBOARD::IKeyboardInputHandler* handler)
+ : m_manager(manager),
+ m_peripheral(peripheral),
+ m_addon(std::move(addon)),
+ m_keyboardInputHandler(handler)
+{
+}
+
+CAddonInputHandling::CAddonInputHandling(CPeripherals& manager,
+ CPeripheral* peripheral,
+ std::shared_ptr<CPeripheralAddon> addon,
+ MOUSE::IMouseInputHandler* handler)
+ : m_manager(manager),
+ m_peripheral(peripheral),
+ m_addon(std::move(addon)),
+ m_mouseInputHandler(handler)
+{
+}
+
+CAddonInputHandling::~CAddonInputHandling(void)
+{
+ m_joystickDriverHandler.reset();
+ m_joystickInputReceiver.reset();
+ m_keyboardDriverHandler.reset();
+ m_mouseDriverHandler.reset();
+ m_buttonMap.reset();
+}
+
+bool CAddonInputHandling::Load()
+{
+ std::string controllerId;
+ if (m_joystickInputHandler != nullptr)
+ controllerId = m_joystickInputHandler->ControllerID();
+ else if (m_keyboardInputHandler != nullptr)
+ controllerId = m_keyboardInputHandler->ControllerID();
+ else if (m_mouseInputHandler != nullptr)
+ controllerId = m_mouseInputHandler->ControllerID();
+
+ if (!controllerId.empty())
+ m_buttonMap = std::make_unique<CAddonButtonMap>(m_peripheral, m_addon, controllerId, m_manager);
+
+ if (m_buttonMap && m_buttonMap->Load())
+ {
+ if (m_joystickInputHandler != nullptr)
+ {
+ m_joystickDriverHandler =
+ std::make_unique<CInputHandling>(m_joystickInputHandler, m_buttonMap.get());
+ if (m_joystickDriverReceiver != nullptr)
+ {
+ m_joystickInputReceiver =
+ std::make_unique<CDriverReceiving>(m_joystickDriverReceiver, m_buttonMap.get());
+
+ // Interfaces are connected here because they share button map as a common resource
+ m_joystickInputHandler->SetInputReceiver(m_joystickInputReceiver.get());
+ }
+ return true;
+ }
+ else if (m_keyboardInputHandler != nullptr)
+ {
+ m_keyboardDriverHandler = std::make_unique<KEYBOARD::CKeyboardInputHandling>(
+ m_keyboardInputHandler, m_buttonMap.get());
+ return true;
+ }
+ else if (m_mouseInputHandler != nullptr)
+ {
+ m_mouseDriverHandler =
+ std::make_unique<MOUSE::CMouseInputHandling>(m_mouseInputHandler, m_buttonMap.get());
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CAddonInputHandling::OnButtonMotion(unsigned int buttonIndex, bool bPressed)
+{
+ if (m_joystickDriverHandler)
+ return m_joystickDriverHandler->OnButtonMotion(buttonIndex, bPressed);
+
+ return false;
+}
+
+bool CAddonInputHandling::OnHatMotion(unsigned int hatIndex, HAT_STATE state)
+{
+ if (m_joystickDriverHandler)
+ return m_joystickDriverHandler->OnHatMotion(hatIndex, state);
+
+ return false;
+}
+
+bool CAddonInputHandling::OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range)
+{
+ if (m_joystickDriverHandler)
+ return m_joystickDriverHandler->OnAxisMotion(axisIndex, position, center, range);
+
+ return false;
+}
+
+void CAddonInputHandling::OnInputFrame(void)
+{
+ if (m_joystickDriverHandler)
+ m_joystickDriverHandler->OnInputFrame();
+}
+
+bool CAddonInputHandling::OnKeyPress(const CKey& key)
+{
+ if (m_keyboardDriverHandler)
+ return m_keyboardDriverHandler->OnKeyPress(key);
+
+ return false;
+}
+
+void CAddonInputHandling::OnKeyRelease(const CKey& key)
+{
+ if (m_keyboardDriverHandler)
+ m_keyboardDriverHandler->OnKeyRelease(key);
+}
+
+bool CAddonInputHandling::OnPosition(int x, int y)
+{
+ if (m_mouseDriverHandler)
+ return m_mouseDriverHandler->OnPosition(x, y);
+
+ return false;
+}
+
+bool CAddonInputHandling::OnButtonPress(MOUSE::BUTTON_ID button)
+{
+ if (m_mouseDriverHandler)
+ return m_mouseDriverHandler->OnButtonPress(button);
+
+ return false;
+}
+
+void CAddonInputHandling::OnButtonRelease(MOUSE::BUTTON_ID button)
+{
+ if (m_mouseDriverHandler)
+ m_mouseDriverHandler->OnButtonRelease(button);
+}
+
+bool CAddonInputHandling::SetRumbleState(const JOYSTICK::FeatureName& feature, float magnitude)
+{
+ if (m_joystickInputReceiver)
+ return m_joystickInputReceiver->SetRumbleState(feature, magnitude);
+
+ return false;
+}
diff --git a/xbmc/peripherals/addons/AddonInputHandling.h b/xbmc/peripherals/addons/AddonInputHandling.h
new file mode 100644
index 0000000..295e6d4
--- /dev/null
+++ b/xbmc/peripherals/addons/AddonInputHandling.h
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2014-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 "input/joysticks/interfaces/IDriverHandler.h"
+#include "input/joysticks/interfaces/IInputReceiver.h"
+#include "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+#include "input/mouse/interfaces/IMouseDriverHandler.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IButtonMap;
+class IDriverReceiver;
+class IInputHandler;
+} // namespace JOYSTICK
+
+namespace KEYBOARD
+{
+class IKeyboardInputHandler;
+}
+
+namespace MOUSE
+{
+class IMouseInputHandler;
+}
+} // namespace KODI
+
+namespace PERIPHERALS
+{
+class CPeripheral;
+class CPeripherals;
+class CPeripheralAddon;
+
+class CAddonInputHandling : public KODI::JOYSTICK::IDriverHandler,
+ public KODI::JOYSTICK::IInputReceiver,
+ public KODI::KEYBOARD::IKeyboardDriverHandler,
+ public KODI::MOUSE::IMouseDriverHandler
+{
+public:
+ CAddonInputHandling(CPeripherals& manager,
+ CPeripheral* peripheral,
+ std::shared_ptr<CPeripheralAddon> addon,
+ KODI::JOYSTICK::IInputHandler* handler,
+ KODI::JOYSTICK::IDriverReceiver* receiver);
+
+ CAddonInputHandling(CPeripherals& manager,
+ CPeripheral* peripheral,
+ std::shared_ptr<CPeripheralAddon> addon,
+ KODI::KEYBOARD::IKeyboardInputHandler* handler);
+
+ CAddonInputHandling(CPeripherals& manager,
+ CPeripheral* peripheral,
+ std::shared_ptr<CPeripheralAddon> addon,
+ KODI::MOUSE::IMouseInputHandler* handler);
+
+ ~CAddonInputHandling(void) override;
+
+ bool Load();
+
+ // implementation of IDriverHandler
+ bool OnButtonMotion(unsigned int buttonIndex, bool bPressed) override;
+ bool OnHatMotion(unsigned int hatIndex, KODI::JOYSTICK::HAT_STATE state) override;
+ bool OnAxisMotion(unsigned int axisIndex,
+ float position,
+ int center,
+ unsigned int range) override;
+ void OnInputFrame(void) override;
+
+ // implementation of IKeyboardDriverHandler
+ bool OnKeyPress(const CKey& key) override;
+ void OnKeyRelease(const CKey& key) override;
+
+ // implementation of IMouseDriverHandler
+ bool OnPosition(int x, int y) override;
+ bool OnButtonPress(KODI::MOUSE::BUTTON_ID button) override;
+ void OnButtonRelease(KODI::MOUSE::BUTTON_ID button) override;
+
+ // implementation of IInputReceiver
+ bool SetRumbleState(const KODI::JOYSTICK::FeatureName& feature, float magnitude) override;
+
+private:
+ // Construction parameters
+ CPeripherals& m_manager;
+ CPeripheral* const m_peripheral;
+ const std::shared_ptr<CPeripheralAddon> m_addon;
+ KODI::JOYSTICK::IInputHandler* const m_joystickInputHandler{nullptr};
+ KODI::JOYSTICK::IDriverReceiver* const m_joystickDriverReceiver{nullptr};
+ KODI::KEYBOARD::IKeyboardInputHandler* m_keyboardInputHandler{nullptr};
+ KODI::MOUSE::IMouseInputHandler* const m_mouseInputHandler{nullptr};
+
+ // Input parameters
+ std::unique_ptr<KODI::JOYSTICK::IDriverHandler> m_joystickDriverHandler;
+ std::unique_ptr<KODI::JOYSTICK::IInputReceiver> m_joystickInputReceiver;
+ std::unique_ptr<KODI::KEYBOARD::IKeyboardDriverHandler> m_keyboardDriverHandler;
+ std::unique_ptr<KODI::MOUSE::IMouseDriverHandler> m_mouseDriverHandler;
+ std::unique_ptr<KODI::JOYSTICK::IButtonMap> m_buttonMap;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/addons/CMakeLists.txt b/xbmc/peripherals/addons/CMakeLists.txt
new file mode 100644
index 0000000..9f31978
--- /dev/null
+++ b/xbmc/peripherals/addons/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES AddonButtonMap.cpp
+ AddonButtonMapping.cpp
+ AddonInputHandling.cpp
+ PeripheralAddon.cpp
+ PeripheralAddonTranslator.cpp)
+
+set(HEADERS AddonButtonMap.h
+ AddonButtonMapping.h
+ AddonInputHandling.h
+ PeripheralAddon.h
+ PeripheralAddonTranslator.h)
+
+core_add_library(peripherals_addons)
diff --git a/xbmc/peripherals/addons/PeripheralAddon.cpp b/xbmc/peripherals/addons/PeripheralAddon.cpp
new file mode 100644
index 0000000..7a55bb3
--- /dev/null
+++ b/xbmc/peripherals/addons/PeripheralAddon.cpp
@@ -0,0 +1,990 @@
+/*
+ * Copyright (C) 2014-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 "PeripheralAddon.h"
+
+#include "FileItem.h"
+#include "PeripheralAddonTranslator.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerManager.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/interfaces/IButtonMap.h"
+#include "peripherals/Peripherals.h"
+#include "peripherals/bus/virtual/PeripheralBusAddon.h"
+#include "peripherals/devices/PeripheralJoystick.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+#include <shared_mutex>
+#include <string.h>
+#include <utility>
+
+using namespace KODI;
+using namespace JOYSTICK;
+using namespace PERIPHERALS;
+using namespace XFILE;
+
+#define KEYBOARD_BUTTON_MAP_NAME "Keyboard"
+#define KEYBOARD_PROVIDER "application"
+
+#define MOUSE_BUTTON_MAP_NAME "Mouse"
+#define MOUSE_PROVIDER "application"
+
+#ifndef SAFE_DELETE
+#define SAFE_DELETE(p) \
+ do \
+ { \
+ delete (p); \
+ (p) = NULL; \
+ } while (0)
+#endif
+
+CPeripheralAddon::CPeripheralAddon(const ADDON::AddonInfoPtr& addonInfo, CPeripherals& manager)
+ : IAddonInstanceHandler(ADDON_INSTANCE_PERIPHERAL, addonInfo),
+ m_manager(manager),
+ m_bSupportsJoystickRumble(false),
+ m_bSupportsJoystickPowerOff(false)
+{
+ m_bProvidesJoysticks =
+ addonInfo->Type(ADDON::AddonType::PERIPHERALDLL)->GetValue("@provides_joysticks").asBoolean();
+ m_bProvidesButtonMaps = addonInfo->Type(ADDON::AddonType::PERIPHERALDLL)
+ ->GetValue("@provides_buttonmaps")
+ .asBoolean();
+
+ // Create "C" interface structures, used as own parts to prevent API problems on update
+ m_ifc.peripheral = new AddonInstance_Peripheral;
+ m_ifc.peripheral->props = new AddonProps_Peripheral();
+ m_ifc.peripheral->toAddon = new KodiToAddonFuncTable_Peripheral();
+ m_ifc.peripheral->toKodi = new AddonToKodiFuncTable_Peripheral();
+
+ ResetProperties();
+}
+
+CPeripheralAddon::~CPeripheralAddon(void)
+{
+ DestroyAddon();
+
+ if (m_ifc.peripheral)
+ {
+ delete m_ifc.peripheral->toAddon;
+ delete m_ifc.peripheral->toKodi;
+ delete m_ifc.peripheral->props;
+ }
+ delete m_ifc.peripheral;
+}
+
+void CPeripheralAddon::ResetProperties(void)
+{
+ // Initialise members
+ m_strUserPath = CSpecialProtocol::TranslatePath(Profile());
+ m_strClientPath = CSpecialProtocol::TranslatePath(Path());
+
+ m_ifc.peripheral->props->user_path = m_strUserPath.c_str();
+ m_ifc.peripheral->props->addon_path = m_strClientPath.c_str();
+
+ m_ifc.peripheral->toKodi->kodiInstance = this;
+ m_ifc.peripheral->toKodi->feature_count = cb_feature_count;
+ m_ifc.peripheral->toKodi->feature_type = cb_feature_type;
+ m_ifc.peripheral->toKodi->refresh_button_maps = cb_refresh_button_maps;
+ m_ifc.peripheral->toKodi->trigger_scan = cb_trigger_scan;
+
+ memset(m_ifc.peripheral->toAddon, 0, sizeof(KodiToAddonFuncTable_Peripheral));
+}
+
+bool CPeripheralAddon::CreateAddon(void)
+{
+ std::unique_lock<CSharedSection> lock(m_dllSection);
+
+ // Reset all properties to defaults
+ ResetProperties();
+
+ // Create directory for user data
+ if (!CDirectory::Exists(m_strUserPath))
+ CDirectory::Create(m_strUserPath);
+
+ // Initialise the add-on
+ CLog::Log(LOGDEBUG, "PERIPHERAL - {} - creating peripheral add-on instance '{}'", __FUNCTION__,
+ Name());
+
+ if (CreateInstance() != ADDON_STATUS_OK)
+ return false;
+
+ if (!GetAddonProperties())
+ {
+ DestroyInstance();
+ return false;
+ }
+
+ return true;
+}
+
+void CPeripheralAddon::DestroyAddon()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_peripherals.clear();
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_buttonMapMutex);
+ // only clear buttonMaps but don't delete them as they are owned by a
+ // CAddonJoystickInputHandling instance
+ m_buttonMaps.clear();
+ }
+
+ {
+ std::unique_lock<CSharedSection> lock(m_dllSection);
+ DestroyInstance();
+ }
+}
+
+bool CPeripheralAddon::GetAddonProperties(void)
+{
+ PERIPHERAL_CAPABILITIES addonCapabilities = {};
+
+ // Get the capabilities
+ m_ifc.peripheral->toAddon->get_capabilities(m_ifc.peripheral, &addonCapabilities);
+
+ // Verify capabilities against addon.xml
+ if (m_bProvidesJoysticks != addonCapabilities.provides_joysticks)
+ {
+ CLog::Log(
+ LOGERROR,
+ "PERIPHERAL - Add-on '{}': provides_joysticks'({}) in add-on DLL doesn't match "
+ "'provides_joysticks'({}) in addon.xml. Please contact the developer of this add-on: {}",
+ Name(), addonCapabilities.provides_joysticks ? "true" : "false",
+ m_bProvidesJoysticks ? "true" : "false", Author());
+ return false;
+ }
+ if (m_bProvidesButtonMaps != addonCapabilities.provides_buttonmaps)
+ {
+ CLog::Log(
+ LOGERROR,
+ "PERIPHERAL - Add-on '{}': provides_buttonmaps' ({}) in add-on DLL doesn't match "
+ "'provides_buttonmaps' ({}) in addon.xml. Please contact the developer of this add-on: {}",
+ Name(), addonCapabilities.provides_buttonmaps ? "true" : "false",
+ m_bProvidesButtonMaps ? "true" : "false", Author());
+ return false;
+ }
+
+ // Read properties that depend on underlying driver
+ m_bSupportsJoystickRumble = addonCapabilities.provides_joystick_rumble;
+ m_bSupportsJoystickPowerOff = addonCapabilities.provides_joystick_power_off;
+
+ return true;
+}
+
+bool CPeripheralAddon::Register(unsigned int peripheralIndex, const PeripheralPtr& peripheral)
+{
+ if (!peripheral)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_peripherals.find(peripheralIndex) == m_peripherals.end())
+ {
+ if (peripheral->Type() == PERIPHERAL_JOYSTICK)
+ {
+ m_peripherals[peripheralIndex] = std::static_pointer_cast<CPeripheralJoystick>(peripheral);
+
+ CLog::Log(LOGINFO, "{} - new {} device registered on {}->{}: {}", __FUNCTION__,
+ PeripheralTypeTranslator::TypeToString(peripheral->Type()),
+ PeripheralTypeTranslator::BusTypeToString(PERIPHERAL_BUS_ADDON),
+ peripheral->Location(), peripheral->DeviceName());
+
+ return true;
+ }
+ }
+ return false;
+}
+
+void CPeripheralAddon::UnregisterRemovedDevices(const PeripheralScanResults& results,
+ PeripheralVector& removedPeripherals)
+{
+ std::vector<unsigned int> removedIndexes;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto& it : m_peripherals)
+ {
+ const PeripheralPtr& peripheral = it.second;
+ PeripheralScanResult updatedDevice(PERIPHERAL_BUS_ADDON);
+ if (!results.GetDeviceOnLocation(peripheral->Location(), &updatedDevice) ||
+ *peripheral != updatedDevice)
+ {
+ // Device removed
+ removedIndexes.push_back(it.first);
+ }
+ }
+ }
+
+ for (auto index : removedIndexes)
+ {
+ auto it = m_peripherals.find(index);
+ const PeripheralPtr& peripheral = it->second;
+ CLog::Log(LOGINFO, "{} - device removed from {}/{}: {} ({}:{})", __FUNCTION__,
+ PeripheralTypeTranslator::TypeToString(peripheral->Type()), peripheral->Location(),
+ peripheral->DeviceName(), peripheral->VendorIdAsString(),
+ peripheral->ProductIdAsString());
+ UnregisterButtonMap(peripheral.get());
+ peripheral->OnDeviceRemoved();
+ removedPeripherals.push_back(peripheral);
+ m_peripherals.erase(it);
+ }
+}
+
+bool CPeripheralAddon::HasFeature(const PeripheralFeature feature) const
+{
+ if (feature == FEATURE_JOYSTICK)
+ return m_bProvidesJoysticks;
+
+ return false;
+}
+
+void CPeripheralAddon::GetFeatures(std::vector<PeripheralFeature>& features) const
+{
+ if (m_bProvidesJoysticks &&
+ std::find(features.begin(), features.end(), FEATURE_JOYSTICK) == features.end())
+ features.push_back(FEATURE_JOYSTICK);
+}
+
+PeripheralPtr CPeripheralAddon::GetPeripheral(unsigned int index) const
+{
+ PeripheralPtr peripheral;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto it = m_peripherals.find(index);
+ if (it != m_peripherals.end())
+ peripheral = it->second;
+ return peripheral;
+}
+
+PeripheralPtr CPeripheralAddon::GetByPath(const std::string& strPath) const
+{
+ PeripheralPtr result;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& it : m_peripherals)
+ {
+ if (StringUtils::EqualsNoCase(strPath, it.second->FileLocation()))
+ {
+ result = it.second;
+ break;
+ }
+ }
+
+ return result;
+}
+
+bool CPeripheralAddon::SupportsFeature(PeripheralFeature feature) const
+{
+ switch (feature)
+ {
+ case FEATURE_RUMBLE:
+ return m_bSupportsJoystickRumble;
+ case FEATURE_POWER_OFF:
+ return m_bSupportsJoystickPowerOff;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+unsigned int CPeripheralAddon::GetPeripheralsWithFeature(PeripheralVector& results,
+ const PeripheralFeature feature) const
+{
+ unsigned int iReturn = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& it : m_peripherals)
+ {
+ if (it.second->HasFeature(feature))
+ {
+ results.push_back(it.second);
+ ++iReturn;
+ }
+ }
+ return iReturn;
+}
+
+unsigned int CPeripheralAddon::GetNumberOfPeripherals(void) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return static_cast<unsigned int>(m_peripherals.size());
+}
+
+unsigned int CPeripheralAddon::GetNumberOfPeripheralsWithId(const int iVendorId,
+ const int iProductId) const
+{
+ unsigned int iReturn = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& it : m_peripherals)
+ {
+ if (it.second->VendorId() == iVendorId && it.second->ProductId() == iProductId)
+ iReturn++;
+ }
+
+ return iReturn;
+}
+
+void CPeripheralAddon::GetDirectory(const std::string& strPath, CFileItemList& items) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& it : m_peripherals)
+ {
+ const PeripheralPtr& peripheral = it.second;
+ if (peripheral->IsHidden())
+ continue;
+
+ CFileItemPtr peripheralFile(new CFileItem(peripheral->DeviceName()));
+ peripheralFile->SetPath(peripheral->FileLocation());
+ peripheralFile->SetProperty("vendor", peripheral->VendorIdAsString());
+ peripheralFile->SetProperty("product", peripheral->ProductIdAsString());
+ peripheralFile->SetProperty(
+ "bus", PeripheralTypeTranslator::BusTypeToString(peripheral->GetBusType()));
+ peripheralFile->SetProperty("location", peripheral->Location());
+ peripheralFile->SetProperty("class",
+ PeripheralTypeTranslator::TypeToString(peripheral->Type()));
+ peripheralFile->SetProperty("version", peripheral->GetVersionInfo());
+ peripheralFile->SetArt("icon", peripheral->GetIcon());
+ items.Add(peripheralFile);
+ }
+}
+
+bool CPeripheralAddon::PerformDeviceScan(PeripheralScanResults& results)
+{
+ unsigned int peripheralCount;
+ PERIPHERAL_INFO* pScanResults;
+ PERIPHERAL_ERROR retVal;
+
+ std::shared_lock<CSharedSection> lock(m_dllSection);
+
+ if (!m_ifc.peripheral->toAddon->perform_device_scan)
+ return false;
+
+ LogError(retVal = m_ifc.peripheral->toAddon->perform_device_scan(m_ifc.peripheral,
+ &peripheralCount, &pScanResults),
+ "PerformDeviceScan()");
+
+ if (retVal == PERIPHERAL_NO_ERROR)
+ {
+ for (unsigned int i = 0; i < peripheralCount; i++)
+ {
+ kodi::addon::Peripheral peripheral(pScanResults[i]);
+ PeripheralScanResult result(PERIPHERAL_BUS_ADDON);
+ switch (peripheral.Type())
+ {
+ case PERIPHERAL_TYPE_JOYSTICK:
+ result.m_type = PERIPHERAL_JOYSTICK;
+ break;
+ default:
+ continue;
+ }
+
+ result.m_strDeviceName = peripheral.Name();
+ result.m_strLocation = StringUtils::Format("{}/{}", ID(), peripheral.Index());
+ result.m_iVendorId = peripheral.VendorID();
+ result.m_iProductId = peripheral.ProductID();
+ result.m_mappedType = PERIPHERAL_JOYSTICK;
+ result.m_mappedBusType = PERIPHERAL_BUS_ADDON;
+ result.m_iSequence = 0;
+
+ if (!results.ContainsResult(result))
+ results.m_results.push_back(result);
+ }
+
+ m_ifc.peripheral->toAddon->free_scan_results(m_ifc.peripheral, peripheralCount, pScanResults);
+
+ return true;
+ }
+
+ return false;
+}
+
+bool CPeripheralAddon::ProcessEvents(void)
+{
+ if (!m_bProvidesJoysticks)
+ return false;
+
+ std::shared_lock<CSharedSection> lock(m_dllSection);
+
+ if (!m_ifc.peripheral->toAddon->get_events)
+ return false;
+
+ PERIPHERAL_ERROR retVal;
+
+ unsigned int eventCount = 0;
+ PERIPHERAL_EVENT* pEvents = nullptr;
+
+ LogError(retVal = m_ifc.peripheral->toAddon->get_events(m_ifc.peripheral, &eventCount, &pEvents),
+ "GetEvents()");
+ if (retVal == PERIPHERAL_NO_ERROR)
+ {
+ for (unsigned int i = 0; i < eventCount; i++)
+ {
+ kodi::addon::PeripheralEvent event(pEvents[i]);
+ PeripheralPtr device = GetPeripheral(event.PeripheralIndex());
+ if (!device)
+ continue;
+
+ switch (device->Type())
+ {
+ case PERIPHERAL_JOYSTICK:
+ {
+ std::shared_ptr<CPeripheralJoystick> joystickDevice =
+ std::static_pointer_cast<CPeripheralJoystick>(device);
+
+ switch (event.Type())
+ {
+ case PERIPHERAL_EVENT_TYPE_DRIVER_BUTTON:
+ {
+ const bool bPressed = (event.ButtonState() == JOYSTICK_STATE_BUTTON_PRESSED);
+ joystickDevice->OnButtonMotion(event.DriverIndex(), bPressed);
+ break;
+ }
+ case PERIPHERAL_EVENT_TYPE_DRIVER_HAT:
+ {
+ const HAT_STATE state =
+ CPeripheralAddonTranslator::TranslateHatState(event.HatState());
+ joystickDevice->OnHatMotion(event.DriverIndex(), state);
+ break;
+ }
+ case PERIPHERAL_EVENT_TYPE_DRIVER_AXIS:
+ {
+ joystickDevice->OnAxisMotion(event.DriverIndex(), event.AxisState());
+ break;
+ }
+ default:
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ for (const auto& it : m_peripherals)
+ {
+ if (it.second->Type() == PERIPHERAL_JOYSTICK)
+ std::static_pointer_cast<CPeripheralJoystick>(it.second)->OnInputFrame();
+ }
+
+ m_ifc.peripheral->toAddon->free_events(m_ifc.peripheral, eventCount, pEvents);
+
+ return true;
+ }
+
+ return false;
+}
+
+bool CPeripheralAddon::SendRumbleEvent(unsigned int peripheralIndex,
+ unsigned int driverIndex,
+ float magnitude)
+{
+ if (!m_bProvidesJoysticks)
+ return false;
+
+ std::shared_lock<CSharedSection> lock(m_dllSection);
+
+ if (!m_ifc.peripheral->toAddon->send_event)
+ return false;
+
+ PERIPHERAL_EVENT eventStruct = {};
+
+ eventStruct.peripheral_index = peripheralIndex;
+ eventStruct.type = PERIPHERAL_EVENT_TYPE_SET_MOTOR;
+ eventStruct.driver_index = driverIndex;
+ eventStruct.motor_state = magnitude;
+
+ return m_ifc.peripheral->toAddon->send_event(m_ifc.peripheral, &eventStruct);
+}
+
+bool CPeripheralAddon::GetJoystickProperties(unsigned int index, CPeripheralJoystick& joystick)
+{
+ if (!m_bProvidesJoysticks)
+ return false;
+
+ std::shared_lock<CSharedSection> lock(m_dllSection);
+
+ if (!m_ifc.peripheral->toAddon->get_joystick_info)
+ return false;
+
+ PERIPHERAL_ERROR retVal;
+
+ JOYSTICK_INFO joystickStruct;
+
+ LogError(retVal = m_ifc.peripheral->toAddon->get_joystick_info(m_ifc.peripheral, index,
+ &joystickStruct),
+ "GetJoystickInfo()");
+ if (retVal == PERIPHERAL_NO_ERROR)
+ {
+ kodi::addon::Joystick addonJoystick(joystickStruct);
+ SetJoystickInfo(joystick, addonJoystick);
+
+ m_ifc.peripheral->toAddon->free_joystick_info(m_ifc.peripheral, &joystickStruct);
+
+ return true;
+ }
+
+ return false;
+}
+
+bool CPeripheralAddon::GetFeatures(const CPeripheral* device,
+ const std::string& strControllerId,
+ FeatureMap& features)
+{
+ if (!m_bProvidesButtonMaps)
+ return false;
+
+ std::shared_lock<CSharedSection> lock(m_dllSection);
+
+ if (!m_ifc.peripheral->toAddon->get_features)
+ return false;
+
+ PERIPHERAL_ERROR retVal;
+
+ kodi::addon::Joystick joystickInfo;
+ GetJoystickInfo(device, joystickInfo);
+
+ JOYSTICK_INFO joystickStruct;
+ joystickInfo.ToStruct(joystickStruct);
+
+ unsigned int featureCount = 0;
+ JOYSTICK_FEATURE* pFeatures = nullptr;
+
+ LogError(retVal = m_ifc.peripheral->toAddon->get_features(m_ifc.peripheral, &joystickStruct,
+ strControllerId.c_str(), &featureCount,
+ &pFeatures),
+ "GetFeatures()");
+
+ kodi::addon::Joystick::FreeStruct(joystickStruct);
+
+ if (retVal == PERIPHERAL_NO_ERROR)
+ {
+ for (unsigned int i = 0; i < featureCount; i++)
+ {
+ kodi::addon::JoystickFeature feature(pFeatures[i]);
+ if (feature.Type() != JOYSTICK_FEATURE_TYPE_UNKNOWN)
+ features[feature.Name()] = feature;
+ }
+
+ m_ifc.peripheral->toAddon->free_features(m_ifc.peripheral, featureCount, pFeatures);
+
+ return true;
+ }
+
+ return false;
+}
+
+bool CPeripheralAddon::MapFeature(const CPeripheral* device,
+ const std::string& strControllerId,
+ const kodi::addon::JoystickFeature& feature)
+{
+ if (!m_bProvidesButtonMaps)
+ return false;
+
+ std::shared_lock<CSharedSection> lock(m_dllSection);
+
+ if (!m_ifc.peripheral->toAddon->map_features)
+ return false;
+
+ PERIPHERAL_ERROR retVal;
+
+ kodi::addon::Joystick joystickInfo;
+ GetJoystickInfo(device, joystickInfo);
+
+ JOYSTICK_INFO joystickStruct;
+ joystickInfo.ToStruct(joystickStruct);
+
+ JOYSTICK_FEATURE addonFeature;
+ feature.ToStruct(addonFeature);
+
+ LogError(retVal = m_ifc.peripheral->toAddon->map_features(
+ m_ifc.peripheral, &joystickStruct, strControllerId.c_str(), 1, &addonFeature),
+ "MapFeatures()");
+
+ kodi::addon::Joystick::FreeStruct(joystickStruct);
+ kodi::addon::JoystickFeature::FreeStruct(addonFeature);
+
+ return retVal == PERIPHERAL_NO_ERROR;
+}
+
+bool CPeripheralAddon::GetIgnoredPrimitives(const CPeripheral* device, PrimitiveVector& primitives)
+{
+ if (!m_bProvidesButtonMaps)
+ return false;
+
+ std::shared_lock<CSharedSection> lock(m_dllSection);
+
+ if (!m_ifc.peripheral->toAddon->get_ignored_primitives)
+ return false;
+
+ PERIPHERAL_ERROR retVal;
+
+ kodi::addon::Joystick joystickInfo;
+ GetJoystickInfo(device, joystickInfo);
+
+ JOYSTICK_INFO joystickStruct;
+ joystickInfo.ToStruct(joystickStruct);
+
+ unsigned int primitiveCount = 0;
+ JOYSTICK_DRIVER_PRIMITIVE* pPrimitives = nullptr;
+
+ LogError(retVal = m_ifc.peripheral->toAddon->get_ignored_primitives(
+ m_ifc.peripheral, &joystickStruct, &primitiveCount, &pPrimitives),
+ "GetIgnoredPrimitives()");
+
+ kodi::addon::Joystick::FreeStruct(joystickStruct);
+
+ if (retVal == PERIPHERAL_NO_ERROR)
+ {
+ for (unsigned int i = 0; i < primitiveCount; i++)
+ primitives.emplace_back(pPrimitives[i]);
+
+ m_ifc.peripheral->toAddon->free_primitives(m_ifc.peripheral, primitiveCount, pPrimitives);
+
+ return true;
+ }
+
+ return false;
+}
+
+bool CPeripheralAddon::SetIgnoredPrimitives(const CPeripheral* device,
+ const PrimitiveVector& primitives)
+{
+ if (!m_bProvidesButtonMaps)
+ return false;
+
+ std::shared_lock<CSharedSection> lock(m_dllSection);
+
+ if (!m_ifc.peripheral->toAddon->set_ignored_primitives)
+ return false;
+
+ PERIPHERAL_ERROR retVal;
+
+ kodi::addon::Joystick joystickInfo;
+ GetJoystickInfo(device, joystickInfo);
+
+ JOYSTICK_INFO joystickStruct;
+ joystickInfo.ToStruct(joystickStruct);
+
+ JOYSTICK_DRIVER_PRIMITIVE* addonPrimitives = nullptr;
+ kodi::addon::DriverPrimitives::ToStructs(primitives, &addonPrimitives);
+ const unsigned int primitiveCount = static_cast<unsigned int>(primitives.size());
+
+ LogError(retVal = m_ifc.peripheral->toAddon->set_ignored_primitives(
+ m_ifc.peripheral, &joystickStruct, primitiveCount, addonPrimitives),
+ "SetIgnoredPrimitives()");
+
+ kodi::addon::Joystick::FreeStruct(joystickStruct);
+ kodi::addon::DriverPrimitives::FreeStructs(primitiveCount, addonPrimitives);
+
+ return retVal == PERIPHERAL_NO_ERROR;
+}
+
+void CPeripheralAddon::SaveButtonMap(const CPeripheral* device)
+{
+ if (!m_bProvidesButtonMaps)
+ return;
+
+ std::shared_lock<CSharedSection> lock(m_dllSection);
+
+ if (!m_ifc.peripheral->toAddon->save_button_map)
+ return;
+
+ kodi::addon::Joystick joystickInfo;
+ GetJoystickInfo(device, joystickInfo);
+
+ JOYSTICK_INFO joystickStruct;
+ joystickInfo.ToStruct(joystickStruct);
+
+ m_ifc.peripheral->toAddon->save_button_map(m_ifc.peripheral, &joystickStruct);
+
+ kodi::addon::Joystick::FreeStruct(joystickStruct);
+
+ // Notify observing button maps
+ RefreshButtonMaps(device->DeviceName());
+}
+
+void CPeripheralAddon::RevertButtonMap(const CPeripheral* device)
+{
+ if (!m_bProvidesButtonMaps)
+ return;
+
+ std::shared_lock<CSharedSection> lock(m_dllSection);
+
+ if (!m_ifc.peripheral->toAddon->revert_button_map)
+ return;
+
+ kodi::addon::Joystick joystickInfo;
+ GetJoystickInfo(device, joystickInfo);
+
+ JOYSTICK_INFO joystickStruct;
+ joystickInfo.ToStruct(joystickStruct);
+
+ m_ifc.peripheral->toAddon->revert_button_map(m_ifc.peripheral, &joystickStruct);
+
+ kodi::addon::Joystick::FreeStruct(joystickStruct);
+}
+
+void CPeripheralAddon::ResetButtonMap(const CPeripheral* device, const std::string& strControllerId)
+{
+ if (!m_bProvidesButtonMaps)
+ return;
+
+ kodi::addon::Joystick joystickInfo;
+ GetJoystickInfo(device, joystickInfo);
+
+ JOYSTICK_INFO joystickStruct;
+ joystickInfo.ToStruct(joystickStruct);
+
+ m_ifc.peripheral->toAddon->reset_button_map(m_ifc.peripheral, &joystickStruct,
+ strControllerId.c_str());
+
+ kodi::addon::Joystick::FreeStruct(joystickStruct);
+
+ // Notify observing button maps
+ RefreshButtonMaps(device->DeviceName());
+}
+
+void CPeripheralAddon::PowerOffJoystick(unsigned int index)
+{
+ if (!HasFeature(FEATURE_JOYSTICK))
+ return;
+
+ if (!SupportsFeature(FEATURE_POWER_OFF))
+ return;
+
+ std::shared_lock<CSharedSection> lock(m_dllSection);
+
+ if (!m_ifc.peripheral->toAddon->power_off_joystick)
+ return;
+
+ m_ifc.peripheral->toAddon->power_off_joystick(m_ifc.peripheral, index);
+}
+
+void CPeripheralAddon::RegisterButtonMap(CPeripheral* device, IButtonMap* buttonMap)
+{
+ std::unique_lock<CCriticalSection> lock(m_buttonMapMutex);
+
+ UnregisterButtonMap(buttonMap);
+ m_buttonMaps.emplace_back(device, buttonMap);
+}
+
+void CPeripheralAddon::UnregisterButtonMap(IButtonMap* buttonMap)
+{
+ std::unique_lock<CCriticalSection> lock(m_buttonMapMutex);
+
+ for (auto it = m_buttonMaps.begin(); it != m_buttonMaps.end(); ++it)
+ {
+ if (it->second == buttonMap)
+ {
+ m_buttonMaps.erase(it);
+ break;
+ }
+ }
+}
+
+void CPeripheralAddon::UnregisterButtonMap(CPeripheral* device)
+{
+ std::unique_lock<CCriticalSection> lock(m_buttonMapMutex);
+
+ m_buttonMaps.erase(
+ std::remove_if(m_buttonMaps.begin(), m_buttonMaps.end(),
+ [device](const std::pair<CPeripheral*, JOYSTICK::IButtonMap*>& buttonMap) {
+ return buttonMap.first == device;
+ }),
+ m_buttonMaps.end());
+}
+
+void CPeripheralAddon::RefreshButtonMaps(const std::string& strDeviceName /* = "" */)
+{
+ std::unique_lock<CCriticalSection> lock(m_buttonMapMutex);
+
+ for (auto it = m_buttonMaps.begin(); it != m_buttonMaps.end(); ++it)
+ {
+ if (strDeviceName.empty() || strDeviceName == it->first->DeviceName())
+ it->second->Load();
+ }
+}
+
+bool CPeripheralAddon::ProvidesJoysticks(const ADDON::AddonInfoPtr& addonInfo)
+{
+ return addonInfo->Type(ADDON::AddonType::PERIPHERALDLL)
+ ->GetValue("@provides_joysticks")
+ .asBoolean();
+}
+
+bool CPeripheralAddon::ProvidesButtonMaps(const ADDON::AddonInfoPtr& addonInfo)
+{
+ return addonInfo->Type(ADDON::AddonType::PERIPHERALDLL)
+ ->GetValue("@provides_buttonmaps")
+ .asBoolean();
+}
+
+void CPeripheralAddon::TriggerDeviceScan()
+{
+ m_manager.TriggerDeviceScan(PERIPHERAL_BUS_ADDON);
+}
+
+unsigned int CPeripheralAddon::FeatureCount(const std::string& controllerId,
+ JOYSTICK_FEATURE_TYPE type) const
+{
+ using namespace GAME;
+
+ unsigned int count = 0;
+
+ CControllerManager& controllerProfiles = m_manager.GetControllerProfiles();
+ ControllerPtr controller = controllerProfiles.GetController(controllerId);
+ if (controller)
+ count = controller->FeatureCount(CPeripheralAddonTranslator::TranslateFeatureType(type));
+
+ return count;
+}
+
+JOYSTICK_FEATURE_TYPE CPeripheralAddon::FeatureType(const std::string& controllerId,
+ const std::string& featureName) const
+{
+ using namespace GAME;
+
+ JOYSTICK_FEATURE_TYPE type = JOYSTICK_FEATURE_TYPE_UNKNOWN;
+
+ CControllerManager& controllerProfiles = m_manager.GetControllerProfiles();
+ ControllerPtr controller = controllerProfiles.GetController(controllerId);
+ if (controller)
+ type = CPeripheralAddonTranslator::TranslateFeatureType(controller->FeatureType(featureName));
+
+ return type;
+}
+
+void CPeripheralAddon::GetPeripheralInfo(const CPeripheral* device,
+ kodi::addon::Peripheral& peripheralInfo)
+{
+ peripheralInfo.SetType(CPeripheralAddonTranslator::TranslateType(device->Type()));
+ peripheralInfo.SetName(device->DeviceName());
+ peripheralInfo.SetVendorID(device->VendorId());
+ peripheralInfo.SetProductID(device->ProductId());
+}
+
+void CPeripheralAddon::GetJoystickInfo(const CPeripheral* device,
+ kodi::addon::Joystick& joystickInfo)
+{
+ GetPeripheralInfo(device, joystickInfo);
+
+ if (device->Type() == PERIPHERAL_JOYSTICK)
+ {
+ const CPeripheralJoystick* joystick = static_cast<const CPeripheralJoystick*>(device);
+ joystickInfo.SetProvider(joystick->Provider());
+ joystickInfo.SetButtonCount(joystick->ButtonCount());
+ joystickInfo.SetHatCount(joystick->HatCount());
+ joystickInfo.SetAxisCount(joystick->AxisCount());
+ joystickInfo.SetMotorCount(joystick->MotorCount());
+ joystickInfo.SetSupportsPowerOff(joystick->SupportsPowerOff());
+ }
+ else if (device->Type() == PERIPHERAL_KEYBOARD || device->Type() == PERIPHERAL_MOUSE)
+ {
+ joystickInfo.SetName(GetDeviceName(device->Type())); // Override name with non-localized version
+ joystickInfo.SetProvider(GetProvider(device->Type()));
+ }
+}
+
+void CPeripheralAddon::SetJoystickInfo(CPeripheralJoystick& joystick,
+ const kodi::addon::Joystick& joystickInfo)
+{
+ joystick.SetProvider(joystickInfo.Provider());
+ joystick.SetRequestedPort(joystickInfo.RequestedPort());
+ joystick.SetButtonCount(joystickInfo.ButtonCount());
+ joystick.SetHatCount(joystickInfo.HatCount());
+ joystick.SetAxisCount(joystickInfo.AxisCount());
+ joystick.SetMotorCount(joystickInfo.MotorCount());
+ joystick.SetSupportsPowerOff(joystickInfo.SupportsPowerOff());
+}
+
+bool CPeripheralAddon::LogError(const PERIPHERAL_ERROR error, const char* strMethod) const
+{
+ if (error != PERIPHERAL_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "PERIPHERAL - {} - addon '{}' returned an error: {}", strMethod, Name(),
+ CPeripheralAddonTranslator::TranslateError(error));
+ return false;
+ }
+ return true;
+}
+
+std::string CPeripheralAddon::GetDeviceName(PeripheralType type)
+{
+ switch (type)
+ {
+ case PERIPHERAL_KEYBOARD:
+ return KEYBOARD_BUTTON_MAP_NAME;
+ case PERIPHERAL_MOUSE:
+ return MOUSE_BUTTON_MAP_NAME;
+ default:
+ break;
+ }
+
+ return "";
+}
+
+std::string CPeripheralAddon::GetProvider(PeripheralType type)
+{
+ switch (type)
+ {
+ case PERIPHERAL_KEYBOARD:
+ return KEYBOARD_PROVIDER;
+ case PERIPHERAL_MOUSE:
+ return MOUSE_PROVIDER;
+ default:
+ break;
+ }
+
+ return "";
+}
+
+void CPeripheralAddon::cb_trigger_scan(void* kodiInstance)
+{
+ if (kodiInstance == nullptr)
+ return;
+
+ static_cast<CPeripheralAddon*>(kodiInstance)->TriggerDeviceScan();
+}
+
+void CPeripheralAddon::cb_refresh_button_maps(void* kodiInstance,
+ const char* deviceName,
+ const char* controllerId)
+{
+ if (!kodiInstance)
+ return;
+
+ static_cast<CPeripheralAddon*>(kodiInstance)->RefreshButtonMaps(deviceName ? deviceName : "");
+}
+
+unsigned int CPeripheralAddon::cb_feature_count(void* kodiInstance,
+ const char* controllerId,
+ JOYSTICK_FEATURE_TYPE type)
+{
+ if (kodiInstance == nullptr || controllerId == nullptr)
+ return 0;
+
+ return static_cast<CPeripheralAddon*>(kodiInstance)->FeatureCount(controllerId, type);
+}
+
+JOYSTICK_FEATURE_TYPE CPeripheralAddon::cb_feature_type(void* kodiInstance,
+ const char* controllerId,
+ const char* featureName)
+{
+ if (kodiInstance == nullptr || controllerId == nullptr || featureName == nullptr)
+ return JOYSTICK_FEATURE_TYPE_UNKNOWN;
+
+ return static_cast<CPeripheralAddon*>(kodiInstance)->FeatureType(controllerId, featureName);
+}
diff --git a/xbmc/peripherals/addons/PeripheralAddon.h b/xbmc/peripherals/addons/PeripheralAddon.h
new file mode 100644
index 0000000..2dd9d22
--- /dev/null
+++ b/xbmc/peripherals/addons/PeripheralAddon.h
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2014-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 "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/kodi-dev-kit/include/kodi/addon-instance/Peripheral.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "peripherals/PeripheralTypes.h"
+#include "threads/CriticalSection.h"
+#include "threads/SharedSection.h"
+
+#include <map>
+#include <memory>
+#include <vector>
+
+class CFileItemList;
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IButtonMap;
+class IDriverHandler;
+} // namespace JOYSTICK
+} // namespace KODI
+
+namespace PERIPHERALS
+{
+class CPeripheral;
+class CPeripheralJoystick;
+class CPeripherals;
+
+typedef std::vector<kodi::addon::DriverPrimitive> PrimitiveVector;
+typedef std::map<KODI::JOYSTICK::FeatureName, kodi::addon::JoystickFeature> FeatureMap;
+
+class CPeripheralAddon : public ADDON::IAddonInstanceHandler
+{
+public:
+ explicit CPeripheralAddon(const ADDON::AddonInfoPtr& addonInfo, CPeripherals& manager);
+ ~CPeripheralAddon(void) override;
+
+ /*!
+ * @brief Initialise the instance of this add-on
+ */
+ bool CreateAddon(void);
+
+ /*!
+ * \brief Deinitialize the instance of this add-on
+ */
+ void DestroyAddon();
+
+ bool Register(unsigned int peripheralIndex, const PeripheralPtr& peripheral);
+ void UnregisterRemovedDevices(const PeripheralScanResults& results,
+ PeripheralVector& removedPeripherals);
+ void GetFeatures(std::vector<PeripheralFeature>& features) const;
+ bool HasFeature(const PeripheralFeature feature) const;
+ PeripheralPtr GetPeripheral(unsigned int index) const;
+ PeripheralPtr GetByPath(const std::string& strPath) const;
+ bool SupportsFeature(PeripheralFeature feature) const;
+ unsigned int GetPeripheralsWithFeature(PeripheralVector& results,
+ const PeripheralFeature feature) const;
+ unsigned int GetNumberOfPeripherals(void) const;
+ unsigned int GetNumberOfPeripheralsWithId(const int iVendorId, const int iProductId) const;
+ void GetDirectory(const std::string& strPath, CFileItemList& items) const;
+
+ /** @name Peripheral add-on methods */
+ //@{
+ bool PerformDeviceScan(PeripheralScanResults& results);
+ bool ProcessEvents(void);
+ bool SendRumbleEvent(unsigned int index, unsigned int driverIndex, float magnitude);
+ //@}
+
+ /** @name Joystick methods */
+ //@{
+ bool GetJoystickProperties(unsigned int index, CPeripheralJoystick& joystick);
+ bool HasButtonMaps(void) const { return m_bProvidesButtonMaps; }
+ bool GetFeatures(const CPeripheral* device,
+ const std::string& strControllerId,
+ FeatureMap& features);
+ bool MapFeature(const CPeripheral* device,
+ const std::string& strControllerId,
+ const kodi::addon::JoystickFeature& feature);
+ bool GetIgnoredPrimitives(const CPeripheral* device, PrimitiveVector& primitives);
+ bool SetIgnoredPrimitives(const CPeripheral* device, const PrimitiveVector& primitives);
+ void SaveButtonMap(const CPeripheral* device);
+ void RevertButtonMap(const CPeripheral* device);
+ void ResetButtonMap(const CPeripheral* device, const std::string& strControllerId);
+ void PowerOffJoystick(unsigned int index);
+ //@}
+
+ void RegisterButtonMap(CPeripheral* device, KODI::JOYSTICK::IButtonMap* buttonMap);
+ void UnregisterButtonMap(KODI::JOYSTICK::IButtonMap* buttonMap);
+
+ static bool ProvidesJoysticks(const ADDON::AddonInfoPtr& addonInfo);
+ static bool ProvidesButtonMaps(const ADDON::AddonInfoPtr& addonInfo);
+
+private:
+ void UnregisterButtonMap(CPeripheral* device);
+
+ // Binary add-on callbacks
+ void TriggerDeviceScan();
+ void RefreshButtonMaps(const std::string& strDeviceName = "");
+ unsigned int FeatureCount(const std::string& controllerId, JOYSTICK_FEATURE_TYPE type) const;
+ JOYSTICK_FEATURE_TYPE FeatureType(const std::string& controllerId,
+ const std::string& featureName) const;
+
+ /*!
+ * @brief Helper functions
+ */
+ static void GetPeripheralInfo(const CPeripheral* device, kodi::addon::Peripheral& peripheralInfo);
+
+ static void GetJoystickInfo(const CPeripheral* device, kodi::addon::Joystick& joystickInfo);
+ static void SetJoystickInfo(CPeripheralJoystick& joystick,
+ const kodi::addon::Joystick& joystickInfo);
+
+ /*!
+ * @brief Reset all class members to their defaults. Called by the constructors
+ */
+ void ResetProperties(void);
+
+ /*!
+ * @brief Retrieve add-on properties from the add-on
+ */
+ bool GetAddonProperties(void);
+
+ bool LogError(const PERIPHERAL_ERROR error, const char* strMethod) const;
+
+ static std::string GetDeviceName(PeripheralType type);
+ static std::string GetProvider(PeripheralType type);
+
+ // Construction parameters
+ CPeripherals& m_manager;
+
+ /* @brief Cache for const char* members in PERIPHERAL_PROPERTIES */
+ std::string m_strUserPath; /*!< @brief translated path to the user profile */
+ std::string m_strClientPath; /*!< @brief translated path to this add-on */
+
+ /*!
+ * @brief Callback functions from addon to kodi
+ */
+ //@{
+ static void cb_trigger_scan(void* kodiInstance);
+ static void cb_refresh_button_maps(void* kodiInstance,
+ const char* deviceName,
+ const char* controllerId);
+ static unsigned int cb_feature_count(void* kodiInstance,
+ const char* controllerId,
+ JOYSTICK_FEATURE_TYPE type);
+ static JOYSTICK_FEATURE_TYPE cb_feature_type(void* kodiInstance,
+ const char* controllerId,
+ const char* featureName);
+ //@}
+
+ /* @brief Add-on properties */
+ bool m_bProvidesJoysticks;
+ bool m_bSupportsJoystickRumble;
+ bool m_bSupportsJoystickPowerOff;
+ bool m_bProvidesButtonMaps;
+
+ /* @brief Map of peripherals belonging to the add-on */
+ std::map<unsigned int, PeripheralPtr> m_peripherals;
+
+ /* @brief Button map observers */
+ std::vector<std::pair<CPeripheral*, KODI::JOYSTICK::IButtonMap*>> m_buttonMaps;
+ CCriticalSection m_buttonMapMutex;
+
+ /* @brief Thread synchronization */
+ mutable CCriticalSection m_critSection;
+
+ CSharedSection m_dllSection;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/addons/PeripheralAddonTranslator.cpp b/xbmc/peripherals/addons/PeripheralAddonTranslator.cpp
new file mode 100644
index 0000000..16dd529
--- /dev/null
+++ b/xbmc/peripherals/addons/PeripheralAddonTranslator.cpp
@@ -0,0 +1,449 @@
+/*
+ * 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 "PeripheralAddonTranslator.h"
+
+#include "games/controllers/ControllerTranslator.h"
+#include "input/joysticks/JoystickUtils.h"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace KODI;
+using namespace JOYSTICK;
+using namespace PERIPHERALS;
+
+// --- Helper function ---------------------------------------------------------
+
+JOYSTICK_DRIVER_SEMIAXIS_DIRECTION operator*(JOYSTICK_DRIVER_SEMIAXIS_DIRECTION dir, int i)
+{
+ return static_cast<JOYSTICK_DRIVER_SEMIAXIS_DIRECTION>(static_cast<int>(dir) * i);
+}
+
+// --- CPeripheralAddonTranslator ----------------------------------------------
+
+const char* CPeripheralAddonTranslator::TranslateError(const PERIPHERAL_ERROR error)
+{
+ switch (error)
+ {
+ case PERIPHERAL_NO_ERROR:
+ return "no error";
+ case PERIPHERAL_ERROR_FAILED:
+ return "command failed";
+ case PERIPHERAL_ERROR_INVALID_PARAMETERS:
+ return "invalid parameters";
+ case PERIPHERAL_ERROR_NOT_IMPLEMENTED:
+ return "not implemented";
+ case PERIPHERAL_ERROR_NOT_CONNECTED:
+ return "not connected";
+ case PERIPHERAL_ERROR_CONNECTION_FAILED:
+ return "connection failed";
+ case PERIPHERAL_ERROR_UNKNOWN:
+ default:
+ return "unknown error";
+ }
+}
+
+PeripheralType CPeripheralAddonTranslator::TranslateType(PERIPHERAL_TYPE type)
+{
+ switch (type)
+ {
+ case PERIPHERAL_TYPE_JOYSTICK:
+ return PERIPHERAL_JOYSTICK;
+ default:
+ break;
+ }
+ return PERIPHERAL_UNKNOWN;
+}
+
+PERIPHERAL_TYPE CPeripheralAddonTranslator::TranslateType(PeripheralType type)
+{
+ switch (type)
+ {
+ case PERIPHERAL_JOYSTICK:
+ return PERIPHERAL_TYPE_JOYSTICK;
+ default:
+ break;
+ }
+ return PERIPHERAL_TYPE_UNKNOWN;
+}
+
+CDriverPrimitive CPeripheralAddonTranslator::TranslatePrimitive(
+ const kodi::addon::DriverPrimitive& primitive)
+{
+ CDriverPrimitive retVal;
+
+ switch (primitive.Type())
+ {
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_BUTTON:
+ {
+ retVal = CDriverPrimitive(PRIMITIVE_TYPE::BUTTON, primitive.DriverIndex());
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_HAT_DIRECTION:
+ {
+ retVal = CDriverPrimitive(primitive.DriverIndex(),
+ TranslateHatDirection(primitive.HatDirection()));
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_SEMIAXIS:
+ {
+ retVal = CDriverPrimitive(primitive.DriverIndex(), primitive.Center(),
+ TranslateSemiAxisDirection(primitive.SemiAxisDirection()),
+ primitive.Range());
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOTOR:
+ {
+ retVal = CDriverPrimitive(PRIMITIVE_TYPE::MOTOR, primitive.DriverIndex());
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_KEY:
+ {
+ KEYBOARD::KeySymbol keycode =
+ GAME::CControllerTranslator::TranslateKeysym(primitive.Keycode());
+ retVal = CDriverPrimitive(keycode);
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_MOUSE_BUTTON:
+ {
+ retVal = CDriverPrimitive(TranslateMouseButton(primitive.MouseIndex()));
+ break;
+ }
+ case JOYSTICK_DRIVER_PRIMITIVE_TYPE_RELPOINTER_DIRECTION:
+ {
+ retVal = CDriverPrimitive(TranslateRelPointerDirection(primitive.RelPointerDirection()));
+ break;
+ }
+ default:
+ break;
+ }
+
+ return retVal;
+}
+
+kodi::addon::DriverPrimitive CPeripheralAddonTranslator::TranslatePrimitive(
+ const CDriverPrimitive& primitive)
+{
+ kodi::addon::DriverPrimitive retVal;
+
+ switch (primitive.Type())
+ {
+ case PRIMITIVE_TYPE::BUTTON:
+ {
+ retVal = kodi::addon::DriverPrimitive::CreateButton(primitive.Index());
+ break;
+ }
+ case PRIMITIVE_TYPE::HAT:
+ {
+ retVal = kodi::addon::DriverPrimitive(primitive.Index(),
+ TranslateHatDirection(primitive.HatDirection()));
+ break;
+ }
+ case PRIMITIVE_TYPE::SEMIAXIS:
+ {
+ retVal = kodi::addon::DriverPrimitive(
+ primitive.Index(), primitive.Center(),
+ TranslateSemiAxisDirection(primitive.SemiAxisDirection()), primitive.Range());
+ break;
+ }
+ case PRIMITIVE_TYPE::MOTOR:
+ {
+ retVal = kodi::addon::DriverPrimitive::CreateMotor(primitive.Index());
+ break;
+ }
+ case PRIMITIVE_TYPE::KEY:
+ {
+ std::string keysym = GAME::CControllerTranslator::TranslateKeycode(primitive.Keycode());
+ retVal = kodi::addon::DriverPrimitive(keysym);
+ break;
+ }
+ case PRIMITIVE_TYPE::MOUSE_BUTTON:
+ {
+ retVal = kodi::addon::DriverPrimitive::CreateMouseButton(
+ TranslateMouseButton(primitive.MouseButton()));
+ break;
+ }
+ case PRIMITIVE_TYPE::RELATIVE_POINTER:
+ {
+ retVal =
+ kodi::addon::DriverPrimitive(TranslateRelPointerDirection(primitive.PointerDirection()));
+ break;
+ }
+ default:
+ break;
+ }
+
+ return retVal;
+}
+
+std::vector<JOYSTICK::CDriverPrimitive> CPeripheralAddonTranslator::TranslatePrimitives(
+ const std::vector<kodi::addon::DriverPrimitive>& primitives)
+{
+ std::vector<JOYSTICK::CDriverPrimitive> ret;
+ std::transform(primitives.begin(), primitives.end(), std::back_inserter(ret),
+ [](const kodi::addon::DriverPrimitive& primitive) {
+ return CPeripheralAddonTranslator::TranslatePrimitive(primitive);
+ });
+ return ret;
+}
+
+std::vector<kodi::addon::DriverPrimitive> CPeripheralAddonTranslator::TranslatePrimitives(
+ const std::vector<JOYSTICK::CDriverPrimitive>& primitives)
+{
+ std::vector<kodi::addon::DriverPrimitive> ret;
+ std::transform(primitives.begin(), primitives.end(), std::back_inserter(ret),
+ [](const JOYSTICK::CDriverPrimitive& primitive) {
+ return CPeripheralAddonTranslator::TranslatePrimitive(primitive);
+ });
+ return ret;
+}
+
+HAT_DIRECTION CPeripheralAddonTranslator::TranslateHatDirection(JOYSTICK_DRIVER_HAT_DIRECTION dir)
+{
+ switch (dir)
+ {
+ case JOYSTICK_DRIVER_HAT_LEFT:
+ return HAT_DIRECTION::LEFT;
+ case JOYSTICK_DRIVER_HAT_RIGHT:
+ return HAT_DIRECTION::RIGHT;
+ case JOYSTICK_DRIVER_HAT_UP:
+ return HAT_DIRECTION::UP;
+ case JOYSTICK_DRIVER_HAT_DOWN:
+ return HAT_DIRECTION::DOWN;
+ default:
+ break;
+ }
+ return HAT_DIRECTION::NONE;
+}
+
+JOYSTICK_DRIVER_HAT_DIRECTION CPeripheralAddonTranslator::TranslateHatDirection(HAT_DIRECTION dir)
+{
+ switch (dir)
+ {
+ case HAT_DIRECTION::UP:
+ return JOYSTICK_DRIVER_HAT_UP;
+ case HAT_DIRECTION::DOWN:
+ return JOYSTICK_DRIVER_HAT_DOWN;
+ case HAT_DIRECTION::RIGHT:
+ return JOYSTICK_DRIVER_HAT_RIGHT;
+ case HAT_DIRECTION::LEFT:
+ return JOYSTICK_DRIVER_HAT_LEFT;
+ default:
+ break;
+ }
+ return JOYSTICK_DRIVER_HAT_UNKNOWN;
+}
+
+HAT_STATE CPeripheralAddonTranslator::TranslateHatState(JOYSTICK_STATE_HAT state)
+{
+ HAT_STATE translatedState = HAT_STATE::NONE;
+
+ if (state & JOYSTICK_STATE_HAT_UP)
+ translatedState |= HAT_STATE::UP;
+ if (state & JOYSTICK_STATE_HAT_DOWN)
+ translatedState |= HAT_STATE::DOWN;
+ if (state & JOYSTICK_STATE_HAT_RIGHT)
+ translatedState |= HAT_STATE::RIGHT;
+ if (state & JOYSTICK_STATE_HAT_LEFT)
+ translatedState |= HAT_STATE::LEFT;
+
+ return translatedState;
+}
+
+SEMIAXIS_DIRECTION CPeripheralAddonTranslator::TranslateSemiAxisDirection(
+ JOYSTICK_DRIVER_SEMIAXIS_DIRECTION dir)
+{
+ switch (dir)
+ {
+ case JOYSTICK_DRIVER_SEMIAXIS_POSITIVE:
+ return SEMIAXIS_DIRECTION::POSITIVE;
+ case JOYSTICK_DRIVER_SEMIAXIS_NEGATIVE:
+ return SEMIAXIS_DIRECTION::NEGATIVE;
+ default:
+ break;
+ }
+ return SEMIAXIS_DIRECTION::ZERO;
+}
+
+JOYSTICK_DRIVER_SEMIAXIS_DIRECTION CPeripheralAddonTranslator::TranslateSemiAxisDirection(
+ SEMIAXIS_DIRECTION dir)
+{
+ switch (dir)
+ {
+ case SEMIAXIS_DIRECTION::POSITIVE:
+ return JOYSTICK_DRIVER_SEMIAXIS_POSITIVE;
+ case SEMIAXIS_DIRECTION::NEGATIVE:
+ return JOYSTICK_DRIVER_SEMIAXIS_NEGATIVE;
+ default:
+ break;
+ }
+ return JOYSTICK_DRIVER_SEMIAXIS_UNKNOWN;
+}
+
+MOUSE::BUTTON_ID CPeripheralAddonTranslator::TranslateMouseButton(
+ JOYSTICK_DRIVER_MOUSE_INDEX button)
+{
+ switch (button)
+ {
+ case JOYSTICK_DRIVER_MOUSE_INDEX_LEFT:
+ return MOUSE::BUTTON_ID::LEFT;
+ case JOYSTICK_DRIVER_MOUSE_INDEX_RIGHT:
+ return MOUSE::BUTTON_ID::RIGHT;
+ case JOYSTICK_DRIVER_MOUSE_INDEX_MIDDLE:
+ return MOUSE::BUTTON_ID::MIDDLE;
+ case JOYSTICK_DRIVER_MOUSE_INDEX_BUTTON4:
+ return MOUSE::BUTTON_ID::BUTTON4;
+ case JOYSTICK_DRIVER_MOUSE_INDEX_BUTTON5:
+ return MOUSE::BUTTON_ID::BUTTON5;
+ case JOYSTICK_DRIVER_MOUSE_INDEX_WHEEL_UP:
+ return MOUSE::BUTTON_ID::WHEEL_UP;
+ case JOYSTICK_DRIVER_MOUSE_INDEX_WHEEL_DOWN:
+ return MOUSE::BUTTON_ID::WHEEL_DOWN;
+ case JOYSTICK_DRIVER_MOUSE_INDEX_HORIZ_WHEEL_LEFT:
+ return MOUSE::BUTTON_ID::HORIZ_WHEEL_LEFT;
+ case JOYSTICK_DRIVER_MOUSE_INDEX_HORIZ_WHEEL_RIGHT:
+ return MOUSE::BUTTON_ID::HORIZ_WHEEL_RIGHT;
+ default:
+ break;
+ }
+
+ return MOUSE::BUTTON_ID::UNKNOWN;
+}
+
+JOYSTICK_DRIVER_MOUSE_INDEX CPeripheralAddonTranslator::TranslateMouseButton(
+ MOUSE::BUTTON_ID button)
+{
+ switch (button)
+ {
+ case MOUSE::BUTTON_ID::LEFT:
+ return JOYSTICK_DRIVER_MOUSE_INDEX_LEFT;
+ case MOUSE::BUTTON_ID::RIGHT:
+ return JOYSTICK_DRIVER_MOUSE_INDEX_RIGHT;
+ case MOUSE::BUTTON_ID::MIDDLE:
+ return JOYSTICK_DRIVER_MOUSE_INDEX_MIDDLE;
+ case MOUSE::BUTTON_ID::BUTTON4:
+ return JOYSTICK_DRIVER_MOUSE_INDEX_BUTTON4;
+ case MOUSE::BUTTON_ID::BUTTON5:
+ return JOYSTICK_DRIVER_MOUSE_INDEX_BUTTON5;
+ case MOUSE::BUTTON_ID::WHEEL_UP:
+ return JOYSTICK_DRIVER_MOUSE_INDEX_WHEEL_UP;
+ case MOUSE::BUTTON_ID::WHEEL_DOWN:
+ return JOYSTICK_DRIVER_MOUSE_INDEX_WHEEL_DOWN;
+ case MOUSE::BUTTON_ID::HORIZ_WHEEL_LEFT:
+ return JOYSTICK_DRIVER_MOUSE_INDEX_HORIZ_WHEEL_LEFT;
+ case MOUSE::BUTTON_ID::HORIZ_WHEEL_RIGHT:
+ return JOYSTICK_DRIVER_MOUSE_INDEX_HORIZ_WHEEL_RIGHT;
+ default:
+ break;
+ }
+
+ return JOYSTICK_DRIVER_MOUSE_INDEX_UNKNOWN;
+}
+
+RELATIVE_POINTER_DIRECTION CPeripheralAddonTranslator::TranslateRelPointerDirection(
+ JOYSTICK_DRIVER_RELPOINTER_DIRECTION dir)
+{
+ switch (dir)
+ {
+ case JOYSTICK_DRIVER_RELPOINTER_LEFT:
+ return RELATIVE_POINTER_DIRECTION::LEFT;
+ case JOYSTICK_DRIVER_RELPOINTER_RIGHT:
+ return RELATIVE_POINTER_DIRECTION::RIGHT;
+ case JOYSTICK_DRIVER_RELPOINTER_UP:
+ return RELATIVE_POINTER_DIRECTION::UP;
+ case JOYSTICK_DRIVER_RELPOINTER_DOWN:
+ return RELATIVE_POINTER_DIRECTION::DOWN;
+ default:
+ break;
+ }
+
+ return RELATIVE_POINTER_DIRECTION::NONE;
+}
+
+JOYSTICK_DRIVER_RELPOINTER_DIRECTION CPeripheralAddonTranslator::TranslateRelPointerDirection(
+ RELATIVE_POINTER_DIRECTION dir)
+{
+ switch (dir)
+ {
+ case RELATIVE_POINTER_DIRECTION::UP:
+ return JOYSTICK_DRIVER_RELPOINTER_UP;
+ case RELATIVE_POINTER_DIRECTION::DOWN:
+ return JOYSTICK_DRIVER_RELPOINTER_DOWN;
+ case RELATIVE_POINTER_DIRECTION::RIGHT:
+ return JOYSTICK_DRIVER_RELPOINTER_RIGHT;
+ case RELATIVE_POINTER_DIRECTION::LEFT:
+ return JOYSTICK_DRIVER_RELPOINTER_LEFT;
+ default:
+ break;
+ }
+ return JOYSTICK_DRIVER_RELPOINTER_UNKNOWN;
+}
+
+JOYSTICK::FEATURE_TYPE CPeripheralAddonTranslator::TranslateFeatureType(JOYSTICK_FEATURE_TYPE type)
+{
+ switch (type)
+ {
+ case JOYSTICK_FEATURE_TYPE_SCALAR:
+ return JOYSTICK::FEATURE_TYPE::SCALAR;
+ case JOYSTICK_FEATURE_TYPE_ANALOG_STICK:
+ return JOYSTICK::FEATURE_TYPE::ANALOG_STICK;
+ case JOYSTICK_FEATURE_TYPE_ACCELEROMETER:
+ return JOYSTICK::FEATURE_TYPE::ACCELEROMETER;
+ case JOYSTICK_FEATURE_TYPE_MOTOR:
+ return JOYSTICK::FEATURE_TYPE::MOTOR;
+ case JOYSTICK_FEATURE_TYPE_RELPOINTER:
+ return JOYSTICK::FEATURE_TYPE::RELPOINTER;
+ case JOYSTICK_FEATURE_TYPE_ABSPOINTER:
+ return JOYSTICK::FEATURE_TYPE::ABSPOINTER;
+ case JOYSTICK_FEATURE_TYPE_WHEEL:
+ return JOYSTICK::FEATURE_TYPE::WHEEL;
+ case JOYSTICK_FEATURE_TYPE_THROTTLE:
+ return JOYSTICK::FEATURE_TYPE::THROTTLE;
+ case JOYSTICK_FEATURE_TYPE_KEY:
+ return JOYSTICK::FEATURE_TYPE::KEY;
+ default:
+ break;
+ }
+ return JOYSTICK::FEATURE_TYPE::UNKNOWN;
+}
+
+JOYSTICK_FEATURE_TYPE CPeripheralAddonTranslator::TranslateFeatureType(JOYSTICK::FEATURE_TYPE type)
+{
+ switch (type)
+ {
+ case JOYSTICK::FEATURE_TYPE::SCALAR:
+ return JOYSTICK_FEATURE_TYPE_SCALAR;
+ case JOYSTICK::FEATURE_TYPE::ANALOG_STICK:
+ return JOYSTICK_FEATURE_TYPE_ANALOG_STICK;
+ case JOYSTICK::FEATURE_TYPE::ACCELEROMETER:
+ return JOYSTICK_FEATURE_TYPE_ACCELEROMETER;
+ case JOYSTICK::FEATURE_TYPE::MOTOR:
+ return JOYSTICK_FEATURE_TYPE_MOTOR;
+ case JOYSTICK::FEATURE_TYPE::RELPOINTER:
+ return JOYSTICK_FEATURE_TYPE_RELPOINTER;
+ case JOYSTICK::FEATURE_TYPE::ABSPOINTER:
+ return JOYSTICK_FEATURE_TYPE_ABSPOINTER;
+ case JOYSTICK::FEATURE_TYPE::WHEEL:
+ return JOYSTICK_FEATURE_TYPE_WHEEL;
+ case JOYSTICK::FEATURE_TYPE::THROTTLE:
+ return JOYSTICK_FEATURE_TYPE_THROTTLE;
+ case JOYSTICK::FEATURE_TYPE::KEY:
+ return JOYSTICK_FEATURE_TYPE_KEY;
+ default:
+ break;
+ }
+ return JOYSTICK_FEATURE_TYPE_UNKNOWN;
+}
+
+kodi::addon::DriverPrimitive CPeripheralAddonTranslator::Opposite(
+ const kodi::addon::DriverPrimitive& primitive)
+{
+ return kodi::addon::DriverPrimitive(primitive.DriverIndex(), primitive.Center() * -1,
+ primitive.SemiAxisDirection() * -1, primitive.Range());
+}
diff --git a/xbmc/peripherals/addons/PeripheralAddonTranslator.h b/xbmc/peripherals/addons/PeripheralAddonTranslator.h
new file mode 100644
index 0000000..308d62c
--- /dev/null
+++ b/xbmc/peripherals/addons/PeripheralAddonTranslator.h
@@ -0,0 +1,62 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/addon-instance/Peripheral.h"
+#include "input/joysticks/DriverPrimitive.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "input/mouse/MouseTypes.h"
+#include "peripherals/PeripheralTypes.h"
+
+#include <vector>
+
+namespace PERIPHERALS
+{
+class CPeripheralAddonTranslator
+{
+public:
+ static const char* TranslateError(PERIPHERAL_ERROR error);
+
+ static PeripheralType TranslateType(PERIPHERAL_TYPE type);
+ static PERIPHERAL_TYPE TranslateType(PeripheralType type);
+
+ static KODI::JOYSTICK::CDriverPrimitive TranslatePrimitive(
+ const kodi::addon::DriverPrimitive& primitive);
+ static kodi::addon::DriverPrimitive TranslatePrimitive(
+ const KODI::JOYSTICK::CDriverPrimitive& primitive);
+
+ static std::vector<KODI::JOYSTICK::CDriverPrimitive> TranslatePrimitives(
+ const std::vector<kodi::addon::DriverPrimitive>& primitives);
+ static std::vector<kodi::addon::DriverPrimitive> TranslatePrimitives(
+ const std::vector<KODI::JOYSTICK::CDriverPrimitive>& primitives);
+
+ static KODI::JOYSTICK::HAT_DIRECTION TranslateHatDirection(JOYSTICK_DRIVER_HAT_DIRECTION dir);
+ static JOYSTICK_DRIVER_HAT_DIRECTION TranslateHatDirection(KODI::JOYSTICK::HAT_DIRECTION dir);
+
+ static KODI::JOYSTICK::HAT_STATE TranslateHatState(JOYSTICK_STATE_HAT state);
+
+ static KODI::JOYSTICK::SEMIAXIS_DIRECTION TranslateSemiAxisDirection(
+ JOYSTICK_DRIVER_SEMIAXIS_DIRECTION dir);
+ static JOYSTICK_DRIVER_SEMIAXIS_DIRECTION TranslateSemiAxisDirection(
+ KODI::JOYSTICK::SEMIAXIS_DIRECTION dir);
+
+ static KODI::MOUSE::BUTTON_ID TranslateMouseButton(JOYSTICK_DRIVER_MOUSE_INDEX button);
+ static JOYSTICK_DRIVER_MOUSE_INDEX TranslateMouseButton(KODI::MOUSE::BUTTON_ID button);
+
+ static KODI::JOYSTICK::RELATIVE_POINTER_DIRECTION TranslateRelPointerDirection(
+ JOYSTICK_DRIVER_RELPOINTER_DIRECTION dir);
+ static JOYSTICK_DRIVER_RELPOINTER_DIRECTION TranslateRelPointerDirection(
+ KODI::JOYSTICK::RELATIVE_POINTER_DIRECTION dir);
+
+ static KODI::JOYSTICK::FEATURE_TYPE TranslateFeatureType(JOYSTICK_FEATURE_TYPE type);
+ static JOYSTICK_FEATURE_TYPE TranslateFeatureType(KODI::JOYSTICK::FEATURE_TYPE type);
+
+ static kodi::addon::DriverPrimitive Opposite(const kodi::addon::DriverPrimitive& semiaxis);
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/bus/CMakeLists.txt b/xbmc/peripherals/bus/CMakeLists.txt
new file mode 100644
index 0000000..81c80c5
--- /dev/null
+++ b/xbmc/peripherals/bus/CMakeLists.txt
@@ -0,0 +1,6 @@
+set(SOURCES PeripheralBus.cpp)
+
+set(HEADERS PeripheralBus.h
+ PeripheralBusUSB.h)
+
+core_add_library(peripherals_bus)
diff --git a/xbmc/peripherals/bus/PeripheralBus.cpp b/xbmc/peripherals/bus/PeripheralBus.cpp
new file mode 100644
index 0000000..59a4c61
--- /dev/null
+++ b/xbmc/peripherals/bus/PeripheralBus.cpp
@@ -0,0 +1,352 @@
+/*
+ * 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 "PeripheralBus.h"
+
+#include "FileItem.h"
+#include "guilib/LocalizeStrings.h"
+#include "peripherals/Peripherals.h"
+#include "peripherals/devices/Peripheral.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace PERIPHERALS;
+using namespace std::chrono_literals;
+
+namespace
+{
+constexpr auto PERIPHERAL_DEFAULT_RESCAN_INTERVAL = 5000ms;
+}
+
+CPeripheralBus::CPeripheralBus(const std::string& threadname,
+ CPeripherals& manager,
+ PeripheralBusType type)
+ : CThread(threadname.c_str()),
+ m_iRescanTime(PERIPHERAL_DEFAULT_RESCAN_INTERVAL),
+ m_bNeedsPolling(true),
+ m_manager(manager),
+ m_type(type),
+ m_triggerEvent(true)
+{
+}
+
+bool CPeripheralBus::InitializeProperties(CPeripheral& peripheral)
+{
+ return true;
+}
+
+void CPeripheralBus::OnDeviceAdded(const std::string& strLocation)
+{
+ ScanForDevices();
+}
+
+void CPeripheralBus::OnDeviceChanged(const std::string& strLocation)
+{
+ ScanForDevices();
+}
+
+void CPeripheralBus::OnDeviceRemoved(const std::string& strLocation)
+{
+ ScanForDevices();
+}
+
+void CPeripheralBus::Clear(void)
+{
+ if (m_bNeedsPolling)
+ {
+ StopThread(false);
+ m_triggerEvent.Set();
+ StopThread(true);
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_peripherals.clear();
+}
+
+void CPeripheralBus::UnregisterRemovedDevices(const PeripheralScanResults& results)
+{
+ PeripheralVector removedPeripherals;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (int iDevicePtr = (int)m_peripherals.size() - 1; iDevicePtr >= 0; iDevicePtr--)
+ {
+ const PeripheralPtr& peripheral = m_peripherals.at(iDevicePtr);
+ PeripheralScanResult updatedDevice(m_type);
+ if (!results.GetDeviceOnLocation(peripheral->Location(), &updatedDevice) ||
+ *peripheral != updatedDevice)
+ {
+ /* device removed */
+ removedPeripherals.push_back(peripheral);
+ m_peripherals.erase(m_peripherals.begin() + iDevicePtr);
+ }
+ }
+ }
+
+ for (auto& peripheral : removedPeripherals)
+ {
+ std::vector<PeripheralFeature> features;
+ peripheral->GetFeatures(features);
+ bool peripheralHasFeatures =
+ features.size() > 1 || (features.size() == 1 && features.at(0) != FEATURE_UNKNOWN);
+ if (peripheral->Type() != PERIPHERAL_UNKNOWN || peripheralHasFeatures)
+ {
+ CLog::Log(LOGINFO, "{} - device removed from {}/{}: {} ({}:{})", __FUNCTION__,
+ PeripheralTypeTranslator::TypeToString(peripheral->Type()), peripheral->Location(),
+ peripheral->DeviceName(), peripheral->VendorIdAsString(),
+ peripheral->ProductIdAsString());
+ peripheral->OnDeviceRemoved();
+ }
+
+ m_manager.OnDeviceDeleted(*this, *peripheral);
+ }
+}
+
+void CPeripheralBus::RegisterNewDevices(const PeripheralScanResults& results)
+{
+ for (unsigned int iResultPtr = 0; iResultPtr < results.m_results.size(); iResultPtr++)
+ {
+ const PeripheralScanResult& result = results.m_results.at(iResultPtr);
+ if (!HasPeripheral(result.m_strLocation))
+ m_manager.CreatePeripheral(*this, result);
+ }
+}
+
+bool CPeripheralBus::ScanForDevices(void)
+{
+ bool bReturn(false);
+
+ PeripheralScanResults results;
+ if (PerformDeviceScan(results))
+ {
+ UnregisterRemovedDevices(results);
+ RegisterNewDevices(results);
+
+ m_manager.NotifyObservers(ObservableMessagePeripheralsChanged);
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CPeripheralBus::HasFeature(const PeripheralFeature feature) const
+{
+ bool bReturn(false);
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (unsigned int iPeripheralPtr = 0; iPeripheralPtr < m_peripherals.size(); iPeripheralPtr++)
+ {
+ if (m_peripherals.at(iPeripheralPtr)->HasFeature(feature))
+ {
+ bReturn = true;
+ break;
+ }
+ }
+ return bReturn;
+}
+
+void CPeripheralBus::GetFeatures(std::vector<PeripheralFeature>& features) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (unsigned int iPeripheralPtr = 0; iPeripheralPtr < m_peripherals.size(); iPeripheralPtr++)
+ m_peripherals.at(iPeripheralPtr)->GetFeatures(features);
+}
+
+PeripheralPtr CPeripheralBus::GetPeripheral(const std::string& strLocation) const
+{
+ PeripheralPtr result;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto& peripheral : m_peripherals)
+ {
+ if (peripheral->Location() == strLocation)
+ {
+ result = peripheral;
+ break;
+ }
+ }
+ return result;
+}
+
+unsigned int CPeripheralBus::GetPeripheralsWithFeature(PeripheralVector& results,
+ const PeripheralFeature feature) const
+{
+ unsigned int iReturn = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto& peripheral : m_peripherals)
+ {
+ if (peripheral->HasFeature(feature))
+ {
+ results.push_back(peripheral);
+ ++iReturn;
+ }
+ }
+
+ return iReturn;
+}
+
+unsigned int CPeripheralBus::GetNumberOfPeripheralsWithId(const int iVendorId,
+ const int iProductId) const
+{
+ unsigned int iReturn = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& peripheral : m_peripherals)
+ {
+ if (peripheral->VendorId() == iVendorId && peripheral->ProductId() == iProductId)
+ iReturn++;
+ }
+
+ return iReturn;
+}
+
+void CPeripheralBus::Process(void)
+{
+ while (!m_bStop)
+ {
+ m_triggerEvent.Reset();
+
+ if (!ScanForDevices())
+ break;
+
+ // depending on bus implementation
+ // needsPolling can be set properly
+ // only after initial scan.
+ // if this is the case, bail out.
+ if (!m_bNeedsPolling)
+ break;
+
+ if (!m_bStop)
+ m_triggerEvent.Wait(m_iRescanTime);
+ }
+}
+
+void CPeripheralBus::Initialise(void)
+{
+ bool bNeedsPolling = false;
+
+ if (!IsRunning())
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bNeedsPolling = m_bNeedsPolling;
+ }
+
+ if (bNeedsPolling)
+ {
+ m_triggerEvent.Reset();
+ Create();
+ SetPriority(ThreadPriority::BELOW_NORMAL);
+ }
+}
+
+void CPeripheralBus::Register(const PeripheralPtr& peripheral)
+{
+ if (!peripheral)
+ return;
+
+ bool bPeripheralAdded = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!HasPeripheral(peripheral->Location()))
+ {
+ m_peripherals.push_back(peripheral);
+ bPeripheralAdded = true;
+ }
+ }
+
+ if (bPeripheralAdded)
+ {
+ CLog::Log(LOGINFO, "{} - new {} device registered on {}->{}: {} ({}:{})", __FUNCTION__,
+ PeripheralTypeTranslator::TypeToString(peripheral->Type()),
+ PeripheralTypeTranslator::BusTypeToString(m_type), peripheral->Location(),
+ peripheral->DeviceName(), peripheral->VendorIdAsString(),
+ peripheral->ProductIdAsString());
+ m_manager.OnDeviceAdded(*this, *peripheral);
+ }
+}
+
+void CPeripheralBus::TriggerDeviceScan(void)
+{
+ bool bNeedsPolling;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bNeedsPolling = m_bNeedsPolling;
+ }
+
+ if (bNeedsPolling)
+ m_triggerEvent.Set();
+ else
+ ScanForDevices();
+}
+
+bool CPeripheralBus::HasPeripheral(const std::string& strLocation) const
+{
+ return (GetPeripheral(strLocation) != NULL);
+}
+
+void CPeripheralBus::GetDirectory(const std::string& strPath, CFileItemList& items) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& peripheral : m_peripherals)
+ {
+ if (peripheral->IsHidden())
+ continue;
+
+ CFileItemPtr peripheralFile(new CFileItem(peripheral->DeviceName()));
+ peripheralFile->SetPath(peripheral->FileLocation());
+ peripheralFile->SetProperty("vendor", peripheral->VendorIdAsString());
+ peripheralFile->SetProperty("product", peripheral->ProductIdAsString());
+ peripheralFile->SetProperty(
+ "bus", PeripheralTypeTranslator::BusTypeToString(peripheral->GetBusType()));
+ peripheralFile->SetProperty("location", peripheral->Location());
+ peripheralFile->SetProperty("class",
+ PeripheralTypeTranslator::TypeToString(peripheral->Type()));
+
+ std::string strVersion(peripheral->GetVersionInfo());
+ if (strVersion.empty())
+ strVersion = g_localizeStrings.Get(13205);
+
+ std::string strDetails = StringUtils::Format("{} {}", g_localizeStrings.Get(24051), strVersion);
+ if (peripheral->GetBusType() == PERIPHERAL_BUS_CEC && !peripheral->GetSettingBool("enabled"))
+ strDetails =
+ StringUtils::Format("{}: {}", g_localizeStrings.Get(126), g_localizeStrings.Get(13106));
+
+ peripheralFile->SetProperty("version", strVersion);
+ peripheralFile->SetLabel2(strDetails);
+ peripheralFile->SetArt("icon", peripheral->GetIcon());
+
+ items.Add(peripheralFile);
+ }
+}
+
+PeripheralPtr CPeripheralBus::GetByPath(const std::string& strPath) const
+{
+ PeripheralPtr result;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto& peripheral : m_peripherals)
+ {
+ if (StringUtils::EqualsNoCase(strPath, peripheral->FileLocation()))
+ {
+ result = peripheral;
+ break;
+ }
+ }
+
+ return result;
+}
+
+unsigned int CPeripheralBus::GetNumberOfPeripherals() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return static_cast<unsigned int>(m_peripherals.size());
+}
diff --git a/xbmc/peripherals/bus/PeripheralBus.h b/xbmc/peripherals/bus/PeripheralBus.h
new file mode 100644
index 0000000..6b67a7f
--- /dev/null
+++ b/xbmc/peripherals/bus/PeripheralBus.h
@@ -0,0 +1,221 @@
+/*
+ * 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 "peripherals/PeripheralTypes.h"
+#include "threads/Thread.h"
+
+#include <memory>
+#include <mutex>
+#include <vector>
+
+class CFileItemList;
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IButtonMap;
+} // namespace JOYSTICK
+} // namespace KODI
+
+namespace PERIPHERALS
+{
+class CPeripheral;
+class CPeripherals;
+
+/*!
+ * @class CPeripheralBus
+ * This represents a bus on the system. By default, this bus instance will scan for changes every 5
+ * seconds. If this bus only has to be updated after a notification sent by the system, set
+ * m_bNeedsPolling to false in the constructor, and implement the OnDeviceAdded(), OnDeviceChanged()
+ * and OnDeviceRemoved() methods.
+ *
+ * The PerformDeviceScan() method has to be implemented by each specific bus implementation.
+ */
+class CPeripheralBus : protected CThread
+{
+public:
+ CPeripheralBus(const std::string& threadname, CPeripherals& manager, PeripheralBusType type);
+ ~CPeripheralBus(void) override { Clear(); }
+
+ /*!
+ * @return The bus type
+ */
+ PeripheralBusType Type(void) const { return m_type; }
+
+ /*!
+ * @return True if this bus needs to be polled for changes, false if this bus performs updates via
+ * callbacks
+ */
+ bool NeedsPolling(void) const
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bNeedsPolling;
+ }
+
+ /*!
+ * \brief Initialize the properties of a peripheral with a known location
+ */
+ virtual bool InitializeProperties(CPeripheral& peripheral);
+
+ /*!
+ * \brief Initialize a joystick buttonmap, if possible
+ */
+ virtual bool InitializeButtonMap(const CPeripheral& peripheral,
+ KODI::JOYSTICK::IButtonMap& buttonMap) const
+ {
+ return false;
+ }
+
+ /*!
+ * @brief Get the instance of the peripheral at the given location.
+ * @param strLocation The location.
+ * @return The peripheral or NULL if it wasn't found.
+ */
+ virtual PeripheralPtr GetPeripheral(const std::string& strLocation) const;
+
+ /*!
+ * @brief Check whether a peripheral is present at the given location.
+ * @param strLocation The location.
+ * @return True when a peripheral was found, false otherwise.
+ */
+ virtual bool HasPeripheral(const std::string& strLocation) const;
+
+ /*!
+ * @brief Check if the bus supports the given feature
+ * @param feature The feature to check for
+ * @return True if the bus supports the feature, false otherwise
+ */
+ virtual bool SupportsFeature(PeripheralFeature feature) const { return false; }
+
+ /*!
+ * @brief Get all peripheral instances that have the given feature.
+ * @param results The list of results.
+ * @param feature The feature to search for.
+ * @return The number of devices that have been found.
+ */
+ virtual unsigned int GetPeripheralsWithFeature(PeripheralVector& results,
+ const PeripheralFeature feature) const;
+
+ virtual unsigned int GetNumberOfPeripherals() const;
+ virtual unsigned int GetNumberOfPeripheralsWithId(const int iVendorId,
+ const int iProductId) const;
+
+ /*!
+ * @brief Get all features that are supported by devices on this bus.
+ * @param features All features.
+ */
+ virtual void GetFeatures(std::vector<PeripheralFeature>& features) const;
+
+ /*!
+ * @brief Check whether there is at least one device present with the given feature.
+ * @param feature The feature to check for.
+ * @return True when at least one device was found with this feature, false otherwise.
+ */
+ virtual bool HasFeature(const PeripheralFeature feature) const;
+
+ /*!
+ * @brief Callback method for when a device has been added. Will perform a device scan.
+ * @param strLocation The location of the device that has been added.
+ */
+ virtual void OnDeviceAdded(const std::string& strLocation);
+
+ /*!
+ * @brief Callback method for when a device has been changed. Will perform a device scan.
+ * @param strLocation The location of the device that has been changed.
+ */
+ virtual void OnDeviceChanged(const std::string& strLocation);
+
+ /*!
+ * @brief Callback method for when a device has been removed. Will perform a device scan.
+ * @param strLocation The location of the device that has been removed.
+ */
+ virtual void OnDeviceRemoved(const std::string& strLocation);
+
+ /*!
+ * @brief Initialise this bus and start a polling thread if this bus needs polling.
+ */
+ virtual void Initialise(void);
+
+ /*!
+ * @brief Stop the polling thread and clear all known devices on this bus.
+ */
+ virtual void Clear(void);
+
+ /*!
+ * @brief Scan for devices.
+ */
+ virtual void TriggerDeviceScan(void);
+
+ /*!
+ * @brief Get all fileitems for a path.
+ * @param strPath The path to the directory to get the items from.
+ * @param items The item list.
+ */
+ virtual void GetDirectory(const std::string& strPath, CFileItemList& items) const;
+
+ /*!
+ * @brief Get the instance of a peripheral given it's path.
+ * @param strPath The path to the peripheral.
+ * @return The peripheral or NULL if it wasn't found.
+ */
+ virtual PeripheralPtr GetByPath(const std::string& strPath) const;
+
+ /*!
+ * @brief Register a new peripheral on this bus.
+ * @param peripheral The peripheral to register.
+ */
+ virtual void Register(const PeripheralPtr& peripheral);
+
+ virtual bool FindComPort(std::string& strLocation) { return false; }
+
+ /*!
+ * \brief Poll for events
+ */
+ virtual void ProcessEvents(void) {}
+
+ /*!
+ * \brief Initialize button mapping
+ * \return True if button mapping is enabled for this bus
+ */
+ virtual void EnableButtonMapping() {}
+
+ /*!
+ * \brief Power off the specified device
+ * \param strLocation The device's location
+ */
+ virtual void PowerOff(const std::string& strLocation) {}
+
+protected:
+ void Process(void) override;
+ virtual bool ScanForDevices(void);
+ virtual void UnregisterRemovedDevices(const PeripheralScanResults& results);
+ virtual void RegisterNewDevices(const PeripheralScanResults& results);
+
+ /*!
+ * @brief Scan for devices on this bus and add them to the results list. This will have to be
+ * implemented for each bus.
+ * @param results The result list.
+ * @return True when the scan was successful, false otherwise.
+ */
+ virtual bool PerformDeviceScan(PeripheralScanResults& results) = 0;
+
+ PeripheralVector m_peripherals;
+ std::chrono::milliseconds m_iRescanTime;
+ bool m_bNeedsPolling; /*!< true when this bus needs to be polled for new devices, false when it
+ uses callbacks to notify this bus of changed */
+ CPeripherals& m_manager;
+ const PeripheralBusType m_type;
+ mutable CCriticalSection m_critSection;
+ CEvent m_triggerEvent;
+};
+using PeripheralBusPtr = std::shared_ptr<CPeripheralBus>;
+using PeripheralBusVector = std::vector<PeripheralBusPtr>;
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/bus/PeripheralBusUSB.h b/xbmc/peripherals/bus/PeripheralBusUSB.h
new file mode 100644
index 0000000..dd32ff6
--- /dev/null
+++ b/xbmc/peripherals/bus/PeripheralBusUSB.h
@@ -0,0 +1,29 @@
+/*
+ * 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
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+#define HAVE_PERIPHERAL_BUS_USB 1
+#include "platform/win32/peripherals/PeripheralBusUSB.h"
+#elif defined(TARGET_WINDOWS_STORE)
+#define HAVE_PERIPHERAL_BUS_USB 1
+#include "platform/win10/peripherals/PeripheralBusUSB.h"
+#elif defined(TARGET_LINUX) && defined(HAVE_LIBUDEV)
+#define HAVE_PERIPHERAL_BUS_USB 1
+#include "platform/linux/peripherals/PeripheralBusUSBLibUdev.h"
+#elif defined(TARGET_LINUX) && defined(HAVE_LIBUSB)
+#define HAVE_PERIPHERAL_BUS_USB 1
+#include "platform/linux/peripherals/PeripheralBusUSBLibUSB.h"
+#elif defined(TARGET_FREEBSD) && defined(HAVE_LIBUSB)
+#define HAVE_PERIPHERAL_BUS_USB 1
+#include "platform/linux/peripherals/PeripheralBusUSBLibUSB.h"
+#elif defined(TARGET_DARWIN_OSX)
+#define HAVE_PERIPHERAL_BUS_USB 1
+#include "platform/darwin/osx/peripherals/PeripheralBusUSB.h"
+#endif
diff --git a/xbmc/peripherals/bus/virtual/CMakeLists.txt b/xbmc/peripherals/bus/virtual/CMakeLists.txt
new file mode 100644
index 0000000..bf59695
--- /dev/null
+++ b/xbmc/peripherals/bus/virtual/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(SOURCES PeripheralBusAddon.cpp
+ PeripheralBusApplication.cpp)
+
+set(HEADERS PeripheralBusAddon.h
+ PeripheralBusApplication.h)
+
+if(CEC_FOUND)
+ list(APPEND SOURCES PeripheralBusCEC.cpp)
+ list(APPEND HEADERS PeripheralBusCEC.h)
+endif()
+
+core_add_library(peripheral_bus_virtual)
diff --git a/xbmc/peripherals/bus/virtual/PeripheralBusAddon.cpp b/xbmc/peripherals/bus/virtual/PeripheralBusAddon.cpp
new file mode 100644
index 0000000..2162c45
--- /dev/null
+++ b/xbmc/peripherals/bus/virtual/PeripheralBusAddon.cpp
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2014-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 "PeripheralBusAddon.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "peripherals/Peripherals.h"
+#include "peripherals/addons/PeripheralAddon.h"
+#include "peripherals/devices/PeripheralJoystick.h"
+#include "threads/SingleLock.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+
+using namespace KODI;
+using namespace PERIPHERALS;
+
+CPeripheralBusAddon::CPeripheralBusAddon(CPeripherals& manager)
+ : CPeripheralBus("PeripBusAddon", manager, PERIPHERAL_BUS_ADDON)
+{
+ using namespace ADDON;
+
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CPeripheralBusAddon::OnEvent);
+
+ UpdateAddons();
+}
+
+CPeripheralBusAddon::~CPeripheralBusAddon()
+{
+ using namespace ADDON;
+
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+
+ // stop everything before destroying any (loaded) addons
+ Clear();
+
+ // destroy any (loaded) addons
+ for (const auto& addon : m_addons)
+ addon->DestroyAddon();
+
+ m_failedAddons.clear();
+ m_addons.clear();
+}
+
+bool CPeripheralBusAddon::GetAddonWithButtonMap(PeripheralAddonPtr& addon) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ auto it = std::find_if(m_addons.begin(), m_addons.end(),
+ [](const PeripheralAddonPtr& addon) { return addon->HasButtonMaps(); });
+
+ if (it != m_addons.end())
+ {
+ addon = *it;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPeripheralBusAddon::GetAddonWithButtonMap(const CPeripheral* device,
+ PeripheralAddonPtr& addon) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // If device is from an add-on, try to use that add-on
+ if (device && device->GetBusType() == PERIPHERAL_BUS_ADDON)
+ {
+ PeripheralAddonPtr addonWithButtonMap;
+ unsigned int index;
+ if (SplitLocation(device->Location(), addonWithButtonMap, index))
+ {
+ if (addonWithButtonMap->HasButtonMaps())
+ addon = std::move(addonWithButtonMap);
+ else
+ CLog::Log(LOGDEBUG, "Add-on {} doesn't provide button maps for its controllers",
+ addonWithButtonMap->ID());
+ }
+ }
+
+ if (!addon)
+ {
+ auto it = std::find_if(m_addons.begin(), m_addons.end(),
+ [](const PeripheralAddonPtr& addon) { return addon->HasButtonMaps(); });
+
+ if (it != m_addons.end())
+ addon = *it;
+ }
+
+ return addon.get() != nullptr;
+}
+
+bool CPeripheralBusAddon::PerformDeviceScan(PeripheralScanResults& results)
+{
+ PeripheralAddonVector addons;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ addons = m_addons;
+ }
+
+ for (const auto& addon : addons)
+ addon->PerformDeviceScan(results);
+
+ // Scan during bus initialization must return true or bus gets deleted
+ return true;
+}
+
+bool CPeripheralBusAddon::InitializeProperties(CPeripheral& peripheral)
+{
+ if (!CPeripheralBus::InitializeProperties(peripheral))
+ return false;
+
+ bool bSuccess = false;
+
+ PeripheralAddonPtr addon;
+ unsigned int index;
+
+ if (SplitLocation(peripheral.Location(), addon, index))
+ {
+ switch (peripheral.Type())
+ {
+ case PERIPHERAL_JOYSTICK:
+ bSuccess =
+ addon->GetJoystickProperties(index, static_cast<CPeripheralJoystick&>(peripheral));
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return bSuccess;
+}
+
+bool CPeripheralBusAddon::SendRumbleEvent(const std::string& strLocation,
+ unsigned int motorIndex,
+ float magnitude)
+{
+ bool bHandled = false;
+
+ PeripheralAddonPtr addon;
+ unsigned int peripheralIndex;
+ if (SplitLocation(strLocation, addon, peripheralIndex))
+ bHandled = addon->SendRumbleEvent(peripheralIndex, motorIndex, magnitude);
+
+ return bHandled;
+}
+
+void CPeripheralBusAddon::ProcessEvents(void)
+{
+ PeripheralAddonVector addons;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ addons = m_addons;
+ }
+
+ for (const auto& addon : addons)
+ addon->ProcessEvents();
+}
+
+void CPeripheralBusAddon::EnableButtonMapping()
+{
+ using namespace ADDON;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ PeripheralAddonPtr dummy;
+
+ if (!GetAddonWithButtonMap(dummy))
+ {
+ std::vector<AddonInfoPtr> disabledAddons;
+ CServiceBroker::GetAddonMgr().GetDisabledAddonInfos(disabledAddons, AddonType::PERIPHERALDLL);
+ if (!disabledAddons.empty())
+ PromptEnableAddons(disabledAddons);
+ }
+}
+
+void CPeripheralBusAddon::PowerOff(const std::string& strLocation)
+{
+ PeripheralAddonPtr addon;
+ unsigned int peripheralIndex;
+ if (SplitLocation(strLocation, addon, peripheralIndex))
+ addon->PowerOffJoystick(peripheralIndex);
+}
+
+void CPeripheralBusAddon::UnregisterRemovedDevices(const PeripheralScanResults& results)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ PeripheralVector removedPeripherals;
+
+ for (const auto& addon : m_addons)
+ addon->UnregisterRemovedDevices(results, removedPeripherals);
+
+ for (const auto& peripheral : removedPeripherals)
+ m_manager.OnDeviceDeleted(*this, *peripheral);
+}
+
+void CPeripheralBusAddon::Register(const PeripheralPtr& peripheral)
+{
+ if (!peripheral)
+ return;
+
+ PeripheralAddonPtr addon;
+ unsigned int peripheralIndex;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (SplitLocation(peripheral->Location(), addon, peripheralIndex))
+ {
+ if (addon->Register(peripheralIndex, peripheral))
+ m_manager.OnDeviceAdded(*this, *peripheral);
+ }
+}
+
+void CPeripheralBusAddon::GetFeatures(std::vector<PeripheralFeature>& features) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& addon : m_addons)
+ addon->GetFeatures(features);
+}
+
+bool CPeripheralBusAddon::HasFeature(const PeripheralFeature feature) const
+{
+ bool bReturn(false);
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& addon : m_addons)
+ bReturn = bReturn || addon->HasFeature(feature);
+ return bReturn;
+}
+
+PeripheralPtr CPeripheralBusAddon::GetPeripheral(const std::string& strLocation) const
+{
+ PeripheralPtr peripheral;
+ PeripheralAddonPtr addon;
+ unsigned int peripheralIndex;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (SplitLocation(strLocation, addon, peripheralIndex))
+ peripheral = addon->GetPeripheral(peripheralIndex);
+
+ return peripheral;
+}
+
+PeripheralPtr CPeripheralBusAddon::GetByPath(const std::string& strPath) const
+{
+ PeripheralPtr result;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& addon : m_addons)
+ {
+ PeripheralPtr peripheral = addon->GetByPath(strPath);
+ if (peripheral)
+ {
+ result = peripheral;
+ break;
+ }
+ }
+
+ return result;
+}
+
+bool CPeripheralBusAddon::SupportsFeature(PeripheralFeature feature) const
+{
+ bool bSupportsFeature = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& addon : m_addons)
+ bSupportsFeature |= addon->SupportsFeature(feature);
+
+ return bSupportsFeature;
+}
+
+unsigned int CPeripheralBusAddon::GetPeripheralsWithFeature(PeripheralVector& results,
+ const PeripheralFeature feature) const
+{
+ unsigned int iReturn = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& addon : m_addons)
+ iReturn += addon->GetPeripheralsWithFeature(results, feature);
+ return iReturn;
+}
+
+unsigned int CPeripheralBusAddon::GetNumberOfPeripherals(void) const
+{
+ unsigned int iReturn = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& addon : m_addons)
+ iReturn += addon->GetNumberOfPeripherals();
+ return iReturn;
+}
+
+unsigned int CPeripheralBusAddon::GetNumberOfPeripheralsWithId(const int iVendorId,
+ const int iProductId) const
+{
+ unsigned int iReturn = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& addon : m_addons)
+ iReturn += addon->GetNumberOfPeripheralsWithId(iVendorId, iProductId);
+ return iReturn;
+}
+
+void CPeripheralBusAddon::GetDirectory(const std::string& strPath, CFileItemList& items) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& addon : m_addons)
+ addon->GetDirectory(strPath, items);
+}
+
+void CPeripheralBusAddon::OnEvent(const ADDON::AddonEvent& event)
+{
+ if (typeid(event) == typeid(ADDON::AddonEvents::Enabled) ||
+ typeid(event) == typeid(ADDON::AddonEvents::ReInstalled))
+ {
+ if (CServiceBroker::GetAddonMgr().HasType(event.addonId, ADDON::AddonType::PERIPHERALDLL))
+ UpdateAddons();
+ }
+ else if (typeid(event) == typeid(ADDON::AddonEvents::Disabled))
+ {
+ if (CServiceBroker::GetAddonMgr().HasType(event.addonId, ADDON::AddonType::PERIPHERALDLL))
+ UnRegisterAddon(event.addonId);
+ }
+ else if (typeid(event) == typeid(ADDON::AddonEvents::UnInstalled))
+ {
+ UnRegisterAddon(event.addonId);
+ }
+}
+
+bool CPeripheralBusAddon::SplitLocation(const std::string& strLocation,
+ PeripheralAddonPtr& addon,
+ unsigned int& peripheralIndex) const
+{
+ std::vector<std::string> parts = StringUtils::Split(strLocation, "/");
+ if (parts.size() == 2)
+ {
+ addon.reset();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::string& strAddonId = parts[0];
+ for (const auto& addonIt : m_addons)
+ {
+ if (addonIt->ID() == strAddonId)
+ {
+ addon = addonIt;
+ break;
+ }
+ }
+
+ if (addon)
+ {
+ const char* strJoystickIndex = parts[1].c_str();
+ char* p = NULL;
+ peripheralIndex = strtol(strJoystickIndex, &p, 10);
+ if (strJoystickIndex != p)
+ return true;
+ }
+ }
+ return false;
+}
+
+void CPeripheralBusAddon::UpdateAddons(void)
+{
+ using namespace ADDON;
+
+ auto GetPeripheralAddonID = [](const PeripheralAddonPtr& addon) { return addon->ID(); };
+ auto GetAddonID = [](const AddonInfoPtr& addon) { return addon->ID(); };
+
+ std::set<std::string> currentIds;
+ std::set<std::string> newIds;
+
+ std::set<std::string> added;
+ std::set<std::string> removed;
+
+ // Get new add-ons
+ std::vector<AddonInfoPtr> newAddons;
+ CServiceBroker::GetAddonMgr().GetAddonInfos(newAddons, true, AddonType::PERIPHERALDLL);
+ std::transform(newAddons.begin(), newAddons.end(), std::inserter(newIds, newIds.end()),
+ GetAddonID);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // Get current add-ons
+ std::transform(m_addons.begin(), m_addons.end(), std::inserter(currentIds, currentIds.end()),
+ GetPeripheralAddonID);
+ std::transform(m_failedAddons.begin(), m_failedAddons.end(),
+ std::inserter(currentIds, currentIds.end()), GetPeripheralAddonID);
+
+ // Differences
+ std::set_difference(newIds.begin(), newIds.end(), currentIds.begin(), currentIds.end(),
+ std::inserter(added, added.end()));
+ std::set_difference(currentIds.begin(), currentIds.end(), newIds.begin(), newIds.end(),
+ std::inserter(removed, removed.end()));
+
+ // Register new add-ons
+ for (const std::string& addonId : added)
+ {
+ CLog::Log(LOGDEBUG, "Add-on bus: Registering add-on {}", addonId);
+
+ auto GetAddon = [&addonId](const AddonInfoPtr& addon) { return addon->ID() == addonId; };
+
+ auto it = std::find_if(newAddons.begin(), newAddons.end(), GetAddon);
+ if (it != newAddons.end())
+ {
+ PeripheralAddonPtr newAddon = std::make_shared<CPeripheralAddon>(*it, m_manager);
+ if (newAddon)
+ {
+ bool bCreated;
+
+ {
+ CSingleExit exit(m_critSection);
+ bCreated = newAddon->CreateAddon();
+ }
+
+ if (bCreated)
+ m_addons.emplace_back(std::move(newAddon));
+ else
+ m_failedAddons.emplace_back(std::move(newAddon));
+ }
+ }
+ }
+
+ // Destroy removed add-ons
+ for (const std::string& addonId : removed)
+ {
+ UnRegisterAddon(addonId);
+ }
+}
+
+void CPeripheralBusAddon::UnRegisterAddon(const std::string& addonId)
+{
+ PeripheralAddonPtr erased;
+ auto ErasePeripheralAddon = [&addonId, &erased](const PeripheralAddonPtr& addon) {
+ if (addon->ID() == addonId)
+ {
+ erased = addon;
+ return true;
+ }
+ return false;
+ };
+
+ m_addons.erase(std::remove_if(m_addons.begin(), m_addons.end(), ErasePeripheralAddon),
+ m_addons.end());
+ if (!erased)
+ m_failedAddons.erase(
+ std::remove_if(m_failedAddons.begin(), m_failedAddons.end(), ErasePeripheralAddon),
+ m_failedAddons.end());
+
+ if (erased)
+ {
+ CLog::Log(LOGDEBUG, "Add-on bus: Unregistered add-on {}", addonId);
+ CSingleExit exit(m_critSection);
+ erased->DestroyAddon();
+ }
+}
+
+void CPeripheralBusAddon::PromptEnableAddons(
+ const std::vector<std::shared_ptr<ADDON::CAddonInfo>>& disabledAddons)
+{
+ using namespace ADDON;
+ using namespace MESSAGING::HELPERS;
+
+ // True if the user confirms enabling the disabled peripheral add-on
+ bool bAccepted = false;
+
+ auto itAddon =
+ std::find_if(disabledAddons.begin(), disabledAddons.end(), [](const AddonInfoPtr& addonInfo) {
+ return CPeripheralAddon::ProvidesJoysticks(addonInfo);
+ });
+
+ if (itAddon != disabledAddons.end())
+ {
+ // "Unable to configure controllers"
+ // "Controller configuration depends on a disabled add-on. Would you like to enable it?"
+ bAccepted =
+ (ShowYesNoDialogLines(CVariant{35017}, CVariant{35018}) == DialogResponse::CHOICE_YES);
+ }
+
+ if (bAccepted)
+ {
+ for (const auto& addonInfo : disabledAddons)
+ {
+ if (CPeripheralAddon::ProvidesJoysticks(addonInfo))
+ CServiceBroker::GetAddonMgr().EnableAddon(addonInfo->ID());
+ }
+ }
+}
diff --git a/xbmc/peripherals/bus/virtual/PeripheralBusAddon.h b/xbmc/peripherals/bus/virtual/PeripheralBusAddon.h
new file mode 100644
index 0000000..696b189
--- /dev/null
+++ b/xbmc/peripherals/bus/virtual/PeripheralBusAddon.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2014-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 "peripherals/PeripheralTypes.h"
+#include "peripherals/bus/PeripheralBus.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace ADDON
+{
+struct AddonEvent;
+class CAddonInfo;
+} // namespace ADDON
+
+namespace PERIPHERALS
+{
+class CPeripheralBusAddon : public CPeripheralBus
+{
+public:
+ explicit CPeripheralBusAddon(CPeripherals& manager);
+ ~CPeripheralBusAddon(void) override;
+
+ void UpdateAddons(void);
+
+ /*!
+ * \brief Get peripheral add-on that can provide button maps
+ */
+ bool GetAddonWithButtonMap(PeripheralAddonPtr& addon) const;
+
+ /*!
+ * \brief Get peripheral add-on that can provide button maps for the given device
+ */
+ bool GetAddonWithButtonMap(const CPeripheral* device, PeripheralAddonPtr& addon) const;
+
+ /*!
+ * \brief Set the rumble state of a rumble motor
+ *
+ * \param strLocation The location of the peripheral with the motor
+ * \param motorIndex The index of the motor being rumbled
+ * \param magnitude The amount of vibration in the closed interval [0.0, 1.0]
+ *
+ * \return true if the rumble motor's state is set, false otherwise
+ *
+ * TODO: Move declaration to parent class
+ */
+ bool SendRumbleEvent(const std::string& strLocation, unsigned int motorIndex, float magnitude);
+
+ // Inherited from CPeripheralBus
+ bool InitializeProperties(CPeripheral& peripheral) override;
+ void Register(const PeripheralPtr& peripheral) override;
+ void GetFeatures(std::vector<PeripheralFeature>& features) const override;
+ bool HasFeature(const PeripheralFeature feature) const override;
+ PeripheralPtr GetPeripheral(const std::string& strLocation) const override;
+ PeripheralPtr GetByPath(const std::string& strPath) const override;
+ bool SupportsFeature(PeripheralFeature feature) const override;
+ unsigned int GetPeripheralsWithFeature(PeripheralVector& results,
+ const PeripheralFeature feature) const override;
+ unsigned int GetNumberOfPeripherals(void) const override;
+ unsigned int GetNumberOfPeripheralsWithId(const int iVendorId,
+ const int iProductId) const override;
+ void GetDirectory(const std::string& strPath, CFileItemList& items) const override;
+ void ProcessEvents(void) override;
+ void EnableButtonMapping() override;
+ void PowerOff(const std::string& strLocation) override;
+
+ bool SplitLocation(const std::string& strLocation,
+ PeripheralAddonPtr& addon,
+ unsigned int& peripheralIndex) const;
+
+protected:
+ // Inherited from CPeripheralBus
+ bool PerformDeviceScan(PeripheralScanResults& results) override;
+ void UnregisterRemovedDevices(const PeripheralScanResults& results) override;
+
+private:
+ void OnEvent(const ADDON::AddonEvent& event);
+ void UnRegisterAddon(const std::string& addonId);
+
+ void PromptEnableAddons(const std::vector<std::shared_ptr<ADDON::CAddonInfo>>& disabledAddons);
+
+ PeripheralAddonVector m_addons;
+ PeripheralAddonVector m_failedAddons;
+};
+using PeripheralBusAddonPtr = std::shared_ptr<CPeripheralBusAddon>;
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/bus/virtual/PeripheralBusApplication.cpp b/xbmc/peripherals/bus/virtual/PeripheralBusApplication.cpp
new file mode 100644
index 0000000..4dad03c
--- /dev/null
+++ b/xbmc/peripherals/bus/virtual/PeripheralBusApplication.cpp
@@ -0,0 +1,83 @@
+/*
+ * 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 "PeripheralBusApplication.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+
+using namespace PERIPHERALS;
+
+CPeripheralBusApplication::CPeripheralBusApplication(CPeripherals& manager)
+ : CPeripheralBus("PeripBusApplication", manager, PERIPHERAL_BUS_APPLICATION)
+{
+ // Initialize CPeripheralBus
+ m_bNeedsPolling = false;
+}
+
+void CPeripheralBusApplication::Initialise(void)
+{
+ CPeripheralBus::Initialise();
+ TriggerDeviceScan();
+}
+
+bool CPeripheralBusApplication::PerformDeviceScan(PeripheralScanResults& results)
+{
+ {
+ PeripheralScanResult result(Type());
+ result.m_type = PERIPHERAL_KEYBOARD;
+ result.m_strDeviceName = g_localizeStrings.Get(35150); // "Keyboard"
+ result.m_strLocation = PeripheralTypeTranslator::TypeToString(PERIPHERAL_KEYBOARD);
+ result.m_iVendorId = 0;
+ result.m_iProductId = 0;
+ result.m_mappedType = PERIPHERAL_KEYBOARD;
+ result.m_mappedBusType = Type();
+ result.m_iSequence = 0;
+
+ if (!results.ContainsResult(result))
+ results.m_results.push_back(result);
+ }
+
+ bool bHasMouse = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_INPUT_ENABLEMOUSE);
+
+ //! @todo Fix game clients to handle mouse disconnecting
+ //! For now mouse is always connected
+ bHasMouse = true;
+
+ if (bHasMouse)
+ {
+ PeripheralScanResult result(Type());
+ result.m_type = PERIPHERAL_MOUSE;
+ result.m_strDeviceName = g_localizeStrings.Get(35171); // "Mouse"
+ result.m_strLocation = PeripheralTypeTranslator::TypeToString(PERIPHERAL_MOUSE);
+ result.m_iVendorId = 0;
+ result.m_iProductId = 0;
+ result.m_mappedType = PERIPHERAL_MOUSE;
+ result.m_mappedBusType = Type();
+ result.m_iSequence = 0;
+
+ if (!results.ContainsResult(result))
+ results.m_results.push_back(result);
+ }
+
+ return true;
+}
+
+void CPeripheralBusApplication::GetDirectory(const std::string& strPath, CFileItemList& items) const
+{
+ // Don't list virtual devices in the GUI
+}
+
+std::string CPeripheralBusApplication::MakeLocation(unsigned int controllerIndex) const
+{
+ return std::to_string(controllerIndex);
+}
diff --git a/xbmc/peripherals/bus/virtual/PeripheralBusApplication.h b/xbmc/peripherals/bus/virtual/PeripheralBusApplication.h
new file mode 100644
index 0000000..4fcc40d
--- /dev/null
+++ b/xbmc/peripherals/bus/virtual/PeripheralBusApplication.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "peripherals/bus/PeripheralBus.h"
+
+namespace PERIPHERALS
+{
+/*!
+ * @class CPeripheralBusApplication
+ *
+ * This exposes peripherals that exist logically at the application level,
+ * such as emulated joysticks.
+ */
+class CPeripheralBusApplication : public CPeripheralBus
+{
+public:
+ explicit CPeripheralBusApplication(CPeripherals& manager);
+ ~CPeripheralBusApplication(void) override = default;
+
+ // implementation of CPeripheralBus
+ void Initialise(void) override;
+ void GetDirectory(const std::string& strPath, CFileItemList& items) const override;
+
+ /*!
+ * \brief Get the location for the specified controller index
+ */
+ std::string MakeLocation(unsigned int controllerIndex) const;
+
+protected:
+ // implementation of CPeripheralBus
+ bool PerformDeviceScan(PeripheralScanResults& results) override;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/bus/virtual/PeripheralBusCEC.cpp b/xbmc/peripherals/bus/virtual/PeripheralBusCEC.cpp
new file mode 100644
index 0000000..e119638
--- /dev/null
+++ b/xbmc/peripherals/bus/virtual/PeripheralBusCEC.cpp
@@ -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.
+ */
+
+#include "PeripheralBusCEC.h"
+
+#include <libcec/cec.h>
+
+using namespace PERIPHERALS;
+using namespace CEC;
+
+CPeripheralBusCEC::CPeripheralBusCEC(CPeripherals& manager)
+ : CPeripheralBus("PeripBusCEC", manager, PERIPHERAL_BUS_CEC)
+{
+ m_cecAdapter = CECInitialise(&m_configuration);
+}
+
+CPeripheralBusCEC::~CPeripheralBusCEC(void)
+{
+ if (m_cecAdapter)
+ CECDestroy(m_cecAdapter);
+}
+
+bool CPeripheralBusCEC::PerformDeviceScan(PeripheralScanResults& results)
+{
+ cec_adapter_descriptor deviceList[10];
+ int8_t iFound = m_cecAdapter->DetectAdapters(deviceList, 10, NULL, true);
+
+ for (uint8_t iDevicePtr = 0; iDevicePtr < iFound; iDevicePtr++)
+ {
+ PeripheralScanResult result(m_type);
+ result.m_iVendorId = deviceList[iDevicePtr].iVendorId;
+ result.m_iProductId = deviceList[iDevicePtr].iProductId;
+ result.m_strLocation = deviceList[iDevicePtr].strComName;
+ result.m_type = PERIPHERAL_CEC;
+
+ // override the bus type, so users don't have to reconfigure their adapters
+ switch (deviceList[iDevicePtr].adapterType)
+ {
+ case ADAPTERTYPE_P8_EXTERNAL:
+ case ADAPTERTYPE_P8_DAUGHTERBOARD:
+ result.m_mappedBusType = PERIPHERAL_BUS_USB;
+ break;
+ default:
+ break;
+ }
+
+ result.m_iSequence = GetNumberOfPeripheralsWithId(result.m_iVendorId, result.m_iProductId);
+ if (!results.ContainsResult(result))
+ results.m_results.push_back(result);
+ }
+ return true;
+}
diff --git a/xbmc/peripherals/bus/virtual/PeripheralBusCEC.h b/xbmc/peripherals/bus/virtual/PeripheralBusCEC.h
new file mode 100644
index 0000000..9aa2643
--- /dev/null
+++ b/xbmc/peripherals/bus/virtual/PeripheralBusCEC.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "peripherals/bus/PeripheralBus.h"
+
+// undefine macro isset, it collides with function in cectypes.h
+#ifdef isset
+#undef isset
+#endif
+#include <libcec/cectypes.h>
+
+namespace CEC
+{
+class ICECAdapter;
+}
+
+namespace PERIPHERALS
+{
+class CPeripherals;
+
+class CPeripheralBusCEC : public CPeripheralBus
+{
+public:
+ explicit CPeripheralBusCEC(CPeripherals& manager);
+ ~CPeripheralBusCEC(void) override;
+
+ /*!
+ * @see PeripheralBus::PerformDeviceScan()
+ */
+ bool PerformDeviceScan(PeripheralScanResults& results) override;
+
+private:
+ CEC::ICECAdapter* m_cecAdapter;
+ CEC::libcec_configuration m_configuration;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/devices/CMakeLists.txt b/xbmc/peripherals/devices/CMakeLists.txt
new file mode 100644
index 0000000..57795f5
--- /dev/null
+++ b/xbmc/peripherals/devices/CMakeLists.txt
@@ -0,0 +1,30 @@
+set(SOURCES Peripheral.cpp
+ PeripheralBluetooth.cpp
+ PeripheralDisk.cpp
+ PeripheralHID.cpp
+ PeripheralImon.cpp
+ PeripheralJoystick.cpp
+ PeripheralKeyboard.cpp
+ PeripheralMouse.cpp
+ PeripheralNIC.cpp
+ PeripheralNyxboard.cpp
+ PeripheralTuner.cpp)
+
+set(HEADERS Peripheral.h
+ PeripheralBluetooth.h
+ PeripheralDisk.h
+ PeripheralHID.h
+ PeripheralImon.h
+ PeripheralJoystick.h
+ PeripheralKeyboard.h
+ PeripheralMouse.h
+ PeripheralNIC.h
+ PeripheralNyxboard.h
+ PeripheralTuner.h)
+
+if(CEC_FOUND)
+ list(APPEND SOURCES PeripheralCecAdapter.cpp)
+ list(APPEND HEADERS PeripheralCecAdapter.h)
+endif()
+
+core_add_library(peripherals_devices)
diff --git a/xbmc/peripherals/devices/Peripheral.cpp b/xbmc/peripherals/devices/Peripheral.cpp
new file mode 100644
index 0000000..6c3f779
--- /dev/null
+++ b/xbmc/peripherals/devices/Peripheral.cpp
@@ -0,0 +1,771 @@
+/*
+ * 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 "Peripheral.h"
+
+#include "Util.h"
+#include "XBDateTime.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerLayout.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/joysticks/interfaces/IInputHandler.h"
+#include "peripherals/Peripherals.h"
+#include "peripherals/addons/AddonButtonMapping.h"
+#include "peripherals/addons/AddonInputHandling.h"
+#include "peripherals/addons/PeripheralAddon.h"
+#include "peripherals/bus/PeripheralBus.h"
+#include "peripherals/bus/virtual/PeripheralBusAddon.h"
+#include "settings/SettingAddon.h"
+#include "settings/lib/Setting.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <utility>
+
+using namespace KODI;
+using namespace JOYSTICK;
+using namespace PERIPHERALS;
+
+struct SortBySettingsOrder
+{
+ bool operator()(const PeripheralDeviceSetting& left, const PeripheralDeviceSetting& right)
+ {
+ return left.m_order < right.m_order;
+ }
+};
+
+CPeripheral::CPeripheral(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : m_manager(manager),
+ m_type(scanResult.m_mappedType),
+ m_busType(scanResult.m_busType),
+ m_mappedBusType(scanResult.m_mappedBusType),
+ m_strLocation(scanResult.m_strLocation),
+ m_strDeviceName(scanResult.m_strDeviceName),
+ m_iVendorId(scanResult.m_iVendorId),
+ m_iProductId(scanResult.m_iProductId),
+ m_strVersionInfo(g_localizeStrings.Get(13205)), // "unknown"
+ m_bInitialised(false),
+ m_bHidden(false),
+ m_bError(false),
+ m_bus(bus)
+{
+ PeripheralTypeTranslator::FormatHexString(scanResult.m_iVendorId, m_strVendorId);
+ PeripheralTypeTranslator::FormatHexString(scanResult.m_iProductId, m_strProductId);
+ if (scanResult.m_iSequence > 0)
+ {
+ m_strFileLocation =
+ StringUtils::Format("peripherals://{}/{}_{}.dev",
+ PeripheralTypeTranslator::BusTypeToString(scanResult.m_busType),
+ scanResult.m_strLocation, scanResult.m_iSequence);
+ }
+ else
+ {
+ m_strFileLocation = StringUtils::Format(
+ "peripherals://{}/{}.dev", PeripheralTypeTranslator::BusTypeToString(scanResult.m_busType),
+ scanResult.m_strLocation);
+ }
+}
+
+CPeripheral::~CPeripheral(void)
+{
+ PersistSettings(true);
+
+ m_subDevices.clear();
+
+ ClearSettings();
+}
+
+bool CPeripheral::operator==(const CPeripheral& right) const
+{
+ return m_type == right.m_type && m_strLocation == right.m_strLocation &&
+ m_iVendorId == right.m_iVendorId && m_iProductId == right.m_iProductId;
+}
+
+bool CPeripheral::operator!=(const CPeripheral& right) const
+{
+ return !(*this == right);
+}
+
+bool CPeripheral::HasFeature(const PeripheralFeature feature) const
+{
+ bool bReturn(false);
+
+ for (unsigned int iFeaturePtr = 0; iFeaturePtr < m_features.size(); iFeaturePtr++)
+ {
+ if (m_features.at(iFeaturePtr) == feature)
+ {
+ bReturn = true;
+ break;
+ }
+ }
+
+ if (!bReturn)
+ {
+ for (unsigned int iSubdevicePtr = 0; iSubdevicePtr < m_subDevices.size(); iSubdevicePtr++)
+ {
+ if (m_subDevices.at(iSubdevicePtr)->HasFeature(feature))
+ {
+ bReturn = true;
+ break;
+ }
+ }
+ }
+
+ return bReturn;
+}
+
+void CPeripheral::GetFeatures(std::vector<PeripheralFeature>& features) const
+{
+ for (unsigned int iFeaturePtr = 0; iFeaturePtr < m_features.size(); iFeaturePtr++)
+ features.push_back(m_features.at(iFeaturePtr));
+
+ for (unsigned int iSubdevicePtr = 0; iSubdevicePtr < m_subDevices.size(); iSubdevicePtr++)
+ m_subDevices.at(iSubdevicePtr)->GetFeatures(features);
+}
+
+bool CPeripheral::Initialise(void)
+{
+ bool bReturn(false);
+
+ if (m_bError)
+ return bReturn;
+
+ bReturn = true;
+ if (m_bInitialised)
+ return bReturn;
+
+ m_manager.GetSettingsFromMapping(*this);
+
+ std::string safeDeviceName = m_strDeviceName;
+ StringUtils::Replace(safeDeviceName, ' ', '_');
+
+ if (m_iVendorId == 0x0000 && m_iProductId == 0x0000)
+ {
+ m_strSettingsFile =
+ StringUtils::Format("special://profile/peripheral_data/{}_{}.xml",
+ PeripheralTypeTranslator::BusTypeToString(m_mappedBusType),
+ CUtil::MakeLegalFileName(safeDeviceName, LEGAL_WIN32_COMPAT));
+ }
+ else
+ {
+ // Backwards compatibility - old settings files didn't include the device name
+ m_strSettingsFile = StringUtils::Format(
+ "special://profile/peripheral_data/{}_{}_{}.xml",
+ PeripheralTypeTranslator::BusTypeToString(m_mappedBusType), m_strVendorId, m_strProductId);
+
+ if (!CFileUtils::Exists(m_strSettingsFile))
+ m_strSettingsFile = StringUtils::Format(
+ "special://profile/peripheral_data/{}_{}_{}_{}.xml",
+ PeripheralTypeTranslator::BusTypeToString(m_mappedBusType), m_strVendorId, m_strProductId,
+ CUtil::MakeLegalFileName(safeDeviceName, LEGAL_WIN32_COMPAT));
+ }
+
+ LoadPersistedSettings();
+
+ for (unsigned int iFeaturePtr = 0; iFeaturePtr < m_features.size(); iFeaturePtr++)
+ {
+ PeripheralFeature feature = m_features.at(iFeaturePtr);
+ bReturn &= InitialiseFeature(feature);
+ }
+
+ for (unsigned int iSubdevicePtr = 0; iSubdevicePtr < m_subDevices.size(); iSubdevicePtr++)
+ bReturn &= m_subDevices.at(iSubdevicePtr)->Initialise();
+
+ if (bReturn)
+ {
+ CLog::Log(LOGDEBUG, "{} - initialised peripheral on '{}' with {} features and {} sub devices",
+ __FUNCTION__, m_strLocation, (int)m_features.size(), (int)m_subDevices.size());
+ m_bInitialised = true;
+ }
+
+ return bReturn;
+}
+
+void CPeripheral::GetSubdevices(PeripheralVector& subDevices) const
+{
+ subDevices = m_subDevices;
+}
+
+bool CPeripheral::IsMultiFunctional(void) const
+{
+ return m_subDevices.size() > 0;
+}
+
+std::vector<std::shared_ptr<CSetting>> CPeripheral::GetSettings(void) const
+{
+ std::vector<PeripheralDeviceSetting> tmpSettings;
+ tmpSettings.reserve(m_settings.size());
+ for (const auto& it : m_settings)
+ tmpSettings.push_back(it.second);
+ sort(tmpSettings.begin(), tmpSettings.end(), SortBySettingsOrder());
+
+ std::vector<std::shared_ptr<CSetting>> settings;
+ settings.reserve(tmpSettings.size());
+ for (const auto& it : tmpSettings)
+ settings.push_back(it.m_setting);
+ return settings;
+}
+
+void CPeripheral::AddSetting(const std::string& strKey, const SettingConstPtr& setting, int order)
+{
+ if (!setting)
+ {
+ CLog::Log(LOGERROR, "{} - invalid setting", __FUNCTION__);
+ return;
+ }
+
+ if (!HasSetting(strKey))
+ {
+ PeripheralDeviceSetting deviceSetting = {NULL, order};
+ switch (setting->GetType())
+ {
+ case SettingType::Boolean:
+ {
+ std::shared_ptr<const CSettingBool> mappedSetting =
+ std::static_pointer_cast<const CSettingBool>(setting);
+ std::shared_ptr<CSettingBool> boolSetting =
+ std::make_shared<CSettingBool>(strKey, *mappedSetting);
+ if (boolSetting)
+ {
+ boolSetting->SetVisible(mappedSetting->IsVisible());
+ deviceSetting.m_setting = boolSetting;
+ }
+ }
+ break;
+ case SettingType::Integer:
+ {
+ std::shared_ptr<const CSettingInt> mappedSetting =
+ std::static_pointer_cast<const CSettingInt>(setting);
+ std::shared_ptr<CSettingInt> intSetting =
+ std::make_shared<CSettingInt>(strKey, *mappedSetting);
+ if (intSetting)
+ {
+ intSetting->SetVisible(mappedSetting->IsVisible());
+ deviceSetting.m_setting = intSetting;
+ }
+ }
+ break;
+ case SettingType::Number:
+ {
+ std::shared_ptr<const CSettingNumber> mappedSetting =
+ std::static_pointer_cast<const CSettingNumber>(setting);
+ std::shared_ptr<CSettingNumber> floatSetting =
+ std::make_shared<CSettingNumber>(strKey, *mappedSetting);
+ if (floatSetting)
+ {
+ floatSetting->SetVisible(mappedSetting->IsVisible());
+ deviceSetting.m_setting = floatSetting;
+ }
+ }
+ break;
+ case SettingType::String:
+ {
+ if (std::dynamic_pointer_cast<const CSettingAddon>(setting))
+ {
+ std::shared_ptr<const CSettingAddon> mappedSetting =
+ std::static_pointer_cast<const CSettingAddon>(setting);
+ std::shared_ptr<CSettingAddon> addonSetting =
+ std::make_shared<CSettingAddon>(strKey, *mappedSetting);
+ addonSetting->SetVisible(mappedSetting->IsVisible());
+ deviceSetting.m_setting = addonSetting;
+ }
+ else
+ {
+ std::shared_ptr<const CSettingString> mappedSetting =
+ std::static_pointer_cast<const CSettingString>(setting);
+ std::shared_ptr<CSettingString> stringSetting =
+ std::make_shared<CSettingString>(strKey, *mappedSetting);
+ stringSetting->SetVisible(mappedSetting->IsVisible());
+ deviceSetting.m_setting = stringSetting;
+ }
+ }
+ break;
+ default:
+ //! @todo add more types if needed
+ break;
+ }
+
+ if (deviceSetting.m_setting != NULL)
+ m_settings.insert(make_pair(strKey, deviceSetting));
+ }
+}
+
+bool CPeripheral::HasSetting(const std::string& strKey) const
+{
+ std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey);
+ return it != m_settings.end();
+}
+
+bool CPeripheral::HasSettings(void) const
+{
+ return !m_settings.empty();
+}
+
+bool CPeripheral::HasConfigurableSettings(void) const
+{
+ bool bReturn(false);
+ std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.begin();
+ while (it != m_settings.end() && !bReturn)
+ {
+ if ((*it).second.m_setting->IsVisible())
+ {
+ bReturn = true;
+ break;
+ }
+
+ ++it;
+ }
+
+ return bReturn;
+}
+
+bool CPeripheral::GetSettingBool(const std::string& strKey) const
+{
+ std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey);
+ if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Boolean)
+ {
+ std::shared_ptr<CSettingBool> boolSetting =
+ std::static_pointer_cast<CSettingBool>((*it).second.m_setting);
+ if (boolSetting)
+ return boolSetting->GetValue();
+ }
+
+ return false;
+}
+
+int CPeripheral::GetSettingInt(const std::string& strKey) const
+{
+ std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey);
+ if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Integer)
+ {
+ std::shared_ptr<CSettingInt> intSetting =
+ std::static_pointer_cast<CSettingInt>((*it).second.m_setting);
+ if (intSetting)
+ return intSetting->GetValue();
+ }
+
+ return 0;
+}
+
+float CPeripheral::GetSettingFloat(const std::string& strKey) const
+{
+ std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey);
+ if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Number)
+ {
+ std::shared_ptr<CSettingNumber> floatSetting =
+ std::static_pointer_cast<CSettingNumber>((*it).second.m_setting);
+ if (floatSetting)
+ return (float)floatSetting->GetValue();
+ }
+
+ return 0;
+}
+
+const std::string CPeripheral::GetSettingString(const std::string& strKey) const
+{
+ std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey);
+ if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::String)
+ {
+ std::shared_ptr<CSettingString> stringSetting =
+ std::static_pointer_cast<CSettingString>((*it).second.m_setting);
+ if (stringSetting)
+ return stringSetting->GetValue();
+ }
+
+ return "";
+}
+
+bool CPeripheral::SetSetting(const std::string& strKey, bool bValue)
+{
+ bool bChanged(false);
+ std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.find(strKey);
+ if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Boolean)
+ {
+ std::shared_ptr<CSettingBool> boolSetting =
+ std::static_pointer_cast<CSettingBool>((*it).second.m_setting);
+ if (boolSetting)
+ {
+ bChanged = boolSetting->GetValue() != bValue;
+ boolSetting->SetValue(bValue);
+ if (bChanged && m_bInitialised)
+ m_changedSettings.insert(strKey);
+ }
+ }
+ return bChanged;
+}
+
+bool CPeripheral::SetSetting(const std::string& strKey, int iValue)
+{
+ bool bChanged(false);
+ std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.find(strKey);
+ if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Integer)
+ {
+ std::shared_ptr<CSettingInt> intSetting =
+ std::static_pointer_cast<CSettingInt>((*it).second.m_setting);
+ if (intSetting)
+ {
+ bChanged = intSetting->GetValue() != iValue;
+ intSetting->SetValue(iValue);
+ if (bChanged && m_bInitialised)
+ m_changedSettings.insert(strKey);
+ }
+ }
+ return bChanged;
+}
+
+bool CPeripheral::SetSetting(const std::string& strKey, float fValue)
+{
+ bool bChanged(false);
+ std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.find(strKey);
+ if (it != m_settings.end() && (*it).second.m_setting->GetType() == SettingType::Number)
+ {
+ std::shared_ptr<CSettingNumber> floatSetting =
+ std::static_pointer_cast<CSettingNumber>((*it).second.m_setting);
+ if (floatSetting)
+ {
+ bChanged = floatSetting->GetValue() != static_cast<double>(fValue);
+ floatSetting->SetValue(static_cast<double>(fValue));
+ if (bChanged && m_bInitialised)
+ m_changedSettings.insert(strKey);
+ }
+ }
+ return bChanged;
+}
+
+void CPeripheral::SetSettingVisible(const std::string& strKey, bool bSetTo)
+{
+ std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.find(strKey);
+ if (it != m_settings.end())
+ (*it).second.m_setting->SetVisible(bSetTo);
+}
+
+bool CPeripheral::IsSettingVisible(const std::string& strKey) const
+{
+ std::map<std::string, PeripheralDeviceSetting>::const_iterator it = m_settings.find(strKey);
+ if (it != m_settings.end())
+ return (*it).second.m_setting->IsVisible();
+ return false;
+}
+
+bool CPeripheral::SetSetting(const std::string& strKey, const std::string& strValue)
+{
+ bool bChanged(false);
+ std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.find(strKey);
+ if (it != m_settings.end())
+ {
+ if ((*it).second.m_setting->GetType() == SettingType::String)
+ {
+ std::shared_ptr<CSettingString> stringSetting =
+ std::static_pointer_cast<CSettingString>((*it).second.m_setting);
+ if (stringSetting)
+ {
+ bChanged = !StringUtils::EqualsNoCase(stringSetting->GetValue(), strValue);
+ stringSetting->SetValue(strValue);
+ if (bChanged && m_bInitialised)
+ m_changedSettings.insert(strKey);
+ }
+ }
+ else if ((*it).second.m_setting->GetType() == SettingType::Integer)
+ bChanged = SetSetting(strKey, strValue.empty() ? 0 : atoi(strValue.c_str()));
+ else if ((*it).second.m_setting->GetType() == SettingType::Number)
+ bChanged = SetSetting(strKey, (float)(strValue.empty() ? 0 : atof(strValue.c_str())));
+ else if ((*it).second.m_setting->GetType() == SettingType::Boolean)
+ bChanged = SetSetting(strKey, strValue == "1");
+ }
+ return bChanged;
+}
+
+void CPeripheral::PersistSettings(bool bExiting /* = false */)
+{
+ CXBMCTinyXML doc;
+ TiXmlElement node("settings");
+ doc.InsertEndChild(node);
+ for (const auto& itr : m_settings)
+ {
+ TiXmlElement nodeSetting("setting");
+ nodeSetting.SetAttribute("id", itr.first.c_str());
+ std::string strValue;
+ switch (itr.second.m_setting->GetType())
+ {
+ case SettingType::String:
+ {
+ std::shared_ptr<CSettingString> stringSetting =
+ std::static_pointer_cast<CSettingString>(itr.second.m_setting);
+ if (stringSetting)
+ strValue = stringSetting->GetValue();
+ }
+ break;
+ case SettingType::Integer:
+ {
+ std::shared_ptr<CSettingInt> intSetting =
+ std::static_pointer_cast<CSettingInt>(itr.second.m_setting);
+ if (intSetting)
+ strValue = std::to_string(intSetting->GetValue());
+ }
+ break;
+ case SettingType::Number:
+ {
+ std::shared_ptr<CSettingNumber> floatSetting =
+ std::static_pointer_cast<CSettingNumber>(itr.second.m_setting);
+ if (floatSetting)
+ strValue = StringUtils::Format("{:.2f}", floatSetting->GetValue());
+ }
+ break;
+ case SettingType::Boolean:
+ {
+ std::shared_ptr<CSettingBool> boolSetting =
+ std::static_pointer_cast<CSettingBool>(itr.second.m_setting);
+ if (boolSetting)
+ strValue = std::to_string(boolSetting->GetValue() ? 1 : 0);
+ }
+ break;
+ default:
+ break;
+ }
+ nodeSetting.SetAttribute("value", strValue.c_str());
+ doc.RootElement()->InsertEndChild(nodeSetting);
+ }
+
+ doc.SaveFile(m_strSettingsFile);
+
+ if (!bExiting)
+ {
+ for (const auto& it : m_changedSettings)
+ OnSettingChanged(it);
+ }
+ m_changedSettings.clear();
+}
+
+void CPeripheral::LoadPersistedSettings(void)
+{
+ CXBMCTinyXML doc;
+ if (doc.LoadFile(m_strSettingsFile))
+ {
+ const TiXmlElement* setting = doc.RootElement()->FirstChildElement("setting");
+ while (setting)
+ {
+ std::string strId = XMLUtils::GetAttribute(setting, "id");
+ std::string strValue = XMLUtils::GetAttribute(setting, "value");
+ SetSetting(strId, strValue);
+
+ setting = setting->NextSiblingElement("setting");
+ }
+ }
+}
+
+void CPeripheral::ResetDefaultSettings(void)
+{
+ ClearSettings();
+ m_manager.GetSettingsFromMapping(*this);
+
+ std::map<std::string, PeripheralDeviceSetting>::iterator it = m_settings.begin();
+ while (it != m_settings.end())
+ {
+ m_changedSettings.insert((*it).first);
+ ++it;
+ }
+
+ PersistSettings();
+}
+
+void CPeripheral::ClearSettings(void)
+{
+ m_settings.clear();
+}
+
+void CPeripheral::RegisterInputHandler(IInputHandler* handler, bool bPromiscuous)
+{
+ auto it = m_inputHandlers.find(handler);
+ if (it == m_inputHandlers.end())
+ {
+ PeripheralAddonPtr addon = m_manager.GetAddonWithButtonMap(this);
+ if (addon)
+ {
+ std::unique_ptr<CAddonInputHandling> addonInput = std::make_unique<CAddonInputHandling>(
+ m_manager, this, std::move(addon), handler, GetDriverReceiver());
+ if (addonInput->Load())
+ {
+ RegisterJoystickDriverHandler(addonInput.get(), bPromiscuous);
+ m_inputHandlers[handler] = std::move(addonInput);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Failed to locate add-on for \"{}\"", m_strLocation);
+ }
+ }
+}
+
+void CPeripheral::UnregisterInputHandler(IInputHandler* handler)
+{
+ handler->ResetInputReceiver();
+
+ auto it = m_inputHandlers.find(handler);
+ if (it != m_inputHandlers.end())
+ {
+ UnregisterJoystickDriverHandler(it->second.get());
+ m_inputHandlers.erase(it);
+ }
+}
+
+void CPeripheral::RegisterKeyboardHandler(KEYBOARD::IKeyboardInputHandler* handler,
+ bool bPromiscuous)
+{
+ auto it = m_keyboardHandlers.find(handler);
+ if (it == m_keyboardHandlers.end())
+ {
+ std::unique_ptr<KODI::KEYBOARD::IKeyboardDriverHandler> keyboardDriverHandler;
+
+ PeripheralAddonPtr addon = m_manager.GetAddonWithButtonMap(this);
+ if (addon)
+ {
+ std::unique_ptr<CAddonInputHandling> addonInput =
+ std::make_unique<CAddonInputHandling>(m_manager, this, std::move(addon), handler);
+ if (addonInput->Load())
+ keyboardDriverHandler = std::move(addonInput);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Failed to locate add-on for \"{}\"", m_strLocation);
+ }
+
+ if (keyboardDriverHandler)
+ {
+ RegisterKeyboardDriverHandler(keyboardDriverHandler.get(), bPromiscuous);
+ m_keyboardHandlers[handler] = std::move(keyboardDriverHandler);
+ }
+ }
+}
+
+void CPeripheral::UnregisterKeyboardHandler(KEYBOARD::IKeyboardInputHandler* handler)
+{
+ auto it = m_keyboardHandlers.find(handler);
+ if (it != m_keyboardHandlers.end())
+ {
+ UnregisterKeyboardDriverHandler(it->second.get());
+ m_keyboardHandlers.erase(it);
+ }
+}
+
+void CPeripheral::RegisterMouseHandler(MOUSE::IMouseInputHandler* handler, bool bPromiscuous)
+{
+ auto it = m_mouseHandlers.find(handler);
+ if (it == m_mouseHandlers.end())
+ {
+ std::unique_ptr<KODI::MOUSE::IMouseDriverHandler> mouseDriverHandler;
+
+ PeripheralAddonPtr addon = m_manager.GetAddonWithButtonMap(this);
+ if (addon)
+ {
+ std::unique_ptr<CAddonInputHandling> addonInput =
+ std::make_unique<CAddonInputHandling>(m_manager, this, std::move(addon), handler);
+ if (addonInput->Load())
+ mouseDriverHandler = std::move(addonInput);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Failed to locate add-on for \"{}\"", m_strLocation);
+ }
+
+ if (mouseDriverHandler)
+ {
+ RegisterMouseDriverHandler(mouseDriverHandler.get(), bPromiscuous);
+ m_mouseHandlers[handler] = std::move(mouseDriverHandler);
+ }
+ }
+}
+
+void CPeripheral::UnregisterMouseHandler(MOUSE::IMouseInputHandler* handler)
+{
+ auto it = m_mouseHandlers.find(handler);
+ if (it != m_mouseHandlers.end())
+ {
+ UnregisterMouseDriverHandler(it->second.get());
+ m_mouseHandlers.erase(it);
+ }
+}
+
+void CPeripheral::RegisterJoystickButtonMapper(IButtonMapper* mapper)
+{
+ auto it = m_buttonMappers.find(mapper);
+ if (it == m_buttonMappers.end())
+ {
+ std::unique_ptr<CAddonButtonMapping> addonMapping(
+ new CAddonButtonMapping(m_manager, this, mapper));
+
+ RegisterJoystickDriverHandler(addonMapping.get(), false);
+ RegisterKeyboardDriverHandler(addonMapping.get(), false);
+ RegisterMouseDriverHandler(addonMapping.get(), false);
+
+ m_buttonMappers[mapper] = std::move(addonMapping);
+ }
+}
+
+void CPeripheral::UnregisterJoystickButtonMapper(IButtonMapper* mapper)
+{
+ auto it = m_buttonMappers.find(mapper);
+ if (it != m_buttonMappers.end())
+ {
+ UnregisterMouseDriverHandler(it->second.get());
+ UnregisterKeyboardDriverHandler(it->second.get());
+ UnregisterJoystickDriverHandler(it->second.get());
+
+ m_buttonMappers.erase(it);
+ }
+}
+
+std::string CPeripheral::GetIcon() const
+{
+ std::string icon;
+
+ // Try controller profile
+ const GAME::ControllerPtr controller = ControllerProfile();
+ if (controller)
+ icon = controller->Layout().ImagePath();
+
+ // Try add-on
+ if (icon.empty() && m_busType == PERIPHERAL_BUS_ADDON)
+ {
+ CPeripheralBusAddon* bus = static_cast<CPeripheralBusAddon*>(m_bus);
+
+ PeripheralAddonPtr addon;
+ unsigned int index;
+ if (bus->SplitLocation(m_strLocation, addon, index))
+ {
+ std::string addonIcon = addon->Icon();
+ if (!addonIcon.empty())
+ icon = std::move(addonIcon);
+ }
+ }
+
+ // Fallback
+ if (icon.empty())
+ icon = "DefaultAddon.png";
+
+ return icon;
+}
+
+bool CPeripheral::operator==(const PeripheralScanResult& right) const
+{
+ return StringUtils::EqualsNoCase(m_strLocation, right.m_strLocation);
+}
+
+bool CPeripheral::operator!=(const PeripheralScanResult& right) const
+{
+ return !(*this == right);
+}
+
+CDateTime CPeripheral::LastActive()
+{
+ return CDateTime();
+}
diff --git a/xbmc/peripherals/devices/Peripheral.h b/xbmc/peripherals/devices/Peripheral.h
new file mode 100644
index 0000000..8c4049b
--- /dev/null
+++ b/xbmc/peripherals/devices/Peripheral.h
@@ -0,0 +1,306 @@
+/*
+ * 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 "games/controllers/ControllerTypes.h"
+#include "input/joysticks/interfaces/IInputProvider.h"
+#include "input/keyboard/interfaces/IKeyboardInputProvider.h"
+#include "input/mouse/interfaces/IMouseInputProvider.h"
+#include "peripherals/PeripheralTypes.h"
+
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+class TiXmlDocument;
+class CDateTime;
+class CSetting;
+class IKeymap;
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class IButtonMapper;
+class IDriverHandler;
+class IDriverReceiver;
+class IInputHandler;
+} // namespace JOYSTICK
+
+namespace KEYBOARD
+{
+class IKeyboardDriverHandler;
+}
+
+namespace MOUSE
+{
+class IMouseDriverHandler;
+}
+} // namespace KODI
+
+namespace PERIPHERALS
+{
+class CAddonButtonMapping;
+class CGUIDialogPeripheralSettings;
+class CPeripheralBus;
+class CPeripherals;
+
+typedef enum
+{
+ STATE_SWITCH_TOGGLE,
+ STATE_ACTIVATE_SOURCE,
+ STATE_STANDBY
+} CecStateChange;
+
+class CPeripheral : public KODI::JOYSTICK::IInputProvider,
+ public KODI::KEYBOARD::IKeyboardInputProvider,
+ public KODI::MOUSE::IMouseInputProvider
+{
+ friend class CGUIDialogPeripheralSettings;
+
+public:
+ CPeripheral(CPeripherals& manager, const PeripheralScanResult& scanResult, CPeripheralBus* bus);
+ ~CPeripheral(void) override;
+
+ bool operator==(const CPeripheral& right) const;
+ bool operator!=(const CPeripheral& right) const;
+ bool operator==(const PeripheralScanResult& right) const;
+ bool operator!=(const PeripheralScanResult& right) const;
+
+ const std::string& FileLocation(void) const { return m_strFileLocation; }
+ const std::string& Location(void) const { return m_strLocation; }
+ int VendorId(void) const { return m_iVendorId; }
+ const char* VendorIdAsString(void) const { return m_strVendorId.c_str(); }
+ int ProductId(void) const { return m_iProductId; }
+ const char* ProductIdAsString(void) const { return m_strProductId.c_str(); }
+ PeripheralType Type(void) const { return m_type; }
+ PeripheralBusType GetBusType(void) const { return m_busType; }
+ const std::string& DeviceName(void) const { return m_strDeviceName; }
+ bool IsHidden(void) const { return m_bHidden; }
+ void SetHidden(bool bSetTo = true) { m_bHidden = bSetTo; }
+ const std::string& GetVersionInfo(void) const { return m_strVersionInfo; }
+
+ /*!
+ * @brief Get an icon for this peripheral
+ * @return Path to an icon, or skin icon file name
+ */
+ virtual std::string GetIcon() const;
+
+ /*!
+ * @brief Check whether this device has the given feature.
+ * @param feature The feature to check for.
+ * @return True when the device has the feature, false otherwise.
+ */
+ bool HasFeature(const PeripheralFeature feature) const;
+
+ /*!
+ * @brief Get all features that are supported by this device.
+ * @param features The features.
+ */
+ void GetFeatures(std::vector<PeripheralFeature>& features) const;
+
+ /*!
+ * @brief Initialises the peripheral.
+ * @return True when the peripheral has been initialised successfully, false otherwise.
+ */
+ bool Initialise(void);
+
+ /*!
+ * @brief Initialise one of the features of this peripheral.
+ * @param feature The feature to initialise.
+ * @return True when the feature has been initialised successfully, false otherwise.
+ */
+ virtual bool InitialiseFeature(const PeripheralFeature feature) { return true; }
+
+ /*!
+ * @brief Briefly activate a feature to notify the user
+ */
+ virtual void OnUserNotification() {}
+
+ /*!
+ * @brief Briefly test one of the features of this peripheral.
+ * @param feature The feature to test.
+ * @return True if the test succeeded, false otherwise.
+ */
+ virtual bool TestFeature(PeripheralFeature feature) { return false; }
+
+ /*!
+ * @brief Called when a setting changed.
+ * @param strChangedSetting The changed setting.
+ */
+ virtual void OnSettingChanged(const std::string& strChangedSetting) {}
+
+ /*!
+ * @brief Called when this device is removed, before calling the destructor.
+ */
+ virtual void OnDeviceRemoved(void) {}
+
+ /*!
+ * @brief Get all subdevices if this device is multifunctional.
+ * @param subDevices The subdevices.
+ */
+ virtual void GetSubdevices(PeripheralVector& subDevices) const;
+
+ /*!
+ * @return True when this device is multifunctional, false otherwise.
+ */
+ virtual bool IsMultiFunctional(void) const;
+
+ /*!
+ * @brief Add a setting to this peripheral. This will overwrite a previous setting with the same
+ * key.
+ * @param strKey The key of the setting.
+ * @param setting The setting.
+ */
+ virtual void AddSetting(const std::string& strKey,
+ const std::shared_ptr<const CSetting>& setting,
+ int order);
+
+ /*!
+ * @brief Check whether a setting is known with the given key.
+ * @param strKey The key to search.
+ * @return True when found, false otherwise.
+ */
+ virtual bool HasSetting(const std::string& strKey) const;
+
+ /*!
+ * @return True when this device has any settings, false otherwise.
+ */
+ virtual bool HasSettings(void) const;
+
+ /*!
+ * @return True when this device has any configurable settings, false otherwise.
+ */
+ virtual bool HasConfigurableSettings(void) const;
+
+ /*!
+ * @brief Get the value of a setting.
+ * @param strKey The key to search.
+ * @return The value or an empty string if it wasn't found.
+ */
+ virtual const std::string GetSettingString(const std::string& strKey) const;
+ virtual bool SetSetting(const std::string& strKey, const std::string& strValue);
+ virtual void SetSettingVisible(const std::string& strKey, bool bSetTo);
+ virtual bool IsSettingVisible(const std::string& strKey) const;
+
+ virtual int GetSettingInt(const std::string& strKey) const;
+ virtual bool SetSetting(const std::string& strKey, int iValue);
+
+ virtual bool GetSettingBool(const std::string& strKey) const;
+ virtual bool SetSetting(const std::string& strKey, bool bValue);
+
+ virtual float GetSettingFloat(const std::string& strKey) const;
+ virtual bool SetSetting(const std::string& strKey, float fValue);
+
+ virtual void PersistSettings(bool bExiting = false);
+ virtual void LoadPersistedSettings(void);
+ virtual void ResetDefaultSettings(void);
+
+ virtual std::vector<std::shared_ptr<CSetting>> GetSettings(void) const;
+
+ virtual bool ErrorOccured(void) const { return m_bError; }
+
+ virtual void RegisterJoystickDriverHandler(KODI::JOYSTICK::IDriverHandler* handler,
+ bool bPromiscuous)
+ {
+ }
+ virtual void UnregisterJoystickDriverHandler(KODI::JOYSTICK::IDriverHandler* handler) {}
+
+ virtual void RegisterKeyboardDriverHandler(KODI::KEYBOARD::IKeyboardDriverHandler* handler,
+ bool bPromiscuous)
+ {
+ }
+ virtual void UnregisterKeyboardDriverHandler(KODI::KEYBOARD::IKeyboardDriverHandler* handler) {}
+
+ virtual void RegisterMouseDriverHandler(KODI::MOUSE::IMouseDriverHandler* handler,
+ bool bPromiscuous)
+ {
+ }
+ virtual void UnregisterMouseDriverHandler(KODI::MOUSE::IMouseDriverHandler* handler) {}
+
+ // implementation of IInputProvider
+ void RegisterInputHandler(KODI::JOYSTICK::IInputHandler* handler, bool bPromiscuous) override;
+ void UnregisterInputHandler(KODI::JOYSTICK::IInputHandler* handler) override;
+
+ // implementation of IKeyboardInputProvider
+ void RegisterKeyboardHandler(KODI::KEYBOARD::IKeyboardInputHandler* handler,
+ bool bPromiscuous) override;
+ void UnregisterKeyboardHandler(KODI::KEYBOARD::IKeyboardInputHandler* handler) override;
+
+ // implementation of IMouseInputProvider
+ void RegisterMouseHandler(KODI::MOUSE::IMouseInputHandler* handler, bool bPromiscuous) override;
+ void UnregisterMouseHandler(KODI::MOUSE::IMouseInputHandler* handler) override;
+
+ virtual void RegisterJoystickButtonMapper(KODI::JOYSTICK::IButtonMapper* mapper);
+ virtual void UnregisterJoystickButtonMapper(KODI::JOYSTICK::IButtonMapper* mapper);
+
+ virtual KODI::JOYSTICK::IDriverReceiver* GetDriverReceiver() { return nullptr; }
+
+ virtual IKeymap* GetKeymap(const std::string& controllerId) { return nullptr; }
+
+ /*!
+ * \brief Return the last time this peripheral was active
+ *
+ * \return The time of last activation, or invalid if unknown/never active
+ */
+ virtual CDateTime LastActive();
+
+ /*!
+ * \brief Get the controller profile that best represents this peripheral
+ *
+ * \return The controller profile, or empty if unknown
+ */
+ virtual KODI::GAME::ControllerPtr ControllerProfile() const { return m_controllerProfile; }
+
+ /*!
+ * \brief Set the controller profile for this peripheral
+ *
+ * \param controller The new controller profile
+ */
+ virtual void SetControllerProfile(const KODI::GAME::ControllerPtr& controller)
+ {
+ m_controllerProfile = controller;
+ }
+
+protected:
+ virtual void ClearSettings(void);
+
+ CPeripherals& m_manager;
+ PeripheralType m_type;
+ PeripheralBusType m_busType;
+ PeripheralBusType m_mappedBusType;
+ std::string m_strLocation;
+ std::string m_strDeviceName;
+ std::string m_strSettingsFile;
+ std::string m_strFileLocation;
+ int m_iVendorId;
+ std::string m_strVendorId;
+ int m_iProductId;
+ std::string m_strProductId;
+ std::string m_strVersionInfo;
+ bool m_bInitialised;
+ bool m_bHidden;
+ bool m_bError;
+ std::vector<PeripheralFeature> m_features;
+ PeripheralVector m_subDevices;
+ std::map<std::string, PeripheralDeviceSetting> m_settings;
+ std::set<std::string> m_changedSettings;
+ CPeripheralBus* m_bus;
+ std::map<KODI::JOYSTICK::IInputHandler*, std::unique_ptr<KODI::JOYSTICK::IDriverHandler>>
+ m_inputHandlers;
+ std::map<KODI::KEYBOARD::IKeyboardInputHandler*,
+ std::unique_ptr<KODI::KEYBOARD::IKeyboardDriverHandler>>
+ m_keyboardHandlers;
+ std::map<KODI::MOUSE::IMouseInputHandler*, std::unique_ptr<KODI::MOUSE::IMouseDriverHandler>>
+ m_mouseHandlers;
+ std::map<KODI::JOYSTICK::IButtonMapper*, std::unique_ptr<CAddonButtonMapping>> m_buttonMappers;
+ KODI::GAME::ControllerPtr m_controllerProfile;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/devices/PeripheralBluetooth.cpp b/xbmc/peripherals/devices/PeripheralBluetooth.cpp
new file mode 100644
index 0000000..6f3802d
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralBluetooth.cpp
@@ -0,0 +1,19 @@
+/*
+ * 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 "PeripheralBluetooth.h"
+
+using namespace PERIPHERALS;
+
+CPeripheralBluetooth::CPeripheralBluetooth(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : CPeripheral(manager, scanResult, bus)
+{
+ m_features.push_back(FEATURE_BLUETOOTH);
+}
diff --git a/xbmc/peripherals/devices/PeripheralBluetooth.h b/xbmc/peripherals/devices/PeripheralBluetooth.h
new file mode 100644
index 0000000..11fa7ca
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralBluetooth.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "Peripheral.h"
+
+namespace PERIPHERALS
+{
+class CPeripheralBluetooth : public CPeripheral
+{
+public:
+ CPeripheralBluetooth(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus);
+ ~CPeripheralBluetooth(void) override = default;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/devices/PeripheralCecAdapter.cpp b/xbmc/peripherals/devices/PeripheralCecAdapter.cpp
new file mode 100644
index 0000000..bc62bea
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralCecAdapter.cpp
@@ -0,0 +1,1861 @@
+/*
+ * 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 "PeripheralCecAdapter.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationEnums.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationPowerHandling.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "input/remote/IRRemote.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pictures/GUIWindowSlideShow.h"
+#include "utils/JobManager.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "xbmc/interfaces/AnnouncementManager.h"
+
+#include <mutex>
+
+#include <libcec/cec.h>
+
+using namespace PERIPHERALS;
+using namespace CEC;
+using namespace ANNOUNCEMENT;
+using namespace std::chrono_literals;
+
+#define CEC_LIB_SUPPORTED_VERSION LIBCEC_VERSION_TO_UINT(4, 0, 0)
+
+/* time in seconds to ignore standby commands from devices after the screensaver has been activated
+ */
+#define SCREENSAVER_TIMEOUT 20
+#define VOLUME_CHANGE_TIMEOUT 250
+#define VOLUME_REFRESH_TIMEOUT 100
+
+#define LOCALISED_ID_TV 36037
+#define LOCALISED_ID_AVR 36038
+#define LOCALISED_ID_TV_AVR 36039
+#define LOCALISED_ID_STOP 36044
+#define LOCALISED_ID_PAUSE 36045
+#define LOCALISED_ID_POWEROFF 13005
+#define LOCALISED_ID_SUSPEND 13011
+#define LOCALISED_ID_HIBERNATE 13010
+#define LOCALISED_ID_QUIT 13009
+#define LOCALISED_ID_IGNORE 36028
+#define LOCALISED_ID_RECORDING_DEVICE 36051
+#define LOCALISED_ID_PLAYBACK_DEVICE 36052
+#define LOCALISED_ID_TUNER_DEVICE 36053
+
+#define LOCALISED_ID_NONE 231
+
+/* time in seconds to suppress source activation after receiving OnStop */
+#define CEC_SUPPRESS_ACTIVATE_SOURCE_AFTER_ON_STOP 2
+
+CPeripheralCecAdapter::CPeripheralCecAdapter(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : CPeripheralHID(manager, scanResult, bus), CThread("CECAdapter"), m_cecAdapter(NULL)
+{
+ ResetMembers();
+ m_features.push_back(FEATURE_CEC);
+ m_strComPort = scanResult.m_strLocation;
+}
+
+CPeripheralCecAdapter::~CPeripheralCecAdapter(void)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+ m_bStop = true;
+ }
+
+ StopThread(true);
+ delete m_queryThread;
+
+ if (m_cecAdapter)
+ {
+ CECDestroy(m_cecAdapter);
+ m_cecAdapter = NULL;
+ }
+}
+
+void CPeripheralCecAdapter::ResetMembers(void)
+{
+ if (m_cecAdapter)
+ CECDestroy(m_cecAdapter);
+ m_cecAdapter = NULL;
+ m_bStarted = false;
+ m_bHasButton = false;
+ m_bIsReady = false;
+ m_bHasConnectedAudioSystem = false;
+ m_strMenuLanguage = "???";
+ m_lastKeypress = {};
+ m_lastChange = VOLUME_CHANGE_NONE;
+ m_iExitCode = EXITCODE_QUIT;
+
+ //! @todo fetch the correct initial value when system audiostatus is
+ //! implemented in libCEC
+ m_bIsMuted = false;
+
+ m_bGoingToStandby = false;
+ m_bIsRunning = false;
+ m_bDeviceRemoved = false;
+ m_bActiveSourcePending = false;
+ m_bStandbyPending = false;
+ m_bActiveSourceBeforeStandby = false;
+ m_bOnPlayReceived = false;
+ m_bPlaybackPaused = false;
+ m_queryThread = NULL;
+ m_bPowerOnScreensaver = false;
+ m_bUseTVMenuLanguage = false;
+ m_bSendInactiveSource = false;
+ m_bPowerOffScreensaver = false;
+ m_bShutdownOnStandby = false;
+
+ m_currentButton.iButton = 0;
+ m_currentButton.iDuration = 0;
+ m_standbySent.SetValid(false);
+ m_configuration.Clear();
+}
+
+void CPeripheralCecAdapter::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (flag == ANNOUNCEMENT::System && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER &&
+ message == "OnQuit" && m_bIsReady)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iExitCode = static_cast<int>(data["exitcode"].asInteger(EXITCODE_QUIT));
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+ StopThread(false);
+ }
+ else if (flag == ANNOUNCEMENT::GUI && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER &&
+ message == "OnScreensaverDeactivated" && m_bIsReady)
+ {
+ bool bIgnoreDeactivate(false);
+ if (data["shuttingdown"].isBoolean())
+ {
+ // don't respond to the deactivation if we are just going to suspend/shutdown anyway
+ // the tv will not have time to switch on before being told to standby and
+ // may not action the standby command.
+ bIgnoreDeactivate = data["shuttingdown"].asBoolean();
+ if (bIgnoreDeactivate)
+ CLog::Log(LOGDEBUG, "{} - ignoring OnScreensaverDeactivated for power action",
+ __FUNCTION__);
+ }
+ if (m_bPowerOnScreensaver && !bIgnoreDeactivate && m_configuration.bActivateSource)
+ {
+ ActivateSource();
+ }
+ }
+ else if (flag == ANNOUNCEMENT::GUI && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER &&
+ message == "OnScreensaverActivated" && m_bIsReady)
+ {
+ // Don't put devices to standby if application is currently playing
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlaying() && m_bPowerOffScreensaver)
+ {
+ // only power off when we're the active source
+ if (m_cecAdapter->IsLibCECActiveSource())
+ StandbyDevices();
+ }
+ }
+ else if (flag == ANNOUNCEMENT::System && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER &&
+ message == "OnSleep")
+ {
+ // this will also power off devices when we're the active source
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bGoingToStandby = true;
+ }
+ StopThread();
+ }
+ else if (flag == ANNOUNCEMENT::System && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER &&
+ message == "OnWake")
+ {
+ CLog::Log(LOGDEBUG, "{} - reconnecting to the CEC adapter after standby mode", __FUNCTION__);
+ if (ReopenConnection())
+ {
+ bool bActivate(false);
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bActivate = m_bActiveSourceBeforeStandby;
+ m_bActiveSourceBeforeStandby = false;
+ }
+ if (bActivate)
+ ActivateSource();
+ }
+ }
+ else if (flag == ANNOUNCEMENT::Player && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER &&
+ message == "OnStop")
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_preventActivateSourceOnPlay = CDateTime::GetCurrentDateTime();
+ m_bOnPlayReceived = false;
+ }
+ else if (flag == ANNOUNCEMENT::Player && sender == CAnnouncementManager::ANNOUNCEMENT_SENDER &&
+ (message == "OnPlay" || message == "OnResume"))
+ {
+ // activate the source when playback started, and the option is enabled
+ bool bActivateSource(false);
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bActivateSource = (m_configuration.bActivateSource && !m_bOnPlayReceived &&
+ !m_cecAdapter->IsLibCECActiveSource() &&
+ (!m_preventActivateSourceOnPlay.IsValid() ||
+ CDateTime::GetCurrentDateTime() - m_preventActivateSourceOnPlay >
+ CDateTimeSpan(0, 0, 0, CEC_SUPPRESS_ACTIVATE_SOURCE_AFTER_ON_STOP)));
+ m_bOnPlayReceived = true;
+ }
+ if (bActivateSource)
+ ActivateSource();
+ }
+}
+
+bool CPeripheralCecAdapter::InitialiseFeature(const PeripheralFeature feature)
+{
+ if (feature == FEATURE_CEC && !m_bStarted && GetSettingBool("enabled"))
+ {
+ // hide settings that have an override set
+ if (!GetSettingString("wake_devices_advanced").empty())
+ SetSettingVisible("wake_devices", false);
+ if (!GetSettingString("standby_devices_advanced").empty())
+ SetSettingVisible("standby_devices", false);
+
+ SetConfigurationFromSettings();
+ m_callbacks.Clear();
+ m_callbacks.logMessage = &CecLogMessage;
+ m_callbacks.keyPress = &CecKeyPress;
+ m_callbacks.commandReceived = &CecCommand;
+ m_callbacks.configurationChanged = &CecConfiguration;
+ m_callbacks.alert = &CecAlert;
+ m_callbacks.sourceActivated = &CecSourceActivated;
+ m_configuration.callbackParam = this;
+ m_configuration.callbacks = &m_callbacks;
+
+ m_cecAdapter = CECInitialise(&m_configuration);
+
+ if (m_configuration.serverVersion < CEC_LIB_SUPPORTED_VERSION)
+ {
+ /* unsupported libcec version */
+ CLog::Log(
+ LOGERROR,
+ "Detected version of libCEC interface ({0:x}) is lower than the supported version {1:x}",
+ m_cecAdapter ? m_configuration.serverVersion : -1, CEC_LIB_SUPPORTED_VERSION);
+
+ // display warning: incompatible libCEC
+ std::string strMessage = StringUtils::Format(
+ g_localizeStrings.Get(36040), m_cecAdapter ? m_configuration.serverVersion : -1,
+ CEC_LIB_SUPPORTED_VERSION);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(36000),
+ strMessage);
+ m_bError = true;
+ if (m_cecAdapter)
+ CECDestroy(m_cecAdapter);
+ m_cecAdapter = NULL;
+
+ m_features.clear();
+ return false;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - using libCEC v{}", __FUNCTION__,
+ m_cecAdapter->VersionToString(m_configuration.serverVersion));
+ SetVersionInfo(m_configuration);
+ }
+
+ m_bStarted = true;
+ Create();
+ }
+
+ return CPeripheral::InitialiseFeature(feature);
+}
+
+void CPeripheralCecAdapter::SetVersionInfo(const libcec_configuration& configuration)
+{
+ m_strVersionInfo = StringUtils::Format("libCEC {} - firmware v{}",
+ m_cecAdapter->VersionToString(configuration.serverVersion),
+ configuration.iFirmwareVersion);
+
+ // append firmware build date
+ if (configuration.iFirmwareBuildDate != CEC_FW_BUILD_UNKNOWN)
+ {
+ CDateTime dt((time_t)configuration.iFirmwareBuildDate);
+ m_strVersionInfo += StringUtils::Format(" ({})", dt.GetAsDBDate());
+ }
+}
+
+bool CPeripheralCecAdapter::OpenConnection(void)
+{
+ bool bIsOpen(false);
+
+ if (!GetSettingBool("enabled"))
+ {
+ CLog::Log(LOGDEBUG, "{} - CEC adapter is disabled in peripheral settings", __FUNCTION__);
+ m_bStarted = false;
+ return bIsOpen;
+ }
+
+ // open the CEC adapter
+ CLog::Log(LOGDEBUG, "{} - opening a connection to the CEC adapter: {}", __FUNCTION__,
+ m_strComPort);
+
+ // scanning the CEC bus takes about 5 seconds, so display a notification to inform users that
+ // we're busy
+ std::string strMessage =
+ StringUtils::Format(g_localizeStrings.Get(21336), g_localizeStrings.Get(36000));
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000),
+ strMessage);
+
+ bool bConnectionFailedDisplayed(false);
+
+ while (!m_bStop && !bIsOpen)
+ {
+ if ((bIsOpen = m_cecAdapter->Open(m_strComPort.c_str(), 10000)) == false)
+ {
+ // display warning: couldn't initialise libCEC
+ CLog::Log(LOGERROR, "{} - could not opening a connection to the CEC adapter", __FUNCTION__);
+ if (!bConnectionFailedDisplayed)
+ CGUIDialogKaiToast::QueueNotification(
+ CGUIDialogKaiToast::Error, g_localizeStrings.Get(36000), g_localizeStrings.Get(36012));
+ bConnectionFailedDisplayed = true;
+
+ CThread::Sleep(10000ms);
+ }
+ }
+
+ if (bIsOpen)
+ {
+ CLog::Log(LOGDEBUG, "{} - connection to the CEC adapter opened", __FUNCTION__);
+
+ // read the configuration
+ libcec_configuration config;
+ if (m_cecAdapter->GetCurrentConfiguration(&config))
+ {
+ // update the local configuration
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ SetConfigurationFromLibCEC(config);
+ }
+ }
+
+ return bIsOpen;
+}
+
+void CPeripheralCecAdapter::Process(void)
+{
+ if (!OpenConnection())
+ return;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iExitCode = EXITCODE_QUIT;
+ m_bGoingToStandby = false;
+ m_bIsRunning = true;
+ m_bActiveSourceBeforeStandby = false;
+ }
+
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+
+ m_queryThread = new CPeripheralCecAdapterUpdateThread(this, &m_configuration);
+ m_queryThread->Create(false);
+
+ while (!m_bStop)
+ {
+ if (!m_bStop)
+ ProcessVolumeChange();
+
+ if (!m_bStop)
+ ProcessActivateSource();
+
+ if (!m_bStop)
+ ProcessStandbyDevices();
+
+ if (!m_bStop)
+ CThread::Sleep(5ms);
+ }
+
+ m_queryThread->StopThread(true);
+
+ bool bSendStandbyCommands(false);
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bSendStandbyCommands = m_iExitCode != EXITCODE_REBOOT && m_iExitCode != EXITCODE_RESTARTAPP &&
+ !m_bDeviceRemoved &&
+ (!m_bGoingToStandby || GetSettingBool("standby_tv_on_pc_standby")) &&
+ GetSettingBool("enabled");
+
+ if (m_bGoingToStandby)
+ m_bActiveSourceBeforeStandby = m_cecAdapter->IsLibCECActiveSource();
+ }
+
+ if (bSendStandbyCommands)
+ {
+ if (m_cecAdapter->IsLibCECActiveSource())
+ {
+ if (!m_configuration.powerOffDevices.IsEmpty())
+ {
+ CLog::Log(LOGDEBUG, "{} - sending standby commands", __FUNCTION__);
+ m_standbySent = CDateTime::GetCurrentDateTime();
+ m_cecAdapter->StandbyDevices();
+ }
+ else if (m_bSendInactiveSource)
+ {
+ CLog::Log(LOGDEBUG, "{} - sending inactive source commands", __FUNCTION__);
+ m_cecAdapter->SetInactiveView();
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - XBMC is not the active source, not sending any standby commands",
+ __FUNCTION__);
+ }
+ }
+
+ m_cecAdapter->Close();
+
+ CLog::Log(LOGDEBUG, "{} - CEC adapter processor thread ended", __FUNCTION__);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bStarted = false;
+ m_bIsRunning = false;
+ }
+}
+
+bool CPeripheralCecAdapter::HasAudioControl(void)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bHasConnectedAudioSystem;
+}
+
+void CPeripheralCecAdapter::SetAudioSystemConnected(bool bSetTo)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bHasConnectedAudioSystem = bSetTo;
+}
+
+void CPeripheralCecAdapter::ProcessVolumeChange(void)
+{
+ bool bSendRelease(false);
+ CecVolumeChange pendingVolumeChange = VOLUME_CHANGE_NONE;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto now = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastKeypress);
+ if (!m_volumeChangeQueue.empty())
+ {
+ /* get the first change from the queue */
+ pendingVolumeChange = m_volumeChangeQueue.front();
+ m_volumeChangeQueue.pop();
+
+ /* remove all dupe entries */
+ while (!m_volumeChangeQueue.empty() && m_volumeChangeQueue.front() == pendingVolumeChange)
+ m_volumeChangeQueue.pop();
+
+ /* send another keypress after VOLUME_REFRESH_TIMEOUT ms */
+
+ bool bRefresh(duration.count() > VOLUME_REFRESH_TIMEOUT);
+
+ /* only send the keypress when it hasn't been sent yet */
+ if (pendingVolumeChange != m_lastChange)
+ {
+ m_lastKeypress = std::chrono::steady_clock::now();
+ m_lastChange = pendingVolumeChange;
+ }
+ else if (bRefresh)
+ {
+ m_lastKeypress = std::chrono::steady_clock::now();
+ pendingVolumeChange = m_lastChange;
+ }
+ else
+ pendingVolumeChange = VOLUME_CHANGE_NONE;
+ }
+ else if (m_lastKeypress.time_since_epoch().count() > 0 &&
+ duration.count() > VOLUME_CHANGE_TIMEOUT)
+ {
+ /* send a key release */
+ m_lastKeypress = {};
+ bSendRelease = true;
+ m_lastChange = VOLUME_CHANGE_NONE;
+ }
+ }
+
+ switch (pendingVolumeChange)
+ {
+ case VOLUME_CHANGE_UP:
+ m_cecAdapter->SendKeypress(CECDEVICE_AUDIOSYSTEM, CEC_USER_CONTROL_CODE_VOLUME_UP, false);
+ break;
+ case VOLUME_CHANGE_DOWN:
+ m_cecAdapter->SendKeypress(CECDEVICE_AUDIOSYSTEM, CEC_USER_CONTROL_CODE_VOLUME_DOWN, false);
+ break;
+ case VOLUME_CHANGE_MUTE:
+ m_cecAdapter->SendKeypress(CECDEVICE_AUDIOSYSTEM, CEC_USER_CONTROL_CODE_MUTE, false);
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bIsMuted = !m_bIsMuted;
+ }
+ break;
+ case VOLUME_CHANGE_NONE:
+ if (bSendRelease)
+ m_cecAdapter->SendKeyRelease(CECDEVICE_AUDIOSYSTEM, false);
+ break;
+ }
+}
+
+void CPeripheralCecAdapter::VolumeUp(void)
+{
+ if (HasAudioControl())
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_volumeChangeQueue.push(VOLUME_CHANGE_UP);
+ }
+}
+
+void CPeripheralCecAdapter::VolumeDown(void)
+{
+ if (HasAudioControl())
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_volumeChangeQueue.push(VOLUME_CHANGE_DOWN);
+ }
+}
+
+void CPeripheralCecAdapter::ToggleMute(void)
+{
+ if (HasAudioControl())
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_volumeChangeQueue.push(VOLUME_CHANGE_MUTE);
+ }
+}
+
+bool CPeripheralCecAdapter::IsMuted(void)
+{
+ if (HasAudioControl())
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsMuted;
+ }
+ return false;
+}
+
+void CPeripheralCecAdapter::SetMenuLanguage(const char* strLanguage)
+{
+ if (StringUtils::EqualsNoCase(m_strMenuLanguage, strLanguage))
+ return;
+
+ std::string strGuiLanguage;
+
+ if (!strcmp(strLanguage, "bul"))
+ strGuiLanguage = "bg_bg";
+ else if (!strcmp(strLanguage, "hrv"))
+ strGuiLanguage = "hr_hr";
+ else if (!strcmp(strLanguage, "cze"))
+ strGuiLanguage = "cs_cz";
+ else if (!strcmp(strLanguage, "dan"))
+ strGuiLanguage = "da_dk";
+ else if (!strcmp(strLanguage, "deu"))
+ strGuiLanguage = "de_de";
+ else if (!strcmp(strLanguage, "dut"))
+ strGuiLanguage = "nl_nl";
+ else if (!strcmp(strLanguage, "eng"))
+ strGuiLanguage = "en_gb";
+ else if (!strcmp(strLanguage, "fin"))
+ strGuiLanguage = "fi_fi";
+ else if (!strcmp(strLanguage, "fre"))
+ strGuiLanguage = "fr_fr";
+ else if (!strcmp(strLanguage, "ger"))
+ strGuiLanguage = "de_de";
+ else if (!strcmp(strLanguage, "gre"))
+ strGuiLanguage = "el_gr";
+ else if (!strcmp(strLanguage, "hun"))
+ strGuiLanguage = "hu_hu";
+ else if (!strcmp(strLanguage, "ita"))
+ strGuiLanguage = "it_it";
+ else if (!strcmp(strLanguage, "nor"))
+ strGuiLanguage = "nb_no";
+ else if (!strcmp(strLanguage, "pol"))
+ strGuiLanguage = "pl_pl";
+ else if (!strcmp(strLanguage, "por"))
+ strGuiLanguage = "pt_pt";
+ else if (!strcmp(strLanguage, "rum"))
+ strGuiLanguage = "ro_ro";
+ else if (!strcmp(strLanguage, "rus"))
+ strGuiLanguage = "ru_ru";
+ else if (!strcmp(strLanguage, "srp"))
+ strGuiLanguage = "sr_rs@latin";
+ else if (!strcmp(strLanguage, "slo"))
+ strGuiLanguage = "sk_sk";
+ else if (!strcmp(strLanguage, "slv"))
+ strGuiLanguage = "sl_si";
+ else if (!strcmp(strLanguage, "spa"))
+ strGuiLanguage = "es_es";
+ else if (!strcmp(strLanguage, "swe"))
+ strGuiLanguage = "sv_se";
+ else if (!strcmp(strLanguage, "tur"))
+ strGuiLanguage = "tr_tr";
+
+ if (!strGuiLanguage.empty())
+ {
+ strGuiLanguage = "resource.language." + strGuiLanguage;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SETLANGUAGE, -1, -1, nullptr, strGuiLanguage);
+ CLog::Log(LOGDEBUG, "{} - language set to '{}'", __FUNCTION__, strGuiLanguage);
+ }
+ else
+ CLog::Log(LOGWARNING, "{} - TV menu language set to unknown value '{}'", __FUNCTION__,
+ strLanguage);
+}
+
+void CPeripheralCecAdapter::OnTvStandby(void)
+{
+ int iActionOnTvStandby = GetSettingInt("standby_pc_on_tv_standby");
+ switch (iActionOnTvStandby)
+ {
+ case LOCALISED_ID_POWEROFF:
+ m_bStarted = false;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SYSTEM_POWERDOWN, TMSG_SHUTDOWN);
+ break;
+ case LOCALISED_ID_SUSPEND:
+ m_bStarted = false;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SYSTEM_POWERDOWN, TMSG_SUSPEND);
+ break;
+ case LOCALISED_ID_HIBERNATE:
+ m_bStarted = false;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SYSTEM_POWERDOWN, TMSG_HIBERNATE);
+ break;
+ case LOCALISED_ID_QUIT:
+ m_bStarted = false;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+ break;
+ case LOCALISED_ID_PAUSE:
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PAUSE_IF_PLAYING);
+ break;
+ case LOCALISED_ID_STOP:
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_STOP);
+ break;
+ }
+ case LOCALISED_ID_IGNORE:
+ break;
+ default:
+ CLog::Log(LOGERROR, "{} - Unexpected [standby_pc_on_tv_standby] setting value", __FUNCTION__);
+ break;
+ }
+}
+
+void CPeripheralCecAdapter::CecCommand(void* cbParam, const cec_command* command)
+{
+ CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam);
+ if (!adapter)
+ return;
+
+ if (adapter->m_bIsReady)
+ {
+ switch (command->opcode)
+ {
+ case CEC_OPCODE_STANDBY:
+ if (command->initiator == CECDEVICE_TV &&
+ (!adapter->m_standbySent.IsValid() ||
+ CDateTime::GetCurrentDateTime() - adapter->m_standbySent >
+ CDateTimeSpan(0, 0, 0, SCREENSAVER_TIMEOUT)))
+ {
+ adapter->OnTvStandby();
+ }
+ break;
+ case CEC_OPCODE_SET_MENU_LANGUAGE:
+ if (adapter->m_bUseTVMenuLanguage == 1 && command->initiator == CECDEVICE_TV &&
+ command->parameters.size == 3)
+ {
+ char strNewLanguage[4];
+ for (int iPtr = 0; iPtr < 3; iPtr++)
+ strNewLanguage[iPtr] = command->parameters[iPtr];
+ strNewLanguage[3] = 0;
+ adapter->SetMenuLanguage(strNewLanguage);
+ }
+ break;
+ case CEC_OPCODE_DECK_CONTROL:
+ if (command->initiator == CECDEVICE_TV && command->parameters.size == 1 &&
+ command->parameters[0] == CEC_DECK_CONTROL_MODE_STOP)
+ {
+ cec_keypress key;
+ key.duration = 500;
+ key.keycode = CEC_USER_CONTROL_CODE_STOP;
+ adapter->PushCecKeypress(key);
+ }
+ break;
+ case CEC_OPCODE_PLAY:
+ if (command->initiator == CECDEVICE_TV && command->parameters.size == 1)
+ {
+ if (command->parameters[0] == CEC_PLAY_MODE_PLAY_FORWARD)
+ {
+ cec_keypress key;
+ key.duration = 500;
+ key.keycode = CEC_USER_CONTROL_CODE_PLAY;
+ adapter->PushCecKeypress(key);
+ }
+ else if (command->parameters[0] == CEC_PLAY_MODE_PLAY_STILL)
+ {
+ cec_keypress key;
+ key.duration = 500;
+ key.keycode = CEC_USER_CONTROL_CODE_PAUSE;
+ adapter->PushCecKeypress(key);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+void CPeripheralCecAdapter::CecConfiguration(void* cbParam, const libcec_configuration* config)
+{
+ CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam);
+ if (!adapter)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(adapter->m_critSection);
+ adapter->SetConfigurationFromLibCEC(*config);
+}
+
+void CPeripheralCecAdapter::CecAlert(void* cbParam,
+ const libcec_alert alert,
+ const libcec_parameter data)
+{
+ CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam);
+ if (!adapter)
+ return;
+
+ bool bReopenConnection(false);
+ int iAlertString(0);
+ switch (alert)
+ {
+ case CEC_ALERT_SERVICE_DEVICE:
+ iAlertString = 36027;
+ break;
+ case CEC_ALERT_CONNECTION_LOST:
+ bReopenConnection = true;
+ iAlertString = 36030;
+ break;
+#if defined(CEC_ALERT_PERMISSION_ERROR)
+ case CEC_ALERT_PERMISSION_ERROR:
+ bReopenConnection = true;
+ iAlertString = 36031;
+ break;
+ case CEC_ALERT_PORT_BUSY:
+ bReopenConnection = true;
+ iAlertString = 36032;
+ break;
+#endif
+ default:
+ break;
+ }
+
+ // display the alert
+ if (iAlertString)
+ {
+ std::string strLog(g_localizeStrings.Get(iAlertString));
+ if (data.paramType == CEC_PARAMETER_TYPE_STRING && data.paramData)
+ strLog += StringUtils::Format(" - {}", (const char*)data.paramData);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000),
+ strLog);
+ }
+
+ if (bReopenConnection)
+ {
+ // Reopen the connection asynchronously. Otherwise a deadlock may occur.
+ // Reconnect means destruction and recreation of our libcec instance, but libcec
+ // calls this callback function synchronously and must not be destroyed meanwhile.
+ adapter->ReopenConnection(true);
+ }
+}
+
+void CPeripheralCecAdapter::CecKeyPress(void* cbParam, const cec_keypress* key)
+{
+ CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam);
+ if (!!adapter)
+ adapter->PushCecKeypress(*key);
+}
+
+void CPeripheralCecAdapter::GetNextKey(void)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bHasButton = false;
+ if (m_bIsReady)
+ {
+ std::vector<CecButtonPress>::iterator it = m_buttonQueue.begin();
+ if (it != m_buttonQueue.end())
+ {
+ m_currentButton = (*it);
+ m_buttonQueue.erase(it);
+ m_bHasButton = true;
+ }
+ }
+}
+
+void CPeripheralCecAdapter::PushCecKeypress(const CecButtonPress& key)
+{
+ CLog::Log(LOGDEBUG, "{} - received key {:2x} duration {}", __FUNCTION__, key.iButton,
+ key.iDuration);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ // avoid the queue getting too long
+ if (m_configuration.iButtonRepeatRateMs && m_buttonQueue.size() > 5)
+ return;
+ if (m_configuration.iButtonRepeatRateMs == 0 && key.iDuration > 0)
+ {
+ if (m_currentButton.iButton == key.iButton && m_currentButton.iDuration == 0)
+ {
+ // update the duration
+ if (m_bHasButton)
+ m_currentButton.iDuration = key.iDuration;
+ // ignore this one, since it's already been handled by xbmc
+ return;
+ }
+ // if we received a keypress with a duration set, try to find the same one without a duration
+ // set, and replace it
+ for (std::vector<CecButtonPress>::reverse_iterator it = m_buttonQueue.rbegin();
+ it != m_buttonQueue.rend(); ++it)
+ {
+ if ((*it).iButton == key.iButton)
+ {
+ if ((*it).iDuration == 0)
+ {
+ // replace this entry
+ (*it).iDuration = key.iDuration;
+ return;
+ }
+ // add a new entry
+ break;
+ }
+ }
+ }
+
+ m_buttonQueue.push_back(key);
+}
+
+void CPeripheralCecAdapter::PushCecKeypress(const cec_keypress& key)
+{
+ CecButtonPress xbmcKey;
+ xbmcKey.iDuration = key.duration;
+
+ switch (key.keycode)
+ {
+ case CEC_USER_CONTROL_CODE_SELECT:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_SELECT;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_UP:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_UP;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_DOWN:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_DOWN;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_LEFT:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_LEFT;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_LEFT_UP:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_LEFT;
+ PushCecKeypress(xbmcKey);
+ xbmcKey.iButton = XINPUT_IR_REMOTE_UP;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_LEFT_DOWN:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_LEFT;
+ PushCecKeypress(xbmcKey);
+ xbmcKey.iButton = XINPUT_IR_REMOTE_DOWN;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_RIGHT:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_RIGHT;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_RIGHT_UP:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_RIGHT;
+ PushCecKeypress(xbmcKey);
+ xbmcKey.iButton = XINPUT_IR_REMOTE_UP;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_RIGHT_DOWN:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_RIGHT;
+ PushCecKeypress(xbmcKey);
+ xbmcKey.iButton = XINPUT_IR_REMOTE_DOWN;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_SETUP_MENU:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_TITLE;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_CONTENTS_MENU:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_CONTENTS_MENU;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_ROOT_MENU:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_ROOT_MENU;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_TOP_MENU:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_TOP_MENU;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_DVD_MENU:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_DVD_MENU;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_FAVORITE_MENU:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_MENU;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_EXIT:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_BACK;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_ENTER:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_ENTER;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_CHANNEL_DOWN:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_MINUS;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_CHANNEL_UP:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_PLUS;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_PREVIOUS_CHANNEL:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_TELETEXT;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_SOUND_SELECT:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_LANGUAGE;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_POWER:
+ case CEC_USER_CONTROL_CODE_POWER_TOGGLE_FUNCTION:
+ case CEC_USER_CONTROL_CODE_POWER_OFF_FUNCTION:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_POWER;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_VOLUME_UP:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_VOLUME_PLUS;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_VOLUME_DOWN:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_VOLUME_MINUS;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_MUTE:
+ case CEC_USER_CONTROL_CODE_MUTE_FUNCTION:
+ case CEC_USER_CONTROL_CODE_RESTORE_VOLUME_FUNCTION:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_MUTE;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_PLAY:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_PLAY;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_STOP:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_STOP;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_PAUSE:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_PAUSE;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_REWIND:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_REVERSE;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_FAST_FORWARD:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_FORWARD;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_NUMBER0:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_0;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_NUMBER1:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_1;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_NUMBER2:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_2;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_NUMBER3:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_3;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_NUMBER4:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_4;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_NUMBER5:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_5;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_NUMBER6:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_6;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_NUMBER7:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_7;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_NUMBER8:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_8;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_NUMBER9:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_9;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_RECORD:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_RECORD;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_CLEAR:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_CLEAR;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_DISPLAY_INFORMATION:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_INFO;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_PAGE_UP:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_PLUS;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_PAGE_DOWN:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_CHANNEL_MINUS;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_FORWARD:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_SKIP_PLUS;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_BACKWARD:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_SKIP_MINUS;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_F1_BLUE:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_BLUE;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_F2_RED:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_RED;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_F3_GREEN:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_GREEN;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_F4_YELLOW:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_YELLOW;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_ELECTRONIC_PROGRAM_GUIDE:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_GUIDE;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_AN_CHANNELS_LIST:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_LIVE_TV;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_NEXT_FAVORITE:
+ case CEC_USER_CONTROL_CODE_DOT:
+ case CEC_USER_CONTROL_CODE_AN_RETURN:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_TITLE; // context menu
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_DATA:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_TELETEXT;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_SUB_PICTURE:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_SUBTITLE;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_EJECT:
+ xbmcKey.iButton = XINPUT_IR_REMOTE_EJECT;
+ PushCecKeypress(xbmcKey);
+ break;
+ case CEC_USER_CONTROL_CODE_POWER_ON_FUNCTION:
+ case CEC_USER_CONTROL_CODE_INPUT_SELECT:
+ case CEC_USER_CONTROL_CODE_INITIAL_CONFIGURATION:
+ case CEC_USER_CONTROL_CODE_HELP:
+ case CEC_USER_CONTROL_CODE_STOP_RECORD:
+ case CEC_USER_CONTROL_CODE_PAUSE_RECORD:
+ case CEC_USER_CONTROL_CODE_ANGLE:
+ case CEC_USER_CONTROL_CODE_VIDEO_ON_DEMAND:
+ case CEC_USER_CONTROL_CODE_TIMER_PROGRAMMING:
+ case CEC_USER_CONTROL_CODE_PLAY_FUNCTION:
+ case CEC_USER_CONTROL_CODE_PAUSE_PLAY_FUNCTION:
+ case CEC_USER_CONTROL_CODE_RECORD_FUNCTION:
+ case CEC_USER_CONTROL_CODE_PAUSE_RECORD_FUNCTION:
+ case CEC_USER_CONTROL_CODE_STOP_FUNCTION:
+ case CEC_USER_CONTROL_CODE_TUNE_FUNCTION:
+ case CEC_USER_CONTROL_CODE_SELECT_MEDIA_FUNCTION:
+ case CEC_USER_CONTROL_CODE_SELECT_AV_INPUT_FUNCTION:
+ case CEC_USER_CONTROL_CODE_SELECT_AUDIO_INPUT_FUNCTION:
+ case CEC_USER_CONTROL_CODE_F5:
+ case CEC_USER_CONTROL_CODE_NUMBER_ENTRY_MODE:
+ case CEC_USER_CONTROL_CODE_NUMBER11:
+ case CEC_USER_CONTROL_CODE_NUMBER12:
+ case CEC_USER_CONTROL_CODE_SELECT_BROADCAST_TYPE:
+ case CEC_USER_CONTROL_CODE_SELECT_SOUND_PRESENTATION:
+ case CEC_USER_CONTROL_CODE_UNKNOWN:
+ default:
+ break;
+ }
+}
+
+int CPeripheralCecAdapter::GetButton(void)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_bHasButton)
+ GetNextKey();
+
+ return m_bHasButton ? m_currentButton.iButton : 0;
+}
+
+unsigned int CPeripheralCecAdapter::GetHoldTime(void)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_bHasButton)
+ GetNextKey();
+
+ return m_bHasButton ? m_currentButton.iDuration : 0;
+}
+
+void CPeripheralCecAdapter::ResetButton(void)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bHasButton = false;
+
+ // wait for the key release if the duration isn't 0
+ if (m_currentButton.iDuration > 0)
+ {
+ m_currentButton.iButton = 0;
+ m_currentButton.iDuration = 0;
+ }
+}
+
+void CPeripheralCecAdapter::OnSettingChanged(const std::string& strChangedSetting)
+{
+ if (StringUtils::EqualsNoCase(strChangedSetting, "enabled"))
+ {
+ bool bEnabled(GetSettingBool("enabled"));
+ if (!bEnabled && IsRunning())
+ {
+ CLog::Log(LOGDEBUG, "{} - closing the CEC connection", __FUNCTION__);
+ StopThread(true);
+ }
+ else if (bEnabled && !IsRunning())
+ {
+ CLog::Log(LOGDEBUG, "{} - starting the CEC connection", __FUNCTION__);
+ SetConfigurationFromSettings();
+ InitialiseFeature(FEATURE_CEC);
+ }
+ }
+ else if (IsRunning())
+ {
+ if (m_queryThread->IsRunning())
+ {
+ CLog::Log(LOGDEBUG, "{} - sending the updated configuration to libCEC", __FUNCTION__);
+ SetConfigurationFromSettings();
+ m_queryThread->UpdateConfiguration(&m_configuration);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - restarting the CEC connection", __FUNCTION__);
+ SetConfigurationFromSettings();
+ InitialiseFeature(FEATURE_CEC);
+ }
+}
+
+void CPeripheralCecAdapter::CecSourceActivated(void* cbParam,
+ const CEC::cec_logical_address address,
+ const uint8_t activated)
+{
+ CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam);
+ if (!adapter)
+ return;
+
+ // wake up the screensaver, so the user doesn't switch to a black screen
+ if (activated == 1)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->WakeUpScreenSaverAndDPMS();
+ }
+
+ if (adapter->GetSettingInt("pause_or_stop_playback_on_deactivate") != LOCALISED_ID_NONE)
+ {
+ bool bShowingSlideshow =
+ (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW);
+ CGUIWindowSlideShow* pSlideShow =
+ bShowingSlideshow
+ ? CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(
+ WINDOW_SLIDESHOW)
+ : NULL;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ bool bPlayingAndDeactivated = activated == 0 && ((pSlideShow && pSlideShow->IsPlaying()) ||
+ !appPlayer->IsPausedPlayback());
+ bool bPausedAndActivated =
+ activated == 1 && adapter->m_bPlaybackPaused &&
+ ((pSlideShow && pSlideShow->IsPaused()) || (appPlayer && appPlayer->IsPausedPlayback()));
+ if (bPlayingAndDeactivated)
+ adapter->m_bPlaybackPaused = true;
+ else if (bPausedAndActivated)
+ adapter->m_bPlaybackPaused = false;
+
+ if ((bPlayingAndDeactivated || bPausedAndActivated) &&
+ adapter->GetSettingInt("pause_or_stop_playback_on_deactivate") == LOCALISED_ID_PAUSE)
+ {
+ if (pSlideShow)
+ // pause/resume slideshow
+ pSlideShow->OnAction(CAction(ACTION_PAUSE));
+ else
+ // pause/resume player
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_PAUSE);
+ }
+ else if (bPlayingAndDeactivated &&
+ adapter->GetSettingInt("pause_or_stop_playback_on_deactivate") == LOCALISED_ID_STOP)
+ {
+ if (pSlideShow)
+ pSlideShow->OnAction(CAction(ACTION_STOP));
+ else
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ }
+ }
+}
+
+void CPeripheralCecAdapter::CecLogMessage(void* cbParam, const cec_log_message* message)
+{
+ CPeripheralCecAdapter* adapter = static_cast<CPeripheralCecAdapter*>(cbParam);
+ if (!adapter)
+ return;
+
+ int iLevel = -1;
+ switch (message->level)
+ {
+ case CEC_LOG_ERROR:
+ iLevel = LOGERROR;
+ break;
+ case CEC_LOG_WARNING:
+ iLevel = LOGWARNING;
+ break;
+ case CEC_LOG_NOTICE:
+ iLevel = LOGDEBUG;
+ break;
+ case CEC_LOG_TRAFFIC:
+ case CEC_LOG_DEBUG:
+ iLevel = LOGDEBUG;
+ break;
+ default:
+ break;
+ }
+
+ if (iLevel >= CEC_LOG_NOTICE ||
+ (iLevel >= 0 && CServiceBroker::GetLogging().IsLogLevelLogged(LOGDEBUG)))
+ CLog::Log(iLevel, LOGCEC, "{} - {}", __FUNCTION__, message->message);
+}
+
+void CPeripheralCecAdapter::SetConfigurationFromLibCEC(const CEC::libcec_configuration& config)
+{
+ bool bChanged(false);
+
+ // set the primary device type
+ m_configuration.deviceTypes.Clear();
+ m_configuration.deviceTypes.Add(config.deviceTypes[0]);
+
+ // hide the "connected device" and "hdmi port number" settings when the PA was autodetected
+ bool bPAAutoDetected(config.bAutodetectAddress == 1);
+
+ SetSettingVisible("connected_device", !bPAAutoDetected);
+ SetSettingVisible("cec_hdmi_port", !bPAAutoDetected);
+
+ // set the connected device
+ m_configuration.baseDevice = config.baseDevice;
+ bChanged |=
+ SetSetting("connected_device",
+ config.baseDevice == CECDEVICE_AUDIOSYSTEM ? LOCALISED_ID_AVR : LOCALISED_ID_TV);
+
+ // set the HDMI port number
+ m_configuration.iHDMIPort = config.iHDMIPort;
+ bChanged |= SetSetting("cec_hdmi_port", config.iHDMIPort);
+
+ // set the physical address, when baseDevice or iHDMIPort are not set
+ std::string strPhysicalAddress("0");
+ if (!bPAAutoDetected && (m_configuration.baseDevice == CECDEVICE_UNKNOWN ||
+ m_configuration.iHDMIPort < CEC_MIN_HDMI_PORTNUMBER ||
+ m_configuration.iHDMIPort > CEC_MAX_HDMI_PORTNUMBER))
+ {
+ m_configuration.iPhysicalAddress = config.iPhysicalAddress;
+ strPhysicalAddress = StringUtils::Format("{:x}", config.iPhysicalAddress);
+ }
+ bChanged |= SetSetting("physical_address", strPhysicalAddress);
+
+ // set the devices to wake when starting
+ m_configuration.wakeDevices = config.wakeDevices;
+ bChanged |= WriteLogicalAddresses(config.wakeDevices, "wake_devices", "wake_devices_advanced");
+
+ // set the devices to power off when stopping
+ m_configuration.powerOffDevices = config.powerOffDevices;
+ bChanged |=
+ WriteLogicalAddresses(config.powerOffDevices, "standby_devices", "standby_devices_advanced");
+
+ // set the boolean settings
+ m_configuration.bActivateSource = config.bActivateSource;
+ bChanged |= SetSetting("activate_source", m_configuration.bActivateSource == 1);
+
+ m_configuration.iDoubleTapTimeoutMs = config.iDoubleTapTimeoutMs;
+ bChanged |= SetSetting("double_tap_timeout_ms", (int)m_configuration.iDoubleTapTimeoutMs);
+
+ m_configuration.iButtonRepeatRateMs = config.iButtonRepeatRateMs;
+ bChanged |= SetSetting("button_repeat_rate_ms", (int)m_configuration.iButtonRepeatRateMs);
+
+ m_configuration.iButtonReleaseDelayMs = config.iButtonReleaseDelayMs;
+ bChanged |= SetSetting("button_release_delay_ms", (int)m_configuration.iButtonReleaseDelayMs);
+
+ m_configuration.bPowerOffOnStandby = config.bPowerOffOnStandby;
+
+ m_configuration.iFirmwareVersion = config.iFirmwareVersion;
+
+ memcpy(m_configuration.strDeviceLanguage, config.strDeviceLanguage, 3);
+ m_configuration.iFirmwareBuildDate = config.iFirmwareBuildDate;
+
+ SetVersionInfo(m_configuration);
+
+ if (bChanged)
+ CLog::Log(LOGDEBUG, "SetConfigurationFromLibCEC - settings updated by libCEC");
+}
+
+void CPeripheralCecAdapter::SetConfigurationFromSettings(void)
+{
+ // client version matches the version of libCEC that we originally used the API from
+ m_configuration.clientVersion = LIBCEC_VERSION_TO_UINT(4, 0, 0);
+
+ // device name 'XBMC'
+ snprintf(m_configuration.strDeviceName, 13, "%s", GetSettingString("device_name").c_str());
+
+ // set the primary device type
+ m_configuration.deviceTypes.Clear();
+ switch (GetSettingInt("device_type"))
+ {
+ case LOCALISED_ID_PLAYBACK_DEVICE:
+ m_configuration.deviceTypes.Add(CEC_DEVICE_TYPE_PLAYBACK_DEVICE);
+ break;
+ case LOCALISED_ID_TUNER_DEVICE:
+ m_configuration.deviceTypes.Add(CEC_DEVICE_TYPE_TUNER);
+ break;
+ case LOCALISED_ID_RECORDING_DEVICE:
+ default:
+ m_configuration.deviceTypes.Add(CEC_DEVICE_TYPE_RECORDING_DEVICE);
+ break;
+ }
+
+ // always try to autodetect the address.
+ // when the firmware supports this, it will override the physical address, connected device and
+ // hdmi port settings
+ m_configuration.bAutodetectAddress = CEC_DEFAULT_SETTING_AUTODETECT_ADDRESS;
+
+ // set the physical address
+ // when set, it will override the connected device and hdmi port settings
+ std::string strPhysicalAddress = GetSettingString("physical_address");
+ int iPhysicalAddress;
+ if (sscanf(strPhysicalAddress.c_str(), "%x", &iPhysicalAddress) &&
+ iPhysicalAddress >= CEC_PHYSICAL_ADDRESS_TV && iPhysicalAddress <= CEC_MAX_PHYSICAL_ADDRESS)
+ m_configuration.iPhysicalAddress = iPhysicalAddress;
+ else
+ m_configuration.iPhysicalAddress = CEC_PHYSICAL_ADDRESS_TV;
+
+ // set the connected device
+ int iConnectedDevice = GetSettingInt("connected_device");
+ if (iConnectedDevice == LOCALISED_ID_AVR)
+ m_configuration.baseDevice = CECDEVICE_AUDIOSYSTEM;
+ else if (iConnectedDevice == LOCALISED_ID_TV)
+ m_configuration.baseDevice = CECDEVICE_TV;
+
+ // set the HDMI port number
+ int iHDMIPort = GetSettingInt("cec_hdmi_port");
+ if (iHDMIPort >= CEC_MIN_HDMI_PORTNUMBER && iHDMIPort <= CEC_MAX_HDMI_PORTNUMBER)
+ m_configuration.iHDMIPort = iHDMIPort;
+
+ // set the tv vendor override
+ int iVendor = GetSettingInt("tv_vendor");
+ if (iVendor >= CEC_MIN_VENDORID && iVendor <= CEC_MAX_VENDORID)
+ m_configuration.tvVendor = iVendor;
+
+ // read the devices to wake when starting
+ std::string strWakeDevices = GetSettingString("wake_devices_advanced");
+ StringUtils::Trim(strWakeDevices);
+ m_configuration.wakeDevices.Clear();
+ if (!strWakeDevices.empty())
+ ReadLogicalAddresses(strWakeDevices, m_configuration.wakeDevices);
+ else
+ ReadLogicalAddresses(GetSettingInt("wake_devices"), m_configuration.wakeDevices);
+
+ // read the devices to power off when stopping
+ std::string strStandbyDevices = GetSettingString("standby_devices_advanced");
+ StringUtils::Trim(strStandbyDevices);
+ m_configuration.powerOffDevices.Clear();
+ if (!strStandbyDevices.empty())
+ ReadLogicalAddresses(strStandbyDevices, m_configuration.powerOffDevices);
+ else
+ ReadLogicalAddresses(GetSettingInt("standby_devices"), m_configuration.powerOffDevices);
+
+ // read the boolean settings
+ m_bUseTVMenuLanguage = GetSettingBool("use_tv_menu_language");
+ m_configuration.bActivateSource = GetSettingBool("activate_source") ? 1 : 0;
+ m_bPowerOffScreensaver = GetSettingBool("cec_standby_screensaver");
+ m_bPowerOnScreensaver = GetSettingBool("cec_wake_screensaver");
+ m_bSendInactiveSource = GetSettingBool("send_inactive_source");
+ m_configuration.bAutoWakeAVR = GetSettingBool("power_avr_on_as") ? 1 : 0;
+
+ // read the mutually exclusive boolean settings
+ int iStandbyAction(GetSettingInt("standby_pc_on_tv_standby"));
+ m_configuration.bPowerOffOnStandby =
+ (iStandbyAction == LOCALISED_ID_SUSPEND || iStandbyAction == LOCALISED_ID_HIBERNATE) ? 1 : 0;
+ m_bShutdownOnStandby = iStandbyAction == LOCALISED_ID_POWEROFF;
+
+ // double tap prevention timeout in ms
+ m_configuration.iDoubleTapTimeoutMs = GetSettingInt("double_tap_timeout_ms");
+ m_configuration.iButtonRepeatRateMs = GetSettingInt("button_repeat_rate_ms");
+ m_configuration.iButtonReleaseDelayMs = GetSettingInt("button_release_delay_ms");
+
+ if (GetSettingBool("pause_playback_on_deactivate"))
+ {
+ SetSetting("pause_or_stop_playback_on_deactivate", LOCALISED_ID_PAUSE);
+ SetSetting("pause_playback_on_deactivate", false);
+ }
+}
+
+void CPeripheralCecAdapter::ReadLogicalAddresses(const std::string& strString,
+ cec_logical_addresses& addresses)
+{
+ for (size_t iPtr = 0; iPtr < strString.size(); iPtr++)
+ {
+ std::string strDevice = strString.substr(iPtr, 1);
+ StringUtils::Trim(strDevice);
+ if (!strDevice.empty())
+ {
+ int iDevice(0);
+ if (sscanf(strDevice.c_str(), "%x", &iDevice) == 1 && iDevice >= 0 && iDevice <= 0xF)
+ addresses.Set((cec_logical_address)iDevice);
+ }
+ }
+}
+
+void CPeripheralCecAdapter::ReadLogicalAddresses(int iLocalisedId, cec_logical_addresses& addresses)
+{
+ addresses.Clear();
+ switch (iLocalisedId)
+ {
+ case LOCALISED_ID_TV:
+ addresses.Set(CECDEVICE_TV);
+ break;
+ case LOCALISED_ID_AVR:
+ addresses.Set(CECDEVICE_AUDIOSYSTEM);
+ break;
+ case LOCALISED_ID_TV_AVR:
+ addresses.Set(CECDEVICE_TV);
+ addresses.Set(CECDEVICE_AUDIOSYSTEM);
+ break;
+ case LOCALISED_ID_NONE:
+ default:
+ break;
+ }
+}
+
+bool CPeripheralCecAdapter::WriteLogicalAddresses(const cec_logical_addresses& addresses,
+ const std::string& strSettingName,
+ const std::string& strAdvancedSettingName)
+{
+ bool bChanged(false);
+
+ // only update the advanced setting if it was set by the user
+ if (!GetSettingString(strAdvancedSettingName).empty())
+ {
+ std::string strPowerOffDevices;
+ for (unsigned int iPtr = CECDEVICE_TV; iPtr <= CECDEVICE_BROADCAST; iPtr++)
+ if (addresses[iPtr])
+ strPowerOffDevices += StringUtils::Format(" {:X}", iPtr);
+ StringUtils::Trim(strPowerOffDevices);
+ bChanged = SetSetting(strAdvancedSettingName, strPowerOffDevices);
+ }
+
+ int iSettingPowerOffDevices = LOCALISED_ID_NONE;
+ if (addresses[CECDEVICE_TV] && addresses[CECDEVICE_AUDIOSYSTEM])
+ iSettingPowerOffDevices = LOCALISED_ID_TV_AVR;
+ else if (addresses[CECDEVICE_TV])
+ iSettingPowerOffDevices = LOCALISED_ID_TV;
+ else if (addresses[CECDEVICE_AUDIOSYSTEM])
+ iSettingPowerOffDevices = LOCALISED_ID_AVR;
+ return SetSetting(strSettingName, iSettingPowerOffDevices) || bChanged;
+}
+
+CPeripheralCecAdapterUpdateThread::CPeripheralCecAdapterUpdateThread(
+ CPeripheralCecAdapter* adapter, libcec_configuration* configuration)
+ : CThread("CECAdapterUpdate"),
+ m_adapter(adapter),
+ m_configuration(*configuration),
+ m_bNextConfigurationScheduled(false),
+ m_bIsUpdating(true)
+{
+ m_nextConfiguration.Clear();
+ m_event.Reset();
+}
+
+CPeripheralCecAdapterUpdateThread::~CPeripheralCecAdapterUpdateThread(void)
+{
+ StopThread(false);
+ m_event.Set();
+ StopThread(true);
+}
+
+void CPeripheralCecAdapterUpdateThread::Signal(void)
+{
+ m_event.Set();
+}
+
+bool CPeripheralCecAdapterUpdateThread::UpdateConfiguration(libcec_configuration* configuration)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!configuration)
+ return false;
+
+ if (m_bIsUpdating)
+ {
+ m_bNextConfigurationScheduled = true;
+ m_nextConfiguration = *configuration;
+ }
+ else
+ {
+ m_configuration = *configuration;
+ m_event.Set();
+ }
+ return true;
+}
+
+bool CPeripheralCecAdapterUpdateThread::WaitReady(void)
+{
+ // don't wait if we're not powering up anything
+ if (m_configuration.wakeDevices.IsEmpty() && m_configuration.bActivateSource == 0)
+ return true;
+
+ // wait for the TV if we're configured to become the active source.
+ // wait for the first device in the wake list otherwise.
+ cec_logical_address waitFor =
+ (m_configuration.bActivateSource == 1) ? CECDEVICE_TV : m_configuration.wakeDevices.primary;
+
+ cec_power_status powerStatus(CEC_POWER_STATUS_UNKNOWN);
+ bool bContinue(true);
+ while (bContinue && !m_adapter->m_bStop && !m_bStop && powerStatus != CEC_POWER_STATUS_ON)
+ {
+ powerStatus = m_adapter->m_cecAdapter->GetDevicePowerStatus(waitFor);
+ if (powerStatus != CEC_POWER_STATUS_ON)
+ bContinue = !m_event.Wait(1000ms);
+ }
+
+ return powerStatus == CEC_POWER_STATUS_ON;
+}
+
+void CPeripheralCecAdapterUpdateThread::UpdateMenuLanguage(void)
+{
+ // request the menu language of the TV
+ if (m_adapter->m_bUseTVMenuLanguage == 1)
+ {
+ CLog::Log(LOGDEBUG, "{} - requesting the menu language of the TV", __FUNCTION__);
+ std::string language(m_adapter->m_cecAdapter->GetDeviceMenuLanguage(CECDEVICE_TV));
+ m_adapter->SetMenuLanguage(language.c_str());
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - using TV menu language is disabled", __FUNCTION__);
+ }
+}
+
+std::string CPeripheralCecAdapterUpdateThread::UpdateAudioSystemStatus(void)
+{
+ std::string strAmpName;
+
+ /* disable the mute setting when an amp is found, because the amp handles the mute setting and
+ set PCM output to 100% */
+ if (m_adapter->m_cecAdapter->IsActiveDeviceType(CEC_DEVICE_TYPE_AUDIO_SYSTEM))
+ {
+ // request the OSD name of the amp
+ std::string ampName(m_adapter->m_cecAdapter->GetDeviceOSDName(CECDEVICE_AUDIOSYSTEM));
+ CLog::Log(LOGDEBUG,
+ "{} - CEC capable amplifier found ({}). volume will be controlled on the amp",
+ __FUNCTION__, ampName);
+ strAmpName += ampName;
+
+ // set amp present
+ m_adapter->SetAudioSystemConnected(true);
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetMute(false);
+ appVolume->SetVolume(CApplicationVolumeHandling::VOLUME_MAXIMUM, false);
+ }
+ else
+ {
+ // set amp present
+ CLog::Log(LOGDEBUG, "{} - no CEC capable amplifier found", __FUNCTION__);
+ m_adapter->SetAudioSystemConnected(false);
+ }
+
+ return strAmpName;
+}
+
+bool CPeripheralCecAdapterUpdateThread::SetInitialConfiguration(void)
+{
+ // the option to make XBMC the active source is set
+ if (m_configuration.bActivateSource == 1)
+ m_adapter->m_cecAdapter->SetActiveSource();
+
+ // devices to wake are set
+ cec_logical_addresses tvOnly;
+ tvOnly.Clear();
+ tvOnly.Set(CECDEVICE_TV);
+ if (!m_configuration.wakeDevices.IsEmpty() &&
+ (m_configuration.wakeDevices != tvOnly || m_configuration.bActivateSource == 0))
+ m_adapter->m_cecAdapter->PowerOnDevices(CECDEVICE_BROADCAST);
+
+ // wait until devices are powered up
+ if (!WaitReady())
+ return false;
+
+ UpdateMenuLanguage();
+
+ // request the OSD name of the TV
+ std::string strNotification;
+ std::string tvName(m_adapter->m_cecAdapter->GetDeviceOSDName(CECDEVICE_TV));
+ strNotification = StringUtils::Format("{}: {}", g_localizeStrings.Get(36016), tvName);
+
+ std::string strAmpName = UpdateAudioSystemStatus();
+ if (!strAmpName.empty())
+ strNotification += StringUtils::Format("- {}", strAmpName);
+
+ m_adapter->m_bIsReady = true;
+
+ // and let the gui know that we're done
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000),
+ strNotification);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bIsUpdating = false;
+ return true;
+}
+
+bool CPeripheralCecAdapter::IsRunning(void) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsRunning;
+}
+
+void CPeripheralCecAdapterUpdateThread::Process(void)
+{
+ // set the initial configuration
+ if (!SetInitialConfiguration())
+ return;
+
+ // and wait for updates
+ bool bUpdate(false);
+ while (!m_bStop)
+ {
+ // update received
+ if (bUpdate || m_event.Wait(500ms))
+ {
+ if (m_bStop)
+ return;
+ // set the new configuration
+ libcec_configuration configuration;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ configuration = m_configuration;
+ m_bIsUpdating = false;
+ }
+
+ CLog::Log(LOGDEBUG, "{} - updating the configuration", __FUNCTION__);
+ bool bConfigSet(m_adapter->m_cecAdapter->SetConfiguration(&configuration));
+ // display message: config updated / failed to update
+ if (!bConfigSet)
+ CLog::Log(LOGERROR, "{} - libCEC couldn't set the new configuration", __FUNCTION__);
+ else
+ {
+ UpdateMenuLanguage();
+ UpdateAudioSystemStatus();
+ }
+
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(36000),
+ g_localizeStrings.Get(bConfigSet ? 36023 : 36024));
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if ((bUpdate = m_bNextConfigurationScheduled) == true)
+ {
+ // another update is scheduled
+ m_bNextConfigurationScheduled = false;
+ m_configuration = m_nextConfiguration;
+ }
+ else
+ {
+ // nothing left to do, wait for updates
+ m_bIsUpdating = false;
+ m_event.Reset();
+ }
+ }
+ }
+ }
+}
+
+void CPeripheralCecAdapter::OnDeviceRemoved(void)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bDeviceRemoved = true;
+}
+
+namespace PERIPHERALS
+{
+
+class CPeripheralCecAdapterReopenJob : public CJob
+{
+public:
+ CPeripheralCecAdapterReopenJob(CPeripheralCecAdapter* adapter) : m_adapter(adapter) {}
+ ~CPeripheralCecAdapterReopenJob() override = default;
+
+ bool DoWork(void) override { return m_adapter->ReopenConnection(false); }
+
+private:
+ CPeripheralCecAdapter* m_adapter;
+};
+
+}; // namespace PERIPHERALS
+
+bool CPeripheralCecAdapter::ReopenConnection(bool bAsync /* = false */)
+{
+ if (bAsync)
+ {
+ CServiceBroker::GetJobManager()->AddJob(new CPeripheralCecAdapterReopenJob(this), nullptr,
+ CJob::PRIORITY_NORMAL);
+ return true;
+ }
+
+ // stop running thread
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iExitCode = EXITCODE_RESTARTAPP;
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+ StopThread(false);
+ }
+ StopThread();
+
+ // reset all members to their defaults
+ ResetMembers();
+
+ // reopen the connection
+ return InitialiseFeature(FEATURE_CEC);
+}
+
+void CPeripheralCecAdapter::ActivateSource(void)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bActiveSourcePending = true;
+}
+
+void CPeripheralCecAdapter::ProcessActivateSource(void)
+{
+ bool bActivate(false);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bActivate = m_bActiveSourcePending;
+ m_bActiveSourcePending = false;
+ }
+
+ if (bActivate)
+ m_cecAdapter->SetActiveSource();
+}
+
+void CPeripheralCecAdapter::StandbyDevices(void)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bStandbyPending = true;
+}
+
+void CPeripheralCecAdapter::ProcessStandbyDevices(void)
+{
+ bool bStandby(false);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bStandby = m_bStandbyPending;
+ m_bStandbyPending = false;
+ if (bStandby)
+ m_bGoingToStandby = true;
+ }
+
+ if (bStandby)
+ {
+ if (!m_configuration.powerOffDevices.IsEmpty())
+ {
+ m_standbySent = CDateTime::GetCurrentDateTime();
+ m_cecAdapter->StandbyDevices(CECDEVICE_BROADCAST);
+ }
+ else if (m_bSendInactiveSource == 1)
+ {
+ CLog::Log(LOGDEBUG, "{} - sending inactive source commands", __FUNCTION__);
+ m_cecAdapter->SetInactiveView();
+ }
+ }
+}
+
+bool CPeripheralCecAdapter::ToggleDeviceState(CecStateChange mode /*= STATE_SWITCH_TOGGLE */,
+ bool forceType /*= false */)
+{
+ if (!IsRunning())
+ return false;
+ if (m_cecAdapter->IsLibCECActiveSource() &&
+ (mode == STATE_SWITCH_TOGGLE || mode == STATE_STANDBY))
+ {
+ CLog::Log(LOGDEBUG, "{} - putting CEC device on standby...", __FUNCTION__);
+ StandbyDevices();
+ return false;
+ }
+ else if (mode == STATE_SWITCH_TOGGLE || mode == STATE_ACTIVATE_SOURCE)
+ {
+ CLog::Log(LOGDEBUG, "{} - waking up CEC device...", __FUNCTION__);
+ ActivateSource();
+ return true;
+ }
+
+ return false;
+}
diff --git a/xbmc/peripherals/devices/PeripheralCecAdapter.h b/xbmc/peripherals/devices/PeripheralCecAdapter.h
new file mode 100644
index 0000000..b3755b3
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralCecAdapter.h
@@ -0,0 +1,226 @@
+/*
+ * 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
+
+#if !defined(HAVE_LIBCEC)
+#include "Peripheral.h"
+
+// an empty implementation, so CPeripherals can be compiled without a bunch of #ifdef's when libCEC
+// is not available
+namespace PERIPHERALS
+{
+class CPeripheralCecAdapter : public CPeripheral
+{
+public:
+ bool HasAudioControl(void) { return false; }
+ void VolumeUp(void) {}
+ void VolumeDown(void) {}
+ bool IsMuted(void) { return false; }
+ void ToggleMute(void) {}
+ bool ToggleDeviceState(CecStateChange mode = STATE_SWITCH_TOGGLE, bool forceType = false)
+ {
+ return false;
+ }
+
+ int GetButton(void) { return 0; }
+ unsigned int GetHoldTime(void) { return 0; }
+ void ResetButton(void) {}
+};
+} // namespace PERIPHERALS
+
+#else
+
+#include "PeripheralHID.h"
+#include "XBDateTime.h"
+#include "interfaces/AnnouncementManager.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <chrono>
+#include <queue>
+#include <vector>
+
+// undefine macro isset, it collides with function in cectypes.h
+#ifdef isset
+#undef isset
+#endif
+#include <libcec/cectypes.h>
+
+class CVariant;
+
+namespace CEC
+{
+class ICECAdapter;
+};
+
+namespace PERIPHERALS
+{
+class CPeripheralCecAdapterUpdateThread;
+class CPeripheralCecAdapterReopenJob;
+
+typedef struct
+{
+ int iButton;
+ unsigned int iDuration;
+} CecButtonPress;
+
+typedef enum
+{
+ VOLUME_CHANGE_NONE,
+ VOLUME_CHANGE_UP,
+ VOLUME_CHANGE_DOWN,
+ VOLUME_CHANGE_MUTE
+} CecVolumeChange;
+
+class CPeripheralCecAdapter : public CPeripheralHID,
+ public ANNOUNCEMENT::IAnnouncer,
+ private CThread
+{
+ friend class CPeripheralCecAdapterUpdateThread;
+ friend class CPeripheralCecAdapterReopenJob;
+
+public:
+ CPeripheralCecAdapter(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus);
+ ~CPeripheralCecAdapter(void) override;
+
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ // audio control
+ bool HasAudioControl(void);
+ void VolumeUp(void);
+ void VolumeDown(void);
+ void ToggleMute(void);
+ bool IsMuted(void);
+
+ // CPeripheral callbacks
+ void OnSettingChanged(const std::string& strChangedSetting) override;
+ void OnDeviceRemoved(void) override;
+
+ // input
+ int GetButton(void);
+ unsigned int GetHoldTime(void);
+ void ResetButton(void);
+
+ // public CEC methods
+ void ActivateSource(void);
+ void StandbyDevices(void);
+ bool ToggleDeviceState(CecStateChange mode = STATE_SWITCH_TOGGLE, bool forceType = false);
+
+private:
+ bool InitialiseFeature(const PeripheralFeature feature) override;
+ void ResetMembers(void);
+ void Process(void) override;
+ bool IsRunning(void) const;
+
+ bool OpenConnection(void);
+ bool ReopenConnection(bool bAsync = false);
+
+ void SetConfigurationFromSettings(void);
+ void SetConfigurationFromLibCEC(const CEC::libcec_configuration& config);
+ void SetVersionInfo(const CEC::libcec_configuration& configuration);
+
+ static void ReadLogicalAddresses(const std::string& strString,
+ CEC::cec_logical_addresses& addresses);
+ static void ReadLogicalAddresses(int iLocalisedId, CEC::cec_logical_addresses& addresses);
+ bool WriteLogicalAddresses(const CEC::cec_logical_addresses& addresses,
+ const std::string& strSettingName,
+ const std::string& strAdvancedSettingName);
+
+ void ProcessActivateSource(void);
+ void ProcessStandbyDevices(void);
+ void ProcessVolumeChange(void);
+
+ void PushCecKeypress(const CEC::cec_keypress& key);
+ void PushCecKeypress(const CecButtonPress& key);
+ void GetNextKey(void);
+
+ void SetAudioSystemConnected(bool bSetTo);
+ void SetMenuLanguage(const char* strLanguage);
+ void OnTvStandby(void);
+
+ // callbacks from libCEC
+ static void CecLogMessage(void* cbParam, const CEC::cec_log_message* message);
+ static void CecCommand(void* cbParam, const CEC::cec_command* command);
+ static void CecConfiguration(void* cbParam, const CEC::libcec_configuration* config);
+ static void CecAlert(void* cbParam,
+ const CEC::libcec_alert alert,
+ const CEC::libcec_parameter data);
+ static void CecSourceActivated(void* param,
+ const CEC::cec_logical_address address,
+ const uint8_t activated);
+ static void CecKeyPress(void* cbParam, const CEC::cec_keypress* key);
+
+ CEC::ICECAdapter* m_cecAdapter;
+ bool m_bStarted;
+ bool m_bHasButton;
+ bool m_bIsReady;
+ bool m_bHasConnectedAudioSystem;
+ std::string m_strMenuLanguage;
+ CDateTime m_standbySent;
+ std::vector<CecButtonPress> m_buttonQueue;
+ CecButtonPress m_currentButton;
+ std::queue<CecVolumeChange> m_volumeChangeQueue;
+ std::chrono::time_point<std::chrono::steady_clock> m_lastKeypress;
+ CecVolumeChange m_lastChange;
+ int m_iExitCode;
+ bool m_bIsMuted;
+ bool m_bGoingToStandby;
+ bool m_bIsRunning;
+ bool m_bDeviceRemoved;
+ CPeripheralCecAdapterUpdateThread* m_queryThread;
+ CEC::ICECCallbacks m_callbacks;
+ mutable CCriticalSection m_critSection;
+ CEC::libcec_configuration m_configuration;
+ bool m_bActiveSourcePending;
+ bool m_bStandbyPending;
+ CDateTime m_preventActivateSourceOnPlay;
+ bool m_bActiveSourceBeforeStandby;
+ bool m_bOnPlayReceived;
+ bool m_bPlaybackPaused;
+ std::string m_strComPort;
+ bool m_bPowerOnScreensaver;
+ bool m_bUseTVMenuLanguage;
+ bool m_bSendInactiveSource;
+ bool m_bPowerOffScreensaver;
+ bool m_bShutdownOnStandby;
+};
+
+class CPeripheralCecAdapterUpdateThread : public CThread
+{
+public:
+ CPeripheralCecAdapterUpdateThread(CPeripheralCecAdapter* adapter,
+ CEC::libcec_configuration* configuration);
+ ~CPeripheralCecAdapterUpdateThread(void) override;
+
+ void Signal(void);
+ bool UpdateConfiguration(CEC::libcec_configuration* configuration);
+
+protected:
+ void UpdateMenuLanguage(void);
+ std::string UpdateAudioSystemStatus(void);
+ bool WaitReady(void);
+ bool SetInitialConfiguration(void);
+ void Process(void) override;
+
+ CPeripheralCecAdapter* m_adapter;
+ CEvent m_event;
+ CCriticalSection m_critSection;
+ CEC::libcec_configuration m_configuration;
+ CEC::libcec_configuration m_nextConfiguration;
+ bool m_bNextConfigurationScheduled;
+ bool m_bIsUpdating;
+};
+} // namespace PERIPHERALS
+
+#endif
diff --git a/xbmc/peripherals/devices/PeripheralDisk.cpp b/xbmc/peripherals/devices/PeripheralDisk.cpp
new file mode 100644
index 0000000..9b0682c
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralDisk.cpp
@@ -0,0 +1,23 @@
+/*
+ * 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 "PeripheralDisk.h"
+
+#include "guilib/LocalizeStrings.h"
+
+using namespace PERIPHERALS;
+
+CPeripheralDisk::CPeripheralDisk(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : CPeripheral(manager, scanResult, bus)
+{
+ m_strDeviceName = scanResult.m_strDeviceName.empty() ? g_localizeStrings.Get(35003)
+ : scanResult.m_strDeviceName;
+ m_features.push_back(FEATURE_DISK);
+}
diff --git a/xbmc/peripherals/devices/PeripheralDisk.h b/xbmc/peripherals/devices/PeripheralDisk.h
new file mode 100644
index 0000000..f81b31e
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralDisk.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "Peripheral.h"
+
+namespace PERIPHERALS
+{
+class CPeripheralDisk : public CPeripheral
+{
+public:
+ CPeripheralDisk(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus);
+ ~CPeripheralDisk(void) override = default;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/devices/PeripheralHID.cpp b/xbmc/peripherals/devices/PeripheralHID.cpp
new file mode 100644
index 0000000..e3d061b
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralHID.cpp
@@ -0,0 +1,86 @@
+/*
+ * 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 "PeripheralHID.h"
+
+#include "guilib/LocalizeStrings.h"
+#include "input/InputManager.h"
+#include "peripherals/Peripherals.h"
+#include "utils/log.h"
+
+using namespace PERIPHERALS;
+
+CPeripheralHID::CPeripheralHID(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : CPeripheral(manager, scanResult, bus)
+{
+ m_strDeviceName = scanResult.m_strDeviceName.empty() ? g_localizeStrings.Get(35001)
+ : scanResult.m_strDeviceName;
+ m_features.push_back(FEATURE_HID);
+}
+
+CPeripheralHID::~CPeripheralHID(void)
+{
+ if (!m_strKeymap.empty() && !GetSettingBool("do_not_use_custom_keymap"))
+ {
+ CLog::Log(LOGDEBUG, "{} - switching active keymapping to: default", __FUNCTION__);
+ m_manager.GetInputManager().RemoveKeymap(m_strKeymap);
+ }
+}
+
+bool CPeripheralHID::InitialiseFeature(const PeripheralFeature feature)
+{
+ if (feature == FEATURE_HID && !m_bInitialised)
+ {
+ m_bInitialised = true;
+
+ if (HasSetting("keymap"))
+ m_strKeymap = GetSettingString("keymap");
+
+ if (m_strKeymap.empty())
+ {
+ m_strKeymap = StringUtils::Format("v{}p{}", VendorIdAsString(), ProductIdAsString());
+ SetSetting("keymap", m_strKeymap);
+ }
+
+ if (!IsSettingVisible("keymap"))
+ SetSettingVisible("do_not_use_custom_keymap", false);
+
+ if (!m_strKeymap.empty())
+ {
+ bool bKeymapEnabled(!GetSettingBool("do_not_use_custom_keymap"));
+ if (bKeymapEnabled)
+ {
+ CLog::Log(LOGDEBUG, "{} - adding keymapping for: {}", __FUNCTION__, m_strKeymap);
+ m_manager.GetInputManager().AddKeymap(m_strKeymap);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - removing keymapping for: {}", __FUNCTION__, m_strKeymap);
+ m_manager.GetInputManager().RemoveKeymap(m_strKeymap);
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "{} - initialised HID device ({}:{})", __FUNCTION__, m_strVendorId,
+ m_strProductId);
+ }
+
+ return CPeripheral::InitialiseFeature(feature);
+}
+
+void CPeripheralHID::OnSettingChanged(const std::string& strChangedSetting)
+{
+ if (m_bInitialised && ((StringUtils::EqualsNoCase(strChangedSetting, "keymap") &&
+ !GetSettingBool("do_not_use_custom_keymap")) ||
+ StringUtils::EqualsNoCase(strChangedSetting, "keymap_enabled")))
+ {
+ m_bInitialised = false;
+ InitialiseFeature(FEATURE_HID);
+ }
+}
diff --git a/xbmc/peripherals/devices/PeripheralHID.h b/xbmc/peripherals/devices/PeripheralHID.h
new file mode 100644
index 0000000..3b35dfd
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralHID.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "Peripheral.h"
+#include "input/XBMC_keyboard.h"
+
+namespace PERIPHERALS
+{
+class CPeripheralHID : public CPeripheral
+{
+public:
+ CPeripheralHID(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus);
+ ~CPeripheralHID(void) override;
+ bool InitialiseFeature(const PeripheralFeature feature) override;
+ virtual bool LookupSymAndUnicode(XBMC_keysym& keysym, uint8_t* key, char* unicode)
+ {
+ return false;
+ }
+ void OnSettingChanged(const std::string& strChangedSetting) override;
+
+protected:
+ std::string m_strKeymap;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/devices/PeripheralImon.cpp b/xbmc/peripherals/devices/PeripheralImon.cpp
new file mode 100644
index 0000000..6683f8b
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralImon.cpp
@@ -0,0 +1,90 @@
+/*
+ * 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 "PeripheralImon.h"
+
+#include "input/InputManager.h"
+#include "settings/Settings.h"
+#include "utils/log.h"
+
+using namespace PERIPHERALS;
+
+std::atomic<long> CPeripheralImon::m_lCountOfImonsConflictWithDInput(0L);
+
+CPeripheralImon::CPeripheralImon(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : CPeripheralHID(manager, scanResult, bus)
+{
+ m_features.push_back(FEATURE_IMON);
+ m_bImonConflictsWithDInput = false;
+}
+
+void CPeripheralImon::OnDeviceRemoved()
+{
+ if (m_bImonConflictsWithDInput)
+ {
+ if (--m_lCountOfImonsConflictWithDInput == 0)
+ ActionOnImonConflict(false);
+ }
+}
+
+bool CPeripheralImon::InitialiseFeature(const PeripheralFeature feature)
+{
+ if (feature == FEATURE_IMON)
+ {
+#if defined(TARGET_WINDOWS)
+ if (HasSetting("disable_winjoystick") && GetSettingBool("disable_winjoystick"))
+ m_bImonConflictsWithDInput = true;
+ else
+#endif // TARGET_WINDOWS
+ m_bImonConflictsWithDInput = false;
+
+ if (m_bImonConflictsWithDInput)
+ {
+ ++m_lCountOfImonsConflictWithDInput;
+ ActionOnImonConflict(true);
+ }
+ return CPeripheral::InitialiseFeature(feature);
+ }
+
+ return CPeripheralHID::InitialiseFeature(feature);
+}
+
+void CPeripheralImon::AddSetting(const std::string& strKey,
+ const std::shared_ptr<const CSetting>& setting,
+ int order)
+{
+#if !defined(TARGET_WINDOWS)
+ if (strKey.compare("disable_winjoystick") != 0)
+#endif // !TARGET_WINDOWS
+ CPeripheralHID::AddSetting(strKey, setting, order);
+}
+
+void CPeripheralImon::OnSettingChanged(const std::string& strChangedSetting)
+{
+ if (strChangedSetting.compare("disable_winjoystick") == 0)
+ {
+ if (m_bImonConflictsWithDInput && !GetSettingBool("disable_winjoystick"))
+ {
+ m_bImonConflictsWithDInput = false;
+ if (--m_lCountOfImonsConflictWithDInput == 0)
+ ActionOnImonConflict(false);
+ }
+ else if (!m_bImonConflictsWithDInput && GetSettingBool("disable_winjoystick"))
+ {
+ m_bImonConflictsWithDInput = true;
+ ++m_lCountOfImonsConflictWithDInput;
+ ActionOnImonConflict(true);
+ }
+ }
+}
+
+void CPeripheralImon::ActionOnImonConflict(bool deviceInserted /*= true*/)
+{
+}
diff --git a/xbmc/peripherals/devices/PeripheralImon.h b/xbmc/peripherals/devices/PeripheralImon.h
new file mode 100644
index 0000000..612d059
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralImon.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "PeripheralHID.h"
+
+#include <atomic>
+
+class CSetting;
+
+namespace PERIPHERALS
+{
+class CPeripheralImon : public CPeripheralHID
+{
+public:
+ CPeripheralImon(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus);
+ ~CPeripheralImon(void) override = default;
+ bool InitialiseFeature(const PeripheralFeature feature) override;
+ void OnSettingChanged(const std::string& strChangedSetting) override;
+ void OnDeviceRemoved() override;
+ void AddSetting(const std::string& strKey,
+ const std::shared_ptr<const CSetting>& setting,
+ int order) override;
+ inline bool IsImonConflictsWithDInput() { return m_bImonConflictsWithDInput; }
+ static inline long GetCountOfImonsConflictWithDInput()
+ {
+ return m_lCountOfImonsConflictWithDInput;
+ }
+ static void ActionOnImonConflict(bool deviceInserted = true);
+
+private:
+ bool m_bImonConflictsWithDInput;
+ static std::atomic<long> m_lCountOfImonsConflictWithDInput;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/devices/PeripheralJoystick.cpp b/xbmc/peripherals/devices/PeripheralJoystick.cpp
new file mode 100644
index 0000000..bccad91
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralJoystick.cpp
@@ -0,0 +1,538 @@
+/*
+ * Copyright (C) 2014-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 "PeripheralJoystick.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "application/Application.h"
+#include "games/GameServices.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerIDs.h"
+#include "games/controllers/ControllerManager.h"
+#include "input/InputManager.h"
+#include "input/joysticks/DeadzoneFilter.h"
+#include "input/joysticks/JoystickMonitor.h"
+#include "input/joysticks/JoystickTranslator.h"
+#include "input/joysticks/RumbleGenerator.h"
+#include "input/joysticks/interfaces/IDriverHandler.h"
+#include "input/joysticks/keymaps/KeymapHandling.h"
+#include "peripherals/Peripherals.h"
+#include "peripherals/addons/AddonButtonMap.h"
+#include "peripherals/bus/virtual/PeripheralBusAddon.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+
+using namespace KODI;
+using namespace JOYSTICK;
+using namespace PERIPHERALS;
+
+CPeripheralJoystick::CPeripheralJoystick(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : CPeripheral(manager, scanResult, bus),
+ m_requestedPort(JOYSTICK_PORT_UNKNOWN),
+ m_buttonCount(0),
+ m_hatCount(0),
+ m_axisCount(0),
+ m_motorCount(0),
+ m_supportsPowerOff(false),
+ m_rumbleGenerator(new CRumbleGenerator)
+{
+ m_features.push_back(FEATURE_JOYSTICK);
+ // FEATURE_RUMBLE conditionally added via SetMotorCount()
+}
+
+CPeripheralJoystick::~CPeripheralJoystick(void)
+{
+ if (m_rumbleGenerator)
+ {
+ m_rumbleGenerator->AbortRumble();
+ m_rumbleGenerator.reset();
+ }
+
+ if (m_joystickMonitor)
+ {
+ UnregisterInputHandler(m_joystickMonitor.get());
+ m_joystickMonitor.reset();
+ }
+
+ m_appInput.reset();
+ m_deadzoneFilter.reset();
+ m_buttonMap.reset();
+
+ // Wait for remaining install tasks
+ for (std::future<void>& task : m_installTasks)
+ task.wait();
+}
+
+bool CPeripheralJoystick::InitialiseFeature(const PeripheralFeature feature)
+{
+ bool bSuccess = false;
+
+ if (CPeripheral::InitialiseFeature(feature))
+ {
+ if (feature == FEATURE_JOYSTICK)
+ {
+ // Ensure an add-on is present to translate input
+ PeripheralAddonPtr addon = m_manager.GetAddonWithButtonMap(this);
+ if (!addon)
+ {
+ CLog::Log(LOGERROR, "CPeripheralJoystick: No button mapping add-on for {}", m_strLocation);
+ }
+ else
+ {
+ if (m_bus->InitializeProperties(*this))
+ bSuccess = true;
+ else
+ CLog::Log(LOGERROR, "CPeripheralJoystick: Invalid location ({})", m_strLocation);
+ }
+
+ if (bSuccess)
+ {
+ m_buttonMap =
+ std::make_unique<CAddonButtonMap>(this, addon, DEFAULT_CONTROLLER_ID, m_manager);
+ if (m_buttonMap->Load())
+ {
+ InitializeDeadzoneFiltering(*m_buttonMap);
+ InitializeControllerProfile(*m_buttonMap);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "CPeripheralJoystick: Failed to load button map for {}",
+ m_strLocation);
+ m_buttonMap.reset();
+ }
+
+ // Give joystick monitor priority over default controller
+ m_appInput.reset(
+ new CKeymapHandling(this, false, m_manager.GetInputManager().KeymapEnvironment()));
+ m_joystickMonitor.reset(new CJoystickMonitor);
+ RegisterInputHandler(m_joystickMonitor.get(), false);
+ }
+ }
+ else if (feature == FEATURE_RUMBLE)
+ {
+ bSuccess = true; // Nothing to do
+ }
+ else if (feature == FEATURE_POWER_OFF)
+ {
+ bSuccess = true; // Nothing to do
+ }
+ }
+
+ return bSuccess;
+}
+
+void CPeripheralJoystick::InitializeDeadzoneFiltering(IButtonMap& buttonMap)
+{
+ m_deadzoneFilter.reset(new CDeadzoneFilter(&buttonMap, this));
+}
+
+void CPeripheralJoystick::InitializeControllerProfile(IButtonMap& buttonMap)
+{
+ const std::string controllerId = buttonMap.GetAppearance();
+ if (controllerId.empty())
+ return;
+
+ auto controller = m_manager.GetControllerProfiles().GetController(controllerId);
+ if (controller)
+ CPeripheral::SetControllerProfile(controller);
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_controllerInstallMutex);
+
+ // Deposit controller into queue
+ m_controllersToInstall.emplace(controllerId);
+
+ // Clean up finished install tasks
+ m_installTasks.erase(std::remove_if(m_installTasks.begin(), m_installTasks.end(),
+ [](std::future<void>& task) {
+ return task.wait_for(std::chrono::seconds(0)) ==
+ std::future_status::ready;
+ }),
+ m_installTasks.end());
+
+ // Install controller off-thread
+ std::future<void> installTask = std::async(std::launch::async, [this]() {
+ // Withdraw controller from queue
+ std::string controllerToInstall;
+ {
+ std::unique_lock<CCriticalSection> lock(m_controllerInstallMutex);
+ if (!m_controllersToInstall.empty())
+ {
+ controllerToInstall = m_controllersToInstall.front();
+ m_controllersToInstall.pop();
+ }
+ }
+
+ // Do the install
+ GAME::ControllerPtr controller = InstallAsync(controllerToInstall);
+ if (controller)
+ CPeripheral::SetControllerProfile(controller);
+ });
+
+ // Hold the task to prevent the destructor from completing during an install
+ m_installTasks.emplace_back(std::move(installTask));
+ }
+}
+
+void CPeripheralJoystick::OnUserNotification()
+{
+ IInputReceiver* inputReceiver = m_appInput->GetInputReceiver(m_rumbleGenerator->ControllerID());
+ m_rumbleGenerator->NotifyUser(inputReceiver);
+}
+
+bool CPeripheralJoystick::TestFeature(PeripheralFeature feature)
+{
+ bool bSuccess = false;
+
+ switch (feature)
+ {
+ case FEATURE_RUMBLE:
+ {
+ IInputReceiver* inputReceiver =
+ m_appInput->GetInputReceiver(m_rumbleGenerator->ControllerID());
+ bSuccess = m_rumbleGenerator->DoTest(inputReceiver);
+ break;
+ }
+ case FEATURE_POWER_OFF:
+ if (m_supportsPowerOff)
+ {
+ PowerOff();
+ bSuccess = true;
+ }
+ break;
+ default:
+ break;
+ }
+
+ return bSuccess;
+}
+
+void CPeripheralJoystick::PowerOff()
+{
+ m_bus->PowerOff(m_strLocation);
+}
+
+void CPeripheralJoystick::RegisterJoystickDriverHandler(IDriverHandler* handler, bool bPromiscuous)
+{
+ std::unique_lock<CCriticalSection> lock(m_handlerMutex);
+
+ DriverHandler driverHandler = {handler, bPromiscuous};
+ m_driverHandlers.insert(m_driverHandlers.begin(), driverHandler);
+}
+
+void CPeripheralJoystick::UnregisterJoystickDriverHandler(IDriverHandler* handler)
+{
+ std::unique_lock<CCriticalSection> lock(m_handlerMutex);
+
+ m_driverHandlers.erase(std::remove_if(m_driverHandlers.begin(), m_driverHandlers.end(),
+ [handler](const DriverHandler& driverHandler) {
+ return driverHandler.handler == handler;
+ }),
+ m_driverHandlers.end());
+}
+
+IKeymap* CPeripheralJoystick::GetKeymap(const std::string& controllerId)
+{
+ return m_appInput->GetKeymap(controllerId);
+}
+
+GAME::ControllerPtr CPeripheralJoystick::ControllerProfile() const
+{
+ // Button map has the freshest state
+ if (m_buttonMap)
+ {
+ const std::string controllerId = m_buttonMap->GetAppearance();
+ if (!controllerId.empty())
+ {
+ auto controller = m_manager.GetControllerProfiles().GetController(controllerId);
+ if (controller)
+ return controller;
+ }
+ }
+
+ // Fall back to last set controller profile
+ if (m_controllerProfile)
+ return m_controllerProfile;
+
+ // Fall back to default controller
+ return m_manager.GetControllerProfiles().GetDefaultController();
+}
+
+void CPeripheralJoystick::SetControllerProfile(const KODI::GAME::ControllerPtr& controller)
+{
+ CPeripheral::SetControllerProfile(controller);
+
+ // Save preference to buttonmap
+ if (m_buttonMap)
+ {
+ if (m_buttonMap->SetAppearance(controller->ID()))
+ m_buttonMap->SaveButtonMap();
+ }
+}
+
+bool CPeripheralJoystick::OnButtonMotion(unsigned int buttonIndex, bool bPressed)
+{
+ // Silence debug log if controllers are not enabled
+ if (m_manager.GetInputManager().IsControllerEnabled())
+ {
+ CLog::Log(LOGDEBUG, "BUTTON [ {} ] on \"{}\" {}", buttonIndex, DeviceName(),
+ bPressed ? "pressed" : "released");
+ }
+
+ // Avoid sending activated input if the app is in the background
+ if (bPressed && !g_application.IsAppFocused())
+ return false;
+
+ m_lastActive = CDateTime::GetCurrentDateTime();
+
+ std::unique_lock<CCriticalSection> lock(m_handlerMutex);
+
+ // Check GUI setting and send button release if controllers are disabled
+ if (!m_manager.GetInputManager().IsControllerEnabled())
+ {
+ for (std::vector<DriverHandler>::iterator it = m_driverHandlers.begin();
+ it != m_driverHandlers.end(); ++it)
+ it->handler->OnButtonMotion(buttonIndex, false);
+ return true;
+ }
+
+ // Process promiscuous handlers
+ for (auto& it : m_driverHandlers)
+ {
+ if (it.bPromiscuous)
+ it.handler->OnButtonMotion(buttonIndex, bPressed);
+ }
+
+ bool bHandled = false;
+
+ // Process regular handlers until one is handled
+ for (auto& it : m_driverHandlers)
+ {
+ if (!it.bPromiscuous)
+ {
+ bHandled |= it.handler->OnButtonMotion(buttonIndex, bPressed);
+
+ // If button is released, force bHandled to false to notify all handlers.
+ // This avoids "sticking".
+ if (!bPressed)
+ bHandled = false;
+
+ // Once a button is handled, we're done
+ if (bHandled)
+ break;
+ }
+ }
+
+ return bHandled;
+}
+
+bool CPeripheralJoystick::OnHatMotion(unsigned int hatIndex, HAT_STATE state)
+{
+ // Silence debug log if controllers are not enabled
+ if (m_manager.GetInputManager().IsControllerEnabled())
+ {
+ CLog::Log(LOGDEBUG, "HAT [ {} ] on \"{}\" {}", hatIndex, DeviceName(),
+ CJoystickTranslator::HatStateToString(state));
+ }
+
+ // Avoid sending activated input if the app is in the background
+ if (state != HAT_STATE::NONE && !g_application.IsAppFocused())
+ return false;
+
+ m_lastActive = CDateTime::GetCurrentDateTime();
+
+ std::unique_lock<CCriticalSection> lock(m_handlerMutex);
+
+ // Check GUI setting and send hat unpressed if controllers are disabled
+ if (!m_manager.GetInputManager().IsControllerEnabled())
+ {
+ for (std::vector<DriverHandler>::iterator it = m_driverHandlers.begin();
+ it != m_driverHandlers.end(); ++it)
+ it->handler->OnHatMotion(hatIndex, HAT_STATE::NONE);
+ return true;
+ }
+
+ // Process promiscuous handlers
+ for (auto& it : m_driverHandlers)
+ {
+ if (it.bPromiscuous)
+ it.handler->OnHatMotion(hatIndex, state);
+ }
+
+ bool bHandled = false;
+
+ // Process regular handlers until one is handled
+ for (auto& it : m_driverHandlers)
+ {
+ if (!it.bPromiscuous)
+ {
+ bHandled |= it.handler->OnHatMotion(hatIndex, state);
+
+ // If hat is centered, force bHandled to false to notify all handlers.
+ // This avoids "sticking".
+ if (state == HAT_STATE::NONE)
+ bHandled = false;
+
+ // Once a hat is handled, we're done
+ if (bHandled)
+ break;
+ }
+ }
+
+ return bHandled;
+}
+
+bool CPeripheralJoystick::OnAxisMotion(unsigned int axisIndex, float position)
+{
+ // Get axis properties
+ int center = 0;
+ unsigned int range = 1;
+ if (m_buttonMap)
+ m_buttonMap->GetAxisProperties(axisIndex, center, range);
+
+ // Apply deadzone filtering
+ if (center == 0 && m_deadzoneFilter)
+ position = m_deadzoneFilter->FilterAxis(axisIndex, position);
+
+ // Avoid sending activated input if the app is in the background
+ if (position != static_cast<float>(center) && !g_application.IsAppFocused())
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_handlerMutex);
+
+ // Check GUI setting and send analog axis centered if controllers are disabled
+ if (!m_manager.GetInputManager().IsControllerEnabled())
+ {
+ for (std::vector<DriverHandler>::iterator it = m_driverHandlers.begin();
+ it != m_driverHandlers.end(); ++it)
+ it->handler->OnAxisMotion(axisIndex, static_cast<float>(center), center, range);
+ return true;
+ }
+
+ // Process promiscuous handlers
+ for (auto& it : m_driverHandlers)
+ {
+ if (it.bPromiscuous)
+ it.handler->OnAxisMotion(axisIndex, position, center, range);
+ }
+
+ bool bHandled = false;
+
+ // Process regular handlers until one is handled
+ for (auto& it : m_driverHandlers)
+ {
+ if (!it.bPromiscuous)
+ {
+ bHandled |= it.handler->OnAxisMotion(axisIndex, position, center, range);
+
+ // If axis is centered, force bHandled to false to notify all handlers.
+ // This avoids "sticking".
+ if (position == static_cast<float>(center))
+ bHandled = false;
+
+ // Once an axis is handled, we're done
+ if (bHandled)
+ break;
+ }
+ }
+
+ if (bHandled)
+ m_lastActive = CDateTime::GetCurrentDateTime();
+
+ return bHandled;
+}
+
+void CPeripheralJoystick::OnInputFrame(void)
+{
+ std::unique_lock<CCriticalSection> lock(m_handlerMutex);
+
+ for (auto& it : m_driverHandlers)
+ it.handler->OnInputFrame();
+}
+
+bool CPeripheralJoystick::SetMotorState(unsigned int motorIndex, float magnitude)
+{
+ bool bHandled = false;
+
+ if (m_mappedBusType == PERIPHERAL_BUS_ADDON)
+ {
+ CPeripheralBusAddon* addonBus = static_cast<CPeripheralBusAddon*>(m_bus);
+ if (addonBus)
+ {
+ bHandled = addonBus->SendRumbleEvent(m_strLocation, motorIndex, magnitude);
+ }
+ }
+ return bHandled;
+}
+
+void CPeripheralJoystick::SetMotorCount(unsigned int motorCount)
+{
+ m_motorCount = motorCount;
+
+ if (m_motorCount == 0)
+ m_features.erase(std::remove(m_features.begin(), m_features.end(), FEATURE_RUMBLE),
+ m_features.end());
+ else if (std::find(m_features.begin(), m_features.end(), FEATURE_RUMBLE) == m_features.end())
+ m_features.push_back(FEATURE_RUMBLE);
+}
+
+void CPeripheralJoystick::SetSupportsPowerOff(bool bSupportsPowerOff)
+{
+ m_supportsPowerOff = bSupportsPowerOff;
+
+ if (!m_supportsPowerOff)
+ m_features.erase(std::remove(m_features.begin(), m_features.end(), FEATURE_POWER_OFF),
+ m_features.end());
+ else if (std::find(m_features.begin(), m_features.end(), FEATURE_POWER_OFF) == m_features.end())
+ m_features.push_back(FEATURE_POWER_OFF);
+}
+
+GAME::ControllerPtr CPeripheralJoystick::InstallAsync(const std::string& controllerId)
+{
+ GAME::ControllerPtr controller;
+
+ // Only 1 install at a time. Remaining installs will wake when this one
+ // is done.
+ std::unique_lock<CCriticalSection> lockInstall(m_manager.GetAddonInstallMutex());
+
+ CLog::LogF(LOGDEBUG, "Installing {}", controllerId);
+
+ if (InstallSync(controllerId))
+ controller = m_manager.GetControllerProfiles().GetController(controllerId);
+ else
+ CLog::LogF(LOGERROR, "Failed to install {}", controllerId);
+
+ return controller;
+}
+
+bool CPeripheralJoystick::InstallSync(const std::string& controllerId)
+{
+ // If the addon isn't installed we need to install it
+ bool installed = CServiceBroker::GetAddonMgr().IsAddonInstalled(controllerId);
+ if (!installed)
+ {
+ ADDON::AddonPtr installedAddon;
+ installed = ADDON::CAddonInstaller::GetInstance().InstallModal(
+ controllerId, installedAddon, ADDON::InstallModalPrompt::CHOICE_NO);
+ }
+
+ if (installed)
+ {
+ // Make sure add-on is enabled
+ if (CServiceBroker::GetAddonMgr().IsAddonDisabled(controllerId))
+ CServiceBroker::GetAddonMgr().EnableAddon(controllerId);
+ }
+
+ return installed;
+}
diff --git a/xbmc/peripherals/devices/PeripheralJoystick.h b/xbmc/peripherals/devices/PeripheralJoystick.h
new file mode 100644
index 0000000..aa55ad7
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralJoystick.h
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2014-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 "Peripheral.h"
+#include "XBDateTime.h"
+#include "games/controllers/ControllerTypes.h"
+#include "input/joysticks/JoystickTypes.h"
+#include "input/joysticks/interfaces/IDriverReceiver.h"
+#include "threads/CriticalSection.h"
+
+#include <future>
+#include <memory>
+#include <queue>
+#include <string>
+#include <vector>
+
+#define JOYSTICK_PORT_UNKNOWN (-1)
+
+namespace KODI
+{
+namespace JOYSTICK
+{
+class CDeadzoneFilter;
+class CKeymapHandling;
+class CRumbleGenerator;
+class IButtonMap;
+class IDriverHandler;
+class IInputHandler;
+} // namespace JOYSTICK
+} // namespace KODI
+
+namespace PERIPHERALS
+{
+class CPeripherals;
+
+class CPeripheralJoystick : public CPeripheral, //! @todo extend CPeripheralHID
+ public KODI::JOYSTICK::IDriverReceiver
+{
+public:
+ CPeripheralJoystick(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus);
+
+ ~CPeripheralJoystick(void) override;
+
+ // implementation of CPeripheral
+ bool InitialiseFeature(const PeripheralFeature feature) override;
+ void OnUserNotification() override;
+ bool TestFeature(PeripheralFeature feature) override;
+ void RegisterJoystickDriverHandler(KODI::JOYSTICK::IDriverHandler* handler,
+ bool bPromiscuous) override;
+ void UnregisterJoystickDriverHandler(KODI::JOYSTICK::IDriverHandler* handler) override;
+ KODI::JOYSTICK::IDriverReceiver* GetDriverReceiver() override { return this; }
+ IKeymap* GetKeymap(const std::string& controllerId) override;
+ CDateTime LastActive() override { return m_lastActive; }
+ KODI::GAME::ControllerPtr ControllerProfile() const override;
+ void SetControllerProfile(const KODI::GAME::ControllerPtr& controller) override;
+
+ bool OnButtonMotion(unsigned int buttonIndex, bool bPressed);
+ bool OnHatMotion(unsigned int hatIndex, KODI::JOYSTICK::HAT_STATE state);
+ bool OnAxisMotion(unsigned int axisIndex, float position);
+ void OnInputFrame(void);
+
+ // implementation of IDriverReceiver
+ bool SetMotorState(unsigned int motorIndex, float magnitude) override;
+
+ /*!
+ * \brief Get the name of the driver or API providing this joystick
+ */
+ const std::string& Provider(void) const { return m_strProvider; }
+
+ /*!
+ * \brief Get the specific port number requested by this joystick
+ *
+ * This could indicate that the joystick is connected to a hardware port
+ * with a number label; some controllers, such as the Xbox 360 controller,
+ * also have LEDs that indicate the controller is on a specific port.
+ *
+ * \return The 0-indexed port number, or JOYSTICK_PORT_UNKNOWN if no port is requested
+ */
+ int RequestedPort(void) const { return m_requestedPort; }
+
+ /*!
+ * \brief Get the number of elements reported by the driver
+ */
+ unsigned int ButtonCount(void) const { return m_buttonCount; }
+ unsigned int HatCount(void) const { return m_hatCount; }
+ unsigned int AxisCount(void) const { return m_axisCount; }
+ unsigned int MotorCount(void) const { return m_motorCount; }
+ bool SupportsPowerOff(void) const { return m_supportsPowerOff; }
+
+ /*!
+ * \brief Set joystick properties
+ */
+ void SetProvider(const std::string& provider) { m_strProvider = provider; }
+ void SetRequestedPort(int port) { m_requestedPort = port; }
+ void SetButtonCount(unsigned int buttonCount) { m_buttonCount = buttonCount; }
+ void SetHatCount(unsigned int hatCount) { m_hatCount = hatCount; }
+ void SetAxisCount(unsigned int axisCount) { m_axisCount = axisCount; }
+ void SetMotorCount(unsigned int motorCount); // specialized to update m_features
+ void SetSupportsPowerOff(bool bSupportsPowerOff); // specialized to update m_features
+
+protected:
+ void InitializeDeadzoneFiltering(KODI::JOYSTICK::IButtonMap& buttonMap);
+ void InitializeControllerProfile(KODI::JOYSTICK::IButtonMap& buttonMap);
+
+ void PowerOff();
+
+ // Helper functions
+ KODI::GAME::ControllerPtr InstallAsync(const std::string& controllerId);
+ static bool InstallSync(const std::string& controllerId);
+
+ struct DriverHandler
+ {
+ KODI::JOYSTICK::IDriverHandler* handler;
+ bool bPromiscuous;
+ };
+
+ // State parameters
+ std::string m_strProvider;
+ int m_requestedPort;
+ unsigned int m_buttonCount;
+ unsigned int m_hatCount;
+ unsigned int m_axisCount;
+ unsigned int m_motorCount;
+ bool m_supportsPowerOff;
+ CDateTime m_lastActive;
+ std::queue<std::string> m_controllersToInstall;
+ std::vector<std::future<void>> m_installTasks;
+
+ // Input clients
+ std::unique_ptr<KODI::JOYSTICK::CKeymapHandling> m_appInput;
+ std::unique_ptr<KODI::JOYSTICK::CRumbleGenerator> m_rumbleGenerator;
+ std::unique_ptr<KODI::JOYSTICK::IInputHandler> m_joystickMonitor;
+ std::unique_ptr<KODI::JOYSTICK::IButtonMap> m_buttonMap;
+ std::unique_ptr<KODI::JOYSTICK::CDeadzoneFilter> m_deadzoneFilter;
+ std::vector<DriverHandler> m_driverHandlers;
+
+ // Synchronization parameters
+ CCriticalSection m_handlerMutex;
+ CCriticalSection m_controllerInstallMutex;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/devices/PeripheralKeyboard.cpp b/xbmc/peripherals/devices/PeripheralKeyboard.cpp
new file mode 100644
index 0000000..271d8b1
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralKeyboard.cpp
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017-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 "PeripheralKeyboard.h"
+
+#include "games/GameServices.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerManager.h"
+#include "input/InputManager.h"
+#include "peripherals/Peripherals.h"
+
+#include <mutex>
+#include <sstream>
+
+using namespace KODI;
+using namespace PERIPHERALS;
+
+CPeripheralKeyboard::CPeripheralKeyboard(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : CPeripheral(manager, scanResult, bus)
+{
+ // Initialize CPeripheral
+ m_features.push_back(FEATURE_KEYBOARD);
+}
+
+CPeripheralKeyboard::~CPeripheralKeyboard(void)
+{
+ m_manager.GetInputManager().UnregisterKeyboardDriverHandler(this);
+}
+
+bool CPeripheralKeyboard::InitialiseFeature(const PeripheralFeature feature)
+{
+ bool bSuccess = false;
+
+ if (CPeripheral::InitialiseFeature(feature))
+ {
+ switch (feature)
+ {
+ case FEATURE_KEYBOARD:
+ {
+ m_manager.GetInputManager().RegisterKeyboardDriverHandler(this);
+ break;
+ }
+ default:
+ break;
+ }
+
+ bSuccess = true;
+ }
+
+ return bSuccess;
+}
+
+void CPeripheralKeyboard::RegisterKeyboardDriverHandler(
+ KODI::KEYBOARD::IKeyboardDriverHandler* handler, bool bPromiscuous)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ KeyboardHandle handle{handler, bPromiscuous};
+ m_keyboardHandlers.insert(m_keyboardHandlers.begin(), handle);
+}
+
+void CPeripheralKeyboard::UnregisterKeyboardDriverHandler(
+ KODI::KEYBOARD::IKeyboardDriverHandler* handler)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ auto it =
+ std::find_if(m_keyboardHandlers.begin(), m_keyboardHandlers.end(),
+ [handler](const KeyboardHandle& handle) { return handle.handler == handler; });
+
+ if (it != m_keyboardHandlers.end())
+ m_keyboardHandlers.erase(it);
+}
+
+GAME::ControllerPtr CPeripheralKeyboard::ControllerProfile() const
+{
+ if (m_controllerProfile)
+ return m_controllerProfile;
+
+ return m_manager.GetControllerProfiles().GetDefaultKeyboard();
+}
+
+bool CPeripheralKeyboard::OnKeyPress(const CKey& key)
+{
+ m_lastActive = CDateTime::GetCurrentDateTime();
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ bool bHandled = false;
+
+ // Process promiscuous handlers
+ for (const KeyboardHandle& handle : m_keyboardHandlers)
+ {
+ if (handle.bPromiscuous)
+ handle.handler->OnKeyPress(key);
+ }
+
+ // Process handlers until one is handled
+ for (const KeyboardHandle& handle : m_keyboardHandlers)
+ {
+ if (!handle.bPromiscuous)
+ {
+ bHandled = handle.handler->OnKeyPress(key);
+ if (bHandled)
+ break;
+ }
+ }
+
+ return bHandled;
+}
+
+void CPeripheralKeyboard::OnKeyRelease(const CKey& key)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ for (const KeyboardHandle& handle : m_keyboardHandlers)
+ handle.handler->OnKeyRelease(key);
+}
diff --git a/xbmc/peripherals/devices/PeripheralKeyboard.h b/xbmc/peripherals/devices/PeripheralKeyboard.h
new file mode 100644
index 0000000..2cf2934
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralKeyboard.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017-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 "Peripheral.h"
+#include "XBDateTime.h"
+#include "input/keyboard/interfaces/IKeyboardDriverHandler.h"
+#include "threads/CriticalSection.h"
+
+#include <vector>
+
+namespace PERIPHERALS
+{
+class CPeripheralKeyboard : public CPeripheral, public KODI::KEYBOARD::IKeyboardDriverHandler
+{
+public:
+ CPeripheralKeyboard(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus);
+
+ ~CPeripheralKeyboard(void) override;
+
+ // implementation of CPeripheral
+ bool InitialiseFeature(const PeripheralFeature feature) override;
+ void RegisterKeyboardDriverHandler(KODI::KEYBOARD::IKeyboardDriverHandler* handler,
+ bool bPromiscuous) override;
+ void UnregisterKeyboardDriverHandler(KODI::KEYBOARD::IKeyboardDriverHandler* handler) override;
+ CDateTime LastActive() override { return m_lastActive; }
+ KODI::GAME::ControllerPtr ControllerProfile() const override;
+
+ // implementation of IKeyboardDriverHandler
+ bool OnKeyPress(const CKey& key) override;
+ void OnKeyRelease(const CKey& key) override;
+
+private:
+ struct KeyboardHandle
+ {
+ KODI::KEYBOARD::IKeyboardDriverHandler* handler;
+ bool bPromiscuous;
+ };
+
+ std::vector<KeyboardHandle> m_keyboardHandlers;
+ CCriticalSection m_mutex;
+ CDateTime m_lastActive;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/devices/PeripheralMouse.cpp b/xbmc/peripherals/devices/PeripheralMouse.cpp
new file mode 100644
index 0000000..4f26236
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralMouse.cpp
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2017-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 "PeripheralMouse.h"
+
+#include "games/GameServices.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerManager.h"
+#include "input/InputManager.h"
+#include "peripherals/Peripherals.h"
+
+#include <mutex>
+#include <sstream>
+
+using namespace KODI;
+using namespace PERIPHERALS;
+
+CPeripheralMouse::CPeripheralMouse(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : CPeripheral(manager, scanResult, bus)
+{
+ // Initialize CPeripheral
+ m_features.push_back(FEATURE_MOUSE);
+}
+
+CPeripheralMouse::~CPeripheralMouse(void)
+{
+ m_manager.GetInputManager().UnregisterMouseDriverHandler(this);
+}
+
+bool CPeripheralMouse::InitialiseFeature(const PeripheralFeature feature)
+{
+ bool bSuccess = false;
+
+ if (CPeripheral::InitialiseFeature(feature))
+ {
+ if (feature == FEATURE_MOUSE)
+ {
+ m_manager.GetInputManager().RegisterMouseDriverHandler(this);
+ }
+
+ bSuccess = true;
+ }
+
+ return bSuccess;
+}
+
+void CPeripheralMouse::RegisterMouseDriverHandler(MOUSE::IMouseDriverHandler* handler,
+ bool bPromiscuous)
+{
+ using namespace KEYBOARD;
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ MouseHandle handle{handler, bPromiscuous};
+ m_mouseHandlers.insert(m_mouseHandlers.begin(), handle);
+}
+
+void CPeripheralMouse::UnregisterMouseDriverHandler(MOUSE::IMouseDriverHandler* handler)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ auto it =
+ std::find_if(m_mouseHandlers.begin(), m_mouseHandlers.end(),
+ [handler](const MouseHandle& handle) { return handle.handler == handler; });
+
+ if (it != m_mouseHandlers.end())
+ m_mouseHandlers.erase(it);
+}
+
+GAME::ControllerPtr CPeripheralMouse::ControllerProfile() const
+{
+ if (m_controllerProfile)
+ return m_controllerProfile;
+
+ return m_manager.GetControllerProfiles().GetDefaultMouse();
+}
+
+bool CPeripheralMouse::OnPosition(int x, int y)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ bool bHandled = false;
+
+ // Process promiscuous handlers
+ for (const MouseHandle& handle : m_mouseHandlers)
+ {
+ if (handle.bPromiscuous)
+ handle.handler->OnPosition(x, y);
+ }
+
+ // Process handlers until one is handled
+ for (const MouseHandle& handle : m_mouseHandlers)
+ {
+ if (!handle.bPromiscuous)
+ {
+ bHandled = handle.handler->OnPosition(x, y);
+ if (bHandled)
+ break;
+ }
+ }
+
+ if (bHandled)
+ m_lastActive = CDateTime::GetCurrentDateTime();
+
+ return bHandled;
+}
+
+bool CPeripheralMouse::OnButtonPress(MOUSE::BUTTON_ID button)
+{
+ m_lastActive = CDateTime::GetCurrentDateTime();
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ bool bHandled = false;
+
+ // Process promiscuous handlers
+ for (const MouseHandle& handle : m_mouseHandlers)
+ {
+ if (handle.bPromiscuous)
+ handle.handler->OnButtonPress(button);
+ }
+
+ // Process handlers until one is handled
+ for (const MouseHandle& handle : m_mouseHandlers)
+ {
+ if (!handle.bPromiscuous)
+ {
+ bHandled = handle.handler->OnButtonPress(button);
+ if (bHandled)
+ break;
+ }
+ }
+
+ return bHandled;
+}
+
+void CPeripheralMouse::OnButtonRelease(MOUSE::BUTTON_ID button)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ for (const MouseHandle& handle : m_mouseHandlers)
+ handle.handler->OnButtonRelease(button);
+}
diff --git a/xbmc/peripherals/devices/PeripheralMouse.h b/xbmc/peripherals/devices/PeripheralMouse.h
new file mode 100644
index 0000000..b46776d
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralMouse.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017-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 "Peripheral.h"
+#include "XBDateTime.h"
+#include "input/mouse/interfaces/IMouseDriverHandler.h"
+#include "threads/CriticalSection.h"
+
+#include <vector>
+
+namespace PERIPHERALS
+{
+class CPeripheralMouse : public CPeripheral, public KODI::MOUSE::IMouseDriverHandler
+{
+public:
+ CPeripheralMouse(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus);
+
+ ~CPeripheralMouse(void) override;
+
+ // implementation of CPeripheral
+ bool InitialiseFeature(const PeripheralFeature feature) override;
+ void RegisterMouseDriverHandler(KODI::MOUSE::IMouseDriverHandler* handler,
+ bool bPromiscuous) override;
+ void UnregisterMouseDriverHandler(KODI::MOUSE::IMouseDriverHandler* handler) override;
+ CDateTime LastActive() override { return m_lastActive; }
+ KODI::GAME::ControllerPtr ControllerProfile() const override;
+
+ // implementation of IMouseDriverHandler
+ bool OnPosition(int x, int y) override;
+ bool OnButtonPress(KODI::MOUSE::BUTTON_ID button) override;
+ void OnButtonRelease(KODI::MOUSE::BUTTON_ID button) override;
+
+private:
+ struct MouseHandle
+ {
+ KODI::MOUSE::IMouseDriverHandler* handler;
+ bool bPromiscuous;
+ };
+
+ std::vector<MouseHandle> m_mouseHandlers;
+ CCriticalSection m_mutex;
+ CDateTime m_lastActive;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/devices/PeripheralNIC.cpp b/xbmc/peripherals/devices/PeripheralNIC.cpp
new file mode 100644
index 0000000..a5b5c32
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralNIC.cpp
@@ -0,0 +1,23 @@
+/*
+ * 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 "PeripheralNIC.h"
+
+#include "guilib/LocalizeStrings.h"
+
+using namespace PERIPHERALS;
+
+CPeripheralNIC::CPeripheralNIC(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : CPeripheral(manager, scanResult, bus)
+{
+ m_strDeviceName = scanResult.m_strDeviceName.empty() ? g_localizeStrings.Get(35002)
+ : scanResult.m_strDeviceName;
+ m_features.push_back(FEATURE_NIC);
+}
diff --git a/xbmc/peripherals/devices/PeripheralNIC.h b/xbmc/peripherals/devices/PeripheralNIC.h
new file mode 100644
index 0000000..01fe5f3
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralNIC.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "Peripheral.h"
+
+namespace PERIPHERALS
+{
+class CPeripheralNIC : public CPeripheral
+{
+public:
+ CPeripheralNIC(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus);
+ ~CPeripheralNIC(void) override = default;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/devices/PeripheralNyxboard.cpp b/xbmc/peripherals/devices/PeripheralNyxboard.cpp
new file mode 100644
index 0000000..548d8cf
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralNyxboard.cpp
@@ -0,0 +1,55 @@
+/*
+ * 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 "PeripheralNyxboard.h"
+
+#include "PeripheralHID.h"
+#include "application/Application.h"
+#include "utils/log.h"
+
+using namespace PERIPHERALS;
+
+CPeripheralNyxboard::CPeripheralNyxboard(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : CPeripheralHID(manager, scanResult, bus)
+{
+ m_features.push_back(FEATURE_NYXBOARD);
+}
+
+bool CPeripheralNyxboard::LookupSymAndUnicode(XBMC_keysym& keysym, uint8_t* key, char* unicode)
+{
+ std::string strCommand;
+ if (keysym.sym == XBMCK_F7 && keysym.mod == XBMCKMOD_NONE &&
+ GetSettingBool("enable_flip_commands"))
+ {
+ /* switched to keyboard side */
+ CLog::Log(LOGDEBUG, "{} - switched to keyboard side", __FUNCTION__);
+ strCommand = GetSettingString("flip_keyboard");
+ }
+ else if (keysym.sym == XBMCK_F7 && keysym.mod == XBMCKMOD_LCTRL &&
+ GetSettingBool("enable_flip_commands"))
+ {
+ /* switched to remote side */
+ CLog::Log(LOGDEBUG, "{} - switched to remote side", __FUNCTION__);
+ strCommand = GetSettingString("flip_remote");
+ }
+
+ if (!strCommand.empty())
+ {
+ CLog::Log(LOGDEBUG, "{} - executing command '{}'", __FUNCTION__, strCommand);
+ if (g_application.ExecuteXBMCAction(strCommand))
+ {
+ *key = 0;
+ *unicode = (char)0;
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/peripherals/devices/PeripheralNyxboard.h b/xbmc/peripherals/devices/PeripheralNyxboard.h
new file mode 100644
index 0000000..f81c3ef
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralNyxboard.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "PeripheralHID.h"
+
+namespace PERIPHERALS
+{
+class CPeripheralNyxboard : public CPeripheralHID
+{
+public:
+ CPeripheralNyxboard(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus);
+ ~CPeripheralNyxboard(void) override = default;
+ bool LookupSymAndUnicode(XBMC_keysym& keysym, uint8_t* key, char* unicode) override;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/devices/PeripheralTuner.cpp b/xbmc/peripherals/devices/PeripheralTuner.cpp
new file mode 100644
index 0000000..770656d
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralTuner.cpp
@@ -0,0 +1,19 @@
+/*
+ * 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 "PeripheralTuner.h"
+
+using namespace PERIPHERALS;
+
+CPeripheralTuner::CPeripheralTuner(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus)
+ : CPeripheral(manager, scanResult, bus)
+{
+ m_features.push_back(FEATURE_TUNER);
+}
diff --git a/xbmc/peripherals/devices/PeripheralTuner.h b/xbmc/peripherals/devices/PeripheralTuner.h
new file mode 100644
index 0000000..aedfc12
--- /dev/null
+++ b/xbmc/peripherals/devices/PeripheralTuner.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "Peripheral.h"
+
+namespace PERIPHERALS
+{
+class CPeripheralTuner : public CPeripheral
+{
+public:
+ CPeripheralTuner(CPeripherals& manager,
+ const PeripheralScanResult& scanResult,
+ CPeripheralBus* bus);
+ ~CPeripheralTuner(void) override = default;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/dialogs/CMakeLists.txt b/xbmc/peripherals/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..28aca66
--- /dev/null
+++ b/xbmc/peripherals/dialogs/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES GUIDialogPeripherals.cpp
+ GUIDialogPeripheralSettings.cpp)
+
+set(HEADERS GUIDialogPeripherals.h
+ GUIDialogPeripheralSettings.h)
+
+core_add_library(peripherals_dialogs)
diff --git a/xbmc/peripherals/dialogs/GUIDialogPeripheralSettings.cpp b/xbmc/peripherals/dialogs/GUIDialogPeripheralSettings.cpp
new file mode 100644
index 0000000..73816df
--- /dev/null
+++ b/xbmc/peripherals/dialogs/GUIDialogPeripheralSettings.cpp
@@ -0,0 +1,308 @@
+/*
+ * 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 "GUIDialogPeripheralSettings.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "games/controllers/Controller.h"
+#include "games/controllers/ControllerManager.h"
+#include "guilib/GUIMessage.h"
+#include "peripherals/Peripherals.h"
+#include "settings/SettingAddon.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingSection.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <string_view>
+#include <utility>
+
+using namespace KODI;
+using namespace PERIPHERALS;
+
+// Settings for peripherals
+constexpr std::string_view SETTING_APPEARANCE = "appearance";
+
+CGUIDialogPeripheralSettings::CGUIDialogPeripheralSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_PERIPHERAL_SETTINGS, "DialogSettings.xml"),
+ m_item(NULL)
+{
+}
+
+CGUIDialogPeripheralSettings::~CGUIDialogPeripheralSettings()
+{
+ if (m_item != NULL)
+ delete m_item;
+
+ m_settingsMap.clear();
+}
+
+bool CGUIDialogPeripheralSettings::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_CLICKED &&
+ message.GetSenderId() == CONTROL_SETTINGS_CUSTOM_BUTTON)
+ {
+ OnResetSettings();
+ return true;
+ }
+
+ return CGUIDialogSettingsManualBase::OnMessage(message);
+}
+
+void CGUIDialogPeripheralSettings::RegisterPeripheralManager(CPeripherals& manager)
+{
+ m_manager = &manager;
+}
+
+void CGUIDialogPeripheralSettings::UnregisterPeripheralManager()
+{
+ m_manager = nullptr;
+}
+
+void CGUIDialogPeripheralSettings::SetFileItem(const CFileItem* item)
+{
+ if (item == NULL)
+ return;
+
+ if (m_item != NULL)
+ delete m_item;
+
+ m_item = new CFileItem(*item);
+}
+
+void CGUIDialogPeripheralSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string& settingId = setting->GetId();
+
+ // we need to copy the new value of the setting from the copy to the
+ // original setting
+ std::map<std::string, std::shared_ptr<CSetting>>::iterator itSetting =
+ m_settingsMap.find(settingId);
+ if (itSetting == m_settingsMap.end())
+ return;
+
+ itSetting->second->FromString(setting->ToString());
+
+ // Get peripheral associated with this setting
+ PeripheralPtr peripheral;
+ if (m_item != nullptr)
+ peripheral = CServiceBroker::GetPeripherals().GetByPath(m_item->GetPath());
+
+ if (!peripheral)
+ return;
+
+ if (settingId == SETTING_APPEARANCE)
+ {
+ // Get the controller profile of the new appearance
+ GAME::ControllerPtr controller;
+
+ if (setting->GetType() == SettingType::String)
+ {
+ std::shared_ptr<const CSettingString> settingString =
+ std::static_pointer_cast<const CSettingString>(setting);
+ const std::string& addonId = settingString->GetValue();
+
+ if (m_manager != nullptr)
+ controller = m_manager->GetControllerProfiles().GetController(addonId);
+ }
+
+ if (controller)
+ peripheral->SetControllerProfile(controller);
+ }
+}
+
+bool CGUIDialogPeripheralSettings::Save()
+{
+ if (m_item == NULL || m_initialising)
+ return true;
+
+ PeripheralPtr peripheral = CServiceBroker::GetPeripherals().GetByPath(m_item->GetPath());
+ if (!peripheral)
+ return true;
+
+ peripheral->PersistSettings();
+
+ return true;
+}
+
+void CGUIDialogPeripheralSettings::OnResetSettings()
+{
+ if (m_item == NULL)
+ return;
+
+ PeripheralPtr peripheral = CServiceBroker::GetPeripherals().GetByPath(m_item->GetPath());
+ if (!peripheral)
+ return;
+
+ if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{10041}, CVariant{10042}))
+ return;
+
+ // reset the settings in the peripheral
+ peripheral->ResetDefaultSettings();
+
+ // re-create all settings and their controls
+ SetupView();
+}
+
+void CGUIDialogPeripheralSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ SetHeading(m_item->GetLabel());
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CUSTOM_BUTTON, 409);
+}
+
+void CGUIDialogPeripheralSettings::InitializeSettings()
+{
+ if (m_item == NULL)
+ {
+ m_initialising = false;
+ return;
+ }
+
+ m_initialising = true;
+ bool usePopup = g_SkinInfo->HasSkinFile("DialogSlider.xml");
+
+ PeripheralPtr peripheral = CServiceBroker::GetPeripherals().GetByPath(m_item->GetPath());
+ if (!peripheral)
+ {
+ CLog::Log(LOGDEBUG, "{} - no peripheral", __FUNCTION__);
+ m_initialising = false;
+ return;
+ }
+
+ m_settingsMap.clear();
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("peripheralsettings", -1);
+ if (category == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogPeripheralSettings: unable to setup settings");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogPeripheralSettings: unable to setup settings");
+ return;
+ }
+
+ std::vector<SettingPtr> settings = peripheral->GetSettings();
+ for (auto& setting : settings)
+ {
+ if (setting == NULL)
+ continue;
+
+ if (!setting->IsVisible())
+ {
+ CLog::Log(LOGDEBUG, "{} - invisible", __FUNCTION__);
+ continue;
+ }
+
+ // we need to create a copy of the setting because the CSetting instances
+ // are destroyed when leaving the dialog
+ SettingPtr settingCopy;
+ switch (setting->GetType())
+ {
+ case SettingType::Boolean:
+ {
+ std::shared_ptr<CSettingBool> settingBool = std::make_shared<CSettingBool>(
+ setting->GetId(), *std::static_pointer_cast<CSettingBool>(setting));
+ settingBool->SetControl(GetCheckmarkControl());
+
+ settingCopy = std::static_pointer_cast<CSetting>(settingBool);
+ break;
+ }
+
+ case SettingType::Integer:
+ {
+ std::shared_ptr<CSettingInt> settingInt = std::make_shared<CSettingInt>(
+ setting->GetId(), *std::static_pointer_cast<CSettingInt>(setting));
+ if (settingInt->GetTranslatableOptions().empty())
+ settingInt->SetControl(GetSliderControl("integer", false, -1, usePopup, -1, "{:d}"));
+ else
+ settingInt->SetControl(GetSpinnerControl("string"));
+
+ settingCopy = std::static_pointer_cast<CSetting>(settingInt);
+ break;
+ }
+
+ case SettingType::Number:
+ {
+ std::shared_ptr<CSettingNumber> settingNumber = std::make_shared<CSettingNumber>(
+ setting->GetId(), *std::static_pointer_cast<CSettingNumber>(setting));
+ settingNumber->SetControl(GetSliderControl("number", false, -1, usePopup, -1, "{:2.2f}"));
+
+ settingCopy = std::static_pointer_cast<CSetting>(settingNumber);
+ break;
+ }
+
+ case SettingType::String:
+ {
+ if (auto settingAsAddon = std::dynamic_pointer_cast<const CSettingAddon>(setting))
+ {
+ std::shared_ptr<CSettingAddon> settingAddon =
+ std::make_shared<CSettingAddon>(setting->GetId(), *settingAsAddon);
+
+ // Control properties
+ const std::string format = "addon";
+ const bool delayed = false;
+ const int heading = -1;
+ const bool hideValue = false;
+ const bool showInstalledAddons = true;
+ const bool showInstallableAddons = true;
+ const bool showMoreAddons = false;
+
+ settingAddon->SetControl(GetButtonControl(format, delayed, heading, hideValue,
+ showInstalledAddons, showInstallableAddons,
+ showMoreAddons));
+
+ GAME::ControllerPtr controller = peripheral->ControllerProfile();
+ if (controller)
+ settingAddon->SetValue(controller->ID());
+
+ settingCopy = std::static_pointer_cast<CSetting>(settingAddon);
+ }
+ else
+ {
+ std::shared_ptr<CSettingString> settingString = std::make_shared<CSettingString>(
+ setting->GetId(), *std::static_pointer_cast<CSettingString>(setting));
+ settingString->SetControl(GetEditControl("string"));
+
+ settingCopy = std::static_pointer_cast<CSetting>(settingString);
+ }
+ break;
+ }
+
+ default:
+ //! @todo add more types if needed
+ CLog::Log(LOGDEBUG, "{} - unknown type", __FUNCTION__);
+ break;
+ }
+
+ if (settingCopy != NULL && settingCopy->GetControl() != NULL)
+ {
+ settingCopy->SetLevel(SettingLevel::Basic);
+ group->AddSetting(settingCopy);
+ m_settingsMap.insert(std::make_pair(setting->GetId(), setting));
+ }
+ }
+
+ m_initialising = false;
+}
diff --git a/xbmc/peripherals/dialogs/GUIDialogPeripheralSettings.h b/xbmc/peripherals/dialogs/GUIDialogPeripheralSettings.h
new file mode 100644
index 0000000..33ad2ee
--- /dev/null
+++ b/xbmc/peripherals/dialogs/GUIDialogPeripheralSettings.h
@@ -0,0 +1,52 @@
+/*
+ * 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 "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+class CFileItem;
+
+namespace PERIPHERALS
+{
+class CPeripherals;
+
+class CGUIDialogPeripheralSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogPeripheralSettings();
+ ~CGUIDialogPeripheralSettings() override;
+
+ // specializations of CGUIControl
+ bool OnMessage(CGUIMessage& message) override;
+
+ void RegisterPeripheralManager(CPeripherals& manager);
+ void UnregisterPeripheralManager();
+
+ virtual void SetFileItem(const CFileItem* item);
+
+protected:
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void OnResetSettings() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+ // Dialog state
+ CPeripherals* m_manager{nullptr};
+ CFileItem* m_item;
+ bool m_initialising = false;
+ std::map<std::string, std::shared_ptr<CSetting>> m_settingsMap;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/peripherals/dialogs/GUIDialogPeripherals.cpp b/xbmc/peripherals/dialogs/GUIDialogPeripherals.cpp
new file mode 100644
index 0000000..7594cb2
--- /dev/null
+++ b/xbmc/peripherals/dialogs/GUIDialogPeripherals.cpp
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2017-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 "GUIDialogPeripherals.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "peripherals/Peripherals.h"
+#include "peripherals/dialogs/GUIDialogPeripheralSettings.h"
+#include "utils/Variant.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace PERIPHERALS;
+
+CGUIDialogPeripherals::CGUIDialogPeripherals()
+{
+ // Initialize CGUIControl via CGUIDialogSelect
+ SetID(WINDOW_DIALOG_PERIPHERALS);
+}
+
+CGUIDialogPeripherals::~CGUIDialogPeripherals() = default;
+
+void CGUIDialogPeripherals::OnInitWindow()
+{
+ UpdatePeripheralsSync();
+ CGUIDialogSelect::OnInitWindow();
+}
+
+void CGUIDialogPeripherals::RegisterPeripheralManager(CPeripherals& manager)
+{
+ m_manager = &manager;
+ m_manager->RegisterObserver(this);
+}
+
+void CGUIDialogPeripherals::UnregisterPeripheralManager()
+{
+ if (m_manager != nullptr)
+ {
+ m_manager->UnregisterObserver(this);
+ m_manager = nullptr;
+ }
+}
+
+CFileItemPtr CGUIDialogPeripherals::GetItem(unsigned int pos) const
+{
+ CFileItemPtr item;
+
+ std::unique_lock<CCriticalSection> lock(m_peripheralsMutex);
+
+ if (static_cast<int>(pos) < m_peripherals.Size())
+ item = m_peripherals[pos];
+
+ return item;
+}
+
+void CGUIDialogPeripherals::Show(CPeripherals& manager)
+{
+ CGUIDialogPeripherals* pDialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPeripherals>(
+ WINDOW_DIALOG_PERIPHERALS);
+ if (pDialog == nullptr)
+ return;
+
+ pDialog->Reset();
+
+ int iPos = -1;
+ do
+ {
+ pDialog->SetHeading(CVariant{35000});
+ pDialog->SetUseDetails(true);
+
+ pDialog->RegisterPeripheralManager(manager);
+
+ pDialog->Open();
+
+ pDialog->UnregisterPeripheralManager();
+
+ iPos = pDialog->IsConfirmed() ? pDialog->GetSelectedItem() : -1;
+
+ if (iPos >= 0)
+ {
+ CFileItemPtr pItem = pDialog->GetItem(iPos);
+
+ // Show an error if the peripheral doesn't have any settings
+ PeripheralPtr peripheral = manager.GetByPath(pItem->GetPath());
+ if (!peripheral || peripheral->GetSettings().empty())
+ {
+ MESSAGING::HELPERS::ShowOKDialogText(CVariant{35000}, CVariant{35004});
+ continue;
+ }
+
+ CGUIDialogPeripheralSettings* pSettingsDialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPeripheralSettings>(
+ WINDOW_DIALOG_PERIPHERAL_SETTINGS);
+ if (pItem && pSettingsDialog)
+ {
+ // Pass peripheral item properties to settings dialog so skin authors
+ // Can use it to show more detailed information about the device
+ pSettingsDialog->SetProperty("vendor", pItem->GetProperty("vendor"));
+ pSettingsDialog->SetProperty("product", pItem->GetProperty("product"));
+ pSettingsDialog->SetProperty("bus", pItem->GetProperty("bus"));
+ pSettingsDialog->SetProperty("location", pItem->GetProperty("location"));
+ pSettingsDialog->SetProperty("class", pItem->GetProperty("class"));
+ pSettingsDialog->SetProperty("version", pItem->GetProperty("version"));
+
+ // Open settings dialog
+ pSettingsDialog->SetFileItem(pItem.get());
+ pSettingsDialog->RegisterPeripheralManager(manager);
+ pSettingsDialog->Open();
+ pSettingsDialog->UnregisterPeripheralManager();
+ }
+ }
+ } while (pDialog->IsConfirmed());
+}
+
+bool CGUIDialogPeripherals::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_REFRESH_LIST:
+ {
+ if (m_manager && message.GetControlId() == -1)
+ UpdatePeripheralsSync();
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return CGUIDialogSelect::OnMessage(message);
+}
+
+void CGUIDialogPeripherals::Notify(const Observable& obs, const ObservableMessage msg)
+{
+ switch (msg)
+ {
+ case ObservableMessagePeripheralsChanged:
+ UpdatePeripheralsAsync();
+ break;
+ default:
+ break;
+ }
+}
+
+void CGUIDialogPeripherals::UpdatePeripheralsAsync()
+{
+ CGUIMessage msg(GUI_MSG_REFRESH_LIST, GetID(), -1);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg);
+}
+
+void CGUIDialogPeripherals::UpdatePeripheralsSync()
+{
+ int iPos = GetSelectedItem();
+
+ std::unique_lock<CCriticalSection> lock(m_peripheralsMutex);
+
+ CFileItemPtr selectedItem;
+ if (iPos > 0)
+ selectedItem = GetItem(iPos);
+
+ m_peripherals.Clear();
+ m_manager->GetDirectory("peripherals://all/", m_peripherals);
+ SetItems(m_peripherals);
+
+ if (selectedItem)
+ {
+ for (int i = 0; i < m_peripherals.Size(); i++)
+ {
+ if (m_peripherals[i]->GetPath() == selectedItem->GetPath())
+ SetSelected(i);
+ }
+ }
+}
diff --git a/xbmc/peripherals/dialogs/GUIDialogPeripherals.h b/xbmc/peripherals/dialogs/GUIDialogPeripherals.h
new file mode 100644
index 0000000..d4a2f5d
--- /dev/null
+++ b/xbmc/peripherals/dialogs/GUIDialogPeripherals.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017-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 "dialogs/GUIDialogSelect.h"
+#include "threads/CriticalSection.h"
+#include "utils/Observer.h"
+
+namespace PERIPHERALS
+{
+class CPeripherals;
+
+class CGUIDialogPeripherals : public CGUIDialogSelect, protected Observer
+{
+public:
+ CGUIDialogPeripherals();
+ ~CGUIDialogPeripherals() override;
+
+ void RegisterPeripheralManager(CPeripherals& manager);
+ void UnregisterPeripheralManager();
+
+ CFileItemPtr GetItem(unsigned int pos) const;
+
+ static void Show(CPeripherals& manager);
+
+ // implementation of CGUIControl via CGUIDialogSelect
+ bool OnMessage(CGUIMessage& message) override;
+
+ // implementation of Observer
+ void Notify(const Observable& obs, const ObservableMessage msg) override;
+
+private:
+ // implementation of CGUIWindow via CGUIDialogSelect
+ void OnInitWindow() override;
+
+ void ShowInternal();
+ void UpdatePeripheralsAsync();
+ void UpdatePeripheralsSync();
+
+ CPeripherals* m_manager = nullptr;
+ CFileItemList m_peripherals;
+ mutable CCriticalSection m_peripheralsMutex;
+};
+} // namespace PERIPHERALS
diff --git a/xbmc/pictures/CMakeLists.txt b/xbmc/pictures/CMakeLists.txt
new file mode 100644
index 0000000..9bbe6cd
--- /dev/null
+++ b/xbmc/pictures/CMakeLists.txt
@@ -0,0 +1,27 @@
+set(SOURCES ExifParse.cpp
+ GUIDialogPictureInfo.cpp
+ GUIViewStatePictures.cpp
+ GUIWindowPictures.cpp
+ GUIWindowSlideShow.cpp
+ IptcParse.cpp
+ JpegParse.cpp
+ libexif.cpp
+ Picture.cpp
+ PictureInfoLoader.cpp
+ PictureInfoTag.cpp
+ PictureScalingAlgorithm.cpp
+ PictureThumbLoader.cpp
+ SlideShowPicture.cpp)
+
+set(HEADERS GUIDialogPictureInfo.h
+ GUIViewStatePictures.h
+ GUIWindowPictures.h
+ GUIWindowSlideShow.h
+ Picture.h
+ PictureInfoLoader.h
+ PictureInfoTag.h
+ PictureScalingAlgorithm.h
+ PictureThumbLoader.h
+ SlideShowPicture.h)
+
+core_add_library(pictures)
diff --git a/xbmc/pictures/ExifParse.cpp b/xbmc/pictures/ExifParse.cpp
new file mode 100644
index 0000000..21ebb60
--- /dev/null
+++ b/xbmc/pictures/ExifParse.cpp
@@ -0,0 +1,958 @@
+/*
+ * 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.
+ */
+
+//--------------------------------------------------------------------------
+// Program to pull the EXIF information out of various types of digital
+// images and present it in a reasonably consistent way
+//
+// Original code pulled from 'jhead' by Matthias Wandel (http://www.sentex.net/~mwandel/) - jhead
+// Adapted for XBMC by DD.
+//--------------------------------------------------------------------------
+
+// Note: Jhead supports TAG_MAKER_NOTE exif field,
+// but that is omitted for now - to make porting easier and addition smaller
+
+#include "ExifParse.h"
+
+#ifdef TARGET_WINDOWS
+#include <windows.h>
+#else
+#include <memory.h>
+#include <cstring>
+#endif
+
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#ifndef min
+#define min(a,b) (a)>(b)?(b):(a)
+#endif
+#ifndef max
+#define max(a,b) (a)<(b)?(b):(a)
+#endif
+
+
+// Prototypes for exif utility functions.
+static void ErrNonfatal(const char* const msg, int a1, int a2);
+
+#define DIR_ENTRY_ADDR(Start, Entry) ((Start)+2+12*(Entry))
+
+
+//--------------------------------------------------------------------------
+// Describes tag values
+#define TAG_DESCRIPTION 0x010E
+#define TAG_MAKE 0x010F
+#define TAG_MODEL 0x0110
+#define TAG_ORIENTATION 0x0112
+#define TAG_X_RESOLUTION 0x011A // Not processed. Format rational64u (see http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.76/html/TagNames/EXIF.html)
+#define TAG_Y_RESOLUTION 0x011B // Not processed. Format rational64u
+#define TAG_RESOLUTION_UNIT 0x0128 // Not processed. Format int16u. Values: 1-none; 2-inches; 3-cm
+#define TAG_SOFTWARE 0x0131
+#define TAG_DATETIME 0x0132
+#define TAG_THUMBNAIL_OFFSET 0x0201
+#define TAG_THUMBNAIL_LENGTH 0x0202
+#define TAG_Y_CB_CR_POS 0x0213 // Not processed. Format int16u. Values: 1-Centered; 2-Co-sited
+#define TAG_EXPOSURETIME 0x829A
+#define TAG_FNUMBER 0x829D
+#define TAG_EXIF_OFFSET 0x8769
+#define TAG_EXPOSURE_PROGRAM 0x8822
+#define TAG_GPSINFO 0x8825
+#define TAG_ISO_EQUIVALENT 0x8827
+#define TAG_EXIF_VERSION 0x9000 // Not processed.
+#define TAG_COMPONENT_CFG 0x9101 // Not processed.
+#define TAG_DATETIME_ORIGINAL 0x9003
+#define TAG_DATETIME_DIGITIZED 0x9004
+#define TAG_SHUTTERSPEED 0x9201
+#define TAG_APERTURE 0x9202
+#define TAG_EXPOSURE_BIAS 0x9204
+#define TAG_MAXAPERTURE 0x9205
+#define TAG_SUBJECT_DISTANCE 0x9206
+#define TAG_METERING_MODE 0x9207
+#define TAG_LIGHT_SOURCE 0x9208
+#define TAG_FLASH 0x9209
+#define TAG_FOCALLENGTH 0x920A
+#define TAG_MAKER_NOTE 0x927C // Not processed yet. Maybe in the future.
+#define TAG_USERCOMMENT 0x9286
+#define TAG_XP_COMMENT 0x9c9c
+#define TAG_FLASHPIX_VERSION 0xA000 // Not processed.
+#define TAG_COLOUR_SPACE 0xA001 // Not processed. Format int16u. Values: 1-RGB; 2-Adobe RGB 65535-Uncalibrated
+#define TAG_EXIF_IMAGEWIDTH 0xa002
+#define TAG_EXIF_IMAGELENGTH 0xa003
+#define TAG_INTEROP_OFFSET 0xa005
+#define TAG_FOCALPLANEXRES 0xa20E
+#define TAG_FOCALPLANEUNITS 0xa210
+#define TAG_EXPOSURE_INDEX 0xa215
+#define TAG_EXPOSURE_MODE 0xa402
+#define TAG_WHITEBALANCE 0xa403
+#define TAG_DIGITALZOOMRATIO 0xA404
+#define TAG_FOCALLENGTH_35MM 0xa405
+
+#define TAG_GPS_LAT_REF 1
+#define TAG_GPS_LAT 2
+#define TAG_GPS_LONG_REF 3
+#define TAG_GPS_LONG 4
+#define TAG_GPS_ALT_REF 5
+#define TAG_GPS_ALT 6
+
+//--------------------------------------------------------------------------
+// Exif format descriptor stuff
+namespace
+{
+constexpr auto FMT_BYTE = 1;
+[[maybe_unused]] constexpr auto FMT_STRING = 2;
+constexpr auto FMT_USHORT = 3;
+constexpr auto FMT_ULONG = 4;
+constexpr auto FMT_URATIONAL = 5;
+constexpr auto FMT_SBYTE = 6;
+[[maybe_unused]] constexpr auto FMT_UNDEFINED = 7;
+constexpr auto FMT_SSHORT = 8;
+constexpr auto FMT_SLONG = 9;
+constexpr auto FMT_SRATIONAL = 10;
+constexpr auto FMT_SINGLE = 11;
+constexpr auto FMT_DOUBLE = 12;
+// NOTE: Remember to change NUM_FORMATS if you define a new format
+constexpr auto NUM_FORMATS = 12;
+
+const unsigned int BytesPerFormat[NUM_FORMATS] = {1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8};
+} // namespace
+
+//--------------------------------------------------------------------------
+// Internationalisation string IDs. The enum order must match that in the
+// language file (e.g. 'language/resource.language.en_gb/strings.po', and EXIF_PARSE_STRING_ID_BASE
+// must match the ID of the first Exif string in that file.
+#define EXIF_PARSE_STRING_ID_BASE 21800
+enum {
+// Distance
+ ExifStrDistanceInfinite = EXIF_PARSE_STRING_ID_BASE,
+// Whitebalance et.al.
+ ExifStrManual,
+ ExifStrAuto,
+// Flash modes
+ ExifStrYes,
+ ExifStrNo,
+ ExifStrFlashNoStrobe,
+ ExifStrFlashStrobe,
+ ExifStrFlashManual,
+ ExifStrFlashManualNoReturn,
+ ExifStrFlashManualReturn,
+ ExifStrFlashAuto,
+ ExifStrFlashAutoNoReturn,
+ ExifStrFlashAutoReturn,
+ ExifStrFlashRedEye,
+ ExifStrFlashRedEyeNoReturn,
+ ExifStrFlashRedEyeReturn,
+ ExifStrFlashManualRedEye,
+ ExifStrFlashManualRedEyeNoReturn,
+ ExifStrFlashManualRedEyeReturn,
+ ExifStrFlashAutoRedEye,
+ ExifStrFlashAutoRedEyeNoReturn,
+ ExifStrFlashAutoRedEyeReturn,
+// Light sources
+ ExifStrDaylight,
+ ExifStrFluorescent,
+ ExifStrIncandescent,
+ ExifStrFlash,
+ ExifStrFineWeather,
+ ExifStrShade,
+// Metering Mode
+ ExifStrMeteringCenter,
+ ExifStrMeteringSpot,
+ ExifStrMeteringMatrix,
+// Exposure Program
+ ExifStrExposureProgram,
+ ExifStrExposureAperture,
+ ExifStrExposureShutter,
+ ExifStrExposureCreative,
+ ExifStrExposureAction,
+ ExifStrExposurePortrait,
+ ExifStrExposureLandscape,
+// Exposure mode
+ ExifStrExposureModeAuto,
+// ISO equivalent
+ ExifStrIsoEquivalent,
+// GPS latitude, longitude, altitude
+ ExifStrGpsLatitude,
+ ExifStrGpsLongitude,
+ ExifStrGpsAltitude,
+};
+
+
+
+
+//--------------------------------------------------------------------------
+// Report non fatal errors. Now that microsoft.net modifies exif headers,
+// there's corrupted ones, and there could be more in the future.
+//--------------------------------------------------------------------------
+static void ErrNonfatal(const char* const msg, int a1, int a2)
+{
+ printf("ExifParse - Nonfatal Error : %s %d %d", msg, a1, a2);
+}
+
+//--------------------------------------------------------------------------
+// Convert a 16 bit unsigned value from file's native byte order
+//--------------------------------------------------------------------------
+int CExifParse::Get16(const void* const Short, const bool motorolaOrder)
+{
+ if (motorolaOrder) {
+ return (((const unsigned char *)Short)[0] << 8) | ((const unsigned char *)Short)[1];
+ } else {
+ return (((const unsigned char *)Short)[1] << 8) | ((const unsigned char *)Short)[0];
+ }
+}
+
+//--------------------------------------------------------------------------
+// Convert a 32 bit signed value from file's native byte order
+//--------------------------------------------------------------------------
+int CExifParse::Get32(const void* const Long, const bool motorolaOrder)
+{
+ if (motorolaOrder) {
+ return (((const char *)Long)[0] << 24) | (((const unsigned char *)Long)[1] << 16)
+ | (((const unsigned char *)Long)[2] << 8 ) | (((const unsigned char *)Long)[3] << 0 );
+ } else {
+ return (((const char *)Long)[3] << 24) | (((const unsigned char *)Long)[2] << 16)
+ | (((const unsigned char *)Long)[1] << 8 ) | (((const unsigned char *)Long)[0] << 0 );
+ }
+}
+
+//--------------------------------------------------------------------------
+// It appears that CStdString constructor replaces "\n" with "\r\n" which results
+// in "\r\r\n" if there already is "\r\n", which in turn results in corrupted
+// display. So this is an attempt to undo effects of a smart constructor. Also,
+// replaces all nonprintable characters with "."
+//--------------------------------------------------------------------------
+/*void CExifParse::FixComment(CStdString& comment)
+{
+ comment.Replace("\r\r\n", "\r\n");
+ for (unsigned int i=0; i<comment.length(); i++)
+ {
+ if ((comment[i] < 32) && (comment[i] != '\n') && (comment[i] != '\t') && (comment[i] != '\r'))
+ {
+ comment[i] = '.';
+ }
+ }
+}*/
+
+//--------------------------------------------------------------------------
+// Evaluate number, be it int, rational, or float from directory.
+//--------------------------------------------------------------------------
+double CExifParse::ConvertAnyFormat(const void* const ValuePtr, int Format)
+{
+ double Value;
+ Value = 0;
+
+ switch(Format)
+ {
+ case FMT_SBYTE: Value = *(const signed char*)ValuePtr; break;
+ case FMT_BYTE: Value = *(const unsigned char*)ValuePtr; break;
+
+ case FMT_USHORT: Value = Get16(ValuePtr, m_MotorolaOrder); break;
+ case FMT_ULONG: Value = (unsigned)Get32(ValuePtr, m_MotorolaOrder); break;
+
+ case FMT_URATIONAL:
+ case FMT_SRATIONAL:
+ {
+ int Num,Den;
+ Num = Get32(ValuePtr, m_MotorolaOrder);
+ Den = Get32(4+(const char *)ValuePtr, m_MotorolaOrder);
+
+ if (Den == 0) Value = 0;
+ else Value = (double)Num/Den;
+ }
+ break;
+
+ case FMT_SSHORT: Value = (signed short)Get16(ValuePtr, m_MotorolaOrder); break;
+ case FMT_SLONG: Value = Get32(ValuePtr, m_MotorolaOrder); break;
+
+ // Not sure if this is correct (never seen float used in Exif format)
+ case FMT_SINGLE: Value = (double)*(const float*)ValuePtr; break;
+ case FMT_DOUBLE: Value = *(const double*)ValuePtr; break;
+
+ default:
+ ErrNonfatal("Illegal format code %d",Format,0);
+ }
+ return Value;
+}
+
+//--------------------------------------------------------------------------
+// Exif date tag is stored as a fixed format string "YYYY:MM:DD HH:MM:SS".
+// If date is not set, then the string is filled with blanks and colons:
+// " : : : : ". We want this string localised.
+//--------------------------------------------------------------------------
+/*void CExifParse::LocaliseDate (void)
+{
+ if (m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME][0] != ' ')
+ {
+ int year = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(0, 4).c_str());
+ int month = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(5, 2).c_str());
+ int day = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(8, 2).c_str());
+ int hour = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(11,2).c_str());
+ int min = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(14,2).c_str());
+ int sec = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(17,2).c_str());
+ CDateTime date(year, month, day, hour, min, sec);
+ m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME] = date.GetAsLocalizedDateTime();
+ }
+}*/
+
+
+//--------------------------------------------------------------------------
+// Convert exposure time into a human readable format
+//--------------------------------------------------------------------------
+/*void CExifParse::GetExposureTime(const float exposureTime, CStdString& outStr)
+{
+ if (exposureTime)
+ {
+ if (exposureTime < 0.010) outStr.Format("%6.4fs ", exposureTime);
+ else outStr.Format("%5.3fs ", exposureTime);
+ if (exposureTime <= 0.5) outStr.Format("%s (1/%d)", outStr, (int)(0.5 + 1/exposureTime));
+ }
+}*/
+
+//--------------------------------------------------------------------------
+// Process one of the nested EXIF directories.
+//--------------------------------------------------------------------------
+void CExifParse::ProcessDir(const unsigned char* const DirStart,
+ const unsigned char* const OffsetBase,
+ const unsigned ExifLength,
+ int NestingLevel)
+{
+ if (NestingLevel > 4)
+ {
+ ErrNonfatal("Maximum directory nesting exceeded (corrupt exif header)", 0,0);
+ return;
+ }
+
+ char IndentString[25];
+ memset(IndentString, ' ', 25);
+ IndentString[NestingLevel * 4] = '\0';
+
+
+ int NumDirEntries = Get16((const void*)DirStart, m_MotorolaOrder);
+
+ const unsigned char* const DirEnd = DIR_ENTRY_ADDR(DirStart, NumDirEntries);
+ if (DirEnd+4 > (OffsetBase+ExifLength))
+ {
+ if (DirEnd+2 == OffsetBase+ExifLength || DirEnd == OffsetBase+ExifLength)
+ {
+ // Version 1.3 of jhead would truncate a bit too much.
+ // This also caught later on as well.
+ }
+ else
+ {
+ ErrNonfatal("Illegally sized directory", 0,0);
+ return;
+ }
+ }
+
+ for (int de=0;de<NumDirEntries;de++)
+ {
+ int Tag, Format, Components;
+ unsigned char* ValuePtr;
+ int ByteCount;
+ const unsigned char* const DirEntry = DIR_ENTRY_ADDR(DirStart, de);
+
+ Tag = Get16(DirEntry, m_MotorolaOrder);
+ Format = Get16(DirEntry+2, m_MotorolaOrder);
+ Components = Get32(DirEntry+4, m_MotorolaOrder);
+
+ if (Format <= 0 || Format > NUM_FORMATS)
+ {
+ ErrNonfatal("Illegal number format %d for tag %04x", Format, Tag);
+ continue;
+ }
+
+ if ((unsigned)Components > 0x10000)
+ {
+ ErrNonfatal("Illegal number of components %d for tag %04x", Components, Tag);
+ continue;
+ }
+
+ ByteCount = Components * BytesPerFormat[Format - 1];
+
+ if (ByteCount > 4)
+ {
+ unsigned OffsetVal;
+ OffsetVal = (unsigned)Get32(DirEntry+8, m_MotorolaOrder);
+ // If its bigger than 4 bytes, the dir entry contains an offset.
+ if (OffsetVal > UINT32_MAX - ByteCount || OffsetVal + ByteCount > ExifLength)
+ {
+ // Bogus pointer offset and / or bytecount value
+ ErrNonfatal("Illegal value pointer for tag %04x", Tag,0);
+ continue;
+ }
+ ValuePtr = (unsigned char*)(const_cast<unsigned char*>(OffsetBase)+OffsetVal);
+
+ if (OffsetVal > m_LargestExifOffset)
+ {
+ m_LargestExifOffset = OffsetVal;
+ }
+
+ }
+ else {
+ // 4 bytes or less and value is in the dir entry itself
+ ValuePtr = (unsigned char*)(const_cast<unsigned char*>(DirEntry)+8);
+ }
+
+
+ // Extract useful components of tag
+ switch(Tag)
+ {
+ case TAG_DESCRIPTION:
+ {
+ int length = max(ByteCount, 0);
+ length = min(length, MAX_COMMENT);
+ strncpy(m_ExifInfo->Description, (char *)ValuePtr, length);
+ m_ExifInfo->Description[length] = '\0';
+ break;
+ }
+ case TAG_MAKE:
+ {
+ int space = sizeof(m_ExifInfo->CameraMake);
+ if (space > 0)
+ {
+ strncpy(m_ExifInfo->CameraMake, (char *)ValuePtr, space - 1);
+ m_ExifInfo->CameraMake[space - 1] = '\0';
+ }
+ break;
+ }
+ case TAG_MODEL:
+ {
+ int space = sizeof(m_ExifInfo->CameraModel);
+ if (space > 0)
+ {
+ strncpy(m_ExifInfo->CameraModel, (char *)ValuePtr, space - 1);
+ m_ExifInfo->CameraModel[space - 1] = '\0';
+ }
+ break;
+ }
+// case TAG_SOFTWARE: strncpy(m_ExifInfo->Software, ValuePtr, 5); break;
+ case TAG_FOCALPLANEXRES: m_FocalPlaneXRes = ConvertAnyFormat(ValuePtr, Format); break;
+ case TAG_THUMBNAIL_OFFSET: m_ExifInfo->ThumbnailOffset = (unsigned)ConvertAnyFormat(ValuePtr, Format); break;
+ case TAG_THUMBNAIL_LENGTH: m_ExifInfo->ThumbnailSize = (unsigned)ConvertAnyFormat(ValuePtr, Format); break;
+
+ case TAG_MAKER_NOTE:
+ continue;
+ break;
+
+ case TAG_DATETIME_ORIGINAL:
+ {
+
+ int space = sizeof(m_ExifInfo->DateTime);
+ if (space > 0)
+ {
+ strncpy(m_ExifInfo->DateTime, (char *)ValuePtr, space - 1);
+ m_ExifInfo->DateTime[space - 1] = '\0';
+ // If we get a DATETIME_ORIGINAL, we use that one.
+ m_DateFound = true;
+ }
+ break;
+ }
+ case TAG_DATETIME_DIGITIZED:
+ case TAG_DATETIME:
+ {
+ if (!m_DateFound)
+ {
+ // If we don't already have a DATETIME_ORIGINAL, use whatever
+ // time fields we may have.
+ int space = sizeof(m_ExifInfo->DateTime);
+ if (space > 0)
+ {
+ strncpy(m_ExifInfo->DateTime, (char *)ValuePtr, space - 1);
+ m_ExifInfo->DateTime[space - 1] = '\0';
+ }
+ }
+ break;
+ }
+ case TAG_USERCOMMENT:
+ {
+ // The UserComment allows comments without the charset limitations of ImageDescription.
+ // Therefore the UserComment field is prefixed by a CharacterCode field (8 Byte):
+ // - ASCII: 'ASCII\0\0\0'
+ // - Unicode: 'UNICODE\0'
+ // - JIS X208-1990: 'JIS\0\0\0\0\0'
+ // - Unknown: '\0\0\0\0\0\0\0\0' (application specific)
+
+ m_ExifInfo->CommentsCharset = EXIF_COMMENT_CHARSET_UNKNOWN;
+
+ const int EXIF_COMMENT_CHARSET_LENGTH = 8;
+ if (ByteCount >= EXIF_COMMENT_CHARSET_LENGTH)
+ {
+ // As some implementations use spaces instead of \0 for the padding,
+ // we're not so strict and check only the prefix.
+ if (memcmp(ValuePtr, "ASCII", 5) == 0)
+ m_ExifInfo->CommentsCharset = EXIF_COMMENT_CHARSET_ASCII;
+ else if (memcmp(ValuePtr, "UNICODE", 7) == 0)
+ m_ExifInfo->CommentsCharset = EXIF_COMMENT_CHARSET_UNICODE;
+ else if (memcmp(ValuePtr, "JIS", 3) == 0)
+ m_ExifInfo->CommentsCharset = EXIF_COMMENT_CHARSET_JIS;
+
+ int length = ByteCount - EXIF_COMMENT_CHARSET_LENGTH;
+ length = min(length, MAX_COMMENT);
+ memcpy(m_ExifInfo->Comments, ValuePtr + EXIF_COMMENT_CHARSET_LENGTH, length);
+ m_ExifInfo->Comments[length] = '\0';
+// FixComment(comment); // Ensure comment is printable
+ }
+ }
+ break;
+
+ case TAG_XP_COMMENT:
+ {
+ // The XP user comment field is always unicode (UCS-2) encoded
+ m_ExifInfo->XPCommentsCharset = EXIF_COMMENT_CHARSET_UNICODE;
+ size_t length = min(ByteCount, MAX_COMMENT);
+ memcpy(m_ExifInfo->XPComment, ValuePtr, length);
+ m_ExifInfo->XPComment[length] = '\0';
+ }
+ break;
+
+ case TAG_FNUMBER:
+ // Simplest way of expressing aperture, so I trust it the most.
+ // (overwrite previously computd value if there is one)
+ m_ExifInfo->ApertureFNumber = (float)ConvertAnyFormat(ValuePtr, Format);
+ break;
+
+ case TAG_APERTURE:
+ case TAG_MAXAPERTURE:
+ // More relevant info always comes earlier, so only use this field if we don't
+ // have appropriate aperture information yet.
+ if (m_ExifInfo->ApertureFNumber == 0)
+ {
+ m_ExifInfo->ApertureFNumber = (float)exp(ConvertAnyFormat(ValuePtr, Format)*log(2.0)*0.5);
+ }
+ break;
+
+ case TAG_FOCALLENGTH:
+ // Nice digital cameras actually save the focal length as a function
+ // of how far they are zoomed in.
+ m_ExifInfo->FocalLength = (float)ConvertAnyFormat(ValuePtr, Format);
+ break;
+
+ case TAG_SUBJECT_DISTANCE:
+ // Inidcates the distacne the autofocus camera is focused to.
+ // Tends to be less accurate as distance increases.
+ {
+ float distance = (float)ConvertAnyFormat(ValuePtr, Format);
+ m_ExifInfo->Distance = distance;
+ }
+ break;
+
+ case TAG_EXPOSURETIME:
+ {
+ // Simplest way of expressing exposure time, so I trust it most.
+ // (overwrite previously computd value if there is one)
+ float expTime = (float)ConvertAnyFormat(ValuePtr, Format);
+ if (expTime)
+ m_ExifInfo->ExposureTime = expTime;
+ }
+ break;
+
+ case TAG_SHUTTERSPEED:
+ // More complicated way of expressing exposure time, so only use
+ // this value if we don't already have it from somewhere else.
+ if (m_ExifInfo->ExposureTime == 0)
+ {
+ m_ExifInfo->ExposureTime = (float)(1/exp(ConvertAnyFormat(ValuePtr, Format)*log(2.0)));
+ }
+ break;
+
+ case TAG_FLASH:
+ m_ExifInfo->FlashUsed = (int)ConvertAnyFormat(ValuePtr, Format);
+ break;
+
+ case TAG_ORIENTATION:
+ m_ExifInfo->Orientation = (int)ConvertAnyFormat(ValuePtr, Format);
+ if (m_ExifInfo->Orientation < 0 || m_ExifInfo->Orientation > 8)
+ {
+ ErrNonfatal("Undefined rotation value %d", m_ExifInfo->Orientation, 0);
+ m_ExifInfo->Orientation = 0;
+ }
+ break;
+
+ case TAG_EXIF_IMAGELENGTH:
+ case TAG_EXIF_IMAGEWIDTH:
+ // Use largest of height and width to deal with images that have been
+ // rotated to portrait format.
+ {
+ int a = (int)ConvertAnyFormat(ValuePtr, Format);
+ if (m_ExifImageWidth < a) m_ExifImageWidth = a;
+ }
+ break;
+
+ case TAG_FOCALPLANEUNITS:
+ switch((int)ConvertAnyFormat(ValuePtr, Format))
+ {
+ // According to the information I was using, 2 means meters.
+ // But looking at the Cannon powershot's files, inches is the only
+ // sensible value.
+ case 1: m_FocalPlaneUnits = 25.4; break; // inch
+ case 2: m_FocalPlaneUnits = 25.4; break;
+ case 3: m_FocalPlaneUnits = 10; break; // centimeter
+ case 4: m_FocalPlaneUnits = 1; break; // millimeter
+ case 5: m_FocalPlaneUnits = .001; break; // micrometer
+ }
+ break;
+
+ case TAG_EXPOSURE_BIAS:
+ m_ExifInfo->ExposureBias = (float)ConvertAnyFormat(ValuePtr, Format);
+ break;
+
+ case TAG_WHITEBALANCE:
+ m_ExifInfo->Whitebalance = (int)ConvertAnyFormat(ValuePtr, Format);
+ break;
+
+ case TAG_LIGHT_SOURCE:
+ //Quercus: 17-1-2004 Added LightSource, some cams return this, whitebalance or both
+ m_ExifInfo->LightSource = (int)ConvertAnyFormat(ValuePtr, Format);
+ break;
+
+ case TAG_METERING_MODE:
+ m_ExifInfo->MeteringMode = (int)ConvertAnyFormat(ValuePtr, Format);
+ break;
+
+ case TAG_EXPOSURE_PROGRAM:
+ m_ExifInfo->ExposureProgram = (int)ConvertAnyFormat(ValuePtr, Format);
+ break;
+
+ case TAG_EXPOSURE_INDEX:
+ if (m_ExifInfo->ISOequivalent == 0)
+ {
+ // Exposure index and ISO equivalent are often used interchangeably,
+ // so we will do the same.
+ // http://photography.about.com/library/glossary/bldef_ei.htm
+ m_ExifInfo->ISOequivalent = (int)ConvertAnyFormat(ValuePtr, Format);
+ }
+ break;
+
+ case TAG_ISO_EQUIVALENT:
+ m_ExifInfo->ISOequivalent = (int)ConvertAnyFormat(ValuePtr, Format);
+ if (m_ExifInfo->ISOequivalent < 50)
+ m_ExifInfo->ISOequivalent *= 200; // Fixes strange encoding on some older digicams.
+ break;
+
+ case TAG_EXPOSURE_MODE:
+ m_ExifInfo->ExposureMode = (int)ConvertAnyFormat(ValuePtr, Format);
+ break;
+
+ case TAG_DIGITALZOOMRATIO:
+ m_ExifInfo->DigitalZoomRatio = (float)ConvertAnyFormat(ValuePtr, Format);
+ break;
+
+ case TAG_EXIF_OFFSET:
+ case TAG_INTEROP_OFFSET:
+ {
+ const unsigned char* const SubdirStart = OffsetBase + (unsigned)Get32(ValuePtr, m_MotorolaOrder);
+ if (SubdirStart < OffsetBase || SubdirStart > OffsetBase+ExifLength)
+ {
+ ErrNonfatal("Illegal exif or interop ofset directory link",0,0);
+ }
+ else
+ {
+ ProcessDir(SubdirStart, OffsetBase, ExifLength, NestingLevel+1);
+ }
+ continue;
+ }
+ break;
+
+ case TAG_GPSINFO:
+ {
+ const unsigned char* const SubdirStart = OffsetBase + (unsigned)Get32(ValuePtr, m_MotorolaOrder);
+ if (SubdirStart < OffsetBase || SubdirStart > OffsetBase+ExifLength)
+ {
+ ErrNonfatal("Illegal GPS directory link",0,0);
+ }
+ else
+ {
+ ProcessGpsInfo(SubdirStart, ByteCount, OffsetBase, ExifLength);
+ }
+ continue;
+ }
+ break;
+
+ case TAG_FOCALLENGTH_35MM:
+ // The focal length equivalent 35 mm is a 2.2 tag (defined as of April 2002)
+ // if its present, use it to compute equivalent focal length instead of
+ // computing it from sensor geometry and actual focal length.
+ m_ExifInfo->FocalLength35mmEquiv = (unsigned)ConvertAnyFormat(ValuePtr, Format);
+ break;
+ }
+ }
+
+
+ // In addition to linking to subdirectories via exif tags,
+ // there's also a potential link to another directory at the end of each
+ // directory. this has got to be the result of a committee!
+ unsigned Offset;
+
+ if (DIR_ENTRY_ADDR(DirStart, NumDirEntries) + 4 <= OffsetBase+ExifLength)
+ {
+ Offset = (unsigned)Get32(DirStart+2+12*NumDirEntries, m_MotorolaOrder);
+ if (Offset)
+ {
+ const unsigned char* const SubdirStart = OffsetBase + Offset;
+ if (SubdirStart > OffsetBase+ExifLength || SubdirStart < OffsetBase)
+ {
+ if (SubdirStart > OffsetBase && SubdirStart < OffsetBase+ExifLength+20)
+ {
+ // Jhead 1.3 or earlier would crop the whole directory!
+ // As Jhead produces this form of format incorrectness,
+ // I'll just let it pass silently
+ }
+ else
+ {
+ ErrNonfatal("Illegal subdirectory link",0,0);
+ }
+ }
+ else
+ {
+ if (SubdirStart <= OffsetBase+ExifLength)
+ {
+ ProcessDir(SubdirStart, OffsetBase, ExifLength, NestingLevel+1);
+ }
+ }
+ if (Offset > m_LargestExifOffset)
+ {
+ m_LargestExifOffset = Offset;
+ }
+ }
+ }
+ else
+ {
+ // The exif header ends before the last next directory pointer.
+ }
+
+ if (m_ExifInfo->ThumbnailOffset)
+ {
+ m_ExifInfo->ThumbnailAtEnd = false;
+
+ if (m_ExifInfo->ThumbnailOffset <= ExifLength)
+ {
+ if (m_ExifInfo->ThumbnailSize > ExifLength - m_ExifInfo->ThumbnailOffset)
+ {
+ // If thumbnail extends past exif header, only save the part that
+ // actually exists. Canon's EOS viewer utility will do this - the
+ // thumbnail extracts ok with this hack.
+ m_ExifInfo->ThumbnailSize = ExifLength - m_ExifInfo->ThumbnailOffset;
+ }
+ }
+ }
+}
+
+
+//--------------------------------------------------------------------------
+// Process a EXIF marker
+// Describes all the drivel that most digital cameras include...
+//--------------------------------------------------------------------------
+bool CExifParse::Process (const unsigned char* const ExifSection, const unsigned short length, ExifInfo_t *info)
+{
+ m_ExifInfo = info;
+ // EXIF signature: "Exif\0\0"
+ // Check EXIF signatures
+ const char ExifHeader[] = "Exif\0\0";
+ const char ExifAlignment0[] = "II";
+ const char ExifAlignment1[] = "MM";
+ const char ExifExtra = 0x2a;
+
+ const char* pos = (const char*)(ExifSection + sizeof(short)); // position data pointer after length field
+
+ if (memcmp(pos, ExifHeader,6))
+ {
+ printf("ExifParse: incorrect Exif header");
+ return false;
+ }
+ pos += 6;
+
+ if (memcmp(pos, ExifAlignment0, strlen(ExifAlignment0)) == 0)
+ {
+ m_MotorolaOrder = false;
+ }
+ else if (memcmp(pos, ExifAlignment1, strlen(ExifAlignment1)) == 0)
+ {
+ m_MotorolaOrder = true;
+ }
+ else
+ {
+ printf("ExifParse: invalid Exif alignment marker");
+ return false;
+ }
+ pos += strlen(ExifAlignment0);
+
+ // Check the next value for correctness.
+ if (Get16((const void*)(pos), m_MotorolaOrder) != ExifExtra)
+ {
+ printf("ExifParse: invalid Exif start (1)");
+ return false;
+ }
+ pos += sizeof(short);
+
+ unsigned long FirstOffset = (unsigned)Get32((const void*)pos, m_MotorolaOrder);
+ if (FirstOffset < 8 || FirstOffset + 8 >= length)
+ {
+ ErrNonfatal("Invalid offset of first IFD value: %u", FirstOffset, 0);
+ return false;
+ }
+
+
+
+ // First directory starts 16 bytes in. All offset are relative to 8 bytes in.
+ ProcessDir(ExifSection+8+FirstOffset, ExifSection+8, length-8, 0);
+
+ m_ExifInfo->ThumbnailAtEnd = m_ExifInfo->ThumbnailOffset >= m_LargestExifOffset;
+
+ // Compute the CCD width, in millimeters.
+ if (m_FocalPlaneXRes != 0)
+ {
+ // Note: With some cameras, its not possible to compute this correctly because
+ // they don't adjust the indicated focal plane resolution units when using less
+ // than maximum resolution, so the CCDWidth value comes out too small. Nothing
+ // that Jhead can do about it - its a camera problem.
+ m_ExifInfo->CCDWidth = (float)(m_ExifImageWidth * m_FocalPlaneUnits / m_FocalPlaneXRes);
+ }
+
+ if (m_ExifInfo->FocalLength)
+ {
+ if (m_ExifInfo->FocalLength35mmEquiv == 0)
+ {
+ // Compute 35 mm equivalent focal length based on sensor geometry if we haven't
+ // already got it explicitly from a tag.
+ if (m_ExifInfo->CCDWidth != 0.0f)
+ {
+ m_ExifInfo->FocalLength35mmEquiv =
+ (int)(m_ExifInfo->FocalLength / m_ExifInfo->CCDWidth * 36 + 0.5f);
+ }
+ }
+ }
+ return true;
+}
+
+
+
+//--------------------------------------------------------------------------
+// GPS Lat/Long extraction helper function
+//--------------------------------------------------------------------------
+void CExifParse::GetLatLong(
+ const unsigned int Format,
+ const unsigned char* ValuePtr,
+ const int ComponentSize,
+ char *latLongString)
+{
+ if (Format != FMT_URATIONAL)
+ {
+ ErrNonfatal("Illegal number format %d for GPS Lat/Long", Format, 0);
+ }
+ else
+ {
+ double Values[3];
+ for (unsigned a=0; a<3 ;a++)
+ {
+ Values[a] = ConvertAnyFormat(ValuePtr+a*ComponentSize, Format);
+ }
+ if (Values[0] < 0 || Values[0] > 180 || Values[1] < 0 || Values[1] >= 60 || Values[2] < 0 || Values[2] >= 60)
+ {
+ // Ignore invalid values (DMS format expected)
+ ErrNonfatal("Invalid Lat/Long value", 0, 0);
+ latLongString[0] = 0;
+ }
+ else
+ {
+ char latLong[30];
+ sprintf(latLong, "%3.0fd %2.0f' %5.2f\"", Values[0], Values[1], Values[2]);
+ strcat(latLongString, latLong);
+ }
+ }
+}
+
+//--------------------------------------------------------------------------
+// Process GPS info directory
+//--------------------------------------------------------------------------
+void CExifParse::ProcessGpsInfo(
+ const unsigned char* const DirStart,
+ int ByteCountUnused,
+ const unsigned char* const OffsetBase,
+ unsigned ExifLength)
+{
+ int NumDirEntries = Get16(DirStart, m_MotorolaOrder);
+
+ for (int de=0;de<NumDirEntries;de++)
+ {
+ const unsigned char* DirEntry = DIR_ENTRY_ADDR(DirStart, de);
+
+ // Fix from aosp 34a2564d3268a5ca1472c5076675782fbaf724d6
+ if (DirEntry + 12 > OffsetBase + ExifLength)
+ {
+ ErrNonfatal("GPS info directory goes past end of exif", 0, 0);
+ return;
+ }
+
+ unsigned Tag = Get16(DirEntry, m_MotorolaOrder);
+ unsigned Format = Get16(DirEntry+2, m_MotorolaOrder);
+ unsigned Components = (unsigned)Get32(DirEntry+4, m_MotorolaOrder);
+ if (Format == 0 || Format > NUM_FORMATS)
+ {
+ ErrNonfatal("Illegal number format %d for tag %04x", Format, Tag);
+ continue;
+ }
+
+ unsigned ComponentSize = BytesPerFormat[Format - 1];
+ unsigned ByteCount = Components * ComponentSize;
+
+ const unsigned char* ValuePtr;
+
+ if (ByteCount > 4)
+ {
+ unsigned OffsetVal = (unsigned)Get32(DirEntry+8, m_MotorolaOrder);
+ // If its bigger than 4 bytes, the dir entry contains an offset.
+ if (OffsetVal > UINT32_MAX - ByteCount || OffsetVal + ByteCount > ExifLength)
+ {
+ // Bogus pointer offset and / or bytecount value
+ ErrNonfatal("Illegal value pointer for tag %04x", Tag,0);
+ continue;
+ }
+ ValuePtr = OffsetBase+OffsetVal;
+ }
+ else
+ {
+ // 4 bytes or less and value is in the dir entry itself
+ ValuePtr = DirEntry+8;
+ }
+
+ switch(Tag)
+ {
+ case TAG_GPS_LAT_REF:
+ m_ExifInfo->GpsLat[0] = ValuePtr[0];
+ m_ExifInfo->GpsLat[1] = 0;
+ break;
+
+ case TAG_GPS_LONG_REF:
+ m_ExifInfo->GpsLong[0] = ValuePtr[0];
+ m_ExifInfo->GpsLong[1] = 0;
+ break;
+
+ case TAG_GPS_LAT:
+ GetLatLong(Format, ValuePtr, ComponentSize, m_ExifInfo->GpsLat);
+ break;
+ case TAG_GPS_LONG:
+ GetLatLong(Format, ValuePtr, ComponentSize, m_ExifInfo->GpsLong);
+ break;
+
+ case TAG_GPS_ALT_REF:
+ if (ValuePtr[0] != 0)
+ m_ExifInfo->GpsAlt[0] = '-';
+ m_ExifInfo->GpsAlt[1] = 0;
+ break;
+
+ case TAG_GPS_ALT:
+ {
+ char temp[18];
+ sprintf(temp, "%.2fm", static_cast<double>(ConvertAnyFormat(ValuePtr, Format)));
+ strcat(m_ExifInfo->GpsAlt, temp);
+ }
+ break;
+ }
+ }
+}
+
diff --git a/xbmc/pictures/ExifParse.h b/xbmc/pictures/ExifParse.h
new file mode 100644
index 0000000..6da3bf2
--- /dev/null
+++ b/xbmc/pictures/ExifParse.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "libexif.h"
+
+class CExifParse
+{
+ public:
+ ~CExifParse(void) = default;
+ bool Process(const unsigned char* const Data, const unsigned short length, ExifInfo_t *info);
+ static int Get16(const void* const Short, const bool motorolaOrder=true);
+ static int Get32(const void* const Long, const bool motorolaOrder=true);
+
+ private:
+ ExifInfo_t *m_ExifInfo = nullptr;
+ double m_FocalPlaneXRes = 0.0;
+ double m_FocalPlaneUnits = 0.0;
+ unsigned m_LargestExifOffset = 0; // Last exif data referenced (to check if thumbnail is at end)
+ int m_ExifImageWidth = 0;
+ bool m_MotorolaOrder = false;
+ bool m_DateFound = false;
+
+// void LocaliseDate (void);
+// void GetExposureTime (const float exposureTime);
+ double ConvertAnyFormat(const void* const ValuePtr, int Format);
+ void ProcessDir(const unsigned char* const DirStart,
+ const unsigned char* const OffsetBase,
+ const unsigned ExifLength, int NestingLevel);
+ void ProcessGpsInfo(const unsigned char* const DirStart,
+ int ByteCountUnused,
+ const unsigned char* const OffsetBase,
+ unsigned ExifLength);
+ void GetLatLong(const unsigned int Format,
+ const unsigned char* ValuePtr,
+ const int ComponentSize,
+ char *latlongString);
+};
+
diff --git a/xbmc/pictures/GUIDialogPictureInfo.cpp b/xbmc/pictures/GUIDialogPictureInfo.cpp
new file mode 100644
index 0000000..90764e0
--- /dev/null
+++ b/xbmc/pictures/GUIDialogPictureInfo.cpp
@@ -0,0 +1,114 @@
+/*
+ * 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 "GUIDialogPictureInfo.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/Key.h"
+
+#define CONTROL_PICTURE_INFO 5
+
+#define SLIDESHOW_STRING_BASE 21800 - SLIDESHOW_LABELS_START
+
+CGUIDialogPictureInfo::CGUIDialogPictureInfo(void)
+ : CGUIDialog(WINDOW_DIALOG_PICTURE_INFO, "DialogPictureInfo.xml")
+{
+ m_pictureInfo = new CFileItemList;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogPictureInfo::~CGUIDialogPictureInfo(void)
+{
+ delete m_pictureInfo;
+}
+
+void CGUIDialogPictureInfo::SetPicture(CFileItem *item)
+{
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPicturesInfoProvider().SetCurrentSlide(item);
+}
+
+void CGUIDialogPictureInfo::OnInitWindow()
+{
+ UpdatePictureInfo();
+ CGUIDialog::OnInitWindow();
+}
+
+bool CGUIDialogPictureInfo::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ // if we're running from slideshow mode, drop the "next picture" and "previous picture" actions through.
+ case ACTION_NEXT_PICTURE:
+ case ACTION_PREV_PICTURE:
+ case ACTION_PLAYER_PLAY:
+ case ACTION_PAUSE:
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW)
+ {
+ CGUIWindow* pWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_SLIDESHOW);
+ return pWindow->OnAction(action);
+ }
+ break;
+
+ case ACTION_SHOW_INFO:
+ Close();
+ return true;
+ };
+ return CGUIDialog::OnAction(action);
+}
+
+void CGUIDialogPictureInfo::FrameMove()
+{
+ const CFileItem* item = CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPicturesInfoProvider().GetCurrentSlide();
+ if (item && item->GetPath() != m_currentPicture)
+ {
+ UpdatePictureInfo();
+ m_currentPicture = item->GetPath();
+ }
+ CGUIDialog::FrameMove();
+}
+
+void CGUIDialogPictureInfo::UpdatePictureInfo()
+{
+ // add stuff from the current slide to the list
+ CGUIMessage msgReset(GUI_MSG_LABEL_RESET, GetID(), CONTROL_PICTURE_INFO);
+ OnMessage(msgReset);
+ m_pictureInfo->Clear();
+ for (int info = SLIDESHOW_LABELS_START; info <= SLIDESHOW_LABELS_END; ++info)
+ {
+ // we only want to add SLIDESHOW_EXIF_DATE_TIME
+ // so we skip the other date formats
+ if (info == SLIDESHOW_EXIF_DATE || info == SLIDESHOW_EXIF_LONG_DATE || info == SLIDESHOW_EXIF_LONG_DATE_TIME )
+ continue;
+
+ std::string picInfo =
+ CServiceBroker::GetGUI()->GetInfoManager().GetLabel(info, INFO::DEFAULT_CONTEXT);
+ if (!picInfo.empty())
+ {
+ CFileItemPtr item(new CFileItem(g_localizeStrings.Get(SLIDESHOW_STRING_BASE + info)));
+ item->SetLabel2(picInfo);
+ m_pictureInfo->Add(item);
+ }
+ }
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_PICTURE_INFO, 0, 0, m_pictureInfo);
+ OnMessage(msg);
+}
+
+void CGUIDialogPictureInfo::OnDeinitWindow(int nextWindowID)
+{
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+ CGUIMessage msgReset(GUI_MSG_LABEL_RESET, GetID(), CONTROL_PICTURE_INFO);
+ OnMessage(msgReset);
+ m_pictureInfo->Clear();
+ m_currentPicture.clear();
+}
diff --git a/xbmc/pictures/GUIDialogPictureInfo.h b/xbmc/pictures/GUIDialogPictureInfo.h
new file mode 100644
index 0000000..20e5b3b
--- /dev/null
+++ b/xbmc/pictures/GUIDialogPictureInfo.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CFileItemList;
+
+class CGUIDialogPictureInfo :
+ public CGUIDialog
+{
+public:
+ CGUIDialogPictureInfo(void);
+ ~CGUIDialogPictureInfo(void) override;
+ void SetPicture(CFileItem *item);
+ void FrameMove() override;
+
+protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ bool OnAction(const CAction& action) override;
+ void UpdatePictureInfo();
+
+ CFileItemList* m_pictureInfo;
+ std::string m_currentPicture;
+};
diff --git a/xbmc/pictures/GUIViewStatePictures.cpp b/xbmc/pictures/GUIViewStatePictures.cpp
new file mode 100644
index 0000000..47d0cdf
--- /dev/null
+++ b/xbmc/pictures/GUIViewStatePictures.cpp
@@ -0,0 +1,86 @@
+/*
+ * 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 "GUIViewStatePictures.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "filesystem/Directory.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileExtensionProvider.h"
+#include "view/ViewState.h"
+#include "view/ViewStateSettings.h"
+
+using namespace XFILE;
+using namespace ADDON;
+
+CGUIViewStateWindowPictures::CGUIViewStateWindowPictures(const CFileItemList& items) : CGUIViewState(items)
+{
+ if (items.IsVirtualDirectoryRoot())
+ {
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS());
+ AddSortMethod(SortByDriveType, 564, LABEL_MASKS());
+ SetSortMethod(SortByLabel);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderAscending);
+ }
+ else
+ {
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS("%L", "%I", "%L", "")); // Filename, Size | Foldername, empty
+ AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I")); // Filename, Size | Foldername, Size
+ AddSortMethod(SortByDate, 552, LABEL_MASKS("%L", "%J", "%L", "%J")); // Filename, Date | Foldername, Date
+ AddSortMethod(SortByDateTaken, 577, LABEL_MASKS("%L", "%t", "%L", "%J")); // Filename, DateTaken | Foldername, Date
+ AddSortMethod(SortByFile, 561, LABEL_MASKS("%L", "%I", "%L", "")); // Filename, Size | FolderName, empty
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("pictures");
+ SetSortMethod(viewState->m_sortDescription);
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ LoadViewState(items.GetPath(), WINDOW_PICTURES);
+}
+
+void CGUIViewStateWindowPictures::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_PICTURES, CViewStateSettings::GetInstance().Get("pictures"));
+}
+
+std::string CGUIViewStateWindowPictures::GetLockType()
+{
+ return "pictures";
+}
+
+std::string CGUIViewStateWindowPictures::GetExtensions()
+{
+ std::string extensions = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_PICTURES_SHOWVIDEOS))
+ extensions += "|" + CServiceBroker::GetFileExtensionProvider().GetVideoExtensions();
+
+ return extensions;
+}
+
+VECSOURCES& CGUIViewStateWindowPictures::GetSources()
+{
+ VECSOURCES *pictureSources = CMediaSourceSettings::GetInstance().GetSources("pictures");
+
+ // Guard against source type not existing
+ if (pictureSources == nullptr)
+ {
+ static VECSOURCES empty;
+ return empty;
+ }
+
+ return *pictureSources;
+}
+
diff --git a/xbmc/pictures/GUIViewStatePictures.h b/xbmc/pictures/GUIViewStatePictures.h
new file mode 100644
index 0000000..343089a
--- /dev/null
+++ b/xbmc/pictures/GUIViewStatePictures.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "view/GUIViewState.h"
+
+class CGUIViewStateWindowPictures : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateWindowPictures(const CFileItemList& items);
+
+ std::string GetLockType() override;
+ std::string GetExtensions() override;
+ VECSOURCES& GetSources() override;
+
+protected:
+ void SaveViewState() override;
+};
+
diff --git a/xbmc/pictures/GUIWindowPictures.cpp b/xbmc/pictures/GUIWindowPictures.cpp
new file mode 100644
index 0000000..ebc8fe9
--- /dev/null
+++ b/xbmc/pictures/GUIWindowPictures.cpp
@@ -0,0 +1,625 @@
+/*
+ * Copyright (C) 2005-2020 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 "GUIWindowPictures.h"
+
+#include "Autorun.h"
+#include "FileItem.h"
+#include "GUIDialogPictureInfo.h"
+#include "GUIPassword.h"
+#include "GUIWindowSlideShow.h"
+#include "PictureInfoLoader.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/gui/GUIDialogAddonInfo.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogMediaSource.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "media/MediaLockState.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "view/GUIViewState.h"
+
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_LABELFILES 12
+
+using namespace XFILE;
+using namespace KODI::MESSAGING;
+
+using namespace std::chrono_literals;
+
+#define CONTROL_BTNSLIDESHOW 6
+#define CONTROL_BTNSLIDESHOW_RECURSIVE 7
+#define CONTROL_SHUFFLE 9
+
+CGUIWindowPictures::CGUIWindowPictures(void)
+ : CGUIMediaWindow(WINDOW_PICTURES, "MyPics.xml")
+{
+ m_thumbLoader.SetObserver(this);
+ m_slideShowStarted = false;
+ m_dlgProgress = NULL;
+}
+
+void CGUIWindowPictures::OnInitWindow()
+{
+ CGUIMediaWindow::OnInitWindow();
+ if (m_slideShowStarted)
+ {
+ CGUIWindowSlideShow* wndw = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ std::string path;
+ if (wndw && wndw->GetCurrentSlide())
+ path = URIUtils::GetDirectory(wndw->GetCurrentSlide()->GetPath());
+ if (m_vecItems->IsPath(path))
+ {
+ if (wndw && wndw->GetCurrentSlide())
+ m_viewControl.SetSelectedItem(wndw->GetCurrentSlide()->GetPath());
+ SaveSelectedItemInHistory();
+ }
+ m_slideShowStarted = false;
+ }
+}
+
+CGUIWindowPictures::~CGUIWindowPictures(void) = default;
+
+bool CGUIWindowPictures::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ // is this the first time accessing this window?
+ if (m_vecItems->GetPath() == "?" && message.GetStringParam().empty())
+ message.SetStringParam(CMediaSourceSettings::GetInstance().GetDefaultSource("pictures"));
+
+ m_dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTNSLIDESHOW) // Slide Show
+ {
+ OnSlideShow();
+ }
+ else if (iControl == CONTROL_BTNSLIDESHOW_RECURSIVE) // Recursive Slide Show
+ {
+ OnSlideShowRecursive();
+ }
+ else if (iControl == CONTROL_SHUFFLE)
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->ToggleBool(CSettings::SETTING_SLIDESHOW_SHUFFLE);
+ settings->Save();
+ }
+ else if (m_viewControl.HasControl(iControl)) // list/thumb control
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ int iAction = message.GetParam1();
+
+ // iItem is checked for validity inside these routines
+ if (iAction == ACTION_DELETE_ITEM)
+ {
+ // is delete allowed?
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_ALLOWFILEDELETION))
+ OnDeleteItem(iItem);
+ else
+ return false;
+ }
+ else if (iAction == ACTION_PLAYER_PLAY)
+ {
+ ShowPicture(iItem, true);
+ return true;
+ }
+ else if (iAction == ACTION_SHOW_INFO)
+ {
+ OnItemInfo(iItem);
+ return true;
+ }
+ }
+ }
+ break;
+ }
+ return CGUIMediaWindow::OnMessage(message);
+}
+
+void CGUIWindowPictures::UpdateButtons()
+{
+ CGUIMediaWindow::UpdateButtons();
+
+ // Update the shuffle button
+ SET_CONTROL_SELECTED(GetID(), CONTROL_SHUFFLE, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SLIDESHOW_SHUFFLE));
+
+ // check we can slideshow or recursive slideshow
+ int nFolders = m_vecItems->GetFolderCount();
+ if (nFolders == m_vecItems->Size() ||
+ m_vecItems->GetPath() == "addons://sources/image/")
+ {
+ CONTROL_DISABLE(CONTROL_BTNSLIDESHOW);
+ }
+ else
+ {
+ CONTROL_ENABLE(CONTROL_BTNSLIDESHOW);
+ }
+ if (m_guiState.get() && !m_guiState->HideParentDirItems())
+ nFolders--;
+ if (m_vecItems->Size() == 0 || nFolders == 0 ||
+ m_vecItems->GetPath() == "addons://sources/image/")
+ {
+ CONTROL_DISABLE(CONTROL_BTNSLIDESHOW_RECURSIVE);
+ }
+ else
+ {
+ CONTROL_ENABLE(CONTROL_BTNSLIDESHOW_RECURSIVE);
+ }
+}
+
+void CGUIWindowPictures::OnPrepareFileItems(CFileItemList& items)
+{
+ CGUIMediaWindow::OnPrepareFileItems(items);
+
+ for (int i=0;i<items.Size();++i )
+ if (StringUtils::EqualsNoCase(items[i]->GetLabel(), "folder.jpg"))
+ items.Remove(i);
+
+ if (items.GetFolderCount() == items.Size() || !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_PICTURES_USETAGS))
+ return;
+
+ // Start the music info loader thread
+ CPictureInfoLoader loader;
+ loader.SetProgressCallback(m_dlgProgress);
+ loader.Load(items);
+
+ bool bShowProgress = !CServiceBroker::GetGUI()->GetWindowManager().HasModalDialog(true);
+ bool bProgressVisible = false;
+
+ auto start = std::chrono::steady_clock::now();
+
+ while (loader.IsLoading() && m_dlgProgress && !m_dlgProgress->IsCanceled())
+ {
+ if (bShowProgress)
+ { // Do we have to init a progress dialog?
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ if (!bProgressVisible && duration.count() > 1500 && m_dlgProgress)
+ { // tag loading takes more then 1.5 secs, show a progress dialog
+ CURL url(items.GetPath());
+
+ m_dlgProgress->SetHeading(CVariant{189});
+ m_dlgProgress->SetLine(0, CVariant{505});
+ m_dlgProgress->SetLine(1, CVariant{""});
+ m_dlgProgress->SetLine(2, CVariant{url.GetWithoutUserDetails()});
+ m_dlgProgress->Open();
+ m_dlgProgress->ShowProgressBar(true);
+ bProgressVisible = true;
+ }
+
+ if (bProgressVisible && m_dlgProgress)
+ { // keep GUI alive
+ m_dlgProgress->Progress();
+ }
+ } // if (bShowProgress)
+ KODI::TIME::Sleep(1ms);
+ } // while (loader.IsLoading())
+
+ if (bProgressVisible && m_dlgProgress)
+ m_dlgProgress->Close();
+}
+
+bool CGUIWindowPictures::Update(const std::string &strDirectory, bool updateFilterPath /* = true */)
+{
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ if (!CGUIMediaWindow::Update(strDirectory, updateFilterPath))
+ return false;
+
+ m_vecItems->SetArt("thumb", "");
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_PICTURES_GENERATETHUMBS))
+ m_thumbLoader.Load(*m_vecItems);
+
+ CPictureThumbLoader thumbLoader;
+ std::string thumb = thumbLoader.GetCachedImage(*m_vecItems, "thumb");
+ m_vecItems->SetArt("thumb", thumb);
+
+ return true;
+}
+
+bool CGUIWindowPictures::OnClick(int iItem, const std::string &player)
+{
+ if ( iItem < 0 || iItem >= m_vecItems->Size() ) return true;
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+
+ if (pItem->IsCBZ() || pItem->IsCBR())
+ {
+ CURL pathToUrl;
+ if (pItem->IsCBZ())
+ pathToUrl = URIUtils::CreateArchivePath("zip", pItem->GetURL(), "");
+ else
+ pathToUrl = URIUtils::CreateArchivePath("rar", pItem->GetURL(), "");
+
+ OnShowPictureRecursive(pathToUrl.Get());
+ return true;
+ }
+ else if (CGUIMediaWindow::OnClick(iItem, player))
+ return true;
+
+ return false;
+}
+
+bool CGUIWindowPictures::GetDirectory(const std::string &strDirectory, CFileItemList& items)
+{
+ if (!CGUIMediaWindow::GetDirectory(strDirectory, items))
+ return false;
+
+ std::string label;
+ if (items.GetLabel().empty() && m_rootDir.IsSource(items.GetPath(), CMediaSourceSettings::GetInstance().GetSources("pictures"), &label))
+ items.SetLabel(label);
+
+ if (items.GetContent().empty() && !items.IsVirtualDirectoryRoot() && !items.IsPlugin())
+ items.SetContent("images");
+ return true;
+}
+
+bool CGUIWindowPictures::OnPlayMedia(int iItem, const std::string &player)
+{
+ if (m_vecItems->Get(iItem)->IsVideo())
+ return CGUIMediaWindow::OnPlayMedia(iItem);
+
+ return ShowPicture(iItem, false);
+}
+
+bool CGUIWindowPictures::ShowPicture(int iItem, bool startSlideShow)
+{
+ if ( iItem < 0 || iItem >= m_vecItems->Size() ) return false;
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+ std::string strPicture = pItem->GetPath();
+
+#ifdef HAS_DVD_DRIVE
+ if (pItem->IsDVD())
+ return MEDIA_DETECT::CAutorun::PlayDiscAskResume(m_vecItems->Get(iItem)->GetPath());
+#endif
+
+ if (pItem->m_bIsShareOrDrive)
+ return false;
+
+ CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (!pSlideShow)
+ return false;
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingVideo())
+ g_application.StopPlaying();
+
+ pSlideShow->Reset();
+ bool bShowVideos = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_PICTURES_SHOWVIDEOS);
+ for (const auto& pItem : *m_vecItems)
+ {
+ if (!pItem->m_bIsFolder &&
+ !(URIUtils::IsRAR(pItem->GetPath()) || URIUtils::IsZIP(pItem->GetPath())) &&
+ (pItem->IsPicture() || (bShowVideos && pItem->IsVideo())))
+ {
+ pSlideShow->Add(pItem.get());
+ }
+ }
+
+ if (pSlideShow->NumSlides() == 0)
+ return false;
+
+ pSlideShow->Select(strPicture);
+
+ if (startSlideShow)
+ pSlideShow->StartSlideShow();
+ else
+ {
+ CVariant param;
+ param["player"]["speed"] = 1;
+ param["player"]["playerid"] = PLAYLIST::TYPE_PICTURE;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPlay",
+ pSlideShow->GetCurrentSlide(), param);
+ }
+
+ m_slideShowStarted = true;
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SLIDESHOW);
+
+ return true;
+}
+
+void CGUIWindowPictures::OnShowPictureRecursive(const std::string& strPath)
+{
+ CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (pSlideShow)
+ {
+ // stop any video
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingVideo())
+ g_application.StopPlaying();
+
+ SortDescription sorting = m_guiState->GetSortMethod();
+ pSlideShow->AddFromPath(strPath, true,
+ sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
+ if (pSlideShow->NumSlides())
+ {
+ m_slideShowStarted = true;
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SLIDESHOW);
+ }
+ }
+}
+
+void CGUIWindowPictures::OnSlideShowRecursive(const std::string &strPicture)
+{
+ CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (pSlideShow)
+ {
+ std::string strExtensions;
+ CFileItemList items;
+ CGUIViewState* viewState=CGUIViewState::GetViewState(GetID(), items);
+ if (viewState)
+ {
+ strExtensions = viewState->GetExtensions();
+ delete viewState;
+ }
+ m_slideShowStarted = true;
+
+ SortDescription sorting = m_guiState->GetSortMethod();
+ pSlideShow->RunSlideShow(strPicture, true,
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SLIDESHOW_SHUFFLE),false,
+ "", true,
+ sorting.sortBy, sorting.sortOrder, sorting.sortAttributes,
+ strExtensions);
+ }
+}
+
+void CGUIWindowPictures::OnSlideShowRecursive()
+{
+ OnSlideShowRecursive(m_vecItems->GetPath());
+}
+
+void CGUIWindowPictures::OnSlideShow()
+{
+ OnSlideShow(m_vecItems->GetPath());
+}
+
+void CGUIWindowPictures::OnSlideShow(const std::string &strPicture)
+{
+ CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (pSlideShow)
+ {
+ std::string strExtensions;
+ CFileItemList items;
+ CGUIViewState* viewState=CGUIViewState::GetViewState(GetID(), items);
+ if (viewState)
+ {
+ strExtensions = viewState->GetExtensions();
+ delete viewState;
+ }
+ m_slideShowStarted = true;
+
+ SortDescription sorting = m_guiState->GetSortMethod();
+ pSlideShow->RunSlideShow(strPicture, false ,false, false,
+ "", true,
+ sorting.sortBy, sorting.sortOrder, sorting.sortAttributes,
+ strExtensions);
+ }
+}
+
+void CGUIWindowPictures::OnRegenerateThumbs()
+{
+ if (m_thumbLoader.IsLoading()) return;
+ m_thumbLoader.SetRegenerateThumbs(true);
+ m_thumbLoader.Load(*m_vecItems);
+}
+
+void CGUIWindowPictures::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+
+ if (item)
+ {
+ if ( m_vecItems->IsVirtualDirectoryRoot() || m_vecItems->GetPath() == "sources://pictures/" )
+ {
+ CGUIDialogContextMenu::GetContextButtons("pictures", item, buttons);
+ }
+ else
+ {
+ if (item)
+ {
+ if (!(item->m_bIsFolder || item->IsZIP() || item->IsRAR() || item->IsCBZ() || item->IsCBR() || item->IsScript()))
+ {
+ if (item->IsPicture())
+ buttons.Add(CONTEXT_BUTTON_INFO, 13406); // picture info
+ buttons.Add(CONTEXT_BUTTON_VIEW_SLIDESHOW, item->m_bIsFolder ? 13317 : 13422); // View Slideshow
+ }
+ if (item->m_bIsFolder)
+ buttons.Add(CONTEXT_BUTTON_RECURSIVE_SLIDESHOW, 13318); // Recursive Slideshow
+
+ if (!m_thumbLoader.IsLoading())
+ buttons.Add(CONTEXT_BUTTON_REFRESH_THUMBS, 13315); // Create Thumbnails
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_ALLOWFILEDELETION) && !item->IsReadOnly())
+ {
+ buttons.Add(CONTEXT_BUTTON_DELETE, 117);
+ buttons.Add(CONTEXT_BUTTON_RENAME, 118);
+ }
+ }
+
+ if (!item->IsPlugin() && !item->IsScript() && !m_vecItems->IsPlugin())
+ buttons.Add(CONTEXT_BUTTON_SWITCH_MEDIA, 523);
+ }
+ }
+ CGUIMediaWindow::GetContextButtons(itemNumber, buttons);
+}
+
+bool CGUIWindowPictures::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ CFileItemPtr item = (itemNumber >= 0 && itemNumber < m_vecItems->Size()) ? m_vecItems->Get(itemNumber) : CFileItemPtr();
+ if (CGUIDialogContextMenu::OnContextButton("pictures", item, button))
+ {
+ Update("");
+ return true;
+ }
+ switch (button)
+ {
+ case CONTEXT_BUTTON_VIEW_SLIDESHOW:
+ if (item && item->m_bIsFolder)
+ OnSlideShow(item->GetPath());
+ else
+ ShowPicture(itemNumber, true);
+ return true;
+ case CONTEXT_BUTTON_RECURSIVE_SLIDESHOW:
+ if (item)
+ OnSlideShowRecursive(item->GetPath());
+ return true;
+ case CONTEXT_BUTTON_INFO:
+ OnItemInfo(itemNumber);
+ return true;
+ case CONTEXT_BUTTON_REFRESH_THUMBS:
+ OnRegenerateThumbs();
+ return true;
+ case CONTEXT_BUTTON_DELETE:
+ OnDeleteItem(itemNumber);
+ return true;
+ case CONTEXT_BUTTON_RENAME:
+ OnRenameItem(itemNumber);
+ return true;
+ case CONTEXT_BUTTON_SWITCH_MEDIA:
+ CGUIDialogContextMenu::SwitchMedia("pictures", m_vecItems->GetPath());
+ return true;
+ default:
+ break;
+ }
+ return CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowPictures::OnAddMediaSource()
+{
+ return CGUIDialogMediaSource::ShowAndAddMediaSource("pictures");
+}
+
+void CGUIWindowPictures::OnItemLoaded(CFileItem *pItem)
+{
+ CPictureThumbLoader::ProcessFoldersAndArchives(pItem);
+}
+
+void CGUIWindowPictures::LoadPlayList(const std::string& strPlayList)
+{
+ CLog::Log(LOGDEBUG,
+ "CGUIWindowPictures::LoadPlayList()... converting playlist into slideshow: {}",
+ strPlayList);
+ std::unique_ptr<PLAYLIST::CPlayList> pPlayList(PLAYLIST::CPlayListFactory::Create(strPlayList));
+ if (nullptr != pPlayList)
+ {
+ if (!pPlayList->Load(strPlayList))
+ {
+ HELPERS::ShowOKDialogText(CVariant{6}, CVariant{477});
+ return ; //hmmm unable to load playlist?
+ }
+ }
+
+ PLAYLIST::CPlayList playlist = *pPlayList;
+ if (playlist.size() > 0)
+ {
+ // set up slideshow
+ CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (!pSlideShow)
+ return;
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingVideo())
+ g_application.StopPlaying();
+
+ // convert playlist items into slideshow items
+ pSlideShow->Reset();
+ for (int i = 0; i < playlist.size(); ++i)
+ {
+ CFileItemPtr pItem = playlist[i];
+ //CLog::Log(LOGDEBUG,"-- playlist item: {}", pItem->GetPath());
+ if (pItem->IsPicture() && !(pItem->IsZIP() || pItem->IsRAR() || pItem->IsCBZ() || pItem->IsCBR()))
+ pSlideShow->Add(pItem.get());
+ }
+
+ // start slideshow if there are items
+ pSlideShow->StartSlideShow();
+ if (pSlideShow->NumSlides())
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SLIDESHOW);
+ }
+}
+
+void CGUIWindowPictures::OnItemInfo(int itemNumber)
+{
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+ if (!item)
+ return;
+ if (!m_vecItems->IsPlugin() && (item->IsPlugin() || item->IsScript()))
+ {
+ CGUIDialogAddonInfo::ShowForItem(item);
+ return;
+ }
+ if (item->m_bIsFolder || item->IsZIP() || item->IsRAR() || item->IsCBZ() || item->IsCBR() || !item->IsPicture())
+ return;
+ CGUIDialogPictureInfo *pictureInfo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPictureInfo>(WINDOW_DIALOG_PICTURE_INFO);
+ if (pictureInfo)
+ {
+ pictureInfo->SetPicture(item.get());
+ pictureInfo->Open();
+ }
+}
+
+std::string CGUIWindowPictures::GetStartFolder(const std::string &dir)
+{
+ if (StringUtils::EqualsNoCase(dir, "plugins") ||
+ StringUtils::EqualsNoCase(dir, "addons"))
+ return "addons://sources/image/";
+
+ SetupShares();
+ VECSOURCES shares;
+ m_rootDir.GetSources(shares);
+ bool bIsSourceName = false;
+ int iIndex = CUtil::GetMatchingSource(dir, shares, bIsSourceName);
+ if (iIndex > -1)
+ {
+ if (iIndex < static_cast<int>(shares.size()) && shares[iIndex].m_iHasLock == LOCK_STATE_LOCKED)
+ {
+ CFileItem item(shares[iIndex]);
+ if (!g_passwordManager.IsItemUnlocked(&item,"pictures"))
+ return "";
+ }
+ if (bIsSourceName)
+ return shares[iIndex].strPath;
+ return dir;
+ }
+ return CGUIMediaWindow::GetStartFolder(dir);
+}
diff --git a/xbmc/pictures/GUIWindowPictures.h b/xbmc/pictures/GUIWindowPictures.h
new file mode 100644
index 0000000..115e562
--- /dev/null
+++ b/xbmc/pictures/GUIWindowPictures.h
@@ -0,0 +1,51 @@
+/*
+ * 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 "PictureThumbLoader.h"
+#include "windows/GUIMediaWindow.h"
+
+class CGUIDialogProgress;
+
+class CGUIWindowPictures : public CGUIMediaWindow, public IBackgroundLoaderObserver
+{
+public:
+ CGUIWindowPictures(void);
+ ~CGUIWindowPictures(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void OnInitWindow() override;
+
+protected:
+ bool GetDirectory(const std::string &strDirectory, CFileItemList& items) override;
+ void OnItemInfo(int item);
+ bool OnClick(int iItem, const std::string &player = "") override;
+ void UpdateButtons() override;
+ void OnPrepareFileItems(CFileItemList& items) override;
+ bool Update(const std::string &strDirectory, bool updateFilterPath = true) override;
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool OnAddMediaSource() override;
+ std::string GetStartFolder(const std::string &dir) override;
+
+ void OnRegenerateThumbs();
+ bool OnPlayMedia(int iItem, const std::string &player = "") override;
+ bool ShowPicture(int iItem, bool startSlideShow);
+ void OnShowPictureRecursive(const std::string& strPath);
+ void OnSlideShow(const std::string& strPicture);
+ void OnSlideShow();
+ void OnSlideShowRecursive(const std::string& strPicture);
+ void OnSlideShowRecursive();
+ void OnItemLoaded(CFileItem* pItem) override;
+ void LoadPlayList(const std::string& strPlayList) override;
+
+ CGUIDialogProgress* m_dlgProgress;
+
+ CPictureThumbLoader m_thumbLoader;
+ bool m_slideShowStarted;
+};
diff --git a/xbmc/pictures/GUIWindowSlideShow.cpp b/xbmc/pictures/GUIWindowSlideShow.cpp
new file mode 100644
index 0000000..a1a898b
--- /dev/null
+++ b/xbmc/pictures/GUIWindowSlideShow.cpp
@@ -0,0 +1,1382 @@
+/*
+ * 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 "GUIWindowSlideShow.h"
+
+#include "FileItem.h"
+#include "GUIDialogPictureInfo.h"
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "URL.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationPowerHandling.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUILabelControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/Texture.h"
+#include "input/Key.h"
+#include "interfaces/AnnouncementManager.h"
+#include "pictures/GUIViewStatePictures.h"
+#include "pictures/PictureThumbLoader.h"
+#include "playlists/PlayListTypes.h"
+#include "rendering/RenderSystem.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Random.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <random>
+
+using namespace XFILE;
+using namespace KODI::MESSAGING;
+using namespace std::chrono_literals;
+
+#define MAX_ZOOM_FACTOR 10
+#define MAX_PICTURE_SIZE 2048*2048
+
+#define IMMEDIATE_TRANSITION_TIME 1
+
+#define PICTURE_MOVE_AMOUNT 0.02f
+#define PICTURE_MOVE_AMOUNT_ANALOG 0.01f
+#define PICTURE_MOVE_AMOUNT_TOUCH 0.002f
+#define PICTURE_VIEW_BOX_COLOR 0xffffff00 // YELLOW
+#define PICTURE_VIEW_BOX_BACKGROUND 0xff000000 // BLACK
+
+#define ROTATION_SNAP_RANGE 10.0f
+
+#define LABEL_ROW1 10
+#define CONTROL_PAUSE 13
+
+static float zoomamount[10] = { 1.0f, 1.2f, 1.5f, 2.0f, 2.8f, 4.0f, 6.0f, 9.0f, 13.5f, 20.0f };
+
+CBackgroundPicLoader::CBackgroundPicLoader()
+ : CThread("BgPicLoader")
+ , m_iPic{0}
+ , m_iSlideNumber{0}
+ , m_maxWidth{0}
+ , m_maxHeight{0}
+ , m_isLoading{false}
+ , m_pCallback{nullptr}
+{
+}
+
+CBackgroundPicLoader::~CBackgroundPicLoader()
+{
+ StopThread();
+}
+
+void CBackgroundPicLoader::Create(CGUIWindowSlideShow *pCallback)
+{
+ m_pCallback = pCallback;
+ m_isLoading = false;
+ CThread::Create(false);
+}
+
+void CBackgroundPicLoader::Process()
+{
+ auto totalTime = std::chrono::milliseconds(0);
+ unsigned int count = 0;
+ while (!m_bStop)
+ { // loop around forever, waiting for the app to call LoadPic
+ if (AbortableWait(m_loadPic, 10ms) == WAIT_SIGNALED)
+ {
+ if (m_pCallback)
+ {
+ auto start = std::chrono::steady_clock::now();
+ std::unique_ptr<CTexture> texture =
+ CTexture::LoadFromFile(m_strFileName, m_maxWidth, m_maxHeight);
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ totalTime += duration;
+ count++;
+ // tell our parent
+ bool bFullSize = false;
+ if (texture)
+ {
+ bFullSize = ((int)texture->GetWidth() < m_maxWidth) && ((int)texture->GetHeight() < m_maxHeight);
+ if (!bFullSize)
+ {
+ int iSize = texture->GetWidth() * texture->GetHeight() - MAX_PICTURE_SIZE;
+ if ((iSize + (int)texture->GetWidth() > 0) || (iSize + (int)texture->GetHeight() > 0))
+ bFullSize = true;
+ if (!bFullSize && texture->GetWidth() == CServiceBroker::GetRenderSystem()->GetMaxTextureSize())
+ bFullSize = true;
+ if (!bFullSize && texture->GetHeight() == CServiceBroker::GetRenderSystem()->GetMaxTextureSize())
+ bFullSize = true;
+ }
+ }
+ m_pCallback->OnLoadPic(m_iPic, m_iSlideNumber, m_strFileName, std::move(texture),
+ bFullSize);
+ m_isLoading = false;
+ }
+ }
+ }
+ if (count > 0)
+ CLog::Log(LOGDEBUG, "Time for loading {} images: {} ms, average {} ms", count,
+ totalTime.count(), totalTime.count() / count);
+}
+
+void CBackgroundPicLoader::LoadPic(int iPic, int iSlideNumber, const std::string &strFileName, const int maxWidth, const int maxHeight)
+{
+ m_iPic = iPic;
+ m_iSlideNumber = iSlideNumber;
+ m_strFileName = strFileName;
+ m_maxWidth = maxWidth;
+ m_maxHeight = maxHeight;
+ m_isLoading = true;
+ m_loadPic.Set();
+}
+
+CGUIWindowSlideShow::CGUIWindowSlideShow(void)
+ : CGUIDialog(WINDOW_SLIDESHOW, "SlideShow.xml")
+{
+ m_Resolution = RES_INVALID;
+ m_loadType = KEEP_IN_MEMORY;
+ m_bLoadNextPic = false;
+ Reset();
+}
+
+void CGUIWindowSlideShow::AnnouncePlayerPlay(const CFileItemPtr& item)
+{
+ CVariant param;
+ param["player"]["speed"] = m_bSlideShow && !m_bPause ? 1 : 0;
+ param["player"]["playerid"] = PLAYLIST::TYPE_PICTURE;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPlay", item, param);
+}
+
+void CGUIWindowSlideShow::AnnouncePlayerPause(const CFileItemPtr& item)
+{
+ CVariant param;
+ param["player"]["speed"] = 0;
+ param["player"]["playerid"] = PLAYLIST::TYPE_PICTURE;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPause", item, param);
+}
+
+void CGUIWindowSlideShow::AnnouncePlayerStop(const CFileItemPtr& item)
+{
+ CVariant param;
+ param["player"]["playerid"] = PLAYLIST::TYPE_PICTURE;
+ param["end"] = true;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnStop", item, param);
+}
+
+void CGUIWindowSlideShow::AnnouncePlaylistClear()
+{
+ CVariant data;
+ data["playlistid"] = PLAYLIST::TYPE_PICTURE;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Playlist, "OnClear", data);
+}
+
+void CGUIWindowSlideShow::AnnouncePlaylistAdd(const CFileItemPtr& item, int pos)
+{
+ CVariant data;
+ data["playlistid"] = PLAYLIST::TYPE_PICTURE;
+ data["position"] = pos;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Playlist, "OnAdd", item, data);
+}
+
+void CGUIWindowSlideShow::AnnouncePropertyChanged(const std::string &strProperty, const CVariant &value)
+{
+ if (strProperty.empty() || value.isNull())
+ return;
+
+ CVariant data;
+ data["player"]["playerid"] = PLAYLIST::TYPE_PICTURE;
+ data["property"][strProperty] = value;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPropertyChanged",
+ data);
+}
+
+bool CGUIWindowSlideShow::IsPlaying() const
+{
+ return m_Image[m_iCurrentPic].IsLoaded();
+}
+
+void CGUIWindowSlideShow::Reset()
+{
+ m_bSlideShow = false;
+ m_bShuffled = false;
+ m_bPause = false;
+ m_bPlayingVideo = false;
+ m_bErrorMessage = false;
+ m_Image[0].UnLoad();
+ m_Image[0].Close();
+ m_Image[1].UnLoad();
+ m_Image[1].Close();
+
+ m_fRotate = 0.0f;
+ m_fInitialRotate = 0.0f;
+ m_iZoomFactor = 1;
+ m_fZoom = 1.0f;
+ m_fInitialZoom = 0.0f;
+ m_iCurrentSlide = 0;
+ m_iNextSlide = 1;
+ m_iCurrentPic = 0;
+ m_iDirection = 1;
+ m_iLastFailedNextSlide = -1;
+ m_slides.clear();
+ AnnouncePlaylistClear();
+ m_Resolution = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution();
+}
+
+void CGUIWindowSlideShow::OnDeinitWindow(int nextWindowID)
+{
+ if (m_Resolution != CDisplaySettings::GetInstance().GetCurrentResolution())
+ {
+ //FIXME: Use GUI resolution for now
+ //CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(CDisplaySettings::GetInstance().GetCurrentResolution(), true);
+ }
+
+ if (nextWindowID != WINDOW_FULLSCREEN_VIDEO &&
+ nextWindowID != WINDOW_FULLSCREEN_GAME)
+ {
+ // wait for any outstanding picture loads
+ if (m_pBackgroundLoader)
+ {
+ // sleep until the loader finishes loading the current pic
+ CLog::Log(LOGDEBUG,"Waiting for BackgroundLoader thread to close");
+ while (m_pBackgroundLoader->IsLoading())
+ KODI::TIME::Sleep(10ms);
+ // stop the thread
+ CLog::Log(LOGDEBUG,"Stopping BackgroundLoader thread");
+ m_pBackgroundLoader->StopThread();
+ m_pBackgroundLoader.reset();
+ }
+ // and close the images.
+ m_Image[0].Close();
+ m_Image[1].Close();
+ }
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPicturesInfoProvider().SetCurrentSlide(nullptr);
+ m_bSlideShow = false;
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIWindowSlideShow::Add(const CFileItem *picture)
+{
+ CFileItemPtr item(new CFileItem(*picture));
+ if (!item->HasVideoInfoTag() && !item->HasPictureInfoTag())
+ {
+ // item without tag; get mimetype then we can tell whether it's video item
+ item->FillInMimeType();
+
+ if (!item->IsVideo())
+ // then it is a picture and force tag generation
+ item->GetPictureInfoTag();
+ }
+ AnnouncePlaylistAdd(item, m_slides.size());
+
+ m_slides.emplace_back(std::move(item));
+}
+
+void CGUIWindowSlideShow::ShowNext()
+{
+ if (m_slides.size() == 1)
+ return;
+
+ m_iDirection = 1;
+ m_iNextSlide = GetNextSlide();
+ m_iZoomFactor = 1;
+ m_fZoom = 1.0f;
+ m_fRotate = 0.0f;
+ m_bLoadNextPic = true;
+}
+
+void CGUIWindowSlideShow::ShowPrevious()
+{
+ if (m_slides.size() == 1)
+ return;
+
+ m_iDirection = -1;
+ m_iNextSlide = GetNextSlide();
+ m_iZoomFactor = 1;
+ m_fZoom = 1.0f;
+ m_fRotate = 0.0f;
+ m_bLoadNextPic = true;
+}
+
+void CGUIWindowSlideShow::Select(const std::string& strPicture)
+{
+ for (size_t i = 0; i < m_slides.size(); ++i)
+ {
+ const CFileItemPtr item = m_slides.at(i);
+ if (item->GetPath() == strPicture)
+ {
+ m_iDirection = 1;
+ if (!m_Image[m_iCurrentPic].IsLoaded() && (!m_pBackgroundLoader || !m_pBackgroundLoader->IsLoading()))
+ {
+ // will trigger loading current slide when next Process call.
+ m_iCurrentSlide = i;
+ m_iNextSlide = GetNextSlide();
+ }
+ else
+ {
+ m_iNextSlide = i;
+ m_bLoadNextPic = true;
+ }
+ return ;
+ }
+ }
+}
+
+void CGUIWindowSlideShow::GetSlideShowContents(CFileItemList &list)
+{
+ for (size_t index = 0; index < m_slides.size(); index++)
+ list.Add(CFileItemPtr(new CFileItem(*m_slides.at(index))));
+}
+
+std::shared_ptr<const CFileItem> CGUIWindowSlideShow::GetCurrentSlide()
+{
+ if (m_iCurrentSlide >= 0 && m_iCurrentSlide < static_cast<int>(m_slides.size()))
+ return m_slides.at(m_iCurrentSlide);
+ return CFileItemPtr();
+}
+
+bool CGUIWindowSlideShow::InSlideShow() const
+{
+ return m_bSlideShow;
+}
+
+void CGUIWindowSlideShow::StartSlideShow()
+{
+ m_bSlideShow = true;
+ m_iDirection = 1;
+ if (m_slides.size())
+ AnnouncePlayerPlay(m_slides.at(m_iCurrentSlide));
+}
+
+void CGUIWindowSlideShow::SetDirection(int direction)
+{
+ direction = direction >= 0 ? 1 : -1;
+ if (m_iDirection != direction)
+ {
+ m_iDirection = direction;
+ m_iNextSlide = GetNextSlide();
+ }
+}
+
+void CGUIWindowSlideShow::Process(unsigned int currentTime, CDirtyRegionList &regions)
+{
+ const RESOLUTION_INFO res = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+
+ // reset the screensaver if we're in a slideshow
+ // (unless we are the screensaver!)
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (m_bSlideShow && !m_bPause && !appPower->IsInScreenSaver())
+ appPower->ResetScreenSaver();
+ int iSlides = m_slides.size();
+ if (!iSlides)
+ return;
+
+ // if we haven't processed yet, we should mark the whole screen
+ if (!HasProcessed())
+ regions.push_back(CDirtyRegion(CRect(0.0f, 0.0f, (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight())));
+
+ if (m_iCurrentSlide < 0 || m_iCurrentSlide >= static_cast<int>(m_slides.size()))
+ m_iCurrentSlide = 0;
+ if (m_iNextSlide < 0 || m_iNextSlide >= static_cast<int>(m_slides.size()))
+ m_iNextSlide = GetNextSlide();
+
+ // Create our background loader if necessary
+ if (!m_pBackgroundLoader)
+ {
+ m_pBackgroundLoader.reset(new CBackgroundPicLoader());
+ m_pBackgroundLoader->Create(this);
+ }
+
+ bool bSlideShow = m_bSlideShow && !m_bPause && !m_bPlayingVideo;
+ if (bSlideShow && m_slides.at(m_iCurrentSlide)->HasProperty("unplayable"))
+ {
+ m_iNextSlide = GetNextSlide();
+ if (m_iCurrentSlide == m_iNextSlide)
+ return;
+ m_iCurrentSlide = m_iNextSlide;
+ m_iNextSlide = GetNextSlide();
+ }
+
+ if (m_bErrorMessage)
+ { // we have an error when loading either the current or next picture
+ // check to see if we have a picture loaded
+ CLog::Log(LOGDEBUG, "We have an error loading picture {}!", m_pBackgroundLoader->SlideNumber());
+ if (m_iCurrentSlide == m_pBackgroundLoader->SlideNumber())
+ {
+ if (m_Image[m_iCurrentPic].IsLoaded())
+ {
+ // current image was already loaded, so we can ignore this error.
+ m_bErrorMessage = false;
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Error loading the current image {}: {}", m_iCurrentSlide,
+ m_slides.at(m_iCurrentSlide)->GetPath());
+ if (!m_slides.at(m_iCurrentPic)->IsVideo())
+ {
+ // try next if we are in slideshow
+ CLog::Log(LOGINFO, "set image {} unplayable", m_slides.at(m_iCurrentSlide)->GetPath());
+ m_slides.at(m_iCurrentSlide)->SetProperty("unplayable", true);
+ }
+ if (m_bLoadNextPic || (bSlideShow && !m_bPause && !m_slides.at(m_iCurrentPic)->IsVideo()))
+ {
+ // change to next item, wait loading.
+ m_iCurrentSlide = m_iNextSlide;
+ m_iNextSlide = GetNextSlide();
+ m_bErrorMessage = false;
+ }
+ // else just drop through - there's nothing we can do (error message will be displayed)
+ }
+ }
+ else if (m_iNextSlide == m_pBackgroundLoader->SlideNumber())
+ {
+ CLog::Log(LOGERROR, "Error loading the next image {}: {}", m_iNextSlide,
+ m_slides.at(m_iNextSlide)->GetPath());
+ // load next image failed, then skip to load next of next if next is not video.
+ if (!m_slides.at(m_iNextSlide)->IsVideo())
+ {
+ CLog::Log(LOGINFO, "set image {} unplayable", m_slides.at(m_iNextSlide)->GetPath());
+ m_slides.at(m_iNextSlide)->SetProperty("unplayable", true);
+ // change to next item, wait loading.
+ m_iNextSlide = GetNextSlide();
+ }
+ else
+ { // prevent reload the next pic and repeat fail.
+ m_iLastFailedNextSlide = m_iNextSlide;
+ }
+ m_bErrorMessage = false;
+ }
+ else
+ { // Non-current and non-next slide, just ignore error.
+ CLog::Log(LOGERROR, "Error loading the non-current non-next image {}/{}: {}", m_iNextSlide,
+ m_pBackgroundLoader->SlideNumber(), m_slides.at(m_iNextSlide)->GetPath());
+ m_bErrorMessage = false;
+ }
+ }
+
+ if (m_bErrorMessage)
+ { // hack, just mark it all
+ regions.push_back(CDirtyRegion(CRect(0.0f, 0.0f, (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight())));
+ return;
+ }
+
+ if (!m_Image[m_iCurrentPic].IsLoaded() && !m_pBackgroundLoader->IsLoading())
+ { // load first image
+ CFileItemPtr item = m_slides.at(m_iCurrentSlide);
+ std::string picturePath = GetPicturePath(item.get());
+ if (!picturePath.empty())
+ {
+ if (item->IsVideo())
+ CLog::Log(LOGDEBUG, "Loading the thumb {} for current video {}: {}", picturePath,
+ m_iCurrentSlide, item->GetPath());
+ else
+ CLog::Log(LOGDEBUG, "Loading the current image {}: {}", m_iCurrentSlide, item->GetPath());
+
+ // load using the background loader
+ int maxWidth, maxHeight;
+
+ GetCheckedSize((float)res.iWidth * m_fZoom,
+ (float)res.iHeight * m_fZoom,
+ maxWidth, maxHeight);
+ m_pBackgroundLoader->LoadPic(m_iCurrentPic, m_iCurrentSlide, picturePath, maxWidth, maxHeight);
+ m_iLastFailedNextSlide = -1;
+ m_bLoadNextPic = false;
+ }
+ }
+
+ // check if we should discard an already loaded next slide
+ if (m_Image[1 - m_iCurrentPic].IsLoaded() && m_Image[1 - m_iCurrentPic].SlideNumber() != m_iNextSlide)
+ m_Image[1 - m_iCurrentPic].Close();
+
+ if (m_iNextSlide != m_iCurrentSlide && m_Image[m_iCurrentPic].IsLoaded() && !m_Image[1 - m_iCurrentPic].IsLoaded() && !m_pBackgroundLoader->IsLoading() && m_iLastFailedNextSlide != m_iNextSlide)
+ { // load the next image
+ m_iLastFailedNextSlide = -1;
+ CFileItemPtr item = m_slides.at(m_iNextSlide);
+ std::string picturePath = GetPicturePath(item.get());
+ if (!picturePath.empty() && (!item->IsVideo() || !m_bSlideShow || m_bPause))
+ {
+ if (item->IsVideo())
+ CLog::Log(LOGDEBUG, "Loading the thumb {} for next video {}: {}", picturePath, m_iNextSlide,
+ item->GetPath());
+ else
+ CLog::Log(LOGDEBUG, "Loading the next image {}: {}", m_iNextSlide, item->GetPath());
+
+ int maxWidth, maxHeight;
+ GetCheckedSize((float)res.iWidth * m_fZoom,
+ (float)res.iHeight * m_fZoom,
+ maxWidth, maxHeight);
+ m_pBackgroundLoader->LoadPic(1 - m_iCurrentPic, m_iNextSlide, picturePath, maxWidth, maxHeight);
+ }
+ }
+
+ bool bPlayVideo = m_slides.at(m_iCurrentSlide)->IsVideo() && m_iVideoSlide != m_iCurrentSlide;
+ if (bPlayVideo)
+ bSlideShow = false;
+
+ // render the current image
+ if (m_Image[m_iCurrentPic].IsLoaded())
+ {
+ m_Image[m_iCurrentPic].SetInSlideshow(bSlideShow);
+ m_Image[m_iCurrentPic].Pause(!bSlideShow);
+ m_Image[m_iCurrentPic].Process(currentTime, regions);
+ }
+
+ // Check if we should be transitioning immediately
+ if (m_bLoadNextPic && m_Image[m_iCurrentPic].IsLoaded())
+ {
+ CLog::Log(LOGDEBUG, "Starting immediate transition due to user wanting slide {}",
+ m_slides.at(m_iNextSlide)->GetPath());
+ if (m_Image[m_iCurrentPic].StartTransition())
+ {
+ m_Image[m_iCurrentPic].SetTransitionTime(1, IMMEDIATE_TRANSITION_TIME);
+ m_bLoadNextPic = false;
+ }
+ }
+
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ // render the next image
+ if (m_Image[m_iCurrentPic].DrawNextImage())
+ {
+ if (m_bSlideShow && !m_bPause && m_slides.at(m_iNextSlide)->IsVideo())
+ {
+ // do not show thumb of video when playing slideshow
+ }
+ else if (m_Image[1 - m_iCurrentPic].IsLoaded())
+ {
+ if (appPlayer->IsPlayingVideo())
+ appPlayer->ClosePlayer();
+ m_bPlayingVideo = false;
+ m_iVideoSlide = -1;
+
+ // first time render the next image, make sure using current display effect.
+ if (!m_Image[1 - m_iCurrentPic].IsStarted())
+ {
+ CSlideShowPic::DISPLAY_EFFECT effect = GetDisplayEffect(m_iNextSlide);
+ if (m_Image[1 - m_iCurrentPic].DisplayEffectNeedChange(effect))
+ m_Image[1 - m_iCurrentPic].Reset(effect);
+ }
+ // set the appropriate transition time
+ m_Image[1 - m_iCurrentPic].SetTransitionTime(0, m_Image[m_iCurrentPic].GetTransitionTime(1));
+ m_Image[1 - m_iCurrentPic].Pause(!m_bSlideShow || m_bPause || m_slides.at(m_iNextSlide)->IsVideo());
+ m_Image[1 - m_iCurrentPic].Process(currentTime, regions);
+ }
+ else // next pic isn't loaded. We should hang around if it is in progress
+ {
+ if (m_pBackgroundLoader->IsLoading())
+ {
+ // CLog::Log(LOGDEBUG, "Having to hold the current image ({}) while we load {}", m_vecSlides[m_iCurrentSlide], m_vecSlides[m_iNextSlide]);
+ m_Image[m_iCurrentPic].Keep();
+ }
+ }
+ }
+
+ // check if we should swap images now
+ if (m_Image[m_iCurrentPic].IsFinished() || (m_bLoadNextPic && !m_Image[m_iCurrentPic].IsLoaded()))
+ {
+ m_bLoadNextPic = false;
+ if (m_Image[m_iCurrentPic].IsFinished())
+ CLog::Log(LOGDEBUG, "Image {} is finished rendering, switching to {}",
+ m_slides.at(m_iCurrentSlide)->GetPath(), m_slides.at(m_iNextSlide)->GetPath());
+ else
+ // what if it's bg loading?
+ CLog::Log(LOGDEBUG, "Image {} is not loaded, switching to {}",
+ m_slides.at(m_iCurrentSlide)->GetPath(), m_slides.at(m_iNextSlide)->GetPath());
+
+ if (m_Image[m_iCurrentPic].IsFinished() && m_iCurrentSlide == m_iNextSlide && m_Image[m_iCurrentPic].SlideNumber() == m_iNextSlide)
+ m_Image[m_iCurrentPic].Reset(GetDisplayEffect(m_iCurrentSlide));
+ else
+ {
+ if (m_Image[m_iCurrentPic].IsLoaded())
+ m_Image[m_iCurrentPic].Reset(GetDisplayEffect(m_iCurrentSlide));
+ else
+ m_Image[m_iCurrentPic].Close();
+
+ if ((m_Image[1 - m_iCurrentPic].IsLoaded() && m_Image[1 - m_iCurrentPic].SlideNumber() == m_iNextSlide) ||
+ (m_pBackgroundLoader->IsLoading() && m_pBackgroundLoader->SlideNumber() == m_iNextSlide && m_pBackgroundLoader->Pic() == 1 - m_iCurrentPic))
+ {
+ m_iCurrentPic = 1 - m_iCurrentPic;
+ }
+ else
+ {
+ m_Image[1 - m_iCurrentPic].Close();
+ m_iCurrentPic = 1 - m_iCurrentPic;
+ }
+ m_iCurrentSlide = m_iNextSlide;
+ m_iNextSlide = GetNextSlide();
+
+ bPlayVideo = m_slides.at(m_iCurrentSlide)->IsVideo() && m_iVideoSlide != m_iCurrentSlide;
+ }
+ AnnouncePlayerPlay(m_slides.at(m_iCurrentSlide));
+
+ m_iZoomFactor = 1;
+ m_fZoom = 1.0f;
+ m_fRotate = 0.0f;
+ }
+
+ if (bPlayVideo && !PlayVideo())
+ return;
+
+ if (m_Image[m_iCurrentPic].IsLoaded())
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPicturesInfoProvider().SetCurrentSlide(m_slides.at(m_iCurrentSlide).get());
+
+ RenderPause();
+ if (m_slides.at(m_iCurrentSlide)->IsVideo() && appPlayer && appPlayer->IsRenderingGuiLayer())
+ {
+ MarkDirtyRegion();
+ }
+ CGUIWindow::Process(currentTime, regions);
+ m_renderRegion.SetRect(0, 0, (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight());
+}
+
+void CGUIWindowSlideShow::Render()
+{
+ if (m_slides.empty())
+ return;
+
+ CGraphicContext& gfxCtx = CServiceBroker::GetWinSystem()->GetGfxContext();
+ gfxCtx.Clear(0xff000000);
+
+ if (m_slides.at(m_iCurrentSlide)->IsVideo())
+ {
+ gfxCtx.SetViewWindow(0, 0, m_coordsRes.iWidth, m_coordsRes.iHeight);
+ gfxCtx.SetRenderingResolution(gfxCtx.GetVideoResolution(), false);
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ if (appPlayer->IsRenderingVideoLayer())
+ {
+ const CRect old = gfxCtx.GetScissors();
+ CRect region = GetRenderRegion();
+ region.Intersect(old);
+ gfxCtx.SetScissors(region);
+ gfxCtx.Clear(0);
+ gfxCtx.SetScissors(old);
+ }
+ else if (appPlayer)
+ {
+ const UTILS::COLOR::Color alpha = gfxCtx.MergeAlpha(0xff000000) >> 24;
+ appPlayer->Render(false, alpha);
+ }
+
+ gfxCtx.SetRenderingResolution(m_coordsRes, m_needsScaling);
+ }
+ else
+ {
+ if (m_Image[m_iCurrentPic].IsLoaded())
+ m_Image[m_iCurrentPic].Render();
+
+ if (m_Image[m_iCurrentPic].DrawNextImage() && m_Image[1 - m_iCurrentPic].IsLoaded())
+ m_Image[1 - m_iCurrentPic].Render();
+ }
+
+ RenderErrorMessage();
+ CGUIWindow::Render();
+}
+
+void CGUIWindowSlideShow::RenderEx()
+{
+ if (m_slides.at(m_iCurrentSlide)->IsVideo())
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->Render(false, 255, false);
+ }
+
+ CGUIWindow::RenderEx();
+}
+
+int CGUIWindowSlideShow::GetNextSlide()
+{
+ if (m_slides.size() <= 1)
+ return m_iCurrentSlide;
+ int step = m_iDirection >= 0 ? 1 : -1;
+ int nextSlide = (m_iCurrentSlide + step + m_slides.size()) % m_slides.size();
+ while (nextSlide != m_iCurrentSlide)
+ {
+ if (!m_slides.at(nextSlide)->HasProperty("unplayable"))
+ return nextSlide;
+ nextSlide = (nextSlide + step + m_slides.size()) % m_slides.size();
+ }
+ return m_iCurrentSlide;
+}
+
+EVENT_RESULT CGUIWindowSlideShow::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_GESTURE_NOTIFY)
+ {
+ int result = EVENT_RESULT_ROTATE | EVENT_RESULT_ZOOM;
+ if (m_iZoomFactor == 1 || !m_Image[m_iCurrentPic].m_bCanMoveHorizontally)
+ result |= EVENT_RESULT_SWIPE;
+ else
+ result |= EVENT_RESULT_PAN_HORIZONTAL;
+
+ if (m_Image[m_iCurrentPic].m_bCanMoveVertically)
+ result |= EVENT_RESULT_PAN_VERTICAL;
+
+ return (EVENT_RESULT)result;
+ }
+ else if (event.m_id == ACTION_GESTURE_BEGIN)
+ {
+ m_firstGesturePoint = point;
+ m_fInitialZoom = m_fZoom;
+ m_fInitialRotate = m_fRotate;
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_PAN)
+ {
+ // zoomed in - free move mode
+ if (m_iZoomFactor != 1 &&
+ (m_Image[m_iCurrentPic].m_bCanMoveHorizontally || m_Image[m_iCurrentPic].m_bCanMoveVertically))
+ {
+ Move(PICTURE_MOVE_AMOUNT_TOUCH / m_iZoomFactor * (m_firstGesturePoint.x - point.x), PICTURE_MOVE_AMOUNT_TOUCH / m_iZoomFactor * (m_firstGesturePoint.y - point.y));
+ m_firstGesturePoint = point;
+ }
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_SWIPE_LEFT || event.m_id == ACTION_GESTURE_SWIPE_RIGHT)
+ {
+ if (m_iZoomFactor == 1 || !m_Image[m_iCurrentPic].m_bCanMoveHorizontally)
+ {
+ // on zoomlevel 1 just detect swipe left and right
+ if (event.m_id == ACTION_GESTURE_SWIPE_LEFT)
+ OnAction(CAction(ACTION_NEXT_PICTURE));
+ else
+ OnAction(CAction(ACTION_PREV_PICTURE));
+ }
+ }
+ else if (event.m_id == ACTION_GESTURE_END || event.m_id == ACTION_GESTURE_ABORT)
+ {
+ if (m_fRotate != 0.0f)
+ {
+ // "snap" to nearest of 0, 90, 180 and 270 if the
+ // difference in angle is +/-10 degrees
+ float reminder = fmodf(m_fRotate, 90.0f);
+ if (fabs(reminder) < ROTATION_SNAP_RANGE)
+ Rotate(-reminder);
+ else if (reminder > 90.0f - ROTATION_SNAP_RANGE)
+ Rotate(90.0f - reminder);
+ else if (-reminder > 90.0f - ROTATION_SNAP_RANGE)
+ Rotate(-90.0f - reminder);
+ }
+
+ m_fInitialZoom = 0.0f;
+ m_fInitialRotate = 0.0f;
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_ZOOM)
+ {
+ ZoomRelative(m_fInitialZoom * event.m_offsetX, true);
+ return EVENT_RESULT_HANDLED;
+ }
+ else if (event.m_id == ACTION_GESTURE_ROTATE)
+ {
+ Rotate(m_fInitialRotate + event.m_offsetX - m_fRotate, true);
+ return EVENT_RESULT_HANDLED;
+ }
+ return EVENT_RESULT_UNHANDLED;
+}
+
+bool CGUIWindowSlideShow::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_SHOW_INFO:
+ {
+ CGUIDialogPictureInfo *pictureInfo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPictureInfo>(WINDOW_DIALOG_PICTURE_INFO);
+ if (pictureInfo)
+ {
+ // no need to set the picture here, it's done in Render()
+ pictureInfo->Open();
+ }
+ }
+ break;
+ case ACTION_STOP:
+ {
+ if (m_slides.size())
+ AnnouncePlayerStop(m_slides.at(m_iCurrentSlide));
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingVideo())
+ appPlayer->ClosePlayer();
+ Close();
+ break;
+ }
+
+ case ACTION_NEXT_PICTURE:
+ ShowNext();
+ break;
+
+ case ACTION_PREV_PICTURE:
+ ShowPrevious();
+ break;
+
+ case ACTION_MOVE_RIGHT:
+ if (m_iZoomFactor == 1 || !m_Image[m_iCurrentPic].m_bCanMoveHorizontally)
+ ShowNext();
+ else
+ Move(PICTURE_MOVE_AMOUNT, 0);
+ break;
+
+ case ACTION_MOVE_LEFT:
+ if (m_iZoomFactor == 1 || !m_Image[m_iCurrentPic].m_bCanMoveHorizontally)
+ ShowPrevious();
+ else
+ Move( -PICTURE_MOVE_AMOUNT, 0);
+ break;
+
+ case ACTION_MOVE_DOWN:
+ Move(0, PICTURE_MOVE_AMOUNT);
+ break;
+
+ case ACTION_MOVE_UP:
+ Move(0, -PICTURE_MOVE_AMOUNT);
+ break;
+
+ case ACTION_PAUSE:
+ case ACTION_PLAYER_PLAY:
+ if (m_slides.size() == 0)
+ break;
+ if (m_slides.at(m_iCurrentSlide)->IsVideo())
+ {
+ if (!m_bPlayingVideo)
+ {
+ if (m_bSlideShow)
+ {
+ SetDirection(1);
+ m_bPause = false;
+ }
+ PlayVideo();
+ }
+ }
+ else if (!m_bSlideShow || m_bPause)
+ {
+ m_bSlideShow = true;
+ m_bPause = false;
+ SetDirection(1);
+ if (m_Image[m_iCurrentPic].IsLoaded())
+ {
+ CSlideShowPic::DISPLAY_EFFECT effect = GetDisplayEffect(m_iCurrentSlide);
+ if (m_Image[m_iCurrentPic].DisplayEffectNeedChange(effect))
+ m_Image[m_iCurrentPic].Reset(effect);
+ }
+ AnnouncePlayerPlay(m_slides.at(m_iCurrentSlide));
+ }
+ else if (action.GetID() == ACTION_PAUSE)
+ {
+ m_bPause = true;
+ AnnouncePlayerPause(m_slides.at(m_iCurrentSlide));
+ }
+ break;
+
+ case ACTION_ZOOM_OUT:
+ Zoom(m_iZoomFactor - 1);
+ break;
+
+ case ACTION_ZOOM_IN:
+ Zoom(m_iZoomFactor + 1);
+ break;
+
+ case ACTION_GESTURE_SWIPE_UP:
+ case ACTION_GESTURE_SWIPE_DOWN:
+ if (m_iZoomFactor == 1 || !m_Image[m_iCurrentPic].m_bCanMoveVertically)
+ {
+ bool swipeOnLeft = action.GetAmount() < CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth() / 2.0f;
+ bool swipeUp = action.GetID() == ACTION_GESTURE_SWIPE_UP;
+ if (swipeUp == swipeOnLeft)
+ Rotate(90.0f);
+ else
+ Rotate(-90.0f);
+ }
+ break;
+
+ case ACTION_ROTATE_PICTURE_CW:
+ Rotate(90.0f);
+ break;
+
+ case ACTION_ROTATE_PICTURE_CCW:
+ Rotate(-90.0f);
+ break;
+
+ case ACTION_ZOOM_LEVEL_NORMAL:
+ case ACTION_ZOOM_LEVEL_1:
+ case ACTION_ZOOM_LEVEL_2:
+ case ACTION_ZOOM_LEVEL_3:
+ case ACTION_ZOOM_LEVEL_4:
+ case ACTION_ZOOM_LEVEL_5:
+ case ACTION_ZOOM_LEVEL_6:
+ case ACTION_ZOOM_LEVEL_7:
+ case ACTION_ZOOM_LEVEL_8:
+ case ACTION_ZOOM_LEVEL_9:
+ Zoom((action.GetID() - ACTION_ZOOM_LEVEL_NORMAL) + 1);
+ break;
+
+ case ACTION_ANALOG_MOVE:
+ // this action is used and works, when CAction object provides both x and y coordinates
+ Move(action.GetAmount()*PICTURE_MOVE_AMOUNT_ANALOG, -action.GetAmount(1)*PICTURE_MOVE_AMOUNT_ANALOG);
+ break;
+ case ACTION_ANALOG_MOVE_X_LEFT:
+ Move(-action.GetAmount()*PICTURE_MOVE_AMOUNT_ANALOG, 0.0f);
+ break;
+ case ACTION_ANALOG_MOVE_X_RIGHT:
+ Move(action.GetAmount()*PICTURE_MOVE_AMOUNT_ANALOG, 0.0f);
+ break;
+ case ACTION_ANALOG_MOVE_Y_UP:
+ Move(0.0f, -action.GetAmount()*PICTURE_MOVE_AMOUNT_ANALOG);
+ break;
+ case ACTION_ANALOG_MOVE_Y_DOWN:
+ Move(0.0f, action.GetAmount()*PICTURE_MOVE_AMOUNT_ANALOG);
+ break;
+
+ default:
+ return CGUIDialog::OnAction(action);
+ }
+ return true;
+}
+
+void CGUIWindowSlideShow::RenderErrorMessage()
+{
+ if (!m_bErrorMessage)
+ return ;
+
+ const CGUIControl *control = GetControl(LABEL_ROW1);
+ if (NULL == control || control->GetControlType() != CGUIControl::GUICONTROL_LABEL)
+ {
+ return;
+ }
+
+ CGUIFont *pFont = static_cast<const CGUILabelControl*>(control)->GetLabelInfo().font;
+ CGUITextLayout::DrawText(pFont, 0.5f*CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), 0.5f*CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight(), 0xffffffff, 0, g_localizeStrings.Get(747), XBFONT_CENTER_X | XBFONT_CENTER_Y);
+}
+
+bool CGUIWindowSlideShow::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_Resolution = (RESOLUTION) CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_PICTURES_DISPLAYRESOLUTION);
+
+ //FIXME: Use GUI resolution for now
+ if (false /*m_Resolution != CDisplaySettings::GetInstance().GetCurrentResolution() && m_Resolution != INVALID && m_Resolution!=AUTORES*/)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(m_Resolution, false);
+ else
+ m_Resolution = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution();
+
+ CGUIDialog::OnMessage(message);
+
+ // turn off slideshow if we only have 1 image
+ if (m_slides.size() <= 1)
+ m_bSlideShow = false;
+
+ return true;
+ }
+ break;
+
+ case GUI_MSG_SHOW_PICTURE:
+ {
+ const std::string& strFile = message.GetStringParam();
+ Reset();
+ CFileItem item(strFile, false);
+ Add(&item);
+ RunSlideShow("", false, false, true, "", false);
+ }
+ break;
+
+ case GUI_MSG_START_SLIDESHOW:
+ {
+ const std::string& strFolder = message.GetStringParam();
+ unsigned int iParams = message.GetParam1();
+ const std::string& beginSlidePath = message.GetStringParam(1);
+ //decode params
+ bool bRecursive = false;
+ bool bRandom = false;
+ bool bNotRandom = false;
+ bool bPause = false;
+ if (iParams > 0)
+ {
+ if ((iParams & 1) == 1)
+ bRecursive = true;
+ if ((iParams & 2) == 2)
+ bRandom = true;
+ if ((iParams & 4) == 4)
+ bNotRandom = true;
+ if ((iParams & 8) == 8)
+ bPause = true;
+ }
+ RunSlideShow(strFolder, bRecursive, bRandom, bNotRandom, beginSlidePath, !bPause);
+ }
+ break;
+
+ case GUI_MSG_PLAYLISTPLAYER_STOPPED:
+ {
+ }
+ break;
+
+ case GUI_MSG_PLAYBACK_STOPPED:
+ {
+ if (m_bPlayingVideo)
+ {
+ m_bPlayingVideo = false;
+ m_iVideoSlide = -1;
+ if (m_bSlideShow)
+ m_bPause = true;
+ }
+ }
+ break;
+
+ case GUI_MSG_PLAYBACK_ENDED:
+ {
+ if (m_bPlayingVideo)
+ {
+ m_bPlayingVideo = false;
+ m_iVideoSlide = -1;
+ if (m_bSlideShow)
+ {
+ m_bPause = false;
+ if (m_iCurrentSlide == m_iNextSlide)
+ break;
+ m_Image[m_iCurrentPic].Close();
+ m_iCurrentPic = 1 - m_iCurrentPic;
+ m_iCurrentSlide = m_iNextSlide;
+ m_iNextSlide = GetNextSlide();
+ AnnouncePlayerPlay(m_slides.at(m_iCurrentSlide));
+ m_iZoomFactor = 1;
+ m_fZoom = 1.0f;
+ m_fRotate = 0.0f;
+ }
+ }
+ }
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIWindowSlideShow::RenderPause()
+{ // display the pause icon
+ if (m_bPause)
+ {
+ SET_CONTROL_VISIBLE(CONTROL_PAUSE);
+ }
+ else
+ {
+ SET_CONTROL_HIDDEN(CONTROL_PAUSE);
+ }
+}
+
+void CGUIWindowSlideShow::Rotate(float fAngle, bool immediate /* = false */)
+{
+ if (m_Image[m_iCurrentPic].DrawNextImage())
+ return;
+
+ m_fRotate += fAngle;
+
+ m_Image[m_iCurrentPic].Rotate(fAngle, immediate);
+}
+
+void CGUIWindowSlideShow::Zoom(int iZoom)
+{
+ if (iZoom > MAX_ZOOM_FACTOR || iZoom < 1)
+ return;
+
+ ZoomRelative(zoomamount[iZoom - 1]);
+}
+
+void CGUIWindowSlideShow::ZoomRelative(float fZoom, bool immediate /* = false */)
+{
+ if (fZoom < zoomamount[0])
+ fZoom = zoomamount[0];
+ else if (fZoom > zoomamount[MAX_ZOOM_FACTOR - 1])
+ fZoom = zoomamount[MAX_ZOOM_FACTOR - 1];
+
+ if (m_Image[m_iCurrentPic].DrawNextImage())
+ return;
+
+ m_fZoom = fZoom;
+
+ // find the nearest zoom factor
+ for (unsigned int i = 1; i < MAX_ZOOM_FACTOR; i++)
+ {
+ if (m_fZoom > zoomamount[i])
+ continue;
+
+ if (fabs(m_fZoom - zoomamount[i - 1]) < fabs(m_fZoom - zoomamount[i]))
+ m_iZoomFactor = i;
+ else
+ m_iZoomFactor = i + 1;
+
+ break;
+ }
+
+ m_Image[m_iCurrentPic].Zoom(m_fZoom, immediate);
+}
+
+void CGUIWindowSlideShow::Move(float fX, float fY)
+{
+ if (m_Image[m_iCurrentPic].IsLoaded() && m_Image[m_iCurrentPic].GetZoom() > 1)
+ { // we move in the opposite direction, due to the fact we are moving
+ // the viewing window, not the picture.
+ m_Image[m_iCurrentPic].Move( -fX, -fY);
+ }
+}
+
+bool CGUIWindowSlideShow::PlayVideo()
+{
+ CFileItemPtr item = m_slides.at(m_iCurrentSlide);
+ if (!item || !item->IsVideo())
+ return false;
+ CLog::Log(LOGDEBUG, "Playing current video slide {}", item->GetPath());
+ m_bPlayingVideo = true;
+ m_iVideoSlide = m_iCurrentSlide;
+ bool ret = g_application.PlayFile(*item, "");
+ if (ret == true)
+ return true;
+ else
+ {
+ CLog::Log(LOGINFO, "set video {} unplayable", item->GetPath());
+ item->SetProperty("unplayable", true);
+ }
+ m_bPlayingVideo = false;
+ m_iVideoSlide = -1;
+ return false;
+}
+
+CSlideShowPic::DISPLAY_EFFECT CGUIWindowSlideShow::GetDisplayEffect(int iSlideNumber) const
+{
+ if (m_bSlideShow && !m_bPause && !m_slides.at(iSlideNumber)->IsVideo())
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SLIDESHOW_DISPLAYEFFECTS) ? CSlideShowPic::EFFECT_RANDOM : CSlideShowPic::EFFECT_NONE;
+ else
+ return CSlideShowPic::EFFECT_NO_TIMEOUT;
+}
+
+void CGUIWindowSlideShow::OnLoadPic(int iPic,
+ int iSlideNumber,
+ const std::string& strFileName,
+ std::unique_ptr<CTexture> pTexture,
+ bool bFullSize)
+{
+ if (pTexture)
+ {
+ // set the pic's texture + size etc.
+ if (iSlideNumber >= static_cast<int>(m_slides.size()) || GetPicturePath(m_slides.at(iSlideNumber).get()) != strFileName)
+ { // throw this away - we must have cleared the slideshow while we were still loading
+ return;
+ }
+ CLog::Log(LOGDEBUG, "Finished background loading slot {}, {}: {}", iPic, iSlideNumber,
+ m_slides.at(iSlideNumber)->GetPath());
+ m_Image[iPic].SetOriginalSize(pTexture->GetOriginalWidth(), pTexture->GetOriginalHeight(), bFullSize);
+ m_Image[iPic].SetTexture(iSlideNumber, std::move(pTexture), GetDisplayEffect(iSlideNumber));
+
+ m_Image[iPic].m_bIsComic = false;
+ if (URIUtils::IsInRAR(m_slides.at(m_iCurrentSlide)->GetPath()) || URIUtils::IsInZIP(m_slides.at(m_iCurrentSlide)->GetPath())) // move to top for cbr/cbz
+ {
+ CURL url(m_slides.at(m_iCurrentSlide)->GetPath());
+ const std::string& strHostName = url.GetHostName();
+ if (URIUtils::HasExtension(strHostName, ".cbr|.cbz"))
+ {
+ m_Image[iPic].m_bIsComic = true;
+ m_Image[iPic].Move((float)m_Image[iPic].GetOriginalWidth(),(float)m_Image[iPic].GetOriginalHeight());
+ }
+ }
+ }
+ else if (iSlideNumber >= static_cast<int>(m_slides.size()) || GetPicturePath(m_slides.at(iSlideNumber).get()) != strFileName)
+ { // Failed to load image. and not match values calling LoadPic, then something is changed, ignore.
+ CLog::Log(LOGDEBUG,
+ "CGUIWindowSlideShow::OnLoadPic({}, {}, {}) on failure not match current state (cur "
+ "{}, next {}, curpic {}, pic[0, 1].slidenumber={}, {}, {})",
+ iPic, iSlideNumber, strFileName, m_iCurrentSlide, m_iNextSlide, m_iCurrentPic,
+ m_Image[0].SlideNumber(), m_Image[1].SlideNumber(),
+ iSlideNumber >= static_cast<int>(m_slides.size())
+ ? ""
+ : m_slides.at(iSlideNumber)->GetPath());
+ }
+ else
+ { // Failed to load image. What should be done??
+ // We should wait for the current pic to finish rendering, then transition it out,
+ // release the texture, and try and reload this pic from scratch
+ m_bErrorMessage = true;
+ }
+ MarkDirtyRegion();
+}
+
+void CGUIWindowSlideShow::Shuffle()
+{
+ KODI::UTILS::RandomShuffle(m_slides.begin(), m_slides.end());
+ m_iCurrentSlide = 0;
+ m_iNextSlide = GetNextSlide();
+ m_bShuffled = true;
+
+ AnnouncePropertyChanged("shuffled", true);
+}
+
+int CGUIWindowSlideShow::NumSlides() const
+{
+ return m_slides.size();
+}
+
+int CGUIWindowSlideShow::CurrentSlide() const
+{
+ return m_iCurrentSlide + 1;
+}
+
+void CGUIWindowSlideShow::AddFromPath(const std::string &strPath,
+ bool bRecursive,
+ SortBy method, SortOrder order, SortAttribute sortAttributes,
+ const std::string &strExtensions)
+{
+ if (strPath!="")
+ {
+ // reset the slideshow
+ Reset();
+ if (bRecursive)
+ {
+ path_set recursivePaths;
+ AddItems(strPath, &recursivePaths, method, order, sortAttributes);
+ }
+ else
+ AddItems(strPath, NULL, method, order, sortAttributes);
+ }
+}
+
+void CGUIWindowSlideShow::RunSlideShow(const std::string &strPath,
+ bool bRecursive /* = false */, bool bRandom /* = false */,
+ bool bNotRandom /* = false */, const std::string &beginSlidePath /* = "" */,
+ bool startSlideShow /* = true */, SortBy method /* = SortByLabel */,
+ SortOrder order /* = SortOrderAscending */, SortAttribute sortAttributes /* = SortAttributeNone */,
+ const std::string &strExtensions)
+{
+ // stop any video
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingVideo())
+ g_application.StopPlaying();
+
+ AddFromPath(strPath, bRecursive, method, order, sortAttributes, strExtensions);
+
+ if (!NumSlides())
+ return;
+
+ // mutually exclusive options
+ // if both are set, clear both and use the gui setting
+ if (bRandom && bNotRandom)
+ bRandom = bNotRandom = false;
+
+ // NotRandom overrides the window setting
+ if ((!bNotRandom && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SLIDESHOW_SHUFFLE)) || bRandom)
+ Shuffle();
+
+ if (!beginSlidePath.empty())
+ Select(beginSlidePath);
+
+ if (startSlideShow)
+ StartSlideShow();
+ else
+ {
+ CVariant param;
+ param["player"]["speed"] = 0;
+ param["player"]["playerid"] = PLAYLIST::TYPE_PICTURE;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPlay",
+ GetCurrentSlide(), param);
+ }
+
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SLIDESHOW);
+}
+
+void CGUIWindowSlideShow::AddItems(const std::string &strPath, path_set *recursivePaths, SortBy method, SortOrder order, SortAttribute sortAttributes)
+{
+ // check whether we've already added this path
+ if (recursivePaths)
+ {
+ std::string path(strPath);
+ URIUtils::RemoveSlashAtEnd(path);
+ if (recursivePaths->find(path) != recursivePaths->end())
+ return;
+ recursivePaths->insert(path);
+ }
+
+ CFileItemList items;
+ CGUIViewStateWindowPictures viewState(items);
+
+ // fetch directory and sort accordingly
+ if (!CDirectory::GetDirectory(strPath, items, viewState.GetExtensions(), DIR_FLAG_NO_FILE_DIRS))
+ return;
+
+ items.Sort(method, order, sortAttributes);
+
+ // need to go into all subdirs
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ if (item->m_bIsFolder && recursivePaths)
+ {
+ AddItems(item->GetPath(), recursivePaths);
+ }
+ else if (!item->m_bIsFolder && !URIUtils::IsRAR(item->GetPath()) && !URIUtils::IsZIP(item->GetPath()))
+ { // add to the slideshow
+ Add(item.get());
+ }
+ }
+}
+
+void CGUIWindowSlideShow::GetCheckedSize(float width, float height, int &maxWidth, int &maxHeight)
+{
+ maxWidth = CServiceBroker::GetRenderSystem()->GetMaxTextureSize();
+ maxHeight = CServiceBroker::GetRenderSystem()->GetMaxTextureSize();
+}
+
+std::string CGUIWindowSlideShow::GetPicturePath(CFileItem *item)
+{
+ bool isVideo = item->IsVideo();
+ std::string picturePath = item->GetDynPath();
+ if (isVideo)
+ {
+ picturePath = item->GetArt("thumb");
+ if (picturePath.empty() && !item->HasProperty("nothumb"))
+ {
+ CPictureThumbLoader thumbLoader;
+ thumbLoader.LoadItem(item);
+ picturePath = item->GetArt("thumb");
+ if (picturePath.empty())
+ item->SetProperty("nothumb", true);
+ }
+ }
+ return picturePath;
+}
+
+
+void CGUIWindowSlideShow::RunSlideShow(const std::vector<std::string>& paths, int start /* = 0*/)
+{
+ auto dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (dialog)
+ {
+ std::vector<CFileItemPtr> items;
+ items.reserve(paths.size());
+ for (const auto& path : paths)
+ items.push_back(std::make_shared<CFileItem>(CTextureUtils::GetWrappedImageURL(path), false));
+
+ dialog->Reset();
+ dialog->m_bPause = true;
+ dialog->m_bSlideShow = false;
+ dialog->m_iDirection = 1;
+ dialog->m_iCurrentSlide = start;
+ dialog->m_iNextSlide = (start + 1) % items.size();
+ dialog->m_slides = std::move(items);
+ dialog->Open();
+ }
+}
diff --git a/xbmc/pictures/GUIWindowSlideShow.h b/xbmc/pictures/GUIWindowSlideShow.h
new file mode 100644
index 0000000..6955fb4
--- /dev/null
+++ b/xbmc/pictures/GUIWindowSlideShow.h
@@ -0,0 +1,155 @@
+/*
+ * 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 "SlideShowPicture.h"
+#include "guilib/GUIDialog.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "utils/SortUtils.h"
+
+#include <memory>
+#include <set>
+
+class CFileItemList;
+class CVariant;
+
+class CGUIWindowSlideShow;
+
+class CBackgroundPicLoader : public CThread
+{
+public:
+ CBackgroundPicLoader();
+ ~CBackgroundPicLoader() override;
+
+ void Create(CGUIWindowSlideShow *pCallback);
+ void LoadPic(int iPic, int iSlideNumber, const std::string &strFileName, const int maxWidth, const int maxHeight);
+ bool IsLoading() { return m_isLoading; }
+ int SlideNumber() const { return m_iSlideNumber; }
+ int Pic() const { return m_iPic; }
+
+private:
+ void Process() override;
+ int m_iPic;
+ int m_iSlideNumber;
+ std::string m_strFileName;
+ int m_maxWidth;
+ int m_maxHeight;
+
+ CEvent m_loadPic;
+ bool m_isLoading;
+
+ CGUIWindowSlideShow *m_pCallback;
+};
+
+class CGUIWindowSlideShow : public CGUIDialog
+{
+public:
+ CGUIWindowSlideShow(void);
+ ~CGUIWindowSlideShow() override = default;
+
+ bool OnMessage(CGUIMessage& message) override;
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+ bool OnAction(const CAction &action) override;
+ void Render() override;
+ void RenderEx() override;
+ void Process(unsigned int currentTime, CDirtyRegionList &regions) override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ void Reset();
+ void Add(const CFileItem *picture);
+ bool IsPlaying() const;
+ void Select(const std::string& strPicture);
+ void GetSlideShowContents(CFileItemList &list);
+ std::shared_ptr<const CFileItem> GetCurrentSlide();
+ void RunSlideShow(const std::string &strPath, bool bRecursive = false,
+ bool bRandom = false, bool bNotRandom = false,
+ const std::string &beginSlidePath="", bool startSlideShow = true,
+ SortBy method = SortByLabel,
+ SortOrder order = SortOrderAscending,
+ SortAttribute sortAttributes = SortAttributeNone,
+ const std::string &strExtensions="");
+ void AddFromPath(const std::string &strPath, bool bRecursive,
+ SortBy method = SortByLabel,
+ SortOrder order = SortOrderAscending,
+ SortAttribute sortAttributes = SortAttributeNone,
+ const std::string &strExtensions="");
+ void StartSlideShow();
+ bool InSlideShow() const;
+ void OnLoadPic(int iPic,
+ int iSlideNumber,
+ const std::string& strFileName,
+ std::unique_ptr<CTexture> pTexture,
+ bool bFullSize);
+ int NumSlides() const;
+ int CurrentSlide() const;
+ void Shuffle();
+ bool IsPaused() const { return m_bPause; }
+ bool IsShuffled() const { return m_bShuffled; }
+ int GetDirection() const { return m_iDirection; }
+
+ static void RunSlideShow(const std::vector<std::string>& paths, int start = 0);
+
+private:
+ void ShowNext();
+ void ShowPrevious();
+ void SetDirection(int direction); // -1: rewind, 1: forward
+
+ typedef std::set<std::string> path_set; // set to track which paths we're adding
+ void AddItems(const std::string &strPath, path_set *recursivePaths,
+ SortBy method = SortByLabel,
+ SortOrder order = SortOrderAscending,
+ SortAttribute sortAttributes = SortAttributeNone);
+ bool PlayVideo();
+ CSlideShowPic::DISPLAY_EFFECT GetDisplayEffect(int iSlideNumber) const;
+ void RenderPause();
+ void RenderErrorMessage();
+ void Rotate(float fAngle, bool immediate = false);
+ void Zoom(int iZoom);
+ void ZoomRelative(float fZoom, bool immediate = false);
+ void Move(float fX, float fY);
+ void GetCheckedSize(float width, float height, int &maxWidth, int &maxHeight);
+ std::string GetPicturePath(CFileItem *item);
+ int GetNextSlide();
+
+ void AnnouncePlayerPlay(const CFileItemPtr& item);
+ void AnnouncePlayerPause(const CFileItemPtr& item);
+ void AnnouncePlayerStop(const CFileItemPtr& item);
+ void AnnouncePlaylistClear();
+ void AnnouncePlaylistAdd(const CFileItemPtr& item, int pos);
+ void AnnouncePropertyChanged(const std::string &strProperty, const CVariant &value);
+
+ int m_iCurrentSlide;
+ int m_iNextSlide;
+ int m_iDirection;
+ float m_fRotate;
+ float m_fInitialRotate;
+ int m_iZoomFactor;
+ float m_fZoom;
+ float m_fInitialZoom;
+
+ bool m_bShuffled;
+ bool m_bSlideShow;
+ bool m_bPause;
+ bool m_bPlayingVideo;
+ int m_iVideoSlide = -1;
+ bool m_bErrorMessage;
+
+ std::vector<CFileItemPtr> m_slides;
+
+ CSlideShowPic m_Image[2];
+
+ int m_iCurrentPic;
+ // background loader
+ std::unique_ptr<CBackgroundPicLoader> m_pBackgroundLoader;
+ int m_iLastFailedNextSlide;
+ bool m_bLoadNextPic;
+ RESOLUTION m_Resolution;
+ CPoint m_firstGesturePoint;
+};
diff --git a/xbmc/pictures/IptcParse.cpp b/xbmc/pictures/IptcParse.cpp
new file mode 100644
index 0000000..dc66f27
--- /dev/null
+++ b/xbmc/pictures/IptcParse.cpp
@@ -0,0 +1,212 @@
+/*
+ * 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.
+ */
+
+//--------------------------------------------------------------------------
+// Module to pull IPTC information out of various types of digital images.
+//--------------------------------------------------------------------------
+
+//--------------------------------------------------------------------------
+// Process IPTC data.
+//--------------------------------------------------------------------------
+
+#include "IptcParse.h"
+
+#include "ExifParse.h"
+
+#ifdef TARGET_WINDOWS
+#include <windows.h>
+#else
+#include <string.h>
+#endif
+
+#include <stdio.h>
+
+#ifndef min
+#define min(a,b) (a)>(b)?(b):(a)
+#endif
+
+// Supported IPTC entry types
+#define IPTC_RECORD_VERSION 0x00
+#define IPTC_SUPLEMENTAL_CATEGORIES 0x14
+#define IPTC_KEYWORDS 0x19
+#define IPTC_CAPTION 0x78
+#define IPTC_AUTHOR 0x7A
+#define IPTC_HEADLINE 0x69
+#define IPTC_SPECIAL_INSTRUCTIONS 0x28
+#define IPTC_CATEGORY 0x0F
+#define IPTC_BYLINE 0x50
+#define IPTC_BYLINE_TITLE 0x55
+#define IPTC_CREDIT 0x6E
+#define IPTC_SOURCE 0x73
+#define IPTC_COPYRIGHT_NOTICE 0x74
+#define IPTC_OBJECT_NAME 0x05
+#define IPTC_CITY 0x5A
+#define IPTC_STATE 0x5F
+#define IPTC_COUNTRY 0x65
+#define IPTC_TRANSMISSION_REFERENCE 0x67
+#define IPTC_DATE 0x37
+#define IPTC_URGENCY 0x0A
+#define IPTC_COUNTRY_CODE 0x64
+#define IPTC_REFERENCE_SERVICE 0x2D
+#define IPTC_TIME_CREATED 0x3C
+#define IPTC_SUB_LOCATION 0x5C
+#define IPTC_IMAGE_TYPE 0x82
+
+
+//--------------------------------------------------------------------------
+// Process IPTC marker. Return FALSE if unable to process marker.
+//
+// IPTC block consists of:
+// - Marker: 1 byte (0xED)
+// - Block length: 2 bytes
+// - IPTC Signature: 14 bytes ("Photoshop 3.0\0")
+// - 8BIM Signature 4 bytes ("8BIM")
+// - IPTC Block start 2 bytes (0x04, 0x04)
+// - IPTC Header length 1 byte
+// - IPTC header Header is padded to even length, counting the length byte
+// - Length 4 bytes
+// - IPTC Data which consists of a number of entries, each of which has the following format:
+// - Signature 2 bytes (0x1C02)
+// - Entry type 1 byte (for defined entry types, see #defines above)
+// - entry length 2 bytes
+// - entry data 'entry length' bytes
+//
+//--------------------------------------------------------------------------
+bool CIptcParse::Process (const unsigned char* const Data, const unsigned short itemlen, IPTCInfo_t *info)
+{
+ if (!info) return false;
+
+ const char IptcSignature1[] = "Photoshop 3.0";
+ const char IptcSignature2[] = "8BIM";
+ const char IptcSignature3[] = {0x04, 0x04};
+
+ // Check IPTC signatures
+ const char* pos = (const char*)(Data + sizeof(short)); // position data pointer after length field
+ const char* maxpos = (const char*)(Data+itemlen);
+ unsigned char headerLen = 0;
+ unsigned char dataLen = 0;
+ memset(info, 0, sizeof(IPTCInfo_t));
+
+ if (itemlen < 25) return false;
+
+ if (memcmp(pos, IptcSignature1, strlen(IptcSignature1)-1) != 0) return false;
+ pos += sizeof(IptcSignature1); // move data pointer to the next field
+
+ if (memcmp(pos, IptcSignature2, strlen(IptcSignature2)-1) != 0) return false;
+ pos += sizeof(IptcSignature2)-1; // move data pointer to the next field
+
+ while (memcmp(pos, IptcSignature3, sizeof(IptcSignature3)) != 0) { // loop on valid Photoshop blocks
+
+ pos += sizeof(IptcSignature3); // move data pointer to the Header Length
+ // Skip header
+ headerLen = *pos; // get header length and move data pointer to the next field
+ pos += (headerLen & 0xfe) + 2; // move data pointer to the next field (Header is padded to even length, counting the length byte)
+
+ pos += 3; // move data pointer to length, assume only one byte, TODO: use all 4 bytes
+
+ dataLen = *pos++;
+ pos += dataLen; // skip data section
+
+ if (memcmp(pos, IptcSignature2, sizeof(IptcSignature2) - 1) != 0) return false;
+ pos += sizeof(IptcSignature2) - 1; // move data pointer to the next field
+ }
+
+ pos += sizeof(IptcSignature3); // move data pointer to the next field
+ if (pos >= maxpos) return false;
+
+ // IPTC section found
+
+ // Skip header
+ headerLen = *pos++; // get header length and move data pointer to the next field
+ pos += headerLen + 1 - (headerLen % 2); // move data pointer to the next field (Header is padded to even length, counting the length byte)
+
+ if (pos + 4 >= maxpos) return false;
+
+ pos += 4; // move data pointer to the next field
+
+ // Now read IPTC data
+ while (pos < (const char*)(Data + itemlen-5))
+ {
+ if (pos + 5 > maxpos) return false;
+
+ short signature = (*pos << 8) + (*(pos+1));
+
+ pos += 2;
+ if (signature != 0x1C01 && signature != 0x1C02)
+ break;
+
+ unsigned char type = *pos++;
+ unsigned short length = (*pos << 8) + (*(pos+1));
+ pos += 2; // Skip tag length
+
+ if (pos + length > maxpos) return false;
+
+ // Process tag here
+ char *tag = NULL;
+ if (signature == 0x1C02)
+ {
+ switch (type)
+ {
+ case IPTC_RECORD_VERSION: tag = info->RecordVersion; break;
+ case IPTC_SUPLEMENTAL_CATEGORIES: tag = info->SupplementalCategories; break;
+ case IPTC_KEYWORDS: tag = info->Keywords; break;
+ case IPTC_CAPTION: tag = info->Caption; break;
+ case IPTC_AUTHOR: tag = info->Author; break;
+ case IPTC_HEADLINE: tag = info->Headline; break;
+ case IPTC_SPECIAL_INSTRUCTIONS: tag = info->SpecialInstructions; break;
+ case IPTC_CATEGORY: tag = info->Category; break;
+ case IPTC_BYLINE: tag = info->Byline; break;
+ case IPTC_BYLINE_TITLE: tag = info->BylineTitle; break;
+ case IPTC_CREDIT: tag = info->Credit; break;
+ case IPTC_SOURCE: tag = info->Source; break;
+ case IPTC_COPYRIGHT_NOTICE: tag = info->CopyrightNotice; break;
+ case IPTC_OBJECT_NAME: tag = info->ObjectName; break;
+ case IPTC_CITY: tag = info->City; break;
+ case IPTC_STATE: tag = info->State; break;
+ case IPTC_COUNTRY: tag = info->Country; break;
+ case IPTC_TRANSMISSION_REFERENCE: tag = info->TransmissionReference; break;
+ case IPTC_DATE: tag = info->Date; break;
+ case IPTC_URGENCY: tag = info->Urgency; break;
+ case IPTC_REFERENCE_SERVICE: tag = info->ReferenceService; break;
+ case IPTC_COUNTRY_CODE: tag = info->CountryCode; break;
+ case IPTC_TIME_CREATED: tag = info->TimeCreated; break;
+ case IPTC_SUB_LOCATION: tag = info->SubLocation; break;
+ case IPTC_IMAGE_TYPE: tag = info->ImageType; break;
+ default:
+ printf("IptcParse: Unrecognised IPTC tag: 0x%02x", type);
+ break;
+ }
+ }
+
+ if (tag)
+ {
+ if (type != IPTC_KEYWORDS || *tag == 0)
+ {
+ strncpy(tag, pos, min(length, MAX_IPTC_STRING - 1));
+ tag[min(length, MAX_IPTC_STRING - 1)] = 0;
+ }
+ else if (type == IPTC_KEYWORDS)
+ {
+ // there may be multiple keywords - lets join them
+ size_t maxLen = MAX_IPTC_STRING - strlen(tag);
+ if (maxLen > 2)
+ {
+ strcat(tag, ", ");
+ strncat(tag, pos, min(length, maxLen - 3));
+ }
+ }
+/* if (id == SLIDESHOW_IPTC_CAPTION)
+ {
+ CExifParse::FixComment(m_IptcInfo[id]); // Ensure comment is printable
+ }*/
+ }
+ pos += length;
+ }
+ return true;
+}
+
diff --git a/xbmc/pictures/IptcParse.h b/xbmc/pictures/IptcParse.h
new file mode 100644
index 0000000..ba00135
--- /dev/null
+++ b/xbmc/pictures/IptcParse.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "libexif.h"
+
+class CIptcParse
+{
+ public:
+ static bool Process(const unsigned char* const Data, const unsigned short length, IPTCInfo_t *info);
+};
+
diff --git a/xbmc/pictures/JpegParse.cpp b/xbmc/pictures/JpegParse.cpp
new file mode 100644
index 0000000..718ac54
--- /dev/null
+++ b/xbmc/pictures/JpegParse.cpp
@@ -0,0 +1,316 @@
+/*
+ * 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.
+ */
+
+//--------------------------------------------------------------------------
+// This module gathers information about a digital image file. This includes:
+// - File name and path
+// - File size
+// - Resolution (if available)
+// - IPTC information (if available)
+// - EXIF information (if available)
+// All gathered information is stored in a vector of 'description' and 'value'
+// pairs (where both description and value fields are of CStdString types).
+//--------------------------------------------------------------------------
+
+#include "JpegParse.h"
+
+#include "filesystem/File.h"
+
+#ifdef TARGET_WINDOWS
+#include <windows.h>
+#else
+#include <memory.h>
+#include <cstring>
+typedef unsigned char BYTE;
+#endif
+
+#ifndef min
+#define min(a,b) (a)>(b)?(b):(a)
+#endif
+
+using namespace XFILE;
+
+//--------------------------------------------------------------------------
+#define JPEG_PARSE_STRING_ID_BASE 21500
+enum {
+ ProcessUnknown = JPEG_PARSE_STRING_ID_BASE,
+ ProcessSof0,
+ ProcessSof1,
+ ProcessSof2,
+ ProcessSof3,
+ ProcessSof5,
+ ProcessSof6,
+ ProcessSof7,
+ ProcessSof9,
+ ProcessSof10,
+ ProcessSof11,
+ ProcessSof13,
+ ProcessSof14,
+ ProcessSof15,
+};
+
+
+
+
+//--------------------------------------------------------------------------
+// Constructor
+//--------------------------------------------------------------------------
+CJpegParse::CJpegParse():
+ m_SectionBuffer(NULL)
+{
+ memset(&m_ExifInfo, 0, sizeof(m_ExifInfo));
+ memset(&m_IPTCInfo, 0, sizeof(m_IPTCInfo));
+}
+
+//--------------------------------------------------------------------------
+// Process a SOFn marker. This is useful for the image dimensions
+//--------------------------------------------------------------------------
+void CJpegParse::ProcessSOFn (void)
+{
+ m_ExifInfo.Height = CExifParse::Get16(m_SectionBuffer+3);
+ m_ExifInfo.Width = CExifParse::Get16(m_SectionBuffer+5);
+
+ unsigned char num_components = m_SectionBuffer[7];
+ if (num_components != 3)
+ {
+ m_ExifInfo.IsColor = 0;
+ }
+ else
+ {
+ m_ExifInfo.IsColor = 1;
+ }
+}
+
+
+//--------------------------------------------------------------------------
+// Read a section from a JPEG file. Note that this function allocates memory.
+// It must be called in pair with ReleaseSection
+//--------------------------------------------------------------------------
+bool CJpegParse::GetSection (CFile& infile, const unsigned short sectionLength)
+{
+ if (sectionLength < 2)
+ {
+ printf("JpgParse: invalid section length");
+ return false;
+ }
+
+ m_SectionBuffer = new unsigned char[sectionLength];
+ if (m_SectionBuffer == NULL)
+ {
+ printf("JpgParse: could not allocate memory");
+ return false;
+ }
+ // Store first two pre-read bytes.
+ m_SectionBuffer[0] = (unsigned char)(sectionLength >> 8);
+ m_SectionBuffer[1] = (unsigned char)(sectionLength & 0x00FF);
+
+ unsigned int len = (unsigned int)sectionLength;
+
+ size_t bytesRead = infile.Read(m_SectionBuffer+sizeof(sectionLength), len-sizeof(sectionLength));
+ if (bytesRead != sectionLength-sizeof(sectionLength))
+ {
+ printf("JpgParse: premature end of file?");
+ ReleaseSection();
+ return false;
+ }
+ return true;
+}
+
+//--------------------------------------------------------------------------
+// Deallocate memory allocated in GetSection. This function must always
+// be paired by a preceding GetSection call.
+//--------------------------------------------------------------------------
+void CJpegParse::ReleaseSection (void)
+{
+ delete[] m_SectionBuffer;
+ m_SectionBuffer = NULL;
+}
+
+//--------------------------------------------------------------------------
+// Parse the marker stream until SOS or EOI is seen; infile has already been
+// successfully open
+//--------------------------------------------------------------------------
+bool CJpegParse::ExtractInfo (CFile& infile)
+{
+ // Get file marker (two bytes - must be 0xFFD8 for JPEG files
+ BYTE a;
+ size_t bytesRead = infile.Read(&a, sizeof(BYTE));
+ if ((bytesRead != sizeof(BYTE)) || (a != 0xFF))
+ {
+ return false;
+ }
+ bytesRead = infile.Read(&a, sizeof(BYTE));
+ if ((bytesRead != sizeof(BYTE)) || (a != M_SOI))
+ {
+ return false;
+ }
+
+ for(;;)
+ {
+ BYTE marker = 0;
+ for (a=0; a<7; a++) {
+ bytesRead = infile.Read(&marker, sizeof(BYTE));
+ if (marker != 0xFF)
+ break;
+
+ if (a >= 6)
+ {
+ printf("JpgParse: too many padding bytes");
+ return false;
+ }
+ marker = 0;
+ }
+
+ // Read the length of the section.
+ unsigned short itemlen = 0;
+ bytesRead = infile.Read(&itemlen, sizeof(itemlen));
+ itemlen = CExifParse::Get16(&itemlen);
+
+ if ((bytesRead != sizeof(itemlen)) || (itemlen < sizeof(itemlen)))
+ {
+ printf("JpgParse: invalid marker");
+ return false;
+ }
+
+ switch(marker)
+ {
+ case M_SOS: // stop before hitting compressed data
+ return true;
+
+ case M_EOI: // in case it's a tables-only JPEG stream
+ printf("JpgParse: No image in jpeg!");
+ return false;
+ break;
+
+ case M_COM: // Comment section
+ GetSection(infile, itemlen);
+ if (m_SectionBuffer != NULL)
+ {
+ // CExifParse::FixComment(comment); // Ensure comment is printable
+ unsigned short length = min(itemlen - 2, MAX_COMMENT);
+ strncpy(m_ExifInfo.FileComment, (char *)&m_SectionBuffer[2], length);
+ m_ExifInfo.FileComment[length] = '\0';
+ }
+ ReleaseSection();
+ break;
+
+ case M_SOF0:
+ case M_SOF1:
+ case M_SOF2:
+ case M_SOF3:
+ case M_SOF5:
+ case M_SOF6:
+ case M_SOF7:
+ case M_SOF9:
+ case M_SOF10:
+ case M_SOF11:
+ case M_SOF13:
+ case M_SOF14:
+ case M_SOF15:
+ GetSection(infile, itemlen);
+ if ((m_SectionBuffer != NULL) && (itemlen >= 7))
+ {
+ ProcessSOFn();
+ m_ExifInfo.Process = marker;
+ }
+ ReleaseSection();
+ break;
+
+ case M_IPTC:
+ GetSection(infile, itemlen);
+ if (m_SectionBuffer != NULL)
+ {
+ CIptcParse::Process(m_SectionBuffer, itemlen, &m_IPTCInfo);
+ }
+ ReleaseSection();
+ break;
+
+ case M_EXIF:
+ // Seen files from some 'U-lead' software with Vivitar scanner
+ // that uses marker 31 for non exif stuff. Thus make sure
+ // it says 'Exif' in the section before treating it as exif.
+ GetSection(infile, itemlen);
+ if (m_SectionBuffer != NULL)
+ {
+ CExifParse exif;
+ exif.Process(m_SectionBuffer, itemlen, &m_ExifInfo);
+ }
+ ReleaseSection();
+ break;
+
+ case M_JFIF:
+ // Regular jpegs always have this tag, exif images have the exif
+ // marker instead, although ACDsee will write images with both markers.
+ // this program will re-create this marker on absence of exif marker.
+ // hence no need to keep the copy from the file.
+ // fall through to default case
+ default:
+ // Skip any other sections.
+ GetSection(infile, itemlen);
+ ReleaseSection();
+ break;
+ }
+ }
+ return true;
+}
+
+//--------------------------------------------------------------------------
+// Process a file. Check if it is JPEG. Extract exif/iptc info if it is.
+//--------------------------------------------------------------------------
+bool CJpegParse::Process (const char *picFileName)
+{
+ CFile file;
+
+ if (!file.Open(picFileName))
+ return false;
+
+ // File exists and successfully opened. Start processing
+ // Gather all information about the file
+
+/* // Get file name...
+ CStdString tmp, urlFName, path;
+ CURL url(picFileName);
+ url.GetURLWithoutUserDetails(urlFName);
+ CUtil::Split(urlFName, path, tmp);
+ m_JpegInfo[SLIDESHOW_FILE_NAME] = tmp;
+ // ...then path...
+ m_JpegInfo[SLIDESHOW_FILE_PATH] = path;
+
+ // ...then size...
+ __stat64 fileStat;
+ CFile::Stat(picFileName, &fileStat);
+ float fileSize = (float)fileStat.st_size;
+ tmp = "";
+ if (fileSize > 1024)
+ {
+ fileSize /= 1024;
+ tmp = "KB";
+ }
+ if (fileSize > 1024)
+ {
+ fileSize /= 1024;
+ tmp = "MB";
+ }
+ if (fileSize > 1024)
+ {
+ fileSize /= 1024;
+ tmp = "GB";
+ }
+ tmp.Format("%.2f %s", fileSize, tmp);
+ m_JpegInfo[SLIDESHOW_FILE_SIZE] = tmp;
+
+ // ...then date and time...
+ CDateTime date((time_t)fileStat.st_mtime);
+ tmp.Format("%s %s", date.GetAsLocalizedDate(), date.GetAsLocalizedTime());
+ m_JpegInfo[SLIDESHOW_FILE_DATE] = tmp;*/
+
+ bool result = ExtractInfo(file);
+ file.Close();
+ return result;
+}
+
diff --git a/xbmc/pictures/JpegParse.h b/xbmc/pictures/JpegParse.h
new file mode 100644
index 0000000..11d2ca6
--- /dev/null
+++ b/xbmc/pictures/JpegParse.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include "ExifParse.h"
+#include "IptcParse.h"
+
+#include <stdio.h>
+
+//--------------------------------------------------------------------------
+// JPEG markers consist of one or more 0xFF bytes, followed by a marker
+// code byte (which is not an FF). Here are the marker codes of interest
+// in this application.
+//--------------------------------------------------------------------------
+
+#define M_SOF0 0xC0 // Start Of Frame N
+#define M_SOF1 0xC1 // N indicates which compression process
+#define M_SOF2 0xC2 // Only SOF0-SOF2 are now in common use
+#define M_SOF3 0xC3
+#define M_SOF5 0xC5 // NB: codes C4 and CC are NOT SOF markers
+#define M_SOF6 0xC6
+#define M_SOF7 0xC7
+#define M_SOF9 0xC9
+#define M_SOF10 0xCA
+#define M_SOF11 0xCB
+#define M_SOF13 0xCD
+#define M_SOF14 0xCE
+#define M_SOF15 0xCF
+#define M_SOI 0xD8 // Start Of Image (beginning of datastream)
+#define M_EOI 0xD9 // End Of Image (end of datastream)
+#define M_SOS 0xDA // Start Of Scan (begins compressed data)
+#define M_JFIF 0xE0 // Jfif marker
+#define M_EXIF 0xE1 // Exif marker
+#define M_COM 0xFE // COMment
+#define M_DQT 0xDB
+#define M_DHT 0xC4
+#define M_DRI 0xDD
+#define M_IPTC 0xED // IPTC marker
+
+namespace XFILE
+{
+ class CFile;
+}
+
+
+class CJpegParse
+{
+ public:
+ CJpegParse();
+ ~CJpegParse(void) = default;
+ bool Process(const char *picFileName);
+ const ExifInfo_t* GetExifInfo() const { return &m_ExifInfo; }
+ const IPTCInfo_t* GetIptcInfo() const { return &m_IPTCInfo; }
+
+ private:
+ bool ExtractInfo(XFILE::CFile& infile);
+ bool GetSection(XFILE::CFile& infile, const unsigned short sectionLength);
+ void ReleaseSection(void);
+ void ProcessSOFn(void);
+
+ unsigned char* m_SectionBuffer;
+ ExifInfo_t m_ExifInfo;
+ IPTCInfo_t m_IPTCInfo;
+};
+
diff --git a/xbmc/pictures/Picture.cpp b/xbmc/pictures/Picture.cpp
new file mode 100644
index 0000000..9c7ec1e
--- /dev/null
+++ b/xbmc/pictures/Picture.cpp
@@ -0,0 +1,588 @@
+/*
+ * 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 <algorithm>
+
+#include "Picture.h"
+#include "URL.h"
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "FileItem.h"
+#include "filesystem/File.h"
+#include "utils/log.h"
+#include "utils/URIUtils.h"
+#include "guilib/Texture.h"
+#include "guilib/imagefactory.h"
+
+extern "C" {
+#include <libswscale/swscale.h>
+}
+
+using namespace XFILE;
+
+bool CPicture::GetThumbnailFromSurface(const unsigned char* buffer, int width, int height, int stride, const std::string &thumbFile, uint8_t* &result, size_t& result_size)
+{
+ unsigned char *thumb = NULL;
+ unsigned int thumbsize = 0;
+
+ // get an image handler
+ IImage* image = ImageFactory::CreateLoader(thumbFile);
+ if (image == NULL ||
+ !image->CreateThumbnailFromSurface(const_cast<unsigned char*>(buffer), width, height,
+ XB_FMT_A8R8G8B8, stride, thumbFile, thumb, thumbsize))
+ {
+ delete image;
+ return false;
+ }
+
+ // copy the resulting buffer
+ result_size = thumbsize;
+ result = new uint8_t[result_size];
+ memcpy(result, thumb, result_size);
+
+ // release the image buffer and the image handler
+ image->ReleaseThumbnailBuffer();
+ delete image;
+
+ return true;
+}
+
+bool CPicture::CreateThumbnailFromSurface(const unsigned char *buffer, int width, int height, int stride, const std::string &thumbFile)
+{
+ CLog::Log(LOGDEBUG, "cached image '{}' size {}x{}", CURL::GetRedacted(thumbFile), width, height);
+
+ unsigned char *thumb = NULL;
+ unsigned int thumbsize=0;
+ IImage* pImage = ImageFactory::CreateLoader(thumbFile);
+ if(pImage == NULL || !pImage->CreateThumbnailFromSurface(const_cast<unsigned char*>(buffer), width, height, XB_FMT_A8R8G8B8, stride, thumbFile.c_str(), thumb, thumbsize))
+ {
+ CLog::Log(LOGERROR, "Failed to CreateThumbnailFromSurface for {}",
+ CURL::GetRedacted(thumbFile));
+ delete pImage;
+ return false;
+ }
+
+ XFILE::CFile file;
+ const bool ret = file.OpenForWrite(thumbFile, true) &&
+ file.Write(thumb, thumbsize) == static_cast<ssize_t>(thumbsize);
+
+ pImage->ReleaseThumbnailBuffer();
+ delete pImage;
+
+ return ret;
+}
+
+CThumbnailWriter::CThumbnailWriter(unsigned char* buffer, int width, int height, int stride, const std::string& thumbFile):
+ m_thumbFile(thumbFile)
+{
+ m_buffer = buffer;
+ m_width = width;
+ m_height = height;
+ m_stride = stride;
+}
+
+CThumbnailWriter::~CThumbnailWriter()
+{
+ delete m_buffer;
+}
+
+bool CThumbnailWriter::DoWork()
+{
+ bool success = true;
+
+ if (!CPicture::CreateThumbnailFromSurface(m_buffer, m_width, m_height, m_stride, m_thumbFile))
+ {
+ CLog::Log(LOGERROR, "CThumbnailWriter::DoWork unable to write {}",
+ CURL::GetRedacted(m_thumbFile));
+ success = false;
+ }
+
+ delete [] m_buffer;
+ m_buffer = NULL;
+
+ return success;
+}
+
+bool CPicture::ResizeTexture(const std::string& image,
+ CTexture* texture,
+ uint32_t& dest_width,
+ uint32_t& dest_height,
+ uint8_t*& result,
+ size_t& result_size,
+ CPictureScalingAlgorithm::Algorithm
+ scalingAlgorithm /* = CPictureScalingAlgorithm::NoAlgorithm */)
+{
+ if (image.empty() || texture == NULL)
+ return false;
+
+ return ResizeTexture(image, texture->GetPixels(), texture->GetWidth(), texture->GetHeight(), texture->GetPitch(),
+ dest_width, dest_height, result, result_size,
+ scalingAlgorithm);
+}
+
+bool CPicture::ResizeTexture(const std::string &image, uint8_t *pixels, uint32_t width, uint32_t height, uint32_t pitch,
+ uint32_t &dest_width, uint32_t &dest_height, uint8_t* &result, size_t& result_size,
+ CPictureScalingAlgorithm::Algorithm scalingAlgorithm /* = CPictureScalingAlgorithm::NoAlgorithm */)
+{
+ if (image.empty() || pixels == NULL)
+ return false;
+
+ dest_width = std::min(width, dest_width);
+ dest_height = std::min(height, dest_height);
+
+ // if no max width or height is specified, don't resize
+ if (dest_width == 0 && dest_height == 0)
+ {
+ dest_width = width;
+ dest_height = height;
+ }
+ else if (dest_width == 0)
+ {
+ double factor = (double)dest_height / (double)height;
+ dest_width = (uint32_t)(width * factor);
+ }
+ else if (dest_height == 0)
+ {
+ double factor = (double)dest_width / (double)width;
+ dest_height = (uint32_t)(height * factor);
+ }
+
+ // nothing special to do if the dimensions already match
+ if (dest_width >= width || dest_height >= height)
+ return GetThumbnailFromSurface(pixels, dest_width, dest_height, pitch, image, result, result_size);
+
+ // create a buffer large enough for the resulting image
+ GetScale(width, height, dest_width, dest_height);
+
+ // Let's align so that stride is always divisible by 16, and then add some 32 bytes more on top
+ // See: https://github.com/FFmpeg/FFmpeg/blob/75638fe9402f70645bdde4d95672fa640a327300/libswscale/tests/swscale.c#L157
+ uint32_t dest_width_aligned = ((dest_width + 15) & ~0x0f);
+ uint32_t stride = dest_width_aligned * sizeof(uint32_t);
+
+ uint32_t* buffer = new uint32_t[dest_width_aligned * dest_height + 4];
+ if (buffer == NULL)
+ {
+ result = NULL;
+ result_size = 0;
+ return false;
+ }
+
+ if (!ScaleImage(pixels, width, height, pitch, AV_PIX_FMT_BGRA, (uint8_t*)buffer, dest_width,
+ dest_height, stride, AV_PIX_FMT_BGRA, scalingAlgorithm))
+ {
+ delete[] buffer;
+ result = NULL;
+ result_size = 0;
+ return false;
+ }
+
+ bool success = GetThumbnailFromSurface((unsigned char*)buffer, dest_width, dest_height, stride,
+ image, result, result_size);
+ delete[] buffer;
+
+ if (!success)
+ {
+ result = NULL;
+ result_size = 0;
+ }
+
+ return success;
+}
+
+bool CPicture::CacheTexture(CTexture* texture,
+ uint32_t& dest_width,
+ uint32_t& dest_height,
+ const std::string& dest,
+ CPictureScalingAlgorithm::Algorithm
+ scalingAlgorithm /* = CPictureScalingAlgorithm::NoAlgorithm */)
+{
+ return CacheTexture(texture->GetPixels(), texture->GetWidth(), texture->GetHeight(), texture->GetPitch(),
+ texture->GetOrientation(), dest_width, dest_height, dest, scalingAlgorithm);
+}
+
+bool CPicture::CacheTexture(uint8_t *pixels, uint32_t width, uint32_t height, uint32_t pitch, int orientation,
+ uint32_t &dest_width, uint32_t &dest_height, const std::string &dest,
+ CPictureScalingAlgorithm::Algorithm scalingAlgorithm /* = CPictureScalingAlgorithm::NoAlgorithm */)
+{
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ // if no max width or height is specified, don't resize
+ if (dest_width == 0)
+ dest_width = width;
+ if (dest_height == 0)
+ dest_height = height;
+ if (scalingAlgorithm == CPictureScalingAlgorithm::NoAlgorithm)
+ scalingAlgorithm = advancedSettings->m_imageScalingAlgorithm;
+
+ uint32_t max_height = advancedSettings->m_imageRes;
+ if (advancedSettings->m_fanartRes > advancedSettings->m_imageRes)
+ { // 16x9 images larger than the fanart res use that rather than the image res
+ if (fabsf(static_cast<float>(width) / static_cast<float>(height) / (16.0f / 9.0f) - 1.0f)
+ <= 0.01f)
+ {
+ max_height = advancedSettings->m_fanartRes; // use height defined in fanartRes
+ }
+ }
+
+ uint32_t max_width = max_height * 16/9;
+
+ dest_height = std::min(dest_height, max_height);
+ dest_width = std::min(dest_width, max_width);
+
+ if (width > dest_width || height > dest_height || orientation)
+ {
+ bool success = false;
+
+ dest_width = std::min(width, dest_width);
+ dest_height = std::min(height, dest_height);
+
+ // create a buffer large enough for the resulting image
+ GetScale(width, height, dest_width, dest_height);
+
+ // Let's align so that stride is always divisible by 16, and then add some 32 bytes more on top
+ // See: https://github.com/FFmpeg/FFmpeg/blob/75638fe9402f70645bdde4d95672fa640a327300/libswscale/tests/swscale.c#L157
+ uint32_t dest_width_aligned = ((dest_width + 15) & ~0x0f);
+ uint32_t stride = dest_width_aligned * sizeof(uint32_t);
+
+ uint32_t* buffer = new uint32_t[dest_width_aligned * dest_height + 4];
+ if (buffer)
+ {
+ if (ScaleImage(pixels, width, height, pitch, AV_PIX_FMT_BGRA, (uint8_t*)buffer, dest_width,
+ dest_height, stride, AV_PIX_FMT_BGRA, scalingAlgorithm))
+ {
+ if (!orientation ||
+ OrientateImage(buffer, dest_width, dest_height, orientation, dest_width_aligned))
+ {
+ success = CreateThumbnailFromSurface((unsigned char*)buffer, dest_width, dest_height,
+ dest_width_aligned * 4, dest);
+ }
+ }
+ delete[] buffer;
+ }
+ return success;
+ }
+ else
+ { // no orientation needed
+ dest_width = width;
+ dest_height = height;
+ return CreateThumbnailFromSurface(pixels, width, height, pitch, dest);
+ }
+ return false;
+}
+
+bool CPicture::CreateTiledThumb(const std::vector<std::string> &files, const std::string &thumb)
+{
+ if (!files.size())
+ return false;
+
+ unsigned int num_across = (unsigned int)ceil(sqrt((float)files.size()));
+ unsigned int num_down = (files.size() + num_across - 1) / num_across;
+
+ unsigned int imageRes = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes;
+
+ unsigned int tile_width = imageRes / num_across;
+ unsigned int tile_height = imageRes / num_down;
+ unsigned int tile_gap = 1;
+ bool success = false;
+
+ // create a buffer for the resulting thumb
+ uint32_t *buffer = static_cast<uint32_t *>(calloc(imageRes * imageRes, 4));
+ if (!buffer)
+ return false;
+ for (unsigned int i = 0; i < files.size(); ++i)
+ {
+ int x = i % num_across;
+ int y = i / num_across;
+ // load in the image
+ unsigned int width = tile_width - 2*tile_gap, height = tile_height - 2*tile_gap;
+ std::unique_ptr<CTexture> texture = CTexture::LoadFromFile(files[i], width, height, true);
+ if (texture && texture->GetWidth() && texture->GetHeight())
+ {
+ GetScale(texture->GetWidth(), texture->GetHeight(), width, height);
+
+ // scale appropriately
+ uint32_t *scaled = new uint32_t[width * height];
+ if (ScaleImage(texture->GetPixels(), texture->GetWidth(), texture->GetHeight(),
+ texture->GetPitch(), AV_PIX_FMT_BGRA, (uint8_t*)scaled, width, height,
+ width * 4, AV_PIX_FMT_BGRA))
+ {
+ unsigned int stridePixels{width};
+ if (!texture->GetOrientation() ||
+ OrientateImage(scaled, width, height, texture->GetOrientation(), stridePixels))
+ {
+ success = true; // Flag that we at least had one successful image processed
+ // drop into the texture
+ unsigned int posX = x*tile_width + (tile_width - width)/2;
+ unsigned int posY = y*tile_height + (tile_height - height)/2;
+ uint32_t *dest = buffer + posX + posY * imageRes;
+ uint32_t *src = scaled;
+ for (unsigned int y = 0; y < height; ++y)
+ {
+ memcpy(dest, src, width*4);
+ dest += imageRes;
+ src += stridePixels;
+ }
+ }
+ }
+ delete[] scaled;
+ }
+ }
+ // now save to a file
+ if (success)
+ success = CreateThumbnailFromSurface((uint8_t *)buffer, imageRes, imageRes, imageRes * 4, thumb);
+
+ free(buffer);
+ return success;
+}
+
+void CPicture::GetScale(unsigned int width, unsigned int height, unsigned int &out_width, unsigned int &out_height)
+{
+ float aspect = (float)width / height;
+ if ((unsigned int)(out_width / aspect + 0.5f) > out_height)
+ out_width = (unsigned int)(out_height * aspect + 0.5f);
+ else
+ out_height = (unsigned int)(out_width / aspect + 0.5f);
+}
+
+bool CPicture::ScaleImage(uint8_t* in_pixels,
+ unsigned int in_width,
+ unsigned int in_height,
+ unsigned int in_pitch,
+ AVPixelFormat in_format,
+ uint8_t* out_pixels,
+ unsigned int out_width,
+ unsigned int out_height,
+ unsigned int out_pitch,
+ AVPixelFormat out_format,
+ CPictureScalingAlgorithm::Algorithm
+ scalingAlgorithm /* = CPictureScalingAlgorithm::NoAlgorithm */)
+{
+ struct SwsContext* context =
+ sws_getContext(in_width, in_height, in_format, out_width, out_height, out_format,
+ CPictureScalingAlgorithm::ToSwscale(scalingAlgorithm), NULL, NULL, NULL);
+
+ uint8_t *src[] = { in_pixels, 0, 0, 0 };
+ int srcStride[] = { (int)in_pitch, 0, 0, 0 };
+ uint8_t *dst[] = { out_pixels , 0, 0, 0 };
+ int dstStride[] = { (int)out_pitch, 0, 0, 0 };
+
+ if (context)
+ {
+ sws_scale(context, src, srcStride, 0, in_height, dst, dstStride);
+ sws_freeContext(context);
+ return true;
+ }
+ return false;
+}
+
+bool CPicture::OrientateImage(uint32_t*& pixels,
+ unsigned int& width,
+ unsigned int& height,
+ int orientation,
+ unsigned int& stridePixels)
+{
+ // ideas for speeding these functions up: http://cgit.freedesktop.org/pixman/tree/pixman/pixman-fast-path.c
+ bool out = false;
+ switch (orientation)
+ {
+ case 1:
+ out = FlipHorizontal(pixels, width, height, stridePixels);
+ break;
+ case 2:
+ out = Rotate180CCW(pixels, width, height, stridePixels);
+ break;
+ case 3:
+ out = FlipVertical(pixels, width, height, stridePixels);
+ break;
+ case 4:
+ out = Transpose(pixels, width, height, stridePixels);
+ break;
+ case 5:
+ out = Rotate270CCW(pixels, width, height, stridePixels);
+ break;
+ case 6:
+ out = TransposeOffAxis(pixels, width, height, stridePixels);
+ break;
+ case 7:
+ out = Rotate90CCW(pixels, width, height, stridePixels);
+ break;
+ default:
+ CLog::Log(LOGERROR, "Unknown orientation {}", orientation);
+ break;
+ }
+ return out;
+}
+
+bool CPicture::FlipHorizontal(uint32_t*& pixels,
+ const unsigned int& width,
+ const unsigned int& height,
+ const unsigned int& stridePixels)
+{
+ // this can be done in-place easily enough
+ for (unsigned int y = 0; y < height; ++y)
+ {
+ uint32_t* line = pixels + y * stridePixels;
+ for (unsigned int x = 0; x < width / 2; ++x)
+ std::swap(line[x], line[width - 1 - x]);
+ }
+ return true;
+}
+
+bool CPicture::FlipVertical(uint32_t*& pixels,
+ const unsigned int& width,
+ const unsigned int& height,
+ const unsigned int& stridePixels)
+{
+ // this can be done in-place easily enough
+ for (unsigned int y = 0; y < height / 2; ++y)
+ {
+ uint32_t* line1 = pixels + y * stridePixels;
+ uint32_t* line2 = pixels + (height - 1 - y) * stridePixels;
+ for (unsigned int x = 0; x < width; ++x)
+ std::swap(*line1++, *line2++);
+ }
+ return true;
+}
+
+bool CPicture::Rotate180CCW(uint32_t*& pixels,
+ const unsigned int& width,
+ const unsigned int& height,
+ const unsigned int& stridePixels)
+{
+ // this can be done in-place easily enough
+ for (unsigned int y = 0; y < height / 2; ++y)
+ {
+ uint32_t* line1 = pixels + y * stridePixels;
+ uint32_t* line2 = pixels + (height - 1 - y) * stridePixels + width - 1;
+ for (unsigned int x = 0; x < width; ++x)
+ std::swap(*line1++, *line2--);
+ }
+ if (height % 2)
+ { // height is odd, so flip the middle row as well
+ uint32_t* line = pixels + (height - 1) / 2 * stridePixels;
+ for (unsigned int x = 0; x < width / 2; ++x)
+ std::swap(line[x], line[width - 1 - x]);
+ }
+ return true;
+}
+
+bool CPicture::Rotate90CCW(uint32_t*& pixels,
+ unsigned int& width,
+ unsigned int& height,
+ unsigned int& stridePixels)
+{
+ uint32_t *dest = new uint32_t[width * height * 4];
+ if (dest)
+ {
+ unsigned int d_height = width, d_width = height;
+ for (unsigned int y = 0; y < d_height; y++)
+ {
+ const uint32_t *src = pixels + (d_height - 1 - y); // y-th col from right, starting at top
+ uint32_t *dst = dest + d_width * y; // y-th row from top, starting at left
+ for (unsigned int x = 0; x < d_width; x++)
+ {
+ *dst++ = *src;
+ src += stridePixels;
+ }
+ }
+ delete[] pixels;
+ pixels = dest;
+ std::swap(width, height);
+ stridePixels = width;
+ return true;
+ }
+ return false;
+}
+
+bool CPicture::Rotate270CCW(uint32_t*& pixels,
+ unsigned int& width,
+ unsigned int& height,
+ unsigned int& stridePixels)
+{
+ uint32_t *dest = new uint32_t[width * height * 4];
+ if (!dest)
+ return false;
+
+ unsigned int d_height = width, d_width = height;
+ for (unsigned int y = 0; y < d_height; y++)
+ {
+ const uint32_t* src =
+ pixels + stridePixels * (d_width - 1) + y; // y-th col from left, starting at bottom
+ uint32_t* dst = dest + d_width * y; // y-th row from top, starting at left
+ for (unsigned int x = 0; x < d_width; x++)
+ {
+ *dst++ = *src;
+ src -= stridePixels;
+ }
+ }
+
+ delete[] pixels;
+ pixels = dest;
+ std::swap(width, height);
+ stridePixels = width;
+ return true;
+}
+
+bool CPicture::Transpose(uint32_t*& pixels,
+ unsigned int& width,
+ unsigned int& height,
+ unsigned int& stridePixels)
+{
+ uint32_t *dest = new uint32_t[width * height * 4];
+ if (!dest)
+ return false;
+
+ unsigned int d_height = width, d_width = height;
+ for (unsigned int y = 0; y < d_height; y++)
+ {
+ const uint32_t *src = pixels + y; // y-th col from left, starting at top
+ uint32_t *dst = dest + d_width * y; // y-th row from top, starting at left
+ for (unsigned int x = 0; x < d_width; x++)
+ {
+ *dst++ = *src;
+ src += stridePixels;
+ }
+ }
+
+ delete[] pixels;
+ pixels = dest;
+ std::swap(width, height);
+ stridePixels = width;
+ return true;
+}
+
+bool CPicture::TransposeOffAxis(uint32_t*& pixels,
+ unsigned int& width,
+ unsigned int& height,
+ unsigned int& stridePixels)
+{
+ uint32_t *dest = new uint32_t[width * height * 4];
+ if (!dest)
+ return false;
+
+ unsigned int d_height = width, d_width = height;
+ for (unsigned int y = 0; y < d_height; y++)
+ {
+ const uint32_t* src = pixels + stridePixels * (d_width - 1) +
+ (d_height - 1 - y); // y-th col from right, starting at bottom
+ uint32_t *dst = dest + d_width * y; // y-th row, starting at left
+ for (unsigned int x = 0; x < d_width; x++)
+ {
+ *dst++ = *src;
+ src -= stridePixels;
+ }
+ }
+
+ delete[] pixels;
+ pixels = dest;
+ std::swap(width, height);
+ stridePixels = width;
+ return true;
+}
diff --git a/xbmc/pictures/Picture.h b/xbmc/pictures/Picture.h
new file mode 100644
index 0000000..192982a
--- /dev/null
+++ b/xbmc/pictures/Picture.h
@@ -0,0 +1,134 @@
+/*
+ * 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 "pictures/PictureScalingAlgorithm.h"
+#include "utils/Job.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+extern "C"
+{
+#include <libavutil/pixfmt.h>
+}
+
+class CTexture;
+
+class CPicture
+{
+public:
+ static bool GetThumbnailFromSurface(const unsigned char* buffer, int width, int height, int stride, const std::string &thumbFile, uint8_t* &result, size_t& result_size);
+ static bool CreateThumbnailFromSurface(const unsigned char* buffer, int width, int height, int stride, const std::string &thumbFile);
+
+ /*! \brief Create a tiled thumb of the given files
+ \param files the files to create the thumb from
+ \param thumb the filename of the thumb
+ */
+ static bool CreateTiledThumb(const std::vector<std::string> &files, const std::string &thumb);
+
+ static bool ResizeTexture(
+ const std::string& image,
+ CTexture* texture,
+ uint32_t& dest_width,
+ uint32_t& dest_height,
+ uint8_t*& result,
+ size_t& result_size,
+ CPictureScalingAlgorithm::Algorithm scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm);
+ static bool ResizeTexture(const std::string &image, uint8_t *pixels, uint32_t width, uint32_t height, uint32_t pitch,
+ uint32_t &dest_width, uint32_t &dest_height, uint8_t* &result, size_t& result_size,
+ CPictureScalingAlgorithm::Algorithm scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm);
+
+ /*! \brief Cache a texture, resizing, rotating and flipping as needed, and saving as a JPG or PNG
+ \param texture a pointer to a CTexture
+ \param dest_width [in/out] maximum width in pixels of cached version - replaced with actual cached width
+ \param dest_height [in/out] maximum height in pixels of cached version - replaced with actual cached height
+ \param dest the output cache file
+ \return true if successful, false otherwise
+ */
+ static bool CacheTexture(
+ CTexture* texture,
+ uint32_t& dest_width,
+ uint32_t& dest_height,
+ const std::string& dest,
+ CPictureScalingAlgorithm::Algorithm scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm);
+ static bool CacheTexture(uint8_t *pixels, uint32_t width, uint32_t height, uint32_t pitch, int orientation,
+ uint32_t &dest_width, uint32_t &dest_height, const std::string &dest,
+ CPictureScalingAlgorithm::Algorithm scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm);
+
+ static void GetScale(unsigned int width, unsigned int height, unsigned int &out_width, unsigned int &out_height);
+ static bool ScaleImage(
+ uint8_t* in_pixels,
+ unsigned int in_width,
+ unsigned int in_height,
+ unsigned int in_pitch,
+ AVPixelFormat in_format,
+ uint8_t* out_pixels,
+ unsigned int out_width,
+ unsigned int out_height,
+ unsigned int out_pitch,
+ AVPixelFormat out_format,
+ CPictureScalingAlgorithm::Algorithm scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm);
+
+private:
+ static bool OrientateImage(uint32_t*& pixels,
+ unsigned int& width,
+ unsigned int& height,
+ int orientation,
+ unsigned int& stridePixels);
+
+ static bool FlipHorizontal(uint32_t*& pixels,
+ const unsigned int& width,
+ const unsigned int& height,
+ const unsigned int& stridePixels);
+ static bool FlipVertical(uint32_t*& pixels,
+ const unsigned int& width,
+ const unsigned int& height,
+ const unsigned int& stridePixels);
+ static bool Rotate90CCW(uint32_t*& pixels,
+ unsigned int& width,
+ unsigned int& height,
+ unsigned int& stridePixels);
+ static bool Rotate270CCW(uint32_t*& pixels,
+ unsigned int& width,
+ unsigned int& height,
+ unsigned int& stridePixels);
+ static bool Rotate180CCW(uint32_t*& pixels,
+ const unsigned int& width,
+ const unsigned int& height,
+ const unsigned int& stridePixels);
+ static bool Transpose(uint32_t*& pixels,
+ unsigned int& width,
+ unsigned int& height,
+ unsigned int& width_aligned);
+ static bool TransposeOffAxis(uint32_t*& pixels,
+ unsigned int& width,
+ unsigned int& height,
+ unsigned int& stridePixels);
+};
+
+//this class calls CreateThumbnailFromSurface in a CJob, so a png file can be written without halting the render thread
+class CThumbnailWriter : public CJob
+{
+ public:
+ //WARNING: buffer is deleted from DoWork()
+ CThumbnailWriter(unsigned char* buffer, int width, int height, int stride, const std::string& thumbFile);
+ ~CThumbnailWriter() override;
+ bool DoWork() override;
+
+ private:
+ unsigned char* m_buffer;
+ int m_width;
+ int m_height;
+ int m_stride;
+ std::string m_thumbFile;
+};
+
diff --git a/xbmc/pictures/PictureInfoLoader.cpp b/xbmc/pictures/PictureInfoLoader.cpp
new file mode 100644
index 0000000..81b8cfa
--- /dev/null
+++ b/xbmc/pictures/PictureInfoLoader.cpp
@@ -0,0 +1,99 @@
+/*
+ * 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 "PictureInfoLoader.h"
+
+#include "FileItem.h"
+#include "PictureInfoTag.h"
+#include "ServiceBroker.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+CPictureInfoLoader::CPictureInfoLoader()
+{
+ m_mapFileItems = new CFileItemList;
+ m_tagReads = 0;
+}
+
+CPictureInfoLoader::~CPictureInfoLoader()
+{
+ StopThread();
+ delete m_mapFileItems;
+}
+
+void CPictureInfoLoader::OnLoaderStart()
+{
+ // Load previously cached items from HD
+ m_mapFileItems->SetPath(m_pVecItems->GetPath());
+ m_mapFileItems->Load();
+ m_mapFileItems->SetFastLookup(true);
+
+ m_tagReads = 0;
+ m_loadTags = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_PICTURES_USETAGS);
+
+ if (m_pProgressCallback)
+ m_pProgressCallback->SetProgressMax(m_pVecItems->GetFileCount());
+}
+
+bool CPictureInfoLoader::LoadItem(CFileItem* pItem)
+{
+ bool result = LoadItemCached(pItem);
+ result |= LoadItemLookup(pItem);
+
+ return result;
+}
+
+bool CPictureInfoLoader::LoadItemCached(CFileItem* pItem)
+{
+ if (!pItem->IsPicture() || pItem->IsZIP() || pItem->IsRAR() || pItem->IsCBR() || pItem->IsCBZ() || pItem->IsInternetStream() || pItem->IsVideo())
+ return false;
+
+ if (pItem->HasPictureInfoTag())
+ return true;
+
+ // Check the cached item
+ CFileItemPtr mapItem = (*m_mapFileItems)[pItem->GetPath()];
+ if (mapItem && mapItem->m_dateTime==pItem->m_dateTime && mapItem->HasPictureInfoTag())
+ { // Query map if we previously cached the file on HD
+ *pItem->GetPictureInfoTag() = *mapItem->GetPictureInfoTag();
+ pItem->SetArt("thumb", mapItem->GetArt("thumb"));
+ return true;
+ }
+
+ return true;
+}
+
+bool CPictureInfoLoader::LoadItemLookup(CFileItem* pItem)
+{
+ if (m_pProgressCallback && !pItem->m_bIsFolder)
+ m_pProgressCallback->SetProgressAdvance();
+
+ if (!pItem->IsPicture() || pItem->IsZIP() || pItem->IsRAR() || pItem->IsCBR() || pItem->IsCBZ() || pItem->IsInternetStream() || pItem->IsVideo())
+ return false;
+
+ if (pItem->HasPictureInfoTag())
+ return false;
+
+ if (m_loadTags)
+ { // Nothing found, load tag from file
+ pItem->GetPictureInfoTag()->Load(pItem->GetPath());
+ m_tagReads++;
+ }
+
+ return true;
+}
+
+void CPictureInfoLoader::OnLoaderFinish()
+{
+ // cleanup cache loaded from HD
+ m_mapFileItems->Clear();
+
+ // Save loaded items to HD
+ if (!m_bStop && m_tagReads > 0)
+ m_pVecItems->Save();
+}
diff --git a/xbmc/pictures/PictureInfoLoader.h b/xbmc/pictures/PictureInfoLoader.h
new file mode 100644
index 0000000..0171fcb
--- /dev/null
+++ b/xbmc/pictures/PictureInfoLoader.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "BackgroundInfoLoader.h"
+
+#include <string>
+
+class CPictureInfoLoader : public CBackgroundInfoLoader
+{
+public:
+ CPictureInfoLoader();
+ ~CPictureInfoLoader() override;
+
+ void UseCacheOnHD(const std::string& strFileName);
+ bool LoadItem(CFileItem* pItem) override;
+ bool LoadItemCached(CFileItem* pItem) override;
+ bool LoadItemLookup(CFileItem* pItem) override;
+
+protected:
+ void OnLoaderStart() override;
+ void OnLoaderFinish() override;
+
+ CFileItemList* m_mapFileItems;
+ unsigned int m_tagReads;
+ bool m_loadTags;
+};
+
diff --git a/xbmc/pictures/PictureInfoTag.cpp b/xbmc/pictures/PictureInfoTag.cpp
new file mode 100644
index 0000000..4aab3e0
--- /dev/null
+++ b/xbmc/pictures/PictureInfoTag.cpp
@@ -0,0 +1,764 @@
+/*
+ * 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 "PictureInfoTag.h"
+
+#include "ServiceBroker.h"
+#include "addons/ExtsMimeSupportList.h"
+#include "addons/ImageDecoder.h"
+#include "addons/addoninfo/AddonType.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "utils/Archive.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <vector>
+
+using namespace KODI::ADDONS;
+
+CPictureInfoTag::ExifInfo::ExifInfo(const ExifInfo_t& other)
+ : CameraMake(other.CameraMake),
+ CameraModel(other.CameraModel),
+ DateTime(other.DateTime),
+ Height(other.Height),
+ Width(other.Width),
+ Orientation(other.Orientation),
+ IsColor(other.IsColor),
+ Process(other.Process),
+ FlashUsed(other.FlashUsed),
+ FocalLength(other.FocalLength),
+ ExposureTime(other.ExposureTime),
+ ApertureFNumber(other.ApertureFNumber),
+ Distance(other.Distance),
+ CCDWidth(other.CCDWidth),
+ ExposureBias(other.ExposureBias),
+ DigitalZoomRatio(other.DigitalZoomRatio),
+ FocalLength35mmEquiv(other.FocalLength35mmEquiv),
+ Whitebalance(other.Whitebalance),
+ MeteringMode(other.MeteringMode),
+ ExposureProgram(other.ExposureProgram),
+ ExposureMode(other.ExposureMode),
+ ISOequivalent(other.ISOequivalent),
+ LightSource(other.LightSource),
+ CommentsCharset(EXIF_COMMENT_CHARSET_CONVERTED),
+ XPCommentsCharset(EXIF_COMMENT_CHARSET_CONVERTED),
+ Comments(Convert(other.CommentsCharset, other.Comments)),
+ FileComment(Convert(EXIF_COMMENT_CHARSET_UNKNOWN, other.FileComment)),
+ XPComment(Convert(other.XPCommentsCharset, other.XPComment)),
+ Description(other.Description),
+ ThumbnailOffset(other.ThumbnailOffset),
+ ThumbnailSize(other.ThumbnailSize),
+ LargestExifOffset(other.LargestExifOffset),
+ ThumbnailAtEnd(other.ThumbnailAtEnd),
+ ThumbnailSizeOffset(other.ThumbnailSizeOffset),
+ DateTimeOffsets(other.DateTimeOffsets, other.DateTimeOffsets + other.numDateTimeTags),
+ GpsInfoPresent(other.GpsInfoPresent),
+ GpsLat(other.GpsLat),
+ GpsLong(other.GpsLong),
+ GpsAlt(other.GpsAlt)
+{
+}
+
+std::string CPictureInfoTag::ExifInfo::Convert(int charset, const char* data)
+{
+ std::string value;
+
+ // The charset used for the UserComment is stored in CommentsCharset:
+ // Ascii, Unicode (UCS2), JIS (X208-1990), Unknown (application specific)
+ if (charset == EXIF_COMMENT_CHARSET_UNICODE)
+ {
+ g_charsetConverter.ucs2ToUTF8(std::u16string(reinterpret_cast<const char16_t*>(data)), value);
+ }
+ else
+ {
+ // Ascii doesn't need to be converted (EXIF_COMMENT_CHARSET_ASCII)
+ // Unknown data can't be converted as it could be any codec (EXIF_COMMENT_CHARSET_UNKNOWN)
+ // JIS data can't be converted as CharsetConverter and iconv lacks support (EXIF_COMMENT_CHARSET_JIS)
+ g_charsetConverter.unknownToUTF8(data, value);
+ }
+
+ return value;
+}
+
+CPictureInfoTag::IPTCInfo::IPTCInfo(const IPTCInfo_t& other)
+ : RecordVersion(other.RecordVersion),
+ SupplementalCategories(other.SupplementalCategories),
+ Keywords(other.Keywords),
+ Caption(other.Caption),
+ Author(other.Author),
+ Headline(other.Headline),
+ SpecialInstructions(other.SpecialInstructions),
+ Category(other.Category),
+ Byline(other.Byline),
+ BylineTitle(other.BylineTitle),
+ Credit(other.Credit),
+ Source(other.Source),
+ CopyrightNotice(other.CopyrightNotice),
+ ObjectName(other.ObjectName),
+ City(other.City),
+ State(other.State),
+ Country(other.Country),
+ TransmissionReference(other.TransmissionReference),
+ Date(other.Date),
+ Urgency(other.Urgency),
+ ReferenceService(other.ReferenceService),
+ CountryCode(other.CountryCode),
+ TimeCreated(other.TimeCreated),
+ SubLocation(other.SubLocation),
+ ImageType(other.ImageType)
+{
+}
+
+void CPictureInfoTag::Reset()
+{
+ m_exifInfo = {};
+ m_iptcInfo = {};
+ m_isLoaded = false;
+ m_isInfoSetExternally = false;
+ m_dateTimeTaken.Reset();
+}
+
+bool CPictureInfoTag::Load(const std::string &path)
+{
+ m_isLoaded = false;
+
+ // Get file extensions to find addon related to it.
+ std::string strExtension = URIUtils::GetExtension(path);
+ StringUtils::ToLower(strExtension);
+ if (!strExtension.empty() && CServiceBroker::IsAddonInterfaceUp())
+ {
+ // Load via available image decoder addons
+ auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetExtensionSupportedAddonInfos(
+ strExtension, CExtsMimeSupportList::FilterSelect::all);
+ for (const auto& addonInfo : addonInfos)
+ {
+ if (addonInfo.first != ADDON::AddonType::IMAGEDECODER)
+ continue;
+
+ std::unique_ptr<CImageDecoder> result = std::make_unique<CImageDecoder>(addonInfo.second, "");
+ if (result->LoadInfoTag(path, this))
+ {
+ m_isLoaded = true;
+ break;
+ }
+ }
+ }
+
+ // Load by Kodi's included own way
+ if (!m_isLoaded)
+ {
+ ExifInfo_t exifInfo;
+ IPTCInfo_t iptcInfo;
+
+ if (process_jpeg(path.c_str(), &exifInfo, &iptcInfo))
+ {
+ m_exifInfo = ExifInfo(exifInfo);
+ m_iptcInfo = IPTCInfo(iptcInfo);
+ m_isLoaded = true;
+ }
+ }
+
+ ConvertDateTime();
+
+ return m_isLoaded;
+}
+
+void CPictureInfoTag::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_isLoaded;
+ ar << m_isInfoSetExternally;
+ ar << m_exifInfo.ApertureFNumber;
+ ar << m_exifInfo.CameraMake;
+ ar << m_exifInfo.CameraModel;
+ ar << m_exifInfo.CCDWidth;
+ ar << m_exifInfo.Comments;
+ ar << m_exifInfo.Description;
+ ar << m_exifInfo.DateTime;
+ for (std::vector<int>::size_type i = 0; i < MAX_DATE_COPIES; ++i)
+ {
+ if (i < m_exifInfo.DateTimeOffsets.size())
+ ar << m_exifInfo.DateTimeOffsets[i];
+ else
+ ar << static_cast<int>(0);
+ }
+ ar << m_exifInfo.DigitalZoomRatio;
+ ar << m_exifInfo.Distance;
+ ar << m_exifInfo.ExposureBias;
+ ar << m_exifInfo.ExposureMode;
+ ar << m_exifInfo.ExposureProgram;
+ ar << m_exifInfo.ExposureTime;
+ ar << m_exifInfo.FlashUsed;
+ ar << m_exifInfo.FocalLength;
+ ar << m_exifInfo.FocalLength35mmEquiv;
+ ar << m_exifInfo.GpsInfoPresent;
+ ar << m_exifInfo.GpsAlt;
+ ar << m_exifInfo.GpsLat;
+ ar << m_exifInfo.GpsLong;
+ ar << m_exifInfo.Height;
+ ar << m_exifInfo.IsColor;
+ ar << m_exifInfo.ISOequivalent;
+ ar << m_exifInfo.LargestExifOffset;
+ ar << m_exifInfo.LightSource;
+ ar << m_exifInfo.MeteringMode;
+ ar << static_cast<int>(m_exifInfo.DateTimeOffsets.size());
+ ar << m_exifInfo.Orientation;
+ ar << m_exifInfo.Process;
+ ar << m_exifInfo.ThumbnailAtEnd;
+ ar << m_exifInfo.ThumbnailOffset;
+ ar << m_exifInfo.ThumbnailSize;
+ ar << m_exifInfo.ThumbnailSizeOffset;
+ ar << m_exifInfo.Whitebalance;
+ ar << m_exifInfo.Width;
+ ar << m_dateTimeTaken;
+
+ ar << m_iptcInfo.Author;
+ ar << m_iptcInfo.Byline;
+ ar << m_iptcInfo.BylineTitle;
+ ar << m_iptcInfo.Caption;
+ ar << m_iptcInfo.Category;
+ ar << m_iptcInfo.City;
+ ar << m_iptcInfo.Urgency;
+ ar << m_iptcInfo.CopyrightNotice;
+ ar << m_iptcInfo.Country;
+ ar << m_iptcInfo.CountryCode;
+ ar << m_iptcInfo.Credit;
+ ar << m_iptcInfo.Date;
+ ar << m_iptcInfo.Headline;
+ ar << m_iptcInfo.Keywords;
+ ar << m_iptcInfo.ObjectName;
+ ar << m_iptcInfo.ReferenceService;
+ ar << m_iptcInfo.Source;
+ ar << m_iptcInfo.SpecialInstructions;
+ ar << m_iptcInfo.State;
+ ar << m_iptcInfo.SupplementalCategories;
+ ar << m_iptcInfo.TransmissionReference;
+ ar << m_iptcInfo.TimeCreated;
+ ar << m_iptcInfo.SubLocation;
+ ar << m_iptcInfo.ImageType;
+ }
+ else
+ {
+ ar >> m_isLoaded;
+ ar >> m_isInfoSetExternally;
+ ar >> m_exifInfo.ApertureFNumber;
+ ar >> m_exifInfo.CameraMake;
+ ar >> m_exifInfo.CameraModel;
+ ar >> m_exifInfo.CCDWidth;
+ ar >> m_exifInfo.Comments;
+ m_exifInfo.CommentsCharset = EXIF_COMMENT_CHARSET_CONVERTED; // Store and restore the comment charset converted
+ ar >> m_exifInfo.Description;
+ ar >> m_exifInfo.DateTime;
+ m_exifInfo.DateTimeOffsets.clear();
+ m_exifInfo.DateTimeOffsets.reserve(MAX_DATE_COPIES);
+ for (std::vector<int>::size_type i = 0; i < MAX_DATE_COPIES; ++i)
+ {
+ int dateTimeOffset;
+ ar >> dateTimeOffset;
+ m_exifInfo.DateTimeOffsets.push_back(dateTimeOffset);
+ }
+ ar >> m_exifInfo.DigitalZoomRatio;
+ ar >> m_exifInfo.Distance;
+ ar >> m_exifInfo.ExposureBias;
+ ar >> m_exifInfo.ExposureMode;
+ ar >> m_exifInfo.ExposureProgram;
+ ar >> m_exifInfo.ExposureTime;
+ ar >> m_exifInfo.FlashUsed;
+ ar >> m_exifInfo.FocalLength;
+ ar >> m_exifInfo.FocalLength35mmEquiv;
+ ar >> m_exifInfo.GpsInfoPresent;
+ ar >> m_exifInfo.GpsAlt;
+ ar >> m_exifInfo.GpsLat;
+ ar >> m_exifInfo.GpsLong;
+ ar >> m_exifInfo.Height;
+ ar >> m_exifInfo.IsColor;
+ ar >> m_exifInfo.ISOequivalent;
+ ar >> m_exifInfo.LargestExifOffset;
+ ar >> m_exifInfo.LightSource;
+ ar >> m_exifInfo.MeteringMode;
+ int numDateTimeTags;
+ ar >> numDateTimeTags;
+ m_exifInfo.DateTimeOffsets.resize(numDateTimeTags);
+ ar >> m_exifInfo.Orientation;
+ ar >> m_exifInfo.Process;
+ ar >> m_exifInfo.ThumbnailAtEnd;
+ ar >> m_exifInfo.ThumbnailOffset;
+ ar >> m_exifInfo.ThumbnailSize;
+ ar >> m_exifInfo.ThumbnailSizeOffset;
+ ar >> m_exifInfo.Whitebalance;
+ ar >> m_exifInfo.Width;
+ ar >> m_dateTimeTaken;
+
+ ar >> m_iptcInfo.Author;
+ ar >> m_iptcInfo.Byline;
+ ar >> m_iptcInfo.BylineTitle;
+ ar >> m_iptcInfo.Caption;
+ ar >> m_iptcInfo.Category;
+ ar >> m_iptcInfo.City;
+ ar >> m_iptcInfo.Urgency;
+ ar >> m_iptcInfo.CopyrightNotice;
+ ar >> m_iptcInfo.Country;
+ ar >> m_iptcInfo.CountryCode;
+ ar >> m_iptcInfo.Credit;
+ ar >> m_iptcInfo.Date;
+ ar >> m_iptcInfo.Headline;
+ ar >> m_iptcInfo.Keywords;
+ ar >> m_iptcInfo.ObjectName;
+ ar >> m_iptcInfo.ReferenceService;
+ ar >> m_iptcInfo.Source;
+ ar >> m_iptcInfo.SpecialInstructions;
+ ar >> m_iptcInfo.State;
+ ar >> m_iptcInfo.SupplementalCategories;
+ ar >> m_iptcInfo.TransmissionReference;
+ ar >> m_iptcInfo.TimeCreated;
+ ar >> m_iptcInfo.SubLocation;
+ ar >> m_iptcInfo.ImageType;
+ }
+}
+
+void CPictureInfoTag::Serialize(CVariant& value) const
+{
+ value["aperturefnumber"] = m_exifInfo.ApertureFNumber;
+ value["cameramake"] = m_exifInfo.CameraMake;
+ value["cameramodel"] = m_exifInfo.CameraModel;
+ value["ccdwidth"] = m_exifInfo.CCDWidth;
+ value["comments"] = m_exifInfo.Comments;
+ value["description"] = m_exifInfo.Description;
+ value["datetime"] = m_exifInfo.DateTime;
+ for (std::vector<int>::size_type i = 0; i < MAX_DATE_COPIES; ++i)
+ {
+ if (i < m_exifInfo.DateTimeOffsets.size())
+ value["datetimeoffsets"][static_cast<int>(i)] = m_exifInfo.DateTimeOffsets[i];
+ else
+ value["datetimeoffsets"][static_cast<int>(i)] = static_cast<int>(0);
+ }
+ value["digitalzoomratio"] = m_exifInfo.DigitalZoomRatio;
+ value["distance"] = m_exifInfo.Distance;
+ value["exposurebias"] = m_exifInfo.ExposureBias;
+ value["exposuremode"] = m_exifInfo.ExposureMode;
+ value["exposureprogram"] = m_exifInfo.ExposureProgram;
+ value["exposuretime"] = m_exifInfo.ExposureTime;
+ value["flashused"] = m_exifInfo.FlashUsed;
+ value["focallength"] = m_exifInfo.FocalLength;
+ value["focallength35mmequiv"] = m_exifInfo.FocalLength35mmEquiv;
+ value["gpsinfopresent"] = m_exifInfo.GpsInfoPresent;
+ value["gpsinfo"]["alt"] = m_exifInfo.GpsAlt;
+ value["gpsinfo"]["lat"] = m_exifInfo.GpsLat;
+ value["gpsinfo"]["long"] = m_exifInfo.GpsLong;
+ value["height"] = m_exifInfo.Height;
+ value["iscolor"] = m_exifInfo.IsColor;
+ value["isoequivalent"] = m_exifInfo.ISOequivalent;
+ value["largestexifoffset"] = m_exifInfo.LargestExifOffset;
+ value["lightsource"] = m_exifInfo.LightSource;
+ value["meteringmode"] = m_exifInfo.MeteringMode;
+ value["numdatetimetags"] = static_cast<int>(m_exifInfo.DateTimeOffsets.size());
+ value["orientation"] = m_exifInfo.Orientation;
+ value["process"] = m_exifInfo.Process;
+ value["thumbnailatend"] = m_exifInfo.ThumbnailAtEnd;
+ value["thumbnailoffset"] = m_exifInfo.ThumbnailOffset;
+ value["thumbnailsize"] = m_exifInfo.ThumbnailSize;
+ value["thumbnailsizeoffset"] = m_exifInfo.ThumbnailSizeOffset;
+ value["whitebalance"] = m_exifInfo.Whitebalance;
+ value["width"] = m_exifInfo.Width;
+
+ value["author"] = m_iptcInfo.Author;
+ value["byline"] = m_iptcInfo.Byline;
+ value["bylinetitle"] = m_iptcInfo.BylineTitle;
+ value["caption"] = m_iptcInfo.Caption;
+ value["category"] = m_iptcInfo.Category;
+ value["city"] = m_iptcInfo.City;
+ value["urgency"] = m_iptcInfo.Urgency;
+ value["copyrightnotice"] = m_iptcInfo.CopyrightNotice;
+ value["country"] = m_iptcInfo.Country;
+ value["countrycode"] = m_iptcInfo.CountryCode;
+ value["credit"] = m_iptcInfo.Credit;
+ value["date"] = m_iptcInfo.Date;
+ value["headline"] = m_iptcInfo.Headline;
+ value["keywords"] = m_iptcInfo.Keywords;
+ value["objectname"] = m_iptcInfo.ObjectName;
+ value["referenceservice"] = m_iptcInfo.ReferenceService;
+ value["source"] = m_iptcInfo.Source;
+ value["specialinstructions"] = m_iptcInfo.SpecialInstructions;
+ value["state"] = m_iptcInfo.State;
+ value["supplementalcategories"] = m_iptcInfo.SupplementalCategories;
+ value["transmissionreference"] = m_iptcInfo.TransmissionReference;
+ value["timecreated"] = m_iptcInfo.TimeCreated;
+ value["sublocation"] = m_iptcInfo.SubLocation;
+ value["imagetype"] = m_iptcInfo.ImageType;
+}
+
+void CPictureInfoTag::ToSortable(SortItem& sortable, Field field) const
+{
+ if (field == FieldDateTaken && m_dateTimeTaken.IsValid())
+ sortable[FieldDateTaken] = m_dateTimeTaken.GetAsDBDateTime();
+}
+
+const std::string CPictureInfoTag::GetInfo(int info) const
+{
+ if (!m_isLoaded && !m_isInfoSetExternally) // If no metadata has been loaded from the picture file or set with SetInfo(), just return
+ return "";
+
+ std::string value;
+ switch (info)
+ {
+ case SLIDESHOW_RESOLUTION:
+ value = StringUtils::Format("{} x {}", m_exifInfo.Width, m_exifInfo.Height);
+ break;
+ case SLIDESHOW_COLOUR:
+ value = m_exifInfo.IsColor ? "Colour" : "Black and White";
+ break;
+ case SLIDESHOW_PROCESS:
+ switch (m_exifInfo.Process)
+ {
+ case M_SOF0:
+ // don't show it if its the plain old boring 'baseline' process, but do
+ // show it if its something else, like 'progressive' (used on web sometimes)
+ value = "Baseline";
+ break;
+ case M_SOF1: value = "Extended sequential"; break;
+ case M_SOF2: value = "Progressive"; break;
+ case M_SOF3: value = "Lossless"; break;
+ case M_SOF5: value = "Differential sequential"; break;
+ case M_SOF6: value = "Differential progressive"; break;
+ case M_SOF7: value = "Differential lossless"; break;
+ case M_SOF9: value = "Extended sequential, arithmetic coding"; break;
+ case M_SOF10: value = "Progressive, arithmetic coding"; break;
+ case M_SOF11: value = "Lossless, arithmetic coding"; break;
+ case M_SOF13: value = "Differential sequential, arithmetic coding"; break;
+ case M_SOF14: value = "Differential progressive, arithmetic coding"; break;
+ case M_SOF15: value = "Differential lossless, arithmetic coding"; break;
+ default: value = "Unknown"; break;
+ }
+ break;
+ case SLIDESHOW_COMMENT:
+ value = m_exifInfo.FileComment;
+ break;
+ case SLIDESHOW_EXIF_COMMENT:
+ value = m_exifInfo.Comments;
+ break;
+ case SLIDESHOW_EXIF_XPCOMMENT:
+ value = m_exifInfo.XPComment;
+ break;
+ case SLIDESHOW_EXIF_LONG_DATE_TIME:
+ if (m_dateTimeTaken.IsValid())
+ value = m_dateTimeTaken.GetAsLocalizedDateTime(true);
+ break;
+ case SLIDESHOW_EXIF_DATE_TIME:
+ if (m_dateTimeTaken.IsValid())
+ value = m_dateTimeTaken.GetAsLocalizedDateTime();
+ break;
+ case SLIDESHOW_EXIF_LONG_DATE:
+ if (m_dateTimeTaken.IsValid())
+ value = m_dateTimeTaken.GetAsLocalizedDate(true);
+ break;
+ case SLIDESHOW_EXIF_DATE:
+ if (m_dateTimeTaken.IsValid())
+ value = m_dateTimeTaken.GetAsLocalizedDate();
+ break;
+ case SLIDESHOW_EXIF_DESCRIPTION:
+ value = m_exifInfo.Description;
+ break;
+ case SLIDESHOW_EXIF_CAMERA_MAKE:
+ value = m_exifInfo.CameraMake;
+ break;
+ case SLIDESHOW_EXIF_CAMERA_MODEL:
+ value = m_exifInfo.CameraModel;
+ break;
+// case SLIDESHOW_EXIF_SOFTWARE:
+// value = m_exifInfo.Software;
+ case SLIDESHOW_EXIF_APERTURE:
+ if (m_exifInfo.ApertureFNumber)
+ value = StringUtils::Format("{:3.1f}", m_exifInfo.ApertureFNumber);
+ break;
+ case SLIDESHOW_EXIF_ORIENTATION:
+ switch (m_exifInfo.Orientation)
+ {
+ case 1: value = "Top Left"; break;
+ case 2: value = "Top Right"; break;
+ case 3: value = "Bottom Right"; break;
+ case 4: value = "Bottom Left"; break;
+ case 5: value = "Left Top"; break;
+ case 6: value = "Right Top"; break;
+ case 7: value = "Right Bottom"; break;
+ case 8: value = "Left Bottom"; break;
+ }
+ break;
+ case SLIDESHOW_EXIF_FOCAL_LENGTH:
+ if (m_exifInfo.FocalLength)
+ {
+ value = StringUtils::Format("{:4.2f}mm", m_exifInfo.FocalLength);
+ if (m_exifInfo.FocalLength35mmEquiv != 0)
+ value += StringUtils::Format(" (35mm Equivalent = {}mm)", m_exifInfo.FocalLength35mmEquiv);
+ }
+ break;
+ case SLIDESHOW_EXIF_FOCUS_DIST:
+ if (m_exifInfo.Distance < 0)
+ value = "Infinite";
+ else if (m_exifInfo.Distance > 0)
+ value = StringUtils::Format("{:4.2f}m", m_exifInfo.Distance);
+ break;
+ case SLIDESHOW_EXIF_EXPOSURE:
+ switch (m_exifInfo.ExposureProgram)
+ {
+ case 1: value = "Manual"; break;
+ case 2: value = "Program (Auto)"; break;
+ case 3: value = "Aperture priority (Semi-Auto)"; break;
+ case 4: value = "Shutter priority (semi-auto)"; break;
+ case 5: value = "Creative Program (based towards depth of field)"; break;
+ case 6: value = "Action program (based towards fast shutter speed)"; break;
+ case 7: value = "Portrait Mode"; break;
+ case 8: value = "Landscape Mode"; break;
+ }
+ break;
+ case SLIDESHOW_EXIF_EXPOSURE_TIME:
+ if (m_exifInfo.ExposureTime)
+ {
+ if (m_exifInfo.ExposureTime < 0.010f)
+ value = StringUtils::Format("{:6.4f}s", m_exifInfo.ExposureTime);
+ else
+ value = StringUtils::Format("{:5.3f}s", m_exifInfo.ExposureTime);
+ if (m_exifInfo.ExposureTime <= 0.5f)
+ value += StringUtils::Format(" (1/{})", static_cast<int>(0.5f + 1 / m_exifInfo.ExposureTime));
+ }
+ break;
+ case SLIDESHOW_EXIF_EXPOSURE_BIAS:
+ if (m_exifInfo.ExposureBias != 0)
+ value = StringUtils::Format("{:4.2f} EV", m_exifInfo.ExposureBias);
+ break;
+ case SLIDESHOW_EXIF_EXPOSURE_MODE:
+ switch (m_exifInfo.ExposureMode)
+ {
+ case 0: value = "Automatic"; break;
+ case 1: value = "Manual"; break;
+ case 2: value = "Auto bracketing"; break;
+ }
+ break;
+ case SLIDESHOW_EXIF_FLASH_USED:
+ if (m_exifInfo.FlashUsed >= 0)
+ {
+ if (m_exifInfo.FlashUsed & 1)
+ {
+ value = "Yes";
+ switch (m_exifInfo.FlashUsed)
+ {
+ case 0x5: value = "Yes (Strobe light not detected)"; break;
+ case 0x7: value = "Yes (Strobe light detected)"; break;
+ case 0x9: value = "Yes (Manual)"; break;
+ case 0xd: value = "Yes (Manual, return light not detected)"; break;
+ case 0xf: value = "Yes (Manual, return light detected)"; break;
+ case 0x19: value = "Yes (Auto)"; break;
+ case 0x1d: value = "Yes (Auto, return light not detected)"; break;
+ case 0x1f: value = "Yes (Auto, return light detected)"; break;
+ case 0x41: value = "Yes (Red eye reduction mode)"; break;
+ case 0x45: value = "Yes (Red eye reduction mode return light not detected)"; break;
+ case 0x47: value = "Yes (Red eye reduction mode return light detected)"; break;
+ case 0x49: value = "Yes (Manual, red eye reduction mode)"; break;
+ case 0x4d: value = "Yes (Manual, red eye reduction mode, return light not detected)"; break;
+ case 0x4f: value = "Yes (Manual, red eye reduction mode, return light detected)"; break;
+ case 0x59: value = "Yes (Auto, red eye reduction mode)"; break;
+ case 0x5d: value = "Yes (Auto, red eye reduction mode, return light not detected)"; break;
+ case 0x5f: value = "Yes (Auto, red eye reduction mode, return light detected)"; break;
+ }
+ }
+ else
+ value = m_exifInfo.FlashUsed == 0x18 ? "No (Auto)" : "No";
+ }
+ break;
+ case SLIDESHOW_EXIF_WHITE_BALANCE:
+ return m_exifInfo.Whitebalance ? "Manual" : "Auto";
+ case SLIDESHOW_EXIF_LIGHT_SOURCE:
+ switch (m_exifInfo.LightSource)
+ {
+ case 1: value = "Daylight"; break;
+ case 2: value = "Fluorescent"; break;
+ case 3: value = "Incandescent"; break;
+ case 4: value = "Flash"; break;
+ case 9: value = "Fine Weather"; break;
+ case 11: value = "Shade"; break;
+ default:; //Quercus: 17-1-2004 There are many more modes for this, check Exif2.2 specs
+ // If it just says 'unknown' or we don't know it, then
+ // don't bother showing it - it doesn't add any useful information.
+ }
+ break;
+ case SLIDESHOW_EXIF_METERING_MODE:
+ switch (m_exifInfo.MeteringMode)
+ {
+ case 2: value = "Center weight"; break;
+ case 3: value = "Spot"; break;
+ case 5: value = "Matrix"; break;
+ }
+ break;
+ case SLIDESHOW_EXIF_ISO_EQUIV:
+ if (m_exifInfo.ISOequivalent)
+ value = StringUtils::Format("{:2}", m_exifInfo.ISOequivalent);
+ break;
+ case SLIDESHOW_EXIF_DIGITAL_ZOOM:
+ if (m_exifInfo.DigitalZoomRatio)
+ value = StringUtils::Format("{:1.3f}x", m_exifInfo.DigitalZoomRatio);
+ break;
+ case SLIDESHOW_EXIF_CCD_WIDTH:
+ if (m_exifInfo.CCDWidth)
+ value = StringUtils::Format("{:4.2f}mm", m_exifInfo.CCDWidth);
+ break;
+ case SLIDESHOW_EXIF_GPS_LATITUDE:
+ value = m_exifInfo.GpsLat;
+ break;
+ case SLIDESHOW_EXIF_GPS_LONGITUDE:
+ value = m_exifInfo.GpsLong;
+ break;
+ case SLIDESHOW_EXIF_GPS_ALTITUDE:
+ value = m_exifInfo.GpsAlt;
+ break;
+ case SLIDESHOW_IPTC_SUP_CATEGORIES: value = m_iptcInfo.SupplementalCategories; break;
+ case SLIDESHOW_IPTC_KEYWORDS: value = m_iptcInfo.Keywords; break;
+ case SLIDESHOW_IPTC_CAPTION: value = m_iptcInfo.Caption; break;
+ case SLIDESHOW_IPTC_AUTHOR: value = m_iptcInfo.Author; break;
+ case SLIDESHOW_IPTC_HEADLINE: value = m_iptcInfo.Headline; break;
+ case SLIDESHOW_IPTC_SPEC_INSTR: value = m_iptcInfo.SpecialInstructions; break;
+ case SLIDESHOW_IPTC_CATEGORY: value = m_iptcInfo.Category; break;
+ case SLIDESHOW_IPTC_BYLINE: value = m_iptcInfo.Byline; break;
+ case SLIDESHOW_IPTC_BYLINE_TITLE: value = m_iptcInfo.BylineTitle; break;
+ case SLIDESHOW_IPTC_CREDIT: value = m_iptcInfo.Credit; break;
+ case SLIDESHOW_IPTC_SOURCE: value = m_iptcInfo.Source; break;
+ case SLIDESHOW_IPTC_COPYRIGHT_NOTICE: value = m_iptcInfo.CopyrightNotice; break;
+ case SLIDESHOW_IPTC_OBJECT_NAME: value = m_iptcInfo.ObjectName; break;
+ case SLIDESHOW_IPTC_CITY: value = m_iptcInfo.City; break;
+ case SLIDESHOW_IPTC_STATE: value = m_iptcInfo.State; break;
+ case SLIDESHOW_IPTC_COUNTRY: value = m_iptcInfo.Country; break;
+ case SLIDESHOW_IPTC_TX_REFERENCE: value = m_iptcInfo.TransmissionReference; break;
+ case SLIDESHOW_IPTC_DATE: value = m_iptcInfo.Date; break;
+ case SLIDESHOW_IPTC_URGENCY: value = m_iptcInfo.Urgency; break;
+ case SLIDESHOW_IPTC_COUNTRY_CODE: value = m_iptcInfo.CountryCode; break;
+ case SLIDESHOW_IPTC_REF_SERVICE: value = m_iptcInfo.ReferenceService; break;
+ case SLIDESHOW_IPTC_TIMECREATED: value = m_iptcInfo.TimeCreated; break;
+ case SLIDESHOW_IPTC_SUBLOCATION: value = m_iptcInfo.SubLocation; break;
+ case SLIDESHOW_IPTC_IMAGETYPE: value = m_iptcInfo.ImageType; break;
+ default:
+ break;
+ }
+ return value;
+}
+
+int CPictureInfoTag::TranslateString(const std::string &info)
+{
+ if (StringUtils::EqualsNoCase(info, "filename")) return SLIDESHOW_FILE_NAME;
+ else if (StringUtils::EqualsNoCase(info, "path")) return SLIDESHOW_FILE_PATH;
+ else if (StringUtils::EqualsNoCase(info, "filesize")) return SLIDESHOW_FILE_SIZE;
+ else if (StringUtils::EqualsNoCase(info, "filedate")) return SLIDESHOW_FILE_DATE;
+ else if (StringUtils::EqualsNoCase(info, "slideindex")) return SLIDESHOW_INDEX;
+ else if (StringUtils::EqualsNoCase(info, "resolution")) return SLIDESHOW_RESOLUTION;
+ else if (StringUtils::EqualsNoCase(info, "slidecomment")) return SLIDESHOW_COMMENT;
+ else if (StringUtils::EqualsNoCase(info, "colour")) return SLIDESHOW_COLOUR;
+ else if (StringUtils::EqualsNoCase(info, "process")) return SLIDESHOW_PROCESS;
+ else if (StringUtils::EqualsNoCase(info, "exiftime")) return SLIDESHOW_EXIF_DATE_TIME;
+ else if (StringUtils::EqualsNoCase(info, "exifdate")) return SLIDESHOW_EXIF_DATE;
+ else if (StringUtils::EqualsNoCase(info, "longexiftime")) return SLIDESHOW_EXIF_LONG_DATE_TIME;
+ else if (StringUtils::EqualsNoCase(info, "longexifdate")) return SLIDESHOW_EXIF_LONG_DATE;
+ else if (StringUtils::EqualsNoCase(info, "exifdescription")) return SLIDESHOW_EXIF_DESCRIPTION;
+ else if (StringUtils::EqualsNoCase(info, "cameramake")) return SLIDESHOW_EXIF_CAMERA_MAKE;
+ else if (StringUtils::EqualsNoCase(info, "cameramodel")) return SLIDESHOW_EXIF_CAMERA_MODEL;
+ else if (StringUtils::EqualsNoCase(info, "exifcomment")) return SLIDESHOW_EXIF_COMMENT;
+ else if (StringUtils::EqualsNoCase(info, "exifsoftware")) return SLIDESHOW_EXIF_SOFTWARE;
+ else if (StringUtils::EqualsNoCase(info, "aperture")) return SLIDESHOW_EXIF_APERTURE;
+ else if (StringUtils::EqualsNoCase(info, "focallength")) return SLIDESHOW_EXIF_FOCAL_LENGTH;
+ else if (StringUtils::EqualsNoCase(info, "focusdistance")) return SLIDESHOW_EXIF_FOCUS_DIST;
+ else if (StringUtils::EqualsNoCase(info, "exposure")) return SLIDESHOW_EXIF_EXPOSURE;
+ else if (StringUtils::EqualsNoCase(info, "exposuretime")) return SLIDESHOW_EXIF_EXPOSURE_TIME;
+ else if (StringUtils::EqualsNoCase(info, "exposurebias")) return SLIDESHOW_EXIF_EXPOSURE_BIAS;
+ else if (StringUtils::EqualsNoCase(info, "exposuremode")) return SLIDESHOW_EXIF_EXPOSURE_MODE;
+ else if (StringUtils::EqualsNoCase(info, "flashused")) return SLIDESHOW_EXIF_FLASH_USED;
+ else if (StringUtils::EqualsNoCase(info, "whitebalance")) return SLIDESHOW_EXIF_WHITE_BALANCE;
+ else if (StringUtils::EqualsNoCase(info, "lightsource")) return SLIDESHOW_EXIF_LIGHT_SOURCE;
+ else if (StringUtils::EqualsNoCase(info, "meteringmode")) return SLIDESHOW_EXIF_METERING_MODE;
+ else if (StringUtils::EqualsNoCase(info, "isoequivalence")) return SLIDESHOW_EXIF_ISO_EQUIV;
+ else if (StringUtils::EqualsNoCase(info, "digitalzoom")) return SLIDESHOW_EXIF_DIGITAL_ZOOM;
+ else if (StringUtils::EqualsNoCase(info, "ccdwidth")) return SLIDESHOW_EXIF_CCD_WIDTH;
+ else if (StringUtils::EqualsNoCase(info, "orientation")) return SLIDESHOW_EXIF_ORIENTATION;
+ else if (StringUtils::EqualsNoCase(info, "supplementalcategories")) return SLIDESHOW_IPTC_SUP_CATEGORIES;
+ else if (StringUtils::EqualsNoCase(info, "keywords")) return SLIDESHOW_IPTC_KEYWORDS;
+ else if (StringUtils::EqualsNoCase(info, "caption")) return SLIDESHOW_IPTC_CAPTION;
+ else if (StringUtils::EqualsNoCase(info, "author")) return SLIDESHOW_IPTC_AUTHOR;
+ else if (StringUtils::EqualsNoCase(info, "headline")) return SLIDESHOW_IPTC_HEADLINE;
+ else if (StringUtils::EqualsNoCase(info, "specialinstructions")) return SLIDESHOW_IPTC_SPEC_INSTR;
+ else if (StringUtils::EqualsNoCase(info, "category")) return SLIDESHOW_IPTC_CATEGORY;
+ else if (StringUtils::EqualsNoCase(info, "byline")) return SLIDESHOW_IPTC_BYLINE;
+ else if (StringUtils::EqualsNoCase(info, "bylinetitle")) return SLIDESHOW_IPTC_BYLINE_TITLE;
+ else if (StringUtils::EqualsNoCase(info, "credit")) return SLIDESHOW_IPTC_CREDIT;
+ else if (StringUtils::EqualsNoCase(info, "source")) return SLIDESHOW_IPTC_SOURCE;
+ else if (StringUtils::EqualsNoCase(info, "copyrightnotice")) return SLIDESHOW_IPTC_COPYRIGHT_NOTICE;
+ else if (StringUtils::EqualsNoCase(info, "objectname")) return SLIDESHOW_IPTC_OBJECT_NAME;
+ else if (StringUtils::EqualsNoCase(info, "city")) return SLIDESHOW_IPTC_CITY;
+ else if (StringUtils::EqualsNoCase(info, "state")) return SLIDESHOW_IPTC_STATE;
+ else if (StringUtils::EqualsNoCase(info, "country")) return SLIDESHOW_IPTC_COUNTRY;
+ else if (StringUtils::EqualsNoCase(info, "transmissionreference")) return SLIDESHOW_IPTC_TX_REFERENCE;
+ else if (StringUtils::EqualsNoCase(info, "iptcdate")) return SLIDESHOW_IPTC_DATE;
+ else if (StringUtils::EqualsNoCase(info, "urgency")) return SLIDESHOW_IPTC_URGENCY;
+ else if (StringUtils::EqualsNoCase(info, "countrycode")) return SLIDESHOW_IPTC_COUNTRY_CODE;
+ else if (StringUtils::EqualsNoCase(info, "referenceservice")) return SLIDESHOW_IPTC_REF_SERVICE;
+ else if (StringUtils::EqualsNoCase(info, "latitude")) return SLIDESHOW_EXIF_GPS_LATITUDE;
+ else if (StringUtils::EqualsNoCase(info, "longitude")) return SLIDESHOW_EXIF_GPS_LONGITUDE;
+ else if (StringUtils::EqualsNoCase(info, "altitude")) return SLIDESHOW_EXIF_GPS_ALTITUDE;
+ else if (StringUtils::EqualsNoCase(info, "timecreated")) return SLIDESHOW_IPTC_TIMECREATED;
+ else if (StringUtils::EqualsNoCase(info, "sublocation")) return SLIDESHOW_IPTC_SUBLOCATION;
+ else if (StringUtils::EqualsNoCase(info, "imagetype")) return SLIDESHOW_IPTC_IMAGETYPE;
+ return 0;
+}
+
+void CPictureInfoTag::SetInfo(const std::string &key, const std::string& value)
+{
+ int info = TranslateString(key);
+
+ switch (info)
+ {
+ case SLIDESHOW_RESOLUTION:
+ {
+ std::vector<std::string> dimension;
+ StringUtils::Tokenize(value, dimension, ",");
+ if (dimension.size() == 2)
+ {
+ m_exifInfo.Width = atoi(dimension[0].c_str());
+ m_exifInfo.Height = atoi(dimension[1].c_str());
+ m_isInfoSetExternally = true; // Set the internal state to show metadata has been set by call to SetInfo
+ }
+ break;
+ }
+ case SLIDESHOW_EXIF_DATE_TIME:
+ {
+ m_exifInfo.DateTime = value;
+ m_isInfoSetExternally = true; // Set the internal state to show metadata has been set by call to SetInfo
+ ConvertDateTime();
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+const CDateTime& CPictureInfoTag::GetDateTimeTaken() const
+{
+ return m_dateTimeTaken;
+}
+
+void CPictureInfoTag::ConvertDateTime()
+{
+ const std::string& dateTime = m_exifInfo.DateTime;
+ if (dateTime.length() >= 19 && dateTime[0] != ' ')
+ {
+ int year = atoi(dateTime.substr(0, 4).c_str());
+ int month = atoi(dateTime.substr(5, 2).c_str());
+ int day = atoi(dateTime.substr(8, 2).c_str());
+ int hour = atoi(dateTime.substr(11,2).c_str());
+ int min = atoi(dateTime.substr(14,2).c_str());
+ int sec = atoi(dateTime.substr(17,2).c_str());
+ m_dateTimeTaken.SetDateTime(year, month, day, hour, min, sec);
+ }
+}
diff --git a/xbmc/pictures/PictureInfoTag.h b/xbmc/pictures/PictureInfoTag.h
new file mode 100644
index 0000000..eff58fb
--- /dev/null
+++ b/xbmc/pictures/PictureInfoTag.h
@@ -0,0 +1,161 @@
+/*
+ * 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 "XBDateTime.h"
+#include "libexif.h"
+#include "utils/IArchivable.h"
+#include "utils/ISerializable.h"
+#include "utils/ISortable.h"
+
+#include <string>
+#include <vector>
+
+namespace KODI
+{
+namespace ADDONS
+{
+class CImageDecoder;
+} /* namespace ADDONS */
+} /* namespace KODI */
+
+class CVariant;
+
+class CPictureInfoTag : public IArchivable, public ISerializable, public ISortable
+{
+ friend class KODI::ADDONS::CImageDecoder;
+
+ // Mimic structs from libexif.h but with C++ types instead of arrays
+ struct ExifInfo
+ {
+ ExifInfo() = default;
+ ExifInfo(const ExifInfo&) = default;
+ ExifInfo(ExifInfo&&) = default;
+ ExifInfo(const ExifInfo_t& other);
+
+ ExifInfo& operator=(const ExifInfo&) = default;
+ ExifInfo& operator=(ExifInfo&&) = default;
+
+ std::string CameraMake;
+ std::string CameraModel;
+ std::string DateTime;
+ int Height{};
+ int Width{};
+ int Orientation{};
+ int IsColor{};
+ int Process{};
+ int FlashUsed{};
+ float FocalLength{};
+ float ExposureTime{};
+ float ApertureFNumber{};
+ float Distance{};
+ float CCDWidth{};
+ float ExposureBias{};
+ float DigitalZoomRatio{};
+ int FocalLength35mmEquiv{};
+ int Whitebalance{};
+ int MeteringMode{};
+ int ExposureProgram{};
+ int ExposureMode{};
+ int ISOequivalent{};
+ int LightSource{};
+ int CommentsCharset{};
+ int XPCommentsCharset{};
+ std::string Comments;
+ std::string FileComment;
+ std::string XPComment;
+ std::string Description;
+
+ unsigned ThumbnailOffset{};
+ unsigned ThumbnailSize{};
+ unsigned LargestExifOffset{};
+
+ char ThumbnailAtEnd{};
+ int ThumbnailSizeOffset{};
+
+ std::vector<int> DateTimeOffsets;
+
+ int GpsInfoPresent{};
+ std::string GpsLat;
+ std::string GpsLong;
+ std::string GpsAlt;
+
+ private:
+ static std::string Convert(int charset, const char* data);
+ };
+
+ struct IPTCInfo
+ {
+ IPTCInfo() = default;
+ IPTCInfo(const IPTCInfo&) = default;
+ IPTCInfo(IPTCInfo&&) = default;
+ IPTCInfo(const IPTCInfo_t& other);
+
+ IPTCInfo& operator=(const IPTCInfo&) = default;
+ IPTCInfo& operator=(IPTCInfo&&) = default;
+
+ std::string RecordVersion;
+ std::string SupplementalCategories;
+ std::string Keywords;
+ std::string Caption;
+ std::string Author;
+ std::string Headline;
+ std::string SpecialInstructions;
+ std::string Category;
+ std::string Byline;
+ std::string BylineTitle;
+ std::string Credit;
+ std::string Source;
+ std::string CopyrightNotice;
+ std::string ObjectName;
+ std::string City;
+ std::string State;
+ std::string Country;
+ std::string TransmissionReference;
+ std::string Date;
+ std::string Urgency;
+ std::string ReferenceService;
+ std::string CountryCode;
+ std::string TimeCreated;
+ std::string SubLocation;
+ std::string ImageType;
+ };
+
+public:
+ CPictureInfoTag() { Reset(); }
+ virtual ~CPictureInfoTag() = default;
+ void Reset();
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& value) const override;
+ void ToSortable(SortItem& sortable, Field field) const override;
+ const std::string GetInfo(int info) const;
+
+ bool Loaded() const { return m_isLoaded; }
+ bool Load(const std::string &path);
+
+ void SetInfo(const std::string& key, const std::string& value);
+
+ /**
+ * GetDateTimeTaken() -- Returns the EXIF DateTimeOriginal for current picture
+ *
+ * The exif library returns DateTimeOriginal if available else the other
+ * DateTime tags. See libexif CExifParse::ProcessDir for details.
+ */
+ const CDateTime& GetDateTimeTaken() const;
+private:
+ static int TranslateString(const std::string &info);
+
+ ExifInfo m_exifInfo;
+ IPTCInfo m_iptcInfo;
+ bool m_isLoaded; // Set to true if metadata has been loaded from the picture file successfully
+ bool m_isInfoSetExternally; // Set to true if metadata has been set by an external call to SetInfo
+ CDateTime m_dateTimeTaken;
+ void ConvertDateTime();
+};
+
diff --git a/xbmc/pictures/PictureScalingAlgorithm.cpp b/xbmc/pictures/PictureScalingAlgorithm.cpp
new file mode 100644
index 0000000..65cfcd9
--- /dev/null
+++ b/xbmc/pictures/PictureScalingAlgorithm.cpp
@@ -0,0 +1,65 @@
+/*
+ * 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 <algorithm>
+
+extern "C" {
+#include <libswscale/swscale.h>
+}
+
+#include "PictureScalingAlgorithm.h"
+#include "utils/StringUtils.h"
+
+CPictureScalingAlgorithm::Algorithm CPictureScalingAlgorithm::Default = CPictureScalingAlgorithm::Bicubic;
+
+CPictureScalingAlgorithm::AlgorithmMap CPictureScalingAlgorithm::m_algorithms = {
+ { FastBilinear, { "fast_bilinear", SWS_FAST_BILINEAR } },
+ { Bilinear, { "bilinear", SWS_BILINEAR } },
+ { Bicubic, { "bicubic", SWS_BICUBIC } },
+ { Experimental, { "experimental", SWS_X } },
+ { NearestNeighbor, { "nearest_neighbor", SWS_POINT } },
+ { AveragingArea, { "averaging_area", SWS_AREA } },
+ { Bicublin, { "bicublin", SWS_BICUBLIN } },
+ { Gaussian, { "gaussian", SWS_GAUSS } },
+ { Sinc, { "sinc", SWS_SINC } },
+ { Lanczos, { "lanczos", SWS_LANCZOS } },
+ { BicubicSpline, { "bicubic_spline", SWS_SPLINE } },
+};
+
+CPictureScalingAlgorithm::Algorithm CPictureScalingAlgorithm::FromString(const std::string& scalingAlgorithm)
+{
+ const auto& algorithm = std::find_if(m_algorithms.begin(), m_algorithms.end(),
+ [&scalingAlgorithm](const std::pair<Algorithm, ScalingAlgorithm>& algo) { return StringUtils::EqualsNoCase(algo.second.name, scalingAlgorithm); });
+ if (algorithm != m_algorithms.end())
+ return algorithm->first;
+
+ return NoAlgorithm;
+}
+
+std::string CPictureScalingAlgorithm::ToString(Algorithm scalingAlgorithm)
+{
+ const auto& algorithm = m_algorithms.find(scalingAlgorithm);
+ if (algorithm != m_algorithms.end())
+ return algorithm->second.name;
+
+ return "";
+}
+
+int CPictureScalingAlgorithm::ToSwscale(const std::string& scalingAlgorithm)
+{
+ return ToSwscale(FromString(scalingAlgorithm));
+}
+
+int CPictureScalingAlgorithm::ToSwscale(Algorithm scalingAlgorithm)
+{
+ const auto& algorithm = m_algorithms.find(scalingAlgorithm);
+ if (algorithm != m_algorithms.end())
+ return algorithm->second.swscale;
+
+ return ToSwscale(Default);
+}
diff --git a/xbmc/pictures/PictureScalingAlgorithm.h b/xbmc/pictures/PictureScalingAlgorithm.h
new file mode 100644
index 0000000..d5aa543
--- /dev/null
+++ b/xbmc/pictures/PictureScalingAlgorithm.h
@@ -0,0 +1,51 @@
+/*
+ * 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 <map>
+#include <string>
+
+class CPictureScalingAlgorithm
+{
+public:
+ typedef enum Algorithm
+ {
+ NoAlgorithm,
+ FastBilinear,
+ Bilinear,
+ Bicubic,
+ Experimental,
+ NearestNeighbor,
+ AveragingArea,
+ Bicublin,
+ Gaussian,
+ Sinc,
+ Lanczos,
+ BicubicSpline
+ } Algorithm;
+
+ static Algorithm Default;
+
+ static Algorithm FromString(const std::string& scalingAlgorithm);
+ static std::string ToString(Algorithm scalingAlgorithm);
+ static int ToSwscale(const std::string& scalingAlgorithm);
+ static int ToSwscale(Algorithm scalingAlgorithm);
+
+private:
+ CPictureScalingAlgorithm();
+
+ typedef struct ScalingAlgorithm
+ {
+ std::string name;
+ int swscale;
+ } ScalingAlgorithm;
+
+ typedef std::map<CPictureScalingAlgorithm::Algorithm, CPictureScalingAlgorithm::ScalingAlgorithm> AlgorithmMap;
+ static AlgorithmMap m_algorithms;
+};
diff --git a/xbmc/pictures/PictureThumbLoader.cpp b/xbmc/pictures/PictureThumbLoader.cpp
new file mode 100644
index 0000000..0cdd56c
--- /dev/null
+++ b/xbmc/pictures/PictureThumbLoader.cpp
@@ -0,0 +1,249 @@
+/*
+ * 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 "PictureThumbLoader.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "Picture.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "URL.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+#include "video/VideoThumbLoader.h"
+
+using namespace XFILE;
+
+CPictureThumbLoader::CPictureThumbLoader() : CThumbLoader(), CJobQueue(true, 1, CJob::PRIORITY_LOW_PAUSABLE)
+{
+ m_regenerateThumbs = false;
+}
+
+CPictureThumbLoader::~CPictureThumbLoader()
+{
+ StopThread();
+}
+
+void CPictureThumbLoader::OnLoaderFinish()
+{
+ m_regenerateThumbs = false;
+ CThumbLoader::OnLoaderFinish();
+}
+
+bool CPictureThumbLoader::LoadItem(CFileItem* pItem)
+{
+ bool result = LoadItemCached(pItem);
+ result |= LoadItemLookup(pItem);
+
+ return result;
+}
+
+bool CPictureThumbLoader::LoadItemCached(CFileItem* pItem)
+{
+ if (pItem->m_bIsShareOrDrive
+ || pItem->IsParentFolder())
+ return false;
+
+ if (pItem->HasArt("thumb") && m_regenerateThumbs)
+ {
+ CServiceBroker::GetTextureCache()->ClearCachedImage(pItem->GetArt("thumb"));
+ if (m_textureDatabase->Open())
+ {
+ m_textureDatabase->ClearTextureForPath(pItem->GetPath(), "thumb");
+ m_textureDatabase->Close();
+ }
+ pItem->SetArt("thumb", "");
+ }
+
+ std::string thumb;
+ if (pItem->IsPicture() && !pItem->IsZIP() && !pItem->IsRAR() && !pItem->IsCBZ() && !pItem->IsCBR() && !pItem->IsPlayList())
+ { // load the thumb from the image file
+ thumb = pItem->HasArt("thumb") ? pItem->GetArt("thumb") : CTextureUtils::GetWrappedThumbURL(pItem->GetPath());
+ }
+ else if (pItem->IsVideo() && !pItem->IsZIP() && !pItem->IsRAR() && !pItem->IsCBZ() && !pItem->IsCBR() && !pItem->IsPlayList())
+ { // video
+ CVideoThumbLoader loader;
+ if (!loader.FillThumb(*pItem))
+ {
+ std::string thumbURL = CVideoThumbLoader::GetEmbeddedThumbURL(*pItem);
+ if (CServiceBroker::GetTextureCache()->HasCachedImage(thumbURL))
+ {
+ thumb = thumbURL;
+ }
+ else if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTTHUMB) && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS))
+ {
+ CFileItem item(*pItem);
+ CThumbExtractor* extract = new CThumbExtractor(item, pItem->GetPath(), true, thumbURL);
+ AddJob(extract);
+ thumb.clear();
+ }
+ }
+ }
+ else if (!pItem->HasArt("thumb"))
+ { // folder, zip, cbz, rar, cbr, playlist may have a previously cached image
+ thumb = GetCachedImage(*pItem, "thumb");
+ }
+ if (!thumb.empty())
+ {
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumb);
+ pItem->SetArt("thumb", thumb);
+ }
+ pItem->FillInDefaultIcon();
+ return true;
+}
+
+bool CPictureThumbLoader::LoadItemLookup(CFileItem* pItem)
+{
+ return false;
+}
+
+void CPictureThumbLoader::OnJobComplete(unsigned int jobID, bool success, CJob* job)
+{
+ if (success)
+ {
+ CThumbExtractor* loader = static_cast<CThumbExtractor*>(job);
+ loader->m_item.SetPath(loader->m_listpath);
+ CFileItemPtr pItem(new CFileItem(loader->m_item));
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, pItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+ CJobQueue::OnJobComplete(jobID, success, job);
+}
+
+void CPictureThumbLoader::ProcessFoldersAndArchives(CFileItem *pItem)
+{
+ if (pItem->HasArt("thumb"))
+ return;
+
+ CTextureDatabase db;
+ db.Open();
+ if (pItem->IsCBR() || pItem->IsCBZ())
+ {
+ std::string strTBN(URIUtils::ReplaceExtension(pItem->GetPath(),".tbn"));
+ if (CFileUtils::Exists(strTBN))
+ {
+ db.SetTextureForPath(pItem->GetPath(), "thumb", strTBN);
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(strTBN);
+ pItem->SetArt("thumb", strTBN);
+ return;
+ }
+ }
+ if ((pItem->m_bIsFolder || pItem->IsCBR() || pItem->IsCBZ()) && !pItem->m_bIsShareOrDrive
+ && !pItem->IsParentFolder() && !pItem->IsPath("add"))
+ {
+ // first check for a folder.jpg
+ std::string thumb = "folder.jpg";
+ CURL pathToUrl = pItem->GetURL();
+ if (pItem->IsCBR())
+ {
+ pathToUrl = URIUtils::CreateArchivePath("rar",pItem->GetURL(),"");
+ thumb = "cover.jpg";
+ }
+ if (pItem->IsCBZ())
+ {
+ pathToUrl = URIUtils::CreateArchivePath("zip",pItem->GetURL(),"");
+ thumb = "cover.jpg";
+ }
+ if (pItem->IsMultiPath())
+ pathToUrl = CURL(CMultiPathDirectory::GetFirstPath(pItem->GetPath()));
+ thumb = URIUtils::AddFileToFolder(pathToUrl.Get(), thumb);
+ if (CFileUtils::Exists(thumb))
+ {
+ db.SetTextureForPath(pItem->GetPath(), "thumb", thumb);
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumb);
+ pItem->SetArt("thumb", thumb);
+ return;
+ }
+ if (!pItem->IsPlugin())
+ {
+ // we load the directory, grab 4 random thumb files (if available) and then generate
+ // the thumb.
+
+ CFileItemList items;
+
+ CDirectory::GetDirectory(pathToUrl, items, CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), DIR_FLAG_NO_FILE_DIRS);
+
+ // create the folder thumb by choosing 4 random thumbs within the folder and putting
+ // them into one thumb.
+ // count the number of images
+ for (int i=0; i < items.Size();)
+ {
+ if (!items[i]->IsPicture() || items[i]->IsZIP() || items[i]->IsRAR() || items[i]->IsPlayList())
+ {
+ items.Remove(i);
+ }
+ else
+ i++;
+ }
+
+ if (items.IsEmpty())
+ {
+ if (pItem->IsCBZ() || pItem->IsCBR())
+ {
+ CDirectory::GetDirectory(pathToUrl, items, CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), DIR_FLAG_NO_FILE_DIRS);
+ for (int i=0;i<items.Size();++i)
+ {
+ CFileItemPtr item = items[i];
+ if (item->m_bIsFolder)
+ {
+ ProcessFoldersAndArchives(item.get());
+ pItem->SetArt("thumb", items[i]->GetArt("thumb"));
+ pItem->SetArt("icon", items[i]->GetArt("icon"));
+ return;
+ }
+ }
+ }
+ return; // no images in this folder
+ }
+
+ // randomize them
+ items.Randomize();
+
+ if (items.Size() < 4 || pItem->IsCBR() || pItem->IsCBZ())
+ { // less than 4 items, so just grab the first thumb
+ items.Sort(SortByLabel, SortOrderAscending);
+ std::string thumb = CTextureUtils::GetWrappedThumbURL(items[0]->GetPath());
+ db.SetTextureForPath(pItem->GetPath(), "thumb", thumb);
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumb);
+ pItem->SetArt("thumb", thumb);
+ }
+ else
+ {
+ // ok, now we've got the files to get the thumbs from, lets create it...
+ // we basically load the 4 images and combine them
+ std::vector<std::string> files;
+ files.reserve(4);
+ for (int thumb = 0; thumb < 4; thumb++)
+ files.push_back(items[thumb]->GetPath());
+ std::string thumb = CTextureUtils::GetWrappedImageURL(pItem->GetPath(), "picturefolder");
+ std::string relativeCacheFile = CTextureCache::GetCacheFile(thumb) + ".png";
+ if (CPicture::CreateTiledThumb(files, CTextureCache::GetCachedPath(relativeCacheFile)))
+ {
+ CTextureDetails details;
+ details.file = relativeCacheFile;
+ details.width = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes;
+ details.height = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes;
+ CServiceBroker::GetTextureCache()->AddCachedTexture(thumb, details);
+ db.SetTextureForPath(pItem->GetPath(), "thumb", thumb);
+ pItem->SetArt("thumb", CTextureCache::GetCachedPath(relativeCacheFile));
+ }
+ }
+ }
+ // refill in the icon to get it to update
+ pItem->FillInDefaultIcon();
+ }
+}
diff --git a/xbmc/pictures/PictureThumbLoader.h b/xbmc/pictures/PictureThumbLoader.h
new file mode 100644
index 0000000..664968e
--- /dev/null
+++ b/xbmc/pictures/PictureThumbLoader.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "ThumbLoader.h"
+#include "utils/JobManager.h"
+
+class CPictureThumbLoader : public CThumbLoader, public CJobQueue
+{
+public:
+ CPictureThumbLoader();
+ ~CPictureThumbLoader() override;
+
+ bool LoadItem(CFileItem* pItem) override;
+ bool LoadItemCached(CFileItem* pItem) override;
+ bool LoadItemLookup(CFileItem* pItem) override;
+ void SetRegenerateThumbs(bool regenerate) { m_regenerateThumbs = regenerate; }
+ static void ProcessFoldersAndArchives(CFileItem *pItem);
+
+ /*!
+ \brief Callback from CThumbExtractor on completion of a generated image
+
+ Performs the callbacks and updates the GUI.
+
+ \sa CImageLoader, IJobCallback
+ */
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+protected:
+ void OnLoaderFinish() override;
+
+private:
+ bool m_regenerateThumbs;
+};
diff --git a/xbmc/pictures/SlideShowPicture.cpp b/xbmc/pictures/SlideShowPicture.cpp
new file mode 100644
index 0000000..63ec8b9
--- /dev/null
+++ b/xbmc/pictures/SlideShowPicture.cpp
@@ -0,0 +1,1016 @@
+/*
+ * 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 "SlideShowPicture.h"
+
+#include "ServiceBroker.h"
+#include "guilib/Texture.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include <mutex>
+
+#ifndef _USE_MATH_DEFINES
+#define _USE_MATH_DEFINES
+#endif
+#include <math.h>
+
+#if defined(HAS_GL)
+#include "rendering/gl/RenderSystemGL.h"
+#include "utils/GLUtils.h"
+#elif defined(HAS_GLES)
+#include "rendering/gles/RenderSystemGLES.h"
+#include "utils/GLUtils.h"
+#elif defined(TARGET_WINDOWS)
+#include "guilib/TextureDX.h"
+#include "rendering/dx/DeviceResources.h"
+#include "rendering/dx/RenderContext.h"
+
+#include <DirectXMath.h>
+using namespace DirectX;
+using namespace Microsoft::WRL;
+#endif
+
+#include <cstddef>
+
+#define IMMEDIATE_TRANSITION_TIME 20
+
+#define PICTURE_MOVE_AMOUNT 0.02f
+#define PICTURE_MOVE_AMOUNT_ANALOG 0.01f
+#define PICTURE_VIEW_BOX_COLOR 0xffffff00 // YELLOW
+#define PICTURE_VIEW_BOX_BACKGROUND 0xff000000 // BLACK
+
+#define FPS 25
+
+static float zoomamount[10] = { 1.0f, 1.2f, 1.5f, 2.0f, 2.8f, 4.0f, 6.0f, 9.0f, 13.5f, 20.0f };
+
+CSlideShowPic::CSlideShowPic() : m_pImage(nullptr)
+{
+ m_bIsLoaded = false;
+ m_bIsFinished = false;
+ m_bDrawNextImage = false;
+ m_bTransitionImmediately = false;
+
+ m_bCanMoveHorizontally = false;
+ m_bCanMoveVertically = false;
+}
+
+CSlideShowPic::~CSlideShowPic()
+{
+ Close();
+}
+
+void CSlideShowPic::Close()
+{
+ std::unique_lock<CCriticalSection> lock(m_textureAccess);
+ m_pImage.reset();
+ m_bIsLoaded = false;
+ m_bIsFinished = false;
+ m_bDrawNextImage = false;
+ m_bTransitionImmediately = false;
+ m_bIsDirty = true;
+ m_alpha = 0;
+#ifdef HAS_DX
+ m_vb = nullptr;
+#endif
+}
+
+void CSlideShowPic::Reset(DISPLAY_EFFECT dispEffect, TRANSITION_EFFECT transEffect)
+{
+ std::unique_lock<CCriticalSection> lock(m_textureAccess);
+ if (m_pImage)
+ SetTexture_Internal(m_iSlideNumber, std::move(m_pImage), dispEffect, transEffect);
+ else
+ Close();
+}
+
+bool CSlideShowPic::DisplayEffectNeedChange(DISPLAY_EFFECT newDispEffect) const
+{
+ if (m_displayEffect == newDispEffect)
+ return false;
+ if (newDispEffect == EFFECT_RANDOM && m_displayEffect != EFFECT_NONE && m_displayEffect != EFFECT_NO_TIMEOUT)
+ return false;
+ return true;
+}
+
+void CSlideShowPic::SetTexture(int iSlideNumber,
+ std::unique_ptr<CTexture> pTexture,
+ DISPLAY_EFFECT dispEffect,
+ TRANSITION_EFFECT transEffect)
+{
+ std::unique_lock<CCriticalSection> lock(m_textureAccess);
+ Close();
+ SetTexture_Internal(iSlideNumber, std::move(pTexture), dispEffect, transEffect);
+}
+
+void CSlideShowPic::SetTexture_Internal(int iSlideNumber,
+ std::unique_ptr<CTexture> pTexture,
+ DISPLAY_EFFECT dispEffect,
+ TRANSITION_EFFECT transEffect)
+{
+ std::unique_lock<CCriticalSection> lock(m_textureAccess);
+ m_bPause = false;
+ m_bNoEffect = false;
+ m_bTransitionImmediately = false;
+ m_iSlideNumber = iSlideNumber;
+
+ m_bIsDirty = true;
+ m_pImage = std::move(pTexture);
+ m_fWidth = static_cast<float>(m_pImage->GetWidth());
+ m_fHeight = static_cast<float>(m_pImage->GetHeight());
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SLIDESHOW_HIGHQUALITYDOWNSCALING))
+ { // activate mipmapping when high quality downscaling is 'on'
+ m_pImage->SetMipmapping();
+ }
+ // reset our counter
+ m_iCounter = 0;
+ // initialize our transition effect
+ m_transitionStart.type = transEffect;
+ m_transitionStart.start = 0;
+
+ // initialize our display effect
+ if (dispEffect == EFFECT_RANDOM)
+ {
+ if (((m_fWidth / m_fHeight) > 1.9f) || ((m_fHeight / m_fWidth) > 1.9f))
+ m_displayEffect = EFFECT_PANORAMA;
+ else
+ m_displayEffect = (DISPLAY_EFFECT)((rand() % (EFFECT_RANDOM - 1)) + 1);
+ }
+ else
+ m_displayEffect = dispEffect;
+
+ // the +1's make sure it actually occurs
+ float fadeTime = 0.2f;
+ if (m_displayEffect != EFFECT_NO_TIMEOUT)
+ fadeTime = std::min(0.2f*CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SLIDESHOW_STAYTIME), 3.0f);
+ m_transitionStart.length = (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS() * fadeTime); // transition time in frames
+ m_transitionEnd.type = transEffect;
+ m_transitionEnd.length = m_transitionStart.length;
+ m_transitionTemp.type = TRANSITION_NONE;
+ m_fTransitionAngle = 0;
+ m_fTransitionZoom = 0;
+ m_fAngle = 0.0f;
+ if (m_pImage->GetOrientation() == 7)
+ { // rotate to 270 degrees
+ m_fAngle = 270.0f;
+ }
+ if (m_pImage->GetOrientation() == 2)
+ { // rotate to 180 degrees
+ m_fAngle = 180.0f;
+ }
+ if (m_pImage->GetOrientation() == 5)
+ { // rotate to 90 degrees
+ m_fAngle = 90.0f;
+ }
+ m_fZoomAmount = 1;
+ m_fZoomLeft = 0;
+ m_fZoomTop = 0;
+ m_fPosX = m_fPosY = 0.0f;
+ m_fPosZ = 1.0f;
+ m_fVelocityX = m_fVelocityY = m_fVelocityZ = 0.0f;
+ int iFrames = std::max((int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS() * CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SLIDESHOW_STAYTIME)), 1);
+ if (m_displayEffect == EFFECT_PANORAMA)
+ {
+ RESOLUTION_INFO res = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ float fScreenWidth = (float)res.Overscan.right - res.Overscan.left;
+ float fScreenHeight = (float)res.Overscan.bottom - res.Overscan.top;
+
+ if (m_fWidth > m_fHeight)
+ {
+ iFrames = (int)(iFrames * (m_fWidth - m_fHeight) / m_fHeight);
+ m_iTotalFrames = m_transitionStart.length + m_transitionEnd.length + iFrames;
+
+ m_fPosX = 0.5f - (fScreenWidth / fScreenHeight) * (m_fHeight / m_fWidth) * 0.5f;
+ if (rand() % 2)
+ m_fPosX = -m_fPosX;
+ m_fVelocityX = -m_fPosX * 2.0f / m_iTotalFrames;
+ }
+ else
+ {
+ iFrames = (int)(iFrames * (m_fHeight - (0.5f * m_fWidth)) / m_fWidth);
+ m_iTotalFrames = m_transitionStart.length + m_transitionEnd.length + iFrames;
+
+ m_fPosY = 0.5f - (fScreenHeight / fScreenWidth) * (m_fWidth / m_fHeight) * 0.5f;
+ if (rand() % 2)
+ m_fPosY = -m_fPosY;
+ m_fVelocityY = -m_fPosY * 2.0f / m_iTotalFrames;
+ }
+ }
+ else
+ {
+ m_iTotalFrames = m_transitionStart.length + m_transitionEnd.length + iFrames;
+
+ if (m_displayEffect == EFFECT_FLOAT)
+ {
+ // Calculate start and end positions
+ // choose a random direction
+ float angle = (rand() % 1000) / 1000.0f * 2 * (float)M_PI;
+ m_fPosX = cos(angle) * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowPanAmount * m_iTotalFrames * 0.00005f;
+ m_fPosY = sin(angle) * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowPanAmount * m_iTotalFrames * 0.00005f;
+ m_fVelocityX = -m_fPosX * 2.0f / m_iTotalFrames;
+ m_fVelocityY = -m_fPosY * 2.0f / m_iTotalFrames;
+ }
+ else if (m_displayEffect == EFFECT_ZOOM)
+ {
+ m_fPosZ = 1.0f;
+ m_fVelocityZ = 0.0001f * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowZoomAmount;
+ }
+ }
+
+ m_transitionEnd.start = m_transitionStart.length + iFrames;
+
+ m_bIsFinished = false;
+ m_bDrawNextImage = false;
+ m_bIsLoaded = true;
+}
+
+void CSlideShowPic::SetOriginalSize(int iOriginalWidth, int iOriginalHeight, bool bFullSize)
+{
+ m_iOriginalWidth = iOriginalWidth;
+ m_iOriginalHeight = iOriginalHeight;
+ m_bFullSize = bFullSize;
+}
+
+int CSlideShowPic::GetOriginalWidth()
+{
+ int iAngle = (int)(m_fAngle / 90.0f + 0.4f);
+ if (iAngle % 2)
+ return m_iOriginalHeight;
+ else
+ return m_iOriginalWidth;
+}
+
+int CSlideShowPic::GetOriginalHeight()
+{
+ int iAngle = (int)(m_fAngle / 90.0f + 0.4f);
+ if (iAngle % 2)
+ return m_iOriginalWidth;
+ else
+ return m_iOriginalHeight;
+}
+
+void CSlideShowPic::UpdateTexture(std::unique_ptr<CTexture> pTexture)
+{
+ std::unique_lock<CCriticalSection> lock(m_textureAccess);
+ m_pImage = std::move(pTexture);
+ m_fWidth = static_cast<float>(m_pImage->GetWidth());
+ m_fHeight = static_cast<float>(m_pImage->GetHeight());
+ m_bIsDirty = true;
+}
+
+static CRect GetRectangle(const float x[4], const float y[4])
+{
+ CRect rect;
+ rect.x1 = *std::min_element(x, x+4);
+ rect.y1 = *std::min_element(y, y+4);
+ rect.x2 = *std::max_element(x, x+4);
+ rect.y2 = *std::max_element(y, y+4);
+ return rect;
+}
+
+void CSlideShowPic::UpdateVertices(float cur_x[4], float cur_y[4], const float new_x[4], const float new_y[4], CDirtyRegionList &dirtyregions)
+{
+ const size_t count = sizeof(float)*4;
+ if(memcmp(cur_x, new_x, count)
+ || memcmp(cur_y, new_y, count)
+ || m_bIsDirty)
+ {
+ dirtyregions.push_back(CDirtyRegion(GetRectangle(cur_x, cur_y)));
+ dirtyregions.push_back(CDirtyRegion(GetRectangle(new_x, new_y)));
+ memcpy(cur_x, new_x, count);
+ memcpy(cur_y, new_y, count);
+ }
+}
+
+void CSlideShowPic::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (!m_pImage || !m_bIsLoaded || m_bIsFinished) return ;
+ UTILS::COLOR::Color alpha = m_alpha;
+ if (m_iCounter <= m_transitionStart.length)
+ { // do start transition
+ if (m_transitionStart.type == CROSSFADE)
+ { // fade in at 1x speed
+ alpha = (UTILS::COLOR::Color)((float)m_iCounter / (float)m_transitionStart.length * 255.0f);
+ }
+ else if (m_transitionStart.type == FADEIN_FADEOUT)
+ { // fade in at 2x speed, then keep solid
+ alpha =
+ (UTILS::COLOR::Color)((float)m_iCounter / (float)m_transitionStart.length * 255.0f * 2);
+ if (alpha > 255) alpha = 255;
+ }
+ else // m_transitionEffect == TRANSITION_NONE
+ {
+ alpha = 0xFF; // opaque
+ }
+ }
+ bool bPaused = m_bPause | (m_fZoomAmount != 1.0f);
+ // check if we're doing a temporary effect (such as rotate + zoom)
+ if (m_transitionTemp.type != TRANSITION_NONE)
+ {
+ bPaused = true;
+ if (m_iCounter >= m_transitionTemp.start)
+ {
+ if (m_iCounter >= m_transitionTemp.start + m_transitionTemp.length)
+ { // we're finished this transition
+ if (m_transitionTemp.type == TRANSITION_ZOOM)
+ { // correct for any introduced inaccuracies.
+ int i;
+ for (i = 0; i < 10; i++)
+ {
+ if (fabs(m_fZoomAmount - zoomamount[i]) < 0.01f * zoomamount[i])
+ {
+ m_fZoomAmount = zoomamount[i];
+ break;
+ }
+ }
+ m_bNoEffect = (m_fZoomAmount != 1.0f); // turn effect rendering back on.
+ }
+ m_transitionTemp.type = TRANSITION_NONE;
+ }
+ else
+ {
+ if (m_transitionTemp.type == TRANSITION_ROTATE)
+ m_fAngle += m_fTransitionAngle;
+ if (m_transitionTemp.type == TRANSITION_ZOOM)
+ m_fZoomAmount += m_fTransitionZoom;
+ }
+ }
+ }
+ // now just display
+ if (!m_bNoEffect && !bPaused)
+ {
+ if (m_displayEffect == EFFECT_PANORAMA)
+ {
+ m_fPosX += m_fVelocityX;
+ m_fPosY += m_fVelocityY;
+ }
+ else if (m_displayEffect == EFFECT_FLOAT)
+ {
+ m_fPosX += m_fVelocityX;
+ m_fPosY += m_fVelocityY;
+ float fMoveAmount = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowPanAmount * m_iTotalFrames * 0.0001f;
+ if (m_fPosX > fMoveAmount)
+ {
+ m_fPosX = fMoveAmount;
+ m_fVelocityX = -m_fVelocityX;
+ }
+ if (m_fPosX < -fMoveAmount)
+ {
+ m_fPosX = -fMoveAmount;
+ m_fVelocityX = -m_fVelocityX;
+ }
+ if (m_fPosY > fMoveAmount)
+ {
+ m_fPosY = fMoveAmount;
+ m_fVelocityY = -m_fVelocityY;
+ }
+ if (m_fPosY < -fMoveAmount)
+ {
+ m_fPosY = -fMoveAmount;
+ m_fVelocityY = -m_fVelocityY;
+ }
+ }
+ else if (m_displayEffect == EFFECT_ZOOM)
+ {
+ m_fPosZ += m_fVelocityZ;
+/* if (m_fPosZ > 1.0f + 0.01f*CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt("Slideshow.ZoomAmount"))
+ {
+ m_fPosZ = 1.0f + 0.01f * CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt("Slideshow.ZoomAmount");
+ m_fVelocityZ = -m_fVelocityZ;
+ }
+ if (m_fPosZ < 1.0f)
+ {
+ m_fPosZ = 1.0f;
+ m_fVelocityZ = -m_fVelocityZ;
+ }*/
+ }
+ }
+ if (m_displayEffect != EFFECT_NO_TIMEOUT && bPaused && !m_bTransitionImmediately)
+ { // paused - increment the last transition start time
+ m_transitionEnd.start++;
+ }
+ if (m_iCounter >= m_transitionEnd.start)
+ { // do end transition
+// CLog::Log(LOGDEBUG,"Transitioning");
+ m_bDrawNextImage = true;
+ if (m_transitionEnd.type == CROSSFADE)
+ { // fade out at 1x speed
+ alpha = 255 - (UTILS::COLOR::Color)((float)(m_iCounter - m_transitionEnd.start) /
+ (float)m_transitionEnd.length * 255.0f);
+ }
+ else if (m_transitionEnd.type == FADEIN_FADEOUT)
+ { // keep solid, then fade out at 2x speed
+ alpha = (UTILS::COLOR::Color)(
+ (float)(m_transitionEnd.length - m_iCounter + m_transitionEnd.start) /
+ (float)m_transitionEnd.length * 255.0f * 2);
+ if (alpha > 255) alpha = 255;
+ }
+ else // m_transitionEffect == TRANSITION_NONE
+ {
+ alpha = 0xFF; // opaque
+ }
+ }
+ if (alpha != m_alpha)
+ {
+ m_alpha = alpha;
+ m_bIsDirty = true;
+ }
+ if (m_displayEffect != EFFECT_NO_TIMEOUT || m_iCounter < m_transitionStart.length || m_iCounter >= m_transitionEnd.start || (m_iCounter >= m_transitionTemp.start && m_iCounter < m_transitionTemp.start + m_transitionTemp.length))
+ {
+ /* this really annoying. there's non-stop logging when viewing a pic outside of the slideshow
+ if (m_displayEffect == EFFECT_NO_TIMEOUT)
+ CLog::Log(LOGDEBUG, "Incrementing counter ({}) while not in slideshow (startlength={},endstart={},endlength={})", m_iCounter, m_transitionStart.length, m_transitionEnd.start, m_transitionEnd.length);
+ */
+ m_iCounter++;
+ }
+ if (m_iCounter > m_transitionEnd.start + m_transitionEnd.length)
+ m_bIsFinished = true;
+
+ RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+
+ // calculate where we should render (and how large it should be)
+ // calculate aspect ratio correction factor
+ float fOffsetX = (float)info.Overscan.left;
+ float fOffsetY = (float)info.Overscan.top;
+ float fScreenWidth = (float)info.Overscan.right - info.Overscan.left;
+ float fScreenHeight = (float)info.Overscan.bottom - info.Overscan.top;
+ float fPixelRatio = info.fPixelRatio;
+
+ // Rotate the image as needed
+ float x[4];
+ float y[4];
+ float si = sin(m_fAngle / 180.0f * static_cast<float>(M_PI));
+ float co = cos(m_fAngle / 180.0f * static_cast<float>(M_PI));
+ x[0] = -m_fWidth * co + m_fHeight * si;
+ y[0] = -m_fWidth * si - m_fHeight * co;
+ x[1] = m_fWidth * co + m_fHeight * si;
+ y[1] = m_fWidth * si - m_fHeight * co;
+ x[2] = m_fWidth * co - m_fHeight * si;
+ y[2] = m_fWidth * si + m_fHeight * co;
+ x[3] = -m_fWidth * co - m_fHeight * si;
+ y[3] = -m_fWidth * si + m_fHeight * co;
+
+ // calculate our scale amounts
+ float fSourceAR = m_fWidth / m_fHeight;
+ float fSourceInvAR = 1 / fSourceAR;
+ float fAR = si * si * (fSourceInvAR - fSourceAR) + fSourceAR;
+
+ //float fOutputFrameAR = fAR / fPixelRatio;
+
+ float fScaleNorm = fScreenWidth / m_fWidth;
+ float fScaleInv = fScreenWidth / m_fHeight;
+
+ bool bFillScreen = false;
+ float fComp = 1.0f + 0.01f * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowBlackBarCompensation;
+ float fScreenRatio = fScreenWidth / fScreenHeight * fPixelRatio;
+ // work out if we should be compensating the zoom to minimize blackbars
+ // we should compute this based on the % of black bars on screen perhaps??
+ //! @todo change m_displayEffect != EFFECT_NO_TIMEOUT to whether we're running the slideshow
+ if (m_displayEffect != EFFECT_NO_TIMEOUT && m_displayEffect != EFFECT_NONE && fScreenRatio < fSourceAR * fComp && fSourceAR < fScreenRatio * fComp)
+ bFillScreen = true;
+ if ((!bFillScreen && fScreenWidth*fPixelRatio > fScreenHeight*fSourceAR) || (bFillScreen && fScreenWidth*fPixelRatio < fScreenHeight*fSourceAR))
+ fScaleNorm = fScreenHeight / (m_fHeight * fPixelRatio);
+ bFillScreen = false;
+ if (m_displayEffect != EFFECT_NO_TIMEOUT && m_displayEffect != EFFECT_NONE && fScreenRatio < fSourceInvAR * fComp && fSourceInvAR < fScreenRatio * fComp)
+ bFillScreen = true;
+ if ((!bFillScreen && fScreenWidth*fPixelRatio > fScreenHeight*fSourceInvAR) || (bFillScreen && fScreenWidth*fPixelRatio < fScreenHeight*fSourceInvAR))
+ fScaleInv = fScreenHeight / (m_fWidth * fPixelRatio);
+
+ float fScale = si * si * (fScaleInv - fScaleNorm) + fScaleNorm;
+ // scale if we need to due to the effect we're using
+ if (m_displayEffect == EFFECT_PANORAMA)
+ {
+ if (m_fWidth > m_fHeight)
+ fScale *= m_fWidth / fScreenWidth * fScreenHeight / m_fHeight;
+ else
+ fScale *= m_fHeight / fScreenHeight * fScreenWidth / m_fWidth;
+ }
+ if (m_displayEffect == EFFECT_FLOAT)
+ fScale *= (1.0f + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowPanAmount * m_iTotalFrames * 0.0001f);
+ if (m_displayEffect == EFFECT_ZOOM)
+ fScale *= m_fPosZ;
+ // zoom image
+ fScale *= m_fZoomAmount;
+
+ // calculate the resultant coordinates
+ for (int i = 0; i < 4; i++)
+ {
+ x[i] *= fScale * 0.5f; // as the offsets x[] and y[] are from center
+ y[i] *= fScale * fPixelRatio * 0.5f;
+ // center it
+ x[i] += 0.5f * fScreenWidth + fOffsetX;
+ y[i] += 0.5f * fScreenHeight + fOffsetY;
+ }
+ // shift if we're zooming
+ if (m_fZoomAmount > 1)
+ {
+ float minx = x[0];
+ float maxx = x[0];
+ float miny = y[0];
+ float maxy = y[0];
+ for (int i = 1; i < 4; i++)
+ {
+ if (x[i] < minx) minx = x[i];
+ if (x[i] > maxx) maxx = x[i];
+ if (y[i] < miny) miny = y[i];
+ if (y[i] > maxy) maxy = y[i];
+ }
+ float w = maxx - minx;
+ float h = maxy - miny;
+ m_bCanMoveHorizontally = (w >= fScreenWidth);
+ m_bCanMoveVertically = (h >= fScreenHeight);
+ if (w >= fScreenWidth)
+ { // must have no black bars
+ if (minx + m_fZoomLeft*w > fOffsetX)
+ m_fZoomLeft = (fOffsetX - minx) / w;
+ if (maxx + m_fZoomLeft*w < fOffsetX + fScreenWidth)
+ m_fZoomLeft = (fScreenWidth + fOffsetX - maxx) / w;
+ for (float& i : x)
+ i += w * m_fZoomLeft;
+ }
+ if (h >= fScreenHeight)
+ { // must have no black bars
+ if (miny + m_fZoomTop*h > fOffsetY)
+ m_fZoomTop = (fOffsetY - miny) / h;
+ if (maxy + m_fZoomTop*h < fOffsetY + fScreenHeight)
+ m_fZoomTop = (fScreenHeight + fOffsetY - maxy) / h;
+ for (float& i : y)
+ i += m_fZoomTop * h;
+ }
+ }
+ // add offset from display effects
+ for (int i = 0; i < 4; i++)
+ {
+ x[i] += m_fPosX * m_fWidth * fScale;
+ y[i] += m_fPosY * m_fHeight * fScale;
+ }
+
+ UpdateVertices(m_ax, m_ay, x, y, dirtyregions);
+
+ // now render the image in the top right corner if we're zooming
+ if (m_fZoomAmount == 1 || m_bIsComic)
+ {
+ const float empty[4] = {};
+ UpdateVertices(m_bx, m_by, empty, empty, dirtyregions);
+ UpdateVertices(m_sx, m_sy, empty, empty, dirtyregions);
+ UpdateVertices(m_ox, m_oy, empty, empty, dirtyregions);
+ m_bIsDirty = false;
+ return;
+ }
+
+ float sx[4], sy[4];
+ sx[0] = -m_fWidth * co + m_fHeight * si;
+ sy[0] = -m_fWidth * si - m_fHeight * co;
+ sx[1] = m_fWidth * co + m_fHeight * si;
+ sy[1] = m_fWidth * si - m_fHeight * co;
+ sx[2] = m_fWidth * co - m_fHeight * si;
+ sy[2] = m_fWidth * si + m_fHeight * co;
+ sx[3] = -m_fWidth * co - m_fHeight * si;
+ sy[3] = -m_fWidth * si + m_fHeight * co;
+ // convert to the appropriate scale
+ float fSmallArea = fScreenWidth * fScreenHeight / 50;
+ float fSmallWidth = sqrt(fSmallArea * fAR / fPixelRatio); // fAR*height = width, so total area*far = width*width
+ float fSmallHeight = fSmallArea / fSmallWidth;
+ float fSmallX = fOffsetX + fScreenWidth * 0.95f - fSmallWidth * 0.5f;
+ float fSmallY = fOffsetY + fScreenHeight * 0.05f + fSmallHeight * 0.5f;
+ fScaleNorm = fSmallWidth / m_fWidth;
+ fScaleInv = fSmallWidth / m_fHeight;
+ fScale = si * si * (fScaleInv - fScaleNorm) + fScaleNorm;
+ for (int i = 0; i < 4; i++)
+ {
+ sx[i] *= fScale * 0.5f;
+ sy[i] *= fScale * fPixelRatio * 0.5f;
+ }
+ // calculate a black border
+ float bx[4];
+ float by[4];
+ for (int i = 0; i < 4; i++)
+ {
+ if (sx[i] > 0)
+ bx[i] = sx[i] + 1;
+ else
+ bx[i] = sx[i] - 1;
+ if (sy[i] > 0)
+ by[i] = sy[i] + 1;
+ else
+ by[i] = sy[i] - 1;
+ sx[i] += fSmallX;
+ sy[i] += fSmallY;
+ bx[i] += fSmallX;
+ by[i] += fSmallY;
+ }
+
+ fSmallX -= fSmallWidth * 0.5f;
+ fSmallY -= fSmallHeight * 0.5f;
+
+ UpdateVertices(m_bx, m_by, bx, by, dirtyregions);
+ UpdateVertices(m_sx, m_sy, sx, sy, dirtyregions);
+
+ // now we must render the wireframe image of the view window
+ // work out the direction of the top of pic vector
+ float scale;
+ if (fabs(x[1] - x[0]) > fabs(x[3] - x[0]))
+ scale = (sx[1] - sx[0]) / (x[1] - x[0]);
+ else
+ scale = (sx[3] - sx[0]) / (x[3] - x[0]);
+ float ox[4];
+ float oy[4];
+ ox[0] = (fOffsetX - x[0]) * scale + sx[0];
+ oy[0] = (fOffsetY - y[0]) * scale + sy[0];
+ ox[1] = (fOffsetX + fScreenWidth - x[0]) * scale + sx[0];
+ oy[1] = (fOffsetY - y[0]) * scale + sy[0];
+ ox[2] = (fOffsetX + fScreenWidth - x[0]) * scale + sx[0];
+ oy[2] = (fOffsetY + fScreenHeight - y[0]) * scale + sy[0];
+ ox[3] = (fOffsetX - x[0]) * scale + sx[0];
+ oy[3] = (fOffsetY + fScreenHeight - y[0]) * scale + sy[0];
+ // crop to within the range of our piccy
+ for (int i = 0; i < 4; i++)
+ {
+ if (ox[i] < fSmallX) ox[i] = fSmallX;
+ if (ox[i] > fSmallX + fSmallWidth) ox[i] = fSmallX + fSmallWidth;
+ if (oy[i] < fSmallY) oy[i] = fSmallY;
+ if (oy[i] > fSmallY + fSmallHeight) oy[i] = fSmallY + fSmallHeight;
+ }
+
+ UpdateVertices(m_ox, m_oy, ox, oy, dirtyregions);
+ m_bIsDirty = false;
+}
+
+void CSlideShowPic::Keep()
+{
+ // this is called if we need to keep the current pic on screen
+ // to wait for the next pic to load
+ if (!m_bDrawNextImage) return ; // don't need to keep pic
+ // hold off the start of the next frame
+ m_transitionEnd.start = m_iCounter;
+}
+
+bool CSlideShowPic::StartTransition()
+{
+ // this is called if we need to start transitioning immediately to the new picture
+ if (m_bDrawNextImage) return false; // don't need to do anything as we are already transitioning
+ // decrease the number of display frame
+ m_transitionEnd.start = m_iCounter;
+ m_bTransitionImmediately = true;
+ return true;
+}
+
+void CSlideShowPic::Pause(bool bPause)
+{
+ if (!m_bDrawNextImage)
+ m_bPause = bPause;
+}
+
+void CSlideShowPic::SetInSlideshow(bool slideshow)
+{
+ if (slideshow && m_displayEffect == EFFECT_NO_TIMEOUT)
+ m_displayEffect = EFFECT_NONE;
+}
+
+int CSlideShowPic::GetTransitionTime(int iType) const
+{
+ if (iType == 0) // start transition
+ return m_transitionStart.length;
+ else // iType == 1 // end transition
+ return m_transitionEnd.length;
+}
+
+void CSlideShowPic::SetTransitionTime(int iType, int iTime)
+{
+ if (iType == 0) // start transition
+ m_transitionStart.length = iTime;
+ else // iType == 1 // end transition
+ m_transitionEnd.length = iTime;
+}
+
+void CSlideShowPic::Rotate(float fRotateAngle, bool immediate /* = false */)
+{
+ if (m_bDrawNextImage) return;
+ if (m_transitionTemp.type == TRANSITION_ZOOM) return;
+ if (immediate)
+ {
+ m_fAngle += fRotateAngle;
+ return;
+ }
+
+ // if there is a rotation ongoing already
+ // add the new angle to the old destination angle
+ if (m_transitionTemp.type == TRANSITION_ROTATE &&
+ m_transitionTemp.start + m_transitionTemp.length > m_iCounter)
+ {
+ int remainder = m_transitionTemp.start + m_transitionTemp.length - m_iCounter;
+ fRotateAngle += m_fTransitionAngle * remainder;
+ }
+
+ m_transitionTemp.type = TRANSITION_ROTATE;
+ m_transitionTemp.start = m_iCounter;
+ m_transitionTemp.length = IMMEDIATE_TRANSITION_TIME;
+ m_fTransitionAngle = fRotateAngle / (float)m_transitionTemp.length;
+ // reset the timer
+ m_transitionEnd.start = m_iCounter + m_transitionStart.length + (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS() * CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SLIDESHOW_STAYTIME));
+}
+
+void CSlideShowPic::Zoom(float fZoom, bool immediate /* = false */)
+{
+ if (m_bDrawNextImage) return;
+ if (m_transitionTemp.type == TRANSITION_ROTATE) return;
+ if (immediate)
+ {
+ m_fZoomAmount = fZoom;
+ return;
+ }
+ m_transitionTemp.type = TRANSITION_ZOOM;
+ m_transitionTemp.start = m_iCounter;
+ m_transitionTemp.length = IMMEDIATE_TRANSITION_TIME;
+ m_fTransitionZoom = (fZoom - m_fZoomAmount) / (float)m_transitionTemp.length;
+ // reset the timer
+ m_transitionEnd.start = m_iCounter + m_transitionStart.length + (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS() * CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SLIDESHOW_STAYTIME));
+ // turn off the render effects until we're back down to normal zoom
+ m_bNoEffect = true;
+}
+
+void CSlideShowPic::Move(float fDeltaX, float fDeltaY)
+{
+ m_fZoomLeft += fDeltaX;
+ m_fZoomTop += fDeltaY;
+ // reset the timer
+ // m_transitionEnd.start = m_iCounter + m_transitionStart.length + (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS() * CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SLIDESHOW_STAYTIME));
+}
+
+void CSlideShowPic::Render()
+{
+ std::unique_lock<CCriticalSection> lock(m_textureAccess);
+
+ Render(m_ax, m_ay, m_pImage.get(), (m_alpha << 24) | 0xFFFFFF);
+
+ // now render the image in the top right corner if we're zooming
+ if (m_fZoomAmount == 1.0f || m_bIsComic) return ;
+
+ Render(m_bx, m_by, NULL, PICTURE_VIEW_BOX_BACKGROUND);
+ Render(m_sx, m_sy, m_pImage.get(), 0xFFFFFFFF);
+ Render(m_ox, m_oy, NULL, PICTURE_VIEW_BOX_COLOR);
+}
+
+#ifdef HAS_DX
+bool CSlideShowPic::UpdateVertexBuffer(Vertex* vertices)
+{
+ if (!m_vb) // create new
+ {
+ CD3D11_BUFFER_DESC desc(sizeof(Vertex) * 5, D3D11_BIND_VERTEX_BUFFER, D3D11_USAGE_DYNAMIC, D3D11_CPU_ACCESS_WRITE);
+ D3D11_SUBRESOURCE_DATA initData = {};
+ initData.pSysMem = vertices;
+ initData.SysMemPitch = sizeof(Vertex) * 5;
+ if (SUCCEEDED(DX::DeviceResources::Get()->GetD3DDevice()->CreateBuffer(&desc, &initData, m_vb.ReleaseAndGetAddressOf())))
+ return true;
+ }
+ else // update
+ {
+ ID3D11DeviceContext* pContext = DX::DeviceResources::Get()->GetD3DContext();
+ D3D11_MAPPED_SUBRESOURCE res;
+ if (SUCCEEDED(pContext->Map(m_vb.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &res)))
+ {
+ memcpy(res.pData, vertices, sizeof(Vertex) * 5);
+ pContext->Unmap(m_vb.Get(), 0);
+ return true;
+ }
+ }
+
+ return false;
+}
+#endif
+
+void CSlideShowPic::Render(float* x, float* y, CTexture* pTexture, UTILS::COLOR::Color color)
+{
+#ifdef HAS_DX
+ Vertex vertex[5];
+ for (int i = 0; i < 4; i++)
+ {
+ vertex[i].pos = XMFLOAT3( x[i], y[i], 0);
+ CD3DHelper::XMStoreColor(&vertex[i].color, color);
+ vertex[i].texCoord = XMFLOAT2(0.0f, 0.0f);
+ vertex[i].texCoord2 = XMFLOAT2(0.0f, 0.0f);
+ }
+
+ if (pTexture)
+ {
+ vertex[1].texCoord.x = vertex[2].texCoord.x = (float) pTexture->GetWidth() / pTexture->GetTextureWidth();
+ vertex[2].texCoord.y = vertex[3].texCoord.y = (float) pTexture->GetHeight() / pTexture->GetTextureHeight();
+ }
+ else
+ {
+ vertex[1].texCoord.x = vertex[2].texCoord.x = 1.0f;
+ vertex[2].texCoord.y = vertex[3].texCoord.y = 1.0f;
+ }
+ vertex[4] = vertex[0]; // Not used when pTexture != NULL
+
+ CGUIShaderDX* pGUIShader = DX::Windowing()->GetGUIShader();
+ pGUIShader->Begin(SHADER_METHOD_RENDER_TEXTURE_BLEND);
+
+ // Set state to render the image
+ if (pTexture)
+ {
+ pTexture->LoadToGPU();
+ CDXTexture* dxTexture = reinterpret_cast<CDXTexture*>(pTexture);
+ ID3D11ShaderResourceView* shaderRes = dxTexture->GetShaderResource();
+ pGUIShader->SetShaderViews(1, &shaderRes);
+ pGUIShader->DrawQuad(vertex[0], vertex[1], vertex[2], vertex[3]);
+ }
+ else
+ {
+ if (!UpdateVertexBuffer(vertex))
+ return;
+
+ ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext();
+
+ unsigned stride = sizeof(Vertex);
+ unsigned offset = 0;
+ pContext->IASetVertexBuffers(0, 1, m_vb.GetAddressOf(), &stride, &offset);
+ pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP);
+
+ pGUIShader->Draw(5, 0);
+ pGUIShader->RestoreBuffers();
+ }
+
+#elif defined(HAS_GL)
+ CRenderSystemGL *renderSystem = dynamic_cast<CRenderSystemGL*>(CServiceBroker::GetRenderSystem());
+ if (pTexture)
+ {
+ pTexture->LoadToGPU();
+ pTexture->BindToUnit(0);
+
+ glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
+ glEnable(GL_BLEND);
+
+ renderSystem->EnableShader(ShaderMethodGL::SM_TEXTURE);
+ }
+ else
+ {
+ renderSystem->EnableShader(ShaderMethodGL::SM_DEFAULT);
+ }
+
+ float u1 = 0, u2 = 1, v1 = 0, v2 = 1;
+ if (pTexture)
+ {
+ u2 = (float)pTexture->GetWidth() / pTexture->GetTextureWidth();
+ v2 = (float)pTexture->GetHeight() / pTexture->GetTextureHeight();
+ }
+
+ GLubyte colour[4];
+ GLubyte idx[4] = {0, 1, 3, 2}; //determines order of the vertices
+ GLuint vertexVBO;
+ GLuint indexVBO;
+ struct PackedVertex
+ {
+ float x, y, z;
+ float u1, v1;
+ } vertex[4];
+
+ // Setup vertex position values
+ vertex[0].x = x[0];
+ vertex[0].y = y[0];
+ vertex[0].z = 0;
+ vertex[0].u1 = u1;
+ vertex[0].v1 = v1;
+
+ vertex[1].x = x[1];
+ vertex[1].y = y[1];
+ vertex[1].z = 0;
+ vertex[1].u1 = u2;
+ vertex[1].v1 = v1;
+
+ vertex[2].x = x[2];
+ vertex[2].y = y[2];
+ vertex[2].z = 0;
+ vertex[2].u1 = u2;
+ vertex[2].v1 = v2;
+
+ vertex[3].x = x[3];
+ vertex[3].y = y[3];
+ vertex[3].z = 0;
+ vertex[3].u1 = u1;
+ vertex[3].v1 = v2;
+
+ GLint posLoc = renderSystem->ShaderGetPos();
+ GLint tex0Loc = renderSystem->ShaderGetCoord0();
+ GLint uniColLoc= renderSystem->ShaderGetUniCol();
+
+ glGenBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex)*4, &vertex[0], GL_STATIC_DRAW);
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x)));
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, sizeof(PackedVertex),
+ reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1)));
+
+ glEnableVertexAttribArray(posLoc);
+ glEnableVertexAttribArray(tex0Loc);
+
+ // Setup Colour values
+ colour[0] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color);
+ colour[1] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color);
+ colour[2] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color);
+ colour[3] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color);
+
+ glUniform4f(uniColLoc,(colour[0] / 255.0f), (colour[1] / 255.0f),
+ (colour[2] / 255.0f), (colour[3] / 255.0f));
+
+ glGenBuffers(1, &indexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVBO);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte)*4, idx, GL_STATIC_DRAW);
+
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0);
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(tex0Loc);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &vertexVBO);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+ glDeleteBuffers(1, &indexVBO);
+
+ renderSystem->DisableShader();
+
+#elif defined(HAS_GLES)
+ CRenderSystemGLES *renderSystem = dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem());
+ if (pTexture)
+ {
+ pTexture->LoadToGPU();
+ pTexture->BindToUnit(0);
+
+ glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
+ glEnable(GL_BLEND); // Turn Blending On
+
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_TEXTURE);
+ }
+ else
+ {
+ renderSystem->EnableGUIShader(ShaderMethodGLES::SM_DEFAULT);
+ }
+
+ float u1 = 0, u2 = 1, v1 = 0, v2 = 1;
+ if (pTexture)
+ {
+ u2 = (float)pTexture->GetWidth() / pTexture->GetTextureWidth();
+ v2 = (float)pTexture->GetHeight() / pTexture->GetTextureHeight();
+ }
+
+ GLubyte col[4];
+ GLfloat ver[4][3];
+ GLfloat tex[4][2];
+ GLubyte idx[4] = {0, 1, 3, 2}; //determines order of triangle strip
+
+ GLint posLoc = renderSystem->GUIShaderGetPos();
+ GLint tex0Loc = renderSystem->GUIShaderGetCoord0();
+ GLint uniColLoc= renderSystem->GUIShaderGetUniCol();
+
+ glVertexAttribPointer(posLoc, 3, GL_FLOAT, 0, 0, ver);
+ glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, 0, tex);
+
+ glEnableVertexAttribArray(posLoc);
+ glEnableVertexAttribArray(tex0Loc);
+
+ // Setup Colour values
+ col[0] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color);
+ col[1] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color);
+ col[2] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color);
+ col[3] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color);
+
+ if (CServiceBroker::GetWinSystem()->UseLimitedColor())
+ {
+ col[0] = (235 - 16) * col[0] / 255 + 16;
+ col[1] = (235 - 16) * col[1] / 255 + 16;
+ col[2] = (235 - 16) * col[2] / 255 + 16;
+ }
+
+ for (int i=0; i<4; i++)
+ {
+ // Setup vertex position values
+ ver[i][0] = x[i];
+ ver[i][1] = y[i];
+ ver[i][2] = 0.0f;
+ }
+ // Setup texture coordinates
+ tex[0][0] = tex[3][0] = u1;
+ tex[0][1] = tex[1][1] = v1;
+ tex[1][0] = tex[2][0] = u2;
+ tex[2][1] = tex[3][1] = v2;
+
+ glUniform4f(uniColLoc,(col[0] / 255.0f), (col[1] / 255.0f), (col[2] / 255.0f), (col[3] / 255.0f));
+ glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, idx);
+
+ glDisableVertexAttribArray(posLoc);
+ glDisableVertexAttribArray(tex0Loc);
+
+ renderSystem->DisableGUIShader();
+
+#endif
+}
diff --git a/xbmc/pictures/SlideShowPicture.h b/xbmc/pictures/SlideShowPicture.h
new file mode 100644
index 0000000..b557cc5
--- /dev/null
+++ b/xbmc/pictures/SlideShowPicture.h
@@ -0,0 +1,139 @@
+/*
+ * 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 "guilib/DirtyRegion.h"
+#include "threads/CriticalSection.h"
+#include "utils/ColorUtils.h"
+
+#include <string>
+#ifdef HAS_DX
+#include "guilib/GUIShaderDX.h"
+#include <wrl/client.h>
+#endif
+
+#include <memory>
+
+class CTexture;
+
+class CSlideShowPic
+{
+public:
+ enum DISPLAY_EFFECT { EFFECT_NONE = 0, EFFECT_FLOAT, EFFECT_ZOOM, EFFECT_RANDOM, EFFECT_PANORAMA, EFFECT_NO_TIMEOUT };
+ enum TRANSITION_EFFECT { TRANSITION_NONE = 0, FADEIN_FADEOUT, CROSSFADE, TRANSITION_ZOOM, TRANSITION_ROTATE };
+
+ struct TRANSITION
+ {
+ TRANSITION_EFFECT type = TRANSITION_NONE;
+ int start = 0;
+ int length = 0;
+ };
+
+ CSlideShowPic();
+ ~CSlideShowPic();
+
+ void SetTexture(int iSlideNumber,
+ std::unique_ptr<CTexture> pTexture,
+ DISPLAY_EFFECT dispEffect = EFFECT_RANDOM,
+ TRANSITION_EFFECT transEffect = FADEIN_FADEOUT);
+ void UpdateTexture(std::unique_ptr<CTexture> pTexture);
+
+ bool IsLoaded() const { return m_bIsLoaded; }
+ void UnLoad() { m_bIsLoaded = false; }
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions);
+ void Render();
+ void Close();
+ void Reset(DISPLAY_EFFECT dispEffect = EFFECT_RANDOM, TRANSITION_EFFECT transEffect = FADEIN_FADEOUT);
+ DISPLAY_EFFECT DisplayEffect() const { return m_displayEffect; }
+ bool DisplayEffectNeedChange(DISPLAY_EFFECT newDispEffect) const;
+ bool IsStarted() const { return m_iCounter > 0; }
+ bool IsFinished() const { return m_bIsFinished; }
+ bool DrawNextImage() const { return m_bDrawNextImage; }
+
+ int GetWidth() const { return (int)m_fWidth; }
+ int GetHeight() const { return (int)m_fHeight; }
+
+ void Keep();
+ bool StartTransition();
+ int GetTransitionTime(int iType) const;
+ void SetTransitionTime(int iType, int iTime);
+
+ int SlideNumber() const { return m_iSlideNumber; }
+
+ void Zoom(float fZoomAmount, bool immediate = false);
+ void Rotate(float fRotateAngle, bool immediate = false);
+ void Pause(bool bPause);
+ void SetInSlideshow(bool slideshow);
+ void SetOriginalSize(int iOriginalWidth, int iOriginalHeight, bool bFullSize);
+ bool FullSize() const { return m_bFullSize; }
+ int GetOriginalWidth();
+ int GetOriginalHeight();
+
+ void Move(float dX, float dY);
+ float GetZoom() const { return m_fZoomAmount; }
+
+ bool m_bIsComic;
+ bool m_bCanMoveHorizontally;
+ bool m_bCanMoveVertically;
+private:
+ void SetTexture_Internal(int iSlideNumber,
+ std::unique_ptr<CTexture> pTexture,
+ DISPLAY_EFFECT dispEffect = EFFECT_RANDOM,
+ TRANSITION_EFFECT transEffect = FADEIN_FADEOUT);
+ void UpdateVertices(float cur_x[4], float cur_y[4], const float new_x[4], const float new_y[4], CDirtyRegionList &dirtyregions);
+ void Render(float* x, float* y, CTexture* pTexture, UTILS::COLOR::Color color);
+ std::unique_ptr<CTexture> m_pImage;
+
+ int m_iOriginalWidth;
+ int m_iOriginalHeight;
+ int m_iSlideNumber;
+ bool m_bIsLoaded;
+ bool m_bIsFinished;
+ bool m_bDrawNextImage;
+ bool m_bIsDirty;
+ std::string m_strFileName;
+ float m_fWidth;
+ float m_fHeight;
+ UTILS::COLOR::Color m_alpha = 0;
+ // stuff relative to middle position
+ float m_fPosX;
+ float m_fPosY;
+ float m_fPosZ;
+ float m_fVelocityX;
+ float m_fVelocityY;
+ float m_fVelocityZ;
+ float m_fZoomAmount;
+ float m_fZoomLeft;
+ float m_fZoomTop;
+ float m_ax[4]{}, m_ay[4]{};
+ float m_sx[4]{}, m_sy[4]{};
+ float m_bx[4]{}, m_by[4]{};
+ float m_ox[4]{}, m_oy[4]{};
+
+ // transition and display effects
+ DISPLAY_EFFECT m_displayEffect = EFFECT_NONE;
+ TRANSITION m_transitionStart;
+ TRANSITION m_transitionEnd;
+ TRANSITION m_transitionTemp; // used for rotations + zooms
+ float m_fAngle; // angle (between 0 and 2pi to display the image)
+ float m_fTransitionAngle;
+ float m_fTransitionZoom;
+ int m_iCounter = 0;
+ int m_iTotalFrames;
+ bool m_bPause;
+ bool m_bNoEffect;
+ bool m_bFullSize;
+ bool m_bTransitionImmediately;
+
+ CCriticalSection m_textureAccess;
+#ifdef HAS_DX
+ Microsoft::WRL::ComPtr<ID3D11Buffer> m_vb;
+ bool UpdateVertexBuffer(Vertex *vertices);
+#endif
+};
diff --git a/xbmc/pictures/libexif.cpp b/xbmc/pictures/libexif.cpp
new file mode 100644
index 0000000..3ee75b9
--- /dev/null
+++ b/xbmc/pictures/libexif.cpp
@@ -0,0 +1,35 @@
+// libexif.cpp : Defines the entry point for the console application.
+//
+
+#include "libexif.h"
+
+#include "JpegParse.h"
+
+#ifdef TARGET_WINDOWS
+#include <windows.h>
+#else
+#include <memory.h>
+#include <cstring>
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+bool process_jpeg(const char *filename, ExifInfo_t *exifInfo, IPTCInfo_t *iptcInfo)
+{
+ if (!exifInfo || !iptcInfo) return false;
+ CJpegParse jpeg;
+ memset(exifInfo, 0, sizeof(ExifInfo_t));
+ memset(iptcInfo, 0, sizeof(IPTCInfo_t));
+ if (jpeg.Process(filename))
+ {
+ memcpy(exifInfo, jpeg.GetExifInfo(), sizeof(ExifInfo_t));
+ memcpy(iptcInfo, jpeg.GetIptcInfo(), sizeof(IPTCInfo_t));
+ return true;
+ }
+ return false;
+}
+#ifdef __cplusplus
+}
+#endif
diff --git a/xbmc/pictures/libexif.h b/xbmc/pictures/libexif.h
new file mode 100644
index 0000000..567255e
--- /dev/null
+++ b/xbmc/pictures/libexif.h
@@ -0,0 +1,138 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifdef _DLL
+#ifdef WIN32
+#define EXIF_EXPORT __declspec(dllexport)
+#else
+#define EXIF_EXPORT
+#endif
+#else
+#define EXIF_EXPORT
+#endif
+
+//--------------------------------------------------------------------------
+// JPEG markers consist of one or more 0xFF bytes, followed by a marker
+// code byte (which is not an FF). Here are the marker codes of interest
+// in this application.
+//--------------------------------------------------------------------------
+
+#define M_SOF0 0xC0 // Start Of Frame N
+#define M_SOF1 0xC1 // N indicates which compression process
+#define M_SOF2 0xC2 // Only SOF0-SOF2 are now in common use
+#define M_SOF3 0xC3
+#define M_SOF5 0xC5 // NB: codes C4 and CC are NOT SOF markers
+#define M_SOF6 0xC6
+#define M_SOF7 0xC7
+#define M_SOF9 0xC9
+#define M_SOF10 0xCA
+#define M_SOF11 0xCB
+#define M_SOF13 0xCD
+#define M_SOF14 0xCE
+#define M_SOF15 0xCF
+#define M_SOI 0xD8 // Start Of Image (beginning of datastream)
+#define M_EOI 0xD9 // End Of Image (end of datastream)
+#define M_SOS 0xDA // Start Of Scan (begins compressed data)
+#define M_JFIF 0xE0 // Jfif marker
+#define M_EXIF 0xE1 // Exif marker
+#define M_COM 0xFE // COMment
+#define M_DQT 0xDB
+#define M_DHT 0xC4
+#define M_DRI 0xDD
+#define M_IPTC 0xED // IPTC marker
+
+#define MAX_IPTC_STRING 256
+
+typedef struct {
+ char RecordVersion[MAX_IPTC_STRING];
+ char SupplementalCategories[MAX_IPTC_STRING];
+ char Keywords[MAX_IPTC_STRING];
+ char Caption[MAX_IPTC_STRING];
+ char Author[MAX_IPTC_STRING];
+ char Headline[MAX_IPTC_STRING];
+ char SpecialInstructions[MAX_IPTC_STRING];
+ char Category[MAX_IPTC_STRING];
+ char Byline[MAX_IPTC_STRING];
+ char BylineTitle[MAX_IPTC_STRING];
+ char Credit[MAX_IPTC_STRING];
+ char Source[MAX_IPTC_STRING];
+ char CopyrightNotice[MAX_IPTC_STRING];
+ char ObjectName[MAX_IPTC_STRING];
+ char City[MAX_IPTC_STRING];
+ char State[MAX_IPTC_STRING];
+ char Country[MAX_IPTC_STRING];
+ char TransmissionReference[MAX_IPTC_STRING];
+ char Date[MAX_IPTC_STRING];
+ char Urgency[MAX_IPTC_STRING];
+ char ReferenceService[MAX_IPTC_STRING];
+ char CountryCode[MAX_IPTC_STRING];
+ char TimeCreated[MAX_IPTC_STRING];
+ char SubLocation[MAX_IPTC_STRING];
+ char ImageType[MAX_IPTC_STRING];
+} IPTCInfo_t;
+
+#define EXIF_COMMENT_CHARSET_CONVERTED -1 // Comments contains converted data
+#define EXIF_COMMENT_CHARSET_UNKNOWN 0 // Exif: Unknown
+#define EXIF_COMMENT_CHARSET_ASCII 2 // Exif: Ascii
+#define EXIF_COMMENT_CHARSET_UNICODE 3 // Exif: Unicode (UTF-16)
+#define EXIF_COMMENT_CHARSET_JIS 4 // Exif: JIS X208-1990
+
+#define MAX_COMMENT 2000
+#define MAX_DATE_COPIES 10
+
+typedef struct {
+ char CameraMake [33];
+ char CameraModel [41];
+ char DateTime [21];
+ int Height, Width;
+ int Orientation;
+ int IsColor;
+ int Process;
+ int FlashUsed;
+ float FocalLength;
+ float ExposureTime;
+ float ApertureFNumber;
+ float Distance;
+ float CCDWidth;
+ float ExposureBias;
+ float DigitalZoomRatio;
+ int FocalLength35mmEquiv; // Exif 2.2 tag - usually not present.
+ int Whitebalance;
+ int MeteringMode;
+ int ExposureProgram;
+ int ExposureMode;
+ int ISOequivalent;
+ int LightSource;
+ int CommentsCharset; // EXIF_COMMENT_CHARSET_*
+ int XPCommentsCharset;
+ char Comments[MAX_COMMENT + 1]; // +1 for null termination
+ char FileComment[MAX_COMMENT + 1];
+ char XPComment[MAX_COMMENT + 1];
+ char Description[MAX_COMMENT + 1];
+
+ unsigned ThumbnailOffset; // Exif offset to thumbnail
+ unsigned ThumbnailSize; // Size of thumbnail.
+ unsigned LargestExifOffset; // Last exif data referenced (to check if thumbnail is at end)
+
+ char ThumbnailAtEnd; // Exif header ends with the thumbnail
+ // (we can only modify the thumbnail if its at the end)
+ int ThumbnailSizeOffset;
+
+ int DateTimeOffsets[MAX_DATE_COPIES];
+ int numDateTimeTags;
+
+ int GpsInfoPresent;
+ char GpsLat[31];
+ char GpsLong[31];
+ char GpsAlt[20];
+} ExifInfo_t;
+
+EXIF_EXPORT bool process_jpeg(const char *filename, ExifInfo_t *exifInfo, IPTCInfo_t *iptcInfo);
+
+#ifdef __cplusplus
+}
+#endif
+
diff --git a/xbmc/platform/CMakeLists.txt b/xbmc/platform/CMakeLists.txt
new file mode 100644
index 0000000..4df5326
--- /dev/null
+++ b/xbmc/platform/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(SOURCES Environment.cpp
+ xbmc.cpp)
+
+set(HEADERS Environment.h
+ Filesystem.h
+ MessagePrinter.h
+ Platform.h
+ xbmc.h)
+
+core_add_library(platform_common)
diff --git a/xbmc/platform/Environment.cpp b/xbmc/platform/Environment.cpp
new file mode 100644
index 0000000..4655dcc
--- /dev/null
+++ b/xbmc/platform/Environment.cpp
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013-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.
+ */
+
+/**
+ * \file platform\Environment.cpp
+ * \brief Implements CEnvironment class functions.
+ *
+ * Some ideas were inspired by PostgreSQL's pgwin32_putenv function.
+ * Refined, updated, enhanced and modified for XBMC by Karlson2k.
+ */
+
+#include "Environment.h"
+
+#include <stdlib.h>
+
+
+// --------------------- Main Functions ---------------------
+
+int CEnvironment::setenv(const std::string &name, const std::string &value, int overwrite /*= 1*/)
+{
+#ifdef TARGET_WINDOWS
+ return (win_setenv(name, value, overwrite ? autoDetect : addOnly) == 0) ? 0 : -1;
+#else
+ if (value.empty() && overwrite != 0)
+ return ::unsetenv(name.c_str());
+ return ::setenv(name.c_str(), value.c_str(), overwrite);
+#endif
+}
+
+std::string CEnvironment::getenv(const std::string &name)
+{
+#ifdef TARGET_WINDOWS
+ return win_getenv(name);
+#else
+ char * str = ::getenv(name.c_str());
+ if (str == NULL)
+ return "";
+ return str;
+#endif
+}
+
+int CEnvironment::unsetenv(const std::string &name)
+{
+#ifdef TARGET_WINDOWS
+ return (win_setenv(name, "", deleteVariable)) == 0 ? 0 : -1;
+#else
+ return ::unsetenv(name.c_str());
+#endif
+}
+
+int CEnvironment::putenv(const std::string &envstring)
+{
+ if (envstring.empty())
+ return 0;
+ size_t pos = envstring.find('=');
+ if (pos == 0) // '=' is the first character
+ return -1;
+ if (pos == std::string::npos)
+ return unsetenv(envstring);
+ if (pos == envstring.length()-1) // '=' is in last position
+ {
+ std::string name(envstring);
+ name.erase(name.length()-1, 1);
+ return unsetenv(name);
+ }
+ std::string name(envstring, 0, pos), value(envstring, pos+1);
+
+ return setenv(name, value);
+}
+
diff --git a/xbmc/platform/Environment.h b/xbmc/platform/Environment.h
new file mode 100644
index 0000000..a05974c
--- /dev/null
+++ b/xbmc/platform/Environment.h
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2013-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
+
+/**
+ * \file utils\Environment.h
+ * \brief Declares CEnvironment class for platform-independent environment variables manipulations.
+ *
+ */
+#include <string>
+
+/**
+ * @class CEnvironment
+ *
+ * @brief Platform-independent environment variables manipulations.
+ *
+ * Provide analog for POSIX functions:
+ * + setenv
+ * + unsetenv
+ * + putenv
+ * + getenv
+ *
+ * You can generally use the functions as you would normally in POSIX-style.
+ * The differences below are just to make things more convenient through use of std::string (2,3),
+ * and to also allow the Win32-style of unsetting variables (4,5) if wanted.
+ * 1. CEnvironment::setenv parameter 'overwrite' is optional, set by default to 1 (allow overwrite).
+ * 2. CEnvironment::putenv uses copy of provided string (rather than string itself) to change environment,
+ * so you can free parameter variable right after call of function.
+ * 3. CEnvironment::getenv returns a copy of environment variable value instead of pointer to value.
+ * 4. CEnvironment::setenv can be used to unset variables. Just pass empty string for 'value' parameter.
+ * 5. CEnvironment::putenv can be used to unset variables. Set parameter to 'var=' (Windows style) or
+ * just 'var' (POSIX style), and 'var' will be unset.
+ *
+ * All 'std::string' types are supposed to be in UTF-8 encoding.
+ * All functions work on all platforms. Special care is taken on Windows platform where Environment is changed for process itself,
+ * for process runtime library and for all runtime libraries (MSVCRT) loaded by third-party modules.
+ * Functions internally make all necessary UTF-8 <-> wide conversions.*
+ */
+
+class CEnvironment
+{
+public:
+ /**
+ * \fn static int CEnvironment::setenv(const std::string &name, const std::string &value,
+ * int overwrite = 1);
+ * \brief Sets or unsets environment variable.
+ * \param name The environment variable name to add/modify/delete.
+ * \param value The environment variable new value. If set to empty string, variable will be
+ * deleted from the environment.
+ * \param overwrite (optional) If set to non-zero, existing variable will be overwritten. If set to zero and
+ * variable is already present, then variable will be unchanged and function returns success.
+ * \return Zero on success, non-zero on error.
+ */
+ static int setenv(const std::string &name, const std::string &value, int overwrite = 1);
+ /**
+ * \fn static int CEnvironment::unsetenv(const std::string &name);
+ * \brief Deletes environment variable.
+ * \param name The environment variable name to delete.
+ * \return Zero on success, non-zero on error.
+ */
+ static int unsetenv(const std::string &name);
+
+ /**
+ * \fn static int CEnvironment::putenv(const std::string &envstring);
+ * \brief Adds/modifies/deletes environment variable.
+ * \param envstring The variable-value string in form 'var=value'. If set to 'var=' or 'var', then variable
+ * will be deleted from the environment.
+ * \return Zero on success, non-zero on error.
+ */
+ static int putenv(const std::string &envstring);
+ /**
+ * \fn static std::string CEnvironment::getenv(const std::string &name);
+ * \brief Gets value of environment variable in UTF-8 encoding.
+ * \param name The name of environment variable.
+ * \return Copy of of environment variable value or empty string if variable in not present in environment.
+ * \sa xbmc_getenvUtf8, xbmc_getenvW
+ */
+ static std::string getenv(const std::string &name);
+private:
+#ifdef TARGET_WINDOWS
+ enum updateAction:int
+ {
+ addOrUpdateOnly = -2,
+ deleteVariable = -1,
+ addOnly = 0,
+ autoDetect = 1
+ };
+ static int win_setenv(const std::string &name, const std::string &value = "", updateAction action = autoDetect);
+ static std::string win_getenv(const std::string &name);
+#endif // TARGET_WINDOWS
+};
+
diff --git a/xbmc/platform/Filesystem.h b/xbmc/platform/Filesystem.h
new file mode 100644
index 0000000..732be92
--- /dev/null
+++ b/xbmc/platform/Filesystem.h
@@ -0,0 +1,33 @@
+/*
+ * 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 <cstdint>
+#include <string>
+#include <system_error>
+namespace KODI
+{
+namespace PLATFORM
+{
+namespace FILESYSTEM
+{
+struct space_info {
+ std::uintmax_t capacity;
+ std::uintmax_t free;
+ std::uintmax_t available;
+};
+
+space_info space(const std::string &path, std::error_code &ec);
+
+std::string temp_directory_path(std::error_code &ec);
+std::string create_temp_directory(std::error_code &ec);
+std::string temp_file_path(const std::string& suffix, std::error_code& ec);
+}
+}
+}
diff --git a/xbmc/platform/MessagePrinter.h b/xbmc/platform/MessagePrinter.h
new file mode 100644
index 0000000..f0aa08e
--- /dev/null
+++ b/xbmc/platform/MessagePrinter.h
@@ -0,0 +1,43 @@
+/*
+ * 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 <string>
+#include <utility>
+#include <vector>
+
+class CMessagePrinter
+{
+public:
+
+ /*! \brief Display a normal message to the user during startup
+ *
+ * \param[in] message message to display
+ */
+ static void DisplayMessage(const std::string& message);
+
+ /*! \brief Display a warning message to the user during startup
+ *
+ * \param[in] warning warning to display
+ */
+ static void DisplayWarning(const std::string& warning);
+
+ /*! \brief Display an error message to the user during startup
+ *
+ * \param[in] error error to display
+ */
+ static void DisplayError(const std::string& error);
+
+ /*! \brief Display the help message with command line options available
+ *
+ * \param[in] help List of commands and explanations,
+ help.push_back(std::make_pair("--help", "this displays the help))
+ */
+ static void DisplayHelpMessage(const std::vector<std::pair<std::string, std::string>>& help);
+};
diff --git a/xbmc/platform/Platform.h b/xbmc/platform/Platform.h
new file mode 100644
index 0000000..ebb441c
--- /dev/null
+++ b/xbmc/platform/Platform.h
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2016-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 "utils/ComponentContainer.h"
+
+//! \brief Base class for services.
+class IPlatformService
+{
+public:
+ virtual ~IPlatformService() = default;
+};
+
+/**\brief Class for the Platform object
+ *
+ * Contains methods to retrieve platform specific information
+ * and methods for doing platform specific environment preparation/initialisation
+ */
+class CPlatform : public CComponentContainer<IPlatformService>
+{
+public:
+ /**\brief Creates the Platform object
+ *
+ *@return the platform object
+ */
+ static CPlatform *CreateInstance();
+
+ /**\brief C'tor */
+ CPlatform() = default;
+
+ /**\brief D'tor */
+ virtual ~CPlatform() = default;
+
+ /**\brief Called at an early stage of application startup
+ *
+ * This method can be used to do platform specific environment preparation
+ * or initialisation (like setting environment variables for example)
+ */
+ virtual bool InitStageOne() { return true; }
+
+ /**\brief Called at a middle stage of application startup
+ *
+ * This method can be used for starting platform specific services that
+ * do not depend on windowing/gui. (eg macos XBMCHelper)
+ */
+ virtual bool InitStageTwo() { return true; }
+
+ /**\brief Called at a late stage of application startup
+ *
+ * This method can be used for starting platform specific Window/GUI related
+ * services/components. (eg , WS-Discovery Daemons)
+ */
+ virtual bool InitStageThree() { return true; }
+
+ /**\brief Called at a late stage of application shutdown
+ *
+ * This method should be used to cleanup resources allocated in InitStageOne
+ */
+ virtual void DeinitStageOne() {}
+
+ /**\brief Called at a middle stage of application shutdown
+ *
+ * This method should be used to cleanup resources allocated in InitStageTwo
+ */
+ virtual void DeinitStageTwo() {}
+
+ /**\brief Called at an early stage of application shutdown
+ *
+ * This method should be used to cleanup resources allocated in InitStageThree
+ */
+ virtual void DeinitStageThree() {}
+
+ /**\brief Flag whether disabled add-ons - installed via packagemanager or manually - should be
+ * offered for configuration and activation on kodi startup for this platform
+ */
+ virtual bool IsConfigureAddonsAtStartupEnabled() { return false; }
+
+ /**\brief Flag whether this platform supports user installation of binary add-ons.
+ */
+ virtual bool SupportsUserInstalledBinaryAddons() { return true; }
+
+ /**\brief Print platform specific info to log
+ *
+ * Logs platform specific system info during application creation startup
+ */
+ virtual void PlatformSyslog() {}
+
+ /**\brief Get a platform service instance.
+ */
+ template<class T>
+ std::shared_ptr<T> GetService()
+ {
+ return this->GetComponent<T>();
+ }
+};
diff --git a/xbmc/platform/common/speech/CMakeLists.txt b/xbmc/platform/common/speech/CMakeLists.txt
new file mode 100644
index 0000000..ac9752b
--- /dev/null
+++ b/xbmc/platform/common/speech/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES SpeechRecognitionStub.cpp)
+
+set(HEADERS SpeechRecognitionStub.h)
+
+core_add_library(platform_common_speech)
diff --git a/xbmc/platform/common/speech/SpeechRecognitionStub.cpp b/xbmc/platform/common/speech/SpeechRecognitionStub.cpp
new file mode 100644
index 0000000..2f76863
--- /dev/null
+++ b/xbmc/platform/common/speech/SpeechRecognitionStub.cpp
@@ -0,0 +1,15 @@
+/*
+ * Copyright (C) 2012-2022 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 "SpeechRecognitionStub.h"
+
+std::shared_ptr<speech::ISpeechRecognition> speech::ISpeechRecognition::CreateInstance()
+{
+ // speech recognition not implemented
+ return {};
+}
diff --git a/xbmc/platform/common/speech/SpeechRecognitionStub.h b/xbmc/platform/common/speech/SpeechRecognitionStub.h
new file mode 100644
index 0000000..ec44bc5
--- /dev/null
+++ b/xbmc/platform/common/speech/SpeechRecognitionStub.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2012-2022 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 "speech/ISpeechRecognition.h"
+
+// This is a stub for all platforms without a speech recognition implementation
+// Saves us from feature/platform ifdeffery.
+class CSpeechRecognitionStub : public speech::ISpeechRecognition
+{
+public:
+ void StartSpeechRecognition(
+ const std::shared_ptr<speech::ISpeechRecognitionListener>& listener) override
+ {
+ }
+};
diff --git a/xbmc/platform/freebsd/CMakeLists.txt b/xbmc/platform/freebsd/CMakeLists.txt
new file mode 100644
index 0000000..9b8782f
--- /dev/null
+++ b/xbmc/platform/freebsd/CMakeLists.txt
@@ -0,0 +1,28 @@
+set(SOURCES ../linux/AppParamParserLinux.cpp
+ CPUInfoFreebsd.cpp
+ OptionalsReg.cpp
+ ../linux/OptionalsReg.cpp
+ ../linux/TimeUtils.cpp
+ MemUtils.cpp
+ PlatformFreebsd.cpp)
+
+set(HEADERS ../linux/AppParamParserLinux.cpp
+ CPUInfoFreebsd.h
+ OptionalsReg.h
+ ../linux/OptionalsReg.h
+ ../linux/TimeUtils.h
+ PlatformFreebsd.h)
+
+if(ALSA_FOUND)
+ list(APPEND SOURCES ../linux/FDEventMonitor.cpp)
+ list(APPEND HEADERS ../linux/FDEventMonitor.h)
+endif()
+
+if(DBUS_FOUND)
+ list(APPEND SOURCES ../linux/DBusMessage.cpp
+ ../linux/DBusUtil.cpp)
+ list(APPEND HEADERS ../linux/DBusMessage.h
+ ../linux/DBusUtil.h)
+endif()
+
+core_add_library(freebsdsupport)
diff --git a/xbmc/platform/freebsd/CPUInfoFreebsd.cpp b/xbmc/platform/freebsd/CPUInfoFreebsd.cpp
new file mode 100644
index 0000000..5128a72
--- /dev/null
+++ b/xbmc/platform/freebsd/CPUInfoFreebsd.cpp
@@ -0,0 +1,268 @@
+/*
+ * 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 "CPUInfoFreebsd.h"
+
+#include "utils/Temperature.h"
+#include "utils/log.h"
+
+#include <array>
+#include <vector>
+
+// clang-format off
+/* sys/types.h must be included early, esp. before sysy/systl.h, otherwise:
+ /usr/include/sys/sysctl.h:1117:25: error: unknown type name 'u_int' */
+
+#include <sys/types.h>
+// clang-format on
+
+#if defined(__i386__) || defined(__x86_64__)
+#include <cpuid.h>
+#elif __has_include(<sys/auxv.h>)
+#include <sys/auxv.h>
+#endif
+
+#include <sys/resource.h>
+#include <sys/sysctl.h>
+
+namespace
+{
+
+struct CpuData
+{
+public:
+ std::size_t GetActiveTime() const { return state[CP_USER] + state[CP_NICE] + state[CP_SYS]; }
+
+ std::size_t GetIdleTime() const { return state[CP_INTR] + state[CP_IDLE]; }
+
+ std::size_t GetTotalTime() const { return GetActiveTime() + GetIdleTime(); }
+
+ std::size_t state[CPUSTATES];
+};
+
+} // namespace
+
+std::shared_ptr<CCPUInfo> CCPUInfo::GetCPUInfo()
+{
+ return std::make_shared<CCPUInfoFreebsd>();
+}
+
+CCPUInfoFreebsd::CCPUInfoFreebsd()
+{
+ int count = 0;
+ size_t countLength = sizeof(count);
+ if (sysctlbyname("hw.ncpu", &count, &countLength, nullptr, 0) == 0)
+ m_cpuCount = count;
+ else
+ m_cpuCount = 1;
+
+ std::array<char, 512> cpuModel;
+ size_t length = cpuModel.size();
+ if (sysctlbyname("hw.model", cpuModel.data(), &length, nullptr, 0) == 0)
+ m_cpuModel = cpuModel.data();
+
+ for (int i = 0; i < m_cpuCount; i++)
+ {
+ CoreInfo core;
+ core.m_id = i;
+ m_cores.emplace_back(core);
+ }
+#if defined(__i386__) || defined(__x86_64__)
+ uint32_t eax, ebx, ecx, edx;
+
+ m_cpuVendor.clear();
+
+ if (__get_cpuid(CPUID_INFOTYPE_MANUFACTURER, &eax, &ebx, &ecx, &edx))
+ {
+ m_cpuVendor.append(reinterpret_cast<const char*>(&ebx), 4);
+ m_cpuVendor.append(reinterpret_cast<const char*>(&edx), 4);
+ m_cpuVendor.append(reinterpret_cast<const char*>(&ecx), 4);
+ }
+
+ if (__get_cpuid(CPUID_INFOTYPE_EXTENDED_IMPLEMENTED, &eax, &ebx, &ecx, &edx))
+ {
+ if (eax >= CPUID_INFOTYPE_PROCESSOR_3)
+ {
+ m_cpuModel.clear();
+
+ if (__get_cpuid(CPUID_INFOTYPE_PROCESSOR_1, &eax, &ebx, &ecx, &edx))
+ {
+ m_cpuModel.append(reinterpret_cast<const char*>(&eax), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ebx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ecx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&edx), 4);
+ }
+
+ if (__get_cpuid(CPUID_INFOTYPE_PROCESSOR_2, &eax, &ebx, &ecx, &edx))
+ {
+ m_cpuModel.append(reinterpret_cast<const char*>(&eax), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ebx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ecx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&edx), 4);
+ }
+
+ if (__get_cpuid(CPUID_INFOTYPE_PROCESSOR_3, &eax, &ebx, &ecx, &edx))
+ {
+ m_cpuModel.append(reinterpret_cast<const char*>(&eax), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ebx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ecx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&edx), 4);
+ }
+ }
+ }
+
+ m_cpuModel = m_cpuModel.substr(0, m_cpuModel.find(char(0))); // remove extra null terminations
+
+ if (__get_cpuid(CPUID_INFOTYPE_STANDARD, &eax, &eax, &ecx, &edx))
+ {
+ if (edx & CPUID_00000001_EDX_MMX)
+ m_cpuFeatures |= CPU_FEATURE_MMX;
+
+ // Set MMX2 when SSE is present as SSE is a superset of MMX2 and Intel doesn't set the MMX2 cap
+ if (edx & CPUID_00000001_EDX_SSE)
+ m_cpuFeatures |= (CPU_FEATURE_SSE | CPU_FEATURE_MMX2);
+
+ if (edx & CPUID_00000001_EDX_SSE2)
+ m_cpuFeatures |= CPU_FEATURE_SSE2;
+
+ if (ecx & CPUID_00000001_ECX_SSE3)
+ m_cpuFeatures |= CPU_FEATURE_SSE3;
+
+ if (ecx & CPUID_00000001_ECX_SSSE3)
+ m_cpuFeatures |= CPU_FEATURE_SSSE3;
+
+ if (ecx & CPUID_00000001_ECX_SSE4)
+ m_cpuFeatures |= CPU_FEATURE_SSE4;
+
+ if (ecx & CPUID_00000001_ECX_SSE42)
+ m_cpuFeatures |= CPU_FEATURE_SSE42;
+ }
+
+ if (__get_cpuid(CPUID_INFOTYPE_EXTENDED_IMPLEMENTED, &eax, &eax, &ecx, &edx))
+ {
+ if (eax >= CPUID_INFOTYPE_EXTENDED)
+ {
+ if (edx & CPUID_80000001_EDX_MMX)
+ m_cpuFeatures |= CPU_FEATURE_MMX;
+
+ if (edx & CPUID_80000001_EDX_MMX2)
+ m_cpuFeatures |= CPU_FEATURE_MMX2;
+
+ if (edx & CPUID_80000001_EDX_3DNOW)
+ m_cpuFeatures |= CPU_FEATURE_3DNOW;
+
+ if (edx & CPUID_80000001_EDX_3DNOWEXT)
+ m_cpuFeatures |= CPU_FEATURE_3DNOWEXT;
+ }
+ }
+#endif
+
+#if defined(HAS_NEON)
+#if defined(__ARM_NEON)
+ m_cpuFeatures |= CPU_FEATURE_NEON;
+#elif __has_include(<sys/auxv.h>)
+ unsigned long hwcap = 0;
+ elf_aux_info(AT_HWCAP, &hwcap, sizeof(hwcap));
+ if (hwcap & HWCAP_NEON)
+ m_cpuFeatures |= CPU_FEATURE_NEON;
+#endif
+#endif
+}
+
+int CCPUInfoFreebsd::GetUsedPercentage()
+{
+ if (!m_nextUsedReadTime.IsTimePast())
+ return m_lastUsedPercentage;
+
+ size_t len = sizeof(long);
+
+ if (sysctlbyname("kern.cp_times", nullptr, &len, nullptr, 0) != 0)
+ return false;
+
+ std::vector<long> cptimes(len);
+ size_t cptimesLength = cptimes.size();
+ if (sysctlbyname("kern.cp_times", cptimes.data(), &cptimesLength, nullptr, 0) != 0)
+ return false;
+
+ size_t activeTime{0};
+ size_t idleTime{0};
+ size_t totalTime{0};
+
+ std::vector<CpuData> cpuData;
+
+ for (int i = 0; i < m_cpuCount; i++)
+ {
+ CpuData info;
+
+ for (size_t state = 0; state < CPUSTATES; state++)
+ {
+ info.state[state] = cptimes[i * CPUSTATES + state];
+ }
+
+ activeTime += info.GetActiveTime();
+ idleTime += info.GetIdleTime();
+ totalTime += info.GetTotalTime();
+
+ cpuData.emplace_back(info);
+ }
+
+ activeTime -= m_activeTime;
+ idleTime -= m_idleTime;
+ totalTime -= m_totalTime;
+
+ m_activeTime += activeTime;
+ m_idleTime += idleTime;
+ m_totalTime += totalTime;
+
+ m_lastUsedPercentage = activeTime * 100.0f / totalTime;
+ m_nextUsedReadTime.Set(MINIMUM_TIME_BETWEEN_READS);
+
+ for (size_t core = 0; core < cpuData.size(); core++)
+ {
+ auto activeTime = cpuData[core].GetActiveTime() - m_cores[core].m_activeTime;
+ auto idleTime = cpuData[core].GetIdleTime() - m_cores[core].m_idleTime;
+ auto totalTime = cpuData[core].GetTotalTime() - m_cores[core].m_totalTime;
+
+ m_cores[core].m_usagePercent = activeTime * 100.0 / totalTime;
+
+ m_cores[core].m_activeTime += activeTime;
+ m_cores[core].m_idleTime += idleTime;
+ m_cores[core].m_totalTime += totalTime;
+ }
+
+ return static_cast<int>(m_lastUsedPercentage);
+}
+
+float CCPUInfoFreebsd::GetCPUFrequency()
+{
+ int hz = 0;
+ size_t len = sizeof(hz);
+ if (sysctlbyname("dev.cpu.0.freq", &hz, &len, nullptr, 0) != 0)
+ hz = 0;
+
+ return static_cast<float>(hz);
+}
+
+
+bool CCPUInfoFreebsd::GetTemperature(CTemperature& temperature)
+{
+ if (CheckUserTemperatureCommand(temperature))
+ return true;
+
+ int value;
+ size_t len = sizeof(value);
+
+ /* Temperature is in Kelvin * 10 */
+ if (sysctlbyname("dev.cpu.0.temperature", &value, &len, nullptr, 0) != 0)
+ return false;
+
+ temperature = CTemperature::CreateFromKelvin(static_cast<double>(value) / 10.0);
+ temperature.SetValid(true);
+
+ return true;
+}
diff --git a/xbmc/platform/freebsd/CPUInfoFreebsd.h b/xbmc/platform/freebsd/CPUInfoFreebsd.h
new file mode 100644
index 0000000..f1743ce
--- /dev/null
+++ b/xbmc/platform/freebsd/CPUInfoFreebsd.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "utils/Temperature.h"
+
+#include "platform/posix/CPUInfoPosix.h"
+
+class CCPUInfoFreebsd : public CCPUInfoPosix
+{
+public:
+ CCPUInfoFreebsd();
+ ~CCPUInfoFreebsd() = default;
+
+ int GetUsedPercentage() override;
+ float GetCPUFrequency() override;
+ bool GetTemperature(CTemperature& temperature) override;
+};
diff --git a/xbmc/platform/freebsd/MemUtils.cpp b/xbmc/platform/freebsd/MemUtils.cpp
new file mode 100644
index 0000000..de3e42c
--- /dev/null
+++ b/xbmc/platform/freebsd/MemUtils.cpp
@@ -0,0 +1,83 @@
+/*
+ * 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 "utils/MemUtils.h"
+
+#include <array>
+#include <cstdlib>
+#include <cstring>
+#include <stdio.h>
+
+#include <unistd.h> /* FreeBSD can't write standalone headers */
+#include <sys/sysctl.h> /* FreeBSD can't write standalone headers */
+#include <sys/types.h>
+
+namespace KODI
+{
+namespace MEMORY
+{
+
+void* AlignedMalloc(size_t s, size_t alignTo)
+{
+ void* p;
+ posix_memalign(&p, alignTo, s);
+
+ return p;
+}
+
+void AlignedFree(void* p)
+{
+ free(p);
+}
+
+void GetMemoryStatus(MemoryStatus* buffer)
+{
+ if (!buffer)
+ return;
+
+ /* sysctl hw.physmem */
+ size_t len = 0;
+
+ /* physmem */
+ size_t physmem = 0;
+ len = sizeof(physmem);
+ if (sysctlbyname("hw.physmem", &physmem, &len, NULL, 0) == 0)
+ {
+ buffer->totalPhys = physmem;
+ }
+
+ /* pagesize */
+ size_t pagesize = 0;
+ len = sizeof(pagesize);
+ if (sysctlbyname("hw.pagesize", &pagesize, &len, NULL, 0) != 0)
+ pagesize = 4096;
+
+ /* mem_inactive */
+ size_t mem_inactive = 0;
+ len = sizeof(mem_inactive);
+ if (sysctlbyname("vm.stats.vm.v_inactive_count", &mem_inactive, &len, NULL, 0) == 0)
+ mem_inactive *= pagesize;
+
+ /* mem_cache */
+ size_t mem_cache = 0;
+ len = sizeof(mem_cache);
+ if (sysctlbyname("vm.stats.vm.v_cache_count", &mem_cache, &len, NULL, 0) == 0)
+ mem_cache *= pagesize;
+
+ /* mem_free */
+ size_t mem_free = 0;
+ len = sizeof(mem_free);
+ if (sysctlbyname("vm.stats.vm.v_free_count", &mem_free, &len, NULL, 0) == 0)
+ mem_free *= pagesize;
+
+ /* mem_avail = mem_inactive + mem_cache + mem_free */
+ buffer->availPhys = mem_inactive + mem_cache + mem_free;
+}
+
+}
+}
diff --git a/xbmc/platform/freebsd/OptionalsReg.cpp b/xbmc/platform/freebsd/OptionalsReg.cpp
new file mode 100644
index 0000000..ed414f1
--- /dev/null
+++ b/xbmc/platform/freebsd/OptionalsReg.cpp
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2005-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.
+ */
+
+#include "OptionalsReg.h"
+
+
+//-----------------------------------------------------------------------------
+// OSS
+//-----------------------------------------------------------------------------
+
+#ifdef HAS_OSS
+#include "cores/AudioEngine/Sinks/AESinkOSS.h"
+bool OPTIONALS::OSSRegister()
+{
+ CAESinkOSS::Register();
+ return true;
+}
+#endif
diff --git a/xbmc/platform/freebsd/OptionalsReg.h b/xbmc/platform/freebsd/OptionalsReg.h
new file mode 100644
index 0000000..36837d0
--- /dev/null
+++ b/xbmc/platform/freebsd/OptionalsReg.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2005-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
+
+//-----------------------------------------------------------------------------
+// OSS
+//-----------------------------------------------------------------------------
+
+namespace OPTIONALS
+{
+bool OSSRegister();
+}
diff --git a/xbmc/platform/freebsd/PlatformFreebsd.cpp b/xbmc/platform/freebsd/PlatformFreebsd.cpp
new file mode 100644
index 0000000..b65501b
--- /dev/null
+++ b/xbmc/platform/freebsd/PlatformFreebsd.cpp
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016-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 "PlatformFreebsd.h"
+
+#include "utils/StringUtils.h"
+
+#include "platform/freebsd/OptionalsReg.h"
+#include "platform/linux/powermanagement/LinuxPowerSyscall.h"
+
+// clang-format off
+#if defined(HAS_GLES)
+#if defined(HAVE_WAYLAND)
+#include "windowing/wayland/WinSystemWaylandEGLContextGLES.h"
+#endif
+#if defined(HAVE_X11)
+#include "windowing/X11/WinSystemX11GLESContext.h"
+#endif
+#if defined(HAVE_GBM)
+#include "windowing/gbm/WinSystemGbmGLESContext.h"
+#endif
+#endif
+
+#if defined(HAS_GL)
+#if defined(HAVE_WAYLAND)
+#include "windowing/wayland/WinSystemWaylandEGLContextGL.h"
+#endif
+#if defined(HAVE_X11)
+#include "windowing/X11/WinSystemX11GLContext.h"
+#endif
+#if defined(HAVE_GBM)
+#include "windowing/gbm/WinSystemGbmGLContext.h"
+#endif
+#endif
+// clang-format on
+
+#include <cstdlib>
+
+CPlatform* CPlatform::CreateInstance()
+{
+ return new CPlatformFreebsd();
+}
+
+bool CPlatformFreebsd::InitStageOne()
+{
+ if (!CPlatformPosix::InitStageOne())
+ return false;
+
+ setenv("OS", "Linux", true); // for python scripts that check the OS
+
+#if defined(HAS_GLES)
+#if defined(HAVE_WAYLAND)
+ KODI::WINDOWING::WAYLAND::CWinSystemWaylandEGLContextGLES::Register();
+#endif
+#if defined(HAVE_X11)
+ KODI::WINDOWING::X11::CWinSystemX11GLESContext::Register();
+#endif
+#if defined(HAVE_GBM)
+ KODI::WINDOWING::GBM::CWinSystemGbmGLESContext::Register();
+#endif
+#endif
+
+#if defined(HAS_GL)
+#if defined(HAVE_WAYLAND)
+ KODI::WINDOWING::WAYLAND::CWinSystemWaylandEGLContextGL::Register();
+#endif
+#if defined(HAVE_X11)
+ KODI::WINDOWING::X11::CWinSystemX11GLContext::Register();
+#endif
+#if defined(HAVE_GBM)
+ KODI::WINDOWING::GBM::CWinSystemGbmGLContext::Register();
+#endif
+#endif
+
+ CLinuxPowerSyscall::Register();
+
+ std::string envSink;
+ if (getenv("KODI_AE_SINK"))
+ envSink = getenv("KODI_AE_SINK");
+
+ if (StringUtils::EqualsNoCase(envSink, "ALSA"))
+ {
+ OPTIONALS::ALSARegister();
+ }
+ else if (StringUtils::EqualsNoCase(envSink, "PULSE"))
+ {
+ OPTIONALS::PulseAudioRegister();
+ }
+ else if (StringUtils::EqualsNoCase(envSink, "OSS"))
+ {
+ OPTIONALS::OSSRegister();
+ }
+ else if (StringUtils::EqualsNoCase(envSink, "SNDIO"))
+ {
+ OPTIONALS::SndioRegister();
+ }
+ else if (StringUtils::EqualsNoCase(envSink, "ALSA+PULSE"))
+ {
+ OPTIONALS::ALSARegister();
+ OPTIONALS::PulseAudioRegister();
+ }
+ else
+ {
+ if (!OPTIONALS::PulseAudioRegister())
+ {
+ if (!OPTIONALS::ALSARegister())
+ {
+ if (!OPTIONALS::SndioRegister())
+ {
+ OPTIONALS::OSSRegister();
+ }
+ }
+ }
+ }
+
+ m_lirc.reset(OPTIONALS::LircRegister());
+
+ return true;
+}
diff --git a/xbmc/platform/freebsd/PlatformFreebsd.h b/xbmc/platform/freebsd/PlatformFreebsd.h
new file mode 100644
index 0000000..48a4151
--- /dev/null
+++ b/xbmc/platform/freebsd/PlatformFreebsd.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016-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 "platform/linux/OptionalsReg.h"
+#include "platform/posix/PlatformPosix.h"
+
+#include <memory>
+
+class CPlatformFreebsd : public CPlatformPosix
+{
+public:
+ CPlatformFreebsd() = default;
+ ~CPlatformFreebsd() override = default;
+
+ bool InitStageOne() override;
+
+private:
+ std::unique_ptr<OPTIONALS::CLircContainer, OPTIONALS::delete_CLircContainer> m_lirc;
+};
diff --git a/xbmc/platform/freebsd/network/CMakeLists.txt b/xbmc/platform/freebsd/network/CMakeLists.txt
new file mode 100644
index 0000000..25d8ee8
--- /dev/null
+++ b/xbmc/platform/freebsd/network/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES NetworkFreebsd.cpp)
+set(HEADERS NetworkFreebsd.h)
+
+core_add_library(platform_freebsd_network)
diff --git a/xbmc/platform/freebsd/network/NetworkFreebsd.cpp b/xbmc/platform/freebsd/network/NetworkFreebsd.cpp
new file mode 100644
index 0000000..94a0a41
--- /dev/null
+++ b/xbmc/platform/freebsd/network/NetworkFreebsd.cpp
@@ -0,0 +1,247 @@
+/*
+ * 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 "NetworkFreebsd.h"
+
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <array>
+#include <errno.h>
+
+#include <arpa/inet.h>
+#include <ifaddrs.h>
+#include <net/if.h>
+#include <net/if_arp.h>
+#include <net/if_dl.h>
+#include <net/route.h>
+#include <netinet/if_ether.h>
+#include <netinet/in.h>
+#include <resolv.h>
+#include <sys/sockio.h>
+#include <sys/wait.h>
+
+CNetworkInterfaceFreebsd::CNetworkInterfaceFreebsd(CNetworkPosix* network,
+ std::string interfaceName,
+ char interfaceMacAddrRaw[6])
+ : CNetworkInterfacePosix(network, interfaceName, interfaceMacAddrRaw)
+{
+}
+
+std::string CNetworkInterfaceFreebsd::GetCurrentDefaultGateway() const
+{
+ std::string result;
+
+ size_t needed;
+ int mib[6];
+ char *buf, *next, *lim;
+ char line[16];
+ struct rt_msghdr* rtm;
+ struct sockaddr* sa;
+ struct sockaddr_in* sockin;
+
+ mib[0] = CTL_NET;
+ mib[1] = PF_ROUTE;
+ mib[2] = 0;
+ mib[3] = 0;
+ mib[4] = NET_RT_DUMP;
+ mib[5] = 0;
+ if (sysctl(mib, 6, NULL, &needed, NULL, 0) < 0)
+ return result;
+
+ if ((buf = (char*)malloc(needed)) == NULL)
+ return result;
+
+ if (sysctl(mib, 6, buf, &needed, NULL, 0) < 0)
+ {
+ free(buf);
+ return result;
+ }
+
+ lim = buf + needed;
+ for (next = buf; next < lim; next += rtm->rtm_msglen)
+ {
+ rtm = (struct rt_msghdr*)next;
+ sa = (struct sockaddr*)(rtm + 1);
+ sa = (struct sockaddr*)(SA_SIZE(sa) + (char*)sa);
+ sockin = (struct sockaddr_in*)sa;
+ if (inet_ntop(AF_INET, &sockin->sin_addr.s_addr, line, sizeof(line)) == NULL)
+ {
+ free(buf);
+ return result;
+ }
+ result = line;
+ break;
+ }
+ free(buf);
+
+ return result;
+}
+
+bool CNetworkInterfaceFreebsd::GetHostMacAddress(unsigned long host_ip, std::string& mac) const
+{
+ bool ret = false;
+ size_t needed;
+ char *buf, *next;
+ struct rt_msghdr* rtm;
+ struct sockaddr_inarp* sin;
+ struct sockaddr_dl* sdl;
+ constexpr std::array<int, 6> mib = {
+ CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_FLAGS, RTF_LLINFO,
+ };
+
+ mac = "";
+
+ if (sysctl(mib.data(), mib.size(), nullptr, &needed, nullptr, 0) == 0)
+ {
+ buf = (char*)malloc(needed);
+ if (buf)
+ {
+ if (sysctl(mib.data(), mib.size(), buf, &needed, nullptr, 0) == 0)
+ {
+ for (next = buf; next < buf + needed; next += rtm->rtm_msglen)
+ {
+
+ rtm = (struct rt_msghdr*)next;
+ sin = (struct sockaddr_inarp*)(rtm + 1);
+ sdl = (struct sockaddr_dl*)(sin + 1);
+
+ if (host_ip != sin->sin_addr.s_addr || sdl->sdl_alen < 6)
+ continue;
+
+ u_char* cp = (u_char*)LLADDR(sdl);
+
+ mac = StringUtils::Format("{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", cp[0], cp[1],
+ cp[2], cp[3], cp[4], cp[5]);
+ ret = true;
+ break;
+ }
+ }
+ free(buf);
+ }
+ }
+ return ret;
+}
+
+std::unique_ptr<CNetworkBase> CNetworkBase::GetNetwork()
+{
+ return std::make_unique<CNetworkFreebsd>();
+}
+
+CNetworkFreebsd::CNetworkFreebsd() : CNetworkPosix()
+{
+ queryInterfaceList();
+}
+
+void CNetworkFreebsd::GetMacAddress(const std::string& interfaceName, char rawMac[6])
+{
+ memset(rawMac, 0, 6);
+
+#if !defined(IFT_ETHER)
+#define IFT_ETHER 0x6 /* Ethernet CSMACD */
+#endif
+ const struct sockaddr_dl* dlAddr = NULL;
+ const uint8_t* base = NULL;
+ // Query the list of interfaces.
+ struct ifaddrs* list;
+ struct ifaddrs* interface;
+
+ if (getifaddrs(&list) < 0)
+ {
+ return;
+ }
+
+ for (interface = list; interface != NULL; interface = interface->ifa_next)
+ {
+ if (interfaceName == interface->ifa_name)
+ {
+ if ((interface->ifa_addr->sa_family == AF_LINK) &&
+ (((const struct sockaddr_dl*)interface->ifa_addr)->sdl_type == IFT_ETHER))
+ {
+ dlAddr = (const struct sockaddr_dl*)interface->ifa_addr;
+ base = (const uint8_t*)&dlAddr->sdl_data[dlAddr->sdl_nlen];
+
+ if (dlAddr->sdl_alen > 5)
+ {
+ memcpy(rawMac, base, 6);
+ }
+ }
+ break;
+ }
+ }
+
+ freeifaddrs(list);
+}
+
+void CNetworkFreebsd::queryInterfaceList()
+{
+ char macAddrRaw[6];
+ m_interfaces.clear();
+
+ // Query the list of interfaces.
+ struct ifaddrs* list;
+ if (getifaddrs(&list) < 0)
+ return;
+
+ struct ifaddrs* cur;
+ for (cur = list; cur != NULL; cur = cur->ifa_next)
+ {
+ if (cur->ifa_addr->sa_family != AF_INET)
+ continue;
+
+ GetMacAddress(cur->ifa_name, macAddrRaw);
+
+ // only add interfaces with non-zero mac addresses
+ if (macAddrRaw[0] || macAddrRaw[1] || macAddrRaw[2] || macAddrRaw[3] || macAddrRaw[4] ||
+ macAddrRaw[5])
+ // Add the interface.
+ m_interfaces.push_back(new CNetworkInterfaceFreebsd(this, cur->ifa_name, macAddrRaw));
+ }
+
+ freeifaddrs(list);
+}
+
+std::vector<std::string> CNetworkFreebsd::GetNameServers()
+{
+ std::vector<std::string> result;
+
+ res_init();
+
+ for (int i = 0; i < _res.nscount; i++)
+ {
+ std::string ns = inet_ntoa(_res.nsaddr_list[i].sin_addr);
+ result.push_back(ns);
+ }
+
+ return result;
+}
+
+bool CNetworkFreebsd::PingHost(unsigned long remote_ip, unsigned int timeout_ms)
+{
+ char cmd_line[64];
+
+ struct in_addr host_ip;
+ host_ip.s_addr = remote_ip;
+
+ sprintf(cmd_line, "ping -c 1 -t %d %s", timeout_ms / 1000 + (timeout_ms % 1000) != 0,
+ inet_ntoa(host_ip));
+
+ int status = -1;
+ status = system(cmd_line);
+ int result = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
+
+ // http://linux.about.com/od/commands/l/blcmdl8_ping.htm ;
+ // 0 reply
+ // 1 no reply
+ // else some error
+
+ if (result < 0 || result > 1)
+ CLog::Log(LOGERROR, "Ping fail : status = {}, errno = {} : '{}'", status, errno, cmd_line);
+
+ return result == 0;
+}
diff --git a/xbmc/platform/freebsd/network/NetworkFreebsd.h b/xbmc/platform/freebsd/network/NetworkFreebsd.h
new file mode 100644
index 0000000..92b00d7
--- /dev/null
+++ b/xbmc/platform/freebsd/network/NetworkFreebsd.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "platform/posix/network/NetworkPosix.h"
+
+#include <string>
+#include <vector>
+
+class CNetworkInterfaceFreebsd : public CNetworkInterfacePosix
+{
+public:
+ CNetworkInterfaceFreebsd(CNetworkPosix* network,
+ std::string interfaceName,
+ char interfaceMacAddrRaw[6]);
+ ~CNetworkInterfaceFreebsd() override = default;
+
+ std::string GetCurrentDefaultGateway() const override;
+ bool GetHostMacAddress(unsigned long host, std::string& mac) const override;
+};
+
+class CNetworkFreebsd : public CNetworkPosix
+{
+public:
+ CNetworkFreebsd();
+ ~CNetworkFreebsd() override = default;
+
+ bool PingHost(unsigned long host, unsigned int timeout_ms = 2000) override;
+ std::vector<std::string> GetNameServers() override;
+
+private:
+ void GetMacAddress(const std::string& interfaceName, char macAddrRaw[6]) override;
+ void queryInterfaceList() override;
+};
diff --git a/xbmc/platform/linux/AppParamParserLinux.cpp b/xbmc/platform/linux/AppParamParserLinux.cpp
new file mode 100644
index 0000000..3a1adf9
--- /dev/null
+++ b/xbmc/platform/linux/AppParamParserLinux.cpp
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2005-2021 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 "AppParamParserLinux.h"
+
+#include "CompileInfo.h"
+#include "application/AppParams.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+#include <array>
+#include <iostream>
+#include <vector>
+
+namespace
+{
+std::vector<std::string> availableWindowSystems = CCompileInfo::GetAvailableWindowSystems();
+std::array<std::string, 1> availableLogTargets = {"console"};
+
+constexpr const char* windowingText =
+ R"""(
+Selected window system not available: {}
+ Available window systems: {}
+)""";
+
+constexpr const char* loggingText =
+ R"""(
+Selected logging target not available: {}
+ Available log targest: {}
+)""";
+
+constexpr const char* helpText =
+ R"""(
+Linux Specific Arguments:
+ --windowing=<system> Select which windowing method to use.
+ Available window systems are: {}
+ --logging=<target> Select which log target to use (log file will always be used in conjunction).
+ Available log targets are: {}
+)""";
+
+
+} // namespace
+
+CAppParamParserLinux::CAppParamParserLinux() : CAppParamParser()
+{
+}
+
+CAppParamParserLinux::~CAppParamParserLinux() = default;
+
+void CAppParamParserLinux::ParseArg(const std::string& arg)
+{
+ CAppParamParser::ParseArg(arg);
+
+ if (arg.substr(0, 12) == "--windowing=")
+ {
+ if (std::find(availableWindowSystems.begin(), availableWindowSystems.end(), arg.substr(12)) !=
+ availableWindowSystems.end())
+ GetAppParams()->SetWindowing(arg.substr(12));
+ else
+ {
+ std::cout << StringUtils::Format(windowingText, arg.substr(12),
+ StringUtils::Join(availableWindowSystems, ", "));
+ exit(0);
+ }
+ }
+ else if (arg.substr(0, 10) == "--logging=")
+ {
+ if (std::find(availableLogTargets.begin(), availableLogTargets.end(), arg.substr(10)) !=
+ availableLogTargets.end())
+ {
+ GetAppParams()->SetLogTarget(arg.substr(10));
+ }
+ else
+ {
+ std::cout << StringUtils::Format(loggingText, arg.substr(10),
+ StringUtils::Join(availableLogTargets, ", "));
+ exit(0);
+ }
+ }
+}
+
+void CAppParamParserLinux::DisplayHelp()
+{
+ CAppParamParser::DisplayHelp();
+
+ std::cout << StringUtils::Format(helpText, StringUtils::Join(availableWindowSystems, ", "),
+ StringUtils::Join(availableLogTargets, ", "));
+}
diff --git a/xbmc/platform/linux/AppParamParserLinux.h b/xbmc/platform/linux/AppParamParserLinux.h
new file mode 100644
index 0000000..ea866ca
--- /dev/null
+++ b/xbmc/platform/linux/AppParamParserLinux.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2005-2021 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 "application/AppParamParser.h"
+
+class CAppParamParserLinux : public CAppParamParser
+{
+public:
+ CAppParamParserLinux();
+ ~CAppParamParserLinux();
+
+protected:
+ void ParseArg(const std::string& arg) override;
+ void DisplayHelp() override;
+};
diff --git a/xbmc/platform/linux/CMakeLists.txt b/xbmc/platform/linux/CMakeLists.txt
new file mode 100644
index 0000000..31f4cef
--- /dev/null
+++ b/xbmc/platform/linux/CMakeLists.txt
@@ -0,0 +1,32 @@
+set(SOURCES AppParamParserLinux.cpp
+ CPUInfoLinux.cpp
+ MemUtils.cpp
+ OptionalsReg.cpp
+ PlatformLinux.cpp
+ SysfsPath.cpp
+ TimeUtils.cpp)
+
+set(HEADERS AppParamParserLinux.h
+ CPUInfoLinux.h
+ OptionalsReg.h
+ PlatformLinux.h
+ SysfsPath.h
+ TimeUtils.h)
+
+if(ALSA_FOUND)
+ list(APPEND SOURCES FDEventMonitor.cpp)
+ list(APPEND HEADERS FDEventMonitor.h)
+endif()
+
+if(DBUS_FOUND)
+ list(APPEND SOURCES DBusMessage.cpp
+ DBusUtil.cpp)
+ list(APPEND HEADERS DBusMessage.h
+ DBusUtil.h)
+endif()
+
+if(ADDONS_CONFIGURE_AT_STARTUP)
+ add_compile_definitions(ADDONS_CONFIGURE_AT_STARTUP)
+endif()
+
+core_add_library(linuxsupport)
diff --git a/xbmc/platform/linux/CPUInfoLinux.cpp b/xbmc/platform/linux/CPUInfoLinux.cpp
new file mode 100644
index 0000000..ef34bbd
--- /dev/null
+++ b/xbmc/platform/linux/CPUInfoLinux.cpp
@@ -0,0 +1,379 @@
+/*
+ * 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 "CPUInfoLinux.h"
+
+#include "utils/StringUtils.h"
+#include "utils/Temperature.h"
+
+#include "platform/linux/SysfsPath.h"
+
+#include <exception>
+#include <fstream>
+#include <regex>
+#include <sstream>
+#include <vector>
+
+#if (defined(__arm__) && defined(HAS_NEON)) || defined(__aarch64__)
+#include <asm/hwcap.h>
+#include <sys/auxv.h>
+#elif defined(__i386__) || defined(__x86_64__)
+#include <cpuid.h>
+#endif
+
+#include <unistd.h>
+
+namespace
+{
+enum CpuStates
+{
+ STATE_USER,
+ STATE_NICE,
+ STATE_SYSTEM,
+ STATE_IDLE,
+ STATE_IOWAIT,
+ STATE_IRQ,
+ STATE_SOFTIRQ,
+ STATE_STEAL,
+ STATE_GUEST,
+ STATE_GUEST_NICE,
+ STATE_MAX
+};
+
+struct CpuData
+{
+public:
+ std::size_t GetActiveTime() const
+ {
+ return state[STATE_USER] + state[STATE_NICE] + state[STATE_SYSTEM] + state[STATE_IRQ] +
+ state[STATE_SOFTIRQ] + state[STATE_STEAL] + state[STATE_GUEST] + state[STATE_GUEST_NICE];
+ }
+
+ std::size_t GetIdleTime() const { return state[STATE_IDLE] + state[STATE_IOWAIT]; }
+
+ std::size_t GetTotalTime() const { return GetActiveTime() + GetIdleTime(); }
+
+ std::string cpu;
+ std::size_t state[STATE_MAX];
+};
+} // namespace
+
+std::shared_ptr<CCPUInfo> CCPUInfo::GetCPUInfo()
+{
+ return std::make_shared<CCPUInfoLinux>();
+}
+
+CCPUInfoLinux::CCPUInfoLinux()
+{
+ CSysfsPath machinePath{"/sys/bus/soc/devices/soc0/machine"};
+ if (machinePath.Exists())
+ m_cpuHardware = machinePath.Get<std::string>().value_or("");
+
+ CSysfsPath familyPath{"/sys/bus/soc/devices/soc0/family"};
+ if (familyPath.Exists())
+ m_cpuSoC = familyPath.Get<std::string>().value_or("");
+
+ CSysfsPath socPath{"/sys/bus/soc/devices/soc0/soc_id"};
+ if (socPath.Exists())
+ m_cpuSoC += " " + socPath.Get<std::string>().value_or("");
+
+ CSysfsPath revisionPath{"/sys/bus/soc/devices/soc0/revision"};
+ if (revisionPath.Exists())
+ m_cpuRevision = revisionPath.Get<std::string>().value_or("");
+
+ CSysfsPath serialPath{"/sys/bus/soc/devices/soc0/serial_number"};
+ if (serialPath.Exists())
+ m_cpuSerial = serialPath.Get<std::string>().value_or("");
+
+ const std::string freqStr{"/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq"};
+ CSysfsPath freqPath{freqStr};
+ if (freqPath.Exists())
+ m_freqPath = freqStr;
+
+ const std::array<std::string, 4> modules = {
+ "coretemp",
+ "k10temp",
+ "scpi_sensors",
+ "imx_thermal_zone",
+ };
+
+ for (int i = 0; i < 20; i++)
+ {
+ CSysfsPath path{"/sys/class/hwmon/hwmon" + std::to_string(i) + "/name"};
+ if (!path.Exists())
+ continue;
+
+ auto name = path.Get<std::string>();
+
+ if (!name.has_value())
+ continue;
+
+ for (const auto& module : modules)
+ {
+ if (module == name)
+ {
+ std::string tempStr{"/sys/class/hwmon/hwmon" + std::to_string(i) + "/temp1_input"};
+ CSysfsPath tempPath{tempStr};
+ if (!tempPath.Exists())
+ continue;
+
+ m_tempPath = tempStr;
+ break;
+ }
+ }
+
+ if (!m_tempPath.empty())
+ break;
+ }
+
+ m_cpuCount = sysconf(_SC_NPROCESSORS_ONLN);
+
+ for (int core = 0; core < m_cpuCount; core++)
+ {
+ CoreInfo coreInfo;
+ coreInfo.m_id = core;
+ m_cores.emplace_back(coreInfo);
+ }
+
+#if defined(__i386__) || defined(__x86_64__)
+ unsigned int eax;
+ unsigned int ebx;
+ unsigned int ecx;
+ unsigned int edx;
+
+ m_cpuVendor.clear();
+
+ if (__get_cpuid(CPUID_INFOTYPE_MANUFACTURER, &eax, &ebx, &ecx, &edx))
+ {
+ m_cpuVendor.append(reinterpret_cast<const char*>(&ebx), 4);
+ m_cpuVendor.append(reinterpret_cast<const char*>(&edx), 4);
+ m_cpuVendor.append(reinterpret_cast<const char*>(&ecx), 4);
+ }
+
+ if (__get_cpuid(CPUID_INFOTYPE_EXTENDED_IMPLEMENTED, &eax, &ebx, &ecx, &edx))
+ {
+ if (eax >= CPUID_INFOTYPE_PROCESSOR_3)
+ {
+ m_cpuModel.clear();
+
+ if (__get_cpuid(CPUID_INFOTYPE_PROCESSOR_1, &eax, &ebx, &ecx, &edx))
+ {
+ m_cpuModel.append(reinterpret_cast<const char*>(&eax), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ebx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ecx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&edx), 4);
+ }
+
+ if (__get_cpuid(CPUID_INFOTYPE_PROCESSOR_2, &eax, &ebx, &ecx, &edx))
+ {
+ m_cpuModel.append(reinterpret_cast<const char*>(&eax), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ebx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ecx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&edx), 4);
+ }
+
+ if (__get_cpuid(CPUID_INFOTYPE_PROCESSOR_3, &eax, &ebx, &ecx, &edx))
+ {
+ m_cpuModel.append(reinterpret_cast<const char*>(&eax), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ebx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&ecx), 4);
+ m_cpuModel.append(reinterpret_cast<const char*>(&edx), 4);
+ }
+ }
+ }
+
+ if (__get_cpuid(CPUID_INFOTYPE_STANDARD, &eax, &eax, &ecx, &edx))
+ {
+ if (edx & CPUID_00000001_EDX_MMX)
+ m_cpuFeatures |= CPU_FEATURE_MMX;
+
+ if (edx & CPUID_00000001_EDX_SSE)
+ m_cpuFeatures |= CPU_FEATURE_SSE;
+
+ if (edx & CPUID_00000001_EDX_SSE2)
+ m_cpuFeatures |= CPU_FEATURE_SSE2;
+
+ if (ecx & CPUID_00000001_ECX_SSE3)
+ m_cpuFeatures |= CPU_FEATURE_SSE3;
+
+ if (ecx & CPUID_00000001_ECX_SSSE3)
+ m_cpuFeatures |= CPU_FEATURE_SSSE3;
+
+ if (ecx & CPUID_00000001_ECX_SSE4)
+ m_cpuFeatures |= CPU_FEATURE_SSE4;
+
+ if (ecx & CPUID_00000001_ECX_SSE42)
+ m_cpuFeatures |= CPU_FEATURE_SSE42;
+ }
+
+ if (__get_cpuid(CPUID_INFOTYPE_EXTENDED_IMPLEMENTED, &eax, &eax, &ecx, &edx))
+ {
+ if (eax >= CPUID_INFOTYPE_EXTENDED)
+ {
+ if (edx & CPUID_80000001_EDX_MMX)
+ m_cpuFeatures |= CPU_FEATURE_MMX;
+
+ if (edx & CPUID_80000001_EDX_MMX2)
+ m_cpuFeatures |= CPU_FEATURE_MMX2;
+
+ if (edx & CPUID_80000001_EDX_3DNOW)
+ m_cpuFeatures |= CPU_FEATURE_3DNOW;
+
+ if (edx & CPUID_80000001_EDX_3DNOWEXT)
+ m_cpuFeatures |= CPU_FEATURE_3DNOWEXT;
+ }
+ }
+#else
+ std::ifstream cpuinfo("/proc/cpuinfo");
+ std::regex re(".*: (.*)$");
+
+ for (std::string line; std::getline(cpuinfo, line);)
+ {
+ std::smatch match;
+
+ if (std::regex_match(line, match, re))
+ {
+ if (match.size() == 2)
+ {
+ std::ssub_match value = match[1];
+
+ if (line.find("model name") != std::string::npos)
+ {
+ if (m_cpuModel.empty())
+ m_cpuModel = value.str();
+ }
+
+ if (line.find("BogoMIPS") != std::string::npos)
+ {
+ if (m_cpuBogoMips.empty())
+ m_cpuBogoMips = value.str();
+ }
+
+ if (line.find("Hardware") != std::string::npos)
+ {
+ if (m_cpuHardware.empty())
+ m_cpuHardware = value.str();
+ }
+
+ if (line.find("Serial") != std::string::npos)
+ {
+ if (m_cpuSerial.empty())
+ m_cpuSerial = value.str();
+ }
+
+ if (line.find("Revision") != std::string::npos)
+ {
+ if (m_cpuRevision.empty())
+ m_cpuRevision = value.str();
+ }
+ }
+ }
+ }
+#endif
+
+ m_cpuModel = m_cpuModel.substr(0, m_cpuModel.find(char(0))); // remove extra null terminations
+
+#if defined(HAS_NEON) && defined(__arm__)
+ if (getauxval(AT_HWCAP) & HWCAP_NEON)
+ m_cpuFeatures |= CPU_FEATURE_NEON;
+#endif
+
+#if defined(HAS_NEON) && defined(__aarch64__)
+ if (getauxval(AT_HWCAP) & HWCAP_ASIMD)
+ m_cpuFeatures |= CPU_FEATURE_NEON;
+#endif
+
+ // Set MMX2 when SSE is present as SSE is a superset of MMX2 and Intel doesn't set the MMX2 cap
+ if (m_cpuFeatures & CPU_FEATURE_SSE)
+ m_cpuFeatures |= CPU_FEATURE_MMX2;
+}
+
+int CCPUInfoLinux::GetUsedPercentage()
+{
+ if (!m_nextUsedReadTime.IsTimePast())
+ return m_lastUsedPercentage;
+
+ std::vector<CpuData> cpuData;
+
+ std::ifstream infile("/proc/stat");
+
+ for (std::string line; std::getline(infile, line);)
+ {
+ if (line.find("cpu") != std::string::npos)
+ {
+ std::istringstream ss(line);
+ CpuData info;
+
+ ss >> info.cpu;
+
+ for (int i = 0; i < STATE_MAX; i++)
+ {
+ ss >> info.state[i];
+ }
+
+ cpuData.emplace_back(info);
+ }
+ }
+
+ auto activeTime = cpuData.front().GetActiveTime() - m_activeTime;
+ auto idleTime = cpuData.front().GetIdleTime() - m_idleTime;
+ auto totalTime = cpuData.front().GetTotalTime() - m_totalTime;
+
+ m_activeTime += activeTime;
+ m_idleTime += idleTime;
+ m_totalTime += totalTime;
+
+ m_lastUsedPercentage = activeTime * 100.0f / totalTime;
+ m_nextUsedReadTime.Set(MINIMUM_TIME_BETWEEN_READS);
+
+ cpuData.erase(cpuData.begin());
+
+ for (std::size_t core = 0; core < cpuData.size(); core++)
+ {
+ auto activeTime = cpuData[core].GetActiveTime() - m_cores[core].m_activeTime;
+ auto idleTime = cpuData[core].GetIdleTime() - m_cores[core].m_idleTime;
+ auto totalTime = cpuData[core].GetTotalTime() - m_cores[core].m_totalTime;
+
+ m_cores[core].m_usagePercent = activeTime * 100.0 / totalTime;
+
+ m_cores[core].m_activeTime += activeTime;
+ m_cores[core].m_idleTime += idleTime;
+ m_cores[core].m_totalTime += totalTime;
+ }
+
+ return static_cast<int>(m_lastUsedPercentage);
+}
+
+float CCPUInfoLinux::GetCPUFrequency()
+{
+ if (m_freqPath.empty())
+ return 0;
+
+ auto freq = CSysfsPath(m_freqPath).Get<float>();
+ return freq.has_value() ? *freq / 1000.0f : 0.0f;
+}
+
+bool CCPUInfoLinux::GetTemperature(CTemperature& temperature)
+{
+ if (CheckUserTemperatureCommand(temperature))
+ return true;
+
+ if (m_tempPath.empty())
+ return false;
+
+ auto temp = CSysfsPath(m_tempPath).Get<double>();
+ if (!temp.has_value())
+ return false;
+
+ double value = *temp / 1000.0;
+
+ temperature = CTemperature::CreateFromCelsius(value);
+ temperature.SetValid(true);
+
+ return true;
+}
diff --git a/xbmc/platform/linux/CPUInfoLinux.h b/xbmc/platform/linux/CPUInfoLinux.h
new file mode 100644
index 0000000..ed79afc
--- /dev/null
+++ b/xbmc/platform/linux/CPUInfoLinux.h
@@ -0,0 +1,30 @@
+/*
+ * 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 "utils/Temperature.h"
+
+#include "platform/posix/CPUInfoPosix.h"
+
+#include <string>
+
+class CCPUInfoLinux : public CCPUInfoPosix
+{
+public:
+ CCPUInfoLinux();
+ ~CCPUInfoLinux() = default;
+
+ int GetUsedPercentage() override;
+ float GetCPUFrequency() override;
+ bool GetTemperature(CTemperature& temperature) override;
+
+private:
+ std::string m_freqPath;
+ std::string m_tempPath;
+};
diff --git a/xbmc/platform/linux/DBusMessage.cpp b/xbmc/platform/linux/DBusMessage.cpp
new file mode 100644
index 0000000..ab09c3e
--- /dev/null
+++ b/xbmc/platform/linux/DBusMessage.cpp
@@ -0,0 +1,198 @@
+/*
+ * 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 "DBusMessage.h"
+
+#include "DBusUtil.h"
+#include "settings/AdvancedSettings.h"
+#include "utils/log.h"
+
+CDBusMessage::CDBusMessage(const char *destination, const char *object, const char *interface, const char *method)
+{
+ m_message.reset(dbus_message_new_method_call(destination, object, interface, method));
+ if (!m_message)
+ {
+ // Fails only due to memory allocation failure
+ throw std::runtime_error("dbus_message_new_method_call");
+ }
+ m_haveArgs = false;
+
+ CLog::Log(LOGDEBUG, LOGDBUS, "DBus: Creating message to {} on {} with interface {} and method {}",
+ destination, object, interface, method);
+}
+
+CDBusMessage::CDBusMessage(const std::string& destination, const std::string& object, const std::string& interface, const std::string& method)
+: CDBusMessage(destination.c_str(), object.c_str(), interface.c_str(), method.c_str())
+{
+}
+
+void DBusMessageDeleter::operator()(DBusMessage* message) const
+{
+ dbus_message_unref(message);
+}
+
+void CDBusMessage::AppendObjectPath(const char *object)
+{
+ AppendWithType(DBUS_TYPE_OBJECT_PATH, &object);
+}
+
+template<>
+void CDBusMessage::AppendArgument<bool>(const bool arg)
+{
+ // dbus_bool_t width might not match C++ bool width
+ dbus_bool_t convArg = (arg == true);
+ AppendWithType(DBUS_TYPE_BOOLEAN, &convArg);
+}
+
+template<>
+void CDBusMessage::AppendArgument<std::string>(const std::string arg)
+{
+ AppendArgument(arg.c_str());
+}
+
+void CDBusMessage::AppendArgument(const char **arrayString, unsigned int length)
+{
+ PrepareArgument();
+ DBusMessageIter sub;
+ if (!dbus_message_iter_open_container(&m_args, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &sub))
+ {
+ throw std::runtime_error("dbus_message_iter_open_container");
+ }
+
+ for (unsigned int i = 0; i < length; i++)
+ {
+ if (!dbus_message_iter_append_basic(&sub, DBUS_TYPE_STRING, &arrayString[i]))
+ {
+ throw std::runtime_error("dbus_message_iter_append_basic");
+ }
+ }
+
+ if (!dbus_message_iter_close_container(&m_args, &sub))
+ {
+ throw std::runtime_error("dbus_message_iter_close_container");
+ }
+}
+
+void CDBusMessage::AppendWithType(int type, const void* value)
+{
+ PrepareArgument();
+ if (!dbus_message_iter_append_basic(&m_args, type, value))
+ {
+ throw std::runtime_error("dbus_message_iter_append_basic");
+ }
+}
+
+DBusMessageIter * CDBusMessage::GetArgumentIter() {
+ PrepareArgument();
+ return &m_args;
+}
+
+DBusMessage *CDBusMessage::SendSystem()
+{
+ return Send(DBUS_BUS_SYSTEM);
+}
+
+DBusMessage *CDBusMessage::SendSession()
+{
+ return Send(DBUS_BUS_SESSION);
+}
+
+DBusMessage *CDBusMessage::SendSystem(CDBusError& error)
+{
+ return Send(DBUS_BUS_SYSTEM, error);
+}
+
+DBusMessage *CDBusMessage::SendSession(CDBusError& error)
+{
+ return Send(DBUS_BUS_SESSION, error);
+}
+
+bool CDBusMessage::SendAsyncSystem()
+{
+ return SendAsync(DBUS_BUS_SYSTEM);
+}
+
+bool CDBusMessage::SendAsyncSession()
+{
+ return SendAsync(DBUS_BUS_SESSION);
+}
+
+DBusMessage *CDBusMessage::Send(DBusBusType type)
+{
+ CDBusError error;
+ CDBusConnection con;
+ if (!con.Connect(type))
+ return nullptr;
+
+ DBusMessage *returnMessage = Send(con, error);
+
+ if (error)
+ error.Log();
+
+ return returnMessage;
+}
+
+DBusMessage *CDBusMessage::Send(DBusBusType type, CDBusError& error)
+{
+ CDBusConnection con;
+ if (!con.Connect(type, error))
+ return nullptr;
+
+ DBusMessage *returnMessage = Send(con, error);
+
+ return returnMessage;
+}
+
+bool CDBusMessage::SendAsync(DBusBusType type)
+{
+ CDBusConnection con;
+ if (!con.Connect(type))
+ return false;
+
+ return dbus_connection_send(con, m_message.get(), nullptr);
+}
+
+DBusMessage *CDBusMessage::Send(DBusConnection *con, CDBusError& error)
+{
+ m_reply.reset(dbus_connection_send_with_reply_and_block(con, m_message.get(), -1, error));
+ return m_reply.get();
+}
+
+void CDBusMessage::PrepareArgument()
+{
+ if (!m_haveArgs)
+ dbus_message_iter_init_append(m_message.get(), &m_args);
+
+ m_haveArgs = true;
+}
+
+bool CDBusMessage::InitializeReplyIter(DBusMessageIter* iter)
+{
+ if (!m_reply)
+ {
+ throw std::logic_error("Cannot get reply arguments of message that does not have reply");
+ }
+ if (!dbus_message_iter_init(m_reply.get(), iter))
+ {
+ CLog::Log(LOGWARNING, "Tried to obtain reply arguments from message that has zero arguments");
+ return false;
+ }
+ return true;
+}
+
+bool CDBusMessage::CheckTypeAndGetValue(DBusMessageIter* iter, int expectType, void* dest)
+{
+ const int haveType = dbus_message_iter_get_arg_type(iter);
+ if (haveType != expectType)
+ {
+ CLog::Log(LOGDEBUG, "DBus argument type mismatch: expected {}, got {}", expectType, haveType);
+ return false;
+ }
+
+ dbus_message_iter_get_basic(iter, dest);
+ return true;
+}
diff --git a/xbmc/platform/linux/DBusMessage.h b/xbmc/platform/linux/DBusMessage.h
new file mode 100644
index 0000000..80c7cd6
--- /dev/null
+++ b/xbmc/platform/linux/DBusMessage.h
@@ -0,0 +1,149 @@
+/*
+ * 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 <cstdint>
+#include <memory>
+#include <string>
+#include <type_traits>
+
+#include <dbus/dbus.h>
+
+class CDBusError;
+
+template<typename T>
+struct ToDBusType;
+template<> struct ToDBusType<bool> { static constexpr int TYPE = DBUS_TYPE_BOOLEAN; };
+template<> struct ToDBusType<char*> { static constexpr int TYPE = DBUS_TYPE_STRING; };
+template<> struct ToDBusType<const char*> { static constexpr int TYPE = DBUS_TYPE_STRING; };
+template<> struct ToDBusType<std::uint8_t> { static constexpr int TYPE = DBUS_TYPE_BYTE; };
+template<> struct ToDBusType<std::int16_t> { static constexpr int TYPE = DBUS_TYPE_INT16; };
+template<> struct ToDBusType<std::uint16_t> { static constexpr int TYPE = DBUS_TYPE_UINT16; };
+template<> struct ToDBusType<std::int32_t> { static constexpr int TYPE = DBUS_TYPE_INT32; };
+template<> struct ToDBusType<std::uint32_t> { static constexpr int TYPE = DBUS_TYPE_UINT32; };
+template<> struct ToDBusType<std::int64_t> { static constexpr int TYPE = DBUS_TYPE_INT64; };
+template<> struct ToDBusType<std::uint64_t> { static constexpr int TYPE = DBUS_TYPE_UINT64; };
+template<> struct ToDBusType<double> { static constexpr int TYPE = DBUS_TYPE_DOUBLE; };
+
+template<typename T>
+using ToDBusTypeFromPointer = ToDBusType<typename std::remove_pointer<T>::type>;
+
+struct DBusMessageDeleter
+{
+ void operator()(DBusMessage* message) const;
+};
+using DBusMessagePtr = std::unique_ptr<DBusMessage, DBusMessageDeleter>;
+
+class CDBusMessage
+{
+public:
+ CDBusMessage(const char *destination, const char *object, const char *interface, const char *method);
+ CDBusMessage(std::string const& destination, std::string const& object, std::string const& interface, std::string const& method);
+
+ void AppendObjectPath(const char *object);
+
+ template<typename T>
+ void AppendArgument(const T arg)
+ {
+ AppendWithType(ToDBusType<T>::TYPE, &arg);
+ }
+ void AppendArgument(const char **arrayString, unsigned int length);
+
+ template<typename TFirst>
+ void AppendArguments(const TFirst first)
+ {
+ AppendArgument(first);
+ // Recursion end
+ }
+ template<typename TFirst, typename... TArgs>
+ void AppendArguments(const TFirst first, const TArgs... args)
+ {
+ AppendArgument(first);
+ AppendArguments(args...);
+ }
+
+ DBusMessageIter * GetArgumentIter();
+
+ /**
+ * Retrieve simple arguments from DBus reply message
+ *
+ * You MUST use the correct fixed-width integer typedefs (e.g. std::uint16_t)
+ * corresponding to the DBus types for the variables or you will get potentially
+ * differing behavior between architectures since the DBus argument type detection
+ * is based on the width of the type.
+ *
+ * Complex arguments (arrays, structs) are not supported.
+ *
+ * Returned pointers for strings are only valid until the instance of this class
+ * is deleted.
+ *
+ * \throw std::logic_error if the message has no reply
+ * \return whether all arguments could be retrieved (false on argument type
+ * mismatch or when more arguments were to be retrieved than there are
+ * in the message)
+ */
+ template<typename... TArgs>
+ bool GetReplyArguments(TArgs*... args)
+ {
+ DBusMessageIter iter;
+ if (!InitializeReplyIter(&iter))
+ {
+ return false;
+ }
+ return GetReplyArgumentsWithIter(&iter, args...);
+ }
+
+ DBusMessage *SendSystem();
+ DBusMessage *SendSession();
+ DBusMessage *SendSystem(CDBusError& error);
+ DBusMessage *SendSession(CDBusError& error);
+
+ bool SendAsyncSystem();
+ bool SendAsyncSession();
+
+ DBusMessage *Send(DBusBusType type);
+ DBusMessage *Send(DBusBusType type, CDBusError& error);
+ DBusMessage *Send(DBusConnection *con, CDBusError& error);
+private:
+ void AppendWithType(int type, const void* value);
+ bool SendAsync(DBusBusType type);
+
+ void PrepareArgument();
+
+ bool InitializeReplyIter(DBusMessageIter* iter);
+ bool CheckTypeAndGetValue(DBusMessageIter* iter, int expectType, void* dest);
+ template<typename TFirst>
+ bool GetReplyArgumentsWithIter(DBusMessageIter* iter, TFirst* first)
+ {
+ // Recursion end
+ return CheckTypeAndGetValue(iter, ToDBusTypeFromPointer<TFirst>::TYPE, first);
+ }
+ template<typename TFirst, typename... TArgs>
+ bool GetReplyArgumentsWithIter(DBusMessageIter* iter, TFirst* first, TArgs*... args)
+ {
+ if (!CheckTypeAndGetValue(iter, ToDBusTypeFromPointer<TFirst>::TYPE, first))
+ {
+ return false;
+ }
+ // Ignore return value, if we try to read past the end of the message this
+ // will be catched by the type check (past-end type is DBUS_TYPE_INVALID)
+ dbus_message_iter_next(iter);
+ return GetReplyArgumentsWithIter(iter, args...);
+ }
+
+ DBusMessagePtr m_message;
+ DBusMessagePtr m_reply = nullptr;
+ DBusMessageIter m_args;
+ bool m_haveArgs;
+};
+
+template<>
+void CDBusMessage::AppendArgument<bool>(const bool arg);
+template<>
+void CDBusMessage::AppendArgument<std::string>(const std::string arg);
diff --git a/xbmc/platform/linux/DBusUtil.cpp b/xbmc/platform/linux/DBusUtil.cpp
new file mode 100644
index 0000000..2efb75c
--- /dev/null
+++ b/xbmc/platform/linux/DBusUtil.cpp
@@ -0,0 +1,296 @@
+/*
+ * 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 "DBusUtil.h"
+
+#include "utils/log.h"
+
+CVariant CDBusUtil::GetVariant(const char *destination, const char *object, const char *interface, const char *property)
+{
+//dbus-send --system --print-reply --dest=destination object org.freedesktop.DBus.Properties.Get string:interface string:property
+ CDBusMessage message(destination, object, "org.freedesktop.DBus.Properties", "Get");
+ CVariant result;
+
+ message.AppendArgument(interface);
+ message.AppendArgument(property);
+ DBusMessage *reply = message.SendSystem();
+
+ if (reply)
+ {
+ DBusMessageIter iter;
+
+ if (dbus_message_iter_init(reply, &iter))
+ {
+ if (!dbus_message_has_signature(reply, "v"))
+ CLog::Log(LOGERROR, "DBus: wrong signature on Get - should be \"v\" but was {}",
+ dbus_message_iter_get_signature(&iter));
+ else
+ result = ParseVariant(&iter);
+ }
+ }
+
+ return result;
+}
+
+CVariant CDBusUtil::GetAll(const char *destination, const char *object, const char *interface)
+{
+ CDBusMessage message(destination, object, "org.freedesktop.DBus.Properties", "GetAll");
+ CVariant properties;
+ message.AppendArgument(interface);
+ DBusMessage *reply = message.SendSystem();
+ if (reply)
+ {
+ DBusMessageIter iter;
+ if (dbus_message_iter_init(reply, &iter))
+ {
+ if (!dbus_message_has_signature(reply, "a{sv}"))
+ CLog::Log(LOGERROR, "DBus: wrong signature on GetAll - should be \"a{sv}\" but was {}",
+ dbus_message_iter_get_signature(&iter));
+ else
+ {
+ do
+ {
+ DBusMessageIter sub;
+ dbus_message_iter_recurse(&iter, &sub);
+ do
+ {
+ DBusMessageIter dict;
+ dbus_message_iter_recurse(&sub, &dict);
+ do
+ {
+ const char * key = NULL;
+
+ dbus_message_iter_get_basic(&dict, &key);
+ if (!dbus_message_iter_next(&dict))
+ break;
+
+ CVariant value = ParseVariant(&dict);
+
+ if (!value.isNull())
+ properties[key] = value;
+
+ } while (dbus_message_iter_next(&dict));
+
+ } while (dbus_message_iter_next(&sub));
+
+ } while (dbus_message_iter_next(&iter));
+ }
+ }
+ }
+
+ return properties;
+}
+
+CVariant CDBusUtil::ParseVariant(DBusMessageIter *itr)
+{
+ DBusMessageIter variant;
+ dbus_message_iter_recurse(itr, &variant);
+
+ return ParseType(&variant);
+}
+
+CVariant CDBusUtil::ParseType(DBusMessageIter *itr)
+{
+ CVariant value;
+ const char * string = NULL;
+ dbus_int32_t int32 = 0;
+ dbus_uint32_t uint32 = 0;
+ dbus_int64_t int64 = 0;
+ dbus_uint64_t uint64 = 0;
+ dbus_bool_t boolean = false;
+ double doublev = 0;
+
+ int type = dbus_message_iter_get_arg_type(itr);
+ switch (type)
+ {
+ case DBUS_TYPE_OBJECT_PATH:
+ case DBUS_TYPE_STRING:
+ dbus_message_iter_get_basic(itr, &string);
+ value = string;
+ break;
+ case DBUS_TYPE_UINT32:
+ dbus_message_iter_get_basic(itr, &uint32);
+ value = (uint64_t)uint32;
+ break;
+ case DBUS_TYPE_BYTE:
+ case DBUS_TYPE_INT32:
+ dbus_message_iter_get_basic(itr, &int32);
+ value = (int64_t)int32;
+ break;
+ case DBUS_TYPE_UINT64:
+ dbus_message_iter_get_basic(itr, &uint64);
+ value = (uint64_t)uint64;
+ break;
+ case DBUS_TYPE_INT64:
+ dbus_message_iter_get_basic(itr, &int64);
+ value = (int64_t)int64;
+ break;
+ case DBUS_TYPE_BOOLEAN:
+ dbus_message_iter_get_basic(itr, &boolean);
+ value = (bool)boolean;
+ break;
+ case DBUS_TYPE_DOUBLE:
+ dbus_message_iter_get_basic(itr, &doublev);
+ value = doublev;
+ break;
+ case DBUS_TYPE_ARRAY:
+ DBusMessageIter array;
+ dbus_message_iter_recurse(itr, &array);
+
+ value = CVariant::VariantTypeArray;
+
+ do
+ {
+ CVariant item = ParseType(&array);
+ if (!item.isNull())
+ value.push_back(item);
+ } while (dbus_message_iter_next(&array));
+ break;
+ }
+
+ return value;
+}
+
+bool CDBusUtil::TryMethodCall(DBusBusType bus, const char* destination, const char* object, const char* interface, const char* method)
+{
+ CDBusMessage message(destination, object, interface, method);
+ CDBusError error;
+ message.Send(bus, error);
+ if (error)
+ {
+ error.Log(LOGDEBUG, std::string("DBus method call to ") + interface + "." + method + " at " + object + " of " + destination + " failed");
+ }
+ return !error;
+}
+
+bool CDBusUtil::TryMethodCall(DBusBusType bus, std::string const& destination, std::string const& object, std::string const& interface, std::string const& method)
+{
+ return TryMethodCall(bus, destination.c_str(), object.c_str(), interface.c_str(), method.c_str());
+}
+
+CDBusConnection::CDBusConnection() = default;
+
+bool CDBusConnection::Connect(DBusBusType bus, bool openPrivate)
+{
+ CDBusError error;
+ Connect(bus, error, openPrivate);
+ if (error)
+ {
+ error.Log(LOGWARNING, "DBus connection failed");
+ return false;
+ }
+
+ return true;
+}
+
+bool CDBusConnection::Connect(DBusBusType bus, CDBusError& error, bool openPrivate)
+{
+ if (m_connection)
+ {
+ throw std::logic_error("Cannot reopen connected DBus connection");
+ }
+
+ m_connection.get_deleter().closeBeforeUnref = openPrivate;
+
+ if (openPrivate)
+ {
+ m_connection.reset(dbus_bus_get_private(bus, error));
+ }
+ else
+ {
+ m_connection.reset(dbus_bus_get(bus, error));
+ }
+
+ return !!m_connection;
+}
+
+CDBusConnection::operator DBusConnection*()
+{
+ return m_connection.get();
+}
+
+void CDBusConnection::DBusConnectionDeleter::operator()(DBusConnection* connection) const
+{
+ if (closeBeforeUnref)
+ {
+ dbus_connection_close(connection);
+ }
+ dbus_connection_unref(connection);
+}
+
+void CDBusConnection::Destroy()
+{
+ m_connection.reset();
+}
+
+
+CDBusError::CDBusError()
+{
+ dbus_error_init(&m_error);
+}
+
+CDBusError::~CDBusError()
+{
+ Reset();
+}
+
+void CDBusError::Reset()
+{
+ dbus_error_free(&m_error);
+}
+
+CDBusError::operator DBusError*()
+{
+ return &m_error;
+}
+
+bool CDBusError::IsSet() const
+{
+ return dbus_error_is_set(&m_error);
+}
+
+CDBusError::operator bool()
+{
+ return IsSet();
+}
+
+CDBusError::operator bool() const
+{
+ return IsSet();
+}
+
+std::string CDBusError::Name() const
+{
+ if (!IsSet())
+ {
+ throw std::logic_error("Cannot retrieve name of unset DBus error");
+ }
+ return m_error.name;
+}
+
+std::string CDBusError::Message() const
+{
+ if (!IsSet())
+ {
+ throw std::logic_error("Cannot retrieve message of unset DBus error");
+ }
+ return m_error.message;
+}
+
+void CDBusError::Log(std::string const& message) const
+{
+ Log(LOGERROR, message);
+}
+
+void CDBusError::Log(int level, const std::string& message) const
+{
+ if (!IsSet())
+ {
+ throw std::logic_error("Cannot log unset DBus error");
+ }
+ CLog::Log(level, "{}: {} - {}", message, m_error.name, m_error.message);
+}
diff --git a/xbmc/platform/linux/DBusUtil.h b/xbmc/platform/linux/DBusUtil.h
new file mode 100644
index 0000000..57349ac
--- /dev/null
+++ b/xbmc/platform/linux/DBusUtil.h
@@ -0,0 +1,88 @@
+/*
+ * 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 "DBusMessage.h"
+#include "utils/Variant.h"
+
+#include <memory>
+#include <string>
+
+#include <dbus/dbus.h>
+
+class CDBusUtil
+{
+public:
+ static CVariant GetAll(const char *destination, const char *object, const char *interface);
+ static CVariant GetVariant(const char *destination, const char *object, const char *interface, const char *property);
+ /**
+ * Try to call a DBus method and return whether the call succeeded
+ */
+ static bool TryMethodCall(DBusBusType bus, const char* destination, const char* object, const char* interface, const char* method);
+ /**
+ * Try to call a DBus method and return whether the call succeeded
+ */
+ static bool TryMethodCall(DBusBusType bus, std::string const& destination, std::string const& object, std::string const& interface, std::string const& method);
+
+private:
+ static CVariant ParseType(DBusMessageIter *itr);
+ static CVariant ParseVariant(DBusMessageIter *itr);
+};
+
+class CDBusConnection
+{
+public:
+ CDBusConnection();
+ bool Connect(DBusBusType bus, bool openPrivate = false);
+ bool Connect(DBusBusType bus, CDBusError& error, bool openPrivate = false);
+ void Destroy();
+ operator DBusConnection*();
+
+private:
+ CDBusConnection(CDBusConnection const& other) = delete;
+ CDBusConnection& operator=(CDBusConnection const& other) = delete;
+
+ struct DBusConnectionDeleter
+ {
+ DBusConnectionDeleter() {}
+ bool closeBeforeUnref = false;
+ void operator()(DBusConnection* connection) const;
+ };
+ std::unique_ptr<DBusConnection, DBusConnectionDeleter> m_connection;
+};
+
+class CDBusError
+{
+public:
+ CDBusError();
+ ~CDBusError();
+ operator DBusError*();
+ bool IsSet() const;
+ /**
+ * Reset this error wrapper
+ *
+ * If there was an error, it was handled and this error wrapper should be used
+ * again in a new call, it must be reset before that call.
+ */
+ void Reset();
+ // Keep because operator DBusError* would be used for if-statements on
+ // non-const CDBusError instead
+ operator bool();
+ operator bool() const;
+ std::string Name() const;
+ std::string Message() const;
+ void Log(std::string const& message = "DBus error") const;
+ void Log(int level, std::string const& message = "DBus error") const;
+
+private:
+ CDBusError(CDBusError const& other) = delete;
+ CDBusError& operator=(CDBusError const& other) = delete;
+
+ DBusError m_error;
+};
diff --git a/xbmc/platform/linux/FDEventMonitor.cpp b/xbmc/platform/linux/FDEventMonitor.cpp
new file mode 100644
index 0000000..9cd27c1
--- /dev/null
+++ b/xbmc/platform/linux/FDEventMonitor.cpp
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2014-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 "FDEventMonitor.h"
+
+#include "threads/SingleLock.h"
+#include "utils/log.h"
+
+#include <errno.h>
+#include <mutex>
+
+#include <poll.h>
+#include <sys/eventfd.h>
+#include <unistd.h>
+
+#include "PlatformDefs.h"
+
+CFDEventMonitor::CFDEventMonitor() :
+ CThread("FDEventMonitor")
+{
+}
+
+CFDEventMonitor::~CFDEventMonitor()
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ InterruptPoll();
+
+ if (m_wakeupfd >= 0)
+ {
+ /* sets m_bStop */
+ StopThread(false);
+
+ /* wake up the poll() call */
+ eventfd_write(m_wakeupfd, 1);
+
+ /* Wait for the thread to stop */
+ {
+ CSingleExit exit(m_mutex);
+ StopThread(true);
+ }
+
+ close(m_wakeupfd);
+ }
+}
+
+void CFDEventMonitor::AddFD(const MonitoredFD& monitoredFD, int& id)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ InterruptPoll();
+
+ AddFDLocked(monitoredFD, id);
+
+ StartMonitoring();
+}
+
+void CFDEventMonitor::AddFDs(const std::vector<MonitoredFD>& monitoredFDs,
+ std::vector<int>& ids)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ InterruptPoll();
+
+ for (unsigned int i = 0; i < monitoredFDs.size(); ++i)
+ {
+ int id;
+ AddFDLocked(monitoredFDs[i], id);
+ ids.push_back(id);
+ }
+
+ StartMonitoring();
+}
+
+void CFDEventMonitor::RemoveFD(int id)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ InterruptPoll();
+
+ if (m_monitoredFDs.erase(id) != 1)
+ {
+ CLog::Log(LOGERROR, "CFDEventMonitor::RemoveFD - Tried to remove non-existing monitoredFD {}",
+ id);
+ }
+
+ UpdatePollDescs();
+}
+
+void CFDEventMonitor::RemoveFDs(const std::vector<int>& ids)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ InterruptPoll();
+
+ for (unsigned int i = 0; i < ids.size(); ++i)
+ {
+ if (m_monitoredFDs.erase(ids[i]) != 1)
+ {
+ CLog::Log(LOGERROR,
+ "CFDEventMonitor::RemoveFDs - Tried to remove non-existing monitoredFD {} while "
+ "removing {} FDs",
+ ids[i], (unsigned)ids.size());
+ }
+ }
+
+ UpdatePollDescs();
+}
+
+void CFDEventMonitor::Process()
+{
+ eventfd_t dummy;
+
+ while (!m_bStop)
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ std::unique_lock<CCriticalSection> pollLock(m_pollMutex);
+
+ /*
+ * Leave the main mutex here to allow another thread to
+ * lock it while we are in poll().
+ * By then calling InterruptPoll() the other thread can
+ * wake up poll and wait for the processing to pause at
+ * the above lock(m_mutex).
+ */
+ lock.unlock();
+
+ int err = poll(&m_pollDescs[0], m_pollDescs.size(), -1);
+
+ if (err < 0 && errno != EINTR)
+ {
+ CLog::Log(LOGERROR, "CFDEventMonitor::Process - poll() failed, error {}, stopping monitoring",
+ errno);
+ StopThread(false);
+ }
+
+ // Something woke us up - either there is data available or we are being
+ // paused/stopped via m_wakeupfd.
+
+ for (unsigned int i = 0; i < m_pollDescs.size(); ++i)
+ {
+ struct pollfd& pollDesc = m_pollDescs[i];
+ int id = m_monitoredFDbyPollDescs[i];
+ const MonitoredFD& monitoredFD = m_monitoredFDs[id];
+
+ if (pollDesc.revents)
+ {
+ if (monitoredFD.callback)
+ {
+ monitoredFD.callback(id, pollDesc.fd, pollDesc.revents,
+ monitoredFD.callbackData);
+ }
+
+ if (pollDesc.revents & (POLLERR | POLLHUP | POLLNVAL))
+ {
+ CLog::Log(LOGERROR,
+ "CFDEventMonitor::Process - polled fd {} got revents 0x{:x}, removing it",
+ pollDesc.fd, pollDesc.revents);
+
+ /* Probably would be nice to inform our caller that their FD was
+ * dropped, but oh well... */
+ m_monitoredFDs.erase(id);
+ UpdatePollDescs();
+ }
+
+ pollDesc.revents = 0;
+ }
+ }
+
+ /* flush wakeup fd */
+ eventfd_read(m_wakeupfd, &dummy);
+
+ }
+}
+
+void CFDEventMonitor::AddFDLocked(const MonitoredFD& monitoredFD, int& id)
+{
+ id = m_nextID;
+
+ while (m_monitoredFDs.count(id))
+ {
+ ++id;
+ }
+ m_nextID = id + 1;
+
+ m_monitoredFDs[id] = monitoredFD;
+
+ AddPollDesc(id, monitoredFD.fd, monitoredFD.events);
+}
+
+void CFDEventMonitor::AddPollDesc(int id, int fd, short events)
+{
+ struct pollfd newPollFD;
+ newPollFD.fd = fd;
+ newPollFD.events = events;
+ newPollFD.revents = 0;
+
+ m_pollDescs.push_back(newPollFD);
+ m_monitoredFDbyPollDescs.push_back(id);
+}
+
+void CFDEventMonitor::UpdatePollDescs()
+{
+ m_monitoredFDbyPollDescs.clear();
+ m_pollDescs.clear();
+
+ for (const auto& it : m_monitoredFDs)
+ {
+ AddPollDesc(it.first, it.second.fd, it.second.events);
+ }
+}
+
+void CFDEventMonitor::StartMonitoring()
+{
+ if (!IsRunning())
+ {
+ /* Start the monitoring thread */
+
+ m_wakeupfd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
+ if (m_wakeupfd < 0)
+ {
+ CLog::Log(LOGERROR, "CFDEventMonitor::StartMonitoring - Failed to create eventfd, error {}",
+ errno);
+ return;
+ }
+
+ /* Add wakeup fd to the fd list */
+ int id;
+ AddFDLocked(MonitoredFD(m_wakeupfd, POLLIN, NULL, NULL), id);
+
+ Create(false);
+ }
+}
+
+void CFDEventMonitor::InterruptPoll()
+{
+ if (m_wakeupfd >= 0)
+ {
+ eventfd_write(m_wakeupfd, 1);
+ /* wait for the poll() result handling (if any) to end */
+ std::unique_lock<CCriticalSection> pollLock(m_pollMutex);
+ }
+}
diff --git a/xbmc/platform/linux/FDEventMonitor.h b/xbmc/platform/linux/FDEventMonitor.h
new file mode 100644
index 0000000..c0b8395
--- /dev/null
+++ b/xbmc/platform/linux/FDEventMonitor.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2014-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 "platform/Platform.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <map>
+#include <vector>
+
+#include <sys/epoll.h>
+
+/**
+ * Monitor a file descriptor with callback on poll() events.
+ */
+class CFDEventMonitor : public IPlatformService, private CThread
+{
+public:
+
+ typedef void (*EventCallback)(int id, int fd, short revents, void *data);
+
+ struct MonitoredFD
+ {
+ int fd = -1; /**< File descriptor to be monitored */
+ short events = 0; /**< Events to be monitored (see poll(2)) */
+
+ EventCallback callback = nullptr; /** Callback to be called on events */
+ void *callbackData = nullptr; /** data parameter for EventCallback */
+
+ MonitoredFD(int fd_, short events_, EventCallback callback_, void *callbackData_) :
+ fd(fd_), events(events_), callback(callback_), callbackData(callbackData_) {}
+ MonitoredFD() = default;
+ };
+
+ CFDEventMonitor();
+ ~CFDEventMonitor() override;
+
+ void AddFD(const MonitoredFD& monitoredFD, int& id);
+ void AddFDs(const std::vector<MonitoredFD>& monitoredFDs, std::vector<int>& ids);
+
+ void RemoveFD(int id);
+ void RemoveFDs(const std::vector<int>& ids);
+
+protected:
+ void Process() override;
+
+private:
+ void AddFDLocked(const MonitoredFD& monitoredFD, int& id);
+
+ void AddPollDesc(int id, int fd, short events);
+ void UpdatePollDescs();
+
+ void StartMonitoring();
+ void InterruptPoll();
+
+ std::map<int, MonitoredFD> m_monitoredFDs;
+
+ /* these are kept synchronized */
+ std::vector<int> m_monitoredFDbyPollDescs;
+ std::vector<struct pollfd> m_pollDescs;
+
+ int m_nextID = 0;
+ int m_wakeupfd = -1;
+
+ CCriticalSection m_mutex;
+ CCriticalSection m_pollMutex;
+};
diff --git a/xbmc/platform/linux/MemUtils.cpp b/xbmc/platform/linux/MemUtils.cpp
new file mode 100644
index 0000000..09dd22f
--- /dev/null
+++ b/xbmc/platform/linux/MemUtils.cpp
@@ -0,0 +1,79 @@
+/*
+ * 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 "utils/MemUtils.h"
+
+#include <cstdlib>
+#include <fstream>
+
+namespace KODI
+{
+namespace MEMORY
+{
+
+void* AlignedMalloc(size_t s, size_t alignTo)
+{
+ void* p = nullptr;
+ int res = posix_memalign(&p, alignTo, s);
+ if (res == EINVAL)
+ {
+ throw std::runtime_error("Failed to align memory, alignment is not a multiple of 2");
+ }
+ else if (res == ENOMEM)
+ {
+ throw std::runtime_error("Failed to align memory, insufficient memory available");
+ }
+ return p;
+}
+
+void AlignedFree(void* p)
+{
+ if (!p)
+ return;
+
+ free(p);
+}
+
+void GetMemoryStatus(MemoryStatus* buffer)
+{
+ if (!buffer)
+ return;
+
+ std::ifstream file("/proc/meminfo");
+
+ if (!file.is_open())
+ return;
+
+ uint64_t buffers;
+ uint64_t cached;
+ uint64_t free;
+ uint64_t total;
+ uint64_t reclaimable;
+
+ std::string token;
+
+ while (file >> token)
+ {
+ if (token == "Buffers:")
+ file >> buffers;
+ if (token == "Cached:")
+ file >> cached;
+ if (token == "MemFree:")
+ file >> free;
+ if (token == "MemTotal:")
+ file >> total;
+ if (token == "SReclaimable:")
+ file >> reclaimable;
+ }
+
+ buffer->totalPhys = total * 1024;
+ buffer->availPhys = (free + cached + reclaimable + buffers) * 1024;
+}
+
+}
+}
diff --git a/xbmc/platform/linux/OptionalsReg.cpp b/xbmc/platform/linux/OptionalsReg.cpp
new file mode 100644
index 0000000..82fb176
--- /dev/null
+++ b/xbmc/platform/linux/OptionalsReg.cpp
@@ -0,0 +1,114 @@
+/*
+ * 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 "OptionalsReg.h"
+
+
+//-----------------------------------------------------------------------------
+// ALSA
+//-----------------------------------------------------------------------------
+
+#ifdef HAS_ALSA
+#include "cores/AudioEngine/Sinks/AESinkALSA.h"
+bool OPTIONALS::ALSARegister()
+{
+ CAESinkALSA::Register();
+ return true;
+}
+#else
+bool OPTIONALS::ALSARegister()
+{
+ return false;
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// PulseAudio
+//-----------------------------------------------------------------------------
+
+#ifdef HAS_PULSEAUDIO
+#include "cores/AudioEngine/Sinks/AESinkPULSE.h"
+bool OPTIONALS::PulseAudioRegister()
+{
+ bool ret = CAESinkPULSE::Register();
+ return ret;
+}
+#else
+bool OPTIONALS::PulseAudioRegister()
+{
+ return false;
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// Pipewire
+//-----------------------------------------------------------------------------
+
+#ifdef HAS_PIPEWIRE
+#include "cores/AudioEngine/Sinks/pipewire/AESinkPipewire.h"
+bool OPTIONALS::PipewireRegister()
+{
+ bool ret = AE::SINK::CAESinkPipewire::Register();
+ return ret;
+}
+#else
+bool OPTIONALS::PipewireRegister()
+{
+ return false;
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// sndio
+//-----------------------------------------------------------------------------
+
+#ifdef HAS_SNDIO
+#include "cores/AudioEngine/Sinks/AESinkSNDIO.h"
+bool OPTIONALS::SndioRegister()
+{
+ CAESinkSNDIO::Register();
+ return true;
+}
+#else
+bool OPTIONALS::SndioRegister()
+{
+ return false;
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// Lirc
+//-----------------------------------------------------------------------------
+
+#ifdef HAS_LIRC
+#include "platform/linux/input/LIRC.h"
+#include "ServiceBroker.h"
+class OPTIONALS::CLircContainer
+{
+public:
+ CLircContainer()
+ {
+ m_lirc.Start();
+ }
+protected:
+ CLirc m_lirc;
+};
+#else
+class OPTIONALS::CLircContainer
+{
+};
+#endif
+
+OPTIONALS::CLircContainer* OPTIONALS::LircRegister()
+{
+ return new CLircContainer();
+}
+void OPTIONALS::delete_CLircContainer::operator()(CLircContainer *p) const
+{
+ delete p;
+}
diff --git a/xbmc/platform/linux/OptionalsReg.h b/xbmc/platform/linux/OptionalsReg.h
new file mode 100644
index 0000000..75fdb6a
--- /dev/null
+++ b/xbmc/platform/linux/OptionalsReg.h
@@ -0,0 +1,59 @@
+/*
+ * 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
+
+//-----------------------------------------------------------------------------
+// ALSA
+//-----------------------------------------------------------------------------
+
+namespace OPTIONALS
+{
+bool ALSARegister();
+}
+
+//-----------------------------------------------------------------------------
+// PulseAudio
+//-----------------------------------------------------------------------------
+
+namespace OPTIONALS
+{
+bool PulseAudioRegister();
+}
+
+//-----------------------------------------------------------------------------
+// Pipewire
+//-----------------------------------------------------------------------------
+
+namespace OPTIONALS
+{
+bool PipewireRegister();
+}
+
+//-----------------------------------------------------------------------------
+// sndio
+//-----------------------------------------------------------------------------
+
+namespace OPTIONALS
+{
+bool SndioRegister();
+}
+
+//-----------------------------------------------------------------------------
+// Lirc
+//-----------------------------------------------------------------------------
+
+namespace OPTIONALS
+{
+class CLircContainer;
+CLircContainer* LircRegister();
+struct delete_CLircContainer
+{
+ void operator()(CLircContainer *p) const;
+};
+}
diff --git a/xbmc/platform/linux/PlatformLinux.cpp b/xbmc/platform/linux/PlatformLinux.cpp
new file mode 100644
index 0000000..a7af098
--- /dev/null
+++ b/xbmc/platform/linux/PlatformLinux.cpp
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2005-2020 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 "PlatformLinux.h"
+
+#if defined(HAS_ALSA)
+#include "cores/AudioEngine/Sinks/alsa/ALSADeviceMonitor.h"
+#include "cores/AudioEngine/Sinks/alsa/ALSAHControlMonitor.h"
+#endif
+
+#include "utils/StringUtils.h"
+
+#if defined(HAS_ALSA)
+#include "platform/linux/FDEventMonitor.h"
+#endif
+
+#include "platform/linux/powermanagement/LinuxPowerSyscall.h"
+
+// clang-format off
+#if defined(HAS_GLES)
+#if defined(HAVE_WAYLAND)
+#include "windowing/wayland/WinSystemWaylandEGLContextGLES.h"
+#endif
+#if defined(HAVE_X11)
+#include "windowing/X11/WinSystemX11GLESContext.h"
+#endif
+#if defined(HAVE_GBM)
+#include "windowing/gbm/WinSystemGbmGLESContext.h"
+#endif
+#endif
+
+#if defined(HAS_GL)
+#if defined(HAVE_WAYLAND)
+#include "windowing/wayland/WinSystemWaylandEGLContextGL.h"
+#endif
+#if defined(HAVE_X11)
+#include "windowing/X11/WinSystemX11GLContext.h"
+#endif
+#if defined(HAVE_GBM)
+#include "windowing/gbm/WinSystemGbmGLContext.h"
+#endif
+#endif
+// clang-format on
+
+#include <cstdlib>
+
+CPlatform* CPlatform::CreateInstance()
+{
+ return new CPlatformLinux();
+}
+
+bool CPlatformLinux::InitStageOne()
+{
+ if (!CPlatformPosix::InitStageOne())
+ return false;
+
+ setenv("OS", "Linux", true); // for python scripts that check the OS
+
+#if defined(HAS_GLES)
+#if defined(HAVE_WAYLAND)
+ KODI::WINDOWING::WAYLAND::CWinSystemWaylandEGLContextGLES::Register();
+#endif
+#if defined(HAVE_X11)
+ KODI::WINDOWING::X11::CWinSystemX11GLESContext::Register();
+#endif
+#if defined(HAVE_GBM)
+ KODI::WINDOWING::GBM::CWinSystemGbmGLESContext::Register();
+#endif
+#endif
+
+#if defined(HAS_GL)
+#if defined(HAVE_WAYLAND)
+ KODI::WINDOWING::WAYLAND::CWinSystemWaylandEGLContextGL::Register();
+#endif
+#if defined(HAVE_X11)
+ KODI::WINDOWING::X11::CWinSystemX11GLContext::Register();
+#endif
+#if defined(HAVE_GBM)
+ KODI::WINDOWING::GBM::CWinSystemGbmGLContext::Register();
+#endif
+#endif
+
+ CLinuxPowerSyscall::Register();
+
+ std::string envSink;
+ if (getenv("KODI_AE_SINK"))
+ envSink = getenv("KODI_AE_SINK");
+
+ if (StringUtils::EqualsNoCase(envSink, "ALSA"))
+ {
+ OPTIONALS::ALSARegister();
+ }
+ else if (StringUtils::EqualsNoCase(envSink, "PULSE"))
+ {
+ OPTIONALS::PulseAudioRegister();
+ }
+ else if (StringUtils::EqualsNoCase(envSink, "PIPEWIRE"))
+ {
+ OPTIONALS::PipewireRegister();
+ }
+ else if (StringUtils::EqualsNoCase(envSink, "SNDIO"))
+ {
+ OPTIONALS::SndioRegister();
+ }
+ else if (StringUtils::EqualsNoCase(envSink, "ALSA+PULSE"))
+ {
+ OPTIONALS::ALSARegister();
+ OPTIONALS::PulseAudioRegister();
+ }
+ else
+ {
+ if (!OPTIONALS::PulseAudioRegister())
+ {
+ if (!OPTIONALS::PipewireRegister())
+ {
+ if (!OPTIONALS::ALSARegister())
+ {
+ OPTIONALS::SndioRegister();
+ }
+ }
+ }
+ }
+
+ m_lirc.reset(OPTIONALS::LircRegister());
+
+#if defined(HAS_ALSA)
+ RegisterComponent(std::make_shared<CFDEventMonitor>());
+#if defined(HAVE_LIBUDEV)
+ RegisterComponent(std::make_shared<CALSADeviceMonitor>());
+#endif
+#if !defined(HAVE_X11)
+ RegisterComponent(std::make_shared<CALSAHControlMonitor>());
+#endif
+#endif // HAS_ALSA
+ return true;
+}
+
+void CPlatformLinux::DeinitStageOne()
+{
+#if defined(HAS_ALSA)
+#if !defined(HAVE_X11)
+ DeregisterComponent(typeid(CALSAHControlMonitor));
+#endif
+#if defined(HAVE_LIBUDEV)
+ DeregisterComponent(typeid(CALSADeviceMonitor));
+#endif
+ DeregisterComponent(typeid(CFDEventMonitor));
+#endif // HAS_ALSA
+}
+
+bool CPlatformLinux::IsConfigureAddonsAtStartupEnabled()
+{
+#if defined(ADDONS_CONFIGURE_AT_STARTUP)
+ return true;
+#else
+ return false;
+#endif
+}
diff --git a/xbmc/platform/linux/PlatformLinux.h b/xbmc/platform/linux/PlatformLinux.h
new file mode 100644
index 0000000..b74deb6
--- /dev/null
+++ b/xbmc/platform/linux/PlatformLinux.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2005-2020 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 "platform/linux/OptionalsReg.h"
+#include "platform/posix/PlatformPosix.h"
+
+#include <memory>
+
+class CPlatformLinux : public CPlatformPosix
+{
+public:
+ CPlatformLinux() = default;
+
+ ~CPlatformLinux() override = default;
+
+ bool InitStageOne() override;
+ void DeinitStageOne() override;
+
+ bool IsConfigureAddonsAtStartupEnabled() override;
+
+private:
+ std::unique_ptr<OPTIONALS::CLircContainer, OPTIONALS::delete_CLircContainer> m_lirc;
+};
diff --git a/xbmc/platform/linux/SysfsPath.cpp b/xbmc/platform/linux/SysfsPath.cpp
new file mode 100644
index 0000000..af4f9cb
--- /dev/null
+++ b/xbmc/platform/linux/SysfsPath.cpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011-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 "SysfsPath.h"
+
+#include <exception>
+
+bool CSysfsPath::Exists()
+{
+ std::ifstream file(m_path);
+
+ if (!file.is_open())
+ return false;
+
+ return true;
+}
+
+template<>
+std::optional<std::string> CSysfsPath::Get()
+{
+ try
+ {
+ std::ifstream file(m_path);
+
+ std::string value;
+
+ std::getline(file, value);
+
+ if (file.bad())
+ {
+ CLog::LogF(LOGERROR, "error reading from '{}'", m_path);
+ return std::nullopt;
+ }
+
+ return value;
+ }
+ catch (const std::exception& e)
+ {
+ CLog::LogF(LOGERROR, "exception reading from '{}': {}", m_path, e.what());
+ return std::nullopt;
+ }
+}
diff --git a/xbmc/platform/linux/SysfsPath.h b/xbmc/platform/linux/SysfsPath.h
new file mode 100644
index 0000000..3c19703
--- /dev/null
+++ b/xbmc/platform/linux/SysfsPath.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2011-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 "utils/log.h"
+
+#include <exception>
+#include <fstream>
+#include <optional>
+#include <string>
+
+class CSysfsPath
+{
+public:
+ CSysfsPath() = default;
+ CSysfsPath(const std::string& path) : m_path(path) {}
+ ~CSysfsPath() = default;
+
+ bool Exists();
+
+ template<typename T>
+ std::optional<T> Get()
+ {
+ try
+ {
+ std::ifstream file(m_path);
+
+ T value;
+
+ file >> value;
+
+ if (file.bad())
+ {
+ CLog::LogF(LOGERROR, "error reading from '{}'", m_path);
+ return std::nullopt;
+ }
+
+ return value;
+ }
+ catch (const std::exception& e)
+ {
+ CLog::LogF(LOGERROR, "exception reading from '{}': {}", m_path, e.what());
+ return std::nullopt;
+ }
+ }
+
+private:
+ std::string m_path;
+};
+
+template<>
+std::optional<std::string> CSysfsPath::Get<std::string>();
diff --git a/xbmc/platform/linux/TimeUtils.cpp b/xbmc/platform/linux/TimeUtils.cpp
new file mode 100644
index 0000000..55cce92
--- /dev/null
+++ b/xbmc/platform/linux/TimeUtils.cpp
@@ -0,0 +1,14 @@
+/*
+ * Copyright (C) 2017-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 "TimeUtils.h"
+
+std::int64_t KODI::LINUX::TimespecDifference(timespec const& start, timespec const& end)
+{
+ return (end.tv_sec - start.tv_sec) * UINT64_C(1000000000) + (end.tv_nsec - start.tv_nsec);
+} \ No newline at end of file
diff --git a/xbmc/platform/linux/TimeUtils.h b/xbmc/platform/linux/TimeUtils.h
new file mode 100644
index 0000000..abbeb13
--- /dev/null
+++ b/xbmc/platform/linux/TimeUtils.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017-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 <cstdint>
+#include <time.h>
+
+namespace KODI
+{
+namespace LINUX
+{
+
+/**
+ * Calculate difference between two timespecs in nanoseconds
+ * \param start earlier time
+ * \param end later time
+ * \return (end - start) in nanoseconds
+ */
+std::int64_t TimespecDifference(timespec const& start, timespec const& end);
+
+}
+}
diff --git a/xbmc/platform/linux/input/CMakeLists.txt b/xbmc/platform/linux/input/CMakeLists.txt
new file mode 100644
index 0000000..ebb2ccd
--- /dev/null
+++ b/xbmc/platform/linux/input/CMakeLists.txt
@@ -0,0 +1,27 @@
+set(SOURCES "")
+set(HEADERS "")
+
+if(LIRCCLIENT_FOUND)
+ list(APPEND SOURCES LIRC.cpp)
+ list(APPEND HEADERS LIRC.h)
+endif()
+
+if("gbm" IN_LIST CORE_PLATFORM_NAME_LC)
+ if(LIBINPUT_FOUND)
+ list(APPEND SOURCES LibInputHandler.cpp
+ LibInputKeyboard.cpp
+ LibInputPointer.cpp
+ LibInputSettings.cpp
+ LibInputTouch.cpp)
+
+ list(APPEND HEADERS LibInputHandler.h
+ LibInputKeyboard.h
+ LibInputPointer.h
+ LibInputSettings.h
+ LibInputTouch.h)
+ endif()
+endif()
+
+if(SOURCES)
+ core_add_library(input_linux)
+endif()
diff --git a/xbmc/platform/linux/input/LIRC.cpp b/xbmc/platform/linux/input/LIRC.cpp
new file mode 100644
index 0000000..6900f5c
--- /dev/null
+++ b/xbmc/platform/linux/input/LIRC.cpp
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2007-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 "LIRC.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SingleLock.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#include <fcntl.h>
+#include <lirc/lirc_client.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#include "PlatformDefs.h"
+
+using namespace std::chrono_literals;
+
+CLirc::CLirc() : CThread("Lirc")
+{
+}
+
+CLirc::~CLirc()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_fd > 0)
+ shutdown(m_fd, SHUT_RDWR);
+ }
+ StopThread();
+}
+
+void CLirc::Start()
+{
+ Create();
+ SetPriority(ThreadPriority::LOWEST);
+}
+
+void CLirc::Process()
+{
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ throw std::runtime_error("CSettingsComponent needs to exist before starting CLirc");
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ throw std::runtime_error("CSettings needs to exist before starting CLirc");
+
+ while (!settings->IsLoaded())
+ CThread::Sleep(1000ms);
+
+ m_profileId = settingsComponent->GetProfileManager()->GetCurrentProfileId();
+ m_irTranslator.Load("Lircmap.xml");
+
+ // make sure work-around (CheckDaemon) uses the same socket path as lirc_init
+ const char* socket_path = getenv("LIRC_SOCKET_PATH");
+ socket_path = socket_path ? socket_path : "/var/run/lirc/lircd";
+ setenv("LIRC_SOCKET_PATH", socket_path, 0);
+
+ while (!m_bStop)
+ {
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // lirc_client is buggy because it does not close socket, if connect fails
+ // work around by checking if daemon is running before calling lirc_init
+ if (!CheckDaemon())
+ {
+ CSingleExit lock(m_critSection);
+ CThread::Sleep(1000ms);
+ continue;
+ }
+
+ m_fd = lirc_init(const_cast<char*>("kodi"), 0);
+ if (m_fd <= 0)
+ {
+ CSingleExit lock(m_critSection);
+ CThread::Sleep(1000ms);
+ continue;
+ }
+ }
+
+ char *code;
+ while (!m_bStop)
+ {
+ int ret = lirc_nextcode(&code);
+ if (ret < 0)
+ {
+ lirc_deinit();
+ CThread::Sleep(1000ms);
+ break;
+ }
+ if (code != nullptr)
+ {
+ int profileId = settingsComponent->GetProfileManager()->GetCurrentProfileId();
+ if (m_profileId != profileId)
+ {
+ m_profileId = profileId;
+ m_irTranslator.Load("Lircmap.xml");
+ }
+ ProcessCode(code);
+ free(code);
+ }
+ }
+ }
+
+ lirc_deinit();
+}
+
+void CLirc::ProcessCode(char *buf)
+{
+ // Parse the result. Sample line:
+ // 000000037ff07bdd 00 OK mceusb
+ char scanCode[128];
+ char buttonName[128];
+ char repeatStr[4];
+ char deviceName[128];
+ sscanf(buf, "%s %s %s %s", &scanCode[0], &repeatStr[0], &buttonName[0], &deviceName[0]);
+
+ // Some template LIRC configuration have button names in apostrophes or quotes.
+ // If we got a quoted button name, strip 'em
+ unsigned int buttonNameLen = strlen(buttonName);
+ if (buttonNameLen > 2 &&
+ ((buttonName[0] == '\'' && buttonName[buttonNameLen-1] == '\'') ||
+ ((buttonName[0] == '"' && buttonName[buttonNameLen-1] == '"'))))
+ {
+ memmove(buttonName, buttonName + 1, buttonNameLen - 2);
+ buttonName[buttonNameLen - 2] = '\0';
+ }
+
+ int button = m_irTranslator.TranslateButton(deviceName, buttonName);
+
+ char *end = nullptr;
+ long repeat = strtol(repeatStr, &end, 16);
+ if (!end || *end != 0)
+ CLog::Log(LOGERROR, "LIRC: invalid non-numeric character in expression {}", repeatStr);
+
+ if (repeat == 0)
+ {
+ CLog::Log(LOGDEBUG, "LIRC: - NEW {} {} {} {} ({})", &scanCode[0], &repeatStr[0], &buttonName[0],
+ &deviceName[0], buttonName);
+ m_firstClickTime = std::chrono::steady_clock::now();
+
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_BUTTON;
+ newEvent.keybutton.button = button;
+ newEvent.keybutton.holdtime = 0;
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ return;
+ }
+ else if (repeat > CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_remoteDelay)
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_BUTTON;
+ newEvent.keybutton.button = button;
+
+ auto now = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_firstClickTime);
+
+ newEvent.keybutton.holdtime = duration.count();
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ }
+}
+
+bool CLirc::CheckDaemon()
+{
+ const char* socket_path = getenv("LIRC_SOCKET_PATH");
+
+ struct sockaddr_un addr_un;
+ if (strlen(socket_path) + 1 > sizeof(addr_un.sun_path))
+ {
+ return false;
+ }
+
+ addr_un.sun_family = AF_UNIX;
+ strcpy(addr_un.sun_path, socket_path);
+
+ int fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd == -1)
+ {
+ return false;
+ }
+
+ if (connect(fd, reinterpret_cast<struct sockaddr*>(&addr_un), sizeof(addr_un)) == -1)
+ {
+ close(fd);
+ return false;
+ }
+
+ close(fd);
+ return true;
+}
diff --git a/xbmc/platform/linux/input/LIRC.h b/xbmc/platform/linux/input/LIRC.h
new file mode 100644
index 0000000..f08638d
--- /dev/null
+++ b/xbmc/platform/linux/input/LIRC.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2007-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 "input/IRTranslator.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <chrono>
+#include <string>
+
+class CLirc : CThread
+{
+public:
+ CLirc();
+ ~CLirc() override;
+ void Start();
+
+protected:
+ void Process() override;
+ void ProcessCode(char *buf);
+ bool CheckDaemon();
+
+ int m_fd = -1;
+ std::chrono::time_point<std::chrono::steady_clock> m_firstClickTime;
+ CCriticalSection m_critSection;
+ CIRTranslator m_irTranslator;
+ int m_profileId;
+};
diff --git a/xbmc/platform/linux/input/LibInputHandler.cpp b/xbmc/platform/linux/input/LibInputHandler.cpp
new file mode 100644
index 0000000..a8c39f5
--- /dev/null
+++ b/xbmc/platform/linux/input/LibInputHandler.cpp
@@ -0,0 +1,301 @@
+/*
+ * 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 "LibInputHandler.h"
+
+#include "LibInputKeyboard.h"
+#include "LibInputPointer.h"
+#include "LibInputSettings.h"
+#include "LibInputTouch.h"
+#include "ServiceBroker.h"
+#include "interfaces/AnnouncementManager.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <string.h>
+
+#include <fcntl.h>
+#include <linux/input.h>
+#include <sys/epoll.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+static int open_restricted(const char *path, int flags, void __attribute__((unused)) *user_data)
+{
+ int fd = open(path, flags);
+
+ if (fd < 0)
+ {
+ CLog::Log(LOGERROR, "{} - failed to open {} ({})", __FUNCTION__, path, strerror(errno));
+ return -errno;
+ }
+
+ auto ret = ioctl(fd, EVIOCGRAB, (void*)1);
+ if (ret < 0)
+ {
+ CLog::Log(LOGDEBUG, "{} - grab requested, but failed for {} ({})", __FUNCTION__, path,
+ strerror(errno));
+ }
+
+ return fd;
+}
+
+static void close_restricted(int fd, void __attribute__((unused)) *user_data)
+{
+ close(fd);
+}
+
+static const struct libinput_interface m_interface =
+{
+ open_restricted,
+ close_restricted,
+};
+
+static void LogHandler(libinput __attribute__((unused)) *libinput, libinput_log_priority priority, const char *format, va_list args)
+{
+ if (priority == LIBINPUT_LOG_PRIORITY_DEBUG)
+ {
+ char buf[512];
+ int n = vsnprintf(buf, sizeof(buf), format, args);
+ if (n > 0)
+ CLog::Log(LOGDEBUG, "libinput: {}", buf);
+ }
+}
+
+CLibInputHandler::CLibInputHandler() : CThread("libinput")
+{
+ m_udev = udev_new();
+ if (!m_udev)
+ {
+ CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to get udev context for libinput",
+ __FUNCTION__);
+ return;
+ }
+
+ m_li = libinput_udev_create_context(&m_interface, nullptr, m_udev);
+ if (!m_li)
+ {
+ CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to get libinput context", __FUNCTION__);
+ return;
+ }
+
+ libinput_log_set_handler(m_li, LogHandler);
+ libinput_log_set_priority(m_li, LIBINPUT_LOG_PRIORITY_DEBUG);
+
+ auto ret = libinput_udev_assign_seat(m_li, "seat0");
+ if (ret < 0)
+ CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to assign seat", __FUNCTION__);
+
+ m_liFd = libinput_get_fd(m_li);
+
+ m_keyboard.reset(new CLibInputKeyboard());
+ m_pointer.reset(new CLibInputPointer());
+ m_touch.reset(new CLibInputTouch());
+ m_settings.reset(new CLibInputSettings(this));
+
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+CLibInputHandler::~CLibInputHandler()
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+ StopThread();
+
+ libinput_unref(m_li);
+ udev_unref(m_udev);
+}
+
+void CLibInputHandler::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (flag & (ANNOUNCEMENT::System))
+ {
+ if (message == "OnSleep")
+ libinput_suspend(m_li);
+ else if (message == "OnWake")
+ {
+ auto ret = libinput_resume(m_li);
+ if (ret < 0)
+ CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to resume monitoring", __FUNCTION__);
+ }
+ }
+}
+
+bool CLibInputHandler::SetKeymap(const std::string& layout)
+{
+ return m_keyboard->SetKeymap(layout);
+}
+
+void CLibInputHandler::Start()
+{
+ Create();
+ SetPriority(ThreadPriority::LOWEST);
+}
+
+void CLibInputHandler::Process()
+{
+ int epollFd = epoll_create1(EPOLL_CLOEXEC);
+ if (epollFd < 0)
+ {
+ CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to create epoll file descriptor: {}",
+ __FUNCTION__, strerror(-errno));
+ return;
+ }
+
+ epoll_event event;
+ event.events = EPOLLIN;
+ event.data.fd = m_liFd;
+
+ auto ret = epoll_ctl(epollFd, EPOLL_CTL_ADD, m_liFd, &event);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to add file descriptor to epoll: {}",
+ __FUNCTION__, strerror(-errno));
+ close(epollFd);
+ return;
+ }
+
+ while (!m_bStop)
+ {
+ epoll_wait(epollFd, &event, 1, 200);
+
+ ret = libinput_dispatch(m_li);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CLibInputHandler::{} - libinput_dispatch failed: {}", __FUNCTION__,
+ strerror(-errno));
+ close(epollFd);
+ return;
+ }
+
+ libinput_event *ev;
+ while ((ev = libinput_get_event(m_li)) != nullptr)
+ {
+ ProcessEvent(ev);
+ libinput_event_destroy(ev);
+ }
+ }
+
+ ret = close(epollFd);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CLibInputHandler::{} - failed to close epoll file descriptor: {}",
+ __FUNCTION__, strerror(-errno));
+ return;
+ }
+}
+
+void CLibInputHandler::ProcessEvent(libinput_event *ev)
+{
+ libinput_event_type type = libinput_event_get_type(ev);
+ libinput_device *dev = libinput_event_get_device(ev);
+
+ switch (type)
+ {
+ case LIBINPUT_EVENT_DEVICE_ADDED:
+ DeviceAdded(dev);
+ break;
+ case LIBINPUT_EVENT_DEVICE_REMOVED:
+ DeviceRemoved(dev);
+ break;
+ case LIBINPUT_EVENT_POINTER_BUTTON:
+ m_pointer->ProcessButton(libinput_event_get_pointer_event(ev));
+ break;
+ case LIBINPUT_EVENT_POINTER_MOTION:
+ m_pointer->ProcessMotion(libinput_event_get_pointer_event(ev));
+ break;
+ case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE:
+ m_pointer->ProcessMotionAbsolute(libinput_event_get_pointer_event(ev));
+ break;
+ case LIBINPUT_EVENT_POINTER_AXIS:
+ m_pointer->ProcessAxis(libinput_event_get_pointer_event(ev));
+ break;
+ case LIBINPUT_EVENT_KEYBOARD_KEY:
+ m_keyboard->ProcessKey(libinput_event_get_keyboard_event(ev));
+ m_keyboard->UpdateLeds(dev);
+ break;
+ case LIBINPUT_EVENT_TOUCH_DOWN:
+ m_touch->ProcessTouchDown(libinput_event_get_touch_event(ev));
+ break;
+ case LIBINPUT_EVENT_TOUCH_MOTION:
+ m_touch->ProcessTouchMotion(libinput_event_get_touch_event(ev));
+ break;
+ case LIBINPUT_EVENT_TOUCH_UP:
+ m_touch->ProcessTouchUp(libinput_event_get_touch_event(ev));
+ break;
+ case LIBINPUT_EVENT_TOUCH_CANCEL:
+ m_touch->ProcessTouchCancel(libinput_event_get_touch_event(ev));
+ break;
+ case LIBINPUT_EVENT_TOUCH_FRAME:
+ m_touch->ProcessTouchFrame(libinput_event_get_touch_event(ev));
+ break;
+
+ default:
+ break;
+ }
+}
+
+void CLibInputHandler::DeviceAdded(libinput_device *dev)
+{
+ const char *sysname = libinput_device_get_sysname(dev);
+ const char *name = libinput_device_get_name(dev);
+
+ if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_TOUCH))
+ {
+ CLog::Log(LOGDEBUG, "CLibInputHandler::{} - touch type device added: {} ({})", __FUNCTION__,
+ name, sysname);
+ m_devices.push_back(libinput_device_ref(dev));
+ }
+
+ if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_POINTER))
+ {
+ CLog::Log(LOGDEBUG, "CLibInputHandler::{} - pointer type device added: {} ({})", __FUNCTION__,
+ name, sysname);
+ m_devices.push_back(libinput_device_ref(dev));
+ }
+
+ if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_KEYBOARD))
+ {
+ CLog::Log(LOGDEBUG, "CLibInputHandler::{} - keyboard type device added: {} ({})", __FUNCTION__,
+ name, sysname);
+ m_devices.push_back(libinput_device_ref(dev));
+ m_keyboard->GetRepeat(dev);
+ }
+}
+
+void CLibInputHandler::DeviceRemoved(libinput_device *dev)
+{
+ const char *sysname = libinput_device_get_sysname(dev);
+ const char *name = libinput_device_get_name(dev);
+
+ if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_TOUCH))
+ {
+ CLog::Log(LOGDEBUG, "CLibInputHandler::{} - touch type device removed: {} ({})", __FUNCTION__,
+ name, sysname);
+ auto device = std::find(m_devices.begin(), m_devices.end(), libinput_device_unref(dev));
+ m_devices.erase(device);
+ }
+
+ if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_POINTER))
+ {
+ CLog::Log(LOGDEBUG, "CLibInputHandler::{} - pointer type device removed: {} ({})", __FUNCTION__,
+ name, sysname);
+ auto device = std::find(m_devices.begin(), m_devices.end(), libinput_device_unref(dev));
+ m_devices.erase(device);
+ }
+
+ if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_KEYBOARD))
+ {
+ CLog::Log(LOGDEBUG, "CLibInputHandler::{} - keyboard type device removed: {} ({})",
+ __FUNCTION__, name, sysname);
+ auto device = std::find(m_devices.begin(), m_devices.end(), libinput_device_unref(dev));
+ m_devices.erase(device);
+ }
+}
diff --git a/xbmc/platform/linux/input/LibInputHandler.h b/xbmc/platform/linux/input/LibInputHandler.h
new file mode 100644
index 0000000..659d086
--- /dev/null
+++ b/xbmc/platform/linux/input/LibInputHandler.h
@@ -0,0 +1,56 @@
+/*
+ * 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 "interfaces/IAnnouncer.h"
+#include "threads/Thread.h"
+
+#include <memory>
+#include <vector>
+
+#include <libinput.h>
+#include <libudev.h>
+
+class CLibInputKeyboard;
+class CLibInputPointer;
+class CLibInputSettings;
+class CLibInputTouch;
+
+class CLibInputHandler : CThread, public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ CLibInputHandler();
+ ~CLibInputHandler() override;
+
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ void Start();
+
+ bool SetKeymap(const std::string& layout);
+
+private:
+ void Process() override;
+ void ProcessEvent(libinput_event *ev);
+ void DeviceAdded(libinput_device *dev);
+ void DeviceRemoved(libinput_device *dev);
+
+ udev *m_udev;
+ libinput *m_li;
+ int m_liFd;
+
+ std::unique_ptr<CLibInputKeyboard> m_keyboard;
+ std::unique_ptr<CLibInputPointer> m_pointer;
+ std::unique_ptr<CLibInputSettings> m_settings;
+ std::unique_ptr<CLibInputTouch> m_touch;
+ std::vector<libinput_device*> m_devices;
+};
+
diff --git a/xbmc/platform/linux/input/LibInputKeyboard.cpp b/xbmc/platform/linux/input/LibInputKeyboard.cpp
new file mode 100644
index 0000000..657588a
--- /dev/null
+++ b/xbmc/platform/linux/input/LibInputKeyboard.cpp
@@ -0,0 +1,387 @@
+/*
+ * 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 "LibInputKeyboard.h"
+
+#include "LibInputSettings.h"
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <map>
+#include <string.h>
+
+#include <fcntl.h>
+#include <linux/input.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+#include <xkbcommon/xkbcommon-keysyms.h>
+#include <xkbcommon/xkbcommon-names.h>
+
+namespace
+{
+constexpr int REPEAT_DELAY = 400;
+constexpr int REPEAT_RATE = 80;
+
+static const std::map<xkb_keysym_t, XBMCKey> xkbMap =
+{
+ // Function keys before start of ASCII printable character range
+ { XKB_KEY_BackSpace, XBMCK_BACKSPACE },
+ { XKB_KEY_Tab, XBMCK_TAB },
+ { XKB_KEY_Clear, XBMCK_CLEAR },
+ { XKB_KEY_Return, XBMCK_RETURN },
+ { XKB_KEY_Pause, XBMCK_PAUSE },
+ { XKB_KEY_Escape, XBMCK_ESCAPE },
+
+ // ASCII printable range - not included here
+
+ // Function keys after end of ASCII printable character range
+ { XKB_KEY_Delete, XBMCK_DELETE },
+
+ // Multimedia keys
+ { XKB_KEY_XF86Back, XBMCK_BROWSER_BACK },
+ { XKB_KEY_XF86Forward, XBMCK_BROWSER_FORWARD },
+ { XKB_KEY_XF86Refresh, XBMCK_BROWSER_REFRESH },
+ { XKB_KEY_XF86Stop, XBMCK_BROWSER_STOP },
+ { XKB_KEY_XF86Search, XBMCK_BROWSER_SEARCH },
+ // XKB_KEY_XF86Favorites could be XBMCK_BROWSER_FAVORITES or XBMCK_FAVORITES,
+ // XBMCK_FAVORITES was chosen here because it is more general
+ { XKB_KEY_XF86HomePage, XBMCK_BROWSER_HOME },
+ { XKB_KEY_XF86AudioMute, XBMCK_VOLUME_MUTE },
+ { XKB_KEY_XF86AudioLowerVolume, XBMCK_VOLUME_DOWN },
+ { XKB_KEY_XF86AudioRaiseVolume, XBMCK_VOLUME_UP },
+ { XKB_KEY_XF86AudioNext, XBMCK_MEDIA_NEXT_TRACK },
+ { XKB_KEY_XF86AudioPrev, XBMCK_MEDIA_PREV_TRACK },
+ { XKB_KEY_XF86AudioStop, XBMCK_MEDIA_STOP },
+ { XKB_KEY_XF86AudioPause, XBMCK_MEDIA_PLAY_PAUSE },
+ { XKB_KEY_XF86Mail, XBMCK_LAUNCH_MAIL },
+ { XKB_KEY_XF86Select, XBMCK_LAUNCH_MEDIA_SELECT },
+ { XKB_KEY_XF86Launch0, XBMCK_LAUNCH_APP1 },
+ { XKB_KEY_XF86Launch1, XBMCK_LAUNCH_APP2 },
+ { XKB_KEY_XF86WWW, XBMCK_LAUNCH_FILE_BROWSER },
+ { XKB_KEY_XF86AudioMedia, XBMCK_LAUNCH_MEDIA_CENTER },
+ { XKB_KEY_XF86AudioRewind, XBMCK_MEDIA_REWIND },
+ { XKB_KEY_XF86AudioForward, XBMCK_MEDIA_FASTFORWARD },
+
+ // Numeric keypad
+ { XKB_KEY_KP_0, XBMCK_KP0 },
+ { XKB_KEY_KP_1, XBMCK_KP1 },
+ { XKB_KEY_KP_2, XBMCK_KP2 },
+ { XKB_KEY_KP_3, XBMCK_KP3 },
+ { XKB_KEY_KP_4, XBMCK_KP4 },
+ { XKB_KEY_KP_5, XBMCK_KP5 },
+ { XKB_KEY_KP_6, XBMCK_KP6 },
+ { XKB_KEY_KP_7, XBMCK_KP7 },
+ { XKB_KEY_KP_8, XBMCK_KP8 },
+ { XKB_KEY_KP_9, XBMCK_KP9 },
+ { XKB_KEY_KP_Decimal, XBMCK_KP_PERIOD },
+ { XKB_KEY_KP_Divide, XBMCK_KP_DIVIDE },
+ { XKB_KEY_KP_Multiply, XBMCK_KP_MULTIPLY },
+ { XKB_KEY_KP_Subtract, XBMCK_KP_MINUS },
+ { XKB_KEY_KP_Add, XBMCK_KP_PLUS },
+ { XKB_KEY_KP_Enter, XBMCK_KP_ENTER },
+ { XKB_KEY_KP_Equal, XBMCK_KP_EQUALS },
+
+ // Arrows + Home/End pad
+ { XKB_KEY_Up, XBMCK_UP },
+ { XKB_KEY_Down, XBMCK_DOWN },
+ { XKB_KEY_Right, XBMCK_RIGHT },
+ { XKB_KEY_Left, XBMCK_LEFT },
+ { XKB_KEY_Insert, XBMCK_INSERT },
+ { XKB_KEY_Home, XBMCK_HOME },
+ { XKB_KEY_End, XBMCK_END },
+ { XKB_KEY_Page_Up, XBMCK_PAGEUP },
+ { XKB_KEY_Page_Down, XBMCK_PAGEDOWN },
+
+ // Key state modifier keys
+ { XKB_KEY_Num_Lock, XBMCK_NUMLOCK },
+ { XKB_KEY_Caps_Lock, XBMCK_CAPSLOCK },
+ { XKB_KEY_Scroll_Lock, XBMCK_SCROLLOCK },
+ { XKB_KEY_Shift_R, XBMCK_RSHIFT },
+ { XKB_KEY_Shift_L, XBMCK_LSHIFT },
+ { XKB_KEY_Control_R, XBMCK_RCTRL },
+ { XKB_KEY_Control_L, XBMCK_LCTRL },
+ { XKB_KEY_Alt_R, XBMCK_RALT },
+ { XKB_KEY_Alt_L, XBMCK_LALT },
+ { XKB_KEY_Meta_R, XBMCK_RMETA },
+ { XKB_KEY_Meta_L, XBMCK_LMETA },
+ { XKB_KEY_Super_R, XBMCK_RSUPER },
+ { XKB_KEY_Super_L, XBMCK_LSUPER },
+ // XKB does not have XBMCK_MODE/"Alt Gr" - probably equal to XKB_KEY_Alt_R
+ { XKB_KEY_Multi_key, XBMCK_COMPOSE },
+
+ // Miscellaneous function keys
+ { XKB_KEY_Help, XBMCK_HELP },
+ { XKB_KEY_Print, XBMCK_PRINT },
+ { XKB_KEY_Sys_Req, XBMCK_SYSREQ},
+ { XKB_KEY_Break, XBMCK_BREAK },
+ { XKB_KEY_Menu, XBMCK_MENU },
+ { XKB_KEY_XF86PowerOff, XBMCK_POWER },
+ { XKB_KEY_EcuSign, XBMCK_EURO },
+ { XKB_KEY_Undo, XBMCK_UNDO },
+ { XKB_KEY_XF86Sleep, XBMCK_SLEEP },
+ // Unmapped: XBMCK_GUIDE, XBMCK_SETTINGS, XBMCK_INFO
+ { XKB_KEY_XF86Red, XBMCK_RED },
+ { XKB_KEY_XF86Green, XBMCK_GREEN },
+ { XKB_KEY_XF86Yellow, XBMCK_YELLOW },
+ { XKB_KEY_XF86Blue, XBMCK_BLUE },
+ // Unmapped: XBMCK_ZOOM, XBMCK_TEXT
+ { XKB_KEY_XF86Favorites, XBMCK_FAVORITES },
+ { XKB_KEY_XF86HomePage, XBMCK_HOMEPAGE },
+ // Unmapped: XBMCK_CONFIG, XBMCK_EPG
+
+ // Media keys
+ { XKB_KEY_XF86Eject, XBMCK_EJECT },
+ { XKB_KEY_Cancel, XBMCK_STOP },
+ { XKB_KEY_XF86AudioRecord, XBMCK_RECORD },
+ // XBMCK_REWIND clashes with XBMCK_MEDIA_REWIND
+ { XKB_KEY_XF86Phone, XBMCK_PHONE },
+ { XKB_KEY_XF86AudioPlay, XBMCK_PLAY },
+ { XKB_KEY_XF86AudioRandomPlay, XBMCK_SHUFFLE }
+ // XBMCK_FASTFORWARD clashes with XBMCK_MEDIA_FASTFORWARD
+};
+} // namespace
+
+CLibInputKeyboard::CLibInputKeyboard()
+ : m_repeatTimer(std::bind(&CLibInputKeyboard::KeyRepeatTimeout, this))
+{
+ m_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+ if (!m_ctx)
+ {
+ CLog::Log(LOGERROR, "CLibInputKeyboard::{} - failed to create xkb context", __FUNCTION__);
+ return;
+ }
+
+ std::string layout = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CLibInputSettings::SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT);
+
+ if (!SetKeymap(layout))
+ {
+ CLog::Log(LOGERROR, "CLibInputKeyboard::{} - failed set default keymap", __FUNCTION__);
+ return;
+ }
+}
+
+CLibInputKeyboard::~CLibInputKeyboard()
+{
+ xkb_state_unref(m_state);
+ xkb_keymap_unref(m_keymap);
+ xkb_context_unref(m_ctx);
+}
+
+bool CLibInputKeyboard::SetKeymap(const std::string& layout)
+{
+ xkb_state_unref(m_state);
+ xkb_keymap_unref(m_keymap);
+
+ xkb_rule_names names;
+
+ names.rules = nullptr;
+ names.model = nullptr;
+ names.layout = layout.c_str();
+ names.variant = nullptr;
+ names.options = nullptr;
+
+ m_keymap = xkb_keymap_new_from_names(m_ctx, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
+ if (!m_keymap)
+ {
+ CLog::Log(LOGERROR, "CLibInputKeyboard::{} - failed to compile keymap", __FUNCTION__);
+ return false;
+ }
+
+ m_state = xkb_state_new(m_keymap);
+ if (!m_state)
+ {
+ CLog::Log(LOGERROR, "CLibInputKeyboard::{} - failed to create xkb state", __FUNCTION__);
+ return false;
+ }
+
+ m_modindex[0] = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_CTRL);
+ m_modindex[1] = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_ALT);
+ m_modindex[2] = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_SHIFT);
+ m_modindex[3] = xkb_keymap_mod_get_index(m_keymap, XKB_MOD_NAME_LOGO);
+
+ m_ledindex[0] = xkb_keymap_led_get_index(m_keymap, XKB_LED_NAME_NUM);
+ m_ledindex[1] = xkb_keymap_led_get_index(m_keymap, XKB_LED_NAME_CAPS);
+ m_ledindex[2] = xkb_keymap_led_get_index(m_keymap, XKB_LED_NAME_SCROLL);
+
+ m_leds = 0;
+
+ return true;
+}
+
+void CLibInputKeyboard::ProcessKey(libinput_event_keyboard *e)
+{
+ if (!m_ctx || !m_keymap || !m_state)
+ return;
+
+ XBMC_Event event = {};
+
+ const uint32_t xkbkey = libinput_event_keyboard_get_key(e) + 8;
+ const bool pressed = libinput_event_keyboard_get_key_state(e) == LIBINPUT_KEY_STATE_PRESSED;
+
+ event.type = pressed ? XBMC_KEYDOWN : XBMC_KEYUP;
+ xkb_state_update_key(m_state, xkbkey, pressed ? XKB_KEY_DOWN : XKB_KEY_UP);
+
+ const xkb_keysym_t keysym = xkb_state_key_get_one_sym(m_state, xkbkey);
+
+ int mod = XBMCKMOD_NONE;
+
+ xkb_state_component modtype = xkb_state_component(XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED);
+ if (xkb_state_mod_index_is_active(m_state, m_modindex[0], modtype) && ((keysym != XBMCK_LCTRL) || !pressed))
+ mod |= XBMCKMOD_CTRL;
+ if (xkb_state_mod_index_is_active(m_state, m_modindex[0], modtype) && ((keysym != XBMCK_RCTRL) || !pressed))
+ mod |= XBMCKMOD_CTRL;
+ if (xkb_state_mod_index_is_active(m_state, m_modindex[1], modtype) && ((keysym != XBMCK_LALT) || !pressed))
+ mod |= XBMCKMOD_ALT;
+ if (xkb_state_mod_index_is_active(m_state, m_modindex[1], modtype) && ((keysym != XBMCK_RALT) || !pressed))
+ mod |= XBMCKMOD_ALT;
+ if (xkb_state_mod_index_is_active(m_state, m_modindex[2], modtype) && ((keysym != XBMCK_LSHIFT) || !pressed))
+ mod |= XBMCKMOD_SHIFT;
+ if (xkb_state_mod_index_is_active(m_state, m_modindex[2], modtype) && ((keysym != XBMCK_RSHIFT) || !pressed))
+ mod |= XBMCKMOD_SHIFT;
+ if (xkb_state_mod_index_is_active(m_state, m_modindex[3], modtype) && ((keysym != XBMCK_LMETA) || !pressed))
+ mod |= XBMCKMOD_META;
+ if (xkb_state_mod_index_is_active(m_state, m_modindex[3], modtype) && ((keysym != XBMCK_RMETA) || !pressed))
+ mod |= XBMCKMOD_META;
+
+ m_leds = 0;
+
+ if (xkb_state_led_index_is_active(m_state, m_ledindex[0]) && ((keysym != XBMCK_NUMLOCK) || !pressed))
+ {
+ m_leds |= LIBINPUT_LED_NUM_LOCK;
+ mod |= XBMCKMOD_NUM;
+ }
+ if (xkb_state_led_index_is_active(m_state, m_ledindex[1]) && ((keysym != XBMCK_CAPSLOCK) || !pressed))
+ {
+ m_leds |= LIBINPUT_LED_CAPS_LOCK;
+ mod |= XBMCKMOD_CAPS;
+ }
+ if (xkb_state_led_index_is_active(m_state, m_ledindex[2]) && ((keysym != XBMCK_SCROLLOCK) || !pressed))
+ {
+ m_leds |= LIBINPUT_LED_SCROLL_LOCK;
+ //mod |= XBMCK_SCROLLOCK;
+ }
+
+ uint32_t unicode = xkb_state_key_get_utf32(m_state, xkbkey);
+ if (unicode > std::numeric_limits<std::uint16_t>::max())
+ {
+ // Kodi event system only supports UTF16, so ignore the codepoint if it does not fit
+ unicode = 0;
+ }
+
+ uint32_t scancode = libinput_event_keyboard_get_key(e);
+ if (scancode > std::numeric_limits<unsigned char>::max())
+ {
+ // Kodi scancodes are limited to unsigned char, pretend the scancode is unknown on overflow
+ scancode = 0;
+ }
+
+ event.key.keysym.mod = XBMCMod(mod);
+ event.key.keysym.sym = XBMCKeyForKeysym(keysym, scancode);
+ event.key.keysym.scancode = scancode;
+ event.key.keysym.unicode = unicode;
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(event);
+
+ if (pressed && xkb_keymap_key_repeats(m_keymap, xkbkey))
+ {
+ libinput_event *ev = libinput_event_keyboard_get_base_event(e);
+ libinput_device *dev = libinput_event_get_device(ev);
+ auto data = m_repeatData.find(dev);
+ if (data != m_repeatData.end())
+ {
+ CLog::Log(LOGDEBUG, "CLibInputKeyboard::{} - using delay: {}ms repeat: {}ms", __FUNCTION__,
+ data->second.at(0), data->second.at(1));
+
+ m_repeatRate = data->second.at(1);
+ m_repeatTimer.Stop(true);
+ m_repeatEvent = event;
+ m_repeatTimer.Start(std::chrono::milliseconds(data->second.at(0)), false);
+ }
+ }
+ else
+ {
+ m_repeatTimer.Stop();
+ }
+}
+
+XBMCKey CLibInputKeyboard::XBMCKeyForKeysym(xkb_keysym_t sym, uint32_t scancode)
+{
+ if (sym >= 'A' && sym <= 'Z')
+ {
+ return static_cast<XBMCKey> (sym + 'a' - 'A');
+ }
+ else if (sym >= XKB_KEY_space && sym <= XKB_KEY_asciitilde)
+ {
+ return static_cast<XBMCKey> (sym);
+ }
+ else if (sym >= XKB_KEY_F1 && sym <= XKB_KEY_F15)
+ {
+ return static_cast<XBMCKey> (XBMCK_F1 + ((int)sym - XKB_KEY_F1));
+ }
+
+ auto xkbmapping = xkbMap.find(sym);
+ if (xkbmapping != xkbMap.end())
+ return xkbmapping->second;
+
+ return XBMCK_UNKNOWN;
+}
+
+void CLibInputKeyboard::KeyRepeatTimeout()
+{
+ m_repeatTimer.RestartAsync(std::chrono::milliseconds(m_repeatRate));
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(m_repeatEvent);
+}
+
+void CLibInputKeyboard::UpdateLeds(libinput_device *dev)
+{
+ libinput_device_led_update(dev, static_cast<libinput_led>(m_leds));
+}
+
+void CLibInputKeyboard::GetRepeat(libinput_device *dev)
+{
+ int kbdrep[2] = {REPEAT_DELAY, REPEAT_RATE};
+ const char *name = libinput_device_get_name(dev);
+ const char *sysname = libinput_device_get_sysname(dev);
+ std::string path("/dev/input/");
+ path.append(sysname);
+ auto fd = open(path.c_str(), O_RDONLY);
+
+ if (fd < 0)
+ {
+ CLog::Log(LOGERROR, "CLibInputKeyboard::{} - failed to open {} ({})", __FUNCTION__, sysname,
+ strerror(errno));
+ }
+ else
+ {
+ auto ret = ioctl(fd, EVIOCGREP, &kbdrep);
+ if (ret < 0)
+ CLog::Log(LOGDEBUG, "CLibInputKeyboard::{} - could not get key repeat for {} ({})",
+ __FUNCTION__, sysname, strerror(errno));
+
+ CLog::Log(LOGDEBUG, "CLibInputKeyboard::{} - delay: {}ms repeat: {}ms for {} ({})",
+ __FUNCTION__, kbdrep[0], kbdrep[1], name, sysname);
+ close(fd);
+ }
+
+ std::vector<int> kbdrepvec(std::begin(kbdrep), std::end(kbdrep));
+
+ auto data = m_repeatData.find(dev);
+ if (data == m_repeatData.end())
+ {
+ m_repeatData.insert(std::make_pair(dev, kbdrepvec));
+ }
+}
diff --git a/xbmc/platform/linux/input/LibInputKeyboard.h b/xbmc/platform/linux/input/LibInputKeyboard.h
new file mode 100644
index 0000000..76aaeff
--- /dev/null
+++ b/xbmc/platform/linux/input/LibInputKeyboard.h
@@ -0,0 +1,48 @@
+/*
+ * 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 "threads/Timer.h"
+#include "windowing/XBMC_events.h"
+
+#include <map>
+#include <vector>
+
+#include <libinput.h>
+#include <xkbcommon/xkbcommon.h>
+
+class CLibInputKeyboard
+{
+public:
+ CLibInputKeyboard();
+ ~CLibInputKeyboard();
+
+ void ProcessKey(libinput_event_keyboard *e);
+ void UpdateLeds(libinput_device *dev);
+ void GetRepeat(libinput_device *dev);
+
+ bool SetKeymap(const std::string& layout);
+
+private:
+ XBMCKey XBMCKeyForKeysym(xkb_keysym_t sym, uint32_t scancode);
+ void KeyRepeatTimeout();
+
+ xkb_context *m_ctx = nullptr;
+ xkb_keymap *m_keymap = nullptr;
+ xkb_state *m_state = nullptr;
+ xkb_mod_index_t m_modindex[4];
+ xkb_led_index_t m_ledindex[3];
+
+ int m_leds;
+
+ XBMC_Event m_repeatEvent;
+ std::map<libinput_device*, std::vector<int>> m_repeatData;
+ CTimer m_repeatTimer;
+ int m_repeatRate;
+};
diff --git a/xbmc/platform/linux/input/LibInputPointer.cpp b/xbmc/platform/linux/input/LibInputPointer.cpp
new file mode 100644
index 0000000..42d2bbe
--- /dev/null
+++ b/xbmc/platform/linux/input/LibInputPointer.cpp
@@ -0,0 +1,153 @@
+/*
+ * 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 "LibInputPointer.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "input/mouse/MouseStat.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <algorithm>
+#include <string.h>
+
+#include <linux/input.h>
+
+void CLibInputPointer::ProcessButton(libinput_event_pointer *e)
+{
+ const uint32_t b = libinput_event_pointer_get_button(e);
+ const bool pressed = libinput_event_pointer_get_button_state(e) == LIBINPUT_BUTTON_STATE_PRESSED;
+ unsigned char xbmc_button = 0;
+
+ switch (b)
+ {
+ case BTN_LEFT:
+ xbmc_button = XBMC_BUTTON_LEFT;
+ break;
+ case BTN_RIGHT:
+ xbmc_button = XBMC_BUTTON_RIGHT;
+ break;
+ case BTN_MIDDLE:
+ xbmc_button = XBMC_BUTTON_MIDDLE;
+ break;
+ case BTN_SIDE:
+ xbmc_button = XBMC_BUTTON_X1;
+ break;
+ case BTN_EXTRA:
+ xbmc_button = XBMC_BUTTON_X2;
+ break;
+ case BTN_FORWARD:
+ xbmc_button = XBMC_BUTTON_X3;
+ break;
+ case BTN_BACK:
+ xbmc_button = XBMC_BUTTON_X4;
+ break;
+ default:
+ break;
+ }
+
+ XBMC_Event event = {};
+
+ event.type = pressed ? XBMC_MOUSEBUTTONDOWN : XBMC_MOUSEBUTTONUP;
+ event.button.button = xbmc_button;
+ event.button.x = static_cast<uint16_t>(m_pos.X);
+ event.button.y = static_cast<uint16_t>(m_pos.Y);
+
+ CLog::Log(LOGDEBUG,
+ "CLibInputPointer::{} - event.type: {}, event.button.button: {}, event.button.x: {}, "
+ "event.button.y: {}",
+ __FUNCTION__, event.type, event.button.button, event.button.x, event.button.y);
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(event);
+}
+
+void CLibInputPointer::ProcessMotion(libinput_event_pointer *e)
+{
+ const double dx = libinput_event_pointer_get_dx(e);
+ const double dy = libinput_event_pointer_get_dy(e);
+
+ m_pos.X += static_cast<int>(dx);
+ m_pos.Y += static_cast<int>(dy);
+
+ // limit the mouse to the screen width
+ m_pos.X = std::min(CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), m_pos.X);
+ m_pos.X = std::max(0, m_pos.X);
+
+ // limit the mouse to the screen height
+ m_pos.Y = std::min(CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight(), m_pos.Y);
+ m_pos.Y = std::max(0, m_pos.Y);
+
+ XBMC_Event event = {};
+
+ event.type = XBMC_MOUSEMOTION;
+ event.motion.x = static_cast<uint16_t>(m_pos.X);
+ event.motion.y = static_cast<uint16_t>(m_pos.Y);
+
+ CLog::Log(LOGDEBUG,
+ "CLibInputPointer::{} - event.type: {}, event.motion.x: {}, event.motion.y: {}",
+ __FUNCTION__, event.type, event.motion.x, event.motion.y);
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(event);
+}
+
+void CLibInputPointer::ProcessMotionAbsolute(libinput_event_pointer *e)
+{
+ m_pos.X = static_cast<int>(libinput_event_pointer_get_absolute_x_transformed(e, CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth()));
+ m_pos.Y = static_cast<int>(libinput_event_pointer_get_absolute_y_transformed(e, CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight()));
+
+ XBMC_Event event = {};
+ event.type = XBMC_MOUSEMOTION;
+ event.motion.x = static_cast<uint16_t>(m_pos.X);
+ event.motion.y = static_cast<uint16_t>(m_pos.Y);
+
+ CLog::Log(LOGDEBUG,
+ "CLibInputPointer::{} - event.type: {}, event.motion.x: {}, event.motion.y: {}",
+ __FUNCTION__, event.type, event.motion.x, event.motion.y);
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(event);
+}
+
+void CLibInputPointer::ProcessAxis(libinput_event_pointer *e)
+{
+ unsigned char scroll = 0;
+ if (!libinput_event_pointer_has_axis(e, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL))
+ return;
+
+ const double v = libinput_event_pointer_get_axis_value(e, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL);
+ if (v < 0)
+ scroll = XBMC_BUTTON_WHEELUP;
+ else
+ scroll = XBMC_BUTTON_WHEELDOWN;
+
+ XBMC_Event event = {};
+
+ event.type = XBMC_MOUSEBUTTONDOWN;
+ event.button.button = scroll;
+ event.button.x = static_cast<uint16_t>(m_pos.X);
+ event.button.y = static_cast<uint16_t>(m_pos.Y);
+
+ CLog::Log(LOGDEBUG, "CLibInputPointer::{} - scroll: {}, event.button.x: {}, event.button.y: {}",
+ __FUNCTION__, scroll == XBMC_BUTTON_WHEELUP ? "up" : "down", event.button.x,
+ event.button.y);
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(event);
+
+ event.type = XBMC_MOUSEBUTTONUP;
+
+ if (appPort)
+ appPort->OnEvent(event);
+}
diff --git a/xbmc/platform/linux/input/LibInputPointer.h b/xbmc/platform/linux/input/LibInputPointer.h
new file mode 100644
index 0000000..608b438
--- /dev/null
+++ b/xbmc/platform/linux/input/LibInputPointer.h
@@ -0,0 +1,32 @@
+/*
+ * 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 <libinput.h>
+
+struct pos
+{
+ int X;
+ int Y;
+};
+
+class CLibInputPointer
+{
+public:
+ CLibInputPointer() = default;
+ ~CLibInputPointer() = default;
+
+ void ProcessButton(libinput_event_pointer *e);
+ void ProcessMotion(libinput_event_pointer *e);
+ void ProcessMotionAbsolute(libinput_event_pointer *e);
+ void ProcessAxis(libinput_event_pointer *e);
+
+private:
+ struct pos m_pos = { 0, 0 };
+};
diff --git a/xbmc/platform/linux/input/LibInputSettings.cpp b/xbmc/platform/linux/input/LibInputSettings.cpp
new file mode 100644
index 0000000..25a916e
--- /dev/null
+++ b/xbmc/platform/linux/input/LibInputSettings.cpp
@@ -0,0 +1,169 @@
+/*
+ * 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 "LibInputSettings.h"
+
+#include "LibInputHandler.h"
+#include "ServiceBroker.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+const std::string CLibInputSettings::SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT = "input.libinputkeyboardlayout";
+static std::vector<StringSettingOption> layouts;
+
+namespace
+{
+ inline bool LayoutSort(const StringSettingOption& i, const StringSettingOption& j)
+ {
+ return (i.value < j.value);
+ }
+} // unnamed namespace
+
+CLibInputSettings::CLibInputSettings(CLibInputHandler *handler) :
+ m_libInputHandler(handler)
+{
+ const auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return;
+
+ auto settingsManager = settings->GetSettingsManager();
+ if (!settingsManager)
+ return;
+
+ auto setting = settings->GetSetting(SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT);
+ if (!setting)
+ {
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT);
+ return;
+ }
+
+ setting->SetVisible(true);
+
+ std::set<std::string> settingSet;
+ settingSet.insert(SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT);
+ settingsManager->RegisterCallback(this, settingSet);
+ settingsManager->RegisterSettingOptionsFiller("libinputkeyboardlayout",
+ SettingOptionsKeyboardLayoutsFiller);
+
+ /* load the keyboard layouts from xkeyboard-config */
+ std::string xkbFile("/usr/share/X11/xkb/rules/base.xml");
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(xkbFile))
+ {
+ CLog::Log(LOGWARNING, "CLibInputSettings: unable to open: {}", xkbFile);
+ return;
+ }
+
+ const TiXmlElement* rootElement = xmlDoc.RootElement();
+ if (!rootElement)
+ {
+ CLog::Log(LOGWARNING, "CLibInputSettings: missing or invalid XML root element in: {}", xkbFile);
+ return;
+ }
+
+ if (rootElement->ValueStr() != "xkbConfigRegistry")
+ {
+ CLog::Log(LOGWARNING, "CLibInputSettings: unexpected XML root element {} in: {}",
+ rootElement->Value(), xkbFile);
+ return;
+ }
+
+ const TiXmlElement* layoutListElement = rootElement->FirstChildElement("layoutList");
+ if (!layoutListElement)
+ {
+ CLog::Log(LOGWARNING, "CLibInputSettings: unexpected XML child element {} in: {}",
+ layoutListElement->Value(), xkbFile);
+ return;
+ }
+
+ const TiXmlElement* layoutElement = layoutListElement->FirstChildElement("layout");
+ while (layoutElement)
+ {
+ const TiXmlElement* configElement = layoutElement->FirstChildElement("configItem");
+ if (!configElement)
+ {
+ CLog::Log(LOGWARNING, "CLibInputSettings: unexpected XML child element {} in: {}",
+ layoutListElement->Value(), xkbFile);
+ return;
+ }
+
+ const TiXmlElement* nameElement = configElement->FirstChildElement("name");
+ if (!nameElement)
+ {
+ CLog::Log(LOGWARNING, "CLibInputSettings: unexpected XML child element {} in: {}",
+ configElement->Value(), xkbFile);
+ return;
+ }
+
+ const TiXmlElement* descriptionElement = configElement->FirstChildElement("description");
+ if (!descriptionElement)
+ {
+ CLog::Log(LOGWARNING, "CLibInputSettings: unexpected XML child element {} in: {}",
+ configElement->Value(), xkbFile);
+ return;
+ }
+
+ std::string layout = nameElement->GetText();
+ std::string layoutDescription = descriptionElement->GetText();
+
+ if (!layout.empty() && !layoutDescription.empty())
+ layouts.emplace_back(StringSettingOption(layoutDescription, layout));
+
+ layoutElement = layoutElement->NextSiblingElement();
+ }
+
+ std::sort(layouts.begin(), layouts.end(), LayoutSort);
+}
+
+CLibInputSettings::~CLibInputSettings()
+{
+ const auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ const std::shared_ptr<CSettings> settings = settingsComponent->GetSettings();
+ if (!settings)
+ return;
+
+ settings->GetSettingsManager()->UnregisterSettingOptionsFiller("libinputkeyboardlayout");
+ settings->GetSettingsManager()->UnregisterCallback(this);
+}
+
+void CLibInputSettings::SettingOptionsKeyboardLayoutsFiller(
+ const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ list = layouts;
+}
+
+void CLibInputSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT)
+ {
+ std::string layout = std::dynamic_pointer_cast<const CSettingString>(setting)->GetValue();
+ m_libInputHandler->SetKeymap(layout);
+ }
+}
diff --git a/xbmc/platform/linux/input/LibInputSettings.h b/xbmc/platform/linux/input/LibInputSettings.h
new file mode 100644
index 0000000..83d5edf
--- /dev/null
+++ b/xbmc/platform/linux/input/LibInputSettings.h
@@ -0,0 +1,35 @@
+/*
+ * 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 "settings/Settings.h"
+#include "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+
+#include <memory>
+#include <vector>
+
+class CLibInputHandler;
+struct StringSettingOption;
+
+class CLibInputSettings : public ISettingCallback, public ISettingsHandler
+{
+public:
+ static const std::string SETTING_INPUT_LIBINPUTKEYBOARDLAYOUT;
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ static void SettingOptionsKeyboardLayoutsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+
+ CLibInputSettings(CLibInputHandler *handler);
+ ~CLibInputSettings() override;
+
+private:
+ CLibInputHandler *m_libInputHandler{nullptr};
+};
diff --git a/xbmc/platform/linux/input/LibInputTouch.cpp b/xbmc/platform/linux/input/LibInputTouch.cpp
new file mode 100644
index 0000000..545d99a
--- /dev/null
+++ b/xbmc/platform/linux/input/LibInputTouch.cpp
@@ -0,0 +1,109 @@
+/*
+ * 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 "LibInputTouch.h"
+
+#include "ServiceBroker.h"
+#include "input/touch/generic/GenericTouchActionHandler.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+static inline CPoint GetPos(libinput_event_touch *e)
+{
+ const double x = libinput_event_touch_get_x_transformed(e, CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth());
+ const double y = libinput_event_touch_get_y_transformed(e, CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight());
+
+ CLog::Log(LOGDEBUG, "CLibInputTouch::{} - x: {:f} y: {:f}", __FUNCTION__, x, y);
+
+ return CPoint(x, y);
+}
+
+CLibInputTouch::CLibInputTouch()
+{
+ m_points.reserve(2);
+ CGenericTouchInputHandler::GetInstance().RegisterHandler(&CGenericTouchActionHandler::GetInstance());
+}
+
+void CLibInputTouch::CheckSlot(int slot)
+{
+ if (slot + 1 > static_cast<int>(m_points.size()))
+ m_points.resize(slot + 1, std::make_pair(TouchInputUnchanged, CPoint(0, 0)));
+}
+
+TouchInput CLibInputTouch::GetEvent(int slot)
+{
+ CheckSlot(slot);
+ return m_points.at(slot).first;
+}
+
+void CLibInputTouch::SetEvent(int slot, TouchInput event)
+{
+ CheckSlot(slot);
+ m_points.at(slot).first = event;
+}
+
+void CLibInputTouch::SetPosition(int slot, CPoint point)
+{
+ CheckSlot(slot);
+ m_points.at(slot).second = point;
+}
+
+void CLibInputTouch::ProcessTouchDown(libinput_event_touch *e)
+{
+ int slot = libinput_event_touch_get_seat_slot(e);
+
+ SetPosition(slot, GetPos(e));
+ SetEvent(slot, TouchInputDown);
+ CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input down", __FUNCTION__);
+}
+
+void CLibInputTouch::ProcessTouchMotion(libinput_event_touch *e)
+{
+ int slot = libinput_event_touch_get_seat_slot(e);
+ uint64_t nanotime = libinput_event_touch_get_time_usec(e) * 1000LL;
+
+ SetPosition(slot, GetPos(e));
+
+ if (GetEvent(slot) != TouchInputDown)
+ SetEvent(slot, TouchInputMove);
+ CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input move", __FUNCTION__);
+
+ CGenericTouchInputHandler::GetInstance().UpdateTouchPointer(slot, GetX(slot), GetY(slot), nanotime);
+}
+
+void CLibInputTouch::ProcessTouchUp(libinput_event_touch *e)
+{
+ int slot = libinput_event_touch_get_seat_slot(e);
+
+ SetEvent(slot, TouchInputUp);
+ CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input up", __FUNCTION__);
+}
+
+void CLibInputTouch::ProcessTouchCancel(libinput_event_touch *e)
+{
+ int slot = libinput_event_touch_get_seat_slot(e);
+ uint64_t nanotime = libinput_event_touch_get_time_usec(e) * 1000LL;
+
+ CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input cancel", __FUNCTION__);
+ CGenericTouchInputHandler::GetInstance().HandleTouchInput(TouchInputAbort, GetX(slot), GetY(slot), nanotime, slot);
+}
+
+void CLibInputTouch::ProcessTouchFrame(libinput_event_touch *e)
+{
+ uint64_t nanotime = libinput_event_touch_get_time_usec(e) * 1000LL;
+
+ for (size_t slot = 0; slot < m_points.size(); ++slot)
+ {
+ CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input frame: event {}", __FUNCTION__,
+ GetEvent(slot));
+ CLog::Log(LOGDEBUG, "CLibInputTouch::{} - touch input frame: slot {}", __FUNCTION__, slot);
+ CGenericTouchInputHandler::GetInstance().HandleTouchInput(GetEvent(slot), GetX(slot), GetY(slot), nanotime, slot);
+ SetEvent(slot, TouchInputUnchanged);
+ }
+}
+
diff --git a/xbmc/platform/linux/input/LibInputTouch.h b/xbmc/platform/linux/input/LibInputTouch.h
new file mode 100644
index 0000000..9805599
--- /dev/null
+++ b/xbmc/platform/linux/input/LibInputTouch.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "input/touch/generic/GenericTouchInputHandler.h"
+#include "utils/Geometry.h"
+
+#include <vector>
+
+#include <libinput.h>
+
+struct libinput_event_touch;
+struct libinput_device;
+
+class CLibInputTouch
+{
+public:
+ CLibInputTouch();
+ void ProcessTouchDown(libinput_event_touch *e);
+ void ProcessTouchMotion(libinput_event_touch *e);
+ void ProcessTouchUp(libinput_event_touch *e);
+ void ProcessTouchCancel(libinput_event_touch *e);
+ void ProcessTouchFrame(libinput_event_touch *e);
+
+private:
+ void CheckSlot(int slot);
+ TouchInput GetEvent(int slot);
+ void SetEvent(int slot, TouchInput event);
+ void SetPosition(int slot, CPoint point);
+ int GetX(int slot) { return m_points.at(slot).second.x; }
+ int GetY(int slot) { return m_points.at(slot).second.y; }
+
+ std::vector<std::pair<TouchInput, CPoint>> m_points{std::make_pair(TouchInputUnchanged, CPoint(0, 0))};
+};
diff --git a/xbmc/platform/linux/network/CMakeLists.txt b/xbmc/platform/linux/network/CMakeLists.txt
new file mode 100644
index 0000000..44aa2e7
--- /dev/null
+++ b/xbmc/platform/linux/network/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES NetworkLinux.cpp)
+set(HEADERS NetworkLinux.h)
+
+core_add_library(platform_linux_network)
diff --git a/xbmc/platform/linux/network/NetworkLinux.cpp b/xbmc/platform/linux/network/NetworkLinux.cpp
new file mode 100644
index 0000000..e9a936a
--- /dev/null
+++ b/xbmc/platform/linux/network/NetworkLinux.cpp
@@ -0,0 +1,223 @@
+/*
+ * 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 "NetworkLinux.h"
+
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <errno.h>
+#include <utility>
+
+#include <arpa/inet.h>
+#include <net/if.h>
+#include <net/if_arp.h>
+#include <resolv.h>
+#include <sys/ioctl.h>
+
+CNetworkInterfaceLinux::CNetworkInterfaceLinux(CNetworkPosix* network,
+ std::string interfaceName,
+ char interfaceMacAddrRaw[6])
+ : CNetworkInterfacePosix(network, std::move(interfaceName), interfaceMacAddrRaw)
+{
+}
+
+std::string CNetworkInterfaceLinux::GetCurrentDefaultGateway() const
+{
+ std::string result;
+
+ FILE* fp = fopen("/proc/net/route", "r");
+ if (!fp)
+ {
+ // TBD: Error
+ return result;
+ }
+
+ char* line = NULL;
+ char iface[16];
+ char dst[128];
+ char gateway[128];
+ size_t linel = 0;
+ int n;
+ int linenum = 0;
+ while (getdelim(&line, &linel, '\n', fp) > 0)
+ {
+ // skip first two lines
+ if (linenum++ < 1)
+ continue;
+
+ // search where the word begins
+ n = sscanf(line, "%15s %127s %127s", iface, dst, gateway);
+
+ if (n < 3)
+ continue;
+
+ if (strcmp(iface, m_interfaceName.c_str()) == 0 && strcmp(dst, "00000000") == 0 &&
+ strcmp(gateway, "00000000") != 0)
+ {
+ unsigned char gatewayAddr[4];
+ int len = CNetworkBase::ParseHex(gateway, gatewayAddr);
+ if (len == 4)
+ {
+ struct in_addr in;
+ in.s_addr = (gatewayAddr[0] << 24) | (gatewayAddr[1] << 16) | (gatewayAddr[2] << 8) |
+ (gatewayAddr[3]);
+ result = inet_ntoa(in);
+ break;
+ }
+ }
+ }
+ free(line);
+ fclose(fp);
+
+ return result;
+}
+
+bool CNetworkInterfaceLinux::GetHostMacAddress(unsigned long host_ip, std::string& mac) const
+{
+ struct arpreq areq;
+ struct sockaddr_in* sin;
+
+ memset(&areq, 0x0, sizeof(areq));
+
+ sin = (struct sockaddr_in*)&areq.arp_pa;
+ sin->sin_family = AF_INET;
+ sin->sin_addr.s_addr = host_ip;
+
+ sin = (struct sockaddr_in*)&areq.arp_ha;
+ sin->sin_family = ARPHRD_ETHER;
+
+ strncpy(areq.arp_dev, m_interfaceName.c_str(), sizeof(areq.arp_dev));
+ areq.arp_dev[sizeof(areq.arp_dev) - 1] = '\0';
+
+ int result = ioctl(m_network->GetSocket(), SIOCGARP, (caddr_t)&areq);
+
+ if (result != 0)
+ {
+ // CLog::Log(LOGERROR, "{} - GetHostMacAddress/ioctl failed with errno ({})", __FUNCTION__, errno);
+ return false;
+ }
+
+ struct sockaddr* res = &areq.arp_ha;
+ mac = StringUtils::Format("{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", (uint8_t)res->sa_data[0],
+ (uint8_t)res->sa_data[1], (uint8_t)res->sa_data[2],
+ (uint8_t)res->sa_data[3], (uint8_t)res->sa_data[4],
+ (uint8_t)res->sa_data[5]);
+
+ for (int i = 0; i < 6; ++i)
+ if (res->sa_data[i])
+ return true;
+
+ return false;
+}
+
+std::unique_ptr<CNetworkBase> CNetworkBase::GetNetwork()
+{
+ return std::make_unique<CNetworkLinux>();
+}
+
+CNetworkLinux::CNetworkLinux() : CNetworkPosix()
+{
+ queryInterfaceList();
+}
+
+void CNetworkLinux::GetMacAddress(const std::string& interfaceName, char rawMac[6])
+{
+ memset(rawMac, 0, 6);
+
+ struct ifreq ifr;
+ strcpy(ifr.ifr_name, interfaceName.c_str());
+ if (ioctl(GetSocket(), SIOCGIFHWADDR, &ifr) >= 0)
+ {
+ memcpy(rawMac, ifr.ifr_hwaddr.sa_data, 6);
+ }
+}
+
+void CNetworkLinux::queryInterfaceList()
+{
+ char macAddrRaw[6];
+ m_interfaces.clear();
+
+ FILE* fp = fopen("/proc/net/dev", "r");
+ if (!fp)
+ {
+ // TBD: Error
+ return;
+ }
+
+ char* line = NULL;
+ size_t linel = 0;
+ int n;
+ char* p;
+ int linenum = 0;
+ while (getdelim(&line, &linel, '\n', fp) > 0)
+ {
+ // skip first two lines
+ if (linenum++ < 2)
+ continue;
+
+ // search where the word begins
+ p = line;
+ while (isspace(*p))
+ ++p;
+
+ // read word until :
+ n = strcspn(p, ": \t");
+ p[n] = 0;
+
+ // save the result
+ std::string interfaceName = p;
+ GetMacAddress(interfaceName, macAddrRaw);
+
+ // only add interfaces with non-zero mac addresses
+ if (macAddrRaw[0] || macAddrRaw[1] || macAddrRaw[2] || macAddrRaw[3] || macAddrRaw[4] ||
+ macAddrRaw[5])
+ m_interfaces.push_back(new CNetworkInterfaceLinux(this, interfaceName, macAddrRaw));
+ }
+ free(line);
+ fclose(fp);
+}
+
+std::vector<std::string> CNetworkLinux::GetNameServers()
+{
+ std::vector<std::string> result;
+
+ res_init();
+
+ for (int i = 0; i < _res.nscount; i++)
+ {
+ std::string ns = inet_ntoa(_res.nsaddr_list[i].sin_addr);
+ result.push_back(ns);
+ }
+ return result;
+}
+
+bool CNetworkLinux::PingHost(unsigned long remote_ip, unsigned int timeout_ms)
+{
+ char cmd_line[64];
+
+ struct in_addr host_ip;
+ host_ip.s_addr = remote_ip;
+
+ sprintf(cmd_line, "ping -c 1 -w %d %s", timeout_ms / 1000 + (timeout_ms % 1000) != 0,
+ inet_ntoa(host_ip));
+
+ int status = -1;
+ status = system(cmd_line);
+ int result = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
+
+ // http://linux.about.com/od/commands/l/blcmdl8_ping.htm ;
+ // 0 reply
+ // 1 no reply
+ // else some error
+
+ if (result < 0 || result > 1)
+ CLog::Log(LOGERROR, "Ping fail : status = {}, errno = {} : '{}'", status, errno, cmd_line);
+
+ return result == 0;
+}
diff --git a/xbmc/platform/linux/network/NetworkLinux.h b/xbmc/platform/linux/network/NetworkLinux.h
new file mode 100644
index 0000000..dc7b4c6
--- /dev/null
+++ b/xbmc/platform/linux/network/NetworkLinux.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "platform/posix/network/NetworkPosix.h"
+
+#include <string>
+#include <vector>
+
+class CNetworkInterfaceLinux : public CNetworkInterfacePosix
+{
+public:
+ CNetworkInterfaceLinux(CNetworkPosix* network,
+ std::string interfaceName,
+ char interfaceMacAddrRaw[6]);
+ ~CNetworkInterfaceLinux() override = default;
+
+ std::string GetCurrentDefaultGateway() const override;
+ bool GetHostMacAddress(unsigned long host, std::string& mac) const override;
+};
+
+class CNetworkLinux : public CNetworkPosix
+{
+public:
+ CNetworkLinux();
+ ~CNetworkLinux() override = default;
+
+ bool PingHost(unsigned long host, unsigned int timeout_ms = 2000) override;
+ std::vector<std::string> GetNameServers(void) override;
+
+private:
+ void GetMacAddress(const std::string& interfaceName, char macAddrRaw[6]) override;
+ void queryInterfaceList() override;
+};
diff --git a/xbmc/platform/linux/network/zeroconf/CMakeLists.txt b/xbmc/platform/linux/network/zeroconf/CMakeLists.txt
new file mode 100644
index 0000000..b31ab73
--- /dev/null
+++ b/xbmc/platform/linux/network/zeroconf/CMakeLists.txt
@@ -0,0 +1,8 @@
+if(AVAHI_FOUND)
+ set(SOURCES ZeroconfAvahi.cpp
+ ZeroconfBrowserAvahi.cpp)
+ set(HEADERS ZeroconfAvahi.h
+ ZeroconfBrowserAvahi.h)
+
+ core_add_library(platform_linux_network_zeroconf)
+endif()
diff --git a/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.cpp b/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.cpp
new file mode 100644
index 0000000..dea038a
--- /dev/null
+++ b/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.cpp
@@ -0,0 +1,434 @@
+/*
+ * 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 "ZeroconfAvahi.h"
+
+#include "utils/log.h"
+
+#include <cassert>
+#include <iostream>
+#include <sstream>
+#include <string>
+
+#include <avahi-client/client.h>
+#include <avahi-common/alternative.h>
+#include <avahi-common/error.h>
+#include <avahi-common/malloc.h>
+#include <avahi-common/thread-watch.h>
+#include <unistd.h> //gethostname
+
+#include "PlatformDefs.h"
+
+///helper RAII-struct to block event loop for modifications
+struct ScopedEventLoopBlock
+{
+ explicit ScopedEventLoopBlock(AvahiThreadedPoll* fp_poll):mp_poll(fp_poll)
+ {
+ avahi_threaded_poll_lock(mp_poll);
+ }
+
+ ~ScopedEventLoopBlock()
+ {
+ avahi_threaded_poll_unlock(mp_poll);
+ }
+private:
+ AvahiThreadedPoll* mp_poll;
+};
+
+///helper to store information on howto create an avahi-group to publish
+struct CZeroconfAvahi::ServiceInfo
+{
+ ServiceInfo(const std::string& fcr_type, const std::string& fcr_name,
+ unsigned int f_port, AvahiStringList *txt, AvahiEntryGroup* fp_group = 0):
+ m_type(fcr_type), m_name(fcr_name), m_port(f_port), mp_txt(txt), mp_group(fp_group)
+ {
+ }
+
+ std::string m_type;
+ std::string m_name;
+ unsigned int m_port;
+ AvahiStringList* mp_txt;
+ AvahiEntryGroup* mp_group;
+};
+
+CZeroconfAvahi::CZeroconfAvahi()
+{
+ if (! (mp_poll = avahi_threaded_poll_new()))
+ {
+ CLog::Log(LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Could not create threaded poll object");
+ //! @todo throw exception?
+ return;
+ }
+
+ if (!createClient())
+ {
+ CLog::Log(LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Could not create client");
+ //yeah, what if not? but should always succeed
+ }
+
+ //start event loop thread
+ if (avahi_threaded_poll_start(mp_poll) < 0)
+ {
+ CLog::Log(LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Failed to start avahi client thread");
+ }
+}
+
+CZeroconfAvahi::~CZeroconfAvahi()
+{
+ CLog::Log(LOGDEBUG, "CZeroconfAvahi::~CZeroconfAvahi() Going down! cleaning up...");
+
+ if (mp_poll)
+ {
+ //normally we would stop the avahi thread here and do our work, but
+ //it looks like this does not work -> www.avahi.org/ticket/251
+ //so instead of calling
+ //avahi_threaded_poll_stop(mp_poll);
+ //we set m_shutdown=true, post an event and wait for it to stop itself
+ struct timeval tv = { 0, 0 }; //! @todo does tv survive the thread?
+ AvahiTimeout* lp_timeout;
+ {
+ ScopedEventLoopBlock l_block(mp_poll);
+ const AvahiPoll* cp_apoll = avahi_threaded_poll_get(mp_poll);
+ m_shutdown = true;
+ lp_timeout = cp_apoll->timeout_new(cp_apoll,
+ &tv,
+ shutdownCallback,
+ this);
+ }
+
+ //now wait for the thread to stop
+ assert(m_thread_id);
+ pthread_join(m_thread_id, NULL);
+ avahi_threaded_poll_get(mp_poll)->timeout_free(lp_timeout);
+ }
+
+ //free the client (frees all browsers, groups, ...)
+ if (mp_client)
+ avahi_client_free(mp_client);
+ if (mp_poll)
+ avahi_threaded_poll_free(mp_poll);
+}
+
+bool CZeroconfAvahi::doPublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ const std::vector<std::pair<std::string, std::string> >& txt)
+{
+ CLog::Log(LOGDEBUG, "CZeroconfAvahi::doPublishService identifier: {} type: {} name:{} port:{}",
+ fcr_identifier, fcr_type, fcr_name, f_port);
+
+ ScopedEventLoopBlock l_block(mp_poll);
+ tServiceMap::iterator it = m_services.find(fcr_identifier);
+ if (it != m_services.end())
+ {
+ //fcr_identifier exists, no update functionality yet, so exit
+ return false;
+ }
+
+ //txt records to AvahiStringList
+ AvahiStringList *txtList = NULL;
+ for (const auto& it : txt)
+ {
+ txtList = avahi_string_list_add_pair(txtList, it.first.c_str(), it.second.c_str());
+ }
+
+ //create service info and add it to service map
+ tServiceMap::mapped_type p_service_info(new CZeroconfAvahi::ServiceInfo(fcr_type, fcr_name, f_port, txtList));
+ it = m_services.insert(it, std::make_pair(fcr_identifier, p_service_info));
+
+ //if client is already running, directly try to add the new service
+ if ( mp_client && avahi_client_get_state(mp_client) == AVAHI_CLIENT_S_RUNNING )
+ {
+ //client's already running, add this new service
+ addService(p_service_info, mp_client);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "CZeroconfAvahi::doPublishService: client not running, queued for publishing");
+ }
+ return true;
+}
+
+bool CZeroconfAvahi::doForceReAnnounceService(const std::string& fcr_identifier)
+{
+ bool ret = false;
+ ScopedEventLoopBlock l_block(mp_poll);
+ tServiceMap::iterator it = m_services.find(fcr_identifier);
+ if (it != m_services.end() && it->second->mp_group)
+ {
+ // to force a reannounce on avahi its enough to reverse the txtrecord list
+ it->second->mp_txt = avahi_string_list_reverse(it->second->mp_txt);
+
+ // this will trigger the reannouncement
+ if ((avahi_entry_group_update_service_txt_strlst(it->second->mp_group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AvahiPublishFlags(0),
+ it->second->m_name.c_str(),
+ it->second->m_type.c_str(), NULL, it->second->mp_txt)) >= 0)
+ ret = true;
+ }
+
+ return ret;
+}
+
+bool CZeroconfAvahi::doRemoveService(const std::string& fcr_ident)
+{
+ CLog::Log(LOGDEBUG, "CZeroconfAvahi::doRemoveService named: {}", fcr_ident);
+ ScopedEventLoopBlock l_block(mp_poll);
+ tServiceMap::iterator it = m_services.find(fcr_ident);
+ if (it == m_services.end())
+ {
+ return false;
+ }
+
+ if (it->second->mp_group)
+ {
+ avahi_entry_group_free(it->second->mp_group);
+ it->second->mp_group = 0;
+ }
+
+ if(it->second->mp_txt)
+ {
+ avahi_string_list_free(it->second->mp_txt);
+ it->second->mp_txt = NULL;
+ }
+
+ m_services.erase(it);
+ return true;
+}
+
+void CZeroconfAvahi::doStop()
+{
+ ScopedEventLoopBlock l_block(mp_poll);
+ for (auto& it : m_services)
+ {
+ if (it.second->mp_group)
+ {
+ avahi_entry_group_free(it.second->mp_group);
+ it.second->mp_group = 0;
+ }
+
+ if (it.second->mp_txt)
+ {
+ avahi_string_list_free(it.second->mp_txt);
+ it.second->mp_txt = nullptr;
+ }
+ }
+ m_services.clear();
+}
+
+void CZeroconfAvahi::clientCallback(AvahiClient* fp_client, AvahiClientState f_state, void* fp_data)
+{
+ CZeroconfAvahi* p_instance = static_cast<CZeroconfAvahi*>(fp_data);
+
+ //store our thread ID and check for shutdown -> check details in destructor
+ p_instance->m_thread_id = pthread_self();
+
+ if (p_instance->m_shutdown)
+ {
+ avahi_threaded_poll_quit(p_instance->mp_poll);
+ return;
+ }
+ switch(f_state)
+ {
+ case AVAHI_CLIENT_S_RUNNING:
+ CLog::Log(LOGDEBUG, "CZeroconfAvahi::clientCallback: client is up and running");
+ p_instance->updateServices(fp_client);
+ break;
+
+ case AVAHI_CLIENT_FAILURE:
+ CLog::Log(LOGINFO, "CZeroconfAvahi::clientCallback: client failure. avahi-daemon stopped? Recreating client...");
+ //We were forced to disconnect from server. now free and recreate the client object
+ avahi_client_free(fp_client);
+ p_instance->mp_client = 0;
+ //freeing the client also frees all groups and browsers, pointers are undefined afterwards, so fix that now
+ for (auto& it : p_instance->m_services)
+ {
+ it.second->mp_group = 0;
+ }
+ p_instance->createClient();
+ break;
+
+ case AVAHI_CLIENT_S_COLLISION:
+ case AVAHI_CLIENT_S_REGISTERING:
+ //HERE WE SHOULD REMOVE ALL OF OUR SERVICES AND "RESCHEDULE" them for later addition
+ CLog::Log(LOGDEBUG, "CZeroconfAvahi::clientCallback: uiuui; coll or reg, anyways, resetting groups");
+ for (const auto& it : p_instance->m_services)
+ {
+ if (it.second->mp_group)
+ avahi_entry_group_reset(it.second->mp_group);
+ }
+ break;
+
+ case AVAHI_CLIENT_CONNECTING:
+ CLog::Log(LOGINFO, "CZeroconfAvahi::clientCallback: avahi server not available. But may become later...");
+ break;
+ }
+}
+
+void CZeroconfAvahi::groupCallback(AvahiEntryGroup *fp_group, AvahiEntryGroupState f_state, void * fp_data)
+{
+ CZeroconfAvahi* p_instance = static_cast<CZeroconfAvahi*>(fp_data);
+ //store our thread ID and check for shutdown -> check details in destructor
+ p_instance->m_thread_id = pthread_self();
+ if (p_instance->m_shutdown)
+ {
+ avahi_threaded_poll_quit(p_instance->mp_poll);
+ return;
+ }
+
+ switch (f_state)
+ {
+ case AVAHI_ENTRY_GROUP_ESTABLISHED :
+ // The entry group has been established successfully
+ CLog::Log(LOGDEBUG, "CZeroconfAvahi::groupCallback: Service successfully established");
+ break;
+
+ case AVAHI_ENTRY_GROUP_COLLISION :
+ {
+ //need to find the ServiceInfo struct for this group
+ tServiceMap::iterator it = p_instance->m_services.begin();
+ for(; it != p_instance->m_services.end(); ++it)
+ {
+ if (it->second->mp_group == fp_group)
+ break;
+ }
+ if( it != p_instance->m_services.end() ) {
+ char* alt_name = avahi_alternative_service_name( it->second->m_name.c_str() );
+ it->second->m_name = alt_name;
+ avahi_free(alt_name);
+ CLog::Log(LOGINFO, "CZeroconfAvahi::groupCallback: Service name collision. Renamed to: {}",
+ it->second->m_name);
+ p_instance->addService(it->second, p_instance->mp_client);
+ }
+ break;
+ }
+
+ case AVAHI_ENTRY_GROUP_FAILURE:
+ CLog::Log(LOGERROR, "CZeroconfAvahi::groupCallback: Entry group failure: {} ",
+ (fp_group)
+ ? avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(fp_group)))
+ : "Unknown");
+ //free the group and set to 0 so it may be added later
+ if (fp_group)
+ {
+ //need to find the ServiceInfo struct for this group
+ for (auto& it : p_instance->m_services)
+ {
+ if (it.second->mp_group == fp_group)
+ {
+ avahi_entry_group_free(it.second->mp_group);
+ it.second->mp_group = 0;
+ if (it.second->mp_txt)
+ {
+ avahi_string_list_free(it.second->mp_txt);
+ it.second->mp_txt = nullptr;
+ }
+ break;
+ }
+ }
+ }
+ break;
+
+ case AVAHI_ENTRY_GROUP_UNCOMMITED:
+ case AVAHI_ENTRY_GROUP_REGISTERING:
+ default:
+ break;
+ }
+}
+
+void CZeroconfAvahi::shutdownCallback(AvahiTimeout *fp_e, void *fp_data)
+{
+ CZeroconfAvahi* p_instance = static_cast<CZeroconfAvahi*>(fp_data);
+ //should only be called on shutdown
+ if (p_instance->m_shutdown)
+ {
+ avahi_threaded_poll_quit(p_instance->mp_poll);
+ }
+}
+
+bool CZeroconfAvahi::createClient()
+{
+ if (mp_client)
+ {
+ avahi_client_free(mp_client);
+ }
+
+ int error = 0;
+ mp_client = avahi_client_new(avahi_threaded_poll_get(mp_poll),
+ AVAHI_CLIENT_NO_FAIL, &clientCallback,this,&error);
+ if (!mp_client)
+ {
+ CLog::Log(LOGERROR, "CZeroconfAvahi::createClient() failed with {}", error);
+ return false;
+ }
+ return true;
+}
+
+void CZeroconfAvahi::updateServices(AvahiClient* fp_client)
+{
+ for (const auto& it : m_services)
+ {
+ if (!it.second->mp_group)
+ addService(it.second, fp_client);
+ }
+}
+
+void CZeroconfAvahi::addService(const tServiceMap::mapped_type& fp_service_info,
+ AvahiClient* fp_client)
+{
+ assert(fp_client);
+ CLog::Log(LOGDEBUG, "CZeroconfAvahi::addService() named: {} type: {} port:{}",
+ fp_service_info->m_name, fp_service_info->m_type, fp_service_info->m_port);
+ //create the group if it doesn't exist
+ if (!fp_service_info->mp_group)
+ {
+ if (!(fp_service_info->mp_group = avahi_entry_group_new(fp_client, &CZeroconfAvahi::groupCallback, this)))
+ {
+ CLog::Log(LOGDEBUG, "CZeroconfAvahi::addService() avahi_entry_group_new() failed: {}",
+ avahi_strerror(avahi_client_errno(fp_client)));
+ fp_service_info->mp_group = 0;
+ return;
+ }
+ }
+
+
+ // add entries to the group if it's empty
+ int ret;
+ if (avahi_entry_group_is_empty(fp_service_info->mp_group))
+ {
+ if ((ret = avahi_entry_group_add_service_strlst(fp_service_info->mp_group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AvahiPublishFlags(0),
+ fp_service_info->m_name.c_str(),
+ fp_service_info->m_type.c_str(), NULL, NULL, fp_service_info->m_port, fp_service_info->mp_txt)) < 0)
+ {
+ if (ret == AVAHI_ERR_COLLISION)
+ {
+ char* alt_name = avahi_alternative_service_name(fp_service_info->m_name.c_str());
+ fp_service_info->m_name = alt_name;
+ avahi_free(alt_name);
+ CLog::Log(LOGINFO, "CZeroconfAvahi::addService: Service name collision. Renamed to: {}",
+ fp_service_info->m_name);
+ addService(fp_service_info, fp_client);
+ return;
+ }
+ CLog::Log(LOGERROR,
+ "CZeroconfAvahi::addService(): failed to add service named:{}@$(HOSTNAME) type:{} "
+ "port:{}. Error:{} :/ FIXME!",
+ fp_service_info->m_name, fp_service_info->m_type, fp_service_info->m_port,
+ avahi_strerror(ret));
+ return;
+ }
+ }
+
+ // Tell the server to register the service
+ if ((ret = avahi_entry_group_commit(fp_service_info->mp_group)) < 0)
+ {
+ CLog::Log(LOGERROR, "CZeroconfAvahi::addService(): Failed to commit entry group! Error:{}",
+ avahi_strerror(ret));
+ //! @todo what now? reset the group? free it?
+ }
+}
diff --git a/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.h b/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.h
new file mode 100644
index 0000000..102df9b
--- /dev/null
+++ b/xbmc/platform/linux/network/zeroconf/ZeroconfAvahi.h
@@ -0,0 +1,79 @@
+/*
+ * 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 "network/Zeroconf.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <avahi-client/client.h>
+#include <avahi-client/publish.h>
+#include <avahi-common/defs.h>
+
+struct AvahiThreadedPoll;
+
+class CZeroconfAvahi : public CZeroconf
+{
+public:
+ CZeroconfAvahi();
+ ~CZeroconfAvahi() override;
+
+protected:
+ //implement base CZeroConf interface
+ bool doPublishService(const std::string& fcr_identifier,
+ const std::string& fcr_type,
+ const std::string& fcr_name,
+ unsigned int f_port,
+ const std::vector<std::pair<std::string, std::string> >& txt) override;
+
+ bool doForceReAnnounceService(const std::string& fcr_identifier) override;
+ bool doRemoveService(const std::string& fcr_ident) override;
+
+ void doStop() override;
+
+private:
+ ///this is where the client calls us if state changes
+ static void clientCallback(AvahiClient* fp_client, AvahiClientState f_state, void*);
+ ///here we get notified of group changes
+ static void groupCallback(AvahiEntryGroup *fp_group, AvahiEntryGroupState f_state, void *);
+ //shutdown callback; works around a problem in avahi < 0.6.24 see destructor for details
+ static void shutdownCallback(AvahiTimeout *fp_e, void *);
+
+ ///creates the avahi client;
+ ///@return true on success
+ bool createClient();
+
+ //don't access stuff below without stopping the client thread
+ //see http://avahi.org/wiki/RunningAvahiClientAsThread
+ //and use struct ScopedEventLoopBlock
+
+ //helper struct for holding information about creating a service / AvahiEntryGroup
+ //we have to hold that as it's needed to recreate the service
+ struct ServiceInfo;
+ typedef std::map<std::string, std::shared_ptr<ServiceInfo> > tServiceMap;
+
+ //goes through a list of todos and publishs them (takes the client a param, as it might be called from
+ // from the callbacks)
+ void updateServices(AvahiClient* fp_client);
+ //helper that actually does the work of publishing
+ void addService(const tServiceMap::mapped_type& fp_service_info, AvahiClient* fp_client);
+
+ AvahiClient* mp_client = 0;
+ AvahiThreadedPoll* mp_poll = 0;
+
+ //this holds all published and unpublished services including info on howto create them
+ tServiceMap m_services;
+
+ //2 variables below are needed for workaround of avahi bug (see destructor for details)
+ bool m_shutdown = false;
+ pthread_t m_thread_id = 0;
+};
diff --git a/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.cpp b/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.cpp
new file mode 100644
index 0000000..491b98b
--- /dev/null
+++ b/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.cpp
@@ -0,0 +1,383 @@
+/*
+ * 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 "ZeroconfBrowserAvahi.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+#include <avahi-common/error.h>
+#include <avahi-common/malloc.h>
+namespace
+{
+///helper RAII-struct to block event loop for modifications
+struct ScopedEventLoopBlock
+{
+ explicit ScopedEventLoopBlock ( AvahiThreadedPoll* fp_poll ) : mp_poll ( fp_poll )
+ {
+ avahi_threaded_poll_lock ( mp_poll );
+ }
+
+ ~ScopedEventLoopBlock()
+ {
+ avahi_threaded_poll_unlock ( mp_poll );
+ }
+private:
+ AvahiThreadedPoll* mp_poll;
+};
+}
+
+CZeroconfBrowserAvahi::CZeroconfBrowserAvahi()
+{
+ if ( ! ( mp_poll = avahi_threaded_poll_new() ) )
+ {
+ CLog::Log ( LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Could not create threaded poll object" );
+ //! @todo throw exception? can this even happen?
+ return;
+ }
+
+ if ( !createClient() )
+ {
+ CLog::Log ( LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Could not create client" );
+ //yeah, what if not? but should always succeed (as client_no_fail or something is passed)
+ }
+
+ //start event loop thread
+ if ( avahi_threaded_poll_start ( mp_poll ) < 0 )
+ {
+ CLog::Log ( LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Failed to start avahi client thread" );
+ }
+}
+
+CZeroconfBrowserAvahi::~CZeroconfBrowserAvahi()
+{
+ CLog::Log ( LOGDEBUG, "CZeroconfAvahi::~CZeroconfAvahi() Going down! cleaning up..." );
+ if ( mp_poll )
+ {
+ //stop avahi polling thread
+ avahi_threaded_poll_stop(mp_poll);
+ }
+ //free the client (frees all browsers, groups, ...)
+ if ( mp_client )
+ avahi_client_free ( mp_client );
+ if ( mp_poll )
+ avahi_threaded_poll_free ( mp_poll );
+}
+
+bool CZeroconfBrowserAvahi::doAddServiceType ( const std::string& fcr_service_type )
+{
+ ScopedEventLoopBlock lock ( mp_poll );
+ tBrowserMap::iterator it = m_browsers.find ( fcr_service_type );
+ if ( it != m_browsers.end() )
+ return false;
+ else
+ it = m_browsers.insert ( std::make_pair ( fcr_service_type, ( AvahiServiceBrowser* ) 0 ) ).first;
+
+ //if the client is running, we directly create a browser for the service here
+ if ( mp_client && avahi_client_get_state ( mp_client ) == AVAHI_CLIENT_S_RUNNING )
+ {
+ AvahiServiceBrowser* browser = createServiceBrowser ( fcr_service_type, mp_client, this);
+ if ( !browser )
+ {
+ m_browsers.erase ( it );
+ return false;
+ }
+ else
+ {
+ it->second = browser;
+ return true;
+ }
+ }
+ else
+ {
+ CLog::Log ( LOGINFO, "CZeroconfBrowserAvahi::doAddServiceType client not available. service browsing queued" );
+ return true;
+ }
+}
+
+bool CZeroconfBrowserAvahi::doRemoveServiceType ( const std::string& fcr_service_type )
+{
+ ScopedEventLoopBlock lock ( mp_poll );
+ tBrowserMap::iterator it = m_browsers.find ( fcr_service_type );
+ if ( it == m_browsers.end() )
+ return false;
+ else
+ {
+ if ( it->second )
+ {
+ avahi_service_browser_free ( it->second );
+ m_all_for_now_browsers.erase ( it->second );
+ }
+ m_browsers.erase ( it );
+ //remove this serviceType from the list of discovered services
+ for (auto itr = m_discovered_services.begin(); itr != m_discovered_services.end();)
+ {
+ if (itr->first.GetType() == fcr_service_type)
+ itr = m_discovered_services.erase(itr);
+ else
+ ++itr;
+ }
+ }
+ return true;
+}
+
+std::vector<CZeroconfBrowser::ZeroconfService> CZeroconfBrowserAvahi::doGetFoundServices()
+{
+ std::vector<CZeroconfBrowser::ZeroconfService> ret;
+ ScopedEventLoopBlock lock ( mp_poll );
+ ret.reserve ( m_discovered_services.size() );
+ for (const auto& it : m_discovered_services)
+ ret.push_back(it.first);
+ return ret;
+}
+
+bool CZeroconfBrowserAvahi::doResolveService ( CZeroconfBrowser::ZeroconfService& fr_service, double f_timeout )
+{
+ {
+ //wait for lock on event-loop to schedule resolving
+ ScopedEventLoopBlock lock ( mp_poll );
+ //avahi can only resolve already discovered services, as it needs info from there
+ tDiscoveredServices::const_iterator it = m_discovered_services.find( fr_service );
+ if ( it == m_discovered_services.end() )
+ {
+ CLog::Log ( LOGERROR, "CZeroconfBrowserAvahi::doResolveService called with undiscovered service, resolving is NOT possible" );
+ return false;
+ }
+ //start resolving
+ m_resolving_service = fr_service;
+ m_resolved_event.Reset();
+ if ( !avahi_service_resolver_new ( mp_client, it->second.interface, it->second.protocol,
+ it->first.GetName().c_str(), it->first.GetType().c_str(), it->first.GetDomain().c_str(),
+ AVAHI_PROTO_UNSPEC, AvahiLookupFlags ( 0 ), resolveCallback, this ) )
+ {
+ CLog::Log(LOGERROR,
+ "CZeroconfBrowserAvahi::doResolveService Failed to resolve service '{}': {}\n",
+ it->first.GetName(), avahi_strerror(avahi_client_errno(mp_client)));
+ return false;
+ }
+ } // end of this block releases lock of eventloop
+
+ //wait for resolve to return or timeout
+ m_resolved_event.Wait(std::chrono::duration<double, std::milli>(f_timeout * 1000));
+ {
+ ScopedEventLoopBlock lock ( mp_poll );
+ fr_service = m_resolving_service;
+ return (!fr_service.GetIP().empty());
+ }
+}
+
+void CZeroconfBrowserAvahi::clientCallback ( AvahiClient* fp_client, AvahiClientState f_state, void* fp_data )
+{
+ CZeroconfBrowserAvahi* p_instance = static_cast<CZeroconfBrowserAvahi*> ( fp_data );
+ switch ( f_state )
+ {
+ case AVAHI_CLIENT_S_RUNNING:
+ {
+ CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::clientCallback: client is up and running" );
+ for (auto& it : p_instance->m_browsers)
+ {
+ assert(!it.second);
+ it.second = createServiceBrowser(it.first, fp_client, fp_data);
+ }
+ break;
+ }
+ case AVAHI_CLIENT_FAILURE:
+ {
+ CLog::Log ( LOGINFO, "CZeroconfBrowserAvahi::clientCallback: client failure. avahi-daemon stopped? Recreating client..." );
+ //We were forced to disconnect from server. now free and recreate the client object
+ avahi_client_free ( fp_client );
+ p_instance->mp_client = 0;
+ //freeing the client also frees all groups and browsers, pointers are undefined afterwards, so fix that now
+ for (auto& it : p_instance->m_browsers)
+ it.second = (AvahiServiceBrowser*)0;
+ //clean the list of discovered services and update gui (if someone is interested)
+ p_instance->m_discovered_services.clear();
+ CGUIMessage message ( GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH );
+ message.SetStringParam ( "zeroconf://" );
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage ( message );
+ p_instance->createClient();
+ break;
+ }
+ case AVAHI_CLIENT_S_COLLISION:
+ case AVAHI_CLIENT_S_REGISTERING:
+ //HERE WE SHOULD REMOVE ALL OF OUR SERVICES AND "RESCHEDULE" them for later addition
+ CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::clientCallback: This should not happen" );
+ break;
+
+ case AVAHI_CLIENT_CONNECTING:
+ CLog::Log ( LOGINFO, "CZeroconfBrowserAvahi::clientCallback: avahi server not available. But may become later..." );
+ break;
+ }
+}
+void CZeroconfBrowserAvahi::browseCallback (
+ AvahiServiceBrowser *browser, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event,
+ const char *name, const char *type, const char *domain,
+ AvahiLookupResultFlags flags, void* fp_data )
+{
+ CZeroconfBrowserAvahi* p_instance = static_cast<CZeroconfBrowserAvahi*> ( fp_data );
+ assert ( browser );
+ bool update_gui = false;
+ /* Called whenever a new services becomes available on the LAN or is removed from the LAN */
+ switch ( event )
+ {
+ case AVAHI_BROWSER_FAILURE:
+ CLog::Log(LOGERROR, "CZeroconfBrowserAvahi::browseCallback error: {}\n",
+ avahi_strerror(avahi_client_errno(avahi_service_browser_get_client(browser))));
+ //! @todo implement
+ return;
+ case AVAHI_BROWSER_NEW:
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "CZeroconfBrowserAvahi::browseCallback NEW: service '{}' of type '{}' in domain '{}'\n",
+ name, type, domain);
+ //store the service
+ ZeroconfService service(name, type, domain);
+ AvahiSpecificInfos info;
+ info.interface = interface;
+ info.protocol = protocol;
+ p_instance->m_discovered_services.insert ( std::make_pair ( service, info ) );
+ //if this browser already sent the all for now message, we need to update the gui now
+ if( p_instance->m_all_for_now_browsers.find(browser) != p_instance->m_all_for_now_browsers.end() )
+ update_gui = true;
+ break;
+ }
+ case AVAHI_BROWSER_REMOVE:
+ {
+ //remove the service
+ ZeroconfService service(name, type, domain);
+ p_instance->m_discovered_services.erase ( service );
+ CLog::Log(LOGDEBUG,
+ "CZeroconfBrowserAvahi::browseCallback REMOVE: service '{}' of type '{}' in "
+ "domain '{}'\n",
+ name, type, domain);
+ //if this browser already sent the all for now message, we need to update the gui now
+ if( p_instance->m_all_for_now_browsers.find(browser) != p_instance->m_all_for_now_browsers.end() )
+ update_gui = true;
+ break;
+ }
+ case AVAHI_BROWSER_CACHE_EXHAUSTED:
+ //do we need that?
+ break;
+ case AVAHI_BROWSER_ALL_FOR_NOW:
+ CLog::Log(LOGDEBUG, "CZeroconfBrowserAvahi::browseCallback all for now (service = {})", type);
+ //if this browser already sent the all for now message, we need to update the gui now
+ bool success = p_instance->m_all_for_now_browsers.insert(browser).second;
+ if(!success)
+ CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::browseCallback AVAHI_BROWSER_ALL_FOR_NOW sent twice?!");
+ update_gui = true;
+ break;
+ }
+ if ( update_gui )
+ {
+ CGUIMessage message ( GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH );
+ message.SetStringParam ( "zeroconf://" );
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage ( message );
+ CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::browseCallback sent gui update for path zeroconf://" );
+ }
+}
+
+CZeroconfBrowser::ZeroconfService::tTxtRecordMap GetTxtRecords(AvahiStringList *txt)
+{
+ AvahiStringList *i = NULL;
+ CZeroconfBrowser::ZeroconfService::tTxtRecordMap recordMap;
+
+ for( i = txt; i; i = i->next )
+ {
+ char *key, *value;
+
+ if( avahi_string_list_get_pair( i, &key, &value, NULL ) < 0 )
+ continue;
+
+ recordMap.insert(
+ std::make_pair(
+ std::string(key),
+ std::string(value)
+ )
+ );
+
+ if( key )
+ avahi_free(key);
+ if( value )
+ avahi_free(value);
+ }
+ return recordMap;
+}
+
+void CZeroconfBrowserAvahi::resolveCallback(
+ AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event,
+ const char *name, const char *type, const char *domain, const char *host_name,
+ const AvahiAddress *address, uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags, void* userdata )
+{
+ assert ( r );
+ assert ( userdata );
+ CZeroconfBrowserAvahi* p_instance = static_cast<CZeroconfBrowserAvahi*> ( userdata );
+ switch ( event )
+ {
+ case AVAHI_RESOLVER_FAILURE:
+ CLog::Log(LOGERROR,
+ "CZeroconfBrowserAvahi::resolveCallback Failed to resolve service '{}' of type "
+ "'{}' in domain '{}': {}\n",
+ name, type, domain,
+ avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r))));
+ break;
+ case AVAHI_RESOLVER_FOUND:
+ {
+ char a[AVAHI_ADDRESS_STR_MAX];
+ CLog::Log(LOGDEBUG,
+ "CZeroconfBrowserAvahi::resolveCallback resolved service '{}' of type '{}' in "
+ "domain '{}':\n",
+ name, type, domain);
+
+ avahi_address_snprint ( a, sizeof ( a ), address );
+ p_instance->m_resolving_service.SetIP(a);
+ p_instance->m_resolving_service.SetPort(port);
+ //get txt-record list
+ p_instance->m_resolving_service.SetTxtRecords(GetTxtRecords(txt));
+ break;
+ }
+ }
+ avahi_service_resolver_free ( r );
+ p_instance->m_resolved_event.Set();
+}
+
+bool CZeroconfBrowserAvahi::createClient()
+{
+ assert ( mp_poll );
+ if ( mp_client )
+ {
+ avahi_client_free ( mp_client );
+ }
+ mp_client = avahi_client_new ( avahi_threaded_poll_get ( mp_poll ),
+ AVAHI_CLIENT_NO_FAIL, &clientCallback, this, 0 );
+ if ( !mp_client )
+ {
+ mp_client = 0;
+ return false;
+ }
+ return true;
+}
+
+AvahiServiceBrowser* CZeroconfBrowserAvahi::createServiceBrowser ( const std::string& fcr_service_type, AvahiClient* fp_client, void* fp_userdata)
+{
+ assert(fp_client);
+ AvahiServiceBrowser* ret = avahi_service_browser_new ( fp_client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, fcr_service_type.c_str(),
+ NULL, ( AvahiLookupFlags ) 0, browseCallback, fp_userdata );
+ if ( !ret )
+ {
+ CLog::Log(
+ LOGERROR,
+ "CZeroconfBrowserAvahi::createServiceBrowser Failed to create service ({}) browser: {}",
+ avahi_strerror(avahi_client_errno(fp_client)), fcr_service_type);
+ }
+ return ret;
+}
diff --git a/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.h b/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.h
new file mode 100644
index 0000000..cc8c6c4
--- /dev/null
+++ b/xbmc/platform/linux/network/zeroconf/ZeroconfBrowserAvahi.h
@@ -0,0 +1,94 @@
+/*
+ * 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 "network/ZeroconfBrowser.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <map>
+#include <memory>
+#include <vector>
+
+#include <avahi-client/client.h>
+#include <avahi-client/lookup.h>
+#include <avahi-common/defs.h>
+#include <avahi-common/thread-watch.h>
+
+//platform specific implementation of zeroconfbrowser interface using avahi
+class CZeroconfBrowserAvahi : public CZeroconfBrowser
+{
+ public:
+ CZeroconfBrowserAvahi();
+ ~CZeroconfBrowserAvahi() override;
+
+ private:
+ ///implementation if CZeroconfBrowser interface
+ ///@{
+ bool doAddServiceType(const std::string& fcr_service_type) override;
+ bool doRemoveServiceType(const std::string& fcr_service_type) override;
+
+ std::vector<CZeroconfBrowser::ZeroconfService> doGetFoundServices() override;
+ bool doResolveService(CZeroconfBrowser::ZeroconfService& fr_service, double f_timeout) override;
+ ///@}
+
+ /// avahi callbacks
+ ///@{
+ static void clientCallback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void * userdata);
+ static void browseCallback(
+ AvahiServiceBrowser *b,
+ AvahiIfIndex interface,
+ AvahiProtocol protocol,
+ AvahiBrowserEvent event,
+ const char *name,
+ const char *type,
+ const char *domain,
+ AVAHI_GCC_UNUSED AvahiLookupResultFlags flags,
+ void* userdata);
+ static void resolveCallback(
+ AvahiServiceResolver *r,
+ AvahiIfIndex interface,
+ AvahiProtocol protocol,
+ AvahiResolverEvent event,
+ const char *name,
+ const char *type,
+ const char *domain,
+ const char *host_name,
+ const AvahiAddress *address,
+ uint16_t port,
+ AvahiStringList *txt,
+ AvahiLookupResultFlags flags,
+ AVAHI_GCC_UNUSED void* userdata);
+ //helpers
+ bool createClient();
+ static AvahiServiceBrowser* createServiceBrowser(const std::string& fcr_service_type, AvahiClient* fp_client, void* fp_userdata);
+
+ //shared variables between avahi thread and interface
+ AvahiClient* mp_client = 0 ;
+ AvahiThreadedPoll* mp_poll = 0 ;
+ // tBrowserMap maps service types the corresponding browser
+ typedef std::map<std::string, AvahiServiceBrowser*> tBrowserMap;
+ tBrowserMap m_browsers;
+
+ // if a browser is in this set, it already sent an ALL_FOR_NOW message
+ // (needed to bundle GUI_MSG_UPDATE_PATH messages
+ std::set<AvahiServiceBrowser*> m_all_for_now_browsers;
+
+ //this information is needed for avahi to resolve a service,
+ //so unfortunately we'll only be able to resolve services already discovered
+ struct AvahiSpecificInfos
+ {
+ AvahiIfIndex interface;
+ AvahiProtocol protocol;
+ };
+ typedef std::map<CZeroconfBrowser::ZeroconfService, AvahiSpecificInfos> tDiscoveredServices;
+ tDiscoveredServices m_discovered_services;
+ CZeroconfBrowser::ZeroconfService m_resolving_service;
+ CEvent m_resolved_event;
+};
diff --git a/xbmc/platform/linux/peripherals/CMakeLists.txt b/xbmc/platform/linux/peripherals/CMakeLists.txt
new file mode 100644
index 0000000..626aa40
--- /dev/null
+++ b/xbmc/platform/linux/peripherals/CMakeLists.txt
@@ -0,0 +1,11 @@
+if(UDEV_FOUND)
+ list(APPEND SOURCES PeripheralBusUSBLibUdev.cpp)
+ list(APPEND HEADERS PeripheralBusUSBLibUdev.h)
+elseif(LIBUSB_FOUND)
+ list(APPEND SOURCES PeripheralBusUSBLibUSB.cpp)
+ list(APPEND HEADERS PeripheralBusUSBLibUSB.h)
+endif()
+
+if(SOURCES)
+ core_add_library(platform_linux_peripherals)
+endif()
diff --git a/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.cpp b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.cpp
new file mode 100644
index 0000000..5c788bb
--- /dev/null
+++ b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.cpp
@@ -0,0 +1,78 @@
+/*
+ * 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 "PeripheralBusUSBLibUSB.h"
+
+#include "peripherals/Peripherals.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <usb.h>
+
+using namespace PERIPHERALS;
+
+CPeripheralBusUSB::CPeripheralBusUSB(CPeripherals& manager) :
+ CPeripheralBus("PeripBusUSB", manager, PERIPHERAL_BUS_USB)
+{
+ usb_init();
+ usb_find_busses();
+ m_busses = usb_get_busses();
+ CLog::Log(LOGDEBUG, "{} - using libusb peripheral scanning", __FUNCTION__);
+}
+
+bool CPeripheralBusUSB::PerformDeviceScan(PeripheralScanResults &results)
+{
+ struct usb_bus *bus;
+ usb_find_devices();
+ for (bus = m_busses; bus; bus = bus->next)
+ {
+ struct usb_device *dev;
+ for (dev = bus->devices; dev; dev = dev->next)
+ {
+ PeripheralScanResult result(m_type);
+ result.m_iVendorId = dev->descriptor.idVendor;
+ result.m_iProductId = dev->descriptor.idProduct;
+ result.m_type = (dev->descriptor.bDeviceClass == USB_CLASS_PER_INTERFACE && dev->descriptor.bNumConfigurations > 0 &&
+ dev->config[0].bNumInterfaces > 0 && dev->config[0].interface[0].num_altsetting > 0) ?
+ GetType(dev->config[0].interface[0].altsetting[0].bInterfaceClass) :
+ GetType(dev->descriptor.bDeviceClass);
+#ifdef TARGET_FREEBSD
+ result.m_strLocation = std::to_string(dev->filename);
+#else
+ result.m_strLocation = StringUtils::Format("/bus{}/dev{}", bus->dirname, dev->filename);
+#endif
+ result.m_iSequence = GetNumberOfPeripheralsWithId(result.m_iVendorId, result.m_iProductId);
+ if (!results.ContainsResult(result))
+ results.m_results.push_back(result);
+ }
+ }
+
+ return true;
+}
+
+const PeripheralType CPeripheralBusUSB::GetType(int iDeviceClass)
+{
+ switch (iDeviceClass)
+ {
+ case USB_CLASS_HID:
+ return PERIPHERAL_HID;
+ case USB_CLASS_COMM:
+ return PERIPHERAL_NIC;
+ case USB_CLASS_MASS_STORAGE:
+ return PERIPHERAL_DISK;
+ case USB_CLASS_PER_INTERFACE:
+ case USB_CLASS_AUDIO:
+ case USB_CLASS_PRINTER:
+ case USB_CLASS_PTP:
+ case USB_CLASS_HUB:
+ case USB_CLASS_DATA:
+ case USB_CLASS_VENDOR_SPEC:
+ default:
+ return PERIPHERAL_UNKNOWN;
+ }
+}
diff --git a/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.h b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.h
new file mode 100644
index 0000000..d2e7e48
--- /dev/null
+++ b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUSB.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "peripherals/bus/PeripheralBus.h"
+#include "peripherals/devices/Peripheral.h"
+
+struct usb_bus;
+
+namespace PERIPHERALS
+{
+ class CPeripherals;
+
+ class CPeripheralBusUSB : public CPeripheralBus
+ {
+ public:
+ explicit CPeripheralBusUSB(CPeripherals& manager);
+
+ /*!
+ * @see PeripheralBus::PerformDeviceScan()
+ */
+ bool PerformDeviceScan(PeripheralScanResults &results);
+
+ protected:
+ static const PeripheralType GetType(int iDeviceClass);
+ struct usb_bus *m_busses;
+ };
+}
diff --git a/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.cpp b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.cpp
new file mode 100644
index 0000000..aeb5e71
--- /dev/null
+++ b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.cpp
@@ -0,0 +1,254 @@
+/*
+ * 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 "PeripheralBusUSBLibUdev.h"
+#include "peripherals/Peripherals.h"
+extern "C" {
+#include <libudev.h>
+}
+#include <cassert>
+#include <poll.h>
+#include "utils/log.h"
+
+#ifndef USB_CLASS_PER_INTERFACE
+#define USB_CLASS_PER_INTERFACE 0
+#endif
+
+#ifndef USB_CLASS_AUDIO
+#define USB_CLASS_AUDIO 1
+#endif
+
+#ifndef USB_CLASS_COMM
+#define USB_CLASS_COMM 2
+#endif
+
+#ifndef USB_CLASS_HID
+#define USB_CLASS_HID 3
+#endif
+
+#ifndef USB_CLASS_PHYSICAL
+#define USB_CLASS_PHYSICAL 5
+#endif
+
+#ifndef USB_CLASS_PTP
+#define USB_CLASS_PTP 6
+#endif
+
+#ifndef USB_CLASS_PRINTER
+#define USB_CLASS_PRINTER 7
+#endif
+
+#ifndef USB_CLASS_MASS_STORAGE
+#define USB_CLASS_MASS_STORAGE 8
+#endif
+
+#ifndef USB_CLASS_HUB
+#define USB_CLASS_HUB 9
+#endif
+
+#ifndef USB_CLASS_DATA
+#define USB_CLASS_DATA 10
+#endif
+
+#ifndef USB_CLASS_APP_SPEC
+#define USB_CLASS_APP_SPEC 0xfe
+#endif
+
+#ifndef USB_CLASS_VENDOR_SPEC
+#define USB_CLASS_VENDOR_SPEC 0xff
+#endif
+
+using namespace PERIPHERALS;
+
+CPeripheralBusUSB::CPeripheralBusUSB(CPeripherals& manager) :
+ CPeripheralBus("PeripBusUSBUdev", manager, PERIPHERAL_BUS_USB)
+{
+ /* the Process() method in this class overrides the one in CPeripheralBus, so leave this set to true */
+ m_bNeedsPolling = true;
+
+ m_udev = NULL;
+ m_udevMon = NULL;
+}
+
+CPeripheralBusUSB::~CPeripheralBusUSB(void)
+{
+ StopThread(true);
+}
+
+bool CPeripheralBusUSB::PerformDeviceScan(PeripheralScanResults &results)
+{
+ // We don't want this one to be called from outside world
+ assert(IsCurrentThread());
+
+ struct udev_enumerate *enumerate;
+ struct udev_list_entry *devices, *dev_list_entry;
+ struct udev_device *dev(NULL), *parent(NULL);
+ enumerate = udev_enumerate_new(m_udev);
+ udev_enumerate_scan_devices(enumerate);
+ devices = udev_enumerate_get_list_entry(enumerate);
+
+ bool bContinue(true);
+ std::string strPath, strClass;
+ udev_list_entry_foreach(dev_list_entry, devices)
+ {
+ strPath = udev_list_entry_get_name(dev_list_entry);
+ if (strPath.empty())
+ bContinue = false;
+
+ if (bContinue)
+ {
+ if (!(parent = udev_device_new_from_syspath(m_udev, strPath.c_str())))
+ bContinue = false;
+ }
+
+ if (bContinue)
+ {
+ dev = udev_device_get_parent(udev_device_get_parent(parent));
+ if (!dev || !udev_device_get_sysattr_value(dev,"idVendor") || !udev_device_get_sysattr_value(dev, "idProduct"))
+ bContinue = false;
+ }
+
+ if (bContinue)
+ {
+ strClass = udev_device_get_sysattr_value(dev, "bDeviceClass");
+ if (strClass.empty())
+ bContinue = false;
+ }
+
+ if (bContinue)
+ {
+ int iClass = PeripheralTypeTranslator::HexStringToInt(strClass.c_str());
+ if (iClass == USB_CLASS_PER_INTERFACE)
+ {
+ //! @todo just assume this is a HID device for now, since the only devices that we're currently
+ //! interested in are HID devices
+ iClass = USB_CLASS_HID;
+ }
+
+ PeripheralScanResult result(m_type);
+ result.m_iVendorId = PeripheralTypeTranslator::HexStringToInt(udev_device_get_sysattr_value(dev, "idVendor"));
+ result.m_iProductId = PeripheralTypeTranslator::HexStringToInt(udev_device_get_sysattr_value(dev, "idProduct"));
+ result.m_type = GetType(iClass);
+ result.m_strLocation = udev_device_get_syspath(dev);
+ result.m_iSequence = GetNumberOfPeripheralsWithId(result.m_iVendorId, result.m_iProductId);
+ if (!results.ContainsResult(result))
+ results.m_results.push_back(result);
+ }
+
+ bContinue = true;
+ if (parent)
+ {
+ /* unref the _parent_ device */
+ udev_device_unref(parent);
+ parent = NULL;
+ }
+ }
+ /* Free the enumerator object */
+ udev_enumerate_unref(enumerate);
+
+ return true;
+}
+
+PeripheralType CPeripheralBusUSB::GetType(int iDeviceClass)
+{
+ switch (iDeviceClass)
+ {
+ case USB_CLASS_HID:
+ return PERIPHERAL_HID;
+ case USB_CLASS_COMM:
+ return PERIPHERAL_NIC;
+ case USB_CLASS_MASS_STORAGE:
+ return PERIPHERAL_DISK;
+ case USB_CLASS_PER_INTERFACE:
+ case USB_CLASS_AUDIO:
+ case USB_CLASS_PRINTER:
+ case USB_CLASS_PTP:
+ case USB_CLASS_HUB:
+ case USB_CLASS_DATA:
+ case USB_CLASS_VENDOR_SPEC:
+ default:
+ return PERIPHERAL_UNKNOWN;
+ }
+}
+
+void CPeripheralBusUSB::Process(void)
+{
+ if (!(m_udev = udev_new()))
+ {
+ CLog::Log(LOGERROR, "{} - failed to allocate udev context", __FUNCTION__);
+ return;
+ }
+
+ /* set up a devices monitor that listen for any device change */
+ m_udevMon = udev_monitor_new_from_netlink(m_udev, "udev");
+
+ /* filter to only receive usb events */
+ if (udev_monitor_filter_add_match_subsystem_devtype(m_udevMon, "usb", nullptr) < 0)
+ {
+ CLog::Log(LOGERROR, "Could not limit filter on USB only");
+ }
+
+ CLog::Log(LOGDEBUG, "{} - initialised udev monitor", __FUNCTION__);
+
+ udev_monitor_enable_receiving(m_udevMon);
+ bool bUpdated(false);
+ ScanForDevices();
+ while (!m_bStop)
+ {
+ bUpdated = WaitForUpdate();
+ if (bUpdated && !m_bStop)
+ ScanForDevices();
+ }
+ udev_monitor_unref(m_udevMon);
+ udev_unref(m_udev);
+}
+
+void CPeripheralBusUSB::Clear(void)
+{
+ StopThread(false);
+
+ CPeripheralBus::Clear();
+}
+
+bool CPeripheralBusUSB::WaitForUpdate()
+{
+ int udevFd = udev_monitor_get_fd(m_udevMon);
+
+ if (udevFd < 0)
+ {
+ CLog::Log(LOGERROR, "{} - get udev monitor", __FUNCTION__);
+ return false;
+ }
+
+ /* poll for udev changes */
+ struct pollfd pollFd;
+ pollFd.fd = udevFd;
+ pollFd.events = POLLIN;
+ int iPollResult;
+ while (!m_bStop && ((iPollResult = poll(&pollFd, 1, 100)) <= 0))
+ if (errno != EINTR && iPollResult != 0)
+ break;
+
+ /* the thread is being stopped, so just return false */
+ if (m_bStop)
+ return false;
+
+ /* we have to read the message from the queue, even though we're not actually using it */
+ struct udev_device *dev = udev_monitor_receive_device(m_udevMon);
+ if (dev)
+ udev_device_unref(dev);
+ else
+ {
+ CLog::Log(LOGERROR, "{} - failed to get device from udev_monitor_receive_device()",
+ __FUNCTION__);
+ Clear();
+ return false;
+ }
+
+ return true;
+}
diff --git a/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.h b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.h
new file mode 100644
index 0000000..72cb437
--- /dev/null
+++ b/xbmc/platform/linux/peripherals/PeripheralBusUSBLibUdev.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "peripherals/bus/PeripheralBus.h"
+#include "peripherals/devices/Peripheral.h"
+
+struct udev;
+struct udev_monitor;
+
+namespace PERIPHERALS
+{
+ class CPeripherals;
+
+ class CPeripheralBusUSB : public CPeripheralBus
+ {
+ public:
+ explicit CPeripheralBusUSB(CPeripherals& manager);
+ ~CPeripheralBusUSB(void) override;
+
+ void Clear(void) override;
+
+ /*!
+ * @see PeripheralBus::PerformDeviceScan()
+ */
+ bool PerformDeviceScan(PeripheralScanResults &results) override;
+
+ protected:
+ static PeripheralType GetType(int iDeviceClass);
+
+ void Process(void) override;
+ bool WaitForUpdate(void);
+
+ struct udev * m_udev;
+ struct udev_monitor *m_udevMon;
+ };
+}
diff --git a/xbmc/platform/linux/powermanagement/CMakeLists.txt b/xbmc/platform/linux/powermanagement/CMakeLists.txt
new file mode 100644
index 0000000..3694a79
--- /dev/null
+++ b/xbmc/platform/linux/powermanagement/CMakeLists.txt
@@ -0,0 +1,17 @@
+set(SOURCES LinuxPowerSyscall.cpp)
+
+set(HEADERS FallbackPowerSyscall.h
+ LinuxPowerSyscall.h)
+
+if(DBUS_FOUND)
+ list(APPEND SOURCES ConsoleUPowerSyscall.cpp
+ LogindUPowerSyscall.cpp
+ UPowerSyscall.cpp)
+ list(APPEND HEADERS ConsoleUPowerSyscall.h
+ LogindUPowerSyscall.h
+ UPowerSyscall.h)
+endif()
+
+if(SOURCES)
+ core_add_library(platform_linux_powermanagement)
+endif()
diff --git a/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.cpp b/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.cpp
new file mode 100644
index 0000000..6f04dd7
--- /dev/null
+++ b/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.cpp
@@ -0,0 +1,50 @@
+/*
+ * 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 "ConsoleUPowerSyscall.h"
+
+#include "utils/log.h"
+
+CConsoleUPowerSyscall::CConsoleUPowerSyscall()
+{
+ m_CanPowerdown = ConsoleKitMethodCall("CanStop");
+ m_CanReboot = ConsoleKitMethodCall("CanRestart");
+}
+
+bool CConsoleUPowerSyscall::Powerdown()
+{
+ CDBusMessage message("org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager", "org.freedesktop.ConsoleKit.Manager", "Stop");
+ return message.SendSystem() != NULL;
+}
+
+bool CConsoleUPowerSyscall::Reboot()
+{
+ CDBusMessage message("org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager", "org.freedesktop.ConsoleKit.Manager", "Restart");
+ return message.SendSystem() != NULL;
+}
+
+bool CConsoleUPowerSyscall::HasConsoleKitAndUPower()
+{
+ return CDBusUtil::TryMethodCall(DBUS_BUS_SYSTEM, "org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager", "org.freedesktop.ConsoleKit.Manager", "CanStop")
+ && HasUPower();
+}
+
+bool CConsoleUPowerSyscall::ConsoleKitMethodCall(const char *method)
+{
+ CDBusMessage message("org.freedesktop.ConsoleKit", "/org/freedesktop/ConsoleKit/Manager", "org.freedesktop.ConsoleKit.Manager", method);
+ DBusMessage *reply = message.SendSystem();
+ if (reply)
+ {
+ dbus_bool_t boolean = FALSE;
+
+ if (dbus_message_get_args (reply, NULL, DBUS_TYPE_BOOLEAN, &boolean, DBUS_TYPE_INVALID))
+ return boolean;
+ }
+
+ return false;
+}
diff --git a/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.h b/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.h
new file mode 100644
index 0000000..9fdd157
--- /dev/null
+++ b/xbmc/platform/linux/powermanagement/ConsoleUPowerSyscall.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "DBusUtil.h"
+#include "UPowerSyscall.h"
+
+class CConsoleUPowerSyscall : public CUPowerSyscall
+{
+public:
+ CConsoleUPowerSyscall();
+ bool Powerdown() override;
+ bool Reboot() override;
+ static bool HasConsoleKitAndUPower();
+private:
+ static bool ConsoleKitMethodCall(const char *method);
+};
diff --git a/xbmc/platform/linux/powermanagement/FallbackPowerSyscall.h b/xbmc/platform/linux/powermanagement/FallbackPowerSyscall.h
new file mode 100644
index 0000000..6988543
--- /dev/null
+++ b/xbmc/platform/linux/powermanagement/FallbackPowerSyscall.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "powermanagement/IPowerSyscall.h"
+
+class CFallbackPowerSyscall : public CPowerSyscallWithoutEvents
+{
+public:
+ bool Powerdown() override {return true; }
+ bool Suspend() override {return false; }
+ bool Hibernate() override {return false; }
+ bool Reboot() override {return true; }
+
+ bool CanPowerdown() override {return true; }
+ bool CanSuspend() override {return false; }
+ bool CanHibernate() override {return false; }
+ bool CanReboot() override {return true; }
+ int BatteryLevel() override {return 0; }
+};
diff --git a/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.cpp b/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.cpp
new file mode 100644
index 0000000..f3dbcdb
--- /dev/null
+++ b/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.cpp
@@ -0,0 +1,65 @@
+/*
+ * 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 "LinuxPowerSyscall.h"
+#include "FallbackPowerSyscall.h"
+#if defined(HAS_DBUS)
+#include "ConsoleUPowerSyscall.h"
+#include "LogindUPowerSyscall.h"
+#include "UPowerSyscall.h"
+#endif // HAS_DBUS
+
+#include <functional>
+#include <list>
+#include <memory>
+#include <utility>
+
+IPowerSyscall* CLinuxPowerSyscall::CreateInstance()
+{
+#if defined(HAS_DBUS)
+ std::unique_ptr<IPowerSyscall> bestPowerManager;
+ std::unique_ptr<IPowerSyscall> currPowerManager;
+ int bestCount = -1;
+ int currCount = -1;
+
+ std::list< std::pair< std::function<bool()>,
+ std::function<IPowerSyscall*()> > > powerManagers =
+ {
+ std::make_pair(CConsoleUPowerSyscall::HasConsoleKitAndUPower,
+ [] { return new CConsoleUPowerSyscall(); }),
+ std::make_pair(CLogindUPowerSyscall::HasLogind,
+ [] { return new CLogindUPowerSyscall(); }),
+ std::make_pair(CUPowerSyscall::HasUPower,
+ [] { return new CUPowerSyscall(); })
+ };
+ for(const auto& powerManager : powerManagers)
+ {
+ if (powerManager.first())
+ {
+ currPowerManager.reset(powerManager.second());
+ currCount = currPowerManager->CountPowerFeatures();
+ if (currCount > bestCount)
+ {
+ bestCount = currCount;
+ bestPowerManager = std::move(currPowerManager);
+ }
+ if (bestCount == IPowerSyscall::MAX_COUNT_POWER_FEATURES)
+ break;
+ }
+ }
+ if (bestPowerManager)
+ return bestPowerManager.release();
+ else
+#endif // HAS_DBUS
+ return new CFallbackPowerSyscall();
+}
+
+void CLinuxPowerSyscall::Register()
+{
+ IPowerSyscall::RegisterPowerSyscall(CLinuxPowerSyscall::CreateInstance);
+}
diff --git a/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.h b/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.h
new file mode 100644
index 0000000..b5cc218
--- /dev/null
+++ b/xbmc/platform/linux/powermanagement/LinuxPowerSyscall.h
@@ -0,0 +1,18 @@
+/*
+ * 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 "powermanagement/IPowerSyscall.h"
+
+class CLinuxPowerSyscall
+{
+public:
+ static IPowerSyscall* CreateInstance();
+ static void Register();
+};
diff --git a/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.cpp b/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.cpp
new file mode 100644
index 0000000..bd04197
--- /dev/null
+++ b/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.cpp
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2012 Denis Yantarev
+ * 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 "LogindUPowerSyscall.h"
+
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <string.h>
+
+#include <unistd.h>
+
+// logind DBus interface specification:
+// http://www.freedesktop.org/wiki/Software/Logind/logind
+//
+// Inhibitor Locks documentation:
+// http://www.freedesktop.org/wiki/Software/Logind/inhibit/
+
+#define LOGIND_DEST "org.freedesktop.login1"
+#define LOGIND_PATH "/org/freedesktop/login1"
+#define LOGIND_IFACE "org.freedesktop.login1.Manager"
+
+CLogindUPowerSyscall::CLogindUPowerSyscall()
+{
+ m_lowBattery = false;
+
+ CLog::Log(LOGINFO, "Selected Logind/UPower as PowerSyscall");
+
+ // Check if we have UPower. If not, we avoid any battery related operations.
+ CDBusMessage message("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "EnumerateDevices");
+ m_hasUPower = message.SendSystem() != NULL;
+
+ if (!m_hasUPower)
+ CLog::Log(LOGINFO, "LogindUPowerSyscall - UPower not found, battery information will not be available");
+
+ m_canPowerdown = LogindCheckCapability("CanPowerOff");
+ m_canReboot = LogindCheckCapability("CanReboot");
+ m_canHibernate = LogindCheckCapability("CanHibernate");
+ m_canSuspend = LogindCheckCapability("CanSuspend");
+
+ InhibitDelayLockSleep();
+
+ m_batteryLevel = 0;
+ if (m_hasUPower)
+ UpdateBatteryLevel();
+
+ if (!m_connection.Connect(DBUS_BUS_SYSTEM, true))
+ {
+ return;
+ }
+
+ CDBusError error;
+ dbus_connection_set_exit_on_disconnect(m_connection, false);
+ dbus_bus_add_match(m_connection, "type='signal',interface='org.freedesktop.login1.Manager',member='PrepareForSleep'", error);
+
+ if (!error && m_hasUPower)
+ dbus_bus_add_match(m_connection, "type='signal',interface='org.freedesktop.UPower',member='DeviceChanged'", error);
+
+ dbus_connection_flush(m_connection);
+
+ if (error)
+ {
+ error.Log("UPowerSyscall: Failed to attach to signal");
+ m_connection.Destroy();
+ }
+}
+
+CLogindUPowerSyscall::~CLogindUPowerSyscall()
+{
+ ReleaseDelayLockSleep();
+ ReleaseDelayLockShutdown();
+}
+
+bool CLogindUPowerSyscall::Powerdown()
+{
+ // delay shutdown so that the app can close properly
+ InhibitDelayLockShutdown();
+ return LogindSetPowerState("PowerOff");
+}
+
+bool CLogindUPowerSyscall::Reboot()
+{
+ return LogindSetPowerState("Reboot");
+}
+
+bool CLogindUPowerSyscall::Suspend()
+{
+ return LogindSetPowerState("Suspend");
+}
+
+bool CLogindUPowerSyscall::Hibernate()
+{
+ return LogindSetPowerState("Hibernate");
+}
+
+bool CLogindUPowerSyscall::CanPowerdown()
+{
+ return m_canPowerdown;
+}
+
+bool CLogindUPowerSyscall::CanSuspend()
+{
+ return m_canSuspend;
+}
+
+bool CLogindUPowerSyscall::CanHibernate()
+{
+ return m_canHibernate;
+}
+
+bool CLogindUPowerSyscall::CanReboot()
+{
+ return m_canReboot;
+}
+
+bool CLogindUPowerSyscall::HasLogind()
+{
+ // recommended method by systemd devs. The seats directory
+ // doesn't exist unless logind created it and therefore is running.
+ // see also https://mail.gnome.org/archives/desktop-devel-list/2013-March/msg00092.html
+ if (access("/run/systemd/seats/", F_OK) >= 0)
+ return true;
+
+ // on some environments "/run/systemd/seats/" doesn't exist, e.g. on flatpak. Try DBUS instead.
+ CDBusMessage message(LOGIND_DEST, LOGIND_PATH, LOGIND_IFACE, "ListSeats");
+ DBusMessage *reply = message.SendSystem();
+ if (!reply)
+ return false;
+
+ DBusMessageIter arrIter;
+ if (dbus_message_iter_init(reply, &arrIter) && dbus_message_iter_get_arg_type(&arrIter) == DBUS_TYPE_ARRAY)
+ {
+ DBusMessageIter structIter;
+ dbus_message_iter_recurse(&arrIter, &structIter);
+ if (dbus_message_iter_get_arg_type(&structIter) == DBUS_TYPE_STRUCT)
+ {
+ DBusMessageIter strIter;
+ dbus_message_iter_recurse(&structIter, &strIter);
+ if (dbus_message_iter_get_arg_type(&strIter) == DBUS_TYPE_STRING)
+ {
+ char *seat;
+ dbus_message_iter_get_basic(&strIter, &seat);
+ if (StringUtils::StartsWith(seat, "seat"))
+ {
+ CLog::Log(LOGDEBUG, "LogindUPowerSyscall::HasLogind - found seat: {}", seat);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+}
+
+bool CLogindUPowerSyscall::LogindSetPowerState(const char *state)
+{
+ CDBusMessage message(LOGIND_DEST, LOGIND_PATH, LOGIND_IFACE, state);
+ // The user_interaction boolean parameters can be used to control
+ // whether PolicyKit should interactively ask the user for authentication
+ // credentials if it needs to.
+ message.AppendArgument(false);
+ return message.SendSystem() != NULL;
+}
+
+bool CLogindUPowerSyscall::LogindCheckCapability(const char *capability)
+{
+ char *arg;
+ CDBusMessage message(LOGIND_DEST, LOGIND_PATH, LOGIND_IFACE, capability);
+ DBusMessage *reply = message.SendSystem();
+ if(reply && dbus_message_get_args(reply, NULL, DBUS_TYPE_STRING, &arg, DBUS_TYPE_INVALID))
+ {
+ // Returns one of "yes", "no" or "challenge". If "challenge" is
+ // returned the operation is available, but only after authorization.
+ return (strcmp(arg, "yes") == 0);
+ }
+ return false;
+}
+
+int CLogindUPowerSyscall::BatteryLevel()
+{
+ return m_batteryLevel;
+}
+
+void CLogindUPowerSyscall::UpdateBatteryLevel()
+{
+ char** source = NULL;
+ int length = 0;
+ double batteryLevelSum = 0;
+ int batteryCount = 0;
+
+ CDBusMessage message("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "EnumerateDevices");
+ DBusMessage *reply = message.SendSystem();
+
+ if (!reply)
+ return;
+
+ if (!dbus_message_get_args(reply, NULL, DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH, &source, &length, DBUS_TYPE_INVALID))
+ {
+ CLog::Log(LOGWARNING, "LogindUPowerSyscall: failed to enumerate devices");
+ return;
+ }
+
+ for (int i = 0; i < length; i++)
+ {
+ CVariant properties = CDBusUtil::GetAll("org.freedesktop.UPower", source[i], "org.freedesktop.UPower.Device");
+ bool isRechargeable = properties["IsRechargeable"].asBoolean();
+
+ if (isRechargeable)
+ {
+ batteryCount++;
+ batteryLevelSum += properties["Percentage"].asDouble();
+ }
+ }
+
+ dbus_free_string_array(source);
+
+ if (batteryCount > 0)
+ {
+ m_batteryLevel = (int)(batteryLevelSum / (double)batteryCount);
+ m_lowBattery = CDBusUtil::GetVariant("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "OnLowBattery").asBoolean();
+ }
+}
+
+bool CLogindUPowerSyscall::PumpPowerEvents(IPowerEventsCallback *callback)
+{
+ bool result = false;
+ bool releaseLockSleep = false;
+
+ if (m_connection)
+ {
+ dbus_connection_read_write(m_connection, 0);
+ DBusMessagePtr msg(dbus_connection_pop_message(m_connection));
+
+ if (msg)
+ {
+ if (dbus_message_is_signal(msg.get(), "org.freedesktop.login1.Manager", "PrepareForSleep"))
+ {
+ dbus_bool_t arg;
+ // the boolean argument defines whether we are going to sleep (true) or just woke up (false)
+ dbus_message_get_args(msg.get(), NULL, DBUS_TYPE_BOOLEAN, &arg, DBUS_TYPE_INVALID);
+ CLog::Log(LOGDEBUG, "LogindUPowerSyscall: Received PrepareForSleep with arg {}", (int)arg);
+ if (arg)
+ {
+ callback->OnSleep();
+ releaseLockSleep = true;
+ }
+ else
+ {
+ callback->OnWake();
+ InhibitDelayLockSleep();
+ }
+
+ result = true;
+ }
+ else if (dbus_message_is_signal(msg.get(), "org.freedesktop.UPower", "DeviceChanged"))
+ {
+ bool lowBattery = m_lowBattery;
+ UpdateBatteryLevel();
+ if (m_lowBattery && !lowBattery)
+ callback->OnLowBattery();
+
+ result = true;
+ }
+ else
+ CLog::Log(LOGDEBUG, "LogindUPowerSyscall - Received unknown signal {}",
+ dbus_message_get_member(msg.get()));
+ }
+ }
+
+ if (releaseLockSleep)
+ ReleaseDelayLockSleep();
+
+ return result;
+}
+
+void CLogindUPowerSyscall::InhibitDelayLockSleep()
+{
+ m_delayLockSleepFd = InhibitDelayLock("sleep");
+}
+
+void CLogindUPowerSyscall::InhibitDelayLockShutdown()
+{
+ m_delayLockShutdownFd = InhibitDelayLock("shutdown");
+}
+
+int CLogindUPowerSyscall::InhibitDelayLock(const char *what)
+{
+#ifdef DBUS_TYPE_UNIX_FD
+ CDBusMessage message("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "Inhibit");
+ message.AppendArgument(what); // what to inhibit
+ message.AppendArgument("XBMC"); // who
+ message.AppendArgument(""); // reason
+ message.AppendArgument("delay"); // mode
+
+ DBusMessage *reply = message.SendSystem();
+
+ if (!reply)
+ {
+ CLog::Log(LOGWARNING, "LogindUPowerSyscall - failed to inhibit {} delay lock", what);
+ return -1;
+ }
+
+ int delayLockFd;
+ if (!dbus_message_get_args(reply, NULL, DBUS_TYPE_UNIX_FD, &delayLockFd, DBUS_TYPE_INVALID))
+ {
+ CLog::Log(LOGWARNING, "LogindUPowerSyscall - failed to get inhibit file descriptor");
+ return -1;
+ }
+
+ CLog::Log(LOGDEBUG, "LogindUPowerSyscall - inhibit lock taken, fd {}", delayLockFd);
+ return delayLockFd;
+#else
+ CLog::Log(LOGWARNING, "LogindUPowerSyscall - inhibit lock support not compiled in");
+ return -1;
+#endif
+}
+
+void CLogindUPowerSyscall::ReleaseDelayLockSleep()
+{
+ ReleaseDelayLock(m_delayLockSleepFd, "sleep");
+ m_delayLockSleepFd = -1;
+}
+
+void CLogindUPowerSyscall::ReleaseDelayLockShutdown()
+{
+ ReleaseDelayLock(m_delayLockShutdownFd, "shutdown");
+ m_delayLockShutdownFd = -1;
+}
+
+void CLogindUPowerSyscall::ReleaseDelayLock(int lockFd, const char *what)
+{
+ if (lockFd != -1)
+ {
+ close(lockFd);
+ CLog::Log(LOGDEBUG, "LogindUPowerSyscall - delay lock {} released", what);
+ }
+}
diff --git a/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.h b/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.h
new file mode 100644
index 0000000..998e706
--- /dev/null
+++ b/xbmc/platform/linux/powermanagement/LogindUPowerSyscall.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2012 Denis Yantarev
+ * 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 "DBusUtil.h"
+#include "powermanagement/IPowerSyscall.h"
+
+class CLogindUPowerSyscall : public CAbstractPowerSyscall
+{
+public:
+ CLogindUPowerSyscall();
+ ~CLogindUPowerSyscall() override;
+ bool Powerdown() override;
+ bool Suspend() override;
+ bool Hibernate() override;
+ bool Reboot() override;
+ bool CanPowerdown() override;
+ bool CanSuspend() override;
+ bool CanHibernate() override;
+ bool CanReboot() override;
+ int BatteryLevel() override;
+ bool PumpPowerEvents(IPowerEventsCallback *callback) override;
+ // we don't require UPower because everything except the battery level works fine without it
+ static bool HasLogind();
+private:
+ CDBusConnection m_connection;
+ bool m_canPowerdown;
+ bool m_canSuspend;
+ bool m_canHibernate;
+ bool m_canReboot;
+ bool m_hasUPower;
+ bool m_lowBattery;
+ int m_batteryLevel;
+ int m_delayLockSleepFd = -1; // file descriptor for the logind sleep delay lock
+ int m_delayLockShutdownFd = -1; // file descriptor for the logind powerdown delay lock
+ void UpdateBatteryLevel();
+ void InhibitDelayLockSleep();
+ void InhibitDelayLockShutdown();
+ int InhibitDelayLock(const char *what);
+ void ReleaseDelayLockSleep();
+ void ReleaseDelayLockShutdown();
+ void ReleaseDelayLock(int lockFd, const char *what);
+ static bool LogindSetPowerState(const char *state);
+ static bool LogindCheckCapability(const char *capability);
+};
diff --git a/xbmc/platform/linux/powermanagement/UPowerSyscall.cpp b/xbmc/platform/linux/powermanagement/UPowerSyscall.cpp
new file mode 100644
index 0000000..27ed2f4
--- /dev/null
+++ b/xbmc/platform/linux/powermanagement/UPowerSyscall.cpp
@@ -0,0 +1,207 @@
+/*
+ * 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 "UPowerSyscall.h"
+
+#include "utils/log.h"
+
+CUPowerSource::CUPowerSource(const char *powerSource)
+{
+ if(powerSource == NULL)
+ m_powerSource = "";
+ else
+ m_powerSource = powerSource;
+
+ CVariant properties = CDBusUtil::GetAll("org.freedesktop.UPower", m_powerSource.c_str(), "org.freedesktop.UPower.Device");
+ m_isRechargeable = properties["IsRechargeable"].asBoolean();
+ Update();
+}
+
+CUPowerSource::~CUPowerSource() = default;
+
+void CUPowerSource::Update()
+{
+ CVariant properties = CDBusUtil::GetAll("org.freedesktop.UPower", m_powerSource.c_str(), "org.freedesktop.UPower.Device");
+ m_batteryLevel = properties["Percentage"].asDouble();
+}
+
+bool CUPowerSource::IsRechargeable()
+{
+ return m_isRechargeable;
+}
+
+double CUPowerSource::BatteryLevel()
+{
+ return m_batteryLevel;
+}
+
+CUPowerSyscall::CUPowerSyscall()
+{
+ CLog::Log(LOGINFO, "Selected UPower as PowerSyscall");
+
+ m_lowBattery = false;
+
+ //! @todo do not use dbus_connection_pop_message() that requires the use of a
+ //! private connection
+ if (m_connection.Connect(DBUS_BUS_SYSTEM, true))
+ {
+ dbus_connection_set_exit_on_disconnect(m_connection, false);
+
+ CDBusError error;
+ dbus_bus_add_match(m_connection, "type='signal',interface='org.freedesktop.UPower'", error);
+ dbus_connection_flush(m_connection);
+
+ if (error)
+ {
+ error.Log("UPower: Failed to attach to signal");
+ m_connection.Destroy();
+ }
+ }
+
+ m_CanPowerdown = false;
+ m_CanReboot = false;
+
+ UpdateCapabilities();
+
+ EnumeratePowerSources();
+}
+
+bool CUPowerSyscall::Powerdown()
+{
+ return false;
+}
+
+bool CUPowerSyscall::Suspend()
+{
+ // UPower 0.9.1 does not signal sleeping unless you tell that its about to sleep...
+ CDBusMessage aboutToSleepMessage("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "AboutToSleep");
+ aboutToSleepMessage.SendAsyncSystem();
+
+ CDBusMessage message("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "Suspend");
+ return message.SendAsyncSystem();
+}
+
+bool CUPowerSyscall::Hibernate()
+{
+ // UPower 0.9.1 does not signal sleeping unless you tell that its about to sleep...
+ CDBusMessage aboutToSleepMessage("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "AboutToSleep");
+ aboutToSleepMessage.SendAsyncSystem();
+
+ CDBusMessage message("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "Hibernate");
+ return message.SendAsyncSystem();
+}
+
+bool CUPowerSyscall::Reboot()
+{
+ return false;
+}
+
+bool CUPowerSyscall::CanPowerdown()
+{
+ return m_CanPowerdown;
+}
+
+bool CUPowerSyscall::CanSuspend()
+{
+ return m_CanSuspend;
+}
+
+bool CUPowerSyscall::CanHibernate()
+{
+ return m_CanHibernate;
+}
+
+bool CUPowerSyscall::CanReboot()
+{
+ return m_CanReboot;
+}
+
+int CUPowerSyscall::BatteryLevel()
+{
+ unsigned int nBatteryCount = 0;
+ double subCapacity = 0;
+ double batteryLevel = 0;
+
+ for (auto& itr : m_powerSources)
+ {
+ itr.Update();
+ if (itr.IsRechargeable())
+ {
+ nBatteryCount++;
+ subCapacity += itr.BatteryLevel();
+ }
+ }
+
+ if(nBatteryCount)
+ batteryLevel = subCapacity / (double)nBatteryCount;
+
+ return (int) batteryLevel;
+}
+
+void CUPowerSyscall::EnumeratePowerSources()
+{
+ CDBusMessage message("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "EnumerateDevices");
+ DBusMessage *reply = message.SendSystem();
+ if (reply)
+ {
+ char** source = NULL;
+ int length = 0;
+
+ if (dbus_message_get_args (reply, NULL, DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH, &source, &length, DBUS_TYPE_INVALID))
+ {
+ for (int i = 0; i < length; i++)
+ {
+ m_powerSources.emplace_back(source[i]);
+ }
+
+ dbus_free_string_array(source);
+ }
+ }
+}
+
+bool CUPowerSyscall::HasUPower()
+{
+ return CDBusUtil::TryMethodCall(DBUS_BUS_SYSTEM, "org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "EnumerateDevices");
+}
+
+bool CUPowerSyscall::PumpPowerEvents(IPowerEventsCallback *callback)
+{
+ bool result = false;
+
+ if (m_connection)
+ {
+ dbus_connection_read_write(m_connection, 0);
+ DBusMessagePtr msg(dbus_connection_pop_message(m_connection));
+
+ if (msg)
+ {
+ result = true;
+ if (dbus_message_is_signal(msg.get(), "org.freedesktop.UPower", "Sleeping"))
+ callback->OnSleep();
+ else if (dbus_message_is_signal(msg.get(), "org.freedesktop.UPower", "Resuming"))
+ callback->OnWake();
+ else if (dbus_message_is_signal(msg.get(), "org.freedesktop.UPower", "Changed"))
+ {
+ bool lowBattery = m_lowBattery;
+ UpdateCapabilities();
+ if (m_lowBattery && !lowBattery)
+ callback->OnLowBattery();
+ }
+ else
+ CLog::Log(LOGDEBUG, "UPower: Received an unknown signal {}",
+ dbus_message_get_member(msg.get()));
+ }
+ }
+ return result;
+}
+
+void CUPowerSyscall::UpdateCapabilities()
+{
+ m_CanSuspend = CDBusUtil::GetVariant("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "CanSuspend").asBoolean(false);
+ m_CanHibernate = CDBusUtil::GetVariant("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", "CanHibernate").asBoolean(false);
+}
diff --git a/xbmc/platform/linux/powermanagement/UPowerSyscall.h b/xbmc/platform/linux/powermanagement/UPowerSyscall.h
new file mode 100644
index 0000000..aed08d8
--- /dev/null
+++ b/xbmc/platform/linux/powermanagement/UPowerSyscall.h
@@ -0,0 +1,61 @@
+/*
+ * 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 "DBusUtil.h"
+#include "powermanagement/IPowerSyscall.h"
+
+#include <list>
+#include <string>
+
+class CUPowerSource
+{
+public:
+ CUPowerSource(const char *powerSource);
+ ~CUPowerSource();
+
+ void Update();
+ bool IsRechargeable();
+ double BatteryLevel();
+
+private:
+ std::string m_powerSource;
+ bool m_isRechargeable;
+ double m_batteryLevel;
+};
+
+class CUPowerSyscall : public CAbstractPowerSyscall
+{
+public:
+ CUPowerSyscall();
+ bool Powerdown() override;
+ bool Suspend() override;
+ bool Hibernate() override;
+ bool Reboot() override;
+ bool CanPowerdown() override;
+ bool CanSuspend() override;
+ bool CanHibernate() override;
+ bool CanReboot() override;
+ int BatteryLevel() override;
+ bool PumpPowerEvents(IPowerEventsCallback *callback) override;
+ static bool HasUPower();
+protected:
+ bool m_CanPowerdown;
+ bool m_CanSuspend;
+ bool m_CanHibernate;
+ bool m_CanReboot;
+
+ void UpdateCapabilities();
+private:
+ std::list<CUPowerSource> m_powerSources;
+ CDBusConnection m_connection;
+
+ bool m_lowBattery;
+ void EnumeratePowerSources();
+};
diff --git a/xbmc/platform/linux/sse4/CMakeLists.txt b/xbmc/platform/linux/sse4/CMakeLists.txt
new file mode 100644
index 0000000..6ec73f1
--- /dev/null
+++ b/xbmc/platform/linux/sse4/CMakeLists.txt
@@ -0,0 +1,8 @@
+if(HAVE_SSE4_1)
+ set(SOURCES CopyFrame.cpp)
+ set(HEADERS DllLibSSE4.h)
+
+ core_add_shared_library(sse4)
+ set_target_properties(sse4 PROPERTIES FOLDER lib)
+ target_compile_options(sse4 PRIVATE -msse4.1)
+endif()
diff --git a/xbmc/platform/linux/sse4/CopyFrame.cpp b/xbmc/platform/linux/sse4/CopyFrame.cpp
new file mode 100644
index 0000000..2147c3c
--- /dev/null
+++ b/xbmc/platform/linux/sse4/CopyFrame.cpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include <smmintrin.h>
+
+#define CACHED_BUFFER_SIZE 4096
+
+extern "C"
+{
+
+/*
+ * http://software.intel.com/en-us/articles/copying-accelerated-video-decode-frame-buffers
+ * COPIES VIDEO FRAMES FROM USWC MEMORY TO WB SYSTEM MEMORY VIA CACHED BUFFER
+ * ASSUMES PITCH IS A MULTIPLE OF 64B CACHE LINE SIZE, WIDTH MAY NOT BE
+ */
+void copy_frame( void * pSrc, void * pDest, void * pCacheBlock,
+ unsigned int width, unsigned int height, unsigned int pitch )
+{
+ __m128i x0, x1, x2, x3;
+ __m128i *pLoad;
+ __m128i *pStore;
+ __m128i *pCache;
+ unsigned int x, y, yLoad, yStore;
+ unsigned int rowsPerBlock;
+ unsigned int width64;
+ unsigned int extraPitch;
+
+
+ rowsPerBlock = CACHED_BUFFER_SIZE / pitch;
+ width64 = (width + 63) & ~0x03f;
+ extraPitch = (pitch - width64) / 16;
+
+ pLoad = (__m128i *)pSrc;
+ pStore = (__m128i *)pDest;
+
+ // COPY THROUGH 4KB CACHED BUFFER
+ for( y = 0; y < height; y += rowsPerBlock )
+ {
+ // ROWS LEFT TO COPY AT END
+ if( y + rowsPerBlock > height )
+ rowsPerBlock = height - y;
+
+ pCache = (__m128i *)pCacheBlock;
+
+ _mm_mfence();
+
+ // LOAD ROWS OF PITCH WIDTH INTO CACHED BLOCK
+ for( yLoad = 0; yLoad < rowsPerBlock; yLoad++ )
+ {
+ // COPY A ROW, CACHE LINE AT A TIME
+ for( x = 0; x < pitch; x +=64 )
+ {
+ x0 = _mm_stream_load_si128( pLoad +0 );
+ x1 = _mm_stream_load_si128( pLoad +1 );
+ x2 = _mm_stream_load_si128( pLoad +2 );
+ x3 = _mm_stream_load_si128( pLoad +3 );
+
+ _mm_store_si128( pCache +0, x0 );
+ _mm_store_si128( pCache +1, x1 );
+ _mm_store_si128( pCache +2, x2 );
+ _mm_store_si128( pCache +3, x3 );
+
+ pCache += 4;
+ pLoad += 4;
+ }
+ }
+
+ _mm_mfence();
+
+ pCache = (__m128i *)pCacheBlock;
+
+ // STORE ROWS OF FRAME WIDTH FROM CACHED BLOCK
+ for( yStore = 0; yStore < rowsPerBlock; yStore++ )
+ {
+ // copy a row, cache line at a time
+ for( x = 0; x < width64; x +=64 )
+ {
+ x0 = _mm_load_si128( pCache );
+ x1 = _mm_load_si128( pCache +1 );
+ x2 = _mm_load_si128( pCache +2 );
+ x3 = _mm_load_si128( pCache +3 );
+
+ _mm_stream_si128( pStore, x0 );
+ _mm_stream_si128( pStore +1, x1 );
+ _mm_stream_si128( pStore +2, x2 );
+ _mm_stream_si128( pStore +3, x3 );
+
+ pCache += 4;
+ pStore += 4;
+ }
+
+ pCache += extraPitch;
+ pStore += extraPitch;
+ }
+ }
+}
+}
diff --git a/xbmc/platform/linux/sse4/DllLibSSE4.h b/xbmc/platform/linux/sse4/DllLibSSE4.h
new file mode 100644
index 0000000..a89f713
--- /dev/null
+++ b/xbmc/platform/linux/sse4/DllLibSSE4.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "DynamicDll.h"
+
+extern "C" {
+
+}
+
+class DllLibSSE4Interface
+{
+public:
+ virtual ~DllLibSSE4Interface() = default;
+ virtual void copy_frame(void * pSrc, void * pDest, void * pCacheBlock, unsigned int width, unsigned int height, unsigned int pitch) = 0;
+};
+
+class DllLibSSE4 : public DllDynamic, DllLibSSE4Interface
+{
+ DECLARE_DLL_WRAPPER(DllLibSSE4, DLL_PATH_LIBSSE4)
+ DEFINE_METHOD6(void, copy_frame, (void *p1, void *p2, void *p3, unsigned int p4, unsigned int p5, unsigned int p6))
+
+ BEGIN_METHOD_RESOLVE()
+ RESOLVE_METHOD(copy_frame)
+ END_METHOD_RESOLVE()
+};
diff --git a/xbmc/platform/linux/storage/CMakeLists.txt b/xbmc/platform/linux/storage/CMakeLists.txt
new file mode 100644
index 0000000..223655a
--- /dev/null
+++ b/xbmc/platform/linux/storage/CMakeLists.txt
@@ -0,0 +1,19 @@
+set(SOURCES LinuxStorageProvider.cpp)
+
+set(HEADERS LinuxStorageProvider.h)
+
+if(DBUS_FOUND)
+ list(APPEND SOURCES UDisksProvider.cpp
+ UDisks2Provider.cpp)
+ list(APPEND HEADERS UDisksProvider.h
+ UDisks2Provider.h)
+endif()
+
+if(UDEV_FOUND)
+ list(APPEND SOURCES UDevProvider.cpp)
+ list(APPEND HEADERS UDevProvider.h)
+endif()
+
+if(SOURCES)
+ core_add_library(platform_linux_storage)
+endif()
diff --git a/xbmc/platform/linux/storage/LinuxStorageProvider.cpp b/xbmc/platform/linux/storage/LinuxStorageProvider.cpp
new file mode 100644
index 0000000..6206466
--- /dev/null
+++ b/xbmc/platform/linux/storage/LinuxStorageProvider.cpp
@@ -0,0 +1,91 @@
+/*
+ * 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 "LinuxStorageProvider.h"
+#include "guilib/LocalizeStrings.h"
+#include "UDevProvider.h"
+#ifdef HAS_DBUS
+#include "UDisksProvider.h"
+#include "UDisks2Provider.h"
+#endif
+#include "platform/posix/PosixMountProvider.h"
+
+std::unique_ptr<IStorageProvider> IStorageProvider::CreateInstance()
+{
+ return std::make_unique<CLinuxStorageProvider>();
+}
+
+CLinuxStorageProvider::CLinuxStorageProvider()
+{
+ m_instance = NULL;
+
+#ifdef HAS_DBUS
+ if (CUDisks2Provider::HasUDisks2())
+ m_instance = new CUDisks2Provider();
+ else if (CUDisksProvider::HasUDisks())
+ m_instance = new CUDisksProvider();
+#endif
+#ifdef HAVE_LIBUDEV
+ if (m_instance == NULL)
+ m_instance = new CUDevProvider();
+#endif
+
+ if (m_instance == NULL)
+ m_instance = new CPosixMountProvider();
+}
+
+CLinuxStorageProvider::~CLinuxStorageProvider()
+{
+ delete m_instance;
+}
+
+void CLinuxStorageProvider::Initialize()
+{
+ m_instance->Initialize();
+}
+
+void CLinuxStorageProvider::Stop()
+{
+ m_instance->Stop();
+}
+
+void CLinuxStorageProvider::GetLocalDrives(VECSOURCES &localDrives)
+{
+ // Home directory
+ CMediaSource share;
+ share.strPath = getenv("HOME");
+ share.strName = g_localizeStrings.Get(21440);
+ share.m_ignore = true;
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ localDrives.push_back(share);
+ share.strPath = "/";
+ share.strName = g_localizeStrings.Get(21453);
+ localDrives.push_back(share);
+
+ m_instance->GetLocalDrives(localDrives);
+}
+
+void CLinuxStorageProvider::GetRemovableDrives(VECSOURCES &removableDrives)
+{
+ m_instance->GetRemovableDrives(removableDrives);
+}
+
+bool CLinuxStorageProvider::Eject(const std::string& mountpath)
+{
+ return m_instance->Eject(mountpath);
+}
+
+std::vector<std::string> CLinuxStorageProvider::GetDiskUsage()
+{
+ return m_instance->GetDiskUsage();
+}
+
+bool CLinuxStorageProvider::PumpDriveChangeEvents(IStorageEventsCallback *callback)
+{
+ return m_instance->PumpDriveChangeEvents(callback);
+}
diff --git a/xbmc/platform/linux/storage/LinuxStorageProvider.h b/xbmc/platform/linux/storage/LinuxStorageProvider.h
new file mode 100644
index 0000000..e1c575f
--- /dev/null
+++ b/xbmc/platform/linux/storage/LinuxStorageProvider.h
@@ -0,0 +1,31 @@
+/*
+ * 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 "storage/IStorageProvider.h"
+
+#include <vector>
+
+class CLinuxStorageProvider : public IStorageProvider
+{
+public:
+ CLinuxStorageProvider();
+ ~CLinuxStorageProvider() override;
+
+ void Initialize() override;
+ void Stop() override;
+ void GetLocalDrives(VECSOURCES &localDrives) override;
+ void GetRemovableDrives(VECSOURCES &removableDrives) override;
+ bool Eject(const std::string& mountpath) override;
+ std::vector<std::string> GetDiskUsage() override;
+ bool PumpDriveChangeEvents(IStorageEventsCallback *callback) override;
+
+private:
+ IStorageProvider *m_instance;
+};
diff --git a/xbmc/platform/linux/storage/UDevProvider.cpp b/xbmc/platform/linux/storage/UDevProvider.cpp
new file mode 100644
index 0000000..7123e87
--- /dev/null
+++ b/xbmc/platform/linux/storage/UDevProvider.cpp
@@ -0,0 +1,298 @@
+/*
+ * 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 "UDevProvider.h"
+
+#include "platform/posix/PosixMountProvider.h"
+#include "utils/log.h"
+#include "utils/URIUtils.h"
+
+extern "C" {
+#include <libudev.h>
+#include <poll.h>
+}
+
+static const char *get_mountpoint(const char *devnode)
+{
+ static char buf[4096];
+ const char *delim = " ";
+ const char *mountpoint = nullptr;
+ FILE *fp = fopen("/proc/mounts", "r");
+ if (!fp)
+ return nullptr;
+
+ while (fgets(buf, sizeof (buf), fp))
+ {
+ const char *node = strtok(buf, delim);
+ if (strcmp(node, devnode) == 0)
+ {
+ mountpoint = strtok(nullptr, delim);
+ break;
+ }
+ }
+
+ if (mountpoint)
+ {
+ // If mount point contain characters like space, it is converted to
+ // "\040". This situation should be handled.
+ char *c1, *c2;
+ for (c1 = c2 = const_cast<char*>(mountpoint); *c2; ++c1)
+ {
+ if (*c2 == '\\')
+ {
+ *c1 = (((c2[1] - '0') << 6) | ((c2[2] - '0') << 3) | (c2[3] - '0'));
+ c2 += 4;
+ continue;
+ }
+ if (c1 != c2)
+ *c1 = *c2;
+ ++c2;
+ }
+ *c1 = *c2;
+ }
+
+ fclose(fp);
+ return mountpoint;
+}
+
+CUDevProvider::CUDevProvider()
+{
+ m_udev = nullptr;
+ m_udevMon = nullptr;
+}
+
+void CUDevProvider::Initialize()
+{
+ CLog::Log(LOGDEBUG, "Selected UDev as storage provider");
+
+ m_udev = udev_new();
+ if (!m_udev)
+ {
+ CLog::Log(LOGERROR, "{} - failed to allocate udev context", __FUNCTION__);
+ return;
+ }
+ /* set up a devices monitor that listen for any device change */
+ m_udevMon = udev_monitor_new_from_netlink(m_udev, "udev");
+ udev_monitor_filter_add_match_subsystem_devtype(m_udevMon, "block", "disk");
+ udev_monitor_filter_add_match_subsystem_devtype(m_udevMon, "block", "partition");
+ udev_monitor_enable_receiving(m_udevMon);
+
+ PumpDriveChangeEvents(nullptr);
+}
+
+void CUDevProvider::Stop()
+{
+ udev_monitor_unref(m_udevMon);
+ udev_unref(m_udev);
+}
+
+void CUDevProvider::GetDisks(VECSOURCES& disks, bool removable)
+{
+ // enumerate existing block devices
+ struct udev_enumerate *u_enum = udev_enumerate_new(m_udev);
+ if (!u_enum)
+ {
+ fprintf(stderr, "Error: udev_enumerate_new(udev)\n");
+ return;
+ }
+
+ udev_enumerate_add_match_subsystem(u_enum, "block");
+ udev_enumerate_add_match_property(u_enum, "DEVTYPE", "disk");
+ udev_enumerate_add_match_property(u_enum, "DEVTYPE", "partition");
+ udev_enumerate_scan_devices(u_enum);
+
+ struct udev_list_entry *u_list_ent;
+ struct udev_list_entry *u_first_list_ent;
+ u_first_list_ent = udev_enumerate_get_list_entry(u_enum);
+ udev_list_entry_foreach(u_list_ent, u_first_list_ent)
+ {
+ const char *name = udev_list_entry_get_name(u_list_ent);
+ struct udev *context = udev_enumerate_get_udev(u_enum);
+ struct udev_device *device = udev_device_new_from_syspath(context, name);
+ if (!device)
+ continue;
+
+ // filter out devices without devnode
+ const char* devnode = udev_device_get_devnode(device);
+ if (!devnode)
+ {
+ udev_device_unref(device);
+ continue;
+ }
+
+ // filter out devices that are not mounted
+ const char* mountpoint = get_mountpoint(devnode);
+ if (!mountpoint)
+ {
+ udev_device_unref(device);
+ continue;
+ }
+
+ // filter out root partition
+ if (strcmp(mountpoint, "/") == 0)
+ {
+ udev_device_unref(device);
+ continue;
+ }
+
+ // filter out things mounted on /tmp
+ if (strstr(mountpoint, "/tmp"))
+ {
+ udev_device_unref(device);
+ continue;
+ }
+
+ // look for devices on the usb bus, or mounted on */media/ (sdcards), or optical devices
+ const char *bus = udev_device_get_property_value(device, "ID_BUS");
+ const char *optical = udev_device_get_property_value(device, "ID_CDROM"); // matches also DVD, Blu-ray
+ bool isRemovable = ((bus && strstr(bus, "usb")) ||
+ (optical && strstr(optical,"1")) ||
+ (mountpoint && strstr(mountpoint, "/media/")));
+
+ // filter according to requested device type
+ if (removable != isRemovable)
+ {
+ udev_device_unref(device);
+ continue;
+ }
+
+ const char *udev_label = udev_device_get_property_value(device, "ID_FS_LABEL");
+ std::string label;
+ if (udev_label)
+ label = udev_label;
+ else
+ label = URIUtils::GetFileName(mountpoint);
+
+ CMediaSource share;
+ share.strName = label;
+ share.strPath = mountpoint;
+ share.m_ignore = true;
+ if (isRemovable)
+ {
+ if (optical)
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_DVD;
+ else
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_REMOVABLE;
+ }
+ else
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+
+ disks.push_back(share);
+ udev_device_unref(device);
+ }
+ udev_enumerate_unref(u_enum);
+}
+
+void CUDevProvider::GetLocalDrives(VECSOURCES &localDrives)
+{
+ GetDisks(localDrives, false);
+}
+
+void CUDevProvider::GetRemovableDrives(VECSOURCES &removableDrives)
+{
+ GetDisks(removableDrives, true);
+}
+
+bool CUDevProvider::Eject(const std::string& mountpath)
+{
+ // just go ahead and try to umount the disk
+ // if it does umount, life is good, if not, no loss.
+ std::string cmd = "umount \"" + mountpath + "\"";
+ int status = system(cmd.c_str());
+
+ if (status == 0)
+ return true;
+
+ return false;
+}
+
+std::vector<std::string> CUDevProvider::GetDiskUsage()
+{
+ CPosixMountProvider legacy;
+ return legacy.GetDiskUsage();
+}
+
+bool CUDevProvider::PumpDriveChangeEvents(IStorageEventsCallback *callback)
+{
+ bool changed = false;
+
+ fd_set readfds;
+ FD_ZERO(&readfds);
+ FD_SET(udev_monitor_get_fd(m_udevMon), &readfds);
+
+ // non-blocking, check the file descriptor for received data
+ struct timeval tv = {};
+ int count = select(udev_monitor_get_fd(m_udevMon) + 1, &readfds, nullptr, nullptr, &tv);
+ if (count < 0)
+ return false;
+
+ if (FD_ISSET(udev_monitor_get_fd(m_udevMon), &readfds))
+ {
+ struct udev_device* dev = udev_monitor_receive_device(m_udevMon);
+ if (!dev)
+ return false;
+
+ const char* action = udev_device_get_action(dev);
+ const char* devnode = udev_device_get_devnode(dev);
+ if (action && devnode)
+ {
+ MEDIA_DETECT::STORAGE::StorageDevice storageDevice;
+ const char *udev_label = udev_device_get_property_value(dev, "ID_FS_LABEL");
+ const char* mountpoint = get_mountpoint(devnode);
+ if (udev_label)
+ storageDevice.label = udev_label;
+ else if (mountpoint)
+ {
+ storageDevice.label = URIUtils::GetFileName(mountpoint);
+ storageDevice.path = mountpoint;
+ }
+
+ const char *fs_usage = udev_device_get_property_value(dev, "ID_FS_USAGE");
+ if (mountpoint && strcmp(action, "add") == 0 && (fs_usage && strcmp(fs_usage, "filesystem") == 0))
+ {
+ CLog::Log(LOGINFO, "UDev: Added {}", mountpoint);
+ if (callback)
+ callback->OnStorageAdded(storageDevice);
+ changed = true;
+ }
+ if (strcmp(action, "remove") == 0 && (fs_usage && strcmp(fs_usage, "filesystem") == 0))
+ {
+ if (callback)
+ callback->OnStorageSafelyRemoved(storageDevice);
+ changed = true;
+ }
+ // browse disk dialog is not wanted for blu-rays
+ const char *bd = udev_device_get_property_value(dev, "ID_CDROM_MEDIA_BD");
+ if (strcmp(action, "change") == 0 && !(bd && strcmp(bd, "1") == 0))
+ {
+ const char *optical = udev_device_get_property_value(dev, "ID_CDROM");
+ bool isOptical = optical && (strcmp(optical, "1") != 0);
+ storageDevice.type =
+ isOptical ? MEDIA_DETECT::STORAGE::Type::OPTICAL : MEDIA_DETECT::STORAGE::Type::UNKNOWN;
+
+ if (mountpoint && !isOptical)
+ {
+ CLog::Log(LOGINFO, "UDev: Changed / Added {}", mountpoint);
+ if (callback)
+ callback->OnStorageAdded(storageDevice);
+ changed = true;
+ }
+ const char *eject_request = udev_device_get_property_value(dev, "DISK_EJECT_REQUEST");
+ if (eject_request && strcmp(eject_request, "1") == 0)
+ {
+ if (callback)
+ callback->OnStorageSafelyRemoved(storageDevice);
+ changed = true;
+ }
+ }
+ }
+ udev_device_unref(dev);
+ }
+
+ return changed;
+}
diff --git a/xbmc/platform/linux/storage/UDevProvider.h b/xbmc/platform/linux/storage/UDevProvider.h
new file mode 100644
index 0000000..a4c83b3
--- /dev/null
+++ b/xbmc/platform/linux/storage/UDevProvider.h
@@ -0,0 +1,42 @@
+/*
+ * 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 "storage/IStorageProvider.h"
+
+#include <string>
+#include <vector>
+
+struct udev;
+struct udev_monitor;
+
+class CUDevProvider : public IStorageProvider
+{
+public:
+ CUDevProvider();
+ ~CUDevProvider() override = default;
+
+ void Initialize() override;
+ void Stop() override;
+
+ void GetLocalDrives(VECSOURCES &localDrives) override;
+ void GetRemovableDrives(VECSOURCES &removableDrives) override;
+
+ bool Eject(const std::string& mountpath) override;
+
+ std::vector<std::string> GetDiskUsage() override;
+
+ bool PumpDriveChangeEvents(IStorageEventsCallback *callback) override;
+
+private:
+ void GetDisks(VECSOURCES& devices, bool removable);
+
+ struct udev *m_udev;
+ struct udev_monitor *m_udevMon;
+};
diff --git a/xbmc/platform/linux/storage/UDisks2Provider.cpp b/xbmc/platform/linux/storage/UDisks2Provider.cpp
new file mode 100644
index 0000000..b3431c7
--- /dev/null
+++ b/xbmc/platform/linux/storage/UDisks2Provider.cpp
@@ -0,0 +1,861 @@
+/*
+ * 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 "UDisks2Provider.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include "platform/posix/PosixMountProvider.h"
+
+#include <algorithm>
+#include <functional>
+
+#define BOOL2SZ(b) ((b) ? "true" : "false")
+
+#define DBUS_INTERFACE_OBJECT_MANAGER "org.freedesktop.DBus.ObjectManager"
+
+#define UDISKS2_SERVICE_UDISKS2 "org.freedesktop.UDisks2"
+
+#define UDISKS2_PATH_MANAGER "/org/freedesktop/UDisks2/Manager"
+#define UDISKS2_PATH_UDISKS2 "/org/freedesktop/UDisks2"
+
+#define UDISKS2_INTERFACE_BLOCK "org.freedesktop.UDisks2.Block"
+#define UDISKS2_INTERFACE_DRIVE "org.freedesktop.UDisks2.Drive"
+#define UDISKS2_INTERFACE_FILESYSTEM "org.freedesktop.UDisks2.Filesystem"
+#define UDISKS2_INTERFACE_MANAGER "org.freedesktop.UDisks2.Manager"
+
+#define UDISKS2_MATCH_RULE "type='signal',sender='" UDISKS2_SERVICE_UDISKS2 "',path_namespace='" UDISKS2_PATH_UDISKS2 "'"
+
+CUDisks2Provider::Drive::Drive(const char *object) : m_object(object)
+{
+}
+
+bool CUDisks2Provider::Drive::IsOptical() const
+{
+ return std::any_of(m_mediaCompatibility.begin(), m_mediaCompatibility.end(),
+ [](const std::string& kind) { return kind.compare(0, 7, "optical") == 0; });
+}
+
+std::string CUDisks2Provider::Drive::ToString() const
+{
+ return StringUtils::Format("Drive {}: IsRemovable {} IsOptical {}", m_object,
+ BOOL2SZ(m_isRemovable), BOOL2SZ(IsOptical()));
+}
+
+CUDisks2Provider::Block::Block(const char *object) : m_object(object)
+{
+}
+
+bool CUDisks2Provider::Block::IsReady()
+{
+ return m_drive != nullptr;
+}
+
+std::string CUDisks2Provider::Block::ToString() const
+{
+ return StringUtils::Format("Block device {}: Device {} Label {} IsSystem: {} Drive {}", m_object,
+ m_device, m_label.empty() ? "none" : m_label, BOOL2SZ(m_isSystem),
+ m_driveobject.empty() ? "none" : m_driveobject);
+}
+
+CUDisks2Provider::Filesystem::Filesystem(const char *object) : m_object(object)
+{
+}
+
+std::string CUDisks2Provider::Filesystem::ToString() const
+{
+ return StringUtils::Format("Filesystem {}: IsMounted {} MountPoint {}", m_object,
+ BOOL2SZ(m_isMounted), m_mountPoint.empty() ? "none" : m_mountPoint);
+}
+
+MEDIA_DETECT::STORAGE::StorageDevice CUDisks2Provider::Filesystem::ToStorageDevice() const
+{
+ MEDIA_DETECT::STORAGE::StorageDevice device;
+ device.label = GetDisplayName();
+ device.path = GetMountPoint();
+ device.type = GetStorageType();
+ return device;
+}
+
+bool CUDisks2Provider::Filesystem::IsReady() const
+{
+ return m_block != nullptr && m_block->IsReady();
+}
+
+bool CUDisks2Provider::Filesystem::IsOptical() const
+{
+ return m_block->m_drive->IsOptical();
+}
+
+MEDIA_DETECT::STORAGE::Type CUDisks2Provider::Filesystem::GetStorageType() const
+{
+ if (m_block == nullptr || !m_block->IsReady())
+ return MEDIA_DETECT::STORAGE::Type::UNKNOWN;
+
+ if (IsOptical())
+ return MEDIA_DETECT::STORAGE::Type::OPTICAL;
+
+ return MEDIA_DETECT::STORAGE::Type::UNKNOWN;
+}
+
+std::string CUDisks2Provider::Filesystem::GetMountPoint() const
+{
+ return m_mountPoint;
+}
+
+std::string CUDisks2Provider::Filesystem::GetObject() const
+{
+ return m_object;
+}
+
+void CUDisks2Provider::Filesystem::ResetMountPoint()
+{
+ m_mountPoint.clear();
+ m_isMounted = false;
+}
+
+void CUDisks2Provider::Filesystem::SetMountPoint(const std::string& mountPoint)
+{
+ m_mountPoint = mountPoint;
+ m_isMounted = true;
+}
+
+bool CUDisks2Provider::Filesystem::IsMounted() const
+{
+ return m_isMounted;
+}
+
+bool CUDisks2Provider::Filesystem::Mount()
+{
+ if (m_block->m_isSystem) {
+ CLog::Log(LOGDEBUG, "UDisks2: Skip mount of system device {}", ToString());
+ return false;
+ }
+ else if (m_isMounted)
+ {
+ CLog::Log(LOGDEBUG, "UDisks2: Skip mount of already mounted device {}", ToString());
+ return false;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "UDisks2: Mounting {}", m_block->m_device);
+ CDBusMessage message(UDISKS2_SERVICE_UDISKS2, m_object, UDISKS2_INTERFACE_FILESYSTEM, "Mount");
+ AppendEmptyOptions(message.GetArgumentIter());
+ DBusMessage *reply = message.SendSystem();
+ return (reply && dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_ERROR);
+ }
+}
+
+bool CUDisks2Provider::Filesystem::Unmount()
+{
+ if (m_block->m_isSystem) {
+ CLog::Log(LOGDEBUG, "UDisks2: Skip unmount of system device {}", ToString());
+ return false;
+ }
+ else if (!m_isMounted)
+ {
+ CLog::Log(LOGDEBUG, "UDisks2: Skip unmount of not mounted device {}", ToString());
+ return false;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "UDisks2: Unmounting {}", m_block->m_device);
+ CDBusMessage message(UDISKS2_SERVICE_UDISKS2, m_object, UDISKS2_INTERFACE_FILESYSTEM, "Unmount");
+ AppendEmptyOptions(message.GetArgumentIter());
+ DBusMessage *reply = message.SendSystem();
+ return (reply && dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_ERROR);
+ }
+}
+
+std::string CUDisks2Provider::Filesystem::GetDisplayName() const
+{
+ if (m_block->m_label.empty())
+ {
+ std::string strSize = StringUtils::SizeToString(m_block->m_size);
+ return StringUtils::Format("{} {}", strSize, g_localizeStrings.Get(155));
+ }
+ else
+ return m_block->m_label;
+}
+
+CMediaSource CUDisks2Provider::Filesystem::ToMediaShare() const
+{
+ CMediaSource source;
+ source.strPath = m_mountPoint;
+ source.strName = GetDisplayName();
+ if (IsOptical())
+ source.m_iDriveType = CMediaSource::SOURCE_TYPE_DVD;
+ else if (m_block->m_isSystem)
+ source.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ else
+ source.m_iDriveType = CMediaSource::SOURCE_TYPE_REMOVABLE;
+ source.m_ignore = true;
+ return source;
+}
+
+bool CUDisks2Provider::Filesystem::IsApproved() const
+{
+ return IsReady() &&
+ (m_isMounted && m_mountPoint != "/" && m_mountPoint != "/boot" && m_mountPoint.compare(0, 6, "/boot/") != 0) /*||
+ m_block->m_drive->m_isOptical*/;
+}
+
+CUDisks2Provider::CUDisks2Provider()
+{
+ if (!m_connection.Connect(DBUS_BUS_SYSTEM, true))
+ {
+ return;
+ }
+
+ dbus_connection_set_exit_on_disconnect(m_connection, static_cast<dbus_bool_t>(false));
+
+ CDBusError error;
+ dbus_bus_add_match(m_connection, UDISKS2_MATCH_RULE, error);
+ dbus_connection_flush(m_connection);
+
+ if (error)
+ {
+ error.Log("UDisks2: Failed to attach to signal");
+ m_connection.Destroy();
+ }
+}
+
+CUDisks2Provider::~CUDisks2Provider()
+{
+ for (auto &elt : m_filesystems)
+ {
+ delete elt.second;
+ }
+ m_filesystems.clear();
+
+ for (auto &elt : m_blocks)
+ {
+ delete elt.second;
+ }
+ m_blocks.clear();
+
+ for (auto &elt : m_drives)
+ {
+ delete elt.second;
+ }
+ m_drives.clear();
+}
+
+void CUDisks2Provider::Initialize()
+{
+ CLog::Log(LOGDEBUG, "Selected UDisks2 as storage provider");
+ m_daemonVersion = CDBusUtil::GetVariant(UDISKS2_SERVICE_UDISKS2, UDISKS2_PATH_MANAGER, UDISKS2_INTERFACE_MANAGER,
+ "Version").asString();
+ CLog::Log(LOGDEBUG, "UDisks2: Daemon version {}", m_daemonVersion);
+
+ CLog::Log(LOGDEBUG, "UDisks2: Querying available devices");
+ CDBusMessage message(UDISKS2_SERVICE_UDISKS2, UDISKS2_PATH_UDISKS2, DBUS_INTERFACE_OBJECT_MANAGER,
+ "GetManagedObjects");
+ DBusMessage *reply = message.SendSystem();
+
+ if (reply && dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_ERROR)
+ {
+ HandleManagedObjects(reply);
+ }
+}
+
+bool CUDisks2Provider::PumpDriveChangeEvents(IStorageEventsCallback *callback)
+{
+ if (m_connection)
+ {
+ dbus_connection_read_write(m_connection, 0);
+ DBusMessagePtr msg(dbus_connection_pop_message(m_connection));
+
+ if (msg)
+ {
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Message received (interface: {}, member: {})",
+ dbus_message_get_interface(msg.get()), dbus_message_get_member(msg.get()));
+
+ if (dbus_message_is_signal(msg.get(), DBUS_INTERFACE_OBJECT_MANAGER, "InterfacesAdded"))
+ {
+ HandleInterfacesAdded(msg.get());
+ return false;
+ }
+ else if (dbus_message_is_signal(msg.get(), DBUS_INTERFACE_OBJECT_MANAGER, "InterfacesRemoved"))
+ {
+ return HandleInterfacesRemoved(msg.get(), callback);
+ }
+ else if (dbus_message_is_signal(msg.get(), DBUS_INTERFACE_PROPERTIES, "PropertiesChanged"))
+ {
+ return HandlePropertiesChanged(msg.get(), callback);
+ }
+ }
+ }
+ return false;
+}
+
+bool CUDisks2Provider::HasUDisks2()
+{
+ return CDBusUtil::TryMethodCall(DBUS_BUS_SYSTEM, UDISKS2_SERVICE_UDISKS2, UDISKS2_PATH_UDISKS2, DBUS_INTERFACE_PEER,
+ "Ping");
+}
+
+bool CUDisks2Provider::Eject(const std::string &mountpath)
+{
+ std::string path(mountpath);
+ URIUtils::RemoveSlashAtEnd(path);
+
+ for (const auto &elt: m_filesystems)
+ {
+ Filesystem *fs = elt.second;
+ if (fs->GetMountPoint() == path)
+ {
+ return fs->Unmount();
+ }
+ }
+
+ return false;
+}
+
+std::vector<std::string> CUDisks2Provider::GetDiskUsage()
+{
+ CPosixMountProvider legacy;
+ return legacy.GetDiskUsage();
+}
+
+void CUDisks2Provider::GetDisks(VECSOURCES &devices, bool enumerateRemovable)
+{
+ for (const auto &elt: m_filesystems)
+ {
+ Filesystem *fs = elt.second;
+ if (fs->IsApproved() && fs->m_block->m_isSystem != enumerateRemovable)
+ devices.push_back(fs->ToMediaShare());
+ }
+}
+
+void CUDisks2Provider::DriveAdded(Drive *drive)
+{
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Drive added - {}", drive->ToString());
+
+ if (m_drives[drive->m_object])
+ {
+ CLog::Log(LOGWARNING, "UDisks2: Inconsistency found! DriveAdded on an indexed drive");
+ delete m_drives[drive->m_object];
+ }
+
+ m_drives[drive->m_object] = drive;
+
+ for (auto &elt: m_blocks)
+ {
+ auto block = elt.second;
+ if (block->m_driveobject == drive->m_object)
+ {
+ block->m_drive = drive;
+ BlockAdded(block, false);
+ }
+ }
+}
+
+bool CUDisks2Provider::DriveRemoved(const std::string& object)
+{
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Drive removed ({})", object);
+
+ if (m_drives.count(object) > 0)
+ {
+ delete m_drives[object];
+ m_drives.erase(object);
+ }
+
+ for (auto &elt: m_blocks)
+ {
+ auto block = elt.second;
+ if (block->m_driveobject == object)
+ {
+ block->m_drive = nullptr;
+ }
+ }
+
+ return false;
+}
+
+void CUDisks2Provider::BlockAdded(Block *block, bool isNew)
+{
+ if (isNew)
+ {
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Block added - {}", block->ToString());
+
+ if (m_drives.count(block->m_driveobject) > 0)
+ block->m_drive = m_drives[block->m_driveobject];
+
+ if (m_blocks[block->m_object])
+ {
+ CLog::Log(LOGWARNING, "UDisks2: Inconsistency found! BlockAdded on an indexed block device");
+ delete m_blocks[block->m_object];
+ }
+
+ m_blocks[block->m_object] = block;
+ }
+
+
+ if (m_filesystems.count(block->m_object) > 0)
+ {
+ auto fs = m_filesystems[block->m_object];
+ fs->m_block = block;
+ FilesystemAdded(fs, false);
+ }
+}
+
+bool CUDisks2Provider::BlockRemoved(const std::string& object)
+{
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Block removed ({})", object);
+
+ if (m_blocks.count(object) > 0)
+ {
+ delete m_blocks[object];
+ m_blocks.erase(object);
+ }
+
+ if (m_filesystems.count(object) > 0)
+ {
+ m_filesystems[object]->m_block = nullptr;
+ }
+
+ return false;
+}
+
+void CUDisks2Provider::FilesystemAdded(Filesystem *fs, bool isNew)
+{
+ if (isNew)
+ {
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Filesystem added - {}", fs->ToString());
+
+ if (m_blocks.count(fs->GetObject()) > 0)
+ fs->m_block = m_blocks[fs->GetObject()];
+
+ if (m_filesystems[fs->GetObject()])
+ {
+ CLog::Log(LOGWARNING, "UDisks2: Inconsistency found! FilesystemAdded on an indexed filesystem");
+ delete m_filesystems[fs->GetObject()];
+ }
+
+ m_filesystems[fs->GetObject()] = fs;
+ }
+
+ if (fs->IsReady() && !fs->IsMounted())
+ {
+ // optical drives should be always mounted by default unless explicitly disabled by the user
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_handleMounting ||
+ (fs->IsOptical() &&
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_autoMountOpticalMedia))
+ {
+ fs->Mount();
+ }
+ }
+}
+
+bool CUDisks2Provider::FilesystemRemoved(const char *object, IStorageEventsCallback *callback)
+{
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Filesystem removed ({})", object);
+ bool result = false;
+ if (m_filesystems.count(object) > 0)
+ {
+ auto fs = m_filesystems[object];
+ if (fs->IsMounted())
+ {
+ callback->OnStorageUnsafelyRemoved(fs->ToStorageDevice());
+ result = true;
+ }
+ delete m_filesystems[object];
+ m_filesystems.erase(object);
+ }
+ return result;
+}
+
+void CUDisks2Provider::HandleManagedObjects(DBusMessage *msg)
+{
+ DBusMessageIter msgIter, dictIter;
+ dbus_message_iter_init(msg, &msgIter);
+ dbus_message_iter_recurse(&msgIter, &dictIter);
+ while (dbus_message_iter_get_arg_type(&dictIter) == DBUS_TYPE_DICT_ENTRY)
+ {
+ DBusMessageIter objIter;
+ dbus_message_iter_recurse(&dictIter, &objIter);
+ ParseInterfaces(&objIter);
+ dbus_message_iter_next(&dictIter);
+ }
+}
+
+void CUDisks2Provider::HandleInterfacesAdded(DBusMessage *msg)
+{
+ DBusMessageIter msgIter;
+ dbus_message_iter_init(msg, &msgIter);
+ ParseInterfaces(&msgIter);
+}
+
+bool CUDisks2Provider::HandleInterfacesRemoved(DBusMessage *msg, IStorageEventsCallback *callback)
+{
+ DBusMessageIter msgIter, ifaceIter;
+ const char *path, *iface;
+ bool result = false;
+ dbus_message_iter_init(msg, &msgIter);
+ dbus_message_iter_get_basic(&msgIter, &path);
+ dbus_message_iter_next(&msgIter);
+ dbus_message_iter_recurse(&msgIter, &ifaceIter);
+ while (dbus_message_iter_get_arg_type(&ifaceIter) == DBUS_TYPE_STRING)
+ {
+ dbus_message_iter_get_basic(&ifaceIter, &iface);
+ result |= RemoveInterface(path, iface, callback);
+ dbus_message_iter_next(&ifaceIter);
+ }
+ return result;
+}
+
+bool CUDisks2Provider::HandlePropertiesChanged(DBusMessage *msg, IStorageEventsCallback *callback)
+{
+ DBusMessageIter msgIter, propsIter;
+ const char *object = dbus_message_get_path(msg);
+ const char *iface;
+
+ dbus_message_iter_init(msg, &msgIter);
+ dbus_message_iter_get_basic(&msgIter, &iface);
+ dbus_message_iter_next(&msgIter);
+ dbus_message_iter_recurse(&msgIter, &propsIter);
+
+ if (strcmp(iface, UDISKS2_INTERFACE_DRIVE) == 0)
+ {
+ return DrivePropertiesChanged(object, &propsIter);
+ }
+ else if (strcmp(iface, UDISKS2_INTERFACE_BLOCK) == 0)
+ {
+ return BlockPropertiesChanged(object, &propsIter);
+ }
+ else if (strcmp(iface, UDISKS2_INTERFACE_FILESYSTEM) == 0)
+ {
+ return FilesystemPropertiesChanged(object, &propsIter, callback);
+ }
+
+ return false;
+}
+
+bool CUDisks2Provider::DrivePropertiesChanged(const char *object, DBusMessageIter *propsIter)
+{
+ if (m_drives.count(object) > 0)
+ {
+ auto drive = m_drives[object];
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Before update: {}", drive->ToString());
+ auto ParseDriveProperty = std::bind(&CUDisks2Provider::ParseDriveProperty, this, std::placeholders::_1,
+ std::placeholders::_2, std::placeholders::_3);
+ ParseProperties(drive, propsIter, ParseDriveProperty);
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: After update: {}", drive->ToString());
+ }
+ return false;
+}
+
+bool CUDisks2Provider::BlockPropertiesChanged(const char *object, DBusMessageIter *propsIter)
+{
+ if (m_blocks.count(object) > 0)
+ {
+ auto block = m_blocks[object];
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Before update: {}", block->ToString());
+ auto ParseBlockProperty = std::bind(&CUDisks2Provider::ParseBlockProperty, this, std::placeholders::_1,
+ std::placeholders::_2, std::placeholders::_3);
+ ParseProperties(block, propsIter, ParseBlockProperty);
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: After update: {}", block->ToString());
+ }
+ return false;
+}
+
+bool CUDisks2Provider::FilesystemPropertiesChanged(const char *object, DBusMessageIter *propsIter, IStorageEventsCallback *callback)
+{
+ if (m_filesystems.count(object) > 0)
+ {
+ auto fs = m_filesystems[object];
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: Before update: {}", fs->ToString());
+ bool wasMounted = fs->IsMounted();
+ auto ParseFilesystemProperty = std::bind(&CUDisks2Provider::ParseFilesystemProperty, this,
+ std::placeholders::_1,
+ std::placeholders::_2, std::placeholders::_3);
+ ParseProperties(fs, propsIter, ParseFilesystemProperty);
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks2: After update: {}", fs->ToString());
+
+ if (!wasMounted && fs->IsMounted() && fs->IsApproved())
+ {
+ CLog::Log(LOGINFO, "UDisks2: Added {}", fs->GetMountPoint());
+ if (callback)
+ callback->OnStorageAdded(fs->ToStorageDevice());
+ return true;
+ }
+ else if (wasMounted && !fs->IsMounted())
+ {
+ CLog::Log(LOGINFO, "UDisks2: Removed {}", fs->m_block->m_device);
+ if (callback)
+ callback->OnStorageSafelyRemoved(fs->ToStorageDevice());
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CUDisks2Provider::RemoveInterface(const char *path, const char *iface, IStorageEventsCallback *callback)
+{
+ if (strcmp(iface, UDISKS2_INTERFACE_DRIVE) == 0)
+ {
+ return DriveRemoved(path);
+ }
+ else if (strcmp(iface, UDISKS2_INTERFACE_BLOCK) == 0)
+ {
+ return BlockRemoved(path);
+ }
+ else if (strcmp(iface, UDISKS2_INTERFACE_FILESYSTEM) == 0)
+ {
+ return FilesystemRemoved(path, callback);
+ }
+ else
+ {
+ return false;
+ }
+}
+
+
+void CUDisks2Provider::ParseInterfaces(DBusMessageIter *objIter)
+{
+ DBusMessageIter dictIter;
+ const char *object;
+ dbus_message_iter_get_basic(objIter, &object);
+ dbus_message_iter_next(objIter);
+ dbus_message_iter_recurse(objIter, &dictIter);
+ while (dbus_message_iter_get_arg_type(&dictIter) == DBUS_TYPE_DICT_ENTRY)
+ {
+ DBusMessageIter ifaceIter, propsIter;
+ const char *iface;
+ dbus_message_iter_recurse(&dictIter, &ifaceIter);
+ dbus_message_iter_get_basic(&ifaceIter, &iface);
+ dbus_message_iter_next(&ifaceIter);
+ dbus_message_iter_recurse(&ifaceIter, &propsIter);
+ ParseInterface(object, iface, &propsIter/*, &discovery*/);
+ dbus_message_iter_next(&dictIter);
+ }
+}
+
+void CUDisks2Provider::ParseInterface(const char *object, const char *iface, DBusMessageIter *propsIter)
+{
+ if (strcmp(iface, UDISKS2_INTERFACE_DRIVE) == 0)
+ {
+ auto *drive = new Drive(object);
+ auto f = std::bind(&CUDisks2Provider::ParseDriveProperty, this, std::placeholders::_1,
+ std::placeholders::_2, std::placeholders::_3);
+ ParseProperties(drive, propsIter, f);
+ DriveAdded(drive);
+ }
+ else if (strcmp(iface, UDISKS2_INTERFACE_BLOCK) == 0)
+ {
+ auto *block = new Block(object);
+ auto f = std::bind(&CUDisks2Provider::ParseBlockProperty, this, std::placeholders::_1,
+ std::placeholders::_2, std::placeholders::_3);
+ ParseProperties(block, propsIter, f);
+ BlockAdded(block);
+ }
+ else if (strcmp(iface, UDISKS2_INTERFACE_FILESYSTEM) == 0)
+ {
+ auto *fs = new Filesystem(object);
+ auto f = std::bind(&CUDisks2Provider::ParseFilesystemProperty, this, std::placeholders::_1,
+ std::placeholders::_2, std::placeholders::_3);
+ ParseProperties(fs, propsIter, f);
+ FilesystemAdded(fs);
+ }
+}
+
+
+template<class Object, class Function>
+void CUDisks2Provider::ParseProperties(Object *ref, DBusMessageIter *propsIter, Function f)
+{
+ while (dbus_message_iter_get_arg_type(propsIter) == DBUS_TYPE_DICT_ENTRY)
+ {
+ DBusMessageIter propIter, varIter;
+ const char *key;
+
+ dbus_message_iter_recurse(propsIter, &propIter);
+
+ dbus_message_iter_get_basic(&propIter, &key);
+ dbus_message_iter_next(&propIter);
+
+ dbus_message_iter_recurse(&propIter, &varIter);
+
+ f(ref, key, &varIter);
+
+ dbus_message_iter_next(propsIter);
+ }
+
+}
+
+void CUDisks2Provider::ParseDriveProperty(Drive *drive, const char *key, DBusMessageIter *varIter)
+{
+ switch (dbus_message_iter_get_arg_type(varIter))
+ {
+ case DBUS_TYPE_BOOLEAN:
+ {
+ dbus_bool_t value;
+
+ if (strcmp(key, "Removable") == 0)
+ {
+ dbus_message_iter_get_basic(varIter, &value);
+ drive->m_isRemovable = static_cast<bool>(value);
+ }
+
+ break;
+ }
+ case DBUS_TYPE_ARRAY:
+ {
+ DBusMessageIter arrIter;
+
+ if (strcmp(key, "MediaCompatibility") == 0)
+ {
+ dbus_message_iter_recurse(varIter, &arrIter);
+ while (dbus_message_iter_get_arg_type(&arrIter) == DBUS_TYPE_STRING)
+ {
+ const char *compatibility;
+ dbus_message_iter_get_basic(&arrIter, &compatibility);
+ drive->m_mediaCompatibility.emplace_back(compatibility);
+ dbus_message_iter_next(&arrIter);
+ }
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+
+void CUDisks2Provider::ParseBlockProperty(Block *block, const char *key, DBusMessageIter *varIter)
+{
+ switch (dbus_message_iter_get_arg_type(varIter))
+ {
+ case DBUS_TYPE_OBJECT_PATH:
+ {
+ const char *value;
+
+ if (strcmp(key, "Drive") == 0)
+ {
+ dbus_message_iter_get_basic(varIter, &value);
+ block->m_driveobject.assign(value);
+ }
+
+ break;
+ }
+ case DBUS_TYPE_STRING:
+ {
+ const char *value;
+
+ if (strcmp(key, "IdLabel") == 0)
+ {
+ dbus_message_iter_get_basic(varIter, &value);
+ block->m_label.assign(value);
+ }
+
+ break;
+ }
+ case DBUS_TYPE_BOOLEAN:
+ {
+ dbus_bool_t value;
+
+ if (strcmp(key, "HintSystem") == 0)
+ {
+ dbus_message_iter_get_basic(varIter, &value);
+ block->m_isSystem = static_cast<bool>(value);
+ }
+
+ break;
+ }
+ case DBUS_TYPE_UINT64:
+ {
+ dbus_uint64_t value;
+
+ if (strcmp(key, "Size") == 0)
+ {
+ dbus_message_iter_get_basic(varIter, &value);
+ block->m_size = value;
+ }
+
+ break;
+ }
+ case DBUS_TYPE_ARRAY:
+ {
+ DBusMessageIter arrIter;
+
+ if (strcmp(key, "PreferredDevice") == 0)
+ {
+ dbus_message_iter_recurse(varIter, &arrIter);
+ block->m_device.assign(ParseByteArray(&arrIter));
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void CUDisks2Provider::ParseFilesystemProperty(Filesystem *fs, const char *key, DBusMessageIter *varIter)
+{
+ switch (dbus_message_iter_get_arg_type(varIter))
+ {
+ case DBUS_TYPE_ARRAY:
+ {
+ DBusMessageIter arrIter;
+
+ if (strcmp(key, "MountPoints") == 0)
+ {
+ dbus_message_iter_recurse(varIter, &arrIter);
+
+ if (dbus_message_iter_get_arg_type(&arrIter) == DBUS_TYPE_ARRAY)
+ {
+ DBusMessageIter valIter;
+
+ dbus_message_iter_recurse(&arrIter, &valIter);
+ fs->SetMountPoint(ParseByteArray(&valIter));
+
+ dbus_message_iter_next(&arrIter);
+ }
+ else
+ {
+ fs->ResetMountPoint();
+ }
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+std::string CUDisks2Provider::ParseByteArray(DBusMessageIter *arrIter)
+{
+ std::ostringstream strStream;
+
+ while (dbus_message_iter_get_arg_type(arrIter) == DBUS_TYPE_BYTE)
+ {
+ dbus_int16_t value = 0;
+ dbus_message_iter_get_basic(arrIter, &value);
+ if (value == 0)
+ break;
+ strStream << static_cast<char>(value);
+ dbus_message_iter_next(arrIter);
+ }
+
+ return strStream.str();
+}
+
+void CUDisks2Provider::AppendEmptyOptions(DBusMessageIter *argsIter)
+{
+ DBusMessageIter dictIter;
+ dbus_message_iter_open_container(argsIter, DBUS_TYPE_ARRAY, "{sv}", &dictIter);
+ dbus_message_iter_close_container(argsIter, &dictIter);
+}
diff --git a/xbmc/platform/linux/storage/UDisks2Provider.h b/xbmc/platform/linux/storage/UDisks2Provider.h
new file mode 100644
index 0000000..b4c85ee
--- /dev/null
+++ b/xbmc/platform/linux/storage/UDisks2Provider.h
@@ -0,0 +1,210 @@
+/*
+ * 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 "DBusUtil.h"
+#include "storage/IStorageProvider.h"
+
+#include <string>
+#include <vector>
+
+class CUDisks2Provider : public IStorageProvider
+{
+ class Drive
+ {
+ public:
+ std::string m_object;
+ bool m_isRemovable = false;
+ std::vector<std::string> m_mediaCompatibility;
+
+ explicit Drive(const char *object);
+ ~Drive() = default;
+
+ /*! \brief Check if the drive is optical
+ * @return true if the drive is optical, false otherwise
+ */
+ bool IsOptical() const;
+
+ /*! \brief Get a representation of the drive as a readable string
+ * @return drive as a string
+ */
+ std::string ToString() const;
+ };
+
+ class Block
+ {
+ public:
+ Drive *m_drive = nullptr;
+ std::string m_object;
+ std::string m_driveobject;
+ std::string m_label;
+ std::string m_device;
+ bool m_isSystem = false;
+ u_int64_t m_size = 0;
+
+ explicit Block(const char *object);
+ ~Block() = default;
+
+ bool IsReady();
+
+ /*! \brief Get a representation of the block as a readable string
+ * @return block as a string
+ */
+ std::string ToString() const;
+ };
+
+ class Filesystem
+ {
+ public:
+ Block *m_block = nullptr;
+
+ explicit Filesystem(const char *object);
+ ~Filesystem() = default;
+
+ bool Mount();
+ bool Unmount();
+
+ /*! \brief Get the device display name/label
+ * @return the device display name/label
+ */
+ std::string GetDisplayName() const;
+
+ /*! \brief Check if the device is approved/whitelisted
+ * @return true if the device is approved/whitelisted, false otherwise
+ */
+ bool IsApproved() const;
+
+ /*! \brief Check if the device is mounted
+ * @return true if the device is mounted, false otherwise
+ */
+ bool IsMounted() const;
+
+ /*! \brief Check if the device is ready
+ * @return true if the device is ready, false otherwise
+ */
+ bool IsReady() const;
+
+ /*! \brief Check if the device is optical
+ * @return true if the device is optical, false otherwise
+ */
+ bool IsOptical() const;
+
+ /*! \brief Get the storage type of this device
+ * @return the storage type (e.g. OPTICAL) or UNKNOWN if
+ * the type couldn't be detected
+ */
+ MEDIA_DETECT::STORAGE::Type GetStorageType() const;
+
+ /*! \brief Get the device mount point
+ * @return the device mount point
+ */
+ std::string GetMountPoint() const;
+
+ /*! \brief Reset the device mount point
+ */
+ void ResetMountPoint();
+
+ /*! \brief Set the device mount point
+ * @param mountPoint the device mount point
+ */
+ void SetMountPoint(const std::string& mountPoint);
+
+ /*! \brief Get the device dbus object
+ * @return the device dbus object
+ */
+ std::string GetObject() const;
+
+ /*! \brief Get a representation of the device as a readable string
+ * @return device as a string
+ */
+ std::string ToString() const;
+
+ /*! \brief Get a representation of the device as a media share
+ * @return the media share
+ */
+ CMediaSource ToMediaShare() const;
+
+ /*! \brief Get a representation of the device as a storage device abstraction
+ * @return the storage device abstraction of the device
+ */
+ MEDIA_DETECT::STORAGE::StorageDevice ToStorageDevice() const;
+
+ private:
+ bool m_isMounted = false;
+ std::string m_object;
+ std::string m_mountPoint;
+ };
+
+ typedef std::map<std::string, Drive *> DriveMap;
+ typedef std::map<std::string, Block *> BlockMap;
+ typedef std::map<std::string, Filesystem *> FilesystemMap;
+
+public:
+ CUDisks2Provider();
+ ~CUDisks2Provider() override;
+
+ void Initialize() override;
+
+ bool PumpDriveChangeEvents(IStorageEventsCallback *callback) override;
+
+ static bool HasUDisks2();
+
+ bool Eject(const std::string &mountpath) override;
+
+ std::vector<std::string> GetDiskUsage() override;
+
+ void GetLocalDrives(VECSOURCES &localDrives) override
+ { GetDisks(localDrives, false); }
+
+ void GetRemovableDrives(VECSOURCES &removableDrives) override
+ { GetDisks(removableDrives, true); }
+
+ void Stop() override
+ {}
+
+private:
+ CDBusConnection m_connection;
+
+ DriveMap m_drives;
+ BlockMap m_blocks;
+ FilesystemMap m_filesystems;
+
+ std::string m_daemonVersion;
+
+ void GetDisks(VECSOURCES &devices, bool enumerateRemovable);
+
+ void DriveAdded(Drive *drive);
+ bool DriveRemoved(const std::string& object);
+ void BlockAdded(Block *block, bool isNew = true);
+ bool BlockRemoved(const std::string& object);
+ void FilesystemAdded(Filesystem *fs, bool isNew = true);
+ bool FilesystemRemoved(const char *object, IStorageEventsCallback *callback);
+
+ bool HandleInterfacesRemoved(DBusMessage *msg, IStorageEventsCallback *callback);
+ void HandleInterfacesAdded(DBusMessage *msg);
+ bool HandlePropertiesChanged(DBusMessage *msg, IStorageEventsCallback *callback);
+
+ bool DrivePropertiesChanged(const char *object, DBusMessageIter *propsIter);
+ bool BlockPropertiesChanged(const char *object, DBusMessageIter *propsIter);
+ bool FilesystemPropertiesChanged(const char *object, DBusMessageIter *propsIter, IStorageEventsCallback *callback);
+
+ bool RemoveInterface(const char *path, const char *iface, IStorageEventsCallback *callback);
+
+ template<class Object, class Function>
+ void ParseProperties(Object *ref, DBusMessageIter *dictIter, Function f);
+ void ParseInterfaces(DBusMessageIter *dictIter);
+ void ParseDriveProperty(Drive *drive, const char *key, DBusMessageIter *varIter);
+ void ParseBlockProperty(Block *block, const char *key, DBusMessageIter *varIter);
+ void ParseFilesystemProperty(Filesystem *fs, const char *key, DBusMessageIter *varIter);
+ std::string ParseByteArray(DBusMessageIter *arrIter);
+ void HandleManagedObjects(DBusMessage *msg);
+ void ParseInterface(const char *object, const char *iface, DBusMessageIter *propsIter);
+
+ static void AppendEmptyOptions(DBusMessageIter *argsIter);
+};
diff --git a/xbmc/platform/linux/storage/UDisksProvider.cpp b/xbmc/platform/linux/storage/UDisksProvider.cpp
new file mode 100644
index 0000000..7a2a034
--- /dev/null
+++ b/xbmc/platform/linux/storage/UDisksProvider.cpp
@@ -0,0 +1,412 @@
+/*
+ * 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 "UDisksProvider.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include "platform/posix/PosixMountProvider.h"
+
+CUDiskDevice::CUDiskDevice(const char *DeviceKitUDI):
+ m_DeviceKitUDI(DeviceKitUDI)
+{
+ m_isMounted = false;
+ m_isMountedByUs = false;
+ m_isRemovable = false;
+ m_isPartition = false;
+ m_isFileSystem = false;
+ m_isSystemInternal = false;
+ m_isOptical = false;
+ m_PartitionSize = 0;
+ Update();
+}
+
+void CUDiskDevice::Update()
+{
+ CVariant properties = CDBusUtil::GetAll("org.freedesktop.UDisks", m_DeviceKitUDI.c_str(), "org.freedesktop.UDisks.Device");
+
+ m_isFileSystem = properties["IdUsage"].asString() == "filesystem";
+ if (m_isFileSystem)
+ {
+ m_UDI = properties["IdUuid"].asString();
+ m_Label = properties["IdLabel"].asString();
+ m_FileSystem = properties["IdType"].asString();
+ }
+ else
+ {
+ m_UDI.clear();
+ m_Label.clear();
+ m_FileSystem.clear();
+ }
+
+ m_isMounted = properties["DeviceIsMounted"].asBoolean();
+ if (m_isMounted && !properties["DeviceMountPaths"].empty())
+ m_MountPath = properties["DeviceMountPaths"][0].asString();
+ else
+ m_MountPath.clear();
+
+ m_PartitionSize = properties["PartitionSize"].asUnsignedInteger();
+ m_isPartition = properties["DeviceIsPartition"].asBoolean();
+ m_isSystemInternal = properties["DeviceIsSystemInternal"].asBoolean();
+ m_isOptical = properties["DeviceIsOpticalDisc"].asBoolean();
+ if (m_isPartition)
+ {
+ CVariant isRemovable = CDBusUtil::GetVariant("org.freedesktop.UDisks", properties["PartitionSlave"].asString().c_str(), "org.freedesktop.UDisks.Device", "DeviceIsRemovable");
+
+ if ( !isRemovable.isNull() )
+ m_isRemovable = isRemovable.asBoolean();
+ else
+ m_isRemovable = false;
+ }
+ else
+ m_isRemovable = properties["DeviceIsRemovable"].asBoolean();
+}
+
+bool CUDiskDevice::Mount()
+{
+ if (!m_isMounted && !m_isSystemInternal && m_isFileSystem)
+ {
+ CLog::Log(LOGDEBUG, "UDisks: Mounting {}", m_DeviceKitUDI);
+ CDBusMessage message("org.freedesktop.UDisks", m_DeviceKitUDI.c_str(), "org.freedesktop.UDisks.Device", "FilesystemMount");
+ message.AppendArgument("");
+ const char *array[] = {};
+ message.AppendArgument(array, 0);
+
+ DBusMessage *reply = message.SendSystem();
+ if (reply)
+ {
+ char *mountPoint;
+ if (dbus_message_get_args (reply, NULL, DBUS_TYPE_STRING, &mountPoint, DBUS_TYPE_INVALID))
+ {
+ m_MountPath = mountPoint;
+ CLog::Log(LOGDEBUG, "UDisks: Successfully mounted {} on {}", m_DeviceKitUDI, mountPoint);
+ m_isMountedByUs = m_isMounted = true;
+ }
+ }
+
+ return m_isMounted;
+ }
+ else
+ CLog::Log(LOGDEBUG, "UDisks: Is not able to mount {}", ToString());
+
+ return false;
+}
+
+bool CUDiskDevice::UnMount()
+{
+ if (m_isMounted && !m_isSystemInternal && m_isFileSystem)
+ {
+ CDBusMessage message("org.freedesktop.UDisks", m_DeviceKitUDI.c_str(), "org.freedesktop.UDisks.Device", "FilesystemUnmount");
+
+ const char *array[1];
+ message.AppendArgument(array, 0);
+
+ DBusMessage *reply = message.SendSystem();
+ if (reply)
+ m_isMountedByUs = m_isMounted = false;
+
+ return !m_isMounted;
+ }
+ else
+ CLog::Log(LOGDEBUG, "UDisks: Is not able to unmount {}", ToString());
+
+ return false;
+}
+
+CMediaSource CUDiskDevice::ToMediaShare() const
+{
+ CMediaSource source;
+ source.strPath = m_MountPath;
+ if (m_Label.empty())
+ {
+ std::string strSize = StringUtils::SizeToString(m_PartitionSize);
+ source.strName = StringUtils::Format("{} {}", strSize, g_localizeStrings.Get(155));
+ }
+ else
+ source.strName = m_Label;
+ if (m_isOptical)
+ source.m_iDriveType = CMediaSource::SOURCE_TYPE_DVD;
+ else if (m_isSystemInternal)
+ source.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ else
+ source.m_iDriveType = CMediaSource::SOURCE_TYPE_REMOVABLE;
+ source.m_ignore = true;
+ return source;
+}
+
+bool CUDiskDevice::IsApproved() const
+{
+ return (m_isFileSystem && m_isMounted && m_UDI.length() > 0 && (m_FileSystem.length() > 0 && m_FileSystem != "swap")
+ && m_MountPath != "/" && m_MountPath != "/boot") || m_isOptical;
+}
+
+bool CUDiskDevice::IsOptical() const
+{
+ return m_isOptical;
+}
+
+MEDIA_DETECT::STORAGE::Type CUDiskDevice::GetStorageType() const
+{
+ if (IsOptical())
+ return MEDIA_DETECT::STORAGE::Type::OPTICAL;
+
+ return MEDIA_DETECT::STORAGE::Type::UNKNOWN;
+}
+
+bool CUDiskDevice::IsMounted() const
+{
+ return m_isMounted;
+}
+
+std::string CUDiskDevice::GetDisplayName() const
+{
+ return m_Label;
+}
+
+std::string CUDiskDevice::GetMountPoint() const
+{
+ return m_MountPath;
+}
+
+bool CUDiskDevice::IsSystemInternal() const
+{
+ return m_isSystemInternal;
+}
+
+MEDIA_DETECT::STORAGE::StorageDevice CUDiskDevice::ToStorageDevice() const
+{
+ MEDIA_DETECT::STORAGE::StorageDevice device;
+ device.label = GetDisplayName();
+ device.path = GetMountPoint();
+ device.type = GetStorageType();
+ return device;
+}
+
+#define BOOL2SZ(b) ((b) ? "true" : "false")
+
+std::string CUDiskDevice::ToString() const
+{
+ return StringUtils::Format("DeviceUDI {}: IsFileSystem {} HasFileSystem {} "
+ "IsSystemInternal {} IsMounted {} IsRemovable {} IsPartition {} "
+ "IsOptical {}",
+ m_DeviceKitUDI, BOOL2SZ(m_isFileSystem), m_FileSystem,
+ BOOL2SZ(m_isSystemInternal), BOOL2SZ(m_isMounted),
+ BOOL2SZ(m_isRemovable), BOOL2SZ(m_isPartition), BOOL2SZ(m_isOptical));
+}
+
+CUDisksProvider::CUDisksProvider()
+{
+ //! @todo do not use dbus_connection_pop_message() that requires the use of a
+ //! private connection
+ if (!m_connection.Connect(DBUS_BUS_SYSTEM, true))
+ {
+ return;
+ }
+
+ dbus_connection_set_exit_on_disconnect(m_connection, false);
+
+ CDBusError error;
+ dbus_bus_add_match(m_connection, "type='signal',interface='org.freedesktop.UDisks'", error);
+ dbus_connection_flush(m_connection);
+
+ if (error)
+ {
+ error.Log("UDisks: Failed to attach to signal");
+ m_connection.Destroy();
+ }
+}
+
+CUDisksProvider::~CUDisksProvider()
+{
+ for (auto& itr : m_AvailableDevices)
+ delete itr.second;
+
+ m_AvailableDevices.clear();
+}
+
+void CUDisksProvider::Initialize()
+{
+ CLog::Log(LOGDEBUG, "Selected UDisks as storage provider");
+ m_DaemonVersion = atoi(CDBusUtil::GetVariant("org.freedesktop.UDisks", "/org/freedesktop/UDisks", "org.freedesktop.UDisks", "DaemonVersion").asString().c_str());
+ CLog::Log(LOGDEBUG, "UDisks: DaemonVersion {}", m_DaemonVersion);
+
+ CLog::Log(LOGDEBUG, "UDisks: Querying available devices");
+ std::vector<std::string> devices = EnumerateDisks();
+ for (unsigned int i = 0; i < devices.size(); i++)
+ DeviceAdded(devices[i].c_str(), NULL);
+}
+
+bool CUDisksProvider::Eject(const std::string& mountpath)
+{
+ std::string path(mountpath);
+ URIUtils::RemoveSlashAtEnd(path);
+
+ for (auto& itr : m_AvailableDevices)
+ {
+ CUDiskDevice* device = itr.second;
+ if (device->GetMountPoint() == path)
+ return device->UnMount();
+ }
+
+ return false;
+}
+
+std::vector<std::string> CUDisksProvider::GetDiskUsage()
+{
+ CPosixMountProvider legacy;
+ return legacy.GetDiskUsage();
+}
+
+bool CUDisksProvider::PumpDriveChangeEvents(IStorageEventsCallback *callback)
+{
+ bool result = false;
+ if (m_connection)
+ {
+ dbus_connection_read_write(m_connection, 0);
+ DBusMessagePtr msg(dbus_connection_pop_message(m_connection));
+
+ if (msg)
+ {
+ char *object;
+ if (dbus_message_get_args (msg.get(), NULL, DBUS_TYPE_OBJECT_PATH, &object, DBUS_TYPE_INVALID))
+ {
+ result = true;
+ if (dbus_message_is_signal(msg.get(), "org.freedesktop.UDisks", "DeviceAdded"))
+ DeviceAdded(object, callback);
+ else if (dbus_message_is_signal(msg.get(), "org.freedesktop.UDisks", "DeviceRemoved"))
+ DeviceRemoved(object, callback);
+ else if (dbus_message_is_signal(msg.get(), "org.freedesktop.UDisks", "DeviceChanged"))
+ DeviceChanged(object, callback);
+ }
+ }
+ }
+ return result;
+}
+
+bool CUDisksProvider::HasUDisks()
+{
+ return CDBusUtil::TryMethodCall(DBUS_BUS_SYSTEM, "org.freedesktop.UDisks", "/org/freedesktop/UDisks", "org.freedesktop.UDisks", "EnumerateDevices");
+}
+
+void CUDisksProvider::DeviceAdded(const char *object, IStorageEventsCallback *callback)
+{
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks: DeviceAdded ({})", object);
+
+ if (m_AvailableDevices[object])
+ {
+ CLog::Log(LOGWARNING, "UDisks: Inconsistency found! DeviceAdded on an indexed disk");
+ delete m_AvailableDevices[object];
+ }
+
+ CUDiskDevice *device = NULL;
+ device = new CUDiskDevice(object);
+ m_AvailableDevices[object] = device;
+
+ // optical drives should be always mounted by default unless explicitly disabled by the user
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_handleMounting ||
+ (device->IsOptical() &&
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_autoMountOpticalMedia))
+ {
+ device->Mount();
+ }
+
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks: DeviceAdded - {}", device->ToString());
+
+ if (device->IsMounted() && device->IsApproved())
+ {
+ CLog::Log(LOGINFO, "UDisks: Added {}", device->GetMountPoint());
+ if (callback)
+ callback->OnStorageAdded(device->ToStorageDevice());
+ }
+}
+
+void CUDisksProvider::DeviceRemoved(const char *object, IStorageEventsCallback *callback)
+{
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks: DeviceRemoved ({})", object);
+
+ CUDiskDevice *device = m_AvailableDevices[object];
+ if (device)
+ {
+ if (device->IsMounted() && callback)
+ callback->OnStorageUnsafelyRemoved(device->ToStorageDevice());
+
+ delete m_AvailableDevices[object];
+ m_AvailableDevices.erase(object);
+ }
+}
+
+void CUDisksProvider::DeviceChanged(const char *object, IStorageEventsCallback *callback)
+{
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks: DeviceChanged ({})", object);
+
+ CUDiskDevice *device = m_AvailableDevices[object];
+ if (device == NULL)
+ {
+ CLog::Log(LOGWARNING, "UDisks: Inconsistency found! DeviceChanged on an unindexed disk");
+ DeviceAdded(object, callback);
+ }
+ else
+ {
+ bool mounted = device->IsMounted();
+ // make sure to not silently remount ejected usb thumb drives that user wants to eject
+ // optical drives should be always mounted by default unless explicitly disabled by the user
+ const auto advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+ if (!mounted && device->IsOptical())
+ {
+ if (advancedSettings->m_handleMounting || advancedSettings->m_autoMountOpticalMedia)
+ {
+ device->Mount();
+ }
+ }
+
+ device->Update();
+ if (!mounted && device->IsMounted() && callback)
+ callback->OnStorageAdded(device->ToStorageDevice());
+ else if (mounted && !device->IsMounted() && callback)
+ callback->OnStorageSafelyRemoved(device->ToStorageDevice());
+
+ CLog::Log(LOGDEBUG, LOGDBUS, "UDisks: DeviceChanged - {}", device->ToString());
+ }
+}
+
+std::vector<std::string> CUDisksProvider::EnumerateDisks()
+{
+ std::vector<std::string> devices;
+ CDBusMessage message("org.freedesktop.UDisks", "/org/freedesktop/UDisks", "org.freedesktop.UDisks", "EnumerateDevices");
+ DBusMessage *reply = message.SendSystem();
+ if (reply)
+ {
+ char** disks = NULL;
+ int length = 0;
+
+ if (dbus_message_get_args (reply, NULL, DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH, &disks, &length, DBUS_TYPE_INVALID))
+ {
+ for (int i = 0; i < length; i++)
+ devices.emplace_back(disks[i]);
+
+ dbus_free_string_array(disks);
+ }
+ }
+
+ return devices;
+}
+
+void CUDisksProvider::GetDisks(VECSOURCES& devices, bool EnumerateRemovable)
+{
+ for (auto& itr : m_AvailableDevices)
+ {
+ CUDiskDevice* device = itr.second;
+ if (device && device->IsApproved() && device->IsSystemInternal() != EnumerateRemovable)
+ devices.push_back(device->ToMediaShare());
+ }
+}
diff --git a/xbmc/platform/linux/storage/UDisksProvider.h b/xbmc/platform/linux/storage/UDisksProvider.h
new file mode 100644
index 0000000..c5372f8
--- /dev/null
+++ b/xbmc/platform/linux/storage/UDisksProvider.h
@@ -0,0 +1,131 @@
+/*
+ * 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 "DBusUtil.h"
+#include "storage/IStorageProvider.h"
+
+#include <string>
+#include <vector>
+
+class CUDiskDevice
+{
+public:
+ CUDiskDevice(const char *DeviceKitUDI);
+ ~CUDiskDevice() = default;
+
+ void Update();
+
+ bool Mount();
+ bool UnMount();
+
+ /*! \brief Check if the device is approved/whitelisted
+ * @return true if the device is approved/whitelisted, false otherwise
+ */
+ bool IsApproved() const;
+
+ /*! \brief Get the storage type of this device
+ * @return the storage type (e.g. OPTICAL) or UNKNOWN if
+ * the type couldn't be detected
+ */
+ MEDIA_DETECT::STORAGE::Type GetStorageType() const;
+
+ /*! \brief Check if the device is optical
+ * @return true if the device is optical, false otherwise
+ */
+ bool IsOptical() const;
+
+ /*! \brief Check if the device is mounted
+ * @return true if the device is mounted, false otherwise
+ */
+ bool IsMounted() const;
+
+ /*! \brief Check if the device is internal to the system
+ * @return true if the device is internal to the system, false otherwise
+ */
+ bool IsSystemInternal() const;
+
+ /*! \brief Get the device display name/label
+ * @return the device display name/label
+ */
+ std::string GetDisplayName() const;
+
+ /*! \brief Get the device mount point
+ * @return the device mount point
+ */
+ std::string GetMountPoint() const;
+
+ /*! \brief Get a representation of the device as a readable string
+ * @return device as a string
+ */
+ std::string ToString() const;
+
+ /*! \brief Get a representation of the device as a media share
+ * @return the media share
+ */
+ CMediaSource ToMediaShare() const;
+
+ /*! \brief Get a representation of the device as a storage device abstraction
+ * @return the storage device abstraction of the device
+ */
+ MEDIA_DETECT::STORAGE::StorageDevice ToStorageDevice() const;
+
+private:
+ std::string m_UDI;
+ std::string m_DeviceKitUDI;
+ std::string m_MountPath;
+ std::string m_FileSystem;
+ std::string m_Label;
+ bool m_isMounted;
+ bool m_isMountedByUs;
+ bool m_isRemovable;
+ bool m_isPartition;
+ bool m_isFileSystem;
+ bool m_isSystemInternal;
+ bool m_isOptical;
+ int64_t m_PartitionSize;
+};
+
+class CUDisksProvider : public IStorageProvider
+{
+public:
+ CUDisksProvider();
+ ~CUDisksProvider() override;
+
+ void Initialize() override;
+ void Stop() override { }
+
+ void GetLocalDrives(VECSOURCES &localDrives) override { GetDisks(localDrives, false); }
+ void GetRemovableDrives(VECSOURCES &removableDrives) override { GetDisks(removableDrives, true); }
+
+ bool Eject(const std::string& mountpath) override;
+
+ std::vector<std::string> GetDiskUsage() override;
+
+ bool PumpDriveChangeEvents(IStorageEventsCallback *callback) override;
+
+ static bool HasUDisks();
+private:
+ typedef std::map<std::string, CUDiskDevice *> DeviceMap;
+ typedef std::pair<std::string, CUDiskDevice *> DevicePair;
+
+ void DeviceAdded(const char *object, IStorageEventsCallback *callback);
+ void DeviceRemoved(const char *object, IStorageEventsCallback *callback);
+ void DeviceChanged(const char *object, IStorageEventsCallback *callback);
+
+ std::vector<std::string> EnumerateDisks();
+
+ void GetDisks(VECSOURCES& devices, bool EnumerateRemovable);
+
+ int m_DaemonVersion;
+
+ DeviceMap m_AvailableDevices;
+
+ CDBusConnection m_connection;
+};
diff --git a/xbmc/platform/linux/test/CMakeLists.txt b/xbmc/platform/linux/test/CMakeLists.txt
new file mode 100644
index 0000000..1fb0281
--- /dev/null
+++ b/xbmc/platform/linux/test/CMakeLists.txt
@@ -0,0 +1,3 @@
+list(APPEND SOURCES TestSysfsPath.cpp)
+
+core_add_test_library(linux_test)
diff --git a/xbmc/platform/linux/test/TestSysfsPath.cpp b/xbmc/platform/linux/test/TestSysfsPath.cpp
new file mode 100644
index 0000000..91e7763
--- /dev/null
+++ b/xbmc/platform/linux/test/TestSysfsPath.cpp
@@ -0,0 +1,91 @@
+/*
+ * 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 "platform/linux/SysfsPath.h"
+#include "utils/StringUtils.h"
+
+#include <fstream>
+#include <sstream>
+#include <string>
+
+#include <gtest/gtest.h>
+
+struct TestSysfsPath : public ::testing::Test
+{
+ std::string GetTestFilePath()
+ {
+ std::string tmpdir{"/tmp"};
+ const char *test_tmpdir = getenv("TMPDIR");
+
+ if (test_tmpdir && test_tmpdir[0] != '\0') {
+ tmpdir.assign(test_tmpdir);
+ }
+
+ return tmpdir + "/kodi-test-" + StringUtils::CreateUUID();
+ }
+};
+
+TEST_F(TestSysfsPath, SysfsPathTestInt)
+{
+ std::string filepath = GetTestFilePath();
+ std::ofstream m_output(filepath);
+
+ int temp{1234};
+ m_output << temp;
+ m_output.close();
+
+ CSysfsPath path(filepath);
+ ASSERT_TRUE(path.Exists());
+ EXPECT_EQ(path.Get<int>(), 1234);
+ EXPECT_EQ(path.Get<float>(), 1234);
+ EXPECT_EQ(path.Get<double>(), 1234);
+ EXPECT_EQ(path.Get<uint64_t>(), 1234);
+ EXPECT_EQ(path.Get<uint16_t>(), 1234);
+ EXPECT_EQ(path.Get<unsigned int>(), 1234);
+ EXPECT_EQ(path.Get<unsigned long int>(), 1234);
+
+ std::remove(filepath.c_str());
+}
+
+TEST_F(TestSysfsPath, SysfsPathTestString)
+{
+ std::string filepath = GetTestFilePath();
+ std::ofstream m_output{filepath};
+
+ std::string temp{"test"};
+ m_output << temp;
+ m_output.close();
+
+ CSysfsPath path(filepath);
+ ASSERT_TRUE(path.Exists());
+ EXPECT_EQ(path.Get<std::string>(), "test");
+
+ std::remove(filepath.c_str());
+}
+
+TEST_F(TestSysfsPath, SysfsPathTestLongString)
+{
+ std::string filepath = GetTestFilePath().append("-long");
+ std::ofstream m_output{filepath};
+
+ std::string temp{"test with spaces"};
+ m_output << temp;
+ m_output.close();
+
+ CSysfsPath path(filepath);
+ ASSERT_TRUE(path.Exists());
+ EXPECT_EQ(path.Get<std::string>(), "test with spaces");
+
+ std::remove(filepath.c_str());
+}
+
+TEST_F(TestSysfsPath, SysfsPathTestPathDoesNotExist)
+{
+ CSysfsPath otherPath{"/thispathdoesnotexist"};
+ ASSERT_FALSE(otherPath.Exists());
+}
diff --git a/xbmc/platform/linux/threads/CMakeLists.txt b/xbmc/platform/linux/threads/CMakeLists.txt
new file mode 100644
index 0000000..b90d55e
--- /dev/null
+++ b/xbmc/platform/linux/threads/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES ThreadImplLinux.cpp)
+set(HEADERS ThreadImplLinux.h)
+
+core_add_library(platform_linux_threads)
diff --git a/xbmc/platform/linux/threads/ThreadImplLinux.cpp b/xbmc/platform/linux/threads/ThreadImplLinux.cpp
new file mode 100644
index 0000000..f4810cf
--- /dev/null
+++ b/xbmc/platform/linux/threads/ThreadImplLinux.cpp
@@ -0,0 +1,171 @@
+/*
+ * 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 "ThreadImplLinux.h"
+
+#include "utils/Map.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <array>
+#include <limits.h>
+
+#include <sys/resource.h>
+#include <unistd.h>
+
+#if !defined(TARGET_ANDROID) && (defined(__GLIBC__) || defined(__UCLIBC__))
+#if defined(__UCLIBC__) || !__GLIBC_PREREQ(2, 30)
+#include <sys/syscall.h>
+#endif
+#endif
+
+namespace
+{
+
+constexpr auto nativeThreadPriorityMap = make_map<ThreadPriority, int>({
+ {ThreadPriority::LOWEST, -1},
+ {ThreadPriority::BELOW_NORMAL, -1},
+ {ThreadPriority::NORMAL, 0},
+ {ThreadPriority::ABOVE_NORMAL, 1},
+ {ThreadPriority::HIGHEST, 1},
+});
+
+static_assert(static_cast<size_t>(ThreadPriority::PRIORITY_COUNT) == nativeThreadPriorityMap.size(),
+ "nativeThreadPriorityMap doesn't match the size of ThreadPriority, did you forget to "
+ "add/remove a mapping?");
+
+constexpr int ThreadPriorityToNativePriority(const ThreadPriority& priority)
+{
+ const auto it = nativeThreadPriorityMap.find(priority);
+ if (it != nativeThreadPriorityMap.cend())
+ {
+ return it->second;
+ }
+ else
+ {
+ throw std::range_error("Priority not found");
+ }
+}
+
+#if !defined(TARGET_ANDROID) && (defined(__GLIBC__) || defined(__UCLIBC__))
+#if defined(__UCLIBC__) || !__GLIBC_PREREQ(2, 30)
+static pid_t gettid()
+{
+ return syscall(__NR_gettid);
+}
+#endif
+#endif
+
+} // namespace
+
+static int s_maxPriority;
+static bool s_maxPriorityIsSet{false};
+
+// We need to return what the best number than can be passed
+// to SetPriority is. It will basically be relative to the
+// the main thread's nice level, inverted (since "higher" priority
+// nice levels are actually lower numbers).
+static int GetUserMaxPriority(int maxPriority)
+{
+ if (s_maxPriorityIsSet)
+ return s_maxPriority;
+
+ // if we're root, then we can do anything. So we'll allow
+ // max priority.
+ if (geteuid() == 0)
+ return maxPriority;
+
+ // get user max prio
+ struct rlimit limit;
+ if (getrlimit(RLIMIT_NICE, &limit) != 0)
+ {
+ // If we fail getting the limit for nice we just assume we can't raise the priority
+ return 0;
+ }
+
+ const int appNice = getpriority(PRIO_PROCESS, getpid());
+ const int rlimVal = limit.rlim_cur;
+
+ // according to the docs, limit.rlim_cur shouldn't be zero, yet, here we are.
+ // if a user has no entry in limits.conf rlim_cur is zero. In this case the best
+ // nice value we can hope to achieve is '0' as a regular user
+ const int userBestNiceValue = (rlimVal == 0) ? 0 : (20 - rlimVal);
+
+ // running the app with nice -n 10 ->
+ // e.g. +10 10 - 0 // default non-root user.
+ // e.g. +30 10 - -20 // if root with rlimits set.
+ // running the app default ->
+ // e.g. 0 0 - 0 // default non-root user.
+ // e.g. +20 0 - -20 // if root with rlimits set.
+ const int bestUserSetPriority = appNice - userBestNiceValue; // nice is inverted from prio.
+
+ // static because we only need to check this once.
+ // we shouldn't expect a user to change RLIMIT_NICE while running
+ // and it won't work anyway for threads that already set their priority.
+ s_maxPriority = std::min(maxPriority, bestUserSetPriority);
+ s_maxPriorityIsSet = true;
+
+ return s_maxPriority;
+}
+
+std::unique_ptr<IThreadImpl> IThreadImpl::CreateThreadImpl(std::thread::native_handle_type handle)
+{
+ return std::make_unique<CThreadImplLinux>(handle);
+}
+
+CThreadImplLinux::CThreadImplLinux(std::thread::native_handle_type handle)
+ : IThreadImpl(handle), m_threadID(gettid())
+{
+}
+
+void CThreadImplLinux::SetThreadInfo(const std::string& name)
+{
+#if defined(__GLIBC__)
+ pthread_setname_np(m_handle, name.c_str());
+#endif
+
+ // get user max prio
+ const int maxPrio = ThreadPriorityToNativePriority(ThreadPriority::HIGHEST);
+ const int userMaxPrio = GetUserMaxPriority(maxPrio);
+
+ // if the user does not have an entry in limits.conf the following
+ // call will fail
+ if (userMaxPrio > 0)
+ {
+ // start thread with nice level of application
+ const int appNice = getpriority(PRIO_PROCESS, getpid());
+ if (setpriority(PRIO_PROCESS, m_threadID, appNice) != 0)
+ CLog::Log(LOGERROR, "[threads] failed to set priority: {}", strerror(errno));
+ }
+}
+
+bool CThreadImplLinux::SetPriority(const ThreadPriority& priority)
+{
+ // keep priority in bounds
+ const int prio = ThreadPriorityToNativePriority(priority);
+ const int maxPrio = ThreadPriorityToNativePriority(ThreadPriority::HIGHEST);
+ const int minPrio = ThreadPriorityToNativePriority(ThreadPriority::LOWEST);
+
+ // get user max prio given max prio (will take the min)
+ const int userMaxPrio = GetUserMaxPriority(maxPrio);
+
+ // clamp to min and max priorities
+ const int adjustedPrio = std::clamp(prio, minPrio, userMaxPrio);
+
+ // nice level of application
+ const int appNice = getpriority(PRIO_PROCESS, getpid());
+ const int newNice = appNice - adjustedPrio;
+
+ if (setpriority(PRIO_PROCESS, m_threadID, newNice) != 0)
+ {
+ CLog::Log(LOGERROR, "[threads] failed to set priority: {}", strerror(errno));
+ return false;
+ }
+
+ return true;
+}
diff --git a/xbmc/platform/linux/threads/ThreadImplLinux.h b/xbmc/platform/linux/threads/ThreadImplLinux.h
new file mode 100644
index 0000000..986ffe5
--- /dev/null
+++ b/xbmc/platform/linux/threads/ThreadImplLinux.h
@@ -0,0 +1,26 @@
+/*
+ * 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 "threads/IThreadImpl.h"
+
+class CThreadImplLinux : public IThreadImpl
+{
+public:
+ CThreadImplLinux(std::thread::native_handle_type handle);
+
+ ~CThreadImplLinux() override = default;
+
+ void SetThreadInfo(const std::string& name) override;
+
+ bool SetPriority(const ThreadPriority& priority) override;
+
+private:
+ pid_t m_threadID;
+};
diff --git a/xbmc/platform/posix/CMakeLists.txt b/xbmc/platform/posix/CMakeLists.txt
new file mode 100644
index 0000000..2d8d4db
--- /dev/null
+++ b/xbmc/platform/posix/CMakeLists.txt
@@ -0,0 +1,21 @@
+set(SOURCES ConvUtils.cpp
+ CPUInfoPosix.cpp
+ Filesystem.cpp
+ MessagePrinter.cpp
+ PlatformPosix.cpp
+ PosixMountProvider.cpp
+ PosixResourceCounter.cpp
+ PosixTimezone.cpp
+ XHandle.cpp
+ XTimeUtils.cpp)
+
+set(HEADERS ConvUtils.h
+ CPUInfoPosix.h
+ PlatformDefs.h
+ PlatformPosix.h
+ PosixMountProvider.h
+ PosixResourceCounter.h
+ PosixTimezone.h
+ XHandle.h)
+
+core_add_library(platform_posix)
diff --git a/xbmc/platform/posix/CPUInfoPosix.cpp b/xbmc/platform/posix/CPUInfoPosix.cpp
new file mode 100644
index 0000000..9d158d0
--- /dev/null
+++ b/xbmc/platform/posix/CPUInfoPosix.cpp
@@ -0,0 +1,60 @@
+/*
+ * 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 "CPUInfoPosix.h"
+
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+
+bool CCPUInfoPosix::GetTemperature(CTemperature& temperature)
+{
+ return CheckUserTemperatureCommand(temperature);
+}
+
+bool CCPUInfoPosix::CheckUserTemperatureCommand(CTemperature& temperature)
+{
+ temperature.SetValid(false);
+
+ auto settingComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingComponent)
+ return false;
+
+ auto advancedSettings = settingComponent->GetAdvancedSettings();
+ if (!advancedSettings)
+ return false;
+
+ std::string cmd = advancedSettings->m_cpuTempCmd;
+
+ if (cmd.empty())
+ return false;
+
+ int value = {};
+ char scale = {};
+
+ auto p = popen(cmd.c_str(), "r");
+ if (p)
+ {
+ int ret = fscanf(p, "%d %c", &value, &scale);
+ pclose(p);
+
+ if (ret < 2)
+ return false;
+ }
+
+ if (scale == 'C' || scale == 'c')
+ temperature = CTemperature::CreateFromCelsius(value);
+ else if (scale == 'F' || scale == 'f')
+ temperature = CTemperature::CreateFromFahrenheit(value);
+ else
+ return false;
+
+ temperature.SetValid(true);
+
+ return true;
+}
diff --git a/xbmc/platform/posix/CPUInfoPosix.h b/xbmc/platform/posix/CPUInfoPosix.h
new file mode 100644
index 0000000..af94b1b
--- /dev/null
+++ b/xbmc/platform/posix/CPUInfoPosix.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "utils/CPUInfo.h"
+
+class CCPUInfoPosix : public CCPUInfo
+{
+public:
+ virtual bool GetTemperature(CTemperature& temperature) override;
+
+protected:
+ CCPUInfoPosix() = default;
+ virtual ~CCPUInfoPosix() = default;
+
+ bool CheckUserTemperatureCommand(CTemperature& temperature);
+};
diff --git a/xbmc/platform/posix/ConvUtils.cpp b/xbmc/platform/posix/ConvUtils.cpp
new file mode 100644
index 0000000..6ad3da2
--- /dev/null
+++ b/xbmc/platform/posix/ConvUtils.cpp
@@ -0,0 +1,23 @@
+/*
+ * 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 <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+
+#include "PlatformDefs.h"
+
+DWORD GetLastError()
+{
+ return errno;
+}
+
+void SetLastError(DWORD dwErrCode)
+{
+ errno = dwErrCode;
+}
diff --git a/xbmc/platform/posix/ConvUtils.h b/xbmc/platform/posix/ConvUtils.h
new file mode 100644
index 0000000..2e7c16a
--- /dev/null
+++ b/xbmc/platform/posix/ConvUtils.h
@@ -0,0 +1,15 @@
+/*
+ * 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 "PlatformDefs.h" // DWORD ...
+
+DWORD GetLastError();
+void SetLastError(DWORD dwErrCode);
+
diff --git a/xbmc/platform/posix/Filesystem.cpp b/xbmc/platform/posix/Filesystem.cpp
new file mode 100644
index 0000000..0db3af5
--- /dev/null
+++ b/xbmc/platform/posix/Filesystem.cpp
@@ -0,0 +1,128 @@
+/*
+ * 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 "platform/Filesystem.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/URIUtils.h"
+
+#if defined(TARGET_LINUX)
+#include <sys/statvfs.h>
+#elif defined(TARGET_DARWIN) || defined(TARGET_FREEBSD)
+#include <sys/param.h>
+#include <sys/mount.h>
+#elif defined(TARGET_ANDROID)
+#include <sys/statfs.h>
+#endif
+
+#include <cstdint>
+#include <cstdlib>
+#include <limits.h>
+#include <string.h>
+
+#include <unistd.h>
+
+namespace KODI
+{
+namespace PLATFORM
+{
+namespace FILESYSTEM
+{
+
+space_info space(const std::string& path, std::error_code& ec)
+{
+ ec.clear();
+ space_info sp;
+#if defined(TARGET_LINUX)
+ struct statvfs64 fsInfo;
+ auto result = statvfs64(CSpecialProtocol::TranslatePath(path).c_str(), &fsInfo);
+#else
+ struct statfs fsInfo;
+ // is 64-bit on android and darwin (10.6SDK + any iOS)
+ auto result = statfs(CSpecialProtocol::TranslatePath(path).c_str(), &fsInfo);
+#endif
+
+ if (result != 0)
+ {
+ ec.assign(result, std::system_category());
+ sp.available = static_cast<uintmax_t>(-1);
+ sp.capacity = static_cast<uintmax_t>(-1);
+ sp.free = static_cast<uintmax_t>(-1);
+ return sp;
+ }
+ sp.available = static_cast<uintmax_t>(fsInfo.f_bavail * fsInfo.f_bsize);
+ sp.capacity = static_cast<uintmax_t>(fsInfo.f_blocks * fsInfo.f_bsize);
+ sp.free = static_cast<uintmax_t>(fsInfo.f_bfree * fsInfo.f_bsize);
+
+ return sp;
+}
+
+std::string temp_directory_path(std::error_code &ec)
+{
+ ec.clear();
+
+ auto result = getenv("TMPDIR");
+ if (result)
+ return URIUtils::AppendSlash(result);
+
+ return "/tmp/";
+}
+
+std::string create_temp_directory(std::error_code &ec)
+{
+ char buf[PATH_MAX];
+
+ auto path = temp_directory_path(ec);
+
+ strncpy(buf, (path + "xbmctempXXXXXX").c_str(), sizeof(buf) - 1);
+ buf[sizeof(buf) - 1] = '\0';
+
+ auto tmp = mkdtemp(buf);
+ if (!tmp)
+ {
+ ec.assign(errno, std::system_category());
+ return std::string();
+ }
+
+ ec.clear();
+ return std::string(tmp);
+}
+
+std::string temp_file_path(const std::string& suffix, std::error_code& ec)
+{
+ char tmp[PATH_MAX];
+
+ auto tempPath = create_temp_directory(ec);
+ if (ec)
+ return std::string();
+
+ tempPath = URIUtils::AddFileToFolder(tempPath, "xbmctempfileXXXXXX" + suffix);
+ if (tempPath.length() >= PATH_MAX)
+ {
+ ec.assign(EOVERFLOW, std::system_category());
+ return std::string();
+ }
+
+ strncpy(tmp, tempPath.c_str(), sizeof(tmp) - 1);
+ tmp[sizeof(tmp) - 1] = '\0';
+
+ auto fd = mkstemps(tmp, suffix.length());
+ if (fd < 0)
+ {
+ ec.assign(errno, std::system_category());
+ return std::string();
+ }
+
+ close(fd);
+
+ ec.clear();
+ return std::string(tmp);
+}
+
+}
+}
+}
diff --git a/xbmc/platform/posix/MessagePrinter.cpp b/xbmc/platform/posix/MessagePrinter.cpp
new file mode 100644
index 0000000..6621740
--- /dev/null
+++ b/xbmc/platform/posix/MessagePrinter.cpp
@@ -0,0 +1,40 @@
+/*
+ * 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 "platform/MessagePrinter.h"
+
+#include "CompileInfo.h"
+
+#include <stdio.h>
+
+void CMessagePrinter::DisplayMessage(const std::string& message)
+{
+ fprintf(stdout, "%s\n", message.c_str());
+}
+
+void CMessagePrinter::DisplayWarning(const std::string& warning)
+{
+ fprintf(stderr, "%s\n", warning.c_str());
+}
+
+void CMessagePrinter::DisplayError(const std::string& error)
+{
+ fprintf(stderr,"%s\n", error.c_str());
+}
+
+void CMessagePrinter::DisplayHelpMessage(const std::vector<std::pair<std::string, std::string>>& help)
+{
+ //very crude implementation, pretty it up when possible
+ std::string message;
+ for (const auto& line : help)
+ {
+ message.append(line.first + "\t" + line.second + "\n");
+ }
+
+ fprintf(stdout, "%s\n", message.c_str());
+}
diff --git a/xbmc/platform/posix/PlatformDefs.h b/xbmc/platform/posix/PlatformDefs.h
new file mode 100644
index 0000000..e6e59fe
--- /dev/null
+++ b/xbmc/platform/posix/PlatformDefs.h
@@ -0,0 +1,153 @@
+/*
+ * 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 <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <string.h>
+#if defined(TARGET_DARWIN)
+#include <stdio.h>
+#include <sched.h>
+#include <AvailabilityMacros.h>
+#ifndef __STDC_FORMAT_MACROS
+ #define __STDC_FORMAT_MACROS
+#endif
+#include <sys/sysctl.h>
+#include <mach/mach.h>
+#if defined(TARGET_DARWIN_OSX)
+#include <libkern/OSTypes.h>
+#endif
+
+#elif defined(TARGET_FREEBSD)
+#include <stdio.h>
+#include <sys/sysctl.h>
+#else
+#include <sys/sysinfo.h>
+#endif
+
+#include <sys/time.h>
+#include <time.h>
+
+#if defined(__ppc__) || defined(__powerpc__)
+#define PIXEL_ASHIFT 0
+#define PIXEL_RSHIFT 8
+#define PIXEL_GSHIFT 16
+#define PIXEL_BSHIFT 24
+#else
+#define PIXEL_ASHIFT 24
+#define PIXEL_RSHIFT 16
+#define PIXEL_GSHIFT 8
+#define PIXEL_BSHIFT 0
+#endif
+
+#include <stdint.h>
+
+#define _fdopen fdopen
+#define _vsnprintf vsnprintf
+
+#define __stdcall
+#define __cdecl
+#define WINAPI __stdcall
+#undef APIENTRY
+struct CXHandle; // forward declaration
+typedef CXHandle* HANDLE;
+
+typedef void* HINSTANCE;
+typedef void* HMODULE;
+
+typedef unsigned int DWORD;
+#define INVALID_HANDLE_VALUE ((HANDLE)~0U)
+
+#define MAXWORD 0xffff
+
+typedef union _LARGE_INTEGER
+{
+ struct {
+ DWORD LowPart;
+ int32_t HighPart;
+ } u;
+ long long QuadPart;
+} LARGE_INTEGER, *PLARGE_INTEGER;
+
+ typedef union _ULARGE_INTEGER {
+ struct {
+ DWORD LowPart;
+ DWORD HighPart;
+ } u;
+ unsigned long long QuadPart;
+} ULARGE_INTEGER;
+
+// Network
+#define SOCKET_ERROR (-1)
+#define INVALID_SOCKET (~0)
+#define closesocket(s) close(s)
+#define ioctlsocket(s, f, v) ioctl(s, f, v)
+#define WSAGetLastError() (errno)
+#define WSAECONNRESET ECONNRESET
+
+typedef int SOCKET;
+
+// Thread
+typedef int (*LPTHREAD_START_ROUTINE)(void *);
+
+// File
+#define O_BINARY 0
+#define _O_TRUNC O_TRUNC
+#define _O_RDONLY O_RDONLY
+#define _O_WRONLY O_WRONLY
+
+#if defined(TARGET_DARWIN) || defined(TARGET_FREEBSD)
+ #define stat64 stat
+ #define __stat64 stat
+ #define fstat64 fstat
+ typedef int64_t off64_t;
+ #if defined(TARGET_FREEBSD)
+ #define statfs64 statfs
+ #endif
+#else
+ #define __stat64 stat64
+#endif
+
+struct _stati64 {
+ dev_t st_dev;
+ ino_t st_ino;
+ unsigned short st_mode;
+ short st_nlink;
+ short st_uid;
+ short st_gid;
+ dev_t st_rdev;
+ long long st_size;
+ time_t _st_atime;
+ time_t _st_mtime;
+ time_t _st_ctime;
+};
+
+#define FILE_BEGIN 0
+#define FILE_CURRENT 1
+#define FILE_END 2
+
+#define _S_IFREG S_IFREG
+#define _S_IFDIR S_IFDIR
+#define MAX_PATH PATH_MAX
+
+// CreateFile defines
+#define FILE_FLAG_NO_BUFFERING 0x20000000
+#define FILE_FLAG_DELETE_ON_CLOSE 0x04000000
+
+#define CREATE_NEW 1
+#define CREATE_ALWAYS 2
+#define OPEN_EXISTING 3
+#define OPEN_ALWAYS 4
+#define TRUNCATE_EXISTING 5
+
+#define FILE_READ_DATA ( 0x0001 )
+#define FILE_WRITE_DATA ( 0x0002 )
diff --git a/xbmc/platform/posix/PlatformPosix.cpp b/xbmc/platform/posix/PlatformPosix.cpp
new file mode 100644
index 0000000..b1caa26
--- /dev/null
+++ b/xbmc/platform/posix/PlatformPosix.cpp
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+#include "PlatformPosix.h"
+
+#include "filesystem/SpecialProtocol.h"
+
+#include <cstdlib>
+#include <time.h>
+
+#ifdef HAS_DBUS
+#include <dbus/dbus.h>
+#endif
+
+std::atomic_flag CPlatformPosix::ms_signalFlag;
+
+bool CPlatformPosix::InitStageOne()
+{
+
+ if (!CPlatform::InitStageOne())
+ return false;
+
+ // Initialize to "set" state
+ ms_signalFlag.test_and_set();
+
+ // Initialize timezone information variables
+ tzset();
+
+ // set special://envhome
+ if (getenv("HOME"))
+ {
+ CSpecialProtocol::SetEnvHomePath(getenv("HOME"));
+ }
+ else
+ {
+ fprintf(stderr, "The HOME environment variable is not set!\n");
+ return false;
+ }
+
+#ifdef HAS_DBUS
+ // call 'dbus_threads_init_default' before any other dbus calls in order to
+ // avoid race conditions with other threads using dbus connections
+ dbus_threads_init_default();
+#endif
+
+ return true;
+}
+
+bool CPlatformPosix::TestQuitFlag()
+{
+ // Keep set, return true when it was cleared before
+ return !ms_signalFlag.test_and_set();
+}
+
+void CPlatformPosix::RequestQuit()
+{
+ ms_signalFlag.clear();
+}
diff --git a/xbmc/platform/posix/PlatformPosix.h b/xbmc/platform/posix/PlatformPosix.h
new file mode 100644
index 0000000..08513b9
--- /dev/null
+++ b/xbmc/platform/posix/PlatformPosix.h
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "platform/Platform.h"
+
+#include <atomic>
+
+class CPlatformPosix : public CPlatform
+{
+public:
+ bool InitStageOne() override;
+
+ static bool TestQuitFlag();
+ static void RequestQuit();
+
+private:
+ static std::atomic_flag ms_signalFlag;
+};
diff --git a/xbmc/platform/posix/PosixMountProvider.cpp b/xbmc/platform/posix/PosixMountProvider.cpp
new file mode 100644
index 0000000..d139be1
--- /dev/null
+++ b/xbmc/platform/posix/PosixMountProvider.cpp
@@ -0,0 +1,144 @@
+/*
+ * 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 "PosixMountProvider.h"
+
+#include "utils/RegExp.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <cstdlib>
+
+CPosixMountProvider::CPosixMountProvider()
+{
+ m_removableLength = 0;
+ PumpDriveChangeEvents(NULL);
+}
+
+void CPosixMountProvider::Initialize()
+{
+ CLog::Log(LOGDEBUG, "Selected Posix mount as storage provider");
+}
+
+void CPosixMountProvider::GetDrives(VECSOURCES &drives)
+{
+ std::vector<std::string> result;
+
+ CRegExp reMount;
+#if defined(TARGET_DARWIN) || defined(TARGET_FREEBSD)
+ reMount.RegComp("on (.+) \\(([^,]+)");
+#else
+ reMount.RegComp("on (.+) type ([^ ]+)");
+#endif
+ char line[1024];
+
+ FILE* pipe = popen("mount", "r");
+
+ if (pipe)
+ {
+ while (fgets(line, sizeof(line) - 1, pipe))
+ {
+ if (reMount.RegFind(line) != -1)
+ {
+ bool accepted = false;
+ std::string mountStr = reMount.GetReplaceString("\\1");
+ std::string fsStr = reMount.GetReplaceString("\\2");
+ const char* mount = mountStr.c_str();
+ const char* fs = fsStr.c_str();
+
+ // Here we choose which filesystems are approved
+ if (strcmp(fs, "fuseblk") == 0 || strcmp(fs, "vfat") == 0
+ || strcmp(fs, "ext2") == 0 || strcmp(fs, "ext3") == 0
+ || strcmp(fs, "reiserfs") == 0 || strcmp(fs, "xfs") == 0
+ || strcmp(fs, "ntfs-3g") == 0 || strcmp(fs, "iso9660") == 0
+ || strcmp(fs, "exfat") == 0
+ || strcmp(fs, "fusefs") == 0 || strcmp(fs, "hfs") == 0)
+ accepted = true;
+
+ // Ignore root
+ if (strcmp(mount, "/") == 0)
+ accepted = false;
+
+ if(accepted)
+ result.emplace_back(mount);
+ }
+ }
+ pclose(pipe);
+ }
+
+ for (unsigned int i = 0; i < result.size(); i++)
+ {
+ CMediaSource share;
+ share.strPath = result[i];
+ share.strName = URIUtils::GetFileName(result[i]);
+ share.m_ignore = true;
+ drives.push_back(share);
+ }
+}
+
+std::vector<std::string> CPosixMountProvider::GetDiskUsage()
+{
+ std::vector<std::string> result;
+ char line[1024];
+
+#if defined(TARGET_DARWIN)
+ FILE* pipe = popen("df -hT ufs,cd9660,hfs,udf", "r");
+#elif defined(TARGET_FREEBSD)
+ FILE* pipe = popen("df -h -t ufs,cd9660,hfs,udf,zfs", "r");
+#else
+ FILE* pipe = popen("df -h", "r");
+#endif
+
+ static const char* excludes[] = {"rootfs","devtmpfs","tmpfs","none","/dev/loop", "udev", NULL};
+
+ if (pipe)
+ {
+ while (fgets(line, sizeof(line) - 1, pipe))
+ {
+ bool ok=true;
+ for (int i=0;excludes[i];++i)
+ {
+ if (strstr(line,excludes[i]))
+ {
+ ok=false;
+ break;
+ }
+ }
+ if (ok)
+ result.emplace_back(line);
+ }
+ pclose(pipe);
+ }
+
+ return result;
+}
+
+bool CPosixMountProvider::Eject(const std::string& mountpath)
+{
+
+#if !defined(TARGET_DARWIN_EMBEDDED)
+ // just go ahead and try to umount the disk
+ // if it does umount, life is good, if not, no loss.
+ std::string cmd = "umount \"" + mountpath + "\"";
+ int status = system(cmd.c_str());
+
+ if (status == 0)
+ return true;
+#endif
+
+ return false;
+}
+
+bool CPosixMountProvider::PumpDriveChangeEvents(IStorageEventsCallback *callback)
+{
+ VECSOURCES drives;
+ GetRemovableDrives(drives);
+ bool changed = drives.size() != m_removableLength;
+ m_removableLength = drives.size();
+ return changed;
+}
diff --git a/xbmc/platform/posix/PosixMountProvider.h b/xbmc/platform/posix/PosixMountProvider.h
new file mode 100644
index 0000000..170c644
--- /dev/null
+++ b/xbmc/platform/posix/PosixMountProvider.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "storage/IStorageProvider.h"
+
+#include <string>
+#include <vector>
+
+class CPosixMountProvider : public IStorageProvider
+{
+public:
+ CPosixMountProvider();
+ ~CPosixMountProvider() override = default;
+
+ void Initialize() override;
+ void Stop() override { }
+
+ void GetLocalDrives(VECSOURCES &localDrives) override { GetDrives(localDrives); }
+ void GetRemovableDrives(VECSOURCES &removableDrives) override { /*GetDrives(removableDrives);*/ }
+
+ std::vector<std::string> GetDiskUsage() override;
+
+ bool Eject(const std::string& mountpath) override;
+
+ bool PumpDriveChangeEvents(IStorageEventsCallback *callback) override;
+private:
+ void GetDrives(VECSOURCES &drives);
+
+ unsigned int m_removableLength;
+};
diff --git a/xbmc/platform/posix/PosixResourceCounter.cpp b/xbmc/platform/posix/PosixResourceCounter.cpp
new file mode 100644
index 0000000..d82ef72
--- /dev/null
+++ b/xbmc/platform/posix/PosixResourceCounter.cpp
@@ -0,0 +1,71 @@
+/*
+ * 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 "PosixResourceCounter.h"
+
+#include "utils/log.h"
+
+#include <errno.h>
+
+#include "PlatformDefs.h"
+
+CPosixResourceCounter::CPosixResourceCounter()
+{
+ Reset();
+}
+
+CPosixResourceCounter::~CPosixResourceCounter() = default;
+
+double CPosixResourceCounter::GetCPUUsage()
+{
+ struct timeval tmNow;
+ if (gettimeofday(&tmNow, NULL) == -1)
+ CLog::Log(LOGERROR, "error {} in gettimeofday", errno);
+ else
+ {
+ double dElapsed = ( ((double)tmNow.tv_sec + (double)tmNow.tv_usec / 1000000.0) -
+ ((double)m_tmLastCheck.tv_sec + (double)m_tmLastCheck.tv_usec / 1000000.0) );
+
+ if (dElapsed >= 3.0)
+ {
+ struct rusage usage;
+ if (getrusage(RUSAGE_SELF, &usage) == -1)
+ CLog::Log(LOGERROR, "error {} in getrusage", errno);
+ else
+ {
+ double dUser = ( ((double)usage.ru_utime.tv_sec + (double)usage.ru_utime.tv_usec / 1000000.0) -
+ ((double)m_usage.ru_utime.tv_sec + (double)m_usage.ru_utime.tv_usec / 1000000.0) );
+ double dSys = ( ((double)usage.ru_stime.tv_sec + (double)usage.ru_stime.tv_usec / 1000000.0) -
+ ((double)m_usage.ru_stime.tv_sec + (double)m_usage.ru_stime.tv_usec / 1000000.0) );
+
+ m_tmLastCheck = tmNow;
+ m_usage = usage;
+ m_dLastUsage = ((dUser+dSys) / dElapsed) * 100.0;
+ return m_dLastUsage;
+ }
+ }
+ }
+
+ return m_dLastUsage;
+}
+
+void CPosixResourceCounter::Reset()
+{
+ if (gettimeofday(&m_tmLastCheck, NULL) == -1)
+ CLog::Log(LOGERROR, "error {} in gettimeofday", errno);
+
+ if (getrusage(RUSAGE_SELF, &m_usage) == -1)
+ CLog::Log(LOGERROR, "error {} in getrusage", errno);
+
+ m_dLastUsage = 0.0;
+}
+
+
+
+
+
diff --git a/xbmc/platform/posix/PosixResourceCounter.h b/xbmc/platform/posix/PosixResourceCounter.h
new file mode 100644
index 0000000..ef61b15
--- /dev/null
+++ b/xbmc/platform/posix/PosixResourceCounter.h
@@ -0,0 +1,31 @@
+/*
+ * 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 <time.h>
+
+#include <sys/resource.h>
+#include <sys/time.h>
+#include <sys/times.h>
+
+class CPosixResourceCounter
+{
+public:
+ CPosixResourceCounter();
+ virtual ~CPosixResourceCounter();
+
+ double GetCPUUsage();
+ void Reset();
+
+protected:
+ struct rusage m_usage;
+ struct timeval m_tmLastCheck;
+ double m_dLastUsage;
+};
+
diff --git a/xbmc/platform/posix/PosixTimezone.cpp b/xbmc/platform/posix/PosixTimezone.cpp
new file mode 100644
index 0000000..6f276a3
--- /dev/null
+++ b/xbmc/platform/posix/PosixTimezone.cpp
@@ -0,0 +1,272 @@
+/*
+ * 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 <time.h>
+#ifdef TARGET_ANDROID
+#include "platform/android/bionic_supplement/bionic_supplement.h"
+#endif
+#include "PlatformDefs.h"
+#include "PosixTimezone.h"
+#include "utils/SystemInfo.h"
+
+#include "ServiceBroker.h"
+#include "utils/StringUtils.h"
+#include "XBDateTime.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include <stdlib.h>
+
+#include <algorithm>
+
+CPosixTimezone::CPosixTimezone()
+{
+ char* line = NULL;
+ size_t linelen = 0;
+ int nameonfourthfield = 0;
+ std::string s;
+ std::vector<std::string> tokens;
+
+ // Load timezones
+ FILE* fp = fopen("/usr/share/zoneinfo/zone.tab", "r");
+ if (fp)
+ {
+ std::string countryCode;
+ std::string timezoneName;
+
+ while (getdelim(&line, &linelen, '\n', fp) > 0)
+ {
+ tokens.clear();
+ s = line;
+ StringUtils::Trim(s);
+
+ if (s.length() == 0)
+ continue;
+
+ if (s[0] == '#')
+ continue;
+
+ StringUtils::Tokenize(s, tokens, " \t");
+ if (tokens.size() < 3)
+ continue;
+
+ countryCode = tokens[0];
+ timezoneName = tokens[2];
+
+ if (m_timezonesByCountryCode.count(countryCode) == 0)
+ {
+ std::vector<std::string> timezones;
+ timezones.push_back(timezoneName);
+ m_timezonesByCountryCode[countryCode] = timezones;
+ }
+ else
+ {
+ std::vector<std::string>& timezones = m_timezonesByCountryCode[countryCode];
+ timezones.push_back(timezoneName);
+ }
+
+ m_countriesByTimezoneName[timezoneName] = countryCode;
+ }
+ fclose(fp);
+ }
+
+ if (line)
+ {
+ free(line);
+ line = NULL;
+ linelen = 0;
+ }
+
+ // Load countries
+ fp = fopen("/usr/share/zoneinfo/iso3166.tab", "r");
+ if (!fp)
+ {
+ fp = fopen("/usr/share/misc/iso3166", "r");
+ nameonfourthfield = 1;
+ }
+ if (fp)
+ {
+ std::string countryCode;
+ std::string countryName;
+
+ while (getdelim(&line, &linelen, '\n', fp) > 0)
+ {
+ s = line;
+ StringUtils::Trim(s);
+
+ //! @todo STRING_CLEANUP
+ if (s.length() == 0)
+ continue;
+
+ if (s[0] == '#')
+ continue;
+
+ // Search for the first non space from the 2nd character and on
+ int i = 2;
+ while (s[i] == ' ' || s[i] == '\t') i++;
+
+ if (nameonfourthfield)
+ {
+ // skip three letter
+ while (s[i] != ' ' && s[i] != '\t') i++;
+ while (s[i] == ' ' || s[i] == '\t') i++;
+ // skip number
+ while (s[i] != ' ' && s[i] != '\t') i++;
+ while (s[i] == ' ' || s[i] == '\t') i++;
+ }
+
+ countryCode = s.substr(0, 2);
+ countryName = s.substr(i);
+
+ m_counties.push_back(countryName);
+ m_countryByCode[countryCode] = countryName;
+ m_countryByName[countryName] = countryCode;
+ }
+ sort(m_counties.begin(), m_counties.end(), sortstringbyname());
+ fclose(fp);
+ }
+ free(line);
+}
+
+void CPosixTimezone::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_LOCALE_TIMEZONE)
+ {
+ SetTimezone(std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+
+ CDateTime::ResetTimezoneBias();
+ }
+ else if (settingId == CSettings::SETTING_LOCALE_TIMEZONECOUNTRY)
+ {
+ // nothing to do here. Changing locale.timezonecountry will trigger an
+ // update of locale.timezone and automatically adjust its value
+ // and execute OnSettingChanged() for it as well (see above)
+ }
+}
+
+void CPosixTimezone::OnSettingsLoaded()
+{
+ SetTimezone(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_TIMEZONE));
+ CDateTime::ResetTimezoneBias();
+}
+
+std::vector<std::string> CPosixTimezone::GetCounties()
+{
+ return m_counties;
+}
+
+std::vector<std::string> CPosixTimezone::GetTimezonesByCountry(const std::string& country)
+{
+ return m_timezonesByCountryCode[m_countryByName[country]];
+}
+
+std::string CPosixTimezone::GetCountryByTimezone(const std::string& timezone)
+{
+#if defined(TARGET_DARWIN)
+ return "?";
+#else
+ return m_countryByCode[m_countriesByTimezoneName[timezone]];
+#endif
+}
+
+void CPosixTimezone::SetTimezone(const std::string& timezoneName)
+{
+#if !defined(TARGET_DARWIN)
+ bool use_timezone = true;
+#else
+ bool use_timezone = false;
+#endif
+
+ if (use_timezone)
+ {
+ static char env_var[255];
+ sprintf(env_var, "TZ=:%s", timezoneName.c_str());
+ putenv(env_var);
+ tzset();
+ }
+}
+
+std::string CPosixTimezone::GetOSConfiguredTimezone()
+{
+ char timezoneName[255];
+
+ // try Slackware approach first
+ ssize_t rlrc = readlink("/etc/localtime-copied-from"
+ , timezoneName, sizeof(timezoneName)-1);
+
+ // RHEL and maybe other distros make /etc/localtime a symlink
+ if (rlrc == -1)
+ rlrc = readlink("/etc/localtime", timezoneName, sizeof(timezoneName)-1);
+
+ if (rlrc != -1)
+ {
+ timezoneName[rlrc] = '\0';
+
+ char* p = strrchr(timezoneName,'/');
+ if (p)
+ { // we want the previous '/'
+ char* q = p;
+ *q = 0;
+ p = strrchr(timezoneName,'/');
+ *q = '/';
+ if (p)
+ p++;
+ }
+ return p;
+ }
+
+ // now try Debian approach
+ timezoneName[0] = 0;
+ FILE* fp = fopen("/etc/timezone", "r");
+ if (fp)
+ {
+ if (fgets(timezoneName, sizeof(timezoneName), fp))
+ timezoneName[strlen(timezoneName)-1] = '\0';
+ fclose(fp);
+ }
+
+ return timezoneName;
+}
+
+void CPosixTimezone::SettingOptionsTimezoneCountriesFiller(
+ const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ std::vector<std::string> countries = g_timezone.GetCounties();
+ for (unsigned int i = 0; i < countries.size(); i++)
+ list.emplace_back(countries[i], countries[i]);
+}
+
+void CPosixTimezone::SettingOptionsTimezonesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ current = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ bool found = false;
+ std::vector<std::string> timezones = g_timezone.GetTimezonesByCountry(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_TIMEZONECOUNTRY));
+ for (unsigned int i = 0; i < timezones.size(); i++)
+ {
+ if (!found && StringUtils::EqualsNoCase(timezones[i], current))
+ found = true;
+
+ list.emplace_back(timezones[i], timezones[i]);
+ }
+
+ if (!found && !timezones.empty())
+ current = timezones[0];
+}
+
+CPosixTimezone g_timezone;
diff --git a/xbmc/platform/posix/PosixTimezone.h b/xbmc/platform/posix/PosixTimezone.h
new file mode 100644
index 0000000..076e87f
--- /dev/null
+++ b/xbmc/platform/posix/PosixTimezone.h
@@ -0,0 +1,58 @@
+/*
+ * 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 "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+class CSetting;
+struct StringSettingOption;
+
+class CPosixTimezone : public ISettingCallback, public ISettingsHandler
+{
+public:
+ CPosixTimezone();
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ void OnSettingsLoaded() override;
+
+ std::string GetOSConfiguredTimezone();
+
+ std::vector<std::string> GetCounties();
+ std::vector<std::string> GetTimezonesByCountry(const std::string& country);
+ std::string GetCountryByTimezone(const std::string& timezone);
+
+ void SetTimezone(const std::string& timezone);
+ int m_IsDST = 0;
+
+ static void SettingOptionsTimezoneCountriesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsTimezonesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+
+private:
+ std::vector<std::string> m_counties;
+ std::map<std::string, std::string> m_countryByCode;
+ std::map<std::string, std::string> m_countryByName;
+
+ std::map<std::string, std::vector<std::string> > m_timezonesByCountryCode;
+ std::map<std::string, std::string> m_countriesByTimezoneName;
+};
+
+extern CPosixTimezone g_timezone;
+
diff --git a/xbmc/platform/posix/XHandle.cpp b/xbmc/platform/posix/XHandle.cpp
new file mode 100644
index 0000000..be7a78b
--- /dev/null
+++ b/xbmc/platform/posix/XHandle.cpp
@@ -0,0 +1,138 @@
+/*
+ * 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 "XHandle.h"
+
+#include "utils/log.h"
+
+#include <cassert>
+#include <mutex>
+
+int CXHandle::m_objectTracker[10] = {};
+
+HANDLE WINAPI GetCurrentProcess(void) {
+ return (HANDLE)-1; // -1 a special value - pseudo handle
+}
+
+CXHandle::CXHandle()
+{
+ Init();
+ m_objectTracker[m_type]++;
+}
+
+CXHandle::CXHandle(HandleType nType)
+{
+ Init();
+ m_type=nType;
+ m_objectTracker[m_type]++;
+}
+
+CXHandle::CXHandle(const CXHandle &src)
+{
+ // we shouldn't get here EVER. however, if we do - try to make the best. copy what we can
+ // and most importantly - not share any pointer.
+ CLog::Log(LOGWARNING, "{}, copy handle.", __FUNCTION__);
+
+ Init();
+
+ if (src.m_hMutex)
+ m_hMutex = new CCriticalSection();
+
+ fd = src.fd;
+ m_bManualEvent = src.m_bManualEvent;
+ m_tmCreation = time(NULL);
+ m_FindFileResults = src.m_FindFileResults;
+ m_nFindFileIterator = src.m_nFindFileIterator;
+ m_FindFileDir = src.m_FindFileDir;
+ m_iOffset = src.m_iOffset;
+ m_bCDROM = src.m_bCDROM;
+ m_objectTracker[m_type]++;
+}
+
+CXHandle::~CXHandle()
+{
+
+ m_objectTracker[m_type]--;
+
+ if (RecursionCount > 0) {
+ CLog::Log(LOGERROR, "{}, destroying handle with recursion count {}", __FUNCTION__,
+ RecursionCount);
+ assert(false);
+ }
+
+ if (m_nRefCount > 1) {
+ CLog::Log(LOGERROR, "{}, destroying handle with ref count {}", __FUNCTION__, m_nRefCount);
+ assert(false);
+ }
+
+ if (m_hMutex) {
+ delete m_hMutex;
+ }
+
+ if (m_internalLock) {
+ delete m_internalLock;
+ }
+
+ if (m_hCond) {
+ delete m_hCond;
+ }
+
+ if ( fd != 0 ) {
+ close(fd);
+ }
+
+}
+
+void CXHandle::Init()
+{
+ fd=0;
+ m_hMutex=NULL;
+ m_hCond=NULL;
+ m_type = HND_NULL;
+ RecursionCount=0;
+ m_bManualEvent=false;
+ m_bEventSet=false;
+ m_nFindFileIterator=0 ;
+ m_nRefCount=1;
+ m_tmCreation = time(NULL);
+ m_internalLock = new CCriticalSection();
+}
+
+void CXHandle::ChangeType(HandleType newType) {
+ m_objectTracker[m_type]--;
+ m_type = newType;
+ m_objectTracker[m_type]++;
+}
+
+void CXHandle::DumpObjectTracker() {
+ for (int i=0; i< 10; i++) {
+ CLog::Log(LOGDEBUG, "object {} --> {} instances", i, m_objectTracker[i]);
+ }
+}
+
+bool CloseHandle(HANDLE hObject) {
+ if (!hObject)
+ return false;
+
+ if (hObject == INVALID_HANDLE_VALUE || hObject == (HANDLE)-1)
+ return true;
+
+ bool bDelete = false;
+ {
+ std::unique_lock<CCriticalSection> lock((*hObject->m_internalLock));
+ if (--hObject->m_nRefCount == 0)
+ bDelete = true;
+ }
+
+ if (bDelete)
+ delete hObject;
+
+ return true;
+}
+
+
diff --git a/xbmc/platform/posix/XHandle.h b/xbmc/platform/posix/XHandle.h
new file mode 100644
index 0000000..158817e
--- /dev/null
+++ b/xbmc/platform/posix/XHandle.h
@@ -0,0 +1,59 @@
+/*
+ * 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 "XHandlePublic.h"
+#include "threads/Condition.h"
+#include "threads/CriticalSection.h"
+
+#include <list>
+#include <string>
+#include <vector>
+
+#include "PlatformDefs.h"
+
+struct CXHandle {
+
+public:
+ typedef enum { HND_NULL = 0, HND_FILE, HND_EVENT, HND_MUTEX, HND_FIND_FILE } HandleType;
+
+ CXHandle();
+ explicit CXHandle(HandleType nType);
+ CXHandle(const CXHandle &src);
+
+ virtual ~CXHandle();
+ void Init();
+ inline HandleType GetType() { return m_type; }
+ void ChangeType(HandleType newType);
+
+ XbmcThreads::ConditionVariable *m_hCond;
+ std::list<CXHandle*> m_hParents;
+
+ // simulate mutex and critical section
+ CCriticalSection *m_hMutex;
+ int RecursionCount; // for mutex - for compatibility with TARGET_WINDOWS critical section
+ int fd;
+ bool m_bManualEvent;
+ time_t m_tmCreation;
+ std::vector<std::string> m_FindFileResults;
+ int m_nFindFileIterator;
+ std::string m_FindFileDir;
+ off64_t m_iOffset;
+ bool m_bCDROM;
+ bool m_bEventSet;
+ int m_nRefCount;
+ CCriticalSection *m_internalLock;
+
+ static void DumpObjectTracker();
+
+protected:
+ HandleType m_type;
+ static int m_objectTracker[10];
+
+};
diff --git a/xbmc/platform/posix/XHandlePublic.h b/xbmc/platform/posix/XHandlePublic.h
new file mode 100644
index 0000000..6d1f840
--- /dev/null
+++ b/xbmc/platform/posix/XHandlePublic.h
@@ -0,0 +1,19 @@
+/*
+ * 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
+
+struct CXHandle;
+typedef CXHandle* HANDLE;
+typedef HANDLE* LPHANDLE;
+
+bool CloseHandle(HANDLE hObject);
+
+#define DUPLICATE_CLOSE_SOURCE 0x00000001
+#define DUPLICATE_SAME_ACCESS 0x00000002
+
diff --git a/xbmc/platform/posix/XTimeUtils.cpp b/xbmc/platform/posix/XTimeUtils.cpp
new file mode 100644
index 0000000..e78e5cf
--- /dev/null
+++ b/xbmc/platform/posix/XTimeUtils.cpp
@@ -0,0 +1,228 @@
+/*
+ * 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 "utils/XTimeUtils.h"
+
+#include "PosixTimezone.h"
+
+#include <errno.h>
+#include <mutex>
+#include <time.h>
+
+#include <sys/times.h>
+
+#if defined(TARGET_ANDROID) && !defined(__LP64__)
+#include <time64.h>
+#endif
+
+#define WIN32_TIME_OFFSET ((unsigned long long)(369 * 365 + 89) * 24 * 3600 * 10000000)
+
+namespace KODI
+{
+namespace TIME
+{
+
+/*
+ * A Leap year is any year that is divisible by four, but not by 100 unless also
+ * divisible by 400
+ */
+#define IsLeapYear(y) ((!(y % 4)) ? (((!(y % 400)) && (y % 100)) ? 1 : 0) : 0)
+
+uint32_t GetTimeZoneInformation(TimeZoneInformation* timeZoneInformation)
+{
+ if (!timeZoneInformation)
+ return KODI_TIME_ZONE_ID_INVALID;
+
+ struct tm t;
+ time_t tt = time(NULL);
+ if (localtime_r(&tt, &t))
+ timeZoneInformation->bias = -t.tm_gmtoff / 60;
+
+ timeZoneInformation->standardName = tzname[0];
+ timeZoneInformation->daylightName = tzname[1];
+
+ return KODI_TIME_ZONE_ID_UNKNOWN;
+}
+
+void GetLocalTime(SystemTime* systemTime)
+{
+ const time_t t = time(NULL);
+ struct tm now;
+
+ localtime_r(&t, &now);
+ systemTime->year = now.tm_year + 1900;
+ systemTime->month = now.tm_mon + 1;
+ systemTime->dayOfWeek = now.tm_wday;
+ systemTime->day = now.tm_mday;
+ systemTime->hour = now.tm_hour;
+ systemTime->minute = now.tm_min;
+ systemTime->second = now.tm_sec;
+ systemTime->milliseconds = 0;
+ // NOTE: localtime_r() is not required to set this, but we Assume that it's set here.
+ g_timezone.m_IsDST = now.tm_isdst;
+}
+
+int FileTimeToLocalFileTime(const FileTime* fileTime, FileTime* localFileTime)
+{
+ ULARGE_INTEGER l;
+ l.u.LowPart = fileTime->lowDateTime;
+ l.u.HighPart = fileTime->highDateTime;
+
+ time_t ft;
+ struct tm tm_ft;
+ FileTimeToTimeT(fileTime, &ft);
+ localtime_r(&ft, &tm_ft);
+
+ l.QuadPart += static_cast<unsigned long long>(tm_ft.tm_gmtoff) * 10000000;
+
+ localFileTime->lowDateTime = l.u.LowPart;
+ localFileTime->highDateTime = l.u.HighPart;
+ return 1;
+}
+
+int SystemTimeToFileTime(const SystemTime* systemTime, FileTime* fileTime)
+{
+ static const int dayoffset[12] = {0, 31, 59, 90, 120, 151, 182, 212, 243, 273, 304, 334};
+#if defined(TARGET_DARWIN)
+ static std::mutex timegm_lock;
+#endif
+
+ struct tm sysTime = {};
+ sysTime.tm_year = systemTime->year - 1900;
+ sysTime.tm_mon = systemTime->month - 1;
+ sysTime.tm_wday = systemTime->dayOfWeek;
+ sysTime.tm_mday = systemTime->day;
+ sysTime.tm_hour = systemTime->hour;
+ sysTime.tm_min = systemTime->minute;
+ sysTime.tm_sec = systemTime->second;
+ sysTime.tm_yday = dayoffset[sysTime.tm_mon] + (sysTime.tm_mday - 1);
+ sysTime.tm_isdst = g_timezone.m_IsDST;
+
+ // If this is a leap year, and we're past the 28th of Feb, increment tm_yday.
+ if (IsLeapYear(systemTime->year) && (sysTime.tm_yday > 58))
+ sysTime.tm_yday++;
+
+#if defined(TARGET_DARWIN)
+ std::lock_guard<std::mutex> lock(timegm_lock);
+#endif
+
+#if defined(TARGET_ANDROID) && !defined(__LP64__)
+ time64_t t = timegm64(&sysTime);
+#else
+ time_t t = timegm(&sysTime);
+#endif
+
+ LARGE_INTEGER result;
+ result.QuadPart = (long long)t * 10000000 + (long long)systemTime->milliseconds * 10000;
+ result.QuadPart += WIN32_TIME_OFFSET;
+
+ fileTime->lowDateTime = result.u.LowPart;
+ fileTime->highDateTime = result.u.HighPart;
+
+ return 1;
+}
+
+long CompareFileTime(const FileTime* fileTime1, const FileTime* fileTime2)
+{
+ ULARGE_INTEGER t1;
+ t1.u.LowPart = fileTime1->lowDateTime;
+ t1.u.HighPart = fileTime1->highDateTime;
+
+ ULARGE_INTEGER t2;
+ t2.u.LowPart = fileTime2->lowDateTime;
+ t2.u.HighPart = fileTime2->highDateTime;
+
+ if (t1.QuadPart == t2.QuadPart)
+ return 0;
+ else if (t1.QuadPart < t2.QuadPart)
+ return -1;
+ else
+ return 1;
+}
+
+int FileTimeToSystemTime(const FileTime* fileTime, SystemTime* systemTime)
+{
+ LARGE_INTEGER file;
+ file.u.LowPart = fileTime->lowDateTime;
+ file.u.HighPart = fileTime->highDateTime;
+
+ file.QuadPart -= WIN32_TIME_OFFSET;
+ file.QuadPart /= 10000; /* to milliseconds */
+ systemTime->milliseconds = file.QuadPart % 1000;
+ file.QuadPart /= 1000; /* to seconds */
+
+ time_t ft = file.QuadPart;
+
+ struct tm tm_ft;
+ gmtime_r(&ft, &tm_ft);
+
+ systemTime->year = tm_ft.tm_year + 1900;
+ systemTime->month = tm_ft.tm_mon + 1;
+ systemTime->dayOfWeek = tm_ft.tm_wday;
+ systemTime->day = tm_ft.tm_mday;
+ systemTime->hour = tm_ft.tm_hour;
+ systemTime->minute = tm_ft.tm_min;
+ systemTime->second = tm_ft.tm_sec;
+
+ return 1;
+}
+
+int LocalFileTimeToFileTime(const FileTime* localFileTime, FileTime* fileTime)
+{
+ ULARGE_INTEGER l;
+ l.u.LowPart = localFileTime->lowDateTime;
+ l.u.HighPart = localFileTime->highDateTime;
+
+ l.QuadPart += (unsigned long long) timezone * 10000000;
+
+ fileTime->lowDateTime = l.u.LowPart;
+ fileTime->highDateTime = l.u.HighPart;
+
+ return 1;
+}
+
+
+int FileTimeToTimeT(const FileTime* localFileTime, time_t* pTimeT)
+{
+ if (!localFileTime || !pTimeT)
+ return false;
+
+ ULARGE_INTEGER fileTime;
+ fileTime.u.LowPart = localFileTime->lowDateTime;
+ fileTime.u.HighPart = localFileTime->highDateTime;
+
+ fileTime.QuadPart -= WIN32_TIME_OFFSET;
+ fileTime.QuadPart /= 10000; /* to milliseconds */
+ fileTime.QuadPart /= 1000; /* to seconds */
+
+ time_t ft = fileTime.QuadPart;
+
+ struct tm tm_ft;
+ localtime_r(&ft, &tm_ft);
+
+ *pTimeT = mktime(&tm_ft);
+ return 1;
+}
+
+int TimeTToFileTime(time_t timeT, FileTime* localFileTime)
+{
+ if (!localFileTime)
+ return false;
+
+ ULARGE_INTEGER result;
+ result.QuadPart = (unsigned long long) timeT * 10000000;
+ result.QuadPart += WIN32_TIME_OFFSET;
+
+ localFileTime->lowDateTime = result.u.LowPart;
+ localFileTime->highDateTime = result.u.HighPart;
+
+ return 1;
+}
+
+} // namespace TIME
+} // namespace KODI
diff --git a/xbmc/platform/posix/filesystem/CMakeLists.txt b/xbmc/platform/posix/filesystem/CMakeLists.txt
new file mode 100644
index 0000000..badb1a8
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(SOURCES PosixDirectory.cpp
+ PosixFile.cpp)
+
+set(HEADERS PosixDirectory.h
+ PosixFile.h)
+
+if(SMBCLIENT_FOUND)
+ list(APPEND SOURCES SMBDirectory.cpp
+ SMBFile.cpp
+ SMBWSDiscovery.cpp
+ SMBWSDiscoveryListener.cpp)
+ list(APPEND HEADERS SMBDirectory.h
+ SMBFile.h
+ SMBWSDiscovery.h
+ SMBWSDiscoveryListener.h)
+endif()
+
+core_add_library(platform_posix_filesystem)
diff --git a/xbmc/platform/posix/filesystem/PosixDirectory.cpp b/xbmc/platform/posix/filesystem/PosixDirectory.cpp
new file mode 100644
index 0000000..6685c0e
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/PosixDirectory.cpp
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2014-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 "PosixDirectory.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "utils/AliasShortcutUtils.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XTimeUtils.h"
+
+#include <dirent.h>
+#include <sys/stat.h>
+
+using namespace XFILE;
+
+CPosixDirectory::CPosixDirectory(void) = default;
+
+CPosixDirectory::~CPosixDirectory(void) = default;
+
+bool CPosixDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ std::string root = url.Get();
+
+ if (IsAliasShortcut(root, true))
+ TranslateAliasShortcut(root);
+
+ DIR *dir = opendir(root.c_str());
+ if (!dir)
+ return false;
+
+ struct dirent* entry;
+ while ((entry = readdir(dir)) != NULL)
+ {
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+ continue;
+
+ std::string itemLabel(entry->d_name);
+ CCharsetConverter::unknownToUTF8(itemLabel);
+ CFileItemPtr pItem(new CFileItem(itemLabel));
+ std::string itemPath(URIUtils::AddFileToFolder(root, entry->d_name));
+
+ bool bStat = false;
+ struct stat buffer;
+
+ // Unix-based readdir implementations may return an incorrect dirent.d_ino value that
+ // is not equal to the (correct) stat() obtained one. In this case the file type
+ // could not be determined and the value of dirent.d_type is set to DT_UNKNOWN.
+ // In order to get a correct value we have to incur the cost of calling stat.
+ if (entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK)
+ {
+ if (stat(itemPath.c_str(), &buffer) == 0)
+ bStat = true;
+ }
+
+ if (entry->d_type == DT_DIR || (bStat && S_ISDIR(buffer.st_mode)))
+ {
+ pItem->m_bIsFolder = true;
+ URIUtils::AddSlashAtEnd(itemPath);
+ }
+ else
+ {
+ pItem->m_bIsFolder = false;
+ }
+
+ if (StringUtils::StartsWith(entry->d_name, "."))
+ pItem->SetProperty("file:hidden", true);
+
+ pItem->SetPath(itemPath);
+
+ if (!(m_flags & DIR_FLAG_NO_FILE_INFO))
+ {
+ if (bStat || stat(pItem->GetPath().c_str(), &buffer) == 0)
+ {
+ KODI::TIME::FileTime fileTime, localTime;
+ KODI::TIME::TimeTToFileTime(buffer.st_mtime, &fileTime);
+ KODI::TIME::FileTimeToLocalFileTime(&fileTime, &localTime);
+ pItem->m_dateTime = localTime;
+
+ if (!pItem->m_bIsFolder)
+ pItem->m_dwSize = buffer.st_size;
+ }
+ }
+ items.Add(pItem);
+ }
+ closedir(dir);
+ return true;
+}
+
+bool CPosixDirectory::Create(const CURL& url)
+{
+ if (!Create(url.Get()))
+ return Exists(url);
+
+ return true;
+}
+
+bool CPosixDirectory::Create(const std::string& path)
+{
+ if (mkdir(path.c_str(), 0755) != 0)
+ {
+ if (errno == ENOENT)
+ {
+ auto sep = path.rfind('/');
+ if (sep == std::string::npos)
+ return false;
+
+ if (Create(path.substr(0, sep)))
+ return Create(path);
+ }
+
+ return false;
+ }
+ return true;
+}
+
+bool CPosixDirectory::Remove(const CURL& url)
+{
+ if (rmdir(url.Get().c_str()) == 0)
+ return true;
+
+ return !Exists(url);
+}
+
+bool CPosixDirectory::RemoveRecursive(const CURL& url)
+{
+ std::string root = url.Get();
+
+ if (IsAliasShortcut(root, true))
+ TranslateAliasShortcut(root);
+
+ DIR *dir = opendir(root.c_str());
+ if (!dir)
+ return false;
+
+ bool success(true);
+ struct dirent* entry;
+ while ((entry = readdir(dir)) != NULL)
+ {
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+ continue;
+
+ std::string itemLabel(entry->d_name);
+ CCharsetConverter::unknownToUTF8(itemLabel);
+ std::string itemPath(URIUtils::AddFileToFolder(root, entry->d_name));
+
+ bool bStat = false;
+ struct stat buffer;
+
+ // Unix-based readdir implementations may return an incorrect dirent.d_ino value that
+ // is not equal to the (correct) stat() obtained one. In this case the file type
+ // could not be determined and the value of dirent.d_type is set to DT_UNKNOWN.
+ // In order to get a correct value we have to incur the cost of calling stat.
+ if (entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK)
+ {
+ if (stat(itemPath.c_str(), &buffer) == 0)
+ bStat = true;
+ }
+
+ if (entry->d_type == DT_DIR || (bStat && S_ISDIR(buffer.st_mode)))
+ {
+ if (!RemoveRecursive(CURL{ itemPath }))
+ {
+ success = false;
+ break;
+ }
+ }
+ else
+ {
+ if (unlink(itemPath.c_str()) != 0)
+ {
+ success = false;
+ break;
+ }
+ }
+ }
+
+ closedir(dir);
+
+ if (success)
+ {
+ if (rmdir(root.c_str()) != 0)
+ success = false;
+ }
+
+ return success;
+}
+
+bool CPosixDirectory::Exists(const CURL& url)
+{
+ std::string path = url.Get();
+
+ if (IsAliasShortcut(path, true))
+ TranslateAliasShortcut(path);
+
+ struct stat buffer;
+ if (stat(path.c_str(), &buffer) != 0)
+ return false;
+ return S_ISDIR(buffer.st_mode) ? true : false;
+}
diff --git a/xbmc/platform/posix/filesystem/PosixDirectory.h b/xbmc/platform/posix/filesystem/PosixDirectory.h
new file mode 100644
index 0000000..eb323b6
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/PosixDirectory.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2014-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 "filesystem/IDirectory.h"
+
+namespace XFILE
+{
+
+class CPosixDirectory : public IDirectory
+{
+public:
+ CPosixDirectory(void);
+ ~CPosixDirectory(void) override;
+ 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;
+ bool RemoveRecursive(const CURL& url) override;
+private:
+ bool Create(const std::string& path);
+};
+}
diff --git a/xbmc/platform/posix/filesystem/PosixFile.cpp b/xbmc/platform/posix/filesystem/PosixFile.cpp
new file mode 100644
index 0000000..3010136
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/PosixFile.cpp
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2014-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 "PosixFile.h"
+
+#include "URL.h"
+#include "filesystem/File.h"
+#include "utils/AliasShortcutUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#include <string>
+
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+
+#if defined(HAVE_STATX) // use statx if available to get file birth date
+#include <sys/sysmacros.h>
+#endif
+#include <unistd.h>
+
+using namespace XFILE;
+
+CPosixFile::~CPosixFile()
+{
+ if (m_fd >= 0)
+ close(m_fd);
+}
+
+// local helper
+static std::string getFilename(const CURL& url)
+{
+ std::string filename(url.GetFileName());
+ if (IsAliasShortcut(filename, false))
+ TranslateAliasShortcut(filename);
+
+ return filename;
+}
+
+
+bool CPosixFile::Open(const CURL& url)
+{
+ if (m_fd >= 0)
+ return false;
+
+ const std::string filename(getFilename(url));
+ if (filename.empty())
+ return false;
+
+ m_fd = open(filename.c_str(), O_RDONLY, S_IRUSR | S_IRGRP | S_IROTH);
+ m_filePos = 0;
+
+ return m_fd != -1;
+}
+
+bool CPosixFile::OpenForWrite(const CURL& url, bool bOverWrite /* = false*/ )
+{
+ if (m_fd >= 0)
+ return false;
+
+ const std::string filename(getFilename(url));
+ if (filename.empty())
+ return false;
+
+ m_fd = open(filename.c_str(), O_RDWR | O_CREAT | (bOverWrite ? O_TRUNC : 0), S_IWUSR | S_IRUSR | S_IRGRP | S_IWGRP | S_IROTH);
+ if (m_fd < 0)
+ return false;
+
+ m_filePos = 0;
+ m_allowWrite = true;
+
+ return true;
+}
+
+void CPosixFile::Close()
+{
+ if (m_fd >= 0)
+ {
+ close(m_fd);
+ m_fd = -1;
+ m_filePos = -1;
+ m_lastDropPos = -1;
+ m_allowWrite = false;
+ }
+}
+
+
+ssize_t CPosixFile::Read(void* lpBuf, size_t uiBufSize)
+{
+ if (m_fd < 0)
+ return -1;
+
+ assert(lpBuf != NULL || uiBufSize == 0);
+ if (lpBuf == NULL && uiBufSize != 0)
+ return -1;
+
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+ const ssize_t res = read(m_fd, lpBuf, uiBufSize);
+ if (res < 0)
+ {
+ Seek(0, SEEK_CUR); // force update file position
+ return -1;
+ }
+
+ if (m_filePos >= 0)
+ {
+ m_filePos += res; // if m_filePos was known - update it
+#if defined(HAVE_POSIX_FADVISE)
+ // Drop the cache between then last drop and 16 MB behind where we
+ // are now, to make sure the file doesn't displace everything else.
+ // However, never throw out the first 16 MB of the file, as it might
+ // be the header etc., and never ask the OS to drop in chunks of
+ // less than 1 MB.
+ const int64_t end_drop = m_filePos - 16 * 1024 * 1024;
+ if (end_drop >= 17 * 1024 * 1024)
+ {
+ const int64_t start_drop = std::max<int64_t>(m_lastDropPos, 16 * 1024 * 1024);
+ if (end_drop - start_drop >= 1 * 1024 * 1024 &&
+ posix_fadvise(m_fd, start_drop, end_drop - start_drop, POSIX_FADV_DONTNEED) == 0)
+ m_lastDropPos = end_drop;
+ }
+#endif
+ }
+
+ return res;
+}
+
+ssize_t CPosixFile::Write(const void* lpBuf, size_t uiBufSize)
+{
+ if (m_fd < 0)
+ return -1;
+
+ assert(lpBuf != NULL || uiBufSize == 0);
+ if ((lpBuf == NULL && uiBufSize != 0) || !m_allowWrite)
+ return -1;
+
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+ const ssize_t res = write(m_fd, lpBuf, uiBufSize);
+ if (res < 0)
+ {
+ Seek(0, SEEK_CUR); // force update file position
+ return -1;
+ }
+
+ if (m_filePos >= 0)
+ m_filePos += res; // if m_filePos was known - update it
+
+ return res;
+}
+
+int64_t CPosixFile::Seek(int64_t iFilePosition, int iWhence /* = SEEK_SET*/)
+{
+ if (m_fd < 0)
+ return -1;
+
+#ifdef TARGET_ANDROID
+ //! @todo properly support with detection in configure
+ //! Android special case: Android doesn't substitute off64_t for off_t and similar functions
+ m_filePos = lseek64(m_fd, (off64_t)iFilePosition, iWhence);
+#else // !TARGET_ANDROID
+ const off_t filePosOffT = (off_t) iFilePosition;
+ // check for parameter overflow
+ if (sizeof(int64_t) != sizeof(off_t) && iFilePosition != filePosOffT)
+ return -1;
+
+ m_filePos = lseek(m_fd, filePosOffT, iWhence);
+#endif // !TARGET_ANDROID
+
+ return m_filePos;
+}
+
+int CPosixFile::Truncate(int64_t size)
+{
+ if (m_fd < 0)
+ return -1;
+
+ const off_t sizeOffT = (off_t) size;
+ // check for parameter overflow
+ if (sizeof(int64_t) != sizeof(off_t) && size != sizeOffT)
+ return -1;
+
+ return ftruncate(m_fd, sizeOffT);
+}
+
+int64_t CPosixFile::GetPosition()
+{
+ if (m_fd < 0)
+ return -1;
+
+ if (m_filePos < 0)
+ m_filePos = lseek(m_fd, 0, SEEK_CUR);
+
+ return m_filePos;
+}
+
+int64_t CPosixFile::GetLength()
+{
+ if (m_fd < 0)
+ return -1;
+
+ struct stat64 st;
+ if (fstat64(m_fd, &st) != 0)
+ return -1;
+
+ return st.st_size;
+}
+
+void CPosixFile::Flush()
+{
+ if (m_fd >= 0)
+ fsync(m_fd);
+}
+
+int CPosixFile::IoControl(EIoControl request, void* param)
+{
+ if (m_fd < 0)
+ return -1;
+
+ if (request == IOCTRL_NATIVE)
+ {
+ if(!param)
+ return -1;
+ return ioctl(m_fd, ((SNativeIoControl*)param)->request, ((SNativeIoControl*)param)->param);
+ }
+ else if (request == IOCTRL_SEEK_POSSIBLE)
+ {
+ if (GetPosition() < 0)
+ return -1; // current position is unknown, can't test seeking
+ else if (m_filePos > 0)
+ {
+ const int64_t orgPos = m_filePos;
+ // try to seek one byte back
+ const bool seekPossible = (Seek(orgPos - 1, SEEK_SET) == (orgPos - 1));
+ // restore file position
+ if (Seek(orgPos, SEEK_SET) != orgPos)
+ return 0; // seeking is not possible
+
+ return seekPossible ? 1 : 0;
+ }
+ else
+ { // m_filePos == 0
+ // try to seek one byte forward
+ const bool seekPossible = (Seek(1, SEEK_SET) == 1);
+ // restore file position
+ if (Seek(0, SEEK_SET) != 0)
+ return 0; // seeking is not possible
+
+ if (seekPossible)
+ return 1;
+
+ if (GetLength() <= 0)
+ return -1; // size of file is zero or can be zero, can't test seeking
+ else
+ return 0; // size of file is 1 byte or more and seeking not possible
+ }
+ }
+
+ return -1;
+}
+
+
+bool CPosixFile::Delete(const CURL& url)
+{
+ const std::string filename(getFilename(url));
+ if (filename.empty())
+ return false;
+
+ if (unlink(filename.c_str()) == 0)
+ return true;
+
+ if (errno == EACCES || errno == EPERM)
+ CLog::LogF(LOGWARNING, "Can't access file \"{}\"", filename);
+
+ return false;
+}
+
+bool CPosixFile::Rename(const CURL& url, const CURL& urlnew)
+{
+ const std::string name(getFilename(url)), newName(getFilename(urlnew));
+ if (name.empty() || newName.empty())
+ return false;
+
+ if (name == newName)
+ return true;
+
+ if (rename(name.c_str(), newName.c_str()) == 0)
+ return true;
+
+ if (errno == EACCES || errno == EPERM)
+ CLog::LogF(LOGWARNING, "Can't access file \"{}\" for rename to \"{}\"", name, newName);
+
+ // rename across mount points - need to copy/delete
+ if (errno == EXDEV)
+ {
+ CLog::LogF(LOGDEBUG,
+ "Source file \"{}\" and target file \"{}\" are located on different filesystems, "
+ "copy&delete will be used instead of rename",
+ name, newName);
+ if (XFILE::CFile::Copy(name, newName))
+ {
+ if (XFILE::CFile::Delete(name))
+ return true;
+ else
+ XFILE::CFile::Delete(newName);
+ }
+ }
+
+ return false;
+}
+
+bool CPosixFile::Exists(const CURL& url)
+{
+ const std::string filename(getFilename(url));
+ if (filename.empty())
+ return false;
+
+ struct stat64 st;
+ return stat64(filename.c_str(), &st) == 0 && !S_ISDIR(st.st_mode);
+}
+
+int CPosixFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ assert(buffer != NULL);
+ const std::string filename(getFilename(url));
+ if (filename.empty() || !buffer)
+ return -1;
+
+// Use statx to get file creation date (btime) which isn't available with just stat. This fills the
+// buffer with the same data as the Windows implementation. Useful for the music library so that
+// tags can be updated without changing the date they were added to the library (as m/ctime does)
+
+#if defined(HAVE_STATX)
+ int dirfd = AT_FDCWD;
+ int flags = AT_STATX_SYNC_AS_STAT;
+ unsigned int mask = STATX_BASIC_STATS | STATX_BTIME;
+ struct statx stxbuf = {};
+ long ret = 0;
+ ret = statx(dirfd, filename.c_str(), flags, mask, &stxbuf);
+ if (ret == 0)
+ {
+ *buffer = {};
+ buffer->st_mtime = stxbuf.stx_mtime.tv_sec; // modification time
+ if (stxbuf.stx_btime.tv_sec != 0)
+ buffer->st_ctime = stxbuf.stx_btime.tv_sec; // birth (creation) time
+ else
+ buffer->st_ctime = stxbuf.stx_ctime.tv_sec; // change time (of metadata or file)
+ // fill everything else we might need (statx buffer is slightly different to stat buffer so
+ // can't just return the statx buffer) Note we might not need all this but lets fill it for
+ // completeness
+ buffer->st_atime = stxbuf.stx_atime.tv_sec;
+ buffer->st_size = stxbuf.stx_size;
+ buffer->st_blksize = stxbuf.stx_blksize;
+ buffer->st_blocks = stxbuf.stx_blocks;
+ buffer->st_ino = stxbuf.stx_ino;
+ buffer->st_nlink = stxbuf.stx_nlink;
+ buffer->st_uid = stxbuf.stx_uid;
+ buffer->st_gid = stxbuf.stx_gid;
+ buffer->st_mode = stxbuf.stx_mode;
+ buffer->st_rdev = makedev(stxbuf.stx_rdev_major, stxbuf.stx_rdev_minor);
+ buffer->st_dev = makedev(stxbuf.stx_dev_major, stxbuf.stx_dev_minor);
+ }
+ return ret;
+#else
+ return stat64(filename.c_str(), buffer);
+#endif
+}
+
+int CPosixFile::Stat(struct __stat64* buffer)
+{
+ assert(buffer != NULL);
+ if (m_fd < 0 || !buffer)
+ return -1;
+
+ return fstat64(m_fd, buffer);
+}
diff --git a/xbmc/platform/posix/filesystem/PosixFile.h b/xbmc/platform/posix/filesystem/PosixFile.h
new file mode 100644
index 0000000..aa5e0a0
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/PosixFile.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014-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 "filesystem/IFile.h"
+
+namespace XFILE
+{
+
+ class CPosixFile : public IFile
+ {
+ public:
+ ~CPosixFile() override;
+
+ bool Open(const CURL& url) override;
+ bool OpenForWrite(const CURL& url, bool bOverWrite = false) override;
+ void Close() 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;
+ int Truncate(int64_t size) override;
+ int64_t GetPosition() override;
+ int64_t GetLength() override;
+ void Flush() override;
+ int IoControl(EIoControl request, void* param) override;
+
+ bool Delete(const CURL& url) override;
+ bool Rename(const CURL& url, const CURL& urlnew) override;
+ bool Exists(const CURL& url) override;
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+ int Stat(struct __stat64* buffer) override;
+
+ protected:
+ int m_fd = -1;
+ int64_t m_filePos = -1;
+ int64_t m_lastDropPos = -1;
+ bool m_allowWrite = false;
+ };
+
+}
diff --git a/xbmc/platform/posix/filesystem/SMBDirectory.cpp b/xbmc/platform/posix/filesystem/SMBDirectory.cpp
new file mode 100644
index 0000000..e2305af
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/SMBDirectory.cpp
@@ -0,0 +1,372 @@
+/*
+ * 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.
+ */
+
+/*
+* know bugs:
+* - when opening a server for the first time with ip address and the second time
+* with server name, access to the server is denied.
+* - when browsing entire network, user can't go back one step
+* share = smb://, user selects a workgroup, user selects a server.
+* doing ".." will go back to smb:// (entire network) and not to workgroup list.
+*
+* debugging is set to a max of 10 for release builds (see local.h)
+*/
+
+#include "SMBDirectory.h"
+
+#include "FileItem.h"
+#include "PasswordManager.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include "platform/posix/filesystem/SMBWSDiscovery.h"
+
+#include <mutex>
+
+#include <libsmbclient.h>
+
+struct CachedDirEntry
+{
+ unsigned int type;
+ std::string name;
+};
+
+using namespace XFILE;
+
+CSMBDirectory::CSMBDirectory(void)
+{
+ smb.AddActiveConnection();
+}
+
+CSMBDirectory::~CSMBDirectory(void)
+{
+ smb.AddIdleConnection();
+}
+
+bool CSMBDirectory::GetDirectory(const CURL& url, CFileItemList &items)
+{
+ // We accept smb://[[[domain;]user[:password@]]server[/share[/path[/file]]]]
+
+ /* samba isn't thread safe with old interface, always lock */
+ std::unique_lock<CCriticalSection> lock(smb);
+
+ smb.Init();
+
+ //Separate roots for the authentication and the containing items to allow browsing to work correctly
+ std::string strRoot = url.Get();
+ std::string strAuth;
+
+ lock.unlock(); // OpenDir is locked
+
+ // if url provided does not having anything except smb protocol
+ // Do a WS-Discovery search to find possible smb servers to mimic smbv1 behaviour
+ if (strRoot == "smb://")
+ {
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return false;
+
+ auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (!settings)
+ return false;
+
+ // Check WS-Discovery daemon enabled, if not return as smb:// cant be handled further
+ if (settings->GetBool(CSettings::SETTING_SERVICES_WSDISCOVERY))
+ {
+ WSDiscovery::CWSDiscoveryPosix& WSInstance =
+ dynamic_cast<WSDiscovery::CWSDiscoveryPosix&>(CServiceBroker::GetWSDiscovery());
+ return WSInstance.GetServerList(items);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ int fd = OpenDir(url, strAuth);
+ if (fd < 0)
+ return false;
+
+ URIUtils::AddSlashAtEnd(strRoot);
+ URIUtils::AddSlashAtEnd(strAuth);
+
+ std::string strFile;
+
+ // need to keep the samba lock for as short as possible.
+ // so we first cache all directory entries and then go over them again asking for stat
+ // "stat" is locked each time. that way the lock is freed between stat requests
+ std::vector<CachedDirEntry> vecEntries;
+ struct smbc_dirent* dirEnt;
+
+ lock.lock();
+ if (!smb.IsSmbValid())
+ return false;
+ while ((dirEnt = smbc_readdir(fd)))
+ {
+ CachedDirEntry aDir;
+ aDir.type = dirEnt->smbc_type;
+ aDir.name = dirEnt->name;
+ vecEntries.push_back(aDir);
+ }
+ smbc_closedir(fd);
+ lock.unlock();
+
+ for (size_t i=0; i<vecEntries.size(); i++)
+ {
+ CachedDirEntry aDir = vecEntries[i];
+
+ // We use UTF-8 internally, as does SMB
+ strFile = aDir.name;
+
+ if (!strFile.empty() && strFile != "." && strFile != ".."
+ && strFile != "lost+found"
+ && aDir.type != SMBC_PRINTER_SHARE && aDir.type != SMBC_IPC_SHARE)
+ {
+ int64_t iSize = 0;
+ bool bIsDir = true;
+ int64_t lTimeDate = 0;
+ bool hidden = false;
+
+ if(StringUtils::EndsWith(strFile, "$") && aDir.type == SMBC_FILE_SHARE )
+ continue;
+
+ if (StringUtils::StartsWith(strFile, "."))
+ hidden = true;
+
+ // only stat files that can give proper responses
+ if ( aDir.type == SMBC_FILE ||
+ aDir.type == SMBC_DIR )
+ {
+ // set this here to if the stat should fail
+ bIsDir = (aDir.type == SMBC_DIR);
+
+ struct stat info = {};
+ if ((m_flags & DIR_FLAG_NO_FILE_INFO)==0 && CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambastatfiles)
+ {
+ // make sure we use the authenticated path which contains any default username
+ const std::string strFullName = strAuth + smb.URLEncode(strFile);
+
+ lock.lock();
+ if (!smb.IsSmbValid())
+ {
+ items.ClearItems();
+ return false;
+ }
+
+ if( smbc_stat(strFullName.c_str(), &info) == 0 )
+ {
+
+ char value[20];
+ // We poll for extended attributes which symbolizes bits but split up into a string. Where 0x02 is hidden and 0x12 is hidden directory.
+ // According to the libsmbclient.h it returns 0 on success and -1 on error.
+ // But before Samba 4.17.5 it seems to return the length of the returned value
+ // (which is 4), see https://bugzilla.samba.org/show_bug.cgi?id=14808.
+ // Checking for >= 0 should work both for the old erroneous and the correct behaviour.
+ if (smbc_getxattr(strFullName.c_str(), "system.dos_attr.mode", value, sizeof(value)) >= 0)
+ {
+ long longvalue = strtol(value, NULL, 16);
+ if (longvalue & SMBC_DOS_MODE_HIDDEN)
+ hidden = true;
+ }
+ else
+ CLog::Log(
+ LOGERROR,
+ "Getting extended attributes for the share: '{}'\nunix_err:'{:x}' error: '{}'",
+ CURL::GetRedacted(strFullName), errno, strerror(errno));
+
+ bIsDir = S_ISDIR(info.st_mode);
+ lTimeDate = info.st_mtime;
+ if(lTimeDate == 0) // if modification date is missing, use create date
+ lTimeDate = info.st_ctime;
+ iSize = info.st_size;
+ }
+ else
+ CLog::Log(LOGERROR, "{} - Failed to stat file {}", __FUNCTION__,
+ CURL::GetRedacted(strFullName));
+
+ lock.unlock();
+ }
+ }
+
+ KODI::TIME::FileTime fileTime, localTime;
+ KODI::TIME::TimeTToFileTime(lTimeDate, &fileTime);
+ KODI::TIME::FileTimeToLocalFileTime(&fileTime, &localTime);
+
+ if (bIsDir)
+ {
+ CFileItemPtr pItem(new CFileItem(strFile));
+ std::string path(strRoot);
+
+ // needed for network / workgroup browsing
+ // skip if root if we are given a server
+ if (aDir.type == SMBC_SERVER)
+ {
+ /* create url with same options, user, pass.. but no filename or host*/
+ CURL rooturl(strRoot);
+ rooturl.SetFileName("");
+ rooturl.SetHostName("");
+ path = smb.URLEncode(rooturl);
+ }
+ path = URIUtils::AddFileToFolder(path,aDir.name);
+ URIUtils::AddSlashAtEnd(path);
+ pItem->SetPath(path);
+ pItem->m_bIsFolder = true;
+ pItem->m_dateTime=localTime;
+ if (hidden)
+ pItem->SetProperty("file:hidden", true);
+ items.Add(pItem);
+ }
+ else
+ {
+ CFileItemPtr pItem(new CFileItem(strFile));
+ pItem->SetPath(strRoot + aDir.name);
+ pItem->m_bIsFolder = false;
+ pItem->m_dwSize = iSize;
+ pItem->m_dateTime=localTime;
+ if (hidden)
+ pItem->SetProperty("file:hidden", true);
+ items.Add(pItem);
+ }
+ }
+ }
+
+ return true;
+}
+
+int CSMBDirectory::Open(const CURL &url)
+{
+ smb.Init();
+ std::string strAuth;
+ return OpenDir(url, strAuth);
+}
+
+/// \brief Checks authentication against SAMBA share and prompts for username and password if needed
+/// \param strAuth The SMB style path
+/// \return SMB file descriptor
+int CSMBDirectory::OpenDir(const CURL& url, std::string& strAuth)
+{
+ int fd = -1;
+
+ /* make a writeable copy */
+ CURL urlIn = CSMB::GetResolvedUrl(url);
+
+ CPasswordManager::GetInstance().AuthenticateURL(urlIn);
+ strAuth = smb.URLEncode(urlIn);
+
+ // remove the / or \ at the end. the samba library does not strip them off
+ // don't do this for smb:// !!
+ std::string s = strAuth;
+ int len = s.length();
+ if (len > 1 && s.at(len - 2) != '/' &&
+ (s.at(len - 1) == '/' || s.at(len - 1) == '\\'))
+ {
+ s.erase(len - 1, 1);
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGSAMBA, "Using authentication url {}", CURL::GetRedacted(s));
+
+ {
+ std::unique_lock<CCriticalSection> lock(smb);
+ if (!smb.IsSmbValid())
+ return -1;
+ fd = smbc_opendir(s.c_str());
+ }
+
+ while (fd < 0) /* only to avoid goto in following code */
+ {
+ std::string cError;
+
+ if (errno == EACCES)
+ {
+ if (m_flags & DIR_FLAG_ALLOW_PROMPT)
+ RequireAuthentication(urlIn);
+ break;
+ }
+
+ if (errno == ENODEV || errno == ENOENT)
+ cError = StringUtils::Format(g_localizeStrings.Get(770), errno);
+ else
+ cError = strerror(errno);
+
+ if (m_flags & DIR_FLAG_ALLOW_PROMPT)
+ SetErrorDialog(257, cError.c_str());
+ break;
+ }
+
+ if (fd < 0)
+ {
+ // write error to logfile
+ CLog::Log(
+ LOGERROR,
+ "SMBDirectory->GetDirectory: Unable to open directory : '{}'\nunix_err:'{:x}' error : '{}'",
+ CURL::GetRedacted(strAuth), errno, strerror(errno));
+ }
+
+ return fd;
+}
+
+bool CSMBDirectory::Create(const CURL& url2)
+{
+ std::unique_lock<CCriticalSection> lock(smb);
+ smb.Init();
+
+ CURL url = CSMB::GetResolvedUrl(url2);
+ CPasswordManager::GetInstance().AuthenticateURL(url);
+ std::string strFileName = smb.URLEncode(url);
+
+ int result = smbc_mkdir(strFileName.c_str(), 0);
+ bool success = (result == 0 || EEXIST == errno);
+ if(!success)
+ CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__, strerror(errno));
+
+ return success;
+}
+
+bool CSMBDirectory::Remove(const CURL& url2)
+{
+ std::unique_lock<CCriticalSection> lock(smb);
+ smb.Init();
+
+ CURL url = CSMB::GetResolvedUrl(url2);
+ CPasswordManager::GetInstance().AuthenticateURL(url);
+ std::string strFileName = smb.URLEncode(url);
+
+ int result = smbc_rmdir(strFileName.c_str());
+
+ if(result != 0 && errno != ENOENT)
+ {
+ CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__, strerror(errno));
+ return false;
+ }
+
+ return true;
+}
+
+bool CSMBDirectory::Exists(const CURL& url2)
+{
+ std::unique_lock<CCriticalSection> lock(smb);
+ smb.Init();
+
+ CURL url = CSMB::GetResolvedUrl(url2);
+ CPasswordManager::GetInstance().AuthenticateURL(url);
+ std::string strFileName = smb.URLEncode(url);
+
+ struct stat info;
+ if (smbc_stat(strFileName.c_str(), &info) != 0)
+ return false;
+
+ return S_ISDIR(info.st_mode);
+}
+
diff --git a/xbmc/platform/posix/filesystem/SMBDirectory.h b/xbmc/platform/posix/filesystem/SMBDirectory.h
new file mode 100644
index 0000000..cef06d2
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/SMBDirectory.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "MediaSource.h"
+#include "SMBFile.h"
+#include "filesystem/IDirectory.h"
+
+namespace XFILE
+{
+class CSMBDirectory : public IDirectory
+{
+public:
+ CSMBDirectory(void);
+ ~CSMBDirectory(void) override;
+ bool GetDirectory(const CURL& url, CFileItemList &items) override;
+ DIR_CACHE_TYPE GetCacheType(const CURL& url) const override { return DIR_CACHE_ONCE; }
+ bool Create(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ bool Remove(const CURL& url) override;
+
+ int Open(const CURL &url);
+
+private:
+ int OpenDir(const CURL &url, std::string& strAuth);
+};
+}
diff --git a/xbmc/platform/posix/filesystem/SMBFile.cpp b/xbmc/platform/posix/filesystem/SMBFile.cpp
new file mode 100644
index 0000000..38a82c3
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/SMBFile.cpp
@@ -0,0 +1,721 @@
+/*
+ * 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.
+ */
+
+// SMBFile.cpp: implementation of the CSMBFile class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "SMBFile.h"
+
+#include "PasswordManager.h"
+#include "SMBDirectory.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "commons/Exception.h"
+#include "filesystem/SpecialProtocol.h"
+#include "network/DNSNameCache.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <inttypes.h>
+#include <mutex>
+
+#include <libsmbclient.h>
+
+using namespace XFILE;
+
+void xb_smbc_log(const char* msg)
+{
+ CLog::Log(LOGINFO, "{}{}", "smb: ", msg);
+}
+
+void xb_smbc_auth(const char *srv, const char *shr, char *wg, int wglen,
+ char *un, int unlen, char *pw, int pwlen)
+{
+}
+
+// WTF is this ?, we get the original server cache only
+// to set the server cache to this function which call the
+// original one anyway. Seems quite silly.
+smbc_get_cached_srv_fn orig_cache;
+SMBCSRV* xb_smbc_cache(SMBCCTX* c, const char* server, const char* share, const char* workgroup, const char* username)
+{
+ return orig_cache(c, server, share, workgroup, username);
+}
+
+bool CSMB::IsFirstInit = true;
+
+CSMB::CSMB()
+{
+ m_context = NULL;
+ m_OpenConnections = 0;
+ m_IdleTimeout = 0;
+}
+
+CSMB::~CSMB()
+{
+ Deinit();
+}
+
+void CSMB::Deinit()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ /* samba goes loco if deinited while it has some files opened */
+ if (m_context)
+ {
+ smbc_set_context(NULL);
+ smbc_free_context(m_context, 1);
+ m_context = NULL;
+ }
+}
+
+void CSMB::Init()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ if (!m_context)
+ {
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ // force libsmbclient to use our own smb.conf by overriding HOME
+ std::string truehome(getenv("HOME"));
+ setenv("HOME", CSpecialProtocol::TranslatePath("special://home").c_str(), 1);
+
+ // Create ~/.kodi/.smb/smb.conf. This file is used by libsmbclient.
+ // http://us1.samba.org/samba/docs/man/manpages-3/libsmbclient.7.html
+ // http://us1.samba.org/samba/docs/man/manpages-3/smb.conf.5.html
+ std::string smb_conf;
+ std::string home(getenv("HOME"));
+ URIUtils::RemoveSlashAtEnd(home);
+ smb_conf = home + "/.smb";
+ int result = mkdir(smb_conf.c_str(), 0755);
+ if (result == 0 || (errno == EEXIST && IsFirstInit))
+ {
+ smb_conf += "/smb.conf";
+ FILE* f = fopen(smb_conf.c_str(), "w");
+ if (f != NULL)
+ {
+ fprintf(f, "[global]\n");
+
+ fprintf(f, "\tlock directory = %s/.smb/\n", home.c_str());
+
+ // set minimum smbclient protocol version
+ if (settings->GetInt(CSettings::SETTING_SMB_MINPROTOCOL) > 0)
+ {
+ if (settings->GetInt(CSettings::SETTING_SMB_MINPROTOCOL) == 1)
+ fprintf(f, "\tclient min protocol = NT1\n");
+ else
+ fprintf(f, "\tclient min protocol = SMB%d\n", settings->GetInt(CSettings::SETTING_SMB_MINPROTOCOL));
+ }
+
+ // set maximum smbclient protocol version
+ if (settings->GetInt(CSettings::SETTING_SMB_MAXPROTOCOL) > 0)
+ {
+ if (settings->GetInt(CSettings::SETTING_SMB_MAXPROTOCOL) == 1)
+ fprintf(f, "\tclient max protocol = NT1\n");
+ else
+ fprintf(f, "\tclient max protocol = SMB%d\n", settings->GetInt(CSettings::SETTING_SMB_MAXPROTOCOL));
+ }
+
+ // set legacy security options
+ if (settings->GetBool(CSettings::SETTING_SMB_LEGACYSECURITY) && (settings->GetInt(CSettings::SETTING_SMB_MAXPROTOCOL) == 1))
+ {
+ fprintf(f, "\tclient NTLMv2 auth = no\n");
+ fprintf(f, "\tclient use spnego = no\n");
+ }
+
+ // set wins server if there's one. name resolve order defaults to 'lmhosts host wins bcast'.
+ // if no WINS server has been specified the wins method will be ignored.
+ if (settings->GetString(CSettings::SETTING_SMB_WINSSERVER).length() > 0 && !StringUtils::EqualsNoCase(settings->GetString(CSettings::SETTING_SMB_WINSSERVER), "0.0.0.0") )
+ {
+ fprintf(f, "\twins server = %s\n", settings->GetString(CSettings::SETTING_SMB_WINSSERVER).c_str());
+ fprintf(f, "\tname resolve order = bcast wins host\n");
+ }
+ else
+ fprintf(f, "\tname resolve order = bcast host\n");
+
+ // use user-configured charset. if no charset is specified,
+ // samba tries to use charset 850 but falls back to ASCII in case it is not available
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambadoscodepage.length() > 0)
+ fprintf(f, "\tdos charset = %s\n", CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambadoscodepage.c_str());
+
+ // include users configuration if available
+ fprintf(f, "\tinclude = %s/.smb/user.conf\n", home.c_str());
+
+ fclose(f);
+ }
+ }
+
+ // reads smb.conf so this MUST be after we create smb.conf
+ // multiple smbc_init calls are ignored by libsmbclient.
+ // note: this is important as it initializes the smb old
+ // interface compatibility. Samba 3.4.0 or higher has the new interface.
+ // note: we leak the following here once, not sure why yet.
+ // 48 bytes -> smb_xmalloc_array
+ // 32 bytes -> set_param_opt
+ // 16 bytes -> set_param_opt
+ smbc_init(xb_smbc_auth, 0);
+
+ // setup our context
+ m_context = smbc_new_context();
+
+ // restore HOME
+ setenv("HOME", truehome.c_str(), 1);
+
+#ifdef DEPRECATED_SMBC_INTERFACE
+ smbc_setDebug(m_context, CServiceBroker::GetLogging().CanLogComponent(LOGSAMBA) ? 10 : 0);
+ smbc_setFunctionAuthData(m_context, xb_smbc_auth);
+ orig_cache = smbc_getFunctionGetCachedServer(m_context);
+ smbc_setFunctionGetCachedServer(m_context, xb_smbc_cache);
+ smbc_setOptionOneSharePerServer(m_context, false);
+ smbc_setOptionBrowseMaxLmbCount(m_context, 0);
+ smbc_setTimeout(m_context, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambaclienttimeout * 1000);
+ // we do not need to strdup these, smbc_setXXX below will make their own copies
+ if (settings->GetString(CSettings::SETTING_SMB_WORKGROUP).length() > 0)
+ //! @bug libsmbclient < 4.9 isn't const correct
+ smbc_setWorkgroup(m_context, const_cast<char*>(settings->GetString(CSettings::SETTING_SMB_WORKGROUP).c_str()));
+ std::string guest = "guest";
+ //! @bug libsmbclient < 4.8 isn't const correct
+ smbc_setUser(m_context, const_cast<char*>(guest.c_str()));
+#else
+ m_context->debug = (CServiceBroker::GetLogging().CanLogComponent(LOGSAMBA) ? 10 : 0);
+ m_context->callbacks.auth_fn = xb_smbc_auth;
+ orig_cache = m_context->callbacks.get_cached_srv_fn;
+ m_context->callbacks.get_cached_srv_fn = xb_smbc_cache;
+ m_context->options.one_share_per_server = false;
+ m_context->options.browse_max_lmb_count = 0;
+ m_context->timeout = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_sambaclienttimeout * 1000;
+ // we need to strdup these, they will get free'd on smbc_free_context
+ if (settings->GetString(CSettings::SETTING_SMB_WORKGROUP).length() > 0)
+ m_context->workgroup = strdup(settings->GetString(CSettings::SETTING_SMB_WORKGROUP).c_str());
+ m_context->user = strdup("guest");
+#endif
+
+ // initialize samba and do some hacking into the settings
+ if (smbc_init_context(m_context))
+ {
+ // setup context using the smb old interface compatibility
+ SMBCCTX *old_context = smbc_set_context(m_context);
+ // free previous context or we leak it, this comes from smbc_init above.
+ // there is a bug in smbclient (old interface), if we init/set a context
+ // then set(null)/free it in DeInit above, the next smbc_set_context
+ // return the already freed previous context, free again and bang, crash.
+ // so we setup a stic bool to track the first init so we can free the
+ // context associated with the initial smbc_init.
+ if (old_context && IsFirstInit)
+ {
+ smbc_free_context(old_context, 1);
+ IsFirstInit = false;
+ }
+ }
+ else
+ {
+ smbc_free_context(m_context, 1);
+ m_context = NULL;
+ }
+ }
+ m_IdleTimeout = 180;
+}
+
+std::string CSMB::URLEncode(const CURL &url)
+{
+ /* due to smb wanting encoded urls we have to build it manually */
+
+ std::string flat = "smb://";
+
+ /* samba messes up of password is set but no username is set. don't know why yet */
+ /* probably the url parser that goes crazy */
+ if(url.GetUserName().length() > 0 /* || url.GetPassWord().length() > 0 */)
+ {
+ if(!url.GetDomain().empty())
+ {
+ flat += URLEncode(url.GetDomain());
+ flat += ";";
+ }
+ flat += URLEncode(url.GetUserName());
+ if(url.GetPassWord().length() > 0)
+ {
+ flat += ":";
+ flat += URLEncode(url.GetPassWord());
+ }
+ flat += "@";
+ }
+ flat += URLEncode(url.GetHostName());
+
+ if (url.HasPort())
+ {
+ flat += StringUtils::Format(":{}", url.GetPort());
+ }
+
+ /* okey sadly since a slash is an invalid name we have to tokenize */
+ std::vector<std::string> parts;
+ StringUtils::Tokenize(url.GetFileName(), parts, "/");
+ for (const std::string& it : parts)
+ {
+ flat += "/";
+ flat += URLEncode((it));
+ }
+
+ /* okey options should go here, thou current samba doesn't support any */
+
+ return flat;
+}
+
+std::string CSMB::URLEncode(const std::string &value)
+{
+ return CURL::Encode(value);
+}
+
+/* This is called from CApplication::ProcessSlow() and is used to tell if smbclient have been idle for too long */
+void CSMB::CheckIfIdle()
+{
+/* We check if there are open connections. This is done without a lock to not halt the mainthread. It should be thread safe as
+ worst case scenario is that m_OpenConnections could read 0 and then changed to 1 if this happens it will enter the if which will lead to another check, which is locked. */
+ if (m_OpenConnections == 0)
+ { /* I've set the the maximum IDLE time to be 1 min and 30 sec. */
+ std::unique_lock<CCriticalSection> lock(*this);
+ if (m_OpenConnections == 0 /* check again - when locked */ && m_context != NULL)
+ {
+ if (m_IdleTimeout > 0)
+ {
+ m_IdleTimeout--;
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "Samba is idle. Closing the remaining connections");
+ smb.Deinit();
+ }
+ }
+ }
+}
+
+void CSMB::SetActivityTime()
+{
+ /* Since we get called every 500ms from ProcessSlow we limit the tick count to 180 */
+ /* That means we have 2 ticks per second which equals 180/2 == 90 seconds */
+ m_IdleTimeout = 180;
+}
+
+/* The following two function is used to keep track on how many Opened files/directories there are.
+ This makes the idle timer not count if a movie is paused for example */
+void CSMB::AddActiveConnection()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ m_OpenConnections++;
+}
+void CSMB::AddIdleConnection()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ m_OpenConnections--;
+ /* If we close a file we reset the idle timer so that we don't have any weird behaviours if a user
+ leaves the movie paused for a long while and then press stop */
+ m_IdleTimeout = 180;
+}
+
+CURL CSMB::GetResolvedUrl(const CURL& url)
+{
+ CURL tmpUrl(url);
+ std::string resolvedHostName;
+
+ if (CDNSNameCache::Lookup(tmpUrl.GetHostName(), resolvedHostName))
+ tmpUrl.SetHostName(resolvedHostName);
+
+ return tmpUrl;
+}
+
+CSMB smb;
+
+CSMBFile::CSMBFile()
+{
+ smb.Init();
+ m_fd = -1;
+ smb.AddActiveConnection();
+ m_allowRetry = true;
+}
+
+CSMBFile::~CSMBFile()
+{
+ Close();
+ smb.AddIdleConnection();
+}
+
+int64_t CSMBFile::GetPosition()
+{
+ if (m_fd == -1)
+ return -1;
+ std::unique_lock<CCriticalSection> lock(smb);
+ if (!smb.IsSmbValid())
+ return -1;
+ return smbc_lseek(m_fd, 0, SEEK_CUR);
+}
+
+int64_t CSMBFile::GetLength()
+{
+ if (m_fd == -1)
+ return -1;
+ return m_fileSize;
+}
+
+bool CSMBFile::Open(const CURL& url)
+{
+ Close();
+
+ // we can't open files like smb://file.f or smb://server/file.f
+ // if a file matches the if below return false, it can't exist on a samba share.
+ if (!IsValidFile(url.GetFileName()))
+ {
+ CLog::Log(LOGINFO, "SMBFile->Open: Bad URL : '{}'", url.GetRedacted());
+ return false;
+ }
+ m_url = url;
+
+ // opening a file to another computer share will create a new session
+ // when opening smb://server xbms will try to find folder.jpg in all shares
+ // listed, which will create lot's of open sessions.
+
+ std::string strFileName;
+ m_fd = OpenFile(url, strFileName);
+
+ CLog::Log(LOGDEBUG, "CSMBFile::Open - opened {}, fd={}", url.GetRedacted(), m_fd);
+ if (m_fd == -1)
+ {
+ // write error to logfile
+ CLog::Log(LOGINFO, "SMBFile->Open: Unable to open file : '{}'\nunix_err:'{:x}' error : '{}'",
+ CURL::GetRedacted(strFileName), errno, strerror(errno));
+ return false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(smb);
+ if (!smb.IsSmbValid())
+ return false;
+ struct stat tmpBuffer;
+ if (smbc_stat(strFileName.c_str(), &tmpBuffer) < 0)
+ {
+ smbc_close(m_fd);
+ m_fd = -1;
+ return false;
+ }
+
+ m_fileSize = tmpBuffer.st_size;
+
+ int64_t ret = smbc_lseek(m_fd, 0, SEEK_SET);
+ if ( ret < 0 )
+ {
+ smbc_close(m_fd);
+ m_fd = -1;
+ return false;
+ }
+ // We've successfully opened the file!
+ return true;
+}
+
+
+/// \brief Checks authentication against SAMBA share. Reads password cache created in CSMBDirectory::OpenDir().
+/// \param strAuth The SMB style path
+/// \return SMB file descriptor
+/*
+int CSMBFile::OpenFile(std::string& strAuth)
+{
+ int fd = -1;
+
+ std::string strPath = g_passwordManager.GetSMBAuthFilename(strAuth);
+
+ fd = smbc_open(strPath.c_str(), O_RDONLY, 0);
+ //! @todo Run a loop here that prompts for our username/password as appropriate?
+ //! We have the ability to run a file (eg from a button action) without browsing to
+ //! the directory first. In the case of a password protected share that we do
+ //! not have the authentication information for, the above smbc_open() will have
+ //! returned negative, and the file will not be opened. While this is not a particular
+ //! likely scenario, we might want to implement prompting for the password in this case.
+ //! The code from SMBDirectory can be used for this.
+ if(fd >= 0)
+ strAuth = strPath;
+
+ return fd;
+}
+*/
+
+int CSMBFile::OpenFile(const CURL &url, std::string& strAuth)
+{
+ int fd = -1;
+ smb.Init();
+
+ strAuth = GetAuthenticatedPath(CSMB::GetResolvedUrl(url));
+ std::string strPath = strAuth;
+
+ {
+ std::unique_lock<CCriticalSection> lock(smb);
+ if (smb.IsSmbValid())
+ fd = smbc_open(strPath.c_str(), O_RDONLY, 0);
+ }
+
+ if (fd >= 0)
+ strAuth = strPath;
+
+ return fd;
+}
+
+bool CSMBFile::Exists(const CURL& url)
+{
+ // we can't open files like smb://file.f or smb://server/file.f
+ // if a file matches the if below return false, it can't exist on a samba share.
+ if (!IsValidFile(url.GetFileName())) return false;
+
+ smb.Init();
+ std::string strFileName = GetAuthenticatedPath(CSMB::GetResolvedUrl(url));
+
+ struct stat info;
+
+ std::unique_lock<CCriticalSection> lock(smb);
+ if (!smb.IsSmbValid())
+ return false;
+ int iResult = smbc_stat(strFileName.c_str(), &info);
+
+ if (iResult < 0) return false;
+ return true;
+}
+
+int CSMBFile::Stat(struct __stat64* buffer)
+{
+ if (m_fd == -1)
+ return -1;
+
+ struct stat tmpBuffer = {};
+
+ std::unique_lock<CCriticalSection> lock(smb);
+ if (!smb.IsSmbValid())
+ return -1;
+ int iResult = smbc_fstat(m_fd, &tmpBuffer);
+ CUtil::StatToStat64(buffer, &tmpBuffer);
+ return iResult;
+}
+
+int CSMBFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ smb.Init();
+ std::string strFileName = GetAuthenticatedPath(CSMB::GetResolvedUrl(url));
+ std::unique_lock<CCriticalSection> lock(smb);
+
+ if (!smb.IsSmbValid())
+ return -1;
+ struct stat tmpBuffer = {};
+ int iResult = smbc_stat(strFileName.c_str(), &tmpBuffer);
+ CUtil::StatToStat64(buffer, &tmpBuffer);
+ return iResult;
+}
+
+int CSMBFile::Truncate(int64_t size)
+{
+ if (m_fd == -1) return 0;
+ /*
+ * This would force us to be dependant on SMBv3.2 which is GPLv3
+ * This is only used by the TagLib writers, which are not currently in use
+ * So log and warn until we implement TagLib writing & can re-implement this better.
+ std::unique_lock<CCriticalSection> lock(smb); // Init not called since it has to be "inited" by now
+
+#if defined(TARGET_ANDROID)
+ int iResult = 0;
+#else
+ int iResult = smbc_ftruncate(m_fd, size);
+#endif
+*/
+ CLog::Log(LOGWARNING, "{} - Warning(smbc_ftruncate called and not implemented)", __FUNCTION__);
+ return 0;
+}
+
+ssize_t CSMBFile::Read(void *lpBuf, size_t uiBufSize)
+{
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+ if (m_fd == -1)
+ return -1;
+
+ // Some external libs (libass) use test read with zero size and
+ // null buffer pointer to check whether file is readable, but
+ // libsmbclient always return "-1" if called with null buffer
+ // regardless of buffer size.
+ // To overcome this, force return "0" in that case.
+ if (uiBufSize == 0 && lpBuf == NULL)
+ return 0;
+
+ std::unique_lock<CCriticalSection> lock(
+ smb); // Init not called since it has to be "inited" by now
+ if (!smb.IsSmbValid())
+ return -1;
+ smb.SetActivityTime();
+
+ ssize_t bytesRead = smbc_read(m_fd, lpBuf, (int)uiBufSize);
+
+ if (m_allowRetry && bytesRead < 0 && errno == EINVAL )
+ {
+ CLog::Log(LOGERROR, "{} - Error( {}, {}, {} ) - Retrying", __FUNCTION__, bytesRead, errno,
+ strerror(errno));
+ bytesRead = smbc_read(m_fd, lpBuf, (int)uiBufSize);
+ }
+
+ if ( bytesRead < 0 )
+ CLog::Log(LOGERROR, "{} - Error( {}, {}, {} )", __FUNCTION__, bytesRead, errno,
+ strerror(errno));
+
+ return bytesRead;
+}
+
+int64_t CSMBFile::Seek(int64_t iFilePosition, int iWhence)
+{
+ if (m_fd == -1) return -1;
+
+ std::unique_lock<CCriticalSection> lock(
+ smb); // Init not called since it has to be "inited" by now
+ if (!smb.IsSmbValid())
+ return -1;
+ smb.SetActivityTime();
+ int64_t pos = smbc_lseek(m_fd, iFilePosition, iWhence);
+
+ if ( pos < 0 )
+ {
+ CLog::Log(LOGERROR, "{} - Error( {}, {}, {} )", __FUNCTION__, pos, errno, strerror(errno));
+ return -1;
+ }
+
+ return pos;
+}
+
+void CSMBFile::Close()
+{
+ if (m_fd != -1)
+ {
+ CLog::Log(LOGDEBUG, "CSMBFile::Close closing fd {}", m_fd);
+ std::unique_lock<CCriticalSection> lock(smb);
+ if (!smb.IsSmbValid())
+ return;
+ smbc_close(m_fd);
+ }
+ m_fd = -1;
+}
+
+ssize_t CSMBFile::Write(const void* lpBuf, size_t uiBufSize)
+{
+ if (m_fd == -1) return -1;
+
+ // lpBuf can be safely casted to void* since xbmc_write will only read from it.
+ std::unique_lock<CCriticalSection> lock(smb);
+ if (!smb.IsSmbValid())
+ return -1;
+
+ return smbc_write(m_fd, lpBuf, uiBufSize);
+}
+
+bool CSMBFile::Delete(const CURL& url)
+{
+ smb.Init();
+ std::string strFile = GetAuthenticatedPath(CSMB::GetResolvedUrl(url));
+
+ std::unique_lock<CCriticalSection> lock(smb);
+ if (!smb.IsSmbValid())
+ return false;
+
+ int result = smbc_unlink(strFile.c_str());
+
+ if(result != 0)
+ CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__, strerror(errno));
+
+ return (result == 0);
+}
+
+bool CSMBFile::Rename(const CURL& url, const CURL& urlnew)
+{
+ smb.Init();
+ std::string strFile = GetAuthenticatedPath(CSMB::GetResolvedUrl(url));
+ std::string strFileNew = GetAuthenticatedPath(CSMB::GetResolvedUrl(urlnew));
+ std::unique_lock<CCriticalSection> lock(smb);
+ if (!smb.IsSmbValid())
+ return false;
+
+ int result = smbc_rename(strFile.c_str(), strFileNew.c_str());
+
+ if(result != 0)
+ CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__, strerror(errno));
+
+ return (result == 0);
+}
+
+bool CSMBFile::OpenForWrite(const CURL& url, bool bOverWrite)
+{
+ m_fileSize = 0;
+
+ Close();
+
+ // we can't open files like smb://file.f or smb://server/file.f
+ // if a file matches the if below return false, it can't exist on a samba share.
+ if (!IsValidFile(url.GetFileName())) return false;
+
+ std::string strFileName = GetAuthenticatedPath(CSMB::GetResolvedUrl(url));
+ std::unique_lock<CCriticalSection> lock(smb);
+ if (!smb.IsSmbValid())
+ return false;
+
+ if (bOverWrite)
+ {
+ CLog::Log(LOGWARNING, "SMBFile::OpenForWrite() called with overwriting enabled! - {}",
+ CURL::GetRedacted(strFileName));
+ m_fd = smbc_creat(strFileName.c_str(), 0);
+ }
+ else
+ {
+ m_fd = smbc_open(strFileName.c_str(), O_RDWR, 0);
+ }
+
+ if (m_fd == -1)
+ {
+ // write error to logfile
+ CLog::Log(LOGERROR, "SMBFile->Open: Unable to open file : '{}'\nunix_err:'{:x}' error : '{}'",
+ CURL::GetRedacted(strFileName), errno, strerror(errno));
+ return false;
+ }
+
+ // We've successfully opened the file!
+ return true;
+}
+
+bool CSMBFile::IsValidFile(const std::string& strFileName)
+{
+ if (strFileName.find('/') == std::string::npos || /* doesn't have sharename */
+ StringUtils::EndsWith(strFileName, "/.") || /* not current folder */
+ StringUtils::EndsWith(strFileName, "/..")) /* not parent folder */
+ return false;
+ return true;
+}
+
+std::string CSMBFile::GetAuthenticatedPath(const CURL &url)
+{
+ CURL authURL(CSMB::GetResolvedUrl(url));
+ CPasswordManager::GetInstance().AuthenticateURL(authURL);
+ return smb.URLEncode(authURL);
+}
+
+int CSMBFile::IoControl(EIoControl request, void* param)
+{
+ if (request == IOCTRL_SEEK_POSSIBLE)
+ return 1;
+
+ if (request == IOCTRL_SET_RETRY)
+ {
+ m_allowRetry = *(bool*) param;
+ return 0;
+ }
+
+ return -1;
+}
+
diff --git a/xbmc/platform/posix/filesystem/SMBFile.h b/xbmc/platform/posix/filesystem/SMBFile.h
new file mode 100644
index 0000000..8cc960e
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/SMBFile.h
@@ -0,0 +1,93 @@
+/*
+ * 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
+
+// SMBFile.h: interface for the CSMBFile class.
+
+//
+
+//////////////////////////////////////////////////////////////////////
+
+
+#include "URL.h"
+#include "filesystem/IFile.h"
+#include "threads/CriticalSection.h"
+
+#define NT_STATUS_CONNECTION_REFUSED long(0xC0000000 | 0x0236)
+#define NT_STATUS_INVALID_HANDLE long(0xC0000000 | 0x0008)
+#define NT_STATUS_ACCESS_DENIED long(0xC0000000 | 0x0022)
+#define NT_STATUS_OBJECT_NAME_NOT_FOUND long(0xC0000000 | 0x0034)
+#define NT_STATUS_INVALID_COMPUTER_NAME long(0xC0000000 | 0x0122)
+
+struct _SMBCCTX;
+typedef _SMBCCTX SMBCCTX;
+
+class CSMB : public CCriticalSection
+{
+public:
+ CSMB();
+ ~CSMB();
+ void Init();
+ void Deinit();
+ /* Makes sense to be called after acquiring the lock */
+ bool IsSmbValid() const { return m_context != nullptr; }
+ void CheckIfIdle();
+ void SetActivityTime();
+ void AddActiveConnection();
+ void AddIdleConnection();
+ std::string URLEncode(const std::string &value);
+ std::string URLEncode(const CURL &url);
+
+ DWORD ConvertUnixToNT(int error);
+ static CURL GetResolvedUrl(const CURL& url);
+
+private:
+ SMBCCTX *m_context;
+ int m_OpenConnections;
+ unsigned int m_IdleTimeout;
+ static bool IsFirstInit;
+};
+
+extern CSMB smb;
+
+namespace XFILE
+{
+class CSMBFile : public IFile
+{
+public:
+ CSMBFile();
+ int OpenFile(const CURL &url, std::string& strAuth);
+ ~CSMBFile() override;
+ void Close() override;
+ int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override;
+ ssize_t Read(void* lpBuf, size_t uiBufSize) override;
+ 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;
+ int Truncate(int64_t size) override;
+ int64_t GetLength() override;
+ int64_t GetPosition() override;
+ ssize_t Write(const void* lpBuf, size_t uiBufSize) 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;
+ int GetChunkSize() override { return 64*1024; }
+ int IoControl(EIoControl request, void* param) override;
+
+protected:
+ CURL m_url;
+ bool IsValidFile(const std::string& strFileName);
+ std::string GetAuthenticatedPath(const CURL &url);
+ int64_t m_fileSize;
+ int m_fd;
+ bool m_allowRetry;
+};
+}
diff --git a/xbmc/platform/posix/filesystem/SMBWSDiscovery.cpp b/xbmc/platform/posix/filesystem/SMBWSDiscovery.cpp
new file mode 100644
index 0000000..8bab0aa
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/SMBWSDiscovery.cpp
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2021- 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 "SMBWSDiscovery.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "network/IWSDiscovery.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include "platform/posix/filesystem/SMBWSDiscoveryListener.h"
+
+#include <algorithm>
+#include <chrono>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace std::chrono;
+using namespace WSDiscovery;
+
+namespace WSDiscovery
+{
+std::unique_ptr<IWSDiscovery> IWSDiscovery::GetInstance()
+{
+ return std::make_unique<WSDiscovery::CWSDiscoveryPosix>();
+}
+
+std::atomic<bool> CWSDiscoveryPosix::m_isInitialized{false};
+
+CWSDiscoveryPosix::CWSDiscoveryPosix()
+{
+ // Set our wsd_instance ID to seconds since epoch
+ auto epochduration = system_clock::now().time_since_epoch();
+ wsd_instance_id = epochduration.count() * system_clock::period::num / system_clock::period::den;
+
+ m_WSDListenerUDP = std::make_unique<CWSDiscoveryListenerUDP>();
+ m_isInitialized = true;
+}
+
+CWSDiscoveryPosix::~CWSDiscoveryPosix()
+{
+ StopServices();
+}
+
+bool CWSDiscoveryPosix::StopServices()
+{
+ m_WSDListenerUDP->Stop();
+
+ return true;
+}
+
+bool CWSDiscoveryPosix::StartServices()
+{
+ m_WSDListenerUDP->Start();
+
+ return true;
+}
+
+bool CWSDiscoveryPosix::IsRunning()
+{
+ return m_WSDListenerUDP->IsRunning();
+}
+
+bool CWSDiscoveryPosix::GetServerList(CFileItemList& items)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critWSD);
+
+ for (const auto& item : m_vecWSDInfo)
+ {
+ auto found = item.computer.find('/');
+ std::string host;
+ if (found != std::string::npos)
+ host = item.computer.substr(0, found);
+ else
+ {
+ if (item.xaddrs_host.empty())
+ continue;
+ host = item.xaddrs_host;
+ }
+
+ CFileItemPtr pItem = std::make_shared<CFileItem>(host);
+ pItem->SetPath("smb://" + host + '/');
+ pItem->m_bIsFolder = true;
+ items.Add(pItem);
+ }
+ }
+ return true;
+}
+
+bool CWSDiscoveryPosix::GetCached(const std::string& strHostName, std::string& strIpAddress)
+{
+ const std::string match = strHostName + "/";
+
+ std::unique_lock<CCriticalSection> lock(m_critWSD);
+ for (const auto& item : m_vecWSDInfo)
+ {
+ if (!item.computer.empty() && StringUtils::StartsWithNoCase(item.computer, match))
+ {
+ strIpAddress = item.xaddrs_host;
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY, "CWSDiscoveryPosix::Lookup - {} -> {}", strHostName,
+ strIpAddress);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CWSDiscoveryPosix::SetItems(std::vector<wsd_req_info> entries)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critWSD);
+ m_vecWSDInfo = std::move(entries);
+ }
+}
+} // namespace WSDiscovery
diff --git a/xbmc/platform/posix/filesystem/SMBWSDiscovery.h b/xbmc/platform/posix/filesystem/SMBWSDiscovery.h
new file mode 100644
index 0000000..139ff11
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/SMBWSDiscovery.h
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2021- 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 "network/IWSDiscovery.h"
+#include "threads/CriticalSection.h"
+#include "threads/SingleLock.h"
+
+#include "platform/posix/filesystem/SMBWSDiscoveryListener.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItemList;
+
+namespace WSDiscovery
+{
+class CWSDiscoveryListenerUDP;
+}
+
+namespace WSDiscovery
+{
+struct wsd_req_info
+{
+ std::string action; // ToDo: Action probably isnt required to be stored
+ std::string msgid;
+ std::string types; // ToDo: Types may not be needed.
+ std::string address;
+ std::string xaddrs;
+ std::string xaddrs_host;
+ std::string computer;
+
+ bool operator==(const wsd_req_info& item) const
+ {
+ return ((item.xaddrs == xaddrs) && (item.address == address));
+ }
+};
+
+class CWSDiscoveryPosix : public WSDiscovery::IWSDiscovery
+{
+public:
+ CWSDiscoveryPosix();
+
+ // IWSDiscovery interface methods
+ ~CWSDiscoveryPosix() override;
+ bool StartServices() override;
+ bool StopServices() override;
+ bool IsRunning() override;
+
+ /*
+ * Get List of smb servers found by WSD
+ * out (CFileItemList&) List of fileitems populated with smb addresses
+ * return (bool) true if >0 WSD addresses found
+ */
+ bool GetServerList(CFileItemList& items);
+
+ long long GetInstanceID() const { return wsd_instance_id; }
+
+ /*
+ * Set List of WSD info request
+ * out (vector<wsd_req_info>) vector of WSD responses received
+ * return void
+ */
+ void SetItems(std::vector<WSDiscovery::wsd_req_info> entries);
+
+ /*
+ * Lookup host name in collected ws-discovery data
+ * in (const std::string&) Host name
+ * out (std::string&) IP address if found
+ * return (bool) true if found
+ */
+ bool GetCached(const std::string& strHostName, std::string& strIpAddress);
+
+ static bool IsInitialized() { return m_isInitialized; }
+
+private:
+ CCriticalSection m_critWSD;
+
+ /*
+ * As per docs - Pg32 - http://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf
+ * MUST be incremented by >= 1 each time the service has gone down, lost state,
+ * and came back up again. SHOULD NOT be incremented otherwise. Means to set
+ * this value include, but are not limited to:
+ * • A counter that is incremented on each 'cold' boot
+ * • The boot time of the service, expressed as seconds elapsed since midnight
+ * January 1, 1970
+ *
+ * Our implementation will only set this on creation of the class
+ * We arent providing services to clients, so this should be ok.
+ */
+ long long wsd_instance_id;
+
+ // WSD UDP daemon
+ std::unique_ptr<WSDiscovery::CWSDiscoveryListenerUDP> m_WSDListenerUDP;
+
+ std::vector<WSDiscovery::wsd_req_info> m_vecWSDInfo;
+
+ static std::atomic<bool> m_isInitialized;
+};
+} // namespace WSDiscovery
diff --git a/xbmc/platform/posix/filesystem/SMBWSDiscoveryListener.cpp b/xbmc/platform/posix/filesystem/SMBWSDiscoveryListener.cpp
new file mode 100644
index 0000000..d42c19b
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/SMBWSDiscoveryListener.cpp
@@ -0,0 +1,608 @@
+/*
+ * Copyright (C) 2021- 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 "SMBWSDiscoveryListener.h"
+
+#include "ServiceBroker.h"
+#include "filesystem/CurlFile.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include "platform/posix/filesystem/SMBWSDiscovery.h"
+
+#include <array>
+#include <chrono>
+#include <mutex>
+#include <stdio.h>
+#include <string>
+#include <utility>
+
+#include <arpa/inet.h>
+#include <fmt/format.h>
+#include <sys/select.h>
+#include <unistd.h>
+
+using namespace WSDiscovery;
+
+namespace WSDiscovery
+{
+
+// Templates for SOAPXML messages for WS-Discovery messages
+static const std::string soap_msg_templ =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ "<soap:Envelope "
+ "xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\" "
+ "xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" "
+ "xmlns:wsd=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" "
+ "xmlns:wsx=\"http://schemas.xmlsoap.org/ws/2004/09/mex\" "
+ "xmlns:wsdp=\"http://schemas.xmlsoap.org/ws/2006/02/devprof\" "
+ "xmlns:un0=\"http://schemas.microsoft.com/windows/pnpx/2005/10\" "
+ "xmlns:pub=\"http://schemas.microsoft.com/windows/pub/2005/07\">\n"
+ "<soap:Header>\n"
+ "<wsa:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>\n"
+ "<wsa:Action>{}</wsa:Action>\n"
+ "<wsa:MessageID>urn:uuid:{}</wsa:MessageID>\n"
+ "<wsd:AppSequence InstanceId=\"{}\" MessageNumber=\"{}\" />\n"
+ "{}"
+ "</soap:Header>\n"
+ "{}"
+ "</soap:Envelope>\n";
+
+static const std::string hello_body = "<soap:Body>\n"
+ "<wsd:Hello>\n"
+ "<wsa:EndpointReference>\n"
+ "<wsa:Address>urn:uuid:{}</wsa:Address>\n"
+ "</wsa:EndpointReference>\n"
+ "<wsd:Types>wsdp:Device pub:Computer</wsd:Types>\n"
+ "<wsd:MetadataVersion>1</wsd:MetadataVersion>\n"
+ "</wsd:Hello>\n"
+ "</soap:Body>\n";
+
+static const std::string bye_body = "<soap:Body>\n"
+ "<wsd:Bye>\n"
+ "<wsa:EndpointReference>\n"
+ "<wsa:Address>urn:uuid:{}</wsa:Address>\n"
+ "</wsa:EndpointReference>\n"
+ "<wsd:Types>wsdp:Device pub:Computer</wsd:Types>\n"
+ "<wsd:MetadataVersion>2</wsd:MetadataVersion>\n"
+ "</wsd:Bye>\n"
+ "</soap:Body>\n";
+
+static const std::string probe_body = "<soap:Body>\n"
+ "<wsd:Probe>\n"
+ "<wsd:Types>wsdp:Device</wsd:Types>\n"
+ "</wsd:Probe>\n"
+ "</soap:Body>\n";
+
+static const std::string resolve_body = "<soap:Body>\n"
+ "<wsd:Resolve>\n"
+ "<wsa:EndpointReference>\n"
+ "<wsa:Address>"
+ "{}"
+ "</wsa:Address>\n"
+ "</wsa:EndpointReference>\n"
+ "</wsd:Resolve>\n"
+ "</soap:Body>\n";
+
+// These are the only actions we concern ourselves with for our WS-D implementation
+static const std::string WSD_ACT_HELLO = "http://schemas.xmlsoap.org/ws/2005/04/discovery/Hello";
+static const std::string WSD_ACT_BYE = "http://schemas.xmlsoap.org/ws/2005/04/discovery/Bye";
+static const std::string WSD_ACT_PROBEMATCH =
+ "http://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches";
+static const std::string WSD_ACT_PROBE = "http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe";
+static const std::string WSD_ACT_RESOLVE =
+ "http://schemas.xmlsoap.org/ws/2005/04/discovery/Resolve";
+static const std::string WSD_ACT_RESOLVEMATCHES =
+ "http://schemas.xmlsoap.org/ws/2005/04/discovery/ResolveMatches";
+
+
+// These are xml start/finish tags we need info from
+static const std::array<std::pair<std::string, std::string>, 2> action_tag{
+ {{"<wsa:Action>", "</wsa:Action>"},
+ {"<wsa:Action SOAP-ENV:mustUnderstand=\"true\">", "</wsa:Action>"}}};
+
+static const std::array<std::pair<std::string, std::string>, 2> msgid_tag{
+ {{"<wsa:MessageID>", "</wsa:MessageID>"},
+ {"<wsa:MessageID SOAP-ENV:mustUnderstand=\"true\">", "</wsa:MessageID>"}}};
+
+static const std::array<std::pair<std::string, std::string>, 1> xaddrs_tag{
+ {{"<wsd:XAddrs>", "</wsd:XAddrs>"}}};
+
+static const std::array<std::pair<std::string, std::string>, 1> address_tag{
+ {{"<wsa:Address>", "</wsa:Address>"}}};
+
+static const std::array<std::pair<std::string, std::string>, 1> types_tag{
+ {{"<wsd:Types>", "</wsd:Types>"}}};
+
+static const std::string get_msg =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ "<soap:Envelope "
+ "xmlns:pnpx=\"http://schemas.microsoft.com/windows/pnpx/2005/10\" "
+ "xmlns:pub=\"http://schemas.microsoft.com/windows/pub/2005/07\" "
+ "xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\" "
+ "xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" "
+ "xmlns:wsd=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" "
+ "xmlns:wsdp=\"http://schemas.xmlsoap.org/ws/2006/02/devprof\" "
+ "xmlns:wsx=\"http://schemas.xmlsoap.org/ws/2004/09/mex\"> "
+ "<soap:Header> "
+ "<wsa:To>{}</wsa:To> "
+ "<wsa:Action>http://schemas.xmlsoap.org/ws/2004/09/transfer/Get</wsa:Action> "
+ "<wsa:MessageID>urn:uuid:{}</wsa:MessageID> "
+ "<wsa:ReplyTo> "
+ "<wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address> "
+ "</wsa:ReplyTo> "
+ "<wsa:From> "
+ "<wsa:Address>urn:uuid:{}</wsa:Address> "
+ "</wsa:From> "
+ "</soap:Header> "
+ "<soap:Body /> "
+ "</soap:Envelope>";
+
+static const std::array<std::pair<std::string, std::string>, 1> computer_tag{
+ {{"<pub:Computer>", "</pub:Computer>"}}};
+
+CWSDiscoveryListenerUDP::CWSDiscoveryListenerUDP()
+ : CThread("WSDiscoveryListenerUDP"), wsd_instance_address(StringUtils::CreateUUID())
+{
+}
+
+CWSDiscoveryListenerUDP::~CWSDiscoveryListenerUDP()
+{
+}
+
+void CWSDiscoveryListenerUDP::Stop()
+{
+ StopThread(true);
+ CLog::Log(LOGINFO, "CWSDiscoveryListenerUDP::Stop - Stopped");
+}
+
+void CWSDiscoveryListenerUDP::Start()
+{
+ if (IsRunning())
+ return;
+
+ // Clear existing data. We dont know how long service has been down, so we may not
+ // have correct services that may have sent BYE actions
+ m_vecWSDInfo.clear();
+ CWSDiscoveryPosix& WSInstance =
+ dynamic_cast<CWSDiscoveryPosix&>(CServiceBroker::GetWSDiscovery());
+ WSInstance.SetItems(m_vecWSDInfo);
+
+ // Create thread and set low priority
+ Create();
+ SetPriority(ThreadPriority::LOWEST);
+ CLog::Log(LOGINFO, "CWSDiscoveryListenerUDP::Start - Started");
+}
+
+void CWSDiscoveryListenerUDP::Process()
+{
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd < 0)
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::Process - Socket Creation failed");
+ return;
+ }
+
+ // set socket reuse
+ int enable = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char*)&enable, sizeof(enable)) < 0)
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY, "CWSDiscoveryListenerUDP::Process - Reuse Option failed");
+ Cleanup(true);
+ return;
+ }
+
+ // set up destination address
+ struct sockaddr_in addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_addr.s_addr = htonl(INADDR_ANY);
+ addr.sin_port = htons(wsdUDP);
+
+ // bind to receive address
+ if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY, "CWSDiscoveryListenerUDP::Process - Bind failed");
+ Cleanup(true);
+ return;
+ }
+
+ // use setsockopt() to request join a multicast group on all interfaces
+ // maybe use firstconnected?
+ struct ip_mreq mreq;
+ mreq.imr_multiaddr.s_addr = inet_addr(WDSIPv4MultiGroup);
+ mreq.imr_interface.s_addr = htonl(INADDR_ANY);
+ if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mreq, sizeof(mreq)) < 0)
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::Process - Multicast Option failed");
+ Cleanup(true);
+ return;
+ }
+
+ // Disable receiving broadcast messages on loopback
+ // So we dont receive messages we send.
+ int disable = 0;
+ if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (char*)&disable, sizeof(disable)) < 0)
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::Process - Loopback Disable Option failed");
+ Cleanup(true);
+ return;
+ }
+
+ std::string bufferoutput;
+ fd_set rset;
+ int nready;
+ struct timeval timeout;
+
+ FD_ZERO(&rset);
+
+ // Send HELLO to the world
+ AddCommand(WSD_ACT_HELLO);
+ DispatchCommand();
+
+ AddCommand(WSD_ACT_PROBE);
+ DispatchCommand();
+
+ while (!m_bStop)
+ {
+ FD_SET(fd, &rset);
+ timeout.tv_sec = 3;
+ timeout.tv_usec = 0;
+ nready = select((fd + 1), &rset, NULL, NULL, &timeout);
+
+ if (nready < 0)
+ break;
+
+ if (m_bStop)
+ break;
+
+ // if udp socket is readable receive the message.
+ if (FD_ISSET(fd, &rset))
+ {
+ bufferoutput = "";
+ char msgbuf[UDPBUFFSIZE];
+ socklen_t addrlen = sizeof(addr);
+ int nbytes = recvfrom(fd, msgbuf, UDPBUFFSIZE, 0, (struct sockaddr*)&addr, &addrlen);
+ msgbuf[nbytes] = '\0';
+ // turn msgbuf into std::string
+ bufferoutput.append(msgbuf, nbytes);
+
+ ParseBuffer(bufferoutput);
+ }
+ // Action any commands queued
+ while (DispatchCommand())
+ {
+ }
+ }
+
+ // Be a nice citizen and send BYE to the world
+ AddCommand(WSD_ACT_BYE);
+ DispatchCommand();
+ Cleanup(false);
+ return;
+}
+
+void CWSDiscoveryListenerUDP::Cleanup(bool aborted)
+{
+ close(fd);
+
+ if (aborted)
+ {
+ // Thread is stopping due to a failure state
+ // Update service setting to be disabled
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (!settings)
+ return;
+
+ if (settings->GetBool(CSettings::SETTING_SERVICES_WSDISCOVERY))
+ {
+ settings->SetBool(CSettings::SETTING_SERVICES_WSDISCOVERY, false);
+ settings->Save();
+ }
+ }
+}
+
+bool CWSDiscoveryListenerUDP::DispatchCommand()
+{
+ Command sendCommand;
+ {
+ std::unique_lock<CCriticalSection> lock(crit_commandqueue);
+ if (m_commandbuffer.size() <= 0)
+ return false;
+
+ auto it = m_commandbuffer.begin();
+ sendCommand = *it;
+ m_commandbuffer.erase(it);
+ }
+
+ int ret;
+
+ // As its udp, send multiple messages due to unreliability
+ // Windows seems to send 4-6 times for reference
+ for (int i = 0; i < retries; i++)
+ {
+ do
+ {
+ ret = sendto(fd, sendCommand.commandMsg.c_str(), sendCommand.commandMsg.size(), 0,
+ (struct sockaddr*)&sendCommand.address, sizeof(sendCommand.address));
+ } while (ret == -1 && !m_bStop);
+ std::chrono::seconds sec(1);
+ CThread::Sleep(sec);
+ }
+
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY, "CWSDiscoveryListenerUDP::DispatchCommand - Command sent");
+
+ return true;
+}
+
+void CWSDiscoveryListenerUDP::AddCommand(const std::string& message,
+ const std::string& extraparameter /* = "" */)
+{
+
+ std::unique_lock<CCriticalSection> lock(crit_commandqueue);
+
+ std::string msg{};
+
+ if (!buildSoapMessage(message, msg, extraparameter))
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::AddCommand - Invalid Soap Message");
+ return;
+ }
+
+ // ToDo: IPv6
+ struct sockaddr_in addr;
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(wsdUDP);
+
+ addr.sin_addr.s_addr = inet_addr(WDSIPv4MultiGroup);
+ memset(&addr.sin_zero, 0, sizeof(addr.sin_zero));
+
+ Command newCommand{{addr}, {msg}};
+
+ m_commandbuffer.push_back(newCommand);
+}
+
+void CWSDiscoveryListenerUDP::ParseBuffer(const std::string& buffer)
+{
+ wsd_req_info info;
+
+ // MUST have an action tag
+ std::string action = wsd_tag_find(buffer, action_tag);
+ if (action.empty())
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::ParseBuffer - No Action tag found");
+ return;
+ }
+
+ info.action = action;
+
+ // Only handle actions we need when received
+ if (!((action == WSD_ACT_HELLO) || (action == WSD_ACT_BYE) ||
+ (action == WSD_ACT_RESOLVEMATCHES) || (action == WSD_ACT_PROBEMATCH)))
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::ParseBuffer - Action not supported");
+ PrintWSDInfo(info);
+ return;
+ }
+
+ // MUST have a msgid tag
+ std::string msgid = wsd_tag_find(buffer, msgid_tag);
+ if (msgid.empty())
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::ParseBuffer - No msgid tag found");
+ return;
+ }
+
+ info.msgid = msgid;
+
+ std::string types = wsd_tag_find(buffer, types_tag);
+ info.types = types;
+ std::string address = wsd_tag_find(buffer, address_tag);
+ info.address = address;
+ std::string xaddrs = wsd_tag_find(buffer, xaddrs_tag);
+ info.xaddrs = xaddrs;
+
+ if (xaddrs.empty() && (action != WSD_ACT_BYE))
+ {
+ // Do a resolve against address to hopefully receive an xaddrs field=
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::ParseBuffer - Resolve Action dispatched");
+ AddCommand(WSD_ACT_RESOLVE, address);
+ // Discard this message
+ PrintWSDInfo(info);
+ return;
+ }
+
+ const std::string delim1 = "://";
+ const std::string delim2 = ":";
+ size_t found = info.xaddrs.find(delim1);
+ if (found != std::string::npos)
+ {
+ std::string tmpxaddrs = info.xaddrs.substr(found + delim1.size());
+ if (tmpxaddrs[0] != '[')
+ {
+ found = std::min(tmpxaddrs.find(delim2), tmpxaddrs.find('/'));
+ info.xaddrs_host = tmpxaddrs.substr(0, found);
+ }
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(crit_wsdqueue);
+ auto searchitem = std::find_if(m_vecWSDInfo.begin(), m_vecWSDInfo.end(),
+ [info](const wsd_req_info& item) { return item == info; });
+
+ if (searchitem == m_vecWSDInfo.end())
+ {
+ CWSDiscoveryPosix& WSInstance =
+ dynamic_cast<CWSDiscoveryPosix&>(CServiceBroker::GetWSDiscovery());
+ if (info.action != WSD_ACT_BYE)
+ {
+ // Acceptable request received, add to our server list
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::ParseBuffer - Actionable message");
+ UnicastGet(info);
+ m_vecWSDInfo.emplace_back(info);
+ WSInstance.SetItems(m_vecWSDInfo);
+ PrintWSDInfo(info);
+ return;
+ }
+ else
+ {
+ // WSD_ACT_BYE does not include an xaddrs tag
+ // We only want to match the address when receiving a WSD_ACT_BYE message to
+ // remove from our maintained list
+ auto searchbye = std::find_if(
+ m_vecWSDInfo.begin(), m_vecWSDInfo.end(),
+ [info, this](const wsd_req_info& item) { return equalsAddress(item, info); });
+ if (searchbye != m_vecWSDInfo.end())
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::ParseBuffer - BYE action -"
+ " removing entry");
+ m_vecWSDInfo.erase(searchbye);
+ PrintWSDInfo(info);
+ WSInstance.SetItems(m_vecWSDInfo);
+ return;
+ }
+ }
+ }
+ }
+
+ // Only duplicate items get this far, silently drop
+ return;
+}
+
+bool CWSDiscoveryListenerUDP::buildSoapMessage(const std::string& action,
+ std::string& msg,
+ const std::string& extraparameter)
+{
+ auto msg_uuid = StringUtils::CreateUUID();
+ std::string body;
+ std::string relatesTo; // Not implemented, may not be needed for our limited usage
+ int messagenumber = 1;
+
+ CWSDiscoveryPosix& WSInstance =
+ dynamic_cast<CWSDiscoveryPosix&>(CServiceBroker::GetWSDiscovery());
+ long long wsd_instance_id = WSInstance.GetInstanceID();
+
+ if (action == WSD_ACT_HELLO)
+ {
+ body = fmt::format(hello_body, wsd_instance_address);
+ }
+ else if (action == WSD_ACT_BYE)
+ {
+ body = fmt::format(bye_body, wsd_instance_address);
+ }
+ else if (action == WSD_ACT_PROBE)
+ {
+ body = probe_body;
+ }
+ else if (action == WSD_ACT_RESOLVE)
+ {
+ body = fmt::format(resolve_body, extraparameter);
+ }
+ if (body.empty())
+ {
+ // May lead to excessive logspam
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::buildSoapMessage unimplemented WSD_ACTION");
+ return false;
+ }
+
+ // Todo: how are fmt exception/failures handled with libfmt?
+ msg = fmt::format(soap_msg_templ, action, msg_uuid, wsd_instance_id, messagenumber, relatesTo,
+ body);
+
+ return true;
+}
+
+template<std::size_t SIZE>
+const std::string CWSDiscoveryListenerUDP::wsd_tag_find(
+ const std::string& xml, const std::array<std::pair<std::string, std::string>, SIZE>& tag)
+{
+ for (const auto& tagpair : tag)
+ {
+ std::size_t found1 = xml.find(tagpair.first);
+ if (found1 != std::string::npos)
+ {
+ std::size_t found2 = xml.find(tagpair.second);
+ if (found2 != std::string::npos)
+ {
+ return xml.substr((found1 + tagpair.first.size()),
+ (found2 - (found1 + tagpair.first.size())));
+ }
+ }
+ }
+ return "";
+}
+
+bool CWSDiscoveryListenerUDP::equalsAddress(const wsd_req_info& lhs, const wsd_req_info& rhs) const
+{
+ return lhs.address == rhs.address;
+}
+
+void CWSDiscoveryListenerUDP::PrintWSDInfo(const wsd_req_info& info)
+{
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryUtils::printstruct - message contents\n"
+ "\tAction: {}\n"
+ "\tMsgID: {}\n"
+ "\tAddress: {}\n"
+ "\tTypes: {}\n"
+ "\tXAddrs: {}\n"
+ "\tComputer: {}\n",
+ info.action, info.msgid, info.address, info.types, info.xaddrs, info.computer);
+}
+
+void CWSDiscoveryListenerUDP::UnicastGet(wsd_req_info& info)
+{
+ if (INADDR_NONE == inet_addr(info.xaddrs_host.c_str()))
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY, "CWSDiscoveryListenerUDP::UnicastGet - No IP address in {}",
+ info.xaddrs);
+ return;
+ }
+
+ std::string msg =
+ fmt::format(get_msg, info.address, StringUtils::CreateUUID(), wsd_instance_address);
+ XFILE::CCurlFile file;
+ file.SetAcceptEncoding("identity");
+ file.SetMimeType("application/soap+xml");
+ file.SetUserAgent("wsd");
+ file.SetRequestHeader("Connection", "Close");
+
+ std::string html;
+ if (!file.Post(info.xaddrs, msg, html))
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::UnicastGet - Could not fetch from {}", info.xaddrs);
+ return;
+ }
+ info.computer = wsd_tag_find(html, computer_tag);
+ if (info.computer.empty())
+ {
+ CLog::Log(LOGDEBUG, LOGWSDISCOVERY,
+ "CWSDiscoveryListenerUDP::UnicastGet - No computer tag found");
+ return;
+ }
+}
+
+} // namespace WSDiscovery
diff --git a/xbmc/platform/posix/filesystem/SMBWSDiscoveryListener.h b/xbmc/platform/posix/filesystem/SMBWSDiscoveryListener.h
new file mode 100644
index 0000000..3459973
--- /dev/null
+++ b/xbmc/platform/posix/filesystem/SMBWSDiscoveryListener.h
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2021- 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/Thread.h"
+
+#include <string>
+#include <vector>
+
+#include <netinet/in.h>
+
+namespace WSDiscovery
+{
+struct wsd_req_info;
+}
+
+namespace WSDiscovery
+{
+class CWSDiscoveryListenerUDP : public CThread
+{
+public:
+ CWSDiscoveryListenerUDP();
+ ~CWSDiscoveryListenerUDP();
+
+ void Start();
+ void Stop();
+
+protected:
+ // CThread
+ void Process() override;
+
+private:
+ struct Command
+ {
+ struct sockaddr_in address;
+ std::string commandMsg;
+ };
+
+ /*
+ * Dispatch UDP command from command Queue
+ * return (bool) true if message dispatched
+ */
+ bool DispatchCommand();
+
+ /*
+ * Build SOAPXML command and add to command Queue
+ * in (string) action type
+ * in (string) extra data field (currently used solely for resolve addresses)
+ * return (void)
+ */
+ void AddCommand(const std::string& message, const std::string& extraparameter = "");
+
+ /*
+ * Process received broadcast messages and add accepted items to
+ * in (string) SOAPXML Message
+ * return (void)
+ */
+ void ParseBuffer(const std::string& buffer);
+
+ /*
+ * Generates an XML SOAP message given a particular action type
+ * in (string) action type
+ * in/out (string) created message
+ * in (string) extra data field (currently used for resolve addresses)
+ * return (bool) true if full message crafted
+ */
+ bool buildSoapMessage(const std::string& action,
+ std::string& msg,
+ const std::string& extraparameter);
+
+ // Closes socket and handles setting state for WS-Discovery
+ void Cleanup(bool aborted);
+
+ /*
+ * Use unicast Get to request computer name
+ * in/out (wsd_req_info&) host info to be updated
+ * return (void)
+ */
+ void UnicastGet(wsd_req_info& info);
+
+private:
+ template<std::size_t SIZE>
+ const std::string wsd_tag_find(const std::string& xml,
+ const std::array<std::pair<std::string, std::string>, SIZE>& tag);
+
+ // Debug print WSD packet data
+ void PrintWSDInfo(const WSDiscovery::wsd_req_info& info);
+
+ // compare WSD entry address
+ bool equalsAddress(const WSDiscovery::wsd_req_info& lhs,
+ const WSDiscovery::wsd_req_info& rhs) const;
+
+ // Socket FD for send/recv
+ int fd;
+
+ std::vector<Command> m_commandbuffer;
+
+ CCriticalSection crit_commandqueue;
+ CCriticalSection crit_wsdqueue;
+
+ std::vector<WSDiscovery::wsd_req_info> m_vecWSDInfo;
+
+ // GUID that should remain constant for an instance
+ const std::string wsd_instance_address;
+
+ // Number of sends for UDP messages
+ const int retries = 4;
+
+ // Max udp packet size (+ UDP header + IP header overhead = 65535)
+ const int UDPBUFFSIZE = 65507;
+
+ // Port for unicast/multicast WSD traffic
+ const int wsdUDP = 3702;
+
+ // ipv4 multicast group WSD - https://specs.xmlsoap.org/ws/2005/04/discovery/ws-discovery.pdf
+ const char* WDSIPv4MultiGroup = "239.255.255.250";
+
+ // ToDo: ipv6 broadcast address
+ // const char* WDSIPv6MultiGroup = "FF02::C"
+};
+} // namespace WSDiscovery
diff --git a/xbmc/platform/posix/main.cpp b/xbmc/platform/posix/main.cpp
new file mode 100644
index 0000000..5028aaa
--- /dev/null
+++ b/xbmc/platform/posix/main.cpp
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+#if defined(TARGET_DARWIN_OSX)
+// SDL redefines main as SDL_main
+#ifdef HAS_SDL
+#include <SDL/SDL.h>
+#endif
+#endif
+
+#include "PlatformPosix.h"
+#include "application/AppEnvironment.h"
+#include "application/AppParamParser.h"
+#include "platform/xbmc.h"
+
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
+#include "platform/linux/AppParamParserLinux.h"
+#endif
+
+#include <cstdio>
+#include <cstring>
+#include <errno.h>
+#include <locale.h>
+#include <signal.h>
+
+#include <sys/resource.h>
+
+namespace
+{
+extern "C" void XBMC_POSIX_HandleSignal(int sig)
+{
+ // Setting an atomic flag is one of the only useful things that is permitted by POSIX
+ // in signal handlers
+ CPlatformPosix::RequestQuit();
+}
+} // namespace
+
+
+int main(int argc, char* argv[])
+{
+#if defined(_DEBUG)
+ struct rlimit rlim;
+ rlim.rlim_cur = rlim.rlim_max = RLIM_INFINITY;
+ if (setrlimit(RLIMIT_CORE, &rlim) == -1)
+ fprintf(stderr, "Failed to set core size limit (%s).\n", strerror(errno));
+#endif
+
+ // Set up global SIGINT/SIGTERM handler
+ struct sigaction signalHandler;
+ std::memset(&signalHandler, 0, sizeof(signalHandler));
+ signalHandler.sa_handler = &XBMC_POSIX_HandleSignal;
+ signalHandler.sa_flags = SA_RESTART;
+ sigaction(SIGINT, &signalHandler, nullptr);
+ sigaction(SIGTERM, &signalHandler, nullptr);
+
+ setlocale(LC_NUMERIC, "C");
+
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
+ CAppParamParserLinux appParamParser;
+#else
+ CAppParamParser appParamParser;
+#endif
+ appParamParser.Parse(argv, argc);
+
+ CAppEnvironment::SetUp(appParamParser.GetAppParams());
+ int status = XBMC_Run(true);
+ CAppEnvironment::TearDown();
+
+ return status;
+}
diff --git a/xbmc/platform/posix/network/CMakeLists.txt b/xbmc/platform/posix/network/CMakeLists.txt
new file mode 100644
index 0000000..3f8f850
--- /dev/null
+++ b/xbmc/platform/posix/network/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES NetworkPosix.cpp)
+
+set(HEADERS NetworkPosix.h)
+
+core_add_library(platform_posix_network)
diff --git a/xbmc/platform/posix/network/NetworkPosix.cpp b/xbmc/platform/posix/network/NetworkPosix.cpp
new file mode 100644
index 0000000..003e5d7
--- /dev/null
+++ b/xbmc/platform/posix/network/NetworkPosix.cpp
@@ -0,0 +1,150 @@
+/*
+ * 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 "NetworkPosix.h"
+
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <utility>
+
+#include <arpa/inet.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+
+CNetworkInterfacePosix::CNetworkInterfacePosix(CNetworkPosix* network,
+ std::string interfaceName,
+ char interfaceMacAddrRaw[6])
+ : m_interfaceName(std::move(interfaceName)),
+ m_interfaceMacAdr(StringUtils::Format("{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
+ (uint8_t)interfaceMacAddrRaw[0],
+ (uint8_t)interfaceMacAddrRaw[1],
+ (uint8_t)interfaceMacAddrRaw[2],
+ (uint8_t)interfaceMacAddrRaw[3],
+ (uint8_t)interfaceMacAddrRaw[4],
+ (uint8_t)interfaceMacAddrRaw[5]))
+{
+ m_network = network;
+ memcpy(m_interfaceMacAddrRaw, interfaceMacAddrRaw, sizeof(m_interfaceMacAddrRaw));
+}
+
+bool CNetworkInterfacePosix::IsEnabled() const
+{
+ struct ifreq ifr;
+ strcpy(ifr.ifr_name, m_interfaceName.c_str());
+ if (ioctl(m_network->GetSocket(), SIOCGIFFLAGS, &ifr) < 0)
+ return false;
+
+ return ((ifr.ifr_flags & IFF_UP) == IFF_UP);
+}
+
+bool CNetworkInterfacePosix::IsConnected() const
+{
+ struct ifreq ifr;
+ int zero = 0;
+ memset(&ifr, 0, sizeof(struct ifreq));
+ strcpy(ifr.ifr_name, m_interfaceName.c_str());
+ if (ioctl(m_network->GetSocket(), SIOCGIFFLAGS, &ifr) < 0)
+ return false;
+
+ // ignore loopback
+ int iRunning = ((ifr.ifr_flags & IFF_RUNNING) && (!(ifr.ifr_flags & IFF_LOOPBACK)));
+
+ if (ioctl(m_network->GetSocket(), SIOCGIFADDR, &ifr) < 0)
+ return false;
+
+ // return only interfaces which has ip address
+ return iRunning && (0 != memcmp(ifr.ifr_addr.sa_data + sizeof(short), &zero, sizeof(int)));
+}
+
+std::string CNetworkInterfacePosix::GetCurrentIPAddress() const
+{
+ std::string result;
+
+ struct ifreq ifr;
+ strcpy(ifr.ifr_name, m_interfaceName.c_str());
+ ifr.ifr_addr.sa_family = AF_INET;
+ if (ioctl(m_network->GetSocket(), SIOCGIFADDR, &ifr) >= 0)
+ {
+ result = inet_ntoa((*((struct sockaddr_in*)&ifr.ifr_addr)).sin_addr);
+ }
+
+ return result;
+}
+
+std::string CNetworkInterfacePosix::GetCurrentNetmask() const
+{
+ std::string result;
+
+ struct ifreq ifr;
+ strcpy(ifr.ifr_name, m_interfaceName.c_str());
+ ifr.ifr_addr.sa_family = AF_INET;
+ if (ioctl(m_network->GetSocket(), SIOCGIFNETMASK, &ifr) >= 0)
+ {
+ result = inet_ntoa((*((struct sockaddr_in*)&ifr.ifr_addr)).sin_addr);
+ }
+
+ return result;
+}
+
+std::string CNetworkInterfacePosix::GetMacAddress() const
+{
+ return m_interfaceMacAdr;
+}
+
+void CNetworkInterfacePosix::GetMacAddressRaw(char rawMac[6]) const
+{
+ memcpy(rawMac, m_interfaceMacAddrRaw, 6);
+}
+
+CNetworkPosix::CNetworkPosix() : CNetworkBase()
+{
+ m_sock = socket(AF_INET, SOCK_DGRAM, 0);
+}
+
+CNetworkPosix::~CNetworkPosix()
+{
+ if (m_sock != -1)
+ close(CNetworkPosix::m_sock);
+
+ std::vector<CNetworkInterface*>::iterator it = m_interfaces.begin();
+ while (it != m_interfaces.end())
+ {
+ CNetworkInterface* nInt = *it;
+ delete nInt;
+ it = m_interfaces.erase(it);
+ }
+}
+
+std::vector<CNetworkInterface*>& CNetworkPosix::GetInterfaceList()
+{
+ return m_interfaces;
+}
+
+//! @bug
+//! Overwrite the GetFirstConnectedInterface and requery
+//! the interface list if no connected device is found
+//! this fixes a bug when no network is available after first start of xbmc
+//! and the interface comes up during runtime
+CNetworkInterface* CNetworkPosix::GetFirstConnectedInterface()
+{
+ CNetworkInterface* pNetIf = CNetworkBase::GetFirstConnectedInterface();
+
+ // no connected Interfaces found? - requeryInterfaceList
+ if (!pNetIf)
+ {
+ CLog::Log(LOGDEBUG, "{} no connected interface found - requery list", __FUNCTION__);
+ queryInterfaceList();
+ //retry finding a connected if
+ pNetIf = CNetworkBase::GetFirstConnectedInterface();
+ }
+
+ return pNetIf;
+}
diff --git a/xbmc/platform/posix/network/NetworkPosix.h b/xbmc/platform/posix/network/NetworkPosix.h
new file mode 100644
index 0000000..68d885e
--- /dev/null
+++ b/xbmc/platform/posix/network/NetworkPosix.h
@@ -0,0 +1,61 @@
+/*
+ * 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 "network/Network.h"
+
+#include <string>
+#include <vector>
+
+class CNetworkPosix;
+
+class CNetworkInterfacePosix : public CNetworkInterface
+{
+public:
+ CNetworkInterfacePosix(CNetworkPosix* network,
+ std::string interfaceName,
+ char interfaceMacAddrRaw[6]);
+ virtual ~CNetworkInterfacePosix() override = default;
+
+ bool IsEnabled() const override;
+ bool IsConnected() const override;
+ std::string GetCurrentIPAddress() const override;
+ std::string GetCurrentNetmask() const override;
+
+ std::string GetMacAddress() const override;
+ void GetMacAddressRaw(char rawMac[6]) const override;
+
+protected:
+ std::string m_interfaceName;
+ CNetworkPosix* m_network;
+
+private:
+ std::string m_interfaceMacAdr;
+ char m_interfaceMacAddrRaw[6];
+};
+
+class CNetworkPosix : public CNetworkBase
+{
+public:
+ virtual ~CNetworkPosix() override;
+
+ std::vector<CNetworkInterface*>& GetInterfaceList() override;
+ CNetworkInterface* GetFirstConnectedInterface() override;
+
+ int GetSocket() { return m_sock; }
+
+protected:
+ CNetworkPosix();
+ std::vector<CNetworkInterface*> m_interfaces;
+
+private:
+ virtual void GetMacAddress(const std::string& interfaceName, char macAddrRaw[6]) = 0;
+ virtual void queryInterfaceList() = 0;
+ int m_sock;
+};
diff --git a/xbmc/platform/posix/storage/discs/CMakeLists.txt b/xbmc/platform/posix/storage/discs/CMakeLists.txt
new file mode 100644
index 0000000..7587043
--- /dev/null
+++ b/xbmc/platform/posix/storage/discs/CMakeLists.txt
@@ -0,0 +1,7 @@
+if(ENABLE_OPTICAL)
+ set(SOURCES DiscDriveHandlerPosix.cpp)
+
+ set(HEADERS DiscDriveHandlerPosix.h)
+
+ core_add_library(platform_posix_storage_discs)
+endif()
diff --git a/xbmc/platform/posix/storage/discs/DiscDriveHandlerPosix.cpp b/xbmc/platform/posix/storage/discs/DiscDriveHandlerPosix.cpp
new file mode 100644
index 0000000..1ef288a
--- /dev/null
+++ b/xbmc/platform/posix/storage/discs/DiscDriveHandlerPosix.cpp
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2022 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 "DiscDriveHandlerPosix.h"
+
+#include "storage/cdioSupport.h"
+#include "utils/log.h"
+
+#include <memory>
+
+namespace
+{
+constexpr int MAX_OPEN_RETRIES = 3;
+}
+using namespace MEDIA_DETECT;
+
+std::shared_ptr<IDiscDriveHandler> IDiscDriveHandler::CreateInstance()
+{
+ return std::make_shared<CDiscDriveHandlerPosix>();
+}
+
+DriveState CDiscDriveHandlerPosix::GetDriveState(const std::string& devicePath)
+{
+ DriveState driveStatus = DriveState::NOT_READY;
+ const std::shared_ptr<CLibcdio> libCdio = CLibcdio::GetInstance();
+ if (!libCdio)
+ {
+ CLog::LogF(LOGERROR, "Failed to obtain libcdio handler");
+ return driveStatus;
+ }
+
+ CdIo_t* cdio = libCdio->cdio_open(devicePath.c_str(), DRIVER_UNKNOWN);
+ if (!cdio)
+ {
+ return driveStatus;
+ }
+
+ CdioTrayStatus status = libCdio->mmc_get_tray_status(cdio);
+
+ switch (status)
+ {
+ case CdioTrayStatus::CLOSED:
+ case CdioTrayStatus::UNKNOWN:
+ driveStatus = DriveState::CLOSED_MEDIA_UNDEFINED;
+ break;
+
+ case CdioTrayStatus::OPEN:
+ driveStatus = DriveState::OPEN;
+ break;
+
+ case CdioTrayStatus::DRIVER_ERROR:
+ driveStatus = DriveState::NOT_READY;
+ break;
+
+ default:
+ CLog::LogF(LOGWARNING, "Unknown/unhandled drive state interpreted as DRIVE_NOT_READY");
+ break;
+ }
+ libCdio->cdio_destroy(cdio);
+
+ return driveStatus;
+}
+
+TrayState CDiscDriveHandlerPosix::GetTrayState(const std::string& devicePath)
+{
+ TrayState trayStatus = TrayState::UNDEFINED;
+ const std::shared_ptr<CLibcdio> libCdio = CLibcdio::GetInstance();
+ if (!libCdio)
+ {
+ CLog::LogF(LOGERROR, "Failed to obtain libcdio handler");
+ return trayStatus;
+ }
+
+ discmode_t discmode = CDIO_DISC_MODE_NO_INFO;
+ CdIo_t* cdio = libCdio->cdio_open(devicePath.c_str(), DRIVER_UNKNOWN);
+ if (!cdio)
+ {
+ return trayStatus;
+ }
+
+ discmode = libCdio->cdio_get_discmode(cdio);
+
+ if (discmode == CDIO_DISC_MODE_NO_INFO)
+ {
+ trayStatus = TrayState::CLOSED_NO_MEDIA;
+ }
+ else if (discmode == CDIO_DISC_MODE_ERROR)
+ {
+ trayStatus = TrayState::UNDEFINED;
+ }
+ else
+ {
+ trayStatus = TrayState::CLOSED_MEDIA_PRESENT;
+ }
+ libCdio->cdio_destroy(cdio);
+
+ return trayStatus;
+}
+
+void CDiscDriveHandlerPosix::EjectDriveTray(const std::string& devicePath)
+{
+ const std::shared_ptr<CLibcdio> libCdio = CLibcdio::GetInstance();
+ if (!libCdio)
+ {
+ CLog::LogF(LOGERROR, "Failed to obtain libcdio handler");
+ return;
+ }
+
+ int retries = MAX_OPEN_RETRIES;
+ CdIo_t* cdio = libCdio->cdio_open(devicePath.c_str(), DRIVER_UNKNOWN);
+ while (cdio && retries-- > 0)
+ {
+ const driver_return_code_t ret = libCdio->cdio_eject_media(&cdio);
+ if (ret == DRIVER_OP_SUCCESS)
+ break;
+ }
+ libCdio->cdio_destroy(cdio);
+}
+
+void CDiscDriveHandlerPosix::CloseDriveTray(const std::string& devicePath)
+{
+ const std::shared_ptr<CLibcdio> libCdio = CLibcdio::GetInstance();
+ if (!libCdio)
+ {
+ CLog::LogF(LOGERROR, "Failed to obtain libcdio handler");
+ return;
+ }
+
+ const driver_return_code_t ret = libCdio->cdio_close_tray(devicePath.c_str(), nullptr);
+ if (ret != DRIVER_OP_SUCCESS)
+ {
+ CLog::LogF(LOGERROR, "Closing tray failed for device {}: {}", devicePath,
+ libCdio->cdio_driver_errmsg(ret));
+ }
+}
+
+void CDiscDriveHandlerPosix::ToggleDriveTray(const std::string& devicePath)
+{
+ if (GetDriveState(devicePath) == DriveState::OPEN)
+ {
+ CloseDriveTray(devicePath);
+ }
+ else
+ {
+ EjectDriveTray(devicePath);
+ }
+}
diff --git a/xbmc/platform/posix/storage/discs/DiscDriveHandlerPosix.h b/xbmc/platform/posix/storage/discs/DiscDriveHandlerPosix.h
new file mode 100644
index 0000000..11c4d9a
--- /dev/null
+++ b/xbmc/platform/posix/storage/discs/DiscDriveHandlerPosix.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 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 "storage/discs/IDiscDriveHandler.h"
+
+#include <string>
+
+class CDiscDriveHandlerPosix : public IDiscDriveHandler
+{
+public:
+ /*! \brief Posix DiscDriveHandler constructor
+ */
+ CDiscDriveHandlerPosix() = default;
+
+ /*! \brief Posix DiscDriveHandler default destructor
+ */
+ ~CDiscDriveHandlerPosix() override = default;
+
+ /*! \brief Get the optical drive state provided its device path
+ * \param devicePath the path for the device drive (e.g. /dev/sr0)
+ * \return The drive state
+ */
+ DriveState GetDriveState(const std::string& devicePath) override;
+
+ /*! \brief Get the optical drive tray state provided the drive device path
+ * \param devicePath the path for the device drive (e.g. /dev/sr0)
+ * \return The drive state
+ */
+ TrayState GetTrayState(const std::string& devicePath) override;
+
+ /*! \brief Eject the provided drive device
+ * \param devicePath the path for the device drive (e.g. /dev/sr0)
+ */
+ void EjectDriveTray(const std::string& devicePath) override;
+
+ /*! \brief Close the provided drive device
+ * \note Some drives support closing appart from opening/eject
+ * \param devicePath the path for the device drive (e.g. /dev/sr0)
+ */
+ void CloseDriveTray(const std::string& devicePath) override;
+
+ /*! \brief Toggle the state of a given drive device
+ *
+ * Will internally call EjectDriveTray or CloseDriveTray depending on
+ * the internal state of the drive (i.e. if open -> CloseDriveTray /
+ * if closed -> EjectDriveTray)
+ *
+ * \param devicePath the path for the device drive (e.g. /dev/sr0)
+ */
+ void ToggleDriveTray(const std::string& devicePath) override;
+};
diff --git a/xbmc/platform/posix/threads/CMakeLists.txt b/xbmc/platform/posix/threads/CMakeLists.txt
new file mode 100644
index 0000000..251a7e1
--- /dev/null
+++ b/xbmc/platform/posix/threads/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES RecursiveMutex.cpp)
+set(HEADERS RecursiveMutex.h)
+
+if(NOT CORE_SYSTEM_NAME STREQUAL linux AND NOT CORE_SYSTEM_NAME STREQUAL android)
+ list(APPEND SOURCES ThreadImplPosix.cpp)
+ list(APPEND HEADERS ThreadImplPosix.h)
+endif()
+
+core_add_library(platform_posix_threads)
diff --git a/xbmc/platform/posix/threads/RecursiveMutex.cpp b/xbmc/platform/posix/threads/RecursiveMutex.cpp
new file mode 100644
index 0000000..652bc37
--- /dev/null
+++ b/xbmc/platform/posix/threads/RecursiveMutex.cpp
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2005-2022 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 "RecursiveMutex.h"
+
+namespace XbmcThreads
+{
+
+static pthread_mutexattr_t recursiveAttr;
+
+static bool SetRecursiveAttr()
+{
+ static bool alreadyCalled = false;
+
+ if (!alreadyCalled)
+ {
+ pthread_mutexattr_init(&recursiveAttr);
+ pthread_mutexattr_settype(&recursiveAttr, PTHREAD_MUTEX_RECURSIVE);
+#if !defined(TARGET_ANDROID)
+ pthread_mutexattr_setprotocol(&recursiveAttr, PTHREAD_PRIO_INHERIT);
+#endif
+ alreadyCalled = true;
+ }
+
+ return true; // note, we never call destroy.
+}
+
+static bool recursiveAttrSet = SetRecursiveAttr();
+
+pthread_mutexattr_t& CRecursiveMutex::getRecursiveAttr()
+{
+ if (!recursiveAttrSet) // this is only possible in the single threaded startup code
+ recursiveAttrSet = SetRecursiveAttr();
+
+ return recursiveAttr;
+}
+
+} // namespace XbmcThreads
diff --git a/xbmc/platform/posix/threads/RecursiveMutex.h b/xbmc/platform/posix/threads/RecursiveMutex.h
new file mode 100644
index 0000000..eed0fb2
--- /dev/null
+++ b/xbmc/platform/posix/threads/RecursiveMutex.h
@@ -0,0 +1,48 @@
+/*
+ * 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 <mutex>
+
+#include <pthread.h>
+namespace XbmcThreads
+{
+
+/*!
+ * \brief This class exists purely for the ability to
+ * set mutex attribute PTHREAD_PRIO_INHERIT.
+ * Currently there is no way to set this using
+ * std::recursive_mutex.
+ *
+ */
+class CRecursiveMutex
+{
+private:
+ pthread_mutex_t m_mutex;
+
+ static pthread_mutexattr_t& getRecursiveAttr();
+
+public:
+ CRecursiveMutex(const CRecursiveMutex&) = delete;
+ CRecursiveMutex& operator=(const CRecursiveMutex&) = delete;
+
+ inline CRecursiveMutex() { pthread_mutex_init(&m_mutex, &getRecursiveAttr()); }
+
+ inline ~CRecursiveMutex() { pthread_mutex_destroy(&m_mutex); }
+
+ inline void lock() { pthread_mutex_lock(&m_mutex); }
+
+ inline void unlock() { pthread_mutex_unlock(&m_mutex); }
+
+ inline bool try_lock() { return (pthread_mutex_trylock(&m_mutex) == 0); }
+
+ inline std::recursive_mutex::native_handle_type native_handle() { return &m_mutex; }
+};
+
+} // namespace XbmcThreads
diff --git a/xbmc/platform/posix/threads/ThreadImplPosix.cpp b/xbmc/platform/posix/threads/ThreadImplPosix.cpp
new file mode 100644
index 0000000..ae5b849
--- /dev/null
+++ b/xbmc/platform/posix/threads/ThreadImplPosix.cpp
@@ -0,0 +1,35 @@
+/*
+ * 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 "ThreadImplPosix.h"
+
+#include "utils/log.h"
+
+#include <pthread.h>
+
+std::unique_ptr<IThreadImpl> IThreadImpl::CreateThreadImpl(std::thread::native_handle_type handle)
+{
+ return std::make_unique<CThreadImplPosix>(handle);
+}
+
+CThreadImplPosix::CThreadImplPosix(std::thread::native_handle_type handle) : IThreadImpl(handle)
+{
+}
+
+void CThreadImplPosix::SetThreadInfo(const std::string& name)
+{
+#if defined(TARGET_DARWIN)
+ pthread_setname_np(name.c_str());
+#endif
+}
+
+bool CThreadImplPosix::SetPriority(const ThreadPriority& priority)
+{
+ CLog::Log(LOGDEBUG, "[threads] setting priority is not supported on this platform");
+ return false;
+}
diff --git a/xbmc/platform/posix/threads/ThreadImplPosix.h b/xbmc/platform/posix/threads/ThreadImplPosix.h
new file mode 100644
index 0000000..7bba5e1
--- /dev/null
+++ b/xbmc/platform/posix/threads/ThreadImplPosix.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "threads/IThreadImpl.h"
+
+class CThreadImplPosix : public IThreadImpl
+{
+public:
+ CThreadImplPosix(std::thread::native_handle_type handle);
+
+ ~CThreadImplPosix() override = default;
+
+ void SetThreadInfo(const std::string& name) override;
+
+ bool SetPriority(const ThreadPriority& priority) override;
+};
diff --git a/xbmc/platform/posix/utils/CMakeLists.txt b/xbmc/platform/posix/utils/CMakeLists.txt
new file mode 100644
index 0000000..8fb2abc
--- /dev/null
+++ b/xbmc/platform/posix/utils/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(SOURCES Mmap.cpp
+ PosixInterfaceForCLog.cpp
+ SharedMemory.cpp)
+
+set(HEADERS FileHandle.h
+ Mmap.h
+ PosixInterfaceForCLog.h
+ SharedMemory.h)
+
+core_add_library(platform_posix_utils)
diff --git a/xbmc/platform/posix/utils/FileHandle.h b/xbmc/platform/posix/utils/FileHandle.h
new file mode 100644
index 0000000..e50a357
--- /dev/null
+++ b/xbmc/platform/posix/utils/FileHandle.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017-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 "utils/ScopeGuard.h"
+
+#include <unistd.h>
+
+namespace KODI
+{
+namespace UTILS
+{
+namespace POSIX
+{
+
+class CFileHandle : public CScopeGuard<int, -1, decltype(close)>
+{
+public:
+ CFileHandle() noexcept : CScopeGuard(close, -1) {}
+ explicit CFileHandle(int fd) : CScopeGuard(close, fd) {}
+};
+
+}
+}
+}
diff --git a/xbmc/platform/posix/utils/Mmap.cpp b/xbmc/platform/posix/utils/Mmap.cpp
new file mode 100644
index 0000000..3d40d3c
--- /dev/null
+++ b/xbmc/platform/posix/utils/Mmap.cpp
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017-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 "Mmap.h"
+
+#include <system_error>
+
+using namespace KODI::UTILS::POSIX;
+
+CMmap::CMmap(void* addr, std::size_t length, int prot, int flags, int fildes, off_t offset)
+: m_size{length}, m_memory{mmap(addr, length, prot, flags, fildes, offset)}
+{
+ if (m_memory == MAP_FAILED)
+ {
+ throw std::system_error(errno, std::generic_category(), "mmap failed");
+ }
+}
+
+CMmap::~CMmap()
+{
+ munmap(m_memory, m_size);
+} \ No newline at end of file
diff --git a/xbmc/platform/posix/utils/Mmap.h b/xbmc/platform/posix/utils/Mmap.h
new file mode 100644
index 0000000..2470d60
--- /dev/null
+++ b/xbmc/platform/posix/utils/Mmap.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017-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 <cstddef>
+
+#include <sys/mman.h>
+
+namespace KODI
+{
+namespace UTILS
+{
+namespace POSIX
+{
+
+/**
+ * Wrapper for mapped memory that automatically calls munmap on destruction
+ */
+class CMmap
+{
+public:
+ /**
+ * See mmap(3p) for parameter description
+ */
+ CMmap(void* addr, std::size_t length, int prot, int flags, int fildes, off_t offset);
+ ~CMmap();
+
+ void* Data() const
+ {
+ return m_memory;
+ }
+ std::size_t Size() const
+ {
+ return m_size;
+ }
+
+private:
+ CMmap(CMmap const& other) = delete;
+ CMmap& operator=(CMmap const& other) = delete;
+
+ std::size_t m_size;
+ void* m_memory;
+};
+
+}
+}
+}
diff --git a/xbmc/platform/posix/utils/PosixInterfaceForCLog.cpp b/xbmc/platform/posix/utils/PosixInterfaceForCLog.cpp
new file mode 100644
index 0000000..6b8bf48
--- /dev/null
+++ b/xbmc/platform/posix/utils/PosixInterfaceForCLog.cpp
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 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 "PosixInterfaceForCLog.h"
+
+#include "ServiceBroker.h"
+#include "application/AppParams.h"
+
+#include <spdlog/sinks/dist_sink.h>
+#include <spdlog/sinks/stdout_color_sinks.h>
+
+#if !defined(TARGET_ANDROID) && !defined(TARGET_DARWIN)
+std::unique_ptr<IPlatformLog> IPlatformLog::CreatePlatformLog()
+{
+ return std::make_unique<CPosixInterfaceForCLog>();
+}
+#endif
+
+void CPosixInterfaceForCLog::AddSinks(
+ std::shared_ptr<spdlog::sinks::dist_sink<std::mutex>> distributionSink) const
+{
+ if (CServiceBroker::GetAppParams()->GetLogTarget() == "console")
+ distributionSink->add_sink(std::make_shared<spdlog::sinks::stdout_color_sink_st>());
+}
diff --git a/xbmc/platform/posix/utils/PosixInterfaceForCLog.h b/xbmc/platform/posix/utils/PosixInterfaceForCLog.h
new file mode 100644
index 0000000..0336b16
--- /dev/null
+++ b/xbmc/platform/posix/utils/PosixInterfaceForCLog.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 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 "utils/IPlatformLog.h"
+
+class CPosixInterfaceForCLog : public IPlatformLog
+{
+public:
+ CPosixInterfaceForCLog() = default;
+ virtual ~CPosixInterfaceForCLog() override = default;
+
+ spdlog_filename_t GetLogFilename(const std::string& filename) const override { return filename; }
+ void AddSinks(
+ std::shared_ptr<spdlog::sinks::dist_sink<std::mutex>> distributionSink) const override;
+};
diff --git a/xbmc/platform/posix/utils/SharedMemory.cpp b/xbmc/platform/posix/utils/SharedMemory.cpp
new file mode 100644
index 0000000..6c1318e
--- /dev/null
+++ b/xbmc/platform/posix/utils/SharedMemory.cpp
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017-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 "SharedMemory.h"
+
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#if defined(HAVE_LINUX_MEMFD)
+#include <linux/memfd.h>
+#include <sys/syscall.h>
+#endif
+
+#include <cerrno>
+#include <cstdlib>
+#include <system_error>
+
+#include "utils/log.h"
+
+using namespace KODI::UTILS::POSIX;
+
+CSharedMemory::CSharedMemory(std::size_t size)
+: m_size{size}, m_fd{Open()}, m_mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0)
+{
+}
+
+CFileHandle CSharedMemory::Open()
+{
+ CFileHandle fd;
+ try
+ {
+ fd = OpenMemfd();
+ }
+ catch (std::system_error const& error)
+ {
+ if (error.code() == std::errc::function_not_supported)
+ {
+ CLog::Log(LOGDEBUG, "Kernel does not support memfd, falling back to plain shm");
+ fd = OpenShm();
+ }
+ else
+ {
+ throw;
+ }
+ }
+
+ if (ftruncate(fd, m_size) < 0)
+ {
+ throw std::system_error(errno, std::generic_category(), "ftruncate");
+ }
+
+ return fd;
+}
+
+CFileHandle CSharedMemory::OpenMemfd()
+{
+#if defined(SYS_memfd_create) && defined(HAVE_LINUX_MEMFD)
+ // This is specific to Linux >= 3.17, but preferred over shm_create if available
+ // because it is race-free
+ int fd = syscall(SYS_memfd_create, "kodi", MFD_CLOEXEC);
+ if (fd < 0)
+ {
+ throw std::system_error(errno, std::generic_category(), "memfd_create");
+ }
+
+ return CFileHandle(fd);
+#else
+ throw std::system_error(std::make_error_code(std::errc::function_not_supported), "memfd_create");
+#endif
+}
+
+CFileHandle CSharedMemory::OpenShm()
+{
+ char* xdgRuntimeDir = std::getenv("XDG_RUNTIME_DIR");
+ if (!xdgRuntimeDir)
+ {
+ throw std::runtime_error("XDG_RUNTIME_DIR environment variable must be set");
+ }
+
+ std::string tmpFilename(xdgRuntimeDir);
+ tmpFilename.append("/kodi-shared-XXXXXX");
+
+ int rawFd;
+#if defined(HAVE_MKOSTEMP)
+ // Opening the file with O_CLOEXEC is preferred since it avoids races where
+ // other threads might exec() before setting the CLOEXEC flag
+ rawFd = mkostemp(&tmpFilename[0], O_CLOEXEC);
+#else
+ rawFd = mkstemp(&tmpFilename[0]);
+#endif
+
+ if (rawFd < 0)
+ {
+ throw std::system_error(errno, std::generic_category(), "mkstemp");
+ }
+ CFileHandle fd(rawFd);
+
+ int flags = fcntl(fd, F_GETFD);
+ if (flags < 0)
+ {
+ throw std::system_error(errno, std::generic_category(), "fcntl F_GETFD");
+ }
+ // Set FD_CLOEXEC if unset
+ if (!(flags & FD_CLOEXEC) && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1)
+ {
+ throw std::system_error(errno, std::generic_category(), "fcntl F_SETFD FD_CLOEXEC");
+ }
+
+ unlink(tmpFilename.c_str());
+
+ return fd;
+} \ No newline at end of file
diff --git a/xbmc/platform/posix/utils/SharedMemory.h b/xbmc/platform/posix/utils/SharedMemory.h
new file mode 100644
index 0000000..b556de6
--- /dev/null
+++ b/xbmc/platform/posix/utils/SharedMemory.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017-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 "FileHandle.h"
+#include "Mmap.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace UTILS
+{
+namespace POSIX
+{
+
+/**
+ * Get a chunk of shared memory of specified size
+ *
+ * The shared memory is automatically allocated, truncated to the correct size
+ * and memory-mapped.
+ */
+class CSharedMemory
+{
+public:
+ explicit CSharedMemory(std::size_t size);
+
+ std::size_t Size() const
+ {
+ return m_size;
+ }
+ void* Data() const
+ {
+ return m_mmap.Data();
+ }
+ int Fd() const
+ {
+ return m_fd;
+ }
+
+private:
+ CSharedMemory(CSharedMemory const& other) = delete;
+ CSharedMemory& operator=(CSharedMemory const& other) = delete;
+
+ CFileHandle Open();
+ CFileHandle OpenMemfd();
+ CFileHandle OpenShm();
+
+ std::size_t m_size;
+ CFileHandle m_fd;
+ CMmap m_mmap;
+};
+
+}
+}
+}
diff --git a/xbmc/platform/xbmc.cpp b/xbmc/platform/xbmc.cpp
new file mode 100644
index 0000000..a6de8fb
--- /dev/null
+++ b/xbmc/platform/xbmc.cpp
@@ -0,0 +1,79 @@
+/*
+ * 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 "application/Application.h"
+#include "platform/MessagePrinter.h"
+
+#ifdef TARGET_WINDOWS_DESKTOP
+#include "platform/win32/IMMNotificationClient.h"
+#include <mmdeviceapi.h>
+#include <wrl/client.h>
+#endif
+
+#if defined(TARGET_ANDROID)
+#include "platform/android/activity/XBMCApp.h"
+#endif
+
+extern "C" int XBMC_Run(bool renderGUI)
+{
+ int status = -1;
+
+ if (!g_application.Create())
+ {
+ CMessagePrinter::DisplayError("ERROR: Unable to create application. Exiting");
+ return status;
+ }
+
+#if defined(TARGET_ANDROID)
+ CXBMCApp::Get().Initialize();
+#endif
+
+ if (renderGUI && !g_application.CreateGUI())
+ {
+ CMessagePrinter::DisplayError("ERROR: Unable to create GUI. Exiting");
+ if (g_application.Stop(EXITCODE_QUIT))
+ g_application.Cleanup();
+ return status;
+ }
+ if (!g_application.Initialize())
+ {
+ CMessagePrinter::DisplayError("ERROR: Unable to Initialize. Exiting");
+ return status;
+ }
+
+#ifdef TARGET_WINDOWS_DESKTOP
+ Microsoft::WRL::ComPtr<IMMDeviceEnumerator> pEnumerator = nullptr;
+ CMMNotificationClient cMMNC;
+ HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator,
+ reinterpret_cast<void**>(pEnumerator.GetAddressOf()));
+ if (SUCCEEDED(hr))
+ {
+ pEnumerator->RegisterEndpointNotificationCallback(&cMMNC);
+ pEnumerator = nullptr;
+ }
+#endif
+
+ status = g_application.Run();
+
+#ifdef TARGET_WINDOWS_DESKTOP
+ // the end
+ hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator,
+ reinterpret_cast<void**>(pEnumerator.GetAddressOf()));
+ if (SUCCEEDED(hr))
+ {
+ pEnumerator->UnregisterEndpointNotificationCallback(&cMMNC);
+ pEnumerator = nullptr;
+ }
+#endif
+
+#if defined(TARGET_ANDROID)
+ CXBMCApp::Get().Deinitialize();
+#endif
+
+ return status;
+}
diff --git a/xbmc/platform/xbmc.h b/xbmc/platform/xbmc.h
new file mode 100644
index 0000000..206acdb
--- /dev/null
+++ b/xbmc/platform/xbmc.h
@@ -0,0 +1,15 @@
+/*
+ * 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 <memory>
+
+class CAppParams;
+
+extern "C" int XBMC_Run(bool renderGUI);
diff --git a/xbmc/playlists/CMakeLists.txt b/xbmc/playlists/CMakeLists.txt
new file mode 100644
index 0000000..c665156
--- /dev/null
+++ b/xbmc/playlists/CMakeLists.txt
@@ -0,0 +1,26 @@
+set(SOURCES PlayListB4S.cpp
+ PlayList.cpp
+ PlayListFactory.cpp
+ PlayListM3U.cpp
+ PlayListPLS.cpp
+ PlayListURL.cpp
+ PlayListWPL.cpp
+ PlayListXML.cpp
+ PlayListXSPF.cpp
+ SmartPlayList.cpp
+ SmartPlaylistFileItemListModifier.cpp)
+
+set(HEADERS PlayList.h
+ PlayListB4S.h
+ PlayListFactory.h
+ PlayListM3U.h
+ PlayListPLS.h
+ PlayListTypes.h
+ PlayListURL.h
+ PlayListWPL.h
+ PlayListXML.h
+ PlayListXSPF.h
+ SmartPlayList.h
+ SmartPlaylistFileItemListModifier.h)
+
+core_add_library(playlists)
diff --git a/xbmc/playlists/PlayList.cpp b/xbmc/playlists/PlayList.cpp
new file mode 100644
index 0000000..7c03884
--- /dev/null
+++ b/xbmc/playlists/PlayList.cpp
@@ -0,0 +1,513 @@
+/*
+ * 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 "PlayList.h"
+
+#include "FileItem.h"
+#include "PlayListFactory.h"
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "interfaces/AnnouncementManager.h"
+#include "music/tags/MusicInfoTag.h"
+#include "utils/Random.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cassert>
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+
+using namespace MUSIC_INFO;
+using namespace XFILE;
+using namespace PLAYLIST;
+
+CPlayList::CPlayList(Id id /* = PLAYLIST::TYPE_NONE */) : m_id(id)
+{
+ m_iPlayableItems = -1;
+ m_bShuffled = false;
+ m_bWasPlayed = false;
+}
+
+void CPlayList::AnnounceRemove(int pos)
+{
+ if (m_id == TYPE_NONE)
+ return;
+
+ CVariant data;
+ data["playlistid"] = m_id;
+ data["position"] = pos;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Playlist, "OnRemove", data);
+}
+
+void CPlayList::AnnounceClear()
+{
+ if (m_id == TYPE_NONE)
+ return;
+
+ CVariant data;
+ data["playlistid"] = m_id;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Playlist, "OnClear", data);
+}
+
+void CPlayList::AnnounceAdd(const std::shared_ptr<CFileItem>& item, int pos)
+{
+ if (m_id == TYPE_NONE)
+ return;
+
+ CVariant data;
+ data["playlistid"] = m_id;
+ data["position"] = pos;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Playlist, "OnAdd", item, data);
+}
+
+void CPlayList::Add(const std::shared_ptr<CFileItem>& item, int iPosition, int iOrder)
+{
+ int iOldSize = size();
+ if (iPosition < 0 || iPosition >= iOldSize)
+ iPosition = iOldSize;
+ if (iOrder < 0 || iOrder >= iOldSize)
+ item->m_iprogramCount = iOldSize;
+ else
+ item->m_iprogramCount = iOrder;
+
+ // increment the playable counter
+ item->ClearProperty("unplayable");
+ if (m_iPlayableItems < 0)
+ m_iPlayableItems = 1;
+ else
+ m_iPlayableItems++;
+
+ // set 'IsPlayable' property - needed for properly handling plugin:// URLs
+ item->SetProperty("IsPlayable", true);
+
+ //CLog::Log(LOGDEBUG,"{} item:({:02}/{:02})[{}]", __FUNCTION__, iPosition, item->m_iprogramCount, item->GetPath());
+ if (iPosition == iOldSize)
+ m_vecItems.push_back(item);
+ else
+ {
+ ivecItems it = m_vecItems.begin() + iPosition;
+ m_vecItems.insert(it, 1, item);
+ // correct any duplicate order values
+ if (iOrder < iOldSize)
+ IncrementOrder(iPosition + 1, iOrder);
+ }
+ AnnounceAdd(item, iPosition);
+}
+
+void CPlayList::Add(const std::shared_ptr<CFileItem>& item)
+{
+ Add(item, -1, -1);
+}
+
+void CPlayList::Add(const CPlayList& playlist)
+{
+ for (int i = 0; i < playlist.size(); i++)
+ Add(playlist[i], -1, -1);
+}
+
+void CPlayList::Add(const CFileItemList& items)
+{
+ for (int i = 0; i < items.Size(); i++)
+ Add(items[i]);
+}
+
+void CPlayList::Insert(const CPlayList& playlist, int iPosition /* = -1 */)
+{
+ // out of bounds so just add to the end
+ int iSize = size();
+ if (iPosition < 0 || iPosition >= iSize)
+ {
+ Add(playlist);
+ return;
+ }
+ for (int i = 0; i < playlist.size(); i++)
+ {
+ int iPos = iPosition + i;
+ Add(playlist[i], iPos, iPos);
+ }
+}
+
+void CPlayList::Insert(const CFileItemList& items, int iPosition /* = -1 */)
+{
+ // out of bounds so just add to the end
+ int iSize = size();
+ if (iPosition < 0 || iPosition >= iSize)
+ {
+ Add(items);
+ return;
+ }
+ for (int i = 0; i < items.Size(); i++)
+ {
+ Add(items[i], iPosition + i, iPosition + i);
+ }
+}
+
+void CPlayList::Insert(const std::shared_ptr<CFileItem>& item, int iPosition /* = -1 */)
+{
+ // out of bounds so just add to the end
+ int iSize = size();
+ if (iPosition < 0 || iPosition >= iSize)
+ {
+ Add(item);
+ return;
+ }
+ Add(item, iPosition, iPosition);
+}
+
+void CPlayList::DecrementOrder(int iOrder)
+{
+ if (iOrder < 0) return;
+
+ // it was the last item so do nothing
+ if (iOrder == size()) return;
+
+ // fix all items with an order greater than the removed iOrder
+ ivecItems it;
+ it = m_vecItems.begin();
+ while (it != m_vecItems.end())
+ {
+ CFileItemPtr item = *it;
+ if (item->m_iprogramCount > iOrder)
+ {
+ //CLog::Log(LOGDEBUG,"{} fixing item at order {}", __FUNCTION__, item->m_iprogramCount);
+ item->m_iprogramCount--;
+ }
+ ++it;
+ }
+}
+
+void CPlayList::IncrementOrder(int iPosition, int iOrder)
+{
+ if (iOrder < 0) return;
+
+ // fix all items with an order equal or greater to the added iOrder at iPos
+ ivecItems it;
+ it = m_vecItems.begin() + iPosition;
+ while (it != m_vecItems.end())
+ {
+ CFileItemPtr item = *it;
+ if (item->m_iprogramCount >= iOrder)
+ {
+ //CLog::Log(LOGDEBUG,"{} fixing item at order {}", __FUNCTION__, item->m_iprogramCount);
+ item->m_iprogramCount++;
+ }
+ ++it;
+ }
+}
+
+void CPlayList::Clear()
+{
+ bool announce = false;
+ if (!m_vecItems.empty())
+ {
+ m_vecItems.erase(m_vecItems.begin(), m_vecItems.end());
+ announce = true;
+ }
+ m_strPlayListName = "";
+ m_iPlayableItems = -1;
+ m_bWasPlayed = false;
+
+ if (announce)
+ AnnounceClear();
+}
+
+int CPlayList::size() const
+{
+ return (int)m_vecItems.size();
+}
+
+const std::shared_ptr<CFileItem> CPlayList::operator[](int iItem) const
+{
+ if (iItem < 0 || iItem >= size())
+ {
+ assert(false);
+ CLog::Log(LOGERROR, "Error trying to retrieve an item that's out of range");
+ return CFileItemPtr();
+ }
+ return m_vecItems[iItem];
+}
+
+std::shared_ptr<CFileItem> CPlayList::operator[](int iItem)
+{
+ if (iItem < 0 || iItem >= size())
+ {
+ assert(false);
+ CLog::Log(LOGERROR, "Error trying to retrieve an item that's out of range");
+ return CFileItemPtr();
+ }
+ return m_vecItems[iItem];
+}
+
+void CPlayList::Shuffle(int iPosition)
+{
+ if (size() == 0)
+ // nothing to shuffle, just set the flag for later
+ m_bShuffled = true;
+ else
+ {
+ if (iPosition >= size())
+ return;
+ if (iPosition < 0)
+ iPosition = 0;
+ CLog::Log(LOGDEBUG, "{} shuffling at pos:{}", __FUNCTION__, iPosition);
+
+ ivecItems it = m_vecItems.begin() + iPosition;
+ KODI::UTILS::RandomShuffle(it, m_vecItems.end());
+
+ // the list is now shuffled!
+ m_bShuffled = true;
+ }
+}
+
+struct SSortPlayListItem
+{
+ static bool PlaylistSort(const CFileItemPtr &left, const CFileItemPtr &right)
+ {
+ return (left->m_iprogramCount < right->m_iprogramCount);
+ }
+};
+
+void CPlayList::UnShuffle()
+{
+ std::sort(m_vecItems.begin(), m_vecItems.end(), SSortPlayListItem::PlaylistSort);
+ // the list is now unshuffled!
+ m_bShuffled = false;
+}
+
+const std::string& CPlayList::GetName() const
+{
+ return m_strPlayListName;
+}
+
+void CPlayList::Remove(const std::string& strFileName)
+{
+ int iOrder = -1;
+ int position = 0;
+ ivecItems it;
+ it = m_vecItems.begin();
+ while (it != m_vecItems.end() )
+ {
+ CFileItemPtr item = *it;
+ if (item->GetPath() == strFileName)
+ {
+ iOrder = item->m_iprogramCount;
+ it = m_vecItems.erase(it);
+ AnnounceRemove(position);
+ //CLog::Log(LOGDEBUG,"PLAYLIST, removing item at order {}", iPos);
+ }
+ else
+ {
+ ++position;
+ ++it;
+ }
+ }
+ DecrementOrder(iOrder);
+}
+
+int CPlayList::FindOrder(int iOrder) const
+{
+ for (int i = 0; i < size(); i++)
+ {
+ if (m_vecItems[i]->m_iprogramCount == iOrder)
+ return i;
+ }
+ return -1;
+}
+
+// remove item from playlist by position
+void CPlayList::Remove(int position)
+{
+ int iOrder = -1;
+ if (position >= 0 && position < (int)m_vecItems.size())
+ {
+ iOrder = m_vecItems[position]->m_iprogramCount;
+ m_vecItems.erase(m_vecItems.begin() + position);
+ }
+ DecrementOrder(iOrder);
+
+ AnnounceRemove(position);
+}
+
+int CPlayList::RemoveDVDItems()
+{
+ std::vector <std::string> vecFilenames;
+
+ // Collect playlist items from DVD share
+ ivecItems it;
+ it = m_vecItems.begin();
+ while (it != m_vecItems.end() )
+ {
+ CFileItemPtr item = *it;
+ if ( item->IsCDDA() || item->IsOnDVD() )
+ {
+ vecFilenames.push_back( item->GetPath() );
+ }
+ ++it;
+ }
+
+ // Delete them from playlist
+ int nFileCount = vecFilenames.size();
+ if ( nFileCount )
+ {
+ std::vector <std::string>::iterator it;
+ it = vecFilenames.begin();
+ while (it != vecFilenames.end() )
+ {
+ std::string& strFilename = *it;
+ Remove( strFilename );
+ ++it;
+ }
+ vecFilenames.erase( vecFilenames.begin(), vecFilenames.end() );
+ }
+ return nFileCount;
+}
+
+bool CPlayList::Swap(int position1, int position2)
+{
+ if (
+ (position1 < 0) ||
+ (position2 < 0) ||
+ (position1 >= size()) ||
+ (position2 >= size())
+ )
+ {
+ return false;
+ }
+
+ if (!IsShuffled())
+ {
+ // swap the ordinals before swapping the items!
+ //CLog::Log(LOGDEBUG,"PLAYLIST swapping items at orders ({}, {})",m_vecItems[position1]->m_iprogramCount,m_vecItems[position2]->m_iprogramCount);
+ std::swap(m_vecItems[position1]->m_iprogramCount, m_vecItems[position2]->m_iprogramCount);
+ }
+
+ // swap the items
+ std::swap(m_vecItems[position1], m_vecItems[position2]);
+ return true;
+}
+
+void CPlayList::SetUnPlayable(int iItem)
+{
+ if (iItem < 0 || iItem >= size())
+ {
+ CLog::Log(LOGWARNING, "Attempt to set unplayable index {}", iItem);
+ return;
+ }
+
+ CFileItemPtr item = m_vecItems[iItem];
+ if (!item->GetProperty("unplayable").asBoolean())
+ {
+ item->SetProperty("unplayable", true);
+ m_iPlayableItems--;
+ }
+}
+
+
+bool CPlayList::Load(const std::string& strFileName)
+{
+ Clear();
+ m_strBasePath = URIUtils::GetDirectory(strFileName);
+
+ CFileStream file;
+ if (!file.Open(strFileName))
+ return false;
+
+ if (file.GetLength() > 1024*1024)
+ {
+ CLog::Log(LOGWARNING, "{} - File is larger than 1 MB, most likely not a playlist",
+ __FUNCTION__);
+ return false;
+ }
+
+ return LoadData(file);
+}
+
+bool CPlayList::LoadData(std::istream &stream)
+{
+ // try to read as a string
+ std::ostringstream ostr;
+ ostr << stream.rdbuf();
+ return LoadData(ostr.str());
+}
+
+bool CPlayList::LoadData(const std::string& strData)
+{
+ return false;
+}
+
+
+bool CPlayList::Expand(int position)
+{
+ CFileItemPtr item = m_vecItems[position];
+ std::unique_ptr<CPlayList> playlist (CPlayListFactory::Create(*item.get()));
+ if (playlist == nullptr)
+ return false;
+
+ std::string path = item->GetDynPath();
+
+ if (!playlist->Load(path))
+ return false;
+
+ // remove any item that points back to itself
+ for (int i = 0;i<playlist->size();i++)
+ {
+ if (StringUtils::EqualsNoCase((*playlist)[i]->GetPath(), path))
+ {
+ playlist->Remove(i);
+ i--;
+ }
+ }
+
+ // @todo
+ // never change original path (id) of a file item
+ for (int i = 0;i<playlist->size();i++)
+ {
+ (*playlist)[i]->SetDynPath((*playlist)[i]->GetPath());
+ (*playlist)[i]->SetPath(item->GetPath());
+ (*playlist)[i]->SetStartOffset(item->GetStartOffset());
+ }
+
+ if (playlist->size() <= 0)
+ return false;
+
+ Remove(position);
+ Insert(*playlist, position);
+ return true;
+}
+
+void CPlayList::UpdateItem(const CFileItem *item)
+{
+ if (!item) return;
+
+ for (ivecItems it = m_vecItems.begin(); it != m_vecItems.end(); ++it)
+ {
+ CFileItemPtr playlistItem = *it;
+ if (playlistItem->IsSamePath(item))
+ {
+ std::string temp = playlistItem->GetPath(); // save path, it may have been altered
+ *playlistItem = *item;
+ playlistItem->SetPath(temp);
+ break;
+ }
+ }
+}
+
+const std::string& CPlayList::ResolveURL(const std::shared_ptr<CFileItem>& item) const
+{
+ if (item->IsMusicDb() && item->HasMusicInfoTag())
+ return item->GetMusicInfoTag()->GetURL();
+ else
+ return item->GetDynPath();
+}
diff --git a/xbmc/playlists/PlayList.h b/xbmc/playlists/PlayList.h
new file mode 100644
index 0000000..8aa92be
--- /dev/null
+++ b/xbmc/playlists/PlayList.h
@@ -0,0 +1,92 @@
+/*
+ * 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 "PlayListTypes.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CFileItemList;
+
+namespace PLAYLIST
+{
+
+class CPlayList
+{
+public:
+ explicit CPlayList(PLAYLIST::Id id = PLAYLIST::TYPE_NONE);
+ virtual ~CPlayList(void) = default;
+ virtual bool Load(const std::string& strFileName);
+ virtual bool LoadData(std::istream &stream);
+ virtual bool LoadData(const std::string& strData);
+ virtual void Save(const std::string& strFileName) const {};
+
+ void Add(const CPlayList& playlist);
+ void Add(const std::shared_ptr<CFileItem>& pItem);
+ void Add(const CFileItemList& items);
+
+ // for Party Mode
+ void Insert(const CPlayList& playlist, int iPosition = -1);
+ void Insert(const CFileItemList& items, int iPosition = -1);
+ void Insert(const std::shared_ptr<CFileItem>& item, int iPosition = -1);
+
+ int FindOrder(int iOrder) const;
+ const std::string& GetName() const;
+ void Remove(const std::string& strFileName);
+ void Remove(int position);
+ bool Swap(int position1, int position2);
+ bool Expand(int position); // expands any playlist at position into this playlist
+ void Clear();
+ int size() const;
+ int RemoveDVDItems();
+
+ const std::shared_ptr<CFileItem> operator[](int iItem) const;
+ std::shared_ptr<CFileItem> operator[](int iItem);
+
+ void Shuffle(int iPosition = 0);
+ void UnShuffle();
+ bool IsShuffled() const { return m_bShuffled; }
+
+ void SetPlayed(bool bPlayed) { m_bWasPlayed = true; }
+ bool WasPlayed() const { return m_bWasPlayed; }
+
+ void SetUnPlayable(int iItem);
+ int GetPlayable() const { return m_iPlayableItems; }
+
+ void UpdateItem(const CFileItem *item);
+
+ const std::string& ResolveURL(const std::shared_ptr<CFileItem>& item) const;
+
+protected:
+ PLAYLIST::Id m_id;
+ std::string m_strPlayListName;
+ std::string m_strBasePath;
+ int m_iPlayableItems;
+ bool m_bShuffled;
+ bool m_bWasPlayed;
+
+// CFileItemList m_vecItems;
+ std::vector<std::shared_ptr<CFileItem>> m_vecItems;
+ typedef std::vector<std::shared_ptr<CFileItem>>::iterator ivecItems;
+
+private:
+ void Add(const std::shared_ptr<CFileItem>& item, int iPosition, int iOrderOffset);
+ void DecrementOrder(int iOrder);
+ void IncrementOrder(int iPosition, int iOrder);
+
+ void AnnounceRemove(int pos);
+ void AnnounceClear();
+ void AnnounceAdd(const std::shared_ptr<CFileItem>& item, int pos);
+};
+
+typedef std::shared_ptr<CPlayList> CPlayListPtr;
+}
diff --git a/xbmc/playlists/PlayListB4S.cpp b/xbmc/playlists/PlayListB4S.cpp
new file mode 100644
index 0000000..52a06fb
--- /dev/null
+++ b/xbmc/playlists/PlayListB4S.cpp
@@ -0,0 +1,133 @@
+/*
+ * 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 "PlayListB4S.h"
+
+#include "FileItem.h"
+#include "Util.h"
+#include "filesystem/File.h"
+#include "music/tags/MusicInfoTag.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <iostream>
+#include <string>
+
+using namespace XFILE;
+using namespace PLAYLIST;
+
+/* ------------------------ example b4s playlist file ---------------------------------
+ <?xml version="1.0" encoding='UTF-8' standalone="yes"?>
+ <WinampXML>
+ <!-- Generated by: Nullsoft Winamp3 version 3.0d -->
+ <playlist num_entries="2" label="Playlist 001">
+ <entry Playstring="file:E:\Program Files\Winamp3\demo.mp3">
+ <Name>demo</Name>
+ <Length>5982</Length>
+ </entry>
+ <entry Playstring="file:E:\Program Files\Winamp3\demo.mp3">
+ <Name>demo</Name>
+ <Length>5982</Length>
+ </entry>
+ </playlist>
+ </WinampXML>
+------------------------ end of example b4s playlist file ---------------------------------*/
+CPlayListB4S::CPlayListB4S(void) = default;
+
+CPlayListB4S::~CPlayListB4S(void) = default;
+
+
+bool CPlayListB4S::LoadData(std::istream& stream)
+{
+ CXBMCTinyXML xmlDoc;
+
+ stream >> xmlDoc;
+
+ if (xmlDoc.Error())
+ {
+ CLog::Log(LOGERROR, "Unable to parse B4S info Error: {}", xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (!pRootElement ) return false;
+
+ TiXmlElement* pPlayListElement = pRootElement->FirstChildElement("playlist");
+ if (!pPlayListElement ) return false;
+ m_strPlayListName = XMLUtils::GetAttribute(pPlayListElement, "label");
+
+ TiXmlElement* pEntryElement = pPlayListElement->FirstChildElement("entry");
+
+ if (!pEntryElement) return false;
+ while (pEntryElement)
+ {
+ std::string strFileName = XMLUtils::GetAttribute(pEntryElement, "Playstring");
+ size_t iColon = strFileName.find(':');
+ if (iColon != std::string::npos)
+ {
+ iColon++;
+ strFileName.erase(0, iColon);
+ }
+ if (strFileName.size())
+ {
+ TiXmlNode* pNodeInfo = pEntryElement->FirstChild("Name");
+ TiXmlNode* pNodeLength = pEntryElement->FirstChild("Length");
+ long lDuration = 0;
+ if (pNodeLength)
+ {
+ lDuration = atol(pNodeLength->FirstChild()->Value());
+ }
+ if (pNodeInfo)
+ {
+ std::string strInfo = pNodeInfo->FirstChild()->Value();
+ strFileName = URIUtils::SubstitutePath(strFileName);
+ CUtil::GetQualifiedFilename(m_strBasePath, strFileName);
+ CFileItemPtr newItem(new CFileItem(strInfo));
+ newItem->SetPath(strFileName);
+ newItem->GetMusicInfoTag()->SetDuration(lDuration);
+ Add(newItem);
+ }
+ }
+ pEntryElement = pEntryElement->NextSiblingElement();
+ }
+ return true;
+}
+
+void CPlayListB4S::Save(const std::string& strFileName) const
+{
+ if (!m_vecItems.size()) return ;
+ std::string strPlaylist = strFileName;
+ strPlaylist = CUtil::MakeLegalPath(strPlaylist);
+ CFile file;
+ if (!file.OpenForWrite(strPlaylist, true))
+ {
+ CLog::Log(LOGERROR, "Could not save B4S playlist: [{}]", strPlaylist);
+ return ;
+ }
+ std::string write;
+ write += StringUtils::Format("<?xml version={}1.0{} encoding='UTF-8' standalone={}yes{}?>\n", 34,
+ 34, 34, 34);
+ write += StringUtils::Format("<WinampXML>\n");
+ write += StringUtils::Format(" <playlist num_entries=\"{0}\" label=\"{1}\">\n",
+ m_vecItems.size(), m_strPlayListName);
+ for (int i = 0; i < (int)m_vecItems.size(); ++i)
+ {
+ const CFileItemPtr item = m_vecItems[i];
+ write += StringUtils::Format(" <entry Playstring={}file:{}{}>\n", 34, item->GetPath(), 34);
+ write += StringUtils::Format(" <Name>{}</Name>\n", item->GetLabel().c_str());
+ write +=
+ StringUtils::Format(" <Length>{}</Length>\n", item->GetMusicInfoTag()->GetDuration());
+ }
+ write += StringUtils::Format(" </playlist>\n");
+ write += StringUtils::Format("</WinampXML>\n");
+ file.Write(write.c_str(), write.size());
+ file.Close();
+}
diff --git a/xbmc/playlists/PlayListB4S.h b/xbmc/playlists/PlayListB4S.h
new file mode 100644
index 0000000..6f688b5
--- /dev/null
+++ b/xbmc/playlists/PlayListB4S.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "PlayList.h"
+
+namespace PLAYLIST
+{
+
+class CPlayListB4S :
+ public CPlayList
+{
+public:
+ CPlayListB4S(void);
+ ~CPlayListB4S(void) override;
+ bool LoadData(std::istream& stream) override;
+ void Save(const std::string& strFileName) const override;
+};
+}
diff --git a/xbmc/playlists/PlayListFactory.cpp b/xbmc/playlists/PlayListFactory.cpp
new file mode 100644
index 0000000..8abb1ba
--- /dev/null
+++ b/xbmc/playlists/PlayListFactory.cpp
@@ -0,0 +1,145 @@
+/*
+ * 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 "PlayListFactory.h"
+
+#include "FileItem.h"
+#include "playlists/PlayListB4S.h"
+#include "playlists/PlayListM3U.h"
+#include "playlists/PlayListPLS.h"
+#include "playlists/PlayListURL.h"
+#include "playlists/PlayListWPL.h"
+#include "playlists/PlayListXML.h"
+#include "playlists/PlayListXSPF.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace PLAYLIST;
+
+CPlayList* CPlayListFactory::Create(const std::string& filename)
+{
+ CFileItem item(filename,false);
+ return Create(item);
+}
+
+CPlayList* CPlayListFactory::Create(const CFileItem& item)
+{
+ if (item.IsInternetStream())
+ {
+ // Ensure the MIME type has been retrieved for http:// and shout:// streams
+ if (item.GetMimeType().empty())
+ const_cast<CFileItem&>(item).FillInMimeType();
+
+ std::string strMimeType = item.GetMimeType();
+ StringUtils::ToLower(strMimeType);
+
+ if (strMimeType == "video/x-ms-asf"
+ || strMimeType == "video/x-ms-asx"
+ || strMimeType == "video/x-ms-wmv"
+ || strMimeType == "video/x-ms-wma"
+ || strMimeType == "video/x-ms-wfs"
+ || strMimeType == "video/x-ms-wvx"
+ || strMimeType == "video/x-ms-wax")
+ return new CPlayListASX();
+
+ if (strMimeType == "audio/x-pn-realaudio")
+ return new CPlayListRAM();
+
+ if (strMimeType == "audio/x-scpls"
+ || strMimeType == "playlist"
+ || strMimeType == "text/html")
+ return new CPlayListPLS();
+
+ // online m3u8 files are for hls streaming -- do not treat as playlist
+ if (strMimeType == "audio/x-mpegurl" && !item.IsType(".m3u8"))
+ return new CPlayListM3U();
+
+ if (strMimeType == "application/vnd.ms-wpl")
+ return new CPlayListWPL();
+
+ if (strMimeType == "application/xspf+xml")
+ return new CPlayListXSPF();
+ }
+
+ std::string path = item.GetDynPath();
+
+ std::string extension = URIUtils::GetExtension(path);
+ StringUtils::ToLower(extension);
+
+ if (extension == ".m3u" || extension == ".strm")
+ return new CPlayListM3U();
+
+ if (extension == ".pls")
+ return new CPlayListPLS();
+
+ if (extension == ".b4s")
+ return new CPlayListB4S();
+
+ if (extension == ".wpl")
+ return new CPlayListWPL();
+
+ if (extension == ".asx")
+ return new CPlayListASX();
+
+ if (extension == ".ram")
+ return new CPlayListRAM();
+
+ if (extension == ".url")
+ return new CPlayListURL();
+
+ if (extension == ".pxml")
+ return new CPlayListXML();
+
+ if (extension == ".xspf")
+ return new CPlayListXSPF();
+
+ return NULL;
+
+}
+
+bool CPlayListFactory::IsPlaylist(const CFileItem& item)
+{
+ std::string strMimeType = item.GetMimeType();
+ StringUtils::ToLower(strMimeType);
+
+/* These are a bit uncertain
+ if(strMimeType == "video/x-ms-asf"
+ || strMimeType == "video/x-ms-asx"
+ || strMimeType == "video/x-ms-wmv"
+ || strMimeType == "video/x-ms-wma"
+ || strMimeType == "video/x-ms-wfs"
+ || strMimeType == "video/x-ms-wvx"
+ || strMimeType == "video/x-ms-wax"
+ || strMimeType == "video/x-ms-asf")
+ return true;
+*/
+
+ // online m3u8 files are hls:// -- do not treat as playlist
+ if (item.IsInternetStream() && item.IsType(".m3u8"))
+ return false;
+
+ if(strMimeType == "audio/x-pn-realaudio"
+ || strMimeType == "playlist"
+ || strMimeType == "audio/x-mpegurl")
+ return true;
+
+ return IsPlaylist(item.GetDynPath());
+}
+
+bool CPlayListFactory::IsPlaylist(const CURL& url)
+{
+ return URIUtils::HasExtension(url,
+ ".m3u|.b4s|.pls|.strm|.wpl|.asx|.ram|.url|.pxml|.xspf");
+}
+
+bool CPlayListFactory::IsPlaylist(const std::string& filename)
+{
+ return URIUtils::HasExtension(filename,
+ ".m3u|.b4s|.pls|.strm|.wpl|.asx|.ram|.url|.pxml|.xspf");
+}
+
diff --git a/xbmc/playlists/PlayListFactory.h b/xbmc/playlists/PlayListFactory.h
new file mode 100644
index 0000000..59a56f2
--- /dev/null
+++ b/xbmc/playlists/PlayListFactory.h
@@ -0,0 +1,29 @@
+/*
+ * 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 <string>
+
+class CFileItem;
+class CURL;
+
+namespace PLAYLIST
+{
+ class CPlayList;
+
+ class CPlayListFactory
+ {
+ public:
+ static CPlayList* Create(const std::string& filename);
+ static CPlayList* Create(const CFileItem& item);
+ static bool IsPlaylist(const CURL& url);
+ static bool IsPlaylist(const std::string& filename);
+ static bool IsPlaylist(const CFileItem& item);
+ };
+}
diff --git a/xbmc/playlists/PlayListM3U.cpp b/xbmc/playlists/PlayListM3U.cpp
new file mode 100644
index 0000000..d39cdce
--- /dev/null
+++ b/xbmc/playlists/PlayListM3U.cpp
@@ -0,0 +1,278 @@
+/*
+ * 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 "PlayListM3U.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/File.h"
+#include "music/tags/MusicInfoTag.h"
+#include "utils/CharsetConverter.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <inttypes.h>
+
+using namespace PLAYLIST;
+using namespace XFILE;
+
+const char* CPlayListM3U::StartMarker = "#EXTCPlayListM3U::M3U";
+const char* CPlayListM3U::InfoMarker = "#EXTINF";
+const char* CPlayListM3U::ArtistMarker = "#EXTART";
+const char* CPlayListM3U::AlbumMarker = "#EXTALB";
+const char* CPlayListM3U::PropertyMarker = "#KODIPROP";
+const char* CPlayListM3U::VLCOptMarker = "#EXTVLCOPT";
+const char* CPlayListM3U::StreamMarker = "#EXT-X-STREAM-INF";
+const char* CPlayListM3U::BandwidthMarker = "BANDWIDTH";
+const char* CPlayListM3U::OffsetMarker = "#EXT-KX-OFFSET";
+
+// example m3u file:
+// #EXTM3U
+// #EXTART:Demo Artist
+// #EXTALB:Demo Album
+// #KODIPROP:name=value
+// #EXTINF:5,demo
+// E:\Program Files\Winamp3\demo.mp3
+
+
+
+// example m3u8 containing streams of different bitrates
+// #EXTM3U
+// #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=1600000
+// playlist_1600.m3u8
+// #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=3000000
+// playlist_3000.m3u8
+// #EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=800000
+// playlist_800.m3u8
+
+
+CPlayListM3U::CPlayListM3U(void) = default;
+
+CPlayListM3U::~CPlayListM3U(void) = default;
+
+
+bool CPlayListM3U::Load(const std::string& strFileName)
+{
+ char szLine[4096];
+ std::string strLine;
+ std::string strInfo;
+ std::vector<std::pair<std::string, std::string> > properties;
+
+ int lDuration = 0;
+ int iStartOffset = 0;
+ int iEndOffset = 0;
+
+ Clear();
+
+ m_strPlayListName = URIUtils::GetFileName(strFileName);
+ URIUtils::GetParentPath(strFileName, m_strBasePath);
+
+ CFile file;
+ if (!file.Open(strFileName) )
+ {
+ file.Close();
+ return false;
+ }
+
+ while (file.ReadString(szLine, 4095))
+ {
+ strLine = szLine;
+ StringUtils::Trim(strLine);
+
+ if (StringUtils::StartsWith(strLine, InfoMarker))
+ {
+ // start of info
+ size_t iColon = strLine.find(':');
+ size_t iComma = strLine.find(',');
+ if (iColon != std::string::npos &&
+ iComma != std::string::npos &&
+ iComma > iColon)
+ {
+ // Read the info and duration
+ iColon++;
+ std::string strLength = strLine.substr(iColon, iComma - iColon);
+ lDuration = atoi(strLength.c_str());
+ iComma++;
+ strInfo = strLine.substr(iComma);
+ g_charsetConverter.unknownToUTF8(strInfo);
+ }
+ }
+ else if (StringUtils::StartsWith(strLine, OffsetMarker))
+ {
+ size_t iColon = strLine.find(':');
+ size_t iComma = strLine.find(',');
+ if (iColon != std::string::npos &&
+ iComma != std::string::npos &&
+ iComma > iColon)
+ {
+ // Read the start and end offset
+ iColon++;
+ iStartOffset = atoi(strLine.substr(iColon, iComma - iColon).c_str());
+ iComma++;
+ iEndOffset = atoi(strLine.substr(iComma).c_str());
+ }
+ }
+ else if (StringUtils::StartsWith(strLine, PropertyMarker)
+ || StringUtils::StartsWith(strLine, VLCOptMarker))
+ {
+ size_t iColon = strLine.find(':');
+ size_t iEqualSign = strLine.find('=');
+ if (iColon != std::string::npos &&
+ iEqualSign != std::string::npos &&
+ iEqualSign > iColon)
+ {
+ std::string strFirst, strSecond;
+ properties.emplace_back(
+ StringUtils::Trim((strFirst = strLine.substr(iColon + 1, iEqualSign - iColon - 1))),
+ StringUtils::Trim((strSecond = strLine.substr(iEqualSign + 1))));
+ }
+ }
+ else if (strLine != StartMarker &&
+ !StringUtils::StartsWith(strLine, ArtistMarker) &&
+ !StringUtils::StartsWith(strLine, AlbumMarker))
+ {
+ std::string strFileName = strLine;
+
+ if (!strFileName.empty() && strFileName[0] == '#')
+ continue; // assume a comment or something else we don't support
+
+ // Skip self - do not load playlist recursively
+ // We compare case-less in case user has input incorrect case of the current playlist
+ if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strFileName), m_strPlayListName))
+ continue;
+
+ if (strFileName.length() > 0)
+ {
+ g_charsetConverter.unknownToUTF8(strFileName);
+
+ // If no info was read from from the extended tag information, use the file name
+ if (strInfo.length() == 0)
+ {
+ strInfo = URIUtils::GetFileName(strFileName);
+ }
+
+ // should substitution occur before or after charset conversion??
+ strFileName = URIUtils::SubstitutePath(strFileName);
+
+ // Get the full path file name and add it to the the play list
+ CUtil::GetQualifiedFilename(m_strBasePath, strFileName);
+ CFileItemPtr newItem(new CFileItem(strInfo));
+ newItem->SetPath(strFileName);
+ if (iStartOffset != 0 || iEndOffset != 0)
+ {
+ newItem->SetStartOffset(iStartOffset);
+ newItem->m_lStartPartNumber = 1;
+ newItem->SetProperty("item_start", iStartOffset);
+ newItem->SetEndOffset(iEndOffset);
+ // Prevent load message from file and override offset set here
+ newItem->GetMusicInfoTag()->SetLoaded();
+ newItem->GetMusicInfoTag()->SetTitle(strInfo);
+ if (iEndOffset)
+ lDuration = static_cast<int>(CUtil::ConvertMilliSecsToSecsIntRounded(iEndOffset - iStartOffset));
+ }
+ if (newItem->IsVideo() && !newItem->HasVideoInfoTag()) // File is a video and needs a VideoInfoTag
+ newItem->GetVideoInfoTag()->Reset(); // Force VideoInfoTag creation
+ if (lDuration && newItem->IsAudio())
+ newItem->GetMusicInfoTag()->SetDuration(lDuration);
+ for (auto &prop : properties)
+ {
+ newItem->SetProperty(prop.first, prop.second);
+ }
+
+ newItem->SetMimeType(newItem->GetProperty("mimetype").asString());
+ if (!newItem->GetMimeType().empty())
+ newItem->SetContentLookup(false);
+
+ Add(newItem);
+
+ // Reset the values just in case there part of the file have the extended marker
+ // and part don't
+ strInfo = "";
+ lDuration = 0;
+ iStartOffset = 0;
+ iEndOffset = 0;
+ properties.clear();
+ }
+ }
+ }
+
+ file.Close();
+ return true;
+}
+
+void CPlayListM3U::Save(const std::string& strFileName) const
+{
+ if (!m_vecItems.size())
+ return;
+ std::string strPlaylist = CUtil::MakeLegalPath(strFileName);
+ CFile file;
+ if (!file.OpenForWrite(strPlaylist,true))
+ {
+ CLog::Log(LOGERROR, "Could not save M3U playlist: [{}]", strPlaylist);
+ return;
+ }
+ std::string strLine = StringUtils::Format("{}\n", StartMarker);
+ if (file.Write(strLine.c_str(), strLine.size()) != static_cast<ssize_t>(strLine.size()))
+ return; // error
+
+ for (int i = 0; i < (int)m_vecItems.size(); ++i)
+ {
+ CFileItemPtr item = m_vecItems[i];
+ std::string strDescription=item->GetLabel();
+ g_charsetConverter.utf8ToStringCharset(strDescription);
+ strLine = StringUtils::Format("{}:{},{}\n", InfoMarker,
+ item->GetMusicInfoTag()->GetDuration(), strDescription);
+ if (file.Write(strLine.c_str(), strLine.size()) != static_cast<ssize_t>(strLine.size()))
+ return; // error
+ if (item->GetStartOffset() != 0 || item->GetEndOffset() != 0)
+ {
+ strLine = StringUtils::Format("{}:{},{}\n", OffsetMarker, item->GetStartOffset(),
+ item->GetEndOffset());
+ file.Write(strLine.c_str(),strLine.size());
+ }
+ std::string strFileName = ResolveURL(item);
+ g_charsetConverter.utf8ToStringCharset(strFileName);
+ strLine = StringUtils::Format("{}\n", strFileName);
+ if (file.Write(strLine.c_str(), strLine.size()) != static_cast<ssize_t>(strLine.size()))
+ return; // error
+ }
+ file.Close();
+}
+
+std::map< std::string, std::string > CPlayListM3U::ParseStreamLine(const std::string &streamLine)
+{
+ std::map< std::string, std::string > params;
+
+ // ensure the line has something beyond the stream marker and ':'
+ if (streamLine.size() < strlen(StreamMarker) + 2)
+ return params;
+
+ // get the actual params following the :
+ std::string strParams(streamLine.substr(strlen(StreamMarker) + 1));
+
+ // separate the parameters
+ std::vector<std::string> vecParams = StringUtils::Split(strParams, ",");
+ for (std::vector<std::string>::iterator i = vecParams.begin(); i != vecParams.end(); ++i)
+ {
+ // split the param, ensure there was an =
+ StringUtils::Trim(*i);
+ std::vector<std::string> vecTuple = StringUtils::Split(*i, "=");
+ if (vecTuple.size() < 2)
+ continue;
+
+ // remove white space from name and value and store it in the dictionary
+ StringUtils::Trim(vecTuple[0]);
+ StringUtils::Trim(vecTuple[1]);
+ params[vecTuple[0]] = vecTuple[1];
+ }
+
+ return params;
+}
+
diff --git a/xbmc/playlists/PlayListM3U.h b/xbmc/playlists/PlayListM3U.h
new file mode 100644
index 0000000..467ed30
--- /dev/null
+++ b/xbmc/playlists/PlayListM3U.h
@@ -0,0 +1,38 @@
+/*
+ * 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 "PlayList.h"
+#include "URL.h"
+
+namespace PLAYLIST
+{
+class CPlayListM3U :
+ public CPlayList
+{
+public:
+ static const char *StartMarker;
+ static const char *InfoMarker;
+ static const char *ArtistMarker;
+ static const char *AlbumMarker;
+ static const char *PropertyMarker;
+ static const char *VLCOptMarker;
+ static const char *StreamMarker;
+ static const char *BandwidthMarker;
+ static const char *OffsetMarker;
+
+public:
+ CPlayListM3U(void);
+ ~CPlayListM3U(void) override;
+ bool Load(const std::string& strFileName) override;
+ void Save(const std::string& strFileName) const override;
+
+ static std::map<std::string,std::string> ParseStreamLine(const std::string &streamLine);
+};
+}
diff --git a/xbmc/playlists/PlayListPLS.cpp b/xbmc/playlists/PlayListPLS.cpp
new file mode 100644
index 0000000..369804d
--- /dev/null
+++ b/xbmc/playlists/PlayListPLS.cpp
@@ -0,0 +1,425 @@
+/*
+ * 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 "PlayListPLS.h"
+
+#include "FileItem.h"
+#include "PlayListFactory.h"
+#include "Util.h"
+#include "filesystem/File.h"
+#include "music/tags/MusicInfoTag.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <iostream>
+#include <memory>
+#include <string>
+#include <vector>
+
+using namespace XFILE;
+using namespace PLAYLIST;
+
+#define START_PLAYLIST_MARKER "[playlist]" // may be case-insensitive (equivalent to .ini file on win32)
+#define PLAYLIST_NAME "PlaylistName"
+
+/*----------------------------------------------------------------------
+[playlist]
+PlaylistName=Playlist 001
+File1=E:\Program Files\Winamp3\demo.mp3
+Title1=demo
+Length1=5
+File2=E:\Program Files\Winamp3\demo.mp3
+Title2=demo
+Length2=5
+NumberOfEntries=2
+Version=2
+----------------------------------------------------------------------*/
+CPlayListPLS::CPlayListPLS(void) = default;
+
+CPlayListPLS::~CPlayListPLS(void) = default;
+
+bool CPlayListPLS::Load(const std::string &strFile)
+{
+ //read it from the file
+ std::string strFileName(strFile);
+ m_strPlayListName = URIUtils::GetFileName(strFileName);
+
+ Clear();
+
+ bool bShoutCast = false;
+ if( StringUtils::StartsWithNoCase(strFileName, "shout://") )
+ {
+ strFileName.replace(0, 8, "http://");
+ m_strBasePath = "";
+ bShoutCast = true;
+ }
+ else
+ URIUtils::GetParentPath(strFileName, m_strBasePath);
+
+ CFile file;
+ if (!file.Open(strFileName) )
+ {
+ file.Close();
+ return false;
+ }
+
+ if (file.GetLength() > 1024*1024)
+ {
+ CLog::Log(LOGWARNING, "{} - File is larger than 1 MB, most likely not a playlist",
+ __FUNCTION__);
+ return false;
+ }
+
+ char szLine[4096];
+ std::string strLine;
+
+ // run through looking for the [playlist] marker.
+ // if we find another http stream, then load it.
+ while (true)
+ {
+ if ( !file.ReadString(szLine, sizeof(szLine) ) )
+ {
+ file.Close();
+ return size() > 0;
+ }
+ strLine = szLine;
+ StringUtils::Trim(strLine);
+ if(StringUtils::EqualsNoCase(strLine, START_PLAYLIST_MARKER))
+ break;
+
+ // if there is something else before playlist marker, this isn't a pls file
+ if(!strLine.empty())
+ return false;
+ }
+
+ bool bFailed = false;
+ while (file.ReadString(szLine, sizeof(szLine) ) )
+ {
+ strLine = szLine;
+ StringUtils::RemoveCRLF(strLine);
+ size_t iPosEqual = strLine.find('=');
+ if (iPosEqual != std::string::npos)
+ {
+ std::string strLeft = strLine.substr(0, iPosEqual);
+ iPosEqual++;
+ std::string strValue = strLine.substr(iPosEqual);
+ StringUtils::ToLower(strLeft);
+ StringUtils::TrimLeft(strLeft);
+
+ if (strLeft == "numberofentries")
+ {
+ m_vecItems.reserve(atoi(strValue.c_str()));
+ }
+ else if (StringUtils::StartsWith(strLeft, "file"))
+ {
+ std::vector <int>::size_type idx = atoi(strLeft.c_str() + 4);
+ if (!Resize(idx))
+ {
+ bFailed = true;
+ break;
+ }
+
+ // Skip self - do not load playlist recursively
+ if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strValue),
+ URIUtils::GetFileName(strFileName)))
+ continue;
+
+ if (m_vecItems[idx - 1]->GetLabel().empty())
+ m_vecItems[idx - 1]->SetLabel(URIUtils::GetFileName(strValue));
+ CFileItem item(strValue, false);
+ if (bShoutCast && !item.IsAudio())
+ strValue.replace(0, 7, "shout://");
+
+ strValue = URIUtils::SubstitutePath(strValue);
+ CUtil::GetQualifiedFilename(m_strBasePath, strValue);
+ g_charsetConverter.unknownToUTF8(strValue);
+ m_vecItems[idx - 1]->SetPath(strValue);
+ }
+ else if (StringUtils::StartsWith(strLeft, "title"))
+ {
+ std::vector <int>::size_type idx = atoi(strLeft.c_str() + 5);
+ if (!Resize(idx))
+ {
+ bFailed = true;
+ break;
+ }
+ g_charsetConverter.unknownToUTF8(strValue);
+ m_vecItems[idx - 1]->SetLabel(strValue);
+ }
+ else if (StringUtils::StartsWith(strLeft, "length"))
+ {
+ std::vector <int>::size_type idx = atoi(strLeft.c_str() + 6);
+ if (!Resize(idx))
+ {
+ bFailed = true;
+ break;
+ }
+ m_vecItems[idx - 1]->GetMusicInfoTag()->SetDuration(atol(strValue.c_str()));
+ }
+ else if (strLeft == "playlistname")
+ {
+ m_strPlayListName = strValue;
+ g_charsetConverter.unknownToUTF8(m_strPlayListName);
+ }
+ }
+ }
+ file.Close();
+
+ if (bFailed)
+ {
+ CLog::Log(LOGERROR,
+ "File {} is not a valid PLS playlist. Location of first file,title or length is not "
+ "permitted (eg. File0 should be File1)",
+ URIUtils::GetFileName(strFileName));
+ return false;
+ }
+
+ // check for missing entries
+ ivecItems p = m_vecItems.begin();
+ while ( p != m_vecItems.end())
+ {
+ if ((*p)->GetPath().empty())
+ {
+ p = m_vecItems.erase(p);
+ }
+ else
+ {
+ ++p;
+ }
+ }
+
+ return true;
+}
+
+void CPlayListPLS::Save(const std::string& strFileName) const
+{
+ if (!m_vecItems.size()) return ;
+ std::string strPlaylist = CUtil::MakeLegalPath(strFileName);
+ CFile file;
+ if (!file.OpenForWrite(strPlaylist, true))
+ {
+ CLog::Log(LOGERROR, "Could not save PLS playlist: [{}]", strPlaylist);
+ return;
+ }
+ std::string write;
+ write += StringUtils::Format("{}\n", START_PLAYLIST_MARKER);
+ std::string strPlayListName=m_strPlayListName;
+ g_charsetConverter.utf8ToStringCharset(strPlayListName);
+ write += StringUtils::Format("PlaylistName={}\n", strPlayListName);
+
+ for (int i = 0; i < (int)m_vecItems.size(); ++i)
+ {
+ CFileItemPtr item = m_vecItems[i];
+ std::string strFileName=item->GetPath();
+ g_charsetConverter.utf8ToStringCharset(strFileName);
+ std::string strDescription=item->GetLabel();
+ g_charsetConverter.utf8ToStringCharset(strDescription);
+ write += StringUtils::Format("File{}={}\n", i + 1, strFileName);
+ write += StringUtils::Format("Title{}={}\n", i + 1, strDescription.c_str());
+ write +=
+ StringUtils::Format("Length{}={}\n", i + 1, item->GetMusicInfoTag()->GetDuration() / 1000);
+ }
+
+ write += StringUtils::Format("NumberOfEntries={0}\n", m_vecItems.size());
+ write += StringUtils::Format("Version=2\n");
+ file.Write(write.c_str(), write.size());
+ file.Close();
+}
+
+bool CPlayListASX::LoadAsxIniInfo(std::istream &stream)
+{
+ CLog::Log(LOGINFO, "Parsing INI style ASX");
+
+ std::string name, value;
+
+ while( stream.good() )
+ {
+ // consume blank rows, and blanks
+ while((stream.peek() == '\r' || stream.peek() == '\n' || stream.peek() == ' ') && stream.good())
+ stream.get();
+
+ if(stream.peek() == '[')
+ {
+ // this is an [section] part, just ignore it
+ while(stream.good() && stream.peek() != '\r' && stream.peek() != '\n')
+ stream.get();
+ continue;
+ }
+ name = "";
+ value = "";
+ // consume name
+ while(stream.peek() != '\r' && stream.peek() != '\n' && stream.peek() != '=' && stream.good())
+ name += stream.get();
+
+ // consume =
+ if(stream.get() != '=')
+ continue;
+
+ // consume value
+ while(stream.peek() != '\r' && stream.peek() != '\n' && stream.good())
+ value += stream.get();
+
+ CLog::Log(LOGINFO, "Adding element {}={}", name, value);
+ CFileItemPtr newItem(new CFileItem(value));
+ newItem->SetPath(value);
+ if (newItem->IsVideo() && !newItem->HasVideoInfoTag()) // File is a video and needs a VideoInfoTag
+ newItem->GetVideoInfoTag()->Reset(); // Force VideoInfoTag creation
+ Add(newItem);
+ }
+
+ return true;
+}
+
+bool CPlayListASX::LoadData(std::istream& stream)
+{
+ CLog::Log(LOGINFO, "Parsing ASX");
+
+ if(stream.peek() == '[')
+ {
+ return LoadAsxIniInfo(stream);
+ }
+ else
+ {
+ std::string asxstream(std::istreambuf_iterator<char>(stream), {});
+ CXBMCTinyXML xmlDoc;
+ xmlDoc.Parse(asxstream, TIXML_DEFAULT_ENCODING);
+
+ if (xmlDoc.Error())
+ {
+ CLog::Log(LOGERROR, "Unable to parse ASX info Error: {}", xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement *pRootElement = xmlDoc.RootElement();
+
+ if (!pRootElement)
+ return false;
+
+ // lowercase every element
+ TiXmlNode *pNode = pRootElement;
+ TiXmlNode *pChild = NULL;
+ std::string value;
+ value = pNode->Value();
+ StringUtils::ToLower(value);
+ pNode->SetValue(value);
+ while(pNode)
+ {
+ pChild = pNode->IterateChildren(pChild);
+ if(pChild)
+ {
+ if (pChild->Type() == TiXmlNode::TINYXML_ELEMENT)
+ {
+ value = pChild->Value();
+ StringUtils::ToLower(value);
+ pChild->SetValue(value);
+
+ TiXmlAttribute* pAttr = pChild->ToElement()->FirstAttribute();
+ while(pAttr)
+ {
+ value = pAttr->Name();
+ StringUtils::ToLower(value);
+ pAttr->SetName(value);
+ pAttr = pAttr->Next();
+ }
+ }
+
+ pNode = pChild;
+ pChild = NULL;
+ continue;
+ }
+
+ pChild = pNode;
+ pNode = pNode->Parent();
+ }
+ std::string roottitle;
+ TiXmlElement *pElement = pRootElement->FirstChildElement();
+ while (pElement)
+ {
+ value = pElement->Value();
+ if (value == "title" && !pElement->NoChildren())
+ {
+ roottitle = pElement->FirstChild()->ValueStr();
+ }
+ else if (value == "entry")
+ {
+ std::string title(roottitle);
+
+ TiXmlElement *pRef = pElement->FirstChildElement("ref");
+ TiXmlElement *pTitle = pElement->FirstChildElement("title");
+
+ if(pTitle && !pTitle->NoChildren())
+ title = pTitle->FirstChild()->ValueStr();
+
+ while (pRef)
+ { // multiple references may appear for one entry
+ // duration may exist on this level too
+ value = XMLUtils::GetAttribute(pRef, "href");
+ if (!value.empty())
+ {
+ if(title.empty())
+ title = value;
+
+ CLog::Log(LOGINFO, "Adding element {}, {}", title, value);
+ CFileItemPtr newItem(new CFileItem(title));
+ newItem->SetPath(value);
+ Add(newItem);
+ }
+ pRef = pRef->NextSiblingElement("ref");
+ }
+ }
+ else if (value == "entryref")
+ {
+ value = XMLUtils::GetAttribute(pElement, "href");
+ if (!value.empty())
+ { // found an entryref, let's try loading that url
+ std::unique_ptr<CPlayList> playlist(CPlayListFactory::Create(value));
+ if (nullptr != playlist)
+ if (playlist->Load(value))
+ Add(*playlist);
+ }
+ }
+ pElement = pElement->NextSiblingElement();
+ }
+ }
+
+ return true;
+}
+
+
+bool CPlayListRAM::LoadData(std::istream& stream)
+{
+ CLog::Log(LOGINFO, "Parsing RAM");
+
+ std::string strMMS;
+ while( stream.peek() != '\n' && stream.peek() != '\r' )
+ strMMS += stream.get();
+
+ CLog::Log(LOGINFO, "Adding element {}", strMMS);
+ CFileItemPtr newItem(new CFileItem(strMMS));
+ newItem->SetPath(strMMS);
+ Add(newItem);
+ return true;
+}
+
+bool CPlayListPLS::Resize(std::vector <int>::size_type newSize)
+{
+ if (newSize == 0)
+ return false;
+
+ while (m_vecItems.size() < newSize)
+ {
+ CFileItemPtr fileItem(new CFileItem());
+ m_vecItems.push_back(fileItem);
+ }
+ return true;
+}
diff --git a/xbmc/playlists/PlayListPLS.h b/xbmc/playlists/PlayListPLS.h
new file mode 100644
index 0000000..50d58e3
--- /dev/null
+++ b/xbmc/playlists/PlayListPLS.h
@@ -0,0 +1,44 @@
+/*
+ * 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 "PlayList.h"
+
+#include <string>
+#include <vector>
+
+namespace PLAYLIST
+{
+class CPlayListPLS :
+ public CPlayList
+{
+public:
+ CPlayListPLS(void);
+ ~CPlayListPLS(void) override;
+ bool Load(const std::string& strFileName) override;
+ void Save(const std::string& strFileName) const override;
+ virtual bool Resize(std::vector<int>::size_type newSize);
+};
+
+class CPlayListASX : public CPlayList
+{
+public:
+ bool LoadData(std::istream &stream) override;
+protected:
+ bool LoadAsxIniInfo(std::istream &stream);
+};
+
+class CPlayListRAM : public CPlayList
+{
+public:
+ bool LoadData(std::istream &stream) override;
+};
+
+
+}
diff --git a/xbmc/playlists/PlayListTypes.h b/xbmc/playlists/PlayListTypes.h
new file mode 100644
index 0000000..84462c1
--- /dev/null
+++ b/xbmc/playlists/PlayListTypes.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 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
+
+namespace PLAYLIST
+{
+
+using Id = int;
+
+constexpr Id TYPE_NONE = -1; //! Playlist id of type none
+constexpr Id TYPE_MUSIC = 0; //! Playlist id of type music
+constexpr Id TYPE_VIDEO = 1; //! Playlist id of type video
+constexpr Id TYPE_PICTURE = 2; //! Playlist id of type picture
+
+/*!
+ * \brief Manages playlist playing.
+ */
+enum class RepeatState
+{
+ NONE,
+ ONE,
+ ALL
+};
+
+} // namespace PLAYLIST
diff --git a/xbmc/playlists/PlayListURL.cpp b/xbmc/playlists/PlayListURL.cpp
new file mode 100644
index 0000000..f2ec4e3
--- /dev/null
+++ b/xbmc/playlists/PlayListURL.cpp
@@ -0,0 +1,69 @@
+/*
+ * 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 "PlayListURL.h"
+
+#include "FileItem.h"
+#include "filesystem/File.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace PLAYLIST;
+using namespace XFILE;
+
+// example url file
+//[DEFAULT]
+//BASEURL=http://msdn2.microsoft.com/en-us/library/ms812698.aspx
+//[InternetShortcut]
+//URL=http://msdn2.microsoft.com/en-us/library/ms812698.aspx
+
+CPlayListURL::CPlayListURL(void) = default;
+
+CPlayListURL::~CPlayListURL(void) = default;
+
+bool CPlayListURL::Load(const std::string& strFileName)
+{
+ char szLine[4096];
+ std::string strLine;
+
+ Clear();
+
+ m_strPlayListName = URIUtils::GetFileName(strFileName);
+ URIUtils::GetParentPath(strFileName, m_strBasePath);
+
+ CFile file;
+ if (!file.Open(strFileName) )
+ {
+ file.Close();
+ return false;
+ }
+
+ while (file.ReadString(szLine, 1024))
+ {
+ strLine = szLine;
+ StringUtils::RemoveCRLF(strLine);
+
+ if (StringUtils::StartsWith(strLine, "[InternetShortcut]"))
+ {
+ if (file.ReadString(szLine,1024))
+ {
+ strLine = szLine;
+ StringUtils::RemoveCRLF(strLine);
+ if (StringUtils::StartsWith(strLine, "URL="))
+ {
+ CFileItemPtr newItem(new CFileItem(strLine.substr(4), false));
+ Add(newItem);
+ }
+ }
+ }
+ }
+
+ file.Close();
+ return true;
+}
+
diff --git a/xbmc/playlists/PlayListURL.h b/xbmc/playlists/PlayListURL.h
new file mode 100644
index 0000000..d0baa0a
--- /dev/null
+++ b/xbmc/playlists/PlayListURL.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "PlayList.h"
+
+namespace PLAYLIST
+{
+class CPlayListURL :
+ public CPlayList
+{
+public:
+ CPlayListURL(void);
+ ~CPlayListURL(void) override;
+ bool Load(const std::string& strFileName) override;
+};
+}
diff --git a/xbmc/playlists/PlayListWPL.cpp b/xbmc/playlists/PlayListWPL.cpp
new file mode 100644
index 0000000..e7142ff
--- /dev/null
+++ b/xbmc/playlists/PlayListWPL.cpp
@@ -0,0 +1,130 @@
+/*
+ * 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 "PlayListWPL.h"
+
+#include "FileItem.h"
+#include "Util.h"
+#include "filesystem/File.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <iostream>
+#include <string>
+
+using namespace XFILE;
+using namespace PLAYLIST;
+
+/* ------------------------ example wpl playlist file ---------------------------------
+ <?wpl version="1.0"?>
+ <smil>
+ <head>
+ <meta name="Generator" content="Microsoft Windows Media Player -- 10.0.0.3646"/>
+ <author/>
+ <title>Playlist</title>
+ </head>
+ <body>
+ <seq>
+ <media src="E:\MP3\Track1.mp3"/>
+ <media src="E:\MP3\Track2.mp3"/>
+ <media src="E:\MP3\track3.mp3"/>
+ </seq>
+ </body>
+ </smil>
+------------------------ end of example wpl playlist file ---------------------------------*/
+//Note: File is utf-8 encoded by default
+
+CPlayListWPL::CPlayListWPL(void) = default;
+
+CPlayListWPL::~CPlayListWPL(void) = default;
+
+
+bool CPlayListWPL::LoadData(std::istream& stream)
+{
+ CXBMCTinyXML xmlDoc;
+
+ stream >> xmlDoc;
+ if (xmlDoc.Error())
+ {
+ CLog::Log(LOGERROR, "Unable to parse B4S info Error: {}", xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (!pRootElement ) return false;
+
+ TiXmlElement* pHeadElement = pRootElement->FirstChildElement("head");
+ if (pHeadElement )
+ {
+ TiXmlElement* pTitelElement = pHeadElement->FirstChildElement("title");
+ if (pTitelElement )
+ m_strPlayListName = pTitelElement->Value();
+ }
+
+ TiXmlElement* pBodyElement = pRootElement->FirstChildElement("body");
+ if (!pBodyElement ) return false;
+
+ TiXmlElement* pSeqElement = pBodyElement->FirstChildElement("seq");
+ if (!pSeqElement ) return false;
+
+ TiXmlElement* pMediaElement = pSeqElement->FirstChildElement("media");
+
+ if (!pMediaElement) return false;
+ while (pMediaElement)
+ {
+ std::string strFileName = XMLUtils::GetAttribute(pMediaElement, "src");
+ if (!strFileName.empty())
+ {
+ std::string strFileNameClean = URIUtils::SubstitutePath(strFileName);
+ CUtil::GetQualifiedFilename(m_strBasePath, strFileNameClean);
+ std::string strDescription = URIUtils::GetFileName(strFileNameClean);
+ CFileItemPtr newItem(new CFileItem(strDescription));
+ newItem->SetPath(strFileNameClean);
+ Add(newItem);
+ }
+ pMediaElement = pMediaElement->NextSiblingElement();
+ }
+ return true;
+}
+
+void CPlayListWPL::Save(const std::string& strFileName) const
+{
+ if (!m_vecItems.size()) return ;
+ std::string strPlaylist = CUtil::MakeLegalPath(strFileName);
+ CFile file;
+ if (!file.OpenForWrite(strPlaylist, true))
+ {
+ CLog::Log(LOGERROR, "Could not save WPL playlist: [{}]", strPlaylist);
+ return ;
+ }
+ std::string write;
+ write += StringUtils::Format("<?wpl version={}1.0{}>\n", 34, 34);
+ write += StringUtils::Format("<smil>\n");
+ write += StringUtils::Format(" <head>\n");
+ write += StringUtils::Format(" <meta name={}Generator{} content={}Microsoft Windows Media "
+ "Player -- 10.0.0.3646{}/>\n",
+ 34, 34, 34, 34);
+ write += StringUtils::Format(" <author/>\n");
+ write += StringUtils::Format(" <title>{}</title>\n", m_strPlayListName.c_str());
+ write += StringUtils::Format(" </head>\n");
+ write += StringUtils::Format(" <body>\n");
+ write += StringUtils::Format(" <seq>\n");
+ for (int i = 0; i < (int)m_vecItems.size(); ++i)
+ {
+ CFileItemPtr item = m_vecItems[i];
+ write += StringUtils::Format(" <media src={}{}{}/>", 34, item->GetPath(), 34);
+ }
+ write += StringUtils::Format(" </seq>\n");
+ write += StringUtils::Format(" </body>\n");
+ write += StringUtils::Format("</smil>\n");
+ file.Write(write.c_str(), write.size());
+ file.Close();
+}
diff --git a/xbmc/playlists/PlayListWPL.h b/xbmc/playlists/PlayListWPL.h
new file mode 100644
index 0000000..e0bdf68
--- /dev/null
+++ b/xbmc/playlists/PlayListWPL.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "PlayList.h"
+
+namespace PLAYLIST
+{
+
+class CPlayListWPL :
+ public CPlayList
+{
+public:
+ CPlayListWPL(void);
+ ~CPlayListWPL(void) override;
+ bool LoadData(std::istream& stream) override;
+ void Save(const std::string& strFileName) const override;
+};
+}
diff --git a/xbmc/playlists/PlayListXML.cpp b/xbmc/playlists/PlayListXML.cpp
new file mode 100644
index 0000000..82af079
--- /dev/null
+++ b/xbmc/playlists/PlayListXML.cpp
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2005-2020 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 "PlayListXML.h"
+
+#include "FileItem.h"
+#include "Util.h"
+#include "filesystem/File.h"
+#include "media/MediaLockState.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+using namespace PLAYLIST;
+using namespace XFILE;
+
+/*
+ Playlist example (must be stored with .pxml extension):
+
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<streams>
+ <!-- Stream definition. To have multiple streams, just add another <stream>...</stream> set. !-->
+ <stream>
+ <!-- Stream URL !-->
+ <url>mms://stream02.rambler.ru/eurosport</url>
+ <!-- Stream name - used for display !-->
+ <name>Евроспорт</name>
+ <!-- Stream category - currently only LIVETV is supported !-->
+ <category>LIVETV</category>
+ <!-- Stream language code !-->
+ <lang>RU</lang>
+ <!-- Stream channel number - will be used to select stream by channel number !-->
+ <channel>1</channel>
+ <!-- Stream is password-protected !-->
+ <lockpassword>123</lockpassword>
+ </stream>
+
+ <stream>
+ <url>mms://video.rfn.ru/vesti_24</url>
+ <name>Вести 24</name>
+ <category>LIVETV</category>
+ <lang>RU</lang>
+ <channel>2</channel>
+ </stream>
+
+</streams>
+*/
+
+
+CPlayListXML::CPlayListXML(void) = default;
+
+CPlayListXML::~CPlayListXML(void) = default;
+
+
+static inline std::string GetString( const TiXmlElement* pRootElement, const char *tagName )
+{
+ std::string strValue;
+ if ( XMLUtils::GetString(pRootElement, tagName, strValue) )
+ return strValue;
+
+ return "";
+}
+
+bool CPlayListXML::Load( const std::string& strFileName )
+{
+ CXBMCTinyXML xmlDoc;
+
+ m_strPlayListName = URIUtils::GetFileName(strFileName);
+ URIUtils::GetParentPath(strFileName, m_strBasePath);
+
+ Clear();
+
+ // Try to load the file as XML. If it does not load, return an error.
+ if ( !xmlDoc.LoadFile( strFileName ) )
+ {
+ CLog::Log(LOGERROR, "Playlist {} has invalid format/malformed xml", strFileName);
+ return false;
+ }
+
+ TiXmlElement *pRootElement = xmlDoc.RootElement();
+
+ // If the stream does not contain "streams", still ok. Not an error.
+ if (!pRootElement || StringUtils::CompareNoCase(pRootElement->Value(), "streams"))
+ {
+ CLog::Log(LOGERROR, "Playlist {} has no <streams> root", strFileName);
+ return false;
+ }
+
+ TiXmlElement* pSet = pRootElement->FirstChildElement("stream");
+
+ while ( pSet )
+ {
+ // Get parameters
+ std::string url = GetString( pSet, "url" );
+ std::string name = GetString( pSet, "name" );
+ std::string category = GetString( pSet, "category" );
+ std::string lang = GetString( pSet, "lang" );
+ std::string channel = GetString( pSet, "channel" );
+ std::string lockpass = GetString( pSet, "lockpassword" );
+
+ // If url is empty, it doesn't make any sense
+ if ( !url.empty() )
+ {
+ // If the name is empty, use url
+ if ( name.empty() )
+ name = url;
+
+ // Append language to the name, and also set as metadata
+ if ( !lang.empty() )
+ name += " [" + lang + "]";
+
+ std::string info = name;
+ CFileItemPtr newItem( new CFileItem(info) );
+ newItem->SetPath(url);
+
+ // Set language as metadata
+ if ( !lang.empty() )
+ newItem->SetProperty("language", lang.c_str() );
+
+ // Set category as metadata
+ if ( !category.empty() )
+ newItem->SetProperty("category", category.c_str() );
+
+ // Set channel as extra info and as metadata
+ if ( !channel.empty() )
+ {
+ newItem->SetProperty("remotechannel", channel.c_str() );
+ newItem->SetExtraInfo( "Channel: " + channel );
+ }
+
+ if ( !lockpass.empty() )
+ {
+ newItem->m_strLockCode = lockpass;
+ newItem->m_iHasLock = LOCK_STATE_LOCKED;
+ newItem->m_iLockMode = LOCK_MODE_NUMERIC;
+ }
+
+ Add(newItem);
+ }
+ else
+ CLog::Log(LOGERROR, "Playlist entry {} in file {} has missing <url> tag", name, strFileName);
+
+ pSet = pSet->NextSiblingElement("stream");
+ }
+
+ return true;
+}
+
+
+void CPlayListXML::Save(const std::string& strFileName) const
+{
+ if (!m_vecItems.size()) return ;
+ std::string strPlaylist = CUtil::MakeLegalPath(strFileName);
+ CFile file;
+ if (!file.OpenForWrite(strPlaylist, true))
+ {
+ CLog::Log(LOGERROR, "Could not save WPL playlist: [{}]", strPlaylist);
+ return ;
+ }
+ std::string write;
+ write += StringUtils::Format("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
+ write += StringUtils::Format("<streams>\n");
+ for (int i = 0; i < (int)m_vecItems.size(); ++i)
+ {
+ CFileItemPtr item = m_vecItems[i];
+ write += StringUtils::Format(" <stream>\n" );
+ write += StringUtils::Format(" <url>{}</url>", item->GetPath().c_str());
+ write += StringUtils::Format(" <name>{}</name>", item->GetLabel());
+
+ if ( !item->GetProperty("language").empty() )
+ write += StringUtils::Format(" <lang>{}</lang>", item->GetProperty("language").asString());
+
+ if ( !item->GetProperty("category").empty() )
+ write += StringUtils::Format(" <category>{}</category>",
+ item->GetProperty("category").asString());
+
+ if ( !item->GetProperty("remotechannel").empty() )
+ write += StringUtils::Format(" <channel>{}</channel>",
+ item->GetProperty("remotechannel").asString());
+
+ if (item->m_iHasLock > LOCK_STATE_NO_LOCK)
+ write += StringUtils::Format(" <lockpassword>{}<lockpassword>", item->m_strLockCode);
+
+ write += StringUtils::Format(" </stream>\n\n" );
+ }
+
+ write += StringUtils::Format("</streams>\n");
+ file.Write(write.c_str(), write.size());
+ file.Close();
+}
diff --git a/xbmc/playlists/PlayListXML.h b/xbmc/playlists/PlayListXML.h
new file mode 100644
index 0000000..d1e18bc
--- /dev/null
+++ b/xbmc/playlists/PlayListXML.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "PlayList.h"
+
+namespace PLAYLIST
+{
+class CPlayListXML :
+ public CPlayList
+{
+public:
+ CPlayListXML(void);
+ ~CPlayListXML(void) override;
+ bool Load(const std::string& strFileName) override;
+ void Save(const std::string& strFileName) const override;
+};
+}
diff --git a/xbmc/playlists/PlayListXSPF.cpp b/xbmc/playlists/PlayListXSPF.cpp
new file mode 100644
index 0000000..6f665a7
--- /dev/null
+++ b/xbmc/playlists/PlayListXSPF.cpp
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2018 Tyler Szabo
+ * Copyright (C) 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 "PlayListXSPF.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+using namespace PLAYLIST;
+
+namespace
+{
+
+constexpr char const* LOCATION_TAGNAME = "location";
+constexpr char const* PLAYLIST_TAGNAME = "playlist";
+constexpr char const* TITLE_TAGNAME = "title";
+constexpr char const* TRACK_TAGNAME = "track";
+constexpr char const* TRACKLIST_TAGNAME = "trackList";
+
+std::string GetXMLText(const TiXmlElement* pXmlElement)
+{
+ std::string result;
+ if (pXmlElement)
+ {
+ const char* const innerText = pXmlElement->GetText();
+ if (innerText)
+ result = innerText;
+ }
+ return result;
+}
+
+}
+
+CPlayListXSPF::CPlayListXSPF(void) = default;
+
+CPlayListXSPF::~CPlayListXSPF(void) = default;
+
+bool CPlayListXSPF::Load(const std::string& strFileName)
+{
+ CXBMCTinyXML xmlDoc;
+
+ if (!xmlDoc.LoadFile(strFileName))
+ {
+ CLog::Log(LOGERROR, "Error parsing XML file {} ({}, {}): {}", strFileName, xmlDoc.ErrorRow(),
+ xmlDoc.ErrorCol(), xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement* pPlaylist = xmlDoc.FirstChildElement(PLAYLIST_TAGNAME);
+ if (!pPlaylist)
+ {
+ CLog::Log(LOGERROR, "Error parsing XML file {}: missing root element {}", strFileName,
+ PLAYLIST_TAGNAME);
+ return false;
+ }
+
+ TiXmlElement* pTracklist = pPlaylist->FirstChildElement(TRACKLIST_TAGNAME);
+ if (!pTracklist)
+ {
+ CLog::Log(LOGERROR, "Error parsing XML file {}: missing element {}", strFileName,
+ TRACKLIST_TAGNAME);
+ return false;
+ }
+
+ Clear();
+ URIUtils::GetParentPath(strFileName, m_strBasePath);
+
+ m_strPlayListName = GetXMLText(pPlaylist->FirstChildElement(TITLE_TAGNAME));
+
+ TiXmlElement* pCurTrack = pTracklist->FirstChildElement(TRACK_TAGNAME);
+ while (pCurTrack)
+ {
+ std::string location = GetXMLText(pCurTrack->FirstChildElement(LOCATION_TAGNAME));
+ if (!location.empty())
+ {
+ std::string label = GetXMLText(pCurTrack->FirstChildElement(TITLE_TAGNAME));
+
+ CFileItemPtr newItem(new CFileItem(label));
+
+ CURL uri(location);
+
+ // at the time of writing CURL doesn't handle file:// URI scheme the way
+ // it's presented in this format, parse to local path instead
+ std::string localpath;
+ if (StringUtils::StartsWith(location, "file:///"))
+ {
+#ifndef TARGET_WINDOWS
+ // Linux absolute path must start with root
+ localpath = "/";
+#endif
+ // Path starts after "file:///"
+ localpath += CURL::Decode(location.substr(8));
+ }
+ else if (uri.GetProtocol().empty())
+ {
+ localpath = URIUtils::AppendSlash(m_strBasePath) + CURL::Decode(location);
+ }
+
+ if (!localpath.empty())
+ {
+ // We don't use URIUtils::CanonicalizePath because m_strBasePath may be a
+ // protocol e.g. smb
+#ifdef TARGET_WINDOWS
+ StringUtils::Replace(localpath, "/", "\\");
+#endif
+ localpath = URIUtils::GetRealPath(localpath);
+
+ newItem->SetPath(localpath);
+ }
+ else
+ {
+ newItem->SetURL(uri);
+ }
+
+ Add(newItem);
+ }
+
+ pCurTrack = pCurTrack->NextSiblingElement(TRACK_TAGNAME);
+ }
+
+ return true;
+}
diff --git a/xbmc/playlists/PlayListXSPF.h b/xbmc/playlists/PlayListXSPF.h
new file mode 100644
index 0000000..8960460
--- /dev/null
+++ b/xbmc/playlists/PlayListXSPF.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 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 "PlayList.h"
+
+namespace PLAYLIST
+{
+class CPlayListXSPF : public CPlayList
+{
+public:
+ CPlayListXSPF(void);
+ ~CPlayListXSPF(void) override;
+
+ // Implementation of CPlayList
+ bool Load(const std::string& strFileName) override;
+};
+}
diff --git a/xbmc/playlists/SmartPlayList.cpp b/xbmc/playlists/SmartPlayList.cpp
new file mode 100644
index 0000000..dc83a75
--- /dev/null
+++ b/xbmc/playlists/SmartPlayList.cpp
@@ -0,0 +1,1610 @@
+/*
+ * 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 "SmartPlayList.h"
+
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dbwrappers/Database.h"
+#include "filesystem/File.h"
+#include "filesystem/SmartPlaylistDirectory.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/DatabaseUtils.h"
+#include "utils/JSONVariantParser.h"
+#include "utils/JSONVariantWriter.h"
+#include "utils/StreamDetails.h"
+#include "utils/StringUtils.h"
+#include "utils/StringValidation.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <cstdlib>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+using namespace XFILE;
+
+typedef struct
+{
+ char string[17];
+ Field field;
+ CDatabaseQueryRule::FIELD_TYPE type;
+ StringValidation::Validator validator;
+ bool browseable;
+ int localizedString;
+} translateField;
+
+// clang-format off
+static const translateField fields[] = {
+ { "none", FieldNone, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 231 },
+ { "filename", FieldFilename, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 561 },
+ { "path", FieldPath, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 573 },
+ { "album", FieldAlbum, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 558 },
+ { "albumartist", FieldAlbumArtist, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 566 },
+ { "artist", FieldArtist, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 557 },
+ { "tracknumber", FieldTrackNumber, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 554 },
+ { "role", FieldRole, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 38033 },
+ { "comment", FieldComment, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 569 },
+ { "review", FieldReview, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 183 },
+ { "themes", FieldThemes, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21895 },
+ { "moods", FieldMoods, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 175 },
+ { "styles", FieldStyles, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 176 },
+ { "type", FieldAlbumType, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 564 },
+ { "compilation", FieldCompilation, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 204 },
+ { "label", FieldMusicLabel, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21899 },
+ { "title", FieldTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 556 },
+ { "sorttitle", FieldSortTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 171 },
+ { "originaltitle", FieldOriginalTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 20376 },
+ { "year", FieldYear, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, true, 562 },
+ { "time", FieldTime, CDatabaseQueryRule::SECONDS_FIELD, StringValidation::IsTime, false, 180 },
+ { "playcount", FieldPlaycount, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 567 },
+ { "lastplayed", FieldLastPlayed, CDatabaseQueryRule::DATE_FIELD, NULL, false, 568 },
+ { "inprogress", FieldInProgress, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 575 },
+ { "rating", FieldRating, CDatabaseQueryRule::REAL_FIELD, CSmartPlaylistRule::ValidateRating, false, 563 },
+ { "userrating", FieldUserRating, CDatabaseQueryRule::REAL_FIELD, CSmartPlaylistRule::ValidateMyRating, false, 38018 },
+ { "votes", FieldVotes, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 205 },
+ { "top250", FieldTop250, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 13409 },
+ { "mpaarating", FieldMPAA, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 20074 },
+ { "dateadded", FieldDateAdded, CDatabaseQueryRule::DATE_FIELD, NULL, false, 570 },
+ { "datemodified", FieldDateModified, CDatabaseQueryRule::DATE_FIELD, NULL, false, 39119 },
+ { "datenew", FieldDateNew, CDatabaseQueryRule::DATE_FIELD, NULL, false, 21877 },
+ { "genre", FieldGenre, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 515 },
+ { "plot", FieldPlot, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 207 },
+ { "plotoutline", FieldPlotOutline, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 203 },
+ { "tagline", FieldTagline, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 202 },
+ { "set", FieldSet, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20457 },
+ { "director", FieldDirector, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20339 },
+ { "actor", FieldActor, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20337 },
+ { "writers", FieldWriter, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20417 },
+ { "airdate", FieldAirDate, CDatabaseQueryRule::DATE_FIELD, NULL, false, 20416 },
+ { "hastrailer", FieldTrailer, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 20423 },
+ { "studio", FieldStudio, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 572 },
+ { "country", FieldCountry, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 574 },
+ { "tvshow", FieldTvShowTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20364 },
+ { "status", FieldTvShowStatus, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 126 },
+ { "season", FieldSeason, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 20373 },
+ { "episode", FieldEpisodeNumber, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 20359 },
+ { "numepisodes", FieldNumberOfEpisodes, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 20360 },
+ { "numwatched", FieldNumberOfWatchedEpisodes, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 21457 },
+ { "videoresolution", FieldVideoResolution, CDatabaseQueryRule::REAL_FIELD, NULL, false, 21443 },
+ { "videocodec", FieldVideoCodec, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21445 },
+ { "videoaspect", FieldVideoAspectRatio, CDatabaseQueryRule::REAL_FIELD, NULL, false, 21374 },
+ { "audiochannels", FieldAudioChannels, CDatabaseQueryRule::REAL_FIELD, NULL, false, 21444 },
+ { "audiocodec", FieldAudioCodec, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21446 },
+ { "audiolanguage", FieldAudioLanguage, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21447 },
+ { "audiocount", FieldAudioCount, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 21481 },
+ { "subtitlecount", FieldSubtitleCount, CDatabaseQueryRule::REAL_FIELD, StringValidation::IsPositiveInteger, false, 21482 },
+ { "subtitlelanguage", FieldSubtitleLanguage, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 21448 },
+ { "random", FieldRandom, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 590 },
+ { "playlist", FieldPlaylist, CDatabaseQueryRule::PLAYLIST_FIELD, NULL, true, 559 },
+ { "virtualfolder", FieldVirtualFolder, CDatabaseQueryRule::PLAYLIST_FIELD, NULL, true, 614 },
+ { "tag", FieldTag, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 20459 },
+ { "instruments", FieldInstruments, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21892 },
+ { "biography", FieldBiography, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21887 },
+ { "born", FieldBorn, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21893 },
+ { "bandformed", FieldBandFormed, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21894 },
+ { "disbanded", FieldDisbanded, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21896 },
+ { "died", FieldDied, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 21897 },
+ { "artisttype", FieldArtistType, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 564 },
+ { "gender", FieldGender, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 39025 },
+ { "disambiguation", FieldDisambiguation, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 39026 },
+ { "source", FieldSource, CDatabaseQueryRule::TEXT_FIELD, NULL, true, 39030 },
+ { "disctitle", FieldDiscTitle, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 38076 },
+ { "isboxset", FieldIsBoxset, CDatabaseQueryRule::BOOLEAN_FIELD, NULL, false, 38074 },
+ { "totaldiscs", FieldTotalDiscs, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 38077 },
+ { "originalyear", FieldOrigYear, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, true, 38078 },
+ { "bpm", FieldBPM, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 38080 },
+ { "samplerate", FieldSampleRate, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 613 },
+ { "bitrate", FieldMusicBitRate, CDatabaseQueryRule::NUMERIC_FIELD, NULL, false, 623 },
+ { "channels", FieldNoOfChannels, CDatabaseQueryRule::NUMERIC_FIELD, StringValidation::IsPositiveInteger, false, 253 },
+ { "albumstatus", FieldAlbumStatus, CDatabaseQueryRule::TEXT_FIELD, NULL, false, 38081 },
+ { "albumduration", FieldAlbumDuration, CDatabaseQueryRule::SECONDS_FIELD, StringValidation::IsTime, false, 180 },
+ { "hdrtype", FieldHdrType, CDatabaseQueryRule::TEXTIN_FIELD, NULL, false, 20474 },
+};
+// clang-format on
+
+typedef struct
+{
+ std::string name;
+ Field field;
+ bool canMix;
+ int localizedString;
+} group;
+
+// clang-format off
+static const group groups[] = { { "", FieldUnknown, false, 571 },
+ { "none", FieldNone, false, 231 },
+ { "sets", FieldSet, true, 20434 },
+ { "genres", FieldGenre, false, 135 },
+ { "years", FieldYear, false, 652 },
+ { "actors", FieldActor, false, 344 },
+ { "directors", FieldDirector, false, 20348 },
+ { "writers", FieldWriter, false, 20418 },
+ { "studios", FieldStudio, false, 20388 },
+ { "countries", FieldCountry, false, 20451 },
+ { "artists", FieldArtist, false, 133 },
+ { "albums", FieldAlbum, false, 132 },
+ { "tags", FieldTag, false, 20459 },
+ { "originalyears", FieldOrigYear, false, 38078 },
+ };
+// clang-format on
+
+#define RULE_VALUE_SEPARATOR " / "
+
+CSmartPlaylistRule::CSmartPlaylistRule() = default;
+
+int CSmartPlaylistRule::TranslateField(const char *field) const
+{
+ for (const translateField& f : fields)
+ if (StringUtils::EqualsNoCase(field, f.string)) return f.field;
+ return FieldNone;
+}
+
+std::string CSmartPlaylistRule::TranslateField(int field) const
+{
+ for (const translateField& f : fields)
+ if (field == f.field) return f.string;
+ return "none";
+}
+
+SortBy CSmartPlaylistRule::TranslateOrder(const char *order)
+{
+ return SortUtils::SortMethodFromString(order);
+}
+
+std::string CSmartPlaylistRule::TranslateOrder(SortBy order)
+{
+ std::string sortOrder = SortUtils::SortMethodToString(order);
+ if (sortOrder.empty())
+ return "none";
+
+ return sortOrder;
+}
+
+Field CSmartPlaylistRule::TranslateGroup(const char *group)
+{
+ for (const auto & i : groups)
+ {
+ if (StringUtils::EqualsNoCase(group, i.name))
+ return i.field;
+ }
+
+ return FieldUnknown;
+}
+
+std::string CSmartPlaylistRule::TranslateGroup(Field group)
+{
+ for (const auto & i : groups)
+ {
+ if (group == i.field)
+ return i.name;
+ }
+
+ return "";
+}
+
+std::string CSmartPlaylistRule::GetLocalizedField(int field)
+{
+ for (const translateField& f : fields)
+ if (field == f.field) return g_localizeStrings.Get(f.localizedString);
+ return g_localizeStrings.Get(16018);
+}
+
+CDatabaseQueryRule::FIELD_TYPE CSmartPlaylistRule::GetFieldType(int field) const
+{
+ for (const translateField& f : fields)
+ if (field == f.field) return f.type;
+ return TEXT_FIELD;
+}
+
+bool CSmartPlaylistRule::IsFieldBrowseable(int field)
+{
+ for (const translateField& f : fields)
+ if (field == f.field) return f.browseable;
+
+ return false;
+}
+
+bool CSmartPlaylistRule::Validate(const std::string &input, void *data)
+{
+ if (data == NULL)
+ return true;
+
+ CSmartPlaylistRule *rule = static_cast<CSmartPlaylistRule*>(data);
+
+ // check if there's a validator for this rule
+ StringValidation::Validator validator = NULL;
+ for (const translateField& field : fields)
+ {
+ if (rule->m_field == field.field)
+ {
+ validator = field.validator;
+ break;
+ }
+ }
+ if (validator == NULL)
+ return true;
+
+ // split the input into multiple values and validate every value separately
+ std::vector<std::string> values = StringUtils::Split(input, RULE_VALUE_SEPARATOR);
+ for (std::vector<std::string>::const_iterator it = values.begin(); it != values.end(); ++it)
+ {
+ if (!validator(*it, data))
+ return false;
+ }
+
+ return true;
+}
+
+bool CSmartPlaylistRule::ValidateRating(const std::string &input, void *data)
+{
+ char *end = NULL;
+ std::string strRating = input;
+ StringUtils::Trim(strRating);
+
+ double rating = std::strtod(strRating.c_str(), &end);
+ return (end == NULL || *end == '\0') &&
+ rating >= 0.0 && rating <= 10.0;
+}
+
+bool CSmartPlaylistRule::ValidateMyRating(const std::string &input, void *data)
+{
+ std::string strRating = input;
+ StringUtils::Trim(strRating);
+
+ int rating = atoi(strRating.c_str());
+ return StringValidation::IsPositiveInteger(input, data) && rating <= 10;
+}
+
+std::vector<Field> CSmartPlaylistRule::GetFields(const std::string &type)
+{
+ std::vector<Field> fields;
+ bool isVideo = false;
+ if (type == "mixed")
+ {
+ fields.push_back(FieldGenre);
+ fields.push_back(FieldAlbum);
+ fields.push_back(FieldArtist);
+ fields.push_back(FieldAlbumArtist);
+ fields.push_back(FieldTitle);
+ fields.push_back(FieldOriginalTitle);
+ fields.push_back(FieldYear);
+ fields.push_back(FieldTime);
+ fields.push_back(FieldTrackNumber);
+ fields.push_back(FieldFilename);
+ fields.push_back(FieldPath);
+ fields.push_back(FieldPlaycount);
+ fields.push_back(FieldLastPlayed);
+ }
+ else if (type == "songs")
+ {
+ fields.push_back(FieldGenre);
+ fields.push_back(FieldSource);
+ fields.push_back(FieldAlbum);
+ fields.push_back(FieldDiscTitle);
+ fields.push_back(FieldArtist);
+ fields.push_back(FieldAlbumArtist);
+ fields.push_back(FieldTitle);
+ fields.push_back(FieldYear);
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ fields.push_back(FieldOrigYear);
+ fields.push_back(FieldTime);
+ fields.push_back(FieldTrackNumber);
+ fields.push_back(FieldFilename);
+ fields.push_back(FieldPath);
+ fields.push_back(FieldPlaycount);
+ fields.push_back(FieldLastPlayed);
+ fields.push_back(FieldRating);
+ fields.push_back(FieldUserRating);
+ fields.push_back(FieldComment);
+ fields.push_back(FieldMoods);
+ fields.push_back(FieldBPM);
+ fields.push_back(FieldSampleRate);
+ fields.push_back(FieldMusicBitRate);
+ fields.push_back(FieldNoOfChannels);
+ fields.push_back(FieldDateAdded);
+ fields.push_back(FieldDateModified);
+ fields.push_back(FieldDateNew);
+ }
+ else if (type == "albums")
+ {
+ fields.push_back(FieldGenre);
+ fields.push_back(FieldSource);
+ fields.push_back(FieldAlbum);
+ fields.push_back(FieldDiscTitle);
+ fields.push_back(FieldTotalDiscs);
+ fields.push_back(FieldIsBoxset);
+ fields.push_back(FieldArtist); // any artist
+ fields.push_back(FieldAlbumArtist); // album artist
+ fields.push_back(FieldYear);
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ fields.push_back(FieldOrigYear);
+ fields.push_back(FieldAlbumDuration);
+ fields.push_back(FieldReview);
+ fields.push_back(FieldThemes);
+ fields.push_back(FieldMoods);
+ fields.push_back(FieldStyles);
+ fields.push_back(FieldCompilation);
+ fields.push_back(FieldAlbumType);
+ fields.push_back(FieldMusicLabel);
+ fields.push_back(FieldRating);
+ fields.push_back(FieldUserRating);
+ fields.push_back(FieldPlaycount);
+ fields.push_back(FieldLastPlayed);
+ fields.push_back(FieldPath);
+ fields.push_back(FieldAlbumStatus);
+ fields.push_back(FieldDateAdded);
+ fields.push_back(FieldDateModified);
+ fields.push_back(FieldDateNew);
+ }
+ else if (type == "artists")
+ {
+ fields.push_back(FieldArtist);
+ fields.push_back(FieldSource);
+ fields.push_back(FieldGenre);
+ fields.push_back(FieldMoods);
+ fields.push_back(FieldStyles);
+ fields.push_back(FieldInstruments);
+ fields.push_back(FieldBiography);
+ fields.push_back(FieldArtistType);
+ fields.push_back(FieldGender);
+ fields.push_back(FieldDisambiguation);
+ fields.push_back(FieldBorn);
+ fields.push_back(FieldBandFormed);
+ fields.push_back(FieldDisbanded);
+ fields.push_back(FieldDied);
+ fields.push_back(FieldRole);
+ fields.push_back(FieldPath);
+ fields.push_back(FieldDateAdded);
+ fields.push_back(FieldDateModified);
+ fields.push_back(FieldDateNew);
+ }
+ else if (type == "tvshows")
+ {
+ fields.push_back(FieldTitle);
+ fields.push_back(FieldOriginalTitle);
+ fields.push_back(FieldPlot);
+ fields.push_back(FieldTvShowStatus);
+ fields.push_back(FieldVotes);
+ fields.push_back(FieldRating);
+ fields.push_back(FieldUserRating);
+ fields.push_back(FieldYear);
+ fields.push_back(FieldGenre);
+ fields.push_back(FieldDirector);
+ fields.push_back(FieldActor);
+ fields.push_back(FieldNumberOfEpisodes);
+ fields.push_back(FieldNumberOfWatchedEpisodes);
+ fields.push_back(FieldPlaycount);
+ fields.push_back(FieldPath);
+ fields.push_back(FieldStudio);
+ fields.push_back(FieldMPAA);
+ fields.push_back(FieldDateAdded);
+ fields.push_back(FieldLastPlayed);
+ fields.push_back(FieldInProgress);
+ fields.push_back(FieldTag);
+ }
+ else if (type == "episodes")
+ {
+ fields.push_back(FieldTitle);
+ fields.push_back(FieldTvShowTitle);
+ fields.push_back(FieldOriginalTitle);
+ fields.push_back(FieldPlot);
+ fields.push_back(FieldVotes);
+ fields.push_back(FieldRating);
+ fields.push_back(FieldUserRating);
+ fields.push_back(FieldTime);
+ fields.push_back(FieldWriter);
+ fields.push_back(FieldAirDate);
+ fields.push_back(FieldPlaycount);
+ fields.push_back(FieldLastPlayed);
+ fields.push_back(FieldInProgress);
+ fields.push_back(FieldGenre);
+ fields.push_back(FieldYear); // premiered
+ fields.push_back(FieldDirector);
+ fields.push_back(FieldActor);
+ fields.push_back(FieldEpisodeNumber);
+ fields.push_back(FieldSeason);
+ fields.push_back(FieldFilename);
+ fields.push_back(FieldPath);
+ fields.push_back(FieldStudio);
+ fields.push_back(FieldMPAA);
+ fields.push_back(FieldDateAdded);
+ fields.push_back(FieldTag);
+ isVideo = true;
+ }
+ else if (type == "movies")
+ {
+ fields.push_back(FieldTitle);
+ fields.push_back(FieldOriginalTitle);
+ fields.push_back(FieldPlot);
+ fields.push_back(FieldPlotOutline);
+ fields.push_back(FieldTagline);
+ fields.push_back(FieldVotes);
+ fields.push_back(FieldRating);
+ fields.push_back(FieldUserRating);
+ fields.push_back(FieldTime);
+ fields.push_back(FieldWriter);
+ fields.push_back(FieldPlaycount);
+ fields.push_back(FieldLastPlayed);
+ fields.push_back(FieldInProgress);
+ fields.push_back(FieldGenre);
+ fields.push_back(FieldCountry);
+ fields.push_back(FieldYear); // premiered
+ fields.push_back(FieldDirector);
+ fields.push_back(FieldActor);
+ fields.push_back(FieldMPAA);
+ fields.push_back(FieldTop250);
+ fields.push_back(FieldStudio);
+ fields.push_back(FieldTrailer);
+ fields.push_back(FieldFilename);
+ fields.push_back(FieldPath);
+ fields.push_back(FieldSet);
+ fields.push_back(FieldTag);
+ fields.push_back(FieldDateAdded);
+ isVideo = true;
+ }
+ else if (type == "musicvideos")
+ {
+ fields.push_back(FieldTitle);
+ fields.push_back(FieldGenre);
+ fields.push_back(FieldAlbum);
+ fields.push_back(FieldYear);
+ fields.push_back(FieldArtist);
+ fields.push_back(FieldFilename);
+ fields.push_back(FieldPath);
+ fields.push_back(FieldPlaycount);
+ fields.push_back(FieldLastPlayed);
+ fields.push_back(FieldRating);
+ fields.push_back(FieldUserRating);
+ fields.push_back(FieldTime);
+ fields.push_back(FieldDirector);
+ fields.push_back(FieldStudio);
+ fields.push_back(FieldPlot);
+ fields.push_back(FieldTag);
+ fields.push_back(FieldDateAdded);
+ isVideo = true;
+ }
+ if (isVideo)
+ {
+ fields.push_back(FieldVideoResolution);
+ fields.push_back(FieldAudioChannels);
+ fields.push_back(FieldAudioCount);
+ fields.push_back(FieldSubtitleCount);
+ fields.push_back(FieldVideoCodec);
+ fields.push_back(FieldAudioCodec);
+ fields.push_back(FieldAudioLanguage);
+ fields.push_back(FieldSubtitleLanguage);
+ fields.push_back(FieldVideoAspectRatio);
+ fields.push_back(FieldHdrType);
+ }
+ fields.push_back(FieldPlaylist);
+ fields.push_back(FieldVirtualFolder);
+
+ return fields;
+}
+
+std::vector<SortBy> CSmartPlaylistRule::GetOrders(const std::string &type)
+{
+ std::vector<SortBy> orders;
+ orders.push_back(SortByNone);
+ if (type == "mixed")
+ {
+ orders.push_back(SortByGenre);
+ orders.push_back(SortByAlbum);
+ orders.push_back(SortByArtist);
+ orders.push_back(SortByTitle);
+ orders.push_back(SortByYear);
+ orders.push_back(SortByTime);
+ orders.push_back(SortByTrackNumber);
+ orders.push_back(SortByFile);
+ orders.push_back(SortByPath);
+ orders.push_back(SortByPlaycount);
+ orders.push_back(SortByLastPlayed);
+ }
+ else if (type == "songs")
+ {
+ orders.push_back(SortByGenre);
+ orders.push_back(SortByAlbum);
+ orders.push_back(SortByArtist);
+ orders.push_back(SortByTitle);
+ orders.push_back(SortByYear);
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ orders.push_back(SortByOrigDate);
+ orders.push_back(SortByTime);
+ orders.push_back(SortByTrackNumber);
+ orders.push_back(SortByFile);
+ orders.push_back(SortByPath);
+ orders.push_back(SortByPlaycount);
+ orders.push_back(SortByLastPlayed);
+ orders.push_back(SortByDateAdded);
+ orders.push_back(SortByRating);
+ orders.push_back(SortByUserRating);
+ orders.push_back(SortByBPM);
+ }
+ else if (type == "albums")
+ {
+ orders.push_back(SortByGenre);
+ orders.push_back(SortByAlbum);
+ orders.push_back(SortByTotalDiscs);
+ orders.push_back(SortByArtist); // any artist
+ orders.push_back(SortByYear);
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ orders.push_back(SortByOrigDate);
+ //orders.push_back(SortByThemes);
+ //orders.push_back(SortByMoods);
+ //orders.push_back(SortByStyles);
+ orders.push_back(SortByAlbumType);
+ //orders.push_back(SortByMusicLabel);
+ orders.push_back(SortByRating);
+ orders.push_back(SortByUserRating);
+ orders.push_back(SortByPlaycount);
+ orders.push_back(SortByLastPlayed);
+ orders.push_back(SortByDateAdded);
+ }
+ else if (type == "artists")
+ {
+ orders.push_back(SortByArtist);
+ }
+ else if (type == "tvshows")
+ {
+ orders.push_back(SortBySortTitle);
+ orders.push_back(SortByOriginalTitle);
+ orders.push_back(SortByTvShowStatus);
+ orders.push_back(SortByVotes);
+ orders.push_back(SortByRating);
+ orders.push_back(SortByUserRating);
+ orders.push_back(SortByYear);
+ orders.push_back(SortByGenre);
+ orders.push_back(SortByNumberOfEpisodes);
+ orders.push_back(SortByNumberOfWatchedEpisodes);
+ //orders.push_back(SortByPlaycount);
+ orders.push_back(SortByPath);
+ orders.push_back(SortByStudio);
+ orders.push_back(SortByMPAA);
+ orders.push_back(SortByDateAdded);
+ orders.push_back(SortByLastPlayed);
+ }
+ else if (type == "episodes")
+ {
+ orders.push_back(SortByTitle);
+ orders.push_back(SortByOriginalTitle);
+ orders.push_back(SortByTvShowTitle);
+ orders.push_back(SortByVotes);
+ orders.push_back(SortByRating);
+ orders.push_back(SortByUserRating);
+ orders.push_back(SortByTime);
+ orders.push_back(SortByPlaycount);
+ orders.push_back(SortByLastPlayed);
+ orders.push_back(SortByYear); // premiered/dateaired
+ orders.push_back(SortByEpisodeNumber);
+ orders.push_back(SortBySeason);
+ orders.push_back(SortByFile);
+ orders.push_back(SortByPath);
+ orders.push_back(SortByStudio);
+ orders.push_back(SortByMPAA);
+ orders.push_back(SortByDateAdded);
+ }
+ else if (type == "movies")
+ {
+ orders.push_back(SortBySortTitle);
+ orders.push_back(SortByOriginalTitle);
+ orders.push_back(SortByVotes);
+ orders.push_back(SortByRating);
+ orders.push_back(SortByUserRating);
+ orders.push_back(SortByTime);
+ orders.push_back(SortByPlaycount);
+ orders.push_back(SortByLastPlayed);
+ orders.push_back(SortByGenre);
+ orders.push_back(SortByCountry);
+ orders.push_back(SortByYear); // premiered
+ orders.push_back(SortByMPAA);
+ orders.push_back(SortByTop250);
+ orders.push_back(SortByStudio);
+ orders.push_back(SortByFile);
+ orders.push_back(SortByPath);
+ orders.push_back(SortByDateAdded);
+ }
+ else if (type == "musicvideos")
+ {
+ orders.push_back(SortByTitle);
+ orders.push_back(SortByGenre);
+ orders.push_back(SortByAlbum);
+ orders.push_back(SortByYear);
+ orders.push_back(SortByArtist);
+ orders.push_back(SortByFile);
+ orders.push_back(SortByPath);
+ orders.push_back(SortByPlaycount);
+ orders.push_back(SortByLastPlayed);
+ orders.push_back(SortByTime);
+ orders.push_back(SortByRating);
+ orders.push_back(SortByUserRating);
+ orders.push_back(SortByStudio);
+ orders.push_back(SortByDateAdded);
+ }
+ orders.push_back(SortByRandom);
+
+ return orders;
+}
+
+std::vector<Field> CSmartPlaylistRule::GetGroups(const std::string &type)
+{
+ std::vector<Field> groups;
+ groups.push_back(FieldUnknown);
+
+ if (type == "artists")
+ groups.push_back(FieldGenre);
+ else if (type == "albums")
+ {
+ groups.push_back(FieldYear);
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ groups.push_back(FieldOrigYear);
+ }
+ if (type == "movies")
+ {
+ groups.push_back(FieldNone);
+ groups.push_back(FieldSet);
+ groups.push_back(FieldGenre);
+ groups.push_back(FieldYear);
+ groups.push_back(FieldActor);
+ groups.push_back(FieldDirector);
+ groups.push_back(FieldWriter);
+ groups.push_back(FieldStudio);
+ groups.push_back(FieldCountry);
+ groups.push_back(FieldTag);
+ }
+ else if (type == "tvshows")
+ {
+ groups.push_back(FieldGenre);
+ groups.push_back(FieldYear);
+ groups.push_back(FieldActor);
+ groups.push_back(FieldDirector);
+ groups.push_back(FieldStudio);
+ groups.push_back(FieldTag);
+ }
+ else if (type == "musicvideos")
+ {
+ groups.push_back(FieldArtist);
+ groups.push_back(FieldAlbum);
+ groups.push_back(FieldGenre);
+ groups.push_back(FieldYear);
+ groups.push_back(FieldDirector);
+ groups.push_back(FieldStudio);
+ groups.push_back(FieldTag);
+ }
+
+ return groups;
+}
+
+std::string CSmartPlaylistRule::GetLocalizedGroup(Field group)
+{
+ for (const auto & i : groups)
+ {
+ if (group == i.field)
+ return g_localizeStrings.Get(i.localizedString);
+ }
+
+ return g_localizeStrings.Get(groups[0].localizedString);
+}
+
+bool CSmartPlaylistRule::CanGroupMix(Field group)
+{
+ for (const auto & i : groups)
+ {
+ if (group == i.field)
+ return i.canMix;
+ }
+
+ return false;
+}
+
+std::string CSmartPlaylistRule::GetLocalizedRule() const
+{
+ return StringUtils::Format("{} {} {}", GetLocalizedField(m_field),
+ GetLocalizedOperator(m_operator), GetParameter());
+}
+
+std::string CSmartPlaylistRule::GetVideoResolutionQuery(const std::string &parameter) const
+{
+ std::string retVal(" IN (SELECT DISTINCT idFile FROM streamdetails WHERE iVideoWidth ");
+ int iRes = (int)std::strtol(parameter.c_str(), NULL, 10);
+
+ int min, max;
+ if (iRes >= 2160)
+ {
+ min = 1921;
+ max = INT_MAX;
+ }
+ else if (iRes >= 1080) { min = 1281; max = 1920; }
+ else if (iRes >= 720) { min = 961; max = 1280; }
+ else if (iRes >= 540) { min = 721; max = 960; }
+ else { min = 0; max = 720; }
+
+ switch (m_operator)
+ {
+ case OPERATOR_EQUALS:
+ retVal += StringUtils::Format(">= {} AND iVideoWidth <= {}", min, max);
+ break;
+ case OPERATOR_DOES_NOT_EQUAL:
+ retVal += StringUtils::Format("< {} OR iVideoWidth > {}", min, max);
+ break;
+ case OPERATOR_LESS_THAN:
+ retVal += StringUtils::Format("< {}", min);
+ break;
+ case OPERATOR_GREATER_THAN:
+ retVal += StringUtils::Format("> {}", max);
+ break;
+ default:
+ break;
+ }
+
+ retVal += ")";
+ return retVal;
+}
+
+std::string CSmartPlaylistRule::GetBooleanQuery(const std::string &negate, const std::string &strType) const
+{
+ if (strType == "movies")
+ {
+ if (m_field == FieldInProgress)
+ return "movie_view.idFile " + negate + " IN (SELECT DISTINCT idFile FROM bookmark WHERE type = 1)";
+ else if (m_field == FieldTrailer)
+ return negate + GetField(m_field, strType) + "!= ''";
+ }
+ else if (strType == "episodes")
+ {
+ if (m_field == FieldInProgress)
+ return "episode_view.idFile " + negate + " IN (SELECT DISTINCT idFile FROM bookmark WHERE type = 1)";
+ }
+ else if (strType == "tvshows")
+ {
+ if (m_field == FieldInProgress)
+ return negate + " ("
+ "(tvshow_view.watchedcount > 0 AND tvshow_view.watchedcount < tvshow_view.totalCount) OR "
+ "(tvshow_view.watchedcount = 0 AND EXISTS "
+ "(SELECT 1 FROM episode_view WHERE episode_view.idShow = " + GetField(FieldId, strType) + " AND episode_view.resumeTimeInSeconds > 0)"
+ ")"
+ ")";
+ }
+ if (strType == "albums")
+ {
+ if (m_field == FieldCompilation)
+ return negate + GetField(m_field, strType);
+ if (m_field == FieldIsBoxset)
+ return negate + "albumview.bBoxedSet = 1";
+ }
+ return "";
+}
+
+CDatabaseQueryRule::SEARCH_OPERATOR CSmartPlaylistRule::GetOperator(const std::string &strType) const
+{
+ SEARCH_OPERATOR op = CDatabaseQueryRule::GetOperator(strType);
+ if ((strType == "tvshows" || strType == "episodes") && m_field == FieldYear)
+ { // special case for premiered which is a date rather than a year
+ //! @todo SMARTPLAYLISTS do we really need this, or should we just make this field the premiered date and request a date?
+ if (op == OPERATOR_EQUALS)
+ op = OPERATOR_CONTAINS;
+ else if (op == OPERATOR_DOES_NOT_EQUAL)
+ op = OPERATOR_DOES_NOT_CONTAIN;
+ }
+ return op;
+}
+
+std::string CSmartPlaylistRule::FormatParameter(const std::string &operatorString, const std::string &param, const CDatabase &db, const std::string &strType) const
+{
+ // special-casing
+ if (m_field == FieldTime || m_field == FieldAlbumDuration)
+ { // translate time to seconds
+ std::string seconds = std::to_string(StringUtils::TimeStringToSeconds(param));
+ return db.PrepareSQL(operatorString, seconds.c_str());
+ }
+ return CDatabaseQueryRule::FormatParameter(operatorString, param, db, strType);
+}
+
+std::string CSmartPlaylistRule::FormatLinkQuery(const char *field, const char *table, const MediaType& mediaType, const std::string& mediaField, const std::string& parameter)
+{
+ // NOTE: no need for a PrepareSQL here, as the parameter has already been formatted
+ return StringUtils::Format(
+ " EXISTS (SELECT 1 FROM {}_link"
+ " JOIN {} ON {}.{}_id={}_link.{}_id"
+ " WHERE {}_link.media_id={} AND {}.name {} AND {}_link.media_type = '{}')",
+ field, table, table, table, field, table, field, mediaField, table, parameter, field,
+ mediaType);
+}
+
+std::string CSmartPlaylistRule::FormatYearQuery(const std::string& field,
+ const std::string& param,
+ const std::string& parameter) const
+{
+ std::string query;
+ if (m_operator == OPERATOR_EQUALS && param == "0")
+ query = "(TRIM(" + field + ") = '' OR " + field + " IS NULL)";
+ else if (m_operator == OPERATOR_DOES_NOT_EQUAL && param == "0")
+ query = "(TRIM(" + field + ") <> '' AND " + field + " IS NOT NULL)";
+ else
+ { // Get year from ISO8601 date string, cast as INTEGER
+ query = "CAST(" + field + " as INTEGER)" + parameter;
+ if (m_operator == OPERATOR_LESS_THAN)
+ query = "(TRIM(" + field + ") = '' OR " + field + " IS NULL OR " + query + ")";
+ }
+ return query;
+}
+
+std::string CSmartPlaylistRule::FormatWhereClause(const std::string &negate, const std::string &oper, const std::string &param,
+ const CDatabase &db, const std::string &strType) const
+{
+ std::string parameter = FormatParameter(oper, param, db, strType);
+
+ std::string query;
+ std::string table;
+ if (strType == "songs")
+ {
+ table = "songview";
+
+ if (m_field == FieldGenre)
+ query = negate + " EXISTS (SELECT 1 FROM song_genre, genre WHERE song_genre.idSong = " + GetField(FieldId, strType) + " AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + ")";
+ else if (m_field == FieldArtist)
+ query = negate + " EXISTS (SELECT 1 FROM song_artist, artist WHERE song_artist.idSong = " + GetField(FieldId, strType) + " AND song_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")";
+ else if (m_field == FieldAlbumArtist)
+ query = negate + " EXISTS (SELECT 1 FROM album_artist, artist WHERE album_artist.idAlbum = " + table + ".idAlbum AND album_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")";
+ else if (m_field == FieldLastPlayed && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST))
+ query = GetField(m_field, strType) + " is NULL or " + GetField(m_field, strType) + parameter;
+ else if (m_field == FieldSource)
+ query = negate + " EXISTS (SELECT 1 FROM album_source, source WHERE album_source.idAlbum = " + table + ".idAlbum AND album_source.idSource = source.idSource AND source.strName" + parameter + ")";
+ else if (m_field == FieldYear || m_field == FieldOrigYear)
+ {
+ std::string field;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ field = GetField(FieldOrigYear, strType);
+ else
+ field = GetField(m_field, strType);
+ query = FormatYearQuery(field, param, parameter);
+ }
+ }
+ else if (strType == "albums")
+ {
+ table = "albumview";
+
+ if (m_field == FieldGenre)
+ query = negate + " EXISTS (SELECT 1 FROM song, song_genre, genre WHERE song.idAlbum = " + GetField(FieldId, strType) + " AND song.idSong = song_genre.idSong AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + ")";
+ else if (m_field == FieldArtist)
+ query = negate + " EXISTS (SELECT 1 FROM song, song_artist, artist WHERE song.idAlbum = " + GetField(FieldId, strType) + " AND song.idSong = song_artist.idSong AND song_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")";
+ else if (m_field == FieldAlbumArtist)
+ query = negate + " EXISTS (SELECT 1 FROM album_artist, artist WHERE album_artist.idAlbum = " + GetField(FieldId, strType) + " AND album_artist.idArtist = artist.idArtist AND artist.strArtist" + parameter + ")";
+ else if (m_field == FieldPath)
+ query = negate + " EXISTS (SELECT 1 FROM song JOIN path on song.idpath = path.idpath WHERE song.idAlbum = " + GetField(FieldId, strType) + " AND path.strPath" + parameter + ")";
+ else if (m_field == FieldLastPlayed && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST))
+ query = GetField(m_field, strType) + " is NULL or " + GetField(m_field, strType) + parameter;
+ else if (m_field == FieldSource)
+ query = negate + " EXISTS (SELECT 1 FROM album_source, source WHERE album_source.idAlbum = " + GetField(FieldId, strType) + " AND album_source.idSource = source.idSource AND source.strName" + parameter + ")";
+ else if (m_field == FieldDiscTitle)
+ query = negate +
+ " EXISTS (SELECT 1 FROM song WHERE song.idAlbum = " + GetField(FieldId, strType) +
+ " AND song.strDiscSubtitle" + parameter + ")";
+ else if (m_field == FieldYear || m_field == FieldOrigYear)
+ {
+ std::string field;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE))
+ field = GetField(FieldOrigYear, strType);
+ else
+ field = GetField(m_field, strType);
+ query = FormatYearQuery(field, param, parameter);
+ }
+ }
+ else if (strType == "artists")
+ {
+ table = "artistview";
+
+ if (m_field == FieldGenre)
+ {
+ query = negate + " (EXISTS (SELECT DISTINCT song_artist.idArtist FROM song_artist, song_genre, genre WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND song_artist.idSong = song_genre.idSong AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + ")";
+ query += " OR ";
+ query += "EXISTS (SELECT DISTINCT album_artist.idArtist FROM album_artist, song, song_genre, genre WHERE album_artist.idArtist = " + GetField(FieldId, strType) + " AND song.idAlbum = album_artist.idAlbum AND song.idSong = song_genre.idSong AND song_genre.idGenre = genre.idGenre AND genre.strGenre" + parameter + "))";
+ }
+ else if (m_field == FieldRole)
+ {
+ query = negate + " (EXISTS (SELECT DISTINCT song_artist.idArtist FROM song_artist, role WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND song_artist.idRole = role.idRole AND role.strRole" + parameter + "))";
+ }
+ else if (m_field == FieldPath)
+ {
+ query = negate + " (EXISTS (SELECT DISTINCT song_artist.idArtist FROM song_artist JOIN song ON song.idSong = song_artist.idSong JOIN path ON song.idpath = path.idpath ";
+ query += "WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND path.strPath" + parameter + "))";
+ }
+ else if (m_field == FieldSource)
+ {
+ query = negate + " (EXISTS(SELECT 1 FROM song_artist, song, album_source, source WHERE song_artist.idArtist = " + GetField(FieldId, strType) + " AND song.idSong = song_artist.idSong AND song_artist.idRole = 1 AND album_source.idAlbum = song.idAlbum AND album_source.idSource = source.idSource AND source.strName" + parameter + ")";
+ query += " OR ";
+ query += " EXISTS (SELECT 1 FROM album_artist, album_source, source WHERE album_artist.idArtist = " + GetField(FieldId, strType) + " AND album_source.idAlbum = album_artist.idAlbum AND album_source.idSource = source.idSource AND source.strName" + parameter + "))";
+ }
+ }
+ else if (strType == "movies")
+ {
+ table = "movie_view";
+
+ if (m_field == FieldGenre)
+ query = negate + FormatLinkQuery("genre", "genre", MediaTypeMovie, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldDirector)
+ query = negate + FormatLinkQuery("director", "actor", MediaTypeMovie, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldActor)
+ query = negate + FormatLinkQuery("actor", "actor", MediaTypeMovie, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldWriter)
+ query = negate + FormatLinkQuery("writer", "actor", MediaTypeMovie, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldStudio)
+ query = negate + FormatLinkQuery("studio", "studio", MediaTypeMovie, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldCountry)
+ query = negate + FormatLinkQuery("country", "country", MediaTypeMovie, GetField(FieldId, strType), parameter);
+ else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST))
+ query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter;
+ else if (m_field == FieldTag)
+ query = negate + FormatLinkQuery("tag", "tag", MediaTypeMovie, GetField(FieldId, strType), parameter);
+ }
+ else if (strType == "musicvideos")
+ {
+ table = "musicvideo_view";
+
+ if (m_field == FieldGenre)
+ query = negate + FormatLinkQuery("genre", "genre", MediaTypeMusicVideo, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldArtist || m_field == FieldAlbumArtist)
+ query = negate + FormatLinkQuery("actor", "actor", MediaTypeMusicVideo, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldStudio)
+ query = negate + FormatLinkQuery("studio", "studio", MediaTypeMusicVideo, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldDirector)
+ query = negate + FormatLinkQuery("director", "actor", MediaTypeMusicVideo, GetField(FieldId, strType), parameter);
+ else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST))
+ query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter;
+ else if (m_field == FieldTag)
+ query = negate + FormatLinkQuery("tag", "tag", MediaTypeMusicVideo, GetField(FieldId, strType), parameter);
+ }
+ else if (strType == "tvshows")
+ {
+ table = "tvshow_view";
+
+ if (m_field == FieldGenre)
+ query = negate + FormatLinkQuery("genre", "genre", MediaTypeTvShow, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldDirector)
+ query = negate + FormatLinkQuery("director", "actor", MediaTypeTvShow, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldActor)
+ query = negate + FormatLinkQuery("actor", "actor", MediaTypeTvShow, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldStudio)
+ query = negate + FormatLinkQuery("studio", "studio", MediaTypeTvShow, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldMPAA)
+ query = negate + " (" + GetField(m_field, strType) + parameter + ")";
+ else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST))
+ query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter;
+ else if (m_field == FieldPlaycount)
+ query = "CASE WHEN COALESCE(" + GetField(FieldNumberOfEpisodes, strType) + " - " + GetField(FieldNumberOfWatchedEpisodes, strType) + ", 0) > 0 THEN 0 ELSE 1 END " + parameter;
+ else if (m_field == FieldTag)
+ query = negate + FormatLinkQuery("tag", "tag", MediaTypeTvShow, GetField(FieldId, strType), parameter);
+ }
+ else if (strType == "episodes")
+ {
+ table = "episode_view";
+
+ if (m_field == FieldGenre)
+ query = negate + FormatLinkQuery("genre", "genre", MediaTypeTvShow, (table + ".idShow").c_str(), parameter);
+ else if (m_field == FieldTag)
+ query = negate + FormatLinkQuery("tag", "tag", MediaTypeTvShow, (table + ".idShow").c_str(), parameter);
+ else if (m_field == FieldDirector)
+ query = negate + FormatLinkQuery("director", "actor", MediaTypeEpisode, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldActor)
+ query = negate + FormatLinkQuery("actor", "actor", MediaTypeEpisode, GetField(FieldId, strType), parameter);
+ else if (m_field == FieldWriter)
+ query = negate + FormatLinkQuery("writer", "actor", MediaTypeEpisode, GetField(FieldId, strType), parameter);
+ else if ((m_field == FieldLastPlayed || m_field == FieldDateAdded) && (m_operator == OPERATOR_LESS_THAN || m_operator == OPERATOR_BEFORE || m_operator == OPERATOR_NOT_IN_THE_LAST))
+ query = GetField(m_field, strType) + " IS NULL OR " + GetField(m_field, strType) + parameter;
+ else if (m_field == FieldStudio)
+ query = negate + FormatLinkQuery("studio", "studio", MediaTypeTvShow, (table + ".idShow").c_str(), parameter);
+ else if (m_field == FieldMPAA)
+ query = negate + " (" + GetField(m_field, strType) + parameter + ")";
+ }
+ if (m_field == FieldVideoResolution)
+ query = table + ".idFile" + negate + GetVideoResolutionQuery(param);
+ else if (m_field == FieldAudioChannels)
+ query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND iAudioChannels " + parameter + ")";
+ else if (m_field == FieldVideoCodec)
+ query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strVideoCodec " + parameter + ")";
+ else if (m_field == FieldAudioCodec)
+ query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strAudioCodec " + parameter + ")";
+ else if (m_field == FieldAudioLanguage)
+ query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strAudioLanguage " + parameter + ")";
+ else if (m_field == FieldSubtitleLanguage)
+ query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strSubtitleLanguage " + parameter + ")";
+ else if (m_field == FieldVideoAspectRatio)
+ query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND fVideoAspect " + parameter + ")";
+ else if (m_field == FieldAudioCount)
+ query = db.PrepareSQL(negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND streamdetails.iStreamtype = %i GROUP BY streamdetails.idFile HAVING COUNT(streamdetails.iStreamType) " + parameter + ")",CStreamDetail::AUDIO);
+ else if (m_field == FieldSubtitleCount)
+ query = db.PrepareSQL(negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND streamdetails.iStreamType = %i GROUP BY streamdetails.idFile HAVING COUNT(streamdetails.iStreamType) " + parameter + ")",CStreamDetail::SUBTITLE);
+ else if (m_field == FieldHdrType)
+ query = negate + " EXISTS (SELECT 1 FROM streamdetails WHERE streamdetails.idFile = " + table + ".idFile AND strHdrType " + parameter + ")";
+ if (m_field == FieldPlaycount && strType != "songs" && strType != "albums" && strType != "tvshows")
+ { // playcount IS stored as NULL OR number IN video db
+ if ((m_operator == OPERATOR_EQUALS && param == "0") ||
+ (m_operator == OPERATOR_DOES_NOT_EQUAL && param != "0") ||
+ (m_operator == OPERATOR_LESS_THAN))
+ {
+ std::string field = GetField(FieldPlaycount, strType);
+ query = field + " IS NULL OR " + field + parameter;
+ }
+ }
+ if (query.empty())
+ query = CDatabaseQueryRule::FormatWhereClause(negate, oper, param, db, strType);
+ return query;
+}
+
+std::string CSmartPlaylistRule::GetField(int field, const std::string &type) const
+{
+ if (field >= FieldUnknown && field < FieldMax)
+ return DatabaseUtils::GetField((Field)field, CMediaTypes::FromString(type), DatabaseQueryPartWhere);
+ return "";
+}
+
+std::string CSmartPlaylistRuleCombination::GetWhereClause(const CDatabase &db, const std::string& strType, std::set<std::string> &referencedPlaylists) const
+{
+ std::string rule;
+
+ // translate the combinations into SQL
+ for (CDatabaseQueryRuleCombinations::const_iterator it = m_combinations.begin(); it != m_combinations.end(); ++it)
+ {
+ if (it != m_combinations.begin())
+ rule += m_type == CombinationAnd ? " AND " : " OR ";
+ std::shared_ptr<CSmartPlaylistRuleCombination> combo = std::static_pointer_cast<CSmartPlaylistRuleCombination>(*it);
+ if (combo)
+ rule += "(" + combo->GetWhereClause(db, strType, referencedPlaylists) + ")";
+ }
+
+ // translate the rules into SQL
+ for (CDatabaseQueryRules::const_iterator it = m_rules.begin(); it != m_rules.end(); ++it)
+ {
+ // don't include playlists that are meant to be displayed
+ // as a virtual folders in the SQL WHERE clause
+ if ((*it)->m_field == FieldVirtualFolder)
+ continue;
+
+ if (!rule.empty())
+ rule += m_type == CombinationAnd ? " AND " : " OR ";
+ rule += "(";
+ std::string currentRule;
+ if ((*it)->m_field == FieldPlaylist)
+ {
+ std::string playlistFile = CSmartPlaylistDirectory::GetPlaylistByName((*it)->m_parameter.at(0), strType);
+ if (!playlistFile.empty() && referencedPlaylists.find(playlistFile) == referencedPlaylists.end())
+ {
+ referencedPlaylists.insert(playlistFile);
+ CSmartPlaylist playlist;
+ if (playlist.Load(playlistFile))
+ {
+ std::string playlistQuery;
+ // only playlists of same type will be part of the query
+ if (playlist.GetType() == strType || (playlist.GetType() == "mixed" && (strType == "songs" || strType == "musicvideos")) || playlist.GetType().empty())
+ {
+ playlist.SetType(strType);
+ playlistQuery = playlist.GetWhereClause(db, referencedPlaylists);
+ }
+ if (playlist.GetType() == strType)
+ {
+ if ((*it)->m_operator == CDatabaseQueryRule::OPERATOR_DOES_NOT_EQUAL)
+ currentRule = StringUtils::Format("NOT ({})", playlistQuery);
+ else
+ currentRule = playlistQuery;
+ }
+ }
+ }
+ }
+ else
+ currentRule = (*it)->GetWhereClause(db, strType);
+ // if we don't get a rule, we add '1' or '0' so the query is still valid and doesn't fail
+ if (currentRule.empty())
+ currentRule = m_type == CombinationAnd ? "'1'" : "'0'";
+ rule += currentRule;
+ rule += ")";
+ }
+
+ return rule;
+}
+
+void CSmartPlaylistRuleCombination::GetVirtualFolders(const std::string& strType, std::vector<std::string> &virtualFolders) const
+{
+ for (CDatabaseQueryRuleCombinations::const_iterator it = m_combinations.begin(); it != m_combinations.end(); ++it)
+ {
+ std::shared_ptr<CSmartPlaylistRuleCombination> combo = std::static_pointer_cast<CSmartPlaylistRuleCombination>(*it);
+ if (combo)
+ combo->GetVirtualFolders(strType, virtualFolders);
+ }
+
+ for (CDatabaseQueryRules::const_iterator it = m_rules.begin(); it != m_rules.end(); ++it)
+ {
+ if (((*it)->m_field != FieldVirtualFolder && (*it)->m_field != FieldPlaylist) || (*it)->m_operator != CDatabaseQueryRule::OPERATOR_EQUALS)
+ continue;
+
+ std::string playlistFile = CSmartPlaylistDirectory::GetPlaylistByName((*it)->m_parameter.at(0), strType);
+ if (playlistFile.empty())
+ continue;
+
+ if ((*it)->m_field == FieldVirtualFolder)
+ virtualFolders.push_back(playlistFile);
+ else
+ {
+ // look for any virtual folders in the expanded playlists
+ CSmartPlaylist playlist;
+ if (!playlist.Load(playlistFile))
+ continue;
+
+ if (CSmartPlaylist::CheckTypeCompatibility(playlist.GetType(), strType))
+ playlist.GetVirtualFolders(virtualFolders);
+ }
+ }
+}
+
+void CSmartPlaylistRuleCombination::AddRule(const CSmartPlaylistRule &rule)
+{
+ std::shared_ptr<CSmartPlaylistRule> ptr(new CSmartPlaylistRule(rule));
+ m_rules.push_back(ptr);
+}
+
+CSmartPlaylist::CSmartPlaylist()
+{
+ Reset();
+}
+
+bool CSmartPlaylist::OpenAndReadName(const CURL &url)
+{
+ if (readNameFromPath(url) == NULL)
+ return false;
+
+ return !m_playlistName.empty();
+}
+
+const TiXmlNode* CSmartPlaylist::readName(const TiXmlNode *root)
+{
+ if (root == NULL)
+ return NULL;
+
+ const TiXmlElement *rootElem = root->ToElement();
+ if (rootElem == NULL)
+ return NULL;
+
+ if (!StringUtils::EqualsNoCase(root->Value(), "smartplaylist"))
+ {
+ CLog::Log(LOGERROR, "Error loading Smart playlist");
+ return NULL;
+ }
+
+ // load the playlist type
+ const char* type = rootElem->Attribute("type");
+ if (type)
+ m_playlistType = type;
+ // backward compatibility:
+ if (m_playlistType == "music")
+ m_playlistType = "songs";
+ if (m_playlistType == "video")
+ m_playlistType = "musicvideos";
+
+ // load the playlist name
+ XMLUtils::GetString(root, "name", m_playlistName);
+
+ return root;
+}
+
+const TiXmlNode* CSmartPlaylist::readNameFromPath(const CURL &url)
+{
+ CFileStream file;
+ if (!file.Open(url))
+ {
+ CLog::Log(LOGERROR, "Error loading Smart playlist {} (failed to read file)", url.GetRedacted());
+ return NULL;
+ }
+
+ m_xmlDoc.Clear();
+ file >> m_xmlDoc;
+
+ const TiXmlNode *root = readName(m_xmlDoc.RootElement());
+ if (m_playlistName.empty())
+ {
+ m_playlistName = CUtil::GetTitleFromPath(url.Get());
+ if (URIUtils::HasExtension(m_playlistName, ".xsp"))
+ URIUtils::RemoveExtension(m_playlistName);
+ }
+
+ return root;
+}
+
+const TiXmlNode* CSmartPlaylist::readNameFromXml(const std::string &xml)
+{
+ if (xml.empty())
+ {
+ CLog::Log(LOGERROR, "Error loading empty Smart playlist");
+ return NULL;
+ }
+
+ m_xmlDoc.Clear();
+ if (!m_xmlDoc.Parse(xml))
+ {
+ CLog::Log(LOGERROR, "Error loading Smart playlist (failed to parse xml: {})",
+ m_xmlDoc.ErrorDesc());
+ return NULL;
+ }
+
+ const TiXmlNode *root = readName(m_xmlDoc.RootElement());
+
+ return root;
+}
+
+bool CSmartPlaylist::load(const TiXmlNode *root)
+{
+ if (root == NULL)
+ return false;
+
+ return LoadFromXML(root);
+}
+
+bool CSmartPlaylist::Load(const CURL &url)
+{
+ return load(readNameFromPath(url));
+}
+
+bool CSmartPlaylist::Load(const std::string &path)
+{
+ const CURL pathToUrl(path);
+ return load(readNameFromPath(pathToUrl));
+}
+
+bool CSmartPlaylist::Load(const CVariant &obj)
+{
+ if (!obj.isObject())
+ return false;
+
+ // load the playlist type
+ if (obj.isMember("type") && obj["type"].isString())
+ m_playlistType = obj["type"].asString();
+
+ // backward compatibility
+ if (m_playlistType == "music")
+ m_playlistType = "songs";
+ if (m_playlistType == "video")
+ m_playlistType = "musicvideos";
+
+ // load the playlist name
+ if (obj.isMember("name") && obj["name"].isString())
+ m_playlistName = obj["name"].asString();
+
+ if (obj.isMember("rules"))
+ m_ruleCombination.Load(obj["rules"], this);
+
+ if (obj.isMember("group") && obj["group"].isMember("type") && obj["group"]["type"].isString())
+ {
+ m_group = obj["group"]["type"].asString();
+ if (obj["group"].isMember("mixed") && obj["group"]["mixed"].isBoolean())
+ m_groupMixed = obj["group"]["mixed"].asBoolean();
+ }
+
+ // now any limits
+ if (obj.isMember("limit") && (obj["limit"].isInteger() || obj["limit"].isUnsignedInteger()) && obj["limit"].asUnsignedInteger() > 0)
+ m_limit = (unsigned int)obj["limit"].asUnsignedInteger();
+
+ // and order
+ if (obj.isMember("order") && obj["order"].isMember("method") && obj["order"]["method"].isString())
+ {
+ const CVariant &order = obj["order"];
+ if (order.isMember("direction") && order["direction"].isString())
+ m_orderDirection = StringUtils::EqualsNoCase(order["direction"].asString(), "ascending") ? SortOrderAscending : SortOrderDescending;
+
+ if (order.isMember("ignorefolders") && obj["ignorefolders"].isBoolean())
+ m_orderAttributes = obj["ignorefolders"].asBoolean() ? SortAttributeIgnoreFolders : SortAttributeNone;
+
+ m_orderField = CSmartPlaylistRule::TranslateOrder(obj["order"]["method"].asString().c_str());
+ }
+
+ return true;
+}
+
+bool CSmartPlaylist::LoadFromXml(const std::string &xml)
+{
+ return load(readNameFromXml(xml));
+}
+
+bool CSmartPlaylist::LoadFromXML(const TiXmlNode *root, const std::string &encoding)
+{
+ if (!root)
+ return false;
+
+ std::string tmp;
+ if (XMLUtils::GetString(root, "match", tmp))
+ m_ruleCombination.SetType(StringUtils::EqualsNoCase(tmp, "all") ? CSmartPlaylistRuleCombination::CombinationAnd : CSmartPlaylistRuleCombination::CombinationOr);
+
+ // now the rules
+ const TiXmlNode *ruleNode = root->FirstChild("rule");
+ while (ruleNode)
+ {
+ CSmartPlaylistRule rule;
+ if (rule.Load(ruleNode, encoding))
+ m_ruleCombination.AddRule(rule);
+
+ ruleNode = ruleNode->NextSibling("rule");
+ }
+
+ const TiXmlElement *groupElement = root->FirstChildElement("group");
+ if (groupElement != NULL && groupElement->FirstChild() != NULL)
+ {
+ m_group = groupElement->FirstChild()->ValueStr();
+ const char* mixed = groupElement->Attribute("mixed");
+ m_groupMixed = mixed != NULL && StringUtils::EqualsNoCase(mixed, "true");
+ }
+
+ // now any limits
+ // format is <limit>25</limit>
+ XMLUtils::GetUInt(root, "limit", m_limit);
+
+ // and order
+ // format is <order direction="ascending">field</order>
+ const TiXmlElement *order = root->FirstChildElement("order");
+ if (order && order->FirstChild())
+ {
+ const char *direction = order->Attribute("direction");
+ if (direction)
+ m_orderDirection = StringUtils::EqualsNoCase(direction, "ascending") ? SortOrderAscending : SortOrderDescending;
+
+ const char *ignorefolders = order->Attribute("ignorefolders");
+ if (ignorefolders != NULL)
+ m_orderAttributes = StringUtils::EqualsNoCase(ignorefolders, "true") ? SortAttributeIgnoreFolders : SortAttributeNone;
+
+ m_orderField = CSmartPlaylistRule::TranslateOrder(order->FirstChild()->Value());
+ }
+ return true;
+}
+
+bool CSmartPlaylist::LoadFromJson(const std::string &json)
+{
+ if (json.empty())
+ return false;
+
+ CVariant obj;
+ if (!CJSONVariantParser::Parse(json, obj))
+ return false;
+
+ return Load(obj);
+}
+
+bool CSmartPlaylist::Save(const std::string &path) const
+{
+ CXBMCTinyXML doc;
+ TiXmlDeclaration decl("1.0", "UTF-8", "yes");
+ doc.InsertEndChild(decl);
+
+ TiXmlElement xmlRootElement("smartplaylist");
+ xmlRootElement.SetAttribute("type",m_playlistType.c_str());
+ TiXmlNode *pRoot = doc.InsertEndChild(xmlRootElement);
+ if (!pRoot)
+ return false;
+
+ // add the <name> tag
+ XMLUtils::SetString(pRoot, "name", m_playlistName);
+
+ // add the <match> tag
+ XMLUtils::SetString(pRoot, "match", m_ruleCombination.GetType() == CSmartPlaylistRuleCombination::CombinationAnd ? "all" : "one");
+
+ // add <rule> tags
+ m_ruleCombination.Save(pRoot);
+
+ // add <group> tag if necessary
+ if (!m_group.empty())
+ {
+ TiXmlElement nodeGroup("group");
+ if (m_groupMixed)
+ nodeGroup.SetAttribute("mixed", "true");
+ TiXmlText group(m_group.c_str());
+ nodeGroup.InsertEndChild(group);
+ pRoot->InsertEndChild(nodeGroup);
+ }
+
+ // add <limit> tag
+ if (m_limit)
+ XMLUtils::SetInt(pRoot, "limit", m_limit);
+
+ // add <order> tag
+ if (m_orderField != SortByNone)
+ {
+ TiXmlText order(CSmartPlaylistRule::TranslateOrder(m_orderField).c_str());
+ TiXmlElement nodeOrder("order");
+ nodeOrder.SetAttribute("direction", m_orderDirection == SortOrderDescending ? "descending" : "ascending");
+ if (m_orderAttributes & SortAttributeIgnoreFolders)
+ nodeOrder.SetAttribute("ignorefolders", "true");
+ nodeOrder.InsertEndChild(order);
+ pRoot->InsertEndChild(nodeOrder);
+ }
+ return doc.SaveFile(path);
+}
+
+bool CSmartPlaylist::Save(CVariant &obj, bool full /* = true */) const
+{
+ if (obj.type() == CVariant::VariantTypeConstNull)
+ return false;
+
+ obj.clear();
+ // add "type"
+ obj["type"] = m_playlistType;
+
+ // add "rules"
+ CVariant rulesObj = CVariant(CVariant::VariantTypeObject);
+ if (m_ruleCombination.Save(rulesObj))
+ obj["rules"] = rulesObj;
+
+ // add "group"
+ if (!m_group.empty())
+ {
+ obj["group"]["type"] = m_group;
+ obj["group"]["mixed"] = m_groupMixed;
+ }
+
+ // add "limit"
+ if (full && m_limit)
+ obj["limit"] = m_limit;
+
+ // add "order"
+ if (full && m_orderField != SortByNone)
+ {
+ obj["order"] = CVariant(CVariant::VariantTypeObject);
+ obj["order"]["method"] = CSmartPlaylistRule::TranslateOrder(m_orderField);
+ obj["order"]["direction"] = m_orderDirection == SortOrderDescending ? "descending" : "ascending";
+ obj["order"]["ignorefolders"] = (m_orderAttributes & SortAttributeIgnoreFolders);
+ }
+
+ return true;
+}
+
+bool CSmartPlaylist::SaveAsJson(std::string &json, bool full /* = true */) const
+{
+ CVariant xsp(CVariant::VariantTypeObject);
+ if (!Save(xsp, full))
+ return false;
+
+ return CJSONVariantWriter::Write(xsp, json, true) && !json.empty();
+}
+
+void CSmartPlaylist::Reset()
+{
+ m_ruleCombination.clear();
+ m_limit = 0;
+ m_orderField = SortByNone;
+ m_orderDirection = SortOrderNone;
+ m_orderAttributes = SortAttributeNone;
+ m_playlistType = "songs"; // sane default
+ m_group.clear();
+ m_groupMixed = false;
+}
+
+void CSmartPlaylist::SetName(const std::string &name)
+{
+ m_playlistName = name;
+}
+
+void CSmartPlaylist::SetType(const std::string &type)
+{
+ m_playlistType = type;
+}
+
+bool CSmartPlaylist::IsVideoType() const
+{
+ return IsVideoType(m_playlistType);
+}
+
+bool CSmartPlaylist::IsMusicType() const
+{
+ return IsMusicType(m_playlistType);
+}
+
+bool CSmartPlaylist::IsVideoType(const std::string &type)
+{
+ return type == "movies" || type == "tvshows" || type == "episodes" ||
+ type == "musicvideos" || type == "mixed";
+}
+
+bool CSmartPlaylist::IsMusicType(const std::string &type)
+{
+ return type == "artists" || type == "albums" ||
+ type == "songs" || type == "mixed";
+}
+
+std::string CSmartPlaylist::GetWhereClause(const CDatabase &db, std::set<std::string> &referencedPlaylists) const
+{
+ return m_ruleCombination.GetWhereClause(db, GetType(), referencedPlaylists);
+}
+
+void CSmartPlaylist::GetVirtualFolders(std::vector<std::string> &virtualFolders) const
+{
+ m_ruleCombination.GetVirtualFolders(GetType(), virtualFolders);
+}
+
+std::string CSmartPlaylist::GetSaveLocation() const
+{
+ if (m_playlistType == "mixed")
+ return "mixed";
+ if (IsMusicType())
+ return "music";
+ // all others are video
+ return "video";
+}
+
+void CSmartPlaylist::GetAvailableFields(const std::string &type, std::vector<std::string> &fieldList)
+{
+ std::vector<Field> typeFields = CSmartPlaylistRule::GetFields(type);
+ for (std::vector<Field>::const_iterator field = typeFields.begin(); field != typeFields.end(); ++field)
+ {
+ for (const translateField& i : fields)
+ {
+ if (*field == i.field)
+ fieldList.emplace_back(i.string);
+ }
+ }
+}
+
+bool CSmartPlaylist::IsEmpty(bool ignoreSortAndLimit /* = true */) const
+{
+ bool empty = m_ruleCombination.empty();
+ if (empty && !ignoreSortAndLimit)
+ empty = m_limit <= 0 && m_orderField == SortByNone && m_orderDirection == SortOrderNone;
+
+ return empty;
+}
+
+bool CSmartPlaylist::CheckTypeCompatibility(const std::string &typeLeft, const std::string &typeRight)
+{
+ if (typeLeft == typeRight)
+ return true;
+
+ if (typeLeft == "mixed" &&
+ (typeRight == "songs" || typeRight == "musicvideos"))
+ return true;
+
+ if (typeRight == "mixed" &&
+ (typeLeft == "songs" || typeLeft == "musicvideos"))
+ return true;
+
+ return false;
+}
+
+CDatabaseQueryRule *CSmartPlaylist::CreateRule() const
+{
+ return new CSmartPlaylistRule();
+}
+CDatabaseQueryRuleCombination *CSmartPlaylist::CreateCombination() const
+{
+ return new CSmartPlaylistRuleCombination();
+}
diff --git a/xbmc/playlists/SmartPlayList.h b/xbmc/playlists/SmartPlayList.h
new file mode 100644
index 0000000..2a153c3
--- /dev/null
+++ b/xbmc/playlists/SmartPlayList.h
@@ -0,0 +1,186 @@
+/*
+ * 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 "dbwrappers/DatabaseQuery.h"
+#include "utils/SortUtils.h"
+#include "utils/XBMCTinyXML.h"
+
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+class CURL;
+class CVariant;
+
+class CSmartPlaylistRule : public CDatabaseQueryRule
+{
+public:
+ CSmartPlaylistRule();
+ ~CSmartPlaylistRule() override = default;
+
+ std::string GetLocalizedRule() const;
+
+ static SortBy TranslateOrder(const char *order);
+ static std::string TranslateOrder(SortBy order);
+ static Field TranslateGroup(const char *group);
+ static std::string TranslateGroup(Field group);
+
+ static std::string GetLocalizedField(int field);
+ static std::string GetLocalizedGroup(Field group);
+ static bool CanGroupMix(Field group);
+
+ static std::vector<Field> GetFields(const std::string &type);
+ static std::vector<SortBy> GetOrders(const std::string &type);
+ static std::vector<Field> GetGroups(const std::string &type);
+ FIELD_TYPE GetFieldType(int field) const override;
+ static bool IsFieldBrowseable(int field);
+
+ static bool Validate(const std::string &input, void *data);
+ static bool ValidateRating(const std::string &input, void *data);
+ static bool ValidateMyRating(const std::string &input, void *data);
+
+protected:
+ std::string GetField(int field, const std::string& type) const override;
+ int TranslateField(const char *field) const override;
+ std::string TranslateField(int field) const override;
+ std::string FormatParameter(const std::string &negate,
+ const std::string &oper,
+ const CDatabase &db,
+ const std::string &type) const override;
+ std::string FormatWhereClause(const std::string &negate,
+ const std::string& oper,
+ const std::string &param,
+ const CDatabase &db,
+ const std::string &type) const override;
+ SEARCH_OPERATOR GetOperator(const std::string &type) const override;
+ std::string GetBooleanQuery(const std::string &negate,
+ const std::string &strType) const override;
+
+private:
+ std::string GetVideoResolutionQuery(const std::string &parameter) const;
+ static std::string FormatLinkQuery(const char *field, const char *table, const MediaType& mediaType, const std::string& mediaField, const std::string& parameter);
+ std::string FormatYearQuery(const std::string& field,
+ const std::string& param,
+ const std::string& parameter) const;
+};
+
+class CSmartPlaylistRuleCombination : public CDatabaseQueryRuleCombination
+{
+public:
+ CSmartPlaylistRuleCombination() = default;
+ ~CSmartPlaylistRuleCombination() override = default;
+
+ std::string GetWhereClause(const CDatabase &db,
+ const std::string& strType,
+ std::set<std::string> &referencedPlaylists) const;
+ void GetVirtualFolders(const std::string& strType,
+ std::vector<std::string> &virtualFolders) const;
+
+ void AddRule(const CSmartPlaylistRule &rule);
+};
+
+class CSmartPlaylist : public IDatabaseQueryRuleFactory
+{
+public:
+ CSmartPlaylist();
+ ~CSmartPlaylist() override = default;
+
+ bool Load(const CURL& url);
+ bool Load(const std::string &path);
+ bool Load(const CVariant &obj);
+ bool LoadFromXml(const std::string &xml);
+ bool LoadFromJson(const std::string &json);
+ bool Save(const std::string &path) const;
+ bool Save(CVariant &obj, bool full = true) const;
+ bool SaveAsJson(std::string &json, bool full = true) const;
+
+ bool OpenAndReadName(const CURL &url);
+ bool LoadFromXML(const TiXmlNode *root, const std::string &encoding = "UTF-8");
+
+ void Reset();
+
+ void SetName(const std::string &name);
+ void SetType(const std::string &type); // music, video, mixed
+ const std::string& GetName() const { return m_playlistName; }
+ const std::string& GetType() const { return m_playlistType; }
+ bool IsVideoType() const;
+ bool IsMusicType() const;
+
+ void SetMatchAllRules(bool matchAll) { m_ruleCombination.SetType(matchAll ? CSmartPlaylistRuleCombination::CombinationAnd : CSmartPlaylistRuleCombination::CombinationOr); }
+ bool GetMatchAllRules() const { return m_ruleCombination.GetType() == CSmartPlaylistRuleCombination::CombinationAnd; }
+
+ void SetLimit(unsigned int limit) { m_limit = limit; }
+ unsigned int GetLimit() const { return m_limit; }
+
+ void SetOrder(SortBy order) { m_orderField = order; }
+ SortBy GetOrder() const { return m_orderField; }
+ void SetOrderAscending(bool orderAscending)
+ {
+ m_orderDirection = orderAscending ? SortOrderAscending : SortOrderDescending;
+ }
+ bool GetOrderAscending() const { return m_orderDirection != SortOrderDescending; }
+ SortOrder GetOrderDirection() const { return m_orderDirection; }
+ void SetOrderAttributes(SortAttribute attributes) { m_orderAttributes = attributes; }
+ SortAttribute GetOrderAttributes() const { return m_orderAttributes; }
+
+ void SetGroup(const std::string &group) { m_group = group; }
+ const std::string& GetGroup() const { return m_group; }
+ void SetGroupMixed(bool mixed) { m_groupMixed = mixed; }
+ bool IsGroupMixed() const { return m_groupMixed; }
+
+ /*! \brief get the where clause for a playlist
+ We handle playlists inside playlists separately in order to ensure we don't introduce infinite loops
+ by playlist A including playlist B which also (perhaps via other playlists) then includes playlistA.
+
+ \param db the database to use to format up results
+ \param referencedPlaylists a set of playlists to know when we reach a cycle
+ \param needWhere whether we need to prepend the where clause with "WHERE "
+ */
+ std::string GetWhereClause(const CDatabase &db, std::set<std::string> &referencedPlaylists) const;
+ void GetVirtualFolders(std::vector<std::string> &virtualFolders) const;
+
+ std::string GetSaveLocation() const;
+
+ static void GetAvailableFields(const std::string &type, std::vector<std::string> &fieldList);
+
+ static bool IsVideoType(const std::string &type);
+ static bool IsMusicType(const std::string &type);
+ static bool CheckTypeCompatibility(const std::string &typeLeft, const std::string &typeRight);
+
+ bool IsEmpty(bool ignoreSortAndLimit = true) const;
+
+ // rule creation
+ CDatabaseQueryRule *CreateRule() const override;
+ CDatabaseQueryRuleCombination *CreateCombination() const override;
+private:
+ friend class CGUIDialogSmartPlaylistEditor;
+ friend class CGUIDialogMediaFilter;
+
+ const TiXmlNode* readName(const TiXmlNode *root);
+ const TiXmlNode* readNameFromPath(const CURL &url);
+ const TiXmlNode* readNameFromXml(const std::string &xml);
+ bool load(const TiXmlNode *root);
+
+ CSmartPlaylistRuleCombination m_ruleCombination;
+ std::string m_playlistName;
+ std::string m_playlistType;
+
+ // order information
+ unsigned int m_limit;
+ SortBy m_orderField;
+ SortOrder m_orderDirection;
+ SortAttribute m_orderAttributes;
+ std::string m_group;
+ bool m_groupMixed;
+
+ CXBMCTinyXML m_xmlDoc;
+};
+
diff --git a/xbmc/playlists/SmartPlaylistFileItemListModifier.cpp b/xbmc/playlists/SmartPlaylistFileItemListModifier.cpp
new file mode 100644
index 0000000..7249bb5
--- /dev/null
+++ b/xbmc/playlists/SmartPlaylistFileItemListModifier.cpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013-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 "SmartPlaylistFileItemListModifier.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "playlists/SmartPlayList.h"
+#include "utils/StringUtils.h"
+
+#include <string>
+
+#define URL_OPTION_XSP "xsp"
+#define PROPERTY_SORT_ORDER "sort.order"
+#define PROPERTY_SORT_ASCENDING "sort.ascending"
+
+bool CSmartPlaylistFileItemListModifier::CanModify(const CFileItemList &items) const
+{
+ return !GetUrlOption(items.GetPath(), URL_OPTION_XSP).empty();
+}
+
+bool CSmartPlaylistFileItemListModifier::Modify(CFileItemList &items) const
+{
+ if (items.HasProperty(PROPERTY_SORT_ORDER))
+ return false;
+
+ std::string xspOption = GetUrlOption(items.GetPath(), URL_OPTION_XSP);
+ if (xspOption.empty())
+ return false;
+
+ // check for smartplaylist-specific sorting information
+ CSmartPlaylist xsp;
+ if (!xsp.LoadFromJson(xspOption))
+ return false;
+
+ items.SetProperty(PROPERTY_SORT_ORDER, (int)xsp.GetOrder());
+ items.SetProperty(PROPERTY_SORT_ASCENDING, xsp.GetOrderDirection() == SortOrderAscending);
+
+ return true;
+}
+
+std::string CSmartPlaylistFileItemListModifier::GetUrlOption(const std::string &path, const std::string &option)
+{
+ if (path.empty() || option.empty())
+ return StringUtils::Empty;
+
+ CURL url(path);
+ return url.GetOption(option);
+}
diff --git a/xbmc/playlists/SmartPlaylistFileItemListModifier.h b/xbmc/playlists/SmartPlaylistFileItemListModifier.h
new file mode 100644
index 0000000..5d64084
--- /dev/null
+++ b/xbmc/playlists/SmartPlaylistFileItemListModifier.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2013-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 "IFileItemListModifier.h"
+
+#include <string>
+
+class CSmartPlaylistFileItemListModifier : public IFileItemListModifier
+{
+public:
+ CSmartPlaylistFileItemListModifier() = default;
+ ~CSmartPlaylistFileItemListModifier() override = default;
+
+ bool CanModify(const CFileItemList &items) const override;
+ bool Modify(CFileItemList &items) const override;
+
+private:
+ static std::string GetUrlOption(const std::string &path, const std::string &option);
+};
diff --git a/xbmc/playlists/test/CMakeLists.txt b/xbmc/playlists/test/CMakeLists.txt
new file mode 100644
index 0000000..f421fac
--- /dev/null
+++ b/xbmc/playlists/test/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES TestPlayListFactory.cpp
+ TestPlayListXSPF.cpp)
+
+core_add_test_library(playlists_test)
diff --git a/xbmc/playlists/test/TestPlayListFactory.cpp b/xbmc/playlists/test/TestPlayListFactory.cpp
new file mode 100644
index 0000000..08b2b4b
--- /dev/null
+++ b/xbmc/playlists/test/TestPlayListFactory.cpp
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 Tyler Szabo
+ * Copyright (C) 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 "URL.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+#include "playlists/PlayListXSPF.h"
+#include "test/TestUtils.h"
+
+#include <gtest/gtest.h>
+
+using namespace PLAYLIST;
+
+
+TEST(TestPlayListFactory, XSPF)
+{
+ std::string filename = XBMC_REF_FILE_PATH("/xbmc/playlists/test/newfile.xspf");
+ CURL url("http://example.com/playlists/playlist.xspf");
+ CPlayList* playlist = nullptr;
+
+ EXPECT_TRUE(CPlayListFactory::IsPlaylist(url));
+ EXPECT_TRUE(CPlayListFactory::IsPlaylist(filename));
+
+ playlist = CPlayListFactory::Create(filename);
+ EXPECT_NE(playlist, nullptr);
+
+ if (playlist)
+ {
+ EXPECT_NE(dynamic_cast<CPlayListXSPF*>(playlist), nullptr);
+ delete playlist;
+ }
+}
diff --git a/xbmc/playlists/test/TestPlayListXSPF.cpp b/xbmc/playlists/test/TestPlayListXSPF.cpp
new file mode 100644
index 0000000..574d05d
--- /dev/null
+++ b/xbmc/playlists/test/TestPlayListXSPF.cpp
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 Tyler Szabo
+ * Copyright (C) 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 "FileItem.h"
+#include "URL.h"
+#include "playlists/PlayListXSPF.h"
+#include "test/TestUtils.h"
+#include "utils/URIUtils.h"
+
+#include <gtest/gtest.h>
+
+using namespace PLAYLIST;
+
+
+TEST(TestPlayListXSPF, Load)
+{
+ std::string filename = XBMC_REF_FILE_PATH("/xbmc/playlists/test/test.xspf");
+ CPlayListXSPF playlist;
+ std::vector<std::string> pathparts;
+ std::vector<std::string>::reverse_iterator it;
+
+ EXPECT_TRUE(playlist.Load(filename));
+
+ EXPECT_EQ(playlist.size(), 5);
+ EXPECT_STREQ(playlist.GetName().c_str(), "Various Music");
+
+
+ ASSERT_GT(playlist.size(), 0);
+ EXPECT_STREQ(playlist[0]->GetLabel().c_str(), "");
+ EXPECT_STREQ(playlist[0]->GetURL().Get().c_str(), "http://example.com/song_1.mp3");
+
+
+ ASSERT_GT(playlist.size(), 1);
+ EXPECT_STREQ(playlist[1]->GetLabel().c_str(), "Relative local file");
+ pathparts = URIUtils::SplitPath(playlist[1]->GetPath());
+ it = pathparts.rbegin();
+ EXPECT_STREQ((*it++).c_str(), "song_2.mp3");
+ EXPECT_STREQ((*it++).c_str(), "path_to");
+ EXPECT_STREQ((*it++).c_str(), "relative");
+ EXPECT_STREQ((*it++).c_str(), "test");
+ EXPECT_STREQ((*it++).c_str(), "playlists");
+ EXPECT_STREQ((*it++).c_str(), "xbmc");
+
+
+ ASSERT_GT(playlist.size(), 2);
+ EXPECT_STREQ(playlist[2]->GetLabel().c_str(), "Don\xC2\x92t Worry, We\xC2\x92ll Be Watching You");
+ pathparts = URIUtils::SplitPath(playlist[2]->GetPath());
+ it = pathparts.rbegin();
+ EXPECT_STREQ((*it++).c_str(), "09 - Don't Worry, We'll Be Watching You.mp3");
+ EXPECT_STREQ((*it++).c_str(), "Making Mirrors");
+ EXPECT_STREQ((*it++).c_str(), "Gotye");
+ EXPECT_STREQ((*it++).c_str(), "Music");
+ EXPECT_STREQ((*it++).c_str(), "jane");
+ EXPECT_STREQ((*it++).c_str(), "Users");
+ EXPECT_STREQ((*it++).c_str(), "C:");
+
+
+ ASSERT_GT(playlist.size(), 3);
+ EXPECT_STREQ(playlist[3]->GetLabel().c_str(), "Rollin' & Scratchin'");
+ pathparts = URIUtils::SplitPath(playlist[3]->GetPath());
+ it = pathparts.rbegin();
+ EXPECT_STREQ((*it++).c_str(), "08 - Rollin' & Scratchin'.mp3");
+ EXPECT_STREQ((*it++).c_str(), "Homework");
+ EXPECT_STREQ((*it++).c_str(), "Daft Punk");
+ EXPECT_STREQ((*it++).c_str(), "Music");
+ EXPECT_STREQ((*it++).c_str(), "jane");
+ EXPECT_STREQ((*it++).c_str(), "home");
+
+
+ ASSERT_GT(playlist.size(), 4);
+ EXPECT_STREQ(playlist[4]->GetLabel().c_str(), "");
+ EXPECT_STREQ(playlist[4]->GetURL().Get().c_str(), "http://example.com/song_2.mp3");
+}
diff --git a/xbmc/playlists/test/test.xspf b/xbmc/playlists/test/test.xspf
new file mode 100644
index 0000000..cf98d64
--- /dev/null
+++ b/xbmc/playlists/test/test.xspf
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<playlist version="1" xmlns="http://xspf.org/ns/0/">
+
+ <!-- title of the playlist -->
+ <title>Various Music</title>
+
+ <!-- name of the author -->
+ <creator>Jane Doe</creator>
+
+ <!-- homepage of the author -->
+ <info>http://example.com/~jane</info>
+
+ <trackList>
+ <track>
+ <location>http://example.com/song_1.mp3</location>
+ </track>
+
+ <track>
+ <title>Relative local file</title>
+ <location>relative/path_to/song_2.mp3</location>
+ </track>
+
+ <track>
+ <!--Absolute Windows path -->
+ <location>file:///C:/Users/jane/Music/Gotye/Making%20Mirrors/09%20-%20Don%27t%20Worry%2C%20We%27ll%20Be%20Watching%20You.mp3</location>
+ <title>Don&#146;t Worry, We&#146;ll Be Watching You</title>
+ <creator>Gotye</creator>
+ <album>Making Mirrors</album>
+ <trackNum>9</trackNum>
+ </track>
+
+ <track>
+ <!--Absolute Linux path -->
+ <location>file:///home/jane/Music/Daft%20Punk/Homework/08%20-%20Rollin%27%20%26%20Scratchin%27.mp3</location>
+ <title>Rollin&#39; &amp; Scratchin&#39;</title>
+ <creator>Daft Punk</creator>
+ <album>Homework</album>
+ <trackNum>8</trackNum>
+ </track>
+
+ <track>
+ <title></title>
+ <location></location>
+ </track>
+
+ <track>
+ <title>Nothing to add for this entry</title>
+ </track>
+
+ <track>
+ <location>http://example.com/song_2.mp3</location>
+ </track>
+ </trackList>
+</playlist>
diff --git a/xbmc/powermanagement/CMakeLists.txt b/xbmc/powermanagement/CMakeLists.txt
new file mode 100644
index 0000000..a1953d4
--- /dev/null
+++ b/xbmc/powermanagement/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES DPMSSupport.cpp
+ IPowerSyscall.cpp
+ PowerManager.cpp)
+
+set(HEADERS DPMSSupport.h
+ IPowerSyscall.h
+ PowerManager.h
+ PowerTypes.h)
+
+if(CORE_SYSTEM_NAME MATCHES windows)
+ list(APPEND HEADERS WinIdleTimer.h)
+endif()
+
+core_add_library(powermanagement)
diff --git a/xbmc/powermanagement/DPMSSupport.cpp b/xbmc/powermanagement/DPMSSupport.cpp
new file mode 100644
index 0000000..077117d
--- /dev/null
+++ b/xbmc/powermanagement/DPMSSupport.cpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2009-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 "DPMSSupport.h"
+
+#include "ServiceBroker.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/log.h"
+
+#include <array>
+#include <string>
+
+CDPMSSupport::CDPMSSupport()
+{
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (settingsComponent)
+ {
+ auto settings = settingsComponent->GetSettings();
+ if (settings)
+ {
+ auto setting = settings->GetSetting(CSettings::SETTING_POWERMANAGEMENT_DISPLAYSOFF);
+ if (setting)
+ setting->SetRequirementsMet(true);
+ }
+ }
+}
+
+bool CDPMSSupport::IsModeSupported(PowerSavingMode mode) const
+{
+ for (const auto& supportedModes : m_supportedModes)
+ {
+ if (supportedModes == mode)
+ return true;
+ }
+
+ return false;
+}
diff --git a/xbmc/powermanagement/DPMSSupport.h b/xbmc/powermanagement/DPMSSupport.h
new file mode 100644
index 0000000..cf10b4c
--- /dev/null
+++ b/xbmc/powermanagement/DPMSSupport.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009-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 <vector>
+
+// This class encapsulates support for monitor power-saving features (DPMS).
+// An instance is connected to a Surface, provides information on which
+// power-saving features are available for that screen, and it is able to
+// turn power-saving on an off.
+// Note that SDL turns off DPMS timeouts at the beginning of the application.
+class CDPMSSupport
+{
+public:
+ // All known DPMS power-saving modes, on any platform.
+ enum PowerSavingMode
+ {
+ STANDBY,
+ SUSPEND,
+ OFF,
+ NUM_MODES,
+ };
+
+ CDPMSSupport();
+ virtual ~CDPMSSupport() = default;
+
+ // Whether power-saving is supported on this screen.
+ bool IsSupported() const { return !m_supportedModes.empty(); }
+
+ // Which power-saving modes are supported, in the order of preference (i.e.
+ // the first mode should be the best choice).
+ const std::vector<PowerSavingMode>& GetSupportedModes() const
+ {
+ return m_supportedModes;
+ }
+
+ // Whether a given mode is supported.
+ bool IsModeSupported(PowerSavingMode mode) const;
+
+ // Turns on the specified power-saving mode, which must be valid
+ // and supported. Returns false if this failed.
+ virtual bool EnablePowerSaving(PowerSavingMode mode) = 0;
+
+ // Turns off power-saving mode. You should only call this if the display
+ // is currently in a power-saving mode, to avoid visual artifacts.
+ virtual bool DisablePowerSaving() = 0;
+
+protected:
+ std::vector<PowerSavingMode> m_supportedModes;
+};
diff --git a/xbmc/powermanagement/IPowerSyscall.cpp b/xbmc/powermanagement/IPowerSyscall.cpp
new file mode 100644
index 0000000..623337f
--- /dev/null
+++ b/xbmc/powermanagement/IPowerSyscall.cpp
@@ -0,0 +1,24 @@
+/*
+ * 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 "IPowerSyscall.h"
+
+CreatePowerSyscallFunc IPowerSyscall::m_createFunc = nullptr;
+
+IPowerSyscall* IPowerSyscall::CreateInstance()
+{
+ if (m_createFunc)
+ return m_createFunc();
+
+ return nullptr;
+}
+
+void IPowerSyscall::RegisterPowerSyscall(CreatePowerSyscallFunc createFunc)
+{
+ m_createFunc = createFunc;
+} \ No newline at end of file
diff --git a/xbmc/powermanagement/IPowerSyscall.h b/xbmc/powermanagement/IPowerSyscall.h
new file mode 100644
index 0000000..f54186e
--- /dev/null
+++ b/xbmc/powermanagement/IPowerSyscall.h
@@ -0,0 +1,111 @@
+/*
+ * 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
+
+class IPowerEventsCallback
+{
+public:
+ virtual ~IPowerEventsCallback() = default;
+
+ virtual void OnSleep() = 0;
+ virtual void OnWake() = 0;
+
+ virtual void OnLowBattery() = 0;
+};
+
+class IPowerSyscall;
+using CreatePowerSyscallFunc = IPowerSyscall* (*)();
+
+class IPowerSyscall
+{
+public:
+ /**\brief Called by power manager to create platform power system adapter
+ *
+ * This method used to create platform specified power system adapter
+ */
+ static IPowerSyscall* CreateInstance();
+ static void RegisterPowerSyscall(CreatePowerSyscallFunc createFunc);
+
+ virtual ~IPowerSyscall() = default;
+ virtual bool Powerdown() = 0;
+ virtual bool Suspend() = 0;
+ virtual bool Hibernate() = 0;
+ virtual bool Reboot() = 0;
+
+// Might need to be membervariables instead for speed
+ virtual bool CanPowerdown() = 0;
+ virtual bool CanSuspend() = 0;
+ virtual bool CanHibernate() = 0;
+ virtual bool CanReboot() = 0;
+
+ virtual int CountPowerFeatures() = 0;
+
+// Battery related functions
+ virtual int BatteryLevel() = 0;
+
+ /*!
+ \brief Pump power related events back to xbmc.
+
+ PumpPowerEvents is called from Application Thread and the platform implementation may signal
+ power related events back to xbmc through the callback.
+
+ return true if an event occurred and false if not.
+
+ \param callback the callback to signal to
+ */
+ virtual bool PumpPowerEvents(IPowerEventsCallback *callback) = 0;
+
+ static const int MAX_COUNT_POWER_FEATURES = 4;
+
+private:
+ static CreatePowerSyscallFunc m_createFunc;
+};
+
+class CAbstractPowerSyscall : public IPowerSyscall
+{
+public:
+ int CountPowerFeatures() override
+ {
+ return (CanPowerdown() ? 1 : 0)
+ + (CanSuspend() ? 1 : 0)
+ + (CanHibernate() ? 1 : 0)
+ + (CanReboot() ? 1 : 0);
+ }
+};
+
+class CPowerSyscallWithoutEvents : public CAbstractPowerSyscall
+{
+public:
+ CPowerSyscallWithoutEvents() { m_OnResume = false; m_OnSuspend = false; }
+
+ bool Suspend() override { m_OnSuspend = true; return false; }
+ bool Hibernate() override { m_OnSuspend = true; return false; }
+
+ bool PumpPowerEvents(IPowerEventsCallback *callback) override
+ {
+ if (m_OnSuspend)
+ {
+ callback->OnSleep();
+ m_OnSuspend = false;
+ m_OnResume = true;
+ return true;
+ }
+ else if (m_OnResume)
+ {
+ callback->OnWake();
+ m_OnResume = false;
+ return true;
+ }
+ else
+ return false;
+ }
+private:
+ bool m_OnResume;
+ bool m_OnSuspend;
+};
diff --git a/xbmc/powermanagement/PowerManager.cpp b/xbmc/powermanagement/PowerManager.cpp
new file mode 100644
index 0000000..3c30817
--- /dev/null
+++ b/xbmc/powermanagement/PowerManager.cpp
@@ -0,0 +1,315 @@
+/*
+ * 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 "PowerManager.h"
+
+#include "FileItem.h"
+#include "PowerTypes.h"
+#include "ServiceBroker.h"
+#include "application/AppParams.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationPowerHandling.h"
+#include "application/ApplicationStackHelper.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "dialogs/GUIDialogBusyNoCancel.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/AnnouncementManager.h"
+#include "network/Network.h"
+#include "pvr/PVRManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/log.h"
+#include "weather/WeatherManager.h"
+
+#include <list>
+#include <memory>
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+extern HWND g_hWnd;
+#endif
+
+CPowerManager::CPowerManager() : m_settings(CServiceBroker::GetSettingsComponent()->GetSettings())
+{
+ m_settings->GetSettingsManager()->RegisterSettingOptionsFiller("shutdownstates", SettingOptionsShutdownStatesFiller);
+}
+
+CPowerManager::~CPowerManager() = default;
+
+void CPowerManager::Initialize()
+{
+ m_instance.reset(IPowerSyscall::CreateInstance());
+}
+
+void CPowerManager::SetDefaults()
+{
+ auto setting = m_settings->GetSetting(CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNSTATE);
+ if (!setting)
+ {
+ CLog::Log(LOGERROR, "Failed to load setting for: {}",
+ CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNSTATE);
+ return;
+ }
+
+ int defaultShutdown = m_settings->GetInt(CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNSTATE);
+
+ switch (defaultShutdown)
+ {
+ case POWERSTATE_QUIT:
+ case POWERSTATE_MINIMIZE:
+ // assume we can shutdown if --standalone is passed
+ if (CServiceBroker::GetAppParams()->IsStandAlone())
+ defaultShutdown = POWERSTATE_SHUTDOWN;
+ break;
+ case POWERSTATE_HIBERNATE:
+ if (!CServiceBroker::GetPowerManager().CanHibernate())
+ {
+ if (CServiceBroker::GetPowerManager().CanSuspend())
+ defaultShutdown = POWERSTATE_SUSPEND;
+ else
+ defaultShutdown = CServiceBroker::GetPowerManager().CanPowerdown() ? POWERSTATE_SHUTDOWN : POWERSTATE_QUIT;
+ }
+ break;
+ case POWERSTATE_SUSPEND:
+ if (!CServiceBroker::GetPowerManager().CanSuspend())
+ {
+ if (CServiceBroker::GetPowerManager().CanHibernate())
+ defaultShutdown = POWERSTATE_HIBERNATE;
+ else
+ defaultShutdown = CServiceBroker::GetPowerManager().CanPowerdown() ? POWERSTATE_SHUTDOWN : POWERSTATE_QUIT;
+ }
+ break;
+ case POWERSTATE_SHUTDOWN:
+ if (!CServiceBroker::GetPowerManager().CanPowerdown())
+ {
+ if (CServiceBroker::GetPowerManager().CanSuspend())
+ defaultShutdown = POWERSTATE_SUSPEND;
+ else
+ defaultShutdown = CServiceBroker::GetPowerManager().CanHibernate() ? POWERSTATE_HIBERNATE : POWERSTATE_QUIT;
+ }
+ break;
+ }
+
+ std::static_pointer_cast<CSettingInt>(setting)->SetDefault(defaultShutdown);
+}
+
+bool CPowerManager::Powerdown()
+{
+ if (CanPowerdown() && m_instance->Powerdown())
+ {
+ CGUIDialogBusyNoCancel* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogBusyNoCancel>(
+ WINDOW_DIALOG_BUSY_NOCANCEL);
+ if (dialog)
+ dialog->Open();
+
+ return true;
+ }
+
+ return false;
+}
+
+bool CPowerManager::Suspend()
+{
+ return (CanSuspend() && m_instance->Suspend());
+}
+
+bool CPowerManager::Hibernate()
+{
+ return (CanHibernate() && m_instance->Hibernate());
+}
+
+bool CPowerManager::Reboot()
+{
+ bool success = CanReboot() ? m_instance->Reboot() : false;
+
+ if (success)
+ {
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::System, "OnRestart");
+
+ CGUIDialogBusyNoCancel* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogBusyNoCancel>(
+ WINDOW_DIALOG_BUSY_NOCANCEL);
+ if (dialog)
+ dialog->Open();
+ }
+
+ return success;
+}
+
+bool CPowerManager::CanPowerdown()
+{
+ return m_instance ? m_instance->CanPowerdown() : false;
+}
+bool CPowerManager::CanSuspend()
+{
+ return m_instance ? m_instance->CanSuspend() : false;
+}
+bool CPowerManager::CanHibernate()
+{
+ return m_instance ? m_instance->CanHibernate() : false;
+}
+bool CPowerManager::CanReboot()
+{
+ return m_instance ? m_instance->CanReboot() : false;
+}
+int CPowerManager::BatteryLevel()
+{
+ return m_instance ? m_instance->BatteryLevel() : 0;
+}
+void CPowerManager::ProcessEvents()
+{
+ if (!m_instance)
+ return;
+
+ static int nesting = 0;
+
+ if (++nesting == 1)
+ m_instance->PumpPowerEvents(this);
+
+ nesting--;
+}
+
+void CPowerManager::OnSleep()
+{
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::System, "OnSleep");
+
+ CGUIDialogBusyNoCancel* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogBusyNoCancel>(
+ WINDOW_DIALOG_BUSY_NOCANCEL);
+ if (dialog)
+ dialog->Open();
+
+ CLog::Log(LOGINFO, "{}: Running sleep jobs", __FUNCTION__);
+
+ StorePlayerState();
+
+ g_application.StopPlaying();
+ CServiceBroker::GetPVRManager().OnSleep();
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->StopShutdownTimer();
+ appPower->StopScreenSaverTimer();
+ g_application.CloseNetworkShares();
+ CServiceBroker::GetActiveAE()->Suspend();
+}
+
+void CPowerManager::OnWake()
+{
+ CLog::Log(LOGINFO, "{}: Running resume jobs", __FUNCTION__);
+
+ CServiceBroker::GetNetwork().WaitForNet();
+
+ // reset out timers
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetShutdownTimers();
+
+ CGUIDialogBusyNoCancel* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogBusyNoCancel>(
+ WINDOW_DIALOG_BUSY_NOCANCEL);
+ if (dialog)
+ dialog->Close(true); // force close. no closing animation, sound etc at this early stage
+
+#if defined(TARGET_DARWIN_OSX) || defined(TARGET_WINDOWS)
+ if (CServiceBroker::GetWinSystem()->IsFullScreen())
+ {
+#if defined(TARGET_WINDOWS_DESKTOP)
+ ShowWindow(g_hWnd, SW_RESTORE);
+ SetForegroundWindow(g_hWnd);
+#endif
+ }
+ appPower->ResetScreenSaver();
+#endif
+
+ CServiceBroker::GetActiveAE()->Resume();
+ g_application.UpdateLibraries();
+ CServiceBroker::GetWeatherManager().Refresh();
+ CServiceBroker::GetPVRManager().OnWake();
+ RestorePlayerState();
+
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::System, "OnWake");
+}
+
+void CPowerManager::OnLowBattery()
+{
+ CLog::Log(LOGINFO, "{}: Running low battery jobs", __FUNCTION__);
+
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, g_localizeStrings.Get(13050), "");
+
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::System, "OnLowBattery");
+}
+
+void CPowerManager::StorePlayerState()
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ {
+ m_lastUsedPlayer = appPlayer->GetCurrentPlayer();
+ m_lastPlayedFileItem.reset(new CFileItem(g_application.CurrentFileItem()));
+ // set the actual offset instead of store and load it from database
+ m_lastPlayedFileItem->SetStartOffset(appPlayer->GetTime());
+ // in case of regular stack, correct the start offset by adding current part start time
+ const auto stackHelper = components.GetComponent<CApplicationStackHelper>();
+ if (stackHelper->IsPlayingRegularStack())
+ m_lastPlayedFileItem->SetStartOffset(m_lastPlayedFileItem->GetStartOffset() +
+ stackHelper->GetCurrentStackPartStartTimeMs());
+ // in case of iso stack, keep track of part number
+ m_lastPlayedFileItem->m_lStartPartNumber =
+ stackHelper->IsPlayingISOStack() ? stackHelper->GetCurrentPartNumber() + 1 : 1;
+ // for iso and iso stacks, keep track of playerstate
+ m_lastPlayedFileItem->SetProperty("savedplayerstate", appPlayer->GetPlayerState());
+ CLog::Log(LOGDEBUG,
+ "CPowerManager::StorePlayerState - store last played item (startOffset: {} ms)",
+ m_lastPlayedFileItem->GetStartOffset());
+ }
+ else
+ {
+ m_lastUsedPlayer.clear();
+ m_lastPlayedFileItem.reset();
+ }
+}
+
+void CPowerManager::RestorePlayerState()
+{
+ if (!m_lastPlayedFileItem)
+ return;
+
+ CLog::Log(LOGDEBUG,
+ "CPowerManager::RestorePlayerState - resume last played item (startOffset: {} ms)",
+ m_lastPlayedFileItem->GetStartOffset());
+ g_application.PlayFile(*m_lastPlayedFileItem, m_lastUsedPlayer);
+}
+
+void CPowerManager::SettingOptionsShutdownStatesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ if (CServiceBroker::GetPowerManager().CanPowerdown())
+ list.emplace_back(g_localizeStrings.Get(13005), POWERSTATE_SHUTDOWN);
+ if (CServiceBroker::GetPowerManager().CanHibernate())
+ list.emplace_back(g_localizeStrings.Get(13010), POWERSTATE_HIBERNATE);
+ if (CServiceBroker::GetPowerManager().CanSuspend())
+ list.emplace_back(g_localizeStrings.Get(13011), POWERSTATE_SUSPEND);
+ if (!CServiceBroker::GetAppParams()->IsStandAlone())
+ {
+ list.emplace_back(g_localizeStrings.Get(13009), POWERSTATE_QUIT);
+#if !defined(TARGET_DARWIN_EMBEDDED)
+ list.emplace_back(g_localizeStrings.Get(13014), POWERSTATE_MINIMIZE);
+#endif
+ }
+}
diff --git a/xbmc/powermanagement/PowerManager.h b/xbmc/powermanagement/PowerManager.h
new file mode 100644
index 0000000..319be9d
--- /dev/null
+++ b/xbmc/powermanagement/PowerManager.h
@@ -0,0 +1,69 @@
+/*
+ * 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 "IPowerSyscall.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CFileItem;
+class CSetting;
+class CSettings;
+
+struct IntegerSettingOption;
+
+// This class will wrap and handle PowerSyscalls.
+// It will handle and decide if syscalls are needed.
+class CPowerManager : public IPowerEventsCallback
+{
+public:
+ CPowerManager();
+ ~CPowerManager() override;
+
+ void Initialize();
+ void SetDefaults();
+
+ bool Powerdown();
+ bool Suspend();
+ bool Hibernate();
+ bool Reboot();
+
+ bool CanPowerdown();
+ bool CanSuspend();
+ bool CanHibernate();
+ bool CanReboot();
+
+ int BatteryLevel();
+
+ void ProcessEvents();
+
+ static void SettingOptionsShutdownStatesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ IPowerSyscall* GetPowerSyscall() const { return m_instance.get(); }
+
+private:
+ void OnSleep() override;
+ void OnWake() override;
+ void OnLowBattery() override;
+ void RestorePlayerState();
+ void StorePlayerState();
+
+ // Construction parameters
+ std::shared_ptr<CSettings> m_settings;
+
+ std::unique_ptr<IPowerSyscall> m_instance;
+ std::unique_ptr<CFileItem> m_lastPlayedFileItem;
+ std::string m_lastUsedPlayer;
+};
diff --git a/xbmc/powermanagement/PowerTypes.h b/xbmc/powermanagement/PowerTypes.h
new file mode 100644
index 0000000..625d00e
--- /dev/null
+++ b/xbmc/powermanagement/PowerTypes.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 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
+
+enum PowerState
+{
+ POWERSTATE_QUIT = 0,
+ POWERSTATE_SHUTDOWN,
+ POWERSTATE_HIBERNATE,
+ POWERSTATE_SUSPEND,
+ POWERSTATE_REBOOT,
+ POWERSTATE_MINIMIZE,
+ POWERSTATE_NONE,
+ POWERSTATE_ASK
+};
diff --git a/xbmc/powermanagement/WinIdleTimer.h b/xbmc/powermanagement/WinIdleTimer.h
new file mode 100644
index 0000000..d7a6f8f
--- /dev/null
+++ b/xbmc/powermanagement/WinIdleTimer.h
@@ -0,0 +1,18 @@
+/*
+ * 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 "utils/Stopwatch.h"
+
+class CWinIdleTimer : public CStopWatch
+{
+public:
+ void StartZero();
+};
+
diff --git a/xbmc/profiles/CMakeLists.txt b/xbmc/profiles/CMakeLists.txt
new file mode 100644
index 0000000..1756c0c
--- /dev/null
+++ b/xbmc/profiles/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES Profile.cpp
+ ProfileManager.cpp)
+
+set(HEADERS Profile.h
+ ProfileManager.h)
+
+core_add_library(profiles)
diff --git a/xbmc/profiles/Profile.cpp b/xbmc/profiles/Profile.cpp
new file mode 100644
index 0000000..5665d25
--- /dev/null
+++ b/xbmc/profiles/Profile.cpp
@@ -0,0 +1,126 @@
+/*
+ * 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 "Profile.h"
+
+#include "XBDateTime.h"
+#include "utils/XMLUtils.h"
+
+CProfile::CLock::CLock(LockType type, const std::string &password):
+ code(password)
+{
+ programs = false;
+ pictures = false;
+ files = false;
+ video = false;
+ music = false;
+ settings = LOCK_LEVEL::NONE;
+ addonManager = false;
+ games = false;
+ mode = type;
+}
+
+void CProfile::CLock::Validate()
+{
+ if (mode != LOCK_MODE_EVERYONE && (code == "-" || code.empty()))
+ mode = LOCK_MODE_EVERYONE;
+
+ if (code.empty() || mode == LOCK_MODE_EVERYONE)
+ code = "-";
+}
+
+CProfile::CProfile(const std::string &directory, const std::string &name, const int id):
+ m_directory(directory),
+ m_name(name)
+{
+ m_id = id;
+ m_bDatabases = true;
+ m_bCanWrite = true;
+ m_bSources = true;
+ m_bCanWriteSources = true;
+ m_bAddons = true;
+}
+
+CProfile::~CProfile(void) = default;
+
+void CProfile::setDate()
+{
+ const CDateTime now = CDateTime::GetCurrentDateTime();
+ std::string strDate = now.GetAsLocalizedDate(false);
+ std::string strTime = now.GetAsLocalizedTime(TIME_FORMAT_GUESS);
+ if (strDate.empty() || strTime.empty())
+ setDate("-");
+ else
+ setDate(strDate+" - "+strTime);
+}
+
+void CProfile::Load(const TiXmlNode *node, int nextIdProfile)
+{
+ if (!XMLUtils::GetInt(node, "id", m_id))
+ m_id = nextIdProfile;
+
+ XMLUtils::GetString(node, "name", m_name);
+ XMLUtils::GetPath(node, "directory", m_directory);
+ XMLUtils::GetPath(node, "thumbnail", m_thumb);
+ XMLUtils::GetBoolean(node, "hasdatabases", m_bDatabases);
+ XMLUtils::GetBoolean(node, "canwritedatabases", m_bCanWrite);
+ XMLUtils::GetBoolean(node, "hassources", m_bSources);
+ XMLUtils::GetBoolean(node, "canwritesources", m_bCanWriteSources);
+ XMLUtils::GetBoolean(node, "lockaddonmanager", m_locks.addonManager);
+ int settings = m_locks.settings;
+ XMLUtils::GetInt(node, "locksettings", settings);
+ m_locks.settings = (LOCK_LEVEL::SETTINGS_LOCK)settings;
+ XMLUtils::GetBoolean(node, "lockfiles", m_locks.files);
+ XMLUtils::GetBoolean(node, "lockmusic", m_locks.music);
+ XMLUtils::GetBoolean(node, "lockvideo", m_locks.video);
+ XMLUtils::GetBoolean(node, "lockpictures", m_locks.pictures);
+ XMLUtils::GetBoolean(node, "lockprograms", m_locks.programs);
+ XMLUtils::GetBoolean(node, "lockgames", m_locks.games);
+
+ int lockMode = m_locks.mode;
+ XMLUtils::GetInt(node, "lockmode", lockMode);
+ m_locks.mode = (LockType)lockMode;
+ if (m_locks.mode > LOCK_MODE_QWERTY || m_locks.mode < LOCK_MODE_EVERYONE)
+ m_locks.mode = LOCK_MODE_EVERYONE;
+
+ XMLUtils::GetString(node, "lockcode", m_locks.code);
+ XMLUtils::GetString(node, "lastdate", m_date);
+}
+
+void CProfile::Save(TiXmlNode *root) const
+{
+ TiXmlElement profileNode("profile");
+ TiXmlNode *node = root->InsertEndChild(profileNode);
+
+ XMLUtils::SetInt(node, "id", m_id);
+ XMLUtils::SetString(node, "name", m_name);
+ XMLUtils::SetPath(node, "directory", m_directory);
+ XMLUtils::SetPath(node, "thumbnail", m_thumb);
+ XMLUtils::SetBoolean(node, "hasdatabases", m_bDatabases);
+ XMLUtils::SetBoolean(node, "canwritedatabases", m_bCanWrite);
+ XMLUtils::SetBoolean(node, "hassources", m_bSources);
+ XMLUtils::SetBoolean(node, "canwritesources", m_bCanWriteSources);
+ XMLUtils::SetBoolean(node, "lockaddonmanager", m_locks.addonManager);
+ XMLUtils::SetInt(node, "locksettings", m_locks.settings);
+ XMLUtils::SetBoolean(node, "lockfiles", m_locks.files);
+ XMLUtils::SetBoolean(node, "lockmusic", m_locks.music);
+ XMLUtils::SetBoolean(node, "lockvideo", m_locks.video);
+ XMLUtils::SetBoolean(node, "lockpictures", m_locks.pictures);
+ XMLUtils::SetBoolean(node, "lockprograms", m_locks.programs);
+ XMLUtils::SetBoolean(node, "lockgames", m_locks.games);
+
+ XMLUtils::SetInt(node, "lockmode", m_locks.mode);
+ XMLUtils::SetString(node,"lockcode", m_locks.code);
+ XMLUtils::SetString(node, "lastdate", m_date);
+}
+
+void CProfile::SetLocks(const CProfile::CLock &locks)
+{
+ m_locks = locks;
+ m_locks.Validate();
+}
diff --git a/xbmc/profiles/Profile.h b/xbmc/profiles/Profile.h
new file mode 100644
index 0000000..42d4fba
--- /dev/null
+++ b/xbmc/profiles/Profile.h
@@ -0,0 +1,98 @@
+/*
+ * 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 "LockType.h"
+#include "SettingsLock.h"
+
+#include <string>
+#include <vector>
+
+class TiXmlNode;
+
+class CProfile
+{
+public:
+ /*! \brief Class for handling lock status
+ */
+ class CLock
+ {
+ public:
+ CLock(LockType type = LOCK_MODE_EVERYONE, const std::string &password = "");
+ void Validate();
+
+ LockType mode;
+ std::string code;
+ bool addonManager;
+ LOCK_LEVEL::SETTINGS_LOCK settings;
+ bool music;
+ bool video;
+ bool files;
+ bool pictures;
+ bool programs;
+ bool games;
+ };
+
+ CProfile(const std::string &directory = "", const std::string &name = "", const int id = -1);
+ ~CProfile(void);
+
+ void Load(const TiXmlNode *node, int nextIdProfile);
+ void Save(TiXmlNode *root) const;
+
+ const std::string& getDate() const { return m_date;}
+ int getId() const { return m_id; }
+ const std::string& getName() const { return m_name;}
+ const std::string& getDirectory() const { return m_directory;}
+ const std::string& getThumb() const { return m_thumb;}
+ const std::string& getLockCode() const { return m_locks.code;}
+ LockType getLockMode() const { return m_locks.mode; }
+
+ bool hasDatabases() const { return m_bDatabases; }
+ bool canWriteDatabases() const { return m_bCanWrite; }
+ bool hasSources() const { return m_bSources; }
+ bool canWriteSources() const { return m_bCanWriteSources; }
+ bool hasAddons() const { return m_bAddons; }
+ /**
+ \brief Returns which settings levels are locked for the current profile
+ \sa LOCK_LEVEL::SETTINGS_LOCK
+ */
+ LOCK_LEVEL::SETTINGS_LOCK settingsLockLevel() const { return m_locks.settings; }
+ bool addonmanagerLocked() const { return m_locks.addonManager; }
+ bool musicLocked() const { return m_locks.music; }
+ bool videoLocked() const { return m_locks.video; }
+ bool picturesLocked() const { return m_locks.pictures; }
+ bool filesLocked() const { return m_locks.files; }
+ bool programsLocked() const { return m_locks.programs; }
+ bool gamesLocked() const { return m_locks.games; }
+ const CLock &GetLocks() const { return m_locks; }
+
+ void setName(const std::string& name) {m_name = name;}
+ void setDirectory(const std::string& directory) {m_directory = directory;}
+ void setDate(const std::string& strDate) { m_date = strDate;}
+ void setDate();
+ void setThumb(const std::string& thumb) {m_thumb = thumb;}
+ void setDatabases(bool bHas) { m_bDatabases = bHas; }
+ void setWriteDatabases(bool bCan) { m_bCanWrite = bCan; }
+ void setSources(bool bHas) { m_bSources = bHas; }
+ void setWriteSources(bool bCan) { m_bCanWriteSources = bCan; }
+ void SetLocks(const CLock &locks);
+
+private:
+ std::string m_directory;
+ int m_id;
+ std::string m_name;
+ std::string m_date;
+ std::string m_thumb;
+ bool m_bDatabases;
+ bool m_bCanWrite;
+ bool m_bSources;
+ bool m_bCanWriteSources;
+ bool m_bAddons;
+ CLock m_locks;
+};
diff --git a/xbmc/profiles/ProfileManager.cpp b/xbmc/profiles/ProfileManager.cpp
new file mode 100644
index 0000000..e4e5bec
--- /dev/null
+++ b/xbmc/profiles/ProfileManager.cpp
@@ -0,0 +1,747 @@
+/*
+ * Copyright (C) 2013-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 "ProfileManager.h"
+
+#include "DatabaseManager.h"
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIPassword.h"
+#include "PasswordManager.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "addons/Skin.h"
+#include "application/Application.h" //! @todo Remove me
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "events/EventLog.h"
+#include "events/EventLogManager.h"
+#include "filesystem/Directory.h"
+#include "filesystem/DirectoryCache.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/InputManager.h"
+#include "music/MusicLibraryQueue.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/SettingsManager.h"
+#include "threads/SingleLock.h"
+
+#include <algorithm>
+#include <mutex>
+#include <string>
+#include <vector>
+#if !defined(TARGET_WINDOWS) && defined(HAS_DVD_DRIVE)
+#include "storage/DetectDVDType.h"
+#endif
+#include "ContextMenuManager.h" //! @todo Remove me
+#include "PlayListPlayer.h" //! @todo Remove me
+#include "addons/AddonManager.h" //! @todo Remove me
+#include "addons/Service.h" //! @todo Remove me
+#include "application/Application.h" //! @todo Remove me
+#include "favourites/FavouritesService.h" //! @todo Remove me
+#include "guilib/StereoscopicsManager.h" //! @todo Remove me
+#include "interfaces/json-rpc/JSONRPC.h" //! @todo Remove me
+#include "network/Network.h" //! @todo Remove me
+#include "network/NetworkServices.h" //! @todo Remove me
+#include "pvr/PVRManager.h" //! @todo Remove me
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+#include "video/VideoLibraryQueue.h" //! @todo Remove me
+#include "weather/WeatherManager.h" //! @todo Remove me
+
+//! @todo
+//! eventually the profile should dictate where special://masterprofile/ is
+//! but for now it makes sense to leave all the profile settings in a user
+//! writeable location like special://masterprofile/
+#define PROFILES_FILE "special://masterprofile/profiles.xml"
+
+#define XML_PROFILES "profiles"
+#define XML_AUTO_LOGIN "autologin"
+#define XML_LAST_LOADED "lastloaded"
+#define XML_LOGIN_SCREEN "useloginscreen"
+#define XML_NEXTID "nextIdProfile"
+#define XML_PROFILE "profile"
+
+using namespace XFILE;
+
+static CProfile EmptyProfile;
+
+CProfileManager::CProfileManager() : m_eventLogs(new CEventLogManager)
+{
+}
+
+CProfileManager::~CProfileManager() = default;
+
+void CProfileManager::Initialize(const std::shared_ptr<CSettings>& settings)
+{
+ m_settings = settings;
+
+ if (m_settings->IsLoaded())
+ OnSettingsLoaded();
+
+ m_settings->GetSettingsManager()->RegisterSettingsHandler(this);
+
+ std::set<std::string> settingSet = {
+ CSettings::SETTING_EVENTLOG_SHOW
+ };
+
+ m_settings->GetSettingsManager()->RegisterCallback(this, settingSet);
+}
+
+void CProfileManager::Uninitialize()
+{
+ m_settings->GetSettingsManager()->UnregisterCallback(this);
+ m_settings->GetSettingsManager()->UnregisterSettingsHandler(this);
+}
+
+void CProfileManager::OnSettingsLoaded()
+{
+ // check them all
+ std::string strDir = m_settings->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH);
+ if (strDir == "set default" || strDir.empty())
+ {
+ strDir = "special://profile/playlists/";
+ m_settings->SetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH, strDir);
+ }
+
+ CDirectory::Create(strDir);
+ CDirectory::Create(URIUtils::AddFileToFolder(strDir,"music"));
+ CDirectory::Create(URIUtils::AddFileToFolder(strDir,"video"));
+ CDirectory::Create(URIUtils::AddFileToFolder(strDir,"mixed"));
+}
+
+void CProfileManager::OnSettingsSaved() const
+{
+ // save mastercode
+ Save();
+}
+
+void CProfileManager::OnSettingsCleared()
+{
+ Clear();
+}
+
+bool CProfileManager::Load()
+{
+ bool ret = true;
+ const std::string file = PROFILES_FILE;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ // clear out our profiles
+ m_profiles.clear();
+
+ if (CFile::Exists(file))
+ {
+ CXBMCTinyXML profilesDoc;
+ if (profilesDoc.LoadFile(file))
+ {
+ const TiXmlElement *rootElement = profilesDoc.RootElement();
+ if (rootElement && StringUtils::EqualsNoCase(rootElement->Value(), XML_PROFILES))
+ {
+ XMLUtils::GetUInt(rootElement, XML_LAST_LOADED, m_lastUsedProfile);
+ XMLUtils::GetBoolean(rootElement, XML_LOGIN_SCREEN, m_usingLoginScreen);
+ XMLUtils::GetInt(rootElement, XML_AUTO_LOGIN, m_autoLoginProfile);
+ XMLUtils::GetInt(rootElement, XML_NEXTID, m_nextProfileId);
+
+ std::string defaultDir("special://home/userdata");
+ if (!CDirectory::Exists(defaultDir))
+ defaultDir = "special://xbmc/userdata";
+
+ const TiXmlElement* pProfile = rootElement->FirstChildElement(XML_PROFILE);
+ while (pProfile)
+ {
+ CProfile profile(defaultDir);
+ profile.Load(pProfile, GetNextProfileId());
+ AddProfile(profile);
+
+ pProfile = pProfile->NextSiblingElement(XML_PROFILE);
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "CProfileManager: error loading {}, no <profiles> node", file);
+ ret = false;
+ }
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "CProfileManager: error loading {}, Line {}\n{}", file,
+ profilesDoc.ErrorRow(), profilesDoc.ErrorDesc());
+ ret = false;
+ }
+ }
+ if (!ret)
+ {
+ CLog::Log(LOGERROR,
+ "Failed to load profile - might be corrupted - falling back to master profile");
+ m_profiles.clear();
+ CFile::Delete(file);
+
+ ret = true;
+ }
+
+ if (m_profiles.empty())
+ { // add the master user
+ CProfile profile("special://masterprofile/", "Master user", 0);
+ AddProfile(profile);
+ }
+
+ // check the validity of the previous profile index
+ if (m_lastUsedProfile >= m_profiles.size())
+ m_lastUsedProfile = 0;
+
+ SetCurrentProfileId(m_lastUsedProfile);
+
+ // check the validity of the auto login profile index
+ if (m_autoLoginProfile < -1 || m_autoLoginProfile >= (int)m_profiles.size())
+ m_autoLoginProfile = -1;
+ else if (m_autoLoginProfile >= 0)
+ SetCurrentProfileId(m_autoLoginProfile);
+
+ // the login screen runs as the master profile, so if we're using this, we need to ensure
+ // we switch to the master profile
+ if (m_usingLoginScreen)
+ SetCurrentProfileId(0);
+
+ return ret;
+}
+
+bool CProfileManager::Save() const
+{
+ const std::string file = PROFILES_FILE;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ CXBMCTinyXML xmlDoc;
+ TiXmlElement xmlRootElement(XML_PROFILES);
+ TiXmlNode *pRoot = xmlDoc.InsertEndChild(xmlRootElement);
+ if (pRoot == nullptr)
+ return false;
+
+ XMLUtils::SetInt(pRoot, XML_LAST_LOADED, m_currentProfile);
+ XMLUtils::SetBoolean(pRoot, XML_LOGIN_SCREEN, m_usingLoginScreen);
+ XMLUtils::SetInt(pRoot, XML_AUTO_LOGIN, m_autoLoginProfile);
+ XMLUtils::SetInt(pRoot, XML_NEXTID, m_nextProfileId);
+
+ for (const auto& profile : m_profiles)
+ profile.Save(pRoot);
+
+ // save the file
+ return xmlDoc.SaveFile(file);
+}
+
+void CProfileManager::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_usingLoginScreen = false;
+ m_profileLoadedForLogin = false;
+ m_previousProfileLoadedForLogin = false;
+ m_lastUsedProfile = 0;
+ m_nextProfileId = 0;
+ SetCurrentProfileId(0);
+ m_profiles.clear();
+}
+
+void CProfileManager::PrepareLoadProfile(unsigned int profileIndex)
+{
+ CContextMenuManager &contextMenuManager = CServiceBroker::GetContextMenuManager();
+ ADDON::CServiceAddonManager &serviceAddons = CServiceBroker::GetServiceAddons();
+ PVR::CPVRManager &pvrManager = CServiceBroker::GetPVRManager();
+ CNetworkBase &networkManager = CServiceBroker::GetNetwork();
+
+ contextMenuManager.Deinit();
+
+ serviceAddons.Stop();
+
+ // stop PVR related services
+ pvrManager.Stop();
+
+ if (profileIndex != 0 || !IsMasterProfile())
+ networkManager.NetworkMessage(CNetworkBase::SERVICES_DOWN, 1);
+}
+
+bool CProfileManager::LoadProfile(unsigned int index)
+{
+ PrepareLoadProfile(index);
+
+ if (index == 0 && IsMasterProfile())
+ {
+ CGUIWindow* pWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_HOME);
+ if (pWindow)
+ pWindow->ResetControlStates();
+
+ UpdateCurrentProfileDate();
+ FinalizeLoadProfile();
+
+ return true;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // check if the index is valid or not
+ if (index >= m_profiles.size())
+ return false;
+
+ // check if the profile is already active
+ if (m_currentProfile == index)
+ return true;
+
+ // save any settings of the currently used skin but only if the (master)
+ // profile hasn't just been loaded as a temporary profile for login
+ if (g_SkinInfo != nullptr && !m_previousProfileLoadedForLogin)
+ g_SkinInfo->SaveSettings();
+
+ // @todo: why is m_settings not used here?
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ // unload any old settings
+ settings->Unload();
+
+ SetCurrentProfileId(index);
+ m_previousProfileLoadedForLogin = false;
+
+ // load the new settings
+ if (!settings->Load())
+ {
+ CLog::Log(LOGFATAL, "CProfileManager: unable to load settings for profile \"{}\"",
+ m_profiles.at(index).getName());
+ return false;
+ }
+ settings->SetLoaded();
+
+ CreateProfileFolders();
+
+ CServiceBroker::GetDatabaseManager().Initialize();
+ CServiceBroker::GetInputManager().LoadKeymaps();
+
+ CServiceBroker::GetInputManager().SetMouseEnabled(settings->GetBool(CSettings::SETTING_INPUT_ENABLEMOUSE));
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ {
+ CGUIInfoManager& infoMgr = gui->GetInfoManager();
+ infoMgr.ResetCache();
+ infoMgr.GetInfoProviders().GetGUIControlsInfoProvider().ResetContainerMovingCache();
+ infoMgr.GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
+ }
+
+ if (m_currentProfile != 0)
+ {
+ CXBMCTinyXML doc;
+ if (doc.LoadFile(URIUtils::AddFileToFolder(GetUserDataFolder(), "guisettings.xml")))
+ {
+ settings->LoadSetting(doc.RootElement(), CSettings::SETTING_MASTERLOCK_MAXRETRIES);
+ settings->LoadSetting(doc.RootElement(), CSettings::SETTING_MASTERLOCK_STARTUPLOCK);
+ }
+ }
+
+ CPasswordManager::GetInstance().Clear();
+
+ // to set labels - shares are reloaded
+#if !defined(TARGET_WINDOWS) && defined(HAS_DVD_DRIVE)
+ MEDIA_DETECT::CDetectDVDMedia::UpdateState();
+#endif
+
+ // init windows
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_WINDOW_RESET);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ CUtil::DeleteDirectoryCache();
+ g_directoryCache.Clear();
+
+ lock.unlock();
+
+ UpdateCurrentProfileDate();
+ FinalizeLoadProfile();
+
+ m_profileLoadedForLogin = false;
+
+ return true;
+}
+
+void CProfileManager::FinalizeLoadProfile()
+{
+ CContextMenuManager &contextMenuManager = CServiceBroker::GetContextMenuManager();
+ ADDON::CServiceAddonManager &serviceAddons = CServiceBroker::GetServiceAddons();
+ PVR::CPVRManager &pvrManager = CServiceBroker::GetPVRManager();
+ CNetworkBase &networkManager = CServiceBroker::GetNetwork();
+ ADDON::CAddonMgr &addonManager = CServiceBroker::GetAddonMgr();
+ CWeatherManager &weatherManager = CServiceBroker::GetWeatherManager();
+ CFavouritesService &favouritesManager = CServiceBroker::GetFavouritesService();
+ PLAYLIST::CPlayListPlayer &playlistManager = CServiceBroker::GetPlaylistPlayer();
+ CStereoscopicsManager &stereoscopicsManager = CServiceBroker::GetGUI()->GetStereoscopicsManager();
+
+ if (m_lastUsedProfile != m_currentProfile)
+ {
+ playlistManager.ClearPlaylist(PLAYLIST::TYPE_VIDEO);
+ playlistManager.ClearPlaylist(PLAYLIST::TYPE_MUSIC);
+ playlistManager.SetCurrentPlaylist(PLAYLIST::TYPE_NONE);
+ }
+
+ networkManager.NetworkMessage(CNetworkBase::SERVICES_UP, 1);
+
+ // reload the add-ons, or we will first load all add-ons from the master account without checking disabled status
+ addonManager.ReInit();
+
+ // let CApplication know that we are logging into a new profile
+ g_application.SetLoggingIn(true);
+
+ if (!g_application.LoadLanguage(true))
+ {
+ CLog::Log(LOGFATAL, "Unable to load language for profile \"{}\"",
+ GetCurrentProfile().getName());
+ return;
+ }
+
+ weatherManager.Refresh();
+
+ JSONRPC::CJSONRPC::Initialize();
+
+ // Restart context menu manager
+ contextMenuManager.Init();
+
+ // Restart PVR services if we are not just loading the master profile for the login screen
+ if (m_previousProfileLoadedForLogin || m_currentProfile != 0 || m_lastUsedProfile == 0)
+ pvrManager.Init();
+
+ favouritesManager.ReInit(GetProfileUserDataFolder());
+
+ // Start these operations only when a profile is loaded, not on the login screen
+ if (!m_profileLoadedForLogin || (m_profileLoadedForLogin && m_lastUsedProfile == 0))
+ {
+ serviceAddons.Start();
+ g_application.UpdateLibraries();
+ }
+
+ stereoscopicsManager.Initialize();
+
+ // Load initial window
+ int firstWindow = g_SkinInfo->GetFirstWindow();
+
+ CServiceBroker::GetGUI()->GetWindowManager().ChangeActiveWindow(firstWindow);
+
+ //the user interfaces has been fully initialized, let everyone know
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, WINDOW_SETTINGS_PROFILES, 0, GUI_MSG_UI_READY);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CProfileManager::LogOff()
+{
+ CNetworkBase &networkManager = CServiceBroker::GetNetwork();
+
+ g_application.StopPlaying();
+
+ if (CMusicLibraryQueue::GetInstance().IsScanningLibrary())
+ CMusicLibraryQueue::GetInstance().StopLibraryScanning();
+
+ if (CVideoLibraryQueue::GetInstance().IsRunning())
+ CVideoLibraryQueue::GetInstance().CancelAllJobs();
+
+ // Stop PVR services
+ CServiceBroker::GetPVRManager().Stop();
+
+ networkManager.NetworkMessage(CNetworkBase::SERVICES_DOWN, 1);
+
+ LoadMasterProfileForLogin();
+
+ g_passwordManager.bMasterUser = false;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->WakeUpScreenSaverAndDPMS();
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_LOGIN_SCREEN, {}, false);
+
+ if (!CServiceBroker::GetNetwork().GetServices().StartEventServer()) // event server could be needed in some situations
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, g_localizeStrings.Get(33102), g_localizeStrings.Get(33100));
+}
+
+bool CProfileManager::DeleteProfile(unsigned int index)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ const CProfile *profile = GetProfile(index);
+ if (profile == NULL)
+ return false;
+
+ CGUIDialogYesNo* dlgYesNo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (dlgYesNo == NULL)
+ return false;
+
+ const std::string& str = g_localizeStrings.Get(13201);
+ dlgYesNo->SetHeading(CVariant{13200});
+ dlgYesNo->SetLine(0, CVariant{StringUtils::Format(str, profile->getName())});
+ dlgYesNo->SetLine(1, CVariant{""});
+ dlgYesNo->SetLine(2, CVariant{""});
+ dlgYesNo->Open();
+
+ if (!dlgYesNo->IsConfirmed())
+ return false;
+
+ // fall back to master profile if necessary
+ if ((int)index == m_autoLoginProfile)
+ m_autoLoginProfile = 0;
+
+ // delete profile
+ std::string strDirectory = profile->getDirectory();
+ m_profiles.erase(m_profiles.begin() + index);
+
+ // fall back to master profile if necessary
+ if (index == m_currentProfile)
+ {
+ LoadProfile(0);
+ m_settings->Save();
+ }
+
+ CFileItemPtr item = CFileItemPtr(new CFileItem(URIUtils::AddFileToFolder(GetUserDataFolder(), strDirectory)));
+ item->SetPath(URIUtils::AddFileToFolder(GetUserDataFolder(), strDirectory + "/"));
+ item->m_bIsFolder = true;
+ item->Select(true);
+
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui && gui->ConfirmDelete(item->GetPath()))
+ CFileUtils::DeleteItem(item);
+
+ return Save();
+}
+
+void CProfileManager::CreateProfileFolders()
+{
+ CDirectory::Create(GetDatabaseFolder());
+ CDirectory::Create(GetCDDBFolder());
+ CDirectory::Create(GetLibraryFolder());
+
+ // create Thumbnails/*
+ CDirectory::Create(GetThumbnailsFolder());
+ CDirectory::Create(GetVideoThumbFolder());
+ CDirectory::Create(GetBookmarksThumbFolder());
+ CDirectory::Create(GetSavestatesFolder());
+ for (size_t hex = 0; hex < 16; hex++)
+ CDirectory::Create(
+ URIUtils::AddFileToFolder(GetThumbnailsFolder(), StringUtils::Format("{:x}", hex)));
+
+ CDirectory::Create("special://profile/addon_data");
+ CDirectory::Create("special://profile/keymaps");
+}
+
+const CProfile& CProfileManager::GetMasterProfile() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (!m_profiles.empty())
+ return m_profiles[0];
+
+ CLog::Log(LOGERROR, "{}: master profile doesn't exist", __FUNCTION__);
+ return EmptyProfile;
+}
+
+const CProfile& CProfileManager::GetCurrentProfile() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (m_currentProfile < m_profiles.size())
+ return m_profiles[m_currentProfile];
+
+ CLog::Log(LOGERROR, "CProfileManager: current profile index ({0}) is outside of the valid range ({1})", m_currentProfile, m_profiles.size());
+ return EmptyProfile;
+}
+
+const CProfile* CProfileManager::GetProfile(unsigned int index) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (index < m_profiles.size())
+ return &m_profiles[index];
+
+ return NULL;
+}
+
+CProfile* CProfileManager::GetProfile(unsigned int index)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (index < m_profiles.size())
+ return &m_profiles[index];
+
+ return NULL;
+}
+
+int CProfileManager::GetProfileIndex(const std::string &name) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ for (int i = 0; i < static_cast<int>(m_profiles.size()); i++)
+ {
+ if (StringUtils::EqualsNoCase(m_profiles[i].getName(), name))
+ return i;
+ }
+
+ return -1;
+}
+
+void CProfileManager::AddProfile(const CProfile &profile)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // data integrity check - covers off migration from old profiles.xml,
+ // incrementing of the m_nextIdProfile,and bad data coming in
+ m_nextProfileId = std::max(m_nextProfileId, profile.getId() + 1);
+
+ m_profiles.push_back(profile);
+ }
+ Save();
+}
+
+void CProfileManager::UpdateCurrentProfileDate()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (m_currentProfile < m_profiles.size())
+ {
+ m_profiles[m_currentProfile].setDate();
+ CSingleExit exit(m_critical);
+ Save();
+ }
+}
+
+void CProfileManager::LoadMasterProfileForLogin()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // save the previous user
+ m_lastUsedProfile = m_currentProfile;
+ if (m_currentProfile != 0)
+ {
+ // determines that the (master) profile has only been loaded for login
+ m_profileLoadedForLogin = true;
+
+ LoadProfile(0);
+
+ // remember that the (master) profile has only been loaded for login
+ m_previousProfileLoadedForLogin = true;
+ }
+}
+
+bool CProfileManager::GetProfileName(const unsigned int profileId, std::string& name) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ const CProfile *profile = GetProfile(profileId);
+ if (!profile)
+ return false;
+
+ name = profile->getName();
+ return true;
+}
+
+std::string CProfileManager::GetUserDataFolder() const
+{
+ return GetMasterProfile().getDirectory();
+}
+
+std::string CProfileManager::GetProfileUserDataFolder() const
+{
+ if (m_currentProfile == 0)
+ return GetUserDataFolder();
+
+ return URIUtils::AddFileToFolder(GetUserDataFolder(), GetCurrentProfile().getDirectory());
+}
+
+std::string CProfileManager::GetDatabaseFolder() const
+{
+ if (GetCurrentProfile().hasDatabases())
+ return URIUtils::AddFileToFolder(GetProfileUserDataFolder(), "Database");
+
+ return URIUtils::AddFileToFolder(GetUserDataFolder(), "Database");
+}
+
+std::string CProfileManager::GetCDDBFolder() const
+{
+ return URIUtils::AddFileToFolder(GetDatabaseFolder(), "CDDB");
+}
+
+std::string CProfileManager::GetThumbnailsFolder() const
+{
+ if (GetCurrentProfile().hasDatabases())
+ return URIUtils::AddFileToFolder(GetProfileUserDataFolder(), "Thumbnails");
+
+ return URIUtils::AddFileToFolder(GetUserDataFolder(), "Thumbnails");
+}
+
+std::string CProfileManager::GetVideoThumbFolder() const
+{
+ return URIUtils::AddFileToFolder(GetThumbnailsFolder(), "Video");
+}
+
+std::string CProfileManager::GetBookmarksThumbFolder() const
+{
+ return URIUtils::AddFileToFolder(GetVideoThumbFolder(), "Bookmarks");
+}
+
+std::string CProfileManager::GetLibraryFolder() const
+{
+ if (GetCurrentProfile().hasDatabases())
+ return URIUtils::AddFileToFolder(GetProfileUserDataFolder(), "library");
+
+ return URIUtils::AddFileToFolder(GetUserDataFolder(), "library");
+}
+
+std::string CProfileManager::GetSavestatesFolder() const
+{
+ if (GetCurrentProfile().hasDatabases())
+ return URIUtils::AddFileToFolder(GetProfileUserDataFolder(), "Savestates");
+
+ return URIUtils::AddFileToFolder(GetUserDataFolder(), "Savestates");
+}
+
+std::string CProfileManager::GetSettingsFile() const
+{
+ if (m_currentProfile == 0)
+ return "special://masterprofile/guisettings.xml";
+
+ return "special://profile/guisettings.xml";
+}
+
+std::string CProfileManager::GetUserDataItem(const std::string& strFile) const
+{
+ std::string path;
+ path = "special://profile/" + strFile;
+
+ // check if item exists in the profile (either for folder or
+ // for a file (depending on slashAtEnd of strFile) otherwise
+ // return path to masterprofile
+ if ((URIUtils::HasSlashAtEnd(path) && !CDirectory::Exists(path)) || !CFile::Exists(path))
+ path = "special://masterprofile/" + strFile;
+
+ return path;
+}
+
+CEventLog& CProfileManager::GetEventLog()
+{
+ return m_eventLogs->GetEventLog(GetCurrentProfileId());
+}
+
+void CProfileManager::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_EVENTLOG_SHOW)
+ GetEventLog().ShowFullEventLog();
+}
+
+void CProfileManager::SetCurrentProfileId(unsigned int profileId)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_currentProfile = profileId;
+ CSpecialProtocol::SetProfilePath(GetProfileUserDataFolder());
+ }
+ Save();
+}
diff --git a/xbmc/profiles/ProfileManager.h b/xbmc/profiles/ProfileManager.h
new file mode 100644
index 0000000..e394518
--- /dev/null
+++ b/xbmc/profiles/ProfileManager.h
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2013-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 "profiles/Profile.h"
+#include "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <stdint.h>
+#include <vector>
+
+class CEventLog;
+class CEventLogManager;
+class CSettings;
+class TiXmlNode;
+
+class CProfileManager : protected ISettingsHandler,
+ protected ISettingCallback
+{
+public:
+ CProfileManager();
+ CProfileManager(const CProfileManager&) = delete;
+ CProfileManager& operator=(CProfileManager const&) = delete;
+ ~CProfileManager() override;
+
+ void Initialize(const std::shared_ptr<CSettings>& settings);
+ void Uninitialize();
+
+ void OnSettingsLoaded() override;
+ void OnSettingsSaved() const override;
+ void OnSettingsCleared() override;
+
+ bool Load();
+ /*! \brief Load the user profile information from disk
+ Loads the profiles.xml file and creates the list of profiles.
+ If no profiles exist, a master user is created. Should be called
+ after special://masterprofile/ has been defined.
+ \param file XML file to load.
+ */
+
+ bool Save() const;
+ /*! \brief Save the user profile information to disk
+ Saves the list of profiles to the profiles.xml file.
+ \param file XML file to save.
+ \return true on success, false on failure to save
+ */
+
+ void Clear();
+
+ bool LoadProfile(unsigned int index);
+ void LogOff();
+
+ bool DeleteProfile(unsigned int index);
+
+ void CreateProfileFolders();
+
+ /*! \brief Retrieve the master profile
+ \return const reference to the master profile
+ */
+ const CProfile& GetMasterProfile() const;
+
+ /*! \brief Retrieve the current profile
+ \return const reference to the current profile
+ */
+ const CProfile& GetCurrentProfile() const;
+
+ /*! \brief Retrieve the profile from an index
+ \param unsigned index of the profile to retrieve
+ \return const pointer to the profile, NULL if the index is invalid
+ */
+ const CProfile* GetProfile(unsigned int index) const;
+
+ /*! \brief Retrieve the profile from an index
+ \param unsigned index of the profile to retrieve
+ \return pointer to the profile, NULL if the index is invalid
+ */
+ CProfile* GetProfile(unsigned int index);
+
+ /*! \brief Retrieve index of a particular profile by name
+ \param name name of the profile index to retrieve
+ \return index of this profile, -1 if invalid.
+ */
+ int GetProfileIndex(const std::string &name) const;
+
+ /*! \brief Retrieve the number of profiles
+ \return number of profiles
+ */
+ size_t GetNumberOfProfiles() const { return m_profiles.size(); }
+
+ /*! \brief Add a new profile
+ \param profile CProfile to add
+ */
+ void AddProfile(const CProfile &profile);
+
+ /*! \brief Are we using the login screen?
+ \return true if we're using the login screen, false otherwise
+ */
+ bool UsingLoginScreen() const { return m_usingLoginScreen; }
+
+ /*! \brief Toggle login screen use on and off
+ Toggles the login screen state
+ */
+ void ToggleLoginScreen()
+ {
+ m_usingLoginScreen = !m_usingLoginScreen;
+ Save();
+ }
+
+ /*! \brief Are we the master user?
+ \return true if the current profile is the master user, false otherwise
+ */
+ bool IsMasterProfile() const { return m_currentProfile == 0; }
+
+ /*! \brief Update the date of the current profile
+ */
+ void UpdateCurrentProfileDate();
+
+ /*! \brief Load the master user for the purposes of logging in
+ Loads the master user. Identical to LoadProfile(0) but doesn't
+ update the last logged in details
+ */
+ void LoadMasterProfileForLogin();
+
+ /*! \brief Retrieve the last used profile index
+ \return the last used profile that logged in. Does not count the
+ master user during login.
+ */
+ uint32_t GetLastUsedProfileIndex() const { return m_lastUsedProfile; }
+
+ /*! \brief Retrieve the current profile index
+ \return the index of the currently logged in profile.
+ */
+ uint32_t GetCurrentProfileIndex() const { return m_currentProfile; }
+
+ /*! \brief Retrieve the next id to use for a new profile
+ \return the unique <id> to be used when creating a new profile
+ */
+ int GetNextProfileId() const { return m_nextProfileId; }
+
+ int GetCurrentProfileId() const { return GetCurrentProfile().getId(); }
+
+ /*! \brief Retrieve the autologin profile id
+ Retrieves the autologin profile id. When set to -1, then the last
+ used profile will be loaded
+ \return the id to the autologin profile
+ */
+ int GetAutoLoginProfileId() const { return m_autoLoginProfile; }
+
+ /*! \brief Retrieve the autologin profile id
+ Retrieves the autologin profile id. When set to -1, then the last
+ used profile will be loaded
+ \return the id to the autologin profile
+ */
+ void SetAutoLoginProfileId(const int profileId)
+ {
+ m_autoLoginProfile = profileId;
+ Save();
+ }
+
+ /*! \brief Retrieve the name of a particular profile by index
+ \param profileId profile index for which to retrieve the name
+ \param name will hold the name of the profile when a valid profile index has been provided
+ \return false if profileId is an invalid index, true if the name parameter is set
+ */
+ bool GetProfileName(const unsigned int profileId, std::string& name) const;
+
+ std::string GetUserDataFolder() const;
+ std::string GetProfileUserDataFolder() const;
+ std::string GetDatabaseFolder() const;
+ std::string GetCDDBFolder() const;
+ std::string GetThumbnailsFolder() const;
+ std::string GetVideoThumbFolder() const;
+ std::string GetBookmarksThumbFolder() const;
+ std::string GetLibraryFolder() const;
+ std::string GetSavestatesFolder() const;
+ std::string GetSettingsFile() const;
+
+ // uses HasSlashAtEnd to determine if a directory or file was meant
+ std::string GetUserDataItem(const std::string& strFile) const;
+
+ // Event log access
+ CEventLog &GetEventLog();
+
+protected:
+ // implementation of ISettingCallback
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+private:
+ /*! \brief Set the current profile id and update the special://profile path
+ \param profileId profile index
+ */
+ void SetCurrentProfileId(unsigned int profileId);
+
+ void PrepareLoadProfile(unsigned int profileIndex);
+ void FinalizeLoadProfile();
+
+ // Construction parameters
+ std::shared_ptr<CSettings> m_settings;
+
+ std::vector<CProfile> m_profiles;
+ bool m_usingLoginScreen = false;
+ bool m_profileLoadedForLogin = false;
+ bool m_previousProfileLoadedForLogin = false;
+ int m_autoLoginProfile = -1;
+ unsigned int m_lastUsedProfile = 0;
+ unsigned int m_currentProfile =
+ 0; // do not modify directly, use SetCurrentProfileId() function instead
+ int m_nextProfileId =
+ 0; // for tracking the next available id to give to a new profile to ensure id's are not re-used
+ mutable CCriticalSection m_critical;
+
+ // Event properties
+ std::unique_ptr<CEventLogManager> m_eventLogs;
+};
diff --git a/xbmc/profiles/dialogs/CMakeLists.txt b/xbmc/profiles/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..e7fc216
--- /dev/null
+++ b/xbmc/profiles/dialogs/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES GUIDialogLockSettings.cpp
+ GUIDialogProfileSettings.cpp)
+
+set(HEADERS GUIDialogLockSettings.h
+ GUIDialogProfileSettings.h)
+
+core_add_library(profiles_dialogs)
diff --git a/xbmc/profiles/dialogs/GUIDialogLockSettings.cpp b/xbmc/profiles/dialogs/GUIDialogLockSettings.cpp
new file mode 100644
index 0000000..52eabc4
--- /dev/null
+++ b/xbmc/profiles/dialogs/GUIDialogLockSettings.cpp
@@ -0,0 +1,320 @@
+/*
+ * 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 "GUIDialogLockSettings.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "dialogs/GUIDialogGamepad.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "favourites/FavouritesService.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingSection.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <utility>
+
+#define SETTING_USERNAME "user.name"
+#define SETTING_PASSWORD "user.password"
+#define SETTING_PASSWORD_REMEMBER "user.rememberpassword"
+
+#define SETTING_LOCKCODE "lock.code"
+#define SETTING_LOCK_MUSIC "lock.music"
+#define SETTING_LOCK_VIDEOS "lock.videos"
+#define SETTING_LOCK_PICTURES "lock.pictures"
+#define SETTING_LOCK_PROGRAMS "lock.programs"
+#define SETTING_LOCK_FILEMANAGER "lock.filemanager"
+#define SETTING_LOCK_SETTINGS "lock.settings"
+#define SETTING_LOCK_ADDONMANAGER "lock.addonmanager"
+
+CGUIDialogLockSettings::CGUIDialogLockSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_LOCK_SETTINGS, "DialogSettings.xml"),
+ m_saveUserDetails(NULL)
+{ }
+
+CGUIDialogLockSettings::~CGUIDialogLockSettings() = default;
+
+bool CGUIDialogLockSettings::ShowAndGetLock(LockType &lockMode, std::string &password, int header /* = 20091 */)
+{
+ CProfile::CLock locks(lockMode, password);
+ if (!ShowAndGetLock(locks, header, false, false))
+ return false;
+
+ locks.Validate();
+ lockMode = locks.mode;
+ password = locks.code;
+
+ return true;
+}
+
+bool CGUIDialogLockSettings::ShowAndGetLock(CProfile::CLock &locks, int buttonLabel /* = 20091 */, bool conditional /* = false */, bool details /* = true */)
+{
+ CGUIDialogLockSettings *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogLockSettings>(WINDOW_DIALOG_LOCK_SETTINGS);
+ if (dialog == NULL)
+ return false;
+
+ dialog->m_locks = locks;
+ dialog->m_buttonLabel = buttonLabel;
+ dialog->m_getUser = false;
+ dialog->m_conditionalDetails = conditional;
+ dialog->m_details = details;
+ dialog->Open();
+
+ if (!dialog->m_changed)
+ return false;
+
+ locks = dialog->m_locks;
+
+ // changed lock settings for certain sections (e.g. video, audio, or pictures)
+ // => refresh favourites due to possible visibility changes
+ CServiceBroker::GetFavouritesService().RefreshFavourites();
+
+ return true;
+}
+
+bool CGUIDialogLockSettings::ShowAndGetUserAndPassword(std::string &user, std::string &password, const std::string &url, bool *saveUserDetails)
+{
+ CGUIDialogLockSettings *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogLockSettings>(WINDOW_DIALOG_LOCK_SETTINGS);
+ if (dialog == NULL)
+ return false;
+
+ dialog->m_getUser = true;
+ dialog->m_locks.code = password;
+ dialog->m_user = user;
+ dialog->m_url = url;
+ dialog->m_saveUserDetails = saveUserDetails;
+ dialog->Open();
+
+ if (!dialog->m_changed)
+ return false;
+
+ user = dialog->m_user;
+ password = dialog->m_locks.code;
+ return true;
+}
+
+void CGUIDialogLockSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_USERNAME)
+ m_user = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_PASSWORD)
+ m_locks.code = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ else if (settingId == SETTING_PASSWORD_REMEMBER)
+ *m_saveUserDetails = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == SETTING_LOCK_MUSIC)
+ m_locks.music = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == SETTING_LOCK_VIDEOS)
+ m_locks.video = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == SETTING_LOCK_PICTURES)
+ m_locks.pictures = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == SETTING_LOCK_PROGRAMS)
+ m_locks.programs = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == SETTING_LOCK_FILEMANAGER)
+ m_locks.files = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == SETTING_LOCK_SETTINGS)
+ m_locks.settings = static_cast<LOCK_LEVEL::SETTINGS_LOCK>(std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ else if (settingId == SETTING_LOCK_ADDONMANAGER)
+ m_locks.addonManager = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+
+ m_changed = true;
+}
+
+void CGUIDialogLockSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_LOCKCODE)
+ {
+ CGUIDialogSelect* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (!dialog)
+ return;
+
+ dialog->Reset();
+ dialog->SetHeading(CVariant{12360});
+ dialog->Add(g_localizeStrings.Get(1223));
+ dialog->Add(g_localizeStrings.Get(12337));
+ dialog->Add(g_localizeStrings.Get(12338));
+ dialog->Add(g_localizeStrings.Get(12339));
+ dialog->SetSelected(GetLockModeLabel());
+ dialog->Open();
+
+ std::string newPassword;
+ LockType iLockMode = LOCK_MODE_UNKNOWN;
+ bool bResult = false;
+ switch (dialog->GetSelectedItem())
+ {
+ case 0:
+ iLockMode = LOCK_MODE_EVERYONE; //Disabled! Need check routine!!!
+ bResult = true;
+ break;
+
+ case 1:
+ iLockMode = LOCK_MODE_NUMERIC;
+ bResult = CGUIDialogNumeric::ShowAndVerifyNewPassword(newPassword);
+ break;
+
+ case 2:
+ iLockMode = LOCK_MODE_GAMEPAD;
+ bResult = CGUIDialogGamepad::ShowAndVerifyNewPassword(newPassword);
+ break;
+
+ case 3:
+ iLockMode = LOCK_MODE_QWERTY;
+ bResult = CGUIKeyboardFactory::ShowAndVerifyNewPassword(newPassword);
+ break;
+
+ default:
+ break;
+ }
+
+ if (bResult)
+ {
+ if (iLockMode == LOCK_MODE_EVERYONE)
+ newPassword = "-";
+ m_locks.code = newPassword;
+ if (m_locks.code == "-")
+ iLockMode = LOCK_MODE_EVERYONE;
+ m_locks.mode = iLockMode;
+
+ SetSettingLockCodeLabel();
+ SetDetailSettingsEnabled(m_locks.mode != LOCK_MODE_EVERYONE);
+ m_changed = true;
+ }
+ }
+}
+
+void CGUIDialogLockSettings::OnCancel()
+{
+ m_changed = false;
+
+ CGUIDialogSettingsManualBase::OnCancel();
+}
+
+void CGUIDialogLockSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ // set the title
+ if (m_getUser)
+ SetHeading(StringUtils::Format(g_localizeStrings.Get(20152), CURL::Decode(m_url)));
+ else
+ {
+ SetHeading(20066);
+ SetSettingLockCodeLabel();
+ SetDetailSettingsEnabled(m_locks.mode != LOCK_MODE_EVERYONE);
+ }
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+}
+
+void CGUIDialogLockSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("locksettings", -1);
+ if (category == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogLockSettings: unable to setup settings");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogLockSettings: unable to setup settings");
+ return;
+ }
+
+ if (m_getUser)
+ {
+ AddEdit(group, SETTING_USERNAME, 20142, SettingLevel::Basic, m_user);
+ AddEdit(group, SETTING_PASSWORD, 12326, SettingLevel::Basic, m_locks.code, false, true);
+ if (m_saveUserDetails != NULL)
+ AddToggle(group, SETTING_PASSWORD_REMEMBER, 13423, SettingLevel::Basic, *m_saveUserDetails);
+
+ return;
+ }
+
+ AddButton(group, SETTING_LOCKCODE, m_buttonLabel, SettingLevel::Basic);
+
+ if (m_details)
+ {
+ const std::shared_ptr<CSettingGroup> groupDetails = AddGroup(category);
+ if (groupDetails == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogLockSettings: unable to setup settings");
+ return;
+ }
+
+ AddToggle(groupDetails, SETTING_LOCK_MUSIC, 20038, SettingLevel::Basic, m_locks.music);
+ AddToggle(groupDetails, SETTING_LOCK_VIDEOS, 20039, SettingLevel::Basic, m_locks.video);
+ AddToggle(groupDetails, SETTING_LOCK_PICTURES, 20040, SettingLevel::Basic, m_locks.pictures);
+ AddToggle(groupDetails, SETTING_LOCK_PROGRAMS, 20041, SettingLevel::Basic, m_locks.programs);
+ AddToggle(groupDetails, SETTING_LOCK_FILEMANAGER, 20042, SettingLevel::Basic, m_locks.files);
+
+ TranslatableIntegerSettingOptions settingsLevelOptions;
+ settingsLevelOptions.push_back(TranslatableIntegerSettingOption(106, LOCK_LEVEL::NONE));
+ settingsLevelOptions.push_back(TranslatableIntegerSettingOption(593, LOCK_LEVEL::ALL));
+ settingsLevelOptions.push_back(TranslatableIntegerSettingOption(10037, LOCK_LEVEL::STANDARD));
+ settingsLevelOptions.push_back(TranslatableIntegerSettingOption(10038, LOCK_LEVEL::ADVANCED));
+ settingsLevelOptions.push_back(TranslatableIntegerSettingOption(10039, LOCK_LEVEL::EXPERT));
+ AddList(groupDetails, SETTING_LOCK_SETTINGS, 20043, SettingLevel::Basic, static_cast<int>(m_locks.settings), settingsLevelOptions, 20043);
+
+ AddToggle(groupDetails, SETTING_LOCK_ADDONMANAGER, 24090, SettingLevel::Basic, m_locks.addonManager);
+ }
+
+ m_changed = false;
+}
+
+std::string CGUIDialogLockSettings::GetLockModeLabel()
+{
+ return g_localizeStrings.Get(m_locks.mode == LOCK_MODE_EVERYONE ? 1223 : 12336 + m_locks.mode);
+}
+
+void CGUIDialogLockSettings::SetDetailSettingsEnabled(bool enabled)
+{
+ if (!m_details)
+ return;
+
+ enabled |= !m_conditionalDetails;
+ GetSettingControl(SETTING_LOCK_MUSIC)->GetSetting()->SetEnabled(enabled);
+ GetSettingControl(SETTING_LOCK_VIDEOS)->GetSetting()->SetEnabled(enabled);
+ GetSettingControl(SETTING_LOCK_PICTURES)->GetSetting()->SetEnabled(enabled);
+ GetSettingControl(SETTING_LOCK_PROGRAMS)->GetSetting()->SetEnabled(enabled);
+ GetSettingControl(SETTING_LOCK_FILEMANAGER)->GetSetting()->SetEnabled(enabled);
+ GetSettingControl(SETTING_LOCK_SETTINGS)->GetSetting()->SetEnabled(enabled);
+ GetSettingControl(SETTING_LOCK_ADDONMANAGER)->GetSetting()->SetEnabled(enabled);
+}
+
+void CGUIDialogLockSettings::SetSettingLockCodeLabel()
+{
+ // adjust label2 of the lock code setting button
+ if (m_locks.mode > LOCK_MODE_QWERTY)
+ m_locks.mode = LOCK_MODE_EVERYONE;
+ BaseSettingControlPtr settingControl = GetSettingControl(SETTING_LOCKCODE);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ SET_CONTROL_LABEL2(settingControl->GetID(), GetLockModeLabel());
+}
diff --git a/xbmc/profiles/dialogs/GUIDialogLockSettings.h b/xbmc/profiles/dialogs/GUIDialogLockSettings.h
new file mode 100644
index 0000000..bdcaed7
--- /dev/null
+++ b/xbmc/profiles/dialogs/GUIDialogLockSettings.h
@@ -0,0 +1,53 @@
+/*
+ * 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 "profiles/Profile.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+class CGUIDialogLockSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogLockSettings();
+ ~CGUIDialogLockSettings() override;
+
+ static bool ShowAndGetLock(LockType &lockMode, std::string &password, int header = 20091);
+ static bool ShowAndGetLock(CProfile::CLock &locks, int buttonLabel = 20091, bool conditional = false, bool details = true);
+ static bool ShowAndGetUserAndPassword(std::string &user, std::string &password, const std::string &url, bool *saveUserDetails);
+
+protected:
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override { return true; }
+ void OnCancel() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+private:
+ std::string GetLockModeLabel();
+ void SetDetailSettingsEnabled(bool enabled);
+ void SetSettingLockCodeLabel();
+
+ bool m_changed = false;
+
+ CProfile::CLock m_locks;
+ std::string m_user;
+ std::string m_url;
+ bool m_details = true;
+ bool m_conditionalDetails = false;
+ bool m_getUser = false;
+ bool* m_saveUserDetails;
+ int m_buttonLabel = 20091;
+};
diff --git a/xbmc/profiles/dialogs/GUIDialogProfileSettings.cpp b/xbmc/profiles/dialogs/GUIDialogProfileSettings.cpp
new file mode 100644
index 0000000..d7ce0da
--- /dev/null
+++ b/xbmc/profiles/dialogs/GUIDialogProfileSettings.cpp
@@ -0,0 +1,397 @@
+/*
+ * 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 "GUIDialogProfileSettings.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "profiles/ProfileManager.h"
+#include "profiles/dialogs/GUIDialogLockSettings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <cassert>
+#include <utility>
+
+#define SETTING_PROFILE_NAME "profile.name"
+#define SETTING_PROFILE_IMAGE "profile.image"
+#define SETTING_PROFILE_DIRECTORY "profile.directory"
+#define SETTING_PROFILE_LOCKS "profile.locks"
+#define SETTING_PROFILE_MEDIA "profile.media"
+#define SETTING_PROFILE_MEDIA_SOURCES "profile.mediasources"
+
+CGUIDialogProfileSettings::CGUIDialogProfileSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_PROFILE_SETTINGS, "DialogSettings.xml")
+{ }
+
+CGUIDialogProfileSettings::~CGUIDialogProfileSettings() = default;
+
+bool CGUIDialogProfileSettings::ShowForProfile(unsigned int iProfile, bool firstLogin)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (firstLogin && iProfile > profileManager->GetNumberOfProfiles())
+ return false;
+
+ CGUIDialogProfileSettings *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProfileSettings>(WINDOW_DIALOG_PROFILE_SETTINGS);
+ if (dialog == NULL)
+ return false;
+
+ dialog->m_needsSaving = false;
+ dialog->m_isDefault = iProfile == 0;
+ dialog->m_showDetails = !firstLogin;
+
+ const CProfile *profile = profileManager->GetProfile(iProfile);
+ if (profile == NULL)
+ {
+ dialog->m_name.clear();
+ dialog->m_dbMode = 2;
+ dialog->m_sourcesMode = 2;
+ dialog->m_locks = CProfile::CLock();
+
+ bool bLock = profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser;
+ dialog->m_locks.addonManager = bLock;
+ dialog->m_locks.settings = (bLock) ? LOCK_LEVEL::ALL : LOCK_LEVEL::NONE;
+ dialog->m_locks.files = bLock;
+
+ dialog->m_directory.clear();
+ dialog->m_thumb.clear();
+
+ // prompt for a name
+ std::string profileName;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(profileName, CVariant{g_localizeStrings.Get(20093)}, false) || profileName.empty())
+ return false;
+ dialog->m_name = profileName;
+
+ // create a default path
+ std::string defaultDir = URIUtils::AddFileToFolder("profiles", CUtil::MakeLegalFileName(dialog->m_name));
+ URIUtils::AddSlashAtEnd(defaultDir);
+ XFILE::CDirectory::Create(URIUtils::AddFileToFolder("special://masterprofile/", defaultDir));
+
+ // prompt for the user to change it if they want
+ std::string userDir = defaultDir;
+ if (GetProfilePath(userDir, false)) // can't be the master user
+ {
+ if (!URIUtils::PathHasParent(userDir, defaultDir)) // user chose a different folder
+ XFILE::CDirectory::Remove(URIUtils::AddFileToFolder("special://masterprofile/", defaultDir));
+ }
+ dialog->m_directory = userDir;
+ dialog->m_needsSaving = true;
+ }
+ else
+ {
+ dialog->m_name = profile->getName();
+ dialog->m_thumb = profile->getThumb();
+ dialog->m_directory = profile->getDirectory();
+ dialog->m_dbMode = profile->canWriteDatabases() ? 0 : 1;
+ if (profile->hasDatabases())
+ dialog->m_dbMode += 2;
+ dialog->m_sourcesMode = profile->canWriteSources() ? 0 : 1;
+ if (profile->hasSources())
+ dialog->m_sourcesMode += 2;
+
+ dialog->m_locks = profile->GetLocks();
+ }
+
+ dialog->Open();
+ if (dialog->m_needsSaving)
+ {
+ if (iProfile >= profileManager->GetNumberOfProfiles())
+ {
+ if (dialog->m_name.empty() || dialog->m_directory.empty())
+ return false;
+
+ /*std::string strLabel;
+ strLabel.Format(g_localizeStrings.Get(20047),dialog->m_strName);
+ if (!CGUIDialogYesNo::ShowAndGetInput(20058, strLabel, dialog->m_strDirectory, ""))
+ {
+ CDirectory::Remove(URIUtils::AddFileToFolder(profileManager.GetUserDataFolder(), dialog->m_strDirectory));
+ return false;
+ }*/
+
+ // check for old profile settings
+ CProfile profile(dialog->m_directory, dialog->m_name, profileManager->GetNextProfileId());
+ profileManager->AddProfile(profile);
+ bool exists = XFILE::CFile::Exists(URIUtils::AddFileToFolder("special://masterprofile/", dialog->m_directory, "guisettings.xml"));
+
+ if (exists && !CGUIDialogYesNo::ShowAndGetInput(CVariant{20058}, CVariant{20104}))
+ exists = false;
+
+ if (!exists)
+ {
+ // copy masterprofile guisettings to new profile guisettings
+ // If the user selects 'start fresh', do nothing as a fresh
+ // guisettings.xml will be created on first profile use.
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20058}, CVariant{20048}, CVariant{""}, CVariant{""}, CVariant{20044}, CVariant{20064}))
+ {
+ XFILE::CFile::Copy(URIUtils::AddFileToFolder("special://masterprofile/", "guisettings.xml"),
+ URIUtils::AddFileToFolder("special://masterprofile/", dialog->m_directory, "guisettings.xml"));
+ }
+ }
+
+ exists = XFILE::CFile::Exists(URIUtils::AddFileToFolder("special://masterprofile/", dialog->m_directory, "sources.xml"));
+ if (exists && !CGUIDialogYesNo::ShowAndGetInput(CVariant{20058}, CVariant{20106}))
+ exists = false;
+
+ if (!exists)
+ {
+ if ((dialog->m_sourcesMode & 2) == 2)
+ // prompt user to copy masterprofile's sources.xml file
+ // If 'start fresh' (no) is selected, do nothing.
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20058}, CVariant{20071}, CVariant{""}, CVariant{""}, CVariant{20044}, CVariant{20064}))
+ {
+ XFILE::CFile::Copy(URIUtils::AddFileToFolder("special://masterprofile/", "sources.xml"),
+ URIUtils::AddFileToFolder("special://masterprofile/", dialog->m_directory, "sources.xml"));
+ }
+ }
+ }
+
+ /*if (!dialog->m_bIsNewUser)
+ if (!CGUIDialogYesNo::ShowAndGetInput(20067, 20103))
+ return false;*/
+
+ CProfile *profile = profileManager->GetProfile(iProfile);
+ assert(profile);
+ profile->setName(dialog->m_name);
+ profile->setDirectory(dialog->m_directory);
+ profile->setThumb(dialog->m_thumb);
+ profile->setWriteDatabases(!((dialog->m_dbMode & 1) == 1));
+ profile->setWriteSources(!((dialog->m_sourcesMode & 1) == 1));
+ profile->setDatabases((dialog->m_dbMode & 2) == 2);
+ profile->setSources((dialog->m_sourcesMode & 2) == 2);
+ profile->SetLocks(dialog->m_locks);
+ profileManager->Save();
+
+ return true;
+ }
+
+ return dialog->m_needsSaving;
+}
+
+void CGUIDialogProfileSettings::OnWindowLoaded()
+{
+ CGUIDialogSettingsManualBase::OnWindowLoaded();
+}
+
+void CGUIDialogProfileSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_PROFILE_NAME)
+ {
+ m_name = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_PROFILE_MEDIA)
+ m_dbMode = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ else if (settingId == SETTING_PROFILE_MEDIA_SOURCES)
+ m_sourcesMode = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+
+ m_needsSaving = true;
+}
+
+void CGUIDialogProfileSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_PROFILE_IMAGE)
+ {
+ VECSOURCES shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+
+ CFileItemList items;
+ if (!m_thumb.empty())
+ {
+ CFileItemPtr item(new CFileItem("thumb://Current", false));
+ item->SetArt("thumb", m_thumb);
+ item->SetLabel(g_localizeStrings.Get(20016));
+ items.Add(item);
+ }
+
+ CFileItemPtr item(new CFileItem("thumb://None", false));
+ item->SetArt("thumb", "DefaultUser.png");
+ item->SetLabel(g_localizeStrings.Get(20018));
+ items.Add(item);
+
+ std::string thumb;
+ if (CGUIDialogFileBrowser::ShowAndGetImage(items, shares, g_localizeStrings.Get(1030), thumb) &&
+ !StringUtils::EqualsNoCase(thumb, "thumb://Current"))
+ {
+ m_needsSaving = true;
+ m_thumb = StringUtils::EqualsNoCase(thumb, "thumb://None") ? "" : thumb;
+
+ UpdateProfileImage();
+ }
+ }
+ else if (settingId == SETTING_PROFILE_DIRECTORY)
+ {
+ if (!GetProfilePath(m_directory, m_isDefault))
+ return;
+
+ m_needsSaving = true;
+ updateProfileDirectory();
+ }
+ else if (settingId == SETTING_PROFILE_LOCKS)
+ {
+ if (m_showDetails)
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (profileManager->GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE && !m_isDefault)
+ {
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20066}, CVariant{20118}))
+ g_passwordManager.SetMasterLockMode(false);
+ if (profileManager->GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE)
+ return;
+ }
+ if (CGUIDialogLockSettings::ShowAndGetLock(m_locks, m_isDefault ? 12360 : 20068,
+ profileManager->GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE || m_isDefault))
+ m_needsSaving = true;
+ }
+ else
+ {
+ if (CGUIDialogLockSettings::ShowAndGetLock(m_locks, m_isDefault ? 12360 : 20068, false, false))
+ m_needsSaving = true;
+ }
+ }
+}
+
+void CGUIDialogProfileSettings::OnCancel()
+{
+ m_needsSaving = false;
+
+ CGUIDialogSettingsManualBase::OnCancel();
+}
+
+void CGUIDialogProfileSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ // set the heading
+ SetHeading(!m_showDetails ? 20255 : 20067);
+
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+
+ // set the profile image and directory
+ UpdateProfileImage();
+ updateProfileDirectory();
+}
+
+void CGUIDialogProfileSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("profilesettings", -1);
+ if (category == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogProfileSettings: unable to setup settings");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogProfileSettings: unable to setup settings");
+ return;
+ }
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ AddEdit(group, SETTING_PROFILE_NAME, 20093, SettingLevel::Basic, m_name);
+ AddButton(group, SETTING_PROFILE_IMAGE, 20065, SettingLevel::Basic);
+
+ if (!m_isDefault && m_showDetails)
+ AddButton(group, SETTING_PROFILE_DIRECTORY, 20070, SettingLevel::Basic);
+
+ if (m_showDetails ||
+ (m_locks.mode == LOCK_MODE_EVERYONE && profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE))
+ AddButton(group, SETTING_PROFILE_LOCKS, 20066, SettingLevel::Basic);
+
+ if (!m_isDefault && m_showDetails)
+ {
+ const std::shared_ptr<CSettingGroup> groupMedia = AddGroup(category);
+ if (groupMedia == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogProfileSettings: unable to setup settings");
+ return;
+ }
+
+ TranslatableIntegerSettingOptions entries;
+ entries.push_back(TranslatableIntegerSettingOption(20062, 0));
+ entries.push_back(TranslatableIntegerSettingOption(20063, 1));
+ entries.push_back(TranslatableIntegerSettingOption(20061, 2));
+ if (profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE)
+ entries.push_back(TranslatableIntegerSettingOption(20107, 3));
+
+ AddSpinner(groupMedia, SETTING_PROFILE_MEDIA, 20060, SettingLevel::Basic, m_dbMode, entries);
+ AddSpinner(groupMedia, SETTING_PROFILE_MEDIA_SOURCES, 20094, SettingLevel::Basic, m_sourcesMode, entries);
+ }
+}
+
+bool CGUIDialogProfileSettings::GetProfilePath(std::string &directory, bool isDefault)
+{
+ VECSOURCES shares;
+ CMediaSource share;
+ share.strName = g_localizeStrings.Get(13200);
+ share.strPath = "special://masterprofile/profiles/";
+ shares.push_back(share);
+
+ std::string strDirectory;
+ if (directory.empty())
+ strDirectory = share.strPath;
+ else
+ strDirectory = URIUtils::AddFileToFolder("special://masterprofile/", directory);
+
+ if (!CGUIDialogFileBrowser::ShowAndGetDirectory(shares, g_localizeStrings.Get(657), strDirectory, true))
+ return false;
+
+ directory = strDirectory;
+ if (!isDefault)
+ directory.erase(0, 24);
+
+ return true;
+}
+
+void CGUIDialogProfileSettings::UpdateProfileImage()
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(SETTING_PROFILE_IMAGE);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ SET_CONTROL_LABEL2(settingControl->GetID(), URIUtils::GetFileName(m_thumb));
+}
+
+void CGUIDialogProfileSettings::updateProfileDirectory()
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(SETTING_PROFILE_DIRECTORY);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ SET_CONTROL_LABEL2(settingControl->GetID(), m_directory);
+}
diff --git a/xbmc/profiles/dialogs/GUIDialogProfileSettings.h b/xbmc/profiles/dialogs/GUIDialogProfileSettings.h
new file mode 100644
index 0000000..5b6f14e
--- /dev/null
+++ b/xbmc/profiles/dialogs/GUIDialogProfileSettings.h
@@ -0,0 +1,62 @@
+/*
+ * 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 "profiles/Profile.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <string>
+
+class CGUIDialogProfileSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogProfileSettings();
+ ~CGUIDialogProfileSettings() override;
+
+ static bool ShowForProfile(unsigned int iProfile, bool firstLogin = false);
+
+protected:
+ // specializations of CGUIWindow
+ void OnWindowLoaded() override;
+
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override { return true; }
+ void OnCancel() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+ /*! \brief Prompt for a change in profile path
+ \param directory Current directory for the profile, new profile directory will be returned here
+ \param isDefault whether this is the default profile or not
+ \return true if the profile path has been changed, false otherwise.
+ */
+ static bool GetProfilePath(std::string &directory, bool isDefault);
+
+ void UpdateProfileImage();
+ void updateProfileDirectory();
+
+ bool m_needsSaving = false;
+ std::string m_name;
+ std::string m_thumb;
+ std::string m_directory;
+ int m_sourcesMode;
+ int m_dbMode;
+ bool m_isDefault;
+ bool m_isNewUser;
+ bool m_showDetails;
+
+ CProfile::CLock m_locks;
+};
diff --git a/xbmc/profiles/windows/CMakeLists.txt b/xbmc/profiles/windows/CMakeLists.txt
new file mode 100644
index 0000000..232d0aa
--- /dev/null
+++ b/xbmc/profiles/windows/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES GUIWindowSettingsProfile.cpp)
+
+set(HEADERS GUIWindowSettingsProfile.h)
+
+core_add_library(profiles_windows)
diff --git a/xbmc/profiles/windows/GUIWindowSettingsProfile.cpp b/xbmc/profiles/windows/GUIWindowSettingsProfile.cpp
new file mode 100644
index 0000000..05f1c09
--- /dev/null
+++ b/xbmc/profiles/windows/GUIWindowSettingsProfile.cpp
@@ -0,0 +1,269 @@
+/*
+ * 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 "GUIWindowSettingsProfile.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "messaging/ApplicationMessenger.h"
+#include "profiles/Profile.h"
+#include "profiles/ProfileManager.h"
+#include "profiles/dialogs/GUIDialogProfileSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "windows/GUIWindowFileManager.h"
+
+using namespace XFILE;
+
+#define CONTROL_PROFILES 2
+#define CONTROL_LOGINSCREEN 4
+#define CONTROL_AUTOLOGIN 5
+
+CGUIWindowSettingsProfile::CGUIWindowSettingsProfile(void)
+ : CGUIWindow(WINDOW_SETTINGS_PROFILES, "SettingsProfile.xml")
+{
+ m_listItems = new CFileItemList;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIWindowSettingsProfile::~CGUIWindowSettingsProfile(void)
+{
+ delete m_listItems;
+}
+
+int CGUIWindowSettingsProfile::GetSelectedItem()
+{
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_PROFILES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ return msg.GetParam1();
+}
+
+void CGUIWindowSettingsProfile::OnPopupMenu(int iItem)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (iItem == (int)profileManager->GetNumberOfProfiles())
+ return;
+
+ // popup the context menu
+ CContextButtons choices;
+ choices.Add(1, 20092); // Load profile
+ if (iItem > 0)
+ choices.Add(2, 117); // Delete
+
+ int choice = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (choice == 1)
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_LOADPROFILE, iItem);
+ return;
+ }
+
+ if (choice == 2)
+ {
+ if (profileManager->DeleteProfile(iItem))
+ iItem--;
+ }
+
+ LoadList();
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(),CONTROL_PROFILES,iItem);
+ OnMessage(msg);
+}
+
+bool CGUIWindowSettingsProfile::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ CGUIWindow::OnMessage(message);
+ ClearListItems();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_PROFILES)
+ {
+ int iAction = message.GetParam1();
+ if (
+ iAction == ACTION_SELECT_ITEM ||
+ iAction == ACTION_MOUSE_LEFT_CLICK ||
+ iAction == ACTION_CONTEXT_MENU ||
+ iAction == ACTION_MOUSE_RIGHT_CLICK
+ )
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_PROFILES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ int iItem = msg.GetParam1();
+ if (iAction == ACTION_CONTEXT_MENU || iAction == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ //contextmenu
+ if (iItem <= static_cast<int>(profileManager->GetNumberOfProfiles()) - 1)
+ {
+ OnPopupMenu(iItem);
+ }
+ return true;
+ }
+ else if (iItem < static_cast<int>(profileManager->GetNumberOfProfiles()))
+ {
+ if (CGUIDialogProfileSettings::ShowForProfile(iItem))
+ {
+ LoadList();
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), 2,iItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ return true;
+ }
+
+ return false;
+ }
+ else if (iItem > static_cast<int>(profileManager->GetNumberOfProfiles()) - 1)
+ {
+ CDirectory::Create(URIUtils::AddFileToFolder(profileManager->GetUserDataFolder(),"profiles"));
+ if (CGUIDialogProfileSettings::ShowForProfile(profileManager->GetNumberOfProfiles()))
+ {
+ LoadList();
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), 2,iItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ return true;
+ }
+
+ return false;
+ }
+ }
+ }
+ else if (iControl == CONTROL_LOGINSCREEN)
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ profileManager->ToggleLoginScreen();
+ profileManager->Save();
+ return true;
+ }
+ else if (iControl == CONTROL_AUTOLOGIN)
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ int currentId = profileManager->GetAutoLoginProfileId();
+ int profileId;
+ if (GetAutoLoginProfileChoice(profileId) && (currentId != profileId))
+ {
+ profileManager->SetAutoLoginProfileId(profileId);
+ profileManager->Save();
+ }
+ return true;
+ }
+ }
+ break;
+ }
+
+ return CGUIWindow::OnMessage(message);
+}
+
+void CGUIWindowSettingsProfile::LoadList()
+{
+ ClearListItems();
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ for (unsigned int i = 0; i < profileManager->GetNumberOfProfiles(); i++)
+ {
+ const CProfile *profile = profileManager->GetProfile(i);
+ CFileItemPtr item(new CFileItem(profile->getName()));
+ item->SetLabel2(profile->getDate());
+ item->SetArt("thumb", profile->getThumb());
+ item->SetOverlayImage(profile->getLockMode() == LOCK_MODE_EVERYONE ? CGUIListItem::ICON_OVERLAY_NONE : CGUIListItem::ICON_OVERLAY_LOCKED);
+ m_listItems->Add(item);
+ }
+ {
+ CFileItemPtr item(new CFileItem(g_localizeStrings.Get(20058)));
+ m_listItems->Add(item);
+ }
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_PROFILES, 0, 0, m_listItems);
+ OnMessage(msg);
+
+ if (profileManager->UsingLoginScreen())
+ {
+ CONTROL_SELECT(CONTROL_LOGINSCREEN);
+ }
+ else
+ {
+ CONTROL_DESELECT(CONTROL_LOGINSCREEN);
+ }
+}
+
+void CGUIWindowSettingsProfile::ClearListItems()
+{
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_PROFILES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ m_listItems->Clear();
+}
+
+void CGUIWindowSettingsProfile::OnInitWindow()
+{
+ LoadList();
+ CGUIWindow::OnInitWindow();
+}
+
+bool CGUIWindowSettingsProfile::GetAutoLoginProfileChoice(int &iProfile)
+{
+ CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (!dialog) return false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // add items
+ // "Last used profile" option comes first, so up indices by 1
+ int autoLoginProfileId = profileManager->GetAutoLoginProfileId() + 1;
+ CFileItemList items;
+ CFileItemPtr item(new CFileItem());
+ item->SetLabel(g_localizeStrings.Get(37014)); // Last used profile
+ item->SetArt("icon", "DefaultUser.png");
+ items.Add(item);
+
+ for (unsigned int i = 0; i < profileManager->GetNumberOfProfiles(); i++)
+ {
+ const CProfile *profile = profileManager->GetProfile(i);
+ const std::string& locked = g_localizeStrings.Get(profile->getLockMode() > 0 ? 20166 : 20165);
+ CFileItemPtr item(new CFileItem(profile->getName()));
+ item->SetLabel2(locked); // lock setting
+ std::string thumb = profile->getThumb();
+ if (thumb.empty())
+ thumb = "DefaultUser.png";
+ item->SetArt("icon", thumb);
+ items.Add(item);
+ }
+
+ dialog->SetHeading(CVariant{20093}); // Profile name
+ dialog->Reset();
+ dialog->SetUseDetails(true);
+ dialog->SetItems(items);
+ dialog->SetSelected(autoLoginProfileId);
+ dialog->Open();
+
+ if (dialog->IsButtonPressed() || dialog->GetSelectedItem() < 0)
+ return false; // user cancelled
+ iProfile = dialog->GetSelectedItem() - 1;
+
+ return true;
+}
diff --git a/xbmc/profiles/windows/GUIWindowSettingsProfile.h b/xbmc/profiles/windows/GUIWindowSettingsProfile.h
new file mode 100644
index 0000000..0057305
--- /dev/null
+++ b/xbmc/profiles/windows/GUIWindowSettingsProfile.h
@@ -0,0 +1,35 @@
+/*
+ * 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 "guilib/GUIWindow.h"
+
+class CFileItemList;
+
+class CGUIWindowSettingsProfile :
+ public CGUIWindow
+{
+public:
+ CGUIWindowSettingsProfile(void);
+ ~CGUIWindowSettingsProfile(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+
+protected:
+ void OnInitWindow() override;
+ CFileItemList *m_listItems;
+
+ void OnPopupMenu(int iItem);
+ void DoRename(int iItem);
+ void DoOverwrite(int iItem);
+ int GetSelectedItem();
+ void LoadList();
+ void SetLastLoaded();
+ void ClearListItems();
+ bool GetAutoLoginProfileChoice(int &iProfile);
+};
diff --git a/xbmc/programs/CMakeLists.txt b/xbmc/programs/CMakeLists.txt
new file mode 100644
index 0000000..58ac8de
--- /dev/null
+++ b/xbmc/programs/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES GUIViewStatePrograms.cpp
+ GUIWindowPrograms.cpp)
+
+set(HEADERS GUIViewStatePrograms.h
+ GUIWindowPrograms.h)
+
+core_add_library(programs)
diff --git a/xbmc/programs/GUIViewStatePrograms.cpp b/xbmc/programs/GUIViewStatePrograms.cpp
new file mode 100644
index 0000000..05e4cfd
--- /dev/null
+++ b/xbmc/programs/GUIViewStatePrograms.cpp
@@ -0,0 +1,72 @@
+/*
+ * 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 "GUIViewStatePrograms.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "filesystem/Directory.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/TextureManager.h"
+#include "guilib/WindowIDs.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "view/ViewState.h"
+#include "view/ViewStateSettings.h"
+
+using namespace XFILE;
+
+CGUIViewStateWindowPrograms::CGUIViewStateWindowPrograms(const CFileItemList& items) : CGUIViewState(items)
+{
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS("%K", "%I", "%L", ""), // Title, Size | Foldername, empty
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("programs");
+ SetSortMethod(viewState->m_sortDescription);
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+
+ LoadViewState(items.GetPath(), WINDOW_PROGRAMS);
+}
+
+void CGUIViewStateWindowPrograms::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_PROGRAMS, CViewStateSettings::GetInstance().Get("programs"));
+}
+
+std::string CGUIViewStateWindowPrograms::GetLockType()
+{
+ return "programs";
+}
+
+std::string CGUIViewStateWindowPrograms::GetExtensions()
+{
+ return ".cut";
+}
+
+VECSOURCES& CGUIViewStateWindowPrograms::GetSources()
+{
+#if defined(TARGET_ANDROID)
+ {
+ CMediaSource source;
+ source.strPath = "androidapp://sources/apps/";
+ source.strName = g_localizeStrings.Get(20244);
+ if (CServiceBroker::GetGUI()->GetTextureManager().HasTexture("DefaultProgram.png"))
+ source.m_strThumbnailImage = "DefaultProgram.png";
+ source.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ source.m_ignore = true;
+ m_sources.emplace_back(std::move(source));
+ }
+#endif
+
+ VECSOURCES *programSources = CMediaSourceSettings::GetInstance().GetSources("programs");
+ AddOrReplace(*programSources, CGUIViewState::GetSources());
+ return *programSources;
+}
+
diff --git a/xbmc/programs/GUIViewStatePrograms.h b/xbmc/programs/GUIViewStatePrograms.h
new file mode 100644
index 0000000..3892888
--- /dev/null
+++ b/xbmc/programs/GUIViewStatePrograms.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "view/GUIViewState.h"
+
+class CGUIViewStateWindowPrograms : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateWindowPrograms(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ std::string GetLockType() override;
+ std::string GetExtensions() override;
+ VECSOURCES& GetSources() override;
+};
+
diff --git a/xbmc/programs/GUIWindowPrograms.cpp b/xbmc/programs/GUIWindowPrograms.cpp
new file mode 100644
index 0000000..1e79c34
--- /dev/null
+++ b/xbmc/programs/GUIWindowPrograms.cpp
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2005-2020 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 "GUIWindowPrograms.h"
+
+#include "Autorun.h"
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "addons/gui/GUIDialogAddonInfo.h"
+#include "dialogs/GUIDialogMediaSource.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/Key.h"
+#include "media/MediaLockState.h"
+#include "settings/MediaSourceSettings.h"
+#include "utils/StringUtils.h"
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_LABELFILES 12
+
+CGUIWindowPrograms::CGUIWindowPrograms(void)
+ : CGUIMediaWindow(WINDOW_PROGRAMS, "MyPrograms.xml")
+{
+ m_thumbLoader.SetObserver(this);
+ m_dlgProgress = NULL;
+ m_rootDir.AllowNonLocalSources(false); // no nonlocal shares for this window please
+}
+
+
+CGUIWindowPrograms::~CGUIWindowPrograms(void) = default;
+
+bool CGUIWindowPrograms::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+
+ // is this the first time accessing this window?
+ if (m_vecItems->GetPath() == "?" && message.GetStringParam().empty())
+ message.SetStringParam(CMediaSourceSettings::GetInstance().GetDefaultSource("programs"));
+
+ return CGUIMediaWindow::OnMessage(message);
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ if (m_viewControl.HasControl(message.GetSenderId())) // list/thumb control
+ {
+ int iAction = message.GetParam1();
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iAction == ACTION_PLAYER_PLAY)
+ {
+ OnPlayMedia(iItem);
+ return true;
+ }
+ else if (iAction == ACTION_SHOW_INFO)
+ {
+ OnItemInfo(iItem);
+ return true;
+ }
+ }
+ }
+ break;
+ }
+
+ return CGUIMediaWindow::OnMessage(message);
+}
+
+void CGUIWindowPrograms::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return;
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+ if (item)
+ {
+ if ( m_vecItems->IsVirtualDirectoryRoot() || m_vecItems->GetPath() == "sources://programs/" )
+ {
+ CGUIDialogContextMenu::GetContextButtons("programs", item, buttons);
+ }
+ }
+ CGUIMediaWindow::GetContextButtons(itemNumber, buttons);
+}
+
+bool CGUIWindowPrograms::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ CFileItemPtr item = (itemNumber >= 0 && itemNumber < m_vecItems->Size()) ? m_vecItems->Get(itemNumber) : CFileItemPtr();
+
+ if (CGUIDialogContextMenu::OnContextButton("programs", item, button))
+ {
+ Update("");
+ return true;
+ }
+ return CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowPrograms::OnAddMediaSource()
+{
+ return CGUIDialogMediaSource::ShowAndAddMediaSource("programs");
+}
+
+bool CGUIWindowPrograms::Update(const std::string &strDirectory, bool updateFilterPath /* = true */)
+{
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ if (!CGUIMediaWindow::Update(strDirectory, updateFilterPath))
+ return false;
+
+ m_thumbLoader.Load(*m_vecItems);
+ return true;
+}
+
+bool CGUIWindowPrograms::OnPlayMedia(int iItem, const std::string&)
+{
+ if ( iItem < 0 || iItem >= m_vecItems->Size() ) return false;
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+
+#ifdef HAS_DVD_DRIVE
+ if (pItem->IsDVD())
+ return MEDIA_DETECT::CAutorun::PlayDiscAskResume(m_vecItems->Get(iItem)->GetPath());
+#endif
+
+ if (pItem->m_bIsFolder) return false;
+
+ return false;
+}
+
+std::string CGUIWindowPrograms::GetStartFolder(const std::string &dir)
+{
+ std::string lower(dir); StringUtils::ToLower(lower);
+ if (lower == "plugins" || lower == "addons")
+ return "addons://sources/executable/";
+ else if (lower == "androidapps")
+ return "androidapp://sources/apps/";
+
+ SetupShares();
+ VECSOURCES shares;
+ m_rootDir.GetSources(shares);
+ bool bIsSourceName = false;
+ int iIndex = CUtil::GetMatchingSource(dir, shares, bIsSourceName);
+ if (iIndex > -1)
+ {
+ if (iIndex < static_cast<int>(shares.size()) && shares[iIndex].m_iHasLock == LOCK_STATE_LOCKED)
+ {
+ CFileItem item(shares[iIndex]);
+ if (!g_passwordManager.IsItemUnlocked(&item,"programs"))
+ return "";
+ }
+ if (bIsSourceName)
+ return shares[iIndex].strPath;
+ return dir;
+ }
+ return CGUIMediaWindow::GetStartFolder(dir);
+}
+
+void CGUIWindowPrograms::OnItemInfo(int iItem)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return;
+
+ CFileItemPtr item = m_vecItems->Get(iItem);
+ if (!m_vecItems->IsPlugin() && (item->IsPlugin() || item->IsScript()))
+ {
+ CGUIDialogAddonInfo::ShowForItem(item);
+ }
+}
diff --git a/xbmc/programs/GUIWindowPrograms.h b/xbmc/programs/GUIWindowPrograms.h
new file mode 100644
index 0000000..21156de
--- /dev/null
+++ b/xbmc/programs/GUIWindowPrograms.h
@@ -0,0 +1,35 @@
+/*
+ * 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 "ThumbLoader.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "windows/GUIMediaWindow.h"
+
+class CGUIWindowPrograms :
+ public CGUIMediaWindow, public IBackgroundLoaderObserver
+{
+public:
+ CGUIWindowPrograms(void);
+ ~CGUIWindowPrograms(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ virtual void OnItemInfo(int iItem);
+protected:
+ void OnItemLoaded(CFileItem* pItem) override {};
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ bool OnPlayMedia(int iItem, const std::string& = "") override;
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool OnAddMediaSource() override;
+ std::string GetStartFolder(const std::string &dir) override;
+
+ CGUIDialogProgress* m_dlgProgress;
+
+ CProgramThumbLoader m_thumbLoader;
+};
diff --git a/xbmc/pvr/CMakeLists.txt b/xbmc/pvr/CMakeLists.txt
new file mode 100644
index 0000000..e7a95a6
--- /dev/null
+++ b/xbmc/pvr/CMakeLists.txt
@@ -0,0 +1,30 @@
+set(SOURCES PVRCachedImage.cpp
+ PVRCachedImages.cpp
+ PVRChannelNumberInputHandler.cpp
+ PVRComponentRegistration.cpp
+ PVRContextMenus.cpp
+ PVRDatabase.cpp
+ PVREdl.cpp
+ PVREventLogJob.cpp
+ PVRItem.cpp
+ PVRManager.cpp
+ PVRPlaybackState.cpp
+ PVRStreamProperties.cpp
+ PVRThumbLoader.cpp)
+
+set(HEADERS IPVRComponent.h
+ PVRCachedImage.h
+ PVRCachedImages.h
+ PVRChannelNumberInputHandler.h
+ PVRComponentRegistration.h
+ PVRContextMenus.h
+ PVRDatabase.h
+ PVREdl.h
+ PVREventLogJob.h
+ PVRItem.h
+ PVRManager.h
+ PVRPlaybackState.h
+ PVRStreamProperties.h
+ PVRThumbLoader.h)
+
+core_add_library(pvr)
diff --git a/xbmc/pvr/IPVRComponent.h b/xbmc/pvr/IPVRComponent.h
new file mode 100644
index 0000000..8695095
--- /dev/null
+++ b/xbmc/pvr/IPVRComponent.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2022 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
+
+namespace PVR
+{
+class IPVRComponent
+{
+public:
+ virtual ~IPVRComponent() = default;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/PVRCachedImage.cpp b/xbmc/pvr/PVRCachedImage.cpp
new file mode 100644
index 0000000..e336563
--- /dev/null
+++ b/xbmc/pvr/PVRCachedImage.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2005-2021 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 "PVRCachedImage.h"
+
+#include "TextureDatabase.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+using namespace PVR;
+
+CPVRCachedImage::CPVRCachedImage(const std::string& owner) : m_owner(owner)
+{
+}
+
+CPVRCachedImage::CPVRCachedImage(const std::string& clientImage, const std::string& owner)
+ : m_clientImage(clientImage), m_owner(owner)
+{
+ UpdateLocalImage();
+}
+
+bool CPVRCachedImage::operator==(const CPVRCachedImage& right) const
+{
+ return (this == &right) || (m_clientImage == right.m_clientImage &&
+ m_localImage == right.m_localImage && m_owner == right.m_owner);
+}
+
+bool CPVRCachedImage::operator!=(const CPVRCachedImage& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRCachedImage::SetClientImage(const std::string& image)
+{
+ if (StringUtils::StartsWith(image, "image://"))
+ {
+ CLog::LogF(LOGERROR, "Not allowed to call this method with an image URL");
+ return;
+ }
+
+ if (m_owner.empty())
+ {
+ CLog::LogF(LOGERROR, "Empty owner");
+ return;
+ }
+
+ m_clientImage = image;
+ UpdateLocalImage();
+}
+
+void CPVRCachedImage::SetOwner(const std::string& owner)
+{
+ if (m_owner != owner)
+ {
+ m_owner = owner;
+ UpdateLocalImage();
+ }
+}
+
+void CPVRCachedImage::UpdateLocalImage()
+{
+ if (m_clientImage.empty())
+ m_localImage.clear();
+ else
+ m_localImage = CTextureUtils::GetWrappedImageURL(m_clientImage, m_owner);
+}
diff --git a/xbmc/pvr/PVRCachedImage.h b/xbmc/pvr/PVRCachedImage.h
new file mode 100644
index 0000000..c8f0210
--- /dev/null
+++ b/xbmc/pvr/PVRCachedImage.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2005-2021 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 <string>
+
+namespace PVR
+{
+
+class CPVRCachedImage
+{
+public:
+ CPVRCachedImage() = delete;
+ virtual ~CPVRCachedImage() = default;
+
+ explicit CPVRCachedImage(const std::string& owner);
+ CPVRCachedImage(const std::string& clientImage, const std::string& owner);
+
+ bool operator==(const CPVRCachedImage& right) const;
+ bool operator!=(const CPVRCachedImage& right) const;
+
+ const std::string& GetClientImage() const { return m_clientImage; }
+ const std::string& GetLocalImage() const { return m_localImage; }
+
+ void SetClientImage(const std::string& image);
+
+ void SetOwner(const std::string& owner);
+
+private:
+ void UpdateLocalImage();
+
+ std::string m_clientImage;
+ std::string m_localImage;
+ std::string m_owner;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRCachedImages.cpp b/xbmc/pvr/PVRCachedImages.cpp
new file mode 100644
index 0000000..ea17fd7
--- /dev/null
+++ b/xbmc/pvr/PVRCachedImages.cpp
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2005-2021 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 "PVRCachedImages.h"
+
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "TextureDatabase.h"
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace PVR;
+
+int CPVRCachedImages::Cleanup(const std::vector<PVRImagePattern>& urlPatterns,
+ const std::vector<std::string>& urlsToCheck,
+ bool clearTextureForPath /* = false */)
+{
+ int iCleanedImages = 0;
+
+ if (urlPatterns.empty())
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "No URL patterns given");
+ return iCleanedImages;
+ }
+
+ CTextureDatabase db;
+ if (!db.Open())
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "Failed to open texture database");
+ return iCleanedImages;
+ }
+
+ CDatabase::Filter filter;
+
+ for (const auto& pattern : urlPatterns)
+ {
+ const std::string encodedPattern =
+ StringUtils::Format("{}@{}", pattern.owner, CURL::Encode(pattern.path));
+
+ std::string escapedPattern;
+ for (size_t i = 0; i < encodedPattern.size(); ++i)
+ {
+ if (encodedPattern[i] == '%' || encodedPattern[i] == '^')
+ escapedPattern += '^';
+
+ escapedPattern += encodedPattern[i];
+ }
+
+ const std::string where =
+ StringUtils::Format("url LIKE 'image://{}%' ESCAPE '^'", escapedPattern);
+ filter.AppendWhere(where, false); // logical OR
+ }
+
+ CVariant items;
+ if (!db.GetTextures(items, filter))
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "Failed to get items from texture database");
+ return iCleanedImages;
+ }
+
+ for (unsigned int i = 0; i < items.size(); ++i)
+ {
+ // Unwrap the image:// URL returned from texture db.
+ const std::string textureURL = UnwrapImageURL(items[i]["url"].asString());
+
+ if (std::none_of(urlsToCheck.cbegin(), urlsToCheck.cend(),
+ [&textureURL](const std::string& url) { return url == textureURL; }))
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Removing stale cached image: '{}'", textureURL);
+ CServiceBroker::GetTextureCache()->ClearCachedImage(items[i]["textureid"].asInteger());
+
+ if (clearTextureForPath)
+ db.ClearTextureForPath(textureURL, "thumb");
+
+ iCleanedImages++;
+ }
+ }
+
+ return iCleanedImages;
+}
+
+std::string CPVRCachedImages::UnwrapImageURL(const std::string& url)
+{
+ return StringUtils::StartsWith(url, "image://") ? CURL(url).GetHostName() : url;
+}
diff --git a/xbmc/pvr/PVRCachedImages.h b/xbmc/pvr/PVRCachedImages.h
new file mode 100644
index 0000000..9dcdd24
--- /dev/null
+++ b/xbmc/pvr/PVRCachedImages.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2005-2021 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 <string>
+#include <vector>
+
+namespace PVR
+{
+
+struct PVRImagePattern
+{
+ PVRImagePattern(const std::string& _owner, const std::string& _path) : owner(_owner), path(_path)
+ {
+ }
+
+ std::string owner;
+ std::string path;
+};
+
+class CPVRCachedImages
+{
+public:
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @param urlPatterns The URL patterns to fetch from texture database.
+ * @param urlsToCheck The URLs to check for still being present in the texture db.
+ * @param clearTextureForPath Whether to clear the path in texture database.
+ * @return number of cleaned up images.
+ */
+ static int Cleanup(const std::vector<PVRImagePattern>& urlPatterns,
+ const std::vector<std::string>& urlsToCheck,
+ bool clearTextureForPath = false);
+
+ /*!
+ * @brief Extract the wrapped URL from an image URL.
+ * @param url The URL to unwrap.
+ * @return The unwrapped URL if url is an image URL, url otherwise.
+ */
+ static std::string UnwrapImageURL(const std::string& url);
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRChannelNumberInputHandler.cpp b/xbmc/pvr/PVRChannelNumberInputHandler.cpp
new file mode 100644
index 0000000..a57c774
--- /dev/null
+++ b/xbmc/pvr/PVRChannelNumberInputHandler.cpp
@@ -0,0 +1,190 @@
+/*
+ * 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 "PVRChannelNumberInputHandler.h"
+
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <mutex>
+#include <string>
+
+using namespace PVR;
+using namespace std::chrono_literals;
+
+CPVRChannelNumberInputHandler::CPVRChannelNumberInputHandler()
+ : CPVRChannelNumberInputHandler(CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iPVRNumericChannelSwitchTimeout,
+ CHANNEL_NUMBER_INPUT_MAX_DIGITS)
+{
+}
+
+CPVRChannelNumberInputHandler::CPVRChannelNumberInputHandler(
+ int iDelay, int iMaxDigits /* = CHANNEL_NUMBER_INPUT_MAX_DIGITS */)
+ : m_iDelay(iDelay), m_iMaxDigits(iMaxDigits), m_timer(this)
+{
+}
+
+void CPVRChannelNumberInputHandler::OnTimeout()
+{
+ if (m_inputBuffer.empty())
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ SetLabel("");
+ }
+ else
+ {
+ // call the overridden worker method
+ OnInputDone();
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ // erase input buffer immediately , but...
+ m_inputBuffer.erase();
+
+ // ... display the label for another .5 secs if we stopped the timer before regular timeout.
+ if (m_timer.IsRunning())
+ SetLabel("");
+ else
+ m_timer.Start(500ms);
+ }
+}
+
+void CPVRChannelNumberInputHandler::ExecuteAction()
+{
+ m_timer.Stop(true /* wait until worker thread ended */);
+ OnTimeout();
+}
+
+bool CPVRChannelNumberInputHandler::CheckInputAndExecuteAction()
+{
+ const CPVRChannelNumber channelNumber = GetChannelNumber();
+ if (channelNumber.IsValid())
+ {
+ // we have a valid channel number; execute the associated action now.
+ ExecuteAction();
+ return true;
+ }
+ return false;
+}
+
+void CPVRChannelNumberInputHandler::AppendChannelNumberCharacter(char cCharacter)
+{
+ if (cCharacter != CPVRChannelNumber::SEPARATOR && (cCharacter < '0' || cCharacter > '9'))
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (cCharacter == CPVRChannelNumber::SEPARATOR)
+ {
+ // no leading separator
+ if (m_inputBuffer.empty())
+ return;
+
+ // max one separator
+ if (m_inputBuffer.find(CPVRChannelNumber::SEPARATOR) != std::string::npos)
+ return;
+ }
+
+ if (m_inputBuffer.size() == static_cast<size_t>(m_iMaxDigits))
+ {
+ m_inputBuffer.erase(m_inputBuffer.begin());
+ SetLabel(m_inputBuffer);
+ }
+ else if (m_inputBuffer.empty())
+ {
+ m_sortedChannelNumbers.clear();
+ GetChannelNumbers(m_sortedChannelNumbers);
+
+ std::sort(m_sortedChannelNumbers.begin(), m_sortedChannelNumbers.end());
+ }
+
+ m_inputBuffer.append(&cCharacter, 1);
+ SetLabel(m_inputBuffer);
+
+ for (auto it = m_sortedChannelNumbers.begin(); it != m_sortedChannelNumbers.end();)
+ {
+ const std::string channel = *it;
+ ++it;
+
+ if (StringUtils::StartsWith(channel, m_inputBuffer))
+ {
+ if (it != m_sortedChannelNumbers.end() && StringUtils::StartsWith(*it, m_inputBuffer))
+ {
+ // there are alternative numbers; wait for more input
+ break;
+ }
+
+ // no alternatives; complete the number and fire immediately
+ m_inputBuffer = channel;
+ SetLabel(m_inputBuffer);
+ ExecuteAction();
+ return;
+ }
+ }
+
+ if (!m_timer.IsRunning())
+ m_timer.Start(std::chrono::milliseconds(m_iDelay));
+ else
+ m_timer.Restart();
+}
+
+CPVRChannelNumber CPVRChannelNumberInputHandler::GetChannelNumber() const
+{
+ int iChannelNumber = 0;
+ int iSubChannelNumber = 0;
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ size_t pos = m_inputBuffer.find(CPVRChannelNumber::SEPARATOR);
+ if (pos != std::string::npos)
+ {
+ // main + sub
+ if (pos != 0)
+ {
+ iChannelNumber = std::atoi(m_inputBuffer.substr(0, pos).c_str());
+ if (pos != m_inputBuffer.size() - 1)
+ iSubChannelNumber = std::atoi(m_inputBuffer.substr(pos + 1).c_str());
+ }
+ }
+ else
+ {
+ // only main
+ iChannelNumber = std::atoi(m_inputBuffer.c_str());
+ }
+
+ return CPVRChannelNumber(iChannelNumber, iSubChannelNumber);
+}
+
+bool CPVRChannelNumberInputHandler::HasChannelNumber() const
+{
+ return !m_inputBuffer.empty();
+}
+
+std::string CPVRChannelNumberInputHandler::GetChannelNumberLabel() const
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ return m_label;
+}
+
+void CPVRChannelNumberInputHandler::SetLabel(const std::string& label)
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ if (label != m_label)
+ {
+ m_label = label;
+
+ // inform subscribers
+ m_events.Publish(PVRChannelNumberInputChangedEvent(m_label));
+ }
+}
diff --git a/xbmc/pvr/PVRChannelNumberInputHandler.h b/xbmc/pvr/PVRChannelNumberInputHandler.h
new file mode 100644
index 0000000..efa28ca
--- /dev/null
+++ b/xbmc/pvr/PVRChannelNumberInputHandler.h
@@ -0,0 +1,118 @@
+/*
+ * 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 "pvr/channels/PVRChannelNumber.h"
+#include "threads/CriticalSection.h"
+#include "threads/Timer.h"
+#include "utils/EventStream.h"
+
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+struct PVRChannelNumberInputChangedEvent
+{
+ explicit PVRChannelNumberInputChangedEvent(const std::string& input) : m_input(input) {}
+ virtual ~PVRChannelNumberInputChangedEvent() = default;
+
+ std::string m_input;
+};
+
+class CPVRChannelNumberInputHandler : private ITimerCallback
+{
+public:
+ static const int CHANNEL_NUMBER_INPUT_MAX_DIGITS = 5;
+
+ CPVRChannelNumberInputHandler();
+
+ /*!
+ * @brief ctor.
+ * @param iDelay timer delay in millisecods.
+ * @param iMaxDigits maximum number of display digits to use.
+ */
+ CPVRChannelNumberInputHandler(int iDelay, int iMaxDigits = CHANNEL_NUMBER_INPUT_MAX_DIGITS);
+
+ ~CPVRChannelNumberInputHandler() override = default;
+
+ /*!
+ * @brief Get the events available for CEventStream.
+ * @return The events.
+ */
+ CEventStream<PVRChannelNumberInputChangedEvent>& Events() { return m_events; }
+
+ // implementation of ITimerCallback
+ void OnTimeout() override;
+
+ /*!
+ * @brief Get the currently available channel numbers.
+ * @param channelNumbers The list to fill with the channel numbers.
+ */
+ virtual void GetChannelNumbers(std::vector<std::string>& channelNumbers) = 0;
+
+ /*!
+ * @brief This method gets called after the channel number input timer has expired.
+ */
+ virtual void OnInputDone() = 0;
+
+ /*!
+ * @brief Appends a channel number character.
+ * @param cCharacter The character to append. value must be CPVRChannelNumber::SEPARATOR ('.') or any char in the range from '0' to '9'.
+ */
+ virtual void AppendChannelNumberCharacter(char cCharacter);
+
+ /*!
+ * @brief Check whether a channel number was entered.
+ * @return True if the handler currently holds a channel number, false otherwise.
+ */
+ bool HasChannelNumber() const;
+
+ /*!
+ * @brief Get the currently entered channel number as a formatted string.
+ * @return the channel number string.
+ */
+ std::string GetChannelNumberLabel() const;
+
+ /*!
+ * @brief If a number was entered, execute the associated action.
+ * @return True, if the action was executed, false otherwise.
+ */
+ bool CheckInputAndExecuteAction();
+
+protected:
+ /*!
+ * @brief Get the currently entered channel number.
+ * @return the channel number.
+ */
+ CPVRChannelNumber GetChannelNumber() const;
+
+ /*!
+ * @brief Get the currently entered number of digits.
+ * @return the number of digits.
+ */
+ size_t GetCurrentDigitCount() const { return m_inputBuffer.size(); }
+
+ mutable CCriticalSection m_mutex;
+
+private:
+ void ExecuteAction();
+
+ void SetLabel(const std::string& label);
+
+ std::vector<std::string> m_sortedChannelNumbers;
+ const int m_iDelay;
+ const int m_iMaxDigits;
+ std::string m_inputBuffer;
+ std::string m_label;
+ CTimer m_timer;
+ CEventSource<PVRChannelNumberInputChangedEvent> m_events;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRComponentRegistration.cpp b/xbmc/pvr/PVRComponentRegistration.cpp
new file mode 100644
index 0000000..7532283
--- /dev/null
+++ b/xbmc/pvr/PVRComponentRegistration.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 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 "PVRComponentRegistration.h"
+
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsClients.h"
+#include "pvr/guilib/PVRGUIActionsDatabase.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsPowerManagement.h"
+#include "pvr/guilib/PVRGUIActionsRecordings.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/guilib/PVRGUIActionsUtils.h"
+
+#include <memory>
+
+using namespace PVR;
+
+CPVRComponentRegistration::CPVRComponentRegistration()
+{
+ RegisterComponent(std::make_shared<CPVRGUIActionsChannels>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsClients>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsDatabase>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsEPG>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsParentalControl>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsPlayback>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsPowerManagement>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsRecordings>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsTimers>());
+ RegisterComponent(std::make_shared<CPVRGUIActionsUtils>());
+}
+
+CPVRComponentRegistration::~CPVRComponentRegistration()
+{
+ DeregisterComponent(typeid(CPVRGUIActionsUtils));
+ DeregisterComponent(typeid(CPVRGUIActionsTimers));
+ DeregisterComponent(typeid(CPVRGUIActionsRecordings));
+ DeregisterComponent(typeid(CPVRGUIActionsPowerManagement));
+ DeregisterComponent(typeid(CPVRGUIActionsPlayback));
+ DeregisterComponent(typeid(CPVRGUIActionsParentalControl));
+ DeregisterComponent(typeid(CPVRGUIActionsEPG));
+ DeregisterComponent(typeid(CPVRGUIActionsDatabase));
+ DeregisterComponent(typeid(CPVRGUIActionsClients));
+ DeregisterComponent(typeid(CPVRGUIActionsChannels));
+}
diff --git a/xbmc/pvr/PVRComponentRegistration.h b/xbmc/pvr/PVRComponentRegistration.h
new file mode 100644
index 0000000..d81d449
--- /dev/null
+++ b/xbmc/pvr/PVRComponentRegistration.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 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 "pvr/IPVRComponent.h"
+#include "utils/ComponentContainer.h"
+
+namespace PVR
+{
+class CPVRComponentRegistration : public CComponentContainer<IPVRComponent>
+{
+public:
+ CPVRComponentRegistration();
+ virtual ~CPVRComponentRegistration();
+};
+} // namespace PVR
diff --git a/xbmc/pvr/PVRContextMenus.cpp b/xbmc/pvr/PVRContextMenus.cpp
new file mode 100644
index 0000000..724872d
--- /dev/null
+++ b/xbmc/pvr/PVRContextMenus.cpp
@@ -0,0 +1,780 @@
+/*
+ * Copyright (C) 2016-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 "PVRContextMenus.h"
+
+#include "ContextMenuItem.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClientMenuHooks.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsRecordings.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "utils/URIUtils.h"
+
+#include <memory>
+#include <string>
+
+namespace PVR
+{
+namespace CONTEXTMENUITEM
+{
+#define DECL_STATICCONTEXTMENUITEM(clazz) \
+ class clazz : public CStaticContextMenuAction \
+ { \
+ public: \
+ explicit clazz(uint32_t label) : CStaticContextMenuAction(label) {} \
+ bool IsVisible(const CFileItem& item) const override; \
+ bool Execute(const CFileItemPtr& item) const override; \
+ };
+
+#define DECL_CONTEXTMENUITEM(clazz) \
+ class clazz : public IContextMenuItem \
+ { \
+ public: \
+ std::string GetLabel(const CFileItem& item) const override; \
+ bool IsVisible(const CFileItem& item) const override; \
+ bool Execute(const CFileItemPtr& item) const override; \
+ };
+
+DECL_STATICCONTEXTMENUITEM(PlayEpgTag);
+DECL_STATICCONTEXTMENUITEM(PlayRecording);
+DECL_CONTEXTMENUITEM(ShowInformation);
+DECL_STATICCONTEXTMENUITEM(ShowChannelGuide);
+DECL_STATICCONTEXTMENUITEM(FindSimilar);
+DECL_STATICCONTEXTMENUITEM(StartRecording);
+DECL_STATICCONTEXTMENUITEM(StopRecording);
+DECL_STATICCONTEXTMENUITEM(AddTimerRule);
+DECL_CONTEXTMENUITEM(EditTimerRule);
+DECL_STATICCONTEXTMENUITEM(DeleteTimerRule);
+DECL_CONTEXTMENUITEM(EditTimer);
+DECL_CONTEXTMENUITEM(DeleteTimer);
+DECL_STATICCONTEXTMENUITEM(EditRecording);
+DECL_CONTEXTMENUITEM(DeleteRecording);
+DECL_STATICCONTEXTMENUITEM(UndeleteRecording);
+DECL_STATICCONTEXTMENUITEM(DeleteWatchedRecordings);
+DECL_CONTEXTMENUITEM(ToggleTimerState);
+DECL_STATICCONTEXTMENUITEM(AddReminder);
+DECL_STATICCONTEXTMENUITEM(ExecuteSearch);
+DECL_STATICCONTEXTMENUITEM(EditSearch);
+DECL_STATICCONTEXTMENUITEM(RenameSearch);
+DECL_STATICCONTEXTMENUITEM(DeleteSearch);
+
+class PVRClientMenuHook : public IContextMenuItem
+{
+public:
+ explicit PVRClientMenuHook(const CPVRClientMenuHook& hook) : m_hook(hook) {}
+
+ std::string GetLabel(const CFileItem& item) const override;
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const CFileItemPtr& item) const override;
+
+ const CPVRClientMenuHook& GetHook() const { return m_hook; }
+
+private:
+ const CPVRClientMenuHook m_hook;
+};
+
+std::shared_ptr<CPVRTimerInfoTag> GetTimerInfoTagFromItem(const CFileItem& item)
+{
+ std::shared_ptr<CPVRTimerInfoTag> timer;
+
+ const std::shared_ptr<CPVREpgInfoTag> epg(item.GetEPGInfoTag());
+ if (epg)
+ timer = CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg);
+
+ if (!timer)
+ timer = item.GetPVRTimerInfoTag();
+
+ return timer;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Play epg tag
+
+bool PlayEpgTag::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epg(item.GetEPGInfoTag());
+ if (epg)
+ return epg->IsPlayable();
+
+ return false;
+}
+
+bool PlayEpgTag::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayEpgTag(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Play recording
+
+bool PlayRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording =
+ CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(item.GetEPGInfoTag());
+ if (recording)
+ return !recording->IsDeleted();
+
+ return false;
+}
+
+bool PlayRecording::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *item, true /* bCheckResume */);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Show information (epg, recording)
+
+std::string ShowInformation::GetLabel(const CFileItem& item) const
+{
+ if (item.GetPVRRecordingInfoTag())
+ return g_localizeStrings.Get(19053); /* Recording Information */
+
+ return g_localizeStrings.Get(19047); /* Programme information */
+}
+
+bool ShowInformation::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel(item.GetPVRChannelInfoTag());
+ if (channel)
+ return channel->GetEPGNow().get() != nullptr;
+
+ if (item.HasEPGInfoTag())
+ return !item.GetEPGInfoTag()->IsGapTag();
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag());
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ return timer->GetEpgInfoTag().get() != nullptr;
+
+ if (item.GetPVRRecordingInfoTag())
+ return true;
+
+ return false;
+}
+
+bool ShowInformation::Execute(const CFileItemPtr& item) const
+{
+ if (item->GetPVRRecordingInfoTag())
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(*item);
+
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Show channel guide
+
+bool ShowChannelGuide::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel(item.GetPVRChannelInfoTag());
+ if (channel)
+ return channel->GetEPGNow().get() != nullptr;
+
+ return false;
+}
+
+bool ShowChannelGuide::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowChannelEPG(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Find similar
+
+bool FindSimilar::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel(item.GetPVRChannelInfoTag());
+ if (channel)
+ return channel->GetEPGNow().get() != nullptr;
+
+ if (item.HasEPGInfoTag())
+ return !item.GetEPGInfoTag()->IsGapTag();
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag());
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ return timer->GetEpgInfoTag().get() != nullptr;
+
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording)
+ return !recording->IsDeleted();
+
+ return false;
+}
+
+bool FindSimilar::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().FindSimilar(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Start recording
+
+bool StartRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(item);
+
+ std::shared_ptr<CPVRChannel> channel = item.GetPVRChannelInfoTag();
+ if (channel)
+ return client && client->GetClientCapabilities().SupportsTimers() &&
+ !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel);
+
+ const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag();
+ if (epg && epg->IsRecordable())
+ {
+ if (epg->IsGapTag())
+ {
+ channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epg);
+ if (channel)
+ {
+ return client && client->GetClientCapabilities().SupportsTimers() &&
+ !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel);
+ }
+ }
+ else
+ {
+ return client && client->GetClientCapabilities().SupportsTimers() &&
+ !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg);
+ }
+ }
+ return false;
+}
+
+bool StartRecording::Execute(const CFileItemPtr& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = item->GetEPGInfoTag();
+ if (!epgTag || epgTag->IsActive())
+ {
+ // instant recording
+ std::shared_ptr<CPVRChannel> channel;
+ if (epgTag)
+ channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epgTag);
+
+ if (!channel)
+ channel = item->GetPVRChannelInfoTag();
+
+ if (channel)
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().SetRecordingOnChannel(channel,
+ true);
+ }
+
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimer(*item, false);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Stop recording
+
+bool StopRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording && recording->IsInProgress())
+ return true;
+
+ std::shared_ptr<CPVRChannel> channel = item.GetPVRChannelInfoTag();
+ if (channel)
+ return CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel);
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ return timer->IsRecording();
+
+ const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag();
+ if (epg && epg->IsGapTag())
+ {
+ channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epg);
+ if (channel)
+ return CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel);
+ }
+
+ return false;
+}
+
+bool StopRecording::Execute(const CFileItemPtr& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = item->GetEPGInfoTag();
+ if (epgTag && epgTag->IsGapTag())
+ {
+ // instance recording
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(epgTag);
+ if (channel)
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().SetRecordingOnChannel(channel,
+ false);
+ }
+
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().StopRecording(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Edit recording
+
+bool EditRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording && !recording->IsDeleted() && !recording->IsInProgress())
+ {
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().CanEditRecording(item);
+ }
+ return false;
+}
+
+bool EditRecording::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().EditRecording(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Delete recording
+
+std::string DeleteRecording::GetLabel(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording && recording->IsDeleted())
+ return g_localizeStrings.Get(19291); /* Delete permanently */
+
+ return g_localizeStrings.Get(117); /* Delete */
+}
+
+bool DeleteRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(item);
+ if (client && !client->GetClientCapabilities().SupportsRecordingsDelete())
+ return false;
+
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording && !recording->IsInProgress())
+ return true;
+
+ // recordings folder?
+ if (item.m_bIsFolder &&
+ CServiceBroker::GetPVRManager().Clients()->AnyClientSupportingRecordingsDelete())
+ {
+ const CPVRRecordingsPath path(item.GetPath());
+ return path.IsValid() && !path.IsRecordingsRoot();
+ }
+
+ return false;
+}
+
+bool DeleteRecording::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteRecording(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Undelete recording
+
+bool UndeleteRecording::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording(item.GetPVRRecordingInfoTag());
+ if (recording && recording->IsDeleted())
+ return true;
+
+ return false;
+}
+
+bool UndeleteRecording::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().UndeleteRecording(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Delete watched recordings
+
+bool DeleteWatchedRecordings::IsVisible(const CFileItem& item) const
+{
+ // recordings folder?
+ if (item.m_bIsFolder && !item.IsParentFolder())
+ return CPVRRecordingsPath(item.GetPath()).IsValid();
+
+ return false;
+}
+
+bool DeleteWatchedRecordings::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteWatchedRecordings(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Add reminder
+
+bool AddReminder::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag();
+ if (epg && !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg) &&
+ epg->StartAsLocalTime() > CDateTime::GetCurrentDateTime())
+ return true;
+
+ return false;
+}
+
+bool AddReminder::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddReminder(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Activate / deactivate timer or timer rule
+
+std::string ToggleTimerState::GetLabel(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag());
+ if (timer && !timer->IsDisabled())
+ return g_localizeStrings.Get(844); /* Deactivate */
+
+ return g_localizeStrings.Get(843); /* Activate */
+}
+
+bool ToggleTimerState::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(item.GetPVRTimerInfoTag());
+ if (!timer || URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER) ||
+ timer->IsBroken())
+ return false;
+
+ return timer->GetTimerType()->SupportsEnableDisable();
+}
+
+bool ToggleTimerState::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimerState(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Add timer rule
+
+bool AddTimerRule::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag();
+ return (epg && !epg->IsGapTag() &&
+ !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg));
+}
+
+bool AddTimerRule::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimerRule(*item, true, true);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Edit timer rule
+
+std::string EditTimerRule::GetLabel(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> parentTimer(
+ CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer));
+ if (parentTimer)
+ {
+ if (!parentTimer->GetTimerType()->IsReadOnly())
+ return g_localizeStrings.Get(19243); /* Edit timer rule */
+ }
+ }
+
+ return g_localizeStrings.Get(19304); /* View timer rule */
+}
+
+bool EditTimerRule::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ return timer->HasParent();
+
+ return false;
+}
+
+bool EditTimerRule::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimerRule(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Delete timer rule
+
+bool DeleteTimerRule::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer && !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> parentTimer(
+ CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer));
+ if (parentTimer)
+ return parentTimer->GetTimerType()->AllowsDelete();
+ }
+
+ return false;
+}
+
+bool DeleteTimerRule::Execute(const CFileItemPtr& item) const
+{
+ auto& timers = CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>();
+ const std::shared_ptr<CFileItem> parentTimer = timers.GetTimerRule(*item);
+ if (parentTimer)
+ return timers.DeleteTimerRule(*parentTimer);
+
+ return false;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Edit / View timer
+
+std::string EditTimer::GetLabel(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer)
+ {
+ const std::shared_ptr<CPVRTimerType> timerType = timer->GetTimerType();
+ if (item.GetEPGInfoTag())
+ {
+ if (timerType->IsReminder())
+ return g_localizeStrings.Get(timerType->IsReadOnly() ? 829 /* View reminder */
+ : 830); /* Edit reminder */
+ else
+ return g_localizeStrings.Get(timerType->IsReadOnly() ? 19241 /* View timer */
+ : 19242); /* Edit timer */
+ }
+ else
+ return g_localizeStrings.Get(timerType->IsReadOnly() ? 21483 : 21450); /* View/Edit */
+ }
+ return g_localizeStrings.Get(19241); /* View timer */
+}
+
+bool EditTimer::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ return timer && (!item.GetEPGInfoTag() ||
+ !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER));
+}
+
+bool EditTimer::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimer(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Delete timer
+
+std::string DeleteTimer::GetLabel(const CFileItem& item) const
+{
+ if (item.GetPVRTimerInfoTag())
+ return g_localizeStrings.Get(117); /* Delete */
+
+ const std::shared_ptr<CPVREpgInfoTag> epg = item.GetEPGInfoTag();
+ if (epg)
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer =
+ CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epg);
+ if (timer && timer->IsReminder())
+ return g_localizeStrings.Get(827); /* Delete reminder */
+ }
+ return g_localizeStrings.Get(19060); /* Delete timer */
+}
+
+bool DeleteTimer::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(GetTimerInfoTagFromItem(item));
+ if (timer &&
+ (!item.GetEPGInfoTag() ||
+ !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER)) &&
+ !timer->IsRecording())
+ return timer->GetTimerType()->AllowsDelete();
+
+ return false;
+}
+
+bool DeleteTimer::Execute(const CFileItemPtr& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().DeleteTimer(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// PVR Client menu hook
+
+std::string PVRClientMenuHook::GetLabel(const CFileItem& item) const
+{
+ return m_hook.GetLabel();
+}
+
+bool PVRClientMenuHook::IsVisible(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(item);
+ if (!client || m_hook.GetAddonId() != client->ID())
+ return false;
+
+ if (m_hook.IsAllHook())
+ return !item.m_bIsFolder &&
+ !URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER);
+ else if (m_hook.IsEpgHook())
+ return item.IsEPG();
+ else if (m_hook.IsChannelHook())
+ return item.IsPVRChannel();
+ else if (m_hook.IsDeletedRecordingHook())
+ return item.IsDeletedPVRRecording();
+ else if (m_hook.IsRecordingHook())
+ return item.IsUsablePVRRecording();
+ else if (m_hook.IsTimerHook())
+ return item.IsPVRTimer();
+ else
+ return false;
+}
+
+bool PVRClientMenuHook::Execute(const CFileItemPtr& item) const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item);
+ if (!client)
+ return false;
+
+ if (item->IsEPG())
+ return client->CallEpgTagMenuHook(m_hook, item->GetEPGInfoTag()) == PVR_ERROR_NO_ERROR;
+ else if (item->IsPVRChannel())
+ return client->CallChannelMenuHook(m_hook, item->GetPVRChannelInfoTag()) == PVR_ERROR_NO_ERROR;
+ else if (item->IsDeletedPVRRecording())
+ return client->CallRecordingMenuHook(m_hook, item->GetPVRRecordingInfoTag(), true) ==
+ PVR_ERROR_NO_ERROR;
+ else if (item->IsUsablePVRRecording())
+ return client->CallRecordingMenuHook(m_hook, item->GetPVRRecordingInfoTag(), false) ==
+ PVR_ERROR_NO_ERROR;
+ else if (item->IsPVRTimer())
+ return client->CallTimerMenuHook(m_hook, item->GetPVRTimerInfoTag()) == PVR_ERROR_NO_ERROR;
+ else
+ return false;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Execute saved search
+
+bool ExecuteSearch::IsVisible(const CFileItem& item) const
+{
+ return item.HasEPGSearchFilter();
+}
+
+bool ExecuteSearch::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ExecuteSavedSearch(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Edit saved search
+
+bool EditSearch::IsVisible(const CFileItem& item) const
+{
+ return item.HasEPGSearchFilter();
+}
+
+bool EditSearch::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().EditSavedSearch(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Rename saved search
+
+bool RenameSearch::IsVisible(const CFileItem& item) const
+{
+ return item.HasEPGSearchFilter();
+}
+
+bool RenameSearch::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().RenameSavedSearch(*item);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Delete saved search
+
+bool DeleteSearch::IsVisible(const CFileItem& item) const
+{
+ return item.HasEPGSearchFilter();
+}
+
+bool DeleteSearch::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().DeleteSavedSearch(*item);
+}
+
+} // namespace CONTEXTMENUITEM
+
+CPVRContextMenuManager& CPVRContextMenuManager::GetInstance()
+{
+ static CPVRContextMenuManager instance;
+ return instance;
+}
+
+CPVRContextMenuManager::CPVRContextMenuManager()
+ : m_items({
+ std::make_shared<CONTEXTMENUITEM::PlayEpgTag>(19190), /* Play programme */
+ std::make_shared<CONTEXTMENUITEM::PlayRecording>(19687), /* Play recording */
+ std::make_shared<CONTEXTMENUITEM::ShowInformation>(),
+ std::make_shared<CONTEXTMENUITEM::ShowChannelGuide>(19686), /* Channel guide */
+ std::make_shared<CONTEXTMENUITEM::FindSimilar>(19003), /* Find similar */
+ std::make_shared<CONTEXTMENUITEM::ToggleTimerState>(),
+ std::make_shared<CONTEXTMENUITEM::AddTimerRule>(19061), /* Add timer */
+ std::make_shared<CONTEXTMENUITEM::EditTimerRule>(),
+ std::make_shared<CONTEXTMENUITEM::DeleteTimerRule>(19295), /* Delete timer rule */
+ std::make_shared<CONTEXTMENUITEM::EditTimer>(),
+ std::make_shared<CONTEXTMENUITEM::DeleteTimer>(),
+ std::make_shared<CONTEXTMENUITEM::StartRecording>(264), /* Record */
+ std::make_shared<CONTEXTMENUITEM::StopRecording>(19059), /* Stop recording */
+ std::make_shared<CONTEXTMENUITEM::EditRecording>(21450), /* Edit */
+ std::make_shared<CONTEXTMENUITEM::DeleteRecording>(),
+ std::make_shared<CONTEXTMENUITEM::UndeleteRecording>(19290), /* Undelete */
+ std::make_shared<CONTEXTMENUITEM::DeleteWatchedRecordings>(19327), /* Delete watched */
+ std::make_shared<CONTEXTMENUITEM::AddReminder>(826), /* Set reminder */
+ std::make_shared<CONTEXTMENUITEM::ExecuteSearch>(137), /* Search */
+ std::make_shared<CONTEXTMENUITEM::EditSearch>(21450), /* Edit */
+ std::make_shared<CONTEXTMENUITEM::RenameSearch>(118), /* Rename */
+ std::make_shared<CONTEXTMENUITEM::DeleteSearch>(117), /* Delete */
+ })
+{
+}
+
+void CPVRContextMenuManager::AddMenuHook(const CPVRClientMenuHook& hook)
+{
+ if (hook.IsSettingsHook())
+ return; // settings hooks are not handled using context menus
+
+ const auto item = std::make_shared<CONTEXTMENUITEM::PVRClientMenuHook>(hook);
+ m_items.emplace_back(item);
+ m_events.Publish(PVRContextMenuEvent(PVRContextMenuEventAction::ADD_ITEM, item));
+}
+
+void CPVRContextMenuManager::RemoveMenuHook(const CPVRClientMenuHook& hook)
+{
+ if (hook.IsSettingsHook())
+ return; // settings hooks are not handled using context menus
+
+ for (auto it = m_items.begin(); it < m_items.end(); ++it)
+ {
+ const CONTEXTMENUITEM::PVRClientMenuHook* cmh =
+ dynamic_cast<const CONTEXTMENUITEM::PVRClientMenuHook*>((*it).get());
+ if (cmh && cmh->GetHook() == hook)
+ {
+ m_events.Publish(PVRContextMenuEvent(PVRContextMenuEventAction::REMOVE_ITEM, *it));
+ m_items.erase(it);
+ return;
+ }
+ }
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRContextMenus.h b/xbmc/pvr/PVRContextMenus.h
new file mode 100644
index 0000000..357db17
--- /dev/null
+++ b/xbmc/pvr/PVRContextMenus.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016-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 "utils/EventStream.h"
+
+#include <memory>
+#include <vector>
+
+class IContextMenuItem;
+
+namespace PVR
+{
+enum class PVRContextMenuEventAction
+{
+ ADD_ITEM,
+ REMOVE_ITEM
+};
+
+struct PVRContextMenuEvent
+{
+ PVRContextMenuEvent(const PVRContextMenuEventAction& a,
+ const std::shared_ptr<IContextMenuItem>& i)
+ : action(a), item(i)
+ {
+ }
+
+ PVRContextMenuEventAction action;
+ std::shared_ptr<IContextMenuItem> item;
+};
+
+class CPVRClientMenuHook;
+
+class CPVRContextMenuManager
+{
+public:
+ static CPVRContextMenuManager& GetInstance();
+
+ std::vector<std::shared_ptr<IContextMenuItem>> GetMenuItems() const { return m_items; }
+
+ void AddMenuHook(const CPVRClientMenuHook& hook);
+ void RemoveMenuHook(const CPVRClientMenuHook& hook);
+
+ /*!
+ * @brief Query the events available for CEventStream
+ */
+ CEventStream<PVRContextMenuEvent>& Events() { return m_events; }
+
+private:
+ CPVRContextMenuManager();
+ CPVRContextMenuManager(const CPVRContextMenuManager&) = delete;
+ CPVRContextMenuManager const& operator=(CPVRContextMenuManager const&) = delete;
+ virtual ~CPVRContextMenuManager() = default;
+
+ std::vector<std::shared_ptr<IContextMenuItem>> m_items;
+ CEventSource<PVRContextMenuEvent> m_events;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRDatabase.cpp b/xbmc/pvr/PVRDatabase.cpp
new file mode 100644
index 0000000..9475c5a
--- /dev/null
+++ b/xbmc/pvr/PVRDatabase.cpp
@@ -0,0 +1,1141 @@
+/*
+ * 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 "PVRDatabase.h"
+
+#include "ServiceBroker.h"
+#include "dbwrappers/dataset.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/providers/PVRProvider.h"
+#include "pvr/providers/PVRProviders.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimerType.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <cstdlib>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace dbiplus;
+using namespace PVR;
+
+namespace
+{
+// clang-format off
+
+ static const std::string sqlCreateTimersTable =
+ "CREATE TABLE timers ("
+ "iClientIndex integer primary key, "
+ "iParentClientIndex integer, "
+ "iClientId integer, "
+ "iTimerType integer, "
+ "iState integer, "
+ "sTitle varchar(255), "
+ "iClientChannelUid integer, "
+ "sSeriesLink varchar(255), "
+ "sStartTime varchar(20), "
+ "bStartAnyTime bool, "
+ "sEndTime varchar(20), "
+ "bEndAnyTime bool, "
+ "sFirstDay varchar(20), "
+ "iWeekdays integer, "
+ "iEpgUid integer, "
+ "iMarginStart integer, "
+ "iMarginEnd integer, "
+ "sEpgSearchString varchar(255), "
+ "bFullTextEpgSearch bool, "
+ "iPreventDuplicates integer,"
+ "iPrority integer,"
+ "iLifetime integer,"
+ "iMaxRecordings integer,"
+ "iRecordingGroup integer"
+ ")";
+
+ static const std::string sqlCreateChannelGroupsTable =
+ "CREATE TABLE channelgroups ("
+ "idGroup integer primary key,"
+ "bIsRadio bool, "
+ "iGroupType integer, "
+ "sName varchar(64), "
+ "iLastWatched integer, "
+ "bIsHidden bool, "
+ "iPosition integer, "
+ "iLastOpened bigint unsigned"
+ ")";
+
+ static const std::string sqlCreateProvidersTable =
+ "CREATE TABLE providers ("
+ "idProvider integer primary key, "
+ "iUniqueId integer, "
+ "iClientId integer, "
+ "sName varchar(64), "
+ "iType integer, "
+ "sIconPath varchar(255), "
+ "sCountries varchar(64), "
+ "sLanguages varchar(64) "
+ ")";
+
+ // clang-format on
+
+ std::string GetClientIdsSQL(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+ {
+ if (clients.empty())
+ return {};
+
+ std::string clientIds = "(";
+ for (auto it = clients.cbegin(); it != clients.cend(); ++it)
+ {
+ if (it != clients.cbegin())
+ clientIds += " OR ";
+
+ clientIds += "iClientId = ";
+ clientIds += std::to_string((*it)->GetID());
+ }
+ clientIds += ")";
+ return clientIds;
+ }
+
+} // unnamed namespace
+
+bool CPVRDatabase::Open()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return CDatabase::Open(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseTV);
+}
+
+void CPVRDatabase::Close()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CDatabase::Close();
+}
+
+void CPVRDatabase::Lock()
+{
+ m_critSection.lock();
+}
+
+void CPVRDatabase::Unlock()
+{
+ m_critSection.unlock();
+}
+
+void CPVRDatabase::CreateTables()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CLog::LogF(LOGINFO, "Creating PVR database tables");
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'channels'");
+ m_pDS->exec("CREATE TABLE channels ("
+ "idChannel integer primary key, "
+ "iUniqueId integer, "
+ "bIsRadio bool, "
+ "bIsHidden bool, "
+ "bIsUserSetIcon bool, "
+ "bIsUserSetName bool, "
+ "bIsLocked bool, "
+ "sIconPath varchar(255), "
+ "sChannelName varchar(64), "
+ "bIsVirtual bool, "
+ "bEPGEnabled bool, "
+ "sEPGScraper varchar(32), "
+ "iLastWatched integer, "
+ "iClientId integer, " //! @todo use mapping table
+ "idEpg integer, "
+ "bHasArchive bool, "
+ "iClientProviderUid integer, "
+ "bIsUserSetHidden bool"
+ ")");
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'channelgroups'");
+ m_pDS->exec(sqlCreateChannelGroupsTable);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'map_channelgroups_channels'");
+ m_pDS->exec(
+ "CREATE TABLE map_channelgroups_channels ("
+ "idChannel integer, "
+ "idGroup integer, "
+ "iChannelNumber integer, "
+ "iSubChannelNumber integer, "
+ "iOrder integer, "
+ "iClientChannelNumber integer, "
+ "iClientSubChannelNumber integer"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'clients'");
+ m_pDS->exec(
+ "CREATE TABLE clients ("
+ "idClient integer primary key, "
+ "iPriority integer"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'timers'");
+ m_pDS->exec(sqlCreateTimersTable);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating table 'providers'");
+ m_pDS->exec(sqlCreateProvidersTable);
+}
+
+void CPVRDatabase::CreateAnalytics()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CLog::LogF(LOGINFO, "Creating PVR database indices");
+ m_pDS->exec("CREATE INDEX idx_clients_idClient on clients(idClient);");
+ m_pDS->exec("CREATE UNIQUE INDEX idx_channels_iClientId_iUniqueId on channels(iClientId, iUniqueId);");
+ m_pDS->exec("CREATE INDEX idx_channelgroups_bIsRadio on channelgroups(bIsRadio);");
+ m_pDS->exec("CREATE UNIQUE INDEX idx_idGroup_idChannel on map_channelgroups_channels(idGroup, idChannel);");
+ m_pDS->exec("CREATE INDEX idx_timers_iClientIndex on timers(iClientIndex);");
+}
+
+void CPVRDatabase::UpdateTables(int iVersion)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (iVersion < 13)
+ m_pDS->exec("ALTER TABLE channels ADD idEpg integer;");
+
+ if (iVersion < 20)
+ m_pDS->exec("ALTER TABLE channels ADD bIsUserSetIcon bool");
+
+ if (iVersion < 21)
+ m_pDS->exec("ALTER TABLE channelgroups ADD iGroupType integer");
+
+ if (iVersion < 22)
+ m_pDS->exec("ALTER TABLE channels ADD bIsLocked bool");
+
+ if (iVersion < 23)
+ m_pDS->exec("ALTER TABLE channelgroups ADD iLastWatched integer");
+
+ if (iVersion < 24)
+ m_pDS->exec("ALTER TABLE channels ADD bIsUserSetName bool");
+
+ if (iVersion < 25)
+ m_pDS->exec("DROP TABLE IF EXISTS channelsettings");
+
+ if (iVersion < 26)
+ {
+ m_pDS->exec("ALTER TABLE channels ADD iClientSubChannelNumber integer");
+ m_pDS->exec("UPDATE channels SET iClientSubChannelNumber = 0");
+ m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iSubChannelNumber integer");
+ m_pDS->exec("UPDATE map_channelgroups_channels SET iSubChannelNumber = 0");
+ }
+
+ if (iVersion < 27)
+ m_pDS->exec("ALTER TABLE channelgroups ADD bIsHidden bool");
+
+ if (iVersion < 28)
+ m_pDS->exec("DROP TABLE clients");
+
+ if (iVersion < 29)
+ m_pDS->exec("ALTER TABLE channelgroups ADD iPosition integer");
+
+ if (iVersion < 32)
+ m_pDS->exec("CREATE TABLE clients (idClient integer primary key, iPriority integer)");
+
+ if (iVersion < 33)
+ m_pDS->exec(sqlCreateTimersTable);
+
+ if (iVersion < 34)
+ m_pDS->exec("ALTER TABLE channels ADD bHasArchive bool");
+
+ if (iVersion < 35)
+ {
+ m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iOrder integer");
+ m_pDS->exec("UPDATE map_channelgroups_channels SET iOrder = 0");
+ }
+
+ if (iVersion < 36)
+ {
+ m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iClientChannelNumber integer");
+ m_pDS->exec("UPDATE map_channelgroups_channels SET iClientChannelNumber = 0");
+ m_pDS->exec("ALTER TABLE map_channelgroups_channels ADD iClientSubChannelNumber integer");
+ m_pDS->exec("UPDATE map_channelgroups_channels SET iClientSubChannelNumber = 0");
+ }
+
+ if (iVersion < 37)
+ m_pDS->exec("ALTER TABLE channelgroups ADD iLastOpened integer");
+
+ if (iVersion < 38)
+ {
+ m_pDS->exec("ALTER TABLE channelgroups "
+ "RENAME TO channelgroups_old");
+
+ m_pDS->exec(sqlCreateChannelGroupsTable);
+
+ m_pDS->exec(
+ "INSERT INTO channelgroups (bIsRadio, iGroupType, sName, iLastWatched, bIsHidden, "
+ "iPosition, iLastOpened) "
+ "SELECT bIsRadio, iGroupType, sName, iLastWatched, bIsHidden, iPosition, iLastOpened "
+ "FROM channelgroups_old");
+
+ m_pDS->exec("DROP TABLE channelgroups_old");
+ }
+
+ if (iVersion < 39)
+ {
+ m_pDS->exec(sqlCreateProvidersTable);
+ m_pDS->exec("CREATE UNIQUE INDEX idx_iUniqueId_iClientId on providers(iUniqueId, iClientId);");
+ m_pDS->exec("ALTER TABLE channels ADD iClientProviderUid integer");
+ m_pDS->exec("UPDATE channels SET iClientProviderUid = -1");
+ }
+
+ if (iVersion < 40)
+ {
+ m_pDS->exec("ALTER TABLE channels ADD bIsUserSetHidden bool");
+ m_pDS->exec("UPDATE channels SET bIsUserSetHidden = bIsHidden");
+ }
+}
+
+/********** Client methods **********/
+
+bool CPVRDatabase::DeleteClients()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all clients from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("clients");
+}
+
+bool CPVRDatabase::Persist(const CPVRClient& client)
+{
+ if (client.GetID() == PVR_INVALID_CLIENT_ID)
+ return false;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting client '{}' to database", client.ID());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::string strQuery = PrepareSQL("REPLACE INTO clients (idClient, iPriority) VALUES (%i, %i);",
+ client.GetID(), client.GetPriority());
+
+ return ExecuteQuery(strQuery);
+}
+
+bool CPVRDatabase::Delete(const CPVRClient& client)
+{
+ if (client.GetID() == PVR_INVALID_CLIENT_ID)
+ return false;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting client '{}' from the database", client.ID());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idClient = '%i'", client.GetID()));
+
+ return DeleteValues("clients", filter);
+}
+
+int CPVRDatabase::GetPriority(const CPVRClient& client)
+{
+ if (client.GetID() == PVR_INVALID_CLIENT_ID)
+ return 0;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Getting priority for client '{}' from the database", client.ID());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::string strWhereClause = PrepareSQL("idClient = '%i'", client.GetID());
+ const std::string strValue = GetSingleValue("clients", "iPriority", strWhereClause);
+
+ if (strValue.empty())
+ return 0;
+
+ return atoi(strValue.c_str());
+}
+
+/********** Channel provider methods **********/
+
+bool CPVRDatabase::DeleteProviders()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all providers from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("providers");
+}
+
+bool CPVRDatabase::Persist(CPVRProvider& provider, bool updateRecord /* = false */)
+{
+ bool bReturn = false;
+ if (provider.GetName().empty())
+ {
+ CLog::LogF(LOGERROR, "Empty provider name");
+ return bReturn;
+ }
+
+ std::string strQuery;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ {
+ /* insert a new entry when this is a new group, or replace the existing one otherwise */
+ if (!updateRecord)
+ strQuery =
+ PrepareSQL("INSERT INTO providers (idProvider, iUniqueId, iClientId, sName, "
+ "iType, sIconPath, sCountries, sLanguages) "
+ "VALUES (%i, %i, %i, '%s', %i, '%s', '%s', '%s');",
+ provider.GetDatabaseId(), provider.GetUniqueId(), provider.GetClientId(),
+ provider.GetName().c_str(), static_cast<int>(provider.GetType()),
+ provider.GetClientIconPath().c_str(), provider.GetCountriesDBString().c_str(),
+ provider.GetLanguagesDBString().c_str());
+ else
+ strQuery =
+ PrepareSQL("REPLACE INTO providers (idProvider, iUniqueId, iClientId, sName, "
+ "iType, sIconPath, sCountries, sLanguages) "
+ "VALUES (%i, %i, %i, '%s', %i, '%s', '%s', '%s');",
+ provider.GetDatabaseId(), provider.GetUniqueId(), provider.GetClientId(),
+ provider.GetName().c_str(), static_cast<int>(provider.GetType()),
+ provider.GetClientIconPath().c_str(), provider.GetCountriesDBString().c_str(),
+ provider.GetLanguagesDBString().c_str());
+
+ bReturn = ExecuteQuery(strQuery);
+
+ /* set the provider id if it was <= 0 */
+ if (bReturn && provider.GetDatabaseId() <= 0)
+ {
+ provider.SetDatabaseId(static_cast<int>(m_pDS->lastinsertid()));
+ }
+ }
+
+ return bReturn;
+}
+
+bool CPVRDatabase::Delete(const CPVRProvider& provider)
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting provider '{}' from the database",
+ provider.GetName());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idProvider = '%i'", provider.GetDatabaseId()));
+
+ return DeleteValues("providers", filter);
+}
+
+bool CPVRDatabase::Get(CPVRProviders& results,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients) const
+{
+ bool bReturn = false;
+
+ std::string strQuery = "SELECT * from providers ";
+ const std::string clientIds = GetClientIdsSQL(clients);
+ if (!clientIds.empty())
+ strQuery += "WHERE " + clientIds + " OR iType = 1"; // always load addon providers
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strQuery = PrepareSQL(strQuery);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ std::shared_ptr<CPVRProvider> provider = std::make_shared<CPVRProvider>(
+ m_pDS->fv("iUniqueId").get_asInt(), m_pDS->fv("iClientId").get_asInt());
+
+ provider->SetDatabaseId(m_pDS->fv("idProvider").get_asInt());
+ provider->SetName(m_pDS->fv("sName").get_asString());
+ provider->SetType(
+ static_cast<PVR_PROVIDER_TYPE>(m_pDS->fv("iType").get_asInt()));
+ provider->SetIconPath(m_pDS->fv("sIconPath").get_asString());
+ provider->SetCountriesFromDBString(m_pDS->fv("sCountries").get_asString());
+ provider->SetLanguagesFromDBString(m_pDS->fv("sLanguages").get_asString());
+
+ results.CheckAndAddEntry(provider, ProviderUpdateMode::BY_DATABASE);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Channel Provider '{}' loaded from PVR database",
+ provider->GetName());
+ m_pDS->next();
+ }
+
+ m_pDS->close();
+ bReturn = true;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Couldn't load providers from PVR database");
+ }
+ }
+
+ return bReturn;
+}
+
+int CPVRDatabase::GetMaxProviderId()
+{
+ std::string strQuery = PrepareSQL("SELECT max(idProvider) as maxProviderId from providers");
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ return GetSingleValueInt(strQuery);
+}
+
+/********** Channel methods **********/
+
+int CPVRDatabase::Get(bool bRadio,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& results) const
+{
+ int iReturn = 0;
+
+ std::string strQuery = "SELECT * from channels WHERE bIsRadio = %u ";
+ const std::string clientIds = GetClientIdsSQL(clients);
+ if (!clientIds.empty())
+ strQuery += "AND " + clientIds;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strQuery = PrepareSQL(strQuery, bRadio);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ const std::shared_ptr<CPVRChannel> channel(new CPVRChannel(
+ m_pDS->fv("bIsRadio").get_asBool(), m_pDS->fv("sIconPath").get_asString()));
+
+ channel->m_iChannelId = m_pDS->fv("idChannel").get_asInt();
+ channel->m_iUniqueId = m_pDS->fv("iUniqueId").get_asInt();
+ channel->m_bIsHidden = m_pDS->fv("bIsHidden").get_asBool();
+ channel->m_bIsUserSetIcon = m_pDS->fv("bIsUserSetIcon").get_asBool();
+ channel->m_bIsUserSetName = m_pDS->fv("bIsUserSetName").get_asBool();
+ channel->m_bIsLocked = m_pDS->fv("bIsLocked").get_asBool();
+ channel->m_strChannelName = m_pDS->fv("sChannelName").get_asString();
+ channel->m_bEPGEnabled = m_pDS->fv("bEPGEnabled").get_asBool();
+ channel->m_strEPGScraper = m_pDS->fv("sEPGScraper").get_asString();
+ channel->m_iLastWatched = static_cast<time_t>(m_pDS->fv("iLastWatched").get_asInt());
+ channel->m_iClientId = m_pDS->fv("iClientId").get_asInt();
+ channel->m_iEpgId = m_pDS->fv("idEpg").get_asInt();
+ channel->m_bHasArchive = m_pDS->fv("bHasArchive").get_asBool();
+ channel->m_iClientProviderUid = m_pDS->fv("iClientProviderUid").get_asInt();
+ channel->m_bIsUserSetHidden = m_pDS->fv("bIsUserSetHidden").get_asBool();
+
+ channel->UpdateEncryptionName();
+
+ results.insert({channel->StorageId(), channel});
+
+ m_pDS->next();
+ ++iReturn;
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Couldn't load channels from PVR database");
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "PVR database query failed");
+ }
+
+ m_pDS->close();
+ return iReturn;
+}
+
+bool CPVRDatabase::DeleteChannels()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all channels from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("channels");
+}
+
+bool CPVRDatabase::QueueDeleteQuery(const CPVRChannel& channel)
+{
+ /* invalid channel */
+ if (channel.ChannelID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel id: {}", channel.ChannelID());
+ return false;
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Queueing delete for channel '{}' from the database",
+ channel.ChannelName());
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idChannel = %i", channel.ChannelID()));
+
+ std::string strQuery;
+ if (BuildSQL(PrepareSQL("DELETE FROM %s ", "channels"), filter, strQuery))
+ return CDatabase::QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+/********** Channel group member methods **********/
+
+bool CPVRDatabase::QueueDeleteQuery(const CPVRChannelGroupMember& groupMember)
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Queueing delete for channel group member '{}' from the database",
+ groupMember.Channel() ? groupMember.Channel()->ChannelName()
+ : std::to_string(groupMember.ChannelDatabaseID()));
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idGroup = %i", groupMember.GroupID()));
+ filter.AppendWhere(PrepareSQL("idChannel = %i", groupMember.ChannelDatabaseID()));
+
+ std::string strQuery;
+ if (BuildSQL(PrepareSQL("DELETE FROM %s ", "map_channelgroups_channels"), filter, strQuery))
+ return CDatabase::QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+/********** Channel group methods **********/
+
+bool CPVRDatabase::RemoveChannelsFromGroup(const CPVRChannelGroup& group)
+{
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idGroup = %i", group.GroupID()));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("map_channelgroups_channels", filter);
+}
+
+bool CPVRDatabase::DeleteChannelGroups()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all channel groups from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("channelgroups") && DeleteValues("map_channelgroups_channels");
+}
+
+bool CPVRDatabase::Delete(const CPVRChannelGroup& group)
+{
+ /* invalid group id */
+ if (group.GroupID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID());
+ return false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idGroup = %i", group.GroupID()));
+ filter.AppendWhere(PrepareSQL("bIsRadio = %u", group.IsRadio()));
+
+ return RemoveChannelsFromGroup(group) && DeleteValues("channelgroups", filter);
+}
+
+int CPVRDatabase::Get(CPVRChannelGroups& results) const
+{
+ int iLoaded = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::string strQuery = PrepareSQL("SELECT * from channelgroups WHERE bIsRadio = %u", results.IsRadio());
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ results.CreateChannelGroup(m_pDS->fv("iGroupType").get_asInt(),
+ CPVRChannelsPath(m_pDS->fv("bIsRadio").get_asBool(),
+ m_pDS->fv("sName").get_asString()));
+
+ group->m_iGroupId = m_pDS->fv("idGroup").get_asInt();
+ group->m_iGroupType = m_pDS->fv("iGroupType").get_asInt();
+ group->m_iLastWatched = static_cast<time_t>(m_pDS->fv("iLastWatched").get_asInt());
+ group->m_bHidden = m_pDS->fv("bIsHidden").get_asBool();
+ group->m_iPosition = m_pDS->fv("iPosition").get_asInt();
+ group->m_iLastOpened = static_cast<uint64_t>(m_pDS->fv("iLastOpened").get_asInt64());
+ results.Update(group);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Group '{}' loaded from PVR database", group->GroupName());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ iLoaded++;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Couldn't load channel groups from PVR database. Exception.");
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Couldn't load channel groups from PVR database. Query failed.");
+ }
+
+ return iLoaded;
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRDatabase::Get(
+ const CPVRChannelGroup& group, const std::vector<std::shared_ptr<CPVRClient>>& clients) const
+{
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> results;
+
+ /* invalid group id */
+ if (group.GroupID() < 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID());
+ return results;
+ }
+
+ std::string strQuery =
+ "SELECT map_channelgroups_channels.idChannel, "
+ "map_channelgroups_channels.iChannelNumber, "
+ "map_channelgroups_channels.iSubChannelNumber, "
+ "map_channelgroups_channels.iOrder, "
+ "map_channelgroups_channels.iClientChannelNumber, "
+ "map_channelgroups_channels.iClientSubChannelNumber, "
+ "channels.iClientId, channels.iUniqueId, channels.bIsRadio "
+ "FROM map_channelgroups_channels "
+ "LEFT JOIN channels ON channels.idChannel = map_channelgroups_channels.idChannel "
+ "WHERE map_channelgroups_channels.idGroup = %i ";
+ const std::string clientIds = GetClientIdsSQL(clients);
+ if (!clientIds.empty())
+ strQuery += "AND " + clientIds;
+ strQuery += " ORDER BY map_channelgroups_channels.iChannelNumber";
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strQuery = PrepareSQL(strQuery, group.GroupID());
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ const auto newMember = std::make_shared<CPVRChannelGroupMember>();
+ newMember->m_iChannelDatabaseID = m_pDS->fv("idChannel").get_asInt();
+ newMember->m_iClientID = m_pDS->fv("iClientId").get_asInt();
+ newMember->m_iChannelUID = m_pDS->fv("iUniqueId").get_asInt();
+ newMember->m_iGroupID = group.GroupID();
+ newMember->m_bIsRadio = m_pDS->fv("bIsRadio").get_asBool();
+ newMember->m_channelNumber = {
+ static_cast<unsigned int>(m_pDS->fv("iChannelNumber").get_asInt()),
+ static_cast<unsigned int>(m_pDS->fv("iSubChannelNumber").get_asInt())};
+ newMember->m_clientChannelNumber = {
+ static_cast<unsigned int>(m_pDS->fv("iClientChannelNumber").get_asInt()),
+ static_cast<unsigned int>(m_pDS->fv("iClientSubChannelNumber").get_asInt())};
+ newMember->m_iOrder = static_cast<int>(m_pDS->fv("iOrder").get_asInt());
+ newMember->SetGroupName(group.GroupName());
+
+ results.emplace_back(newMember);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch(...)
+ {
+ CLog::LogF(LOGERROR, "Failed to get channel group members");
+ }
+ }
+
+ return results;
+}
+
+bool CPVRDatabase::PersistChannels(const CPVRChannelGroup& group)
+{
+ /* invalid group id */
+ if (group.GroupID() < 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID());
+ return false;
+ }
+
+ bool bReturn(true);
+
+ std::shared_ptr<CPVRChannel> channel;
+ for (const auto& groupMember : group.m_members)
+ {
+ channel = groupMember.second->Channel();
+ if (channel->IsChanged() || channel->IsNew())
+ {
+ if (Persist(*channel, false))
+ {
+ channel->Persisted();
+ bReturn = true;
+ }
+ }
+ }
+
+ bReturn &= CommitInsertQueries();
+
+ if (bReturn)
+ {
+ std::string strQuery;
+ std::string strValue;
+ for (const auto& groupMember : group.m_members)
+ {
+ channel = groupMember.second->Channel();
+ strQuery =
+ PrepareSQL("iUniqueId = %i AND iClientId = %i", channel->UniqueID(), channel->ClientID());
+ strValue = GetSingleValue("channels", "idChannel", strQuery);
+ if (!strValue.empty() && StringUtils::IsInteger(strValue))
+ {
+ const int iChannelID = std::atoi(strValue.c_str());
+ channel->SetChannelID(iChannelID);
+ groupMember.second->m_iChannelDatabaseID = iChannelID;
+ }
+ }
+ }
+
+ return bReturn;
+}
+
+bool CPVRDatabase::PersistGroupMembers(const CPVRChannelGroup& group)
+{
+ /* invalid group id */
+ if (group.GroupID() < 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel group id: {}", group.GroupID());
+ return false;
+ }
+
+ bool bReturn = true;
+
+ if (group.HasChannels())
+ {
+ for (const auto& groupMember : group.m_sortedMembers)
+ {
+ if (groupMember->NeedsSave())
+ {
+ if (groupMember->ChannelDatabaseID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel id: {}", groupMember->ChannelDatabaseID());
+ continue;
+ }
+
+ const std::string strWhereClause =
+ PrepareSQL("idChannel = %i AND idGroup = %i AND iChannelNumber = %u AND "
+ "iSubChannelNumber = %u AND "
+ "iOrder = %i AND iClientChannelNumber = %u AND iClientSubChannelNumber = %u",
+ groupMember->ChannelDatabaseID(), group.GroupID(),
+ groupMember->ChannelNumber().GetChannelNumber(),
+ groupMember->ChannelNumber().GetSubChannelNumber(), groupMember->Order(),
+ groupMember->ClientChannelNumber().GetChannelNumber(),
+ groupMember->ClientChannelNumber().GetSubChannelNumber());
+
+ const std::string strValue =
+ GetSingleValue("map_channelgroups_channels", "idChannel", strWhereClause);
+ if (strValue.empty())
+ {
+ const std::string strQuery =
+ PrepareSQL("REPLACE INTO map_channelgroups_channels ("
+ "idGroup, idChannel, iChannelNumber, iSubChannelNumber, iOrder, "
+ "iClientChannelNumber, iClientSubChannelNumber) "
+ "VALUES (%i, %i, %i, %i, %i, %i, %i);",
+ group.GroupID(), groupMember->ChannelDatabaseID(),
+ groupMember->ChannelNumber().GetChannelNumber(),
+ groupMember->ChannelNumber().GetSubChannelNumber(), groupMember->Order(),
+ groupMember->ClientChannelNumber().GetChannelNumber(),
+ groupMember->ClientChannelNumber().GetSubChannelNumber());
+ QueueInsertQuery(strQuery);
+ }
+ }
+ }
+
+ bReturn = CommitInsertQueries();
+
+ if (bReturn)
+ {
+ for (const auto& groupMember : group.m_sortedMembers)
+ {
+ groupMember->SetSaved();
+ }
+ }
+ }
+
+ return bReturn;
+}
+
+/********** Client methods **********/
+
+bool CPVRDatabase::ResetEPG()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("UPDATE channels SET idEpg = 0");
+ return ExecuteQuery(strQuery);
+}
+
+bool CPVRDatabase::Persist(CPVRChannelGroup& group)
+{
+ bool bReturn(false);
+ if (group.GroupName().empty())
+ {
+ CLog::LogF(LOGERROR, "Empty group name");
+ return bReturn;
+ }
+
+ std::string strQuery;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (group.HasChanges() || group.IsNew())
+ {
+ /* insert a new entry when this is a new group, or replace the existing one otherwise */
+ if (group.IsNew())
+ strQuery =
+ PrepareSQL("INSERT INTO channelgroups (bIsRadio, iGroupType, sName, iLastWatched, "
+ "bIsHidden, iPosition, iLastOpened) VALUES (%i, %i, '%s', %u, %i, %i, %llu)",
+ (group.IsRadio() ? 1 : 0), group.GroupType(), group.GroupName().c_str(),
+ static_cast<unsigned int>(group.LastWatched()), group.IsHidden(),
+ group.GetPosition(), group.LastOpened());
+ else
+ strQuery = PrepareSQL(
+ "REPLACE INTO channelgroups (idGroup, bIsRadio, iGroupType, sName, iLastWatched, "
+ "bIsHidden, iPosition, iLastOpened) VALUES (%i, %i, %i, '%s', %u, %i, %i, %llu)",
+ group.GroupID(), (group.IsRadio() ? 1 : 0), group.GroupType(), group.GroupName().c_str(),
+ static_cast<unsigned int>(group.LastWatched()), group.IsHidden(), group.GetPosition(),
+ group.LastOpened());
+
+ bReturn = ExecuteQuery(strQuery);
+
+ // set the group ID for new groups
+ if (bReturn && group.IsNew())
+ group.SetGroupID(static_cast<int>(m_pDS->lastinsertid()));
+ }
+ else
+ bReturn = true;
+
+ /* only persist the channel data for the internal groups */
+ if (group.IsInternalGroup())
+ bReturn &= PersistChannels(group);
+
+ /* persist the group member entries */
+ if (bReturn)
+ bReturn = PersistGroupMembers(group);
+
+ return bReturn;
+}
+
+bool CPVRDatabase::Persist(CPVRChannel& channel, bool bCommit)
+{
+ bool bReturn(false);
+
+ /* invalid channel */
+ if (channel.UniqueID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel uid: {}", channel.UniqueID());
+ return bReturn;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // Note: Do not use channel.ChannelID value to check presence of channel in channels table. It might not yet be set correctly.
+ std::string strQuery =
+ PrepareSQL("iUniqueId = %i AND iClientId = %i", channel.UniqueID(), channel.ClientID());
+ const std::string strValue = GetSingleValue("channels", "idChannel", strQuery);
+ if (strValue.empty())
+ {
+ /* new channel */
+ strQuery = PrepareSQL(
+ "INSERT INTO channels ("
+ "iUniqueId, bIsRadio, bIsHidden, bIsUserSetIcon, bIsUserSetName, bIsLocked, "
+ "sIconPath, sChannelName, bIsVirtual, bEPGEnabled, sEPGScraper, iLastWatched, iClientId, "
+ "idEpg, bHasArchive, iClientProviderUid, bIsUserSetHidden) "
+ "VALUES (%i, %i, %i, %i, %i, %i, '%s', '%s', %i, %i, '%s', %u, %i, %i, %i, %i, %i)",
+ channel.UniqueID(), (channel.IsRadio() ? 1 : 0), (channel.IsHidden() ? 1 : 0),
+ (channel.IsUserSetIcon() ? 1 : 0), (channel.IsUserSetName() ? 1 : 0),
+ (channel.IsLocked() ? 1 : 0), channel.IconPath().c_str(), channel.ChannelName().c_str(), 0,
+ (channel.EPGEnabled() ? 1 : 0), channel.EPGScraper().c_str(),
+ static_cast<unsigned int>(channel.LastWatched()), channel.ClientID(), channel.EpgID(),
+ channel.HasArchive(), channel.ClientProviderUid(), channel.IsUserSetHidden() ? 1 : 0);
+ }
+ else
+ {
+ /* update channel */
+ strQuery = PrepareSQL(
+ "REPLACE INTO channels ("
+ "iUniqueId, bIsRadio, bIsHidden, bIsUserSetIcon, bIsUserSetName, bIsLocked, "
+ "sIconPath, sChannelName, bIsVirtual, bEPGEnabled, sEPGScraper, iLastWatched, iClientId, "
+ "idChannel, idEpg, bHasArchive, iClientProviderUid, bIsUserSetHidden) "
+ "VALUES (%i, %i, %i, %i, %i, %i, '%s', '%s', %i, %i, '%s', %u, %i, %s, %i, %i, %i, %i)",
+ channel.UniqueID(), (channel.IsRadio() ? 1 : 0), (channel.IsHidden() ? 1 : 0),
+ (channel.IsUserSetIcon() ? 1 : 0), (channel.IsUserSetName() ? 1 : 0),
+ (channel.IsLocked() ? 1 : 0), channel.ClientIconPath().c_str(),
+ channel.ChannelName().c_str(), 0, (channel.EPGEnabled() ? 1 : 0),
+ channel.EPGScraper().c_str(), static_cast<unsigned int>(channel.LastWatched()),
+ channel.ClientID(), strValue.c_str(), channel.EpgID(), channel.HasArchive(),
+ channel.ClientProviderUid(), channel.IsUserSetHidden() ? 1 : 0);
+ }
+
+ if (QueueInsertQuery(strQuery))
+ {
+ bReturn = true;
+
+ if (bCommit)
+ bReturn = CommitInsertQueries();
+ }
+
+ return bReturn;
+}
+
+bool CPVRDatabase::UpdateLastWatched(const CPVRChannel& channel)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("UPDATE channels SET iLastWatched = %u WHERE idChannel = %i",
+ static_cast<unsigned int>(channel.LastWatched()), channel.ChannelID());
+ return ExecuteQuery(strQuery);
+}
+
+bool CPVRDatabase::UpdateLastWatched(const CPVRChannelGroup& group)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("UPDATE channelgroups SET iLastWatched = %u WHERE idGroup = %i",
+ static_cast<unsigned int>(group.LastWatched()), group.GroupID());
+ return ExecuteQuery(strQuery);
+}
+
+bool CPVRDatabase::UpdateLastOpened(const CPVRChannelGroup& group)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("UPDATE channelgroups SET iLastOpened = %llu WHERE idGroup = %i",
+ group.LastOpened(), group.GroupID());
+ return ExecuteQuery(strQuery);
+}
+
+/********** Timer methods **********/
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRDatabase::GetTimers(
+ CPVRTimers& timers, const std::vector<std::shared_ptr<CPVRClient>>& clients) const
+{
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> result;
+
+ std::string strQuery = "SELECT * FROM timers ";
+ const std::string clientIds = GetClientIdsSQL(clients);
+ if (!clientIds.empty())
+ strQuery += "WHERE " + clientIds;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strQuery = PrepareSQL(strQuery);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ std::shared_ptr<CPVRTimerInfoTag> newTag(new CPVRTimerInfoTag());
+
+ newTag->m_iClientIndex = -m_pDS->fv("iClientIndex").get_asInt();
+ newTag->m_iParentClientIndex = m_pDS->fv("iParentClientIndex").get_asInt();
+ newTag->m_iClientId = m_pDS->fv("iClientId").get_asInt();
+ newTag->SetTimerType(CPVRTimerType::CreateFromIds(m_pDS->fv("iTimerType").get_asInt(), -1));
+ newTag->m_state = static_cast<PVR_TIMER_STATE>(m_pDS->fv("iState").get_asInt());
+ newTag->m_strTitle = m_pDS->fv("sTitle").get_asString().c_str();
+ newTag->m_iClientChannelUid = m_pDS->fv("iClientChannelUid").get_asInt();
+ newTag->m_strSeriesLink = m_pDS->fv("sSeriesLink").get_asString().c_str();
+ newTag->SetStartFromUTC(CDateTime::FromDBDateTime(m_pDS->fv("sStartTime").get_asString().c_str()));
+ newTag->m_bStartAnyTime = m_pDS->fv("bStartAnyTime").get_asBool();
+ newTag->SetEndFromUTC(CDateTime::FromDBDateTime(m_pDS->fv("sEndTime").get_asString().c_str()));
+ newTag->m_bEndAnyTime = m_pDS->fv("bEndAnyTime").get_asBool();
+ newTag->SetFirstDayFromUTC(CDateTime::FromDBDateTime(m_pDS->fv("sFirstDay").get_asString().c_str()));
+ newTag->m_iWeekdays = m_pDS->fv("iWeekdays").get_asInt();
+ newTag->m_iEpgUid = m_pDS->fv("iEpgUid").get_asInt();
+ newTag->m_iMarginStart = m_pDS->fv("iMarginStart").get_asInt();
+ newTag->m_iMarginEnd = m_pDS->fv("iMarginEnd").get_asInt();
+ newTag->m_strEpgSearchString = m_pDS->fv("sEpgSearchString").get_asString().c_str();
+ newTag->m_bFullTextEpgSearch = m_pDS->fv("bFullTextEpgSearch").get_asBool();
+ newTag->m_iPreventDupEpisodes = m_pDS->fv("iPreventDuplicates").get_asInt();
+ newTag->m_iPriority = m_pDS->fv("iPrority").get_asInt();
+ newTag->m_iLifetime = m_pDS->fv("iLifetime").get_asInt();
+ newTag->m_iMaxRecordings = m_pDS->fv("iMaxRecordings").get_asInt();
+ newTag->m_iRecordingGroup = m_pDS->fv("iRecordingGroup").get_asInt();
+ newTag->UpdateSummary();
+
+ result.emplace_back(newTag);
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load timer data from the database");
+ }
+ }
+ return result;
+}
+
+bool CPVRDatabase::Persist(CPVRTimerInfoTag& timer)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // insert a new entry if this is a new timer, or replace the existing one otherwise
+ std::string strQuery;
+ if (timer.m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX)
+ strQuery = PrepareSQL("INSERT INTO timers "
+ "(iParentClientIndex, iClientId, iTimerType, iState, sTitle, iClientChannelUid, sSeriesLink, sStartTime,"
+ " bStartAnyTime, sEndTime, bEndAnyTime, sFirstDay, iWeekdays, iEpgUid, iMarginStart, iMarginEnd,"
+ " sEpgSearchString, bFullTextEpgSearch, iPreventDuplicates, iPrority, iLifetime, iMaxRecordings, iRecordingGroup) "
+ "VALUES (%i, %i, %u, %i, '%s', %i, '%s', '%s', %i, '%s', %i, '%s', %i, %u, %i, %i, '%s', %i, %i, %i, %i, %i, %i);",
+ timer.m_iParentClientIndex, timer.m_iClientId, timer.GetTimerType()->GetTypeId(), timer.m_state,
+ timer.Title().c_str(), timer.m_iClientChannelUid, timer.SeriesLink().c_str(),
+ timer.StartAsUTC().GetAsDBDateTime().c_str(), timer.m_bStartAnyTime ? 1 : 0,
+ timer.EndAsUTC().GetAsDBDateTime().c_str(), timer.m_bEndAnyTime ? 1 : 0,
+ timer.FirstDayAsUTC().GetAsDBDateTime().c_str(), timer.m_iWeekdays, timer.UniqueBroadcastID(),
+ timer.m_iMarginStart, timer.m_iMarginEnd, timer.m_strEpgSearchString.c_str(), timer.m_bFullTextEpgSearch ? 1 : 0,
+ timer.m_iPreventDupEpisodes, timer.m_iPriority, timer.m_iLifetime, timer.m_iMaxRecordings, timer.m_iRecordingGroup);
+ else
+ strQuery = PrepareSQL("REPLACE INTO timers "
+ "(iClientIndex,"
+ " iParentClientIndex, iClientId, iTimerType, iState, sTitle, iClientChannelUid, sSeriesLink, sStartTime,"
+ " bStartAnyTime, sEndTime, bEndAnyTime, sFirstDay, iWeekdays, iEpgUid, iMarginStart, iMarginEnd,"
+ " sEpgSearchString, bFullTextEpgSearch, iPreventDuplicates, iPrority, iLifetime, iMaxRecordings, iRecordingGroup) "
+ "VALUES (%i, %i, %i, %u, %i, '%s', %i, '%s', '%s', %i, '%s', %i, '%s', %i, %u, %i, %i, '%s', %i, %i, %i, %i, %i, %i);",
+ -timer.m_iClientIndex,
+ timer.m_iParentClientIndex, timer.m_iClientId, timer.GetTimerType()->GetTypeId(), timer.m_state,
+ timer.Title().c_str(), timer.m_iClientChannelUid, timer.SeriesLink().c_str(),
+ timer.StartAsUTC().GetAsDBDateTime().c_str(), timer.m_bStartAnyTime ? 1 : 0,
+ timer.EndAsUTC().GetAsDBDateTime().c_str(), timer.m_bEndAnyTime ? 1 : 0,
+ timer.FirstDayAsUTC().GetAsDBDateTime().c_str(), timer.m_iWeekdays, timer.UniqueBroadcastID(),
+ timer.m_iMarginStart, timer.m_iMarginEnd, timer.m_strEpgSearchString.c_str(), timer.m_bFullTextEpgSearch ? 1 : 0,
+ timer.m_iPreventDupEpisodes, timer.m_iPriority, timer.m_iLifetime, timer.m_iMaxRecordings, timer.m_iRecordingGroup);
+
+ bool bReturn = ExecuteQuery(strQuery);
+
+ // set the client index for just inserted timers
+ if (bReturn && timer.m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX)
+ {
+ // index must be negative for local timers!
+ timer.m_iClientIndex = -static_cast<int>(m_pDS->lastinsertid());
+ }
+
+ return bReturn;
+}
+
+bool CPVRDatabase::Delete(const CPVRTimerInfoTag& timer)
+{
+ if (timer.m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX)
+ return false;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting timer '{}' from the database", timer.m_iClientIndex);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("iClientIndex = '%i'", -timer.m_iClientIndex));
+
+ return DeleteValues("timers", filter);
+}
+
+bool CPVRDatabase::DeleteTimers()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting all timers from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("timers");
+}
diff --git a/xbmc/pvr/PVRDatabase.h b/xbmc/pvr/PVRDatabase.h
new file mode 100644
index 0000000..1e9cbf5
--- /dev/null
+++ b/xbmc/pvr/PVRDatabase.h
@@ -0,0 +1,320 @@
+/*
+ * 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 "dbwrappers/Database.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <vector>
+
+namespace PVR
+{
+ class CPVRChannel;
+ class CPVRChannelGroup;
+ class CPVRChannelGroupMember;
+ class CPVRChannelGroups;
+ class CPVRProvider;
+ class CPVRProviders;
+ class CPVRClient;
+ class CPVRTimerInfoTag;
+ class CPVRTimers;
+
+ /** The PVR database */
+
+ static constexpr int CHANNEL_COMMIT_QUERY_COUNT_LIMIT = 10000;
+
+ class CPVRDatabase : public CDatabase
+ {
+ public:
+ /*!
+ * @brief Create a new instance of the PVR database.
+ */
+ CPVRDatabase() = default;
+ ~CPVRDatabase() override = default;
+
+ /*!
+ * @brief Open the database.
+ * @return True if it was opened successfully, false otherwise.
+ */
+ bool Open() override;
+
+ /*!
+ * @brief Close the database.
+ */
+ void Close() override;
+
+ /*!
+ * @brief Lock the database.
+ */
+ void Lock();
+
+ /*!
+ * @brief Unlock the database.
+ */
+ void Unlock();
+
+ /*!
+ * @brief Get the minimal database version that is required to operate correctly.
+ * @return The minimal database version.
+ */
+ int GetSchemaVersion() const override { return 40; }
+
+ /*!
+ * @brief Get the default sqlite database filename.
+ * @return The default filename.
+ */
+ const char* GetBaseDBName() const override { return "TV"; }
+
+ /*! @name Client methods */
+ //@{
+
+ /*!
+ * @brief Remove all client entries from the database.
+ * @return True if all client entries were removed, false otherwise.
+ */
+ bool DeleteClients();
+
+ /*!
+ * @brief Add or update a client entry in the database
+ * @param client The client to persist.
+ * @return True when persisted, false otherwise.
+ */
+ bool Persist(const CPVRClient& client);
+
+ /*!
+ * @brief Remove a client entry from the database
+ * @param client The client to remove.
+ * @return True if the client was removed, false otherwise.
+ */
+ bool Delete(const CPVRClient& client);
+
+ /*!
+ * @brief Get the priority for a given client from the database.
+ * @param client The client.
+ * @return The priority.
+ */
+ int GetPriority(const CPVRClient& client);
+
+ /*! @name Channel methods */
+ //@{
+
+ /*!
+ * @brief Remove all channels from the database.
+ * @return True if all channels were removed, false otherwise.
+ */
+ bool DeleteChannels();
+
+ /*!
+ * @brief Get channels from the database.
+ * @param bRadio Whether to fetch radio or TV channels.
+ * @param clients The PVR clients the channels should be loaded for. Leave empty for all clients.
+ * @param results The container for the channels.
+ * @return The number of channels loaded.
+ */
+ int Get(bool bRadio,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& results) const;
+
+ /*!
+ * @brief Add or update a channel entry in the database
+ * @param channel The channel to persist.
+ * @param bCommit queue only or queue and commit
+ * @return True when persisted or queued, false otherwise.
+ */
+ bool Persist(CPVRChannel& channel, bool bCommit);
+
+ /*!
+ * @brief Remove a channel entry from the database
+ * @param channel The channel to remove.
+ * @return True if the channel was removed, false otherwise.
+ */
+ bool QueueDeleteQuery(const CPVRChannel& channel);
+
+ //@}
+
+ /*! @name Channel group member methods */
+ //@{
+
+ /*!
+ * @brief Remove a channel group member entry from the database
+ * @param groupMember The group member to remove.
+ * @return True if the member was removed, false otherwise.
+ */
+ bool QueueDeleteQuery(const CPVRChannelGroupMember& groupMember);
+
+ //@}
+
+ /*! @name Channel provider methods */
+ //@{
+
+ /*!
+ * @brief Remove all providers from the database.
+ * @return True if all providers were removed, false otherwise.
+ */
+ bool DeleteProviders();
+
+ /*!
+ * @brief Add or update a provider entry in the database
+ * @param provider The provider to persist.
+ * @param updateRecord True if record to be updated, false for insert
+ * @return True when persisted, false otherwise.
+ */
+ bool Persist(CPVRProvider& provider, bool updateRecord = false);
+
+ /*!
+ * @brief Remove a provider entry from the database
+ * @param provider The provider to remove.
+ * @return True if the provider was removed, false otherwise.
+ */
+ bool Delete(const CPVRProvider& provider);
+
+ /*!
+ * @brief Get the list of providers from the database
+ * @param results The providers to store the results in.
+ * @param clients The PVR clients the providers should be loaded for. Leave empty for all clients.
+ * @return The amount of providers that were added.
+ */
+ bool Get(CPVRProviders& results, const std::vector<std::shared_ptr<CPVRClient>>& clients) const;
+
+ /*!
+ * @brief Get the maximum provider id in the database
+ * @return The maximum provider id in the database
+ */
+ int GetMaxProviderId();
+
+ //@}
+
+ /*! @name Channel group methods */
+ //@{
+
+ /*!
+ * @brief Remove all channel groups from the database
+ * @return True if all channel groups were removed.
+ */
+ bool DeleteChannelGroups();
+
+ /*!
+ * @brief Delete a channel group and all its members from the database.
+ * @param group The group to delete.
+ * @return True if the group was deleted successfully, false otherwise.
+ */
+ bool Delete(const CPVRChannelGroup& group);
+
+ /*!
+ * @brief Get the channel groups.
+ * @param results The container to store the results in.
+ * @return The number of groups loaded.
+ */
+ int Get(CPVRChannelGroups& results) const;
+
+ /*!
+ * @brief Get the members of a channel group.
+ * @param group The group to get the members for.
+ * @param clients The PVR clients the group members should be loaded for. Leave empty for all clients.
+ * @return The group members.
+ */
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> Get(
+ const CPVRChannelGroup& group,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients) const;
+
+ /*!
+ * @brief Add or update a channel group entry in the database.
+ * @param group The group to persist.
+ * @return True if the group was persisted successfully, false otherwise.
+ */
+ bool Persist(CPVRChannelGroup& group);
+
+ /*!
+ * @brief Reset all epg ids to 0
+ * @return True when reset, false otherwise.
+ */
+ bool ResetEPG();
+
+ /*! @name Timer methods */
+ //@{
+
+ /*!
+ * @brief Get the timers.
+ * @param timers The container for the timers.
+ * @param clients The PVR clients the timers should be loaded for. Leave empty for all clients.
+ * @return The timers.
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetTimers(
+ CPVRTimers& timers, const std::vector<std::shared_ptr<CPVRClient>>& clients) const;
+
+ /*!
+ * @brief Add or update a timer entry in the database
+ * @param channel The timer to persist.
+ * @return True if persisted, false otherwise.
+ */
+ bool Persist(CPVRTimerInfoTag& timer);
+
+ /*!
+ * @brief Remove a timer from the database
+ * @param timer The timer to remove.
+ * @return True if the timer was removed, false otherwise.
+ */
+ bool Delete(const CPVRTimerInfoTag& timer);
+
+ /*!
+ * @brief Remove all timer entries from the database.
+ * @return True if all timer entries were removed, false otherwise.
+ */
+ bool DeleteTimers();
+ //@}
+
+ /*! @name Client methods */
+ //@{
+
+ /*!
+ * @brief Updates the last watched timestamp for the channel
+ * @param channel the channel
+ * @return whether the update was successful
+ */
+ bool UpdateLastWatched(const CPVRChannel& channel);
+
+ /*!
+ * @brief Updates the last watched timestamp for the channel group
+ * @param group the group
+ * @return whether the update was successful
+ */
+ bool UpdateLastWatched(const CPVRChannelGroup& group);
+ //@}
+
+ /*!
+ * @brief Updates the last opened timestamp for the channel group
+ * @param group the group
+ * @return whether the update was successful
+ */
+ bool UpdateLastOpened(const CPVRChannelGroup& group);
+ //@}
+
+ private:
+ /*!
+ * @brief Create the PVR database tables.
+ */
+ void CreateTables() override;
+ void CreateAnalytics() override;
+ /*!
+ * @brief Update an old version of the database.
+ * @param version The version to update the database from.
+ */
+ void UpdateTables(int version) override;
+ int GetMinSchemaVersion() const override { return 11; }
+
+ bool PersistGroupMembers(const CPVRChannelGroup& group);
+
+ bool PersistChannels(const CPVRChannelGroup& group);
+
+ bool RemoveChannelsFromGroup(const CPVRChannelGroup& group);
+
+ mutable CCriticalSection m_critSection;
+ };
+}
diff --git a/xbmc/pvr/PVREdl.cpp b/xbmc/pvr/PVREdl.cpp
new file mode 100644
index 0000000..72a3ee4
--- /dev/null
+++ b/xbmc/pvr/PVREdl.cpp
@@ -0,0 +1,68 @@
+/*
+ * 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 "PVREdl.h"
+
+#include "FileItem.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_edl.h"
+#include "cores/EdlEdit.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "utils/log.h"
+
+namespace PVR
+{
+
+std::vector<EDL::Edit> CPVREdl::GetEdits(const CFileItem& item)
+{
+ std::vector<PVR_EDL_ENTRY> edl;
+
+ if (item.HasPVRRecordingInfoTag())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Reading EDL for recording: {}",
+ item.GetPVRRecordingInfoTag()->m_strTitle);
+ edl = item.GetPVRRecordingInfoTag()->GetEdl();
+ }
+ else if (item.HasEPGInfoTag())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Reading EDL for EPG tag: {}", item.GetEPGInfoTag()->Title());
+ edl = item.GetEPGInfoTag()->GetEdl();
+ }
+
+ std::vector<EDL::Edit> editlist;
+ for (const auto& entry : edl)
+ {
+ EDL::Edit edit;
+ edit.start = entry.start;
+ edit.end = entry.end;
+
+ switch (entry.type)
+ {
+ case PVR_EDL_TYPE_CUT:
+ edit.action = EDL::Action::CUT;
+ break;
+ case PVR_EDL_TYPE_MUTE:
+ edit.action = EDL::Action::MUTE;
+ break;
+ case PVR_EDL_TYPE_SCENE:
+ edit.action = EDL::Action::SCENE;
+ break;
+ case PVR_EDL_TYPE_COMBREAK:
+ edit.action = EDL::Action::COMM_BREAK;
+ break;
+ default:
+ CLog::LogF(LOGWARNING, "Ignoring entry of unknown EDL type: {}", entry.type);
+ continue;
+ }
+
+ editlist.emplace_back(edit);
+ }
+ return editlist;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVREdl.h b/xbmc/pvr/PVREdl.h
new file mode 100644
index 0000000..7440b08
--- /dev/null
+++ b/xbmc/pvr/PVREdl.h
@@ -0,0 +1,34 @@
+/*
+ * 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 <vector>
+
+class CFileItem;
+
+namespace EDL
+{
+struct Edit;
+}
+
+namespace PVR
+{
+
+class CPVREdl
+{
+public:
+ /*!
+ * @brief Get the EDL edits for the given item.
+ * @param item The item.
+ * @return The EDL edits or an empty vector if no edits exist.
+ */
+ static std::vector<EDL::Edit> GetEdits(const CFileItem& item);
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVREventLogJob.cpp b/xbmc/pvr/PVREventLogJob.cpp
new file mode 100644
index 0000000..b70ad1c
--- /dev/null
+++ b/xbmc/pvr/PVREventLogJob.cpp
@@ -0,0 +1,56 @@
+/*
+ * 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 "PVREventLogJob.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+
+namespace PVR
+{
+
+CPVREventLogJob::CPVREventLogJob(bool bNotifyUser,
+ EventLevel eLevel,
+ const std::string& label,
+ const std::string& msg,
+ const std::string& icon)
+{
+ AddEvent(bNotifyUser, eLevel, label, msg, icon);
+}
+
+void CPVREventLogJob::AddEvent(bool bNotifyUser,
+ EventLevel eLevel,
+ const std::string& label,
+ const std::string& msg,
+ const std::string& icon)
+{
+ m_events.emplace_back(Event(bNotifyUser, eLevel, label, msg, icon));
+}
+
+bool CPVREventLogJob::DoWork()
+{
+ for (const auto& event : m_events)
+ {
+ if (event.m_bNotifyUser)
+ CGUIDialogKaiToast::QueueNotification(event.m_eLevel == EventLevel::Error
+ ? CGUIDialogKaiToast::Error
+ : CGUIDialogKaiToast::Info,
+ event.m_label, event.m_msg, 5000, true);
+
+ // Write event log entry.
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(std::make_shared<CNotificationEvent>(event.m_label, event.m_msg, event.m_icon,
+ event.m_eLevel));
+ }
+ return true;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVREventLogJob.h b/xbmc/pvr/PVREventLogJob.h
new file mode 100644
index 0000000..a5f0be7
--- /dev/null
+++ b/xbmc/pvr/PVREventLogJob.h
@@ -0,0 +1,62 @@
+/*
+ * 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 "events/EventLog.h"
+#include "utils/Job.h"
+
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+class CPVREventLogJob : public CJob
+{
+public:
+ CPVREventLogJob() = default;
+
+ CPVREventLogJob(bool bNotifyUser,
+ EventLevel eLevel,
+ const std::string& label,
+ const std::string& msg,
+ const std::string& icon);
+
+ ~CPVREventLogJob() override = default;
+ const char* GetType() const override { return "pvr-eventlog-job"; }
+
+ void AddEvent(bool bNotifyUser,
+ EventLevel eLevel,
+ const std::string& label,
+ const std::string& msg,
+ const std::string& icon);
+
+ bool DoWork() override;
+
+private:
+ struct Event
+ {
+ bool m_bNotifyUser;
+ EventLevel m_eLevel{EventLevel::Information};
+ std::string m_label;
+ std::string m_msg;
+ std::string m_icon;
+
+ Event(bool bNotifyUser,
+ EventLevel elevel,
+ const std::string& label,
+ const std::string& msg,
+ const std::string& icon)
+ : m_bNotifyUser(bNotifyUser), m_eLevel(elevel), m_label(label), m_msg(msg), m_icon(icon)
+ {
+ }
+ };
+
+ std::vector<Event> m_events;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/PVRItem.cpp b/xbmc/pvr/PVRItem.cpp
new file mode 100644
index 0000000..5eee911
--- /dev/null
+++ b/xbmc/pvr/PVRItem.cpp
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2016-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 "PVRItem.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+
+namespace PVR
+{
+std::shared_ptr<CPVREpgInfoTag> CPVRItem::GetEpgInfoTag() const
+{
+ if (m_item->IsEPG())
+ {
+ return m_item->GetEPGInfoTag();
+ }
+ else if (m_item->IsPVRChannel())
+ {
+ return m_item->GetPVRChannelInfoTag()->GetEPGNow();
+ }
+ else if (m_item->IsPVRTimer())
+ {
+ return m_item->GetPVRTimerInfoTag()->GetEpgInfoTag();
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRItem::GetNextEpgInfoTag() const
+{
+ if (m_item->IsEPG())
+ {
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(
+ m_item->GetEPGInfoTag());
+ if (channel)
+ return channel->GetEPGNext();
+ }
+ else if (m_item->IsPVRChannel())
+ {
+ return m_item->GetPVRChannelInfoTag()->GetEPGNext();
+ }
+ else if (m_item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRChannel> channel = m_item->GetPVRTimerInfoTag()->Channel();
+ if (channel)
+ return channel->GetEPGNext();
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return {};
+}
+
+std::shared_ptr<CPVRChannel> CPVRItem::GetChannel() const
+{
+ if (m_item->IsPVRChannel())
+ {
+ return m_item->GetPVRChannelInfoTag();
+ }
+ else if (m_item->IsEPG())
+ {
+ return CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(
+ m_item->GetEPGInfoTag());
+ }
+ else if (m_item->IsPVRTimer())
+ {
+ return m_item->GetPVRTimerInfoTag()->Channel();
+ }
+ else if (m_item->IsPVRRecording())
+ {
+ return m_item->GetPVRRecordingInfoTag()->Channel();
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return {};
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRItem::GetTimerInfoTag() const
+{
+ if (m_item->IsPVRTimer())
+ {
+ return m_item->GetPVRTimerInfoTag();
+ }
+ else if (m_item->IsEPG())
+ {
+ return CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(m_item->GetEPGInfoTag());
+ }
+ else if (m_item->IsPVRChannel())
+ {
+ return CServiceBroker::GetPVRManager().Timers()->GetActiveTimerForChannel(
+ m_item->GetPVRChannelInfoTag());
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return {};
+}
+
+std::shared_ptr<CPVRRecording> CPVRItem::GetRecording() const
+{
+ if (m_item->IsPVRRecording())
+ {
+ return m_item->GetPVRRecordingInfoTag();
+ }
+ else if (m_item->IsEPG())
+ {
+ return CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(
+ m_item->GetEPGInfoTag());
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return {};
+}
+
+bool CPVRItem::IsRadio() const
+{
+ if (m_item->IsPVRChannel())
+ {
+ return m_item->GetPVRChannelInfoTag()->IsRadio();
+ }
+ else if (m_item->IsEPG())
+ {
+ return m_item->GetEPGInfoTag()->IsRadio();
+ }
+ else if (m_item->IsPVRRecording())
+ {
+ return m_item->GetPVRRecordingInfoTag()->IsRadio();
+ }
+ else if (m_item->IsPVRTimer())
+ {
+ return m_item->GetPVRTimerInfoTag()->IsRadio();
+ }
+ else if (URIUtils::IsPVR(m_item->GetDynPath()))
+ {
+ CLog::LogF(LOGERROR, "Unsupported item type!");
+ }
+ return false;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRItem.h b/xbmc/pvr/PVRItem.h
new file mode 100644
index 0000000..e41ca15
--- /dev/null
+++ b/xbmc/pvr/PVRItem.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016-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 <memory>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVREpgInfoTag;
+class CPVRRecording;
+class CPVRTimerInfoTag;
+
+class CPVRItem
+{
+public:
+ explicit CPVRItem(const std::shared_ptr<CFileItem>& item) : m_item(item.get()) {}
+ explicit CPVRItem(const CFileItem* item) : m_item(item) {}
+ explicit CPVRItem(const CFileItem& item) : m_item(&item) {}
+
+ std::shared_ptr<CPVREpgInfoTag> GetEpgInfoTag() const;
+ std::shared_ptr<CPVREpgInfoTag> GetNextEpgInfoTag() const;
+ std::shared_ptr<CPVRChannel> GetChannel() const;
+ std::shared_ptr<CPVRTimerInfoTag> GetTimerInfoTag() const;
+ std::shared_ptr<CPVRRecording> GetRecording() const;
+
+ bool IsRadio() const;
+
+private:
+ const CFileItem* m_item;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRManager.cpp b/xbmc/pvr/PVRManager.cpp
new file mode 100644
index 0000000..715397e
--- /dev/null
+++ b/xbmc/pvr/PVRManager.cpp
@@ -0,0 +1,1029 @@
+/*
+ * 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 "PVRManager.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRComponentRegistration.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupInternal.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIChannelIconUpdater.h"
+#include "pvr/guilib/PVRGUIProgressHandler.h"
+#include "pvr/guilib/guiinfo/PVRGUIInfo.h"
+#include "pvr/providers/PVRProvider.h"
+#include "pvr/providers/PVRProviders.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/Settings.h"
+#include "utils/JobManager.h"
+#include "utils/Stopwatch.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+using namespace std::chrono_literals;
+
+namespace
+{
+
+class CPVRJob
+{
+public:
+ virtual ~CPVRJob() = default;
+
+ virtual bool DoWork() = 0;
+ virtual const std::string GetType() const = 0;
+
+protected:
+};
+
+template<typename F>
+class CPVRLambdaJob : public CPVRJob
+{
+public:
+ CPVRLambdaJob() = delete;
+ CPVRLambdaJob(const std::string& type, F&& f) : m_type(type), m_f(std::forward<F>(f)) {}
+
+ bool DoWork() override
+ {
+ m_f();
+ return true;
+ }
+
+ const std::string GetType() const override { return m_type; }
+
+private:
+ std::string m_type;
+ F m_f;
+};
+
+} // unnamed namespace
+
+namespace PVR
+{
+
+class CPVRManagerJobQueue
+{
+public:
+ CPVRManagerJobQueue() : m_triggerEvent(false) {}
+
+ void Start();
+ void Stop();
+ void Clear();
+
+ template<typename F>
+ void Append(const std::string& type, F&& f)
+ {
+ AppendJob(new CPVRLambdaJob<F>(type, std::forward<F>(f)));
+ }
+
+ void ExecutePendingJobs();
+
+ bool WaitForJobs(unsigned int milliSeconds)
+ {
+ return m_triggerEvent.Wait(std::chrono::milliseconds(milliSeconds));
+ }
+
+private:
+ void AppendJob(CPVRJob* job);
+
+ CCriticalSection m_critSection;
+ CEvent m_triggerEvent;
+ std::vector<CPVRJob*> m_pendingUpdates;
+ bool m_bStopped = true;
+};
+
+} // namespace PVR
+
+void CPVRManagerJobQueue::Start()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bStopped = false;
+ m_triggerEvent.Set();
+}
+
+void CPVRManagerJobQueue::Stop()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bStopped = true;
+ m_triggerEvent.Reset();
+}
+
+void CPVRManagerJobQueue::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (CPVRJob* updateJob : m_pendingUpdates)
+ delete updateJob;
+
+ m_pendingUpdates.clear();
+ m_triggerEvent.Set();
+}
+
+void CPVRManagerJobQueue::AppendJob(CPVRJob* job)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // check for another pending job of given type...
+ if (std::any_of(m_pendingUpdates.cbegin(), m_pendingUpdates.cend(),
+ [job](CPVRJob* updateJob) { return updateJob->GetType() == job->GetType(); }))
+ {
+ delete job;
+ return;
+ }
+
+ m_pendingUpdates.push_back(job);
+ m_triggerEvent.Set();
+}
+
+void CPVRManagerJobQueue::ExecutePendingJobs()
+{
+ std::vector<CPVRJob*> pendingUpdates;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bStopped)
+ return;
+
+ pendingUpdates = std::move(m_pendingUpdates);
+ m_triggerEvent.Reset();
+ }
+
+ CPVRJob* job = nullptr;
+ while (!pendingUpdates.empty())
+ {
+ job = pendingUpdates.front();
+ pendingUpdates.erase(pendingUpdates.begin());
+
+ job->DoWork();
+ delete job;
+ }
+}
+
+CPVRManager::CPVRManager()
+ : CThread("PVRManager"),
+ m_providers(new CPVRProviders),
+ m_channelGroups(new CPVRChannelGroupsContainer),
+ m_recordings(new CPVRRecordings),
+ m_timers(new CPVRTimers),
+ m_addons(new CPVRClients),
+ m_guiInfo(new CPVRGUIInfo),
+ m_components(new CPVRComponentRegistration),
+ m_epgContainer(m_events),
+ m_pendingUpdates(new CPVRManagerJobQueue),
+ m_database(new CPVRDatabase),
+ m_parentalTimer(new CStopWatch),
+ m_playbackState(new CPVRPlaybackState),
+ m_settings({CSettings::SETTING_PVRPOWERMANAGEMENT_ENABLED,
+ CSettings::SETTING_PVRPOWERMANAGEMENT_SETWAKEUPCMD,
+ CSettings::SETTING_PVRPARENTAL_ENABLED, CSettings::SETTING_PVRPARENTAL_DURATION})
+{
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+ m_actionListener.Init(*this);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "PVR Manager instance created");
+}
+
+CPVRManager::~CPVRManager()
+{
+ m_actionListener.Deinit(*this);
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "PVR Manager instance destroyed");
+}
+
+void CPVRManager::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (!IsStarted())
+ return;
+
+ if ((flag & (ANNOUNCEMENT::GUI)))
+ {
+ if (message == "OnScreensaverActivated")
+ m_addons->OnPowerSavingActivated();
+ else if (message == "OnScreensaverDeactivated")
+ m_addons->OnPowerSavingDeactivated();
+ }
+}
+
+std::shared_ptr<CPVRDatabase> CPVRManager::GetTVDatabase() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_database || !m_database->IsOpen())
+ CLog::LogF(LOGERROR, "Failed to open the PVR database");
+
+ return m_database;
+}
+
+std::shared_ptr<CPVRProviders> CPVRManager::Providers() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_providers;
+}
+
+std::shared_ptr<CPVRChannelGroupsContainer> CPVRManager::ChannelGroups() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelGroups;
+}
+
+std::shared_ptr<CPVRRecordings> CPVRManager::Recordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_recordings;
+}
+
+std::shared_ptr<CPVRTimers> CPVRManager::Timers() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_timers;
+}
+
+std::shared_ptr<CPVRClients> CPVRManager::Clients() const
+{
+ // note: m_addons is const (only set/reset in ctor/dtor). no need for a lock here.
+ return m_addons;
+}
+
+std::shared_ptr<CPVRClient> CPVRManager::GetClient(const CFileItem& item) const
+{
+ int iClientID = PVR_INVALID_CLIENT_ID;
+
+ if (item.HasPVRChannelInfoTag())
+ iClientID = item.GetPVRChannelInfoTag()->ClientID();
+ else if (item.HasPVRRecordingInfoTag())
+ iClientID = item.GetPVRRecordingInfoTag()->ClientID();
+ else if (item.HasPVRTimerInfoTag())
+ iClientID = item.GetPVRTimerInfoTag()->ClientID();
+ else if (item.HasEPGInfoTag())
+ iClientID = item.GetEPGInfoTag()->ClientID();
+ else if (URIUtils::IsPVRChannel(item.GetPath()))
+ {
+ const std::shared_ptr<CPVRChannel> channel = m_channelGroups->GetByPath(item.GetPath());
+ if (channel)
+ iClientID = channel->ClientID();
+ }
+ else if (URIUtils::IsPVRRecording(item.GetPath()))
+ {
+ const std::shared_ptr<CPVRRecording> recording = m_recordings->GetByPath(item.GetPath());
+ if (recording)
+ iClientID = recording->ClientID();
+ }
+ return GetClient(iClientID);
+}
+
+std::shared_ptr<CPVRClient> CPVRManager::GetClient(int iClientId) const
+{
+ return m_addons->GetCreatedClient(iClientId);
+}
+
+std::shared_ptr<CPVRPlaybackState> CPVRManager::PlaybackState() const
+{
+ // note: m_playbackState is const (only set/reset in ctor/dtor). no need for a lock here.
+ return m_playbackState;
+}
+
+CPVREpgContainer& CPVRManager::EpgContainer()
+{
+ // note: m_epgContainer is const (only set/reset in ctor/dtor). no need for a lock here.
+ return m_epgContainer;
+}
+
+void CPVRManager::Clear()
+{
+ m_playbackState->Clear();
+ m_pendingUpdates->Clear();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_guiInfo.reset();
+ m_timers.reset();
+ m_recordings.reset();
+ m_providers.reset();
+ m_channelGroups.reset();
+ m_parentalTimer.reset();
+ m_database.reset();
+
+ m_bEpgsCreated = false;
+}
+
+void CPVRManager::ResetProperties()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ Clear();
+
+ m_database.reset(new CPVRDatabase);
+ m_providers.reset(new CPVRProviders);
+ m_channelGroups.reset(new CPVRChannelGroupsContainer);
+ m_recordings.reset(new CPVRRecordings);
+ m_timers.reset(new CPVRTimers);
+ m_guiInfo.reset(new CPVRGUIInfo);
+ m_parentalTimer.reset(new CStopWatch);
+ m_knownClients.clear();
+}
+
+void CPVRManager::Init()
+{
+ // initial check for enabled addons
+ // if at least one pvr addon is enabled, PVRManager start up
+ CServiceBroker::GetJobManager()->Submit([this] {
+ Clients()->Start();
+ return true;
+ });
+}
+
+void CPVRManager::Start()
+{
+ std::unique_lock<CCriticalSection> initLock(m_startStopMutex);
+
+ // Prevent concurrent starts
+ if (IsInitialising())
+ return;
+
+ // Note: Stop() must not be called while holding pvr manager's mutex. Stop() calls
+ // StopThread() which can deadlock if the worker thread tries to acquire pvr manager's
+ // lock while StopThread() is waiting for the worker to exit. Thus, we introduce another
+ // lock here (m_startStopMutex), which only gets hold while starting/restarting pvr manager.
+ Stop(true);
+
+ if (!m_addons->HasCreatedClients())
+ return;
+
+ CLog::Log(LOGINFO, "PVR Manager: Starting");
+ SetState(ManagerState::STATE_STARTING);
+
+ /* create the pvrmanager thread, which will ensure that all data will be loaded */
+ Create();
+ SetPriority(ThreadPriority::BELOW_NORMAL);
+}
+
+void CPVRManager::Stop(bool bRestart /* = false */)
+{
+ std::unique_lock<CCriticalSection> initLock(m_startStopMutex);
+
+ // Prevent concurrent stops
+ if (IsStopped())
+ return;
+
+ /* stop playback if needed */
+ if (!bRestart && m_playbackState->IsPlaying())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Stopping PVR playback");
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ }
+
+ CLog::Log(LOGINFO, "PVR Manager: Stopping");
+ SetState(ManagerState::STATE_SSTOPPING);
+
+ StopThread();
+}
+
+void CPVRManager::Unload()
+{
+ // stop pvr manager thread and clear all pvr data
+ Stop();
+ Clear();
+}
+
+void CPVRManager::Deinit()
+{
+ SetWakeupCommand();
+ Unload();
+
+ // release addons
+ m_addons.reset();
+}
+
+CPVRManager::ManagerState CPVRManager::GetState() const
+{
+ std::unique_lock<CCriticalSection> lock(m_managerStateMutex);
+ return m_managerState;
+}
+
+void CPVRManager::SetState(CPVRManager::ManagerState state)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_managerStateMutex);
+ if (m_managerState == state)
+ return;
+
+ m_managerState = state;
+ }
+
+ PVREvent event;
+ switch (state)
+ {
+ case ManagerState::STATE_ERROR:
+ event = PVREvent::ManagerError;
+ break;
+ case ManagerState::STATE_STOPPED:
+ event = PVREvent::ManagerStopped;
+ break;
+ case ManagerState::STATE_STARTING:
+ event = PVREvent::ManagerStarting;
+ break;
+ case ManagerState::STATE_SSTOPPING:
+ event = PVREvent::ManagerStopped;
+ break;
+ case ManagerState::STATE_INTERRUPTED:
+ event = PVREvent::ManagerInterrupted;
+ break;
+ case ManagerState::STATE_STARTED:
+ event = PVREvent::ManagerStarted;
+ break;
+ default:
+ return;
+ }
+
+ PublishEvent(event);
+}
+
+void CPVRManager::PublishEvent(PVREvent event)
+{
+ m_events.Publish(event);
+}
+
+void CPVRManager::Process()
+{
+ m_addons->Continue();
+ m_database->Open();
+
+ if (!IsInitialising())
+ {
+ CLog::Log(LOGINFO, "PVR Manager: Start aborted");
+ return;
+ }
+
+ UnloadComponents();
+
+ if (!IsInitialising())
+ {
+ CLog::Log(LOGINFO, "PVR Manager: Start aborted");
+ return;
+ }
+
+ // Wait for at least one client to come up and load/update data
+ UpdateComponents(ManagerState::STATE_STARTING);
+
+ if (!IsInitialising())
+ {
+ CLog::Log(LOGINFO, "PVR Manager: Start aborted");
+ return;
+ }
+
+ // Load EPGs from database.
+ m_epgContainer.Load();
+
+ // Reinit playbackstate
+ m_playbackState->ReInit();
+
+ m_guiInfo->Start();
+ m_epgContainer.Start();
+ m_timers->Start();
+ m_pendingUpdates->Start();
+
+ SetState(ManagerState::STATE_STARTED);
+ CLog::Log(LOGINFO, "PVR Manager: Started");
+
+ bool bRestart(false);
+ XbmcThreads::EndTime<> cachedImagesCleanupTimeout(30s); // first timeout after 30 secs
+
+ while (IsStarted() && m_addons->HasCreatedClients() && !bRestart)
+ {
+ // In case any new client connected, load from db and fetch data update from new client(s)
+ UpdateComponents(ManagerState::STATE_STARTED);
+
+ if (cachedImagesCleanupTimeout.IsTimePast())
+ {
+ // We don't know for sure what to delete if there are not (yet) connected clients
+ if (m_addons->HasIgnoredClients())
+ {
+ cachedImagesCleanupTimeout.Set(10s); // try again in 10 secs
+ }
+ else
+ {
+ // start a job to erase stale texture db entries and image files
+ TriggerCleanupCachedImages();
+ cachedImagesCleanupTimeout.Set(12h); // following timeouts after 12 hours
+ }
+ }
+
+ /* first startup */
+ if (m_bFirstStart)
+ {
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bFirstStart = false;
+ }
+
+ /* start job to search for missing channel icons */
+ TriggerSearchMissingChannelIcons();
+
+ /* try to play channel on startup */
+ TriggerPlayChannelOnStartup();
+ }
+
+ if (m_addons->AnyClientSupportingRecordingsSize())
+ TriggerRecordingsSizeInProgressUpdate();
+
+ /* execute the next pending jobs if there are any */
+ try
+ {
+ m_pendingUpdates->ExecutePendingJobs();
+ }
+ catch (...)
+ {
+ CLog::LogF(
+ LOGERROR,
+ "An error occurred while trying to execute the last PVR update job, trying to recover");
+ bRestart = true;
+ }
+
+ if (IsStarted() && !bRestart)
+ m_pendingUpdates->WaitForJobs(1000);
+ }
+
+ m_addons->Stop();
+ m_pendingUpdates->Stop();
+ m_timers->Stop();
+ m_epgContainer.Stop();
+ m_guiInfo->Stop();
+
+ SetState(ManagerState::STATE_INTERRUPTED);
+
+ UnloadComponents();
+ m_database->Close();
+
+ ResetProperties();
+
+ CLog::Log(LOGINFO, "PVR Manager: Stopped");
+ SetState(ManagerState::STATE_STOPPED);
+}
+
+bool CPVRManager::SetWakeupCommand()
+{
+#if !defined(TARGET_DARWIN_EMBEDDED) && !defined(TARGET_WINDOWS_STORE)
+ if (!m_settings.GetBoolValue(CSettings::SETTING_PVRPOWERMANAGEMENT_ENABLED))
+ return false;
+
+ const std::string strWakeupCommand(
+ m_settings.GetStringValue(CSettings::SETTING_PVRPOWERMANAGEMENT_SETWAKEUPCMD));
+ if (!strWakeupCommand.empty() && m_timers)
+ {
+ const CDateTime nextEvent = m_timers->GetNextEventTime();
+ if (nextEvent.IsValid())
+ {
+ time_t iWakeupTime;
+ nextEvent.GetAsTime(iWakeupTime);
+
+ std::string strExecCommand = StringUtils::Format("{} {}", strWakeupCommand, iWakeupTime);
+
+ const int iReturn = system(strExecCommand.c_str());
+ if (iReturn != 0)
+ CLog::LogF(LOGERROR, "PVR Manager failed to execute wakeup command '{}': {} ({})",
+ strExecCommand, strerror(iReturn), iReturn);
+
+ return iReturn == 0;
+ }
+ }
+#endif
+ return false;
+}
+
+void CPVRManager::OnSleep()
+{
+ PublishEvent(PVREvent::SystemSleep);
+
+ SetWakeupCommand();
+
+ m_epgContainer.OnSystemSleep();
+
+ m_addons->OnSystemSleep();
+}
+
+void CPVRManager::OnWake()
+{
+ m_addons->OnSystemWake();
+
+ m_epgContainer.OnSystemWake();
+
+ PublishEvent(PVREvent::SystemWake);
+
+ /* start job to search for missing channel icons */
+ TriggerSearchMissingChannelIcons();
+
+ /* try to play channel on startup */
+ TriggerPlayChannelOnStartup();
+
+ /* trigger PVR data updates */
+ TriggerChannelGroupsUpdate();
+ TriggerProvidersUpdate();
+ TriggerChannelsUpdate();
+ TriggerRecordingsUpdate();
+ TriggerEpgsCreate();
+ TriggerTimersUpdate();
+}
+
+void CPVRManager::UpdateComponents(ManagerState stateToCheck)
+{
+ XbmcThreads::EndTime<> progressTimeout(30s);
+ std::unique_ptr<CPVRGUIProgressHandler> progressHandler(
+ new CPVRGUIProgressHandler(g_localizeStrings.Get(19235))); // PVR manager is starting up
+
+ // Wait for at least one client to come up and load/update data
+ while (!UpdateComponents(stateToCheck, progressHandler) && m_addons->HasCreatedClients() &&
+ (stateToCheck == GetState()))
+ {
+ CThread::Sleep(1000ms);
+
+ if (progressTimeout.IsTimePast())
+ progressHandler.reset();
+ }
+}
+
+bool CPVRManager::UpdateComponents(ManagerState stateToCheck,
+ const std::unique_ptr<CPVRGUIProgressHandler>& progressHandler)
+{
+ // find clients which appeared since last check and update them
+ const CPVRClientMap clientMap = m_addons->GetCreatedClients();
+ if (clientMap.empty())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "All created PVR clients gone!");
+ m_knownClients.clear(); // start over
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return false;
+ }
+
+ std::vector<std::shared_ptr<CPVRClient>> newClients;
+ for (const auto& entry : clientMap)
+ {
+ // skip not (yet) connected clients
+ if (entry.second->IgnoreClient())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Skipping not (yet) connected PVR client '{}'",
+ entry.second->ID());
+ continue;
+ }
+
+ if (!IsKnownClient(entry.first))
+ {
+ m_knownClients.emplace_back(entry.second);
+ newClients.emplace_back(entry.second);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Adding new PVR client '{}' to list of known clients",
+ entry.second->ID());
+ }
+ }
+
+ if (newClients.empty())
+ return !m_knownClients.empty();
+
+ // Load all channels and groups
+ if (progressHandler)
+ progressHandler->UpdateProgress(g_localizeStrings.Get(19236), 0); // Loading channels and groups
+
+ if (!m_providers->Update(newClients) || (stateToCheck != GetState()))
+ {
+ CLog::LogF(LOGERROR, "Failed to load PVR providers.");
+ m_knownClients.clear(); // start over
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return false;
+ }
+
+ if (!m_channelGroups->Update(newClients) || (stateToCheck != GetState()))
+ {
+ CLog::LogF(LOGERROR, "Failed to load PVR channels / groups.");
+ m_knownClients.clear(); // start over
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return false;
+ }
+
+ // Load all timers
+ if (progressHandler)
+ progressHandler->UpdateProgress(g_localizeStrings.Get(19237), 50); // Loading timers
+
+ if (!m_timers->Update(newClients) || (stateToCheck != GetState()))
+ {
+ CLog::LogF(LOGERROR, "Failed to load PVR timers.");
+ m_knownClients.clear(); // start over
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return false;
+ }
+
+ // Load all recordings
+ if (progressHandler)
+ progressHandler->UpdateProgress(g_localizeStrings.Get(19238), 75); // Loading recordings
+
+ if (!m_recordings->Update(newClients) || (stateToCheck != GetState()))
+ {
+ CLog::LogF(LOGERROR, "Failed to load PVR recordings.");
+ m_knownClients.clear(); // start over
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return false;
+ }
+
+ // reinit playbackstate as new client may provide new last opened group / last played channel
+ m_playbackState->ReInit();
+
+ PublishEvent(PVREvent::ClientsInvalidated);
+ return true;
+}
+
+void CPVRManager::UnloadComponents()
+{
+ m_recordings->Unload();
+ m_timers->Unload();
+ m_channelGroups->Unload();
+ m_providers->Unload();
+ m_epgContainer.Unload();
+}
+
+bool CPVRManager::IsKnownClient(int clientID) const
+{
+ return std::any_of(m_knownClients.cbegin(), m_knownClients.cend(),
+ [clientID](const auto& client) { return client->GetID() == clientID; });
+}
+
+void CPVRManager::TriggerPlayChannelOnStartup()
+{
+ if (IsStarted())
+ {
+ CServiceBroker::GetJobManager()->Submit(
+ [this] { return Get<PVR::GUI::Playback>().PlayChannelOnStartup(); });
+ }
+}
+
+void CPVRManager::RestartParentalTimer()
+{
+ if (m_parentalTimer)
+ m_parentalTimer->StartZero();
+}
+
+bool CPVRManager::IsParentalLocked(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ return m_channelGroups && epgTag &&
+ IsCurrentlyParentalLocked(
+ m_channelGroups->GetByUniqueID(epgTag->UniqueChannelID(), epgTag->ClientID()),
+ epgTag->IsParentalLocked());
+}
+
+bool CPVRManager::IsParentalLocked(const std::shared_ptr<CPVRChannel>& channel) const
+{
+ return channel && IsCurrentlyParentalLocked(channel, channel->IsLocked());
+}
+
+bool CPVRManager::IsCurrentlyParentalLocked(const std::shared_ptr<CPVRChannel>& channel,
+ bool bGenerallyLocked) const
+{
+ bool bReturn = false;
+
+ if (!channel || !bGenerallyLocked)
+ return bReturn;
+
+ const std::shared_ptr<CPVRChannel> currentChannel = m_playbackState->GetPlayingChannel();
+
+ if ( // if channel in question is currently playing it must be currently unlocked.
+ (!currentChannel || channel != currentChannel) &&
+ // parental control enabled
+ m_settings.GetBoolValue(CSettings::SETTING_PVRPARENTAL_ENABLED))
+ {
+ float parentalDurationMs =
+ m_settings.GetIntValue(CSettings::SETTING_PVRPARENTAL_DURATION) * 1000.0f;
+ bReturn = m_parentalTimer && (!m_parentalTimer->IsRunning() ||
+ m_parentalTimer->GetElapsedMilliseconds() > parentalDurationMs);
+ }
+
+ return bReturn;
+}
+
+void CPVRManager::OnPlaybackStarted(const CFileItem& item)
+{
+ m_playbackState->OnPlaybackStarted(item);
+ Get<PVR::GUI::Channels>().OnPlaybackStarted(item);
+ m_epgContainer.OnPlaybackStarted();
+}
+
+void CPVRManager::OnPlaybackStopped(const CFileItem& item)
+{
+ // Playback ended due to user interaction
+ if (m_playbackState->OnPlaybackStopped(item))
+ PublishEvent(PVREvent::ChannelPlaybackStopped);
+
+ Get<PVR::GUI::Channels>().OnPlaybackStopped(item);
+ m_epgContainer.OnPlaybackStopped();
+}
+
+void CPVRManager::OnPlaybackEnded(const CFileItem& item)
+{
+ // Playback ended, but not due to user interaction
+ OnPlaybackStopped(item);
+}
+
+void CPVRManager::LocalizationChanged()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (IsStarted())
+ {
+ static_cast<CPVRChannelGroupInternal*>(m_channelGroups->GetGroupAllRadio().get())
+ ->CheckGroupName();
+ static_cast<CPVRChannelGroupInternal*>(m_channelGroups->GetGroupAllTV().get())
+ ->CheckGroupName();
+ }
+}
+
+bool CPVRManager::EpgsCreated() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bEpgsCreated;
+}
+
+void CPVRManager::TriggerEpgsCreate()
+{
+ m_pendingUpdates->Append("pvr-create-epgs", [this]() { return CreateChannelEpgs(); });
+}
+
+void CPVRManager::TriggerRecordingsSizeInProgressUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-recordings-size",
+ [this]() { return Recordings()->UpdateInProgressSize(); });
+}
+
+void CPVRManager::TriggerRecordingsUpdate(int clientId)
+{
+ m_pendingUpdates->Append("pvr-update-recordings-" + std::to_string(clientId), [this, clientId]() {
+ if (!IsKnownClient(clientId))
+ return;
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ Recordings()->UpdateFromClients({client});
+ });
+}
+
+void CPVRManager::TriggerRecordingsUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-recordings",
+ [this]() { Recordings()->UpdateFromClients({}); });
+}
+
+void CPVRManager::TriggerTimersUpdate(int clientId)
+{
+ m_pendingUpdates->Append("pvr-update-timers-" + std::to_string(clientId), [this, clientId]() {
+ if (!IsKnownClient(clientId))
+ return;
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ Timers()->UpdateFromClients({client});
+ });
+}
+
+void CPVRManager::TriggerTimersUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-timers", [this]() { Timers()->UpdateFromClients({}); });
+}
+
+void CPVRManager::TriggerProvidersUpdate(int clientId)
+{
+ m_pendingUpdates->Append("pvr-update-channel-providers-" + std::to_string(clientId),
+ [this, clientId]() {
+ if (!IsKnownClient(clientId))
+ return;
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ Providers()->UpdateFromClients({client});
+ });
+}
+
+void CPVRManager::TriggerProvidersUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-channel-providers",
+ [this]() { Providers()->UpdateFromClients({}); });
+}
+
+void CPVRManager::TriggerChannelsUpdate(int clientId)
+{
+ m_pendingUpdates->Append("pvr-update-channels-" + std::to_string(clientId), [this, clientId]() {
+ if (!IsKnownClient(clientId))
+ return;
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ ChannelGroups()->UpdateFromClients({client}, true);
+ });
+}
+
+void CPVRManager::TriggerChannelsUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-channels",
+ [this]() { ChannelGroups()->UpdateFromClients({}, true); });
+}
+
+void CPVRManager::TriggerChannelGroupsUpdate(int clientId)
+{
+ m_pendingUpdates->Append("pvr-update-channelgroups-" + std::to_string(clientId),
+ [this, clientId]() {
+ if (!IsKnownClient(clientId))
+ return;
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ ChannelGroups()->UpdateFromClients({client}, false);
+ });
+}
+
+void CPVRManager::TriggerChannelGroupsUpdate()
+{
+ m_pendingUpdates->Append("pvr-update-channelgroups",
+ [this]() { ChannelGroups()->UpdateFromClients({}, false); });
+}
+
+void CPVRManager::TriggerSearchMissingChannelIcons()
+{
+ m_pendingUpdates->Append("pvr-search-missing-channel-icons", [this]() {
+ CPVRGUIChannelIconUpdater updater(
+ {ChannelGroups()->GetGroupAllTV(), ChannelGroups()->GetGroupAllRadio()}, true);
+ updater.SearchAndUpdateMissingChannelIcons();
+ return true;
+ });
+}
+
+void CPVRManager::TriggerSearchMissingChannelIcons(const std::shared_ptr<CPVRChannelGroup>& group)
+{
+ m_pendingUpdates->Append("pvr-search-missing-channel-icons-" + std::to_string(group->GroupID()),
+ [group]() {
+ CPVRGUIChannelIconUpdater updater({group}, false);
+ updater.SearchAndUpdateMissingChannelIcons();
+ return true;
+ });
+}
+
+void CPVRManager::TriggerCleanupCachedImages()
+{
+ m_pendingUpdates->Append("pvr-cleanup-cached-images", [this]() {
+ int iCleanedImages = 0;
+ CLog::Log(LOGINFO, "PVR Manager: Starting cleanup of cached images.");
+ iCleanedImages += Recordings()->CleanupCachedImages();
+ iCleanedImages += ChannelGroups()->CleanupCachedImages();
+ iCleanedImages += Providers()->CleanupCachedImages();
+ iCleanedImages += EpgContainer().CleanupCachedImages();
+ CLog::Log(LOGINFO, "PVR Manager: Cleaned up {} cached images.", iCleanedImages);
+ return true;
+ });
+}
+
+void CPVRManager::ConnectionStateChange(CPVRClient* client,
+ const std::string& connectString,
+ PVR_CONNECTION_STATE state,
+ const std::string& message)
+{
+ CServiceBroker::GetJobManager()->Submit([this, client, connectString, state, message] {
+ Clients()->ConnectionStateChange(client, connectString, state, message);
+ return true;
+ });
+}
+
+bool CPVRManager::CreateChannelEpgs()
+{
+ if (EpgsCreated())
+ return true;
+
+ bool bEpgsCreated = m_channelGroups->CreateChannelEpgs();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bEpgsCreated = bEpgsCreated;
+ return m_bEpgsCreated;
+}
diff --git a/xbmc/pvr/PVRManager.h b/xbmc/pvr/PVRManager.h
new file mode 100644
index 0000000..06bcb44
--- /dev/null
+++ b/xbmc/pvr/PVRManager.h
@@ -0,0 +1,482 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h"
+#include "interfaces/IAnnouncer.h"
+#include "pvr/PVRComponentRegistration.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/guilib/PVRGUIActionListener.h"
+#include "pvr/settings/PVRSettings.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "utils/EventStream.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CStopWatch;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRChannelGroup;
+class CPVRChannelGroupsContainer;
+class CPVRProviders;
+class CPVRClient;
+class CPVRClients;
+class CPVRDatabase;
+class CPVRGUIInfo;
+class CPVRGUIProgressHandler;
+class CPVRManagerJobQueue;
+class CPVRPlaybackState;
+class CPVRRecording;
+class CPVRRecordings;
+class CPVRTimers;
+
+enum class PVREvent
+{
+ // PVR Manager states
+ ManagerError = 0,
+ ManagerStopped,
+ ManagerStarting,
+ ManagerStopping,
+ ManagerInterrupted,
+ ManagerStarted,
+
+ // Channel events
+ ChannelPlaybackStopped,
+
+ // Channel group events
+ ChannelGroup,
+ ChannelGroupInvalidated,
+ ChannelGroupsInvalidated,
+
+ // Recording events
+ RecordingsInvalidated,
+
+ // Timer events
+ AnnounceReminder,
+ Timers,
+ TimersInvalidated,
+
+ // Client events
+ ClientsInvalidated,
+
+ // EPG events
+ Epg,
+ EpgActiveItem,
+ EpgContainer,
+ EpgItemUpdate,
+ EpgUpdatePending,
+ EpgDeleted,
+
+ // Saved searches events
+ SavedSearchesInvalidated,
+
+ // Item events
+ CurrentItem,
+
+ // System events
+ SystemSleep,
+ SystemWake,
+};
+
+class CPVRManager : private CThread, public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ /*!
+ * @brief Create a new CPVRManager instance, which handles all PVR related operations in XBMC.
+ */
+ CPVRManager();
+
+ /*!
+ * @brief Stop the PVRManager and destroy all objects it created.
+ */
+ ~CPVRManager() override;
+
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ /*!
+ * @brief Get a PVR component.
+ * @return The component.
+ */
+ template<class T>
+ T& Get()
+ {
+ return *m_components->GetComponent<T>();
+ }
+
+ /*!
+ * @brief Get the providers container.
+ * @return The providers container.
+ */
+ std::shared_ptr<CPVRProviders> Providers() const;
+
+ /*!
+ * @brief Get the channel groups container.
+ * @return The groups container.
+ */
+ std::shared_ptr<CPVRChannelGroupsContainer> ChannelGroups() const;
+
+ /*!
+ * @brief Get the recordings container.
+ * @return The recordings container.
+ */
+ std::shared_ptr<CPVRRecordings> Recordings() const;
+
+ /*!
+ * @brief Get the timers container.
+ * @return The timers container.
+ */
+ std::shared_ptr<CPVRTimers> Timers() const;
+
+ /*!
+ * @brief Get the timers container.
+ * @return The timers container.
+ */
+ std::shared_ptr<CPVRClients> Clients() const;
+
+ /*!
+ * @brief Get the instance of a client that matches the given item.
+ * @param item The item containing a PVR recording, a PVR channel, a PVR timer or a PVR EPG event.
+ * @return the requested client on success, nullptr otherwise.
+ */
+ std::shared_ptr<CPVRClient> GetClient(const CFileItem& item) const;
+
+ /*!
+ * @brief Get the instance of a client that matches the given id.
+ * @param iClientId The id of a PVR client.
+ * @return the requested client on success, nullptr otherwise.
+ */
+ std::shared_ptr<CPVRClient> GetClient(int iClientId) const;
+
+ /*!
+ * @brief Get access to the pvr playback state.
+ * @return The playback state.
+ */
+ std::shared_ptr<CPVRPlaybackState> PlaybackState() const;
+
+ /*!
+ * @brief Get access to the epg container.
+ * @return The epg container.
+ */
+ CPVREpgContainer& EpgContainer();
+
+ /*!
+ * @brief Init PVRManager.
+ */
+ void Init();
+
+ /*!
+ * @brief Start the PVRManager, which loads all PVR data and starts some threads to update the PVR data.
+ */
+ void Start();
+
+ /*!
+ * @brief Stop PVRManager.
+ */
+ void Stop(bool bRestart = false);
+
+ /*!
+ * @brief Stop PVRManager, unload data.
+ */
+ void Unload();
+
+ /*!
+ * @brief Deinit PVRManager, unload data, unload addons.
+ */
+ void Deinit();
+
+ /*!
+ * @brief Propagate event on system sleep
+ */
+ void OnSleep();
+
+ /*!
+ * @brief Propagate event on system wake
+ */
+ void OnWake();
+
+ /*!
+ * @brief Get the TV database.
+ * @return The TV database.
+ */
+ std::shared_ptr<CPVRDatabase> GetTVDatabase() const;
+
+ /*!
+ * @brief Check whether the PVRManager has fully started.
+ * @return True if started, false otherwise.
+ */
+ bool IsStarted() const { return GetState() == ManagerState::STATE_STARTED; }
+
+ /*!
+ * @brief Check whether EPG tags for channels have been created.
+ * @return True if EPG tags have been created, false otherwise.
+ */
+ bool EpgsCreated() const;
+
+ /*!
+ * @brief Inform PVR manager that playback of an item just started.
+ * @param item The item that started to play.
+ */
+ void OnPlaybackStarted(const CFileItem& item);
+
+ /*!
+ * @brief Inform PVR manager that playback of an item was stopped due to user interaction.
+ * @param item The item that stopped to play.
+ */
+ void OnPlaybackStopped(const CFileItem& item);
+
+ /*!
+ * @brief Inform PVR manager that playback of an item has stopped without user interaction.
+ * @param item The item that ended to play.
+ */
+ void OnPlaybackEnded(const CFileItem& item);
+
+ /*!
+ * @brief Let the background thread create epg tags for all channels.
+ */
+ void TriggerEpgsCreate();
+
+ /*!
+ * @brief Let the background thread update the recordings list.
+ * @param clientId The id of the PVR client to update.
+ */
+ void TriggerRecordingsUpdate(int clientId);
+ void TriggerRecordingsUpdate();
+
+ /*!
+ * @brief Let the background thread update the size for any in progress recordings.
+ */
+ void TriggerRecordingsSizeInProgressUpdate();
+
+ /*!
+ * @brief Let the background thread update the timer list.
+ * @param clientId The id of the PVR client to update.
+ */
+ void TriggerTimersUpdate(int clientId);
+ void TriggerTimersUpdate();
+
+ /*!
+ * @brief Let the background thread update the channel list.
+ * @param clientId The id of the PVR client to update.
+ */
+ void TriggerChannelsUpdate(int clientId);
+ void TriggerChannelsUpdate();
+
+ /*!
+ * @brief Let the background thread update the provider list.
+ * @param clientId The id of the PVR client to update.
+ */
+ void TriggerProvidersUpdate(int clientId);
+ void TriggerProvidersUpdate();
+
+ /*!
+ * @brief Let the background thread update the channel groups list.
+ * @param clientId The id of the PVR client to update.
+ */
+ void TriggerChannelGroupsUpdate(int clientId);
+ void TriggerChannelGroupsUpdate();
+
+ /*!
+ * @brief Let the background thread search for all missing channel icons.
+ */
+ void TriggerSearchMissingChannelIcons();
+
+ /*!
+ * @brief Let the background thread erase stale texture db entries and image files.
+ */
+ void TriggerCleanupCachedImages();
+
+ /*!
+ * @brief Let the background thread search for missing channel icons for channels contained in the given group.
+ * @param group The channel group.
+ */
+ void TriggerSearchMissingChannelIcons(const std::shared_ptr<CPVRChannelGroup>& group);
+
+ /*!
+ * @brief Check whether names are still correct after the language settings changed.
+ */
+ void LocalizationChanged();
+
+ /*!
+ * @brief Check if parental lock is overridden at the given moment.
+ * @param channel The channel to check.
+ * @return True if parental lock is overridden, false otherwise.
+ */
+ bool IsParentalLocked(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Check if parental lock is overridden at the given moment.
+ * @param epgTag The epg tag to check.
+ * @return True if parental lock is overridden, false otherwise.
+ */
+ bool IsParentalLocked(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ /*!
+ * @brief Restart the parental timer.
+ */
+ void RestartParentalTimer();
+
+ /*!
+ * @brief Create EPG tags for all channels in internal channel groups
+ * @return True if EPG tags where created successfully, false otherwise
+ */
+ bool CreateChannelEpgs();
+
+ /*!
+ * @brief Signal a connection change of a client
+ */
+ void ConnectionStateChange(CPVRClient* client,
+ const std::string& connectString,
+ PVR_CONNECTION_STATE state,
+ const std::string& message);
+
+ /*!
+ * @brief Query the events available for CEventStream
+ */
+ CEventStream<PVREvent>& Events() { return m_events; }
+
+ /*!
+ * @brief Publish an event
+ * @param state the event
+ */
+ void PublishEvent(PVREvent state);
+
+protected:
+ /*!
+ * @brief PVR update and control thread.
+ */
+ void Process() override;
+
+private:
+ /*!
+ * @brief Executes "pvrpowermanagement.setwakeupcmd"
+ */
+ bool SetWakeupCommand();
+
+ enum class ManagerState
+ {
+ STATE_ERROR = 0,
+ STATE_STOPPED,
+ STATE_STARTING,
+ STATE_SSTOPPING,
+ STATE_INTERRUPTED,
+ STATE_STARTED
+ };
+
+ /*!
+ * @return True while the PVRManager is initialising.
+ */
+ bool IsInitialising() const { return GetState() == ManagerState::STATE_STARTING; }
+
+ /*!
+ * @brief Check whether the PVRManager has been stopped.
+ * @return True if stopped, false otherwise.
+ */
+ bool IsStopped() const { return GetState() == ManagerState::STATE_STOPPED; }
+
+ /*!
+ * @brief Get the current state of the PVR manager.
+ * @return the state.
+ */
+ ManagerState GetState() const;
+
+ /*!
+ * @brief Set the current state of the PVR manager.
+ * @param state the new state.
+ */
+ void SetState(ManagerState state);
+
+ /*!
+ * @brief Wait until at least one client is up. Update all data from database and the given PVR clients.
+ * @param stateToCheck Required state of the PVR manager while this method gets called.
+ */
+ void UpdateComponents(ManagerState stateToCheck);
+
+ /*!
+ * @brief Update all data from database and the given PVR clients.
+ * @param stateToCheck Required state of the PVR manager while this method gets called.
+ * @param progressHandler The progress handler to use for showing the different stages.
+ * @return True if at least one client is known and successfully loaded, false otherwise.
+ */
+ bool UpdateComponents(ManagerState stateToCheck,
+ const std::unique_ptr<CPVRGUIProgressHandler>& progressHandler);
+
+ /*!
+ * @brief Unload all PVR data (recordings, timers, channelgroups).
+ */
+ void UnloadComponents();
+
+ /*!
+ * @brief Check whether the given client id belongs to a known client.
+ * @return True if the client is known, false otherwise.
+ */
+ bool IsKnownClient(int clientID) const;
+
+ /*!
+ * @brief Reset all properties.
+ */
+ void ResetProperties();
+
+ /*!
+ * @brief Destroy PVRManager's objects.
+ */
+ void Clear();
+
+ /*!
+ * @brief Continue playback on the last played channel.
+ */
+ void TriggerPlayChannelOnStartup();
+
+ bool IsCurrentlyParentalLocked(const std::shared_ptr<CPVRChannel>& channel,
+ bool bGenerallyLocked) const;
+
+ CEventSource<PVREvent> m_events;
+
+ /** @name containers */
+ //@{
+ std::shared_ptr<CPVRProviders> m_providers; /*!< pointer to the providers container */
+ std::shared_ptr<CPVRChannelGroupsContainer>
+ m_channelGroups; /*!< pointer to the channel groups container */
+ std::shared_ptr<CPVRRecordings> m_recordings; /*!< pointer to the recordings container */
+ std::shared_ptr<CPVRTimers> m_timers; /*!< pointer to the timers container */
+ std::shared_ptr<CPVRClients> m_addons; /*!< pointer to the pvr addon container */
+ std::unique_ptr<CPVRGUIInfo> m_guiInfo; /*!< pointer to the guiinfo data */
+ std::shared_ptr<CPVRComponentRegistration> m_components; /*!< pointer to the PVR components */
+ CPVREpgContainer m_epgContainer; /*!< the epg container */
+ //@}
+
+ std::vector<std::shared_ptr<CPVRClient>> m_knownClients; /*!< vector with all known clients */
+ std::unique_ptr<CPVRManagerJobQueue> m_pendingUpdates; /*!< vector of pending pvr updates */
+ std::shared_ptr<CPVRDatabase> m_database; /*!< the database for all PVR related data */
+ mutable CCriticalSection
+ m_critSection; /*!< critical section for all changes to this class, except for changes to triggers */
+ bool m_bFirstStart = true; /*!< true when the PVR manager was started first, false otherwise */
+ bool m_bEpgsCreated = false; /*!< true if epg data for channels has been created */
+
+ mutable CCriticalSection m_managerStateMutex;
+ ManagerState m_managerState = ManagerState::STATE_STOPPED;
+ std::unique_ptr<CStopWatch> m_parentalTimer;
+
+ CCriticalSection
+ m_startStopMutex; // mutex for protecting pvr manager's start/restart/stop sequence */
+
+ const std::shared_ptr<CPVRPlaybackState> m_playbackState;
+ CPVRGUIActionListener m_actionListener;
+ CPVRSettings m_settings;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/PVRPlaybackState.cpp b/xbmc/pvr/PVRPlaybackState.cpp
new file mode 100644
index 0000000..56bbd5b
--- /dev/null
+++ b/xbmc/pvr/PVRPlaybackState.cpp
@@ -0,0 +1,566 @@
+/*
+ * 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 "PVRPlaybackState.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "cores/DataCacheCore.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/Timer.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <mutex>
+
+using namespace PVR;
+
+class CPVRPlaybackState::CLastWatchedUpdateTimer : public CTimer, private ITimerCallback
+{
+public:
+ CLastWatchedUpdateTimer(CPVRPlaybackState& state,
+ const std::shared_ptr<CPVRChannelGroupMember>& channel,
+ const CDateTime& time)
+ : CTimer(this), m_state(state), m_channel(channel), m_time(time)
+ {
+ }
+
+ // ITimerCallback implementation
+ void OnTimeout() override { m_state.UpdateLastWatched(m_channel, m_time); }
+
+private:
+ CLastWatchedUpdateTimer() = delete;
+
+ CPVRPlaybackState& m_state;
+ const std::shared_ptr<CPVRChannelGroupMember> m_channel;
+ const CDateTime m_time;
+};
+
+CPVRPlaybackState::CPVRPlaybackState() = default;
+
+CPVRPlaybackState::~CPVRPlaybackState() = default;
+
+void CPVRPlaybackState::ReInit()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Clear();
+
+ if (m_playingClientId != -1)
+ {
+ if (m_playingChannelUniqueId != -1)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetByIdFromAll(m_playingGroupId);
+ if (group)
+ m_playingChannel = group->GetByUniqueID({m_playingClientId, m_playingChannelUniqueId});
+ else
+ CLog::LogFC(LOGERROR, LOGPVR, "Failed to obtain group with id '{}'", m_playingGroupId);
+ }
+ else if (!m_strPlayingRecordingUniqueId.empty())
+ {
+ m_playingRecording = CServiceBroker::GetPVRManager().Recordings()->GetById(
+ m_playingClientId, m_strPlayingRecordingUniqueId);
+ }
+ else if (m_playingEpgTagChannelUniqueId != -1 && m_playingEpgTagUniqueId != 0)
+ {
+ const std::shared_ptr<CPVREpg> epg =
+ CServiceBroker::GetPVRManager().EpgContainer().GetByChannelUid(
+ m_playingClientId, m_playingEpgTagChannelUniqueId);
+ if (epg)
+ m_playingEpgTag = epg->GetTagByBroadcastId(m_playingEpgTagUniqueId);
+ }
+ }
+
+ const std::shared_ptr<CPVRChannelGroupsContainer> groups =
+ CServiceBroker::GetPVRManager().ChannelGroups();
+ const CPVRChannelGroups* groupsTV = groups->GetTV();
+ const CPVRChannelGroups* groupsRadio = groups->GetRadio();
+
+ m_activeGroupTV = groupsTV->GetLastOpenedGroup();
+ m_activeGroupRadio = groupsRadio->GetLastOpenedGroup();
+ if (!m_activeGroupTV)
+ m_activeGroupTV = groupsTV->GetGroupAll();
+ if (!m_activeGroupRadio)
+ m_activeGroupRadio = groupsRadio->GetGroupAll();
+
+ GroupMemberPair lastPlayed = groupsTV->GetLastAndPreviousToLastPlayedChannelGroupMember();
+ m_lastPlayedChannelTV = lastPlayed.first;
+ m_previousToLastPlayedChannelTV = lastPlayed.second;
+
+ lastPlayed = groupsRadio->GetLastAndPreviousToLastPlayedChannelGroupMember();
+ m_lastPlayedChannelRadio = lastPlayed.first;
+ m_previousToLastPlayedChannelRadio = lastPlayed.second;
+}
+
+void CPVRPlaybackState::ClearData()
+{
+ m_playingGroupId = -1;
+ m_playingChannelUniqueId = -1;
+ m_strPlayingRecordingUniqueId.clear();
+ m_playingEpgTagChannelUniqueId = -1;
+ m_playingEpgTagUniqueId = 0;
+ m_playingClientId = -1;
+ m_strPlayingClientName.clear();
+}
+
+void CPVRPlaybackState::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_playingChannel.reset();
+ m_playingRecording.reset();
+ m_playingEpgTag.reset();
+ m_lastPlayedChannelTV.reset();
+ m_lastPlayedChannelRadio.reset();
+ m_previousToLastPlayedChannelTV.reset();
+ m_previousToLastPlayedChannelRadio.reset();
+ m_lastWatchedUpdateTimer.reset();
+ m_activeGroupTV.reset();
+ m_activeGroupRadio.reset();
+}
+
+void CPVRPlaybackState::OnPlaybackStarted(const CFileItem& item)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_playingChannel.reset();
+ m_playingRecording.reset();
+ m_playingEpgTag.reset();
+ ClearData();
+
+ if (item.HasPVRChannelGroupMemberInfoTag())
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> channel = item.GetPVRChannelGroupMemberInfoTag();
+
+ m_playingChannel = channel;
+ m_playingGroupId = m_playingChannel->GroupID();
+ m_playingClientId = m_playingChannel->Channel()->ClientID();
+ m_playingChannelUniqueId = m_playingChannel->Channel()->UniqueID();
+
+ SetActiveChannelGroup(channel);
+
+ if (channel->Channel()->IsRadio())
+ {
+ if (m_lastPlayedChannelRadio != channel)
+ {
+ m_previousToLastPlayedChannelRadio = m_lastPlayedChannelRadio;
+ m_lastPlayedChannelRadio = channel;
+ }
+ }
+ else
+ {
+ if (m_lastPlayedChannelTV != channel)
+ {
+ m_previousToLastPlayedChannelTV = m_lastPlayedChannelTV;
+ m_lastPlayedChannelTV = channel;
+ }
+ }
+
+ int iLastWatchedDelay = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRPLAYBACK_DELAYMARKLASTWATCHED) *
+ 1000;
+ if (iLastWatchedDelay > 0)
+ {
+ // Insert new / replace existing last watched update timer
+ if (m_lastWatchedUpdateTimer)
+ m_lastWatchedUpdateTimer->Stop(true);
+
+ m_lastWatchedUpdateTimer.reset(
+ new CLastWatchedUpdateTimer(*this, channel, CDateTime::GetUTCDateTime()));
+ m_lastWatchedUpdateTimer->Start(std::chrono::milliseconds(iLastWatchedDelay));
+ }
+ else
+ {
+ // Store last watched timestamp immediately
+ UpdateLastWatched(channel, CDateTime::GetUTCDateTime());
+ }
+ }
+ else if (item.HasPVRRecordingInfoTag())
+ {
+ m_playingRecording = item.GetPVRRecordingInfoTag();
+ m_playingClientId = m_playingRecording->ClientID();
+ m_strPlayingRecordingUniqueId = m_playingRecording->ClientRecordingID();
+ }
+ else if (item.HasEPGInfoTag())
+ {
+ m_playingEpgTag = item.GetEPGInfoTag();
+ m_playingClientId = m_playingEpgTag->ClientID();
+ m_playingEpgTagChannelUniqueId = m_playingEpgTag->UniqueChannelID();
+ m_playingEpgTagUniqueId = m_playingEpgTag->UniqueBroadcastID();
+ }
+ else if (item.HasPVRChannelInfoTag())
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "Channel item without channel group member!");
+ }
+
+ if (m_playingClientId != -1)
+ {
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_playingClientId);
+ if (client)
+ m_strPlayingClientName = client->GetFriendlyName();
+ }
+}
+
+bool CPVRPlaybackState::OnPlaybackStopped(const CFileItem& item)
+{
+ // Playback ended due to user interaction
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bool bChanged = false;
+
+ if (item.HasPVRChannelGroupMemberInfoTag() &&
+ item.GetPVRChannelGroupMemberInfoTag()->Channel()->ClientID() == m_playingClientId &&
+ item.GetPVRChannelGroupMemberInfoTag()->Channel()->UniqueID() == m_playingChannelUniqueId)
+ {
+ bool bUpdateLastWatched = true;
+
+ if (m_lastWatchedUpdateTimer)
+ {
+ if (m_lastWatchedUpdateTimer->IsRunning())
+ {
+ // If last watched timer is still running, cancel it. Channel was not watched long enough to store the value.
+ m_lastWatchedUpdateTimer->Stop(true);
+ bUpdateLastWatched = false;
+ }
+ m_lastWatchedUpdateTimer.reset();
+ }
+
+ if (bUpdateLastWatched)
+ {
+ // If last watched timer is not running (any more), channel was watched long enough to store the value.
+ UpdateLastWatched(m_playingChannel, CDateTime::GetUTCDateTime());
+ }
+
+ bChanged = true;
+ m_playingChannel.reset();
+ ClearData();
+ }
+ else if (item.HasPVRRecordingInfoTag() &&
+ item.GetPVRRecordingInfoTag()->ClientID() == m_playingClientId &&
+ item.GetPVRRecordingInfoTag()->ClientRecordingID() == m_strPlayingRecordingUniqueId)
+ {
+ bChanged = true;
+ m_playingRecording.reset();
+ ClearData();
+ }
+ else if (item.HasEPGInfoTag() && item.GetEPGInfoTag()->ClientID() == m_playingClientId &&
+ item.GetEPGInfoTag()->UniqueChannelID() == m_playingEpgTagChannelUniqueId &&
+ item.GetEPGInfoTag()->UniqueBroadcastID() == m_playingEpgTagUniqueId)
+ {
+ bChanged = true;
+ m_playingEpgTag.reset();
+ ClearData();
+ }
+ else if (item.HasPVRChannelInfoTag())
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "Channel item without channel group member!");
+ }
+
+ return bChanged;
+}
+
+void CPVRPlaybackState::OnPlaybackEnded(const CFileItem& item)
+{
+ // Playback ended, but not due to user interaction
+ OnPlaybackStopped(item);
+}
+
+bool CPVRPlaybackState::IsPlaying() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel != nullptr || m_playingRecording != nullptr || m_playingEpgTag != nullptr;
+}
+
+bool CPVRPlaybackState::IsPlayingTV() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel && !m_playingChannel->Channel()->IsRadio();
+}
+
+bool CPVRPlaybackState::IsPlayingRadio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel && m_playingChannel->Channel()->IsRadio();
+}
+
+bool CPVRPlaybackState::IsPlayingEncryptedChannel() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel && m_playingChannel->Channel()->IsEncrypted();
+}
+
+bool CPVRPlaybackState::IsPlayingRecording() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingRecording != nullptr;
+}
+
+bool CPVRPlaybackState::IsPlayingEpgTag() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingEpgTag != nullptr;
+}
+
+bool CPVRPlaybackState::IsPlayingChannel(int iClientID, int iUniqueChannelID) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingClientId == iClientID && m_playingChannelUniqueId == iUniqueChannelID;
+}
+
+bool CPVRPlaybackState::IsPlayingChannel(const std::shared_ptr<CPVRChannel>& channel) const
+{
+ if (channel)
+ {
+ const std::shared_ptr<CPVRChannel> current = GetPlayingChannel();
+ if (current && current->ClientID() == channel->ClientID() &&
+ current->UniqueID() == channel->UniqueID())
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRPlaybackState::IsPlayingRecording(const std::shared_ptr<CPVRRecording>& recording) const
+{
+ if (recording)
+ {
+ const std::shared_ptr<CPVRRecording> current = GetPlayingRecording();
+ if (current && current->ClientID() == recording->ClientID() &&
+ current->ClientRecordingID() == recording->ClientRecordingID())
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRPlaybackState::IsPlayingEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (epgTag)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> current = GetPlayingEpgTag();
+ if (current && current->ClientID() == epgTag->ClientID() &&
+ current->UniqueChannelID() == epgTag->UniqueChannelID() &&
+ current->UniqueBroadcastID() == epgTag->UniqueBroadcastID())
+ return true;
+ }
+
+ return false;
+}
+
+std::shared_ptr<CPVRChannel> CPVRPlaybackState::GetPlayingChannel() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel ? m_playingChannel->Channel() : std::shared_ptr<CPVRChannel>();
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRPlaybackState::GetPlayingChannelGroupMember() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannel;
+}
+
+std::shared_ptr<CPVRRecording> CPVRPlaybackState::GetPlayingRecording() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingRecording;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRPlaybackState::GetPlayingEpgTag() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingEpgTag;
+}
+
+int CPVRPlaybackState::GetPlayingChannelUniqueID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingChannelUniqueId;
+}
+
+std::string CPVRPlaybackState::GetPlayingClientName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strPlayingClientName;
+}
+
+int CPVRPlaybackState::GetPlayingClientID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_playingClientId;
+}
+
+bool CPVRPlaybackState::IsRecording() const
+{
+ return CServiceBroker::GetPVRManager().Timers()->IsRecording();
+}
+
+bool CPVRPlaybackState::IsRecordingOnPlayingChannel() const
+{
+ const std::shared_ptr<CPVRChannel> currentChannel = GetPlayingChannel();
+ return currentChannel &&
+ CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*currentChannel);
+}
+
+bool CPVRPlaybackState::IsPlayingActiveRecording() const
+{
+ const std::shared_ptr<CPVRRecording> currentRecording = GetPlayingRecording();
+ return currentRecording && currentRecording->IsInProgress();
+}
+
+bool CPVRPlaybackState::CanRecordOnPlayingChannel() const
+{
+ const std::shared_ptr<CPVRChannel> currentChannel = GetPlayingChannel();
+ return currentChannel && currentChannel->CanRecord();
+}
+
+void CPVRPlaybackState::SetActiveChannelGroup(const std::shared_ptr<CPVRChannelGroup>& group)
+{
+ if (group)
+ {
+ if (group->IsRadio())
+ m_activeGroupRadio = group;
+ else
+ m_activeGroupTV = group;
+
+ auto duration = std::chrono::system_clock::now().time_since_epoch();
+ uint64_t tsMillis = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
+ group->SetLastOpened(tsMillis);
+ }
+}
+
+void CPVRPlaybackState::SetActiveChannelGroup(
+ const std::shared_ptr<CPVRChannelGroupMember>& channel)
+{
+ const bool bRadio = channel->Channel()->IsRadio();
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio)->GetById(channel->GroupID());
+
+ SetActiveChannelGroup(group);
+}
+
+namespace
+{
+std::shared_ptr<CPVRChannelGroup> GetFirstNonDeletedAndNonHiddenChannelGroup(bool bRadio)
+{
+ CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio);
+ if (groups)
+ {
+ const std::vector<std::shared_ptr<CPVRChannelGroup>> members =
+ groups->GetMembers(true); // exclude hidden
+
+ const auto it = std::find_if(members.cbegin(), members.cend(),
+ [](const auto& group) { return !group->IsDeleted(); });
+ if (it != members.cend())
+ return (*it);
+ }
+
+ CLog::LogFC(LOGERROR, LOGPVR, "Failed to obtain any non-deleted and non-hidden group");
+ return {};
+}
+} // unnamed namespace
+
+std::shared_ptr<CPVRChannelGroup> CPVRPlaybackState::GetActiveChannelGroup(bool bRadio) const
+{
+ if (bRadio)
+ {
+ if (m_activeGroupRadio && (m_activeGroupRadio->IsDeleted() || m_activeGroupRadio->IsHidden()))
+ {
+ // switch to first non-deleted and non-hidden group
+ const auto group = GetFirstNonDeletedAndNonHiddenChannelGroup(bRadio);
+ if (group)
+ const_cast<CPVRPlaybackState*>(this)->SetActiveChannelGroup(group);
+ }
+ return m_activeGroupRadio;
+ }
+ else
+ {
+ if (m_activeGroupTV && (m_activeGroupTV->IsDeleted() || m_activeGroupTV->IsHidden()))
+ {
+ // switch to first non-deleted and non-hidden group
+ const auto group = GetFirstNonDeletedAndNonHiddenChannelGroup(bRadio);
+ if (group)
+ const_cast<CPVRPlaybackState*>(this)->SetActiveChannelGroup(group);
+ }
+ return m_activeGroupTV;
+ }
+}
+
+CDateTime CPVRPlaybackState::GetPlaybackTime(int iClientID, int iUniqueChannelID) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = GetPlayingEpgTag();
+ if (epgTag && iClientID == epgTag->ClientID() && iUniqueChannelID == epgTag->UniqueChannelID())
+ {
+ // playing an epg tag on requested channel
+ return epgTag->StartAsUTC() +
+ CDateTimeSpan(0, 0, 0, CServiceBroker::GetDataCacheCore().GetPlayTime() / 1000);
+ }
+
+ // not playing / playing live / playing timeshifted
+ return GetChannelPlaybackTime(iClientID, iUniqueChannelID);
+}
+
+CDateTime CPVRPlaybackState::GetChannelPlaybackTime(int iClientID, int iUniqueChannelID) const
+{
+ if (IsPlayingChannel(iClientID, iUniqueChannelID))
+ {
+ // start time valid?
+ time_t startTime = CServiceBroker::GetDataCacheCore().GetStartTime();
+ if (startTime > 0)
+ return CDateTime(startTime + CServiceBroker::GetDataCacheCore().GetPlayTime() / 1000);
+ }
+
+ // not playing / playing live
+ return CDateTime::GetUTCDateTime();
+}
+
+void CPVRPlaybackState::UpdateLastWatched(const std::shared_ptr<CPVRChannelGroupMember>& channel,
+ const CDateTime& time)
+{
+ time_t iTime;
+ time.GetAsTime(iTime);
+
+ channel->Channel()->SetLastWatched(iTime);
+
+ // update last watched timestamp for group
+ const bool bRadio = channel->Channel()->IsRadio();
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio)->GetById(channel->GroupID());
+ if (group)
+ group->SetLastWatched(iTime);
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRPlaybackState::GetLastPlayedChannelGroupMember(
+ bool bRadio) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return bRadio ? m_lastPlayedChannelRadio : m_lastPlayedChannelTV;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRPlaybackState::
+ GetPreviousToLastPlayedChannelGroupMember(bool bRadio) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return bRadio ? m_previousToLastPlayedChannelRadio : m_previousToLastPlayedChannelTV;
+}
diff --git a/xbmc/pvr/PVRPlaybackState.h b/xbmc/pvr/PVRPlaybackState.h
new file mode 100644
index 0000000..2ef7500
--- /dev/null
+++ b/xbmc/pvr/PVRPlaybackState.h
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2012-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 "threads/CriticalSection.h"
+
+#include <memory>
+#include <string>
+
+class CDateTime;
+class CFileItem;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRChannelGroup;
+class CPVRChannelGroupMember;
+class CPVREpgInfoTag;
+class CPVRRecording;
+
+class CPVRPlaybackState
+{
+public:
+ /*!
+ * @brief ctor.
+ */
+ CPVRPlaybackState();
+
+ /*!
+ * @brief dtor.
+ */
+ virtual ~CPVRPlaybackState();
+
+ /*!
+ * @brief clear instances, keep stored UIDs.
+ */
+ void Clear();
+
+ /*!
+ * @brief re-init using stored UIDs.
+ */
+ void ReInit();
+
+ /*!
+ * @brief Inform that playback of an item just started.
+ * @param item The item that started to play.
+ */
+ void OnPlaybackStarted(const CFileItem& item);
+
+ /*!
+ * @brief Inform that playback of an item was stopped due to user interaction.
+ * @param item The item that stopped to play.
+ * @return True, if the state has changed, false otherwise
+ */
+ bool OnPlaybackStopped(const CFileItem& item);
+
+ /*!
+ * @brief Inform that playback of an item has stopped without user interaction.
+ * @param item The item that ended to play.
+ */
+ void OnPlaybackEnded(const CFileItem& item);
+
+ /*!
+ * @brief Check if a TV channel, radio channel or recording is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlaying() const;
+
+ /*!
+ * @brief Check if a TV channel is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingTV() const;
+
+ /*!
+ * @brief Check if a radio channel is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingRadio() const;
+
+ /*!
+ * @brief Check if a an encrypted TV or radio channel is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingEncryptedChannel() const;
+
+ /*!
+ * @brief Check if a recording is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingRecording() const;
+
+ /*!
+ * @brief Check if an epg tag is playing.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingEpgTag() const;
+
+ /*!
+ * @brief Check whether playing channel matches given uids.
+ * @param iClientID The client id.
+ * @param iUniqueChannelID The channel uid.
+ * @return True on match, false if there is no match or no channel is playing.
+ */
+ bool IsPlayingChannel(int iClientID, int iUniqueChannelID) const;
+
+ /*!
+ * @brief Check if the given channel is playing.
+ * @param channel The channel to check.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingChannel(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Check if the given recording is playing.
+ * @param recording The recording to check.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingRecording(const std::shared_ptr<CPVRRecording>& recording) const;
+
+ /*!
+ * @brief Check if the given epg tag is playing.
+ * @param epgTag The tag to check.
+ * @return True if it's playing, false otherwise.
+ */
+ bool IsPlayingEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ /*!
+ * @brief Return the channel that is currently playing.
+ * @return The channel or nullptr if none is playing.
+ */
+ std::shared_ptr<CPVRChannel> GetPlayingChannel() const;
+
+ /*!
+ * @brief Return the channel group member that is currently playing.
+ * @return The channel group member or nullptr if none is playing.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetPlayingChannelGroupMember() const;
+
+ /*!
+ * @brief Return the recording that is currently playing.
+ * @return The recording or nullptr if none is playing.
+ */
+ std::shared_ptr<CPVRRecording> GetPlayingRecording() const;
+
+ /*!
+ * @brief Return the epg tag that is currently playing.
+ * @return The tag or nullptr if none is playing.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetPlayingEpgTag() const;
+
+ /*!
+ * @brief Return playing channel unique identifier
+ * @return The channel id or -1 if not present
+ */
+ int GetPlayingChannelUniqueID() const;
+
+ /*!
+ * @brief Get the name of the playing client, if there is one.
+ * @return The name of the client or an empty string if nothing is playing.
+ */
+ std::string GetPlayingClientName() const;
+
+ /*!
+ * @brief Get the ID of the playing client, if there is one.
+ * @return The ID or -1 if no client is playing.
+ */
+ int GetPlayingClientID() const;
+
+ /*!
+ * @brief Check whether there are active recordings.
+ * @return True if there are active recordings, false otherwise.
+ */
+ bool IsRecording() const;
+
+ /*!
+ * @brief Check whether there is an active recording on the currenlyt playing channel.
+ * @return True if there is a playing channel and there is an active recording on that channel, false otherwise.
+ */
+ bool IsRecordingOnPlayingChannel() const;
+
+ /*!
+ * @brief Check if an active recording is playing.
+ * @return True if an in-progress (active) recording is playing, false otherwise.
+ */
+ bool IsPlayingActiveRecording() const;
+
+ /*!
+ * @brief Check whether the currently playing channel can be recorded.
+ * @return True if there is a playing channel that can be recorded, false otherwise.
+ */
+ bool CanRecordOnPlayingChannel() const;
+
+ /*!
+ * @brief Set the active channel group.
+ * @param group The new group.
+ */
+ void SetActiveChannelGroup(const std::shared_ptr<CPVRChannelGroup>& group);
+
+ /*!
+ * @brief Get the active channel group.
+ * @param bRadio True to get the active radio group, false to get the active TV group.
+ * @return The current group or the group containing all channels if it's not set.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetActiveChannelGroup(bool bRadio) const;
+
+ /*!
+ * @brief Get the last played channel group member.
+ * @param bRadio True to get the radio group member, false to get the TV group member.
+ * @return The last played group member or nullptr if it's not available.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetLastPlayedChannelGroupMember(bool bRadio) const;
+
+ /*!
+ * @brief Get the channel group member that was played before the last played member.
+ * @param bRadio True to get the radio group member, false to get the TV group member.
+ * @return The previous played group member or nullptr if it's not available.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetPreviousToLastPlayedChannelGroupMember(
+ bool bRadio) const;
+
+ /*!
+ * @brief Get current playback time for the given channel, taking timeshifting and playing
+ * epg tags into account.
+ * @param iClientID The client id.
+ * @param iUniqueChannelID The channel uid.
+ * @return The playback time or 'now' if not playing.
+ */
+ CDateTime GetPlaybackTime(int iClientID, int iUniqueChannelID) const;
+
+ /*!
+ * @brief Get current playback time for the given channel, taking timeshifting into account.
+ * @param iClientID The client id.
+ * @param iUniqueChannelID The channel uid.
+ * @return The playback time or 'now' if not playing.
+ */
+ CDateTime GetChannelPlaybackTime(int iClientID, int iUniqueChannelID) const;
+
+private:
+ void ClearData();
+
+ /*!
+ * @brief Set the active group to the group of the supplied channel group member.
+ * @param channel The channel group member
+ */
+ void SetActiveChannelGroup(const std::shared_ptr<CPVRChannelGroupMember>& channel);
+
+ /*!
+ * @brief Updates the last watched timestamps of the channel and group which are currently playing.
+ * @param channel The channel which is updated
+ * @param time The last watched time to set
+ */
+ void UpdateLastWatched(const std::shared_ptr<CPVRChannelGroupMember>& channel,
+ const CDateTime& time);
+
+ mutable CCriticalSection m_critSection;
+
+ std::shared_ptr<CPVRChannelGroupMember> m_playingChannel;
+ std::shared_ptr<CPVRRecording> m_playingRecording;
+ std::shared_ptr<CPVREpgInfoTag> m_playingEpgTag;
+ std::shared_ptr<CPVRChannelGroupMember> m_lastPlayedChannelTV;
+ std::shared_ptr<CPVRChannelGroupMember> m_lastPlayedChannelRadio;
+ std::shared_ptr<CPVRChannelGroupMember> m_previousToLastPlayedChannelTV;
+ std::shared_ptr<CPVRChannelGroupMember> m_previousToLastPlayedChannelRadio;
+ std::string m_strPlayingClientName;
+ int m_playingGroupId = -1;
+ int m_playingClientId = -1;
+ int m_playingChannelUniqueId = -1;
+ std::string m_strPlayingRecordingUniqueId;
+ int m_playingEpgTagChannelUniqueId = -1;
+ unsigned int m_playingEpgTagUniqueId = 0;
+ std::shared_ptr<CPVRChannelGroup> m_activeGroupTV;
+ std::shared_ptr<CPVRChannelGroup> m_activeGroupRadio;
+
+ class CLastWatchedUpdateTimer;
+ std::unique_ptr<CLastWatchedUpdateTimer> m_lastWatchedUpdateTimer;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/PVRStreamProperties.cpp b/xbmc/pvr/PVRStreamProperties.cpp
new file mode 100644
index 0000000..272b33a
--- /dev/null
+++ b/xbmc/pvr/PVRStreamProperties.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012-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.
+ */
+
+#include "PVRStreamProperties.h"
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+
+using namespace PVR;
+
+std::string CPVRStreamProperties::GetStreamURL() const
+{
+ const auto it = std::find_if(cbegin(), cend(), [](const auto& prop) {
+ return prop.first == PVR_STREAM_PROPERTY_STREAMURL;
+ });
+ return it != cend() ? (*it).second : std::string();
+}
+
+std::string CPVRStreamProperties::GetStreamMimeType() const
+{
+ const auto it = std::find_if(cbegin(), cend(), [](const auto& prop) {
+ return prop.first == PVR_STREAM_PROPERTY_MIMETYPE;
+ });
+ return it != cend() ? (*it).second : std::string();
+}
+
+bool CPVRStreamProperties::EPGPlaybackAsLive() const
+{
+ const auto it = std::find_if(cbegin(), cend(), [](const auto& prop) {
+ return prop.first == PVR_STREAM_PROPERTY_EPGPLAYBACKASLIVE;
+ });
+ return it != cend() ? StringUtils::EqualsNoCase((*it).second, "true") : false;
+}
diff --git a/xbmc/pvr/PVRStreamProperties.h b/xbmc/pvr/PVRStreamProperties.h
new file mode 100644
index 0000000..6690660
--- /dev/null
+++ b/xbmc/pvr/PVRStreamProperties.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012-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 <string>
+#include <utility>
+#include <vector>
+
+namespace PVR
+{
+
+class CPVRStreamProperties : public std::vector<std::pair<std::string, std::string>>
+{
+public:
+ CPVRStreamProperties() = default;
+ virtual ~CPVRStreamProperties() = default;
+
+ /*!
+ * @brief Obtain the URL of the stream.
+ * @return The stream URL or empty string, if not found.
+ */
+ std::string GetStreamURL() const;
+
+ /*!
+ * @brief Obtain the MIME type of the stream.
+ * @return The stream's MIME type or empty string, if not found.
+ */
+ std::string GetStreamMimeType() const;
+
+ /*!
+ * @brief If props are from an EPG tag indicates if playback should be as live playback would be
+ * @return true if it should be played back as live, false otherwise.
+ */
+ bool EPGPlaybackAsLive() const;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/PVRThumbLoader.cpp b/xbmc/pvr/PVRThumbLoader.cpp
new file mode 100644
index 0000000..4b3544c
--- /dev/null
+++ b/xbmc/pvr/PVRThumbLoader.cpp
@@ -0,0 +1,136 @@
+/*
+ * 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 "PVRThumbLoader.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "TextureCacheJob.h"
+#include "pictures/Picture.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRManager.h"
+#include "pvr/filesystem/PVRGUIDirectory.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <ctime>
+
+using namespace PVR;
+
+bool CPVRThumbLoader::LoadItem(CFileItem* item)
+{
+ bool result = LoadItemCached(item);
+ result |= LoadItemLookup(item);
+ return result;
+}
+
+bool CPVRThumbLoader::LoadItemCached(CFileItem* item)
+{
+ return FillThumb(*item);
+}
+
+bool CPVRThumbLoader::LoadItemLookup(CFileItem* item)
+{
+ return false;
+}
+
+void CPVRThumbLoader::OnLoaderFinish()
+{
+ if (m_bInvalidated)
+ {
+ m_bInvalidated = false;
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated);
+ }
+ CThumbLoader::OnLoaderFinish();
+}
+
+void CPVRThumbLoader::ClearCachedImage(CFileItem& item)
+{
+ const std::string thumb = item.GetArt("thumb");
+ if (!thumb.empty())
+ {
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumb);
+ if (m_textureDatabase->Open())
+ {
+ m_textureDatabase->ClearTextureForPath(item.GetPath(), "thumb");
+ m_textureDatabase->Close();
+ }
+ item.SetArt("thumb", "");
+ m_bInvalidated = true;
+ }
+}
+
+void CPVRThumbLoader::ClearCachedImages(const CFileItemList& items)
+{
+ for (auto& item : items)
+ ClearCachedImage(*item);
+}
+
+bool CPVRThumbLoader::FillThumb(CFileItem& item)
+{
+ // see whether we have a cached image for this item
+ std::string thumb = GetCachedImage(item, "thumb");
+ if (thumb.empty())
+ {
+ if (item.IsPVRChannelGroup())
+ thumb = CreateChannelGroupThumb(item);
+ else
+ CLog::LogF(LOGERROR, "Unsupported PVR item '{}'", item.GetPath());
+
+ if (!thumb.empty())
+ {
+ SetCachedImage(item, "thumb", thumb);
+ m_bInvalidated = true;
+ }
+ }
+
+ if (thumb.empty())
+ return false;
+
+ item.SetArt("thumb", thumb);
+ return true;
+}
+
+std::string CPVRThumbLoader::CreateChannelGroupThumb(const CFileItem& channelGroupItem)
+{
+ const CPVRGUIDirectory channelGroupDir(channelGroupItem.GetPath());
+ CFileItemList channels;
+ if (channelGroupDir.GetChannelsDirectory(channels))
+ {
+ std::vector<std::string> channelIcons;
+ for (const auto& channel : channels)
+ {
+ const std::string& icon = channel->GetArt("icon");
+ if (!icon.empty())
+ channelIcons.emplace_back(CPVRCachedImages::UnwrapImageURL(icon));
+
+ if (channelIcons.size() == 9) // limit number of tiles
+ break;
+ }
+
+ std::string thumb = StringUtils::Format(
+ "{}?ts={}", // append timestamp to Thumb URL to enforce texture refresh
+ CTextureUtils::GetWrappedImageURL(channelGroupItem.GetPath(), "pvr"), std::time(nullptr));
+ const std::string relativeCacheFile = CTextureCache::GetCacheFile(thumb) + ".png";
+ if (CPicture::CreateTiledThumb(channelIcons, CTextureCache::GetCachedPath(relativeCacheFile)))
+ {
+ CTextureDetails details;
+ details.file = relativeCacheFile;
+ details.width = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes;
+ details.height = details.width;
+ CServiceBroker::GetTextureCache()->AddCachedTexture(thumb, details);
+ return thumb;
+ }
+ }
+
+ return {};
+}
diff --git a/xbmc/pvr/PVRThumbLoader.h b/xbmc/pvr/PVRThumbLoader.h
new file mode 100644
index 0000000..33e1bd4
--- /dev/null
+++ b/xbmc/pvr/PVRThumbLoader.h
@@ -0,0 +1,41 @@
+/*
+ * 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 "ThumbLoader.h"
+
+#include <string>
+
+namespace PVR
+{
+
+class CPVRThumbLoader : public CThumbLoader
+{
+public:
+ CPVRThumbLoader() = default;
+ ~CPVRThumbLoader() override = default;
+
+ bool LoadItem(CFileItem* item) override;
+ bool LoadItemCached(CFileItem* item) override;
+ bool LoadItemLookup(CFileItem* item) override;
+
+ void ClearCachedImage(CFileItem& item);
+ void ClearCachedImages(const CFileItemList& items);
+
+protected:
+ void OnLoaderFinish() override;
+
+private:
+ bool FillThumb(CFileItem& item);
+ std::string CreateChannelGroupThumb(const CFileItem& channelGroupItem);
+
+ bool m_bInvalidated = false;
+};
+
+}
diff --git a/xbmc/pvr/addons/CMakeLists.txt b/xbmc/pvr/addons/CMakeLists.txt
new file mode 100644
index 0000000..e8a0a51
--- /dev/null
+++ b/xbmc/pvr/addons/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES PVRClient.cpp
+ PVRClientCapabilities.cpp
+ PVRClientMenuHooks.cpp
+ PVRClientUID.cpp
+ PVRClients.cpp)
+
+set(HEADERS PVRClient.h
+ PVRClientCapabilities.h
+ PVRClientMenuHooks.h
+ PVRClientUID.h
+ PVRClients.h)
+
+core_add_library(pvr_addons)
diff --git a/xbmc/pvr/addons/PVRClient.cpp b/xbmc/pvr/addons/PVRClient.cpp
new file mode 100644
index 0000000..2a09277
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClient.cpp
@@ -0,0 +1,2023 @@
+/*
+ * 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 "PVRClient.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/binary-addons/AddonDll.h"
+#include "cores/VideoPlayer/DVDDemuxers/DVDDemuxUtils.h"
+#include "dialogs/GUIDialogKaiToast.h" //! @todo get rid of GUI in core
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRStreamProperties.h"
+#include "pvr/addons/PVRClientMenuHooks.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupInternal.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/providers/PVRProvider.h"
+#include "pvr/providers/PVRProviders.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimerType.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <map>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+
+extern "C"
+{
+#include <libavcodec/avcodec.h>
+}
+
+using namespace ADDON;
+
+namespace PVR
+{
+
+#define DEFAULT_INFO_STRING_VALUE "unknown"
+
+CPVRClient::CPVRClient(const ADDON::AddonInfoPtr& addonInfo,
+ ADDON::AddonInstanceId instanceId,
+ int clientId)
+ : IAddonInstanceHandler(ADDON_INSTANCE_PVR, addonInfo, instanceId), m_iClientId(clientId)
+{
+ // Create all interface parts independent to make API changes easier if
+ // something is added
+ m_ifc.pvr = new AddonInstance_PVR;
+ m_ifc.pvr->props = new AddonProperties_PVR();
+ m_ifc.pvr->toKodi = new AddonToKodiFuncTable_PVR();
+ m_ifc.pvr->toAddon = new KodiToAddonFuncTable_PVR();
+
+ ResetProperties();
+}
+
+CPVRClient::~CPVRClient()
+{
+ Destroy();
+
+ if (m_ifc.pvr)
+ {
+ delete m_ifc.pvr->props;
+ delete m_ifc.pvr->toKodi;
+ delete m_ifc.pvr->toAddon;
+ }
+ delete m_ifc.pvr;
+}
+
+void CPVRClient::StopRunningInstance()
+{
+ // stop the pvr manager and stop and unload the running pvr addon. pvr manager will be restarted on demand.
+ CServiceBroker::GetPVRManager().Stop();
+ CServiceBroker::GetPVRManager().Clients()->StopClient(m_iClientId, false);
+}
+
+void CPVRClient::OnPreInstall()
+{
+ // note: this method is also called on update; thus stop and unload possibly running instance
+ StopRunningInstance();
+}
+
+void CPVRClient::OnPreUnInstall()
+{
+ StopRunningInstance();
+}
+
+void CPVRClient::ResetProperties()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* initialise members */
+ m_strUserPath = CSpecialProtocol::TranslatePath(Profile());
+ m_strClientPath = CSpecialProtocol::TranslatePath(Path());
+ m_bReadyToUse = false;
+ m_bBlockAddonCalls = false;
+ m_iAddonCalls = 0;
+ m_allAddonCallsFinished.Set();
+ m_connectionState = PVR_CONNECTION_STATE_UNKNOWN;
+ m_prevConnectionState = PVR_CONNECTION_STATE_UNKNOWN;
+ m_ignoreClient = false;
+ m_iPriority = 0;
+ m_bPriorityFetched = false;
+ m_strBackendVersion = DEFAULT_INFO_STRING_VALUE;
+ m_strConnectionString = DEFAULT_INFO_STRING_VALUE;
+ m_strBackendName = DEFAULT_INFO_STRING_VALUE;
+ m_strBackendHostname.clear();
+ m_menuhooks.reset();
+ m_timertypes.clear();
+ m_clientCapabilities.clear();
+
+ m_ifc.pvr->props->strUserPath = m_strUserPath.c_str();
+ m_ifc.pvr->props->strClientPath = m_strClientPath.c_str();
+ m_ifc.pvr->props->iEpgMaxPastDays =
+ CServiceBroker::GetPVRManager().EpgContainer().GetPastDaysToDisplay();
+ m_ifc.pvr->props->iEpgMaxFutureDays =
+ CServiceBroker::GetPVRManager().EpgContainer().GetFutureDaysToDisplay();
+
+ m_ifc.pvr->toKodi->kodiInstance = this;
+ m_ifc.pvr->toKodi->TransferEpgEntry = cb_transfer_epg_entry;
+ m_ifc.pvr->toKodi->TransferChannelEntry = cb_transfer_channel_entry;
+ m_ifc.pvr->toKodi->TransferProviderEntry = cb_transfer_provider_entry;
+ m_ifc.pvr->toKodi->TransferTimerEntry = cb_transfer_timer_entry;
+ m_ifc.pvr->toKodi->TransferRecordingEntry = cb_transfer_recording_entry;
+ m_ifc.pvr->toKodi->AddMenuHook = cb_add_menu_hook;
+ m_ifc.pvr->toKodi->RecordingNotification = cb_recording_notification;
+ m_ifc.pvr->toKodi->TriggerChannelUpdate = cb_trigger_channel_update;
+ m_ifc.pvr->toKodi->TriggerProvidersUpdate = cb_trigger_provider_update;
+ m_ifc.pvr->toKodi->TriggerChannelGroupsUpdate = cb_trigger_channel_groups_update;
+ m_ifc.pvr->toKodi->TriggerTimerUpdate = cb_trigger_timer_update;
+ m_ifc.pvr->toKodi->TriggerRecordingUpdate = cb_trigger_recording_update;
+ m_ifc.pvr->toKodi->TriggerEpgUpdate = cb_trigger_epg_update;
+ m_ifc.pvr->toKodi->FreeDemuxPacket = cb_free_demux_packet;
+ m_ifc.pvr->toKodi->AllocateDemuxPacket = cb_allocate_demux_packet;
+ m_ifc.pvr->toKodi->TransferChannelGroup = cb_transfer_channel_group;
+ m_ifc.pvr->toKodi->TransferChannelGroupMember = cb_transfer_channel_group_member;
+ m_ifc.pvr->toKodi->ConnectionStateChange = cb_connection_state_change;
+ m_ifc.pvr->toKodi->EpgEventStateChange = cb_epg_event_state_change;
+ m_ifc.pvr->toKodi->GetCodecByName = cb_get_codec_by_name;
+
+ // Clear function addresses to have NULL if not set by addon
+ memset(m_ifc.pvr->toAddon, 0, sizeof(KodiToAddonFuncTable_PVR));
+}
+
+ADDON_STATUS CPVRClient::Create()
+{
+ ADDON_STATUS status(ADDON_STATUS_UNKNOWN);
+
+ ResetProperties();
+
+ /* initialise the add-on */
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Creating PVR add-on instance '{}'", ID());
+
+ bool bReadyToUse = false;
+ if ((status = CreateInstance()) == ADDON_STATUS_OK)
+ bReadyToUse = GetAddonProperties();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Created PVR add-on instance '{}'. readytouse={} ", ID(),
+ bReadyToUse);
+
+ m_bReadyToUse = bReadyToUse;
+ return status;
+}
+
+void CPVRClient::Destroy()
+{
+ if (!m_bReadyToUse)
+ return;
+
+ m_bReadyToUse = false;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Destroying PVR add-on instance '{}'", ID());
+
+ m_bBlockAddonCalls = true;
+ m_allAddonCallsFinished.Wait();
+
+ DestroyInstance();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Destroyed PVR add-on instance '{}'", ID());
+
+ if (m_menuhooks)
+ m_menuhooks->Clear();
+
+ ResetProperties();
+}
+
+void CPVRClient::Stop()
+{
+ m_bBlockAddonCalls = true;
+ m_bPriorityFetched = false;
+}
+
+void CPVRClient::Continue()
+{
+ m_bBlockAddonCalls = false;
+}
+
+void CPVRClient::ReCreate()
+{
+ Destroy();
+ Create();
+}
+
+bool CPVRClient::ReadyToUse() const
+{
+ return m_bReadyToUse;
+}
+
+PVR_CONNECTION_STATE CPVRClient::GetConnectionState() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_connectionState;
+}
+
+void CPVRClient::SetConnectionState(PVR_CONNECTION_STATE state)
+{
+ if (state == PVR_CONNECTION_STATE_CONNECTED)
+ {
+ // update properties - some will only be available after add-on is connected to backend
+ if (!GetAddonProperties())
+ CLog::LogF(LOGERROR, "Error reading PVR client properties");
+ }
+ else
+ {
+ if (!GetAddonNameStringProperties())
+ CLog::LogF(LOGERROR, "Cannot read PVR client name string properties");
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_prevConnectionState = m_connectionState;
+ m_connectionState = state;
+
+ if (m_connectionState == PVR_CONNECTION_STATE_CONNECTED)
+ m_ignoreClient = false;
+ else if (m_connectionState == PVR_CONNECTION_STATE_CONNECTING &&
+ m_prevConnectionState == PVR_CONNECTION_STATE_UNKNOWN)
+ m_ignoreClient = true; // ignore until connected
+}
+
+PVR_CONNECTION_STATE CPVRClient::GetPreviousConnectionState() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_prevConnectionState;
+}
+
+bool CPVRClient::IgnoreClient() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_ignoreClient;
+}
+
+bool CPVRClient::IsEnabled() const
+{
+ if (InstanceId() == ADDON_SINGLETON_INSTANCE_ID)
+ {
+ return !CServiceBroker::GetAddonMgr().IsAddonDisabled(ID());
+ }
+ else
+ {
+ bool instanceEnabled{false};
+ Addon()->ReloadSettings(InstanceId());
+ Addon()->GetSettingBool(ADDON_SETTING_INSTANCE_ENABLED_VALUE, instanceEnabled, InstanceId());
+ return instanceEnabled;
+ }
+}
+
+int CPVRClient::GetID() const
+{
+ return m_iClientId;
+}
+
+bool CPVRClient::GetAddonProperties()
+{
+ if (!GetAddonNameStringProperties())
+ return false;
+
+ PVR_ADDON_CAPABILITIES addonCapabilities = {};
+ std::vector<std::shared_ptr<CPVRTimerType>> timerTypes;
+
+ /* get the capabilities */
+ PVR_ERROR retVal = DoAddonCall(
+ __func__,
+ [&addonCapabilities](const AddonInstance* addon) {
+ return addon->toAddon->GetCapabilities(addon, &addonCapabilities);
+ },
+ true, false);
+
+ if (retVal != PVR_ERROR_NO_ERROR)
+ return false;
+
+ /* timer types */
+ retVal = DoAddonCall(
+ __func__,
+ [this, &addonCapabilities, &timerTypes](const AddonInstance* addon) {
+ std::unique_ptr<PVR_TIMER_TYPE[]> types_array(
+ new PVR_TIMER_TYPE[PVR_ADDON_TIMERTYPE_ARRAY_SIZE]);
+ int size = PVR_ADDON_TIMERTYPE_ARRAY_SIZE;
+
+ PVR_ERROR retval = addon->toAddon->GetTimerTypes(addon, types_array.get(), &size);
+
+ if (retval == PVR_ERROR_NOT_IMPLEMENTED)
+ {
+ // begin compat section
+ CLog::LogF(LOGWARNING,
+ "Add-on {} does not support timer types. It will work, but not benefit from "
+ "the timer features introduced with PVR Addon API 2.0.0",
+ GetFriendlyName());
+
+ // Create standard timer types (mostly) matching the timer functionality available in Isengard.
+ // This is for migration only and does not make changes to the addons obsolete. Addons should
+ // work and benefit from some UI changes (e.g. some of the timer settings dialog enhancements),
+ // but all old problems/bugs due to static attributes and values will remain the same as in
+ // Isengard. Also, new features (like epg search) are not available to addons automatically.
+ // This code can be removed once all addons actually support the respective PVR Addon API version.
+
+ size = 0;
+ // manual one time
+ memset(&types_array[size], 0, sizeof(types_array[size]));
+ types_array[size].iId = size + 1;
+ types_array[size].iAttributes =
+ PVR_TIMER_TYPE_IS_MANUAL | PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS | PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME | PVR_TIMER_TYPE_SUPPORTS_PRIORITY |
+ PVR_TIMER_TYPE_SUPPORTS_LIFETIME | PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS;
+ ++size;
+
+ // manual timer rule
+ memset(&types_array[size], 0, sizeof(types_array[size]));
+ types_array[size].iId = size + 1;
+ types_array[size].iAttributes =
+ PVR_TIMER_TYPE_IS_MANUAL | PVR_TIMER_TYPE_IS_REPEATING |
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE | PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME | PVR_TIMER_TYPE_SUPPORTS_END_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_PRIORITY | PVR_TIMER_TYPE_SUPPORTS_LIFETIME |
+ PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY | PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS |
+ PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS;
+ ++size;
+
+ if (addonCapabilities.bSupportsEPG)
+ {
+ // One-shot epg-based
+ memset(&types_array[size], 0, sizeof(types_array[size]));
+ types_array[size].iId = size + 1;
+ types_array[size].iAttributes =
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE | PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS | PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME | PVR_TIMER_TYPE_SUPPORTS_PRIORITY |
+ PVR_TIMER_TYPE_SUPPORTS_LIFETIME | PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS;
+ ++size;
+ }
+
+ retval = PVR_ERROR_NO_ERROR;
+ // end compat section
+ }
+
+ if (retval == PVR_ERROR_NO_ERROR)
+ {
+ timerTypes.reserve(size);
+ for (int i = 0; i < size; ++i)
+ {
+ if (types_array[i].iId == PVR_TIMER_TYPE_NONE)
+ {
+ CLog::LogF(LOGERROR, "Invalid timer type supplied by add-on '{}'.", ID());
+ continue;
+ }
+ timerTypes.emplace_back(
+ std::shared_ptr<CPVRTimerType>(new CPVRTimerType(types_array[i], m_iClientId)));
+ }
+ }
+ return retval;
+ },
+ addonCapabilities.bSupportsTimers, false);
+
+ if (retVal == PVR_ERROR_NOT_IMPLEMENTED)
+ retVal = PVR_ERROR_NO_ERROR; // timer support is optional.
+
+ /* update the members */
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_clientCapabilities = addonCapabilities;
+ m_timertypes = timerTypes;
+
+ return retVal == PVR_ERROR_NO_ERROR;
+}
+
+bool CPVRClient::GetAddonNameStringProperties()
+{
+ char strBackendName[PVR_ADDON_NAME_STRING_LENGTH] = {};
+ char strConnectionString[PVR_ADDON_NAME_STRING_LENGTH] = {};
+ char strBackendVersion[PVR_ADDON_NAME_STRING_LENGTH] = {};
+ char strBackendHostname[PVR_ADDON_NAME_STRING_LENGTH] = {};
+
+ /* get the name of the backend */
+ PVR_ERROR retVal = DoAddonCall(
+ __func__,
+ [&strBackendName](const AddonInstance* addon) {
+ return addon->toAddon->GetBackendName(addon, strBackendName, sizeof(strBackendName));
+ },
+ true, false);
+
+ if (retVal != PVR_ERROR_NO_ERROR)
+ return false;
+
+ /* get the connection string */
+ retVal = DoAddonCall(
+ __func__,
+ [&strConnectionString](const AddonInstance* addon) {
+ return addon->toAddon->GetConnectionString(addon, strConnectionString,
+ sizeof(strConnectionString));
+ },
+ true, false);
+
+ if (retVal != PVR_ERROR_NO_ERROR && retVal != PVR_ERROR_NOT_IMPLEMENTED)
+ return false;
+
+ /* backend version number */
+ retVal = DoAddonCall(
+ __func__,
+ [&strBackendVersion](const AddonInstance* addon) {
+ return addon->toAddon->GetBackendVersion(addon, strBackendVersion,
+ sizeof(strBackendVersion));
+ },
+ true, false);
+
+ if (retVal != PVR_ERROR_NO_ERROR)
+ return false;
+
+ /* backend hostname */
+ retVal = DoAddonCall(
+ __func__,
+ [&strBackendHostname](const AddonInstance* addon) {
+ return addon->toAddon->GetBackendHostname(addon, strBackendHostname,
+ sizeof(strBackendHostname));
+ },
+ true, false);
+
+ if (retVal != PVR_ERROR_NO_ERROR && retVal != PVR_ERROR_NOT_IMPLEMENTED)
+ return false;
+
+ /* update the members */
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strBackendName = strBackendName;
+ m_strConnectionString = strConnectionString;
+ m_strBackendVersion = strBackendVersion;
+ m_strBackendHostname = strBackendHostname;
+
+ return true;
+}
+
+const std::string& CPVRClient::GetBackendName() const
+{
+ return m_strBackendName;
+}
+
+const std::string& CPVRClient::GetBackendVersion() const
+{
+ return m_strBackendVersion;
+}
+
+const std::string& CPVRClient::GetBackendHostname() const
+{
+ return m_strBackendHostname;
+}
+
+const std::string& CPVRClient::GetConnectionString() const
+{
+ return m_strConnectionString;
+}
+
+const std::string CPVRClient::GetFriendlyName() const
+{
+
+ if (Addon()->SupportsInstanceSettings())
+ {
+ std::string instanceName;
+ Addon()->GetSettingString(ADDON_SETTING_INSTANCE_NAME_VALUE, instanceName, InstanceId());
+ if (!instanceName.empty())
+ return StringUtils::Format("{} ({})", Name(), instanceName);
+ }
+ return Name();
+}
+
+PVR_ERROR CPVRClient::GetDriveSpace(uint64_t& iTotal, uint64_t& iUsed)
+{
+ /* default to 0 in case of error */
+ iTotal = 0;
+ iUsed = 0;
+
+ return DoAddonCall(__func__, [&iTotal, &iUsed](const AddonInstance* addon) {
+ uint64_t iTotalSpace = 0;
+ uint64_t iUsedSpace = 0;
+ PVR_ERROR error = addon->toAddon->GetDriveSpace(addon, &iTotalSpace, &iUsedSpace);
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ iTotal = iTotalSpace;
+ iUsed = iUsedSpace;
+ }
+ return error;
+ });
+}
+
+PVR_ERROR CPVRClient::StartChannelScan()
+{
+ return DoAddonCall(
+ __func__,
+ [](const AddonInstance* addon) { return addon->toAddon->OpenDialogChannelScan(addon); },
+ m_clientCapabilities.SupportsChannelScan());
+}
+
+PVR_ERROR CPVRClient::OpenDialogChannelAdd(const std::shared_ptr<CPVRChannel>& channel)
+{
+ return DoAddonCall(
+ __func__,
+ [channel](const AddonInstance* addon) {
+ PVR_CHANNEL addonChannel;
+ channel->FillAddonData(addonChannel);
+ return addon->toAddon->OpenDialogChannelAdd(addon, &addonChannel);
+ },
+ m_clientCapabilities.SupportsChannelSettings());
+}
+
+PVR_ERROR CPVRClient::OpenDialogChannelSettings(const std::shared_ptr<CPVRChannel>& channel)
+{
+ return DoAddonCall(
+ __func__,
+ [channel](const AddonInstance* addon) {
+ PVR_CHANNEL addonChannel;
+ channel->FillAddonData(addonChannel);
+ return addon->toAddon->OpenDialogChannelSettings(addon, &addonChannel);
+ },
+ m_clientCapabilities.SupportsChannelSettings());
+}
+
+PVR_ERROR CPVRClient::DeleteChannel(const std::shared_ptr<CPVRChannel>& channel)
+{
+ return DoAddonCall(
+ __func__,
+ [channel](const AddonInstance* addon) {
+ PVR_CHANNEL addonChannel;
+ channel->FillAddonData(addonChannel);
+ return addon->toAddon->DeleteChannel(addon, &addonChannel);
+ },
+ m_clientCapabilities.SupportsChannelSettings());
+}
+
+PVR_ERROR CPVRClient::RenameChannel(const std::shared_ptr<CPVRChannel>& channel)
+{
+ return DoAddonCall(
+ __func__,
+ [channel](const AddonInstance* addon) {
+ PVR_CHANNEL addonChannel;
+ channel->FillAddonData(addonChannel);
+ strncpy(addonChannel.strChannelName, channel->ChannelName().c_str(),
+ sizeof(addonChannel.strChannelName) - 1);
+ return addon->toAddon->RenameChannel(addon, &addonChannel);
+ },
+ m_clientCapabilities.SupportsChannelSettings());
+}
+
+PVR_ERROR CPVRClient::GetEPGForChannel(int iChannelUid, CPVREpg* epg, time_t start, time_t end)
+{
+ return DoAddonCall(
+ __func__,
+ [this, iChannelUid, epg, start, end](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = epg;
+
+ int iPVRTimeCorrection =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection;
+
+ return addon->toAddon->GetEPGForChannel(addon, &handle, iChannelUid,
+ start ? start - iPVRTimeCorrection : 0,
+ end ? end - iPVRTimeCorrection : 0);
+ },
+ m_clientCapabilities.SupportsEPG());
+}
+
+PVR_ERROR CPVRClient::SetEPGMaxPastDays(int iPastDays)
+{
+ return DoAddonCall(
+ __func__,
+ [iPastDays](const AddonInstance* addon) {
+ return addon->toAddon->SetEPGMaxPastDays(addon, iPastDays);
+ },
+ m_clientCapabilities.SupportsEPG());
+}
+
+PVR_ERROR CPVRClient::SetEPGMaxFutureDays(int iFutureDays)
+{
+ return DoAddonCall(
+ __func__,
+ [iFutureDays](const AddonInstance* addon) {
+ return addon->toAddon->SetEPGMaxFutureDays(addon, iFutureDays);
+ },
+ m_clientCapabilities.SupportsEPG());
+}
+
+// This class wraps an EPG_TAG (PVR Addon API struct) to ensure that the string members of
+// that struct, which are const char pointers, stay valid until the EPG_TAG gets destructed.
+// Please note that this struct is also used to transfer huge amount of EPG_TAGs from
+// addon to Kodi. Thus, changing the struct to contain char arrays is not recommended,
+// because this would lead to huge amount of string copies when transferring epg data
+// from addon to Kodi.
+class CAddonEpgTag : public EPG_TAG
+{
+public:
+ CAddonEpgTag() = delete;
+ explicit CAddonEpgTag(const std::shared_ptr<const CPVREpgInfoTag>& kodiTag)
+ : m_strTitle(kodiTag->Title()),
+ m_strPlotOutline(kodiTag->PlotOutline()),
+ m_strPlot(kodiTag->Plot()),
+ m_strOriginalTitle(kodiTag->OriginalTitle()),
+ m_strCast(kodiTag->DeTokenize(kodiTag->Cast())),
+ m_strDirector(kodiTag->DeTokenize(kodiTag->Directors())),
+ m_strWriter(kodiTag->DeTokenize(kodiTag->Writers())),
+ m_strIMDBNumber(kodiTag->IMDBNumber()),
+ m_strEpisodeName(kodiTag->EpisodeName()),
+ m_strIconPath(kodiTag->ClientIconPath()),
+ m_strSeriesLink(kodiTag->SeriesLink()),
+ m_strGenreDescription(kodiTag->GenreDescription()),
+ m_strParentalRatingCode(kodiTag->ParentalRatingCode())
+ {
+ time_t t;
+ kodiTag->StartAsUTC().GetAsTime(t);
+ startTime = t;
+ kodiTag->EndAsUTC().GetAsTime(t);
+ endTime = t;
+
+ const CDateTime firstAired = kodiTag->FirstAired();
+ if (firstAired.IsValid())
+ m_strFirstAired = firstAired.GetAsW3CDate();
+
+ iUniqueBroadcastId = kodiTag->UniqueBroadcastID();
+ iUniqueChannelId = kodiTag->UniqueChannelID();
+ iParentalRating = kodiTag->ParentalRating();
+ iSeriesNumber = kodiTag->SeriesNumber();
+ iEpisodeNumber = kodiTag->EpisodeNumber();
+ iEpisodePartNumber = kodiTag->EpisodePart();
+ iStarRating = kodiTag->StarRating();
+ iYear = kodiTag->Year();
+ iFlags = kodiTag->Flags();
+ iGenreType = kodiTag->GenreType();
+ iGenreSubType = kodiTag->GenreSubType();
+ strTitle = m_strTitle.c_str();
+ strPlotOutline = m_strPlotOutline.c_str();
+ strPlot = m_strPlot.c_str();
+ strOriginalTitle = m_strOriginalTitle.c_str();
+ strCast = m_strCast.c_str();
+ strDirector = m_strDirector.c_str();
+ strWriter = m_strWriter.c_str();
+ strIMDBNumber = m_strIMDBNumber.c_str();
+ strEpisodeName = m_strEpisodeName.c_str();
+ strIconPath = m_strIconPath.c_str();
+ strSeriesLink = m_strSeriesLink.c_str();
+ strGenreDescription = m_strGenreDescription.c_str();
+ strFirstAired = m_strFirstAired.c_str();
+ strParentalRatingCode = m_strParentalRatingCode.c_str();
+ }
+
+ virtual ~CAddonEpgTag() = default;
+
+private:
+ std::string m_strTitle;
+ std::string m_strPlotOutline;
+ std::string m_strPlot;
+ std::string m_strOriginalTitle;
+ std::string m_strCast;
+ std::string m_strDirector;
+ std::string m_strWriter;
+ std::string m_strIMDBNumber;
+ std::string m_strEpisodeName;
+ std::string m_strIconPath;
+ std::string m_strSeriesLink;
+ std::string m_strGenreDescription;
+ std::string m_strFirstAired;
+ std::string m_strParentalRatingCode;
+};
+
+PVR_ERROR CPVRClient::IsRecordable(const std::shared_ptr<const CPVREpgInfoTag>& tag,
+ bool& bIsRecordable) const
+{
+ return DoAddonCall(
+ __func__,
+ [tag, &bIsRecordable](const AddonInstance* addon) {
+ CAddonEpgTag addonTag(tag);
+ return addon->toAddon->IsEPGTagRecordable(addon, &addonTag, &bIsRecordable);
+ },
+ m_clientCapabilities.SupportsRecordings() && m_clientCapabilities.SupportsEPG());
+}
+
+PVR_ERROR CPVRClient::IsPlayable(const std::shared_ptr<const CPVREpgInfoTag>& tag,
+ bool& bIsPlayable) const
+{
+ return DoAddonCall(
+ __func__,
+ [tag, &bIsPlayable](const AddonInstance* addon) {
+ CAddonEpgTag addonTag(tag);
+ return addon->toAddon->IsEPGTagPlayable(addon, &addonTag, &bIsPlayable);
+ },
+ m_clientCapabilities.SupportsEPG());
+}
+
+void CPVRClient::WriteStreamProperties(const PVR_NAMED_VALUE* properties,
+ unsigned int iPropertyCount,
+ CPVRStreamProperties& props)
+{
+ for (unsigned int i = 0; i < iPropertyCount; ++i)
+ {
+ props.emplace_back(std::make_pair(properties[i].strName, properties[i].strValue));
+ }
+}
+
+PVR_ERROR CPVRClient::GetEpgTagStreamProperties(const std::shared_ptr<CPVREpgInfoTag>& tag,
+ CPVRStreamProperties& props)
+{
+ return DoAddonCall(__func__, [&tag, &props](const AddonInstance* addon) {
+ CAddonEpgTag addonTag(tag);
+
+ unsigned int iPropertyCount = STREAM_MAX_PROPERTY_COUNT;
+ std::unique_ptr<PVR_NAMED_VALUE[]> properties(new PVR_NAMED_VALUE[iPropertyCount]);
+ memset(properties.get(), 0, iPropertyCount * sizeof(PVR_NAMED_VALUE));
+
+ PVR_ERROR error = addon->toAddon->GetEPGTagStreamProperties(addon, &addonTag, properties.get(),
+ &iPropertyCount);
+ if (error == PVR_ERROR_NO_ERROR)
+ WriteStreamProperties(properties.get(), iPropertyCount, props);
+
+ return error;
+ });
+}
+
+PVR_ERROR CPVRClient::GetEpgTagEdl(const std::shared_ptr<const CPVREpgInfoTag>& epgTag,
+ std::vector<PVR_EDL_ENTRY>& edls)
+{
+ edls.clear();
+ return DoAddonCall(
+ __func__,
+ [&epgTag, &edls](const AddonInstance* addon) {
+ CAddonEpgTag addonTag(epgTag);
+
+ PVR_EDL_ENTRY edl_array[PVR_ADDON_EDL_LENGTH];
+ int size = PVR_ADDON_EDL_LENGTH;
+ PVR_ERROR error = addon->toAddon->GetEPGTagEdl(addon, &addonTag, edl_array, &size);
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ edls.reserve(size);
+ for (int i = 0; i < size; ++i)
+ edls.emplace_back(edl_array[i]);
+ }
+ return error;
+ },
+ m_clientCapabilities.SupportsEpgTagEdl());
+}
+
+PVR_ERROR CPVRClient::GetChannelGroupsAmount(int& iGroups)
+{
+ iGroups = -1;
+ return DoAddonCall(
+ __func__,
+ [&iGroups](const AddonInstance* addon) {
+ return addon->toAddon->GetChannelGroupsAmount(addon, &iGroups);
+ },
+ m_clientCapabilities.SupportsChannelGroups());
+}
+
+PVR_ERROR CPVRClient::GetChannelGroups(CPVRChannelGroups* groups)
+{
+ return DoAddonCall(__func__,
+ [this, groups](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = groups;
+ return addon->toAddon->GetChannelGroups(addon, &handle, groups->IsRadio());
+ },
+ m_clientCapabilities.SupportsChannelGroups());
+}
+
+PVR_ERROR CPVRClient::GetChannelGroupMembers(
+ CPVRChannelGroup* group, std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers)
+{
+ return DoAddonCall(__func__,
+ [this, group, &groupMembers](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = &groupMembers;
+
+ PVR_CHANNEL_GROUP tag;
+ group->FillAddonData(tag);
+ return addon->toAddon->GetChannelGroupMembers(addon, &handle, &tag);
+ },
+ m_clientCapabilities.SupportsChannelGroups());
+}
+
+PVR_ERROR CPVRClient::GetProvidersAmount(int& iProviders)
+{
+ iProviders = -1;
+ return DoAddonCall(__func__, [&iProviders](const AddonInstance* addon) {
+ return addon->toAddon->GetProvidersAmount(addon, &iProviders);
+ });
+}
+
+PVR_ERROR CPVRClient::GetProviders(CPVRProvidersContainer& providers)
+{
+ return DoAddonCall(__func__,
+ [this, &providers](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = &providers;
+ return addon->toAddon->GetProviders(addon, &handle);
+ },
+ m_clientCapabilities.SupportsProviders());
+}
+
+PVR_ERROR CPVRClient::GetChannelsAmount(int& iChannels)
+{
+ iChannels = -1;
+ return DoAddonCall(__func__, [&iChannels](const AddonInstance* addon) {
+ return addon->toAddon->GetChannelsAmount(addon, &iChannels);
+ });
+}
+
+PVR_ERROR CPVRClient::GetChannels(bool radio, std::vector<std::shared_ptr<CPVRChannel>>& channels)
+{
+ return DoAddonCall(__func__,
+ [this, radio, &channels](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = &channels;
+ return addon->toAddon->GetChannels(addon, &handle, radio);
+ },
+ (radio && m_clientCapabilities.SupportsRadio()) ||
+ (!radio && m_clientCapabilities.SupportsTV()));
+}
+
+PVR_ERROR CPVRClient::GetRecordingsAmount(bool deleted, int& iRecordings)
+{
+ iRecordings = -1;
+ return DoAddonCall(
+ __func__,
+ [deleted, &iRecordings](const AddonInstance* addon) {
+ return addon->toAddon->GetRecordingsAmount(addon, deleted, &iRecordings);
+ },
+ m_clientCapabilities.SupportsRecordings() &&
+ (!deleted || m_clientCapabilities.SupportsRecordingsUndelete()));
+}
+
+PVR_ERROR CPVRClient::GetRecordings(CPVRRecordings* results, bool deleted)
+{
+ return DoAddonCall(__func__,
+ [this, results, deleted](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = results;
+ return addon->toAddon->GetRecordings(addon, &handle, deleted);
+ },
+ m_clientCapabilities.SupportsRecordings() &&
+ (!deleted || m_clientCapabilities.SupportsRecordingsUndelete()));
+}
+
+PVR_ERROR CPVRClient::DeleteRecording(const CPVRRecording& recording)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->DeleteRecording(addon, &tag);
+ },
+ m_clientCapabilities.SupportsRecordings() && m_clientCapabilities.SupportsRecordingsDelete());
+}
+
+PVR_ERROR CPVRClient::UndeleteRecording(const CPVRRecording& recording)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->UndeleteRecording(addon, &tag);
+ },
+ m_clientCapabilities.SupportsRecordingsUndelete());
+}
+
+PVR_ERROR CPVRClient::DeleteAllRecordingsFromTrash()
+{
+ return DoAddonCall(
+ __func__,
+ [](const AddonInstance* addon) {
+ return addon->toAddon->DeleteAllRecordingsFromTrash(addon);
+ },
+ m_clientCapabilities.SupportsRecordingsUndelete());
+}
+
+PVR_ERROR CPVRClient::RenameRecording(const CPVRRecording& recording)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->RenameRecording(addon, &tag);
+ },
+ m_clientCapabilities.SupportsRecordings());
+}
+
+PVR_ERROR CPVRClient::SetRecordingLifetime(const CPVRRecording& recording)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->SetRecordingLifetime(addon, &tag);
+ },
+ m_clientCapabilities.SupportsRecordingsLifetimeChange());
+}
+
+PVR_ERROR CPVRClient::SetRecordingPlayCount(const CPVRRecording& recording, int count)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording, count](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->SetRecordingPlayCount(addon, &tag, count);
+ },
+ m_clientCapabilities.SupportsRecordingsPlayCount());
+}
+
+PVR_ERROR CPVRClient::SetRecordingLastPlayedPosition(const CPVRRecording& recording,
+ int lastplayedposition)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording, lastplayedposition](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->SetRecordingLastPlayedPosition(addon, &tag, lastplayedposition);
+ },
+ m_clientCapabilities.SupportsRecordingsLastPlayedPosition());
+}
+
+PVR_ERROR CPVRClient::GetRecordingLastPlayedPosition(const CPVRRecording& recording, int& iPosition)
+{
+ iPosition = -1;
+ return DoAddonCall(
+ __func__,
+ [&recording, &iPosition](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->GetRecordingLastPlayedPosition(addon, &tag, &iPosition);
+ },
+ m_clientCapabilities.SupportsRecordingsLastPlayedPosition());
+}
+
+PVR_ERROR CPVRClient::GetRecordingEdl(const CPVRRecording& recording,
+ std::vector<PVR_EDL_ENTRY>& edls)
+{
+ edls.clear();
+ return DoAddonCall(
+ __func__,
+ [&recording, &edls](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+
+ PVR_EDL_ENTRY edl_array[PVR_ADDON_EDL_LENGTH];
+ int size = PVR_ADDON_EDL_LENGTH;
+ PVR_ERROR error = addon->toAddon->GetRecordingEdl(addon, &tag, edl_array, &size);
+ if (error == PVR_ERROR_NO_ERROR)
+ {
+ edls.reserve(size);
+ for (int i = 0; i < size; ++i)
+ edls.emplace_back(edl_array[i]);
+ }
+ return error;
+ },
+ m_clientCapabilities.SupportsRecordingsEdl());
+}
+
+PVR_ERROR CPVRClient::GetRecordingSize(const CPVRRecording& recording, int64_t& sizeInBytes)
+{
+ return DoAddonCall(
+ __func__,
+ [&recording, &sizeInBytes](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording.FillAddonData(tag);
+ return addon->toAddon->GetRecordingSize(addon, &tag, &sizeInBytes);
+ },
+ m_clientCapabilities.SupportsRecordingsSize());
+}
+
+PVR_ERROR CPVRClient::GetTimersAmount(int& iTimers)
+{
+ iTimers = -1;
+ return DoAddonCall(
+ __func__,
+ [&iTimers](const AddonInstance* addon) {
+ return addon->toAddon->GetTimersAmount(addon, &iTimers);
+ },
+ m_clientCapabilities.SupportsTimers());
+}
+
+PVR_ERROR CPVRClient::GetTimers(CPVRTimersContainer* results)
+{
+ return DoAddonCall(__func__,
+ [this, results](const AddonInstance* addon) {
+ PVR_HANDLE_STRUCT handle = {};
+ handle.callerAddress = this;
+ handle.dataAddress = results;
+ return addon->toAddon->GetTimers(addon, &handle);
+ },
+ m_clientCapabilities.SupportsTimers());
+}
+
+PVR_ERROR CPVRClient::AddTimer(const CPVRTimerInfoTag& timer)
+{
+ return DoAddonCall(
+ __func__,
+ [&timer](const AddonInstance* addon) {
+ PVR_TIMER tag;
+ timer.FillAddonData(tag);
+ return addon->toAddon->AddTimer(addon, &tag);
+ },
+ m_clientCapabilities.SupportsTimers());
+}
+
+PVR_ERROR CPVRClient::DeleteTimer(const CPVRTimerInfoTag& timer, bool bForce /* = false */)
+{
+ return DoAddonCall(
+ __func__,
+ [&timer, bForce](const AddonInstance* addon) {
+ PVR_TIMER tag;
+ timer.FillAddonData(tag);
+ return addon->toAddon->DeleteTimer(addon, &tag, bForce);
+ },
+ m_clientCapabilities.SupportsTimers());
+}
+
+PVR_ERROR CPVRClient::UpdateTimer(const CPVRTimerInfoTag& timer)
+{
+ return DoAddonCall(
+ __func__,
+ [&timer](const AddonInstance* addon) {
+ PVR_TIMER tag;
+ timer.FillAddonData(tag);
+ return addon->toAddon->UpdateTimer(addon, &tag);
+ },
+ m_clientCapabilities.SupportsTimers());
+}
+
+PVR_ERROR CPVRClient::GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ results = m_timertypes;
+ return PVR_ERROR_NO_ERROR;
+}
+
+PVR_ERROR CPVRClient::GetStreamReadChunkSize(int& iChunkSize)
+{
+ return DoAddonCall(
+ __func__,
+ [&iChunkSize](const AddonInstance* addon) {
+ return addon->toAddon->GetStreamReadChunkSize(addon, &iChunkSize);
+ },
+ m_clientCapabilities.SupportsRecordings() || m_clientCapabilities.HandlesInputStream());
+}
+
+PVR_ERROR CPVRClient::ReadLiveStream(void* lpBuf, int64_t uiBufSize, int& iRead)
+{
+ iRead = -1;
+ return DoAddonCall(__func__, [&lpBuf, uiBufSize, &iRead](const AddonInstance* addon) {
+ iRead = addon->toAddon->ReadLiveStream(addon, static_cast<unsigned char*>(lpBuf),
+ static_cast<int>(uiBufSize));
+ return (iRead == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::ReadRecordedStream(void* lpBuf, int64_t uiBufSize, int& iRead)
+{
+ iRead = -1;
+ return DoAddonCall(__func__, [&lpBuf, uiBufSize, &iRead](const AddonInstance* addon) {
+ iRead = addon->toAddon->ReadRecordedStream(addon, static_cast<unsigned char*>(lpBuf),
+ static_cast<int>(uiBufSize));
+ return (iRead == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::SeekLiveStream(int64_t iFilePosition, int iWhence, int64_t& iPosition)
+{
+ iPosition = -1;
+ return DoAddonCall(__func__, [iFilePosition, iWhence, &iPosition](const AddonInstance* addon) {
+ iPosition = addon->toAddon->SeekLiveStream(addon, iFilePosition, iWhence);
+ return (iPosition == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::SeekRecordedStream(int64_t iFilePosition, int iWhence, int64_t& iPosition)
+{
+ iPosition = -1;
+ return DoAddonCall(__func__, [iFilePosition, iWhence, &iPosition](const AddonInstance* addon) {
+ iPosition = addon->toAddon->SeekRecordedStream(addon, iFilePosition, iWhence);
+ return (iPosition == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::SeekTime(double time, bool backwards, double* startpts)
+{
+ return DoAddonCall(__func__, [time, backwards, &startpts](const AddonInstance* addon) {
+ return addon->toAddon->SeekTime(addon, time, backwards, startpts) ? PVR_ERROR_NO_ERROR
+ : PVR_ERROR_NOT_IMPLEMENTED;
+ });
+}
+
+PVR_ERROR CPVRClient::GetLiveStreamLength(int64_t& iLength)
+{
+ iLength = -1;
+ return DoAddonCall(__func__, [&iLength](const AddonInstance* addon) {
+ iLength = addon->toAddon->LengthLiveStream(addon);
+ return (iLength == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::GetRecordedStreamLength(int64_t& iLength)
+{
+ iLength = -1;
+ return DoAddonCall(__func__, [&iLength](const AddonInstance* addon) {
+ iLength = addon->toAddon->LengthRecordedStream(addon);
+ return (iLength == -1) ? PVR_ERROR_NOT_IMPLEMENTED : PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::SignalQuality(int channelUid, PVR_SIGNAL_STATUS& qualityinfo)
+{
+ return DoAddonCall(__func__, [channelUid, &qualityinfo](const AddonInstance* addon) {
+ return addon->toAddon->GetSignalStatus(addon, channelUid, &qualityinfo);
+ });
+}
+
+PVR_ERROR CPVRClient::GetDescrambleInfo(int channelUid, PVR_DESCRAMBLE_INFO& descrambleinfo) const
+{
+ return DoAddonCall(
+ __func__,
+ [channelUid, &descrambleinfo](const AddonInstance* addon) {
+ return addon->toAddon->GetDescrambleInfo(addon, channelUid, &descrambleinfo);
+ },
+ m_clientCapabilities.SupportsDescrambleInfo());
+}
+
+PVR_ERROR CPVRClient::GetChannelStreamProperties(const std::shared_ptr<CPVRChannel>& channel,
+ CPVRStreamProperties& props)
+{
+ return DoAddonCall(__func__, [this, &channel, &props](const AddonInstance* addon) {
+ if (!CanPlayChannel(channel))
+ return PVR_ERROR_NO_ERROR; // no error, but no need to obtain the values from the addon
+
+ PVR_CHANNEL tag = {};
+ channel->FillAddonData(tag);
+
+ unsigned int iPropertyCount = STREAM_MAX_PROPERTY_COUNT;
+ std::unique_ptr<PVR_NAMED_VALUE[]> properties(new PVR_NAMED_VALUE[iPropertyCount]);
+ memset(properties.get(), 0, iPropertyCount * sizeof(PVR_NAMED_VALUE));
+
+ PVR_ERROR error =
+ addon->toAddon->GetChannelStreamProperties(addon, &tag, properties.get(), &iPropertyCount);
+ if (error == PVR_ERROR_NO_ERROR)
+ WriteStreamProperties(properties.get(), iPropertyCount, props);
+
+ return error;
+ });
+}
+
+PVR_ERROR CPVRClient::GetRecordingStreamProperties(const std::shared_ptr<CPVRRecording>& recording,
+ CPVRStreamProperties& props)
+{
+ return DoAddonCall(__func__, [this, &recording, &props](const AddonInstance* addon) {
+ if (!m_clientCapabilities.SupportsRecordings())
+ return PVR_ERROR_NO_ERROR; // no error, but no need to obtain the values from the addon
+
+ PVR_RECORDING tag = {};
+ recording->FillAddonData(tag);
+
+ unsigned int iPropertyCount = STREAM_MAX_PROPERTY_COUNT;
+ std::unique_ptr<PVR_NAMED_VALUE[]> properties(new PVR_NAMED_VALUE[iPropertyCount]);
+ memset(properties.get(), 0, iPropertyCount * sizeof(PVR_NAMED_VALUE));
+
+ PVR_ERROR error = addon->toAddon->GetRecordingStreamProperties(addon, &tag, properties.get(),
+ &iPropertyCount);
+ if (error == PVR_ERROR_NO_ERROR)
+ WriteStreamProperties(properties.get(), iPropertyCount, props);
+
+ return error;
+ });
+}
+
+PVR_ERROR CPVRClient::GetStreamProperties(PVR_STREAM_PROPERTIES* props)
+{
+ return DoAddonCall(__func__, [&props](const AddonInstance* addon) {
+ return addon->toAddon->GetStreamProperties(addon, props);
+ });
+}
+
+PVR_ERROR CPVRClient::DemuxReset()
+{
+ return DoAddonCall(
+ __func__,
+ [](const AddonInstance* addon) {
+ addon->toAddon->DemuxReset(addon);
+ return PVR_ERROR_NO_ERROR;
+ },
+ m_clientCapabilities.HandlesDemuxing());
+}
+
+PVR_ERROR CPVRClient::DemuxAbort()
+{
+ return DoAddonCall(
+ __func__,
+ [](const AddonInstance* addon) {
+ addon->toAddon->DemuxAbort(addon);
+ return PVR_ERROR_NO_ERROR;
+ },
+ m_clientCapabilities.HandlesDemuxing());
+}
+
+PVR_ERROR CPVRClient::DemuxFlush()
+{
+ return DoAddonCall(
+ __func__,
+ [](const AddonInstance* addon) {
+ addon->toAddon->DemuxFlush(addon);
+ return PVR_ERROR_NO_ERROR;
+ },
+ m_clientCapabilities.HandlesDemuxing());
+}
+
+PVR_ERROR CPVRClient::DemuxRead(DemuxPacket*& packet)
+{
+ return DoAddonCall(
+ __func__,
+ [&packet](const AddonInstance* addon) {
+ packet = static_cast<DemuxPacket*>(addon->toAddon->DemuxRead(addon));
+ return packet ? PVR_ERROR_NO_ERROR : PVR_ERROR_NOT_IMPLEMENTED;
+ },
+ m_clientCapabilities.HandlesDemuxing());
+}
+
+const char* CPVRClient::ToString(const PVR_ERROR error)
+{
+ switch (error)
+ {
+ case PVR_ERROR_NO_ERROR:
+ return "no error";
+ case PVR_ERROR_NOT_IMPLEMENTED:
+ return "not implemented";
+ case PVR_ERROR_SERVER_ERROR:
+ return "server error";
+ case PVR_ERROR_SERVER_TIMEOUT:
+ return "server timeout";
+ case PVR_ERROR_RECORDING_RUNNING:
+ return "recording already running";
+ case PVR_ERROR_ALREADY_PRESENT:
+ return "already present";
+ case PVR_ERROR_REJECTED:
+ return "rejected by the backend";
+ case PVR_ERROR_INVALID_PARAMETERS:
+ return "invalid parameters for this method";
+ case PVR_ERROR_FAILED:
+ return "the command failed";
+ case PVR_ERROR_UNKNOWN:
+ default:
+ return "unknown error";
+ }
+}
+
+PVR_ERROR CPVRClient::DoAddonCall(const char* strFunctionName,
+ const std::function<PVR_ERROR(const AddonInstance*)>& function,
+ bool bIsImplemented /* = true */,
+ bool bCheckReadyToUse /* = true */) const
+{
+ // Check preconditions.
+ if (!bIsImplemented)
+ return PVR_ERROR_NOT_IMPLEMENTED;
+
+ if (m_bBlockAddonCalls)
+ {
+ CLog::Log(LOGWARNING, "{}: Blocking call to add-on '{}'.", strFunctionName, ID());
+ return PVR_ERROR_SERVER_ERROR;
+ }
+
+ if (bCheckReadyToUse && IgnoreClient())
+ {
+ CLog::Log(LOGWARNING, "{}: Blocking call to add-on '{}'. Add-on not (yet) connected.",
+ strFunctionName, ID());
+ return PVR_ERROR_SERVER_ERROR;
+ }
+
+ if (bCheckReadyToUse && !ReadyToUse())
+ {
+ CLog::Log(LOGWARNING, "{}: Blocking call to add-on '{}'. Add-on not ready to use.",
+ strFunctionName, ID());
+ return PVR_ERROR_SERVER_ERROR;
+ }
+
+ // Call.
+ m_allAddonCallsFinished.Reset();
+ m_iAddonCalls++;
+
+ const PVR_ERROR error = function(m_ifc.pvr);
+
+ m_iAddonCalls--;
+ if (m_iAddonCalls == 0)
+ m_allAddonCallsFinished.Set();
+
+ // Log error, if any.
+ if (error != PVR_ERROR_NO_ERROR && error != PVR_ERROR_NOT_IMPLEMENTED)
+ CLog::Log(LOGERROR, "{}: Add-on '{}' returned an error: {}", strFunctionName, ID(),
+ ToString(error));
+
+ return error;
+}
+
+bool CPVRClient::CanPlayChannel(const std::shared_ptr<CPVRChannel>& channel) const
+{
+ return (m_bReadyToUse && ((m_clientCapabilities.SupportsTV() && !channel->IsRadio()) ||
+ (m_clientCapabilities.SupportsRadio() && channel->IsRadio())));
+}
+
+PVR_ERROR CPVRClient::OpenLiveStream(const std::shared_ptr<CPVRChannel>& channel)
+{
+ if (!channel)
+ return PVR_ERROR_INVALID_PARAMETERS;
+
+ return DoAddonCall(__func__, [this, channel](const AddonInstance* addon) {
+ CloseLiveStream();
+
+ if (!CanPlayChannel(channel))
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Add-on '{}' can not play channel '{}'", ID(),
+ channel->ChannelName());
+ return PVR_ERROR_SERVER_ERROR;
+ }
+ else
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Opening live stream for channel '{}'", channel->ChannelName());
+ PVR_CHANNEL tag;
+ channel->FillAddonData(tag);
+ return addon->toAddon->OpenLiveStream(addon, &tag) ? PVR_ERROR_NO_ERROR
+ : PVR_ERROR_NOT_IMPLEMENTED;
+ }
+ });
+}
+
+PVR_ERROR CPVRClient::OpenRecordedStream(const std::shared_ptr<CPVRRecording>& recording)
+{
+ if (!recording)
+ return PVR_ERROR_INVALID_PARAMETERS;
+
+ return DoAddonCall(
+ __func__,
+ [this, recording](const AddonInstance* addon) {
+ CloseRecordedStream();
+
+ PVR_RECORDING tag;
+ recording->FillAddonData(tag);
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Opening stream for recording '{}'", recording->m_strTitle);
+ return addon->toAddon->OpenRecordedStream(addon, &tag) ? PVR_ERROR_NO_ERROR
+ : PVR_ERROR_NOT_IMPLEMENTED;
+ },
+ m_clientCapabilities.SupportsRecordings());
+}
+
+PVR_ERROR CPVRClient::CloseLiveStream()
+{
+ return DoAddonCall(__func__, [](const AddonInstance* addon) {
+ addon->toAddon->CloseLiveStream(addon);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::CloseRecordedStream()
+{
+ return DoAddonCall(__func__, [](const AddonInstance* addon) {
+ addon->toAddon->CloseRecordedStream(addon);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::PauseStream(bool bPaused)
+{
+ return DoAddonCall(__func__, [bPaused](const AddonInstance* addon) {
+ addon->toAddon->PauseStream(addon, bPaused);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::SetSpeed(int speed)
+{
+ return DoAddonCall(__func__, [speed](const AddonInstance* addon) {
+ addon->toAddon->SetSpeed(addon, speed);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::FillBuffer(bool mode)
+{
+ return DoAddonCall(__func__, [mode](const AddonInstance* addon) {
+ addon->toAddon->FillBuffer(addon, mode);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::CanPauseStream(bool& bCanPause) const
+{
+ bCanPause = false;
+ return DoAddonCall(__func__, [&bCanPause](const AddonInstance* addon) {
+ bCanPause = addon->toAddon->CanPauseStream(addon);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::CanSeekStream(bool& bCanSeek) const
+{
+ bCanSeek = false;
+ return DoAddonCall(__func__, [&bCanSeek](const AddonInstance* addon) {
+ bCanSeek = addon->toAddon->CanSeekStream(addon);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::GetStreamTimes(PVR_STREAM_TIMES* times)
+{
+ return DoAddonCall(__func__, [&times](const AddonInstance* addon) {
+ return addon->toAddon->GetStreamTimes(addon, times);
+ });
+}
+
+PVR_ERROR CPVRClient::IsRealTimeStream(bool& bRealTime) const
+{
+ bRealTime = false;
+ return DoAddonCall(__func__, [&bRealTime](const AddonInstance* addon) {
+ bRealTime = addon->toAddon->IsRealTimeStream(addon);
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+PVR_ERROR CPVRClient::OnSystemSleep()
+{
+ return DoAddonCall(
+ __func__, [](const AddonInstance* addon) { return addon->toAddon->OnSystemSleep(addon); });
+}
+
+PVR_ERROR CPVRClient::OnSystemWake()
+{
+ return DoAddonCall(
+ __func__, [](const AddonInstance* addon) { return addon->toAddon->OnSystemWake(addon); });
+}
+
+PVR_ERROR CPVRClient::OnPowerSavingActivated()
+{
+ return DoAddonCall(__func__, [](const AddonInstance* addon) {
+ return addon->toAddon->OnPowerSavingActivated(addon);
+ });
+}
+
+PVR_ERROR CPVRClient::OnPowerSavingDeactivated()
+{
+ return DoAddonCall(__func__, [](const AddonInstance* addon) {
+ return addon->toAddon->OnPowerSavingDeactivated(addon);
+ });
+}
+
+std::shared_ptr<CPVRClientMenuHooks> CPVRClient::GetMenuHooks()
+{
+ if (!m_menuhooks)
+ m_menuhooks.reset(new CPVRClientMenuHooks(ID()));
+
+ return m_menuhooks;
+}
+
+PVR_ERROR CPVRClient::CallEpgTagMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ return DoAddonCall(__func__, [&hook, &tag](const AddonInstance* addon) {
+ CAddonEpgTag addonTag(tag);
+
+ PVR_MENUHOOK menuHook;
+ menuHook.category = PVR_MENUHOOK_EPG;
+ menuHook.iHookId = hook.GetId();
+ menuHook.iLocalizedStringId = hook.GetLabelId();
+
+ return addon->toAddon->CallEPGMenuHook(addon, &menuHook, &addonTag);
+ });
+}
+
+PVR_ERROR CPVRClient::CallChannelMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRChannel>& channel)
+{
+ return DoAddonCall(__func__, [&hook, &channel](const AddonInstance* addon) {
+ PVR_CHANNEL tag;
+ channel->FillAddonData(tag);
+
+ PVR_MENUHOOK menuHook;
+ menuHook.category = PVR_MENUHOOK_CHANNEL;
+ menuHook.iHookId = hook.GetId();
+ menuHook.iLocalizedStringId = hook.GetLabelId();
+
+ return addon->toAddon->CallChannelMenuHook(addon, &menuHook, &tag);
+ });
+}
+
+PVR_ERROR CPVRClient::CallRecordingMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRRecording>& recording,
+ bool bDeleted)
+{
+ return DoAddonCall(__func__, [&hook, &recording, &bDeleted](const AddonInstance* addon) {
+ PVR_RECORDING tag;
+ recording->FillAddonData(tag);
+
+ PVR_MENUHOOK menuHook;
+ menuHook.category = bDeleted ? PVR_MENUHOOK_DELETED_RECORDING : PVR_MENUHOOK_RECORDING;
+ menuHook.iHookId = hook.GetId();
+ menuHook.iLocalizedStringId = hook.GetLabelId();
+
+ return addon->toAddon->CallRecordingMenuHook(addon, &menuHook, &tag);
+ });
+}
+
+PVR_ERROR CPVRClient::CallTimerMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRTimerInfoTag>& timer)
+{
+ return DoAddonCall(__func__, [&hook, &timer](const AddonInstance* addon) {
+ PVR_TIMER tag;
+ timer->FillAddonData(tag);
+
+ PVR_MENUHOOK menuHook;
+ menuHook.category = PVR_MENUHOOK_TIMER;
+ menuHook.iHookId = hook.GetId();
+ menuHook.iLocalizedStringId = hook.GetLabelId();
+
+ return addon->toAddon->CallTimerMenuHook(addon, &menuHook, &tag);
+ });
+}
+
+PVR_ERROR CPVRClient::CallSettingsMenuHook(const CPVRClientMenuHook& hook)
+{
+ return DoAddonCall(__func__, [&hook](const AddonInstance* addon) {
+ PVR_MENUHOOK menuHook;
+ menuHook.category = PVR_MENUHOOK_SETTING;
+ menuHook.iHookId = hook.GetId();
+ menuHook.iLocalizedStringId = hook.GetLabelId();
+
+ return addon->toAddon->CallSettingsMenuHook(addon, &menuHook);
+ });
+}
+
+void CPVRClient::SetPriority(int iPriority)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_iPriority != iPriority)
+ {
+ m_iPriority = iPriority;
+ if (m_iClientId > PVR_INVALID_CLIENT_ID)
+ {
+ CServiceBroker::GetPVRManager().GetTVDatabase()->Persist(*this);
+ m_bPriorityFetched = true;
+ }
+ }
+}
+
+int CPVRClient::GetPriority() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_bPriorityFetched && m_iClientId > PVR_INVALID_CLIENT_ID)
+ {
+ m_iPriority = CServiceBroker::GetPVRManager().GetTVDatabase()->GetPriority(*this);
+ m_bPriorityFetched = true;
+ }
+ return m_iPriority;
+}
+
+void CPVRClient::HandleAddonCallback(const char* strFunctionName,
+ void* kodiInstance,
+ const std::function<void(CPVRClient* client)>& function,
+ bool bForceCall /* = false */)
+{
+ // Check preconditions.
+ CPVRClient* client = static_cast<CPVRClient*>(kodiInstance);
+ if (!client)
+ {
+ CLog::Log(LOGERROR, "{}: No instance pointer given!", strFunctionName);
+ return;
+ }
+
+ if (!bForceCall && client->m_bBlockAddonCalls && client->m_iAddonCalls == 0)
+ {
+ CLog::Log(LOGWARNING, LOGPVR, "{}: Ignoring callback from PVR client '{}'", strFunctionName,
+ client->ID());
+ return;
+ }
+
+ // Call.
+ function(client);
+}
+
+void CPVRClient::cb_transfer_channel_group(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL_GROUP* group)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !group)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ if (strlen(group->strGroupName) == 0)
+ {
+ CLog::LogF(LOGERROR, "Empty group name");
+ return;
+ }
+
+ // transfer this entry to the groups container
+ CPVRChannelGroups* kodiGroups = static_cast<CPVRChannelGroups*>(handle->dataAddress);
+ const auto transferGroup =
+ std::make_shared<CPVRChannelGroup>(*group, kodiGroups->GetGroupAll());
+ kodiGroups->UpdateFromClient(transferGroup);
+ });
+}
+
+void CPVRClient::cb_transfer_channel_group_member(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL_GROUP_MEMBER* member)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !member)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetByUniqueID(member->iChannelUniqueId,
+ client->GetID());
+ if (!channel)
+ {
+ CLog::LogF(LOGERROR, "Cannot find group '{}' or channel '{}'", member->strGroupName,
+ member->iChannelUniqueId);
+ }
+ else
+ {
+ auto* groupMembers =
+ static_cast<std::vector<std::shared_ptr<CPVRChannelGroupMember>>*>(handle->dataAddress);
+ groupMembers->emplace_back(
+ std::make_shared<CPVRChannelGroupMember>(member->strGroupName, member->iOrder, channel));
+ }
+ });
+}
+
+void CPVRClient::cb_transfer_epg_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const EPG_TAG* epgentry)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !epgentry)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ // transfer this entry to the epg
+ CPVREpg* epg = static_cast<CPVREpg*>(handle->dataAddress);
+ epg->UpdateEntry(epgentry, client->GetID());
+ });
+}
+
+void CPVRClient::cb_transfer_provider_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_PROVIDER* provider)
+{
+ if (!handle)
+ {
+ CLog::LogF(LOGERROR, "Invalid handler data");
+ return;
+ }
+
+ CPVRClient* client = static_cast<CPVRClient*>(kodiInstance);
+ CPVRProvidersContainer* kodiProviders = static_cast<CPVRProvidersContainer*>(handle->dataAddress);
+ if (!provider || !client || !kodiProviders)
+ {
+ CLog::LogF(LOGERROR, "Invalid handler data");
+ return;
+ }
+
+ /* transfer this entry to the internal channels group */
+ std::shared_ptr<CPVRProvider> transferProvider(
+ std::make_shared<CPVRProvider>(*provider, client->GetID()));
+ kodiProviders->UpdateFromClient(transferProvider);
+}
+
+void CPVRClient::cb_transfer_channel_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL* channel)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !channel)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ auto* channels = static_cast<std::vector<std::shared_ptr<CPVRChannel>>*>(handle->dataAddress);
+ channels->emplace_back(std::make_shared<CPVRChannel>(*channel, client->GetID()));
+ });
+}
+
+void CPVRClient::cb_transfer_recording_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_RECORDING* recording)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !recording)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ // transfer this entry to the recordings container
+ const std::shared_ptr<CPVRRecording> transferRecording =
+ std::make_shared<CPVRRecording>(*recording, client->GetID());
+ CPVRRecordings* recordings = static_cast<CPVRRecordings*>(handle->dataAddress);
+ recordings->UpdateFromClient(transferRecording, *client);
+ });
+}
+
+void CPVRClient::cb_transfer_timer_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_TIMER* timer)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!handle || !timer)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ // Note: channel can be nullptr here, for instance for epg-based timer rules
+ // ("record on any channel" condition)
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetByUniqueID(timer->iClientChannelUid,
+ client->GetID());
+
+ // transfer this entry to the timers container
+ const std::shared_ptr<CPVRTimerInfoTag> transferTimer =
+ std::make_shared<CPVRTimerInfoTag>(*timer, channel, client->GetID());
+ CPVRTimersContainer* timers = static_cast<CPVRTimersContainer*>(handle->dataAddress);
+ timers->UpdateFromClient(transferTimer);
+ });
+}
+
+void CPVRClient::cb_add_menu_hook(void* kodiInstance, const PVR_MENUHOOK* hook)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!hook)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ client->GetMenuHooks()->AddHook(*hook);
+ });
+}
+
+void CPVRClient::cb_recording_notification(void* kodiInstance,
+ const char* strName,
+ const char* strFileName,
+ bool bOnOff)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!strFileName)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ const std::string strLine1 = StringUtils::Format(g_localizeStrings.Get(bOnOff ? 19197 : 19198),
+ client->GetFriendlyName());
+ std::string strLine2;
+ if (strName)
+ strLine2 = strName;
+ else
+ strLine2 = strFileName;
+
+ // display a notification for 5 seconds
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, strLine1, strLine2, 5000,
+ false);
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(
+ new CNotificationEvent(client->GetFriendlyName(), strLine1, client->Icon(), strLine2)));
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Recording {} on client '{}'. name='{}' filename='{}'",
+ bOnOff ? "started" : "finished", client->ID(), strName, strFileName);
+ });
+}
+
+void CPVRClient::cb_trigger_channel_update(void* kodiInstance)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ // update channels in the next iteration of the pvrmanager's main loop
+ CServiceBroker::GetPVRManager().TriggerChannelsUpdate(client->GetID());
+ });
+}
+
+void CPVRClient::cb_trigger_provider_update(void* kodiInstance)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ /* update the providers table in the next iteration of the pvrmanager's main loop */
+ CServiceBroker::GetPVRManager().TriggerProvidersUpdate(client->GetID());
+ });
+}
+
+void CPVRClient::cb_trigger_timer_update(void* kodiInstance)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ // update timers in the next iteration of the pvrmanager's main loop
+ CServiceBroker::GetPVRManager().TriggerTimersUpdate(client->GetID());
+ });
+}
+
+void CPVRClient::cb_trigger_recording_update(void* kodiInstance)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ // update recordings in the next iteration of the pvrmanager's main loop
+ CServiceBroker::GetPVRManager().TriggerRecordingsUpdate(client->GetID());
+ });
+}
+
+void CPVRClient::cb_trigger_channel_groups_update(void* kodiInstance)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ // update all channel groups in the next iteration of the pvrmanager's main loop
+ CServiceBroker::GetPVRManager().TriggerChannelGroupsUpdate(client->GetID());
+ });
+}
+
+void CPVRClient::cb_trigger_epg_update(void* kodiInstance, unsigned int iChannelUid)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ CServiceBroker::GetPVRManager().EpgContainer().UpdateRequest(client->GetID(), iChannelUid);
+ });
+}
+
+void CPVRClient::cb_free_demux_packet(void* kodiInstance, DEMUX_PACKET* pPacket)
+{
+ HandleAddonCallback(__func__, kodiInstance,
+ [&](CPVRClient* client) {
+ CDVDDemuxUtils::FreeDemuxPacket(static_cast<DemuxPacket*>(pPacket));
+ },
+ true);
+}
+
+DEMUX_PACKET* CPVRClient::cb_allocate_demux_packet(void* kodiInstance, int iDataSize)
+{
+ DEMUX_PACKET* result = nullptr;
+
+ HandleAddonCallback(
+ __func__, kodiInstance,
+ [&](CPVRClient* client) { result = CDVDDemuxUtils::AllocateDemuxPacket(iDataSize); }, true);
+
+ return result;
+}
+
+void CPVRClient::cb_connection_state_change(void* kodiInstance,
+ const char* strConnectionString,
+ PVR_CONNECTION_STATE newState,
+ const char* strMessage)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!strConnectionString)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ const PVR_CONNECTION_STATE prevState(client->GetConnectionState());
+ if (prevState == newState)
+ return;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR,
+ "State for connection '{}' on client '{}' changed from '{}' to '{}'",
+ strConnectionString, client->ID(), prevState, newState);
+
+ client->SetConnectionState(newState);
+
+ std::string msg;
+ if (strMessage)
+ msg = strMessage;
+
+ CServiceBroker::GetPVRManager().ConnectionStateChange(client, std::string(strConnectionString),
+ newState, msg);
+ });
+}
+
+void CPVRClient::cb_epg_event_state_change(void* kodiInstance,
+ EPG_TAG* tag,
+ EPG_EVENT_STATE newState)
+{
+ HandleAddonCallback(__func__, kodiInstance, [&](CPVRClient* client) {
+ if (!tag)
+ {
+ CLog::LogF(LOGERROR, "Invalid callback parameter(s)");
+ return;
+ }
+
+ // Note: channel data and epg id may not yet be available. Tag will be fully initialized later.
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ std::make_shared<CPVREpgInfoTag>(*tag, client->GetID(), nullptr, -1);
+ CServiceBroker::GetPVRManager().EpgContainer().UpdateFromClient(epgTag, newState);
+ });
+}
+
+class CCodecIds
+{
+public:
+ virtual ~CCodecIds() = default;
+
+ static CCodecIds& GetInstance()
+ {
+ static CCodecIds _instance;
+ return _instance;
+ }
+
+ PVR_CODEC GetCodecByName(const char* strCodecName)
+ {
+ PVR_CODEC retVal = PVR_INVALID_CODEC;
+ if (strlen(strCodecName) == 0)
+ return retVal;
+
+ std::string strUpperCodecName = strCodecName;
+ StringUtils::ToUpper(strUpperCodecName);
+
+ std::map<std::string, PVR_CODEC>::const_iterator it = m_lookup.find(strUpperCodecName);
+ if (it != m_lookup.end())
+ retVal = it->second;
+
+ return retVal;
+ }
+
+private:
+ CCodecIds()
+ {
+ // get ids and names
+ const AVCodec* codec = nullptr;
+ void* i = nullptr;
+ PVR_CODEC tmp;
+ while ((codec = av_codec_iterate(&i)))
+ {
+ if (av_codec_is_decoder(codec))
+ {
+ tmp.codec_type = static_cast<PVR_CODEC_TYPE>(codec->type);
+ tmp.codec_id = codec->id;
+
+ std::string strUpperCodecName = codec->name;
+ StringUtils::ToUpper(strUpperCodecName);
+
+ m_lookup.insert(std::make_pair(strUpperCodecName, tmp));
+ }
+ }
+
+ // teletext is not returned by av_codec_next. we got our own decoder
+ tmp.codec_type = PVR_CODEC_TYPE_SUBTITLE;
+ tmp.codec_id = AV_CODEC_ID_DVB_TELETEXT;
+ m_lookup.insert(std::make_pair("TELETEXT", tmp));
+
+ // rds is not returned by av_codec_next. we got our own decoder
+ tmp.codec_type = PVR_CODEC_TYPE_RDS;
+ tmp.codec_id = AV_CODEC_ID_NONE;
+ m_lookup.insert(std::make_pair("RDS", tmp));
+
+ // ID3 is not returned by av_codec_next. we got our own decoder
+ tmp.codec_type = PVR_CODEC_TYPE_ID3;
+ tmp.codec_id = AV_CODEC_ID_NONE;
+ m_lookup.insert({"ID3", tmp});
+ }
+
+ std::map<std::string, PVR_CODEC> m_lookup;
+};
+
+PVR_CODEC CPVRClient::cb_get_codec_by_name(const void* kodiInstance, const char* strCodecName)
+{
+ PVR_CODEC result = PVR_INVALID_CODEC;
+
+ HandleAddonCallback(
+ __func__, const_cast<void*>(kodiInstance),
+ [&](CPVRClient* client) { result = CCodecIds::GetInstance().GetCodecByName(strCodecName); },
+ true);
+
+ return result;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/addons/PVRClient.h b/xbmc/pvr/addons/PVRClient.h
new file mode 100644
index 0000000..10203cf
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClient.h
@@ -0,0 +1,1062 @@
+/*
+ * 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 "addons/binary-addons/AddonInstanceHandler.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr.h"
+#include "pvr/addons/PVRClientCapabilities.h"
+#include "threads/Event.h"
+
+#include <atomic>
+#include <functional>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+struct DemuxPacket;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRChannelGroup;
+class CPVRChannelGroupMember;
+class CPVRChannelGroups;
+class CPVRProvider;
+class CPVRProvidersContainer;
+class CPVRClientMenuHook;
+class CPVRClientMenuHooks;
+class CPVREpg;
+class CPVREpgInfoTag;
+class CPVRRecording;
+class CPVRRecordings;
+class CPVRStreamProperties;
+class CPVRTimerInfoTag;
+class CPVRTimerType;
+class CPVRTimersContainer;
+
+#define PVR_INVALID_CLIENT_ID (-2)
+
+/*!
+ * Interface from Kodi to a PVR add-on.
+ *
+ * Also translates Kodi's C++ structures to the add-on's C structures.
+ */
+class CPVRClient : public ADDON::IAddonInstanceHandler
+{
+public:
+ CPVRClient(const ADDON::AddonInfoPtr& addonInfo, ADDON::AddonInstanceId instanceId, int clientId);
+ ~CPVRClient() override;
+
+ void OnPreInstall() override;
+ void OnPreUnInstall() override;
+
+ /** @name PVR add-on methods */
+ //@{
+
+ /*!
+ * @brief Initialise the instance of this add-on.
+ */
+ ADDON_STATUS Create();
+
+ /*!
+ * @brief Stop this add-on instance. No more client add-on access after this call.
+ */
+ void Stop();
+
+ /*!
+ * @brief Continue this add-on instance. Client add-on access is okay again after this call.
+ */
+ void Continue();
+
+ /*!
+ * @brief Destroy the instance of this add-on.
+ */
+ void Destroy();
+
+ /*!
+ * @brief Destroy and recreate this add-on.
+ */
+ void ReCreate();
+
+ /*!
+ * @return True if this instance is initialised (ADDON_Create returned true), false otherwise.
+ */
+ bool ReadyToUse() const;
+
+ /*!
+ * @brief Gets the backend connection state.
+ * @return the backend connection state.
+ */
+ PVR_CONNECTION_STATE GetConnectionState() const;
+
+ /*!
+ * @brief Sets the backend connection state.
+ * @param state the new backend connection state.
+ */
+ void SetConnectionState(PVR_CONNECTION_STATE state);
+
+ /*!
+ * @brief Gets the backend's previous connection state.
+ * @return the backend's previous connection state.
+ */
+ PVR_CONNECTION_STATE GetPreviousConnectionState() const;
+
+ /*!
+ * @brief Check whether this client should be ignored.
+ * @return True if this client should be ignored, false otherwise.
+ */
+ bool IgnoreClient() const;
+
+ /*!
+ * @brief Check whether this client is enabled, according to its instance/add-on configuration.
+ * @return True if this client is enabled, false otherwise.
+ */
+ bool IsEnabled() const;
+
+ /*!
+ * @return The ID of this instance.
+ */
+ int GetID() const;
+
+ //@}
+ /** @name PVR server methods */
+ //@{
+
+ /*!
+ * @brief Query this add-on's capabilities.
+ * @return The add-on's capabilities.
+ */
+ const CPVRClientCapabilities& GetClientCapabilities() const { return m_clientCapabilities; }
+
+ /*!
+ * @brief Get the stream properties of the stream that's currently being read.
+ * @param pProperties The properties.
+ * @return PVR_ERROR_NO_ERROR if the properties have been fetched successfully.
+ */
+ PVR_ERROR GetStreamProperties(PVR_STREAM_PROPERTIES* pProperties);
+
+ /*!
+ * @return The name reported by the backend.
+ */
+ const std::string& GetBackendName() const;
+
+ /*!
+ * @return The version string reported by the backend.
+ */
+ const std::string& GetBackendVersion() const;
+
+ /*!
+ * @brief the ip address or alias of the pvr backend server
+ */
+ const std::string& GetBackendHostname() const;
+
+ /*!
+ * @return The connection string reported by the backend.
+ */
+ const std::string& GetConnectionString() const;
+
+ /*!
+ * @brief A friendly name used to uniquely identify the addon instance
+ * @return string that can be used in log messages and the GUI.
+ */
+ const std::string GetFriendlyName() const;
+
+ /*!
+ * @brief Get the disk space reported by the server.
+ * @param iTotal The total disk space.
+ * @param iUsed The used disk space.
+ * @return PVR_ERROR_NO_ERROR if the drive space has been fetched successfully.
+ */
+ PVR_ERROR GetDriveSpace(uint64_t& iTotal, uint64_t& iUsed);
+
+ /*!
+ * @brief Start a channel scan on the server.
+ * @return PVR_ERROR_NO_ERROR if the channel scan has been started successfully.
+ */
+ PVR_ERROR StartChannelScan();
+
+ /*!
+ * @brief Request the client to open dialog about given channel to add
+ * @param channel The channel to add
+ * @return PVR_ERROR_NO_ERROR if the add has been fetched successfully.
+ */
+ PVR_ERROR OpenDialogChannelAdd(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Request the client to open dialog about given channel settings
+ * @param channel The channel to edit
+ * @return PVR_ERROR_NO_ERROR if the edit has been fetched successfully.
+ */
+ PVR_ERROR OpenDialogChannelSettings(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Request the client to delete given channel
+ * @param channel The channel to delete
+ * @return PVR_ERROR_NO_ERROR if the delete has been fetched successfully.
+ */
+ PVR_ERROR DeleteChannel(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Request the client to rename given channel
+ * @param channel The channel to rename
+ * @return PVR_ERROR_NO_ERROR if the rename has been fetched successfully.
+ */
+ PVR_ERROR RenameChannel(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*
+ * @brief Check if an epg tag can be recorded
+ * @param tag The epg tag
+ * @param bIsRecordable Set to true if the tag can be recorded
+ * @return PVR_ERROR_NO_ERROR if bIsRecordable has been set successfully.
+ */
+ PVR_ERROR IsRecordable(const std::shared_ptr<const CPVREpgInfoTag>& tag,
+ bool& bIsRecordable) const;
+
+ /*
+ * @brief Check if an epg tag can be played
+ * @param tag The epg tag
+ * @param bIsPlayable Set to true if the tag can be played
+ * @return PVR_ERROR_NO_ERROR if bIsPlayable has been set successfully.
+ */
+ PVR_ERROR IsPlayable(const std::shared_ptr<const CPVREpgInfoTag>& tag, bool& bIsPlayable) const;
+
+ /*!
+ * @brief Fill the given container with the properties required for playback
+ * of the given EPG tag. Values are obtained from the PVR backend.
+ *
+ * @param tag The EPG tag.
+ * @param props The container to be filled with the stream properties.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetEpgTagStreamProperties(const std::shared_ptr<CPVREpgInfoTag>& tag,
+ CPVRStreamProperties& props);
+
+ //@}
+ /** @name PVR EPG methods */
+ //@{
+
+ /*!
+ * @brief Request an EPG table for a channel from the client.
+ * @param iChannelUid The UID of the channel to get the EPG table for.
+ * @param epg The table to write the data to.
+ * @param start The start time to use.
+ * @param end The end time to use.
+ * @return PVR_ERROR_NO_ERROR if the table has been fetched successfully.
+ */
+ PVR_ERROR GetEPGForChannel(int iChannelUid, CPVREpg* epg, time_t start, time_t end);
+
+ /*!
+ * @brief Tell the client the past time frame to use when notifying epg events back
+ * to Kodi.
+ *
+ * The client might push epg events asynchronously to Kodi using the callback
+ * function EpgEventStateChange. To be able to only push events that are
+ * actually of interest for Kodi, client needs to know about the past epg time
+ * frame Kodi uses.
+ *
+ * @param[in] iPastDays number of days before "now".
+ @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all epg events,
+ regardless of event times.
+ * @return PVR_ERROR_NO_ERROR if new value was successfully set.
+ */
+ PVR_ERROR SetEPGMaxPastDays(int iPastDays);
+
+ /*!
+ * @brief Tell the client the future time frame to use when notifying epg events back
+ * to Kodi.
+ *
+ * The client might push epg events asynchronously to Kodi using the callback
+ * function EpgEventStateChange. To be able to only push events that are
+ * actually of interest for Kodi, client needs to know about the future epg time
+ * frame Kodi uses.
+ *
+ * @param[in] iFutureDays number of days after "now".
+ @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all epg events,
+ regardless of event times.
+ * @return PVR_ERROR_NO_ERROR if new value was successfully set.
+ */
+ PVR_ERROR SetEPGMaxFutureDays(int iFutureDays);
+
+ //@}
+ /** @name PVR channel group methods */
+ //@{
+
+ /*!
+ * @brief Get the total amount of channel groups from the backend.
+ * @param iGroups The total amount of channel groups on the server or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetChannelGroupsAmount(int& iGroups);
+
+ /*!
+ * @brief Request the list of all channel groups from the backend.
+ * @param groups The groups container to get the groups for.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetChannelGroups(CPVRChannelGroups* groups);
+
+ /*!
+ * @brief Request the list of all group members from the backend.
+ * @param group The group to get the members for.
+ * @param groupMembers The container for the group members.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetChannelGroupMembers(
+ CPVRChannelGroup* group, std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers);
+
+ //@}
+ /** @name PVR channel methods */
+ //@{
+
+ /*!
+ * @brief Get the total amount of channels from the backend.
+ * @param iChannels The total amount of channels on the server or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetChannelsAmount(int& iChannels);
+
+ /*!
+ * @brief Request the list of all channels from the backend.
+ * @param bRadio True to get the radio channels, false to get the TV channels.
+ * @param channels The container for the channels.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetChannels(bool bRadio, std::vector<std::shared_ptr<CPVRChannel>>& channels);
+
+ /*!
+ * @brief Get the total amount of providers from the backend.
+ * @param iChannels The total amount of channels on the server or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetProvidersAmount(int& iProviders);
+
+ /*!
+ * @brief Request the list of all providers from the backend.
+ * @param providers The providers list to add the providers to.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetProviders(CPVRProvidersContainer& providers);
+
+ //@}
+ /** @name PVR recording methods */
+ //@{
+
+ /*!
+ * @brief Get the total amount of recordings from the backend.
+ * @param deleted True to return deleted recordings.
+ * @param iRecordings The total amount of recordings on the server or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordingsAmount(bool deleted, int& iRecordings);
+
+ /*!
+ * @brief Request the list of all recordings from the backend.
+ * @param results The container to add the recordings to.
+ * @param deleted True to return deleted recordings.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetRecordings(CPVRRecordings* results, bool deleted);
+
+ /*!
+ * @brief Delete a recording on the backend.
+ * @param recording The recording to delete.
+ * @return PVR_ERROR_NO_ERROR if the recording has been deleted successfully.
+ */
+ PVR_ERROR DeleteRecording(const CPVRRecording& recording);
+
+ /*!
+ * @brief Undelete a recording on the backend.
+ * @param recording The recording to undelete.
+ * @return PVR_ERROR_NO_ERROR if the recording has been undeleted successfully.
+ */
+ PVR_ERROR UndeleteRecording(const CPVRRecording& recording);
+
+ /*!
+ * @brief Delete all recordings permanent which in the deleted folder on the backend.
+ * @return PVR_ERROR_NO_ERROR if the recordings has been deleted successfully.
+ */
+ PVR_ERROR DeleteAllRecordingsFromTrash();
+
+ /*!
+ * @brief Rename a recording on the backend.
+ * @param recording The recording to rename.
+ * @return PVR_ERROR_NO_ERROR if the recording has been renamed successfully.
+ */
+ PVR_ERROR RenameRecording(const CPVRRecording& recording);
+
+ /*!
+ * @brief Set the lifetime of a recording on the backend.
+ * @param recording The recording to set the lifetime for. recording.m_iLifetime contains the new lifetime value.
+ * @return PVR_ERROR_NO_ERROR if the recording's lifetime has been set successfully.
+ */
+ PVR_ERROR SetRecordingLifetime(const CPVRRecording& recording);
+
+ /*!
+ * @brief Set the play count of a recording on the backend.
+ * @param recording The recording to set the play count.
+ * @param count Play count.
+ * @return PVR_ERROR_NO_ERROR if the recording's play count has been set successfully.
+ */
+ PVR_ERROR SetRecordingPlayCount(const CPVRRecording& recording, int count);
+
+ /*!
+ * @brief Set the last watched position of a recording on the backend.
+ * @param recording The recording.
+ * @param lastplayedposition The last watched position in seconds
+ * @return PVR_ERROR_NO_ERROR if the position has been stored successfully.
+ */
+ PVR_ERROR SetRecordingLastPlayedPosition(const CPVRRecording& recording, int lastplayedposition);
+
+ /*!
+ * @brief Retrieve the last watched position of a recording on the backend.
+ * @param recording The recording.
+ * @param iPosition The last watched position in seconds or -1 on error
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordingLastPlayedPosition(const CPVRRecording& recording, int& iPosition);
+
+ /*!
+ * @brief Retrieve the edit decision list (EDL) from the backend.
+ * @param recording The recording.
+ * @param edls The edit decision list (empty on error).
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordingEdl(const CPVRRecording& recording, std::vector<PVR_EDL_ENTRY>& edls);
+
+ /*!
+ * @brief Retrieve the size of a recording on the backend.
+ * @param recording The recording.
+ * @param sizeInBytes The size in bytes
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordingSize(const CPVRRecording& recording, int64_t& sizeInBytes);
+
+ /*!
+ * @brief Retrieve the edit decision list (EDL) from the backend.
+ * @param epgTag The EPG tag.
+ * @param edls The edit decision list (empty on error).
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetEpgTagEdl(const std::shared_ptr<const CPVREpgInfoTag>& epgTag,
+ std::vector<PVR_EDL_ENTRY>& edls);
+
+ //@}
+ /** @name PVR timer methods */
+ //@{
+
+ /*!
+ * @brief Get the total amount of timers from the backend.
+ * @param iTimers The total amount of timers on the backend or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetTimersAmount(int& iTimers);
+
+ /*!
+ * @brief Request the list of all timers from the backend.
+ * @param results The container to store the result in.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetTimers(CPVRTimersContainer* results);
+
+ /*!
+ * @brief Add a timer on the backend.
+ * @param timer The timer to add.
+ * @return PVR_ERROR_NO_ERROR if the timer has been added successfully.
+ */
+ PVR_ERROR AddTimer(const CPVRTimerInfoTag& timer);
+
+ /*!
+ * @brief Delete a timer on the backend.
+ * @param timer The timer to delete.
+ * @param bForce Set to true to delete a timer that is currently recording a program.
+ * @return PVR_ERROR_NO_ERROR if the timer has been deleted successfully.
+ */
+ PVR_ERROR DeleteTimer(const CPVRTimerInfoTag& timer, bool bForce = false);
+
+ /*!
+ * @brief Update the timer information on the server.
+ * @param timer The timer to update.
+ * @return PVR_ERROR_NO_ERROR if the timer has been updated successfully.
+ */
+ PVR_ERROR UpdateTimer(const CPVRTimerInfoTag& timer);
+
+ /*!
+ * @brief Get all timer types supported by the backend.
+ * @param results The container to store the result in.
+ * @return PVR_ERROR_NO_ERROR if the list has been fetched successfully.
+ */
+ PVR_ERROR GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const;
+
+ //@}
+ /** @name PVR live stream methods */
+ //@{
+
+ /*!
+ * @brief Open a live stream on the server.
+ * @param channel The channel to stream.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR OpenLiveStream(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Close an open live stream.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CloseLiveStream();
+
+ /*!
+ * @brief Read from an open live stream.
+ * @param lpBuf The buffer to store the data in.
+ * @param uiBufSize The amount of bytes to read.
+ * @param iRead The amount of bytes that were actually read from the stream.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR ReadLiveStream(void* lpBuf, int64_t uiBufSize, int& iRead);
+
+ /*!
+ * @brief Seek in a live stream on a backend.
+ * @param iFilePosition The position to seek to.
+ * @param iWhence ?
+ * @param iPosition The new position or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR SeekLiveStream(int64_t iFilePosition, int iWhence, int64_t& iPosition);
+
+ /*!
+ * @brief Get the length of the currently playing live stream, if any.
+ * @param iLength The total length of the stream that's currently being read or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetLiveStreamLength(int64_t& iLength);
+
+ /*!
+ * @brief (Un)Pause a stream.
+ * @param bPaused True to pause the stream, false to unpause.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR PauseStream(bool bPaused);
+
+ /*!
+ * @brief Get the signal quality of the stream that's currently open.
+ * @param channelUid Channel unique identifier
+ * @param qualityinfo The signal quality.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR SignalQuality(int channelUid, PVR_SIGNAL_STATUS& qualityinfo);
+
+ /*!
+ * @brief Get the descramble information of the stream that's currently open.
+ * @param channelUid Channel unique identifier
+ * @param descrambleinfo The descramble information.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetDescrambleInfo(int channelUid, PVR_DESCRAMBLE_INFO& descrambleinfo) const;
+
+ /*!
+ * @brief Fill the given container with the properties required for playback of the given channel. Values are obtained from the PVR backend.
+ * @param channel The channel.
+ * @param props The container to be filled with the stream properties.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetChannelStreamProperties(const std::shared_ptr<CPVRChannel>& channel,
+ CPVRStreamProperties& props);
+
+ /*!
+ * @brief Check whether PVR backend supports pausing the currently playing stream
+ * @param bCanPause True if the stream can be paused, false otherwise.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CanPauseStream(bool& bCanPause) const;
+
+ /*!
+ * @brief Check whether PVR backend supports seeking for the currently playing stream
+ * @param bCanSeek True if the stream can be seeked, false otherwise.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CanSeekStream(bool& bCanSeek) const;
+
+ /*!
+ * @brief Notify the pvr addon/demuxer that Kodi wishes to seek the stream by time
+ * @param time The absolute time since stream start
+ * @param backwards True to seek to keyframe BEFORE time, else AFTER
+ * @param startpts can be updated to point to where display should start
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ * @remarks Optional, and only used if addon has its own demuxer.
+ */
+ PVR_ERROR SeekTime(double time, bool backwards, double* startpts);
+
+ /*!
+ * @brief Notify the pvr addon/demuxer that Kodi wishes to change playback speed
+ * @param speed The requested playback speed
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ * @remarks Optional, and only used if addon has its own demuxer.
+ */
+ PVR_ERROR SetSpeed(int speed);
+
+ /*!
+ * @brief Notify the pvr addon/demuxer that Kodi wishes to fill demux queue
+ * @param mode for setting on/off
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ * @remarks Optional, and only used if addon has its own demuxer.
+ */
+ PVR_ERROR FillBuffer(bool mode);
+
+ //@}
+ /** @name PVR recording stream methods */
+ //@{
+
+ /*!
+ * @brief Open a recording on the server.
+ * @param recording The recording to open.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR OpenRecordedStream(const std::shared_ptr<CPVRRecording>& recording);
+
+ /*!
+ * @brief Close an open recording stream.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CloseRecordedStream();
+
+ /*!
+ * @brief Read from an open recording stream.
+ * @param lpBuf The buffer to store the data in.
+ * @param uiBufSize The amount of bytes to read.
+ * @param iRead The amount of bytes that were actually read from the stream.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR ReadRecordedStream(void* lpBuf, int64_t uiBufSize, int& iRead);
+
+ /*!
+ * @brief Seek in a recording stream on a backend.
+ * @param iFilePosition The position to seek to.
+ * @param iWhence ?
+ * @param iPosition The new position or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR SeekRecordedStream(int64_t iFilePosition, int iWhence, int64_t& iPosition);
+
+ /*!
+ * @brief Get the length of the currently playing recording stream, if any.
+ * @param iLength The total length of the stream that's currently being read or -1 on error.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordedStreamLength(int64_t& iLength);
+
+ /*!
+ * @brief Fill the given container with the properties required for playback of the given recording. Values are obtained from the PVR backend.
+ * @param recording The recording.
+ * @param props The container to be filled with the stream properties.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetRecordingStreamProperties(const std::shared_ptr<CPVRRecording>& recording,
+ CPVRStreamProperties& props);
+
+ //@}
+ /** @name PVR demultiplexer methods */
+ //@{
+
+ /*!
+ * @brief Reset the demultiplexer in the add-on.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR DemuxReset();
+
+ /*!
+ * @brief Abort the demultiplexer thread in the add-on.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR DemuxAbort();
+
+ /*!
+ * @brief Flush all data that's currently in the demultiplexer buffer in the add-on.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR DemuxFlush();
+
+ /*!
+ * @brief Read a packet from the demultiplexer.
+ * @param packet The packet read.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR DemuxRead(DemuxPacket*& packet);
+
+ static const char* ToString(const PVR_ERROR error);
+
+ /*!
+ * @brief Check whether the currently playing stream, if any, is a real-time stream.
+ * @param bRealTime True if real-time, false otherwise.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR IsRealTimeStream(bool& bRealTime) const;
+
+ /*!
+ * @brief Get Stream times for the currently playing stream, if any (will be moved to inputstream).
+ * @param times The stream times.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetStreamTimes(PVR_STREAM_TIMES* times);
+
+ /*!
+ * @brief Get the client's menu hooks.
+ * @return The hooks. Guaranteed never to be nullptr.
+ */
+ std::shared_ptr<CPVRClientMenuHooks> GetMenuHooks();
+
+ /*!
+ * @brief Call one of the EPG tag menu hooks of the client.
+ * @param hook The hook to call.
+ * @param tag The EPG tag associated with the hook to be called.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CallEpgTagMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVREpgInfoTag>& tag);
+
+ /*!
+ * @brief Call one of the channel menu hooks of the client.
+ * @param hook The hook to call.
+ * @param tag The channel associated with the hook to be called.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CallChannelMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Call one of the recording menu hooks of the client.
+ * @param hook The hook to call.
+ * @param tag The recording associated with the hook to be called.
+ * @param bDeleted True, if the recording is deleted (trashed), false otherwise
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CallRecordingMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRRecording>& recording,
+ bool bDeleted);
+
+ /*!
+ * @brief Call one of the timer menu hooks of the client.
+ * @param hook The hook to call.
+ * @param tag The timer associated with the hook to be called.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CallTimerMenuHook(const CPVRClientMenuHook& hook,
+ const std::shared_ptr<CPVRTimerInfoTag>& timer);
+
+ /*!
+ * @brief Call one of the settings menu hooks of the client.
+ * @param hook The hook to call.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR CallSettingsMenuHook(const CPVRClientMenuHook& hook);
+
+ /*!
+ * @brief Propagate power management events to this add-on
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR OnSystemSleep();
+ PVR_ERROR OnSystemWake();
+ PVR_ERROR OnPowerSavingActivated();
+ PVR_ERROR OnPowerSavingDeactivated();
+
+ /*!
+ * @brief Get the priority of this client. Larger value means higher priority.
+ * @return The priority.
+ */
+ int GetPriority() const;
+
+ /*!
+ * @brief Set a new priority for this client.
+ * @param iPriority The new priority.
+ */
+ void SetPriority(int iPriority);
+
+ /*!
+ * @brief Obtain the chunk size to use when reading streams.
+ * @param iChunkSize the chunk size in bytes.
+ * @return PVR_ERROR_NO_ERROR on success, respective error code otherwise.
+ */
+ PVR_ERROR GetStreamReadChunkSize(int& iChunkSize);
+
+ /*!
+ * @brief Get the interface table used between addon and Kodi.
+ * @todo This function will be removed after old callback library system is removed.
+ */
+ AddonInstance_PVR* GetInstanceInterface() { return m_ifc.pvr; }
+
+private:
+ /*!
+ * @brief Resets all class members to their defaults, accept the client id.
+ */
+ void ResetProperties();
+
+ /*!
+ * @brief reads the client's properties.
+ * @return True on success, false otherwise.
+ */
+ bool GetAddonProperties();
+
+ /*!
+ * @brief reads the client's name string properties
+ * @return True on success, false otherwise.
+ */
+ bool GetAddonNameStringProperties();
+
+ /*!
+ * @brief Write the given addon properties to the given properties container.
+ * @param properties Pointer to an array of addon properties.
+ * @param iPropertyCount The number of properties contained in the addon properties array.
+ * @param props The container the addon properties shall be written to.
+ */
+ static void WriteStreamProperties(const PVR_NAMED_VALUE* properties,
+ unsigned int iPropertyCount,
+ CPVRStreamProperties& props);
+
+ /*!
+ * @brief Whether a channel can be played by this add-on
+ * @param channel The channel to check.
+ * @return True when it can be played, false otherwise.
+ */
+ bool CanPlayChannel(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Stop this instance, if it is currently running.
+ */
+ void StopRunningInstance();
+
+ /*!
+ * @brief Wraps an addon function call in order to do common pre and post function invocation actions.
+ * @param strFunctionName The function name, for logging purposes.
+ * @param function The function to wrap. It has to have return type PVR_ERROR and must take one parameter of type const AddonInstance*.
+ * @param bIsImplemented If false, this method will return PVR_ERROR_NOT_IMPLEMENTED.
+ * @param bCheckReadyToUse If true, this method will check whether this instance is ready for use and return PVR_ERROR_SERVER_ERROR if it is not.
+ * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise.
+ */
+ typedef AddonInstance_PVR AddonInstance;
+ PVR_ERROR DoAddonCall(const char* strFunctionName,
+ const std::function<PVR_ERROR(const AddonInstance*)>& function,
+ bool bIsImplemented = true,
+ bool bCheckReadyToUse = true) const;
+
+ /*!
+ * @brief Wraps an addon callback function call in order to do common pre and post function invocation actions.
+ * @param strFunctionName The function name, for logging purposes.
+ * @param kodiInstance The addon instance pointer.
+ * @param function The function to wrap. It must take one parameter of type CPVRClient*.
+ * @param bForceCall If true, make the call, ignoring client's state.
+ */
+ static void HandleAddonCallback(const char* strFunctionName,
+ void* kodiInstance,
+ const std::function<void(CPVRClient* client)>& function,
+ bool bForceCall = false);
+
+ /*!
+ * @brief Callback functions from addon to kodi
+ */
+ //@{
+
+ /*!
+ * @brief Transfer a channel group from the add-on to Kodi. The group will be created if it doesn't exist.
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the channel groups list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_channel_group(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL_GROUP* entry);
+
+ /*!
+ * @brief Transfer a channel group member entry from the add-on to Kodi. The channel will be added to the group if the group can be found.
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the channel group members list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_channel_group_member(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL_GROUP_MEMBER* entry);
+
+ /*!
+ * @brief Transfer an EPG tag from the add-on to Kodi
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the EPG data
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_epg_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const EPG_TAG* entry);
+
+ /*!
+ * @brief Transfer a channel entry from the add-on to Kodi
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the channel list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_channel_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_CHANNEL* entry);
+
+ /*!
+ * @brief Transfer a provider entry from the add-on to Kodi
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the channel list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_provider_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_PROVIDER* entry);
+
+ /*!
+ * @brief Transfer a timer entry from the add-on to Kodi
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the timers list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_timer_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_TIMER* entry);
+
+ /*!
+ * @brief Transfer a recording entry from the add-on to Kodi
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param handle The handle parameter that Kodi used when requesting the recordings list
+ * @param entry The entry to transfer to Kodi
+ */
+ static void cb_transfer_recording_entry(void* kodiInstance,
+ const PVR_HANDLE handle,
+ const PVR_RECORDING* entry);
+
+ /*!
+ * @brief Add or replace a menu hook for the context menu for this add-on
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param hook The hook to add.
+ */
+ static void cb_add_menu_hook(void* kodiInstance, const PVR_MENUHOOK* hook);
+
+ /*!
+ * @brief Display a notification in Kodi that a recording started or stopped on the server
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param strName The name of the recording to display
+ * @param strFileName The filename of the recording
+ * @param bOnOff True when recording started, false when it stopped
+ */
+ static void cb_recording_notification(void* kodiInstance,
+ const char* strName,
+ const char* strFileName,
+ bool bOnOff);
+
+ /*!
+ * @brief Request Kodi to update it's list of channels
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ */
+ static void cb_trigger_channel_update(void* kodiInstance);
+
+ /*!
+ * @brief Request Kodi to update it's list of providers
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ */
+ static void cb_trigger_provider_update(void* kodiInstance);
+
+ /*!
+ * @brief Request Kodi to update it's list of timers
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ */
+ static void cb_trigger_timer_update(void* kodiInstance);
+
+ /*!
+ * @brief Request Kodi to update it's list of recordings
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ */
+ static void cb_trigger_recording_update(void* kodiInstance);
+
+ /*!
+ * @brief Request Kodi to update it's list of channel groups
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ */
+ static void cb_trigger_channel_groups_update(void* kodiInstance);
+
+ /*!
+ * @brief Schedule an EPG update for the given channel channel
+ * @param kodiInstance A pointer to the add-on
+ * @param iChannelUid The unique id of the channel for this add-on
+ */
+ static void cb_trigger_epg_update(void* kodiInstance, unsigned int iChannelUid);
+
+ /*!
+ * @brief Free a packet that was allocated with AllocateDemuxPacket
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param pPacket The packet to free.
+ */
+ static void cb_free_demux_packet(void* kodiInstance, DEMUX_PACKET* pPacket);
+
+ /*!
+ * @brief Allocate a demux packet. Free with FreeDemuxPacket
+ * @param kodiInstance Pointer to Kodi's CPVRClient class.
+ * @param iDataSize The size of the data that will go into the packet
+ * @return The allocated packet.
+ */
+ static DEMUX_PACKET* cb_allocate_demux_packet(void* kodiInstance, int iDataSize = 0);
+
+ /*!
+ * @brief Notify a state change for a PVR backend connection
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param strConnectionString The connection string reported by the backend that can be displayed in the UI.
+ * @param newState The new state.
+ * @param strMessage A localized addon-defined string representing the new state, that can be displayed
+ * in the UI or NULL if the Kodi-defined default string for the new state shall be displayed.
+ */
+ static void cb_connection_state_change(void* kodiInstance,
+ const char* strConnectionString,
+ PVR_CONNECTION_STATE newState,
+ const char* strMessage);
+
+ /*!
+ * @brief Notify a state change for an EPG event
+ * @param kodiInstance Pointer to Kodi's CPVRClient class
+ * @param tag The EPG event.
+ * @param newState The new state.
+ * @param newState The new state. For EPG_EVENT_CREATED and EPG_EVENT_UPDATED, tag must be filled with all available
+ * event data, not just a delta. For EPG_EVENT_DELETED, it is sufficient to fill EPG_TAG.iUniqueBroadcastId
+ */
+ static void cb_epg_event_state_change(void* kodiInstance, EPG_TAG* tag, EPG_EVENT_STATE newState);
+
+ /*! @todo remove the use complete from them, or add as generl function?!
+ * Returns the ffmpeg codec id from given ffmpeg codec string name
+ */
+ static PVR_CODEC cb_get_codec_by_name(const void* kodiInstance, const char* strCodecName);
+ //@}
+
+ const int m_iClientId; /*!< unique ID of the client */
+ std::atomic<bool>
+ m_bReadyToUse; /*!< true if this add-on is initialised (ADDON_Create returned true), false otherwise */
+ std::atomic<bool> m_bBlockAddonCalls; /*!< true if no add-on API calls are allowed */
+ mutable std::atomic<int> m_iAddonCalls; /*!< number of in-progress addon calls */
+ mutable CEvent m_allAddonCallsFinished; /*!< fires after last in-progress addon call finished */
+ PVR_CONNECTION_STATE m_connectionState; /*!< the backend connection state */
+ PVR_CONNECTION_STATE m_prevConnectionState; /*!< the previous backend connection state */
+ bool
+ m_ignoreClient; /*!< signals to PVRManager to ignore this client until it has been connected */
+ std::vector<std::shared_ptr<CPVRTimerType>>
+ m_timertypes; /*!< timer types supported by this backend */
+ mutable int m_iPriority; /*!< priority of the client */
+ mutable bool m_bPriorityFetched;
+
+ /* cached data */
+ std::string m_strBackendName; /*!< the cached backend version */
+ std::string m_strBackendVersion; /*!< the cached backend version */
+ std::string m_strConnectionString; /*!< the cached connection string */
+ std::string m_strBackendHostname; /*!< the cached backend hostname */
+ CPVRClientCapabilities m_clientCapabilities; /*!< the cached add-on's capabilities */
+ std::shared_ptr<CPVRClientMenuHooks> m_menuhooks; /*!< the menu hooks for this add-on */
+
+ /* stored strings to make sure const char* members in AddonProperties_PVR stay valid */
+ std::string m_strUserPath; /*!< @brief translated path to the user profile */
+ std::string m_strClientPath; /*!< @brief translated path to this add-on */
+
+ mutable CCriticalSection m_critSection;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/addons/PVRClientCapabilities.cpp b/xbmc/pvr/addons/PVRClientCapabilities.cpp
new file mode 100644
index 0000000..8650465
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientCapabilities.cpp
@@ -0,0 +1,85 @@
+/*
+ * 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 "PVRClientCapabilities.h"
+
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace PVR;
+
+CPVRClientCapabilities::CPVRClientCapabilities(const CPVRClientCapabilities& other)
+{
+ if (other.m_addonCapabilities)
+ m_addonCapabilities.reset(new PVR_ADDON_CAPABILITIES(*other.m_addonCapabilities));
+ InitRecordingsLifetimeValues();
+}
+
+const CPVRClientCapabilities& CPVRClientCapabilities::operator=(const CPVRClientCapabilities& other)
+{
+ if (other.m_addonCapabilities)
+ m_addonCapabilities.reset(new PVR_ADDON_CAPABILITIES(*other.m_addonCapabilities));
+ InitRecordingsLifetimeValues();
+ return *this;
+}
+
+const CPVRClientCapabilities& CPVRClientCapabilities::operator=(
+ const PVR_ADDON_CAPABILITIES& addonCapabilities)
+{
+ m_addonCapabilities.reset(new PVR_ADDON_CAPABILITIES(addonCapabilities));
+ InitRecordingsLifetimeValues();
+ return *this;
+}
+
+void CPVRClientCapabilities::clear()
+{
+ m_recordingsLifetimeValues.clear();
+ m_addonCapabilities.reset();
+}
+
+void CPVRClientCapabilities::InitRecordingsLifetimeValues()
+{
+ m_recordingsLifetimeValues.clear();
+ if (m_addonCapabilities && m_addonCapabilities->iRecordingsLifetimesSize > 0)
+ {
+ for (unsigned int i = 0; i < m_addonCapabilities->iRecordingsLifetimesSize; ++i)
+ {
+ int iValue = m_addonCapabilities->recordingsLifetimeValues[i].iValue;
+ std::string strDescr(m_addonCapabilities->recordingsLifetimeValues[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = std::to_string(iValue);
+ }
+ m_recordingsLifetimeValues.emplace_back(strDescr, iValue);
+ }
+ }
+ else if (SupportsRecordingsLifetimeChange())
+ {
+ // No values given by addon, but lifetime supported. Use default values 1..365
+ for (int i = 1; i < 366; ++i)
+ {
+ m_recordingsLifetimeValues.emplace_back(StringUtils::Format(g_localizeStrings.Get(17999), i),
+ i); // "{} days"
+ }
+ }
+ else
+ {
+ // No lifetime supported.
+ }
+}
+
+void CPVRClientCapabilities::GetRecordingsLifetimeValues(
+ std::vector<std::pair<std::string, int>>& list) const
+{
+ std::copy(m_recordingsLifetimeValues.cbegin(), m_recordingsLifetimeValues.cend(),
+ std::back_inserter(list));
+}
diff --git a/xbmc/pvr/addons/PVRClientCapabilities.h b/xbmc/pvr/addons/PVRClientCapabilities.h
new file mode 100644
index 0000000..edf2090
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientCapabilities.h
@@ -0,0 +1,277 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace PVR
+{
+
+class CPVRClientCapabilities
+{
+public:
+ CPVRClientCapabilities() = default;
+ virtual ~CPVRClientCapabilities() = default;
+
+ CPVRClientCapabilities(const CPVRClientCapabilities& other);
+ const CPVRClientCapabilities& operator=(const CPVRClientCapabilities& other);
+
+ const CPVRClientCapabilities& operator=(const PVR_ADDON_CAPABILITIES& addonCapabilities);
+
+ void clear();
+
+ /////////////////////////////////////////////////////////////////////////////////
+ //
+ // Channels
+ //
+ /////////////////////////////////////////////////////////////////////////////////
+
+ /*!
+ * @brief Check whether this add-on supports TV channels.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsTV() const { return m_addonCapabilities && m_addonCapabilities->bSupportsTV; }
+
+ /*!
+ * @brief Check whether this add-on supports radio channels.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRadio() const { return m_addonCapabilities && m_addonCapabilities->bSupportsRadio; }
+
+ /*!
+ * @brief Check whether this add-on supports providers.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsProviders() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsProviders;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports channel groups.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsChannelGroups() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsChannelGroups;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports scanning for new channels on the backend.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsChannelScan() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsChannelScan;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports the following functions:
+ * DeleteChannel, RenameChannel, DialogChannelSettings and DialogAddChannel.
+ *
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsChannelSettings() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsChannelSettings;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports descramble information for playing channels.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsDescrambleInfo() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsDescrambleInfo;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////
+ //
+ // EPG
+ //
+ /////////////////////////////////////////////////////////////////////////////////
+
+ /*!
+ * @brief Check whether this add-on provides EPG information.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsEPG() const { return m_addonCapabilities && m_addonCapabilities->bSupportsEPG; }
+
+ /*!
+ * @brief Check whether this add-on supports asynchronous transfer of epg events.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsAsyncEPGTransfer() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsAsyncEPGTransfer;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////
+ //
+ // Timers
+ //
+ /////////////////////////////////////////////////////////////////////////////////
+
+ /*!
+ * @brief Check whether this add-on supports the creation and editing of timers.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsTimers() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsTimers;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////
+ //
+ // Recordings
+ //
+ /////////////////////////////////////////////////////////////////////////////////
+
+ /*!
+ * @brief Check whether this add-on supports recordings.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordings() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports undelete of deleted recordings.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsUndelete() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingsUndelete;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports play count for recordings.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsPlayCount() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingPlayCount;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports store/retrieve of last played position for recordings..
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsLastPlayedPosition() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsLastPlayedPosition;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports retrieving an edit decision list for recordings.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsEdl() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingEdl;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports retrieving an edit decision list for epg tags.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsEpgTagEdl() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsEPG &&
+ m_addonCapabilities->bSupportsEPGEdl;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports renaming recordings..
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsRename() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingsRename;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports changing lifetime of recording.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsLifetimeChange() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingsLifetimeChange;
+ }
+
+ /*!
+ * @brief Obtain a list with all possible values for recordings lifetime.
+ * @param list out, the list with the values or an empty list, if lifetime is not supported.
+ */
+ void GetRecordingsLifetimeValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Check whether this add-on supports retrieving the size recordings..
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsSize() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingSize;
+ }
+
+ /*!
+ * @brief Check whether this add-on supports deleting recordings.
+ * @return True if supported, false otherwise.
+ */
+ bool SupportsRecordingsDelete() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bSupportsRecordings &&
+ m_addonCapabilities->bSupportsRecordingsDelete;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////
+ //
+ // Streams
+ //
+ /////////////////////////////////////////////////////////////////////////////////
+
+ /*!
+ * @brief Check whether this add-on provides an input stream. false if Kodi handles the stream.
+ * @return True if supported, false otherwise.
+ */
+ bool HandlesInputStream() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bHandlesInputStream;
+ }
+
+ /*!
+ * @brief Check whether this add-on demultiplexes packets.
+ * @return True if supported, false otherwise.
+ */
+ bool HandlesDemuxing() const
+ {
+ return m_addonCapabilities && m_addonCapabilities->bHandlesDemuxing;
+ }
+
+private:
+ void InitRecordingsLifetimeValues();
+
+ std::unique_ptr<PVR_ADDON_CAPABILITIES> m_addonCapabilities;
+ std::vector<std::pair<std::string, int>> m_recordingsLifetimeValues;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/addons/PVRClientMenuHooks.cpp b/xbmc/pvr/addons/PVRClientMenuHooks.cpp
new file mode 100644
index 0000000..12f150a
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientMenuHooks.cpp
@@ -0,0 +1,185 @@
+/*
+ * 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 "PVRClientMenuHooks.h"
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_menu_hook.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRContextMenus.h"
+#include "utils/log.h"
+
+namespace PVR
+{
+
+CPVRClientMenuHook::CPVRClientMenuHook(const std::string& addonId, const PVR_MENUHOOK& hook)
+: m_addonId(addonId),
+ m_hook(new PVR_MENUHOOK(hook))
+{
+ if (hook.category != PVR_MENUHOOK_UNKNOWN &&
+ hook.category != PVR_MENUHOOK_ALL &&
+ hook.category != PVR_MENUHOOK_CHANNEL &&
+ hook.category != PVR_MENUHOOK_TIMER &&
+ hook.category != PVR_MENUHOOK_EPG &&
+ hook.category != PVR_MENUHOOK_RECORDING &&
+ hook.category != PVR_MENUHOOK_DELETED_RECORDING &&
+ hook.category != PVR_MENUHOOK_SETTING)
+ CLog::LogF(LOGERROR, "Unknown PVR_MENUHOOK_CAT value: {}", hook.category);
+}
+
+bool CPVRClientMenuHook::operator ==(const CPVRClientMenuHook& right) const
+{
+ if (this == &right)
+ return true;
+
+ return m_addonId == right.m_addonId &&
+ m_hook->iHookId == right.m_hook->iHookId &&
+ m_hook->iLocalizedStringId == right.m_hook->iLocalizedStringId &&
+ m_hook->category == right.m_hook->category;
+}
+
+bool CPVRClientMenuHook::IsAllHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_ALL;
+}
+
+bool CPVRClientMenuHook::IsChannelHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_CHANNEL;
+}
+
+bool CPVRClientMenuHook::IsTimerHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_TIMER;
+}
+
+bool CPVRClientMenuHook::IsEpgHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_EPG;
+}
+
+bool CPVRClientMenuHook::IsRecordingHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_RECORDING;
+}
+
+bool CPVRClientMenuHook::IsDeletedRecordingHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_DELETED_RECORDING;
+}
+
+bool CPVRClientMenuHook::IsSettingsHook() const
+{
+ return m_hook->category == PVR_MENUHOOK_SETTING;
+}
+
+std::string CPVRClientMenuHook::GetAddonId() const
+{
+ return m_addonId;
+}
+
+unsigned int CPVRClientMenuHook::GetId() const
+{
+ return m_hook->iHookId;
+}
+
+unsigned int CPVRClientMenuHook::GetLabelId() const
+{
+ return m_hook->iLocalizedStringId;
+}
+
+std::string CPVRClientMenuHook::GetLabel() const
+{
+ return g_localizeStrings.GetAddonString(m_addonId, m_hook->iLocalizedStringId);
+}
+
+void CPVRClientMenuHooks::AddHook(const PVR_MENUHOOK& addonHook)
+{
+ if (!m_hooks)
+ m_hooks.reset(new std::vector<CPVRClientMenuHook>());
+
+ const CPVRClientMenuHook hook(m_addonId, addonHook);
+ m_hooks->emplace_back(hook);
+ CPVRContextMenuManager::GetInstance().AddMenuHook(hook);
+}
+
+void CPVRClientMenuHooks::Clear()
+{
+ if (!m_hooks)
+ return;
+
+ for (const auto& hook : *m_hooks)
+ CPVRContextMenuManager::GetInstance().RemoveMenuHook(hook);
+
+ m_hooks.reset();
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetHooks(
+ const std::function<bool(const CPVRClientMenuHook& hook)>& function) const
+{
+ std::vector<CPVRClientMenuHook> hooks;
+
+ if (!m_hooks)
+ return hooks;
+
+ for (const CPVRClientMenuHook& hook : *m_hooks)
+ {
+ if (function(hook) || hook.IsAllHook())
+ hooks.emplace_back(hook);
+ }
+ return hooks;
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetChannelHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsChannelHook();
+ });
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetTimerHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsTimerHook();
+ });
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetEpgHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsEpgHook();
+ });
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetRecordingHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsRecordingHook();
+ });
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetDeletedRecordingHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsDeletedRecordingHook();
+ });
+}
+
+std::vector<CPVRClientMenuHook> CPVRClientMenuHooks::GetSettingsHooks() const
+{
+ return GetHooks([](const CPVRClientMenuHook& hook)
+ {
+ return hook.IsSettingsHook();
+ });
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/addons/PVRClientMenuHooks.h b/xbmc/pvr/addons/PVRClientMenuHooks.h
new file mode 100644
index 0000000..69d7f79
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientMenuHooks.h
@@ -0,0 +1,73 @@
+/*
+ * 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 <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+struct PVR_MENUHOOK;
+
+namespace PVR
+{
+ class CPVRClientMenuHook
+ {
+ public:
+ CPVRClientMenuHook() = delete;
+ virtual ~CPVRClientMenuHook() = default;
+
+ CPVRClientMenuHook(const std::string& addonId, const PVR_MENUHOOK& hook);
+
+ bool operator ==(const CPVRClientMenuHook& right) const;
+
+ bool IsAllHook() const;
+ bool IsChannelHook() const;
+ bool IsTimerHook() const;
+ bool IsEpgHook() const;
+ bool IsRecordingHook() const;
+ bool IsDeletedRecordingHook() const;
+ bool IsSettingsHook() const;
+
+ std::string GetAddonId() const;
+ unsigned int GetId() const;
+ unsigned int GetLabelId() const;
+ std::string GetLabel() const;
+
+ private:
+ std::string m_addonId;
+ std::shared_ptr<PVR_MENUHOOK> m_hook;
+ };
+
+ class CPVRClientMenuHooks
+ {
+ public:
+ CPVRClientMenuHooks() = default;
+ virtual ~CPVRClientMenuHooks() = default;
+
+ explicit CPVRClientMenuHooks(const std::string& addonId) : m_addonId(addonId) {}
+
+ void AddHook(const PVR_MENUHOOK& addonHook);
+ void Clear();
+
+ std::vector<CPVRClientMenuHook> GetChannelHooks() const;
+ std::vector<CPVRClientMenuHook> GetTimerHooks() const;
+ std::vector<CPVRClientMenuHook> GetEpgHooks() const;
+ std::vector<CPVRClientMenuHook> GetRecordingHooks() const;
+ std::vector<CPVRClientMenuHook> GetDeletedRecordingHooks() const;
+ std::vector<CPVRClientMenuHook> GetSettingsHooks() const;
+
+ private:
+ std::vector<CPVRClientMenuHook> GetHooks(
+ const std::function<bool(const CPVRClientMenuHook& hook)>& function) const;
+
+ std::string m_addonId;
+ std::unique_ptr<std::vector<CPVRClientMenuHook>> m_hooks;
+ };
+}
diff --git a/xbmc/pvr/addons/PVRClientUID.cpp b/xbmc/pvr/addons/PVRClientUID.cpp
new file mode 100644
index 0000000..8788385
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientUID.cpp
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012-2022 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 "PVRClientUID.h"
+
+#include <functional>
+
+using namespace PVR;
+
+int CPVRClientUID::GetUID() const
+{
+ if (!m_uidCreated)
+ {
+ std::hash<std::string> hasher;
+
+ // Note: For database backwards compatibility reasons the hash of the first instance
+ // must be calculated just from the addonId, not from addonId and instanceId.
+ m_uid = static_cast<int>(hasher(
+ (m_instanceID > ADDON::ADDON_FIRST_INSTANCE_ID ? std::to_string(m_instanceID) + "@" : "") +
+ m_addonID));
+ if (m_uid < 0)
+ m_uid = -m_uid;
+
+ m_uidCreated = true;
+ }
+
+ return m_uid;
+}
diff --git a/xbmc/pvr/addons/PVRClientUID.h b/xbmc/pvr/addons/PVRClientUID.h
new file mode 100644
index 0000000..5b5d1c0
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClientUID.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012-2022 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 "addons/IAddon.h"
+
+#include <string>
+
+namespace PVR
+{
+class CPVRClientUID final
+{
+public:
+ CPVRClientUID(const std::string& addonID, ADDON::AddonInstanceId instanceID)
+ : m_addonID(addonID), m_instanceID(instanceID)
+ {
+ }
+
+ virtual ~CPVRClientUID() = default;
+
+ /*!
+ * @brief Return the numeric UID.
+ * @return The numeric UID.
+ */
+ int GetUID() const;
+
+private:
+ CPVRClientUID() = delete;
+
+ std::string m_addonID;
+ ADDON::AddonInstanceId m_instanceID{ADDON::ADDON_SINGLETON_INSTANCE_ID};
+
+ mutable bool m_uidCreated{false};
+ mutable int m_uid{0};
+};
+} // namespace PVR
diff --git a/xbmc/pvr/addons/PVRClients.cpp b/xbmc/pvr/addons/PVRClients.cpp
new file mode 100644
index 0000000..8cda035
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClients.cpp
@@ -0,0 +1,977 @@
+/*
+ * 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 "PVRClients.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVREventLogJob.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClientUID.h"
+#include "pvr/channels/PVRChannelGroupInternal.h"
+#include "pvr/guilib/PVRGUIProgressHandler.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace ADDON;
+using namespace PVR;
+
+CPVRClients::CPVRClients()
+{
+ CServiceBroker::GetAddonMgr().RegisterAddonMgrCallback(AddonType::PVRDLL, this);
+ CServiceBroker::GetAddonMgr().Events().Subscribe(this, &CPVRClients::OnAddonEvent);
+}
+
+CPVRClients::~CPVRClients()
+{
+ CServiceBroker::GetAddonMgr().Events().Unsubscribe(this);
+ CServiceBroker::GetAddonMgr().UnregisterAddonMgrCallback(AddonType::PVRDLL);
+
+ for (const auto& client : m_clientMap)
+ {
+ client.second->Destroy();
+ }
+}
+
+void CPVRClients::Start()
+{
+ UpdateClients();
+}
+
+void CPVRClients::Stop()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& client : m_clientMap)
+ {
+ client.second->Stop();
+ }
+}
+
+void CPVRClients::Continue()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& client : m_clientMap)
+ {
+ client.second->Continue();
+ }
+}
+
+void CPVRClients::UpdateClients(
+ const std::string& changedAddonId /* = "" */,
+ ADDON::AddonInstanceId changedInstanceId /* = ADDON::ADDON_SINGLETON_INSTANCE_ID */)
+{
+ std::vector<std::pair<AddonInfoPtr, bool>> addonsWithStatus;
+ if (!GetAddonsWithStatus(changedAddonId, addonsWithStatus))
+ return;
+
+ std::vector<std::shared_ptr<CPVRClient>> clientsToCreate; // client
+ std::vector<std::pair<int, std::string>> clientsToReCreate; // client id, addon name
+ std::vector<int> clientsToDestroy; // client id
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& addonWithStatus : addonsWithStatus)
+ {
+ const AddonInfoPtr addon = addonWithStatus.first;
+ const std::vector<std::pair<ADDON::AddonInstanceId, bool>> instanceIdsWithStatus =
+ GetInstanceIdsWithStatus(addon, addonWithStatus.second);
+
+ for (const auto& instanceIdWithStatus : instanceIdsWithStatus)
+ {
+ const ADDON::AddonInstanceId instanceId = instanceIdWithStatus.first;
+ bool instanceEnabled = instanceIdWithStatus.second;
+ const CPVRClientUID clientUID(addon->ID(), instanceId);
+ const int clientId = clientUID.GetUID();
+
+ if (instanceEnabled && (!IsKnownClient(clientId) || !IsCreatedClient(clientId)))
+ {
+ std::shared_ptr<CPVRClient> client;
+ const bool isKnownClient = IsKnownClient(clientId);
+ if (isKnownClient)
+ {
+ client = GetClient(clientId);
+ }
+ else
+ {
+ client = std::make_shared<CPVRClient>(addon, instanceId, clientId);
+ if (!client)
+ {
+ CLog::LogF(LOGERROR, "Severe error, incorrect add-on type");
+ continue;
+ }
+ }
+
+ // determine actual enabled state of instance
+ if (instanceId != ADDON_SINGLETON_INSTANCE_ID)
+ instanceEnabled = client->IsEnabled();
+
+ if (instanceEnabled)
+ clientsToCreate.emplace_back(client);
+ else if (isKnownClient)
+ clientsToDestroy.emplace_back(clientId);
+ }
+ else if (IsCreatedClient(clientId))
+ {
+ // determine actual enabled state of instance
+ if (instanceEnabled && instanceId != ADDON_SINGLETON_INSTANCE_ID)
+ {
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ instanceEnabled = client ? client->IsEnabled() : false;
+ }
+
+ if (instanceEnabled)
+ clientsToReCreate.emplace_back(clientId, addon->Name());
+ else
+ clientsToDestroy.emplace_back(clientId);
+ }
+ }
+ }
+ }
+
+ if (!clientsToCreate.empty() || !clientsToReCreate.empty() || !clientsToDestroy.empty())
+ {
+ CServiceBroker::GetPVRManager().Stop();
+
+ auto progressHandler = std::make_unique<CPVRGUIProgressHandler>(
+ g_localizeStrings.Get(19239)); // Creating PVR clients
+
+ unsigned int i = 0;
+ for (const auto& client : clientsToCreate)
+ {
+ progressHandler->UpdateProgress(client->Name(), i++,
+ clientsToCreate.size() + clientsToReCreate.size());
+
+ const ADDON_STATUS status = client->Create();
+
+ if (status != ADDON_STATUS_OK)
+ {
+ CLog::LogF(LOGERROR, "Failed to create add-on {}, status = {}", client->ID(), status);
+ if (status == ADDON_STATUS_PERMANENT_FAILURE)
+ {
+ CServiceBroker::GetAddonMgr().DisableAddon(client->ID(),
+ AddonDisabledReason::PERMANENT_FAILURE);
+ CServiceBroker::GetJobManager()->AddJob(
+ new CPVREventLogJob(true, EventLevel::Error, client->Name(),
+ g_localizeStrings.Get(24070), client->Icon()),
+ nullptr);
+ }
+ }
+ }
+
+ for (const auto& clientInfo : clientsToReCreate)
+ {
+ progressHandler->UpdateProgress(clientInfo.second, i++,
+ clientsToCreate.size() + clientsToReCreate.size());
+
+ // stop and recreate client
+ StopClient(clientInfo.first, true /* restart */);
+ }
+
+ progressHandler.reset();
+
+ for (const auto& client : clientsToDestroy)
+ {
+ // destroy client
+ StopClient(client, false /* no restart */);
+ }
+
+ if (!clientsToCreate.empty())
+ {
+ // update created clients map
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& client : clientsToCreate)
+ {
+ if (m_clientMap.find(client->GetID()) == m_clientMap.end())
+ {
+ m_clientMap.insert({client->GetID(), client});
+ }
+ }
+ }
+
+ CServiceBroker::GetPVRManager().Start();
+ }
+}
+
+bool CPVRClients::RequestRestart(const std::string& addonId,
+ ADDON::AddonInstanceId instanceId,
+ bool bDataChanged)
+{
+ CServiceBroker::GetJobManager()->Submit([this, addonId, instanceId] {
+ UpdateClients(addonId, instanceId);
+ return true;
+ });
+ return true;
+}
+
+bool CPVRClients::StopClient(int clientId, bool restart)
+{
+ // stop playback if needed
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying())
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client)
+ {
+ if (restart)
+ {
+ client->ReCreate();
+ }
+ else
+ {
+ const auto it = m_clientMap.find(clientId);
+ if (it != m_clientMap.end())
+ m_clientMap.erase(it);
+
+ client->Destroy();
+ }
+ return true;
+ }
+
+ return false;
+}
+
+void CPVRClients::OnAddonEvent(const AddonEvent& event)
+{
+ if (typeid(event) == typeid(AddonEvents::Enabled) || // also called on install,
+ typeid(event) == typeid(AddonEvents::Disabled) || // not called on uninstall
+ typeid(event) == typeid(AddonEvents::UnInstalled) ||
+ typeid(event) == typeid(AddonEvents::ReInstalled) ||
+ typeid(event) == typeid(AddonEvents::InstanceAdded) ||
+ typeid(event) == typeid(AddonEvents::InstanceRemoved))
+ {
+ // update addons
+ const std::string addonId = event.addonId;
+ const ADDON::AddonInstanceId instanceId = event.instanceId;
+ if (CServiceBroker::GetAddonMgr().HasType(addonId, AddonType::PVRDLL))
+ {
+ CServiceBroker::GetJobManager()->Submit([this, addonId, instanceId] {
+ UpdateClients(addonId, instanceId);
+ return true;
+ });
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// client access
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+std::shared_ptr<CPVRClient> CPVRClients::GetClient(int clientId) const
+{
+ if (clientId <= PVR_INVALID_CLIENT_ID)
+ return {};
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = m_clientMap.find(clientId);
+ if (it != m_clientMap.end())
+ return it->second;
+
+ return {};
+}
+
+int CPVRClients::CreatedClientAmount() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::count_if(m_clientMap.cbegin(), m_clientMap.cend(),
+ [](const auto& client) { return client.second->ReadyToUse(); });
+}
+
+bool CPVRClients::HasCreatedClients() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(),
+ [](const auto& client) { return client.second->ReadyToUse(); });
+}
+
+bool CPVRClients::IsKnownClient(int clientId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // valid client IDs start at 1
+ const auto it = m_clientMap.find(clientId);
+ return (it != m_clientMap.end() && (*it).second->GetID() > 0);
+}
+
+bool CPVRClients::IsCreatedClient(int iClientId) const
+{
+ return GetCreatedClient(iClientId) != nullptr;
+}
+
+std::shared_ptr<CPVRClient> CPVRClients::GetCreatedClient(int clientId) const
+{
+ std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ if (client && client->ReadyToUse())
+ return client;
+
+ return {};
+}
+
+CPVRClientMap CPVRClients::GetCreatedClients() const
+{
+ CPVRClientMap clients;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& client : m_clientMap)
+ {
+ if (client.second->ReadyToUse())
+ {
+ clients.insert(std::make_pair(client.second->GetID(), client.second));
+ }
+ }
+
+ return clients;
+}
+
+std::vector<CVariant> CPVRClients::GetClientProviderInfos() const
+{
+ std::vector<AddonInfoPtr> addonInfos;
+ // Get enabled and disabled PVR client addon infos
+ CServiceBroker::GetAddonMgr().GetAddonInfos(addonInfos, false, AddonType::PVRDLL);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ std::vector<CVariant> clientProviderInfos;
+ for (const auto& addonInfo : addonInfos)
+ {
+ std::vector<ADDON::AddonInstanceId> instanceIds = addonInfo->GetKnownInstanceIds();
+ for (const auto& instanceId : instanceIds)
+ {
+ CVariant clientProviderInfo(CVariant::VariantTypeObject);
+ clientProviderInfo["clientid"] = CPVRClientUID(addonInfo->ID(), instanceId).GetUID();
+ clientProviderInfo["addonid"] = addonInfo->ID();
+ clientProviderInfo["instanceid"] = instanceId;
+ clientProviderInfo["enabled"] =
+ !CServiceBroker::GetAddonMgr().IsAddonDisabled(addonInfo->ID());
+ clientProviderInfo["name"] = addonInfo->Name();
+ clientProviderInfo["icon"] = addonInfo->Icon();
+ auto& artMap = addonInfo->Art();
+ auto thumbEntry = artMap.find("thumb");
+ if (thumbEntry != artMap.end())
+ clientProviderInfo["thumb"] = thumbEntry->second;
+
+ clientProviderInfos.emplace_back(clientProviderInfo);
+ }
+ }
+
+ return clientProviderInfos;
+}
+
+int CPVRClients::GetFirstCreatedClientID()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = std::find_if(m_clientMap.cbegin(), m_clientMap.cend(),
+ [](const auto& client) { return client.second->ReadyToUse(); });
+ return it != m_clientMap.cend() ? (*it).second->GetID() : -1;
+}
+
+PVR_ERROR CPVRClients::GetCallableClients(CPVRClientMap& clientsReady,
+ std::vector<int>& clientsNotReady) const
+{
+ clientsNotReady.clear();
+
+ std::vector<AddonInfoPtr> addons;
+ CServiceBroker::GetAddonMgr().GetAddonInfos(addons, true, AddonType::PVRDLL);
+
+ for (const auto& addon : addons)
+ {
+ std::vector<ADDON::AddonInstanceId> instanceIds = addon->GetKnownInstanceIds();
+ for (const auto& instanceId : instanceIds)
+ {
+ const int clientId = CPVRClientUID(addon->ID(), instanceId).GetUID();
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+
+ if (client && client->ReadyToUse() && !client->IgnoreClient())
+ {
+ clientsReady.insert(std::make_pair(clientId, client));
+ }
+ else
+ {
+ clientsNotReady.emplace_back(clientId);
+ }
+ }
+ }
+
+ return clientsNotReady.empty() ? PVR_ERROR_NO_ERROR : PVR_ERROR_SERVER_ERROR;
+}
+
+int CPVRClients::EnabledClientAmount() const
+{
+ CPVRClientMap clientMap;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ clientMap = m_clientMap;
+ }
+
+ ADDON::CAddonMgr& addonMgr = CServiceBroker::GetAddonMgr();
+ return std::count_if(clientMap.cbegin(), clientMap.cend(), [&addonMgr](const auto& client) {
+ return !addonMgr.IsAddonDisabled(client.second->ID());
+ });
+}
+
+bool CPVRClients::IsEnabledClient(int clientId) const
+{
+ const std::shared_ptr<CPVRClient> client = GetClient(clientId);
+ return client && !CServiceBroker::GetAddonMgr().IsAddonDisabled(client->ID());
+}
+
+std::vector<CVariant> CPVRClients::GetEnabledClientInfos() const
+{
+ std::vector<CVariant> clientInfos;
+
+ CPVRClientMap clientMap;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ clientMap = m_clientMap;
+ }
+
+ for (const auto& client : clientMap)
+ {
+ const auto& addonInfo =
+ CServiceBroker::GetAddonMgr().GetAddonInfo(client.second->ID(), AddonType::PVRDLL);
+
+ if (addonInfo)
+ {
+ // This will be the same variant structure used in the json api
+ CVariant clientInfo(CVariant::VariantTypeObject);
+ clientInfo["clientid"] = client.first;
+ clientInfo["addonid"] = client.second->ID();
+ clientInfo["instanceid"] = client.second->InstanceId();
+ clientInfo["label"] = addonInfo->Name(); // Note that this is called label instead of name
+
+ const auto& capabilities = client.second->GetClientCapabilities();
+ clientInfo["supportstv"] = capabilities.SupportsTV();
+ clientInfo["supportsradio"] = capabilities.SupportsRadio();
+ clientInfo["supportsepg"] = capabilities.SupportsEPG();
+ clientInfo["supportsrecordings"] = capabilities.SupportsRecordings();
+ clientInfo["supportstimers"] = capabilities.SupportsTimers();
+ clientInfo["supportschannelgroups"] = capabilities.SupportsChannelGroups();
+ clientInfo["supportschannelscan"] = capabilities.SupportsChannelScan();
+ clientInfo["supportchannelproviders"] = capabilities.SupportsProviders();
+
+ clientInfos.push_back(clientInfo);
+ }
+ }
+
+ return clientInfos;
+}
+
+bool CPVRClients::HasIgnoredClients() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(),
+ [](const auto& client) { return client.second->IgnoreClient(); });
+}
+
+std::vector<ADDON::AddonInstanceId> CPVRClients::GetKnownInstanceIds(
+ const std::string& addonID) const
+{
+ std::vector<ADDON::AddonInstanceId> instanceIds;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& entry : m_clientMap)
+ {
+ if (entry.second->ID() == addonID)
+ instanceIds.emplace_back(entry.second->InstanceId());
+ }
+
+ return instanceIds;
+}
+
+bool CPVRClients::GetAddonsWithStatus(
+ const std::string& changedAddonId,
+ std::vector<std::pair<AddonInfoPtr, bool>>& addonsWithStatus) const
+{
+ std::vector<AddonInfoPtr> addons;
+ CServiceBroker::GetAddonMgr().GetAddonInfos(addons, false, AddonType::PVRDLL);
+
+ if (addons.empty())
+ return false;
+
+ bool foundChangedAddon = changedAddonId.empty();
+ for (const auto& addon : addons)
+ {
+ bool enabled = !CServiceBroker::GetAddonMgr().IsAddonDisabled(addon->ID());
+ addonsWithStatus.emplace_back(std::make_pair(addon, enabled));
+
+ if (!foundChangedAddon && addon->ID() == changedAddonId)
+ foundChangedAddon = true;
+ }
+
+ return foundChangedAddon;
+}
+
+std::vector<std::pair<ADDON::AddonInstanceId, bool>> CPVRClients::GetInstanceIdsWithStatus(
+ const AddonInfoPtr& addon, bool addonIsEnabled) const
+{
+ std::vector<std::pair<ADDON::AddonInstanceId, bool>> instanceIdsWithStatus;
+
+ std::vector<ADDON::AddonInstanceId> instanceIds = addon->GetKnownInstanceIds();
+ std::transform(instanceIds.cbegin(), instanceIds.cend(),
+ std::back_inserter(instanceIdsWithStatus), [addonIsEnabled](const auto& id) {
+ return std::pair<ADDON::AddonInstanceId, bool>(id, addonIsEnabled);
+ });
+
+ // find removed instances
+ const std::vector<ADDON::AddonInstanceId> knownInstanceIds = GetKnownInstanceIds(addon->ID());
+ for (const auto& knownInstanceId : knownInstanceIds)
+ {
+ if (std::find(instanceIds.begin(), instanceIds.end(), knownInstanceId) == instanceIds.end())
+ {
+ // instance was removed
+ instanceIdsWithStatus.emplace_back(
+ std::pair<ADDON::AddonInstanceId, bool>(knownInstanceId, false));
+ }
+ }
+
+ return instanceIdsWithStatus;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// client API calls
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+std::vector<SBackend> CPVRClients::GetBackendProperties() const
+{
+ std::vector<SBackend> backendProperties;
+
+ ForCreatedClients(__FUNCTION__, [&backendProperties](const std::shared_ptr<CPVRClient>& client) {
+ SBackend properties;
+
+ if (client->GetDriveSpace(properties.diskTotal, properties.diskUsed) == PVR_ERROR_NO_ERROR)
+ {
+ properties.diskTotal *= 1024;
+ properties.diskUsed *= 1024;
+ }
+
+ int iAmount = 0;
+ if (client->GetProvidersAmount(iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numProviders = iAmount;
+ if (client->GetChannelGroupsAmount(iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numChannelGroups = iAmount;
+ if (client->GetChannelsAmount(iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numChannels = iAmount;
+ if (client->GetTimersAmount(iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numTimers = iAmount;
+ if (client->GetRecordingsAmount(false, iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numRecordings = iAmount;
+ if (client->GetRecordingsAmount(true, iAmount) == PVR_ERROR_NO_ERROR)
+ properties.numDeletedRecordings = iAmount;
+ properties.name = client->GetBackendName();
+ properties.version = client->GetBackendVersion();
+ properties.host = client->GetConnectionString();
+
+ backendProperties.emplace_back(properties);
+ return PVR_ERROR_NO_ERROR;
+ });
+
+ return backendProperties;
+}
+
+bool CPVRClients::GetTimers(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRTimersContainer* timers,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [timers](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetTimers(timers);
+ },
+ failedClients) == PVR_ERROR_NO_ERROR;
+}
+
+PVR_ERROR CPVRClients::GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const
+{
+ return ForCreatedClients(__FUNCTION__, [&results](const std::shared_ptr<CPVRClient>& client) {
+ std::vector<std::shared_ptr<CPVRTimerType>> types;
+ PVR_ERROR ret = client->GetTimerTypes(types);
+ if (ret == PVR_ERROR_NO_ERROR)
+ results.insert(results.end(), types.begin(), types.end());
+ return ret;
+ });
+}
+
+PVR_ERROR CPVRClients::GetRecordings(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRRecordings* recordings,
+ bool deleted,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [recordings, deleted](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetRecordings(recordings, deleted);
+ },
+ failedClients);
+}
+
+PVR_ERROR CPVRClients::DeleteAllRecordingsFromTrash()
+{
+ return ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) {
+ return client->DeleteAllRecordingsFromTrash();
+ });
+}
+
+PVR_ERROR CPVRClients::SetEPGMaxPastDays(int iPastDays)
+{
+ return ForCreatedClients(__FUNCTION__, [iPastDays](const std::shared_ptr<CPVRClient>& client) {
+ return client->SetEPGMaxPastDays(iPastDays);
+ });
+}
+
+PVR_ERROR CPVRClients::SetEPGMaxFutureDays(int iFutureDays)
+{
+ return ForCreatedClients(__FUNCTION__, [iFutureDays](const std::shared_ptr<CPVRClient>& client) {
+ return client->SetEPGMaxFutureDays(iFutureDays);
+ });
+}
+
+PVR_ERROR CPVRClients::GetChannels(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ bool bRadio,
+ std::vector<std::shared_ptr<CPVRChannel>>& channels,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [bRadio, &channels](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetChannels(bRadio, channels);
+ },
+ failedClients);
+}
+
+PVR_ERROR CPVRClients::GetProviders(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRProvidersContainer* providers,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [providers](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetProviders(*providers);
+ },
+ failedClients);
+}
+
+PVR_ERROR CPVRClients::GetChannelGroups(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRChannelGroups* groups,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [groups](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetChannelGroups(groups);
+ },
+ failedClients);
+}
+
+PVR_ERROR CPVRClients::GetChannelGroupMembers(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRChannelGroup* group,
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers,
+ std::vector<int>& failedClients)
+{
+ return ForClients(__FUNCTION__, clients,
+ [group, &groupMembers](const std::shared_ptr<CPVRClient>& client) {
+ return client->GetChannelGroupMembers(group, groupMembers);
+ },
+ failedClients);
+}
+
+std::vector<std::shared_ptr<CPVRClient>> CPVRClients::GetClientsSupportingChannelScan() const
+{
+ std::vector<std::shared_ptr<CPVRClient>> possibleScanClients;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& entry : m_clientMap)
+ {
+ const auto& client = entry.second;
+ if (client->ReadyToUse() && !client->IgnoreClient() &&
+ client->GetClientCapabilities().SupportsChannelScan())
+ possibleScanClients.emplace_back(client);
+ }
+
+ return possibleScanClients;
+}
+
+std::vector<std::shared_ptr<CPVRClient>> CPVRClients::GetClientsSupportingChannelSettings(bool bRadio) const
+{
+ std::vector<std::shared_ptr<CPVRClient>> possibleSettingsClients;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& entry : m_clientMap)
+ {
+ const auto& client = entry.second;
+ if (client->ReadyToUse() && !client->IgnoreClient())
+ {
+ const CPVRClientCapabilities& caps = client->GetClientCapabilities();
+ if (caps.SupportsChannelSettings() &&
+ ((bRadio && caps.SupportsRadio()) || (!bRadio && caps.SupportsTV())))
+ possibleSettingsClients.emplace_back(client);
+ }
+ }
+
+ return possibleSettingsClients;
+}
+
+bool CPVRClients::AnyClientSupportingRecordingsSize() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) {
+ const auto& client = entry.second;
+ return client->ReadyToUse() && !client->IgnoreClient() &&
+ client->GetClientCapabilities().SupportsRecordingsSize();
+ });
+}
+
+bool CPVRClients::AnyClientSupportingEPG() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) {
+ const auto& client = entry.second;
+ return client->ReadyToUse() && !client->IgnoreClient() &&
+ client->GetClientCapabilities().SupportsEPG();
+ });
+}
+
+bool CPVRClients::AnyClientSupportingRecordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) {
+ const auto& client = entry.second;
+ return client->ReadyToUse() && !client->IgnoreClient() &&
+ client->GetClientCapabilities().SupportsRecordings();
+ });
+}
+
+bool CPVRClients::AnyClientSupportingRecordingsDelete() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_clientMap.cbegin(), m_clientMap.cend(), [](const auto& entry) {
+ const auto& client = entry.second;
+ return client->ReadyToUse() && !client->IgnoreClient() &&
+ client->GetClientCapabilities().SupportsRecordingsDelete();
+ });
+}
+
+void CPVRClients::OnSystemSleep()
+{
+ ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) {
+ client->OnSystemSleep();
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+void CPVRClients::OnSystemWake()
+{
+ ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) {
+ client->OnSystemWake();
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+void CPVRClients::OnPowerSavingActivated()
+{
+ ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) {
+ client->OnPowerSavingActivated();
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+void CPVRClients::OnPowerSavingDeactivated()
+{
+ ForCreatedClients(__FUNCTION__, [](const std::shared_ptr<CPVRClient>& client) {
+ client->OnPowerSavingDeactivated();
+ return PVR_ERROR_NO_ERROR;
+ });
+}
+
+void CPVRClients::ConnectionStateChange(CPVRClient* client,
+ const std::string& strConnectionString,
+ PVR_CONNECTION_STATE newState,
+ const std::string& strMessage)
+{
+ if (!client)
+ return;
+
+ int iMsg = -1;
+ EventLevel eLevel = EventLevel::Error;
+ bool bNotify = true;
+
+ switch (newState)
+ {
+ case PVR_CONNECTION_STATE_SERVER_UNREACHABLE:
+ iMsg = 35505; // Server is unreachable
+ if (client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_UNKNOWN ||
+ client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_CONNECTING)
+ {
+ // Make our users happy. There were so many complaints about this notification because their TV backend
+ // was not up quick enough after Kodi start. So, ignore the very first 'server not reachable' notification.
+ bNotify = false;
+ }
+ break;
+ case PVR_CONNECTION_STATE_SERVER_MISMATCH:
+ iMsg = 35506; // Server does not respond properly
+ break;
+ case PVR_CONNECTION_STATE_VERSION_MISMATCH:
+ iMsg = 35507; // Server version is not compatible
+ break;
+ case PVR_CONNECTION_STATE_ACCESS_DENIED:
+ iMsg = 35508; // Access denied
+ break;
+ case PVR_CONNECTION_STATE_CONNECTED:
+ eLevel = EventLevel::Basic;
+ iMsg = 36034; // Connection established
+ if (client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_UNKNOWN ||
+ client->GetPreviousConnectionState() == PVR_CONNECTION_STATE_CONNECTING)
+ bNotify = false;
+ break;
+ case PVR_CONNECTION_STATE_DISCONNECTED:
+ iMsg = 36030; // Connection lost
+ break;
+ case PVR_CONNECTION_STATE_CONNECTING:
+ eLevel = EventLevel::Information;
+ iMsg = 35509; // Connecting
+ bNotify = false;
+ break;
+ default:
+ CLog::LogF(LOGERROR, "Unknown connection state");
+ return;
+ }
+
+ // Use addon-supplied message, if present
+ std::string strMsg;
+ if (!strMessage.empty())
+ strMsg = strMessage;
+ else
+ strMsg = g_localizeStrings.Get(iMsg);
+
+ if (!strConnectionString.empty())
+ strMsg = StringUtils::Format("{} ({})", strMsg, strConnectionString);
+
+ // Notify user.
+ CServiceBroker::GetJobManager()->AddJob(
+ new CPVREventLogJob(bNotify, eLevel, client->GetFriendlyName(), strMsg, client->Icon()),
+ nullptr);
+}
+
+namespace
+{
+
+void LogClientWarning(const char* strFunctionName, const std::shared_ptr<CPVRClient>& client)
+{
+ if (client->IgnoreClient())
+ CLog::Log(LOGWARNING, "{}: Not calling add-on '{}'. Add-on not (yet) connected.",
+ strFunctionName, client->ID());
+ else if (!client->ReadyToUse())
+ CLog::Log(LOGWARNING, "{}: Not calling add-on '{}'. Add-on not ready to use.", strFunctionName,
+ client->ID());
+ else
+ CLog::Log(LOGERROR, "{}: Not calling add-on '{}' for unexpected reason.", strFunctionName,
+ client->ID());
+}
+
+} // unnamed namespace
+
+PVR_ERROR CPVRClients::ForCreatedClients(const char* strFunctionName,
+ const PVRClientFunction& function) const
+{
+ std::vector<int> failedClients;
+ return ForCreatedClients(strFunctionName, function, failedClients);
+}
+
+PVR_ERROR CPVRClients::ForCreatedClients(const char* strFunctionName,
+ const PVRClientFunction& function,
+ std::vector<int>& failedClients) const
+{
+ PVR_ERROR lastError = PVR_ERROR_NO_ERROR;
+
+ CPVRClientMap clients;
+ GetCallableClients(clients, failedClients);
+
+ if (!failedClients.empty())
+ {
+ std::shared_ptr<CPVRClient> client;
+ for (int id : failedClients)
+ {
+ client = GetClient(id);
+ if (client)
+ LogClientWarning(strFunctionName, client);
+ }
+ }
+
+ for (const auto& clientEntry : clients)
+ {
+ PVR_ERROR currentError = function(clientEntry.second);
+
+ if (currentError != PVR_ERROR_NO_ERROR && currentError != PVR_ERROR_NOT_IMPLEMENTED)
+ {
+ lastError = currentError;
+ failedClients.emplace_back(clientEntry.first);
+ }
+ }
+ return lastError;
+}
+
+PVR_ERROR CPVRClients::ForClients(const char* strFunctionName,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ const PVRClientFunction& function,
+ std::vector<int>& failedClients) const
+{
+ if (clients.empty())
+ return ForCreatedClients(strFunctionName, function, failedClients);
+
+ PVR_ERROR lastError = PVR_ERROR_NO_ERROR;
+
+ failedClients.clear();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& entry : m_clientMap)
+ {
+ if (entry.second->ReadyToUse() && !entry.second->IgnoreClient() &&
+ std::any_of(clients.cbegin(), clients.cend(),
+ [&entry](const auto& client) { return client->GetID() == entry.first; }))
+ {
+ // Allow ready to use clients that shall be called
+ continue;
+ }
+
+ failedClients.emplace_back(entry.first);
+ }
+ }
+
+ for (const auto& client : clients)
+ {
+ if (std::none_of(failedClients.cbegin(), failedClients.cend(),
+ [&client](int failedClientId) { return failedClientId == client->GetID(); }))
+ {
+ PVR_ERROR currentError = function(client);
+
+ if (currentError != PVR_ERROR_NO_ERROR && currentError != PVR_ERROR_NOT_IMPLEMENTED)
+ {
+ lastError = currentError;
+ failedClients.emplace_back(client->GetID());
+ }
+ }
+ else
+ {
+ LogClientWarning(strFunctionName, client);
+ }
+ }
+ return lastError;
+}
diff --git a/xbmc/pvr/addons/PVRClients.h b/xbmc/pvr/addons/PVRClients.h
new file mode 100644
index 0000000..32248fa
--- /dev/null
+++ b/xbmc/pvr/addons/PVRClients.h
@@ -0,0 +1,485 @@
+/*
+ * 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 "addons/IAddonManagerCallback.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_general.h"
+#include "threads/CriticalSection.h"
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CVariant;
+
+namespace ADDON
+{
+ struct AddonEvent;
+ class CAddonInfo;
+}
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRChannelGroupInternal;
+class CPVRChannelGroup;
+class CPVRChannelGroupMember;
+class CPVRChannelGroups;
+class CPVRProvidersContainer;
+class CPVRClient;
+class CPVREpg;
+class CPVRRecordings;
+class CPVRTimerType;
+class CPVRTimersContainer;
+
+typedef std::map<int, std::shared_ptr<CPVRClient>> CPVRClientMap;
+
+/**
+ * Holds generic data about a backend (number of channels etc.)
+ */
+struct SBackend
+{
+ std::string name;
+ std::string version;
+ std::string host;
+ int numTimers = 0;
+ int numRecordings = 0;
+ int numDeletedRecordings = 0;
+ int numProviders = 0;
+ int numChannelGroups = 0;
+ int numChannels = 0;
+ uint64_t diskUsed = 0;
+ uint64_t diskTotal = 0;
+};
+
+ class CPVRClients : public ADDON::IAddonMgrCallback
+ {
+ public:
+ CPVRClients();
+ ~CPVRClients() override;
+
+ /*!
+ * @brief Start all clients.
+ */
+ void Start();
+
+ /*!
+ * @brief Stop all clients.
+ */
+ void Stop();
+
+ /*!
+ * @brief Continue all clients.
+ */
+ void Continue();
+
+ /*!
+ * @brief Update all clients, sync with Addon Manager state (start, restart, shutdown clients).
+ * @param changedAddonId The id of the changed addon, empty string denotes 'any addon'.
+ * @param changedInstanceId The Identifier of the changed add-on instance
+ */
+ void UpdateClients(
+ const std::string& changedAddonId = "",
+ ADDON::AddonInstanceId changedInstanceId = ADDON::ADDON_SINGLETON_INSTANCE_ID);
+
+ /*!
+ * @brief Restart a single client add-on.
+ * @param addonId The add-on to restart.
+ * @param instanceId Instance identifier to use
+ * @param bDataChanged True if the client's data changed, false otherwise (unused).
+ * @return True if the client was found and restarted, false otherwise.
+ */
+ bool RequestRestart(const std::string& addonId,
+ ADDON::AddonInstanceId instanceId,
+ bool bDataChanged) override;
+
+ /*!
+ * @brief Stop a client.
+ * @param clientId The id of the client to stop.
+ * @param restart If true, restart the client.
+ * @return True if the client was found, false otherwise.
+ */
+ bool StopClient(int clientId, bool restart);
+
+ /*!
+ * @brief Handle addon events (enable, disable, ...).
+ * @param event The addon event.
+ */
+ void OnAddonEvent(const ADDON::AddonEvent& event);
+
+ /*!
+ * @brief Get the number of created clients.
+ * @return The amount of created clients.
+ */
+ int CreatedClientAmount() const;
+
+ /*!
+ * @brief Check whether there are any created clients.
+ * @return True if at least one client is created.
+ */
+ bool HasCreatedClients() const;
+
+ /*!
+ * @brief Check whether a given client ID points to a created client.
+ * @param iClientId The client ID.
+ * @return True if the the client ID represents a created client, false otherwise.
+ */
+ bool IsCreatedClient(int iClientId) const;
+
+ /*!
+ * @brief Get the the client for the given client id, if it is created.
+ * @param clientId The ID of the client to get.
+ * @return The client if found, nullptr otherwise.
+ */
+ std::shared_ptr<CPVRClient> GetCreatedClient(int clientId) const;
+
+ /*!
+ * @brief Get all created clients.
+ * @return All created clients.
+ */
+ CPVRClientMap GetCreatedClients() const;
+
+ /*!
+ * @brief Get the ID of the first created client.
+ * @return the ID or -1 if no clients are created;
+ */
+ int GetFirstCreatedClientID();
+
+ /*!
+ * @brief Check whether there are any created, but not (yet) connected clients.
+ * @return True if at least one client is ignored.
+ */
+ bool HasIgnoredClients() const;
+
+ /*!
+ * @brief Get the number of enabled clients.
+ * @return The amount of enabled clients.
+ */
+ int EnabledClientAmount() const;
+
+ /*!
+ * @brief Check whether a given client ID points to an enabled client.
+ * @param clientId The client ID.
+ * @return True if the the client ID represents an enabled client, false otherwise.
+ */
+ bool IsEnabledClient(int clientId) const;
+
+ /*!
+ * @brief Get a list of the enabled client infos.
+ * @return A list of enabled client infos.
+ */
+ std::vector<CVariant> GetEnabledClientInfos() const;
+
+ /*!
+ * @brief Get info required for providers. Include both enabled and disabled PVR add-ons
+ * @return A list containing the information required to create client providers.
+ */
+ std::vector<CVariant> GetClientProviderInfos() const;
+
+ //@}
+
+ /*! @name general methods */
+ //@{
+
+ /*!
+ * @brief Returns properties about all created clients
+ * @return the properties
+ */
+ std::vector<SBackend> GetBackendProperties() const;
+
+ //@}
+
+ /*! @name Timer methods */
+ //@{
+
+ /*!
+ * @brief Get all timers from the given clients
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param timers Store the timers in this container.
+ * @param failedClients in case of errors will contain the ids of the clients for which the timers could not be obtained.
+ * @return true on success for all clients, false in case of error for at least one client.
+ */
+ bool GetTimers(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRTimersContainer* timers,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Get all supported timer types.
+ * @param results The container to store the result in.
+ * @return PVR_ERROR_NO_ERROR if the operation succeeded, the respective PVR_ERROR value otherwise.
+ */
+ PVR_ERROR GetTimerTypes(std::vector<std::shared_ptr<CPVRTimerType>>& results) const;
+
+ //@}
+
+ /*! @name Recording methods */
+ //@{
+
+ /*!
+ * @brief Get all recordings from the given clients
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param recordings Store the recordings in this container.
+ * @param deleted If true, return deleted recordings, return not deleted recordings otherwise.
+ * @param failedClients in case of errors will contain the ids of the clients for which the recordings could not be obtained.
+ * @return PVR_ERROR_NO_ERROR if the operation succeeded, the respective PVR_ERROR value otherwise.
+ */
+ PVR_ERROR GetRecordings(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRRecordings* recordings,
+ bool deleted,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Delete all "soft" deleted recordings permanently on the backend.
+ * @return PVR_ERROR_NO_ERROR if the operation succeeded, the respective PVR_ERROR value otherwise.
+ */
+ PVR_ERROR DeleteAllRecordingsFromTrash();
+
+ //@}
+
+ /*! @name EPG methods */
+ //@{
+
+ /*!
+ * @brief Tell all clients the past time frame to use when notifying epg events back to Kodi.
+ *
+ * The clients might push epg events asynchronously to Kodi using the callback function
+ * EpgEventStateChange. To be able to only push events that are actually of interest for Kodi,
+ * clients need to know about the future epg time frame Kodi uses.
+ *
+ * @param[in] iPastDays number of days before "now".
+ * @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all
+ * epg events, regardless of event times.
+ * @return @ref PVR_ERROR_NO_ERROR if the operation succeeded, the respective @ref PVR_ERROR
+ * value otherwise.
+ */
+ PVR_ERROR SetEPGMaxPastDays(int iPastDays);
+
+ /*!
+ * @brief Tell all clients the future time frame to use when notifying epg events back to Kodi.
+ *
+ * The clients might push epg events asynchronously to Kodi using the callback function
+ * EpgEventStateChange. To be able to only push events that are actually of interest for Kodi,
+ * clients need to know about the future epg time frame Kodi uses.
+ *
+ * @param[in] iFutureDays number of days from "now".
+ * @ref EPG_TIMEFRAME_UNLIMITED means that Kodi is interested in all
+ * epg events, regardless of event times.
+ * @return @ref PVR_ERROR_NO_ERROR if the operation succeeded, the respective @ref PVR_ERROR
+ * value otherwise.
+ */
+ PVR_ERROR SetEPGMaxFutureDays(int iFutureDays);
+
+ //@}
+
+ /*! @name Channel methods */
+ //@{
+
+ /*!
+ * @brief Get all channels from the given clients.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param bRadio Whether to fetch radio or TV channels.
+ * @param channels The container to store the channels.
+ * @param failedClients in case of errors will contain the ids of the clients for which the channels could not be obtained.
+ * @return PVR_ERROR_NO_ERROR if the channels were fetched successfully, last error otherwise.
+ */
+ PVR_ERROR GetChannels(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ bool bRadio,
+ std::vector<std::shared_ptr<CPVRChannel>>& channels,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Get all providers from backends.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param group The container to store the providers in.
+ * @param failedClients in case of errors will contain the ids of the clients for which the providers could not be obtained.
+ * @return PVR_ERROR_NO_ERROR if the providers were fetched successfully, last error otherwise.
+ */
+ PVR_ERROR GetProviders(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRProvidersContainer* providers,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Get all channel groups from the given clients.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param groups Store the channel groups in this container.
+ * @param failedClients in case of errors will contain the ids of the clients for which the channel groups could not be obtained.
+ * @return PVR_ERROR_NO_ERROR if the channel groups were fetched successfully, last error otherwise.
+ */
+ PVR_ERROR GetChannelGroups(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRChannelGroups* groups,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Get all group members of a channel group from the given clients.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param group The group to get the member for.
+ * @param groupMembers The container for the group members.
+ * @param failedClients in case of errors will contain the ids of the clients for which the channel group members could not be obtained.
+ * @return PVR_ERROR_NO_ERROR if the channel group members were fetched successfully, last error otherwise.
+ */
+ PVR_ERROR GetChannelGroupMembers(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ CPVRChannelGroup* group,
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers,
+ std::vector<int>& failedClients);
+
+ /*!
+ * @brief Get a list of clients providing a channel scan dialog.
+ * @return All clients supporting channel scan.
+ */
+ std::vector<std::shared_ptr<CPVRClient>> GetClientsSupportingChannelScan() const;
+
+ /*!
+ * @brief Get a list of clients providing a channel settings dialog.
+ * @return All clients supporting channel settings.
+ */
+ std::vector<std::shared_ptr<CPVRClient>> GetClientsSupportingChannelSettings(bool bRadio) const;
+
+ /*!
+ * @brief Get whether or not any client supports recording size.
+ * @return True if any client supports recording size.
+ */
+ bool AnyClientSupportingRecordingsSize() const;
+
+ /*!
+ * @brief Get whether or not any client supports EPG.
+ * @return True if any client supports EPG.
+ */
+ bool AnyClientSupportingEPG() const;
+
+ /*!
+ * @brief Get whether or not any client supports recordings.
+ * @return True if any client supports recordings.
+ */
+ bool AnyClientSupportingRecordings() const;
+ //@}
+
+ /*!
+ * @brief Get whether or not any client supports recordings delete.
+ * @return True if any client supports recordings delete.
+ */
+ bool AnyClientSupportingRecordingsDelete() const;
+ //@}
+
+ /*! @name Power management methods */
+ //@{
+
+ /*!
+ * @brief Propagate "system sleep" event to clients
+ */
+ void OnSystemSleep();
+
+ /*!
+ * @brief Propagate "system wakeup" event to clients
+ */
+ void OnSystemWake();
+
+ /*!
+ * @brief Propagate "power saving activated" event to clients
+ */
+ void OnPowerSavingActivated();
+
+ /*!
+ * @brief Propagate "power saving deactivated" event to clients
+ */
+ void OnPowerSavingDeactivated();
+
+ //@}
+
+ /*!
+ * @brief Notify a change of an addon connection state.
+ * @param client The changed client.
+ * @param strConnectionString A human-readable string providing additional information.
+ * @param newState The new connection state.
+ * @param strMessage A human readable string replacing default state message.
+ */
+ void ConnectionStateChange(CPVRClient* client,
+ const std::string& strConnectionString,
+ PVR_CONNECTION_STATE newState,
+ const std::string& strMessage);
+
+ private:
+ /*!
+ * @brief Get the known instance ids for a given addon id.
+ * @param addonID The addon id.
+ * @return The list of known instance ids.
+ */
+ std::vector<ADDON::AddonInstanceId> GetKnownInstanceIds(const std::string& addonID) const;
+
+ bool GetAddonsWithStatus(
+ const std::string& changedAddonId,
+ std::vector<std::pair<std::shared_ptr<ADDON::CAddonInfo>, bool>>& addonsWithStatus) const;
+
+ std::vector<std::pair<ADDON::AddonInstanceId, bool>> GetInstanceIdsWithStatus(
+ const std::shared_ptr<ADDON::CAddonInfo>& addon, bool addonIsEnabled) const;
+
+ /*!
+ * @brief Get the client instance for a given client id.
+ * @param clientId The id of the client to get.
+ * @return The client if found, nullptr otherwise.
+ */
+ std::shared_ptr<CPVRClient> GetClient(int clientId) const;
+
+ /*!
+ * @brief Check whether a client is known.
+ * @param iClientId The id of the client to check.
+ * @return True if this client is known, false otherwise.
+ */
+ bool IsKnownClient(int iClientId) const;
+
+ /*!
+ * @brief Get all created clients and clients not (yet) ready to use.
+ * @param clientsReady Store the created clients in this map.
+ * @param clientsNotReady Store the the ids of the not (yet) ready clients in this list.
+ * @return PVR_ERROR_NO_ERROR in case all clients are ready, PVR_ERROR_SERVER_ERROR otherwise.
+ */
+ PVR_ERROR GetCallableClients(CPVRClientMap& clientsReady,
+ std::vector<int>& clientsNotReady) const;
+
+ typedef std::function<PVR_ERROR(const std::shared_ptr<CPVRClient>&)> PVRClientFunction;
+
+ /*!
+ * @brief Wraps calls to the given clients in order to do common pre and post function invocation actions.
+ * @param strFunctionName The function name, for logging purposes.
+ * @param clients The clients to wrap.
+ * @param function The function to wrap. It has to have return type PVR_ERROR and must take a const reference to a std::shared_ptr<CPVRClient> as parameter.
+ * @param failedClients Contains a list of the ids of clients for that the call failed, if any.
+ * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise.
+ */
+ PVR_ERROR ForClients(const char* strFunctionName,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ const PVRClientFunction& function,
+ std::vector<int>& failedClients) const;
+
+ /*!
+ * @brief Wraps calls to all created clients in order to do common pre and post function invocation actions.
+ * @param strFunctionName The function name, for logging purposes.
+ * @param function The function to wrap. It has to have return type PVR_ERROR and must take a const reference to a std::shared_ptr<CPVRClient> as parameter.
+ * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise.
+ */
+ PVR_ERROR ForCreatedClients(const char* strFunctionName,
+ const PVRClientFunction& function) const;
+
+ /*!
+ * @brief Wraps calls to all created clients in order to do common pre and post function invocation actions.
+ * @param strFunctionName The function name, for logging purposes.
+ * @param function The function to wrap. It has to have return type PVR_ERROR and must take a const reference to a std::shared_ptr<CPVRClient> as parameter.
+ * @param failedClients Contains a list of the ids of clients for that the call failed, if any.
+ * @return PVR_ERROR_NO_ERROR on success, any other PVR_ERROR_* value otherwise.
+ */
+ PVR_ERROR ForCreatedClients(const char* strFunctionName,
+ const PVRClientFunction& function,
+ std::vector<int>& failedClients) const;
+
+ mutable CCriticalSection m_critSection;
+ CPVRClientMap m_clientMap;
+ };
+}
diff --git a/xbmc/pvr/channels/CMakeLists.txt b/xbmc/pvr/channels/CMakeLists.txt
new file mode 100644
index 0000000..714f232
--- /dev/null
+++ b/xbmc/pvr/channels/CMakeLists.txt
@@ -0,0 +1,23 @@
+set(SOURCES PVRChannel.cpp
+ PVRChannelGroup.cpp
+ PVRChannelGroupInternal.cpp
+ PVRChannelGroupMember.cpp
+ PVRChannelGroupSettings.cpp
+ PVRChannelGroups.cpp
+ PVRChannelGroupsContainer.cpp
+ PVRChannelNumber.cpp
+ PVRRadioRDSInfoTag.cpp
+ PVRChannelsPath.cpp)
+
+set(HEADERS PVRChannel.h
+ PVRChannelGroup.h
+ PVRChannelGroupInternal.h
+ PVRChannelGroupMember.h
+ PVRChannelGroupSettings.h
+ PVRChannelGroups.h
+ PVRChannelGroupsContainer.h
+ PVRChannelNumber.h
+ PVRRadioRDSInfoTag.h
+ PVRChannelsPath.h)
+
+core_add_library(pvr_channels)
diff --git a/xbmc/pvr/channels/PVRChannel.cpp b/xbmc/pvr/channels/PVRChannel.cpp
new file mode 100644
index 0000000..135719f
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannel.cpp
@@ -0,0 +1,843 @@
+/*
+ * 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 "PVRChannel.h"
+
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/providers/PVRProviders.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+
+using namespace PVR;
+
+const std::string CPVRChannel::IMAGE_OWNER_PATTERN = "pvrchannel_{}";
+
+bool CPVRChannel::operator==(const CPVRChannel& right) const
+{
+ return (m_bIsRadio == right.m_bIsRadio && m_iUniqueId == right.m_iUniqueId &&
+ m_iClientId == right.m_iClientId);
+}
+
+bool CPVRChannel::operator!=(const CPVRChannel& right) const
+{
+ return !(*this == right);
+}
+
+CPVRChannel::CPVRChannel(bool bRadio)
+ : m_bIsRadio(bRadio),
+ m_iconPath("", StringUtils::Format(IMAGE_OWNER_PATTERN, bRadio ? "radio" : "tv"))
+{
+ UpdateEncryptionName();
+}
+
+CPVRChannel::CPVRChannel(bool bRadio, const std::string& iconPath)
+ : m_bIsRadio(bRadio),
+ m_iconPath(iconPath, StringUtils::Format(IMAGE_OWNER_PATTERN, bRadio ? "radio" : "tv"))
+{
+ UpdateEncryptionName();
+}
+
+CPVRChannel::CPVRChannel(const PVR_CHANNEL& channel, unsigned int iClientId)
+ : m_bIsRadio(channel.bIsRadio),
+ m_bIsHidden(channel.bIsHidden),
+ m_iconPath(channel.strIconPath,
+ StringUtils::Format(IMAGE_OWNER_PATTERN, channel.bIsRadio ? "radio" : "tv")),
+ m_strChannelName(channel.strChannelName),
+ m_bHasArchive(channel.bHasArchive),
+ m_bEPGEnabled(!channel.bIsHidden),
+ m_iUniqueId(channel.iUniqueId),
+ m_iClientId(iClientId),
+ m_clientChannelNumber(channel.iChannelNumber, channel.iSubChannelNumber),
+ m_strClientChannelName(channel.strChannelName),
+ m_strMimeType(channel.strMimeType),
+ m_iClientEncryptionSystem(channel.iEncryptionSystem),
+ m_iClientOrder(channel.iOrder),
+ m_iClientProviderUid(channel.iClientProviderUid)
+{
+ if (m_strChannelName.empty())
+ m_strChannelName = StringUtils::Format("{} {}", g_localizeStrings.Get(19029), m_iUniqueId);
+
+ UpdateEncryptionName();
+}
+
+CPVRChannel::~CPVRChannel()
+{
+ ResetEPG();
+}
+
+void CPVRChannel::FillAddonData(PVR_CHANNEL& channel) const
+{
+ channel = {};
+ channel.iUniqueId = UniqueID();
+ channel.iChannelNumber = ClientChannelNumber().GetChannelNumber();
+ channel.iSubChannelNumber = ClientChannelNumber().GetSubChannelNumber();
+ strncpy(channel.strChannelName, ClientChannelName().c_str(), sizeof(channel.strChannelName) - 1);
+ strncpy(channel.strIconPath, ClientIconPath().c_str(), sizeof(channel.strIconPath) - 1);
+ channel.iEncryptionSystem = EncryptionSystem();
+ channel.bIsRadio = IsRadio();
+ channel.bIsHidden = IsHidden();
+ strncpy(channel.strMimeType, MimeType().c_str(), sizeof(channel.strMimeType) - 1);
+ channel.iClientProviderUid = ClientProviderUid();
+ channel.bHasArchive = HasArchive();
+}
+
+void CPVRChannel::Serialize(CVariant& value) const
+{
+ value["channelid"] = m_iChannelId;
+ value["channeltype"] = m_bIsRadio ? "radio" : "tv";
+ value["hidden"] = m_bIsHidden;
+ value["locked"] = m_bIsLocked;
+ value["icon"] = ClientIconPath();
+ value["channel"] = m_strChannelName;
+ value["uniqueid"] = m_iUniqueId;
+ CDateTime lastPlayed(m_iLastWatched);
+ value["lastplayed"] = lastPlayed.IsValid() ? lastPlayed.GetAsDBDate() : "";
+
+ std::shared_ptr<CPVREpgInfoTag> epg = GetEPGNow();
+ if (epg)
+ {
+ // add the properties of the current EPG item to the main object
+ epg->Serialize(value);
+ // and add an extra sub-object with only the current EPG details
+ epg->Serialize(value["broadcastnow"]);
+ }
+
+ epg = GetEPGNext();
+ if (epg)
+ epg->Serialize(value["broadcastnext"]);
+
+ value["hasarchive"] = m_bHasArchive;
+ value["clientid"] = m_iClientId;
+}
+
+bool CPVRChannel::QueueDelete()
+{
+ bool bReturn = false;
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (!database)
+ return bReturn;
+
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ ResetEPG();
+
+ bReturn = database->QueueDeleteQuery(*this);
+ return bReturn;
+}
+
+std::shared_ptr<CPVREpg> CPVRChannel::GetEPG() const
+{
+ const_cast<CPVRChannel*>(this)->CreateEPG();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_bIsHidden && m_bEPGEnabled)
+ return m_epg;
+
+ return {};
+}
+
+bool CPVRChannel::CreateEPG()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_epg)
+ {
+ m_epg = CServiceBroker::GetPVRManager().EpgContainer().CreateChannelEpg(
+ m_iEpgId, m_strEPGScraper, std::make_shared<CPVREpgChannelData>(*this));
+ if (m_epg)
+ {
+ if (m_epg->EpgID() != m_iEpgId)
+ {
+ m_iEpgId = m_epg->EpgID();
+ m_bChanged = true;
+ }
+
+ // Subscribe for EPG delete event
+ m_epg->Events().Subscribe(this, &CPVRChannel::Notify);
+ return true;
+ }
+ }
+ return false;
+}
+
+void CPVRChannel::Notify(const PVREvent& event)
+{
+ if (event == PVREvent::EpgDeleted)
+ {
+ ResetEPG();
+ }
+}
+
+void CPVRChannel::ResetEPG()
+{
+ std::shared_ptr<CPVREpg> epgToUnsubscribe;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_epg)
+ {
+ epgToUnsubscribe = m_epg;
+ m_epg.reset();
+ }
+ }
+
+ if (epgToUnsubscribe)
+ epgToUnsubscribe->Events().Unsubscribe(this);
+}
+
+bool CPVRChannel::UpdateFromClient(const std::shared_ptr<CPVRChannel>& channel)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ SetClientID(channel->ClientID());
+ SetArchive(channel->HasArchive());
+ SetClientProviderUid(channel->ClientProviderUid());
+
+ m_clientChannelNumber = channel->m_clientChannelNumber;
+ m_strMimeType = channel->MimeType();
+ m_iClientEncryptionSystem = channel->EncryptionSystem();
+ m_strClientChannelName = channel->ClientChannelName();
+
+ UpdateEncryptionName();
+
+ // only update the channel name, icon, and hidden flag if the user hasn't changed them manually
+ if (m_strChannelName.empty() || !IsUserSetName())
+ SetChannelName(channel->ClientChannelName());
+ if (IconPath().empty() || !IsUserSetIcon())
+ SetIconPath(channel->ClientIconPath());
+ if (!IsUserSetHidden())
+ SetHidden(channel->IsHidden());
+
+ return m_bChanged;
+}
+
+bool CPVRChannel::Persist()
+{
+ {
+ // not changed
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_bChanged && m_iChannelId > 0)
+ return true;
+ }
+
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting channel '{}'", m_strChannelName);
+
+ bool bReturn = database->Persist(*this, true);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bChanged = !bReturn;
+ return bReturn;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetChannelID(int iChannelId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_iChannelId != iChannelId)
+ {
+ m_iChannelId = iChannelId;
+
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ epg->GetChannelData()->SetChannelId(m_iChannelId);
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetHidden(bool bIsHidden, bool bIsUserSetHidden /*= false*/)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsHidden != bIsHidden)
+ {
+ m_bIsHidden = bIsHidden;
+ m_bIsUserSetHidden = bIsUserSetHidden;
+
+ if (m_epg)
+ m_epg->GetChannelData()->SetHidden(m_bIsHidden);
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetLocked(bool bIsLocked)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsLocked != bIsLocked)
+ {
+ m_bIsLocked = bIsLocked;
+
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ epg->GetChannelData()->SetLocked(m_bIsLocked);
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+std::shared_ptr<CPVRRadioRDSInfoTag> CPVRChannel::GetRadioRDSInfoTag() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_rdsTag;
+}
+
+void CPVRChannel::SetRadioRDSInfoTag(const std::shared_ptr<CPVRRadioRDSInfoTag>& tag)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_rdsTag = tag;
+}
+
+bool CPVRChannel::HasArchive() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bHasArchive;
+}
+
+bool CPVRChannel::SetArchive(bool bHasArchive)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bHasArchive != bHasArchive)
+ {
+ m_bHasArchive = bHasArchive;
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetIconPath(const std::string& strIconPath, bool bIsUserSetIcon /* = false */)
+{
+ if (StringUtils::StartsWith(strIconPath, "image://"))
+ {
+ CLog::LogF(LOGERROR, "Not allowed to call this method with an image URL");
+ return false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (ClientIconPath() == strIconPath)
+ return false;
+
+ m_iconPath.SetClientImage(strIconPath);
+
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ epg->GetChannelData()->SetChannelIconPath(strIconPath);
+
+ m_bChanged = true;
+ m_bIsUserSetIcon = bIsUserSetIcon && !IconPath().empty();
+ return true;
+}
+
+bool CPVRChannel::SetChannelName(const std::string& strChannelName, bool bIsUserSetName /*= false*/)
+{
+ std::string strName(strChannelName);
+
+ if (strName.empty())
+ strName = StringUtils::Format(g_localizeStrings.Get(19085),
+ m_clientChannelNumber.FormattedChannelNumber());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_strChannelName != strName)
+ {
+ m_strChannelName = strName;
+ m_bIsUserSetName = bIsUserSetName;
+
+ /* if the user changes the name manually to an empty string we reset the
+ flag and use the name from the client instead */
+ if (bIsUserSetName && strChannelName.empty())
+ {
+ m_bIsUserSetName = false;
+ m_strChannelName = ClientChannelName();
+ }
+
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ epg->GetChannelData()->SetChannelName(m_strChannelName);
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetLastWatched(time_t iLastWatched)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iLastWatched = iLastWatched;
+ }
+
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ return database->UpdateLastWatched(*this);
+
+ return false;
+}
+
+/********** Client related channel methods **********/
+
+bool CPVRChannel::SetClientID(int iClientId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iClientId != iClientId)
+ {
+ m_iClientId = iClientId;
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+std::string CPVRChannel::GetEncryptionName(int iCaid)
+{
+ // http://www.dvb.org/index.php?id=174
+ // http://en.wikipedia.org/wiki/Conditional_access_system
+ std::string strName(g_localizeStrings.Get(13205)); /* Unknown */
+
+ if (iCaid == 0x0000)
+ strName = g_localizeStrings.Get(19013); /* Free To Air */
+ else if (iCaid >= 0x0001 && iCaid <= 0x009F)
+ strName = g_localizeStrings.Get(19014); /* Fixed */
+ else if (iCaid >= 0x00A0 && iCaid <= 0x00A1)
+ strName = g_localizeStrings.Get(338); /* Analog */
+ else if (iCaid >= 0x00A2 && iCaid <= 0x00FF)
+ strName = g_localizeStrings.Get(19014); /* Fixed */
+ else if (iCaid >= 0x0100 && iCaid <= 0x01FF)
+ strName = "SECA Mediaguard";
+ else if (iCaid == 0x0464)
+ strName = "EuroDec";
+ else if (iCaid >= 0x0500 && iCaid <= 0x05FF)
+ strName = "Viaccess";
+ else if (iCaid >= 0x0600 && iCaid <= 0x06FF)
+ strName = "Irdeto";
+ else if (iCaid >= 0x0900 && iCaid <= 0x09FF)
+ strName = "NDS Videoguard";
+ else if (iCaid >= 0x0B00 && iCaid <= 0x0BFF)
+ strName = "Conax";
+ else if (iCaid >= 0x0D00 && iCaid <= 0x0DFF)
+ strName = "CryptoWorks";
+ else if (iCaid >= 0x0E00 && iCaid <= 0x0EFF)
+ strName = "PowerVu";
+ else if (iCaid == 0x1000)
+ strName = "RAS";
+ else if (iCaid >= 0x1200 && iCaid <= 0x12FF)
+ strName = "NagraVision";
+ else if (iCaid >= 0x1700 && iCaid <= 0x17FF)
+ strName = "BetaCrypt";
+ else if (iCaid >= 0x1800 && iCaid <= 0x18FF)
+ strName = "NagraVision";
+ else if (iCaid == 0x22F0)
+ strName = "Codicrypt";
+ else if (iCaid == 0x2600)
+ strName = "BISS";
+ else if (iCaid == 0x4347)
+ strName = "CryptOn";
+ else if (iCaid == 0x4800)
+ strName = "Accessgate";
+ else if (iCaid == 0x4900)
+ strName = "China Crypt";
+ else if (iCaid == 0x4A10)
+ strName = "EasyCas";
+ else if (iCaid == 0x4A20)
+ strName = "AlphaCrypt";
+ else if (iCaid == 0x4A70)
+ strName = "DreamCrypt";
+ else if (iCaid == 0x4A60)
+ strName = "SkyCrypt";
+ else if (iCaid == 0x4A61)
+ strName = "Neotioncrypt";
+ else if (iCaid == 0x4A62)
+ strName = "SkyCrypt";
+ else if (iCaid == 0x4A63)
+ strName = "Neotion SHL";
+ else if (iCaid >= 0x4A64 && iCaid <= 0x4A6F)
+ strName = "SkyCrypt";
+ else if (iCaid == 0x4A80)
+ strName = "ThalesCrypt";
+ else if (iCaid == 0x4AA1)
+ strName = "KeyFly";
+ else if (iCaid == 0x4ABF)
+ strName = "DG-Crypt";
+ else if (iCaid >= 0x4AD0 && iCaid <= 0x4AD1)
+ strName = "X-Crypt";
+ else if (iCaid == 0x4AD4)
+ strName = "OmniCrypt";
+ else if (iCaid == 0x4AE0)
+ strName = "RossCrypt";
+ else if (iCaid == 0x5500)
+ strName = "Z-Crypt";
+ else if (iCaid == 0x5501)
+ strName = "Griffin";
+ else if (iCaid == 0x5601)
+ strName = "Verimatrix";
+
+ if (iCaid >= 0)
+ strName += StringUtils::Format(" ({:04X})", iCaid);
+
+ return strName;
+}
+
+void CPVRChannel::UpdateEncryptionName()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strClientEncryptionName = GetEncryptionName(m_iClientEncryptionSystem);
+}
+
+bool CPVRChannel::SetClientProviderUid(int iClientProviderUid)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iClientProviderUid != iClientProviderUid)
+ {
+ m_iClientProviderUid = iClientProviderUid;
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+/********** EPG methods **********/
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVRChannel::GetEpgTags() const
+{
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (!epg)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Cannot get EPG for channel '{}'", m_strChannelName);
+ return {};
+ }
+
+ return epg->GetTags();
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVRChannel::GetEPGTimeline(
+ const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const
+{
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ {
+ return epg->GetTimeline(timelineStart, timelineEnd, minEventEnd, maxEventStart);
+ }
+ else
+ {
+ // return single gap tag spanning whole timeline
+ return std::vector<std::shared_ptr<CPVREpgInfoTag>>{
+ CreateEPGGapTag(timelineStart, timelineEnd)};
+ }
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRChannel::CreateEPGGapTag(const CDateTime& start,
+ const CDateTime& end) const
+{
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ return std::make_shared<CPVREpgInfoTag>(epg->GetChannelData(), epg->EpgID(), start, end, true);
+ else
+ return std::make_shared<CPVREpgInfoTag>(std::make_shared<CPVREpgChannelData>(*this), -1, start,
+ end, true);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRChannel::GetEPGNow() const
+{
+ std::shared_ptr<CPVREpgInfoTag> tag;
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ tag = epg->GetTagNow();
+
+ return tag;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRChannel::GetEPGNext() const
+{
+ std::shared_ptr<CPVREpgInfoTag> tag;
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ tag = epg->GetTagNext();
+
+ return tag;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRChannel::GetEPGPrevious() const
+{
+ std::shared_ptr<CPVREpgInfoTag> tag;
+ const std::shared_ptr<CPVREpg> epg = GetEPG();
+ if (epg)
+ tag = epg->GetTagPrevious();
+
+ return tag;
+}
+
+bool CPVRChannel::SetEPGEnabled(bool bEPGEnabled)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bEPGEnabled != bEPGEnabled)
+ {
+ m_bEPGEnabled = bEPGEnabled;
+
+ if (m_epg)
+ {
+ m_epg->GetChannelData()->SetEPGEnabled(m_bEPGEnabled);
+
+ if (m_bEPGEnabled)
+ m_epg->ForceUpdate();
+ else
+ m_epg->Clear();
+ }
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRChannel::SetEPGScraper(const std::string& strScraper)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_strEPGScraper != strScraper)
+ {
+ bool bCleanEPG = !m_strEPGScraper.empty() || strScraper.empty();
+
+ m_strEPGScraper = strScraper;
+
+ if (bCleanEPG && m_epg)
+ m_epg->Clear();
+
+ m_bChanged = true;
+ return true;
+ }
+
+ return false;
+}
+
+void CPVRChannel::ToSortable(SortItem& sortable, Field field) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (field == FieldChannelName)
+ sortable[FieldChannelName] = m_strChannelName;
+ else if (field == FieldLastPlayed)
+ {
+ const CDateTime lastWatched(m_iLastWatched);
+ sortable[FieldLastPlayed] =
+ lastWatched.IsValid() ? lastWatched.GetAsDBDateTime() : StringUtils::Empty;
+ }
+ else if (field == FieldProvider)
+ sortable[FieldProvider] = StringUtils::Format("{} {}", m_iClientId, m_iClientProviderUid);
+}
+
+int CPVRChannel::ChannelID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iChannelId;
+}
+
+bool CPVRChannel::IsNew() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iChannelId <= 0;
+}
+
+bool CPVRChannel::IsHidden() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsHidden;
+}
+
+bool CPVRChannel::IsLocked() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsLocked;
+}
+
+std::string CPVRChannel::ClientIconPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iconPath.GetClientImage();
+}
+
+std::string CPVRChannel::IconPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iconPath.GetLocalImage();
+}
+
+bool CPVRChannel::IsUserSetIcon() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsUserSetIcon;
+}
+
+bool CPVRChannel::IsUserSetName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsUserSetName;
+}
+
+bool CPVRChannel::IsUserSetHidden() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsUserSetHidden;
+}
+
+std::string CPVRChannel::ChannelName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strChannelName;
+}
+
+time_t CPVRChannel::LastWatched() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iLastWatched;
+}
+
+bool CPVRChannel::IsChanged() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bChanged;
+}
+
+void CPVRChannel::Persisted()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bChanged = false;
+}
+
+int CPVRChannel::UniqueID() const
+{
+ return m_iUniqueId;
+}
+
+int CPVRChannel::ClientID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientId;
+}
+
+const CPVRChannelNumber& CPVRChannel::ClientChannelNumber() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_clientChannelNumber;
+}
+
+std::string CPVRChannel::ClientChannelName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strClientChannelName;
+}
+
+std::string CPVRChannel::MimeType() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strMimeType;
+}
+
+bool CPVRChannel::IsEncrypted() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientEncryptionSystem > 0;
+}
+
+int CPVRChannel::EncryptionSystem() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientEncryptionSystem;
+}
+
+std::string CPVRChannel::EncryptionName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strClientEncryptionName;
+}
+
+int CPVRChannel::EpgID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iEpgId;
+}
+
+bool CPVRChannel::EPGEnabled() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bEPGEnabled;
+}
+
+std::string CPVRChannel::EPGScraper() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strEPGScraper;
+}
+
+bool CPVRChannel::CanRecord() const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ return client && client->GetClientCapabilities().SupportsRecordings() &&
+ client->GetClientCapabilities().SupportsTimers();
+}
+
+std::shared_ptr<CPVRProvider> CPVRChannel::GetDefaultProvider() const
+{
+ return CServiceBroker::GetPVRManager().Providers()->GetByClient(m_iClientId,
+ PVR_PROVIDER_INVALID_UID);
+}
+
+bool CPVRChannel::HasClientProvider() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientProviderUid != PVR_PROVIDER_INVALID_UID;
+}
+
+std::shared_ptr<CPVRProvider> CPVRChannel::GetProvider() const
+{
+ auto provider =
+ CServiceBroker::GetPVRManager().Providers()->GetByClient(m_iClientId, m_iClientProviderUid);
+
+ if (!provider)
+ provider = GetDefaultProvider();
+
+ return provider;
+}
diff --git a/xbmc/pvr/channels/PVRChannel.h b/xbmc/pvr/channels/PVRChannel.h
new file mode 100644
index 0000000..8ff5054
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannel.h
@@ -0,0 +1,550 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h"
+#include "pvr/PVRCachedImage.h"
+#include "pvr/channels/PVRChannelNumber.h"
+#include "threads/CriticalSection.h"
+#include "utils/ISerializable.h"
+#include "utils/ISortable.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CDateTime;
+
+namespace PVR
+{
+enum class PVREvent;
+
+class CPVRProvider;
+class CPVREpg;
+class CPVREpgInfoTag;
+class CPVRRadioRDSInfoTag;
+
+class CPVRChannel : public ISerializable, public ISortable
+{
+ friend class CPVRDatabase;
+
+public:
+ static const std::string IMAGE_OWNER_PATTERN;
+
+ explicit CPVRChannel(bool bRadio);
+ CPVRChannel(bool bRadio, const std::string& iconPath);
+ CPVRChannel(const PVR_CHANNEL& channel, unsigned int iClientId);
+
+ virtual ~CPVRChannel();
+
+ bool operator==(const CPVRChannel& right) const;
+ bool operator!=(const CPVRChannel& right) const;
+
+ /*!
+ * @brief Copy over data to the given PVR_CHANNEL instance.
+ * @param channel The channel instance to fill.
+ */
+ void FillAddonData(PVR_CHANNEL& channel) const;
+
+ void Serialize(CVariant& value) const override;
+
+ /*! @name Kodi related channel methods
+ */
+ //@{
+
+ /*!
+ * @brief Delete this channel from the database.
+ * @return True if it was deleted successfully, false otherwise.
+ */
+ bool QueueDelete();
+
+ /*!
+ * @brief Update this channel tag with the data of the given channel tag.
+ * @param channel The new channel data.
+ * @return True if something changed, false otherwise.
+ */
+ bool UpdateFromClient(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Persists the changes in the database.
+ * @return True if the changes were saved successfully, false otherwise.
+ */
+ bool Persist();
+
+ /*!
+ * @return The identifier given to this channel by the TV database.
+ */
+ int ChannelID() const;
+
+ /*!
+ * @return True when not persisted yet, false otherwise.
+ */
+ bool IsNew() const;
+
+ /*!
+ * @brief Set the identifier for this channel.
+ * @param iDatabaseId The new channel ID
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetChannelID(int iDatabaseId);
+
+ /*!
+ * @return True if this channel is a radio channel, false if not.
+ */
+ bool IsRadio() const { return m_bIsRadio; }
+
+ /*!
+ * @return True if this channel is hidden. False if not.
+ */
+ bool IsHidden() const;
+
+ /*!
+ * @brief Set to true to hide this channel. Set to false to unhide it.
+ *
+ * Set to true to hide this channel. Set to false to unhide it.
+ * The EPG of hidden channels won't be updated.
+ * @param bIsHidden The new setting.
+ * @param bIsUserSetIcon true if user changed the hidden flag via GUI, false otherwise.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetHidden(bool bIsHidden, bool bIsUserSetHidden = false);
+
+ /*!
+ * @return True if this channel is locked. False if not.
+ */
+ bool IsLocked() const;
+
+ /*!
+ * @brief Set to true to lock this channel. Set to false to unlock it.
+ *
+ * Set to true to lock this channel. Set to false to unlock it.
+ * Locked channels need can only be viewed if parental PIN entered.
+ * @param bIsLocked The new setting.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetLocked(bool bIsLocked);
+
+ /*!
+ * @brief Obtain the Radio RDS data for this channel, if available.
+ * @return The Radio RDS data or nullptr.
+ */
+ std::shared_ptr<CPVRRadioRDSInfoTag> GetRadioRDSInfoTag() const;
+
+ /*!
+ * @brief Set the Radio RDS data for the channel.
+ * @param tag The RDS data.
+ */
+ void SetRadioRDSInfoTag(const std::shared_ptr<CPVRRadioRDSInfoTag>& tag);
+
+ /*!
+ * @return True if this channel has archive support, false otherwise
+ */
+ bool HasArchive() const;
+
+ /*!
+ * @brief Set the archive support flag for this channel.
+ * @param bHasArchive True to set the flag, false to reset.
+ * @return True if the flag was changed, false otherwise.
+ */
+ bool SetArchive(bool bHasArchive);
+
+ /*!
+ * @return The path to the icon for this channel as given by the client.
+ */
+ std::string ClientIconPath() const;
+
+ /*!
+ * @return The path to the icon for this channel.
+ */
+ std::string IconPath() const;
+
+ /*!
+ * @return True if this user changed icon via GUI. False if not.
+ */
+ bool IsUserSetIcon() const;
+
+ /*!
+ * @return whether the user has changed the channel name through the GUI
+ */
+ bool IsUserSetName() const;
+
+ /*!
+ * @return True if user changed the hidden flag via GUI, False if not
+ */
+ bool IsUserSetHidden() const;
+
+ /*!
+ * @brief Set the path to the icon for this channel.
+ * @param strIconPath The new path.
+ * @param bIsUserSetIcon true if user changed the icon via GUI, false otherwise.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetIconPath(const std::string& strIconPath, bool bIsUserSetIcon = false);
+
+ /*!
+ * @return The name for this channel used by XBMC.
+ */
+ std::string ChannelName() const;
+
+ /*!
+ * @brief Set the name for this channel used by XBMC.
+ * @param strChannelName The new channel name.
+ * @param bIsUserSetName whether the change was triggered by the user directly
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetChannelName(const std::string& strChannelName, bool bIsUserSetName = false);
+
+ /*!
+ * @return Time channel has been watched last.
+ */
+ time_t LastWatched() const;
+
+ /*!
+ * @brief Last time channel has been watched
+ * @param iLastWatched The new value.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetLastWatched(time_t iLastWatched);
+
+ /*!
+ * @brief Check whether this channel has unpersisted data changes.
+ * @return True if this channel has changes to persist, false otherwise
+ */
+ bool IsChanged() const;
+
+ /*!
+ * @brief reset changed flag after persist
+ */
+ void Persisted();
+ //@}
+
+ /*! @name Client related channel methods
+ */
+ //@{
+
+ /*!
+ * @brief A unique identifier for this channel.
+ *
+ * A unique identifier for this channel.
+ * It can be used to find the same channel on different providers
+ *
+ * @return The Unique ID.
+ */
+ int UniqueID() const;
+
+ /*!
+ * @return The identifier of the client that serves this channel.
+ */
+ int ClientID() const;
+
+ /*!
+ * @brief Set the identifier of the client that serves this channel.
+ * @param iClientId The new ID.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetClientID(int iClientId);
+
+ /*!
+ * Get the channel number on the client.
+ * @return The channel number on the client.
+ */
+ const CPVRChannelNumber& ClientChannelNumber() const;
+
+ /*!
+ * @return The name of this channel on the client.
+ */
+ std::string ClientChannelName() const;
+
+ /*!
+ * @brief The stream input mime type
+ *
+ * The stream input type
+ * If it is empty, ffmpeg will try to scan the stream to find the right input format.
+ * See https://www.iana.org/assignments/media-types/media-types.xhtml for a
+ * list of the input formats.
+ *
+ * @return The stream input type
+ */
+ std::string MimeType() const;
+
+ // ISortable implementation
+ void ToSortable(SortItem& sortable, Field field) const override;
+
+ /*!
+ * @return Storage id for this channel in CPVRChannelGroup
+ */
+ std::pair<int, int> StorageId() const { return std::make_pair(m_iClientId, m_iUniqueId); }
+
+ /*!
+ * @brief Return true if this channel is encrypted.
+ *
+ * Return true if this channel is encrypted. Does not inform whether XBMC can play the file.
+ * Decryption should be done by the client.
+ *
+ * @return Return true if this channel is encrypted.
+ */
+ bool IsEncrypted() const;
+
+ /*!
+ * @brief Return the encryption system ID for this channel. 0 for FTA.
+ *
+ * Return the encryption system ID for this channel. 0 for FTA.
+ * The values are documented on: http://www.dvb.org/index.php?id=174.
+ *
+ * @return Return the encryption system ID for this channel.
+ */
+ int EncryptionSystem() const;
+
+ /*!
+ * @return A friendly name for the used encryption system.
+ */
+ std::string EncryptionName() const;
+ //@}
+
+ /*! @name EPG methods
+ */
+ //@{
+
+ /*!
+ * @return The ID of the EPG table to use for this channel or -1 if it isn't set.
+ */
+ int EpgID() const;
+
+ /*!
+ * @brief Create the EPG for this channel, if it does not yet exist
+ * @return true if a new epg was created, false otherwise.
+ */
+ bool CreateEPG();
+
+ /*!
+ * @brief Get the EPG table for this channel.
+ * @return The EPG for this channel.
+ */
+ std::shared_ptr<CPVREpg> GetEPG() const;
+
+ /*!
+ * @brief Get the EPG tags for this channel.
+ * @return The tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTags() const;
+
+ /*!
+ * @brief Get all EPG tags for the given time frame, including "gap" tags.
+ * @param timelineStart Start of time line.
+ * @param timelineEnd End of time line.
+ * @param minEventEnd The minimum end time of the events to return.
+ * @param maxEventStart The maximum start time of the events to return.
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEPGTimeline(const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const;
+
+ /*!
+ * @brief Create a "gap" EPG tag.
+ * @param start Start of gap.
+ * @param end End of gap.
+ * @return The tag.
+ */
+ std::shared_ptr<CPVREpgInfoTag> CreateEPGGapTag(const CDateTime& start,
+ const CDateTime& end) const;
+
+ /*!
+ * @brief Get the EPG tag that is now active on this channel.
+ *
+ * Get the EPG tag that is now active on this channel.
+ * Will return an empty tag if there is none.
+ *
+ * @return The EPG tag that is now active.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEPGNow() const;
+
+ /*!
+ * @brief Get the EPG tag that was previously active on this channel.
+ *
+ * Get the EPG tag that was previously active on this channel.
+ * Will return an empty tag if there is none.
+ *
+ * @return The EPG tag that was previously active.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEPGPrevious() const;
+
+ /*!
+ * @brief Get the EPG tag that will be next active on this channel.
+ *
+ * Get the EPG tag that will be next active on this channel.
+ * Will return an empty tag if there is none.
+ *
+ * @return The EPG tag that will be next active.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEPGNext() const;
+
+ /*!
+ * @return Don't use an EPG for this channel if set to false.
+ */
+ bool EPGEnabled() const;
+
+ /*!
+ * @brief Set to true if an EPG should be used for this channel. Set to false otherwise.
+ * @param bEPGEnabled The new value.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetEPGEnabled(bool bEPGEnabled);
+
+ /*!
+ * @brief Get the name of the scraper to be used for this channel.
+ *
+ * Get the name of the scraper to be used for this channel.
+ * The default is 'client', which means the EPG should be loaded from the backend.
+ *
+ * @return The name of the scraper to be used for this channel.
+ */
+ std::string EPGScraper() const;
+
+ /*!
+ * @brief Set the name of the scraper to be used for this channel.
+ *
+ * Set the name of the scraper to be used for this channel.
+ * Set to "client" to load the EPG from the backend
+ *
+ * @param strScraper The new scraper name.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetEPGScraper(const std::string& strScraper);
+
+ bool CanRecord() const;
+
+ static std::string GetEncryptionName(int iCaid);
+
+ /*!
+ * @brief Get the client order for this channel
+ * @return iOrder The order for this channel
+ */
+ int ClientOrder() const { return m_iClientOrder; }
+
+ /*!
+ * @brief Get the client provider Uid for this channel
+ * @return m_iClientProviderUid The provider Uid for this channel
+ */
+ int ClientProviderUid() const { return m_iClientProviderUid; }
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+
+ /*!
+ * @brief Lock the instance. No other thread gets access to this channel until Unlock was called.
+ */
+ void Lock() { m_critSection.lock(); }
+
+ /*!
+ * @brief Unlock the instance. Other threads may get access to this channel again.
+ */
+ void Unlock() { m_critSection.unlock(); }
+
+ /*!
+ * @brief Get the default provider of this channel. The default
+ * provider represents the PVR add-on itself.
+ * @return The default provider of this channel
+ */
+ std::shared_ptr<CPVRProvider> GetDefaultProvider() const;
+
+ /*!
+ * @brief Whether or not this channel has a provider set by the client.
+ * @return True if a provider was set by the client, false otherwise.
+ */
+ bool HasClientProvider() const;
+
+ /*!
+ * @brief Get the provider of this channel. This may be the default provider or a
+ * custom provider set by the client. If @ref "HasClientProvider()" returns true
+ * the provider will be custom from the client, otherwise the default provider.
+ * @return The provider of this channel
+ */
+ std::shared_ptr<CPVRProvider> GetProvider() const;
+
+ //@}
+private:
+ CPVRChannel() = delete;
+ CPVRChannel(const CPVRChannel& tag) = delete;
+ CPVRChannel& operator=(const CPVRChannel& channel) = delete;
+
+ /*!
+ * @brief Update the encryption name after SetEncryptionSystem() has been called.
+ */
+ void UpdateEncryptionName();
+
+ /*!
+ * @brief Reset the EPG instance pointer.
+ */
+ void ResetEPG();
+
+ /*!
+ * @brief Set the client provider Uid for this channel
+ * @param iClientProviderUid The provider Uid for this channel
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetClientProviderUid(int iClientProviderUid);
+
+ /*! @name Kodi related channel data
+ */
+ //@{
+ int m_iChannelId = -1; /*!< the identifier given to this channel by the TV database */
+ bool m_bIsRadio = false; /*!< true if this channel is a radio channel, false if not */
+ bool m_bIsHidden = false; /*!< true if this channel is hidden, false if not */
+ bool m_bIsUserSetName = false; /*!< true if user set the channel name via GUI, false if not */
+ bool m_bIsUserSetIcon = false; /*!< true if user set the icon via GUI, false if not */
+ bool m_bIsUserSetHidden = false; /*!< true if user set the hidden flag via GUI, false if not */
+ bool m_bIsLocked = false; /*!< true if channel is locked, false if not */
+ CPVRCachedImage m_iconPath; /*!< the path to the icon for this channel */
+ std::string m_strChannelName; /*!< the name for this channel used by Kodi */
+ time_t m_iLastWatched = 0; /*!< last time channel has been watched */
+ bool m_bChanged =
+ false; /*!< true if anything in this entry was changed that needs to be persisted */
+ std::shared_ptr<CPVRRadioRDSInfoTag>
+ m_rdsTag; /*! < the radio rds data, if available for the channel. */
+ bool m_bHasArchive = false; /*!< true if this channel supports archive */
+ //@}
+
+ /*! @name EPG related channel data
+ */
+ //@{
+ int m_iEpgId = -1; /*!< the id of the EPG for this channel */
+ bool m_bEPGEnabled = false; /*!< don't use an EPG for this channel if set to false */
+ std::string m_strEPGScraper =
+ "client"; /*!< the name of the scraper to be used for this channel */
+ std::shared_ptr<CPVREpg> m_epg;
+ //@}
+
+ /*! @name Client related channel data
+ */
+ //@{
+ int m_iUniqueId = -1; /*!< the unique identifier for this channel */
+ int m_iClientId = -1; /*!< the identifier of the client that serves this channel */
+ CPVRChannelNumber m_clientChannelNumber; /*!< the channel number on the client */
+ std::string m_strClientChannelName; /*!< the name of this channel on the client */
+ std::string
+ m_strMimeType; /*!< the stream input type based mime type, see @ref https://www.iana.org/assignments/media-types/media-types.xhtml#video */
+ int m_iClientEncryptionSystem =
+ -1; /*!< the encryption system used by this channel. 0 for FreeToAir, -1 for unknown */
+ std::string
+ m_strClientEncryptionName; /*!< the name of the encryption system used by this channel */
+ int m_iClientOrder = 0; /*!< the order from this channels group member */
+ int m_iClientProviderUid =
+ PVR_PROVIDER_INVALID_UID; /*!< the unique id for this provider from the client */
+ //@}
+
+ mutable CCriticalSection m_critSection;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/channels/PVRChannelGroup.cpp b/xbmc/pvr/channels/PVRChannelGroup.cpp
new file mode 100644
index 0000000..7f03430
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroup.cpp
@@ -0,0 +1,1176 @@
+/*
+ * 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.
+ */
+
+//! @todo use Observable here, so we can use event driven operations later
+
+#include "PVRChannelGroup.h"
+
+#include "ServiceBroker.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channel_groups.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+CPVRChannelGroup::CPVRChannelGroup(const CPVRChannelsPath& path,
+ const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup)
+ : m_allChannelsGroup(allChannelsGroup), m_path(path)
+{
+ GetSettings()->RegisterCallback(this);
+}
+
+CPVRChannelGroup::CPVRChannelGroup(const PVR_CHANNEL_GROUP& group,
+ const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup)
+ : m_iPosition(group.iPosition),
+ m_allChannelsGroup(allChannelsGroup),
+ m_path(group.bIsRadio, group.strGroupName)
+{
+ GetSettings()->RegisterCallback(this);
+}
+
+CPVRChannelGroup::~CPVRChannelGroup()
+{
+ GetSettings()->UnregisterCallback(this);
+}
+
+bool CPVRChannelGroup::operator==(const CPVRChannelGroup& right) const
+{
+ return (m_iGroupType == right.m_iGroupType && m_iGroupId == right.m_iGroupId &&
+ m_iPosition == right.m_iPosition && m_path == right.m_path);
+}
+
+bool CPVRChannelGroup::operator!=(const CPVRChannelGroup& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRChannelGroup::FillAddonData(PVR_CHANNEL_GROUP& group) const
+{
+ group = {};
+ group.bIsRadio = IsRadio();
+ strncpy(group.strGroupName, GroupName().c_str(), sizeof(group.strGroupName) - 1);
+ group.iPosition = GetPosition();
+}
+
+CCriticalSection CPVRChannelGroup::m_settingsSingletonCritSection;
+std::weak_ptr<CPVRChannelGroupSettings> CPVRChannelGroup::m_settingsSingleton;
+
+std::shared_ptr<CPVRChannelGroupSettings> CPVRChannelGroup::GetSettings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_settings)
+ {
+ std::unique_lock<CCriticalSection> singletonLock(m_settingsSingletonCritSection);
+ const std::shared_ptr<CPVRChannelGroupSettings> settings = m_settingsSingleton.lock();
+ if (settings)
+ {
+ m_settings = settings;
+ }
+ else
+ {
+ m_settings = std::make_shared<CPVRChannelGroupSettings>();
+ m_settingsSingleton = m_settings;
+ }
+ }
+ return m_settings;
+}
+
+bool CPVRChannelGroup::LoadFromDatabase(
+ const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ const int iChannelCount = m_iGroupId > 0 ? LoadFromDatabase(clients) : 0;
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Fetched {} {} group members from the database for group '{}'",
+ iChannelCount, IsRadio() ? "radio" : "TV", GroupName());
+
+ for (const auto& groupMember : m_members)
+ {
+ if (groupMember.second->Channel())
+ continue;
+
+ auto channel = channels.find(groupMember.first);
+ if (channel == channels.end())
+ {
+ CLog::Log(LOGERROR, "Cannot find group member '{},{}' in channels!", groupMember.first.first,
+ groupMember.first.second);
+ // No workaround here, please. We need to find and fix the root cause of this case!
+ }
+ groupMember.second->SetChannel((*channel).second);
+ }
+
+ m_bLoaded = true;
+ return true;
+}
+
+void CPVRChannelGroup::Unload()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_sortedMembers.clear();
+ m_members.clear();
+ m_failedClients.clear();
+}
+
+bool CPVRChannelGroup::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ if (GroupType() == PVR_GROUP_TYPE_USER_DEFINED || !GetSettings()->SyncChannelGroups())
+ return true;
+
+ // get the channel group members from the backends.
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers;
+ CServiceBroker::GetPVRManager().Clients()->GetChannelGroupMembers(clients, this, groupMembers,
+ m_failedClients);
+ return UpdateGroupEntries(groupMembers);
+}
+
+const CPVRChannelsPath& CPVRChannelGroup::GetPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_path;
+}
+
+void CPVRChannelGroup::SetPath(const CPVRChannelsPath& path)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_path != path)
+ {
+ m_path = path;
+ if (m_bLoaded)
+ {
+ // note: path contains both the radio flag and the group name, which are stored in the db
+ m_bChanged = true;
+ Persist(); //! @todo why must we persist immediately?
+ }
+ }
+}
+
+bool CPVRChannelGroup::SetChannelNumber(const std::shared_ptr<CPVRChannel>& channel,
+ const CPVRChannelNumber& channelNumber)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it =
+ std::find_if(m_sortedMembers.cbegin(), m_sortedMembers.cend(),
+ [&channel](const auto& member) { return *member->Channel() == *channel; });
+
+ if (it != m_sortedMembers.cend() && (*it)->ChannelNumber() != channelNumber)
+ {
+ (*it)->SetChannelNumber(channelNumber);
+ return true;
+ }
+
+ return false;
+}
+
+/********** sort methods **********/
+
+struct sortByClientChannelNumber
+{
+ bool operator()(const std::shared_ptr<CPVRChannelGroupMember>& channel1,
+ const std::shared_ptr<CPVRChannelGroupMember>& channel2) const
+ {
+ if (channel1->ClientPriority() == channel2->ClientPriority())
+ {
+ if (channel1->ClientChannelNumber() == channel2->ClientChannelNumber())
+ return channel1->Channel()->ChannelName() < channel2->Channel()->ChannelName();
+
+ return channel1->ClientChannelNumber() < channel2->ClientChannelNumber();
+ }
+ return channel1->ClientPriority() > channel2->ClientPriority();
+ }
+};
+
+struct sortByChannelNumber
+{
+ bool operator()(const std::shared_ptr<CPVRChannelGroupMember>& channel1,
+ const std::shared_ptr<CPVRChannelGroupMember>& channel2) const
+ {
+ return channel1->ChannelNumber() < channel2->ChannelNumber();
+ }
+};
+
+void CPVRChannelGroup::Sort()
+{
+ if (GetSettings()->UseBackendChannelOrder())
+ SortByClientChannelNumber();
+ else
+ SortByChannelNumber();
+}
+
+bool CPVRChannelGroup::SortAndRenumber()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ Sort();
+ return Renumber();
+}
+
+void CPVRChannelGroup::SortByClientChannelNumber()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::sort(m_sortedMembers.begin(), m_sortedMembers.end(), sortByClientChannelNumber());
+}
+
+void CPVRChannelGroup::SortByChannelNumber()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::sort(m_sortedMembers.begin(), m_sortedMembers.end(), sortByChannelNumber());
+}
+
+bool CPVRChannelGroup::UpdateClientPriorities()
+{
+ const std::shared_ptr<CPVRClients> clients = CServiceBroker::GetPVRManager().Clients();
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const bool bUseBackendChannelOrder = GetSettings()->UseBackendChannelOrder();
+ for (auto& member : m_sortedMembers)
+ {
+ int iNewPriority = 0;
+
+ if (bUseBackendChannelOrder)
+ {
+ const std::shared_ptr<CPVRClient> client =
+ clients->GetCreatedClient(member->Channel()->ClientID());
+ if (!client)
+ continue;
+
+ iNewPriority = client->GetPriority();
+ }
+ else
+ {
+ iNewPriority = 0;
+ }
+
+ bChanged |= (member->ClientPriority() != iNewPriority);
+ member->SetClientPriority(iNewPriority);
+ }
+
+ return bChanged;
+}
+
+/********** getters **********/
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetByUniqueID(
+ const std::pair<int, int>& id) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = m_members.find(id);
+ return it != m_members.end() ? it->second : std::shared_ptr<CPVRChannelGroupMember>();
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroup::GetByUniqueID(int iUniqueChannelId,
+ int iClientID) const
+{
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ GetByUniqueID(std::make_pair(iClientID, iUniqueChannelId));
+ return groupMember ? groupMember->Channel() : std::shared_ptr<CPVRChannel>();
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroup::GetByChannelID(int iChannelID) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it =
+ std::find_if(m_members.cbegin(), m_members.cend(), [iChannelID](const auto& member) {
+ return member.second->Channel()->ChannelID() == iChannelID;
+ });
+ return it != m_members.cend() ? (*it).second->Channel() : std::shared_ptr<CPVRChannel>();
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetLastPlayedChannelGroupMember(
+ int iCurrentChannel /* = -1 */) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ std::shared_ptr<CPVRChannelGroupMember> groupMember;
+ for (const auto& memberPair : m_members)
+ {
+ const std::shared_ptr<CPVRChannel> channel = memberPair.second->Channel();
+ if (channel->ChannelID() != iCurrentChannel &&
+ CServiceBroker::GetPVRManager().Clients()->IsCreatedClient(channel->ClientID()) &&
+ channel->LastWatched() > 0 &&
+ (!groupMember || channel->LastWatched() > groupMember->Channel()->LastWatched()))
+ {
+ groupMember = memberPair.second;
+ }
+ }
+
+ return groupMember;
+}
+
+GroupMemberPair CPVRChannelGroup::GetLastAndPreviousToLastPlayedChannelGroupMember() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_sortedMembers.empty())
+ return {};
+
+ auto members = m_sortedMembers;
+ lock.unlock();
+
+ std::sort(members.begin(), members.end(), [](const auto& a, const auto& b) {
+ return a->Channel()->LastWatched() > b->Channel()->LastWatched();
+ });
+
+ std::shared_ptr<CPVRChannelGroupMember> last;
+ std::shared_ptr<CPVRChannelGroupMember> previousToLast;
+ if (members[0]->Channel()->LastWatched())
+ {
+ last = members[0];
+ if (members.size() > 1 && members[1]->Channel()->LastWatched())
+ previousToLast = members[1];
+ }
+
+ return {last, previousToLast};
+}
+
+CPVRChannelNumber CPVRChannelGroup::GetChannelNumber(
+ const std::shared_ptr<CPVRChannel>& channel) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRChannelGroupMember> member = GetByUniqueID(channel->StorageId());
+ return member ? member->ChannelNumber() : CPVRChannelNumber();
+}
+
+CPVRChannelNumber CPVRChannelGroup::GetClientChannelNumber(
+ const std::shared_ptr<CPVRChannel>& channel) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRChannelGroupMember> member = GetByUniqueID(channel->StorageId());
+ return member ? member->ClientChannelNumber() : CPVRChannelNumber();
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetByChannelNumber(
+ const CPVRChannelNumber& channelNumber) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const bool bUseBackendChannelNumbers = GetSettings()->UseBackendChannelNumbers();
+ for (const auto& member : m_sortedMembers)
+ {
+ CPVRChannelNumber activeChannelNumber =
+ bUseBackendChannelNumbers ? member->ClientChannelNumber() : member->ChannelNumber();
+ if (activeChannelNumber == channelNumber)
+ return member;
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetNextChannelGroupMember(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const
+{
+ std::shared_ptr<CPVRChannelGroupMember> nextMember;
+
+ if (groupMember)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto it = m_sortedMembers.cbegin(); !nextMember && it != m_sortedMembers.cend(); ++it)
+ {
+ if (*it == groupMember)
+ {
+ do
+ {
+ if ((++it) == m_sortedMembers.cend())
+ it = m_sortedMembers.cbegin();
+ if ((*it)->Channel() && !(*it)->Channel()->IsHidden())
+ nextMember = *it;
+ } while (!nextMember && *it != groupMember);
+
+ break;
+ }
+ }
+ }
+
+ return nextMember;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroup::GetPreviousChannelGroupMember(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const
+{
+ std::shared_ptr<CPVRChannelGroupMember> previousMember;
+
+ if (groupMember)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto it = m_sortedMembers.crbegin(); !previousMember && it != m_sortedMembers.crend();
+ ++it)
+ {
+ if (*it == groupMember)
+ {
+ do
+ {
+ if ((++it) == m_sortedMembers.crend())
+ it = m_sortedMembers.crbegin();
+ if ((*it)->Channel() && !(*it)->Channel()->IsHidden())
+ previousMember = *it;
+ } while (!previousMember && *it != groupMember);
+
+ break;
+ }
+ }
+ }
+ return previousMember;
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRChannelGroup::GetMembers(
+ Include eFilter /* = Include::ALL */) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (eFilter == Include::ALL)
+ return m_sortedMembers;
+
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> members;
+ for (const auto& member : m_sortedMembers)
+ {
+ switch (eFilter)
+ {
+ case Include::ONLY_HIDDEN:
+ if (!member->Channel()->IsHidden())
+ continue;
+ break;
+ case Include::ONLY_VISIBLE:
+ if (member->Channel()->IsHidden())
+ continue;
+ break;
+ default:
+ break;
+ }
+
+ members.emplace_back(member);
+ }
+
+ return members;
+}
+
+void CPVRChannelGroup::GetChannelNumbers(std::vector<std::string>& channelNumbers) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const bool bUseBackendChannelNumbers = GetSettings()->UseBackendChannelNumbers();
+ for (const auto& member : m_sortedMembers)
+ {
+ CPVRChannelNumber activeChannelNumber =
+ bUseBackendChannelNumbers ? member->ClientChannelNumber() : member->ChannelNumber();
+ channelNumbers.emplace_back(activeChannelNumber.FormattedChannelNumber());
+ }
+}
+
+int CPVRChannelGroup::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase());
+ if (!database)
+ return -1;
+
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> results =
+ database->Get(*this, clients);
+
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> membersToDelete;
+ if (!results.empty())
+ {
+ const std::shared_ptr<CPVRClients> allClients = CServiceBroker::GetPVRManager().Clients();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& member : results)
+ {
+ // Consistency check.
+ if (member->ClientID() > 0 && member->ChannelUID() > 0 && member->IsRadio() == IsRadio())
+ {
+ // Ignore data from unknown/disabled clients
+ if (allClients->IsEnabledClient(member->ClientID()))
+ {
+ m_sortedMembers.emplace_back(member);
+ m_members.emplace(std::make_pair(member->ClientID(), member->ChannelUID()), member);
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGWARNING,
+ "Skipping member with channel database id {} of {} channel group '{}'. "
+ "Channel not found in the database or radio flag changed.",
+ member->ChannelDatabaseID(), IsRadio() ? "radio" : "TV", GroupName());
+ membersToDelete.emplace_back(member);
+ }
+ }
+
+ SortByChannelNumber();
+ }
+
+ DeleteGroupMembersFromDb(membersToDelete);
+
+ return results.size() - membersToDelete.size();
+}
+
+void CPVRChannelGroup::DeleteGroupMembersFromDb(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& membersToDelete)
+{
+ if (!membersToDelete.empty())
+ {
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No TV database");
+ return;
+ }
+
+ // Note: We must lock the db the whole time, otherwise races may occur.
+ database->Lock();
+
+ bool commitPending = false;
+
+ for (const auto& member : membersToDelete)
+ {
+ commitPending |= member->QueueDelete();
+
+ size_t queryCount = database->GetDeleteQueriesCount();
+ if (queryCount > CHANNEL_COMMIT_QUERY_COUNT_LIMIT)
+ database->CommitDeleteQueries();
+ }
+
+ if (commitPending)
+ database->CommitDeleteQueries();
+
+ database->Unlock();
+ }
+}
+
+bool CPVRChannelGroup::UpdateFromClient(const std::shared_ptr<CPVRChannelGroupMember>& groupMember)
+{
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::shared_ptr<CPVRChannel> channel = groupMember->Channel();
+ const std::shared_ptr<CPVRChannelGroupMember> existingMember =
+ GetByUniqueID(channel->StorageId());
+ if (existingMember)
+ {
+ // update existing channel
+ if (IsInternalGroup() && existingMember->Channel()->UpdateFromClient(channel))
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updated {} channel '{}' from PVR client",
+ IsRadio() ? "radio" : "TV", channel->ChannelName());
+ bChanged = true;
+ }
+
+ existingMember->SetClientChannelNumber(channel->ClientChannelNumber());
+ existingMember->SetOrder(groupMember->Order());
+
+ if (existingMember->NeedsSave())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updated {} channel group member '{}' in group '{}'",
+ IsRadio() ? "radio" : "TV", channel->ChannelName(), GroupName());
+ bChanged = true;
+ }
+ }
+ else
+ {
+ if (groupMember->GroupID() == -1)
+ groupMember->SetGroupID(GroupID());
+
+ m_sortedMembers.emplace_back(groupMember);
+ m_members.emplace(channel->StorageId(), groupMember);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Added {} channel group member '{}' to group '{}'",
+ IsRadio() ? "radio" : "TV", channel->ChannelName(), GroupName());
+
+ // create EPG for new channel
+ if (IsInternalGroup() && channel->CreateEPG())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Created EPG for {} channel '{}' from PVR client",
+ IsRadio() ? "radio" : "TV", channel->ChannelName());
+ }
+
+ bChanged = true;
+ }
+
+ return bChanged;
+}
+
+bool CPVRChannelGroup::AddAndUpdateGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers)
+{
+ return std::accumulate(groupMembers.cbegin(), groupMembers.cend(), false,
+ [this](bool changed, const auto& groupMember) {
+ return UpdateFromClient(groupMember) ? true : changed;
+ });
+}
+
+bool CPVRChannelGroup::HasValidDataForClient(int iClientId) const
+{
+ return std::find(m_failedClients.begin(), m_failedClients.end(), iClientId) ==
+ m_failedClients.end();
+}
+
+bool CPVRChannelGroup::HasValidDataForClients(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients) const
+{
+ return m_failedClients.empty() || std::none_of(clients.cbegin(), clients.cend(),
+ [this](const std::shared_ptr<CPVRClient>& client) {
+ return !HasValidDataForClient(client->GetID());
+ });
+}
+
+bool CPVRChannelGroup::UpdateChannelNumbersFromAllChannelsGroup()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bool bChanged = false;
+
+ if (!IsInternalGroup())
+ {
+ // If we don't sync channel groups make sure the channel numbers are set from
+ // the all channels group using the non default renumber call before sorting
+ if (Renumber(IGNORE_NUMBERING_FROM_ONE) || SortAndRenumber())
+ bChanged = true;
+ }
+
+ m_events.Publish(IsInternalGroup() || bChanged ? PVREvent::ChannelGroupInvalidated
+ : PVREvent::ChannelGroup);
+
+ return bChanged;
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRChannelGroup::RemoveDeletedGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers)
+{
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> membersToRemove;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // put group members into map to speedup the following lookups
+ std::map<std::pair<int, int>, std::shared_ptr<CPVRChannelGroupMember>> membersMap;
+ std::transform(groupMembers.begin(), groupMembers.end(),
+ std::inserter(membersMap, membersMap.end()),
+ [](const std::shared_ptr<CPVRChannelGroupMember>& member) {
+ return std::make_pair(member->Channel()->StorageId(), member);
+ });
+
+ // check for deleted channels
+ for (auto it = m_sortedMembers.begin(); it != m_sortedMembers.end();)
+ {
+ const std::shared_ptr<CPVRChannel> channel = (*it)->Channel();
+ auto mapIt = membersMap.find(channel->StorageId());
+ if (mapIt == membersMap.end())
+ {
+ if (HasValidDataForClient(channel->ClientID()))
+ {
+ CLog::Log(LOGINFO, "Removed stale {} channel '{}' from group '{}'",
+ IsRadio() ? "radio" : "TV", channel->ChannelName(), GroupName());
+ membersToRemove.emplace_back(*it);
+
+ m_members.erase(channel->StorageId());
+ it = m_sortedMembers.erase(it);
+ continue;
+ }
+ }
+ else
+ {
+ membersMap.erase(mapIt);
+ }
+ ++it;
+ }
+
+ DeleteGroupMembersFromDb(membersToRemove);
+
+ return membersToRemove;
+}
+
+bool CPVRChannelGroup::UpdateGroupEntries(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers)
+{
+ bool bReturn = false;
+ bool bChanged = false;
+ bool bRemoved = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bRemoved = !RemoveDeletedGroupMembers(groupMembers).empty();
+ bChanged = AddAndUpdateGroupMembers(groupMembers) || bRemoved;
+ bChanged |= UpdateClientPriorities();
+
+ if (bChanged)
+ {
+ // renumber to make sure all group members have a channel number. New members were added at the
+ // back, so they'll get the highest numbers
+ bool bRenumbered = SortAndRenumber();
+ bReturn = Persist();
+ m_events.Publish(HasNewChannels() || bRemoved || bRenumbered ? PVREvent::ChannelGroupInvalidated
+ : PVREvent::ChannelGroup);
+ }
+ else
+ {
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CPVRChannelGroup::RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel)
+{
+ bool bReturn = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (auto it = m_sortedMembers.begin(); it != m_sortedMembers.end(); ++it)
+ {
+ const auto storageId = (*it)->Channel()->StorageId();
+ if (channel->StorageId() == storageId)
+ {
+ m_members.erase(storageId);
+ m_sortedMembers.erase(it);
+ bReturn = true;
+ break;
+ }
+ }
+
+ // no need to renumber if nothing was removed
+ if (bReturn)
+ Renumber();
+
+ return bReturn;
+}
+
+bool CPVRChannelGroup::AppendToGroup(const std::shared_ptr<CPVRChannel>& channel)
+{
+ bool bReturn = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!CPVRChannelGroup::IsGroupMember(channel))
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> allGroupMember =
+ m_allChannelsGroup->GetByUniqueID(channel->StorageId());
+
+ if (allGroupMember)
+ {
+ unsigned int channelNumberMax =
+ std::accumulate(m_sortedMembers.cbegin(), m_sortedMembers.cend(), 0,
+ [](unsigned int last, const auto& member) {
+ return (member->ChannelNumber().GetChannelNumber() > last)
+ ? member->ChannelNumber().GetChannelNumber()
+ : last;
+ });
+
+ const auto newMember = std::make_shared<CPVRChannelGroupMember>(GroupID(), GroupName(),
+ allGroupMember->Channel());
+ newMember->SetChannelNumber(CPVRChannelNumber(channelNumberMax + 1, 0));
+ newMember->SetClientPriority(allGroupMember->ClientPriority());
+
+ m_sortedMembers.emplace_back(newMember);
+ m_members.emplace(allGroupMember->Channel()->StorageId(), newMember);
+
+ SortAndRenumber();
+ bReturn = true;
+ }
+ }
+ return bReturn;
+}
+
+bool CPVRChannelGroup::IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_members.find(channel->StorageId()) != m_members.end();
+}
+
+bool CPVRChannelGroup::Persist()
+{
+ bool bReturn(true);
+ const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // do not persist if the group is not fully loaded and was saved before.
+ if (!m_bLoaded && m_iGroupId != INVALID_GROUP_ID)
+ return bReturn;
+
+ // Mark newly created groups as loaded so future updates will also be persisted...
+ if (m_iGroupId == INVALID_GROUP_ID)
+ m_bLoaded = true;
+
+ if (database)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting channel group '{}' with {} channels", GroupName(),
+ static_cast<int>(m_members.size()));
+
+ bReturn = database->Persist(*this);
+ m_bChanged = false;
+ }
+ else
+ {
+ bReturn = false;
+ }
+
+ return bReturn;
+}
+
+void CPVRChannelGroup::Delete()
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No TV database");
+ return;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iGroupId > 0)
+ {
+ if (database->Delete(*this))
+ m_bDeleted = true;
+ }
+}
+
+bool CPVRChannelGroup::Renumber(RenumberMode mode /* = NORMAL */)
+{
+ bool bReturn(false);
+ unsigned int iChannelNumber(0);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const bool bUseBackendChannelNumbers = GetSettings()->UseBackendChannelNumbers();
+ const bool bStartGroupChannelNumbersFromOne = GetSettings()->StartGroupChannelNumbersFromOne();
+
+ CPVRChannelNumber currentChannelNumber;
+ CPVRChannelNumber currentClientChannelNumber;
+ for (auto& sortedMember : m_sortedMembers)
+ {
+ currentClientChannelNumber = sortedMember->ClientChannelNumber();
+ if (m_allChannelsGroup && !currentClientChannelNumber.IsValid())
+ currentClientChannelNumber =
+ m_allChannelsGroup->GetClientChannelNumber(sortedMember->Channel());
+
+ if (bUseBackendChannelNumbers)
+ {
+ currentChannelNumber = currentClientChannelNumber;
+ }
+ else if (sortedMember->Channel()->IsHidden())
+ {
+ currentChannelNumber = CPVRChannelNumber(0, 0);
+ }
+ else
+ {
+ if (IsInternalGroup())
+ {
+ currentChannelNumber = CPVRChannelNumber(++iChannelNumber, 0);
+ }
+ else
+ {
+ if (bStartGroupChannelNumbersFromOne && mode != IGNORE_NUMBERING_FROM_ONE)
+ currentChannelNumber = CPVRChannelNumber(++iChannelNumber, 0);
+ else
+ currentChannelNumber = m_allChannelsGroup->GetChannelNumber(sortedMember->Channel());
+ }
+ }
+
+ if (sortedMember->ChannelNumber() != currentChannelNumber ||
+ sortedMember->ClientChannelNumber() != currentClientChannelNumber)
+ {
+ bReturn = true;
+ sortedMember->SetChannelNumber(currentChannelNumber);
+ sortedMember->SetClientChannelNumber(currentClientChannelNumber);
+ }
+ }
+
+ if (bReturn)
+ Sort();
+
+ return bReturn;
+}
+
+bool CPVRChannelGroup::HasNewChannels() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::any_of(m_members.cbegin(), m_members.cend(),
+ [](const auto& member) { return member.second->Channel()->ChannelID() <= 0; });
+}
+
+bool CPVRChannelGroup::HasChanges() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bChanged;
+}
+
+bool CPVRChannelGroup::IsNew() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iGroupId <= 0;
+}
+
+void CPVRChannelGroup::UseBackendChannelOrderChanged()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ UpdateClientPriorities();
+ OnSettingChanged();
+}
+
+void CPVRChannelGroup::UseBackendChannelNumbersChanged()
+{
+ OnSettingChanged();
+}
+
+void CPVRChannelGroup::StartGroupChannelNumbersFromOneChanged()
+{
+ OnSettingChanged();
+}
+
+void CPVRChannelGroup::OnSettingChanged()
+{
+ //! @todo while pvr manager is starting up do accept setting changes.
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ {
+ CLog::Log(LOGWARNING, "Channel group setting change ignored while PVR Manager is starting");
+ return;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR,
+ "Renumbering channel group '{}' to use the backend channel order and/or numbers",
+ GroupName());
+
+ // If we don't sync channel groups make sure the channel numbers are set from
+ // the all channels group using the non default renumber call before sorting
+ if (!GetSettings()->SyncChannelGroups())
+ Renumber(IGNORE_NUMBERING_FROM_ONE);
+
+ const bool bRenumbered = SortAndRenumber();
+ Persist();
+
+ m_events.Publish(bRenumbered ? PVREvent::ChannelGroupInvalidated : PVREvent::ChannelGroup);
+}
+
+int CPVRChannelGroup::GroupID() const
+{
+ return m_iGroupId;
+}
+
+void CPVRChannelGroup::SetGroupID(int iGroupId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (iGroupId >= 0 && m_iGroupId != iGroupId)
+ {
+ m_iGroupId = iGroupId;
+
+ // propagate the new id to the group members
+ for (const auto& member : m_members)
+ member.second->SetGroupID(iGroupId);
+ }
+}
+
+void CPVRChannelGroup::SetGroupType(int iGroupType)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_iGroupType != iGroupType)
+ {
+ m_iGroupType = iGroupType;
+ if (m_bLoaded)
+ m_bChanged = true;
+ }
+}
+
+int CPVRChannelGroup::GroupType() const
+{
+ return m_iGroupType;
+}
+
+std::string CPVRChannelGroup::GroupName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_path.GetGroupName();
+}
+
+void CPVRChannelGroup::SetGroupName(const std::string& strGroupName)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_path.GetGroupName() != strGroupName)
+ {
+ m_path = CPVRChannelsPath(m_path.IsRadio(), strGroupName);
+ if (m_bLoaded)
+ {
+ m_bChanged = true;
+ Persist(); //! @todo why must we persist immediately?
+ }
+ }
+}
+
+bool CPVRChannelGroup::IsRadio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_path.IsRadio();
+}
+
+time_t CPVRChannelGroup::LastWatched() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iLastWatched;
+}
+
+void CPVRChannelGroup::SetLastWatched(time_t iLastWatched)
+{
+ const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iLastWatched != iLastWatched)
+ {
+ m_iLastWatched = iLastWatched;
+ if (m_bLoaded && database)
+ database->UpdateLastWatched(*this);
+ }
+}
+
+uint64_t CPVRChannelGroup::LastOpened() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iLastOpened;
+}
+
+void CPVRChannelGroup::SetLastOpened(uint64_t iLastOpened)
+{
+ const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iLastOpened != iLastOpened)
+ {
+ m_iLastOpened = iLastOpened;
+ if (m_bLoaded && database)
+ database->UpdateLastOpened(*this);
+ }
+}
+
+bool CPVRChannelGroup::UpdateChannel(const std::pair<int, int>& storageId,
+ const std::string& strChannelName,
+ const std::string& strIconPath,
+ int iEPGSource,
+ int iChannelNumber,
+ bool bHidden,
+ bool bEPGEnabled,
+ bool bParentalLocked,
+ bool bUserSetIcon,
+ bool bUserSetHidden)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* get the real channel from the group */
+ const std::shared_ptr<CPVRChannel> channel = GetByUniqueID(storageId)->Channel();
+ if (!channel)
+ return false;
+
+ channel->SetChannelName(strChannelName, true);
+ channel->SetHidden(bHidden, bUserSetHidden);
+ channel->SetLocked(bParentalLocked);
+ channel->SetIconPath(strIconPath, bUserSetIcon);
+
+ if (iEPGSource == 0)
+ channel->SetEPGScraper("client");
+
+ //! @todo add other scrapers
+ channel->SetEPGEnabled(bEPGEnabled);
+
+ /* set new values in the channel tag */
+ if (bHidden)
+ {
+ // sort or previous changes will be overwritten
+ Sort();
+
+ RemoveFromGroup(channel);
+ }
+ else if (iChannelNumber > 0)
+ {
+ SetChannelNumber(channel, CPVRChannelNumber(iChannelNumber, 0));
+ }
+
+ return true;
+}
+
+size_t CPVRChannelGroup::Size() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_members.size();
+}
+
+bool CPVRChannelGroup::HasChannels() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return !m_members.empty();
+}
+
+bool CPVRChannelGroup::CreateChannelEpgs(bool bForce /* = false */)
+{
+ /* used only by internal channel groups */
+ return true;
+}
+
+bool CPVRChannelGroup::SetHidden(bool bHidden)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bHidden != bHidden)
+ {
+ m_bHidden = bHidden;
+ if (m_bLoaded)
+ m_bChanged = true;
+ }
+
+ return m_bChanged;
+}
+
+bool CPVRChannelGroup::IsHidden() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bHidden;
+}
+
+int CPVRChannelGroup::GetPosition() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iPosition;
+}
+
+void CPVRChannelGroup::SetPosition(int iPosition)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iPosition != iPosition)
+ {
+ m_iPosition = iPosition;
+ if (m_bLoaded)
+ m_bChanged = true;
+ }
+}
+
+int CPVRChannelGroup::CleanupCachedImages()
+{
+ std::vector<std::string> urlsToCheck;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::transform(
+ m_members.cbegin(), m_members.cend(), std::back_inserter(urlsToCheck),
+ [](const auto& groupMember) { return groupMember.second->Channel()->ClientIconPath(); });
+ }
+
+ const std::string owner =
+ StringUtils::Format(CPVRChannel::IMAGE_OWNER_PATTERN, IsRadio() ? "radio" : "tv");
+ return CPVRCachedImages::Cleanup({{owner, ""}}, urlsToCheck);
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroup.h b/xbmc/pvr/channels/PVRChannelGroup.h
new file mode 100644
index 0000000..66c45d6
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroup.h
@@ -0,0 +1,548 @@
+/*
+ * 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 "pvr/channels/PVRChannelGroupSettings.h"
+#include "pvr/channels/PVRChannelNumber.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "utils/EventStream.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+struct PVR_CHANNEL_GROUP;
+
+namespace PVR
+{
+#define PVR_GROUP_TYPE_DEFAULT 0
+#define PVR_GROUP_TYPE_INTERNAL 1
+#define PVR_GROUP_TYPE_USER_DEFINED 2
+
+enum class PVREvent;
+
+class CPVRChannel;
+class CPVRChannelGroupMember;
+class CPVRClient;
+class CPVREpgInfoTag;
+
+enum RenumberMode
+{
+ NORMAL = 0,
+ IGNORE_NUMBERING_FROM_ONE = 1
+};
+
+using GroupMemberPair =
+ std::pair<std::shared_ptr<CPVRChannelGroupMember>, std::shared_ptr<CPVRChannelGroupMember>>;
+
+class CPVRChannelGroup : public IChannelGroupSettingsCallback
+{
+ friend class CPVRDatabase;
+
+public:
+ static const int INVALID_GROUP_ID = -1;
+
+ /*!
+ * @brief Create a new channel group instance.
+ * @param path The channel group path.
+ * @param allChannelsGroup The channel group containing all TV or radio channels.
+ */
+ CPVRChannelGroup(const CPVRChannelsPath& path,
+ const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup);
+
+ /*!
+ * @brief Create a new channel group instance from a channel group provided by an add-on.
+ * @param group The channel group provided by the add-on.
+ * @param allChannelsGroup The channel group containing all TV or radio channels.
+ */
+ CPVRChannelGroup(const PVR_CHANNEL_GROUP& group,
+ const std::shared_ptr<CPVRChannelGroup>& allChannelsGroup);
+
+ ~CPVRChannelGroup() override;
+
+ bool operator==(const CPVRChannelGroup& right) const;
+ bool operator!=(const CPVRChannelGroup& right) const;
+
+ /*!
+ * @brief Copy over data to the given PVR_CHANNEL_GROUP instance.
+ * @param group The group instance to fill.
+ */
+ void FillAddonData(PVR_CHANNEL_GROUP& group) const;
+
+ /*!
+ * @brief Query the events available for CEventStream
+ */
+ CEventStream<PVREvent>& Events() { return m_events; }
+
+ /*!
+ * @brief Load the channels from the database.
+ * @param channels All available channels.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True when loaded successfully, false otherwise.
+ */
+ virtual bool LoadFromDatabase(
+ const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Clear all data.
+ */
+ virtual void Unload();
+
+ /*!
+ * @return The amount of group members
+ */
+ size_t Size() const;
+
+ /*!
+ * @brief Update data with channel group members from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @return True on success, false otherwise.
+ */
+ virtual bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Get the path of this group.
+ * @return the path.
+ */
+ const CPVRChannelsPath& GetPath() const;
+
+ /*!
+ * @brief Set the path of this group.
+ * @param the path.
+ */
+ void SetPath(const CPVRChannelsPath& path);
+
+ /*!
+ * @brief Change the channelnumber of a group. Used by CGUIDialogPVRChannelManager.
+ * Call SortByChannelNumber() and Renumber() after all changes are done.
+ * @param channel The channel to change the channel number for.
+ * @param channelNumber The new channel number.
+ */
+ bool SetChannelNumber(const std::shared_ptr<CPVRChannel>& channel,
+ const CPVRChannelNumber& channelNumber);
+
+ /*!
+ * @brief Remove a channel from this container.
+ * @param channel The channel to remove.
+ * @return True if the channel was found and removed, false otherwise.
+ */
+ virtual bool RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Append a channel to this container.
+ * @param channel The channel to append.
+ * @return True if the channel was appended, false otherwise.
+ */
+ virtual bool AppendToGroup(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Change the name of this group.
+ * @param strGroupName The new group name.
+ */
+ void SetGroupName(const std::string& strGroupName);
+
+ /*!
+ * @brief Persist changed or new data.
+ * @return True if the channel was persisted, false otherwise.
+ */
+ bool Persist();
+
+ /*!
+ * @brief Check whether a channel is in this container.
+ * @param channel The channel to find.
+ * @return True if the channel was found, false otherwise.
+ */
+ virtual bool IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Check if this group is the internal group containing all channels.
+ * @return True if it's the internal group, false otherwise.
+ */
+ bool IsInternalGroup() const { return m_iGroupType == PVR_GROUP_TYPE_INTERNAL; }
+
+ /*!
+ * @brief True if this group holds radio channels, false if it holds TV channels.
+ * @return True if this group holds radio channels, false if it holds TV channels.
+ */
+ bool IsRadio() const;
+
+ /*!
+ * @brief The database ID of this group.
+ * @return The database ID of this group.
+ */
+ int GroupID() const;
+
+ /*!
+ * @brief Set the database ID of this group.
+ * @param iGroupId The new database ID.
+ */
+ void SetGroupID(int iGroupId);
+
+ /*!
+ * @brief Set the type of this group.
+ * @param the new type for this group.
+ */
+ void SetGroupType(int iGroupType);
+
+ /*!
+ * @brief Return the type of this group.
+ */
+ int GroupType() const;
+
+ /*!
+ * @return Time group has been watched last.
+ */
+ time_t LastWatched() const;
+
+ /*!
+ * @brief Last time group has been watched
+ * @param iLastWatched The new value.
+ */
+ void SetLastWatched(time_t iLastWatched);
+
+ /*!
+ * @return Time in milliseconds from epoch this group was last opened.
+ */
+ uint64_t LastOpened() const;
+
+ /*!
+ * @brief Set the time in milliseconds from epoch this group was last opened.
+ * @param iLastOpened The new value.
+ */
+ void SetLastOpened(uint64_t iLastOpened);
+
+ /*!
+ * @brief The name of this group.
+ * @return The name of this group.
+ */
+ std::string GroupName() const;
+
+ /*! @name Sort methods
+ */
+ //@{
+
+ /*!
+ * @brief Sort the group.
+ */
+ void Sort();
+
+ /*!
+ * @brief Sort the group and fix up channel numbers.
+ * @return True when numbering changed, false otherwise
+ */
+ bool SortAndRenumber();
+
+ /*!
+ * @brief Remove invalid channels and updates the channel numbers.
+ * @param mode the numbering mode to use
+ * @return True if something changed, false otherwise.
+ */
+ bool Renumber(RenumberMode mode = NORMAL);
+
+ //@}
+
+ // IChannelGroupSettingsCallback implementation
+ void UseBackendChannelOrderChanged() override;
+ void UseBackendChannelNumbersChanged() override;
+ void StartGroupChannelNumbersFromOneChanged() override;
+
+ /*!
+ * @brief Get the channel group member that was played last.
+ * @param iCurrentChannel The channelid of the current channel that is playing, or -1 if none
+ * @return The requested channel group member or nullptr.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetLastPlayedChannelGroupMember(
+ int iCurrentChannel = -1) const;
+
+ /*!
+ * @brief Get the last and previous to last played channel group members.
+ * @return The members. pair.first contains the last, pair.second the previous to last member.
+ */
+ GroupMemberPair GetLastAndPreviousToLastPlayedChannelGroupMember() const;
+
+ /*!
+ * @brief Get a channel group member given it's active channel number
+ * @param channelNumber The channel number.
+ * @return The channel group member or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetByChannelNumber(
+ const CPVRChannelNumber& channelNumber) const;
+
+ /*!
+ * @brief Get the channel number in this group of the given channel.
+ * @param channel The channel to get the channel number for.
+ * @return The channel number in this group.
+ */
+ CPVRChannelNumber GetChannelNumber(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Get the client channel number in this group of the given channel.
+ * @param channel The channel to get the channel number for.
+ * @return The client channel number in this group.
+ */
+ CPVRChannelNumber GetClientChannelNumber(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Get the next channel group member in this group.
+ * @param groupMember The current channel group member.
+ * @return The channel group member or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetNextChannelGroupMember(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const;
+
+ /*!
+ * @brief Get the previous channel group member in this group.
+ * @param groupMember The current channel group member.
+ * @return The channel group member or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetPreviousChannelGroupMember(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember) const;
+
+ /*!
+ * @brief Get a channel given it's channel ID.
+ * @param iChannelID The channel ID.
+ * @return The channel or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannel> GetByChannelID(int iChannelID) const;
+
+ enum class Include
+ {
+ ALL,
+ ONLY_HIDDEN,
+ ONLY_VISIBLE
+ };
+
+ /*!
+ * @brief Get the current members of this group
+ * @param eFilter A filter to apply.
+ * @return The group members
+ */
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> GetMembers(
+ Include eFilter = Include::ALL) const;
+
+ /*!
+ * @brief Get the list of active channel numbers in a group.
+ * @param channelNumbers The list to store the numbers in.
+ */
+ void GetChannelNumbers(std::vector<std::string>& channelNumbers) const;
+
+ /*!
+ * @brief The amount of hidden channels in this container.
+ * @return The amount of hidden channels in this container.
+ */
+ virtual size_t GetNumHiddenChannels() const { return 0; }
+
+ /*!
+ * @brief Does this container holds channels.
+ * @return True if there is at least one channel in this container, otherwise false.
+ */
+ bool HasChannels() const;
+
+ /*!
+ * @return True if there is at least one new channel in this group that hasn't been persisted, false otherwise.
+ */
+ bool HasNewChannels() const;
+
+ /*!
+ * @return True if anything changed in this group that hasn't been persisted, false otherwise.
+ */
+ bool HasChanges() const;
+
+ /*!
+ * @return True if the group was never persisted, false otherwise.
+ */
+ bool IsNew() const;
+
+ /*!
+ * @brief Create an EPG table for each channel.
+ * @brief bForce Create the tables, even if they already have been created before.
+ * @return True if all tables were created successfully, false otherwise.
+ */
+ virtual bool CreateChannelEpgs(bool bForce = false);
+
+ /*!
+ * @brief Update a channel group member with given data.
+ * @param storageId The storage id of the channel.
+ * @param strChannelName The channel name to set.
+ * @param strIconPath The icon path to set.
+ * @param iEPGSource The EPG id.
+ * @param iChannelNumber The channel number to set.
+ * @param bHidden Set/Remove hidden flag for the channel group member identified by storage id.
+ * @param bEPGEnabled Set/Remove EPG enabled flag for the channel group member identified by storage id.
+ * @param bParentalLocked Set/Remove parental locked flag for the channel group member identified by storage id.
+ * @param bUserSetIcon Set/Remove user set icon flag for the channel group member identified by storage id.
+ * @param bUserSetHidden Set/Remove user set hidden flag for the channel group member identified by storage id.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateChannel(const std::pair<int, int>& storageId,
+ const std::string& strChannelName,
+ const std::string& strIconPath,
+ int iEPGSource,
+ int iChannelNumber,
+ bool bHidden,
+ bool bEPGEnabled,
+ bool bParentalLocked,
+ bool bUserSetIcon,
+ bool bUserSetHidden);
+
+ /*!
+ * @brief Get a channel given the channel number on the client.
+ * @param iUniqueChannelId The unique channel id on the client.
+ * @param iClientID The ID of the client.
+ * @return The channel or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannel> GetByUniqueID(int iUniqueChannelId, int iClientID) const;
+
+ /*!
+ * @brief Get a channel group member given its storage id.
+ * @param id The storage id (a pair of client id and unique channel id).
+ * @return The channel group member or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetByUniqueID(const std::pair<int, int>& id) const;
+
+ bool SetHidden(bool bHidden);
+ bool IsHidden() const;
+
+ int GetPosition() const;
+ void SetPosition(int iPosition);
+
+ /*!
+ * @brief Check, whether data for a given pvr client are currently valid. For instance, data
+ * can be invalid because the client's backend was offline when data was last queried.
+ * @param iClientId The id of the client.
+ * @return True, if data is currently valid, false otherwise.
+ */
+ bool HasValidDataForClient(int iClientId) const;
+
+ /*!
+ * @brief Check, whether data for given pvr clients are currently valid. For instance, data
+ * can be invalid because the client's backend was offline when data was last queried.
+ * @param clients The clients to check. Check all active clients if vector is empty.
+ * @return True, if data is currently valid, false otherwise.
+ */
+ bool HasValidDataForClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) const;
+
+ /*!
+ * @brief Update the channel numbers according to the all channels group and publish event.
+ * @return True, if a channel number was changed, false otherwise.
+ */
+ bool UpdateChannelNumbersFromAllChannelsGroup();
+
+ /*!
+ * @brief Remove this group from database.
+ */
+ void Delete();
+
+ /*!
+ * @brief Whether this group is deleted.
+ * @return True, if deleted, false otherwise.
+ */
+ bool IsDeleted() const { return m_bDeleted; }
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+protected:
+ /*!
+ * @brief Remove deleted group members from this group.
+ * @param groupMembers The group members to use to update this list.
+ * @return The removed members .
+ */
+ virtual std::vector<std::shared_ptr<CPVRChannelGroupMember>> RemoveDeletedGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers);
+
+ /*!
+ * @brief Update the current channel group members with the given list.
+ * @param groupMembers The group members to use to update this list.
+ * @return True if everything went well, false otherwise.
+ */
+ bool UpdateGroupEntries(const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers);
+
+ /*!
+ * @brief Sort the current channel list by client channel number.
+ */
+ void SortByClientChannelNumber();
+
+ /*!
+ * @brief Sort the current channel list by channel number.
+ */
+ void SortByChannelNumber();
+
+ /*!
+ * @brief Update the priority for all members of all channel groups.
+ */
+ bool UpdateClientPriorities();
+
+ std::shared_ptr<CPVRChannelGroupSettings> GetSettings() const;
+
+ int m_iGroupType = PVR_GROUP_TYPE_DEFAULT; /*!< The type of this group */
+ int m_iGroupId = INVALID_GROUP_ID; /*!< The ID of this group in the database */
+ bool m_bLoaded = false; /*!< True if this container is loaded, false otherwise */
+ bool m_bChanged =
+ false; /*!< true if anything changed in this group that hasn't been persisted, false otherwise */
+ time_t m_iLastWatched = 0; /*!< last time group has been watched */
+ uint64_t m_iLastOpened = 0; /*!< time in milliseconds from epoch this group was last opened */
+ bool m_bHidden = false; /*!< true if this group is hidden, false otherwise */
+ int m_iPosition = 0; /*!< the position of this group within the group list */
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>>
+ m_sortedMembers; /*!< members sorted by channel number */
+ std::map<std::pair<int, int>, std::shared_ptr<CPVRChannelGroupMember>>
+ m_members; /*!< members with key clientid+uniqueid */
+ mutable CCriticalSection m_critSection;
+ std::vector<int> m_failedClients;
+ CEventSource<PVREvent> m_events;
+ mutable std::shared_ptr<CPVRChannelGroupSettings> m_settings;
+
+ // the settings singleton shared between all group instances
+ static CCriticalSection m_settingsSingletonCritSection;
+ static std::weak_ptr<CPVRChannelGroupSettings> m_settingsSingleton;
+
+private:
+ /*!
+ * @brief Load the channel group members stored in the database.
+ * @param clients The PVR clients to load data for. Leave empty for all clients.
+ * @return The amount of channel group members that were added.
+ */
+ int LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Delete channel group members from database.
+ * @param membersToDelete The channel group members.
+ */
+ void DeleteGroupMembersFromDb(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& membersToDelete);
+
+ /*!
+ * @brief Update this group's data with a channel group member provided by a client.
+ * @param groupMember The updated group member.
+ * @return True if group member data were changed, false otherwise.
+ */
+ bool UpdateFromClient(const std::shared_ptr<CPVRChannelGroupMember>& groupMember);
+
+ /*!
+ * @brief Add new channel group members to this group; update data.
+ * @param groupMembers The group members to use to update this list.
+ * @return True if group member data were changed, false otherwise.
+ */
+ bool AddAndUpdateGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers);
+
+ void OnSettingChanged();
+
+ std::shared_ptr<CPVRChannelGroup> m_allChannelsGroup;
+ CPVRChannelsPath m_path;
+ bool m_bDeleted = false;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/channels/PVRChannelGroupInternal.cpp b/xbmc/pvr/channels/PVRChannelGroupInternal.cpp
new file mode 100644
index 0000000..f79e02f
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupInternal.cpp
@@ -0,0 +1,245 @@
+/*
+ * 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 "PVRChannelGroupInternal.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/epg/EpgContainer.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+CPVRChannelGroupInternal::CPVRChannelGroupInternal(bool bRadio)
+ : CPVRChannelGroup(CPVRChannelsPath(bRadio, g_localizeStrings.Get(19287)), nullptr),
+ m_iHiddenChannels(0)
+{
+ m_iGroupType = PVR_GROUP_TYPE_INTERNAL;
+}
+
+CPVRChannelGroupInternal::CPVRChannelGroupInternal(const CPVRChannelsPath& path)
+ : CPVRChannelGroup(path, nullptr), m_iHiddenChannels(0)
+{
+ m_iGroupType = PVR_GROUP_TYPE_INTERNAL;
+}
+
+CPVRChannelGroupInternal::~CPVRChannelGroupInternal()
+{
+ CServiceBroker::GetPVRManager().Events().Unsubscribe(this);
+}
+
+bool CPVRChannelGroupInternal::LoadFromDatabase(
+ const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ if (CPVRChannelGroup::LoadFromDatabase(channels, clients))
+ {
+ for (const auto& groupMember : m_members)
+ {
+ const std::shared_ptr<CPVRChannel> channel = groupMember.second->Channel();
+
+ // create the EPG for the channel
+ if (channel->CreateEPG())
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Created EPG for {} channel '{}'", IsRadio() ? "radio" : "TV",
+ channel->ChannelName());
+ }
+ }
+
+ UpdateChannelPaths();
+ CServiceBroker::GetPVRManager().Events().Subscribe(this, &CPVRChannelGroupInternal::OnPVRManagerEvent);
+ return true;
+ }
+
+ CLog::LogF(LOGERROR, "Failed to load channels");
+ return false;
+}
+
+void CPVRChannelGroupInternal::Unload()
+{
+ CServiceBroker::GetPVRManager().Events().Unsubscribe(this);
+ CPVRChannelGroup::Unload();
+}
+
+void CPVRChannelGroupInternal::CheckGroupName()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* check whether the group name is still correct, or channels will fail to load after the language setting changed */
+ const std::string& strNewGroupName = g_localizeStrings.Get(19287);
+ if (GroupName() != strNewGroupName)
+ {
+ SetGroupName(strNewGroupName);
+ UpdateChannelPaths();
+ }
+}
+
+void CPVRChannelGroupInternal::UpdateChannelPaths()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iHiddenChannels = 0;
+ for (auto& groupMemberPair : m_members)
+ {
+ if (groupMemberPair.second->Channel()->IsHidden())
+ ++m_iHiddenChannels;
+ else
+ groupMemberPair.second->SetGroupName(GroupName());
+ }
+}
+
+bool CPVRChannelGroupInternal::UpdateFromClients(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ // get the channels from the given clients
+ std::vector<std::shared_ptr<CPVRChannel>> channels;
+ CServiceBroker::GetPVRManager().Clients()->GetChannels(clients, IsRadio(), channels,
+ m_failedClients);
+
+ // create group members for the channels
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers;
+ std::transform(channels.cbegin(), channels.cend(), std::back_inserter(groupMembers),
+ [this](const auto& channel) {
+ return std::make_shared<CPVRChannelGroupMember>(GroupID(), GroupName(), channel);
+ });
+
+ return UpdateGroupEntries(groupMembers);
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroupMember>> CPVRChannelGroupInternal::
+ RemoveDeletedGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers)
+{
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> removedMembers =
+ CPVRChannelGroup::RemoveDeletedGroupMembers(groupMembers);
+ if (!removedMembers.empty())
+ {
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No TV database");
+ }
+ else
+ {
+ std::vector<std::shared_ptr<CPVREpg>> epgsToRemove;
+ for (const auto& member : removedMembers)
+ {
+ const auto channel = member->Channel();
+ const auto epg = channel->GetEPG();
+ if (epg)
+ epgsToRemove.emplace_back(epg);
+
+ // Note: We need to obtain a lock for every channel instance before we can lock
+ // the TV db. This order is important. Otherwise deadlocks may occur.
+ channel->Lock();
+ }
+
+ // Note: We must lock the db the whole time, otherwise races may occur.
+ database->Lock();
+
+ bool commitPending = false;
+
+ for (const auto& member : removedMembers)
+ {
+ // since channel was not found in the internal group, it was deleted from the backend
+
+ const auto channel = member->Channel();
+ commitPending |= channel->QueueDelete();
+ channel->Unlock();
+
+ size_t queryCount = database->GetDeleteQueriesCount();
+ if (queryCount > CHANNEL_COMMIT_QUERY_COUNT_LIMIT)
+ database->CommitDeleteQueries();
+ }
+
+ if (commitPending)
+ database->CommitDeleteQueries();
+
+ database->Unlock();
+
+ // delete the EPG data for the removed channels
+ CServiceBroker::GetPVRManager().EpgContainer().QueueDeleteEpgs(epgsToRemove);
+ }
+ }
+ return removedMembers;
+}
+
+bool CPVRChannelGroupInternal::AppendToGroup(const std::shared_ptr<CPVRChannel>& channel)
+{
+ if (IsGroupMember(channel))
+ return false;
+
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember = GetByUniqueID(channel->StorageId());
+ if (!groupMember)
+ return false;
+
+ channel->SetHidden(false, true);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iHiddenChannels > 0)
+ m_iHiddenChannels--;
+
+ const unsigned int iChannelNumber = m_members.size() - m_iHiddenChannels;
+ groupMember->SetChannelNumber(CPVRChannelNumber(iChannelNumber, 0));
+
+ SortAndRenumber();
+ return true;
+}
+
+bool CPVRChannelGroupInternal::RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel)
+{
+ if (!IsGroupMember(channel))
+ return false;
+
+ channel->SetHidden(true, true);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ ++m_iHiddenChannels;
+
+ SortAndRenumber();
+ return true;
+}
+
+bool CPVRChannelGroupInternal::IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const
+{
+ return !channel->IsHidden();
+}
+
+bool CPVRChannelGroupInternal::CreateChannelEpgs(bool bForce /* = false */)
+{
+ if (!CServiceBroker::GetPVRManager().EpgContainer().IsStarted())
+ return false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto& groupMemberPair : m_members)
+ groupMemberPair.second->Channel()->CreateEPG();
+ }
+
+ return Persist();
+}
+
+void CPVRChannelGroupInternal::OnPVRManagerEvent(const PVR::PVREvent& event)
+{
+ if (event == PVREvent::ManagerStarted)
+ CServiceBroker::GetPVRManager().TriggerEpgsCreate();
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupInternal.h b/xbmc/pvr/channels/PVRChannelGroupInternal.h
new file mode 100644
index 0000000..5f8a06d
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupInternal.h
@@ -0,0 +1,116 @@
+/*
+ * 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 "pvr/channels/PVRChannelGroup.h"
+
+#include <memory>
+#include <vector>
+
+namespace PVR
+{
+ enum class PVREvent;
+
+ class CPVRChannel;
+ class CPVRChannelNumber;
+
+ class CPVRChannelGroupInternal : public CPVRChannelGroup
+ {
+ public:
+ CPVRChannelGroupInternal() = delete;
+
+ /*!
+ * @brief Create a new internal channel group.
+ * @param bRadio True if this group holds radio channels.
+ */
+ explicit CPVRChannelGroupInternal(bool bRadio);
+
+ /*!
+ * @brief Create a new internal channel group.
+ * @param path The path for the new group.
+ */
+ explicit CPVRChannelGroupInternal(const CPVRChannelsPath& path);
+
+ ~CPVRChannelGroupInternal() override;
+
+ /**
+ * @brief The amount of channels in this container.
+ * @return The amount of channels in this container.
+ */
+ size_t GetNumHiddenChannels() const override { return m_iHiddenChannels; }
+
+ /*!
+ * @see CPVRChannelGroup::IsGroupMember
+ */
+ bool IsGroupMember(const std::shared_ptr<CPVRChannel>& channel) const override;
+
+ /*!
+ * @see CPVRChannelGroup::AppendToGroup
+ */
+ bool AppendToGroup(const std::shared_ptr<CPVRChannel>& channel) override;
+
+ /*!
+ * @see CPVRChannelGroup::RemoveFromGroup
+ */
+ bool RemoveFromGroup(const std::shared_ptr<CPVRChannel>& channel) override;
+
+ /*!
+ * @brief Check whether the group name is still correct after the language setting changed.
+ */
+ void CheckGroupName();
+
+ /*!
+ * @brief Create an EPG table for each channel.
+ * @brief bForce Create the tables, even if they already have been created before.
+ * @return True if all tables were created successfully, false otherwise.
+ */
+ bool CreateChannelEpgs(bool bForce = false) override;
+
+ protected:
+ /*!
+ * @brief Remove deleted group members from this group. Delete stale channels.
+ * @param groupMembers The group members to use to update this list.
+ * @return The removed members .
+ */
+ std::vector<std::shared_ptr<CPVRChannelGroupMember>> RemoveDeletedGroupMembers(
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>>& groupMembers) override;
+
+ /*!
+ * @brief Update data with 'all channels' group members from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) override;
+
+ /*!
+ * @brief Load the channels from the database.
+ * @param channels All available channels.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True when loaded successfully, false otherwise.
+ */
+ bool LoadFromDatabase(
+ const std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>>& channels,
+ const std::vector<std::shared_ptr<CPVRClient>>& clients) override;
+
+ /*!
+ * @brief Clear all data.
+ */
+ void Unload() override;
+
+ /*!
+ * @brief Update the vfs paths of all channels.
+ */
+ void UpdateChannelPaths();
+
+ size_t m_iHiddenChannels; /*!< the amount of hidden channels in this container */
+
+ private:
+ void OnPVRManagerEvent(const PVREvent& event);
+ };
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupMember.cpp b/xbmc/pvr/channels/PVRChannelGroupMember.cpp
new file mode 100644
index 0000000..27592b5
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupMember.cpp
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012-2021 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 "PVRChannelGroupMember.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "utils/DatabaseUtils.h"
+#include "utils/SortUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace PVR;
+
+CPVRChannelGroupMember::CPVRChannelGroupMember(const std::string& groupName,
+ int order,
+ const std::shared_ptr<CPVRChannel>& channel)
+ : m_clientChannelNumber(channel->ClientChannelNumber()), m_iOrder(order)
+{
+ SetChannel(channel);
+ SetGroupName(groupName);
+}
+
+CPVRChannelGroupMember::CPVRChannelGroupMember(int iGroupID,
+ const std::string& groupName,
+ const std::shared_ptr<CPVRChannel>& channel)
+ : m_iGroupID(iGroupID),
+ m_clientChannelNumber(channel->ClientChannelNumber()),
+ m_iOrder(channel->ClientOrder())
+{
+ SetChannel(channel);
+ SetGroupName(groupName);
+}
+
+void CPVRChannelGroupMember::Serialize(CVariant& value) const
+{
+ value["channelnumber"] = m_channelNumber.GetChannelNumber();
+ value["subchannelnumber"] = m_channelNumber.GetSubChannelNumber();
+}
+
+void CPVRChannelGroupMember::SetChannel(const std::shared_ptr<CPVRChannel>& channel)
+{
+ // note: no need to set m_bChanged, as these values are not persisted in the db
+ m_channel = channel;
+ m_iClientID = channel->ClientID();
+ m_iChannelUID = channel->UniqueID();
+ m_iChannelDatabaseID = channel->ChannelID();
+ m_bIsRadio = channel->IsRadio();
+}
+
+void CPVRChannelGroupMember::ToSortable(SortItem& sortable, Field field) const
+{
+ if (field == FieldChannelNumber)
+ {
+ sortable[FieldChannelNumber] = m_channelNumber.SortableChannelNumber();
+ }
+ else if (field == FieldClientChannelOrder)
+ {
+ if (m_iOrder)
+ sortable[FieldClientChannelOrder] = m_iOrder;
+ else
+ sortable[FieldClientChannelOrder] = m_clientChannelNumber.SortableChannelNumber();
+ }
+}
+
+void CPVRChannelGroupMember::SetGroupID(int iGroupID)
+{
+ if (m_iGroupID != iGroupID)
+ {
+ m_iGroupID = iGroupID;
+ m_bNeedsSave = true;
+ }
+}
+
+void CPVRChannelGroupMember::SetGroupName(const std::string& groupName)
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientID);
+ if (client)
+ m_path =
+ CPVRChannelsPath(m_bIsRadio, groupName, client->ID(), client->InstanceId(), m_iChannelUID);
+ else
+ CLog::LogF(LOGERROR, "Unable to obtain instance for client id: {}", m_iClientID);
+}
+
+void CPVRChannelGroupMember::SetChannelNumber(const CPVRChannelNumber& channelNumber)
+{
+ if (m_channelNumber != channelNumber)
+ {
+ m_channelNumber = channelNumber;
+ m_bNeedsSave = true;
+ }
+}
+
+void CPVRChannelGroupMember::SetClientChannelNumber(const CPVRChannelNumber& clientChannelNumber)
+{
+ if (m_clientChannelNumber != clientChannelNumber)
+ {
+ m_clientChannelNumber = clientChannelNumber;
+ m_bNeedsSave = true;
+ }
+}
+
+void CPVRChannelGroupMember::SetClientPriority(int iClientPriority)
+{
+ if (m_iClientPriority != iClientPriority)
+ {
+ m_iClientPriority = iClientPriority;
+ // Note: do not set m_bNeedsSave here as priority is not stored in database
+ }
+}
+
+void CPVRChannelGroupMember::SetOrder(int iOrder)
+{
+ if (m_iOrder != iOrder)
+ {
+ m_iOrder = iOrder;
+ m_bNeedsSave = true;
+ }
+}
+
+bool CPVRChannelGroupMember::QueueDelete()
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (!database)
+ return false;
+
+ return database->QueueDeleteQuery(*this);
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupMember.h b/xbmc/pvr/channels/PVRChannelGroupMember.h
new file mode 100644
index 0000000..cb3ac83
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupMember.h
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012-2021 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 "pvr/channels/PVRChannelNumber.h"
+#include "utils/ISerializable.h"
+#include "utils/ISortable.h"
+
+#include <memory>
+#include <string>
+
+namespace PVR
+{
+
+class CPVRChannel;
+
+class CPVRChannelGroupMember : public ISerializable, public ISortable
+{
+ friend class CPVRDatabase;
+
+public:
+ CPVRChannelGroupMember() : m_bNeedsSave(false) {}
+
+ CPVRChannelGroupMember(const std::string& groupName,
+ int order,
+ const std::shared_ptr<CPVRChannel>& channel);
+
+ CPVRChannelGroupMember(int iGroupID,
+ const std::string& groupName,
+ const std::shared_ptr<CPVRChannel>& channel);
+
+ virtual ~CPVRChannelGroupMember() = default;
+
+ // ISerializable implementation
+ void Serialize(CVariant& value) const override;
+
+ // ISortable implementation
+ void ToSortable(SortItem& sortable, Field field) const override;
+
+ std::shared_ptr<CPVRChannel> Channel() const { return m_channel; }
+ void SetChannel(const std::shared_ptr<CPVRChannel>& channel);
+
+ int GroupID() const { return m_iGroupID; }
+ void SetGroupID(int iGroupID);
+
+ const std::string& Path() const { return m_path; }
+ void SetGroupName(const std::string& groupName);
+
+ const CPVRChannelNumber& ChannelNumber() const { return m_channelNumber; }
+ void SetChannelNumber(const CPVRChannelNumber& channelNumber);
+
+ const CPVRChannelNumber& ClientChannelNumber() const { return m_clientChannelNumber; }
+ void SetClientChannelNumber(const CPVRChannelNumber& clientChannelNumber);
+
+ int ClientPriority() const { return m_iClientPriority; }
+ void SetClientPriority(int iClientPriority);
+
+ int Order() const { return m_iOrder; }
+ void SetOrder(int iOrder);
+
+ bool NeedsSave() const { return m_bNeedsSave; }
+ void SetSaved() { m_bNeedsSave = false; }
+
+ int ClientID() const { return m_iClientID; }
+
+ int ChannelUID() const { return m_iChannelUID; }
+
+ int ChannelDatabaseID() const { return m_iChannelDatabaseID; }
+
+ bool IsRadio() const { return m_bIsRadio; }
+
+ /*!
+ * @brief Delete this group member from the database.
+ * @return True if it was deleted successfully, false otherwise.
+ */
+ bool QueueDelete();
+
+private:
+ int m_iGroupID = -1;
+ int m_iClientID = -1;
+ int m_iChannelUID = -1;
+ int m_iChannelDatabaseID = -1;
+ bool m_bIsRadio = false;
+ std::shared_ptr<CPVRChannel> m_channel;
+ std::string m_path;
+ CPVRChannelNumber m_channelNumber; // the channel number this channel has in the group
+ CPVRChannelNumber
+ m_clientChannelNumber; // the client channel number this channel has in the group
+ int m_iClientPriority = 0;
+ int m_iOrder = 0; // The value denoting the order of this member in the group
+
+ bool m_bNeedsSave = true;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/channels/PVRChannelGroupSettings.cpp b/xbmc/pvr/channels/PVRChannelGroupSettings.cpp
new file mode 100644
index 0000000..3cbc0bd
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupSettings.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012-2021 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 "PVRChannelGroupSettings.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "settings/Settings.h"
+#include "settings/lib/Setting.h"
+
+using namespace PVR;
+
+CPVRChannelGroupSettings::CPVRChannelGroupSettings()
+ : m_settings({CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS,
+ CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER,
+ CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS,
+ CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS,
+ CSettings::SETTING_PVRMANAGER_STARTGROUPCHANNELNUMBERSFROMONE})
+{
+ UpdateSyncChannelGroups();
+ UpdateUseBackendChannelOrder();
+ UpdateUseBackendChannelNumbers();
+ UpdateStartGroupChannelNumbersFromOne();
+
+ m_settings.RegisterCallback(this);
+}
+
+CPVRChannelGroupSettings::~CPVRChannelGroupSettings()
+{
+ m_settings.UnregisterCallback(this);
+}
+
+void CPVRChannelGroupSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (!setting)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS)
+ {
+ if (SyncChannelGroups() != UpdateSyncChannelGroups())
+ {
+ for (const auto& callback : m_callbacks)
+ callback->SyncChannelGroupsChanged();
+ }
+ }
+ else if (settingId == CSettings::CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER)
+ {
+ if (UseBackendChannelOrder() != UpdateUseBackendChannelOrder())
+ {
+ for (const auto& callback : m_callbacks)
+ callback->UseBackendChannelOrderChanged();
+ }
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS ||
+ settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS)
+ {
+ if (UseBackendChannelNumbers() != UpdateUseBackendChannelNumbers())
+ {
+ for (const auto& callback : m_callbacks)
+ callback->UseBackendChannelNumbersChanged();
+ }
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_STARTGROUPCHANNELNUMBERSFROMONE)
+ {
+ if (StartGroupChannelNumbersFromOne() != UpdateStartGroupChannelNumbersFromOne())
+ {
+ for (const auto& callback : m_callbacks)
+ callback->StartGroupChannelNumbersFromOneChanged();
+ }
+ }
+}
+
+void CPVRChannelGroupSettings::RegisterCallback(IChannelGroupSettingsCallback* callback)
+{
+ m_callbacks.insert(callback);
+}
+
+void CPVRChannelGroupSettings::UnregisterCallback(IChannelGroupSettingsCallback* callback)
+{
+ m_callbacks.erase(callback);
+}
+
+bool CPVRChannelGroupSettings::UpdateSyncChannelGroups()
+{
+ m_bSyncChannelGroups = m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS);
+ return m_bSyncChannelGroups;
+}
+
+bool CPVRChannelGroupSettings::UpdateUseBackendChannelOrder()
+{
+ m_bUseBackendChannelOrder =
+ m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER);
+ return m_bUseBackendChannelOrder;
+}
+
+bool CPVRChannelGroupSettings::UpdateUseBackendChannelNumbers()
+{
+ const int enabledClientAmount = CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount();
+ m_bUseBackendChannelNumbers =
+ m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS) &&
+ (enabledClientAmount == 1 ||
+ (m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS) &&
+ enabledClientAmount > 1));
+ return m_bUseBackendChannelNumbers;
+}
+
+bool CPVRChannelGroupSettings::UpdateStartGroupChannelNumbersFromOne()
+{
+ m_bStartGroupChannelNumbersFromOne =
+ m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_STARTGROUPCHANNELNUMBERSFROMONE) &&
+ !UseBackendChannelNumbers();
+ return m_bStartGroupChannelNumbersFromOne;
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupSettings.h b/xbmc/pvr/channels/PVRChannelGroupSettings.h
new file mode 100644
index 0000000..abf842a
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupSettings.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012-2021 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 "pvr/settings/PVRSettings.h"
+#include "settings/lib/ISettingCallback.h"
+
+#include <memory>
+#include <set>
+
+namespace PVR
+{
+
+class IChannelGroupSettingsCallback
+{
+public:
+ virtual ~IChannelGroupSettingsCallback() = default;
+
+ virtual void SyncChannelGroupsChanged() {}
+ virtual void UseBackendChannelOrderChanged() {}
+ virtual void UseBackendChannelNumbersChanged() {}
+ virtual void StartGroupChannelNumbersFromOneChanged() {}
+};
+
+class CPVRChannelGroupSettings : public ISettingCallback
+{
+public:
+ CPVRChannelGroupSettings();
+ virtual ~CPVRChannelGroupSettings();
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ void RegisterCallback(IChannelGroupSettingsCallback* callback);
+ void UnregisterCallback(IChannelGroupSettingsCallback* callback);
+
+ bool SyncChannelGroups() const { return m_bSyncChannelGroups; }
+ bool UseBackendChannelOrder() const { return m_bUseBackendChannelOrder; }
+ bool UseBackendChannelNumbers() const { return m_bUseBackendChannelNumbers; }
+ bool StartGroupChannelNumbersFromOne() const { return m_bStartGroupChannelNumbersFromOne; }
+
+private:
+ bool UpdateSyncChannelGroups();
+ bool UpdateUseBackendChannelOrder();
+ bool UpdateUseBackendChannelNumbers();
+ bool UpdateStartGroupChannelNumbersFromOne();
+
+ bool m_bSyncChannelGroups = false;
+ bool m_bUseBackendChannelOrder = false;
+ bool m_bUseBackendChannelNumbers = false;
+ bool m_bStartGroupChannelNumbersFromOne = false;
+
+ CPVRSettings m_settings;
+ std::set<IChannelGroupSettingsCallback*> m_callbacks;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/channels/PVRChannelGroups.cpp b/xbmc/pvr/channels/PVRChannelGroups.cpp
new file mode 100644
index 0000000..77cc2b5
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroups.cpp
@@ -0,0 +1,621 @@
+/*
+ * 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 "PVRChannelGroups.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClientUID.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupInternal.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <chrono>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+CPVRChannelGroups::CPVRChannelGroups(bool bRadio) :
+ m_bRadio(bRadio)
+{
+}
+
+CPVRChannelGroups::~CPVRChannelGroups()
+{
+ Unload();
+}
+
+void CPVRChannelGroups::Unload()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& group : m_groups)
+ group->Unload();
+
+ m_groups.clear();
+ m_failedClientsForChannelGroups.clear();
+}
+
+bool CPVRChannelGroups::Update(const std::shared_ptr<CPVRChannelGroup>& group,
+ bool bUpdateFromClient /* = false */)
+{
+ if (group->GroupName().empty() && group->GroupID() <= 0)
+ return true;
+
+ std::shared_ptr<CPVRChannelGroup> updateGroup;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // There can be only one internal group! Make sure we never push a new one!
+ if (group->IsInternalGroup())
+ updateGroup = GetGroupAll();
+
+ // try to find the group by id
+ if (!updateGroup && group->GroupID() > 0)
+ updateGroup = GetById(group->GroupID());
+
+ // try to find the group by name if we didn't find it yet
+ if (!updateGroup)
+ updateGroup = GetByName(group->GroupName());
+
+ if (updateGroup)
+ {
+ updateGroup->SetPath(group->GetPath());
+ updateGroup->SetGroupID(group->GroupID());
+ updateGroup->SetGroupType(group->GroupType());
+ updateGroup->SetPosition(group->GetPosition());
+
+ // don't override properties we only store locally in our PVR database
+ if (!bUpdateFromClient)
+ {
+ updateGroup->SetLastWatched(group->LastWatched());
+ updateGroup->SetHidden(group->IsHidden());
+ updateGroup->SetLastOpened(group->LastOpened());
+ }
+ }
+ else
+ {
+ updateGroup = group;
+ m_groups.emplace_back(updateGroup);
+ }
+ }
+
+ // sort groups
+ SortGroups();
+
+ // persist changes
+ if (bUpdateFromClient)
+ return updateGroup->Persist();
+
+ return true;
+}
+
+void CPVRChannelGroups::SortGroups()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // check if one of the group holds a valid sort position
+ const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(),
+ [](const auto& group) { return (group->GetPosition() > 0); });
+
+ // sort by position if we found a valid sort position
+ if (it != m_groups.cend())
+ {
+ std::sort(m_groups.begin(), m_groups.end(), [](const auto& group1, const auto& group2) {
+ return group1->GetPosition() < group2->GetPosition();
+ });
+ }
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroups::GetChannelGroupMemberByPath(
+ const CPVRChannelsPath& path) const
+{
+ if (path.IsChannel())
+ {
+ const std::shared_ptr<CPVRChannelGroup> group = GetByName(path.GetGroupName());
+ if (group)
+ return group->GetByUniqueID(
+ {CPVRClientUID(path.GetAddonID(), path.GetInstanceID()).GetUID(), path.GetChannelUID()});
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetById(int iGroupId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [iGroupId](const auto& group) {
+ return group->GroupID() == iGroupId;
+ });
+ return (it != m_groups.cend()) ? (*it) : std::shared_ptr<CPVRChannelGroup>();
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroup>> CPVRChannelGroups::GetGroupsByChannel(const std::shared_ptr<CPVRChannel>& channel, bool bExcludeHidden /* = false */) const
+{
+ std::vector<std::shared_ptr<CPVRChannelGroup>> groups;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::copy_if(m_groups.cbegin(), m_groups.cend(), std::back_inserter(groups),
+ [bExcludeHidden, &channel](const auto& group) {
+ return (!bExcludeHidden || !group->IsHidden()) && group->IsGroupMember(channel);
+ });
+ return groups;
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetGroupByPath(const std::string& strInPath) const
+{
+ const CPVRChannelsPath path(strInPath);
+ if (path.IsChannelGroup())
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(),
+ [&path](const auto& group) { return group->GetPath() == path; });
+ if (it != m_groups.cend())
+ return (*it);
+ }
+ return {};
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetByName(const std::string& strName) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [&strName](const auto& group) {
+ return group->GroupName() == strName;
+ });
+ return (it != m_groups.cend()) ? (*it) : std::shared_ptr<CPVRChannelGroup>();
+}
+
+bool CPVRChannelGroups::HasValidDataForClients(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients) const
+{
+ return m_failedClientsForChannelGroups.empty() ||
+ std::none_of(clients.cbegin(), clients.cend(),
+ [this](const std::shared_ptr<CPVRClient>& client) {
+ return std::find(m_failedClientsForChannelGroups.cbegin(),
+ m_failedClientsForChannelGroups.cend(),
+ client->GetID()) != m_failedClientsForChannelGroups.cend();
+ });
+}
+
+bool CPVRChannelGroups::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ bool bChannelsOnly /* = false */)
+{
+ bool bSyncWithBackends = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS);
+ bool bUpdateAllGroups = !bChannelsOnly && bSyncWithBackends;
+ bool bReturn = true;
+
+ // sync groups
+ const int iSize = m_groups.size();
+ if (bUpdateAllGroups)
+ {
+ // get channel groups from the clients
+ CServiceBroker::GetPVRManager().Clients()->GetChannelGroups(clients, this,
+ m_failedClientsForChannelGroups);
+ CLog::LogFC(LOGDEBUG, LOGPVR, "{} new user defined {} channel groups fetched from clients",
+ (m_groups.size() - iSize), m_bRadio ? "radio" : "TV");
+ }
+ else if (!bSyncWithBackends)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "'sync channelgroups' is disabled; skipping groups from clients");
+ }
+
+ // sync channels in groups
+ std::vector<std::shared_ptr<CPVRChannelGroup>> groups;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ groups = m_groups;
+ }
+
+ std::vector<std::shared_ptr<CPVRChannelGroup>> emptyGroups;
+
+ for (const auto& group : groups)
+ {
+ if (bUpdateAllGroups || group->IsInternalGroup())
+ {
+ const int iMemberCount = group->Size();
+ if (!group->UpdateFromClients(clients))
+ {
+ CLog::LogFC(LOGERROR, LOGPVR, "Failed to update channel group '{}'", group->GroupName());
+ bReturn = false;
+ }
+
+ const int iChangedMembersCount = static_cast<int>(group->Size()) - iMemberCount;
+ if (iChangedMembersCount > 0)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "{} channel group members added to group '{}'",
+ iChangedMembersCount, group->GroupName());
+ }
+ else if (iChangedMembersCount < 0)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "{} channel group members removed from group '{}'",
+ -iChangedMembersCount, group->GroupName());
+ }
+ else
+ {
+ // could still be changed if same amount of members was removed as was added, but too
+ // complicated to calculate just for debug logging
+ }
+ }
+
+ // remove empty groups if sync with backend is enabled and we have valid data from all clients
+ if (bSyncWithBackends && group->Size() == 0 && !group->IsInternalGroup() &&
+ HasValidDataForClients(clients) && group->HasValidDataForClients(clients))
+ {
+ emptyGroups.emplace_back(group);
+ }
+
+ if (bReturn &&
+ group->IsInternalGroup() &&
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bPVRChannelIconsAutoScan)
+ {
+ CServiceBroker::GetPVRManager().TriggerSearchMissingChannelIcons(group);
+ }
+ }
+
+ for (const auto& group : emptyGroups)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleting empty channel group '{}'", group->GroupName());
+ DeleteGroup(group);
+ }
+
+ if (bChannelsOnly)
+ {
+ // changes in the all channels group may require resorting/renumbering of other groups.
+ // if we updated all groups this already has been done while updating the single groups.
+ UpdateChannelNumbersFromAllChannelsGroup();
+ }
+
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated);
+
+ // persist changes
+ return PersistAll() && bReturn;
+}
+
+bool CPVRChannelGroups::UpdateChannelNumbersFromAllChannelsGroup()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::accumulate(
+ m_groups.cbegin(), m_groups.cend(), false, [](bool changed, const auto& group) {
+ return group->UpdateChannelNumbersFromAllChannelsGroup() ? true : changed;
+ });
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::CreateChannelGroup(
+ int iType, const CPVRChannelsPath& path)
+{
+ if (iType == PVR_GROUP_TYPE_INTERNAL)
+ return std::make_shared<CPVRChannelGroupInternal>(path);
+ else
+ return std::make_shared<CPVRChannelGroup>(path, GetGroupAll());
+}
+
+bool CPVRChannelGroups::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ const std::shared_ptr<CPVRDatabase> database(CServiceBroker::GetPVRManager().GetTVDatabase());
+ if (!database)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // Ensure we have an internal group. It is important that the internal group is created before
+ // loading contents from database and that it gets inserted in front of m_groups. Look at
+ // GetGroupAll() implementation to see why.
+ if (m_groups.empty())
+ {
+ const auto internalGroup = std::make_shared<CPVRChannelGroupInternal>(m_bRadio);
+ m_groups.emplace_back(internalGroup);
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Loading all {} channel groups and members",
+ m_bRadio ? "radio" : "TV");
+
+ // load all channels from the database
+ std::map<std::pair<int, int>, std::shared_ptr<CPVRChannel>> channels;
+ database->Get(m_bRadio, clients, channels);
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Fetched {} {} channels from the database", channels.size(),
+ m_bRadio ? "radio" : "TV");
+
+ // load all groups from the database
+ const int iLoaded = database->Get(*this);
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Fetched {} {} groups from the database", iLoaded,
+ m_bRadio ? "radio" : "TV");
+
+ // load all group members from the database
+ for (const auto& group : m_groups)
+ {
+ if (!group->LoadFromDatabase(channels, clients))
+ {
+ CLog::LogFC(LOGERROR, LOGPVR,
+ "Failed to load members of {} channel group '{}' from the database",
+ m_bRadio ? "radio" : "TV", group->GroupName());
+ }
+ }
+
+ // Hide empty groups
+ for (auto it = m_groups.begin(); it != m_groups.end();)
+ {
+ if ((*it)->Size() == 0 && !(*it)->IsInternalGroup())
+ it = m_groups.erase(it);
+ else
+ ++it;
+ }
+
+ return true;
+}
+
+bool CPVRChannelGroups::PersistAll()
+{
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Persisting all channel group changes");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::accumulate(
+ m_groups.cbegin(), m_groups.cend(), true,
+ [](bool success, const auto& group) { return !group->Persist() ? false : success; });
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetGroupAll() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_groups.empty())
+ return m_groups.front();
+
+ return std::shared_ptr<CPVRChannelGroup>();
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetLastGroup() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_groups.empty())
+ return m_groups.back();
+
+ return std::shared_ptr<CPVRChannelGroup>();
+}
+
+GroupMemberPair CPVRChannelGroups::GetLastAndPreviousToLastPlayedChannelGroupMember() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_groups.empty())
+ return {};
+
+ auto groups = m_groups;
+ lock.unlock();
+
+ std::sort(groups.begin(), groups.end(),
+ [](const auto& a, const auto& b) { return a->LastWatched() > b->LastWatched(); });
+
+ // Last is always 'first' of last played group.
+ const GroupMemberPair members = groups[0]->GetLastAndPreviousToLastPlayedChannelGroupMember();
+ std::shared_ptr<CPVRChannelGroupMember> last = members.first;
+
+ // Previous to last is either 'second' of first group or 'first' of second group.
+ std::shared_ptr<CPVRChannelGroupMember> previousToLast = members.second;
+ if (groups.size() > 1 && groups[0]->LastWatched() && groups[1]->LastWatched() && members.second &&
+ members.second->Channel()->LastWatched())
+ {
+ if (groups[1]->LastWatched() >= members.second->Channel()->LastWatched())
+ {
+ const GroupMemberPair membersPreviousToLastPlayedGroup =
+ groups[1]->GetLastAndPreviousToLastPlayedChannelGroupMember();
+ previousToLast = membersPreviousToLastPlayedGroup.first;
+ }
+ }
+
+ return {last, previousToLast};
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetLastOpenedGroup() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::accumulate(
+ m_groups.cbegin(), m_groups.cend(), std::shared_ptr<CPVRChannelGroup>{},
+ [](const std::shared_ptr<CPVRChannelGroup>& last,
+ const std::shared_ptr<CPVRChannelGroup>& group)
+ {
+ return group->LastOpened() > 0 && (!last || group->LastOpened() > last->LastOpened())
+ ? group
+ : last;
+ });
+}
+
+std::vector<std::shared_ptr<CPVRChannelGroup>> CPVRChannelGroups::GetMembers(bool bExcludeHidden /* = false */) const
+{
+ std::vector<std::shared_ptr<CPVRChannelGroup>> groups;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::copy_if(
+ m_groups.cbegin(), m_groups.cend(), std::back_inserter(groups),
+ [bExcludeHidden](const auto& group) { return (!bExcludeHidden || !group->IsHidden()); });
+ return groups;
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetPreviousGroup(const CPVRChannelGroup& group) const
+{
+ {
+ bool bReturnNext = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_reverse_iterator it = m_groups.rbegin(); it != m_groups.rend(); ++it)
+ {
+ // return this entry
+ if (bReturnNext && !(*it)->IsHidden())
+ return *it;
+
+ // return the next entry
+ if ((*it)->GroupID() == group.GroupID())
+ bReturnNext = true;
+ }
+
+ // no match return last visible group
+ for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_reverse_iterator it = m_groups.rbegin(); it != m_groups.rend(); ++it)
+ {
+ if (!(*it)->IsHidden())
+ return *it;
+ }
+ }
+
+ // no match
+ return GetLastGroup();
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroups::GetNextGroup(const CPVRChannelGroup& group) const
+{
+ {
+ bool bReturnNext = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_iterator it = m_groups.begin(); it != m_groups.end(); ++it)
+ {
+ // return this entry
+ if (bReturnNext && !(*it)->IsHidden())
+ return *it;
+
+ // return the next entry
+ if ((*it)->GroupID() == group.GroupID())
+ bReturnNext = true;
+ }
+
+ // no match return first visible group
+ for (std::vector<std::shared_ptr<CPVRChannelGroup>>::const_iterator it = m_groups.begin(); it != m_groups.end(); ++it)
+ {
+ if (!(*it)->IsHidden())
+ return *it;
+ }
+ }
+
+ // no match
+ return GetFirstGroup();
+}
+
+bool CPVRChannelGroups::AddGroup(const std::string& strName)
+{
+ bool bPersist(false);
+ std::shared_ptr<CPVRChannelGroup> group;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // check if there's no group with the same name yet
+ group = GetByName(strName);
+ if (!group)
+ {
+ // create a new group
+ group.reset(new CPVRChannelGroup(CPVRChannelsPath(m_bRadio, strName), GetGroupAll()));
+
+ m_groups.push_back(group);
+ bPersist = true;
+
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated);
+ }
+ }
+
+ // persist in the db if a new group was added
+ return bPersist ? group->Persist() : true;
+}
+
+bool CPVRChannelGroups::DeleteGroup(const std::shared_ptr<CPVRChannelGroup>& group)
+{
+ // don't delete internal groups
+ if (group->IsInternalGroup())
+ {
+ CLog::LogF(LOGERROR, "Internal channel group cannot be deleted");
+ return false;
+ }
+
+ bool bFound(false);
+
+ // delete the group in this container
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (auto it = m_groups.begin(); it != m_groups.end(); ++it)
+ {
+ if (*it == group || (group->GroupID() > 0 && (*it)->GroupID() == group->GroupID()))
+ {
+ m_groups.erase(it);
+ bFound = true;
+ break;
+ }
+ }
+ }
+
+ if (bFound && group->GroupID() > 0)
+ {
+ // delete the group from the database
+ group->Delete();
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated);
+ }
+ return bFound;
+}
+
+bool CPVRChannelGroups::HideGroup(const std::shared_ptr<CPVRChannelGroup>& group, bool bHide)
+{
+ bool bReturn = false;
+
+ if (group)
+ {
+ if (group->SetHidden(bHide))
+ {
+ // state changed
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::ChannelGroupsInvalidated);
+ }
+ bReturn = true;
+ }
+ return bReturn;
+}
+
+bool CPVRChannelGroups::CreateChannelEpgs()
+{
+ bool bReturn(false);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (std::vector<std::shared_ptr<CPVRChannelGroup>>::iterator it = m_groups.begin(); it != m_groups.end(); ++it)
+ {
+ /* Only create EPGs for the internal groups */
+ if ((*it)->IsInternalGroup())
+ bReturn = (*it)->CreateChannelEpgs();
+ }
+ return bReturn;
+}
+
+int CPVRChannelGroups::CleanupCachedImages()
+{
+ int iCleanedImages = 0;
+
+ // cleanup channels
+ iCleanedImages += GetGroupAll()->CleanupCachedImages();
+
+ // cleanup groups
+ std::vector<std::string> urlsToCheck;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::transform(m_groups.cbegin(), m_groups.cend(), std::back_inserter(urlsToCheck),
+ [](const auto& group) { return group->GetPath(); });
+ }
+
+ // kodi-generated thumbnail (see CPVRThumbLoader)
+ const std::string path = StringUtils::Format("pvr://channels/{}/", IsRadio() ? "radio" : "tv");
+ iCleanedImages += CPVRCachedImages::Cleanup({{"pvr", path}}, urlsToCheck, true);
+
+ return iCleanedImages;
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroups.h b/xbmc/pvr/channels/PVRChannelGroups.h
new file mode 100644
index 0000000..709dcfb
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroups.h
@@ -0,0 +1,244 @@
+/*
+ * 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 "pvr/channels/PVRChannelGroup.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+ class CPVRChannel;
+ class CPVRClient;
+
+ /** A container class for channel groups */
+
+ class CPVRChannelGroups
+ {
+ public:
+ /*!
+ * @brief Create a new group container.
+ * @param bRadio True if this is a container for radio channels, false if it is for tv channels.
+ */
+ explicit CPVRChannelGroups(bool bRadio);
+ virtual ~CPVRChannelGroups();
+
+ /*!
+ * @brief Remove all groups from this container.
+ */
+ void Unload();
+
+ /*!
+ * @brief Load all channel groups and all channels from PVR database.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Create a channel group matching the given type.
+ * @param iType The type for the group.
+ * @param path The path of the group.
+ * @return The new group.
+ */
+ std::shared_ptr<CPVRChannelGroup> CreateChannelGroup(int iType, const CPVRChannelsPath& path);
+
+ /*!
+ * @return Amount of groups in this container
+ */
+ size_t Size() const
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_groups.size();
+ }
+
+ /*!
+ * @brief Update a group or add it if it's not in here yet.
+ * @param group The group to update.
+ * @param bUpdateFromClient True to save the changes in the db.
+ * @return True if the group was added or update successfully, false otherwise.
+ */
+ bool Update(const std::shared_ptr<CPVRChannelGroup>& group, bool bUpdateFromClient = false);
+
+ /*!
+ * @brief Called by the add-on callback to add a new group
+ * @param group The group to add
+ * @return True when updated, false otherwise
+ */
+ bool UpdateFromClient(const std::shared_ptr<CPVRChannelGroup>& group)
+ {
+ return Update(group, true);
+ }
+
+ /*!
+ * @brief Get a channel group member given its path
+ * @param strPath The path to the channel group member
+ * @return The channel group member, or nullptr if not found
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMemberByPath(
+ const CPVRChannelsPath& path) const;
+
+ /*!
+ * @brief Get a pointer to a channel group given its ID.
+ * @param iGroupId The ID of the group.
+ * @return The group or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetById(int iGroupId) const;
+
+ /*!
+ * @brief Get all groups the given channel is a member.
+ * @param channel The channel.
+ * @param bExcludeHidden Whenever to exclude hidden channel groups.
+ * @return A list of groups the channel is a member.
+ */
+ std::vector<std::shared_ptr<CPVRChannelGroup>> GetGroupsByChannel(const std::shared_ptr<CPVRChannel>& channel, bool bExcludeHidden = false) const;
+
+ /*!
+ * @brief Get a channel group given its path
+ * @param strPath The path to the channel group
+ * @return The channel group, or nullptr if not found
+ */
+ std::shared_ptr<CPVRChannelGroup> GetGroupByPath(const std::string& strPath) const;
+
+ /*!
+ * @brief Get a group given its name.
+ * @param strName The name.
+ * @return The group or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetByName(const std::string& strName) const;
+
+ /*!
+ * @brief Get the group that contains all channels.
+ * @return The group that contains all channels.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetGroupAll() const;
+
+ /*!
+ * @return The first group in this container, which always is the group with all channels.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetFirstGroup() const { return GetGroupAll(); }
+
+ /*!
+ * @return The last group in this container.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetLastGroup() const;
+
+ /*!
+ * @return The last and previous to last played channel group members. pair.first contains the last, pair.second the previous to last member.
+ */
+ GroupMemberPair GetLastAndPreviousToLastPlayedChannelGroupMember() const;
+
+ /*!
+ * @return The last opened group.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetLastOpenedGroup() const;
+
+ /*!
+ * @brief Get the list of groups.
+ * @param groups The list to store the results in.
+ * @param bExcludeHidden Whenever to exclude hidden channel groups.
+ * @return The amount of items that were added.
+ */
+ std::vector<std::shared_ptr<CPVRChannelGroup>> GetMembers(bool bExcludeHidden = false) const;
+
+ /*!
+ * @brief Get the previous group in this container.
+ * @param group The current group.
+ * @return The previous group or the group containing all channels if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetPreviousGroup(const CPVRChannelGroup& group) const;
+
+ /*!
+ * @brief Get the next group in this container.
+ * @param group The current group.
+ * @return The next group or the group containing all channels if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetNextGroup(const CPVRChannelGroup& group) const;
+
+ /*!
+ * @brief Add a group to this container.
+ * @param strName The name of the group.
+ * @return True if the group was added, false otherwise.
+ */
+ bool AddGroup(const std::string& strName);
+
+ /*!
+ * @brief Remove a group from this container and delete it from the database.
+ * @param group The group to delete.
+ * @return True if it was deleted successfully, false if not.
+ */
+ bool DeleteGroup(const std::shared_ptr<CPVRChannelGroup>& group);
+
+ /*!
+ * @brief Hide/unhide a group in this container.
+ * @param group The group to hide/unhide.
+ * @param bHide True to hide the group, false to unhide it.
+ * @return True on success, false otherwise.
+ */
+ bool HideGroup(const std::shared_ptr<CPVRChannelGroup>& group, bool bHide);
+
+ /*!
+ * @brief Create EPG tags for all channels of the internal group.
+ * @return True if EPG tags where created successfully, false if not.
+ */
+ bool CreateChannelEpgs();
+
+ /*!
+ * @brief Persist all changes in channel groups.
+ * @return True if everything was persisted, false otherwise.
+ */
+ bool PersistAll();
+
+ /*!
+ * @return True when this container contains radio groups, false otherwise
+ */
+ bool IsRadio() const { return m_bRadio; }
+
+ /*!
+ * @brief Update data with groups and channels from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param bChannelsOnly Set to true to only update channels, not the groups themselves.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ bool bChannelsOnly = false);
+
+ /*!
+ * @brief Update the channel numbers across the channel groups from the all channels group
+ * @return True if any channel number was changed, false otherwise.
+ */
+ bool UpdateChannelNumbersFromAllChannelsGroup();
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+ private:
+ void SortGroups();
+
+ /*!
+ * @brief Check, whether data for given pvr clients are currently valid. For instance, data
+ * can be invalid because the client's backend was offline when data was last queried.
+ * @param clients The clients to check. Check all active clients if vector is empty.
+ * @return True, if data is currently valid, false otherwise.
+ */
+ bool HasValidDataForClients(const std::vector<std::shared_ptr<CPVRClient>>& clients) const;
+
+ bool m_bRadio; /*!< true if this is a container for radio channels, false if it is for tv channels */
+ std::vector<std::shared_ptr<CPVRChannelGroup>> m_groups; /*!< the groups in this container */
+ mutable CCriticalSection m_critSection;
+ std::vector<int> m_failedClientsForChannelGroups;
+ };
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupsContainer.cpp b/xbmc/pvr/channels/PVRChannelGroupsContainer.cpp
new file mode 100644
index 0000000..5f65cdd
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupsContainer.cpp
@@ -0,0 +1,167 @@
+/*
+ * 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 "PVRChannelGroupsContainer.h"
+
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+
+using namespace PVR;
+
+CPVRChannelGroupsContainer::CPVRChannelGroupsContainer() :
+ m_groupsRadio(new CPVRChannelGroups(true)),
+ m_groupsTV(new CPVRChannelGroups(false))
+{
+}
+
+CPVRChannelGroupsContainer::~CPVRChannelGroupsContainer()
+{
+ Unload();
+ delete m_groupsRadio;
+ delete m_groupsTV;
+}
+
+bool CPVRChannelGroupsContainer::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ return LoadFromDatabase(clients) && UpdateFromClients(clients);
+}
+
+bool CPVRChannelGroupsContainer::LoadFromDatabase(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ return m_groupsTV->LoadFromDatabase(clients) && m_groupsRadio->LoadFromDatabase(clients);
+}
+
+bool CPVRChannelGroupsContainer::UpdateFromClients(
+ const std::vector<std::shared_ptr<CPVRClient>>& clients, bool bChannelsOnly /* = false */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bIsUpdating)
+ return false;
+ m_bIsUpdating = true;
+ lock.unlock();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updating {}", bChannelsOnly ? "channels" : "channel groups");
+ bool bReturn = m_groupsTV->UpdateFromClients(clients, bChannelsOnly) &&
+ m_groupsRadio->UpdateFromClients(clients, bChannelsOnly);
+
+ lock.lock();
+ m_bIsUpdating = false;
+ lock.unlock();
+
+ return bReturn;
+}
+
+void CPVRChannelGroupsContainer::Unload()
+{
+ m_groupsRadio->Unload();
+ m_groupsTV->Unload();
+}
+
+CPVRChannelGroups* CPVRChannelGroupsContainer::Get(bool bRadio) const
+{
+ return bRadio ? m_groupsRadio : m_groupsTV;
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroupsContainer::GetGroupAll(bool bRadio) const
+{
+ return Get(bRadio)->GetGroupAll();
+}
+
+std::shared_ptr<CPVRChannelGroup> CPVRChannelGroupsContainer::GetByIdFromAll(int iGroupId) const
+{
+ std::shared_ptr<CPVRChannelGroup> group = m_groupsTV->GetById(iGroupId);
+ if (!group)
+ group = m_groupsRadio->GetById(iGroupId);
+
+ return group;
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetChannelById(int iChannelId) const
+{
+ std::shared_ptr<CPVRChannel> channel = m_groupsTV->GetGroupAll()->GetByChannelID(iChannelId);
+ if (!channel)
+ channel = m_groupsRadio->GetGroupAll()->GetByChannelID(iChannelId);
+
+ return channel;
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetChannelForEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (!epgTag)
+ return {};
+
+ return Get(epgTag->IsRadio())->GetGroupAll()->GetByUniqueID(epgTag->UniqueChannelID(), epgTag->ClientID());
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroupsContainer::GetChannelGroupMemberByPath(
+ const std::string& strPath) const
+{
+ const CPVRChannelsPath path(strPath);
+ if (path.IsValid())
+ return Get(path.IsRadio())->GetChannelGroupMemberByPath(path);
+
+ return {};
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetByPath(const std::string& strPath) const
+{
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember = GetChannelGroupMemberByPath(strPath);
+ if (groupMember)
+ return groupMember->Channel();
+
+ return {};
+}
+
+std::shared_ptr<CPVRChannel> CPVRChannelGroupsContainer::GetByUniqueID(int iUniqueChannelId, int iClientID) const
+{
+ std::shared_ptr<CPVRChannel> channel;
+ std::shared_ptr<CPVRChannelGroup> channelgroup = GetGroupAllTV();
+ if (channelgroup)
+ channel = channelgroup->GetByUniqueID(iUniqueChannelId, iClientID);
+
+ if (!channelgroup || !channel)
+ channelgroup = GetGroupAllRadio();
+ if (channelgroup)
+ channel = channelgroup->GetByUniqueID(iUniqueChannelId, iClientID);
+
+ return channel;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRChannelGroupsContainer::
+ GetLastPlayedChannelGroupMember() const
+{
+ std::shared_ptr<CPVRChannelGroupMember> channelTV =
+ m_groupsTV->GetGroupAll()->GetLastPlayedChannelGroupMember();
+ std::shared_ptr<CPVRChannelGroupMember> channelRadio =
+ m_groupsRadio->GetGroupAll()->GetLastPlayedChannelGroupMember();
+
+ if (!channelTV || (channelRadio &&
+ channelRadio->Channel()->LastWatched() > channelTV->Channel()->LastWatched()))
+ return channelRadio;
+
+ return channelTV;
+}
+
+bool CPVRChannelGroupsContainer::CreateChannelEpgs()
+{
+ bool bReturn = m_groupsTV->CreateChannelEpgs();
+ bReturn &= m_groupsRadio->CreateChannelEpgs();
+ return bReturn;
+}
+
+int CPVRChannelGroupsContainer::CleanupCachedImages()
+{
+ return m_groupsTV->CleanupCachedImages() + m_groupsRadio->CleanupCachedImages();
+}
diff --git a/xbmc/pvr/channels/PVRChannelGroupsContainer.h b/xbmc/pvr/channels/PVRChannelGroupsContainer.h
new file mode 100644
index 0000000..d7b7f57
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelGroupsContainer.h
@@ -0,0 +1,175 @@
+/*
+ * 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 <memory>
+#include <vector>
+
+namespace PVR
+{
+ class CPVRChannel;
+ class CPVRChannelGroup;
+ class CPVRChannelGroupMember;
+ class CPVRChannelGroups;
+ class CPVRClient;
+ class CPVREpgInfoTag;
+
+ class CPVRChannelGroupsContainer
+ {
+ public:
+ /*!
+ * @brief Create a new container for all channel groups
+ */
+ CPVRChannelGroupsContainer();
+
+ /*!
+ * @brief Destroy this container.
+ */
+ virtual ~CPVRChannelGroupsContainer();
+
+ /*!
+ * @brief Update all channel groups and all channels from PVR database and from given clients.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Update data with groups and channels from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @param bChannelsOnly Set to true to only update channels, not the groups themselves.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients,
+ bool bChannelsOnly = false);
+
+ /*!
+ * @brief Unload and destruct all channel groups and all channels in them.
+ */
+ void Unload();
+
+ /*!
+ * @brief Get the TV channel groups.
+ * @return The TV channel groups.
+ */
+ CPVRChannelGroups* GetTV() const { return Get(false); }
+
+ /*!
+ * @brief Get the radio channel groups.
+ * @return The radio channel groups.
+ */
+ CPVRChannelGroups* GetRadio() const { return Get(true); }
+
+ /*!
+ * @brief Get the radio or TV channel groups.
+ * @param bRadio If true, get the radio channel groups. Get the TV channel groups otherwise.
+ * @return The requested groups.
+ */
+ CPVRChannelGroups* Get(bool bRadio) const;
+
+ /*!
+ * @brief Get the group containing all TV channels.
+ * @return The group containing all TV channels.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetGroupAllTV() const { return GetGroupAll(false); }
+
+ /*!
+ * @brief Get the group containing all radio channels.
+ * @return The group containing all radio channels.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetGroupAllRadio() const { return GetGroupAll(true); }
+
+ /*!
+ * @brief Get the group containing all TV or radio channels.
+ * @param bRadio If true, get the group containing all radio channels. Get the group containing all TV channels otherwise.
+ * @return The requested group.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetGroupAll(bool bRadio) const;
+
+ /*!
+ * @brief Get a group given it's ID.
+ * @param iGroupId The ID of the group.
+ * @return The requested group or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetByIdFromAll(int iGroupId) const;
+
+ /*!
+ * @brief Get a channel given it's database ID.
+ * @param iChannelId The ID of the channel.
+ * @return The channel or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannel> GetChannelById(int iChannelId) const;
+
+ /*!
+ * @brief Get the channel for the given epg tag.
+ * @param epgTag The epg tag.
+ * @return The channel.
+ */
+ std::shared_ptr<CPVRChannel> GetChannelForEpgTag(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ /*!
+ * @brief Get a channel given it's path.
+ * @param strPath The path.
+ * @return The channel or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannel> GetByPath(const std::string& strPath) const;
+
+ /*!
+ * @brief Get a channel group member given it's path.
+ * @param strPath The path.
+ * @return The channel group member or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMemberByPath(
+ const std::string& strPath) const;
+
+ /*!
+ * @brief Get a channel given it's channel ID from all containers.
+ * @param iUniqueChannelId The unique channel id on the client.
+ * @param iClientID The ID of the client.
+ * @return The channel or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVRChannel> GetByUniqueID(int iUniqueChannelId, int iClientID) const;
+
+ /*!
+ * @brief Get the channel group member that was played last.
+ * @return The requested channel group member or nullptr.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetLastPlayedChannelGroupMember() const;
+
+ /*!
+ * @brief Create EPG tags for channels in all internal channel groups.
+ * @return True if EPG tags were created successfully.
+ */
+ bool CreateChannelEpgs();
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+ private:
+ CPVRChannelGroupsContainer& operator=(const CPVRChannelGroupsContainer&) = delete;
+ CPVRChannelGroupsContainer(const CPVRChannelGroupsContainer&) = delete;
+
+ /*!
+ * @brief Load all channel groups and all channels from PVR database.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ CPVRChannelGroups* m_groupsRadio; /*!< all radio channel groups */
+ CPVRChannelGroups* m_groupsTV; /*!< all TV channel groups */
+ CCriticalSection m_critSection;
+ bool m_bIsUpdating = false;
+ };
+}
diff --git a/xbmc/pvr/channels/PVRChannelNumber.cpp b/xbmc/pvr/channels/PVRChannelNumber.cpp
new file mode 100644
index 0000000..403938e
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelNumber.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017-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 "PVRChannelNumber.h"
+
+#include "utils/StringUtils.h"
+
+using namespace PVR;
+
+const char CPVRChannelNumber::SEPARATOR = '.';
+
+std::string CPVRChannelNumber::FormattedChannelNumber() const
+{
+ return ToString(SEPARATOR);
+}
+
+std::string CPVRChannelNumber::SortableChannelNumber() const
+{
+ // Note: The subchannel separator is a character that does not work for a
+ // SortItem (at least not on all platforms). See SortUtils::Sort for
+ // details. Only numbers, letters and the blank are safe to use.
+ return ToString(' ');
+}
+
+std::string CPVRChannelNumber::ToString(char separator) const
+{
+ if (m_iSubChannelNumber == 0)
+ return std::to_string(m_iChannelNumber);
+ else
+ return StringUtils::Format("{}{}{}", m_iChannelNumber, separator, m_iSubChannelNumber);
+}
diff --git a/xbmc/pvr/channels/PVRChannelNumber.h b/xbmc/pvr/channels/PVRChannelNumber.h
new file mode 100644
index 0000000..b6e2efc
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelNumber.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017-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 <string>
+
+namespace PVR
+{
+ class CPVRChannelNumber
+ {
+ public:
+ CPVRChannelNumber() = default;
+
+ constexpr CPVRChannelNumber(unsigned int iChannelNumber, unsigned int iSubChannelNumber)
+ : m_iChannelNumber(iChannelNumber), m_iSubChannelNumber(iSubChannelNumber) {}
+
+ constexpr bool operator ==(const CPVRChannelNumber& right) const
+ {
+ return (m_iChannelNumber == right.m_iChannelNumber &&
+ m_iSubChannelNumber == right.m_iSubChannelNumber);
+ }
+
+ constexpr bool operator !=(const CPVRChannelNumber& right) const
+ {
+ return !(*this == right);
+ }
+
+ constexpr bool operator <(const CPVRChannelNumber& right) const
+ {
+ return m_iChannelNumber == right.m_iChannelNumber
+ ? m_iSubChannelNumber < right.m_iSubChannelNumber
+ : m_iChannelNumber < right.m_iChannelNumber;
+ }
+
+ /*!
+ * @brief Check whether this channel number is valid (main channel number > 0).
+ * @return True if valid, false otherwise..
+ */
+ constexpr bool IsValid() const { return m_iChannelNumber > 0; }
+
+ /*!
+ * @brief Set the primary channel number.
+ * @return The channel number.
+ */
+ constexpr unsigned int GetChannelNumber() const
+ {
+ return m_iChannelNumber;
+ }
+
+ /*!
+ * @brief Set the sub channel number.
+ * @return The sub channel number (ATSC).
+ */
+ constexpr unsigned int GetSubChannelNumber() const
+ {
+ return m_iSubChannelNumber;
+ }
+
+ /*!
+ * @brief The character used to separate channel and subchannel number.
+ */
+ static const char SEPARATOR; // '.'
+
+ /*!
+ * @brief Get a string representation for the channel number.
+ * @return The formatted string in the form <channel>SEPARATOR<subchannel>.
+ */
+ std::string FormattedChannelNumber() const;
+
+ /*!
+ * @brief Get a string representation for the channel number that can be used for SortItems.
+ * @return The sortable string in the form <channel> <subchannel>.
+ */
+ std::string SortableChannelNumber() const;
+
+ private:
+ std::string ToString(char separator) const;
+
+ unsigned int m_iChannelNumber = 0;
+ unsigned int m_iSubChannelNumber = 0;
+ };
+}
diff --git a/xbmc/pvr/channels/PVRChannelsPath.cpp b/xbmc/pvr/channels/PVRChannelsPath.cpp
new file mode 100644
index 0000000..4ae2e28
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelsPath.cpp
@@ -0,0 +1,190 @@
+/*
+ * 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 "PVRChannelsPath.h"
+
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVRChannelsPath::PATH_TV_CHANNELS = "pvr://channels/tv/";
+const std::string CPVRChannelsPath::PATH_RADIO_CHANNELS = "pvr://channels/radio/";
+
+
+CPVRChannelsPath::CPVRChannelsPath(const std::string& strPath)
+{
+ std::string strVarPath = TrimSlashes(strPath);
+ const std::vector<std::string> segments = URIUtils::SplitPath(strVarPath);
+
+ for (const std::string& segment : segments)
+ {
+ switch (m_kind)
+ {
+ case Kind::INVALID:
+ if (segment == "pvr://")
+ m_kind = Kind::PROTO; // pvr:// followed by something => go on
+ else if (segment == "pvr:" && segments.size() == 1) // just pvr:// => invalid
+ strVarPath = "pvr:/";
+ break;
+
+ case Kind::PROTO:
+ if (segment == "channels")
+ m_kind = Kind::EMPTY; // pvr://channels
+ else
+ m_kind = Kind::INVALID;
+ break;
+
+ case Kind::EMPTY:
+ if (segment == "tv" || segment == "radio")
+ {
+ m_kind = Kind::ROOT; // pvr://channels/(tv|radio)
+ m_bRadio = (segment == "radio");
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Invalid channels path '{}' - channel root segment syntax error.",
+ strPath);
+ m_kind = Kind::INVALID;
+ }
+ break;
+
+ case Kind::ROOT:
+ m_kind = Kind::GROUP; // pvr://channels/(tv|radio)/<groupname>
+ m_group = CURL::Decode(segment);
+ break;
+
+ case Kind::GROUP:
+ {
+ std::vector<std::string> tokens = StringUtils::Split(segment, "_");
+ if (tokens.size() == 2)
+ {
+ std::vector<std::string> instance = StringUtils::Split(tokens[0], "@");
+ if (instance.size() == 2)
+ {
+ m_instanceID = std::atoi(instance[0].c_str());
+ m_addonID = instance[1];
+ }
+ else
+ {
+ m_instanceID = ADDON::ADDON_SINGLETON_INSTANCE_ID;
+ m_addonID = tokens[0];
+ }
+
+ tokens = StringUtils::Split(tokens[1], ".");
+ if (tokens.size() == 2 && tokens[1] == "pvr")
+ {
+ std::string channelUID = tokens[0];
+ if (!channelUID.empty() && channelUID.find_first_not_of("0123456789") == std::string::npos)
+ m_iChannelUID = std::atoi(channelUID.c_str());
+ }
+ }
+
+ if (!m_addonID.empty() && m_iChannelUID >= 0)
+ {
+ m_kind = Kind::
+ CHANNEL; // pvr://channels/(tv|radio)/<groupname>/<instanceid>@<addonid>_<channeluid>.pvr
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Invalid channels path '{}' - channel segment syntax error.",
+ strPath);
+ m_kind = Kind::INVALID;
+ }
+ break;
+ }
+
+ case Kind::CHANNEL:
+ CLog::LogF(LOGERROR, "Invalid channels path '{}' - too many path segments.", strPath);
+ m_kind = Kind::INVALID; // too many segments
+ break;
+ }
+
+ if (m_kind == Kind::INVALID)
+ break;
+ }
+
+ // append slash to all folders
+ if (m_kind < Kind::CHANNEL)
+ strVarPath.append("/");
+
+ m_path = strVarPath;
+}
+
+CPVRChannelsPath::CPVRChannelsPath(bool bRadio, bool bHidden, const std::string& strGroupName)
+ : m_bRadio(bRadio)
+{
+ if (!bHidden && strGroupName.empty())
+ m_kind = Kind::EMPTY;
+ else
+ m_kind = Kind::GROUP;
+
+ m_group = bHidden ? ".hidden" : strGroupName;
+ m_path =
+ StringUtils::Format("pvr://channels/{}/{}", bRadio ? "radio" : "tv", CURL::Encode(m_group));
+
+ if (!m_group.empty())
+ m_path.append("/");
+}
+
+CPVRChannelsPath::CPVRChannelsPath(bool bRadio, const std::string& strGroupName)
+ : m_bRadio(bRadio)
+{
+ if (strGroupName.empty())
+ m_kind = Kind::EMPTY;
+ else
+ m_kind = Kind::GROUP;
+
+ m_group = strGroupName;
+ m_path =
+ StringUtils::Format("pvr://channels/{}/{}", bRadio ? "radio" : "tv", CURL::Encode(m_group));
+
+ if (!m_group.empty())
+ m_path.append("/");
+}
+
+CPVRChannelsPath::CPVRChannelsPath(bool bRadio,
+ const std::string& strGroupName,
+ const std::string& strAddonID,
+ ADDON::AddonInstanceId instanceID,
+ int iChannelUID)
+ : m_bRadio(bRadio)
+{
+ if (!strGroupName.empty() && !strAddonID.empty() && iChannelUID >= 0)
+ {
+ m_kind = Kind::CHANNEL;
+ m_group = strGroupName;
+ m_addonID = strAddonID;
+ m_instanceID = instanceID;
+ m_iChannelUID = iChannelUID;
+ m_path = StringUtils::Format("pvr://channels/{}/{}/{}@{}_{}.pvr", bRadio ? "radio" : "tv",
+ CURL::Encode(m_group), m_instanceID, m_addonID, m_iChannelUID);
+ }
+}
+
+bool CPVRChannelsPath::IsHiddenChannelGroup() const
+{
+ return m_kind == Kind::GROUP && m_group == ".hidden";
+}
+
+std::string CPVRChannelsPath::TrimSlashes(const std::string& strString)
+{
+ std::string strTrimmed = strString;
+ while (!strTrimmed.empty() && strTrimmed.front() == '/')
+ strTrimmed.erase(0, 1);
+
+ while (!strTrimmed.empty() && strTrimmed.back() == '/')
+ strTrimmed.pop_back();
+
+ return strTrimmed;
+}
diff --git a/xbmc/pvr/channels/PVRChannelsPath.h b/xbmc/pvr/channels/PVRChannelsPath.h
new file mode 100644
index 0000000..7b825c1
--- /dev/null
+++ b/xbmc/pvr/channels/PVRChannelsPath.h
@@ -0,0 +1,73 @@
+/*
+ * 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 "addons/IAddon.h"
+
+class CDateTime;
+
+namespace PVR
+{
+ class CPVRChannelsPath
+ {
+ public:
+ static const std::string PATH_TV_CHANNELS;
+ static const std::string PATH_RADIO_CHANNELS;
+
+ explicit CPVRChannelsPath(const std::string& strPath);
+ CPVRChannelsPath(bool bRadio, const std::string& strGroupName);
+ CPVRChannelsPath(bool bRadio, bool bHidden, const std::string& strGroupName);
+ CPVRChannelsPath(bool bRadio,
+ const std::string& strGroupName,
+ const std::string& strAddonID,
+ ADDON::AddonInstanceId instanceID,
+ int iChannelUID);
+
+ operator std::string() const { return m_path; }
+ bool operator ==(const CPVRChannelsPath& right) const { return m_path == right.m_path; }
+ bool operator !=(const CPVRChannelsPath& right) const { return !(*this == right); }
+
+ bool IsValid() const { return m_kind > Kind::PROTO; }
+
+ bool IsEmpty() const { return m_kind == Kind::EMPTY; }
+ bool IsChannelsRoot() const { return m_kind == Kind::ROOT; }
+ bool IsChannelGroup() const { return m_kind == Kind::GROUP; }
+ bool IsChannel() const { return m_kind == Kind::CHANNEL; }
+
+ bool IsHiddenChannelGroup() const;
+
+ bool IsRadio() const { return m_bRadio; }
+
+ const std::string& GetGroupName() const { return m_group; }
+ const std::string& GetAddonID() const { return m_addonID; }
+ ADDON::AddonInstanceId GetInstanceID() const { return m_instanceID; }
+ int GetChannelUID() const { return m_iChannelUID; }
+
+ private:
+ static std::string TrimSlashes(const std::string& strString);
+
+ enum class Kind
+ {
+ INVALID,
+ PROTO,
+ EMPTY,
+ ROOT,
+ GROUP,
+ CHANNEL,
+ };
+
+ Kind m_kind = Kind::INVALID;
+ bool m_bRadio = false;;
+ std::string m_path;
+ std::string m_group;
+ std::string m_addonID;
+ ADDON::AddonInstanceId m_instanceID{ADDON::ADDON_SINGLETON_INSTANCE_ID};
+ int m_iChannelUID = -1;
+ };
+}
diff --git a/xbmc/pvr/channels/PVRRadioRDSInfoTag.cpp b/xbmc/pvr/channels/PVRRadioRDSInfoTag.cpp
new file mode 100644
index 0000000..e1ee649
--- /dev/null
+++ b/xbmc/pvr/channels/PVRRadioRDSInfoTag.cpp
@@ -0,0 +1,736 @@
+/*
+ * 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 "PVRRadioRDSInfoTag.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/Archive.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <mutex>
+#include <string>
+#include <utility>
+
+using namespace PVR;
+
+CPVRRadioRDSInfoTag::CPVRRadioRDSInfoTag()
+{
+ Clear();
+}
+
+void CPVRRadioRDSInfoTag::Serialize(CVariant& value) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ value["strLanguage"] = m_strLanguage;
+ value["strCountry"] = m_strCountry;
+ value["strTitle"] = m_strTitle;
+ value["strBand"] = m_strBand;
+ value["strArtist"] = m_strArtist;
+ value["strComposer"] = m_strComposer;
+ value["strConductor"] = m_strConductor;
+ value["strAlbum"] = m_strAlbum;
+ value["iAlbumTracknumber"] = m_iAlbumTracknumber;
+ value["strProgStation"] = m_strProgStation;
+ value["strProgStyle"] = m_strProgStyle;
+ value["strProgHost"] = m_strProgHost;
+ value["strProgWebsite"] = m_strProgWebsite;
+ value["strProgNow"] = m_strProgNow;
+ value["strProgNext"] = m_strProgNext;
+ value["strPhoneHotline"] = m_strPhoneHotline;
+ value["strEMailHotline"] = m_strEMailHotline;
+ value["strPhoneStudio"] = m_strPhoneStudio;
+ value["strEMailStudio"] = m_strEMailStudio;
+ value["strSMSStudio"] = m_strSMSStudio;
+ value["strRadioStyle"] = m_strRadioStyle;
+}
+
+void CPVRRadioRDSInfoTag::Archive(CArchive& ar)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (ar.IsStoring())
+ {
+ ar << m_strLanguage;
+ ar << m_strCountry;
+ ar << m_strTitle;
+ ar << m_strBand;
+ ar << m_strArtist;
+ ar << m_strComposer;
+ ar << m_strConductor;
+ ar << m_strAlbum;
+ ar << m_iAlbumTracknumber;
+ ar << m_strProgStation;
+ ar << m_strProgStyle;
+ ar << m_strProgHost;
+ ar << m_strProgWebsite;
+ ar << m_strProgNow;
+ ar << m_strProgNext;
+ ar << m_strPhoneHotline;
+ ar << m_strEMailHotline;
+ ar << m_strPhoneStudio;
+ ar << m_strEMailStudio;
+ ar << m_strSMSStudio;
+ ar << m_strRadioStyle;
+ }
+ else
+ {
+ ar >> m_strLanguage;
+ ar >> m_strCountry;
+ ar >> m_strTitle;
+ ar >> m_strBand;
+ ar >> m_strArtist;
+ ar >> m_strComposer;
+ ar >> m_strConductor;
+ ar >> m_strAlbum;
+ ar >> m_iAlbumTracknumber;
+ ar >> m_strProgStation;
+ ar >> m_strProgStyle;
+ ar >> m_strProgHost;
+ ar >> m_strProgWebsite;
+ ar >> m_strProgNow;
+ ar >> m_strProgNext;
+ ar >> m_strPhoneHotline;
+ ar >> m_strEMailHotline;
+ ar >> m_strPhoneStudio;
+ ar >> m_strEMailStudio;
+ ar >> m_strSMSStudio;
+ ar >> m_strRadioStyle;
+ }
+}
+
+bool CPVRRadioRDSInfoTag::operator==(const CPVRRadioRDSInfoTag& right) const
+{
+ if (this == &right)
+ return true;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return (
+ m_strLanguage == right.m_strLanguage && m_strCountry == right.m_strCountry &&
+ m_strTitle == right.m_strTitle && m_strBand == right.m_strBand &&
+ m_strArtist == right.m_strArtist && m_strComposer == right.m_strComposer &&
+ m_strConductor == right.m_strConductor && m_strAlbum == right.m_strAlbum &&
+ m_iAlbumTracknumber == right.m_iAlbumTracknumber && m_strInfoNews == right.m_strInfoNews &&
+ m_strInfoNewsLocal == right.m_strInfoNewsLocal && m_strInfoSport == right.m_strInfoSport &&
+ m_strInfoStock == right.m_strInfoStock && m_strInfoWeather == right.m_strInfoWeather &&
+ m_strInfoLottery == right.m_strInfoLottery && m_strInfoOther == right.m_strInfoOther &&
+ m_strProgStyle == right.m_strProgStyle && m_strProgHost == right.m_strProgHost &&
+ m_strProgStation == right.m_strProgStation && m_strProgWebsite == right.m_strProgWebsite &&
+ m_strProgNow == right.m_strProgNow && m_strProgNext == right.m_strProgNext &&
+ m_strPhoneHotline == right.m_strPhoneHotline &&
+ m_strEMailHotline == right.m_strEMailHotline && m_strPhoneStudio == right.m_strPhoneStudio &&
+ m_strEMailStudio == right.m_strEMailStudio && m_strSMSStudio == right.m_strSMSStudio &&
+ m_strRadioStyle == right.m_strRadioStyle && m_strInfoHoroscope == right.m_strInfoHoroscope &&
+ m_strInfoCinema == right.m_strInfoCinema && m_strComment == right.m_strComment &&
+ m_strEditorialStaff == right.m_strEditorialStaff && m_strRadioText == right.m_strRadioText &&
+ m_strProgramServiceText == right.m_strProgramServiceText &&
+ m_strProgramServiceLine0 == right.m_strProgramServiceLine0 &&
+ m_strProgramServiceLine1 == right.m_strProgramServiceLine1 &&
+ m_bHaveRadioText == right.m_bHaveRadioText &&
+ m_bHaveRadioTextPlus == right.m_bHaveRadioTextPlus);
+}
+
+bool CPVRRadioRDSInfoTag::operator !=(const CPVRRadioRDSInfoTag& right) const
+{
+ if (this == &right)
+ return false;
+
+ return !(*this == right);
+}
+
+void CPVRRadioRDSInfoTag::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_RDS_SpeechActive = false;
+
+ ResetSongInformation();
+
+ m_strLanguage.erase();
+ m_strCountry.erase();
+ m_strComment.erase();
+ m_strInfoNews.Clear();
+ m_strInfoNewsLocal.Clear();
+ m_strInfoSport.Clear();
+ m_strInfoStock.Clear();
+ m_strInfoWeather.Clear();
+ m_strInfoLottery.Clear();
+ m_strInfoOther.Clear();
+ m_strInfoHoroscope.Clear();
+ m_strInfoCinema.Clear();
+ m_strProgStyle.erase();
+ m_strProgHost.erase();
+ m_strProgStation.erase();
+ m_strProgWebsite.erase();
+ m_strPhoneHotline.erase();
+ m_strEMailHotline.erase();
+ m_strPhoneStudio.erase();
+ m_strEMailStudio.erase();
+ m_strSMSStudio.erase();
+ m_strRadioStyle = "unknown";
+ m_strEditorialStaff.Clear();
+ m_strRadioText.Clear();
+ m_strProgramServiceText.Clear();
+ m_strProgramServiceLine0.erase();
+ m_strProgramServiceLine1.erase();
+
+ m_bHaveRadioText = false;
+ m_bHaveRadioTextPlus = false;
+}
+
+void CPVRRadioRDSInfoTag::ResetSongInformation()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_strTitle.erase();
+ m_strBand.erase();
+ m_strArtist.erase();
+ m_strComposer.erase();
+ m_strConductor.erase();
+ m_strAlbum.erase();
+ m_iAlbumTracknumber = 0;
+}
+
+void CPVRRadioRDSInfoTag::SetSpeechActive(bool active)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_RDS_SpeechActive = active;
+}
+
+void CPVRRadioRDSInfoTag::SetLanguage(const std::string& strLanguage)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strLanguage = Trim(strLanguage);
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetLanguage() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strLanguage;
+}
+
+void CPVRRadioRDSInfoTag::SetCountry(const std::string& strCountry)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strCountry = Trim(strCountry);
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetCountry() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strCountry;
+}
+
+void CPVRRadioRDSInfoTag::SetTitle(const std::string& strTitle)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strTitle = Trim(strTitle);
+}
+
+void CPVRRadioRDSInfoTag::SetArtist(const std::string& strArtist)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strArtist = Trim(strArtist);
+}
+
+void CPVRRadioRDSInfoTag::SetBand(const std::string& strBand)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strBand = Trim(strBand);
+ g_charsetConverter.unknownToUTF8(m_strBand);
+}
+
+void CPVRRadioRDSInfoTag::SetComposer(const std::string& strComposer)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strComposer = Trim(strComposer);
+ g_charsetConverter.unknownToUTF8(m_strComposer);
+}
+
+void CPVRRadioRDSInfoTag::SetConductor(const std::string& strConductor)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strConductor = Trim(strConductor);
+ g_charsetConverter.unknownToUTF8(m_strConductor);
+}
+
+void CPVRRadioRDSInfoTag::SetAlbum(const std::string& strAlbum)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strAlbum = Trim(strAlbum);
+ g_charsetConverter.unknownToUTF8(m_strAlbum);
+}
+
+void CPVRRadioRDSInfoTag::SetAlbumTrackNumber(int track)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iAlbumTracknumber = track;
+}
+
+void CPVRRadioRDSInfoTag::SetComment(const std::string& strComment)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strComment = Trim(strComment);
+ g_charsetConverter.unknownToUTF8(m_strComment);
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetTitle() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strTitle;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetArtist() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strArtist;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetBand() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strBand;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetComposer() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strComposer;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetConductor() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strConductor;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetAlbum() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strAlbum;
+}
+
+int CPVRRadioRDSInfoTag::GetAlbumTrackNumber() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iAlbumTracknumber;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetComment() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strComment;
+}
+
+void CPVRRadioRDSInfoTag::SetInfoNews(const std::string& strNews)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoNews.Add(strNews);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoNews() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoNews.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoNewsLocal(const std::string& strNews)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoNewsLocal.Add(strNews);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoNewsLocal() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoNewsLocal.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoSport(const std::string& strSport)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoSport.Add(strSport);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoSport() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoSport.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoStock(const std::string& strStock)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoStock.Add(strStock);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoStock() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoStock.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoWeather(const std::string& strWeather)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoWeather.Add(strWeather);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoWeather() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoWeather.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoLottery(const std::string& strLottery)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoLottery.Add(strLottery);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoLottery() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoLottery.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetEditorialStaff(const std::string& strEditorialStaff)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strEditorialStaff.Add(strEditorialStaff);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetEditorialStaff() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strEditorialStaff.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoHoroscope(const std::string& strHoroscope)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoHoroscope.Add(strHoroscope);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoHoroscope() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoHoroscope.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoCinema(const std::string& strCinema)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoCinema.Add(strCinema);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoCinema() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoCinema.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetInfoOther(const std::string& strOther)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strInfoOther.Add(strOther);
+}
+
+const std::string CPVRRadioRDSInfoTag::GetInfoOther() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strInfoOther.GetText();
+}
+
+void CPVRRadioRDSInfoTag::SetProgStation(const std::string& strProgStation)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgStation = Trim(strProgStation);
+ g_charsetConverter.unknownToUTF8(m_strProgStation);
+}
+
+void CPVRRadioRDSInfoTag::SetProgHost(const std::string& strProgHost)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgHost = Trim(strProgHost);
+ g_charsetConverter.unknownToUTF8(m_strProgHost);
+}
+
+void CPVRRadioRDSInfoTag::SetProgStyle(const std::string& strProgStyle)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgStyle = Trim(strProgStyle);
+ g_charsetConverter.unknownToUTF8(m_strProgStyle);
+}
+
+void CPVRRadioRDSInfoTag::SetProgWebsite(const std::string& strWebsite)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgWebsite = Trim(strWebsite);
+ g_charsetConverter.unknownToUTF8(m_strProgWebsite);
+}
+
+void CPVRRadioRDSInfoTag::SetProgNow(const std::string& strNow)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgNow = Trim(strNow);
+ g_charsetConverter.unknownToUTF8(m_strProgNow);
+}
+
+void CPVRRadioRDSInfoTag::SetProgNext(const std::string& strNext)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgNext = Trim(strNext);
+ g_charsetConverter.unknownToUTF8(m_strProgNext);
+}
+
+void CPVRRadioRDSInfoTag::SetPhoneHotline(const std::string& strHotline)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strPhoneHotline = Trim(strHotline);
+ g_charsetConverter.unknownToUTF8(m_strPhoneHotline);
+}
+
+void CPVRRadioRDSInfoTag::SetEMailHotline(const std::string& strHotline)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strEMailHotline = Trim(strHotline);
+ g_charsetConverter.unknownToUTF8(m_strEMailHotline);
+}
+
+void CPVRRadioRDSInfoTag::SetPhoneStudio(const std::string& strPhone)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strPhoneStudio = Trim(strPhone);
+ g_charsetConverter.unknownToUTF8(m_strPhoneStudio);
+}
+
+void CPVRRadioRDSInfoTag::SetEMailStudio(const std::string& strEMail)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strEMailStudio = Trim(strEMail);
+ g_charsetConverter.unknownToUTF8(m_strEMailStudio);
+}
+
+void CPVRRadioRDSInfoTag::SetSMSStudio(const std::string& strSMS)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strSMSStudio = Trim(strSMS);
+ g_charsetConverter.unknownToUTF8(m_strSMSStudio);
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgStyle() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgStyle;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgHost() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgHost;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgStation() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgStation;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgWebsite() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgWebsite;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgNow() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgNow;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetProgNext() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProgNext;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetPhoneHotline() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strPhoneHotline;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetEMailHotline() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strEMailHotline;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetPhoneStudio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strPhoneStudio;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetEMailStudio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strEMailStudio;
+}
+
+const std::string& CPVRRadioRDSInfoTag::GetSMSStudio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strSMSStudio;
+}
+
+void CPVRRadioRDSInfoTag::SetRadioStyle(const std::string& style)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strRadioStyle = style;
+}
+
+const std::string CPVRRadioRDSInfoTag::GetRadioStyle() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strRadioStyle;
+}
+
+void CPVRRadioRDSInfoTag::SetRadioText(const std::string& strRadioText)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strRadioText.Add(strRadioText);
+}
+
+std::string CPVRRadioRDSInfoTag::GetRadioText(unsigned int line) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_strRadioText.Size() > 0)
+ {
+ return m_strRadioText.GetLine(line);
+ }
+ else if (line == 0)
+ {
+ return m_strProgramServiceLine0;
+ }
+ else if (line == 1)
+ {
+ return m_strProgramServiceLine1;
+ }
+ return {};
+}
+
+void CPVRRadioRDSInfoTag::SetProgramServiceText(const std::string& strPSText)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strProgramServiceText.Add(strPSText);
+
+ m_strProgramServiceLine0.erase();
+ for (size_t i = m_strProgramServiceText.MaxSize() / 2 + 1; i < m_strProgramServiceText.MaxSize();
+ ++i)
+ {
+ m_strProgramServiceLine0 += m_strProgramServiceText.GetLine(i);
+ m_strProgramServiceLine0 += ' ';
+ }
+
+ m_strProgramServiceLine1.erase();
+ for (size_t i = 0; i < m_strProgramServiceText.MaxSize() / 2; ++i)
+ {
+ m_strProgramServiceLine1 += m_strProgramServiceText.GetLine(i);
+ m_strProgramServiceLine1 += ' ';
+ }
+}
+
+void CPVRRadioRDSInfoTag::SetPlayingRadioText(bool yesNo)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bHaveRadioText = yesNo;
+}
+
+bool CPVRRadioRDSInfoTag::IsPlayingRadioText() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bHaveRadioText;
+}
+
+void CPVRRadioRDSInfoTag::SetPlayingRadioTextPlus(bool yesNo)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bHaveRadioTextPlus = yesNo;
+}
+
+bool CPVRRadioRDSInfoTag::IsPlayingRadioTextPlus() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bHaveRadioTextPlus;
+}
+
+std::string CPVRRadioRDSInfoTag::Trim(const std::string& value)
+{
+ std::string trimmedValue(value);
+ StringUtils::TrimLeft(trimmedValue);
+ StringUtils::TrimRight(trimmedValue, " \n\r");
+ return trimmedValue;
+}
+
+bool CPVRRadioRDSInfoTag::Info::operator==(const CPVRRadioRDSInfoTag::Info& right) const
+{
+ if (this == &right)
+ return true;
+
+ return (m_infoText == right.m_infoText && m_data == right.m_data &&
+ m_maxSize == right.m_maxSize && m_prependData == right.m_prependData);
+}
+
+void CPVRRadioRDSInfoTag::Info::Clear()
+{
+ m_data.clear();
+ m_infoText.clear();
+}
+
+void CPVRRadioRDSInfoTag::Info::Add(const std::string& text)
+{
+ std::string tmp = Trim(text);
+ g_charsetConverter.unknownToUTF8(tmp);
+
+ if (std::find(m_data.begin(), m_data.end(), tmp) != m_data.end())
+ return;
+
+ if (m_data.size() >= m_maxSize)
+ {
+ if (m_prependData)
+ m_data.pop_back();
+ else
+ m_data.pop_front();
+ }
+
+ if (m_prependData)
+ m_data.emplace_front(std::move(tmp));
+ else
+ m_data.emplace_back(std::move(tmp));
+
+ m_infoText.clear();
+ for (const std::string& data : m_data)
+ {
+ m_infoText += data;
+ m_infoText += '\n';
+ }
+
+ // send a message to all windows to tell them to update the radiotext
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_RADIOTEXT);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
diff --git a/xbmc/pvr/channels/PVRRadioRDSInfoTag.h b/xbmc/pvr/channels/PVRRadioRDSInfoTag.h
new file mode 100644
index 0000000..bc66989
--- /dev/null
+++ b/xbmc/pvr/channels/PVRRadioRDSInfoTag.h
@@ -0,0 +1,208 @@
+/*
+ * 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 "threads/CriticalSection.h"
+#include "utils/IArchivable.h"
+#include "utils/ISerializable.h"
+
+#include <deque>
+#include <string>
+
+namespace PVR
+{
+
+class CPVRRadioRDSInfoTag final : public IArchivable, public ISerializable
+{
+public:
+ CPVRRadioRDSInfoTag();
+
+ bool operator ==(const CPVRRadioRDSInfoTag& right) const;
+ bool operator !=(const CPVRRadioRDSInfoTag& right) const;
+
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& value) const override;
+
+ void Clear();
+ void ResetSongInformation();
+
+ /**! Basic RDS related information */
+ void SetSpeechActive(bool active);
+ void SetLanguage(const std::string& strLanguage);
+ const std::string& GetLanguage() const;
+ void SetCountry(const std::string& strCountry);
+ const std::string& GetCountry() const;
+ void SetRadioText(const std::string& strRadioText);
+ std::string GetRadioText(unsigned int line) const;
+ void SetProgramServiceText(const std::string& strPSText);
+
+ /**! RDS RadioText related information */
+ void SetTitle(const std::string& strTitle);
+ void SetBand(const std::string& strBand);
+ void SetArtist(const std::string& strArtist);
+ void SetComposer(const std::string& strComposer);
+ void SetConductor(const std::string& strConductor);
+ void SetAlbum(const std::string& strAlbum);
+ void SetComment(const std::string& strComment);
+ void SetAlbumTrackNumber(int track);
+
+ const std::string& GetTitle() const;
+ const std::string& GetBand() const;
+ const std::string& GetArtist() const;
+ const std::string& GetComposer() const;
+ const std::string& GetConductor() const;
+ const std::string& GetAlbum() const;
+ const std::string& GetComment() const;
+ int GetAlbumTrackNumber() const;
+
+ void SetProgStation(const std::string& strProgStation);
+ void SetProgStyle(const std::string& strProgStyle);
+ void SetProgHost(const std::string& strProgHost);
+ void SetProgWebsite(const std::string& strWebsite);
+ void SetProgNow(const std::string& strNow);
+ void SetProgNext(const std::string& strNext);
+ void SetPhoneHotline(const std::string& strHotline);
+ void SetEMailHotline(const std::string& strHotline);
+ void SetPhoneStudio(const std::string& strPhone);
+ void SetEMailStudio(const std::string& strEMail);
+ void SetSMSStudio(const std::string& strSMS);
+
+ const std::string& GetProgStation() const;
+ const std::string& GetProgStyle() const;
+ const std::string& GetProgHost() const;
+ const std::string& GetProgWebsite() const;
+ const std::string& GetProgNow() const;
+ const std::string& GetProgNext() const;
+ const std::string& GetPhoneHotline() const;
+ const std::string& GetEMailHotline() const;
+ const std::string& GetPhoneStudio() const;
+ const std::string& GetEMailStudio() const;
+ const std::string& GetSMSStudio() const;
+
+ void SetInfoNews(const std::string& strNews);
+ const std::string GetInfoNews() const;
+
+ void SetInfoNewsLocal(const std::string& strNews);
+ const std::string GetInfoNewsLocal() const;
+
+ void SetInfoSport(const std::string& strSport);
+ const std::string GetInfoSport() const;
+
+ void SetInfoStock(const std::string& strSport);
+ const std::string GetInfoStock() const;
+
+ void SetInfoWeather(const std::string& strWeather);
+ const std::string GetInfoWeather() const;
+
+ void SetInfoHoroscope(const std::string& strHoroscope);
+ const std::string GetInfoHoroscope() const;
+
+ void SetInfoCinema(const std::string& strCinema);
+ const std::string GetInfoCinema() const;
+
+ void SetInfoLottery(const std::string& strLottery);
+ const std::string GetInfoLottery() const;
+
+ void SetInfoOther(const std::string& strOther);
+ const std::string GetInfoOther() const;
+
+ void SetEditorialStaff(const std::string& strEditorialStaff);
+ const std::string GetEditorialStaff() const;
+
+ void SetRadioStyle(const std::string& style);
+ const std::string GetRadioStyle() const;
+
+ void SetPlayingRadioText(bool yesNo);
+ bool IsPlayingRadioText() const;
+
+ void SetPlayingRadioTextPlus(bool yesNo);
+ bool IsPlayingRadioTextPlus() const;
+
+private:
+ CPVRRadioRDSInfoTag(const CPVRRadioRDSInfoTag& tag) = delete;
+ const CPVRRadioRDSInfoTag& operator =(const CPVRRadioRDSInfoTag& tag) = delete;
+
+ static std::string Trim(const std::string& value);
+
+ mutable CCriticalSection m_critSection;
+
+ bool m_RDS_SpeechActive;
+
+ std::string m_strLanguage;
+ std::string m_strCountry;
+ std::string m_strTitle;
+ std::string m_strBand;
+ std::string m_strArtist;
+ std::string m_strComposer;
+ std::string m_strConductor;
+ std::string m_strAlbum;
+ std::string m_strComment;
+ int m_iAlbumTracknumber;
+ std::string m_strRadioStyle;
+
+ class Info
+ {
+ public:
+ Info() = delete;
+ Info(size_t maxSize, bool prependData) : m_maxSize(maxSize), m_prependData(prependData) {}
+
+ bool operator==(const Info& right) const;
+
+ size_t Size() const { return m_data.size(); }
+ size_t MaxSize() const { return m_maxSize; }
+
+ void Clear();
+ void Add(const std::string& text);
+
+ const std::string& GetText() const { return m_infoText; }
+ std::string GetLine(unsigned int line) const
+ {
+ return line < m_data.size() ? m_data.at(line) : "";
+ }
+
+ private:
+ const size_t m_maxSize = 10;
+ const bool m_prependData = false;
+ std::deque<std::string> m_data;
+ std::string m_infoText;
+ };
+
+ Info m_strInfoNews{10, false};
+ Info m_strInfoNewsLocal{10, false};
+ Info m_strInfoSport{10, false};
+ Info m_strInfoStock{10, false};
+ Info m_strInfoWeather{10, false};
+ Info m_strInfoLottery{10, false};
+ Info m_strInfoOther{10, false};
+ Info m_strInfoHoroscope{10, false};
+ Info m_strInfoCinema{10, false};
+ Info m_strEditorialStaff{10, false};
+
+ Info m_strRadioText{6, true};
+
+ Info m_strProgramServiceText{12, false};
+ std::string m_strProgramServiceLine0;
+ std::string m_strProgramServiceLine1;
+
+ std::string m_strProgStyle;
+ std::string m_strProgHost;
+ std::string m_strProgStation;
+ std::string m_strProgWebsite;
+ std::string m_strProgNow;
+ std::string m_strProgNext;
+ std::string m_strPhoneHotline;
+ std::string m_strEMailHotline;
+ std::string m_strPhoneStudio;
+ std::string m_strEMailStudio;
+ std::string m_strSMSStudio;
+
+ bool m_bHaveRadioText;
+ bool m_bHaveRadioTextPlus;
+};
+}
diff --git a/xbmc/pvr/channels/test/CMakeLists.txt b/xbmc/pvr/channels/test/CMakeLists.txt
new file mode 100644
index 0000000..d88bab8
--- /dev/null
+++ b/xbmc/pvr/channels/test/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES TestPVRChannelsPath.cpp)
+set(HEADERS)
+
+core_add_test_library(pvrchannels_test)
diff --git a/xbmc/pvr/channels/test/TestPVRChannelsPath.cpp b/xbmc/pvr/channels/test/TestPVRChannelsPath.cpp
new file mode 100644
index 0000000..c7232db
--- /dev/null
+++ b/xbmc/pvr/channels/test/TestPVRChannelsPath.cpp
@@ -0,0 +1,404 @@
+/*
+ * 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 "pvr/channels/PVRChannelsPath.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestPVRChannelsPath, Parse_Protocol)
+{
+ // pvr protocol is generally fine, but not sufficient for channels pvr paths - component is missing for that.
+ PVR::CPVRChannelsPath path("pvr://");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Component_1)
+{
+ PVR::CPVRChannelsPath path("pvr://channels");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_TRUE(path.IsEmpty());
+}
+
+TEST(TestPVRChannelsPath, Parse_Component_2)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_TRUE(path.IsEmpty());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_Component)
+{
+ PVR::CPVRChannelsPath path("pvr://foo/");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_TV_Root_1)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_TRUE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_TV_Root_2)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_TRUE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Radio_Root_1)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/radio");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_TRUE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Radio_Root_2)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/radio/");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_TRUE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_Root)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/foo");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_TV_Group_1)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_TV_Group_2)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/Group1/");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Hidden_TV_Group)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/.hidden");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/.hidden/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_TRUE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), ".hidden");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Special_TV_Group)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/foo%2Fbar%20baz");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/foo%2Fbar%20baz/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "foo/bar baz");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_Special_TV_Group)
+{
+ // special chars in group name not escaped
+ PVR::CPVRChannelsPath path("pvr://channels/tv/foo/bar baz");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Radio_Group)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/Group1/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Parse_TV_Channel)
+{
+ PVR::CPVRChannelsPath path("pvr://channels/tv/Group1/5@pvr.demo_4711.pvr");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/5@pvr.demo_4711.pvr");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_TRUE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "pvr.demo");
+ EXPECT_EQ(path.GetInstanceID(), 5);
+ EXPECT_EQ(path.GetChannelUID(), 4711);
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_1)
+{
+ // trailing ".pvr" missing
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_4711");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_2)
+{
+ // '-' instead of '_' as clientid / channeluid delimiter
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo-4711.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_3)
+{
+ // channeluid not numerical
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_abc4711.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_4)
+{
+ // channeluid not positive or zero
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_-4711.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_5)
+{
+ // empty clientid
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/_4711.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_6)
+{
+ // empty channeluid
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/pvr.demo_.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_7)
+{
+ // empty clientid and empty channeluid
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/_.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, Parse_Invalid_TV_Channel_8)
+{
+ // empty clientid and empty channeluid, only extension ".pvr" given
+ PVR::CPVRChannelsPath path("pvr://channels/radio/Group1/.pvr");
+
+ EXPECT_FALSE(path.IsValid());
+}
+
+TEST(TestPVRChannelsPath, TV_Channelgroup)
+{
+ PVR::CPVRChannelsPath path(false, "Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Radio_Channelgroup)
+{
+ PVR::CPVRChannelsPath path(true, "Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/Group1/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Hidden_TV_Channelgroup)
+{
+ PVR::CPVRChannelsPath path(false, true, "Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/.hidden/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_TRUE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), ".hidden");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, Hidden_Radio_Channelgroup)
+{
+ PVR::CPVRChannelsPath path(true, true, "Group1");
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/.hidden/");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_TRUE(path.IsChannelGroup());
+ EXPECT_TRUE(path.IsHiddenChannelGroup());
+ EXPECT_FALSE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), ".hidden");
+ EXPECT_EQ(path.GetAddonID(), "");
+ EXPECT_EQ(path.GetChannelUID(), -1);
+}
+
+TEST(TestPVRChannelsPath, TV_Channel)
+{
+ PVR::CPVRChannelsPath path(false, "Group1", "pvr.demo", ADDON::ADDON_SINGLETON_INSTANCE_ID, 4711);
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/tv/Group1/0@pvr.demo_4711.pvr");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_FALSE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_TRUE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "pvr.demo");
+ EXPECT_EQ(path.GetChannelUID(), 4711);
+}
+
+TEST(TestPVRChannelsPath, Radio_Channel)
+{
+ PVR::CPVRChannelsPath path(true, "Group1", "pvr.demo", ADDON::ADDON_SINGLETON_INSTANCE_ID, 4711);
+
+ EXPECT_EQ(static_cast<std::string>(path), "pvr://channels/radio/Group1/0@pvr.demo_4711.pvr");
+ EXPECT_TRUE(path.IsValid());
+ EXPECT_FALSE(path.IsEmpty());
+ EXPECT_TRUE(path.IsRadio());
+ EXPECT_FALSE(path.IsChannelsRoot());
+ EXPECT_FALSE(path.IsChannelGroup());
+ EXPECT_FALSE(path.IsHiddenChannelGroup());
+ EXPECT_TRUE(path.IsChannel());
+ EXPECT_EQ(path.GetGroupName(), "Group1");
+ EXPECT_EQ(path.GetAddonID(), "pvr.demo");
+ EXPECT_EQ(path.GetChannelUID(), 4711);
+}
+
+TEST(TestPVRChannelsPath, Operator_Equals)
+{
+ PVR::CPVRChannelsPath path2(true, "Group1");
+ PVR::CPVRChannelsPath path(static_cast<std::string>(path2));
+
+ EXPECT_EQ(path, path2);
+}
diff --git a/xbmc/pvr/dialogs/CMakeLists.txt b/xbmc/pvr/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..39b88ed
--- /dev/null
+++ b/xbmc/pvr/dialogs/CMakeLists.txt
@@ -0,0 +1,29 @@
+set(SOURCES GUIDialogPVRChannelManager.cpp
+ GUIDialogPVRChannelsOSD.cpp
+ GUIDialogPVRGroupManager.cpp
+ GUIDialogPVRGuideInfo.cpp
+ GUIDialogPVRChannelGuide.cpp
+ GUIDialogPVRGuideControls.cpp
+ GUIDialogPVRGuideSearch.cpp
+ GUIDialogPVRRadioRDSInfo.cpp
+ GUIDialogPVRRecordingInfo.cpp
+ GUIDialogPVRRecordingSettings.cpp
+ GUIDialogPVRTimerSettings.cpp
+ GUIDialogPVRClientPriorities.cpp
+ GUIDialogPVRItemsViewBase.cpp)
+
+set(HEADERS GUIDialogPVRChannelManager.h
+ GUIDialogPVRChannelsOSD.h
+ GUIDialogPVRGroupManager.h
+ GUIDialogPVRGuideInfo.h
+ GUIDialogPVRChannelGuide.h
+ GUIDialogPVRGuideControls.h
+ GUIDialogPVRGuideSearch.h
+ GUIDialogPVRRadioRDSInfo.h
+ GUIDialogPVRRecordingInfo.h
+ GUIDialogPVRRecordingSettings.h
+ GUIDialogPVRTimerSettings.h
+ GUIDialogPVRClientPriorities.h
+ GUIDialogPVRItemsViewBase.h)
+
+core_add_library(pvr_dialogs)
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.cpp b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.cpp
new file mode 100644
index 0000000..10b2623
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.cpp
@@ -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 "GUIDialogPVRChannelGuide.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/epg/EpgInfoTag.h"
+
+#include <memory>
+#include <vector>
+
+using namespace PVR;
+
+CGUIDialogPVRChannelGuide::CGUIDialogPVRChannelGuide()
+ : CGUIDialogPVRItemsViewBase(WINDOW_DIALOG_PVR_CHANNEL_GUIDE, "DialogPVRChannelGuide.xml")
+{
+}
+
+void CGUIDialogPVRChannelGuide::Open(const std::shared_ptr<CPVRChannel>& channel)
+{
+ m_channel = channel;
+ CGUIDialogPVRItemsViewBase::Open();
+}
+
+void CGUIDialogPVRChannelGuide::OnInitWindow()
+{
+ // no user-specific channel is set; use current playing channel
+ if (!m_channel)
+ m_channel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+
+ if (!m_channel)
+ {
+ Close();
+ return;
+ }
+
+ Init();
+
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags = m_channel->GetEpgTags();
+ for (const auto& tag : tags)
+ {
+ m_vecItems->Add(std::make_shared<CFileItem>(tag));
+ }
+
+ m_viewControl.SetItems(*m_vecItems);
+
+ CGUIDialogPVRItemsViewBase::OnInitWindow();
+
+ // select the active entry
+ unsigned int iSelectedItem = 0;
+ for (int iEpgPtr = 0; iEpgPtr < m_vecItems->Size(); ++iEpgPtr)
+ {
+ const CFileItemPtr entry = m_vecItems->Get(iEpgPtr);
+ if (entry->HasEPGInfoTag() && entry->GetEPGInfoTag()->IsActive())
+ {
+ iSelectedItem = iEpgPtr;
+ break;
+ }
+ }
+ m_viewControl.SetSelectedItem(iSelectedItem);
+}
+
+void CGUIDialogPVRChannelGuide::OnDeinitWindow(int nextWindowID)
+{
+ CGUIDialogPVRItemsViewBase::OnDeinitWindow(nextWindowID);
+ m_channel.reset();
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.h b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.h
new file mode 100644
index 0000000..b348a9e
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelGuide.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "pvr/dialogs/GUIDialogPVRItemsViewBase.h"
+
+#include <memory>
+
+namespace PVR
+{
+ class CPVRChannel;
+
+ class CGUIDialogPVRChannelGuide : public CGUIDialogPVRItemsViewBase
+ {
+ public:
+ CGUIDialogPVRChannelGuide();
+ ~CGUIDialogPVRChannelGuide() override = default;
+
+ void Open(const std::shared_ptr<CPVRChannel>& channel);
+
+ protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ private:
+ std::shared_ptr<CPVRChannel> m_channel;
+ };
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.cpp b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.cpp
new file mode 100644
index 0000000..267b3c6
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.cpp
@@ -0,0 +1,1076 @@
+/*
+ * 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 "GUIDialogPVRChannelManager.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUISpinControlEx.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "profiles/ProfileManager.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/dialogs/GUIDialogPVRGroupManager.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#define BUTTON_OK 4
+#define BUTTON_APPLY 5
+#define BUTTON_CANCEL 6
+#define RADIOBUTTON_ACTIVE 7
+#define EDIT_NAME 8
+#define BUTTON_CHANNEL_LOGO 9
+#define IMAGE_CHANNEL_LOGO 10
+#define RADIOBUTTON_USEEPG 12
+#define SPIN_EPGSOURCE_SELECTION 13
+#define RADIOBUTTON_PARENTAL_LOCK 14
+#define CONTROL_LIST_CHANNELS 20
+#define BUTTON_GROUP_MANAGER 30
+#define BUTTON_NEW_CHANNEL 31
+#define BUTTON_RADIO_TV 34
+#define BUTTON_REFRESH_LOGOS 35
+
+namespace
+{
+constexpr const char* LABEL_CHANNEL_DISABLED = "0";
+
+// Note: strings must not be changed; they are part of the public skinning API for this dialog.
+constexpr const char* PROPERTY_CHANNEL_NUMBER = "Number";
+constexpr const char* PROPERTY_CHANNEL_ENABLED = "ActiveChannel";
+constexpr const char* PROPERTY_CHANNEL_USER_SET_HIDDEN = "UserSetHidden";
+constexpr const char* PROPERTY_CHANNEL_LOCKED = "ParentalLocked";
+constexpr const char* PROPERTY_CHANNEL_ICON = "Icon";
+constexpr const char* PROPERTY_CHANNEL_CUSTOM_ICON = "UserSetIcon";
+constexpr const char* PROPERTY_CHANNEL_NAME = "Name";
+constexpr const char* PROPERTY_CHANNEL_EPG_ENABLED = "UseEPG";
+constexpr const char* PROPERTY_CHANNEL_EPG_SOURCE = "EPGSource";
+constexpr const char* PROPERTY_CLIENT_SUPPORTS_SETTINGS = "SupportsSettings";
+constexpr const char* PROPERTY_CLIENT_NAME = "ClientName";
+constexpr const char* PROPERTY_ITEM_CHANGED = "Changed";
+
+} // namespace
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+CGUIDialogPVRChannelManager::CGUIDialogPVRChannelManager() :
+ CGUIDialog(WINDOW_DIALOG_PVR_CHANNEL_MANAGER, "DialogPVRChannelManager.xml"),
+ m_channelItems(new CFileItemList)
+{
+ SetRadio(false);
+}
+
+CGUIDialogPVRChannelManager::~CGUIDialogPVRChannelManager()
+{
+ delete m_channelItems;
+}
+
+bool CGUIDialogPVRChannelManager::OnActionMove(const CAction& action)
+{
+ bool bReturn(false);
+ int iActionId = action.GetID();
+
+ if (GetFocusedControlID() == CONTROL_LIST_CHANNELS)
+ {
+ if (iActionId == ACTION_MOUSE_MOVE)
+ {
+ int iSelected = m_viewControl.GetSelectedItem();
+ if (m_iSelected < iSelected)
+ {
+ iActionId = ACTION_MOVE_DOWN;
+ }
+ else if (m_iSelected > iSelected)
+ {
+ iActionId = ACTION_MOVE_UP;
+ }
+ else
+ {
+ return bReturn;
+ }
+ }
+
+ if (iActionId == ACTION_MOVE_DOWN || iActionId == ACTION_MOVE_UP ||
+ iActionId == ACTION_PAGE_DOWN || iActionId == ACTION_PAGE_UP ||
+ iActionId == ACTION_FIRST_PAGE || iActionId == ACTION_LAST_PAGE)
+ {
+ CGUIDialog::OnAction(action);
+ int iSelected = m_viewControl.GetSelectedItem();
+
+ bReturn = true;
+ if (!m_bMovingMode)
+ {
+ if (iSelected != m_iSelected)
+ {
+ m_iSelected = iSelected;
+ SetData(m_iSelected);
+ }
+ }
+ else
+ {
+ bool bMoveUp = iActionId == ACTION_PAGE_UP || iActionId == ACTION_MOVE_UP || iActionId == ACTION_FIRST_PAGE;
+ unsigned int iLines = bMoveUp ? abs(m_iSelected - iSelected) : 1;
+ bool bOutOfBounds = bMoveUp ? m_iSelected <= 0 : m_iSelected >= m_channelItems->Size() - 1;
+ if (bOutOfBounds)
+ {
+ bMoveUp = !bMoveUp;
+ iLines = m_channelItems->Size() - 1;
+ }
+ for (unsigned int iLine = 0; iLine < iLines; ++iLine)
+ {
+ unsigned int iNewSelect = bMoveUp ? m_iSelected - 1 : m_iSelected + 1;
+
+ const CFileItemPtr newItem = m_channelItems->Get(iNewSelect);
+ const std::string number = newItem->GetProperty(PROPERTY_CHANNEL_NUMBER).asString();
+ if (number != LABEL_CHANNEL_DISABLED)
+ {
+ // Swap channel numbers
+ const CFileItemPtr item = m_channelItems->Get(m_iSelected);
+ newItem->SetProperty(PROPERTY_CHANNEL_NUMBER,
+ item->GetProperty(PROPERTY_CHANNEL_NUMBER));
+ SetItemChanged(newItem);
+ item->SetProperty(PROPERTY_CHANNEL_NUMBER, number);
+ SetItemChanged(item);
+ }
+
+ // swap items
+ m_channelItems->Swap(iNewSelect, m_iSelected);
+ m_iSelected = iNewSelect;
+ }
+
+ m_viewControl.SetItems(*m_channelItems);
+ m_viewControl.SetSelectedItem(m_iSelected);
+ }
+ }
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRChannelManager::OnAction(const CAction& action)
+{
+ return OnActionMove(action) ||
+ CGUIDialog::OnAction(action);
+}
+
+void CGUIDialogPVRChannelManager::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ m_iSelected = 0;
+ m_bMovingMode = false;
+ m_bAllowNewChannel = false;
+
+ EnableChannelOptions(false);
+ CONTROL_DISABLE(BUTTON_APPLY);
+
+ // prevent resorting channels if backend channel numbers or backend channel order shall be used
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ m_bAllowRenumber = !settings->GetBool(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS);
+ m_bAllowReorder =
+ m_bAllowRenumber && !settings->GetBool(CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER);
+
+ Update();
+
+ if (m_initialSelection)
+ {
+ // set initial selection
+ const std::shared_ptr<CPVRChannel> channel = m_initialSelection->GetPVRChannelInfoTag();
+ for (int i = 0; i < m_channelItems->Size(); ++i)
+ {
+ if (m_channelItems->Get(i)->GetPVRChannelInfoTag() == channel)
+ {
+ m_iSelected = i;
+ m_viewControl.SetSelectedItem(m_iSelected);
+ break;
+ }
+ }
+ m_initialSelection.reset();
+ }
+ SetData(m_iSelected);
+}
+
+void CGUIDialogPVRChannelManager::OnDeinitWindow(int nextWindowID)
+{
+ Clear();
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIDialogPVRChannelManager::SetRadio(bool bIsRadio)
+{
+ m_bIsRadio = bIsRadio;
+ SetProperty("IsRadio", m_bIsRadio ? "true" : "");
+}
+
+void CGUIDialogPVRChannelManager::Open(const std::shared_ptr<CFileItem>& initialSelection)
+{
+ m_initialSelection = initialSelection;
+ CGUIDialog::Open();
+}
+
+bool CGUIDialogPVRChannelManager::OnClickListChannels(const CGUIMessage& message)
+{
+ if (!m_bMovingMode)
+ {
+ int iAction = message.GetParam1();
+ int iItem = m_viewControl.GetSelectedItem();
+
+ /* Check file item is in list range and get his pointer */
+ if (iItem < 0 || iItem >= m_channelItems->Size()) return true;
+
+ /* Process actions */
+ if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_CONTEXT_MENU || iAction == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ /* Show Contextmenu */
+ OnPopupMenu(iItem);
+
+ return true;
+ }
+ }
+ else
+ {
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (pItem)
+ {
+ pItem->Select(false);
+ m_bMovingMode = false;
+ SetItemChanged(pItem);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonOK()
+{
+ SaveList();
+ Close();
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonApply()
+{
+ SaveList();
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonCancel()
+{
+ Close();
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonRadioTV()
+{
+ PromptAndSaveList();
+
+ m_iSelected = 0;
+ m_bMovingMode = false;
+ m_bAllowNewChannel = false;
+ m_bIsRadio = !m_bIsRadio;
+ SetProperty("IsRadio", m_bIsRadio ? "true" : "");
+ Update();
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonRadioActive()
+{
+ CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), RADIOBUTTON_ACTIVE);
+ if (OnMessage(msg))
+ {
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (pItem)
+ {
+ const bool selected = (msg.GetParam1() == 1);
+ if (pItem->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean() != selected)
+ {
+ pItem->SetProperty(PROPERTY_CHANNEL_ENABLED, selected);
+ pItem->SetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN, true);
+ SetItemChanged(pItem);
+ Renumber();
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonRadioParentalLocked()
+{
+ CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), RADIOBUTTON_PARENTAL_LOCK);
+ if (!OnMessage(msg))
+ return false;
+
+ bool selected(msg.GetParam1() == 1);
+
+ // ask for PIN first
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalPIN() !=
+ ParentalCheckResult::SUCCESS)
+ { // failed - reset to previous
+ SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_PARENTAL_LOCK, !selected);
+ return false;
+ }
+
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (pItem)
+ {
+ if (pItem->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean() != selected)
+ {
+ pItem->SetProperty(PROPERTY_CHANNEL_LOCKED, selected);
+ SetItemChanged(pItem);
+ Renumber();
+ }
+ return true;
+ }
+
+ return false;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonEditName()
+{
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), EDIT_NAME);
+ if (OnMessage(msg))
+ {
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (pItem)
+ {
+ if (pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString() != msg.GetLabel())
+ {
+ pItem->SetProperty(PROPERTY_CHANNEL_NAME, msg.GetLabel());
+ SetItemChanged(pItem);
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonChannelLogo()
+{
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (!pItem)
+ return false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (profileManager->GetCurrentProfile().canWriteSources() && !g_passwordManager.IsProfileLockUnlocked())
+ return false;
+
+ // setup our thumb list
+ CFileItemList items;
+
+ // add the current thumb, if available
+ if (!pItem->GetProperty(PROPERTY_CHANNEL_ICON).asString().empty())
+ {
+ CFileItemPtr current(new CFileItem("thumb://Current", false));
+ current->SetArt("thumb", pItem->GetPVRChannelInfoTag()->IconPath());
+ current->SetLabel(g_localizeStrings.Get(19282));
+ items.Add(current);
+ }
+ else if (pItem->HasArt("thumb"))
+ { // already have a thumb that the share doesn't know about - must be a local one, so we mayaswell reuse it.
+ CFileItemPtr current(new CFileItem("thumb://Current", false));
+ current->SetArt("thumb", pItem->GetArt("thumb"));
+ current->SetLabel(g_localizeStrings.Get(19282));
+ items.Add(current);
+ }
+
+ // and add a "no thumb" entry as well
+ CFileItemPtr nothumb(new CFileItem("thumb://None", false));
+ nothumb->SetArt("icon", pItem->GetArt("icon"));
+ nothumb->SetLabel(g_localizeStrings.Get(19283));
+ items.Add(nothumb);
+
+ std::string strThumb;
+ VECSOURCES shares;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetString(CSettings::SETTING_PVRMENU_ICONPATH) != "")
+ {
+ CMediaSource share1;
+ share1.strPath = settings->GetString(CSettings::SETTING_PVRMENU_ICONPATH);
+ share1.strName = g_localizeStrings.Get(19066);
+ shares.push_back(share1);
+ }
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ if (!CGUIDialogFileBrowser::ShowAndGetImage(items, shares, g_localizeStrings.Get(19285), strThumb, NULL, 19285))
+ return false;
+
+ if (strThumb == "thumb://Current")
+ return true;
+
+ if (strThumb == "thumb://None")
+ strThumb = "";
+
+ if (pItem->GetProperty(PROPERTY_CHANNEL_ICON).asString() != strThumb)
+ {
+ pItem->SetProperty(PROPERTY_CHANNEL_ICON, strThumb);
+ pItem->SetProperty(PROPERTY_CHANNEL_CUSTOM_ICON, true);
+ SetItemChanged(pItem);
+ }
+
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonUseEPG()
+{
+ CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), RADIOBUTTON_USEEPG);
+ if (OnMessage(msg))
+ {
+ CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ if (pItem)
+ {
+ const bool selected = (msg.GetParam1() == 1);
+ if (pItem->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean() != selected)
+ {
+ pItem->SetProperty(PROPERTY_CHANNEL_EPG_ENABLED, selected);
+ SetItemChanged(pItem);
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickEPGSourceSpin()
+{
+ //! @todo Add EPG scraper support
+ return true;
+ // CGUISpinControlEx* pSpin = static_cast<CGUISpinControlEx*>(GetControl(SPIN_EPGSOURCE_SELECTION));
+ // if (pSpin)
+ // {
+ // CFileItemPtr pItem = m_channelItems->Get(m_iSelected);
+ // if (pItem)
+ // {
+ // if (pItem->GetProperty(PROPERTY_CHANNEL_EPG_SOURCE).asInteger() != 0)
+ // {
+ // pItem->SetProperty(PROPERTY_CHANNEL_EPG_SOURCE, static_cast<int>(0));
+ // SetItemChanged(pItem);
+ // }
+ // return true;
+ // }
+ // }
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonGroupManager()
+{
+ PromptAndSaveList();
+
+ /* Load group manager dialog */
+ CGUIDialogPVRGroupManager* pDlgInfo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGroupManager>(WINDOW_DIALOG_PVR_GROUP_MANAGER);
+ if (!pDlgInfo)
+ return false;
+
+ pDlgInfo->SetRadio(m_bIsRadio);
+
+ /* Open dialog window */
+ pDlgInfo->Open();
+
+ Update();
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonNewChannel()
+{
+ PromptAndSaveList();
+
+ int iSelection = 0;
+ if (m_clientsWithSettingsList.size() > 1)
+ {
+ CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (!pDlgSelect)
+ return false;
+
+ pDlgSelect->SetHeading(CVariant{19213}); // Select Client
+
+ for (const auto& client : m_clientsWithSettingsList)
+ pDlgSelect->Add(client->Name());
+ pDlgSelect->Open();
+
+ iSelection = pDlgSelect->GetSelectedItem();
+ }
+
+ if (iSelection >= 0 && iSelection < static_cast<int>(m_clientsWithSettingsList.size()))
+ {
+ int iClientID = m_clientsWithSettingsList[iSelection]->GetID();
+
+ std::shared_ptr<CPVRChannel> channel(new CPVRChannel(m_bIsRadio));
+ channel->SetChannelName(g_localizeStrings.Get(19204)); // New channel
+ channel->SetClientID(iClientID);
+
+ PVR_ERROR ret = PVR_ERROR_UNKNOWN;
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(iClientID);
+ if (client)
+ {
+ channel->SetEPGEnabled(client->GetClientCapabilities().SupportsEPG());
+ ret = client->OpenDialogChannelAdd(channel);
+ }
+
+ if (ret == PVR_ERROR_NO_ERROR)
+ {
+ CFileItemList prevChannelItems;
+ prevChannelItems.Assign(*m_channelItems);
+
+ Update();
+
+ for (int index = 0; index < m_channelItems->Size(); ++index)
+ {
+ if (!prevChannelItems.Contains(m_channelItems->Get(index)->GetPath()))
+ {
+ m_iSelected = index;
+ m_viewControl.SetSelectedItem(m_iSelected);
+ SetData(m_iSelected);
+ break;
+ }
+ }
+ }
+ else if (ret == PVR_ERROR_NOT_IMPLEMENTED)
+ HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19038}); // "Information", "Not supported by the PVR backend."
+ else
+ HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // "Add-on error", "Check the log for more information about this message."
+ }
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnClickButtonRefreshChannelLogos()
+{
+ for (const auto& item : *m_channelItems)
+ {
+ const std::string thumb = item->GetArt("thumb");
+ if (!thumb.empty())
+ {
+ // clear current cached image
+ CServiceBroker::GetTextureCache()->ClearCachedImage(thumb);
+ item->SetArt("thumb", "");
+ }
+ }
+
+ m_iSelected = 0;
+ Update();
+
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_REFRESH_THUMBS);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ return true;
+}
+
+bool CGUIDialogPVRChannelManager::OnMessageClick(const CGUIMessage& message)
+{
+ int iControl = message.GetSenderId();
+ switch(iControl)
+ {
+ case CONTROL_LIST_CHANNELS:
+ return OnClickListChannels(message);
+ case BUTTON_OK:
+ return OnClickButtonOK();
+ case BUTTON_APPLY:
+ return OnClickButtonApply();
+ case BUTTON_CANCEL:
+ return OnClickButtonCancel();
+ case BUTTON_RADIO_TV:
+ return OnClickButtonRadioTV();
+ case RADIOBUTTON_ACTIVE:
+ return OnClickButtonRadioActive();
+ case RADIOBUTTON_PARENTAL_LOCK:
+ return OnClickButtonRadioParentalLocked();
+ case EDIT_NAME:
+ return OnClickButtonEditName();
+ case BUTTON_CHANNEL_LOGO:
+ return OnClickButtonChannelLogo();
+ case RADIOBUTTON_USEEPG:
+ return OnClickButtonUseEPG();
+ case SPIN_EPGSOURCE_SELECTION:
+ return OnClickEPGSourceSpin();
+ case BUTTON_GROUP_MANAGER:
+ return OnClickButtonGroupManager();
+ case BUTTON_NEW_CHANNEL:
+ return OnClickButtonNewChannel();
+ case BUTTON_REFRESH_LOGOS:
+ return OnClickButtonRefreshChannelLogos();
+ default:
+ return false;
+ }
+}
+
+bool CGUIDialogPVRChannelManager::OnMessage(CGUIMessage& message)
+{
+ unsigned int iMessage = message.GetMessage();
+
+ switch (iMessage)
+ {
+ case GUI_MSG_CLICKED:
+ return OnMessageClick(message);
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogPVRChannelManager::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+
+ m_viewControl.Reset();
+ m_viewControl.SetParentWindow(GetID());
+ m_viewControl.AddView(GetControl(CONTROL_LIST_CHANNELS));
+}
+
+void CGUIDialogPVRChannelManager::OnWindowUnload()
+{
+ CGUIDialog::OnWindowUnload();
+ m_viewControl.Reset();
+}
+
+CFileItemPtr CGUIDialogPVRChannelManager::GetCurrentListItem(int offset)
+{
+ return m_channelItems->Get(m_iSelected);
+}
+
+bool CGUIDialogPVRChannelManager::OnPopupMenu(int iItem)
+{
+ // popup the context menu
+ // grab our context menu
+ CContextButtons buttons;
+
+ // mark the item
+ if (iItem >= 0 && iItem < m_channelItems->Size())
+ m_channelItems->Get(iItem)->Select(true);
+ else
+ return false;
+
+ CFileItemPtr pItem = m_channelItems->Get(iItem);
+ if (!pItem)
+ return false;
+
+ if (m_bAllowReorder &&
+ pItem->GetProperty(PROPERTY_CHANNEL_NUMBER).asString() != LABEL_CHANNEL_DISABLED)
+ buttons.Add(CONTEXT_BUTTON_MOVE, 116); /* Move channel up or down */
+
+ if (pItem->GetProperty(PROPERTY_CLIENT_SUPPORTS_SETTINGS).asBoolean())
+ {
+ buttons.Add(CONTEXT_BUTTON_SETTINGS, 10004); /* Open add-on channel settings dialog */
+ buttons.Add(CONTEXT_BUTTON_DELETE, 117); /* Delete add-on channel */
+ }
+
+ int choice = CGUIDialogContextMenu::ShowAndGetChoice(buttons);
+
+ // deselect our item
+ if (iItem >= 0 && iItem < m_channelItems->Size())
+ m_channelItems->Get(iItem)->Select(false);
+
+ if (choice < 0)
+ return false;
+
+ return OnContextButton(iItem, (CONTEXT_BUTTON)choice);
+}
+
+bool CGUIDialogPVRChannelManager::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ /* Check file item is in list range and get his pointer */
+ if (itemNumber < 0 || itemNumber >= m_channelItems->Size()) return false;
+
+ CFileItemPtr pItem = m_channelItems->Get(itemNumber);
+ if (!pItem)
+ return false;
+
+ if (button == CONTEXT_BUTTON_MOVE)
+ {
+ m_bMovingMode = true;
+ pItem->Select(true);
+ }
+ else if (button == CONTEXT_BUTTON_SETTINGS)
+ {
+ PromptAndSaveList();
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*pItem);
+ PVR_ERROR ret = PVR_ERROR_UNKNOWN;
+ if (client)
+ ret = client->OpenDialogChannelSettings(pItem->GetPVRChannelInfoTag());
+
+ if (ret == PVR_ERROR_NO_ERROR)
+ {
+ Update();
+ SetData(m_iSelected);
+ }
+ else if (ret == PVR_ERROR_NOT_IMPLEMENTED)
+ HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19038}); // "Information", "Not supported by the PVR backend."
+ else
+ HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // "Add-on error", "Check the log for more information about this message."
+ }
+ else if (button == CONTEXT_BUTTON_DELETE)
+ {
+ CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (!pDialog)
+ return true;
+
+ pDialog->SetHeading(CVariant{19211}); // Delete channel
+ pDialog->SetText(CVariant{750}); // Are you sure?
+ pDialog->Open();
+
+ if (pDialog->IsConfirmed())
+ {
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*pItem);
+ if (client)
+ {
+ const std::shared_ptr<CPVRChannel> channel = pItem->GetPVRChannelInfoTag();
+ PVR_ERROR ret = client->DeleteChannel(channel);
+ if (ret == PVR_ERROR_NO_ERROR)
+ {
+ CPVRChannelGroups* groups =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio);
+ if (groups)
+ {
+ groups->UpdateFromClients({});
+ Update();
+ }
+ }
+ else if (ret == PVR_ERROR_NOT_IMPLEMENTED)
+ HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19038}); // "Information", "Not supported by the PVR backend."
+ else
+ HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // "Add-on error", "Check the log for more information about this message."
+ }
+ }
+ }
+ return true;
+}
+
+void CGUIDialogPVRChannelManager::SetData(int iItem)
+{
+ if (iItem < 0 || iItem >= m_channelItems->Size())
+ {
+ ClearChannelOptions();
+ EnableChannelOptions(false);
+ return;
+ }
+
+ CFileItemPtr pItem = m_channelItems->Get(iItem);
+ if (!pItem)
+ return;
+
+ SET_CONTROL_LABEL2(EDIT_NAME, pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString());
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), EDIT_NAME, CGUIEditControl::INPUT_TYPE_TEXT, 19208);
+ OnMessage(msg);
+
+ SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_ACTIVE,
+ pItem->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean());
+ SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_USEEPG,
+ pItem->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean());
+ SET_CONTROL_SELECTED(GetID(), RADIOBUTTON_PARENTAL_LOCK,
+ pItem->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean());
+
+ EnableChannelOptions(true);
+}
+
+void CGUIDialogPVRChannelManager::Update()
+{
+ m_viewControl.SetCurrentView(CONTROL_LIST_CHANNELS);
+
+ // empty the lists ready for population
+ Clear();
+
+ std::shared_ptr<CPVRChannelGroup> channels = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio);
+
+ // No channels available, nothing to do.
+ if (!channels)
+ return;
+
+ channels->UpdateFromClients({});
+
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = channels->GetMembers();
+ std::shared_ptr<CFileItem> channelFile;
+ for (const auto& member : groupMembers)
+ {
+ channelFile = std::make_shared<CFileItem>(member);
+ if (!channelFile)
+ continue;
+ const std::shared_ptr<CPVRChannel> channel(channelFile->GetPVRChannelInfoTag());
+
+ channelFile->SetProperty(PROPERTY_CHANNEL_ENABLED, !channel->IsHidden());
+ channelFile->SetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN, channel->IsUserSetHidden());
+ channelFile->SetProperty(PROPERTY_CHANNEL_NAME, channel->ChannelName());
+ channelFile->SetProperty(PROPERTY_CHANNEL_EPG_ENABLED, channel->EPGEnabled());
+ channelFile->SetProperty(PROPERTY_CHANNEL_ICON, channel->ClientIconPath());
+ channelFile->SetProperty(PROPERTY_CHANNEL_CUSTOM_ICON, channel->IsUserSetIcon());
+ channelFile->SetProperty(PROPERTY_CHANNEL_EPG_SOURCE, 0);
+ channelFile->SetProperty(PROPERTY_CHANNEL_LOCKED, channel->IsLocked());
+ channelFile->SetProperty(PROPERTY_CHANNEL_NUMBER,
+ member->ChannelNumber().FormattedChannelNumber());
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*channelFile);
+ if (client)
+ {
+ channelFile->SetProperty(PROPERTY_CLIENT_NAME, client->GetFriendlyName());
+ channelFile->SetProperty(PROPERTY_CLIENT_SUPPORTS_SETTINGS,
+ client->GetClientCapabilities().SupportsChannelSettings());
+ }
+
+ m_channelItems->Add(channelFile);
+ }
+
+ {
+ std::vector< std::pair<std::string, int> > labels;
+ labels.emplace_back(g_localizeStrings.Get(19210), 0);
+ //! @todo Add Labels for EPG scrapers here
+ SET_CONTROL_LABELS(SPIN_EPGSOURCE_SELECTION, 0, &labels);
+ }
+
+ m_clientsWithSettingsList = CServiceBroker::GetPVRManager().Clients()->GetClientsSupportingChannelSettings(m_bIsRadio);
+ if (!m_clientsWithSettingsList.empty())
+ m_bAllowNewChannel = true;
+
+ if (m_bAllowNewChannel)
+ CONTROL_ENABLE(BUTTON_NEW_CHANNEL);
+ else
+ CONTROL_DISABLE(BUTTON_NEW_CHANNEL);
+
+ Renumber();
+ m_viewControl.SetItems(*m_channelItems);
+ if (m_iSelected >= m_channelItems->Size())
+ m_iSelected = m_channelItems->Size() - 1;
+ m_viewControl.SetSelectedItem(m_iSelected);
+ SetData(m_iSelected);
+}
+
+void CGUIDialogPVRChannelManager::Clear()
+{
+ m_viewControl.Clear();
+ m_channelItems->Clear();
+
+ ClearChannelOptions();
+ EnableChannelOptions(false);
+
+ CONTROL_DISABLE(BUTTON_APPLY);
+}
+
+void CGUIDialogPVRChannelManager::ClearChannelOptions()
+{
+ CONTROL_DESELECT(RADIOBUTTON_ACTIVE);
+ SET_CONTROL_LABEL2(EDIT_NAME, "");
+ SET_CONTROL_FILENAME(BUTTON_CHANNEL_LOGO, "");
+ CONTROL_DESELECT(RADIOBUTTON_USEEPG);
+
+ std::vector<std::pair<std::string, int>> labels = {{g_localizeStrings.Get(19210), 0}};
+ SET_CONTROL_LABELS(SPIN_EPGSOURCE_SELECTION, 0, &labels);
+
+ CONTROL_DESELECT(RADIOBUTTON_PARENTAL_LOCK);
+}
+
+void CGUIDialogPVRChannelManager::EnableChannelOptions(bool bEnable)
+{
+ if (bEnable)
+ {
+ CONTROL_ENABLE(RADIOBUTTON_ACTIVE);
+ CONTROL_ENABLE(EDIT_NAME);
+ CONTROL_ENABLE(BUTTON_CHANNEL_LOGO);
+ CONTROL_ENABLE(IMAGE_CHANNEL_LOGO);
+ CONTROL_ENABLE(RADIOBUTTON_USEEPG);
+ CONTROL_ENABLE(SPIN_EPGSOURCE_SELECTION);
+ CONTROL_ENABLE(RADIOBUTTON_PARENTAL_LOCK);
+ }
+ else
+ {
+ CONTROL_DISABLE(RADIOBUTTON_ACTIVE);
+ CONTROL_DISABLE(EDIT_NAME);
+ CONTROL_DISABLE(BUTTON_CHANNEL_LOGO);
+ CONTROL_DISABLE(IMAGE_CHANNEL_LOGO);
+ CONTROL_DISABLE(RADIOBUTTON_USEEPG);
+ CONTROL_DISABLE(SPIN_EPGSOURCE_SELECTION);
+ CONTROL_DISABLE(RADIOBUTTON_PARENTAL_LOCK);
+ }
+}
+
+void CGUIDialogPVRChannelManager::RenameChannel(const CFileItemPtr& pItem)
+{
+ std::string strChannelName = pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString();
+ if (strChannelName != pItem->GetPVRChannelInfoTag()->ChannelName())
+ {
+ std::shared_ptr<CPVRChannel> channel = pItem->GetPVRChannelInfoTag();
+ channel->SetChannelName(strChannelName);
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*pItem);
+ if (!client || (client->RenameChannel(channel) != PVR_ERROR_NO_ERROR))
+ HELPERS::ShowOKDialogText(CVariant{2103}, CVariant{16029}); // Add-on error;Check the log file for details.
+ }
+}
+
+bool CGUIDialogPVRChannelManager::PersistChannel(const CFileItemPtr& pItem,
+ const std::shared_ptr<CPVRChannelGroup>& group)
+{
+ if (!pItem || !group)
+ return false;
+
+ return group->UpdateChannel(
+ pItem->GetPVRChannelInfoTag()->StorageId(),
+ pItem->GetProperty(PROPERTY_CHANNEL_NAME).asString(),
+ pItem->GetProperty(PROPERTY_CHANNEL_ICON).asString(),
+ static_cast<int>(pItem->GetProperty(PROPERTY_CHANNEL_EPG_SOURCE).asInteger()),
+ m_bAllowRenumber ? pItem->GetProperty(PROPERTY_CHANNEL_NUMBER).asInteger() : 0,
+ !pItem->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean(), // hidden
+ pItem->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean(),
+ pItem->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean(),
+ pItem->GetProperty(PROPERTY_CHANNEL_CUSTOM_ICON).asBoolean(),
+ pItem->GetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN).asBoolean());
+}
+
+void CGUIDialogPVRChannelManager::PromptAndSaveList()
+{
+ if (!HasChangedItems())
+ return;
+
+ CGUIDialogYesNo* pDialogYesNo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (pDialogYesNo)
+ {
+ pDialogYesNo->SetHeading(CVariant{20052});
+ pDialogYesNo->SetLine(0, CVariant{""});
+ pDialogYesNo->SetLine(1, CVariant{19212});
+ pDialogYesNo->SetLine(2, CVariant{20103});
+ pDialogYesNo->Open();
+
+ if (pDialogYesNo->IsConfirmed())
+ SaveList();
+ else
+ Update();
+ }
+}
+
+void CGUIDialogPVRChannelManager::SaveList()
+{
+ if (!HasChangedItems())
+ return;
+
+ /* display the progress dialog */
+ CGUIDialogProgress* pDlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ pDlgProgress->SetHeading(CVariant{190});
+ pDlgProgress->SetLine(0, CVariant{""});
+ pDlgProgress->SetLine(1, CVariant{328});
+ pDlgProgress->SetLine(2, CVariant{""});
+ pDlgProgress->Open();
+ pDlgProgress->Progress();
+ pDlgProgress->SetPercentage(0);
+
+ /* persist all channels */
+ std::shared_ptr<CPVRChannelGroup> group = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio);
+ if (!group)
+ return;
+
+ for (int iListPtr = 0; iListPtr < m_channelItems->Size(); ++iListPtr)
+ {
+ CFileItemPtr pItem = m_channelItems->Get(iListPtr);
+ if (pItem && pItem->GetProperty(PROPERTY_ITEM_CHANGED).asBoolean())
+ {
+ if (pItem->GetProperty(PROPERTY_CLIENT_SUPPORTS_SETTINGS).asBoolean())
+ RenameChannel(pItem);
+
+ if (PersistChannel(pItem, group))
+ pItem->SetProperty(PROPERTY_ITEM_CHANGED, false);
+ }
+
+ pDlgProgress->SetPercentage(iListPtr * 100 / m_channelItems->Size());
+ }
+
+ group->SortAndRenumber();
+
+ auto channelGroups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio);
+ channelGroups->UpdateChannelNumbersFromAllChannelsGroup();
+ channelGroups->PersistAll();
+ pDlgProgress->Close();
+
+ CONTROL_DISABLE(BUTTON_APPLY);
+}
+
+bool CGUIDialogPVRChannelManager::HasChangedItems() const
+{
+ return std::any_of(m_channelItems->cbegin(), m_channelItems->cend(), [](const auto& item) {
+ return item && item->GetProperty(PROPERTY_ITEM_CHANGED).asBoolean();
+ });
+}
+
+namespace
+{
+
+bool IsItemChanged(const std::shared_ptr<CFileItem>& item)
+{
+ const std::shared_ptr<CPVRChannelGroupMember> member = item->GetPVRChannelGroupMemberInfoTag();
+ const std::shared_ptr<CPVRChannel> channel = member->Channel();
+
+ return item->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean() == channel->IsHidden() ||
+ item->GetProperty(PROPERTY_CHANNEL_USER_SET_HIDDEN).asBoolean() !=
+ channel->IsUserSetHidden() ||
+ item->GetProperty(PROPERTY_CHANNEL_NAME).asString() != channel->ChannelName() ||
+ item->GetProperty(PROPERTY_CHANNEL_EPG_ENABLED).asBoolean() != channel->EPGEnabled() ||
+ item->GetProperty(PROPERTY_CHANNEL_ICON).asString() != channel->ClientIconPath() ||
+ item->GetProperty(PROPERTY_CHANNEL_CUSTOM_ICON).asBoolean() != channel->IsUserSetIcon() ||
+ item->GetProperty(PROPERTY_CHANNEL_EPG_SOURCE).asInteger() != 0 ||
+ item->GetProperty(PROPERTY_CHANNEL_LOCKED).asBoolean() != channel->IsLocked() ||
+ item->GetProperty(PROPERTY_CHANNEL_NUMBER).asString() !=
+ member->ChannelNumber().FormattedChannelNumber();
+}
+
+} // namespace
+
+void CGUIDialogPVRChannelManager::SetItemChanged(const CFileItemPtr& pItem)
+{
+ const bool changed = IsItemChanged(pItem);
+ pItem->SetProperty(PROPERTY_ITEM_CHANGED, changed);
+
+ if (changed || HasChangedItems())
+ CONTROL_ENABLE(BUTTON_APPLY);
+ else
+ CONTROL_DISABLE(BUTTON_APPLY);
+}
+
+void CGUIDialogPVRChannelManager::Renumber()
+{
+ if (!m_bAllowRenumber)
+ return;
+
+ int iNextChannelNumber = 0;
+ for (const auto& item : *m_channelItems)
+ {
+ const std::string number = item->GetProperty(PROPERTY_CHANNEL_ENABLED).asBoolean()
+ ? std::to_string(++iNextChannelNumber)
+ : LABEL_CHANNEL_DISABLED;
+
+ if (item->GetProperty(PROPERTY_CHANNEL_NUMBER).asString() != number)
+ {
+ item->SetProperty(PROPERTY_CHANNEL_NUMBER, number);
+ SetItemChanged(item);
+ }
+ }
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.h b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.h
new file mode 100644
index 0000000..0716436
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelManager.h
@@ -0,0 +1,95 @@
+/*
+ * 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 "dialogs/GUIDialogContextMenu.h"
+#include "guilib/GUIDialog.h"
+#include "view/GUIViewControl.h"
+
+#include <memory>
+#include <vector>
+
+class CAction;
+class CFileItemList;
+class CGUIMessage;
+
+namespace PVR
+{
+ class CPVRChannelGroup;
+ class CPVRClient;
+
+ class CGUIDialogPVRChannelManager : public CGUIDialog
+ {
+ public:
+ CGUIDialogPVRChannelManager();
+ ~CGUIDialogPVRChannelManager() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ bool HasListItems() const override{ return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+
+ void Open(const std::shared_ptr<CFileItem>& initialSelection);
+ void SetRadio(bool bIsRadio);
+
+ protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ private:
+ void Clear();
+ void Update();
+ void PromptAndSaveList();
+ void SaveList();
+ void Renumber();
+ void SetData(int iItem);
+ void RenameChannel(const CFileItemPtr& pItem);
+
+ void ClearChannelOptions();
+ void EnableChannelOptions(bool bEnable);
+
+ bool OnPopupMenu(int iItem);
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button);
+ bool OnActionMove(const CAction& action);
+ bool OnMessageClick(const CGUIMessage& message);
+ bool OnClickListChannels(const CGUIMessage& message);
+ bool OnClickButtonOK();
+ bool OnClickButtonApply();
+ bool OnClickButtonCancel();
+ bool OnClickButtonRadioTV();
+ bool OnClickButtonRadioActive();
+ bool OnClickButtonRadioParentalLocked();
+ bool OnClickButtonEditName();
+ bool OnClickButtonChannelLogo();
+ bool OnClickButtonUseEPG();
+ bool OnClickEPGSourceSpin();
+ bool OnClickButtonGroupManager();
+ bool OnClickButtonNewChannel();
+ bool OnClickButtonRefreshChannelLogos();
+
+ bool PersistChannel(const CFileItemPtr& pItem, const std::shared_ptr<CPVRChannelGroup>& group);
+
+ bool HasChangedItems() const;
+ void SetItemChanged(const CFileItemPtr& pItem);
+
+ bool m_bIsRadio = false;
+ bool m_bMovingMode = false;
+ bool m_bAllowNewChannel = false;
+ bool m_bAllowRenumber = false;
+ bool m_bAllowReorder = false;
+
+ std::shared_ptr<CFileItem> m_initialSelection;
+ int m_iSelected = 0;
+ CFileItemList* m_channelItems;
+ CGUIViewControl m_viewControl;
+
+ std::vector<std::shared_ptr<CPVRClient>> m_clientsWithSettingsList;
+ };
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.cpp b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.cpp
new file mode 100644
index 0000000..b5830d4
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.cpp
@@ -0,0 +1,299 @@
+/*
+ * 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 "GUIDialogPVRChannelsOSD.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+using namespace std::chrono_literals;
+
+#define MAX_INVALIDATION_FREQUENCY 2000ms // limit to one invalidation per X milliseconds
+
+CGUIDialogPVRChannelsOSD::CGUIDialogPVRChannelsOSD()
+ : CGUIDialogPVRItemsViewBase(WINDOW_DIALOG_PVR_OSD_CHANNELS, "DialogPVRChannelsOSD.xml")
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().RegisterChannelNumberInputHandler(this);
+}
+
+CGUIDialogPVRChannelsOSD::~CGUIDialogPVRChannelsOSD()
+{
+ auto& mgr = CServiceBroker::GetPVRManager();
+ mgr.Events().Unsubscribe(this);
+ mgr.Get<PVR::GUI::Channels>().DeregisterChannelNumberInputHandler(this);
+}
+
+bool CGUIDialogPVRChannelsOSD::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_REFRESH_LIST)
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::CurrentItem:
+ m_viewControl.SetItems(*m_vecItems);
+ return true;
+
+ case PVREvent::Epg:
+ case PVREvent::EpgContainer:
+ case PVREvent::EpgActiveItem:
+ if (IsActive())
+ SetInvalid();
+ return true;
+
+ default:
+ break;
+ }
+ }
+ return CGUIDialogPVRItemsViewBase::OnMessage(message);
+}
+
+void CGUIDialogPVRChannelsOSD::OnInitWindow()
+{
+ if (!CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV() &&
+ !CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio())
+ {
+ Close();
+ return;
+ }
+
+ Init();
+ Update();
+ CGUIDialogPVRItemsViewBase::OnInitWindow();
+}
+
+void CGUIDialogPVRChannelsOSD::OnDeinitWindow(int nextWindowID)
+{
+ if (m_group)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().SetSelectedChannelPath(
+ m_group->IsRadio(), m_viewControl.GetSelectedItemPath());
+
+ // next OnInitWindow will set the group which is then selected
+ m_group.reset();
+ }
+
+ CGUIDialogPVRItemsViewBase::OnDeinitWindow(nextWindowID);
+}
+
+bool CGUIDialogPVRChannelsOSD::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ // If direct channel number input is active, select the entered channel.
+ if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .CheckInputAndExecuteAction())
+ return true;
+
+ if (m_viewControl.HasControl(GetFocusedControlID()))
+ {
+ // Switch to channel
+ GotoChannel(m_viewControl.GetSelectedItem());
+ return true;
+ }
+ break;
+ }
+ case ACTION_PREVIOUS_CHANNELGROUP:
+ case ACTION_NEXT_CHANNELGROUP:
+ {
+ // save control states and currently selected item of group
+ SaveControlStates();
+
+ // switch to next or previous group
+ const CPVRChannelGroups* groups =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_group->IsRadio());
+ const std::shared_ptr<CPVRChannelGroup> nextGroup = action.GetID() == ACTION_NEXT_CHANNELGROUP
+ ? groups->GetNextGroup(*m_group)
+ : groups->GetPreviousGroup(*m_group);
+ CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(nextGroup);
+ m_group = nextGroup;
+ Init();
+ Update();
+
+ // restore control states and previously selected item of group
+ RestoreControlStates();
+ return true;
+ }
+ case REMOTE_0:
+ case REMOTE_1:
+ case REMOTE_2:
+ case REMOTE_3:
+ case REMOTE_4:
+ case REMOTE_5:
+ case REMOTE_6:
+ case REMOTE_7:
+ case REMOTE_8:
+ case REMOTE_9:
+ {
+ AppendChannelNumberCharacter(static_cast<char>(action.GetID() - REMOTE_0) + '0');
+ return true;
+ }
+ case ACTION_CHANNEL_NUMBER_SEP:
+ {
+ AppendChannelNumberCharacter(CPVRChannelNumber::SEPARATOR);
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return CGUIDialogPVRItemsViewBase::OnAction(action);
+}
+
+void CGUIDialogPVRChannelsOSD::Update()
+{
+ CPVRManager& pvrMgr = CServiceBroker::GetPVRManager();
+ pvrMgr.Events().Subscribe(this, &CGUIDialogPVRChannelsOSD::Notify);
+
+ const std::shared_ptr<CPVRChannel> channel = pvrMgr.PlaybackState()->GetPlayingChannel();
+ if (channel)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ pvrMgr.PlaybackState()->GetActiveChannelGroup(channel->IsRadio());
+ if (group)
+ {
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ group->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ for (const auto& groupMember : groupMembers)
+ {
+ m_vecItems->Add(std::make_shared<CFileItem>(groupMember));
+ }
+
+ m_viewControl.SetItems(*m_vecItems);
+
+ if (!m_group)
+ {
+ m_group = group;
+ m_viewControl.SetSelectedItem(
+ pvrMgr.Get<PVR::GUI::Channels>().GetSelectedChannelPath(channel->IsRadio()));
+ SaveSelectedItemPath(group->GroupID());
+ }
+ }
+ }
+}
+
+void CGUIDialogPVRChannelsOSD::SetInvalid()
+{
+ if (m_refreshTimeout.IsTimePast())
+ {
+ for (const auto& item : *m_vecItems)
+ item->SetInvalid();
+
+ CGUIDialogPVRItemsViewBase::SetInvalid();
+ m_refreshTimeout.Set(MAX_INVALIDATION_FREQUENCY);
+ }
+}
+
+void CGUIDialogPVRChannelsOSD::SaveControlStates()
+{
+ CGUIDialogPVRItemsViewBase::SaveControlStates();
+
+ if (m_group)
+ SaveSelectedItemPath(m_group->GroupID());
+}
+
+void CGUIDialogPVRChannelsOSD::RestoreControlStates()
+{
+ CGUIDialogPVRItemsViewBase::RestoreControlStates();
+
+ if (m_group)
+ {
+ const std::string path = GetLastSelectedItemPath(m_group->GroupID());
+ if (path.empty())
+ m_viewControl.SetSelectedItem(0);
+ else
+ m_viewControl.SetSelectedItem(path);
+ }
+}
+
+void CGUIDialogPVRChannelsOSD::GotoChannel(int iItem)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return;
+
+ // Preserve the item before closing self, because this will clear m_vecItems
+ const std::shared_ptr<CFileItem> item = m_vecItems->Get(iItem);
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRMENU_CLOSECHANNELOSDONSWITCH))
+ Close();
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ *item, true /* bCheckResume */);
+}
+
+void CGUIDialogPVRChannelsOSD::Notify(const PVREvent& event)
+{
+ const CGUIMessage m(GUI_MSG_REFRESH_LIST, GetID(), 0, static_cast<int>(event));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+}
+
+void CGUIDialogPVRChannelsOSD::SaveSelectedItemPath(int iGroupID)
+{
+ m_groupSelectedItemPaths[iGroupID] = m_viewControl.GetSelectedItemPath();
+}
+
+std::string CGUIDialogPVRChannelsOSD::GetLastSelectedItemPath(int iGroupID) const
+{
+ const auto it = m_groupSelectedItemPaths.find(iGroupID);
+ if (it != m_groupSelectedItemPaths.end())
+ return it->second;
+
+ return std::string();
+}
+
+void CGUIDialogPVRChannelsOSD::GetChannelNumbers(std::vector<std::string>& channelNumbers)
+{
+ if (m_group)
+ m_group->GetChannelNumbers(channelNumbers);
+}
+
+void CGUIDialogPVRChannelsOSD::OnInputDone()
+{
+ const CPVRChannelNumber channelNumber = GetChannelNumber();
+ if (channelNumber.IsValid())
+ {
+ int itemIndex = 0;
+ for (const CFileItemPtr& channel : *m_vecItems)
+ {
+ if (channel->GetPVRChannelGroupMemberInfoTag()->ChannelNumber() == channelNumber)
+ {
+ m_viewControl.SetSelectedItem(itemIndex);
+ return;
+ }
+ ++itemIndex;
+ }
+ }
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.h b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.h
new file mode 100644
index 0000000..c23fb92
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRChannelsOSD.h
@@ -0,0 +1,61 @@
+/*
+ * 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 "pvr/PVRChannelNumberInputHandler.h"
+#include "pvr/dialogs/GUIDialogPVRItemsViewBase.h"
+#include "threads/SystemClock.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+namespace PVR
+{
+enum class PVREvent;
+
+class CPVRChannelGroup;
+
+class CGUIDialogPVRChannelsOSD : public CGUIDialogPVRItemsViewBase,
+ public CPVRChannelNumberInputHandler
+{
+public:
+ CGUIDialogPVRChannelsOSD();
+ ~CGUIDialogPVRChannelsOSD() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+
+ // CPVRChannelNumberInputHandler implementation
+ void GetChannelNumbers(std::vector<std::string>& channelNumbers) override;
+ void OnInputDone() override;
+
+protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ void RestoreControlStates() override;
+ void SaveControlStates() override;
+ void SetInvalid() override;
+
+private:
+ void GotoChannel(int iItem);
+ void Update();
+ void SaveSelectedItemPath(int iGroupID);
+ std::string GetLastSelectedItemPath(int iGroupID) const;
+
+ std::shared_ptr<CPVRChannelGroup> m_group;
+ std::map<int, std::string> m_groupSelectedItemPaths;
+ XbmcThreads::EndTime<> m_refreshTimeout;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.cpp b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.cpp
new file mode 100644
index 0000000..ca83ba1
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.cpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017-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 "GUIDialogPVRClientPriorities.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIMessage.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <cstdlib>
+#include <memory>
+
+using namespace PVR;
+
+CGUIDialogPVRClientPriorities::CGUIDialogPVRClientPriorities() :
+ CGUIDialogSettingsManualBase(WINDOW_DIALOG_PVR_CLIENT_PRIORITIES, "DialogSettings.xml")
+{
+ m_loadType = LOAD_EVERY_TIME;
+}
+
+void CGUIDialogPVRClientPriorities::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ SetHeading(19240); // Client priorities
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186); // OK
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222); // Cancel
+}
+
+std::string CGUIDialogPVRClientPriorities::GetSettingsLabel(
+ const std::shared_ptr<ISetting>& pSetting)
+{
+ int iClientId = std::atoi(pSetting->GetId().c_str());
+ auto clientEntry = m_clients.find(iClientId);
+ if (clientEntry != m_clients.end())
+ return clientEntry->second->GetFriendlyName();
+
+ CLog::LogF(LOGERROR, "Unable to obtain pvr client with id '{}'", iClientId);
+ return CGUIDialogSettingsManualBase::GetLocalizedString(13205); // Unknown
+}
+
+void CGUIDialogPVRClientPriorities::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("pvrclientpriorities", -1);
+ if (category == nullptr)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings category");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == nullptr)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings group");
+ return;
+ }
+
+ m_clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients();
+ for (const auto& client : m_clients)
+ {
+ AddEdit(group, std::to_string(client.second->GetID()), 13205 /* Unknown */, SettingLevel::Basic,
+ client.second->GetPriority());
+ }
+}
+
+void CGUIDialogPVRClientPriorities::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ {
+ CLog::LogF(LOGERROR, "No setting");
+ return;
+ }
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ m_changedValues[setting->GetId()] = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+}
+
+bool CGUIDialogPVRClientPriorities::Save()
+{
+ for (const auto& changedClient : m_changedValues)
+ {
+ int iClientId = std::atoi(changedClient.first.c_str());
+ auto clientEntry = m_clients.find(iClientId);
+ if (clientEntry != m_clients.end())
+ clientEntry->second->SetPriority(changedClient.second);
+ }
+
+ return true;
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.h b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.h
new file mode 100644
index 0000000..a6ca62d
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRClientPriorities.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017-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 "pvr/addons/PVRClients.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <map>
+#include <string>
+
+namespace PVR
+{
+ class CGUIDialogPVRClientPriorities : public CGUIDialogSettingsManualBase
+ {
+ public:
+ CGUIDialogPVRClientPriorities();
+
+ protected:
+ // implementation of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ std::string GetSettingsLabel(const std::shared_ptr<ISetting>& pSetting) override;
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+ private:
+ CPVRClientMap m_clients;
+ std::map<std::string, int> m_changedValues;
+ };
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.cpp
new file mode 100644
index 0000000..dbc37f3
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.cpp
@@ -0,0 +1,546 @@
+/*
+ * 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 "GUIDialogPVRGroupManager.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIRadioButtonControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/helpers//DialogOKHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/filesystem/PVRGUIDirectory.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+using namespace KODI::MESSAGING;
+using namespace PVR;
+
+#define CONTROL_LIST_CHANNELS_LEFT 11
+#define CONTROL_LIST_CHANNELS_RIGHT 12
+#define CONTROL_LIST_CHANNEL_GROUPS 13
+#define CONTROL_CURRENT_GROUP_LABEL 20
+#define CONTROL_UNGROUPED_LABEL 21
+#define CONTROL_IN_GROUP_LABEL 22
+#define BUTTON_HIDE_GROUP 25
+#define BUTTON_NEWGROUP 26
+#define BUTTON_RENAMEGROUP 27
+#define BUTTON_DELGROUP 28
+#define BUTTON_OK 29
+#define BUTTON_TOGGLE_RADIO_TV 34
+#define BUTTON_RECREATE_GROUP_THUMB 35
+
+CGUIDialogPVRGroupManager::CGUIDialogPVRGroupManager() :
+ CGUIDialog(WINDOW_DIALOG_PVR_GROUP_MANAGER, "DialogPVRGroupManager.xml")
+{
+ m_ungroupedChannels = new CFileItemList;
+ m_groupMembers = new CFileItemList;
+ m_channelGroups = new CFileItemList;
+
+ SetRadio(false);
+}
+
+CGUIDialogPVRGroupManager::~CGUIDialogPVRGroupManager()
+{
+ delete m_ungroupedChannels;
+ delete m_groupMembers;
+ delete m_channelGroups;
+}
+
+void CGUIDialogPVRGroupManager::SetRadio(bool bIsRadio)
+{
+ m_bIsRadio = bIsRadio;
+ SetProperty("IsRadio", m_bIsRadio ? "true" : "");
+}
+
+bool CGUIDialogPVRGroupManager::PersistChanges()
+{
+ return CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio)->PersistAll();
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonOk(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (iControl == BUTTON_OK)
+ {
+ PersistChanges();
+ Close();
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonNewGroup(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (iControl == BUTTON_NEWGROUP)
+ {
+ std::string strGroupName = "";
+ /* prompt for a group name */
+ if (CGUIKeyboardFactory::ShowAndGetInput(strGroupName, CVariant{g_localizeStrings.Get(19139)}, false))
+ {
+ if (strGroupName != "")
+ {
+ /* add the group if it doesn't already exist */
+ CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio);
+ if (groups->AddGroup(strGroupName))
+ {
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio)->GetByName(strGroupName)->SetGroupType(PVR_GROUP_TYPE_USER_DEFINED);
+ m_iSelectedChannelGroup = groups->Size() - 1;
+ Update();
+ }
+ }
+ }
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonDeleteGroup(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (iControl == BUTTON_DELGROUP)
+ {
+ if (!m_selectedGroup)
+ return bReturn;
+
+ CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (!pDialog)
+ return bReturn;
+
+ pDialog->SetHeading(CVariant{117});
+ pDialog->SetLine(0, CVariant{""});
+ pDialog->SetLine(1, CVariant{m_selectedGroup->GroupName()});
+ pDialog->SetLine(2, CVariant{""});
+ pDialog->Open();
+
+ if (pDialog->IsConfirmed())
+ {
+ ClearSelectedGroupsThumbnail();
+ if (CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(m_bIsRadio)
+ ->DeleteGroup(m_selectedGroup))
+ Update();
+ }
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonRenameGroup(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (iControl == BUTTON_RENAMEGROUP)
+ {
+ if (!m_selectedGroup)
+ return bReturn;
+
+ std::string strGroupName(m_selectedGroup->GroupName());
+ if (CGUIKeyboardFactory::ShowAndGetInput(strGroupName, CVariant{g_localizeStrings.Get(19139)}, false))
+ {
+ if (!strGroupName.empty())
+ {
+ ClearSelectedGroupsThumbnail();
+ m_selectedGroup->SetGroupName(strGroupName);
+ Update();
+ }
+ }
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonUngroupedChannels(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (m_viewUngroupedChannels.HasControl(iControl)) // list/thumb control
+ {
+ m_iSelectedUngroupedChannel = m_viewUngroupedChannels.GetSelectedItem();
+ int iAction = message.GetParam1();
+
+ if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK)
+ {
+ if (m_channelGroups->GetFolderCount() == 0)
+ {
+ HELPERS::ShowOKDialogText(CVariant{19033}, CVariant{19137});
+ }
+ else if (m_ungroupedChannels->GetFileCount() > 0)
+ {
+ CFileItemPtr pItemChannel = m_ungroupedChannels->Get(m_iSelectedUngroupedChannel);
+
+ if (m_selectedGroup->AppendToGroup(pItemChannel->GetPVRChannelInfoTag()))
+ {
+ ClearSelectedGroupsThumbnail();
+ Update();
+ }
+ }
+ }
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonGroupMembers(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (m_viewGroupMembers.HasControl(iControl)) // list/thumb control
+ {
+ m_iSelectedGroupMember = m_viewGroupMembers.GetSelectedItem();
+ int iAction = message.GetParam1();
+
+ if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK)
+ {
+ if (m_selectedGroup && m_groupMembers->GetFileCount() > 0)
+ {
+ CFileItemPtr pItemChannel = m_groupMembers->Get(m_iSelectedGroupMember);
+ m_selectedGroup->RemoveFromGroup(pItemChannel->GetPVRChannelInfoTag());
+ ClearSelectedGroupsThumbnail();
+ Update();
+ }
+ }
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonChannelGroups(const CGUIMessage& message)
+{
+ bool bReturn = false;
+ unsigned int iControl = message.GetSenderId();
+
+ if (m_viewChannelGroups.HasControl(iControl)) // list/thumb control
+ {
+ int iAction = message.GetParam1();
+
+ if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK)
+ {
+ m_iSelectedChannelGroup = m_viewChannelGroups.GetSelectedItem();
+ Update();
+ }
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonHideGroup(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == BUTTON_HIDE_GROUP && m_selectedGroup)
+ {
+ CGUIRadioButtonControl* button = static_cast<CGUIRadioButtonControl*>(GetControl(message.GetSenderId()));
+ if (button)
+ {
+ CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(m_bIsRadio)
+ ->HideGroup(m_selectedGroup, button->IsSelected());
+ Update();
+ }
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonToggleRadioTV(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == BUTTON_TOGGLE_RADIO_TV)
+ {
+ PersistChanges();
+ SetRadio(!m_bIsRadio);
+ Update();
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::ActionButtonRecreateThumbnail(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == BUTTON_RECREATE_GROUP_THUMB)
+ {
+ m_thumbLoader.ClearCachedImages(*m_channelGroups);
+ Update();
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::OnMessageClick(const CGUIMessage& message)
+{
+ return ActionButtonOk(message) ||
+ ActionButtonNewGroup(message) ||
+ ActionButtonDeleteGroup(message) ||
+ ActionButtonRenameGroup(message) ||
+ ActionButtonUngroupedChannels(message) ||
+ ActionButtonGroupMembers(message) ||
+ ActionButtonChannelGroups(message) ||
+ ActionButtonHideGroup(message) ||
+ ActionButtonToggleRadioTV(message) ||
+ ActionButtonRecreateThumbnail(message);
+}
+
+bool CGUIDialogPVRGroupManager::OnMessage(CGUIMessage& message)
+{
+ unsigned int iMessage = message.GetMessage();
+
+ switch (iMessage)
+ {
+ case GUI_MSG_CLICKED:
+ {
+ OnMessageClick(message);
+ }
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogPVRGroupManager::OnActionMove(const CAction& action)
+{
+ bool bReturn = false;
+ int iActionId = action.GetID();
+
+ if (GetFocusedControlID() == CONTROL_LIST_CHANNEL_GROUPS)
+ {
+ if (iActionId == ACTION_MOUSE_MOVE)
+ {
+ int iSelected = m_viewChannelGroups.GetSelectedItem();
+ if (m_iSelectedChannelGroup < iSelected)
+ {
+ iActionId = ACTION_MOVE_DOWN;
+ }
+ else if (m_iSelectedChannelGroup > iSelected)
+ {
+ iActionId = ACTION_MOVE_UP;
+ }
+ else
+ {
+ return bReturn;
+ }
+ }
+
+ if (iActionId == ACTION_MOVE_DOWN || iActionId == ACTION_MOVE_UP ||
+ iActionId == ACTION_PAGE_DOWN || iActionId == ACTION_PAGE_UP ||
+ iActionId == ACTION_FIRST_PAGE || iActionId == ACTION_LAST_PAGE)
+ {
+ CGUIDialog::OnAction(action);
+ int iSelected = m_viewChannelGroups.GetSelectedItem();
+
+ bReturn = true;
+ if (iSelected != m_iSelectedChannelGroup)
+ {
+ m_iSelectedChannelGroup = iSelected;
+ Update();
+ }
+ }
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGroupManager::OnAction(const CAction& action)
+{
+ return OnActionMove(action) ||
+ CGUIDialog::OnAction(action);
+}
+
+void CGUIDialogPVRGroupManager::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+ m_iSelectedUngroupedChannel = 0;
+ m_iSelectedGroupMember = 0;
+ m_iSelectedChannelGroup = 0;
+ Update();
+}
+
+void CGUIDialogPVRGroupManager::OnDeinitWindow(int nextWindowID)
+{
+ Clear();
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIDialogPVRGroupManager::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+
+ m_viewUngroupedChannels.Reset();
+ m_viewUngroupedChannels.SetParentWindow(GetID());
+ m_viewUngroupedChannels.AddView(GetControl(CONTROL_LIST_CHANNELS_LEFT));
+
+ m_viewGroupMembers.Reset();
+ m_viewGroupMembers.SetParentWindow(GetID());
+ m_viewGroupMembers.AddView(GetControl(CONTROL_LIST_CHANNELS_RIGHT));
+
+ m_viewChannelGroups.Reset();
+ m_viewChannelGroups.SetParentWindow(GetID());
+ m_viewChannelGroups.AddView(GetControl(CONTROL_LIST_CHANNEL_GROUPS));
+}
+
+void CGUIDialogPVRGroupManager::OnWindowUnload()
+{
+ CGUIDialog::OnWindowUnload();
+ m_viewUngroupedChannels.Reset();
+ m_viewGroupMembers.Reset();
+ m_viewChannelGroups.Reset();
+}
+
+void CGUIDialogPVRGroupManager::Update()
+{
+ m_viewUngroupedChannels.SetCurrentView(CONTROL_LIST_CHANNELS_LEFT);
+ m_viewGroupMembers.SetCurrentView(CONTROL_LIST_CHANNELS_RIGHT);
+ m_viewChannelGroups.SetCurrentView(CONTROL_LIST_CHANNEL_GROUPS);
+
+ Clear();
+
+ // get the groups list
+ CPVRGUIDirectory::GetChannelGroupsDirectory(m_bIsRadio, false, *m_channelGroups);
+
+ // Load group thumbnails
+ m_thumbLoader.Load(*m_channelGroups);
+
+ m_viewChannelGroups.SetItems(*m_channelGroups);
+ m_viewChannelGroups.SetSelectedItem(m_iSelectedChannelGroup);
+
+ /* select a group or select the default group if no group was selected */
+ CFileItemPtr pItem = m_channelGroups->Get(m_viewChannelGroups.GetSelectedItem());
+ m_selectedGroup = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bIsRadio)->GetByName(pItem->m_strTitle);
+ if (m_selectedGroup)
+ {
+ /* set this group in the pvrmanager, so it becomes the selected group in other dialogs too */
+ CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(m_selectedGroup);
+ SET_CONTROL_LABEL(CONTROL_CURRENT_GROUP_LABEL, m_selectedGroup->GroupName());
+ SET_CONTROL_SELECTED(GetID(), BUTTON_HIDE_GROUP, m_selectedGroup->IsHidden());
+
+ CONTROL_ENABLE_ON_CONDITION(BUTTON_DELGROUP, !m_selectedGroup->IsInternalGroup());
+
+ if (m_selectedGroup->IsInternalGroup())
+ {
+ std::string strNewLabel = StringUtils::Format("{} {}", g_localizeStrings.Get(19022),
+ m_bIsRadio ? g_localizeStrings.Get(19024)
+ : g_localizeStrings.Get(19023));
+ SET_CONTROL_LABEL(CONTROL_UNGROUPED_LABEL, strNewLabel);
+
+ strNewLabel = StringUtils::Format("{} {}", g_localizeStrings.Get(19218),
+ m_bIsRadio ? g_localizeStrings.Get(19024)
+ : g_localizeStrings.Get(19023));
+ SET_CONTROL_LABEL(CONTROL_IN_GROUP_LABEL, strNewLabel);
+ }
+ else
+ {
+ std::string strNewLabel = g_localizeStrings.Get(19219);
+ SET_CONTROL_LABEL(CONTROL_UNGROUPED_LABEL, strNewLabel);
+
+ strNewLabel =
+ StringUtils::Format("{} {}", g_localizeStrings.Get(19220), m_selectedGroup->GroupName());
+ SET_CONTROL_LABEL(CONTROL_IN_GROUP_LABEL, strNewLabel);
+ }
+
+ // Slightly different handling for "all" group...
+ if (m_selectedGroup->IsInternalGroup())
+ {
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ m_selectedGroup->GetMembers(CPVRChannelGroup::Include::ALL);
+ for (const auto& groupMember : groupMembers)
+ {
+ if (groupMember->Channel()->IsHidden())
+ m_ungroupedChannels->Add(std::make_shared<CFileItem>(groupMember));
+ else
+ m_groupMembers->Add(std::make_shared<CFileItem>(groupMember));
+ }
+ }
+ else
+ {
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ m_selectedGroup->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ for (const auto& groupMember : groupMembers)
+ {
+ m_groupMembers->Add(std::make_shared<CFileItem>(groupMember));
+ }
+
+ /* for the center part, get all channels of the "all" channels group that are not in this group */
+ const std::shared_ptr<CPVRChannelGroup> allGroup = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio);
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> allGroupMembers =
+ allGroup->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ for (const auto& groupMember : allGroupMembers)
+ {
+ if (!m_selectedGroup->IsGroupMember(groupMember->Channel()))
+ m_ungroupedChannels->Add(std::make_shared<CFileItem>(groupMember));
+ }
+ }
+ m_viewGroupMembers.SetItems(*m_groupMembers);
+ m_viewGroupMembers.SetSelectedItem(m_iSelectedGroupMember);
+
+ m_viewUngroupedChannels.SetItems(*m_ungroupedChannels);
+ m_viewUngroupedChannels.SetSelectedItem(m_iSelectedUngroupedChannel);
+ }
+}
+
+void CGUIDialogPVRGroupManager::Clear()
+{
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ m_viewUngroupedChannels.Clear();
+ m_viewGroupMembers.Clear();
+ m_viewChannelGroups.Clear();
+
+ m_ungroupedChannels->Clear();
+ m_groupMembers->Clear();
+ m_channelGroups->Clear();
+}
+
+void CGUIDialogPVRGroupManager::ClearSelectedGroupsThumbnail()
+{
+ m_thumbLoader.ClearCachedImage(*m_channelGroups->Get(m_iSelectedChannelGroup));
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.h b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.h
new file mode 100644
index 0000000..524205a
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGroupManager.h
@@ -0,0 +1,75 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "pvr/PVRThumbLoader.h"
+#include "view/GUIViewControl.h"
+
+#include <memory>
+
+class CFileItemList;
+class CGUIMessage;
+
+namespace PVR
+{
+ class CPVRChannelGroup;
+
+ class CGUIDialogPVRGroupManager : public CGUIDialog
+ {
+ public:
+ CGUIDialogPVRGroupManager();
+ ~CGUIDialogPVRGroupManager() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+
+ void SetRadio(bool bIsRadio);
+
+ protected:
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+ private:
+ void Clear();
+ void ClearSelectedGroupsThumbnail();
+ void Update();
+ bool PersistChanges();
+ bool ActionButtonOk(const CGUIMessage& message);
+ bool ActionButtonNewGroup(const CGUIMessage& message);
+ bool ActionButtonDeleteGroup(const CGUIMessage& message);
+ bool ActionButtonRenameGroup(const CGUIMessage& message);
+ bool ActionButtonUngroupedChannels(const CGUIMessage& message);
+ bool ActionButtonGroupMembers(const CGUIMessage& message);
+ bool ActionButtonChannelGroups(const CGUIMessage& message);
+ bool ActionButtonHideGroup(const CGUIMessage& message);
+ bool ActionButtonToggleRadioTV(const CGUIMessage& message);
+ bool ActionButtonRecreateThumbnail(const CGUIMessage& message);
+ bool OnMessageClick(const CGUIMessage& message);
+ bool OnActionMove(const CAction& action);
+
+ std::shared_ptr<CPVRChannelGroup> m_selectedGroup;
+ bool m_bIsRadio;
+
+ int m_iSelectedUngroupedChannel = 0;
+ int m_iSelectedGroupMember = 0;
+ int m_iSelectedChannelGroup = 0;
+
+ CFileItemList * m_ungroupedChannels;
+ CFileItemList * m_groupMembers;
+ CFileItemList * m_channelGroups;
+
+ CGUIViewControl m_viewUngroupedChannels;
+ CGUIViewControl m_viewGroupMembers;
+ CGUIViewControl m_viewChannelGroups;
+
+ CPVRThumbLoader m_thumbLoader;
+ };
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.cpp
new file mode 100644
index 0000000..2f41af5
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.cpp
@@ -0,0 +1,19 @@
+/*
+ * 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 "GUIDialogPVRGuideControls.h"
+
+using namespace PVR;
+
+CGUIDialogPVRGuideControls::CGUIDialogPVRGuideControls()
+ : CGUIDialog(WINDOW_DIALOG_PVR_GUIDE_CONTROLS, "DialogPVRGuideControls.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogPVRGuideControls::~CGUIDialogPVRGuideControls() = default;
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.h b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.h
new file mode 100644
index 0000000..9b81f2b
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideControls.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+namespace PVR
+{
+class CGUIDialogPVRGuideControls : public CGUIDialog
+{
+public:
+ CGUIDialogPVRGuideControls();
+ ~CGUIDialogPVRGuideControls() override;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.cpp
new file mode 100644
index 0000000..9bce21e
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.cpp
@@ -0,0 +1,255 @@
+/*
+ * 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 "GUIDialogPVRGuideInfo.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIMessage.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+
+#include <memory>
+
+using namespace PVR;
+
+#define CONTROL_BTN_FIND 4
+#define CONTROL_BTN_SWITCH 5
+#define CONTROL_BTN_RECORD 6
+#define CONTROL_BTN_OK 7
+#define CONTROL_BTN_PLAY_RECORDING 8
+#define CONTROL_BTN_ADD_TIMER 9
+#define CONTROL_BTN_PLAY_EPGTAG 10
+#define CONTROL_BTN_SET_REMINDER 11
+
+CGUIDialogPVRGuideInfo::CGUIDialogPVRGuideInfo()
+ : CGUIDialog(WINDOW_DIALOG_PVR_GUIDE_INFO, "DialogPVRInfo.xml")
+{
+}
+
+CGUIDialogPVRGuideInfo::~CGUIDialogPVRGuideInfo() = default;
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonOK(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_OK)
+ {
+ Close();
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonRecord(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_RECORD)
+ {
+ auto& mgr = CServiceBroker::GetPVRManager();
+
+ const std::shared_ptr<CPVRTimerInfoTag> timerTag =
+ mgr.Timers()->GetTimerForEpgTag(m_progItem->GetEPGInfoTag());
+ if (timerTag)
+ {
+ if (timerTag->IsRecording())
+ bReturn = mgr.Get<PVR::GUI::Timers>().StopRecording(CFileItem(timerTag));
+ else
+ bReturn = mgr.Get<PVR::GUI::Timers>().DeleteTimer(CFileItem(timerTag));
+ }
+ else
+ {
+ bReturn = mgr.Get<PVR::GUI::Timers>().AddTimer(*m_progItem, false);
+ }
+ }
+
+ if (bReturn)
+ Close();
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonAddTimer(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_ADD_TIMER)
+ {
+ auto& mgr = CServiceBroker::GetPVRManager();
+ if (m_progItem && !mgr.Timers()->GetTimerForEpgTag(m_progItem->GetEPGInfoTag()))
+ {
+ bReturn = mgr.Get<PVR::GUI::Timers>().AddTimerRule(*m_progItem, true, true);
+ }
+ }
+
+ if (bReturn)
+ Close();
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonSetReminder(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_SET_REMINDER)
+ {
+ auto& mgr = CServiceBroker::GetPVRManager();
+ if (m_progItem && !mgr.Timers()->GetTimerForEpgTag(m_progItem->GetEPGInfoTag()))
+ {
+ bReturn = mgr.Get<PVR::GUI::Timers>().AddReminder(*m_progItem);
+ }
+ }
+
+ if (bReturn)
+ Close();
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonPlay(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_SWITCH ||
+ message.GetSenderId() == CONTROL_BTN_PLAY_RECORDING ||
+ message.GetSenderId() == CONTROL_BTN_PLAY_EPGTAG)
+ {
+ Close();
+
+ if (m_progItem)
+ {
+ if (message.GetSenderId() == CONTROL_BTN_PLAY_RECORDING)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *m_progItem, true /* bCheckResume */);
+ else if (message.GetSenderId() == CONTROL_BTN_PLAY_EPGTAG &&
+ m_progItem->GetEPGInfoTag()->IsPlayable())
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayEpgTag(*m_progItem);
+ else
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ *m_progItem, true /* bCheckResume */);
+
+ bReturn = true;
+ }
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnClickButtonFind(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_FIND)
+ {
+ Close();
+ if (m_progItem)
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().FindSimilar(*m_progItem);
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRGuideInfo::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ return OnClickButtonOK(message) || OnClickButtonRecord(message) ||
+ OnClickButtonPlay(message) || OnClickButtonFind(message) ||
+ OnClickButtonAddTimer(message) || OnClickButtonSetReminder(message);
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogPVRGuideInfo::OnInfo(int actionID)
+{
+ Close();
+ return true;
+}
+
+void CGUIDialogPVRGuideInfo::SetProgInfo(const std::shared_ptr<CFileItem>& item)
+{
+ m_progItem = item;
+}
+
+CFileItemPtr CGUIDialogPVRGuideInfo::GetCurrentListItem(int offset)
+{
+ return m_progItem;
+}
+
+void CGUIDialogPVRGuideInfo::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ if (!m_progItem)
+ {
+ /* no epg event selected */
+ return;
+ }
+
+ auto& mgr = CServiceBroker::GetPVRManager();
+ const auto epgTag = m_progItem->GetEPGInfoTag();
+
+ if (!mgr.Recordings()->GetRecordingForEpgTag(epgTag))
+ {
+ /* not recording. hide the play recording button */
+ SET_CONTROL_HIDDEN(CONTROL_BTN_PLAY_RECORDING);
+ }
+
+ bool bHideRecord = true;
+ bool bHideAddTimer = true;
+ const std::shared_ptr<CPVRTimerInfoTag> timer = mgr.Timers()->GetTimerForEpgTag(epgTag);
+ bool bHideSetReminder = timer || (epgTag->StartAsLocalTime() <= CDateTime::GetCurrentDateTime());
+
+ if (timer)
+ {
+ if (timer->IsRecording())
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_RECORD, 19059); /* Stop recording */
+ bHideRecord = false;
+ }
+ else if (!timer->GetTimerType()->IsReadOnly())
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_RECORD, 19060); /* Delete timer */
+ bHideRecord = false;
+ }
+ }
+ else if (epgTag->IsRecordable())
+ {
+ const std::shared_ptr<CPVRClient> client = mgr.GetClient(epgTag->ClientID());
+ if (client && client->GetClientCapabilities().SupportsTimers())
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_RECORD, 264); /* Record */
+ bHideRecord = false;
+ bHideAddTimer = false;
+ }
+ }
+
+ if (!epgTag->IsPlayable())
+ SET_CONTROL_HIDDEN(CONTROL_BTN_PLAY_EPGTAG);
+
+ if (bHideRecord)
+ SET_CONTROL_HIDDEN(CONTROL_BTN_RECORD);
+
+ if (bHideAddTimer)
+ SET_CONTROL_HIDDEN(CONTROL_BTN_ADD_TIMER);
+
+ if (bHideSetReminder)
+ SET_CONTROL_HIDDEN(CONTROL_BTN_SET_REMINDER);
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.h b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.h
new file mode 100644
index 0000000..8d9bda5
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideInfo.h
@@ -0,0 +1,44 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+#include <memory>
+
+class CGUIMessage;
+
+namespace PVR
+{
+class CGUIDialogPVRGuideInfo : public CGUIDialog
+{
+public:
+ CGUIDialogPVRGuideInfo();
+ ~CGUIDialogPVRGuideInfo() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnInfo(int actionID) override;
+ bool HasListItems() const override { return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+
+ void SetProgInfo(const std::shared_ptr<CFileItem>& item);
+
+protected:
+ void OnInitWindow() override;
+
+private:
+ bool OnClickButtonOK(const CGUIMessage& message);
+ bool OnClickButtonRecord(const CGUIMessage& message);
+ bool OnClickButtonPlay(const CGUIMessage& message);
+ bool OnClickButtonFind(const CGUIMessage& message);
+ bool OnClickButtonAddTimer(const CGUIMessage& message);
+ bool OnClickButtonSetReminder(const CGUIMessage& message);
+
+ std::shared_ptr<CFileItem> m_progItem;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.cpp b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.cpp
new file mode 100644
index 0000000..4cf7f2f
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.cpp
@@ -0,0 +1,391 @@
+/*
+ * 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 "GUIDialogPVRGuideSearch.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "utils/StringUtils.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+#define CONTROL_EDIT_SEARCH 9
+#define CONTROL_BTN_INC_DESC 10
+#define CONTROL_BTN_CASE_SENS 11
+#define CONTROL_SPIN_MIN_DURATION 12
+#define CONTROL_SPIN_MAX_DURATION 13
+#define CONTROL_EDIT_START_DATE 14
+#define CONTROL_EDIT_STOP_DATE 15
+#define CONTROL_EDIT_START_TIME 16
+#define CONTROL_EDIT_STOP_TIME 17
+#define CONTROL_SPIN_GENRE 18
+#define CONTROL_SPIN_NO_REPEATS 19
+#define CONTROL_BTN_UNK_GENRE 20
+#define CONTROL_SPIN_GROUPS 21
+#define CONTROL_BTN_FTA_ONLY 22
+#define CONTROL_SPIN_CHANNELS 23
+#define CONTROL_BTN_IGNORE_TMR 24
+#define CONTROL_BTN_CANCEL 25
+#define CONTROL_BTN_SEARCH 26
+#define CONTROL_BTN_IGNORE_REC 27
+#define CONTROL_BTN_DEFAULTS 28
+static constexpr int CONTROL_BTN_SAVE = 29;
+static constexpr int CONTROL_BTN_IGNORE_FINISHED = 30;
+static constexpr int CONTROL_BTN_IGNORE_FUTURE = 31;
+
+CGUIDialogPVRGuideSearch::CGUIDialogPVRGuideSearch()
+ : CGUIDialog(WINDOW_DIALOG_PVR_GUIDE_SEARCH, "DialogPVRGuideSearch.xml")
+{
+}
+
+void CGUIDialogPVRGuideSearch::SetFilterData(
+ const std::shared_ptr<CPVREpgSearchFilter>& searchFilter)
+{
+ m_searchFilter = searchFilter;
+}
+
+void CGUIDialogPVRGuideSearch::UpdateChannelSpin()
+{
+ int iChannelGroup = GetSpinValue(CONTROL_SPIN_GROUPS);
+
+ std::vector< std::pair<std::string, int> > labels;
+ if (m_searchFilter->IsRadio())
+ labels.emplace_back(g_localizeStrings.Get(19216), EPG_SEARCH_UNSET); // All radio channels
+ else
+ labels.emplace_back(g_localizeStrings.Get(19217), EPG_SEARCH_UNSET); // All TV channels
+
+ std::shared_ptr<CPVRChannelGroup> group;
+ if (iChannelGroup != EPG_SEARCH_UNSET)
+ group = CServiceBroker::GetPVRManager().ChannelGroups()->GetByIdFromAll(iChannelGroup);
+
+ if (!group)
+ group = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_searchFilter->IsRadio());
+
+ m_channelsMap.clear();
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ group->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ int iIndex = 0;
+ int iSelectedChannel = EPG_SEARCH_UNSET;
+ for (const auto& groupMember : groupMembers)
+ {
+ labels.emplace_back(std::make_pair(groupMember->Channel()->ChannelName(), iIndex));
+ m_channelsMap.insert(std::make_pair(iIndex, groupMember));
+
+ if (iSelectedChannel == EPG_SEARCH_UNSET &&
+ groupMember->ChannelUID() == m_searchFilter->GetChannelUID() &&
+ groupMember->ClientID() == m_searchFilter->GetClientID())
+ iSelectedChannel = iIndex;
+
+ ++iIndex;
+ }
+
+ SET_CONTROL_LABELS(CONTROL_SPIN_CHANNELS, iSelectedChannel, &labels);
+}
+
+void CGUIDialogPVRGuideSearch::UpdateGroupsSpin()
+{
+ std::vector<std::pair<std::string, int>> labels;
+ const std::vector<std::shared_ptr<CPVRChannelGroup>> groups =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_searchFilter->IsRadio())->GetMembers();
+ int selectedGroup = EPG_SEARCH_UNSET;
+ for (const auto& group : groups)
+ {
+ labels.emplace_back(group->GroupName(), group->GroupID());
+
+ if (selectedGroup == EPG_SEARCH_UNSET &&
+ group->GroupID() == m_searchFilter->GetChannelGroupID())
+ selectedGroup = group->GroupID();
+ }
+
+ SET_CONTROL_LABELS(CONTROL_SPIN_GROUPS, selectedGroup, &labels);
+}
+
+void CGUIDialogPVRGuideSearch::UpdateGenreSpin()
+{
+ std::vector< std::pair<std::string, int> > labels;
+ labels.emplace_back(g_localizeStrings.Get(593), EPG_SEARCH_UNSET);
+ labels.emplace_back(g_localizeStrings.Get(19500), EPG_EVENT_CONTENTMASK_MOVIEDRAMA);
+ labels.emplace_back(g_localizeStrings.Get(19516), EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS);
+ labels.emplace_back(g_localizeStrings.Get(19532), EPG_EVENT_CONTENTMASK_SHOW);
+ labels.emplace_back(g_localizeStrings.Get(19548), EPG_EVENT_CONTENTMASK_SPORTS);
+ labels.emplace_back(g_localizeStrings.Get(19564), EPG_EVENT_CONTENTMASK_CHILDRENYOUTH);
+ labels.emplace_back(g_localizeStrings.Get(19580), EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE);
+ labels.emplace_back(g_localizeStrings.Get(19596), EPG_EVENT_CONTENTMASK_ARTSCULTURE);
+ labels.emplace_back(g_localizeStrings.Get(19612), EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS);
+ labels.emplace_back(g_localizeStrings.Get(19628), EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE);
+ labels.emplace_back(g_localizeStrings.Get(19644), EPG_EVENT_CONTENTMASK_LEISUREHOBBIES);
+ labels.emplace_back(g_localizeStrings.Get(19660), EPG_EVENT_CONTENTMASK_SPECIAL);
+ labels.emplace_back(g_localizeStrings.Get(19499), EPG_EVENT_CONTENTMASK_USERDEFINED);
+
+ SET_CONTROL_LABELS(CONTROL_SPIN_GENRE, m_searchFilter->GetGenreType(), &labels);
+}
+
+void CGUIDialogPVRGuideSearch::UpdateDurationSpin()
+{
+ /* minimum duration */
+ std::vector< std::pair<std::string, int> > labels;
+
+ labels.emplace_back("-", EPG_SEARCH_UNSET);
+ for (int i = 1; i < 12*60/5; ++i)
+ labels.emplace_back(StringUtils::Format(g_localizeStrings.Get(14044), i * 5), i * 5);
+
+ SET_CONTROL_LABELS(CONTROL_SPIN_MIN_DURATION, m_searchFilter->GetMinimumDuration(), &labels);
+
+ /* maximum duration */
+ labels.clear();
+
+ labels.emplace_back("-", EPG_SEARCH_UNSET);
+ for (int i = 1; i < 12*60/5; ++i)
+ labels.emplace_back(StringUtils::Format(g_localizeStrings.Get(14044), i * 5), i * 5);
+
+ SET_CONTROL_LABELS(CONTROL_SPIN_MAX_DURATION, m_searchFilter->GetMaximumDuration(), &labels);
+}
+
+bool CGUIDialogPVRGuideSearch::OnMessage(CGUIMessage& message)
+{
+ CGUIDialog::OnMessage(message);
+
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTN_SEARCH)
+ {
+ // Read data from controls, update m_searchfilter accordingly
+ UpdateSearchFilter();
+
+ m_result = Result::SEARCH;
+ Close();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_CANCEL)
+ {
+ m_result = Result::CANCEL;
+ Close();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_DEFAULTS)
+ {
+ if (m_searchFilter)
+ {
+ m_searchFilter->Reset();
+ Update();
+ }
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_SAVE)
+ {
+ // Read data from controls, update m_searchfilter accordingly
+ UpdateSearchFilter();
+
+ std::string title = m_searchFilter->GetTitle();
+ if (title.empty())
+ {
+ title = m_searchFilter->GetSearchTerm();
+ if (title.empty())
+ title = g_localizeStrings.Get(137); // "Search"
+ else
+ StringUtils::Trim(title, "\"");
+
+ if (!CGUIKeyboardFactory::ShowAndGetInput(
+ title, CVariant{g_localizeStrings.Get(528)}, // "Enter title"
+ false))
+ {
+ return false;
+ }
+ m_searchFilter->SetTitle(title);
+ }
+
+ m_result = Result::SAVE;
+ Close();
+ return true;
+ }
+ else if (iControl == CONTROL_SPIN_GROUPS)
+ {
+ UpdateChannelSpin();
+ return true;
+ }
+ }
+ break;
+ }
+
+ return false;
+}
+
+void CGUIDialogPVRGuideSearch::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ m_result = Result::CANCEL;
+}
+
+void CGUIDialogPVRGuideSearch::OnWindowLoaded()
+{
+ Update();
+ return CGUIDialog::OnWindowLoaded();
+}
+
+CDateTime CGUIDialogPVRGuideSearch::ReadDateTime(const std::string& strDate, const std::string& strTime) const
+{
+ CDateTime dateTime;
+ int iHours, iMinutes;
+ sscanf(strTime.c_str(), "%d:%d", &iHours, &iMinutes);
+ dateTime.SetFromDBDate(strDate);
+ dateTime.SetDateTime(dateTime.GetYear(), dateTime.GetMonth(), dateTime.GetDay(), iHours, iMinutes, 0);
+ return dateTime.GetAsUTCDateTime();
+}
+
+bool CGUIDialogPVRGuideSearch::IsRadioSelected(int controlID)
+{
+ CGUIMessage msg(GUI_MSG_IS_SELECTED, GetID(), controlID);
+ OnMessage(msg);
+ return (msg.GetParam1() == 1);
+}
+
+int CGUIDialogPVRGuideSearch::GetSpinValue(int controlID)
+{
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), controlID);
+ OnMessage(msg);
+ return msg.GetParam1();
+}
+
+std::string CGUIDialogPVRGuideSearch::GetEditValue(int controlID)
+{
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), controlID);
+ OnMessage(msg);
+ return msg.GetLabel();
+}
+
+void CGUIDialogPVRGuideSearch::UpdateSearchFilter()
+{
+ if (!m_searchFilter)
+ return;
+
+ m_searchFilter->SetSearchTerm(GetEditValue(CONTROL_EDIT_SEARCH));
+
+ m_searchFilter->SetSearchInDescription(IsRadioSelected(CONTROL_BTN_INC_DESC));
+ m_searchFilter->SetCaseSensitive(IsRadioSelected(CONTROL_BTN_CASE_SENS));
+ m_searchFilter->SetFreeToAirOnly(IsRadioSelected(CONTROL_BTN_FTA_ONLY));
+ m_searchFilter->SetIncludeUnknownGenres(IsRadioSelected(CONTROL_BTN_UNK_GENRE));
+ m_searchFilter->SetIgnorePresentRecordings(IsRadioSelected(CONTROL_BTN_IGNORE_REC));
+ m_searchFilter->SetIgnorePresentTimers(IsRadioSelected(CONTROL_BTN_IGNORE_TMR));
+ m_searchFilter->SetRemoveDuplicates(IsRadioSelected(CONTROL_SPIN_NO_REPEATS));
+ m_searchFilter->SetIgnoreFinishedBroadcasts(IsRadioSelected(CONTROL_BTN_IGNORE_FINISHED));
+ m_searchFilter->SetIgnoreFutureBroadcasts(IsRadioSelected(CONTROL_BTN_IGNORE_FUTURE));
+ m_searchFilter->SetGenreType(GetSpinValue(CONTROL_SPIN_GENRE));
+ m_searchFilter->SetMinimumDuration(GetSpinValue(CONTROL_SPIN_MIN_DURATION));
+ m_searchFilter->SetMaximumDuration(GetSpinValue(CONTROL_SPIN_MAX_DURATION));
+
+ auto it = m_channelsMap.find(GetSpinValue(CONTROL_SPIN_CHANNELS));
+ m_searchFilter->SetClientID(it == m_channelsMap.end() ? -1 : (*it).second->ClientID());
+ m_searchFilter->SetChannelUID(it == m_channelsMap.end() ? -1 : (*it).second->ChannelUID());
+ m_searchFilter->SetChannelGroupID(GetSpinValue(CONTROL_SPIN_GROUPS));
+
+ const CDateTime start =
+ ReadDateTime(GetEditValue(CONTROL_EDIT_START_DATE), GetEditValue(CONTROL_EDIT_START_TIME));
+ if (start != m_startDateTime)
+ {
+ m_searchFilter->SetStartDateTime(start);
+ m_startDateTime = start;
+ }
+ const CDateTime end =
+ ReadDateTime(GetEditValue(CONTROL_EDIT_STOP_DATE), GetEditValue(CONTROL_EDIT_STOP_TIME));
+ if (end != m_endDateTime)
+ {
+ m_searchFilter->SetEndDateTime(end);
+ m_endDateTime = end;
+ }
+}
+
+void CGUIDialogPVRGuideSearch::Update()
+{
+ if (!m_searchFilter)
+ return;
+
+ SET_CONTROL_LABEL2(CONTROL_EDIT_SEARCH, m_searchFilter->GetSearchTerm());
+ {
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_SEARCH, CGUIEditControl::INPUT_TYPE_TEXT, 16017);
+ OnMessage(msg);
+ }
+
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_CASE_SENS, m_searchFilter->IsCaseSensitive());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_INC_DESC, m_searchFilter->ShouldSearchInDescription());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_FTA_ONLY, m_searchFilter->IsFreeToAirOnly());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_UNK_GENRE, m_searchFilter->ShouldIncludeUnknownGenres());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_REC, m_searchFilter->ShouldIgnorePresentRecordings());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_TMR, m_searchFilter->ShouldIgnorePresentTimers());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_SPIN_NO_REPEATS, m_searchFilter->ShouldRemoveDuplicates());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_FINISHED,
+ m_searchFilter->ShouldIgnoreFinishedBroadcasts());
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTN_IGNORE_FUTURE,
+ m_searchFilter->ShouldIgnoreFutureBroadcasts());
+
+ // Set start/end datetime fields
+ m_startDateTime = m_searchFilter->GetStartDateTime();
+ m_endDateTime = m_searchFilter->GetEndDateTime();
+ if (!m_startDateTime.IsValid() || !m_endDateTime.IsValid())
+ {
+ const auto dates = CServiceBroker::GetPVRManager().EpgContainer().GetFirstAndLastEPGDate();
+ if (!m_startDateTime.IsValid())
+ m_startDateTime = dates.first;
+ if (!m_endDateTime.IsValid())
+ m_endDateTime = dates.second;
+ }
+
+ if (!m_startDateTime.IsValid())
+ m_startDateTime = CDateTime::GetUTCDateTime();
+
+ if (!m_endDateTime.IsValid())
+ m_endDateTime = m_startDateTime + CDateTimeSpan(10, 0, 0, 0); // default to start + 10 days
+
+ CDateTime startLocal;
+ startLocal.SetFromUTCDateTime(m_startDateTime);
+ CDateTime endLocal;
+ endLocal.SetFromUTCDateTime(m_endDateTime);
+
+ SET_CONTROL_LABEL2(CONTROL_EDIT_START_TIME, startLocal.GetAsLocalizedTime("", false));
+ {
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_START_TIME, CGUIEditControl::INPUT_TYPE_TIME, 14066);
+ OnMessage(msg);
+ }
+ SET_CONTROL_LABEL2(CONTROL_EDIT_STOP_TIME, endLocal.GetAsLocalizedTime("", false));
+ {
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_STOP_TIME, CGUIEditControl::INPUT_TYPE_TIME, 14066);
+ OnMessage(msg);
+ }
+ SET_CONTROL_LABEL2(CONTROL_EDIT_START_DATE, startLocal.GetAsDBDate());
+ {
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_START_DATE, CGUIEditControl::INPUT_TYPE_DATE, 14067);
+ OnMessage(msg);
+ }
+ SET_CONTROL_LABEL2(CONTROL_EDIT_STOP_DATE, endLocal.GetAsDBDate());
+ {
+ CGUIMessage msg(GUI_MSG_SET_TYPE, GetID(), CONTROL_EDIT_STOP_DATE, CGUIEditControl::INPUT_TYPE_DATE, 14067);
+ OnMessage(msg);
+ }
+
+ UpdateDurationSpin();
+ UpdateGroupsSpin();
+ UpdateChannelSpin();
+ UpdateGenreSpin();
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.h b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.h
new file mode 100644
index 0000000..4d02167
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRGuideSearch.h
@@ -0,0 +1,64 @@
+/*
+ * 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 "XBDateTime.h"
+#include "guilib/GUIDialog.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+namespace PVR
+{
+ class CPVREpgSearchFilter;
+ class CPVRChannelGroupMember;
+
+ class CGUIDialogPVRGuideSearch : public CGUIDialog
+ {
+ public:
+ CGUIDialogPVRGuideSearch();
+ ~CGUIDialogPVRGuideSearch() override = default;
+ bool OnMessage(CGUIMessage& message) override;
+ void OnWindowLoaded() override;
+
+ void SetFilterData(const std::shared_ptr<CPVREpgSearchFilter>& searchFilter);
+
+ enum class Result
+ {
+ SEARCH,
+ SAVE,
+ CANCEL
+ };
+ Result GetResult() const { return m_result; }
+
+ protected:
+ void OnInitWindow() override;
+
+ private:
+ void UpdateSearchFilter();
+ void UpdateChannelSpin();
+ void UpdateGroupsSpin();
+ void UpdateGenreSpin();
+ void UpdateDurationSpin();
+ CDateTime ReadDateTime(const std::string& strDate, const std::string& strTime) const;
+ void Update();
+
+ bool IsRadioSelected(int controlID);
+ int GetSpinValue(int controlID);
+ std::string GetEditValue(int controlID);
+
+ Result m_result = Result::CANCEL;
+ std::shared_ptr<CPVREpgSearchFilter> m_searchFilter;
+ std::map<int, std::shared_ptr<CPVRChannelGroupMember>> m_channelsMap;
+
+ CDateTime m_startDateTime;
+ CDateTime m_endDateTime;
+ };
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.cpp b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.cpp
new file mode 100644
index 0000000..2add783
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.cpp
@@ -0,0 +1,154 @@
+/*
+ * 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 "GUIDialogPVRItemsViewBase.h"
+
+#include "ContextMenuManager.h"
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "view/ViewState.h"
+
+#include <utility>
+
+#define CONTROL_LIST 11
+
+using namespace PVR;
+
+CGUIDialogPVRItemsViewBase::CGUIDialogPVRItemsViewBase(int id, const std::string& xmlFile)
+ : CGUIDialog(id, xmlFile), m_vecItems(new CFileItemList)
+{
+}
+
+void CGUIDialogPVRItemsViewBase::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+ m_viewControl.Reset();
+ m_viewControl.SetParentWindow(GetID());
+ m_viewControl.AddView(GetControl(CONTROL_LIST));
+}
+
+void CGUIDialogPVRItemsViewBase::OnWindowUnload()
+{
+ CGUIDialog::OnWindowUnload();
+ m_viewControl.Reset();
+}
+
+void CGUIDialogPVRItemsViewBase::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+}
+
+void CGUIDialogPVRItemsViewBase::OnDeinitWindow(int nextWindowID)
+{
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+ Clear();
+}
+
+bool CGUIDialogPVRItemsViewBase::OnAction(const CAction& action)
+{
+ if (m_viewControl.HasControl(GetFocusedControlID()))
+ {
+ switch (action.GetID())
+ {
+ case ACTION_SHOW_INFO:
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ ShowInfo(m_viewControl.GetSelectedItem());
+ return true;
+
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ return ContextMenu(m_viewControl.GetSelectedItem());
+
+ default:
+ break;
+ }
+ }
+ return CGUIDialog::OnAction(action);
+}
+
+CGUIControl* CGUIDialogPVRItemsViewBase::GetFirstFocusableControl(int id)
+{
+ if (m_viewControl.HasControl(id))
+ id = m_viewControl.GetCurrentControl();
+
+ return CGUIDialog::GetFirstFocusableControl(id);
+}
+
+void CGUIDialogPVRItemsViewBase::ShowInfo(int itemIdx)
+{
+ if (itemIdx < 0 || itemIdx >= m_vecItems->Size())
+ return;
+
+ const std::shared_ptr<CFileItem> item = m_vecItems->Get(itemIdx);
+ if (!item)
+ return;
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*item);
+}
+
+bool CGUIDialogPVRItemsViewBase::ContextMenu(int itemIdx)
+{
+ auto InRange = [](size_t i, std::pair<size_t, size_t> range) {
+ return i >= range.first && i < range.second;
+ };
+
+ if (itemIdx < 0 || itemIdx >= m_vecItems->Size())
+ return false;
+
+ const CFileItemPtr item = m_vecItems->Get(itemIdx);
+ if (!item)
+ return false;
+
+ CContextButtons buttons;
+
+ // Add the global menu
+ const ContextMenuView globalMenu =
+ CServiceBroker::GetContextMenuManager().GetItems(*item, CContextMenuManager::MAIN);
+ auto globalMenuRange = std::make_pair(buttons.size(), buttons.size() + globalMenu.size());
+ for (const auto& menu : globalMenu)
+ buttons.emplace_back(~buttons.size(), menu->GetLabel(*item));
+
+ // Add addon menus
+ const ContextMenuView addonMenu =
+ CServiceBroker::GetContextMenuManager().GetAddonItems(*item, CContextMenuManager::MAIN);
+ auto addonMenuRange = std::make_pair(buttons.size(), buttons.size() + addonMenu.size());
+ for (const auto& menu : addonMenu)
+ buttons.emplace_back(~buttons.size(), menu->GetLabel(*item));
+
+ if (buttons.empty())
+ return true;
+
+ int idx = CGUIDialogContextMenu::Show(buttons);
+ if (idx < 0 || idx >= static_cast<int>(buttons.size()))
+ return false;
+
+ Close();
+
+ if (InRange(idx, globalMenuRange))
+ return CONTEXTMENU::LoopFrom(*globalMenu[idx - globalMenuRange.first], item);
+
+ return CONTEXTMENU::LoopFrom(*addonMenu[idx - addonMenuRange.first], item);
+}
+
+void CGUIDialogPVRItemsViewBase::Init()
+{
+ m_viewControl.SetCurrentView(DEFAULT_VIEW_LIST);
+ Clear();
+}
+
+void CGUIDialogPVRItemsViewBase::Clear()
+{
+ m_viewControl.Clear();
+ m_vecItems->Clear();
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.h b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.h
new file mode 100644
index 0000000..66530e4
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRItemsViewBase.h
@@ -0,0 +1,47 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "view/GUIViewControl.h"
+
+#include <memory>
+#include <string>
+
+class CFileItemList;
+
+namespace PVR
+{
+class CGUIDialogPVRItemsViewBase : public CGUIDialog
+{
+public:
+ CGUIDialogPVRItemsViewBase() = delete;
+ CGUIDialogPVRItemsViewBase(int id, const std::string& xmlFile);
+ ~CGUIDialogPVRItemsViewBase() override = default;
+
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ bool OnAction(const CAction& action) override;
+
+protected:
+ void Init();
+
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ CGUIControl* GetFirstFocusableControl(int id) override;
+
+ std::unique_ptr<CFileItemList> m_vecItems;
+ CGUIViewControl m_viewControl;
+
+private:
+ void Clear();
+ void ShowInfo(int itemIdx);
+ bool ContextMenu(int iItemIdx);
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.cpp b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.cpp
new file mode 100644
index 0000000..ead92d2
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.cpp
@@ -0,0 +1,216 @@
+/*
+ * 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 "GUIDialogPVRRadioRDSInfo.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUISpinControl.h"
+#include "guilib/GUITextBox.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRRadioRDSInfoTag.h"
+
+using namespace PVR;
+
+#define CONTROL_BTN_OK 10
+#define SPIN_CONTROL_INFO 21
+#define TEXT_INFO 22
+#define CONTROL_NEXT_PAGE 60
+#define CONTROL_INFO_LIST 70
+
+#define INFO_NEWS 1
+#define INFO_NEWS_LOCAL 2
+#define INFO_SPORT 3
+#define INFO_WEATHER 4
+#define INFO_LOTTERY 5
+#define INFO_STOCK 6
+#define INFO_OTHER 7
+#define INFO_CINEMA 8
+#define INFO_HOROSCOPE 9
+
+CGUIDialogPVRRadioRDSInfo::CGUIDialogPVRRadioRDSInfo()
+ : CGUIDialog(WINDOW_DIALOG_PVR_RADIO_RDS_INFO, "DialogPVRRadioRDSInfo.xml")
+ , m_InfoNews(29916, INFO_NEWS)
+ , m_InfoNewsLocal(29917, INFO_NEWS_LOCAL)
+ , m_InfoSport(29918, INFO_SPORT)
+ , m_InfoWeather(400, INFO_WEATHER)
+ , m_InfoLottery(29919, INFO_LOTTERY)
+ , m_InfoStock(29920, INFO_STOCK)
+ , m_InfoOther(29921, INFO_OTHER)
+ , m_InfoCinema(19602, INFO_CINEMA)
+ , m_InfoHoroscope(29922, INFO_HOROSCOPE)
+{
+}
+
+bool CGUIDialogPVRRadioRDSInfo::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_CLICKED)
+ {
+ int iControl = message.GetSenderId();
+
+ if (iControl == CONTROL_BTN_OK)
+ {
+ Close();
+ return true;
+ }
+ else if (iControl == SPIN_CONTROL_INFO)
+ {
+ const std::shared_ptr<CPVRChannel> channel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (!channel)
+ return false;
+
+ const std::shared_ptr<CPVRRadioRDSInfoTag> currentRDS = channel->GetRadioRDSInfoTag();
+ if (!currentRDS)
+ return false;
+
+ const CGUISpinControl* spin = static_cast<CGUISpinControl*>(GetControl(SPIN_CONTROL_INFO));
+ if (!spin)
+ return false;
+
+ CGUITextBox* textbox = static_cast<CGUITextBox*>(GetControl(TEXT_INFO));
+ if (!textbox)
+ return false;
+
+ switch (spin->GetValue())
+ {
+ case INFO_NEWS:
+ textbox->SetInfo(currentRDS->GetInfoNews());
+ break;
+ case INFO_NEWS_LOCAL:
+ textbox->SetInfo(currentRDS->GetInfoNewsLocal());
+ break;
+ case INFO_SPORT:
+ textbox->SetInfo(currentRDS->GetInfoSport());
+ break;
+ case INFO_WEATHER:
+ textbox->SetInfo(currentRDS->GetInfoWeather());
+ break;
+ case INFO_LOTTERY:
+ textbox->SetInfo(currentRDS->GetInfoLottery());
+ break;
+ case INFO_STOCK:
+ textbox->SetInfo(currentRDS->GetInfoStock());
+ break;
+ case INFO_OTHER:
+ textbox->SetInfo(currentRDS->GetInfoOther());
+ break;
+ case INFO_CINEMA:
+ textbox->SetInfo(currentRDS->GetInfoCinema());
+ break;
+ case INFO_HOROSCOPE:
+ textbox->SetInfo(currentRDS->GetInfoHoroscope());
+ break;
+ }
+
+ SET_CONTROL_VISIBLE(CONTROL_INFO_LIST);
+ }
+ }
+ else if (message.GetMessage() == GUI_MSG_NOTIFY_ALL)
+ {
+ if (message.GetParam1() == GUI_MSG_UPDATE_RADIOTEXT && IsActive())
+ {
+ UpdateInfoControls();
+ }
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogPVRRadioRDSInfo::OnInitWindow()
+{
+ CGUIDialog::OnInitWindow();
+
+ InitInfoControls();
+}
+
+void CGUIDialogPVRRadioRDSInfo::InitInfoControls()
+{
+ SET_CONTROL_HIDDEN(CONTROL_INFO_LIST);
+
+ CGUISpinControl* spin = static_cast<CGUISpinControl*>(GetControl(SPIN_CONTROL_INFO));
+ if (spin)
+ spin->Clear();
+
+ CGUITextBox* textbox = static_cast<CGUITextBox*>(GetControl(TEXT_INFO));
+
+ m_InfoNews.Init(spin, textbox);
+ m_InfoNewsLocal.Init(spin, textbox);
+ m_InfoSport.Init(spin, textbox);
+ m_InfoWeather.Init(spin, textbox);
+ m_InfoLottery.Init(spin, textbox);
+ m_InfoStock.Init(spin, textbox);
+ m_InfoOther.Init(spin, textbox);
+ m_InfoCinema.Init(spin, textbox);
+ m_InfoHoroscope.Init(spin, textbox);
+
+ if (spin && textbox)
+ UpdateInfoControls();
+}
+
+void CGUIDialogPVRRadioRDSInfo::UpdateInfoControls()
+{
+ const std::shared_ptr<CPVRChannel> channel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (!channel)
+ return;
+
+ const std::shared_ptr<CPVRRadioRDSInfoTag> currentRDS = channel->GetRadioRDSInfoTag();
+ if (!currentRDS)
+ return;
+
+ bool bInfoPresent = m_InfoNews.Update(currentRDS->GetInfoNews());
+ bInfoPresent |= m_InfoNewsLocal.Update(currentRDS->GetInfoNewsLocal());
+ bInfoPresent |= m_InfoSport.Update(currentRDS->GetInfoSport());
+ bInfoPresent |= m_InfoWeather.Update(currentRDS->GetInfoWeather());
+ bInfoPresent |= m_InfoLottery.Update(currentRDS->GetInfoLottery());
+ bInfoPresent |= m_InfoStock.Update(currentRDS->GetInfoStock());
+ bInfoPresent |= m_InfoOther.Update(currentRDS->GetInfoOther());
+ bInfoPresent |= m_InfoCinema.Update(currentRDS->GetInfoCinema());
+ bInfoPresent |= m_InfoHoroscope.Update(currentRDS->GetInfoHoroscope());
+
+ if (bInfoPresent)
+ SET_CONTROL_VISIBLE(CONTROL_INFO_LIST);
+}
+
+CGUIDialogPVRRadioRDSInfo::InfoControl::InfoControl(uint32_t iSpinLabelId, uint32_t iSpinControlId)
+: m_iSpinLabelId(iSpinLabelId),
+ m_iSpinControlId(iSpinControlId)
+{
+}
+
+void CGUIDialogPVRRadioRDSInfo::InfoControl::Init(CGUISpinControl* spin, CGUITextBox* textbox)
+{
+ m_spinControl = spin;
+ m_textbox = textbox;
+ m_bSpinLabelPresent = false;
+ m_textboxValue.clear();
+}
+
+bool CGUIDialogPVRRadioRDSInfo::InfoControl::Update(const std::string& textboxValue)
+{
+ if (m_spinControl && m_textbox && !textboxValue.empty())
+ {
+ if (!m_bSpinLabelPresent)
+ {
+ m_spinControl->AddLabel(g_localizeStrings.Get(m_iSpinLabelId), m_iSpinControlId);
+ m_bSpinLabelPresent = true;
+ }
+
+ if (m_textboxValue != textboxValue)
+ {
+ m_spinControl->SetValue(m_iSpinControlId);
+ m_textboxValue = textboxValue;
+ m_textbox->SetInfo(textboxValue);
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.h b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.h
new file mode 100644
index 0000000..4ae1b0b
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRadioRDSInfo.h
@@ -0,0 +1,60 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+#include <string>
+
+class CGUISpinControl;
+class CGUITextBox;
+
+namespace PVR
+{
+ class CGUIDialogPVRRadioRDSInfo : public CGUIDialog
+ {
+ public:
+ CGUIDialogPVRRadioRDSInfo();
+ ~CGUIDialogPVRRadioRDSInfo() override = default;
+ bool OnMessage(CGUIMessage& message) override;
+
+ protected:
+ void OnInitWindow() override;
+
+ private:
+ class InfoControl
+ {
+ public:
+ InfoControl(uint32_t iSpinLabelId, uint32_t iSpinControlId);
+ void Init(CGUISpinControl* spin, CGUITextBox* textbox);
+ bool Update(const std::string& textboxValue);
+
+ private:
+ CGUISpinControl* m_spinControl = nullptr;
+ uint32_t m_iSpinLabelId = 0;
+ uint32_t m_iSpinControlId = 0;
+ CGUITextBox* m_textbox = nullptr;
+ bool m_bSpinLabelPresent = false;
+ std::string m_textboxValue;
+ };
+
+ void InitInfoControls();
+ void UpdateInfoControls();
+
+ InfoControl m_InfoNews;
+ InfoControl m_InfoNewsLocal;
+ InfoControl m_InfoSport;
+ InfoControl m_InfoWeather;
+ InfoControl m_InfoLottery;
+ InfoControl m_InfoStock;
+ InfoControl m_InfoOther;
+ InfoControl m_InfoCinema;
+ InfoControl m_InfoHoroscope;
+ };
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.cpp b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.cpp
new file mode 100644
index 0000000..9396083
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.cpp
@@ -0,0 +1,102 @@
+/*
+ * 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 "GUIDialogPVRRecordingInfo.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIMessage.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+
+using namespace PVR;
+
+#define CONTROL_BTN_FIND 4
+#define CONTROL_BTN_OK 7
+#define CONTROL_BTN_PLAY_RECORDING 8
+
+CGUIDialogPVRRecordingInfo::CGUIDialogPVRRecordingInfo()
+ : CGUIDialog(WINDOW_DIALOG_PVR_RECORDING_INFO, "DialogPVRInfo.xml"), m_recordItem(new CFileItem)
+{
+}
+
+bool CGUIDialogPVRRecordingInfo::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ return OnClickButtonOK(message) || OnClickButtonPlay(message) || OnClickButtonFind(message);
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogPVRRecordingInfo::OnClickButtonOK(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_OK)
+ {
+ Close();
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRRecordingInfo::OnClickButtonPlay(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_PLAY_RECORDING)
+ {
+ Close();
+
+ if (m_recordItem)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *m_recordItem, true /* check resume */);
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRRecordingInfo::OnClickButtonFind(const CGUIMessage& message)
+{
+ bool bReturn = false;
+
+ if (message.GetSenderId() == CONTROL_BTN_FIND)
+ {
+ Close();
+
+ if (m_recordItem)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().FindSimilar(*m_recordItem);
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIDialogPVRRecordingInfo::OnInfo(int actionID)
+{
+ Close();
+ return true;
+}
+
+void CGUIDialogPVRRecordingInfo::SetRecording(const CFileItem& item)
+{
+ m_recordItem = std::make_shared<CFileItem>(item);
+}
+
+CFileItemPtr CGUIDialogPVRRecordingInfo::GetCurrentListItem(int offset)
+{
+ return m_recordItem;
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.h b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.h
new file mode 100644
index 0000000..b22e0d7
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingInfo.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CFileItem;
+class CGUIMessage;
+
+namespace PVR
+{
+class CGUIDialogPVRRecordingInfo : public CGUIDialog
+{
+public:
+ CGUIDialogPVRRecordingInfo();
+ ~CGUIDialogPVRRecordingInfo() override = default;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnInfo(int actionID) override;
+ bool HasListItems() const override { return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+
+ void SetRecording(const CFileItem& item);
+
+private:
+ bool OnClickButtonFind(const CGUIMessage& message);
+ bool OnClickButtonOK(const CGUIMessage& message);
+ bool OnClickButtonPlay(const CGUIMessage& message);
+
+ CFileItemPtr m_recordItem;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.cpp b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.cpp
new file mode 100644
index 0000000..ed8cd7b
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.cpp
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2017-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 "GUIDialogPVRRecordingSettings.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "settings/dialogs/GUIDialogSettingsBase.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+#define SETTING_RECORDING_NAME "recording.name"
+#define SETTING_RECORDING_PLAYCOUNT "recording.playcount"
+#define SETTING_RECORDING_LIFETIME "recording.lifetime"
+
+CGUIDialogPVRRecordingSettings::CGUIDialogPVRRecordingSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_PVR_RECORDING_SETTING, "DialogSettings.xml")
+{
+ m_loadType = LOAD_EVERY_TIME;
+}
+
+void CGUIDialogPVRRecordingSettings::SetRecording(const std::shared_ptr<CPVRRecording>& recording)
+{
+ if (!recording)
+ {
+ CLog::LogF(LOGERROR, "No recording given");
+ return;
+ }
+
+ m_recording = recording;
+
+ // Copy data we need from tag. Do not modify the tag itself until Save()!
+ m_strTitle = m_recording->m_strTitle;
+ m_iPlayCount = m_recording->GetLocalPlayCount();
+ m_iLifetime = m_recording->LifeTime();
+}
+
+void CGUIDialogPVRRecordingSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+ SetHeading(19068); // Recording settings
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186); // OK
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222); // Cancel
+}
+
+void CGUIDialogPVRRecordingSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("pvrrecordingsettings", -1);
+ if (category == nullptr)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings category");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == nullptr)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings group");
+ return;
+ }
+
+ std::shared_ptr<CSetting> setting = nullptr;
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_recording->ClientID());
+
+ // Name
+ setting = AddEdit(group, SETTING_RECORDING_NAME, 19075, SettingLevel::Basic, m_strTitle);
+ setting->SetEnabled(client && client->GetClientCapabilities().SupportsRecordingsRename());
+
+ // Play count
+ if (client && client->GetClientCapabilities().SupportsRecordingsPlayCount())
+ setting = AddEdit(group, SETTING_RECORDING_PLAYCOUNT, 567, SettingLevel::Basic,
+ m_recording->GetLocalPlayCount());
+
+ // Lifetime
+ if (client && client->GetClientCapabilities().SupportsRecordingsLifetimeChange())
+ setting = AddList(group, SETTING_RECORDING_LIFETIME, 19083, SettingLevel::Basic, m_iLifetime,
+ LifetimesFiller, 19083);
+}
+
+bool CGUIDialogPVRRecordingSettings::CanEditRecording(const CFileItem& item)
+{
+ if (!item.HasPVRRecordingInfoTag())
+ return false;
+
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(item.GetPVRRecordingInfoTag()->ClientID());
+
+ if (!client)
+ return false;
+
+ const CPVRClientCapabilities& capabilities = client->GetClientCapabilities();
+
+ return capabilities.SupportsRecordingsRename() || capabilities.SupportsRecordingsPlayCount() ||
+ capabilities.SupportsRecordingsLifetimeChange();
+}
+
+bool CGUIDialogPVRRecordingSettings::OnSettingChanging(
+ const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ {
+ CLog::LogF(LOGERROR, "No setting");
+ return false;
+ }
+
+ const std::string& settingId = setting->GetId();
+
+ if (settingId == SETTING_RECORDING_LIFETIME)
+ {
+ int iNewLifetime = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ if (m_recording->WillBeExpiredWithNewLifetime(iNewLifetime))
+ {
+ if (HELPERS::ShowYesNoDialogText(
+ CVariant{19068}, // "Recording settings"
+ StringUtils::Format(g_localizeStrings.Get(19147),
+ iNewLifetime)) // "Setting the lifetime..."
+ != HELPERS::DialogResponse::CHOICE_YES)
+ return false;
+ }
+ }
+
+ return CGUIDialogSettingsManualBase::OnSettingChanging(setting);
+}
+
+void CGUIDialogPVRRecordingSettings::OnSettingChanged(
+ const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ {
+ CLog::LogF(LOGERROR, "No setting");
+ return;
+ }
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string& settingId = setting->GetId();
+
+ if (settingId == SETTING_RECORDING_NAME)
+ {
+ m_strTitle = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_RECORDING_PLAYCOUNT)
+ {
+ m_iPlayCount = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_RECORDING_LIFETIME)
+ {
+ m_iLifetime = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+}
+
+bool CGUIDialogPVRRecordingSettings::Save()
+{
+ // Name
+ m_recording->m_strTitle = m_strTitle;
+
+ // Play count
+ m_recording->SetLocalPlayCount(m_iPlayCount);
+
+ // Lifetime
+ m_recording->SetLifeTime(m_iLifetime);
+
+ return true;
+}
+
+void CGUIDialogPVRRecordingSettings::LifetimesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRRecordingSettings* pThis = static_cast<CGUIDialogPVRRecordingSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(pThis->m_recording->ClientID());
+ if (client)
+ {
+ std::vector<std::pair<std::string, int>> values;
+ client->GetClientCapabilities().GetRecordingsLifetimeValues(values);
+ std::transform(
+ values.cbegin(), values.cend(), std::back_inserter(list),
+ [](const auto& value) { return IntegerSettingOption(value.first, value.second); });
+ }
+
+ current = pThis->m_iLifetime;
+
+ auto it = list.begin();
+ while (it != list.end())
+ {
+ if (it->value == current)
+ break; // value already in list
+
+ ++it;
+ }
+
+ if (it == list.end())
+ {
+ // PVR backend supplied value is not in the list of predefined values. Insert it.
+ list.insert(it, IntegerSettingOption(
+ StringUtils::Format(g_localizeStrings.Get(17999), current) /* {} days */,
+ current));
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.h b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.h
new file mode 100644
index 0000000..f0de299
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRRecordingSettings.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017-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/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CSetting;
+
+struct IntegerSettingOption;
+
+namespace PVR
+{
+class CPVRRecording;
+
+class CGUIDialogPVRRecordingSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogPVRRecordingSettings();
+
+ void SetRecording(const std::shared_ptr<CPVRRecording>& recording);
+ static bool CanEditRecording(const CFileItem& item);
+
+protected:
+ // implementation of ISettingCallback
+ bool OnSettingChanging(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+private:
+ static void LifetimesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ std::shared_ptr<CPVRRecording> m_recording;
+ std::string m_strTitle;
+ int m_iPlayCount = 0;
+ int m_iLifetime = 0;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.cpp b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.cpp
new file mode 100644
index 0000000..f786627
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.cpp
@@ -0,0 +1,1513 @@
+/*
+ * 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 "GUIDialogPVRTimerSettings.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimerType.h"
+#include "settings/SettingUtils.h"
+#include "settings/dialogs/GUIDialogSettingsBase.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingsManager.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+#define SETTING_TMR_TYPE "timer.type"
+#define SETTING_TMR_ACTIVE "timer.active"
+#define SETTING_TMR_NAME "timer.name"
+#define SETTING_TMR_EPGSEARCH "timer.epgsearch"
+#define SETTING_TMR_FULLTEXT "timer.fulltext"
+#define SETTING_TMR_CHANNEL "timer.channel"
+#define SETTING_TMR_START_ANYTIME "timer.startanytime"
+#define SETTING_TMR_END_ANYTIME "timer.endanytime"
+#define SETTING_TMR_START_DAY "timer.startday"
+#define SETTING_TMR_END_DAY "timer.endday"
+#define SETTING_TMR_BEGIN "timer.begin"
+#define SETTING_TMR_END "timer.end"
+#define SETTING_TMR_WEEKDAYS "timer.weekdays"
+#define SETTING_TMR_FIRST_DAY "timer.firstday"
+#define SETTING_TMR_NEW_EPISODES "timer.newepisodes"
+#define SETTING_TMR_BEGIN_PRE "timer.startmargin"
+#define SETTING_TMR_END_POST "timer.endmargin"
+#define SETTING_TMR_PRIORITY "timer.priority"
+#define SETTING_TMR_LIFETIME "timer.lifetime"
+#define SETTING_TMR_MAX_REC "timer.maxrecordings"
+#define SETTING_TMR_DIR "timer.directory"
+#define SETTING_TMR_REC_GROUP "timer.recgroup"
+
+#define TYPE_DEP_VISIBI_COND_ID_POSTFIX "visibi.typedep"
+#define TYPE_DEP_ENABLE_COND_ID_POSTFIX "enable.typedep"
+#define CHANNEL_DEP_VISIBI_COND_ID_POSTFIX "visibi.channeldep"
+#define START_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX "visibi.startanytimedep"
+#define END_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX "visibi.endanytimedep"
+
+CGUIDialogPVRTimerSettings::CGUIDialogPVRTimerSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_PVR_TIMER_SETTING, "DialogSettings.xml"),
+ m_iWeekdays(PVR_WEEKDAY_NONE)
+{
+ m_loadType = LOAD_EVERY_TIME;
+}
+
+CGUIDialogPVRTimerSettings::~CGUIDialogPVRTimerSettings() = default;
+
+bool CGUIDialogPVRTimerSettings::CanBeActivated() const
+{
+ if (!m_timerInfoTag)
+ {
+ CLog::LogF(LOGERROR, "No timer info tag");
+ return false;
+ }
+ return true;
+}
+
+void CGUIDialogPVRTimerSettings::SetTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer)
+{
+ if (!timer)
+ {
+ CLog::LogF(LOGERROR, "No timer given");
+ return;
+ }
+
+ m_timerInfoTag = timer;
+
+ // Copy data we need from tag. Do not modify the tag itself until Save()!
+ m_timerType = m_timerInfoTag->GetTimerType();
+ m_bIsRadio = m_timerInfoTag->m_bIsRadio;
+ m_bIsNewTimer = m_timerInfoTag->m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX;
+ m_bTimerActive = m_bIsNewTimer || !m_timerType->SupportsEnableDisable() ||
+ !(m_timerInfoTag->m_state == PVR_TIMER_STATE_DISABLED);
+ m_bStartAnyTime =
+ m_bIsNewTimer || !m_timerType->SupportsStartAnyTime() || m_timerInfoTag->m_bStartAnyTime;
+ m_bEndAnyTime =
+ m_bIsNewTimer || !m_timerType->SupportsEndAnyTime() || m_timerInfoTag->m_bEndAnyTime;
+ m_strTitle = m_timerInfoTag->m_strTitle;
+
+ m_startLocalTime = m_timerInfoTag->StartAsLocalTime();
+ m_endLocalTime = m_timerInfoTag->EndAsLocalTime();
+
+ m_timerStartTimeStr = m_startLocalTime.GetAsLocalizedTime("", false);
+ m_timerEndTimeStr = m_endLocalTime.GetAsLocalizedTime("", false);
+ m_firstDayLocalTime = m_timerInfoTag->FirstDayAsLocalTime();
+
+ m_strEpgSearchString = m_timerInfoTag->m_strEpgSearchString;
+ if ((m_bIsNewTimer || !m_timerType->SupportsEpgTitleMatch()) && m_strEpgSearchString.empty())
+ m_strEpgSearchString = m_strTitle;
+
+ m_bFullTextEpgSearch = m_timerInfoTag->m_bFullTextEpgSearch;
+
+ m_iWeekdays = m_timerInfoTag->m_iWeekdays;
+ if ((m_bIsNewTimer || !m_timerType->SupportsWeekdays()) && m_iWeekdays == PVR_WEEKDAY_NONE)
+ m_iWeekdays = PVR_WEEKDAY_ALLDAYS;
+
+ m_iPreventDupEpisodes = m_timerInfoTag->m_iPreventDupEpisodes;
+ m_iMarginStart = m_timerInfoTag->m_iMarginStart;
+ m_iMarginEnd = m_timerInfoTag->m_iMarginEnd;
+ m_iPriority = m_timerInfoTag->m_iPriority;
+ m_iLifetime = m_timerInfoTag->m_iLifetime;
+ m_iMaxRecordings = m_timerInfoTag->m_iMaxRecordings;
+
+ if (m_bIsNewTimer && m_timerInfoTag->m_strDirectory.empty() &&
+ m_timerType->SupportsRecordingFolders())
+ m_strDirectory = m_strTitle;
+ else
+ m_strDirectory = m_timerInfoTag->m_strDirectory;
+
+ m_iRecordingGroup = m_timerInfoTag->m_iRecordingGroup;
+
+ InitializeChannelsList();
+ InitializeTypesList();
+
+ // Channel
+ m_channel = ChannelDescriptor();
+
+ if (m_timerInfoTag->m_iClientChannelUid == PVR_CHANNEL_INVALID_UID)
+ {
+ if (m_timerType->SupportsAnyChannel())
+ {
+ // Select first matching "Any channel" entry.
+ const auto it = std::find_if(m_channelEntries.cbegin(), m_channelEntries.cend(),
+ [this](const auto& channel) {
+ return channel.second.channelUid == PVR_CHANNEL_INVALID_UID &&
+ channel.second.clientId == m_timerInfoTag->m_iClientId;
+ });
+
+ if (it != m_channelEntries.cend())
+ {
+ m_channel = (*it).second;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to map PVR_CHANNEL_INVALID_UID to channel entry!");
+ }
+ }
+ else if (m_bIsNewTimer)
+ {
+ // Select first matching regular (not "Any channel") entry.
+ const auto it = std::find_if(m_channelEntries.cbegin(), m_channelEntries.cend(),
+ [this](const auto& channel) {
+ return channel.second.channelUid != PVR_CHANNEL_INVALID_UID &&
+ channel.second.clientId == m_timerInfoTag->m_iClientId;
+ });
+
+ if (it != m_channelEntries.cend())
+ {
+ m_channel = (*it).second;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to map PVR_CHANNEL_INVALID_UID to channel entry!");
+ }
+ }
+ }
+ else
+ {
+ // Find matching channel entry
+ const auto it = std::find_if(
+ m_channelEntries.cbegin(), m_channelEntries.cend(), [this](const auto& channel) {
+ return channel.second.channelUid == m_timerInfoTag->m_iClientChannelUid &&
+ channel.second.clientId == m_timerInfoTag->m_iClientId;
+ });
+
+ if (it != m_channelEntries.cend())
+ {
+ m_channel = (*it).second;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to map channel uid to channel entry!");
+ }
+ }
+}
+
+void CGUIDialogPVRTimerSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+ SetHeading(19065);
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+ SetButtonLabels();
+}
+
+void CGUIDialogPVRTimerSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("pvrtimersettings", -1);
+ if (category == NULL)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings category");
+ return;
+ }
+
+ const std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == NULL)
+ {
+ CLog::LogF(LOGERROR, "Unable to add settings group");
+ return;
+ }
+
+ std::shared_ptr<CSetting> setting = NULL;
+
+ // Timer type
+ bool useDetails = false;
+ bool foundClientSupportingTimers = false;
+
+ const CPVRClientMap clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients();
+ for (const auto& client : clients)
+ {
+ if (client.second->GetClientCapabilities().SupportsTimers())
+ {
+ if (foundClientSupportingTimers)
+ {
+ // found second client supporting timers, use detailed timer type list layout
+ useDetails = true;
+ break;
+ }
+ foundClientSupportingTimers = true;
+ }
+ }
+
+ setting = AddList(group, SETTING_TMR_TYPE, 803, SettingLevel::Basic, 0, TypesFiller, 803, true,
+ -1, useDetails);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_TYPE);
+
+ // Timer enabled/disabled
+ setting = AddToggle(group, SETTING_TMR_ACTIVE, 305, SettingLevel::Basic, m_bTimerActive);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_ACTIVE);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_ACTIVE);
+
+ // Name
+ setting =
+ AddEdit(group, SETTING_TMR_NAME, 19075, SettingLevel::Basic, m_strTitle, true, false, 19097);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_NAME);
+
+ // epg search string (only for epg-based timer rules)
+ setting = AddEdit(group, SETTING_TMR_EPGSEARCH, 804, SettingLevel::Basic, m_strEpgSearchString,
+ true, false, 805);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_EPGSEARCH);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_EPGSEARCH);
+
+ // epg fulltext search (only for epg-based timer rules)
+ setting = AddToggle(group, SETTING_TMR_FULLTEXT, 806, SettingLevel::Basic, m_bFullTextEpgSearch);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_FULLTEXT);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_FULLTEXT);
+
+ // Channel
+ setting =
+ AddList(group, SETTING_TMR_CHANNEL, 19078, SettingLevel::Basic, 0, ChannelsFiller, 19078);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_CHANNEL);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_CHANNEL);
+
+ // Days of week (only for timer rules)
+ std::vector<int> weekdaysPreselect;
+ if (m_iWeekdays & PVR_WEEKDAY_MONDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_MONDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_TUESDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_TUESDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_WEDNESDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_WEDNESDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_THURSDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_THURSDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_FRIDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_FRIDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_SATURDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_SATURDAY);
+ if (m_iWeekdays & PVR_WEEKDAY_SUNDAY)
+ weekdaysPreselect.push_back(PVR_WEEKDAY_SUNDAY);
+
+ setting = AddList(group, SETTING_TMR_WEEKDAYS, 19079, SettingLevel::Basic, weekdaysPreselect,
+ WeekdaysFiller, 19079, 1, -1, true, -1, WeekdaysValueFormatter);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_WEEKDAYS);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_WEEKDAYS);
+
+ // "Start any time" (only for timer rules)
+ setting = AddToggle(group, SETTING_TMR_START_ANYTIME, 810, SettingLevel::Basic, m_bStartAnyTime);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_START_ANYTIME);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_START_ANYTIME);
+
+ // Start day (day + month + year only, no hours, minutes)
+ setting = AddSpinner(group, SETTING_TMR_START_DAY, 19128, SettingLevel::Basic,
+ GetDateAsIndex(m_startLocalTime), DaysFiller);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_START_DAY);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_START_DAY);
+ AddStartAnytimeDependentVisibilityCondition(setting, SETTING_TMR_START_DAY);
+
+ // Start time (hours + minutes only, no day, month, year)
+ setting = AddButton(group, SETTING_TMR_BEGIN, 19126, SettingLevel::Basic);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_BEGIN);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_BEGIN);
+ AddStartAnytimeDependentVisibilityCondition(setting, SETTING_TMR_BEGIN);
+
+ // "End any time" (only for timer rules)
+ setting = AddToggle(group, SETTING_TMR_END_ANYTIME, 817, SettingLevel::Basic, m_bEndAnyTime);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END_ANYTIME);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_END_ANYTIME);
+
+ // End day (day + month + year only, no hours, minutes)
+ setting = AddSpinner(group, SETTING_TMR_END_DAY, 19129, SettingLevel::Basic,
+ GetDateAsIndex(m_endLocalTime), DaysFiller);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END_DAY);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_END_DAY);
+ AddEndAnytimeDependentVisibilityCondition(setting, SETTING_TMR_END_DAY);
+
+ // End time (hours + minutes only, no day, month, year)
+ setting = AddButton(group, SETTING_TMR_END, 19127, SettingLevel::Basic);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_END);
+ AddEndAnytimeDependentVisibilityCondition(setting, SETTING_TMR_END);
+
+ // First day (only for timer rules)
+ setting = AddSpinner(group, SETTING_TMR_FIRST_DAY, 19084, SettingLevel::Basic,
+ GetDateAsIndex(m_firstDayLocalTime), DaysFiller);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_FIRST_DAY);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_FIRST_DAY);
+
+ // "Prevent duplicate episodes" (only for timer rules)
+ setting = AddList(group, SETTING_TMR_NEW_EPISODES, 812, SettingLevel::Basic,
+ m_iPreventDupEpisodes, DupEpisodesFiller, 812);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_NEW_EPISODES);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_NEW_EPISODES);
+
+ // Pre and post record time
+ setting = AddList(group, SETTING_TMR_BEGIN_PRE, 813, SettingLevel::Basic, m_iMarginStart,
+ MarginTimeFiller, 813);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_BEGIN_PRE);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_BEGIN_PRE);
+
+ setting = AddList(group, SETTING_TMR_END_POST, 814, SettingLevel::Basic, m_iMarginEnd,
+ MarginTimeFiller, 814);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_END_POST);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_END_POST);
+
+ // Priority
+ setting = AddList(group, SETTING_TMR_PRIORITY, 19082, SettingLevel::Basic, m_iPriority,
+ PrioritiesFiller, 19082);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_PRIORITY);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_PRIORITY);
+
+ // Lifetime
+ setting = AddList(group, SETTING_TMR_LIFETIME, 19083, SettingLevel::Basic, m_iLifetime,
+ LifetimesFiller, 19083);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_LIFETIME);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_LIFETIME);
+
+ // MaxRecordings
+ setting = AddList(group, SETTING_TMR_MAX_REC, 818, SettingLevel::Basic, m_iMaxRecordings,
+ MaxRecordingsFiller, 818);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_MAX_REC);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_MAX_REC);
+
+ // Recording folder
+ setting = AddEdit(group, SETTING_TMR_DIR, 19076, SettingLevel::Basic, m_strDirectory, true, false,
+ 19104);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_DIR);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_DIR);
+
+ // Recording group
+ setting = AddList(group, SETTING_TMR_REC_GROUP, 811, SettingLevel::Basic, m_iRecordingGroup,
+ RecordingGroupFiller, 811);
+ AddTypeDependentVisibilityCondition(setting, SETTING_TMR_REC_GROUP);
+ AddTypeDependentEnableCondition(setting, SETTING_TMR_REC_GROUP);
+}
+
+int CGUIDialogPVRTimerSettings::GetWeekdaysFromSetting(const SettingConstPtr& setting)
+{
+ std::shared_ptr<const CSettingList> settingList =
+ std::static_pointer_cast<const CSettingList>(setting);
+ if (settingList->GetElementType() != SettingType::Integer)
+ {
+ CLog::LogF(LOGERROR, "Wrong weekdays element type");
+ return 0;
+ }
+ int weekdays = 0;
+ std::vector<CVariant> list = CSettingUtils::GetList(settingList);
+ for (const auto& value : list)
+ {
+ if (!value.isInteger())
+ {
+ CLog::LogF(LOGERROR, "Wrong weekdays value type");
+ return 0;
+ }
+ weekdays += static_cast<int>(value.asInteger());
+ }
+
+ return weekdays;
+}
+
+void CGUIDialogPVRTimerSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ {
+ CLog::LogF(LOGERROR, "No setting");
+ return;
+ }
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string& settingId = setting->GetId();
+
+ if (settingId == SETTING_TMR_TYPE)
+ {
+ int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ const auto it = m_typeEntries.find(idx);
+ if (it != m_typeEntries.end())
+ {
+ m_timerType = it->second;
+
+ // reset certain settings to the defaults of the new timer type
+
+ if (m_timerType->SupportsPriority())
+ m_iPriority = m_timerType->GetPriorityDefault();
+
+ if (m_timerType->SupportsLifetime())
+ m_iLifetime = m_timerType->GetLifetimeDefault();
+
+ if (m_timerType->SupportsMaxRecordings())
+ m_iMaxRecordings = m_timerType->GetMaxRecordingsDefault();
+
+ if (m_timerType->SupportsRecordingGroup())
+ m_iRecordingGroup = m_timerType->GetRecordingGroupDefault();
+
+ if (m_timerType->SupportsRecordOnlyNewEpisodes())
+ m_iPreventDupEpisodes = m_timerType->GetPreventDuplicateEpisodesDefault();
+
+ if (m_timerType->IsTimerRule() && (m_iWeekdays == PVR_WEEKDAY_ALLDAYS))
+ SetButtonLabels(); // update "Any day" vs. "Every day"
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to get 'type' value");
+ }
+ }
+ else if (settingId == SETTING_TMR_ACTIVE)
+ {
+ m_bTimerActive = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_NAME)
+ {
+ m_strTitle = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_EPGSEARCH)
+ {
+ m_strEpgSearchString = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_FULLTEXT)
+ {
+ m_bFullTextEpgSearch = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_CHANNEL)
+ {
+ int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ const auto it = m_channelEntries.find(idx);
+ if (it != m_channelEntries.end())
+ {
+ m_channel = it->second;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to get 'type' value");
+ }
+ }
+ else if (settingId == SETTING_TMR_WEEKDAYS)
+ {
+ m_iWeekdays = GetWeekdaysFromSetting(setting);
+ }
+ else if (settingId == SETTING_TMR_START_ANYTIME)
+ {
+ m_bStartAnyTime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_END_ANYTIME)
+ {
+ m_bEndAnyTime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_START_DAY)
+ {
+ SetDateFromIndex(m_startLocalTime,
+ std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ }
+ else if (settingId == SETTING_TMR_END_DAY)
+ {
+ SetDateFromIndex(m_endLocalTime,
+ std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ }
+ else if (settingId == SETTING_TMR_FIRST_DAY)
+ {
+ SetDateFromIndex(m_firstDayLocalTime,
+ std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ }
+ else if (settingId == SETTING_TMR_NEW_EPISODES)
+ {
+ m_iPreventDupEpisodes = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_BEGIN_PRE)
+ {
+ m_iMarginStart = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_END_POST)
+ {
+ m_iMarginEnd = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_PRIORITY)
+ {
+ m_iPriority = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_LIFETIME)
+ {
+ m_iLifetime = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_MAX_REC)
+ {
+ m_iMaxRecordings = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_DIR)
+ {
+ m_strDirectory = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ }
+ else if (settingId == SETTING_TMR_REC_GROUP)
+ {
+ m_iRecordingGroup = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ }
+}
+
+void CGUIDialogPVRTimerSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ {
+ CLog::LogF(LOGERROR, "No setting");
+ return;
+ }
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == SETTING_TMR_BEGIN)
+ {
+ KODI::TIME::SystemTime timerStartTime;
+ m_startLocalTime.GetAsSystemTime(timerStartTime);
+ if (CGUIDialogNumeric::ShowAndGetTime(timerStartTime, g_localizeStrings.Get(14066)))
+ {
+ SetTimeFromSystemTime(m_startLocalTime, timerStartTime);
+ m_timerStartTimeStr = m_startLocalTime.GetAsLocalizedTime("", false);
+ SetButtonLabels();
+ }
+ }
+ else if (settingId == SETTING_TMR_END)
+ {
+ KODI::TIME::SystemTime timerEndTime;
+ m_endLocalTime.GetAsSystemTime(timerEndTime);
+ if (CGUIDialogNumeric::ShowAndGetTime(timerEndTime, g_localizeStrings.Get(14066)))
+ {
+ SetTimeFromSystemTime(m_endLocalTime, timerEndTime);
+ m_timerEndTimeStr = m_endLocalTime.GetAsLocalizedTime("", false);
+ SetButtonLabels();
+ }
+ }
+}
+
+bool CGUIDialogPVRTimerSettings::Validate()
+{
+ // @todo: Timer rules may have no date (time-only), so we can't check those for now.
+ // We need to extend the api with additional attributes to properly fix this
+ if (m_timerType->IsTimerRule())
+ return true;
+
+ bool bStartAnyTime = m_bStartAnyTime;
+ bool bEndAnyTime = m_bEndAnyTime;
+
+ if (!m_timerType->SupportsStartAnyTime() ||
+ !m_timerType->IsEpgBased()) // Start anytime toggle is not displayed
+ bStartAnyTime = false; // Assume start time change needs checking for
+
+ if (!m_timerType->SupportsEndAnyTime() ||
+ !m_timerType->IsEpgBased()) // End anytime toggle is not displayed
+ bEndAnyTime = false; // Assume end time change needs checking for
+
+ // Begin and end time
+ if (!bStartAnyTime && !bEndAnyTime)
+ {
+ if (m_timerType->SupportsStartTime() && m_timerType->SupportsEndTime() &&
+ m_endLocalTime < m_startLocalTime)
+ {
+ HELPERS::ShowOKDialogText(CVariant{19065}, // "Timer settings"
+ CVariant{19072}); // In order to add/update a timer
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool CGUIDialogPVRTimerSettings::Save()
+{
+ if (!Validate())
+ return false;
+
+ // Timer type
+ m_timerInfoTag->SetTimerType(m_timerType);
+
+ // Timer active/inactive
+ m_timerInfoTag->m_state = m_bTimerActive ? PVR_TIMER_STATE_SCHEDULED : PVR_TIMER_STATE_DISABLED;
+
+ // Name
+ m_timerInfoTag->m_strTitle = m_strTitle;
+
+ // epg search string (only for epg-based timer rules)
+ m_timerInfoTag->m_strEpgSearchString = m_strEpgSearchString;
+
+ // epg fulltext search, instead of just title match. (only for epg-based timer rules)
+ m_timerInfoTag->m_bFullTextEpgSearch = m_bFullTextEpgSearch;
+
+ // Channel
+ m_timerInfoTag->m_iClientChannelUid = m_channel.channelUid;
+ m_timerInfoTag->m_iClientId = m_channel.clientId;
+ m_timerInfoTag->m_bIsRadio = m_bIsRadio;
+ m_timerInfoTag->UpdateChannel();
+
+ if (!m_timerType->SupportsStartAnyTime() ||
+ !m_timerType->IsEpgBased()) // Start anytime toggle is not displayed
+ m_bStartAnyTime = false; // Assume start time change needs checking for
+ m_timerInfoTag->m_bStartAnyTime = m_bStartAnyTime;
+
+ if (!m_timerType->SupportsEndAnyTime() ||
+ !m_timerType->IsEpgBased()) // End anytime toggle is not displayed
+ m_bEndAnyTime = false; // Assume end time change needs checking for
+ m_timerInfoTag->m_bEndAnyTime = m_bEndAnyTime;
+
+ // Begin and end time
+ if (!m_bStartAnyTime && !m_bEndAnyTime)
+ {
+ if (m_timerType->SupportsStartTime() && // has start clock entry
+ m_timerType->SupportsEndTime() && // and end clock entry
+ m_timerType->IsTimerRule()) // but no associated start/end day spinners
+ {
+ if (m_endLocalTime < m_startLocalTime) // And the end clock is earlier than the start clock
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "End before start, adding a day.");
+ m_endLocalTime += CDateTimeSpan(1, 0, 0, 0);
+ if (m_endLocalTime < m_startLocalTime)
+ {
+ CLog::Log(LOGWARNING,
+ "Timer settings dialog: End before start. Setting end time to start time.");
+ m_endLocalTime = m_startLocalTime;
+ }
+ }
+ else if (m_endLocalTime >
+ (m_startLocalTime + CDateTimeSpan(1, 0, 0, 0))) // Or the duration is more than a day
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "End > 1 day after start, removing a day.");
+ m_endLocalTime -= CDateTimeSpan(1, 0, 0, 0);
+ if (m_endLocalTime > (m_startLocalTime + CDateTimeSpan(1, 0, 0, 0)))
+ {
+ CLog::Log(
+ LOGWARNING,
+ "Timer settings dialog: End > 1 day after start. Setting end time to start time.");
+ m_endLocalTime = m_startLocalTime;
+ }
+ }
+ }
+ else if (m_endLocalTime < m_startLocalTime)
+ {
+ // this case will fail validation so this can't be reached.
+ }
+ m_timerInfoTag->SetStartFromLocalTime(m_startLocalTime);
+ m_timerInfoTag->SetEndFromLocalTime(m_endLocalTime);
+ }
+ else if (!m_bStartAnyTime)
+ m_timerInfoTag->SetStartFromLocalTime(m_startLocalTime);
+ else if (!m_bEndAnyTime)
+ m_timerInfoTag->SetEndFromLocalTime(m_endLocalTime);
+
+ // Days of week (only for timer rules)
+ if (m_timerType->IsTimerRule())
+ m_timerInfoTag->m_iWeekdays = m_iWeekdays;
+ else
+ m_timerInfoTag->m_iWeekdays = PVR_WEEKDAY_NONE;
+
+ // First day (only for timer rules)
+ m_timerInfoTag->SetFirstDayFromLocalTime(m_firstDayLocalTime);
+
+ // "New episodes only" (only for timer rules)
+ m_timerInfoTag->m_iPreventDupEpisodes = m_iPreventDupEpisodes;
+
+ // Pre and post record time
+ m_timerInfoTag->m_iMarginStart = m_iMarginStart;
+ m_timerInfoTag->m_iMarginEnd = m_iMarginEnd;
+
+ // Priority
+ m_timerInfoTag->m_iPriority = m_iPriority;
+
+ // Lifetime
+ m_timerInfoTag->m_iLifetime = m_iLifetime;
+
+ // MaxRecordings
+ m_timerInfoTag->m_iMaxRecordings = m_iMaxRecordings;
+
+ // Recording folder
+ m_timerInfoTag->m_strDirectory = m_strDirectory;
+
+ // Recording group
+ m_timerInfoTag->m_iRecordingGroup = m_iRecordingGroup;
+
+ // Set the timer's title to the channel name if it's empty or 'New Timer'
+ if (m_strTitle.empty() || m_strTitle == g_localizeStrings.Get(19056))
+ {
+ const std::string channelName = m_timerInfoTag->ChannelName();
+ if (!channelName.empty())
+ m_timerInfoTag->m_strTitle = channelName;
+ }
+
+ // Update summary
+ m_timerInfoTag->UpdateSummary();
+
+ return true;
+}
+
+void CGUIDialogPVRTimerSettings::SetButtonLabels()
+{
+ // timer start time
+ BaseSettingControlPtr settingControl = GetSettingControl(SETTING_TMR_BEGIN);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ {
+ SET_CONTROL_LABEL2(settingControl->GetID(), m_timerStartTimeStr);
+ }
+
+ // timer end time
+ settingControl = GetSettingControl(SETTING_TMR_END);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ {
+ SET_CONTROL_LABEL2(settingControl->GetID(), m_timerEndTimeStr);
+ }
+}
+
+void CGUIDialogPVRTimerSettings::AddCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier,
+ SettingConditionCheck condition,
+ SettingDependencyType depType,
+ const std::string& settingId)
+{
+ GetSettingsManager()->AddDynamicCondition(identifier, condition, this);
+ CSettingDependency dep(depType, GetSettingsManager());
+ dep.And()->Add(CSettingDependencyConditionPtr(
+ new CSettingDependencyCondition(identifier, "true", settingId, false, GetSettingsManager())));
+ SettingDependencies deps(setting->GetDependencies());
+ deps.push_back(dep);
+ setting->SetDependencies(deps);
+}
+
+int CGUIDialogPVRTimerSettings::GetDateAsIndex(const CDateTime& datetime)
+{
+ const CDateTime date(datetime.GetYear(), datetime.GetMonth(), datetime.GetDay(), 0, 0, 0);
+ time_t t(0);
+ date.GetAsTime(t);
+ return static_cast<int>(t);
+}
+
+void CGUIDialogPVRTimerSettings::SetDateFromIndex(CDateTime& datetime, int date)
+{
+ const CDateTime newDate(static_cast<time_t>(date));
+ datetime.SetDateTime(newDate.GetYear(), newDate.GetMonth(), newDate.GetDay(), datetime.GetHour(),
+ datetime.GetMinute(), datetime.GetSecond());
+}
+
+void CGUIDialogPVRTimerSettings::SetTimeFromSystemTime(CDateTime& datetime,
+ const KODI::TIME::SystemTime& time)
+{
+ const CDateTime newTime(time);
+ datetime.SetDateTime(datetime.GetYear(), datetime.GetMonth(), datetime.GetDay(),
+ newTime.GetHour(), newTime.GetMinute(), newTime.GetSecond());
+}
+
+void CGUIDialogPVRTimerSettings::InitializeTypesList()
+{
+ m_typeEntries.clear();
+
+ // If timer is read-only or was created by a timer rule, only add current type, for information. Type can't be changed.
+ if (m_timerType->IsReadOnly() || m_timerInfoTag->HasParent())
+ {
+ m_typeEntries.insert(std::make_pair(0, m_timerType));
+ return;
+ }
+
+ bool bFoundThisType(false);
+ int idx(0);
+ const std::vector<std::shared_ptr<CPVRTimerType>> types(CPVRTimerType::GetAllTypes());
+ for (const auto& type : types)
+ {
+ // Type definition prohibits created of new instances.
+ // But the dialog can act as a viewer for these types.
+ if (type->ForbidsNewInstances())
+ continue;
+
+ // Read-only timers cannot be created using this dialog.
+ // But the dialog can act as a viewer for read-only types.
+ if (type->IsReadOnly())
+ continue;
+
+ // Drop TimerTypes that require EPGInfo, if none is populated
+ if (type->RequiresEpgTagOnCreate() && !m_timerInfoTag->GetEpgInfoTag())
+ continue;
+
+ // Drop TimerTypes without 'Series' EPG attributes if none are set
+ if (type->RequiresEpgSeriesOnCreate())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(m_timerInfoTag->GetEpgInfoTag());
+ if (epgTag && !epgTag->IsSeries())
+ continue;
+ }
+
+ // Drop TimerTypes which need series link if none is set
+ if (type->RequiresEpgSeriesLinkOnCreate())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(m_timerInfoTag->GetEpgInfoTag());
+ if (!epgTag || epgTag->SeriesLink().empty())
+ continue;
+ }
+
+ // Drop TimerTypes that forbid EPGInfo, if it is populated
+ if (type->ForbidsEpgTagOnCreate() && m_timerInfoTag->GetEpgInfoTag())
+ continue;
+
+ // Drop TimerTypes that aren't rules and cannot be recorded
+ if (!type->IsTimerRule())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(m_timerInfoTag->GetEpgInfoTag());
+ bool bCanRecord = epgTag ? epgTag->IsRecordable()
+ : m_timerInfoTag->EndAsLocalTime() > CDateTime::GetCurrentDateTime();
+ if (!bCanRecord)
+ continue;
+ }
+
+ if (!bFoundThisType && *type == *m_timerType)
+ bFoundThisType = true;
+
+ m_typeEntries.insert(std::make_pair(idx++, type));
+ }
+
+ if (!bFoundThisType)
+ m_typeEntries.insert(std::make_pair(idx++, m_timerType));
+}
+
+void CGUIDialogPVRTimerSettings::InitializeChannelsList()
+{
+ m_channelEntries.clear();
+
+ int index = 0;
+
+ // Add special "any channel" entries - one for every client (used for epg-based timer rules).
+ const CPVRClientMap clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients();
+ for (const auto& client : clients)
+ {
+ m_channelEntries.insert(
+ {index, ChannelDescriptor(PVR_CHANNEL_INVALID_UID, client.second->GetID(),
+ g_localizeStrings.Get(809))}); // "Any channel"
+ ++index;
+ }
+
+ // Add regular channels
+ const std::shared_ptr<CPVRChannelGroup> allGroup =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(m_bIsRadio);
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ allGroup->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+ for (const auto& groupMember : groupMembers)
+ {
+ const std::shared_ptr<CPVRChannel> channel = groupMember->Channel();
+ const std::string channelDescription = StringUtils::Format(
+ "{} {}", groupMember->ChannelNumber().FormattedChannelNumber(), channel->ChannelName());
+ m_channelEntries.insert(
+ {index, ChannelDescriptor(channel->UniqueID(), channel->ClientID(), channelDescription)});
+ ++index;
+ }
+}
+
+void CGUIDialogPVRTimerSettings::TypesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+ current = 0;
+
+ static const std::vector<std::pair<std::string, CVariant>> reminderTimerProps{
+ std::make_pair("PVR.IsRemindingTimer", CVariant{true})};
+ static const std::vector<std::pair<std::string, CVariant>> recordingTimerProps{
+ std::make_pair("PVR.IsRecordingTimer", CVariant{true})};
+
+ const auto clients = CServiceBroker::GetPVRManager().Clients();
+
+ bool foundCurrent(false);
+ for (const auto& typeEntry : pThis->m_typeEntries)
+ {
+ std::string clientName;
+
+ const auto client = clients->GetCreatedClient(typeEntry.second->GetClientId());
+ if (client)
+ clientName = client->GetFriendlyName();
+
+ list.emplace_back(typeEntry.second->GetDescription(), clientName, typeEntry.first,
+ typeEntry.second->IsReminder() ? reminderTimerProps : recordingTimerProps);
+
+ if (!foundCurrent && (*(pThis->m_timerType) == *(typeEntry.second)))
+ {
+ current = typeEntry.first;
+ foundCurrent = true;
+ }
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::ChannelsFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+ current = 0;
+
+ bool foundCurrent(false);
+ for (const auto& channelEntry : pThis->m_channelEntries)
+ {
+ // Only include channels for the currently selected timer type or all channels if type is client-independent.
+ if (pThis->m_timerType->GetClientId() == -1 || // client-independent
+ pThis->m_timerType->GetClientId() == channelEntry.second.clientId)
+ {
+ // Do not add "any channel" entry if not supported by selected timer type.
+ if (channelEntry.second.channelUid == PVR_CHANNEL_INVALID_UID &&
+ !pThis->m_timerType->SupportsAnyChannel())
+ continue;
+
+ list.emplace_back(
+ IntegerSettingOption(channelEntry.second.description, channelEntry.first));
+ }
+
+ if (!foundCurrent && (pThis->m_channel == channelEntry.second))
+ {
+ current = channelEntry.first;
+ foundCurrent = true;
+ }
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::DaysFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+ current = 0;
+
+ // Data range: "today" until "yesterday next year"
+ const CDateTime now(CDateTime::GetCurrentDateTime());
+ CDateTime time(now.GetYear(), now.GetMonth(), now.GetDay(), 0, 0, 0);
+ const CDateTime yesterdayPlusOneYear(CDateTime(time.GetYear() + 1, time.GetMonth(),
+ time.GetDay(), time.GetHour(), time.GetMinute(),
+ time.GetSecond()) -
+ CDateTimeSpan(1, 0, 0, 0));
+
+ CDateTime oldCDateTime;
+ if (setting->GetId() == SETTING_TMR_FIRST_DAY)
+ oldCDateTime = pThis->m_timerInfoTag->FirstDayAsLocalTime();
+ else if (setting->GetId() == SETTING_TMR_START_DAY)
+ oldCDateTime = pThis->m_timerInfoTag->StartAsLocalTime();
+ else
+ oldCDateTime = pThis->m_timerInfoTag->EndAsLocalTime();
+ const CDateTime oldCDate(oldCDateTime.GetYear(), oldCDateTime.GetMonth(), oldCDateTime.GetDay(),
+ 0, 0, 0);
+
+ if ((oldCDate < time) || (oldCDate > yesterdayPlusOneYear))
+ list.emplace_back(oldCDate.GetAsLocalizedDate(true /*long date*/), GetDateAsIndex(oldCDate));
+
+ while (time <= yesterdayPlusOneYear)
+ {
+ list.emplace_back(time.GetAsLocalizedDate(true /*long date*/), GetDateAsIndex(time));
+ time += CDateTimeSpan(1, 0, 0, 0);
+ }
+
+ if (setting->GetId() == SETTING_TMR_FIRST_DAY)
+ current = GetDateAsIndex(pThis->m_firstDayLocalTime);
+ else if (setting->GetId() == SETTING_TMR_START_DAY)
+ current = GetDateAsIndex(pThis->m_startLocalTime);
+ else
+ current = GetDateAsIndex(pThis->m_endLocalTime);
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::DupEpisodesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ std::vector<std::pair<std::string, int>> values;
+ pThis->m_timerType->GetPreventDuplicateEpisodesValues(values);
+ std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) {
+ return IntegerSettingOption(value.first, value.second);
+ });
+
+ current = pThis->m_iPreventDupEpisodes;
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::WeekdaysFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+ list.emplace_back(g_localizeStrings.Get(831), PVR_WEEKDAY_MONDAY); // "Mondays"
+ list.emplace_back(g_localizeStrings.Get(832), PVR_WEEKDAY_TUESDAY); // "Tuesdays"
+ list.emplace_back(g_localizeStrings.Get(833), PVR_WEEKDAY_WEDNESDAY); // "Wednesdays"
+ list.emplace_back(g_localizeStrings.Get(834), PVR_WEEKDAY_THURSDAY); // "Thursdays"
+ list.emplace_back(g_localizeStrings.Get(835), PVR_WEEKDAY_FRIDAY); // "Fridays"
+ list.emplace_back(g_localizeStrings.Get(836), PVR_WEEKDAY_SATURDAY); // "Saturdays"
+ list.emplace_back(g_localizeStrings.Get(837), PVR_WEEKDAY_SUNDAY); // "Sundays"
+
+ current = pThis->m_iWeekdays;
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::PrioritiesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ std::vector<std::pair<std::string, int>> values;
+ pThis->m_timerType->GetPriorityValues(values);
+ std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) {
+ return IntegerSettingOption(value.first, value.second);
+ });
+
+ current = pThis->m_iPriority;
+
+ auto it = list.begin();
+ while (it != list.end())
+ {
+ if (it->value == current)
+ break; // value already in list
+
+ ++it;
+ }
+
+ if (it == list.end())
+ {
+ // PVR backend supplied value is not in the list of predefined values. Insert it.
+ list.insert(it, IntegerSettingOption(std::to_string(current), current));
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::LifetimesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ std::vector<std::pair<std::string, int>> values;
+ pThis->m_timerType->GetLifetimeValues(values);
+ std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) {
+ return IntegerSettingOption(value.first, value.second);
+ });
+
+ current = pThis->m_iLifetime;
+
+ auto it = list.begin();
+ while (it != list.end())
+ {
+ if (it->value == current)
+ break; // value already in list
+
+ ++it;
+ }
+
+ if (it == list.end())
+ {
+ // PVR backend supplied value is not in the list of predefined values. Insert it.
+ list.insert(it, IntegerSettingOption(
+ StringUtils::Format(g_localizeStrings.Get(17999), current) /* {} days */,
+ current));
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::MaxRecordingsFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ std::vector<std::pair<std::string, int>> values;
+ pThis->m_timerType->GetMaxRecordingsValues(values);
+ std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) {
+ return IntegerSettingOption(value.first, value.second);
+ });
+
+ current = pThis->m_iMaxRecordings;
+
+ auto it = list.begin();
+ while (it != list.end())
+ {
+ if (it->value == current)
+ break; // value already in list
+
+ ++it;
+ }
+
+ if (it == list.end())
+ {
+ // PVR backend supplied value is not in the list of predefined values. Insert it.
+ list.insert(it, IntegerSettingOption(std::to_string(current), current));
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::RecordingGroupFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ std::vector<std::pair<std::string, int>> values;
+ pThis->m_timerType->GetRecordingGroupValues(values);
+ std::transform(values.cbegin(), values.cend(), std::back_inserter(list), [](const auto& value) {
+ return IntegerSettingOption(value.first, value.second);
+ });
+
+ current = pThis->m_iRecordingGroup;
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+void CGUIDialogPVRTimerSettings::MarginTimeFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis)
+ {
+ list.clear();
+
+ // Get global settings values
+ CPVRSettings::MarginTimeFiller(setting, list, current, data);
+
+ if (setting->GetId() == SETTING_TMR_BEGIN_PRE)
+ current = pThis->m_iMarginStart;
+ else
+ current = pThis->m_iMarginEnd;
+
+ bool bInsertValue = true;
+ auto it = list.begin();
+ while (it != list.end())
+ {
+ if (it->value == current)
+ {
+ bInsertValue = false;
+ break; // value already in list
+ }
+
+ if (it->value > current)
+ break;
+
+ ++it;
+ }
+
+ if (bInsertValue)
+ {
+ // PVR backend supplied value is not in the list of predefined values. Insert it.
+ list.insert(it, IntegerSettingOption(
+ StringUtils::Format(g_localizeStrings.Get(14044), current) /* {} min */,
+ current));
+ }
+ }
+ else
+ CLog::LogF(LOGERROR, "No dialog");
+}
+
+std::string CGUIDialogPVRTimerSettings::WeekdaysValueFormatter(const SettingConstPtr& setting)
+{
+ return CPVRTimerInfoTag::GetWeekdaysString(GetWeekdaysFromSetting(setting), true, true);
+}
+
+void CGUIDialogPVRTimerSettings::AddTypeDependentEnableCondition(
+ const std::shared_ptr<CSetting>& setting, const std::string& identifier)
+{
+ // Enable setting depending on read-only attribute of the selected timer type
+ std::string id(identifier);
+ id.append(TYPE_DEP_ENABLE_COND_ID_POSTFIX);
+ AddCondition(setting, id, TypeReadOnlyCondition, SettingDependencyType::Enable, SETTING_TMR_TYPE);
+}
+
+bool CGUIDialogPVRTimerSettings::TypeReadOnlyCondition(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis == NULL)
+ {
+ CLog::LogF(LOGERROR, "No dialog");
+ return false;
+ }
+
+ if (!StringUtils::EqualsNoCase(value, "true"))
+ return false;
+
+ std::string cond(condition);
+ cond.erase(cond.find(TYPE_DEP_ENABLE_COND_ID_POSTFIX));
+
+ // If only one type is available, disable type selector.
+ if (pThis->m_typeEntries.size() == 1)
+ {
+ if (cond == SETTING_TMR_TYPE)
+ return false;
+ }
+
+ // For existing one time epg-based timers, disable editing of epg-filled data.
+ if (!pThis->m_bIsNewTimer && pThis->m_timerType->IsEpgBasedOnetime())
+ {
+ if ((cond == SETTING_TMR_NAME) || (cond == SETTING_TMR_CHANNEL) ||
+ (cond == SETTING_TMR_START_DAY) || (cond == SETTING_TMR_END_DAY) ||
+ (cond == SETTING_TMR_BEGIN) || (cond == SETTING_TMR_END))
+ return false;
+ }
+
+ /* Always enable enable/disable, if supported by the timer type. */
+ if (pThis->m_timerType->SupportsEnableDisable() && !pThis->m_timerInfoTag->IsBroken())
+ {
+ if (cond == SETTING_TMR_ACTIVE)
+ return true;
+ }
+
+ // Let the PVR client decide...
+ int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ const auto entry = pThis->m_typeEntries.find(idx);
+ if (entry != pThis->m_typeEntries.end())
+ return !entry->second->IsReadOnly();
+ else
+ CLog::LogF(LOGERROR, "No type entry");
+
+ return false;
+}
+
+void CGUIDialogPVRTimerSettings::AddTypeDependentVisibilityCondition(
+ const std::shared_ptr<CSetting>& setting, const std::string& identifier)
+{
+ // Show or hide setting depending on attributes of the selected timer type
+ std::string id(identifier);
+ id.append(TYPE_DEP_VISIBI_COND_ID_POSTFIX);
+ AddCondition(setting, id, TypeSupportsCondition, SettingDependencyType::Visible,
+ SETTING_TMR_TYPE);
+}
+
+bool CGUIDialogPVRTimerSettings::TypeSupportsCondition(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis == NULL)
+ {
+ CLog::LogF(LOGERROR, "No dialog");
+ return false;
+ }
+
+ if (!StringUtils::EqualsNoCase(value, "true"))
+ return false;
+
+ int idx = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ const auto entry = pThis->m_typeEntries.find(idx);
+ if (entry != pThis->m_typeEntries.end())
+ {
+ std::string cond(condition);
+ cond.erase(cond.find(TYPE_DEP_VISIBI_COND_ID_POSTFIX));
+
+ if (cond == SETTING_TMR_EPGSEARCH)
+ return entry->second->SupportsEpgTitleMatch() || entry->second->SupportsEpgFulltextMatch();
+ else if (cond == SETTING_TMR_FULLTEXT)
+ return entry->second->SupportsEpgFulltextMatch();
+ else if (cond == SETTING_TMR_ACTIVE)
+ return entry->second->SupportsEnableDisable();
+ else if (cond == SETTING_TMR_CHANNEL)
+ return entry->second->SupportsChannels();
+ else if (cond == SETTING_TMR_START_ANYTIME)
+ return entry->second->SupportsStartAnyTime() && entry->second->IsEpgBased();
+ else if (cond == SETTING_TMR_END_ANYTIME)
+ return entry->second->SupportsEndAnyTime() && entry->second->IsEpgBased();
+ else if (cond == SETTING_TMR_START_DAY)
+ return entry->second->SupportsStartTime() && entry->second->IsOnetime();
+ else if (cond == SETTING_TMR_END_DAY)
+ return entry->second->SupportsEndTime() && entry->second->IsOnetime();
+ else if (cond == SETTING_TMR_BEGIN)
+ return entry->second->SupportsStartTime();
+ else if (cond == SETTING_TMR_END)
+ return entry->second->SupportsEndTime();
+ else if (cond == SETTING_TMR_WEEKDAYS)
+ return entry->second->SupportsWeekdays();
+ else if (cond == SETTING_TMR_FIRST_DAY)
+ return entry->second->SupportsFirstDay();
+ else if (cond == SETTING_TMR_NEW_EPISODES)
+ return entry->second->SupportsRecordOnlyNewEpisodes();
+ else if (cond == SETTING_TMR_BEGIN_PRE)
+ return entry->second->SupportsStartMargin();
+ else if (cond == SETTING_TMR_END_POST)
+ return entry->second->SupportsEndMargin();
+ else if (cond == SETTING_TMR_PRIORITY)
+ return entry->second->SupportsPriority();
+ else if (cond == SETTING_TMR_LIFETIME)
+ return entry->second->SupportsLifetime();
+ else if (cond == SETTING_TMR_MAX_REC)
+ return entry->second->SupportsMaxRecordings();
+ else if (cond == SETTING_TMR_DIR)
+ return entry->second->SupportsRecordingFolders();
+ else if (cond == SETTING_TMR_REC_GROUP)
+ return entry->second->SupportsRecordingGroup();
+ else
+ CLog::LogF(LOGERROR, "Unknown condition");
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "No type entry");
+ }
+ return false;
+}
+
+void CGUIDialogPVRTimerSettings::AddStartAnytimeDependentVisibilityCondition(
+ const std::shared_ptr<CSetting>& setting, const std::string& identifier)
+{
+ // Show or hide setting depending on value of setting "any time"
+ std::string id(identifier);
+ id.append(START_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX);
+ AddCondition(setting, id, StartAnytimeSetCondition, SettingDependencyType::Visible,
+ SETTING_TMR_START_ANYTIME);
+}
+
+bool CGUIDialogPVRTimerSettings::StartAnytimeSetCondition(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis == NULL)
+ {
+ CLog::LogF(LOGERROR, "No dialog");
+ return false;
+ }
+
+ if (!StringUtils::EqualsNoCase(value, "true"))
+ return false;
+
+ // "any time" setting is only relevant for epg-based timers.
+ if (!pThis->m_timerType->IsEpgBased())
+ return true;
+
+ // If 'Start anytime' option isn't supported, don't hide start time
+ if (!pThis->m_timerType->SupportsStartAnyTime())
+ return true;
+
+ std::string cond(condition);
+ cond.erase(cond.find(START_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX));
+
+ if ((cond == SETTING_TMR_START_DAY) || (cond == SETTING_TMR_BEGIN))
+ {
+ bool bAnytime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ return !bAnytime;
+ }
+ return false;
+}
+
+void CGUIDialogPVRTimerSettings::AddEndAnytimeDependentVisibilityCondition(
+ const std::shared_ptr<CSetting>& setting, const std::string& identifier)
+{
+ // Show or hide setting depending on value of setting "any time"
+ std::string id(identifier);
+ id.append(END_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX);
+ AddCondition(setting, id, EndAnytimeSetCondition, SettingDependencyType::Visible,
+ SETTING_TMR_END_ANYTIME);
+}
+
+bool CGUIDialogPVRTimerSettings::EndAnytimeSetCondition(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ CGUIDialogPVRTimerSettings* pThis = static_cast<CGUIDialogPVRTimerSettings*>(data);
+ if (pThis == NULL)
+ {
+ CLog::LogF(LOGERROR, "No dialog");
+ return false;
+ }
+
+ if (!StringUtils::EqualsNoCase(value, "true"))
+ return false;
+
+ // "any time" setting is only relevant for epg-based timers.
+ if (!pThis->m_timerType->IsEpgBased())
+ return true;
+
+ // If 'End anytime' option isn't supported, don't hide end time
+ if (!pThis->m_timerType->SupportsEndAnyTime())
+ return true;
+
+ std::string cond(condition);
+ cond.erase(cond.find(END_ANYTIME_DEP_VISIBI_COND_ID_POSTFIX));
+
+ if ((cond == SETTING_TMR_END_DAY) || (cond == SETTING_TMR_END))
+ {
+ bool bAnytime = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ return !bAnytime;
+ }
+ return false;
+}
diff --git a/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.h b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.h
new file mode 100644
index 0000000..d3e9e3e
--- /dev/null
+++ b/xbmc/pvr/dialogs/GUIDialogPVRTimerSettings.h
@@ -0,0 +1,196 @@
+/*
+ * 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 "XBDateTime.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" // PVR_CHANNEL_INVALID_UID
+#include "settings/SettingConditions.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+#include "settings/lib/SettingDependency.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CSetting;
+
+struct IntegerSettingOption;
+
+namespace PVR
+{
+class CPVRTimerInfoTag;
+class CPVRTimerType;
+
+class CGUIDialogPVRTimerSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogPVRTimerSettings();
+ ~CGUIDialogPVRTimerSettings() override;
+
+ bool CanBeActivated() const override;
+
+ void SetTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer);
+
+protected:
+ // implementation of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+private:
+ bool Validate();
+ void InitializeTypesList();
+ void InitializeChannelsList();
+ void SetButtonLabels();
+
+ static int GetDateAsIndex(const CDateTime& datetime);
+ static void SetDateFromIndex(CDateTime& datetime, int date);
+ static void SetTimeFromSystemTime(CDateTime& datetime, const KODI::TIME::SystemTime& time);
+
+ static int GetWeekdaysFromSetting(const std::shared_ptr<const CSetting>& setting);
+
+ static void TypesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void ChannelsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void DaysFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void DupEpisodesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void WeekdaysFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void PrioritiesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void LifetimesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void MaxRecordingsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void RecordingGroupFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void MarginTimeFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ static std::string WeekdaysValueFormatter(const std::shared_ptr<const CSetting>& setting);
+
+ void AddCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier,
+ SettingConditionCheck condition,
+ SettingDependencyType depType,
+ const std::string& settingId);
+
+ void AddTypeDependentEnableCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier);
+ static bool TypeReadOnlyCondition(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+ void AddTypeDependentVisibilityCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier);
+ static bool TypeSupportsCondition(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+ void AddStartAnytimeDependentVisibilityCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier);
+ static bool StartAnytimeSetCondition(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+ void AddEndAnytimeDependentVisibilityCondition(const std::shared_ptr<CSetting>& setting,
+ const std::string& identifier);
+ static bool EndAnytimeSetCondition(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+ typedef std::map<int, std::shared_ptr<CPVRTimerType>> TypeEntriesMap;
+
+ typedef struct ChannelDescriptor
+ {
+ int channelUid;
+ int clientId;
+ std::string description;
+
+ ChannelDescriptor(int _channelUid = PVR_CHANNEL_INVALID_UID,
+ int _clientId = -1,
+ const std::string& _description = "")
+ : channelUid(_channelUid), clientId(_clientId), description(_description)
+ {
+ }
+
+ inline bool operator==(const ChannelDescriptor& right) const
+ {
+ return (channelUid == right.channelUid && clientId == right.clientId &&
+ description == right.description);
+ }
+
+ } ChannelDescriptor;
+
+ typedef std::map<int, ChannelDescriptor> ChannelEntriesMap;
+
+ std::shared_ptr<CPVRTimerInfoTag> m_timerInfoTag;
+ TypeEntriesMap m_typeEntries;
+ ChannelEntriesMap m_channelEntries;
+ std::string m_timerStartTimeStr;
+ std::string m_timerEndTimeStr;
+
+ std::shared_ptr<CPVRTimerType> m_timerType;
+ bool m_bIsRadio = false;
+ bool m_bIsNewTimer = true;
+ bool m_bTimerActive = false;
+ std::string m_strTitle;
+ std::string m_strEpgSearchString;
+ bool m_bFullTextEpgSearch = true;
+ ChannelDescriptor m_channel;
+ CDateTime m_startLocalTime;
+ CDateTime m_endLocalTime;
+ bool m_bStartAnyTime = false;
+ bool m_bEndAnyTime = false;
+ unsigned int m_iWeekdays;
+ CDateTime m_firstDayLocalTime;
+ unsigned int m_iPreventDupEpisodes = 0;
+ unsigned int m_iMarginStart = 0;
+ unsigned int m_iMarginEnd = 0;
+ int m_iPriority = 0;
+ int m_iLifetime = 0;
+ int m_iMaxRecordings = 0;
+ std::string m_strDirectory;
+ unsigned int m_iRecordingGroup = 0;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/epg/CMakeLists.txt b/xbmc/pvr/epg/CMakeLists.txt
new file mode 100644
index 0000000..0ea75b7
--- /dev/null
+++ b/xbmc/pvr/epg/CMakeLists.txt
@@ -0,0 +1,22 @@
+set(SOURCES EpgContainer.cpp
+ Epg.cpp
+ EpgDatabase.cpp
+ EpgInfoTag.cpp
+ EpgSearchFilter.cpp
+ EpgSearchPath.cpp
+ EpgChannelData.cpp
+ EpgTagsCache.cpp
+ EpgTagsContainer.cpp)
+
+set(HEADERS Epg.h
+ EpgContainer.h
+ EpgDatabase.h
+ EpgInfoTag.h
+ EpgSearchData.h
+ EpgSearchFilter.h
+ EpgSearchPath.h
+ EpgChannelData.h
+ EpgTagsCache.h
+ EpgTagsContainer.h)
+
+core_add_library(pvr_epg)
diff --git a/xbmc/pvr/epg/Epg.cpp b/xbmc/pvr/epg/Epg.cpp
new file mode 100644
index 0000000..1112b2f
--- /dev/null
+++ b/xbmc/pvr/epg/Epg.cpp
@@ -0,0 +1,554 @@
+/*
+ * 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 "Epg.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+CPVREpg::CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+ : m_iEpgID(iEpgID),
+ m_strName(strName),
+ m_strScraperName(strScraperName),
+ m_channelData(new CPVREpgChannelData),
+ m_tags(m_iEpgID, m_channelData, database)
+{
+}
+
+CPVREpg::CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+ : m_bChanged(true),
+ m_iEpgID(iEpgID),
+ m_strName(strName),
+ m_strScraperName(strScraperName),
+ m_channelData(channelData),
+ m_tags(m_iEpgID, m_channelData, database)
+{
+}
+
+CPVREpg::~CPVREpg()
+{
+ Clear();
+}
+
+void CPVREpg::ForceUpdate()
+{
+ m_bUpdatePending = true;
+ m_events.Publish(PVREvent::EpgUpdatePending);
+}
+
+void CPVREpg::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_tags.Clear();
+}
+
+void CPVREpg::Cleanup(int iPastDays)
+{
+ const CDateTime cleanupTime = CDateTime::GetUTCDateTime() - CDateTimeSpan(iPastDays, 0, 0, 0);
+ Cleanup(cleanupTime);
+}
+
+void CPVREpg::Cleanup(const CDateTime& time)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_tags.Cleanup(time);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagNow() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetActiveTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagNext() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetNextStartingTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagPrevious() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetLastEndedTag();
+}
+
+bool CPVREpg::CheckPlayingEvent()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_tags.UpdateActiveTag())
+ {
+ m_events.Publish(PVREvent::EpgActiveItem);
+ return true;
+ }
+ return false;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagByBroadcastId(unsigned int iUniqueBroadcastId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetTag(iUniqueBroadcastId);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagByDatabaseId(int iDatabaseId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetTagByDatabaseID(iDatabaseId);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpg::GetTagBetween(const CDateTime& beginTime, const CDateTime& endTime, bool bUpdateFromClient /* = false */)
+{
+ std::shared_ptr<CPVREpgInfoTag> tag;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ tag = m_tags.GetTagBetween(beginTime, endTime);
+
+ if (!tag && bUpdateFromClient)
+ {
+ // not found locally; try to fetch from client
+ time_t b;
+ beginTime.GetAsTime(b);
+ time_t e;
+ endTime.GetAsTime(e);
+
+ const std::shared_ptr<CPVREpg> tmpEpg = std::make_shared<CPVREpg>(
+ m_iEpgID, m_strName, m_strScraperName, m_channelData, std::shared_ptr<CPVREpgDatabase>());
+ if (tmpEpg->UpdateFromScraper(b, e, true))
+ tag = tmpEpg->GetTagBetween(beginTime, endTime, false);
+
+ if (tag)
+ m_tags.UpdateEntry(tag);
+ }
+
+ return tag;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpg::GetTimeline(
+ const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetTimeline(timelineStart, timelineEnd, minEventEnd, maxEventStart);
+}
+
+bool CPVREpg::UpdateEntries(const CPVREpg& epg)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* copy over tags */
+ m_tags.UpdateEntries(epg.m_tags);
+
+ /* update the last scan time of this table */
+ m_lastScanTime = CDateTime::GetUTCDateTime();
+ m_bUpdateLastScanTime = true;
+
+ m_events.Publish(PVREvent::Epg);
+ return true;
+}
+
+namespace
+{
+
+bool IsTagExpired(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ // Respect epg linger time.
+ const int iPastDays = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_EPG_PAST_DAYSTODISPLAY);
+ const CDateTime cleanupTime(CDateTime::GetUTCDateTime() - CDateTimeSpan(iPastDays, 0, 0, 0));
+
+ return tag->EndAsUTC() < cleanupTime;
+}
+
+} // unnamed namespace
+
+bool CPVREpg::UpdateEntry(const EPG_TAG* data, int iClientId)
+{
+ if (!data)
+ return false;
+
+ const std::shared_ptr<CPVREpgInfoTag> tag =
+ std::make_shared<CPVREpgInfoTag>(*data, iClientId, m_channelData, m_iEpgID);
+
+ return !IsTagExpired(tag) && m_tags.UpdateEntry(tag);
+}
+
+bool CPVREpg::UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE newState)
+{
+ bool bRet = true;
+ bool bNotify = true;
+
+ if (newState == EPG_EVENT_CREATED || newState == EPG_EVENT_UPDATED)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bRet = !IsTagExpired(tag) && m_tags.UpdateEntry(tag);
+ }
+ else if (newState == EPG_EVENT_DELETED)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVREpgInfoTag> existingTag = m_tags.GetTag(tag->UniqueBroadcastID());
+ if (!existingTag)
+ {
+ bRet = false;
+ }
+ else
+ {
+ if (IsTagExpired(existingTag))
+ {
+ m_tags.DeleteEntry(existingTag);
+ }
+ else
+ {
+ bNotify = false;
+ }
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unknown epg event state value: {}", newState);
+ bRet = false;
+ }
+
+ if (bRet && bNotify)
+ m_events.Publish(PVREvent::EpgItemUpdate);
+
+ return bRet;
+}
+
+bool CPVREpg::Update(time_t start,
+ time_t end,
+ int iUpdateTime,
+ int iPastDays,
+ const std::shared_ptr<CPVREpgDatabase>& database,
+ bool bForceUpdate /* = false */)
+{
+ bool bUpdate = false;
+ std::shared_ptr<CPVREpg> tmpEpg;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_lastScanTime.IsValid())
+ {
+ database->GetLastEpgScanTime(m_iEpgID, &m_lastScanTime);
+
+ if (!m_lastScanTime.IsValid())
+ {
+ m_lastScanTime.SetFromUTCDateTime(time_t(0));
+ m_bUpdateLastScanTime = true;
+ }
+ }
+
+ // enforce advanced settings update interval override for channels with no EPG data
+ if (m_tags.IsEmpty() && m_channelData->ChannelId() > 0) //! @todo why the channelid check?
+ iUpdateTime = CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iEpgUpdateEmptyTagsInterval;
+
+ if (bForceUpdate)
+ {
+ bUpdate = true;
+ }
+ else
+ {
+ // check if we have to update
+ time_t iNow = 0;
+ CDateTime::GetUTCDateTime().GetAsTime(iNow);
+
+ time_t iLastUpdate = 0;
+ m_lastScanTime.GetAsTime(iLastUpdate);
+
+ bUpdate = (iNow > iLastUpdate + iUpdateTime);
+ }
+
+ if (bUpdate)
+ {
+ tmpEpg = std::make_shared<CPVREpg>(m_iEpgID, m_strName, m_strScraperName, m_channelData,
+ std::shared_ptr<CPVREpgDatabase>());
+ }
+ }
+
+ // remove obsolete tags
+ Cleanup(iPastDays);
+
+ bool bGrabSuccess = true;
+
+ if (bUpdate)
+ {
+ bGrabSuccess = tmpEpg->UpdateFromScraper(start, end, bForceUpdate) && UpdateEntries(*tmpEpg);
+
+ if (!bGrabSuccess)
+ CLog::LogF(LOGERROR, "Failed to update table '{}'", Name());
+ }
+
+ m_bUpdatePending = false;
+ return bGrabSuccess;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpg::GetTags() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetAllTags();
+}
+
+bool CPVREpg::QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ // Note: It is guaranteed that both this EPG instance and database instance are already
+ // locked when this method gets called! No additional locking is needed here!
+
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ if (m_iEpgID <= 0 || m_bChanged)
+ {
+ const int iId = database->Persist(*this, m_iEpgID > 0);
+ if (iId > 0 && m_iEpgID != iId)
+ {
+ m_iEpgID = iId;
+ m_tags.SetEpgID(iId);
+ }
+ }
+
+ if (m_tags.NeedsSave())
+ m_tags.QueuePersistQuery();
+
+ if (m_bUpdateLastScanTime)
+ database->QueuePersistLastEpgScanTimeQuery(m_iEpgID, m_lastScanTime);
+
+ m_bChanged = false;
+ m_bUpdateLastScanTime = false;
+
+ return true;
+}
+
+bool CPVREpg::QueueDeleteQueries(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // delete own epg db entry
+ database->QueueDeleteEpgQuery(*this);
+
+ // delete last scan time db entry for this epg
+ database->QueueDeleteLastEpgScanTimeQuery(*this);
+
+ // delete all tags for this epg from db
+ m_tags.QueueDelete();
+
+ Clear();
+
+ return true;
+}
+
+std::pair<CDateTime, CDateTime> CPVREpg::GetFirstAndLastUncommitedEPGDate() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_tags.GetFirstAndLastUncommitedEPGDate();
+}
+
+bool CPVREpg::UpdateFromScraper(time_t start, time_t end, bool bForceUpdate)
+{
+ if (m_strScraperName.empty())
+ {
+ CLog::LogF(LOGERROR, "No EPG scraper defined for table '{}'", m_strName);
+ }
+ else if (m_strScraperName == "client")
+ {
+ if (!CServiceBroker::GetPVRManager().EpgsCreated())
+ return false;
+
+ if (!m_channelData->IsEPGEnabled() || m_channelData->IsHidden())
+ {
+ // ignore. not interested in any updates.
+ return true;
+ }
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+ if (client)
+ {
+ if (!client->GetClientCapabilities().SupportsEPG())
+ {
+ CLog::LogF(LOGERROR, "The backend for channel '{}' on client '{}' does not support EPGs",
+ m_channelData->ChannelName(), m_channelData->ClientId());
+ }
+ else if (!bForceUpdate && client->GetClientCapabilities().SupportsAsyncEPGTransfer())
+ {
+ // nothing to do. client will provide epg updates asynchronously
+ return true;
+ }
+ else
+ {
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Updating EPG for channel '{}' from client '{}'",
+ m_channelData->ChannelName(), m_channelData->ClientId());
+ return (client->GetEPGForChannel(m_channelData->UniqueClientChannelId(), this, start, end) == PVR_ERROR_NO_ERROR);
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Client '{}' not found, can't update", m_channelData->ClientId());
+ }
+ }
+ else // other non-empty scraper name...
+ {
+ CLog::LogF(LOGERROR, "Loading the EPG via scraper is not yet implemented!");
+ //! @todo Add Support for Web EPG Scrapers here
+ }
+
+ return false;
+}
+
+const std::string& CPVREpg::ConvertGenreIdToString(int iID, int iSubID)
+{
+ unsigned int iLabelId = 19499;
+ switch (iID)
+ {
+ case EPG_EVENT_CONTENTMASK_MOVIEDRAMA:
+ iLabelId = (iSubID <= 8) ? 19500 + iSubID : 19500;
+ break;
+ case EPG_EVENT_CONTENTMASK_NEWSCURRENTAFFAIRS:
+ iLabelId = (iSubID <= 4) ? 19516 + iSubID : 19516;
+ break;
+ case EPG_EVENT_CONTENTMASK_SHOW:
+ iLabelId = (iSubID <= 3) ? 19532 + iSubID : 19532;
+ break;
+ case EPG_EVENT_CONTENTMASK_SPORTS:
+ iLabelId = (iSubID <= 11) ? 19548 + iSubID : 19548;
+ break;
+ case EPG_EVENT_CONTENTMASK_CHILDRENYOUTH:
+ iLabelId = (iSubID <= 5) ? 19564 + iSubID : 19564;
+ break;
+ case EPG_EVENT_CONTENTMASK_MUSICBALLETDANCE:
+ iLabelId = (iSubID <= 6) ? 19580 + iSubID : 19580;
+ break;
+ case EPG_EVENT_CONTENTMASK_ARTSCULTURE:
+ iLabelId = (iSubID <= 11) ? 19596 + iSubID : 19596;
+ break;
+ case EPG_EVENT_CONTENTMASK_SOCIALPOLITICALECONOMICS:
+ iLabelId = (iSubID <= 3) ? 19612 + iSubID : 19612;
+ break;
+ case EPG_EVENT_CONTENTMASK_EDUCATIONALSCIENCE:
+ iLabelId = (iSubID <= 7) ? 19628 + iSubID : 19628;
+ break;
+ case EPG_EVENT_CONTENTMASK_LEISUREHOBBIES:
+ iLabelId = (iSubID <= 7) ? 19644 + iSubID : 19644;
+ break;
+ case EPG_EVENT_CONTENTMASK_SPECIAL:
+ iLabelId = (iSubID <= 3) ? 19660 + iSubID : 19660;
+ break;
+ case EPG_EVENT_CONTENTMASK_USERDEFINED:
+ iLabelId = (iSubID <= 8) ? 19676 + iSubID : 19676;
+ break;
+ default:
+ break;
+ }
+
+ return g_localizeStrings.Get(iLabelId);
+}
+
+std::shared_ptr<CPVREpgChannelData> CPVREpg::GetChannelData() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData;
+}
+
+void CPVREpg::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channelData = data;
+ m_tags.SetChannelData(data);
+}
+
+int CPVREpg::ChannelID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->ChannelId();
+}
+
+const std::string& CPVREpg::ScraperName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strScraperName;
+}
+
+const std::string& CPVREpg::Name() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strName;
+}
+
+int CPVREpg::EpgID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iEpgID;
+}
+
+bool CPVREpg::UpdatePending() const
+{
+ return m_bUpdatePending;
+}
+
+bool CPVREpg::NeedsSave() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bChanged || m_bUpdateLastScanTime || m_tags.NeedsSave();
+}
+
+bool CPVREpg::IsValid() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (ScraperName() == "client")
+ return m_channelData->ClientId() != -1 && m_channelData->UniqueClientChannelId() != PVR_CHANNEL_INVALID_UID;
+
+ return true;
+}
+
+void CPVREpg::RemovedFromContainer()
+{
+ m_events.Publish(PVREvent::EpgDeleted);
+}
+
+int CPVREpg::CleanupCachedImages(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ const std::vector<std::string> urlsToCheck = database->GetAllIconPaths(EpgID());
+ const std::string owner = StringUtils::Format(CPVREpgInfoTag::IMAGE_OWNER_PATTERN, EpgID());
+
+ return CPVRCachedImages::Cleanup({{owner, ""}}, urlsToCheck);
+}
diff --git a/xbmc/pvr/epg/Epg.h b/xbmc/pvr/epg/Epg.h
new file mode 100644
index 0000000..31bc8d0
--- /dev/null
+++ b/xbmc/pvr/epg/Epg.h
@@ -0,0 +1,327 @@
+/*
+ * 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 "XBDateTime.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h"
+#include "pvr/epg/EpgTagsContainer.h"
+#include "threads/CriticalSection.h"
+#include "utils/EventStream.h"
+
+#include <atomic>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+ enum class PVREvent;
+
+ class CPVREpgChannelData;
+ class CPVREpgDatabase;
+ class CPVREpgInfoTag;
+
+ class CPVREpg
+ {
+ friend class CPVREpgDatabase;
+
+ public:
+ /*!
+ * @brief Create a new EPG instance.
+ * @param iEpgID The ID of this table or <= 0 to create a new ID.
+ * @param strName The name of this table.
+ * @param strScraperName The name of the scraper to use.
+ * @param database The EPG database
+ */
+ CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Create a new EPG instance.
+ * @param iEpgID The ID of this table or <= 0 to create a new ID.
+ * @param strName The name of this table.
+ * @param strScraperName The name of the scraper to use.
+ * @param channelData The channel data.
+ * @param database The EPG database
+ */
+ CPVREpg(int iEpgID,
+ const std::string& strName,
+ const std::string& strScraperName,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Destroy this EPG instance.
+ */
+ virtual ~CPVREpg();
+
+ /*!
+ * @brief Get data for the channel associated with this EPG.
+ * @return The data.
+ */
+ std::shared_ptr<CPVREpgChannelData> GetChannelData() const;
+
+ /*!
+ * @brief Set data for the channel associated with this EPG.
+ * @param data The data.
+ */
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ /*!
+ * @brief The id of the channel associated with this EPG.
+ * @return The channel id or -1 if no channel is associated
+ */
+ int ChannelID() const;
+
+ /*!
+ * @brief Get the name of the scraper to use for this table.
+ * @return The name of the scraper to use for this table.
+ */
+ const std::string& ScraperName() const;
+
+ /*!
+ * @brief Returns if there is a manual update pending for this EPG
+ * @return True if there is a manual update pending, false otherwise
+ */
+ bool UpdatePending() const;
+
+ /*!
+ * @brief Clear the current tags and schedule manual update
+ */
+ void ForceUpdate();
+
+ /*!
+ * @brief Get the name of this table.
+ * @return The name of this table.
+ */
+ const std::string& Name() const;
+
+ /*!
+ * @brief Get the database ID of this table.
+ * @return The database ID of this table.
+ */
+ int EpgID() const;
+
+ /*!
+ * @brief Remove all entries from this EPG that finished before the given time.
+ * @param time Delete entries with an end time before this time in UTC.
+ */
+ void Cleanup(const CDateTime& time);
+
+ /*!
+ * @brief Remove all entries from this EPG.
+ */
+ void Clear();
+
+ /*!
+ * @brief Get the event that is occurring now
+ * @return The current event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagNow() const;
+
+ /*!
+ * @brief Get the event that will occur next
+ * @return The next event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagNext() const;
+
+ /*!
+ * @brief Get the event that occurred previously
+ * @return The previous event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagPrevious() const;
+
+ /*!
+ * @brief Get the event that occurs between the given begin and end time.
+ * @param beginTime Minimum start time in UTC of the event.
+ * @param endTime Maximum end time in UTC of the event.
+ * @param bUpdateFromClient if true, try to fetch the event from the client if not found locally.
+ * @return The found tag or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagBetween(const CDateTime& beginTime, const CDateTime& endTime, bool bUpdateFromClient = false);
+
+ /*!
+ * @brief Get the event matching the given unique broadcast id
+ * @param iUniqueBroadcastId The uid to look up
+ * @return The matching event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByBroadcastId(unsigned int iUniqueBroadcastId) const;
+
+ /*!
+ * @brief Get the event matching the given database id
+ * @param iDatabaseId The id to look up
+ * @return The matching event or NULL if it wasn't found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByDatabaseId(int iDatabaseId) const;
+
+ /*!
+ * @brief Update an entry in this EPG.
+ * @param data The tag to update.
+ * @param iClientId The id of the pvr client this event belongs to.
+ * @return True if it was updated successfully, false otherwise.
+ */
+ bool UpdateEntry(const EPG_TAG* data, int iClientId);
+
+ /*!
+ * @brief Update an entry in this EPG.
+ * @param tag The tag to update.
+ * @param newState the new state of the event.
+ * @return True if it was updated successfully, false otherwise.
+ */
+ bool UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE newState);
+
+ /*!
+ * @brief Update the EPG from 'start' till 'end'.
+ * @param start The start time.
+ * @param end The end time.
+ * @param iUpdateTime Update the table after the given amount of time has passed.
+ * @param iPastDays Amount of past days from now on, for which past entries are to be kept.
+ * @param database If given, the database to store the data.
+ * @param bForceUpdate Force update from client even if it's not the time to
+ * @return True if the update was successful, false otherwise.
+ */
+ bool Update(time_t start, time_t end, int iUpdateTime, int iPastDays, const std::shared_ptr<CPVREpgDatabase>& database, bool bForceUpdate = false);
+
+ /*!
+ * @brief Get all EPG tags.
+ * @return The tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTags() const;
+
+ /*!
+ * @brief Get all EPG tags for the given time frame, including "gap" tags.
+ * @param timelineStart Start of time line
+ * @param timelineEnd End of time line
+ * @param minEventEnd The minimum end time of the events to return
+ * @param maxEventStart The maximum start time of the events to return
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTimeline(const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const;
+
+ /*!
+ * @brief Write the query to persist data into given database's queue
+ * @param database The database.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Write the delete queries into the given database's queue
+ * @param database The database.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteQueries(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Get the start and end time of the last not yet commited entry in this table.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastUncommitedEPGDate() const;
+
+ /*!
+ * @brief Notify observers when the currently active tag changed.
+ * @return True if the playing tag has changed, false otherwise.
+ */
+ bool CheckPlayingEvent();
+
+ /*!
+ * @brief Convert a genre id and subid to a human readable name.
+ * @param iID The genre ID.
+ * @param iSubID The genre sub ID.
+ * @return A human readable name.
+ */
+ static const std::string& ConvertGenreIdToString(int iID, int iSubID);
+
+ /*!
+ * @brief Check whether this EPG has unsaved data.
+ * @return True if this EPG contains unsaved data, false otherwise.
+ */
+ bool NeedsSave() const;
+
+ /*!
+ * @brief Check whether this EPG is valid.
+ * @return True if this EPG is valid and can be updated, false otherwise.
+ */
+ bool IsValid() const;
+
+ /*!
+ * @brief Query the events available for CEventStream
+ */
+ CEventStream<PVREvent>& Events() { return m_events; }
+
+ /*!
+ * @brief Lock the instance. No other thread gets access to this EPG until Unlock was called.
+ */
+ void Lock() { m_critSection.lock(); }
+
+ /*!
+ * @brief Unlock the instance. Other threads may get access to this EPG again.
+ */
+ void Unlock() { m_critSection.unlock(); }
+
+ /*!
+ * @brief Called to inform the EPG that it has been removed from the EPG container.
+ */
+ void RemovedFromContainer();
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @param database The EPG database
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ private:
+ CPVREpg() = delete;
+ CPVREpg(const CPVREpg&) = delete;
+ CPVREpg& operator =(const CPVREpg&) = delete;
+
+ /*!
+ * @brief Update the EPG from a scraper set in the channel tag.
+ * @todo not implemented yet for non-pvr EPGs
+ * @param start Get entries with a start date after this time.
+ * @param end Get entries with an end date before this time.
+ * @param bForceUpdate Force update from client even if it's not the time to
+ * @return True if the update was successful, false otherwise.
+ */
+ bool UpdateFromScraper(time_t start, time_t end, bool bForceUpdate);
+
+ /*!
+ * @brief Update the contents of this table with the contents provided in "epg"
+ * @param epg The updated contents.
+ * @return True if the update was successful, false otherwise.
+ */
+ bool UpdateEntries(const CPVREpg& epg);
+
+ /*!
+ * @brief Remove all entries from this EPG that finished before the given amount of days.
+ * @param iPastDays Delete entries with an end time before the given amount of days from now on.
+ */
+ void Cleanup(int iPastDays);
+
+ bool m_bChanged = false; /*!< true if anything changed that needs to be persisted, false otherwise */
+ std::atomic<bool> m_bUpdatePending = {false}; /*!< true if manual update is pending */
+ int m_iEpgID = 0; /*!< the database ID of this table */
+ std::string m_strName; /*!< the name of this table */
+ std::string m_strScraperName; /*!< the name of the scraper to use */
+ CDateTime m_lastScanTime; /*!< the last time the EPG has been updated */
+ mutable CCriticalSection m_critSection; /*!< critical section for changes in this table */
+ bool m_bUpdateLastScanTime = false;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ CPVREpgTagsContainer m_tags;
+
+ CEventSource<PVREvent> m_events;
+ };
+}
diff --git a/xbmc/pvr/epg/EpgChannelData.cpp b/xbmc/pvr/epg/EpgChannelData.cpp
new file mode 100644
index 0000000..259b296
--- /dev/null
+++ b/xbmc/pvr/epg/EpgChannelData.cpp
@@ -0,0 +1,107 @@
+/*
+ * 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 "EpgChannelData.h"
+
+#include "XBDateTime.h"
+#include "pvr/channels/PVRChannel.h"
+
+using namespace PVR;
+
+CPVREpgChannelData::CPVREpgChannelData(int iClientId, int iUniqueClientChannelId)
+ : m_iClientId(iClientId), m_iUniqueClientChannelId(iUniqueClientChannelId)
+{
+}
+
+CPVREpgChannelData::CPVREpgChannelData(const CPVRChannel& channel)
+ : m_bIsRadio(channel.IsRadio()),
+ m_iClientId(channel.ClientID()),
+ m_iUniqueClientChannelId(channel.UniqueID()),
+ m_bIsHidden(channel.IsHidden()),
+ m_bIsLocked(channel.IsLocked()),
+ m_bIsEPGEnabled(channel.EPGEnabled()),
+ m_iChannelId(channel.ChannelID()),
+ m_strChannelName(channel.ChannelName()),
+ m_strChannelIconPath(channel.IconPath())
+{
+}
+
+bool CPVREpgChannelData::IsRadio() const
+{
+ return m_bIsRadio;
+}
+
+int CPVREpgChannelData::ClientId() const
+{
+ return m_iClientId;
+}
+
+int CPVREpgChannelData::UniqueClientChannelId() const
+{
+ return m_iUniqueClientChannelId;
+}
+
+bool CPVREpgChannelData::IsHidden() const
+{
+ return m_bIsHidden;
+}
+
+void CPVREpgChannelData::SetHidden(bool bIsHidden)
+{
+ m_bIsHidden = bIsHidden;
+}
+
+bool CPVREpgChannelData::IsLocked() const
+{
+ return m_bIsLocked;
+}
+
+void CPVREpgChannelData::SetLocked(bool bIsLocked)
+{
+ m_bIsLocked = bIsLocked;
+}
+
+bool CPVREpgChannelData::IsEPGEnabled() const
+{
+ return m_bIsEPGEnabled;
+}
+
+void CPVREpgChannelData::SetEPGEnabled(bool bIsEPGEnabled)
+{
+ m_bIsEPGEnabled = bIsEPGEnabled;
+}
+
+int CPVREpgChannelData::ChannelId() const
+{
+ return m_iChannelId;
+}
+
+void CPVREpgChannelData::SetChannelId(int iChannelId)
+{
+ m_iChannelId = iChannelId;
+}
+
+const std::string& CPVREpgChannelData::ChannelName() const
+{
+ return m_strChannelName;
+}
+
+void CPVREpgChannelData::SetChannelName(const std::string& strChannelName)
+{
+ m_strChannelName = strChannelName;
+}
+
+const std::string& CPVREpgChannelData::ChannelIconPath() const
+{
+ return m_strChannelIconPath;
+}
+
+void CPVREpgChannelData::SetChannelIconPath(const std::string& strChannelIconPath)
+{
+ m_strChannelIconPath = strChannelIconPath;
+}
diff --git a/xbmc/pvr/epg/EpgChannelData.h b/xbmc/pvr/epg/EpgChannelData.h
new file mode 100644
index 0000000..a2a63ec
--- /dev/null
+++ b/xbmc/pvr/epg/EpgChannelData.h
@@ -0,0 +1,59 @@
+/*
+ * 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 <ctime>
+#include <string>
+
+namespace PVR
+{
+class CPVRChannel;
+
+class CPVREpgChannelData
+{
+public:
+ CPVREpgChannelData() = default;
+ CPVREpgChannelData(int iClientId, int iUniqueClientChannelId);
+ explicit CPVREpgChannelData(const CPVRChannel& channel);
+
+ int ClientId() const;
+ int UniqueClientChannelId() const;
+ bool IsRadio() const;
+
+ bool IsHidden() const;
+ void SetHidden(bool bIsHidden);
+
+ bool IsLocked() const;
+ void SetLocked(bool bIsLocked);
+
+ bool IsEPGEnabled() const;
+ void SetEPGEnabled(bool bIsEPGEnabled);
+
+ int ChannelId() const;
+ void SetChannelId(int iChannelId);
+
+ const std::string& ChannelName() const;
+ void SetChannelName(const std::string& strChannelName);
+
+ const std::string& ChannelIconPath() const;
+ void SetChannelIconPath(const std::string& strChannelIconPath);
+
+private:
+ const bool m_bIsRadio = false;
+ const int m_iClientId = -1;
+ const int m_iUniqueClientChannelId = -1;
+
+ bool m_bIsHidden = false;
+ bool m_bIsLocked = false;
+ bool m_bIsEPGEnabled = true;
+ int m_iChannelId = -1;
+ std::string m_strChannelName;
+ std::string m_strChannelIconPath;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgContainer.cpp b/xbmc/pvr/epg/EpgContainer.cpp
new file mode 100644
index 0000000..d6f2015
--- /dev/null
+++ b/xbmc/pvr/epg/EpgContainer.cpp
@@ -0,0 +1,1028 @@
+/*
+ * 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 "EpgContainer.h"
+
+#include "ServiceBroker.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" // PVR_CHANNEL_INVALID_UID
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIProgressHandler.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SystemClock.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <utility>
+#include <vector>
+
+using namespace std::chrono_literals;
+
+namespace PVR
+{
+
+class CEpgUpdateRequest
+{
+public:
+ CEpgUpdateRequest() : CEpgUpdateRequest(-1, PVR_CHANNEL_INVALID_UID) {}
+ CEpgUpdateRequest(int iClientID, int iUniqueChannelID) : m_iClientID(iClientID), m_iUniqueChannelID(iUniqueChannelID) {}
+
+ void Deliver();
+
+private:
+ int m_iClientID;
+ int m_iUniqueChannelID;
+};
+
+void CEpgUpdateRequest::Deliver()
+{
+ const std::shared_ptr<CPVREpg> epg = CServiceBroker::GetPVRManager().EpgContainer().GetByChannelUid(m_iClientID, m_iUniqueChannelID);
+ if (!epg)
+ {
+ CLog::LogF(LOGERROR,
+ "Unable to obtain EPG for client {} and channel {}! Unable to deliver the epg "
+ "update request!",
+ m_iClientID, m_iUniqueChannelID);
+ return;
+ }
+
+ epg->ForceUpdate();
+}
+
+class CEpgTagStateChange
+{
+public:
+ CEpgTagStateChange() = default;
+ CEpgTagStateChange(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE eNewState) : m_epgtag(tag), m_state(eNewState) {}
+
+ void Deliver();
+
+private:
+ std::shared_ptr<CPVREpgInfoTag> m_epgtag;
+ EPG_EVENT_STATE m_state = EPG_EVENT_CREATED;
+};
+
+void CEpgTagStateChange::Deliver()
+{
+ const CPVREpgContainer& epgContainer = CServiceBroker::GetPVRManager().EpgContainer();
+
+ const std::shared_ptr<CPVREpg> epg = epgContainer.GetByChannelUid(m_epgtag->ClientID(), m_epgtag->UniqueChannelID());
+ if (!epg)
+ {
+ CLog::LogF(LOGERROR,
+ "Unable to obtain EPG for client {} and channel {}! Unable to deliver state change "
+ "for tag '{}'!",
+ m_epgtag->ClientID(), m_epgtag->UniqueChannelID(), m_epgtag->UniqueBroadcastID());
+ return;
+ }
+
+ if (m_epgtag->EpgID() < 0)
+ {
+ // now that we have the epg instance, fully initialize the tag
+ m_epgtag->SetEpgID(epg->EpgID());
+ m_epgtag->SetChannelData(epg->GetChannelData());
+ }
+
+ epg->UpdateEntry(m_epgtag, m_state);
+}
+
+CPVREpgContainer::CPVREpgContainer(CEventSource<PVREvent>& eventSource)
+ : CThread("EPGUpdater"),
+ m_database(new CPVREpgDatabase),
+ m_settings({CSettings::SETTING_EPG_EPGUPDATE, CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY,
+ CSettings::SETTING_EPG_PAST_DAYSTODISPLAY,
+ CSettings::SETTING_EPG_PREVENTUPDATESWHILEPLAYINGTV}),
+ m_events(eventSource)
+{
+ m_bStop = true; // base class member
+ m_updateEvent.Reset();
+}
+
+CPVREpgContainer::~CPVREpgContainer()
+{
+ Stop();
+ Unload();
+}
+
+std::shared_ptr<CPVREpgDatabase> CPVREpgContainer::GetEpgDatabase() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_database->IsOpen())
+ m_database->Open();
+
+ return m_database;
+}
+
+bool CPVREpgContainer::IsStarted() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bStarted;
+}
+
+int CPVREpgContainer::NextEpgId()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return ++m_iNextEpgId;
+}
+
+void CPVREpgContainer::Start()
+{
+ Stop();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bIsInitialising = true;
+
+ Create();
+ SetPriority(ThreadPriority::BELOW_NORMAL);
+
+ m_bStarted = true;
+ }
+}
+
+void CPVREpgContainer::Stop()
+{
+ StopThread();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bStarted = false;
+ }
+}
+
+bool CPVREpgContainer::Load()
+{
+ // EPGs must be loaded via PVR Manager -> channel groups -> EPG container to associate the
+ // channels with the right EPG.
+ CServiceBroker::GetPVRManager().TriggerEpgsCreate();
+ return true;
+}
+
+void CPVREpgContainer::Unload()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_updateRequestsLock);
+ m_updateRequests.clear();
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_epgTagChangesLock);
+ m_epgTagChanges.clear();
+ }
+
+ std::vector<std::shared_ptr<CPVREpg>> epgs;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* clear all epg tables and remove pointers to epg tables on channels */
+ std::transform(m_epgIdToEpgMap.cbegin(), m_epgIdToEpgMap.cend(), std::back_inserter(epgs),
+ [](const auto& epgEntry) { return epgEntry.second; });
+
+ m_epgIdToEpgMap.clear();
+ m_channelUidToEpgMap.clear();
+
+ m_iNextEpgUpdate = 0;
+ m_iNextEpgId = 0;
+ m_iNextEpgActiveTagCheck = 0;
+ m_bUpdateNotificationPending = false;
+ m_bLoaded = false;
+
+ m_database->Close();
+ }
+
+ for (const auto& epg : epgs)
+ {
+ epg->Events().Unsubscribe(this);
+ epg->RemovedFromContainer();
+ }
+}
+
+void CPVREpgContainer::Notify(const PVREvent& event)
+{
+ if (event == PVREvent::EpgItemUpdate)
+ {
+ // there can be many of these notifications during short time period. Thus, announce async and not every event.
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bUpdateNotificationPending = true;
+ return;
+ }
+ else if (event == PVREvent::EpgUpdatePending)
+ {
+ SetHasPendingUpdates(true);
+ return;
+ }
+ else if (event == PVREvent::EpgActiveItem)
+ {
+ // No need to propagate the change. See CPVREpgContainer::CheckPlayingEvents
+ return;
+ }
+
+ m_events.Publish(event);
+}
+
+void CPVREpgContainer::LoadFromDatabase()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bLoaded)
+ return;
+
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ database->Lock();
+ m_iNextEpgId = database->GetLastEPGId();
+ const std::vector<std::shared_ptr<CPVREpg>> result = database->GetAll();
+ database->Unlock();
+
+ for (const auto& entry : result)
+ InsertFromDB(entry);
+
+ m_bLoaded = true;
+}
+
+bool CPVREpgContainer::PersistAll(unsigned int iMaxTimeslice) const
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ std::vector<std::shared_ptr<CPVREpg>> changedEpgs;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& epg : m_epgIdToEpgMap)
+ {
+ if (epg.second && epg.second->NeedsSave())
+ {
+ // Note: We need to obtain a lock for every epg instance before we can lock
+ // the epg db. This order is important. Otherwise deadlocks may occur.
+ epg.second->Lock();
+ changedEpgs.emplace_back(epg.second);
+ }
+ }
+ }
+
+ bool bReturn = true;
+
+ if (!changedEpgs.empty())
+ {
+ // Note: We must lock the db the whole time, otherwise races may occur.
+ database->Lock();
+
+ XbmcThreads::EndTime<> processTimeslice{std::chrono::milliseconds(iMaxTimeslice)};
+ for (const auto& epg : changedEpgs)
+ {
+ if (!processTimeslice.IsTimePast())
+ {
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Container: Persisting events for channel '{}'...",
+ epg->GetChannelData()->ChannelName());
+
+ bReturn &= epg->QueuePersistQuery(database);
+
+ size_t queryCount = database->GetInsertQueriesCount() + database->GetDeleteQueriesCount();
+ if (queryCount > EPG_COMMIT_QUERY_COUNT_LIMIT)
+ {
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Container: committing {} queries in loop.",
+ queryCount);
+ database->CommitDeleteQueries();
+ database->CommitInsertQueries();
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Container: committed {} queries in loop.", queryCount);
+ }
+ }
+
+ epg->Unlock();
+ }
+
+ if (bReturn)
+ {
+ database->CommitDeleteQueries();
+ database->CommitInsertQueries();
+ }
+
+ database->Unlock();
+ }
+
+ return bReturn;
+}
+
+void CPVREpgContainer::Process()
+{
+ time_t iNow = 0;
+ time_t iLastSave = 0;
+
+ SetPriority(ThreadPriority::LOWEST);
+
+ while (!m_bStop)
+ {
+ time_t iLastEpgCleanup = 0;
+ bool bUpdateEpg = true;
+
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNow);
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bUpdateEpg = (iNow >= m_iNextEpgUpdate) && !m_bSuspended;
+ iLastEpgCleanup = m_iLastEpgCleanup;
+ }
+
+ /* update the EPG */
+ if (!InterruptUpdate() && bUpdateEpg && CServiceBroker::GetPVRManager().EpgsCreated() && UpdateEPG())
+ m_bIsInitialising = false;
+
+ /* clean up old entries */
+ if (!m_bStop && !m_bSuspended &&
+ iNow >= iLastEpgCleanup + CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_iEpgCleanupInterval)
+ RemoveOldEntries();
+
+ /* check for pending manual EPG updates */
+
+ while (!m_bStop && !m_bSuspended)
+ {
+ CEpgUpdateRequest request;
+ {
+ std::unique_lock<CCriticalSection> lock(m_updateRequestsLock);
+ if (m_updateRequests.empty())
+ break;
+
+ request = m_updateRequests.front();
+ m_updateRequests.pop_front();
+ }
+
+ // do the update
+ request.Deliver();
+ }
+
+ /* check for pending EPG tag changes */
+
+ // during Kodi startup, addons may push updates very early, even before EPGs are ready to use.
+ if (!m_bStop && !m_bSuspended && CServiceBroker::GetPVRManager().EpgsCreated())
+ {
+ unsigned int iProcessed = 0;
+ XbmcThreads::EndTime<> processTimeslice(
+ 1000ms); // max 1 sec per cycle, regardless of how many events are in the queue
+
+ while (!InterruptUpdate())
+ {
+ CEpgTagStateChange change;
+ {
+ std::unique_lock<CCriticalSection> lock(m_epgTagChangesLock);
+ if (processTimeslice.IsTimePast() || m_epgTagChanges.empty())
+ {
+ if (iProcessed > 0)
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Processed {} queued epg event changes.", iProcessed);
+
+ break;
+ }
+
+ change = m_epgTagChanges.front();
+ m_epgTagChanges.pop_front();
+ }
+
+ iProcessed++;
+
+ // deliver the updated tag to the respective epg
+ change.Deliver();
+ }
+ }
+
+ if (!m_bStop && !m_bSuspended)
+ {
+ bool bHasPendingUpdates = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bHasPendingUpdates = (m_pendingUpdates > 0);
+ }
+
+ if (bHasPendingUpdates)
+ UpdateEPG(true);
+ }
+
+ /* check for updated active tag */
+ if (!m_bStop)
+ CheckPlayingEvents();
+
+ /* check for pending update notifications */
+ if (!m_bStop)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bUpdateNotificationPending)
+ {
+ m_bUpdateNotificationPending = false;
+ m_events.Publish(PVREvent::Epg);
+ }
+ }
+
+ /* check for changes that need to be saved every 60 seconds */
+ if ((iNow - iLastSave > 60) && !InterruptUpdate())
+ {
+ PersistAll(1000);
+ iLastSave = iNow;
+ }
+
+ CThread::Sleep(1000ms);
+ }
+
+ // store data on exit
+ CLog::Log(LOGINFO, "EPG Container: Persisting unsaved events...");
+ PersistAll(std::numeric_limits<unsigned int>::max());
+ CLog::Log(LOGINFO, "EPG Container: Persisting events done");
+}
+
+std::vector<std::shared_ptr<CPVREpg>> CPVREpgContainer::GetAllEpgs() const
+{
+ std::vector<std::shared_ptr<CPVREpg>> epgs;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::transform(m_epgIdToEpgMap.cbegin(), m_epgIdToEpgMap.cend(), std::back_inserter(epgs),
+ [](const auto& epgEntry) { return epgEntry.second; });
+
+ return epgs;
+}
+
+std::shared_ptr<CPVREpg> CPVREpgContainer::GetById(int iEpgId) const
+{
+ std::shared_ptr<CPVREpg> retval;
+
+ if (iEpgId < 0)
+ return retval;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto& epgEntry = m_epgIdToEpgMap.find(iEpgId);
+ if (epgEntry != m_epgIdToEpgMap.end())
+ retval = epgEntry->second;
+
+ return retval;
+}
+
+std::shared_ptr<CPVREpg> CPVREpgContainer::GetByChannelUid(int iClientId, int iChannelUid) const
+{
+ std::shared_ptr<CPVREpg> epg;
+
+ if (iClientId < 0 || iChannelUid < 0)
+ return epg;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto& epgEntry = m_channelUidToEpgMap.find(std::pair<int, int>(iClientId, iChannelUid));
+ if (epgEntry != m_channelUidToEpgMap.end())
+ epg = epgEntry->second;
+
+ return epg;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgContainer::GetTagById(const std::shared_ptr<CPVREpg>& epg, unsigned int iBroadcastId) const
+{
+ std::shared_ptr<CPVREpgInfoTag> retval;
+
+ if (iBroadcastId == EPG_TAG_INVALID_UID)
+ return retval;
+
+ if (epg)
+ {
+ retval = epg->GetTagByBroadcastId(iBroadcastId);
+ }
+
+ return retval;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgContainer::GetTagByDatabaseId(int iDatabaseId) const
+{
+ std::shared_ptr<CPVREpgInfoTag> retval;
+
+ if (iDatabaseId <= 0)
+ return retval;
+
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ for (const auto& epgEntry : epgs)
+ {
+ retval = epgEntry.second->GetTagByDatabaseId(iDatabaseId);
+ if (retval)
+ break;
+ }
+
+ return retval;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgContainer::GetTags(
+ const PVREpgSearchData& searchData) const
+{
+ // make sure we have up-to-date data in the database.
+ PersistAll(std::numeric_limits<unsigned int>::max());
+
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> results = database->GetEpgTags(searchData);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tag : results)
+ {
+ const auto& it = m_epgIdToEpgMap.find(tag->EpgID());
+ if (it != m_epgIdToEpgMap.cend())
+ tag->SetChannelData((*it).second->GetChannelData());
+ }
+
+ return results;
+}
+
+void CPVREpgContainer::InsertFromDB(const std::shared_ptr<CPVREpg>& newEpg)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // table might already have been created when pvr channels were loaded
+ std::shared_ptr<CPVREpg> epg = GetById(newEpg->EpgID());
+ if (!epg)
+ {
+ // create a new epg table
+ epg = newEpg;
+ m_epgIdToEpgMap.insert({epg->EpgID(), epg});
+ epg->Events().Subscribe(this, &CPVREpgContainer::Notify);
+ }
+}
+
+std::shared_ptr<CPVREpg> CPVREpgContainer::CreateChannelEpg(int iEpgId, const std::string& strScraperName, const std::shared_ptr<CPVREpgChannelData>& channelData)
+{
+ std::shared_ptr<CPVREpg> epg;
+
+ WaitForUpdateFinish();
+ LoadFromDatabase();
+
+ if (iEpgId > 0)
+ epg = GetById(iEpgId);
+
+ if (!epg)
+ {
+ if (iEpgId <= 0)
+ iEpgId = NextEpgId();
+
+ epg.reset(new CPVREpg(iEpgId, channelData->ChannelName(), strScraperName, channelData,
+ GetEpgDatabase()));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_epgIdToEpgMap.insert({iEpgId, epg});
+ m_channelUidToEpgMap.insert({{channelData->ClientId(), channelData->UniqueClientChannelId()}, epg});
+ epg->Events().Subscribe(this, &CPVREpgContainer::Notify);
+ }
+ else if (epg->ChannelID() == -1)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channelUidToEpgMap.insert({{channelData->ClientId(), channelData->UniqueClientChannelId()}, epg});
+ epg->SetChannelData(channelData);
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPreventUpdates = false;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(m_iNextEpgUpdate);
+ }
+
+ m_events.Publish(PVREvent::EpgContainer);
+
+ return epg;
+}
+
+bool CPVREpgContainer::RemoveOldEntries()
+{
+ const CDateTime cleanupTime(CDateTime::GetUTCDateTime() - CDateTimeSpan(GetPastDaysToDisplay(), 0, 0, 0));
+
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ for (const auto& epgEntry : epgs)
+ epgEntry.second->Cleanup(cleanupTime);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(m_iLastEpgCleanup);
+
+ return true;
+}
+
+bool CPVREpgContainer::QueueDeleteEpgs(const std::vector<std::shared_ptr<CPVREpg>>& epgs)
+{
+ if (epgs.empty())
+ return true;
+
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return false;
+ }
+
+ for (const auto& epg : epgs)
+ {
+ // Note: We need to obtain a lock for every epg instance before we can lock
+ // the epg db. This order is important. Otherwise deadlocks may occur.
+ epg->Lock();
+ }
+
+ database->Lock();
+ for (const auto& epg : epgs)
+ {
+ QueueDeleteEpg(epg, database);
+ epg->Unlock();
+
+ size_t queryCount = database->GetDeleteQueriesCount();
+ if (queryCount > EPG_COMMIT_QUERY_COUNT_LIMIT)
+ database->CommitDeleteQueries();
+ }
+ database->CommitDeleteQueries();
+ database->Unlock();
+
+ return true;
+}
+
+bool CPVREpgContainer::QueueDeleteEpg(const std::shared_ptr<CPVREpg>& epg,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ if (!epg || epg->EpgID() < 0)
+ return false;
+
+ std::shared_ptr<CPVREpg> epgToDelete;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const auto& epgEntry = m_epgIdToEpgMap.find(epg->EpgID());
+ if (epgEntry == m_epgIdToEpgMap.end())
+ return false;
+
+ const auto& epgEntry1 = m_channelUidToEpgMap.find(std::make_pair(
+ epg->GetChannelData()->ClientId(), epg->GetChannelData()->UniqueClientChannelId()));
+ if (epgEntry1 != m_channelUidToEpgMap.end())
+ m_channelUidToEpgMap.erase(epgEntry1);
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting EPG table {} ({})", epg->Name(), epg->EpgID());
+ epgEntry->second->QueueDeleteQueries(database);
+
+ epgToDelete = epgEntry->second;
+ m_epgIdToEpgMap.erase(epgEntry);
+ }
+
+ epgToDelete->Events().Unsubscribe(this);
+ epgToDelete->RemovedFromContainer();
+ return true;
+}
+
+bool CPVREpgContainer::InterruptUpdate() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bStop ||
+ m_bPreventUpdates ||
+ (m_bPlaying && m_settings.GetBoolValue(CSettings::SETTING_EPG_PREVENTUPDATESWHILEPLAYINGTV));
+}
+
+void CPVREpgContainer::WaitForUpdateFinish()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPreventUpdates = true;
+
+ if (!m_bIsUpdating)
+ return;
+
+ m_updateEvent.Reset();
+ }
+
+ m_updateEvent.Wait();
+}
+
+bool CPVREpgContainer::UpdateEPG(bool bOnlyPending /* = false */)
+{
+ bool bInterrupted = false;
+ unsigned int iUpdatedTables = 0;
+ const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ /* set start and end time */
+ time_t start;
+ time_t end;
+ CDateTime::GetUTCDateTime().GetAsTime(start);
+ end = start + GetFutureDaysToDisplay() * 24 * 60 * 60;
+ start -= GetPastDaysToDisplay() * 24 * 60 * 60;
+
+ bool bShowProgress = (m_bIsInitialising || advancedSettings->m_bEpgDisplayIncrementalUpdatePopup) &&
+ advancedSettings->m_bEpgDisplayUpdatePopup;
+ int pendingUpdates = 0;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bIsUpdating || InterruptUpdate())
+ return false;
+
+ m_bIsUpdating = true;
+ pendingUpdates = m_pendingUpdates;
+ }
+
+ std::vector<std::shared_ptr<CPVREpg>> invalidTables;
+
+ unsigned int iCounter = 0;
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+
+ m_critSection.lock();
+ const auto epgsToUpdate = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ std::unique_ptr<CPVRGUIProgressHandler> progressHandler;
+ if (bShowProgress && !bOnlyPending && !epgsToUpdate.empty())
+ progressHandler.reset(
+ new CPVRGUIProgressHandler(g_localizeStrings.Get(19004))); // Loading programme guide
+
+ for (const auto& epgEntry : epgsToUpdate)
+ {
+ if (InterruptUpdate())
+ {
+ bInterrupted = true;
+ break;
+ }
+
+ const std::shared_ptr<CPVREpg> epg = epgEntry.second;
+ if (!epg)
+ continue;
+
+ if (progressHandler)
+ progressHandler->UpdateProgress(epg->GetChannelData()->ChannelName(), ++iCounter,
+ epgsToUpdate.size());
+
+ if ((!bOnlyPending || epg->UpdatePending()) &&
+ epg->Update(start,
+ end,
+ m_settings.GetIntValue(CSettings::SETTING_EPG_EPGUPDATE) * 60,
+ m_settings.GetIntValue(CSettings::SETTING_EPG_PAST_DAYSTODISPLAY),
+ database,
+ bOnlyPending))
+ {
+ iUpdatedTables++;
+ }
+ else if (!epg->IsValid())
+ {
+ invalidTables.push_back(epg);
+ }
+ }
+
+ progressHandler.reset();
+
+ QueueDeleteEpgs(invalidTables);
+
+ if (bInterrupted)
+ {
+ /* the update has been interrupted. try again later */
+ time_t iNow;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNow);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iNextEpgUpdate = iNow + advancedSettings->m_iEpgRetryInterruptedUpdateInterval;
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(m_iNextEpgUpdate);
+ m_iNextEpgUpdate += advancedSettings->m_iEpgUpdateCheckInterval;
+ if (m_pendingUpdates == pendingUpdates)
+ m_pendingUpdates = 0;
+ }
+
+ if (iUpdatedTables > 0)
+ m_events.Publish(PVREvent::EpgContainer);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bIsUpdating = false;
+ m_updateEvent.Set();
+
+ return !bInterrupted;
+}
+
+std::pair<CDateTime, CDateTime> CPVREpgContainer::GetFirstAndLastEPGDate() const
+{
+ // Get values from db
+ std::pair<CDateTime, CDateTime> dbDates;
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (database)
+ dbDates = database->GetFirstAndLastEPGDate();
+
+ // Merge not yet commited changes
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ CDateTime first(dbDates.first);
+ CDateTime last(dbDates.second);
+
+ for (const auto& epgEntry : epgs)
+ {
+ const auto dates = epgEntry.second->GetFirstAndLastUncommitedEPGDate();
+
+ if (dates.first.IsValid() && (!first.IsValid() || dates.first < first))
+ first = dates.first;
+
+ if (dates.second.IsValid() && (!last.IsValid() || dates.second > last))
+ last = dates.second;
+ }
+
+ return {first, last};
+}
+
+bool CPVREpgContainer::CheckPlayingEvents()
+{
+ bool bReturn = false;
+ bool bFoundChanges = false;
+
+ m_critSection.lock();
+ const auto epgs = m_epgIdToEpgMap;
+ time_t iNextEpgActiveTagCheck = m_iNextEpgActiveTagCheck;
+ m_critSection.unlock();
+
+ time_t iNow;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNow);
+ if (iNow >= iNextEpgActiveTagCheck)
+ {
+ bFoundChanges = std::accumulate(epgs.cbegin(), epgs.cend(), bFoundChanges,
+ [](bool found, const auto& epgEntry) {
+ return epgEntry.second->CheckPlayingEvent() ? true : found;
+ });
+
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(iNextEpgActiveTagCheck);
+ iNextEpgActiveTagCheck += CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iEpgActiveTagCheckInterval;
+
+ /* pvr tags always start on the full minute */
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ iNextEpgActiveTagCheck -= iNextEpgActiveTagCheck % 60;
+
+ bReturn = true;
+ }
+
+ if (bReturn)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iNextEpgActiveTagCheck = iNextEpgActiveTagCheck;
+ }
+
+ if (bFoundChanges)
+ m_events.Publish(PVREvent::EpgActiveItem);
+
+ return bReturn;
+}
+
+void CPVREpgContainer::SetHasPendingUpdates(bool bHasPendingUpdates /* = true */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (bHasPendingUpdates)
+ m_pendingUpdates++;
+ else
+ m_pendingUpdates = 0;
+}
+
+void CPVREpgContainer::UpdateRequest(int iClientID, int iUniqueChannelID)
+{
+ std::unique_lock<CCriticalSection> lock(m_updateRequestsLock);
+ m_updateRequests.emplace_back(CEpgUpdateRequest(iClientID, iUniqueChannelID));
+}
+
+void CPVREpgContainer::UpdateFromClient(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE eNewState)
+{
+ std::unique_lock<CCriticalSection> lock(m_epgTagChangesLock);
+ m_epgTagChanges.emplace_back(CEpgTagStateChange(tag, eNewState));
+}
+
+int CPVREpgContainer::GetPastDaysToDisplay() const
+{
+ return m_settings.GetIntValue(CSettings::SETTING_EPG_PAST_DAYSTODISPLAY);
+}
+
+int CPVREpgContainer::GetFutureDaysToDisplay() const
+{
+ return m_settings.GetIntValue(CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY);
+}
+
+void CPVREpgContainer::OnPlaybackStarted()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPlaying = true;
+}
+
+void CPVREpgContainer::OnPlaybackStopped()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bPlaying = false;
+}
+
+void CPVREpgContainer::OnSystemSleep()
+{
+ m_bSuspended = true;
+}
+
+void CPVREpgContainer::OnSystemWake()
+{
+ m_bSuspended = false;
+}
+
+int CPVREpgContainer::CleanupCachedImages()
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return 0;
+ }
+
+ // Processing can take some time. Do not block.
+ m_critSection.lock();
+ const std::map<int, std::shared_ptr<CPVREpg>> epgIdToEpgMap = m_epgIdToEpgMap;
+ m_critSection.unlock();
+
+ return std::accumulate(epgIdToEpgMap.cbegin(), epgIdToEpgMap.cend(), 0,
+ [&database](int cleanedImages, const auto& epg) {
+ return cleanedImages + epg.second->CleanupCachedImages(database);
+ });
+}
+
+std::vector<std::shared_ptr<CPVREpgSearchFilter>> CPVREpgContainer::GetSavedSearches(bool bRadio)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ return database->GetSavedSearches(bRadio);
+}
+
+std::shared_ptr<CPVREpgSearchFilter> CPVREpgContainer::GetSavedSearchById(bool bRadio, int iId)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ return database->GetSavedSearchById(bRadio, iId);
+}
+
+bool CPVREpgContainer::PersistSavedSearch(CPVREpgSearchFilter& search)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ if (database->Persist(search))
+ {
+ m_events.Publish(PVREvent::SavedSearchesInvalidated);
+ return true;
+ }
+ return false;
+}
+
+bool CPVREpgContainer::UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ return database->UpdateSavedSearchLastExecuted(epgSearch);
+}
+
+bool CPVREpgContainer::DeleteSavedSearch(const CPVREpgSearchFilter& search)
+{
+ const std::shared_ptr<CPVREpgDatabase> database = GetEpgDatabase();
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "No EPG database");
+ return {};
+ }
+
+ if (database->Delete(search))
+ {
+ m_events.Publish(PVREvent::SavedSearchesInvalidated);
+ return true;
+ }
+ return false;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgContainer.h b/xbmc/pvr/epg/EpgContainer.h
new file mode 100644
index 0000000..68cf50d
--- /dev/null
+++ b/xbmc/pvr/epg/EpgContainer.h
@@ -0,0 +1,360 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h"
+#include "pvr/settings/PVRSettings.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "utils/EventStream.h"
+
+#include <atomic>
+#include <list>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CDateTime;
+
+namespace PVR
+{
+ class CEpgUpdateRequest;
+ class CEpgTagStateChange;
+ class CPVREpg;
+ class CPVREpgChannelData;
+ class CPVREpgDatabase;
+ class CPVREpgInfoTag;
+ class CPVREpgSearchFilter;
+
+ enum class PVREvent;
+
+ struct PVREpgSearchData;
+
+ class CPVREpgContainer : private CThread
+ {
+ friend class CPVREpgDatabase;
+
+ public:
+ CPVREpgContainer() = delete;
+
+ /*!
+ * @brief Create a new EPG table container.
+ */
+ explicit CPVREpgContainer(CEventSource<PVREvent>& eventSource);
+
+ /*!
+ * @brief Destroy this instance.
+ */
+ ~CPVREpgContainer() override;
+
+ /*!
+ * @brief Get a pointer to the database instance.
+ * @return A pointer to the database instance.
+ */
+ std::shared_ptr<CPVREpgDatabase> GetEpgDatabase() const;
+
+ /*!
+ * @brief Start the EPG update thread.
+ */
+ void Start();
+
+ /*!
+ * @brief Stop the EPG update thread.
+ */
+ void Stop();
+
+ /**
+ * @brief (re)load EPG data.
+ * @return True if loaded successfully, false otherwise.
+ */
+ bool Load();
+
+ /**
+ * @brief unload all EPG data.
+ */
+ void Unload();
+
+ /*!
+ * @brief Check whether the EpgContainer has fully started.
+ * @return True if started, false otherwise.
+ */
+ bool IsStarted() const;
+
+ /*!
+ * @brief Queue the deletion of the given EPG tables from this container.
+ * @param epg The tables to delete.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteEpgs(const std::vector<std::shared_ptr<CPVREpg>>& epgs);
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+
+ /*!
+ * @brief Create the EPg for a given channel.
+ * @param iEpgId The EPG id.
+ * @param strScraperName The scraper name.
+ * @param channelData The channel data.
+ * @return the created EPG
+ */
+ std::shared_ptr<CPVREpg> CreateChannelEpg(int iEpgId, const std::string& strScraperName, const std::shared_ptr<CPVREpgChannelData>& channelData);
+
+ /*!
+ * @brief Get the start and end time across all EPGs.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastEPGDate() const;
+
+ /*!
+ * @brief Get all EPGs.
+ * @return The EPGs.
+ */
+ std::vector<std::shared_ptr<CPVREpg>> GetAllEpgs() const;
+
+ /*!
+ * @brief Get an EPG given its ID.
+ * @param iEpgId The database ID of the table.
+ * @return The EPG or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVREpg> GetById(int iEpgId) const;
+
+ /*!
+ * @brief Get an EPG given its client id and channel uid.
+ * @param iClientId the id of the pvr client providing the EPG
+ * @param iChannelUid the uid of the channel for the EPG
+ * @return The EPG or nullptr if it wasn't found.
+ */
+ std::shared_ptr<CPVREpg> GetByChannelUid(int iClientId, int iChannelUid) const;
+
+ /*!
+ * @brief Get the EPG event with the given event id
+ * @param epg The epg to lookup the event.
+ * @param iBroadcastId The event id to lookup.
+ * @return The requested event, or an empty tag when not found
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagById(const std::shared_ptr<CPVREpg>& epg, unsigned int iBroadcastId) const;
+
+ /*!
+ * @brief Get the EPG event with the given database id
+ * @param iDatabaseId The id to lookup.
+ * @return The requested event, or an empty tag when not found
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByDatabaseId(int iDatabaseId) const;
+
+ /*!
+ * @brief Get all EPG tags matching the given search criteria.
+ * @param searchData The search criteria.
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTags(const PVREpgSearchData& searchData) const;
+
+ /*!
+ * @brief Notify EPG container that there are pending manual EPG updates
+ * @param bHasPendingUpdates The new value
+ */
+ void SetHasPendingUpdates(bool bHasPendingUpdates = true);
+
+ /*!
+ * @brief A client triggered an epg update request for a channel
+ * @param iClientID The id of the client which triggered the update request
+ * @param iUniqueChannelID The uid of the channel for which the epg shall be updated
+ */
+ void UpdateRequest(int iClientID, int iUniqueChannelID);
+
+ /*!
+ * @brief A client announced an updated epg tag for a channel
+ * @param tag The epg tag containing the updated data
+ * @param eNewState The kind of change (CREATED, UPDATED, DELETED)
+ */
+ void UpdateFromClient(const std::shared_ptr<CPVREpgInfoTag>& tag, EPG_EVENT_STATE eNewState);
+
+ /*!
+ * @brief Get the number of past days to show in the guide and to import from backends.
+ * @return the number of past epg days.
+ */
+ int GetPastDaysToDisplay() const;
+
+ /*!
+ * @brief Get the number of future days to show in the guide and to import from backends.
+ * @return the number of future epg days.
+ */
+ int GetFutureDaysToDisplay() const;
+
+ /*!
+ * @brief Inform the epg container that playback of an item just started.
+ */
+ void OnPlaybackStarted();
+
+ /*!
+ * @brief Inform the epg container that playback of an item was stopped due to user interaction.
+ */
+ void OnPlaybackStopped();
+
+ /*!
+ * @brief Inform the epg container that the system is going to sleep
+ */
+ void OnSystemSleep();
+
+ /*!
+ * @brief Inform the epg container that the system gets awake from sleep
+ */
+ void OnSystemWake();
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+ /*!
+ * @brief Get all saved searches from the database.
+ * @param bRadio Whether to fetch saved searches for radio or TV.
+ * @return The searches.
+ */
+ std::vector<std::shared_ptr<CPVREpgSearchFilter>> GetSavedSearches(bool bRadio);
+
+ /*!
+ * @brief Get the saved search matching the given id.
+ * @param bRadio Whether to fetch a TV or radio saved search.
+ * @param iId The id.
+ * @return The saved search or nullptr if not found.
+ */
+ std::shared_ptr<CPVREpgSearchFilter> GetSavedSearchById(bool bRadio, int iId);
+
+ /*!
+ * @brief Persist a saved search in the database.
+ * @param search The saved search.
+ * @return True on success, false otherwise.
+ */
+ bool PersistSavedSearch(CPVREpgSearchFilter& search);
+
+ /*!
+ * @brief Update time last executed for the given search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Delete a saved search from the database.
+ * @param search The saved search.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteSavedSearch(const CPVREpgSearchFilter& search);
+
+ private:
+ /*!
+ * @brief Notify EPG table observers when the currently active tag changed.
+ * @return True if the check was done, false if it was not the right time to check
+ */
+ bool CheckPlayingEvents();
+
+ /*!
+ * @brief The next EPG ID to be given to a table when the db isn't being used.
+ * @return The next ID.
+ */
+ int NextEpgId();
+
+ /*!
+ * @brief Wait for an EPG update to finish.
+ */
+ void WaitForUpdateFinish();
+
+ /*!
+ * @brief Call Persist() on each table
+ * @param iMaxTimeslice time in milliseconds for max processing. Return after this time
+ * even if not all data was persisted, unless value is -1
+ * @return True when they all were persisted, false otherwise.
+ */
+ bool PersistAll(unsigned int iMaxTimeslice) const;
+
+ /*!
+ * @brief Remove old EPG entries.
+ * @return True if the old entries were removed successfully, false otherwise.
+ */
+ bool RemoveOldEntries();
+
+ /*!
+ * @brief Load and update the EPG data.
+ * @param bOnlyPending Only check and update EPG tables with pending manual updates
+ * @return True if the update has not been interrupted, false otherwise.
+ */
+ bool UpdateEPG(bool bOnlyPending = false);
+
+ /*!
+ * @brief Check whether a running update should be interrupted.
+ * @return True if a running update should be interrupted, false otherwise.
+ */
+ bool InterruptUpdate() const;
+
+ /*!
+ * @brief EPG update thread
+ */
+ void Process() override;
+
+ /*!
+ * @brief Load all tables from the database
+ */
+ void LoadFromDatabase();
+
+ /*!
+ * @brief Insert data from database
+ * @param newEpg the EPG containing the updated data.
+ */
+ void InsertFromDB(const std::shared_ptr<CPVREpg>& newEpg);
+
+ /*!
+ * @brief Queue the deletion of an EPG table from this container.
+ * @param epg The table to delete.
+ * @param database The database containing the epg data.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteEpg(const std::shared_ptr<CPVREpg>& epg,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+
+ std::shared_ptr<CPVREpgDatabase> m_database; /*!< the EPG database */
+
+ bool m_bIsUpdating = false; /*!< true while an update is running */
+ std::atomic<bool> m_bIsInitialising = {
+ true}; /*!< true while the epg manager hasn't loaded all tables */
+ bool m_bStarted = false; /*!< true if EpgContainer has fully started */
+ bool m_bLoaded = false; /*!< true after epg data is initially loaded from the database */
+ bool m_bPreventUpdates = false; /*!< true to prevent EPG updates */
+ bool m_bPlaying = false; /*!< true if Kodi is currently playing something */
+ int m_pendingUpdates = 0; /*!< count of pending manual updates */
+ time_t m_iLastEpgCleanup = 0; /*!< the time the EPG was cleaned up */
+ time_t m_iNextEpgUpdate = 0; /*!< the time the EPG will be updated */
+ time_t m_iNextEpgActiveTagCheck = 0; /*!< the time the EPG will be checked for active tag updates */
+ int m_iNextEpgId = 0; /*!< the next epg ID that will be given to a new table when the db isn't being used */
+
+ std::map<int, std::shared_ptr<CPVREpg>> m_epgIdToEpgMap; /*!< the EPGs in this container. maps epg ids to epgs */
+ std::map<std::pair<int, int>, std::shared_ptr<CPVREpg>> m_channelUidToEpgMap; /*!< the EPGs in this container. maps channel uids to epgs */
+
+ mutable CCriticalSection m_critSection; /*!< a critical section for changes to this container */
+ CEvent m_updateEvent; /*!< trigger when an update finishes */
+
+ std::list<CEpgUpdateRequest> m_updateRequests; /*!< list of update requests triggered by addon */
+ CCriticalSection m_updateRequestsLock; /*!< protect update requests */
+
+ std::list<CEpgTagStateChange> m_epgTagChanges; /*!< list of updated epg tags announced by addon */
+ CCriticalSection m_epgTagChangesLock; /*!< protect changed epg tags list */
+
+ bool m_bUpdateNotificationPending = false; /*!< true while an epg updated notification to observers is pending. */
+ CPVRSettings m_settings;
+ CEventSource<PVREvent>& m_events;
+
+ std::atomic<bool> m_bSuspended = {false};
+ };
+}
diff --git a/xbmc/pvr/epg/EpgDatabase.cpp b/xbmc/pvr/epg/EpgDatabase.cpp
new file mode 100644
index 0000000..191f595
--- /dev/null
+++ b/xbmc/pvr/epg/EpgDatabase.cpp
@@ -0,0 +1,1494 @@
+/*
+ * 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 "EpgDatabase.h"
+
+#include "ServiceBroker.h"
+#include "dbwrappers/dataset.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgSearchData.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace dbiplus;
+using namespace PVR;
+
+bool CPVREpgDatabase::Open()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return CDatabase::Open(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseEpg);
+}
+
+void CPVREpgDatabase::Close()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ CDatabase::Close();
+}
+
+void CPVREpgDatabase::Lock()
+{
+ m_critSection.lock();
+}
+
+void CPVREpgDatabase::Unlock()
+{
+ m_critSection.unlock();
+}
+
+void CPVREpgDatabase::CreateTables()
+{
+ CLog::Log(LOGINFO, "Creating EPG database tables");
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'epg'");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_pDS->exec(
+ "CREATE TABLE epg ("
+ "idEpg integer primary key, "
+ "sName varchar(64),"
+ "sScraperName varchar(32)"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'epgtags'");
+ m_pDS->exec(
+ "CREATE TABLE epgtags ("
+ "idBroadcast integer primary key, "
+ "iBroadcastUid integer, "
+ "idEpg integer, "
+ "sTitle varchar(128), "
+ "sPlotOutline text, "
+ "sPlot text, "
+ "sOriginalTitle varchar(128), "
+ "sCast varchar(255), "
+ "sDirector varchar(255), "
+ "sWriter varchar(255), "
+ "iYear integer, "
+ "sIMDBNumber varchar(50), "
+ "sIconPath varchar(255), "
+ "iStartTime integer, "
+ "iEndTime integer, "
+ "iGenreType integer, "
+ "iGenreSubType integer, "
+ "sGenre varchar(128), "
+ "sFirstAired varchar(32), "
+ "iParentalRating integer, "
+ "iStarRating integer, "
+ "iSeriesId integer, "
+ "iEpisodeId integer, "
+ "iEpisodePart integer, "
+ "sEpisodeName varchar(128), "
+ "iFlags integer, "
+ "sSeriesLink varchar(255), "
+ "sParentalRatingCode varchar(64)"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'lastepgscan'");
+ m_pDS->exec("CREATE TABLE lastepgscan ("
+ "idEpg integer primary key, "
+ "sLastScan varchar(20)"
+ ")"
+ );
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating table 'savedsearches'");
+ m_pDS->exec("CREATE TABLE savedsearches ("
+ "idSearch integer primary key,"
+ "sTitle varchar(255), "
+ "sLastExecutedDateTime varchar(20), "
+ "sSearchTerm varchar(255), "
+ "bSearchInDescription bool, "
+ "iGenreType integer, "
+ "sStartDateTime varchar(20), "
+ "sEndDateTime varchar(20), "
+ "bIsCaseSensitive bool, "
+ "iMinimumDuration integer, "
+ "iMaximumDuration integer, "
+ "bIsRadio bool, "
+ "iClientId integer, "
+ "iChannelUid integer, "
+ "bIncludeUnknownGenres bool, "
+ "bRemoveDuplicates bool, "
+ "bIgnoreFinishedBroadcasts bool, "
+ "bIgnoreFutureBroadcasts bool, "
+ "bFreeToAirOnly bool, "
+ "bIgnorePresentTimers bool, "
+ "bIgnorePresentRecordings bool,"
+ "iChannelGroup integer"
+ ")");
+}
+
+void CPVREpgDatabase::CreateAnalytics()
+{
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Creating EPG database indices");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_pDS->exec("CREATE UNIQUE INDEX idx_epg_idEpg_iStartTime on epgtags(idEpg, iStartTime desc);");
+ m_pDS->exec("CREATE INDEX idx_epg_iEndTime on epgtags(iEndTime);");
+}
+
+void CPVREpgDatabase::UpdateTables(int iVersion)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (iVersion < 5)
+ m_pDS->exec("ALTER TABLE epgtags ADD sGenre varchar(128);");
+
+ if (iVersion < 9)
+ m_pDS->exec("ALTER TABLE epgtags ADD sIconPath varchar(255);");
+
+ if (iVersion < 10)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD sOriginalTitle varchar(128);");
+ m_pDS->exec("ALTER TABLE epgtags ADD sCast varchar(255);");
+ m_pDS->exec("ALTER TABLE epgtags ADD sDirector varchar(255);");
+ m_pDS->exec("ALTER TABLE epgtags ADD sWriter varchar(255);");
+ m_pDS->exec("ALTER TABLE epgtags ADD iYear integer;");
+ m_pDS->exec("ALTER TABLE epgtags ADD sIMDBNumber varchar(50);");
+ }
+
+ if (iVersion < 11)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD iFlags integer;");
+ }
+
+ if (iVersion < 12)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD sSeriesLink varchar(255);");
+ }
+
+ if (iVersion < 13)
+ {
+ const bool isMySQL = StringUtils::EqualsNoCase(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseEpg.type, "mysql");
+
+ m_pDS->exec(
+ "CREATE TABLE epgtags_new ("
+ "idBroadcast integer primary key, "
+ "iBroadcastUid integer, "
+ "idEpg integer, "
+ "sTitle varchar(128), "
+ "sPlotOutline text, "
+ "sPlot text, "
+ "sOriginalTitle varchar(128), "
+ "sCast varchar(255), "
+ "sDirector varchar(255), "
+ "sWriter varchar(255), "
+ "iYear integer, "
+ "sIMDBNumber varchar(50), "
+ "sIconPath varchar(255), "
+ "iStartTime integer, "
+ "iEndTime integer, "
+ "iGenreType integer, "
+ "iGenreSubType integer, "
+ "sGenre varchar(128), "
+ "sFirstAired varchar(32), "
+ "iParentalRating integer, "
+ "iStarRating integer, "
+ "iSeriesId integer, "
+ "iEpisodeId integer, "
+ "iEpisodePart integer, "
+ "sEpisodeName varchar(128), "
+ "iFlags integer, "
+ "sSeriesLink varchar(255)"
+ ")"
+ );
+
+ m_pDS->exec(
+ "INSERT INTO epgtags_new ("
+ "idBroadcast, "
+ "iBroadcastUid, "
+ "idEpg, "
+ "sTitle, "
+ "sPlotOutline, "
+ "sPlot, "
+ "sOriginalTitle, "
+ "sCast, "
+ "sDirector, "
+ "sWriter, "
+ "iYear, "
+ "sIMDBNumber, "
+ "sIconPath, "
+ "iStartTime, "
+ "iEndTime, "
+ "iGenreType, "
+ "iGenreSubType, "
+ "sGenre, "
+ "sFirstAired, "
+ "iParentalRating, "
+ "iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, "
+ "iEpisodePart, "
+ "sEpisodeName, "
+ "iFlags, "
+ "sSeriesLink"
+ ") "
+ "SELECT "
+ "idBroadcast, "
+ "iBroadcastUid, "
+ "idEpg, "
+ "sTitle, "
+ "sPlotOutline, "
+ "sPlot, "
+ "sOriginalTitle, "
+ "sCast, "
+ "sDirector, "
+ "sWriter, "
+ "iYear, "
+ "sIMDBNumber, "
+ "sIconPath, "
+ "iStartTime, "
+ "iEndTime, "
+ "iGenreType, "
+ "iGenreSubType, "
+ "sGenre, "
+ "'' AS sFirstAired, "
+ "iParentalRating, "
+ "iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, "
+ "iEpisodePart, "
+ "sEpisodeName, "
+ "iFlags, "
+ "sSeriesLink "
+ "FROM epgtags"
+ );
+
+ if (isMySQL)
+ m_pDS->exec(
+ "UPDATE epgtags_new INNER JOIN epgtags ON epgtags_new.idBroadcast = epgtags.idBroadcast "
+ "SET epgtags_new.sFirstAired = DATE(FROM_UNIXTIME(epgtags.iFirstAired)) "
+ "WHERE epgtags.iFirstAired > 0"
+ );
+ else
+ m_pDS->exec(
+ "UPDATE epgtags_new SET sFirstAired = "
+ "COALESCE((SELECT STRFTIME('%Y-%m-%d', iFirstAired, 'UNIXEPOCH') "
+ "FROM epgtags WHERE epgtags.idBroadcast = epgtags_new.idBroadcast "
+ "AND epgtags.iFirstAired > 0), '')"
+ );
+
+ m_pDS->exec("DROP TABLE epgtags");
+ m_pDS->exec("ALTER TABLE epgtags_new RENAME TO epgtags");
+ }
+
+ if (iVersion < 14)
+ {
+ m_pDS->exec("ALTER TABLE epgtags ADD sParentalRatingCode varchar(64);");
+ }
+
+ if (iVersion < 15)
+ {
+ m_pDS->exec("CREATE TABLE savedsearches ("
+ "idSearch integer primary key,"
+ "sTitle varchar(255), "
+ "sLastExecutedDateTime varchar(20), "
+ "sSearchTerm varchar(255), "
+ "bSearchInDescription bool, "
+ "iGenreType integer, "
+ "sStartDateTime varchar(20), "
+ "sEndDateTime varchar(20), "
+ "bIsCaseSensitive bool, "
+ "iMinimumDuration integer, "
+ "iMaximumDuration integer, "
+ "bIsRadio bool, "
+ "iClientId integer, "
+ "iChannelUid integer, "
+ "bIncludeUnknownGenres bool, "
+ "bRemoveDuplicates bool, "
+ "bIgnoreFinishedBroadcasts bool, "
+ "bIgnoreFutureBroadcasts bool, "
+ "bFreeToAirOnly bool, "
+ "bIgnorePresentTimers bool, "
+ "bIgnorePresentRecordings bool"
+ ")");
+ }
+
+ if (iVersion < 16)
+ {
+ m_pDS->exec("ALTER TABLE savedsearches ADD iChannelGroup integer;");
+ m_pDS->exec("UPDATE savedsearches SET iChannelGroup = -1");
+ }
+}
+
+bool CPVREpgDatabase::DeleteEpg()
+{
+ bool bReturn(false);
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting all EPG data from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ bReturn = DeleteValues("epg") || bReturn;
+ bReturn = DeleteValues("epgtags") || bReturn;
+ bReturn = DeleteValues("lastepgscan") || bReturn;
+
+ return bReturn;
+}
+
+bool CPVREpgDatabase::QueueDeleteEpgQuery(const CPVREpg& table)
+{
+ /* invalid channel */
+ if (table.EpgID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid channel id: {}", table.EpgID());
+ return false;
+ }
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", table.EpgID()));
+
+ std::string strQuery;
+ if (BuildSQL(PrepareSQL("DELETE FROM %s ", "epg"), filter, strQuery))
+ return QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+bool CPVREpgDatabase::QueueDeleteTagQuery(const CPVREpgInfoTag& tag)
+{
+ /* tag without a database ID was not persisted */
+ if (tag.DatabaseID() <= 0)
+ return false;
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idBroadcast = %u", tag.DatabaseID()));
+
+ std::string strQuery;
+ BuildSQL(PrepareSQL("DELETE FROM %s ", "epgtags"), filter, strQuery);
+ return QueueDeleteQuery(strQuery);
+}
+
+std::vector<std::shared_ptr<CPVREpg>> CPVREpgDatabase::GetAll()
+{
+ std::vector<std::shared_ptr<CPVREpg>> result;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strQuery = PrepareSQL("SELECT idEpg, sName, sScraperName FROM epg;");
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ int iEpgID = m_pDS->fv("idEpg").get_asInt();
+ std::string strName = m_pDS->fv("sName").get_asString().c_str();
+ std::string strScraperName = m_pDS->fv("sScraperName").get_asString().c_str();
+
+ result.emplace_back(new CPVREpg(iEpgID, strName, strScraperName, shared_from_this()));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG data from the database");
+ }
+ }
+
+ return result;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::CreateEpgTag(
+ const std::unique_ptr<dbiplus::Dataset>& pDS)
+{
+ if (!pDS->eof())
+ {
+ std::shared_ptr<CPVREpgInfoTag> newTag(
+ new CPVREpgInfoTag(m_pDS->fv("idEpg").get_asInt(), m_pDS->fv("sIconPath").get_asString()));
+
+ time_t iStartTime;
+ iStartTime = static_cast<time_t>(m_pDS->fv("iStartTime").get_asInt());
+ const CDateTime startTime(iStartTime);
+ newTag->m_startTime = startTime;
+
+ time_t iEndTime = static_cast<time_t>(m_pDS->fv("iEndTime").get_asInt());
+ const CDateTime endTime(iEndTime);
+ newTag->m_endTime = endTime;
+
+ const std::string sFirstAired = m_pDS->fv("sFirstAired").get_asString();
+ if (sFirstAired.length() > 0)
+ newTag->m_firstAired.SetFromW3CDate(sFirstAired);
+
+ int iBroadcastUID = m_pDS->fv("iBroadcastUid").get_asInt();
+ // Compat: null value for broadcast uid changed from numerical -1 to 0 with PVR Addon API v4.0.0
+ newTag->m_iUniqueBroadcastID = iBroadcastUID == -1 ? EPG_TAG_INVALID_UID : iBroadcastUID;
+
+ newTag->m_iDatabaseID = m_pDS->fv("idBroadcast").get_asInt();
+ newTag->m_strTitle = m_pDS->fv("sTitle").get_asString();
+ newTag->m_strPlotOutline = m_pDS->fv("sPlotOutline").get_asString();
+ newTag->m_strPlot = m_pDS->fv("sPlot").get_asString();
+ newTag->m_strOriginalTitle = m_pDS->fv("sOriginalTitle").get_asString();
+ newTag->m_cast = newTag->Tokenize(m_pDS->fv("sCast").get_asString());
+ newTag->m_directors = newTag->Tokenize(m_pDS->fv("sDirector").get_asString());
+ newTag->m_writers = newTag->Tokenize(m_pDS->fv("sWriter").get_asString());
+ newTag->m_iYear = m_pDS->fv("iYear").get_asInt();
+ newTag->m_strIMDBNumber = m_pDS->fv("sIMDBNumber").get_asString();
+ newTag->m_iParentalRating = m_pDS->fv("iParentalRating").get_asInt();
+ newTag->m_iStarRating = m_pDS->fv("iStarRating").get_asInt();
+ newTag->m_iEpisodeNumber = m_pDS->fv("iEpisodeId").get_asInt();
+ newTag->m_iEpisodePart = m_pDS->fv("iEpisodePart").get_asInt();
+ newTag->m_strEpisodeName = m_pDS->fv("sEpisodeName").get_asString();
+ newTag->m_iSeriesNumber = m_pDS->fv("iSeriesId").get_asInt();
+ newTag->m_iFlags = m_pDS->fv("iFlags").get_asInt();
+ newTag->m_strSeriesLink = m_pDS->fv("sSeriesLink").get_asString();
+ newTag->m_strParentalRatingCode = m_pDS->fv("sParentalRatingCode").get_asString();
+ newTag->m_iGenreType = m_pDS->fv("iGenreType").get_asInt();
+ newTag->m_iGenreSubType = m_pDS->fv("iGenreSubType").get_asInt();
+ newTag->m_strGenreDescription = m_pDS->fv("sGenre").get_asString();
+
+ return newTag;
+ }
+ return {};
+}
+
+bool CPVREpgDatabase::HasTags(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT iStartTime FROM epgtags WHERE idEpg = %u LIMIT 1;", iEpgID);
+ std::string strValue = GetSingleValue(strQuery);
+ return !strValue.empty();
+}
+
+CDateTime CPVREpgDatabase::GetLastEndTime(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT MAX(iEndTime) FROM epgtags WHERE idEpg = %u;", iEpgID);
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {};
+}
+
+std::pair<CDateTime, CDateTime> CPVREpgDatabase::GetFirstAndLastEPGDate()
+{
+ CDateTime first;
+ CDateTime last;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // 1st query: get min start time
+ std::string strQuery = PrepareSQL("SELECT MIN(iStartTime) FROM epgtags;");
+
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ first = CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ // 2nd query: get max end time
+ strQuery = PrepareSQL("SELECT MAX(iEndTime) FROM epgtags;");
+
+ strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ last = CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {first, last};
+}
+
+CDateTime CPVREpgDatabase::GetMinStartTime(int iEpgID, const CDateTime& minStart)
+{
+ time_t t;
+ minStart.GetAsTime(t);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT MIN(iStartTime) "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime > %u;",
+ iEpgID, static_cast<unsigned int>(t));
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {};
+}
+
+CDateTime CPVREpgDatabase::GetMaxEndTime(int iEpgID, const CDateTime& maxEnd)
+{
+ time_t t;
+ maxEnd.GetAsTime(t);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT MAX(iEndTime) "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iEndTime <= %u;",
+ iEpgID, static_cast<unsigned int>(t));
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return CDateTime(static_cast<time_t>(std::atoi(strValue.c_str())));
+
+ return {};
+}
+
+namespace
+{
+
+class CSearchTermConverter
+{
+public:
+ explicit CSearchTermConverter(const std::string& strSearchTerm) { Parse(strSearchTerm); }
+
+ std::string ToSQL(const std::string& strFieldName) const
+ {
+ std::string result = "(";
+
+ for (auto it = m_fragments.cbegin(); it != m_fragments.cend();)
+ {
+ result += (*it);
+
+ ++it;
+ if (it != m_fragments.cend())
+ result += strFieldName;
+ }
+
+ StringUtils::TrimRight(result);
+ result += ")";
+ return result;
+ }
+
+private:
+ void Parse(const std::string& strSearchTerm)
+ {
+ std::string strParsedSearchTerm(strSearchTerm);
+ StringUtils::Trim(strParsedSearchTerm);
+
+ std::string strFragment;
+
+ bool bNextOR = false;
+ while (!strParsedSearchTerm.empty())
+ {
+ StringUtils::TrimLeft(strParsedSearchTerm);
+
+ if (StringUtils::StartsWith(strParsedSearchTerm, "!") ||
+ StringUtils::StartsWithNoCase(strParsedSearchTerm, "not"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ strFragment += " NOT ";
+ bNextOR = false;
+ }
+ else if (StringUtils::StartsWith(strParsedSearchTerm, "+") ||
+ StringUtils::StartsWithNoCase(strParsedSearchTerm, "and"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ strFragment += " AND ";
+ bNextOR = false;
+ }
+ else if (StringUtils::StartsWith(strParsedSearchTerm, "|") ||
+ StringUtils::StartsWithNoCase(strParsedSearchTerm, "or"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ strFragment += " OR ";
+ bNextOR = false;
+ }
+ else
+ {
+ std::string strTerm;
+ GetAndCutNextTerm(strParsedSearchTerm, strTerm);
+ if (!strTerm.empty())
+ {
+ if (bNextOR && !m_fragments.empty())
+ strFragment += " OR "; // default operator
+
+ strFragment += "(UPPER(";
+
+ m_fragments.emplace_back(strFragment);
+ strFragment.clear();
+
+ strFragment += ") LIKE UPPER('%";
+ StringUtils::Replace(strTerm, "'", "''"); // escape '
+ strFragment += strTerm;
+ strFragment += "%')) ";
+
+ bNextOR = true;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ StringUtils::TrimLeft(strParsedSearchTerm);
+ }
+
+ if (!strFragment.empty())
+ m_fragments.emplace_back(strFragment);
+ }
+
+ static void GetAndCutNextTerm(std::string& strSearchTerm, std::string& strNextTerm)
+ {
+ std::string strFindNext(" ");
+
+ if (StringUtils::EndsWith(strSearchTerm, "\""))
+ {
+ strSearchTerm.erase(0, 1);
+ strFindNext = "\"";
+ }
+
+ const size_t iNextPos = strSearchTerm.find(strFindNext);
+ if (iNextPos != std::string::npos)
+ {
+ strNextTerm = strSearchTerm.substr(0, iNextPos);
+ strSearchTerm.erase(0, iNextPos + 1);
+ }
+ else
+ {
+ strNextTerm = strSearchTerm;
+ strSearchTerm.clear();
+ }
+ }
+
+ std::vector<std::string> m_fragments;
+};
+
+} // unnamed namespace
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetEpgTags(
+ const PVREpgSearchData& searchData)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ std::string strQuery = PrepareSQL("SELECT * FROM epgtags");
+
+ Filter filter;
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // min start datetime
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_startDateTime.IsValid())
+ {
+ time_t minStart;
+ searchData.m_startDateTime.GetAsTime(minStart);
+ filter.AppendWhere(PrepareSQL("iStartTime >= %u", static_cast<unsigned int>(minStart)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // max end datetime
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_endDateTime.IsValid())
+ {
+ time_t maxEnd;
+ searchData.m_endDateTime.GetAsTime(maxEnd);
+ filter.AppendWhere(PrepareSQL("iEndTime <= %u", static_cast<unsigned int>(maxEnd)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // ignore finished broadcasts
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_bIgnoreFinishedBroadcasts)
+ {
+ const time_t minEnd = std::time(nullptr); // now
+ filter.AppendWhere(PrepareSQL("iEndTime > %u", static_cast<unsigned int>(minEnd)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // ignore future broadcasts
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_bIgnoreFutureBroadcasts)
+ {
+ const time_t maxStart = std::time(nullptr); // now
+ filter.AppendWhere(PrepareSQL("iStartTime < %u", static_cast<unsigned int>(maxStart)));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // genre type
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (searchData.m_iGenreType != EPG_SEARCH_UNSET)
+ {
+ if (searchData.m_bIncludeUnknownGenres)
+ {
+ // match the exact genre and everything with unknown genre
+ filter.AppendWhere(PrepareSQL("(iGenreType == %u) OR (iGenreType < %u) OR (iGenreType > %u)",
+ searchData.m_iGenreType, EPG_EVENT_CONTENTMASK_MOVIEDRAMA,
+ EPG_EVENT_CONTENTMASK_USERDEFINED));
+ }
+ else
+ {
+ // match only the exact genre
+ filter.AppendWhere(PrepareSQL("iGenreType == %u", searchData.m_iGenreType));
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // search term
+ /////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (!searchData.m_strSearchTerm.empty())
+ {
+ const CSearchTermConverter conv(searchData.m_strSearchTerm);
+
+ // title
+ std::string strWhere = conv.ToSQL("sTitle");
+
+ // plot outline
+ strWhere += " OR ";
+ strWhere += conv.ToSQL("sPlotOutline");
+
+ if (searchData.m_bSearchInDescription)
+ {
+ // plot
+ strWhere += " OR ";
+ strWhere += conv.ToSQL("sPlot");
+ }
+
+ filter.AppendWhere(strWhere);
+ }
+
+ if (BuildSQL(strQuery, filter, strQuery))
+ {
+ try
+ {
+ if (m_pDS->query(strQuery))
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags for given search criteria");
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByUniqueBroadcastID(
+ int iEpgID, unsigned int iUniqueBroadcastId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iBroadcastUid = %u;",
+ iEpgID, iUniqueBroadcastId);
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG tag with unique broadcast ID ({}) from the database",
+ iUniqueBroadcastId);
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByDatabaseID(int iEpgID, int iDatabaseId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND idBroadcast = %u;",
+ iEpgID, iDatabaseId);
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG tag with database ID ({}) from the database",
+ iDatabaseId);
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByStartTime(int iEpgID,
+ const CDateTime& startTime)
+{
+ time_t start;
+ startTime.GetAsTime(start);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery = PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime = %u;",
+ iEpgID, static_cast<unsigned int>(start));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG tag with start time ({}) from the database",
+ startTime.GetAsDBDateTime());
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByMinStartTime(
+ int iEpgID, const CDateTime& minStartTime)
+{
+ time_t minStart;
+ minStartTime.GetAsTime(minStart);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime >= %u ORDER BY iStartTime ASC LIMIT 1;",
+ iEpgID, static_cast<unsigned int>(minStart));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags with min start time ({}) for EPG ({})",
+ minStartTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgDatabase::GetEpgTagByMaxEndTime(int iEpgID,
+ const CDateTime& maxEndTime)
+{
+ time_t maxEnd;
+ maxEndTime.GetAsTime(maxEnd);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iEndTime <= %u ORDER BY iStartTime DESC LIMIT 1;",
+ iEpgID, static_cast<unsigned int>(maxEnd));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgInfoTag> tag = CreateEpgTag(m_pDS);
+ m_pDS->close();
+ return tag;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags with max end time ({}) for EPG ({})",
+ maxEndTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetEpgTagsByMinStartMaxEndTime(
+ int iEpgID, const CDateTime& minStartTime, const CDateTime& maxEndTime)
+{
+ time_t minStart;
+ minStartTime.GetAsTime(minStart);
+
+ time_t maxEnd;
+ maxEndTime.GetAsTime(maxEnd);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iStartTime >= %u AND iEndTime <= %u ORDER BY iStartTime;",
+ iEpgID, static_cast<unsigned int>(minStart), static_cast<unsigned int>(maxEnd));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR,
+ "Could not load tags with min start time ({}) and max end time ({}) for EPG ({})",
+ minStartTime.GetAsDBDateTime(), maxEndTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetEpgTagsByMinEndMaxStartTime(
+ int iEpgID, const CDateTime& minEndTime, const CDateTime& maxStartTime)
+{
+ time_t minEnd;
+ minEndTime.GetAsTime(minEnd);
+
+ time_t maxStart;
+ maxStartTime.GetAsTime(maxStart);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * "
+ "FROM epgtags "
+ "WHERE idEpg = %u AND iEndTime >= %u AND iStartTime <= %u ORDER BY iStartTime;",
+ iEpgID, static_cast<unsigned int>(minEnd), static_cast<unsigned int>(maxStart));
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR,
+ "Could not load tags with min end time ({}) and max start time ({}) for EPG ({})",
+ minEndTime.GetAsDBDateTime(), maxStartTime.GetAsDBDateTime(), iEpgID);
+ }
+ }
+
+ return {};
+}
+
+bool CPVREpgDatabase::QueueDeleteEpgTagsByMinEndMaxStartTimeQuery(int iEpgID,
+ const CDateTime& minEndTime,
+ const CDateTime& maxStartTime)
+{
+ time_t minEnd;
+ minEndTime.GetAsTime(minEnd);
+
+ time_t maxStart;
+ maxStartTime.GetAsTime(maxStart);
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u AND iEndTime >= %u AND iStartTime <= %u", iEpgID,
+ static_cast<unsigned int>(minEnd),
+ static_cast<unsigned int>(maxStart)));
+
+ std::string strQuery;
+ if (BuildSQL("DELETE FROM epgtags", filter, strQuery))
+ return QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgDatabase::GetAllEpgTags(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * FROM epgtags WHERE idEpg = %u ORDER BY iStartTime;", iEpgID);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ while (!m_pDS->eof())
+ {
+ tags.emplace_back(CreateEpgTag(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return tags;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags for EPG ({})", iEpgID);
+ }
+ }
+ return {};
+}
+
+std::vector<std::string> CPVREpgDatabase::GetAllIconPaths(int iEpgID)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT sIconPath FROM epgtags WHERE idEpg = %u;", iEpgID);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::vector<std::string> paths;
+ while (!m_pDS->eof())
+ {
+ paths.emplace_back(m_pDS->fv("sIconPath").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return paths;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load tags for EPG ({})", iEpgID);
+ }
+ }
+ return {};
+}
+
+bool CPVREpgDatabase::GetLastEpgScanTime(int iEpgId, CDateTime* lastScan)
+{
+ bool bReturn = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strWhereClause = PrepareSQL("idEpg = %u", iEpgId);
+ std::string strValue = GetSingleValue("lastepgscan", "sLastScan", strWhereClause);
+
+ if (!strValue.empty())
+ {
+ lastScan->SetFromDBDateTime(strValue);
+ bReturn = true;
+ }
+ else
+ {
+ lastScan->SetValid(false);
+ }
+
+ return bReturn;
+}
+
+bool CPVREpgDatabase::QueuePersistLastEpgScanTimeQuery(int iEpgId, const CDateTime& lastScanTime)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strQuery = PrepareSQL("REPLACE INTO lastepgscan(idEpg, sLastScan) VALUES (%u, '%s');",
+ iEpgId, lastScanTime.GetAsDBDateTime().c_str());
+
+ return QueueInsertQuery(strQuery);
+}
+
+bool CPVREpgDatabase::QueueDeleteLastEpgScanTimeQuery(const CPVREpg& table)
+{
+ if (table.EpgID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Invalid EPG id: {}", table.EpgID());
+ return false;
+ }
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", table.EpgID()));
+
+ std::string strQuery;
+ if (BuildSQL(PrepareSQL("DELETE FROM %s ", "lastepgscan"), filter, strQuery))
+ return QueueDeleteQuery(strQuery);
+
+ return false;
+}
+
+int CPVREpgDatabase::Persist(const CPVREpg& epg, bool bQueueWrite)
+{
+ int iReturn = -1;
+ std::string strQuery;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (epg.EpgID() > 0)
+ strQuery = PrepareSQL("REPLACE INTO epg (idEpg, sName, sScraperName) "
+ "VALUES (%u, '%s', '%s');",
+ epg.EpgID(), epg.Name().c_str(), epg.ScraperName().c_str());
+ else
+ strQuery = PrepareSQL("INSERT INTO epg (sName, sScraperName) "
+ "VALUES ('%s', '%s');",
+ epg.Name().c_str(), epg.ScraperName().c_str());
+
+ if (bQueueWrite)
+ {
+ if (QueueInsertQuery(strQuery))
+ iReturn = epg.EpgID() <= 0 ? 0 : epg.EpgID();
+ }
+ else
+ {
+ if (ExecuteQuery(strQuery))
+ iReturn = epg.EpgID() <= 0 ? static_cast<int>(m_pDS->lastinsertid()) : epg.EpgID();
+ }
+
+ return iReturn;
+}
+
+bool CPVREpgDatabase::DeleteEpgTags(int iEpgId, const CDateTime& maxEndTime)
+{
+ time_t iMaxEndTime;
+ maxEndTime.GetAsTime(iMaxEndTime);
+
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(
+ PrepareSQL("idEpg = %u AND iEndTime < %u", iEpgId, static_cast<unsigned int>(iMaxEndTime)));
+ return DeleteValues("epgtags", filter);
+}
+
+bool CPVREpgDatabase::DeleteEpgTags(int iEpgId)
+{
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", iEpgId));
+ return DeleteValues("epgtags", filter);
+}
+
+bool CPVREpgDatabase::QueueDeleteEpgTags(int iEpgId)
+{
+ Filter filter;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ filter.AppendWhere(PrepareSQL("idEpg = %u", iEpgId));
+
+ std::string strQuery;
+ BuildSQL(PrepareSQL("DELETE FROM %s ", "epgtags"), filter, strQuery);
+ return QueueDeleteQuery(strQuery);
+}
+
+bool CPVREpgDatabase::QueuePersistQuery(const CPVREpgInfoTag& tag)
+{
+ if (tag.EpgID() <= 0)
+ {
+ CLog::LogF(LOGERROR, "Tag '{}' does not have a valid table", tag.Title());
+ return false;
+ }
+
+ time_t iStartTime, iEndTime;
+ tag.StartAsUTC().GetAsTime(iStartTime);
+ tag.EndAsUTC().GetAsTime(iEndTime);
+
+ std::string sFirstAired;
+ if (tag.FirstAired().IsValid())
+ sFirstAired = tag.FirstAired().GetAsW3CDate();
+
+ int iBroadcastId = tag.DatabaseID();
+ std::string strQuery;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (iBroadcastId < 0)
+ {
+ strQuery = PrepareSQL(
+ "REPLACE INTO epgtags (idEpg, iStartTime, "
+ "iEndTime, sTitle, sPlotOutline, sPlot, sOriginalTitle, sCast, sDirector, sWriter, iYear, "
+ "sIMDBNumber, "
+ "sIconPath, iGenreType, iGenreSubType, sGenre, sFirstAired, iParentalRating, iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, iEpisodePart, sEpisodeName, iFlags, sSeriesLink, sParentalRatingCode, "
+ "iBroadcastUid) "
+ "VALUES (%u, %u, %u, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', %i, %i, "
+ "'%s', '%s', %i, %i, %i, %i, %i, '%s', %i, '%s', '%s', %i);",
+ tag.EpgID(), static_cast<unsigned int>(iStartTime), static_cast<unsigned int>(iEndTime),
+ tag.Title().c_str(), tag.PlotOutline().c_str(), tag.Plot().c_str(),
+ tag.OriginalTitle().c_str(), tag.DeTokenize(tag.Cast()).c_str(),
+ tag.DeTokenize(tag.Directors()).c_str(), tag.DeTokenize(tag.Writers()).c_str(), tag.Year(),
+ tag.IMDBNumber().c_str(), tag.ClientIconPath().c_str(), tag.GenreType(), tag.GenreSubType(),
+ tag.GenreDescription().c_str(), sFirstAired.c_str(), tag.ParentalRating(), tag.StarRating(),
+ tag.SeriesNumber(), tag.EpisodeNumber(), tag.EpisodePart(), tag.EpisodeName().c_str(),
+ tag.Flags(), tag.SeriesLink().c_str(), tag.ParentalRatingCode().c_str(),
+ tag.UniqueBroadcastID());
+ }
+ else
+ {
+ strQuery = PrepareSQL(
+ "REPLACE INTO epgtags (idEpg, iStartTime, "
+ "iEndTime, sTitle, sPlotOutline, sPlot, sOriginalTitle, sCast, sDirector, sWriter, iYear, "
+ "sIMDBNumber, "
+ "sIconPath, iGenreType, iGenreSubType, sGenre, sFirstAired, iParentalRating, iStarRating, "
+ "iSeriesId, "
+ "iEpisodeId, iEpisodePart, sEpisodeName, iFlags, sSeriesLink, sParentalRatingCode, "
+ "iBroadcastUid, idBroadcast) "
+ "VALUES (%u, %u, %u, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %i, '%s', '%s', %i, %i, "
+ "'%s', '%s', %i, %i, %i, %i, %i, '%s', %i, '%s', '%s', %i, %i);",
+ tag.EpgID(), static_cast<unsigned int>(iStartTime), static_cast<unsigned int>(iEndTime),
+ tag.Title().c_str(), tag.PlotOutline().c_str(), tag.Plot().c_str(),
+ tag.OriginalTitle().c_str(), tag.DeTokenize(tag.Cast()).c_str(),
+ tag.DeTokenize(tag.Directors()).c_str(), tag.DeTokenize(tag.Writers()).c_str(), tag.Year(),
+ tag.IMDBNumber().c_str(), tag.ClientIconPath().c_str(), tag.GenreType(), tag.GenreSubType(),
+ tag.GenreDescription().c_str(), sFirstAired.c_str(), tag.ParentalRating(), tag.StarRating(),
+ tag.SeriesNumber(), tag.EpisodeNumber(), tag.EpisodePart(), tag.EpisodeName().c_str(),
+ tag.Flags(), tag.SeriesLink().c_str(), tag.ParentalRatingCode().c_str(),
+ tag.UniqueBroadcastID(), iBroadcastId);
+ }
+
+ QueueInsertQuery(strQuery);
+ return true;
+}
+
+int CPVREpgDatabase::GetLastEPGId()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::string strQuery = PrepareSQL("SELECT MAX(idEpg) FROM epg");
+ std::string strValue = GetSingleValue(strQuery);
+ if (!strValue.empty())
+ return std::atoi(strValue.c_str());
+ return 0;
+}
+
+/********** Saved searches methods **********/
+
+std::shared_ptr<CPVREpgSearchFilter> CPVREpgDatabase::CreateEpgSearchFilter(
+ bool bRadio, const std::unique_ptr<dbiplus::Dataset>& pDS)
+{
+ if (!pDS->eof())
+ {
+ auto newSearch = std::make_shared<CPVREpgSearchFilter>(bRadio);
+
+ newSearch->SetDatabaseId(m_pDS->fv("idSearch").get_asInt());
+ newSearch->SetTitle(m_pDS->fv("sTitle").get_asString());
+
+ const std::string lastExec = m_pDS->fv("sLastExecutedDateTime").get_asString();
+ if (!lastExec.empty())
+ newSearch->SetLastExecutedDateTime(CDateTime::FromDBDateTime(lastExec));
+
+ newSearch->SetSearchTerm(m_pDS->fv("sSearchTerm").get_asString());
+ newSearch->SetSearchInDescription(m_pDS->fv("bSearchInDescription").get_asBool());
+ newSearch->SetGenreType(m_pDS->fv("iGenreType").get_asInt());
+
+ const std::string start = m_pDS->fv("sStartDateTime").get_asString();
+ if (!start.empty())
+ newSearch->SetStartDateTime(CDateTime::FromDBDateTime(start));
+
+ const std::string end = m_pDS->fv("sEndDateTime").get_asString();
+ if (!end.empty())
+ newSearch->SetEndDateTime(CDateTime::FromDBDateTime(end));
+
+ newSearch->SetCaseSensitive(m_pDS->fv("bIsCaseSensitive").get_asBool());
+ newSearch->SetMinimumDuration(m_pDS->fv("iMinimumDuration").get_asInt());
+ newSearch->SetMaximumDuration(m_pDS->fv("iMaximumDuration").get_asInt());
+ newSearch->SetClientID(m_pDS->fv("iClientId").get_asInt());
+ newSearch->SetChannelUID(m_pDS->fv("iChannelUid").get_asInt());
+ newSearch->SetIncludeUnknownGenres(m_pDS->fv("bIncludeUnknownGenres").get_asBool());
+ newSearch->SetRemoveDuplicates(m_pDS->fv("bRemoveDuplicates").get_asBool());
+ newSearch->SetIgnoreFinishedBroadcasts(m_pDS->fv("bIgnoreFinishedBroadcasts").get_asBool());
+ newSearch->SetIgnoreFutureBroadcasts(m_pDS->fv("bIgnoreFutureBroadcasts").get_asBool());
+ newSearch->SetFreeToAirOnly(m_pDS->fv("bFreeToAirOnly").get_asBool());
+ newSearch->SetIgnorePresentTimers(m_pDS->fv("bIgnorePresentTimers").get_asBool());
+ newSearch->SetIgnorePresentRecordings(m_pDS->fv("bIgnorePresentRecordings").get_asBool());
+ newSearch->SetChannelGroupID(m_pDS->fv("iChannelGroup").get_asInt());
+
+ newSearch->SetChanged(false);
+
+ return newSearch;
+ }
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgSearchFilter>> CPVREpgDatabase::GetSavedSearches(bool bRadio)
+{
+ std::vector<std::shared_ptr<CPVREpgSearchFilter>> result;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * FROM savedsearches WHERE bIsRadio = %u", bRadio);
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ while (!m_pDS->eof())
+ {
+ result.emplace_back(CreateEpgSearchFilter(bRadio, m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG search data from the database");
+ }
+ }
+ return result;
+}
+
+std::shared_ptr<CPVREpgSearchFilter> CPVREpgDatabase::GetSavedSearchById(bool bRadio, int iId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strQuery =
+ PrepareSQL("SELECT * FROM savedsearches WHERE bIsRadio = %u AND idSearch = %u;", bRadio, iId);
+
+ if (ResultQuery(strQuery))
+ {
+ try
+ {
+ std::shared_ptr<CPVREpgSearchFilter> filter = CreateEpgSearchFilter(bRadio, m_pDS);
+ m_pDS->close();
+ return filter;
+ }
+ catch (...)
+ {
+ CLog::LogF(LOGERROR, "Could not load EPG search filter with id ({}) from the database", iId);
+ }
+ }
+
+ return {};
+}
+
+bool CPVREpgDatabase::Persist(CPVREpgSearchFilter& epgSearch)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // Insert a new entry if this is a new search, replace the existing otherwise
+ std::string strQuery;
+ if (epgSearch.GetDatabaseId() == -1)
+ strQuery = PrepareSQL(
+ "INSERT INTO savedsearches "
+ "(sTitle, sLastExecutedDateTime, sSearchTerm, bSearchInDescription, bIsCaseSensitive, "
+ "iGenreType, bIncludeUnknownGenres, sStartDateTime, sEndDateTime, iMinimumDuration, "
+ "iMaximumDuration, bIsRadio, iClientId, iChannelUid, bRemoveDuplicates, "
+ "bIgnoreFinishedBroadcasts, bIgnoreFutureBroadcasts, bFreeToAirOnly, bIgnorePresentTimers, "
+ "bIgnorePresentRecordings, iChannelGroup) "
+ "VALUES ('%s', '%s', '%s', %i, %i, %i, %i, '%s', '%s', %i, %i, %i, %i, %i, %i, %i, %i, "
+ "%i, %i, %i, %i);",
+ epgSearch.GetTitle().c_str(),
+ epgSearch.GetLastExecutedDateTime().IsValid()
+ ? epgSearch.GetLastExecutedDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetSearchTerm().c_str(), epgSearch.ShouldSearchInDescription() ? 1 : 0,
+ epgSearch.IsCaseSensitive() ? 1 : 0, epgSearch.GetGenreType(),
+ epgSearch.ShouldIncludeUnknownGenres() ? 1 : 0,
+ epgSearch.GetStartDateTime().IsValid()
+ ? epgSearch.GetStartDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetEndDateTime().IsValid() ? epgSearch.GetEndDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetMinimumDuration(), epgSearch.GetMaximumDuration(), epgSearch.IsRadio() ? 1 : 0,
+ epgSearch.GetClientID(), epgSearch.GetChannelUID(),
+ epgSearch.ShouldRemoveDuplicates() ? 1 : 0,
+ epgSearch.ShouldIgnoreFinishedBroadcasts() ? 1 : 0,
+ epgSearch.ShouldIgnoreFutureBroadcasts() ? 1 : 0, epgSearch.IsFreeToAirOnly() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentTimers() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentRecordings() ? 1 : 0, epgSearch.GetChannelGroupID());
+ else
+ strQuery = PrepareSQL(
+ "REPLACE INTO savedsearches "
+ "(idSearch, sTitle, sLastExecutedDateTime, sSearchTerm, bSearchInDescription, "
+ "bIsCaseSensitive, iGenreType, bIncludeUnknownGenres, sStartDateTime, sEndDateTime, "
+ "iMinimumDuration, iMaximumDuration, bIsRadio, iClientId, iChannelUid, bRemoveDuplicates, "
+ "bIgnoreFinishedBroadcasts, bIgnoreFutureBroadcasts, bFreeToAirOnly, bIgnorePresentTimers, "
+ "bIgnorePresentRecordings, iChannelGroup) "
+ "VALUES (%i, '%s', '%s', '%s', %i, %i, %i, %i, '%s', '%s', %i, %i, %i, %i, %i, %i, %i, %i, "
+ "%i, %i, %i, %i);",
+ epgSearch.GetDatabaseId(), epgSearch.GetTitle().c_str(),
+ epgSearch.GetLastExecutedDateTime().IsValid()
+ ? epgSearch.GetLastExecutedDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetSearchTerm().c_str(), epgSearch.ShouldSearchInDescription() ? 1 : 0,
+ epgSearch.IsCaseSensitive() ? 1 : 0, epgSearch.GetGenreType(),
+ epgSearch.ShouldIncludeUnknownGenres() ? 1 : 0,
+ epgSearch.GetStartDateTime().IsValid()
+ ? epgSearch.GetStartDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetEndDateTime().IsValid() ? epgSearch.GetEndDateTime().GetAsDBDateTime().c_str()
+ : "",
+ epgSearch.GetMinimumDuration(), epgSearch.GetMaximumDuration(), epgSearch.IsRadio() ? 1 : 0,
+ epgSearch.GetClientID(), epgSearch.GetChannelUID(),
+ epgSearch.ShouldRemoveDuplicates() ? 1 : 0,
+ epgSearch.ShouldIgnoreFinishedBroadcasts() ? 1 : 0,
+ epgSearch.ShouldIgnoreFutureBroadcasts() ? 1 : 0, epgSearch.IsFreeToAirOnly() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentTimers() ? 1 : 0,
+ epgSearch.ShouldIgnorePresentRecordings() ? 1 : 0, epgSearch.GetChannelGroupID());
+
+ bool bReturn = ExecuteQuery(strQuery);
+
+ if (bReturn)
+ {
+ // Set the database id for searches persisted for the first time
+ if (epgSearch.GetDatabaseId() == -1)
+ epgSearch.SetDatabaseId(static_cast<int>(m_pDS->lastinsertid()));
+
+ epgSearch.SetChanged(false);
+ }
+
+ return bReturn;
+}
+
+bool CPVREpgDatabase::UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch)
+{
+ if (epgSearch.GetDatabaseId() == -1)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::string strQuery = PrepareSQL(
+ "UPDATE savedsearches SET sLastExecutedDateTime = '%s' WHERE idSearch = %i",
+ epgSearch.GetLastExecutedDateTime().GetAsDBDateTime().c_str(), epgSearch.GetDatabaseId());
+ return ExecuteQuery(strQuery);
+}
+
+bool CPVREpgDatabase::Delete(const CPVREpgSearchFilter& epgSearch)
+{
+ if (epgSearch.GetDatabaseId() == -1)
+ return false;
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting saved search '{}' from the database",
+ epgSearch.GetTitle());
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ Filter filter;
+ filter.AppendWhere(PrepareSQL("idSearch = '%i'", epgSearch.GetDatabaseId()));
+
+ return DeleteValues("savedsearches", filter);
+}
+
+bool CPVREpgDatabase::DeleteSavedSearches()
+{
+ CLog::LogFC(LOGDEBUG, LOGEPG, "Deleting all saved searches from the database");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return DeleteValues("savedsearches");
+}
diff --git a/xbmc/pvr/epg/EpgDatabase.h b/xbmc/pvr/epg/EpgDatabase.h
new file mode 100644
index 0000000..580568d
--- /dev/null
+++ b/xbmc/pvr/epg/EpgDatabase.h
@@ -0,0 +1,377 @@
+/*
+ * 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 "dbwrappers/Database.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <vector>
+
+class CDateTime;
+
+namespace PVR
+{
+ class CPVREpg;
+ class CPVREpgInfoTag;
+ class CPVREpgSearchFilter;
+
+ struct PVREpgSearchData;
+
+ /** The EPG database */
+
+ static constexpr int EPG_COMMIT_QUERY_COUNT_LIMIT = 10000;
+
+ class CPVREpgDatabase : public CDatabase, public std::enable_shared_from_this<CPVREpgDatabase>
+ {
+ public:
+ /*!
+ * @brief Create a new instance of the EPG database.
+ */
+ CPVREpgDatabase() = default;
+
+ /*!
+ * @brief Destroy this instance.
+ */
+ ~CPVREpgDatabase() override = default;
+
+ /*!
+ * @brief Open the database.
+ * @return True if it was opened successfully, false otherwise.
+ */
+ bool Open() override;
+
+ /*!
+ * @brief Close the database.
+ */
+ void Close() override;
+
+ /*!
+ * @brief Lock the database.
+ */
+ void Lock();
+
+ /*!
+ * @brief Unlock the database.
+ */
+ void Unlock();
+
+ /*!
+ * @brief Get the minimal database version that is required to operate correctly.
+ * @return The minimal database version.
+ */
+ int GetSchemaVersion() const override { return 16; }
+
+ /*!
+ * @brief Get the default sqlite database filename.
+ * @return The default filename.
+ */
+ const char* GetBaseDBName() const override { return "Epg"; }
+
+ /*! @name EPG methods */
+ //@{
+
+ /*!
+ * @brief Remove all EPG information from the database
+ * @return True if the EPG information was erased, false otherwise.
+ */
+ bool DeleteEpg();
+
+ /*!
+ * @brief Queue deletionof an EPG table.
+ * @param tag The table to queue for deletion.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteEpgQuery(const CPVREpg& table);
+
+ /*!
+ * @brief Write the query to delete the given EPG tag to db query queue.
+ * @param tag The EPG tag to remove.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteTagQuery(const CPVREpgInfoTag& tag);
+
+ /*!
+ * @brief Get all EPG tables from the database. Does not get the EPG tables' entries.
+ * @return The entries.
+ */
+ std::vector<std::shared_ptr<CPVREpg>> GetAll();
+
+ /*!
+ * @brief Get all tags for a given EPG id.
+ * @param iEpgID The ID of the EPG.
+ * @return The entries.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetAllEpgTags(int iEpgID);
+
+ /*!
+ * @brief Get all icon paths for a given EPG id.
+ * @param iEpgID The ID of the EPG.
+ * @return The entries.
+ */
+ std::vector<std::string> GetAllIconPaths(int iEpgID);
+
+ /*!
+ * @brief Check whether this EPG has any tags.
+ * @param iEpgID The ID of the EPG.
+ * @return True in case there are tags, false otherwise.
+ */
+ bool HasTags(int iEpgID);
+
+ /*!
+ * @brief Get the end time of the last tag in this EPG.
+ * @param iEpgID The ID of the EPG.
+ * @return The time.
+ */
+ CDateTime GetLastEndTime(int iEpgID);
+
+ /*!
+ * @brief Get the start and end time across all EPGs.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastEPGDate();
+
+ /*!
+ * @brief Get the start time of the first tag with a start time greater than the given min time.
+ * @param iEpgID The ID of the EPG.
+ * @param minStart The min start time.
+ * @return The time.
+ */
+ CDateTime GetMinStartTime(int iEpgID, const CDateTime& minStart);
+
+ /*!
+ * @brief Get the end time of the first tag with an end time less than the given max time.
+ * @param iEpgID The ID of the EPG.
+ * @param maxEnd The mx end time.
+ * @return The time.
+ */
+ CDateTime GetMaxEndTime(int iEpgID, const CDateTime& maxEnd);
+
+ /*!
+ * @brief Get all EPG tags matching the given search criteria.
+ * @param searchData The search criteria.
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTags(const PVREpgSearchData& searchData);
+
+ /*!
+ * @brief Get an EPG tag given its EPG id and unique broadcast ID.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param iUniqueBroadcastId The unique broadcast ID for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByUniqueBroadcastID(int iEpgID,
+ unsigned int iUniqueBroadcastId);
+
+ /*!
+ * @brief Get an EPG tag given its EPG id and database ID.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param iDatabaseId The database ID for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByDatabaseID(int iEpgID, int iDatabaseId);
+
+ /*!
+ * @brief Get an EPG tag given its EPG ID and start time.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param startTime The start time for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByStartTime(int iEpgID, const CDateTime& startTime);
+
+ /*!
+ * @brief Get the next EPG tag matching the given EPG id and min start time.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param minStartTime The min start time for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByMinStartTime(int iEpgID,
+ const CDateTime& minStartTime);
+
+ /*!
+ * @brief Get the next EPG tag matching the given EPG id and max end time.
+ * @param iEpgID The ID of the EPG for the tag to get.
+ * @param maxEndTime The max end time for the tag to get.
+ * @return The tag or nullptr, if not found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgTagByMaxEndTime(int iEpgID, const CDateTime& maxEndTime);
+
+ /*!
+ * @brief Get all EPG tags matching the given EPG id, min start time and max end time.
+ * @param iEpgID The ID of the EPG for the tags to get.
+ * @param minStartTime The min start time for the tags to get.
+ * @param maxEndTime The max end time for the tags to get.
+ * @return The tags or empty vector, if no tags were found.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTagsByMinStartMaxEndTime(
+ int iEpgID, const CDateTime& minStartTime, const CDateTime& maxEndTime);
+
+ /*!
+ * @brief Get all EPG tags matching the given EPG id, min end time and max start time.
+ * @param iEpgID The ID of the EPG for the tags to get.
+ * @param minEndTime The min end time for the tags to get.
+ * @param maxStartTime The max start time for the tags to get.
+ * @return The tags or empty vector, if no tags were found.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTagsByMinEndMaxStartTime(
+ int iEpgID, const CDateTime& minEndTime, const CDateTime& maxStartTime);
+
+ /*!
+ * @brief Write the query to delete all EPG tags in range of given EPG id, min end time and max
+ * start time to db query queue. .
+ * @param iEpgID The ID of the EPG for the tags to delete.
+ * @param minEndTime The min end time for the tags to delete.
+ * @param maxStartTime The max start time for the tags to delete.
+ * @return True if it was removed or queued successfully, false otherwise.
+ */
+ bool QueueDeleteEpgTagsByMinEndMaxStartTimeQuery(int iEpgID,
+ const CDateTime& minEndTime,
+ const CDateTime& maxStartTime);
+
+ /*!
+ * @brief Get the last stored EPG scan time.
+ * @param iEpgId The table to update the time for. Use 0 for a global value.
+ * @param lastScan The last scan time or -1 if it wasn't found.
+ * @return True if the time was fetched successfully, false otherwise.
+ */
+ bool GetLastEpgScanTime(int iEpgId, CDateTime* lastScan);
+
+ /*!
+ * @brief Write the query to update the last scan time for the given EPG to db query queue.
+ * @param iEpgId The table to update the time for.
+ * @param lastScanTime The time to write to the database.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistLastEpgScanTimeQuery(int iEpgId, const CDateTime& lastScanTime);
+
+ /*!
+ * @brief Write the query to delete the last scan time for the given EPG to db query queue.
+ * @param iEpgId The table to delete the time for.
+ * @return True on success, false otherwise.
+ */
+ bool QueueDeleteLastEpgScanTimeQuery(const CPVREpg& table);
+
+ /*!
+ * @brief Persist an EPG table. It's entries are not persisted.
+ * @param epg The table to persist.
+ * @param bQueueWrite If true, don't execute the query immediately but queue it.
+ * @return The database ID of this entry or 0 if bQueueWrite is false and the query was queued.
+ */
+ int Persist(const CPVREpg& epg, bool bQueueWrite);
+
+ /*!
+ * @brief Erase all EPG tags with the given epg ID and an end time less than the given time.
+ * @param iEpgId The ID of the EPG.
+ * @param maxEndTime The maximum allowed end time.
+ * @return True if the entries were removed successfully, false otherwise.
+ */
+ bool DeleteEpgTags(int iEpgId, const CDateTime& maxEndTime);
+
+ /*!
+ * @brief Erase all EPG tags with the given epg ID.
+ * @param iEpgId The ID of the EPG.
+ * @return True if the entries were removed successfully, false otherwise.
+ */
+ bool DeleteEpgTags(int iEpgId);
+
+ /*!
+ * @brief Queue the erase all EPG tags with the given epg ID.
+ * @param iEpgId The ID of the EPG.
+ * @return True if the entries were queued successfully, false otherwise.
+ */
+ bool QueueDeleteEpgTags(int iEpgId);
+
+ /*!
+ * @brief Write the query to persist the given EPG tag to db query queue.
+ * @param tag The tag to persist.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistQuery(const CPVREpgInfoTag& tag);
+
+ /*!
+ * @return Last EPG id in the database
+ */
+ int GetLastEPGId();
+
+ //@}
+
+ /*! @name EPG searches methods */
+ //@{
+
+ /*!
+ * @brief Get all saved searches from the database.
+ * @param bRadio Whether to fetch saved searches for radio or TV.
+ * @return The searches.
+ */
+ std::vector<std::shared_ptr<CPVREpgSearchFilter>> GetSavedSearches(bool bRadio);
+
+ /*!
+ * @brief Get the saved search matching the given id.
+ * @param bRadio Whether to fetch a TV or radio saved search.
+ * @param iId The id.
+ * @return The saved search or nullptr if not found.
+ */
+ std::shared_ptr<CPVREpgSearchFilter> GetSavedSearchById(bool bRadio, int iId);
+
+ /*!
+ * @brief Persist a search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool Persist(CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Update time last executed for the given search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateSavedSearchLastExecuted(const CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Delete a saved search.
+ * @param epgSearch The search.
+ * @return True on success, false otherwise.
+ */
+ bool Delete(const CPVREpgSearchFilter& epgSearch);
+
+ /*!
+ * @brief Delete all saved searches.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteSavedSearches();
+
+ //@}
+
+ private:
+ /*!
+ * @brief Create the EPG database tables.
+ */
+ void CreateTables() override;
+
+ /*!
+ * @brief Create the EPG database analytics.
+ */
+ void CreateAnalytics() override;
+
+ /*!
+ * @brief Update an old version of the database.
+ * @param version The version to update the database from.
+ */
+ void UpdateTables(int version) override;
+
+ int GetMinSchemaVersion() const override { return 4; }
+
+ std::shared_ptr<CPVREpgInfoTag> CreateEpgTag(const std::unique_ptr<dbiplus::Dataset>& pDS);
+
+ std::shared_ptr<CPVREpgSearchFilter> CreateEpgSearchFilter(
+ bool bRadio, const std::unique_ptr<dbiplus::Dataset>& pDS);
+
+ CCriticalSection m_critSection;
+ };
+}
diff --git a/xbmc/pvr/epg/EpgInfoTag.cpp b/xbmc/pvr/epg/EpgInfoTag.cpp
new file mode 100644
index 0000000..8b0930e
--- /dev/null
+++ b/xbmc/pvr/epg/EpgInfoTag.cpp
@@ -0,0 +1,681 @@
+/*
+ * 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 "EpgInfoTag.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVREpgInfoTag::IMAGE_OWNER_PATTERN = "epgtag_{}";
+
+CPVREpgInfoTag::CPVREpgInfoTag(int iEpgID, const std::string& iconPath)
+ : m_iUniqueBroadcastID(EPG_TAG_INVALID_UID),
+ m_iconPath(iconPath, StringUtils::Format(IMAGE_OWNER_PATTERN, iEpgID)),
+ m_iFlags(EPG_TAG_FLAG_UNDEFINED),
+ m_channelData(new CPVREpgChannelData),
+ m_iEpgID(iEpgID)
+{
+}
+
+CPVREpgInfoTag::CPVREpgInfoTag(const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID,
+ const CDateTime& start,
+ const CDateTime& end,
+ bool bIsGapTag)
+ : m_iUniqueBroadcastID(EPG_TAG_INVALID_UID),
+ m_iconPath(StringUtils::Format(IMAGE_OWNER_PATTERN, iEpgID)),
+ m_iFlags(EPG_TAG_FLAG_UNDEFINED),
+ m_bIsGapTag(bIsGapTag),
+ m_iEpgID(iEpgID)
+{
+ if (channelData)
+ m_channelData = channelData;
+ else
+ m_channelData = std::make_shared<CPVREpgChannelData>();
+
+ const CDateTimeSpan correction(
+ 0, 0, 0, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection);
+ m_startTime = start + correction;
+ m_endTime = end + correction;
+}
+
+CPVREpgInfoTag::CPVREpgInfoTag(const EPG_TAG& data,
+ int iClientId,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID)
+ : m_iGenreType(data.iGenreType),
+ m_iGenreSubType(data.iGenreSubType),
+ m_iParentalRating(data.iParentalRating),
+ m_iStarRating(data.iStarRating),
+ m_iSeriesNumber(data.iSeriesNumber),
+ m_iEpisodeNumber(data.iEpisodeNumber),
+ m_iEpisodePart(data.iEpisodePartNumber),
+ m_iUniqueBroadcastID(data.iUniqueBroadcastId),
+ m_iYear(data.iYear),
+ m_iconPath(data.strIconPath ? data.strIconPath : "",
+ StringUtils::Format(IMAGE_OWNER_PATTERN, iEpgID)),
+ m_startTime(
+ data.startTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection),
+ m_endTime(data.endTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection),
+ m_iFlags(data.iFlags),
+ m_iEpgID(iEpgID)
+{
+ // strFirstAired is optional, so check if supported before assigning it
+ if (data.strFirstAired && strlen(data.strFirstAired) > 0)
+ m_firstAired.SetFromW3CDate(data.strFirstAired);
+
+ if (channelData)
+ {
+ m_channelData = channelData;
+
+ if (m_channelData->ClientId() != iClientId)
+ CLog::LogF(LOGERROR, "Client id mismatch (channel: {}, epg: {})!", m_channelData->ClientId(),
+ iClientId);
+ if (m_channelData->UniqueClientChannelId() != static_cast<int>(data.iUniqueChannelId))
+ CLog::LogF(LOGERROR, "Channel uid mismatch (channel: {}, epg: {})!",
+ m_channelData->UniqueClientChannelId(), data.iUniqueChannelId);
+ }
+ else
+ {
+ // provide minimalistic channel data until we get fully initialized later
+ m_channelData = std::make_shared<CPVREpgChannelData>(iClientId, data.iUniqueChannelId);
+ }
+
+ // explicit NULL check, because there is no implicit NULL constructor for std::string
+ if (data.strTitle)
+ m_strTitle = data.strTitle;
+ if (data.strGenreDescription)
+ m_strGenreDescription = data.strGenreDescription;
+ if (data.strPlotOutline)
+ m_strPlotOutline = data.strPlotOutline;
+ if (data.strPlot)
+ m_strPlot = data.strPlot;
+ if (data.strOriginalTitle)
+ m_strOriginalTitle = data.strOriginalTitle;
+ if (data.strCast)
+ m_cast = Tokenize(data.strCast);
+ if (data.strDirector)
+ m_directors = Tokenize(data.strDirector);
+ if (data.strWriter)
+ m_writers = Tokenize(data.strWriter);
+ if (data.strIMDBNumber)
+ m_strIMDBNumber = data.strIMDBNumber;
+ if (data.strEpisodeName)
+ m_strEpisodeName = data.strEpisodeName;
+ if (data.strSeriesLink)
+ m_strSeriesLink = data.strSeriesLink;
+ if (data.strParentalRatingCode)
+ m_strParentalRatingCode = data.strParentalRatingCode;
+}
+
+void CPVREpgInfoTag::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (data)
+ m_channelData = data;
+ else
+ m_channelData.reset(new CPVREpgChannelData);
+}
+
+bool CPVREpgInfoTag::operator==(const CPVREpgInfoTag& right) const
+{
+ if (this == &right)
+ return true;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return (m_iUniqueBroadcastID == right.m_iUniqueBroadcastID && m_channelData &&
+ right.m_channelData &&
+ m_channelData->UniqueClientChannelId() == right.m_channelData->UniqueClientChannelId() &&
+ m_channelData->ClientId() == right.m_channelData->ClientId());
+}
+
+bool CPVREpgInfoTag::operator!=(const CPVREpgInfoTag& right) const
+{
+ if (this == &right)
+ return false;
+
+ return !(*this == right);
+}
+
+void CPVREpgInfoTag::Serialize(CVariant& value) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ value["broadcastid"] = m_iDatabaseID; // Use DB id here as it is unique across PVR clients
+ value["channeluid"] = m_channelData->UniqueClientChannelId();
+ value["parentalrating"] = m_iParentalRating;
+ value["parentalratingcode"] = m_strParentalRatingCode;
+ value["rating"] = m_iStarRating;
+ value["title"] = m_strTitle;
+ value["plotoutline"] = m_strPlotOutline;
+ value["plot"] = m_strPlot;
+ value["originaltitle"] = m_strOriginalTitle;
+ value["thumbnail"] = ClientIconPath();
+ value["cast"] = DeTokenize(m_cast);
+ value["director"] = DeTokenize(m_directors);
+ value["writer"] = DeTokenize(m_writers);
+ value["year"] = m_iYear;
+ value["imdbnumber"] = m_strIMDBNumber;
+ value["genre"] = Genre();
+ value["filenameandpath"] = Path();
+ value["starttime"] = m_startTime.IsValid() ? m_startTime.GetAsDBDateTime() : StringUtils::Empty;
+ value["endtime"] = m_endTime.IsValid() ? m_endTime.GetAsDBDateTime() : StringUtils::Empty;
+ value["runtime"] = GetDuration() / 60;
+ value["firstaired"] = m_firstAired.IsValid() ? m_firstAired.GetAsDBDate() : StringUtils::Empty;
+ value["progress"] = Progress();
+ value["progresspercentage"] = ProgressPercentage();
+ value["episodename"] = m_strEpisodeName;
+ value["episodenum"] = m_iEpisodeNumber;
+ value["episodepart"] = m_iEpisodePart;
+ value["seasonnum"] = m_iSeriesNumber;
+ value["isactive"] = IsActive();
+ value["wasactive"] = WasActive();
+ value["isseries"] = IsSeries();
+ value["serieslink"] = m_strSeriesLink;
+ value["clientid"] = m_channelData->ClientId();
+}
+
+int CPVREpgInfoTag::ClientID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->ClientId();
+}
+
+CDateTime CPVREpgInfoTag::GetCurrentPlayingTime() const
+{
+ return CServiceBroker::GetPVRManager().PlaybackState()->GetChannelPlaybackTime(ClientID(),
+ UniqueChannelID());
+}
+
+bool CPVREpgInfoTag::IsActive() const
+{
+ CDateTime now = GetCurrentPlayingTime();
+ return (m_startTime <= now && m_endTime > now);
+}
+
+bool CPVREpgInfoTag::WasActive() const
+{
+ CDateTime now = GetCurrentPlayingTime();
+ return (m_endTime < now);
+}
+
+bool CPVREpgInfoTag::IsUpcoming() const
+{
+ CDateTime now = GetCurrentPlayingTime();
+ return (m_startTime > now);
+}
+
+float CPVREpgInfoTag::ProgressPercentage() const
+{
+ float fReturn = 0.0f;
+
+ time_t currentTime, startTime, endTime;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(currentTime);
+ m_startTime.GetAsTime(startTime);
+ m_endTime.GetAsTime(endTime);
+ int iDuration = endTime - startTime > 0 ? endTime - startTime : 3600;
+
+ if (currentTime >= startTime && currentTime <= endTime)
+ fReturn = static_cast<float>(currentTime - startTime) * 100.0f / iDuration;
+ else if (currentTime > endTime)
+ fReturn = 100.0f;
+
+ return fReturn;
+}
+
+int CPVREpgInfoTag::Progress() const
+{
+ time_t currentTime, startTime;
+ CDateTime::GetCurrentDateTime().GetAsUTCDateTime().GetAsTime(currentTime);
+ m_startTime.GetAsTime(startTime);
+ int iDuration = currentTime - startTime;
+
+ if (iDuration <= 0)
+ return 0;
+
+ return iDuration;
+}
+
+void CPVREpgInfoTag::SetUniqueBroadcastID(unsigned int iUniqueBroadcastID)
+{
+ m_iUniqueBroadcastID = iUniqueBroadcastID;
+}
+
+unsigned int CPVREpgInfoTag::UniqueBroadcastID() const
+{
+ return m_iUniqueBroadcastID;
+}
+
+int CPVREpgInfoTag::DatabaseID() const
+{
+ return m_iDatabaseID;
+}
+
+int CPVREpgInfoTag::UniqueChannelID() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->UniqueClientChannelId();
+}
+
+std::string CPVREpgInfoTag::ChannelIconPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->ChannelIconPath();
+}
+
+CDateTime CPVREpgInfoTag::StartAsUTC() const
+{
+ return m_startTime;
+}
+
+CDateTime CPVREpgInfoTag::StartAsLocalTime() const
+{
+ CDateTime retVal;
+ retVal.SetFromUTCDateTime(m_startTime);
+ return retVal;
+}
+
+CDateTime CPVREpgInfoTag::EndAsUTC() const
+{
+ return m_endTime;
+}
+
+CDateTime CPVREpgInfoTag::EndAsLocalTime() const
+{
+ CDateTime retVal;
+ retVal.SetFromUTCDateTime(m_endTime);
+ return retVal;
+}
+
+void CPVREpgInfoTag::SetEndFromUTC(const CDateTime& end)
+{
+ m_endTime = end;
+}
+
+int CPVREpgInfoTag::GetDuration() const
+{
+ time_t start, end;
+ m_startTime.GetAsTime(start);
+ m_endTime.GetAsTime(end);
+ return end - start > 0 ? end - start : 3600;
+}
+
+std::string CPVREpgInfoTag::Title() const
+{
+ return m_strTitle;
+}
+
+std::string CPVREpgInfoTag::PlotOutline() const
+{
+ return m_strPlotOutline;
+}
+
+std::string CPVREpgInfoTag::Plot() const
+{
+ return m_strPlot;
+}
+
+std::string CPVREpgInfoTag::OriginalTitle() const
+{
+ return m_strOriginalTitle;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Cast() const
+{
+ return m_cast;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Directors() const
+{
+ return m_directors;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Writers() const
+{
+ return m_writers;
+}
+
+const std::string CPVREpgInfoTag::GetCastLabel() const
+{
+ // Note: see CVideoInfoTag::GetCast for reference implementation.
+ std::string strLabel;
+ for (const auto& castEntry : m_cast)
+ strLabel += StringUtils::Format("{}\n", castEntry);
+
+ return StringUtils::TrimRight(strLabel, "\n");
+}
+
+const std::string CPVREpgInfoTag::GetDirectorsLabel() const
+{
+ return StringUtils::Join(
+ m_directors,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+}
+
+const std::string CPVREpgInfoTag::GetWritersLabel() const
+{
+ return StringUtils::Join(
+ m_writers,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+}
+
+const std::string CPVREpgInfoTag::GetGenresLabel() const
+{
+ return StringUtils::Join(
+ Genre(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+}
+
+int CPVREpgInfoTag::Year() const
+{
+ return m_iYear;
+}
+
+std::string CPVREpgInfoTag::IMDBNumber() const
+{
+ return m_strIMDBNumber;
+}
+
+int CPVREpgInfoTag::GenreType() const
+{
+ return m_iGenreType;
+}
+
+int CPVREpgInfoTag::GenreSubType() const
+{
+ return m_iGenreSubType;
+}
+
+std::string CPVREpgInfoTag::GenreDescription() const
+{
+ return m_strGenreDescription;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Genre() const
+{
+ if (m_genre.empty())
+ {
+ if ((m_iGenreType == EPG_GENRE_USE_STRING || m_iGenreSubType == EPG_GENRE_USE_STRING) &&
+ !m_strGenreDescription.empty())
+ {
+ // Type and sub type are both not given. No EPG color coding possible unless sub type is
+ // used to specify EPG_GENRE_USE_STRING leaving type available for genre category, use the
+ // provided genre description for the text.
+ m_genre = Tokenize(m_strGenreDescription);
+ }
+
+ if (m_genre.empty())
+ {
+ // Determine the genre from the type and subtype IDs.
+ m_genre = Tokenize(CPVREpg::ConvertGenreIdToString(m_iGenreType, m_iGenreSubType));
+ }
+ }
+ return m_genre;
+}
+
+CDateTime CPVREpgInfoTag::FirstAired() const
+{
+ return m_firstAired;
+}
+
+int CPVREpgInfoTag::ParentalRating() const
+{
+ return m_iParentalRating;
+}
+
+std::string CPVREpgInfoTag::ParentalRatingCode() const
+{
+ return m_strParentalRatingCode;
+}
+
+int CPVREpgInfoTag::StarRating() const
+{
+ return m_iStarRating;
+}
+
+int CPVREpgInfoTag::SeriesNumber() const
+{
+ return m_iSeriesNumber;
+}
+
+std::string CPVREpgInfoTag::SeriesLink() const
+{
+ return m_strSeriesLink;
+}
+
+int CPVREpgInfoTag::EpisodeNumber() const
+{
+ return m_iEpisodeNumber;
+}
+
+int CPVREpgInfoTag::EpisodePart() const
+{
+ return m_iEpisodePart;
+}
+
+std::string CPVREpgInfoTag::EpisodeName() const
+{
+ return m_strEpisodeName;
+}
+
+std::string CPVREpgInfoTag::IconPath() const
+{
+ return m_iconPath.GetLocalImage();
+}
+
+std::string CPVREpgInfoTag::ClientIconPath() const
+{
+ return m_iconPath.GetClientImage();
+}
+
+std::string CPVREpgInfoTag::Path() const
+{
+ return StringUtils::Format("pvr://guide/{:04}/{}.epg", EpgID(), m_startTime.GetAsDBDateTime());
+}
+
+bool CPVREpgInfoTag::Update(const CPVREpgInfoTag& tag, bool bUpdateBroadcastId /* = true */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ bool bChanged =
+ (m_strTitle != tag.m_strTitle || m_strPlotOutline != tag.m_strPlotOutline ||
+ m_strPlot != tag.m_strPlot || m_strOriginalTitle != tag.m_strOriginalTitle ||
+ m_cast != tag.m_cast || m_directors != tag.m_directors || m_writers != tag.m_writers ||
+ m_iYear != tag.m_iYear || m_strIMDBNumber != tag.m_strIMDBNumber ||
+ m_startTime != tag.m_startTime || m_endTime != tag.m_endTime ||
+ m_iGenreType != tag.m_iGenreType || m_iGenreSubType != tag.m_iGenreSubType ||
+ m_strGenreDescription != tag.m_strGenreDescription || m_firstAired != tag.m_firstAired ||
+ m_iParentalRating != tag.m_iParentalRating ||
+ m_strParentalRatingCode != tag.m_strParentalRatingCode ||
+ m_iStarRating != tag.m_iStarRating || m_iEpisodeNumber != tag.m_iEpisodeNumber ||
+ m_iEpisodePart != tag.m_iEpisodePart || m_iSeriesNumber != tag.m_iSeriesNumber ||
+ m_strEpisodeName != tag.m_strEpisodeName ||
+ m_iUniqueBroadcastID != tag.m_iUniqueBroadcastID || m_iEpgID != tag.m_iEpgID ||
+ m_genre != tag.m_genre || m_iconPath != tag.m_iconPath || m_iFlags != tag.m_iFlags ||
+ m_strSeriesLink != tag.m_strSeriesLink || m_channelData != tag.m_channelData);
+
+ if (bUpdateBroadcastId)
+ bChanged |= (m_iDatabaseID != tag.m_iDatabaseID);
+
+ if (bChanged)
+ {
+ if (bUpdateBroadcastId)
+ m_iDatabaseID = tag.m_iDatabaseID;
+
+ m_strTitle = tag.m_strTitle;
+ m_strPlotOutline = tag.m_strPlotOutline;
+ m_strPlot = tag.m_strPlot;
+ m_strOriginalTitle = tag.m_strOriginalTitle;
+ m_cast = tag.m_cast;
+ m_directors = tag.m_directors;
+ m_writers = tag.m_writers;
+ m_iYear = tag.m_iYear;
+ m_strIMDBNumber = tag.m_strIMDBNumber;
+ m_startTime = tag.m_startTime;
+ m_endTime = tag.m_endTime;
+ m_iGenreType = tag.m_iGenreType;
+ m_iGenreSubType = tag.m_iGenreSubType;
+ m_strGenreDescription = tag.m_strGenreDescription;
+ m_genre = tag.m_genre;
+ m_iEpgID = tag.m_iEpgID;
+ m_iFlags = tag.m_iFlags;
+ m_strSeriesLink = tag.m_strSeriesLink;
+ m_firstAired = tag.m_firstAired;
+ m_iParentalRating = tag.m_iParentalRating;
+ m_strParentalRatingCode = tag.m_strParentalRatingCode;
+ m_iStarRating = tag.m_iStarRating;
+ m_iEpisodeNumber = tag.m_iEpisodeNumber;
+ m_iEpisodePart = tag.m_iEpisodePart;
+ m_iSeriesNumber = tag.m_iSeriesNumber;
+ m_strEpisodeName = tag.m_strEpisodeName;
+ m_iUniqueBroadcastID = tag.m_iUniqueBroadcastID;
+ m_iconPath = tag.m_iconPath;
+ m_channelData = tag.m_channelData;
+ }
+
+ return bChanged;
+}
+
+bool CPVREpgInfoTag::QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database)
+{
+ if (!database)
+ {
+ CLog::LogF(LOGERROR, "Could not open the EPG database");
+ return false;
+ }
+
+ return database->QueuePersistQuery(*this);
+}
+
+std::vector<PVR_EDL_ENTRY> CPVREpgInfoTag::GetEdl() const
+{
+ std::vector<PVR_EDL_ENTRY> edls;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+
+ if (client && client->GetClientCapabilities().SupportsEpgTagEdl())
+ client->GetEpgTagEdl(shared_from_this(), edls);
+
+ return edls;
+}
+
+int CPVREpgInfoTag::EpgID() const
+{
+ return m_iEpgID;
+}
+
+void CPVREpgInfoTag::SetEpgID(int iEpgID)
+{
+ m_iEpgID = iEpgID;
+ m_iconPath.SetOwner(StringUtils::Format(IMAGE_OWNER_PATTERN, m_iEpgID));
+}
+
+bool CPVREpgInfoTag::IsRecordable() const
+{
+ bool bIsRecordable = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+ if (!client || (client->IsRecordable(shared_from_this(), bIsRecordable) != PVR_ERROR_NO_ERROR))
+ {
+ // event end time based fallback
+ bIsRecordable = EndAsLocalTime() > CDateTime::GetCurrentDateTime();
+ }
+ return bIsRecordable;
+}
+
+bool CPVREpgInfoTag::IsPlayable() const
+{
+ bool bIsPlayable = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_channelData->ClientId());
+ if (!client || (client->IsPlayable(shared_from_this(), bIsPlayable) != PVR_ERROR_NO_ERROR))
+ {
+ // fallback
+ bIsPlayable = false;
+ }
+ return bIsPlayable;
+}
+
+bool CPVREpgInfoTag::IsSeries() const
+{
+ if ((m_iFlags & EPG_TAG_FLAG_IS_SERIES) > 0 || SeriesNumber() >= 0 || EpisodeNumber() >= 0 ||
+ EpisodePart() >= 0)
+ return true;
+ else
+ return false;
+}
+
+bool CPVREpgInfoTag::IsRadio() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->IsRadio();
+}
+
+bool CPVREpgInfoTag::IsParentalLocked() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelData->IsLocked();
+}
+
+bool CPVREpgInfoTag::IsGapTag() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bIsGapTag;
+}
+
+bool CPVREpgInfoTag::IsNew() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_NEW) > 0;
+}
+
+bool CPVREpgInfoTag::IsPremiere() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_PREMIERE) > 0;
+}
+
+bool CPVREpgInfoTag::IsFinale() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_FINALE) > 0;
+}
+
+bool CPVREpgInfoTag::IsLive() const
+{
+ return (m_iFlags & EPG_TAG_FLAG_IS_LIVE) > 0;
+}
+
+const std::vector<std::string> CPVREpgInfoTag::Tokenize(const std::string& str)
+{
+ return StringUtils::Split(str, EPG_STRING_TOKEN_SEPARATOR);
+}
+
+const std::string CPVREpgInfoTag::DeTokenize(const std::vector<std::string>& tokens)
+{
+ return StringUtils::Join(tokens, EPG_STRING_TOKEN_SEPARATOR);
+}
diff --git a/xbmc/pvr/epg/EpgInfoTag.h b/xbmc/pvr/epg/EpgInfoTag.h
new file mode 100644
index 0000000..89f2e0c
--- /dev/null
+++ b/xbmc/pvr/epg/EpgInfoTag.h
@@ -0,0 +1,513 @@
+/*
+ * 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 "XBDateTime.h"
+#include "pvr/PVRCachedImage.h"
+#include "threads/CriticalSection.h"
+#include "utils/ISerializable.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+struct EPG_TAG;
+struct PVR_EDL_ENTRY;
+
+namespace PVR
+{
+class CPVREpgChannelData;
+class CPVREpgDatabase;
+
+class CPVREpgInfoTag final : public ISerializable,
+ public std::enable_shared_from_this<CPVREpgInfoTag>
+{
+ friend class CPVREpgDatabase;
+
+public:
+ static const std::string IMAGE_OWNER_PATTERN;
+
+ /*!
+ * @brief Create a new EPG infotag.
+ * @param data The tag's data.
+ * @param iClientId The client id.
+ * @param channelData The channel data.
+ * @param iEpgId The id of the EPG this tag belongs to.
+ */
+ CPVREpgInfoTag(const EPG_TAG& data,
+ int iClientId,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID);
+
+ /*!
+ * @brief Create a new EPG infotag.
+ * @param channelData The channel data.
+ * @param iEpgId The id of the EPG this tag belongs to.
+ * @param start The start time of the event
+ * @param end The end time of the event
+ * @param bIsGapTagTrue if this is a "gap" tag, false if this is a real EPG event
+ */
+ CPVREpgInfoTag(const std::shared_ptr<CPVREpgChannelData>& channelData,
+ int iEpgID,
+ const CDateTime& start,
+ const CDateTime& end,
+ bool bIsGapTag);
+
+ /*!
+ * @brief Set data for the channel linked to this EPG infotag.
+ * @param data The channel data.
+ */
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ bool operator==(const CPVREpgInfoTag& right) const;
+ bool operator!=(const CPVREpgInfoTag& right) const;
+
+ // ISerializable implementation
+ void Serialize(CVariant& value) const override;
+
+ /*!
+ * @brief Get the identifier of the client that serves this event.
+ * @return The identifier.
+ */
+ int ClientID() const;
+
+ /*!
+ * @brief Check if this event is currently active.
+ * @return True if it's active, false otherwise.
+ */
+ bool IsActive() const;
+
+ /*!
+ * @brief Check if this event is in the past.
+ * @return True when this event has already passed, false otherwise.
+ */
+ bool WasActive() const;
+
+ /*!
+ * @brief Check if this event is in the future.
+ * @return True when this event is an upcoming event, false otherwise.
+ */
+ bool IsUpcoming() const;
+
+ /*!
+ * @brief Get the progress of this tag in percent.
+ * @return The current progress of this tag.
+ */
+ float ProgressPercentage() const;
+
+ /*!
+ * @brief Get the progress of this tag in seconds.
+ * @return The current progress of this tag in seconds.
+ */
+ int Progress() const;
+
+ /*!
+ * @brief Get EPG ID of this tag.
+ * @return The epg ID.
+ */
+ int EpgID() const;
+
+ /*!
+ * @brief Sets the EPG id for this event.
+ * @param iEpgID The EPG id.
+ */
+ void SetEpgID(int iEpgID);
+
+ /*!
+ * @brief Change the unique broadcast ID of this event.
+ * @param iUniqueBroadcastId The new unique broadcast ID.
+ */
+ void SetUniqueBroadcastID(unsigned int iUniqueBroadcastID);
+
+ /*!
+ * @brief Get the unique broadcast ID.
+ * @return The unique broadcast ID.
+ */
+ unsigned int UniqueBroadcastID() const;
+
+ /*!
+ * @brief Get the event's database ID.
+ * @return The database ID.
+ */
+ int DatabaseID() const;
+
+ /*!
+ * @brief Get the unique ID of the channel associated with this event.
+ * @return The unique channel ID.
+ */
+ int UniqueChannelID() const;
+
+ /*!
+ * @brief Get the path for the icon of the channel associated with this event.
+ * @return The channel icon path.
+ */
+ std::string ChannelIconPath() const;
+
+ /*!
+ * @brief Get the event's start time.
+ * @return The start time in UTC.
+ */
+ CDateTime StartAsUTC() const;
+
+ /*!
+ * @brief Get the event's start time.
+ * @return The start time as local time.
+ */
+ CDateTime StartAsLocalTime() const;
+
+ /*!
+ * @brief Get the event's end time.
+ * @return The end time in UTC.
+ */
+ CDateTime EndAsUTC() const;
+
+ /*!
+ * @brief Get the event's end time.
+ * @return The end time as local time.
+ */
+ CDateTime EndAsLocalTime() const;
+
+ /*!
+ * @brief Change the event's end time.
+ * @param end The new end time.
+ */
+ void SetEndFromUTC(const CDateTime& end);
+
+ /*!
+ * @brief Get the duration of this event in seconds.
+ * @return The duration.
+ */
+ int GetDuration() const;
+
+ /*!
+ * @brief Get the title of this event.
+ * @return The title.
+ */
+ std::string Title() const;
+
+ /*!
+ * @brief Get the plot outline of this event.
+ * @return The plot outline.
+ */
+ std::string PlotOutline() const;
+
+ /*!
+ * @brief Get the plot of this event.
+ * @return The plot.
+ */
+ std::string Plot() const;
+
+ /*!
+ * @brief Get the original title of this event.
+ * @return The original title.
+ */
+ std::string OriginalTitle() const;
+
+ /*!
+ * @brief Get the cast of this event.
+ * @return The cast.
+ */
+ const std::vector<std::string> Cast() const;
+
+ /*!
+ * @brief Get the director(s) of this event.
+ * @return The director(s).
+ */
+ const std::vector<std::string> Directors() const;
+
+ /*!
+ * @brief Get the writer(s) of this event.
+ * @return The writer(s).
+ */
+ const std::vector<std::string> Writers() const;
+
+ /*!
+ * @brief Get the cast of this event as formatted string.
+ * @return The cast label.
+ */
+ const std::string GetCastLabel() const;
+
+ /*!
+ * @brief Get the director(s) of this event as formatted string.
+ * @return The directors label.
+ */
+ const std::string GetDirectorsLabel() const;
+
+ /*!
+ * @brief Get the writer(s) of this event as formatted string.
+ * @return The writers label.
+ */
+ const std::string GetWritersLabel() const;
+
+ /*!
+ * @brief Get the genre(s) of this event as formatted string.
+ * @return The genres label.
+ */
+ const std::string GetGenresLabel() const;
+
+ /*!
+ * @brief Get the year of this event.
+ * @return The year.
+ */
+ int Year() const;
+
+ /*!
+ * @brief Get the imdbnumber of this event.
+ * @return The imdbnumber.
+ */
+ std::string IMDBNumber() const;
+
+ /*!
+ * @brief Get the genre type ID of this event.
+ * @return The genre type ID.
+ */
+ int GenreType() const;
+
+ /*!
+ * @brief Get the genre subtype ID of this event.
+ * @return The genre subtype ID.
+ */
+ int GenreSubType() const;
+
+ /*!
+ * @brief Get the genre description of this event.
+ * @return The genre.
+ */
+ std::string GenreDescription() const;
+
+ /*!
+ * @brief Get the genre as human readable string.
+ * @return The genre.
+ */
+ const std::vector<std::string> Genre() const;
+
+ /*!
+ * @brief Get the first air date of this event.
+ * @return The first air date.
+ */
+ CDateTime FirstAired() const;
+
+ /*!
+ * @brief Get the parental rating of this event.
+ * @return The parental rating.
+ */
+ int ParentalRating() const;
+
+ /*!
+ * @brief Get the parental rating code of this event.
+ * @return The parental rating code.
+ */
+ std::string ParentalRatingCode() const;
+
+ /*!
+ * @brief Get the star rating of this event.
+ * @return The star rating.
+ */
+ int StarRating() const;
+
+ /*!
+ * @brief The series number of this event.
+ * @return The series number.
+ */
+ int SeriesNumber() const;
+
+ /*!
+ * @brief The series link for this event.
+ * @return The series link or empty string, if not available.
+ */
+ std::string SeriesLink() const;
+
+ /*!
+ * @brief The episode number of this event.
+ * @return The episode number.
+ */
+ int EpisodeNumber() const;
+
+ /*!
+ * @brief The episode part number of this event.
+ * @return The episode part number.
+ */
+ int EpisodePart() const;
+
+ /*!
+ * @brief The episode name of this event.
+ * @return The episode name.
+ */
+ std::string EpisodeName() const;
+
+ /*!
+ * @brief Get the path to the icon for this event used by Kodi.
+ * @return The path to the icon
+ */
+ std::string IconPath() const;
+
+ /*!
+ * @brief Get the path to the icon for this event as given by the client.
+ * @return The path to the icon
+ */
+ std::string ClientIconPath() const;
+
+ /*!
+ * @brief The path to this event.
+ * @return The path.
+ */
+ std::string Path() const;
+
+ /*!
+ * @brief Check if this event can be recorded.
+ * @return True if it can be recorded, false otherwise.
+ */
+ bool IsRecordable() const;
+
+ /*!
+ * @brief Check if this event can be played.
+ * @return True if it can be played, false otherwise.
+ */
+ bool IsPlayable() const;
+
+ /*!
+ * @brief Write query to persist this tag in the query queue of the given database.
+ * @param database The database.
+ * @return True on success, false otherwise.
+ */
+ bool QueuePersistQuery(const std::shared_ptr<CPVREpgDatabase>& database);
+
+ /*!
+ * @brief Update the information in this tag with the info in the given tag.
+ * @param tag The new info.
+ * @param bUpdateBroadcastId If set to false, the tag BroadcastId (locally unique) will not be
+ * checked/updated
+ * @return True if something changed, false otherwise.
+ */
+ bool Update(const CPVREpgInfoTag& tag, bool bUpdateBroadcastId = true);
+
+ /*!
+ * @brief Retrieve the edit decision list (EDL) of an EPG tag.
+ * @return The edit decision list (empty on error)
+ */
+ std::vector<PVR_EDL_ENTRY> GetEdl() const;
+
+ /*!
+ * @brief Check whether this tag has any series attributes.
+ * @return True if this tag has any series attributes, false otherwise
+ */
+ bool IsSeries() const;
+
+ /*!
+ * @brief Check whether this tag is associated with a radion or TV channel.
+ * @return True if this tag is associated with a radio channel, false otherwise.
+ */
+ bool IsRadio() const;
+
+ /*!
+ * @brief Check whether this event is parental locked.
+ * @return True if whether this event is parental locked, false otherwise.
+ */
+ bool IsParentalLocked() const;
+
+ /*!
+ * @brief Check whether this event is a real event or a gap in the EPG timeline.
+ * @return True if this event is a gap, false otherwise.
+ */
+ bool IsGapTag() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as new.
+ * @return True if this tag will be flagged as new, false otherwise
+ */
+ bool IsNew() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as a premiere.
+ * @return True if this tag will be flagged as a premiere, false otherwise
+ */
+ bool IsPremiere() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as a finale.
+ * @return True if this tag will be flagged as a finale, false otherwise
+ */
+ bool IsFinale() const;
+
+ /*!
+ * @brief Check whether this tag will be flagged as live.
+ * @return True if this tag will be flagged as live, false otherwise
+ */
+ bool IsLive() const;
+
+ /*!
+ * @brief Return the flags (EPG_TAG_FLAG_*) of this event as a bitfield.
+ * @return the flags.
+ */
+ unsigned int Flags() const { return m_iFlags; }
+
+ /*!
+ * @brief Split the given string into tokens. Interprets occurrences of EPG_STRING_TOKEN_SEPARATOR
+ * in the string as separator.
+ * @param str The string to tokenize.
+ * @return the tokens.
+ */
+ static const std::vector<std::string> Tokenize(const std::string& str);
+
+ /*!
+ * @brief Combine the given strings to a single string. Inserts EPG_STRING_TOKEN_SEPARATOR as
+ * separator.
+ * @param tokens The tokens.
+ * @return the combined string.
+ */
+ static const std::string DeTokenize(const std::vector<std::string>& tokens);
+
+private:
+ CPVREpgInfoTag(int iEpgID, const std::string& iconPath);
+
+ CPVREpgInfoTag() = delete;
+ CPVREpgInfoTag(const CPVREpgInfoTag& tag) = delete;
+ CPVREpgInfoTag& operator=(const CPVREpgInfoTag& other) = delete;
+
+ /*!
+ * @brief Get current time, taking timeshifting into account.
+ * @return The playing time.
+ */
+ CDateTime GetCurrentPlayingTime() const;
+
+ int m_iDatabaseID = -1; /*!< database ID */
+ int m_iGenreType = 0; /*!< genre type */
+ int m_iGenreSubType = 0; /*!< genre subtype */
+ std::string m_strGenreDescription; /*!< genre description */
+ int m_iParentalRating = 0; /*!< parental rating */
+ std::string m_strParentalRatingCode; /*!< parental rating code */
+ int m_iStarRating = 0; /*!< star rating */
+ int m_iSeriesNumber = -1; /*!< series number */
+ int m_iEpisodeNumber = -1; /*!< episode number */
+ int m_iEpisodePart = -1; /*!< episode part number */
+ unsigned int m_iUniqueBroadcastID = 0; /*!< unique broadcast ID */
+ std::string m_strTitle; /*!< title */
+ std::string m_strPlotOutline; /*!< plot outline */
+ std::string m_strPlot; /*!< plot */
+ std::string m_strOriginalTitle; /*!< original title */
+ std::vector<std::string> m_cast; /*!< cast */
+ std::vector<std::string> m_directors; /*!< director(s) */
+ std::vector<std::string> m_writers; /*!< writer(s) */
+ int m_iYear = 0; /*!< year */
+ std::string m_strIMDBNumber; /*!< imdb number */
+ mutable std::vector<std::string> m_genre; /*!< genre */
+ std::string m_strEpisodeName; /*!< episode name */
+ CPVRCachedImage m_iconPath; /*!< the path to the icon */
+ CDateTime m_startTime; /*!< event start time */
+ CDateTime m_endTime; /*!< event end time */
+ CDateTime m_firstAired; /*!< first airdate */
+ unsigned int m_iFlags = 0; /*!< the flags applicable to this EPG entry */
+ std::string m_strSeriesLink; /*!< series link */
+ bool m_bIsGapTag = false;
+
+ mutable CCriticalSection m_critSection;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ int m_iEpgID = -1;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgSearchData.h b/xbmc/pvr/epg/EpgSearchData.h
new file mode 100644
index 0000000..e5e6ef7
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchData.h
@@ -0,0 +1,44 @@
+/*
+ * 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 "XBDateTime.h"
+
+#include <string>
+
+namespace PVR
+{
+
+static constexpr int EPG_SEARCH_UNSET = -1;
+
+struct PVREpgSearchData
+{
+ std::string m_strSearchTerm; /*!< The term to search for */
+ bool m_bSearchInDescription = false; /*!< Search for strSearchTerm in the description too */
+ bool m_bIncludeUnknownGenres = false; /*!< Whether to include unknown genres */
+ int m_iGenreType = EPG_SEARCH_UNSET; /*!< The genre type for an entry */
+ bool m_bIgnoreFinishedBroadcasts; /*!< True to ignore finished broadcasts, false if not */
+ bool m_bIgnoreFutureBroadcasts; /*!< True to ignore future broadcasts, false if not */
+ CDateTime m_startDateTime; /*!< The minimum start time for an entry */
+ CDateTime m_endDateTime; /*!< The maximum end time for an entry */
+
+ void Reset()
+ {
+ m_strSearchTerm.clear();
+ m_bSearchInDescription = false;
+ m_bIncludeUnknownGenres = false;
+ m_iGenreType = EPG_SEARCH_UNSET;
+ m_bIgnoreFinishedBroadcasts = true;
+ m_bIgnoreFutureBroadcasts = false;
+ m_startDateTime.SetValid(false);
+ m_endDateTime.SetValid(false);
+ }
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgSearchFilter.cpp b/xbmc/pvr/epg/EpgSearchFilter.cpp
new file mode 100644
index 0000000..e1cc35f
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchFilter.cpp
@@ -0,0 +1,403 @@
+/*
+ * 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 "EpgSearchFilter.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgSearchPath.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimers.h"
+#include "utils/TextSearch.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+
+using namespace PVR;
+
+CPVREpgSearchFilter::CPVREpgSearchFilter(bool bRadio)
+: m_bIsRadio(bRadio)
+{
+ Reset();
+}
+
+void CPVREpgSearchFilter::Reset()
+{
+ m_searchData.Reset();
+ m_bEpgSearchDataFiltered = false;
+
+ m_bIsCaseSensitive = false;
+ m_iMinimumDuration = EPG_SEARCH_UNSET;
+ m_iMaximumDuration = EPG_SEARCH_UNSET;
+ m_bRemoveDuplicates = false;
+
+ /* pvr specific filters */
+ m_iClientID = -1;
+ m_iChannelGroupID = -1;
+ m_iChannelUID = -1;
+ m_bFreeToAirOnly = false;
+ m_bIgnorePresentTimers = true;
+ m_bIgnorePresentRecordings = true;
+
+ m_groupIdMatches.reset();
+
+ m_iDatabaseId = -1;
+ m_title.clear();
+ m_lastExecutedDateTime.SetValid(false);
+}
+
+std::string CPVREpgSearchFilter::GetPath() const
+{
+ return CPVREpgSearchPath(*this).GetPath();
+}
+
+void CPVREpgSearchFilter::SetSearchTerm(const std::string& strSearchTerm)
+{
+ if (m_searchData.m_strSearchTerm != strSearchTerm)
+ {
+ m_searchData.m_strSearchTerm = strSearchTerm;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetSearchPhrase(const std::string& strSearchPhrase)
+{
+ // match the exact phrase
+ SetSearchTerm("\"" + strSearchPhrase + "\"");
+}
+
+void CPVREpgSearchFilter::SetCaseSensitive(bool bIsCaseSensitive)
+{
+ if (m_bIsCaseSensitive != bIsCaseSensitive)
+ {
+ m_bIsCaseSensitive = bIsCaseSensitive;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetSearchInDescription(bool bSearchInDescription)
+{
+ if (m_searchData.m_bSearchInDescription != bSearchInDescription)
+ {
+ m_searchData.m_bSearchInDescription = bSearchInDescription;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetGenreType(int iGenreType)
+{
+ if (m_searchData.m_iGenreType != iGenreType)
+ {
+ m_searchData.m_iGenreType = iGenreType;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetMinimumDuration(int iMinimumDuration)
+{
+ if (m_iMinimumDuration != iMinimumDuration)
+ {
+ m_iMinimumDuration = iMinimumDuration;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetMaximumDuration(int iMaximumDuration)
+{
+ if (m_iMaximumDuration != iMaximumDuration)
+ {
+ m_iMaximumDuration = iMaximumDuration;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnoreFinishedBroadcasts(bool bIgnoreFinishedBroadcasts)
+{
+ if (m_searchData.m_bIgnoreFinishedBroadcasts != bIgnoreFinishedBroadcasts)
+ {
+ m_searchData.m_bIgnoreFinishedBroadcasts = bIgnoreFinishedBroadcasts;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnoreFutureBroadcasts(bool bIgnoreFutureBroadcasts)
+{
+ if (m_searchData.m_bIgnoreFutureBroadcasts != bIgnoreFutureBroadcasts)
+ {
+ m_searchData.m_bIgnoreFutureBroadcasts = bIgnoreFutureBroadcasts;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetStartDateTime(const CDateTime& startDateTime)
+{
+ if (m_searchData.m_startDateTime != startDateTime)
+ {
+ m_searchData.m_startDateTime = startDateTime;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetEndDateTime(const CDateTime& endDateTime)
+{
+ if (m_searchData.m_endDateTime != endDateTime)
+ {
+ m_searchData.m_endDateTime = endDateTime;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIncludeUnknownGenres(bool bIncludeUnknownGenres)
+{
+ if (m_searchData.m_bIncludeUnknownGenres != bIncludeUnknownGenres)
+ {
+ m_searchData.m_bIncludeUnknownGenres = bIncludeUnknownGenres;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetRemoveDuplicates(bool bRemoveDuplicates)
+{
+ if (m_bRemoveDuplicates != bRemoveDuplicates)
+ {
+ m_bRemoveDuplicates = bRemoveDuplicates;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetClientID(int iClientID)
+{
+ if (m_iClientID != iClientID)
+ {
+ m_iClientID = iClientID;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetChannelGroupID(int iChannelGroupID)
+{
+ if (m_iChannelGroupID != iChannelGroupID)
+ {
+ m_iChannelGroupID = iChannelGroupID;
+ m_groupIdMatches.reset();
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetChannelUID(int iChannelUID)
+{
+ if (m_iChannelUID != iChannelUID)
+ {
+ m_iChannelUID = iChannelUID;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetFreeToAirOnly(bool bFreeToAirOnly)
+{
+ if (m_bFreeToAirOnly != bFreeToAirOnly)
+ {
+ m_bFreeToAirOnly = bFreeToAirOnly;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnorePresentTimers(bool bIgnorePresentTimers)
+{
+ if (m_bIgnorePresentTimers != bIgnorePresentTimers)
+ {
+ m_bIgnorePresentTimers = bIgnorePresentTimers;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetIgnorePresentRecordings(bool bIgnorePresentRecordings)
+{
+ if (m_bIgnorePresentRecordings != bIgnorePresentRecordings)
+ {
+ m_bIgnorePresentRecordings = bIgnorePresentRecordings;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetDatabaseId(int iDatabaseId)
+{
+ if (m_iDatabaseId != iDatabaseId)
+ {
+ m_iDatabaseId = iDatabaseId;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetTitle(const std::string& title)
+{
+ if (m_title != title)
+ {
+ m_title = title;
+ m_bChanged = true;
+ }
+}
+
+void CPVREpgSearchFilter::SetLastExecutedDateTime(const CDateTime& lastExecutedDateTime)
+{
+ // Note: No need to set m_bChanged here
+ m_lastExecutedDateTime = lastExecutedDateTime;
+}
+
+bool CPVREpgSearchFilter::MatchGenre(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_bEpgSearchDataFiltered)
+ return true;
+
+ if (m_searchData.m_iGenreType != EPG_SEARCH_UNSET)
+ {
+ if (m_searchData.m_bIncludeUnknownGenres)
+ {
+ // match the exact genre and everything with unknown genre
+ return (tag->GenreType() == m_searchData.m_iGenreType ||
+ tag->GenreType() < EPG_EVENT_CONTENTMASK_MOVIEDRAMA ||
+ tag->GenreType() > EPG_EVENT_CONTENTMASK_USERDEFINED);
+ }
+ else
+ {
+ // match only the exact genre
+ return (tag->GenreType() == m_searchData.m_iGenreType);
+ }
+ }
+
+ // match any genre
+ return true;
+}
+
+bool CPVREpgSearchFilter::MatchDuration(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ bool bReturn(true);
+
+ if (m_iMinimumDuration != EPG_SEARCH_UNSET)
+ bReturn = (tag->GetDuration() > m_iMinimumDuration * 60);
+
+ if (bReturn && m_iMaximumDuration != EPG_SEARCH_UNSET)
+ bReturn = (tag->GetDuration() < m_iMaximumDuration * 60);
+
+ return bReturn;
+}
+
+bool CPVREpgSearchFilter::MatchStartAndEndTimes(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_bEpgSearchDataFiltered)
+ return true;
+
+ return ((!m_searchData.m_bIgnoreFinishedBroadcasts ||
+ tag->EndAsUTC() > CDateTime::GetUTCDateTime()) &&
+ (!m_searchData.m_bIgnoreFutureBroadcasts ||
+ tag->StartAsUTC() < CDateTime::GetUTCDateTime()) &&
+ (!m_searchData.m_startDateTime.IsValid() || // invalid => match any datetime
+ tag->StartAsUTC() >= m_searchData.m_startDateTime) &&
+ (!m_searchData.m_endDateTime.IsValid() || // invalid => match any datetime
+ tag->EndAsUTC() <= m_searchData.m_endDateTime));
+}
+
+bool CPVREpgSearchFilter::MatchSearchTerm(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ bool bReturn(true);
+
+ if (!m_searchData.m_strSearchTerm.empty())
+ {
+ bReturn = !CServiceBroker::GetPVRManager().IsParentalLocked(tag);
+ if (bReturn && (m_bIsCaseSensitive || !m_bEpgSearchDataFiltered))
+ {
+ CTextSearch search(m_searchData.m_strSearchTerm, m_bIsCaseSensitive, SEARCH_DEFAULT_OR);
+
+ bReturn = search.Search(tag->Title()) || search.Search(tag->PlotOutline()) ||
+ (m_searchData.m_bSearchInDescription && search.Search(tag->Plot()));
+ }
+ }
+
+ return bReturn;
+}
+
+bool CPVREpgSearchFilter::FilterEntry(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return MatchGenre(tag) && MatchDuration(tag) && MatchStartAndEndTimes(tag) &&
+ MatchSearchTerm(tag) && MatchChannel(tag) && MatchChannelGroup(tag) && MatchTimers(tag) &&
+ MatchRecordings(tag) && MatchFreeToAir(tag);
+}
+
+void CPVREpgSearchFilter::RemoveDuplicates(std::vector<std::shared_ptr<CPVREpgInfoTag>>& results)
+{
+ for (auto it = results.begin(); it != results.end();)
+ {
+ it = results.erase(std::remove_if(results.begin(),
+ results.end(),
+ [&it](const std::shared_ptr<CPVREpgInfoTag>& entry)
+ {
+ return *it != entry &&
+ (*it)->Title() == entry->Title() &&
+ (*it)->Plot() == entry->Plot() &&
+ (*it)->PlotOutline() == entry->PlotOutline();
+ }),
+ results.end());
+ }
+}
+
+bool CPVREpgSearchFilter::MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return tag && (tag->IsRadio() == m_bIsRadio) &&
+ (m_iClientID == -1 || tag->ClientID() == m_iClientID) &&
+ (m_iChannelUID == -1 || tag->UniqueChannelID() == m_iChannelUID) &&
+ CServiceBroker::GetPVRManager().Clients()->IsCreatedClient(tag->ClientID());
+}
+
+bool CPVREpgSearchFilter::MatchChannelGroup(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_iChannelGroupID != -1)
+ {
+ if (!m_groupIdMatches.has_value())
+ {
+ const std::shared_ptr<CPVRChannelGroup> group = CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(m_bIsRadio)
+ ->GetById(m_iChannelGroupID);
+ m_groupIdMatches =
+ group && (group->GetByUniqueID({tag->ClientID(), tag->UniqueChannelID()}) != nullptr);
+ }
+
+ return *m_groupIdMatches;
+ }
+
+ return true;
+}
+
+bool CPVREpgSearchFilter::MatchFreeToAir(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (m_bFreeToAirOnly)
+ {
+ const std::shared_ptr<CPVRChannel> channel = CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(tag);
+ return channel && !channel->IsEncrypted();
+ }
+
+ return true;
+}
+
+bool CPVREpgSearchFilter::MatchTimers(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return (!m_bIgnorePresentTimers || !CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(tag));
+}
+
+bool CPVREpgSearchFilter::MatchRecordings(const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ return (!m_bIgnorePresentRecordings || !CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(tag));
+}
diff --git a/xbmc/pvr/epg/EpgSearchFilter.h b/xbmc/pvr/epg/EpgSearchFilter.h
new file mode 100644
index 0000000..aa6a2e5
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchFilter.h
@@ -0,0 +1,171 @@
+/*
+ * 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 "XBDateTime.h"
+#include "pvr/epg/EpgSearchData.h"
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+ class CPVREpgInfoTag;
+
+ class CPVREpgSearchFilter
+ {
+ public:
+ CPVREpgSearchFilter() = delete;
+
+ /*!
+ * @brief ctor.
+ * @param bRadio the type of channels to search - if true, 'radio'. 'tv', otherwise.
+ */
+ explicit CPVREpgSearchFilter(bool bRadio);
+
+ /*!
+ * @brief Clear this filter.
+ */
+ void Reset();
+
+ /*!
+ * @brief Return the path for this filter.
+ * @return the path.
+ */
+ std::string GetPath() const;
+
+ /*!
+ * @brief Check if a tag will be filtered or not.
+ * @param tag The tag to check.
+ * @return True if this tag matches the filter, false if not.
+ */
+ bool FilterEntry(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+
+ /*!
+ * @brief remove duplicates from a list of epg tags.
+ * @param results The list of epg tags.
+ */
+ static void RemoveDuplicates(std::vector<std::shared_ptr<CPVREpgInfoTag>>& results);
+
+ /*!
+ * @brief Get the type of channels to search.
+ * @return true, if 'radio'. false, otherwise.
+ */
+ bool IsRadio() const { return m_bIsRadio; }
+
+ const std::string& GetSearchTerm() const { return m_searchData.m_strSearchTerm; }
+ void SetSearchTerm(const std::string& strSearchTerm);
+
+ void SetSearchPhrase(const std::string& strSearchPhrase);
+
+ bool IsCaseSensitive() const { return m_bIsCaseSensitive; }
+ void SetCaseSensitive(bool bIsCaseSensitive);
+
+ bool ShouldSearchInDescription() const { return m_searchData.m_bSearchInDescription; }
+ void SetSearchInDescription(bool bSearchInDescription);
+
+ int GetGenreType() const { return m_searchData.m_iGenreType; }
+ void SetGenreType(int iGenreType);
+
+ int GetMinimumDuration() const { return m_iMinimumDuration; }
+ void SetMinimumDuration(int iMinimumDuration);
+
+ int GetMaximumDuration() const { return m_iMaximumDuration; }
+ void SetMaximumDuration(int iMaximumDuration);
+
+ bool ShouldIgnoreFinishedBroadcasts() const { return m_searchData.m_bIgnoreFinishedBroadcasts; }
+ void SetIgnoreFinishedBroadcasts(bool bIgnoreFinishedBroadcasts);
+
+ bool ShouldIgnoreFutureBroadcasts() const { return m_searchData.m_bIgnoreFutureBroadcasts; }
+ void SetIgnoreFutureBroadcasts(bool bIgnoreFutureBroadcasts);
+
+ const CDateTime& GetStartDateTime() const { return m_searchData.m_startDateTime; }
+ void SetStartDateTime(const CDateTime& startDateTime);
+
+ const CDateTime& GetEndDateTime() const { return m_searchData.m_endDateTime; }
+ void SetEndDateTime(const CDateTime& endDateTime);
+
+ bool ShouldIncludeUnknownGenres() const { return m_searchData.m_bIncludeUnknownGenres; }
+ void SetIncludeUnknownGenres(bool bIncludeUnknownGenres);
+
+ bool ShouldRemoveDuplicates() const { return m_bRemoveDuplicates; }
+ void SetRemoveDuplicates(bool bRemoveDuplicates);
+
+ int GetClientID() const { return m_iClientID; }
+ void SetClientID(int iClientID);
+
+ int GetChannelGroupID() const { return m_iChannelGroupID; }
+ void SetChannelGroupID(int iChannelGroupID);
+
+ int GetChannelUID() const { return m_iChannelUID; }
+ void SetChannelUID(int iChannelUID);
+
+ bool IsFreeToAirOnly() const { return m_bFreeToAirOnly; }
+ void SetFreeToAirOnly(bool bFreeToAirOnly);
+
+ bool ShouldIgnorePresentTimers() const { return m_bIgnorePresentTimers; }
+ void SetIgnorePresentTimers(bool bIgnorePresentTimers);
+
+ bool ShouldIgnorePresentRecordings() const { return m_bIgnorePresentRecordings; }
+ void SetIgnorePresentRecordings(bool bIgnorePresentRecordings);
+
+ int GetDatabaseId() const { return m_iDatabaseId; }
+ void SetDatabaseId(int iDatabaseId);
+
+ const std::string& GetTitle() const { return m_title; }
+ void SetTitle(const std::string& title);
+
+ const CDateTime& GetLastExecutedDateTime() const { return m_lastExecutedDateTime; }
+ void SetLastExecutedDateTime(const CDateTime& lastExecutedDateTime);
+
+ const PVREpgSearchData& GetEpgSearchData() const { return m_searchData; }
+ void SetEpgSearchDataFiltered() { m_bEpgSearchDataFiltered = true; }
+
+ bool IsChanged() const { return m_bChanged; }
+ void SetChanged(bool bChanged) { m_bChanged = bChanged; }
+
+ private:
+ bool MatchGenre(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchDuration(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchStartAndEndTimes(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchSearchTerm(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchChannelGroup(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchFreeToAir(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchTimers(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+ bool MatchRecordings(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+
+ bool m_bChanged = false;
+
+ PVREpgSearchData m_searchData;
+ bool m_bEpgSearchDataFiltered = false;
+
+ bool m_bIsCaseSensitive; /*!< Do a case sensitive search */
+ int m_iMinimumDuration; /*!< The minimum duration for an entry */
+ int m_iMaximumDuration; /*!< The maximum duration for an entry */
+ bool m_bRemoveDuplicates; /*!< True to remove duplicate events, false if not */
+
+ // PVR specific filters
+ bool m_bIsRadio; /*!< True to filter radio channels only, false to tv only */
+ int m_iClientID = -1; /*!< The client id */
+ int m_iChannelGroupID{-1}; /*! The channel group id */
+ int m_iChannelUID = -1; /*!< The channel uid */
+ bool m_bFreeToAirOnly; /*!< Include free to air channels only */
+ bool m_bIgnorePresentTimers; /*!< True to ignore currently present timers (future recordings), false if not */
+ bool m_bIgnorePresentRecordings; /*!< True to ignore currently active recordings, false if not */
+
+ mutable std::optional<bool> m_groupIdMatches;
+
+ int m_iDatabaseId = -1;
+ std::string m_title;
+ CDateTime m_lastExecutedDateTime;
+ };
+}
diff --git a/xbmc/pvr/epg/EpgSearchPath.cpp b/xbmc/pvr/epg/EpgSearchPath.cpp
new file mode 100644
index 0000000..9b24e10
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchPath.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012-2021 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 "EpgSearchPath.h"
+
+#include "pvr/epg/EpgSearchFilter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <cstdlib>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVREpgSearchPath::PATH_SEARCH_DIALOG = "pvr://search/search_dialog";
+const std::string CPVREpgSearchPath::PATH_TV_SEARCH = "pvr://search/tv/";
+const std::string CPVREpgSearchPath::PATH_TV_SAVEDSEARCHES = "pvr://search/tv/savedsearches/";
+const std::string CPVREpgSearchPath::PATH_RADIO_SEARCH = "pvr://search/radio/";
+const std::string CPVREpgSearchPath::PATH_RADIO_SAVEDSEARCHES = "pvr://search/radio/savedsearches/";
+
+CPVREpgSearchPath::CPVREpgSearchPath(const std::string& strPath)
+{
+ Init(strPath);
+}
+
+CPVREpgSearchPath::CPVREpgSearchPath(const CPVREpgSearchFilter& search)
+ : m_path(StringUtils::Format("pvr://search/{}/savedsearches/{}",
+ search.IsRadio() ? "radio" : "tv",
+ search.GetDatabaseId())),
+ m_bValid(true),
+ m_bRoot(false),
+ m_bRadio(search.IsRadio()),
+ m_bSavedSearchesRoot(false)
+{
+}
+
+bool CPVREpgSearchPath::Init(const std::string& strPath)
+{
+ std::string strVarPath(strPath);
+ URIUtils::RemoveSlashAtEnd(strVarPath);
+
+ m_path = strVarPath;
+ const std::vector<std::string> segments = URIUtils::SplitPath(m_path);
+
+ m_bValid =
+ ((segments.size() >= 3) && (segments.size() <= 5) && (segments.at(1) == "search") &&
+ ((segments.at(2) == "radio") || (segments.at(2) == "tv") || (segments.at(2) == "search")) &&
+ ((segments.size() == 3) || (segments.at(3) == "savedsearches")));
+ m_bRoot = (m_bValid && (segments.size() == 3) && (segments.at(2) != "search"));
+ m_bRadio = (m_bValid && (segments.at(2) == "radio"));
+ m_bSavedSearchesRoot =
+ (m_bValid && (segments.size() == 4) && (segments.at(3) == "savedsearches"));
+ m_bSavedSearch = (m_bValid && (segments.size() == 5));
+ if (m_bSavedSearch)
+ m_iId = std::stoi(segments.at(4));
+
+ return m_bValid;
+}
diff --git a/xbmc/pvr/epg/EpgSearchPath.h b/xbmc/pvr/epg/EpgSearchPath.h
new file mode 100644
index 0000000..559a3c6
--- /dev/null
+++ b/xbmc/pvr/epg/EpgSearchPath.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012-2021 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 <string>
+
+namespace PVR
+{
+class CPVREpgSearchFilter;
+
+class CPVREpgSearchPath
+{
+public:
+ static const std::string PATH_SEARCH_DIALOG;
+ static const std::string PATH_TV_SEARCH;
+ static const std::string PATH_TV_SAVEDSEARCHES;
+ static const std::string PATH_RADIO_SEARCH;
+ static const std::string PATH_RADIO_SAVEDSEARCHES;
+
+ explicit CPVREpgSearchPath(const std::string& strPath);
+ explicit CPVREpgSearchPath(const CPVREpgSearchFilter& search);
+
+ bool IsValid() const { return m_bValid; }
+
+ const std::string& GetPath() const { return m_path; }
+ bool IsSearchRoot() const { return m_bRoot; }
+ bool IsRadio() const { return m_bRadio; }
+ bool IsSavedSearchesRoot() const { return m_bSavedSearchesRoot; }
+ bool IsSavedSearch() const { return m_bSavedSearch; }
+ int GetId() const { return m_iId; }
+
+private:
+ bool Init(const std::string& strPath);
+
+ std::string m_path;
+ bool m_bValid = false;
+ bool m_bRoot = false;
+ bool m_bRadio = false;
+ bool m_bSavedSearchesRoot = false;
+ bool m_bSavedSearch = false;
+ int m_iId = -1;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgTagsCache.cpp b/xbmc/pvr/epg/EpgTagsCache.cpp
new file mode 100644
index 0000000..2c4d111
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsCache.cpp
@@ -0,0 +1,179 @@
+/*
+ * 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 "EpgTagsCache.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+using namespace PVR;
+
+namespace
+{
+const CDateTimeSpan ONE_SECOND(0, 0, 0, 1);
+}
+
+void CPVREpgTagsCache::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ m_channelData = data;
+
+ if (m_lastEndedTag)
+ m_lastEndedTag->SetChannelData(data);
+ if (m_nowActiveTag)
+ m_nowActiveTag->SetChannelData(data);
+ if (m_nextStartingTag)
+ m_nextStartingTag->SetChannelData(data);
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsCache::GetLastEndedTag()
+{
+ Refresh();
+ return m_lastEndedTag;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsCache::GetNowActiveTag()
+{
+ Refresh();
+ return m_nowActiveTag;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsCache::GetNextStartingTag()
+{
+ Refresh();
+ return m_nextStartingTag;
+}
+
+void CPVREpgTagsCache::Reset()
+{
+ m_lastEndedTag.reset();
+
+ m_nowActiveTag.reset();
+ m_nowActiveStart.Reset();
+ m_nowActiveEnd.Reset();
+
+ m_nextStartingTag.reset();
+}
+
+bool CPVREpgTagsCache::Refresh()
+{
+ const CDateTime activeTime =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetChannelPlaybackTime(
+ m_channelData->ClientId(), m_channelData->UniqueClientChannelId());
+
+ if (m_nowActiveStart.IsValid() && m_nowActiveEnd.IsValid() && m_nowActiveStart <= activeTime &&
+ m_nowActiveEnd > activeTime)
+ return false;
+
+ const std::shared_ptr<CPVREpgInfoTag> prevNowActiveTag = m_nowActiveTag;
+
+ m_lastEndedTag.reset();
+ m_nowActiveTag.reset();
+ m_nextStartingTag.reset();
+
+ const auto it =
+ std::find_if(m_changedTags.cbegin(), m_changedTags.cend(), [&activeTime](const auto& tag) {
+ return tag.second->StartAsUTC() <= activeTime && tag.second->EndAsUTC() > activeTime;
+ });
+
+ if (it != m_changedTags.cend())
+ {
+ m_nowActiveTag = (*it).second;
+ m_nowActiveStart = m_nowActiveTag->StartAsUTC();
+ m_nowActiveEnd = m_nowActiveTag->EndAsUTC();
+ }
+
+ if (!m_nowActiveTag && m_database)
+ {
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags =
+ m_database->GetEpgTagsByMinEndMaxStartTime(m_iEpgID, activeTime + ONE_SECOND, activeTime);
+ if (!tags.empty())
+ {
+ if (tags.size() > 1)
+ CLog::LogF(LOGWARNING, "Got multiple results. Picking up the first.");
+
+ m_nowActiveTag = tags.front();
+ m_nowActiveTag->SetChannelData(m_channelData);
+ m_nowActiveStart = m_nowActiveTag->StartAsUTC();
+ m_nowActiveEnd = m_nowActiveTag->EndAsUTC();
+ }
+ }
+
+ RefreshLastEndedTag(activeTime);
+ RefreshNextStartingTag(activeTime);
+
+ if (!m_nowActiveTag)
+ {
+ // we're in a gap. remember start and end time of that gap to avoid unneeded db load.
+ if (m_lastEndedTag)
+ m_nowActiveStart = m_lastEndedTag->EndAsUTC();
+ else
+ m_nowActiveStart = activeTime - CDateTimeSpan(1000, 0, 0, 0); // fake start far in the past
+
+ if (m_nextStartingTag)
+ m_nowActiveEnd = m_nextStartingTag->StartAsUTC();
+ else
+ m_nowActiveEnd = activeTime + CDateTimeSpan(1000, 0, 0, 0); // fake end far in the future
+ }
+
+ const bool tagChanged =
+ m_nowActiveTag && (!prevNowActiveTag || *prevNowActiveTag != *m_nowActiveTag);
+ const bool tagRemoved = !m_nowActiveTag && prevNowActiveTag;
+
+ return (tagChanged || tagRemoved);
+}
+
+void CPVREpgTagsCache::RefreshLastEndedTag(const CDateTime& activeTime)
+{
+ if (m_database)
+ {
+ m_lastEndedTag = m_database->GetEpgTagByMaxEndTime(m_iEpgID, activeTime);
+ if (m_lastEndedTag)
+ m_lastEndedTag->SetChannelData(m_channelData);
+ }
+
+ for (auto it = m_changedTags.rbegin(); it != m_changedTags.rend(); ++it)
+ {
+ if (it->second->WasActive())
+ {
+ if (!m_lastEndedTag || m_lastEndedTag->EndAsUTC() < it->second->EndAsUTC())
+ {
+ m_lastEndedTag = it->second;
+ break;
+ }
+ }
+ }
+}
+
+void CPVREpgTagsCache::RefreshNextStartingTag(const CDateTime& activeTime)
+{
+ if (m_database)
+ {
+ m_nextStartingTag = m_database->GetEpgTagByMinStartTime(m_iEpgID, activeTime + ONE_SECOND);
+ if (m_nextStartingTag)
+ m_nextStartingTag->SetChannelData(m_channelData);
+ }
+
+ for (const auto& tag : m_changedTags)
+ {
+ if (tag.second->IsUpcoming())
+ {
+ if (!m_nextStartingTag || m_nextStartingTag->StartAsUTC() > tag.second->StartAsUTC())
+ {
+ m_nextStartingTag = tag.second;
+ break;
+ }
+ }
+ }
+}
diff --git a/xbmc/pvr/epg/EpgTagsCache.h b/xbmc/pvr/epg/EpgTagsCache.h
new file mode 100644
index 0000000..f3505bc
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsCache.h
@@ -0,0 +1,61 @@
+/*
+ * 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 "XBDateTime.h"
+
+#include <map>
+#include <memory>
+
+namespace PVR
+{
+class CPVREpgChannelData;
+class CPVREpgDatabase;
+class CPVREpgInfoTag;
+
+class CPVREpgTagsCache
+{
+public:
+ CPVREpgTagsCache() = delete;
+ CPVREpgTagsCache(int iEpgID,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database,
+ const std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& changedTags)
+ : m_iEpgID(iEpgID), m_channelData(channelData), m_database(database), m_changedTags(changedTags)
+ {
+ }
+
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ void Reset();
+
+ bool Refresh();
+
+ std::shared_ptr<CPVREpgInfoTag> GetLastEndedTag();
+ std::shared_ptr<CPVREpgInfoTag> GetNowActiveTag();
+ std::shared_ptr<CPVREpgInfoTag> GetNextStartingTag();
+
+private:
+ void RefreshLastEndedTag(const CDateTime& activeTime);
+ void RefreshNextStartingTag(const CDateTime& activeTime);
+
+ int m_iEpgID;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ std::shared_ptr<CPVREpgDatabase> m_database;
+ const std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& m_changedTags;
+
+ std::shared_ptr<CPVREpgInfoTag> m_lastEndedTag;
+ std::shared_ptr<CPVREpgInfoTag> m_nowActiveTag;
+ std::shared_ptr<CPVREpgInfoTag> m_nextStartingTag;
+
+ CDateTime m_nowActiveStart;
+ CDateTime m_nowActiveEnd;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/epg/EpgTagsContainer.cpp b/xbmc/pvr/epg/EpgTagsContainer.cpp
new file mode 100644
index 0000000..b4a778c
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsContainer.cpp
@@ -0,0 +1,668 @@
+/*
+ * 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 "EpgTagsContainer.h"
+
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_epg.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgTagsCache.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace PVR;
+
+namespace
+{
+const CDateTimeSpan ONE_SECOND(0, 0, 0, 1);
+}
+
+CPVREpgTagsContainer::CPVREpgTagsContainer(int iEpgID,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database)
+ : m_iEpgID(iEpgID),
+ m_channelData(channelData),
+ m_database(database),
+ m_tagsCache(new CPVREpgTagsCache(iEpgID, channelData, database, m_changedTags))
+{
+}
+
+CPVREpgTagsContainer::~CPVREpgTagsContainer() = default;
+
+void CPVREpgTagsContainer::SetEpgID(int iEpgID)
+{
+ m_iEpgID = iEpgID;
+ for (const auto& tag : m_changedTags)
+ tag.second->SetEpgID(iEpgID);
+}
+
+void CPVREpgTagsContainer::SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data)
+{
+ m_channelData = data;
+ m_tagsCache->SetChannelData(data);
+ for (const auto& tag : m_changedTags)
+ tag.second->SetChannelData(data);
+}
+
+namespace
+{
+
+void ResolveConflictingTags(const std::shared_ptr<CPVREpgInfoTag>& changedTag,
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags)
+{
+ const CDateTime changedTagStart = changedTag->StartAsUTC();
+ const CDateTime changedTagEnd = changedTag->EndAsUTC();
+
+ for (auto it = tags.begin(); it != tags.end();)
+ {
+ bool bInsert = false;
+
+ if (changedTagEnd > (*it)->StartAsUTC() && changedTagStart < (*it)->EndAsUTC())
+ {
+ it = tags.erase(it);
+
+ if (it == tags.end())
+ {
+ bInsert = true;
+ }
+ }
+ else if ((*it)->StartAsUTC() >= changedTagEnd)
+ {
+ bInsert = true;
+ }
+ else
+ {
+ ++it;
+ }
+
+ if (bInsert)
+ {
+ tags.emplace(it, changedTag);
+ break;
+ }
+ }
+}
+
+bool FixOverlap(const std::shared_ptr<CPVREpgInfoTag>& previousTag,
+ const std::shared_ptr<CPVREpgInfoTag>& currentTag)
+{
+ if (!previousTag)
+ return true;
+
+ if (previousTag->EndAsUTC() >= currentTag->EndAsUTC())
+ {
+ // delete the current tag. it's completely overlapped
+ CLog::LogF(LOGDEBUG,
+ "Erasing completely overlapped event from EPG timeline "
+ "({} - {} - {} - {}) "
+ "({} - {} - {} - {}).",
+ previousTag->UniqueBroadcastID(), previousTag->Title(),
+ previousTag->StartAsUTC().GetAsDBDateTime(),
+ previousTag->EndAsUTC().GetAsDBDateTime(), currentTag->UniqueBroadcastID(),
+ currentTag->Title(), currentTag->StartAsUTC().GetAsDBDateTime(),
+ currentTag->EndAsUTC().GetAsDBDateTime());
+
+ return false;
+ }
+ else if (previousTag->EndAsUTC() > currentTag->StartAsUTC())
+ {
+ // fix the end time of the predecessor of the event
+ CLog::LogF(LOGDEBUG,
+ "Fixing partly overlapped event in EPG timeline "
+ "({} - {} - {} - {}) "
+ "({} - {} - {} - {}).",
+ previousTag->UniqueBroadcastID(), previousTag->Title(),
+ previousTag->StartAsUTC().GetAsDBDateTime(),
+ previousTag->EndAsUTC().GetAsDBDateTime(), currentTag->UniqueBroadcastID(),
+ currentTag->Title(), currentTag->StartAsUTC().GetAsDBDateTime(),
+ currentTag->EndAsUTC().GetAsDBDateTime());
+
+ previousTag->SetEndFromUTC(currentTag->StartAsUTC());
+ }
+ return true;
+}
+
+} // unnamed namespace
+
+bool CPVREpgTagsContainer::UpdateEntries(const CPVREpgTagsContainer& tags)
+{
+ if (tags.m_changedTags.empty())
+ return false;
+
+ if (m_database)
+ {
+ const CDateTime minEventEnd = (*tags.m_changedTags.cbegin()).second->StartAsUTC() + ONE_SECOND;
+ const CDateTime maxEventStart = (*tags.m_changedTags.crbegin()).second->EndAsUTC();
+
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> existingTags =
+ m_database->GetEpgTagsByMinEndMaxStartTime(m_iEpgID, minEventEnd, maxEventStart);
+
+ if (!m_changedTags.empty())
+ {
+ // Fix data inconsistencies
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ const auto& changedTag = changedTagsEntry.second;
+
+ if (changedTag->EndAsUTC() > minEventEnd && changedTag->StartAsUTC() < maxEventStart)
+ {
+ // tag is in queried range, thus it could cause inconsistencies...
+ ResolveConflictingTags(changedTag, existingTags);
+ }
+ }
+ }
+
+ bool bResetCache = false;
+ for (const auto& tagsEntry : tags.m_changedTags)
+ {
+ const auto& tag = tagsEntry.second;
+
+ tag->SetChannelData(m_channelData);
+ tag->SetEpgID(m_iEpgID);
+
+ const auto it =
+ std::find_if(existingTags.cbegin(), existingTags.cend(),
+ [&tag](const auto& t) { return t->StartAsUTC() == tag->StartAsUTC(); });
+
+ if (it != existingTags.cend())
+ {
+ const std::shared_ptr<CPVREpgInfoTag>& existingTag = *it;
+
+ existingTag->SetChannelData(m_channelData);
+ existingTag->SetEpgID(m_iEpgID);
+
+ if (existingTag->Update(*tag, false))
+ {
+ // tag differs from existing tag and must be persisted
+ m_changedTags.insert({existingTag->StartAsUTC(), existingTag});
+ bResetCache = true;
+ }
+ }
+ else
+ {
+ // new tags must always be persisted
+ m_changedTags.insert({tag->StartAsUTC(), tag});
+ bResetCache = true;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+ }
+ else
+ {
+ for (const auto& tag : tags.m_changedTags)
+ UpdateEntry(tag.second);
+ }
+
+ return true;
+}
+
+void CPVREpgTagsContainer::FixOverlappingEvents(
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ bool bResetCache = false;
+
+ std::shared_ptr<CPVREpgInfoTag> previousTag;
+ for (auto it = tags.begin(); it != tags.end();)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> currentTag = *it;
+ if (FixOverlap(previousTag, currentTag))
+ {
+ previousTag = currentTag;
+ ++it;
+ }
+ else
+ {
+ it = tags.erase(it);
+ bResetCache = true;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+}
+
+void CPVREpgTagsContainer::FixOverlappingEvents(
+ std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ bool bResetCache = false;
+
+ std::shared_ptr<CPVREpgInfoTag> previousTag;
+ for (auto it = tags.begin(); it != tags.end();)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> currentTag = (*it).second;
+ if (FixOverlap(previousTag, currentTag))
+ {
+ previousTag = currentTag;
+ ++it;
+ }
+ else
+ {
+ it = tags.erase(it);
+ bResetCache = true;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::CreateEntry(
+ const std::shared_ptr<CPVREpgInfoTag>& tag) const
+{
+ if (tag)
+ {
+ tag->SetChannelData(m_channelData);
+ }
+ return tag;
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::CreateEntries(
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ for (auto& tag : tags)
+ {
+ tag->SetChannelData(m_channelData);
+ }
+ return tags;
+}
+
+bool CPVREpgTagsContainer::UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ tag->SetChannelData(m_channelData);
+ tag->SetEpgID(m_iEpgID);
+
+ std::shared_ptr<CPVREpgInfoTag> existingTag = GetTag(tag->StartAsUTC());
+ if (existingTag)
+ {
+ if (existingTag->Update(*tag, false))
+ {
+ // tag differs from existing tag and must be persisted
+ m_changedTags.insert({existingTag->StartAsUTC(), existingTag});
+ m_tagsCache->Reset();
+ }
+ }
+ else
+ {
+ // new tags must always be persisted
+ m_changedTags.insert({tag->StartAsUTC(), tag});
+ m_tagsCache->Reset();
+ }
+
+ return true;
+}
+
+bool CPVREpgTagsContainer::DeleteEntry(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ m_changedTags.erase(tag->StartAsUTC());
+ m_deletedTags.insert({tag->StartAsUTC(), tag});
+ m_tagsCache->Reset();
+ return true;
+}
+
+void CPVREpgTagsContainer::Cleanup(const CDateTime& time)
+{
+ bool bResetCache = false;
+ for (auto it = m_changedTags.begin(); it != m_changedTags.end();)
+ {
+ if (it->second->EndAsUTC() < time)
+ {
+ const auto it1 = m_deletedTags.find(it->first);
+ if (it1 != m_deletedTags.end())
+ m_deletedTags.erase(it1);
+
+ it = m_changedTags.erase(it);
+ bResetCache = true;
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ if (bResetCache)
+ m_tagsCache->Reset();
+
+ if (m_database)
+ m_database->DeleteEpgTags(m_iEpgID, time);
+}
+
+void CPVREpgTagsContainer::Clear()
+{
+ m_changedTags.clear();
+ m_tagsCache->Reset();
+}
+
+bool CPVREpgTagsContainer::IsEmpty() const
+{
+ if (!m_changedTags.empty())
+ return false;
+
+ if (m_database)
+ return !m_database->HasTags(m_iEpgID);
+
+ return true;
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTag(const CDateTime& startTime) const
+{
+ const auto it = m_changedTags.find(startTime);
+ if (it != m_changedTags.cend())
+ return (*it).second;
+
+ if (m_database)
+ return CreateEntry(m_database->GetEpgTagByStartTime(m_iEpgID, startTime));
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTag(unsigned int iUniqueBroadcastID) const
+{
+ if (iUniqueBroadcastID == EPG_TAG_INVALID_UID)
+ return {};
+
+ const auto it = std::find_if(m_changedTags.cbegin(), m_changedTags.cend(),
+ [iUniqueBroadcastID](const auto& tag) {
+ return tag.second->UniqueBroadcastID() == iUniqueBroadcastID;
+ });
+
+ if (it != m_changedTags.cend())
+ return (*it).second;
+
+ if (m_database)
+ return CreateEntry(m_database->GetEpgTagByUniqueBroadcastID(m_iEpgID, iUniqueBroadcastID));
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTagByDatabaseID(int iDatabaseID) const
+{
+ if (iDatabaseID <= 0)
+ return {};
+
+ const auto it =
+ std::find_if(m_changedTags.cbegin(), m_changedTags.cend(), [iDatabaseID](const auto& tag) {
+ return tag.second->DatabaseID() == iDatabaseID;
+ });
+
+ if (it != m_changedTags.cend())
+ return (*it).second;
+
+ if (m_database)
+ return CreateEntry(m_database->GetEpgTagByDatabaseID(m_iEpgID, iDatabaseID));
+
+ return {};
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetTagBetween(const CDateTime& start,
+ const CDateTime& end) const
+{
+ for (const auto& tag : m_changedTags)
+ {
+ if (tag.second->StartAsUTC() >= start)
+ {
+ if (tag.second->EndAsUTC() <= end)
+ return tag.second;
+ else
+ break;
+ }
+ }
+
+ if (m_database)
+ {
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags =
+ CreateEntries(m_database->GetEpgTagsByMinStartMaxEndTime(m_iEpgID, start, end));
+ if (!tags.empty())
+ {
+ if (tags.size() > 1)
+ CLog::LogF(LOGWARNING, "Got multiple tags. Picking up the first.");
+
+ return tags.front();
+ }
+ }
+
+ return {};
+}
+
+bool CPVREpgTagsContainer::UpdateActiveTag()
+{
+ return m_tagsCache->Refresh();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetActiveTag() const
+{
+ return m_tagsCache->GetNowActiveTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetLastEndedTag() const
+{
+ return m_tagsCache->GetLastEndedTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::GetNextStartingTag() const
+{
+ return m_tagsCache->GetNextStartingTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVREpgTagsContainer::CreateGapTag(const CDateTime& start,
+ const CDateTime& end) const
+{
+ return std::make_shared<CPVREpgInfoTag>(m_channelData, m_iEpgID, start, end, true);
+}
+
+void CPVREpgTagsContainer::MergeTags(const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart,
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const
+{
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ const auto& changedTag = changedTagsEntry.second;
+
+ if (changedTag->EndAsUTC() > minEventEnd && changedTag->StartAsUTC() < maxEventStart)
+ tags.emplace_back(changedTag);
+ }
+
+ if (!tags.empty())
+ FixOverlappingEvents(tags);
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::GetTimeline(
+ const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const
+{
+ if (m_database)
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+
+ bool loadFromDb = true;
+ if (!m_changedTags.empty())
+ {
+ const CDateTime lastEnd = m_database->GetLastEndTime(m_iEpgID);
+ if (!lastEnd.IsValid() || lastEnd < minEventEnd)
+ {
+ // nothing in the db yet. take what we have in memory.
+ loadFromDb = false;
+ MergeTags(minEventEnd, maxEventStart, tags);
+ }
+ }
+
+ if (loadFromDb)
+ {
+ tags = m_database->GetEpgTagsByMinEndMaxStartTime(m_iEpgID, minEventEnd, maxEventStart);
+
+ if (!m_changedTags.empty())
+ {
+ // Fix data inconsistencies
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ const auto& changedTag = changedTagsEntry.second;
+
+ if (changedTag->EndAsUTC() > minEventEnd && changedTag->StartAsUTC() < maxEventStart)
+ {
+ // tag is in queried range, thus it could cause inconsistencies...
+ ResolveConflictingTags(changedTag, tags);
+ }
+ }
+
+ // Append missing tags
+ MergeTags(tags.empty() ? minEventEnd : tags.back()->EndAsUTC(), maxEventStart, tags);
+ }
+ }
+
+ tags = CreateEntries(tags);
+
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> result;
+
+ for (const auto& epgTag : tags)
+ {
+ if (!result.empty())
+ {
+ const CDateTime currStart = epgTag->StartAsUTC();
+ const CDateTime prevEnd = result.back()->EndAsUTC();
+ if ((currStart - prevEnd) >= ONE_SECOND)
+ {
+ // insert gap tag before current tag
+ result.emplace_back(CreateGapTag(prevEnd, currStart));
+ }
+ }
+
+ result.emplace_back(epgTag);
+ }
+
+ if (result.empty())
+ {
+ // create single gap tag
+ CDateTime maxEnd = m_database->GetMaxEndTime(m_iEpgID, minEventEnd);
+ if (!maxEnd.IsValid() || maxEnd < timelineStart)
+ maxEnd = timelineStart;
+
+ CDateTime minStart = m_database->GetMinStartTime(m_iEpgID, maxEventStart);
+ if (!minStart.IsValid() || minStart > timelineEnd)
+ minStart = timelineEnd;
+
+ result.emplace_back(CreateGapTag(maxEnd, minStart));
+ }
+ else
+ {
+ if (result.front()->StartAsUTC() > minEventEnd)
+ {
+ // prepend gap tag
+ CDateTime maxEnd = m_database->GetMaxEndTime(m_iEpgID, minEventEnd);
+ if (!maxEnd.IsValid() || maxEnd < timelineStart)
+ maxEnd = timelineStart;
+
+ result.insert(result.begin(), CreateGapTag(maxEnd, result.front()->StartAsUTC()));
+ }
+
+ if (result.back()->EndAsUTC() < maxEventStart)
+ {
+ // append gap tag
+ CDateTime minStart = m_database->GetMinStartTime(m_iEpgID, maxEventStart);
+ if (!minStart.IsValid() || minStart > timelineEnd)
+ minStart = timelineEnd;
+
+ result.emplace_back(CreateGapTag(result.back()->EndAsUTC(), minStart));
+ }
+ }
+
+ return result;
+ }
+
+ return {};
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CPVREpgTagsContainer::GetAllTags() const
+{
+ if (m_database)
+ {
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ if (!m_changedTags.empty() && !m_database->HasTags(m_iEpgID))
+ {
+ // nothing in the db yet. take what we have in memory.
+ std::transform(m_changedTags.cbegin(), m_changedTags.cend(), std::back_inserter(tags),
+ [](const auto& tag) { return tag.second; });
+
+ FixOverlappingEvents(tags);
+ }
+ else
+ {
+ tags = m_database->GetAllEpgTags(m_iEpgID);
+
+ if (!m_changedTags.empty())
+ {
+ // Fix data inconsistencies
+ for (const auto& changedTagsEntry : m_changedTags)
+ {
+ ResolveConflictingTags(changedTagsEntry.second, tags);
+ }
+ }
+ }
+
+ return CreateEntries(tags);
+ }
+
+ return {};
+}
+
+std::pair<CDateTime, CDateTime> CPVREpgTagsContainer::GetFirstAndLastUncommitedEPGDate() const
+{
+ if (m_changedTags.empty())
+ return {};
+
+ return {(*m_changedTags.cbegin()).second->StartAsUTC(),
+ (*m_changedTags.crbegin()).second->EndAsUTC()};
+}
+
+bool CPVREpgTagsContainer::NeedsSave() const
+{
+ return !m_changedTags.empty() || !m_deletedTags.empty();
+}
+
+void CPVREpgTagsContainer::QueuePersistQuery()
+{
+ if (m_database)
+ {
+ m_database->Lock();
+
+ CLog::LogFC(LOGDEBUG, LOGEPG, "EPG Tags Container: Updating {}, deleting {} events...",
+ m_changedTags.size(), m_deletedTags.size());
+
+ for (const auto& tag : m_deletedTags)
+ m_database->QueueDeleteTagQuery(*tag.second);
+
+ m_deletedTags.clear();
+
+ FixOverlappingEvents(m_changedTags);
+
+ for (const auto& tag : m_changedTags)
+ {
+ // remove any conflicting events from database before persisting the new event
+ m_database->QueueDeleteEpgTagsByMinEndMaxStartTimeQuery(
+ m_iEpgID, tag.second->StartAsUTC() + ONE_SECOND, tag.second->EndAsUTC() - ONE_SECOND);
+
+ tag.second->QueuePersistQuery(m_database);
+ }
+
+ Clear();
+
+ m_database->Unlock();
+ }
+}
+
+void CPVREpgTagsContainer::QueueDelete()
+{
+ if (m_database)
+ m_database->QueueDeleteEpgTags(m_iEpgID);
+
+ Clear();
+}
diff --git a/xbmc/pvr/epg/EpgTagsContainer.h b/xbmc/pvr/epg/EpgTagsContainer.h
new file mode 100644
index 0000000..fcbe0d1
--- /dev/null
+++ b/xbmc/pvr/epg/EpgTagsContainer.h
@@ -0,0 +1,227 @@
+/*
+ * 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 "XBDateTime.h"
+
+#include <map>
+#include <memory>
+#include <vector>
+
+namespace PVR
+{
+class CPVREpgTagsCache;
+class CPVREpgChannelData;
+class CPVREpgDatabase;
+class CPVREpgInfoTag;
+
+class CPVREpgTagsContainer
+{
+public:
+ CPVREpgTagsContainer() = delete;
+ CPVREpgTagsContainer(int iEpgID,
+ const std::shared_ptr<CPVREpgChannelData>& channelData,
+ const std::shared_ptr<CPVREpgDatabase>& database);
+ virtual ~CPVREpgTagsContainer();
+
+ /*!
+ * @brief Set the EPG id for this EPG.
+ * @param iEpgID The ID.
+ */
+ void SetEpgID(int iEpgID);
+
+ /*!
+ * @brief Set the channel data for this EPG.
+ * @param data The channel data.
+ */
+ void SetChannelData(const std::shared_ptr<CPVREpgChannelData>& data);
+
+ /*!
+ * @brief Update an entry.
+ * @param tag The tag to update.
+ * @return True if it was updated successfully, false otherwise.
+ */
+ bool UpdateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag);
+
+ /*!
+ * @brief Delete an entry.
+ * @param tag The tag to delete.
+ * @return True if it was deleted successfully, false otherwise.
+ */
+ bool DeleteEntry(const std::shared_ptr<CPVREpgInfoTag>& tag);
+
+ /*!
+ * @brief Update all entries with the provided tags.
+ * @param tags The updated tags.
+ * @return True if the update was successful, false otherwise.
+ */
+ bool UpdateEntries(const CPVREpgTagsContainer& tags);
+
+ /*!
+ * @brief Release all entries.
+ */
+ void Clear();
+
+ /*!
+ * @brief Remove all entries which were finished before the given time.
+ * @param time Delete entries with an end time before this time.
+ */
+ void Cleanup(const CDateTime& time);
+
+ /*!
+ * @brief Check whether this container is empty.
+ * @return True if the container does not contain any entries, false otherwise.
+ */
+ bool IsEmpty() const;
+
+ /*!
+ * @brief Get an EPG tag given its start time.
+ * @param startTime The start time
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTag(const CDateTime& startTime) const;
+
+ /*!
+ * @brief Get an EPG tag given its unique broadcast ID.
+ * @param iUniqueBroadcastID The ID.
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTag(unsigned int iUniqueBroadcastID) const;
+
+ /*!
+ * @brief Get an EPG tag given its database ID.
+ * @param iDatabaseID The ID.
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagByDatabaseID(int iDatabaseID) const;
+
+ /*!
+ * @brief Get the event that occurs between the given begin and end time.
+ * @param start The start of the time interval.
+ * @param end The end of the time interval.
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetTagBetween(const CDateTime& start, const CDateTime& end) const;
+
+ /*!
+ * @brief Update the currently active event.
+ * @return True if the active event was updated, false otherwise.
+ */
+ bool UpdateActiveTag();
+
+ /*!
+ * @brief Get the event that is occurring now
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetActiveTag() const;
+
+ /*!
+ * @brief Get the event that will occur next
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetNextStartingTag() const;
+
+ /*!
+ * @brief Get the event that occurred previously
+ * @return The tag or nullptr if no tag was found.
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetLastEndedTag() const;
+
+ /*!
+ * @brief Get all EPG tags for the given time frame, including "gap" tags.
+ * @param timelineStart Start of time line
+ * @param timelineEnd End of time line
+ * @param minEventEnd The minimum end time of the events to return
+ * @param maxEventStart The maximum start time of the events to return
+ * @return The matching tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetTimeline(const CDateTime& timelineStart,
+ const CDateTime& timelineEnd,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const;
+
+ /*!
+ * @brief Get all EPG tags.
+ * @return The tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetAllTags() const;
+
+ /*!
+ * @brief Get the start and end time of the last not yet commited entry in this EPG.
+ * @return The times; first: start time, second: end time.
+ */
+ std::pair<CDateTime, CDateTime> GetFirstAndLastUncommitedEPGDate() const;
+
+ /*!
+ * @brief Check whether this container has unsaved data.
+ * @return True if this container contains unsaved data, false otherwise.
+ */
+ bool NeedsSave() const;
+
+ /*!
+ * @brief Write the query to persist data into database's queue
+ */
+ void QueuePersistQuery();
+
+ /*!
+ * @brief Queue the deletion of this container from its database.
+ */
+ void QueueDelete();
+
+private:
+ /*!
+ * @brief Complete the instance data for the given tags.
+ * @param tags The tags to complete.
+ * @return The completed tags.
+ */
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> CreateEntries(
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+
+ /*!
+ * @brief Complete the instance data for the given tag.
+ * @param tags The tag to complete.
+ * @return The completed tag.
+ */
+ std::shared_ptr<CPVREpgInfoTag> CreateEntry(const std::shared_ptr<CPVREpgInfoTag>& tag) const;
+
+ /*!
+ * @brief Create a "gap" tag
+ * @param start The start time of the gap.
+ * @param end The end time of the gap.
+ * @return The tag.
+ */
+ std::shared_ptr<CPVREpgInfoTag> CreateGapTag(const CDateTime& start, const CDateTime& end) const;
+
+ /*!
+ * @brief Merge m_changedTags tags into given tags, resolving conflicts.
+ * @param minEventEnd The minimum end time of the events to return
+ * @param maxEventStart The maximum start time of the events to return
+ * @param tags The merged tags.
+ */
+ void MergeTags(const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart,
+ std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+
+ /*!
+ * @brief Fix overlapping events.
+ * @param tags The events to check/fix.
+ */
+ void FixOverlappingEvents(std::vector<std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+ void FixOverlappingEvents(std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>>& tags) const;
+
+ int m_iEpgID = 0;
+ std::shared_ptr<CPVREpgChannelData> m_channelData;
+ const std::shared_ptr<CPVREpgDatabase> m_database;
+ const std::unique_ptr<CPVREpgTagsCache> m_tagsCache;
+
+ std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>> m_changedTags;
+ std::map<CDateTime, std::shared_ptr<CPVREpgInfoTag>> m_deletedTags;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/filesystem/CMakeLists.txt b/xbmc/pvr/filesystem/CMakeLists.txt
new file mode 100644
index 0000000..823d783
--- /dev/null
+++ b/xbmc/pvr/filesystem/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES PVRGUIDirectory.cpp)
+
+set(HEADERS PVRGUIDirectory.h)
+
+core_add_library(pvr_filesystem)
diff --git a/xbmc/pvr/filesystem/PVRGUIDirectory.cpp b/xbmc/pvr/filesystem/PVRGUIDirectory.cpp
new file mode 100644
index 0000000..1ef7810
--- /dev/null
+++ b/xbmc/pvr/filesystem/PVRGUIDirectory.cpp
@@ -0,0 +1,625 @@
+/*
+ * Copyright (C) 2012-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.
+ */
+
+#include "PVRGUIDirectory.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "input/WindowTranslator.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "pvr/epg/EpgSearchPath.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+bool CPVRGUIDirectory::Exists() const
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return false;
+
+ return m_url.IsProtocol("pvr") && StringUtils::StartsWith(m_url.GetFileName(), "recordings");
+}
+
+bool CPVRGUIDirectory::SupportsWriteFileOperations() const
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return false;
+
+ const std::string filename = m_url.GetFileName();
+ return URIUtils::IsPVRRecording(filename);
+}
+
+namespace
+{
+
+bool GetRootDirectory(bool bRadio, CFileItemList& results)
+{
+ std::shared_ptr<CFileItem> item;
+
+ const std::shared_ptr<CPVRClients> clients = CServiceBroker::GetPVRManager().Clients();
+
+ // EPG
+ const bool bAnyClientSupportingEPG = clients->AnyClientSupportingEPG();
+ if (bAnyClientSupportingEPG)
+ {
+ item.reset(
+ new CFileItem(StringUtils::Format("pvr://guide/{}/", bRadio ? "radio" : "tv"), true));
+ item->SetLabel(g_localizeStrings.Get(19069)); // Guide
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_GUIDE
+ : WINDOW_TV_GUIDE));
+ item->SetArt("icon", "DefaultPVRGuide.png");
+ results.Add(item);
+ }
+
+ // Channels
+ item.reset(new CFileItem(
+ bRadio ? CPVRChannelsPath::PATH_RADIO_CHANNELS : CPVRChannelsPath::PATH_TV_CHANNELS, true));
+ item->SetLabel(g_localizeStrings.Get(19019)); // Channels
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_CHANNELS
+ : WINDOW_TV_CHANNELS));
+ item->SetArt("icon", "DefaultPVRChannels.png");
+ results.Add(item);
+
+ // Recordings
+ if (clients->AnyClientSupportingRecordings())
+ {
+ item.reset(new CFileItem(bRadio ? CPVRRecordingsPath::PATH_ACTIVE_RADIO_RECORDINGS
+ : CPVRRecordingsPath::PATH_ACTIVE_TV_RECORDINGS,
+ true));
+ item->SetLabel(g_localizeStrings.Get(19017)); // Recordings
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(
+ bRadio ? WINDOW_RADIO_RECORDINGS : WINDOW_TV_RECORDINGS));
+ item->SetArt("icon", "DefaultPVRRecordings.png");
+ results.Add(item);
+ }
+
+ // Timers/Timer rules
+ // - always present, because Reminders are always available, no client support needed for this
+ item.reset(new CFileItem(
+ bRadio ? CPVRTimersPath::PATH_RADIO_TIMERS : CPVRTimersPath::PATH_TV_TIMERS, true));
+ item->SetLabel(g_localizeStrings.Get(19040)); // Timers
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_TIMERS
+ : WINDOW_TV_TIMERS));
+ item->SetArt("icon", "DefaultPVRTimers.png");
+ results.Add(item);
+
+ item.reset(new CFileItem(
+ bRadio ? CPVRTimersPath::PATH_RADIO_TIMER_RULES : CPVRTimersPath::PATH_TV_TIMER_RULES, true));
+ item->SetLabel(g_localizeStrings.Get(19138)); // Timer rules
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(
+ bRadio ? WINDOW_RADIO_TIMER_RULES : WINDOW_TV_TIMER_RULES));
+ item->SetArt("icon", "DefaultPVRTimerRules.png");
+ results.Add(item);
+
+ // Search
+ if (bAnyClientSupportingEPG)
+ {
+ item.reset(new CFileItem(
+ bRadio ? CPVREpgSearchPath::PATH_RADIO_SEARCH : CPVREpgSearchPath::PATH_TV_SEARCH, true));
+ item->SetLabel(g_localizeStrings.Get(137)); // Search
+ item->SetProperty("node.target", CWindowTranslator::TranslateWindow(bRadio ? WINDOW_RADIO_SEARCH
+ : WINDOW_TV_SEARCH));
+ item->SetArt("icon", "DefaultPVRSearch.png");
+ results.Add(item);
+ }
+
+ return true;
+}
+
+} // unnamed namespace
+
+bool CPVRGUIDirectory::GetDirectory(CFileItemList& results) const
+{
+ std::string base = m_url.Get();
+ URIUtils::RemoveSlashAtEnd(base);
+
+ std::string fileName = m_url.GetFileName();
+ URIUtils::RemoveSlashAtEnd(fileName);
+
+ results.SetCacheToDisc(CFileItemList::CACHE_NEVER);
+
+ if (fileName.empty())
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ std::shared_ptr<CFileItem> item;
+
+ item.reset(new CFileItem(base + "channels/", true));
+ item->SetLabel(g_localizeStrings.Get(19019)); // Channels
+ item->SetLabelPreformatted(true);
+ results.Add(item);
+
+ item.reset(new CFileItem(base + "recordings/active/", true));
+ item->SetLabel(g_localizeStrings.Get(19017)); // Recordings
+ item->SetLabelPreformatted(true);
+ results.Add(item);
+
+ item.reset(new CFileItem(base + "recordings/deleted/", true));
+ item->SetLabel(g_localizeStrings.Get(19184)); // Deleted recordings
+ item->SetLabelPreformatted(true);
+ results.Add(item);
+
+ // Sort by name only. Labels are preformatted.
+ results.AddSortMethod(SortByLabel, 551 /* Name */, LABEL_MASKS("%L", "", "%L", ""));
+ }
+ return true;
+ }
+ else if (StringUtils::StartsWith(fileName, "tv"))
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ return GetRootDirectory(false, results);
+ }
+ return true;
+ }
+ else if (StringUtils::StartsWith(fileName, "radio"))
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ return GetRootDirectory(true, results);
+ }
+ return true;
+ }
+ else if (StringUtils::StartsWith(fileName, "recordings"))
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ return GetRecordingsDirectory(results);
+ }
+ return true;
+ }
+ else if (StringUtils::StartsWith(fileName, "channels"))
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ return GetChannelsDirectory(results);
+ }
+ return true;
+ }
+ else if (StringUtils::StartsWith(fileName, "timers"))
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ return GetTimersDirectory(results);
+ }
+ return true;
+ }
+
+ const CPVREpgSearchPath path(m_url.Get());
+ if (path.IsValid())
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ if (path.IsSavedSearchesRoot())
+ return GetSavedSearchesDirectory(path.IsRadio(), results);
+ }
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRGUIDirectory::HasTVRecordings()
+{
+ return CServiceBroker::GetPVRManager().IsStarted() &&
+ CServiceBroker::GetPVRManager().Recordings()->GetNumTVRecordings() > 0;
+}
+
+bool CPVRGUIDirectory::HasDeletedTVRecordings()
+{
+ return CServiceBroker::GetPVRManager().IsStarted() &&
+ CServiceBroker::GetPVRManager().Recordings()->HasDeletedTVRecordings();
+}
+
+bool CPVRGUIDirectory::HasRadioRecordings()
+{
+ return CServiceBroker::GetPVRManager().IsStarted() &&
+ CServiceBroker::GetPVRManager().Recordings()->GetNumRadioRecordings() > 0;
+}
+
+bool CPVRGUIDirectory::HasDeletedRadioRecordings()
+{
+ return CServiceBroker::GetPVRManager().IsStarted() &&
+ CServiceBroker::GetPVRManager().Recordings()->HasDeletedRadioRecordings();
+}
+
+namespace
+{
+
+std::string TrimSlashes(const std::string& strOrig)
+{
+ std::string strReturn = strOrig;
+ while (strReturn[0] == '/')
+ strReturn.erase(0, 1);
+
+ URIUtils::RemoveSlashAtEnd(strReturn);
+ return strReturn;
+}
+
+bool IsDirectoryMember(const std::string& strDirectory,
+ const std::string& strEntryDirectory,
+ bool bGrouped)
+{
+ const std::string strUseDirectory = TrimSlashes(strDirectory);
+ const std::string strUseEntryDirectory = TrimSlashes(strEntryDirectory);
+
+ // Case-insensitive comparison since sub folders are created with case-insensitive matching (GetSubDirectories)
+ if (bGrouped)
+ return StringUtils::EqualsNoCase(strUseDirectory, strUseEntryDirectory);
+ else
+ return StringUtils::StartsWithNoCase(strUseEntryDirectory, strUseDirectory);
+}
+
+void GetSubDirectories(const CPVRRecordingsPath& recParentPath,
+ const std::vector<std::shared_ptr<CPVRRecording>>& recordings,
+ CFileItemList& results)
+{
+ // Only active recordings are fetched to provide sub directories.
+ // Not applicable for deleted view which is supposed to be flattened.
+ std::set<std::shared_ptr<CFileItem>> unwatchedFolders;
+ bool bRadio = recParentPath.IsRadio();
+
+ for (const auto& recording : recordings)
+ {
+ if (recording->IsDeleted())
+ continue;
+
+ if (recording->IsRadio() != bRadio)
+ continue;
+
+ const std::string strCurrent =
+ recParentPath.GetUnescapedSubDirectoryPath(recording->Directory());
+ if (strCurrent.empty())
+ continue;
+
+ CPVRRecordingsPath recChildPath(recParentPath);
+ recChildPath.AppendSegment(strCurrent);
+ const std::string strFilePath = recChildPath;
+
+ std::shared_ptr<CFileItem> item;
+ if (!results.Contains(strFilePath))
+ {
+ item.reset(new CFileItem(strCurrent, true));
+ item->SetPath(strFilePath);
+ item->SetLabel(strCurrent);
+ item->SetLabelPreformatted(true);
+ item->m_dateTime = recording->RecordingTimeAsLocalTime();
+ item->SetProperty("totalepisodes", 0);
+ item->SetProperty("watchedepisodes", 0);
+ item->SetProperty("unwatchedepisodes", 0);
+ item->SetProperty("sizeinbytes", UINT64_C(0));
+
+ // Assume all folders are watched, we'll change the overlay later
+ item->SetOverlayImage(CGUIListItem::ICON_OVERLAY_WATCHED, false);
+ results.Add(item);
+ }
+ else
+ {
+ item = results.Get(strFilePath);
+ if (item->m_dateTime < recording->RecordingTimeAsLocalTime())
+ item->m_dateTime = recording->RecordingTimeAsLocalTime();
+ }
+
+ item->IncrementProperty("totalepisodes", 1);
+ if (recording->GetPlayCount() == 0)
+ {
+ unwatchedFolders.insert(item);
+ item->IncrementProperty("unwatchedepisodes", 1);
+ }
+ else
+ {
+ item->IncrementProperty("watchedepisodes", 1);
+ }
+ item->SetLabel2(StringUtils::Format("{} / {}", item->GetProperty("watchedepisodes").asString(),
+ item->GetProperty("totalepisodes").asString()));
+
+ item->IncrementProperty("sizeinbytes", recording->GetSizeInBytes());
+ }
+
+ // Replace the incremental size of the recordings with a string equivalent
+ for (auto& item : results.GetList())
+ {
+ int64_t size = item->GetProperty("sizeinbytes").asInteger();
+ item->ClearProperty("sizeinbytes");
+ item->m_dwSize = size; // We'll also sort recording folders by size
+ if (size > 0)
+ item->SetProperty("recordingsize", StringUtils::SizeToString(size));
+ }
+
+ // Change the watched overlay to unwatched for folders containing unwatched entries
+ for (auto& item : unwatchedFolders)
+ item->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, false);
+}
+
+} // unnamed namespace
+
+bool CPVRGUIDirectory::GetRecordingsDirectory(CFileItemList& results) const
+{
+ results.SetContent("recordings");
+
+ bool bGrouped = false;
+ const std::vector<std::shared_ptr<CPVRRecording>> recordings =
+ CServiceBroker::GetPVRManager().Recordings()->GetAll();
+
+ if (m_url.HasOption("view"))
+ {
+ const std::string view = m_url.GetOption("view");
+ if (view == "grouped")
+ bGrouped = true;
+ else if (view == "flat")
+ bGrouped = false;
+ else
+ {
+ CLog::LogF(LOGERROR, "Unsupported value '{}' for url parameter 'view'", view);
+ return false;
+ }
+ }
+ else
+ {
+ bGrouped = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRRECORD_GROUPRECORDINGS);
+ }
+
+ const CPVRRecordingsPath recPath(m_url.GetWithoutOptions());
+ if (recPath.IsValid())
+ {
+ // Get the directory structure if in non-flatten mode
+ // Deleted view is always flatten. So only for an active view
+ const std::string strDirectory = recPath.GetUnescapedDirectoryPath();
+ if (!recPath.IsDeleted() && bGrouped)
+ GetSubDirectories(recPath, recordings, results);
+
+ // get all files of the current directory or recursively all files starting at the current directory if in flatten mode
+ std::shared_ptr<CFileItem> item;
+ for (const auto& recording : recordings)
+ {
+ // Omit recordings not matching criteria
+ if (recording->IsDeleted() != recPath.IsDeleted() ||
+ recording->IsRadio() != recPath.IsRadio() ||
+ !IsDirectoryMember(strDirectory, recording->Directory(), bGrouped))
+ continue;
+
+ item = std::make_shared<CFileItem>(recording);
+ item->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, recording->GetPlayCount() > 0);
+ results.Add(item);
+ }
+ }
+
+ return recPath.IsValid();
+}
+
+bool CPVRGUIDirectory::GetSavedSearchesDirectory(bool bRadio, CFileItemList& results) const
+{
+ const std::vector<std::shared_ptr<CPVREpgSearchFilter>> searches =
+ CServiceBroker::GetPVRManager().EpgContainer().GetSavedSearches(bRadio);
+
+ for (const auto& search : searches)
+ {
+ results.Add(std::make_shared<CFileItem>(search));
+ }
+ return true;
+}
+
+bool CPVRGUIDirectory::GetChannelGroupsDirectory(bool bRadio,
+ bool bExcludeHidden,
+ CFileItemList& results)
+{
+ const CPVRChannelGroups* channelGroups =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio);
+ if (channelGroups)
+ {
+ std::shared_ptr<CFileItem> item;
+ const std::vector<std::shared_ptr<CPVRChannelGroup>> groups =
+ channelGroups->GetMembers(bExcludeHidden);
+ for (const auto& group : groups)
+ {
+ item = std::make_shared<CFileItem>(group->GetPath(), true);
+ item->m_strTitle = group->GroupName();
+ item->SetLabel(group->GroupName());
+ results.Add(item);
+ }
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIDirectory::GetChannelsDirectory(CFileItemList& results) const
+{
+ const CPVRChannelsPath path(m_url.GetWithoutOptions());
+ if (path.IsValid())
+ {
+ if (path.IsEmpty())
+ {
+ std::shared_ptr<CFileItem> item;
+
+ // all tv channels
+ item.reset(new CFileItem(CPVRChannelsPath::PATH_TV_CHANNELS, true));
+ item->SetLabel(g_localizeStrings.Get(19020)); // TV
+ item->SetLabelPreformatted(true);
+ results.Add(item);
+
+ // all radio channels
+ item.reset(new CFileItem(CPVRChannelsPath::PATH_RADIO_CHANNELS, true));
+ item->SetLabel(g_localizeStrings.Get(19021)); // Radio
+ item->SetLabelPreformatted(true);
+ results.Add(item);
+
+ return true;
+ }
+ else if (path.IsChannelsRoot())
+ {
+ return GetChannelGroupsDirectory(path.IsRadio(), true, results);
+ }
+ else if (path.IsChannelGroup())
+ {
+ const std::string& strGroupName = path.GetGroupName();
+ bool bShowHiddenChannels = path.IsHiddenChannelGroup();
+
+ std::shared_ptr<CPVRChannelGroup> group;
+ if (bShowHiddenChannels || strGroupName == "*") // all channels
+ {
+ group = CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(path.IsRadio());
+ }
+ else
+ {
+ group = CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(path.IsRadio())
+ ->GetByName(strGroupName);
+ }
+
+ if (group)
+ {
+ const bool playedOnly =
+ (m_url.HasOption("view") && (m_url.GetOption("view") == "lastplayed"));
+
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ group->GetMembers();
+ for (const auto& groupMember : groupMembers)
+ {
+ if (bShowHiddenChannels != groupMember->Channel()->IsHidden())
+ continue;
+
+ if (playedOnly && !groupMember->Channel()->LastWatched())
+ continue;
+
+ results.Add(std::make_shared<CFileItem>(groupMember));
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to obtain members of channel group '{}'", strGroupName);
+ return false;
+ }
+
+ return true;
+ }
+ }
+ return false;
+}
+
+namespace
+{
+
+bool GetTimersRootDirectory(const CPVRTimersPath& path,
+ bool bHideDisabled,
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>>& timers,
+ CFileItemList& results)
+{
+ bool bRadio = path.IsRadio();
+ bool bRules = path.IsRules();
+
+ for (const auto& timer : timers)
+ {
+ if ((bRadio == timer->IsRadio() ||
+ (bRules && timer->ClientChannelUID() == PVR_TIMER_ANY_CHANNEL)) &&
+ (bRules == timer->IsTimerRule()) && (!bHideDisabled || !timer->IsDisabled()))
+ {
+ const auto item = std::make_shared<CFileItem>(timer);
+ const CPVRTimersPath timersPath(path.GetPath(), timer->ClientID(), timer->ClientIndex());
+ item->SetPath(timersPath.GetPath());
+ results.Add(item);
+ }
+ }
+ return true;
+}
+
+bool GetTimersSubDirectory(const CPVRTimersPath& path,
+ bool bHideDisabled,
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>>& timers,
+ CFileItemList& results)
+{
+ bool bRadio = path.IsRadio();
+ int iParentId = path.GetParentId();
+ int iClientId = path.GetClientId();
+
+ std::shared_ptr<CFileItem> item;
+
+ for (const auto& timer : timers)
+ {
+ if ((timer->IsRadio() == bRadio) && timer->HasParent() && (timer->ClientID() == iClientId) &&
+ (timer->ParentClientIndex() == iParentId) && (!bHideDisabled || !timer->IsDisabled()))
+ {
+ item.reset(new CFileItem(timer));
+ const CPVRTimersPath timersPath(path.GetPath(), timer->ClientID(), timer->ClientIndex());
+ item->SetPath(timersPath.GetPath());
+ results.Add(item);
+ }
+ }
+ return true;
+}
+
+} // unnamed namespace
+
+bool CPVRGUIDirectory::GetTimersDirectory(CFileItemList& results) const
+{
+ const CPVRTimersPath path(m_url.GetWithoutOptions());
+ if (path.IsValid() && (path.IsTimersRoot() || path.IsTimerRule()))
+ {
+ bool bHideDisabled = false;
+ if (m_url.HasOption("view"))
+ {
+ const std::string view = m_url.GetOption("view");
+ if (view == "hidedisabled")
+ {
+ bHideDisabled = true;
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unsupported value '{}' for url parameter 'view'", view);
+ return false;
+ }
+ }
+ else
+ {
+ bHideDisabled = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRTIMERS_HIDEDISABLEDTIMERS);
+ }
+
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>> timers =
+ CServiceBroker::GetPVRManager().Timers()->GetAll();
+
+ if (path.IsTimersRoot())
+ {
+ /* Root folder containing either timer rules or timers. */
+ return GetTimersRootDirectory(path, bHideDisabled, timers, results);
+ }
+ else if (path.IsTimerRule())
+ {
+ /* Sub folder containing the timers scheduled by the given timer rule. */
+ return GetTimersSubDirectory(path, bHideDisabled, timers, results);
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/pvr/filesystem/PVRGUIDirectory.h b/xbmc/pvr/filesystem/PVRGUIDirectory.h
new file mode 100644
index 0000000..462c553
--- /dev/null
+++ b/xbmc/pvr/filesystem/PVRGUIDirectory.h
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2012-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 "URL.h"
+
+#include <string>
+
+class CFileItemList;
+
+namespace PVR
+{
+
+class CPVRGUIDirectory
+{
+public:
+ /*!
+ * @brief PVR GUI directory ctor.
+ * @param url The directory's URL.
+ */
+ explicit CPVRGUIDirectory(const CURL& url) : m_url(url) {}
+
+ /*!
+ * @brief PVR GUI directory ctor.
+ * @param path The directory's path.
+ */
+ explicit CPVRGUIDirectory(const std::string& path) : m_url(path) {}
+
+ /*!
+ * @brief PVR GUI directory dtor.
+ */
+ virtual ~CPVRGUIDirectory() = default;
+
+ /*!
+ * @brief Check existence of this directory.
+ * @return True if the directory exists, false otherwise.
+ */
+ bool Exists() const;
+
+ /*!
+ * @brief Obtain the directory listing.
+ * @param results The list to fill with the results.
+ * @return True on success, false otherwise.
+ */
+ bool GetDirectory(CFileItemList& results) const;
+
+ /*!
+ * @brief Check if this directory supports file write operations.
+ * @return True if the directory supports file write operations, false otherwise.
+ */
+ bool SupportsWriteFileOperations() const;
+
+ /*!
+ * @brief Check if any TV recordings are existing.
+ * @return True if TV recordings exists, false otherwise.
+ */
+ static bool HasTVRecordings();
+
+ /*!
+ * @brief Check if any deleted TV recordings are existing.
+ * @return True if deleted TV recordings exists, false otherwise.
+ */
+ static bool HasDeletedTVRecordings();
+
+ /*!
+ * @brief Check if any radio recordings are existing.
+ * @return True if radio recordings exists, false otherwise.
+ */
+ static bool HasRadioRecordings();
+
+ /*!
+ * @brief Check if any deleted radio recordings are existing.
+ * @return True if deleted radio recordings exists, false otherwise.
+ */
+ static bool HasDeletedRadioRecordings();
+
+ /*!
+ * @brief Get the list of channel groups.
+ * @param bRadio If true, obtain radio groups, tv groups otherwise.
+ * @param bExcludeHidden If true exclude hidden groups, include hidden groups otherwise.
+ * @param results The file list to store the results in.
+ * @return True on success, false otherwise..
+ */
+ static bool GetChannelGroupsDirectory(bool bRadio, bool bExcludeHidden, CFileItemList& results);
+
+ /*!
+ * @brief Get the list of channels.
+ * @param results The file list to store the results in.
+ * @return True on success, false otherwise..
+ */
+ bool GetChannelsDirectory(CFileItemList& results) const;
+
+private:
+ bool GetTimersDirectory(CFileItemList& results) const;
+ bool GetRecordingsDirectory(CFileItemList& results) const;
+ bool GetSavedSearchesDirectory(bool bRadio, CFileItemList& results) const;
+
+ const CURL m_url;
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/CMakeLists.txt b/xbmc/pvr/guilib/CMakeLists.txt
new file mode 100644
index 0000000..3a73a68
--- /dev/null
+++ b/xbmc/pvr/guilib/CMakeLists.txt
@@ -0,0 +1,35 @@
+set(SOURCES GUIEPGGridContainer.cpp
+ GUIEPGGridContainerModel.cpp
+ PVRGUIActionListener.cpp
+ PVRGUIActionsChannels.cpp
+ PVRGUIActionsClients.cpp
+ PVRGUIActionsDatabase.cpp
+ PVRGUIActionsEPG.cpp
+ PVRGUIActionsUtils.cpp
+ PVRGUIActionsParentalControl.cpp
+ PVRGUIActionsPlayback.cpp
+ PVRGUIActionsPowerManagement.cpp
+ PVRGUIActionsRecordings.cpp
+ PVRGUIActionsTimers.cpp
+ PVRGUIChannelIconUpdater.cpp
+ PVRGUIChannelNavigator.cpp
+ PVRGUIProgressHandler.cpp)
+
+set(HEADERS GUIEPGGridContainer.h
+ GUIEPGGridContainerModel.h
+ PVRGUIActionListener.h
+ PVRGUIActionsChannels.h
+ PVRGUIActionsClients.h
+ PVRGUIActionsDatabase.h
+ PVRGUIActionsEPG.h
+ PVRGUIActionsUtils.h
+ PVRGUIActionsParentalControl.h
+ PVRGUIActionsPlayback.h
+ PVRGUIActionsPowerManagement.h
+ PVRGUIActionsRecordings.h
+ PVRGUIActionsTimers.h
+ PVRGUIChannelIconUpdater.h
+ PVRGUIChannelNavigator.h
+ PVRGUIProgressHandler.h)
+
+core_add_library(pvr_guilib)
diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.cpp b/xbmc/pvr/guilib/GUIEPGGridContainer.cpp
new file mode 100644
index 0000000..d88dc86
--- /dev/null
+++ b/xbmc/pvr/guilib/GUIEPGGridContainer.cpp
@@ -0,0 +1,2549 @@
+/*
+ * 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 "GUIEPGGridContainer.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/DirtyRegion.h"
+#include "guilib/GUIAction.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "input/Key.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/GUIEPGGridContainerModel.h"
+#include "utils/MathUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+
+#include <tinyxml.h>
+
+using namespace PVR;
+
+#define BLOCKJUMP 4 // how many blocks are jumped with each analogue scroll action
+static const int BLOCK_SCROLL_OFFSET = 60 / CGUIEPGGridContainerModel::MINSPERBLOCK; // how many blocks are jumped if we are at left/right edge of grid
+
+CGUIEPGGridContainer::CGUIEPGGridContainer(int parentID,
+ int controlID,
+ float posX,
+ float posY,
+ float width,
+ float height,
+ ORIENTATION orientation,
+ int scrollTime,
+ int preloadItems,
+ int timeBlocks,
+ int rulerUnit,
+ const CTextureInfo& progressIndicatorTexture)
+ : IGUIContainer(parentID, controlID, posX, posY, width, height),
+ m_orientation(orientation),
+ m_channelLayout(nullptr),
+ m_focusedChannelLayout(nullptr),
+ m_programmeLayout(nullptr),
+ m_focusedProgrammeLayout(nullptr),
+ m_rulerLayout(nullptr),
+ m_rulerDateLayout(nullptr),
+ m_pageControl(0),
+ m_rulerUnit(rulerUnit),
+ m_channelsPerPage(0),
+ m_programmesPerPage(0),
+ m_channelCursor(0),
+ m_channelOffset(0),
+ m_blocksPerPage(timeBlocks),
+ m_blockCursor(0),
+ m_blockOffset(0),
+ m_blockTravelAxis(0),
+ m_cacheChannelItems(preloadItems),
+ m_cacheProgrammeItems(preloadItems),
+ m_cacheRulerItems(preloadItems),
+ m_rulerDateHeight(0),
+ m_rulerDateWidth(0),
+ m_rulerPosX(0),
+ m_rulerPosY(0),
+ m_rulerHeight(0),
+ m_rulerWidth(0),
+ m_channelPosX(0),
+ m_channelPosY(0),
+ m_channelHeight(0),
+ m_channelWidth(0),
+ m_gridPosX(0),
+ m_gridPosY(0),
+ m_gridWidth(0),
+ m_gridHeight(0),
+ m_blockSize(0),
+ m_analogScrollCount(0),
+ m_guiProgressIndicatorTexture(
+ CGUITexture::CreateTexture(posX, posY, width, height, progressIndicatorTexture)),
+ m_scrollTime(scrollTime ? scrollTime : 1),
+ m_programmeScrollLastTime(0),
+ m_programmeScrollSpeed(0),
+ m_programmeScrollOffset(0),
+ m_channelScrollLastTime(0),
+ m_channelScrollSpeed(0),
+ m_channelScrollOffset(0),
+ m_gridModel(new CGUIEPGGridContainerModel)
+{
+ ControlType = GUICONTAINER_EPGGRID;
+}
+
+CGUIEPGGridContainer::CGUIEPGGridContainer(const CGUIEPGGridContainer& other)
+ : IGUIContainer(other),
+ m_renderOffset(other.m_renderOffset),
+ m_orientation(other.m_orientation),
+ m_channelLayouts(other.m_channelLayouts),
+ m_focusedChannelLayouts(other.m_focusedChannelLayouts),
+ m_focusedProgrammeLayouts(other.m_focusedProgrammeLayouts),
+ m_programmeLayouts(other.m_programmeLayouts),
+ m_rulerLayouts(other.m_rulerLayouts),
+ m_rulerDateLayouts(other.m_rulerDateLayouts),
+ m_channelLayout(other.m_channelLayout),
+ m_focusedChannelLayout(other.m_focusedChannelLayout),
+ m_programmeLayout(other.m_programmeLayout),
+ m_focusedProgrammeLayout(other.m_focusedProgrammeLayout),
+ m_rulerLayout(other.m_rulerLayout),
+ m_rulerDateLayout(other.m_rulerDateLayout),
+ m_pageControl(other.m_pageControl),
+ m_rulerUnit(other.m_rulerUnit),
+ m_channelsPerPage(other.m_channelsPerPage),
+ m_programmesPerPage(other.m_programmesPerPage),
+ m_channelCursor(other.m_channelCursor),
+ m_channelOffset(other.m_channelOffset),
+ m_blocksPerPage(other.m_blocksPerPage),
+ m_blockCursor(other.m_blockCursor),
+ m_blockOffset(other.m_blockOffset),
+ m_blockTravelAxis(other.m_blockTravelAxis),
+ m_cacheChannelItems(other.m_cacheChannelItems),
+ m_cacheProgrammeItems(other.m_cacheProgrammeItems),
+ m_cacheRulerItems(other.m_cacheRulerItems),
+ m_rulerDateHeight(other.m_rulerDateHeight),
+ m_rulerDateWidth(other.m_rulerDateWidth),
+ m_rulerPosX(other.m_rulerPosX),
+ m_rulerPosY(other.m_rulerPosY),
+ m_rulerHeight(other.m_rulerHeight),
+ m_rulerWidth(other.m_rulerWidth),
+ m_channelPosX(other.m_channelPosX),
+ m_channelPosY(other.m_channelPosY),
+ m_channelHeight(other.m_channelHeight),
+ m_channelWidth(other.m_channelWidth),
+ m_gridPosX(other.m_gridPosX),
+ m_gridPosY(other.m_gridPosY),
+ m_gridWidth(other.m_gridWidth),
+ m_gridHeight(other.m_gridHeight),
+ m_blockSize(other.m_blockSize),
+ m_analogScrollCount(other.m_analogScrollCount),
+ m_guiProgressIndicatorTexture(other.m_guiProgressIndicatorTexture->Clone()),
+ m_lastItem(other.m_lastItem),
+ m_lastChannel(other.m_lastChannel),
+ m_scrollTime(other.m_scrollTime),
+ m_programmeScrollLastTime(other.m_programmeScrollLastTime),
+ m_programmeScrollSpeed(other.m_programmeScrollSpeed),
+ m_programmeScrollOffset(other.m_programmeScrollOffset),
+ m_channelScrollLastTime(other.m_channelScrollLastTime),
+ m_channelScrollSpeed(other.m_channelScrollSpeed),
+ m_channelScrollOffset(other.m_channelScrollOffset),
+ m_gridModel(new CGUIEPGGridContainerModel(*other.m_gridModel)),
+ m_updatedGridModel(other.m_updatedGridModel
+ ? new CGUIEPGGridContainerModel(*other.m_updatedGridModel)
+ : nullptr),
+ m_itemStartBlock(other.m_itemStartBlock)
+{
+}
+
+bool CGUIEPGGridContainer::HasData() const
+{
+ return m_gridModel && m_gridModel->HasChannelItems();
+}
+
+void CGUIEPGGridContainer::AllocResources()
+{
+ IGUIContainer::AllocResources();
+ m_guiProgressIndicatorTexture->AllocResources();
+}
+
+void CGUIEPGGridContainer::FreeResources(bool immediately)
+{
+ m_guiProgressIndicatorTexture->FreeResources(immediately);
+ IGUIContainer::FreeResources(immediately);
+}
+
+void CGUIEPGGridContainer::SetPageControl(int id)
+{
+ m_pageControl = id;
+}
+
+void CGUIEPGGridContainer::Process(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ ValidateOffset();
+
+ if (m_bInvalidated)
+ {
+ UpdateLayout();
+
+ if (m_pageControl)
+ {
+ int iItemsPerPage;
+ int iTotalItems;
+
+ if (m_orientation == VERTICAL)
+ {
+ iItemsPerPage = m_channelsPerPage;
+ iTotalItems = m_gridModel->ChannelItemsSize();
+ }
+ else
+ {
+ iItemsPerPage = m_blocksPerPage;
+ iTotalItems = m_gridModel->GridItemsSize();
+ }
+
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), m_pageControl, iItemsPerPage, iTotalItems);
+ SendWindowMessage(msg);
+ }
+ }
+
+ UpdateScrollOffset(currentTime);
+ ProcessChannels(currentTime, dirtyregions);
+ ProcessRulerDate(currentTime, dirtyregions);
+ ProcessRuler(currentTime, dirtyregions);
+ ProcessProgrammeGrid(currentTime, dirtyregions);
+ ProcessProgressIndicator(currentTime, dirtyregions);
+
+ if (m_pageControl)
+ {
+ int iItem =
+ (m_orientation == VERTICAL)
+ ? MathUtils::round_int(static_cast<double>(m_channelScrollOffset / m_channelHeight))
+ : MathUtils::round_int(
+ static_cast<double>(m_programmeScrollOffset / (m_gridHeight / m_blocksPerPage)));
+
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), m_pageControl, iItem);
+ SendWindowMessage(msg);
+ }
+
+ CGUIControl::Process(currentTime, dirtyregions);
+}
+
+void CGUIEPGGridContainer::Render()
+{
+ RenderChannels();
+ RenderRulerDate();
+ RenderRuler();
+ RenderProgrammeGrid();
+ RenderProgressIndicator();
+
+ CGUIControl::Render();
+}
+
+void CGUIEPGGridContainer::ProcessChannels(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ HandleChannels(false, currentTime, dirtyregions);
+}
+
+void CGUIEPGGridContainer::RenderChannels()
+{
+ // params not needed for render.
+ unsigned int dummyTime = 0;
+ CDirtyRegionList dummyRegions;
+ HandleChannels(true, dummyTime, dummyRegions);
+}
+
+void CGUIEPGGridContainer::ProcessRulerDate(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ HandleRulerDate(false, currentTime, dirtyregions);
+}
+
+void CGUIEPGGridContainer::RenderRulerDate()
+{
+ // params not needed for render.
+ unsigned int dummyTime = 0;
+ CDirtyRegionList dummyRegions;
+ HandleRulerDate(true, dummyTime, dummyRegions);
+}
+
+void CGUIEPGGridContainer::ProcessRuler(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ HandleRuler(false, currentTime, dirtyregions);
+}
+
+void CGUIEPGGridContainer::RenderRuler()
+{
+ // params not needed for render.
+ unsigned int dummyTime = 0;
+ CDirtyRegionList dummyRegions;
+ HandleRuler(true, dummyTime, dummyRegions);
+}
+
+void CGUIEPGGridContainer::ProcessProgrammeGrid(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ HandleProgrammeGrid(false, currentTime, dirtyregions);
+}
+
+void CGUIEPGGridContainer::RenderProgrammeGrid()
+{
+ // params not needed for render.
+ unsigned int dummyTime = 0;
+ CDirtyRegionList dummyRegions;
+ HandleProgrammeGrid(true, dummyTime, dummyRegions);
+}
+
+float CGUIEPGGridContainer::GetCurrentTimePositionOnPage() const
+{
+ if (!m_gridModel->GetGridStart().IsValid())
+ return -1.0f;
+
+ const CDateTimeSpan startDelta(CDateTime::GetUTCDateTime() - m_gridModel->GetGridStart());
+ const float fPos = (startDelta.GetSecondsTotal() * m_blockSize) /
+ (CGUIEPGGridContainerModel::MINSPERBLOCK * 60) -
+ GetProgrammeScrollOffsetPos();
+ return std::min(fPos, m_orientation == VERTICAL ? m_gridWidth : m_gridHeight);
+}
+
+float CGUIEPGGridContainer::GetProgressIndicatorWidth() const
+{
+ return (m_orientation == VERTICAL) ? GetCurrentTimePositionOnPage() : m_rulerWidth + m_gridWidth;
+}
+
+float CGUIEPGGridContainer::GetProgressIndicatorHeight() const
+{
+ return (m_orientation == VERTICAL) ? m_rulerHeight + m_gridHeight : GetCurrentTimePositionOnPage();
+}
+
+void CGUIEPGGridContainer::ProcessProgressIndicator(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ float width = GetProgressIndicatorWidth();
+ float height = GetProgressIndicatorHeight();
+
+ if (width > 0 && height > 0)
+ {
+ m_guiProgressIndicatorTexture->SetVisible(true);
+ m_guiProgressIndicatorTexture->SetPosition(m_rulerPosX + m_renderOffset.x,
+ m_rulerPosY + m_renderOffset.y);
+ m_guiProgressIndicatorTexture->SetWidth(width);
+ m_guiProgressIndicatorTexture->SetHeight(height);
+ }
+ else
+ {
+ m_guiProgressIndicatorTexture->SetVisible(false);
+ }
+
+ m_guiProgressIndicatorTexture->Process(currentTime);
+}
+
+void CGUIEPGGridContainer::RenderProgressIndicator()
+{
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, GetProgressIndicatorWidth(), GetProgressIndicatorHeight()))
+ {
+ m_guiProgressIndicatorTexture->SetDiffuseColor(m_diffuseColor);
+ m_guiProgressIndicatorTexture->Render();
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+}
+
+void CGUIEPGGridContainer::ProcessItem(float posX, float posY, const CFileItemPtr& item, CFileItemPtr& lastitem,
+ bool focused, CGUIListItemLayout* normallayout, CGUIListItemLayout* focusedlayout,
+ unsigned int currentTime, CDirtyRegionList& dirtyregions, float resize /* = -1.0f */)
+{
+ if (!normallayout || !focusedlayout)
+ return;
+
+ // set the origin
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(posX, posY);
+
+ if (m_bInvalidated)
+ item->SetInvalid();
+
+ if (focused)
+ {
+ if (!item->GetFocusedLayout())
+ {
+ item->SetFocusedLayout(std::make_unique<CGUIListItemLayout>(*focusedlayout, this));
+ }
+
+ if (resize != -1.0f)
+ {
+ if (m_orientation == VERTICAL)
+ item->GetFocusedLayout()->SetWidth(resize);
+ else
+ item->GetFocusedLayout()->SetHeight(resize);
+ }
+
+ if (item != lastitem || !HasFocus())
+ item->GetFocusedLayout()->SetFocusedItem(0);
+
+ if (item != lastitem && HasFocus())
+ {
+ item->GetFocusedLayout()->ResetAnimation(ANIM_TYPE_UNFOCUS);
+
+ unsigned int subItem = 1;
+ if (lastitem && lastitem->GetFocusedLayout())
+ subItem = lastitem->GetFocusedLayout()->GetFocusedItem();
+
+ item->GetFocusedLayout()->SetFocusedItem(subItem ? subItem : 1);
+ }
+
+ item->GetFocusedLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions);
+ lastitem = item;
+ }
+ else
+ {
+ if (!item->GetLayout())
+ {
+ item->SetLayout(std::make_unique<CGUIListItemLayout>(*normallayout, this));
+ }
+
+ if (resize != -1.0f)
+ {
+ if (m_orientation == VERTICAL)
+ item->GetLayout()->SetWidth(resize);
+ else
+ item->GetLayout()->SetHeight(resize);
+ }
+
+ if (item->GetFocusedLayout())
+ item->GetFocusedLayout()->SetFocusedItem(0);
+
+ if (item->GetFocusedLayout() && item->GetFocusedLayout()->IsAnimating(ANIM_TYPE_UNFOCUS))
+ item->GetFocusedLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions);
+ else
+ item->GetLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions);
+ }
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
+}
+
+void CGUIEPGGridContainer::RenderItem(float posX, float posY, CGUIListItem* item, bool focused)
+{
+ // set the origin
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(posX, posY);
+
+ if (focused)
+ {
+ if (item->GetFocusedLayout())
+ item->GetFocusedLayout()->Render(item, m_parentID);
+ }
+ else
+ {
+ if (item->GetFocusedLayout() && item->GetFocusedLayout()->IsAnimating(ANIM_TYPE_UNFOCUS))
+ item->GetFocusedLayout()->Render(item, m_parentID);
+ else if (item->GetLayout())
+ item->GetLayout()->Render(item, m_parentID);
+ }
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
+}
+
+bool CGUIEPGGridContainer::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_MOVE_LEFT:
+ case ACTION_MOVE_RIGHT:
+ case ACTION_MOVE_DOWN:
+ case ACTION_MOVE_UP:
+ case ACTION_NAV_BACK:
+ // use base class implementation
+ return CGUIControl::OnAction(action);
+
+ case ACTION_NEXT_ITEM:
+ // skip +12h
+ ScrollToBlockOffset(m_blockOffset + (12 * 60 / CGUIEPGGridContainerModel::MINSPERBLOCK));
+ SetBlock(m_blockCursor);
+ return true;
+
+ case ACTION_PREV_ITEM:
+ // skip -12h
+ ScrollToBlockOffset(m_blockOffset - (12 * 60 / CGUIEPGGridContainerModel::MINSPERBLOCK));
+ SetBlock(m_blockCursor);
+ return true;
+
+ case REMOTE_0:
+ GoToNow();
+ return true;
+
+ case ACTION_PAGE_UP:
+ if (m_orientation == VERTICAL)
+ {
+ if (m_channelOffset == 0)
+ {
+ // already on the first page, so move to the first item
+ SetChannel(0);
+ }
+ else
+ {
+ // scroll up to the previous page
+ ChannelScroll(-m_channelsPerPage);
+ }
+ }
+ else
+ {
+ if (m_blockOffset == 0)
+ {
+ // already on the first page, so move to the first item
+ SetBlock(0);
+ }
+ else
+ {
+ // scroll up to the previous page
+ ProgrammesScroll(-m_blocksPerPage);
+ }
+ }
+ return true;
+
+ case ACTION_PAGE_DOWN:
+ if (m_orientation == VERTICAL)
+ {
+ if (m_channelOffset == m_gridModel->ChannelItemsSize() - m_channelsPerPage ||
+ m_gridModel->ChannelItemsSize() < m_channelsPerPage)
+ {
+ // already at the last page, so move to the last item.
+ SetChannel(m_gridModel->GetLastChannel() - m_channelOffset);
+ }
+ else
+ {
+ // scroll down to the next page
+ ChannelScroll(m_channelsPerPage);
+ }
+ }
+ else
+ {
+ if (m_blockOffset == m_gridModel->GridItemsSize() - m_blocksPerPage ||
+ m_gridModel->GridItemsSize() < m_blocksPerPage)
+ {
+ // already at the last page, so move to the last item.
+ SetBlock(m_gridModel->GetLastBlock() - m_blockOffset);
+ }
+ else
+ {
+ // scroll down to the next page
+ ProgrammesScroll(m_blocksPerPage);
+ }
+ }
+
+ return true;
+
+ // smooth scrolling (for analog controls)
+ case ACTION_TELETEXT_RED:
+ case ACTION_TELETEXT_GREEN:
+ case ACTION_SCROLL_UP: // left horizontal scrolling
+ if (m_orientation == VERTICAL)
+ {
+ int blocksToJump = action.GetID() == ACTION_TELETEXT_RED ? m_blocksPerPage / 2 : m_blocksPerPage / 4;
+
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+
+ if (m_blockOffset > 0 && m_blockCursor <= m_blocksPerPage / 2)
+ ProgrammesScroll(-blocksToJump);
+ else if (m_blockCursor > blocksToJump)
+ SetBlock(m_blockCursor - blocksToJump);
+ }
+ return handled;
+ }
+ else
+ {
+ int channelsToJump = action.GetID() == ACTION_TELETEXT_RED ? m_channelsPerPage / 2 : m_channelsPerPage / 4;
+
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+
+ if (m_channelOffset > 0 && m_channelCursor <= m_channelsPerPage / 2)
+ ChannelScroll(-channelsToJump);
+ else if (m_channelCursor > channelsToJump)
+ SetChannel(m_channelCursor - channelsToJump);
+ }
+ return handled;
+ }
+ break;
+
+ case ACTION_TELETEXT_BLUE:
+ case ACTION_TELETEXT_YELLOW:
+ case ACTION_SCROLL_DOWN: // right horizontal scrolling
+ if (m_orientation == VERTICAL)
+ {
+ int blocksToJump = action.GetID() == ACTION_TELETEXT_BLUE ? m_blocksPerPage / 2 : m_blocksPerPage / 4;
+
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+
+ if (m_blockOffset + m_blocksPerPage < m_gridModel->GridItemsSize() &&
+ m_blockCursor >= m_blocksPerPage / 2)
+ ProgrammesScroll(blocksToJump);
+ else if (m_blockCursor < m_blocksPerPage - blocksToJump &&
+ m_blockOffset + m_blockCursor < m_gridModel->GridItemsSize() - blocksToJump)
+ SetBlock(m_blockCursor + blocksToJump);
+ }
+ return handled;
+ }
+ else
+ {
+ int channelsToJump = action.GetID() == ACTION_TELETEXT_BLUE ? m_channelsPerPage / 2 : m_channelsPerPage / 4;
+
+ m_analogScrollCount += action.GetAmount() * action.GetAmount();
+ bool handled = false;
+
+ while (m_analogScrollCount > 0.4f)
+ {
+ handled = true;
+ m_analogScrollCount -= 0.4f;
+
+ if (m_channelOffset + m_channelsPerPage < m_gridModel->ChannelItemsSize() && m_channelCursor >= m_channelsPerPage / 2)
+ ChannelScroll(channelsToJump);
+ else if (m_channelCursor < m_channelsPerPage - channelsToJump && m_channelOffset + m_channelCursor < m_gridModel->ChannelItemsSize() - channelsToJump)
+ SetChannel(m_channelCursor + channelsToJump);
+ }
+ return handled;
+ }
+ break;
+
+ default:
+ if (action.GetID())
+ return OnClick(action.GetID());
+
+ break;
+ }
+
+ return false;
+}
+
+bool CGUIEPGGridContainer::OnMessage(CGUIMessage& message)
+{
+ if (message.GetControlId() == GetID())
+ {
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_PAGE_CHANGE:
+ if (message.GetSenderId() == m_pageControl && IsVisible())
+ {
+ if (m_orientation == VERTICAL)
+ {
+ ScrollToChannelOffset(message.GetParam1());
+ SetChannel(m_channelCursor);
+ }
+ else
+ {
+ ScrollToBlockOffset(message.GetParam1());
+ SetBlock(m_blockCursor);
+ }
+ return true;
+ }
+ break;
+
+ case GUI_MSG_LABEL_BIND:
+ UpdateItems();
+ return true;
+
+ case GUI_MSG_REFRESH_LIST:
+ // update our list contents
+ m_gridModel->SetInvalid();
+ break;
+ }
+ }
+
+ return CGUIControl::OnMessage(message);
+}
+
+void CGUIEPGGridContainer::UpdateItems()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_updatedGridModel)
+ return;
+
+ // Save currently selected epg tag and grid coordinates. Selection shall be restored after update.
+ std::shared_ptr<CPVREpgInfoTag> prevSelectedEpgTag;
+ if (HasData())
+ prevSelectedEpgTag =
+ m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset)
+ ->GetEPGInfoTag();
+
+ const int oldChannelIndex = m_channelOffset + m_channelCursor;
+ const int oldBlockIndex = m_blockOffset + m_blockCursor;
+ const CDateTime oldGridStart(m_gridModel->GetGridStart());
+ int eventOffset = oldBlockIndex;
+ int newChannelIndex = oldChannelIndex;
+ int newBlockIndex = oldBlockIndex;
+ int channelUid = -1;
+ unsigned int broadcastUid = 0;
+
+ if (prevSelectedEpgTag)
+ {
+ // get the block offset relative to the first block of the selected event
+ eventOffset =
+ oldBlockIndex - m_gridModel->GetGridItemStartBlock(oldChannelIndex, oldBlockIndex);
+
+ if (!prevSelectedEpgTag->IsGapTag()) // "normal" tag selected
+ {
+ if (oldGridStart >= prevSelectedEpgTag->StartAsUTC())
+ {
+ // start of previously selected event is before grid start
+ newBlockIndex = eventOffset;
+ }
+ else
+ {
+ newBlockIndex = m_gridModel->GetFirstEventBlock(prevSelectedEpgTag) + eventOffset;
+ }
+
+ channelUid = prevSelectedEpgTag->UniqueChannelID();
+ broadcastUid = prevSelectedEpgTag->UniqueBroadcastID();
+ }
+ else // "gap" tag selected
+ {
+ channelUid = prevSelectedEpgTag->UniqueChannelID();
+
+ // As gap tags do not have a unique broadcast id, we will look for the real tag preceding
+ // the gap tag and add the respective offset to restore the gap tag selection, assuming that
+ // the real tag is still the predecessor of the gap tag after the grid model update.
+
+ const std::shared_ptr<CFileItem> prevItem = GetPrevItem().first;
+ if (prevItem)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> tag = prevItem->GetEPGInfoTag();
+ if (tag && !tag->IsGapTag())
+ {
+ if (oldGridStart >= tag->StartAsUTC())
+ {
+ // start of previously selected event is before grid start
+ newBlockIndex = eventOffset;
+ }
+ else
+ {
+ newBlockIndex = m_gridModel->GetFirstEventBlock(tag);
+ eventOffset += m_gridModel->GetFirstEventBlock(prevSelectedEpgTag) - newBlockIndex;
+ }
+
+ broadcastUid = tag->UniqueBroadcastID();
+ }
+ }
+ }
+ }
+
+ m_lastItem = nullptr;
+ m_lastChannel = nullptr;
+
+ // always use asynchronously precalculated grid data.
+ m_gridModel = std::move(m_updatedGridModel);
+
+ if (prevSelectedEpgTag)
+ {
+ if (oldGridStart != m_gridModel->GetGridStart())
+ {
+ // grid start changed. block offset for selected event might have changed.
+ newBlockIndex += m_gridModel->GetBlock(oldGridStart);
+ if (newBlockIndex < 0 || newBlockIndex > m_gridModel->GetLastBlock())
+ {
+ // previous selection is no longer in grid.
+ SetInvalid();
+ m_bEnableChannelScrolling = false;
+ GoToChannel(newChannelIndex);
+ m_bEnableProgrammeScrolling = false;
+ GoToNow();
+ return;
+ }
+ }
+
+ if (newChannelIndex >= m_gridModel->ChannelItemsSize() ||
+ newBlockIndex >= m_gridModel->GridItemsSize() ||
+ m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag() !=
+ prevSelectedEpgTag)
+ {
+ int iChannelIndex = CGUIEPGGridContainerModel::INVALID_INDEX;
+ int iBlockIndex = CGUIEPGGridContainerModel::INVALID_INDEX;
+ m_gridModel->FindChannelAndBlockIndex(channelUid, broadcastUid, eventOffset, iChannelIndex, iBlockIndex);
+
+ if (iBlockIndex != CGUIEPGGridContainerModel::INVALID_INDEX)
+ {
+ newBlockIndex = iBlockIndex;
+ }
+ else if (newBlockIndex > m_gridModel->GetLastBlock())
+ {
+ // default to now
+ newBlockIndex = m_gridModel->GetNowBlock();
+
+ if (newBlockIndex > m_gridModel->GetLastBlock())
+ {
+ // last block is in the past. default to last block
+ newBlockIndex = m_gridModel->GetLastBlock();
+ }
+ }
+
+ if (iChannelIndex != CGUIEPGGridContainerModel::INVALID_INDEX)
+ {
+ newChannelIndex = iChannelIndex;
+ }
+ else if (newChannelIndex >= m_gridModel->ChannelItemsSize() ||
+ (m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag()->UniqueChannelID() != prevSelectedEpgTag->UniqueChannelID() &&
+ m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag()->ClientID() != prevSelectedEpgTag->ClientID()))
+ {
+ // default to first channel
+ newChannelIndex = 0;
+ }
+ }
+
+ // restore previous selection.
+ if (newChannelIndex == oldChannelIndex && newBlockIndex == oldBlockIndex)
+ {
+ // same coordinates, keep current grid view port
+ UpdateItem();
+ }
+ else
+ {
+ // new coordinates, move grid view port accordingly
+ SetInvalid();
+
+ if (newBlockIndex != oldBlockIndex)
+ {
+ m_bEnableProgrammeScrolling = false;
+ GoToBlock(newBlockIndex);
+ }
+
+ if (newChannelIndex != oldChannelIndex)
+ {
+ m_bEnableChannelScrolling = false;
+ GoToChannel(newChannelIndex);
+ }
+ }
+ }
+ else
+ {
+ // no previous selection, goto now
+ SetInvalid();
+ m_bEnableProgrammeScrolling = false;
+ GoToNow();
+ }
+}
+
+float CGUIEPGGridContainer::GetChannelScrollOffsetPos() const
+{
+ if (m_bEnableChannelScrolling)
+ return m_channelScrollOffset;
+ else
+ return m_channelOffset * m_channelLayout->Size(m_orientation);
+}
+
+float CGUIEPGGridContainer::GetProgrammeScrollOffsetPos() const
+{
+ if (m_bEnableProgrammeScrolling)
+ return m_programmeScrollOffset;
+ else
+ return m_blockOffset * m_blockSize;
+}
+
+int CGUIEPGGridContainer::GetChannelScrollOffset(CGUIListItemLayout* layout) const
+{
+ if (m_bEnableChannelScrolling)
+ return MathUtils::round_int(
+ static_cast<double>(m_channelScrollOffset / layout->Size(m_orientation)));
+ else
+ return m_channelOffset;
+}
+
+int CGUIEPGGridContainer::GetProgrammeScrollOffset() const
+{
+ if (m_bEnableProgrammeScrolling)
+ return MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize));
+ else
+ return m_blockOffset;
+}
+
+void CGUIEPGGridContainer::ChannelScroll(int amount)
+{
+ // increase or decrease the vertical offset
+ int offset = m_channelOffset + amount;
+
+ if (offset > m_gridModel->ChannelItemsSize() - m_channelsPerPage)
+ offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage;
+
+ if (offset < 0)
+ offset = 0;
+
+ ScrollToChannelOffset(offset);
+ SetChannel(m_channelCursor);
+}
+
+void CGUIEPGGridContainer::ProgrammesScroll(int amount)
+{
+ // increase or decrease the horizontal offset
+ ScrollToBlockOffset(m_blockOffset + amount);
+ SetBlock(m_blockCursor);
+}
+
+void CGUIEPGGridContainer::OnUp()
+{
+ if (!HasData())
+ return CGUIControl::OnUp();
+
+ if (m_orientation == VERTICAL)
+ {
+ CGUIAction action = GetAction(ACTION_MOVE_UP);
+ if (m_channelCursor > 0)
+ {
+ SetChannel(m_channelCursor - 1);
+ }
+ else if (m_channelCursor == 0 && m_channelOffset)
+ {
+ ScrollToChannelOffset(m_channelOffset - 1);
+ SetChannel(0);
+ }
+ else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around
+ {
+ int offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage;
+
+ if (offset < 0)
+ offset = 0;
+
+ SetChannel(m_gridModel->GetLastChannel() - offset);
+ ScrollToChannelOffset(offset);
+ }
+ else
+ CGUIControl::OnUp();
+ }
+ else
+ {
+ if (m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset) > m_blockOffset)
+ {
+ // this is not first item on page
+ SetItem(GetPrevItem());
+ UpdateBlock();
+ return;
+ }
+ else if (m_blockCursor <= 0 && m_blockOffset && m_blockOffset - BLOCK_SCROLL_OFFSET >= 0)
+ {
+ // this is the first item on page
+ ScrollToBlockOffset(m_blockOffset - BLOCK_SCROLL_OFFSET);
+ UpdateBlock();
+ return;
+ }
+
+ CGUIControl::OnUp();
+ }
+}
+
+void CGUIEPGGridContainer::OnDown()
+{
+ if (!HasData())
+ return CGUIControl::OnDown();
+
+ if (m_orientation == VERTICAL)
+ {
+ CGUIAction action = GetAction(ACTION_MOVE_DOWN);
+ if (m_channelOffset + m_channelCursor < m_gridModel->GetLastChannel())
+ {
+ if (m_channelCursor + 1 < m_channelsPerPage)
+ {
+ SetChannel(m_channelCursor + 1);
+ }
+ else
+ {
+ ScrollToChannelOffset(m_channelOffset + 1);
+ SetChannel(m_channelsPerPage - 1);
+ }
+ }
+ else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around
+ {
+ ScrollToChannelOffset(0);
+ SetChannel(0);
+ }
+ else
+ CGUIControl::OnDown();
+ }
+ else
+ {
+ if (m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset) <
+ (m_blockOffset + m_blocksPerPage - 1))
+ {
+ // this is not last item on page
+ SetItem(GetNextItem());
+ UpdateBlock();
+ return;
+ }
+ else if ((m_blockOffset != m_gridModel->GridItemsSize() - m_blocksPerPage) &&
+ m_gridModel->GridItemsSize() > m_blocksPerPage &&
+ m_blockOffset + BLOCK_SCROLL_OFFSET < m_gridModel->GetLastBlock())
+ {
+ // this is the last item on page
+ ScrollToBlockOffset(m_blockOffset + BLOCK_SCROLL_OFFSET);
+ UpdateBlock();
+ return;
+ }
+
+ CGUIControl::OnDown();
+ }
+}
+
+void CGUIEPGGridContainer::OnLeft()
+{
+ if (!HasData())
+ return CGUIControl::OnLeft();
+
+ if (m_orientation == VERTICAL)
+ {
+ if (m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset) > m_blockOffset)
+ {
+ // this is not first item on page
+ SetItem(GetPrevItem());
+ UpdateBlock();
+ return;
+ }
+ else if (m_blockCursor <= 0 && m_blockOffset && m_blockOffset - BLOCK_SCROLL_OFFSET >= 0)
+ {
+ // this is the first item on page
+ ScrollToBlockOffset(m_blockOffset - BLOCK_SCROLL_OFFSET);
+ UpdateBlock();
+ return;
+ }
+
+ CGUIControl::OnLeft();
+ }
+ else
+ {
+ CGUIAction action = GetAction(ACTION_MOVE_LEFT);
+ if (m_channelCursor > 0)
+ {
+ SetChannel(m_channelCursor - 1);
+ }
+ else if (m_channelCursor == 0 && m_channelOffset)
+ {
+ ScrollToChannelOffset(m_channelOffset - 1);
+ SetChannel(0);
+ }
+ else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around
+ {
+ int offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage;
+
+ if (offset < 0)
+ offset = 0;
+
+ SetChannel(m_gridModel->GetLastChannel() - offset);
+ ScrollToChannelOffset(offset);
+ }
+ else
+ CGUIControl::OnLeft();
+ }
+}
+
+void CGUIEPGGridContainer::OnRight()
+{
+ if (!HasData())
+ return CGUIControl::OnRight();
+
+ if (m_orientation == VERTICAL)
+ {
+ if (m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset) <
+ (m_blockOffset + m_blocksPerPage - 1))
+ {
+ // this is not last item on page
+ SetItem(GetNextItem());
+ UpdateBlock();
+ return;
+ }
+ else if ((m_blockOffset != m_gridModel->GridItemsSize() - m_blocksPerPage) &&
+ m_gridModel->GridItemsSize() > m_blocksPerPage &&
+ m_blockOffset + BLOCK_SCROLL_OFFSET < m_gridModel->GetLastBlock())
+ {
+ // this is the last item on page
+ ScrollToBlockOffset(m_blockOffset + BLOCK_SCROLL_OFFSET);
+ UpdateBlock();
+ return;
+ }
+
+ CGUIControl::OnRight();
+ }
+ else
+ {
+ CGUIAction action = GetAction(ACTION_MOVE_RIGHT);
+ if (m_channelOffset + m_channelCursor < m_gridModel->GetLastChannel())
+ {
+ if (m_channelCursor + 1 < m_channelsPerPage)
+ {
+ SetChannel(m_channelCursor + 1);
+ }
+ else
+ {
+ ScrollToChannelOffset(m_channelOffset + 1);
+ SetChannel(m_channelsPerPage - 1);
+ }
+ }
+ else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around
+ {
+ SetChannel(0);
+ ScrollToChannelOffset(0);
+ }
+ else
+ CGUIControl::OnRight();
+ }
+}
+
+bool CGUIEPGGridContainer::SetChannel(const std::string& channel)
+{
+ for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++)
+ {
+ std::string strPath = m_gridModel->GetChannelItem(iIndex)->GetProperty("path").asString();
+ if (strPath == channel)
+ {
+ GoToChannel(iIndex);
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIEPGGridContainer::SetChannel(const std::shared_ptr<CPVRChannel>& channel)
+{
+ for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++)
+ {
+ int iChannelId = static_cast<int>(m_gridModel->GetChannelItem(iIndex)->GetProperty("channelid").asInteger(-1));
+ if (iChannelId == channel->ChannelID())
+ {
+ GoToChannel(iIndex);
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIEPGGridContainer::SetChannel(const CPVRChannelNumber& channelNumber)
+{
+ for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++)
+ {
+ const CPVRChannelNumber& number =
+ m_gridModel->GetChannelItem(iIndex)->GetPVRChannelGroupMemberInfoTag()->ChannelNumber();
+ if (number == channelNumber)
+ {
+ GoToChannel(iIndex);
+ return true;
+ }
+ }
+ return false;
+}
+
+void CGUIEPGGridContainer::SetChannel(int channel)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ int channelIndex = channel + m_channelOffset;
+ int blockIndex = m_blockCursor + m_blockOffset;
+ if (channelIndex < m_gridModel->ChannelItemsSize() && blockIndex < m_gridModel->GridItemsSize())
+ {
+ if (SetItem(m_gridModel->GetGridItem(channelIndex, m_blockTravelAxis), channelIndex,
+ m_blockTravelAxis))
+ {
+ m_channelCursor = channel;
+ MarkDirtyRegion();
+ UpdateBlock(false);
+ }
+ }
+}
+
+void CGUIEPGGridContainer::SetBlock(int block, bool bUpdateBlockTravelAxis /* = true */)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (block < 0)
+ m_blockCursor = 0;
+ else if (block > m_blocksPerPage - 1)
+ m_blockCursor = m_blocksPerPage - 1;
+ else
+ m_blockCursor = block;
+
+ if (bUpdateBlockTravelAxis)
+ m_blockTravelAxis = m_blockOffset + m_blockCursor;
+
+ UpdateItem();
+ MarkDirtyRegion();
+}
+
+void CGUIEPGGridContainer::UpdateBlock(bool bUpdateBlockTravelAxis /* = true */)
+{
+ SetBlock(m_itemStartBlock > 0 ? m_itemStartBlock - m_blockOffset : 0, bUpdateBlockTravelAxis);
+}
+
+CGUIListItemLayout* CGUIEPGGridContainer::GetFocusedLayout() const
+{
+ CGUIListItemPtr item = GetListItem(0);
+
+ if (item)
+ return item->GetFocusedLayout();
+
+ return nullptr;
+}
+
+bool CGUIEPGGridContainer::SelectItemFromPoint(const CPoint& point, bool justGrid /* = false */)
+{
+ /* point has already had origin set to m_posX, m_posY */
+ if (!m_focusedProgrammeLayout || !m_programmeLayout || (justGrid && point.x < 0))
+ return false;
+
+ int channel;
+ int block;
+
+ if (m_orientation == VERTICAL)
+ {
+ channel = point.y / m_channelHeight;
+ block = point.x / m_blockSize;
+ }
+ else
+ {
+ channel = point.x / m_channelWidth;
+ block = point.y / m_blockSize;
+ }
+
+ if (channel > m_channelsPerPage)
+ channel = m_channelsPerPage - 1;
+
+ if (channel >= m_gridModel->ChannelItemsSize())
+ channel = m_gridModel->GetLastChannel();
+
+ if (channel < 0)
+ channel = 0;
+
+ if (block > m_blocksPerPage)
+ block = m_blocksPerPage - 1;
+
+ if (block < 0)
+ block = 0;
+
+ int channelIndex = channel + m_channelOffset;
+ int blockIndex = block + m_blockOffset;
+
+ // bail if out of range
+ if (channelIndex >= m_gridModel->ChannelItemsSize() || blockIndex >= m_gridModel->GridItemsSize())
+ return false;
+
+ // bail if block isn't occupied
+ if (!m_gridModel->GetGridItem(channelIndex, blockIndex))
+ return false;
+
+ SetChannel(channel);
+ SetBlock(block);
+ return true;
+}
+
+EVENT_RESULT CGUIEPGGridContainer::OnMouseEvent(const CPoint& point, const CMouseEvent& event)
+{
+ switch (event.m_id)
+ {
+ case ACTION_MOUSE_LEFT_CLICK:
+ OnMouseClick(0, point);
+ return EVENT_RESULT_HANDLED;
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnMouseClick(1, point);
+ return EVENT_RESULT_HANDLED;
+ case ACTION_MOUSE_DOUBLE_CLICK:
+ OnMouseDoubleClick(0, point);
+ return EVENT_RESULT_HANDLED;
+ case ACTION_MOUSE_WHEEL_UP:
+ OnMouseWheel(-1, point);
+ return EVENT_RESULT_HANDLED;
+ case ACTION_MOUSE_WHEEL_DOWN:
+ OnMouseWheel(1, point);
+ return EVENT_RESULT_HANDLED;
+ case ACTION_GESTURE_BEGIN:
+ {
+ // we want exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
+ SendWindowMessage(msg);
+ return EVENT_RESULT_HANDLED;
+ }
+ case ACTION_GESTURE_END:
+ case ACTION_GESTURE_ABORT:
+ {
+ // we're done with exclusive access
+ CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
+ SendWindowMessage(msg);
+ ScrollToChannelOffset(MathUtils::round_int(
+ static_cast<double>(m_channelScrollOffset / m_channelLayout->Size(m_orientation))));
+ SetChannel(m_channelCursor);
+ ScrollToBlockOffset(
+ MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize)));
+ SetBlock(m_blockCursor);
+ return EVENT_RESULT_HANDLED;
+ }
+ case ACTION_GESTURE_PAN:
+ {
+ m_programmeScrollOffset -= event.m_offsetX;
+ m_channelScrollOffset -= event.m_offsetY;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_channelOffset = MathUtils::round_int(
+ static_cast<double>(m_channelScrollOffset / m_channelLayout->Size(m_orientation)));
+ m_blockOffset =
+ MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize));
+ ValidateOffset();
+ }
+ return EVENT_RESULT_HANDLED;
+ }
+ default:
+ return EVENT_RESULT_UNHANDLED;
+ }
+}
+
+bool CGUIEPGGridContainer::OnMouseOver(const CPoint& point)
+{
+ // select the item under the pointer
+ SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY), false);
+ return CGUIControl::OnMouseOver(point);
+}
+
+bool CGUIEPGGridContainer::OnMouseClick(int dwButton, const CPoint& point)
+{
+ if (SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY)))
+ {
+ // send click message to window
+ OnClick(ACTION_MOUSE_LEFT_CLICK + dwButton);
+ return true;
+ }
+ return false;
+}
+
+bool CGUIEPGGridContainer::OnMouseDoubleClick(int dwButton, const CPoint& point)
+{
+ if (SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY)))
+ {
+ // send double click message to window
+ OnClick(ACTION_MOUSE_DOUBLE_CLICK + dwButton);
+ return true;
+ }
+ return false;
+}
+
+bool CGUIEPGGridContainer::OnClick(int actionID)
+{
+ int subItem = 0;
+
+ if (actionID == ACTION_SELECT_ITEM || actionID == ACTION_MOUSE_LEFT_CLICK)
+ {
+ // grab the currently focused subitem (if applicable)
+ CGUIListItemLayout* focusedLayout = GetFocusedLayout();
+
+ if (focusedLayout)
+ subItem = focusedLayout->GetFocusedItem();
+ }
+
+ // Don't know what to do, so send to our parent window.
+ CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID(), actionID, subItem);
+ return SendWindowMessage(msg);
+}
+
+bool CGUIEPGGridContainer::OnMouseWheel(char wheel, const CPoint& point)
+{
+ // doesn't work while an item is selected?
+ ProgrammesScroll(-wheel);
+ return true;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CGUIEPGGridContainer::GetSelectedChannelGroupMember() const
+{
+ CFileItemPtr fileItem;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_channelCursor + m_channelOffset < m_gridModel->ChannelItemsSize())
+ fileItem = m_gridModel->GetChannelItem(m_channelCursor + m_channelOffset);
+ }
+
+ if (fileItem)
+ return fileItem->GetPVRChannelGroupMemberInfoTag();
+
+ return {};
+}
+
+CDateTime CGUIEPGGridContainer::GetSelectedDate() const
+{
+ return m_gridModel->GetStartTimeForBlock(m_blockOffset + m_blockCursor);
+}
+
+CFileItemPtr CGUIEPGGridContainer::GetSelectedGridItem(int offset /*= 0*/) const
+{
+ CFileItemPtr item;
+
+ if (m_channelCursor + m_channelOffset + offset < m_gridModel->ChannelItemsSize() &&
+ m_blockCursor + m_blockOffset < m_gridModel->GridItemsSize())
+ item = m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset);
+
+ return item;
+}
+
+
+CGUIListItemPtr CGUIEPGGridContainer::GetListItem(int offset, unsigned int flag) const
+{
+ if (!m_gridModel->HasChannelItems())
+ return CGUIListItemPtr();
+
+ int item = m_channelCursor + m_channelOffset + offset;
+ if (flag & INFOFLAG_LISTITEM_POSITION)
+ item = GetChannelScrollOffset(m_channelLayout);
+
+ if (flag & INFOFLAG_LISTITEM_WRAP)
+ {
+ item %= m_gridModel->ChannelItemsSize();
+ if (item < 0)
+ item += m_gridModel->ChannelItemsSize();
+
+ return m_gridModel->GetChannelItem(item);
+ }
+ else
+ {
+ if (item >= 0 && item < m_gridModel->ChannelItemsSize())
+ return m_gridModel->GetChannelItem(item);
+ }
+ return CGUIListItemPtr();
+}
+
+std::string CGUIEPGGridContainer::GetLabel(int info) const
+{
+ std::string label;
+ switch (info)
+ {
+ case CONTAINER_NUM_PAGES:
+ if (m_channelsPerPage > 0)
+ label = std::to_string((m_gridModel->ChannelItemsSize() + m_channelsPerPage - 1) /
+ m_channelsPerPage);
+ else
+ label = std::to_string(0);
+ break;
+ case CONTAINER_CURRENT_PAGE:
+ if (m_channelsPerPage > 0)
+ label = std::to_string(1 + (m_channelCursor + m_channelOffset) / m_channelsPerPage);
+ else
+ label = std::to_string(1);
+ break;
+ case CONTAINER_POSITION:
+ label = std::to_string(1 + m_channelCursor + m_channelOffset);
+ break;
+ case CONTAINER_NUM_ITEMS:
+ label = std::to_string(m_gridModel->ChannelItemsSize());
+ break;
+ default:
+ break;
+ }
+ return label;
+}
+
+void CGUIEPGGridContainer::SetItem(const std::pair<std::shared_ptr<CFileItem>, int>& itemInfo)
+{
+ SetItem(itemInfo.first, m_channelCursor + m_channelOffset, itemInfo.second);
+}
+
+bool CGUIEPGGridContainer::SetItem(const std::shared_ptr<CFileItem>& item,
+ int channelIndex,
+ int blockIndex)
+{
+ if (item && channelIndex < m_gridModel->ChannelItemsSize() &&
+ blockIndex < m_gridModel->GridItemsSize())
+ {
+ m_itemStartBlock = m_gridModel->GetGridItemStartBlock(channelIndex, blockIndex);
+ return true;
+ }
+ else
+ {
+ m_itemStartBlock = 0;
+ return false;
+ }
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainer::GetItem() const
+{
+ const int channelIndex = m_channelCursor + m_channelOffset;
+ const int blockIndex = m_blockCursor + m_blockOffset;
+
+ if (channelIndex >= m_gridModel->ChannelItemsSize() || blockIndex >= m_gridModel->GridItemsSize())
+ return {};
+
+ return m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset);
+}
+
+std::pair<std::shared_ptr<CFileItem>, int> CGUIEPGGridContainer::GetNextItem() const
+{
+ int block = m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset);
+ if (block < m_gridModel->GridItemsSize())
+ {
+ // first block of next event is one block after end block of selected event
+ block += 1;
+ }
+
+ return {m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, block), block};
+}
+
+std::pair<std::shared_ptr<CFileItem>, int> CGUIEPGGridContainer::GetPrevItem() const
+{
+ int block = m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset,
+ m_blockCursor + m_blockOffset);
+ if (block > 0)
+ {
+ // last block of previous event is one block before start block of selected event
+ block -= 1;
+ }
+
+ return {m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, block), block};
+}
+
+void CGUIEPGGridContainer::UpdateItem()
+{
+ SetItem(GetItem(), m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset);
+}
+
+void CGUIEPGGridContainer::SetFocus(bool focus)
+{
+ if (focus != HasFocus())
+ SetInvalid();
+
+ CGUIControl::SetFocus(focus);
+}
+
+void CGUIEPGGridContainer::ScrollToChannelOffset(int offset)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ float size = m_programmeLayout->Size(m_orientation);
+ int range = m_channelsPerPage / 4;
+
+ if (range <= 0)
+ range = 1;
+
+ if (offset * size < m_channelScrollOffset && m_channelScrollOffset - offset * size > size * range)
+ {
+ // scrolling up, and we're jumping more than 0.5 of a screen
+ m_channelScrollOffset = (offset + range) * size;
+ }
+
+ if (offset * size > m_channelScrollOffset && offset * size - m_channelScrollOffset > size * range)
+ {
+ // scrolling down, and we're jumping more than 0.5 of a screen
+ m_channelScrollOffset = (offset - range) * size;
+ }
+
+ m_channelScrollSpeed = (offset * size - m_channelScrollOffset) / m_scrollTime;
+ m_channelOffset = offset;
+ MarkDirtyRegion();
+}
+
+void CGUIEPGGridContainer::ScrollToBlockOffset(int offset)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // make sure offset is in valid range
+ offset = std::max(0, std::min(offset, m_gridModel->GridItemsSize() - m_blocksPerPage));
+
+ float size = m_blockSize;
+ int range = m_blocksPerPage / 1;
+
+ if (range <= 0)
+ range = 1;
+
+ if (offset * size < m_programmeScrollOffset && m_programmeScrollOffset - offset * size > size * range)
+ {
+ // scrolling left, and we're jumping more than 0.5 of a screen
+ m_programmeScrollOffset = (offset + range) * size;
+ }
+
+ if (offset * size > m_programmeScrollOffset && offset * size - m_programmeScrollOffset > size * range)
+ {
+ // scrolling right, and we're jumping more than 0.5 of a screen
+ m_programmeScrollOffset = (offset - range) * size;
+ }
+
+ m_programmeScrollSpeed = (offset * size - m_programmeScrollOffset) / m_scrollTime;
+ m_blockOffset = offset;
+ MarkDirtyRegion();
+}
+
+void CGUIEPGGridContainer::ValidateOffset()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_programmeLayout)
+ return;
+
+ float pos = (m_orientation == VERTICAL) ? m_channelHeight : m_channelWidth;
+
+ if (m_gridModel->ChannelItemsSize() &&
+ (m_channelOffset > m_gridModel->ChannelItemsSize() - m_channelsPerPage ||
+ m_channelScrollOffset > (m_gridModel->ChannelItemsSize() - m_channelsPerPage) * pos))
+ {
+ m_channelOffset = m_gridModel->ChannelItemsSize() - m_channelsPerPage;
+ m_channelScrollOffset = m_channelOffset * pos;
+ }
+
+ if (m_channelOffset < 0 || m_channelScrollOffset < 0)
+ {
+ m_channelOffset = 0;
+ m_channelScrollOffset = 0;
+ }
+
+ if (m_gridModel->GridItemsSize() &&
+ (m_blockOffset > m_gridModel->GridItemsSize() - m_blocksPerPage ||
+ m_programmeScrollOffset > (m_gridModel->GridItemsSize() - m_blocksPerPage) * m_blockSize))
+ {
+ m_blockOffset = m_gridModel->GridItemsSize() - m_blocksPerPage;
+ m_programmeScrollOffset = m_blockOffset * m_blockSize;
+ }
+
+ if (m_blockOffset < 0 || m_programmeScrollOffset < 0)
+ {
+ m_blockOffset = 0;
+ m_programmeScrollOffset = 0;
+ }
+}
+
+void CGUIEPGGridContainer::LoadLayout(TiXmlElement* layout)
+{
+ /* layouts for the channel column */
+ TiXmlElement* itemElement = layout->FirstChildElement("channellayout");
+ while (itemElement)
+ {
+ m_channelLayouts.emplace_back();
+ m_channelLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("channellayout");
+ }
+ itemElement = layout->FirstChildElement("focusedchannellayout");
+ while (itemElement)
+ {
+ m_focusedChannelLayouts.emplace_back();
+ m_focusedChannelLayouts.back().LoadLayout(itemElement, GetParentID(), true, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("focusedchannellayout");
+ }
+
+ /* layouts for the grid items */
+ itemElement = layout->FirstChildElement("focusedlayout");
+ while (itemElement)
+ {
+ m_focusedProgrammeLayouts.emplace_back();
+ m_focusedProgrammeLayouts.back().LoadLayout(itemElement, GetParentID(), true, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("focusedlayout");
+ }
+ itemElement = layout->FirstChildElement("itemlayout");
+ while (itemElement)
+ {
+ m_programmeLayouts.emplace_back();
+ m_programmeLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("itemlayout");
+ }
+
+ /* layout for the date label for the grid */
+ itemElement = layout->FirstChildElement("rulerdatelayout");
+ while (itemElement)
+ {
+ m_rulerDateLayouts.emplace_back();
+ m_rulerDateLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("rulerdatelayout");
+ }
+
+ /* layout for the timeline for the grid */
+ itemElement = layout->FirstChildElement("rulerlayout");
+ while (itemElement)
+ {
+ m_rulerLayouts.emplace_back();
+ m_rulerLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height);
+ itemElement = itemElement->NextSiblingElement("rulerlayout");
+ }
+
+ UpdateLayout();
+}
+
+std::string CGUIEPGGridContainer::GetDescription() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const int channelIndex = m_channelCursor + m_channelOffset;
+ const int blockIndex = m_blockCursor + m_blockOffset;
+
+ if (channelIndex < m_gridModel->ChannelItemsSize() && blockIndex < m_gridModel->GridItemsSize())
+ {
+ const std::shared_ptr<CFileItem> item = m_gridModel->GetGridItem(channelIndex, blockIndex);
+ if (item)
+ return item->GetLabel();
+ }
+
+ return {};
+}
+
+void CGUIEPGGridContainer::JumpToNow()
+{
+ m_bEnableProgrammeScrolling = false;
+ GoToNow();
+}
+
+void CGUIEPGGridContainer::JumpToDate(const CDateTime& date)
+{
+ m_bEnableProgrammeScrolling = false;
+ GoToDate(date);
+}
+
+void CGUIEPGGridContainer::GoToBegin()
+{
+ ScrollToBlockOffset(0);
+ SetBlock(0);
+}
+
+void CGUIEPGGridContainer::GoToEnd()
+{
+ ScrollToBlockOffset(m_gridModel->GetLastBlock() - m_blocksPerPage + 1);
+ SetBlock(m_blocksPerPage - 1);
+}
+
+void CGUIEPGGridContainer::GoToNow()
+{
+ GoToDate(CDateTime::GetUTCDateTime());
+}
+
+void CGUIEPGGridContainer::GoToDate(const CDateTime& date)
+{
+ unsigned int offset = m_gridModel->GetPageNowOffset();
+ ScrollToBlockOffset(m_gridModel->GetBlock(date) - offset);
+
+ // ensure we're selecting the active event, not its predecessor.
+ const int iChannel = m_channelOffset + m_channelCursor;
+ const int iBlock = m_blockOffset + offset;
+ if (iChannel >= m_gridModel->ChannelItemsSize() || iBlock >= m_gridModel->GridItemsSize() ||
+ m_gridModel->GetGridItemEndTime(iChannel, iBlock) > date)
+ {
+ SetBlock(offset);
+ }
+ else
+ {
+ SetBlock(offset + 1);
+ }
+}
+
+void CGUIEPGGridContainer::GoToFirstChannel()
+{
+ GoToChannel(0);
+}
+
+void CGUIEPGGridContainer::GoToLastChannel()
+{
+ if (m_gridModel->ChannelItemsSize())
+ GoToChannel(m_gridModel->GetLastChannel());
+ else
+ GoToChannel(0);
+}
+
+void CGUIEPGGridContainer::GoToTop()
+{
+ if (m_orientation == VERTICAL)
+ {
+ GoToChannel(0);
+ }
+ else
+ {
+ GoToBlock(0);
+ }
+}
+
+void CGUIEPGGridContainer::GoToBottom()
+{
+ if (m_orientation == VERTICAL)
+ {
+ if (m_gridModel->HasChannelItems())
+ GoToChannel(m_gridModel->GetLastChannel());
+ else
+ GoToChannel(0);
+ }
+ else
+ {
+ if (m_gridModel->GridItemsSize())
+ GoToBlock(m_gridModel->GetLastBlock());
+ else
+ GoToBlock(0);
+ }
+}
+
+void CGUIEPGGridContainer::GoToMostLeft()
+{
+ if (m_orientation == VERTICAL)
+ {
+ GoToBlock(0);
+ }
+ else
+ {
+ GoToChannel(0);
+ }
+}
+
+void CGUIEPGGridContainer::GoToMostRight()
+{
+ if (m_orientation == VERTICAL)
+ {
+ if (m_gridModel->GridItemsSize())
+ GoToBlock(m_gridModel->GetLastBlock());
+ else
+ GoToBlock(0);
+ }
+ else
+ {
+ if (m_gridModel->HasChannelItems())
+ GoToChannel(m_gridModel->GetLastChannel());
+ else
+ GoToChannel(0);
+ }
+}
+
+void CGUIEPGGridContainer::SetTimelineItems(const std::unique_ptr<CFileItemList>& items,
+ const CDateTime& gridStart,
+ const CDateTime& gridEnd)
+{
+ int iRulerUnit;
+ int iFirstChannel;
+ int iChannelsPerPage;
+ int iBlocksPerPage;
+ int iFirstBlock;
+ float fBlockSize;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ iRulerUnit = m_rulerUnit;
+ iFirstChannel = m_channelOffset;
+ iChannelsPerPage = m_channelsPerPage;
+ iFirstBlock = m_blockOffset;
+ iBlocksPerPage = m_blocksPerPage;
+ fBlockSize = m_blockSize;
+ }
+
+ std::unique_ptr<CGUIEPGGridContainerModel> oldUpdatedGridModel;
+ std::unique_ptr<CGUIEPGGridContainerModel> newUpdatedGridModel(new CGUIEPGGridContainerModel);
+
+ newUpdatedGridModel->Initialize(items, gridStart, gridEnd, iFirstChannel, iChannelsPerPage,
+ iFirstBlock, iBlocksPerPage, iRulerUnit, fBlockSize);
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // grid contains CFileItem instances. CFileItem dtor locks global graphics mutex.
+ // by increasing its refcount make sure, old data are not deleted while we're holding own mutex.
+ oldUpdatedGridModel = std::move(m_updatedGridModel);
+
+ m_updatedGridModel = std::move(newUpdatedGridModel);
+ }
+}
+
+std::unique_ptr<CFileItemList> CGUIEPGGridContainer::GetCurrentTimeLineItems() const
+{
+ return m_gridModel->GetCurrentTimeLineItems(m_channelOffset, m_channelsPerPage);
+}
+
+void CGUIEPGGridContainer::GoToChannel(int channelIndex)
+{
+ if (channelIndex < m_channelsPerPage)
+ {
+ // first page
+ ScrollToChannelOffset(0);
+ SetChannel(channelIndex);
+ }
+ else if (channelIndex > m_gridModel->ChannelItemsSize() - m_channelsPerPage)
+ {
+ // last page
+ ScrollToChannelOffset(m_gridModel->ChannelItemsSize() - m_channelsPerPage);
+ SetChannel(channelIndex - (m_gridModel->ChannelItemsSize() - m_channelsPerPage));
+ }
+ else
+ {
+ ScrollToChannelOffset(channelIndex - m_channelCursor);
+ SetChannel(m_channelCursor);
+ }
+}
+
+void CGUIEPGGridContainer::GoToBlock(int blockIndex)
+{
+ int lastPage = m_gridModel->GridItemsSize() - m_blocksPerPage;
+ if (blockIndex > lastPage)
+ {
+ // last page
+ ScrollToBlockOffset(lastPage);
+ SetBlock(blockIndex - lastPage);
+ }
+ else
+ {
+ ScrollToBlockOffset(blockIndex - m_blockCursor);
+ SetBlock(m_blockCursor);
+ }
+}
+
+void CGUIEPGGridContainer::UpdateLayout()
+{
+ CGUIListItemLayout* oldFocusedChannelLayout = m_focusedChannelLayout;
+ CGUIListItemLayout* oldChannelLayout = m_channelLayout;
+ CGUIListItemLayout* oldFocusedProgrammeLayout = m_focusedProgrammeLayout;
+ CGUIListItemLayout* oldProgrammeLayout = m_programmeLayout;
+ CGUIListItemLayout* oldRulerLayout = m_rulerLayout;
+ CGUIListItemLayout* oldRulerDateLayout = m_rulerDateLayout;
+
+ GetCurrentLayouts();
+
+ // Note: m_rulerDateLayout is optional
+ if (!m_focusedProgrammeLayout || !m_programmeLayout || !m_focusedChannelLayout || !m_channelLayout || !m_rulerLayout)
+ return;
+
+ if (oldChannelLayout == m_channelLayout && oldFocusedChannelLayout == m_focusedChannelLayout &&
+ oldProgrammeLayout == m_programmeLayout && oldFocusedProgrammeLayout == m_focusedProgrammeLayout &&
+ oldRulerLayout == m_rulerLayout && oldRulerDateLayout == m_rulerDateLayout)
+ return; // nothing has changed, so don't update stuff
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_channelHeight = m_channelLayout->Size(VERTICAL);
+ m_channelWidth = m_channelLayout->Size(HORIZONTAL);
+
+ m_rulerDateHeight = m_rulerDateLayout ? m_rulerDateLayout->Size(VERTICAL) : 0;
+ m_rulerDateWidth = m_rulerDateLayout ? m_rulerDateLayout->Size(HORIZONTAL) : 0;
+
+ if (m_orientation == VERTICAL)
+ {
+ m_rulerHeight = m_rulerLayout->Size(VERTICAL);
+ m_gridPosX = m_posX + m_channelWidth;
+ m_gridPosY = m_posY + m_rulerHeight + m_rulerDateHeight;
+ m_gridWidth = m_width - m_channelWidth;
+ m_gridHeight = m_height - m_rulerHeight - m_rulerDateHeight;
+ m_blockSize = m_gridWidth / m_blocksPerPage;
+ m_rulerWidth = m_rulerUnit * m_blockSize;
+ m_channelPosX = m_posX;
+ m_channelPosY = m_posY + m_rulerHeight + m_rulerDateHeight;
+ m_rulerPosX = m_posX + m_channelWidth;
+ m_rulerPosY = m_posY + m_rulerDateHeight;
+ m_channelsPerPage = m_gridHeight / m_channelHeight;
+ m_programmesPerPage = (m_gridWidth / m_blockSize) + 1;
+
+ m_programmeLayout->SetHeight(m_channelHeight);
+ m_focusedProgrammeLayout->SetHeight(m_channelHeight);
+ }
+ else
+ {
+ m_rulerWidth = m_rulerLayout->Size(HORIZONTAL);
+ m_gridPosX = m_posX + m_rulerWidth;
+ m_gridPosY = m_posY + m_channelHeight + m_rulerDateHeight;
+ m_gridWidth = m_width - m_rulerWidth;
+ m_gridHeight = m_height - m_channelHeight - m_rulerDateHeight;
+ m_blockSize = m_gridHeight / m_blocksPerPage;
+ m_rulerHeight = m_rulerUnit * m_blockSize;
+ m_channelPosX = m_posX + m_rulerWidth;
+ m_channelPosY = m_posY + m_rulerDateHeight;
+ m_rulerPosX = m_posX;
+ m_rulerPosY = m_posY + m_channelHeight + m_rulerDateHeight;
+ m_channelsPerPage = m_gridWidth / m_channelWidth;
+ m_programmesPerPage = (m_gridHeight / m_blockSize) + 1;
+
+ m_programmeLayout->SetWidth(m_channelWidth);
+ m_focusedProgrammeLayout->SetWidth(m_channelWidth);
+ }
+
+ // ensure that the scroll offsets are a multiple of our sizes
+ m_channelScrollOffset = m_channelOffset * m_programmeLayout->Size(m_orientation);
+ m_programmeScrollOffset = m_blockOffset * m_blockSize;
+}
+
+void CGUIEPGGridContainer::UpdateScrollOffset(unsigned int currentTime)
+{
+ if (!m_programmeLayout)
+ return;
+
+ m_channelScrollOffset += m_channelScrollSpeed * (currentTime - m_channelScrollLastTime);
+ if ((m_channelScrollSpeed < 0 && m_channelScrollOffset < m_channelOffset * m_programmeLayout->Size(m_orientation)) ||
+ (m_channelScrollSpeed > 0 && m_channelScrollOffset > m_channelOffset * m_programmeLayout->Size(m_orientation)))
+ {
+ m_channelScrollOffset = m_channelOffset * m_programmeLayout->Size(m_orientation);
+ m_channelScrollSpeed = 0;
+ m_bEnableChannelScrolling = true;
+ }
+
+ m_channelScrollLastTime = currentTime;
+ m_programmeScrollOffset += m_programmeScrollSpeed * (currentTime - m_programmeScrollLastTime);
+
+ if ((m_programmeScrollSpeed < 0 && m_programmeScrollOffset < m_blockOffset * m_blockSize) ||
+ (m_programmeScrollSpeed > 0 && m_programmeScrollOffset > m_blockOffset * m_blockSize))
+ {
+ m_programmeScrollOffset = m_blockOffset * m_blockSize;
+ m_programmeScrollSpeed = 0;
+ m_bEnableProgrammeScrolling = true;
+ }
+
+ m_programmeScrollLastTime = currentTime;
+
+ if (m_channelScrollSpeed || m_programmeScrollSpeed)
+ MarkDirtyRegion();
+}
+
+void CGUIEPGGridContainer::GetCurrentLayouts()
+{
+ m_channelLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_channelLayouts.size(); i++)
+ {
+ if (m_channelLayouts[i].CheckCondition())
+ {
+ m_channelLayout = &m_channelLayouts[i];
+ break;
+ }
+ }
+
+ if (!m_channelLayout && !m_channelLayouts.empty())
+ m_channelLayout = &m_channelLayouts[0]; // failsafe
+
+ m_focusedChannelLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_focusedChannelLayouts.size(); i++)
+ {
+ if (m_focusedChannelLayouts[i].CheckCondition())
+ {
+ m_focusedChannelLayout = &m_focusedChannelLayouts[i];
+ break;
+ }
+ }
+
+ if (!m_focusedChannelLayout && !m_focusedChannelLayouts.empty())
+ m_focusedChannelLayout = &m_focusedChannelLayouts[0]; // failsafe
+
+ m_programmeLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_programmeLayouts.size(); i++)
+ {
+ if (m_programmeLayouts[i].CheckCondition())
+ {
+ m_programmeLayout = &m_programmeLayouts[i];
+ break;
+ }
+ }
+
+ if (!m_programmeLayout && !m_programmeLayouts.empty())
+ m_programmeLayout = &m_programmeLayouts[0]; // failsafe
+
+ m_focusedProgrammeLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_focusedProgrammeLayouts.size(); i++)
+ {
+ if (m_focusedProgrammeLayouts[i].CheckCondition())
+ {
+ m_focusedProgrammeLayout = &m_focusedProgrammeLayouts[i];
+ break;
+ }
+ }
+
+ if (!m_focusedProgrammeLayout && !m_focusedProgrammeLayouts.empty())
+ m_focusedProgrammeLayout = &m_focusedProgrammeLayouts[0]; // failsafe
+
+ m_rulerLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_rulerLayouts.size(); i++)
+ {
+ if (m_rulerLayouts[i].CheckCondition())
+ {
+ m_rulerLayout = &m_rulerLayouts[i];
+ break;
+ }
+ }
+
+ if (!m_rulerLayout && !m_rulerLayouts.empty())
+ m_rulerLayout = &m_rulerLayouts[0]; // failsafe
+
+ m_rulerDateLayout = nullptr;
+
+ for (unsigned int i = 0; i < m_rulerDateLayouts.size(); i++)
+ {
+ if (m_rulerDateLayouts[i].CheckCondition())
+ {
+ m_rulerDateLayout = &m_rulerDateLayouts[i];
+ break;
+ }
+ }
+
+ // Note: m_rulerDateLayout is optional; so no "failsafe" logic here (see above)
+}
+
+void CGUIEPGGridContainer::SetRenderOffset(const CPoint& offset)
+{
+ m_renderOffset = offset;
+}
+
+void CGUIEPGGridContainer::GetChannelCacheOffsets(int& cacheBefore, int& cacheAfter)
+{
+ if (m_channelScrollSpeed > 0)
+ {
+ cacheBefore = 0;
+ cacheAfter = m_cacheChannelItems;
+ }
+ else if (m_channelScrollSpeed < 0)
+ {
+ cacheBefore = m_cacheChannelItems;
+ cacheAfter = 0;
+ }
+ else
+ {
+ cacheBefore = m_cacheChannelItems / 2;
+ cacheAfter = m_cacheChannelItems / 2;
+ }
+}
+
+void CGUIEPGGridContainer::GetProgrammeCacheOffsets(int& cacheBefore, int& cacheAfter)
+{
+ if (m_programmeScrollSpeed > 0)
+ {
+ cacheBefore = 0;
+ cacheAfter = m_cacheProgrammeItems;
+ }
+ else if (m_programmeScrollSpeed < 0)
+ {
+ cacheBefore = m_cacheProgrammeItems;
+ cacheAfter = 0;
+ }
+ else
+ {
+ cacheBefore = m_cacheProgrammeItems / 2;
+ cacheAfter = m_cacheProgrammeItems / 2;
+ }
+}
+
+void CGUIEPGGridContainer::HandleChannels(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ if (!m_focusedChannelLayout || !m_channelLayout)
+ return;
+
+ const int chanOffset = GetChannelScrollOffset(m_programmeLayout);
+
+ int cacheBeforeChannel, cacheAfterChannel;
+ GetChannelCacheOffsets(cacheBeforeChannel, cacheAfterChannel);
+
+ if (bRender)
+ {
+ if (m_orientation == VERTICAL)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_channelPosX, m_channelPosY, m_channelWidth, m_gridHeight);
+ else
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_channelPosX, m_channelPosY, m_gridWidth, m_channelHeight);
+ }
+ else
+ {
+ // Free memory not used on screen
+ if (m_gridModel->ChannelItemsSize() > m_channelsPerPage + cacheBeforeChannel + cacheAfterChannel)
+ m_gridModel->FreeChannelMemory(chanOffset - cacheBeforeChannel,
+ chanOffset + m_channelsPerPage - 1 + cacheAfterChannel);
+ }
+
+ CPoint originChannel = CPoint(m_channelPosX, m_channelPosY) + m_renderOffset;
+ float pos;
+ float end;
+
+ if (m_orientation == VERTICAL)
+ {
+ pos = originChannel.y;
+ end = m_posY + m_height;
+ }
+ else
+ {
+ pos = originChannel.x;
+ end = m_posX + m_width;
+ }
+
+ // we offset our draw position to take into account scrolling and whether or not our focused
+ // item is offscreen "above" the list.
+ float drawOffset = (chanOffset - cacheBeforeChannel) * m_channelLayout->Size(m_orientation) -
+ GetChannelScrollOffsetPos();
+ if (m_channelOffset + m_channelCursor < chanOffset)
+ drawOffset += m_focusedChannelLayout->Size(m_orientation) - m_channelLayout->Size(m_orientation);
+
+ pos += drawOffset;
+ end += cacheAfterChannel * m_channelLayout->Size(m_orientation);
+
+ float focusedPos = 0;
+ CGUIListItemPtr focusedItem;
+
+ CFileItemPtr item;
+ int current = chanOffset - cacheBeforeChannel;
+ while (pos < end && m_gridModel->HasChannelItems())
+ {
+ int itemNo = current;
+ if (itemNo >= m_gridModel->ChannelItemsSize())
+ break;
+
+ bool focused = (current == m_channelOffset + m_channelCursor);
+ if (itemNo >= 0)
+ {
+ item = m_gridModel->GetChannelItem(itemNo);
+ if (bRender)
+ {
+ // render our item
+ if (focused)
+ {
+ focusedPos = pos;
+ focusedItem = item;
+ }
+ else
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(originChannel.x, pos, item.get(), false);
+ else
+ RenderItem(pos, originChannel.y, item.get(), false);
+ }
+ }
+ else
+ {
+ // process our item
+ if (m_orientation == VERTICAL)
+ ProcessItem(originChannel.x, pos, item, m_lastItem, focused, m_channelLayout, m_focusedChannelLayout, currentTime, dirtyregions);
+ else
+ ProcessItem(pos, originChannel.y, item, m_lastItem, focused, m_channelLayout, m_focusedChannelLayout, currentTime, dirtyregions);
+ }
+ }
+ // increment our position
+ pos += focused ? m_focusedChannelLayout->Size(m_orientation) : m_channelLayout->Size(m_orientation);
+ current++;
+ }
+
+ if (bRender)
+ {
+ // render focused item last so it can overlap other items
+ if (focusedItem)
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(originChannel.x, focusedPos, focusedItem.get(), true);
+ else
+ RenderItem(focusedPos, originChannel.y, focusedItem.get(), true);
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+}
+
+void CGUIEPGGridContainer::HandleRulerDate(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ if (!m_rulerDateLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration())
+ return;
+
+ CFileItemPtr item(m_gridModel->GetRulerItem(0));
+
+ if (bRender)
+ {
+ // Render single ruler item with date of selected programme
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_rulerDateWidth, m_rulerDateHeight);
+ RenderItem(m_posX, m_posY, item.get(), false);
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+ else
+ {
+ const int rulerOffset = GetProgrammeScrollOffset();
+ item->SetLabel(m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1)->GetLabel2());
+
+ CFileItemPtr lastitem;
+ ProcessItem(m_posX, m_posY, item, lastitem, false, m_rulerDateLayout, m_rulerDateLayout, currentTime, dirtyregions);
+ }
+}
+
+void CGUIEPGGridContainer::HandleRuler(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ if (!m_rulerLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration())
+ return;
+
+ int rulerOffset = GetProgrammeScrollOffset();
+
+ CFileItemPtr item(m_gridModel->GetRulerItem(0));
+ CFileItemPtr lastitem;
+ int cacheBeforeRuler, cacheAfterRuler;
+
+ if (bRender)
+ {
+ if (!m_rulerDateLayout)
+ {
+ // Render single ruler item with date of selected programme
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height);
+ RenderItem(m_posX, m_posY, item.get(), false);
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+
+ // render ruler items
+ GetProgrammeCacheOffsets(cacheBeforeRuler, cacheAfterRuler);
+
+ if (m_orientation == VERTICAL)
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, m_gridWidth, m_rulerHeight);
+ else
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, m_rulerWidth, m_gridHeight);
+ }
+ else
+ {
+ if (!m_rulerDateLayout)
+ {
+ item->SetLabel(m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1)->GetLabel2());
+ ProcessItem(m_posX, m_posY, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_channelWidth);
+ }
+
+ GetProgrammeCacheOffsets(cacheBeforeRuler, cacheAfterRuler);
+
+ // Free memory not used on screen
+ if (m_gridModel->RulerItemsSize() > m_blocksPerPage + cacheBeforeRuler + cacheAfterRuler)
+ m_gridModel->FreeRulerMemory(rulerOffset / m_rulerUnit + 1 - cacheBeforeRuler,
+ rulerOffset / m_rulerUnit + 1 + m_blocksPerPage - 1 +
+ cacheAfterRuler);
+ }
+
+ CPoint originRuler = CPoint(m_rulerPosX, m_rulerPosY) + m_renderOffset;
+ float pos;
+ float end;
+
+ if (m_orientation == VERTICAL)
+ {
+ pos = originRuler.x;
+ end = m_posX + m_width;
+ }
+ else
+ {
+ pos = originRuler.y;
+ end = m_posY + m_height;
+ }
+
+ const float drawOffset =
+ (rulerOffset - cacheBeforeRuler) * m_blockSize - GetProgrammeScrollOffsetPos();
+ pos += drawOffset;
+ end += cacheAfterRuler * m_rulerLayout->Size(m_orientation == VERTICAL ? HORIZONTAL : VERTICAL);
+
+ if (rulerOffset % m_rulerUnit != 0)
+ {
+ /* first ruler marker starts before current view */
+ int startBlock = rulerOffset - 1;
+
+ while (startBlock % m_rulerUnit != 0)
+ startBlock--;
+
+ int missingSection = rulerOffset - startBlock;
+
+ pos -= missingSection * m_blockSize;
+ }
+
+ while (pos < end && (rulerOffset / m_rulerUnit + 1) < m_gridModel->RulerItemsSize())
+ {
+ item = m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1);
+
+ if (m_orientation == VERTICAL)
+ {
+ if (bRender)
+ RenderItem(pos, originRuler.y, item.get(), false);
+ else
+ ProcessItem(pos, originRuler.y, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_rulerWidth);
+
+ pos += m_rulerWidth;
+ }
+ else
+ {
+ if (bRender)
+ RenderItem(originRuler.x, pos, item.get(), false);
+ else
+ ProcessItem(originRuler.x, pos, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_rulerHeight);
+
+ pos += m_rulerHeight;
+ }
+
+ rulerOffset += m_rulerUnit;
+ }
+
+ if (bRender)
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+}
+
+void CGUIEPGGridContainer::HandleProgrammeGrid(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ if (!m_focusedProgrammeLayout || !m_programmeLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration())
+ return;
+
+ const int blockOffset = GetProgrammeScrollOffset();
+ const int chanOffset = GetChannelScrollOffset(m_programmeLayout);
+
+ int cacheBeforeProgramme, cacheAfterProgramme;
+ GetProgrammeCacheOffsets(cacheBeforeProgramme, cacheAfterProgramme);
+
+ if (bRender)
+ {
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_gridPosX, m_gridPosY, m_gridWidth, m_gridHeight);
+ }
+ else
+ {
+ int cacheBeforeChannel, cacheAfterChannel;
+ GetChannelCacheOffsets(cacheBeforeChannel, cacheAfterChannel);
+
+ // Free memory not used on screen
+ int firstChannel = chanOffset - cacheBeforeChannel;
+ if (firstChannel < 0)
+ firstChannel = 0;
+ int lastChannel = chanOffset + m_channelsPerPage - 1 + cacheAfterChannel;
+ if (lastChannel > m_gridModel->GetLastChannel())
+ lastChannel = m_gridModel->GetLastChannel();
+ int firstBlock = blockOffset - cacheBeforeProgramme;
+ if (firstBlock < 0)
+ firstBlock = 0;
+ int lastBlock = blockOffset + m_programmesPerPage - 1 + cacheAfterProgramme;
+ if (lastBlock > m_gridModel->GetLastBlock())
+ lastBlock = m_gridModel->GetLastBlock();
+
+ if (m_gridModel->FreeProgrammeMemory(firstChannel, lastChannel, firstBlock, lastBlock))
+ {
+ // announce changed viewport
+ const CGUIMessage msg(
+ GUI_MSG_REFRESH_LIST, GetParentID(), GetID(), static_cast<int>(PVREvent::Epg));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg);
+ }
+ }
+
+ CPoint originProgramme = CPoint(m_gridPosX, m_gridPosY) + m_renderOffset;
+ float posA;
+ float endA;
+ float posB;
+ float endB;
+
+ if (m_orientation == VERTICAL)
+ {
+ posA = originProgramme.x;
+ endA = m_posX + m_width;
+ posB = originProgramme.y;
+ endB = m_gridPosY + m_gridHeight;
+ }
+ else
+ {
+ posA = originProgramme.y;
+ endA = m_posY + m_height;
+ posB = originProgramme.x;
+ endB = m_gridPosX + m_gridWidth;
+ }
+
+ endA += cacheAfterProgramme * m_blockSize;
+
+ const float drawOffsetA = blockOffset * m_blockSize - GetProgrammeScrollOffsetPos();
+ posA += drawOffsetA;
+ const float drawOffsetB =
+ (chanOffset - cacheBeforeProgramme) * m_channelLayout->Size(m_orientation) -
+ GetChannelScrollOffsetPos();
+ posB += drawOffsetB;
+
+ int channel = chanOffset - cacheBeforeProgramme;
+
+ float focusedPosX = 0;
+ float focusedPosY = 0;
+ CFileItemPtr focusedItem;
+ CFileItemPtr item;
+
+ const int lastChannel = m_gridModel->GetLastChannel();
+ while (posB < endB && HasData() && channel <= lastChannel)
+ {
+ if (channel >= 0)
+ {
+ int block = blockOffset;
+ float posA2 = posA;
+
+ const int startBlock = blockOffset == 0 ? 0 : blockOffset - 1;
+ if (startBlock == 0 || m_gridModel->IsSameGridItem(channel, block, startBlock))
+ {
+ // First program starts before current view
+ block = m_gridModel->GetGridItemStartBlock(channel, startBlock);
+ const int missingSection = blockOffset - block;
+ posA2 -= missingSection * m_blockSize;
+ }
+
+ const int lastBlock = m_gridModel->GetLastBlock();
+ while (posA2 < endA && HasData() && block <= lastBlock)
+ {
+ item = m_gridModel->GetGridItem(channel, block);
+
+ bool focused = (channel == m_channelOffset + m_channelCursor) &&
+ m_gridModel->IsSameGridItem(m_channelOffset + m_channelCursor,
+ m_blockOffset + m_blockCursor, block);
+
+ if (bRender)
+ {
+ // reset to grid start position if first item is out of grid view
+ if (posA2 < posA)
+ posA2 = posA;
+
+ // render our item
+ if (focused)
+ {
+ focusedPosX = posA2;
+ focusedPosY = posB;
+ focusedItem = item;
+ }
+ else
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(posA2, posB, item.get(), focused);
+ else
+ RenderItem(posB, posA2, item.get(), focused);
+ }
+ }
+ else
+ {
+ // calculate the size to truncate if item is out of grid view
+ float truncateSize = 0;
+ if (posA2 < posA)
+ {
+ truncateSize = posA - posA2;
+ posA2 = posA; // reset to grid start position
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ // truncate item's width
+ m_gridModel->DecreaseGridItemWidth(channel, block, truncateSize);
+ }
+
+ if (m_orientation == VERTICAL)
+ ProcessItem(posA2, posB, item, m_lastChannel, focused, m_programmeLayout,
+ m_focusedProgrammeLayout, currentTime, dirtyregions,
+ m_gridModel->GetGridItemWidth(channel, block));
+ else
+ ProcessItem(posB, posA2, item, m_lastChannel, focused, m_programmeLayout,
+ m_focusedProgrammeLayout, currentTime, dirtyregions,
+ m_gridModel->GetGridItemWidth(channel, block));
+ }
+
+ // increment our X position
+ posA2 += m_gridModel->GetGridItemWidth(
+ channel, block); // assumes focused & unfocused layouts have equal length
+ block += MathUtils::round_int(
+ static_cast<double>(m_gridModel->GetGridItemOriginWidth(channel, block) / m_blockSize));
+ }
+ }
+
+ // increment our Y position
+ channel++;
+ posB += (m_orientation == VERTICAL) ? m_channelHeight : m_channelWidth;
+ }
+
+ if (bRender)
+ {
+ // and render the focused item last (for overlapping purposes)
+ if (focusedItem)
+ {
+ if (m_orientation == VERTICAL)
+ RenderItem(focusedPosX, focusedPosY, focusedItem.get(), true);
+ else
+ RenderItem(focusedPosY, focusedPosX, focusedItem.get(), true);
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
+ }
+}
diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.dox b/xbmc/pvr/guilib/GUIEPGGridContainer.dox
new file mode 100644
index 0000000..1916cde
--- /dev/null
+++ b/xbmc/pvr/guilib/GUIEPGGridContainer.dox
@@ -0,0 +1,245 @@
+/*!
+
+\page EPGGrid_control EPGGrid control
+\brief **Used to display the EPG guide in the PVR section.**
+
+\tableofcontents
+
+The epggrid control is used for creating an epg timeline in Kodi. You can choose
+the position, size, and look of the grid and it's contents.
+
+
+--------------------------------------------------------------------------------
+\section EPGGrid_control_sect1 Example
+
+~~~~~~~~~~~~~
+<control type="epggrid" id="10">
+ <description>EPG Grid</description>
+ <posx>80</posx>
+ <posy>81</posy>
+ <width>1120</width>
+ <height>555</height>
+ <pagecontrol>10</pagecontrol>
+ <scrolltime>350</scrolltime>
+ <timeblocks>40</timeblocks>
+ <rulerunit>6</rulerunit>
+ <progresstexture border="5">PVR-EpgProgressIndicator.png</progresstexture>
+ <orienttation>vertical</orientation>
+ <onleft>31</onleft>
+ <onright>31</onright>
+ <onup>10</onup>
+ <ondown>10</ondown>
+ <rulerlayout height="35" width="40">
+ <control type="image" id="1">
+ <width>40</width>
+ <height>29</height>
+ <posx>0</posx>
+ <posy>0</posy>
+ <texture border="5">button-nofocus.png</texture>
+ </control>
+ <control type="label" id="2">
+ <posx>10</posx>
+ <posy>0</posy>
+ <width>34</width>
+ <height>29</height>
+ <font>font12</font>
+ <aligny>center</aligny>
+ <selectedcolor>selected</selectedcolor>
+ <align>left</align>
+ <label>$INFO[ListItem.Label]</label>
+ </control>
+ </rulerlayout>
+ <channellayout height="52" width="280">
+ <animation effect="fade" start="110" time="200">UnFocus</animation>
+ <control type="image" id="1">
+ <posx>0</posx>
+ <posy>0</posy>
+ <width>270</width>
+ <height>52</height>
+ <texture border="5">button-nofocus.png</texture>
+ </control>
+ <control type="label">
+ <posx>5</posx>
+ <posy>5</posy>
+ <width>40</width>
+ <height>35</height>
+ <font>font12</font>
+ <align>left</align>
+ <aligny>center</aligny>
+ <textcolor>grey</textcolor>
+ <selectedcolor>grey</selectedcolor>
+ <info>ListItem.ChannelNumber</info>
+ </control>
+ <control type="image">
+ <posx>45</posx>
+ <posy>4</posy>
+ <width>45</width>
+ <height>44</height>
+ <texture>$INFO[ListItem.Icon]</texture>
+ </control>
+ <control type="label" id="1">
+ <posx>94</posx>
+ <posy>0</posy>
+ <width>160</width>
+ <height>52</height>
+ <font>special12</font>
+ <aligny>center</aligny>
+ <selectedcolor>selected</selectedcolor>
+ <align>left</align>
+ <label>$INFO[ListItem.ChannelName]</label>
+ </control>
+ </channellayout>
+ <focusedchannellayout height="52" width="280">
+ <animation effect="fade" start="110" time="200">OnFocus</animation>
+ <control type="image" id="1">
+ <posx>0</posx>
+ <posy>0</posy>
+ <width>270</width>
+ <height>52</height>
+ <texture border="5">button-focus.png</texture>
+ </control>
+ <control type="label">
+ <posx>5</posx>
+ <posy>5</posy>
+ <width>40</width>
+ <height>35</height>
+ <font>font12</font>
+ <align>left</align>
+ <aligny>center</aligny>
+ <textcolor>grey</textcolor>
+ <selectedcolor>grey</selectedcolor>
+ <info>ListItem.ChannelNumber</info>
+ </control>
+ <control type="image">
+ <posx>45</posx>
+ <posy>4</posy>
+ <width>45</width>
+ <height>44</height>
+ <texture>$INFO[ListItem.Icon]</texture>
+ </control>
+ <control type="label" id="1">
+ <posx>94</posx>
+ <posy>0</posy>
+ <width>160</width>
+ <height>52</height>
+ <font>special12</font>
+ <aligny>center</aligny>
+ <selectedcolor>selected</selectedcolor>
+ <align>left</align>
+ <label>$INFO[ListItem.ChannelName]</label>
+ </control>
+ </focusedchannellayout>
+ <itemlayout height="52" width="40">
+ <control type="image" id="2">
+ <width>40</width>
+ <height>52</height>
+ <posx>0</posx>
+ <posy>0</posy>
+ <aspectratio>stretch</aspectratio>
+ <texture border="3">epg-genres/$INFO[ListItem.Property(GenreType)].png</texture>
+ </control>
+ <control type="label" id="1">
+ <posx>6</posx>
+ <posy>3</posy>
+ <width>30</width>
+ <height>25</height>
+ <font>font12</font>
+ <aligny>center</aligny>
+ <selectedcolor>selected</selectedcolor>
+ <align>left</align>
+ <info>ListItem.Label</info>
+ </control>
+ <control type="image">
+ <posx>5</posx>
+ <posy>28</posy>
+ <width>30</width>
+ <height>20</height>
+ <texture>PVR-IsRecording.png</texture>
+ <visible>ListItem.IsRecording</visible>
+ </control>
+ <control type="image">
+ <posx>5</posx>
+ <posy>28</posy>
+ <width>20</width>
+ <height>20</height>
+ <texture>PVR-HasTimer.png</texture>
+ <visible>ListItem.HasTimer + !ListItem.IsRecording</visible>
+ </control>
+ </itemlayout>
+ <focusedlayout height="52" width="40">
+ <control type="image" id="14">
+ <width>40</width>
+ <height>52</height>
+ <posx>0</posx>
+ <posy>0</posy>
+ <texture border="5">folder-focus.png</texture>
+ </control>
+ <control type="image" id="2">
+ <width>40</width>
+ <height>52</height>
+ <posx>0</posx>
+ <posy>0</posy>
+ <aspectratio>stretch</aspectratio>
+ <texture border="3">epg-genres/$INFO[ListItem.Property(GenreType)].png</texture>
+ </control>
+ <control type="label" id="1">
+ <posx>6</posx>
+ <posy>3</posy>
+ <width>30</width>
+ <height>25</height>
+ <font>font12</font>
+ <aligny>center</aligny>
+ <selectedcolor>selected</selectedcolor>
+ <align>left</align>
+ <info>ListItem.Label</info>
+ </control>
+ <control type="image">
+ <posx>5</posx>
+ <posy>28</posy>
+ <width>30</width>
+ <height>20</height>
+ <texture>PVR-IsRecording.png</texture>
+ <visible>ListItem.IsRecording</visible>
+ </control>
+ <control type="image">
+ <posx>5</posx>
+ <posy>28</posy>
+ <width>20</width>
+ <height>20</height>
+ <texture>PVR-HasTimer.png</texture>
+ <visible>ListItem.HasTimer + !ListItem.IsRecording</visible>
+ </control>
+ </focusedlayout>
+</control>
+~~~~~~~~~~~~~
+
+
+--------------------------------------------------------------------------------
+\section EPGGrid_control_sect2 Available tags
+
+In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags)
+the following tags are available. Note that each tag is **lower case** only. This is
+important, as `xml` tags are case-sensitive.
+
+| Tag | Description |
+|----------------------:|:--------------------------------------------------------------|
+| timeblocks | The number of timeframes on the ruler.
+| rulerunit | Timeframe of each unit on ruler. 1 unit equals 5 minutes.
+| rulerdatelayout | The layout of a ruler date item (usually used to display the start date of current epg page).
+| rulerlayout | The layout of a ruler item.
+| progresstexture | A texture which indicates the current progress time
+| channellayout | The layout of a channel item.
+| focusedchannellayout | The focused layout of a channel item.
+| itemlayout | The layout of the grid
+| focusedlayout | The focused layout of the grid
+
+
+--------------------------------------------------------------------------------
+\section EPGGrid_control_sect3 See also
+
+#### Development:
+
+- [Add-on development](http://kodi.wiki/view/Add-on_development)
+- [Skinning](http://kodi.wiki/view/Skinning)
+
+*/
diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.h b/xbmc/pvr/guilib/GUIEPGGridContainer.h
new file mode 100644
index 0000000..360ade9
--- /dev/null
+++ b/xbmc/pvr/guilib/GUIEPGGridContainer.h
@@ -0,0 +1,274 @@
+/*
+ * 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 "guilib/DirtyRegion.h"
+#include "guilib/GUIControl.h"
+#include "guilib/GUIListItemLayout.h"
+#include "guilib/GUITexture.h"
+#include "guilib/IGUIContainer.h"
+#include "threads/CriticalSection.h"
+#include "utils/Geometry.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+class CDateTime;
+class CFileItem;
+class CFileItemList;
+class CGUIListItem;
+class CGUIListItemLayout;
+
+namespace PVR
+{
+ class CPVRChannel;
+ class CPVRChannelGroupMember;
+ class CPVRChannelNumber;
+
+ class CGUIEPGGridContainerModel;
+
+ class CGUIEPGGridContainer : public IGUIContainer
+ {
+ public:
+ CGUIEPGGridContainer(int parentID, int controlID, float posX, float posY, float width, float height,
+ ORIENTATION orientation, int scrollTime, int preloadItems, int minutesPerPage,
+ int rulerUnit, const CTextureInfo& progressIndicatorTexture);
+ CGUIEPGGridContainer(const CGUIEPGGridContainer& other);
+
+ CGUIEPGGridContainer* Clone() const override { return new CGUIEPGGridContainer(*this); }
+
+ /*!
+ * @brief Check whether the control currently holds data.
+ * @return true if the control has data, false otherwise.
+ */
+ bool HasData() const;
+
+ void AllocResources() override;
+ void FreeResources(bool immediately) override;
+
+ bool OnAction(const CAction& action) override;
+ void OnDown() override;
+ void OnUp() override;
+ void OnLeft() override;
+ void OnRight() override;
+ bool OnMouseOver(const CPoint& point) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void SetFocus(bool focus) override;
+ std::string GetDescription() const override;
+ EVENT_RESULT OnMouseEvent(const CPoint& point, const CMouseEvent& event) override;
+
+ void Process(unsigned int currentTime, CDirtyRegionList& dirtyregions) override;
+ void Render() override;
+
+ CGUIListItemPtr GetListItem(int offset, unsigned int flag = 0) const override;
+ std::string GetLabel(int info) const override;
+
+ std::shared_ptr<CFileItem> GetSelectedGridItem(int offset = 0) const;
+ std::shared_ptr<CPVRChannelGroupMember> GetSelectedChannelGroupMember() const;
+ CDateTime GetSelectedDate() const;
+
+ void LoadLayout(TiXmlElement* layout);
+ void SetPageControl(int id);
+
+ /*! \brief Set the offset of the first item in the container from the container's position
+ Useful for lists/panels where the focused item may be larger than the non-focused items and thus
+ normally cut off from the clipping window defined by the container's position + size.
+ \param offset CPoint holding the offset in skin coordinates.
+ */
+ void SetRenderOffset(const CPoint& offset);
+
+ void JumpToNow();
+ void JumpToDate(const CDateTime& date);
+
+ void GoToBegin();
+ void GoToEnd();
+ void GoToNow();
+ void GoToDate(const CDateTime& date);
+
+ void GoToFirstChannel();
+ void GoToLastChannel();
+
+ void GoToTop();
+ void GoToBottom();
+ void GoToMostLeft();
+ void GoToMostRight();
+
+ void SetTimelineItems(const std::unique_ptr<CFileItemList>& items,
+ const CDateTime& gridStart,
+ const CDateTime& gridEnd);
+
+ std::unique_ptr<CFileItemList> GetCurrentTimeLineItems() const;
+
+ /*!
+ * @brief Set the control's selection to the given channel and set the control's view port to show the channel.
+ * @param channel the channel.
+ * @return true if the selection was set to the given channel, false otherwise.
+ */
+ bool SetChannel(const std::shared_ptr<CPVRChannel>& channel);
+
+ /*!
+ * @brief Set the control's selection to the given channel and set the control's view port to show the channel.
+ * @param channel the channel's path.
+ * @return true if the selection was set to the given channel, false otherwise.
+ */
+ bool SetChannel(const std::string& channel);
+
+ /*!
+ * @brief Set the control's selection to the given channel and set the control's view port to show the channel.
+ * @param channelNumber the channel's number.
+ * @return true if the selection was set to the given channel, false otherwise.
+ */
+ bool SetChannel(const CPVRChannelNumber& channelNumber);
+
+ private:
+ bool OnClick(int actionID);
+ bool SelectItemFromPoint(const CPoint& point, bool justGrid = true);
+
+ void SetChannel(int channel);
+
+ void SetBlock(int block, bool bUpdateBlockTravelAxis = true);
+ void UpdateBlock(bool bUpdateBlockTravelAxis = true);
+
+ void ChannelScroll(int amount);
+ void ProgrammesScroll(int amount);
+ void ValidateOffset();
+ void UpdateLayout();
+
+ void SetItem(const std::pair<std::shared_ptr<CFileItem>, int>& itemInfo);
+ bool SetItem(const std::shared_ptr<CFileItem>& item, int channelIndex, int blockIndex);
+ std::shared_ptr<CFileItem> GetItem() const;
+ std::pair<std::shared_ptr<CFileItem>, int> GetNextItem() const;
+ std::pair<std::shared_ptr<CFileItem>, int> GetPrevItem() const;
+ void UpdateItem();
+
+ void MoveToRow(int row);
+
+ CGUIListItemLayout* GetFocusedLayout() const;
+
+ void ScrollToBlockOffset(int offset);
+ void ScrollToChannelOffset(int offset);
+ void GoToBlock(int blockIndex);
+ void GoToChannel(int channelIndex);
+ void UpdateScrollOffset(unsigned int currentTime);
+ void ProcessItem(float posX, float posY, const std::shared_ptr<CFileItem>& item, std::shared_ptr<CFileItem>& lastitem, bool focused, CGUIListItemLayout* normallayout, CGUIListItemLayout* focusedlayout, unsigned int currentTime, CDirtyRegionList& dirtyregions, float resize = -1.0f);
+ void RenderItem(float posX, float posY, CGUIListItem* item, bool focused);
+ void GetCurrentLayouts();
+
+ void ProcessChannels(unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void ProcessRuler(unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void ProcessRulerDate(unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void ProcessProgrammeGrid(unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void ProcessProgressIndicator(unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void RenderChannels();
+ void RenderRulerDate();
+ void RenderRuler();
+ void RenderProgrammeGrid();
+ void RenderProgressIndicator();
+
+ CPoint m_renderOffset; ///< \brief render offset of the first item in the list \sa SetRenderOffset
+
+ ORIENTATION m_orientation;
+
+ std::vector<CGUIListItemLayout> m_channelLayouts;
+ std::vector<CGUIListItemLayout> m_focusedChannelLayouts;
+ std::vector<CGUIListItemLayout> m_focusedProgrammeLayouts;
+ std::vector<CGUIListItemLayout> m_programmeLayouts;
+ std::vector<CGUIListItemLayout> m_rulerLayouts;
+ std::vector<CGUIListItemLayout> m_rulerDateLayouts;
+
+ CGUIListItemLayout* m_channelLayout;
+ CGUIListItemLayout* m_focusedChannelLayout;
+ CGUIListItemLayout* m_programmeLayout;
+ CGUIListItemLayout* m_focusedProgrammeLayout;
+ CGUIListItemLayout* m_rulerLayout;
+ CGUIListItemLayout* m_rulerDateLayout;
+
+ int m_pageControl;
+
+ void GetChannelCacheOffsets(int& cacheBefore, int& cacheAfter);
+ void GetProgrammeCacheOffsets(int& cacheBefore, int& cacheAfter);
+
+ private:
+ bool OnMouseClick(int dwButton, const CPoint& point);
+ bool OnMouseDoubleClick(int dwButton, const CPoint& point);
+ bool OnMouseWheel(char wheel, const CPoint& point);
+
+ void HandleChannels(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void HandleRuler(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void HandleRulerDate(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions);
+ void HandleProgrammeGrid(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions);
+
+ float GetCurrentTimePositionOnPage() const;
+ float GetProgressIndicatorWidth() const;
+ float GetProgressIndicatorHeight() const;
+
+ void UpdateItems();
+
+ float GetChannelScrollOffsetPos() const;
+ float GetProgrammeScrollOffsetPos() const;
+ int GetChannelScrollOffset(CGUIListItemLayout* layout) const;
+ int GetProgrammeScrollOffset() const;
+
+ int m_rulerUnit; //! number of blocks that makes up one element of the ruler
+ int m_channelsPerPage;
+ int m_programmesPerPage;
+ int m_channelCursor;
+ int m_channelOffset;
+ int m_blocksPerPage;
+ int m_blockCursor;
+ int m_blockOffset;
+ int m_blockTravelAxis;
+ int m_cacheChannelItems;
+ int m_cacheProgrammeItems;
+ int m_cacheRulerItems;
+
+ float m_rulerDateHeight; //! height of ruler date item
+ float m_rulerDateWidth; //! width of ruler date item
+ float m_rulerPosX; //! X position of first ruler item
+ float m_rulerPosY; //! Y position of first ruler item
+ float m_rulerHeight; //! height of the scrolling timeline above the ruler items
+ float m_rulerWidth; //! width of each element of the ruler
+ float m_channelPosX; //! X position of first channel row
+ float m_channelPosY; //! Y position of first channel row
+ float m_channelHeight; //! height of the channel item
+ float m_channelWidth; //! width of the channel item
+ float m_gridPosX; //! X position of first grid item
+ float m_gridPosY; //! Y position of first grid item
+ float m_gridWidth; //! width of the epg grid control
+ float m_gridHeight; //! height of the epg grid control
+ float m_blockSize; //! a block's width in pixels
+ float m_analogScrollCount;
+
+ std::unique_ptr<CGUITexture> m_guiProgressIndicatorTexture;
+
+ std::shared_ptr<CFileItem> m_lastItem;
+ std::shared_ptr<CFileItem> m_lastChannel;
+
+ bool m_bEnableProgrammeScrolling = true;
+ bool m_bEnableChannelScrolling = true;
+
+ int m_scrollTime;
+
+ int m_programmeScrollLastTime;
+ float m_programmeScrollSpeed;
+ float m_programmeScrollOffset;
+
+ int m_channelScrollLastTime;
+ float m_channelScrollSpeed;
+ float m_channelScrollOffset;
+
+ mutable CCriticalSection m_critSection;
+ std::unique_ptr<CGUIEPGGridContainerModel> m_gridModel;
+ std::unique_ptr<CGUIEPGGridContainerModel> m_updatedGridModel;
+
+ int m_itemStartBlock = 0;
+ };
+}
diff --git a/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp b/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp
new file mode 100644
index 0000000..e8fac01
--- /dev/null
+++ b/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp
@@ -0,0 +1,723 @@
+/*
+ * 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 "GUIEPGGridContainerModel.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cmath>
+#include <iterator>
+#include <memory>
+#include <vector>
+
+using namespace PVR;
+
+static const unsigned int GRID_START_PADDING = 30; // minutes
+
+void CGUIEPGGridContainerModel::SetInvalid()
+{
+ for (const auto& gridItem : m_gridIndex)
+ gridItem.second.item->SetInvalid();
+ for (const auto& channel : m_channelItems)
+ channel->SetInvalid();
+ for (const auto& ruler : m_rulerItems)
+ ruler->SetInvalid();
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::CreateGapItem(int iChannel) const
+{
+ const std::shared_ptr<CPVRChannel> channel = m_channelItems[iChannel]->GetPVRChannelInfoTag();
+ const std::shared_ptr<CPVREpgInfoTag> gapTag = channel->CreateEPGGapTag(m_gridStart, m_gridEnd);
+ return std::make_shared<CFileItem>(gapTag);
+}
+
+std::vector<std::shared_ptr<CPVREpgInfoTag>> CGUIEPGGridContainerModel::GetEPGTimeline(
+ int iChannel, const CDateTime& minEventEnd, const CDateTime& maxEventStart) const
+{
+ CDateTime min = minEventEnd - CDateTimeSpan(0, 0, MINSPERBLOCK, 0) + CDateTimeSpan(0, 0, 0, 1);
+ CDateTime max = maxEventStart + CDateTimeSpan(0, 0, MINSPERBLOCK, 0);
+
+ if (min < m_gridStart)
+ min = m_gridStart;
+
+ if (max > m_gridEnd)
+ max = m_gridEnd;
+
+ return m_channelItems[iChannel]->GetPVRChannelInfoTag()->GetEPGTimeline(m_gridStart, m_gridEnd,
+ min, max);
+}
+
+void CGUIEPGGridContainerModel::Initialize(const std::unique_ptr<CFileItemList>& items,
+ const CDateTime& gridStart,
+ const CDateTime& gridEnd,
+ int iFirstChannel,
+ int iChannelsPerPage,
+ int iFirstBlock,
+ int iBlocksPerPage,
+ int iRulerUnit,
+ float fBlockSize)
+{
+ if (!m_channelItems.empty())
+ {
+ CLog::LogF(LOGERROR, "Already initialized!");
+ return;
+ }
+
+ m_fBlockSize = fBlockSize;
+
+ ////////////////////////////////////////////////////////////////////////
+ // Create channel items
+ std::copy(items->cbegin(), items->cend(), std::back_inserter(m_channelItems));
+
+ /* check for invalid start and end time */
+ if (gridStart >= gridEnd)
+ {
+ // default to start "now minus GRID_START_PADDING minutes" and end "start plus one page".
+ m_gridStart = CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0);
+ m_gridEnd = m_gridStart + CDateTimeSpan(0, 0, iBlocksPerPage * MINSPERBLOCK, 0);
+ }
+ else if (gridStart >
+ (CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0)))
+ {
+ // adjust to start "now minus GRID_START_PADDING minutes".
+ m_gridStart = CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0);
+ m_gridEnd = gridEnd;
+ }
+ else
+ {
+ m_gridStart = gridStart;
+ m_gridEnd = gridEnd;
+ }
+
+ // roundup
+ m_gridStart = CDateTime(m_gridStart.GetYear(), m_gridStart.GetMonth(), m_gridStart.GetDay(),
+ m_gridStart.GetHour(), m_gridStart.GetMinute() >= 30 ? 30 : 0, 0);
+ m_gridEnd = CDateTime(m_gridEnd.GetYear(), m_gridEnd.GetMonth(), m_gridEnd.GetDay(),
+ m_gridEnd.GetHour(), m_gridEnd.GetMinute() >= 30 ? 30 : 0, 0);
+
+ m_blocks = GetBlock(m_gridEnd) + 1;
+
+ const int iBlocksLastPage = m_blocks % iBlocksPerPage;
+ if (iBlocksLastPage > 0)
+ {
+ m_gridEnd += CDateTimeSpan(0, 0, (iBlocksPerPage - iBlocksLastPage) * MINSPERBLOCK, 0);
+ m_blocks += (iBlocksPerPage - iBlocksLastPage);
+ }
+
+ ////////////////////////////////////////////////////////////////////////
+ // Create ruler items
+ CDateTime ruler;
+ ruler.SetFromUTCDateTime(m_gridStart);
+ CDateTime rulerEnd;
+ rulerEnd.SetFromUTCDateTime(m_gridEnd);
+ CFileItemPtr rulerItem(new CFileItem(ruler.GetAsLocalizedDate(true)));
+ rulerItem->SetProperty("DateLabel", true);
+ m_rulerItems.emplace_back(rulerItem);
+
+ const CDateTimeSpan unit(0, 0, iRulerUnit * MINSPERBLOCK, 0);
+ for (; ruler < rulerEnd; ruler += unit)
+ {
+ rulerItem.reset(new CFileItem(ruler.GetAsLocalizedTime("", false)));
+ rulerItem->SetLabel2(ruler.GetAsLocalizedDate(true));
+ m_rulerItems.emplace_back(rulerItem);
+ }
+
+ m_firstActiveChannel = iFirstChannel;
+ m_lastActiveChannel = iFirstChannel + iChannelsPerPage - 1;
+ m_firstActiveBlock = iFirstBlock;
+ m_lastActiveBlock = iFirstBlock + iBlocksPerPage - 1;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::CreateEpgTags(int iChannel, int iBlock) const
+{
+ std::shared_ptr<CFileItem> result;
+
+ const int firstBlock = iBlock < m_firstActiveBlock ? iBlock : m_firstActiveBlock;
+ const int lastBlock = iBlock > m_lastActiveBlock ? iBlock : m_lastActiveBlock;
+
+ const auto tags =
+ GetEPGTimeline(iChannel, GetStartTimeForBlock(firstBlock), GetStartTimeForBlock(lastBlock));
+
+ const int firstResultBlock = GetFirstEventBlock(tags.front());
+ const int lastResultBlock = GetLastEventBlock(tags.back());
+ if (firstResultBlock > lastResultBlock)
+ return result;
+
+ auto it = m_epgItems.insert({iChannel, EpgTags()}).first;
+ EpgTags& epgTags = (*it).second;
+
+ epgTags.firstBlock = firstResultBlock;
+ epgTags.lastBlock = lastResultBlock;
+
+ for (const auto& tag : tags)
+ {
+ if (GetFirstEventBlock(tag) > GetLastEventBlock(tag))
+ continue;
+
+ const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(tag);
+ if (!result && IsEventMemberOfBlock(tag, iBlock))
+ result = item;
+
+ epgTags.tags.emplace_back(item);
+ }
+
+ return result;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTags(EpgTagsMap::iterator& itEpg,
+ int iChannel,
+ int iBlock) const
+{
+ std::shared_ptr<CFileItem> result;
+
+ EpgTags& epgTags = (*itEpg).second;
+
+ if (iBlock < epgTags.firstBlock)
+ {
+ result = GetEpgTagsBefore(epgTags, iChannel, iBlock);
+ }
+ else if (iBlock > epgTags.lastBlock)
+ {
+ result = GetEpgTagsAfter(epgTags, iChannel, iBlock);
+ }
+ else
+ {
+ const auto it =
+ std::find_if(epgTags.tags.cbegin(), epgTags.tags.cend(), [this, iBlock](const auto& item) {
+ return IsEventMemberOfBlock(item->GetEPGInfoTag(), iBlock);
+ });
+ if (it != epgTags.tags.cend())
+ result = (*it);
+ }
+
+ return result;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTagsBefore(EpgTags& epgTags,
+ int iChannel,
+ int iBlock) const
+{
+ std::shared_ptr<CFileItem> result;
+
+ int lastBlock = epgTags.firstBlock - 1;
+ if (lastBlock < 0)
+ lastBlock = 0;
+
+ const auto tags =
+ GetEPGTimeline(iChannel, GetStartTimeForBlock(iBlock), GetStartTimeForBlock(lastBlock));
+
+ if (epgTags.lastBlock == -1)
+ epgTags.lastBlock = lastBlock;
+
+ if (tags.empty())
+ {
+ epgTags.firstBlock = iBlock;
+ }
+ else
+ {
+ const int firstResultBlock = GetFirstEventBlock(tags.front());
+ const int lastResultBlock = GetLastEventBlock(tags.back());
+ if (firstResultBlock > lastResultBlock)
+ return result;
+
+ // insert before the existing tags
+ epgTags.firstBlock = firstResultBlock;
+
+ auto it = tags.crbegin();
+ if (!epgTags.tags.empty())
+ {
+ // ptr comp does not work for gap tags!
+ // if ((*it) == epgTags.tags.front()->GetEPGInfoTag())
+
+ const std::shared_ptr<CPVREpgInfoTag> t = epgTags.tags.front()->GetEPGInfoTag();
+ if ((*it)->StartAsUTC() == t->StartAsUTC() && (*it)->EndAsUTC() == t->EndAsUTC())
+ {
+ if (!result && IsEventMemberOfBlock(*it, iBlock))
+ result = epgTags.tags.front();
+
+ ++it; // skip, because we already have that epg tag
+ }
+ }
+
+ for (; it != tags.crend(); ++it)
+ {
+ if (GetFirstEventBlock(*it) > GetLastEventBlock(*it))
+ continue;
+
+ const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(*it);
+ if (!result && IsEventMemberOfBlock(*it, iBlock))
+ result = item;
+
+ epgTags.tags.insert(epgTags.tags.begin(), item);
+ }
+ }
+
+ return result;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTagsAfter(EpgTags& epgTags,
+ int iChannel,
+ int iBlock) const
+{
+ std::shared_ptr<CFileItem> result;
+
+ int firstBlock = epgTags.lastBlock + 1;
+ if (firstBlock >= GetLastBlock())
+ firstBlock = GetLastBlock();
+
+ const auto tags =
+ GetEPGTimeline(iChannel, GetStartTimeForBlock(firstBlock), GetStartTimeForBlock(iBlock));
+
+ if (epgTags.firstBlock == -1)
+ epgTags.firstBlock = firstBlock;
+
+ if (tags.empty())
+ {
+ epgTags.lastBlock = iBlock;
+ }
+ else
+ {
+ const int firstResultBlock = GetFirstEventBlock(tags.front());
+ const int lastResultBlock = GetLastEventBlock(tags.back());
+ if (firstResultBlock > lastResultBlock)
+ return result;
+
+ // append to the existing tags
+ epgTags.lastBlock = lastResultBlock;
+
+ auto it = tags.cbegin();
+ if (!epgTags.tags.empty())
+ {
+ // ptr comp does not work for gap tags!
+ // if ((*it) == epgTags.tags.back()->GetEPGInfoTag())
+
+ const std::shared_ptr<CPVREpgInfoTag> t = epgTags.tags.back()->GetEPGInfoTag();
+ if ((*it)->StartAsUTC() == t->StartAsUTC() && (*it)->EndAsUTC() == t->EndAsUTC())
+ {
+ if (!result && IsEventMemberOfBlock(*it, iBlock))
+ result = epgTags.tags.back();
+
+ ++it; // skip, because we already have that epg tag
+ }
+ }
+
+ for (; it != tags.cend(); ++it)
+ {
+ if (GetFirstEventBlock(*it) > GetLastEventBlock(*it))
+ continue;
+
+ const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(*it);
+ if (!result && IsEventMemberOfBlock(*it, iBlock))
+ result = item;
+
+ epgTags.tags.emplace_back(item);
+ }
+ }
+
+ return result;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetItem(int iChannel, int iBlock) const
+{
+ std::shared_ptr<CFileItem> result;
+
+ auto itEpg = m_epgItems.find(iChannel);
+ if (itEpg == m_epgItems.end())
+ {
+ result = CreateEpgTags(iChannel, iBlock);
+ }
+ else
+ {
+ result = GetEpgTags(itEpg, iChannel, iBlock);
+ }
+
+ if (!result)
+ {
+ // Must never happen. if it does, fix the root cause, don't tolerate nullptr!
+ CLog::LogF(LOGERROR, "EPG tag ({}, {}) not found!", iChannel, iBlock);
+ }
+
+ return result;
+}
+
+void CGUIEPGGridContainerModel::FindChannelAndBlockIndex(int channelUid,
+ unsigned int broadcastUid,
+ int eventOffset,
+ int& newChannelIndex,
+ int& newBlockIndex) const
+{
+ newChannelIndex = INVALID_INDEX;
+ newBlockIndex = INVALID_INDEX;
+
+ // find the new channel index
+ int iCurrentChannel = 0;
+ for (const auto& channel : m_channelItems)
+ {
+ if (channel->GetPVRChannelInfoTag()->UniqueID() == channelUid)
+ {
+ newChannelIndex = iCurrentChannel;
+
+ // find the new block index
+ const std::shared_ptr<CPVREpg> epg = channel->GetPVRChannelInfoTag()->GetEPG();
+ if (epg)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> tag = epg->GetTagByBroadcastId(broadcastUid);
+ if (tag)
+ newBlockIndex = GetFirstEventBlock(tag) + eventOffset;
+ }
+ break; // done
+ }
+ iCurrentChannel++;
+ }
+}
+
+GridItem* CGUIEPGGridContainerModel::GetGridItemPtr(int iChannel, int iBlock) const
+{
+ auto it = m_gridIndex.find({iChannel, iBlock});
+ if (it == m_gridIndex.end())
+ {
+ const CDateTime startTime = GetStartTimeForBlock(iBlock);
+ if (startTime < m_gridStart || m_gridEnd < startTime)
+ {
+ CLog::LogF(LOGERROR, "Requested EPG tag ({}, {}) outside grid boundaries!", iChannel, iBlock);
+ return nullptr;
+ }
+
+ const std::shared_ptr<CFileItem> item = GetItem(iChannel, iBlock);
+ if (!item)
+ {
+ CLog::LogF(LOGERROR, "Got no EPG tag ({}, {})!", iChannel, iBlock);
+ return nullptr;
+ }
+
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = item->GetEPGInfoTag();
+
+ const int startBlock = GetFirstEventBlock(epgTag);
+ const int endBlock = GetLastEventBlock(epgTag);
+
+ //! @todo it seems that this should be done somewhere else. CFileItem ctor maybe.
+ item->SetProperty("GenreType", epgTag->GenreType());
+
+ const float fItemWidth = (endBlock - startBlock + 1) * m_fBlockSize;
+ it = m_gridIndex.insert({{iChannel, iBlock}, {item, fItemWidth, startBlock, endBlock}}).first;
+ }
+
+ return &(*it).second;
+}
+
+bool CGUIEPGGridContainerModel::IsSameGridItem(int iChannel, int iBlock1, int iBlock2) const
+{
+ if (iBlock1 == iBlock2)
+ return true;
+
+ const GridItem* item1 = GetGridItemPtr(iChannel, iBlock1);
+ const GridItem* item2 = GetGridItemPtr(iChannel, iBlock2);
+
+ // compare the instances, not instance pointers, pointers are not unique.
+ return *item1 == *item2;
+}
+
+std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetGridItem(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->item;
+}
+
+int CGUIEPGGridContainerModel::GetGridItemStartBlock(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->startBlock;
+}
+
+int CGUIEPGGridContainerModel::GetGridItemEndBlock(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->endBlock;
+}
+
+CDateTime CGUIEPGGridContainerModel::GetGridItemEndTime(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->item->GetEPGInfoTag()->EndAsUTC();
+}
+
+float CGUIEPGGridContainerModel::GetGridItemWidth(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->width;
+}
+
+float CGUIEPGGridContainerModel::GetGridItemOriginWidth(int iChannel, int iBlock) const
+{
+ return GetGridItemPtr(iChannel, iBlock)->originWidth;
+}
+
+void CGUIEPGGridContainerModel::DecreaseGridItemWidth(int iChannel, int iBlock, float fSize)
+{
+ auto it = m_gridIndex.find({iChannel, iBlock});
+ if (it != m_gridIndex.end() && (*it).second.width != ((*it).second.originWidth - fSize))
+ (*it).second.width = (*it).second.originWidth - fSize;
+}
+
+unsigned int CGUIEPGGridContainerModel::GetGridStartPadding() const
+{
+ unsigned int iPastMinutes =
+ CServiceBroker::GetPVRManager().EpgContainer().GetPastDaysToDisplay() * 24 * 60;
+
+ if (iPastMinutes < GRID_START_PADDING)
+ return iPastMinutes;
+
+ return GRID_START_PADDING; // minutes
+}
+
+void CGUIEPGGridContainerModel::FreeChannelMemory(int keepStart, int keepEnd)
+{
+ if (keepStart < keepEnd)
+ {
+ // remove before keepStart and after keepEnd
+ for (int i = 0; i < keepStart && i < ChannelItemsSize(); ++i)
+ m_channelItems[i]->FreeMemory();
+ for (int i = keepEnd + 1; i < ChannelItemsSize(); ++i)
+ m_channelItems[i]->FreeMemory();
+ }
+ else
+ {
+ // wrapping
+ for (int i = keepEnd + 1; i < keepStart && i < ChannelItemsSize(); ++i)
+ m_channelItems[i]->FreeMemory();
+ }
+}
+
+bool CGUIEPGGridContainerModel::FreeProgrammeMemory(int firstChannel,
+ int lastChannel,
+ int firstBlock,
+ int lastBlock)
+{
+ const bool channelsChanged =
+ (firstChannel != m_firstActiveChannel || lastChannel != m_lastActiveChannel);
+ const bool blocksChanged = (firstBlock != m_firstActiveBlock || lastBlock != m_lastActiveBlock);
+ if (!channelsChanged && !blocksChanged)
+ return false;
+
+ // clear the grid. it will be recreated on-demand.
+ m_gridIndex.clear();
+
+ bool newChannels = false;
+
+ if (channelsChanged)
+ {
+ // purge epg tags for inactive channels
+ for (auto it = m_epgItems.begin(); it != m_epgItems.end();)
+ {
+ if ((*it).first < firstChannel || (*it).first > lastChannel)
+ {
+ it = m_epgItems.erase(it);
+ continue; // next channel
+ }
+ ++it;
+ }
+
+ newChannels = (firstChannel < m_firstActiveChannel) || (lastChannel > m_lastActiveChannel);
+ }
+
+ if (blocksChanged || newChannels)
+ {
+ // clear and refetch epg tags for active channels
+ const CDateTime maxEnd = GetStartTimeForBlock(firstBlock);
+ const CDateTime minStart = GetStartTimeForBlock(lastBlock);
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> tags;
+ for (int i = firstChannel; i <= lastChannel; ++i)
+ {
+ auto it = m_epgItems.find(i);
+ if (it == m_epgItems.end())
+ it = m_epgItems.insert({i, EpgTags()}).first;
+
+ if (blocksChanged || i < m_firstActiveChannel || i > m_lastActiveChannel)
+ {
+ EpgTags& epgTags = (*it).second;
+
+ (*it).second.tags.clear();
+
+ tags = GetEPGTimeline(i, maxEnd, minStart);
+ const int firstResultBlock = GetFirstEventBlock(tags.front());
+ const int lastResultBlock = GetLastEventBlock(tags.back());
+ if (firstResultBlock > lastResultBlock)
+ continue;
+
+ epgTags.firstBlock = firstResultBlock;
+ epgTags.lastBlock = lastResultBlock;
+
+ for (const auto& tag : tags)
+ {
+ if (GetFirstEventBlock(tag) > GetLastEventBlock(tag))
+ continue;
+
+ epgTags.tags.emplace_back(std::make_shared<CFileItem>(tag));
+ }
+ }
+ }
+ }
+
+ m_firstActiveChannel = firstChannel;
+ m_lastActiveChannel = lastChannel;
+ m_firstActiveBlock = firstBlock;
+ m_lastActiveBlock = lastBlock;
+
+ return true;
+}
+
+void CGUIEPGGridContainerModel::FreeRulerMemory(int keepStart, int keepEnd)
+{
+ if (keepStart < keepEnd)
+ {
+ // remove before keepStart and after keepEnd
+ for (int i = 1; i < keepStart && i < RulerItemsSize(); ++i)
+ m_rulerItems[i]->FreeMemory();
+ for (int i = keepEnd + 1; i < RulerItemsSize(); ++i)
+ m_rulerItems[i]->FreeMemory();
+ }
+ else
+ {
+ // wrapping
+ for (int i = keepEnd + 1; i < keepStart && i < RulerItemsSize(); ++i)
+ {
+ if (i == 0)
+ continue;
+
+ m_rulerItems[i]->FreeMemory();
+ }
+ }
+}
+
+unsigned int CGUIEPGGridContainerModel::GetPageNowOffset() const
+{
+ return GetGridStartPadding() / MINSPERBLOCK; // this is the 'now' block relative to page start
+}
+
+CDateTime CGUIEPGGridContainerModel::GetStartTimeForBlock(int block) const
+{
+ if (block < 0)
+ block = 0;
+ else if (block >= GridItemsSize())
+ block = GetLastBlock();
+
+ return m_gridStart + CDateTimeSpan(0, 0, block * MINSPERBLOCK, 0);
+}
+
+int CGUIEPGGridContainerModel::GetBlock(const CDateTime& datetime) const
+{
+ int diff;
+
+ if (m_gridStart == datetime)
+ return 0; // block is at grid start
+ else if (m_gridStart > datetime)
+ diff = -1 * (m_gridStart - datetime).GetSecondsTotal(); // block is before grid start
+ else
+ diff = (datetime - m_gridStart).GetSecondsTotal(); // block is after grid start
+
+ // Note: Subtract 1 second from diff to ensure that events ending exactly at block boundary
+ // are unambiguous. Example: An event ending at 5:00:00 shall be mapped to block 9 and
+ // an event starting at 5:00:00 shall be mapped to block 10, not both at block 10.
+ // Only exception is grid end, because there is no successor.
+ if (datetime >= m_gridEnd)
+ return diff / 60 / MINSPERBLOCK; // block is equal or after grid end
+ else
+ return (diff - 1) / 60 / MINSPERBLOCK;
+}
+
+int CGUIEPGGridContainerModel::GetNowBlock() const
+{
+ return GetBlock(CDateTime::GetUTCDateTime()) - GetPageNowOffset();
+}
+
+int CGUIEPGGridContainerModel::GetFirstEventBlock(
+ const std::shared_ptr<CPVREpgInfoTag>& event) const
+{
+ const CDateTime eventStart = event->StartAsUTC();
+ int diff;
+
+ if (m_gridStart == eventStart)
+ return 0; // block is at grid start
+ else if (m_gridStart > eventStart)
+ diff = -1 * (m_gridStart - eventStart).GetSecondsTotal();
+ else
+ diff = (eventStart - m_gridStart).GetSecondsTotal();
+
+ // First block of a tag is always the block calculated using event's start time, rounded up.
+ float fBlockIndex = diff / 60.0f / MINSPERBLOCK;
+ return static_cast<int>(std::ceil(fBlockIndex));
+}
+
+int CGUIEPGGridContainerModel::GetLastEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const
+{
+ // Last block of a tag is always the block calculated using event's end time, not rounded up.
+ return GetBlock(event->EndAsUTC());
+}
+
+bool CGUIEPGGridContainerModel::IsEventMemberOfBlock(const std::shared_ptr<CPVREpgInfoTag>& event,
+ int iBlock) const
+{
+ const int iFirstBlock = GetFirstEventBlock(event);
+ const int iLastBlock = GetLastEventBlock(event);
+
+ if (iFirstBlock > iLastBlock)
+ {
+ return false;
+ }
+ else if (iFirstBlock == iBlock)
+ {
+ return true;
+ }
+ else if (iFirstBlock < iBlock)
+ {
+ return (iBlock <= iLastBlock);
+ }
+ return false;
+}
+
+std::unique_ptr<CFileItemList> CGUIEPGGridContainerModel::GetCurrentTimeLineItems(
+ int firstChannel, int numChannels) const
+{
+ // Note: No need to keep this in a member. Gets generally not called multiple times for the
+ // same timeline, but content must be synced with m_epgItems, which changes quite often.
+
+ std::unique_ptr<CFileItemList> items(new CFileItemList);
+
+ if (numChannels > ChannelItemsSize())
+ numChannels = ChannelItemsSize();
+
+ int i = 0;
+ for (int channel = firstChannel; channel < (firstChannel + numChannels); ++channel)
+ {
+ // m_epgItems is not sorted, fileitemlist must be sorted, so we have to 'find' the channel
+ const auto itEpg = m_epgItems.find(channel);
+ if (itEpg != m_epgItems.end())
+ {
+ // tags are sorted, so we can iterate and append
+ for (const auto& tag : (*itEpg).second.tags)
+ {
+ tag->SetProperty("TimelineIndex", i);
+ items->Add(tag);
+ ++i;
+ }
+ }
+ else
+ {
+ // fake empty EPG
+ const std::shared_ptr<CFileItem> tag = CreateGapItem(channel);
+ tag->SetProperty("TimelineIndex", i);
+ items->Add(tag);
+ ++i;
+ }
+ }
+ return items;
+}
diff --git a/xbmc/pvr/guilib/GUIEPGGridContainerModel.h b/xbmc/pvr/guilib/GUIEPGGridContainerModel.h
new file mode 100644
index 0000000..2e0a6d6
--- /dev/null
+++ b/xbmc/pvr/guilib/GUIEPGGridContainerModel.h
@@ -0,0 +1,178 @@
+/*
+ * 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 "XBDateTime.h"
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+class CFileItem;
+class CFileItemList;
+
+namespace PVR
+{
+struct GridItem
+{
+ GridItem(const std::shared_ptr<CFileItem>& _item, float _width, int _startBlock, int _endBlock)
+ : item(_item), originWidth(_width), width(_width), startBlock(_startBlock), endBlock(_endBlock)
+ {
+ }
+
+ bool operator==(const GridItem& other) const
+ {
+ return (startBlock == other.startBlock && endBlock == other.endBlock);
+ }
+
+ std::shared_ptr<CFileItem> item;
+ float originWidth = 0.0f;
+ float width = 0.0f;
+ int startBlock = 0;
+ int endBlock = 0;
+};
+
+class CPVREpgInfoTag;
+
+class CGUIEPGGridContainerModel
+{
+public:
+ static constexpr int MINSPERBLOCK = 5; // minutes
+
+ CGUIEPGGridContainerModel() = default;
+ virtual ~CGUIEPGGridContainerModel() = default;
+
+ void Initialize(const std::unique_ptr<CFileItemList>& items,
+ const CDateTime& gridStart,
+ const CDateTime& gridEnd,
+ int iFirstChannel,
+ int iChannelsPerPage,
+ int iFirstBlock,
+ int iBlocksPerPage,
+ int iRulerUnit,
+ float fBlockSize);
+ void SetInvalid();
+
+ static const int INVALID_INDEX = -1;
+ void FindChannelAndBlockIndex(int channelUid,
+ unsigned int broadcastUid,
+ int eventOffset,
+ int& newChannelIndex,
+ int& newBlockIndex) const;
+
+ void FreeChannelMemory(int keepStart, int keepEnd);
+ bool FreeProgrammeMemory(int firstChannel, int lastChannel, int firstBlock, int lastBlock);
+ void FreeRulerMemory(int keepStart, int keepEnd);
+
+ std::shared_ptr<CFileItem> GetChannelItem(int iIndex) const { return m_channelItems[iIndex]; }
+ bool HasChannelItems() const { return !m_channelItems.empty(); }
+ int ChannelItemsSize() const { return static_cast<int>(m_channelItems.size()); }
+ int GetLastChannel() const
+ {
+ return m_channelItems.empty() ? -1 : static_cast<int>(m_channelItems.size()) - 1;
+ }
+
+ std::shared_ptr<CFileItem> GetRulerItem(int iIndex) const { return m_rulerItems[iIndex]; }
+ int RulerItemsSize() const { return static_cast<int>(m_rulerItems.size()); }
+
+ int GridItemsSize() const { return m_blocks; }
+ bool IsSameGridItem(int iChannel, int iBlock1, int iBlock2) const;
+ std::shared_ptr<CFileItem> GetGridItem(int iChannel, int iBlock) const;
+ int GetGridItemStartBlock(int iChannel, int iBlock) const;
+ int GetGridItemEndBlock(int iChannel, int iBlock) const;
+ CDateTime GetGridItemEndTime(int iChannel, int iBlock) const;
+ float GetGridItemWidth(int iChannel, int iBlock) const;
+ float GetGridItemOriginWidth(int iChannel, int iBlock) const;
+ void DecreaseGridItemWidth(int iChannel, int iBlock, float fSize);
+
+ bool IsZeroGridDuration() const { return (m_gridEnd - m_gridStart) == CDateTimeSpan(0, 0, 0, 0); }
+ const CDateTime& GetGridStart() const { return m_gridStart; }
+ const CDateTime& GetGridEnd() const { return m_gridEnd; }
+ unsigned int GetGridStartPadding() const;
+
+ unsigned int GetPageNowOffset() const;
+ int GetNowBlock() const;
+ int GetLastBlock() const { return m_blocks - 1; }
+
+ CDateTime GetStartTimeForBlock(int block) const;
+ int GetBlock(const CDateTime& datetime) const;
+ int GetFirstEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const;
+ int GetLastEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const;
+ bool IsEventMemberOfBlock(const std::shared_ptr<CPVREpgInfoTag>& event, int iBlock) const;
+
+ std::unique_ptr<CFileItemList> GetCurrentTimeLineItems(int firstChannel, int numChannels) const;
+
+private:
+ GridItem* GetGridItemPtr(int iChannel, int iBlock) const;
+ std::shared_ptr<CFileItem> CreateGapItem(int iChannel) const;
+ std::shared_ptr<CFileItem> GetItem(int iChannel, int iBlock) const;
+
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEPGTimeline(int iChannel,
+ const CDateTime& minEventEnd,
+ const CDateTime& maxEventStart) const;
+
+ struct EpgTags
+ {
+ std::vector<std::shared_ptr<CFileItem>> tags;
+ int firstBlock = -1;
+ int lastBlock = -1;
+ };
+
+ using EpgTagsMap = std::unordered_map<int, EpgTags>;
+
+ std::shared_ptr<CFileItem> CreateEpgTags(int iChannel, int iBlock) const;
+ std::shared_ptr<CFileItem> GetEpgTags(EpgTagsMap::iterator& itEpg,
+ int iChannel,
+ int iBlock) const;
+ std::shared_ptr<CFileItem> GetEpgTagsBefore(EpgTags& epgTags, int iChannel, int iBlock) const;
+ std::shared_ptr<CFileItem> GetEpgTagsAfter(EpgTags& epgTags, int iChannel, int iBlock) const;
+
+ mutable EpgTagsMap m_epgItems;
+
+ CDateTime m_gridStart;
+ CDateTime m_gridEnd;
+
+ std::vector<std::shared_ptr<CFileItem>> m_channelItems;
+ std::vector<std::shared_ptr<CFileItem>> m_rulerItems;
+
+ struct GridCoordinates
+ {
+ GridCoordinates(int _channel, int _block) : channel(_channel), block(_block) {}
+
+ bool operator==(const GridCoordinates& other) const
+ {
+ return (channel == other.channel && block == other.block);
+ }
+
+ int channel = 0;
+ int block = 0;
+ };
+
+ struct GridCoordinatesHash
+ {
+ std::size_t operator()(const GridCoordinates& coordinates) const
+ {
+ return std::hash<int>()(coordinates.channel) ^ std::hash<int>()(coordinates.block);
+ }
+ };
+
+ mutable std::unordered_map<GridCoordinates, GridItem, GridCoordinatesHash> m_gridIndex;
+
+ int m_blocks = 0;
+ float m_fBlockSize = 0.0f;
+
+ int m_firstActiveChannel = 0;
+ int m_lastActiveChannel = 0;
+ int m_firstActiveBlock = 0;
+ int m_lastActiveBlock = 0;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionListener.cpp b/xbmc/pvr/guilib/PVRGUIActionListener.cpp
new file mode 100644
index 0000000..3e68386
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionListener.cpp
@@ -0,0 +1,420 @@
+/*
+ * 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 "PVRGUIActionListener.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationActionListeners.h"
+#include "application/ApplicationComponents.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsClients.h"
+#include "pvr/guilib/PVRGUIActionsDatabase.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+
+#include <memory>
+#include <string>
+
+namespace PVR
+{
+
+CPVRGUIActionListener::CPVRGUIActionListener()
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appListener = components.GetComponent<CApplicationActionListeners>();
+ appListener->RegisterActionListener(this);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->RegisterCallback(
+ this,
+ {CSettings::SETTING_PVRPARENTAL_ENABLED, CSettings::SETTING_PVRMANAGER_RESETDB,
+ CSettings::SETTING_EPG_RESETEPG, CSettings::SETTING_PVRMANAGER_ADDONS,
+ CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES, CSettings::SETTING_PVRMANAGER_CHANNELMANAGER,
+ CSettings::SETTING_PVRMANAGER_GROUPMANAGER, CSettings::SETTING_PVRMANAGER_CHANNELSCAN,
+ CSettings::SETTING_PVRMENU_SEARCHICONS, CSettings::SETTING_PVRCLIENT_MENUHOOK,
+ CSettings::SETTING_EPG_PAST_DAYSTODISPLAY, CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY});
+}
+
+CPVRGUIActionListener::~CPVRGUIActionListener()
+{
+ CServiceBroker::GetSettingsComponent()->GetSettings()->UnregisterCallback(this);
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appListener = components.GetComponent<CApplicationActionListeners>();
+ appListener->UnregisterActionListener(this);
+}
+
+void CPVRGUIActionListener::Init(CPVRManager& mgr)
+{
+ mgr.Events().Subscribe(this, &CPVRGUIActionListener::OnPVRManagerEvent);
+}
+
+void CPVRGUIActionListener::Deinit(CPVRManager& mgr)
+{
+ mgr.Events().Unsubscribe(this);
+}
+
+void CPVRGUIActionListener::OnPVRManagerEvent(const PVREvent& event)
+{
+ if (event == PVREvent::AnnounceReminder)
+ {
+ if (g_application.IsInitialized())
+ {
+ // if GUI is ready, dispatch to GUI thread and handle the action there
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(ACTION_PVR_ANNOUNCE_REMINDERS)));
+ }
+ }
+}
+
+ChannelSwitchMode CPVRGUIActionListener::GetChannelSwitchMode(int iAction)
+{
+ if ((iAction == ACTION_MOVE_UP || iAction == ACTION_MOVE_DOWN) &&
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRPLAYBACK_CONFIRMCHANNELSWITCH))
+ return ChannelSwitchMode::NO_SWITCH;
+
+ return ChannelSwitchMode::INSTANT_OR_DELAYED_SWITCH;
+}
+
+bool CPVRGUIActionListener::OnAction(const CAction& action)
+{
+ bool bIsJumpSMS = false;
+ bool bIsPlayingPVR = CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying() &&
+ g_application.CurrentFileItem().HasPVRChannelInfoTag();
+
+ switch (action.GetID())
+ {
+ case ACTION_PVR_PLAY:
+ case ACTION_PVR_PLAY_TV:
+ case ACTION_PVR_PLAY_RADIO:
+ {
+ // see if we're already playing a PVR stream and if not or the stream type
+ // doesn't match the demanded type, start playback of according type
+ switch (action.GetID())
+ {
+ case ACTION_PVR_PLAY:
+ if (!bIsPlayingPVR)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ PlaybackTypeAny);
+ break;
+ case ACTION_PVR_PLAY_TV:
+ if (!bIsPlayingPVR || g_application.CurrentFileItem().GetPVRChannelInfoTag()->IsRadio())
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ PlaybackTypeTV);
+ break;
+ case ACTION_PVR_PLAY_RADIO:
+ if (!bIsPlayingPVR || !g_application.CurrentFileItem().GetPVRChannelInfoTag()->IsRadio())
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ PlaybackTypeRadio);
+ break;
+ }
+ return true;
+ }
+
+ case ACTION_JUMP_SMS2:
+ case ACTION_JUMP_SMS3:
+ case ACTION_JUMP_SMS4:
+ case ACTION_JUMP_SMS5:
+ case ACTION_JUMP_SMS6:
+ case ACTION_JUMP_SMS7:
+ case ACTION_JUMP_SMS8:
+ case ACTION_JUMP_SMS9:
+ bIsJumpSMS = true;
+ // fallthru is intended
+ [[fallthrough]];
+ case REMOTE_0:
+ case REMOTE_1:
+ case REMOTE_2:
+ case REMOTE_3:
+ case REMOTE_4:
+ case REMOTE_5:
+ case REMOTE_6:
+ case REMOTE_7:
+ case REMOTE_8:
+ case REMOTE_9:
+ case ACTION_CHANNEL_NUMBER_SEP:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ if (CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_FULLSCREEN_VIDEO) ||
+ CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_VISUALISATION))
+ {
+ // do not consume action if a python modal is the top most dialog
+ // as a python modal can't return that it consumed the action.
+ if (CServiceBroker::GetGUI()->GetWindowManager().IsPythonWindow(
+ CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog()))
+ return false;
+
+ char cCharacter;
+ if (action.GetID() == ACTION_CHANNEL_NUMBER_SEP)
+ {
+ cCharacter = CPVRChannelNumber::SEPARATOR;
+ }
+ else
+ {
+ int iRemote =
+ bIsJumpSMS ? action.GetID() - (ACTION_JUMP_SMS2 - REMOTE_2) : action.GetID();
+ cCharacter = static_cast<char>(iRemote - REMOTE_0) + '0';
+ }
+ CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .AppendChannelNumberCharacter(cCharacter);
+ return true;
+ }
+ return false;
+ }
+
+ case ACTION_SHOW_INFO:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelNavigator().ToggleInfo();
+ return true;
+ }
+
+ case ACTION_SELECT_ITEM:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ // If the button that caused this action matches action "Select" ...
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRPLAYBACK_CONFIRMCHANNELSWITCH) &&
+ CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNavigator()
+ .IsPreview())
+ {
+ // ... and if "confirm channel switch" setting is active and a channel
+ // preview is currently shown, switch to the currently previewed channel.
+ CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNavigator()
+ .SwitchToCurrentChannel();
+ return true;
+ }
+ else if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .CheckInputAndExecuteAction())
+ {
+ // ... or if the action was processed by direct channel number input, we're done.
+ return true;
+ }
+ return false;
+ }
+
+ case ACTION_NEXT_ITEM:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SeekForward();
+ return true;
+ }
+
+ case ACTION_PREV_ITEM:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SeekBackward(
+ CApplication::ACTION_PREV_ITEM_THRESHOLD);
+ return true;
+ }
+
+ case ACTION_MOVE_UP:
+ case ACTION_CHANNEL_UP:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNavigator()
+ .SelectNextChannel(GetChannelSwitchMode(action.GetID()));
+ return true;
+ }
+
+ case ACTION_MOVE_DOWN:
+ case ACTION_CHANNEL_DOWN:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNavigator()
+ .SelectPreviousChannel(GetChannelSwitchMode(action.GetID()));
+ return true;
+ }
+
+ case ACTION_CHANNEL_SWITCH:
+ {
+ if (!bIsPlayingPVR)
+ return false;
+
+ int iChannelNumber = static_cast<int>(action.GetAmount(0));
+ int iSubChannelNumber = static_cast<int>(action.GetAmount(1));
+
+ const std::shared_ptr<CPVRPlaybackState> playbackState =
+ CServiceBroker::GetPVRManager().PlaybackState();
+ const std::shared_ptr<CPVRChannelGroup> activeGroup =
+ playbackState->GetActiveChannelGroup(playbackState->IsPlayingRadio());
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ activeGroup->GetByChannelNumber(CPVRChannelNumber(iChannelNumber, iSubChannelNumber));
+
+ if (!groupMember)
+ return false;
+
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ CFileItem(groupMember), false);
+ return true;
+ }
+
+ case ACTION_RECORD:
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleRecordingOnPlayingChannel();
+ return true;
+ }
+
+ case ACTION_PVR_ANNOUNCE_REMINDERS:
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AnnounceReminders();
+ return true;
+ }
+ }
+ return false;
+}
+
+void CPVRGUIActionListener::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_PVRPARENTAL_ENABLED)
+ {
+ if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue() &&
+ CServiceBroker::GetSettingsComponent()
+ ->GetSettings()
+ ->GetString(CSettings::SETTING_PVRPARENTAL_PIN)
+ .empty())
+ {
+ std::string newPassword = "";
+ // password set... save it
+ if (CGUIDialogNumeric::ShowAndVerifyNewPassword(newPassword))
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(
+ CSettings::SETTING_PVRPARENTAL_PIN, newPassword);
+ // password not set... disable parental
+ else
+ std::static_pointer_cast<CSettingBool>(std::const_pointer_cast<CSetting>(setting))
+ ->SetValue(false);
+ }
+ }
+ else if (settingId == CSettings::SETTING_EPG_PAST_DAYSTODISPLAY)
+ {
+ CServiceBroker::GetPVRManager().Clients()->SetEPGMaxPastDays(
+ std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ }
+ else if (settingId == CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY)
+ {
+ CServiceBroker::GetPVRManager().Clients()->SetEPGMaxFutureDays(
+ std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ }
+}
+
+void CPVRGUIActionListener::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_PVRMANAGER_RESETDB)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Database>().ResetDatabase(false);
+ }
+ else if (settingId == CSettings::SETTING_EPG_RESETEPG)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Database>().ResetDatabase(true);
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES)
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ CGUIDialog* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetDialog(
+ WINDOW_DIALOG_PVR_CLIENT_PRIORITIES);
+ if (dialog)
+ {
+ dialog->Open();
+ CServiceBroker::GetPVRManager().ChannelGroups()->UpdateFromClients({});
+ }
+ }
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_CHANNELMANAGER)
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ CGUIDialog* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_CHANNEL_MANAGER);
+ if (dialog)
+ dialog->Open();
+ }
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_GROUPMANAGER)
+ {
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ CGUIDialog* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_GROUP_MANAGER);
+ if (dialog)
+ dialog->Open();
+ }
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_CHANNELSCAN)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().StartChannelScan();
+ }
+ else if (settingId == CSettings::SETTING_PVRMENU_SEARCHICONS)
+ {
+ CServiceBroker::GetPVRManager().TriggerSearchMissingChannelIcons();
+ }
+ else if (settingId == CSettings::SETTING_PVRCLIENT_MENUHOOK)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Clients>().ProcessSettingsMenuHooks();
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_ADDONS)
+ {
+ const std::vector<std::string> params{"addons://default_binary_addons_source/kodi.pvrclient",
+ "return"};
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_ADDON_BROWSER, params);
+ }
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionListener.h b/xbmc/pvr/guilib/PVRGUIActionListener.h
new file mode 100644
index 0000000..d24818b
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionListener.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "interfaces/IActionListener.h"
+#include "settings/lib/ISettingCallback.h"
+
+namespace PVR
+{
+
+class CPVRManager;
+enum class ChannelSwitchMode;
+enum class PVREvent;
+
+class CPVRGUIActionListener : public IActionListener, public ISettingCallback
+{
+public:
+ CPVRGUIActionListener();
+ ~CPVRGUIActionListener() override;
+
+ void Init(CPVRManager& mgr);
+ void Deinit(CPVRManager& mgr);
+
+ // IActionListener implementation
+ bool OnAction(const CAction& action) override;
+
+ // ISettingCallback implementation
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ void OnPVRManagerEvent(const PVREvent& event);
+
+private:
+ CPVRGUIActionListener(const CPVRGUIActionListener&) = delete;
+ CPVRGUIActionListener& operator=(const CPVRGUIActionListener&) = delete;
+
+ static ChannelSwitchMode GetChannelSwitchMode(int iAction);
+};
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp b/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp
new file mode 100644
index 0000000..80fb90e
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2016-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 "PVRGUIActionsChannels.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/windows/GUIWindowPVRBase.h"
+#include "settings/Settings.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <chrono>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+void CPVRChannelSwitchingInputHandler::AppendChannelNumberCharacter(char cCharacter)
+{
+ // special case. if only a single zero was typed in, switch to previously played channel.
+ if (GetCurrentDigitCount() == 0 && cCharacter == '0')
+ {
+ SwitchToPreviousChannel();
+ return;
+ }
+
+ CPVRChannelNumberInputHandler::AppendChannelNumberCharacter(cCharacter);
+}
+
+void CPVRChannelSwitchingInputHandler::GetChannelNumbers(std::vector<std::string>& channelNumbers)
+{
+ const CPVRManager& pvrMgr = CServiceBroker::GetPVRManager();
+ const std::shared_ptr<CPVRChannel> playingChannel = pvrMgr.PlaybackState()->GetPlayingChannel();
+ if (playingChannel)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ pvrMgr.ChannelGroups()->GetGroupAll(playingChannel->IsRadio());
+ if (group)
+ group->GetChannelNumbers(channelNumbers);
+ }
+}
+
+void CPVRChannelSwitchingInputHandler::OnInputDone()
+{
+ CPVRChannelNumber channelNumber = GetChannelNumber();
+ if (channelNumber.GetChannelNumber())
+ SwitchToChannel(channelNumber);
+}
+
+void CPVRChannelSwitchingInputHandler::SwitchToChannel(const CPVRChannelNumber& channelNumber)
+{
+ if (channelNumber.IsValid() && CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying())
+ {
+ const std::shared_ptr<CPVRChannel> playingChannel =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (playingChannel)
+ {
+ bool bRadio = playingChannel->IsRadio();
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bRadio);
+
+ if (channelNumber != group->GetChannelNumber(playingChannel))
+ {
+ // channel number present in active group?
+ std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ group->GetByChannelNumber(channelNumber);
+
+ if (!groupMember)
+ {
+ // channel number present in any group?
+ const CPVRChannelGroups* groupAccess =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio);
+ const std::vector<std::shared_ptr<CPVRChannelGroup>> groups =
+ groupAccess->GetMembers(true);
+ for (const auto& currentGroup : groups)
+ {
+ if (currentGroup == group) // we have already checked this group
+ continue;
+
+ groupMember = currentGroup->GetByChannelNumber(channelNumber);
+ if (groupMember)
+ break;
+ }
+ }
+
+ if (groupMember)
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(
+ ACTION_CHANNEL_SWITCH, static_cast<float>(channelNumber.GetChannelNumber()),
+ static_cast<float>(channelNumber.GetSubChannelNumber()))));
+ }
+ }
+ }
+ }
+}
+
+void CPVRChannelSwitchingInputHandler::SwitchToPreviousChannel()
+{
+ const std::shared_ptr<CPVRPlaybackState> playbackState =
+ CServiceBroker::GetPVRManager().PlaybackState();
+ if (playbackState->IsPlaying())
+ {
+ const std::shared_ptr<CPVRChannel> playingChannel = playbackState->GetPlayingChannel();
+ if (playingChannel)
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ playbackState->GetPreviousToLastPlayedChannelGroupMember(playingChannel->IsRadio());
+ if (groupMember)
+ {
+ const CPVRChannelNumber channelNumber = groupMember->ChannelNumber();
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(
+ ACTION_CHANNEL_SWITCH, static_cast<float>(channelNumber.GetChannelNumber()),
+ static_cast<float>(channelNumber.GetSubChannelNumber()))));
+ }
+ }
+ }
+}
+
+CPVRGUIActionsChannels::CPVRGUIActionsChannels()
+ : m_settings({CSettings::SETTING_PVRMANAGER_PRESELECTPLAYINGCHANNEL})
+{
+ RegisterChannelNumberInputHandler(&m_channelNumberInputHandler);
+}
+
+CPVRGUIActionsChannels::~CPVRGUIActionsChannels()
+{
+ DeregisterChannelNumberInputHandler(&m_channelNumberInputHandler);
+}
+
+void CPVRGUIActionsChannels::RegisterChannelNumberInputHandler(
+ CPVRChannelNumberInputHandler* handler)
+{
+ if (handler)
+ handler->Events().Subscribe(this, &CPVRGUIActionsChannels::Notify);
+}
+
+void CPVRGUIActionsChannels::DeregisterChannelNumberInputHandler(
+ CPVRChannelNumberInputHandler* handler)
+{
+ if (handler)
+ handler->Events().Unsubscribe(this);
+}
+
+void CPVRGUIActionsChannels::Notify(const PVRChannelNumberInputChangedEvent& event)
+{
+ m_events.Publish(event);
+}
+
+bool CPVRGUIActionsChannels::HideChannel(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel = item.GetPVRChannelInfoTag();
+
+ if (!channel)
+ return false;
+
+ if (!CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19054}, // "Hide channel"
+ CVariant{19039}, // "Are you sure you want to hide this channel?"
+ CVariant{""}, CVariant{channel->ChannelName()}))
+ return false;
+
+ if (!CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->GetGroupAll(channel->IsRadio())
+ ->RemoveFromGroup(channel))
+ return false;
+
+ CGUIWindowPVRBase* pvrWindow =
+ dynamic_cast<CGUIWindowPVRBase*>(CServiceBroker::GetGUI()->GetWindowManager().GetWindow(
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()));
+ if (pvrWindow)
+ pvrWindow->DoRefresh();
+ else
+ CLog::LogF(LOGERROR, "Called on non-pvr window. No refresh possible.");
+
+ return true;
+}
+
+bool CPVRGUIActionsChannels::StartChannelScan()
+{
+ return StartChannelScan(PVR_INVALID_CLIENT_ID);
+}
+
+bool CPVRGUIActionsChannels::StartChannelScan(int clientId)
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted() || IsRunningChannelScan())
+ return false;
+
+ std::shared_ptr<CPVRClient> scanClient;
+ std::vector<std::shared_ptr<CPVRClient>> possibleScanClients =
+ CServiceBroker::GetPVRManager().Clients()->GetClientsSupportingChannelScan();
+ m_bChannelScanRunning = true;
+
+ if (clientId != PVR_INVALID_CLIENT_ID)
+ {
+ const auto it =
+ std::find_if(possibleScanClients.cbegin(), possibleScanClients.cend(),
+ [clientId](const auto& client) { return client->GetID() == clientId; });
+
+ if (it != possibleScanClients.cend())
+ scanClient = (*it);
+
+ if (!scanClient)
+ {
+ CLog::LogF(LOGERROR,
+ "Provided client id '{}' could not be found in list of possible scan clients!",
+ clientId);
+ m_bChannelScanRunning = false;
+ return false;
+ }
+ }
+ /* multiple clients found */
+ else if (possibleScanClients.size() > 1)
+ {
+ CGUIDialogSelect* pDialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!pDialog)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!");
+ m_bChannelScanRunning = false;
+ return false;
+ }
+
+ pDialog->Reset();
+ pDialog->SetHeading(CVariant{19119}); // "On which backend do you want to search?"
+
+ for (const auto& client : possibleScanClients)
+ pDialog->Add(client->GetFriendlyName());
+
+ pDialog->Open();
+
+ int selection = pDialog->GetSelectedItem();
+ if (selection >= 0)
+ scanClient = possibleScanClients[selection];
+ }
+ /* one client found */
+ else if (possibleScanClients.size() == 1)
+ {
+ scanClient = possibleScanClients[0];
+ }
+ /* no clients found */
+ else if (!scanClient)
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{19033}, // "Information"
+ CVariant{19192}); // "None of the connected PVR backends supports scanning for channels."
+ m_bChannelScanRunning = false;
+ return false;
+ }
+
+ /* start the channel scan */
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Starting to scan for channels on client {}",
+ scanClient->GetFriendlyName());
+ auto start = std::chrono::steady_clock::now();
+
+ /* do the scan */
+ if (scanClient->StartChannelScan() != PVR_ERROR_NO_ERROR)
+ HELPERS::ShowOKDialogText(
+ CVariant{257}, // "Error"
+ CVariant{
+ 19193}); // "The channel scan can't be started. Check the log for more information about this message."
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Channel scan finished after {} ms", duration.count());
+
+ m_bChannelScanRunning = false;
+ return true;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRGUIActionsChannels::GetChannelGroupMember(
+ const std::shared_ptr<CPVRChannel>& channel) const
+{
+ if (!channel)
+ return {};
+
+ std::shared_ptr<CPVRChannelGroupMember> groupMember;
+
+ // first, try whether the channel is contained in the active channel group, except
+ // if a window is active which never uses the active channel group, e.g. Timers window
+ const int activeWindowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+
+ static std::vector<int> windowIDs = {
+ WINDOW_TV_RECORDINGS, WINDOW_TV_TIMERS, WINDOW_TV_TIMER_RULES, WINDOW_TV_SEARCH,
+ WINDOW_RADIO_RECORDINGS, WINDOW_RADIO_TIMERS, WINDOW_RADIO_TIMER_RULES, WINDOW_RADIO_SEARCH,
+ };
+
+ if (std::find(windowIDs.cbegin(), windowIDs.cend(), activeWindowID) == windowIDs.cend())
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(channel->IsRadio());
+ if (group)
+ groupMember = group->GetByUniqueID(channel->StorageId());
+ }
+
+ // as fallback, obtain the member from the 'all channels' group
+ if (!groupMember)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(channel->IsRadio());
+ if (group)
+ groupMember = group->GetByUniqueID(channel->StorageId());
+ }
+
+ return groupMember;
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRGUIActionsChannels::GetChannelGroupMember(
+ const CFileItem& item) const
+{
+ std::shared_ptr<CPVRChannelGroupMember> groupMember = item.GetPVRChannelGroupMemberInfoTag();
+
+ if (!groupMember)
+ groupMember = GetChannelGroupMember(CPVRItem(std::make_shared<CFileItem>(item)).GetChannel());
+
+ return groupMember;
+}
+
+CPVRChannelNumberInputHandler& CPVRGUIActionsChannels::GetChannelNumberInputHandler()
+{
+ // window/dialog specific input handler
+ CPVRChannelNumberInputHandler* windowInputHandler = dynamic_cast<CPVRChannelNumberInputHandler*>(
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow(
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog()));
+ if (windowInputHandler)
+ return *windowInputHandler;
+
+ // default
+ return m_channelNumberInputHandler;
+}
+
+CPVRGUIChannelNavigator& CPVRGUIActionsChannels::GetChannelNavigator()
+{
+ return m_channelNavigator;
+}
+
+void CPVRGUIActionsChannels::OnPlaybackStarted(const CFileItem& item)
+{
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember = GetChannelGroupMember(item);
+ if (groupMember)
+ {
+ m_channelNavigator.SetPlayingChannel(groupMember);
+ SetSelectedChannelPath(groupMember->Channel()->IsRadio(), groupMember->Path());
+ }
+}
+
+void CPVRGUIActionsChannels::OnPlaybackStopped(const CFileItem& item)
+{
+ if (item.HasPVRChannelInfoTag() || item.HasEPGInfoTag())
+ {
+ m_channelNavigator.ClearPlayingChannel();
+ }
+}
+
+void CPVRGUIActionsChannels::SetSelectedChannelPath(bool bRadio, const std::string& path)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (bRadio)
+ m_selectedChannelPathRadio = path;
+ else
+ m_selectedChannelPathTV = path;
+}
+
+std::string CPVRGUIActionsChannels::GetSelectedChannelPath(bool bRadio) const
+{
+ if (m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_PRESELECTPLAYINGCHANNEL))
+ {
+ CPVRManager& mgr = CServiceBroker::GetPVRManager();
+
+ // if preselect playing channel is activated, return the path of the playing channel, if any.
+ const std::shared_ptr<CPVRChannelGroupMember> playingChannel =
+ mgr.PlaybackState()->GetPlayingChannelGroupMember();
+ if (playingChannel && playingChannel->IsRadio() == bRadio)
+ return playingChannel->Path();
+
+ const std::shared_ptr<CPVREpgInfoTag> playingTag = mgr.PlaybackState()->GetPlayingEpgTag();
+ if (playingTag && playingTag->IsRadio() == bRadio)
+ {
+ const std::shared_ptr<CPVRChannel> channel =
+ mgr.ChannelGroups()->GetChannelForEpgTag(playingTag);
+ if (channel)
+ return GetChannelGroupMember(channel)->Path();
+ }
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return bRadio ? m_selectedChannelPathRadio : m_selectedChannelPathTV;
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsChannels.h b/xbmc/pvr/guilib/PVRGUIActionsChannels.h
new file mode 100644
index 0000000..a7656fe
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsChannels.h
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2016-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 "pvr/IPVRComponent.h"
+#include "pvr/PVRChannelNumberInputHandler.h"
+#include "pvr/guilib/PVRGUIChannelNavigator.h"
+#include "pvr/settings/PVRSettings.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRChannelGroupMember;
+
+class CPVRChannelSwitchingInputHandler : public CPVRChannelNumberInputHandler
+{
+public:
+ // CPVRChannelNumberInputHandler implementation
+ void GetChannelNumbers(std::vector<std::string>& channelNumbers) override;
+ void AppendChannelNumberCharacter(char cCharacter) override;
+ void OnInputDone() override;
+
+private:
+ /*!
+ * @brief Switch to the channel with the given number.
+ * @param channelNumber the channel number
+ */
+ void SwitchToChannel(const CPVRChannelNumber& channelNumber);
+
+ /*!
+ * @brief Switch to the previously played channel.
+ */
+ void SwitchToPreviousChannel();
+};
+
+class CPVRGUIActionsChannels : public IPVRComponent
+{
+public:
+ CPVRGUIActionsChannels();
+ ~CPVRGUIActionsChannels() override;
+
+ /*!
+ * @brief Get the events available for CEventStream.
+ * @return The events.
+ */
+ CEventStream<PVRChannelNumberInputChangedEvent>& Events() { return m_events; }
+
+ /*!
+ * @brief Register a handler for channel number input.
+ * @param handler The handler to register.
+ */
+ void RegisterChannelNumberInputHandler(CPVRChannelNumberInputHandler* handler);
+
+ /*!
+ * @brief Deregister a handler for channel number input.
+ * @param handler The handler to deregister.
+ */
+ void DeregisterChannelNumberInputHandler(CPVRChannelNumberInputHandler* handler);
+
+ /*!
+ * @brief CEventStream callback for channel number input changes.
+ * @param event The event.
+ */
+ void Notify(const PVRChannelNumberInputChangedEvent& event);
+
+ /*!
+ * @brief Hide a channel, always showing a confirmation dialog.
+ * @param item containing a channel or an epg tag.
+ * @return true on success, false otherwise.
+ */
+ bool HideChannel(const CFileItem& item) const;
+
+ /*!
+ * @brief Open a selection dialog and start a channel scan on the selected client.
+ * @return true on success, false otherwise.
+ */
+ bool StartChannelScan();
+
+ /*!
+ * @brief Start a channel scan on the specified client or open a dialog to select a client
+ * @param clientId the id of client to scan or PVR_INVALID_CLIENT_ID if a dialog will be opened
+ * @return true on success, false otherwise.
+ */
+ bool StartChannelScan(int clientId);
+
+ /*!
+ * @return True when a channel scan is currently running, false otherwise.
+ */
+ bool IsRunningChannelScan() const { return m_bChannelScanRunning; }
+
+ /*!
+ * @brief Get a channel group member for the given channel, either from the currently active
+ * group or if not found there, from the 'all channels' group.
+ * @param channel the channel.
+ * @return the group member or nullptr if not found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMember(
+ const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Get a channel group member for the given item, either from the currently active group
+ * or if not found there, from the 'all channels' group.
+ * @param item the item containing a channel, channel group, recording, timer or epg tag.
+ * @return the group member or nullptr if not found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMember(const CFileItem& item) const;
+
+ /*!
+ * @brief Get the currently active channel number input handler.
+ * @return the handler.
+ */
+ CPVRChannelNumberInputHandler& GetChannelNumberInputHandler();
+
+ /*!
+ * @brief Get the channel navigator.
+ * @return the navigator.
+ */
+ CPVRGUIChannelNavigator& GetChannelNavigator();
+
+ /*!
+ * @brief Inform GUI actions that playback of an item just started.
+ * @param item The item that started to play.
+ */
+ void OnPlaybackStarted(const CFileItem& item);
+
+ /*!
+ * @brief Inform GUI actions that playback of an item was stopped due to user interaction.
+ * @param item The item that stopped to play.
+ */
+ void OnPlaybackStopped(const CFileItem& item);
+
+ /*!
+ * @brief Get the currently selected channel item path; used across several windows/dialogs to
+ * share item selection.
+ * @param bRadio True to query the selected path for PVR radio, false for Live TV.
+ * @return the path.
+ */
+ std::string GetSelectedChannelPath(bool bRadio) const;
+
+ /*!
+ * @brief Set the currently selected channel item path; used across several windows/dialogs to
+ * share item selection.
+ * @param bRadio True to set the selected path for PVR radio, false for Live TV.
+ * @param path The new path to set.
+ */
+ void SetSelectedChannelPath(bool bRadio, const std::string& path);
+
+private:
+ CPVRGUIActionsChannels(const CPVRGUIActionsChannels&) = delete;
+ CPVRGUIActionsChannels const& operator=(CPVRGUIActionsChannels const&) = delete;
+
+ CPVRChannelSwitchingInputHandler m_channelNumberInputHandler;
+ bool m_bChannelScanRunning{false};
+ CPVRGUIChannelNavigator m_channelNavigator;
+ CEventSource<PVRChannelNumberInputChangedEvent> m_events;
+
+ mutable CCriticalSection m_critSection;
+ CPVRSettings m_settings;
+ std::string m_selectedChannelPathTV;
+ std::string m_selectedChannelPathRadio;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Channels = CPVRGUIActionsChannels;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsClients.cpp b/xbmc/pvr/guilib/PVRGUIActionsClients.cpp
new file mode 100644
index 0000000..b9c598c
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsClients.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016-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 "PVRGUIActionsClients.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClientMenuHooks.h"
+#include "pvr/addons/PVRClients.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <utility>
+#include <vector>
+
+using namespace KODI::MESSAGING;
+
+using namespace PVR;
+
+bool CPVRGUIActionsClients::ProcessSettingsMenuHooks()
+{
+ const CPVRClientMap clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients();
+
+ std::vector<std::pair<std::shared_ptr<CPVRClient>, CPVRClientMenuHook>> settingsHooks;
+ for (const auto& client : clients)
+ {
+ const auto hooks = client.second->GetMenuHooks()->GetSettingsHooks();
+ std::transform(hooks.cbegin(), hooks.cend(), std::back_inserter(settingsHooks),
+ [&client](const auto& hook) { return std::make_pair(client.second, hook); });
+ }
+
+ if (settingsHooks.empty())
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{19033}, // "Information"
+ CVariant{19347}); // "None of the active PVR clients does provide client-specific settings."
+ return true; // no settings hooks, no error
+ }
+
+ auto selectedHook = settingsHooks.begin();
+
+ // if there is only one settings hook, execute it directly, otherwise let the user select
+ if (settingsHooks.size() > 1)
+ {
+ CGUIDialogSelect* pDialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!pDialog)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!");
+ return false;
+ }
+
+ pDialog->Reset();
+ pDialog->SetHeading(CVariant{19196}); // "PVR client specific actions"
+
+ for (const auto& hook : settingsHooks)
+ {
+ if (clients.size() == 1)
+ pDialog->Add(hook.second.GetLabel());
+ else
+ pDialog->Add(hook.first->GetFriendlyName() + ": " + hook.second.GetLabel());
+ }
+
+ pDialog->Open();
+
+ int selection = pDialog->GetSelectedItem();
+ if (selection < 0)
+ return true; // cancelled
+
+ std::advance(selectedHook, selection);
+ }
+ return selectedHook->first->CallSettingsMenuHook(selectedHook->second) == PVR_ERROR_NO_ERROR;
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsClients.h b/xbmc/pvr/guilib/PVRGUIActionsClients.h
new file mode 100644
index 0000000..7c48274
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsClients.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016-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 "pvr/IPVRComponent.h"
+
+namespace PVR
+{
+class CPVRGUIActionsClients : public IPVRComponent
+{
+public:
+ CPVRGUIActionsClients() = default;
+ ~CPVRGUIActionsClients() override = default;
+
+ /*!
+ * @brief Select and invoke client-specific settings actions
+ * @return true on success, false otherwise.
+ */
+ bool ProcessSettingsMenuHooks();
+
+private:
+ CPVRGUIActionsClients(const CPVRGUIActionsClients&) = delete;
+ CPVRGUIActionsClients const& operator=(CPVRGUIActionsClients const&) = delete;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Clients = CPVRGUIActionsClients;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp b/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp
new file mode 100644
index 0000000..53bbf81
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2016-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 "PVRGUIActionsDatabase.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgDatabase.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <memory>
+#include <string>
+
+using namespace PVR;
+
+namespace
+{
+class CPVRGUIDatabaseResetComponentsSelector
+{
+public:
+ CPVRGUIDatabaseResetComponentsSelector() = default;
+ virtual ~CPVRGUIDatabaseResetComponentsSelector() = default;
+
+ bool Select()
+ {
+ CGUIDialogSelect* pDlgSelect =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (!pDlgSelect)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!");
+ return false;
+ }
+
+ CFileItemList options;
+
+ const std::shared_ptr<CFileItem> itemAll =
+ std::make_shared<CFileItem>(StringUtils::Format(g_localizeStrings.Get(593))); // All
+ itemAll->SetPath("all");
+ options.Add(itemAll);
+
+ // if channels are cleared, groups, EPG data and providers must also be cleared
+ const std::shared_ptr<CFileItem> itemChannels =
+ std::make_shared<CFileItem>(StringUtils::Format("{}, {}, {}, {}",
+ g_localizeStrings.Get(19019), // Channels
+ g_localizeStrings.Get(19146), // Groups
+ g_localizeStrings.Get(19069), // Guide
+ g_localizeStrings.Get(19334))); // Providers
+ itemChannels->SetPath("channels");
+ itemChannels->Select(true); // preselect this item in dialog
+ options.Add(itemChannels);
+
+ const std::shared_ptr<CFileItem> itemGroups =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(19146)); // Groups
+ itemGroups->SetPath("groups");
+ options.Add(itemGroups);
+
+ const std::shared_ptr<CFileItem> itemGuide =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(19069)); // Guide
+ itemGuide->SetPath("guide");
+ options.Add(itemGuide);
+
+ const std::shared_ptr<CFileItem> itemProviders =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(19334)); // Providers
+ itemProviders->SetPath("providers");
+ options.Add(itemProviders);
+
+ const std::shared_ptr<CFileItem> itemReminders =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(19215)); // Reminders
+ itemReminders->SetPath("reminders");
+ options.Add(itemReminders);
+
+ const std::shared_ptr<CFileItem> itemRecordings =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(19017)); // Recordings
+ itemRecordings->SetPath("recordings");
+ options.Add(itemRecordings);
+
+ const std::shared_ptr<CFileItem> itemClients =
+ std::make_shared<CFileItem>(g_localizeStrings.Get(24019)); // PVR clients
+ itemClients->SetPath("clients");
+ options.Add(itemClients);
+
+ pDlgSelect->Reset();
+ pDlgSelect->SetHeading(CVariant{g_localizeStrings.Get(19185)}); // "Clear data"
+ pDlgSelect->SetItems(options);
+ pDlgSelect->SetMultiSelection(true);
+ pDlgSelect->Open();
+
+ if (!pDlgSelect->IsConfirmed())
+ return false;
+
+ for (int i : pDlgSelect->GetSelectedItems())
+ {
+ const std::string path = options.Get(i)->GetPath();
+
+ m_bResetChannels |= (path == "channels" || path == "all");
+ m_bResetGroups |= (path == "groups" || path == "all");
+ m_bResetGuide |= (path == "guide" || path == "all");
+ m_bResetProviders |= (path == "providers" || path == "all");
+ m_bResetReminders |= (path == "reminders" || path == "all");
+ m_bResetRecordings |= (path == "recordings" || path == "all");
+ m_bResetClients |= (path == "clients" || path == "all");
+ }
+
+ m_bResetGroups |= m_bResetChannels;
+ m_bResetGuide |= m_bResetChannels;
+ m_bResetProviders |= m_bResetChannels;
+
+ return (m_bResetChannels || m_bResetGroups || m_bResetGuide || m_bResetProviders ||
+ m_bResetReminders || m_bResetRecordings || m_bResetClients);
+ }
+
+ bool IsResetChannelsSelected() const { return m_bResetChannels; }
+ bool IsResetGroupsSelected() const { return m_bResetGroups; }
+ bool IsResetGuideSelected() const { return m_bResetGuide; }
+ bool IsResetProvidersSelected() const { return m_bResetProviders; }
+ bool IsResetRemindersSelected() const { return m_bResetReminders; }
+ bool IsResetRecordingsSelected() const { return m_bResetRecordings; }
+ bool IsResetClientsSelected() const { return m_bResetClients; }
+
+private:
+ bool m_bResetChannels = false;
+ bool m_bResetGroups = false;
+ bool m_bResetGuide = false;
+ bool m_bResetProviders = false;
+ bool m_bResetReminders = false;
+ bool m_bResetRecordings = false;
+ bool m_bResetClients = false;
+};
+
+} // unnamed namespace
+
+bool CPVRGUIActionsDatabase::ResetDatabase(bool bResetEPGOnly)
+{
+ CGUIDialogProgress* pDlgProgress =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
+ WINDOW_DIALOG_PROGRESS);
+ if (!pDlgProgress)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PROGRESS!");
+ return false;
+ }
+
+ bool bResetChannels = false;
+ bool bResetGroups = false;
+ bool bResetGuide = false;
+ bool bResetProviders = false;
+ bool bResetReminders = false;
+ bool bResetRecordings = false;
+ bool bResetClients = false;
+
+ if (bResetEPGOnly)
+ {
+ if (!CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19098}, // "Warning!"
+ CVariant{19188})) // "All guide data will be cleared. Are you sure?"
+ return false;
+
+ bResetGuide = true;
+ }
+ else
+ {
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalPIN() !=
+ ParentalCheckResult::SUCCESS)
+ return false;
+
+ CPVRGUIDatabaseResetComponentsSelector selector;
+ if (!selector.Select())
+ return false;
+
+ if (!CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19098}, // "Warning!"
+ CVariant{19186})) // "All selected data will be cleared. ... Are you sure?"
+ return false;
+
+ bResetChannels = selector.IsResetChannelsSelected();
+ bResetGroups = selector.IsResetGroupsSelected();
+ bResetGuide = selector.IsResetGuideSelected();
+ bResetProviders = selector.IsResetProvidersSelected();
+ bResetReminders = selector.IsResetRemindersSelected();
+ bResetRecordings = selector.IsResetRecordingsSelected();
+ bResetClients = selector.IsResetClientsSelected();
+ }
+
+ CDateTime::ResetTimezoneBias();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "PVR clearing {} database", bResetEPGOnly ? "EPG" : "PVR and EPG");
+
+ pDlgProgress->SetHeading(CVariant{313}); // "Cleaning database"
+ pDlgProgress->SetLine(0, CVariant{g_localizeStrings.Get(19187)}); // "Clearing all related data."
+ pDlgProgress->SetLine(1, CVariant{""});
+ pDlgProgress->SetLine(2, CVariant{""});
+
+ pDlgProgress->Open();
+ pDlgProgress->Progress();
+
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying())
+ {
+ CLog::Log(LOGINFO, "PVR is stopping playback for {} database reset",
+ bResetEPGOnly ? "EPG" : "PVR and EPG");
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP);
+ }
+
+ const std::shared_ptr<CPVRDatabase> pvrDatabase(CServiceBroker::GetPVRManager().GetTVDatabase());
+ const std::shared_ptr<CPVREpgDatabase> epgDatabase(
+ CServiceBroker::GetPVRManager().EpgContainer().GetEpgDatabase());
+
+ // increase db open refcounts, so they don't get closed during following pvr manager shutdown
+ pvrDatabase->Open();
+ epgDatabase->Open();
+
+ // stop pvr manager; close both pvr and epg databases
+ CServiceBroker::GetPVRManager().Stop();
+
+ const int iProgressStepPercentage =
+ 100 / ((2 * bResetChannels) + bResetGroups + bResetGuide + bResetProviders + bResetReminders +
+ bResetRecordings + bResetClients + 1);
+ int iProgressStepsDone = 0;
+
+ if (bResetProviders)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all providers
+ pvrDatabase->DeleteProviders();
+ }
+
+ if (bResetGuide)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // reset channel's EPG pointers
+ pvrDatabase->ResetEPG();
+
+ // delete all entries from the EPG database
+ epgDatabase->DeleteEpg();
+ }
+
+ if (bResetGroups)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all channel groups (including data only available locally, like user defined groups)
+ pvrDatabase->DeleteChannelGroups();
+ }
+
+ if (bResetChannels)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all channels (including data only available locally, like user set icons)
+ pvrDatabase->DeleteChannels();
+ }
+
+ if (bResetReminders)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all timers data (e.g. all reminders, which are only stored locally)
+ pvrDatabase->DeleteTimers();
+ }
+
+ if (bResetClients)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all clients data (e.g priorities, which are only stored locally)
+ pvrDatabase->DeleteClients();
+ }
+
+ if (bResetChannels || bResetRecordings)
+ {
+ CVideoDatabase videoDatabase;
+
+ if (videoDatabase.Open())
+ {
+ if (bResetChannels)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all channel's entries (e.g. settings, bookmarks, stream details)
+ videoDatabase.EraseAllForPath("pvr://channels/");
+ }
+
+ if (bResetRecordings)
+ {
+ pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone);
+ pDlgProgress->Progress();
+
+ // delete all recording's entries (e.g. settings, bookmarks, stream details)
+ videoDatabase.EraseAllForPath(CPVRRecordingsPath::PATH_RECORDINGS);
+ }
+
+ videoDatabase.Close();
+ }
+ }
+
+ // decrease db open refcounts; this actually closes dbs because refcounts drops to zero
+ pvrDatabase->Close();
+ epgDatabase->Close();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "{} database cleared", bResetEPGOnly ? "EPG" : "PVR and EPG");
+
+ CLog::Log(LOGINFO, "Restarting the PVR Manager after {} database reset",
+ bResetEPGOnly ? "EPG" : "PVR and EPG");
+ CServiceBroker::GetPVRManager().Start();
+
+ pDlgProgress->SetPercentage(100);
+ pDlgProgress->Close();
+ return true;
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsDatabase.h b/xbmc/pvr/guilib/PVRGUIActionsDatabase.h
new file mode 100644
index 0000000..62eb484
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsDatabase.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016-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 "pvr/IPVRComponent.h"
+
+namespace PVR
+{
+class CPVRGUIActionsDatabase : public IPVRComponent
+{
+public:
+ CPVRGUIActionsDatabase() = default;
+ ~CPVRGUIActionsDatabase() override = default;
+
+ /*!
+ * @brief Reset the TV database to it's initial state and delete all the data.
+ * @param bResetEPGOnly True to only reset the EPG database, false to reset both PVR and EPG
+ * database.
+ * @return true on success, false otherwise.
+ */
+ bool ResetDatabase(bool bResetEPGOnly);
+
+private:
+ CPVRGUIActionsDatabase(const CPVRGUIActionsDatabase&) = delete;
+ CPVRGUIActionsDatabase const& operator=(CPVRGUIActionsDatabase const&) = delete;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Database = CPVRGUIActionsDatabase;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp b/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp
new file mode 100644
index 0000000..8b5cc9b
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2016-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 "PVRGUIActionsEPG.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/dialogs/GUIDialogPVRChannelGuide.h"
+#include "pvr/dialogs/GUIDialogPVRGuideInfo.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "pvr/windows/GUIWindowPVRSearch.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <string>
+
+using namespace PVR;
+
+namespace
+{
+PVR::CGUIWindowPVRSearchBase* GetSearchWindow(bool bRadio)
+{
+ const int windowSearchId = bRadio ? WINDOW_RADIO_SEARCH : WINDOW_TV_SEARCH;
+
+ PVR::CGUIWindowPVRSearchBase* windowSearch;
+
+ CGUIWindowManager& windowMgr = CServiceBroker::GetGUI()->GetWindowManager();
+ if (bRadio)
+ windowSearch = windowMgr.GetWindow<PVR::CGUIWindowPVRRadioSearch>(windowSearchId);
+ else
+ windowSearch = windowMgr.GetWindow<PVR::CGUIWindowPVRTVSearch>(windowSearchId);
+
+ if (!windowSearch)
+ CLog::LogF(LOGERROR, "Unable to get {}!", bRadio ? "WINDOW_RADIO_SEARCH" : "WINDOW_TV_SEARCH");
+
+ return windowSearch;
+}
+} // unnamed namespace
+
+bool CPVRGUIActionsEPG::ShowEPGInfo(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel());
+ if (channel && CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(
+ channel) != ParentalCheckResult::SUCCESS)
+ return false;
+
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(CPVRItem(item).GetEpgInfoTag());
+ if (!epgTag)
+ {
+ CLog::LogF(LOGERROR, "No epg tag!");
+ return false;
+ }
+
+ CGUIDialogPVRGuideInfo* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGuideInfo>(
+ WINDOW_DIALOG_PVR_GUIDE_INFO);
+ if (!pDlgInfo)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_GUIDE_INFO!");
+ return false;
+ }
+
+ pDlgInfo->SetProgInfo(std::make_shared<CFileItem>(epgTag));
+ pDlgInfo->Open();
+ return true;
+}
+
+bool CPVRGUIActionsEPG::ShowChannelEPG(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel());
+ if (channel && CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(
+ channel) != ParentalCheckResult::SUCCESS)
+ return false;
+
+ CGUIDialogPVRChannelGuide* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRChannelGuide>(
+ WINDOW_DIALOG_PVR_CHANNEL_GUIDE);
+ if (!pDlgInfo)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_CHANNEL_GUIDE!");
+ return false;
+ }
+
+ pDlgInfo->Open(channel);
+ return true;
+}
+
+bool CPVRGUIActionsEPG::FindSimilar(const CFileItem& item) const
+{
+ CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(CPVRItem(item).IsRadio());
+ if (!windowSearch)
+ return false;
+
+ //! @todo If we want dialogs to spawn program search in a clean way - without having to force-close any
+ // other dialogs - we must introduce a search dialog with functionality similar to the search window.
+
+ for (int iId = CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog(
+ true /* ignoreClosing */);
+ iId != WINDOW_INVALID;
+ iId = CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog(
+ true /* ignoreClosing */))
+ {
+ CLog::LogF(LOGWARNING,
+ "Have to close modal dialog with id {} before search window can be opened.", iId);
+
+ CGUIWindow* window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(iId);
+ if (window)
+ {
+ window->Close();
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to get window instance {}! Cannot open search window.", iId);
+ return false; // return, otherwise we run into an endless loop
+ }
+ }
+
+ windowSearch->SetItemToSearch(item);
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID());
+ return true;
+};
+
+bool CPVRGUIActionsEPG::ExecuteSavedSearch(const CFileItem& item)
+{
+ const auto searchFilter = item.GetEPGSearchFilter();
+
+ if (!searchFilter)
+ {
+ CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present.");
+ return false;
+ }
+
+ CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(searchFilter->IsRadio());
+ if (!windowSearch)
+ return false;
+
+ windowSearch->SetItemToSearch(item);
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID());
+ return true;
+}
+
+bool CPVRGUIActionsEPG::EditSavedSearch(const CFileItem& item)
+{
+ const auto searchFilter = item.GetEPGSearchFilter();
+
+ if (!searchFilter)
+ {
+ CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present.");
+ return false;
+ }
+
+ CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(searchFilter->IsRadio());
+ if (!windowSearch)
+ return false;
+
+ if (windowSearch->OpenDialogSearch(item) == CGUIDialogPVRGuideSearch::Result::SEARCH)
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID());
+
+ return true;
+}
+
+bool CPVRGUIActionsEPG::RenameSavedSearch(const CFileItem& item)
+{
+ const auto searchFilter = item.GetEPGSearchFilter();
+
+ if (!searchFilter)
+ {
+ CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present.");
+ return false;
+ }
+
+ std::string title = searchFilter->GetTitle();
+ if (CGUIKeyboardFactory::ShowAndGetInput(title,
+ CVariant{g_localizeStrings.Get(528)}, // "Enter title"
+ false))
+ {
+ searchFilter->SetTitle(title);
+ CServiceBroker::GetPVRManager().EpgContainer().PersistSavedSearch(*searchFilter);
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIActionsEPG::DeleteSavedSearch(const CFileItem& item)
+{
+ const auto searchFilter = item.GetEPGSearchFilter();
+
+ if (!searchFilter)
+ {
+ CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present.");
+ return false;
+ }
+
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{122}, // "Confirm delete"
+ CVariant{19338}, // "Delete this saved search?"
+ CVariant{""}, CVariant{item.GetLabel()}))
+ {
+ return CServiceBroker::GetPVRManager().EpgContainer().DeleteSavedSearch(*searchFilter);
+ }
+ return false;
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsEPG.h b/xbmc/pvr/guilib/PVRGUIActionsEPG.h
new file mode 100644
index 0000000..e0a3edc
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsEPG.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016-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 "pvr/IPVRComponent.h"
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRGUIActionsEPG : public IPVRComponent
+{
+public:
+ CPVRGUIActionsEPG() = default;
+ ~CPVRGUIActionsEPG() override = default;
+
+ /*!
+ * @brief Open a dialog with epg information for a given item.
+ * @param item containing epg data to show. item must be an epg tag, a channel or a timer.
+ * @return true on success, false otherwise.
+ */
+ bool ShowEPGInfo(const CFileItem& item) const;
+
+ /*!
+ * @brief Open a dialog with the epg list for a given item.
+ * @param item containing channel info. item must be an epg tag, a channel or a timer.
+ * @return true on success, false otherwise.
+ */
+ bool ShowChannelEPG(const CFileItem& item) const;
+
+ /*!
+ * @brief Open a window containing a list of epg tags 'similar' to a given item.
+ * @param item containing epg data for matching. item must be an epg tag, a channel or a
+ * recording.
+ * @return true on success, false otherwise.
+ */
+ bool FindSimilar(const CFileItem& item) const;
+
+ /*!
+ * @brief Execute a saved search. Displays result in search window if it is open.
+ * @param item The item containing a search filter.
+ * @return True on success, false otherwise.
+ */
+ bool ExecuteSavedSearch(const CFileItem& item);
+
+ /*!
+ * @brief Edit a saved search. Opens the search dialog.
+ * @param item The item containing a search filter.
+ * @return True on success, false otherwise.
+ */
+ bool EditSavedSearch(const CFileItem& item);
+
+ /*!
+ * @brief Rename a saved search. Opens a title input dialog.
+ * @param item The item containing a search filter.
+ * @return True on success, false otherwise.
+ */
+ bool RenameSavedSearch(const CFileItem& item);
+
+ /*!
+ * @brief Delete a saved search. Opens confirmation dialog before deleting.
+ * @param item The item containing a search filter.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteSavedSearch(const CFileItem& item);
+
+private:
+ CPVRGUIActionsEPG(const CPVRGUIActionsEPG&) = delete;
+ CPVRGUIActionsEPG const& operator=(CPVRGUIActionsEPG const&) = delete;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using EPG = CPVRGUIActionsEPG;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp
new file mode 100644
index 0000000..96947c4
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016-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 "PVRGUIActionsParentalControl.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "settings/Settings.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <string>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+CPVRGUIActionsParentalControl::CPVRGUIActionsParentalControl()
+ : m_settings({CSettings::SETTING_PVRPARENTAL_PIN, CSettings::SETTING_PVRPARENTAL_ENABLED})
+{
+}
+
+ParentalCheckResult CPVRGUIActionsParentalControl::CheckParentalLock(
+ const std::shared_ptr<CPVRChannel>& channel) const
+{
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(channel))
+ return ParentalCheckResult::SUCCESS;
+
+ ParentalCheckResult ret = CheckParentalPIN();
+
+ if (ret == ParentalCheckResult::FAILED)
+ CLog::LogF(LOGERROR, "Parental lock verification failed for channel '{}': wrong PIN entered.",
+ channel->ChannelName());
+
+ return ret;
+}
+
+ParentalCheckResult CPVRGUIActionsParentalControl::CheckParentalPIN() const
+{
+ if (!m_settings.GetBoolValue(CSettings::SETTING_PVRPARENTAL_ENABLED))
+ return ParentalCheckResult::SUCCESS;
+
+ std::string pinCode = m_settings.GetStringValue(CSettings::SETTING_PVRPARENTAL_PIN);
+ if (pinCode.empty())
+ return ParentalCheckResult::SUCCESS;
+
+ InputVerificationResult ret = CGUIDialogNumeric::ShowAndVerifyInput(
+ pinCode, g_localizeStrings.Get(19262), true); // "Parental control. Enter PIN:"
+
+ if (ret == InputVerificationResult::SUCCESS)
+ {
+ CServiceBroker::GetPVRManager().RestartParentalTimer();
+ return ParentalCheckResult::SUCCESS;
+ }
+ else if (ret == InputVerificationResult::FAILED)
+ {
+ HELPERS::ShowOKDialogText(CVariant{19264},
+ CVariant{19265}); // "Incorrect PIN", "The entered PIN was incorrect."
+ return ParentalCheckResult::FAILED;
+ }
+ else
+ {
+ return ParentalCheckResult::CANCELED;
+ }
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h
new file mode 100644
index 0000000..921f597
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016-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 "pvr/IPVRComponent.h"
+#include "pvr/settings/PVRSettings.h"
+
+#include <memory>
+
+namespace PVR
+{
+enum class ParentalCheckResult
+{
+ CANCELED,
+ FAILED,
+ SUCCESS
+};
+
+class CPVRChannel;
+
+class CPVRGUIActionsParentalControl : public IPVRComponent
+{
+public:
+ CPVRGUIActionsParentalControl();
+ ~CPVRGUIActionsParentalControl() override = default;
+
+ /*!
+ * @brief Check if channel is parental locked. Ask for PIN if necessary.
+ * @param channel The channel to do the check for.
+ * @return the result of the check (success, failed, or canceled by user).
+ */
+ ParentalCheckResult CheckParentalLock(const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @brief Open Numeric dialog to check for parental PIN.
+ * @return the result of the check (success, failed, or canceled by user).
+ */
+ ParentalCheckResult CheckParentalPIN() const;
+
+private:
+ CPVRGUIActionsParentalControl(const CPVRGUIActionsParentalControl&) = delete;
+ CPVRGUIActionsParentalControl const& operator=(CPVRGUIActionsParentalControl const&) = delete;
+
+ CPVRSettings m_settings;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Parental = CPVRGUIActionsParentalControl;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp b/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp
new file mode 100644
index 0000000..66fdb80
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp
@@ -0,0 +1,564 @@
+/*
+ * Copyright (C) 2016-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 "PVRGUIActionsPlayback.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationEnums.h"
+#include "cores/DataCacheCore.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/PVRStreamProperties.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+CPVRGUIActionsPlayback::CPVRGUIActionsPlayback()
+ : m_settings({CSettings::SETTING_LOOKANDFEEL_STARTUPACTION,
+ CSettings::SETTING_PVRPLAYBACK_SWITCHTOFULLSCREENCHANNELTYPES})
+{
+}
+
+std::string CPVRGUIActionsPlayback::GetResumeLabel(const CFileItem& item) const
+{
+ std::string resumeString;
+
+ const std::shared_ptr<CPVRRecording> recording(
+ CPVRItem(CFileItemPtr(new CFileItem(item))).GetRecording());
+ if (recording && !recording->IsDeleted())
+ {
+ int positionInSeconds = lrint(recording->GetResumePoint().timeInSeconds);
+ if (positionInSeconds > 0)
+ resumeString = StringUtils::Format(
+ g_localizeStrings.Get(12022),
+ StringUtils::SecondsToTimeString(positionInSeconds, TIME_FORMAT_HH_MM_SS));
+ }
+ return resumeString;
+}
+
+bool CPVRGUIActionsPlayback::CheckResumeRecording(const CFileItem& item) const
+{
+ bool bPlayIt(true);
+ std::string resumeString(GetResumeLabel(item));
+ if (!resumeString.empty())
+ {
+ CContextButtons choices;
+ choices.Add(CONTEXT_BUTTON_RESUME_ITEM, resumeString);
+ choices.Add(CONTEXT_BUTTON_PLAY_ITEM, 12021); // Play from beginning
+ int choice = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (choice > 0)
+ const_cast<CFileItem*>(&item)->SetStartOffset(
+ choice == CONTEXT_BUTTON_RESUME_ITEM ? STARTOFFSET_RESUME : 0);
+ else
+ bPlayIt = false; // context menu cancelled
+ }
+ return bPlayIt;
+}
+
+bool CPVRGUIActionsPlayback::ResumePlayRecording(const CFileItem& item, bool bFallbackToPlay) const
+{
+ bool bCanResume = !GetResumeLabel(item).empty();
+ if (bCanResume)
+ {
+ const_cast<CFileItem*>(&item)->SetStartOffset(STARTOFFSET_RESUME);
+ }
+ else
+ {
+ if (bFallbackToPlay)
+ const_cast<CFileItem*>(&item)->SetStartOffset(0);
+ else
+ return false;
+ }
+
+ return PlayRecording(item, false);
+}
+
+void CPVRGUIActionsPlayback::CheckAndSwitchToFullscreen(bool bFullscreen) const
+{
+ CMediaSettings::GetInstance().SetMediaStartWindowed(!bFullscreen);
+
+ if (bFullscreen)
+ {
+ CGUIMessage msg(GUI_MSG_FULLSCREEN, 0,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+}
+
+void CPVRGUIActionsPlayback::StartPlayback(CFileItem* item,
+ bool bFullscreen,
+ const CPVRStreamProperties* epgProps) const
+{
+ // Obtain dynamic playback url and properties from the respective pvr client
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item);
+ if (client)
+ {
+ CPVRStreamProperties props;
+
+ if (item->IsPVRChannel())
+ {
+ // If this was an EPG Tag to be played as live then PlayEpgTag() will create a channel
+ // fileitem instead and pass the epg tags props so we use those and skip the client call
+ if (epgProps)
+ props = *epgProps;
+ else
+ client->GetChannelStreamProperties(item->GetPVRChannelInfoTag(), props);
+ }
+ else if (item->IsPVRRecording())
+ {
+ client->GetRecordingStreamProperties(item->GetPVRRecordingInfoTag(), props);
+ }
+ else if (item->IsEPG())
+ {
+ if (epgProps) // we already have props from PlayEpgTag()
+ props = *epgProps;
+ else
+ client->GetEpgTagStreamProperties(item->GetEPGInfoTag(), props);
+ }
+
+ if (props.size())
+ {
+ const std::string url = props.GetStreamURL();
+ if (!url.empty())
+ item->SetDynPath(url);
+
+ const std::string mime = props.GetStreamMimeType();
+ if (!mime.empty())
+ {
+ item->SetMimeType(mime);
+ item->SetContentLookup(false);
+ }
+
+ for (const auto& prop : props)
+ item->SetProperty(prop.first, prop.second);
+ }
+ }
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0, static_cast<void*>(item));
+ CheckAndSwitchToFullscreen(bFullscreen);
+}
+
+bool CPVRGUIActionsPlayback::PlayRecording(const CFileItem& item, bool bCheckResume) const
+{
+ const std::shared_ptr<CPVRRecording> recording(CPVRItem(item).GetRecording());
+ if (!recording)
+ return false;
+
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRecording(recording))
+ {
+ CGUIMessage msg(GUI_MSG_FULLSCREEN, 0,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ return true;
+ }
+
+ if (!bCheckResume || CheckResumeRecording(item))
+ {
+ CFileItem* itemToPlay = new CFileItem(recording);
+ itemToPlay->SetStartOffset(item.GetStartOffset());
+ StartPlayback(itemToPlay, true);
+ }
+ return true;
+}
+
+bool CPVRGUIActionsPlayback::PlayEpgTag(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(CPVRItem(item).GetEpgInfoTag());
+ if (!epgTag)
+ return false;
+
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingEpgTag(epgTag))
+ {
+ CGUIMessage msg(GUI_MSG_FULLSCREEN, 0,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ return true;
+ }
+
+ // Obtain dynamic playback url and properties from the respective pvr client
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(epgTag->ClientID());
+ if (!client)
+ return false;
+
+ CPVRStreamProperties props;
+ client->GetEpgTagStreamProperties(epgTag, props);
+
+ CFileItem* itemToPlay = nullptr;
+ if (props.EPGPlaybackAsLive())
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(item);
+ if (!groupMember)
+ return false;
+
+ itemToPlay = new CFileItem(groupMember);
+ }
+ else
+ {
+ itemToPlay = new CFileItem(epgTag);
+ }
+
+ StartPlayback(itemToPlay, true, &props);
+ return true;
+}
+
+bool CPVRGUIActionsPlayback::SwitchToChannel(const CFileItem& item, bool bCheckResume) const
+{
+ if (item.m_bIsFolder)
+ return false;
+
+ std::shared_ptr<CPVRRecording> recording;
+ const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel());
+ if (channel)
+ {
+ bool bSwitchToFullscreen =
+ CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingChannel(channel);
+
+ if (!bSwitchToFullscreen)
+ {
+ recording =
+ CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(channel->GetEPGNow());
+ bSwitchToFullscreen =
+ recording &&
+ CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRecording(recording);
+ }
+
+ if (bSwitchToFullscreen)
+ {
+ CGUIMessage msg(GUI_MSG_FULLSCREEN, 0,
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ return true;
+ }
+ }
+
+ ParentalCheckResult result =
+ channel ? CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel)
+ : ParentalCheckResult::FAILED;
+ if (result == ParentalCheckResult::SUCCESS)
+ {
+ // switch to channel or if recording present, ask whether to switch or play recording...
+ if (!recording)
+ recording =
+ CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(channel->GetEPGNow());
+
+ if (recording)
+ {
+ bool bCancel(false);
+ bool bPlayRecording = CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19687}, // "Play recording"
+ CVariant{""}, CVariant{12021}, // "Play from beginning"
+ CVariant{recording->m_strTitle}, bCancel, CVariant{19000}, // "Switch to channel"
+ CVariant{19687}, // "Play recording"
+ 0); // no autoclose
+ if (bCancel)
+ return false;
+
+ if (bPlayRecording)
+ return PlayRecording(CFileItem(recording), bCheckResume);
+ }
+
+ bool bFullscreen;
+ switch (m_settings.GetIntValue(CSettings::SETTING_PVRPLAYBACK_SWITCHTOFULLSCREENCHANNELTYPES))
+ {
+ case 0: // never
+ bFullscreen = false;
+ break;
+ case 1: // TV channels
+ bFullscreen = !channel->IsRadio();
+ break;
+ case 2: // Radio channels
+ bFullscreen = channel->IsRadio();
+ break;
+ case 3: // TV and radio channels
+ default:
+ bFullscreen = true;
+ break;
+ }
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(item);
+ if (!groupMember)
+ return false;
+
+ StartPlayback(new CFileItem(groupMember), bFullscreen);
+ return true;
+ }
+ else if (result == ParentalCheckResult::FAILED)
+ {
+ const std::string channelName =
+ channel ? channel->ChannelName() : g_localizeStrings.Get(19029); // Channel
+ const std::string msg = StringUtils::Format(
+ g_localizeStrings.Get(19035),
+ channelName); // CHANNELNAME could not be played. Check the log for details.
+
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(19166),
+ msg); // PVR information
+ }
+
+ return false;
+}
+
+bool CPVRGUIActionsPlayback::SwitchToChannel(PlaybackType type) const
+{
+ std::shared_ptr<CPVRChannelGroupMember> groupMember;
+ bool bIsRadio(false);
+
+ // check if the desired PlaybackType is already playing,
+ // and if not, try to grab the last played channel of this type
+ switch (type)
+ {
+ case PlaybackTypeRadio:
+ {
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio())
+ return true;
+
+ const std::shared_ptr<CPVRChannelGroup> allGroup =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAllRadio();
+ if (allGroup)
+ groupMember = allGroup->GetLastPlayedChannelGroupMember();
+
+ bIsRadio = true;
+ break;
+ }
+ case PlaybackTypeTV:
+ {
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV())
+ return true;
+
+ const std::shared_ptr<CPVRChannelGroup> allGroup =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAllTV();
+ if (allGroup)
+ groupMember = allGroup->GetLastPlayedChannelGroupMember();
+
+ break;
+ }
+ default:
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying())
+ return true;
+
+ groupMember =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetLastPlayedChannelGroupMember();
+ break;
+ }
+
+ // if we have a last played channel, start playback
+ if (groupMember)
+ {
+ return SwitchToChannel(CFileItem(groupMember), true);
+ }
+ else
+ {
+ // if we don't, find the active channel group of the demanded type and play it's first channel
+ const std::shared_ptr<CPVRChannelGroup> channelGroup =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bIsRadio);
+ if (channelGroup)
+ {
+ // try to start playback of first channel in this group
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ channelGroup->GetMembers();
+ if (!groupMembers.empty())
+ {
+ return SwitchToChannel(CFileItem(*groupMembers.begin()), true);
+ }
+ }
+ }
+
+ CLog::LogF(LOGERROR,
+ "Could not determine {} channel to playback. No last played channel found, and "
+ "first channel of active group could also not be determined.",
+ bIsRadio ? "Radio" : "TV");
+
+ CGUIDialogKaiToast::QueueNotification(
+ CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(19166), // PVR information
+ StringUtils::Format(
+ g_localizeStrings.Get(19035),
+ g_localizeStrings.Get(
+ bIsRadio ? 19021
+ : 19020))); // Radio/TV could not be played. Check the log for details.
+ return false;
+}
+
+bool CPVRGUIActionsPlayback::PlayChannelOnStartup() const
+{
+ int iAction = m_settings.GetIntValue(CSettings::SETTING_LOOKANDFEEL_STARTUPACTION);
+ if (iAction != STARTUP_ACTION_PLAY_TV && iAction != STARTUP_ACTION_PLAY_RADIO)
+ return false;
+
+ bool playRadio = (iAction == STARTUP_ACTION_PLAY_RADIO);
+
+ // get the last played channel or fallback to first channel of all channels group
+ std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetLastPlayedChannelGroupMember(playRadio);
+
+ if (!groupMember)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().ChannelGroups()->Get(playRadio)->GetGroupAll();
+ auto channels = group->GetMembers();
+ if (channels.empty())
+ return false;
+
+ groupMember = channels.front();
+ if (!groupMember)
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "PVR is starting playback of channel '{}'",
+ groupMember->Channel()->ChannelName());
+ return SwitchToChannel(CFileItem(groupMember), true);
+}
+
+bool CPVRGUIActionsPlayback::PlayMedia(const CFileItem& item) const
+{
+ std::unique_ptr<CFileItem> pvrItem = std::make_unique<CFileItem>(item);
+ if (URIUtils::IsPVRChannel(item.GetPath()) && !item.HasPVRChannelInfoTag())
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelGroupMemberByPath(
+ item.GetPath());
+ if (groupMember)
+ pvrItem = std::make_unique<CFileItem>(groupMember);
+ }
+ else if (URIUtils::IsPVRRecording(item.GetPath()) && !item.HasPVRRecordingInfoTag())
+ {
+ const std::shared_ptr<CPVRRecording> recording =
+ CServiceBroker::GetPVRManager().Recordings()->GetByPath(item.GetPath());
+ if (recording)
+ {
+ pvrItem = std::make_unique<CFileItem>(recording);
+ pvrItem->SetStartOffset(item.GetStartOffset());
+ }
+ }
+ bool bCheckResume = true;
+ if (item.HasProperty("check_resume"))
+ bCheckResume = item.GetProperty("check_resume").asBoolean();
+
+ if (pvrItem && pvrItem->HasPVRChannelInfoTag())
+ {
+ return SwitchToChannel(*pvrItem, bCheckResume);
+ }
+ else if (pvrItem && pvrItem->HasPVRRecordingInfoTag())
+ {
+ return PlayRecording(*pvrItem, bCheckResume);
+ }
+
+ return false;
+}
+
+void CPVRGUIActionsPlayback::SeekForward()
+{
+ time_t playbackStartTime = CServiceBroker::GetDataCacheCore().GetStartTime();
+ if (playbackStartTime > 0)
+ {
+ const std::shared_ptr<CPVRChannel> playingChannel =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (playingChannel)
+ {
+ time_t nextTime = 0;
+ std::shared_ptr<CPVREpgInfoTag> next = playingChannel->GetEPGNext();
+ if (next)
+ {
+ next->StartAsUTC().GetAsTime(nextTime);
+ }
+ else
+ {
+ // if there is no next event, jump to end of currently playing event
+ next = playingChannel->GetEPGNow();
+ if (next)
+ next->EndAsUTC().GetAsTime(nextTime);
+ }
+
+ int64_t seekTime = 0;
+ if (nextTime != 0)
+ {
+ seekTime = (nextTime - playbackStartTime) * 1000;
+ }
+ else
+ {
+ // no epg; jump to end of buffer
+ seekTime = CServiceBroker::GetDataCacheCore().GetMaxTime();
+ }
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_SEEK_TIME, seekTime);
+ }
+ }
+}
+
+void CPVRGUIActionsPlayback::SeekBackward(unsigned int iThreshold)
+{
+ time_t playbackStartTime = CServiceBroker::GetDataCacheCore().GetStartTime();
+ if (playbackStartTime > 0)
+ {
+ const std::shared_ptr<CPVRChannel> playingChannel =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (playingChannel)
+ {
+ time_t prevTime = 0;
+ std::shared_ptr<CPVREpgInfoTag> prev = playingChannel->GetEPGNow();
+ if (prev)
+ {
+ prev->StartAsUTC().GetAsTime(prevTime);
+
+ // if playback time of current event is above threshold jump to start of current event
+ int64_t playTime = CServiceBroker::GetDataCacheCore().GetPlayTime() / 1000;
+ if ((playbackStartTime + playTime - prevTime) <= iThreshold)
+ {
+ // jump to start of previous event
+ prevTime = 0;
+ prev = playingChannel->GetEPGPrevious();
+ if (prev)
+ prev->StartAsUTC().GetAsTime(prevTime);
+ }
+ }
+
+ int64_t seekTime = 0;
+ if (prevTime != 0)
+ {
+ seekTime = (prevTime - playbackStartTime) * 1000;
+ }
+ else
+ {
+ // no epg; jump to begin of buffer
+ seekTime = CServiceBroker::GetDataCacheCore().GetMinTime();
+ }
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_SEEK_TIME, seekTime);
+ }
+ }
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsPlayback.h b/xbmc/pvr/guilib/PVRGUIActionsPlayback.h
new file mode 100644
index 0000000..8b0ab55
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsPlayback.h
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2016-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 "pvr/IPVRComponent.h"
+#include "pvr/settings/PVRSettings.h"
+
+#include <string>
+
+class CFileItem;
+
+namespace PVR
+{
+enum PlaybackType
+{
+ PlaybackTypeAny = 0,
+ PlaybackTypeTV,
+ PlaybackTypeRadio
+};
+
+class CPVRStreamProperties;
+
+class CPVRGUIActionsPlayback : public IPVRComponent
+{
+public:
+ CPVRGUIActionsPlayback();
+ ~CPVRGUIActionsPlayback() override = default;
+
+ /*!
+ * @brief Get a localized resume play label, if the given item can be resumed.
+ * @param item containing a recording or an epg tag.
+ * @return the localized resume play label that can be used for instance as context menu item
+ * label or an empty string if resume is not possible.
+ */
+ std::string GetResumeLabel(const CFileItem& item) const;
+
+ /*!
+ * @brief Resume a previously not completely played recording.
+ * @param item containing a recording or an epg tag.
+ * @param bFallbackToPlay controls whether playback of the recording should be started at the
+ * beginning ig no resume data are available.
+ * @return true on success, false otherwise.
+ */
+ bool ResumePlayRecording(const CFileItem& item, bool bFallbackToPlay) const;
+
+ /*!
+ * @brief Play recording.
+ * @param item containing a recording or an epg tag.
+ * @param bCheckResume controls resume check.
+ * @return true on success, false otherwise.
+ */
+ bool PlayRecording(const CFileItem& item, bool bCheckResume) const;
+
+ /*!
+ * @brief Play EPG tag.
+ * @param item containing an epg tag.
+ * @return true on success, false otherwise.
+ */
+ bool PlayEpgTag(const CFileItem& item) const;
+
+ /*!
+ * @brief Switch channel.
+ * @param item containing a channel or an epg tag.
+ * @param bCheckResume controls resume check in case a recording for the current epg event is
+ * present.
+ * @return true on success, false otherwise.
+ */
+ bool SwitchToChannel(const CFileItem& item, bool bCheckResume) const;
+
+ /*!
+ * @brief Playback the given file item.
+ * @param item containing a channel or a recording.
+ * @return True if the item could be played, false otherwise.
+ */
+ bool PlayMedia(const CFileItem& item) const;
+
+ /*!
+ * @brief Start playback of the last played channel, and if there is none, play first channel in
+ * the current channelgroup.
+ * @param type The type of playback to be started (any, radio, tv). See PlaybackType enum
+ * @return True if playback was started, false otherwise.
+ */
+ bool SwitchToChannel(PlaybackType type) const;
+
+ /*!
+ * @brief Plays the last played channel or the first channel of TV or Radio on startup.
+ * @return True if playback was started, false otherwise.
+ */
+ bool PlayChannelOnStartup() const;
+
+ /*!
+ * @brief Seek to the start of the next epg event in timeshift buffer, relative to the currently
+ * playing event. If there is no next event, seek to the end of the currently playing event (to
+ * the 'live' position).
+ */
+ void SeekForward();
+
+ /*!
+ * @brief Seek to the start of the previous epg event in timeshift buffer, relative to the
+ * currently playing event or if there is no previous event or if playback time is greater than
+ * given threshold, seek to the start of the playing event.
+ * @param iThreshold the value in seconds to trigger seek to start of current event instead of
+ * start of previous event.
+ */
+ void SeekBackward(unsigned int iThreshold);
+
+private:
+ CPVRGUIActionsPlayback(const CPVRGUIActionsPlayback&) = delete;
+ CPVRGUIActionsPlayback const& operator=(CPVRGUIActionsPlayback const&) = delete;
+
+ /*!
+ * @brief Check whether resume play is possible for a given item, display "resume from ..."/"play
+ * from start" context menu in case.
+ * @param item containing a recording or an epg tag.
+ * @return true, to play/resume the item, false otherwise.
+ */
+ bool CheckResumeRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Check "play minimized" settings value and switch to fullscreen if not set.
+ * @param bFullscreen switch to fullscreen or set windowed playback.
+ */
+ void CheckAndSwitchToFullscreen(bool bFullscreen) const;
+
+ /*!
+ * @brief Start playback of the given item.
+ * @param bFullscreen start playback fullscreen or not.
+ * @param epgProps properties to be used instead of calling to the client if supplied.
+ * @param item containing a channel or a recording.
+ */
+ void StartPlayback(CFileItem* item,
+ bool bFullscreen,
+ const CPVRStreamProperties* epgProps = nullptr) const;
+
+ CPVRSettings m_settings;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Playback = CPVRGUIActionsPlayback;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp
new file mode 100644
index 0000000..1a0a99a
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2016-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 "PVRGUIActionsPowerManagement.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "network/Network.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/Settings.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+CPVRGUIActionsPowerManagement::CPVRGUIActionsPowerManagement()
+ : m_settings({CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME,
+ CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME})
+{
+}
+
+bool CPVRGUIActionsPowerManagement::CanSystemPowerdown(bool bAskUser /*= true*/) const
+{
+ bool bReturn(true);
+ if (CServiceBroker::GetPVRManager().IsStarted())
+ {
+ std::shared_ptr<CPVRTimerInfoTag> cause;
+ if (!AllLocalBackendsIdle(cause))
+ {
+ if (bAskUser)
+ {
+ std::string text;
+
+ if (cause)
+ {
+ if (cause->IsRecording())
+ {
+ text = StringUtils::Format(
+ g_localizeStrings.Get(19691), // "PVR is currently recording...."
+ cause->Title(), cause->ChannelName());
+ }
+ else
+ {
+ // Next event is due to a local recording or reminder.
+ const CDateTime now(CDateTime::GetUTCDateTime());
+ const CDateTime start(cause->StartAsUTC());
+ const CDateTimeSpan prestart(0, 0, cause->MarginStart(), 0);
+
+ CDateTimeSpan diff(start - now);
+ diff -= prestart;
+ int mins = diff.GetSecondsTotal() / 60;
+
+ std::string dueStr;
+ if (mins > 1)
+ {
+ // "%d minutes"
+ dueStr = StringUtils::Format(g_localizeStrings.Get(19694), mins);
+ }
+ else
+ {
+ // "about a minute"
+ dueStr = g_localizeStrings.Get(19695);
+ }
+
+ text = StringUtils::Format(
+ cause->IsReminder()
+ ? g_localizeStrings.Get(19690) // "PVR has scheduled a reminder...."
+ : g_localizeStrings.Get(19692), // "PVR will start recording...."
+ cause->Title(), cause->ChannelName(), dueStr);
+ }
+ }
+ else
+ {
+ // Next event is due to automatic daily wakeup of PVR.
+ const CDateTime now(CDateTime::GetUTCDateTime());
+
+ CDateTime dailywakeuptime;
+ dailywakeuptime.SetFromDBTime(
+ m_settings.GetStringValue(CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME));
+ dailywakeuptime = dailywakeuptime.GetAsUTCDateTime();
+
+ const CDateTimeSpan diff(dailywakeuptime - now);
+ int mins = diff.GetSecondsTotal() / 60;
+
+ std::string dueStr;
+ if (mins > 1)
+ {
+ // "%d minutes"
+ dueStr = StringUtils::Format(g_localizeStrings.Get(19694), mins);
+ }
+ else
+ {
+ // "about a minute"
+ dueStr = g_localizeStrings.Get(19695);
+ }
+
+ text = StringUtils::Format(g_localizeStrings.Get(19693), // "Daily wakeup is due in...."
+ dueStr);
+ }
+
+ // Inform user about PVR being busy. Ask if user wants to powerdown anyway.
+ bReturn = HELPERS::ShowYesNoDialogText(CVariant{19685}, // "Confirm shutdown"
+ CVariant{text}, CVariant{222}, // "Shutdown anyway",
+ CVariant{19696}, // "Cancel"
+ 10000) // timeout value before closing
+ == HELPERS::DialogResponse::CHOICE_YES;
+ }
+ else
+ bReturn = false; // do not powerdown (busy, but no user interaction requested).
+ }
+ }
+ return bReturn;
+}
+
+bool CPVRGUIActionsPowerManagement::AllLocalBackendsIdle(
+ std::shared_ptr<CPVRTimerInfoTag>& causingEvent) const
+{
+ // active recording on local backend?
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>> activeRecordings =
+ CServiceBroker::GetPVRManager().Timers()->GetActiveRecordings();
+ for (const auto& timer : activeRecordings)
+ {
+ if (EventOccursOnLocalBackend(timer))
+ {
+ causingEvent = timer;
+ return false;
+ }
+ }
+
+ // soon recording on local backend?
+ if (IsNextEventWithinBackendIdleTime())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer =
+ CServiceBroker::GetPVRManager().Timers()->GetNextActiveTimer(false);
+ if (!timer)
+ {
+ // Next event is due to automatic daily wakeup of PVR!
+ causingEvent.reset();
+ return false;
+ }
+
+ if (EventOccursOnLocalBackend(timer))
+ {
+ causingEvent = timer;
+ return false;
+ }
+ }
+ return true;
+}
+
+bool CPVRGUIActionsPowerManagement::EventOccursOnLocalBackend(
+ const std::shared_ptr<CPVRTimerInfoTag>& event) const
+{
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(CFileItem(event));
+ if (client)
+ {
+ const std::string hostname = client->GetBackendHostname();
+ if (!hostname.empty() && CServiceBroker::GetNetwork().IsLocalHost(hostname))
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIActionsPowerManagement::IsNextEventWithinBackendIdleTime() const
+{
+ // timers going off soon?
+ const CDateTime now(CDateTime::GetUTCDateTime());
+ const CDateTimeSpan idle(
+ 0, 0, m_settings.GetIntValue(CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME), 0);
+ const CDateTime next(CServiceBroker::GetPVRManager().Timers()->GetNextEventTime());
+ const CDateTimeSpan delta(next - now);
+
+ return (delta <= idle);
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h
new file mode 100644
index 0000000..bf99819
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016-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 "pvr/IPVRComponent.h"
+#include "pvr/settings/PVRSettings.h"
+
+#include <memory>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRTimerInfoTag;
+
+class CPVRGUIActionsPowerManagement : public IPVRComponent
+{
+public:
+ CPVRGUIActionsPowerManagement();
+ ~CPVRGUIActionsPowerManagement() override = default;
+
+ /*!
+ * @brief Check whether the system Kodi is running on can be powered down
+ * (shutdown/reboot/suspend/hibernate) without stopping any active recordings and/or without
+ * preventing the start of recordings scheduled for now + pvrpowermanagement.backendidletime.
+ * @param bAskUser True to informs user in case of potential data loss. User can decide to allow
+ * powerdown anyway. False to not to ask user and to not confirm power down.
+ * @return True if system can be safely powered down, false otherwise.
+ */
+ bool CanSystemPowerdown(bool bAskUser = true) const;
+
+private:
+ CPVRGUIActionsPowerManagement(const CPVRGUIActionsPowerManagement&) = delete;
+ CPVRGUIActionsPowerManagement const& operator=(CPVRGUIActionsPowerManagement const&) = delete;
+
+ bool AllLocalBackendsIdle(std::shared_ptr<CPVRTimerInfoTag>& causingEvent) const;
+ bool EventOccursOnLocalBackend(const std::shared_ptr<CPVRTimerInfoTag>& event) const;
+ bool IsNextEventWithinBackendIdleTime() const;
+
+ CPVRSettings m_settings;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using PowerManagement = CPVRGUIActionsPowerManagement;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp b/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp
new file mode 100644
index 0000000..df23f38
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2016-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 "PVRGUIActionsRecordings.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/IDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/dialogs/GUIDialogPVRRecordingInfo.h"
+#include "pvr/dialogs/GUIDialogPVRRecordingSettings.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "settings/Settings.h"
+#include "threads/IRunnable.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <numeric>
+#include <string>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+namespace
+{
+class AsyncRecordingAction : private IRunnable
+{
+public:
+ bool Execute(const CFileItem& item);
+
+protected:
+ AsyncRecordingAction() = default;
+
+private:
+ // IRunnable implementation
+ void Run() override;
+
+ // the worker function
+ virtual bool DoRun(const std::shared_ptr<CFileItem>& item) = 0;
+
+ std::shared_ptr<CFileItem> m_item;
+ bool m_bSuccess = false;
+};
+
+bool AsyncRecordingAction::Execute(const CFileItem& item)
+{
+ m_item = std::make_shared<CFileItem>(item);
+ CGUIDialogBusy::Wait(this, 100, false);
+ return m_bSuccess;
+}
+
+void AsyncRecordingAction::Run()
+{
+ m_bSuccess = DoRun(m_item);
+
+ if (m_bSuccess)
+ CServiceBroker::GetPVRManager().TriggerRecordingsUpdate();
+}
+
+class AsyncRenameRecording : public AsyncRecordingAction
+{
+public:
+ explicit AsyncRenameRecording(const std::string& strNewName) : m_strNewName(strNewName) {}
+
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ if (item->IsUsablePVRRecording())
+ {
+ return item->GetPVRRecordingInfoTag()->Rename(m_strNewName);
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Cannot rename item '{}': no valid recording tag", item->GetPath());
+ return false;
+ }
+ }
+ std::string m_strNewName;
+};
+
+class AsyncDeleteRecording : public AsyncRecordingAction
+{
+public:
+ explicit AsyncDeleteRecording(bool bWatchedOnly = false) : m_bWatchedOnly(bWatchedOnly) {}
+
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ CFileItemList items;
+ if (item->m_bIsFolder)
+ {
+ CUtil::GetRecursiveListing(item->GetPath(), items, "", XFILE::DIR_FLAG_NO_FILE_INFO);
+ }
+ else
+ {
+ items.Add(item);
+ }
+
+ return std::accumulate(
+ items.cbegin(), items.cend(), true, [this](bool success, const auto& itemToDelete) {
+ return (itemToDelete->IsPVRRecording() &&
+ (!m_bWatchedOnly || itemToDelete->GetPVRRecordingInfoTag()->GetPlayCount() > 0) &&
+ !itemToDelete->GetPVRRecordingInfoTag()->Delete())
+ ? false
+ : success;
+ });
+ }
+ bool m_bWatchedOnly = false;
+};
+
+class AsyncEmptyRecordingsTrash : public AsyncRecordingAction
+{
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ return CServiceBroker::GetPVRManager().Clients()->DeleteAllRecordingsFromTrash() ==
+ PVR_ERROR_NO_ERROR;
+ }
+};
+
+class AsyncUndeleteRecording : public AsyncRecordingAction
+{
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ if (item->IsDeletedPVRRecording())
+ {
+ return item->GetPVRRecordingInfoTag()->Undelete();
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Cannot undelete item '{}': no valid recording tag", item->GetPath());
+ return false;
+ }
+ }
+};
+
+class AsyncSetRecordingPlayCount : public AsyncRecordingAction
+{
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item);
+ if (client)
+ {
+ const std::shared_ptr<CPVRRecording> recording = item->GetPVRRecordingInfoTag();
+ return client->SetRecordingPlayCount(*recording, recording->GetLocalPlayCount()) ==
+ PVR_ERROR_NO_ERROR;
+ }
+ return false;
+ }
+};
+
+class AsyncSetRecordingLifetime : public AsyncRecordingAction
+{
+private:
+ bool DoRun(const std::shared_ptr<CFileItem>& item) override
+ {
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item);
+ if (client)
+ return client->SetRecordingLifetime(*item->GetPVRRecordingInfoTag()) == PVR_ERROR_NO_ERROR;
+ return false;
+ }
+};
+
+} // unnamed namespace
+
+bool CPVRGUIActionsRecordings::ShowRecordingInfo(const CFileItem& item) const
+{
+ if (!item.IsPVRRecording())
+ {
+ CLog::LogF(LOGERROR, "No recording!");
+ return false;
+ }
+
+ CGUIDialogPVRRecordingInfo* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRRecordingInfo>(
+ WINDOW_DIALOG_PVR_RECORDING_INFO);
+ if (!pDlgInfo)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_RECORDING_INFO!");
+ return false;
+ }
+
+ pDlgInfo->SetRecording(item);
+ pDlgInfo->Open();
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::EditRecording(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRRecording> recording = CPVRItem(item).GetRecording();
+ if (!recording)
+ {
+ CLog::LogF(LOGERROR, "No recording!");
+ return false;
+ }
+
+ std::shared_ptr<CPVRRecording> origRecording(new CPVRRecording);
+ origRecording->Update(*recording,
+ *CServiceBroker::GetPVRManager().GetClient(recording->ClientID()));
+
+ if (!ShowRecordingSettings(recording))
+ return false;
+
+ if (origRecording->m_strTitle != recording->m_strTitle)
+ {
+ if (!AsyncRenameRecording(recording->m_strTitle).Execute(item))
+ CLog::LogF(LOGERROR, "Renaming recording failed!");
+ }
+
+ if (origRecording->GetLocalPlayCount() != recording->GetLocalPlayCount())
+ {
+ if (!AsyncSetRecordingPlayCount().Execute(item))
+ CLog::LogF(LOGERROR, "Setting recording playcount failed!");
+ }
+
+ if (origRecording->LifeTime() != recording->LifeTime())
+ {
+ if (!AsyncSetRecordingLifetime().Execute(item))
+ CLog::LogF(LOGERROR, "Setting recording lifetime failed!");
+ }
+
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::CanEditRecording(const CFileItem& item) const
+{
+ return CGUIDialogPVRRecordingSettings::CanEditRecording(item);
+}
+
+bool CPVRGUIActionsRecordings::DeleteRecording(const CFileItem& item) const
+{
+ if ((!item.IsPVRRecording() && !item.m_bIsFolder) || item.IsParentFolder())
+ return false;
+
+ if (!ConfirmDeleteRecording(item))
+ return false;
+
+ if (!AsyncDeleteRecording().Execute(item))
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19111}); // "Error", "PVR backend error. Check the log for more information about this message."
+ return false;
+ }
+
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::ConfirmDeleteRecording(const CFileItem& item) const
+{
+ return CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{122}, // "Confirm delete"
+ item.m_bIsFolder
+ ? CVariant{19113} // "Delete all recordings in this folder?"
+ : item.GetPVRRecordingInfoTag()->IsDeleted()
+ ? CVariant{19294}
+ // "Remove this deleted recording from trash? This operation cannot be reverted."
+ : CVariant{19112}, // "Delete this recording?"
+ CVariant{""}, CVariant{item.GetLabel()});
+}
+
+bool CPVRGUIActionsRecordings::DeleteWatchedRecordings(const CFileItem& item) const
+{
+ if (!item.m_bIsFolder || item.IsParentFolder())
+ return false;
+
+ if (!ConfirmDeleteWatchedRecordings(item))
+ return false;
+
+ if (!AsyncDeleteRecording(true).Execute(item))
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19111}); // "Error", "PVR backend error. Check the log for more information about this message."
+ return false;
+ }
+
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::ConfirmDeleteWatchedRecordings(const CFileItem& item) const
+{
+ return CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{122}, // "Confirm delete"
+ CVariant{19328}, // "Delete all watched recordings in this folder?"
+ CVariant{""}, CVariant{item.GetLabel()});
+}
+
+bool CPVRGUIActionsRecordings::DeleteAllRecordingsFromTrash() const
+{
+ if (!ConfirmDeleteAllRecordingsFromTrash())
+ return false;
+
+ if (!AsyncEmptyRecordingsTrash().Execute({}))
+ return false;
+
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::ConfirmDeleteAllRecordingsFromTrash() const
+{
+ return CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19292}, // "Delete all permanently"
+ CVariant{
+ 19293}); // "Remove all deleted recordings from trash? This operation cannot be reverted."
+}
+
+bool CPVRGUIActionsRecordings::UndeleteRecording(const CFileItem& item) const
+{
+ if (!item.IsDeletedPVRRecording())
+ return false;
+
+ if (!AsyncUndeleteRecording().Execute(item))
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19111}); // "Error", "PVR backend error. Check the log for more information about this message."
+ return false;
+ }
+
+ return true;
+}
+
+bool CPVRGUIActionsRecordings::ShowRecordingSettings(
+ const std::shared_ptr<CPVRRecording>& recording) const
+{
+ CGUIDialogPVRRecordingSettings* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRRecordingSettings>(
+ WINDOW_DIALOG_PVR_RECORDING_SETTING);
+ if (!pDlgInfo)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_RECORDING_SETTING!");
+ return false;
+ }
+
+ pDlgInfo->SetRecording(recording);
+ pDlgInfo->Open();
+
+ return pDlgInfo->IsConfirmed();
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsRecordings.h b/xbmc/pvr/guilib/PVRGUIActionsRecordings.h
new file mode 100644
index 0000000..795a2c7
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsRecordings.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016-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 "pvr/IPVRComponent.h"
+
+#include <memory>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRRecording;
+
+class CPVRGUIActionsRecordings : public IPVRComponent
+{
+public:
+ CPVRGUIActionsRecordings() = default;
+ ~CPVRGUIActionsRecordings() override = default;
+
+ /*!
+ * @brief Open a dialog with information for a given recording.
+ * @param item containing a recording.
+ * @return true on success, false otherwise.
+ */
+ bool ShowRecordingInfo(const CFileItem& item) const;
+
+ /*!
+ * @brief Open the recording settings dialog to edit a recording.
+ * @param item containing the recording to edit.
+ * @return true on success, false otherwise.
+ */
+ bool EditRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Check if any recording settings can be edited.
+ * @param item containing the recording to edit.
+ * @return true on success, false otherwise.
+ */
+ bool CanEditRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Delete a recording, always showing a confirmation dialog.
+ * @param item containing a recording to delete.
+ * @return true, if the recording was deleted successfully, false otherwise.
+ */
+ bool DeleteRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Delete all watched recordings contained in the given folder, always showing a
+ * confirmation dialog.
+ * @param item containing a recording folder containing the items to delete.
+ * @return true, if the recordings were deleted successfully, false otherwise.
+ */
+ bool DeleteWatchedRecordings(const CFileItem& item) const;
+
+ /*!
+ * @brief Delete all recordings from trash, always showing a confirmation dialog.
+ * @return true, if the recordings were permanently deleted successfully, false otherwise.
+ */
+ bool DeleteAllRecordingsFromTrash() const;
+
+ /*!
+ * @brief Undelete a recording.
+ * @param item containing a recording to undelete.
+ * @return true, if the recording was undeleted successfully, false otherwise.
+ */
+ bool UndeleteRecording(const CFileItem& item) const;
+
+private:
+ CPVRGUIActionsRecordings(const CPVRGUIActionsRecordings&) = delete;
+ CPVRGUIActionsRecordings const& operator=(CPVRGUIActionsRecordings const&) = delete;
+
+ /*!
+ * @brief Open a dialog to confirm to delete a recording.
+ * @param item the recording to delete.
+ * @return true, to proceed with delete, false otherwise.
+ */
+ bool ConfirmDeleteRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Open a dialog to confirm delete all watched recordings contained in the given folder.
+ * @param item containing a recording folder containing the items to delete.
+ * @return true, to proceed with delete, false otherwise.
+ */
+ bool ConfirmDeleteWatchedRecordings(const CFileItem& item) const;
+
+ /*!
+ * @brief Open a dialog to confirm to permanently remove all deleted recordings.
+ * @return true, to proceed with delete, false otherwise.
+ */
+ bool ConfirmDeleteAllRecordingsFromTrash() const;
+
+ /*!
+ * @brief Open the recording settings dialog.
+ * @param recording containing the recording the settings shall be displayed for.
+ * @return true, if the dialog was ended successfully, false otherwise.
+ */
+ bool ShowRecordingSettings(const std::shared_ptr<CPVRRecording>& recording) const;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Recordings = CPVRGUIActionsRecordings;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp b/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp
new file mode 100644
index 0000000..be8e313
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp
@@ -0,0 +1,1009 @@
+/*
+ * Copyright (C) 2016-2022 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 "PVRGUIActionsTimers.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVREventLogJob.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/dialogs/GUIDialogPVRTimerSettings.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/Settings.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <map>
+#include <memory>
+#include <string>
+#include <thread>
+#include <utility>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+CPVRGUIActionsTimers::CPVRGUIActionsTimers()
+ : m_settings({CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME,
+ CSettings::SETTING_PVRRECORD_INSTANTRECORDACTION,
+ CSettings::SETTING_PVRREMINDERS_AUTOCLOSEDELAY,
+ CSettings::SETTING_PVRREMINDERS_AUTORECORD,
+ CSettings::SETTING_PVRREMINDERS_AUTOSWITCH})
+{
+}
+
+bool CPVRGUIActionsTimers::ShowTimerSettings(const std::shared_ptr<CPVRTimerInfoTag>& timer) const
+{
+ CGUIDialogPVRTimerSettings* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRTimerSettings>(
+ WINDOW_DIALOG_PVR_TIMER_SETTING);
+ if (!pDlgInfo)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_TIMER_SETTING!");
+ return false;
+ }
+
+ pDlgInfo->SetTimer(timer);
+ pDlgInfo->Open();
+
+ return pDlgInfo->IsConfirmed();
+}
+
+bool CPVRGUIActionsTimers::AddReminder(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ if (!epgTag)
+ {
+ CLog::LogF(LOGERROR, "No epg tag!");
+ return false;
+ }
+
+ if (CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epgTag))
+ {
+ HELPERS::ShowOKDialogText(CVariant{19033}, // "Information"
+ CVariant{19034}); // "There is already a timer set for this event"
+ return false;
+ }
+
+ const std::shared_ptr<CPVRTimerInfoTag> newTimer =
+ CPVRTimerInfoTag::CreateReminderFromEpg(epgTag);
+ if (!newTimer)
+ {
+ HELPERS::ShowOKDialogText(CVariant{19033}, // "Information"
+ CVariant{19094}); // Timer creation failed. Unsupported timer type.
+ return false;
+ }
+
+ return AddTimer(newTimer);
+}
+
+bool CPVRGUIActionsTimers::AddTimer(bool bRadio) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> newTimer(new CPVRTimerInfoTag(bRadio));
+ if (ShowTimerSettings(newTimer))
+ {
+ return AddTimer(newTimer);
+ }
+ return false;
+}
+
+bool CPVRGUIActionsTimers::AddTimer(const CFileItem& item, bool bShowTimerSettings) const
+{
+ return AddTimer(item, false, bShowTimerSettings, false);
+}
+
+bool CPVRGUIActionsTimers::AddTimerRule(const CFileItem& item,
+ bool bShowTimerSettings,
+ bool bFallbackToOneShotTimer) const
+{
+ return AddTimer(item, true, bShowTimerSettings, bFallbackToOneShotTimer);
+}
+
+bool CPVRGUIActionsTimers::AddTimer(const CFileItem& item,
+ bool bCreateRule,
+ bool bShowTimerSettings,
+ bool bFallbackToOneShotTimer) const
+{
+ const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel());
+ if (!channel)
+ {
+ CLog::LogF(LOGERROR, "No channel!");
+ return false;
+ }
+
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel) !=
+ ParentalCheckResult::SUCCESS)
+ return false;
+
+ std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ if (epgTag)
+ {
+ if (epgTag->IsGapTag())
+ epgTag.reset(); // for gap tags, we can only create instant timers
+ }
+ else if (bCreateRule)
+ {
+ CLog::LogF(LOGERROR, "No epg tag!");
+ return false;
+ }
+
+ std::shared_ptr<CPVRTimerInfoTag> timer(
+ bCreateRule || !epgTag ? nullptr
+ : CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epgTag));
+ std::shared_ptr<CPVRTimerInfoTag> rule(
+ bCreateRule ? CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer) : nullptr);
+ if (timer || rule)
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{19033},
+ CVariant{19034}); // "Information", "There is already a timer set for this event"
+ return false;
+ }
+
+ std::shared_ptr<CPVRTimerInfoTag> newTimer(
+ epgTag ? CPVRTimerInfoTag::CreateFromEpg(epgTag, bCreateRule)
+ : CPVRTimerInfoTag::CreateInstantTimerTag(channel));
+ if (!newTimer)
+ {
+ if (bCreateRule && bFallbackToOneShotTimer)
+ newTimer = CPVRTimerInfoTag::CreateFromEpg(epgTag, false);
+
+ if (!newTimer)
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{19033}, // "Information"
+ bCreateRule ? CVariant{19095} // Timer rule creation failed. Unsupported timer type.
+ : CVariant{19094}); // Timer creation failed. Unsupported timer type.
+ return false;
+ }
+ }
+
+ if (bShowTimerSettings)
+ {
+ if (!ShowTimerSettings(newTimer))
+ return false;
+ }
+
+ return AddTimer(newTimer);
+}
+
+bool CPVRGUIActionsTimers::AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& item) const
+{
+ if (!item->Channel() && !item->GetTimerType()->IsEpgBasedTimerRule())
+ {
+ CLog::LogF(LOGERROR, "No channel given");
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19109}); // "Error", "Could not save the timer. Check the log for more information about this message."
+ return false;
+ }
+
+ if (!item->IsTimerRule() && item->GetEpgInfoTag() && !item->GetEpgInfoTag()->IsRecordable())
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{19033},
+ CVariant{19189}); // "Information", "The PVR backend does not allow to record this event."
+ return false;
+ }
+
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(
+ item->Channel()) != ParentalCheckResult::SUCCESS)
+ return false;
+
+ if (!CServiceBroker::GetPVRManager().Timers()->AddTimer(item))
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19109}); // "Error", "Could not save the timer. Check the log for more information about this message."
+ return false;
+ }
+
+ return true;
+}
+
+namespace
+{
+enum PVRRECORD_INSTANTRECORDACTION
+{
+ NONE = -1,
+ RECORD_CURRENT_SHOW = 0,
+ RECORD_INSTANTRECORDTIME = 1,
+ ASK = 2,
+ RECORD_30_MINUTES = 3,
+ RECORD_60_MINUTES = 4,
+ RECORD_120_MINUTES = 5,
+ RECORD_NEXT_SHOW = 6
+};
+
+class InstantRecordingActionSelector
+{
+public:
+ explicit InstantRecordingActionSelector(int iInstantRecordTime);
+ virtual ~InstantRecordingActionSelector() = default;
+
+ void AddAction(PVRRECORD_INSTANTRECORDACTION eAction, const std::string& title);
+ void PreSelectAction(PVRRECORD_INSTANTRECORDACTION eAction);
+ PVRRECORD_INSTANTRECORDACTION Select();
+
+private:
+ int m_iInstantRecordTime;
+ CGUIDialogSelect* m_pDlgSelect; // not owner!
+ std::map<PVRRECORD_INSTANTRECORDACTION, int> m_actions;
+};
+
+InstantRecordingActionSelector::InstantRecordingActionSelector(int iInstantRecordTime)
+ : m_iInstantRecordTime(iInstantRecordTime),
+ m_pDlgSelect(CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT))
+{
+ if (m_pDlgSelect)
+ {
+ m_pDlgSelect->Reset();
+ m_pDlgSelect->SetMultiSelection(false);
+ m_pDlgSelect->SetHeading(CVariant{19086}); // Instant recording action
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to obtain WINDOW_DIALOG_SELECT instance");
+ }
+}
+
+void InstantRecordingActionSelector::AddAction(PVRRECORD_INSTANTRECORDACTION eAction,
+ const std::string& title)
+{
+ if (m_actions.find(eAction) == m_actions.end())
+ {
+ switch (eAction)
+ {
+ case RECORD_INSTANTRECORDTIME:
+ m_pDlgSelect->Add(
+ StringUtils::Format(g_localizeStrings.Get(19090),
+ m_iInstantRecordTime)); // Record next <default duration> minutes
+ break;
+ case RECORD_30_MINUTES:
+ m_pDlgSelect->Add(
+ StringUtils::Format(g_localizeStrings.Get(19090), 30)); // Record next 30 minutes
+ break;
+ case RECORD_60_MINUTES:
+ m_pDlgSelect->Add(
+ StringUtils::Format(g_localizeStrings.Get(19090), 60)); // Record next 60 minutes
+ break;
+ case RECORD_120_MINUTES:
+ m_pDlgSelect->Add(
+ StringUtils::Format(g_localizeStrings.Get(19090), 120)); // Record next 120 minutes
+ break;
+ case RECORD_CURRENT_SHOW:
+ m_pDlgSelect->Add(StringUtils::Format(g_localizeStrings.Get(19091),
+ title)); // Record current show (<title>)
+ break;
+ case RECORD_NEXT_SHOW:
+ m_pDlgSelect->Add(StringUtils::Format(g_localizeStrings.Get(19092),
+ title)); // Record next show (<title>)
+ break;
+ case NONE:
+ case ASK:
+ default:
+ return;
+ }
+
+ m_actions.insert(std::make_pair(eAction, static_cast<int>(m_actions.size())));
+ }
+}
+
+void InstantRecordingActionSelector::PreSelectAction(PVRRECORD_INSTANTRECORDACTION eAction)
+{
+ const auto& it = m_actions.find(eAction);
+ if (it != m_actions.end())
+ m_pDlgSelect->SetSelected(it->second);
+}
+
+PVRRECORD_INSTANTRECORDACTION InstantRecordingActionSelector::Select()
+{
+ PVRRECORD_INSTANTRECORDACTION eAction = NONE;
+
+ m_pDlgSelect->Open();
+
+ if (m_pDlgSelect->IsConfirmed())
+ {
+ int iSelection = m_pDlgSelect->GetSelectedItem();
+ const auto it =
+ std::find_if(m_actions.cbegin(), m_actions.cend(),
+ [iSelection](const auto& action) { return action.second == iSelection; });
+
+ if (it != m_actions.cend())
+ eAction = (*it).first;
+ }
+
+ return eAction;
+}
+
+} // unnamed namespace
+
+bool CPVRGUIActionsTimers::ToggleRecordingOnPlayingChannel()
+{
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (channel && channel->CanRecord())
+ return SetRecordingOnChannel(
+ channel, !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel));
+
+ return false;
+}
+
+bool CPVRGUIActionsTimers::SetRecordingOnChannel(const std::shared_ptr<CPVRChannel>& channel,
+ bool bOnOff)
+{
+ bool bReturn = false;
+
+ if (!channel)
+ return bReturn;
+
+ if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel) !=
+ ParentalCheckResult::SUCCESS)
+ return bReturn;
+
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(channel->ClientID());
+ if (client && client->GetClientCapabilities().SupportsTimers())
+ {
+ /* timers are supported on this channel */
+ if (bOnOff && !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel))
+ {
+ std::shared_ptr<CPVREpgInfoTag> epgTag;
+ int iDuration = m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME);
+
+ int iAction = m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDACTION);
+ switch (iAction)
+ {
+ case RECORD_CURRENT_SHOW:
+ epgTag = channel->GetEPGNow();
+ break;
+
+ case RECORD_INSTANTRECORDTIME:
+ epgTag.reset();
+ break;
+
+ case ASK:
+ {
+ PVRRECORD_INSTANTRECORDACTION ePreselect = RECORD_INSTANTRECORDTIME;
+ const int iDurationDefault =
+ m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME);
+ InstantRecordingActionSelector selector(iDurationDefault);
+ std::shared_ptr<CPVREpgInfoTag> epgTagNext;
+
+ // fixed length recordings
+ selector.AddAction(RECORD_30_MINUTES, "");
+ selector.AddAction(RECORD_60_MINUTES, "");
+ selector.AddAction(RECORD_120_MINUTES, "");
+
+ if (iDurationDefault != 30 && iDurationDefault != 60 && iDurationDefault != 120)
+ selector.AddAction(RECORD_INSTANTRECORDTIME, "");
+
+ // epg-based recordings
+ epgTag = channel->GetEPGNow();
+ if (epgTag)
+ {
+ bool bLocked = CServiceBroker::GetPVRManager().IsParentalLocked(epgTag);
+
+ // "now"
+ const std::string currentTitle =
+ bLocked ? g_localizeStrings.Get(19266) /* Parental locked */ : epgTag->Title();
+ selector.AddAction(RECORD_CURRENT_SHOW, currentTitle);
+ ePreselect = RECORD_CURRENT_SHOW;
+
+ // "next"
+ epgTagNext = channel->GetEPGNext();
+ if (epgTagNext)
+ {
+ const std::string nextTitle = bLocked
+ ? g_localizeStrings.Get(19266) /* Parental locked */
+ : epgTagNext->Title();
+ selector.AddAction(RECORD_NEXT_SHOW, nextTitle);
+
+ // be smart. if current show is almost over, preselect next show.
+ if (epgTag->ProgressPercentage() > 90.0f)
+ ePreselect = RECORD_NEXT_SHOW;
+ }
+ }
+
+ if (ePreselect == RECORD_INSTANTRECORDTIME)
+ {
+ if (iDurationDefault == 30)
+ ePreselect = RECORD_30_MINUTES;
+ else if (iDurationDefault == 60)
+ ePreselect = RECORD_60_MINUTES;
+ else if (iDurationDefault == 120)
+ ePreselect = RECORD_120_MINUTES;
+ }
+
+ selector.PreSelectAction(ePreselect);
+
+ PVRRECORD_INSTANTRECORDACTION eSelected = selector.Select();
+ switch (eSelected)
+ {
+ case NONE:
+ return false; // dialog canceled
+
+ case RECORD_30_MINUTES:
+ iDuration = 30;
+ epgTag.reset();
+ break;
+
+ case RECORD_60_MINUTES:
+ iDuration = 60;
+ epgTag.reset();
+ break;
+
+ case RECORD_120_MINUTES:
+ iDuration = 120;
+ epgTag.reset();
+ break;
+
+ case RECORD_INSTANTRECORDTIME:
+ iDuration = iDurationDefault;
+ epgTag.reset();
+ break;
+
+ case RECORD_CURRENT_SHOW:
+ break;
+
+ case RECORD_NEXT_SHOW:
+ epgTag = epgTagNext;
+ break;
+
+ default:
+ CLog::LogF(LOGERROR,
+ "Unknown instant record action selection ({}), defaulting to fixed "
+ "length recording.",
+ static_cast<int>(eSelected));
+ epgTag.reset();
+ break;
+ }
+ break;
+ }
+
+ default:
+ CLog::LogF(LOGERROR,
+ "Unknown instant record action setting value ({}), defaulting to fixed "
+ "length recording.",
+ iAction);
+ break;
+ }
+
+ const std::shared_ptr<CPVRTimerInfoTag> newTimer(
+ epgTag ? CPVRTimerInfoTag::CreateFromEpg(epgTag, false)
+ : CPVRTimerInfoTag::CreateInstantTimerTag(channel, iDuration));
+
+ if (newTimer)
+ bReturn = CServiceBroker::GetPVRManager().Timers()->AddTimer(newTimer);
+
+ if (!bReturn)
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19164}); // "Error", "Could not start recording. Check the log for more information about this message."
+ }
+ else if (!bOnOff && CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel))
+ {
+ /* delete active timers */
+ bReturn =
+ CServiceBroker::GetPVRManager().Timers()->DeleteTimersOnChannel(channel, true, true);
+
+ if (!bReturn)
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19170}); // "Error", "Could not stop recording. Check the log for more information about this message."
+ }
+ }
+
+ return bReturn;
+}
+
+bool CPVRGUIActionsTimers::ToggleTimer(const CFileItem& item) const
+{
+ if (!item.HasEPGInfoTag())
+ return false;
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer(CPVRItem(item).GetTimerInfoTag());
+ if (timer)
+ {
+ if (timer->IsRecording())
+ return StopRecording(item);
+ else
+ return DeleteTimer(item);
+ }
+ else
+ return AddTimer(item, false);
+}
+
+bool CPVRGUIActionsTimers::ToggleTimerState(const CFileItem& item) const
+{
+ if (!item.HasPVRTimerInfoTag())
+ return false;
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer = item.GetPVRTimerInfoTag();
+ if (timer->IsDisabled())
+ timer->SetState(PVR_TIMER_STATE_SCHEDULED);
+ else
+ timer->SetState(PVR_TIMER_STATE_DISABLED);
+
+ if (CServiceBroker::GetPVRManager().Timers()->UpdateTimer(timer))
+ return true;
+
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19263}); // "Error", "Could not update the timer. Check the log for more information about this message."
+ return false;
+}
+
+bool CPVRGUIActionsTimers::EditTimer(const CFileItem& item) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer(CPVRItem(item).GetTimerInfoTag());
+ if (!timer)
+ {
+ CLog::LogF(LOGERROR, "No timer!");
+ return false;
+ }
+
+ // clone the timer.
+ const std::shared_ptr<CPVRTimerInfoTag> newTimer(new CPVRTimerInfoTag);
+ newTimer->UpdateEntry(timer);
+
+ if (ShowTimerSettings(newTimer) &&
+ (!timer->GetTimerType()->IsReadOnly() || timer->GetTimerType()->SupportsEnableDisable()))
+ {
+ if (newTimer->GetTimerType() == timer->GetTimerType())
+ {
+ if (CServiceBroker::GetPVRManager().Timers()->UpdateTimer(newTimer))
+ return true;
+
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19263}); // "Error", "Could not update the timer. Check the log for more information about this message."
+ return false;
+ }
+ else
+ {
+ // timer type changed. delete the original timer, then create the new timer. this order is
+ // important. for instance, the new timer might be a rule which schedules the original timer.
+ // deleting the original timer after creating the rule would do literally this and we would
+ // end up with one timer missing wrt to the rule defined by the new timer.
+ if (DeleteTimer(timer, timer->IsRecording(), false))
+ {
+ if (AddTimer(newTimer))
+ return true;
+
+ // rollback.
+ return AddTimer(timer);
+ }
+ }
+ }
+ return false;
+}
+
+bool CPVRGUIActionsTimers::EditTimerRule(const CFileItem& item) const
+{
+ const std::shared_ptr<CFileItem> parentTimer = GetTimerRule(item);
+ if (parentTimer)
+ return EditTimer(*parentTimer);
+
+ return false;
+}
+
+std::shared_ptr<CFileItem> CPVRGUIActionsTimers::GetTimerRule(const CFileItem& item) const
+{
+ std::shared_ptr<CPVRTimerInfoTag> timer;
+ if (item.HasEPGInfoTag())
+ timer = CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(item.GetEPGInfoTag());
+ else if (item.HasPVRTimerInfoTag())
+ timer = item.GetPVRTimerInfoTag();
+
+ if (timer)
+ {
+ timer = CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer);
+ if (timer)
+ return std::make_shared<CFileItem>(timer);
+ }
+ return {};
+}
+
+bool CPVRGUIActionsTimers::DeleteTimer(const CFileItem& item) const
+{
+ return DeleteTimer(item, false, false);
+}
+
+bool CPVRGUIActionsTimers::DeleteTimerRule(const CFileItem& item) const
+{
+ return DeleteTimer(item, false, true);
+}
+
+bool CPVRGUIActionsTimers::DeleteTimer(const CFileItem& item,
+ bool bIsRecording,
+ bool bDeleteRule) const
+{
+ std::shared_ptr<CPVRTimerInfoTag> timer;
+ const std::shared_ptr<CPVRRecording> recording(CPVRItem(item).GetRecording());
+ if (recording)
+ timer = recording->GetRecordingTimer();
+
+ if (!timer)
+ timer = CPVRItem(item).GetTimerInfoTag();
+
+ if (!timer)
+ {
+ CLog::LogF(LOGERROR, "No timer!");
+ return false;
+ }
+
+ if (bDeleteRule && !timer->IsTimerRule())
+ timer = CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer);
+
+ if (!timer)
+ {
+ CLog::LogF(LOGERROR, "No timer rule!");
+ return false;
+ }
+
+ if (bIsRecording)
+ {
+ if (ConfirmStopRecording(timer))
+ {
+ if (CServiceBroker::GetPVRManager().Timers()->DeleteTimer(timer, true, false) ==
+ TimerOperationResult::OK)
+ return true;
+
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19170}); // "Error", "Could not stop recording. Check the log for more information about this message."
+ return false;
+ }
+ }
+ else if (!timer->GetTimerType()->AllowsDelete())
+ {
+ return false;
+ }
+ else
+ {
+ bool bAlsoDeleteRule(false);
+ if (ConfirmDeleteTimer(timer, bAlsoDeleteRule))
+ return DeleteTimer(timer, false, bAlsoDeleteRule);
+ }
+ return false;
+}
+
+bool CPVRGUIActionsTimers::DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ bool bIsRecording,
+ bool bDeleteRule) const
+{
+ TimerOperationResult result =
+ CServiceBroker::GetPVRManager().Timers()->DeleteTimer(timer, bIsRecording, bDeleteRule);
+ switch (result)
+ {
+ case TimerOperationResult::RECORDING:
+ {
+ // recording running. ask the user if it should be deleted anyway
+ if (HELPERS::ShowYesNoDialogText(
+ CVariant{122}, // "Confirm delete"
+ CVariant{
+ 19122}) // "This timer is still recording. Are you sure you want to delete this timer?"
+ != HELPERS::DialogResponse::CHOICE_YES)
+ return false;
+
+ return DeleteTimer(timer, true, bDeleteRule);
+ }
+ case TimerOperationResult::OK:
+ {
+ return true;
+ }
+ case TimerOperationResult::FAILED:
+ {
+ HELPERS::ShowOKDialogText(
+ CVariant{257},
+ CVariant{
+ 19110}); // "Error", "Could not delete the timer. Check the log for more information about this message."
+ return false;
+ }
+ default:
+ {
+ CLog::LogF(LOGERROR, "Unhandled TimerOperationResult ({})!", static_cast<int>(result));
+ break;
+ }
+ }
+ return false;
+}
+
+bool CPVRGUIActionsTimers::ConfirmDeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ bool& bDeleteRule) const
+{
+ bool bConfirmed(false);
+ const std::shared_ptr<CPVRTimerInfoTag> parentTimer(
+ CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer));
+
+ if (parentTimer && parentTimer->GetTimerType()->AllowsDelete())
+ {
+ // timer was scheduled by a deletable timer rule. prompt user for confirmation for deleting the timer rule, including scheduled timers.
+ bool bCancel(false);
+ bDeleteRule = CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{122}, // "Confirm delete"
+ CVariant{
+ 840}, // "Do you want to delete only this timer or also the timer rule that has scheduled it?"
+ CVariant{""}, CVariant{timer->Title()}, bCancel, CVariant{841}, // "Only this"
+ CVariant{593}, // "All"
+ 0); // no autoclose
+ bConfirmed = !bCancel;
+ }
+ else
+ {
+ bDeleteRule = false;
+
+ // prompt user for confirmation for deleting the timer
+ bConfirmed = CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{122}, // "Confirm delete"
+ timer->IsTimerRule()
+ ? CVariant{845}
+ // "Are you sure you want to delete this timer rule and all timers it has scheduled?"
+ : CVariant{846}, // "Are you sure you want to delete this timer?"
+ CVariant{""}, CVariant{timer->Title()});
+ }
+
+ return bConfirmed;
+}
+
+bool CPVRGUIActionsTimers::StopRecording(const CFileItem& item) const
+{
+ if (!DeleteTimer(item, true, false))
+ return false;
+
+ CServiceBroker::GetPVRManager().TriggerRecordingsUpdate();
+ return true;
+}
+
+bool CPVRGUIActionsTimers::ConfirmStopRecording(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer) const
+{
+ return CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{847}, // "Confirm stop recording"
+ CVariant{848}, // "Are you sure you want to stop this recording?"
+ CVariant{""}, CVariant{timer->Title()});
+}
+
+namespace
+{
+std::string GetAnnouncerText(const std::shared_ptr<CPVRTimerInfoTag>& timer, int idEpg, int idNoEpg)
+{
+ std::string text;
+ if (timer->IsEpgBased())
+ {
+ text = StringUtils::Format(g_localizeStrings.Get(idEpg),
+ timer->Title(), // tv show title
+ timer->ChannelName(),
+ timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false));
+ }
+ else
+ {
+ text = StringUtils::Format(g_localizeStrings.Get(idNoEpg), timer->ChannelName(),
+ timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false));
+ }
+ return text;
+}
+
+void AddEventLogEntry(const std::shared_ptr<CPVRTimerInfoTag>& timer, int idEpg, int idNoEpg)
+{
+ std::string name;
+ std::string icon;
+
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(timer->GetTimerType()->GetClientId());
+ if (client)
+ {
+ name = client->GetFriendlyName();
+ icon = client->Icon();
+ }
+ else
+ {
+ name = g_sysinfo.GetAppName();
+ icon = "special://xbmc/media/icon256x256.png";
+ }
+
+ CPVREventLogJob* job = new CPVREventLogJob;
+ job->AddEvent(false, // do not display a toast, only log event
+ EventLevel::Information, // info, no error
+ name, GetAnnouncerText(timer, idEpg, idNoEpg), icon);
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+}
+} // unnamed namespace
+
+void CPVRGUIActionsTimers::AnnounceReminder(const std::shared_ptr<CPVRTimerInfoTag>& timer) const
+{
+ if (!timer->IsReminder())
+ {
+ CLog::LogF(LOGERROR, "No reminder timer!");
+ return;
+ }
+
+ if (timer->EndAsUTC() < CDateTime::GetUTCDateTime())
+ {
+ // expired. timer end is in the past. write event log entry.
+ AddEventLogEntry(timer, 19305, 19306); // Deleted missed PVR reminder ...
+ return;
+ }
+
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingChannel(timer->Channel()))
+ {
+ // no need for an announcement. channel in question is already playing.
+ return;
+ }
+
+ // show the reminder dialog
+ CGUIDialogProgress* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
+ WINDOW_DIALOG_PROGRESS);
+ if (!dialog)
+ return;
+
+ dialog->Reset();
+
+ dialog->SetHeading(CVariant{19312}); // "PVR reminder"
+ dialog->ShowChoice(0, CVariant{19165}); // "Switch"
+
+ std::string text = GetAnnouncerText(timer, 19307, 19308); // Reminder for ...
+
+ bool bCanRecord = false;
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(timer->ClientID());
+ if (client && client->GetClientCapabilities().SupportsTimers())
+ {
+ bCanRecord = true;
+ dialog->ShowChoice(1, CVariant{264}); // "Record"
+ dialog->ShowChoice(2, CVariant{222}); // "Cancel"
+
+ if (m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTORECORD))
+ text += "\n\n" + g_localizeStrings.Get(
+ 19309); // (Auto-close of this reminder will schedule a recording...)
+ else if (m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTOSWITCH))
+ text += "\n\n" + g_localizeStrings.Get(
+ 19331); // (Auto-close of this reminder will switch to channel...)
+ }
+ else
+ {
+ dialog->ShowChoice(1, CVariant{222}); // "Cancel"
+ }
+
+ dialog->SetText(text);
+ dialog->SetPercentage(100);
+
+ dialog->Open();
+
+ int result = CGUIDialogProgress::CHOICE_NONE;
+
+ static constexpr int PROGRESS_TIMESLICE_MILLISECS = 50;
+
+ const int iWait = m_settings.GetIntValue(CSettings::SETTING_PVRREMINDERS_AUTOCLOSEDELAY) * 1000;
+ int iRemaining = iWait;
+ while (iRemaining > 0)
+ {
+ result = dialog->GetChoice();
+ if (result != CGUIDialogProgress::CHOICE_NONE)
+ break;
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(PROGRESS_TIMESLICE_MILLISECS));
+
+ iRemaining -= PROGRESS_TIMESLICE_MILLISECS;
+ dialog->SetPercentage(iRemaining * 100 / iWait);
+ dialog->Progress();
+ }
+
+ dialog->Close();
+
+ bool bAutoClosed = (iRemaining <= 0);
+ bool bSwitch = (result == 0);
+ bool bRecord = (result == 1);
+
+ if (bAutoClosed)
+ {
+ bRecord = (bCanRecord && m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTORECORD));
+ bSwitch = m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTOSWITCH);
+ }
+
+ if (bRecord)
+ {
+ std::shared_ptr<CPVRTimerInfoTag> newTimer;
+
+ std::shared_ptr<CPVREpgInfoTag> epgTag = timer->GetEpgInfoTag();
+ if (epgTag)
+ {
+ newTimer = CPVRTimerInfoTag::CreateFromEpg(epgTag, false);
+ if (newTimer)
+ {
+ // an epgtag can only have max one timer - we need to clear the reminder to be able to
+ // attach the recording timer
+ DeleteTimer(timer, false, false);
+ }
+ }
+ else
+ {
+ int iDuration = (timer->EndAsUTC() - timer->StartAsUTC()).GetSecondsTotal() / 60;
+ newTimer = CPVRTimerInfoTag::CreateTimerTag(timer->Channel(), timer->StartAsUTC(), iDuration);
+ }
+
+ if (newTimer)
+ {
+ // schedule recording
+ AddTimer(CFileItem(newTimer), false);
+ }
+
+ if (bAutoClosed)
+ {
+ AddEventLogEntry(timer, 19310,
+ 19311); // Scheduled recording for auto-closed PVR reminder ...
+ }
+ }
+
+ if (bSwitch)
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(
+ timer->Channel());
+ if (groupMember)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ CFileItem(groupMember), false);
+
+ if (bAutoClosed)
+ {
+ AddEventLogEntry(timer, 19332,
+ 19333); // Switched channel for auto-closed PVR reminder ...
+ }
+ }
+ }
+}
+
+void CPVRGUIActionsTimers::AnnounceReminders() const
+{
+ // Prevent multiple yesno dialogs, all on same call stack, due to gui message processing while dialog is open.
+ if (m_bReminderAnnouncementRunning)
+ return;
+
+ m_bReminderAnnouncementRunning = true;
+ std::shared_ptr<CPVRTimerInfoTag> timer =
+ CServiceBroker::GetPVRManager().Timers()->GetNextReminderToAnnnounce();
+ while (timer)
+ {
+ AnnounceReminder(timer);
+ timer = CServiceBroker::GetPVRManager().Timers()->GetNextReminderToAnnnounce();
+ }
+ m_bReminderAnnouncementRunning = false;
+}
diff --git a/xbmc/pvr/guilib/PVRGUIActionsTimers.h b/xbmc/pvr/guilib/PVRGUIActionsTimers.h
new file mode 100644
index 0000000..f6ae8fe
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsTimers.h
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2016-2022 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 "pvr/IPVRComponent.h"
+#include "pvr/settings/PVRSettings.h"
+
+#include <memory>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRTimerInfoTag;
+
+class CPVRGUIActionsTimers : public IPVRComponent
+{
+public:
+ CPVRGUIActionsTimers();
+ ~CPVRGUIActionsTimers() override = default;
+
+ /*!
+ * @brief Open the timer settings dialog to create a new tv or radio timer.
+ * @param bRadio indicates whether a radio or tv timer shall be created.
+ * @return true on success, false otherwise.
+ */
+ bool AddTimer(bool bRadio) const;
+
+ /*!
+ * @brief Create a new timer, either interactive or non-interactive.
+ * @param item containing epg data to create a timer for. item must be an epg tag or a channel.
+ * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior
+ * creating the timer.
+ * @return true, if the timer was created successfully, false otherwise.
+ */
+ bool AddTimer(const CFileItem& item, bool bShowTimerSettings) const;
+
+ /*!
+ * @brief Add a timer to the client. Doesn't add the timer to the container. The backend will
+ * do this.
+ * @return True if it was sent correctly, false if not.
+ */
+ bool AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& item) const;
+
+ /*!
+ * @brief Create a new timer rule, either interactive or non-interactive.
+ * @param item containing epg data to create a timer rule for. item must be an epg tag or a
+ * channel.
+ * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior
+ * creating the timer rule.
+ * @param bFallbackToOneShotTimer if no timer rule can be created, try to create a one-shot
+ * timer instead.
+ * @return true, if the timer rule was created successfully, false otherwise.
+ */
+ bool AddTimerRule(const CFileItem& item,
+ bool bShowTimerSettings,
+ bool bFallbackToOneShotTimer) const;
+
+ /*!
+ * @brief Creates or deletes a timer for the given epg tag.
+ * @param item containing an epg tag.
+ * @return true on success, false otherwise.
+ */
+ bool ToggleTimer(const CFileItem& item) const;
+
+ /*!
+ * @brief Toggles a given timer's enabled/disabled state.
+ * @param item containing a timer.
+ * @return true on success, false otherwise.
+ */
+ bool ToggleTimerState(const CFileItem& item) const;
+
+ /*!
+ * @brief Open the timer settings dialog to edit an existing timer.
+ * @param item containing an epg tag or a timer.
+ * @return true on success, false otherwise.
+ */
+ bool EditTimer(const CFileItem& item) const;
+
+ /*!
+ * @brief Open the timer settings dialog to edit an existing timer rule.
+ * @param item containing an epg tag or a timer.
+ * @return true on success, false otherwise.
+ */
+ bool EditTimerRule(const CFileItem& item) const;
+
+ /*!
+ * @brief Get the timer rule for a given timer
+ * @param item containing an item to query the timer rule for. item must be a timer or an epg tag.
+ * @return The timer rule item, or nullptr if none was found.
+ */
+ std::shared_ptr<CFileItem> GetTimerRule(const CFileItem& item) const;
+
+ /*!
+ * @brief Delete a timer, always showing a confirmation dialog.
+ * @param item containing a timer to delete. item must be a timer, an epg tag or a channel.
+ * @return true, if the timer was deleted successfully, false otherwise.
+ */
+ bool DeleteTimer(const CFileItem& item) const;
+
+ /*!
+ * @brief Delete a timer rule, always showing a confirmation dialog.
+ * @param item containing a timer rule to delete. item must be a timer, an epg tag or a channel.
+ * @return true, if the timer rule was deleted successfully, false otherwise.
+ */
+ bool DeleteTimerRule(const CFileItem& item) const;
+
+ /*!
+ * @brief Toggle recording on the currently playing channel, if any.
+ * @return True if the recording was started or stopped successfully, false otherwise.
+ */
+ bool ToggleRecordingOnPlayingChannel();
+
+ /*!
+ * @brief Start or stop recording on a given channel.
+ * @param channel the channel to start/stop recording.
+ * @param bOnOff True to start recording, false to stop.
+ * @return True if the recording was started or stopped successfully, false otherwise.
+ */
+ bool SetRecordingOnChannel(const std::shared_ptr<CPVRChannel>& channel, bool bOnOff);
+
+ /*!
+ * @brief Stop a currently active recording, always showing a confirmation dialog.
+ * @param item containing a recording to stop. item must be a timer, an epg tag or a channel.
+ * @return true, if the recording was stopped successfully, false otherwise.
+ */
+ bool StopRecording(const CFileItem& item) const;
+
+ /*!
+ * @brief Create a new reminder timer, non-interactive.
+ * @param item containing epg data to create a reminder timer for. item must be an epg tag.
+ * @return true, if the timer was created successfully, false otherwise.
+ */
+ bool AddReminder(const CFileItem& item) const;
+
+ /*!
+ * @brief Announce due reminders, if any.
+ */
+ void AnnounceReminders() const;
+
+private:
+ CPVRGUIActionsTimers(const CPVRGUIActionsTimers&) = delete;
+ CPVRGUIActionsTimers const& operator=(CPVRGUIActionsTimers const&) = delete;
+
+ /*!
+ * @brief Open the timer settings dialog.
+ * @param timer containing the timer the settings shall be displayed for.
+ * @return true, if the dialog was ended successfully, false otherwise.
+ */
+ bool ShowTimerSettings(const std::shared_ptr<CPVRTimerInfoTag>& timer) const;
+
+ /*!
+ * @brief Add a timer or timer rule, either interactive or non-interactive.
+ * @param item containing epg data to create a timer or timer rule for. item must be an epg tag
+ * or a channel.
+ * @param bCreateteRule denotes whether to create a one-shot timer or a timer rule.
+ * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior
+ * creating the timer or timer rule.
+ * @param bFallbackToOneShotTimer if bCreateteRule is true and no timer rule can be created, try
+ * to create a one-shot timer instead.
+ * @return true, if the timer or timer rule was created successfully, false otherwise.
+ */
+ bool AddTimer(const CFileItem& item,
+ bool bCreateRule,
+ bool bShowTimerSettings,
+ bool bFallbackToOneShotTimer) const;
+
+ /*!
+ * @brief Delete a timer or timer rule, always showing a confirmation dialog.
+ * @param item containing a timer or timer rule to delete. item must be a timer, an epg tag or
+ * a channel.
+ * @param bIsRecording denotes whether the timer is currently recording (controls correct
+ * confirmation dialog).
+ * @param bDeleteRule denotes to delete a timer rule. For convenience, one can pass a timer
+ * created by a rule.
+ * @return true, if the timer or timer rule was deleted successfully, false otherwise.
+ */
+ bool DeleteTimer(const CFileItem& item, bool bIsRecording, bool bDeleteRule) const;
+
+ /*!
+ * @brief Delete a timer or timer rule, showing a confirmation dialog in case a timer currently
+ * recording shall be deleted.
+ * @param timer containing a timer or timer rule to delete.
+ * @param bIsRecording denotes whether the timer is currently recording (controls correct
+ * confirmation dialog).
+ * @param bDeleteRule denotes to delete a timer rule. For convenience, one can pass a timer
+ * created by a rule.
+ * @return true, if the timer or timer rule was deleted successfully, false otherwise.
+ */
+ bool DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ bool bIsRecording,
+ bool bDeleteRule) const;
+
+ /*!
+ * @brief Open a dialog to confirm timer delete.
+ * @param timer the timer to delete.
+ * @param bDeleteRule in: ignored. out, for one shot timer scheduled by a timer rule: true to
+ * also delete the timer rule that has scheduled this timer, false to only delete the one shot
+ * timer. out, for one shot timer not scheduled by a timer rule: ignored
+ * @return true, to proceed with delete, false otherwise.
+ */
+ bool ConfirmDeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer, bool& bDeleteRule) const;
+
+ /*!
+ * @brief Open a dialog to confirm stop recording.
+ * @param timer the recording to stop (actually the timer to delete).
+ * @return true, to proceed with delete, false otherwise.
+ */
+ bool ConfirmStopRecording(const std::shared_ptr<CPVRTimerInfoTag>& timer) const;
+
+ /*!
+ * @brief Announce and process a reminder timer.
+ * @param timer The reminder timer.
+ */
+ void AnnounceReminder(const std::shared_ptr<CPVRTimerInfoTag>& timer) const;
+
+ CPVRSettings m_settings;
+ mutable bool m_bReminderAnnouncementRunning{false};
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Timers = CPVRGUIActionsTimers;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp b/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp
new file mode 100644
index 0000000..28e5582
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016-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 "PVRGUIActionsUtils.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsRecordings.h"
+
+namespace PVR
+{
+bool CPVRGUIActionsUtils::OnInfo(const CFileItem& item)
+{
+ if (item.HasPVRRecordingInfoTag())
+ {
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(item);
+ }
+ else if (item.HasPVRChannelInfoTag() || item.HasPVRTimerInfoTag())
+ {
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(item);
+ }
+ else if (item.HasEPGSearchFilter())
+ {
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().EditSavedSearch(item);
+ }
+ return false;
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIActionsUtils.h b/xbmc/pvr/guilib/PVRGUIActionsUtils.h
new file mode 100644
index 0000000..a880548
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIActionsUtils.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016-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 "pvr/IPVRComponent.h"
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVRGUIActionsUtils : public IPVRComponent
+{
+public:
+ CPVRGUIActionsUtils() = default;
+ ~CPVRGUIActionsUtils() override = default;
+
+ /*!
+ * @brief Process info action for the given item.
+ * @param item The item.
+ */
+ bool OnInfo(const CFileItem& item);
+
+private:
+ CPVRGUIActionsUtils(const CPVRGUIActionsUtils&) = delete;
+ CPVRGUIActionsUtils const& operator=(CPVRGUIActionsUtils const&) = delete;
+};
+
+namespace GUI
+{
+// pretty scope and name
+using Utils = CPVRGUIActionsUtils;
+} // namespace GUI
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp
new file mode 100644
index 0000000..a239fe2
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012-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.
+ */
+
+#include "PVRGUIChannelIconUpdater.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "filesystem/Directory.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/guilib/PVRGUIProgressHandler.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+void CPVRGUIChannelIconUpdater::SearchAndUpdateMissingChannelIcons() const
+{
+ const std::string iconPath = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_PVRMENU_ICONPATH);
+ if (iconPath.empty())
+ return;
+
+ // fetch files in icon path for fast lookup
+ CFileItemList fileItemList;
+ XFILE::CDirectory::GetDirectory(iconPath, fileItemList, ".jpg|.png|.tbn", XFILE::DIR_FLAG_DEFAULTS);
+
+ if (fileItemList.IsEmpty())
+ return;
+
+ CLog::Log(LOGINFO, "Starting PVR channel icon search");
+
+ // create a map for fast lookup of normalized file base name
+ std::map<std::string, std::string> fileItemMap;
+ for (const auto& item : fileItemList)
+ {
+ std::string baseName = URIUtils::GetFileName(item->GetPath());
+ URIUtils::RemoveExtension(baseName);
+ StringUtils::ToLower(baseName);
+ fileItemMap.insert({baseName, item->GetPath()});
+ }
+
+ std::unique_ptr<CPVRGUIProgressHandler> progressHandler;
+ if (!m_groups.empty())
+ progressHandler.reset(
+ new CPVRGUIProgressHandler(g_localizeStrings.Get(19286))); // Searching for channel icons
+
+ for (const auto& group : m_groups)
+ {
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> members = group->GetMembers();
+ int channelIndex = 0;
+ for (const auto& member : members)
+ {
+ const std::shared_ptr<CPVRChannel> channel = member->Channel();
+
+ progressHandler->UpdateProgress(channel->ChannelName(), channelIndex++, members.size());
+
+ // skip if an icon is already set and exists
+ if (CFileUtils::Exists(channel->IconPath()))
+ continue;
+
+ // reset icon before searching for a new one
+ channel->SetIconPath("");
+
+ const std::string strChannelUid = StringUtils::Format("{:08}", channel->UniqueID());
+ std::string strLegalClientChannelName =
+ CUtil::MakeLegalFileName(channel->ClientChannelName());
+ StringUtils::ToLower(strLegalClientChannelName);
+ std::string strLegalChannelName = CUtil::MakeLegalFileName(channel->ChannelName());
+ StringUtils::ToLower(strLegalChannelName);
+
+ std::map<std::string, std::string>::iterator itItem;
+ if ((itItem = fileItemMap.find(strLegalClientChannelName)) != fileItemMap.end() ||
+ (itItem = fileItemMap.find(strLegalChannelName)) != fileItemMap.end() ||
+ (itItem = fileItemMap.find(strChannelUid)) != fileItemMap.end())
+ {
+ channel->SetIconPath(itItem->second, CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_bPVRAutoScanIconsUserSet);
+ }
+
+ if (m_bUpdateDb)
+ channel->Persist();
+ }
+ }
+}
diff --git a/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h
new file mode 100644
index 0000000..d64a41f
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012-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 <memory>
+#include <vector>
+
+namespace PVR
+{
+
+class CPVRChannelGroup;
+
+class CPVRGUIChannelIconUpdater
+{
+public:
+ /*!
+ * @brief ctor.
+ * @param groups The channel groups for which the channel icons shall be updated.
+ * @param bUpdateDb If true, persist the changed values in the PVR database.
+ */
+ CPVRGUIChannelIconUpdater(const std::vector<std::shared_ptr<CPVRChannelGroup>>& groups, bool bUpdateDb)
+ : m_groups(groups), m_bUpdateDb(bUpdateDb) {}
+
+ CPVRGUIChannelIconUpdater() = delete;
+ CPVRGUIChannelIconUpdater(const CPVRGUIChannelIconUpdater&) = delete;
+ CPVRGUIChannelIconUpdater& operator=(const CPVRGUIChannelIconUpdater&) = delete;
+
+ virtual ~CPVRGUIChannelIconUpdater() = default;
+
+ /*!
+ * @brief Search and update missing channel icons.
+ */
+ void SearchAndUpdateMissingChannelIcons() const;
+
+private:
+ const std::vector<std::shared_ptr<CPVRChannelGroup>> m_groups;
+ const bool m_bUpdateDb = false;
+};
+
+}
diff --git a/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp b/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp
new file mode 100644
index 0000000..0ccfdf8
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2017-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 "PVRGUIChannelNavigator.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SystemClock.h"
+#include "utils/Job.h"
+#include "utils/JobManager.h"
+#include "utils/XTimeUtils.h"
+
+#include <mutex>
+
+using namespace KODI::GUILIB::GUIINFO;
+using namespace PVR;
+using namespace std::chrono_literals;
+
+namespace
+{
+class CPVRChannelTimeoutJobBase : public CJob, public IJobCallback
+{
+public:
+ CPVRChannelTimeoutJobBase() = delete;
+ CPVRChannelTimeoutJobBase(PVR::CPVRGUIChannelNavigator& channelNavigator,
+ std::chrono::milliseconds timeout)
+ : m_channelNavigator(channelNavigator)
+ {
+ m_delayTimer.Set(timeout);
+ }
+
+ ~CPVRChannelTimeoutJobBase() override = default;
+
+ virtual void OnTimeout() = 0;
+
+ void OnJobComplete(unsigned int iJobID, bool bSuccess, CJob* job) override {}
+
+ bool DoWork() override
+ {
+ while (!ShouldCancel(0, 0))
+ {
+ if (m_delayTimer.IsTimePast())
+ {
+ OnTimeout();
+ return true;
+ }
+ KODI::TIME::Sleep(10ms);
+ }
+ return false;
+ }
+
+protected:
+ PVR::CPVRGUIChannelNavigator& m_channelNavigator;
+
+private:
+ XbmcThreads::EndTime<> m_delayTimer;
+};
+
+class CPVRChannelEntryTimeoutJob : public CPVRChannelTimeoutJobBase
+{
+public:
+ CPVRChannelEntryTimeoutJob(PVR::CPVRGUIChannelNavigator& channelNavigator,
+ std::chrono::milliseconds timeout)
+ : CPVRChannelTimeoutJobBase(channelNavigator, timeout)
+ {
+ }
+ ~CPVRChannelEntryTimeoutJob() override = default;
+ const char* GetType() const override { return "pvr-channel-entry-timeout-job"; }
+ void OnTimeout() override { m_channelNavigator.SwitchToCurrentChannel(); }
+};
+
+class CPVRChannelInfoTimeoutJob : public CPVRChannelTimeoutJobBase
+{
+public:
+ CPVRChannelInfoTimeoutJob(PVR::CPVRGUIChannelNavigator& channelNavigator,
+ std::chrono::milliseconds timeout)
+ : CPVRChannelTimeoutJobBase(channelNavigator, timeout)
+ {
+ }
+ ~CPVRChannelInfoTimeoutJob() override = default;
+ const char* GetType() const override { return "pvr-channel-info-timeout-job"; }
+ void OnTimeout() override { m_channelNavigator.HideInfo(); }
+};
+} // unnamed namespace
+
+CPVRGUIChannelNavigator::CPVRGUIChannelNavigator()
+{
+ // Note: we cannot subscribe to PlayerInfoProvider here, as we're getting constructed
+ // before the info providers. We will subscribe once our first subscriber appears.
+}
+
+CPVRGUIChannelNavigator::~CPVRGUIChannelNavigator()
+{
+ const auto gui = CServiceBroker::GetGUI();
+ if (!gui)
+ return;
+
+ gui->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().Events().Unsubscribe(this);
+}
+
+void CPVRGUIChannelNavigator::SubscribeToShowInfoEventStream()
+{
+ CServiceBroker::GetGUI()
+ ->GetInfoManager()
+ .GetInfoProviders()
+ .GetPlayerInfoProvider()
+ .Events()
+ .Subscribe(this, &CPVRGUIChannelNavigator::Notify);
+}
+
+void CPVRGUIChannelNavigator::CheckAndPublishPreviewAndPlayerShowInfoChangedEvent()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const bool currentValue = IsPreview() && m_playerShowInfo;
+ if (m_previewAndPlayerShowInfo != currentValue)
+ {
+ m_previewAndPlayerShowInfo = currentValue;
+
+ // inform subscribers
+ m_events.Publish(PVRPreviewAndPlayerShowInfoChangedEvent(currentValue));
+ }
+}
+
+void CPVRGUIChannelNavigator::Notify(const PlayerShowInfoChangedEvent& event)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_playerShowInfo = event.m_showInfo;
+ CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+}
+
+void CPVRGUIChannelNavigator::SelectNextChannel(ChannelSwitchMode eSwitchMode)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_playerShowInfo && eSwitchMode == ChannelSwitchMode::NO_SWITCH)
+ {
+ // show info for current channel on first next channel selection.
+ ShowInfo(false);
+ return;
+ }
+
+ const std::shared_ptr<CPVRChannelGroupMember> nextMember = GetNextOrPrevChannel(true);
+ if (nextMember)
+ SelectChannel(nextMember, eSwitchMode);
+}
+
+void CPVRGUIChannelNavigator::SelectPreviousChannel(ChannelSwitchMode eSwitchMode)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (!m_playerShowInfo && eSwitchMode == ChannelSwitchMode::NO_SWITCH)
+ {
+ // show info for current channel on first previous channel selection.
+ ShowInfo(false);
+ return;
+ }
+
+ const std::shared_ptr<CPVRChannelGroupMember> prevMember = GetNextOrPrevChannel(false);
+ if (prevMember)
+ SelectChannel(prevMember, eSwitchMode);
+}
+
+std::shared_ptr<CPVRChannelGroupMember> CPVRGUIChannelNavigator::GetNextOrPrevChannel(bool bNext)
+{
+ const bool bPlayingRadio = CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio();
+ const bool bPlayingTV = CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV();
+
+ if (bPlayingTV || bPlayingRadio)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bPlayingRadio);
+ if (group)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return bNext ? group->GetNextChannelGroupMember(m_currentChannel)
+ : group->GetPreviousChannelGroupMember(m_currentChannel);
+ }
+ }
+ return {};
+}
+
+void CPVRGUIChannelNavigator::SelectChannel(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember, ChannelSwitchMode eSwitchMode)
+{
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(CFileItem(groupMember));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_currentChannel = groupMember;
+ ShowInfo(false);
+
+ CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+
+ if (IsPreview() && eSwitchMode == ChannelSwitchMode::INSTANT_OR_DELAYED_SWITCH)
+ {
+ auto timeout =
+ std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRPLAYBACK_CHANNELENTRYTIMEOUT));
+ if (timeout > 0ms)
+ {
+ // delayed switch
+ if (m_iChannelEntryJobId >= 0)
+ CServiceBroker::GetJobManager()->CancelJob(m_iChannelEntryJobId);
+
+ CPVRChannelEntryTimeoutJob* job = new CPVRChannelEntryTimeoutJob(*this, timeout);
+ m_iChannelEntryJobId =
+ CServiceBroker::GetJobManager()->AddJob(job, dynamic_cast<IJobCallback*>(job));
+ }
+ else
+ {
+ // instant switch
+ SwitchToCurrentChannel();
+ }
+ }
+}
+
+void CPVRGUIChannelNavigator::SwitchToCurrentChannel()
+{
+ std::unique_ptr<CFileItem> item;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iChannelEntryJobId >= 0)
+ {
+ CServiceBroker::GetJobManager()->CancelJob(m_iChannelEntryJobId);
+ m_iChannelEntryJobId = -1;
+ }
+
+ item = std::make_unique<CFileItem>(m_currentChannel);
+ }
+
+ if (item)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(*item, false);
+}
+
+bool CPVRGUIChannelNavigator::IsPreview() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_currentChannel != m_playingChannel;
+}
+
+bool CPVRGUIChannelNavigator::IsPreviewAndShowInfo() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_previewAndPlayerShowInfo;
+}
+
+void CPVRGUIChannelNavigator::ShowInfo()
+{
+ ShowInfo(true);
+}
+
+void CPVRGUIChannelNavigator::ShowInfo(bool bForce)
+{
+ auto timeout = std::chrono::seconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRMENU_DISPLAYCHANNELINFO));
+
+ if (bForce || timeout > 0s)
+ {
+ CServiceBroker::GetGUI()
+ ->GetInfoManager()
+ .GetInfoProviders()
+ .GetPlayerInfoProvider()
+ .SetShowInfo(true);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iChannelInfoJobId >= 0)
+ {
+ CServiceBroker::GetJobManager()->CancelJob(m_iChannelInfoJobId);
+ m_iChannelInfoJobId = -1;
+ }
+
+ if (!bForce && timeout > 0s)
+ {
+ CPVRChannelInfoTimeoutJob* job = new CPVRChannelInfoTimeoutJob(*this, timeout);
+ m_iChannelInfoJobId =
+ CServiceBroker::GetJobManager()->AddJob(job, dynamic_cast<IJobCallback*>(job));
+ }
+ }
+}
+
+void CPVRGUIChannelNavigator::HideInfo()
+{
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().SetShowInfo(
+ false);
+
+ CFileItemPtr item;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iChannelInfoJobId >= 0)
+ {
+ CServiceBroker::GetJobManager()->CancelJob(m_iChannelInfoJobId);
+ m_iChannelInfoJobId = -1;
+ }
+
+ if (m_currentChannel != m_playingChannel)
+ {
+ m_currentChannel = m_playingChannel;
+ if (m_playingChannel)
+ item.reset(new CFileItem(m_playingChannel));
+ }
+
+ CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+ }
+
+ if (item)
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*item);
+}
+
+void CPVRGUIChannelNavigator::ToggleInfo()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_playerShowInfo)
+ HideInfo();
+ else
+ ShowInfo();
+}
+
+void CPVRGUIChannelNavigator::SetPlayingChannel(
+ const std::shared_ptr<CPVRChannelGroupMember>& groupMember)
+{
+ CFileItemPtr item;
+
+ if (groupMember)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_playingChannel = groupMember;
+ if (m_currentChannel != m_playingChannel)
+ {
+ m_currentChannel = m_playingChannel;
+ if (m_playingChannel)
+ item.reset(new CFileItem(m_playingChannel));
+ }
+
+ CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+ }
+
+ if (item)
+ CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*item);
+
+ ShowInfo(false);
+}
+
+void CPVRGUIChannelNavigator::ClearPlayingChannel()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_playingChannel.reset();
+ HideInfo();
+
+ CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+}
diff --git a/xbmc/pvr/guilib/PVRGUIChannelNavigator.h b/xbmc/pvr/guilib/PVRGUIChannelNavigator.h
new file mode 100644
index 0000000..5c27074
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIChannelNavigator.h
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2017-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 "utils/EventStream.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+struct PlayerShowInfoChangedEvent;
+}
+} // namespace GUILIB
+} // namespace KODI
+
+namespace PVR
+{
+enum class ChannelSwitchMode
+{
+ NO_SWITCH, // no channel switch
+ INSTANT_OR_DELAYED_SWITCH // switch according to SETTING_PVRPLAYBACK_CHANNELENTRYTIMEOUT
+};
+
+struct PVRPreviewAndPlayerShowInfoChangedEvent
+{
+ explicit PVRPreviewAndPlayerShowInfoChangedEvent(bool previewAndPlayerShowInfo)
+ : m_previewAndPlayerShowInfo(previewAndPlayerShowInfo)
+ {
+ }
+ virtual ~PVRPreviewAndPlayerShowInfoChangedEvent() = default;
+
+ bool m_previewAndPlayerShowInfo{false};
+};
+
+class CPVRChannelGroupMember;
+
+class CPVRGUIChannelNavigator
+{
+public:
+ CPVRGUIChannelNavigator();
+ virtual ~CPVRGUIChannelNavigator();
+
+ /*!
+ * @brief Subscribe to the event stream for changes of channel preview and player show info.
+ * @param owner The subscriber.
+ * @param fn The callback function of the subscriber for the events.
+ */
+ template<typename A>
+ void Subscribe(A* owner, void (A::*fn)(const PVRPreviewAndPlayerShowInfoChangedEvent&))
+ {
+ SubscribeToShowInfoEventStream();
+ m_events.Subscribe(owner, fn);
+ }
+
+ /*!
+ * @brief Unsubscribe from the event stream for changes of channel preview and player show info.
+ * @param obj The subscriber.
+ */
+ template<typename A>
+ void Unsubscribe(A* obj)
+ {
+ m_events.Unsubscribe(obj);
+ }
+
+ /*!
+ * @brief CEventStream callback for player show info flag changes.
+ * @param event The event.
+ */
+ void Notify(const KODI::GUILIB::GUIINFO::PlayerShowInfoChangedEvent& event);
+
+ /*!
+ * @brief Select the next channel in currently playing channel group, relative to the currently
+ * selected channel.
+ * @param eSwitchMode controls whether only the channel info OSD is triggered or whether
+ * additionally a (delayed) channel switch will be done.
+ */
+ void SelectNextChannel(ChannelSwitchMode eSwitchMode);
+
+ /*!
+ * @brief Select the previous channel in currently playing channel group, relative to the
+ * currently selected channel.
+ * @param eSwitchMode controls whether only the channel info OSD is triggered or whether
+ * additionally a (delayed) channel switch will be done.
+ */
+ void SelectPreviousChannel(ChannelSwitchMode eSwitchMode);
+
+ /*!
+ * @brief Switch to the currently selected channel.
+ */
+ void SwitchToCurrentChannel();
+
+ /*!
+ * @brief Query the state of channel preview.
+ * @return True, if the currently selected channel is different from the currently playing
+ * channel, False otherwise.
+ */
+ bool IsPreview() const;
+
+ /*!
+ * @brief Query the state of channel preview and channel info OSD.
+ * @return True, if the currently selected channel is different from the currently playing channel
+ * and channel info OSD is active, False otherwise.
+ */
+ bool IsPreviewAndShowInfo() const;
+
+ /*!
+ * @brief Show the channel info OSD.
+ */
+ void ShowInfo();
+
+ /*!
+ * @brief Hide the channel info OSD.
+ */
+ void HideInfo();
+
+ /*!
+ * @brief Toggle the channel info OSD visibility.
+ */
+ void ToggleInfo();
+
+ /*!
+ * @brief Set a new playing channel group member and show the channel info OSD for the new
+ * channel.
+ * @param groupMember The new playing channel group member
+ */
+ void SetPlayingChannel(const std::shared_ptr<CPVRChannelGroupMember>& groupMember);
+
+ /*!
+ * @brief Clear the currently playing channel and hide the channel info OSD.
+ */
+ void ClearPlayingChannel();
+
+private:
+ /*!
+ * @brief Get next or previous channel group member of the playing channel group, relative to the
+ * currently selected channel group member.
+ * @param bNext True to get the next channel group member, false to get the previous channel group
+ * member.
+ * @param return The channel or nullptr if not found.
+ */
+ std::shared_ptr<CPVRChannelGroupMember> GetNextOrPrevChannel(bool bNext);
+
+ /*!
+ * @brief Select a given channel group member, display channel info OSD, switch according to given
+ * switch mode.
+ * @param groupMember The channel group member to select.
+ * @param eSwitchMode The channel switch mode.
+ */
+ void SelectChannel(const std::shared_ptr<CPVRChannelGroupMember>& groupMember,
+ ChannelSwitchMode eSwitchMode);
+
+ /*!
+ * @brief Show the channel info OSD.
+ * @param bForce True ignores value of SETTING_PVRMENU_DISPLAYCHANNELINFO and always activates the
+ * info, False acts aaccording settings value.
+ */
+ void ShowInfo(bool bForce);
+
+ /*!
+ * @brief Subscribe to the event stream for changes of player show info.
+ */
+ void SubscribeToShowInfoEventStream();
+
+ /*!
+ * @brief Check if property preview and show info value changed, inform subscribers in case.
+ */
+ void CheckAndPublishPreviewAndPlayerShowInfoChangedEvent();
+
+ mutable CCriticalSection m_critSection;
+ std::shared_ptr<CPVRChannelGroupMember> m_playingChannel;
+ std::shared_ptr<CPVRChannelGroupMember> m_currentChannel;
+ int m_iChannelEntryJobId = -1;
+ int m_iChannelInfoJobId = -1;
+ CEventSource<PVRPreviewAndPlayerShowInfoChangedEvent> m_events;
+ bool m_playerShowInfo{false};
+ bool m_previewAndPlayerShowInfo{false};
+};
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp b/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp
new file mode 100644
index 0000000..f876522
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017-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 "PVRGUIProgressHandler.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+
+#include <algorithm>
+#include <cmath>
+#include <mutex>
+#include <string>
+
+using namespace std::chrono_literals;
+
+namespace PVR
+{
+
+CPVRGUIProgressHandler::CPVRGUIProgressHandler(const std::string& strTitle)
+ : CThread("PVRGUIProgressHandler"), m_strTitle(strTitle)
+{
+}
+
+void CPVRGUIProgressHandler::UpdateProgress(const std::string& strText, float fProgress)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bChanged = true;
+ m_strText = strText;
+ m_fProgress = fProgress;
+
+ if (!m_bCreated)
+ {
+ m_bCreated = true;
+ Create();
+ }
+}
+
+void CPVRGUIProgressHandler::UpdateProgress(const std::string& strText, int iCurrent, int iMax)
+{
+ float fPercentage = (iCurrent * 100.0f) / iMax;
+ if (!std::isnan(fPercentage))
+ fPercentage = std::min(100.0f, fPercentage);
+
+ UpdateProgress(strText, fPercentage);
+}
+
+void CPVRGUIProgressHandler::Process()
+{
+ CGUIDialogExtendedProgressBar* progressBar =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(
+ WINDOW_DIALOG_EXT_PROGRESS);
+ if (m_bStop || !progressBar)
+ return;
+
+ CGUIDialogProgressBarHandle* progressHandle = progressBar->GetHandle(m_strTitle);
+ if (!progressHandle)
+ return;
+
+ while (!m_bStop)
+ {
+ float fProgress = 0.0;
+ std::string strText;
+ bool bUpdate = false;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bChanged)
+ {
+ m_bChanged = false;
+ fProgress = m_fProgress;
+ strText = m_strText;
+ bUpdate = true;
+ }
+ }
+
+ if (bUpdate)
+ {
+ progressHandle->SetPercentage(fProgress);
+ progressHandle->SetText(strText);
+ }
+
+ // Intentionally ignore some changes that come in too fast. Humans cannot read as fast as Mr. Data ;-)
+ CThread::Sleep(100ms);
+ }
+
+ progressHandle->MarkFinished();
+}
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/PVRGUIProgressHandler.h b/xbmc/pvr/guilib/PVRGUIProgressHandler.h
new file mode 100644
index 0000000..6e7b72f
--- /dev/null
+++ b/xbmc/pvr/guilib/PVRGUIProgressHandler.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017-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/Thread.h"
+
+#include <string>
+
+namespace PVR
+{
+ class CPVRGUIProgressHandler : private CThread
+ {
+ public:
+ CPVRGUIProgressHandler() = delete;
+
+ /*!
+ * @brief Creates and asynchronously shows a progress dialog with the given title.
+ * @param strTitle The title for the progress dialog.
+ */
+ explicit CPVRGUIProgressHandler(const std::string& strTitle);
+
+ ~CPVRGUIProgressHandler() override = default;
+
+ /*!
+ * @brief Update the progress dialogs's content.
+ * @param strText The new progress text.
+ * @param fProgress The new progress value, in a range from 0.0 to 100.0.
+ */
+ void UpdateProgress(const std::string& strText, float fProgress);
+
+ /*!
+ * @brief Update the progress dialogs's content.
+ * @param strText The new progress text.
+ * @param iCurrent The new current progress value, must be less or equal iMax.
+ * @param iMax The new maximum progress value, must be greater or equal iCurrent.
+ */
+ void UpdateProgress(const std::string& strText, int iCurrent, int iMax);
+
+ protected:
+ // CThread implementation
+ void Process() override;
+
+ private:
+ CCriticalSection m_critSection;
+ const std::string m_strTitle;
+ std::string m_strText;
+ float m_fProgress{0.0f};
+ bool m_bChanged{false};
+ bool m_bCreated{false};
+ };
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/guiinfo/CMakeLists.txt b/xbmc/pvr/guilib/guiinfo/CMakeLists.txt
new file mode 100644
index 0000000..18491ee
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES PVRGUIInfo.cpp
+ PVRGUITimerInfo.cpp
+ PVRGUITimesInfo.cpp)
+
+set(HEADERS PVRGUIInfo.h
+ PVRGUITimerInfo.h
+ PVRGUITimesInfo.h)
+
+core_add_library(pvr_guilib_guiinfo)
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp
new file mode 100644
index 0000000..9cffb9a
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp
@@ -0,0 +1,2017 @@
+/*
+ * 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 "PVRGUIInfo.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfo.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/channels/PVRRadioRDSInfoTag.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/providers/PVRProvider.h"
+#include "pvr/providers/PVRProviders.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SingleLock.h"
+#include "threads/SystemClock.h"
+#include "utils/StringUtils.h"
+
+#include <cmath>
+#include <ctime>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::GUILIB::GUIINFO;
+using namespace std::chrono_literals;
+
+CPVRGUIInfo::CPVRGUIInfo() : CThread("PVRGUIInfo")
+{
+ ResetProperties();
+}
+
+void CPVRGUIInfo::ResetProperties()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_anyTimersInfo.ResetProperties();
+ m_tvTimersInfo.ResetProperties();
+ m_radioTimersInfo.ResetProperties();
+ m_timesInfo.Reset();
+ m_bHasTVRecordings = false;
+ m_bHasRadioRecordings = false;
+ m_iCurrentActiveClient = 0;
+ m_strPlayingClientName.clear();
+ m_strBackendName.clear();
+ m_strBackendVersion.clear();
+ m_strBackendHost.clear();
+ m_strBackendTimers.clear();
+ m_strBackendRecordings.clear();
+ m_strBackendDeletedRecordings.clear();
+ m_strBackendProviders.clear();
+ m_strBackendChannelGroups.clear();
+ m_strBackendChannels.clear();
+ m_iBackendDiskTotal = 0;
+ m_iBackendDiskUsed = 0;
+ m_bIsPlayingTV = false;
+ m_bIsPlayingRadio = false;
+ m_bIsPlayingRecording = false;
+ m_bIsPlayingEpgTag = false;
+ m_bIsPlayingEncryptedStream = false;
+ m_bIsRecordingPlayingChannel = false;
+ m_bCanRecordPlayingChannel = false;
+ m_bIsPlayingActiveRecording = false;
+ m_bHasTVChannels = false;
+ m_bHasRadioChannels = false;
+
+ ClearQualityInfo(m_qualityInfo);
+ ClearDescrambleInfo(m_descrambleInfo);
+
+ m_updateBackendCacheRequested = false;
+ m_bRegistered = false;
+}
+
+void CPVRGUIInfo::ClearQualityInfo(PVR_SIGNAL_STATUS& qualityInfo)
+{
+ memset(&qualityInfo, 0, sizeof(qualityInfo));
+ strncpy(qualityInfo.strAdapterName, g_localizeStrings.Get(13106).c_str(),
+ PVR_ADDON_NAME_STRING_LENGTH - 1);
+ strncpy(qualityInfo.strAdapterStatus, g_localizeStrings.Get(13106).c_str(),
+ PVR_ADDON_NAME_STRING_LENGTH - 1);
+}
+
+void CPVRGUIInfo::ClearDescrambleInfo(PVR_DESCRAMBLE_INFO& descrambleInfo)
+{
+ descrambleInfo = {};
+}
+
+void CPVRGUIInfo::Start()
+{
+ ResetProperties();
+ Create();
+ SetPriority(ThreadPriority::BELOW_NORMAL);
+}
+
+void CPVRGUIInfo::Stop()
+{
+ StopThread();
+
+ auto& mgr = CServiceBroker::GetPVRManager();
+ auto& channels = mgr.Get<PVR::GUI::Channels>();
+ channels.GetChannelNavigator().Unsubscribe(this);
+ channels.Events().Unsubscribe(this);
+ mgr.Events().Unsubscribe(this);
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ {
+ gui->GetInfoManager().UnregisterInfoProvider(this);
+ m_bRegistered = false;
+ }
+}
+
+void CPVRGUIInfo::Notify(const PVREvent& event)
+{
+ if (event == PVREvent::Timers || event == PVREvent::TimersInvalidated)
+ UpdateTimersCache();
+}
+
+void CPVRGUIInfo::Notify(const PVRChannelNumberInputChangedEvent& event)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channelNumberInput = event.m_input;
+}
+
+void CPVRGUIInfo::Notify(const PVRPreviewAndPlayerShowInfoChangedEvent& event)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_previewAndPlayerShowInfo = event.m_previewAndPlayerShowInfo;
+}
+
+void CPVRGUIInfo::Process()
+{
+ auto toggleIntervalMs = std::chrono::milliseconds(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRInfoToggleInterval);
+ XbmcThreads::EndTime<> cacheTimer(toggleIntervalMs);
+
+ auto& mgr = CServiceBroker::GetPVRManager();
+ mgr.Events().Subscribe(this, &CPVRGUIInfo::Notify);
+
+ auto& channels = mgr.Get<PVR::GUI::Channels>();
+ channels.Events().Subscribe(this, &CPVRGUIInfo::Notify);
+ channels.GetChannelNavigator().Subscribe(this, &CPVRGUIInfo::Notify);
+
+ /* updated on request */
+ UpdateTimersCache();
+
+ /* update the backend cache once initially */
+ m_updateBackendCacheRequested = true;
+
+ while (!g_application.m_bStop && !m_bStop)
+ {
+ if (!m_bRegistered)
+ {
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ {
+ gui->GetInfoManager().RegisterInfoProvider(this);
+ m_bRegistered = true;
+ }
+ }
+
+ if (!m_bStop)
+ UpdateQualityData();
+ std::this_thread::yield();
+
+ if (!m_bStop)
+ UpdateDescrambleData();
+ std::this_thread::yield();
+
+ if (!m_bStop)
+ UpdateMisc();
+ std::this_thread::yield();
+
+ if (!m_bStop)
+ UpdateTimeshiftData();
+ std::this_thread::yield();
+
+ if (!m_bStop)
+ UpdateTimersToggle();
+ std::this_thread::yield();
+
+ if (!m_bStop)
+ UpdateNextTimer();
+ std::this_thread::yield();
+
+ // Update the backend cache every toggleInterval seconds
+ if (!m_bStop && cacheTimer.IsTimePast())
+ {
+ UpdateBackendCache();
+ cacheTimer.Set(toggleIntervalMs);
+ }
+
+ if (!m_bStop)
+ CThread::Sleep(500ms);
+ }
+}
+
+void CPVRGUIInfo::UpdateQualityData()
+{
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRPLAYBACK_SIGNALQUALITY))
+ return;
+
+ const std::shared_ptr<CPVRPlaybackState> playbackState =
+ CServiceBroker::GetPVRManager().PlaybackState();
+ if (!playbackState)
+ return;
+
+ PVR_SIGNAL_STATUS qualityInfo;
+ ClearQualityInfo(qualityInfo);
+
+ const int channelUid = playbackState->GetPlayingChannelUniqueID();
+ if (channelUid > 0)
+ {
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().Clients()->GetCreatedClient(
+ playbackState->GetPlayingClientID());
+ if (client)
+ client->SignalQuality(channelUid, qualityInfo);
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_qualityInfo = qualityInfo;
+}
+
+void CPVRGUIInfo::UpdateDescrambleData()
+{
+ const std::shared_ptr<CPVRPlaybackState> playbackState =
+ CServiceBroker::GetPVRManager().PlaybackState();
+ if (!playbackState)
+ return;
+
+ PVR_DESCRAMBLE_INFO descrambleInfo;
+ ClearDescrambleInfo(descrambleInfo);
+
+ const int channelUid = playbackState->GetPlayingChannelUniqueID();
+ if (channelUid > 0)
+ {
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().Clients()->GetCreatedClient(
+ playbackState->GetPlayingClientID());
+ if (client)
+ client->GetDescrambleInfo(channelUid, descrambleInfo);
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_descrambleInfo = descrambleInfo;
+}
+
+void CPVRGUIInfo::UpdateMisc()
+{
+ const CPVRManager& mgr = CServiceBroker::GetPVRManager();
+ bool bStarted = mgr.IsStarted();
+ const std::shared_ptr<CPVRPlaybackState> state = mgr.PlaybackState();
+
+ /* safe to fetch these unlocked, since they're updated from the same thread as this one */
+ const std::string strPlayingClientName = bStarted ? state->GetPlayingClientName() : "";
+ const bool bHasTVRecordings = bStarted && mgr.Recordings()->GetNumTVRecordings() > 0;
+ const bool bHasRadioRecordings = bStarted && mgr.Recordings()->GetNumRadioRecordings() > 0;
+ const bool bIsPlayingTV = bStarted && state->IsPlayingTV();
+ const bool bIsPlayingRadio = bStarted && state->IsPlayingRadio();
+ const bool bIsPlayingRecording = bStarted && state->IsPlayingRecording();
+ const bool bIsPlayingEpgTag = bStarted && state->IsPlayingEpgTag();
+ const bool bIsPlayingEncryptedStream = bStarted && state->IsPlayingEncryptedChannel();
+ const bool bHasTVChannels = bStarted && mgr.ChannelGroups()->GetGroupAllTV()->HasChannels();
+ const bool bHasRadioChannels = bStarted && mgr.ChannelGroups()->GetGroupAllRadio()->HasChannels();
+ const bool bCanRecordPlayingChannel = bStarted && state->CanRecordOnPlayingChannel();
+ const bool bIsRecordingPlayingChannel = bStarted && state->IsRecordingOnPlayingChannel();
+ const bool bIsPlayingActiveRecording = bStarted && state->IsPlayingActiveRecording();
+ const std::string strPlayingTVGroup =
+ (bStarted && bIsPlayingTV) ? state->GetActiveChannelGroup(false)->GroupName() : "";
+ const std::string strPlayingRadioGroup =
+ (bStarted && bIsPlayingRadio) ? state->GetActiveChannelGroup(true)->GroupName() : "";
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strPlayingClientName = strPlayingClientName;
+ m_bHasTVRecordings = bHasTVRecordings;
+ m_bHasRadioRecordings = bHasRadioRecordings;
+ m_bIsPlayingTV = bIsPlayingTV;
+ m_bIsPlayingRadio = bIsPlayingRadio;
+ m_bIsPlayingRecording = bIsPlayingRecording;
+ m_bIsPlayingEpgTag = bIsPlayingEpgTag;
+ m_bIsPlayingEncryptedStream = bIsPlayingEncryptedStream;
+ m_bHasTVChannels = bHasTVChannels;
+ m_bHasRadioChannels = bHasRadioChannels;
+ m_strPlayingTVGroup = strPlayingTVGroup;
+ m_strPlayingRadioGroup = strPlayingRadioGroup;
+ m_bCanRecordPlayingChannel = bCanRecordPlayingChannel;
+ m_bIsRecordingPlayingChannel = bIsRecordingPlayingChannel;
+ m_bIsPlayingActiveRecording = bIsPlayingActiveRecording;
+}
+
+void CPVRGUIInfo::UpdateTimeshiftData()
+{
+ m_timesInfo.Update();
+}
+
+bool CPVRGUIInfo::InitCurrentItem(CFileItem* item)
+{
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::CurrentItem);
+ return false;
+}
+
+bool CPVRGUIInfo::GetLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const CGUIInfo& info,
+ std::string* fallback) const
+{
+ return GetListItemAndPlayerLabel(item, info, value) || GetPVRLabel(item, info, value) ||
+ GetRadioRDSLabel(item, info, value);
+}
+
+namespace
+{
+std::string GetAsLocalizedDateString(const CDateTime& datetime, bool bLongDate)
+{
+ return datetime.IsValid() ? datetime.GetAsLocalizedDate(bLongDate) : "";
+}
+
+std::string GetAsLocalizedTimeString(const CDateTime& datetime)
+{
+ return datetime.IsValid() ? datetime.GetAsLocalizedTime("", false) : "";
+}
+
+std::string GetAsLocalizedDateTimeString(const CDateTime& datetime)
+{
+ return datetime.IsValid() ? datetime.GetAsLocalizedDateTime(false, false) : "";
+}
+
+std::string GetEpgTagTitle(const std::shared_ptr<CPVREpgInfoTag>& epgTag)
+{
+ if (epgTag)
+ {
+ if (CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ return g_localizeStrings.Get(19266); // Parental locked
+ else if (!epgTag->Title().empty())
+ return epgTag->Title();
+ }
+
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_EPG_HIDENOINFOAVAILABLE))
+ return g_localizeStrings.Get(19055); // no information available
+
+ return {};
+}
+
+} // unnamed namespace
+
+bool CPVRGUIInfo::GetListItemAndPlayerLabel(const CFileItem* item,
+ const CGUIInfo& info,
+ std::string& strValue) const
+{
+ const std::shared_ptr<CPVRTimerInfoTag> timer = item->GetPVRTimerInfoTag();
+ if (timer)
+ {
+ switch (info.m_info)
+ {
+ case LISTITEM_DATE:
+ strValue = timer->Summary();
+ return true;
+ case LISTITEM_STARTDATE:
+ strValue = GetAsLocalizedDateString(timer->StartAsLocalTime(), true);
+ return true;
+ case LISTITEM_STARTTIME:
+ strValue = GetAsLocalizedTimeString(timer->StartAsLocalTime());
+ return true;
+ case LISTITEM_ENDDATE:
+ strValue = GetAsLocalizedDateString(timer->EndAsLocalTime(), true);
+ return true;
+ case LISTITEM_ENDTIME:
+ strValue = GetAsLocalizedTimeString(timer->EndAsLocalTime());
+ return true;
+ case LISTITEM_DURATION:
+ if (timer->GetDuration() > 0)
+ {
+ strValue = StringUtils::SecondsToTimeString(timer->GetDuration(),
+ static_cast<TIME_FORMAT>(info.GetData4()));
+ return true;
+ }
+ return false;
+ case LISTITEM_TITLE:
+ strValue = timer->Title();
+ return true;
+ case LISTITEM_COMMENT:
+ strValue =
+ timer->GetStatus(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() ==
+ WINDOW_RADIO_TIMER_RULES);
+ return true;
+ case LISTITEM_TIMERTYPE:
+ strValue = timer->GetTypeAsString();
+ return true;
+ case LISTITEM_CHANNEL_NAME:
+ strValue = timer->ChannelName();
+ return true;
+ case LISTITEM_EPG_EVENT_TITLE:
+ case LISTITEM_EPG_EVENT_ICON:
+ case LISTITEM_GENRE:
+ case LISTITEM_PLOT:
+ case LISTITEM_PLOT_OUTLINE:
+ case LISTITEM_ORIGINALTITLE:
+ case LISTITEM_YEAR:
+ case LISTITEM_SEASON:
+ case LISTITEM_EPISODE:
+ case LISTITEM_EPISODENAME:
+ case LISTITEM_DIRECTOR:
+ case LISTITEM_CHANNEL_NUMBER:
+ case LISTITEM_PREMIERED:
+ break; // obtain value from channel/epg
+ default:
+ return false;
+ }
+ }
+
+ const std::shared_ptr<CPVRRecording> recording(item->GetPVRRecordingInfoTag());
+ if (recording)
+ {
+ // Note: CPVRRecoding is derived from CVideoInfoTag. All base class properties will be handled
+ // by CVideoGUIInfoProvider. Only properties introduced by CPVRRecording need to be handled here.
+ switch (info.m_info)
+ {
+ case LISTITEM_DATE:
+ strValue = GetAsLocalizedDateTimeString(recording->RecordingTimeAsLocalTime());
+ return true;
+ case LISTITEM_STARTDATE:
+ strValue = GetAsLocalizedDateString(recording->RecordingTimeAsLocalTime(), true);
+ return true;
+ case VIDEOPLAYER_STARTTIME:
+ case LISTITEM_STARTTIME:
+ strValue = GetAsLocalizedTimeString(recording->RecordingTimeAsLocalTime());
+ return true;
+ case LISTITEM_ENDDATE:
+ strValue = GetAsLocalizedDateString(recording->EndTimeAsLocalTime(), true);
+ return true;
+ case VIDEOPLAYER_ENDTIME:
+ case LISTITEM_ENDTIME:
+ strValue = GetAsLocalizedTimeString(recording->EndTimeAsLocalTime());
+ return true;
+ case LISTITEM_EXPIRATION_DATE:
+ if (recording->HasExpirationTime())
+ {
+ strValue = GetAsLocalizedDateString(recording->ExpirationTimeAsLocalTime(), false);
+ return true;
+ }
+ break;
+ case LISTITEM_EXPIRATION_TIME:
+ if (recording->HasExpirationTime())
+ {
+ strValue = GetAsLocalizedTimeString(recording->ExpirationTimeAsLocalTime());
+ ;
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_EPISODENAME:
+ case LISTITEM_EPISODENAME:
+ strValue = recording->EpisodeName();
+ // fixup multiline episode name strings (which do not fit in any way in our GUI)
+ StringUtils::Replace(strValue, "\n", ", ");
+ return true;
+ case VIDEOPLAYER_CHANNEL_NAME:
+ case LISTITEM_CHANNEL_NAME:
+ strValue = recording->ChannelName();
+ if (strValue.empty())
+ {
+ if (recording->ProviderName().empty())
+ {
+ const auto& provider = recording->GetProvider();
+ strValue = provider->GetName();
+ }
+ else
+ {
+ strValue = recording->ProviderName();
+ }
+ }
+ return true;
+ case VIDEOPLAYER_CHANNEL_NUMBER:
+ case LISTITEM_CHANNEL_NUMBER:
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(*item);
+ if (groupMember)
+ {
+ strValue = groupMember->ChannelNumber().FormattedChannelNumber();
+ return true;
+ }
+ break;
+ }
+ case LISTITEM_ICON:
+ if (recording->ClientIconPath().empty() && recording->ClientThumbnailPath().empty() &&
+ // Only use a fallback if there is more than a single provider available
+ // Note: an add-on itself is a provider, hence we don't use > 0
+ CServiceBroker::GetPVRManager().Providers()->GetNumProviders() > 1 &&
+ !recording->Channel())
+ {
+ auto provider = recording->GetProvider();
+ if (!provider->GetIconPath().empty())
+ {
+ strValue = provider->GetIconPath();
+ return true;
+ }
+ }
+ return false;
+ case VIDEOPLAYER_CHANNEL_GROUP:
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strValue = recording->IsRadio() ? m_strPlayingRadioGroup : m_strPlayingTVGroup;
+ return true;
+ }
+ case VIDEOPLAYER_PREMIERED:
+ case LISTITEM_PREMIERED:
+ if (recording->FirstAired().IsValid())
+ {
+ strValue = recording->FirstAired().GetAsLocalizedDate();
+ return true;
+ }
+ else if (recording->HasYear())
+ {
+ strValue = std::to_string(recording->GetYear());
+ return true;
+ }
+ return false;
+ case LISTITEM_SIZE:
+ if (recording->GetSizeInBytes() > 0)
+ {
+ strValue = StringUtils::SizeToString(recording->GetSizeInBytes());
+ return true;
+ }
+ return false;
+ }
+ return false;
+ }
+
+ const std::shared_ptr<CPVREpgSearchFilter> filter = item->GetEPGSearchFilter();
+ if (filter)
+ {
+ switch (info.m_info)
+ {
+ case LISTITEM_DATE:
+ {
+ CDateTime lastExecLocal;
+ lastExecLocal.SetFromUTCDateTime(filter->GetLastExecutedDateTime());
+ strValue = GetAsLocalizedDateTimeString(lastExecLocal);
+ if (strValue.empty())
+ strValue = g_localizeStrings.Get(10006); // "N/A"
+ return true;
+ }
+ }
+ return false;
+ }
+
+ std::shared_ptr<CPVREpgInfoTag> epgTag;
+ std::shared_ptr<CPVRChannel> channel;
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ CPVRItem pvrItem(item);
+ channel = pvrItem.GetChannel();
+
+ switch (info.m_info)
+ {
+ case VIDEOPLAYER_NEXT_TITLE:
+ case VIDEOPLAYER_NEXT_GENRE:
+ case VIDEOPLAYER_NEXT_PLOT:
+ case VIDEOPLAYER_NEXT_PLOT_OUTLINE:
+ case VIDEOPLAYER_NEXT_STARTTIME:
+ case VIDEOPLAYER_NEXT_ENDTIME:
+ case VIDEOPLAYER_NEXT_DURATION:
+ case LISTITEM_NEXT_TITLE:
+ case LISTITEM_NEXT_GENRE:
+ case LISTITEM_NEXT_PLOT:
+ case LISTITEM_NEXT_PLOT_OUTLINE:
+ case LISTITEM_NEXT_STARTDATE:
+ case LISTITEM_NEXT_STARTTIME:
+ case LISTITEM_NEXT_ENDDATE:
+ case LISTITEM_NEXT_ENDTIME:
+ case LISTITEM_NEXT_DURATION:
+ // next playing event
+ epgTag = pvrItem.GetNextEpgInfoTag();
+ break;
+ default:
+ // now playing event
+ epgTag = pvrItem.GetEpgInfoTag();
+ break;
+ }
+
+ switch (info.m_info)
+ {
+ // special handling for channels without epg or with radio rds data
+ case PLAYER_TITLE:
+ case LISTITEM_TITLE:
+ case VIDEOPLAYER_NEXT_TITLE:
+ case LISTITEM_NEXT_TITLE:
+ case LISTITEM_EPG_EVENT_TITLE:
+ // Note: in difference to LISTITEM_TITLE, LISTITEM_EPG_EVENT_TITLE returns the title
+ // associated with the epg event of a timer, if any, and not the title of the timer.
+ strValue = GetEpgTagTitle(epgTag);
+ return true;
+ }
+ }
+
+ if (epgTag)
+ {
+ switch (info.m_info)
+ {
+ case VIDEOPLAYER_GENRE:
+ case LISTITEM_GENRE:
+ case VIDEOPLAYER_NEXT_GENRE:
+ case LISTITEM_NEXT_GENRE:
+ strValue = epgTag->GetGenresLabel();
+ return true;
+ case VIDEOPLAYER_PLOT:
+ case LISTITEM_PLOT:
+ case VIDEOPLAYER_NEXT_PLOT:
+ case LISTITEM_NEXT_PLOT:
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ strValue = epgTag->Plot();
+ return true;
+ case VIDEOPLAYER_PLOT_OUTLINE:
+ case LISTITEM_PLOT_OUTLINE:
+ case VIDEOPLAYER_NEXT_PLOT_OUTLINE:
+ case LISTITEM_NEXT_PLOT_OUTLINE:
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ strValue = epgTag->PlotOutline();
+ return true;
+ case LISTITEM_DATE:
+ strValue = GetAsLocalizedDateTimeString(epgTag->StartAsLocalTime());
+ return true;
+ case LISTITEM_STARTDATE:
+ case LISTITEM_NEXT_STARTDATE:
+ strValue = GetAsLocalizedDateString(epgTag->StartAsLocalTime(), true);
+ return true;
+ case VIDEOPLAYER_STARTTIME:
+ case VIDEOPLAYER_NEXT_STARTTIME:
+ case LISTITEM_STARTTIME:
+ case LISTITEM_NEXT_STARTTIME:
+ strValue = GetAsLocalizedTimeString(epgTag->StartAsLocalTime());
+ return true;
+ case LISTITEM_ENDDATE:
+ case LISTITEM_NEXT_ENDDATE:
+ strValue = GetAsLocalizedDateString(epgTag->EndAsLocalTime(), true);
+ return true;
+ case VIDEOPLAYER_ENDTIME:
+ case VIDEOPLAYER_NEXT_ENDTIME:
+ case LISTITEM_ENDTIME:
+ case LISTITEM_NEXT_ENDTIME:
+ strValue = GetAsLocalizedTimeString(epgTag->EndAsLocalTime());
+ return true;
+ // note: for some reason, there is no VIDEOPLAYER_DURATION
+ case LISTITEM_DURATION:
+ case VIDEOPLAYER_NEXT_DURATION:
+ case LISTITEM_NEXT_DURATION:
+ if (epgTag->GetDuration() > 0)
+ {
+ strValue = StringUtils::SecondsToTimeString(epgTag->GetDuration(),
+ static_cast<TIME_FORMAT>(info.GetData4()));
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_IMDBNUMBER:
+ case LISTITEM_IMDBNUMBER:
+ strValue = epgTag->IMDBNumber();
+ return true;
+ case VIDEOPLAYER_ORIGINALTITLE:
+ case LISTITEM_ORIGINALTITLE:
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ strValue = epgTag->OriginalTitle();
+ return true;
+ case VIDEOPLAYER_YEAR:
+ case LISTITEM_YEAR:
+ if (epgTag->Year() > 0)
+ {
+ strValue = std::to_string(epgTag->Year());
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_SEASON:
+ case LISTITEM_SEASON:
+ if (epgTag->SeriesNumber() >= 0)
+ {
+ strValue = std::to_string(epgTag->SeriesNumber());
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_EPISODE:
+ case LISTITEM_EPISODE:
+ if (epgTag->EpisodeNumber() >= 0)
+ {
+ strValue = std::to_string(epgTag->EpisodeNumber());
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_EPISODENAME:
+ case LISTITEM_EPISODENAME:
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ {
+ strValue = epgTag->EpisodeName();
+ // fixup multiline episode name strings (which do not fit in any way in our GUI)
+ StringUtils::Replace(strValue, "\n", ", ");
+ }
+ return true;
+ case VIDEOPLAYER_CAST:
+ case LISTITEM_CAST:
+ strValue = epgTag->GetCastLabel();
+ return true;
+ case VIDEOPLAYER_DIRECTOR:
+ case LISTITEM_DIRECTOR:
+ strValue = epgTag->GetDirectorsLabel();
+ return true;
+ case VIDEOPLAYER_WRITER:
+ case LISTITEM_WRITER:
+ strValue = epgTag->GetWritersLabel();
+ return true;
+ case LISTITEM_EPG_EVENT_ICON:
+ strValue = epgTag->IconPath();
+ return true;
+ case VIDEOPLAYER_PARENTAL_RATING:
+ case LISTITEM_PARENTAL_RATING:
+ if (epgTag->ParentalRating() > 0)
+ {
+ strValue = std::to_string(epgTag->ParentalRating());
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_PREMIERED:
+ case LISTITEM_PREMIERED:
+ if (epgTag->FirstAired().IsValid())
+ {
+ strValue = epgTag->FirstAired().GetAsLocalizedDate();
+ return true;
+ }
+ else if (epgTag->Year() > 0)
+ {
+ strValue = std::to_string(epgTag->Year());
+ return true;
+ }
+ return false;
+ case VIDEOPLAYER_RATING:
+ case LISTITEM_RATING:
+ {
+ int iStarRating = epgTag->StarRating();
+ if (iStarRating > 0)
+ {
+ strValue = StringUtils::FormatNumber(iStarRating);
+ return true;
+ }
+ return false;
+ }
+ }
+ }
+
+ if (channel)
+ {
+ switch (info.m_info)
+ {
+ case MUSICPLAYER_CHANNEL_NAME:
+ {
+ const std::shared_ptr<CPVRRadioRDSInfoTag> rdsTag = channel->GetRadioRDSInfoTag();
+ if (rdsTag)
+ {
+ strValue = rdsTag->GetProgStation();
+ if (!strValue.empty())
+ return true;
+ }
+ // fall-thru is intended
+ [[fallthrough]];
+ }
+ case VIDEOPLAYER_CHANNEL_NAME:
+ case LISTITEM_CHANNEL_NAME:
+ strValue = channel->ChannelName();
+ return true;
+ case MUSICPLAYER_CHANNEL_NUMBER:
+ case VIDEOPLAYER_CHANNEL_NUMBER:
+ case LISTITEM_CHANNEL_NUMBER:
+ {
+ auto groupMember = item->GetPVRChannelGroupMemberInfoTag();
+ if (!groupMember)
+ groupMember =
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(
+ *item);
+ if (groupMember)
+ {
+ strValue = groupMember->ChannelNumber().FormattedChannelNumber();
+ return true;
+ }
+ break;
+ }
+ case MUSICPLAYER_CHANNEL_GROUP:
+ case VIDEOPLAYER_CHANNEL_GROUP:
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ strValue = channel->IsRadio() ? m_strPlayingRadioGroup : m_strPlayingTVGroup;
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool CPVRGUIInfo::GetPVRLabel(const CFileItem* item,
+ const CGUIInfo& info,
+ std::string& strValue) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ switch (info.m_info)
+ {
+ case PVR_EPG_EVENT_ICON:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ if (epgTag)
+ {
+ strValue = epgTag->IconPath();
+ }
+ return true;
+ }
+ case PVR_EPG_EVENT_DURATION:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ strValue = m_timesInfo.GetEpgEventDuration(epgTag, static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PVR_EPG_EVENT_ELAPSED_TIME:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ strValue =
+ m_timesInfo.GetEpgEventElapsedTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PVR_EPG_EVENT_REMAINING_TIME:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ strValue =
+ m_timesInfo.GetEpgEventRemainingTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PVR_EPG_EVENT_FINISH_TIME:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ strValue =
+ m_timesInfo.GetEpgEventFinishTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PVR_TIMESHIFT_START_TIME:
+ strValue = m_timesInfo.GetTimeshiftStartTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_END_TIME:
+ strValue = m_timesInfo.GetTimeshiftEndTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_PLAY_TIME:
+ strValue = m_timesInfo.GetTimeshiftPlayTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_OFFSET:
+ strValue = m_timesInfo.GetTimeshiftOffset(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_DURATION:
+ strValue =
+ m_timesInfo.GetTimeshiftProgressDuration(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_START_TIME:
+ strValue =
+ m_timesInfo.GetTimeshiftProgressStartTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_END_TIME:
+ strValue = m_timesInfo.GetTimeshiftProgressEndTime(static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ case PVR_EPG_EVENT_SEEK_TIME:
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ strValue = m_timesInfo.GetEpgEventSeekTime(appPlayer->GetSeekHandler().GetSeekSize(),
+ static_cast<TIME_FORMAT>(info.GetData1()));
+ return true;
+ }
+ case PVR_NOW_RECORDING_TITLE:
+ strValue = m_anyTimersInfo.GetActiveTimerTitle();
+ return true;
+ case PVR_NOW_RECORDING_CHANNEL:
+ strValue = m_anyTimersInfo.GetActiveTimerChannelName();
+ return true;
+ case PVR_NOW_RECORDING_CHAN_ICO:
+ strValue = m_anyTimersInfo.GetActiveTimerChannelIcon();
+ return true;
+ case PVR_NOW_RECORDING_DATETIME:
+ strValue = m_anyTimersInfo.GetActiveTimerDateTime();
+ return true;
+ case PVR_NEXT_RECORDING_TITLE:
+ strValue = m_anyTimersInfo.GetNextTimerTitle();
+ return true;
+ case PVR_NEXT_RECORDING_CHANNEL:
+ strValue = m_anyTimersInfo.GetNextTimerChannelName();
+ return true;
+ case PVR_NEXT_RECORDING_CHAN_ICO:
+ strValue = m_anyTimersInfo.GetNextTimerChannelIcon();
+ return true;
+ case PVR_NEXT_RECORDING_DATETIME:
+ strValue = m_anyTimersInfo.GetNextTimerDateTime();
+ return true;
+ case PVR_TV_NOW_RECORDING_TITLE:
+ strValue = m_tvTimersInfo.GetActiveTimerTitle();
+ return true;
+ case PVR_TV_NOW_RECORDING_CHANNEL:
+ strValue = m_tvTimersInfo.GetActiveTimerChannelName();
+ return true;
+ case PVR_TV_NOW_RECORDING_CHAN_ICO:
+ strValue = m_tvTimersInfo.GetActiveTimerChannelIcon();
+ return true;
+ case PVR_TV_NOW_RECORDING_DATETIME:
+ strValue = m_tvTimersInfo.GetActiveTimerDateTime();
+ return true;
+ case PVR_TV_NEXT_RECORDING_TITLE:
+ strValue = m_tvTimersInfo.GetNextTimerTitle();
+ return true;
+ case PVR_TV_NEXT_RECORDING_CHANNEL:
+ strValue = m_tvTimersInfo.GetNextTimerChannelName();
+ return true;
+ case PVR_TV_NEXT_RECORDING_CHAN_ICO:
+ strValue = m_tvTimersInfo.GetNextTimerChannelIcon();
+ return true;
+ case PVR_TV_NEXT_RECORDING_DATETIME:
+ strValue = m_tvTimersInfo.GetNextTimerDateTime();
+ return true;
+ case PVR_RADIO_NOW_RECORDING_TITLE:
+ strValue = m_radioTimersInfo.GetActiveTimerTitle();
+ return true;
+ case PVR_RADIO_NOW_RECORDING_CHANNEL:
+ strValue = m_radioTimersInfo.GetActiveTimerChannelName();
+ return true;
+ case PVR_RADIO_NOW_RECORDING_CHAN_ICO:
+ strValue = m_radioTimersInfo.GetActiveTimerChannelIcon();
+ return true;
+ case PVR_RADIO_NOW_RECORDING_DATETIME:
+ strValue = m_radioTimersInfo.GetActiveTimerDateTime();
+ return true;
+ case PVR_RADIO_NEXT_RECORDING_TITLE:
+ strValue = m_radioTimersInfo.GetNextTimerTitle();
+ return true;
+ case PVR_RADIO_NEXT_RECORDING_CHANNEL:
+ strValue = m_radioTimersInfo.GetNextTimerChannelName();
+ return true;
+ case PVR_RADIO_NEXT_RECORDING_CHAN_ICO:
+ strValue = m_radioTimersInfo.GetNextTimerChannelIcon();
+ return true;
+ case PVR_RADIO_NEXT_RECORDING_DATETIME:
+ strValue = m_radioTimersInfo.GetNextTimerDateTime();
+ return true;
+ case PVR_NEXT_TIMER:
+ strValue = m_anyTimersInfo.GetNextTimer();
+ return true;
+ case PVR_ACTUAL_STREAM_SIG:
+ CharInfoSignal(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_SNR:
+ CharInfoSNR(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_BER:
+ CharInfoBER(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_UNC:
+ CharInfoUNC(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_CLIENT:
+ CharInfoPlayingClientName(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_DEVICE:
+ CharInfoFrontendName(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_STATUS:
+ CharInfoFrontendStatus(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_CRYPTION:
+ CharInfoEncryption(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_SERVICE:
+ CharInfoService(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_MUX:
+ CharInfoMux(strValue);
+ return true;
+ case PVR_ACTUAL_STREAM_PROVIDER:
+ CharInfoProvider(strValue);
+ return true;
+ case PVR_BACKEND_NAME:
+ CharInfoBackendName(strValue);
+ return true;
+ case PVR_BACKEND_VERSION:
+ CharInfoBackendVersion(strValue);
+ return true;
+ case PVR_BACKEND_HOST:
+ CharInfoBackendHost(strValue);
+ return true;
+ case PVR_BACKEND_DISKSPACE:
+ CharInfoBackendDiskspace(strValue);
+ return true;
+ case PVR_BACKEND_PROVIDERS:
+ CharInfoBackendProviders(strValue);
+ return true;
+ case PVR_BACKEND_CHANNEL_GROUPS:
+ CharInfoBackendChannelGroups(strValue);
+ return true;
+ case PVR_BACKEND_CHANNELS:
+ CharInfoBackendChannels(strValue);
+ return true;
+ case PVR_BACKEND_TIMERS:
+ CharInfoBackendTimers(strValue);
+ return true;
+ case PVR_BACKEND_RECORDINGS:
+ CharInfoBackendRecordings(strValue);
+ return true;
+ case PVR_BACKEND_DELETED_RECORDINGS:
+ CharInfoBackendDeletedRecordings(strValue);
+ return true;
+ case PVR_BACKEND_NUMBER:
+ CharInfoBackendNumber(strValue);
+ return true;
+ case PVR_TOTAL_DISKSPACE:
+ CharInfoTotalDiskSpace(strValue);
+ return true;
+ case PVR_CHANNEL_NUMBER_INPUT:
+ strValue = m_channelNumberInput;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRGUIInfo::GetRadioRDSLabel(const CFileItem* item,
+ const CGUIInfo& info,
+ std::string& strValue) const
+{
+ if (!item->HasPVRChannelInfoTag())
+ return false;
+
+ const std::shared_ptr<CPVRRadioRDSInfoTag> tag =
+ item->GetPVRChannelInfoTag()->GetRadioRDSInfoTag();
+ if (tag)
+ {
+ switch (info.m_info)
+ {
+ case RDS_CHANNEL_COUNTRY:
+ strValue = tag->GetCountry();
+ return true;
+ case RDS_TITLE:
+ strValue = tag->GetTitle();
+ return true;
+ case RDS_ARTIST:
+ strValue = tag->GetArtist();
+ return true;
+ case RDS_BAND:
+ strValue = tag->GetBand();
+ return true;
+ case RDS_COMPOSER:
+ strValue = tag->GetComposer();
+ return true;
+ case RDS_CONDUCTOR:
+ strValue = tag->GetConductor();
+ return true;
+ case RDS_ALBUM:
+ strValue = tag->GetAlbum();
+ return true;
+ case RDS_ALBUM_TRACKNUMBER:
+ if (tag->GetAlbumTrackNumber() > 0)
+ {
+ strValue = std::to_string(tag->GetAlbumTrackNumber());
+ return true;
+ }
+ break;
+ case RDS_GET_RADIO_STYLE:
+ strValue = tag->GetRadioStyle();
+ return true;
+ case RDS_COMMENT:
+ strValue = tag->GetComment();
+ return true;
+ case RDS_INFO_NEWS:
+ strValue = tag->GetInfoNews();
+ return true;
+ case RDS_INFO_NEWS_LOCAL:
+ strValue = tag->GetInfoNewsLocal();
+ return true;
+ case RDS_INFO_STOCK:
+ strValue = tag->GetInfoStock();
+ return true;
+ case RDS_INFO_STOCK_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoStock().size()));
+ return true;
+ case RDS_INFO_SPORT:
+ strValue = tag->GetInfoSport();
+ return true;
+ case RDS_INFO_SPORT_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoSport().size()));
+ return true;
+ case RDS_INFO_LOTTERY:
+ strValue = tag->GetInfoLottery();
+ return true;
+ case RDS_INFO_LOTTERY_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoLottery().size()));
+ return true;
+ case RDS_INFO_WEATHER:
+ strValue = tag->GetInfoWeather();
+ return true;
+ case RDS_INFO_WEATHER_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoWeather().size()));
+ return true;
+ case RDS_INFO_HOROSCOPE:
+ strValue = tag->GetInfoHoroscope();
+ return true;
+ case RDS_INFO_HOROSCOPE_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoHoroscope().size()));
+ return true;
+ case RDS_INFO_CINEMA:
+ strValue = tag->GetInfoCinema();
+ return true;
+ case RDS_INFO_CINEMA_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoCinema().size()));
+ return true;
+ case RDS_INFO_OTHER:
+ strValue = tag->GetInfoOther();
+ return true;
+ case RDS_INFO_OTHER_SIZE:
+ strValue = std::to_string(static_cast<int>(tag->GetInfoOther().size()));
+ return true;
+ case RDS_PROG_HOST:
+ strValue = tag->GetProgHost();
+ return true;
+ case RDS_PROG_EDIT_STAFF:
+ strValue = tag->GetEditorialStaff();
+ return true;
+ case RDS_PROG_HOMEPAGE:
+ strValue = tag->GetProgWebsite();
+ return true;
+ case RDS_PROG_STYLE:
+ strValue = tag->GetProgStyle();
+ return true;
+ case RDS_PHONE_HOTLINE:
+ strValue = tag->GetPhoneHotline();
+ return true;
+ case RDS_PHONE_STUDIO:
+ strValue = tag->GetPhoneStudio();
+ return true;
+ case RDS_SMS_STUDIO:
+ strValue = tag->GetSMSStudio();
+ return true;
+ case RDS_EMAIL_HOTLINE:
+ strValue = tag->GetEMailHotline();
+ return true;
+ case RDS_EMAIL_STUDIO:
+ strValue = tag->GetEMailStudio();
+ return true;
+ case RDS_PROG_STATION:
+ strValue = tag->GetProgStation();
+ return true;
+ case RDS_PROG_NOW:
+ strValue = tag->GetProgNow();
+ return true;
+ case RDS_PROG_NEXT:
+ strValue = tag->GetProgNext();
+ return true;
+ case RDS_AUDIO_LANG:
+ strValue = tag->GetLanguage();
+ return true;
+ case RDS_GET_RADIOTEXT_LINE:
+ strValue = tag->GetRadioText(info.GetData1());
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetFallbackLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const CGUIInfo& info,
+ std::string* fallback)
+{
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ switch (info.m_info)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // VIDEOPLAYER_*, MUSICPLAYER_*
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ case VIDEOPLAYER_TITLE:
+ case MUSICPLAYER_TITLE:
+ value = GetEpgTagTitle(CPVRItem(item).GetEpgInfoTag());
+ return !value.empty();
+ default:
+ break;
+ }
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetInt(int& value,
+ const CGUIListItem* item,
+ int contextWindow,
+ const CGUIInfo& info) const
+{
+ if (!item->IsFileItem())
+ return false;
+
+ const CFileItem* fitem = static_cast<const CFileItem*>(item);
+ return GetListItemAndPlayerInt(fitem, info, value) || GetPVRInt(fitem, info, value);
+}
+
+bool CPVRGUIInfo::GetListItemAndPlayerInt(const CFileItem* item,
+ const CGUIInfo& info,
+ int& iValue) const
+{
+ switch (info.m_info)
+ {
+ case LISTITEM_PROGRESS:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ if (epgTag)
+ iValue = static_cast<int>(epgTag->ProgressPercentage());
+ }
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetPVRInt(const CFileItem* item, const CGUIInfo& info, int& iValue) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ switch (info.m_info)
+ {
+ case PVR_EPG_EVENT_DURATION:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ iValue = m_timesInfo.GetEpgEventDuration(epgTag);
+ return true;
+ }
+ case PVR_EPG_EVENT_PROGRESS:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr;
+ iValue = m_timesInfo.GetEpgEventProgress(epgTag);
+ return true;
+ }
+ case PVR_TIMESHIFT_PROGRESS:
+ iValue = m_timesInfo.GetTimeshiftProgress();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_DURATION:
+ iValue = m_timesInfo.GetTimeshiftProgressDuration();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_PLAY_POS:
+ iValue = m_timesInfo.GetTimeshiftProgressPlayPosition();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_EPG_START:
+ iValue = m_timesInfo.GetTimeshiftProgressEpgStart();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_EPG_END:
+ iValue = m_timesInfo.GetTimeshiftProgressEpgEnd();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_BUFFER_START:
+ iValue = m_timesInfo.GetTimeshiftProgressBufferStart();
+ return true;
+ case PVR_TIMESHIFT_PROGRESS_BUFFER_END:
+ iValue = m_timesInfo.GetTimeshiftProgressBufferEnd();
+ return true;
+ case PVR_TIMESHIFT_SEEKBAR:
+ iValue = GetTimeShiftSeekPercent();
+ return true;
+ case PVR_ACTUAL_STREAM_SIG_PROGR:
+ iValue = std::lrintf(static_cast<float>(m_qualityInfo.iSignal) / 0xFFFF * 100);
+ return true;
+ case PVR_ACTUAL_STREAM_SNR_PROGR:
+ iValue = std::lrintf(static_cast<float>(m_qualityInfo.iSNR) / 0xFFFF * 100);
+ return true;
+ case PVR_BACKEND_DISKSPACE_PROGR:
+ if (m_iBackendDiskTotal > 0)
+ iValue = std::lrintf(static_cast<float>(m_iBackendDiskUsed) / m_iBackendDiskTotal * 100);
+ else
+ iValue = 0xFF;
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetBool(bool& value,
+ const CGUIListItem* item,
+ int contextWindow,
+ const CGUIInfo& info) const
+{
+ if (!item->IsFileItem())
+ return false;
+
+ const CFileItem* fitem = static_cast<const CFileItem*>(item);
+ return GetListItemAndPlayerBool(fitem, info, value) || GetPVRBool(fitem, info, value) ||
+ GetRadioRDSBool(fitem, info, value);
+}
+
+bool CPVRGUIInfo::GetListItemAndPlayerBool(const CFileItem* item,
+ const CGUIInfo& info,
+ bool& bValue) const
+{
+ switch (info.m_info)
+ {
+ case LISTITEM_HASARCHIVE:
+ if (item->IsPVRChannel())
+ {
+ bValue = item->GetPVRChannelInfoTag()->HasArchive();
+ return true;
+ }
+ break;
+ case LISTITEM_ISPLAYABLE:
+ if (item->IsEPG())
+ {
+ bValue = item->GetEPGInfoTag()->IsPlayable();
+ return true;
+ }
+ break;
+ case LISTITEM_ISRECORDING:
+ if (item->IsPVRChannel())
+ {
+ bValue = CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(
+ *item->GetPVRChannelInfoTag());
+ return true;
+ }
+ else if (item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->IsRecording();
+ return true;
+ }
+ else if (item->IsPVRRecording())
+ {
+ bValue = item->GetPVRRecordingInfoTag()->IsInProgress();
+ return true;
+ }
+ break;
+ case LISTITEM_INPROGRESS:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ if (epgTag)
+ bValue = epgTag->IsActive();
+ return true;
+ }
+ break;
+ case LISTITEM_HASTIMER:
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = true;
+ return true;
+ }
+ break;
+ case LISTITEM_HASTIMERSCHEDULE:
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->HasParent();
+ return true;
+ }
+ break;
+ case LISTITEM_HASREMINDER:
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->IsReminder();
+ return true;
+ }
+ break;
+ case LISTITEM_HASREMINDERRULE:
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->IsReminder() && timer->HasParent();
+ return true;
+ }
+ break;
+ case LISTITEM_TIMERISACTIVE:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->IsActive();
+ break;
+ }
+ break;
+ case LISTITEM_TIMERHASCONFLICT:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = timer->HasConflict();
+ return true;
+ }
+ break;
+ case LISTITEM_TIMERHASERROR:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag();
+ if (timer)
+ bValue = (timer->IsBroken() && !timer->HasConflict());
+ return true;
+ }
+ break;
+ case LISTITEM_HASRECORDING:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ if (epgTag)
+ bValue = !!CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(epgTag);
+ return true;
+ }
+ break;
+ case LISTITEM_HAS_EPG:
+ if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag();
+ bValue = (epgTag != nullptr);
+ return true;
+ }
+ break;
+ case LISTITEM_ISENCRYPTED:
+ if (item->IsPVRChannel() || item->IsEPG())
+ {
+ const std::shared_ptr<CPVRChannel> channel = CPVRItem(item).GetChannel();
+ if (channel)
+ bValue = channel->IsEncrypted();
+ return true;
+ }
+ break;
+ case LISTITEM_IS_NEW:
+ if (item->IsEPG())
+ {
+ if (item->GetEPGInfoTag())
+ {
+ bValue = item->GetEPGInfoTag()->IsNew();
+ return true;
+ }
+ }
+ else if (item->IsPVRRecording())
+ {
+ bValue = item->GetPVRRecordingInfoTag()->IsNew();
+ return true;
+ }
+ else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag())
+ {
+ bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsNew();
+ return true;
+ }
+ else if (item->IsPVRChannel())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow();
+ bValue = epgNow ? epgNow->IsNew() : false;
+ return true;
+ }
+ break;
+ case LISTITEM_IS_PREMIERE:
+ if (item->IsEPG())
+ {
+ bValue = item->GetEPGInfoTag()->IsPremiere();
+ return true;
+ }
+ else if (item->IsPVRRecording())
+ {
+ bValue = item->GetPVRRecordingInfoTag()->IsPremiere();
+ return true;
+ }
+ else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag())
+ {
+ bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsPremiere();
+ return true;
+ }
+ else if (item->IsPVRChannel())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow();
+ bValue = epgNow ? epgNow->IsPremiere() : false;
+ return true;
+ }
+ break;
+ case LISTITEM_IS_FINALE:
+ if (item->IsEPG())
+ {
+ bValue = item->GetEPGInfoTag()->IsFinale();
+ return true;
+ }
+ else if (item->IsPVRRecording())
+ {
+ bValue = item->GetPVRRecordingInfoTag()->IsFinale();
+ return true;
+ }
+ else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag())
+ {
+ bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsFinale();
+ return true;
+ }
+ else if (item->IsPVRChannel())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow();
+ bValue = epgNow ? epgNow->IsFinale() : false;
+ return true;
+ }
+ break;
+ case LISTITEM_IS_LIVE:
+ if (item->IsEPG())
+ {
+ bValue = item->GetEPGInfoTag()->IsLive();
+ return true;
+ }
+ else if (item->IsPVRRecording())
+ {
+ bValue = item->GetPVRRecordingInfoTag()->IsLive();
+ return true;
+ }
+ else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag())
+ {
+ bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsLive();
+ return true;
+ }
+ else if (item->IsPVRChannel())
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow();
+ bValue = epgNow ? epgNow->IsLive() : false;
+ return true;
+ }
+ break;
+ case MUSICPLAYER_CONTENT:
+ case VIDEOPLAYER_CONTENT:
+ if (item->IsPVRChannel())
+ {
+ bValue = StringUtils::EqualsNoCase(info.GetData3(), "livetv");
+ return bValue; // if no match for this provider, other providers shall be asked.
+ }
+ break;
+ case VIDEOPLAYER_HAS_INFO:
+ if (item->IsPVRChannel())
+ {
+ bValue = !item->GetPVRChannelInfoTag()->ChannelName().empty();
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_HAS_EPG:
+ if (item->IsPVRChannel())
+ {
+ bValue = (item->GetPVRChannelInfoTag()->GetEPGNow() != nullptr);
+ return true;
+ }
+ break;
+ case VIDEOPLAYER_CAN_RESUME_LIVE_TV:
+ if (item->IsPVRRecording())
+ {
+ const std::shared_ptr<CPVRRecording> recording = item->GetPVRRecordingInfoTag();
+ const std::shared_ptr<CPVREpg> epg =
+ recording->Channel() ? recording->Channel()->GetEPG() : nullptr;
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ CServiceBroker::GetPVRManager().EpgContainer().GetTagById(epg,
+ recording->BroadcastUid());
+ bValue = (epgTag && epgTag->IsActive());
+ return true;
+ }
+ break;
+ case PLAYER_IS_CHANNEL_PREVIEW_ACTIVE:
+ if (item->IsPVRChannel())
+ {
+ if (m_previewAndPlayerShowInfo)
+ {
+ bValue = true;
+ }
+ else
+ {
+ bValue = !m_videoInfo.valid;
+ if (bValue && item->GetPVRChannelInfoTag()->IsRadio())
+ bValue = !m_audioInfo.valid;
+ }
+ return true;
+ }
+ break;
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetPVRBool(const CFileItem* item, const CGUIInfo& info, bool& bValue) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ switch (info.m_info)
+ {
+ case PVR_IS_RECORDING:
+ bValue = m_anyTimersInfo.HasRecordingTimers();
+ return true;
+ case PVR_IS_RECORDING_TV:
+ bValue = m_tvTimersInfo.HasRecordingTimers();
+ return true;
+ case PVR_IS_RECORDING_RADIO:
+ bValue = m_radioTimersInfo.HasRecordingTimers();
+ return true;
+ case PVR_HAS_TIMER:
+ bValue = m_anyTimersInfo.HasTimers();
+ return true;
+ case PVR_HAS_TV_TIMER:
+ bValue = m_tvTimersInfo.HasTimers();
+ return true;
+ case PVR_HAS_RADIO_TIMER:
+ bValue = m_radioTimersInfo.HasTimers();
+ return true;
+ case PVR_HAS_TV_CHANNELS:
+ bValue = m_bHasTVChannels;
+ return true;
+ case PVR_HAS_RADIO_CHANNELS:
+ bValue = m_bHasRadioChannels;
+ return true;
+ case PVR_HAS_NONRECORDING_TIMER:
+ bValue = m_anyTimersInfo.HasNonRecordingTimers();
+ return true;
+ case PVR_HAS_NONRECORDING_TV_TIMER:
+ bValue = m_tvTimersInfo.HasNonRecordingTimers();
+ return true;
+ case PVR_HAS_NONRECORDING_RADIO_TIMER:
+ bValue = m_radioTimersInfo.HasNonRecordingTimers();
+ return true;
+ case PVR_IS_PLAYING_TV:
+ bValue = m_bIsPlayingTV;
+ return true;
+ case PVR_IS_PLAYING_RADIO:
+ bValue = m_bIsPlayingRadio;
+ return true;
+ case PVR_IS_PLAYING_RECORDING:
+ bValue = m_bIsPlayingRecording;
+ return true;
+ case PVR_IS_PLAYING_EPGTAG:
+ bValue = m_bIsPlayingEpgTag;
+ return true;
+ case PVR_ACTUAL_STREAM_ENCRYPTED:
+ bValue = m_bIsPlayingEncryptedStream;
+ return true;
+ case PVR_IS_TIMESHIFTING:
+ bValue = m_timesInfo.IsTimeshifting();
+ return true;
+ case PVR_CAN_RECORD_PLAYING_CHANNEL:
+ bValue = m_bCanRecordPlayingChannel;
+ return true;
+ case PVR_IS_RECORDING_PLAYING_CHANNEL:
+ bValue = m_bIsRecordingPlayingChannel;
+ return true;
+ case PVR_IS_PLAYING_ACTIVE_RECORDING:
+ bValue = m_bIsPlayingActiveRecording;
+ return true;
+ }
+ return false;
+}
+
+bool CPVRGUIInfo::GetRadioRDSBool(const CFileItem* item, const CGUIInfo& info, bool& bValue) const
+{
+ if (!item->HasPVRChannelInfoTag())
+ return false;
+
+ const std::shared_ptr<CPVRRadioRDSInfoTag> tag =
+ item->GetPVRChannelInfoTag()->GetRadioRDSInfoTag();
+ if (tag)
+ {
+ switch (info.m_info)
+ {
+ case RDS_HAS_RADIOTEXT:
+ bValue = tag->IsPlayingRadioText();
+ return true;
+ case RDS_HAS_RADIOTEXT_PLUS:
+ bValue = tag->IsPlayingRadioTextPlus();
+ return true;
+ case RDS_HAS_HOTLINE_DATA:
+ bValue = (!tag->GetEMailHotline().empty() || !tag->GetPhoneHotline().empty());
+ return true;
+ case RDS_HAS_STUDIO_DATA:
+ bValue = (!tag->GetEMailStudio().empty() || !tag->GetSMSStudio().empty() ||
+ !tag->GetPhoneStudio().empty());
+ return true;
+ }
+ }
+
+ switch (info.m_info)
+ {
+ case RDS_HAS_RDS:
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ bValue = appPlayer->IsPlayingRDS();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CPVRGUIInfo::CharInfoBackendNumber(std::string& strValue) const
+{
+ size_t numBackends = m_backendProperties.size();
+
+ if (numBackends > 0)
+ strValue = StringUtils::Format("{0} {1} {2}", m_iCurrentActiveClient + 1,
+ g_localizeStrings.Get(20163), numBackends);
+ else
+ strValue = g_localizeStrings.Get(14023);
+}
+
+void CPVRGUIInfo::CharInfoTotalDiskSpace(std::string& strValue) const
+{
+ strValue = StringUtils::SizeToString(m_iBackendDiskTotal).c_str();
+}
+
+void CPVRGUIInfo::CharInfoSignal(std::string& strValue) const
+{
+ strValue = StringUtils::Format("{} %", m_qualityInfo.iSignal / 655);
+}
+
+void CPVRGUIInfo::CharInfoSNR(std::string& strValue) const
+{
+ strValue = StringUtils::Format("{} %", m_qualityInfo.iSNR / 655);
+}
+
+void CPVRGUIInfo::CharInfoBER(std::string& strValue) const
+{
+ strValue = StringUtils::Format("{:08X}", m_qualityInfo.iBER);
+}
+
+void CPVRGUIInfo::CharInfoUNC(std::string& strValue) const
+{
+ strValue = StringUtils::Format("{:08X}", m_qualityInfo.iUNC);
+}
+
+void CPVRGUIInfo::CharInfoFrontendName(std::string& strValue) const
+{
+ if (!strlen(m_qualityInfo.strAdapterName))
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_qualityInfo.strAdapterName;
+}
+
+void CPVRGUIInfo::CharInfoFrontendStatus(std::string& strValue) const
+{
+ if (!strlen(m_qualityInfo.strAdapterStatus))
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_qualityInfo.strAdapterStatus;
+}
+
+void CPVRGUIInfo::CharInfoBackendName(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendName;
+}
+
+void CPVRGUIInfo::CharInfoBackendVersion(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendVersion;
+}
+
+void CPVRGUIInfo::CharInfoBackendHost(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendHost;
+}
+
+void CPVRGUIInfo::CharInfoBackendDiskspace(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+
+ auto diskTotal = m_iBackendDiskTotal;
+ auto diskUsed = m_iBackendDiskUsed;
+
+ if (diskTotal > 0)
+ {
+ strValue = StringUtils::Format(g_localizeStrings.Get(802),
+ StringUtils::SizeToString(diskTotal - diskUsed),
+ StringUtils::SizeToString(diskTotal));
+ }
+ else
+ strValue = g_localizeStrings.Get(13205);
+}
+
+void CPVRGUIInfo::CharInfoBackendProviders(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendProviders;
+}
+
+void CPVRGUIInfo::CharInfoBackendChannelGroups(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendChannelGroups;
+}
+
+void CPVRGUIInfo::CharInfoBackendChannels(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendChannels;
+}
+
+void CPVRGUIInfo::CharInfoBackendTimers(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendTimers;
+}
+
+void CPVRGUIInfo::CharInfoBackendRecordings(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendRecordings;
+}
+
+void CPVRGUIInfo::CharInfoBackendDeletedRecordings(std::string& strValue) const
+{
+ m_updateBackendCacheRequested = true;
+ strValue = m_strBackendDeletedRecordings;
+}
+
+void CPVRGUIInfo::CharInfoPlayingClientName(std::string& strValue) const
+{
+ if (m_strPlayingClientName.empty())
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_strPlayingClientName;
+}
+
+void CPVRGUIInfo::CharInfoEncryption(std::string& strValue) const
+{
+ if (m_descrambleInfo.iCaid != PVR_DESCRAMBLE_INFO_NOT_AVAILABLE)
+ {
+ // prefer dynamically updated info, if available
+ strValue = CPVRChannel::GetEncryptionName(m_descrambleInfo.iCaid);
+ return;
+ }
+ else
+ {
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ if (channel)
+ {
+ strValue = channel->EncryptionName();
+ return;
+ }
+ }
+
+ strValue.clear();
+}
+
+void CPVRGUIInfo::CharInfoService(std::string& strValue) const
+{
+ if (!strlen(m_qualityInfo.strServiceName))
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_qualityInfo.strServiceName;
+}
+
+void CPVRGUIInfo::CharInfoMux(std::string& strValue) const
+{
+ if (!strlen(m_qualityInfo.strMuxName))
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_qualityInfo.strMuxName;
+}
+
+void CPVRGUIInfo::CharInfoProvider(std::string& strValue) const
+{
+ if (!strlen(m_qualityInfo.strProviderName))
+ strValue = g_localizeStrings.Get(13205);
+ else
+ strValue = m_qualityInfo.strProviderName;
+}
+
+void CPVRGUIInfo::UpdateBackendCache()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // Update the backend information for all backends if
+ // an update has been requested
+ if (m_iCurrentActiveClient == 0 && m_updateBackendCacheRequested)
+ {
+ std::vector<SBackend> backendProperties;
+ {
+ CSingleExit exit(m_critSection);
+ backendProperties = CServiceBroker::GetPVRManager().Clients()->GetBackendProperties();
+ }
+
+ m_backendProperties = backendProperties;
+ m_updateBackendCacheRequested = false;
+ }
+
+ // Store some defaults
+ m_strBackendName = g_localizeStrings.Get(13205);
+ m_strBackendVersion = g_localizeStrings.Get(13205);
+ m_strBackendHost = g_localizeStrings.Get(13205);
+ m_strBackendProviders = g_localizeStrings.Get(13205);
+ m_strBackendChannelGroups = g_localizeStrings.Get(13205);
+ m_strBackendChannels = g_localizeStrings.Get(13205);
+ m_strBackendTimers = g_localizeStrings.Get(13205);
+ m_strBackendRecordings = g_localizeStrings.Get(13205);
+ m_strBackendDeletedRecordings = g_localizeStrings.Get(13205);
+ m_iBackendDiskTotal = 0;
+ m_iBackendDiskUsed = 0;
+
+ // Update with values from the current client when we have at least one
+ if (!m_backendProperties.empty())
+ {
+ const auto& backend = m_backendProperties[m_iCurrentActiveClient];
+
+ m_strBackendName = backend.name;
+ m_strBackendVersion = backend.version;
+ m_strBackendHost = backend.host;
+
+ // We always display one extra as the add-on itself counts as a provider
+ if (backend.numProviders >= 0)
+ m_strBackendProviders = std::to_string(backend.numProviders + 1);
+
+ if (backend.numChannelGroups >= 0)
+ m_strBackendChannelGroups = std::to_string(backend.numChannelGroups);
+
+ if (backend.numChannels >= 0)
+ m_strBackendChannels = std::to_string(backend.numChannels);
+
+ if (backend.numTimers >= 0)
+ m_strBackendTimers = std::to_string(backend.numTimers);
+
+ if (backend.numRecordings >= 0)
+ m_strBackendRecordings = std::to_string(backend.numRecordings);
+
+ if (backend.numDeletedRecordings >= 0)
+ m_strBackendDeletedRecordings = std::to_string(backend.numDeletedRecordings);
+
+ m_iBackendDiskTotal = backend.diskTotal;
+ m_iBackendDiskUsed = backend.diskUsed;
+ }
+
+ // Update the current active client, eventually wrapping around
+ if (++m_iCurrentActiveClient >= m_backendProperties.size())
+ m_iCurrentActiveClient = 0;
+}
+
+void CPVRGUIInfo::UpdateTimersCache()
+{
+ m_anyTimersInfo.UpdateTimersCache();
+ m_tvTimersInfo.UpdateTimersCache();
+ m_radioTimersInfo.UpdateTimersCache();
+}
+
+void CPVRGUIInfo::UpdateTimersToggle()
+{
+ m_anyTimersInfo.UpdateTimersToggle();
+ m_tvTimersInfo.UpdateTimersToggle();
+ m_radioTimersInfo.UpdateTimersToggle();
+}
+
+void CPVRGUIInfo::UpdateNextTimer()
+{
+ m_anyTimersInfo.UpdateNextTimer();
+ m_tvTimersInfo.UpdateNextTimer();
+ m_radioTimersInfo.UpdateNextTimer();
+}
+
+int CPVRGUIInfo::GetTimeShiftSeekPercent() const
+{
+ int progress = m_timesInfo.GetTimeshiftProgressPlayPosition();
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ int seekSize = appPlayer->GetSeekHandler().GetSeekSize();
+ if (seekSize != 0)
+ {
+ int total = m_timesInfo.GetTimeshiftProgressDuration();
+
+ float totalTime = static_cast<float>(total);
+ if (totalTime == 0.0f)
+ return 0;
+
+ float percentPerSecond = 100.0f / totalTime;
+ float percent = progress + percentPerSecond * seekSize;
+ percent = std::max(0.0f, std::min(percent, 100.0f));
+ return std::lrintf(percent);
+ }
+ return progress;
+}
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h
new file mode 100644
index 0000000..5591353
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h
@@ -0,0 +1,217 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h"
+#include "guilib/guiinfo/GUIInfoProvider.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/guilib/guiinfo/PVRGUITimerInfo.h"
+#include "pvr/guilib/guiinfo/PVRGUITimesInfo.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <atomic>
+#include <string>
+#include <vector>
+
+class CFileItem;
+
+namespace KODI
+{
+namespace GUILIB
+{
+namespace GUIINFO
+{
+class CGUIInfo;
+}
+} // namespace GUILIB
+} // namespace KODI
+
+namespace PVR
+{
+enum class PVREvent;
+struct PVRChannelNumberInputChangedEvent;
+struct PVRPreviewAndPlayerShowInfoChangedEvent;
+
+class CPVRGUIInfo : public KODI::GUILIB::GUIINFO::CGUIInfoProvider, private CThread
+{
+public:
+ CPVRGUIInfo();
+ ~CPVRGUIInfo() override = default;
+
+ void Start();
+ void Stop();
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+
+ /*!
+ * @brief CEventStream callback for channel number input changes.
+ * @param event The event.
+ */
+ void Notify(const PVRChannelNumberInputChangedEvent& event);
+
+ /*!
+ * @brief CEventStream callback for channel preview and player show info changes.
+ * @param event The event.
+ */
+ void Notify(const PVRPreviewAndPlayerShowInfoChangedEvent& event);
+
+ // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation
+ bool InitCurrentItem(CFileItem* item) override;
+ bool GetLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ std::string* fallback) const override;
+ bool GetFallbackLabel(std::string& value,
+ const CFileItem* item,
+ int contextWindow,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ std::string* fallback) override;
+ bool GetInt(int& value,
+ const CGUIListItem* item,
+ int contextWindow,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info) const override;
+ bool GetBool(bool& value,
+ const CGUIListItem* item,
+ int contextWindow,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info) const override;
+
+private:
+ void ResetProperties();
+ void ClearQualityInfo(PVR_SIGNAL_STATUS& qualityInfo);
+ void ClearDescrambleInfo(PVR_DESCRAMBLE_INFO& descrambleInfo);
+
+ void Process() override;
+
+ void UpdateTimersCache();
+ void UpdateBackendCache();
+ void UpdateQualityData();
+ void UpdateDescrambleData();
+ void UpdateMisc();
+ void UpdateNextTimer();
+ void UpdateTimeshiftData();
+ void UpdateTimeshiftProgressData();
+
+ void UpdateTimersToggle();
+
+ bool GetListItemAndPlayerLabel(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ std::string& strValue) const;
+ bool GetPVRLabel(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ std::string& strValue) const;
+ bool GetRadioRDSLabel(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ std::string& strValue) const;
+
+ bool GetListItemAndPlayerInt(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ int& iValue) const;
+ bool GetPVRInt(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ int& iValue) const;
+ int GetTimeShiftSeekPercent() const;
+
+ bool GetListItemAndPlayerBool(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ bool& bValue) const;
+ bool GetPVRBool(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ bool& bValue) const;
+ bool GetRadioRDSBool(const CFileItem* item,
+ const KODI::GUILIB::GUIINFO::CGUIInfo& info,
+ bool& bValue) const;
+
+ void CharInfoBackendNumber(std::string& strValue) const;
+ void CharInfoTotalDiskSpace(std::string& strValue) const;
+ void CharInfoSignal(std::string& strValue) const;
+ void CharInfoSNR(std::string& strValue) const;
+ void CharInfoBER(std::string& strValue) const;
+ void CharInfoUNC(std::string& strValue) const;
+ void CharInfoFrontendName(std::string& strValue) const;
+ void CharInfoFrontendStatus(std::string& strValue) const;
+ void CharInfoBackendName(std::string& strValue) const;
+ void CharInfoBackendVersion(std::string& strValue) const;
+ void CharInfoBackendHost(std::string& strValue) const;
+ void CharInfoBackendDiskspace(std::string& strValue) const;
+ void CharInfoBackendProviders(std::string& strValue) const;
+ void CharInfoBackendChannelGroups(std::string& strValue) const;
+ void CharInfoBackendChannels(std::string& strValue) const;
+ void CharInfoBackendTimers(std::string& strValue) const;
+ void CharInfoBackendRecordings(std::string& strValue) const;
+ void CharInfoBackendDeletedRecordings(std::string& strValue) const;
+ void CharInfoPlayingClientName(std::string& strValue) const;
+ void CharInfoEncryption(std::string& strValue) const;
+ void CharInfoService(std::string& strValue) const;
+ void CharInfoMux(std::string& strValue) const;
+ void CharInfoProvider(std::string& strValue) const;
+
+ /** @name PVRGUIInfo data */
+ //@{
+ CPVRGUIAnyTimerInfo m_anyTimersInfo; // tv + radio
+ CPVRGUITVTimerInfo m_tvTimersInfo;
+ CPVRGUIRadioTimerInfo m_radioTimersInfo;
+
+ CPVRGUITimesInfo m_timesInfo;
+
+ bool m_bHasTVRecordings;
+ bool m_bHasRadioRecordings;
+ unsigned int m_iCurrentActiveClient;
+ std::string m_strPlayingClientName;
+ std::string m_strBackendName;
+ std::string m_strBackendVersion;
+ std::string m_strBackendHost;
+ std::string m_strBackendTimers;
+ std::string m_strBackendRecordings;
+ std::string m_strBackendDeletedRecordings;
+ std::string m_strBackendProviders;
+ std::string m_strBackendChannelGroups;
+ std::string m_strBackendChannels;
+ long long m_iBackendDiskTotal;
+ long long m_iBackendDiskUsed;
+ bool m_bIsPlayingTV;
+ bool m_bIsPlayingRadio;
+ bool m_bIsPlayingRecording;
+ bool m_bIsPlayingEpgTag;
+ bool m_bIsPlayingEncryptedStream;
+ bool m_bHasTVChannels;
+ bool m_bHasRadioChannels;
+ bool m_bCanRecordPlayingChannel;
+ bool m_bIsRecordingPlayingChannel;
+ bool m_bIsPlayingActiveRecording;
+ std::string m_strPlayingTVGroup;
+ std::string m_strPlayingRadioGroup;
+
+ //@}
+
+ PVR_SIGNAL_STATUS m_qualityInfo; /*!< stream quality information */
+ PVR_DESCRAMBLE_INFO m_descrambleInfo; /*!< stream descramble information */
+ std::vector<SBackend> m_backendProperties;
+
+ std::string m_channelNumberInput;
+ bool m_previewAndPlayerShowInfo{false};
+
+ mutable CCriticalSection m_critSection;
+
+ /**
+ * The various backend-related fields will only be updated when this
+ * flag is set. This is done to limit the amount of unnecessary
+ * backend querying when we're not displaying any of the queried
+ * information.
+ */
+ mutable std::atomic<bool> m_updateBackendCacheRequested;
+
+ bool m_bRegistered;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp
new file mode 100644
index 0000000..24a468a
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp
@@ -0,0 +1,270 @@
+/*
+ * 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 "PVRGUITimerInfo.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+CPVRGUITimerInfo::CPVRGUITimerInfo()
+{
+ ResetProperties();
+}
+
+void CPVRGUITimerInfo::ResetProperties()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strActiveTimerTitle.clear();
+ m_strActiveTimerChannelName.clear();
+ m_strActiveTimerChannelIcon.clear();
+ m_strActiveTimerTime.clear();
+ m_strNextTimerInfo.clear();
+ m_strNextRecordingTitle.clear();
+ m_strNextRecordingChannelName.clear();
+ m_strNextRecordingChannelIcon.clear();
+ m_strNextRecordingTime.clear();
+ m_iTimerAmount = 0;
+ m_iRecordingTimerAmount = 0;
+ m_iTimerInfoToggleStart = {};
+ m_iTimerInfoToggleCurrent = 0;
+}
+
+bool CPVRGUITimerInfo::TimerInfoToggle()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_iTimerInfoToggleStart.time_since_epoch().count() == 0)
+ {
+ m_iTimerInfoToggleStart = std::chrono::steady_clock::now();
+ m_iTimerInfoToggleCurrent = 0;
+ return true;
+ }
+
+ auto now = std::chrono::steady_clock::now();
+ auto duration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - m_iTimerInfoToggleStart);
+
+ if (duration.count() >
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRInfoToggleInterval)
+ {
+ unsigned int iPrevious = m_iTimerInfoToggleCurrent;
+ unsigned int iBoundary = m_iRecordingTimerAmount > 0 ? m_iRecordingTimerAmount : m_iTimerAmount;
+ if (++m_iTimerInfoToggleCurrent > iBoundary - 1)
+ m_iTimerInfoToggleCurrent = 0;
+
+ if (m_iTimerInfoToggleCurrent != iPrevious)
+ {
+ m_iTimerInfoToggleStart = std::chrono::steady_clock::now();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CPVRGUITimerInfo::UpdateTimersToggle()
+{
+ if (!TimerInfoToggle())
+ return;
+
+ std::string strActiveTimerTitle;
+ std::string strActiveTimerChannelName;
+ std::string strActiveTimerChannelIcon;
+ std::string strActiveTimerTime;
+
+ /* safe to fetch these unlocked, since they're updated from the same thread as this one */
+ if (m_iRecordingTimerAmount > 0)
+ {
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> activeTags = GetActiveRecordings();
+ if (m_iTimerInfoToggleCurrent < activeTags.size())
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> tag = activeTags.at(m_iTimerInfoToggleCurrent);
+ strActiveTimerTitle = tag->Title();
+ strActiveTimerChannelName = tag->ChannelName();
+ strActiveTimerChannelIcon = tag->ChannelIcon();
+ strActiveTimerTime = tag->StartAsLocalTime().GetAsLocalizedDateTime(false, false);
+ }
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strActiveTimerTitle = strActiveTimerTitle;
+ m_strActiveTimerChannelName = strActiveTimerChannelName;
+ m_strActiveTimerChannelIcon = strActiveTimerChannelIcon;
+ m_strActiveTimerTime = strActiveTimerTime;
+}
+
+void CPVRGUITimerInfo::UpdateTimersCache()
+{
+ int iTimerAmount = AmountActiveTimers();
+ int iRecordingTimerAmount = AmountActiveRecordings();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_iTimerAmount = iTimerAmount;
+ m_iRecordingTimerAmount = iRecordingTimerAmount;
+ m_iTimerInfoToggleStart = {};
+ }
+
+ UpdateTimersToggle();
+}
+
+void CPVRGUITimerInfo::UpdateNextTimer()
+{
+ std::string strNextRecordingTitle;
+ std::string strNextRecordingChannelName;
+ std::string strNextRecordingChannelIcon;
+ std::string strNextRecordingTime;
+ std::string strNextTimerInfo;
+
+ const std::shared_ptr<CPVRTimerInfoTag> timer = GetNextActiveTimer();
+ if (timer)
+ {
+ strNextRecordingTitle = timer->Title();
+ strNextRecordingChannelName = timer->ChannelName();
+ strNextRecordingChannelIcon = timer->ChannelIcon();
+ strNextRecordingTime = timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false);
+
+ strNextTimerInfo = StringUtils::Format("{} {} {} {}", g_localizeStrings.Get(19106),
+ timer->StartAsLocalTime().GetAsLocalizedDate(true),
+ g_localizeStrings.Get(19107),
+ timer->StartAsLocalTime().GetAsLocalizedTime("", false));
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strNextRecordingTitle = strNextRecordingTitle;
+ m_strNextRecordingChannelName = strNextRecordingChannelName;
+ m_strNextRecordingChannelIcon = strNextRecordingChannelIcon;
+ m_strNextRecordingTime = strNextRecordingTime;
+ m_strNextTimerInfo = strNextTimerInfo;
+}
+
+const std::string& CPVRGUITimerInfo::GetActiveTimerTitle() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strActiveTimerTitle;
+}
+
+const std::string& CPVRGUITimerInfo::GetActiveTimerChannelName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strActiveTimerChannelName;
+}
+
+const std::string& CPVRGUITimerInfo::GetActiveTimerChannelIcon() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strActiveTimerChannelIcon;
+}
+
+const std::string& CPVRGUITimerInfo::GetActiveTimerDateTime() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strActiveTimerTime;
+}
+
+const std::string& CPVRGUITimerInfo::GetNextTimerTitle() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strNextRecordingTitle;
+}
+
+const std::string& CPVRGUITimerInfo::GetNextTimerChannelName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strNextRecordingChannelName;
+}
+
+const std::string& CPVRGUITimerInfo::GetNextTimerChannelIcon() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strNextRecordingChannelIcon;
+}
+
+const std::string& CPVRGUITimerInfo::GetNextTimerDateTime() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strNextRecordingTime;
+}
+
+const std::string& CPVRGUITimerInfo::GetNextTimer() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strNextTimerInfo;
+}
+
+int CPVRGUIAnyTimerInfo::AmountActiveTimers()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveTimers();
+}
+
+int CPVRGUIAnyTimerInfo::AmountActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveRecordings();
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUIAnyTimerInfo::GetActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetActiveRecordings();
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRGUIAnyTimerInfo::GetNextActiveTimer()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetNextActiveTimer();
+}
+
+int CPVRGUITVTimerInfo::AmountActiveTimers()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveTVTimers();
+}
+
+int CPVRGUITVTimerInfo::AmountActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveTVRecordings();
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUITVTimerInfo::GetActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetActiveTVRecordings();
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRGUITVTimerInfo::GetNextActiveTimer()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetNextActiveTVTimer();
+}
+
+int CPVRGUIRadioTimerInfo::AmountActiveTimers()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveRadioTimers();
+}
+
+int CPVRGUIRadioTimerInfo::AmountActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->AmountActiveRadioRecordings();
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUIRadioTimerInfo::GetActiveRecordings()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetActiveRadioRecordings();
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRGUIRadioTimerInfo::GetNextActiveTimer()
+{
+ return CServiceBroker::GetPVRManager().Timers()->GetNextActiveRadioTimer();
+}
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h
new file mode 100644
index 0000000..260db6c
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h
@@ -0,0 +1,111 @@
+/*
+ * 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 <chrono>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+ class CPVRTimerInfoTag;
+
+ class CPVRGUITimerInfo
+ {
+ public:
+ CPVRGUITimerInfo();
+ virtual ~CPVRGUITimerInfo() = default;
+
+ void ResetProperties();
+
+ void UpdateTimersCache();
+ void UpdateTimersToggle();
+ void UpdateNextTimer();
+
+ const std::string& GetActiveTimerTitle() const;
+ const std::string& GetActiveTimerChannelName() const;
+ const std::string& GetActiveTimerChannelIcon() const;
+ const std::string& GetActiveTimerDateTime() const;
+ const std::string& GetNextTimerTitle() const;
+ const std::string& GetNextTimerChannelName() const;
+ const std::string& GetNextTimerChannelIcon() const;
+ const std::string& GetNextTimerDateTime() const;
+ const std::string& GetNextTimer() const;
+
+ bool HasTimers() const { return m_iTimerAmount > 0; }
+ bool HasRecordingTimers() const { return m_iRecordingTimerAmount > 0; }
+ bool HasNonRecordingTimers() const { return m_iTimerAmount - m_iRecordingTimerAmount > 0; }
+
+ private:
+ bool TimerInfoToggle();
+
+ virtual int AmountActiveTimers() = 0;
+ virtual int AmountActiveRecordings() = 0;
+ virtual std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() = 0;
+ virtual std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() = 0;
+
+ unsigned int m_iTimerAmount;
+ unsigned int m_iRecordingTimerAmount;
+
+ std::string m_strActiveTimerTitle;
+ std::string m_strActiveTimerChannelName;
+ std::string m_strActiveTimerChannelIcon;
+ std::string m_strActiveTimerTime;
+ std::string m_strNextRecordingTitle;
+ std::string m_strNextRecordingChannelName;
+ std::string m_strNextRecordingChannelIcon;
+ std::string m_strNextRecordingTime;
+ std::string m_strNextTimerInfo;
+
+ std::chrono::time_point<std::chrono::steady_clock> m_iTimerInfoToggleStart;
+ unsigned int m_iTimerInfoToggleCurrent;
+
+ mutable CCriticalSection m_critSection;
+ };
+
+ class CPVRGUIAnyTimerInfo : public CPVRGUITimerInfo
+ {
+ public:
+ CPVRGUIAnyTimerInfo() = default;
+
+ private:
+ int AmountActiveTimers() override;
+ int AmountActiveRecordings() override;
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override;
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override;
+ };
+
+ class CPVRGUITVTimerInfo : public CPVRGUITimerInfo
+ {
+ public:
+ CPVRGUITVTimerInfo() = default;
+
+ private:
+ int AmountActiveTimers() override;
+ int AmountActiveRecordings() override;
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override;
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override;
+ };
+
+ class CPVRGUIRadioTimerInfo : public CPVRGUITimerInfo
+ {
+ public:
+ CPVRGUIRadioTimerInfo() = default;
+
+ private:
+ int AmountActiveTimers() override;
+ int AmountActiveRecordings() override;
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override;
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override;
+ };
+
+} // namespace PVR
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp
new file mode 100644
index 0000000..e5dfdb9
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp
@@ -0,0 +1,424 @@
+/*
+ * 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 "PVRGUITimesInfo.h"
+
+#include "ServiceBroker.h"
+#include "cores/DataCacheCore.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+
+#include <cmath>
+#include <ctime>
+#include <memory>
+#include <mutex>
+
+using namespace PVR;
+
+CPVRGUITimesInfo::CPVRGUITimesInfo()
+{
+ Reset();
+}
+
+void CPVRGUITimesInfo::Reset()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_iStartTime = 0;
+ m_iDuration = 0;
+ m_iTimeshiftStartTime = 0;
+ m_iTimeshiftEndTime = 0;
+ m_iTimeshiftPlayTime = 0;
+ m_iTimeshiftOffset = 0;
+
+ m_iTimeshiftProgressStartTime = 0;
+ m_iTimeshiftProgressEndTime = 0;
+ m_iTimeshiftProgressDuration = 0;
+
+ m_playingEpgTag.reset();
+ m_playingChannel.reset();
+}
+
+void CPVRGUITimesInfo::UpdatePlayingTag()
+{
+ const std::shared_ptr<CPVRChannel> currentChannel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+ std::shared_ptr<CPVREpgInfoTag> currentTag = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingEpgTag();
+
+ if (currentChannel || currentTag)
+ {
+ if (currentChannel && !currentTag)
+ currentTag = currentChannel->GetEPGNow();
+
+ const std::shared_ptr<CPVRChannelGroupsContainer> groups = CServiceBroker::GetPVRManager().ChannelGroups();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::shared_ptr<CPVRChannel> playingChannel =
+ m_playingEpgTag ? groups->GetChannelForEpgTag(m_playingEpgTag) : nullptr;
+
+ if (!m_playingEpgTag || !currentTag || !playingChannel || !currentChannel ||
+ m_playingEpgTag->StartAsUTC() != currentTag->StartAsUTC() ||
+ m_playingEpgTag->EndAsUTC() != currentTag->EndAsUTC() || *playingChannel != *currentChannel)
+ {
+ if (currentTag)
+ {
+ m_playingEpgTag = currentTag;
+ m_iDuration = m_playingEpgTag->GetDuration();
+ }
+ else if (m_iTimeshiftEndTime > m_iTimeshiftStartTime)
+ {
+ m_playingEpgTag.reset();
+ m_iDuration = m_iTimeshiftEndTime - m_iTimeshiftStartTime;
+ }
+ else
+ {
+ m_playingEpgTag.reset();
+ m_iDuration = 0;
+ }
+ }
+ }
+ else
+ {
+ const std::shared_ptr<CPVRRecording> recording = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingRecording();
+ if (recording)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_playingEpgTag.reset();
+ m_iDuration = recording->GetDuration();
+ }
+ }
+}
+
+void CPVRGUITimesInfo::UpdateTimeshiftData()
+{
+ if (!CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV() && !CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio())
+ {
+ // If nothing is playing (anymore), there is no need to update data.
+ Reset();
+ return;
+ }
+
+ time_t now = std::time(nullptr);
+ time_t iStartTime;
+ int64_t iPlayTime, iMinTime, iMaxTime;
+ CServiceBroker::GetDataCacheCore().GetPlayTimes(iStartTime, iPlayTime, iMinTime, iMaxTime);
+ bool bPlaying = CServiceBroker::GetDataCacheCore().GetSpeed() == 1.0f;
+ const std::shared_ptr<CPVRChannel> playingChannel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (playingChannel != m_playingChannel)
+ {
+ // playing channel changed. we need to reset offset and playtime.
+ m_iTimeshiftOffset = 0;
+ m_iTimeshiftPlayTime = 0;
+ m_playingChannel = playingChannel;
+ }
+
+ if (!iStartTime)
+ {
+ if (m_iStartTime == 0)
+ iStartTime = now;
+ else
+ iStartTime = m_iStartTime;
+
+ iMinTime = iPlayTime;
+ iMaxTime = iPlayTime;
+ }
+
+ m_iStartTime = iStartTime;
+ m_iTimeshiftStartTime = iStartTime + iMinTime / 1000;
+ m_iTimeshiftEndTime = iStartTime + iMaxTime / 1000;
+
+ if (m_iTimeshiftEndTime > m_iTimeshiftStartTime)
+ {
+ // timeshifting supported
+ m_iTimeshiftPlayTime = iStartTime + iPlayTime / 1000;
+ if (iMaxTime > iPlayTime)
+ m_iTimeshiftOffset = (iMaxTime - iPlayTime) / 1000;
+ else
+ m_iTimeshiftOffset = 0;
+ }
+ else
+ {
+ // timeshifting not supported
+ if (bPlaying)
+ m_iTimeshiftPlayTime = now - m_iTimeshiftOffset;
+
+ m_iTimeshiftOffset = now - m_iTimeshiftPlayTime;
+ }
+
+ UpdateTimeshiftProgressData();
+}
+
+void CPVRGUITimesInfo::UpdateTimeshiftProgressData()
+{
+ // Note: General idea of the ts progress is always to be able to visualise both the complete
+ // ts buffer and the complete playing epg event (if any) side by side with the same time
+ // scale. TS progress start and end times will be calculated accordingly.
+ // + Start is usually ts buffer start, except if start time of playing epg event is
+ // before ts buffer start, then progress start is epg event start.
+ // + End is usually ts buffer end, except if end time of playing epg event is
+ // after ts buffer end, then progress end is epg event end.
+ // In simple timeshift mode (settings value), progress start is always the start time of
+ // playing epg event and progress end is always the end time of playing epg event.
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // start time
+ //////////////////////////////////////////////////////////////////////////////////////
+ bool bUpdatedStartTime = false;
+ if (m_playingEpgTag)
+ {
+ time_t start = 0;
+ m_playingEpgTag->StartAsUTC().GetAsTime(start);
+ if (start < m_iTimeshiftStartTime ||
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bPVRTimeshiftSimpleOSD)
+ {
+ // playing event started before start of ts buffer or simple ts osd to be used
+ m_iTimeshiftProgressStartTime = start;
+ bUpdatedStartTime = true;
+ }
+ }
+
+ if (!bUpdatedStartTime)
+ {
+ // default to ts buffer start
+ m_iTimeshiftProgressStartTime = m_iTimeshiftStartTime;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // end time
+ //////////////////////////////////////////////////////////////////////////////////////
+ bool bUpdatedEndTime = false;
+ if (m_playingEpgTag)
+ {
+ time_t end = 0;
+ m_playingEpgTag->EndAsUTC().GetAsTime(end);
+ if (end > m_iTimeshiftEndTime ||
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bPVRTimeshiftSimpleOSD)
+ {
+ // playing event will end after end of ts buffer or simple ts osd to be used
+ m_iTimeshiftProgressEndTime = end;
+ bUpdatedEndTime = true;
+ }
+ }
+
+ if (!bUpdatedEndTime)
+ {
+ // default to ts buffer end
+ m_iTimeshiftProgressEndTime = m_iTimeshiftEndTime;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // duration
+ //////////////////////////////////////////////////////////////////////////////////////
+ m_iTimeshiftProgressDuration = m_iTimeshiftProgressEndTime - m_iTimeshiftProgressStartTime;
+}
+
+void CPVRGUITimesInfo::Update()
+{
+ UpdatePlayingTag();
+ UpdateTimeshiftData();
+}
+
+std::string CPVRGUITimesInfo::TimeToTimeString(time_t datetime, TIME_FORMAT format, bool withSeconds)
+{
+ CDateTime time;
+ time.SetFromUTCDateTime(datetime);
+ return time.GetAsLocalizedTime(format, withSeconds);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftStartTime(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return TimeToTimeString(m_iTimeshiftStartTime, format, false);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftEndTime(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return TimeToTimeString(m_iTimeshiftEndTime, format, false);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftPlayTime(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return TimeToTimeString(m_iTimeshiftPlayTime, format, true);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftOffset(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return StringUtils::SecondsToTimeString(m_iTimeshiftOffset, format);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftProgressDuration(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return StringUtils::SecondsToTimeString(m_iTimeshiftProgressDuration, format);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftProgressStartTime(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return TimeToTimeString(m_iTimeshiftProgressStartTime, format, false);
+}
+
+std::string CPVRGUITimesInfo::GetTimeshiftProgressEndTime(TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return TimeToTimeString(m_iTimeshiftProgressEndTime, format, false);
+}
+
+std::string CPVRGUITimesInfo::GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return StringUtils::SecondsToTimeString(GetEpgEventDuration(epgTag), format);
+}
+
+std::string CPVRGUITimesInfo::GetEpgEventElapsedTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const
+{
+ int iElapsed = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag)
+ iElapsed = epgTag->Progress();
+ else
+ iElapsed = GetElapsedTime();
+
+ return StringUtils::SecondsToTimeString(iElapsed, format);
+}
+
+std::string CPVRGUITimesInfo::GetEpgEventRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return StringUtils::SecondsToTimeString(GetRemainingTime(epgTag), format);
+}
+
+std::string CPVRGUITimesInfo::GetEpgEventFinishTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const
+{
+ CDateTime finish = CDateTime::GetCurrentDateTime();
+ finish += CDateTimeSpan(0, 0, 0, GetRemainingTime(epgTag));
+ return finish.GetAsLocalizedTime(format);
+}
+
+std::string CPVRGUITimesInfo::GetEpgEventSeekTime(int iSeekSize, TIME_FORMAT format) const
+{
+ return StringUtils::SecondsToTimeString(GetElapsedTime() + iSeekSize, format);
+}
+
+int CPVRGUITimesInfo::GetElapsedTime() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_playingEpgTag || m_iTimeshiftStartTime)
+ {
+ CDateTime current(m_iTimeshiftPlayTime);
+ CDateTime start = m_playingEpgTag ? m_playingEpgTag->StartAsUTC() : CDateTime(m_iTimeshiftStartTime);
+ CDateTimeSpan time = current > start ? current - start : CDateTimeSpan(0, 0, 0, 0);
+ return time.GetSecondsTotal();
+ }
+ else
+ {
+ return 0;
+ }
+}
+
+int CPVRGUITimesInfo::GetRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag)
+ return epgTag->GetDuration() - epgTag->Progress();
+ else
+ return m_iDuration - GetElapsedTime();
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgress() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::lrintf(static_cast<float>(m_iTimeshiftPlayTime - m_iTimeshiftStartTime) / (m_iTimeshiftEndTime - m_iTimeshiftStartTime) * 100);
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressDuration() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iTimeshiftProgressDuration;
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressPlayPosition() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::lrintf(static_cast<float>(m_iTimeshiftPlayTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100);
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressEpgStart() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_playingEpgTag)
+ {
+ time_t epgStart = 0;
+ m_playingEpgTag->StartAsUTC().GetAsTime(epgStart);
+ return std::lrintf(static_cast<float>(epgStart - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100);
+ }
+ return 0;
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressEpgEnd() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_playingEpgTag)
+ {
+ time_t epgEnd = 0;
+ m_playingEpgTag->EndAsUTC().GetAsTime(epgEnd);
+ return std::lrintf(static_cast<float>(epgEnd - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100);
+ }
+ return 0;
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressBufferStart() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::lrintf(static_cast<float>(m_iTimeshiftStartTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100);
+}
+
+int CPVRGUITimesInfo::GetTimeshiftProgressBufferEnd() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return std::lrintf(static_cast<float>(m_iTimeshiftEndTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100);
+}
+
+int CPVRGUITimesInfo::GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag)
+ return epgTag->GetDuration();
+ else
+ return m_iDuration;
+}
+
+int CPVRGUITimesInfo::GetEpgEventProgress(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag)
+ return std::lrintf(epgTag->ProgressPercentage());
+ else
+ return std::lrintf(static_cast<float>(GetElapsedTime()) / m_iDuration * 100);
+}
+
+bool CPVRGUITimesInfo::IsTimeshifting() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return (m_iTimeshiftOffset > static_cast<unsigned int>(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeshiftThreshold));
+}
diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h
new file mode 100644
index 0000000..47dd9f5
--- /dev/null
+++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h
@@ -0,0 +1,87 @@
+/*
+ * 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 "utils/TimeFormat.h"
+
+#include <memory>
+
+namespace PVR
+{
+ class CPVRChannel;
+ class CPVREpgInfoTag;
+
+ class CPVRGUITimesInfo
+ {
+ public:
+ CPVRGUITimesInfo();
+ virtual ~CPVRGUITimesInfo() = default;
+
+ void Reset();
+ void Update();
+
+ // GUI info labels
+ std::string GetTimeshiftStartTime(TIME_FORMAT format) const;
+ std::string GetTimeshiftEndTime(TIME_FORMAT format) const;
+ std::string GetTimeshiftPlayTime(TIME_FORMAT format) const;
+ std::string GetTimeshiftOffset(TIME_FORMAT format) const;
+ std::string GetTimeshiftProgressDuration(TIME_FORMAT format) const;
+ std::string GetTimeshiftProgressStartTime(TIME_FORMAT format) const;
+ std::string GetTimeshiftProgressEndTime(TIME_FORMAT format) const;
+
+ std::string GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const;
+ std::string GetEpgEventElapsedTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const;
+ std::string GetEpgEventRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const;
+ std::string GetEpgEventFinishTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const;
+ std::string GetEpgEventSeekTime(int iSeekSize, TIME_FORMAT format) const;
+
+ // GUI info ints
+ int GetTimeshiftProgress() const;
+ int GetTimeshiftProgressDuration() const;
+ int GetTimeshiftProgressPlayPosition() const;
+ int GetTimeshiftProgressEpgStart() const;
+ int GetTimeshiftProgressEpgEnd() const;
+ int GetTimeshiftProgressBufferStart() const;
+ int GetTimeshiftProgressBufferEnd() const;
+
+ int GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ int GetEpgEventProgress(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ // GUI info bools
+ bool IsTimeshifting() const;
+
+ private:
+ void UpdatePlayingTag();
+ void UpdateTimeshiftData();
+ void UpdateTimeshiftProgressData();
+
+ static std::string TimeToTimeString(time_t datetime, TIME_FORMAT format, bool withSeconds);
+
+ int GetElapsedTime() const;
+ int GetRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ mutable CCriticalSection m_critSection;
+
+ std::shared_ptr<CPVREpgInfoTag> m_playingEpgTag;
+ std::shared_ptr<CPVRChannel> m_playingChannel;
+
+ time_t m_iStartTime;
+ unsigned int m_iDuration;
+ time_t m_iTimeshiftStartTime;
+ time_t m_iTimeshiftEndTime;
+ time_t m_iTimeshiftPlayTime;
+ unsigned int m_iTimeshiftOffset;
+
+ time_t m_iTimeshiftProgressStartTime;
+ time_t m_iTimeshiftProgressEndTime;
+ unsigned int m_iTimeshiftProgressDuration;
+ };
+
+} // namespace PVR
diff --git a/xbmc/pvr/providers/CMakeLists.txt b/xbmc/pvr/providers/CMakeLists.txt
new file mode 100644
index 0000000..f01a35a
--- /dev/null
+++ b/xbmc/pvr/providers/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES PVRProvider.cpp
+ PVRProviders.cpp)
+
+set(HEADERS PVRProvider.h
+ PVRProviders.h)
+
+core_add_library(pvr_providers)
diff --git a/xbmc/pvr/providers/PVRProvider.cpp b/xbmc/pvr/providers/PVRProvider.cpp
new file mode 100644
index 0000000..ab2e7a3
--- /dev/null
+++ b/xbmc/pvr/providers/PVRProvider.cpp
@@ -0,0 +1,393 @@
+/*
+ * 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 "PVRProvider.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+
+using namespace PVR;
+
+
+const std::string CPVRProvider::IMAGE_OWNER_PATTERN = "pvrprovider";
+
+CPVRProvider::CPVRProvider(int iUniqueId, int iClientId)
+ : m_iUniqueId(iUniqueId),
+ m_iClientId(iClientId),
+ m_iconPath(IMAGE_OWNER_PATTERN),
+ m_thumbPath(IMAGE_OWNER_PATTERN)
+{
+}
+
+CPVRProvider::CPVRProvider(const PVR_PROVIDER& provider, int iClientId)
+ : m_iUniqueId(provider.iUniqueId),
+ m_iClientId(iClientId),
+ m_strName(provider.strName),
+ m_type(provider.type),
+ m_iconPath(provider.strIconPath, IMAGE_OWNER_PATTERN),
+ m_strCountries(provider.strCountries),
+ m_strLanguages(provider.strLanguages),
+ m_thumbPath(IMAGE_OWNER_PATTERN)
+{
+}
+
+CPVRProvider::CPVRProvider(int iClientId,
+ const std::string& addonProviderName,
+ const std::string& addonIconPath,
+ const std::string& addonThumbPath)
+ : m_iClientId(iClientId),
+ m_strName(addonProviderName),
+ m_type(PVR_PROVIDER_TYPE_ADDON),
+ m_iconPath(addonIconPath, IMAGE_OWNER_PATTERN),
+ m_bIsClientProvider(true),
+ m_thumbPath(addonThumbPath, IMAGE_OWNER_PATTERN)
+{
+}
+
+bool CPVRProvider::operator==(const CPVRProvider& right) const
+{
+ return (m_iUniqueId == right.m_iUniqueId && m_iClientId == right.m_iClientId);
+}
+
+bool CPVRProvider::operator!=(const CPVRProvider& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRProvider::Serialize(CVariant& value) const
+{
+ value["providerid"] = m_iDatabaseId;
+ value["clientid"] = m_iClientId;
+ value["providername"] = m_strName;
+ switch (m_type)
+ {
+ case PVR_PROVIDER_TYPE_ADDON:
+ value["providertype"] = "addon";
+ break;
+ case PVR_PROVIDER_TYPE_SATELLITE:
+ value["providertype"] = "satellite";
+ break;
+ case PVR_PROVIDER_TYPE_CABLE:
+ value["providertype"] = "cable";
+ break;
+ case PVR_PROVIDER_TYPE_AERIAL:
+ value["providertype"] = "aerial";
+ break;
+ case PVR_PROVIDER_TYPE_IPTV:
+ value["providertype"] = "iptv";
+ break;
+ case PVR_PROVIDER_TYPE_OTHER:
+ value["providertype"] = "other";
+ break;
+ default:
+ value["state"] = "unknown";
+ break;
+ }
+ value["iconpath"] = GetClientIconPath();
+ value["countries"] = m_strCountries;
+ value["languages"] = m_strLanguages;
+}
+
+int CPVRProvider::GetDatabaseId() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iDatabaseId;
+}
+
+bool CPVRProvider::SetDatabaseId(int iDatabaseId)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_iDatabaseId != iDatabaseId)
+ {
+ m_iDatabaseId = iDatabaseId;
+ return true;
+ }
+
+ return false;
+}
+
+int CPVRProvider::GetUniqueId() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iUniqueId;
+}
+
+int CPVRProvider::GetClientId() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientId;
+}
+
+std::string CPVRProvider::GetName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strName;
+}
+
+bool CPVRProvider::SetName(const std::string& strName)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_strName != strName)
+ {
+ m_strName = strName;
+ return true;
+ }
+
+ return false;
+}
+
+PVR_PROVIDER_TYPE CPVRProvider::GetType() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_type;
+}
+
+bool CPVRProvider::SetType(PVR_PROVIDER_TYPE type)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_type != type)
+ {
+ m_type = type;
+ return true;
+ }
+
+ return false;
+}
+
+std::string CPVRProvider::GetClientIconPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iconPath.GetClientImage();
+}
+
+std::string CPVRProvider::GetIconPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iconPath.GetLocalImage();
+}
+
+bool CPVRProvider::SetIconPath(const std::string& strIconPath)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (GetClientIconPath() != strIconPath)
+ {
+ m_iconPath.SetClientImage(strIconPath);
+ return true;
+ }
+
+ return false;
+}
+
+namespace
+{
+
+const std::vector<std::string> Tokenize(const std::string& str)
+{
+ return StringUtils::Split(str, PROVIDER_STRING_TOKEN_SEPARATOR);
+}
+
+const std::string DeTokenize(const std::vector<std::string>& tokens)
+{
+ return StringUtils::Join(tokens, PROVIDER_STRING_TOKEN_SEPARATOR);
+}
+
+} // unnamed namespace
+
+std::vector<std::string> CPVRProvider::GetCountries() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ return Tokenize(m_strCountries);
+}
+
+bool CPVRProvider::SetCountries(const std::vector<std::string>& countries)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strCountries = DeTokenize(countries);
+ if (m_strCountries != strCountries)
+ {
+ m_strCountries = strCountries;
+ return true;
+ }
+
+ return false;
+}
+
+std::string CPVRProvider::GetCountriesDBString() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strCountries;
+}
+
+bool CPVRProvider::SetCountriesFromDBString(const std::string& strCountries)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_strCountries != strCountries)
+ {
+ m_strCountries = strCountries;
+ return true;
+ }
+
+ return false;
+}
+
+std::vector<std::string> CPVRProvider::GetLanguages() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return Tokenize(m_strLanguages);
+}
+
+bool CPVRProvider::SetLanguages(const std::vector<std::string>& languages)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::string strLanguages = DeTokenize(languages);
+ if (m_strLanguages != strLanguages)
+ {
+ m_strLanguages = strLanguages;
+ return true;
+ }
+
+ return false;
+}
+
+std::string CPVRProvider::GetLanguagesDBString() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strLanguages;
+}
+
+bool CPVRProvider::SetLanguagesFromDBString(const std::string& strLanguages)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_strLanguages != strLanguages)
+ {
+ m_strLanguages = strLanguages;
+ return true;
+ }
+
+ return false;
+}
+
+bool CPVRProvider::Persist(bool updateRecord /* = false */)
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return database->Persist(*this, updateRecord);
+ }
+
+ return false;
+}
+
+bool CPVRProvider::DeleteFromDatabase()
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return database->Delete(*this);
+ }
+
+ return false;
+}
+
+bool CPVRProvider::UpdateEntry(const std::shared_ptr<CPVRProvider>& fromProvider,
+ ProviderUpdateMode updateMode)
+{
+ bool bChanged = false;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (updateMode == ProviderUpdateMode::BY_DATABASE)
+ {
+ m_iDatabaseId = fromProvider->m_iDatabaseId;
+
+ m_strName = fromProvider->m_strName;
+ m_type = fromProvider->m_type;
+ m_iconPath = fromProvider->m_iconPath;
+
+ if (fromProvider->m_bIsClientProvider)
+ {
+ m_thumbPath = fromProvider->m_thumbPath;
+ m_bIsClientProvider = fromProvider->m_bIsClientProvider;
+ }
+
+ m_strCountries = fromProvider->m_strCountries;
+ m_strLanguages = fromProvider->m_strLanguages;
+ }
+ else if (updateMode == ProviderUpdateMode::BY_CLIENT)
+ {
+ if (m_strName != fromProvider->m_strName)
+ {
+ m_strName = fromProvider->m_strName;
+ bChanged = true;
+ }
+
+ if (m_type != fromProvider->m_type)
+ {
+ m_type = fromProvider->m_type;
+ bChanged = true;
+ }
+
+ if (m_iconPath != fromProvider->m_iconPath)
+ {
+ m_iconPath = fromProvider->m_iconPath;
+ bChanged = true;
+ }
+
+ if (fromProvider->m_bIsClientProvider)
+ {
+ m_thumbPath = fromProvider->m_thumbPath;
+ m_bIsClientProvider = fromProvider->m_bIsClientProvider;
+ }
+
+ if (m_strCountries != fromProvider->m_strCountries)
+ {
+ m_strCountries = fromProvider->m_strCountries;
+ bChanged = true;
+ }
+
+ if (m_strLanguages != fromProvider->m_strLanguages)
+ {
+ m_strLanguages = fromProvider->m_strLanguages;
+ bChanged = true;
+ }
+ }
+
+ return bChanged;
+}
+
+bool CPVRProvider::HasThumbPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return (m_type == PVR_PROVIDER_TYPE_ADDON && !m_thumbPath.GetLocalImage().empty());
+}
+
+std::string CPVRProvider::GetThumbPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_thumbPath.GetLocalImage();
+}
+
+std::string CPVRProvider::GetClientThumbPath() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_thumbPath.GetClientImage();
+}
diff --git a/xbmc/pvr/providers/PVRProvider.h b/xbmc/pvr/providers/PVRProvider.h
new file mode 100644
index 0000000..1e8d835
--- /dev/null
+++ b/xbmc/pvr/providers/PVRProvider.h
@@ -0,0 +1,245 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h"
+#include "pvr/PVRCachedImage.h"
+#include "threads/CriticalSection.h"
+#include "utils/ISerializable.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace PVR
+{
+
+enum class ProviderUpdateMode
+{
+ BY_CLIENT,
+ BY_DATABASE
+};
+
+static constexpr int PVR_PROVIDER_ADDON_UID = -1;
+static constexpr int PVR_PROVIDER_INVALID_DB_ID = -1;
+
+class CPVRProvider final : public ISerializable
+{
+public:
+ static const std::string IMAGE_OWNER_PATTERN;
+
+ CPVRProvider(int iUniqueId, int iClientId);
+ CPVRProvider(const PVR_PROVIDER& provider, int iClientId);
+ CPVRProvider(int iClientId,
+ const std::string& addonProviderName,
+ const std::string& addonIconPath,
+ const std::string& addonThumbPath);
+
+ bool operator==(const CPVRProvider& right) const;
+ bool operator!=(const CPVRProvider& right) const;
+
+ void Serialize(CVariant& value) const override;
+
+ /*!
+ * @brief The database id of this provider
+ *
+ * A unique identifier for this provider.
+ * It can be used to find the same provider on this clients backend
+ *
+ * @return The database id of this provider
+ */
+ int GetDatabaseId() const;
+
+ /*!
+ * @brief Set the database id of this provider
+ * @param iDatabaseId The new ID.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetDatabaseId(int iDatabaseId);
+
+ /*!
+ * @brief A unique identifier for this provider.
+ *
+ * A unique identifier for this provider.
+ * It can be used to find the same provider on this clients backend
+ *
+ * @return The Unique ID.
+ */
+ int GetUniqueId() const;
+
+ /*!
+ * @return The identifier of the client that supplies this provider.
+ */
+ int GetClientId() const;
+
+ /*!
+ * @return The name of the provider. Can be user provided or the backend name
+ */
+ std::string GetName() const;
+
+ /*!
+ * @brief Set the name of the provider.
+ * @param name The new name of the provider.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetName(const std::string& iName);
+
+ /*!
+ * @brief Checks whether this provider has a known type
+ * @return True if this provider has a type other than unknown, false otherwise
+ */
+ bool HasType() const { return m_type != PVR_PROVIDER_TYPE_UNKNOWN; }
+
+ /*!
+ * @brief Gets the type of this provider.
+ * @return the type of this provider.
+ */
+ PVR_PROVIDER_TYPE GetType() const;
+
+ /*!
+ * @brief Sets the type of this provider.
+ * @param type the new provider type.
+ * @return True if the something changed, false otherwise.
+ */
+ bool SetType(PVR_PROVIDER_TYPE type);
+
+ /*!
+ * @brief Get the path for this provider's icon
+ * @return iconpath for this provider's icon
+ */
+ std::string GetIconPath() const;
+
+ /*!
+ * @brief Set the path for this icon
+ * @param strIconPath The new path of the icon.
+ * @return true if the icon path was updated successfully
+ */
+ bool SetIconPath(const std::string& strIconPath);
+
+ /*!
+ * @return Get the path to the icon for this provider as given by the client.
+ */
+ std::string GetClientIconPath() const;
+
+ /*!
+ * @brief Get this provider's country codes (ISO 3166).
+ * @return This provider's country codes.
+ */
+ std::vector<std::string> GetCountries() const;
+
+ /*!
+ * @brief Set the country codes for this provider
+ * @param countries The new ISO 3166 country codes for this provider.
+ * @return true if the country codes were updated successfully
+ */
+ bool SetCountries(const std::vector<std::string>& countries);
+
+ /*!
+ * @brief Get this provider's country codes (ISO 3166) as a string.
+ * @return This provider's country codes.
+ */
+ std::string GetCountriesDBString() const;
+
+ /*!
+ * @brief Set the country codes for this provider from a string
+ * @param strCountries The new ISO 3166 country codes for this provider.
+ * @return true if the country codes were updated successfully
+ */
+ bool SetCountriesFromDBString(const std::string& strCountries);
+
+ /*!
+ * @brief Get this provider's language codes (RFC 5646).
+ * @return This provider's language codes
+ */
+ std::vector<std::string> GetLanguages() const;
+
+ /*!
+ * @brief Set the language codes for this provider
+ * @param languages The new RFC 5646 language codes for this provider.
+ * @return true if the language codes were updated successfully
+ */
+ bool SetLanguages(const std::vector<std::string>& languages);
+
+ /*!
+ * @brief Get this provider's language codes (RFC 5646) as a string.
+ * @return This provider's language codes.
+ */
+ std::string GetLanguagesDBString() const;
+
+ /*!
+ * @brief Set the language codes for this provider from a string
+ * @param strLanguages The new RFC 5646 language codes for this provider.
+ * @return true if the language codes were updated successfully
+ */
+ bool SetLanguagesFromDBString(const std::string& strLanguages);
+
+ /*!
+ * @brief Get if this provider has a thumb image path.
+ * @return True if this add-on provider has a thumb image path, false otherwise.
+ */
+ bool HasThumbPath() const;
+
+ /*!
+ * @brief Get this provider's thumb image path. Note only PVR add-on providers will set this value.
+ * @return This add-on provider's thumb image path.
+ */
+ std::string GetThumbPath() const;
+
+ /*!
+ * @return Get the path to the thumb for this provider as given by the client.
+ */
+ std::string GetClientThumbPath() const;
+
+ /*!
+ * @brief Whether a provider is a default provider of a PVR Client add-on or not
+ * @return True if this provider is of a PVR Client add-on, false otherwise.
+ */
+ bool IsClientProvider() const { return m_bIsClientProvider; }
+
+ /*!
+ * @brief updates this provider from the provided entry
+ * @param fromProvider A provider containing the data that shall be merged into this provider's data.
+ * @param updateMode update as User, Client or DB
+ * @return true if the provider was updated successfully
+ */
+ bool UpdateEntry(const std::shared_ptr<CPVRProvider>& fromProvider,
+ ProviderUpdateMode updateMode);
+
+ /*!
+ * @brief Persist this provider in the local database.
+ * @param updateRecord True if an existing record should be updated, false for an insert
+ * @return True on success, false otherwise.
+ */
+ bool Persist(bool updateRecord = false);
+
+ /*!
+ * @brief Delete this provider from the local database.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteFromDatabase();
+
+private:
+ CPVRProvider(const CPVRProvider& provider) = delete;
+ CPVRProvider& operator=(const CPVRProvider& orig) = delete;
+
+ int m_iDatabaseId = PVR_PROVIDER_INVALID_DB_ID; /*!< the identifier given to this provider by the TV database */
+
+ int m_iUniqueId = PVR_PROVIDER_ADDON_UID; /*!< @brief unique ID of the provider on the backend */
+ int m_iClientId; /*!< @brief ID of the backend */
+ std::string m_strName; /*!< @brief name of this provider */
+ PVR_PROVIDER_TYPE m_type = PVR_PROVIDER_TYPE_UNKNOWN; /*!< @brief service type for this provider */
+ CPVRCachedImage m_iconPath; /*!< @brief the path to the icon for this provider */
+ std::string m_strCountries; /*!< @brief the country codes for this provider (empty if undefined) */
+ std::string m_strLanguages; /*!< @brief the language codes for this provider (empty if undefined) */
+ bool m_bIsClientProvider = false; /*!< the provider is a default provider of a PVR Client add-on */
+ CPVRCachedImage m_thumbPath; /*!< a thumb image path for providers that are PVR add-ons */
+
+ mutable CCriticalSection m_critSection;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/providers/PVRProviders.cpp b/xbmc/pvr/providers/PVRProviders.cpp
new file mode 100644
index 0000000..6d751a2
--- /dev/null
+++ b/xbmc/pvr/providers/PVRProviders.cpp
@@ -0,0 +1,380 @@
+/*
+ * 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 "PVRProviders.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/providers/PVRProvider.h"
+#include "settings/Settings.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+bool CPVRProvidersContainer::UpdateFromClient(const std::shared_ptr<CPVRProvider>& provider)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const std::shared_ptr<CPVRProvider> providerToUpdate =
+ GetByClient(provider->GetClientId(), provider->GetUniqueId());
+ if (providerToUpdate)
+ {
+ return providerToUpdate->UpdateEntry(provider, ProviderUpdateMode::BY_CLIENT);
+ }
+ else
+ {
+ provider->SetDatabaseId(++m_iLastId);
+ InsertEntry(provider, ProviderUpdateMode::BY_CLIENT);
+ }
+
+ return true;
+}
+
+std::shared_ptr<CPVRProvider> CPVRProvidersContainer::GetByClient(int iClientId,
+ int iUniqueId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = std::find_if(
+ m_providers.cbegin(), m_providers.cend(), [iClientId, iUniqueId](const auto& provider) {
+ return provider->GetClientId() == iClientId && provider->GetUniqueId() == iUniqueId;
+ });
+ return it != m_providers.cend() ? (*it) : std::shared_ptr<CPVRProvider>();
+}
+
+void CPVRProvidersContainer::InsertEntry(const std::shared_ptr<CPVRProvider>& newProvider,
+ ProviderUpdateMode updateMode)
+{
+ bool found = false;
+ for (auto& provider : m_providers)
+ {
+ if (provider->GetClientId() == newProvider->GetClientId() &&
+ provider->GetUniqueId() == newProvider->GetUniqueId())
+ {
+ found = true;
+ provider->UpdateEntry(newProvider, updateMode);
+ }
+ }
+
+ if (!found)
+ {
+ m_providers.emplace_back(newProvider);
+ }
+}
+
+std::vector<std::shared_ptr<CPVRProvider>> CPVRProvidersContainer::GetProvidersList() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_providers;
+}
+
+std::size_t CPVRProvidersContainer::GetNumProviders() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_providers.size();
+}
+
+bool CPVRProviders::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ return LoadFromDatabase(clients) && UpdateFromClients(clients);
+}
+
+void CPVRProviders::Unload()
+{
+ // remove all tags
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_providers.clear();
+}
+
+bool CPVRProviders::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ {
+ m_iLastId = database->GetMaxProviderId();
+
+ CPVRProviders providers;
+ database->Get(providers, clients);
+
+ for (auto& provider : providers.GetProvidersList())
+ CheckAndAddEntry(provider, ProviderUpdateMode::BY_DATABASE);
+ }
+ return true;
+}
+
+bool CPVRProviders::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bIsUpdating)
+ return false;
+ m_bIsUpdating = true;
+ }
+
+ // Default client providers are the add-ons themselves, we retrieve both enabled
+ // and disabled add-ons as we don't want them removed from the DB
+ CPVRProviders newAddonProviderList;
+ std::vector<int> disabledClients;
+ std::vector<CVariant> clientProviderInfos =
+ CServiceBroker::GetPVRManager().Clients()->GetClientProviderInfos();
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Adding default providers, found {} PVR add-ons",
+ clientProviderInfos.size());
+ for (const auto& clientInfo : clientProviderInfos)
+ {
+ auto addonProvider = std::make_shared<CPVRProvider>(
+ clientInfo["clientid"].asInteger32(), clientInfo["name"].asString(),
+ clientInfo["icon"].asString(), clientInfo["thumb"].asString());
+
+ newAddonProviderList.CheckAndAddEntry(addonProvider, ProviderUpdateMode::BY_CLIENT);
+
+ if (!clientInfo["enabled"].asBoolean())
+ disabledClients.emplace_back(clientInfo["clientid"].asInteger32());
+ }
+ UpdateDefaultEntries(newAddonProviderList);
+
+ // Client providers are retrieved from the clients
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updating providers");
+ CPVRProvidersContainer newProviderList;
+ std::vector<int> failedClients;
+ CServiceBroker::GetPVRManager().Clients()->GetProviders(clients, &newProviderList, failedClients);
+ return UpdateClientEntries(newProviderList, failedClients, disabledClients);
+}
+
+bool CPVRProviders::UpdateDefaultEntries(const CPVRProvidersContainer& newProviders)
+{
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // go through the provider list and check for updated or new providers
+ const auto newProviderList = newProviders.GetProvidersList();
+ bChanged = std::accumulate(
+ newProviderList.cbegin(), newProviderList.cend(), false,
+ [this](bool changed, const auto& newProvider) {
+ return (CheckAndPersistEntry(newProvider, ProviderUpdateMode::BY_CLIENT) != nullptr)
+ ? true
+ : changed;
+ });
+
+ // check for deleted providers
+ for (std::vector<std::shared_ptr<CPVRProvider>>::iterator it = m_providers.begin();
+ it != m_providers.end();)
+ {
+ const std::shared_ptr<CPVRProvider> provider = *it;
+ if (!newProviders.GetByClient(provider->GetClientId(), provider->GetUniqueId()))
+ {
+ // provider was not found
+ bool bIgnoreProvider = false;
+
+ // ignore add-on any providers that are no PVR Client addon providers
+ if (!provider->IsClientProvider())
+ bIgnoreProvider = true;
+
+ if (bIgnoreProvider)
+ {
+ ++it;
+ continue;
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted provider {} on client {}", provider->GetUniqueId(),
+ provider->GetClientId());
+
+ (*it)->DeleteFromDatabase();
+ it = m_providers.erase(it);
+
+ bChanged |= true;
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ return bChanged;
+}
+
+bool CPVRProviders::UpdateClientEntries(const CPVRProvidersContainer& newProviders,
+ const std::vector<int>& failedClients,
+ const std::vector<int>& disabledClients)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // go through the provider list and check for updated or new providers
+ for (const auto& newProvider : newProviders.GetProvidersList())
+ {
+ CheckAndPersistEntry(newProvider, ProviderUpdateMode::BY_CLIENT);
+ }
+
+ // check for deleted providers
+ for (auto it = m_providers.begin(); it != m_providers.end();)
+ {
+ const std::shared_ptr<CPVRProvider> provider = *it;
+ if (!newProviders.GetByClient(provider->GetClientId(), provider->GetUniqueId()))
+ {
+ const bool bIgnoreProvider =
+ (provider->IsClientProvider() || // ignore add-on providers as they are a special case
+ std::any_of(failedClients.cbegin(), failedClients.cend(),
+ [&provider](const auto& failedClient) {
+ return failedClient == provider->GetClientId();
+ }) ||
+ std::any_of(disabledClients.cbegin(), disabledClients.cend(),
+ [&provider](const auto& disabledClient) {
+ return disabledClient == provider->GetClientId();
+ }));
+ if (bIgnoreProvider)
+ {
+ ++it;
+ continue;
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted provider {} on client {}", provider->GetUniqueId(),
+ provider->GetClientId());
+
+ (*it)->DeleteFromDatabase();
+ it = m_providers.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ m_bIsUpdating = false;
+
+ return true;
+}
+
+std::shared_ptr<CPVRProvider> CPVRProviders::CheckAndAddEntry(
+ const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode)
+{
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::shared_ptr<CPVRProvider> provider =
+ GetByClient(newProvider->GetClientId(), newProvider->GetUniqueId());
+ if (provider)
+ {
+ bChanged = provider->UpdateEntry(newProvider, updateMode);
+ }
+ else
+ {
+ // We don't set an id as this came from the DB so it has one already
+ InsertEntry(newProvider, updateMode);
+
+ if (newProvider->GetDatabaseId() > m_iLastId)
+ m_iLastId = newProvider->GetDatabaseId();
+
+ provider = newProvider;
+ bChanged = true;
+ }
+
+ if (bChanged)
+ return provider;
+
+ return {};
+}
+
+std::shared_ptr<CPVRProvider> CPVRProviders::CheckAndPersistEntry(
+ const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode)
+{
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::shared_ptr<CPVRProvider> provider =
+ GetByClient(newProvider->GetClientId(), newProvider->GetUniqueId());
+ if (provider)
+ {
+ bChanged = provider->UpdateEntry(newProvider, updateMode);
+
+ if (bChanged)
+ provider->Persist(true);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updated provider {} on client {}", newProvider->GetUniqueId(),
+ newProvider->GetClientId());
+ }
+ else
+ {
+ newProvider->SetDatabaseId(++m_iLastId);
+ InsertEntry(newProvider, updateMode);
+
+ newProvider->Persist();
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Added provider {} on client {}", newProvider->GetUniqueId(),
+ newProvider->GetClientId());
+
+ provider = newProvider;
+ bChanged = true;
+ }
+
+ if (bChanged)
+ return provider;
+
+ return {};
+}
+
+bool CPVRProviders::PersistUserChanges(const std::vector<std::shared_ptr<CPVRProvider>>& providers)
+{
+ for (const auto& provider : providers)
+ {
+ provider->Persist(true);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updated provider {} on client {}", provider->GetUniqueId(),
+ provider->GetClientId());
+ }
+
+ return true;
+}
+
+std::shared_ptr<CPVRProvider> CPVRProviders::GetById(int iProviderId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it =
+ std::find_if(m_providers.cbegin(), m_providers.cend(), [iProviderId](const auto& provider) {
+ return provider->GetDatabaseId() == iProviderId;
+ });
+ return it != m_providers.cend() ? (*it) : std::shared_ptr<CPVRProvider>();
+}
+
+void CPVRProviders::RemoveEntry(const std::shared_ptr<CPVRProvider>& provider)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_providers.erase(
+ std::remove_if(m_providers.begin(), m_providers.end(),
+ [&provider](const std::shared_ptr<CPVRProvider>& providerToRemove) {
+ return provider->GetClientId() == providerToRemove->GetClientId() &&
+ provider->GetUniqueId() == providerToRemove->GetUniqueId();
+ }),
+ m_providers.end());
+}
+
+int CPVRProviders::CleanupCachedImages()
+{
+ std::vector<std::string> urlsToCheck;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& provider : m_providers)
+ {
+ urlsToCheck.emplace_back(provider->GetClientIconPath());
+ urlsToCheck.emplace_back(provider->GetClientThumbPath());
+ }
+ }
+
+ static const std::vector<PVRImagePattern> urlPatterns = {{CPVRProvider::IMAGE_OWNER_PATTERN, ""}};
+ return CPVRCachedImages::Cleanup(urlPatterns, urlsToCheck);
+}
diff --git a/xbmc/pvr/providers/PVRProviders.h b/xbmc/pvr/providers/PVRProviders.h
new file mode 100644
index 0000000..8196d3b
--- /dev/null
+++ b/xbmc/pvr/providers/PVRProviders.h
@@ -0,0 +1,139 @@
+/*
+ * 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 <memory>
+#include <vector>
+
+namespace PVR
+{
+class CPVRClient;
+class CPVRProvider;
+
+enum class ProviderUpdateMode;
+
+class CPVRProvidersContainer
+{
+public:
+ /*!
+ * @brief Add a provider to this container or update the provider if already present in this container.
+ * @param The provider
+ * @return True, if the update was successful. False, otherwise.
+ */
+ bool UpdateFromClient(const std::shared_ptr<CPVRProvider>& provider);
+
+ /*!
+ * @brief Get the provider denoted by given client id and unique client provider id.
+ * @param iClientId The client id.
+ * @param iUniqueId The client provider id.
+ * @return the provider if found, null otherwise.
+ */
+ std::shared_ptr<CPVRProvider> GetByClient(int iClientId, int iUniqueId) const;
+
+ /*!
+ * Get all providers in this container
+ * @return The list of all providers
+ */
+ std::vector<std::shared_ptr<CPVRProvider>> GetProvidersList() const;
+
+ /*!
+ * Get the number of providers in this container
+ * @return The total number of providers
+ */
+ std::size_t GetNumProviders() const;
+
+protected:
+ void InsertEntry(const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode);
+
+ mutable CCriticalSection m_critSection;
+ int m_iLastId = 0;
+ std::vector<std::shared_ptr<CPVRProvider>> m_providers;
+};
+
+class CPVRProviders : public CPVRProvidersContainer
+{
+public:
+ CPVRProviders() = default;
+ ~CPVRProviders() = default;
+
+ /**
+ * @brief Update all providers from PVR database and from given clients.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /**
+ * @brief unload all providers.
+ */
+ void Unload();
+
+ /**
+ * @brief Update data with providers from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /**
+ * @brief Load all local providers from PVR database.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief Check if the entry exists in the container, if it does update it otherwise add it
+ * @param newProvider The provider entry to update/add in/to the container
+ * @param updateMode update as Client (respect User set values) or DB (update all values)
+ * @return The provider if updated or added, otherwise an empty object (nullptr)
+ */
+ std::shared_ptr<CPVRProvider> CheckAndAddEntry(const std::shared_ptr<CPVRProvider>& newProvider,
+ ProviderUpdateMode updateMode);
+
+ /*!
+ * @brief Check if the entry exists in the container, if it does update it and persist
+ * it in the DB otherwise add it and persist it in the DB.
+ * @param newProvider The provider entry to update/add in/to the container and DB
+ * @param updateMode update as Client (respect User set values) or DB (update all values)
+ * @return The provider if updated or added, otherwise an empty object (nullptr)
+ */
+ std::shared_ptr<CPVRProvider> CheckAndPersistEntry(
+ const std::shared_ptr<CPVRProvider>& newProvider, ProviderUpdateMode updateMode);
+
+ /**
+ * @brief Persist user changes to the current state of the providers in the DB.
+ */
+ bool PersistUserChanges(const std::vector<std::shared_ptr<CPVRProvider>>& providers);
+
+ /*!
+ * @brief Get a provider given it's database ID
+ * @param iProviderId The ID to find
+ * @return The provider, or an empty one when not found
+ */
+ std::shared_ptr<CPVRProvider> GetById(int iProviderId) const;
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+private:
+ void RemoveEntry(const std::shared_ptr<CPVRProvider>& provider);
+ bool UpdateDefaultEntries(const CPVRProvidersContainer& newProviders);
+ bool UpdateClientEntries(const CPVRProvidersContainer& newProviders,
+ const std::vector<int>& failedClients,
+ const std::vector<int>& disabledClients);
+
+ bool m_bIsUpdating = false;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/recordings/CMakeLists.txt b/xbmc/pvr/recordings/CMakeLists.txt
new file mode 100644
index 0000000..55f7028
--- /dev/null
+++ b/xbmc/pvr/recordings/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES PVRRecording.cpp
+ PVRRecordings.cpp
+ PVRRecordingsPath.cpp)
+
+set(HEADERS PVRRecording.h
+ PVRRecordings.h
+ PVRRecordingsPath.h)
+
+core_add_library(pvr_recordings)
diff --git a/xbmc/pvr/recordings/PVRRecording.cpp b/xbmc/pvr/recordings/PVRRecording.cpp
new file mode 100644
index 0000000..a2e1cbd
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecording.cpp
@@ -0,0 +1,730 @@
+/*
+ * 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 "PVRRecording.h"
+
+#include "ServiceBroker.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_recordings.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/providers/PVRProvider.h"
+#include "pvr/providers/PVRProviders.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+using namespace std::chrono_literals;
+
+CPVRRecordingUid::CPVRRecordingUid(int iClientId, const std::string& strRecordingId)
+ : m_iClientId(iClientId), m_strRecordingId(strRecordingId)
+{
+}
+
+bool CPVRRecordingUid::operator>(const CPVRRecordingUid& right) const
+{
+ return (m_iClientId == right.m_iClientId) ? m_strRecordingId > right.m_strRecordingId
+ : m_iClientId > right.m_iClientId;
+}
+
+bool CPVRRecordingUid::operator<(const CPVRRecordingUid& right) const
+{
+ return (m_iClientId == right.m_iClientId) ? m_strRecordingId < right.m_strRecordingId
+ : m_iClientId < right.m_iClientId;
+}
+
+bool CPVRRecordingUid::operator==(const CPVRRecordingUid& right) const
+{
+ return m_iClientId == right.m_iClientId && m_strRecordingId == right.m_strRecordingId;
+}
+
+bool CPVRRecordingUid::operator!=(const CPVRRecordingUid& right) const
+{
+ return m_iClientId != right.m_iClientId || m_strRecordingId != right.m_strRecordingId;
+}
+
+const std::string CPVRRecording::IMAGE_OWNER_PATTERN = "pvrrecording";
+
+CPVRRecording::CPVRRecording()
+ : m_iconPath(IMAGE_OWNER_PATTERN),
+ m_thumbnailPath(IMAGE_OWNER_PATTERN),
+ m_fanartPath(IMAGE_OWNER_PATTERN)
+{
+ Reset();
+}
+
+CPVRRecording::CPVRRecording(const PVR_RECORDING& recording, unsigned int iClientId)
+ : m_iconPath(recording.strIconPath, IMAGE_OWNER_PATTERN),
+ m_thumbnailPath(recording.strThumbnailPath, IMAGE_OWNER_PATTERN),
+ m_fanartPath(recording.strFanartPath, IMAGE_OWNER_PATTERN)
+{
+ Reset();
+
+ m_strRecordingId = recording.strRecordingId;
+ m_strTitle = recording.strTitle;
+ m_strShowTitle = recording.strEpisodeName;
+ m_iSeason = recording.iSeriesNumber;
+ m_iEpisode = recording.iEpisodeNumber;
+ if (recording.iYear > 0)
+ SetYear(recording.iYear);
+ m_iClientId = iClientId;
+ m_recordingTime =
+ recording.recordingTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection;
+ m_iPriority = recording.iPriority;
+ m_iLifetime = recording.iLifetime;
+ // Deleted recording is placed at the root of the deleted view
+ m_strDirectory = recording.bIsDeleted ? "" : recording.strDirectory;
+ m_strPlot = recording.strPlot;
+ m_strPlotOutline = recording.strPlotOutline;
+ m_strChannelName = recording.strChannelName;
+ m_bIsDeleted = recording.bIsDeleted;
+ m_iEpgEventId = recording.iEpgEventId;
+ m_iChannelUid = recording.iChannelUid;
+ if (strlen(recording.strFirstAired) > 0)
+ m_firstAired.SetFromW3CDateTime(recording.strFirstAired);
+ m_iFlags = recording.iFlags;
+ if (recording.sizeInBytes >= 0)
+ m_sizeInBytes = recording.sizeInBytes;
+ m_strProviderName = recording.strProviderName;
+ m_iClientProviderUniqueId = recording.iClientProviderUid;
+
+ SetGenre(recording.iGenreType, recording.iGenreSubType, recording.strGenreDescription);
+ CVideoInfoTag::SetPlayCount(recording.iPlayCount);
+ if (recording.iLastPlayedPosition > 0 && recording.iDuration > recording.iLastPlayedPosition)
+ CVideoInfoTag::SetResumePoint(recording.iLastPlayedPosition, recording.iDuration, "");
+ SetDuration(recording.iDuration);
+
+ // As the channel a recording was done on (probably long time ago) might no longer be
+ // available today prefer addon-supplied channel type (tv/radio) over channel attribute.
+ if (recording.channelType != PVR_RECORDING_CHANNEL_TYPE_UNKNOWN)
+ {
+ m_bRadio = recording.channelType == PVR_RECORDING_CHANNEL_TYPE_RADIO;
+ }
+ else
+ {
+ const std::shared_ptr<CPVRChannel> channel(Channel());
+ if (channel)
+ {
+ m_bRadio = channel->IsRadio();
+ }
+ else
+ {
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ bool bSupportsRadio = client && client->GetClientCapabilities().SupportsRadio();
+ if (bSupportsRadio && client && client->GetClientCapabilities().SupportsTV())
+ {
+ CLog::Log(LOGWARNING, "Unable to determine channel type. Defaulting to TV.");
+ m_bRadio = false; // Assume TV.
+ }
+ else
+ {
+ m_bRadio = bSupportsRadio;
+ }
+ }
+ }
+
+ UpdatePath();
+}
+
+bool CPVRRecording::operator==(const CPVRRecording& right) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return (this == &right) ||
+ (m_strRecordingId == right.m_strRecordingId && m_iClientId == right.m_iClientId &&
+ m_strChannelName == right.m_strChannelName && m_recordingTime == right.m_recordingTime &&
+ GetDuration() == right.GetDuration() && m_strPlotOutline == right.m_strPlotOutline &&
+ m_strPlot == right.m_strPlot && m_iPriority == right.m_iPriority &&
+ m_iLifetime == right.m_iLifetime && m_strDirectory == right.m_strDirectory &&
+ m_strFileNameAndPath == right.m_strFileNameAndPath && m_strTitle == right.m_strTitle &&
+ m_strShowTitle == right.m_strShowTitle && m_iSeason == right.m_iSeason &&
+ m_iEpisode == right.m_iEpisode && GetPremiered() == right.GetPremiered() &&
+ m_iconPath == right.m_iconPath && m_thumbnailPath == right.m_thumbnailPath &&
+ m_fanartPath == right.m_fanartPath && m_iRecordingId == right.m_iRecordingId &&
+ m_bIsDeleted == right.m_bIsDeleted && m_iEpgEventId == right.m_iEpgEventId &&
+ m_iChannelUid == right.m_iChannelUid && m_bRadio == right.m_bRadio &&
+ m_genre == right.m_genre && m_iGenreType == right.m_iGenreType &&
+ m_iGenreSubType == right.m_iGenreSubType && m_firstAired == right.m_firstAired &&
+ m_iFlags == right.m_iFlags && m_sizeInBytes == right.m_sizeInBytes &&
+ m_strProviderName == right.m_strProviderName &&
+ m_iClientProviderUniqueId == right.m_iClientProviderUniqueId);
+}
+
+bool CPVRRecording::operator!=(const CPVRRecording& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRRecording::FillAddonData(PVR_RECORDING& recording) const
+{
+ time_t recTime;
+ RecordingTimeAsUTC().GetAsTime(recTime);
+
+ recording = {};
+ strncpy(recording.strRecordingId, ClientRecordingID().c_str(),
+ sizeof(recording.strRecordingId) - 1);
+ strncpy(recording.strTitle, m_strTitle.c_str(), sizeof(recording.strTitle) - 1);
+ strncpy(recording.strEpisodeName, m_strShowTitle.c_str(), sizeof(recording.strEpisodeName) - 1);
+ recording.iSeriesNumber = m_iSeason;
+ recording.iEpisodeNumber = m_iEpisode;
+ recording.iYear = GetYear();
+ strncpy(recording.strDirectory, Directory().c_str(), sizeof(recording.strDirectory) - 1);
+ strncpy(recording.strPlotOutline, m_strPlotOutline.c_str(), sizeof(recording.strPlotOutline) - 1);
+ strncpy(recording.strPlot, m_strPlot.c_str(), sizeof(recording.strPlot) - 1);
+ strncpy(recording.strGenreDescription, GetGenresLabel().c_str(),
+ sizeof(recording.strGenreDescription) - 1);
+ strncpy(recording.strChannelName, ChannelName().c_str(), sizeof(recording.strChannelName) - 1);
+ strncpy(recording.strIconPath, ClientIconPath().c_str(), sizeof(recording.strIconPath) - 1);
+ strncpy(recording.strThumbnailPath, ClientThumbnailPath().c_str(),
+ sizeof(recording.strThumbnailPath) - 1);
+ strncpy(recording.strFanartPath, ClientFanartPath().c_str(), sizeof(recording.strFanartPath) - 1);
+ recording.recordingTime =
+ recTime - CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection;
+ recording.iDuration = GetDuration();
+ recording.iPriority = Priority();
+ recording.iLifetime = LifeTime();
+ recording.iGenreType = GenreType();
+ recording.iGenreSubType = GenreSubType();
+ recording.iPlayCount = GetLocalPlayCount();
+ recording.iLastPlayedPosition = std::lrint(GetLocalResumePoint().timeInSeconds);
+ recording.bIsDeleted = IsDeleted();
+ recording.iEpgEventId = m_iEpgEventId;
+ recording.iChannelUid = ChannelUid();
+ recording.channelType =
+ IsRadio() ? PVR_RECORDING_CHANNEL_TYPE_RADIO : PVR_RECORDING_CHANNEL_TYPE_TV;
+ if (FirstAired().IsValid())
+ strncpy(recording.strFirstAired, FirstAired().GetAsW3CDate().c_str(),
+ sizeof(recording.strFirstAired) - 1);
+ recording.iFlags = Flags();
+ recording.sizeInBytes = GetSizeInBytes();
+ strncpy(recording.strProviderName, ProviderName().c_str(), sizeof(recording.strProviderName) - 1);
+ recording.iClientProviderUid = ClientProviderUniqueId();
+}
+
+void CPVRRecording::Serialize(CVariant& value) const
+{
+ CVideoInfoTag::Serialize(value);
+
+ value["channel"] = m_strChannelName;
+ value["lifetime"] = m_iLifetime;
+ value["directory"] = m_strDirectory;
+ value["icon"] = ClientIconPath();
+ value["starttime"] = m_recordingTime.IsValid() ? m_recordingTime.GetAsDBDateTime() : "";
+ value["endtime"] = m_recordingTime.IsValid() ? EndTimeAsUTC().GetAsDBDateTime() : "";
+ value["recordingid"] = m_iRecordingId;
+ value["isdeleted"] = m_bIsDeleted;
+ value["epgeventid"] = m_iEpgEventId;
+ value["channeluid"] = m_iChannelUid;
+ value["radio"] = m_bRadio;
+ value["genre"] = m_genre;
+
+ if (!value.isMember("art"))
+ value["art"] = CVariant(CVariant::VariantTypeObject);
+ if (!ClientThumbnailPath().empty())
+ value["art"]["thumb"] = ClientThumbnailPath();
+ if (!ClientFanartPath().empty())
+ value["art"]["fanart"] = ClientFanartPath();
+
+ value["clientid"] = m_iClientId;
+}
+
+void CPVRRecording::ToSortable(SortItem& sortable, Field field) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (field == FieldSize)
+ sortable[FieldSize] = m_sizeInBytes;
+ else if (field == FieldProvider)
+ sortable[FieldProvider] = StringUtils::Format("{} {}", m_iClientId, m_iClientProviderUniqueId);
+ else
+ CVideoInfoTag::ToSortable(sortable, field);
+}
+
+void CPVRRecording::Reset()
+{
+ m_strRecordingId.clear();
+ m_iClientId = -1;
+ m_strChannelName.clear();
+ m_strDirectory.clear();
+ m_iPriority = -1;
+ m_iLifetime = -1;
+ m_strFileNameAndPath.clear();
+ m_bGotMetaData = false;
+ m_iRecordingId = 0;
+ m_bIsDeleted = false;
+ m_bInProgress = true;
+ m_iEpgEventId = EPG_TAG_INVALID_UID;
+ m_iSeason = -1;
+ m_iEpisode = -1;
+ m_iChannelUid = PVR_CHANNEL_INVALID_UID;
+ m_bRadio = false;
+ m_iFlags = PVR_RECORDING_FLAG_UNDEFINED;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_sizeInBytes = 0;
+ }
+ m_strProviderName.clear();
+ m_iClientProviderUniqueId = PVR_PROVIDER_INVALID_UID;
+
+ m_recordingTime.Reset();
+ CVideoInfoTag::Reset();
+}
+
+bool CPVRRecording::Delete()
+{
+ std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ return client && (client->DeleteRecording(*this) == PVR_ERROR_NO_ERROR);
+}
+
+bool CPVRRecording::Undelete()
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ return client && (client->UndeleteRecording(*this) == PVR_ERROR_NO_ERROR);
+}
+
+bool CPVRRecording::Rename(const std::string& strNewName)
+{
+ m_strTitle = strNewName;
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ return client && (client->RenameRecording(*this) == PVR_ERROR_NO_ERROR);
+}
+
+bool CPVRRecording::SetPlayCount(int count)
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsPlayCount())
+ {
+ if (client->SetRecordingPlayCount(*this, count) != PVR_ERROR_NO_ERROR)
+ return false;
+ }
+
+ return CVideoInfoTag::SetPlayCount(count);
+}
+
+bool CPVRRecording::IncrementPlayCount()
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsPlayCount())
+ {
+ if (client->SetRecordingPlayCount(*this, CVideoInfoTag::GetPlayCount() + 1) !=
+ PVR_ERROR_NO_ERROR)
+ return false;
+ }
+
+ return CVideoInfoTag::IncrementPlayCount();
+}
+
+bool CPVRRecording::SetResumePoint(const CBookmark& resumePoint)
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsLastPlayedPosition())
+ {
+ if (client->SetRecordingLastPlayedPosition(*this, lrint(resumePoint.timeInSeconds)) !=
+ PVR_ERROR_NO_ERROR)
+ return false;
+ }
+
+ return CVideoInfoTag::SetResumePoint(resumePoint);
+}
+
+bool CPVRRecording::SetResumePoint(double timeInSeconds,
+ double totalTimeInSeconds,
+ const std::string& playerState /* = "" */)
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsLastPlayedPosition())
+ {
+ if (client->SetRecordingLastPlayedPosition(*this, lrint(timeInSeconds)) != PVR_ERROR_NO_ERROR)
+ return false;
+ }
+
+ return CVideoInfoTag::SetResumePoint(timeInSeconds, totalTimeInSeconds, playerState);
+}
+
+CBookmark CPVRRecording::GetResumePoint() const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsLastPlayedPosition() &&
+ m_resumePointRefetchTimeout.IsTimePast())
+ {
+ // @todo: root cause should be fixed. details: https://github.com/xbmc/xbmc/pull/14961
+ m_resumePointRefetchTimeout.Set(10s); // update resume point from backend at most every 10 secs
+
+ int pos = -1;
+ client->GetRecordingLastPlayedPosition(*this, pos);
+
+ if (pos >= 0)
+ {
+ CBookmark resumePoint(CVideoInfoTag::GetResumePoint());
+ resumePoint.timeInSeconds = pos;
+ resumePoint.totalTimeInSeconds = (pos == 0) ? 0 : m_duration;
+ CPVRRecording* pThis = const_cast<CPVRRecording*>(this);
+ pThis->CVideoInfoTag::SetResumePoint(resumePoint);
+ }
+ }
+ return CVideoInfoTag::GetResumePoint();
+}
+
+bool CPVRRecording::UpdateRecordingSize()
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsSize() &&
+ m_recordingSizeRefetchTimeout.IsTimePast())
+ {
+ // @todo: root cause should be fixed. details: https://github.com/xbmc/xbmc/pull/14961
+ m_recordingSizeRefetchTimeout.Set(10s); // update size from backend at most every 10 secs
+
+ int64_t sizeInBytes = -1;
+ client->GetRecordingSize(*this, sizeInBytes);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (sizeInBytes >= 0 && sizeInBytes != m_sizeInBytes)
+ {
+ m_sizeInBytes = sizeInBytes;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void CPVRRecording::UpdateMetadata(CVideoDatabase& db, const CPVRClient& client)
+{
+ if (m_bGotMetaData || !db.IsOpen())
+ return;
+
+ if (!client.GetClientCapabilities().SupportsRecordingsPlayCount())
+ CVideoInfoTag::SetPlayCount(db.GetPlayCount(m_strFileNameAndPath));
+
+ if (!client.GetClientCapabilities().SupportsRecordingsLastPlayedPosition())
+ {
+ CBookmark resumePoint;
+ if (db.GetResumeBookMark(m_strFileNameAndPath, resumePoint))
+ CVideoInfoTag::SetResumePoint(resumePoint);
+ }
+
+ m_lastPlayed = db.GetLastPlayed(m_strFileNameAndPath);
+
+ m_bGotMetaData = true;
+}
+
+std::vector<PVR_EDL_ENTRY> CPVRRecording::GetEdl() const
+{
+ std::vector<PVR_EDL_ENTRY> edls;
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsRecordingsEdl())
+ client->GetRecordingEdl(*this, edls);
+
+ return edls;
+}
+
+void CPVRRecording::Update(const CPVRRecording& tag, const CPVRClient& client)
+{
+ m_strRecordingId = tag.m_strRecordingId;
+ m_iClientId = tag.m_iClientId;
+ m_strTitle = tag.m_strTitle;
+ m_strShowTitle = tag.m_strShowTitle;
+ m_iSeason = tag.m_iSeason;
+ m_iEpisode = tag.m_iEpisode;
+ SetPremiered(tag.GetPremiered());
+ m_recordingTime = tag.m_recordingTime;
+ m_iPriority = tag.m_iPriority;
+ m_iLifetime = tag.m_iLifetime;
+ m_strDirectory = tag.m_strDirectory;
+ m_strPlot = tag.m_strPlot;
+ m_strPlotOutline = tag.m_strPlotOutline;
+ m_strChannelName = tag.m_strChannelName;
+ m_genre = tag.m_genre;
+ m_iconPath = tag.m_iconPath;
+ m_thumbnailPath = tag.m_thumbnailPath;
+ m_fanartPath = tag.m_fanartPath;
+ m_bIsDeleted = tag.m_bIsDeleted;
+ m_iEpgEventId = tag.m_iEpgEventId;
+ m_iChannelUid = tag.m_iChannelUid;
+ m_bRadio = tag.m_bRadio;
+ m_firstAired = tag.m_firstAired;
+ m_iFlags = tag.m_iFlags;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_sizeInBytes = tag.m_sizeInBytes;
+ m_strProviderName = tag.m_strProviderName;
+ m_iClientProviderUniqueId = tag.m_iClientProviderUniqueId;
+ }
+
+ if (client.GetClientCapabilities().SupportsRecordingsPlayCount())
+ CVideoInfoTag::SetPlayCount(tag.GetLocalPlayCount());
+
+ if (client.GetClientCapabilities().SupportsRecordingsLastPlayedPosition())
+ CVideoInfoTag::SetResumePoint(tag.GetLocalResumePoint());
+
+ SetDuration(tag.GetDuration());
+
+ if (m_iGenreType == EPG_GENRE_USE_STRING || m_iGenreSubType == EPG_GENRE_USE_STRING)
+ {
+ /* No type/subtype. Use the provided description */
+ m_genre = tag.m_genre;
+ }
+ else
+ {
+ /* Determine genre description by type/subtype */
+ m_genre = StringUtils::Split(
+ CPVREpg::ConvertGenreIdToString(tag.m_iGenreType, tag.m_iGenreSubType),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+
+ //Old Method of identifying TV show title and subtitle using m_strDirectory and strPlotOutline (deprecated)
+ std::string strShow = StringUtils::Format("{} - ", g_localizeStrings.Get(20364));
+ if (StringUtils::StartsWithNoCase(m_strPlotOutline, strShow))
+ {
+ CLog::Log(LOGWARNING, "PVR addon provides episode name in strPlotOutline which is deprecated");
+ std::string strEpisode = m_strPlotOutline;
+ std::string strTitle = m_strDirectory;
+
+ size_t pos = strTitle.rfind('/');
+ strTitle.erase(0, pos + 1);
+ strEpisode.erase(0, strShow.size());
+ m_strTitle = strTitle;
+ pos = strEpisode.find('-');
+ strEpisode.erase(0, pos + 2);
+ m_strShowTitle = strEpisode;
+ }
+
+ UpdatePath();
+}
+
+void CPVRRecording::UpdatePath()
+{
+ m_strFileNameAndPath = CPVRRecordingsPath(m_bIsDeleted, m_bRadio, m_strDirectory, m_strTitle,
+ m_iSeason, m_iEpisode, GetYear(), m_strShowTitle,
+ m_strChannelName, m_recordingTime, m_strRecordingId);
+}
+
+const CDateTime& CPVRRecording::RecordingTimeAsLocalTime() const
+{
+ static CDateTime tmp;
+ tmp.SetFromUTCDateTime(m_recordingTime);
+
+ return tmp;
+}
+
+CDateTime CPVRRecording::EndTimeAsUTC() const
+{
+ unsigned int duration = GetDuration();
+ return m_recordingTime + CDateTimeSpan(0, 0, duration / 60, duration % 60);
+}
+
+CDateTime CPVRRecording::EndTimeAsLocalTime() const
+{
+ CDateTime ret;
+ ret.SetFromUTCDateTime(EndTimeAsUTC());
+ return ret;
+}
+
+bool CPVRRecording::WillBeExpiredWithNewLifetime(int iLifetime) const
+{
+ if (iLifetime > 0)
+ return (EndTimeAsUTC() + CDateTimeSpan(iLifetime, 0, 0, 0)) <= CDateTime::GetUTCDateTime();
+
+ return false;
+}
+
+CDateTime CPVRRecording::ExpirationTimeAsLocalTime() const
+{
+ CDateTime ret;
+ if (m_iLifetime > 0)
+ ret = EndTimeAsLocalTime() + CDateTimeSpan(m_iLifetime, 0, 0, 0);
+
+ return ret;
+}
+
+std::string CPVRRecording::GetTitleFromURL(const std::string& url)
+{
+ return CPVRRecordingsPath(url).GetTitle();
+}
+
+std::shared_ptr<CPVRChannel> CPVRRecording::Channel() const
+{
+ if (m_iChannelUid != PVR_CHANNEL_INVALID_UID)
+ return CServiceBroker::GetPVRManager().ChannelGroups()->GetByUniqueID(m_iChannelUid,
+ m_iClientId);
+
+ return std::shared_ptr<CPVRChannel>();
+}
+
+int CPVRRecording::ChannelUid() const
+{
+ return m_iChannelUid;
+}
+
+int CPVRRecording::ClientID() const
+{
+ return m_iClientId;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRRecording::GetRecordingTimer() const
+{
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>> recordingTimers =
+ CServiceBroker::GetPVRManager().Timers()->GetActiveRecordings();
+
+ for (const auto& timer : recordingTimers)
+ {
+ if (timer->ClientID() == ClientID() && timer->ClientChannelUID() == ChannelUid())
+ {
+ // first, match epg event uids, if available
+ if (timer->UniqueBroadcastID() == BroadcastUid() &&
+ timer->UniqueBroadcastID() != EPG_TAG_INVALID_UID)
+ return timer;
+
+ // alternatively, match start and end times
+ const CDateTime timerStart =
+ timer->StartAsUTC() - CDateTimeSpan(0, 0, timer->MarginStart(), 0);
+ const CDateTime timerEnd = timer->EndAsUTC() + CDateTimeSpan(0, 0, timer->MarginEnd(), 0);
+ if (timerStart <= RecordingTimeAsUTC() && timerEnd >= EndTimeAsUTC())
+ return timer;
+ }
+ }
+ return {};
+}
+
+bool CPVRRecording::IsInProgress() const
+{
+ // Note: It is not enough to only check recording time and duration against 'now'.
+ // Only the state of the related timer is a safe indicator that the backend
+ // actually is recording this.
+ // Once the recording is known to not be in progress that will never change.
+ if (m_bInProgress)
+ m_bInProgress = GetRecordingTimer() != nullptr;
+ return m_bInProgress;
+}
+
+void CPVRRecording::SetGenre(int iGenreType, int iGenreSubType, const std::string& strGenre)
+{
+ m_iGenreType = iGenreType;
+ m_iGenreSubType = iGenreSubType;
+
+ if ((iGenreType == EPG_GENRE_USE_STRING || iGenreSubType == EPG_GENRE_USE_STRING) &&
+ !strGenre.empty())
+ {
+ /* Type and sub type are not given. Use the provided genre description if available. */
+ m_genre = StringUtils::Split(
+ strGenre,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+ else
+ {
+ /* Determine the genre description from the type and subtype IDs */
+ m_genre = StringUtils::Split(
+ CPVREpg::ConvertGenreIdToString(iGenreType, iGenreSubType),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+}
+
+const std::string CPVRRecording::GetGenresLabel() const
+{
+ return StringUtils::Join(
+ m_genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+}
+
+CDateTime CPVRRecording::FirstAired() const
+{
+ return m_firstAired;
+}
+
+void CPVRRecording::SetYear(int year)
+{
+ if (year > 0)
+ m_premiered = CDateTime(year, 1, 1, 0, 0, 0);
+}
+
+int CPVRRecording::GetYear() const
+{
+ return m_premiered.IsValid() ? m_premiered.GetYear() : 0;
+}
+
+bool CPVRRecording::HasYear() const
+{
+ return m_premiered.IsValid();
+}
+
+bool CPVRRecording::IsNew() const
+{
+ return (m_iFlags & PVR_RECORDING_FLAG_IS_NEW) > 0;
+}
+
+bool CPVRRecording::IsPremiere() const
+{
+ return (m_iFlags & PVR_RECORDING_FLAG_IS_PREMIERE) > 0;
+}
+
+bool CPVRRecording::IsLive() const
+{
+ return (m_iFlags & PVR_RECORDING_FLAG_IS_LIVE) > 0;
+}
+
+bool CPVRRecording::IsFinale() const
+{
+ return (m_iFlags & PVR_RECORDING_FLAG_IS_FINALE) > 0;
+}
+
+int64_t CPVRRecording::GetSizeInBytes() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_sizeInBytes;
+}
+
+int CPVRRecording::ClientProviderUniqueId() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientProviderUniqueId;
+}
+
+std::string CPVRRecording::ProviderName() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_strProviderName;
+}
+
+std::shared_ptr<CPVRProvider> CPVRRecording::GetDefaultProvider() const
+{
+ return CServiceBroker::GetPVRManager().Providers()->GetByClient(m_iClientId,
+ PVR_PROVIDER_INVALID_UID);
+}
+
+bool CPVRRecording::HasClientProvider() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iClientProviderUniqueId != PVR_PROVIDER_INVALID_UID;
+}
+
+std::shared_ptr<CPVRProvider> CPVRRecording::GetProvider() const
+{
+ auto provider = CServiceBroker::GetPVRManager().Providers()->GetByClient(
+ m_iClientId, m_iClientProviderUniqueId);
+
+ if (!provider)
+ provider = GetDefaultProvider();
+
+ return provider;
+}
diff --git a/xbmc/pvr/recordings/PVRRecording.h b/xbmc/pvr/recordings/PVRRecording.h
new file mode 100644
index 0000000..bdd8378
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecording.h
@@ -0,0 +1,541 @@
+/*
+ * 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 "XBDateTime.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_providers.h"
+#include "pvr/PVRCachedImage.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "video/Bookmark.h"
+#include "video/VideoInfoTag.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CVideoDatabase;
+
+struct PVR_EDL_ENTRY;
+struct PVR_RECORDING;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRClient;
+class CPVRProvider;
+class CPVRTimerInfoTag;
+
+/*!
+ * @brief Representation of a CPVRRecording unique ID.
+ */
+class CPVRRecordingUid final
+{
+public:
+ int m_iClientId; /*!< ID of the backend */
+ std::string m_strRecordingId; /*!< unique ID of the recording on the client */
+
+ CPVRRecordingUid(int iClientId, const std::string& strRecordingId);
+
+ bool operator>(const CPVRRecordingUid& right) const;
+ bool operator<(const CPVRRecordingUid& right) const;
+ bool operator==(const CPVRRecordingUid& right) const;
+ bool operator!=(const CPVRRecordingUid& right) const;
+};
+
+class CPVRRecording final : public CVideoInfoTag
+{
+public:
+ static const std::string IMAGE_OWNER_PATTERN;
+
+ CPVRRecording();
+ CPVRRecording(const PVR_RECORDING& recording, unsigned int iClientId);
+
+ bool operator==(const CPVRRecording& right) const;
+ bool operator!=(const CPVRRecording& right) const;
+
+ /*!
+ * @brief Copy over data to the given PVR_RECORDING instance.
+ * @param recording The recording instance to fill.
+ */
+ void FillAddonData(PVR_RECORDING& recording) const;
+
+ void Serialize(CVariant& value) const override;
+
+ // ISortable implementation
+ void ToSortable(SortItem& sortable, Field field) const override;
+
+ /*!
+ * @brief Reset this tag to it's initial state.
+ */
+ void Reset();
+
+ /*!
+ * @brief Delete this recording on the client (if supported).
+ * @return True if it was deleted successfully, false otherwise.
+ */
+ bool Delete();
+
+ /*!
+ * @brief Undelete this recording on the client (if supported).
+ * @return True if it was undeleted successfully, false otherwise.
+ */
+ bool Undelete();
+
+ /*!
+ * @brief Rename this recording on the client (if supported).
+ * @param strNewName The new name.
+ * @return True if it was renamed successfully, false otherwise.
+ */
+ bool Rename(const std::string& strNewName);
+
+ /*!
+ * @brief Set this recording's play count. The value will be transferred to the backend if it supports server-side play counts.
+ * @param count play count.
+ * @return True if play count was set successfully, false otherwise.
+ */
+ bool SetPlayCount(int count) override;
+
+ /*!
+ * @brief Increment this recording's play count. The value will be transferred to the backend if it supports server-side play counts.
+ * @return True if play count was increased successfully, false otherwise.
+ */
+ bool IncrementPlayCount() override;
+
+ /*!
+ * @brief Set this recording's play count without transferring the value to the backend, even if it supports server-side play counts.
+ * @param count play count.
+ * @return True if play count was set successfully, false otherwise.
+ */
+ bool SetLocalPlayCount(int count) { return CVideoInfoTag::SetPlayCount(count); }
+
+ /*!
+ * @brief Get this recording's local play count. The value will not be obtained from the backend, even if it supports server-side play counts.
+ * @return the play count.
+ */
+ int GetLocalPlayCount() const { return CVideoInfoTag::GetPlayCount(); }
+
+ /*!
+ * @brief Set this recording's resume point. The value will be transferred to the backend if it supports server-side resume points.
+ * @param resumePoint resume point.
+ * @return True if resume point was set successfully, false otherwise.
+ */
+ bool SetResumePoint(const CBookmark& resumePoint) override;
+
+ /*!
+ * @brief Set this recording's resume point. The value will be transferred to the backend if it supports server-side resume points.
+ * @param timeInSeconds the time of the resume point
+ * @param totalTimeInSeconds the total time of the video
+ * @param playerState the player state
+ * @return True if resume point was set successfully, false otherwise.
+ */
+ bool SetResumePoint(double timeInSeconds,
+ double totalTimeInSeconds,
+ const std::string& playerState = "") override;
+
+ /*!
+ * @brief Get this recording's resume point. The value will be obtained from the backend if it supports server-side resume points.
+ * @return the resume point.
+ */
+ CBookmark GetResumePoint() const override;
+
+ /*!
+ * @brief Update this recording's size. The value will be obtained from the backend if it supports server-side size retrieval.
+ * @return true if the the updated value is different, false otherwise.
+ */
+ bool UpdateRecordingSize();
+
+ /*!
+ * @brief Get this recording's local resume point. The value will not be obtained from the backend even if it supports server-side resume points.
+ * @return the resume point.
+ */
+ CBookmark GetLocalResumePoint() const { return CVideoInfoTag::GetResumePoint(); }
+
+ /*!
+ * @brief Retrieve the edit decision list (EDL) of a recording on the backend.
+ * @return The edit decision list (empty on error)
+ */
+ std::vector<PVR_EDL_ENTRY> GetEdl() const;
+
+ /*!
+ * @brief Get the resume point and play count from the database if the
+ * client doesn't handle it itself.
+ * @param db The database to read the data from.
+ * @param client The client this recording belongs to.
+ */
+ void UpdateMetadata(CVideoDatabase& db, const CPVRClient& client);
+
+ /*!
+ * @brief Update this tag with the contents of the given tag.
+ * @param tag The new tag info.
+ * @param client The client this recording belongs to.
+ */
+ void Update(const CPVRRecording& tag, const CPVRClient& client);
+
+ /*!
+ * @brief Retrieve the recording start as UTC time
+ * @return the recording start time
+ */
+ const CDateTime& RecordingTimeAsUTC() const { return m_recordingTime; }
+
+ /*!
+ * @brief Retrieve the recording start as local time
+ * @return the recording start time
+ */
+ const CDateTime& RecordingTimeAsLocalTime() const;
+
+ /*!
+ * @brief Retrieve the recording end as UTC time
+ * @return the recording end time
+ */
+ CDateTime EndTimeAsUTC() const;
+
+ /*!
+ * @brief Retrieve the recording end as local time
+ * @return the recording end time
+ */
+ CDateTime EndTimeAsLocalTime() const;
+
+ /*!
+ * @brief Check whether this recording has an expiration time
+ * @return True if the recording has an expiration time, false otherwise
+ */
+ bool HasExpirationTime() const { return m_iLifetime > 0; }
+
+ /*!
+ * @brief Retrieve the recording expiration time as local time
+ * @return the recording expiration time
+ */
+ CDateTime ExpirationTimeAsLocalTime() const;
+
+ /*!
+ * @brief Check whether this recording will immediately expire if the given lifetime value would be set
+ * @param iLifetime The lifetime value to check
+ * @return True if the recording would immediately expire, false otherwiese
+ */
+ bool WillBeExpiredWithNewLifetime(int iLifetime) const;
+
+ /*!
+ * @brief Retrieve the recording title from the URL path
+ * @param url the URL for the recording
+ * @return Title of the recording
+ */
+ static std::string GetTitleFromURL(const std::string& url);
+
+ /*!
+ * @brief If deleted but can be undeleted it is true
+ */
+ bool IsDeleted() const { return m_bIsDeleted; }
+
+ /*!
+ * @brief Check whether this is a tv or radio recording
+ * @return true if this is a radio recording, false if this is a tv recording
+ */
+ bool IsRadio() const { return m_bRadio; }
+
+ /*!
+ * @return Broadcast id of the EPG event associated with this recording or EPG_TAG_INVALID_UID
+ */
+ unsigned int BroadcastUid() const { return m_iEpgEventId; }
+
+ /*!
+ * @return Get the channel on which this recording is/was running
+ * @note Only works if the recording has a channel uid provided by the add-on
+ */
+ std::shared_ptr<CPVRChannel> Channel() const;
+
+ /*!
+ * @brief Get the uid of the channel on which this recording is/was running
+ * @return the uid of the channel or PVR_CHANNEL_INVALID_UID
+ */
+ int ChannelUid() const;
+
+ /*!
+ * @brief the identifier of the client that serves this recording
+ * @return the client identifier
+ */
+ int ClientID() const;
+
+ /*!
+ * @brief Get the recording ID as upplied by the client
+ * @return the recording identifier
+ */
+ std::string ClientRecordingID() const { return m_strRecordingId; }
+
+ /*!
+ * @brief Get the recording ID as upplied by the client
+ * @return the recording identifier
+ */
+ unsigned int RecordingID() const { return m_iRecordingId; }
+
+ /*!
+ * @brief Set the recording ID
+ * @param recordingId The new identifier
+ */
+ void SetRecordingID(unsigned int recordingId) { m_iRecordingId = recordingId; }
+
+ /*!
+ * @brief Get the directory for this recording
+ * @return the directory
+ */
+ std::string Directory() const { return m_strDirectory; }
+
+ /*!
+ * @brief Get the priority for this recording
+ * @return the priority
+ */
+ int Priority() const { return m_iPriority; }
+
+ /*!
+ * @brief Get the lifetime for this recording
+ * @return the lifetime
+ */
+ int LifeTime() const { return m_iLifetime; }
+
+ /*!
+ * @brief Set the lifetime for this recording
+ * @param lifeTime The lifetime
+ */
+ void SetLifeTime(int lifeTime) { m_iLifetime = lifeTime; }
+
+ /*!
+ * @brief Get the channel name for this recording
+ * @return the channel name
+ */
+ std::string ChannelName() const { return m_strChannelName; }
+
+ /*!
+ * @brief Return the icon path as given by the client.
+ * @return The path.
+ */
+ const std::string& ClientIconPath() const { return m_iconPath.GetClientImage(); }
+
+ /*!
+ * @brief Return the thumbnail path as given by the client.
+ * @return The path.
+ */
+ const std::string& ClientThumbnailPath() const { return m_thumbnailPath.GetClientImage(); }
+
+ /*!
+ * @brief Return the fanart path as given by the client.
+ * @return The path.
+ */
+ const std::string& ClientFanartPath() const { return m_fanartPath.GetClientImage(); }
+
+ /*!
+ * @brief Return the icon path used by Kodi.
+ * @return The path.
+ */
+ const std::string& IconPath() const { return m_iconPath.GetLocalImage(); }
+
+ /*!
+ * @brief Return the thumnail path used by Kodi.
+ * @return The path.
+ */
+ const std::string& ThumbnailPath() const { return m_thumbnailPath.GetLocalImage(); }
+
+ /*!
+ * @brief Return the fanart path used by Kodi.
+ * @return The path.
+ */
+ const std::string& FanartPath() const { return m_fanartPath.GetLocalImage(); }
+
+ /*!
+ * @brief Retrieve the recording Episode Name
+ * @note Returns an empty string if no Episode Name was provided by the PVR client
+ */
+ std::string EpisodeName() const { return m_strShowTitle; }
+
+ /*!
+ * @brief check whether this recording is currently in progress
+ * @return true if the recording is in progress, false otherwise
+ */
+ bool IsInProgress() const;
+
+ /*!
+ * @brief return the timer for an in-progress recording, if any
+ * @return the timer if the recording is in progress, nullptr otherwise
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetRecordingTimer() const;
+
+ /*!
+ * @brief set the genre for this recording.
+ * @param iGenreType The genre type ID. If set to EPG_GENRE_USE_STRING, set genre to the value provided with strGenre. Otherwise, compile the genre string from the values given by iGenreType and iGenreSubType
+ * @param iGenreSubType The genre subtype ID
+ * @param strGenre The genre
+ */
+ void SetGenre(int iGenreType, int iGenreSubType, const std::string& strGenre);
+
+ /*!
+ * @brief Get the genre type ID of this recording.
+ * @return The genre type ID.
+ */
+ int GenreType() const { return m_iGenreType; }
+
+ /*!
+ * @brief Get the genre subtype ID of this recording.
+ * @return The genre subtype ID.
+ */
+ int GenreSubType() const { return m_iGenreSubType; }
+
+ /*!
+ * @brief Get the genre as human readable string.
+ * @return The genre.
+ */
+ const std::vector<std::string> Genre() const { return m_genre; }
+
+ /*!
+ * @brief Get the genre(s) of this recording as formatted string.
+ * @return The genres label.
+ */
+ const std::string GetGenresLabel() const;
+
+ /*!
+ * @brief Get the first air date of this recording.
+ * @return The first air date.
+ */
+ CDateTime FirstAired() const;
+
+ /*!
+ * @brief Get the premiere year of this recording.
+ * @return The premiere year
+ */
+ int GetYear() const override;
+
+ /*!
+ * @brief Set the premiere year of this recording.
+ * @param year The premiere year
+ */
+ void SetYear(int year) override;
+
+ /*!
+ * @brief Check if the premiere year of this recording is valid
+ * @return True if the recording has as valid premiere date, false otherwise
+ */
+ bool HasYear() const override;
+
+ /*!
+ * @brief Check whether this recording will be flagged as new.
+ * @return True if this recording will be flagged as new, false otherwise
+ */
+ bool IsNew() const;
+
+ /*!
+ * @brief Check whether this recording will be flagged as a premiere.
+ * @return True if this recording will be flagged as a premiere, false otherwise
+ */
+ bool IsPremiere() const;
+
+ /*!
+ * @brief Check whether this recording will be flagged as a finale.
+ * @return True if this recording will be flagged as a finale, false otherwise
+ */
+ bool IsFinale() const;
+
+ /*!
+ * @brief Check whether this recording will be flagged as live.
+ * @return True if this recording will be flagged as live, false otherwise
+ */
+ bool IsLive() const;
+
+ /*!
+ * @brief Return the flags (PVR_RECORDING_FLAG_*) of this recording as a bitfield.
+ * @return the flags.
+ */
+ unsigned int Flags() const { return m_iFlags; }
+
+ /*!
+ * @brief Return the size of this recording in bytes.
+ * @return the size in bytes.
+ */
+ int64_t GetSizeInBytes() const;
+
+ /*!
+ * @brief Mark a recording as dirty/clean.
+ * @param bDirty true to mark as dirty, false to mark as clean.
+ */
+ void SetDirty(bool bDirty) { m_bDirty = bDirty; }
+
+ /*!
+ * @brief Return whether the recording is marked dirty.
+ * @return true if dirty, false otherwise.
+ */
+ bool IsDirty() const { return m_bDirty; }
+
+ /*!
+ * @brief Get the uid of the provider on the client which this recording is from
+ * @return the client uid of the provider or PVR_PROVIDER_INVALID_UID
+ */
+ int ClientProviderUniqueId() const;
+
+ /*!
+ * @brief Get the client provider name for this recording
+ * @return m_strProviderName The name for this recording provider
+ */
+ std::string ProviderName() const;
+
+ /*!
+ * @brief Get the default provider of this recording. The default
+ * provider represents the PVR add-on itself.
+ * @return The default provider of this recording
+ */
+ std::shared_ptr<CPVRProvider> GetDefaultProvider() const;
+
+ /*!
+ * @brief Whether or not this recording has a provider set by the client.
+ * @return True if a provider was set by the client, false otherwise.
+ */
+ bool HasClientProvider() const;
+
+ /*!
+ * @brief Get the provider of this recording. This may be the default provider or a
+ * custom provider set by the client. If @ref "HasClientProvider()" returns true
+ * the provider will be custom from the client, otherwise the default provider.
+ * @return The provider of this recording
+ */
+ std::shared_ptr<CPVRProvider> GetProvider() const;
+
+private:
+ CPVRRecording(const CPVRRecording& tag) = delete;
+ CPVRRecording& operator=(const CPVRRecording& other) = delete;
+
+ void UpdatePath();
+
+ int m_iClientId; /*!< ID of the backend */
+ std::string m_strRecordingId; /*!< unique ID of the recording on the client */
+ std::string m_strChannelName; /*!< name of the channel this was recorded from */
+ int m_iPriority; /*!< priority of this recording */
+ int m_iLifetime; /*!< lifetime of this recording */
+ std::string m_strDirectory; /*!< directory of this recording on the client */
+ unsigned int m_iRecordingId; /*!< id that won't change while xbmc is running */
+
+ CPVRCachedImage m_iconPath; /*!< icon path */
+ CPVRCachedImage m_thumbnailPath; /*!< thumbnail path */
+ CPVRCachedImage m_fanartPath; /*!< fanart path */
+ CDateTime m_recordingTime; /*!< start time of the recording */
+ bool m_bGotMetaData;
+ bool m_bIsDeleted; /*!< set if entry is a deleted recording which can be undelete */
+ mutable bool m_bInProgress; /*!< set if recording might be in progress */
+ unsigned int m_iEpgEventId; /*!< epg broadcast id associated with this recording */
+ int m_iChannelUid; /*!< channel uid associated with this recording */
+ bool m_bRadio; /*!< radio or tv recording */
+ int m_iGenreType = 0; /*!< genre type */
+ int m_iGenreSubType = 0; /*!< genre subtype */
+ mutable XbmcThreads::EndTime<> m_resumePointRefetchTimeout;
+ unsigned int m_iFlags = 0; /*!< the flags applicable to this recording */
+ mutable XbmcThreads::EndTime<> m_recordingSizeRefetchTimeout;
+ int64_t m_sizeInBytes = 0; /*!< the size of the recording in bytes */
+ bool m_bDirty = false;
+ std::string m_strProviderName; /*!< name of the provider this recording is from */
+ int m_iClientProviderUniqueId =
+ PVR_PROVIDER_INVALID_UID; /*!< provider uid associated with this recording on the client */
+
+ mutable CCriticalSection m_critSection;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/recordings/PVRRecordings.cpp b/xbmc/pvr/recordings/PVRRecordings.cpp
new file mode 100644
index 0000000..d6fd480
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecordings.cpp
@@ -0,0 +1,361 @@
+/*
+ * 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 "PVRRecordings.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRCachedImages.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+CPVRRecordings::CPVRRecordings() = default;
+
+CPVRRecordings::~CPVRRecordings()
+{
+ if (m_database && m_database->IsOpen())
+ m_database->Close();
+}
+
+bool CPVRRecordings::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_bIsUpdating)
+ return false;
+
+ m_bIsUpdating = true;
+
+ for (const auto& recording : m_recordings)
+ recording.second->SetDirty(true);
+
+ std::vector<int> failedClients;
+ CServiceBroker::GetPVRManager().Clients()->GetRecordings(clients, this, false, failedClients);
+ CServiceBroker::GetPVRManager().Clients()->GetRecordings(clients, this, true, failedClients);
+
+ // remove recordings that were deleted at the backend
+ for (auto it = m_recordings.begin(); it != m_recordings.end();)
+ {
+ if ((*it).second->IsDirty() && std::find(failedClients.begin(), failedClients.end(),
+ (*it).second->ClientID()) == failedClients.end())
+ it = m_recordings.erase(it);
+ else
+ ++it;
+ }
+
+ m_bIsUpdating = false;
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated);
+ return true;
+}
+
+bool CPVRRecordings::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ return UpdateFromClients(clients);
+}
+
+void CPVRRecordings::Unload()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bDeletedTVRecordings = false;
+ m_bDeletedRadioRecordings = false;
+ m_iTVRecordings = 0;
+ m_iRadioRecordings = 0;
+ m_recordings.clear();
+}
+
+void CPVRRecordings::UpdateInProgressSize()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bIsUpdating)
+ return;
+ m_bIsUpdating = true;
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updating recordings size");
+ bool bHaveUpdatedInProgessRecording = false;
+ for (auto& recording : m_recordings)
+ {
+ if (recording.second->IsInProgress())
+ {
+ if (recording.second->UpdateRecordingSize())
+ bHaveUpdatedInProgessRecording = true;
+ }
+ }
+
+ m_bIsUpdating = false;
+
+ if (bHaveUpdatedInProgessRecording)
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated);
+}
+
+int CPVRRecordings::GetNumTVRecordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iTVRecordings;
+}
+
+bool CPVRRecordings::HasDeletedTVRecordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bDeletedTVRecordings;
+}
+
+int CPVRRecordings::GetNumRadioRecordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_iRadioRecordings;
+}
+
+bool CPVRRecordings::HasDeletedRadioRecordings() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_bDeletedRadioRecordings;
+}
+
+std::vector<std::shared_ptr<CPVRRecording>> CPVRRecordings::GetAll() const
+{
+ std::vector<std::shared_ptr<CPVRRecording>> recordings;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::transform(m_recordings.cbegin(), m_recordings.cend(), std::back_inserter(recordings),
+ [](const auto& recordingEntry) { return recordingEntry.second; });
+
+ return recordings;
+}
+
+std::shared_ptr<CPVRRecording> CPVRRecordings::GetById(unsigned int iId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it =
+ std::find_if(m_recordings.cbegin(), m_recordings.cend(),
+ [iId](const auto& recording) { return recording.second->RecordingID() == iId; });
+ return it != m_recordings.cend() ? (*it).second : std::shared_ptr<CPVRRecording>();
+}
+
+std::shared_ptr<CPVRRecording> CPVRRecordings::GetByPath(const std::string& path) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CPVRRecordingsPath recPath(path);
+ if (recPath.IsValid())
+ {
+ bool bDeleted = recPath.IsDeleted();
+ bool bRadio = recPath.IsRadio();
+
+ for (const auto& recording : m_recordings)
+ {
+ std::shared_ptr<CPVRRecording> current = recording.second;
+ // Omit recordings not matching criteria
+ if (!URIUtils::PathEquals(path, current->m_strFileNameAndPath) ||
+ bDeleted != current->IsDeleted() || bRadio != current->IsRadio())
+ continue;
+
+ return current;
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRRecording> CPVRRecordings::GetById(int iClientId,
+ const std::string& strRecordingId) const
+{
+ std::shared_ptr<CPVRRecording> retVal;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = m_recordings.find(CPVRRecordingUid(iClientId, strRecordingId));
+ if (it != m_recordings.end())
+ retVal = it->second;
+
+ return retVal;
+}
+
+void CPVRRecordings::UpdateFromClient(const std::shared_ptr<CPVRRecording>& tag,
+ const CPVRClient& client)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (tag->IsDeleted())
+ {
+ if (tag->IsRadio())
+ m_bDeletedRadioRecordings = true;
+ else
+ m_bDeletedTVRecordings = true;
+ }
+
+ std::shared_ptr<CPVRRecording> existingTag = GetById(tag->ClientID(), tag->ClientRecordingID());
+ if (existingTag)
+ {
+ existingTag->Update(*tag, client);
+ existingTag->SetDirty(false);
+ }
+ else
+ {
+ tag->UpdateMetadata(GetVideoDatabase(), client);
+ tag->SetRecordingID(++m_iLastId);
+ m_recordings.insert({CPVRRecordingUid(tag->ClientID(), tag->ClientRecordingID()), tag});
+ if (tag->IsRadio())
+ ++m_iRadioRecordings;
+ else
+ ++m_iTVRecordings;
+ }
+}
+
+std::shared_ptr<CPVRRecording> CPVRRecordings::GetRecordingForEpgTag(
+ const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (!epgTag)
+ return {};
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& recording : m_recordings)
+ {
+ if (recording.second->IsDeleted())
+ continue;
+
+ if (recording.second->ClientID() != epgTag->ClientID())
+ continue;
+
+ if (recording.second->ChannelUid() != epgTag->UniqueChannelID())
+ continue;
+
+ unsigned int iEpgEvent = recording.second->BroadcastUid();
+ if (iEpgEvent != EPG_TAG_INVALID_UID)
+ {
+ if (iEpgEvent == epgTag->UniqueBroadcastID())
+ return recording.second;
+ }
+ else
+ {
+ if (recording.second->RecordingTimeAsUTC() <= epgTag->StartAsUTC() &&
+ recording.second->EndTimeAsUTC() >= epgTag->EndAsUTC())
+ return recording.second;
+ }
+ }
+
+ return std::shared_ptr<CPVRRecording>();
+}
+
+bool CPVRRecordings::SetRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording,
+ int count)
+{
+ return ChangeRecordingsPlayCount(recording, count);
+}
+
+bool CPVRRecordings::IncrementRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording)
+{
+ return ChangeRecordingsPlayCount(recording, INCREMENT_PLAY_COUNT);
+}
+
+bool CPVRRecordings::ChangeRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording,
+ int count)
+{
+ if (recording)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CVideoDatabase& db = GetVideoDatabase();
+ if (db.IsOpen())
+ {
+ if (count == INCREMENT_PLAY_COUNT)
+ recording->IncrementPlayCount();
+ else
+ recording->SetPlayCount(count);
+
+ // Clear resume bookmark
+ if (recording->GetPlayCount() > 0)
+ {
+ db.ClearBookMarksOfFile(recording->m_strFileNameAndPath, CBookmark::RESUME);
+ recording->SetResumePoint(CBookmark());
+ }
+
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CPVRRecordings::MarkWatched(const std::shared_ptr<CPVRRecording>& recording, bool bWatched)
+{
+ if (bWatched)
+ return IncrementRecordingsPlayCount(recording);
+ else
+ return SetRecordingsPlayCount(recording, 0);
+}
+
+bool CPVRRecordings::ResetResumePoint(const std::shared_ptr<CPVRRecording>& recording)
+{
+ bool bResult = false;
+
+ if (recording)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ CVideoDatabase& db = GetVideoDatabase();
+ if (db.IsOpen())
+ {
+ bResult = true;
+
+ db.ClearBookMarksOfFile(recording->m_strFileNameAndPath, CBookmark::RESUME);
+ recording->SetResumePoint(CBookmark());
+
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::RecordingsInvalidated);
+ }
+ }
+ return bResult;
+}
+
+CVideoDatabase& CPVRRecordings::GetVideoDatabase()
+{
+ if (!m_database)
+ {
+ m_database.reset(new CVideoDatabase());
+ m_database->Open();
+
+ if (!m_database->IsOpen())
+ CLog::LogF(LOGERROR, "Failed to open the video database");
+ }
+
+ return *m_database;
+}
+
+int CPVRRecordings::CleanupCachedImages()
+{
+ std::vector<std::string> urlsToCheck;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& recording : m_recordings)
+ {
+ urlsToCheck.emplace_back(recording.second->ClientIconPath());
+ urlsToCheck.emplace_back(recording.second->ClientThumbnailPath());
+ urlsToCheck.emplace_back(recording.second->ClientFanartPath());
+ urlsToCheck.emplace_back(recording.second->m_strFileNameAndPath);
+ }
+ }
+
+ static const std::vector<PVRImagePattern> urlPatterns = {
+ {CPVRRecording::IMAGE_OWNER_PATTERN, ""}, // client-supplied icon, thumbnail, fanart
+ {"video", "pvr://recordings/"}, // kodi-generated video thumbnail
+ };
+ return CPVRCachedImages::Cleanup(urlPatterns, urlsToCheck);
+}
diff --git a/xbmc/pvr/recordings/PVRRecordings.h b/xbmc/pvr/recordings/PVRRecordings.h
new file mode 100644
index 0000000..1a7753f
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecordings.h
@@ -0,0 +1,154 @@
+/*
+ * 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 <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CVideoDatabase;
+
+namespace PVR
+{
+class CPVRClient;
+class CPVREpgInfoTag;
+class CPVRRecording;
+class CPVRRecordingUid;
+class CPVRRecordingsPath;
+
+class CPVRRecordings
+{
+public:
+ CPVRRecordings();
+ virtual ~CPVRRecordings();
+
+ /*!
+ * @brief Update all recordings from the given PVR clients.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief unload all recordings.
+ */
+ void Unload();
+
+ /*!
+ * @brief Update data with recordings from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created clients.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief client has delivered a new/updated recording.
+ * @param tag The recording
+ * @param client The client the recording belongs to.
+ */
+ void UpdateFromClient(const std::shared_ptr<CPVRRecording>& tag, const CPVRClient& client);
+
+ /*!
+ * @brief refresh the size of any in progress recordings from the clients.
+ */
+ void UpdateInProgressSize();
+
+ int GetNumTVRecordings() const;
+ bool HasDeletedTVRecordings() const;
+ int GetNumRadioRecordings() const;
+ bool HasDeletedRadioRecordings() const;
+
+ /*!
+ * @brief Set a recording's watched state
+ * @param recording The recording
+ * @param bWatched True to set watched, false to set unwatched state
+ * @return True on success, false otherwise
+ */
+ bool MarkWatched(const std::shared_ptr<CPVRRecording>& recording, bool bWatched);
+
+ /*!
+ * @brief Reset a recording's resume point, if any
+ * @param recording The recording
+ * @return True on success, false otherwise
+ */
+ bool ResetResumePoint(const std::shared_ptr<CPVRRecording>& recording);
+
+ /*!
+ * @brief Get a list of all recordings
+ * @return the list of all recordings
+ */
+ std::vector<std::shared_ptr<CPVRRecording>> GetAll() const;
+
+ std::shared_ptr<CPVRRecording> GetByPath(const std::string& path) const;
+ std::shared_ptr<CPVRRecording> GetById(int iClientId, const std::string& strRecordingId) const;
+ std::shared_ptr<CPVRRecording> GetById(unsigned int iId) const;
+
+ /*!
+ * @brief Get the recording for the given epg tag, if any.
+ * @param epgTag The epg tag.
+ * @return The requested recording, or an empty recordingptr if none was found.
+ */
+ std::shared_ptr<CPVRRecording> GetRecordingForEpgTag(
+ const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ /*!
+ * @brief Erase stale texture db entries and image files.
+ * @return number of cleaned up images.
+ */
+ int CleanupCachedImages();
+
+private:
+ /*!
+ * @brief Get/Open the video database.
+ * @return A reference to the video database.
+ */
+ CVideoDatabase& GetVideoDatabase();
+
+ /*!
+ * @brief Set a recording's play count
+ * @param recording The recording
+ * @param count The new play count
+ * @return True on success, false otherwise
+ */
+ bool SetRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording, int count);
+
+ /*!
+ * @brief Increment a recording's play count
+ * @param recording The recording
+ * @return True on success, false otherwise
+ */
+ bool IncrementRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording);
+
+ /*!
+ * @brief special value for parameter count of method ChangeRecordingsPlayCount
+ */
+ static const int INCREMENT_PLAY_COUNT = -1;
+
+ /*!
+ * @brief change the play count of the given recording
+ * @param recording The recording
+ * @param count The new play count or INCREMENT_PLAY_COUNT to denote that the current play count is to be incremented by one
+ * @return true if the play count was changed successfully
+ */
+ bool ChangeRecordingsPlayCount(const std::shared_ptr<CPVRRecording>& recording, int count);
+
+ mutable CCriticalSection m_critSection;
+ bool m_bIsUpdating = false;
+ std::map<CPVRRecordingUid, std::shared_ptr<CPVRRecording>> m_recordings;
+ unsigned int m_iLastId = 0;
+ std::unique_ptr<CVideoDatabase> m_database;
+ bool m_bDeletedTVRecordings = false;
+ bool m_bDeletedRadioRecordings = false;
+ unsigned int m_iTVRecordings = 0;
+ unsigned int m_iRadioRecordings = 0;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/recordings/PVRRecordingsPath.cpp b/xbmc/pvr/recordings/PVRRecordingsPath.cpp
new file mode 100644
index 0000000..470d674
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecordingsPath.cpp
@@ -0,0 +1,258 @@
+/*
+ * 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 "PVRRecordingsPath.h"
+
+#include "URL.h"
+#include "XBDateTime.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVRRecordingsPath::PATH_RECORDINGS = "pvr://recordings/";
+const std::string CPVRRecordingsPath::PATH_ACTIVE_TV_RECORDINGS = "pvr://recordings/tv/active/";
+const std::string CPVRRecordingsPath::PATH_ACTIVE_RADIO_RECORDINGS =
+ "pvr://recordings/radio/active/";
+const std::string CPVRRecordingsPath::PATH_DELETED_TV_RECORDINGS = "pvr://recordings/tv/deleted/";
+const std::string CPVRRecordingsPath::PATH_DELETED_RADIO_RECORDINGS =
+ "pvr://recordings/radio/deleted/";
+
+CPVRRecordingsPath::CPVRRecordingsPath(const std::string& strPath)
+{
+ std::string strVarPath(TrimSlashes(strPath));
+ const std::vector<std::string> segments = URIUtils::SplitPath(strVarPath);
+
+ m_bValid = ((segments.size() >= 4) && // at least pvr://recordings/[tv|radio]/[active|deleted]
+ StringUtils::StartsWith(strVarPath, "pvr://") && (segments.at(1) == "recordings") &&
+ ((segments.at(2) == "tv") || (segments.at(2) == "radio")) &&
+ ((segments.at(3) == "active") || (segments.at(3) == "deleted")));
+ if (m_bValid)
+ {
+ m_bRoot = (segments.size() == 4);
+ m_bRadio = (segments.at(2) == "radio");
+ m_bActive = (segments.at(3) == "active");
+
+ if (m_bRoot)
+ strVarPath.append("/");
+ else
+ {
+ size_t paramStart = m_path.find(", TV");
+ if (paramStart == std::string::npos)
+ m_directoryPath = strVarPath.substr(GetDirectoryPathPosition());
+ else
+ {
+ size_t dirStart = GetDirectoryPathPosition();
+ m_directoryPath = strVarPath.substr(dirStart, paramStart - dirStart);
+ m_params = strVarPath.substr(paramStart);
+ }
+ }
+
+ m_path = strVarPath;
+ }
+ else
+ {
+ m_bRoot = false;
+ m_bActive = false;
+ m_bRadio = false;
+ }
+}
+
+CPVRRecordingsPath::CPVRRecordingsPath(bool bDeleted, bool bRadio)
+ : m_bValid(true),
+ m_bRoot(true),
+ m_bActive(!bDeleted),
+ m_bRadio(bRadio),
+ m_path(StringUtils::Format(
+ "pvr://recordings/{}/{}/", bRadio ? "radio" : "tv", bDeleted ? "deleted" : "active"))
+{
+}
+
+CPVRRecordingsPath::CPVRRecordingsPath(bool bDeleted,
+ bool bRadio,
+ const std::string& strDirectory,
+ const std::string& strTitle,
+ int iSeason,
+ int iEpisode,
+ int iYear,
+ const std::string& strSubtitle,
+ const std::string& strChannelName,
+ const CDateTime& recordingTime,
+ const std::string& strId)
+ : m_bValid(true), m_bRoot(false), m_bActive(!bDeleted), m_bRadio(bRadio)
+{
+ std::string strDirectoryN(TrimSlashes(strDirectory));
+ if (!strDirectoryN.empty())
+ strDirectoryN = StringUtils::Format("{}/", strDirectoryN);
+
+ std::string strTitleN(strTitle);
+ strTitleN = CURL::Encode(strTitleN);
+
+ std::string strSeasonEpisodeN;
+ if ((iSeason > -1 && iEpisode > -1 && (iSeason > 0 || iEpisode > 0)))
+ strSeasonEpisodeN = StringUtils::Format("s{:02}e{:02}", iSeason, iEpisode);
+ if (!strSeasonEpisodeN.empty())
+ strSeasonEpisodeN = StringUtils::Format(" {}", strSeasonEpisodeN);
+
+ std::string strYearN(iYear > 0 ? StringUtils::Format(" ({})", iYear) : "");
+
+ std::string strSubtitleN;
+ if (!strSubtitle.empty())
+ {
+ strSubtitleN = StringUtils::Format(" {}", strSubtitle);
+ strSubtitleN = CURL::Encode(strSubtitleN);
+ }
+
+ std::string strChannelNameN;
+ if (!strChannelName.empty())
+ {
+ strChannelNameN = StringUtils::Format(" ({})", strChannelName);
+ strChannelNameN = CURL::Encode(strChannelNameN);
+ }
+
+ m_directoryPath = StringUtils::Format("{}{}{}{}{}", strDirectoryN, strTitleN, strSeasonEpisodeN,
+ strYearN, strSubtitleN);
+ m_params = StringUtils::Format(", TV{}, {}, {}.pvr", strChannelNameN,
+ recordingTime.GetAsSaveString(), strId);
+ m_path = StringUtils::Format("pvr://recordings/{}/{}/{}{}", bRadio ? "radio" : "tv",
+ bDeleted ? "deleted" : "active", m_directoryPath, m_params);
+}
+
+std::string CPVRRecordingsPath::GetUnescapedDirectoryPath() const
+{
+ return CURL::Decode(m_directoryPath);
+}
+
+namespace
+{
+bool PathHasParent(const std::string& path, const std::string& parent)
+{
+ if (path == parent)
+ return true;
+
+ if (!parent.empty() && parent.back() != '/')
+ return StringUtils::StartsWith(path, parent + '/');
+
+ return StringUtils::StartsWith(path, parent);
+}
+} // unnamed namespace
+
+std::string CPVRRecordingsPath::GetUnescapedSubDirectoryPath(const std::string& strPath) const
+{
+ // note: strPath must be unescaped.
+
+ std::string strReturn;
+ std::string strUsePath(TrimSlashes(strPath));
+
+ const std::string strUnescapedDirectoryPath(GetUnescapedDirectoryPath());
+
+ /* adding "/" to make sure that base matches the complete folder name and not only parts of it */
+ if (!strUnescapedDirectoryPath.empty() &&
+ (strUsePath.size() <= strUnescapedDirectoryPath.size() ||
+ !PathHasParent(strUsePath, strUnescapedDirectoryPath)))
+ return strReturn;
+
+ strUsePath.erase(0, strUnescapedDirectoryPath.size());
+ strUsePath = TrimSlashes(strUsePath);
+
+ /* check for more occurrences */
+ size_t iDelimiter = strUsePath.find('/');
+ if (iDelimiter == std::string::npos)
+ strReturn = strUsePath;
+ else
+ strReturn = strUsePath.substr(0, iDelimiter);
+
+ return strReturn;
+}
+
+const std::string CPVRRecordingsPath::GetTitle() const
+{
+ if (m_bValid)
+ {
+ CRegExp reg(true);
+ if (reg.RegComp("pvr://recordings/(.*/)*(.*), TV( \\(.*\\))?, "
+ "(19[0-9][0-9]|20[0-9][0-9])[0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9]"
+ ", (.*).pvr"))
+ {
+ if (reg.RegFind(m_path.c_str()) >= 0)
+ return reg.GetMatch(2);
+ }
+ }
+ return "";
+}
+
+void CPVRRecordingsPath::AppendSegment(const std::string& strSegment)
+{
+ if (!m_bValid || strSegment.empty() || strSegment == "/")
+ return;
+
+ std::string strVarSegment(TrimSlashes(strSegment));
+ strVarSegment = CURL::Encode(strVarSegment);
+
+ if (!m_directoryPath.empty() && m_directoryPath.back() != '/')
+ m_directoryPath.push_back('/');
+
+ m_directoryPath += strVarSegment;
+ m_directoryPath.push_back('/');
+
+ size_t paramStart = m_path.find(", TV");
+ if (paramStart == std::string::npos)
+ {
+ if (!m_path.empty() && m_path.back() != '/')
+ m_path.push_back('/');
+
+ // append the segment
+ m_path += strVarSegment;
+ m_path.push_back('/');
+ }
+ else
+ {
+ if (m_path.back() != '/')
+ m_path.insert(paramStart, "/");
+
+ // insert the segment between end of current directory path and parameters
+ m_path.insert(paramStart, strVarSegment);
+ }
+
+ m_bRoot = false;
+}
+
+std::string CPVRRecordingsPath::TrimSlashes(const std::string& strString)
+{
+ std::string strTrimmed(strString);
+ while (!strTrimmed.empty() && strTrimmed.front() == '/')
+ strTrimmed.erase(0, 1);
+
+ while (!strTrimmed.empty() && strTrimmed.back() == '/')
+ strTrimmed.pop_back();
+
+ return strTrimmed;
+}
+
+size_t CPVRRecordingsPath::GetDirectoryPathPosition() const
+{
+ if (m_bActive)
+ {
+ if (m_bRadio)
+ return PATH_ACTIVE_RADIO_RECORDINGS.size();
+ else
+ return PATH_ACTIVE_TV_RECORDINGS.size();
+ }
+ else
+ {
+ if (m_bRadio)
+ return PATH_DELETED_RADIO_RECORDINGS.size();
+ else
+ return PATH_DELETED_TV_RECORDINGS.size();
+ }
+ // unreachable
+}
diff --git a/xbmc/pvr/recordings/PVRRecordingsPath.h b/xbmc/pvr/recordings/PVRRecordingsPath.h
new file mode 100644
index 0000000..e95dc92
--- /dev/null
+++ b/xbmc/pvr/recordings/PVRRecordingsPath.h
@@ -0,0 +1,68 @@
+/*
+ * 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 <string>
+
+class CDateTime;
+
+namespace PVR
+{
+class CPVRRecordingsPath
+{
+public:
+ static const std::string PATH_RECORDINGS;
+ static const std::string PATH_ACTIVE_TV_RECORDINGS;
+ static const std::string PATH_ACTIVE_RADIO_RECORDINGS;
+ static const std::string PATH_DELETED_TV_RECORDINGS;
+ static const std::string PATH_DELETED_RADIO_RECORDINGS;
+
+ explicit CPVRRecordingsPath(const std::string& strPath);
+ CPVRRecordingsPath(bool bDeleted, bool bRadio);
+ CPVRRecordingsPath(bool bDeleted,
+ bool bRadio,
+ const std::string& strDirectory,
+ const std::string& strTitle,
+ int iSeason,
+ int iEpisode,
+ int iYear,
+ const std::string& strSubtitle,
+ const std::string& strChannelName,
+ const CDateTime& recordingTime,
+ const std::string& strId);
+
+ operator std::string() const { return m_path; }
+
+ bool IsValid() const { return m_bValid; }
+
+ const std::string& GetPath() const { return m_path; }
+ bool IsRecordingsRoot() const { return m_bRoot; }
+ bool IsActive() const { return m_bActive; }
+ bool IsDeleted() const { return !IsActive(); }
+ bool IsRadio() const { return m_bRadio; }
+ bool IsTV() const { return !IsRadio(); }
+ std::string GetUnescapedDirectoryPath() const;
+ std::string GetUnescapedSubDirectoryPath(const std::string& strPath) const;
+
+ const std::string GetTitle() const;
+ void AppendSegment(const std::string& strSegment);
+
+private:
+ static std::string TrimSlashes(const std::string& strString);
+ size_t GetDirectoryPathPosition() const;
+
+ bool m_bValid;
+ bool m_bRoot;
+ bool m_bActive;
+ bool m_bRadio;
+ std::string m_directoryPath;
+ std::string m_params;
+ std::string m_path;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/settings/CMakeLists.txt b/xbmc/pvr/settings/CMakeLists.txt
new file mode 100644
index 0000000..6b32503
--- /dev/null
+++ b/xbmc/pvr/settings/CMakeLists.txt
@@ -0,0 +1,5 @@
+set(SOURCES PVRSettings.cpp)
+
+set(HEADERS PVRSettings.h)
+
+core_add_library(pvr_settings)
diff --git a/xbmc/pvr/settings/PVRSettings.cpp b/xbmc/pvr/settings/PVRSettings.cpp
new file mode 100644
index 0000000..f28651a
--- /dev/null
+++ b/xbmc/pvr/settings/PVRSettings.cpp
@@ -0,0 +1,232 @@
+/*
+ * 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 "PVRSettings.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/guilib/PVRGUIActionsParentalControl.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <set>
+#include <string>
+#include <utility>
+
+using namespace PVR;
+
+unsigned int CPVRSettings::m_iInstances = 0;
+
+CPVRSettings::CPVRSettings(const std::set<std::string>& settingNames)
+{
+ Init(settingNames);
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->GetSettingsManager()->RegisterSettingsHandler(this);
+ settings->RegisterCallback(this, settingNames);
+
+ if (m_iInstances == 0)
+ {
+ // statics must only be registered once, not per instance
+ settings->GetSettingsManager()->RegisterSettingOptionsFiller("pvrrecordmargins", MarginTimeFiller);
+ settings->GetSettingsManager()->AddDynamicCondition("pvrsettingvisible", IsSettingVisible);
+ settings->GetSettingsManager()->AddDynamicCondition("checkpvrparentalpin", CheckParentalPin);
+ }
+ m_iInstances++;
+}
+
+CPVRSettings::~CPVRSettings()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ m_iInstances--;
+ if (m_iInstances == 0)
+ {
+ settings->GetSettingsManager()->RemoveDynamicCondition("checkpvrparentalpin");
+ settings->GetSettingsManager()->RemoveDynamicCondition("pvrsettingvisible");
+ settings->GetSettingsManager()->UnregisterSettingOptionsFiller("pvrrecordmargins");
+ }
+
+ settings->UnregisterCallback(this);
+ settings->GetSettingsManager()->UnregisterSettingsHandler(this);
+}
+
+void CPVRSettings::RegisterCallback(ISettingCallback* callback)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_callbacks.insert(callback);
+}
+
+void CPVRSettings::UnregisterCallback(ISettingCallback* callback)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_callbacks.erase(callback);
+}
+
+void CPVRSettings::Init(const std::set<std::string>& settingNames)
+{
+ for (const auto& settingName : settingNames)
+ {
+ SettingPtr setting = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(settingName);
+ if (!setting)
+ {
+ CLog::LogF(LOGERROR, "Unknown PVR setting '{}'", settingName);
+ continue;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_settings.insert(std::make_pair(settingName, setting->Clone(settingName)));
+ }
+}
+
+void CPVRSettings::OnSettingsLoaded()
+{
+ std::set<std::string> settingNames;
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& settingName : m_settings)
+ settingNames.insert(settingName.first);
+
+ m_settings.clear();
+ }
+
+ Init(settingNames);
+}
+
+void CPVRSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_settings[setting->GetId()] = setting->Clone(setting->GetId());
+ const auto callbacks(m_callbacks);
+ lock.unlock();
+
+ for (const auto& callback : callbacks)
+ callback->OnSettingChanged(setting);
+}
+
+bool CPVRSettings::GetBoolValue(const std::string& settingName) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto settingIt = m_settings.find(settingName);
+ if (settingIt != m_settings.end() && (*settingIt).second->GetType() == SettingType::Boolean)
+ {
+ std::shared_ptr<const CSettingBool> setting = std::dynamic_pointer_cast<const CSettingBool>((*settingIt).second);
+ if (setting)
+ return setting->GetValue();
+ }
+
+ CLog::LogF(LOGERROR, "PVR setting '{}' not found or wrong type given", settingName);
+ return false;
+}
+
+int CPVRSettings::GetIntValue(const std::string& settingName) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto settingIt = m_settings.find(settingName);
+ if (settingIt != m_settings.end() && (*settingIt).second->GetType() == SettingType::Integer)
+ {
+ std::shared_ptr<const CSettingInt> setting = std::dynamic_pointer_cast<const CSettingInt>((*settingIt).second);
+ if (setting)
+ return setting->GetValue();
+ }
+
+ CLog::LogF(LOGERROR, "PVR setting '{}' not found or wrong type given", settingName);
+ return -1;
+}
+
+std::string CPVRSettings::GetStringValue(const std::string& settingName) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ auto settingIt = m_settings.find(settingName);
+ if (settingIt != m_settings.end() && (*settingIt).second->GetType() == SettingType::String)
+ {
+ std::shared_ptr<const CSettingString> setting = std::dynamic_pointer_cast<const CSettingString>((*settingIt).second);
+ if (setting)
+ return setting->GetValue();
+ }
+
+ CLog::LogF(LOGERROR, "PVR setting '{}' not found or wrong type given", settingName);
+ return "";
+}
+
+void CPVRSettings::MarginTimeFiller(const SettingConstPtr& /*setting*/,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* /*data*/)
+{
+ list.clear();
+
+ static const int marginTimeValues[] =
+ {
+ 0, 1, 3, 5, 10, 15, 20, 30, 60, 90, 120, 180 // minutes
+ };
+
+ for (int iValue : marginTimeValues)
+ {
+ list.emplace_back(StringUtils::Format(g_localizeStrings.Get(14044), iValue) /* {} min */,
+ iValue);
+ }
+}
+
+bool CPVRSettings::IsSettingVisible(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data)
+{
+ if (setting == nullptr)
+ return false;
+
+ const std::string& settingId = setting->GetId();
+
+ if (settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS)
+ {
+ // Setting is only visible if exactly one PVR client is enabled or
+ // the expert setting to always use backend numbers is enabled
+ const auto& settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ int enabledClientAmount = CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount();
+
+ return enabledClientAmount == 1 ||
+ (settings->GetBool(CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS) &&
+ enabledClientAmount > 1);
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS)
+ {
+ // Setting is only visible if more than one PVR client is enabled.
+ return CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount() > 1;
+ }
+ else if (settingId == CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES)
+ {
+ // Setting is only visible if more than one PVR client is enabeld.
+ return CServiceBroker::GetPVRManager().Clients()->EnabledClientAmount() > 1;
+ }
+ else
+ {
+ // Show all other settings unconditionally.
+ return true;
+ }
+}
+
+bool CPVRSettings::CheckParentalPin(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data)
+{
+ return CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalPIN() ==
+ ParentalCheckResult::SUCCESS;
+}
diff --git a/xbmc/pvr/settings/PVRSettings.h b/xbmc/pvr/settings/PVRSettings.h
new file mode 100644
index 0000000..7cab9d6
--- /dev/null
+++ b/xbmc/pvr/settings/PVRSettings.h
@@ -0,0 +1,76 @@
+/*
+ * 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 "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+class CSetting;
+
+struct IntegerSettingOption;
+
+namespace PVR
+{
+ class CPVRSettings : private ISettingsHandler, private ISettingCallback
+ {
+ public:
+ explicit CPVRSettings(const std::set<std::string>& settingNames);
+ ~CPVRSettings() override;
+
+ void RegisterCallback(ISettingCallback* callback);
+ void UnregisterCallback(ISettingCallback* callback);
+
+ // ISettingsHandler implementation
+ void OnSettingsLoaded() override;
+
+ // ISettingCallback implementation
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ bool GetBoolValue(const std::string& settingName) const;
+ int GetIntValue(const std::string& settingName) const;
+ std::string GetStringValue(const std::string& settingName) const;
+
+ // settings value filler for start/end recording margin time for PVR timers.
+ static void MarginTimeFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ // Dynamically hide or show settings.
+ static bool IsSettingVisible(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+ // Do parental PIN check.
+ static bool CheckParentalPin(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+ private:
+ CPVRSettings(const CPVRSettings&) = delete;
+ CPVRSettings& operator=(CPVRSettings const&) = delete;
+
+ void Init(const std::set<std::string>& settingNames);
+
+ mutable CCriticalSection m_critSection;
+ std::map<std::string, std::shared_ptr<CSetting>> m_settings;
+ std::set<ISettingCallback*> m_callbacks;
+
+ static unsigned int m_iInstances;
+ };
+}
diff --git a/xbmc/pvr/timers/CMakeLists.txt b/xbmc/pvr/timers/CMakeLists.txt
new file mode 100644
index 0000000..e1031b4
--- /dev/null
+++ b/xbmc/pvr/timers/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES PVRTimerInfoTag.cpp
+ PVRTimerRuleMatcher.cpp
+ PVRTimers.cpp
+ PVRTimersPath.cpp
+ PVRTimerType.cpp)
+
+set(HEADERS PVRTimerInfoTag.h
+ PVRTimerRuleMatcher.h
+ PVRTimers.h
+ PVRTimersPath.h
+ PVRTimerType.h)
+
+core_add_library(pvr_timers)
diff --git a/xbmc/pvr/timers/PVRTimerInfoTag.cpp b/xbmc/pvr/timers/PVRTimerInfoTag.cpp
new file mode 100644
index 0000000..24df4cc
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerInfoTag.cpp
@@ -0,0 +1,1389 @@
+/*
+ * 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 "PVRTimerInfoTag.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <ctime>
+#include <memory>
+#include <mutex>
+#include <stdexcept>
+#include <string>
+
+using namespace PVR;
+
+CPVRTimerInfoTag::CPVRTimerInfoTag(bool bRadio /* = false */)
+ : m_strTitle(g_localizeStrings.Get(19056)), // New Timer
+ m_iClientId(CServiceBroker::GetPVRManager().Clients()->GetFirstCreatedClientID()),
+ m_iClientIndex(PVR_TIMER_NO_CLIENT_INDEX),
+ m_iParentClientIndex(PVR_TIMER_NO_PARENT),
+ m_iClientChannelUid(PVR_CHANNEL_INVALID_UID),
+ m_iPriority(DEFAULT_RECORDING_PRIORITY),
+ m_iLifetime(DEFAULT_RECORDING_LIFETIME),
+ m_iPreventDupEpisodes(DEFAULT_RECORDING_DUPLICATEHANDLING),
+ m_bIsRadio(bRadio),
+ m_iMarginStart(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRRECORD_MARGINSTART)),
+ m_iMarginEnd(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRRECORD_MARGINEND)),
+ m_iEpgUid(EPG_TAG_INVALID_UID),
+ m_StartTime(CDateTime::GetUTCDateTime()),
+ m_StopTime(m_StartTime + CDateTimeSpan(0, 2, 0, 0)),
+ m_FirstDay(m_StartTime)
+{
+ static const uint64_t iMustHaveAttr = PVR_TIMER_TYPE_IS_MANUAL;
+ static const uint64_t iMustNotHaveAttr =
+ PVR_TIMER_TYPE_IS_REPEATING | PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES;
+
+ std::shared_ptr<CPVRTimerType> type;
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsTimers())
+ {
+ // default to first available type for given client
+ type = CPVRTimerType::GetFirstAvailableType(client);
+ }
+
+ // fallback to manual one-shot reminder, which is always available
+ if (!type)
+ type = CPVRTimerType::CreateFromAttributes(iMustHaveAttr | PVR_TIMER_TYPE_IS_REMINDER,
+ iMustNotHaveAttr, m_iClientId);
+
+ if (type)
+ SetTimerType(type);
+ else
+ {
+ CLog::LogF(LOGERROR, "Unable to obtain timer type!");
+ throw std::logic_error("CPVRTimerInfoTag::CPVRTimerInfoTag - Unable to obtain timer type!");
+ }
+
+ m_iWeekdays = m_timerType->IsTimerRule() ? PVR_WEEKDAY_ALLDAYS : PVR_WEEKDAY_NONE;
+
+ UpdateSummary();
+}
+
+CPVRTimerInfoTag::CPVRTimerInfoTag(const PVR_TIMER& timer,
+ const std::shared_ptr<CPVRChannel>& channel,
+ unsigned int iClientId)
+ : m_strTitle(timer.strTitle),
+ m_strEpgSearchString(timer.strEpgSearchString),
+ m_bFullTextEpgSearch(timer.bFullTextEpgSearch),
+ m_strDirectory(timer.strDirectory),
+ m_state(timer.state),
+ m_iClientId(iClientId),
+ m_iClientIndex(timer.iClientIndex),
+ m_iParentClientIndex(timer.iParentClientIndex),
+ m_iClientChannelUid(channel ? channel->UniqueID()
+ : (timer.iClientChannelUid > 0) ? timer.iClientChannelUid
+ : PVR_CHANNEL_INVALID_UID),
+ m_bStartAnyTime(timer.bStartAnyTime),
+ m_bEndAnyTime(timer.bEndAnyTime),
+ m_iPriority(timer.iPriority),
+ m_iLifetime(timer.iLifetime),
+ m_iMaxRecordings(timer.iMaxRecordings),
+ m_iWeekdays(timer.iWeekdays),
+ m_iPreventDupEpisodes(timer.iPreventDuplicateEpisodes),
+ m_iRecordingGroup(timer.iRecordingGroup),
+ m_strFileNameAndPath(
+ StringUtils::Format("pvr://client{}/timers/{}", m_iClientId, m_iClientIndex)),
+ m_bIsRadio(channel && channel->IsRadio()),
+ m_iMarginStart(timer.iMarginStart),
+ m_iMarginEnd(timer.iMarginEnd),
+ m_iEpgUid(timer.iEpgUid),
+ m_strSeriesLink(timer.strSeriesLink),
+ m_StartTime(
+ timer.startTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection),
+ m_StopTime(timer.endTime +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection),
+ m_channel(channel)
+{
+ if (timer.firstDay)
+ m_FirstDay = CDateTime(
+ timer.firstDay +
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection);
+ else
+ m_FirstDay = CDateTime::GetUTCDateTime();
+
+ if (m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX)
+ CLog::LogF(LOGERROR, "Invalid client index supplied by client {} (must be > 0)!", m_iClientId);
+
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client && client->GetClientCapabilities().SupportsTimers())
+ {
+ // begin compat section
+
+ // Create timer type according to certain timer values already available in Isengard.
+ // This is for migration only and does not make changes to the addons obsolete. Addons should
+ // work and benefit from some UI changes (e.g. some of the timer settings dialog enhancements),
+ // but all old problems/bugs due to static attributes and values will remain the same as in
+ // Isengard. Also, new features (like epg search) are not available to addons automatically.
+ // This code can be removed once all addons actually support the respective PVR Addon API version.
+ if (timer.iTimerType == PVR_TIMER_TYPE_NONE)
+ {
+ // Create type according to certain timer values.
+ uint64_t iMustHave = PVR_TIMER_TYPE_ATTRIBUTE_NONE;
+ uint64_t iMustNotHave = PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES;
+
+ if (timer.iEpgUid == PVR_TIMER_NO_EPG_UID && timer.iWeekdays != PVR_WEEKDAY_NONE)
+ iMustHave |= PVR_TIMER_TYPE_IS_REPEATING;
+ else
+ iMustNotHave |= PVR_TIMER_TYPE_IS_REPEATING;
+
+ if (timer.iEpgUid == PVR_TIMER_NO_EPG_UID)
+ iMustHave |= PVR_TIMER_TYPE_IS_MANUAL;
+ else
+ iMustNotHave |= PVR_TIMER_TYPE_IS_MANUAL;
+
+ const std::shared_ptr<CPVRTimerType> type =
+ CPVRTimerType::CreateFromAttributes(iMustHave, iMustNotHave, m_iClientId);
+
+ if (type)
+ SetTimerType(type);
+ }
+ // end compat section
+ else
+ {
+ SetTimerType(CPVRTimerType::CreateFromIds(timer.iTimerType, m_iClientId));
+ }
+
+ if (!m_timerType)
+ {
+ CLog::LogF(LOGERROR, "No timer type, although timers are supported by client {}!",
+ m_iClientId);
+ throw std::logic_error("CPVRTimerInfoTag::CPVRTimerInfoTag - Unable to obtain timer type!");
+ }
+ else if (m_iEpgUid == EPG_TAG_INVALID_UID && m_timerType->IsEpgBasedOnetime())
+ {
+ CLog::LogF(LOGERROR, "No epg tag given for epg based timer type ({})!",
+ m_timerType->GetTypeId());
+ }
+ }
+
+ UpdateSummary();
+ UpdateEpgInfoTag();
+}
+
+bool CPVRTimerInfoTag::operator==(const CPVRTimerInfoTag& right) const
+{
+ bool bChannelsMatch = true;
+ if (m_channel && right.m_channel)
+ bChannelsMatch = *m_channel == *right.m_channel;
+ else if (m_channel != right.m_channel)
+ bChannelsMatch = false;
+
+ return (bChannelsMatch && m_iClientIndex == right.m_iClientIndex &&
+ m_iParentClientIndex == right.m_iParentClientIndex &&
+ m_strSummary == right.m_strSummary && m_iClientChannelUid == right.m_iClientChannelUid &&
+ m_bIsRadio == right.m_bIsRadio && m_iPreventDupEpisodes == right.m_iPreventDupEpisodes &&
+ m_iRecordingGroup == right.m_iRecordingGroup && m_StartTime == right.m_StartTime &&
+ m_StopTime == right.m_StopTime && m_bStartAnyTime == right.m_bStartAnyTime &&
+ m_bEndAnyTime == right.m_bEndAnyTime && m_FirstDay == right.m_FirstDay &&
+ m_iWeekdays == right.m_iWeekdays && m_iPriority == right.m_iPriority &&
+ m_iLifetime == right.m_iLifetime && m_iMaxRecordings == right.m_iMaxRecordings &&
+ m_strFileNameAndPath == right.m_strFileNameAndPath && m_strTitle == right.m_strTitle &&
+ m_strEpgSearchString == right.m_strEpgSearchString &&
+ m_bFullTextEpgSearch == right.m_bFullTextEpgSearch &&
+ m_strDirectory == right.m_strDirectory && m_iClientId == right.m_iClientId &&
+ m_iMarginStart == right.m_iMarginStart && m_iMarginEnd == right.m_iMarginEnd &&
+ m_state == right.m_state && m_timerType == right.m_timerType &&
+ m_iTimerId == right.m_iTimerId && m_strSeriesLink == right.m_strSeriesLink &&
+ m_iEpgUid == right.m_iEpgUid && m_iTVChildTimersActive == right.m_iTVChildTimersActive &&
+ m_iTVChildTimersConflictNOK == right.m_iTVChildTimersConflictNOK &&
+ m_iTVChildTimersRecording == right.m_iTVChildTimersRecording &&
+ m_iTVChildTimersErrors == right.m_iTVChildTimersErrors &&
+ m_iRadioChildTimersActive == right.m_iRadioChildTimersActive &&
+ m_iRadioChildTimersConflictNOK == right.m_iRadioChildTimersConflictNOK &&
+ m_iRadioChildTimersRecording == right.m_iRadioChildTimersRecording &&
+ m_iRadioChildTimersErrors == right.m_iRadioChildTimersErrors);
+}
+
+bool CPVRTimerInfoTag::operator!=(const CPVRTimerInfoTag& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRTimerInfoTag::FillAddonData(PVR_TIMER& timer) const
+{
+ time_t start, end, firstDay;
+ StartAsUTC().GetAsTime(start);
+ EndAsUTC().GetAsTime(end);
+ FirstDayAsUTC().GetAsTime(firstDay);
+ const std::shared_ptr<CPVREpgInfoTag> epgTag = GetEpgInfoTag();
+ const int iPVRTimeCorrection =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeCorrection;
+
+ timer = {};
+ timer.iClientIndex = m_iClientIndex;
+ timer.iParentClientIndex = m_iParentClientIndex;
+ timer.state = m_state;
+ timer.iTimerType = GetTimerType()->GetTypeId();
+ timer.iClientChannelUid = m_iClientChannelUid;
+ strncpy(timer.strTitle, m_strTitle.c_str(), sizeof(timer.strTitle) - 1);
+ strncpy(timer.strEpgSearchString, m_strEpgSearchString.c_str(),
+ sizeof(timer.strEpgSearchString) - 1);
+ timer.bFullTextEpgSearch = m_bFullTextEpgSearch;
+ strncpy(timer.strDirectory, m_strDirectory.c_str(), sizeof(timer.strDirectory) - 1);
+ timer.iPriority = m_iPriority;
+ timer.iLifetime = m_iLifetime;
+ timer.iMaxRecordings = m_iMaxRecordings;
+ timer.iPreventDuplicateEpisodes = m_iPreventDupEpisodes;
+ timer.iRecordingGroup = m_iRecordingGroup;
+ timer.iWeekdays = m_iWeekdays;
+ timer.startTime = start - iPVRTimeCorrection;
+ timer.endTime = end - iPVRTimeCorrection;
+ timer.bStartAnyTime = m_bStartAnyTime;
+ timer.bEndAnyTime = m_bEndAnyTime;
+ timer.firstDay = firstDay - iPVRTimeCorrection;
+ timer.iEpgUid = epgTag ? epgTag->UniqueBroadcastID() : PVR_TIMER_NO_EPG_UID;
+ strncpy(timer.strSummary, m_strSummary.c_str(), sizeof(timer.strSummary) - 1);
+ timer.iMarginStart = m_iMarginStart;
+ timer.iMarginEnd = m_iMarginEnd;
+ timer.iGenreType = epgTag ? epgTag->GenreType() : 0;
+ timer.iGenreSubType = epgTag ? epgTag->GenreSubType() : 0;
+ strncpy(timer.strSeriesLink, SeriesLink().c_str(), sizeof(timer.strSeriesLink) - 1);
+}
+
+void CPVRTimerInfoTag::Serialize(CVariant& value) const
+{
+ value["channelid"] = m_channel != NULL ? m_channel->ChannelID() : -1;
+ value["summary"] = m_strSummary;
+ value["isradio"] = m_bIsRadio;
+ value["preventduplicateepisodes"] = m_iPreventDupEpisodes;
+ value["starttime"] = m_StartTime.IsValid() ? m_StartTime.GetAsDBDateTime() : "";
+ value["endtime"] = m_StopTime.IsValid() ? m_StopTime.GetAsDBDateTime() : "";
+ value["startanytime"] = m_bStartAnyTime;
+ value["endanytime"] = m_bEndAnyTime;
+ value["runtime"] = m_StartTime.IsValid() && m_StopTime.IsValid()
+ ? (m_StopTime - m_StartTime).GetSecondsTotal()
+ : 0;
+ value["firstday"] = m_FirstDay.IsValid() ? m_FirstDay.GetAsDBDate() : "";
+
+ CVariant weekdays(CVariant::VariantTypeArray);
+ if (m_iWeekdays & PVR_WEEKDAY_MONDAY)
+ weekdays.push_back("monday");
+ if (m_iWeekdays & PVR_WEEKDAY_TUESDAY)
+ weekdays.push_back("tuesday");
+ if (m_iWeekdays & PVR_WEEKDAY_WEDNESDAY)
+ weekdays.push_back("wednesday");
+ if (m_iWeekdays & PVR_WEEKDAY_THURSDAY)
+ weekdays.push_back("thursday");
+ if (m_iWeekdays & PVR_WEEKDAY_FRIDAY)
+ weekdays.push_back("friday");
+ if (m_iWeekdays & PVR_WEEKDAY_SATURDAY)
+ weekdays.push_back("saturday");
+ if (m_iWeekdays & PVR_WEEKDAY_SUNDAY)
+ weekdays.push_back("sunday");
+ value["weekdays"] = weekdays;
+
+ value["priority"] = m_iPriority;
+ value["lifetime"] = m_iLifetime;
+ value["title"] = m_strTitle;
+ value["directory"] = m_strDirectory;
+ value["startmargin"] = m_iMarginStart;
+ value["endmargin"] = m_iMarginEnd;
+
+ value["timerid"] = m_iTimerId;
+
+ switch (m_state)
+ {
+ case PVR_TIMER_STATE_NEW:
+ value["state"] = "new";
+ break;
+ case PVR_TIMER_STATE_SCHEDULED:
+ value["state"] = "scheduled";
+ break;
+ case PVR_TIMER_STATE_RECORDING:
+ value["state"] = "recording";
+ break;
+ case PVR_TIMER_STATE_COMPLETED:
+ value["state"] = "completed";
+ break;
+ case PVR_TIMER_STATE_ABORTED:
+ value["state"] = "aborted";
+ break;
+ case PVR_TIMER_STATE_CANCELLED:
+ value["state"] = "cancelled";
+ break;
+ case PVR_TIMER_STATE_CONFLICT_OK:
+ value["state"] = "conflict_ok";
+ break;
+ case PVR_TIMER_STATE_CONFLICT_NOK:
+ value["state"] = "conflict_notok";
+ break;
+ case PVR_TIMER_STATE_ERROR:
+ value["state"] = "error";
+ break;
+ case PVR_TIMER_STATE_DISABLED:
+ value["state"] = "disabled";
+ break;
+ default:
+ value["state"] = "unknown";
+ break;
+ }
+
+ value["istimerrule"] = m_timerType->IsTimerRule();
+ value["ismanual"] = m_timerType->IsManual();
+ value["isreadonly"] = m_timerType->IsReadOnly();
+ value["isreminder"] = m_timerType->IsReminder();
+
+ value["epgsearchstring"] = m_strEpgSearchString;
+ value["fulltextepgsearch"] = m_bFullTextEpgSearch;
+ value["recordinggroup"] = m_iRecordingGroup;
+ value["maxrecordings"] = m_iMaxRecordings;
+ value["epguid"] = m_iEpgUid;
+ value["broadcastid"] = m_epgTag ? m_epgTag->DatabaseID() : -1;
+ value["serieslink"] = m_strSeriesLink;
+
+ value["clientid"] = m_iClientId;
+}
+
+void CPVRTimerInfoTag::UpdateSummary()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_strSummary.clear();
+
+ const std::string startDate(StartAsLocalTime().GetAsLocalizedDate());
+ const std::string endDate(EndAsLocalTime().GetAsLocalizedDate());
+
+ if (m_bEndAnyTime)
+ {
+ m_strSummary = StringUtils::Format(
+ "{} {} {}",
+ m_iWeekdays != PVR_WEEKDAY_NONE ? GetWeekdaysString()
+ : startDate, //for "Any day" set PVR_WEEKDAY_ALLDAYS
+ g_localizeStrings.Get(19107), // "at"
+ m_bStartAnyTime ? g_localizeStrings.Get(19161) /* "any time" */
+ : StartAsLocalTime().GetAsLocalizedTime("", false));
+ }
+ else if ((m_iWeekdays != PVR_WEEKDAY_NONE) || (startDate == endDate))
+ {
+ m_strSummary = StringUtils::Format(
+ "{} {} {} {} {}",
+ m_iWeekdays != PVR_WEEKDAY_NONE ? GetWeekdaysString()
+ : startDate, //for "Any day" set PVR_WEEKDAY_ALLDAYS
+ g_localizeStrings.Get(19159), // "from"
+ m_bStartAnyTime ? g_localizeStrings.Get(19161) /* "any time" */
+ : StartAsLocalTime().GetAsLocalizedTime("", false),
+ g_localizeStrings.Get(19160), // "to"
+ m_bEndAnyTime ? g_localizeStrings.Get(19161) /* "any time" */
+ : EndAsLocalTime().GetAsLocalizedTime("", false));
+ }
+ else
+ {
+ m_strSummary =
+ StringUtils::Format("{} {} {} {} {} {}", startDate,
+ g_localizeStrings.Get(19159), // "from"
+ m_bStartAnyTime ? g_localizeStrings.Get(19161) /* "any time" */
+ : StartAsLocalTime().GetAsLocalizedTime("", false),
+ g_localizeStrings.Get(19160), // "to"
+ endDate,
+ m_bEndAnyTime ? g_localizeStrings.Get(19161) /* "any time" */
+ : EndAsLocalTime().GetAsLocalizedTime("", false));
+ }
+}
+
+void CPVRTimerInfoTag::SetTimerType(const std::shared_ptr<CPVRTimerType>& type)
+{
+ if (!type)
+ throw std::logic_error("CPVRTimerInfoTag::SetTimerType - Attempt to set 'null' timer type!");
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_timerType = type;
+
+ if (m_iClientIndex == PVR_TIMER_NO_CLIENT_INDEX)
+ {
+ m_iPriority = m_timerType->GetPriorityDefault();
+ m_iLifetime = m_timerType->GetLifetimeDefault();
+ m_iMaxRecordings = m_timerType->GetMaxRecordingsDefault();
+ m_iPreventDupEpisodes = m_timerType->GetPreventDuplicateEpisodesDefault();
+ m_iRecordingGroup = m_timerType->GetRecordingGroupDefault();
+ }
+
+ if (!m_timerType->IsTimerRule())
+ m_iWeekdays = PVR_WEEKDAY_NONE;
+}
+
+std::string CPVRTimerInfoTag::GetStatus(bool bRadio) const
+{
+ std::string strReturn = g_localizeStrings.Get(305);
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (URIUtils::PathEquals(m_strFileNameAndPath, CPVRTimersPath::PATH_ADDTIMER))
+ strReturn = g_localizeStrings.Get(19026);
+ else if (m_state == PVR_TIMER_STATE_CANCELLED || m_state == PVR_TIMER_STATE_ABORTED)
+ strReturn = g_localizeStrings.Get(13106);
+ else if (m_state == PVR_TIMER_STATE_RECORDING)
+ strReturn = g_localizeStrings.Get(19162);
+ else if (m_state == PVR_TIMER_STATE_CONFLICT_OK)
+ strReturn = g_localizeStrings.Get(19275);
+ else if (m_state == PVR_TIMER_STATE_CONFLICT_NOK)
+ strReturn = g_localizeStrings.Get(19276);
+ else if (m_state == PVR_TIMER_STATE_ERROR)
+ strReturn = g_localizeStrings.Get(257);
+ else if (m_state == PVR_TIMER_STATE_DISABLED)
+ strReturn = g_localizeStrings.Get(13106);
+ else if (m_state == PVR_TIMER_STATE_COMPLETED)
+ {
+ if ((m_iTVChildTimersRecording > 0 && !bRadio) || (m_iRadioChildTimersRecording > 0 && bRadio))
+ strReturn = g_localizeStrings.Get(19162); // "Recording active"
+ else
+ strReturn = g_localizeStrings.Get(19256); // "Completed"
+ }
+ else if (m_state == PVR_TIMER_STATE_SCHEDULED || m_state == PVR_TIMER_STATE_NEW)
+ {
+ if ((m_iTVChildTimersRecording > 0 && !bRadio) || (m_iRadioChildTimersRecording > 0 && bRadio))
+ strReturn = g_localizeStrings.Get(19162); // "Recording active"
+ else if ((m_iTVChildTimersErrors > 0 && !bRadio) || (m_iRadioChildTimersErrors > 0 && bRadio))
+ strReturn = g_localizeStrings.Get(257); // "Error"
+ else if ((m_iTVChildTimersConflictNOK > 0 && !bRadio) ||
+ (m_iRadioChildTimersConflictNOK > 0 && bRadio))
+ strReturn = g_localizeStrings.Get(19276); // "Conflict error"
+ else if ((m_iTVChildTimersActive > 0 && !bRadio) || (m_iRadioChildTimersActive > 0 && bRadio))
+ strReturn = StringUtils::Format(g_localizeStrings.Get(19255),
+ bRadio ? m_iRadioChildTimersActive
+ : m_iTVChildTimersActive); // "{} scheduled"
+ }
+
+ return strReturn;
+}
+
+/**
+ * Get the type string of this timer
+ */
+std::string CPVRTimerInfoTag::GetTypeAsString() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_timerType->GetDescription();
+}
+
+namespace
+{
+void AppendDay(std::string& strReturn, unsigned int iId)
+{
+ if (!strReturn.empty())
+ strReturn += "-";
+
+ if (iId > 0)
+ strReturn += g_localizeStrings.Get(iId).c_str();
+ else
+ strReturn += "__";
+}
+} // unnamed namespace
+
+std::string CPVRTimerInfoTag::GetWeekdaysString(unsigned int iWeekdays,
+ bool bEpgBased,
+ bool bLongMultiDaysFormat)
+{
+ std::string strReturn;
+
+ if (iWeekdays == PVR_WEEKDAY_NONE)
+ return strReturn;
+ else if (iWeekdays == PVR_WEEKDAY_ALLDAYS)
+ strReturn = bEpgBased ? g_localizeStrings.Get(807) // "Any day"
+ : g_localizeStrings.Get(808); // "Every day"
+ else if (iWeekdays == PVR_WEEKDAY_MONDAY)
+ strReturn = g_localizeStrings.Get(831); // "Mondays"
+ else if (iWeekdays == PVR_WEEKDAY_TUESDAY)
+ strReturn = g_localizeStrings.Get(832); // "Tuesdays"
+ else if (iWeekdays == PVR_WEEKDAY_WEDNESDAY)
+ strReturn = g_localizeStrings.Get(833); // "Wednesdays"
+ else if (iWeekdays == PVR_WEEKDAY_THURSDAY)
+ strReturn = g_localizeStrings.Get(834); // "Thursdays"
+ else if (iWeekdays == PVR_WEEKDAY_FRIDAY)
+ strReturn = g_localizeStrings.Get(835); // "Fridays"
+ else if (iWeekdays == PVR_WEEKDAY_SATURDAY)
+ strReturn = g_localizeStrings.Get(836); // "Saturdays"
+ else if (iWeekdays == PVR_WEEKDAY_SUNDAY)
+ strReturn = g_localizeStrings.Get(837); // "Sundays"
+ else
+ {
+ // Any other combination. Assemble custom string.
+ if (iWeekdays & PVR_WEEKDAY_MONDAY)
+ AppendDay(strReturn, 19149); // Mo
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_TUESDAY)
+ AppendDay(strReturn, 19150); // Tu
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_WEDNESDAY)
+ AppendDay(strReturn, 19151); // We
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_THURSDAY)
+ AppendDay(strReturn, 19152); // Th
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_FRIDAY)
+ AppendDay(strReturn, 19153); // Fr
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_SATURDAY)
+ AppendDay(strReturn, 19154); // Sa
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+
+ if (iWeekdays & PVR_WEEKDAY_SUNDAY)
+ AppendDay(strReturn, 19155); // So
+ else if (bLongMultiDaysFormat)
+ AppendDay(strReturn, 0);
+ }
+ return strReturn;
+}
+
+std::string CPVRTimerInfoTag::GetWeekdaysString() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return GetWeekdaysString(m_iWeekdays, m_timerType->IsEpgBased(), false);
+}
+
+bool CPVRTimerInfoTag::IsOwnedByClient() const
+{
+ return m_timerType->GetClientId() > -1;
+}
+
+bool CPVRTimerInfoTag::AddToClient() const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ if (client)
+ return client->AddTimer(*this) == PVR_ERROR_NO_ERROR;
+ return false;
+}
+
+TimerOperationResult CPVRTimerInfoTag::DeleteFromClient(bool bForce /* = false */) const
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ PVR_ERROR error = PVR_ERROR_UNKNOWN;
+
+ if (client)
+ error = client->DeleteTimer(*this, bForce);
+
+ if (error == PVR_ERROR_RECORDING_RUNNING)
+ return TimerOperationResult::RECORDING;
+
+ return (error == PVR_ERROR_NO_ERROR) ? TimerOperationResult::OK : TimerOperationResult::FAILED;
+}
+
+bool CPVRTimerInfoTag::Persist()
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ return database->Persist(*this);
+
+ return false;
+}
+
+bool CPVRTimerInfoTag::DeleteFromDatabase()
+{
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ return database->Delete(*this);
+
+ return false;
+}
+
+bool CPVRTimerInfoTag::UpdateEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ m_iClientId = tag->m_iClientId;
+ m_iClientIndex = tag->m_iClientIndex;
+ m_iParentClientIndex = tag->m_iParentClientIndex;
+ m_strTitle = tag->m_strTitle;
+ m_strEpgSearchString = tag->m_strEpgSearchString;
+ m_bFullTextEpgSearch = tag->m_bFullTextEpgSearch;
+ m_strDirectory = tag->m_strDirectory;
+ m_iClientChannelUid = tag->m_iClientChannelUid;
+ m_StartTime = tag->m_StartTime;
+ m_StopTime = tag->m_StopTime;
+ m_bStartAnyTime = tag->m_bStartAnyTime;
+ m_bEndAnyTime = tag->m_bEndAnyTime;
+ m_FirstDay = tag->m_FirstDay;
+ m_iPriority = tag->m_iPriority;
+ m_iLifetime = tag->m_iLifetime;
+ m_iMaxRecordings = tag->m_iMaxRecordings;
+ m_state = tag->m_state;
+ m_iPreventDupEpisodes = tag->m_iPreventDupEpisodes;
+ m_iRecordingGroup = tag->m_iRecordingGroup;
+ m_iWeekdays = tag->m_iWeekdays;
+ m_bIsRadio = tag->m_bIsRadio;
+ m_iMarginStart = tag->m_iMarginStart;
+ m_iMarginEnd = tag->m_iMarginEnd;
+ m_strSeriesLink = tag->m_strSeriesLink;
+ m_iEpgUid = tag->m_iEpgUid;
+ m_epgTag = tag->m_epgTag;
+ m_strSummary = tag->m_strSummary;
+ m_channel = tag->m_channel;
+ m_bProbedEpgTag = tag->m_bProbedEpgTag;
+
+ m_iTVChildTimersActive = tag->m_iTVChildTimersActive;
+ m_iTVChildTimersConflictNOK = tag->m_iTVChildTimersConflictNOK;
+ m_iTVChildTimersRecording = tag->m_iTVChildTimersRecording;
+ m_iTVChildTimersErrors = tag->m_iTVChildTimersErrors;
+ m_iRadioChildTimersActive = tag->m_iRadioChildTimersActive;
+ m_iRadioChildTimersConflictNOK = tag->m_iRadioChildTimersConflictNOK;
+ m_iRadioChildTimersRecording = tag->m_iRadioChildTimersRecording;
+ m_iRadioChildTimersErrors = tag->m_iRadioChildTimersErrors;
+
+ SetTimerType(tag->m_timerType);
+
+ if (m_strSummary.empty())
+ UpdateSummary();
+
+ UpdateEpgInfoTag();
+
+ return true;
+}
+
+bool CPVRTimerInfoTag::UpdateChildState(const std::shared_ptr<CPVRTimerInfoTag>& childTimer,
+ bool bAdd)
+{
+ if (!childTimer || childTimer->m_iParentClientIndex != m_iClientIndex)
+ return false;
+
+ int iDelta = bAdd ? +1 : -1;
+ switch (childTimer->m_state)
+ {
+ case PVR_TIMER_STATE_NEW:
+ case PVR_TIMER_STATE_SCHEDULED:
+ case PVR_TIMER_STATE_CONFLICT_OK:
+ if (childTimer->m_bIsRadio)
+ m_iRadioChildTimersActive += iDelta;
+ else
+ m_iTVChildTimersActive += iDelta;
+ break;
+ case PVR_TIMER_STATE_RECORDING:
+ if (childTimer->m_bIsRadio)
+ {
+ m_iRadioChildTimersActive += iDelta;
+ m_iRadioChildTimersRecording += iDelta;
+ }
+ else
+ {
+ m_iTVChildTimersActive += iDelta;
+ m_iTVChildTimersRecording += iDelta;
+ }
+ break;
+ case PVR_TIMER_STATE_CONFLICT_NOK:
+ if (childTimer->m_bIsRadio)
+ m_iRadioChildTimersConflictNOK += iDelta;
+ else
+ m_iTVChildTimersConflictNOK += iDelta;
+ break;
+ case PVR_TIMER_STATE_ERROR:
+ if (childTimer->m_bIsRadio)
+ m_iRadioChildTimersErrors += iDelta;
+ else
+ m_iTVChildTimersErrors += iDelta;
+ break;
+ case PVR_TIMER_STATE_COMPLETED:
+ case PVR_TIMER_STATE_ABORTED:
+ case PVR_TIMER_STATE_CANCELLED:
+ case PVR_TIMER_STATE_DISABLED:
+ //these are not the child timers we are looking for
+ break;
+ }
+ return true;
+}
+
+void CPVRTimerInfoTag::ResetChildState()
+{
+ m_iTVChildTimersActive = 0;
+ m_iTVChildTimersRecording = 0;
+ m_iTVChildTimersConflictNOK = 0;
+ m_iTVChildTimersErrors = 0;
+ m_iRadioChildTimersActive = 0;
+ m_iRadioChildTimersRecording = 0;
+ m_iRadioChildTimersConflictNOK = 0;
+ m_iRadioChildTimersErrors = 0;
+}
+
+bool CPVRTimerInfoTag::UpdateOnClient()
+{
+ const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(m_iClientId);
+ return client && (client->UpdateTimer(*this) == PVR_ERROR_NO_ERROR);
+}
+
+std::string CPVRTimerInfoTag::ChannelName() const
+{
+ std::string strReturn;
+ std::shared_ptr<CPVRChannel> channeltag = Channel();
+ if (channeltag)
+ strReturn = channeltag->ChannelName();
+ else if (m_timerType->IsEpgBasedTimerRule())
+ strReturn = StringUtils::Format("({})", g_localizeStrings.Get(809)); // "Any channel"
+
+ return strReturn;
+}
+
+std::string CPVRTimerInfoTag::ChannelIcon() const
+{
+ std::string strReturn;
+ std::shared_ptr<CPVRChannel> channeltag = Channel();
+ if (channeltag)
+ strReturn = channeltag->IconPath();
+ return strReturn;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateReminderFromDate(
+ const CDateTime& start,
+ int iDuration,
+ const std::shared_ptr<CPVRTimerInfoTag>& parent /* = std::shared_ptr<CPVRTimerInfoTag>() */)
+{
+ bool bReadOnly = !!parent; // children of reminder rules are always read-only
+ std::shared_ptr<CPVRTimerInfoTag> newTimer =
+ CreateFromDate(parent->Channel(), start, iDuration, true, bReadOnly);
+ if (newTimer && parent)
+ {
+ // set parent
+ newTimer->m_iParentClientIndex = parent->m_iClientIndex;
+
+ // set relevant props for the new one-time reminder timer
+ newTimer->m_strTitle = parent->Title();
+ }
+ return newTimer;
+}
+
+static const time_t INSTANT_TIMER_START =
+ 0; // PVR addon API: special start time value to denote an instant timer
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateInstantTimerTag(
+ const std::shared_ptr<CPVRChannel>& channel,
+ int iDuration /* = DEFAULT_PVRRECORD_INSTANTRECORDTIME */)
+{
+ return CreateFromDate(channel, CDateTime(INSTANT_TIMER_START), iDuration, false, false);
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateTimerTag(
+ const std::shared_ptr<CPVRChannel>& channel, const CDateTime& start, int iDuration)
+{
+ return CreateFromDate(channel, start, iDuration, false, false);
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateFromDate(
+ const std::shared_ptr<CPVRChannel>& channel,
+ const CDateTime& start,
+ int iDuration,
+ bool bCreateReminder,
+ bool bReadOnly)
+{
+ if (!channel)
+ {
+ CLog::LogF(LOGERROR, "No channel");
+ return std::shared_ptr<CPVRTimerInfoTag>();
+ }
+
+ bool bInstantStart = (start == CDateTime(INSTANT_TIMER_START));
+
+ std::shared_ptr<CPVREpgInfoTag> epgTag;
+ if (!bCreateReminder) // time-based reminders never have epg tags
+ {
+ if (bInstantStart)
+ epgTag = channel->GetEPGNow();
+ else if (channel->GetEPG())
+ epgTag = channel->GetEPG()->GetTagBetween(start, start + CDateTimeSpan(0, 0, iDuration, 0));
+ }
+
+ std::shared_ptr<CPVRTimerInfoTag> newTimer;
+ if (epgTag)
+ {
+ if (epgTag->IsRecordable())
+ {
+ newTimer = CreateFromEpg(epgTag, false, bCreateReminder, bReadOnly);
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "EPG tag is not recordable");
+ return std::shared_ptr<CPVRTimerInfoTag>();
+ }
+ }
+
+ if (!newTimer)
+ {
+ newTimer.reset(new CPVRTimerInfoTag);
+
+ newTimer->m_iClientIndex = PVR_TIMER_NO_CLIENT_INDEX;
+ newTimer->m_iParentClientIndex = PVR_TIMER_NO_PARENT;
+ newTimer->m_channel = channel;
+ newTimer->m_strTitle = channel->ChannelName();
+ newTimer->m_iClientChannelUid = channel->UniqueID();
+ newTimer->m_iClientId = channel->ClientID();
+ newTimer->m_bIsRadio = channel->IsRadio();
+
+ uint64_t iMustHaveAttribs = PVR_TIMER_TYPE_IS_MANUAL;
+ if (bCreateReminder)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_REMINDER;
+ if (bReadOnly)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_READONLY;
+
+ // timertype: manual one-shot timer for given client
+ const std::shared_ptr<CPVRTimerType> timerType = CPVRTimerType::CreateFromAttributes(
+ iMustHaveAttribs, PVR_TIMER_TYPE_IS_REPEATING | PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES,
+ channel->ClientID());
+ if (!timerType)
+ {
+ CLog::LogF(LOGERROR, "Unable to create one shot manual timer type");
+ return std::shared_ptr<CPVRTimerInfoTag>();
+ }
+
+ newTimer->SetTimerType(timerType);
+
+ if (epgTag)
+ newTimer->SetEpgInfoTag(epgTag);
+ else
+ newTimer->UpdateEpgInfoTag();
+ }
+
+ /* no matter the timer was created from an epg tag, overwrite timer start and end times. */
+ CDateTime now(CDateTime::GetUTCDateTime());
+ if (bInstantStart)
+ newTimer->SetStartFromUTC(now);
+ else
+ newTimer->SetStartFromUTC(start);
+
+ if (iDuration == DEFAULT_PVRRECORD_INSTANTRECORDTIME)
+ iDuration = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME);
+
+ if (bInstantStart)
+ {
+ CDateTime endTime = now + CDateTimeSpan(0, 0, iDuration ? iDuration : 120, 0);
+ newTimer->SetEndFromUTC(endTime);
+ }
+ else
+ {
+ CDateTime endTime = start + CDateTimeSpan(0, 0, iDuration ? iDuration : 120, 0);
+ newTimer->SetEndFromUTC(endTime);
+ }
+
+ /* update summary string according to instant recording start/end time */
+ newTimer->UpdateSummary();
+
+ if (bInstantStart)
+ {
+ // "Instant recording: <summary>
+ newTimer->m_strSummary = StringUtils::Format(g_localizeStrings.Get(19093), newTimer->Summary());
+
+ // now that we have a nice summary, we can set the "special" start time value that indicates an instant recording
+ newTimer->SetStartFromUTC(start);
+ }
+
+ newTimer->m_iMarginStart = 0;
+ newTimer->m_iMarginEnd = 0;
+
+ /* unused only for reference */
+ newTimer->m_strFileNameAndPath = CPVRTimersPath::PATH_NEW;
+
+ return newTimer;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateReminderFromEpg(
+ const std::shared_ptr<CPVREpgInfoTag>& tag,
+ const std::shared_ptr<CPVRTimerInfoTag>& parent /* = std::shared_ptr<CPVRTimerInfoTag>() */)
+{
+ bool bReadOnly = !!parent; // children of reminder rules are always read-only
+ std::shared_ptr<CPVRTimerInfoTag> newTimer = CreateFromEpg(tag, false, true, bReadOnly);
+ if (newTimer && parent)
+ {
+ // set parent
+ newTimer->m_iParentClientIndex = parent->m_iClientIndex;
+
+ // set relevant props for the new one-time reminder timer (everything not epg-related)
+ newTimer->m_iMarginStart = parent->m_iMarginStart;
+ newTimer->m_iMarginEnd = parent->m_iMarginEnd;
+ }
+ return newTimer;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateFromEpg(
+ const std::shared_ptr<CPVREpgInfoTag>& tag, bool bCreateRule /* = false */)
+{
+ return CreateFromEpg(tag, bCreateRule, false, false);
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimerInfoTag::CreateFromEpg(
+ const std::shared_ptr<CPVREpgInfoTag>& tag,
+ bool bCreateRule,
+ bool bCreateReminder,
+ bool bReadOnly /* = false */)
+{
+ std::shared_ptr<CPVRTimerInfoTag> newTag(new CPVRTimerInfoTag());
+
+ /* check if a valid channel is set */
+ const std::shared_ptr<CPVRChannel> channel =
+ CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelForEpgTag(tag);
+ if (!channel)
+ {
+ CLog::LogF(LOGERROR, "EPG tag has no channel");
+ return std::shared_ptr<CPVRTimerInfoTag>();
+ }
+
+ newTag->m_iClientIndex = PVR_TIMER_NO_CLIENT_INDEX;
+ newTag->m_iParentClientIndex = PVR_TIMER_NO_PARENT;
+ if (!CServiceBroker::GetPVRManager().IsParentalLocked(tag))
+ newTag->m_strTitle = tag->Title();
+ if (newTag->m_strTitle.empty())
+ newTag->m_strTitle = channel->ChannelName();
+ newTag->m_iClientChannelUid = channel->UniqueID();
+ newTag->m_iClientId = channel->ClientID();
+ newTag->m_bIsRadio = channel->IsRadio();
+ newTag->m_channel = channel;
+ newTag->m_strSeriesLink = tag->SeriesLink();
+ newTag->m_iEpgUid = tag->UniqueBroadcastID();
+ newTag->SetStartFromUTC(tag->StartAsUTC());
+ newTag->SetEndFromUTC(tag->EndAsUTC());
+
+ const uint64_t iMustNotHaveAttribs = PVR_TIMER_TYPE_IS_MANUAL |
+ PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES |
+ PVR_TIMER_TYPE_FORBIDS_EPG_TAG_ON_CREATE;
+ std::shared_ptr<CPVRTimerType> timerType;
+ if (bCreateRule)
+ {
+ // create epg-based timer rule, prefer rule using series link if available.
+
+ uint64_t iMustHaveAttribs = PVR_TIMER_TYPE_IS_REPEATING;
+ if (bCreateReminder)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_REMINDER;
+ if (bReadOnly)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_READONLY;
+
+ if (!tag->SeriesLink().empty())
+ timerType = CPVRTimerType::CreateFromAttributes(
+ iMustHaveAttribs | PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE, iMustNotHaveAttribs,
+ channel->ClientID());
+ if (!timerType)
+ timerType = CPVRTimerType::CreateFromAttributes(
+ iMustHaveAttribs, iMustNotHaveAttribs | PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE,
+ channel->ClientID());
+ if (timerType)
+ {
+ if (timerType->SupportsEpgTitleMatch())
+ newTag->m_strEpgSearchString = newTag->m_strTitle;
+
+ if (timerType->SupportsWeekdays())
+ newTag->m_iWeekdays = PVR_WEEKDAY_ALLDAYS;
+
+ if (timerType->SupportsStartAnyTime())
+ newTag->m_bStartAnyTime = true;
+
+ if (timerType->SupportsEndAnyTime())
+ newTag->m_bEndAnyTime = true;
+ }
+ }
+ else
+ {
+ // create one-shot epg-based timer
+
+ uint64_t iMustHaveAttribs = PVR_TIMER_TYPE_ATTRIBUTE_NONE;
+ if (bCreateReminder)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_REMINDER;
+ if (bReadOnly)
+ iMustHaveAttribs |= PVR_TIMER_TYPE_IS_READONLY;
+
+ timerType = CPVRTimerType::CreateFromAttributes(
+ iMustHaveAttribs, PVR_TIMER_TYPE_IS_REPEATING | iMustNotHaveAttribs, channel->ClientID());
+ }
+
+ if (!timerType)
+ {
+ CLog::LogF(LOGERROR, "Unable to create any epg-based timer type");
+ return std::shared_ptr<CPVRTimerInfoTag>();
+ }
+
+ newTag->SetTimerType(timerType);
+ newTag->UpdateSummary();
+ newTag->SetEpgInfoTag(tag);
+
+ /* unused only for reference */
+ newTag->m_strFileNameAndPath = CPVRTimersPath::PATH_NEW;
+
+ return newTag;
+}
+
+//! @todo CDateTime class does not handle daylight saving timezone bias correctly (and cannot easily
+// be changed to do so due to performance and platform specific issues). In most cases this only
+// causes GUI presentation glitches, but reminder timer rules rely on correct local time values.
+
+namespace
+{
+#define IsLeapYear(y) ((y % 4 == 0) && (y % 100 != 0 || y % 400 == 0))
+
+int days_from_0(int year)
+{
+ year--;
+ return 365 * year + (year / 400) - (year / 100) + (year / 4);
+}
+
+int days_from_1970(int32_t year)
+{
+ static const int days_from_0_to_1970 = days_from_0(1970);
+ return days_from_0(year) - days_from_0_to_1970;
+}
+
+int days_from_1jan(int year, int month, int day)
+{
+ static const int days[2][12] = {{0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334},
+ {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}};
+ return days[IsLeapYear(year)][month - 1] + day - 1;
+}
+
+time_t mytimegm(struct tm* time)
+{
+ int year = time->tm_year + 1900;
+ int month = time->tm_mon;
+
+ if (month > 11)
+ {
+ year += month / 12;
+ month %= 12;
+ }
+ else if (month < 0)
+ {
+ int years_diff = (-month + 11) / 12;
+ year -= years_diff;
+ month += 12 * years_diff;
+ }
+
+ month++;
+
+ int day_of_year = days_from_1jan(year, month, time->tm_mday);
+ int days_since_epoch = days_from_1970(year) + day_of_year;
+
+ return 3600 * 24 * days_since_epoch + 3600 * time->tm_hour + 60 * time->tm_min + time->tm_sec;
+}
+} // namespace
+
+CDateTime CPVRTimerInfoTag::ConvertUTCToLocalTime(const CDateTime& utc)
+{
+ time_t time = 0;
+ utc.GetAsTime(time);
+
+ struct tm* tms;
+#ifdef HAVE_LOCALTIME_R
+ struct tm gbuf;
+ tms = localtime_r(&time, &gbuf);
+#else
+ tms = localtime(&time);
+#endif
+
+ if (!tms)
+ {
+ CLog::LogF(LOGWARNING, "localtime() returned NULL!");
+ return {};
+ }
+
+ return CDateTime(mytimegm(tms));
+}
+
+CDateTime CPVRTimerInfoTag::ConvertLocalTimeToUTC(const CDateTime& local)
+{
+ time_t time = 0;
+ local.GetAsTime(time);
+
+ struct tm* tms;
+
+ // obtain dst flag for given datetime
+#ifdef HAVE_LOCALTIME_R
+ struct tm loc_buf;
+ tms = localtime_r(&time, &loc_buf);
+#else
+ tms = localtime(&time);
+#endif
+
+ if (!tms)
+ {
+ CLog::LogF(LOGWARNING, "localtime() returned NULL!");
+ return {};
+ }
+
+ int isdst = tms->tm_isdst;
+
+#ifdef HAVE_GMTIME_R
+ struct tm gm_buf;
+ tms = gmtime_r(&time, &gm_buf);
+#else
+ tms = gmtime(&time);
+#endif
+
+ if (!tms)
+ {
+ CLog::LogF(LOGWARNING, "gmtime() returned NULL!");
+ return {};
+ }
+
+ tms->tm_isdst = isdst;
+ return CDateTime(mktime(tms));
+}
+
+CDateTime CPVRTimerInfoTag::StartAsUTC() const
+{
+ return m_StartTime;
+}
+
+CDateTime CPVRTimerInfoTag::StartAsLocalTime() const
+{
+ return ConvertUTCToLocalTime(m_StartTime);
+}
+
+void CPVRTimerInfoTag::SetStartFromUTC(const CDateTime& start)
+{
+ m_StartTime = start;
+}
+
+void CPVRTimerInfoTag::SetStartFromLocalTime(const CDateTime& start)
+{
+ m_StartTime = ConvertLocalTimeToUTC(start);
+}
+
+CDateTime CPVRTimerInfoTag::EndAsUTC() const
+{
+ return m_StopTime;
+}
+
+CDateTime CPVRTimerInfoTag::EndAsLocalTime() const
+{
+ return ConvertUTCToLocalTime(m_StopTime);
+}
+
+void CPVRTimerInfoTag::SetEndFromUTC(const CDateTime& end)
+{
+ m_StopTime = end;
+}
+
+void CPVRTimerInfoTag::SetEndFromLocalTime(const CDateTime& end)
+{
+ m_StopTime = ConvertLocalTimeToUTC(end);
+}
+
+int CPVRTimerInfoTag::GetDuration() const
+{
+ time_t start, end;
+ m_StartTime.GetAsTime(start);
+ m_StopTime.GetAsTime(end);
+ return end - start > 0 ? end - start : 3600;
+}
+
+CDateTime CPVRTimerInfoTag::FirstDayAsUTC() const
+{
+ return m_FirstDay;
+}
+
+CDateTime CPVRTimerInfoTag::FirstDayAsLocalTime() const
+{
+ return ConvertUTCToLocalTime(m_FirstDay);
+}
+
+void CPVRTimerInfoTag::SetFirstDayFromUTC(const CDateTime& firstDay)
+{
+ m_FirstDay = firstDay;
+}
+
+void CPVRTimerInfoTag::SetFirstDayFromLocalTime(const CDateTime& firstDay)
+{
+ m_FirstDay = ConvertLocalTimeToUTC(firstDay);
+}
+
+std::string CPVRTimerInfoTag::GetNotificationText() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ int stringID = 0;
+
+ switch (m_state)
+ {
+ case PVR_TIMER_STATE_ABORTED:
+ case PVR_TIMER_STATE_CANCELLED:
+ stringID = 19224; // Recording aborted
+ break;
+ case PVR_TIMER_STATE_SCHEDULED:
+ if (IsTimerRule())
+ stringID = 19058; // Timer enabled
+ else
+ stringID = 19225; // Recording scheduled
+ break;
+ case PVR_TIMER_STATE_RECORDING:
+ stringID = 19226; // Recording started
+ break;
+ case PVR_TIMER_STATE_COMPLETED:
+ stringID = 19227; // Recording completed
+ break;
+ case PVR_TIMER_STATE_CONFLICT_OK:
+ case PVR_TIMER_STATE_CONFLICT_NOK:
+ stringID = 19277; // Recording conflict
+ break;
+ case PVR_TIMER_STATE_ERROR:
+ stringID = 19278; // Recording error
+ break;
+ case PVR_TIMER_STATE_DISABLED:
+ stringID = 19057; // Timer disabled
+ break;
+ default:
+ break;
+ }
+
+ if (stringID != 0)
+ return StringUtils::Format("{}: '{}'", g_localizeStrings.Get(stringID), m_strTitle);
+
+ return {};
+}
+
+std::string CPVRTimerInfoTag::GetDeletedNotificationText() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ int stringID = 0;
+ // The state in this case is the state the timer had when it was last seen
+ switch (m_state)
+ {
+ case PVR_TIMER_STATE_RECORDING:
+ stringID = 19227; // Recording completed
+ break;
+ case PVR_TIMER_STATE_SCHEDULED:
+ default:
+ if (IsTimerRule())
+ stringID = 828; // Timer rule deleted
+ else
+ stringID = 19228; // Timer deleted
+ }
+
+ return StringUtils::Format("{}: '{}'", g_localizeStrings.Get(stringID), m_strTitle);
+}
+
+void CPVRTimerInfoTag::SetEpgInfoTag(const std::shared_ptr<CPVREpgInfoTag>& tag)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_epgTag = tag;
+ m_bProbedEpgTag = true;
+}
+
+void CPVRTimerInfoTag::UpdateEpgInfoTag()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_epgTag.reset();
+ m_bProbedEpgTag = false;
+ GetEpgInfoTag();
+}
+
+std::shared_ptr<CPVREpgInfoTag> CPVRTimerInfoTag::GetEpgInfoTag(bool bCreate /* = true */) const
+{
+ if (!m_epgTag && !m_bProbedEpgTag && bCreate && CServiceBroker::GetPVRManager().EpgsCreated())
+ {
+ std::shared_ptr<CPVRChannel> channel(m_channel);
+ if (!channel)
+ {
+ channel = CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(m_bIsRadio)
+ ->GetGroupAll()
+ ->GetByUniqueID(m_iClientChannelUid, m_iClientId);
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channel = channel;
+ }
+
+ if (channel)
+ {
+ const std::shared_ptr<CPVREpg> epg(channel->GetEPG());
+ if (epg)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_epgTag && m_iEpgUid != EPG_TAG_INVALID_UID)
+ {
+ m_epgTag = epg->GetTagByBroadcastId(m_iEpgUid);
+ }
+
+ if (!m_epgTag && !IsTimerRule() && IsOwnedByClient())
+ {
+ time_t startTime = 0;
+ time_t endTime = 0;
+
+ StartAsUTC().GetAsTime(startTime);
+ if (startTime > 0)
+ EndAsUTC().GetAsTime(endTime);
+
+ if (startTime > 0 && endTime > 0)
+ {
+ // try to fetch missing epg tag from backend
+ m_epgTag = epg->GetTagBetween(StartAsUTC() - CDateTimeSpan(0, 0, 2, 0),
+ EndAsUTC() + CDateTimeSpan(0, 0, 2, 0), true);
+ if (m_epgTag)
+ m_iEpgUid = m_epgTag->UniqueBroadcastID();
+ }
+ }
+ }
+ }
+ m_bProbedEpgTag = true;
+ }
+ return m_epgTag;
+}
+
+bool CPVRTimerInfoTag::HasChannel() const
+{
+ return m_channel.get() != nullptr;
+}
+
+std::shared_ptr<CPVRChannel> CPVRTimerInfoTag::Channel() const
+{
+ return m_channel;
+}
+
+void CPVRTimerInfoTag::UpdateChannel()
+{
+ const std::shared_ptr<CPVRChannel> channel(CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->Get(m_bIsRadio)
+ ->GetGroupAll()
+ ->GetByUniqueID(m_iClientChannelUid, m_iClientId));
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channel = channel;
+}
+
+const std::string& CPVRTimerInfoTag::Title() const
+{
+ return m_strTitle;
+}
+
+const std::string& CPVRTimerInfoTag::Summary() const
+{
+ return m_strSummary;
+}
+
+const std::string& CPVRTimerInfoTag::Path() const
+{
+ return m_strFileNameAndPath;
+}
+
+const std::string& CPVRTimerInfoTag::SeriesLink() const
+{
+ return m_strSeriesLink;
+}
diff --git a/xbmc/pvr/timers/PVRTimerInfoTag.h b/xbmc/pvr/timers/PVRTimerInfoTag.h
new file mode 100644
index 0000000..ad2d3d8
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerInfoTag.h
@@ -0,0 +1,644 @@
+/*
+ * 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 "XBDateTime.h"
+#include "pvr/timers/PVRTimerType.h"
+#include "threads/CriticalSection.h"
+#include "utils/ISerializable.h"
+
+#include <memory>
+#include <string>
+
+struct PVR_TIMER;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVREpgInfoTag;
+
+enum class TimerOperationResult
+{
+ OK = 0,
+ FAILED,
+ RECORDING // The timer was not deleted because it is currently recording (see DeleteTimer).
+};
+
+class CPVRTimerInfoTag final : public ISerializable
+{
+ // allow these classes direct access to members as they act as timer tag instance factories.
+ friend class CGUIDialogPVRTimerSettings;
+ friend class CPVRDatabase;
+
+public:
+ explicit CPVRTimerInfoTag(bool bRadio = false);
+ CPVRTimerInfoTag(const PVR_TIMER& timer,
+ const std::shared_ptr<CPVRChannel>& channel,
+ unsigned int iClientId);
+
+ bool operator==(const CPVRTimerInfoTag& right) const;
+ bool operator!=(const CPVRTimerInfoTag& right) const;
+
+ /*!
+ * @brief Copy over data to the given PVR_TIMER instance.
+ * @param timer The timer instance to fill.
+ */
+ void FillAddonData(PVR_TIMER& timer) const;
+
+ // ISerializable implementation
+ void Serialize(CVariant& value) const override;
+
+ static constexpr int DEFAULT_PVRRECORD_INSTANTRECORDTIME = -1;
+
+ /*!
+ * @brief create a tag for an instant timer for a given channel
+ * @param channel is the channel the instant timer is to be created for
+ * @param iDuration is the duration for the instant timer, DEFAULT_PVRRECORD_INSTANTRECORDTIME
+ * denotes system default (setting value)
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateInstantTimerTag(
+ const std::shared_ptr<CPVRChannel>& channel,
+ int iDuration = DEFAULT_PVRRECORD_INSTANTRECORDTIME);
+
+ /*!
+ * @brief create a tag for a timer for a given channel at given time for given duration
+ * @param channel is the channel the timer is to be created for
+ * @param start is the start time for the recording
+ * @param iDuration is the duration of the recording
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateTimerTag(
+ const std::shared_ptr<CPVRChannel>& channel, const CDateTime& start, int iDuration);
+
+ /*!
+ * @brief create a recording or reminder timer or timer rule for the given epg info tag.
+ * @param tag the epg info tag
+ * @param bCreateRule if true, create a timer rule, create a one shot timer otherwise
+ * @param bCreateReminder if true, create a reminder timer or rule, create a recording timer
+ * or rule otherwise
+ * @param bReadOnly whether the timer/rule is read only
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateFromEpg(const std::shared_ptr<CPVREpgInfoTag>& tag,
+ bool bCreateRule,
+ bool bCreateReminder,
+ bool bReadOnly = false);
+
+ /*!
+ * @brief create a timer or timer rule for the given epg info tag.
+ * @param tag the epg info tag
+ * @param bCreateRule if true, create a timer rule, create a one shot timer otherwise
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateFromEpg(const std::shared_ptr<CPVREpgInfoTag>& tag,
+ bool bCreateRule = false);
+
+ /*!
+ * @brief create a reminder timer for the given start date.
+ * @param start the start date
+ * @param iDuration the duration the reminder is valid
+ * @param parent If non-zero, the new timer will be made a child of the given timer rule
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateReminderFromDate(
+ const CDateTime& start,
+ int iDuration,
+ const std::shared_ptr<CPVRTimerInfoTag>& parent = std::shared_ptr<CPVRTimerInfoTag>());
+
+ /*!
+ * @brief create a reminder timer for the given epg info tag.
+ * @param tag the epg info tag
+ * @param parent If non-zero, the new timer will be made a child of the given timer rule
+ * @return the timer or null if timer could not be created
+ */
+ static std::shared_ptr<CPVRTimerInfoTag> CreateReminderFromEpg(
+ const std::shared_ptr<CPVREpgInfoTag>& tag,
+ const std::shared_ptr<CPVRTimerInfoTag>& parent = std::shared_ptr<CPVRTimerInfoTag>());
+
+ /*!
+ * @brief Associate the given epg tag with this timer.
+ * @param tag The epg tag to assign.
+ */
+ void SetEpgInfoTag(const std::shared_ptr<CPVREpgInfoTag>& tag);
+
+ /*!
+ * @brief get the epg info tag associated with this timer, if any
+ * @param bCreate if true, try to find the epg tag if not yet set (lazy evaluation)
+ * @return the epg info tag associated with this timer or null if there is no tag
+ */
+ std::shared_ptr<CPVREpgInfoTag> GetEpgInfoTag(bool bCreate = true) const;
+
+ /*!
+ * @brief updates this timer excluding the state of any children.
+ * @param tag A timer containing the data that shall be merged into this timer's data.
+ * @return true if the timer was updated successfully
+ */
+ bool UpdateEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag);
+
+ /*!
+ * @brief merge in the state of this child timer.
+ * @param childTimer The child timer
+ * @param bAdd If true, add child's data to parent's state, otherwise subtract.
+ * @return true if the child timer's state was merged successfully
+ */
+ bool UpdateChildState(const std::shared_ptr<CPVRTimerInfoTag>& childTimer, bool bAdd);
+
+ /*!
+ * @brief reset the state of children related to this timer.
+ */
+ void ResetChildState();
+
+ /*!
+ * @brief Whether this timer is active.
+ * @return True if this timer is active, false otherwise.
+ */
+ bool IsActive() const
+ {
+ return m_state == PVR_TIMER_STATE_SCHEDULED || m_state == PVR_TIMER_STATE_RECORDING ||
+ m_state == PVR_TIMER_STATE_CONFLICT_OK || m_state == PVR_TIMER_STATE_CONFLICT_NOK ||
+ m_state == PVR_TIMER_STATE_ERROR;
+ }
+
+ /*!
+ * @brief Whether this timer is broken.
+ * @return True if this timer won't result in a recording because it is broken for some reason,
+ * false otherwise
+ */
+ bool IsBroken() const
+ {
+ return m_state == PVR_TIMER_STATE_CONFLICT_NOK || m_state == PVR_TIMER_STATE_ERROR;
+ }
+
+ /*!
+ * @brief Whether this timer has a conflict.
+ * @return True if this timer won't result in a recording because it is in conflict with another
+ * timer or live stream, false otherwise.
+ */
+ bool HasConflict() const { return m_state == PVR_TIMER_STATE_CONFLICT_NOK; }
+
+ /*!
+ * @brief Whether this timer is currently recording.
+ * @return True if recording, false otherwise.
+ */
+ bool IsRecording() const { return m_state == PVR_TIMER_STATE_RECORDING; }
+
+ /*!
+ * @brief Whether this timer is disabled, for example by the user.
+ * @return True if disabled, false otherwise.
+ */
+ bool IsDisabled() const { return m_state == PVR_TIMER_STATE_DISABLED; }
+
+ /*!
+ * @brief Gets the type of this timer.
+ * @return the timer type or NULL if this tag has no timer type.
+ */
+ const std::shared_ptr<CPVRTimerType> GetTimerType() const { return m_timerType; }
+
+ /*!
+ * @brief Sets the type of this timer.
+ * @param the new timer type.
+ */
+ void SetTimerType(const std::shared_ptr<CPVRTimerType>& type);
+
+ /*!
+ * @brief Checks whether this is a timer rule (vs. one time timer).
+ * @return True if this is a timer rule, false otherwise.
+ */
+ bool IsTimerRule() const { return m_timerType && m_timerType->IsTimerRule(); }
+
+ /*!
+ * @brief Checks whether this is a reminder timer (vs. recording timer).
+ * @return True if this is a reminder timer, false otherwise.
+ */
+ bool IsReminder() const { return m_timerType && m_timerType->IsReminder(); }
+
+ /*!
+ * @brief Checks whether this is a manual (vs. epg-based) timer.
+ * @return True if this is a manual timer, false otherwise.
+ */
+ bool IsManual() const { return m_timerType && m_timerType->IsManual(); }
+
+ /*!
+ * @brief Checks whether this is an epg-based (vs. manual) timer.
+ * @return True if this is an epg-Based timer, false otherwise.
+ */
+ bool IsEpgBased() const { return !IsManual(); }
+
+ /*!
+ * @brief The ID of the client for this timer.
+ * @return The client ID or -1 if this is a local timer.
+ */
+ int ClientID() const { return m_iClientId; }
+
+ /*!
+ * @brief Check, whether this timer is owned by a pvr client or by Kodi.
+ * @return True, if owned by a pvr client, false otherwise.
+ */
+ bool IsOwnedByClient() const;
+
+ /*!
+ * @brief Whether this timer is for Radio or TV.
+ * @return True if Radio, false otherwise.
+ */
+ bool IsRadio() const { return m_bIsRadio; }
+
+ /*!
+ * @brief The path that identifies this timer.
+ * @return The path.
+ */
+ const std::string& Path() const;
+
+ /*!
+ * @brief The index for this timer, as given by the client.
+ * @return The client index or PVR_TIMER_NO_CLIENT_INDEX if the timer was just created locally
+ * by Kodi and was not yet added by the client.
+ */
+ int ClientIndex() const { return m_iClientIndex; }
+
+ /*!
+ * @brief The index for the parent of this timer, as given by the client. Timers scheduled by a
+ * timer rule will have a parant index != PVR_TIMER_NO_PARENT.
+ * @return The client index or PVR_TIMER_NO_PARENT if the timer has no parent.
+ */
+ int ParentClientIndex() const { return m_iParentClientIndex; }
+
+ /*!
+ * @brief Whether this timer has a parent.
+ * @return True if timer has a parent, false otherwise.
+ */
+ bool HasParent() const { return m_iParentClientIndex != PVR_TIMER_NO_PARENT; }
+
+ /*!
+ * @brief The local ID for this timer, as given by Kodi.
+ * @return The ID or 0 if not yet set.
+ */
+ unsigned int TimerID() const { return m_iTimerId; }
+
+ /*!
+ * @brief Set the local ID for this timer.
+ * @param id The ID to set.
+ */
+ void SetTimerID(unsigned int id) { m_iTimerId = id; }
+
+ /*!
+ * @brief The UID of the channel for this timer.
+ * @return The channel UID or PVR_CHANNEL_INVALID_UID if not available.
+ */
+ int ClientChannelUID() const { return m_iClientChannelUid; }
+
+ /*!
+ * @brief The state for this timer.
+ * @return The state.
+ */
+ PVR_TIMER_STATE State() const { return m_state; }
+
+ /*!
+ * @brief Set the state for this timer.
+ * @param state The state to set.
+ */
+ void SetState(PVR_TIMER_STATE state) { m_state = state; }
+
+ /*!
+ * @brief The title for this timer.
+ * @return The title.
+ */
+ const std::string& Title() const;
+
+ /*!
+ * @brief Check whether this timer has an associated channel.
+ * @return True if this timer has a channel set, false otherwise.
+ */
+ bool HasChannel() const;
+
+ /*!
+ * @brief Get the channel associated with this timer, if any.
+ * @return the channel or null if non is associated with this timer.
+ */
+ std::shared_ptr<CPVRChannel> Channel() const;
+
+ /*!
+ * @brief Update the channel associated with this timer, based on current client ID and
+ * channel UID.
+ */
+ void UpdateChannel();
+
+ /*!
+ * @brief The name of the channel associated with this timer, if any.
+ * @return The channel name.
+ */
+ std::string ChannelName() const;
+
+ /*!
+ * @brief The path for the channel icon associated with this timer, if any.
+ * @return The channel icon path.
+ */
+ std::string ChannelIcon() const;
+
+ /*!
+ * @brief The start date and time for this timer, as UTC.
+ * @return The start date and time.
+ */
+ CDateTime StartAsUTC() const;
+
+ /*!
+ * @brief The start date and time for this timer, as local time.
+ * @return The start date and time.
+ */
+ CDateTime StartAsLocalTime() const;
+
+ /*!
+ * @brief Set the start date and time from a CDateTime instance carrying the data as UTC.
+ * @param start The start date and time as UTC.
+ */
+ void SetStartFromUTC(const CDateTime& start);
+
+ /*!
+ * @brief Set the start date and time from a CDateTime instance carrying the data as local time.
+ * @param start The start date and time as local time.
+ */
+ void SetStartFromLocalTime(const CDateTime& start);
+
+ /*!
+ * @brief The end date and time for this timer, as UTC.
+ * @return The start date and time.
+ */
+ CDateTime EndAsUTC() const;
+
+ /*!
+ * @brief The end date and time for this timer, as local time.
+ * @return The start date and time.
+ */
+ CDateTime EndAsLocalTime() const;
+
+ /*!
+ * @brief Set the end date and time from a CDateTime instance carrying the data as UTC.
+ * @param start The end date and time as UTC.
+ */
+ void SetEndFromUTC(const CDateTime& end);
+
+ /*!
+ * @brief Set the end date and time from a CDateTime instance carrying the data as local time.
+ * @param start The end date and time as local time.
+ */
+ void SetEndFromLocalTime(const CDateTime& end);
+
+ /*!
+ * @brief The first day for this timer, as UTC.
+ * @return The first day.
+ */
+ CDateTime FirstDayAsUTC() const;
+
+ /*!
+ * @brief The first day for this timer, as local time.
+ * @return The first day.
+ */
+ CDateTime FirstDayAsLocalTime() const;
+
+ /*!
+ * @brief Set the first dday from a CDateTime instance carrying the data as UTC.
+ * @param start The first day as UTC.
+ */
+ void SetFirstDayFromUTC(const CDateTime& firstDay);
+
+ /*!
+ * @brief Set the first dday from a CDateTime instance carrying the data as local time.
+ * @param start The first day as local time.
+ */
+ void SetFirstDayFromLocalTime(const CDateTime& firstDay);
+
+ /*!
+ * @brief Helper function to convert a given CDateTime containing data as UTC to local time.
+ * @param utc A CDateTime instance carrying data as UTC.
+ * @return A CDateTime instance carrying data as local time.
+ */
+ static CDateTime ConvertUTCToLocalTime(const CDateTime& utc);
+
+ /*!
+ * @brief Helper function to convert a given CDateTime containing data as local time to UTC.
+ * @param local A CDateTime instance carrying data as local time.
+ * @return A CDateTime instance carrying data as UTC.
+ */
+ static CDateTime ConvertLocalTimeToUTC(const CDateTime& local);
+
+ /*!
+ * @brief Get the duration of this timer in seconds, excluding padding times.
+ * @return The duration.
+ */
+ int GetDuration() const;
+
+ /*!
+ * @brief Get time in minutes to start the recording before the start time of the programme.
+ * @return The start padding time.
+ */
+ unsigned int MarginStart() const { return m_iMarginStart; }
+
+ /*!
+ * @brief Get time in minutes to end the recording after the end time of the programme.
+ * @return The end padding time.
+ */
+ unsigned int MarginEnd() const { return m_iMarginEnd; }
+
+ /*!
+ * @brief For timer rules, the days of week this timer rule is scheduled for.
+ * @return The days of week.
+ */
+ unsigned int WeekDays() const { return m_iWeekdays; }
+
+ /*!
+ * @brief For timer rules, whether start time is "any time", not a particular time.
+ * @return True, if timer start is "any time", false otherwise.
+ */
+ bool IsStartAnyTime() const { return m_bStartAnyTime; }
+
+ /*!
+ * @brief For timer rules, whether end time is "any time", not a particular time.
+ * @return True, if timer end is "any time", false otherwise.
+ */
+ bool IsEndAnyTime() const { return m_bEndAnyTime; }
+
+ /*!
+ * @brief For timer rules, whether only the EPG programme title shall be searched or also other
+ * data like the programme's plot, if available.
+ * @return True, if not only the programme's title shall be included in EPG search,
+ * false otherwise.
+ */
+ bool IsFullTextEpgSearch() const { return m_bFullTextEpgSearch; }
+
+ /*!
+ * @brief For timer rules, the epg data match string for searches. Format is backend-dependent,
+ * for example regexp.
+ * @return The search string
+ */
+ const std::string& EpgSearchString() const { return m_strEpgSearchString; }
+
+ /*!
+ * @brief The series link for this timer.
+ * @return The series link or empty string, if not available.
+ */
+ const std::string& SeriesLink() const;
+
+ /*!
+ * @brief Get the UID of the epg event associated with this timer tag, if any.
+ * @return The UID or EPG_TAG_INVALID_UID.
+ */
+ unsigned int UniqueBroadcastID() const { return m_iEpgUid; }
+
+ /*!
+ * @brief Add this timer to the backend, transferring all local data of this timer to the backend.
+ * @return True on success, false otherwise.
+ */
+ bool AddToClient() const;
+
+ /*!
+ * @brief Delete this timer on the backend.
+ * @param bForce Control what to do in case the timer is currently recording.
+ * True to force to delete the timer, false to return TimerDeleteResult::RECORDING.
+ * @return The result.
+ */
+ TimerOperationResult DeleteFromClient(bool bForce = false) const;
+
+ /*!
+ * @brief Update this timer on the backend, transferring all local data of this timer to
+ * the backend.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateOnClient();
+
+ /*!
+ * @brief Persist this timer in the local database.
+ * @return True on success, false otherwise.
+ */
+ bool Persist();
+
+ /*!
+ * @brief Delete this timer from the local database.
+ * @return True on success, false otherwise.
+ */
+ bool DeleteFromDatabase();
+
+ /*!
+ * @brief GUI support: Get the text for the timer GUI notification.
+ * @return The notification text.
+ */
+ std::string GetNotificationText() const;
+
+ /*!
+ * @brief GUI support: Get the text for the timer GUI notification when a timer has been deleted.
+ * @return The notification text.
+ */
+ std::string GetDeletedNotificationText() const;
+
+ /*!
+ * @brief GUI support: Get the summary text for this timer, reflecting the timer schedule in a
+ * human readable form.
+ * @return The summary string.
+ */
+ const std::string& Summary() const;
+
+ /*!
+ * @brief GUI support: Update the summary text for this timer.
+ */
+ void UpdateSummary();
+
+ /*!
+ * @brief GUI support: Get the status text for this timer, reflecting its current state in a
+ * human readable form.
+ * @return The status string.
+ */
+ std::string GetStatus(bool bRadio) const;
+
+ /*!
+ * @brief GUI support: Get the timer string in a human readable form.
+ * @return The type string.
+ */
+ std::string GetTypeAsString() const;
+
+ /*!
+ * @brief GUI support: Return string representation for any possible combination of weekdays.
+ * @param iWeekdays weekdays as bit mask (0x01 = Mo, 0x02 = Tu, ...)
+ * @param bEpgBased context is an epg-based timer
+ * @param bLongMultiDaysFormat use long format. ("Mo-__-We-__-Fr-Sa-__" vs. "Mo-We-Fr-Sa")
+ * @return The weekdays string representation.
+ */
+ static std::string GetWeekdaysString(unsigned int iWeekdays,
+ bool bEpgBased,
+ bool bLongMultiDaysFormat);
+
+private:
+ CPVRTimerInfoTag(const CPVRTimerInfoTag& tag) = delete;
+ CPVRTimerInfoTag& operator=(const CPVRTimerInfoTag& orig) = delete;
+
+ std::string GetWeekdaysString() const;
+ void UpdateEpgInfoTag();
+
+ static std::shared_ptr<CPVRTimerInfoTag> CreateFromDate(
+ const std::shared_ptr<CPVRChannel>& channel,
+ const CDateTime& start,
+ int iDuration,
+ bool bCreateReminder,
+ bool bReadOnly);
+
+ mutable CCriticalSection m_critSection;
+
+ std::string m_strTitle; /*!< @brief name of this timer */
+ std::string
+ m_strEpgSearchString; /*!< @brief a epg data match string for epg-based timer rules. Format is backend-dependent, for example regexp */
+ bool m_bFullTextEpgSearch =
+ false; /*!< @brief indicates whether only epg episode title can be matched by the pvr backend or "more" (backend-dependent") data. */
+ std::string m_strDirectory; /*!< @brief directory where the recording must be stored */
+ std::string m_strSummary; /*!< @brief summary string with the time to show inside a GUI list */
+ PVR_TIMER_STATE m_state = PVR_TIMER_STATE_SCHEDULED; /*!< @brief the state of this timer */
+ int m_iClientId; /*!< @brief ID of the backend */
+ int m_iClientIndex; /*!< @brief index number of the tag, given by the backend, PVR_TIMER_NO_CLIENT_INDEX for new */
+ int m_iParentClientIndex; /*!< @brief for timers scheduled by a timer rule, the index number of the parent, given by the backend, PVR_TIMER_NO_PARENT for no parent */
+ int m_iClientChannelUid; /*!< @brief channel uid */
+ bool m_bStartAnyTime =
+ false; /*!< @brief Ignore start date and time clock. Record at 'Any Time' */
+ bool m_bEndAnyTime = false; /*!< @brief Ignore end date and time clock. Record at 'Any Time' */
+ int m_iPriority; /*!< @brief priority of the timer */
+ int m_iLifetime; /*!< @brief lifetime of the timer in days */
+ int m_iMaxRecordings =
+ 0; /*!< @brief (optional) backend setting for maximum number of recordings to keep*/
+ unsigned int m_iWeekdays; /*!< @brief bit based store of weekdays for timer rules */
+ unsigned int
+ m_iPreventDupEpisodes; /*!< @brief only record new episodes for epg-based timer rules */
+ unsigned int m_iRecordingGroup =
+ 0; /*!< @brief (optional) if set, the addon/backend stores the recording to a group (sub-folder) */
+ std::string m_strFileNameAndPath; /*!< @brief file name is only for reference */
+ bool m_bIsRadio; /*!< @brief is radio channel if set */
+ unsigned int m_iTimerId = 0; /*!< @brief id that won't change as long as Kodi is running */
+ unsigned int
+ m_iMarginStart; /*!< @brief (optional) if set, the backend starts the recording iMarginStart minutes before startTime. */
+ unsigned int
+ m_iMarginEnd; /*!< @brief (optional) if set, the backend ends the recording iMarginEnd minutes after endTime. */
+ mutable unsigned int
+ m_iEpgUid; /*!< id of epg event associated with this timer, EPG_TAG_INVALID_UID if none. */
+ std::string m_strSeriesLink; /*!< series link */
+
+ CDateTime m_StartTime; /*!< start time */
+ CDateTime m_StopTime; /*!< stop time */
+ CDateTime m_FirstDay; /*!< if it is a manual timer rule the first date it starts */
+ std::shared_ptr<CPVRTimerType> m_timerType; /*!< the type of this timer */
+
+ unsigned int m_iTVChildTimersActive = 0;
+ unsigned int m_iTVChildTimersConflictNOK = 0;
+ unsigned int m_iTVChildTimersRecording = 0;
+ unsigned int m_iTVChildTimersErrors = 0;
+ unsigned int m_iRadioChildTimersActive = 0;
+ unsigned int m_iRadioChildTimersConflictNOK = 0;
+ unsigned int m_iRadioChildTimersRecording = 0;
+ unsigned int m_iRadioChildTimersErrors = 0;
+
+ mutable std::shared_ptr<CPVREpgInfoTag> m_epgTag; /*!< epg info tag matching m_iEpgUid. */
+ mutable std::shared_ptr<CPVRChannel> m_channel;
+
+ mutable bool m_bProbedEpgTag = false;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/timers/PVRTimerRuleMatcher.cpp b/xbmc/pvr/timers/PVRTimerRuleMatcher.cpp
new file mode 100644
index 0000000..9178e85
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerRuleMatcher.cpp
@@ -0,0 +1,193 @@
+/*
+ * 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 "PVRTimerRuleMatcher.h"
+
+#include "XBDateTime.h"
+#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" // PVR_CHANNEL_INVALID_UID
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "utils/RegExp.h"
+
+using namespace PVR;
+
+CPVRTimerRuleMatcher::CPVRTimerRuleMatcher(const std::shared_ptr<CPVRTimerInfoTag>& timerRule,
+ const CDateTime& start)
+ : m_timerRule(timerRule), m_start(CPVRTimerInfoTag::ConvertUTCToLocalTime(start))
+{
+}
+
+CPVRTimerRuleMatcher::~CPVRTimerRuleMatcher() = default;
+
+std::shared_ptr<CPVRChannel> CPVRTimerRuleMatcher::GetChannel() const
+{
+ if (m_timerRule->GetTimerType()->SupportsChannels())
+ return m_timerRule->Channel();
+
+ return {};
+}
+
+CDateTime CPVRTimerRuleMatcher::GetNextTimerStart() const
+{
+ if (!m_timerRule->GetTimerType()->SupportsStartTime())
+ return CDateTime(); // invalid datetime
+
+ const CDateTime startDateLocal = m_timerRule->GetTimerType()->SupportsFirstDay()
+ ? m_timerRule->FirstDayAsLocalTime()
+ : m_start;
+ const CDateTime startTimeLocal = m_timerRule->StartAsLocalTime();
+ CDateTime nextStart(startDateLocal.GetYear(), startDateLocal.GetMonth(), startDateLocal.GetDay(),
+ startTimeLocal.GetHour(), startTimeLocal.GetMinute(), 0);
+
+ const CDateTimeSpan oneDay(1, 0, 0, 0);
+ while (nextStart < m_start)
+ {
+ nextStart += oneDay;
+ }
+
+ if (m_timerRule->GetTimerType()->SupportsWeekdays() &&
+ m_timerRule->WeekDays() != PVR_WEEKDAY_ALLDAYS)
+ {
+ bool bMatch = false;
+ while (!bMatch)
+ {
+ int startWeekday = nextStart.GetDayOfWeek();
+ if (startWeekday == 0)
+ startWeekday = 7;
+
+ bMatch = ((1 << (startWeekday - 1)) & m_timerRule->WeekDays());
+ if (!bMatch)
+ nextStart += oneDay;
+ }
+ }
+
+ return nextStart.GetAsUTCDateTime();
+}
+
+bool CPVRTimerRuleMatcher::Matches(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ return epgTag && CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->EndAsUTC()) > m_start &&
+ MatchSeriesLink(epgTag) && MatchChannel(epgTag) && MatchStart(epgTag) &&
+ MatchEnd(epgTag) && MatchDayOfWeek(epgTag) && MatchSearchText(epgTag);
+}
+
+bool CPVRTimerRuleMatcher::MatchSeriesLink(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->RequiresEpgSeriesLinkOnCreate())
+ return epgTag->SeriesLink() == m_timerRule->SeriesLink();
+ else
+ return true;
+}
+
+bool CPVRTimerRuleMatcher::MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->SupportsAnyChannel() &&
+ m_timerRule->ClientChannelUID() == PVR_CHANNEL_INVALID_UID)
+ return true; // matches any channel
+
+ if (m_timerRule->GetTimerType()->SupportsChannels())
+ return m_timerRule->ClientChannelUID() != PVR_CHANNEL_INVALID_UID &&
+ epgTag->ClientID() == m_timerRule->ClientID() &&
+ epgTag->UniqueChannelID() == m_timerRule->ClientChannelUID();
+ else
+ return true;
+}
+
+bool CPVRTimerRuleMatcher::MatchStart(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->SupportsFirstDay())
+ {
+ // only year, month and day do matter here...
+ const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC());
+ const CDateTime startEpg(startEpgLocal.GetYear(), startEpgLocal.GetMonth(),
+ startEpgLocal.GetDay(), 0, 0, 0);
+ const CDateTime firstDayLocal = m_timerRule->FirstDayAsLocalTime();
+ const CDateTime startTimer(firstDayLocal.GetYear(), firstDayLocal.GetMonth(),
+ firstDayLocal.GetDay(), 0, 0, 0);
+ if (startEpg < startTimer)
+ return false;
+ }
+
+ if (m_timerRule->GetTimerType()->SupportsStartAnyTime() && m_timerRule->IsStartAnyTime())
+ return true; // matches any start time
+
+ if (m_timerRule->GetTimerType()->SupportsStartTime())
+ {
+ // only hours and minutes do matter here...
+ const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC());
+ const CDateTime startEpg(2000, 1, 1, startEpgLocal.GetHour(), startEpgLocal.GetMinute(), 0);
+ const CDateTime startTimerLocal = m_timerRule->StartAsLocalTime();
+ const CDateTime startTimer(2000, 1, 1, startTimerLocal.GetHour(), startTimerLocal.GetMinute(),
+ 0);
+ return startEpg >= startTimer;
+ }
+ else
+ return true;
+}
+
+bool CPVRTimerRuleMatcher::MatchEnd(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->SupportsEndAnyTime() && m_timerRule->IsEndAnyTime())
+ return true; // matches any end time
+
+ if (m_timerRule->GetTimerType()->SupportsEndTime())
+ {
+ // only hours and minutes do matter here...
+ const CDateTime endEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->EndAsUTC());
+ const CDateTime endEpg(2000, 1, 1, endEpgLocal.GetHour(), endEpgLocal.GetMinute(), 0);
+ const CDateTime endTimerLocal = m_timerRule->EndAsLocalTime();
+ const CDateTime endTimer(2000, 1, 1, endTimerLocal.GetHour(), endTimerLocal.GetMinute(), 0);
+ return endEpg <= endTimer;
+ }
+ else
+ return true;
+}
+
+bool CPVRTimerRuleMatcher::MatchDayOfWeek(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->SupportsWeekdays())
+ {
+ if (m_timerRule->WeekDays() != PVR_WEEKDAY_ALLDAYS)
+ {
+ const CDateTime startEpgLocal = CPVRTimerInfoTag::ConvertUTCToLocalTime(epgTag->StartAsUTC());
+ int startWeekday = startEpgLocal.GetDayOfWeek();
+ if (startWeekday == 0)
+ startWeekday = 7;
+
+ return ((1 << (startWeekday - 1)) & m_timerRule->WeekDays());
+ }
+ }
+ return true;
+}
+
+bool CPVRTimerRuleMatcher::MatchSearchText(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (m_timerRule->GetTimerType()->SupportsEpgFulltextMatch() && m_timerRule->IsFullTextEpgSearch())
+ {
+ if (!m_textSearch)
+ {
+ m_textSearch.reset(new CRegExp(true /* case insensitive */));
+ m_textSearch->RegComp(m_timerRule->EpgSearchString());
+ }
+ return m_textSearch->RegFind(epgTag->Title()) >= 0 ||
+ m_textSearch->RegFind(epgTag->EpisodeName()) >= 0 ||
+ m_textSearch->RegFind(epgTag->PlotOutline()) >= 0 ||
+ m_textSearch->RegFind(epgTag->Plot()) >= 0;
+ }
+ else if (m_timerRule->GetTimerType()->SupportsEpgTitleMatch())
+ {
+ if (!m_textSearch)
+ {
+ m_textSearch.reset(new CRegExp(true /* case insensitive */));
+ m_textSearch->RegComp(m_timerRule->EpgSearchString());
+ }
+ return m_textSearch->RegFind(epgTag->Title()) >= 0;
+ }
+ else
+ return true;
+}
diff --git a/xbmc/pvr/timers/PVRTimerRuleMatcher.h b/xbmc/pvr/timers/PVRTimerRuleMatcher.h
new file mode 100644
index 0000000..9d502af
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerRuleMatcher.h
@@ -0,0 +1,47 @@
+/*
+ * 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 "XBDateTime.h"
+
+#include <memory>
+
+class CRegExp;
+
+namespace PVR
+{
+class CPVRChannel;
+class CPVRTimerInfoTag;
+class CPVREpgInfoTag;
+
+class CPVRTimerRuleMatcher
+{
+public:
+ CPVRTimerRuleMatcher(const std::shared_ptr<CPVRTimerInfoTag>& timerRule, const CDateTime& start);
+ virtual ~CPVRTimerRuleMatcher();
+
+ std::shared_ptr<CPVRTimerInfoTag> GetTimerRule() const { return m_timerRule; }
+
+ std::shared_ptr<CPVRChannel> GetChannel() const;
+ CDateTime GetNextTimerStart() const;
+ bool Matches(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+private:
+ bool MatchSeriesLink(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ bool MatchChannel(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ bool MatchStart(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ bool MatchEnd(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ bool MatchDayOfWeek(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+ bool MatchSearchText(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ const std::shared_ptr<CPVRTimerInfoTag> m_timerRule;
+ CDateTime m_start;
+ mutable std::unique_ptr<CRegExp> m_textSearch;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/timers/PVRTimerType.cpp b/xbmc/pvr/timers/PVRTimerType.cpp
new file mode 100644
index 0000000..db20fa1
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerType.cpp
@@ -0,0 +1,418 @@
+/*
+ * 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 "PVRTimerType.h"
+
+#include "ServiceBroker.h"
+#include "guilib/LocalizeStrings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+
+const std::vector<std::shared_ptr<CPVRTimerType>> CPVRTimerType::GetAllTypes()
+{
+ std::vector<std::shared_ptr<CPVRTimerType>> allTypes;
+ CServiceBroker::GetPVRManager().Clients()->GetTimerTypes(allTypes);
+
+ // Add local reminder timer types. Local reminders are always available.
+ int iTypeId = PVR_TIMER_TYPE_NONE;
+
+ // one time time-based reminder
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_MANUAL |
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME));
+
+ // one time epg-based reminder
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_START_MARGIN));
+
+ // time-based reminder rule
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_REPEATING |
+ PVR_TIMER_TYPE_IS_MANUAL |
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY |
+ PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS));
+
+ // one time read-only time-based reminder (created by timer rule)
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_MANUAL |
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_IS_READONLY |
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME,
+ g_localizeStrings.Get(819))); // One time (Scheduled by timer rule)
+
+ // epg-based reminder rule
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_REPEATING |
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE |
+ PVR_TIMER_TYPE_SUPPORTS_TITLE_EPG_MATCH |
+ PVR_TIMER_TYPE_SUPPORTS_FULLTEXT_EPG_MATCH |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_ANY_CHANNEL |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_START_ANYTIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_END_ANYTIME |
+ PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY |
+ PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS |
+ PVR_TIMER_TYPE_SUPPORTS_START_END_MARGIN));
+
+ // one time read-only epg-based reminder (created by timer rule)
+ allTypes.emplace_back(std::make_shared<CPVRTimerType>(++iTypeId,
+ PVR_TIMER_TYPE_IS_REMINDER |
+ PVR_TIMER_TYPE_IS_READONLY |
+ PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE |
+ PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE |
+ PVR_TIMER_TYPE_SUPPORTS_CHANNELS |
+ PVR_TIMER_TYPE_SUPPORTS_START_TIME |
+ PVR_TIMER_TYPE_SUPPORTS_START_MARGIN,
+ g_localizeStrings.Get(819))); // One time (Scheduled by timer rule)
+
+ return allTypes;
+}
+
+const std::shared_ptr<CPVRTimerType> CPVRTimerType::GetFirstAvailableType(const std::shared_ptr<CPVRClient>& client)
+{
+ if (client)
+ {
+ std::vector<std::shared_ptr<CPVRTimerType>> types;
+ if (client->GetTimerTypes(types) == PVR_ERROR_NO_ERROR && !types.empty())
+ {
+ return *(types.begin());
+ }
+ }
+ return {};
+}
+
+std::shared_ptr<CPVRTimerType> CPVRTimerType::CreateFromIds(unsigned int iTypeId, int iClientId)
+{
+ const std::vector<std::shared_ptr<CPVRTimerType>> types = GetAllTypes();
+ const auto it =
+ std::find_if(types.cbegin(), types.cend(), [iClientId, iTypeId](const auto& type) {
+ return type->GetClientId() == iClientId && type->GetTypeId() == iTypeId;
+ });
+ if (it != types.cend())
+ return (*it);
+
+ if (iClientId != -1)
+ {
+ // fallback. try to obtain local timer type.
+ std::shared_ptr<CPVRTimerType> type = CreateFromIds(iTypeId, -1);
+ if (type)
+ return type;
+ }
+
+ CLog::LogF(LOGERROR, "Unable to resolve numeric timer type ({}, {})", iTypeId, iClientId);
+ return {};
+}
+
+std::shared_ptr<CPVRTimerType> CPVRTimerType::CreateFromAttributes(uint64_t iMustHaveAttr,
+ uint64_t iMustNotHaveAttr,
+ int iClientId)
+{
+ const std::vector<std::shared_ptr<CPVRTimerType>> types = GetAllTypes();
+ const auto it = std::find_if(types.cbegin(), types.cend(),
+ [iClientId, iMustHaveAttr, iMustNotHaveAttr](const auto& type) {
+ return type->GetClientId() == iClientId &&
+ (type->GetAttributes() & iMustHaveAttr) == iMustHaveAttr &&
+ (type->GetAttributes() & iMustNotHaveAttr) == 0;
+ });
+ if (it != types.cend())
+ return (*it);
+
+ if (iClientId != -1)
+ {
+ // fallback. try to obtain local timer type.
+ std::shared_ptr<CPVRTimerType> type = CreateFromAttributes(iMustHaveAttr, iMustNotHaveAttr, -1);
+ if (type)
+ return type;
+ }
+
+ CLog::LogF(LOGERROR, "Unable to resolve timer type (0x{:x}, 0x{:x}, {})", iMustHaveAttr,
+ iMustNotHaveAttr, iClientId);
+ return {};
+}
+
+CPVRTimerType::CPVRTimerType() :
+ m_iTypeId(PVR_TIMER_TYPE_NONE),
+ m_iAttributes(PVR_TIMER_TYPE_ATTRIBUTE_NONE)
+{
+}
+
+CPVRTimerType::CPVRTimerType(const PVR_TIMER_TYPE& type, int iClientId) :
+ m_iClientId(iClientId),
+ m_iTypeId(type.iId),
+ m_iAttributes(type.iAttributes),
+ m_strDescription(type.strDescription)
+{
+ InitDescription();
+ InitAttributeValues(type);
+}
+
+CPVRTimerType::CPVRTimerType(unsigned int iTypeId,
+ uint64_t iAttributes,
+ const std::string& strDescription)
+ : m_iTypeId(iTypeId), m_iAttributes(iAttributes), m_strDescription(strDescription)
+{
+ InitDescription();
+}
+
+CPVRTimerType::~CPVRTimerType() = default;
+
+bool CPVRTimerType::operator ==(const CPVRTimerType& right) const
+{
+ return (m_iClientId == right.m_iClientId &&
+ m_iTypeId == right.m_iTypeId &&
+ m_iAttributes == right.m_iAttributes &&
+ m_strDescription == right.m_strDescription &&
+ m_priorityValues == right.m_priorityValues &&
+ m_iPriorityDefault == right.m_iPriorityDefault &&
+ m_lifetimeValues == right.m_lifetimeValues &&
+ m_iLifetimeDefault == right.m_iLifetimeDefault &&
+ m_maxRecordingsValues == right.m_maxRecordingsValues &&
+ m_iMaxRecordingsDefault == right.m_iMaxRecordingsDefault &&
+ m_preventDupEpisodesValues == right.m_preventDupEpisodesValues &&
+ m_iPreventDupEpisodesDefault == right.m_iPreventDupEpisodesDefault &&
+ m_recordingGroupValues == right.m_recordingGroupValues &&
+ m_iRecordingGroupDefault == right.m_iRecordingGroupDefault);
+}
+
+bool CPVRTimerType::operator !=(const CPVRTimerType& right) const
+{
+ return !(*this == right);
+}
+
+void CPVRTimerType::InitDescription()
+{
+ // if no description was given, compile it
+ if (m_strDescription.empty())
+ {
+ int id;
+ if (m_iAttributes & PVR_TIMER_TYPE_IS_REPEATING)
+ {
+ id = (m_iAttributes & PVR_TIMER_TYPE_IS_MANUAL)
+ ? 822 // "Timer rule"
+ : 823; // "Timer rule (guide-based)"
+ }
+ else
+ {
+ id = (m_iAttributes & PVR_TIMER_TYPE_IS_MANUAL)
+ ? 820 // "One time"
+ : 821; // "One time (guide-based)
+ }
+ m_strDescription = g_localizeStrings.Get(id);
+ }
+
+ // add reminder/recording prefix
+ int prefixId = (m_iAttributes & PVR_TIMER_TYPE_IS_REMINDER)
+ ? 824 // Reminder: ...
+ : 825; // Recording: ...
+
+ m_strDescription = StringUtils::Format(g_localizeStrings.Get(prefixId), m_strDescription);
+}
+
+void CPVRTimerType::InitAttributeValues(const PVR_TIMER_TYPE& type)
+{
+ InitPriorityValues(type);
+ InitLifetimeValues(type);
+ InitMaxRecordingsValues(type);
+ InitPreventDuplicateEpisodesValues(type);
+ InitRecordingGroupValues(type);
+}
+
+void CPVRTimerType::InitPriorityValues(const PVR_TIMER_TYPE& type)
+{
+ if (type.iPrioritiesSize > 0)
+ {
+ for (unsigned int i = 0; i < type.iPrioritiesSize; ++i)
+ {
+ std::string strDescr(type.priorities[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = std::to_string(type.priorities[i].iValue);
+ }
+ m_priorityValues.emplace_back(strDescr, type.priorities[i].iValue);
+ }
+
+ m_iPriorityDefault = type.iPrioritiesDefault;
+ }
+ else if (SupportsPriority())
+ {
+ // No values given by addon, but priority supported. Use default values 1..100
+ for (int i = 1; i < 101; ++i)
+ m_priorityValues.emplace_back(std::to_string(i), i);
+
+ m_iPriorityDefault = DEFAULT_RECORDING_PRIORITY;
+ }
+ else
+ {
+ // No priority supported.
+ m_iPriorityDefault = DEFAULT_RECORDING_PRIORITY;
+ }
+}
+
+void CPVRTimerType::GetPriorityValues(std::vector<std::pair<std::string, int>>& list) const
+{
+ std::copy(m_priorityValues.cbegin(), m_priorityValues.cend(), std::back_inserter(list));
+}
+
+void CPVRTimerType::InitLifetimeValues(const PVR_TIMER_TYPE& type)
+{
+ if (type.iLifetimesSize > 0)
+ {
+ for (unsigned int i = 0; i < type.iLifetimesSize; ++i)
+ {
+ int iValue = type.lifetimes[i].iValue;
+ std::string strDescr(type.lifetimes[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = std::to_string(iValue);
+ }
+ m_lifetimeValues.emplace_back(strDescr, iValue);
+ }
+
+ m_iLifetimeDefault = type.iLifetimesDefault;
+ }
+ else if (SupportsLifetime())
+ {
+ // No values given by addon, but lifetime supported. Use default values 1..365
+ for (int i = 1; i < 366; ++i)
+ {
+ m_lifetimeValues.emplace_back(StringUtils::Format(g_localizeStrings.Get(17999), i),
+ i); // "{} days"
+ }
+ m_iLifetimeDefault = DEFAULT_RECORDING_LIFETIME;
+ }
+ else
+ {
+ // No lifetime supported.
+ m_iLifetimeDefault = DEFAULT_RECORDING_LIFETIME;
+ }
+}
+
+void CPVRTimerType::GetLifetimeValues(std::vector<std::pair<std::string, int>>& list) const
+{
+ std::copy(m_lifetimeValues.cbegin(), m_lifetimeValues.cend(), std::back_inserter(list));
+}
+
+void CPVRTimerType::InitMaxRecordingsValues(const PVR_TIMER_TYPE& type)
+{
+ if (type.iMaxRecordingsSize > 0)
+ {
+ for (unsigned int i = 0; i < type.iMaxRecordingsSize; ++i)
+ {
+ std::string strDescr(type.maxRecordings[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = std::to_string(type.maxRecordings[i].iValue);
+ }
+ m_maxRecordingsValues.emplace_back(strDescr, type.maxRecordings[i].iValue);
+ }
+
+ m_iMaxRecordingsDefault = type.iMaxRecordingsDefault;
+ }
+}
+
+void CPVRTimerType::GetMaxRecordingsValues(std::vector<std::pair<std::string, int>>& list) const
+{
+ std::copy(m_maxRecordingsValues.cbegin(), m_maxRecordingsValues.cend(), std::back_inserter(list));
+}
+
+void CPVRTimerType::InitPreventDuplicateEpisodesValues(const PVR_TIMER_TYPE& type)
+{
+ if (type.iPreventDuplicateEpisodesSize > 0)
+ {
+ for (unsigned int i = 0; i < type.iPreventDuplicateEpisodesSize; ++i)
+ {
+ std::string strDescr(type.preventDuplicateEpisodes[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = std::to_string(type.preventDuplicateEpisodes[i].iValue);
+ }
+ m_preventDupEpisodesValues.emplace_back(strDescr, type.preventDuplicateEpisodes[i].iValue);
+ }
+
+ m_iPreventDupEpisodesDefault = type.iPreventDuplicateEpisodesDefault;
+ }
+ else if (SupportsRecordOnlyNewEpisodes())
+ {
+ // No values given by addon, but prevent duplicate episodes supported. Use default values 0..1
+ m_preventDupEpisodesValues.emplace_back(g_localizeStrings.Get(815), 0); // "Record all episodes"
+ m_preventDupEpisodesValues.emplace_back(g_localizeStrings.Get(816), 1); // "Record only new episodes"
+ m_iPreventDupEpisodesDefault = DEFAULT_RECORDING_DUPLICATEHANDLING;
+ }
+ else
+ {
+ // No prevent duplicate episodes supported.
+ m_iPreventDupEpisodesDefault = DEFAULT_RECORDING_DUPLICATEHANDLING;
+ }
+}
+
+void CPVRTimerType::GetPreventDuplicateEpisodesValues(std::vector<std::pair<std::string, int>>& list) const
+{
+ std::copy(m_preventDupEpisodesValues.cbegin(), m_preventDupEpisodesValues.cend(),
+ std::back_inserter(list));
+}
+
+void CPVRTimerType::InitRecordingGroupValues(const PVR_TIMER_TYPE& type)
+{
+ if (type.iRecordingGroupSize > 0)
+ {
+ for (unsigned int i = 0; i < type.iRecordingGroupSize; ++i)
+ {
+ std::string strDescr(type.recordingGroup[i].strDescription);
+ if (strDescr.empty())
+ {
+ // No description given by addon. Create one from value.
+ strDescr = StringUtils::Format("{} {}",
+ g_localizeStrings.Get(811), // Recording group
+ type.recordingGroup[i].iValue);
+ }
+ m_recordingGroupValues.emplace_back(strDescr, type.recordingGroup[i].iValue);
+ }
+
+ m_iRecordingGroupDefault = type.iRecordingGroupDefault;
+ }
+}
+
+void CPVRTimerType::GetRecordingGroupValues(std::vector< std::pair<std::string, int>>& list) const
+{
+ std::copy(m_recordingGroupValues.cbegin(), m_recordingGroupValues.cend(),
+ std::back_inserter(list));
+}
diff --git a/xbmc/pvr/timers/PVRTimerType.h b/xbmc/pvr/timers/PVRTimerType.h
new file mode 100644
index 0000000..8ee9e22
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimerType.h
@@ -0,0 +1,411 @@
+/*
+ * 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 "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_timers.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+struct PVR_TIMER_TYPE;
+
+namespace PVR
+{
+ class CPVRClient;
+
+ static const int DEFAULT_RECORDING_PRIORITY = 50;
+ static const int DEFAULT_RECORDING_LIFETIME = 99; // days
+ static const unsigned int DEFAULT_RECORDING_DUPLICATEHANDLING = 0;
+
+ class CPVRTimerType
+ {
+ public:
+ /*!
+ * @brief Return a list with all known timer types.
+ * @return A list of timer types or an empty list if no types available.
+ */
+ static const std::vector<std::shared_ptr<CPVRTimerType>> GetAllTypes();
+
+ /*!
+ * @brief Return the first available timer type from given client.
+ * @param client the PVR client.
+ * @return A timer type or NULL if none available.
+ */
+ static const std::shared_ptr<CPVRTimerType> GetFirstAvailableType(const std::shared_ptr<CPVRClient>& client);
+
+ /*!
+ * @brief Create a timer type from given timer type id and client id.
+ * @param iTimerType the timer type id.
+ * @param iClientId the PVR client id.
+ * @return A timer type instance.
+ */
+ static std::shared_ptr<CPVRTimerType> CreateFromIds(unsigned int iTypeId, int iClientId);
+
+ /*!
+ * @brief Create a timer type from given timer type attributes and client id.
+ * @param iMustHaveAttr a combination of PVR_TIMER_TYPE_* attributes the type to create must have.
+ * @param iMustNotHaveAttr a combination of PVR_TIMER_TYPE_* attributes the type to create must not have.
+ * @param iClientId the PVR client id.
+ * @return A timer type instance.
+ */
+ static std::shared_ptr<CPVRTimerType> CreateFromAttributes(uint64_t iMustHaveAttr,
+ uint64_t iMustNotHaveAttr,
+ int iClientId);
+
+ CPVRTimerType();
+ CPVRTimerType(const PVR_TIMER_TYPE& type, int iClientId);
+ CPVRTimerType(unsigned int iTypeId,
+ uint64_t iAttributes,
+ const std::string& strDescription = "");
+
+ virtual ~CPVRTimerType();
+
+ CPVRTimerType(const CPVRTimerType& type) = delete;
+ CPVRTimerType& operator=(const CPVRTimerType& orig) = delete;
+
+ bool operator ==(const CPVRTimerType& right) const;
+ bool operator !=(const CPVRTimerType& right) const;
+
+ /*!
+ * @brief Get the PVR client id for this type.
+ * @return The PVR client id.
+ */
+ int GetClientId() const { return m_iClientId; }
+
+ /*!
+ * @brief Get the numeric type id of this type.
+ * @return The type id.
+ */
+ unsigned int GetTypeId() const { return m_iTypeId; }
+
+ /*!
+ * @brief Get the plain text (UI) description of this type.
+ * @return The description.
+ */
+ const std::string& GetDescription() const { return m_strDescription; }
+
+ /*!
+ * @brief Get the attributes of this type.
+ * @return The attributes.
+ */
+ uint64_t GetAttributes() const { return m_iAttributes; }
+
+ /*!
+ * @brief Check whether this type is for timer rules or one time timers.
+ * @return True if type represents a timer rule, false otherwise.
+ */
+ bool IsTimerRule() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_REPEATING) > 0; }
+
+ /*!
+ * @brief Check whether this type is for reminder timers or recording timers.
+ * @return True if type represents a reminder timer, false otherwise.
+ */
+ bool IsReminder() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_REMINDER) > 0; }
+
+ /*!
+ * @brief Check whether this type is for timer rules or one time timers.
+ * @return True if type represents a one time timer, false otherwise.
+ */
+ bool IsOnetime() const { return !IsTimerRule(); }
+
+ /*!
+ * @brief Check whether this type is for epg-based or manual timers.
+ * @return True if manual, false otherwise.
+ */
+ bool IsManual() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_MANUAL) > 0; }
+
+ /*!
+ * @brief Check whether this type is for epg-based or manual timers.
+ * @return True if epg-based, false otherwise.
+ */
+ bool IsEpgBased() const { return !IsManual(); }
+
+ /*!
+ * @brief Check whether this type is for epg-based timer rules.
+ * @return True if epg-based timer rule, false otherwise.
+ */
+ bool IsEpgBasedTimerRule() const { return IsEpgBased() && IsTimerRule(); }
+
+ /*!
+ * @brief Check whether this type is for one time epg-based timers.
+ * @return True if one time epg-based, false otherwise.
+ */
+ bool IsEpgBasedOnetime() const { return IsEpgBased() && IsOnetime(); }
+
+ /*!
+ * @brief Check whether this type is for manual timer rules.
+ * @return True if manual timer rule, false otherwise.
+ */
+ bool IsManualTimerRule() const { return IsManual() && IsTimerRule(); }
+
+ /*!
+ * @brief Check whether this type is for one time manual timers.
+ * @return True if one time manual, false otherwise.
+ */
+ bool IsManualOnetime() const { return IsManual() && IsOnetime(); }
+
+ /*!
+ * @brief Check whether this type is readonly (must not be modified after initial creation).
+ * @return True if readonly, false otherwise.
+ */
+ bool IsReadOnly() const { return (m_iAttributes & PVR_TIMER_TYPE_IS_READONLY) > 0; }
+
+ /*!
+ * @brief Check whether this type allows deletion.
+ * @return True if type allows deletion, false otherwise.
+ */
+ bool AllowsDelete() const { return !IsReadOnly() || SupportsReadOnlyDelete(); }
+
+ /*!
+ * @brief Check whether this type forbids creation of new timers of this type.
+ * @return True if new instances are forbidden, false otherwise.
+ */
+ bool ForbidsNewInstances() const { return (m_iAttributes & PVR_TIMER_TYPE_FORBIDS_NEW_INSTANCES) > 0; }
+
+ /*!
+ * @brief Check whether this timer type is forbidden when epg tag info is present.
+ * @return True if new instances are forbidden when epg info is present, false otherwise.
+ */
+ bool ForbidsEpgTagOnCreate() const { return (m_iAttributes & PVR_TIMER_TYPE_FORBIDS_EPG_TAG_ON_CREATE) > 0; }
+
+ /*!
+ * @brief Check whether this timer type requires epg tag info to be present.
+ * @return True if new instances require EPG info, false otherwise.
+ */
+ bool RequiresEpgTagOnCreate() const { return (m_iAttributes & (PVR_TIMER_TYPE_REQUIRES_EPG_TAG_ON_CREATE |
+ PVR_TIMER_TYPE_REQUIRES_EPG_SERIES_ON_CREATE |
+ PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE)) > 0; }
+
+ /*!
+ * @brief Check whether this timer type requires epg tag info including series attributes to be present.
+ * @return True if new instances require an EPG tag with series attributes, false otherwise.
+ */
+ bool RequiresEpgSeriesOnCreate() const { return (m_iAttributes & PVR_TIMER_TYPE_REQUIRES_EPG_SERIES_ON_CREATE) > 0; }
+
+ /*!
+ * @brief Check whether this timer type requires epg tag info including a series link to be present.
+ * @return True if new instances require an EPG tag with a series link, false otherwise.
+ */
+ bool RequiresEpgSeriesLinkOnCreate() const { return (m_iAttributes & PVR_TIMER_TYPE_REQUIRES_EPG_SERIESLINK_ON_CREATE) > 0; }
+
+ /*!
+ * @brief Check whether this type supports the "enabling/disabling" of timers of its type.
+ * @return True if "enabling/disabling" feature is supported, false otherwise.
+ */
+ bool SupportsEnableDisable() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_ENABLE_DISABLE) > 0; }
+
+ /*!
+ * @brief Check whether this type supports channels.
+ * @return True if channels are supported, false otherwise.
+ */
+ bool SupportsChannels() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_CHANNELS) > 0; }
+
+ /*!
+ * @brief Check whether this type supports start time.
+ * @return True if start time values are supported, false otherwise.
+ */
+ bool SupportsStartTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_TIME) > 0; }
+
+ /*!
+ * @brief Check whether this type supports end time.
+ * @return True if end time values are supported, false otherwise.
+ */
+ bool SupportsEndTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_END_TIME) > 0; }
+ /*!
+ * @brief Check whether this type supports start any time.
+ * @return True if start any time is supported, false otherwise.
+ */
+ bool SupportsStartAnyTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_ANYTIME) > 0; }
+
+ /*!
+ * @brief Check whether this type supports end any time.
+ * @return True if end any time is supported, false otherwise.
+ */
+ bool SupportsEndAnyTime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_END_ANYTIME) > 0; }
+
+ /*!
+ * @brief Check whether this type supports matching a search string against epg episode title.
+ * @return True if title matching is supported, false otherwise.
+ */
+ bool SupportsEpgTitleMatch() const { return (m_iAttributes & (PVR_TIMER_TYPE_SUPPORTS_TITLE_EPG_MATCH | PVR_TIMER_TYPE_SUPPORTS_FULLTEXT_EPG_MATCH)) > 0; }
+
+ /*!
+ * @brief Check whether this type supports matching a search string against extended (fulltext) epg data. This
+ includes title matching.
+ * @return True if fulltext matching is supported, false otherwise.
+ */
+ bool SupportsEpgFulltextMatch() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_FULLTEXT_EPG_MATCH) > 0; }
+
+ /*!
+ * @brief Check whether this type supports a first day the timer is active.
+ * @return True if first day is supported, false otherwise.
+ */
+ bool SupportsFirstDay() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_FIRST_DAY) > 0; }
+
+ /*!
+ * @brief Check whether this type supports weekdays for timer schedules.
+ * @return True if weekdays are supported, false otherwise.
+ */
+ bool SupportsWeekdays() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_WEEKDAYS) > 0; }
+
+ /*!
+ * @brief Check whether this type supports the "record only new episodes" feature.
+ * @return True if the "record only new episodes" feature is supported, false otherwise.
+ */
+ bool SupportsRecordOnlyNewEpisodes() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_RECORD_ONLY_NEW_EPISODES) > 0; }
+
+ /*!
+ * @brief Check whether this type supports pre record time.
+ * @return True if pre record time is supported, false otherwise.
+ */
+ bool SupportsStartMargin() const
+ {
+ return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_MARGIN) > 0 ||
+ (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_END_MARGIN) > 0;
+ }
+
+ /*!
+ * @brief Check whether this type supports post record time.
+ * @return True if post record time is supported, false otherwise.
+ */
+ bool SupportsEndMargin() const
+ {
+ return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_END_MARGIN) > 0 ||
+ (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_START_END_MARGIN) > 0;
+ }
+
+ /*!
+ * @brief Check whether this type supports recording priorities.
+ * @return True if recording priority is supported, false otherwise.
+ */
+ bool SupportsPriority() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_PRIORITY) > 0; }
+
+ /*!
+ * @brief Check whether this type supports lifetime for recordings.
+ * @return True if recording lifetime is supported, false otherwise.
+ */
+ bool SupportsLifetime() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_LIFETIME) > 0; }
+
+ /*!
+ * @brief Check whether this type supports MaxRecordings for recordings.
+ * @return True if MaxRecordings is supported, false otherwise.
+ */
+ bool SupportsMaxRecordings() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_MAX_RECORDINGS) > 0; }
+
+ /*!
+ * @brief Check whether this type supports user specified recording folders.
+ * @return True if recording folders are supported, false otherwise.
+ */
+ bool SupportsRecordingFolders() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_RECORDING_FOLDERS) > 0; }
+
+ /*!
+ * @brief Check whether this type supports recording groups.
+ * @return True if recording groups are supported, false otherwise.
+ */
+ bool SupportsRecordingGroup() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_RECORDING_GROUP) > 0; }
+
+ /*!
+ * @brief Check whether this type supports 'any channel', for example for defining a timer rule that should match any channel instead of a particular channel.
+ * @return True if any channel is supported, false otherwise.
+ */
+ bool SupportsAnyChannel() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_ANY_CHANNEL) > 0; }
+
+ /*!
+ * @brief Check whether this type supports deletion of an otherwise read-only timer.
+ * @return True if read-only deletion is supported, false otherwise.
+ */
+ bool SupportsReadOnlyDelete() const { return (m_iAttributes & PVR_TIMER_TYPE_SUPPORTS_READONLY_DELETE) > 0; }
+
+ /*!
+ * @brief Obtain a list with all possible values for the priority attribute.
+ * @param list out, the list with the values or an empty list, if priority is not supported by this type.
+ */
+ void GetPriorityValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Obtain the default value for the priority attribute.
+ * @return the default value.
+ */
+ int GetPriorityDefault() const { return m_iPriorityDefault; }
+
+ /*!
+ * @brief Obtain a list with all possible values for the lifetime attribute.
+ * @param list out, the list with the values or an empty list, if lifetime is not supported by this type.
+ */
+ void GetLifetimeValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Obtain the default value for the lifetime attribute.
+ * @return the default value.
+ */
+ int GetLifetimeDefault() const { return m_iLifetimeDefault; }
+
+ /*!
+ * @brief Obtain a list with all possible values for the MaxRecordings attribute.
+ * @param list out, the list with the values or an empty list, if MaxRecordings is not supported by this type.
+ */
+ void GetMaxRecordingsValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Obtain the default value for the MaxRecordings attribute.
+ * @return the default value.
+ */
+ int GetMaxRecordingsDefault() const { return m_iMaxRecordingsDefault; }
+
+ /*!
+ * @brief Obtain a list with all possible values for the duplicate episode prevention attribute.
+ * @param list out, the list with the values or an empty list, if duplicate episode prevention is not supported by this type.
+ */
+ void GetPreventDuplicateEpisodesValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Obtain the default value for the duplicate episode prevention attribute.
+ * @return the default value.
+ */
+ int GetPreventDuplicateEpisodesDefault() const { return m_iPreventDupEpisodesDefault; }
+
+ /*!
+ * @brief Obtain a list with all possible values for the recording group attribute.
+ * @param list out, the list with the values or an empty list, if recording group is not supported by this type.
+ */
+ void GetRecordingGroupValues(std::vector<std::pair<std::string, int>>& list) const;
+
+ /*!
+ * @brief Obtain the default value for the Recording Group attribute.
+ * @return the default value.
+ */
+ int GetRecordingGroupDefault() const { return m_iRecordingGroupDefault; }
+
+ private:
+ void InitDescription();
+ void InitAttributeValues(const PVR_TIMER_TYPE& type);
+ void InitPriorityValues(const PVR_TIMER_TYPE& type);
+ void InitLifetimeValues(const PVR_TIMER_TYPE& type);
+ void InitMaxRecordingsValues(const PVR_TIMER_TYPE& type);
+ void InitPreventDuplicateEpisodesValues(const PVR_TIMER_TYPE& type);
+ void InitRecordingGroupValues(const PVR_TIMER_TYPE& type);
+
+ int m_iClientId = -1;
+ unsigned int m_iTypeId;
+ uint64_t m_iAttributes;
+ std::string m_strDescription;
+ std::vector< std::pair<std::string, int> > m_priorityValues;
+ int m_iPriorityDefault = DEFAULT_RECORDING_PRIORITY;
+ std::vector< std::pair<std::string, int> > m_lifetimeValues;
+ int m_iLifetimeDefault = DEFAULT_RECORDING_LIFETIME;
+ std::vector< std::pair<std::string, int> > m_maxRecordingsValues;
+ int m_iMaxRecordingsDefault = 0;
+ std::vector< std::pair<std::string, int> > m_preventDupEpisodesValues;
+ unsigned int m_iPreventDupEpisodesDefault = DEFAULT_RECORDING_DUPLICATEHANDLING;
+ std::vector< std::pair<std::string, int> > m_recordingGroupValues;
+ unsigned int m_iRecordingGroupDefault = 0;
+ };
+}
diff --git a/xbmc/pvr/timers/PVRTimers.cpp b/xbmc/pvr/timers/PVRTimers.cpp
new file mode 100644
index 0000000..760bb19
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimers.cpp
@@ -0,0 +1,1338 @@
+/*
+ * 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 "PVRTimers.h"
+
+#include "ServiceBroker.h"
+#include "pvr/PVRDatabase.h"
+#include "pvr/PVREventLogJob.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClient.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimerRuleMatcher.h"
+#include "settings/Settings.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <iterator>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace PVR;
+using namespace std::chrono_literals;
+
+namespace
+{
+constexpr auto MAX_NOTIFICATION_DELAY = 10s;
+}
+
+bool CPVRTimersContainer::UpdateFromClient(const std::shared_ptr<CPVRTimerInfoTag>& timer)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::shared_ptr<CPVRTimerInfoTag> tag = GetByClient(timer->ClientID(), timer->ClientIndex());
+ if (tag)
+ {
+ return tag->UpdateEntry(timer);
+ }
+ else
+ {
+ timer->SetTimerID(++m_iLastId);
+ InsertEntry(timer);
+ }
+
+ return true;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimersContainer::GetByClient(int iClientId,
+ int iClientIndex) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& startDates : m_tags)
+ {
+ const auto it = std::find_if(startDates.second.cbegin(), startDates.second.cend(),
+ [iClientId, iClientIndex](const auto& timer) {
+ return timer->ClientID() == iClientId &&
+ timer->ClientIndex() == iClientIndex;
+ });
+ if (it != startDates.second.cend())
+ return (*it);
+ }
+
+ return {};
+}
+
+void CPVRTimersContainer::InsertEntry(const std::shared_ptr<CPVRTimerInfoTag>& newTimer)
+{
+ auto it = m_tags.find(newTimer->IsStartAnyTime() ? CDateTime() : newTimer->StartAsUTC());
+ if (it == m_tags.end())
+ {
+ VecTimerInfoTag addEntry({newTimer});
+ m_tags.insert(std::make_pair(newTimer->IsStartAnyTime() ? CDateTime() : newTimer->StartAsUTC(),
+ addEntry));
+ }
+ else
+ {
+ it->second.emplace_back(newTimer);
+ }
+}
+
+CPVRTimers::CPVRTimers()
+ : CThread("PVRTimers"),
+ m_settings({CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUP,
+ CSettings::SETTING_PVRPOWERMANAGEMENT_PREWAKEUP,
+ CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME,
+ CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME,
+ CSettings::SETTING_PVRRECORD_TIMERNOTIFICATIONS})
+{
+}
+
+bool CPVRTimers::Update(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ return LoadFromDatabase(clients) && UpdateFromClients(clients);
+}
+
+bool CPVRTimers::LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ // load local timers from database
+ const std::shared_ptr<CPVRDatabase> database = CServiceBroker::GetPVRManager().GetTVDatabase();
+ if (database)
+ {
+ const std::vector<std::shared_ptr<CPVRTimerInfoTag>> timers =
+ database->GetTimers(*this, clients);
+
+ if (std::accumulate(timers.cbegin(), timers.cend(), false,
+ [this](bool changed, const auto& timer) {
+ return (UpdateEntry(timer) != nullptr) ? true : changed;
+ }))
+ NotifyTimersEvent();
+ }
+
+ // ensure that every timer has its channel set
+ UpdateChannels();
+ return true;
+}
+
+void CPVRTimers::Unload()
+{
+ // remove all tags
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_tags.clear();
+}
+
+void CPVRTimers::Start()
+{
+ Stop();
+
+ CServiceBroker::GetPVRManager().Events().Subscribe(this, &CPVRTimers::Notify);
+ Create();
+}
+
+void CPVRTimers::Stop()
+{
+ StopThread();
+ CServiceBroker::GetPVRManager().Events().Unsubscribe(this);
+}
+
+bool CPVRTimers::UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_bIsUpdating)
+ return false;
+ m_bIsUpdating = true;
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updating timers");
+ CPVRTimersContainer newTimerList;
+ std::vector<int> failedClients;
+ CServiceBroker::GetPVRManager().Clients()->GetTimers(clients, &newTimerList, failedClients);
+ return UpdateEntries(newTimerList, failedClients);
+}
+
+void CPVRTimers::Process()
+{
+ while (!m_bStop)
+ {
+ // update all timers not owned by a client (e.g. reminders)
+ UpdateEntries(MAX_NOTIFICATION_DELAY.count());
+
+ CThread::Sleep(MAX_NOTIFICATION_DELAY);
+ }
+}
+
+bool CPVRTimers::IsRecording() const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ if (std::any_of(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [](const auto& timersEntry) { return timersEntry->IsRecording(); }))
+ return true;
+ }
+
+ return false;
+}
+
+void CPVRTimers::RemoveEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ auto it = m_tags.find(tag->IsStartAnyTime() ? CDateTime() : tag->StartAsUTC());
+ if (it != m_tags.end())
+ {
+ it->second.erase(std::remove_if(it->second.begin(), it->second.end(),
+ [&tag](const std::shared_ptr<CPVRTimerInfoTag>& timer) {
+ return tag->ClientID() == timer->ClientID() &&
+ tag->ClientIndex() == timer->ClientIndex();
+ }),
+ it->second.end());
+
+ if (it->second.empty())
+ m_tags.erase(it);
+ }
+}
+
+bool CPVRTimers::CheckAndAppendTimerNotification(
+ std::vector<std::pair<int, std::string>>& timerNotifications,
+ const std::shared_ptr<CPVRTimerInfoTag>& tag,
+ bool bDeleted) const
+{
+ // no notification on first update or if previous update failed for tag's client.
+ if (!m_bFirstUpdate && std::find(m_failedClients.cbegin(), m_failedClients.cend(),
+ tag->ClientID()) == m_failedClients.cend())
+ {
+ const std::string strMessage =
+ bDeleted ? tag->GetDeletedNotificationText() : tag->GetNotificationText();
+ timerNotifications.emplace_back(std::make_pair(tag->ClientID(), strMessage));
+ return true;
+ }
+ return false;
+}
+
+bool CPVRTimers::UpdateEntries(const CPVRTimersContainer& timers,
+ const std::vector<int>& failedClients)
+{
+ bool bChanged(false);
+ bool bAddedOrDeleted(false);
+ std::vector<std::pair<int, std::string>> timerNotifications;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* go through the timer list and check for updated or new timers */
+ for (const auto& tagsEntry : timers.GetTags())
+ {
+ for (const auto& timersEntry : tagsEntry.second)
+ {
+ /* check if this timer is present in this container */
+ const std::shared_ptr<CPVRTimerInfoTag> existingTimer =
+ GetByClient(timersEntry->ClientID(), timersEntry->ClientIndex());
+ if (existingTimer)
+ {
+ /* if it's present, update the current tag */
+ bool bStateChanged(existingTimer->State() != timersEntry->State());
+ if (existingTimer->UpdateEntry(timersEntry))
+ {
+ bChanged = true;
+ existingTimer->ResetChildState();
+
+ if (bStateChanged)
+ CheckAndAppendTimerNotification(timerNotifications, existingTimer, false);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Updated timer {} on client {}", timersEntry->ClientIndex(),
+ timersEntry->ClientID());
+ }
+ }
+ else
+ {
+ /* new timer */
+ std::shared_ptr<CPVRTimerInfoTag> newTimer =
+ std::shared_ptr<CPVRTimerInfoTag>(new CPVRTimerInfoTag);
+ newTimer->UpdateEntry(timersEntry);
+ newTimer->SetTimerID(++m_iLastId);
+ InsertEntry(newTimer);
+
+ bChanged = true;
+ bAddedOrDeleted = true;
+
+ CheckAndAppendTimerNotification(timerNotifications, newTimer, false);
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Added timer {} on client {}", timersEntry->ClientIndex(),
+ timersEntry->ClientID());
+ }
+ }
+ }
+
+ /* to collect timer with changed starting time */
+ VecTimerInfoTag timersToMove;
+
+ /* check for deleted timers */
+ for (auto it = m_tags.begin(); it != m_tags.end();)
+ {
+ for (auto it2 = it->second.begin(); it2 != it->second.end();)
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> timer = *it2;
+ if (!timers.GetByClient(timer->ClientID(), timer->ClientIndex()))
+ {
+ /* timer was not found */
+ bool bIgnoreTimer = !timer->IsOwnedByClient();
+ if (!bIgnoreTimer)
+ {
+ bIgnoreTimer = std::any_of(
+ failedClients.cbegin(), failedClients.cend(),
+ [&timer](const auto& failedClient) { return failedClient == timer->ClientID(); });
+ }
+
+ if (bIgnoreTimer)
+ {
+ ++it2;
+ continue;
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted timer {} on client {}", timer->ClientIndex(),
+ timer->ClientID());
+
+ CheckAndAppendTimerNotification(timerNotifications, timer, true);
+
+ it2 = it->second.erase(it2);
+
+ bChanged = true;
+ bAddedOrDeleted = true;
+ }
+ else if ((timer->IsStartAnyTime() && it->first != CDateTime()) ||
+ (!timer->IsStartAnyTime() && timer->StartAsUTC() != it->first))
+ {
+ /* timer start has changed */
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Changed start time timer {} on client {}",
+ timer->ClientIndex(), timer->ClientID());
+
+ /* remember timer */
+ timersToMove.push_back(timer);
+
+ /* remove timer for now, reinsert later */
+ it2 = it->second.erase(it2);
+
+ bChanged = true;
+ bAddedOrDeleted = true;
+ }
+ else
+ {
+ ++it2;
+ }
+ }
+ if (it->second.empty())
+ it = m_tags.erase(it);
+ else
+ ++it;
+ }
+
+ /* reinsert timers with changed timer start */
+ for (const auto& timer : timersToMove)
+ {
+ InsertEntry(timer);
+ }
+
+ /* update child information for all parent timers */
+ for (const auto& tagsEntry : m_tags)
+ {
+ for (const auto& timersEntry : tagsEntry.second)
+ {
+ if (timersEntry->IsTimerRule())
+ timersEntry->ResetChildState();
+ }
+
+ for (const auto& timersEntry : tagsEntry.second)
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> parentTimer = GetTimerRule(timersEntry);
+ if (parentTimer)
+ parentTimer->UpdateChildState(timersEntry, true);
+ }
+ }
+
+ m_failedClients = failedClients;
+ m_bFirstUpdate = false;
+ m_bIsUpdating = false;
+
+ if (bChanged)
+ {
+ UpdateChannels();
+ lock.unlock();
+
+ NotifyTimersEvent(bAddedOrDeleted);
+
+ if (!timerNotifications.empty())
+ {
+ CPVREventLogJob* job = new CPVREventLogJob;
+
+ /* queue notifications / fill eventlog */
+ for (const auto& entry : timerNotifications)
+ {
+ const std::shared_ptr<CPVRClient> client =
+ CServiceBroker::GetPVRManager().GetClient(entry.first);
+ if (client)
+ {
+ job->AddEvent(m_settings.GetBoolValue(CSettings::SETTING_PVRRECORD_TIMERNOTIFICATIONS),
+ EventLevel::Information, // info, no error
+ client->GetFriendlyName(), entry.second, client->Icon());
+ }
+ }
+
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+ }
+ }
+
+ return true;
+}
+
+namespace
+{
+std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEpgTagsForTimerRule(
+ const CPVRTimerRuleMatcher& matcher)
+{
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> matches;
+
+ const std::shared_ptr<CPVRChannel> channel = matcher.GetChannel();
+ if (channel)
+ {
+ // match single channel
+ const std::shared_ptr<CPVREpg> epg = channel->GetEPG();
+ if (epg)
+ {
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags = epg->GetTags();
+ std::copy_if(tags.cbegin(), tags.cend(), std::back_inserter(matches),
+ [&matcher](const auto& tag) { return matcher.Matches(tag); });
+ }
+ }
+ else
+ {
+ // match any channel
+ const std::vector<std::shared_ptr<CPVREpg>> epgs =
+ CServiceBroker::GetPVRManager().EpgContainer().GetAllEpgs();
+
+ for (const auto& epg : epgs)
+ {
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> tags = epg->GetTags();
+ std::copy_if(tags.cbegin(), tags.cend(), std::back_inserter(matches),
+ [&matcher](const auto& tag) { return matcher.Matches(tag); });
+ }
+ }
+
+ return matches;
+}
+
+void AddTimerRuleToEpgMap(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ const CDateTime& now,
+ std::map<std::shared_ptr<CPVREpg>, std::vector<std::shared_ptr<CPVRTimerRuleMatcher>>>& epgMap,
+ bool& bFetchedAllEpgs)
+{
+ const std::shared_ptr<CPVRChannel> channel = timer->Channel();
+ if (channel)
+ {
+ const std::shared_ptr<CPVREpg> epg = channel->GetEPG();
+ if (epg)
+ {
+ const std::shared_ptr<CPVRTimerRuleMatcher> matcher =
+ std::make_shared<CPVRTimerRuleMatcher>(timer, now);
+ auto it = epgMap.find(epg);
+ if (it == epgMap.end())
+ epgMap.insert({epg, {matcher}});
+ else
+ it->second.emplace_back(matcher);
+ }
+ }
+ else
+ {
+ // rule matches "any channel" => we need to check all channels
+ if (!bFetchedAllEpgs)
+ {
+ const std::vector<std::shared_ptr<CPVREpg>> epgs =
+ CServiceBroker::GetPVRManager().EpgContainer().GetAllEpgs();
+ for (const auto& epg : epgs)
+ {
+ const std::shared_ptr<CPVRTimerRuleMatcher> matcher =
+ std::make_shared<CPVRTimerRuleMatcher>(timer, now);
+ auto it = epgMap.find(epg);
+ if (it == epgMap.end())
+ epgMap.insert({epg, {matcher}});
+ else
+ it->second.emplace_back(matcher);
+ }
+ bFetchedAllEpgs = true;
+ }
+ else
+ {
+ for (auto& epgMapEntry : epgMap)
+ {
+ const std::shared_ptr<CPVRTimerRuleMatcher> matcher =
+ std::make_shared<CPVRTimerRuleMatcher>(timer, now);
+ epgMapEntry.second.emplace_back(matcher);
+ }
+ }
+ }
+}
+} // unnamed namespace
+
+bool CPVRTimers::UpdateEntries(int iMaxNotificationDelay)
+{
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> timersToReinsert;
+ std::vector<std::pair<std::shared_ptr<CPVRTimerInfoTag>, std::shared_ptr<CPVRTimerInfoTag>>>
+ childTimersToInsert;
+ bool bChanged = false;
+ const CDateTime now = CDateTime::GetUTCDateTime();
+ bool bFetchedAllEpgs = false;
+ std::map<std::shared_ptr<CPVREpg>, std::vector<std::shared_ptr<CPVRTimerRuleMatcher>>> epgMap;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (auto it = m_tags.begin(); it != m_tags.end();)
+ {
+ for (auto it2 = it->second.begin(); it2 != it->second.end();)
+ {
+ std::shared_ptr<CPVRTimerInfoTag> timer = *it2;
+ bool bDeleteTimer = false;
+ if (!timer->IsOwnedByClient())
+ {
+ if (timer->IsEpgBased())
+ {
+ // update epg tag
+ const std::shared_ptr<CPVREpg> epg =
+ CServiceBroker::GetPVRManager().EpgContainer().GetByChannelUid(
+ timer->Channel()->ClientID(), timer->Channel()->UniqueID());
+ if (epg)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> epgTag =
+ epg->GetTagByBroadcastId(timer->UniqueBroadcastID());
+ if (epgTag)
+ {
+ timer->SetEpgInfoTag(epgTag);
+
+ bool bStartChanged =
+ !timer->IsStartAnyTime() && epgTag->StartAsUTC() != timer->StartAsUTC();
+ bool bEndChanged = !timer->IsEndAnyTime() && epgTag->EndAsUTC() != timer->EndAsUTC();
+ if (bStartChanged || bEndChanged)
+ {
+ if (bStartChanged)
+ timer->SetStartFromUTC(epgTag->StartAsUTC());
+ if (bEndChanged)
+ timer->SetEndFromUTC(epgTag->EndAsUTC());
+
+ timer->UpdateSummary();
+ bChanged = true;
+
+ if (bStartChanged)
+ {
+ // start time changed. timer must be reinserted in timer map
+ bDeleteTimer = true;
+ timersToReinsert.emplace_back(timer); // remember and reinsert/save later
+ }
+ else
+ {
+ // save changes to database
+ timer->Persist();
+ }
+ }
+ }
+ }
+ }
+
+ // check for due timers and announce/delete them
+ int iMarginStart = timer->GetTimerType()->SupportsStartMargin() ? timer->MarginStart() : 0;
+ if (!timer->IsTimerRule() &&
+ (timer->StartAsUTC() - CDateTimeSpan(0, 0, iMarginStart, iMaxNotificationDelay)) < now)
+ {
+ if (timer->IsReminder() && !timer->IsDisabled())
+ {
+ // reminder is due / over due. announce it.
+ m_remindersToAnnounce.push(timer);
+ }
+
+ if (timer->EndAsUTC() >= now)
+ {
+ // disable timer until timer's end time is due
+ if (!timer->IsDisabled())
+ {
+ timer->SetState(PVR_TIMER_STATE_DISABLED);
+ bChanged = true;
+ }
+ }
+ else
+ {
+ // end time due. delete completed timer
+ bChanged = true;
+ bDeleteTimer = true;
+ timer->DeleteFromDatabase();
+ }
+ }
+
+ if (timer->IsTimerRule() && timer->IsReminder() && timer->IsActive())
+ {
+ if (timer->IsEpgBased())
+ {
+ if (m_bReminderRulesUpdatePending)
+ AddTimerRuleToEpgMap(timer, now, epgMap, bFetchedAllEpgs);
+ }
+ else
+ {
+ // create new children of time-based reminder timer rules
+ const CPVRTimerRuleMatcher matcher(timer, now);
+ const CDateTime nextStart = matcher.GetNextTimerStart();
+ if (nextStart.IsValid())
+ {
+ bool bCreate = false;
+ const auto it1 = m_tags.find(nextStart);
+ if (it1 == m_tags.end())
+ bCreate = true;
+ else
+ bCreate = std::none_of(it1->second.cbegin(), it1->second.cend(),
+ [&timer](const std::shared_ptr<CPVRTimerInfoTag>& tmr) {
+ return tmr->ParentClientIndex() == timer->ClientIndex();
+ });
+ if (bCreate)
+ {
+ const CDateTimeSpan duration = timer->EndAsUTC() - timer->StartAsUTC();
+ const std::shared_ptr<CPVRTimerInfoTag> childTimer =
+ CPVRTimerInfoTag::CreateReminderFromDate(
+ nextStart, duration.GetSecondsTotal() / 60, timer);
+ if (childTimer)
+ {
+ bChanged = true;
+ childTimersToInsert.emplace_back(
+ std::make_pair(timer, childTimer)); // remember and insert/save later
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (bDeleteTimer)
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> parent = GetTimerRule(timer);
+ if (parent)
+ parent->UpdateChildState(timer, false);
+
+ it2 = it->second.erase(it2);
+ }
+ else
+ {
+ ++it2;
+ }
+ }
+
+ if (it->second.empty())
+ it = m_tags.erase(it);
+ else
+ ++it;
+ }
+
+ // create new children of local epg-based reminder timer rules
+ for (const auto& epgMapEntry : epgMap)
+ {
+ const auto epgTags = epgMapEntry.first->GetTags();
+ for (const auto& epgTag : epgTags)
+ {
+ if (GetTimerForEpgTag(epgTag))
+ continue;
+
+ for (const auto& matcher : epgMapEntry.second)
+ {
+ if (!matcher->Matches(epgTag))
+ continue;
+
+ const std::shared_ptr<CPVRTimerInfoTag> childTimer =
+ CPVRTimerInfoTag::CreateReminderFromEpg(epgTag, matcher->GetTimerRule());
+ if (childTimer)
+ {
+ bChanged = true;
+ childTimersToInsert.emplace_back(std::make_pair(
+ matcher->GetTimerRule(), childTimer)); // remember and insert/save later
+ }
+ }
+ }
+ }
+
+ // reinsert timers with changed timer start
+ for (const auto& timer : timersToReinsert)
+ {
+ InsertEntry(timer);
+ timer->Persist();
+ }
+
+ // insert new children of time-based local timer rules
+ for (const auto& timerPair : childTimersToInsert)
+ {
+ PersistAndUpdateLocalTimer(timerPair.second, timerPair.first);
+ }
+
+ m_bReminderRulesUpdatePending = false;
+
+ // announce changes
+ if (bChanged)
+ NotifyTimersEvent();
+
+ if (!m_remindersToAnnounce.empty())
+ CServiceBroker::GetPVRManager().PublishEvent(PVREvent::AnnounceReminder);
+
+ return bChanged;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextReminderToAnnnounce()
+{
+ std::shared_ptr<CPVRTimerInfoTag> ret;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (!m_remindersToAnnounce.empty())
+ {
+ ret = m_remindersToAnnounce.front();
+ m_remindersToAnnounce.pop();
+ }
+ return ret;
+}
+
+bool CPVRTimers::KindMatchesTag(const TimerKind& eKind,
+ const std::shared_ptr<CPVRTimerInfoTag>& tag) const
+{
+ return (eKind == TimerKindAny) || (eKind == TimerKindTV && !tag->IsRadio()) ||
+ (eKind == TimerKindRadio && tag->IsRadio());
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveTimer(const TimerKind& eKind,
+ bool bIgnoreReminders) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ for (const auto& timersEntry : tagsEntry.second)
+ {
+ if (bIgnoreReminders && timersEntry->IsReminder())
+ continue;
+
+ if (KindMatchesTag(eKind, timersEntry) && timersEntry->IsActive() &&
+ !timersEntry->IsRecording() && !timersEntry->IsTimerRule() && !timersEntry->IsBroken())
+ return timersEntry;
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveTimer(
+ bool bIgnoreReminders /* = true */) const
+{
+ return GetNextActiveTimer(TimerKindAny, bIgnoreReminders);
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveTVTimer() const
+{
+ return GetNextActiveTimer(TimerKindTV, true);
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetNextActiveRadioTimer() const
+{
+ return GetNextActiveTimer(TimerKindRadio, true);
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveTimers() const
+{
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> tags;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ std::copy_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(), std::back_inserter(tags),
+ [](const auto& timersEntry) {
+ return timersEntry->IsActive() && !timersEntry->IsBroken() &&
+ !timersEntry->IsReminder() && !timersEntry->IsTimerRule();
+ });
+ }
+
+ return tags;
+}
+
+int CPVRTimers::AmountActiveTimers(const TimerKind& eKind) const
+{
+ int iReturn = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ iReturn += std::count_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [this, &eKind](const auto& timersEntry) {
+ return KindMatchesTag(eKind, timersEntry) &&
+ timersEntry->IsActive() && !timersEntry->IsBroken() &&
+ !timersEntry->IsReminder() && !timersEntry->IsTimerRule();
+ });
+ }
+
+ return iReturn;
+}
+
+int CPVRTimers::AmountActiveTimers() const
+{
+ return AmountActiveTimers(TimerKindAny);
+}
+
+int CPVRTimers::AmountActiveTVTimers() const
+{
+ return AmountActiveTimers(TimerKindTV);
+}
+
+int CPVRTimers::AmountActiveRadioTimers() const
+{
+ return AmountActiveTimers(TimerKindRadio);
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveRecordings(
+ const TimerKind& eKind) const
+{
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> tags;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ std::copy_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(), std::back_inserter(tags),
+ [this, &eKind](const auto& timersEntry) {
+ return KindMatchesTag(eKind, timersEntry) && timersEntry->IsRecording() &&
+ !timersEntry->IsTimerRule() && !timersEntry->IsBroken() &&
+ !timersEntry->IsReminder();
+ });
+ }
+
+ return tags;
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveRecordings() const
+{
+ return GetActiveRecordings(TimerKindAny);
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveTVRecordings() const
+{
+ return GetActiveRecordings(TimerKindTV);
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetActiveRadioRecordings() const
+{
+ return GetActiveRecordings(TimerKindRadio);
+}
+
+int CPVRTimers::AmountActiveRecordings(const TimerKind& eKind) const
+{
+ int iReturn = 0;
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ iReturn += std::count_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [this, &eKind](const auto& timersEntry) {
+ return KindMatchesTag(eKind, timersEntry) &&
+ timersEntry->IsRecording() && !timersEntry->IsTimerRule() &&
+ !timersEntry->IsBroken() && !timersEntry->IsReminder();
+ });
+ }
+
+ return iReturn;
+}
+
+int CPVRTimers::AmountActiveRecordings() const
+{
+ return AmountActiveRecordings(TimerKindAny);
+}
+
+int CPVRTimers::AmountActiveTVRecordings() const
+{
+ return AmountActiveRecordings(TimerKindTV);
+}
+
+int CPVRTimers::AmountActiveRadioRecordings() const
+{
+ return AmountActiveRecordings(TimerKindRadio);
+}
+
+/********** channel methods **********/
+
+bool CPVRTimers::DeleteTimersOnChannel(const std::shared_ptr<CPVRChannel>& channel,
+ bool bDeleteTimerRules /* = true */,
+ bool bCurrentlyActiveOnly /* = false */)
+{
+ bool bReturn = false;
+ bool bChanged = false;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (MapTags::reverse_iterator it = m_tags.rbegin(); it != m_tags.rend(); ++it)
+ {
+ for (const auto& timersEntry : (*it).second)
+ {
+ bool bDeleteActiveItem = !bCurrentlyActiveOnly || timersEntry->IsRecording();
+ bool bDeleteTimerRuleItem = bDeleteTimerRules || !timersEntry->IsTimerRule();
+ bool bChannelsMatch = timersEntry->HasChannel() && timersEntry->Channel() == channel;
+
+ if (bDeleteActiveItem && bDeleteTimerRuleItem && bChannelsMatch)
+ {
+ CLog::LogFC(LOGDEBUG, LOGPVR, "Deleted timer {} on client {}", timersEntry->ClientIndex(),
+ timersEntry->ClientID());
+ bReturn = (timersEntry->DeleteFromClient(true) == TimerOperationResult::OK) || bReturn;
+ bChanged = true;
+ }
+ }
+ }
+ }
+
+ if (bChanged)
+ NotifyTimersEvent();
+
+ return bReturn;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::UpdateEntry(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer)
+{
+ bool bChanged = false;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ std::shared_ptr<CPVRTimerInfoTag> tag = GetByClient(timer->ClientID(), timer->ClientIndex());
+ if (tag)
+ {
+ bool bReinsert = tag->StartAsUTC() != timer->StartAsUTC();
+ if (bReinsert)
+ {
+ RemoveEntry(tag);
+ }
+
+ bChanged = tag->UpdateEntry(timer);
+
+ if (bReinsert)
+ {
+ InsertEntry(tag);
+ }
+ }
+ else
+ {
+ tag.reset(new CPVRTimerInfoTag());
+ if (tag->UpdateEntry(timer))
+ {
+ tag->SetTimerID(++m_iLastId);
+ InsertEntry(tag);
+ bChanged = true;
+ }
+ }
+
+ return bChanged ? tag : std::shared_ptr<CPVRTimerInfoTag>();
+}
+
+bool CPVRTimers::AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag)
+{
+ bool bReturn = false;
+ if (tag->IsOwnedByClient())
+ {
+ bReturn = tag->AddToClient();
+ }
+ else
+ {
+ bReturn = AddLocalTimer(tag, true);
+ }
+ return bReturn;
+}
+
+TimerOperationResult CPVRTimers::DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag,
+ bool bForce /* = false */,
+ bool bDeleteRule /* = false */)
+{
+ TimerOperationResult ret = TimerOperationResult::FAILED;
+ if (!tag)
+ return ret;
+
+ std::shared_ptr<CPVRTimerInfoTag> tagToDelete = tag;
+
+ if (bDeleteRule)
+ {
+ /* delete the timer rule that scheduled this timer. */
+ const std::shared_ptr<CPVRTimerInfoTag> ruleTag = GetTimerRule(tagToDelete);
+ if (!ruleTag)
+ {
+ CLog::LogF(LOGERROR, "Unable to obtain timer rule for given timer");
+ return ret;
+ }
+
+ tagToDelete = ruleTag;
+ }
+
+ if (tagToDelete->IsOwnedByClient())
+ {
+ ret = tagToDelete->DeleteFromClient(bForce);
+ }
+ else
+ {
+ if (DeleteLocalTimer(tagToDelete, true))
+ ret = TimerOperationResult::OK;
+ }
+
+ return ret;
+}
+
+bool CPVRTimers::UpdateTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag)
+{
+ bool bReturn = false;
+ if (tag->IsOwnedByClient())
+ {
+ bReturn = tag->UpdateOnClient();
+ }
+ else
+ {
+ bReturn = UpdateLocalTimer(tag);
+ }
+ return bReturn;
+}
+
+bool CPVRTimers::AddLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ const std::shared_ptr<CPVRTimerInfoTag> persistedTimer = PersistAndUpdateLocalTimer(tag, nullptr);
+ bool bReturn = !!persistedTimer;
+
+ if (bReturn && persistedTimer->IsTimerRule() && persistedTimer->IsActive())
+ {
+ if (persistedTimer->IsEpgBased())
+ {
+ // create and persist children of local epg-based timer rule
+ const std::vector<std::shared_ptr<CPVREpgInfoTag>> epgTags =
+ GetEpgTagsForTimerRule(CPVRTimerRuleMatcher(persistedTimer, CDateTime::GetUTCDateTime()));
+ for (const auto& epgTag : epgTags)
+ {
+ const std::shared_ptr<CPVRTimerInfoTag> childTimer =
+ CPVRTimerInfoTag::CreateReminderFromEpg(epgTag, persistedTimer);
+ if (childTimer)
+ {
+ PersistAndUpdateLocalTimer(childTimer, persistedTimer);
+ }
+ }
+ }
+ else
+ {
+ // create and persist children of local time-based timer rule
+ const CDateTime nextStart =
+ CPVRTimerRuleMatcher(persistedTimer, CDateTime::GetUTCDateTime()).GetNextTimerStart();
+ if (nextStart.IsValid())
+ {
+ const CDateTimeSpan duration = persistedTimer->EndAsUTC() - persistedTimer->StartAsUTC();
+ const std::shared_ptr<CPVRTimerInfoTag> childTimer =
+ CPVRTimerInfoTag::CreateReminderFromDate(nextStart, duration.GetSecondsTotal() / 60,
+ persistedTimer);
+ if (childTimer)
+ {
+ PersistAndUpdateLocalTimer(childTimer, persistedTimer);
+ }
+ }
+ }
+ }
+
+ if (bNotify && bReturn)
+ {
+ lock.unlock();
+ NotifyTimersEvent();
+ }
+
+ return bReturn;
+}
+
+bool CPVRTimers::DeleteLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ RemoveEntry(tag);
+
+ bool bReturn = tag->DeleteFromDatabase();
+
+ if (bReturn && tag->IsTimerRule())
+ {
+ // delete children of local timer rule
+ for (auto it = m_tags.begin(); it != m_tags.end();)
+ {
+ for (auto it2 = it->second.begin(); it2 != it->second.end();)
+ {
+ std::shared_ptr<CPVRTimerInfoTag> timer = *it2;
+ if (timer->ParentClientIndex() == tag->ClientIndex())
+ {
+ tag->UpdateChildState(timer, false);
+ it2 = it->second.erase(it2);
+ timer->DeleteFromDatabase();
+ }
+ else
+ {
+ ++it2;
+ }
+ }
+
+ if (it->second.empty())
+ it = m_tags.erase(it);
+ else
+ ++it;
+ }
+ }
+
+ if (bNotify && bReturn)
+ {
+ lock.unlock();
+ NotifyTimersEvent();
+ }
+
+ return bReturn;
+}
+
+bool CPVRTimers::UpdateLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag)
+{
+ // delete and re-create timer and children, if any.
+ bool bReturn = DeleteLocalTimer(tag, false);
+
+ if (bReturn)
+ bReturn = AddLocalTimer(tag, false);
+
+ if (bReturn)
+ NotifyTimersEvent();
+
+ return bReturn;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::PersistAndUpdateLocalTimer(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ const std::shared_ptr<CPVRTimerInfoTag>& parentTimer)
+{
+ std::shared_ptr<CPVRTimerInfoTag> tag;
+ bool bReturn = timer->Persist();
+ if (bReturn)
+ {
+ tag = UpdateEntry(timer);
+ if (tag && parentTimer)
+ parentTimer->UpdateChildState(timer, true);
+ }
+ return bReturn ? tag : std::shared_ptr<CPVRTimerInfoTag>();
+}
+
+bool CPVRTimers::IsRecordingOnChannel(const CPVRChannel& channel) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ if (std::any_of(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [&channel](const auto& timersEntry) {
+ return timersEntry->IsRecording() &&
+ timersEntry->ClientChannelUID() == channel.UniqueID() &&
+ timersEntry->ClientID() == channel.ClientID();
+ }))
+ return true;
+ }
+
+ return false;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetActiveTimerForChannel(
+ const std::shared_ptr<CPVRChannel>& channel) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tagsEntry : m_tags)
+ {
+ const auto it = std::find_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [&channel](const auto& timersEntry) {
+ return timersEntry->IsRecording() &&
+ timersEntry->ClientChannelUID() == channel->UniqueID() &&
+ timersEntry->ClientID() == channel->ClientID();
+ });
+ if (it != tagsEntry.second.cend())
+ return (*it);
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetTimerForEpgTag(
+ const std::shared_ptr<CPVREpgInfoTag>& epgTag) const
+{
+ if (epgTag)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ for (const auto& tagsEntry : m_tags)
+ {
+ for (const auto& timersEntry : tagsEntry.second)
+ {
+ if (timersEntry->IsTimerRule())
+ continue;
+
+ if (timersEntry->GetEpgInfoTag(false) == epgTag)
+ return timersEntry;
+
+ if (timersEntry->ClientChannelUID() != PVR_CHANNEL_INVALID_UID &&
+ timersEntry->ClientChannelUID() == epgTag->UniqueChannelID() &&
+ timersEntry->ClientID() == epgTag->ClientID())
+ {
+ if (timersEntry->UniqueBroadcastID() != EPG_TAG_INVALID_UID &&
+ timersEntry->UniqueBroadcastID() == epgTag->UniqueBroadcastID())
+ return timersEntry;
+
+ if (timersEntry->IsRadio() == epgTag->IsRadio() &&
+ timersEntry->StartAsUTC() <= epgTag->StartAsUTC() &&
+ timersEntry->EndAsUTC() >= epgTag->EndAsUTC())
+ return timersEntry;
+ }
+ }
+ }
+ }
+
+ return {};
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetTimerRule(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer) const
+{
+ if (timer)
+ {
+ const int iParentClientIndex = timer->ParentClientIndex();
+ if (iParentClientIndex != PVR_TIMER_NO_PARENT)
+ {
+ int iClientId = timer->ClientID();
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tagsEntry : m_tags)
+ {
+ const auto it = std::find_if(tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [iClientId, iParentClientIndex](const auto& timersEntry) {
+ return timersEntry->ClientID() == iClientId &&
+ timersEntry->ClientIndex() == iParentClientIndex;
+ });
+ if (it != tagsEntry.second.cend())
+ return (*it);
+ }
+ }
+ }
+
+ return {};
+}
+
+void CPVRTimers::Notify(const PVREvent& event)
+{
+ switch (static_cast<PVREvent>(event))
+ {
+ case PVREvent::EpgContainer:
+ CServiceBroker::GetPVRManager().TriggerTimersUpdate();
+ break;
+ case PVREvent::Epg:
+ case PVREvent::EpgItemUpdate:
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_bReminderRulesUpdatePending = true;
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+CDateTime CPVRTimers::GetNextEventTime() const
+{
+ const bool dailywakup =
+ m_settings.GetBoolValue(CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUP);
+ const CDateTime now = CDateTime::GetUTCDateTime();
+ const CDateTimeSpan prewakeup(
+ 0, 0, m_settings.GetIntValue(CSettings::SETTING_PVRPOWERMANAGEMENT_PREWAKEUP), 0);
+ const CDateTimeSpan idle(
+ 0, 0, m_settings.GetIntValue(CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME), 0);
+
+ CDateTime wakeuptime;
+
+ /* Check next active time */
+ const std::shared_ptr<CPVRTimerInfoTag> timer = GetNextActiveTimer(false);
+ if (timer)
+ {
+ const CDateTimeSpan prestart(0, 0, timer->MarginStart(), 0);
+ const CDateTime start = timer->StartAsUTC();
+ wakeuptime =
+ ((start - prestart - prewakeup - idle) > now) ? start - prestart - prewakeup : now + idle;
+ }
+
+ /* check daily wake up */
+ if (dailywakup)
+ {
+ CDateTime dailywakeuptime;
+ dailywakeuptime.SetFromDBTime(
+ m_settings.GetStringValue(CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME));
+ dailywakeuptime = dailywakeuptime.GetAsUTCDateTime();
+
+ dailywakeuptime.SetDateTime(now.GetYear(), now.GetMonth(), now.GetDay(),
+ dailywakeuptime.GetHour(), dailywakeuptime.GetMinute(),
+ dailywakeuptime.GetSecond());
+
+ if ((dailywakeuptime - idle) < now)
+ {
+ const CDateTimeSpan oneDay(1, 0, 0, 0);
+ dailywakeuptime += oneDay;
+ }
+ if (!wakeuptime.IsValid() || dailywakeuptime < wakeuptime)
+ wakeuptime = dailywakeuptime;
+ }
+
+ const CDateTime retVal(wakeuptime);
+ return retVal;
+}
+
+void CPVRTimers::UpdateChannels()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tagsEntry : m_tags)
+ {
+ for (const auto& timersEntry : tagsEntry.second)
+ timersEntry->UpdateChannel();
+ }
+}
+
+std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRTimers::GetAll() const
+{
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> timers;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tagsEntry : m_tags)
+ {
+ std::copy(tagsEntry.second.cbegin(), tagsEntry.second.cend(), std::back_inserter(timers));
+ }
+
+ return timers;
+}
+
+std::shared_ptr<CPVRTimerInfoTag> CPVRTimers::GetById(unsigned int iTimerId) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ for (const auto& tagsEntry : m_tags)
+ {
+ const auto it = std::find_if(
+ tagsEntry.second.cbegin(), tagsEntry.second.cend(),
+ [iTimerId](const auto& timersEntry) { return timersEntry->TimerID() == iTimerId; });
+ if (it != tagsEntry.second.cend())
+ return (*it);
+ }
+
+ return {};
+}
+
+void CPVRTimers::NotifyTimersEvent(bool bAddedOrDeleted /* = true */)
+{
+ CServiceBroker::GetPVRManager().PublishEvent(bAddedOrDeleted ? PVREvent::TimersInvalidated
+ : PVREvent::Timers);
+}
diff --git a/xbmc/pvr/timers/PVRTimers.h b/xbmc/pvr/timers/PVRTimers.h
new file mode 100644
index 0000000..6b0e4e7
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimers.h
@@ -0,0 +1,331 @@
+/*
+ * 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 "pvr/settings/PVRSettings.h"
+#include "threads/Thread.h"
+
+#include <map>
+#include <memory>
+#include <queue>
+#include <vector>
+
+class CDateTime;
+
+namespace PVR
+{
+enum class TimerOperationResult;
+enum class PVREvent;
+
+class CPVRChannel;
+class CPVRClient;
+class CPVREpgInfoTag;
+class CPVRTimerInfoTag;
+class CPVRTimersPath;
+
+class CPVRTimersContainer
+{
+public:
+ /*!
+ * @brief Add a timer tag to this container or update the tag if already present in this
+ * container.
+ * @param The timer tag
+ * @return True, if the update was successful. False, otherwise.
+ */
+ bool UpdateFromClient(const std::shared_ptr<CPVRTimerInfoTag>& timer);
+
+ /*!
+ * @brief Get the timer tag denoted by given client id and timer id.
+ * @param iClientId The client id.
+ * @param iClientIndex The timer id.
+ * @return the timer tag if found, null otherwise.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetByClient(int iClientId, int iClientIndex) const;
+
+ typedef std::vector<std::shared_ptr<CPVRTimerInfoTag>> VecTimerInfoTag;
+ typedef std::map<CDateTime, VecTimerInfoTag> MapTags;
+
+ /*!
+ * @brief Get the timertags map.
+ * @return The map.
+ */
+ const MapTags& GetTags() const { return m_tags; }
+
+protected:
+ void InsertEntry(const std::shared_ptr<CPVRTimerInfoTag>& newTimer);
+
+ mutable CCriticalSection m_critSection;
+ unsigned int m_iLastId = 0;
+ MapTags m_tags;
+};
+
+class CPVRTimers : public CPVRTimersContainer, private CThread
+{
+public:
+ CPVRTimers();
+ ~CPVRTimers() override = default;
+
+ /*!
+ * @brief start the timer update thread.
+ */
+ void Start();
+
+ /*!
+ * @brief stop the timer update thread.
+ */
+ void Stop();
+
+ /*!
+ * @brief Update all timers from PVR database and from given clients.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool Update(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @brief unload all timers.
+ */
+ void Unload();
+
+ /*!
+ * @brief Update data with recordings from the given clients, sync with local data.
+ * @param clients The clients to fetch data from. Leave empty to fetch data from all created
+ * clients.
+ * @return True on success, false otherwise.
+ */
+ bool UpdateFromClients(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ /*!
+ * @param bIgnoreReminders include or ignore reminders
+ * @return The tv or radio timer that will be active next (state scheduled), or nullptr if none.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer(bool bIgnoreReminders = true) const;
+
+ /*!
+ * @return The tv timer that will be active next (state scheduled), or nullptr if none.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTVTimer() const;
+
+ /*!
+ * @return The radio timer that will be active next (state scheduled), or nullptr if none.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveRadioTimer() const;
+
+ /*!
+ * @return All timers that are active (states scheduled or recording)
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveTimers() const;
+
+ /*!
+ * @return Next due reminder, if any. Removes it from the queue of due reminders.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetNextReminderToAnnnounce();
+
+ /*!
+ * Get all timers
+ * @return The list of all timers
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetAll() const;
+
+ /*!
+ * @return The amount of tv and radio timers that are active (states scheduled or recording)
+ */
+ int AmountActiveTimers() const;
+
+ /*!
+ * @return The amount of tv timers that are active (states scheduled or recording)
+ */
+ int AmountActiveTVTimers() const;
+
+ /*!
+ * @return The amount of radio timers that are active (states scheduled or recording)
+ */
+ int AmountActiveRadioTimers() const;
+
+ /*!
+ * @return All tv and radio timers that are recording
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() const;
+
+ /*!
+ * @return All tv timers that are recording
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveTVRecordings() const;
+
+ /*!
+ * @return All radio timers that are recording
+ */
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRadioRecordings() const;
+
+ /*!
+ * @return True when recording, false otherwise.
+ */
+ bool IsRecording() const;
+
+ /*!
+ * @brief Check if a recording is running on the given channel.
+ * @param channel The channel to check.
+ * @return True when recording, false otherwise.
+ */
+ bool IsRecordingOnChannel(const CPVRChannel& channel) const;
+
+ /*!
+ * @brief Obtain the active timer for a given channel.
+ * @param channel The channel to check.
+ * @return the timer, null otherwise.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetActiveTimerForChannel(
+ const std::shared_ptr<CPVRChannel>& channel) const;
+
+ /*!
+ * @return The amount of tv and radio timers that are currently recording
+ */
+ int AmountActiveRecordings() const;
+
+ /*!
+ * @return The amount of tv timers that are currently recording
+ */
+ int AmountActiveTVRecordings() const;
+
+ /*!
+ * @return The amount of radio timers that are currently recording
+ */
+ int AmountActiveRadioRecordings() const;
+
+ /*!
+ * @brief Delete all timers on a channel.
+ * @param channel The channel to delete the timers for.
+ * @param bDeleteTimerRules True to delete timer rules too, false otherwise.
+ * @param bCurrentlyActiveOnly True to delete timers that are currently running only.
+ * @return True if timers any were deleted, false otherwise.
+ */
+ bool DeleteTimersOnChannel(const std::shared_ptr<CPVRChannel>& channel,
+ bool bDeleteTimerRules = true,
+ bool bCurrentlyActiveOnly = false);
+
+ /*!
+ * @return Next event time (timer or daily wake up)
+ */
+ CDateTime GetNextEventTime() const;
+
+ /*!
+ * @brief Add a timer to the client. Doesn't add the timer to the container. The backend will do
+ * this.
+ * @param tag The timer to add.
+ * @return True if timer add request was sent correctly, false if not.
+ */
+ bool AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag);
+
+ /*!
+ * @brief Delete a timer on the client. Doesn't delete the timer from the container. The backend
+ * will do this.
+ * @param tag The timer to delete.
+ * @param bForce Control what to do in case the timer is currently recording.
+ * True to force to delete the timer, false to return TimerDeleteResult::RECORDING.
+ * @param bDeleteRule Also delete the timer rule that scheduled the timer instead of single timer
+ * only.
+ * @return The result.
+ */
+ TimerOperationResult DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag,
+ bool bForce = false,
+ bool bDeleteRule = false);
+
+ /*!
+ * @brief Update the timer on the client. Doesn't update the timer in the container. The backend
+ * will do this.
+ * @param tag The timer to update.
+ * @return True if timer update request was sent correctly, false if not.
+ */
+ bool UpdateTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag);
+
+ /*!
+ * @brief Get the timer tag that matches the given epg tag.
+ * @param epgTag The epg tag.
+ * @return The requested timer tag, or nullptr if none was found.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetTimerForEpgTag(
+ const std::shared_ptr<CPVREpgInfoTag>& epgTag) const;
+
+ /*!
+ * @brief Get the timer rule for a given timer tag
+ * @param timer The timer to query the timer rule for
+ * @return The timer rule, or nullptr if none was found.
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetTimerRule(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer) const;
+
+ /*!
+ * @brief Update the channel pointers.
+ */
+ void UpdateChannels();
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+
+ /*!
+ * @brief Get a timer tag given it's unique ID
+ * @param iTimerId The ID to find
+ * @return The tag, or an empty one when not found
+ */
+ std::shared_ptr<CPVRTimerInfoTag> GetById(unsigned int iTimerId) const;
+
+private:
+ void Process() override;
+
+ /*!
+ * @brief Load all timers from PVR database.
+ * @param clients The PVR clients data should be loaded for. Leave empty for all clients.
+ * @return True on success, false otherwise.
+ */
+ bool LoadFromDatabase(const std::vector<std::shared_ptr<CPVRClient>>& clients);
+
+ void RemoveEntry(const std::shared_ptr<CPVRTimerInfoTag>& tag);
+ bool UpdateEntries(const CPVRTimersContainer& timers, const std::vector<int>& failedClients);
+ bool UpdateEntries(int iMaxNotificationDelay);
+ std::shared_ptr<CPVRTimerInfoTag> UpdateEntry(const std::shared_ptr<CPVRTimerInfoTag>& timer);
+
+ bool AddLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify);
+ bool DeleteLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag, bool bNotify);
+ bool UpdateLocalTimer(const std::shared_ptr<CPVRTimerInfoTag>& tag);
+ std::shared_ptr<CPVRTimerInfoTag> PersistAndUpdateLocalTimer(
+ const std::shared_ptr<CPVRTimerInfoTag>& timer,
+ const std::shared_ptr<CPVRTimerInfoTag>& parentTimer);
+ void NotifyTimersEvent(bool bAddedOrDeleted = true);
+
+ enum TimerKind
+ {
+ TimerKindAny = 0,
+ TimerKindTV,
+ TimerKindRadio
+ };
+
+ bool KindMatchesTag(const TimerKind& eKind, const std::shared_ptr<CPVRTimerInfoTag>& tag) const;
+
+ std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer(const TimerKind& eKind,
+ bool bIgnoreReminders) const;
+ int AmountActiveTimers(const TimerKind& eKind) const;
+ std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings(const TimerKind& eKind) const;
+ int AmountActiveRecordings(const TimerKind& eKind) const;
+
+ bool CheckAndAppendTimerNotification(std::vector<std::pair<int, std::string>>& timerNotifications,
+ const std::shared_ptr<CPVRTimerInfoTag>& tag,
+ bool bDeleted) const;
+
+ bool m_bIsUpdating = false;
+ CPVRSettings m_settings;
+ std::queue<std::shared_ptr<CPVRTimerInfoTag>> m_remindersToAnnounce;
+ bool m_bReminderRulesUpdatePending = false;
+
+ bool m_bFirstUpdate = true;
+ std::vector<int> m_failedClients;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/timers/PVRTimersPath.cpp b/xbmc/pvr/timers/PVRTimersPath.cpp
new file mode 100644
index 0000000..ea2265c
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimersPath.cpp
@@ -0,0 +1,82 @@
+/*
+ * 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 "PVRTimersPath.h"
+
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <cstdlib>
+#include <string>
+#include <vector>
+
+using namespace PVR;
+
+const std::string CPVRTimersPath::PATH_ADDTIMER = "pvr://timers/addtimer/";
+const std::string CPVRTimersPath::PATH_NEW = "pvr://timers/new/";
+const std::string CPVRTimersPath::PATH_TV_TIMERS = "pvr://timers/tv/timers/";
+const std::string CPVRTimersPath::PATH_RADIO_TIMERS = "pvr://timers/radio/timers/";
+const std::string CPVRTimersPath::PATH_TV_TIMER_RULES = "pvr://timers/tv/rules/";
+const std::string CPVRTimersPath::PATH_RADIO_TIMER_RULES = "pvr://timers/radio/rules/";
+
+CPVRTimersPath::CPVRTimersPath(const std::string& strPath)
+{
+ Init(strPath);
+}
+
+CPVRTimersPath::CPVRTimersPath(const std::string& strPath, int iClientId, int iParentId)
+{
+ if (Init(strPath))
+ {
+ // set/replace client and parent id.
+ m_path = StringUtils::Format("pvr://timers/{}/{}/{}/{}", m_bRadio ? "radio" : "tv",
+ m_bTimerRules ? "rules" : "timers", iClientId, iParentId);
+ m_iClientId = iClientId;
+ m_iParentId = iParentId;
+ m_bRoot = false;
+ }
+}
+
+CPVRTimersPath::CPVRTimersPath(bool bRadio, bool bTimerRules)
+ : m_path(StringUtils::Format(
+ "pvr://timers/{}/{}", bRadio ? "radio" : "tv", bTimerRules ? "rules" : "timers")),
+ m_bValid(true),
+ m_bRoot(true),
+ m_bRadio(bRadio),
+ m_bTimerRules(bTimerRules)
+{
+}
+
+bool CPVRTimersPath::Init(const std::string& strPath)
+{
+ std::string strVarPath(strPath);
+ URIUtils::RemoveSlashAtEnd(strVarPath);
+
+ m_path = strVarPath;
+ const std::vector<std::string> segments = URIUtils::SplitPath(m_path);
+
+ m_bValid = (((segments.size() == 4) || (segments.size() == 6)) && (segments.at(1) == "timers") &&
+ ((segments.at(2) == "radio") || (segments.at(2) == "tv")) &&
+ ((segments.at(3) == "rules") || (segments.at(3) == "timers")));
+ m_bRoot = (m_bValid && (segments.size() == 4));
+ m_bRadio = (m_bValid && (segments.at(2) == "radio"));
+ m_bTimerRules = (m_bValid && (segments.at(3) == "rules"));
+
+ if (!m_bValid || m_bRoot)
+ {
+ m_iClientId = -1;
+ m_iParentId = 0;
+ }
+ else
+ {
+ m_iClientId = std::stoi(segments.at(4));
+ m_iParentId = std::stoi(segments.at(5));
+ }
+
+ return m_bValid;
+}
diff --git a/xbmc/pvr/timers/PVRTimersPath.h b/xbmc/pvr/timers/PVRTimersPath.h
new file mode 100644
index 0000000..01fd5f0
--- /dev/null
+++ b/xbmc/pvr/timers/PVRTimersPath.h
@@ -0,0 +1,50 @@
+/*
+ * 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 <string>
+
+namespace PVR
+{
+class CPVRTimersPath
+{
+public:
+ static const std::string PATH_ADDTIMER;
+ static const std::string PATH_NEW;
+ static const std::string PATH_TV_TIMERS;
+ static const std::string PATH_TV_TIMER_RULES;
+ static const std::string PATH_RADIO_TIMERS;
+ static const std::string PATH_RADIO_TIMER_RULES;
+
+ explicit CPVRTimersPath(const std::string& strPath);
+ CPVRTimersPath(const std::string& strPath, int iClientId, int iParentId);
+ CPVRTimersPath(bool bRadio, bool bTimerRules);
+
+ bool IsValid() const { return m_bValid; }
+
+ const std::string& GetPath() const { return m_path; }
+ bool IsTimersRoot() const { return m_bRoot; }
+ bool IsTimerRule() const { return !IsTimersRoot(); }
+ bool IsRadio() const { return m_bRadio; }
+ bool IsRules() const { return m_bTimerRules; }
+ int GetClientId() const { return m_iClientId; }
+ int GetParentId() const { return m_iParentId; }
+
+private:
+ bool Init(const std::string& strPath);
+
+ std::string m_path;
+ bool m_bValid = false;
+ bool m_bRoot = false;
+ bool m_bRadio = false;
+ bool m_bTimerRules = false;
+ int m_iClientId = -1;
+ int m_iParentId = 0;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/CMakeLists.txt b/xbmc/pvr/windows/CMakeLists.txt
new file mode 100644
index 0000000..9f5097e
--- /dev/null
+++ b/xbmc/pvr/windows/CMakeLists.txt
@@ -0,0 +1,21 @@
+set(SOURCES GUIViewStatePVR.cpp
+ GUIWindowPVRBase.cpp
+ GUIWindowPVRChannels.cpp
+ GUIWindowPVRGuide.cpp
+ GUIWindowPVRRecordings.cpp
+ GUIWindowPVRSearch.cpp
+ GUIWindowPVRTimers.cpp
+ GUIWindowPVRTimersBase.cpp
+ GUIWindowPVRTimerRules.cpp)
+
+set(HEADERS GUIViewStatePVR.h
+ GUIWindowPVRBase.h
+ GUIWindowPVRChannels.h
+ GUIWindowPVRGuide.h
+ GUIWindowPVRRecordings.h
+ GUIWindowPVRSearch.h
+ GUIWindowPVRTimerRules.h
+ GUIWindowPVRTimers.h
+ GUIWindowPVRTimersBase.h)
+
+core_add_library(pvr_windows)
diff --git a/xbmc/pvr/windows/GUIViewStatePVR.cpp b/xbmc/pvr/windows/GUIViewStatePVR.cpp
new file mode 100644
index 0000000..d86f6fc
--- /dev/null
+++ b/xbmc/pvr/windows/GUIViewStatePVR.cpp
@@ -0,0 +1,182 @@
+/*
+ * 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 "GUIViewStatePVR.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "pvr/PVRManager.h"
+#include "pvr/addons/PVRClients.h"
+#include "pvr/epg/EpgSearchPath.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "view/ViewStateSettings.h"
+
+using namespace PVR;
+
+CGUIViewStateWindowPVRChannels::CGUIViewStateWindowPVRChannels(const int windowId,
+ const CFileItemList& items)
+ : CGUIViewStatePVR(windowId, items)
+{
+ AddSortMethod(SortByChannelNumber, 549, // "Number"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByChannel, 551, // "Name"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(
+ SortByLastPlayed, 568, // "Last played"
+ LABEL_MASKS("%L", "%p", "%L", "%p")); // Filename, LastPlayed | Foldername, LastPlayed
+ AddSortMethod(SortByClientChannelOrder, 19315, // "Backend number"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByProvider, 19348, // "Provider"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+
+ // Default sorting
+ SetSortMethod(SortByChannelNumber);
+
+ LoadViewState("pvr://channels/", m_windowId);
+}
+
+void CGUIViewStateWindowPVRChannels::SaveViewState()
+{
+ SaveViewToDb("pvr://channels/", m_windowId, CViewStateSettings::GetInstance().Get("pvrchannels"));
+}
+
+CGUIViewStateWindowPVRRecordings::CGUIViewStateWindowPVRRecordings(const int windowId,
+ const CFileItemList& items)
+ : CGUIViewStatePVR(windowId, items)
+{
+ AddSortMethod(SortByLabel, 551, // "Name"
+ LABEL_MASKS("%L", "%d", "%L", ""), // Filename, DateTime | Foldername, empty
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)
+ ? SortAttributeIgnoreArticle
+ : SortAttributeNone);
+ AddSortMethod(SortByDate, 552, // "Date"
+ LABEL_MASKS("%L", "%d", "%L", "%d")); // Filename, DateTime | Foldername, DateTime
+ AddSortMethod(SortByTime, 180, // "Duration"
+ LABEL_MASKS("%L", "%D", "%L", "")); // Filename, Duration | Foldername, empty
+ AddSortMethod(SortByFile, 561, // "File"
+ LABEL_MASKS("%L", "%d", "%L", "")); // Filename, DateTime | Foldername, empty
+
+ if (CServiceBroker::GetPVRManager().Clients()->AnyClientSupportingRecordingsSize())
+ {
+ // "Size" : Filename, Size | Foldername, Size
+ AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I"));
+ }
+
+ AddSortMethod(SortByEpisodeNumber, 20359, // "Episode"
+ LABEL_MASKS("%L", "%d", "%L", "")); // Filename, DateTime | Foldername, empty
+ AddSortMethod(SortByProvider, 19348, // "Provider"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+
+ SetSortMethod(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_PVRDefaultSortOrder);
+
+ LoadViewState(items.GetPath(), m_windowId);
+}
+
+void CGUIViewStateWindowPVRRecordings::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), m_windowId,
+ CViewStateSettings::GetInstance().Get("pvrrecordings"));
+}
+
+bool CGUIViewStateWindowPVRRecordings::HideParentDirItems()
+{
+ return (CGUIViewState::HideParentDirItems() ||
+ CPVRRecordingsPath(m_items.GetPath()).IsRecordingsRoot());
+}
+
+CGUIViewStateWindowPVRGuide::CGUIViewStateWindowPVRGuide(const int windowId,
+ const CFileItemList& items)
+ : CGUIViewStatePVR(windowId, items)
+{
+ AddSortMethod(SortByChannelNumber, 549, // "Number"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByChannel, 551, // "Name"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(
+ SortByLastPlayed, SortAttributeIgnoreLabel, 568, // "Last played"
+ LABEL_MASKS("%L", "%p", "%L", "%p")); // Filename, LastPlayed | Foldername, LastPlayed
+ AddSortMethod(SortByClientChannelOrder, 19315, // "Backend number"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByProvider, 19348, // "Provider"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+
+ // Default sorting
+ SetSortMethod(SortByChannelNumber);
+
+ LoadViewState("pvr://guide/", m_windowId);
+}
+
+void CGUIViewStateWindowPVRGuide::SaveViewState()
+{
+ SaveViewToDb("pvr://guide/", m_windowId, CViewStateSettings::GetInstance().Get("pvrguide"));
+}
+
+CGUIViewStateWindowPVRTimers::CGUIViewStateWindowPVRTimers(const int windowId,
+ const CFileItemList& items)
+ : CGUIViewStatePVR(windowId, items)
+{
+ int sortAttributes(CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING)
+ ? SortAttributeIgnoreArticle
+ : SortAttributeNone);
+ sortAttributes |= SortAttributeIgnoreFolders;
+ AddSortMethod(SortByLabel, static_cast<SortAttribute>(sortAttributes), 551, // "Name"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByDate, static_cast<SortAttribute>(sortAttributes), 552, // "Date"
+ LABEL_MASKS("%L", "%d", "%L", "%d")); // Filename, DateTime | Foldername, DateTime
+
+ // Default sorting
+ SetSortMethod(SortByDate);
+
+ LoadViewState("pvr://timers/", m_windowId);
+}
+
+void CGUIViewStateWindowPVRTimers::SaveViewState()
+{
+ SaveViewToDb("pvr://timers/", m_windowId, CViewStateSettings::GetInstance().Get("pvrtimers"));
+}
+
+bool CGUIViewStateWindowPVRTimers::HideParentDirItems()
+{
+ return (CGUIViewState::HideParentDirItems() || CPVRTimersPath(m_items.GetPath()).IsTimersRoot());
+}
+
+CGUIViewStateWindowPVRSearch::CGUIViewStateWindowPVRSearch(const int windowId,
+ const CFileItemList& items)
+ : CGUIViewStatePVR(windowId, items)
+{
+ AddSortMethod(SortByLabel, 551, // "Name"
+ LABEL_MASKS("%L", "", "%L", "")); // Filename, empty | Foldername, empty
+ AddSortMethod(SortByDate, 552, // "Date"
+ LABEL_MASKS("%L", "%d", "%L", "%d")); // Filename, DateTime | Foldername, DateTime
+
+ // Default sorting
+ if (CPVREpgSearchPath(m_items.GetPath()).IsSavedSearchesRoot())
+ SetSortMethod(SortByDate, SortOrderDescending);
+ else
+ SetSortMethod(SortByDate, SortOrderAscending);
+
+ LoadViewState(m_items.GetPath(), m_windowId);
+}
+
+void CGUIViewStateWindowPVRSearch::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), m_windowId, CViewStateSettings::GetInstance().Get("pvrsearch"));
+}
+
+bool CGUIViewStateWindowPVRSearch::HideParentDirItems()
+{
+ return (CGUIViewState::HideParentDirItems() ||
+ CPVREpgSearchPath(m_items.GetPath()).IsSearchRoot());
+}
diff --git a/xbmc/pvr/windows/GUIViewStatePVR.h b/xbmc/pvr/windows/GUIViewStatePVR.h
new file mode 100644
index 0000000..ce39dd7
--- /dev/null
+++ b/xbmc/pvr/windows/GUIViewStatePVR.h
@@ -0,0 +1,78 @@
+/*
+ * 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 "view/GUIViewState.h"
+
+class CFileItemList;
+
+namespace PVR
+{
+class CGUIViewStatePVR : public CGUIViewState
+{
+public:
+ CGUIViewStatePVR(const int windowId, const CFileItemList& items) : CGUIViewState(items)
+ {
+ m_windowId = windowId;
+ }
+
+protected:
+ bool HideParentDirItems() override { return true; }
+
+ int m_windowId;
+};
+
+class CGUIViewStateWindowPVRChannels : public CGUIViewStatePVR
+{
+public:
+ CGUIViewStateWindowPVRChannels(const int windowId, const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateWindowPVRRecordings : public CGUIViewStatePVR
+{
+public:
+ CGUIViewStateWindowPVRRecordings(const int windowId, const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ bool HideParentDirItems() override;
+};
+
+class CGUIViewStateWindowPVRGuide : public CGUIViewStatePVR
+{
+public:
+ CGUIViewStateWindowPVRGuide(const int windowId, const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateWindowPVRTimers : public CGUIViewStatePVR
+{
+public:
+ CGUIViewStateWindowPVRTimers(const int windowId, const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ bool HideParentDirItems() override;
+};
+
+class CGUIViewStateWindowPVRSearch : public CGUIViewStatePVR
+{
+public:
+ CGUIViewStateWindowPVRSearch(const int windowId, const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ bool HideParentDirItems() override;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/GUIWindowPVRBase.cpp b/xbmc/pvr/windows/GUIWindowPVRBase.cpp
new file mode 100644
index 0000000..96a14b5
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRBase.cpp
@@ -0,0 +1,563 @@
+/*
+ * 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 "GUIWindowPVRBase.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroups.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/filesystem/PVRGUIDirectory.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <iterator>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace std::chrono_literals;
+
+#define MAX_INVALIDATION_FREQUENCY 2000ms // limit to one invalidation per X milliseconds
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+namespace PVR
+{
+
+class CGUIPVRChannelGroupsSelector
+{
+public:
+ virtual ~CGUIPVRChannelGroupsSelector() = default;
+
+ bool Initialize(CGUIWindow* parent, bool bRadio);
+
+ bool HasFocus() const;
+ std::shared_ptr<CPVRChannelGroup> GetSelectedChannelGroup() const;
+ bool SelectChannelGroup(const std::shared_ptr<CPVRChannelGroup>& newGroup);
+
+private:
+ CGUIControl* m_control = nullptr;
+ std::vector<std::shared_ptr<CPVRChannelGroup>> m_channelGroups;
+};
+
+} // namespace PVR
+
+bool CGUIPVRChannelGroupsSelector::Initialize(CGUIWindow* parent, bool bRadio)
+{
+ CGUIControl* control = parent->GetControl(CONTROL_LSTCHANNELGROUPS);
+ if (control && control->IsContainer())
+ {
+ m_control = control;
+ m_channelGroups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio)->GetMembers(true);
+
+ CFileItemList channelGroupItems;
+ CPVRGUIDirectory::GetChannelGroupsDirectory(bRadio, true, channelGroupItems);
+
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, m_control->GetID(), CONTROL_LSTCHANNELGROUPS, 0, 0, &channelGroupItems);
+ m_control->OnMessage(msg);
+ return true;
+ }
+ return false;
+}
+
+bool CGUIPVRChannelGroupsSelector::HasFocus() const
+{
+ return m_control && m_control->HasFocus();
+}
+
+std::shared_ptr<CPVRChannelGroup> CGUIPVRChannelGroupsSelector::GetSelectedChannelGroup() const
+{
+ if (m_control)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, m_control->GetID(), CONTROL_LSTCHANNELGROUPS);
+ m_control->OnMessage(msg);
+
+ const auto it = std::next(m_channelGroups.begin(), msg.GetParam1());
+ if (it != m_channelGroups.end())
+ {
+ return *it;
+ }
+ }
+ return std::shared_ptr<CPVRChannelGroup>();
+}
+
+bool CGUIPVRChannelGroupsSelector::SelectChannelGroup(const std::shared_ptr<CPVRChannelGroup>& newGroup)
+{
+ if (m_control && newGroup)
+ {
+ int iIndex = 0;
+ for (const auto& group : m_channelGroups)
+ {
+ if (*newGroup == *group)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, m_control->GetID(), CONTROL_LSTCHANNELGROUPS, iIndex);
+ m_control->OnMessage(msg);
+ return true;
+ }
+ ++iIndex;
+ }
+ }
+ return false;
+}
+
+CGUIWindowPVRBase::CGUIWindowPVRBase(bool bRadio, int id, const std::string& xmlFile) :
+ CGUIMediaWindow(id, xmlFile.c_str()),
+ m_bRadio(bRadio),
+ m_channelGroupsSelector(new CGUIPVRChannelGroupsSelector),
+ m_progressHandle(nullptr)
+{
+ // prevent removable drives to appear in directory listing (base class default behavior).
+ m_rootDir.AllowNonLocalSources(false);
+
+ CServiceBroker::GetPVRManager().Events().Subscribe(this, &CGUIWindowPVRBase::Notify);
+}
+
+CGUIWindowPVRBase::~CGUIWindowPVRBase()
+{
+ if (m_channelGroup)
+ m_channelGroup->Events().Unsubscribe(this);
+
+ CServiceBroker::GetPVRManager().Events().Unsubscribe(this);
+}
+
+void CGUIWindowPVRBase::UpdateSelectedItemPath()
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().SetSelectedChannelPath(
+ m_bRadio, m_viewControl.GetSelectedItemPath());
+}
+
+void CGUIWindowPVRBase::Notify(const PVREvent& event)
+{
+ // call virtual event handler function
+ NotifyEvent(event);
+}
+
+void CGUIWindowPVRBase::NotifyEvent(const PVREvent& event)
+{
+ if (event == PVREvent::ManagerStopped)
+ {
+ ClearData();
+ }
+ else if (m_active)
+ {
+ if (event == PVREvent::SystemSleep)
+ {
+ CGUIMessage m(GUI_MSG_SYSTEM_SLEEP, GetID(), 0, static_cast<int>(event));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+ }
+ else if (event == PVREvent::SystemWake)
+ {
+ CGUIMessage m(GUI_MSG_SYSTEM_WAKE, GetID(), 0, static_cast<int>(event));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+ }
+ else
+ {
+ CGUIMessage m(GUI_MSG_REFRESH_LIST, GetID(), 0, static_cast<int>(event));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+ }
+ }
+}
+
+bool CGUIWindowPVRBase::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_PREVIOUS_CHANNELGROUP:
+ ActivatePreviousChannelGroup();
+ return true;
+
+ case ACTION_NEXT_CHANNELGROUP:
+ ActivateNextChannelGroup();
+ return true;
+
+ case ACTION_MOVE_RIGHT:
+ case ACTION_MOVE_LEFT:
+ {
+ if (m_channelGroupsSelector->HasFocus() && CGUIMediaWindow::OnAction(action))
+ {
+ SetChannelGroup(m_channelGroupsSelector->GetSelectedChannelGroup());
+ return true;
+ }
+ }
+ }
+
+ return CGUIMediaWindow::OnAction(action);
+}
+
+bool CGUIWindowPVRBase::ActivatePreviousChannelGroup()
+{
+ const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup();
+ if (channelGroup)
+ {
+ const CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(channelGroup->IsRadio());
+ if (groups)
+ {
+ SetChannelGroup(groups->GetPreviousGroup(*channelGroup));
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIWindowPVRBase::ActivateNextChannelGroup()
+{
+ const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup();
+ if (channelGroup)
+ {
+ const CPVRChannelGroups* groups = CServiceBroker::GetPVRManager().ChannelGroups()->Get(channelGroup->IsRadio());
+ if (groups)
+ {
+ SetChannelGroup(groups->GetNextGroup(*channelGroup));
+ return true;
+ }
+ }
+ return false;
+}
+
+void CGUIWindowPVRBase::ClearData()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_channelGroup.reset();
+ m_channelGroupsSelector.reset(new CGUIPVRChannelGroupsSelector);
+}
+
+void CGUIWindowPVRBase::OnInitWindow()
+{
+ SetProperty("IsRadio", m_bRadio ? "true" : "");
+
+ if (InitChannelGroup())
+ {
+ m_channelGroupsSelector->Initialize(this, m_bRadio);
+
+ CGUIMediaWindow::OnInitWindow();
+
+ // mark item as selected by channel path
+ m_viewControl.SetSelectedItem(
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetSelectedChannelPath(m_bRadio));
+
+ // This has to be done after base class OnInitWindow to restore correct selection
+ m_channelGroupsSelector->SelectChannelGroup(GetChannelGroup());
+ }
+ else
+ {
+ CGUIWindow::OnInitWindow(); // do not call CGUIMediaWindow as it will do a Refresh which in no case works in this state (no channelgroup!)
+ ShowProgressDialog(g_localizeStrings.Get(19235), 0); // PVR manager is starting up
+ }
+}
+
+void CGUIWindowPVRBase::OnDeinitWindow(int nextWindowID)
+{
+ HideProgressDialog();
+ UpdateSelectedItemPath();
+ CGUIMediaWindow::OnDeinitWindow(nextWindowID);
+}
+
+bool CGUIWindowPVRBase::OnMessage(CGUIMessage& message)
+{
+ bool bReturn = false;
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ switch (message.GetSenderId())
+ {
+ case CONTROL_BTNCHANNELGROUPS:
+ return OpenChannelGroupSelectionDialog();
+
+ case CONTROL_LSTCHANNELGROUPS:
+ {
+ switch (message.GetParam1())
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ SetChannelGroup(m_channelGroupsSelector->GetSelectedChannelGroup());
+ bReturn = true;
+ break;
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ case GUI_MSG_REFRESH_LIST:
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::ManagerStarted:
+ case PVREvent::ClientsInvalidated:
+ case PVREvent::ChannelGroupsInvalidated:
+ {
+ if (InitChannelGroup())
+ {
+ m_channelGroupsSelector->Initialize(this, m_bRadio);
+ m_channelGroupsSelector->SelectChannelGroup(GetChannelGroup());
+ HideProgressDialog();
+ Refresh(true);
+ m_viewControl.SetFocused();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (IsActive())
+ {
+ // Only the active window must set the selected item path which is shared
+ // between all PVR windows, not the last notified window (observer).
+ UpdateSelectedItemPath();
+ }
+ bReturn = true;
+ break;
+ }
+
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ switch (message.GetParam1())
+ {
+ case GUI_MSG_UPDATE_SOURCES:
+ {
+ // removable drive connected/disconnected. base class triggers a window
+ // content refresh, which makes no sense for pvr windows.
+ bReturn = true;
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ return bReturn || CGUIMediaWindow::OnMessage(message);
+}
+
+void CGUIWindowPVRBase::SetInvalid()
+{
+ if (m_refreshTimeout.IsTimePast())
+ {
+ for (const auto& item : *m_vecItems)
+ item->SetInvalid();
+
+ CGUIMediaWindow::SetInvalid();
+ m_refreshTimeout.Set(MAX_INVALIDATION_FREQUENCY);
+ }
+}
+
+bool CGUIWindowPVRBase::CanBeActivated() const
+{
+ // check if there is at least one enabled PVR add-on
+ if (!CServiceBroker::GetAddonMgr().HasAddons(ADDON::AddonType::PVRDLL))
+ {
+ HELPERS::ShowOKDialogText(CVariant{19296}, CVariant{19272}); // No PVR add-on enabled, You need a tuner, backend software...
+ return false;
+ }
+
+ return true;
+}
+
+bool CGUIWindowPVRBase::OpenChannelGroupSelectionDialog()
+{
+ CGUIDialogSelect* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (!dialog)
+ return false;
+
+ CFileItemList options;
+ CPVRGUIDirectory::GetChannelGroupsDirectory(m_bRadio, true, options);
+
+ dialog->Reset();
+ dialog->SetHeading(CVariant{g_localizeStrings.Get(19146)});
+ dialog->SetItems(options);
+ dialog->SetMultiSelection(false);
+ if (const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup())
+ {
+ dialog->SetSelected(channelGroup->GroupName());
+ }
+ dialog->Open();
+
+ if (!dialog->IsConfirmed())
+ return false;
+
+ const CFileItemPtr item = dialog->GetSelectedFileItem();
+ if (!item)
+ return false;
+
+ SetChannelGroup(CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bRadio)->GetByName(item->m_strTitle));
+
+ return true;
+}
+
+bool CGUIWindowPVRBase::InitChannelGroup()
+{
+ if (!CServiceBroker::GetPVRManager().IsStarted())
+ return false;
+
+ std::shared_ptr<CPVRChannelGroup> group;
+ if (m_channelGroupPath.empty())
+ {
+ group = CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(m_bRadio);
+ }
+ else
+ {
+ group = CServiceBroker::GetPVRManager().ChannelGroups()->Get(m_bRadio)->GetGroupByPath(m_channelGroupPath);
+ if (group)
+ CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(group);
+ else
+ CLog::LogF(LOGERROR, "Found no {} channel group with path '{}'!", m_bRadio ? "radio" : "TV",
+ m_channelGroupPath);
+
+ m_channelGroupPath.clear();
+ }
+
+ if (group)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_channelGroup != group)
+ {
+ m_viewControl.SetSelectedItem(0);
+ SetChannelGroup(std::move(group), false);
+ }
+ // Path might have changed since last init. Set it always, not just on group change.
+ m_vecItems->SetPath(GetDirectoryPath());
+ return true;
+ }
+ return false;
+}
+
+std::shared_ptr<CPVRChannelGroup> CGUIWindowPVRBase::GetChannelGroup()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_channelGroup;
+}
+
+void CGUIWindowPVRBase::SetChannelGroup(std::shared_ptr<CPVRChannelGroup> &&group, bool bUpdate /* = true */)
+{
+ if (!group)
+ return;
+
+ std::shared_ptr<CPVRChannelGroup> updateChannelGroup;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_channelGroup != group)
+ {
+ if (m_channelGroup)
+ m_channelGroup->Events().Unsubscribe(this);
+ m_channelGroup = std::move(group);
+ // we need to register the window to receive changes from the new group
+ m_channelGroup->Events().Subscribe(this, &CGUIWindowPVRBase::Notify);
+ if (bUpdate)
+ updateChannelGroup = m_channelGroup;
+ }
+ }
+
+ if (updateChannelGroup)
+ {
+ CServiceBroker::GetPVRManager().PlaybackState()->SetActiveChannelGroup(updateChannelGroup);
+ Update(GetDirectoryPath());
+ }
+}
+
+bool CGUIWindowPVRBase::Update(const std::string& strDirectory, bool updateFilterPath /*= true*/)
+{
+ if (m_bUpdating)
+ {
+ // no concurrent updates
+ return false;
+ }
+
+ CUpdateGuard guard(m_bUpdating);
+
+ if (!GetChannelGroup())
+ {
+ // no updates before fully initialized
+ return false;
+ }
+
+ int iOldCount = m_vecItems->Size();
+ int iSelectedItem = m_viewControl.GetSelectedItem();
+ const std::string oldPath = m_vecItems->GetPath();
+
+ bool bReturn = CGUIMediaWindow::Update(strDirectory, updateFilterPath);
+
+ if (bReturn &&
+ iSelectedItem != -1) // something must have been selected
+ {
+ int iNewCount = m_vecItems->Size();
+ if (iOldCount > iNewCount && // at least one item removed by Update()
+ oldPath == m_vecItems->GetPath()) // update not due changing into another folder
+ {
+ // restore selected item if we just deleted one or more items.
+ if (iSelectedItem >= iNewCount)
+ iSelectedItem = iNewCount - 1;
+
+ m_viewControl.SetSelectedItem(iSelectedItem);
+ }
+ }
+
+ return bReturn;
+}
+
+void CGUIWindowPVRBase::UpdateButtons()
+{
+ CGUIMediaWindow::UpdateButtons();
+
+ const std::shared_ptr<CPVRChannelGroup> channelGroup = GetChannelGroup();
+ if (channelGroup)
+ {
+ SET_CONTROL_LABEL(CONTROL_BTNCHANNELGROUPS, g_localizeStrings.Get(19141) + ": " + channelGroup->GroupName());
+ }
+
+ m_channelGroupsSelector->SelectChannelGroup(channelGroup);
+}
+
+void CGUIWindowPVRBase::ShowProgressDialog(const std::string& strText, int iProgress)
+{
+ if (!m_progressHandle)
+ {
+ CGUIDialogExtendedProgressBar* loadingProgressDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
+ if (!loadingProgressDialog)
+ {
+ CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_EXT_PROGRESS!");
+ return;
+ }
+ m_progressHandle = loadingProgressDialog->GetHandle(g_localizeStrings.Get(19235)); // PVR manager is starting up
+ }
+
+ m_progressHandle->SetPercentage(static_cast<float>(iProgress));
+ m_progressHandle->SetText(strText);
+}
+
+void CGUIWindowPVRBase::HideProgressDialog()
+{
+ if (m_progressHandle)
+ {
+ m_progressHandle->MarkFinished();
+ m_progressHandle = nullptr;
+ }
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRBase.h b/xbmc/pvr/windows/GUIWindowPVRBase.h
new file mode 100644
index 0000000..1fe27bb
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRBase.h
@@ -0,0 +1,138 @@
+/*
+ * 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/SystemClock.h"
+#include "windows/GUIMediaWindow.h"
+
+#include <atomic>
+#include <memory>
+#include <string>
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_BTNGROUPITEMS 5
+#define CONTROL_BTNSHOWHIDDEN 6
+#define CONTROL_BTNSHOWDELETED 7
+#define CONTROL_BTNHIDEDISABLEDTIMERS 8
+#define CONTROL_BTNSHOWMODE 10
+#define CONTROL_LSTCHANNELGROUPS 11
+
+#define CONTROL_BTNCHANNELGROUPS 28
+#define CONTROL_BTNFILTERCHANNELS 31
+
+#define CONTROL_LABEL_HEADER1 29
+#define CONTROL_LABEL_HEADER2 30
+
+class CGUIDialogProgressBarHandle;
+
+namespace PVR
+{
+ enum class PVREvent;
+
+ enum EPGSelectAction
+ {
+ EPG_SELECT_ACTION_CONTEXT_MENU = 0,
+ EPG_SELECT_ACTION_SWITCH = 1,
+ EPG_SELECT_ACTION_INFO = 2,
+ EPG_SELECT_ACTION_RECORD = 3,
+ EPG_SELECT_ACTION_PLAY_RECORDING = 4,
+ EPG_SELECT_ACTION_SMART_SELECT = 5
+ };
+
+ class CPVRChannelGroup;
+ class CGUIPVRChannelGroupsSelector;
+
+ class CGUIWindowPVRBase : public CGUIMediaWindow
+ {
+ public:
+ ~CGUIWindowPVRBase() override;
+
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void UpdateButtons() override;
+ bool OnAction(const CAction& action) override;
+ void SetInvalid() override;
+ bool CanBeActivated() const override;
+
+ bool UseFileDirectories() override { return false; }
+
+ /*!
+ * @brief CEventStream callback for PVR events.
+ * @param event The event.
+ */
+ void Notify(const PVREvent& event);
+ virtual void NotifyEvent(const PVREvent& event);
+
+ /*!
+ * @brief Refresh window content.
+ * @return true, if refresh succeeded, false otherwise.
+ */
+ bool DoRefresh() { return Refresh(true); }
+
+ bool ActivatePreviousChannelGroup();
+ bool ActivateNextChannelGroup();
+ bool OpenChannelGroupSelectionDialog();
+
+ protected:
+ CGUIWindowPVRBase(bool bRadio, int id, const std::string& xmlFile);
+
+ virtual std::string GetDirectoryPath() = 0;
+
+ virtual void ClearData();
+
+ /*!
+ * @brief Init this window's channel group with the currently active (the "playing") channel group.
+ * @return true if group could be set, false otherwise.
+ */
+ bool InitChannelGroup();
+
+ /*!
+ * @brief Get the channel group for this window.
+ * @return the group or null, if no group set.
+ */
+ std::shared_ptr<CPVRChannelGroup> GetChannelGroup();
+
+ /*!
+ * @brief Set a new channel group, start listening to this group, optionally update window content.
+ * @param group The new group.
+ * @param bUpdate if true, window content will be updated.
+ */
+ void SetChannelGroup(std::shared_ptr<CPVRChannelGroup> &&group, bool bUpdate = true);
+
+ virtual void UpdateSelectedItemPath();
+
+ CCriticalSection m_critSection;
+ std::string m_channelGroupPath;
+ bool m_bRadio;
+ std::atomic_bool m_bUpdating = {false};
+
+ private:
+ /*!
+ * @brief Show or update the progress dialog.
+ * @param strText The current status.
+ * @param iProgress The current progress in %.
+ */
+ void ShowProgressDialog(const std::string& strText, int iProgress);
+
+ /*!
+ * @brief Hide the progress dialog if it's visible.
+ */
+ void HideProgressDialog();
+
+ std::unique_ptr<CGUIPVRChannelGroupsSelector> m_channelGroupsSelector;
+ std::shared_ptr<CPVRChannelGroup> m_channelGroup;
+ XbmcThreads::EndTime<> m_refreshTimeout;
+ CGUIDialogProgressBarHandle* m_progressHandle; /*!< progress dialog that is displayed while the pvr manager is loading */
+ };
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRChannels.cpp b/xbmc/pvr/windows/GUIWindowPVRChannels.cpp
new file mode 100644
index 0000000..2b476f7
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRChannels.cpp
@@ -0,0 +1,414 @@
+/*
+ * 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 "GUIWindowPVRChannels.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIRadioButtonControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "input/actions/Action.h"
+#include "pvr/PVRManager.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "pvr/dialogs/GUIDialogPVRChannelManager.h"
+#include "pvr/dialogs/GUIDialogPVRGroupManager.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <mutex>
+#include <string>
+
+using namespace PVR;
+
+CGUIWindowPVRChannelsBase::CGUIWindowPVRChannelsBase(bool bRadio,
+ int id,
+ const std::string& xmlFile)
+ : CGUIWindowPVRBase(bRadio, id, xmlFile), m_bShowHiddenChannels(false)
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().RegisterChannelNumberInputHandler(this);
+}
+
+CGUIWindowPVRChannelsBase::~CGUIWindowPVRChannelsBase()
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().DeregisterChannelNumberInputHandler(
+ this);
+}
+
+std::string CGUIWindowPVRChannelsBase::GetRootPath() const
+{
+ //! @todo Would it make sense to change GetRootPath() declaration in CGUIMediaWindow
+ //! to be non-const to get rid of the const_cast's here?
+
+ CGUIWindowPVRChannelsBase* pThis = const_cast<CGUIWindowPVRChannelsBase*>(this);
+ if (pThis->InitChannelGroup())
+ return pThis->GetDirectoryPath();
+
+ return CGUIWindowPVRBase::GetRootPath();
+}
+
+void CGUIWindowPVRChannelsBase::GetContextButtons(int itemNumber, CContextButtons& buttons)
+{
+ // Add parent buttons before the Manage button
+ CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons);
+
+ buttons.Add(CONTEXT_BUTTON_EDIT, 16106); /* Manage... */
+}
+
+bool CGUIWindowPVRChannelsBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return false;
+
+ return OnContextButtonManage(m_vecItems->Get(itemNumber), button) ||
+ CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowPVRChannelsBase::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ bool bReturn = CGUIWindowPVRBase::Update(strDirectory);
+
+ if (bReturn)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ /* empty list for hidden channels */
+ if (m_vecItems->GetObjectCount() == 0 && m_bShowHiddenChannels)
+ {
+ /* show the visible channels instead */
+ m_bShowHiddenChannels = false;
+ lock.unlock();
+ Update(GetDirectoryPath());
+ }
+ }
+ return bReturn;
+}
+
+void CGUIWindowPVRChannelsBase::UpdateButtons()
+{
+ CGUIRadioButtonControl* btnShowHidden =
+ static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWHIDDEN));
+ if (btnShowHidden)
+ {
+ btnShowHidden->SetVisible(CServiceBroker::GetPVRManager()
+ .ChannelGroups()
+ ->GetGroupAll(m_bRadio)
+ ->GetNumHiddenChannels() > 0);
+ btnShowHidden->SetSelected(m_bShowHiddenChannels);
+ }
+
+ CGUIWindowPVRBase::UpdateButtons();
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, m_bShowHiddenChannels ? g_localizeStrings.Get(19022)
+ : GetChannelGroup()->GroupName());
+}
+
+bool CGUIWindowPVRChannelsBase::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case REMOTE_0:
+ case REMOTE_1:
+ case REMOTE_2:
+ case REMOTE_3:
+ case REMOTE_4:
+ case REMOTE_5:
+ case REMOTE_6:
+ case REMOTE_7:
+ case REMOTE_8:
+ case REMOTE_9:
+ AppendChannelNumberCharacter(static_cast<char>(action.GetID() - REMOTE_0) + '0');
+ return true;
+
+ case ACTION_CHANNEL_NUMBER_SEP:
+ AppendChannelNumberCharacter(CPVRChannelNumber::SEPARATOR);
+ return true;
+ }
+
+ return CGUIWindowPVRBase::OnAction(action);
+}
+
+bool CGUIWindowPVRChannelsBase::OnMessage(CGUIMessage& message)
+{
+ bool bReturn = false;
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ const CPVRChannelsPath path(message.GetStringParam(0));
+ if (path.IsValid() && path.IsChannelGroup())
+ {
+ // if a path to a channel group is given we must init
+ // that group instead of last played/selected group
+ m_channelGroupPath = message.GetStringParam(0);
+ }
+ break;
+ }
+
+ case GUI_MSG_CLICKED:
+ if (message.GetSenderId() == m_viewControl.GetCurrentControl())
+ {
+ if (message.GetParam1() == ACTION_SELECT_ITEM ||
+ message.GetParam1() == ACTION_MOUSE_LEFT_CLICK)
+ {
+ // If direct channel number input is active, select the entered channel.
+ if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .CheckInputAndExecuteAction())
+ {
+ bReturn = true;
+ break;
+ }
+ }
+
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ bReturn = true;
+ switch (message.GetParam1())
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ case ACTION_PLAYER_PLAY:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ *(m_vecItems->Get(iItem)), true);
+ break;
+ case ACTION_SHOW_INFO:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(
+ *(m_vecItems->Get(iItem)));
+ break;
+ case ACTION_DELETE_ITEM:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().HideChannel(
+ *(m_vecItems->Get(iItem)));
+ break;
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnPopupMenu(iItem);
+ break;
+ default:
+ bReturn = false;
+ break;
+ }
+ }
+ }
+ else if (message.GetSenderId() == CONTROL_BTNSHOWHIDDEN)
+ {
+ CGUIRadioButtonControl* radioButton =
+ static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWHIDDEN));
+ if (radioButton)
+ {
+ m_bShowHiddenChannels = radioButton->IsSelected();
+ Update(GetDirectoryPath());
+ }
+
+ bReturn = true;
+ }
+ else if (message.GetSenderId() == CONTROL_BTNFILTERCHANNELS)
+ {
+ std::string filter = GetProperty("filter").asString();
+ CGUIKeyboardFactory::ShowAndGetFilter(filter, false);
+ OnFilterItems(filter);
+ UpdateButtons();
+
+ bReturn = true;
+ }
+ break;
+
+ case GUI_MSG_REFRESH_LIST:
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::ChannelGroup:
+ case PVREvent::CurrentItem:
+ case PVREvent::Epg:
+ case PVREvent::EpgActiveItem:
+ case PVREvent::EpgContainer:
+ case PVREvent::RecordingsInvalidated:
+ case PVREvent::Timers:
+ SetInvalid();
+ break;
+
+ case PVREvent::ChannelGroupInvalidated:
+ Refresh(true);
+ break;
+
+ default:
+ break;
+ }
+ break;
+ }
+ }
+
+ return bReturn || CGUIWindowPVRBase::OnMessage(message);
+}
+
+bool CGUIWindowPVRChannelsBase::OnContextButtonManage(const CFileItemPtr& item,
+ CONTEXT_BUTTON button)
+{
+ bool bReturn = false;
+
+ if (button == CONTEXT_BUTTON_EDIT)
+ {
+ // Create context sub menu
+ CContextButtons buttons;
+ buttons.Add(CONTEXT_BUTTON_GROUP_MANAGER, 19048);
+ buttons.Add(CONTEXT_BUTTON_CHANNEL_MANAGER, 19199);
+ buttons.Add(CONTEXT_BUTTON_UPDATE_EPG, 19251);
+
+ // Get the response
+ int choice = CGUIDialogContextMenu::ShowAndGetChoice(buttons);
+ if (choice >= 0)
+ {
+ switch (static_cast<CONTEXT_BUTTON>(choice))
+ {
+ case CONTEXT_BUTTON_GROUP_MANAGER:
+ ShowGroupManager();
+ break;
+ case CONTEXT_BUTTON_CHANNEL_MANAGER:
+ ShowChannelManager();
+ break;
+ case CONTEXT_BUTTON_UPDATE_EPG:
+ UpdateEpg(item);
+ break;
+ default:
+ break;
+ }
+
+ // Refresh the list in case anything was changed
+ Refresh(true);
+ }
+
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+void CGUIWindowPVRChannelsBase::UpdateEpg(const CFileItemPtr& item)
+{
+ const std::shared_ptr<CPVRChannel> channel(item->GetPVRChannelInfoTag());
+
+ if (!CGUIDialogYesNo::ShowAndGetInput(
+ CVariant{19251}, // "Update guide information"
+ CVariant{19252}, // "Schedule guide update for this channel?"
+ CVariant{""}, CVariant{channel->ChannelName()}))
+ return;
+
+ const std::shared_ptr<CPVREpg> epg = channel->GetEPG();
+ if (epg)
+ {
+ epg->ForceUpdate();
+
+ const std::string strMessage =
+ StringUtils::Format("{}: '{}'",
+ g_localizeStrings.Get(19253), // "Guide update scheduled for channel"
+ channel->ChannelName());
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info,
+ g_localizeStrings.Get(19166), // "PVR information"
+ strMessage);
+ }
+ else
+ {
+ const std::string strMessage =
+ StringUtils::Format("{}: '{}'",
+ g_localizeStrings.Get(19254), // "Guide update failed for channel"
+ channel->ChannelName());
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(19166), // "PVR information"
+ strMessage);
+ }
+}
+
+void CGUIWindowPVRChannelsBase::ShowChannelManager()
+{
+ CGUIDialogPVRChannelManager* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRChannelManager>(
+ WINDOW_DIALOG_PVR_CHANNEL_MANAGER);
+ if (!dialog)
+ return;
+
+ dialog->SetRadio(m_bRadio);
+
+ const int iItem = m_viewControl.GetSelectedItem();
+ dialog->Open(iItem >= 0 && iItem < m_vecItems->Size() ? m_vecItems->Get(iItem) : nullptr);
+}
+
+void CGUIWindowPVRChannelsBase::ShowGroupManager()
+{
+ /* Load group manager dialog */
+ CGUIDialogPVRGroupManager* pDlgInfo =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGroupManager>(
+ WINDOW_DIALOG_PVR_GROUP_MANAGER);
+ if (!pDlgInfo)
+ return;
+
+ pDlgInfo->SetRadio(m_bRadio);
+ pDlgInfo->Open();
+}
+
+void CGUIWindowPVRChannelsBase::OnInputDone()
+{
+ const CPVRChannelNumber channelNumber = GetChannelNumber();
+ if (channelNumber.IsValid())
+ {
+ int itemIndex = 0;
+ for (const CFileItemPtr& channel : *m_vecItems)
+ {
+ if (channel->GetPVRChannelGroupMemberInfoTag()->ChannelNumber() == channelNumber)
+ {
+ m_viewControl.SetSelectedItem(itemIndex);
+ return;
+ }
+ ++itemIndex;
+ }
+ }
+}
+
+void CGUIWindowPVRChannelsBase::GetChannelNumbers(std::vector<std::string>& channelNumbers)
+{
+ const std::shared_ptr<CPVRChannelGroup> group = GetChannelGroup();
+ if (group)
+ group->GetChannelNumbers(channelNumbers);
+}
+
+CGUIWindowPVRTVChannels::CGUIWindowPVRTVChannels()
+ : CGUIWindowPVRChannelsBase(false, WINDOW_TV_CHANNELS, "MyPVRChannels.xml")
+{
+}
+
+std::string CGUIWindowPVRTVChannels::GetDirectoryPath()
+{
+ return CPVRChannelsPath(false, m_bShowHiddenChannels, GetChannelGroup()->GroupName());
+}
+
+CGUIWindowPVRRadioChannels::CGUIWindowPVRRadioChannels()
+ : CGUIWindowPVRChannelsBase(true, WINDOW_RADIO_CHANNELS, "MyPVRChannels.xml")
+{
+}
+
+std::string CGUIWindowPVRRadioChannels::GetDirectoryPath()
+{
+ return CPVRChannelsPath(true, m_bShowHiddenChannels, GetChannelGroup()->GroupName());
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRChannels.h b/xbmc/pvr/windows/GUIWindowPVRChannels.h
new file mode 100644
index 0000000..ef8e848
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRChannels.h
@@ -0,0 +1,65 @@
+/*
+ * 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 "dialogs/GUIDialogContextMenu.h"
+#include "pvr/PVRChannelNumberInputHandler.h"
+#include "pvr/windows/GUIWindowPVRBase.h"
+
+#include <string>
+
+namespace PVR
+{
+class CGUIWindowPVRChannelsBase : public CGUIWindowPVRBase, public CPVRChannelNumberInputHandler
+{
+public:
+ CGUIWindowPVRChannelsBase(bool bRadio, int id, const std::string& xmlFile);
+ ~CGUIWindowPVRChannelsBase() override;
+
+ std::string GetRootPath() const override;
+ bool OnMessage(CGUIMessage& message) override;
+ void GetContextButtons(int itemNumber, CContextButtons& buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void UpdateButtons() override;
+ bool OnAction(const CAction& action) override;
+
+ // CPVRChannelNumberInputHandler implementation
+ void GetChannelNumbers(std::vector<std::string>& channelNumbers) override;
+ void OnInputDone() override;
+
+private:
+ bool OnContextButtonManage(const CFileItemPtr& item, CONTEXT_BUTTON button);
+
+ void ShowChannelManager();
+ void ShowGroupManager();
+ void UpdateEpg(const CFileItemPtr& item);
+
+protected:
+ bool m_bShowHiddenChannels;
+};
+
+class CGUIWindowPVRTVChannels : public CGUIWindowPVRChannelsBase
+{
+public:
+ CGUIWindowPVRTVChannels();
+
+protected:
+ std::string GetDirectoryPath() override;
+};
+
+class CGUIWindowPVRRadioChannels : public CGUIWindowPVRChannelsBase
+{
+public:
+ CGUIWindowPVRRadioChannels();
+
+protected:
+ std::string GetDirectoryPath() override;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/GUIWindowPVRGuide.cpp b/xbmc/pvr/windows/GUIWindowPVRGuide.cpp
new file mode 100644
index 0000000..f4c247f
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRGuide.cpp
@@ -0,0 +1,973 @@
+/*
+ * 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 "GUIWindowPVRGuide.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/PVRPlaybackState.h"
+#include "pvr/channels/PVRChannel.h"
+#include "pvr/channels/PVRChannelGroup.h"
+#include "pvr/channels/PVRChannelGroupMember.h"
+#include "pvr/channels/PVRChannelGroupsContainer.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "pvr/epg/EpgChannelData.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/guilib/GUIEPGGridContainer.h"
+#include "pvr/guilib/PVRGUIActionsChannels.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/timers/PVRTimers.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "view/GUIViewState.h"
+
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+using namespace KODI::MESSAGING;
+using namespace PVR;
+using namespace std::chrono_literals;
+
+CGUIWindowPVRGuideBase::CGUIWindowPVRGuideBase(bool bRadio, int id, const std::string& xmlFile)
+ : CGUIWindowPVRBase(bRadio, id, xmlFile)
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().RegisterChannelNumberInputHandler(this);
+}
+
+CGUIWindowPVRGuideBase::~CGUIWindowPVRGuideBase()
+{
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().DeregisterChannelNumberInputHandler(
+ this);
+
+ m_bRefreshTimelineItems = false;
+ m_bSyncRefreshTimelineItems = false;
+ StopRefreshTimelineItemsThread();
+}
+
+CGUIEPGGridContainer* CGUIWindowPVRGuideBase::GetGridControl()
+{
+ return dynamic_cast<CGUIEPGGridContainer*>(GetControl(m_viewControl.GetCurrentControl()));
+}
+
+void CGUIWindowPVRGuideBase::InitEpgGridControl()
+{
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ {
+ CPVRManager& mgr = CServiceBroker::GetPVRManager();
+
+ const std::shared_ptr<CPVRChannel> channel = mgr.ChannelGroups()->GetByPath(
+ mgr.Get<PVR::GUI::Channels>().GetSelectedChannelPath(m_bRadio));
+
+ if (channel)
+ {
+ m_bChannelSelectionRestored = epgGridContainer->SetChannel(channel);
+ epgGridContainer->JumpToDate(
+ mgr.PlaybackState()->GetPlaybackTime(channel->ClientID(), channel->UniqueID()));
+ }
+ else
+ {
+ m_bChannelSelectionRestored = false;
+ epgGridContainer->JumpToNow();
+ }
+
+ if (!epgGridContainer->HasData())
+ m_bSyncRefreshTimelineItems = true; // force data update on first window open
+ }
+
+ StartRefreshTimelineItemsThread();
+}
+
+void CGUIWindowPVRGuideBase::ClearData()
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_cachedChannelGroup.reset();
+ }
+
+ CGUIWindowPVRBase::ClearData();
+}
+
+void CGUIWindowPVRGuideBase::OnInitWindow()
+{
+ if (m_guiState)
+ m_viewControl.SetCurrentView(m_guiState->GetViewAsControl(), false);
+
+ if (InitChannelGroup()) // no channels -> lazy init
+ InitEpgGridControl();
+
+ CGUIWindowPVRBase::OnInitWindow();
+}
+
+void CGUIWindowPVRGuideBase::OnDeinitWindow(int nextWindowID)
+{
+ StopRefreshTimelineItemsThread();
+
+ m_bChannelSelectionRestored = false;
+
+ CGUIDialog* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_GUIDE_CONTROLS);
+ if (dialog && dialog->IsDialogRunning())
+ {
+ dialog->Close();
+ }
+
+ CGUIWindowPVRBase::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIWindowPVRGuideBase::StartRefreshTimelineItemsThread()
+{
+ StopRefreshTimelineItemsThread();
+ m_refreshTimelineItemsThread.reset(new CPVRRefreshTimelineItemsThread(this));
+ m_refreshTimelineItemsThread->Create();
+}
+
+void CGUIWindowPVRGuideBase::StopRefreshTimelineItemsThread()
+{
+ if (m_refreshTimelineItemsThread)
+ m_refreshTimelineItemsThread->Stop();
+}
+
+void CGUIWindowPVRGuideBase::NotifyEvent(const PVREvent& event)
+{
+ if (event == PVREvent::Epg || event == PVREvent::EpgContainer ||
+ event == PVREvent::ChannelGroupInvalidated || event == PVREvent::ChannelGroup)
+ {
+ m_bRefreshTimelineItems = true;
+ // no base class call => do async refresh
+ return;
+ }
+ else if (event == PVREvent::ChannelPlaybackStopped)
+ {
+ if (m_guiState && m_guiState->GetSortMethod().sortBy == SortByLastPlayed)
+ {
+ // set dirty to force sync refresh
+ m_bSyncRefreshTimelineItems = true;
+ }
+ }
+
+ // do sync refresh if dirty
+ CGUIWindowPVRBase::NotifyEvent(event);
+}
+
+void CGUIWindowPVRGuideBase::SetInvalid()
+{
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ epgGridContainer->SetInvalid();
+
+ CGUIWindowPVRBase::SetInvalid();
+}
+
+void CGUIWindowPVRGuideBase::GetContextButtons(int itemNumber, CContextButtons& buttons)
+{
+ CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons);
+
+ buttons.Add(CONTEXT_BUTTON_NAVIGATE, 19326); // Navigate...
+}
+
+void CGUIWindowPVRGuideBase::UpdateSelectedItemPath()
+{
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ {
+ const std::shared_ptr<CPVRChannelGroupMember> groupMember =
+ epgGridContainer->GetSelectedChannelGroupMember();
+ if (groupMember)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().SetSelectedChannelPath(
+ m_bRadio, groupMember->Path());
+ }
+}
+
+void CGUIWindowPVRGuideBase::UpdateButtons()
+{
+ CGUIWindowPVRBase::UpdateButtons();
+
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, g_localizeStrings.Get(19032));
+
+ const std::shared_ptr<CPVRChannelGroup> group = GetChannelGroup();
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, group ? group->GroupName() : "");
+}
+
+bool CGUIWindowPVRGuideBase::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ if (m_bUpdating)
+ {
+ // Prevent concurrent updates. Instead, let the timeline items refresh thread pick it up later.
+ m_bRefreshTimelineItems = true;
+ return true;
+ }
+
+ bool bReturn = CGUIWindowPVRBase::Update(strDirectory, updateFilterPath);
+
+ if (bReturn && !m_bChannelSelectionRestored)
+ {
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ m_bChannelSelectionRestored = epgGridContainer->SetChannel(
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetSelectedChannelPath(
+ m_bRadio));
+ }
+
+ return bReturn;
+}
+
+bool CGUIWindowPVRGuideBase::GetDirectory(const std::string& strDirectory, CFileItemList& items)
+{
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ if (m_cachedChannelGroup && *m_cachedChannelGroup != *GetChannelGroup())
+ {
+ // channel group change and not very first open of this window. force immediate update.
+ m_bSyncRefreshTimelineItems = true;
+ }
+ }
+
+ if (m_bSyncRefreshTimelineItems)
+ m_refreshTimelineItemsThread->DoRefresh(true);
+
+ const CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ {
+ const std::unique_ptr<CFileItemList> newTimeline = GetGridControl()->GetCurrentTimeLineItems();
+ items.RemoveDiscCache(GetID());
+ items.Assign(*newTimeline, false);
+ }
+
+ return true;
+}
+
+void CGUIWindowPVRGuideBase::FormatAndSort(CFileItemList& items)
+{
+ // Speedup: Nothing to do here as sorting was already done in RefreshTimelineItems
+ return;
+}
+
+CFileItemPtr CGUIWindowPVRGuideBase::GetCurrentListItem(int offset /*= 0*/)
+{
+ const CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ return epgGridContainer->GetSelectedGridItem(offset);
+
+ return {};
+}
+
+int CGUIWindowPVRGuideBase::GetCurrentListItemIndex(const std::shared_ptr<CFileItem>& item)
+{
+ return item ? item->GetProperty("TimelineIndex").asInteger() : -1;
+}
+
+bool CGUIWindowPVRGuideBase::ShouldNavigateToGridContainer(int iAction)
+{
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ CGUIControl* control = GetControl(CONTROL_LSTCHANNELGROUPS);
+ if (epgGridContainer && control && GetFocusedControlID() == control->GetID())
+ {
+ int iNavigationId = control->GetAction(iAction).GetNavigation();
+ if (iNavigationId > 0)
+ {
+ control = epgGridContainer;
+ while (control !=
+ this) // navigation target could be the grid control or one of its parent controls.
+ {
+ if (iNavigationId == control->GetID())
+ {
+ // channel group selector control's target for the action is the grid control
+ return true;
+ }
+ control = control->GetParentControl();
+ }
+ }
+ }
+ return false;
+}
+
+bool CGUIWindowPVRGuideBase::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_MOVE_UP:
+ case ACTION_MOVE_DOWN:
+ case ACTION_MOVE_LEFT:
+ case ACTION_MOVE_RIGHT:
+ {
+ // Check whether grid container is configured as channel group selector's navigation target for the given action.
+ if (ShouldNavigateToGridContainer(action.GetID()))
+ {
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ {
+ CGUIWindowPVRBase::OnAction(action);
+
+ switch (action.GetID())
+ {
+ case ACTION_MOVE_UP:
+ epgGridContainer->GoToBottom();
+ return true;
+ case ACTION_MOVE_DOWN:
+ epgGridContainer->GoToTop();
+ return true;
+ case ACTION_MOVE_LEFT:
+ epgGridContainer->GoToMostRight();
+ return true;
+ case ACTION_MOVE_RIGHT:
+ epgGridContainer->GoToMostLeft();
+ return true;
+ default:
+ break;
+ }
+ }
+ }
+ break;
+ }
+ case REMOTE_0:
+ if (GetCurrentDigitCount() == 0)
+ {
+ // single zero input is handled by epg grid container
+ break;
+ }
+ // fall-thru is intended
+ [[fallthrough]];
+ case REMOTE_1:
+ case REMOTE_2:
+ case REMOTE_3:
+ case REMOTE_4:
+ case REMOTE_5:
+ case REMOTE_6:
+ case REMOTE_7:
+ case REMOTE_8:
+ case REMOTE_9:
+ AppendChannelNumberCharacter(static_cast<char>(action.GetID() - REMOTE_0) + '0');
+ return true;
+
+ case ACTION_CHANNEL_NUMBER_SEP:
+ AppendChannelNumberCharacter(CPVRChannelNumber::SEPARATOR);
+ return true;
+ }
+
+ return CGUIWindowPVRBase::OnAction(action);
+}
+
+void CGUIWindowPVRGuideBase::RefreshView(CGUIMessage& message, bool bInitGridControl)
+{
+ CGUIWindowPVRBase::OnMessage(message);
+
+ // force grid data update
+ m_bSyncRefreshTimelineItems = true;
+
+ if (bInitGridControl)
+ InitEpgGridControl();
+
+ Refresh(true);
+}
+
+bool CGUIWindowPVRGuideBase::OnMessage(CGUIMessage& message)
+{
+ bool bReturn = false;
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ const CPVRChannelsPath path(message.GetStringParam(0));
+ if (path.IsValid() && path.IsChannelGroup())
+ {
+ // if a path to a channel group is given we must init
+ // that group instead of last played/selected group
+ m_channelGroupPath = message.GetStringParam(0);
+ }
+ break;
+ }
+
+ case GUI_MSG_ITEM_SELECTED:
+ message.SetParam1(GetCurrentListItemIndex(GetCurrentListItem()));
+ bReturn = true;
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ if (message.GetSenderId() == m_viewControl.GetCurrentControl())
+ {
+ if (message.GetParam1() == ACTION_SELECT_ITEM ||
+ message.GetParam1() == ACTION_MOUSE_LEFT_CLICK)
+ {
+ // If direct channel number input is active, select the entered channel.
+ if (CServiceBroker::GetPVRManager()
+ .Get<PVR::GUI::Channels>()
+ .GetChannelNumberInputHandler()
+ .CheckInputAndExecuteAction())
+ {
+ bReturn = true;
+ break;
+ }
+ }
+
+ const std::shared_ptr<CFileItem> pItem = GetCurrentListItem();
+ if (pItem)
+ {
+ switch (message.GetParam1())
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ switch (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_EPG_SELECTACTION))
+ {
+ case EPG_SELECT_ACTION_CONTEXT_MENU:
+ OnPopupMenu(GetCurrentListItemIndex(pItem));
+ bReturn = true;
+ break;
+ case EPG_SELECT_ACTION_SWITCH:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(*pItem,
+ true);
+ bReturn = true;
+ break;
+ case EPG_SELECT_ACTION_PLAY_RECORDING:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(*pItem,
+ true);
+ bReturn = true;
+ break;
+ case EPG_SELECT_ACTION_INFO:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem);
+ bReturn = true;
+ break;
+ case EPG_SELECT_ACTION_RECORD:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimer(*pItem);
+ bReturn = true;
+ break;
+ case EPG_SELECT_ACTION_SMART_SELECT:
+ {
+ const std::shared_ptr<CPVREpgInfoTag> tag(pItem->GetEPGInfoTag());
+ if (tag)
+ {
+ const CDateTime start(tag->StartAsUTC());
+ const CDateTime end(tag->EndAsUTC());
+ const CDateTime now(CDateTime::GetUTCDateTime());
+
+ if (start <= now && now <= end)
+ {
+ // current event
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ *pItem, true);
+ }
+ else if (now < start)
+ {
+ // future event
+ if (CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(tag))
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimer(*pItem);
+ else
+ {
+ bool bCanRecord = true;
+ const std::shared_ptr<CPVRChannel> channel = CPVRItem(pItem).GetChannel();
+ if (channel)
+ bCanRecord = channel->CanRecord();
+
+ const int iTextID =
+ bCanRecord
+ ? 19302 // "Do you want to record the selected programme or to switch to the current programme?"
+ : 19344; // "Do you want to set a reminder for the selected programme or to switch to the current programme?"
+ const int iNoButtonID = bCanRecord ? 264 // No => "Record"
+ : 826; // "Set reminder"
+
+ HELPERS::DialogResponse ret =
+ HELPERS::ShowYesNoDialogText(CVariant{19096}, // "Smart select"
+ CVariant{iTextID}, CVariant{iNoButtonID},
+ CVariant{19165}); // Yes => "Switch"
+ if (ret == HELPERS::DialogResponse::CHOICE_NO)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimer(*pItem,
+ false);
+ else if (ret == HELPERS::DialogResponse::CHOICE_YES)
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(
+ *pItem, true);
+ }
+ }
+ else
+ {
+ // past event
+ if (CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(tag))
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *pItem, true);
+ else if (tag->IsPlayable())
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayEpgTag(
+ *pItem);
+ else
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem);
+ }
+ bReturn = true;
+ }
+ break;
+ }
+ }
+ break;
+ case ACTION_SHOW_INFO:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem);
+ bReturn = true;
+ break;
+ case ACTION_PLAYER_PLAY:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(*pItem,
+ true);
+ bReturn = true;
+ break;
+ case ACTION_RECORD:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimer(*pItem);
+ bReturn = true;
+ break;
+ case ACTION_PVR_SHOW_TIMER_RULE:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimerRule(*pItem, true,
+ false);
+ bReturn = true;
+ break;
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnPopupMenu(GetCurrentListItemIndex(pItem));
+ bReturn = true;
+ break;
+ }
+ }
+ }
+ else if (message.GetSenderId() == CONTROL_BTNVIEWASICONS ||
+ message.GetSenderId() == CONTROL_BTNSORTBY ||
+ message.GetSenderId() == CONTROL_BTNSORTASC)
+ {
+ RefreshView(message, false);
+ bReturn = true;
+ }
+ break;
+ }
+ case GUI_MSG_CHANGE_SORT_DIRECTION:
+ case GUI_MSG_CHANGE_SORT_METHOD:
+ case GUI_MSG_CHANGE_VIEW_MODE:
+ {
+ RefreshView(message, message.GetMessage() == GUI_MSG_CHANGE_VIEW_MODE);
+ bReturn = true;
+ break;
+ }
+ case GUI_MSG_REFRESH_LIST:
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::ManagerStarted:
+ if (InitChannelGroup())
+ InitEpgGridControl();
+ break;
+
+ case PVREvent::ChannelGroup:
+ case PVREvent::ChannelGroupInvalidated:
+ case PVREvent::ClientsInvalidated:
+ case PVREvent::ChannelPlaybackStopped:
+ case PVREvent::Epg:
+ case PVREvent::EpgContainer:
+ if (InitChannelGroup())
+ Refresh(true);
+ break;
+
+ case PVREvent::Timers:
+ case PVREvent::TimersInvalidated:
+ SetInvalid();
+ break;
+
+ default:
+ break;
+ }
+ break;
+ }
+ case GUI_MSG_SYSTEM_WAKE:
+ GotoCurrentProgramme();
+ bReturn = true;
+ break;
+ }
+
+ return bReturn || CGUIWindowPVRBase::OnMessage(message);
+}
+
+bool CGUIWindowPVRGuideBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ if (OnContextButtonNavigate(button))
+ return true;
+
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return false;
+
+ return CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+namespace
+{
+
+template<typename A>
+class CContextMenuFunctions : public CContextButtons
+{
+public:
+ explicit CContextMenuFunctions(A* instance) : m_instance(instance) {}
+
+ void Add(bool (A::*function)(), unsigned int resId)
+ {
+ CContextButtons::Add(size(), resId);
+ m_functions.emplace_back(std::bind(function, m_instance));
+ }
+
+ bool Call(int idx)
+ {
+ if (idx < 0 || idx >= static_cast<int>(m_functions.size()))
+ return false;
+
+ return m_functions[idx]();
+ }
+
+private:
+ A* m_instance = nullptr;
+ std::vector<std::function<bool()>> m_functions;
+};
+
+} // unnamed namespace
+
+bool CGUIWindowPVRGuideBase::OnContextButtonNavigate(CONTEXT_BUTTON button)
+{
+ bool bReturn = false;
+
+ if (button == CONTEXT_BUTTON_NAVIGATE)
+ {
+ if (g_SkinInfo->HasSkinFile("DialogPVRGuideControls.xml"))
+ {
+ // use controls dialog
+ CGUIDialog* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_GUIDE_CONTROLS);
+ if (dialog && !dialog->IsDialogRunning())
+ {
+ dialog->Open();
+ }
+ }
+ else
+ {
+ // use context menu
+ CContextMenuFunctions<CGUIWindowPVRGuideBase> buttons(this);
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoBegin, 19063); // First programme
+ buttons.Add(&CGUIWindowPVRGuideBase::Go12HoursBack, 19317); // 12 hours back
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoCurrentProgramme, 19070); // Current programme
+ buttons.Add(&CGUIWindowPVRGuideBase::Go12HoursForward, 19318); // 12 hours forward
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoEnd, 19064); // Last programme
+ buttons.Add(&CGUIWindowPVRGuideBase::OpenDateSelectionDialog, 19288); // Date selector
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoFirstChannel, 19322); // First channel
+ if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV() ||
+ CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio() ||
+ CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingEpgTag())
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoPlayingChannel, 19323); // Playing channel
+ buttons.Add(&CGUIWindowPVRGuideBase::GotoLastChannel, 19324); // Last channel
+ buttons.Add(&CGUIWindowPVRBase::ActivatePreviousChannelGroup, 19319); // Previous group
+ buttons.Add(&CGUIWindowPVRBase::ActivateNextChannelGroup, 19320); // Next group
+ buttons.Add(&CGUIWindowPVRBase::OpenChannelGroupSelectionDialog, 19321); // Group selector
+
+ int buttonIdx = 0;
+ int lastButtonIdx = 2; // initially select "Current programme"
+
+ // loop until canceled
+ while (buttonIdx >= 0)
+ {
+ buttonIdx = CGUIDialogContextMenu::Show(buttons, lastButtonIdx);
+ lastButtonIdx = buttonIdx;
+ buttons.Call(buttonIdx);
+ }
+ }
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIWindowPVRGuideBase::RefreshTimelineItems()
+{
+ if (m_bRefreshTimelineItems || m_bSyncRefreshTimelineItems)
+ {
+ m_bRefreshTimelineItems = false;
+ m_bSyncRefreshTimelineItems = false;
+
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ if (epgGridContainer)
+ {
+ const std::shared_ptr<CPVRChannelGroup> group(GetChannelGroup());
+ if (!group)
+ return false;
+
+ CPVREpgContainer& epgContainer = CServiceBroker::GetPVRManager().EpgContainer();
+
+ const std::pair<CDateTime, CDateTime> dates = epgContainer.GetFirstAndLastEPGDate();
+ CDateTime startDate = dates.first;
+ CDateTime endDate = dates.second;
+ const CDateTime currentDate = CDateTime::GetUTCDateTime();
+
+ if (!startDate.IsValid())
+ startDate = currentDate;
+
+ if (!endDate.IsValid() || endDate < startDate)
+ endDate = startDate;
+
+ // limit start to past days to display
+ const int iPastDays = epgContainer.GetPastDaysToDisplay();
+ const CDateTime maxPastDate(currentDate - CDateTimeSpan(iPastDays, 0, 0, 0));
+ if (startDate < maxPastDate)
+ startDate = maxPastDate;
+
+ // limit end to future days to display
+ const int iFutureDays = epgContainer.GetFutureDaysToDisplay();
+ const CDateTime maxFutureDate(currentDate + CDateTimeSpan(iFutureDays, 0, 0, 0));
+ if (endDate > maxFutureDate)
+ endDate = maxFutureDate;
+
+ std::unique_ptr<CFileItemList> channels(new CFileItemList);
+ const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers =
+ group->GetMembers(CPVRChannelGroup::Include::ONLY_VISIBLE);
+
+ for (const auto& groupMember : groupMembers)
+ {
+ channels->Add(std::make_shared<CFileItem>(groupMember));
+ }
+
+ if (m_guiState)
+ channels->Sort(m_guiState->GetSortMethod());
+
+ epgGridContainer->SetTimelineItems(channels, startDate, endDate);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_cachedChannelGroup = group;
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CGUIWindowPVRGuideBase::GotoBegin()
+{
+ GetGridControl()->GoToBegin();
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::GotoEnd()
+{
+ GetGridControl()->GoToEnd();
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::GotoCurrentProgramme()
+{
+ const CPVRManager& mgr = CServiceBroker::GetPVRManager();
+ std::shared_ptr<CPVRChannel> channel = mgr.PlaybackState()->GetPlayingChannel();
+
+ if (!channel)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> playingTag = mgr.PlaybackState()->GetPlayingEpgTag();
+ if (playingTag)
+ channel = mgr.ChannelGroups()->GetChannelForEpgTag(playingTag);
+ }
+
+ if (channel)
+ GetGridControl()->GoToDate(
+ mgr.PlaybackState()->GetPlaybackTime(channel->ClientID(), channel->UniqueID()));
+ else
+ GetGridControl()->GoToNow();
+
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::OpenDateSelectionDialog()
+{
+ bool bReturn = false;
+
+ KODI::TIME::SystemTime date;
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ epgGridContainer->GetSelectedDate().GetAsSystemTime(date);
+
+ if (CGUIDialogNumeric::ShowAndGetDate(date, g_localizeStrings.Get(19288))) /* Go to date */
+ {
+ epgGridContainer->GoToDate(CDateTime(date));
+ bReturn = true;
+ }
+
+ return bReturn;
+}
+
+bool CGUIWindowPVRGuideBase::Go12HoursBack()
+{
+ return GotoDate(-12);
+}
+
+bool CGUIWindowPVRGuideBase::Go12HoursForward()
+{
+ return GotoDate(+12);
+}
+
+bool CGUIWindowPVRGuideBase::GotoDate(int deltaHours)
+{
+ CGUIEPGGridContainer* epgGridContainer = GetGridControl();
+ epgGridContainer->GoToDate(epgGridContainer->GetSelectedDate() +
+ CDateTimeSpan(0, deltaHours, 0, 0));
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::GotoFirstChannel()
+{
+ GetGridControl()->GoToFirstChannel();
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::GotoLastChannel()
+{
+ GetGridControl()->GoToLastChannel();
+ return true;
+}
+
+bool CGUIWindowPVRGuideBase::GotoPlayingChannel()
+{
+ const CPVRManager& mgr = CServiceBroker::GetPVRManager();
+ std::shared_ptr<CPVRChannel> channel = mgr.PlaybackState()->GetPlayingChannel();
+
+ if (!channel)
+ {
+ const std::shared_ptr<CPVREpgInfoTag> playingTag = mgr.PlaybackState()->GetPlayingEpgTag();
+ if (playingTag)
+ channel = mgr.ChannelGroups()->GetChannelForEpgTag(playingTag);
+ }
+
+ if (channel)
+ {
+ GetGridControl()->SetChannel(channel);
+ return true;
+ }
+ return false;
+}
+
+void CGUIWindowPVRGuideBase::OnInputDone()
+{
+ const CPVRChannelNumber channelNumber = GetChannelNumber();
+ if (channelNumber.IsValid())
+ {
+ GetGridControl()->SetChannel(channelNumber);
+ }
+}
+
+void CGUIWindowPVRGuideBase::GetChannelNumbers(std::vector<std::string>& channelNumbers)
+{
+ const std::shared_ptr<CPVRChannelGroup> group = GetChannelGroup();
+ if (group)
+ group->GetChannelNumbers(channelNumbers);
+}
+
+CPVRRefreshTimelineItemsThread::CPVRRefreshTimelineItemsThread(CGUIWindowPVRGuideBase* pGuideWindow)
+ : CThread("epg-grid-refresh-timeline-items"),
+ m_pGuideWindow(pGuideWindow),
+ m_ready(true),
+ m_done(false)
+{
+}
+
+CPVRRefreshTimelineItemsThread::~CPVRRefreshTimelineItemsThread()
+{
+ // Note: CThread dtor will also call StopThread(true), but if thread worker function exits that
+ // late, it might access member variables of this which are already destroyed. Thus, stop
+ // the thread worker here and synchronously, while all members of this are still alive.
+ StopThread(true);
+}
+
+void CPVRRefreshTimelineItemsThread::Stop()
+{
+ StopThread(false);
+ m_ready.Set(); // wake up the worker thread to let it exit
+}
+
+void CPVRRefreshTimelineItemsThread::DoRefresh(bool bWait)
+{
+ m_ready.Set(); // wake up the worker thread
+
+ if (bWait)
+ {
+ m_done.Reset();
+ CGUIDialogBusy::WaitOnEvent(m_done, 100, false);
+ }
+}
+
+void CPVRRefreshTimelineItemsThread::Process()
+{
+ static const int BOOSTED_SLEEPS_THRESHOLD = 4;
+
+ int iLastEpgItemsCount = 0;
+ int iUpdatesWithoutChange = 0;
+
+ while (!m_bStop)
+ {
+ m_done.Reset();
+
+ if (m_pGuideWindow->RefreshTimelineItems() && !m_bStop)
+ {
+ CGUIMessage m(GUI_MSG_REFRESH_LIST, m_pGuideWindow->GetID(), 0,
+ static_cast<int>(PVREvent::Epg));
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+ }
+
+ if (m_bStop)
+ break;
+
+ m_done.Set();
+
+ // in order to fill the guide window asap, use a short update interval until we the
+ // same amount of epg events for BOOSTED_SLEEPS_THRESHOLD + 1 times in a row .
+ if (iUpdatesWithoutChange < BOOSTED_SLEEPS_THRESHOLD)
+ {
+ int iCurrentEpgItemsCount = m_pGuideWindow->CurrentDirectory().Size();
+
+ if (iCurrentEpgItemsCount == iLastEpgItemsCount)
+ iUpdatesWithoutChange++;
+ else
+ iUpdatesWithoutChange = 0; // reset
+
+ iLastEpgItemsCount = iCurrentEpgItemsCount;
+
+ m_ready.Wait(1000ms); // boosted update cycle
+ }
+ else
+ {
+ m_ready.Wait(5000ms); // normal update cycle
+ }
+
+ m_ready.Reset();
+ }
+
+ m_ready.Reset();
+ m_done.Set();
+}
+
+std::string CGUIWindowPVRTVGuide::GetRootPath() const
+{
+ return "pvr://guide/tv/";
+}
+
+std::string CGUIWindowPVRRadioGuide::GetRootPath() const
+{
+ return "pvr://guide/radio/";
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRGuide.h b/xbmc/pvr/windows/GUIWindowPVRGuide.h
new file mode 100644
index 0000000..87477ca
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRGuide.h
@@ -0,0 +1,129 @@
+/*
+ * 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 "pvr/PVRChannelNumberInputHandler.h"
+#include "pvr/windows/GUIWindowPVRBase.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+
+#include <atomic>
+#include <memory>
+#include <string>
+
+class CFileItemList;
+class CGUIMessage;
+
+namespace PVR
+{
+enum class PVREvent;
+
+class CPVRChannelGroup;
+class CGUIEPGGridContainer;
+class CPVRRefreshTimelineItemsThread;
+
+class CGUIWindowPVRGuideBase : public CGUIWindowPVRBase, public CPVRChannelNumberInputHandler
+{
+public:
+ CGUIWindowPVRGuideBase(bool bRadio, int id, const std::string& xmlFile);
+ ~CGUIWindowPVRGuideBase() override;
+
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void GetContextButtons(int itemNumber, CContextButtons& buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ void UpdateButtons() override;
+ void SetInvalid() override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+
+ void NotifyEvent(const PVREvent& event) override;
+
+ bool RefreshTimelineItems();
+
+ // CPVRChannelNumberInputHandler implementation
+ void GetChannelNumbers(std::vector<std::string>& channelNumbers) override;
+ void OnInputDone() override;
+
+ bool GotoBegin();
+ bool GotoEnd();
+ bool GotoCurrentProgramme();
+ bool GotoDate(int deltaHours);
+ bool OpenDateSelectionDialog();
+ bool Go12HoursBack();
+ bool Go12HoursForward();
+ bool GotoFirstChannel();
+ bool GotoLastChannel();
+ bool GotoPlayingChannel();
+
+protected:
+ void UpdateSelectedItemPath() override;
+ std::string GetDirectoryPath() override { return ""; }
+ bool GetDirectory(const std::string& strDirectory, CFileItemList& items) override;
+ void FormatAndSort(CFileItemList& items) override;
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+
+ void ClearData() override;
+
+private:
+ CGUIEPGGridContainer* GetGridControl();
+ void InitEpgGridControl();
+
+ bool OnContextButtonNavigate(CONTEXT_BUTTON button);
+
+ bool ShouldNavigateToGridContainer(int iAction);
+
+ void StartRefreshTimelineItemsThread();
+ void StopRefreshTimelineItemsThread();
+
+ void RefreshView(CGUIMessage& message, bool bInitGridControl);
+
+ int GetCurrentListItemIndex(const std::shared_ptr<CFileItem>& item);
+
+ std::unique_ptr<CPVRRefreshTimelineItemsThread> m_refreshTimelineItemsThread;
+ std::atomic_bool m_bRefreshTimelineItems{false};
+ std::atomic_bool m_bSyncRefreshTimelineItems{false};
+
+ std::shared_ptr<CPVRChannelGroup> m_cachedChannelGroup;
+
+ bool m_bChannelSelectionRestored{false};
+};
+
+class CGUIWindowPVRTVGuide : public CGUIWindowPVRGuideBase
+{
+public:
+ CGUIWindowPVRTVGuide() : CGUIWindowPVRGuideBase(false, WINDOW_TV_GUIDE, "MyPVRGuide.xml") {}
+ std::string GetRootPath() const override;
+};
+
+class CGUIWindowPVRRadioGuide : public CGUIWindowPVRGuideBase
+{
+public:
+ CGUIWindowPVRRadioGuide() : CGUIWindowPVRGuideBase(true, WINDOW_RADIO_GUIDE, "MyPVRGuide.xml") {}
+ std::string GetRootPath() const override;
+};
+
+class CPVRRefreshTimelineItemsThread : public CThread
+{
+public:
+ explicit CPVRRefreshTimelineItemsThread(CGUIWindowPVRGuideBase* pGuideWindow);
+ ~CPVRRefreshTimelineItemsThread() override;
+
+ void Process() override;
+
+ void DoRefresh(bool bWait);
+ void Stop();
+
+private:
+ CGUIWindowPVRGuideBase* m_pGuideWindow;
+ CEvent m_ready;
+ CEvent m_done;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/GUIWindowPVRRecordings.cpp b/xbmc/pvr/windows/GUIWindowPVRRecordings.cpp
new file mode 100644
index 0000000..d127217
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRRecordings.cpp
@@ -0,0 +1,440 @@
+/*
+ * 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 "GUIWindowPVRRecordings.h"
+
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIRadioButtonControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsPlayback.h"
+#include "pvr/guilib/PVRGUIActionsRecordings.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "pvr/recordings/PVRRecordingsPath.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+#include "video/VideoLibraryQueue.h"
+#include "video/VideoUtils.h"
+#include "video/windows/GUIWindowVideoBase.h"
+#include "video/windows/GUIWindowVideoNav.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+
+using namespace PVR;
+
+CGUIWindowPVRRecordingsBase::CGUIWindowPVRRecordingsBase(bool bRadio,
+ int id,
+ const std::string& xmlFile)
+ : CGUIWindowPVRBase(bRadio, id, xmlFile),
+ m_settings(
+ {CSettings::SETTING_PVRRECORD_GROUPRECORDINGS, CSettings::SETTING_MYVIDEOS_SELECTACTION})
+{
+}
+
+CGUIWindowPVRRecordingsBase::~CGUIWindowPVRRecordingsBase() = default;
+
+void CGUIWindowPVRRecordingsBase::OnWindowLoaded()
+{
+ CONTROL_SELECT(CONTROL_BTNGROUPITEMS);
+}
+
+std::string CGUIWindowPVRRecordingsBase::GetDirectoryPath()
+{
+ const std::string basePath = CPVRRecordingsPath(m_bShowDeletedRecordings, m_bRadio);
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath()
+ : basePath;
+}
+
+void CGUIWindowPVRRecordingsBase::GetContextButtons(int itemNumber, CContextButtons& buttons)
+{
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return;
+ CFileItemPtr pItem = m_vecItems->Get(itemNumber);
+
+ if (pItem->IsParentFolder())
+ {
+ // No context menu for ".." items
+ return;
+ }
+
+ bool isDeletedRecording = false;
+
+ std::shared_ptr<CPVRRecording> recording(pItem->GetPVRRecordingInfoTag());
+ if (recording)
+ {
+ isDeletedRecording = recording->IsDeleted();
+
+ if (isDeletedRecording)
+ {
+ if (m_vecItems->GetObjectCount() > 1)
+ buttons.Add(CONTEXT_BUTTON_DELETE_ALL, 19292); /* Delete all permanently */
+ }
+ }
+
+ if (!isDeletedRecording)
+ CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons);
+}
+
+bool CGUIWindowPVRRecordingsBase::OnAction(const CAction& action)
+{
+ if (action.GetID() == ACTION_PARENT_DIR || action.GetID() == ACTION_NAV_BACK)
+ {
+ CPVRRecordingsPath path(m_vecItems->GetPath());
+ if (path.IsValid() && !path.IsRecordingsRoot())
+ {
+ GoParentFolder();
+ return true;
+ }
+ }
+ else if (action.GetID() == ACTION_TOGGLE_WATCHED)
+ {
+ const std::shared_ptr<CFileItem> pItem = m_vecItems->Get(m_viewControl.GetSelectedItem());
+ if (!pItem || pItem->IsParentFolder())
+ return false;
+
+ bool bUnWatched = false;
+ if (pItem->HasPVRRecordingInfoTag())
+ bUnWatched = pItem->GetPVRRecordingInfoTag()->GetPlayCount() == 0;
+ else if (pItem->m_bIsFolder)
+ bUnWatched = pItem->GetProperty("unwatchedepisodes").asInteger() > 0;
+ else
+ return false;
+
+ CVideoLibraryQueue::GetInstance().MarkAsWatched(pItem, bUnWatched);
+ return true;
+ }
+
+ return CGUIWindowPVRBase::OnAction(action);
+}
+
+bool CGUIWindowPVRRecordingsBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return false;
+ CFileItemPtr pItem = m_vecItems->Get(itemNumber);
+
+ return OnContextButtonDeleteAll(pItem.get(), button) ||
+ CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowPVRRecordingsBase::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ m_thumbLoader.StopThread();
+
+ int iOldCount = m_vecItems->GetObjectCount();
+ const std::string oldPath = m_vecItems->GetPath();
+
+ bool bReturn = CGUIWindowPVRBase::Update(strDirectory);
+
+ if (bReturn)
+ {
+ // TODO: does it make sense to show the non-deleted recordings, although user wants
+ // to see the deleted recordings? Or is this just another hack to avoid misbehavior
+ // of CGUIMediaWindow if it has no content?
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ /* empty list for deleted recordings */
+ if (m_vecItems->GetObjectCount() == 0 && m_bShowDeletedRecordings)
+ {
+ /* show the normal recordings instead */
+ m_bShowDeletedRecordings = false;
+ lock.unlock();
+ Update(GetDirectoryPath());
+ return bReturn;
+ }
+ }
+
+ if (bReturn && iOldCount > 0 && m_vecItems->GetObjectCount() == 0 &&
+ oldPath == m_vecItems->GetPath())
+ {
+ /* go to the parent folder if we're in a subdirectory and for instance just deleted the last item */
+ const CPVRRecordingsPath path(m_vecItems->GetPath());
+ if (path.IsValid() && !path.IsRecordingsRoot())
+ GoParentFolder();
+ }
+ return bReturn;
+}
+
+void CGUIWindowPVRRecordingsBase::UpdateButtons()
+{
+ int iWatchMode = CMediaSettings::GetInstance().GetWatchedMode("recordings");
+ int iStringId = 257; // "Error"
+
+ if (iWatchMode == WatchedModeAll)
+ iStringId = 22015; // "All recordings"
+ else if (iWatchMode == WatchedModeUnwatched)
+ iStringId = 16101; // "Unwatched"
+ else if (iWatchMode == WatchedModeWatched)
+ iStringId = 16102; // "Watched"
+
+ SET_CONTROL_LABEL(CONTROL_BTNSHOWMODE, g_localizeStrings.Get(iStringId));
+
+ bool bGroupRecordings = m_settings.GetBoolValue(CSettings::SETTING_PVRRECORD_GROUPRECORDINGS);
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTNGROUPITEMS, bGroupRecordings);
+
+ CGUIRadioButtonControl* btnShowDeleted =
+ static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWDELETED));
+ if (btnShowDeleted)
+ {
+ btnShowDeleted->SetVisible(
+ m_bRadio ? CServiceBroker::GetPVRManager().Recordings()->HasDeletedRadioRecordings()
+ : CServiceBroker::GetPVRManager().Recordings()->HasDeletedTVRecordings());
+ btnShowDeleted->SetSelected(m_bShowDeletedRecordings);
+ }
+
+ CGUIWindowPVRBase::UpdateButtons();
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, m_bShowDeletedRecordings
+ ? g_localizeStrings.Get(19179)
+ : ""); /* Deleted recordings trash */
+
+ const CPVRRecordingsPath path(m_vecItems->GetPath());
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2,
+ bGroupRecordings && path.IsValid() ? path.GetUnescapedDirectoryPath() : "");
+}
+
+bool CGUIWindowPVRRecordingsBase::OnMessage(CGUIMessage& message)
+{
+ bool bReturn = false;
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ if (message.GetSenderId() == m_viewControl.GetCurrentControl())
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ const CFileItemPtr item(m_vecItems->Get(iItem));
+ switch (message.GetParam1())
+ {
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ case ACTION_PLAYER_PLAY:
+ {
+ const CPVRRecordingsPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsRecordingsRoot() && item->IsParentFolder())
+ {
+ // handle special 'go home' item.
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_HOME);
+ bReturn = true;
+ break;
+ }
+
+ if (!item->IsParentFolder() && message.GetParam1() == ACTION_PLAYER_PLAY)
+ {
+ if (item->m_bIsFolder)
+ {
+ if (CGUIWindowVideoNav::ShowResumeMenu(*item))
+ VIDEO_UTILS::PlayItem(item);
+ }
+ else
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *item, true /* check resume */);
+
+ bReturn = true;
+ }
+ else if (item->m_bIsFolder)
+ {
+ // recording folders and ".." folders in subfolders are handled by base class.
+ bReturn = false;
+ }
+ else
+ {
+ switch (m_settings.GetIntValue(CSettings::SETTING_MYVIDEOS_SELECTACTION))
+ {
+ case SELECT_ACTION_CHOOSE:
+ OnPopupMenu(iItem);
+ bReturn = true;
+ break;
+ case SELECT_ACTION_PLAY_OR_RESUME:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().PlayRecording(
+ *item, true /* check resume */);
+ bReturn = true;
+ break;
+ case SELECT_ACTION_RESUME:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().ResumePlayRecording(
+ *item, true /* fall back to play if no resume possible */);
+ bReturn = true;
+ break;
+ case SELECT_ACTION_INFO:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(
+ *item);
+ bReturn = true;
+ break;
+ default:
+ bReturn = false;
+ break;
+ }
+ }
+ break;
+ }
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnPopupMenu(iItem);
+ bReturn = true;
+ break;
+ case ACTION_SHOW_INFO:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(*item);
+ bReturn = true;
+ break;
+ case ACTION_DELETE_ITEM:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteRecording(*item);
+ bReturn = true;
+ break;
+ default:
+ bReturn = false;
+ break;
+ }
+ }
+ }
+ else if (message.GetSenderId() == CONTROL_BTNGROUPITEMS)
+ {
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->ToggleBool(CSettings::SETTING_PVRRECORD_GROUPRECORDINGS);
+ settings->Save();
+ Refresh(true);
+ }
+ else if (message.GetSenderId() == CONTROL_BTNSHOWDELETED)
+ {
+ CGUIRadioButtonControl* radioButton =
+ static_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_BTNSHOWDELETED));
+ if (radioButton)
+ {
+ m_bShowDeletedRecordings = radioButton->IsSelected();
+ Update(GetDirectoryPath());
+ }
+ bReturn = true;
+ }
+ else if (message.GetSenderId() == CONTROL_BTNSHOWMODE)
+ {
+ CMediaSettings::GetInstance().CycleWatchedMode("recordings");
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ OnFilterItems(GetProperty("filter").asString());
+ UpdateButtons();
+ return true;
+ }
+ break;
+ case GUI_MSG_REFRESH_LIST:
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::CurrentItem:
+ case PVREvent::Epg:
+ case PVREvent::EpgActiveItem:
+ case PVREvent::EpgContainer:
+ case PVREvent::Timers:
+ SetInvalid();
+ break;
+
+ case PVREvent::RecordingsInvalidated:
+ case PVREvent::TimersInvalidated:
+ Refresh(true);
+ break;
+
+ default:
+ break;
+ }
+ break;
+ }
+ }
+
+ return bReturn || CGUIWindowPVRBase::OnMessage(message);
+}
+
+bool CGUIWindowPVRRecordingsBase::OnContextButtonDeleteAll(CFileItem* item, CONTEXT_BUTTON button)
+{
+ if (button == CONTEXT_BUTTON_DELETE_ALL)
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().DeleteAllRecordingsFromTrash();
+ return true;
+ }
+
+ return false;
+}
+
+void CGUIWindowPVRRecordingsBase::OnPrepareFileItems(CFileItemList& items)
+{
+ if (items.IsEmpty())
+ return;
+
+ CFileItemList files;
+ for (const auto& item : items)
+ {
+ if (!item->m_bIsFolder)
+ files.Add(item);
+ }
+
+ if (!files.IsEmpty())
+ {
+ if (m_database.Open())
+ {
+ CGUIWindowVideoBase::LoadVideoInfo(files, m_database, false);
+ m_database.Close();
+ }
+ m_thumbLoader.Load(files);
+ }
+
+ CGUIWindowPVRBase::OnPrepareFileItems(items);
+}
+
+bool CGUIWindowPVRRecordingsBase::GetFilteredItems(const std::string& filter, CFileItemList& items)
+{
+ bool listchanged = CGUIWindowPVRBase::GetFilteredItems(filter, items);
+
+ int watchMode = CMediaSettings::GetInstance().GetWatchedMode("recordings");
+
+ CFileItemPtr item;
+ for (int i = 0; i < items.Size(); i++)
+ {
+ item = items.Get(i);
+
+ if (item->IsParentFolder()) // Don't delete the go to parent folder
+ continue;
+
+ if (!item->HasPVRRecordingInfoTag())
+ continue;
+
+ int iPlayCount = item->GetPVRRecordingInfoTag()->GetPlayCount();
+ if ((watchMode == WatchedModeWatched && iPlayCount == 0) ||
+ (watchMode == WatchedModeUnwatched && iPlayCount > 0))
+ {
+ items.Remove(i);
+ i--;
+ listchanged = true;
+ }
+ }
+
+ // Remove the parent folder item, if it's the only item in the folder.
+ if (items.GetObjectCount() == 0 && items.GetFileCount() > 0 && items.Get(0)->IsParentFolder())
+ items.Remove(0);
+
+ return listchanged;
+}
+
+std::string CGUIWindowPVRTVRecordings::GetRootPath() const
+{
+ return CPVRRecordingsPath(m_bShowDeletedRecordings, false);
+}
+
+std::string CGUIWindowPVRRadioRecordings::GetRootPath() const
+{
+ return CPVRRecordingsPath(m_bShowDeletedRecordings, true);
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRRecordings.h b/xbmc/pvr/windows/GUIWindowPVRRecordings.h
new file mode 100644
index 0000000..5091fde
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRRecordings.h
@@ -0,0 +1,71 @@
+/*
+ * 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 "dialogs/GUIDialogContextMenu.h"
+#include "pvr/settings/PVRSettings.h"
+#include "pvr/windows/GUIWindowPVRBase.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoThumbLoader.h"
+
+#include <string>
+
+class CFileItem;
+
+namespace PVR
+{
+class CGUIWindowPVRRecordingsBase : public CGUIWindowPVRBase
+{
+public:
+ CGUIWindowPVRRecordingsBase(bool bRadio, int id, const std::string& xmlFile);
+ ~CGUIWindowPVRRecordingsBase() override;
+
+ void OnWindowLoaded() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void GetContextButtons(int itemNumber, CContextButtons& buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void UpdateButtons() override;
+
+protected:
+ std::string GetDirectoryPath() override;
+ void OnPrepareFileItems(CFileItemList& items) override;
+ bool GetFilteredItems(const std::string& filter, CFileItemList& items) override;
+
+ bool m_bShowDeletedRecordings{false};
+
+private:
+ bool OnContextButtonDeleteAll(CFileItem* item, CONTEXT_BUTTON button);
+
+ CVideoThumbLoader m_thumbLoader;
+ CVideoDatabase m_database;
+ CPVRSettings m_settings;
+};
+
+class CGUIWindowPVRTVRecordings : public CGUIWindowPVRRecordingsBase
+{
+public:
+ CGUIWindowPVRTVRecordings()
+ : CGUIWindowPVRRecordingsBase(false, WINDOW_TV_RECORDINGS, "MyPVRRecordings.xml")
+ {
+ }
+ std::string GetRootPath() const override;
+};
+
+class CGUIWindowPVRRadioRecordings : public CGUIWindowPVRRecordingsBase
+{
+public:
+ CGUIWindowPVRRadioRecordings()
+ : CGUIWindowPVRRecordingsBase(true, WINDOW_RADIO_RECORDINGS, "MyPVRRecordings.xml")
+ {
+ }
+ std::string GetRootPath() const override;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/GUIWindowPVRSearch.cpp b/xbmc/pvr/windows/GUIWindowPVRSearch.cpp
new file mode 100644
index 0000000..f0cd3fe
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRSearch.cpp
@@ -0,0 +1,544 @@
+/*
+ * 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 "GUIWindowPVRSearch.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "pvr/PVRItem.h"
+#include "pvr/PVRManager.h"
+#include "pvr/dialogs/GUIDialogPVRGuideSearch.h"
+#include "pvr/epg/Epg.h"
+#include "pvr/epg/EpgContainer.h"
+#include "pvr/epg/EpgInfoTag.h"
+#include "pvr/epg/EpgSearchFilter.h"
+#include "pvr/epg/EpgSearchPath.h"
+#include "pvr/guilib/PVRGUIActionsEPG.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "threads/IRunnable.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <memory>
+#include <vector>
+
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+namespace
+{
+class AsyncSearchAction : private IRunnable
+{
+public:
+ AsyncSearchAction() = delete;
+ AsyncSearchAction(CFileItemList* items, CPVREpgSearchFilter* filter)
+ : m_items(items), m_filter(filter)
+ {
+ }
+ bool Execute();
+
+private:
+ // IRunnable implementation
+ void Run() override;
+
+ CFileItemList* m_items;
+ CPVREpgSearchFilter* m_filter;
+};
+
+bool AsyncSearchAction::Execute()
+{
+ CGUIDialogBusy::Wait(this, 100, false);
+ return true;
+}
+
+void AsyncSearchAction::Run()
+{
+ std::vector<std::shared_ptr<CPVREpgInfoTag>> results =
+ CServiceBroker::GetPVRManager().EpgContainer().GetTags(m_filter->GetEpgSearchData());
+ m_filter->SetEpgSearchDataFiltered();
+
+ // Tags can still contain false positives, for search criteria that cannot be handled via
+ // database. So, run extended search filters on what we got from the database.
+ for (auto it = results.begin(); it != results.end();)
+ {
+ it = results.erase(std::remove_if(results.begin(), results.end(),
+ [this](const std::shared_ptr<CPVREpgInfoTag>& entry) {
+ return !m_filter->FilterEntry(entry);
+ }),
+ results.end());
+ }
+
+ if (m_filter->ShouldRemoveDuplicates())
+ m_filter->RemoveDuplicates(results);
+
+ m_filter->SetLastExecutedDateTime(CDateTime::GetUTCDateTime());
+
+ for (const auto& tag : results)
+ {
+ m_items->Add(std::make_shared<CFileItem>(tag));
+ }
+}
+} // unnamed namespace
+
+CGUIWindowPVRSearchBase::CGUIWindowPVRSearchBase(bool bRadio, int id, const std::string& xmlFile)
+ : CGUIWindowPVRBase(bRadio, id, xmlFile), m_bSearchConfirmed(false)
+{
+}
+
+CGUIWindowPVRSearchBase::~CGUIWindowPVRSearchBase()
+{
+}
+
+void CGUIWindowPVRSearchBase::GetContextButtons(int itemNumber, CContextButtons& buttons)
+{
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return;
+
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ const bool bIsSavedSearchesRoot = (path.IsValid() && path.IsSavedSearchesRoot());
+ if (!bIsSavedSearchesRoot)
+ buttons.Add(CONTEXT_BUTTON_CLEAR, 19232); // "Clear search results"
+
+ CGUIWindowPVRBase::GetContextButtons(itemNumber, buttons);
+}
+
+bool CGUIWindowPVRSearchBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ if (itemNumber < 0 || itemNumber >= m_vecItems->Size())
+ return false;
+ CFileItemPtr pItem = m_vecItems->Get(itemNumber);
+
+ return OnContextButtonClear(pItem.get(), button) ||
+ CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+void CGUIWindowPVRSearchBase::SetItemToSearch(const CFileItem& item)
+{
+ if (item.HasEPGSearchFilter())
+ {
+ SetSearchFilter(item.GetEPGSearchFilter());
+ }
+ else if (item.IsUsablePVRRecording())
+ {
+ SetSearchFilter(std::make_shared<CPVREpgSearchFilter>(m_bRadio));
+ m_searchfilter->SetSearchPhrase(item.GetPVRRecordingInfoTag()->m_strTitle);
+ }
+ else
+ {
+ SetSearchFilter(std::make_shared<CPVREpgSearchFilter>(m_bRadio));
+
+ const std::shared_ptr<CPVREpgInfoTag> epgTag(CPVRItem(item).GetEpgInfoTag());
+ if (epgTag && !CServiceBroker::GetPVRManager().IsParentalLocked(epgTag))
+ m_searchfilter->SetSearchPhrase(epgTag->Title());
+ }
+
+ ExecuteSearch();
+}
+
+void CGUIWindowPVRSearchBase::OnPrepareFileItems(CFileItemList& items)
+{
+ if (m_bSearchConfirmed)
+ {
+ items.Clear();
+ }
+
+ if (items.IsEmpty())
+ {
+ auto item = std::make_shared<CFileItem>(CPVREpgSearchPath::PATH_SEARCH_DIALOG, false);
+ item->SetLabel(g_localizeStrings.Get(
+ m_searchfilter == nullptr ? 19335 : 19336)); // "New search..." / "Edit search..."
+ item->SetLabelPreformatted(true);
+ item->SetSpecialSort(SortSpecialOnTop);
+ item->SetArt("icon", "DefaultPVRSearch.png");
+ items.Add(item);
+
+ item = std::make_shared<CFileItem>(m_bRadio ? CPVREpgSearchPath::PATH_RADIO_SAVEDSEARCHES
+ : CPVREpgSearchPath::PATH_TV_SAVEDSEARCHES,
+ true);
+ item->SetLabel(g_localizeStrings.Get(19337)); // "Saved searches"
+ item->SetLabelPreformatted(true);
+ item->SetSpecialSort(SortSpecialOnTop);
+ item->SetArt("icon", "DefaultFolder.png");
+ items.Add(item);
+ }
+
+ if (m_bSearchConfirmed)
+ {
+ const int itemCount = items.GetObjectCount();
+
+ AsyncSearchAction(&items, m_searchfilter.get()).Execute();
+
+ if (items.GetObjectCount() == itemCount)
+ {
+ HELPERS::ShowOKDialogText(CVariant{284}, // "No results found"
+ m_searchfilter->GetSearchTerm());
+ }
+ }
+}
+
+bool CGUIWindowPVRSearchBase::OnAction(const CAction& action)
+{
+ if (action.GetID() == ACTION_PARENT_DIR || action.GetID() == ACTION_NAV_BACK)
+ {
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsSavedSearchesRoot())
+ {
+ // Go to root dir and show previous search results if any
+ m_bSearchConfirmed = (m_searchfilter != nullptr);
+ GoParentFolder();
+ return true;
+ }
+ }
+ return CGUIWindowPVRBase::OnAction(action);
+}
+
+bool CGUIWindowPVRSearchBase::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_CLICKED)
+ {
+ if (message.GetSenderId() == m_viewControl.GetCurrentControl())
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+
+ /* process actions */
+ switch (message.GetParam1())
+ {
+ case ACTION_SHOW_INFO:
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ const CPVREpgSearchPath path(pItem->GetPath());
+ const bool bIsSavedSearch = (path.IsValid() && path.IsSavedSearch());
+ const bool bIsSavedSearchesRoot = (path.IsValid() && path.IsSavedSearchesRoot());
+
+ if (message.GetParam1() != ACTION_SHOW_INFO)
+ {
+ if (pItem->IsParentFolder())
+ {
+ // Go to root dir and show previous search results if any
+ m_bSearchConfirmed = (m_searchfilter != nullptr);
+ break; // handled by base class
+ }
+
+ if (bIsSavedSearchesRoot)
+ {
+ // List saved searches
+ m_bSearchConfirmed = false;
+ break; // handled by base class
+ }
+
+ if (bIsSavedSearch)
+ {
+ // Execute selected saved search
+ SetSearchFilter(pItem->GetEPGSearchFilter());
+ ExecuteSearch();
+ return true;
+ }
+ }
+
+ if (bIsSavedSearch)
+ {
+ OpenDialogSearch(*pItem);
+ }
+ else if (pItem->GetPath() == CPVREpgSearchPath::PATH_SEARCH_DIALOG)
+ {
+ OpenDialogSearch(m_searchfilter);
+ }
+ else
+ {
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(*pItem);
+ }
+ return true;
+ }
+
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnPopupMenu(iItem);
+ return true;
+
+ case ACTION_RECORD:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleTimer(*pItem);
+ return true;
+ }
+ }
+ }
+ }
+ else if (message.GetMessage() == GUI_MSG_REFRESH_LIST)
+ {
+ if (static_cast<PVREvent>(message.GetParam1()) == PVREvent::SavedSearchesInvalidated)
+ {
+ Refresh(true);
+
+ // Refresh triggered by deleted saved search?
+ if (m_searchfilter)
+ {
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ const bool bIsSavedSearchesRoot = (path.IsValid() && path.IsSavedSearchesRoot());
+ if (bIsSavedSearchesRoot)
+ {
+ const std::string filterPath = m_searchfilter->GetPath();
+ bool bFound = false;
+ for (const auto& item : *m_vecItems)
+ {
+ const auto filter = item->GetEPGSearchFilter();
+ if (filter && filter->GetPath() == filterPath)
+ {
+ bFound = true;
+ break;
+ }
+ }
+ if (!bFound)
+ SetSearchFilter(nullptr);
+ }
+ }
+ }
+ }
+ else if (message.GetMessage() == GUI_MSG_WINDOW_INIT)
+ {
+ const CPVREpgSearchPath path(message.GetStringParam(0));
+ if (path.IsValid() && path.IsSavedSearch())
+ {
+ const std::shared_ptr<CPVREpgSearchFilter> filter =
+ CServiceBroker::GetPVRManager().EpgContainer().GetSavedSearchById(path.IsRadio(),
+ path.GetId());
+ if (filter)
+ {
+ SetSearchFilter(filter);
+ m_bSearchConfirmed = true;
+ }
+ }
+ }
+
+ return CGUIWindowPVRBase::OnMessage(message);
+}
+
+bool CGUIWindowPVRSearchBase::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ if (m_vecItems->GetObjectCount() > 0)
+ {
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsSavedSearchesRoot())
+ {
+ const std::string oldPath = m_vecItems->GetPath();
+
+ const bool bReturn = CGUIWindowPVRBase::Update(strDirectory);
+
+ if (bReturn && oldPath == m_vecItems->GetPath() && m_vecItems->GetObjectCount() == 0)
+ {
+ // Go to parent folder if we're in a subdir and for instance just deleted the last item
+ GoParentFolder();
+ }
+ return bReturn;
+ }
+ }
+ return CGUIWindowPVRBase::Update(strDirectory);
+}
+
+void CGUIWindowPVRSearchBase::UpdateButtons()
+{
+ CGUIWindowPVRBase::UpdateButtons();
+
+ bool bSavedSearchesRoot = false;
+ std::string header;
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsSavedSearchesRoot())
+ {
+ bSavedSearchesRoot = true;
+ header = g_localizeStrings.Get(19337); // "Saved searches"
+ }
+
+ if (header.empty() && m_searchfilter)
+ {
+ header = m_searchfilter->GetTitle();
+ if (header.empty())
+ {
+ header = m_searchfilter->GetSearchTerm();
+ StringUtils::Trim(header, "\"");
+ }
+ }
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, header);
+
+ if (!bSavedSearchesRoot && m_searchfilter && m_searchfilter->IsChanged())
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, g_localizeStrings.Get(19342)); // "[not saved]"
+ else if (!bSavedSearchesRoot && m_searchfilter)
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, g_localizeStrings.Get(19343)); // "[saved]"
+ else
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER2, "");
+}
+
+bool CGUIWindowPVRSearchBase::OnContextButtonClear(CFileItem* item, CONTEXT_BUTTON button)
+{
+ bool bReturn = false;
+
+ if (button == CONTEXT_BUTTON_CLEAR)
+ {
+ bReturn = true;
+
+ m_bSearchConfirmed = false;
+ SetSearchFilter(nullptr);
+
+ Refresh(true);
+ }
+
+ return bReturn;
+}
+
+CGUIDialogPVRGuideSearch::Result CGUIWindowPVRSearchBase::OpenDialogSearch(const CFileItem& item)
+{
+ const auto searchFilter = item.GetEPGSearchFilter();
+ if (!searchFilter)
+ return CGUIDialogPVRGuideSearch::Result::CANCEL;
+
+ return OpenDialogSearch(searchFilter);
+}
+
+CGUIDialogPVRGuideSearch::Result CGUIWindowPVRSearchBase::OpenDialogSearch(
+ const std::shared_ptr<CPVREpgSearchFilter>& searchFilter)
+{
+ CGUIDialogPVRGuideSearch* dlgSearch =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGuideSearch>(
+ WINDOW_DIALOG_PVR_GUIDE_SEARCH);
+
+ if (!dlgSearch)
+ return CGUIDialogPVRGuideSearch::Result::CANCEL;
+
+ const std::shared_ptr<CPVREpgSearchFilter> tmpSearchFilter =
+ searchFilter != nullptr ? std::make_shared<CPVREpgSearchFilter>(*searchFilter)
+ : std::make_shared<CPVREpgSearchFilter>(m_bRadio);
+
+ dlgSearch->SetFilterData(tmpSearchFilter);
+
+ /* Open dialog window */
+ dlgSearch->Open();
+
+ const CGUIDialogPVRGuideSearch::Result result = dlgSearch->GetResult();
+ if (result == CGUIDialogPVRGuideSearch::Result::SEARCH)
+ {
+ SetSearchFilter(tmpSearchFilter);
+ ExecuteSearch();
+ }
+ else if (result == CGUIDialogPVRGuideSearch::Result::SAVE)
+ {
+ CServiceBroker::GetPVRManager().EpgContainer().PersistSavedSearch(*tmpSearchFilter);
+ if (searchFilter)
+ searchFilter->SetDatabaseId(tmpSearchFilter->GetDatabaseId());
+
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsSearchRoot())
+ {
+ SetSearchFilter(tmpSearchFilter);
+ ExecuteSearch();
+ }
+ }
+
+ return result;
+}
+
+void CGUIWindowPVRSearchBase::ExecuteSearch()
+{
+ m_bSearchConfirmed = true;
+
+ const CPVREpgSearchPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsSavedSearchesRoot())
+ {
+ GoParentFolder();
+ }
+ else if (IsActive())
+ {
+ Refresh(true);
+ }
+
+ // Save if not a transient search
+ if (m_searchfilter->GetDatabaseId() != -1)
+ CServiceBroker::GetPVRManager().EpgContainer().UpdateSavedSearchLastExecuted(*m_searchfilter);
+}
+
+void CGUIWindowPVRSearchBase::SetSearchFilter(
+ const std::shared_ptr<CPVREpgSearchFilter>& searchFilter)
+{
+ if (m_searchfilter && m_searchfilter->IsChanged() &&
+ (!searchFilter || m_searchfilter->GetPath() != searchFilter->GetPath()))
+ {
+ bool bCanceled = false;
+ if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{14117}, // "Warning"
+ CVariant{19341}, // "Save the current search?"
+ CVariant{},
+ CVariant{!m_searchfilter->GetTitle().empty()
+ ? m_searchfilter->GetTitle()
+ : m_searchfilter->GetSearchTerm()},
+ bCanceled, CVariant{107}, // "Yes"
+ CVariant{106}, // "No"
+ CGUIDialogYesNo::NO_TIMEOUT) &&
+ !bCanceled)
+ {
+ std::string title = m_searchfilter->GetTitle();
+ if (title.empty())
+ {
+ title = m_searchfilter->GetSearchTerm();
+ if (title.empty())
+ title = g_localizeStrings.Get(137); // "Search"
+ else
+ StringUtils::Trim(title, "\"");
+
+ m_searchfilter->SetTitle(title);
+ }
+ CServiceBroker::GetPVRManager().EpgContainer().PersistSavedSearch(*m_searchfilter);
+ }
+ }
+ m_searchfilter = searchFilter;
+}
+
+std::string CGUIWindowPVRTVSearch::GetRootPath() const
+{
+ return CPVREpgSearchPath::PATH_TV_SEARCH;
+}
+
+std::string CGUIWindowPVRTVSearch::GetStartFolder(const std::string& dir)
+{
+ return CPVREpgSearchPath::PATH_TV_SEARCH;
+}
+
+std::string CGUIWindowPVRTVSearch::GetDirectoryPath()
+{
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), CPVREpgSearchPath::PATH_TV_SEARCH)
+ ? m_vecItems->GetPath()
+ : CPVREpgSearchPath::PATH_TV_SEARCH;
+}
+
+std::string CGUIWindowPVRRadioSearch::GetRootPath() const
+{
+ return CPVREpgSearchPath::PATH_RADIO_SEARCH;
+}
+
+std::string CGUIWindowPVRRadioSearch::GetStartFolder(const std::string& dir)
+{
+ return CPVREpgSearchPath::PATH_RADIO_SEARCH;
+}
+
+std::string CGUIWindowPVRRadioSearch::GetDirectoryPath()
+{
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), CPVREpgSearchPath::PATH_RADIO_SEARCH)
+ ? m_vecItems->GetPath()
+ : CPVREpgSearchPath::PATH_RADIO_SEARCH;
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRSearch.h b/xbmc/pvr/windows/GUIWindowPVRSearch.h
new file mode 100644
index 0000000..d6f6a35
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRSearch.h
@@ -0,0 +1,90 @@
+/*
+ * 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 "dialogs/GUIDialogContextMenu.h"
+#include "pvr/dialogs/GUIDialogPVRGuideSearch.h"
+#include "pvr/windows/GUIWindowPVRBase.h"
+
+#include <memory>
+#include <string>
+
+class CFileItem;
+
+namespace PVR
+{
+class CPVREpgSearchFilter;
+
+class CGUIWindowPVRSearchBase : public CGUIWindowPVRBase
+{
+public:
+ CGUIWindowPVRSearchBase(bool bRadio, int id, const std::string& xmlFile);
+ ~CGUIWindowPVRSearchBase() override;
+
+ bool OnAction(const CAction& action) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void GetContextButtons(int itemNumber, CContextButtons& buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void UpdateButtons() override;
+
+ /*!
+ * @brief set the item to search similar events for.
+ * @param item the epg event to search similar events for.
+ */
+ void SetItemToSearch(const CFileItem& item);
+
+ /*!
+ * @brief Open the search dialog for the given search filter item.
+ * @param item the epg search filter.
+ * @return The result of the dialog
+ */
+ CGUIDialogPVRGuideSearch::Result OpenDialogSearch(const CFileItem& item);
+
+protected:
+ void OnPrepareFileItems(CFileItemList& items) override;
+
+private:
+ bool OnContextButtonClear(CFileItem* item, CONTEXT_BUTTON button);
+
+ CGUIDialogPVRGuideSearch::Result OpenDialogSearch(
+ const std::shared_ptr<CPVREpgSearchFilter>& searchFilter);
+
+ void ExecuteSearch();
+
+ void SetSearchFilter(const std::shared_ptr<CPVREpgSearchFilter>& searchFilter);
+
+ bool m_bSearchConfirmed;
+ std::shared_ptr<CPVREpgSearchFilter> m_searchfilter;
+};
+
+class CGUIWindowPVRTVSearch : public CGUIWindowPVRSearchBase
+{
+public:
+ CGUIWindowPVRTVSearch() : CGUIWindowPVRSearchBase(false, WINDOW_TV_SEARCH, "MyPVRSearch.xml") {}
+
+protected:
+ std::string GetRootPath() const override;
+ std::string GetStartFolder(const std::string& dir) override;
+ std::string GetDirectoryPath() override;
+};
+
+class CGUIWindowPVRRadioSearch : public CGUIWindowPVRSearchBase
+{
+public:
+ CGUIWindowPVRRadioSearch() : CGUIWindowPVRSearchBase(true, WINDOW_RADIO_SEARCH, "MyPVRSearch.xml")
+ {
+ }
+
+protected:
+ std::string GetRootPath() const override;
+ std::string GetStartFolder(const std::string& dir) override;
+ std::string GetDirectoryPath() override;
+};
+} // namespace PVR
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimerRules.cpp b/xbmc/pvr/windows/GUIWindowPVRTimerRules.cpp
new file mode 100644
index 0000000..1b203d4
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimerRules.cpp
@@ -0,0 +1,47 @@
+/*
+ * 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 "GUIWindowPVRTimerRules.h"
+
+#include "FileItem.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "utils/URIUtils.h"
+
+using namespace PVR;
+
+CGUIWindowPVRTVTimerRules::CGUIWindowPVRTVTimerRules()
+: CGUIWindowPVRTimersBase(false, WINDOW_TV_TIMER_RULES, "MyPVRTimers.xml")
+{
+}
+
+std::string CGUIWindowPVRTVTimerRules::GetRootPath() const
+{
+ return CPVRTimersPath::PATH_TV_TIMER_RULES;
+}
+
+std::string CGUIWindowPVRTVTimerRules::GetDirectoryPath()
+{
+ const std::string basePath(CPVRTimersPath(false, true).GetPath());
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath;
+}
+
+CGUIWindowPVRRadioTimerRules::CGUIWindowPVRRadioTimerRules()
+: CGUIWindowPVRTimersBase(true, WINDOW_RADIO_TIMER_RULES, "MyPVRTimers.xml")
+{
+}
+
+std::string CGUIWindowPVRRadioTimerRules::GetRootPath() const
+{
+ return CPVRTimersPath::PATH_RADIO_TIMER_RULES;
+}
+
+std::string CGUIWindowPVRRadioTimerRules::GetDirectoryPath()
+{
+ const std::string basePath(CPVRTimersPath(true, true).GetPath());
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath;
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimerRules.h b/xbmc/pvr/windows/GUIWindowPVRTimerRules.h
new file mode 100644
index 0000000..dc3f050
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimerRules.h
@@ -0,0 +1,38 @@
+/*
+ * 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 "pvr/windows/GUIWindowPVRTimersBase.h"
+
+#include <string>
+
+namespace PVR
+{
+ class CGUIWindowPVRTVTimerRules : public CGUIWindowPVRTimersBase
+ {
+ public:
+ CGUIWindowPVRTVTimerRules();
+ ~CGUIWindowPVRTVTimerRules() override = default;
+
+ protected:
+ std::string GetRootPath() const override;
+ std::string GetDirectoryPath() override;
+ };
+
+ class CGUIWindowPVRRadioTimerRules : public CGUIWindowPVRTimersBase
+ {
+ public:
+ CGUIWindowPVRRadioTimerRules();
+ ~CGUIWindowPVRRadioTimerRules() override = default;
+
+ protected:
+ std::string GetRootPath() const override;
+ std::string GetDirectoryPath() override;
+ };
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimers.cpp b/xbmc/pvr/windows/GUIWindowPVRTimers.cpp
new file mode 100644
index 0000000..610571b
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimers.cpp
@@ -0,0 +1,47 @@
+/*
+ * 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 "GUIWindowPVRTimers.h"
+
+#include "FileItem.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "utils/URIUtils.h"
+
+using namespace PVR;
+
+CGUIWindowPVRTVTimers::CGUIWindowPVRTVTimers()
+: CGUIWindowPVRTimersBase(false, WINDOW_TV_TIMERS, "MyPVRTimers.xml")
+{
+}
+
+std::string CGUIWindowPVRTVTimers::GetRootPath() const
+{
+ return CPVRTimersPath::PATH_TV_TIMERS;
+}
+
+std::string CGUIWindowPVRTVTimers::GetDirectoryPath()
+{
+ const std::string basePath(CPVRTimersPath(false, false).GetPath());
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath;
+}
+
+CGUIWindowPVRRadioTimers::CGUIWindowPVRRadioTimers()
+: CGUIWindowPVRTimersBase(true, WINDOW_RADIO_TIMERS, "MyPVRTimers.xml")
+{
+}
+
+std::string CGUIWindowPVRRadioTimers::GetRootPath() const
+{
+ return CPVRTimersPath::PATH_RADIO_TIMERS;
+}
+
+std::string CGUIWindowPVRRadioTimers::GetDirectoryPath()
+{
+ const std::string basePath(CPVRTimersPath(true, false).GetPath());
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), basePath) ? m_vecItems->GetPath() : basePath;
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimers.h b/xbmc/pvr/windows/GUIWindowPVRTimers.h
new file mode 100644
index 0000000..2193ac1
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimers.h
@@ -0,0 +1,36 @@
+/*
+ * 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 "pvr/windows/GUIWindowPVRTimersBase.h"
+
+#include <string>
+
+namespace PVR
+{
+ class CGUIWindowPVRTVTimers : public CGUIWindowPVRTimersBase
+ {
+ public:
+ CGUIWindowPVRTVTimers();
+
+ protected:
+ std::string GetRootPath() const override;
+ std::string GetDirectoryPath() override;
+ };
+
+ class CGUIWindowPVRRadioTimers : public CGUIWindowPVRTimersBase
+ {
+ public:
+ CGUIWindowPVRRadioTimers();
+
+ protected:
+ std::string GetRootPath() const override;
+ std::string GetDirectoryPath() override;
+ };
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimersBase.cpp b/xbmc/pvr/windows/GUIWindowPVRTimersBase.cpp
new file mode 100644
index 0000000..41677eb
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimersBase.cpp
@@ -0,0 +1,204 @@
+/*
+ * 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 "GUIWindowPVRTimersBase.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsTimers.h"
+#include "pvr/timers/PVRTimerInfoTag.h"
+#include "pvr/timers/PVRTimersPath.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+
+#include <memory>
+#include <string>
+
+using namespace PVR;
+
+CGUIWindowPVRTimersBase::CGUIWindowPVRTimersBase(bool bRadio, int id, const std::string& xmlFile)
+ : CGUIWindowPVRBase(bRadio, id, xmlFile)
+{
+}
+
+CGUIWindowPVRTimersBase::~CGUIWindowPVRTimersBase() = default;
+
+bool CGUIWindowPVRTimersBase::OnAction(const CAction& action)
+{
+ if (action.GetID() == ACTION_PARENT_DIR || action.GetID() == ACTION_NAV_BACK)
+ {
+ CPVRTimersPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsTimerRule())
+ {
+ m_currentFileItem.reset();
+ GoParentFolder();
+ return true;
+ }
+ }
+ return CGUIWindowPVRBase::OnAction(action);
+}
+
+void CGUIWindowPVRTimersBase::OnPrepareFileItems(CFileItemList& items)
+{
+ const CPVRTimersPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsTimersRoot())
+ {
+ const auto item = std::make_shared<CFileItem>(CPVRTimersPath::PATH_ADDTIMER, false);
+ item->SetLabel(g_localizeStrings.Get(19026)); // "Add timer..."
+ item->SetLabelPreformatted(true);
+ item->SetSpecialSort(SortSpecialOnTop);
+ item->SetArt("icon", "DefaultTVShows.png");
+
+ items.AddFront(item, 0);
+ }
+}
+
+bool CGUIWindowPVRTimersBase::Update(const std::string& strDirectory,
+ bool updateFilterPath /* = true */)
+{
+ int iOldCount = m_vecItems->GetObjectCount();
+ const std::string oldPath = m_vecItems->GetPath();
+
+ bool bReturn = CGUIWindowPVRBase::Update(strDirectory);
+
+ if (bReturn && iOldCount > 0 && m_vecItems->GetObjectCount() == 0 &&
+ oldPath == m_vecItems->GetPath())
+ {
+ /* go to the parent folder if we're in a subdirectory and for instance just deleted the last item */
+ const CPVRTimersPath path(m_vecItems->GetPath());
+ if (path.IsValid() && path.IsTimerRule())
+ {
+ m_currentFileItem.reset();
+ GoParentFolder();
+ }
+ }
+
+ return bReturn;
+}
+
+void CGUIWindowPVRTimersBase::UpdateButtons()
+{
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTNHIDEDISABLEDTIMERS,
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_PVRTIMERS_HIDEDISABLEDTIMERS));
+
+ CGUIWindowPVRBase::UpdateButtons();
+
+ std::string strHeaderTitle;
+ if (m_currentFileItem && m_currentFileItem->HasPVRTimerInfoTag())
+ {
+ std::shared_ptr<CPVRTimerInfoTag> timer = m_currentFileItem->GetPVRTimerInfoTag();
+ strHeaderTitle = timer->Title();
+ }
+
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER1, strHeaderTitle);
+}
+
+bool CGUIWindowPVRTimersBase::OnMessage(CGUIMessage& message)
+{
+ bool bReturn = false;
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ if (message.GetSenderId() == m_viewControl.GetCurrentControl())
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ bReturn = true;
+ switch (message.GetParam1())
+ {
+ case ACTION_SHOW_INFO:
+ case ACTION_SELECT_ITEM:
+ case ACTION_MOUSE_LEFT_CLICK:
+ {
+ CFileItemPtr item(m_vecItems->Get(iItem));
+ if (item->m_bIsFolder && (message.GetParam1() != ACTION_SHOW_INFO))
+ {
+ m_currentFileItem = item;
+ bReturn = false; // folders are handled by base class
+ }
+ else
+ {
+ m_currentFileItem.reset();
+ ActionShowTimer(*item);
+ }
+ break;
+ }
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ OnPopupMenu(iItem);
+ break;
+ case ACTION_DELETE_ITEM:
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().DeleteTimer(
+ *(m_vecItems->Get(iItem)));
+ break;
+ default:
+ bReturn = false;
+ break;
+ }
+ }
+ }
+ else if (message.GetSenderId() == CONTROL_BTNHIDEDISABLEDTIMERS)
+ {
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->ToggleBool(CSettings::SETTING_PVRTIMERS_HIDEDISABLEDTIMERS);
+ settings->Save();
+ Update(GetDirectoryPath());
+ bReturn = true;
+ }
+ break;
+ case GUI_MSG_REFRESH_LIST:
+ {
+ switch (static_cast<PVREvent>(message.GetParam1()))
+ {
+ case PVREvent::CurrentItem:
+ case PVREvent::Epg:
+ case PVREvent::EpgActiveItem:
+ case PVREvent::EpgContainer:
+ case PVREvent::Timers:
+ SetInvalid();
+ break;
+
+ case PVREvent::TimersInvalidated:
+ Refresh(true);
+ break;
+
+ default:
+ break;
+ }
+ break;
+ }
+ }
+
+ return bReturn || CGUIWindowPVRBase::OnMessage(message);
+}
+
+bool CGUIWindowPVRTimersBase::ActionShowTimer(const CFileItem& item)
+{
+ bool bReturn = false;
+
+ /* Check if "Add timer..." entry is selected, if yes
+ create a new timer and open settings dialog, otherwise
+ open settings for selected timer entry */
+ if (URIUtils::PathEquals(item.GetPath(), CPVRTimersPath::PATH_ADDTIMER))
+ bReturn = CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AddTimer(m_bRadio);
+ else
+ bReturn = CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().EditTimer(item);
+
+ return bReturn;
+}
diff --git a/xbmc/pvr/windows/GUIWindowPVRTimersBase.h b/xbmc/pvr/windows/GUIWindowPVRTimersBase.h
new file mode 100644
index 0000000..2f4232d
--- /dev/null
+++ b/xbmc/pvr/windows/GUIWindowPVRTimersBase.h
@@ -0,0 +1,36 @@
+/*
+ * 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 "pvr/windows/GUIWindowPVRBase.h"
+
+#include <memory>
+
+class CFileItem;
+
+namespace PVR
+{
+class CGUIWindowPVRTimersBase : public CGUIWindowPVRBase
+{
+public:
+ CGUIWindowPVRTimersBase(bool bRadio, int id, const std::string& xmlFile);
+ ~CGUIWindowPVRTimersBase() override;
+
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void OnPrepareFileItems(CFileItemList& items) override;
+ bool Update(const std::string& strDirectory, bool updateFilterPath = true) override;
+ void UpdateButtons() override;
+
+private:
+ bool ActionShowTimer(const CFileItem& item);
+
+ std::shared_ptr<CFileItem> m_currentFileItem;
+};
+} // namespace PVR
diff --git a/xbmc/rendering/CMakeLists.txt b/xbmc/rendering/CMakeLists.txt
new file mode 100644
index 0000000..c212a96
--- /dev/null
+++ b/xbmc/rendering/CMakeLists.txt
@@ -0,0 +1,27 @@
+set(SOURCES RenderSystem.cpp)
+
+set(HEADERS RenderSystem.h
+ RenderSystemTypes.h)
+
+if(OPENGL_FOUND OR OPENGLES_FOUND)
+ list(APPEND SOURCES MatrixGL.cpp)
+ list(APPEND HEADERS MatrixGL.h)
+
+ if(ARCH MATCHES arm AND ENABLE_NEON)
+ list(APPEND SOURCES MatrixGL.neon.cpp)
+ if(NOT DEFINED NEON_FLAGS)
+ set_source_files_properties(MatrixGL.neon.cpp PROPERTIES COMPILE_OPTIONS -mfpu=neon)
+ endif()
+ endif()
+endif()
+
+core_add_library(rendering)
+if(NOT CORE_SYSTEM_NAME STREQUAL windows AND NOT CORE_SYSTEM_NAME STREQUAL windowsstore)
+ if(HAVE_SSE)
+ target_compile_options(${CORE_LIBRARY} PRIVATE -msse)
+ endif()
+ if(HAVE_SSE2)
+ target_compile_options(${CORE_LIBRARY} PRIVATE -msse2)
+ endif()
+endif()
+
diff --git a/xbmc/rendering/MatrixGL.cpp b/xbmc/rendering/MatrixGL.cpp
new file mode 100644
index 0000000..6e90383
--- /dev/null
+++ b/xbmc/rendering/MatrixGL.cpp
@@ -0,0 +1,261 @@
+/*
+ * 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 "MatrixGL.h"
+
+#include "ServiceBroker.h"
+#include "utils/TransformMatrix.h"
+
+#if defined(HAS_NEON) && !defined(__LP64__)
+#include "utils/CPUInfo.h"
+void Matrix4Mul(float* src_mat_1, const float* src_mat_2);
+#endif
+
+#include <cmath>
+
+CMatrixGLStack glMatrixModview = CMatrixGLStack();
+CMatrixGLStack glMatrixProject = CMatrixGLStack();
+CMatrixGLStack glMatrixTexture = CMatrixGLStack();
+
+CMatrixGL::CMatrixGL(const TransformMatrix &src) noexcept
+{
+ for(int i = 0; i < 3; i++)
+ for(int j = 0; j < 4; j++)
+ m_pMatrix[j * 4 + i] = src.m[i][j];
+
+ m_pMatrix[3] = 0.0f;
+ m_pMatrix[7] = 0.0f;
+ m_pMatrix[11] = 0.0f;
+ m_pMatrix[15] = 1.0f;
+}
+
+void CMatrixGL::LoadIdentity()
+{
+ m_pMatrix[0] = 1.0f; m_pMatrix[4] = 0.0f; m_pMatrix[8] = 0.0f; m_pMatrix[12] = 0.0f;
+ m_pMatrix[1] = 0.0f; m_pMatrix[5] = 1.0f; m_pMatrix[9] = 0.0f; m_pMatrix[13] = 0.0f;
+ m_pMatrix[2] = 0.0f; m_pMatrix[6] = 0.0f; m_pMatrix[10] = 1.0f; m_pMatrix[14] = 0.0f;
+ m_pMatrix[3] = 0.0f; m_pMatrix[7] = 0.0f; m_pMatrix[11] = 0.0f; m_pMatrix[15] = 1.0f;
+}
+
+void CMatrixGL::Ortho(GLfloat l, GLfloat r, GLfloat b, GLfloat t, GLfloat n, GLfloat f)
+{
+ GLfloat u = 2.0f / (r - l);
+ GLfloat v = 2.0f / (t - b);
+ GLfloat w = -2.0f / (f - n);
+ GLfloat x = - (r + l) / (r - l);
+ GLfloat y = - (t + b) / (t - b);
+ GLfloat z = - (f + n) / (f - n);
+ const CMatrixGL matrix{ u, 0.0f, 0.0f, 0.0f,
+ 0.0f, v, 0.0f, 0.0f,
+ 0.0f, 0.0f, w, 0.0f,
+ x, y, z, 1.0f};
+ MultMatrixf(matrix);
+}
+
+void CMatrixGL::Ortho2D(GLfloat l, GLfloat r, GLfloat b, GLfloat t)
+{
+ GLfloat u = 2.0f / (r - l);
+ GLfloat v = 2.0f / (t - b);
+ GLfloat x = - (r + l) / (r - l);
+ GLfloat y = - (t + b) / (t - b);
+ const CMatrixGL matrix{ u, 0.0f, 0.0f, 0.0f,
+ 0.0f, v, 0.0f, 0.0f,
+ 0.0f, 0.0f,-1.0f, 0.0f,
+ x, y, 0.0f, 1.0f};
+ MultMatrixf(matrix);
+}
+
+void CMatrixGL::Frustum(GLfloat l, GLfloat r, GLfloat b, GLfloat t, GLfloat n, GLfloat f)
+{
+ GLfloat u = (2.0f * n) / (r - l);
+ GLfloat v = (2.0f * n) / (t - b);
+ GLfloat w = (r + l) / (r - l);
+ GLfloat x = (t + b) / (t - b);
+ GLfloat y = - (f + n) / (f - n);
+ GLfloat z = - (2.0f * f * n) / (f - n);
+ const CMatrixGL matrix{ u, 0.0f, 0.0f, 0.0f,
+ 0.0f, v, 0.0f, 0.0f,
+ w, x, y,-1.0f,
+ 0.0f, 0.0f, z, 0.0f};
+ MultMatrixf(matrix);
+}
+
+void CMatrixGL::Translatef(GLfloat x, GLfloat y, GLfloat z)
+{
+ const CMatrixGL matrix{1.0f, 0.0f, 0.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f, 0.0f,
+ 0.0f, 0.0f, 1.0f, 0.0f,
+ x, y, z, 1.0f};
+ MultMatrixf(matrix);
+}
+
+void CMatrixGL::Scalef(GLfloat x, GLfloat y, GLfloat z)
+{
+ const CMatrixGL matrix{ x, 0.0f, 0.0f, 0.0f,
+ 0.0f, y, 0.0f, 0.0f,
+ 0.0f, 0.0f, z, 0.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f};
+ MultMatrixf(matrix);
+}
+
+void CMatrixGL::Rotatef(GLfloat angle, GLfloat x, GLfloat y, GLfloat z)
+{
+ GLfloat modulus = std::sqrt((x*x)+(y*y)+(z*z));
+ if (modulus != 0.0f)
+ {
+ x /= modulus;
+ y /= modulus;
+ z /= modulus;
+ }
+ GLfloat cosine = std::cos(angle);
+ GLfloat sine = std::sin(angle);
+ GLfloat cos1 = 1 - cosine;
+ GLfloat a = (x*x*cos1) + cosine;
+ GLfloat b = (x*y*cos1) - (z*sine);
+ GLfloat c = (x*z*cos1) + (y*sine);
+ GLfloat d = (y*x*cos1) + (z*sine);
+ GLfloat e = (y*y*cos1) + cosine;
+ GLfloat f = (y*z*cos1) - (x*sine);
+ GLfloat g = (z*x*cos1) - (y*sine);
+ GLfloat h = (z*y*cos1) + (x*sine);
+ GLfloat i = (z*z*cos1) + cosine;
+ const CMatrixGL matrix{ a, d, g, 0.0f,
+ b, e, h, 0.0f,
+ c, f, i, 0.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f};
+ MultMatrixf(matrix);
+}
+
+void CMatrixGL::MultMatrixf(const CMatrixGL &matrix) noexcept
+{
+#if defined(HAS_NEON) && !defined(__LP64__)
+ if ((CServiceBroker::GetCPUInfo()->GetCPUFeatures() & CPU_FEATURE_NEON) == CPU_FEATURE_NEON)
+ {
+ Matrix4Mul(m_pMatrix, matrix.m_pMatrix);
+ return;
+ }
+#endif
+ GLfloat a = (matrix.m_pMatrix[0] * m_pMatrix[0]) + (matrix.m_pMatrix[1] * m_pMatrix[4]) + (matrix.m_pMatrix[2] * m_pMatrix[8]) + (matrix.m_pMatrix[3] * m_pMatrix[12]);
+ GLfloat b = (matrix.m_pMatrix[0] * m_pMatrix[1]) + (matrix.m_pMatrix[1] * m_pMatrix[5]) + (matrix.m_pMatrix[2] * m_pMatrix[9]) + (matrix.m_pMatrix[3] * m_pMatrix[13]);
+ GLfloat c = (matrix.m_pMatrix[0] * m_pMatrix[2]) + (matrix.m_pMatrix[1] * m_pMatrix[6]) + (matrix.m_pMatrix[2] * m_pMatrix[10]) + (matrix.m_pMatrix[3] * m_pMatrix[14]);
+ GLfloat d = (matrix.m_pMatrix[0] * m_pMatrix[3]) + (matrix.m_pMatrix[1] * m_pMatrix[7]) + (matrix.m_pMatrix[2] * m_pMatrix[11]) + (matrix.m_pMatrix[3] * m_pMatrix[15]);
+ GLfloat e = (matrix.m_pMatrix[4] * m_pMatrix[0]) + (matrix.m_pMatrix[5] * m_pMatrix[4]) + (matrix.m_pMatrix[6] * m_pMatrix[8]) + (matrix.m_pMatrix[7] * m_pMatrix[12]);
+ GLfloat f = (matrix.m_pMatrix[4] * m_pMatrix[1]) + (matrix.m_pMatrix[5] * m_pMatrix[5]) + (matrix.m_pMatrix[6] * m_pMatrix[9]) + (matrix.m_pMatrix[7] * m_pMatrix[13]);
+ GLfloat g = (matrix.m_pMatrix[4] * m_pMatrix[2]) + (matrix.m_pMatrix[5] * m_pMatrix[6]) + (matrix.m_pMatrix[6] * m_pMatrix[10]) + (matrix.m_pMatrix[7] * m_pMatrix[14]);
+ GLfloat h = (matrix.m_pMatrix[4] * m_pMatrix[3]) + (matrix.m_pMatrix[5] * m_pMatrix[7]) + (matrix.m_pMatrix[6] * m_pMatrix[11]) + (matrix.m_pMatrix[7] * m_pMatrix[15]);
+ GLfloat i = (matrix.m_pMatrix[8] * m_pMatrix[0]) + (matrix.m_pMatrix[9] * m_pMatrix[4]) + (matrix.m_pMatrix[10] * m_pMatrix[8]) + (matrix.m_pMatrix[11] * m_pMatrix[12]);
+ GLfloat j = (matrix.m_pMatrix[8] * m_pMatrix[1]) + (matrix.m_pMatrix[9] * m_pMatrix[5]) + (matrix.m_pMatrix[10] * m_pMatrix[9]) + (matrix.m_pMatrix[11] * m_pMatrix[13]);
+ GLfloat k = (matrix.m_pMatrix[8] * m_pMatrix[2]) + (matrix.m_pMatrix[9] * m_pMatrix[6]) + (matrix.m_pMatrix[10] * m_pMatrix[10]) + (matrix.m_pMatrix[11] * m_pMatrix[14]);
+ GLfloat l = (matrix.m_pMatrix[8] * m_pMatrix[3]) + (matrix.m_pMatrix[9] * m_pMatrix[7]) + (matrix.m_pMatrix[10] * m_pMatrix[11]) + (matrix.m_pMatrix[11] * m_pMatrix[15]);
+ GLfloat m = (matrix.m_pMatrix[12] * m_pMatrix[0]) + (matrix.m_pMatrix[13] * m_pMatrix[4]) + (matrix.m_pMatrix[14] * m_pMatrix[8]) + (matrix.m_pMatrix[15] * m_pMatrix[12]);
+ GLfloat n = (matrix.m_pMatrix[12] * m_pMatrix[1]) + (matrix.m_pMatrix[13] * m_pMatrix[5]) + (matrix.m_pMatrix[14] * m_pMatrix[9]) + (matrix.m_pMatrix[15] * m_pMatrix[13]);
+ GLfloat o = (matrix.m_pMatrix[12] * m_pMatrix[2]) + (matrix.m_pMatrix[13] * m_pMatrix[6]) + (matrix.m_pMatrix[14] * m_pMatrix[10]) + (matrix.m_pMatrix[15] * m_pMatrix[14]);
+ GLfloat p = (matrix.m_pMatrix[12] * m_pMatrix[3]) + (matrix.m_pMatrix[13] * m_pMatrix[7]) + (matrix.m_pMatrix[14] * m_pMatrix[11]) + (matrix.m_pMatrix[15] * m_pMatrix[15]);
+ m_pMatrix[0] = a; m_pMatrix[4] = e; m_pMatrix[8] = i; m_pMatrix[12] = m;
+ m_pMatrix[1] = b; m_pMatrix[5] = f; m_pMatrix[9] = j; m_pMatrix[13] = n;
+ m_pMatrix[2] = c; m_pMatrix[6] = g; m_pMatrix[10] = k; m_pMatrix[14] = o;
+ m_pMatrix[3] = d; m_pMatrix[7] = h; m_pMatrix[11] = l; m_pMatrix[15] = p;
+}
+
+// gluLookAt implementation taken from Mesa3D
+void CMatrixGL::LookAt(GLfloat eyex, GLfloat eyey, GLfloat eyez, GLfloat centerx, GLfloat centery, GLfloat centerz, GLfloat upx, GLfloat upy, GLfloat upz)
+{
+ GLfloat forward[3], side[3], up[3];
+
+ forward[0] = centerx - eyex;
+ forward[1] = centery - eyey;
+ forward[2] = centerz - eyez;
+
+ up[0] = upx;
+ up[1] = upy;
+ up[2] = upz;
+
+ GLfloat tmp = std::sqrt(forward[0]*forward[0] + forward[1]*forward[1] + forward[2]*forward[2]);
+ if (tmp != 0.0f)
+ {
+ forward[0] /= tmp;
+ forward[1] /= tmp;
+ forward[2] /= tmp;
+ }
+
+ side[0] = forward[1]*up[2] - forward[2]*up[1];
+ side[1] = forward[2]*up[0] - forward[0]*up[2];
+ side[2] = forward[0]*up[1] - forward[1]*up[0];
+
+ tmp = std::sqrt(side[0]*side[0] + side[1]*side[1] + side[2]*side[2]);
+ if (tmp != 0.0f)
+ {
+ side[0] /= tmp;
+ side[1] /= tmp;
+ side[2] /= tmp;
+ }
+
+ up[0] = side[1]*forward[2] - side[2]*forward[1];
+ up[1] = side[2]*forward[0] - side[0]*forward[2];
+ up[2] = side[0]*forward[1] - side[1]*forward[0];
+
+ const CMatrixGL matrix{
+ side[0], up[0], -forward[0], 0.0f,
+ side[1], up[1], -forward[1], 0.0f,
+ side[2], up[2], -forward[2], 0.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f,
+ };
+
+ MultMatrixf(matrix);
+ Translatef(-eyex, -eyey, -eyez);
+}
+
+static void __gluMultMatrixVecf(const GLfloat matrix[16], const GLfloat in[4], GLfloat out[4])
+{
+ int i;
+
+ for (i=0; i<4; i++)
+ {
+ out[i] = in[0] * matrix[0*4+i] +
+ in[1] * matrix[1*4+i] +
+ in[2] * matrix[2*4+i] +
+ in[3] * matrix[3*4+i];
+ }
+}
+
+// gluProject implementation taken from Mesa3D
+bool CMatrixGL::Project(GLfloat objx, GLfloat objy, GLfloat objz, const GLfloat modelMatrix[16], const GLfloat projMatrix[16], const GLint viewport[4], GLfloat* winx, GLfloat* winy, GLfloat* winz)
+{
+ GLfloat in[4];
+ GLfloat out[4];
+
+ in[0]=objx;
+ in[1]=objy;
+ in[2]=objz;
+ in[3]=1.0;
+ __gluMultMatrixVecf(modelMatrix, in, out);
+ __gluMultMatrixVecf(projMatrix, out, in);
+ if (in[3] == 0.0f)
+ return false;
+ in[0] /= in[3];
+ in[1] /= in[3];
+ in[2] /= in[3];
+ /* Map x, y and z to range 0-1 */
+ in[0] = in[0] * 0.5f + 0.5f;
+ in[1] = in[1] * 0.5f + 0.5f;
+ in[2] = in[2] * 0.5f + 0.5f;
+
+ /* Map x,y to viewport */
+ in[0] = in[0] * viewport[2] + viewport[0];
+ in[1] = in[1] * viewport[3] + viewport[1];
+
+ *winx=in[0];
+ *winy=in[1];
+ *winz=in[2];
+ return true;
+}
+
+void CMatrixGLStack::Load()
+{
+
+}
diff --git a/xbmc/rendering/MatrixGL.h b/xbmc/rendering/MatrixGL.h
new file mode 100644
index 0000000..333da2f
--- /dev/null
+++ b/xbmc/rendering/MatrixGL.h
@@ -0,0 +1,85 @@
+/*
+ * 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 <stack>
+
+#include "system_gl.h"
+
+class TransformMatrix;
+
+class CMatrixGL
+{
+public:
+ CMatrixGL() = default;
+
+ constexpr CMatrixGL(GLfloat x0, GLfloat x1, GLfloat x2, GLfloat x3,
+ GLfloat x4, GLfloat x5, GLfloat x6, GLfloat x7,
+ GLfloat x8, GLfloat x9, GLfloat x10, GLfloat x11,
+ GLfloat x12, GLfloat x13, GLfloat x14, GLfloat x15)
+ :m_pMatrix{x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15} {}
+
+ CMatrixGL(const TransformMatrix &src) noexcept;
+
+ operator const float*() const { return m_pMatrix; }
+
+ void LoadIdentity();
+ void Ortho(GLfloat l, GLfloat r, GLfloat b, GLfloat t, GLfloat n, GLfloat f);
+ void Ortho2D(GLfloat l, GLfloat r, GLfloat b, GLfloat t);
+ void Frustum(GLfloat l, GLfloat r, GLfloat b, GLfloat t, GLfloat n, GLfloat f);
+ void Translatef(GLfloat x, GLfloat y, GLfloat z);
+ void Scalef(GLfloat x, GLfloat y, GLfloat z);
+ void Rotatef(GLfloat angle, GLfloat x, GLfloat y, GLfloat z);
+ void MultMatrixf(const CMatrixGL &matrix) noexcept;
+ void LookAt(GLfloat eyex, GLfloat eyey, GLfloat eyez, GLfloat centerx, GLfloat centery, GLfloat centerz, GLfloat upx, GLfloat upy, GLfloat upz);
+
+ static bool Project(GLfloat objx, GLfloat objy, GLfloat objz, const GLfloat modelMatrix[16], const GLfloat projMatrix[16], const GLint viewport[4], GLfloat* winx, GLfloat* winy, GLfloat* winz);
+
+private:
+ /* alignas(16) allows better SIMD optimizations (e.g. SSE2 benefits
+ a lot from this) */
+ alignas(16) GLfloat m_pMatrix[16];
+};
+
+class CMatrixGLStack
+{
+public:
+ void Push()
+ {
+ m_stack.push(m_current);
+ }
+
+ void Clear()
+ {
+ m_stack = std::stack<CMatrixGL>();
+ }
+
+ void Pop()
+ {
+ if(!m_stack.empty())
+ {
+ m_current = m_stack.top();
+ m_stack.pop();
+ }
+ }
+
+ void Load();
+ void PopLoad() { Pop(); Load(); }
+
+ CMatrixGL& Get() { return m_current; }
+ CMatrixGL* operator->() { return &m_current; }
+
+private:
+ std::stack<CMatrixGL> m_stack;
+ CMatrixGL m_current;
+};
+
+extern CMatrixGLStack glMatrixModview;
+extern CMatrixGLStack glMatrixProject;
+extern CMatrixGLStack glMatrixTexture;
diff --git a/xbmc/rendering/MatrixGL.neon.cpp b/xbmc/rendering/MatrixGL.neon.cpp
new file mode 100644
index 0000000..3f3f33c
--- /dev/null
+++ b/xbmc/rendering/MatrixGL.neon.cpp
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+
+void Matrix4Mul(float* src_mat_1, const float* src_mat_2)
+{
+ asm volatile (
+ // Store A & B leaving room at top of registers for result (q0-q3)
+ "vldmia %0, { q4-q7 } \n\t"
+ "vldmia %1, { q8-q11 } \n\t"
+
+ // result = first column of B x first row of A
+ "vmul.f32 q0, q8, d8[0]\n\t"
+ "vmul.f32 q1, q8, d10[0]\n\t"
+ "vmul.f32 q2, q8, d12[0]\n\t"
+ "vmul.f32 q3, q8, d14[0]\n\t"
+
+ // result += second column of B x second row of A
+ "vmla.f32 q0, q9, d8[1]\n\t"
+ "vmla.f32 q1, q9, d10[1]\n\t"
+ "vmla.f32 q2, q9, d12[1]\n\t"
+ "vmla.f32 q3, q9, d14[1]\n\t"
+
+ // result += third column of B x third row of A
+ "vmla.f32 q0, q10, d9[0]\n\t"
+ "vmla.f32 q1, q10, d11[0]\n\t"
+ "vmla.f32 q2, q10, d13[0]\n\t"
+ "vmla.f32 q3, q10, d15[0]\n\t"
+
+ // result += last column of B x last row of A
+ "vmla.f32 q0, q11, d9[1]\n\t"
+ "vmla.f32 q1, q11, d11[1]\n\t"
+ "vmla.f32 q2, q11, d13[1]\n\t"
+ "vmla.f32 q3, q11, d15[1]\n\t"
+
+ // output = result registers
+ "vstmia %1, { q0-q3 }"
+ : //no output
+ : "r" (src_mat_2), "r" (src_mat_1) // input - note *value* of pointer doesn't change
+ : "memory", "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "q9", "q10", "q11" //clobber
+ );
+}
diff --git a/xbmc/rendering/RenderSystem.cpp b/xbmc/rendering/RenderSystem.cpp
new file mode 100644
index 0000000..3f10562
--- /dev/null
+++ b/xbmc/rendering/RenderSystem.cpp
@@ -0,0 +1,110 @@
+/*
+ * 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 "RenderSystem.h"
+
+#include "Util.h"
+#include "guilib/GUIFontManager.h"
+#include "guilib/GUIImage.h"
+#include "guilib/GUILabelControl.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+
+CRenderSystemBase::CRenderSystemBase()
+{
+ m_bRenderCreated = false;
+ m_bVSync = true;
+ m_maxTextureSize = 2048;
+ m_RenderVersionMajor = 0;
+ m_RenderVersionMinor = 0;
+ m_minDXTPitch = 0;
+}
+
+CRenderSystemBase::~CRenderSystemBase() = default;
+
+void CRenderSystemBase::GetRenderVersion(unsigned int& major, unsigned int& minor) const
+{
+ major = m_RenderVersionMajor;
+ minor = m_RenderVersionMinor;
+}
+
+bool CRenderSystemBase::SupportsNPOT(bool dxt) const
+{
+ if (dxt)
+ return false;
+
+ return true;
+}
+
+bool CRenderSystemBase::SupportsStereo(RENDER_STEREO_MODE mode) const
+{
+ switch(mode)
+ {
+ case RENDER_STEREO_MODE_OFF:
+ case RENDER_STEREO_MODE_SPLIT_HORIZONTAL:
+ case RENDER_STEREO_MODE_SPLIT_VERTICAL:
+ case RENDER_STEREO_MODE_MONO:
+ return true;
+ default:
+ return false;
+ }
+}
+
+void CRenderSystemBase::ShowSplash(const std::string& message)
+{
+ if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_splashImage && !(m_splashImage || !message.empty()))
+ return;
+
+ if (!m_splashImage)
+ {
+ m_splashImage = std::unique_ptr<CGUIImage>(new CGUIImage(0, 0, 0, 0, CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(),
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight(), CTextureInfo(CUtil::GetSplashPath())));
+ m_splashImage->SetAspectRatio(CAspectRatio::AR_SCALE);
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().lock();
+ CServiceBroker::GetWinSystem()->GetGfxContext().Clear();
+
+ RESOLUTION_INFO res = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(res, true);
+
+ //render splash image
+ BeginRender();
+
+ m_splashImage->AllocResources();
+ m_splashImage->Render();
+ m_splashImage->FreeResources();
+
+ if (!message.empty())
+ {
+ if (!m_splashMessageLayout)
+ {
+ auto messageFont = g_fontManager.LoadTTF("__splash__", "arial.ttf", 0xFFFFFFFF, 0, 20, FONT_STYLE_NORMAL, false, 1.0f, 1.0f, &res);
+ if (messageFont)
+ m_splashMessageLayout = std::unique_ptr<CGUITextLayout>(new CGUITextLayout(messageFont, true, 0));
+ }
+
+ if (m_splashMessageLayout)
+ {
+ m_splashMessageLayout->Update(message, 1150, false, true);
+ float textWidth, textHeight;
+ m_splashMessageLayout->GetTextExtent(textWidth, textHeight);
+
+ int width = CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth();
+ int height = CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight();
+ float y = height - textHeight - 100;
+ m_splashMessageLayout->RenderOutline(width/2, y, 0, 0xFF000000, XBFONT_CENTER_X, width);
+ }
+ }
+
+ //show it on screen
+ EndRender();
+ CServiceBroker::GetWinSystem()->GetGfxContext().unlock();
+ CServiceBroker::GetWinSystem()->GetGfxContext().Flip(true, false);
+}
+
diff --git a/xbmc/rendering/RenderSystem.h b/xbmc/rendering/RenderSystem.h
new file mode 100644
index 0000000..0c3d6e0
--- /dev/null
+++ b/xbmc/rendering/RenderSystem.h
@@ -0,0 +1,98 @@
+/*
+ * 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 "RenderSystemTypes.h"
+#include "utils/ColorUtils.h"
+#include "utils/Geometry.h"
+
+#include <memory>
+#include <string>
+
+/*
+ * CRenderSystemBase interface allows us to create the rendering engine we use.
+ * We currently have two engines: OpenGL and DirectX
+ * This interface is very basic since a lot of the actual details will go in to the derived classes
+ */
+
+class CGUIImage;
+class CGUITextLayout;
+
+class CRenderSystemBase
+{
+public:
+ CRenderSystemBase();
+ virtual ~CRenderSystemBase();
+
+ virtual bool InitRenderSystem() = 0;
+ virtual bool DestroyRenderSystem() = 0;
+ virtual bool ResetRenderSystem(int width, int height) = 0;
+
+ virtual bool BeginRender() = 0;
+ virtual bool EndRender() = 0;
+ virtual void PresentRender(bool rendered, bool videoLayer) = 0;
+ virtual bool ClearBuffers(UTILS::COLOR::Color color) = 0;
+ virtual bool IsExtSupported(const char* extension) const = 0;
+
+ virtual void SetViewPort(const CRect& viewPort) = 0;
+ virtual void GetViewPort(CRect& viewPort) = 0;
+ virtual void RestoreViewPort() {}
+
+ virtual bool ScissorsCanEffectClipping() { return false; }
+ virtual CRect ClipRectToScissorRect(const CRect &rect) { return CRect(); }
+ virtual void SetScissors(const CRect &rect) = 0;
+ virtual void ResetScissors() = 0;
+
+ virtual void CaptureStateBlock() = 0;
+ virtual void ApplyStateBlock() = 0;
+
+ virtual void SetCameraPosition(const CPoint &camera, int screenWidth, int screenHeight, float stereoFactor = 0.f) = 0;
+ virtual void SetStereoMode(RENDER_STEREO_MODE mode, RENDER_STEREO_VIEW view)
+ {
+ m_stereoMode = mode;
+ m_stereoView = view;
+ }
+
+ /**
+ * Project (x,y,z) 3d scene coordinates to (x,y) 2d screen coordinates
+ */
+ virtual void Project(float &x, float &y, float &z) { }
+
+ virtual std::string GetShaderPath(const std::string &filename) { return ""; }
+
+ void GetRenderVersion(unsigned int& major, unsigned int& minor) const;
+ const std::string& GetRenderVendor() const { return m_RenderVendor; }
+ const std::string& GetRenderRenderer() const { return m_RenderRenderer; }
+ const std::string& GetRenderVersionString() const { return m_RenderVersion; }
+ virtual bool SupportsNPOT(bool dxt) const;
+ virtual bool SupportsStereo(RENDER_STEREO_MODE mode) const;
+ unsigned int GetMaxTextureSize() const { return m_maxTextureSize; }
+ unsigned int GetMinDXTPitch() const { return m_minDXTPitch; }
+
+ virtual void ShowSplash(const std::string& message);
+
+protected:
+ bool m_bRenderCreated;
+ bool m_bVSync;
+ unsigned int m_maxTextureSize;
+ unsigned int m_minDXTPitch;
+
+ std::string m_RenderRenderer;
+ std::string m_RenderVendor;
+ std::string m_RenderVersion;
+ int m_RenderVersionMinor;
+ int m_RenderVersionMajor;
+ RENDER_STEREO_VIEW m_stereoView = RENDER_STEREO_VIEW_OFF;
+ RENDER_STEREO_MODE m_stereoMode = RENDER_STEREO_MODE_OFF;
+ bool m_limitedColorRange = false;
+
+ std::unique_ptr<CGUIImage> m_splashImage;
+ std::unique_ptr<CGUITextLayout> m_splashMessageLayout;
+};
+
diff --git a/xbmc/rendering/RenderSystemTypes.h b/xbmc/rendering/RenderSystemTypes.h
new file mode 100644
index 0000000..c818183
--- /dev/null
+++ b/xbmc/rendering/RenderSystemTypes.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017-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
+
+enum RENDER_STEREO_VIEW
+{
+ RENDER_STEREO_VIEW_OFF,
+ RENDER_STEREO_VIEW_LEFT,
+ RENDER_STEREO_VIEW_RIGHT,
+};
+
+enum RENDER_STEREO_MODE
+{
+ RENDER_STEREO_MODE_OFF,
+ RENDER_STEREO_MODE_SPLIT_HORIZONTAL,
+ RENDER_STEREO_MODE_SPLIT_VERTICAL,
+ RENDER_STEREO_MODE_ANAGLYPH_RED_CYAN,
+ RENDER_STEREO_MODE_ANAGLYPH_GREEN_MAGENTA,
+ RENDER_STEREO_MODE_ANAGLYPH_YELLOW_BLUE,
+ RENDER_STEREO_MODE_INTERLACED,
+ RENDER_STEREO_MODE_CHECKERBOARD,
+ RENDER_STEREO_MODE_HARDWAREBASED,
+ RENDER_STEREO_MODE_MONO,
+ RENDER_STEREO_MODE_COUNT,
+
+ // Pseudo modes
+ RENDER_STEREO_MODE_AUTO = 100,
+ RENDER_STEREO_MODE_UNDEFINED = 999,
+};
diff --git a/xbmc/rendering/dx/CMakeLists.txt b/xbmc/rendering/dx/CMakeLists.txt
new file mode 100644
index 0000000..7466907
--- /dev/null
+++ b/xbmc/rendering/dx/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES DeviceResources.cpp
+ RenderSystemDX.cpp
+ ScreenshotSurfaceWindows.cpp)
+
+set(HEADERS DeviceResources.h
+ DirectXHelper.h
+ RenderContext.h
+ RenderSystemDX.h
+ ScreenshotSurfaceWindows.h)
+
+core_add_library(rendering_dx)
diff --git a/xbmc/rendering/dx/DeviceResources.cpp b/xbmc/rendering/dx/DeviceResources.cpp
new file mode 100644
index 0000000..b3ae890
--- /dev/null
+++ b/xbmc/rendering/dx/DeviceResources.cpp
@@ -0,0 +1,1361 @@
+/*
+ * 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 "DeviceResources.h"
+
+#include "DirectXHelper.h"
+#include "RenderContext.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SystemInfo.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include "platform/win32/CharsetConverter.h"
+#include "platform/win32/WIN32Util.h"
+
+#ifdef TARGET_WINDOWS_STORE
+#include <winrt/Windows.Graphics.Display.Core.h>
+
+extern "C"
+{
+#include <libavutil/rational.h>
+}
+#endif
+
+#ifdef _DEBUG
+#include <dxgidebug.h>
+#pragma comment(lib, "dxgi.lib")
+#endif // _DEBUG
+
+using namespace DirectX;
+using namespace Microsoft::WRL;
+using namespace Concurrency;
+namespace winrt
+{
+ using namespace Windows::Foundation;
+}
+
+#ifdef _DEBUG
+#define breakOnDebug __debugbreak()
+#else
+#define breakOnDebug
+#endif
+#define LOG_HR(hr) \
+ CLog::LogF(LOGERROR, "function call at line {} ends with error: {}", __LINE__, \
+ DX::GetErrorDescription(hr));
+#define CHECK_ERR() if (FAILED(hr)) { LOG_HR(hr); breakOnDebug; return; }
+#define RETURN_ERR(ret) if (FAILED(hr)) { LOG_HR(hr); breakOnDebug; return (##ret); }
+
+bool DX::DeviceResources::CBackBuffer::Acquire(ID3D11Texture2D* pTexture)
+{
+ if (!pTexture)
+ return false;
+
+ D3D11_TEXTURE2D_DESC desc;
+ pTexture->GetDesc(&desc);
+
+ m_width = desc.Width;
+ m_height = desc.Height;
+ m_format = desc.Format;
+ m_usage = desc.Usage;
+
+ m_texture = pTexture;
+ return true;
+}
+
+std::shared_ptr<DX::DeviceResources> DX::DeviceResources::Get()
+{
+ static std::shared_ptr<DeviceResources> sDeviceResources(new DeviceResources);
+ return sDeviceResources;
+}
+
+// Constructor for DeviceResources.
+DX::DeviceResources::DeviceResources()
+ : m_screenViewport()
+ , m_d3dFeatureLevel(D3D_FEATURE_LEVEL_9_1)
+ , m_outputSize()
+ , m_logicalSize()
+ , m_dpi(DisplayMetrics::Dpi100)
+ , m_effectiveDpi(DisplayMetrics::Dpi100)
+ , m_deviceNotify(nullptr)
+ , m_stereoEnabled(false)
+ , m_bDeviceCreated(false)
+ , m_IsHDROutput(false)
+ , m_IsTransferPQ(false)
+{
+}
+
+DX::DeviceResources::~DeviceResources() = default;
+
+void DX::DeviceResources::Release()
+{
+ if (!m_bDeviceCreated)
+ return;
+
+ ReleaseBackBuffer();
+ OnDeviceLost(true);
+ DestroySwapChain();
+
+ m_adapter = nullptr;
+ m_dxgiFactory = nullptr;
+ m_output = nullptr;
+ m_deferrContext = nullptr;
+ m_d3dContext = nullptr;
+ m_d3dDevice = nullptr;
+ m_bDeviceCreated = false;
+#ifdef _DEBUG
+ if (m_d3dDebug)
+ {
+ m_d3dDebug->ReportLiveDeviceObjects(D3D11_RLDO_SUMMARY | D3D11_RLDO_DETAIL);
+ m_d3dDebug = nullptr;
+ }
+#endif
+}
+
+void DX::DeviceResources::GetOutput(IDXGIOutput** ppOutput) const
+{
+ ComPtr<IDXGIOutput> pOutput;
+ if (!m_swapChain || FAILED(m_swapChain->GetContainingOutput(pOutput.GetAddressOf())) || !pOutput)
+ m_output.As(&pOutput);
+ *ppOutput = pOutput.Detach();
+}
+
+void DX::DeviceResources::GetAdapterDesc(DXGI_ADAPTER_DESC* desc) const
+{
+ if (m_adapter)
+ m_adapter->GetDesc(desc);
+
+ // GetDesc() returns VendorId == 0 in Xbox however, we need to know that
+ // GPU is AMD to apply workarounds in DXVA.cpp CheckCompatibility() same as desktop
+ if (CSysInfo::GetWindowsDeviceFamily() == CSysInfo::Xbox)
+ desc->VendorId = PCIV_AMD;
+}
+
+void DX::DeviceResources::GetDisplayMode(DXGI_MODE_DESC* mode) const
+{
+ DXGI_OUTPUT_DESC outDesc;
+ ComPtr<IDXGIOutput> pOutput;
+ DXGI_SWAP_CHAIN_DESC scDesc;
+
+ if (!m_swapChain)
+ return;
+
+ m_swapChain->GetDesc(&scDesc);
+
+ GetOutput(pOutput.GetAddressOf());
+ pOutput->GetDesc(&outDesc);
+
+ // desktop coords depend on DPI
+ mode->Width = DX::ConvertDipsToPixels(outDesc.DesktopCoordinates.right - outDesc.DesktopCoordinates.left, m_dpi);
+ mode->Height = DX::ConvertDipsToPixels(outDesc.DesktopCoordinates.bottom - outDesc.DesktopCoordinates.top, m_dpi);
+ mode->Format = scDesc.BufferDesc.Format;
+ mode->Scaling = scDesc.BufferDesc.Scaling;
+ mode->ScanlineOrdering = scDesc.BufferDesc.ScanlineOrdering;
+
+#ifdef TARGET_WINDOWS_DESKTOP
+ DEVMODEW sDevMode = {};
+ sDevMode.dmSize = sizeof(sDevMode);
+
+ // EnumDisplaySettingsW is only one way to detect current refresh rate
+ if (EnumDisplaySettingsW(outDesc.DeviceName, ENUM_CURRENT_SETTINGS, &sDevMode))
+ {
+ int i = (((sDevMode.dmDisplayFrequency + 1) % 24) == 0 || ((sDevMode.dmDisplayFrequency + 1) % 30) == 0) ? 1 : 0;
+ mode->RefreshRate.Numerator = (sDevMode.dmDisplayFrequency + i) * 1000;
+ mode->RefreshRate.Denominator = 1000 + i;
+ if (sDevMode.dmDisplayFlags & DM_INTERLACED)
+ {
+ mode->RefreshRate.Numerator *= 2;
+ mode->ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UPPER_FIELD_FIRST; // guessing
+ }
+ else
+ mode->ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE;
+ }
+#else
+ using namespace winrt::Windows::Graphics::Display::Core;
+
+ auto hdmiInfo = HdmiDisplayInformation::GetForCurrentView();
+ if (hdmiInfo) // Xbox only
+ {
+ auto currentMode = hdmiInfo.GetCurrentDisplayMode();
+ AVRational refresh = av_d2q(currentMode.RefreshRate(), 60000);
+ mode->RefreshRate.Numerator = refresh.num;
+ mode->RefreshRate.Denominator = refresh.den;
+ }
+#endif
+}
+
+void DX::DeviceResources::SetViewPort(D3D11_VIEWPORT& viewPort) const
+{
+ // convert logical viewport to real
+ D3D11_VIEWPORT realViewPort =
+ {
+ viewPort.TopLeftX,
+ viewPort.TopLeftY,
+ viewPort.Width,
+ viewPort.Height,
+ viewPort.MinDepth,
+ viewPort.MinDepth
+ };
+
+ m_deferrContext->RSSetViewports(1, &realViewPort);
+}
+
+bool DX::DeviceResources::SetFullScreen(bool fullscreen, RESOLUTION_INFO& res)
+{
+ if (!m_bDeviceCreated || !m_swapChain)
+ return false;
+
+ critical_section::scoped_lock lock(m_criticalSection);
+
+ BOOL bFullScreen;
+ m_swapChain->GetFullscreenState(&bFullScreen, nullptr);
+
+ CLog::LogF(LOGDEBUG, "switching from {}({:.0f} x {:.0f}) to {}({} x {})",
+ bFullScreen ? "fullscreen " : "", m_outputSize.Width, m_outputSize.Height,
+ fullscreen ? "fullscreen " : "", res.iWidth, res.iHeight);
+
+ bool recreate = m_stereoEnabled != (CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_HARDWAREBASED);
+ if (!!bFullScreen && !fullscreen)
+ {
+ CLog::LogF(LOGDEBUG, "switching to windowed");
+ recreate |= SUCCEEDED(m_swapChain->SetFullscreenState(false, nullptr));
+ }
+ else if (fullscreen)
+ {
+ const bool isResValid = res.iWidth > 0 && res.iHeight > 0 && res.fRefreshRate > 0.f;
+ if (isResValid)
+ {
+ DXGI_MODE_DESC currentMode = {};
+ GetDisplayMode(&currentMode);
+ DXGI_SWAP_CHAIN_DESC scDesc;
+ m_swapChain->GetDesc(&scDesc);
+
+ bool is_interlaced = scDesc.BufferDesc.ScanlineOrdering > DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE;
+ float refreshRate = res.fRefreshRate;
+ if (res.dwFlags & D3DPRESENTFLAG_INTERLACED)
+ refreshRate *= 2;
+
+ if (currentMode.Width != res.iWidth
+ || currentMode.Height != res.iHeight
+ || DX::RationalToFloat(currentMode.RefreshRate) != refreshRate
+ || is_interlaced != (res.dwFlags & D3DPRESENTFLAG_INTERLACED ? true : false)
+ // force resolution change for stereo mode
+ // some drivers unable to create stereo swapchain if mode does not match @23.976
+ || CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_HARDWAREBASED)
+ {
+ CLog::Log(LOGDEBUG, __FUNCTION__ ": changing display mode to {}x{}@{:0.3f}", res.iWidth,
+ res.iHeight, res.fRefreshRate,
+ res.dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : "");
+
+ int refresh = static_cast<int>(res.fRefreshRate);
+ int i = (refresh + 1) % 24 == 0 || (refresh + 1) % 30 == 0 ? 1 : 0;
+
+ currentMode.Width = res.iWidth;
+ currentMode.Height = res.iHeight;
+ currentMode.RefreshRate.Numerator = (refresh + i) * 1000;
+ currentMode.RefreshRate.Denominator = 1000 + i;
+ if (res.dwFlags & D3DPRESENTFLAG_INTERLACED)
+ {
+ currentMode.RefreshRate.Numerator *= 2;
+ currentMode.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UPPER_FIELD_FIRST; // guessing;
+ }
+ // sometimes the OS silently brings Kodi out of full screen mode
+ // in this case switching a resolution has no any effect and
+ // we have to enter into full screen mode before switching
+ if (!bFullScreen)
+ {
+ ComPtr<IDXGIOutput> pOutput;
+ GetOutput(pOutput.GetAddressOf());
+
+ CLog::LogF(LOGDEBUG, "fixup fullscreen mode before switching resolution");
+ recreate |= SUCCEEDED(m_swapChain->SetFullscreenState(true, pOutput.Get()));
+ m_swapChain->GetFullscreenState(&bFullScreen, nullptr);
+ }
+ bool resized = SUCCEEDED(m_swapChain->ResizeTarget(&currentMode));
+ if (resized)
+ {
+ // some system doesn't inform windowing about desktop size changes
+ // so we have to change output size before resizing buffers
+ m_outputSize.Width = static_cast<float>(currentMode.Width);
+ m_outputSize.Height = static_cast<float>(currentMode.Height);
+ }
+ recreate |= resized;
+ }
+ }
+ if (!bFullScreen)
+ {
+ ComPtr<IDXGIOutput> pOutput;
+ GetOutput(pOutput.GetAddressOf());
+
+ CLog::LogF(LOGDEBUG, "switching to fullscreen");
+ recreate |= SUCCEEDED(m_swapChain->SetFullscreenState(true, pOutput.Get()));
+ }
+ }
+
+ // resize backbuffer to proper handle fullscreen/stereo transition
+ if (recreate)
+ ResizeBuffers();
+
+ CLog::LogF(LOGDEBUG, "switching done.");
+
+ return recreate;
+}
+
+// Configures resources that don't depend on the Direct3D device.
+void DX::DeviceResources::CreateDeviceIndependentResources()
+{
+}
+
+// Configures the Direct3D device, and stores handles to it and the device context.
+void DX::DeviceResources::CreateDeviceResources()
+{
+ CLog::LogF(LOGDEBUG, "creating DirectX 11 device.");
+
+ CreateFactory();
+
+ UINT creationFlags = D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
+#if defined(_DEBUG)
+ if (DX::SdkLayersAvailable())
+ {
+ // If the project is in a debug build, enable debugging via SDK Layers with this flag.
+ creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
+ }
+#endif
+
+ // This array defines the set of DirectX hardware feature levels this app will support.
+ // Note the ordering should be preserved.
+ // Don't forget to declare your application's minimum required feature level in its
+ // description. All applications are assumed to support 9.1 unless otherwise stated.
+ std::vector<D3D_FEATURE_LEVEL> featureLevels;
+ if (CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin10))
+ {
+ featureLevels.push_back(D3D_FEATURE_LEVEL_12_1);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_12_0);
+ }
+ if (CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin8))
+ featureLevels.push_back(D3D_FEATURE_LEVEL_11_1);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_11_0);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_10_1);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_10_0);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_9_3);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_9_2);
+ featureLevels.push_back(D3D_FEATURE_LEVEL_9_1);
+
+ // Create the Direct3D 11 API device object and a corresponding context.
+ ComPtr<ID3D11Device> device;
+ ComPtr<ID3D11DeviceContext> context;
+
+ D3D_DRIVER_TYPE drivertType = m_adapter != nullptr ? D3D_DRIVER_TYPE_UNKNOWN : D3D_DRIVER_TYPE_HARDWARE;
+ HRESULT hr = D3D11CreateDevice(
+ m_adapter.Get(), // Create a device on specified adapter.
+ drivertType, // Create a device using scepcified driver.
+ nullptr, // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE.
+ creationFlags, // Set debug and Direct2D compatibility flags.
+ featureLevels.data(), // List of feature levels this app can support.
+ featureLevels.size(), // Size of the list above.
+ D3D11_SDK_VERSION, // Always set this to D3D11_SDK_VERSION for Windows Store apps.
+ &device, // Returns the Direct3D device created.
+ &m_d3dFeatureLevel, // Returns feature level of device created.
+ &context // Returns the device immediate context.
+ );
+
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "unable to create hardware device, trying to create WARP devices then.");
+ hr = D3D11CreateDevice(
+ nullptr,
+ D3D_DRIVER_TYPE_WARP, // Create a WARP device instead of a hardware device.
+ nullptr,
+ creationFlags,
+ featureLevels.data(),
+ featureLevels.size(),
+ D3D11_SDK_VERSION,
+ &device,
+ &m_d3dFeatureLevel,
+ &context
+ );
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGFATAL, "unable to create WARP device. Rendering in not possible.");
+ CHECK_ERR();
+ }
+ }
+
+ // Store pointers to the Direct3D 11.1 API device and immediate context.
+ hr = device.As(&m_d3dDevice); CHECK_ERR();
+
+ // Check shared textures support
+ CheckNV12SharedTexturesSupport();
+
+#ifdef _DEBUG
+ if (SUCCEEDED(m_d3dDevice.As(&m_d3dDebug)))
+ {
+ ComPtr<ID3D11InfoQueue> d3dInfoQueue;
+ if (SUCCEEDED(m_d3dDebug.As(&d3dInfoQueue)))
+ {
+ std::vector<D3D11_MESSAGE_ID> hide =
+ {
+ D3D11_MESSAGE_ID_GETVIDEOPROCESSORFILTERRANGE_UNSUPPORTED, // avoid GETVIDEOPROCESSORFILTERRANGE_UNSUPPORTED (dx bug)
+ D3D11_MESSAGE_ID_DEVICE_RSSETSCISSORRECTS_NEGATIVESCISSOR // avoid warning for some labels out of screen
+ // Add more message IDs here as needed
+ };
+
+ D3D11_INFO_QUEUE_FILTER filter = {};
+ filter.DenyList.NumIDs = hide.size();
+ filter.DenyList.pIDList = hide.data();
+ d3dInfoQueue->AddStorageFilterEntries(&filter);
+ }
+ }
+#endif
+
+ hr = context.As(&m_d3dContext); CHECK_ERR();
+ hr = m_d3dDevice->CreateDeferredContext1(0, &m_deferrContext); CHECK_ERR();
+
+ if (!m_adapter)
+ {
+ ComPtr<IDXGIDevice1> dxgiDevice;
+ ComPtr<IDXGIAdapter> adapter;
+ hr = m_d3dDevice.As(&dxgiDevice); CHECK_ERR();
+ hr = dxgiDevice->GetAdapter(&adapter); CHECK_ERR();
+ hr = adapter.As(&m_adapter); CHECK_ERR();
+ }
+
+ DXGI_ADAPTER_DESC aDesc;
+ m_adapter->GetDesc(&aDesc);
+
+ CLog::LogF(LOGINFO, "device is created on adapter '{}' with {}",
+ KODI::PLATFORM::WINDOWS::FromW(aDesc.Description),
+ GetFeatureLevelDescription(m_d3dFeatureLevel));
+
+ CheckDXVA2SharedDecoderSurfaces();
+
+ m_bDeviceCreated = true;
+}
+
+void DX::DeviceResources::ReleaseBackBuffer()
+{
+ CLog::LogF(LOGDEBUG, "release buffers.");
+
+ m_backBufferTex.Release();
+ m_d3dDepthStencilView = nullptr;
+ if (m_deferrContext)
+ {
+ // Clear the previous window size specific context.
+ ID3D11RenderTargetView* nullViews[] = { nullptr, nullptr, nullptr, nullptr };
+ m_deferrContext->OMSetRenderTargets(4, nullViews, nullptr);
+ FinishCommandList(false);
+
+ m_deferrContext->Flush();
+ m_d3dContext->Flush();
+ }
+}
+
+void DX::DeviceResources::CreateBackBuffer()
+{
+ if (!m_bDeviceCreated || !m_swapChain)
+ return;
+
+ CLog::LogF(LOGDEBUG, "create buffers.");
+
+ // Get swap chain back buffer.
+ ComPtr<ID3D11Texture2D> backBuffer;
+ HRESULT hr = m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer)); CHECK_ERR();
+
+ // Create back buffer texture from swap chain texture
+ if (!m_backBufferTex.Acquire(backBuffer.Get()))
+ {
+ CLog::LogF(LOGERROR, "failed to create render target.");
+ return;
+ }
+
+ // Create a depth stencil view for use with 3D rendering if needed.
+ CD3D11_TEXTURE2D_DESC depthStencilDesc(
+ DXGI_FORMAT_D24_UNORM_S8_UINT,
+ lround(m_outputSize.Width),
+ lround(m_outputSize.Height),
+ 1, // This depth stencil view has only one texture.
+ 1, // Use a single mipmap level.
+ D3D11_BIND_DEPTH_STENCIL
+ );
+
+ ComPtr<ID3D11Texture2D> depthStencil;
+ hr = m_d3dDevice->CreateTexture2D(
+ &depthStencilDesc,
+ nullptr,
+ &depthStencil
+ ); CHECK_ERR();
+
+ CD3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc(D3D11_DSV_DIMENSION_TEXTURE2D);
+ hr = m_d3dDevice->CreateDepthStencilView(
+ depthStencil.Get(),
+ &depthStencilViewDesc,
+ &m_d3dDepthStencilView
+ ); CHECK_ERR();
+
+ // Set the 3D rendering viewport to target the entire window.
+ m_screenViewport = CD3D11_VIEWPORT(
+ 0.0f,
+ 0.0f,
+ m_outputSize.Width,
+ m_outputSize.Height
+ );
+
+ m_deferrContext->RSSetViewports(1, &m_screenViewport);
+}
+
+HRESULT DX::DeviceResources::CreateSwapChain(DXGI_SWAP_CHAIN_DESC1& desc, DXGI_SWAP_CHAIN_FULLSCREEN_DESC& fsDesc, IDXGISwapChain1** ppSwapChain) const
+{
+ HRESULT hr;
+#ifdef TARGET_WINDOWS_DESKTOP
+ hr = m_dxgiFactory->CreateSwapChainForHwnd(
+ m_d3dDevice.Get(),
+ m_window,
+ &desc,
+ &fsDesc,
+ nullptr,
+ ppSwapChain
+ ); RETURN_ERR(hr);
+ hr = m_dxgiFactory->MakeWindowAssociation(m_window, /*DXGI_MWA_NO_WINDOW_CHANGES |*/ DXGI_MWA_NO_ALT_ENTER);
+#else
+ hr = m_dxgiFactory->CreateSwapChainForCoreWindow(
+ m_d3dDevice.Get(),
+ winrt::get_unknown(m_coreWindow),
+ &desc,
+ nullptr,
+ ppSwapChain
+ ); RETURN_ERR(hr);
+#endif
+ return hr;
+}
+
+void DX::DeviceResources::DestroySwapChain()
+{
+ if (!m_swapChain)
+ return;
+
+ BOOL bFullcreen = 0;
+ m_swapChain->GetFullscreenState(&bFullcreen, nullptr);
+ if (!!bFullcreen)
+ m_swapChain->SetFullscreenState(false, nullptr); // mandatory before releasing swapchain
+ m_swapChain = nullptr;
+ m_deferrContext->Flush();
+ m_d3dContext->Flush();
+ m_IsTransferPQ = false;
+}
+
+void DX::DeviceResources::ResizeBuffers()
+{
+ if (!m_bDeviceCreated)
+ return;
+
+ CLog::LogF(LOGDEBUG, "resize buffers.");
+
+ bool bHWStereoEnabled = RENDER_STEREO_MODE_HARDWAREBASED ==
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode();
+ bool windowed = true;
+ HRESULT hr = E_FAIL;
+ DXGI_SWAP_CHAIN_DESC1 scDesc = {};
+
+ if (m_swapChain)
+ {
+ BOOL bFullcreen = 0;
+ m_swapChain->GetFullscreenState(&bFullcreen, nullptr);
+ if (!!bFullcreen)
+ windowed = false;
+
+ m_swapChain->GetDesc1(&scDesc);
+ if ((scDesc.Stereo == TRUE) != bHWStereoEnabled) // check if swapchain needs to be recreated
+ DestroySwapChain();
+ }
+
+ if (m_swapChain) // If the swap chain already exists, resize it.
+ {
+ m_swapChain->GetDesc1(&scDesc);
+ hr = m_swapChain->ResizeBuffers(scDesc.BufferCount, lround(m_outputSize.Width),
+ lround(m_outputSize.Height), scDesc.Format,
+ windowed ? 0 : DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH);
+
+ if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET)
+ {
+ // If the device was removed for any reason, a new device and swap chain will need to be created.
+ HandleDeviceLost(hr == DXGI_ERROR_DEVICE_REMOVED);
+
+ // Everything is set up now. Do not continue execution of this method. HandleDeviceLost will reenter this method
+ // and correctly set up the new device.
+ return;
+ }
+ else if (hr == DXGI_ERROR_INVALID_CALL)
+ {
+ // Called when Windows HDR is toggled externally to Kodi.
+ // Is forced to re-create swap chain to avoid crash.
+ CreateWindowSizeDependentResources();
+ return;
+ }
+ CHECK_ERR();
+ }
+ else // Otherwise, create a new one using the same adapter as the existing Direct3D device.
+ {
+ HDR_STATUS hdrStatus = CWIN32Util::GetWindowsHDRStatus();
+ const bool isHdrEnabled = (hdrStatus == HDR_STATUS::HDR_ON);
+ bool use10bit = (hdrStatus != HDR_STATUS::HDR_UNSUPPORTED);
+
+// Xbox needs 10 bit swapchain to output true 4K resolution
+#ifdef TARGET_WINDOWS_DESKTOP
+ DXGI_ADAPTER_DESC ad = {};
+ GetAdapterDesc(&ad);
+
+ // Some AMD graphics has issues with 10 bit in SDR.
+ // Enabled by default only in Intel and NVIDIA with latest drivers/hardware
+ if (m_d3dFeatureLevel < D3D_FEATURE_LEVEL_12_1 || ad.VendorId == PCIV_AMD)
+ use10bit = false;
+#endif
+
+ // 0 = Auto | 1 = Never | 2 = Always
+ int use10bitSetting = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_VIDEOSCREEN_10BITSURFACES);
+
+ if (use10bitSetting == 1)
+ use10bit = false;
+ else if (use10bitSetting == 2)
+ use10bit = true;
+
+ DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
+ swapChainDesc.Width = lround(m_outputSize.Width);
+ swapChainDesc.Height = lround(m_outputSize.Height);
+ swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+ swapChainDesc.Stereo = bHWStereoEnabled;
+ swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
+#ifdef TARGET_WINDOWS_DESKTOP
+ swapChainDesc.BufferCount = 6; // HDR 60 fps needs 6 buffers to avoid frame drops
+#else
+ swapChainDesc.BufferCount = 3; // Xbox don't like 6 backbuffers (3 is fine even for 4K 60 fps)
+#endif
+ // FLIP_DISCARD improves performance (needed in some systems for 4K HDR 60 fps)
+ swapChainDesc.SwapEffect = CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin10)
+ ? DXGI_SWAP_EFFECT_FLIP_DISCARD
+ : DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
+ swapChainDesc.Flags = windowed ? 0 : DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
+ swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE;
+ swapChainDesc.SampleDesc.Count = 1;
+ swapChainDesc.SampleDesc.Quality = 0;
+
+ DXGI_SWAP_CHAIN_FULLSCREEN_DESC scFSDesc = {}; // unused for uwp
+ scFSDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
+ scFSDesc.Windowed = windowed;
+
+ ComPtr<IDXGISwapChain1> swapChain;
+ if (m_d3dFeatureLevel >= D3D_FEATURE_LEVEL_11_0 && !bHWStereoEnabled &&
+ (isHdrEnabled || use10bit))
+ {
+ swapChainDesc.Format = DXGI_FORMAT_R10G10B10A2_UNORM;
+ hr = CreateSwapChain(swapChainDesc, scFSDesc, &swapChain);
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGWARNING, "creating 10bit swapchain failed, fallback to 8bit.");
+ swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+ }
+ }
+
+ if (!swapChain)
+ hr = CreateSwapChain(swapChainDesc, scFSDesc, &swapChain);
+
+ if (FAILED(hr) && bHWStereoEnabled)
+ {
+ // switch to stereo mode failed, create mono swapchain
+ CLog::LogF(LOGERROR, "creating stereo swap chain failed with error.");
+ CLog::LogF(LOGINFO, "fallback to monoscopic mode.");
+
+ swapChainDesc.Stereo = false;
+ bHWStereoEnabled = false;
+
+ hr = CreateSwapChain(swapChainDesc, scFSDesc, &swapChain); CHECK_ERR();
+
+ // fallback to split_horizontal mode.
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetStereoMode(
+ RENDER_STEREO_MODE_SPLIT_HORIZONTAL);
+ }
+
+ if (FAILED(hr))
+ {
+ CLog::LogF(LOGERROR, "unable to create swapchain.");
+ return;
+ }
+
+ m_IsHDROutput = (swapChainDesc.Format == DXGI_FORMAT_R10G10B10A2_UNORM) && isHdrEnabled;
+
+ const int bits = (swapChainDesc.Format == DXGI_FORMAT_R10G10B10A2_UNORM) ? 10 : 8;
+ std::string flip =
+ (swapChainDesc.SwapEffect == DXGI_SWAP_EFFECT_FLIP_DISCARD) ? "discard" : "sequential";
+
+ CLog::LogF(LOGINFO, "{} bit swapchain is used with {} flip {} buffers and {} output", bits,
+ swapChainDesc.BufferCount, flip, m_IsHDROutput ? "HDR" : "SDR");
+
+ hr = swapChain.As(&m_swapChain); CHECK_ERR();
+ m_stereoEnabled = bHWStereoEnabled;
+
+ // Ensure that DXGI does not queue more than one frame at a time. This both reduces latency and
+ // ensures that the application will only render after each VSync, minimizing power consumption.
+ ComPtr<IDXGIDevice1> dxgiDevice;
+ hr = m_d3dDevice.As(&dxgiDevice); CHECK_ERR();
+ dxgiDevice->SetMaximumFrameLatency(1);
+ }
+
+ CLog::LogF(LOGDEBUG, "end resize buffers.");
+}
+
+// These resources need to be recreated every time the window size is changed.
+void DX::DeviceResources::CreateWindowSizeDependentResources()
+{
+ ReleaseBackBuffer();
+
+ DestroySwapChain();
+
+ if (!m_dxgiFactory->IsCurrent()) // HDR toggling requires re-create factory
+ CreateFactory();
+
+ UpdateRenderTargetSize();
+ ResizeBuffers();
+
+ CreateBackBuffer();
+}
+
+// Determine the dimensions of the render target and whether it will be scaled down.
+void DX::DeviceResources::UpdateRenderTargetSize()
+{
+ m_effectiveDpi = m_dpi;
+
+ // To improve battery life on high resolution devices, render to a smaller render target
+ // and allow the GPU to scale the output when it is presented.
+ if (!DisplayMetrics::SupportHighResolutions && m_dpi > DisplayMetrics::DpiThreshold)
+ {
+ float width = DX::ConvertDipsToPixels(m_logicalSize.Width, m_dpi);
+ float height = DX::ConvertDipsToPixels(m_logicalSize.Height, m_dpi);
+
+ // When the device is in portrait orientation, height > width. Compare the
+ // larger dimension against the width threshold and the smaller dimension
+ // against the height threshold.
+ if (std::max(width, height) > DisplayMetrics::WidthThreshold && std::min(width, height) > DisplayMetrics::HeightThreshold)
+ {
+ // To scale the app we change the effective DPI. Logical size does not change.
+ m_effectiveDpi /= 2.0f;
+ }
+ }
+
+ // Calculate the necessary render target size in pixels.
+ m_outputSize.Width = DX::ConvertDipsToPixels(m_logicalSize.Width, m_effectiveDpi);
+ m_outputSize.Height = DX::ConvertDipsToPixels(m_logicalSize.Height, m_effectiveDpi);
+
+ // Prevent zero size DirectX content from being created.
+ m_outputSize.Width = std::max(m_outputSize.Width, 1.f);
+ m_outputSize.Height = std::max(m_outputSize.Height, 1.f);
+}
+
+void DX::DeviceResources::Register(ID3DResource* resource)
+{
+ critical_section::scoped_lock lock(m_resourceSection);
+ m_resources.push_back(resource);
+}
+
+void DX::DeviceResources::Unregister(ID3DResource* resource)
+{
+ critical_section::scoped_lock lock(m_resourceSection);
+ std::vector<ID3DResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource);
+ if (i != m_resources.end())
+ m_resources.erase(i);
+}
+
+void DX::DeviceResources::FinishCommandList(bool bExecute) const
+{
+ if (m_d3dContext == m_deferrContext)
+ return;
+
+ ComPtr<ID3D11CommandList> pCommandList;
+ if (FAILED(m_deferrContext->FinishCommandList(true, &pCommandList)))
+ {
+ CLog::LogF(LOGERROR, "failed to finish command queue.");
+ return;
+ }
+
+ if (bExecute)
+ m_d3dContext->ExecuteCommandList(pCommandList.Get(), false);
+}
+
+// This method is called in the event handler for the SizeChanged event.
+void DX::DeviceResources::SetLogicalSize(float width, float height)
+{
+ if
+#if defined(TARGET_WINDOWS_DESKTOP)
+ (!m_window)
+#else
+ (!m_coreWindow)
+#endif
+ return;
+
+ CLog::LogF(LOGDEBUG, "receive changing logical size to {:f} x {:f}", width, height);
+
+ if (m_logicalSize.Width != width || m_logicalSize.Height != height)
+ {
+ CLog::LogF(LOGDEBUG, "change logical size to {:f} x {:f}", width, height);
+
+ m_logicalSize = winrt::Size(width, height);
+
+ UpdateRenderTargetSize();
+ ResizeBuffers();
+ }
+}
+
+// This method is called in the event handler for the DpiChanged event.
+void DX::DeviceResources::SetDpi(float dpi)
+{
+ dpi = std::max(dpi, DisplayMetrics::Dpi100);
+ if (dpi != m_dpi)
+ m_dpi = dpi;
+}
+
+// This method is called in the event handler for the DisplayContentsInvalidated event.
+void DX::DeviceResources::ValidateDevice()
+{
+ // The D3D Device is no longer valid if the default adapter changed since the device
+ // was created or if the device has been removed.
+
+ // First, get the information for the default adapter from when the device was created.
+ ComPtr<IDXGIDevice1> dxgiDevice;
+ m_d3dDevice.As(&dxgiDevice);
+
+ ComPtr<IDXGIAdapter> deviceAdapter;
+ dxgiDevice->GetAdapter(&deviceAdapter);
+
+ ComPtr<IDXGIFactory2> dxgiFactory;
+ deviceAdapter->GetParent(IID_PPV_ARGS(&dxgiFactory));
+
+ DXGI_ADAPTER_DESC1 previousDesc;
+ {
+ ComPtr<IDXGIAdapter1> previousDefaultAdapter;
+ dxgiFactory->EnumAdapters1(0, &previousDefaultAdapter);
+
+ previousDefaultAdapter->GetDesc1(&previousDesc);
+ }
+
+ // Next, get the information for the current default adapter.
+ DXGI_ADAPTER_DESC1 currentDesc;
+ {
+ ComPtr<IDXGIFactory1> currentFactory;
+ CreateDXGIFactory1(IID_PPV_ARGS(&currentFactory));
+
+ ComPtr<IDXGIAdapter1> currentDefaultAdapter;
+ currentFactory->EnumAdapters1(0, &currentDefaultAdapter);
+
+ currentDefaultAdapter->GetDesc1(&currentDesc);
+ }
+ // If the adapter LUIDs don't match, or if the device reports that it has been removed,
+ // a new D3D device must be created.
+ HRESULT hr = m_d3dDevice->GetDeviceRemovedReason();
+ if ( previousDesc.AdapterLuid.LowPart != currentDesc.AdapterLuid.LowPart
+ || previousDesc.AdapterLuid.HighPart != currentDesc.AdapterLuid.HighPart
+ || FAILED(hr))
+ {
+ // Release references to resources related to the old device.
+ dxgiDevice = nullptr;
+ deviceAdapter = nullptr;
+ dxgiFactory = nullptr;
+
+ // Create a new device and swap chain.
+ HandleDeviceLost(hr == DXGI_ERROR_DEVICE_REMOVED);
+ }
+}
+
+void DX::DeviceResources::OnDeviceLost(bool removed)
+{
+ auto pGUI = CServiceBroker::GetGUI();
+ if (pGUI)
+ pGUI->GetWindowManager().SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_RENDERER_LOST);
+
+ // tell any shared resources
+ for (auto res : m_resources)
+ {
+ // the most of resources like textures and buffers try to
+ // receive and save their status from current device.
+ // `removed` means that we have no possibility
+ // to use the device anymore, tell all resources about this.
+ res->OnDestroyDevice(removed);
+ }
+}
+
+void DX::DeviceResources::OnDeviceRestored()
+{
+ // tell any shared resources
+ for (auto res : m_resources)
+ res->OnCreateDevice();
+
+ auto pGUI = CServiceBroker::GetGUI();
+ if (pGUI)
+ pGUI->GetWindowManager().SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_RENDERER_RESET);
+}
+
+// Recreate all device resources and set them back to the current state.
+void DX::DeviceResources::HandleDeviceLost(bool removed)
+{
+ bool backbuferExists = m_backBufferTex.Get() != nullptr;
+
+ OnDeviceLost(removed);
+ if (m_deviceNotify != nullptr)
+ m_deviceNotify->OnDXDeviceLost();
+
+ if (backbuferExists)
+ ReleaseBackBuffer();
+
+ DestroySwapChain();
+
+ CreateDeviceResources();
+ UpdateRenderTargetSize();
+ ResizeBuffers();
+
+ if (backbuferExists)
+ CreateBackBuffer();
+
+ if (m_deviceNotify != nullptr)
+ m_deviceNotify->OnDXDeviceRestored();
+ OnDeviceRestored();
+
+ if (removed)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ "ReloadSkin");
+}
+
+bool DX::DeviceResources::Begin()
+{
+ HRESULT hr = m_swapChain->Present(0, DXGI_PRESENT_TEST);
+
+ // If the device was removed either by a disconnection or a driver upgrade, we
+ // must recreate all device resources.
+ if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET)
+ {
+ HandleDeviceLost(hr == DXGI_ERROR_DEVICE_REMOVED);
+ }
+ else
+ {
+ // not fatal errors
+ if (hr == DXGI_ERROR_INVALID_CALL)
+ {
+ CreateWindowSizeDependentResources();
+ }
+ }
+
+ m_deferrContext->OMSetRenderTargets(1, m_backBufferTex.GetAddressOfRTV(), m_d3dDepthStencilView.Get());
+
+ return true;
+}
+
+// Present the contents of the swap chain to the screen.
+void DX::DeviceResources::Present()
+{
+ FinishCommandList();
+
+ // The first argument instructs DXGI to block until VSync, putting the application
+ // to sleep until the next VSync. This ensures we don't waste any cycles rendering
+ // frames that will never be displayed to the screen.
+ DXGI_PRESENT_PARAMETERS parameters = {};
+ HRESULT hr = m_swapChain->Present1(1, 0, &parameters);
+
+ // If the device was removed either by a disconnection or a driver upgrade, we
+ // must recreate all device resources.
+ if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET)
+ {
+ HandleDeviceLost(hr == DXGI_ERROR_DEVICE_REMOVED);
+ }
+ else
+ {
+ // not fatal errors
+ if (hr == DXGI_ERROR_INVALID_CALL)
+ {
+ CreateWindowSizeDependentResources();
+ }
+ }
+
+ if (m_d3dContext == m_deferrContext)
+ {
+ m_deferrContext->OMSetRenderTargets(1, m_backBufferTex.GetAddressOfRTV(), m_d3dDepthStencilView.Get());
+ }
+}
+
+void DX::DeviceResources::ClearDepthStencil() const
+{
+ m_deferrContext->ClearDepthStencilView(m_d3dDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0, 0);
+}
+
+void DX::DeviceResources::ClearRenderTarget(ID3D11RenderTargetView* pRTView, float color[4]) const
+{
+ m_deferrContext->ClearRenderTargetView(pRTView, color);
+}
+
+void DX::DeviceResources::HandleOutputChange(const std::function<bool(DXGI_OUTPUT_DESC)>& cmpFunc)
+{
+ DXGI_ADAPTER_DESC currentDesc = {};
+ DXGI_ADAPTER_DESC foundDesc = {};
+
+ ComPtr<IDXGIFactory1> factory;
+ if (m_adapter)
+ m_adapter->GetDesc(&currentDesc);
+
+ CreateDXGIFactory1(IID_IDXGIFactory1, &factory);
+
+ ComPtr<IDXGIAdapter1> adapter;
+ for (int i = 0; factory->EnumAdapters1(i, adapter.ReleaseAndGetAddressOf()) != DXGI_ERROR_NOT_FOUND; i++)
+ {
+ adapter->GetDesc(&foundDesc);
+ ComPtr<IDXGIOutput> output;
+ for (int j = 0; adapter->EnumOutputs(j, output.ReleaseAndGetAddressOf()) != DXGI_ERROR_NOT_FOUND; j++)
+ {
+ DXGI_OUTPUT_DESC outputDesc;
+ output->GetDesc(&outputDesc);
+ if (cmpFunc(outputDesc))
+ {
+ output.As(&m_output);
+ // check if adapter is changed
+ if (currentDesc.AdapterLuid.HighPart != foundDesc.AdapterLuid.HighPart
+ || currentDesc.AdapterLuid.LowPart != foundDesc.AdapterLuid.LowPart)
+ {
+ // adapter is changed
+ m_adapter = adapter;
+ CLog::LogF(LOGDEBUG, "selected {} adapter. ",
+ KODI::PLATFORM::WINDOWS::FromW(foundDesc.Description));
+ // (re)init hooks into new driver
+ Windowing()->InitHooks(output.Get());
+ // recreate d3d11 device on new adapter
+ if (m_d3dDevice)
+ HandleDeviceLost(false);
+ }
+ return;
+ }
+ }
+ }
+}
+
+bool DX::DeviceResources::CreateFactory()
+{
+ HRESULT hr;
+#if defined(_DEBUG) && defined(TARGET_WINDOWS_STORE)
+ bool debugDXGI = false;
+ {
+ ComPtr<IDXGIInfoQueue> dxgiInfoQueue;
+ if (SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(dxgiInfoQueue.GetAddressOf()))))
+ {
+ debugDXGI = true;
+
+ hr = CreateDXGIFactory2(DXGI_CREATE_FACTORY_DEBUG, IID_PPV_ARGS(m_dxgiFactory.ReleaseAndGetAddressOf())); RETURN_ERR(false);
+
+ dxgiInfoQueue->SetBreakOnSeverity(DXGI_DEBUG_ALL, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_ERROR, true);
+ dxgiInfoQueue->SetBreakOnSeverity(DXGI_DEBUG_ALL, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_CORRUPTION, true);
+ }
+ }
+
+ if (!debugDXGI)
+#endif
+ hr = CreateDXGIFactory1(IID_PPV_ARGS(m_dxgiFactory.ReleaseAndGetAddressOf())); RETURN_ERR(false);
+
+ return true;
+}
+
+void DX::DeviceResources::SetMonitor(HMONITOR monitor)
+{
+ HandleOutputChange([monitor](DXGI_OUTPUT_DESC outputDesc) {
+ return outputDesc.Monitor == monitor;
+ });
+}
+
+void DX::DeviceResources::RegisterDeviceNotify(IDeviceNotify* deviceNotify)
+{
+ m_deviceNotify = deviceNotify;
+}
+
+HMONITOR DX::DeviceResources::GetMonitor() const
+{
+ if (m_swapChain)
+ {
+ ComPtr<IDXGIOutput> output;
+ GetOutput(output.GetAddressOf());
+ if (output)
+ {
+ DXGI_OUTPUT_DESC desc;
+ output->GetDesc(&desc);
+ return desc.Monitor;
+ }
+ }
+ return nullptr;
+}
+
+bool DX::DeviceResources::IsStereoAvailable() const
+{
+ if (m_dxgiFactory)
+ return m_dxgiFactory->IsWindowedStereoEnabled();
+
+ return false;
+}
+
+void DX::DeviceResources::CheckNV12SharedTexturesSupport()
+{
+ if (m_d3dFeatureLevel < D3D_FEATURE_LEVEL_10_0 ||
+ CSysInfo::GetWindowsDeviceFamily() != CSysInfo::Desktop)
+ return;
+
+ D3D11_FEATURE_DATA_D3D11_OPTIONS4 op4 = {};
+ HRESULT hr = m_d3dDevice->CheckFeatureSupport(D3D11_FEATURE_D3D11_OPTIONS4, &op4, sizeof(op4));
+ m_NV12SharedTexturesSupport = SUCCEEDED(hr) && !!op4.ExtendedNV12SharedTextureSupported;
+ CLog::LogF(LOGINFO, "extended NV12 shared textures is{}supported",
+ m_NV12SharedTexturesSupport ? " " : " NOT ");
+}
+
+void DX::DeviceResources::CheckDXVA2SharedDecoderSurfaces()
+{
+ if (CSysInfo::GetWindowsDeviceFamily() != CSysInfo::Desktop)
+ return;
+
+ VideoDriverInfo driver = GetVideoDriverVersion();
+
+ if (!m_NV12SharedTexturesSupport)
+ return;
+
+ DXGI_ADAPTER_DESC ad = {};
+ GetAdapterDesc(&ad);
+
+ m_DXVA2SharedDecoderSurfaces =
+ ad.VendorId == PCIV_Intel ||
+ (ad.VendorId == PCIV_NVIDIA && driver.valid && driver.majorVersion >= 465) ||
+ (ad.VendorId == PCIV_AMD && driver.valid && driver.majorVersion >= 30 &&
+ m_d3dFeatureLevel >= D3D_FEATURE_LEVEL_12_1);
+
+ CLog::LogF(LOGINFO, "DXVA2 shared decoder surfaces is{}supported",
+ m_DXVA2SharedDecoderSurfaces ? " " : " NOT ");
+}
+
+VideoDriverInfo DX::DeviceResources::GetVideoDriverVersion()
+{
+ DXGI_ADAPTER_DESC ad = {};
+ GetAdapterDesc(&ad);
+
+ VideoDriverInfo driver = CWIN32Util::GetVideoDriverInfo(ad.VendorId, ad.Description);
+
+ if (ad.VendorId == PCIV_NVIDIA)
+ CLog::LogF(LOGINFO, "video driver version is {} {}.{} ({})", GetGFXProviderName(ad.VendorId),
+ driver.majorVersion, driver.minorVersion, driver.version);
+ else
+ CLog::LogF(LOGINFO, "video driver version is {} {}", GetGFXProviderName(ad.VendorId),
+ driver.version);
+
+ return driver;
+}
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+// This method is called when the window (WND) is created (or re-created).
+void DX::DeviceResources::SetWindow(HWND window)
+{
+ m_window = window;
+
+ CreateDeviceIndependentResources();
+ CreateDeviceResources();
+}
+#elif defined(TARGET_WINDOWS_STORE)
+// This method is called when the CoreWindow is created (or re-created).
+void DX::DeviceResources::SetWindow(const winrt::Windows::UI::Core::CoreWindow& window)
+{
+ using namespace winrt::Windows::UI::Core;
+ using namespace winrt::Windows::Graphics::Display;
+
+ m_coreWindow = window;
+ auto dispatcher = m_coreWindow.Dispatcher();
+ DispatchedHandler handler([&]()
+ {
+ auto coreWindow = CoreWindow::GetForCurrentThread();
+ m_logicalSize = winrt::Size(coreWindow.Bounds().Width, coreWindow.Bounds().Height);
+ m_dpi = DisplayInformation::GetForCurrentView().LogicalDpi();
+ SetWindowPos(coreWindow.Bounds());
+ });
+ if (dispatcher.HasThreadAccess())
+ handler();
+ else
+ dispatcher.RunAsync(CoreDispatcherPriority::High, handler).get();
+
+ CreateDeviceIndependentResources();
+ CreateDeviceResources();
+ // we have to call this because we will not get initial WM_SIZE
+ CreateWindowSizeDependentResources();
+}
+
+void DX::DeviceResources::SetWindowPos(winrt::Rect rect)
+{
+ int centerX = rect.X + rect.Width / 2;
+ int centerY = rect.Y + rect.Height / 2;
+
+ HandleOutputChange([centerX, centerY](DXGI_OUTPUT_DESC outputDesc) {
+ // DesktopCoordinates depends on the DPI of the desktop
+ return outputDesc.DesktopCoordinates.left <= centerX && outputDesc.DesktopCoordinates.right >= centerX
+ && outputDesc.DesktopCoordinates.top <= centerY && outputDesc.DesktopCoordinates.bottom >= centerY;
+ });
+}
+
+// Call this method when the app suspends. It provides a hint to the driver that the app
+// is entering an idle state and that temporary buffers can be reclaimed for use by other apps.
+void DX::DeviceResources::Trim() const
+{
+ ComPtr<IDXGIDevice3> dxgiDevice;
+ m_d3dDevice.As(&dxgiDevice);
+
+ dxgiDevice->Trim();
+}
+
+#endif
+
+void DX::DeviceResources::SetHdrMetaData(DXGI_HDR_METADATA_HDR10& hdr10) const
+{
+ ComPtr<IDXGISwapChain4> swapChain4;
+
+ if (!m_swapChain)
+ return;
+
+ if (SUCCEEDED(m_swapChain.As(&swapChain4)))
+ {
+ if (SUCCEEDED(swapChain4->SetHDRMetaData(DXGI_HDR_METADATA_TYPE_HDR10, sizeof(hdr10), &hdr10)))
+ {
+ CLog::LogF(LOGDEBUG,
+ "(raw) RP {} {} | GP {} {} | BP {} {} | WP {} {} | Max ML {} | min ML "
+ "{} | Max CLL {} | Max FALL {}",
+ hdr10.RedPrimary[0], hdr10.RedPrimary[1], hdr10.GreenPrimary[0],
+ hdr10.GreenPrimary[1], hdr10.BluePrimary[0], hdr10.BluePrimary[1],
+ hdr10.WhitePoint[0], hdr10.WhitePoint[1], hdr10.MaxMasteringLuminance,
+ hdr10.MinMasteringLuminance, hdr10.MaxContentLightLevel,
+ hdr10.MaxFrameAverageLightLevel);
+
+ constexpr double FACTOR_1 = 50000.0;
+ constexpr double FACTOR_2 = 10000.0;
+ const double RP_0 = static_cast<double>(hdr10.RedPrimary[0]) / FACTOR_1;
+ const double RP_1 = static_cast<double>(hdr10.RedPrimary[1]) / FACTOR_1;
+ const double GP_0 = static_cast<double>(hdr10.GreenPrimary[0]) / FACTOR_1;
+ const double GP_1 = static_cast<double>(hdr10.GreenPrimary[1]) / FACTOR_1;
+ const double BP_0 = static_cast<double>(hdr10.BluePrimary[0]) / FACTOR_1;
+ const double BP_1 = static_cast<double>(hdr10.BluePrimary[1]) / FACTOR_1;
+ const double WP_0 = static_cast<double>(hdr10.WhitePoint[0]) / FACTOR_1;
+ const double WP_1 = static_cast<double>(hdr10.WhitePoint[1]) / FACTOR_1;
+ const double Max_ML = static_cast<double>(hdr10.MaxMasteringLuminance) / FACTOR_2;
+ const double min_ML = static_cast<double>(hdr10.MinMasteringLuminance) / FACTOR_2;
+
+ CLog::LogF(LOGINFO,
+ "RP {:.3f} {:.3f} | GP {:.3f} {:.3f} | BP {:.3f} {:.3f} | WP {:.3f} "
+ "{:.3f} | Max ML {:.0f} | min ML {:.4f} | Max CLL {} | Max FALL {}",
+ RP_0, RP_1, GP_0, GP_1, BP_0, BP_1, WP_0, WP_1, Max_ML, min_ML,
+ hdr10.MaxContentLightLevel, hdr10.MaxFrameAverageLightLevel);
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "DXGI SetHDRMetaData failed");
+ }
+ }
+}
+
+void DX::DeviceResources::SetHdrColorSpace(const DXGI_COLOR_SPACE_TYPE colorSpace)
+{
+ ComPtr<IDXGISwapChain3> swapChain3;
+
+ if (!m_swapChain)
+ return;
+
+ if (SUCCEEDED(m_swapChain.As(&swapChain3)))
+ {
+ if (SUCCEEDED(swapChain3->SetColorSpace1(colorSpace)))
+ {
+ m_IsTransferPQ = (colorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020);
+
+ CLog::LogF(LOGDEBUG, "DXGI SetColorSpace1 success");
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "DXGI SetColorSpace1 failed");
+ }
+ }
+}
+
+HDR_STATUS DX::DeviceResources::ToggleHDR()
+{
+ DXGI_MODE_DESC md = {};
+ GetDisplayMode(&md);
+
+ DX::Windowing()->SetTogglingHDR(true);
+ DX::Windowing()->SetAlteringWindow(true);
+
+ // Toggle display HDR
+ HDR_STATUS hdrStatus = CWIN32Util::ToggleWindowsHDR(md);
+
+ // Kill swapchain
+ if (m_swapChain && hdrStatus != HDR_STATUS::HDR_TOGGLE_FAILED)
+ {
+ CLog::LogF(LOGDEBUG, "Re-create swapchain due HDR <-> SDR switch");
+ DestroySwapChain();
+ }
+
+ DX::Windowing()->SetAlteringWindow(false);
+
+ // Re-create swapchain
+ if (hdrStatus != HDR_STATUS::HDR_TOGGLE_FAILED)
+ {
+ CreateWindowSizeDependentResources();
+
+ DX::Windowing()->NotifyAppFocusChange(true);
+ }
+
+ return hdrStatus;
+}
+
+void DX::DeviceResources::ApplyDisplaySettings()
+{
+ CLog::LogF(LOGDEBUG, "Re-create swapchain due Display Settings changed");
+
+ DestroySwapChain();
+ CreateWindowSizeDependentResources();
+}
+
+DEBUG_INFO_RENDER DX::DeviceResources::GetDebugInfo() const
+{
+ if (!m_swapChain)
+ return {};
+
+ DXGI_SWAP_CHAIN_DESC1 desc = {};
+ m_swapChain->GetDesc1(&desc);
+
+ DXGI_MODE_DESC md = {};
+ GetDisplayMode(&md);
+
+ const int bits = (desc.Format == DXGI_FORMAT_R10G10B10A2_UNORM) ? 10 : 8;
+ const int max = (desc.Format == DXGI_FORMAT_R10G10B10A2_UNORM) ? 1024 : 256;
+ const int range_min = DX::Windowing()->UseLimitedColor() ? (max * 16) / 256 : 0;
+ const int range_max = DX::Windowing()->UseLimitedColor() ? (max * 235) / 256 : max - 1;
+
+ DEBUG_INFO_RENDER info;
+
+ info.renderFlags = StringUtils::Format(
+ "Swapchain: {} buffers, flip {}, {}, EOTF: {} (Windows HDR {})", desc.BufferCount,
+ (desc.SwapEffect == DXGI_SWAP_EFFECT_FLIP_DISCARD) ? "discard" : "sequential",
+ Windowing()->IsFullScreen()
+ ? ((desc.Flags == DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH) ? "fullscreen exclusive"
+ : "fullscreen windowed")
+ : "windowed screen",
+ m_IsTransferPQ ? "PQ" : "SDR", m_IsHDROutput ? "on" : "off");
+
+ info.videoOutput = StringUtils::Format(
+ "Surfaces: {}x{}{} @ {:.3f} Hz, pixel: {} {}-bit, range: {} ({}-{})", desc.Width, desc.Height,
+ (md.ScanlineOrdering > DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE) ? "i" : "p",
+ static_cast<double>(md.RefreshRate.Numerator) /
+ static_cast<double>(md.RefreshRate.Denominator),
+ (desc.Format == DXGI_FORMAT_R10G10B10A2_UNORM) ? "R10G10B10A2" : "B8G8R8A8", bits,
+ DX::Windowing()->UseLimitedColor() ? "limited" : "full", range_min, range_max);
+
+ return info;
+}
diff --git a/xbmc/rendering/dx/DeviceResources.h b/xbmc/rendering/dx/DeviceResources.h
new file mode 100644
index 0000000..d458902
--- /dev/null
+++ b/xbmc/rendering/dx/DeviceResources.h
@@ -0,0 +1,187 @@
+/*
+ * 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 "DirectXHelper.h"
+#include "HDRStatus.h"
+#include "guilib/D3DResource.h"
+
+#include <functional>
+#include <memory>
+
+#include <concrt.h>
+#include <dxgi1_5.h>
+#include <wrl.h>
+#include <wrl/client.h>
+
+struct RESOLUTION_INFO;
+struct DEBUG_INFO_RENDER;
+struct VideoDriverInfo;
+
+namespace DX
+{
+ interface IDeviceNotify
+ {
+ virtual void OnDXDeviceLost() = 0;
+ virtual void OnDXDeviceRestored() = 0;
+ };
+
+ // Controls all the DirectX device resources.
+ class DeviceResources
+ {
+ public:
+ static std::shared_ptr<DX::DeviceResources> Get();
+
+ DeviceResources();
+ virtual ~DeviceResources();
+ void Release();
+
+ void ValidateDevice();
+ void HandleDeviceLost(bool removed);
+ bool Begin();
+ void Present();
+
+ // The size of the render target, in pixels.
+ winrt::Windows::Foundation::Size GetOutputSize() const { return m_outputSize; }
+ // The size of the render target, in dips.
+ winrt::Windows::Foundation::Size GetLogicalSize() const { return m_logicalSize; }
+ void SetLogicalSize(float width, float height);
+ float GetDpi() const { return m_effectiveDpi; }
+ void SetDpi(float dpi);
+
+ // D3D Accessors.
+ bool HasValidDevice() const { return m_bDeviceCreated; }
+ ID3D11Device1* GetD3DDevice() const { return m_d3dDevice.Get(); }
+ ID3D11DeviceContext1* GetD3DContext() const { return m_deferrContext.Get(); }
+ ID3D11DeviceContext1* GetImmediateContext() const { return m_d3dContext.Get(); }
+ IDXGISwapChain1* GetSwapChain() const { return m_swapChain.Get(); }
+ IDXGIFactory2* GetIDXGIFactory2() const { return m_dxgiFactory.Get(); }
+ IDXGIAdapter1* GetAdapter() const { return m_adapter.Get(); }
+ ID3D11DepthStencilView* GetDSV() const { return m_d3dDepthStencilView.Get(); }
+ D3D_FEATURE_LEVEL GetDeviceFeatureLevel() const { return m_d3dFeatureLevel; }
+ CD3DTexture& GetBackBuffer() { return m_backBufferTex; }
+
+ void GetOutput(IDXGIOutput** ppOutput) const;
+ void GetAdapterDesc(DXGI_ADAPTER_DESC *desc) const;
+ void GetDisplayMode(DXGI_MODE_DESC *mode) const;
+
+ D3D11_VIEWPORT GetScreenViewport() const { return m_screenViewport; }
+ void SetViewPort(D3D11_VIEWPORT& viewPort) const;
+
+ void ReleaseBackBuffer();
+ void CreateBackBuffer();
+ void ResizeBuffers();
+
+ bool SetFullScreen(bool fullscreen, RESOLUTION_INFO& res);
+
+ // Apply display settings changes
+ void ApplyDisplaySettings();
+
+ // HDR display support
+ HDR_STATUS ToggleHDR();
+ void SetHdrMetaData(DXGI_HDR_METADATA_HDR10& hdr10) const;
+ void SetHdrColorSpace(const DXGI_COLOR_SPACE_TYPE colorSpace);
+ bool IsHDROutput() const { return m_IsHDROutput; }
+ bool IsTransferPQ() const { return m_IsTransferPQ; }
+
+ // DX resources registration
+ void Register(ID3DResource *resource);
+ void Unregister(ID3DResource *resource);
+
+ void FinishCommandList(bool bExecute = true) const;
+ void ClearDepthStencil() const;
+ void ClearRenderTarget(ID3D11RenderTargetView* pRTView, float color[4]) const;
+ void RegisterDeviceNotify(IDeviceNotify* deviceNotify);
+
+ bool IsStereoAvailable() const;
+ bool IsStereoEnabled() const { return m_stereoEnabled; }
+ void SetStereoIdx(byte idx) { m_backBufferTex.SetViewIdx(idx); }
+
+ void SetMonitor(HMONITOR monitor);
+ HMONITOR GetMonitor() const;
+#if defined(TARGET_WINDOWS_DESKTOP)
+ void SetWindow(HWND window);
+#elif defined(TARGET_WINDOWS_STORE)
+ void Trim() const;
+ void SetWindow(const winrt::Windows::UI::Core::CoreWindow& window);
+ void SetWindowPos(winrt::Windows::Foundation::Rect rect);
+#endif // TARGET_WINDOWS_STORE
+ bool IsNV12SharedTexturesSupported() const { return m_NV12SharedTexturesSupport; }
+ bool IsDXVA2SharedDecoderSurfaces() const { return m_DXVA2SharedDecoderSurfaces; }
+
+ // Gets debug info from swapchain
+ DEBUG_INFO_RENDER GetDebugInfo() const;
+
+ private:
+ class CBackBuffer : public CD3DTexture
+ {
+ public:
+ CBackBuffer() : CD3DTexture() {}
+ void SetViewIdx(unsigned idx) { m_viewIdx = idx; }
+ bool Acquire(ID3D11Texture2D* pTexture);
+ };
+
+ HRESULT CreateSwapChain(DXGI_SWAP_CHAIN_DESC1 &desc, DXGI_SWAP_CHAIN_FULLSCREEN_DESC &fsDesc, IDXGISwapChain1 **ppSwapChain) const;
+ void DestroySwapChain();
+ void CreateDeviceIndependentResources();
+ void CreateDeviceResources();
+ void CreateWindowSizeDependentResources();
+ void UpdateRenderTargetSize();
+ void OnDeviceLost(bool removed);
+ void OnDeviceRestored();
+ void HandleOutputChange(const std::function<bool(DXGI_OUTPUT_DESC)>& cmpFunc);
+ bool CreateFactory();
+ void CheckNV12SharedTexturesSupport();
+ VideoDriverInfo GetVideoDriverVersion();
+ void CheckDXVA2SharedDecoderSurfaces();
+
+ HWND m_window{ nullptr };
+#if defined(TARGET_WINDOWS_STORE)
+ winrt::Windows::UI::Core::CoreWindow m_coreWindow = nullptr;
+#endif
+ Microsoft::WRL::ComPtr<IDXGIFactory2> m_dxgiFactory;
+ Microsoft::WRL::ComPtr<IDXGIAdapter1> m_adapter;
+ Microsoft::WRL::ComPtr<IDXGIOutput1> m_output;
+
+ Microsoft::WRL::ComPtr<ID3D11Device1> m_d3dDevice;
+ Microsoft::WRL::ComPtr<ID3D11DeviceContext1> m_d3dContext;
+ Microsoft::WRL::ComPtr<ID3D11DeviceContext1> m_deferrContext;
+ Microsoft::WRL::ComPtr<IDXGISwapChain1> m_swapChain;
+#ifdef _DEBUG
+ Microsoft::WRL::ComPtr<ID3D11Debug> m_d3dDebug;
+#endif
+
+ CBackBuffer m_backBufferTex;
+ Microsoft::WRL::ComPtr<ID3D11DepthStencilView> m_d3dDepthStencilView;
+ D3D11_VIEWPORT m_screenViewport;
+
+ // Cached device properties.
+ D3D_FEATURE_LEVEL m_d3dFeatureLevel;
+ winrt::Windows::Foundation::Size m_outputSize;
+ winrt::Windows::Foundation::Size m_logicalSize;
+ float m_dpi;
+
+ // This is the DPI that will be reported back to the app. It takes into account whether the app supports high resolution screens or not.
+ float m_effectiveDpi;
+ // The IDeviceNotify can be held directly as it owns the DeviceResources.
+ IDeviceNotify* m_deviceNotify;
+
+ // scritical section
+ Concurrency::critical_section m_criticalSection;
+ Concurrency::critical_section m_resourceSection;
+ std::vector<ID3DResource*> m_resources;
+
+ bool m_stereoEnabled;
+ bool m_bDeviceCreated;
+ bool m_IsHDROutput;
+ bool m_IsTransferPQ;
+ bool m_NV12SharedTexturesSupport{false};
+ bool m_DXVA2SharedDecoderSurfaces{false};
+ };
+}
diff --git a/xbmc/rendering/dx/DirectXHelper.h b/xbmc/rendering/dx/DirectXHelper.h
new file mode 100644
index 0000000..bb51ca6
--- /dev/null
+++ b/xbmc/rendering/dx/DirectXHelper.h
@@ -0,0 +1,198 @@
+/*
+ * 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 "commons/Exception.h"
+#include "dxerr.h"
+
+#include "platform/win32/CharsetConverter.h"
+
+#include <d3d11_4.h>
+#include <ppltasks.h> // For create_task
+
+enum PCI_Vendors
+{
+ PCIV_AMD = 0x1002,
+ PCIV_NVIDIA = 0x10DE,
+ PCIV_Intel = 0x8086,
+};
+
+namespace DX
+{
+#define RATIONAL_TO_FLOAT(rational) ((rational.Denominator != 0) ? \
+ static_cast<float>(rational.Numerator) / static_cast<float>(rational.Denominator) : 0.0f)
+
+ namespace DisplayMetrics
+ {
+ // High resolution displays can require a lot of GPU and battery power to render.
+ // High resolution phones, for example, may suffer from poor battery life if
+ // games attempt to render at 60 frames per second at full fidelity.
+ // The decision to render at full fidelity across all platforms and form factors
+ // should be deliberate.
+ static const bool SupportHighResolutions = true;
+
+ // The default thresholds that define a "high resolution" display. If the thresholds
+ // are exceeded and SupportHighResolutions is false, the dimensions will be scaled
+ // by 50%.
+ static const float Dpi100 = 96.0f; // 100% of standard desktop display.
+ static const float DpiThreshold = 192.0f; // 200% of standard desktop display.
+ static const float WidthThreshold = 1920.0f; // 1080p width.
+ static const float HeightThreshold = 1080.0f; // 1080p height.
+ };
+
+ inline void BreakIfFailed(HRESULT hr)
+ {
+ if (FAILED(hr))
+ {
+ // Set a breakpoint on this line to catch Win32 API errors.
+#if _DEBUG && !defined(TARGET_WINDOWS_STORE)
+ DebugBreak();
+#endif
+ throw new XbmcCommons::UncheckedException(__FUNCTION__, "Unhandled error");
+ }
+ }
+
+ // Converts a length in device-independent pixels (DIPs) to a length in physical pixels.
+ inline float ConvertDipsToPixels(float dips, float dpi)
+ {
+ static const float dipsPerInch = DisplayMetrics::Dpi100;
+ return floorf(dips * dpi / dipsPerInch + 0.5f); // Round to nearest integer.
+ }
+
+ inline float ConvertPixelsToDips(float pixels, float dpi)
+ {
+ static const float dipsPerInch = DisplayMetrics::Dpi100;
+ return floorf(pixels / (dpi / dipsPerInch) + 0.5f); // Round to nearest integer.
+ }
+
+ inline float RationalToFloat(DXGI_RATIONAL rational)
+ {
+ return RATIONAL_TO_FLOAT(rational);
+ }
+
+ inline void GetRefreshRatio(uint32_t refresh, uint32_t *num, uint32_t *den)
+ {
+ int i = (((refresh + 1) % 24) == 0 || ((refresh + 1) % 30) == 0) ? 1 : 0;
+ *num = (refresh + i) * 1000;
+ *den = 1000 + i;
+ }
+
+ inline std::string GetErrorDescription(HRESULT hr)
+ {
+ using namespace KODI::PLATFORM::WINDOWS;
+
+ WCHAR buff[2048];
+ DXGetErrorDescriptionW(hr, buff, 2048);
+
+ return FromW(StringUtils::Format(L"{:X} - {} ({})", hr, DXGetErrorStringW(hr), buff));
+ }
+
+ inline std::string GetFeatureLevelDescription(D3D_FEATURE_LEVEL featureLevel)
+ {
+ uint32_t fl_major = (featureLevel & 0xF000u) >> 12;
+ uint32_t fl_minor = (featureLevel & 0x0F00u) >> 8;
+
+ return StringUtils::Format("D3D_FEATURE_LEVEL_{}_{}", fl_major, fl_minor);
+ }
+
+ inline std::string GetGFXProviderName(UINT vendorId)
+ {
+ std::string name;
+ switch (vendorId)
+ {
+ case PCIV_AMD:
+ name = "AMD";
+ break;
+ case PCIV_Intel:
+ name = "Intel";
+ break;
+ case PCIV_NVIDIA:
+ name = "NVIDIA";
+ break;
+ }
+
+ return name;
+ }
+
+ template <typename T> struct SizeGen
+ {
+ SizeGen<T>() { Width = Height = 0; }
+ SizeGen<T>(T width, T height) { Width = width; Height = height; }
+
+ bool operator !=(const SizeGen<T> &size) const
+ {
+ return Width != size.Width || Height != size.Height;
+ }
+
+ const SizeGen<T> &operator -=(const SizeGen<T> &size)
+ {
+ Width -= size.Width;
+ Height -= size.Height;
+ return *this;
+ };
+
+ const SizeGen<T> &operator +=(const SizeGen<T> &size)
+ {
+ Width += size.Width;
+ Height += size.Height;
+ return *this;
+ };
+
+ const SizeGen<T> &operator -=(const T &size)
+ {
+ Width -= size;
+ Height -= size;
+ return *this;
+ };
+
+ const SizeGen<T> &operator +=(const T &size)
+ {
+ Width += size;
+ Height += size;
+ return *this;
+ };
+
+ T Width, Height;
+ };
+
+#if defined(_DEBUG)
+ // Check for SDK Layer support.
+ inline bool SdkLayersAvailable()
+ {
+ HRESULT hr = D3D11CreateDevice(
+ nullptr,
+ D3D_DRIVER_TYPE_NULL, // There is no need to create a real hardware device.
+ nullptr,
+ D3D11_CREATE_DEVICE_DEBUG, // Check for the SDK layers.
+ nullptr, // Any feature level will do.
+ 0,
+ D3D11_SDK_VERSION, // Always set this to D3D11_SDK_VERSION for Windows Store apps.
+ nullptr, // No need to keep the D3D device reference.
+ nullptr, // No need to know the feature level.
+ nullptr // No need to keep the D3D device context reference.
+ );
+
+ return SUCCEEDED(hr);
+ }
+#endif
+}
+
+#ifdef TARGET_WINDOWS_DESKTOP
+namespace winrt
+{
+ namespace Windows
+ {
+ namespace Foundation
+ {
+ typedef DX::SizeGen<float> Size;
+ typedef DX::SizeGen<int> SizeInt;
+ }
+ }
+}
+#endif
diff --git a/xbmc/rendering/dx/RenderContext.h b/xbmc/rendering/dx/RenderContext.h
new file mode 100644
index 0000000..9878691
--- /dev/null
+++ b/xbmc/rendering/dx/RenderContext.h
@@ -0,0 +1,31 @@
+/*
+ * 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
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+#include "windowing/windows/WinSystemWin32DX.h"
+#elif defined(TARGET_WINDOWS_STORE)
+#include "windowing/win10/WinSystemWin10DX.h"
+#endif
+#include "ServiceBroker.h"
+
+namespace DX
+{
+#if defined(TARGET_WINDOWS_DESKTOP)
+ __inline CWinSystemWin32DX* Windowing()
+ {
+ return dynamic_cast<CWinSystemWin32DX*>(CServiceBroker::GetRenderSystem());
+ }
+#elif defined(TARGET_WINDOWS_STORE)
+ __inline CWinSystemWin10DX* Windowing()
+ {
+ return dynamic_cast<CWinSystemWin10DX*>(CServiceBroker::GetRenderSystem());
+ }
+#endif
+}
diff --git a/xbmc/rendering/dx/RenderSystemDX.cpp b/xbmc/rendering/dx/RenderSystemDX.cpp
new file mode 100644
index 0000000..cb1b0d5
--- /dev/null
+++ b/xbmc/rendering/dx/RenderSystemDX.cpp
@@ -0,0 +1,706 @@
+/*
+ * 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 "RenderSystemDX.h"
+
+#include "application/Application.h"
+
+#include <mutex>
+#if defined(TARGET_WINDOWS_DESKTOP)
+#include "cores/RetroPlayer/process/windows/RPProcessInfoWin.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPWinRenderer.h"
+#endif
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DXVA.h"
+#if defined(TARGET_WINDOWS_STORE)
+#include "cores/VideoPlayer/Process/windows/ProcessInfoWin10.h"
+#else
+#include "cores/VideoPlayer/Process/windows/ProcessInfoWin.h"
+#endif
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "cores/VideoPlayer/VideoRenderers/WinRenderer.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderManager.h"
+#include "guilib/D3DResource.h"
+#include "guilib/GUIShaderDX.h"
+#include "guilib/GUITextureD3D.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/MathUtils.h"
+#include "utils/log.h"
+
+#include <DirectXPackedVector.h>
+
+extern "C" {
+#include <libavutil/pixfmt.h>
+}
+
+using namespace KODI;
+using namespace DirectX;
+using namespace DirectX::PackedVector;
+using namespace Microsoft::WRL;
+using namespace std::chrono_literals;
+
+CRenderSystemDX::CRenderSystemDX() : CRenderSystemBase()
+ , m_interlaced(false)
+{
+ m_bVSync = true;
+
+ memset(&m_viewPort, 0, sizeof m_viewPort);
+ memset(&m_scissor, 0, sizeof m_scissor);
+}
+
+CRenderSystemDX::~CRenderSystemDX() = default;
+
+bool CRenderSystemDX::InitRenderSystem()
+{
+ m_bVSync = true;
+
+ // check various device capabilities
+ CheckDeviceCaps();
+
+ if (!CreateStates() || !InitGUIShader())
+ return false;
+
+ m_bRenderCreated = true;
+ m_deviceResources->RegisterDeviceNotify(this);
+
+ // register platform dependent objects
+#if defined(TARGET_WINDOWS_STORE)
+ VIDEOPLAYER::CProcessInfoWin10::Register();
+#else
+ VIDEOPLAYER::CProcessInfoWin::Register();
+#endif
+ CDVDFactoryCodec::ClearHWAccels();
+ DXVA::CDecoder::Register();
+ VIDEOPLAYER::CRendererFactory::ClearRenderer();
+ CWinRenderer::Register();
+#if defined(TARGET_WINDOWS_DESKTOP)
+ RETRO::CRPProcessInfoWin::Register();
+ RETRO::CRPProcessInfoWin::RegisterRendererFactory(new RETRO::CWinRendererFactory);
+#endif
+ m_viewPort = m_deviceResources->GetScreenViewport();
+ RestoreViewPort();
+
+ auto outputSize = m_deviceResources->GetOutputSize();
+ // set camera to center of screen
+ CPoint camPoint = { outputSize.Width * 0.5f, outputSize.Height * 0.5f };
+ SetCameraPosition(camPoint, outputSize.Width, outputSize.Height);
+
+ DXGI_ADAPTER_DESC AIdentifier = {};
+ m_deviceResources->GetAdapterDesc(&AIdentifier);
+ m_RenderRenderer = KODI::PLATFORM::WINDOWS::FromW(AIdentifier.Description);
+ uint32_t version = 0;
+ for (uint32_t decimal = m_deviceResources->GetDeviceFeatureLevel() >> 8, round = 0; decimal > 0; decimal >>= 4, ++round)
+ version += (decimal % 16) * std::pow(10, round);
+ m_RenderVersion = StringUtils::Format("{:.1f}", static_cast<float>(version) / 10.0f);
+
+ CGUITextureD3D::Register();
+
+ return true;
+}
+
+void CRenderSystemDX::OnResize()
+{
+ if (!m_bRenderCreated)
+ return;
+
+ auto outputSize = m_deviceResources->GetOutputSize();
+
+ // set camera to center of screen
+ CPoint camPoint = { outputSize.Width * 0.5f, outputSize.Height * 0.5f };
+ SetCameraPosition(camPoint, outputSize.Width, outputSize.Height);
+
+ CheckInterlacedStereoView();
+}
+
+bool CRenderSystemDX::IsFormatSupport(DXGI_FORMAT format, unsigned int usage) const
+{
+ ComPtr<ID3D11Device1> pD3DDev = m_deviceResources->GetD3DDevice();
+ UINT supported;
+ pD3DDev->CheckFormatSupport(format, &supported);
+ return (supported & usage) != 0;
+}
+
+bool CRenderSystemDX::DestroyRenderSystem()
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+
+ if (m_pGUIShader)
+ {
+ m_pGUIShader->End();
+ delete m_pGUIShader;
+ m_pGUIShader = nullptr;
+ }
+ m_rightEyeTex.Release();
+ m_BlendEnableState = nullptr;
+ m_BlendDisableState = nullptr;
+ m_RSScissorDisable = nullptr;
+ m_RSScissorEnable = nullptr;
+ m_depthStencilState = nullptr;
+
+ return true;
+}
+
+void CRenderSystemDX::CheckInterlacedStereoView()
+{
+ RENDER_STEREO_MODE stereoMode = CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode();
+
+ if ( m_rightEyeTex.Get()
+ && RENDER_STEREO_MODE_INTERLACED != stereoMode
+ && RENDER_STEREO_MODE_CHECKERBOARD != stereoMode)
+ {
+ m_rightEyeTex.Release();
+ }
+
+ if ( !m_rightEyeTex.Get()
+ && ( RENDER_STEREO_MODE_INTERLACED == stereoMode
+ || RENDER_STEREO_MODE_CHECKERBOARD == stereoMode))
+ {
+ const auto outputSize = m_deviceResources->GetOutputSize();
+ DXGI_FORMAT texFormat = m_deviceResources->GetBackBuffer().GetFormat();
+ if (!m_rightEyeTex.Create(outputSize.Width, outputSize.Height, 1, D3D11_USAGE_DEFAULT, texFormat))
+ {
+ CLog::Log(LOGERROR, "{} - Failed to create right eye buffer.", __FUNCTION__);
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetStereoMode(RENDER_STEREO_MODE_SPLIT_HORIZONTAL); // try fallback to split horizontal
+ }
+ else
+ m_deviceResources->Unregister(&m_rightEyeTex); // we will handle its health
+ }
+}
+
+bool CRenderSystemDX::CreateStates()
+{
+ auto m_pD3DDev = m_deviceResources->GetD3DDevice();
+ auto m_pContext = m_deviceResources->GetD3DContext();
+
+ if (!m_pD3DDev)
+ return false;
+
+ m_BlendEnableState = nullptr;
+ m_BlendDisableState = nullptr;
+
+ // Initialize the description of the stencil state.
+ D3D11_DEPTH_STENCIL_DESC depthStencilDesc = {};
+
+ // Set up the description of the stencil state.
+ depthStencilDesc.DepthEnable = false;
+ depthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
+ depthStencilDesc.DepthFunc = D3D11_COMPARISON_NEVER;
+ depthStencilDesc.StencilEnable = false;
+ depthStencilDesc.StencilReadMask = 0xFF;
+ depthStencilDesc.StencilWriteMask = 0xFF;
+
+ // Stencil operations if pixel is front-facing.
+ depthStencilDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
+ depthStencilDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_INCR;
+ depthStencilDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
+ depthStencilDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
+
+ // Stencil operations if pixel is back-facing.
+ depthStencilDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
+ depthStencilDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_DECR;
+ depthStencilDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
+ depthStencilDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
+
+ // Create the depth stencil state.
+ HRESULT hr = m_pD3DDev->CreateDepthStencilState(&depthStencilDesc, &m_depthStencilState);
+ if(FAILED(hr))
+ return false;
+
+ // Set the depth stencil state.
+ m_pContext->OMSetDepthStencilState(m_depthStencilState.Get(), 0);
+
+ D3D11_RASTERIZER_DESC rasterizerState;
+ rasterizerState.CullMode = D3D11_CULL_NONE;
+ rasterizerState.FillMode = D3D11_FILL_SOLID;// DEBUG - D3D11_FILL_WIREFRAME
+ rasterizerState.FrontCounterClockwise = false;
+ rasterizerState.DepthBias = 0;
+ rasterizerState.DepthBiasClamp = 0.0f;
+ rasterizerState.DepthClipEnable = true;
+ rasterizerState.SlopeScaledDepthBias = 0.0f;
+ rasterizerState.ScissorEnable = false;
+ rasterizerState.MultisampleEnable = false;
+ rasterizerState.AntialiasedLineEnable = false;
+
+ if (FAILED(m_pD3DDev->CreateRasterizerState(&rasterizerState, &m_RSScissorDisable)))
+ return false;
+
+ rasterizerState.ScissorEnable = true;
+ if (FAILED(m_pD3DDev->CreateRasterizerState(&rasterizerState, &m_RSScissorEnable)))
+ return false;
+
+ m_pContext->RSSetState(m_RSScissorDisable.Get()); // by default
+
+ D3D11_BLEND_DESC blendState = {};
+ blendState.RenderTarget[0].BlendEnable = true;
+ blendState.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; // D3D11_BLEND_SRC_ALPHA;
+ blendState.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; // D3D11_BLEND_INV_SRC_ALPHA;
+ blendState.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
+ blendState.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
+ blendState.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
+ blendState.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
+ blendState.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
+
+ m_pD3DDev->CreateBlendState(&blendState, &m_BlendEnableState);
+
+ blendState.RenderTarget[0].BlendEnable = false;
+ m_pD3DDev->CreateBlendState(&blendState, &m_BlendDisableState);
+
+ // by default
+ m_pContext->OMSetBlendState(m_BlendEnableState.Get(), nullptr, 0xFFFFFFFF);
+ m_BlendEnabled = true;
+
+ return true;
+}
+
+void CRenderSystemDX::PresentRender(bool rendered, bool videoLayer)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ if ( rendered
+ && ( m_stereoMode == RENDER_STEREO_MODE_INTERLACED
+ || m_stereoMode == RENDER_STEREO_MODE_CHECKERBOARD))
+ {
+ auto m_pContext = m_deviceResources->GetD3DContext();
+
+ // all views prepared, let's merge them before present
+ m_pContext->OMSetRenderTargets(1, m_deviceResources->GetBackBuffer().GetAddressOfRTV(), m_deviceResources->GetDSV());
+
+ auto outputSize = m_deviceResources->GetOutputSize();
+ CRect destRect = { 0.0f, 0.0f, float(outputSize.Width), float(outputSize.Height) };
+
+ SHADER_METHOD method = RENDER_STEREO_MODE_INTERLACED == m_stereoMode
+ ? SHADER_METHOD_RENDER_STEREO_INTERLACED_RIGHT
+ : SHADER_METHOD_RENDER_STEREO_CHECKERBOARD_RIGHT;
+ SetAlphaBlendEnable(true);
+ CD3DTexture::DrawQuad(destRect, 0, &m_rightEyeTex, nullptr, method);
+ CD3DHelper::PSClearShaderResources(m_pContext);
+ }
+
+ // time for decoder that may require the context
+ {
+ std::unique_lock<CCriticalSection> lock(m_decoderSection);
+ XbmcThreads::EndTime<> timer;
+ timer.Set(5ms);
+ while (!m_decodingTimer.IsTimePast() && !timer.IsTimePast())
+ {
+ m_decodingEvent.wait(lock, 1ms);
+ }
+ }
+
+ PresentRenderImpl(rendered);
+}
+
+void CRenderSystemDX::RequestDecodingTime()
+{
+ std::unique_lock<CCriticalSection> lock(m_decoderSection);
+ m_decodingTimer.Set(3ms);
+}
+
+void CRenderSystemDX::ReleaseDecodingTime()
+{
+ std::unique_lock<CCriticalSection> lock(m_decoderSection);
+ m_decodingTimer.SetExpired();
+ m_decodingEvent.notify();
+}
+
+bool CRenderSystemDX::BeginRender()
+{
+ if (!m_bRenderCreated)
+ return false;
+
+ m_limitedColorRange = CServiceBroker::GetWinSystem()->UseLimitedColor();
+ m_inScene = m_deviceResources->Begin();
+ return m_inScene;
+}
+
+bool CRenderSystemDX::EndRender()
+{
+ m_inScene = false;
+
+ if (!m_bRenderCreated)
+ return false;
+
+ return true;
+}
+
+bool CRenderSystemDX::ClearBuffers(UTILS::COLOR::Color color)
+{
+ if (!m_bRenderCreated)
+ return false;
+
+ float fColor[4];
+ CD3DHelper::XMStoreColor(fColor, color);
+ ID3D11RenderTargetView* pRTView = m_deviceResources->GetBackBuffer().GetRenderTarget();
+
+ if ( m_stereoMode != RENDER_STEREO_MODE_OFF
+ && m_stereoMode != RENDER_STEREO_MODE_MONO)
+ {
+ // if stereo anaglyph/tab/sbs, data was cleared when left view was rendered
+ if (m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ {
+ // execute command's queue
+ m_deviceResources->FinishCommandList();
+
+ // do not clear RT for anaglyph modes
+ if ( m_stereoMode == RENDER_STEREO_MODE_ANAGLYPH_GREEN_MAGENTA
+ || m_stereoMode == RENDER_STEREO_MODE_ANAGLYPH_RED_CYAN
+ || m_stereoMode == RENDER_STEREO_MODE_ANAGLYPH_YELLOW_BLUE)
+ {
+ pRTView = nullptr;
+ }
+ // for interlaced/checkerboard clear view for right texture
+ else if (m_stereoMode == RENDER_STEREO_MODE_INTERLACED
+ || m_stereoMode == RENDER_STEREO_MODE_CHECKERBOARD)
+ {
+ pRTView = m_rightEyeTex.GetRenderTarget();
+ }
+ }
+ }
+
+ if (pRTView == nullptr)
+ return true;
+
+ auto outputSize = m_deviceResources->GetOutputSize();
+ CRect clRect(0.0f, 0.0f,
+ static_cast<float>(outputSize.Width),
+ static_cast<float>(outputSize.Height));
+
+ // Unlike Direct3D 9, D3D11 ClearRenderTargetView always clears full extent of the resource view.
+ // Viewport and scissor settings are not applied. So clear RT by drawing full sized rect with clear color
+ if (m_ScissorsEnabled && m_scissor != clRect)
+ {
+ bool alphaEnabled = m_BlendEnabled;
+ if (alphaEnabled)
+ SetAlphaBlendEnable(false);
+
+ CGUITextureD3D::DrawQuad(clRect, color);
+
+ if (alphaEnabled)
+ SetAlphaBlendEnable(true);
+ }
+ else
+ m_deviceResources->ClearRenderTarget(pRTView, fColor);
+
+ m_deviceResources->ClearDepthStencil();
+ return true;
+}
+
+void CRenderSystemDX::CaptureStateBlock()
+{
+ if (!m_bRenderCreated)
+ return;
+}
+
+void CRenderSystemDX::ApplyStateBlock()
+{
+ if (!m_bRenderCreated)
+ return;
+
+ auto m_pContext = m_deviceResources->GetD3DContext();
+
+ m_pContext->RSSetState(m_ScissorsEnabled ? m_RSScissorEnable.Get() : m_RSScissorDisable.Get());
+ m_pContext->OMSetDepthStencilState(m_depthStencilState.Get(), 0);
+ float factors[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
+ m_pContext->OMSetBlendState(m_BlendEnabled ? m_BlendEnableState.Get() : m_BlendDisableState.Get(), factors, 0xFFFFFFFF);
+
+ m_pGUIShader->ApplyStateBlock();
+}
+
+void CRenderSystemDX::SetCameraPosition(const CPoint &camera, int screenWidth, int screenHeight, float stereoFactor)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ // grab the viewport dimensions and location
+ float w = m_viewPort.Width * 0.5f;
+ float h = m_viewPort.Height * 0.5f;
+
+ XMFLOAT2 offset = XMFLOAT2(camera.x - screenWidth*0.5f, camera.y - screenHeight*0.5f);
+
+ // world view. Until this is moved onto the GPU (via a vertex shader for instance), we set it to the identity here.
+ m_pGUIShader->SetWorld(XMMatrixIdentity());
+
+ // Initialize the view matrix camera view.
+ // Multiply the Y coord by -1 then translate so that everything is relative to the camera position.
+ XMMATRIX flipY = XMMatrixScaling(1.0, -1.0f, 1.0f);
+ XMMATRIX translate = XMMatrixTranslation(-(w + offset.x - stereoFactor), -(h + offset.y), 2 * h);
+ m_pGUIShader->SetView(XMMatrixMultiply(translate, flipY));
+
+ // projection onto screen space
+ m_pGUIShader->SetProjection(XMMatrixPerspectiveOffCenterLH((-w - offset.x)*0.5f, (w - offset.x)*0.5f, (-h + offset.y)*0.5f, (h + offset.y)*0.5f, h, 100 * h));
+}
+
+void CRenderSystemDX::Project(float &x, float &y, float &z)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ m_pGUIShader->Project(x, y, z);
+}
+
+CRect CRenderSystemDX::GetBackBufferRect()
+{
+ auto outputSize = m_deviceResources->GetOutputSize();
+ return CRect(0.f, 0.f, static_cast<float>(outputSize.Width), static_cast<float>(outputSize.Height));
+}
+
+void CRenderSystemDX::GetViewPort(CRect& viewPort)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ viewPort.x1 = m_viewPort.TopLeftX;
+ viewPort.y1 = m_viewPort.TopLeftY;
+ viewPort.x2 = m_viewPort.TopLeftX + m_viewPort.Width;
+ viewPort.y2 = m_viewPort.TopLeftY + m_viewPort.Height;
+}
+
+void CRenderSystemDX::SetViewPort(const CRect& viewPort)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ m_viewPort.MinDepth = 0.0f;
+ m_viewPort.MaxDepth = 1.0f;
+ m_viewPort.TopLeftX = viewPort.x1;
+ m_viewPort.TopLeftY = viewPort.y1;
+ m_viewPort.Width = viewPort.x2 - viewPort.x1;
+ m_viewPort.Height = viewPort.y2 - viewPort.y1;
+
+ m_deviceResources->SetViewPort(m_viewPort);
+ m_pGUIShader->SetViewPort(m_viewPort);
+}
+
+void CRenderSystemDX::RestoreViewPort()
+{
+ if (!m_bRenderCreated)
+ return;
+
+ m_deviceResources->SetViewPort(m_viewPort);
+ m_pGUIShader->SetViewPort(m_viewPort);
+}
+
+bool CRenderSystemDX::ScissorsCanEffectClipping()
+{
+ if (!m_bRenderCreated)
+ return false;
+
+ return m_pGUIShader != nullptr && m_pGUIShader->HardwareClipIsPossible();
+}
+
+CRect CRenderSystemDX::ClipRectToScissorRect(const CRect &rect)
+{
+ if (!m_bRenderCreated)
+ return CRect();
+
+ float xFactor = m_pGUIShader->GetClipXFactor();
+ float xOffset = m_pGUIShader->GetClipXOffset();
+ float yFactor = m_pGUIShader->GetClipYFactor();
+ float yOffset = m_pGUIShader->GetClipYOffset();
+
+ return CRect(rect.x1 * xFactor + xOffset,
+ rect.y1 * yFactor + yOffset,
+ rect.x2 * xFactor + xOffset,
+ rect.y2 * yFactor + yOffset);
+}
+
+void CRenderSystemDX::SetScissors(const CRect& rect)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ auto m_pContext = m_deviceResources->GetD3DContext();
+
+ m_scissor = rect;
+ CD3D11_RECT scissor(MathUtils::round_int(rect.x1)
+ , MathUtils::round_int(rect.y1)
+ , MathUtils::round_int(rect.x2)
+ , MathUtils::round_int(rect.y2));
+
+ m_pContext->RSSetScissorRects(1, &scissor);
+ m_pContext->RSSetState(m_RSScissorEnable.Get());
+ m_ScissorsEnabled = true;
+}
+
+void CRenderSystemDX::ResetScissors()
+{
+ if (!m_bRenderCreated)
+ return;
+
+ auto m_pContext = m_deviceResources->GetD3DContext();
+ auto outputSize = m_deviceResources->GetOutputSize();
+
+ m_scissor.SetRect(0.0f, 0.0f,
+ static_cast<float>(outputSize.Width),
+ static_cast<float>(outputSize.Height));
+
+ m_pContext->RSSetState(m_RSScissorDisable.Get());
+ m_ScissorsEnabled = false;
+}
+
+void CRenderSystemDX::OnDXDeviceLost()
+{
+ CRenderSystemDX::DestroyRenderSystem();
+}
+
+void CRenderSystemDX::OnDXDeviceRestored()
+{
+ CRenderSystemDX::InitRenderSystem();
+}
+
+void CRenderSystemDX::SetStereoMode(RENDER_STEREO_MODE mode, RENDER_STEREO_VIEW view)
+{
+ CRenderSystemBase::SetStereoMode(mode, view);
+
+ if (!m_bRenderCreated)
+ return;
+
+ auto m_pContext = m_deviceResources->GetD3DContext();
+
+ UINT writeMask = D3D11_COLOR_WRITE_ENABLE_ALL;
+ if(m_stereoMode == RENDER_STEREO_MODE_ANAGLYPH_RED_CYAN)
+ {
+ if(m_stereoView == RENDER_STEREO_VIEW_LEFT)
+ writeMask = D3D11_COLOR_WRITE_ENABLE_RED;
+ else if(m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ writeMask = D3D11_COLOR_WRITE_ENABLE_BLUE | D3D11_COLOR_WRITE_ENABLE_GREEN;
+ }
+ if(m_stereoMode == RENDER_STEREO_MODE_ANAGLYPH_GREEN_MAGENTA)
+ {
+ if(m_stereoView == RENDER_STEREO_VIEW_LEFT)
+ writeMask = D3D11_COLOR_WRITE_ENABLE_GREEN;
+ else if(m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ writeMask = D3D11_COLOR_WRITE_ENABLE_BLUE | D3D11_COLOR_WRITE_ENABLE_RED;
+ }
+ if (m_stereoMode == RENDER_STEREO_MODE_ANAGLYPH_YELLOW_BLUE)
+ {
+ if (m_stereoView == RENDER_STEREO_VIEW_LEFT)
+ writeMask = D3D11_COLOR_WRITE_ENABLE_RED | D3D11_COLOR_WRITE_ENABLE_GREEN;
+ else if (m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ writeMask = D3D11_COLOR_WRITE_ENABLE_BLUE;
+ }
+ if ( RENDER_STEREO_MODE_INTERLACED == m_stereoMode
+ || RENDER_STEREO_MODE_CHECKERBOARD == m_stereoMode)
+ {
+ if (m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ {
+ m_pContext->OMSetRenderTargets(1, m_rightEyeTex.GetAddressOfRTV(), m_deviceResources->GetDSV());
+ }
+ }
+ else if (RENDER_STEREO_MODE_HARDWAREBASED == m_stereoMode)
+ {
+ m_deviceResources->SetStereoIdx(m_stereoView == RENDER_STEREO_VIEW_RIGHT ? 1 : 0);
+
+ m_pContext->OMSetRenderTargets(1, m_deviceResources->GetBackBuffer().GetAddressOfRTV(), m_deviceResources->GetDSV());
+ }
+
+ auto m_pD3DDev = m_deviceResources->GetD3DDevice();
+
+ D3D11_BLEND_DESC desc;
+ m_BlendEnableState->GetDesc(&desc);
+ // update blend state
+ if (desc.RenderTarget[0].RenderTargetWriteMask != writeMask)
+ {
+ m_BlendDisableState = nullptr;
+ m_BlendEnableState = nullptr;
+
+ desc.RenderTarget[0].RenderTargetWriteMask = writeMask;
+ m_pD3DDev->CreateBlendState(&desc, &m_BlendEnableState);
+
+ desc.RenderTarget[0].BlendEnable = false;
+ m_pD3DDev->CreateBlendState(&desc, &m_BlendDisableState);
+
+ float blendFactors[] = { 0.0f, 0.0f, 0.0f, 0.0f };
+ m_pContext->OMSetBlendState(m_BlendEnabled ? m_BlendEnableState.Get() : m_BlendDisableState.Get(), blendFactors, 0xFFFFFFFF);
+ }
+}
+
+bool CRenderSystemDX::SupportsStereo(RENDER_STEREO_MODE mode) const
+{
+ switch (mode)
+ {
+ case RENDER_STEREO_MODE_ANAGLYPH_RED_CYAN:
+ case RENDER_STEREO_MODE_ANAGLYPH_GREEN_MAGENTA:
+ case RENDER_STEREO_MODE_ANAGLYPH_YELLOW_BLUE:
+ case RENDER_STEREO_MODE_INTERLACED:
+ case RENDER_STEREO_MODE_CHECKERBOARD:
+ return true;
+ case RENDER_STEREO_MODE_HARDWAREBASED:
+ return m_deviceResources->IsStereoAvailable();
+ default:
+ return CRenderSystemBase::SupportsStereo(mode);
+ }
+}
+
+void CRenderSystemDX::FlushGPU() const
+{
+ if (!m_bRenderCreated)
+ return;
+
+ m_deviceResources->FinishCommandList();
+ m_deviceResources->GetImmediateContext()->Flush();
+}
+
+bool CRenderSystemDX::InitGUIShader()
+{
+ delete m_pGUIShader;
+ m_pGUIShader = nullptr;
+
+ m_pGUIShader = new CGUIShaderDX();
+ if (!m_pGUIShader->Initialize())
+ {
+ CLog::LogF(LOGERROR, "Failed to initialize GUI shader.");
+ return false;
+ }
+
+ m_pGUIShader->ApplyStateBlock();
+ return true;
+}
+
+void CRenderSystemDX::SetAlphaBlendEnable(bool enable)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ m_deviceResources->GetD3DContext()->OMSetBlendState(enable ? m_BlendEnableState.Get() : m_BlendDisableState.Get(), nullptr, 0xFFFFFFFF);
+ m_BlendEnabled = enable;
+}
+
+CD3DTexture& CRenderSystemDX::GetBackBuffer()
+{
+ if (m_stereoView == RENDER_STEREO_VIEW_RIGHT && m_rightEyeTex.Get())
+ return m_rightEyeTex;
+
+ return m_deviceResources->GetBackBuffer();
+}
+
+void CRenderSystemDX::CheckDeviceCaps()
+{
+ const auto feature_level = m_deviceResources->GetDeviceFeatureLevel();
+ if (feature_level < D3D_FEATURE_LEVEL_9_3)
+ m_maxTextureSize = D3D_FL9_1_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ else if (feature_level < D3D_FEATURE_LEVEL_10_0)
+ m_maxTextureSize = D3D_FL9_3_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ else if (feature_level < D3D_FEATURE_LEVEL_11_0)
+ m_maxTextureSize = D3D10_REQ_TEXTURE2D_U_OR_V_DIMENSION;
+ else
+ // 11_x and greater feature level. Limit this size to avoid memory overheads
+ m_maxTextureSize = D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION >> 1;
+}
+
+bool CRenderSystemDX::SupportsNPOT(bool dxt) const
+{
+ // MSDN says:
+ // At feature levels 9_1, 9_2 and 9_3, the display device supports the use
+ // of 2D textures with dimensions that are not powers of two under two conditions:
+ // 1) only one MIP-map level for each texture can be created - we are using both 1 and 0 mipmap levels
+ // 2) no wrap sampler modes for textures are allowed - we are using clamp everywhere
+ // At feature levels 10_0, 10_1 and 11_0, the display device unconditionally supports the use of 2D textures with dimensions that are not powers of two.
+ // taking in account first condition we setup caps NPOT for FE > 9.x only
+ return m_deviceResources->GetDeviceFeatureLevel() > D3D_FEATURE_LEVEL_9_3 ? true : false;
+}
diff --git a/xbmc/rendering/dx/RenderSystemDX.h b/xbmc/rendering/dx/RenderSystemDX.h
new file mode 100644
index 0000000..9a18e22
--- /dev/null
+++ b/xbmc/rendering/dx/RenderSystemDX.h
@@ -0,0 +1,106 @@
+/*
+ * 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 "DeviceResources.h"
+#include "rendering/RenderSystem.h"
+#include "threads/Condition.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "utils/ColorUtils.h"
+
+#include <wrl/client.h>
+
+class ID3DResource;
+class CGUIShaderDX;
+enum AVPixelFormat;
+enum AVPixelFormat;
+
+class CRenderSystemDX : public CRenderSystemBase, DX::IDeviceNotify
+{
+public:
+ CRenderSystemDX();
+ virtual ~CRenderSystemDX();
+
+ // CRenderBase overrides
+ bool InitRenderSystem() override;
+ bool DestroyRenderSystem() override;
+ bool BeginRender() override;
+ bool EndRender() override;
+ void PresentRender(bool rendered, bool videoLayer) override;
+ bool ClearBuffers(UTILS::COLOR::Color color) override;
+ void SetViewPort(const CRect& viewPort) override;
+ void GetViewPort(CRect& viewPort) override;
+ void RestoreViewPort() override;
+ CRect ClipRectToScissorRect(const CRect &rect) override;
+ bool ScissorsCanEffectClipping() override;
+ void SetScissors(const CRect &rect) override;
+ void ResetScissors() override;
+ void CaptureStateBlock() override;
+ void ApplyStateBlock() override;
+ void SetCameraPosition(const CPoint &camera, int screenWidth, int screenHeight, float stereoFactor = 0.f) override;
+ void SetStereoMode(RENDER_STEREO_MODE mode, RENDER_STEREO_VIEW view) override;
+ bool SupportsStereo(RENDER_STEREO_MODE mode) const override;
+ void Project(float &x, float &y, float &z) override;
+ bool SupportsNPOT(bool dxt) const override;
+
+ // IDeviceNotify overrides
+ void OnDXDeviceLost() override;
+ void OnDXDeviceRestored() override;
+
+ // CRenderSystemDX methods
+ CGUIShaderDX* GetGUIShader() const { return m_pGUIShader; }
+ bool Interlaced() const { return m_interlaced; }
+ bool IsFormatSupport(DXGI_FORMAT format, unsigned int usage) const;
+ CRect GetBackBufferRect();
+ CD3DTexture& GetBackBuffer();
+
+ void FlushGPU() const;
+ void RequestDecodingTime();
+ void ReleaseDecodingTime();
+ void SetAlphaBlendEnable(bool enable);
+
+ // empty overrides
+ bool IsExtSupported(const char* extension) const override { return false; }
+ bool ResetRenderSystem(int width, int height) override { return true; }
+
+protected:
+ virtual void PresentRenderImpl(bool rendered) = 0;
+
+ bool CreateStates();
+ bool InitGUIShader();
+ void OnResize();
+ void CheckInterlacedStereoView(void);
+ void CheckDeviceCaps();
+
+ CCriticalSection m_resourceSection;
+ CCriticalSection m_decoderSection;
+
+ // our adapter could change as we go
+ bool m_interlaced;
+ bool m_inScene{ false }; ///< True if we're in a BeginScene()/EndScene() block
+ bool m_BlendEnabled{ false };
+ bool m_ScissorsEnabled{ false };
+ D3D11_VIEWPORT m_viewPort;
+ CRect m_scissor;
+ CGUIShaderDX* m_pGUIShader{ nullptr };
+ Microsoft::WRL::ComPtr<ID3D11DepthStencilState> m_depthStencilState;
+ Microsoft::WRL::ComPtr<ID3D11BlendState> m_BlendEnableState;
+ Microsoft::WRL::ComPtr<ID3D11BlendState> m_BlendDisableState;
+ Microsoft::WRL::ComPtr<ID3D11RasterizerState> m_RSScissorDisable;
+ Microsoft::WRL::ComPtr<ID3D11RasterizerState> m_RSScissorEnable;
+ // stereo interlaced/checkerboard intermediate target
+ CD3DTexture m_rightEyeTex;
+
+ XbmcThreads::EndTime<> m_decodingTimer;
+ XbmcThreads::ConditionVariable m_decodingEvent;
+
+ std::shared_ptr<DX::DeviceResources> m_deviceResources;
+};
+
diff --git a/xbmc/rendering/dx/ScreenshotSurfaceWindows.cpp b/xbmc/rendering/dx/ScreenshotSurfaceWindows.cpp
new file mode 100644
index 0000000..7a62ad6
--- /dev/null
+++ b/xbmc/rendering/dx/ScreenshotSurfaceWindows.cpp
@@ -0,0 +1,110 @@
+/*
+ * 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 "ScreenshotSurfaceWindows.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "rendering/dx/DeviceResources.h"
+#include "utils/Screenshot.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <mutex>
+
+#include <wrl/client.h>
+
+using namespace Microsoft::WRL;
+
+void CScreenshotSurfaceWindows::Register()
+{
+ CScreenShot::Register(CScreenshotSurfaceWindows::CreateSurface);
+}
+
+std::unique_ptr<IScreenshotSurface> CScreenshotSurfaceWindows::CreateSurface()
+{
+ return std::unique_ptr<CScreenshotSurfaceWindows>(new CScreenshotSurfaceWindows());
+}
+
+bool CScreenshotSurfaceWindows::Capture()
+{
+ CWinSystemBase* winsystem = CServiceBroker::GetWinSystem();
+ if (!winsystem)
+ return false;
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (!gui)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(winsystem->GetGfxContext());
+ gui->GetWindowManager().Render();
+
+ auto deviceResources = DX::DeviceResources::Get();
+ deviceResources->FinishCommandList();
+
+ ComPtr<ID3D11DeviceContext> pImdContext = deviceResources->GetImmediateContext();
+ ComPtr<ID3D11Device> pDevice = deviceResources->GetD3DDevice();
+ CD3DTexture& backbuffer = deviceResources->GetBackBuffer();
+ if (!backbuffer.Get())
+ return false;
+
+ D3D11_TEXTURE2D_DESC desc = {};
+ backbuffer.GetDesc(&desc);
+ desc.Usage = D3D11_USAGE_STAGING;
+ desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
+ desc.BindFlags = 0;
+
+ ComPtr<ID3D11Texture2D> pCopyTexture = nullptr;
+ if (SUCCEEDED(pDevice->CreateTexture2D(&desc, nullptr, &pCopyTexture)))
+ {
+ // take copy
+ pImdContext->CopyResource(pCopyTexture.Get(), backbuffer.Get());
+
+ D3D11_MAPPED_SUBRESOURCE res;
+ if (SUCCEEDED(pImdContext->Map(pCopyTexture.Get(), 0, D3D11_MAP_READ, 0, &res)))
+ {
+ m_width = desc.Width;
+ m_height = desc.Height;
+ m_stride = res.RowPitch;
+ m_buffer = new unsigned char[m_height * m_stride];
+ if (desc.Format == DXGI_FORMAT_R10G10B10A2_UNORM)
+ {
+ // convert R10G10B10A2 -> B8G8R8A8
+ for (int y = 0; y < m_height; y++)
+ {
+ uint32_t* pixels10 = reinterpret_cast<uint32_t*>(static_cast<uint8_t*>(res.pData) + y * res.RowPitch);
+ uint8_t* pixels8 = m_buffer + y * m_stride;
+
+ for (int x = 0; x < m_width; x++, pixels10++, pixels8 += 4)
+ {
+ // actual bit per channel is A2B10G10R10
+ uint32_t pixel = *pixels10;
+ // R
+ pixels8[2] = static_cast<uint8_t>((pixel & 0x3FF) * 255 / 1023);
+ // G
+ pixel >>= 10;
+ pixels8[1] = static_cast<uint8_t>((pixel & 0x3FF) * 255 / 1023);
+ // B
+ pixel >>= 10;
+ pixels8[0] = static_cast<uint8_t>((pixel & 0x3FF) * 255 / 1023);
+ // A
+ pixels8[3] = 0xFF;
+ }
+ }
+ }
+ else
+ memcpy(m_buffer, res.pData, m_height * m_stride);
+ pImdContext->Unmap(pCopyTexture.Get(), 0);
+ }
+ else
+ CLog::LogF(LOGERROR, "MAP_READ failed.");
+ }
+
+ return m_buffer != nullptr;
+}
diff --git a/xbmc/rendering/dx/ScreenshotSurfaceWindows.h b/xbmc/rendering/dx/ScreenshotSurfaceWindows.h
new file mode 100644
index 0000000..e492e44
--- /dev/null
+++ b/xbmc/rendering/dx/ScreenshotSurfaceWindows.h
@@ -0,0 +1,22 @@
+/*
+ * 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 "utils/IScreenshotSurface.h"
+
+#include <memory>
+
+class CScreenshotSurfaceWindows : public IScreenshotSurface
+{
+public:
+ static void Register();
+ static std::unique_ptr<IScreenshotSurface> CreateSurface();
+
+ bool Capture() override;
+};
diff --git a/xbmc/rendering/gl/CMakeLists.txt b/xbmc/rendering/gl/CMakeLists.txt
new file mode 100644
index 0000000..6aadafa
--- /dev/null
+++ b/xbmc/rendering/gl/CMakeLists.txt
@@ -0,0 +1,12 @@
+if(OPENGL_FOUND)
+ set(SOURCES RenderSystemGL.cpp
+ ScreenshotSurfaceGL.cpp
+ GLShader.cpp)
+
+ set(HEADERS RenderSystemGL.h
+ ScreenshotSurfaceGL.h
+ GLShader.h)
+
+ core_add_library(rendering_gl)
+endif()
+
diff --git a/xbmc/rendering/gl/GLShader.cpp b/xbmc/rendering/gl/GLShader.cpp
new file mode 100644
index 0000000..e91af97
--- /dev/null
+++ b/xbmc/rendering/gl/GLShader.cpp
@@ -0,0 +1,162 @@
+/*
+ * 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 "GLShader.h"
+
+#include "ServiceBroker.h"
+#include "rendering/MatrixGL.h"
+#include "rendering/RenderSystem.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+using namespace Shaders;
+
+CGLShader::CGLShader(const char* shader, const std::string& prefix)
+{
+ m_proj = nullptr;
+ m_model = nullptr;
+ m_clipPossible = false;
+
+ VertexShader()->LoadSource("gl_shader_vert.glsl");
+ PixelShader()->LoadSource(shader, prefix);
+}
+
+CGLShader::CGLShader(const char* vshader, const char* fshader, const std::string& prefix)
+{
+ m_proj = nullptr;
+ m_model = nullptr;
+ m_clipPossible = false;
+
+ VertexShader()->LoadSource(vshader, prefix);
+ PixelShader()->LoadSource(fshader, prefix);
+}
+
+void CGLShader::OnCompiledAndLinked()
+{
+ // This is called after CompileAndLink()
+
+ // Variables passed directly to the Fragment shader
+ m_hTex0 = glGetUniformLocation(ProgramHandle(), "m_samp0");
+ m_hTex1 = glGetUniformLocation(ProgramHandle(), "m_samp1");
+ m_hUniCol = glGetUniformLocation(ProgramHandle(), "m_unicol");
+
+ // Variables passed directly to the Vertex shader
+ m_hProj = glGetUniformLocation(ProgramHandle(), "m_proj");
+ m_hModel = glGetUniformLocation(ProgramHandle(), "m_model");
+
+ // Vertex attributes
+ m_hPos = glGetAttribLocation(ProgramHandle(), "m_attrpos");
+ m_hCol = glGetAttribLocation(ProgramHandle(), "m_attrcol");
+ m_hCord0 = glGetAttribLocation(ProgramHandle(), "m_attrcord0");
+ m_hCord1 = glGetAttribLocation(ProgramHandle(), "m_attrcord1");
+
+ // It's okay to do this only one time. Textures units never change.
+ glUseProgram(ProgramHandle());
+ glUniform1i(m_hTex0, 0);
+ glUniform1i(m_hTex1, 1);
+ glUniform4f(m_hUniCol, 1.0, 1.0, 1.0, 1.0);
+
+ glUseProgram(0);
+}
+
+bool CGLShader::OnEnabled()
+{
+ // This is called after glUseProgram()
+
+ const GLfloat *projMatrix = glMatrixProject.Get();
+ const GLfloat *modelMatrix = glMatrixModview.Get();
+ glUniformMatrix4fv(m_hProj, 1, GL_FALSE, projMatrix);
+ glUniformMatrix4fv(m_hModel, 1, GL_FALSE, modelMatrix);
+
+ const TransformMatrix &guiMatrix = CServiceBroker::GetWinSystem()->GetGfxContext().GetGUIMatrix();
+ CRect viewPort; // absolute positions of corners
+ CServiceBroker::GetRenderSystem()->GetViewPort(viewPort);
+
+ /* glScissor operates in window coordinates. In order that we can use it to
+ * perform clipping, we must ensure that there is an independent linear
+ * transformation from the coordinate system used by CGraphicContext::ClipRect
+ * to window coordinates, separately for X and Y (in other words, no
+ * rotation or shear is introduced at any stage). To do, this, we need to
+ * check that zeros are present in the following locations:
+ *
+ * GUI matrix:
+ * / * 0 * * \
+ * | 0 * * * |
+ * \ 0 0 * * /
+ * ^ TransformMatrix::TransformX/Y/ZCoord are only ever called with
+ * input z = 0, so this column doesn't matter
+ * Model-view matrix:
+ * / * 0 0 * \
+ * | 0 * 0 * |
+ * | 0 0 * * |
+ * \ * * * * / <- eye w has no influence on window x/y (last column below
+ * is either 0 or ignored)
+ * Projection matrix:
+ * / * 0 0 0 \
+ * | 0 * 0 0 |
+ * | * * * * | <- normalised device coordinate z has no influence on window x/y
+ * \ 0 0 * 0 /
+ *
+ * Some of these zeros are not strictly required to ensure this, but they tend
+ * to be zeroed in the common case, so by checking for zeros here, we simplify
+ * the calculation of the window x/y coordinates further down the line.
+ *
+ * (Minor detail: we don't quite deal in window coordinates as defined by
+ * OpenGL, because CRenderSystemGLES::SetScissors flips the Y axis. But all
+ * that's needed to handle that is an effective negation at the stage where
+ * Y is in normalised device coordinates.)
+ */
+ m_clipPossible = guiMatrix.m[0][1] == 0 &&
+ guiMatrix.m[1][0] == 0 &&
+ guiMatrix.m[2][0] == 0 &&
+ guiMatrix.m[2][1] == 0 &&
+ modelMatrix[0+1*4] == 0 &&
+ modelMatrix[0+2*4] == 0 &&
+ modelMatrix[1+0*4] == 0 &&
+ modelMatrix[1+2*4] == 0 &&
+ modelMatrix[2+0*4] == 0 &&
+ modelMatrix[2+1*4] == 0 &&
+ projMatrix[0+1*4] == 0 &&
+ projMatrix[0+2*4] == 0 &&
+ projMatrix[0+3*4] == 0 &&
+ projMatrix[1+0*4] == 0 &&
+ projMatrix[1+2*4] == 0 &&
+ projMatrix[1+3*4] == 0 &&
+ projMatrix[3+0*4] == 0 &&
+ projMatrix[3+1*4] == 0 &&
+ projMatrix[3+3*4] == 0;
+
+ m_clipXFactor = 0.0;
+ m_clipXOffset = 0.0;
+ m_clipYFactor = 0.0;
+ m_clipYOffset = 0.0;
+
+ if (m_clipPossible)
+ {
+ m_clipXFactor = guiMatrix.m[0][0] * modelMatrix[0+0*4] * projMatrix[0+0*4];
+ m_clipXOffset = (guiMatrix.m[0][3] * modelMatrix[0+0*4] + modelMatrix[0+3*4]) * projMatrix[0+0*4];
+ m_clipYFactor = guiMatrix.m[1][1] * modelMatrix[1+1*4] * projMatrix[1+1*4];
+ m_clipYOffset = (guiMatrix.m[1][3] * modelMatrix[1+1*4] + modelMatrix[1+3*4]) * projMatrix[1+1*4];
+ float clipW = (guiMatrix.m[2][3] * modelMatrix[2+2*4] + modelMatrix[2+3*4]) * projMatrix[3+2*4];
+ float xMult = (viewPort.x2 - viewPort.x1) / (2 * clipW);
+ float yMult = (viewPort.y1 - viewPort.y2) / (2 * clipW); // correct for inverted window coordinate scheme
+ m_clipXFactor = m_clipXFactor * xMult;
+ m_clipXOffset = m_clipXOffset * xMult + (viewPort.x2 + viewPort.x1) / 2;
+ m_clipYFactor = m_clipYFactor * yMult;
+ m_clipYOffset = m_clipYOffset * yMult + (viewPort.y2 + viewPort.y1) / 2;
+ }
+
+ return true;
+}
+
+void CGLShader::Free()
+{
+ // Do Cleanup here
+ CGLSLShaderProgram::Free();
+}
diff --git a/xbmc/rendering/gl/GLShader.h b/xbmc/rendering/gl/GLShader.h
new file mode 100644
index 0000000..d5b82f3
--- /dev/null
+++ b/xbmc/rendering/gl/GLShader.h
@@ -0,0 +1,55 @@
+/*
+ * 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 "guilib/Shader.h"
+
+#include <string>
+
+class CGLShader : public Shaders::CGLSLShaderProgram
+{
+public:
+ CGLShader(const char* shader, const std::string& prefix);
+ CGLShader(const char* vshader, const char* fshader, const std::string& prefix);
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+ void Free();
+
+ GLint GetPosLoc() {return m_hPos;}
+ GLint GetColLoc() {return m_hCol;}
+ GLint GetCord0Loc() {return m_hCord0;}
+ GLint GetCord1Loc() {return m_hCord1;}
+ GLint GetUniColLoc() {return m_hUniCol;}
+ GLint GetModelLoc() {return m_hModel; }
+ bool HardwareClipIsPossible() {return m_clipPossible; }
+ GLfloat GetClipXFactor() {return m_clipXFactor; }
+ GLfloat GetClipXOffset() {return m_clipXOffset; }
+ GLfloat GetClipYFactor() {return m_clipYFactor; }
+ GLfloat GetClipYOffset() {return m_clipYOffset; }
+
+protected:
+ GLint m_hTex0 = 0;
+ GLint m_hTex1 = 0;
+ GLint m_hUniCol = 0;
+ GLint m_hProj = 0;
+ GLint m_hModel = 0;
+ GLint m_hPos = 0;
+ GLint m_hCol = 0;
+ GLint m_hCord0 = 0;
+ GLint m_hCord1 = 0;
+
+ const GLfloat *m_proj = nullptr;
+ const GLfloat *m_model = nullptr;
+
+ bool m_clipPossible = false;
+ GLfloat m_clipXFactor;
+ GLfloat m_clipXOffset;
+ GLfloat m_clipYFactor;
+ GLfloat m_clipYOffset;
+};
diff --git a/xbmc/rendering/gl/RenderSystemGL.cpp b/xbmc/rendering/gl/RenderSystemGL.cpp
new file mode 100644
index 0000000..46a354c
--- /dev/null
+++ b/xbmc/rendering/gl/RenderSystemGL.cpp
@@ -0,0 +1,795 @@
+/*
+ * 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 "RenderSystemGL.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "guilib/GUITextureGL.h"
+#include "rendering/MatrixGL.h"
+#include "utils/FileUtils.h"
+#include "utils/GLUtils.h"
+#include "utils/MathUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+
+using namespace std::chrono_literals;
+
+CRenderSystemGL::CRenderSystemGL() : CRenderSystemBase()
+{
+}
+
+CRenderSystemGL::~CRenderSystemGL() = default;
+
+bool CRenderSystemGL::InitRenderSystem()
+{
+ m_bVSync = false;
+ m_bVsyncInit = false;
+ m_maxTextureSize = 2048;
+
+ // Get the GL version number
+ m_RenderVersionMajor = 0;
+ m_RenderVersionMinor = 0;
+ const char* ver = (const char*)glGetString(GL_VERSION);
+ if (ver != 0)
+ {
+ sscanf(ver, "%d.%d", &m_RenderVersionMajor, &m_RenderVersionMinor);
+ m_RenderVersion = ver;
+ }
+
+ CLog::Log(LOGINFO, "CRenderSystemGL::{} - Version: {}, Major: {}, Minor: {}", __FUNCTION__, ver,
+ m_RenderVersionMajor, m_RenderVersionMinor);
+
+ m_RenderExtensions = " ";
+ if (m_RenderVersionMajor > 3 ||
+ (m_RenderVersionMajor == 3 && m_RenderVersionMinor >= 2))
+ {
+ GLint n = 0;
+ glGetIntegerv(GL_NUM_EXTENSIONS, &n);
+ if (n > 0)
+ {
+ GLint i;
+ for (i = 0; i < n; i++)
+ {
+ m_RenderExtensions += (const char*) glGetStringi(GL_EXTENSIONS, i);
+ m_RenderExtensions += " ";
+ }
+ }
+ }
+ else
+ {
+ auto extensions = (const char*) glGetString(GL_EXTENSIONS);
+ if (extensions)
+ {
+ m_RenderExtensions += extensions;
+ }
+ }
+ m_RenderExtensions += " ";
+
+ ver = (const char*)glGetString(GL_SHADING_LANGUAGE_VERSION);
+ if (ver)
+ {
+ sscanf(ver, "%d.%d", &m_glslMajor, &m_glslMinor);
+ }
+ else
+ {
+ m_glslMajor = 1;
+ m_glslMinor = 0;
+ }
+
+ LogGraphicsInfo();
+
+ // Get our driver vendor and renderer
+ const char* tmpVendor = (const char*) glGetString(GL_VENDOR);
+ m_RenderVendor.clear();
+ if (tmpVendor != NULL)
+ m_RenderVendor = tmpVendor;
+
+ const char* tmpRenderer = (const char*) glGetString(GL_RENDERER);
+ m_RenderRenderer.clear();
+ if (tmpRenderer != NULL)
+ m_RenderRenderer = tmpRenderer;
+
+ m_bRenderCreated = true;
+
+ if (m_RenderVersionMajor > 3 ||
+ (m_RenderVersionMajor == 3 && m_RenderVersionMinor >= 2))
+ {
+ glGenVertexArrays(1, &m_vertexArray);
+ glBindVertexArray(m_vertexArray);
+ }
+
+ InitialiseShaders();
+
+ CGUITextureGL::Register();
+
+ return true;
+}
+
+bool CRenderSystemGL::ResetRenderSystem(int width, int height)
+{
+ if (!m_bRenderCreated)
+ return false;
+
+ m_width = width;
+ m_height = height;
+
+ if (m_RenderVersionMajor > 3 ||
+ (m_RenderVersionMajor == 3 && m_RenderVersionMinor >= 2))
+ {
+ glBindVertexArray(0);
+ glDeleteVertexArrays(1, &m_vertexArray);
+ glGenVertexArrays(1, &m_vertexArray);
+ glBindVertexArray(m_vertexArray);
+ }
+
+ ReleaseShaders();
+ InitialiseShaders();
+
+ glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
+
+ CalculateMaxTexturesize();
+
+ CRect rect( 0, 0, width, height );
+ SetViewPort( rect );
+
+ glEnable(GL_SCISSOR_TEST);
+
+ glMatrixProject.Clear();
+ glMatrixProject->LoadIdentity();
+ glMatrixProject->Ortho(0.0f, width-1, height-1, 0.0f, -1.0f, 1.0f);
+ glMatrixProject.Load();
+
+ glMatrixModview.Clear();
+ glMatrixModview->LoadIdentity();
+ glMatrixModview.Load();
+
+ glMatrixTexture.Clear();
+ glMatrixTexture->LoadIdentity();
+ glMatrixTexture.Load();
+
+ if (IsExtSupported("GL_ARB_multitexture"))
+ {
+ //clear error flags
+ ResetGLErrors();
+
+ GLint maxtex;
+ glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxtex);
+
+ //some sanity checks
+ GLenum error = glGetError();
+ if (error != GL_NO_ERROR)
+ {
+ CLog::Log(LOGERROR, "ResetRenderSystem() GL_MAX_TEXTURE_IMAGE_UNITS returned error {}",
+ (int)error);
+ maxtex = 3;
+ }
+ else if (maxtex < 1 || maxtex > 32)
+ {
+ CLog::Log(LOGERROR,
+ "ResetRenderSystem() GL_MAX_TEXTURE_IMAGE_UNITS returned invalid value {}",
+ (int)maxtex);
+ maxtex = 3;
+ }
+
+ //reset texture matrix for all textures
+ for (GLint i = 0; i < maxtex; i++)
+ {
+ glActiveTexture(GL_TEXTURE0 + i);
+ glMatrixTexture.Load();
+ }
+ glActiveTexture(GL_TEXTURE0);
+ }
+
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+ glEnable(GL_BLEND); // Turn Blending On
+ glDisable(GL_DEPTH_TEST);
+
+ return true;
+}
+
+bool CRenderSystemGL::DestroyRenderSystem()
+{
+ if (m_vertexArray != GL_NONE)
+ {
+ glDeleteVertexArrays(1, &m_vertexArray);
+ }
+
+ ReleaseShaders();
+ m_bRenderCreated = false;
+
+ return true;
+}
+
+bool CRenderSystemGL::BeginRender()
+{
+ if (!m_bRenderCreated)
+ return false;
+
+ bool useLimited = CServiceBroker::GetWinSystem()->UseLimitedColor();
+
+ if (m_limitedColorRange != useLimited)
+ {
+ ReleaseShaders();
+ InitialiseShaders();
+ }
+
+ m_limitedColorRange = useLimited;
+ return true;
+}
+
+bool CRenderSystemGL::EndRender()
+{
+ if (!m_bRenderCreated)
+ return false;
+
+ return true;
+}
+
+bool CRenderSystemGL::ClearBuffers(UTILS::COLOR::Color color)
+{
+ if (!m_bRenderCreated)
+ return false;
+
+ /* clear is not affected by stipple pattern, so we can only clear on first frame */
+ if(m_stereoMode == RENDER_STEREO_MODE_INTERLACED && m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ return true;
+
+ float r = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color) / 255.0f;
+ float g = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color) / 255.0f;
+ float b = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color) / 255.0f;
+ float a = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color) / 255.0f;
+
+ glClearColor(r, g, b, a);
+
+ GLbitfield flags = GL_COLOR_BUFFER_BIT;
+ glClear(flags);
+
+ return true;
+}
+
+bool CRenderSystemGL::IsExtSupported(const char* extension) const
+{
+ if (m_RenderVersionMajor > 3 ||
+ (m_RenderVersionMajor == 3 && m_RenderVersionMinor >= 2))
+ {
+ if (strcmp( extension, "GL_EXT_framebuffer_object") == 0)
+ {
+ return true;
+ }
+ if (strcmp( extension, "GL_ARB_texture_non_power_of_two") == 0)
+ {
+ return true;
+ }
+ }
+
+ std::string name;
+ name = " ";
+ name += extension;
+ name += " ";
+
+ return m_RenderExtensions.find(name) != std::string::npos;
+}
+
+bool CRenderSystemGL::SupportsNPOT(bool dxt) const
+{
+ return true;
+}
+
+void CRenderSystemGL::PresentRender(bool rendered, bool videoLayer)
+{
+ SetVSync(true);
+
+ if (!m_bRenderCreated)
+ return;
+
+ PresentRenderImpl(rendered);
+
+ if (!rendered)
+ KODI::TIME::Sleep(40ms);
+}
+
+void CRenderSystemGL::SetVSync(bool enable)
+{
+ if (m_bVSync == enable && m_bVsyncInit == true)
+ return;
+
+ if (!m_bRenderCreated)
+ return;
+
+ if (enable)
+ CLog::Log(LOGINFO, "GL: Enabling VSYNC");
+ else
+ CLog::Log(LOGINFO, "GL: Disabling VSYNC");
+
+ m_bVSync = enable;
+ m_bVsyncInit = true;
+
+ SetVSyncImpl(enable);
+}
+
+void CRenderSystemGL::CaptureStateBlock()
+{
+ if (!m_bRenderCreated)
+ return;
+
+ glMatrixProject.Push();
+ glMatrixModview.Push();
+ glMatrixTexture.Push();
+
+ glDisable(GL_SCISSOR_TEST); // fixes FBO corruption on Macs
+ glActiveTexture(GL_TEXTURE0);
+}
+
+void CRenderSystemGL::ApplyStateBlock()
+{
+ if (!m_bRenderCreated)
+ return;
+
+ glBindVertexArray(m_vertexArray);
+
+ glViewport(m_viewPort[0], m_viewPort[1], m_viewPort[2], m_viewPort[3]);
+
+ glMatrixProject.PopLoad();
+ glMatrixModview.PopLoad();
+ glMatrixTexture.PopLoad();
+
+ glActiveTexture(GL_TEXTURE0);
+ glEnable(GL_BLEND);
+ glEnable(GL_SCISSOR_TEST);
+}
+
+void CRenderSystemGL::SetCameraPosition(const CPoint &camera, int screenWidth, int screenHeight, float stereoFactor)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ CPoint offset = camera - CPoint(screenWidth*0.5f, screenHeight*0.5f);
+
+
+ float w = (float)m_viewPort[2]*0.5f;
+ float h = (float)m_viewPort[3]*0.5f;
+
+ glMatrixModview->LoadIdentity();
+ glMatrixModview->Translatef(-(w + offset.x - stereoFactor), +(h + offset.y), 0);
+ glMatrixModview->LookAt(0.0f, 0.0f, -2.0f * h, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f);
+ glMatrixModview.Load();
+
+ glMatrixProject->LoadIdentity();
+ glMatrixProject->Frustum( (-w - offset.x)*0.5f, (w - offset.x)*0.5f, (-h + offset.y)*0.5f, (h + offset.y)*0.5f, h, 100*h);
+ glMatrixProject.Load();
+}
+
+void CRenderSystemGL::Project(float &x, float &y, float &z)
+{
+ GLfloat coordX, coordY, coordZ;
+ if (CMatrixGL::Project(x, y, z, glMatrixModview.Get(), glMatrixProject.Get(), m_viewPort, &coordX, &coordY, &coordZ))
+ {
+ x = coordX;
+ y = (float)(m_viewPort[1] + m_viewPort[3] - coordY);
+ z = 0;
+ }
+}
+
+void CRenderSystemGL::CalculateMaxTexturesize()
+{
+ GLint width = 256;
+
+ // reset any previous GL errors
+ ResetGLErrors();
+
+ // max out at 2^(8+8)
+ for (int i = 0 ; i<8 ; i++)
+ {
+ glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, width, width, 0, GL_BGRA,
+ GL_UNSIGNED_BYTE, NULL);
+ glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH,
+ &width);
+
+ // GMA950 on OS X sets error instead
+ if (width == 0 || (glGetError() != GL_NO_ERROR) )
+ break;
+
+ m_maxTextureSize = width;
+ width *= 2;
+ if (width > 65536) // have an upper limit in case driver acts stupid
+ {
+ CLog::Log(LOGERROR, "GL: Could not determine maximum texture width, falling back to 2048");
+ m_maxTextureSize = 2048;
+ break;
+ }
+ }
+
+ CLog::Log(LOGINFO, "GL: Maximum texture width: {}", m_maxTextureSize);
+}
+
+void CRenderSystemGL::GetViewPort(CRect& viewPort)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ viewPort.x1 = m_viewPort[0];
+ viewPort.y1 = m_height - m_viewPort[1] - m_viewPort[3];
+ viewPort.x2 = m_viewPort[0] + m_viewPort[2];
+ viewPort.y2 = viewPort.y1 + m_viewPort[3];
+}
+
+void CRenderSystemGL::SetViewPort(const CRect& viewPort)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ glScissor((GLint) viewPort.x1, (GLint) (m_height - viewPort.y1 - viewPort.Height()), (GLsizei) viewPort.Width(), (GLsizei) viewPort.Height());
+ glViewport((GLint) viewPort.x1, (GLint) (m_height - viewPort.y1 - viewPort.Height()), (GLsizei) viewPort.Width(), (GLsizei) viewPort.Height());
+ m_viewPort[0] = viewPort.x1;
+ m_viewPort[1] = m_height - viewPort.y1 - viewPort.Height();
+ m_viewPort[2] = viewPort.Width();
+ m_viewPort[3] = viewPort.Height();
+}
+
+bool CRenderSystemGL::ScissorsCanEffectClipping()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->HardwareClipIsPossible();
+
+ return false;
+}
+
+CRect CRenderSystemGL::ClipRectToScissorRect(const CRect &rect)
+{
+ if (!m_pShader[m_method])
+ return CRect();
+ float xFactor = m_pShader[m_method]->GetClipXFactor();
+ float xOffset = m_pShader[m_method]->GetClipXOffset();
+ float yFactor = m_pShader[m_method]->GetClipYFactor();
+ float yOffset = m_pShader[m_method]->GetClipYOffset();
+ return CRect(rect.x1 * xFactor + xOffset,
+ rect.y1 * yFactor + yOffset,
+ rect.x2 * xFactor + xOffset,
+ rect.y2 * yFactor + yOffset);
+}
+
+void CRenderSystemGL::SetScissors(const CRect &rect)
+{
+ if (!m_bRenderCreated)
+ return;
+ GLint x1 = MathUtils::round_int(static_cast<double>(rect.x1));
+ GLint y1 = MathUtils::round_int(static_cast<double>(rect.y1));
+ GLint x2 = MathUtils::round_int(static_cast<double>(rect.x2));
+ GLint y2 = MathUtils::round_int(static_cast<double>(rect.y2));
+ glScissor(x1, m_height - y2, x2-x1, y2-y1);
+}
+
+void CRenderSystemGL::ResetScissors()
+{
+ SetScissors(CRect(0, 0, (float)m_width, (float)m_height));
+}
+
+void CRenderSystemGL::GetGLSLVersion(int& major, int& minor)
+{
+ major = m_glslMajor;
+ minor = m_glslMinor;
+}
+
+void CRenderSystemGL::ResetGLErrors()
+{
+ int count = 0;
+ while (glGetError() != GL_NO_ERROR)
+ {
+ count++;
+ if (count >= 100)
+ {
+ CLog::Log(
+ LOGWARNING,
+ "CRenderSystemGL::ResetGLErrors glGetError didn't return GL_NO_ERROR after {} iterations",
+ count);
+ break;
+ }
+ }
+}
+static const GLubyte stipple_3d[] = {
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00,
+};
+
+void CRenderSystemGL::SetStereoMode(RENDER_STEREO_MODE mode, RENDER_STEREO_VIEW view)
+{
+ CRenderSystemBase::SetStereoMode(mode, view);
+
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+ glDrawBuffer(GL_BACK);
+
+ if(m_stereoMode == RENDER_STEREO_MODE_ANAGLYPH_RED_CYAN)
+ {
+ if(m_stereoView == RENDER_STEREO_VIEW_LEFT)
+ glColorMask(GL_TRUE, GL_FALSE, GL_FALSE, GL_TRUE);
+ else if(m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ glColorMask(GL_FALSE, GL_TRUE, GL_TRUE, GL_TRUE);
+ }
+ if(m_stereoMode == RENDER_STEREO_MODE_ANAGLYPH_GREEN_MAGENTA)
+ {
+ if(m_stereoView == RENDER_STEREO_VIEW_LEFT)
+ glColorMask(GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
+ else if(m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ glColorMask(GL_TRUE, GL_FALSE, GL_TRUE, GL_TRUE);
+ }
+ if(m_stereoMode == RENDER_STEREO_MODE_ANAGLYPH_YELLOW_BLUE)
+ {
+ if(m_stereoView == RENDER_STEREO_VIEW_LEFT)
+ glColorMask(GL_TRUE, GL_TRUE, GL_FALSE, GL_TRUE);
+ else if(m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_TRUE);
+ }
+
+ if(m_stereoMode == RENDER_STEREO_MODE_INTERLACED)
+ {
+ glEnable(GL_POLYGON_STIPPLE);
+ if(m_stereoView == RENDER_STEREO_VIEW_LEFT)
+ glPolygonStipple(stipple_3d);
+ else if(m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ glPolygonStipple(stipple_3d+4);
+ }
+
+ if(m_stereoMode == RENDER_STEREO_MODE_HARDWAREBASED)
+ {
+ if(m_stereoView == RENDER_STEREO_VIEW_LEFT)
+ glDrawBuffer(GL_BACK_LEFT);
+ else if(m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ glDrawBuffer(GL_BACK_RIGHT);
+ }
+
+}
+
+bool CRenderSystemGL::SupportsStereo(RENDER_STEREO_MODE mode) const
+{
+ switch(mode)
+ {
+ case RENDER_STEREO_MODE_ANAGLYPH_RED_CYAN:
+ case RENDER_STEREO_MODE_ANAGLYPH_GREEN_MAGENTA:
+ case RENDER_STEREO_MODE_ANAGLYPH_YELLOW_BLUE:
+ case RENDER_STEREO_MODE_INTERLACED:
+ return true;
+ case RENDER_STEREO_MODE_HARDWAREBASED: {
+ //This is called by setting init, at which point GL is not inited
+ //luckily if GL doesn't support this, it will just behave as if
+ //it was not in effect.
+ //GLboolean stereo = GL_FALSE;
+ //glGetBooleanv(GL_STEREO, &stereo);
+ //return stereo == GL_TRUE ? true : false;
+ return true;
+ }
+ default:
+ return CRenderSystemBase::SupportsStereo(mode);
+ }
+}
+
+// -----------------------------------------------------------------------------
+// shaders
+// -----------------------------------------------------------------------------
+void CRenderSystemGL::InitialiseShaders()
+{
+ std::string defines;
+ m_limitedColorRange = CServiceBroker::GetWinSystem()->UseLimitedColor();
+ if (m_limitedColorRange)
+ {
+ defines += "#define KODI_LIMITED_RANGE 1\n";
+ }
+
+ m_pShader[ShaderMethodGL::SM_DEFAULT] = std::make_unique<CGLShader>(
+ "gl_shader_vert_default.glsl", "gl_shader_frag_default.glsl", defines);
+ if (!m_pShader[ShaderMethodGL::SM_DEFAULT]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGL::SM_DEFAULT]->Free();
+ m_pShader[ShaderMethodGL::SM_DEFAULT].reset();
+ CLog::Log(LOGERROR, "GUI Shader gl_shader_frag_default.glsl - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGL::SM_TEXTURE] =
+ std::make_unique<CGLShader>("gl_shader_frag_texture.glsl", defines);
+ if (!m_pShader[ShaderMethodGL::SM_TEXTURE]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGL::SM_TEXTURE]->Free();
+ m_pShader[ShaderMethodGL::SM_TEXTURE].reset();
+ CLog::Log(LOGERROR, "GUI Shader gl_shader_frag_texture.glsl - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGL::SM_TEXTURE_LIM] =
+ std::make_unique<CGLShader>("gl_shader_frag_texture_lim.glsl", defines);
+ if (!m_pShader[ShaderMethodGL::SM_TEXTURE_LIM]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGL::SM_TEXTURE_LIM]->Free();
+ m_pShader[ShaderMethodGL::SM_TEXTURE_LIM].reset();
+ CLog::Log(LOGERROR, "GUI Shader gl_shader_frag_texture_lim.glsl - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGL::SM_MULTI] =
+ std::make_unique<CGLShader>("gl_shader_frag_multi.glsl", defines);
+ if (!m_pShader[ShaderMethodGL::SM_MULTI]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGL::SM_MULTI]->Free();
+ m_pShader[ShaderMethodGL::SM_MULTI].reset();
+ CLog::Log(LOGERROR, "GUI Shader gl_shader_frag_multi.glsl - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGL::SM_FONTS] =
+ std::make_unique<CGLShader>("gl_shader_frag_fonts.glsl", defines);
+ if (!m_pShader[ShaderMethodGL::SM_FONTS]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGL::SM_FONTS]->Free();
+ m_pShader[ShaderMethodGL::SM_FONTS].reset();
+ CLog::Log(LOGERROR, "GUI Shader gl_shader_frag_fonts.glsl - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGL::SM_TEXTURE_NOBLEND] =
+ std::make_unique<CGLShader>("gl_shader_frag_texture_noblend.glsl", defines);
+ if (!m_pShader[ShaderMethodGL::SM_TEXTURE_NOBLEND]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGL::SM_TEXTURE_NOBLEND]->Free();
+ m_pShader[ShaderMethodGL::SM_TEXTURE_NOBLEND].reset();
+ CLog::Log(LOGERROR, "GUI Shader gl_shader_frag_texture_noblend.glsl - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGL::SM_MULTI_BLENDCOLOR] =
+ std::make_unique<CGLShader>("gl_shader_frag_multi_blendcolor.glsl", defines);
+ if (!m_pShader[ShaderMethodGL::SM_MULTI_BLENDCOLOR]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGL::SM_MULTI_BLENDCOLOR]->Free();
+ m_pShader[ShaderMethodGL::SM_MULTI_BLENDCOLOR].reset();
+ CLog::Log(LOGERROR, "GUI Shader gl_shader_frag_multi_blendcolor.glsl - compile and link failed");
+ }
+}
+
+void CRenderSystemGL::ReleaseShaders()
+{
+ if (m_pShader[ShaderMethodGL::SM_DEFAULT])
+ m_pShader[ShaderMethodGL::SM_DEFAULT]->Free();
+ m_pShader[ShaderMethodGL::SM_DEFAULT].reset();
+
+ if (m_pShader[ShaderMethodGL::SM_TEXTURE])
+ m_pShader[ShaderMethodGL::SM_TEXTURE]->Free();
+ m_pShader[ShaderMethodGL::SM_TEXTURE].reset();
+
+ if (m_pShader[ShaderMethodGL::SM_TEXTURE_LIM])
+ m_pShader[ShaderMethodGL::SM_TEXTURE_LIM]->Free();
+ m_pShader[ShaderMethodGL::SM_TEXTURE_LIM].reset();
+
+ if (m_pShader[ShaderMethodGL::SM_MULTI])
+ m_pShader[ShaderMethodGL::SM_MULTI]->Free();
+ m_pShader[ShaderMethodGL::SM_MULTI].reset();
+
+ if (m_pShader[ShaderMethodGL::SM_FONTS])
+ m_pShader[ShaderMethodGL::SM_FONTS]->Free();
+ m_pShader[ShaderMethodGL::SM_FONTS].reset();
+
+ if (m_pShader[ShaderMethodGL::SM_TEXTURE_NOBLEND])
+ m_pShader[ShaderMethodGL::SM_TEXTURE_NOBLEND]->Free();
+ m_pShader[ShaderMethodGL::SM_TEXTURE_NOBLEND].reset();
+
+ if (m_pShader[ShaderMethodGL::SM_MULTI_BLENDCOLOR])
+ m_pShader[ShaderMethodGL::SM_MULTI_BLENDCOLOR]->Free();
+ m_pShader[ShaderMethodGL::SM_MULTI_BLENDCOLOR].reset();
+}
+
+void CRenderSystemGL::EnableShader(ShaderMethodGL method)
+{
+ m_method = method;
+ if (m_pShader[m_method])
+ {
+ m_pShader[m_method]->Enable();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Invalid GUI Shader selected {}", method);
+ }
+}
+
+void CRenderSystemGL::DisableShader()
+{
+ if (m_pShader[m_method])
+ {
+ m_pShader[m_method]->Disable();
+ }
+ m_method = ShaderMethodGL::SM_DEFAULT;
+}
+
+GLint CRenderSystemGL::ShaderGetPos()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetPosLoc();
+
+ return -1;
+}
+
+GLint CRenderSystemGL::ShaderGetCol()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetColLoc();
+
+ return -1;
+}
+
+GLint CRenderSystemGL::ShaderGetCoord0()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetCord0Loc();
+
+ return -1;
+}
+
+GLint CRenderSystemGL::ShaderGetCoord1()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetCord1Loc();
+
+ return -1;
+}
+
+GLint CRenderSystemGL::ShaderGetUniCol()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetUniColLoc();
+
+ return -1;
+}
+
+GLint CRenderSystemGL::ShaderGetModel()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetModelLoc();
+
+ return -1;
+}
+
+std::string CRenderSystemGL::GetShaderPath(const std::string &filename)
+{
+ std::string path = "GL/1.2/";
+
+ if (m_glslMajor >= 4)
+ {
+ std::string file = "special://xbmc/system/shaders/GL/4.0/" + filename;
+ const CURL pathToUrl(file);
+ if (CFileUtils::Exists(pathToUrl.Get()))
+ return "GL/4.0/";
+ }
+ if (m_glslMajor >= 2 || (m_glslMajor == 1 && m_glslMinor >= 50))
+ path = "GL/1.5/";
+
+ return path;
+}
diff --git a/xbmc/rendering/gl/RenderSystemGL.h b/xbmc/rendering/gl/RenderSystemGL.h
new file mode 100644
index 0000000..191c97f
--- /dev/null
+++ b/xbmc/rendering/gl/RenderSystemGL.h
@@ -0,0 +1,139 @@
+/*
+ * 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 "GLShader.h"
+#include "rendering/RenderSystem.h"
+#include "utils/ColorUtils.h"
+#include "utils/Map.h"
+
+#include <map>
+#include <memory>
+
+#include <fmt/format.h>
+
+#include "system_gl.h"
+
+enum class ShaderMethodGL
+{
+ SM_DEFAULT = 0,
+ SM_TEXTURE,
+ SM_TEXTURE_LIM,
+ SM_MULTI,
+ SM_FONTS,
+ SM_TEXTURE_NOBLEND,
+ SM_MULTI_BLENDCOLOR,
+ SM_MAX
+};
+
+template<>
+struct fmt::formatter<ShaderMethodGL> : fmt::formatter<std::string_view>
+{
+ template<typename FormatContext>
+ constexpr auto format(const ShaderMethodGL& shaderMethod, FormatContext& ctx)
+ {
+ const auto it = ShaderMethodGLMap.find(shaderMethod);
+ if (it == ShaderMethodGLMap.cend())
+ throw std::range_error("no string mapping found for shader method");
+
+ return fmt::formatter<string_view>::format(it->second, ctx);
+ }
+
+private:
+ static constexpr auto ShaderMethodGLMap = make_map<ShaderMethodGL, std::string_view>({
+ {ShaderMethodGL::SM_DEFAULT, "default"},
+ {ShaderMethodGL::SM_TEXTURE, "texture"},
+ {ShaderMethodGL::SM_TEXTURE_LIM, "texture limited"},
+ {ShaderMethodGL::SM_MULTI, "multi"},
+ {ShaderMethodGL::SM_FONTS, "fonts"},
+ {ShaderMethodGL::SM_TEXTURE_NOBLEND, "texture no blending"},
+ {ShaderMethodGL::SM_MULTI_BLENDCOLOR, "multi blend colour"},
+ });
+
+ static_assert(static_cast<size_t>(ShaderMethodGL::SM_MAX) == ShaderMethodGLMap.size(),
+ "ShaderMethodGLMap doesn't match the size of ShaderMethodGL, did you forget to "
+ "add/remove a mapping?");
+};
+
+class CRenderSystemGL : public CRenderSystemBase
+{
+public:
+ CRenderSystemGL();
+ ~CRenderSystemGL() override;
+ bool InitRenderSystem() override;
+ bool DestroyRenderSystem() override;
+ bool ResetRenderSystem(int width, int height) override;
+
+ bool BeginRender() override;
+ bool EndRender() override;
+ void PresentRender(bool rendered, bool videoLayer) override;
+ bool ClearBuffers(UTILS::COLOR::Color color) override;
+ bool IsExtSupported(const char* extension) const override;
+
+ void SetVSync(bool vsync);
+ void ResetVSync() { m_bVsyncInit = false; }
+
+ void SetViewPort(const CRect& viewPort) override;
+ void GetViewPort(CRect& viewPort) override;
+
+ bool ScissorsCanEffectClipping() override;
+ CRect ClipRectToScissorRect(const CRect &rect) override;
+ void SetScissors(const CRect &rect) override;
+ void ResetScissors() override;
+
+ void CaptureStateBlock() override;
+ void ApplyStateBlock() override;
+
+ void SetCameraPosition(const CPoint &camera, int screenWidth, int screenHeight, float stereoFactor = 0.0f) override;
+
+ void SetStereoMode(RENDER_STEREO_MODE mode, RENDER_STEREO_VIEW view) override;
+ bool SupportsStereo(RENDER_STEREO_MODE mode) const override;
+ bool SupportsNPOT(bool dxt) const override;
+
+ void Project(float &x, float &y, float &z) override;
+
+ std::string GetShaderPath(const std::string &filename) override;
+
+ void GetGLVersion(int& major, int& minor);
+ void GetGLSLVersion(int& major, int& minor);
+
+ void ResetGLErrors();
+
+ // shaders
+ void EnableShader(ShaderMethodGL method);
+ void DisableShader();
+ GLint ShaderGetPos();
+ GLint ShaderGetCol();
+ GLint ShaderGetCoord0();
+ GLint ShaderGetCoord1();
+ GLint ShaderGetUniCol();
+ GLint ShaderGetModel();
+
+protected:
+ virtual void SetVSyncImpl(bool enable) = 0;
+ virtual void PresentRenderImpl(bool rendered) = 0;
+ void CalculateMaxTexturesize();
+ void InitialiseShaders();
+ void ReleaseShaders();
+
+ bool m_bVsyncInit = false;
+ int m_width;
+ int m_height;
+
+ std::string m_RenderExtensions;
+
+ int m_glslMajor = 0;
+ int m_glslMinor = 0;
+
+ GLint m_viewPort[4];
+
+ std::map<ShaderMethodGL, std::unique_ptr<CGLShader>> m_pShader;
+ ShaderMethodGL m_method = ShaderMethodGL::SM_DEFAULT;
+ GLuint m_vertexArray = GL_NONE;
+};
diff --git a/xbmc/rendering/gl/ScreenshotSurfaceGL.cpp b/xbmc/rendering/gl/ScreenshotSurfaceGL.cpp
new file mode 100644
index 0000000..022611b
--- /dev/null
+++ b/xbmc/rendering/gl/ScreenshotSurfaceGL.cpp
@@ -0,0 +1,65 @@
+/*
+ * 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 "ScreenshotSurfaceGL.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/Screenshot.h"
+#include "windowing/GraphicContext.h"
+
+#include <mutex>
+#include <vector>
+
+#include "system_gl.h"
+
+void CScreenshotSurfaceGL::Register()
+{
+ CScreenShot::Register(CScreenshotSurfaceGL::CreateSurface);
+}
+
+std::unique_ptr<IScreenshotSurface> CScreenshotSurfaceGL::CreateSurface()
+{
+ return std::unique_ptr<CScreenshotSurfaceGL>(new CScreenshotSurfaceGL());
+}
+
+bool CScreenshotSurfaceGL::Capture()
+{
+ CWinSystemBase* winsystem = CServiceBroker::GetWinSystem();
+ if (!winsystem)
+ return false;
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (!gui)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(winsystem->GetGfxContext());
+ gui->GetWindowManager().Render();
+
+ glReadBuffer(GL_BACK);
+
+ // get current viewport
+ GLint viewport[4];
+ glGetIntegerv(GL_VIEWPORT, viewport);
+
+ m_width = viewport[2] - viewport[0];
+ m_height = viewport[3] - viewport[1];
+ m_stride = m_width * 4;
+ std::vector<uint8_t> surface(m_stride * m_height);
+
+ // read pixels from the backbuffer
+ glReadPixels(viewport[0], viewport[1], viewport[2], viewport[3], GL_BGRA, GL_UNSIGNED_BYTE, static_cast<GLvoid*>(surface.data()));
+
+ // make a new buffer and copy the read image to it with the Y axis inverted
+ m_buffer = new unsigned char[m_stride * m_height];
+ for (int y = 0; y < m_height; y++)
+ memcpy(m_buffer + y * m_stride, surface.data() + (m_height - y - 1) * m_stride, m_stride);
+
+ return m_buffer != nullptr;
+}
diff --git a/xbmc/rendering/gl/ScreenshotSurfaceGL.h b/xbmc/rendering/gl/ScreenshotSurfaceGL.h
new file mode 100644
index 0000000..01f0590
--- /dev/null
+++ b/xbmc/rendering/gl/ScreenshotSurfaceGL.h
@@ -0,0 +1,22 @@
+/*
+ * 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 "utils/IScreenshotSurface.h"
+
+#include <memory>
+
+class CScreenshotSurfaceGL : public IScreenshotSurface
+{
+public:
+ static void Register();
+ static std::unique_ptr<IScreenshotSurface> CreateSurface();
+
+ bool Capture() override;
+};
diff --git a/xbmc/rendering/gles/CMakeLists.txt b/xbmc/rendering/gles/CMakeLists.txt
new file mode 100644
index 0000000..74cfe63
--- /dev/null
+++ b/xbmc/rendering/gles/CMakeLists.txt
@@ -0,0 +1,11 @@
+if(OPENGLES_FOUND)
+ set(SOURCES RenderSystemGLES.cpp
+ ScreenshotSurfaceGLES.cpp
+ GLESShader.cpp)
+
+ set(HEADERS RenderSystemGLES.h
+ ScreenshotSurfaceGLES.h
+ GLESShader.h)
+
+ core_add_library(rendering_gles)
+endif()
diff --git a/xbmc/rendering/gles/GLESShader.cpp b/xbmc/rendering/gles/GLESShader.cpp
new file mode 100644
index 0000000..3964f4b
--- /dev/null
+++ b/xbmc/rendering/gles/GLESShader.cpp
@@ -0,0 +1,178 @@
+/*
+ * 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 "GLESShader.h"
+
+#include "ServiceBroker.h"
+#include "rendering/MatrixGL.h"
+#include "rendering/RenderSystem.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+using namespace Shaders;
+
+CGLESShader::CGLESShader(const char* shader, const std::string& prefix)
+{
+ m_proj = nullptr;
+ m_model = nullptr;
+ m_clipPossible = false;
+
+ VertexShader()->LoadSource("gles_shader.vert");
+ PixelShader()->LoadSource(shader, prefix);
+}
+
+CGLESShader::CGLESShader(const char* vshader, const char* fshader, const std::string& prefix)
+{
+ m_proj = nullptr;
+ m_model = nullptr;
+ m_clipPossible = false;
+
+ VertexShader()->LoadSource(vshader, prefix);
+ PixelShader()->LoadSource(fshader, prefix);
+}
+
+void CGLESShader::OnCompiledAndLinked()
+{
+ // This is called after CompileAndLink()
+
+ // Variables passed directly to the Fragment shader
+ m_hTex0 = glGetUniformLocation(ProgramHandle(), "m_samp0");
+ m_hTex1 = glGetUniformLocation(ProgramHandle(), "m_samp1");
+ m_hUniCol = glGetUniformLocation(ProgramHandle(), "m_unicol");
+ m_hField = glGetUniformLocation(ProgramHandle(), "m_field");
+ m_hStep = glGetUniformLocation(ProgramHandle(), "m_step");
+ m_hContrast = glGetUniformLocation(ProgramHandle(), "m_contrast");
+ m_hBrightness = glGetUniformLocation(ProgramHandle(), "m_brightness");
+
+ // Variables passed directly to the Vertex shader
+ m_hProj = glGetUniformLocation(ProgramHandle(), "m_proj");
+ m_hModel = glGetUniformLocation(ProgramHandle(), "m_model");
+ m_hCoord0Matrix = glGetUniformLocation(ProgramHandle(), "m_coord0Matrix");
+
+ // Vertex attributes
+ m_hPos = glGetAttribLocation(ProgramHandle(), "m_attrpos");
+ m_hCol = glGetAttribLocation(ProgramHandle(), "m_attrcol");
+ m_hCord0 = glGetAttribLocation(ProgramHandle(), "m_attrcord0");
+ m_hCord1 = glGetAttribLocation(ProgramHandle(), "m_attrcord1");
+
+ // It's okay to do this only one time. Textures units never change.
+ glUseProgram( ProgramHandle() );
+ glUniform1i(m_hTex0, 0);
+ glUniform1i(m_hTex1, 1);
+ glUniform4f(m_hUniCol, 1.0, 1.0, 1.0, 1.0);
+
+ const float identity[16] = {
+ 1.0f, 0.0f, 0.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f, 0.0f,
+ 0.0f, 0.0f, 1.0f, 0.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f
+ };
+ glUniformMatrix4fv(m_hCoord0Matrix, 1, GL_FALSE, identity);
+
+ glUseProgram( 0 );
+}
+
+bool CGLESShader::OnEnabled()
+{
+ // This is called after glUseProgram()
+
+ const GLfloat *projMatrix = glMatrixProject.Get();
+ const GLfloat *modelMatrix = glMatrixModview.Get();
+ glUniformMatrix4fv(m_hProj, 1, GL_FALSE, projMatrix);
+ glUniformMatrix4fv(m_hModel, 1, GL_FALSE, modelMatrix);
+
+ const TransformMatrix &guiMatrix = CServiceBroker::GetWinSystem()->GetGfxContext().GetGUIMatrix();
+ CRect viewPort; // absolute positions of corners
+ CServiceBroker::GetRenderSystem()->GetViewPort(viewPort);
+
+ /* glScissor operates in window coordinates. In order that we can use it to
+ * perform clipping, we must ensure that there is an independent linear
+ * transformation from the coordinate system used by CGraphicContext::ClipRect
+ * to window coordinates, separately for X and Y (in other words, no
+ * rotation or shear is introduced at any stage). To do, this, we need to
+ * check that zeros are present in the following locations:
+ *
+ * GUI matrix:
+ * / * 0 * * \
+ * | 0 * * * |
+ * \ 0 0 * * /
+ * ^ TransformMatrix::TransformX/Y/ZCoord are only ever called with
+ * input z = 0, so this column doesn't matter
+ * Model-view matrix:
+ * / * 0 0 * \
+ * | 0 * 0 * |
+ * | 0 0 * * |
+ * \ * * * * / <- eye w has no influence on window x/y (last column below
+ * is either 0 or ignored)
+ * Projection matrix:
+ * / * 0 0 0 \
+ * | 0 * 0 0 |
+ * | * * * * | <- normalised device coordinate z has no influence on window x/y
+ * \ 0 0 * 0 /
+ *
+ * Some of these zeros are not strictly required to ensure this, but they tend
+ * to be zeroed in the common case, so by checking for zeros here, we simplify
+ * the calculation of the window x/y coordinates further down the line.
+ *
+ * (Minor detail: we don't quite deal in window coordinates as defined by
+ * OpenGL, because CRenderSystemGLES::SetScissors flips the Y axis. But all
+ * that's needed to handle that is an effective negation at the stage where
+ * Y is in normalised device coordinates.)
+ */
+ m_clipPossible = guiMatrix.m[0][1] == 0 &&
+ guiMatrix.m[1][0] == 0 &&
+ guiMatrix.m[2][0] == 0 &&
+ guiMatrix.m[2][1] == 0 &&
+ modelMatrix[0+1*4] == 0 &&
+ modelMatrix[0+2*4] == 0 &&
+ modelMatrix[1+0*4] == 0 &&
+ modelMatrix[1+2*4] == 0 &&
+ modelMatrix[2+0*4] == 0 &&
+ modelMatrix[2+1*4] == 0 &&
+ projMatrix[0+1*4] == 0 &&
+ projMatrix[0+2*4] == 0 &&
+ projMatrix[0+3*4] == 0 &&
+ projMatrix[1+0*4] == 0 &&
+ projMatrix[1+2*4] == 0 &&
+ projMatrix[1+3*4] == 0 &&
+ projMatrix[3+0*4] == 0 &&
+ projMatrix[3+1*4] == 0 &&
+ projMatrix[3+3*4] == 0;
+
+ m_clipXFactor = 0.0;
+ m_clipXOffset = 0.0;
+ m_clipYFactor = 0.0;
+ m_clipYOffset = 0.0;
+
+ if (m_clipPossible)
+ {
+ m_clipXFactor = guiMatrix.m[0][0] * modelMatrix[0+0*4] * projMatrix[0+0*4];
+ m_clipXOffset = (guiMatrix.m[0][3] * modelMatrix[0+0*4] + modelMatrix[0+3*4]) * projMatrix[0+0*4];
+ m_clipYFactor = guiMatrix.m[1][1] * modelMatrix[1+1*4] * projMatrix[1+1*4];
+ m_clipYOffset = (guiMatrix.m[1][3] * modelMatrix[1+1*4] + modelMatrix[1+3*4]) * projMatrix[1+1*4];
+ float clipW = (guiMatrix.m[2][3] * modelMatrix[2+2*4] + modelMatrix[2+3*4]) * projMatrix[3+2*4];
+ float xMult = (viewPort.x2 - viewPort.x1) / (2 * clipW);
+ float yMult = (viewPort.y1 - viewPort.y2) / (2 * clipW); // correct for inverted window coordinate scheme
+ m_clipXFactor = m_clipXFactor * xMult;
+ m_clipXOffset = m_clipXOffset * xMult + (viewPort.x2 + viewPort.x1) / 2;
+ m_clipYFactor = m_clipYFactor * yMult;
+ m_clipYOffset = m_clipYOffset * yMult + (viewPort.y2 + viewPort.y1) / 2;
+ }
+
+ glUniform1f(m_hBrightness, 0.0f);
+ glUniform1f(m_hContrast, 1.0f);
+
+ return true;
+}
+
+void CGLESShader::Free()
+{
+ // Do Cleanup here
+ CGLSLShaderProgram::Free();
+}
+
diff --git a/xbmc/rendering/gles/GLESShader.h b/xbmc/rendering/gles/GLESShader.h
new file mode 100644
index 0000000..8ce31e5
--- /dev/null
+++ b/xbmc/rendering/gles/GLESShader.h
@@ -0,0 +1,65 @@
+/*
+ * 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 "guilib/Shader.h"
+
+#include <string>
+
+class CGLESShader : public Shaders::CGLSLShaderProgram
+{
+public:
+ CGLESShader(const char* shader, const std::string& prefix);
+ CGLESShader(const char* vshader, const char* fshader, const std::string& prefix);
+ void OnCompiledAndLinked() override;
+ bool OnEnabled() override;
+ void Free();
+
+ GLint GetPosLoc() { return m_hPos; }
+ GLint GetColLoc() { return m_hCol; }
+ GLint GetCord0Loc() { return m_hCord0; }
+ GLint GetCord1Loc() { return m_hCord1; }
+ GLint GetUniColLoc() { return m_hUniCol; }
+ GLint GetCoord0MatrixLoc() { return m_hCoord0Matrix; }
+ GLint GetFieldLoc() { return m_hField; }
+ GLint GetStepLoc() { return m_hStep; }
+ GLint GetContrastLoc() { return m_hContrast; }
+ GLint GetBrightnessLoc() { return m_hBrightness; }
+ GLint GetModelLoc() { return m_hModel; }
+ bool HardwareClipIsPossible() { return m_clipPossible; }
+ GLfloat GetClipXFactor() { return m_clipXFactor; }
+ GLfloat GetClipXOffset() { return m_clipXOffset; }
+ GLfloat GetClipYFactor() { return m_clipYFactor; }
+ GLfloat GetClipYOffset() { return m_clipYOffset; }
+
+protected:
+ GLint m_hTex0 = 0;
+ GLint m_hTex1 = 0;
+ GLint m_hUniCol = 0;
+ GLint m_hProj = 0;
+ GLint m_hModel = 0;
+ GLint m_hPos = 0;
+ GLint m_hCol = 0;
+ GLint m_hCord0 = 0;
+ GLint m_hCord1 = 0;
+ GLint m_hCoord0Matrix = 0;
+ GLint m_hField = 0;
+ GLint m_hStep = 0;
+ GLint m_hContrast = 0;
+ GLint m_hBrightness = 0;
+
+ const GLfloat *m_proj;
+ const GLfloat *m_model;
+
+ bool m_clipPossible;
+ GLfloat m_clipXFactor;
+ GLfloat m_clipXOffset;
+ GLfloat m_clipYFactor;
+ GLfloat m_clipYOffset;
+};
diff --git a/xbmc/rendering/gles/RenderSystemGLES.cpp b/xbmc/rendering/gles/RenderSystemGLES.cpp
new file mode 100644
index 0000000..d8557ff
--- /dev/null
+++ b/xbmc/rendering/gles/RenderSystemGLES.cpp
@@ -0,0 +1,669 @@
+/*
+ * 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 "RenderSystemGLES.h"
+
+#include "guilib/DirtyRegion.h"
+#include "guilib/GUITextureGLES.h"
+#include "rendering/MatrixGL.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/GLUtils.h"
+#include "utils/MathUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/TimeUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#if defined(TARGET_LINUX)
+#include "utils/EGLUtils.h"
+#endif
+
+using namespace std::chrono_literals;
+
+CRenderSystemGLES::CRenderSystemGLES()
+ : CRenderSystemBase()
+{
+}
+
+bool CRenderSystemGLES::InitRenderSystem()
+{
+ GLint maxTextureSize;
+
+ glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);
+
+ m_maxTextureSize = maxTextureSize;
+
+ // Get the GLES version number
+ m_RenderVersionMajor = 0;
+ m_RenderVersionMinor = 0;
+
+ const char* ver = (const char*)glGetString(GL_VERSION);
+ if (ver != 0)
+ {
+ sscanf(ver, "%d.%d", &m_RenderVersionMajor, &m_RenderVersionMinor);
+ if (!m_RenderVersionMajor)
+ sscanf(ver, "%*s %*s %d.%d", &m_RenderVersionMajor, &m_RenderVersionMinor);
+ m_RenderVersion = ver;
+ }
+
+ // Get our driver vendor and renderer
+ const char *tmpVendor = (const char*) glGetString(GL_VENDOR);
+ m_RenderVendor.clear();
+ if (tmpVendor != NULL)
+ m_RenderVendor = tmpVendor;
+
+ const char *tmpRenderer = (const char*) glGetString(GL_RENDERER);
+ m_RenderRenderer.clear();
+ if (tmpRenderer != NULL)
+ m_RenderRenderer = tmpRenderer;
+
+ m_RenderExtensions = " ";
+
+ const char *tmpExtensions = (const char*) glGetString(GL_EXTENSIONS);
+ if (tmpExtensions != NULL)
+ {
+ m_RenderExtensions += tmpExtensions;
+ }
+
+ m_RenderExtensions += " ";
+
+#if defined(GL_KHR_debug) && defined(TARGET_LINUX)
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_openGlDebugging)
+ {
+ if (IsExtSupported("GL_KHR_debug"))
+ {
+ auto glDebugMessageCallback = CEGLUtils::GetRequiredProcAddress<PFNGLDEBUGMESSAGECALLBACKKHRPROC>("glDebugMessageCallbackKHR");
+ auto glDebugMessageControl = CEGLUtils::GetRequiredProcAddress<PFNGLDEBUGMESSAGECONTROLKHRPROC>("glDebugMessageControlKHR");
+
+ glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_KHR);
+ glDebugMessageCallback(KODI::UTILS::GL::GlErrorCallback, nullptr);
+
+ // ignore shader compilation information
+ glDebugMessageControl(GL_DEBUG_SOURCE_SHADER_COMPILER_KHR, GL_DEBUG_TYPE_OTHER_KHR, GL_DONT_CARE, 0, nullptr, GL_FALSE);
+
+ CLog::Log(LOGDEBUG, "OpenGL(ES): debugging enabled");
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "OpenGL(ES): debugging requested but the required extension isn't available (GL_KHR_debug)");
+ }
+ }
+#endif
+
+ LogGraphicsInfo();
+
+ m_bRenderCreated = true;
+
+ InitialiseShaders();
+
+ CGUITextureGLES::Register();
+
+ return true;
+}
+
+bool CRenderSystemGLES::ResetRenderSystem(int width, int height)
+{
+ m_width = width;
+ m_height = height;
+
+ glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
+ CalculateMaxTexturesize();
+
+ CRect rect( 0, 0, width, height );
+ SetViewPort( rect );
+
+ glEnable(GL_SCISSOR_TEST);
+
+ glMatrixProject.Clear();
+ glMatrixProject->LoadIdentity();
+ glMatrixProject->Ortho(0.0f, width-1, height-1, 0.0f, -1.0f, 1.0f);
+ glMatrixProject.Load();
+
+ glMatrixModview.Clear();
+ glMatrixModview->LoadIdentity();
+ glMatrixModview.Load();
+
+ glMatrixTexture.Clear();
+ glMatrixTexture->LoadIdentity();
+ glMatrixTexture.Load();
+
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+ glEnable(GL_BLEND); // Turn Blending On
+ glDisable(GL_DEPTH_TEST);
+
+ return true;
+}
+
+bool CRenderSystemGLES::DestroyRenderSystem()
+{
+ ResetScissors();
+ CDirtyRegionList dirtyRegions;
+ CDirtyRegion dirtyWindow(CServiceBroker::GetWinSystem()->GetGfxContext().GetViewWindow());
+ dirtyRegions.push_back(dirtyWindow);
+
+ ClearBuffers(0);
+ glFinish();
+ PresentRenderImpl(true);
+
+ ReleaseShaders();
+ m_bRenderCreated = false;
+
+ return true;
+}
+
+bool CRenderSystemGLES::BeginRender()
+{
+ if (!m_bRenderCreated)
+ return false;
+
+ bool useLimited = CServiceBroker::GetWinSystem()->UseLimitedColor();
+
+ if (m_limitedColorRange != useLimited)
+ {
+ ReleaseShaders();
+ InitialiseShaders();
+ }
+
+ m_limitedColorRange = useLimited;
+
+ return true;
+}
+
+bool CRenderSystemGLES::EndRender()
+{
+ if (!m_bRenderCreated)
+ return false;
+
+ return true;
+}
+
+bool CRenderSystemGLES::ClearBuffers(UTILS::COLOR::Color color)
+{
+ if (!m_bRenderCreated)
+ return false;
+
+ float r = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color) / 255.0f;
+ float g = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color) / 255.0f;
+ float b = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color) / 255.0f;
+ float a = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color) / 255.0f;
+
+ glClearColor(r, g, b, a);
+
+ GLbitfield flags = GL_COLOR_BUFFER_BIT;
+ glClear(flags);
+
+ return true;
+}
+
+bool CRenderSystemGLES::IsExtSupported(const char* extension) const
+{
+ if (strcmp( extension, "GL_EXT_framebuffer_object" ) == 0)
+ {
+ // GLES has FBO as a core element, not an extension!
+ return true;
+ }
+ else
+ {
+ std::string name;
+ name = " ";
+ name += extension;
+ name += " ";
+
+ return m_RenderExtensions.find(name) != std::string::npos;
+ }
+}
+
+void CRenderSystemGLES::PresentRender(bool rendered, bool videoLayer)
+{
+ SetVSync(true);
+
+ if (!m_bRenderCreated)
+ return;
+
+ PresentRenderImpl(rendered);
+
+ // if video is rendered to a separate layer, we should not block this thread
+ if (!rendered && !videoLayer)
+ KODI::TIME::Sleep(40ms);
+}
+
+void CRenderSystemGLES::SetVSync(bool enable)
+{
+ if (m_bVsyncInit)
+ return;
+
+ if (!m_bRenderCreated)
+ return;
+
+ if (enable)
+ CLog::Log(LOGINFO, "GLES: Enabling VSYNC");
+ else
+ CLog::Log(LOGINFO, "GLES: Disabling VSYNC");
+
+ m_bVsyncInit = true;
+
+ SetVSyncImpl(enable);
+}
+
+void CRenderSystemGLES::CaptureStateBlock()
+{
+ if (!m_bRenderCreated)
+ return;
+
+ glMatrixProject.Push();
+ glMatrixModview.Push();
+ glMatrixTexture.Push();
+
+ glDisable(GL_SCISSOR_TEST); // fixes FBO corruption on Macs
+ glActiveTexture(GL_TEXTURE0);
+//! @todo - NOTE: Only for Screensavers & Visualisations
+// glColor3f(1.0, 1.0, 1.0);
+}
+
+void CRenderSystemGLES::ApplyStateBlock()
+{
+ if (!m_bRenderCreated)
+ return;
+
+ glMatrixProject.PopLoad();
+ glMatrixModview.PopLoad();
+ glMatrixTexture.PopLoad();
+ glActiveTexture(GL_TEXTURE0);
+ glEnable(GL_BLEND);
+ glEnable(GL_SCISSOR_TEST);
+ glClear(GL_DEPTH_BUFFER_BIT);
+}
+
+void CRenderSystemGLES::SetCameraPosition(const CPoint &camera, int screenWidth, int screenHeight, float stereoFactor)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ CPoint offset = camera - CPoint(screenWidth*0.5f, screenHeight*0.5f);
+
+ float w = (float)m_viewPort[2]*0.5f;
+ float h = (float)m_viewPort[3]*0.5f;
+
+ glMatrixModview->LoadIdentity();
+ glMatrixModview->Translatef(-(w + offset.x - stereoFactor), +(h + offset.y), 0);
+ glMatrixModview->LookAt(0.0f, 0.0f, -2.0f * h, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f);
+ glMatrixModview.Load();
+
+ glMatrixProject->LoadIdentity();
+ glMatrixProject->Frustum( (-w - offset.x)*0.5f, (w - offset.x)*0.5f, (-h + offset.y)*0.5f, (h + offset.y)*0.5f, h, 100*h);
+ glMatrixProject.Load();
+}
+
+void CRenderSystemGLES::Project(float &x, float &y, float &z)
+{
+ GLfloat coordX, coordY, coordZ;
+ if (CMatrixGL::Project(x, y, z, glMatrixModview.Get(), glMatrixProject.Get(), m_viewPort, &coordX, &coordY, &coordZ))
+ {
+ x = coordX;
+ y = (float)(m_viewPort[1] + m_viewPort[3] - coordY);
+ z = 0;
+ }
+}
+
+void CRenderSystemGLES::CalculateMaxTexturesize()
+{
+ // GLES cannot do PROXY textures to determine maximum size,
+ CLog::Log(LOGINFO, "GLES: Maximum texture width: {}", m_maxTextureSize);
+}
+
+void CRenderSystemGLES::GetViewPort(CRect& viewPort)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ viewPort.x1 = m_viewPort[0];
+ viewPort.y1 = m_height - m_viewPort[1] - m_viewPort[3];
+ viewPort.x2 = m_viewPort[0] + m_viewPort[2];
+ viewPort.y2 = viewPort.y1 + m_viewPort[3];
+}
+
+void CRenderSystemGLES::SetViewPort(const CRect& viewPort)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ glScissor((GLint) viewPort.x1, (GLint) (m_height - viewPort.y1 - viewPort.Height()), (GLsizei) viewPort.Width(), (GLsizei) viewPort.Height());
+ glViewport((GLint) viewPort.x1, (GLint) (m_height - viewPort.y1 - viewPort.Height()), (GLsizei) viewPort.Width(), (GLsizei) viewPort.Height());
+ m_viewPort[0] = viewPort.x1;
+ m_viewPort[1] = m_height - viewPort.y1 - viewPort.Height();
+ m_viewPort[2] = viewPort.Width();
+ m_viewPort[3] = viewPort.Height();
+}
+
+bool CRenderSystemGLES::ScissorsCanEffectClipping()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->HardwareClipIsPossible();
+
+ return false;
+}
+
+CRect CRenderSystemGLES::ClipRectToScissorRect(const CRect &rect)
+{
+ if (!m_pShader[m_method])
+ return CRect();
+ float xFactor = m_pShader[m_method]->GetClipXFactor();
+ float xOffset = m_pShader[m_method]->GetClipXOffset();
+ float yFactor = m_pShader[m_method]->GetClipYFactor();
+ float yOffset = m_pShader[m_method]->GetClipYOffset();
+ return CRect(rect.x1 * xFactor + xOffset,
+ rect.y1 * yFactor + yOffset,
+ rect.x2 * xFactor + xOffset,
+ rect.y2 * yFactor + yOffset);
+}
+
+void CRenderSystemGLES::SetScissors(const CRect &rect)
+{
+ if (!m_bRenderCreated)
+ return;
+ GLint x1 = MathUtils::round_int(static_cast<double>(rect.x1));
+ GLint y1 = MathUtils::round_int(static_cast<double>(rect.y1));
+ GLint x2 = MathUtils::round_int(static_cast<double>(rect.x2));
+ GLint y2 = MathUtils::round_int(static_cast<double>(rect.y2));
+ glScissor(x1, m_height - y2, x2-x1, y2-y1);
+}
+
+void CRenderSystemGLES::ResetScissors()
+{
+ SetScissors(CRect(0, 0, (float)m_width, (float)m_height));
+}
+
+void CRenderSystemGLES::InitialiseShaders()
+{
+ std::string defines;
+ m_limitedColorRange = CServiceBroker::GetWinSystem()->UseLimitedColor();
+ if (m_limitedColorRange)
+ {
+ defines += "#define KODI_LIMITED_RANGE 1\n";
+ }
+
+ m_pShader[ShaderMethodGLES::SM_DEFAULT] =
+ std::make_unique<CGLESShader>("gles_shader.vert", "gles_shader_default.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_DEFAULT]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_DEFAULT]->Free();
+ m_pShader[ShaderMethodGLES::SM_DEFAULT].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_default.frag - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGLES::SM_TEXTURE] =
+ std::make_unique<CGLESShader>("gles_shader_texture.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_TEXTURE]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_TEXTURE]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_texture.frag - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGLES::SM_MULTI] =
+ std::make_unique<CGLESShader>("gles_shader_multi.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_MULTI]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_MULTI]->Free();
+ m_pShader[ShaderMethodGLES::SM_MULTI].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_multi.frag - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGLES::SM_FONTS] =
+ std::make_unique<CGLESShader>("gles_shader_fonts.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_FONTS]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_FONTS]->Free();
+ m_pShader[ShaderMethodGLES::SM_FONTS].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_fonts.frag - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_NOBLEND] =
+ std::make_unique<CGLESShader>("gles_shader_texture_noblend.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_TEXTURE_NOBLEND]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_NOBLEND]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_NOBLEND].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_texture_noblend.frag - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGLES::SM_MULTI_BLENDCOLOR] =
+ std::make_unique<CGLESShader>("gles_shader_multi_blendcolor.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_MULTI_BLENDCOLOR]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_MULTI_BLENDCOLOR]->Free();
+ m_pShader[ShaderMethodGLES::SM_MULTI_BLENDCOLOR].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_multi_blendcolor.frag - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA] =
+ std::make_unique<CGLESShader>("gles_shader_rgba.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_rgba.frag - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BLENDCOLOR] =
+ std::make_unique<CGLESShader>("gles_shader_rgba_blendcolor.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BLENDCOLOR]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BLENDCOLOR]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BLENDCOLOR].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_rgba_blendcolor.frag - compile and link failed");
+ }
+
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB] =
+ std::make_unique<CGLESShader>("gles_shader_rgba_bob.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_rgba_bob.frag - compile and link failed");
+ }
+
+ if (IsExtSupported("GL_OES_EGL_image_external"))
+ {
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_OES] =
+ std::make_unique<CGLESShader>("gles_shader_rgba_oes.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_OES]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_OES]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_OES].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_rgba_oes.frag - compile and link failed");
+ }
+
+
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB_OES] =
+ std::make_unique<CGLESShader>("gles_shader_rgba_bob_oes.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB_OES]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB_OES]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB_OES].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_rgba_bob_oes.frag - compile and link failed");
+ }
+ }
+
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_NOALPHA] =
+ std::make_unique<CGLESShader>("gles_shader_texture_noalpha.frag", defines);
+ if (!m_pShader[ShaderMethodGLES::SM_TEXTURE_NOALPHA]->CompileAndLink())
+ {
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_NOALPHA]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_NOALPHA].reset();
+ CLog::Log(LOGERROR, "GUI Shader gles_shader_texture_noalpha.frag - compile and link failed");
+ }
+}
+
+void CRenderSystemGLES::ReleaseShaders()
+{
+ if (m_pShader[ShaderMethodGLES::SM_DEFAULT])
+ m_pShader[ShaderMethodGLES::SM_DEFAULT]->Free();
+ m_pShader[ShaderMethodGLES::SM_DEFAULT].reset();
+
+ if (m_pShader[ShaderMethodGLES::SM_TEXTURE])
+ m_pShader[ShaderMethodGLES::SM_TEXTURE]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE].reset();
+
+ if (m_pShader[ShaderMethodGLES::SM_MULTI])
+ m_pShader[ShaderMethodGLES::SM_MULTI]->Free();
+ m_pShader[ShaderMethodGLES::SM_MULTI].reset();
+
+ if (m_pShader[ShaderMethodGLES::SM_FONTS])
+ m_pShader[ShaderMethodGLES::SM_FONTS]->Free();
+ m_pShader[ShaderMethodGLES::SM_FONTS].reset();
+
+ if (m_pShader[ShaderMethodGLES::SM_TEXTURE_NOBLEND])
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_NOBLEND]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_NOBLEND].reset();
+
+ if (m_pShader[ShaderMethodGLES::SM_MULTI_BLENDCOLOR])
+ m_pShader[ShaderMethodGLES::SM_MULTI_BLENDCOLOR]->Free();
+ m_pShader[ShaderMethodGLES::SM_MULTI_BLENDCOLOR].reset();
+
+ if (m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA])
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA].reset();
+
+ if (m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BLENDCOLOR])
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BLENDCOLOR]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BLENDCOLOR].reset();
+
+ if (m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB])
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB].reset();
+
+ if (m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_OES])
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_OES]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_OES].reset();
+
+ if (m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB_OES])
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB_OES]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_RGBA_BOB_OES].reset();
+
+ if (m_pShader[ShaderMethodGLES::SM_TEXTURE_NOALPHA])
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_NOALPHA]->Free();
+ m_pShader[ShaderMethodGLES::SM_TEXTURE_NOALPHA].reset();
+}
+
+void CRenderSystemGLES::EnableGUIShader(ShaderMethodGLES method)
+{
+ m_method = method;
+ if (m_pShader[m_method])
+ {
+ m_pShader[m_method]->Enable();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "Invalid GUI Shader selected - {}", method);
+ }
+}
+
+void CRenderSystemGLES::DisableGUIShader()
+{
+ if (m_pShader[m_method])
+ {
+ m_pShader[m_method]->Disable();
+ }
+ m_method = ShaderMethodGLES::SM_DEFAULT;
+}
+
+GLint CRenderSystemGLES::GUIShaderGetPos()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetPosLoc();
+
+ return -1;
+}
+
+GLint CRenderSystemGLES::GUIShaderGetCol()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetColLoc();
+
+ return -1;
+}
+
+GLint CRenderSystemGLES::GUIShaderGetCoord0()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetCord0Loc();
+
+ return -1;
+}
+
+GLint CRenderSystemGLES::GUIShaderGetCoord1()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetCord1Loc();
+
+ return -1;
+}
+
+GLint CRenderSystemGLES::GUIShaderGetUniCol()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetUniColLoc();
+
+ return -1;
+}
+
+GLint CRenderSystemGLES::GUIShaderGetCoord0Matrix()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetCoord0MatrixLoc();
+
+ return -1;
+}
+
+GLint CRenderSystemGLES::GUIShaderGetField()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetFieldLoc();
+
+ return -1;
+}
+
+GLint CRenderSystemGLES::GUIShaderGetStep()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetStepLoc();
+
+ return -1;
+}
+
+GLint CRenderSystemGLES::GUIShaderGetContrast()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetContrastLoc();
+
+ return -1;
+}
+
+GLint CRenderSystemGLES::GUIShaderGetBrightness()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetBrightnessLoc();
+
+ return -1;
+}
+
+bool CRenderSystemGLES::SupportsStereo(RENDER_STEREO_MODE mode) const
+{
+ return CRenderSystemBase::SupportsStereo(mode);
+}
+
+GLint CRenderSystemGLES::GUIShaderGetModel()
+{
+ if (m_pShader[m_method])
+ return m_pShader[m_method]->GetModelLoc();
+
+ return -1;
+}
diff --git a/xbmc/rendering/gles/RenderSystemGLES.h b/xbmc/rendering/gles/RenderSystemGLES.h
new file mode 100644
index 0000000..e0cd72b
--- /dev/null
+++ b/xbmc/rendering/gles/RenderSystemGLES.h
@@ -0,0 +1,143 @@
+/*
+ * 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 "GLESShader.h"
+#include "rendering/RenderSystem.h"
+#include "utils/ColorUtils.h"
+#include "utils/Map.h"
+
+#include <map>
+
+#include <fmt/format.h>
+
+#include "system_gl.h"
+
+enum class ShaderMethodGLES
+{
+ SM_DEFAULT,
+ SM_TEXTURE,
+ SM_MULTI,
+ SM_FONTS,
+ SM_TEXTURE_NOBLEND,
+ SM_MULTI_BLENDCOLOR,
+ SM_TEXTURE_RGBA,
+ SM_TEXTURE_RGBA_OES,
+ SM_TEXTURE_RGBA_BLENDCOLOR,
+ SM_TEXTURE_RGBA_BOB,
+ SM_TEXTURE_RGBA_BOB_OES,
+ SM_TEXTURE_NOALPHA,
+ SM_MAX
+};
+
+template<>
+struct fmt::formatter<ShaderMethodGLES> : fmt::formatter<std::string_view>
+{
+ template<typename FormatContext>
+ constexpr auto format(const ShaderMethodGLES& shaderMethod, FormatContext& ctx)
+ {
+ const auto it = ShaderMethodGLESMap.find(shaderMethod);
+ if (it == ShaderMethodGLESMap.cend())
+ throw std::range_error("no string mapping found for shader method");
+
+ return fmt::formatter<string_view>::format(it->second, ctx);
+ }
+
+private:
+ static constexpr auto ShaderMethodGLESMap = make_map<ShaderMethodGLES, std::string_view>({
+ {ShaderMethodGLES::SM_DEFAULT, "default"},
+ {ShaderMethodGLES::SM_TEXTURE, "texture"},
+ {ShaderMethodGLES::SM_MULTI, "multi"},
+ {ShaderMethodGLES::SM_FONTS, "fonts"},
+ {ShaderMethodGLES::SM_TEXTURE_NOBLEND, "texture no blending"},
+ {ShaderMethodGLES::SM_MULTI_BLENDCOLOR, "multi blend colour"},
+ {ShaderMethodGLES::SM_TEXTURE_RGBA, "texure rgba"},
+ {ShaderMethodGLES::SM_TEXTURE_RGBA_OES, "texture rgba OES"},
+ {ShaderMethodGLES::SM_TEXTURE_RGBA_BLENDCOLOR, "texture rgba blend colour"},
+ {ShaderMethodGLES::SM_TEXTURE_RGBA_BOB, "texture rgba bob"},
+ {ShaderMethodGLES::SM_TEXTURE_RGBA_BOB_OES, "texture rgba bob OES"},
+ {ShaderMethodGLES::SM_TEXTURE_NOALPHA, "texture no alpha"},
+ });
+
+ static_assert(static_cast<size_t>(ShaderMethodGLES::SM_MAX) == ShaderMethodGLESMap.size(),
+ "ShaderMethodGLESMap doesn't match the size of ShaderMethodGLES, did you forget to "
+ "add/remove a mapping?");
+};
+
+class CRenderSystemGLES : public CRenderSystemBase
+{
+public:
+ CRenderSystemGLES();
+ ~CRenderSystemGLES() override = default;
+
+ bool InitRenderSystem() override;
+ bool DestroyRenderSystem() override;
+ bool ResetRenderSystem(int width, int height) override;
+
+ bool BeginRender() override;
+ bool EndRender() override;
+ void PresentRender(bool rendered, bool videoLayer) override;
+ bool ClearBuffers(UTILS::COLOR::Color color) override;
+ bool IsExtSupported(const char* extension) const override;
+
+ void SetVSync(bool vsync);
+ void ResetVSync() { m_bVsyncInit = false; }
+
+ void SetViewPort(const CRect& viewPort) override;
+ void GetViewPort(CRect& viewPort) override;
+
+ bool ScissorsCanEffectClipping() override;
+ CRect ClipRectToScissorRect(const CRect &rect) override;
+ void SetScissors(const CRect& rect) override;
+ void ResetScissors() override;
+
+ void CaptureStateBlock() override;
+ void ApplyStateBlock() override;
+
+ void SetCameraPosition(const CPoint &camera, int screenWidth, int screenHeight, float stereoFactor = 0.0f) override;
+
+ bool SupportsStereo(RENDER_STEREO_MODE mode) const override;
+
+ void Project(float &x, float &y, float &z) override;
+
+ std::string GetShaderPath(const std::string &filename) override { return "GLES/2.0/"; }
+
+ void InitialiseShaders();
+ void ReleaseShaders();
+ void EnableGUIShader(ShaderMethodGLES method);
+ void DisableGUIShader();
+
+ GLint GUIShaderGetPos();
+ GLint GUIShaderGetCol();
+ GLint GUIShaderGetCoord0();
+ GLint GUIShaderGetCoord1();
+ GLint GUIShaderGetUniCol();
+ GLint GUIShaderGetCoord0Matrix();
+ GLint GUIShaderGetField();
+ GLint GUIShaderGetStep();
+ GLint GUIShaderGetContrast();
+ GLint GUIShaderGetBrightness();
+ GLint GUIShaderGetModel();
+
+protected:
+ virtual void SetVSyncImpl(bool enable) = 0;
+ virtual void PresentRenderImpl(bool rendered) = 0;
+ void CalculateMaxTexturesize();
+
+ bool m_bVsyncInit{false};
+ int m_width;
+ int m_height;
+
+ std::string m_RenderExtensions;
+
+ std::map<ShaderMethodGLES, std::unique_ptr<CGLESShader>> m_pShader;
+ ShaderMethodGLES m_method = ShaderMethodGLES::SM_DEFAULT;
+
+ GLint m_viewPort[4];
+};
diff --git a/xbmc/rendering/gles/ScreenshotSurfaceGLES.cpp b/xbmc/rendering/gles/ScreenshotSurfaceGLES.cpp
new file mode 100644
index 0000000..3c290ec
--- /dev/null
+++ b/xbmc/rendering/gles/ScreenshotSurfaceGLES.cpp
@@ -0,0 +1,72 @@
+/*
+ * 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 "ScreenshotSurfaceGLES.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/Screenshot.h"
+#include "windowing/GraphicContext.h"
+
+#include <mutex>
+#include <vector>
+
+#include "system_gl.h"
+
+void CScreenshotSurfaceGLES::Register()
+{
+ CScreenShot::Register(CScreenshotSurfaceGLES::CreateSurface);
+}
+
+std::unique_ptr<IScreenshotSurface> CScreenshotSurfaceGLES::CreateSurface()
+{
+ return std::unique_ptr<CScreenshotSurfaceGLES>(new CScreenshotSurfaceGLES());
+}
+
+bool CScreenshotSurfaceGLES::Capture()
+{
+ CWinSystemBase* winsystem = CServiceBroker::GetWinSystem();
+ if (!winsystem)
+ return false;
+
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (!gui)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(winsystem->GetGfxContext());
+ gui->GetWindowManager().Render();
+
+ //get current viewport
+ GLint viewport[4];
+ glGetIntegerv(GL_VIEWPORT, viewport);
+
+ m_width = viewport[2] - viewport[0];
+ m_height = viewport[3] - viewport[1];
+ m_stride = m_width * 4;
+ std::vector<uint8_t> surface(m_stride * m_height);
+
+ //read pixels from the backbuffer
+ glReadPixels(viewport[0], viewport[1], viewport[2], viewport[3], GL_RGBA, GL_UNSIGNED_BYTE, static_cast<GLvoid*>(surface.data()));
+
+ //make a new buffer and copy the read image to it with the Y axis inverted
+ m_buffer = new unsigned char[m_stride * m_height];
+ for (int y = 0; y < m_height; y++)
+ {
+ // we need to save in BGRA order so XOR Swap RGBA -> BGRA
+ unsigned char* swap_pixels = surface.data() + (m_height - y - 1) * m_stride;
+ for (int x = 0; x < m_width; x++, swap_pixels += 4)
+ {
+ std::swap(swap_pixels[0], swap_pixels[2]);
+ }
+
+ memcpy(m_buffer + y * m_stride, surface.data() + (m_height - y - 1) * m_stride, m_stride);
+ }
+
+ return m_buffer != nullptr;
+}
diff --git a/xbmc/rendering/gles/ScreenshotSurfaceGLES.h b/xbmc/rendering/gles/ScreenshotSurfaceGLES.h
new file mode 100644
index 0000000..1ca1730
--- /dev/null
+++ b/xbmc/rendering/gles/ScreenshotSurfaceGLES.h
@@ -0,0 +1,22 @@
+/*
+ * 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 "utils/IScreenshotSurface.h"
+
+#include <memory>
+
+class CScreenshotSurfaceGLES : public IScreenshotSurface
+{
+public:
+ static void Register();
+ static std::unique_ptr<IScreenshotSurface> CreateSurface();
+
+ bool Capture() override;
+};
diff --git a/xbmc/settings/AdvancedSettings.cpp b/xbmc/settings/AdvancedSettings.cpp
new file mode 100644
index 0000000..4fd93d0
--- /dev/null
+++ b/xbmc/settings/AdvancedSettings.cpp
@@ -0,0 +1,1559 @@
+/*
+ * 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 "AdvancedSettings.h"
+
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "application/AppParams.h"
+#include "filesystem/SpecialProtocol.h"
+#include "network/DNSNameCache.h"
+#include "profiles/ProfileManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/FileUtils.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <climits>
+#include <regex>
+#include <string>
+#include <vector>
+
+using namespace ADDON;
+
+CAdvancedSettings::CAdvancedSettings()
+{
+ m_initialized = false;
+ m_fullScreen = false;
+}
+
+void CAdvancedSettings::OnSettingsLoaded()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // load advanced settings
+ Load(*profileManager);
+
+ // default players?
+ CLog::Log(LOGINFO, "Default Video Player: {}", m_videoDefaultPlayer);
+ CLog::Log(LOGINFO, "Default Audio Player: {}", m_audioDefaultPlayer);
+
+ // setup any logging...
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_DEBUG_SHOWLOGINFO))
+ {
+ m_logLevel = std::max(m_logLevelHint, LOG_LEVEL_DEBUG_FREEMEM);
+ CLog::Log(LOGINFO, "Enabled debug logging due to GUI setting ({})", m_logLevel);
+ }
+ else
+ {
+ m_logLevel = std::min(m_logLevelHint, LOG_LEVEL_DEBUG/*LOG_LEVEL_NORMAL*/);
+ CLog::Log(LOGINFO, "Disabled debug logging due to GUI setting. Level {}.", m_logLevel);
+ }
+ CServiceBroker::GetLogging().SetLogLevel(m_logLevel);
+}
+
+void CAdvancedSettings::OnSettingsUnloaded()
+{
+ m_initialized = false;
+}
+
+void CAdvancedSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_DEBUG_SHOWLOGINFO)
+ SetDebugMode(std::static_pointer_cast<const CSettingBool>(setting)->GetValue());
+}
+
+void CAdvancedSettings::Initialize(CSettingsManager& settingsMgr)
+{
+ Initialize();
+
+ const auto params = CServiceBroker::GetAppParams();
+
+ if (params->GetLogLevel() == LOG_LEVEL_DEBUG)
+ {
+ m_logLevel = LOG_LEVEL_DEBUG;
+ m_logLevelHint = LOG_LEVEL_DEBUG;
+ CServiceBroker::GetLogging().SetLogLevel(LOG_LEVEL_DEBUG);
+ }
+
+ const std::string& settingsFile = params->GetSettingsFile();
+ if (!settingsFile.empty())
+ AddSettingsFile(settingsFile);
+
+ if (params->IsStartFullScreen())
+ m_startFullScreen = true;
+
+ if (params->IsStandAlone())
+ m_handleMounting = true;
+
+ settingsMgr.RegisterSettingsHandler(this, true);
+ std::set<std::string> settingSet;
+ settingSet.insert(CSettings::SETTING_DEBUG_SHOWLOGINFO);
+ settingsMgr.RegisterCallback(this, settingSet);
+}
+
+void CAdvancedSettings::Uninitialize(CSettingsManager& settingsMgr)
+{
+ settingsMgr.UnregisterCallback(this);
+ settingsMgr.UnregisterSettingsHandler(this);
+ settingsMgr.UnregisterSettingOptionsFiller("loggingcomponents");
+
+ Clear();
+
+ m_initialized = false;
+}
+
+void CAdvancedSettings::Initialize()
+{
+ if (m_initialized)
+ return;
+
+ m_audioApplyDrc = -1.0f;
+ m_VideoPlayerIgnoreDTSinWAV = false;
+
+ //default hold time of 25 ms, this allows a 20 hertz sine to pass undistorted
+ m_limiterHold = 0.025f;
+ m_limiterRelease = 0.1f;
+
+ m_seekSteps = { 10, 30, 60, 180, 300, 600, 1800 };
+
+ m_audioDefaultPlayer = "paplayer";
+ m_audioPlayCountMinimumPercent = 90.0f;
+
+ m_videoSubsDelayRange = 60;
+ m_videoAudioDelayRange = 10;
+ m_videoUseTimeSeeking = true;
+ m_videoTimeSeekForward = 30;
+ m_videoTimeSeekBackward = -30;
+ m_videoTimeSeekForwardBig = 600;
+ m_videoTimeSeekBackwardBig = -600;
+ m_videoPercentSeekForward = 2;
+ m_videoPercentSeekBackward = -2;
+ m_videoPercentSeekForwardBig = 10;
+ m_videoPercentSeekBackwardBig = -10;
+
+ m_videoPPFFmpegPostProc = "ha:128:7,va,dr";
+ m_videoDefaultPlayer = "VideoPlayer";
+ m_videoIgnoreSecondsAtStart = 3*60;
+ m_videoIgnorePercentAtEnd = 8.0f;
+ m_videoPlayCountMinimumPercent = 90.0f;
+ m_videoVDPAUScaling = -1;
+ m_videoNonLinStretchRatio = 0.5f;
+ m_videoAutoScaleMaxFps = 30.0f;
+ m_videoCaptureUseOcclusionQuery = -1; //-1 is auto detect
+ m_videoVDPAUtelecine = false;
+ m_videoVDPAUdeintSkipChromaHD = false;
+ m_DXVACheckCompatibility = false;
+ m_DXVACheckCompatibilityPresent = false;
+ m_videoFpsDetect = 1;
+ m_maxTempo = 1.55f;
+ m_videoPreferStereoStream = false;
+
+ m_videoDefaultLatency = 0.0;
+
+ m_musicUseTimeSeeking = true;
+ m_musicTimeSeekForward = 10;
+ m_musicTimeSeekBackward = -10;
+ m_musicTimeSeekForwardBig = 60;
+ m_musicTimeSeekBackwardBig = -60;
+ m_musicPercentSeekForward = 1;
+ m_musicPercentSeekBackward = -1;
+ m_musicPercentSeekForwardBig = 10;
+ m_musicPercentSeekBackwardBig = -10;
+
+ m_slideshowPanAmount = 2.5f;
+ m_slideshowZoomAmount = 5.0f;
+ m_slideshowBlackBarCompensation = 20.0f;
+
+ m_songInfoDuration = 10;
+
+ m_cddbAddress = "gnudb.gnudb.org";
+ m_addSourceOnTop = false;
+
+ m_handleMounting = CServiceBroker::GetAppParams()->IsStandAlone();
+
+ m_fullScreenOnMovieStart = true;
+ m_cachePath = "special://temp/";
+
+ m_videoCleanDateTimeRegExp = "(.*[^ _\\,\\.\\(\\)\\[\\]\\-])[ _\\.\\(\\)\\[\\]\\-]+(19[0-9][0-9]|20[0-9][0-9])([ _\\,\\.\\(\\)\\[\\]\\-]|[^0-9]$)?";
+
+ m_videoCleanStringRegExps.clear();
+ m_videoCleanStringRegExps.emplace_back(
+ "[ "
+ "_\\,\\.\\(\\)\\[\\]\\-](10bit|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|3d|aac|ac3|"
+ "aka|atmos|avi|bd5|bdrip|bluray|brrip|cam|cd[1-9]|custom|dc|ddp|divx|divx5|dolbydigital|"
+ "dolbyvision|dsr|dsrip|dts|dts-hdma|dts-hra|dts-x|dv|dvd|dvd5|dvd9|dvdivx|dvdrip|dvdscr|"
+ "dvdscreener|extended|fragment|fs|h264|h265|hdr|hdr10|hevc|hddvd|hdrip|hdtv|hdtvrip|hrhd|"
+ "hrhdtv|internal|limited|multisubs|nfofix|ntsc|ogg|ogm|pal|pdtv|proper|r3|r5|read.nfo|"
+ "remastered|remux|repack|rerip|retail|screener|se|svcd|tc|telecine|telesync|truehd|ts|uhd|"
+ "unrated|ws|x264|x265|xvid|xvidvd|xxx|web-dl|webrip|www.www|\\[.*\\])([ "
+ "_\\,\\.\\(\\)\\[\\]\\-]|$)");
+ m_videoCleanStringRegExps.emplace_back("(\\[.*\\])");
+
+ // this vector will be inserted at the end to
+ // m_moviesExcludeFromScanRegExps, m_tvshowExcludeFromScanRegExps and
+ // m_audioExcludeFromScanRegExps
+ m_allExcludeFromScanRegExps.clear();
+ m_allExcludeFromScanRegExps.emplace_back("[\\/].+\\.ite[\\/]"); // ignore itunes extras dir
+ m_allExcludeFromScanRegExps.emplace_back("[\\/]\\.\\_");
+ m_allExcludeFromScanRegExps.emplace_back("\\.DS_Store");
+ m_allExcludeFromScanRegExps.emplace_back("\\.AppleDouble");
+
+ m_moviesExcludeFromScanRegExps.clear();
+ m_moviesExcludeFromScanRegExps.emplace_back("-trailer");
+ m_moviesExcludeFromScanRegExps.emplace_back("[!-._ \\\\/]sample[-._ \\\\/]");
+ m_moviesExcludeFromScanRegExps.emplace_back("[\\/](proof|subs)[\\/]");
+ m_moviesExcludeFromScanRegExps.insert(m_moviesExcludeFromScanRegExps.end(),
+ m_allExcludeFromScanRegExps.begin(),
+ m_allExcludeFromScanRegExps.end());
+
+
+ m_tvshowExcludeFromScanRegExps.clear();
+ m_tvshowExcludeFromScanRegExps.emplace_back("[!-._ \\\\/]sample[-._ \\\\/]");
+ m_tvshowExcludeFromScanRegExps.insert(m_tvshowExcludeFromScanRegExps.end(),
+ m_allExcludeFromScanRegExps.begin(),
+ m_allExcludeFromScanRegExps.end());
+
+
+ m_audioExcludeFromScanRegExps.clear();
+ m_audioExcludeFromScanRegExps.insert(m_audioExcludeFromScanRegExps.end(),
+ m_allExcludeFromScanRegExps.begin(),
+ m_allExcludeFromScanRegExps.end());
+
+ m_folderStackRegExps.clear();
+ m_folderStackRegExps.emplace_back("((cd|dvd|dis[ck])[0-9]+)$");
+
+ m_videoStackRegExps.clear();
+ m_videoStackRegExps.emplace_back("(.*?)([ _.-]*(?:cd|dvd|p(?:(?:ar)?t)|dis[ck])[ _.-]*[0-9]+)(.*?)(\\.[^.]+)$");
+ m_videoStackRegExps.emplace_back("(.*?)([ _.-]*(?:cd|dvd|p(?:(?:ar)?t)|dis[ck])[ _.-]*[a-d])(.*?)(\\.[^.]+)$");
+ m_videoStackRegExps.emplace_back("(.*?)([ ._-]*[a-d])(.*?)(\\.[^.]+)$");
+ // This one is a bit too greedy to enable by default. It will stack sequels
+ // in a flat dir structure, but is perfectly safe in a dir-per-vid one.
+ //m_videoStackRegExps.push_back("(.*?)([ ._-]*[0-9])(.*?)(\\.[^.]+)$");
+
+ m_tvshowEnumRegExps.clear();
+ // foo.s01.e01, foo.s01_e01, S01E02 foo, S01 - E02, S01xE02
+ m_tvshowEnumRegExps.push_back(TVShowRegexp(false,"s([0-9]+)[ ._x-]*e([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$"));
+ // foo.ep01, foo.EP_01, foo.E01
+ m_tvshowEnumRegExps.push_back(TVShowRegexp(
+ false, "[\\._ -]?()e(?:p[ ._-]?)?([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$"));
+ // foo.yyyy.mm.dd.* (byDate=true)
+ m_tvshowEnumRegExps.push_back(TVShowRegexp(true,"([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})"));
+ // foo.mm.dd.yyyy.* (byDate=true)
+ m_tvshowEnumRegExps.push_back(TVShowRegexp(true,"([0-9]{2})[\\.-]([0-9]{2})[\\.-]([0-9]{4})"));
+ // foo.1x09* or just /1x09*
+ m_tvshowEnumRegExps.push_back(TVShowRegexp(false,"[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$"));
+ // Part I, Pt.VI, Part 1
+ m_tvshowEnumRegExps.push_back(TVShowRegexp(false,"[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$"));
+ // This regexp is for matching special episodes by their title, e.g. foo.special.mp4
+ m_tvshowEnumRegExps.push_back(
+ TVShowRegexp(false, "[\\\\/]([^\\\\/]+)\\.special\\.[a-z0-9]+$", 0, true));
+
+ // foo.103*, 103 foo
+ // XXX: This regex is greedy and will match years in show names. It should always be last.
+ m_tvshowEnumRegExps.push_back(TVShowRegexp(false,"[\\\\/\\._ -]([0-9]+)([0-9][0-9](?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([\\._ -][^\\\\/]*)$"));
+
+ m_tvshowMultiPartEnumRegExp = "^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)";
+
+ m_remoteDelay = 3;
+ m_bScanIRServer = true;
+
+ m_playlistAsFolders = true;
+ m_detectAsUdf = false;
+
+ m_fanartRes = 1080;
+ m_imageRes = 720;
+ m_imageScalingAlgorithm = CPictureScalingAlgorithm::Default;
+ m_imageQualityJpeg = 4;
+
+ m_sambaclienttimeout = 30;
+ m_sambadoscodepage = "";
+ m_sambastatfiles = true;
+
+ m_bHTTPDirectoryStatFilesize = false;
+
+ m_bFTPThumbs = false;
+
+ m_bShoutcastArt = true;
+
+ m_musicThumbs = "folder.jpg|Folder.jpg|folder.JPG|Folder.JPG|cover.jpg|Cover.jpg|cover.jpeg|thumb.jpg|Thumb.jpg|thumb.JPG|Thumb.JPG";
+ m_musicArtistExtraArt = {};
+ m_musicAlbumExtraArt = {};
+
+ m_bMusicLibraryAllItemsOnBottom = false;
+ m_bMusicLibraryCleanOnUpdate = false;
+ m_bMusicLibraryArtistSortOnUpdate = false;
+ m_iMusicLibraryRecentlyAddedItems = 25;
+ m_strMusicLibraryAlbumFormat = "";
+ m_prioritiseAPEv2tags = false;
+ m_musicItemSeparator = " / ";
+ m_musicArtistSeparators = { ";", " feat. ", " ft. " };
+ m_videoItemSeparator = " / ";
+ m_iMusicLibraryDateAdded = 1; // prefer mtime over ctime and current time
+ m_bMusicLibraryUseISODates = false;
+
+ m_bVideoLibraryAllItemsOnBottom = false;
+ m_iVideoLibraryRecentlyAddedItems = 25;
+ m_bVideoLibraryCleanOnUpdate = false;
+ m_bVideoLibraryUseFastHash = true;
+ m_bVideoScannerIgnoreErrors = false;
+ m_iVideoLibraryDateAdded = 1; // prefer mtime over ctime and current time
+
+ m_videoEpisodeExtraArt = {};
+ m_videoTvShowExtraArt = {};
+ m_videoTvSeasonExtraArt = {};
+ m_videoMovieExtraArt = {};
+ m_videoMovieSetExtraArt = {};
+ m_videoMusicVideoExtraArt = {};
+
+ m_iEpgUpdateCheckInterval = 300; /* Check every X seconds, if EPG data need to be updated. This does not mean that
+ every X seconds an EPG update is actually triggered, it's just the interval how
+ often to check whether an update should be triggered. If this value is greater
+ than GUI setting 'epg.epgupdate' value, then EPG updates will done with the value
+ specified for 'updatecheckinterval', effectively overriding the GUI setting's value. */
+ m_iEpgCleanupInterval = 900; /* Remove old entries from the EPG every X seconds */
+ m_iEpgActiveTagCheckInterval = 60; /* Check for updated active tags every X seconds */
+ m_iEpgRetryInterruptedUpdateInterval = 30; /* Retry an interrupted EPG update after X seconds */
+ m_iEpgUpdateEmptyTagsInterval = 7200; /* If a TV channel has no EPG data, try to obtain data for that channel every
+ X seconds. This overrides the GUI setting 'epg.epgupdate' value, but only
+ for channels without EPG data. If this value is less than 'updatecheckinterval'
+ value, then data update will be done with the interval specified by
+ 'updatecheckinterval'.
+ Example 1: epg.epgupdate = 120 (minutes!), updatecheckinterval = 300,
+ updateemptytagsinterval = 60 => trigger an EPG update for every
+ channel without EPG data every 5 minutes and trigger an EPG update
+ for every channel with EPG data every 2 hours.
+ Example 2: epg.epgupdate = 120 (minutes!), updatecheckinterval = 300,
+ updateemptytagsinterval = 3600 => trigger an EPG update for every
+ channel without EPG data every 2 hours and trigger an EPG update
+ for every channel with EPG data every 1 hour. */
+ m_bEpgDisplayUpdatePopup = true; /* Display a progress popup while updating EPG data from clients */
+ m_bEpgDisplayIncrementalUpdatePopup = false; /* Display a progress popup while doing incremental EPG updates, but
+ only if 'displayupdatepopup' is also enabled. */
+
+ m_bEdlMergeShortCommBreaks = false; // Off by default
+ m_EdlDisplayCommbreakNotifications = true; // On by default
+ m_iEdlMaxCommBreakLength = 8 * 30 + 10; // Just over 8 * 30 second commercial break.
+ m_iEdlMinCommBreakLength = 3 * 30; // 3 * 30 second commercial breaks.
+ m_iEdlMaxCommBreakGap = 4 * 30; // 4 * 30 second commercial breaks.
+ m_iEdlMaxStartGap = 5 * 60; // 5 minutes.
+ m_iEdlCommBreakAutowait = 0; // Off by default
+ m_iEdlCommBreakAutowind = 0; // Off by default
+
+ m_curlconnecttimeout = 30;
+ m_curllowspeedtime = 20;
+ m_curlretries = 2;
+ m_curlKeepAliveInterval = 30;
+ m_curlDisableIPV6 = false; //Certain hardware/OS combinations have trouble
+ //with ipv6.
+ m_curlDisableHTTP2 = false;
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+ m_minimizeToTray = false;
+#endif
+#if defined(TARGET_DARWIN_EMBEDDED)
+ m_startFullScreen = true;
+#else
+ m_startFullScreen = false;
+#endif
+ m_showExitButton = true;
+ m_splashImage = true;
+
+ m_playlistRetries = 100;
+ m_playlistTimeout = 20; // 20 seconds timeout
+ m_GLRectangleHack = false;
+ m_iSkipLoopFilter = 0;
+ m_bVirtualShares = true;
+
+ m_cpuTempCmd = "";
+ m_gpuTempCmd = "";
+#if defined(TARGET_DARWIN)
+ // default for osx is fullscreen always on top
+ m_alwaysOnTop = true;
+#else
+ // default for windows is not always on top
+ m_alwaysOnTop = false;
+#endif
+
+ m_iPVRTimeCorrection = 0;
+ m_iPVRInfoToggleInterval = 5000;
+ m_bPVRChannelIconsAutoScan = true;
+ m_bPVRAutoScanIconsUserSet = false;
+ m_iPVRNumericChannelSwitchTimeout = 2000;
+ m_iPVRTimeshiftThreshold = 10;
+ m_bPVRTimeshiftSimpleOSD = true;
+ m_PVRDefaultSortOrder.sortBy = SortByDate;
+ m_PVRDefaultSortOrder.sortOrder = SortOrderDescending;
+
+ m_cacheMemSize = 1024 * 1024 * 20; // 20 MiB
+ m_cacheBufferMode = CACHE_BUFFER_MODE_NETWORK; // Default (buffer all network filesystems)
+ m_cacheChunkSize = 128 * 1024; // 128 KiB
+
+ // the following setting determines the readRate of a player data
+ // as multiply of the default data read rate
+ m_cacheReadFactor = 4.0f;
+
+ m_addonPackageFolderSize = 200;
+
+ m_jsonOutputCompact = true;
+ m_jsonTcpPort = 9090;
+
+ m_enableMultimediaKeys = false;
+
+ m_canWindowed = true;
+ m_guiVisualizeDirtyRegions = false;
+ m_guiAlgorithmDirtyRegions = 3;
+ m_guiSmartRedraw = false;
+ m_airTunesPort = 36666;
+ m_airPlayPort = 36667;
+
+ m_databaseMusic.Reset();
+ m_databaseVideo.Reset();
+
+ m_useLocaleCollation = true;
+
+ m_pictureExtensions = ".png|.jpg|.jpeg|.bmp|.gif|.ico|.tif|.tiff|.tga|.pcx|.cbz|.zip|.rss|.webp|.jp2|.apng";
+ m_musicExtensions = ".nsv|.m4a|.flac|.aac|.strm|.pls|.rm|.rma|.mpa|.wav|.wma|.ogg|.mp3|.mp2|.m3u|.gdm|.imf|.m15|.sfx|.uni|.ac3|.dts|.cue|.aif|.aiff|.wpl|.xspf|.ape|.mac|.mpc|.mp+|.mpp|.shn|.zip|.wv|.dsp|.xsp|.xwav|.waa|.wvs|.wam|.gcm|.idsp|.mpdsp|.mss|.spt|.rsd|.sap|.cmc|.cmr|.dmc|.mpt|.mpd|.rmt|.tmc|.tm8|.tm2|.oga|.url|.pxml|.tta|.rss|.wtv|.mka|.tak|.opus|.dff|.dsf|.m4b|.dtshd";
+ m_videoExtensions = ".m4v|.3g2|.3gp|.nsv|.tp|.ts|.ty|.strm|.pls|.rm|.rmvb|.mpd|.m3u|.m3u8|.ifo|.mov|.qt|.divx|.xvid|.bivx|.vob|.nrg|.img|.iso|.udf|.pva|.wmv|.asf|.asx|.ogm|.m2v|.avi|.bin|.dat|.mpg|.mpeg|.mp4|.mkv|.mk3d|.avc|.vp3|.svq3|.nuv|.viv|.dv|.fli|.flv|.001|.wpl|.xspf|.zip|.vdr|.dvr-ms|.xsp|.mts|.m2t|.m2ts|.evo|.ogv|.sdp|.avs|.rec|.url|.pxml|.vc1|.h264|.rcv|.rss|.mpls|.mpl|.webm|.bdmv|.bdm|.wtv|.trp|.f4v";
+ m_subtitlesExtensions = ".utf|.utf8|.utf-8|.sub|.srt|.smi|.rt|.txt|.ssa|.text|.ssa|.aqt|.jss|"
+ ".ass|.vtt|.idx|.ifo|.zip|.sup";
+ m_discStubExtensions = ".disc";
+ // internal music extensions
+ m_musicExtensions += "|.cdda";
+ // internal video extensions
+ m_videoExtensions += "|.pvr";
+
+ m_stereoscopicregex_3d = "[-. _]3d[-. _]";
+ m_stereoscopicregex_sbs = "[-. _]h?sbs[-. _]";
+ m_stereoscopicregex_tab = "[-. _]h?tab[-. _]";
+
+ m_logLevelHint = m_logLevel = LOG_LEVEL_NORMAL;
+
+ m_openGlDebugging = false;
+
+ m_userAgent = g_sysinfo.GetUserAgent();
+
+ m_nfsTimeout = 30;
+ m_nfsRetries = -1;
+
+ m_initialized = true;
+}
+
+bool CAdvancedSettings::Load(const CProfileManager &profileManager)
+{
+ // NOTE: This routine should NOT set the default of any of these parameters
+ // it should instead use the versions of GetString/Integer/Float that
+ // don't take defaults in. Defaults are set in the constructor above
+ Initialize(); // In case of profile switch.
+ ParseSettingsFile("special://xbmc/system/advancedsettings.xml");
+ for (unsigned int i = 0; i < m_settingsFiles.size(); i++)
+ ParseSettingsFile(m_settingsFiles[i]);
+
+ ParseSettingsFile(profileManager.GetUserDataItem("advancedsettings.xml"));
+
+ // Add the list of disc stub extensions (if any) to the list of video extensions
+ if (!m_discStubExtensions.empty())
+ m_videoExtensions += "|" + m_discStubExtensions;
+
+ return true;
+}
+
+void CAdvancedSettings::ParseSettingsFile(const std::string &file)
+{
+ CXBMCTinyXML advancedXML;
+ if (!CFileUtils::Exists(file))
+ {
+ CLog::Log(LOGINFO, "No settings file to load ({})", file);
+ return;
+ }
+
+ if (!advancedXML.LoadFile(file))
+ {
+ CLog::Log(LOGERROR, "Error loading {}, Line {}\n{}", file, advancedXML.ErrorRow(),
+ advancedXML.ErrorDesc());
+ return;
+ }
+
+ TiXmlElement *pRootElement = advancedXML.RootElement();
+ if (!pRootElement || StringUtils::CompareNoCase(pRootElement->Value(), "advancedsettings") != 0)
+ {
+ CLog::Log(LOGERROR, "Error loading {}, no <advancedsettings> node", file);
+ return;
+ }
+
+ // succeeded - tell the user it worked
+ CLog::Log(LOGINFO, "Loaded settings file from {}", file);
+
+ //Make a copy of the AS.xml and hide advancedsettings passwords
+ CXBMCTinyXML advancedXMLCopy(advancedXML);
+ TiXmlNode *pRootElementCopy = advancedXMLCopy.RootElement();
+ for (const auto& dbname : { "videodatabase", "musicdatabase", "tvdatabase", "epgdatabase" })
+ {
+ TiXmlNode *db = pRootElementCopy->FirstChild(dbname);
+ if (db)
+ {
+ TiXmlNode *passTag = db->FirstChild("pass");
+ if (passTag)
+ {
+ TiXmlNode *pass = passTag->FirstChild();
+ if (pass)
+ {
+ passTag->RemoveChild(pass);
+ passTag->LinkEndChild(new TiXmlText("*****"));
+ }
+ }
+ }
+ }
+ TiXmlNode *network = pRootElementCopy->FirstChild("network");
+ if (network)
+ {
+ TiXmlNode *passTag = network->FirstChild("httpproxypassword");
+ if (passTag)
+ {
+ TiXmlNode *pass = passTag->FirstChild();
+ if (pass)
+ {
+ passTag->RemoveChild(pass);
+ passTag->LinkEndChild(new TiXmlText("*****"));
+ }
+ }
+ if (network->FirstChildElement("nfstimeout"))
+ {
+#ifdef HAS_NFS_SET_TIMEOUT
+ XMLUtils::GetUInt(network, "nfstimeout", m_nfsTimeout, 0, 3600);
+#else
+ CLog::Log(LOGWARNING, "nfstimeout unsupported");
+#endif
+ }
+ if (network->FirstChildElement("nfsretries"))
+ {
+ XMLUtils::GetInt(network, "nfsretries", m_nfsRetries, -1, 30);
+ }
+ }
+
+ // Dump contents of copied AS.xml to debug log
+ TiXmlPrinter printer;
+ printer.SetLineBreak("\n");
+ printer.SetIndent(" ");
+ advancedXMLCopy.Accept(&printer);
+ // redact User/pass in URLs
+ std::regex redactRe("(\\w+://)\\S+:\\S+@");
+ CLog::Log(LOGINFO, "Contents of {} are...\n{}", file,
+ std::regex_replace(printer.CStr(), redactRe, "$1USERNAME:PASSWORD@"));
+
+ TiXmlElement *pElement = pRootElement->FirstChildElement("audio");
+ if (pElement)
+ {
+ XMLUtils::GetString(pElement, "defaultplayer", m_audioDefaultPlayer);
+ // 101 on purpose - can be used to never automark as watched
+ XMLUtils::GetFloat(pElement, "playcountminimumpercent", m_audioPlayCountMinimumPercent, 0.0f, 101.0f);
+
+ XMLUtils::GetBoolean(pElement, "usetimeseeking", m_musicUseTimeSeeking);
+ XMLUtils::GetInt(pElement, "timeseekforward", m_musicTimeSeekForward, 0, 6000);
+ XMLUtils::GetInt(pElement, "timeseekbackward", m_musicTimeSeekBackward, -6000, 0);
+ XMLUtils::GetInt(pElement, "timeseekforwardbig", m_musicTimeSeekForwardBig, 0, 6000);
+ XMLUtils::GetInt(pElement, "timeseekbackwardbig", m_musicTimeSeekBackwardBig, -6000, 0);
+
+ XMLUtils::GetInt(pElement, "percentseekforward", m_musicPercentSeekForward, 0, 100);
+ XMLUtils::GetInt(pElement, "percentseekbackward", m_musicPercentSeekBackward, -100, 0);
+ XMLUtils::GetInt(pElement, "percentseekforwardbig", m_musicPercentSeekForwardBig, 0, 100);
+ XMLUtils::GetInt(pElement, "percentseekbackwardbig", m_musicPercentSeekBackwardBig, -100, 0);
+
+ TiXmlElement* pAudioExcludes = pElement->FirstChildElement("excludefromlisting");
+ if (pAudioExcludes)
+ GetCustomRegexps(pAudioExcludes, m_audioExcludeFromListingRegExps);
+
+ pAudioExcludes = pElement->FirstChildElement("excludefromscan");
+ if (pAudioExcludes)
+ GetCustomRegexps(pAudioExcludes, m_audioExcludeFromScanRegExps);
+
+ XMLUtils::GetFloat(pElement, "applydrc", m_audioApplyDrc);
+ XMLUtils::GetBoolean(pElement, "VideoPlayerignoredtsinwav", m_VideoPlayerIgnoreDTSinWAV);
+
+ XMLUtils::GetFloat(pElement, "limiterhold", m_limiterHold, 0.0f, 100.0f);
+ XMLUtils::GetFloat(pElement, "limiterrelease", m_limiterRelease, 0.001f, 100.0f);
+ XMLUtils::GetUInt(pElement, "maxpassthroughoffsyncduration", m_maxPassthroughOffSyncDuration,
+ 10, 100);
+ }
+
+ pElement = pRootElement->FirstChildElement("x11");
+ if (pElement)
+ {
+ XMLUtils::GetBoolean(pElement, "omlsync", m_omlSync);
+ }
+
+ pElement = pRootElement->FirstChildElement("video");
+ if (pElement)
+ {
+ XMLUtils::GetString(pElement, "stereoscopicregex3d", m_stereoscopicregex_3d);
+ XMLUtils::GetString(pElement, "stereoscopicregexsbs", m_stereoscopicregex_sbs);
+ XMLUtils::GetString(pElement, "stereoscopicregextab", m_stereoscopicregex_tab);
+ XMLUtils::GetFloat(pElement, "subsdelayrange", m_videoSubsDelayRange, 10, 600);
+ XMLUtils::GetFloat(pElement, "audiodelayrange", m_videoAudioDelayRange, 10, 600);
+ XMLUtils::GetString(pElement, "defaultplayer", m_videoDefaultPlayer);
+ XMLUtils::GetBoolean(pElement, "fullscreenonmoviestart", m_fullScreenOnMovieStart);
+ // 101 on purpose - can be used to never automark as watched
+ XMLUtils::GetFloat(pElement, "playcountminimumpercent", m_videoPlayCountMinimumPercent, 0.0f, 101.0f);
+ XMLUtils::GetInt(pElement, "ignoresecondsatstart", m_videoIgnoreSecondsAtStart, 0, 900);
+ XMLUtils::GetFloat(pElement, "ignorepercentatend", m_videoIgnorePercentAtEnd, 0, 100.0f);
+
+ XMLUtils::GetBoolean(pElement, "usetimeseeking", m_videoUseTimeSeeking);
+ XMLUtils::GetInt(pElement, "timeseekforward", m_videoTimeSeekForward, 0, 6000);
+ XMLUtils::GetInt(pElement, "timeseekbackward", m_videoTimeSeekBackward, -6000, 0);
+ XMLUtils::GetInt(pElement, "timeseekforwardbig", m_videoTimeSeekForwardBig, 0, 6000);
+ XMLUtils::GetInt(pElement, "timeseekbackwardbig", m_videoTimeSeekBackwardBig, -6000, 0);
+
+ XMLUtils::GetInt(pElement, "percentseekforward", m_videoPercentSeekForward, 0, 100);
+ XMLUtils::GetInt(pElement, "percentseekbackward", m_videoPercentSeekBackward, -100, 0);
+ XMLUtils::GetInt(pElement, "percentseekforwardbig", m_videoPercentSeekForwardBig, 0, 100);
+ XMLUtils::GetInt(pElement, "percentseekbackwardbig", m_videoPercentSeekBackwardBig, -100, 0);
+
+ TiXmlElement* pVideoExcludes = pElement->FirstChildElement("excludefromlisting");
+ if (pVideoExcludes)
+ GetCustomRegexps(pVideoExcludes, m_videoExcludeFromListingRegExps);
+
+ pVideoExcludes = pElement->FirstChildElement("excludefromscan");
+ if (pVideoExcludes)
+ GetCustomRegexps(pVideoExcludes, m_moviesExcludeFromScanRegExps);
+
+ pVideoExcludes = pElement->FirstChildElement("excludetvshowsfromscan");
+ if (pVideoExcludes)
+ GetCustomRegexps(pVideoExcludes, m_tvshowExcludeFromScanRegExps);
+
+ pVideoExcludes = pElement->FirstChildElement("cleanstrings");
+ if (pVideoExcludes)
+ GetCustomRegexps(pVideoExcludes, m_videoCleanStringRegExps);
+
+ XMLUtils::GetString(pElement,"cleandatetime", m_videoCleanDateTimeRegExp);
+ XMLUtils::GetString(pElement,"ppffmpegpostprocessing",m_videoPPFFmpegPostProc);
+ XMLUtils::GetInt(pElement,"vdpauscaling",m_videoVDPAUScaling);
+ XMLUtils::GetFloat(pElement, "nonlinearstretchratio", m_videoNonLinStretchRatio, 0.01f, 1.0f);
+ XMLUtils::GetFloat(pElement,"autoscalemaxfps",m_videoAutoScaleMaxFps, 0.0f, 1000.0f);
+ XMLUtils::GetInt(pElement, "useocclusionquery", m_videoCaptureUseOcclusionQuery, -1, 1);
+ XMLUtils::GetBoolean(pElement,"vdpauInvTelecine",m_videoVDPAUtelecine);
+ XMLUtils::GetBoolean(pElement,"vdpauHDdeintSkipChroma",m_videoVDPAUdeintSkipChromaHD);
+
+ TiXmlElement* pAdjustRefreshrate = pElement->FirstChildElement("adjustrefreshrate");
+ if (pAdjustRefreshrate)
+ {
+ TiXmlElement* pRefreshOverride = pAdjustRefreshrate->FirstChildElement("override");
+ while (pRefreshOverride)
+ {
+ RefreshOverride override = {};
+
+ float fps;
+ if (XMLUtils::GetFloat(pRefreshOverride, "fps", fps))
+ {
+ override.fpsmin = fps - 0.01f;
+ override.fpsmax = fps + 0.01f;
+ }
+
+ float fpsmin, fpsmax;
+ if (XMLUtils::GetFloat(pRefreshOverride, "fpsmin", fpsmin) &&
+ XMLUtils::GetFloat(pRefreshOverride, "fpsmax", fpsmax))
+ {
+ override.fpsmin = fpsmin;
+ override.fpsmax = fpsmax;
+ }
+
+ float refresh;
+ if (XMLUtils::GetFloat(pRefreshOverride, "refresh", refresh))
+ {
+ override.refreshmin = refresh - 0.01f;
+ override.refreshmax = refresh + 0.01f;
+ }
+
+ float refreshmin, refreshmax;
+ if (XMLUtils::GetFloat(pRefreshOverride, "refreshmin", refreshmin) &&
+ XMLUtils::GetFloat(pRefreshOverride, "refreshmax", refreshmax))
+ {
+ override.refreshmin = refreshmin;
+ override.refreshmax = refreshmax;
+ }
+
+ bool fpsCorrect = (override.fpsmin > 0.0f && override.fpsmax >= override.fpsmin);
+ bool refreshCorrect = (override.refreshmin > 0.0f && override.refreshmax >= override.refreshmin);
+
+ if (fpsCorrect && refreshCorrect)
+ m_videoAdjustRefreshOverrides.push_back(override);
+ else
+ CLog::Log(LOGWARNING,
+ "Ignoring malformed refreshrate override, fpsmin:{:f} fpsmax:{:f} "
+ "refreshmin:{:f} refreshmax:{:f}",
+ override.fpsmin, override.fpsmax, override.refreshmin, override.refreshmax);
+
+ pRefreshOverride = pRefreshOverride->NextSiblingElement("override");
+ }
+
+ TiXmlElement* pRefreshFallback = pAdjustRefreshrate->FirstChildElement("fallback");
+ while (pRefreshFallback)
+ {
+ RefreshOverride fallback = {};
+ fallback.fallback = true;
+
+ float refresh;
+ if (XMLUtils::GetFloat(pRefreshFallback, "refresh", refresh))
+ {
+ fallback.refreshmin = refresh - 0.01f;
+ fallback.refreshmax = refresh + 0.01f;
+ }
+
+ float refreshmin, refreshmax;
+ if (XMLUtils::GetFloat(pRefreshFallback, "refreshmin", refreshmin) &&
+ XMLUtils::GetFloat(pRefreshFallback, "refreshmax", refreshmax))
+ {
+ fallback.refreshmin = refreshmin;
+ fallback.refreshmax = refreshmax;
+ }
+
+ if (fallback.refreshmin > 0.0f && fallback.refreshmax >= fallback.refreshmin)
+ m_videoAdjustRefreshOverrides.push_back(fallback);
+ else
+ CLog::Log(LOGWARNING,
+ "Ignoring malformed refreshrate fallback, fpsmin:{:f} fpsmax:{:f} "
+ "refreshmin:{:f} refreshmax:{:f}",
+ fallback.fpsmin, fallback.fpsmax, fallback.refreshmin, fallback.refreshmax);
+
+ pRefreshFallback = pRefreshFallback->NextSiblingElement("fallback");
+ }
+ }
+
+ m_DXVACheckCompatibilityPresent = XMLUtils::GetBoolean(pElement,"checkdxvacompatibility", m_DXVACheckCompatibility);
+
+ //0 = disable fps detect, 1 = only detect on timestamps with uniform spacing, 2 detect on all timestamps
+ XMLUtils::GetInt(pElement, "fpsdetect", m_videoFpsDetect, 0, 2);
+ XMLUtils::GetFloat(pElement, "maxtempo", m_maxTempo, 1.5, 2.1);
+ XMLUtils::GetBoolean(pElement, "preferstereostream", m_videoPreferStereoStream);
+
+ // Store global display latency settings
+ TiXmlElement* pVideoLatency = pElement->FirstChildElement("latency");
+ if (pVideoLatency)
+ {
+ float refresh, refreshmin, refreshmax, delay;
+ TiXmlElement* pRefreshVideoLatency = pVideoLatency->FirstChildElement("refresh");
+
+ while (pRefreshVideoLatency)
+ {
+ RefreshVideoLatency videolatency = {};
+
+ if (XMLUtils::GetFloat(pRefreshVideoLatency, "rate", refresh))
+ {
+ videolatency.refreshmin = refresh - 0.01f;
+ videolatency.refreshmax = refresh + 0.01f;
+ }
+ else if (XMLUtils::GetFloat(pRefreshVideoLatency, "min", refreshmin) &&
+ XMLUtils::GetFloat(pRefreshVideoLatency, "max", refreshmax))
+ {
+ videolatency.refreshmin = refreshmin;
+ videolatency.refreshmax = refreshmax;
+ }
+ if (XMLUtils::GetFloat(pRefreshVideoLatency, "delay", delay, -600.0f, 600.0f))
+ videolatency.delay = delay;
+
+ if (videolatency.refreshmin > 0.0f && videolatency.refreshmax >= videolatency.refreshmin)
+ m_videoRefreshLatency.push_back(videolatency);
+ else
+ CLog::Log(LOGWARNING,
+ "Ignoring malformed display latency <refresh> entry, min:{:f} max:{:f}",
+ videolatency.refreshmin, videolatency.refreshmax);
+
+ pRefreshVideoLatency = pRefreshVideoLatency->NextSiblingElement("refresh");
+ }
+
+ // Get default global display latency
+ XMLUtils::GetFloat(pVideoLatency, "delay", m_videoDefaultLatency, -600.0f, 600.0f);
+ }
+ }
+
+ pElement = pRootElement->FirstChildElement("musiclibrary");
+ if (pElement)
+ {
+ XMLUtils::GetInt(pElement, "recentlyaddeditems", m_iMusicLibraryRecentlyAddedItems, 1, INT_MAX);
+ XMLUtils::GetBoolean(pElement, "prioritiseapetags", m_prioritiseAPEv2tags);
+ XMLUtils::GetBoolean(pElement, "allitemsonbottom", m_bMusicLibraryAllItemsOnBottom);
+ XMLUtils::GetBoolean(pElement, "cleanonupdate", m_bMusicLibraryCleanOnUpdate);
+ XMLUtils::GetBoolean(pElement, "artistsortonupdate", m_bMusicLibraryArtistSortOnUpdate);
+ XMLUtils::GetString(pElement, "albumformat", m_strMusicLibraryAlbumFormat);
+ XMLUtils::GetString(pElement, "itemseparator", m_musicItemSeparator);
+ XMLUtils::GetInt(pElement, "dateadded", m_iMusicLibraryDateAdded);
+ XMLUtils::GetBoolean(pElement, "useisodates", m_bMusicLibraryUseISODates);
+ //Music artist name separators
+ TiXmlElement* separators = pElement->FirstChildElement("artistseparators");
+ if (separators)
+ {
+ m_musicArtistSeparators.clear();
+ TiXmlNode* separator = separators->FirstChild("separator");
+ while (separator)
+ {
+ if (separator->FirstChild())
+ m_musicArtistSeparators.push_back(separator->FirstChild()->ValueStr());
+ separator = separator->NextSibling("separator");
+ }
+ }
+
+ SetExtraArtwork(pElement->FirstChildElement("artistextraart"), m_musicArtistExtraArt);
+ SetExtraArtwork(pElement->FirstChildElement("albumextraart"), m_musicAlbumExtraArt);
+ }
+
+ pElement = pRootElement->FirstChildElement("videolibrary");
+ if (pElement)
+ {
+ XMLUtils::GetBoolean(pElement, "allitemsonbottom", m_bVideoLibraryAllItemsOnBottom);
+ XMLUtils::GetInt(pElement, "recentlyaddeditems", m_iVideoLibraryRecentlyAddedItems, 1, INT_MAX);
+ XMLUtils::GetBoolean(pElement, "cleanonupdate", m_bVideoLibraryCleanOnUpdate);
+ XMLUtils::GetBoolean(pElement, "usefasthash", m_bVideoLibraryUseFastHash);
+ XMLUtils::GetString(pElement, "itemseparator", m_videoItemSeparator);
+ XMLUtils::GetBoolean(pElement, "importwatchedstate", m_bVideoLibraryImportWatchedState);
+ XMLUtils::GetBoolean(pElement, "importresumepoint", m_bVideoLibraryImportResumePoint);
+ XMLUtils::GetInt(pElement, "dateadded", m_iVideoLibraryDateAdded);
+
+ SetExtraArtwork(pElement->FirstChildElement("episodeextraart"), m_videoEpisodeExtraArt);
+ SetExtraArtwork(pElement->FirstChildElement("tvshowextraart"), m_videoTvShowExtraArt);
+ SetExtraArtwork(pElement->FirstChildElement("tvseasonextraart"), m_videoTvSeasonExtraArt);
+ SetExtraArtwork(pElement->FirstChildElement("movieextraart"), m_videoMovieExtraArt);
+ SetExtraArtwork(pElement->FirstChildElement("moviesetextraart"), m_videoMovieSetExtraArt);
+ SetExtraArtwork(pElement->FirstChildElement("musicvideoextraart"), m_videoMusicVideoExtraArt);
+ }
+
+ pElement = pRootElement->FirstChildElement("videoscanner");
+ if (pElement)
+ {
+ XMLUtils::GetBoolean(pElement, "ignoreerrors", m_bVideoScannerIgnoreErrors);
+ }
+
+ // Backward-compatibility of ExternalPlayer config
+ pElement = pRootElement->FirstChildElement("externalplayer");
+ if (pElement)
+ {
+ CLog::Log(LOGWARNING, "External player configuration has been removed from advancedsettings.xml. It can now be configured in userdata/playercorefactory.xml");
+ }
+ pElement = pRootElement->FirstChildElement("slideshow");
+ if (pElement)
+ {
+ XMLUtils::GetFloat(pElement, "panamount", m_slideshowPanAmount, 0.0f, 20.0f);
+ XMLUtils::GetFloat(pElement, "zoomamount", m_slideshowZoomAmount, 0.0f, 20.0f);
+ XMLUtils::GetFloat(pElement, "blackbarcompensation", m_slideshowBlackBarCompensation, 0.0f, 50.0f);
+ }
+
+ pElement = pRootElement->FirstChildElement("network");
+ if (pElement)
+ {
+ XMLUtils::GetInt(pElement, "curlclienttimeout", m_curlconnecttimeout, 1, 1000);
+ XMLUtils::GetInt(pElement, "curllowspeedtime", m_curllowspeedtime, 1, 1000);
+ XMLUtils::GetInt(pElement, "curlretries", m_curlretries, 0, 10);
+ XMLUtils::GetInt(pElement, "curlkeepaliveinterval", m_curlKeepAliveInterval, 0, 300);
+ XMLUtils::GetBoolean(pElement, "disableipv6", m_curlDisableIPV6);
+ XMLUtils::GetBoolean(pElement, "disablehttp2", m_curlDisableHTTP2);
+ XMLUtils::GetString(pElement, "catrustfile", m_caTrustFile);
+ }
+
+ pElement = pRootElement->FirstChildElement("cache");
+ if (pElement)
+ {
+ XMLUtils::GetUInt(pElement, "memorysize", m_cacheMemSize);
+ XMLUtils::GetUInt(pElement, "buffermode", m_cacheBufferMode, 0, 4);
+ XMLUtils::GetUInt(pElement, "chunksize", m_cacheChunkSize, 256, 1024 * 1024);
+ XMLUtils::GetFloat(pElement, "readfactor", m_cacheReadFactor);
+ }
+
+ pElement = pRootElement->FirstChildElement("jsonrpc");
+ if (pElement)
+ {
+ XMLUtils::GetBoolean(pElement, "compactoutput", m_jsonOutputCompact);
+ XMLUtils::GetUInt(pElement, "tcpport", m_jsonTcpPort);
+ }
+
+ pElement = pRootElement->FirstChildElement("samba");
+ if (pElement)
+ {
+ XMLUtils::GetString(pElement, "doscodepage", m_sambadoscodepage);
+ XMLUtils::GetInt(pElement, "clienttimeout", m_sambaclienttimeout, 5, 100);
+ XMLUtils::GetBoolean(pElement, "statfiles", m_sambastatfiles);
+ }
+
+ pElement = pRootElement->FirstChildElement("httpdirectory");
+ if (pElement)
+ XMLUtils::GetBoolean(pElement, "statfilesize", m_bHTTPDirectoryStatFilesize);
+
+ pElement = pRootElement->FirstChildElement("ftp");
+ if (pElement)
+ {
+ XMLUtils::GetBoolean(pElement, "remotethumbs", m_bFTPThumbs);
+ }
+
+ pElement = pRootElement->FirstChildElement("loglevel");
+ if (pElement)
+ { // read the loglevel setting, so set the setting advanced to hide it in GUI
+ // as altering it will do nothing - we don't write to advancedsettings.xml
+ XMLUtils::GetInt(pRootElement, "loglevel", m_logLevelHint, LOG_LEVEL_NONE, LOG_LEVEL_MAX);
+ const char* hide = pElement->Attribute("hide");
+ if (hide == NULL || StringUtils::CompareNoCase("false", hide, 5) != 0)
+ {
+ SettingPtr setting = CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(CSettings::SETTING_DEBUG_SHOWLOGINFO);
+ if (setting != NULL)
+ setting->SetVisible(false);
+ }
+ m_logLevel = std::max(m_logLevel, m_logLevelHint);
+ CServiceBroker::GetLogging().SetLogLevel(m_logLevel);
+ }
+
+ XMLUtils::GetString(pRootElement, "cddbaddress", m_cddbAddress);
+ XMLUtils::GetBoolean(pRootElement, "addsourceontop", m_addSourceOnTop);
+
+ //airtunes + airplay
+ XMLUtils::GetInt(pRootElement, "airtunesport", m_airTunesPort);
+ XMLUtils::GetInt(pRootElement, "airplayport", m_airPlayPort);
+
+ XMLUtils::GetBoolean(pRootElement, "handlemounting", m_handleMounting);
+ XMLUtils::GetBoolean(pRootElement, "automountopticalmedia", m_autoMountOpticalMedia);
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+ XMLUtils::GetBoolean(pRootElement, "minimizetotray", m_minimizeToTray);
+#endif
+#if defined(TARGET_DARWIN_OSX) || defined(TARGET_WINDOWS)
+ XMLUtils::GetBoolean(pRootElement, "fullscreen", m_startFullScreen);
+#endif
+ XMLUtils::GetBoolean(pRootElement, "splash", m_splashImage);
+ XMLUtils::GetBoolean(pRootElement, "showexitbutton", m_showExitButton);
+ XMLUtils::GetBoolean(pRootElement, "canwindowed", m_canWindowed);
+
+ XMLUtils::GetInt(pRootElement, "songinfoduration", m_songInfoDuration, 0, INT_MAX);
+ XMLUtils::GetInt(pRootElement, "playlistretries", m_playlistRetries, -1, 5000);
+ XMLUtils::GetInt(pRootElement, "playlisttimeout", m_playlistTimeout, 0, 5000);
+
+ XMLUtils::GetBoolean(pRootElement,"glrectanglehack", m_GLRectangleHack);
+ XMLUtils::GetInt(pRootElement,"skiploopfilter", m_iSkipLoopFilter, -16, 48);
+
+ XMLUtils::GetBoolean(pRootElement,"virtualshares", m_bVirtualShares);
+ XMLUtils::GetUInt(pRootElement, "packagefoldersize", m_addonPackageFolderSize);
+
+ // EPG
+ pElement = pRootElement->FirstChildElement("epg");
+ if (pElement)
+ {
+ XMLUtils::GetInt(pElement, "updatecheckinterval", m_iEpgUpdateCheckInterval);
+ XMLUtils::GetInt(pElement, "cleanupinterval", m_iEpgCleanupInterval);
+ XMLUtils::GetInt(pElement, "activetagcheckinterval", m_iEpgActiveTagCheckInterval);
+ XMLUtils::GetInt(pElement, "retryinterruptedupdateinterval", m_iEpgRetryInterruptedUpdateInterval);
+ XMLUtils::GetInt(pElement, "updateemptytagsinterval", m_iEpgUpdateEmptyTagsInterval);
+ XMLUtils::GetBoolean(pElement, "displayupdatepopup", m_bEpgDisplayUpdatePopup);
+ XMLUtils::GetBoolean(pElement, "displayincrementalupdatepopup", m_bEpgDisplayIncrementalUpdatePopup);
+ }
+
+ // EDL commercial break handling
+ pElement = pRootElement->FirstChildElement("edl");
+ if (pElement)
+ {
+ XMLUtils::GetBoolean(pElement, "mergeshortcommbreaks", m_bEdlMergeShortCommBreaks);
+ XMLUtils::GetBoolean(pElement, "displaycommbreaknotifications",
+ m_EdlDisplayCommbreakNotifications);
+ XMLUtils::GetInt(pElement, "maxcommbreaklength", m_iEdlMaxCommBreakLength, 0, 10 * 60); // Between 0 and 10 minutes
+ XMLUtils::GetInt(pElement, "mincommbreaklength", m_iEdlMinCommBreakLength, 0, 5 * 60); // Between 0 and 5 minutes
+ XMLUtils::GetInt(pElement, "maxcommbreakgap", m_iEdlMaxCommBreakGap, 0, 5 * 60); // Between 0 and 5 minutes.
+ XMLUtils::GetInt(pElement, "maxstartgap", m_iEdlMaxStartGap, 0, 10 * 60); // Between 0 and 10 minutes
+ XMLUtils::GetInt(pElement, "commbreakautowait", m_iEdlCommBreakAutowait, -60, 60); // Between -60 and 60 seconds
+ XMLUtils::GetInt(pElement, "commbreakautowind", m_iEdlCommBreakAutowind, -60, 60); // Between -60 and 60 seconds
+ }
+
+ // picture exclude regexps
+ TiXmlElement* pPictureExcludes = pRootElement->FirstChildElement("pictureexcludes");
+ if (pPictureExcludes)
+ GetCustomRegexps(pPictureExcludes, m_pictureExcludeFromListingRegExps);
+
+ // picture extensions
+ TiXmlElement* pExts = pRootElement->FirstChildElement("pictureextensions");
+ if (pExts)
+ GetCustomExtensions(pExts, m_pictureExtensions);
+
+ // music extensions
+ pExts = pRootElement->FirstChildElement("musicextensions");
+ if (pExts)
+ GetCustomExtensions(pExts, m_musicExtensions);
+
+ // video extensions
+ pExts = pRootElement->FirstChildElement("videoextensions");
+ if (pExts)
+ GetCustomExtensions(pExts, m_videoExtensions);
+
+ // stub extensions
+ pExts = pRootElement->FirstChildElement("discstubextensions");
+ if (pExts)
+ GetCustomExtensions(pExts, m_discStubExtensions);
+
+ m_vecTokens.clear();
+ CLangInfo::LoadTokens(pRootElement->FirstChild("sorttokens"),m_vecTokens);
+
+ //! @todo Should cache path be given in terms of our predefined paths??
+ //! Are we even going to have predefined paths??
+ std::string tmp;
+ if (XMLUtils::GetPath(pRootElement, "cachepath", tmp))
+ m_cachePath = tmp;
+ URIUtils::AddSlashAtEnd(m_cachePath);
+
+ g_LangCodeExpander.LoadUserCodes(pRootElement->FirstChildElement("languagecodes"));
+
+ // trailer matching regexps
+ TiXmlElement* pTrailerMatching = pRootElement->FirstChildElement("trailermatching");
+ if (pTrailerMatching)
+ GetCustomRegexps(pTrailerMatching, m_trailerMatchRegExps);
+
+ //everything that's a trailer is not a movie
+ m_moviesExcludeFromScanRegExps.insert(m_moviesExcludeFromScanRegExps.end(),
+ m_trailerMatchRegExps.begin(),
+ m_trailerMatchRegExps.end());
+
+ // video stacking regexps
+ TiXmlElement* pVideoStacking = pRootElement->FirstChildElement("moviestacking");
+ if (pVideoStacking)
+ GetCustomRegexps(pVideoStacking, m_videoStackRegExps);
+
+ // folder stacking regexps
+ TiXmlElement* pFolderStacking = pRootElement->FirstChildElement("folderstacking");
+ if (pFolderStacking)
+ GetCustomRegexps(pFolderStacking, m_folderStackRegExps);
+
+ //tv stacking regexps
+ TiXmlElement* pTVStacking = pRootElement->FirstChildElement("tvshowmatching");
+ if (pTVStacking)
+ GetCustomTVRegexps(pTVStacking, m_tvshowEnumRegExps);
+
+ //tv multipart enumeration regexp
+ XMLUtils::GetString(pRootElement, "tvmultipartmatching", m_tvshowMultiPartEnumRegExp);
+
+ // path substitutions
+ TiXmlElement* pPathSubstitution = pRootElement->FirstChildElement("pathsubstitution");
+ if (pPathSubstitution)
+ {
+ m_pathSubstitutions.clear();
+ CLog::Log(LOGDEBUG,"Configuring path substitutions");
+ TiXmlNode* pSubstitute = pPathSubstitution->FirstChildElement("substitute");
+ while (pSubstitute)
+ {
+ std::string strFrom, strTo;
+ TiXmlNode* pFrom = pSubstitute->FirstChild("from");
+ if (pFrom && !pFrom->NoChildren())
+ strFrom = CSpecialProtocol::TranslatePath(pFrom->FirstChild()->Value()).c_str();
+ TiXmlNode* pTo = pSubstitute->FirstChild("to");
+ if (pTo && !pTo->NoChildren())
+ strTo = pTo->FirstChild()->Value();
+
+ if (!strFrom.empty() && !strTo.empty())
+ {
+ CLog::Log(LOGDEBUG," Registering substitution pair:");
+ CLog::Log(LOGDEBUG, " From: [{}]", CURL::GetRedacted(strFrom));
+ CLog::Log(LOGDEBUG, " To: [{}]", CURL::GetRedacted(strTo));
+ m_pathSubstitutions.push_back(std::make_pair(strFrom,strTo));
+ }
+ else
+ {
+ // error message about missing tag
+ if (strFrom.empty())
+ CLog::Log(LOGERROR," Missing <from> tag");
+ else
+ CLog::Log(LOGERROR," Missing <to> tag");
+ }
+
+ // get next one
+ pSubstitute = pSubstitute->NextSiblingElement("substitute");
+ }
+ }
+
+ XMLUtils::GetInt(pRootElement, "remotedelay", m_remoteDelay, 0, 20);
+ XMLUtils::GetBoolean(pRootElement, "scanirserver", m_bScanIRServer);
+
+ XMLUtils::GetUInt(pRootElement, "fanartres", m_fanartRes, 0, 9999);
+ XMLUtils::GetUInt(pRootElement, "imageres", m_imageRes, 0, 9999);
+ if (XMLUtils::GetString(pRootElement, "imagescalingalgorithm", tmp))
+ m_imageScalingAlgorithm = CPictureScalingAlgorithm::FromString(tmp);
+ XMLUtils::GetUInt(pRootElement, "imagequalityjpeg", m_imageQualityJpeg, 0, 21);
+ XMLUtils::GetBoolean(pRootElement, "playlistasfolders", m_playlistAsFolders);
+ XMLUtils::GetBoolean(pRootElement, "uselocalecollation", m_useLocaleCollation);
+ XMLUtils::GetBoolean(pRootElement, "detectasudf", m_detectAsUdf);
+
+ // music thumbs
+ TiXmlElement* pThumbs = pRootElement->FirstChildElement("musicthumbs");
+ if (pThumbs)
+ GetCustomExtensions(pThumbs,m_musicThumbs);
+
+ // show art for shoutcast v2 streams (set to false for devices with limited storage)
+ XMLUtils::GetBoolean(pRootElement, "shoutcastart", m_bShoutcastArt);
+ // music filename->tag filters
+ TiXmlElement* filters = pRootElement->FirstChildElement("musicfilenamefilters");
+ if (filters)
+ {
+ TiXmlNode* filter = filters->FirstChild("filter");
+ while (filter)
+ {
+ if (filter->FirstChild())
+ m_musicTagsFromFileFilters.push_back(filter->FirstChild()->ValueStr());
+ filter = filter->NextSibling("filter");
+ }
+ }
+
+ TiXmlElement* pHostEntries = pRootElement->FirstChildElement("hosts");
+ if (pHostEntries)
+ {
+ TiXmlElement* element = pHostEntries->FirstChildElement("entry");
+ while(element)
+ {
+ if(!element->NoChildren())
+ {
+ std::string name = XMLUtils::GetAttribute(element, "name");
+ std::string value = element->FirstChild()->ValueStr();
+ if (!name.empty())
+ CDNSNameCache::Add(name, value);
+ }
+ element = element->NextSiblingElement("entry");
+ }
+ }
+
+ XMLUtils::GetString(pRootElement, "cputempcommand", m_cpuTempCmd);
+ XMLUtils::GetString(pRootElement, "gputempcommand", m_gpuTempCmd);
+
+ XMLUtils::GetBoolean(pRootElement, "alwaysontop", m_alwaysOnTop);
+
+ TiXmlElement *pPVR = pRootElement->FirstChildElement("pvr");
+ if (pPVR)
+ {
+ XMLUtils::GetInt(pPVR, "timecorrection", m_iPVRTimeCorrection, 0, 1440);
+ XMLUtils::GetInt(pPVR, "infotoggleinterval", m_iPVRInfoToggleInterval, 0, 30000);
+ XMLUtils::GetBoolean(pPVR, "channeliconsautoscan", m_bPVRChannelIconsAutoScan);
+ XMLUtils::GetBoolean(pPVR, "autoscaniconsuserset", m_bPVRAutoScanIconsUserSet);
+ XMLUtils::GetInt(pPVR, "numericchannelswitchtimeout", m_iPVRNumericChannelSwitchTimeout, 50, 60000);
+ XMLUtils::GetInt(pPVR, "timeshiftthreshold", m_iPVRTimeshiftThreshold, 0, 60);
+ XMLUtils::GetBoolean(pPVR, "timeshiftsimpleosd", m_bPVRTimeshiftSimpleOSD);
+ TiXmlElement* pSortDecription = pPVR->FirstChildElement("pvrrecordings");
+ if (pSortDecription)
+ {
+ const char* XML_SORTMETHOD = "sortmethod";
+ const char* XML_SORTORDER = "sortorder";
+ int sortMethod;
+ // ignore SortByTime for duration defaults
+ if (XMLUtils::GetInt(pSortDecription, XML_SORTMETHOD, sortMethod, SortByLabel, SortByFile))
+ {
+ int sortOrder;
+ if (XMLUtils::GetInt(pSortDecription, XML_SORTORDER, sortOrder, SortOrderAscending,
+ SortOrderDescending))
+ {
+ m_PVRDefaultSortOrder.sortBy = (SortBy)sortMethod;
+ m_PVRDefaultSortOrder.sortOrder = (SortOrder)sortOrder;
+ }
+ }
+ }
+ }
+
+ TiXmlElement* pDatabase = pRootElement->FirstChildElement("videodatabase");
+ if (pDatabase)
+ {
+ CLog::Log(LOGWARNING, "VIDEO database configuration is experimental.");
+ XMLUtils::GetString(pDatabase, "type", m_databaseVideo.type);
+ XMLUtils::GetString(pDatabase, "host", m_databaseVideo.host);
+ XMLUtils::GetString(pDatabase, "port", m_databaseVideo.port);
+ XMLUtils::GetString(pDatabase, "user", m_databaseVideo.user);
+ XMLUtils::GetString(pDatabase, "pass", m_databaseVideo.pass);
+ XMLUtils::GetString(pDatabase, "name", m_databaseVideo.name);
+ XMLUtils::GetString(pDatabase, "key", m_databaseVideo.key);
+ XMLUtils::GetString(pDatabase, "cert", m_databaseVideo.cert);
+ XMLUtils::GetString(pDatabase, "ca", m_databaseVideo.ca);
+ XMLUtils::GetString(pDatabase, "capath", m_databaseVideo.capath);
+ XMLUtils::GetString(pDatabase, "ciphers", m_databaseVideo.ciphers);
+ XMLUtils::GetBoolean(pDatabase, "compression", m_databaseVideo.compression);
+ }
+
+ pDatabase = pRootElement->FirstChildElement("musicdatabase");
+ if (pDatabase)
+ {
+ XMLUtils::GetString(pDatabase, "type", m_databaseMusic.type);
+ XMLUtils::GetString(pDatabase, "host", m_databaseMusic.host);
+ XMLUtils::GetString(pDatabase, "port", m_databaseMusic.port);
+ XMLUtils::GetString(pDatabase, "user", m_databaseMusic.user);
+ XMLUtils::GetString(pDatabase, "pass", m_databaseMusic.pass);
+ XMLUtils::GetString(pDatabase, "name", m_databaseMusic.name);
+ XMLUtils::GetString(pDatabase, "key", m_databaseMusic.key);
+ XMLUtils::GetString(pDatabase, "cert", m_databaseMusic.cert);
+ XMLUtils::GetString(pDatabase, "ca", m_databaseMusic.ca);
+ XMLUtils::GetString(pDatabase, "capath", m_databaseMusic.capath);
+ XMLUtils::GetString(pDatabase, "ciphers", m_databaseMusic.ciphers);
+ XMLUtils::GetBoolean(pDatabase, "compression", m_databaseMusic.compression);
+ }
+
+ pDatabase = pRootElement->FirstChildElement("tvdatabase");
+ if (pDatabase)
+ {
+ XMLUtils::GetString(pDatabase, "type", m_databaseTV.type);
+ XMLUtils::GetString(pDatabase, "host", m_databaseTV.host);
+ XMLUtils::GetString(pDatabase, "port", m_databaseTV.port);
+ XMLUtils::GetString(pDatabase, "user", m_databaseTV.user);
+ XMLUtils::GetString(pDatabase, "pass", m_databaseTV.pass);
+ XMLUtils::GetString(pDatabase, "name", m_databaseTV.name);
+ XMLUtils::GetString(pDatabase, "key", m_databaseTV.key);
+ XMLUtils::GetString(pDatabase, "cert", m_databaseTV.cert);
+ XMLUtils::GetString(pDatabase, "ca", m_databaseTV.ca);
+ XMLUtils::GetString(pDatabase, "capath", m_databaseTV.capath);
+ XMLUtils::GetString(pDatabase, "ciphers", m_databaseTV.ciphers);
+ XMLUtils::GetBoolean(pDatabase, "compression", m_databaseTV.compression);
+ }
+
+ pDatabase = pRootElement->FirstChildElement("epgdatabase");
+ if (pDatabase)
+ {
+ XMLUtils::GetString(pDatabase, "type", m_databaseEpg.type);
+ XMLUtils::GetString(pDatabase, "host", m_databaseEpg.host);
+ XMLUtils::GetString(pDatabase, "port", m_databaseEpg.port);
+ XMLUtils::GetString(pDatabase, "user", m_databaseEpg.user);
+ XMLUtils::GetString(pDatabase, "pass", m_databaseEpg.pass);
+ XMLUtils::GetString(pDatabase, "name", m_databaseEpg.name);
+ XMLUtils::GetString(pDatabase, "key", m_databaseEpg.key);
+ XMLUtils::GetString(pDatabase, "cert", m_databaseEpg.cert);
+ XMLUtils::GetString(pDatabase, "ca", m_databaseEpg.ca);
+ XMLUtils::GetString(pDatabase, "capath", m_databaseEpg.capath);
+ XMLUtils::GetString(pDatabase, "ciphers", m_databaseEpg.ciphers);
+ XMLUtils::GetBoolean(pDatabase, "compression", m_databaseEpg.compression);
+ }
+
+ pElement = pRootElement->FirstChildElement("enablemultimediakeys");
+ if (pElement)
+ {
+ XMLUtils::GetBoolean(pRootElement, "enablemultimediakeys", m_enableMultimediaKeys);
+ }
+
+ pElement = pRootElement->FirstChildElement("gui");
+ if (pElement)
+ {
+ XMLUtils::GetBoolean(pElement, "visualizedirtyregions", m_guiVisualizeDirtyRegions);
+ XMLUtils::GetInt(pElement, "algorithmdirtyregions", m_guiAlgorithmDirtyRegions);
+ XMLUtils::GetBoolean(pElement, "smartredraw", m_guiSmartRedraw);
+ }
+
+ std::string seekSteps;
+ XMLUtils::GetString(pRootElement, "seeksteps", seekSteps);
+ if (!seekSteps.empty())
+ {
+ m_seekSteps.clear();
+ std::vector<std::string> steps = StringUtils::Split(seekSteps, ',');
+ for(std::vector<std::string>::iterator it = steps.begin(); it != steps.end(); ++it)
+ m_seekSteps.push_back(atoi((*it).c_str()));
+ }
+
+ XMLUtils::GetBoolean(pRootElement, "opengldebugging", m_openGlDebugging);
+
+ // load in the settings overrides
+ CServiceBroker::GetSettingsComponent()->GetSettings()->LoadHidden(pRootElement);
+
+ // Migration of old style art options from advanced setting to GUI setting
+ MigrateOldArtSettings();
+}
+
+void CAdvancedSettings::Clear()
+{
+ m_videoCleanStringRegExps.clear();
+ m_moviesExcludeFromScanRegExps.clear();
+ m_tvshowExcludeFromScanRegExps.clear();
+ m_videoExcludeFromListingRegExps.clear();
+ m_videoStackRegExps.clear();
+ m_folderStackRegExps.clear();
+ m_allExcludeFromScanRegExps.clear();
+ m_audioExcludeFromScanRegExps.clear();
+ m_audioExcludeFromListingRegExps.clear();
+ m_pictureExcludeFromListingRegExps.clear();
+
+ m_pictureExtensions.clear();
+ m_musicExtensions.clear();
+ m_videoExtensions.clear();
+ m_discStubExtensions.clear();
+
+ m_userAgent.clear();
+}
+
+void CAdvancedSettings::GetCustomTVRegexps(TiXmlElement *pRootElement, SETTINGS_TVSHOWLIST& settings)
+{
+ TiXmlElement *pElement = pRootElement;
+ while (pElement)
+ {
+ int iAction = 0; // overwrite
+ // for backward compatibility
+ const char* szAppend = pElement->Attribute("append");
+ if ((szAppend && StringUtils::CompareNoCase(szAppend, "yes") == 0))
+ iAction = 1;
+ // action takes precedence if both attributes exist
+ const char* szAction = pElement->Attribute("action");
+ if (szAction)
+ {
+ iAction = 0; // overwrite
+ if (StringUtils::CompareNoCase(szAction, "append") == 0)
+ iAction = 1; // append
+ else if (StringUtils::CompareNoCase(szAction, "prepend") == 0)
+ iAction = 2; // prepend
+ }
+ if (iAction == 0)
+ settings.clear();
+ TiXmlNode* pRegExp = pElement->FirstChild("regexp");
+ int i = 0;
+ while (pRegExp)
+ {
+ if (pRegExp->FirstChild())
+ {
+ bool bByDate = false;
+ bool byTitle = false;
+ int iDefaultSeason = 1;
+ if (pRegExp->ToElement())
+ {
+ std::string byDate = XMLUtils::GetAttribute(pRegExp->ToElement(), "bydate");
+ if (byDate == "true")
+ {
+ bByDate = true;
+ }
+ std::string byTitleAttr = XMLUtils::GetAttribute(pRegExp->ToElement(), "bytitle");
+ byTitle = (byTitleAttr == "true");
+ std::string defaultSeason = XMLUtils::GetAttribute(pRegExp->ToElement(), "defaultseason");
+ if(!defaultSeason.empty())
+ {
+ iDefaultSeason = atoi(defaultSeason.c_str());
+ }
+ }
+ std::string regExp = pRegExp->FirstChild()->Value();
+ if (iAction == 2)
+ {
+ settings.insert(settings.begin() + i++, 1,
+ TVShowRegexp(bByDate, regExp, iDefaultSeason, byTitle));
+ }
+ else
+ {
+ settings.push_back(TVShowRegexp(bByDate, regExp, iDefaultSeason, byTitle));
+ }
+ }
+ pRegExp = pRegExp->NextSibling("regexp");
+ }
+
+ pElement = pElement->NextSiblingElement(pRootElement->Value());
+ }
+}
+
+void CAdvancedSettings::GetCustomRegexps(TiXmlElement *pRootElement, std::vector<std::string>& settings)
+{
+ TiXmlElement *pElement = pRootElement;
+ while (pElement)
+ {
+ int iAction = 0; // overwrite
+ // for backward compatibility
+ const char* szAppend = pElement->Attribute("append");
+ if ((szAppend && StringUtils::CompareNoCase(szAppend, "yes") == 0))
+ iAction = 1;
+ // action takes precedence if both attributes exist
+ const char* szAction = pElement->Attribute("action");
+ if (szAction)
+ {
+ iAction = 0; // overwrite
+ if (StringUtils::CompareNoCase(szAction, "append") == 0)
+ iAction = 1; // append
+ else if (StringUtils::CompareNoCase(szAction, "prepend") == 0)
+ iAction = 2; // prepend
+ }
+ if (iAction == 0)
+ settings.clear();
+ TiXmlNode* pRegExp = pElement->FirstChild("regexp");
+ int i = 0;
+ while (pRegExp)
+ {
+ if (pRegExp->FirstChild())
+ {
+ std::string regExp = pRegExp->FirstChild()->Value();
+ if (iAction == 2)
+ settings.insert(settings.begin() + i++, 1, regExp);
+ else
+ settings.push_back(regExp);
+ }
+ pRegExp = pRegExp->NextSibling("regexp");
+ }
+
+ pElement = pElement->NextSiblingElement(pRootElement->Value());
+ }
+}
+
+void CAdvancedSettings::GetCustomExtensions(TiXmlElement *pRootElement, std::string& extensions)
+{
+ std::string extraExtensions;
+ if (XMLUtils::GetString(pRootElement, "add", extraExtensions) && !extraExtensions.empty())
+ extensions += "|" + extraExtensions;
+ if (XMLUtils::GetString(pRootElement, "remove", extraExtensions) && !extraExtensions.empty())
+ {
+ std::vector<std::string> exts = StringUtils::Split(extraExtensions, '|');
+ for (std::vector<std::string>::const_iterator i = exts.begin(); i != exts.end(); ++i)
+ {
+ size_t iPos = extensions.find(*i);
+ if (iPos != std::string::npos)
+ extensions.erase(iPos,i->size()+1);
+ }
+ }
+}
+
+void CAdvancedSettings::AddSettingsFile(const std::string &filename)
+{
+ m_settingsFiles.push_back(filename);
+}
+
+float CAdvancedSettings::GetLatencyTweak(float refreshrate)
+{
+ float delay = m_videoDefaultLatency;
+ for (int i = 0; i < (int) m_videoRefreshLatency.size(); i++)
+ {
+ RefreshVideoLatency& videolatency = m_videoRefreshLatency[i];
+ if (refreshrate >= videolatency.refreshmin && refreshrate <= videolatency.refreshmax)
+ delay = videolatency.delay;
+ }
+
+ return delay; // in milliseconds
+}
+
+void CAdvancedSettings::SetDebugMode(bool debug)
+{
+ if (debug)
+ {
+ int level = std::max(m_logLevelHint, LOG_LEVEL_DEBUG_FREEMEM);
+ m_logLevel = level;
+ CServiceBroker::GetLogging().SetLogLevel(level);
+ CLog::Log(LOGINFO, "Enabled debug logging due to GUI setting. Level {}.", level);
+ }
+ else
+ {
+ int level = std::min(m_logLevelHint, LOG_LEVEL_DEBUG/*LOG_LEVEL_NORMAL*/);
+ CLog::Log(LOGINFO, "Disabled debug logging due to GUI setting. Level {}.", level);
+ m_logLevel = level;
+ CServiceBroker::GetLogging().SetLogLevel(level);
+ }
+}
+
+void CAdvancedSettings::SetExtraArtwork(const TiXmlElement* arttypes, std::vector<std::string>& artworkMap)
+{
+ if (!arttypes)
+ return;
+ artworkMap.clear();
+ const TiXmlNode* arttype = arttypes->FirstChild("arttype");
+ while (arttype)
+ {
+ if (arttype->FirstChild())
+ artworkMap.push_back(arttype->FirstChild()->ValueStr());
+ arttype = arttype->NextSibling("arttype");
+ }
+}
+
+void ConvertToWhitelist(const std::vector<std::string>& oldlist, std::vector<CVariant>& whitelist)
+{
+ for (auto& it : oldlist)
+ {
+ size_t last_index = it.find_last_not_of("0123456789");
+ std::string strFamilyType = it.substr(0, last_index + 1); // "fanart" of "fanart16"
+ if (std::find(whitelist.begin(), whitelist.end(), strFamilyType) == whitelist.end())
+ whitelist.emplace_back(strFamilyType);
+ }
+}
+
+void CAdvancedSettings::MigrateOldArtSettings()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (!settings->GetBool(CSettings::SETTING_MUSICLIBRARY_ARTSETTINGS_UPDATED))
+ {
+ CLog::Log(LOGINFO, "Migrating old music library artwork settings to new GUI settings");
+ // Convert numeric art type variants into simple art type family entry
+ // e.g. {"banner", "fanart1", "fanart2", "fanart3"... } into { "banner", "fanart"}
+ if (!m_musicArtistExtraArt.empty())
+ {
+ std::vector<CVariant> whitelist;
+ ConvertToWhitelist(m_musicArtistExtraArt, whitelist);
+ settings->SetList(CSettings::SETTING_MUSICLIBRARY_ARTISTART_WHITELIST, whitelist);
+ }
+ if (!m_musicAlbumExtraArt.empty())
+ {
+ std::vector<CVariant> whitelist;
+ ConvertToWhitelist(m_musicAlbumExtraArt, whitelist);
+ settings->SetList(CSettings::SETTING_MUSICLIBRARY_ALBUMART_WHITELIST, whitelist);
+ }
+
+ // Convert value like "folder.jpg|Folder.jpg|folder.JPG|Folder.JPG|cover.jpg|Cover.jpg|
+ // cover.jpeg|thumb.jpg|Thumb.jpg|thumb.JPG|Thumb.JPG" into case-insensitive unique elements
+ // e.g. {"folder.jpg", "cover.jpg", "cover.jpeg", "thumb.jpg"}
+ if (!m_musicThumbs.empty())
+ {
+ std::vector<std::string> thumbs1 = StringUtils::Split(m_musicThumbs, "|");
+ std::vector<std::string> thumbs2;
+ for (auto& it : thumbs1)
+ {
+ StringUtils::ToLower(it);
+ if (std::find(thumbs2.begin(), thumbs2.end(), it) == thumbs2.end())
+ thumbs2.emplace_back(it);
+ }
+ std::vector<CVariant> thumbs;
+ thumbs.reserve(thumbs2.size());
+ for (const auto& it : thumbs2)
+ thumbs.emplace_back(it);
+ settings->SetList(CSettings::SETTING_MUSICLIBRARY_MUSICTHUMBS, thumbs);
+ }
+
+ // Whitelists configured, set artwork level to custom
+ if (!m_musicAlbumExtraArt.empty() || !m_musicArtistExtraArt.empty())
+ settings->SetInt(CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL, 2);
+
+ // Flag migration of settings so not done again
+ settings->SetBool(CSettings::SETTING_MUSICLIBRARY_ARTSETTINGS_UPDATED, true);
+ }
+
+ if (!settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_ARTSETTINGS_UPDATED))
+ {
+ CLog::Log(LOGINFO, "Migrating old video library artwork settings to new GUI settings");
+ // Convert numeric art type variants into simple art type family entry
+ // e.g. {"banner", "fanart1", "fanart2", "fanart3"... } into { "banner", "fanart"}
+ if (!m_videoEpisodeExtraArt.empty())
+ {
+ std::vector<CVariant> whitelist;
+ ConvertToWhitelist(m_videoEpisodeExtraArt, whitelist);
+ settings->SetList(CSettings::SETTING_VIDEOLIBRARY_EPISODEART_WHITELIST, whitelist);
+ }
+ if (!m_videoTvShowExtraArt.empty())
+ {
+ std::vector<CVariant> whitelist;
+ ConvertToWhitelist(m_videoTvShowExtraArt, whitelist);
+ settings->SetList(CSettings::SETTING_VIDEOLIBRARY_TVSHOWART_WHITELIST, whitelist);
+ }
+ if (!m_videoMovieExtraArt.empty())
+ {
+ std::vector<CVariant> whitelist;
+ ConvertToWhitelist(m_videoMovieExtraArt, whitelist);
+ settings->SetList(CSettings::SETTING_VIDEOLIBRARY_MOVIEART_WHITELIST, whitelist);
+ }
+ if (!m_videoMusicVideoExtraArt.empty())
+ {
+ std::vector<CVariant> whitelist;
+ ConvertToWhitelist(m_videoMusicVideoExtraArt, whitelist);
+ settings->SetList(CSettings::SETTING_VIDEOLIBRARY_MUSICVIDEOART_WHITELIST, whitelist);
+ }
+
+ // Whitelists configured, set artwork level to custom
+ if (!m_videoEpisodeExtraArt.empty() || !m_videoTvShowExtraArt.empty()
+ || !m_videoMovieExtraArt.empty() || !m_videoMusicVideoExtraArt.empty())
+ settings->SetInt(CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL,
+ CSettings::MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM);
+
+ // Flag migration of settings so not done again
+ settings->SetBool(CSettings::SETTING_VIDEOLIBRARY_ARTSETTINGS_UPDATED, true);
+ }
+}
diff --git a/xbmc/settings/AdvancedSettings.h b/xbmc/settings/AdvancedSettings.h
new file mode 100644
index 0000000..4ba4e7e
--- /dev/null
+++ b/xbmc/settings/AdvancedSettings.h
@@ -0,0 +1,391 @@
+/*
+ * 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 "pictures/PictureScalingAlgorithm.h"
+#include "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+#include "utils/SortUtils.h"
+
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#define CACHE_BUFFER_MODE_INTERNET 0
+#define CACHE_BUFFER_MODE_ALL 1
+#define CACHE_BUFFER_MODE_TRUE_INTERNET 2
+#define CACHE_BUFFER_MODE_NONE 3
+#define CACHE_BUFFER_MODE_NETWORK 4
+
+class CProfileManager;
+class CSettingsManager;
+class CVariant;
+struct IntegerSettingOption;
+
+class TiXmlElement;
+namespace ADDON
+{
+ class IAddon;
+}
+
+class DatabaseSettings
+{
+public:
+ DatabaseSettings() { Reset(); }
+ void Reset()
+ {
+ type.clear();
+ host.clear();
+ port.clear();
+ user.clear();
+ pass.clear();
+ name.clear();
+ key.clear();
+ cert.clear();
+ ca.clear();
+ capath.clear();
+ ciphers.clear();
+ compression = false;
+ };
+ std::string type;
+ std::string host;
+ std::string port;
+ std::string user;
+ std::string pass;
+ std::string name;
+ std::string key;
+ std::string cert;
+ std::string ca;
+ std::string capath;
+ std::string ciphers;
+ bool compression;
+};
+
+struct TVShowRegexp
+{
+ bool byDate;
+ bool byTitle;
+ std::string regexp;
+ int defaultSeason;
+ TVShowRegexp(bool d, const std::string& r, int s = 1, bool t = false) : regexp(r)
+ {
+ byDate = d;
+ defaultSeason = s;
+ byTitle = t;
+ }
+};
+
+struct RefreshOverride
+{
+ float fpsmin;
+ float fpsmax;
+
+ float refreshmin;
+ float refreshmax;
+
+ bool fallback;
+};
+
+
+struct RefreshVideoLatency
+{
+ float refreshmin;
+ float refreshmax;
+
+ float delay;
+};
+
+typedef std::vector<TVShowRegexp> SETTINGS_TVSHOWLIST;
+
+class CAdvancedSettings : public ISettingCallback, public ISettingsHandler
+{
+ public:
+ CAdvancedSettings();
+
+ void OnSettingsLoaded() override;
+ void OnSettingsUnloaded() override;
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ void Initialize(CSettingsManager& settingsMgr);
+ void Uninitialize(CSettingsManager& settingsMgr);
+ bool Initialized() const { return m_initialized; }
+ void AddSettingsFile(const std::string &filename);
+ bool Load(const CProfileManager &profileManager);
+
+ static void GetCustomTVRegexps(TiXmlElement *pRootElement, SETTINGS_TVSHOWLIST& settings);
+ static void GetCustomRegexps(TiXmlElement *pRootElement, std::vector<std::string> &settings);
+ static void GetCustomExtensions(TiXmlElement *pRootElement, std::string& extensions);
+
+ std::string m_audioDefaultPlayer;
+ float m_audioPlayCountMinimumPercent;
+ bool m_VideoPlayerIgnoreDTSinWAV;
+ float m_limiterHold;
+ float m_limiterRelease;
+
+ bool m_omlSync = true;
+
+ float m_videoSubsDelayRange;
+ float m_videoAudioDelayRange;
+ bool m_videoUseTimeSeeking;
+ int m_videoTimeSeekForward;
+ int m_videoTimeSeekBackward;
+ int m_videoTimeSeekForwardBig;
+ int m_videoTimeSeekBackwardBig;
+ int m_videoPercentSeekForward;
+ int m_videoPercentSeekBackward;
+ int m_videoPercentSeekForwardBig;
+ int m_videoPercentSeekBackwardBig;
+ std::vector<int> m_seekSteps;
+ std::string m_videoPPFFmpegPostProc;
+ bool m_videoVDPAUtelecine;
+ bool m_videoVDPAUdeintSkipChromaHD;
+ bool m_musicUseTimeSeeking;
+ int m_musicTimeSeekForward;
+ int m_musicTimeSeekBackward;
+ int m_musicTimeSeekForwardBig;
+ int m_musicTimeSeekBackwardBig;
+ int m_musicPercentSeekForward;
+ int m_musicPercentSeekBackward;
+ int m_musicPercentSeekForwardBig;
+ int m_musicPercentSeekBackwardBig;
+ int m_videoIgnoreSecondsAtStart;
+ float m_videoIgnorePercentAtEnd;
+ float m_audioApplyDrc;
+ unsigned int m_maxPassthroughOffSyncDuration = 10; // when 10 ms off adjust
+
+ int m_videoVDPAUScaling;
+ float m_videoNonLinStretchRatio;
+ float m_videoAutoScaleMaxFps;
+ std::vector<RefreshOverride> m_videoAdjustRefreshOverrides;
+ std::vector<RefreshVideoLatency> m_videoRefreshLatency;
+ float m_videoDefaultLatency;
+ int m_videoCaptureUseOcclusionQuery;
+ bool m_DXVACheckCompatibility;
+ bool m_DXVACheckCompatibilityPresent;
+ int m_videoFpsDetect;
+ float m_maxTempo;
+ bool m_videoPreferStereoStream = false;
+
+ std::string m_videoDefaultPlayer;
+ float m_videoPlayCountMinimumPercent;
+
+ float m_slideshowBlackBarCompensation;
+ float m_slideshowZoomAmount;
+ float m_slideshowPanAmount;
+
+ int m_songInfoDuration;
+ int m_logLevel;
+ int m_logLevelHint;
+ std::string m_cddbAddress;
+ bool m_addSourceOnTop; //!< True to put 'add source' buttons on top
+
+ //airtunes + airplay
+ int m_airTunesPort;
+ int m_airPlayPort;
+
+ /*! \brief Only used in linux for the udisks and udisks2 providers
+ * defines if kodi should automount media drives
+ * @note if kodi is running standalone (--standalone option) it will
+ * be set to tue
+ */
+ bool m_handleMounting;
+ /*! \brief Only used in linux for the udisks and udisks2 providers
+ * defines if kodi should automount optical discs
+ */
+ bool m_autoMountOpticalMedia{true};
+
+ bool m_fullScreenOnMovieStart;
+ std::string m_cachePath;
+ std::string m_videoCleanDateTimeRegExp;
+ std::vector<std::string> m_videoCleanStringRegExps;
+ std::vector<std::string> m_videoExcludeFromListingRegExps;
+ std::vector<std::string> m_allExcludeFromScanRegExps;
+ std::vector<std::string> m_moviesExcludeFromScanRegExps;
+ std::vector<std::string> m_tvshowExcludeFromScanRegExps;
+ std::vector<std::string> m_audioExcludeFromListingRegExps;
+ std::vector<std::string> m_audioExcludeFromScanRegExps;
+ std::vector<std::string> m_pictureExcludeFromListingRegExps;
+ std::vector<std::string> m_videoStackRegExps;
+ std::vector<std::string> m_folderStackRegExps;
+ std::vector<std::string> m_trailerMatchRegExps;
+ SETTINGS_TVSHOWLIST m_tvshowEnumRegExps;
+ std::string m_tvshowMultiPartEnumRegExp;
+ typedef std::vector< std::pair<std::string, std::string> > StringMapping;
+ StringMapping m_pathSubstitutions;
+ int m_remoteDelay; ///< \brief number of remote messages to ignore before repeating
+ bool m_bScanIRServer;
+
+ bool m_playlistAsFolders;
+ bool m_detectAsUdf;
+
+ unsigned int m_fanartRes; ///< \brief the maximal resolution to cache fanart at (assumes 16x9)
+ unsigned int m_imageRes; ///< \brief the maximal resolution to cache images at (assumes 16x9)
+ CPictureScalingAlgorithm::Algorithm m_imageScalingAlgorithm;
+ unsigned int
+ m_imageQualityJpeg; ///< \brief the stored jpeg quality the lower the better (default: 4)
+
+ int m_sambaclienttimeout;
+ std::string m_sambadoscodepage;
+ bool m_sambastatfiles;
+
+ bool m_bHTTPDirectoryStatFilesize;
+
+ bool m_bFTPThumbs;
+ bool m_bShoutcastArt;
+
+ std::string m_musicThumbs;
+ std::vector<std::string> m_musicArtistExtraArt;
+ std::vector<std::string> m_musicAlbumExtraArt;
+
+ int m_iMusicLibraryRecentlyAddedItems;
+ int m_iMusicLibraryDateAdded;
+ bool m_bMusicLibraryAllItemsOnBottom;
+ bool m_bMusicLibraryCleanOnUpdate;
+ bool m_bMusicLibraryArtistSortOnUpdate;
+ bool m_bMusicLibraryUseISODates;
+ std::string m_strMusicLibraryAlbumFormat;
+ bool m_prioritiseAPEv2tags;
+ std::string m_musicItemSeparator;
+ std::vector<std::string> m_musicArtistSeparators;
+ std::string m_videoItemSeparator;
+ std::vector<std::string> m_musicTagsFromFileFilters;
+
+ bool m_bVideoLibraryAllItemsOnBottom;
+ int m_iVideoLibraryRecentlyAddedItems;
+ bool m_bVideoLibraryCleanOnUpdate;
+ bool m_bVideoLibraryUseFastHash;
+ bool m_bVideoLibraryImportWatchedState{true};
+ bool m_bVideoLibraryImportResumePoint{true};
+ std::vector<std::string> m_videoEpisodeExtraArt;
+ std::vector<std::string> m_videoTvShowExtraArt;
+ std::vector<std::string> m_videoTvSeasonExtraArt;
+ std::vector<std::string> m_videoMovieExtraArt;
+ std::vector<std::string> m_videoMovieSetExtraArt;
+ std::vector<std::string> m_videoMusicVideoExtraArt;
+
+ bool m_bVideoScannerIgnoreErrors;
+ int m_iVideoLibraryDateAdded;
+
+ std::set<std::string> m_vecTokens;
+
+ int m_iEpgUpdateCheckInterval; // seconds
+ int m_iEpgCleanupInterval; // seconds
+ int m_iEpgActiveTagCheckInterval; // seconds
+ int m_iEpgRetryInterruptedUpdateInterval; // seconds
+ int m_iEpgUpdateEmptyTagsInterval; // seconds
+ bool m_bEpgDisplayUpdatePopup;
+ bool m_bEpgDisplayIncrementalUpdatePopup;
+
+ // EDL Commercial Break
+ bool m_bEdlMergeShortCommBreaks;
+ /*!< @brief If GUI notifications should be shown when reaching the start of commercial breaks */
+ bool m_EdlDisplayCommbreakNotifications;
+ int m_iEdlMaxCommBreakLength; // seconds
+ int m_iEdlMinCommBreakLength; // seconds
+ int m_iEdlMaxCommBreakGap; // seconds
+ int m_iEdlMaxStartGap; // seconds
+ int m_iEdlCommBreakAutowait; // seconds
+ int m_iEdlCommBreakAutowind; // seconds
+
+ int m_curlconnecttimeout;
+ int m_curllowspeedtime;
+ int m_curlretries;
+ int m_curlKeepAliveInterval; // seconds
+ bool m_curlDisableIPV6;
+ bool m_curlDisableHTTP2;
+
+ std::string m_caTrustFile;
+
+ bool m_minimizeToTray; /* win32 only */
+ bool m_fullScreen;
+ bool m_startFullScreen;
+ bool m_showExitButton; /* Ideal for appliances to hide a 'useless' button */
+ bool m_canWindowed;
+ bool m_splashImage;
+ bool m_alwaysOnTop; /* makes xbmc to run always on top .. osx/win32 only .. */
+ int m_playlistRetries;
+ int m_playlistTimeout;
+ bool m_GLRectangleHack;
+ int m_iSkipLoopFilter;
+
+ bool m_bVirtualShares;
+
+ std::string m_cpuTempCmd;
+ std::string m_gpuTempCmd;
+
+ /* PVR/TV related advanced settings */
+ int m_iPVRTimeCorrection; /*!< @brief correct all times (epg tags, timer tags, recording tags) by this amount of minutes. defaults to 0. */
+ int m_iPVRInfoToggleInterval; /*!< @brief if there are more than 1 pvr gui info item available (e.g. multiple recordings active at the same time), use this toggle delay in milliseconds. defaults to 3000. */
+ bool m_bPVRChannelIconsAutoScan; /*!< @brief automatically scan user defined folder for channel icons when loading internal channel groups */
+ bool m_bPVRAutoScanIconsUserSet; /*!< @brief mark channel icons populated by auto scan as "user set" */
+ int m_iPVRNumericChannelSwitchTimeout; /*!< @brief time in msecs after that a channel switch occurs after entering a channel number, if confirmchannelswitch is disabled */
+ int m_iPVRTimeshiftThreshold; /*!< @brief time diff between current playing time and timeshift buffer end, in seconds, before a playing stream is displayed as timeshifting. */
+ bool m_bPVRTimeshiftSimpleOSD; /*!< @brief use simple timeshift OSD (with progress only for the playing event instead of progress for the whole ts buffer). */
+ SortDescription m_PVRDefaultSortOrder; /*!< @brief SortDecription used to store default recording sort type and sort order */
+
+ DatabaseSettings m_databaseMusic; // advanced music database setup
+ DatabaseSettings m_databaseVideo; // advanced video database setup
+ DatabaseSettings m_databaseTV; // advanced tv database setup
+ DatabaseSettings m_databaseEpg; /*!< advanced EPG database setup */
+
+ bool m_useLocaleCollation;
+
+ bool m_guiVisualizeDirtyRegions;
+ int m_guiAlgorithmDirtyRegions;
+ bool m_guiSmartRedraw;
+ unsigned int m_addonPackageFolderSize;
+
+ unsigned int m_cacheMemSize;
+ unsigned int m_cacheBufferMode;
+ unsigned int m_cacheChunkSize;
+ float m_cacheReadFactor;
+
+ bool m_jsonOutputCompact;
+ unsigned int m_jsonTcpPort;
+
+ bool m_enableMultimediaKeys;
+ std::vector<std::string> m_settingsFiles;
+ void ParseSettingsFile(const std::string &file);
+
+ float GetLatencyTweak(float refreshrate);
+ bool m_initialized;
+
+ void SetDebugMode(bool debug);
+
+ //! \brief Toggles dirty-region visualization
+ void ToggleDirtyRegionVisualization()
+ {
+ m_guiVisualizeDirtyRegions = !m_guiVisualizeDirtyRegions;
+ }
+
+ // runtime settings which cannot be set from advancedsettings.xml
+ std::string m_videoExtensions;
+ std::string m_discStubExtensions;
+ std::string m_subtitlesExtensions;
+ std::string m_musicExtensions;
+ std::string m_pictureExtensions;
+
+ std::string m_stereoscopicregex_3d;
+ std::string m_stereoscopicregex_sbs;
+ std::string m_stereoscopicregex_tab;
+
+ bool m_openGlDebugging;
+
+ std::string m_userAgent;
+ uint32_t m_nfsTimeout;
+ int m_nfsRetries;
+
+ private:
+ void Initialize();
+ void Clear();
+ void SetExtraArtwork(const TiXmlElement* arttypes, std::vector<std::string>& artworkMap);
+ void MigrateOldArtSettings();
+};
diff --git a/xbmc/settings/CMakeLists.txt b/xbmc/settings/CMakeLists.txt
new file mode 100644
index 0000000..f53d12b
--- /dev/null
+++ b/xbmc/settings/CMakeLists.txt
@@ -0,0 +1,49 @@
+set(SOURCES AdvancedSettings.cpp
+ DisplaySettings.cpp
+ GameSettings.cpp
+ LibExportSettings.cpp
+ MediaSettings.cpp
+ MediaSourceSettings.cpp
+ SettingAddon.cpp
+ SettingConditions.cpp
+ SettingControl.cpp
+ SettingCreator.cpp
+ SettingDateTime.cpp
+ SettingPath.cpp
+ Settings.cpp
+ SettingsBase.cpp
+ SettingsValueFlatJsonSerializer.cpp
+ SettingsValueXmlSerializer.cpp
+ SettingUtils.cpp
+ SkinSettings.cpp
+ SettingsComponent.cpp
+ SubtitlesSettings.cpp)
+
+set(HEADERS AdvancedSettings.h
+ DiscSettings.h
+ DisplaySettings.h
+ GameSettings.h
+ ISubSettings.h
+ LibExportSettings.h
+ MediaSettings.h
+ MediaSourceSettings.h
+ SettingAddon.h
+ SettingConditions.h
+ SettingControl.h
+ SettingCreator.h
+ SettingDateTime.h
+ SettingPath.h
+ Settings.h
+ SettingsBase.h
+ SettingsValueFlatJsonSerializer.h
+ SettingsValueXmlSerializer.h
+ SettingUtils.h
+ SkinSettings.h
+ SettingsComponent.h
+ SubtitlesSettings.h)
+
+if(BLURAY_FOUND)
+ list(APPEND SOURCES DiscSettings.cpp)
+endif()
+
+core_add_library(settings)
diff --git a/xbmc/settings/DiscSettings.cpp b/xbmc/settings/DiscSettings.cpp
new file mode 100644
index 0000000..de8bb33
--- /dev/null
+++ b/xbmc/settings/DiscSettings.cpp
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017-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 "DiscSettings.h"
+
+#include "Settings.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "lib/Setting.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <string>
+
+#include <libbluray/bluray-version.h>
+#include <libbluray/bluray.h>
+
+using namespace KODI::MESSAGING;
+
+CDiscSettings& CDiscSettings::GetInstance()
+{
+ static CDiscSettings sDiscSettings;
+ return sDiscSettings;
+}
+
+void CDiscSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+#if (BLURAY_VERSION >= BLURAY_VERSION_CODE(1,0,1))
+ if (setting == NULL)
+ return;
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == CSettings::SETTING_DISC_PLAYBACK)
+ {
+ int mode = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ if (mode == BD_PLAYBACK_DISC_MENU)
+ {
+ bool bdjWorking = false;
+ BLURAY* bd = bd_init();
+ const BLURAY_DISC_INFO* info = bd_get_disc_info(bd);
+
+ if (!info->libjvm_detected)
+ CLog::Log(LOGDEBUG, "DiscSettings - Could not load the java vm.");
+ else if (!info->bdj_handled)
+ CLog::Log(LOGDEBUG, "DiscSettings - Could not load the libbluray.jar.");
+ else
+ bdjWorking = true;
+
+ bd_close(bd);
+
+ if (!bdjWorking)
+ HELPERS::ShowOKDialogText(CVariant{ 29803 }, CVariant{ 29804 });
+ }
+ }
+#endif
+}
diff --git a/xbmc/settings/DiscSettings.h b/xbmc/settings/DiscSettings.h
new file mode 100644
index 0000000..9d62eea
--- /dev/null
+++ b/xbmc/settings/DiscSettings.h
@@ -0,0 +1,34 @@
+/*
+ * 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
+
+/**
+* Playback settings
+*/
+enum BDPlaybackMode
+{
+ BD_PLAYBACK_SIMPLE_MENU = 0,
+ BD_PLAYBACK_DISC_MENU,
+ BD_PLAYBACK_MAIN_TITLE,
+};
+
+#include "settings/lib/ISettingCallback.h"
+
+class CDiscSettings : public ISettingCallback
+{
+public:
+ /* ISettingCallback*/
+
+ static CDiscSettings& GetInstance();
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+private:
+ CDiscSettings() = default;
+ ~CDiscSettings() override = default;
+};
diff --git a/xbmc/settings/DisplaySettings.cpp b/xbmc/settings/DisplaySettings.cpp
new file mode 100644
index 0000000..25f230a
--- /dev/null
+++ b/xbmc/settings/DisplaySettings.cpp
@@ -0,0 +1,948 @@
+/*
+ * Copyright (C) 2013-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 "DisplaySettings.h"
+
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/VideoRenderers/ColorManager.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/StereoscopicsManager.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "rendering/RenderSystem.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <float.h>
+#include <mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+#ifdef TARGET_WINDOWS
+#include "rendering/dx/DeviceResources.h"
+#endif
+
+using namespace KODI::MESSAGING;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+// 0.1 second increments
+#define MAX_REFRESH_CHANGE_DELAY 200
+
+static RESOLUTION_INFO EmptyResolution;
+static RESOLUTION_INFO EmptyModifiableResolution;
+
+float square_error(float x, float y)
+{
+ float yonx = (x > 0) ? y / x : 0;
+ float xony = (y > 0) ? x / y : 0;
+ return std::max(yonx, xony);
+}
+
+static std::string ModeFlagsToString(unsigned int flags, bool identifier)
+{
+ std::string res;
+ if(flags & D3DPRESENTFLAG_INTERLACED)
+ res += "i";
+ else
+ res += "p";
+
+ if(!identifier)
+ res += " ";
+
+ if(flags & D3DPRESENTFLAG_MODE3DSBS)
+ res += "sbs";
+ else if(flags & D3DPRESENTFLAG_MODE3DTB)
+ res += "tab";
+ else if(identifier)
+ res += "std";
+ return res;
+}
+
+CDisplaySettings::CDisplaySettings()
+{
+ m_resolutions.resize(RES_CUSTOM);
+
+ m_zoomAmount = 1.0f;
+ m_pixelRatio = 1.0f;
+ m_verticalShift = 0.0f;
+ m_nonLinearStretched = false;
+ m_resolutionChangeAborted = false;
+}
+
+CDisplaySettings::~CDisplaySettings() = default;
+
+CDisplaySettings& CDisplaySettings::GetInstance()
+{
+ static CDisplaySettings sDisplaySettings;
+ return sDisplaySettings;
+}
+
+bool CDisplaySettings::Load(const TiXmlNode *settings)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_calibrations.clear();
+
+ if (settings == NULL)
+ return false;
+
+ const TiXmlElement *pElement = settings->FirstChildElement("resolutions");
+ if (!pElement)
+ {
+ CLog::Log(LOGERROR, "CDisplaySettings: settings file doesn't contain <resolutions>");
+ return false;
+ }
+
+ const TiXmlElement *pResolution = pElement->FirstChildElement("resolution");
+ while (pResolution)
+ {
+ // get the data for this calibration
+ RESOLUTION_INFO cal;
+
+ XMLUtils::GetString(pResolution, "description", cal.strMode);
+ XMLUtils::GetInt(pResolution, "subtitles", cal.iSubtitles);
+ XMLUtils::GetFloat(pResolution, "pixelratio", cal.fPixelRatio);
+#ifdef HAVE_X11
+ XMLUtils::GetFloat(pResolution, "refreshrate", cal.fRefreshRate);
+ XMLUtils::GetString(pResolution, "output", cal.strOutput);
+ XMLUtils::GetString(pResolution, "xrandrid", cal.strId);
+#endif
+
+ const TiXmlElement *pOverscan = pResolution->FirstChildElement("overscan");
+ if (pOverscan)
+ {
+ XMLUtils::GetInt(pOverscan, "left", cal.Overscan.left);
+ XMLUtils::GetInt(pOverscan, "top", cal.Overscan.top);
+ XMLUtils::GetInt(pOverscan, "right", cal.Overscan.right);
+ XMLUtils::GetInt(pOverscan, "bottom", cal.Overscan.bottom);
+ }
+
+ // mark calibration as not updated
+ // we must not delete those, resolution just might not be available
+ cal.iWidth = cal.iHeight = 0;
+
+ // store calibration, avoid adding duplicates
+ bool found = false;
+ for (ResolutionInfos::const_iterator it = m_calibrations.begin(); it != m_calibrations.end(); ++it)
+ {
+ if (StringUtils::EqualsNoCase(it->strMode, cal.strMode))
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ m_calibrations.push_back(cal);
+
+ // iterate around
+ pResolution = pResolution->NextSiblingElement("resolution");
+ }
+
+ ApplyCalibrations();
+ return true;
+}
+
+bool CDisplaySettings::Save(TiXmlNode *settings) const
+{
+ if (settings == NULL)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ TiXmlElement xmlRootElement("resolutions");
+ TiXmlNode *pRoot = settings->InsertEndChild(xmlRootElement);
+ if (pRoot == NULL)
+ return false;
+
+ // save calibrations
+ for (ResolutionInfos::const_iterator it = m_calibrations.begin(); it != m_calibrations.end(); ++it)
+ {
+ // Write the resolution tag
+ TiXmlElement resElement("resolution");
+ TiXmlNode *pNode = pRoot->InsertEndChild(resElement);
+ if (pNode == NULL)
+ return false;
+
+ // Now write each of the pieces of information we need...
+ XMLUtils::SetString(pNode, "description", it->strMode);
+ XMLUtils::SetInt(pNode, "subtitles", it->iSubtitles);
+ XMLUtils::SetFloat(pNode, "pixelratio", it->fPixelRatio);
+#ifdef HAVE_X11
+ XMLUtils::SetFloat(pNode, "refreshrate", it->fRefreshRate);
+ XMLUtils::SetString(pNode, "output", it->strOutput);
+ XMLUtils::SetString(pNode, "xrandrid", it->strId);
+#endif
+
+ // create the overscan child
+ TiXmlElement overscanElement("overscan");
+ TiXmlNode *pOverscanNode = pNode->InsertEndChild(overscanElement);
+ if (pOverscanNode == NULL)
+ return false;
+
+ XMLUtils::SetInt(pOverscanNode, "left", it->Overscan.left);
+ XMLUtils::SetInt(pOverscanNode, "top", it->Overscan.top);
+ XMLUtils::SetInt(pOverscanNode, "right", it->Overscan.right);
+ XMLUtils::SetInt(pOverscanNode, "bottom", it->Overscan.bottom);
+ }
+
+ return true;
+}
+
+void CDisplaySettings::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_calibrations.clear();
+ m_resolutions.clear();
+ m_resolutions.resize(RES_CUSTOM);
+
+ m_zoomAmount = 1.0f;
+ m_pixelRatio = 1.0f;
+ m_verticalShift = 0.0f;
+ m_nonLinearStretched = false;
+}
+
+void CDisplaySettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == "videoscreen.cms3dlut")
+ {
+ std::string path = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ VECSOURCES shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ if (CGUIDialogFileBrowser::ShowAndGetFile(shares, ".3dlut", g_localizeStrings.Get(36580), path))
+ {
+ std::static_pointer_cast<CSettingString>(std::const_pointer_cast<CSetting>(setting))->SetValue(path);
+ }
+ }
+ else if (settingId == "videoscreen.displayprofile")
+ {
+ std::string path = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ VECSOURCES shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ if (CGUIDialogFileBrowser::ShowAndGetFile(shares, ".icc|.icm", g_localizeStrings.Get(36581), path))
+ {
+ std::static_pointer_cast<CSettingString>(std::const_pointer_cast<CSetting>(setting))->SetValue(path);
+ }
+ }
+}
+
+bool CDisplaySettings::OnSettingChanging(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return false;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_VIDEOSCREEN_RESOLUTION ||
+ settingId == CSettings::SETTING_VIDEOSCREEN_SCREEN)
+ {
+ RESOLUTION newRes = RES_DESKTOP;
+ if (settingId == CSettings::SETTING_VIDEOSCREEN_RESOLUTION)
+ newRes = (RESOLUTION)std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ else if (settingId == CSettings::SETTING_VIDEOSCREEN_SCREEN)
+ {
+ int screen = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+
+ // if triggered by a change of screenmode, screen may not have changed
+ if (screen == GetCurrentDisplayMode())
+ return true;
+
+ // get desktop resolution for screen
+ newRes = GetResolutionForScreen();
+ }
+
+ std::string screenmode = GetStringFromResolution(newRes);
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_VIDEOSCREEN_SCREENMODE, screenmode))
+ return false;
+ }
+
+ if (settingId == CSettings::SETTING_VIDEOSCREEN_SCREENMODE)
+ {
+ RESOLUTION oldRes = GetCurrentResolution();
+ RESOLUTION newRes = GetResolutionFromString(std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+
+ SetCurrentResolution(newRes, false);
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(newRes, false);
+
+ // check if the old or the new resolution was/is windowed
+ // in which case we don't show any prompt to the user
+ if (oldRes != RES_WINDOW && newRes != RES_WINDOW && oldRes != newRes)
+ {
+ if (!m_resolutionChangeAborted)
+ {
+ if (HELPERS::ShowYesNoDialogText(CVariant{13110}, CVariant{13111}, CVariant{""},
+ CVariant{""}, 15000) != DialogResponse::CHOICE_YES)
+ {
+ m_resolutionChangeAborted = true;
+ return false;
+ }
+ }
+ else
+ m_resolutionChangeAborted = false;
+ }
+ }
+ else if (settingId == CSettings::SETTING_VIDEOSCREEN_MONITOR)
+ {
+ CServiceBroker::GetWinSystem()->UpdateResolutions();
+ RESOLUTION newRes = GetResolutionForScreen();
+
+ SetCurrentResolution(newRes, false);
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(newRes, true);
+
+ if (!m_resolutionChangeAborted)
+ {
+ if (HELPERS::ShowYesNoDialogText(CVariant{13110}, CVariant{13111}, CVariant{""}, CVariant{""},
+ 10000) != DialogResponse::CHOICE_YES)
+ {
+ m_resolutionChangeAborted = true;
+ return false;
+ }
+ }
+ else
+ m_resolutionChangeAborted = false;
+
+ return true;
+ }
+ else if (settingId == CSettings::SETTING_VIDEOSCREEN_10BITSURFACES)
+ {
+#ifdef TARGET_WINDOWS
+ DX::DeviceResources::Get()->ApplyDisplaySettings();
+ return true;
+#endif
+ }
+#if defined(HAVE_X11) || defined(TARGET_WINDOWS_DESKTOP) || defined(TARGET_DARWIN_OSX)
+ else if (settingId == CSettings::SETTING_VIDEOSCREEN_BLANKDISPLAYS)
+ {
+ auto winSystem = CServiceBroker::GetWinSystem();
+#if defined(HAVE_X11)
+ winSystem->UpdateResolutions();
+#elif defined(TARGET_WINDOWS_DESKTOP) || defined(TARGET_DARWIN_OSX)
+ CGraphicContext& gfxContext = winSystem->GetGfxContext();
+ gfxContext.SetVideoResolution(gfxContext.GetVideoResolution(), true);
+#endif
+ }
+#endif
+
+ return true;
+}
+
+bool CDisplaySettings::OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode)
+{
+ if (setting == NULL)
+ return false;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_VIDEOSCREEN_SCREENMODE)
+ {
+ std::shared_ptr<CSettingString> screenmodeSetting = std::static_pointer_cast<CSettingString>(setting);
+ std::string screenmode = screenmodeSetting->GetValue();
+ // in Eden there was no character ("i" or "p") indicating interlaced/progressive
+ // at the end so we just add a "p" and assume progressive
+ // no 3d mode existed before, so just assume std modes
+ if (screenmode.size() == 20)
+ return screenmodeSetting->SetValue(screenmode + "pstd");
+ if (screenmode.size() == 21)
+ return screenmodeSetting->SetValue(screenmode + "std");
+ }
+ else if (settingId == CSettings::SETTING_VIDEOSCREEN_PREFEREDSTEREOSCOPICMODE)
+ {
+ std::shared_ptr<CSettingInt> stereomodeSetting = std::static_pointer_cast<CSettingInt>(setting);
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ STEREOSCOPIC_PLAYBACK_MODE playbackMode = (STEREOSCOPIC_PLAYBACK_MODE) settings->GetInt(CSettings::SETTING_VIDEOPLAYER_STEREOSCOPICPLAYBACKMODE);
+ if (stereomodeSetting->GetValue() == RENDER_STEREO_MODE_OFF)
+ {
+ // if preferred playback mode was OFF, update playback mode to ignore
+ if (playbackMode == STEREOSCOPIC_PLAYBACK_MODE_PREFERRED)
+ settings->SetInt(CSettings::SETTING_VIDEOPLAYER_STEREOSCOPICPLAYBACKMODE, STEREOSCOPIC_PLAYBACK_MODE_IGNORE);
+ return stereomodeSetting->SetValue(RENDER_STEREO_MODE_AUTO);
+ }
+ else if (stereomodeSetting->GetValue() == RENDER_STEREO_MODE_MONO)
+ {
+ // if preferred playback mode was MONO, update playback mode
+ if (playbackMode == STEREOSCOPIC_PLAYBACK_MODE_PREFERRED)
+ settings->SetInt(CSettings::SETTING_VIDEOPLAYER_STEREOSCOPICPLAYBACKMODE, STEREOSCOPIC_PLAYBACK_MODE_MONO);
+ return stereomodeSetting->SetValue(RENDER_STEREO_MODE_AUTO);
+ }
+ }
+
+ return false;
+}
+
+void CDisplaySettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (!setting)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_VIDEOSCREEN_WHITELIST)
+ CResolutionUtils::PrintWhitelist();
+}
+
+void CDisplaySettings::SetMonitor(const std::string& monitor)
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ const std::string curMonitor = settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR);
+ if (curMonitor != monitor)
+ {
+ m_resolutionChangeAborted = true;
+ settings->SetString(CSettings::SETTING_VIDEOSCREEN_MONITOR, monitor);
+ }
+}
+
+void CDisplaySettings::SetCurrentResolution(RESOLUTION resolution, bool save /* = false */)
+{
+ if (resolution == RES_WINDOW && !CServiceBroker::GetWinSystem()->CanDoWindowed())
+ resolution = RES_DESKTOP;
+
+ if (save)
+ {
+ // Save videoscreen.screenmode setting
+ std::string mode = GetStringFromResolution(resolution);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_VIDEOSCREEN_SCREENMODE, mode.c_str());
+
+ // Check if videoscreen.screen setting also needs to be saved
+ // e.g. if ToggleFullscreen is called
+ int currentDisplayMode = GetCurrentDisplayMode();
+ int currentDisplayModeSetting = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOSCREEN_SCREEN);
+ if (currentDisplayMode != currentDisplayModeSetting)
+ {
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(CSettings::SETTING_VIDEOSCREEN_SCREEN, currentDisplayMode);
+ }
+ }
+ else if (resolution != m_currentResolution)
+ {
+ m_currentResolution = resolution;
+ SetChanged();
+ }
+}
+
+RESOLUTION CDisplaySettings::GetDisplayResolution() const
+{
+ return GetResolutionFromString(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_SCREENMODE));
+}
+
+const RESOLUTION_INFO& CDisplaySettings::GetResolutionInfo(size_t index) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (index >= m_resolutions.size())
+ return EmptyResolution;
+
+ return m_resolutions[index];
+}
+
+const RESOLUTION_INFO& CDisplaySettings::GetResolutionInfo(RESOLUTION resolution) const
+{
+ if (resolution <= RES_INVALID)
+ return EmptyResolution;
+
+ return GetResolutionInfo((size_t)resolution);
+}
+
+RESOLUTION_INFO& CDisplaySettings::GetResolutionInfo(size_t index)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (index >= m_resolutions.size())
+ {
+ EmptyModifiableResolution = RESOLUTION_INFO();
+ return EmptyModifiableResolution;
+ }
+
+ return m_resolutions[index];
+}
+
+RESOLUTION_INFO& CDisplaySettings::GetResolutionInfo(RESOLUTION resolution)
+{
+ if (resolution <= RES_INVALID)
+ {
+ EmptyModifiableResolution = RESOLUTION_INFO();
+ return EmptyModifiableResolution;
+ }
+
+ return GetResolutionInfo((size_t)resolution);
+}
+
+void CDisplaySettings::AddResolutionInfo(const RESOLUTION_INFO &resolution)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ RESOLUTION_INFO res(resolution);
+
+ if((res.dwFlags & D3DPRESENTFLAG_MODE3DTB) == 0)
+ {
+ /* add corrections for some special case modes frame packing modes */
+
+ if(res.iScreenWidth == 1920
+ && res.iScreenHeight == 2205)
+ {
+ res.iBlanking = 45;
+ res.dwFlags |= D3DPRESENTFLAG_MODE3DTB;
+ }
+
+ if(res.iScreenWidth == 1280
+ && res.iScreenHeight == 1470)
+ {
+ res.iBlanking = 30;
+ res.dwFlags |= D3DPRESENTFLAG_MODE3DTB;
+ }
+ }
+ m_resolutions.push_back(res);
+}
+
+void CDisplaySettings::ApplyCalibrations()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // apply all calibrations to the resolutions
+ for (ResolutionInfos::const_iterator itCal = m_calibrations.begin(); itCal != m_calibrations.end(); ++itCal)
+ {
+ // find resolutions
+ for (size_t res = RES_DESKTOP; res < m_resolutions.size(); ++res)
+ {
+ if (StringUtils::EqualsNoCase(itCal->strMode, m_resolutions[res].strMode))
+ {
+ // overscan
+ m_resolutions[res].Overscan.left = itCal->Overscan.left;
+ if (m_resolutions[res].Overscan.left < -m_resolutions[res].iWidth/4)
+ m_resolutions[res].Overscan.left = -m_resolutions[res].iWidth/4;
+ if (m_resolutions[res].Overscan.left > m_resolutions[res].iWidth/4)
+ m_resolutions[res].Overscan.left = m_resolutions[res].iWidth/4;
+
+ m_resolutions[res].Overscan.top = itCal->Overscan.top;
+ if (m_resolutions[res].Overscan.top < -m_resolutions[res].iHeight/4)
+ m_resolutions[res].Overscan.top = -m_resolutions[res].iHeight/4;
+ if (m_resolutions[res].Overscan.top > m_resolutions[res].iHeight/4)
+ m_resolutions[res].Overscan.top = m_resolutions[res].iHeight/4;
+
+ m_resolutions[res].Overscan.right = itCal->Overscan.right;
+ if (m_resolutions[res].Overscan.right < m_resolutions[res].iWidth / 2)
+ m_resolutions[res].Overscan.right = m_resolutions[res].iWidth / 2;
+ if (m_resolutions[res].Overscan.right > m_resolutions[res].iWidth * 3/2)
+ m_resolutions[res].Overscan.right = m_resolutions[res].iWidth *3/2;
+
+ m_resolutions[res].Overscan.bottom = itCal->Overscan.bottom;
+ if (m_resolutions[res].Overscan.bottom < m_resolutions[res].iHeight / 2)
+ m_resolutions[res].Overscan.bottom = m_resolutions[res].iHeight / 2;
+ if (m_resolutions[res].Overscan.bottom > m_resolutions[res].iHeight * 3/2)
+ m_resolutions[res].Overscan.bottom = m_resolutions[res].iHeight * 3/2;
+
+ m_resolutions[res].iSubtitles = itCal->iSubtitles;
+ if (m_resolutions[res].iSubtitles < 0)
+ m_resolutions[res].iSubtitles = 0;
+ if (m_resolutions[res].iSubtitles > m_resolutions[res].iHeight * 3 / 2)
+ m_resolutions[res].iSubtitles = m_resolutions[res].iHeight * 3 / 2;
+
+ m_resolutions[res].fPixelRatio = itCal->fPixelRatio;
+ if (m_resolutions[res].fPixelRatio < 0.5f)
+ m_resolutions[res].fPixelRatio = 0.5f;
+ if (m_resolutions[res].fPixelRatio > 2.0f)
+ m_resolutions[res].fPixelRatio = 2.0f;
+ break;
+ }
+ }
+ }
+}
+
+void CDisplaySettings::UpdateCalibrations()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ if (m_resolutions.size() <= RES_DESKTOP)
+ return;
+
+ // Add new (unique) resolutions
+ for (ResolutionInfos::const_iterator res(m_resolutions.cbegin() + RES_CUSTOM); res != m_resolutions.cend(); ++res)
+ if (std::find_if(m_calibrations.cbegin(), m_calibrations.cend(),
+ [&](const RESOLUTION_INFO& info) { return StringUtils::EqualsNoCase(res->strMode, info.strMode); }) == m_calibrations.cend())
+ m_calibrations.push_back(*res);
+
+ for (auto &cal : m_calibrations)
+ {
+ ResolutionInfos::const_iterator res(std::find_if(m_resolutions.cbegin() + RES_DESKTOP, m_resolutions.cend(),
+ [&](const RESOLUTION_INFO& info) { return StringUtils::EqualsNoCase(cal.strMode, info.strMode); }));
+
+ if (res != m_resolutions.cend())
+ {
+ //! @todo erase calibrations with default values
+ cal = *res;
+ }
+ }
+}
+
+void CDisplaySettings::ClearCalibrations()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_calibrations.clear();
+}
+
+DisplayMode CDisplaySettings::GetCurrentDisplayMode() const
+{
+ if (GetCurrentResolution() == RES_WINDOW)
+ return DM_WINDOWED;
+
+ return DM_FULLSCREEN;
+}
+
+RESOLUTION CDisplaySettings::FindBestMatchingResolution(const std::map<RESOLUTION, RESOLUTION_INFO> &resolutionInfos, int width, int height, float refreshrate, unsigned flags)
+{
+ // find the closest match to these in our res vector. If we have the screen, we score the res
+ RESOLUTION bestRes = RES_DESKTOP;
+ float bestScore = FLT_MAX;
+ flags &= D3DPRESENTFLAG_MODEMASK;
+
+ for (std::map<RESOLUTION, RESOLUTION_INFO>::const_iterator it = resolutionInfos.begin(); it != resolutionInfos.end(); ++it)
+ {
+ const RESOLUTION_INFO &info = it->second;
+
+ if ((info.dwFlags & D3DPRESENTFLAG_MODEMASK) != flags)
+ continue;
+
+ float score = 10 * (square_error((float)info.iScreenWidth, (float)width) +
+ square_error((float)info.iScreenHeight, (float)height) +
+ square_error(info.fRefreshRate, refreshrate));
+ if (score < bestScore)
+ {
+ bestScore = score;
+ bestRes = it->first;
+ }
+ }
+
+ return bestRes;
+}
+
+RESOLUTION CDisplaySettings::GetResolutionFromString(const std::string &strResolution)
+{
+
+ if (strResolution == "DESKTOP")
+ return RES_DESKTOP;
+ else if (strResolution == "WINDOW")
+ return RES_WINDOW;
+ else if (strResolution.size() >= 20)
+ {
+ // format: WWWWWHHHHHRRR.RRRRRP333, where W = width, H = height, R = refresh, P = interlace, 3 = stereo mode
+ int width = std::strtol(StringUtils::Mid(strResolution, 0,5).c_str(), NULL, 10);
+ int height = std::strtol(StringUtils::Mid(strResolution, 5,5).c_str(), NULL, 10);
+ float refresh = (float)std::strtod(StringUtils::Mid(strResolution, 10,9).c_str(), NULL);
+ unsigned flags = 0;
+
+ // look for 'i' and treat everything else as progressive,
+ if(StringUtils::Mid(strResolution, 19,1) == "i")
+ flags |= D3DPRESENTFLAG_INTERLACED;
+
+ if(StringUtils::Mid(strResolution, 20,3) == "sbs")
+ flags |= D3DPRESENTFLAG_MODE3DSBS;
+ else if(StringUtils::Mid(strResolution, 20,3) == "tab")
+ flags |= D3DPRESENTFLAG_MODE3DTB;
+
+ std::map<RESOLUTION, RESOLUTION_INFO> resolutionInfos;
+ for (size_t resolution = RES_DESKTOP; resolution < CDisplaySettings::GetInstance().ResolutionInfoSize(); resolution++)
+ resolutionInfos.insert(std::make_pair((RESOLUTION)resolution, CDisplaySettings::GetInstance().GetResolutionInfo(resolution)));
+
+ return FindBestMatchingResolution(resolutionInfos, width, height, refresh, flags);
+ }
+
+ return RES_DESKTOP;
+}
+
+std::string CDisplaySettings::GetStringFromResolution(RESOLUTION resolution, float refreshrate /* = 0.0f */)
+{
+ if (resolution == RES_WINDOW)
+ return "WINDOW";
+
+ if (resolution >= RES_DESKTOP && resolution < (RESOLUTION)CDisplaySettings::GetInstance().ResolutionInfoSize())
+ {
+ const RESOLUTION_INFO &info = CDisplaySettings::GetInstance().GetResolutionInfo(resolution);
+ // also handle RES_DESKTOP resolutions with non-default refresh rates
+ if (resolution != RES_DESKTOP || (refreshrate > 0.0f && refreshrate != info.fRefreshRate))
+ {
+ return StringUtils::Format("{:05}{:05}{:09.5f}{}", info.iScreenWidth, info.iScreenHeight,
+ refreshrate > 0.0f ? refreshrate : info.fRefreshRate,
+ ModeFlagsToString(info.dwFlags, true));
+ }
+ }
+
+ return "DESKTOP";
+}
+
+RESOLUTION CDisplaySettings::GetResolutionForScreen()
+{
+ DisplayMode mode = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOSCREEN_SCREEN);
+ if (mode == DM_WINDOWED)
+ return RES_WINDOW;
+
+ return RES_DESKTOP;
+}
+
+static inline bool ModeSort(const StringSettingOption& i, const StringSettingOption& j)
+{
+ return (i.value > j.value);
+}
+
+void CDisplaySettings::SettingOptionsModesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ for (auto index = (unsigned int)RES_CUSTOM; index < CDisplaySettings::GetInstance().ResolutionInfoSize(); ++index)
+ {
+ const auto mode = CDisplaySettings::GetInstance().GetResolutionInfo(index);
+
+ if (mode.dwFlags ^ D3DPRESENTFLAG_INTERLACED)
+ {
+ auto setting = GetStringFromResolution((RESOLUTION)index, mode.fRefreshRate);
+
+ list.emplace_back(
+ StringUtils::Format("{}x{}{} {:0.2f}Hz", mode.iScreenWidth, mode.iScreenHeight,
+ ModeFlagsToString(mode.dwFlags, false), mode.fRefreshRate),
+ setting);
+ }
+ }
+
+ std::sort(list.begin(), list.end(), ModeSort);
+}
+
+void CDisplaySettings::SettingOptionsRefreshChangeDelaysFiller(
+ const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(13551), 0);
+
+ for (int i = 1; i <= MAX_REFRESH_CHANGE_DELAY; i++)
+ list.emplace_back(
+ StringUtils::Format(g_localizeStrings.Get(13553), static_cast<double>(i) / 10.0), i);
+}
+
+void CDisplaySettings::SettingOptionsRefreshRatesFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ // get the proper resolution
+ RESOLUTION res = CDisplaySettings::GetInstance().GetDisplayResolution();
+ if (res < RES_WINDOW)
+ return;
+
+ // only add "Windowed" if in windowed mode
+ if (res == RES_WINDOW)
+ {
+ current = "WINDOW";
+ list.emplace_back(g_localizeStrings.Get(242), current);
+ return;
+ }
+
+ RESOLUTION_INFO resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(res);
+ // The only meaningful parts of res here are iScreenWidth, iScreenHeight
+ std::vector<REFRESHRATE> refreshrates = CServiceBroker::GetWinSystem()->RefreshRates(resInfo.iScreenWidth, resInfo.iScreenHeight, resInfo.dwFlags);
+
+ bool match = false;
+ for (std::vector<REFRESHRATE>::const_iterator refreshrate = refreshrates.begin(); refreshrate != refreshrates.end(); ++refreshrate)
+ {
+ std::string screenmode = GetStringFromResolution((RESOLUTION)refreshrate->ResInfo_Index, refreshrate->RefreshRate);
+ if (!match && StringUtils::EqualsNoCase(std::static_pointer_cast<const CSettingString>(setting)->GetValue(), screenmode))
+ match = true;
+ list.emplace_back(StringUtils::Format("{:.2f}", refreshrate->RefreshRate), screenmode);
+ }
+
+ if (!match)
+ current = GetStringFromResolution(res, CServiceBroker::GetWinSystem()->DefaultRefreshRate(refreshrates).RefreshRate);
+}
+
+void CDisplaySettings::SettingOptionsResolutionsFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ RESOLUTION res = CDisplaySettings::GetInstance().GetDisplayResolution();
+ RESOLUTION_INFO info = CDisplaySettings::GetInstance().GetResolutionInfo(res);
+ if (res == RES_WINDOW)
+ {
+ current = res;
+ list.emplace_back(g_localizeStrings.Get(242), res);
+ }
+ else
+ {
+ std::map<RESOLUTION, RESOLUTION_INFO> resolutionInfos;
+ std::vector<RESOLUTION_WHR> resolutions = CServiceBroker::GetWinSystem()->ScreenResolutions(info.fRefreshRate);
+ for (std::vector<RESOLUTION_WHR>::const_iterator resolution = resolutions.begin(); resolution != resolutions.end(); ++resolution)
+ {
+ list.emplace_back(StringUtils::Format("{}x{}{}", resolution->width, resolution->height,
+ ModeFlagsToString(resolution->flags, false)),
+ resolution->ResInfo_Index);
+
+ resolutionInfos.insert(std::make_pair((RESOLUTION)resolution->ResInfo_Index, CDisplaySettings::GetInstance().GetResolutionInfo(resolution->ResInfo_Index)));
+ }
+
+ current = FindBestMatchingResolution(resolutionInfos,
+ info.iScreenWidth, info.iScreenHeight,
+ info.fRefreshRate, info.dwFlags);
+ }
+}
+
+void CDisplaySettings::SettingOptionsDispModeFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ // The user should only be able to disable windowed modes with the canwindowed
+ // setting. When the user sets canwindowed to true but the windowing system
+ // does not support windowed modes, we would just shoot ourselves in the foot
+ // by offering the option.
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_canWindowed && CServiceBroker::GetWinSystem()->CanDoWindowed())
+ list.emplace_back(g_localizeStrings.Get(242), DM_WINDOWED);
+
+ list.emplace_back(g_localizeStrings.Get(244), DM_FULLSCREEN);
+}
+
+void CDisplaySettings::SettingOptionsStereoscopicModesFiller(
+ const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui != nullptr)
+ {
+ const CStereoscopicsManager &stereoscopicsManager = gui->GetStereoscopicsManager();
+
+ for (int i = RENDER_STEREO_MODE_OFF; i < RENDER_STEREO_MODE_COUNT; i++)
+ {
+ RENDER_STEREO_MODE mode = (RENDER_STEREO_MODE) i;
+ if (CServiceBroker::GetRenderSystem()->SupportsStereo(mode))
+ list.emplace_back(stereoscopicsManager.GetLabelForStereoMode(mode), mode);
+ }
+ }
+}
+
+void CDisplaySettings::SettingOptionsPreferredStereoscopicViewModesFiller(
+ const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ const CStereoscopicsManager &stereoscopicsManager = CServiceBroker::GetGUI()->GetStereoscopicsManager();
+
+ list.emplace_back(stereoscopicsManager.GetLabelForStereoMode(RENDER_STEREO_MODE_AUTO),
+ RENDER_STEREO_MODE_AUTO); // option for autodetect
+ // don't add "off" to the list of preferred modes as this doesn't make sense
+ for (int i = RENDER_STEREO_MODE_OFF +1; i < RENDER_STEREO_MODE_COUNT; i++)
+ {
+ RENDER_STEREO_MODE mode = (RENDER_STEREO_MODE) i;
+ // also skip "mono" mode which is no real stereoscopic mode
+ if (mode != RENDER_STEREO_MODE_MONO && CServiceBroker::GetRenderSystem()->SupportsStereo(mode))
+ list.emplace_back(stereoscopicsManager.GetLabelForStereoMode(mode), mode);
+ }
+}
+
+void CDisplaySettings::SettingOptionsMonitorsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ auto winSystem = CServiceBroker::GetWinSystem();
+ if (!winSystem)
+ return;
+
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return;
+
+ const std::vector<std::string> monitors = winSystem->GetConnectedOutputs();
+ std::string currentMonitor = settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR);
+
+ bool foundMonitor = false;
+ for (auto const& monitor : monitors)
+ {
+ if(monitor == currentMonitor)
+ {
+ foundMonitor = true;
+ }
+ list.emplace_back(monitor, monitor);
+ }
+
+ if (!foundMonitor && !current.empty())
+ {
+ // Add current value so no monitor change is triggered when entering the settings screen and
+ // the preferred monitor is preserved
+ list.emplace_back(current, current);
+ }
+}
+
+void CDisplaySettings::ClearCustomResolutions()
+{
+ if (m_resolutions.size() > RES_CUSTOM)
+ {
+ std::vector<RESOLUTION_INFO>::iterator firstCustom = m_resolutions.begin()+RES_CUSTOM;
+ m_resolutions.erase(firstCustom, m_resolutions.end());
+ }
+}
+
+void CDisplaySettings::SettingOptionsCmsModesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(36580), CMS_MODE_3DLUT);
+#ifdef HAVE_LCMS2
+ list.emplace_back(g_localizeStrings.Get(36581), CMS_MODE_PROFILE);
+#endif
+}
+
+void CDisplaySettings::SettingOptionsCmsWhitepointsFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(36586), CMS_WHITEPOINT_D65);
+ list.emplace_back(g_localizeStrings.Get(36587), CMS_WHITEPOINT_D93);
+}
+
+void CDisplaySettings::SettingOptionsCmsPrimariesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(36588), CMS_PRIMARIES_AUTO);
+ list.emplace_back(g_localizeStrings.Get(36589), CMS_PRIMARIES_BT709);
+ list.emplace_back(g_localizeStrings.Get(36579), CMS_PRIMARIES_BT2020);
+ list.emplace_back(g_localizeStrings.Get(36590), CMS_PRIMARIES_170M);
+ list.emplace_back(g_localizeStrings.Get(36591), CMS_PRIMARIES_BT470M);
+ list.emplace_back(g_localizeStrings.Get(36592), CMS_PRIMARIES_BT470BG);
+ list.emplace_back(g_localizeStrings.Get(36593), CMS_PRIMARIES_240M);
+}
+
+void CDisplaySettings::SettingOptionsCmsGammaModesFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(36582), CMS_TRC_BT1886);
+ list.emplace_back(g_localizeStrings.Get(36583), CMS_TRC_INPUT_OFFSET);
+ list.emplace_back(g_localizeStrings.Get(36584), CMS_TRC_OUTPUT_OFFSET);
+ list.emplace_back(g_localizeStrings.Get(36585), CMS_TRC_ABSOLUTE);
+}
+
diff --git a/xbmc/settings/DisplaySettings.h b/xbmc/settings/DisplaySettings.h
new file mode 100644
index 0000000..e02e80c
--- /dev/null
+++ b/xbmc/settings/DisplaySettings.h
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2013-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/ISubSettings.h"
+#include "settings/lib/ISettingCallback.h"
+#include "threads/CriticalSection.h"
+#include "utils/Observer.h"
+#include "windowing/Resolution.h"
+
+#include <map>
+#include <set>
+#include <utility>
+#include <vector>
+
+class TiXmlNode;
+struct IntegerSettingOption;
+struct StringSettingOption;
+
+class CDisplaySettings : public ISettingCallback, public ISubSettings,
+ public Observable
+{
+public:
+ static CDisplaySettings& GetInstance();
+
+ bool Load(const TiXmlNode *settings) override;
+ bool Save(TiXmlNode *settings) const override;
+ void Clear() override;
+
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+ bool OnSettingChanging(const std::shared_ptr<const CSetting>& setting) override;
+ bool OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode) override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ /*!
+ \brief Returns the currently active resolution
+
+ This resolution might differ from the display resolution which is based on
+ the user's settings.
+
+ \sa SetCurrentResolution
+ \sa GetResolutionInfo
+ \sa GetDisplayResolution
+ */
+ RESOLUTION GetCurrentResolution() const { return m_currentResolution; }
+ void SetCurrentResolution(RESOLUTION resolution, bool save = false);
+ /*!
+ \brief Returns the best-matching resolution of the videoscreen.screenmode setting value
+
+ This resolution might differ from the current resolution which is based on
+ the properties of the operating system and the attached displays.
+
+ \sa GetCurrentResolution
+ */
+ RESOLUTION GetDisplayResolution() const;
+
+ const RESOLUTION_INFO& GetResolutionInfo(size_t index) const;
+ const RESOLUTION_INFO& GetResolutionInfo(RESOLUTION resolution) const;
+ RESOLUTION_INFO& GetResolutionInfo(size_t index);
+ RESOLUTION_INFO& GetResolutionInfo(RESOLUTION resolution);
+ size_t ResolutionInfoSize() const { return m_resolutions.size(); }
+ void AddResolutionInfo(const RESOLUTION_INFO &resolution);
+
+ const RESOLUTION_INFO& GetCurrentResolutionInfo() const { return GetResolutionInfo(m_currentResolution); }
+ RESOLUTION_INFO& GetCurrentResolutionInfo() { return GetResolutionInfo(m_currentResolution); }
+ RESOLUTION GetResFromString(const std::string &strResolution) { return GetResolutionFromString(strResolution); }
+ std::string GetStringFromRes(const RESOLUTION resolution, float refreshrate = 0.0f) { return GetStringFromResolution(resolution, refreshrate); }
+
+ void ApplyCalibrations();
+ void UpdateCalibrations();
+ void ClearCalibrations();
+ void ClearCustomResolutions();
+
+ float GetZoomAmount() const { return m_zoomAmount; }
+ void SetZoomAmount(float zoomAmount) { m_zoomAmount = zoomAmount; }
+ float GetPixelRatio() const { return m_pixelRatio; }
+ void SetPixelRatio(float pixelRatio) { m_pixelRatio = pixelRatio; }
+ float GetVerticalShift() const { return m_verticalShift; }
+ void SetVerticalShift(float verticalShift) { m_verticalShift = verticalShift; }
+ bool IsNonLinearStretched() const { return m_nonLinearStretched; }
+ void SetNonLinearStretched(bool nonLinearStretch) { m_nonLinearStretched = nonLinearStretch; }
+ void SetMonitor(const std::string& monitor);
+
+ static void SettingOptionsModesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsRefreshChangeDelaysFiller(
+ const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void SettingOptionsRefreshRatesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsResolutionsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void SettingOptionsDispModeFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void SettingOptionsStereoscopicModesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void SettingOptionsPreferredStereoscopicViewModesFiller(
+ const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void SettingOptionsMonitorsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+ static void SettingOptionsCmsModesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void SettingOptionsCmsWhitepointsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void SettingOptionsCmsPrimariesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+ static void SettingOptionsCmsGammaModesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+
+protected:
+ CDisplaySettings();
+ CDisplaySettings(const CDisplaySettings&) = delete;
+ CDisplaySettings& operator=(CDisplaySettings const&) = delete;
+ ~CDisplaySettings() override;
+
+ DisplayMode GetCurrentDisplayMode() const;
+
+ static RESOLUTION GetResolutionFromString(const std::string &strResolution);
+ static std::string GetStringFromResolution(RESOLUTION resolution, float refreshrate = 0.0f);
+ static RESOLUTION GetResolutionForScreen();
+
+ static RESOLUTION FindBestMatchingResolution(const std::map<RESOLUTION, RESOLUTION_INFO> &resolutionInfos, int width, int height, float refreshrate, unsigned int flags);
+
+private:
+ // holds the real gui resolution
+ RESOLUTION m_currentResolution;
+
+ typedef std::vector<RESOLUTION_INFO> ResolutionInfos;
+ ResolutionInfos m_resolutions;
+ ResolutionInfos m_calibrations;
+
+ float m_zoomAmount; // current zoom amount
+ float m_pixelRatio; // current pixel ratio
+ float m_verticalShift; // current vertical shift
+ bool m_nonLinearStretched; // current non-linear stretch
+
+ bool m_resolutionChangeAborted;
+ mutable CCriticalSection m_critical;
+};
diff --git a/xbmc/settings/GameSettings.cpp b/xbmc/settings/GameSettings.cpp
new file mode 100644
index 0000000..77d58ef
--- /dev/null
+++ b/xbmc/settings/GameSettings.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017-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 "GameSettings.h"
+
+using namespace KODI;
+
+CGameSettings &CGameSettings::operator=(const CGameSettings &rhs)
+{
+ if (this != &rhs)
+ {
+ m_videoFilter = rhs.m_videoFilter;
+ m_stretchMode = rhs.m_stretchMode;
+ m_rotationDegCCW = rhs.m_rotationDegCCW;
+ }
+ return *this;
+}
+
+void CGameSettings::Reset()
+{
+ m_videoFilter.clear();
+ m_stretchMode = RETRO::STRETCHMODE::Normal;
+ m_rotationDegCCW = 0;
+}
+
+bool CGameSettings::operator==(const CGameSettings &rhs) const
+{
+ return m_videoFilter == rhs.m_videoFilter &&
+ m_stretchMode == rhs.m_stretchMode &&
+ m_rotationDegCCW == rhs.m_rotationDegCCW;
+}
+
+void CGameSettings::SetVideoFilter(const std::string &videoFilter)
+{
+ if (videoFilter != m_videoFilter)
+ {
+ m_videoFilter = videoFilter;
+ SetChanged();
+ }
+}
+
+void CGameSettings::SetStretchMode(RETRO::STRETCHMODE stretchMode)
+{
+ if (stretchMode != m_stretchMode)
+ {
+ m_stretchMode = stretchMode;
+ SetChanged();
+ }
+}
+
+void CGameSettings::SetRotationDegCCW(unsigned int rotation)
+{
+ if (rotation != m_rotationDegCCW)
+ {
+ m_rotationDegCCW = rotation;
+ SetChanged();
+ }
+}
diff --git a/xbmc/settings/GameSettings.h b/xbmc/settings/GameSettings.h
new file mode 100644
index 0000000..f4c6424
--- /dev/null
+++ b/xbmc/settings/GameSettings.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017-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/GameSettings.h"
+#include "utils/Observer.h"
+
+#include <string>
+
+class CGameSettings : public Observable
+{
+public:
+ CGameSettings() { Reset(); }
+ CGameSettings(const CGameSettings &other) { *this = other; }
+
+ CGameSettings &operator=(const CGameSettings &rhs);
+
+ // Restore game settings to default
+ void Reset();
+
+ bool operator==(const CGameSettings &rhs) const;
+ bool operator!=(const CGameSettings &rhs) const { return !(*this == rhs); }
+
+ const std::string &VideoFilter() const { return m_videoFilter; }
+ void SetVideoFilter(const std::string &videoFilter);
+
+ KODI::RETRO::STRETCHMODE StretchMode() const { return m_stretchMode; }
+ void SetStretchMode(KODI::RETRO::STRETCHMODE stretchMode);
+
+ unsigned int RotationDegCCW() const { return m_rotationDegCCW; }
+ void SetRotationDegCCW(unsigned int rotation);
+
+private:
+ // Video settings
+ std::string m_videoFilter;
+ KODI::RETRO::STRETCHMODE m_stretchMode;
+ unsigned int m_rotationDegCCW;
+};
diff --git a/xbmc/settings/ISubSettings.h b/xbmc/settings/ISubSettings.h
new file mode 100644
index 0000000..4cc2456
--- /dev/null
+++ b/xbmc/settings/ISubSettings.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013-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 TiXmlNode;
+
+/*!
+ \ingroup settings
+ \brief Interface defining methods to load additional setting values from an
+ XML file being loaded by the settings system.
+ */
+class ISubSettings
+{
+public:
+ virtual ~ISubSettings() = default;
+
+ /*!
+ \brief Load settings from the given XML node.
+
+ \param settings XML node containing setting values
+ \return True if loading the settings was successful, false otherwise.
+ */
+ virtual bool Load(const TiXmlNode *settings) { return true; }
+ /*!
+ \brief Save settings to the given XML node.
+
+ \param settings XML node in which the settings will be saved
+ \return True if saving the settings was successful, false otherwise.
+ */
+ virtual bool Save(TiXmlNode *settings) const { return true; }
+ /*!
+ \brief Clear any loaded setting values.
+ */
+ virtual void Clear() { }
+};
diff --git a/xbmc/settings/LibExportSettings.cpp b/xbmc/settings/LibExportSettings.cpp
new file mode 100644
index 0000000..15be272
--- /dev/null
+++ b/xbmc/settings/LibExportSettings.cpp
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017-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.
+ */
+// LibExportSettings.cpp: implementation of the CLibExportSettings class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "LibExportSettings.h"
+
+//////////////////////////////////////////////////////////////////////
+// Construction/Destruction
+//////////////////////////////////////////////////////////////////////
+
+CLibExportSettings::CLibExportSettings()
+{
+ m_exporttype = ELIBEXPORT_SINGLEFILE;
+ m_itemstoexport = ELIBEXPORT_ALBUMS + ELIBEXPORT_ALBUMARTISTS;
+ m_overwrite = false;
+ m_artwork = false;
+ m_unscraped = false;
+ m_skipnfo = false;
+}
+
+bool CLibExportSettings::operator!=(const CLibExportSettings &right) const
+{
+ if (m_exporttype != right.m_exporttype)
+ return true;
+ if (m_strPath != right.m_strPath)
+ return true;
+ if (m_overwrite != right.m_overwrite)
+ return true;
+ if (m_itemstoexport != right.m_itemstoexport)
+ return true;
+
+ if (m_artwork != right.m_artwork)
+ return true;
+ if (m_unscraped != right.m_unscraped)
+ return true;
+ if (m_skipnfo != right.m_skipnfo)
+ return true;
+
+ return false;
+}
+
+bool CLibExportSettings::IsItemExported(ELIBEXPORTOPTIONS item) const
+{
+ return (m_itemstoexport & item);
+}
+
+bool CLibExportSettings::IsArtists() const
+{
+ return (m_itemstoexport & ELIBEXPORT_ALBUMARTISTS) ||
+ (m_itemstoexport & ELIBEXPORT_SONGARTISTS) ||
+ (m_itemstoexport & ELIBEXPORT_OTHERARTISTS);
+}
+
+std::vector<int> CLibExportSettings::GetExportItems() const
+{
+ std::vector<int> values;
+ if (IsItemExported(ELIBEXPORT_ALBUMS))
+ values.emplace_back(ELIBEXPORT_ALBUMS);
+ if (IsItemExported(ELIBEXPORT_ALBUMARTISTS))
+ values.emplace_back(ELIBEXPORT_ALBUMARTISTS);
+ if (IsItemExported(ELIBEXPORT_SONGARTISTS))
+ values.emplace_back(ELIBEXPORT_SONGARTISTS);
+ if (IsItemExported(ELIBEXPORT_OTHERARTISTS))
+ values.emplace_back(ELIBEXPORT_OTHERARTISTS);
+ if (IsItemExported(ELIBEXPORT_ACTORTHUMBS))
+ values.emplace_back(ELIBEXPORT_ACTORTHUMBS);
+ if (IsItemExported(ELIBEXPORT_SONGS))
+ values.emplace_back(ELIBEXPORT_SONGS);
+ return values;
+}
+
+std::vector<int> CLibExportSettings::GetLimitedItems(int items) const
+{
+ std::vector<int> values;
+ if (IsItemExported(ELIBEXPORT_ALBUMS) && (items & ELIBEXPORT_ALBUMS))
+ values.emplace_back(ELIBEXPORT_ALBUMS);
+ if (IsItemExported(ELIBEXPORT_ALBUMARTISTS) && (items & ELIBEXPORT_ALBUMARTISTS))
+ values.emplace_back(ELIBEXPORT_ALBUMARTISTS);
+ if (IsItemExported(ELIBEXPORT_SONGARTISTS) && (items & ELIBEXPORT_SONGARTISTS))
+ values.emplace_back(ELIBEXPORT_SONGARTISTS);
+ if (IsItemExported(ELIBEXPORT_OTHERARTISTS) && (items & ELIBEXPORT_OTHERARTISTS))
+ values.emplace_back(ELIBEXPORT_OTHERARTISTS);
+ if (IsItemExported(ELIBEXPORT_ACTORTHUMBS) && (items & ELIBEXPORT_ACTORTHUMBS))
+ values.emplace_back(ELIBEXPORT_ACTORTHUMBS);
+ if (IsItemExported(ELIBEXPORT_SONGS) && (items & ELIBEXPORT_SONGS))
+ values.emplace_back(ELIBEXPORT_SONGS);
+ return values;
+}
+
+bool CLibExportSettings::IsSingleFile() const
+{
+ return (m_exporttype == ELIBEXPORT_SINGLEFILE);
+}
+
+bool CLibExportSettings::IsSeparateFiles() const
+{
+ return (m_exporttype == ELIBEXPORT_SEPARATEFILES);
+}
+
+bool CLibExportSettings::IsToLibFolders() const
+{
+ return (m_exporttype == ELIBEXPORT_TOLIBRARYFOLDER);
+}
+
+bool CLibExportSettings::IsArtistFoldersOnly() const
+{
+ return (m_exporttype == ELIBEXPORT_ARTISTFOLDERS);
+}
diff --git a/xbmc/settings/LibExportSettings.h b/xbmc/settings/LibExportSettings.h
new file mode 100644
index 0000000..c7395fb
--- /dev/null
+++ b/xbmc/settings/LibExportSettings.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017-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
+
+// LibExportSettings.h: interface for the CLibExportSettings class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "settings/lib/Setting.h"
+
+#include <string>
+
+// Enumeration of library export options (possibly OR'd together)
+enum ELIBEXPORTOPTIONS
+{
+ ELIBEXPORT_SINGLEFILE = 0x0000,
+ ELIBEXPORT_SEPARATEFILES = 0x0001,
+ ELIBEXPORT_TOLIBRARYFOLDER = 0x0002,
+ ELIBEXPORT_OVERWRITE = 0x0004,
+ ELIBEXPORT_UNSCRAPED = 0x0008,
+ ELIBEXPORT_ALBUMS = 0x0010,
+ ELIBEXPORT_ALBUMARTISTS = 0x0020,
+ ELIBEXPORT_SONGARTISTS = 0x0040,
+ ELIBEXPORT_OTHERARTISTS = 0x0080,
+ ELIBEXPORT_ARTWORK = 0x0100,
+ ELIBEXPORT_NFOFILES = 0x0200,
+ ELIBEXPORT_ACTORTHUMBS = 0x0400,
+ ELIBEXPORT_ARTISTFOLDERS = 0x0800,
+ ELIBEXPORT_SONGS = 0x1000
+};
+
+class CLibExportSettings
+{
+public:
+ CLibExportSettings();
+ ~CLibExportSettings() = default;
+
+ bool operator!=(const CLibExportSettings &right) const;
+ bool IsItemExported(ELIBEXPORTOPTIONS item) const;
+ bool IsArtists() const;
+ std::vector<int> GetExportItems() const;
+ std::vector<int> GetLimitedItems(int items) const;
+ void ClearItems() { m_itemstoexport = 0; }
+ void AddItem(ELIBEXPORTOPTIONS item) { m_itemstoexport += item; }
+ unsigned int GetItemsToExport() { return m_itemstoexport; }
+ void SetItemsToExport(int itemstoexport) { m_itemstoexport = static_cast<unsigned int>(itemstoexport); }
+ unsigned int GetExportType() { return m_exporttype; }
+ void SetExportType(int exporttype) { m_exporttype = static_cast<unsigned int>(exporttype); }
+ bool IsSingleFile() const;
+ bool IsSeparateFiles() const;
+ bool IsToLibFolders() const;
+ bool IsArtistFoldersOnly() const;
+
+ std::string m_strPath;
+ bool m_overwrite;
+ bool m_artwork;
+ bool m_unscraped;
+ bool m_skipnfo;
+private:
+ unsigned int m_exporttype; //singlefile, separate files, to library folder
+ unsigned int m_itemstoexport;
+};
diff --git a/xbmc/settings/MediaSettings.cpp b/xbmc/settings/MediaSettings.cpp
new file mode 100644
index 0000000..09aad05
--- /dev/null
+++ b/xbmc/settings/MediaSettings.cpp
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2013-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 "MediaSettings.h"
+
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "cores/RetroPlayer/RetroPlayerUtils.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/AnnouncementManager.h"
+#include "interfaces/builtins/Builtins.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicLibraryQueue.h"
+#include "settings/Settings.h"
+#include "settings/dialogs/GUIDialogLibExportSettings.h"
+#include "settings/lib/Setting.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoLibraryQueue.h"
+
+#include <limits.h>
+#include <mutex>
+#include <string>
+
+using namespace KODI;
+using namespace KODI::MESSAGING;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+CMediaSettings::CMediaSettings()
+{
+ m_watchedModes["files"] = WatchedModeAll;
+ m_watchedModes["movies"] = WatchedModeAll;
+ m_watchedModes["tvshows"] = WatchedModeAll;
+ m_watchedModes["musicvideos"] = WatchedModeAll;
+ m_watchedModes["recordings"] = WatchedModeAll;
+
+ m_musicPlaylistRepeat = false;
+ m_musicPlaylistShuffle = false;
+ m_videoPlaylistRepeat = false;
+ m_videoPlaylistShuffle = false;
+
+ m_mediaStartWindowed = false;
+ m_additionalSubtitleDirectoryChecked = 0;
+
+ m_musicNeedsUpdate = 0;
+ m_videoNeedsUpdate = 0;
+}
+
+CMediaSettings::~CMediaSettings() = default;
+
+CMediaSettings& CMediaSettings::GetInstance()
+{
+ static CMediaSettings sMediaSettings;
+ return sMediaSettings;
+}
+
+bool CMediaSettings::Load(const TiXmlNode *settings)
+{
+ if (settings == NULL)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ const TiXmlElement *pElement = settings->FirstChildElement("defaultvideosettings");
+ if (pElement != NULL)
+ {
+ int interlaceMethod;
+ XMLUtils::GetInt(pElement, "interlacemethod", interlaceMethod, VS_INTERLACEMETHOD_NONE, VS_INTERLACEMETHOD_MAX);
+
+ m_defaultVideoSettings.m_InterlaceMethod = (EINTERLACEMETHOD)interlaceMethod;
+ int scalingMethod;
+ if (!XMLUtils::GetInt(pElement, "scalingmethod", scalingMethod, VS_SCALINGMETHOD_NEAREST, VS_SCALINGMETHOD_MAX))
+ scalingMethod = (int)VS_SCALINGMETHOD_LINEAR;
+ m_defaultVideoSettings.m_ScalingMethod = (ESCALINGMETHOD)scalingMethod;
+
+ XMLUtils::GetInt(pElement, "viewmode", m_defaultVideoSettings.m_ViewMode, ViewModeNormal, ViewModeZoom110Width);
+ if (!XMLUtils::GetFloat(pElement, "zoomamount", m_defaultVideoSettings.m_CustomZoomAmount, 0.5f, 2.0f))
+ m_defaultVideoSettings.m_CustomZoomAmount = 1.0f;
+ if (!XMLUtils::GetFloat(pElement, "pixelratio", m_defaultVideoSettings.m_CustomPixelRatio, 0.5f, 2.0f))
+ m_defaultVideoSettings.m_CustomPixelRatio = 1.0f;
+ if (!XMLUtils::GetFloat(pElement, "verticalshift", m_defaultVideoSettings.m_CustomVerticalShift, -2.0f, 2.0f))
+ m_defaultVideoSettings.m_CustomVerticalShift = 0.0f;
+ if (!XMLUtils::GetFloat(pElement, "volumeamplification", m_defaultVideoSettings.m_VolumeAmplification, VOLUME_DRC_MINIMUM * 0.01f, VOLUME_DRC_MAXIMUM * 0.01f))
+ m_defaultVideoSettings.m_VolumeAmplification = VOLUME_DRC_MINIMUM * 0.01f;
+ if (!XMLUtils::GetFloat(pElement, "noisereduction", m_defaultVideoSettings.m_NoiseReduction, 0.0f, 1.0f))
+ m_defaultVideoSettings.m_NoiseReduction = 0.0f;
+ XMLUtils::GetBoolean(pElement, "postprocess", m_defaultVideoSettings.m_PostProcess);
+ if (!XMLUtils::GetFloat(pElement, "sharpness", m_defaultVideoSettings.m_Sharpness, -1.0f, 1.0f))
+ m_defaultVideoSettings.m_Sharpness = 0.0f;
+ XMLUtils::GetBoolean(pElement, "showsubtitles", m_defaultVideoSettings.m_SubtitleOn);
+ if (!XMLUtils::GetFloat(pElement, "brightness", m_defaultVideoSettings.m_Brightness, 0, 100))
+ m_defaultVideoSettings.m_Brightness = 50;
+ if (!XMLUtils::GetFloat(pElement, "contrast", m_defaultVideoSettings.m_Contrast, 0, 100))
+ m_defaultVideoSettings.m_Contrast = 50;
+ if (!XMLUtils::GetFloat(pElement, "gamma", m_defaultVideoSettings.m_Gamma, 0, 100))
+ m_defaultVideoSettings.m_Gamma = 20;
+ if (!XMLUtils::GetFloat(pElement, "audiodelay", m_defaultVideoSettings.m_AudioDelay, -10.0f, 10.0f))
+ m_defaultVideoSettings.m_AudioDelay = 0.0f;
+ if (!XMLUtils::GetFloat(pElement, "subtitledelay", m_defaultVideoSettings.m_SubtitleDelay, -10.0f, 10.0f))
+ m_defaultVideoSettings.m_SubtitleDelay = 0.0f;
+ XMLUtils::GetBoolean(pElement, "nonlinstretch", m_defaultVideoSettings.m_CustomNonLinStretch);
+ if (!XMLUtils::GetInt(pElement, "stereomode", m_defaultVideoSettings.m_StereoMode))
+ m_defaultVideoSettings.m_StereoMode = 0;
+ if (!XMLUtils::GetInt(pElement, "centermixlevel", m_defaultVideoSettings.m_CenterMixLevel))
+ m_defaultVideoSettings.m_CenterMixLevel = 0;
+
+ int toneMapMethod;
+ if (!XMLUtils::GetInt(pElement, "tonemapmethod", toneMapMethod, VS_TONEMAPMETHOD_OFF,
+ VS_TONEMAPMETHOD_MAX))
+ toneMapMethod = VS_TONEMAPMETHOD_REINHARD;
+ m_defaultVideoSettings.m_ToneMapMethod = static_cast<ETONEMAPMETHOD>(toneMapMethod);
+
+ if (!XMLUtils::GetFloat(pElement, "tonemapparam", m_defaultVideoSettings.m_ToneMapParam, 0.1f, 5.0f))
+ m_defaultVideoSettings.m_ToneMapParam = 1.0f;
+ }
+
+ m_defaultGameSettings.Reset();
+ pElement = settings->FirstChildElement("defaultgamesettings");
+ if (pElement != nullptr)
+ {
+ std::string videoFilter;
+ if (XMLUtils::GetString(pElement, "videofilter", videoFilter))
+ m_defaultGameSettings.SetVideoFilter(videoFilter);
+
+ std::string stretchMode;
+ if (XMLUtils::GetString(pElement, "stretchmode", stretchMode))
+ {
+ RETRO::STRETCHMODE sm = RETRO::CRetroPlayerUtils::IdentifierToStretchMode(stretchMode);
+ m_defaultGameSettings.SetStretchMode(sm);
+ }
+
+ int rotation;
+ if (XMLUtils::GetInt(pElement, "rotation", rotation, 0, 270) && rotation >= 0)
+ m_defaultGameSettings.SetRotationDegCCW(static_cast<unsigned int>(rotation));
+ }
+
+ // mymusic settings
+ pElement = settings->FirstChildElement("mymusic");
+ if (pElement != NULL)
+ {
+ const TiXmlElement *pChild = pElement->FirstChildElement("playlist");
+ if (pChild != NULL)
+ {
+ XMLUtils::GetBoolean(pChild, "repeat", m_musicPlaylistRepeat);
+ XMLUtils::GetBoolean(pChild, "shuffle", m_musicPlaylistShuffle);
+ }
+ if (!XMLUtils::GetInt(pElement, "needsupdate", m_musicNeedsUpdate, 0, INT_MAX))
+ m_musicNeedsUpdate = 0;
+ }
+
+ // Set music playlist player repeat and shuffle from loaded settings
+ if (m_musicPlaylistRepeat)
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_MUSIC, PLAYLIST::RepeatState::ALL);
+ else
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_MUSIC,
+ PLAYLIST::RepeatState::NONE);
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(PLAYLIST::TYPE_MUSIC, m_musicPlaylistShuffle);
+
+ // Read the watchmode settings for the various media views
+ pElement = settings->FirstChildElement("myvideos");
+ if (pElement != NULL)
+ {
+ int tmp;
+ if (XMLUtils::GetInt(pElement, "watchmodemovies", tmp, (int)WatchedModeAll, (int)WatchedModeWatched))
+ m_watchedModes["movies"] = (WatchedMode)tmp;
+ if (XMLUtils::GetInt(pElement, "watchmodetvshows", tmp, (int)WatchedModeAll, (int)WatchedModeWatched))
+ m_watchedModes["tvshows"] = (WatchedMode)tmp;
+ if (XMLUtils::GetInt(pElement, "watchmodemusicvideos", tmp, (int)WatchedModeAll, (int)WatchedModeWatched))
+ m_watchedModes["musicvideos"] = (WatchedMode)tmp;
+ if (XMLUtils::GetInt(pElement, "watchmoderecordings", tmp, static_cast<int>(WatchedModeAll), static_cast<int>(WatchedModeWatched)))
+ m_watchedModes["recordings"] = static_cast<WatchedMode>(tmp);
+
+ const TiXmlElement *pChild = pElement->FirstChildElement("playlist");
+ if (pChild != NULL)
+ {
+ XMLUtils::GetBoolean(pChild, "repeat", m_videoPlaylistRepeat);
+ XMLUtils::GetBoolean(pChild, "shuffle", m_videoPlaylistShuffle);
+ }
+ if (!XMLUtils::GetInt(pElement, "needsupdate", m_videoNeedsUpdate, 0, INT_MAX))
+ m_videoNeedsUpdate = 0;
+ }
+
+ // Set video playlist player repeat and shuffle from loaded settings
+ if (m_videoPlaylistRepeat)
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_VIDEO, PLAYLIST::RepeatState::ALL);
+ else
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_VIDEO,
+ PLAYLIST::RepeatState::NONE);
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(PLAYLIST::TYPE_VIDEO, m_videoPlaylistShuffle);
+
+ return true;
+}
+
+bool CMediaSettings::Save(TiXmlNode *settings) const
+{
+ if (settings == NULL)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // default video settings
+ TiXmlElement videoSettingsNode("defaultvideosettings");
+ TiXmlNode *pNode = settings->InsertEndChild(videoSettingsNode);
+ if (pNode == NULL)
+ return false;
+
+ XMLUtils::SetInt(pNode, "interlacemethod", m_defaultVideoSettings.m_InterlaceMethod);
+ XMLUtils::SetInt(pNode, "scalingmethod", m_defaultVideoSettings.m_ScalingMethod);
+ XMLUtils::SetFloat(pNode, "noisereduction", m_defaultVideoSettings.m_NoiseReduction);
+ XMLUtils::SetBoolean(pNode, "postprocess", m_defaultVideoSettings.m_PostProcess);
+ XMLUtils::SetFloat(pNode, "sharpness", m_defaultVideoSettings.m_Sharpness);
+ XMLUtils::SetInt(pNode, "viewmode", m_defaultVideoSettings.m_ViewMode);
+ XMLUtils::SetFloat(pNode, "zoomamount", m_defaultVideoSettings.m_CustomZoomAmount);
+ XMLUtils::SetFloat(pNode, "pixelratio", m_defaultVideoSettings.m_CustomPixelRatio);
+ XMLUtils::SetFloat(pNode, "verticalshift", m_defaultVideoSettings.m_CustomVerticalShift);
+ XMLUtils::SetFloat(pNode, "volumeamplification", m_defaultVideoSettings.m_VolumeAmplification);
+ XMLUtils::SetBoolean(pNode, "showsubtitles", m_defaultVideoSettings.m_SubtitleOn);
+ XMLUtils::SetFloat(pNode, "brightness", m_defaultVideoSettings.m_Brightness);
+ XMLUtils::SetFloat(pNode, "contrast", m_defaultVideoSettings.m_Contrast);
+ XMLUtils::SetFloat(pNode, "gamma", m_defaultVideoSettings.m_Gamma);
+ XMLUtils::SetFloat(pNode, "audiodelay", m_defaultVideoSettings.m_AudioDelay);
+ XMLUtils::SetFloat(pNode, "subtitledelay", m_defaultVideoSettings.m_SubtitleDelay);
+ XMLUtils::SetBoolean(pNode, "nonlinstretch", m_defaultVideoSettings.m_CustomNonLinStretch);
+ XMLUtils::SetInt(pNode, "stereomode", m_defaultVideoSettings.m_StereoMode);
+ XMLUtils::SetInt(pNode, "centermixlevel", m_defaultVideoSettings.m_CenterMixLevel);
+ XMLUtils::SetInt(pNode, "tonemapmethod", m_defaultVideoSettings.m_ToneMapMethod);
+ XMLUtils::SetFloat(pNode, "tonemapparam", m_defaultVideoSettings.m_ToneMapParam);
+
+ // default audio settings for dsp addons
+ TiXmlElement audioSettingsNode("defaultaudiosettings");
+ pNode = settings->InsertEndChild(audioSettingsNode);
+ if (pNode == NULL)
+ return false;
+
+ // Default game settings
+ TiXmlElement gameSettingsNode("defaultgamesettings");
+ pNode = settings->InsertEndChild(gameSettingsNode);
+ if (pNode == nullptr)
+ return false;
+
+ XMLUtils::SetString(pNode, "videofilter", m_defaultGameSettings.VideoFilter());
+ std::string sm = RETRO::CRetroPlayerUtils::StretchModeToIdentifier(m_defaultGameSettings.StretchMode());
+ XMLUtils::SetString(pNode, "stretchmode", sm);
+ XMLUtils::SetInt(pNode, "rotation", m_defaultGameSettings.RotationDegCCW());
+
+ // mymusic
+ pNode = settings->FirstChild("mymusic");
+ if (pNode == NULL)
+ {
+ TiXmlElement videosNode("mymusic");
+ pNode = settings->InsertEndChild(videosNode);
+ if (pNode == NULL)
+ return false;
+ }
+
+ TiXmlElement musicPlaylistNode("playlist");
+ TiXmlNode *playlistNode = pNode->InsertEndChild(musicPlaylistNode);
+ if (playlistNode == NULL)
+ return false;
+ XMLUtils::SetBoolean(playlistNode, "repeat", m_musicPlaylistRepeat);
+ XMLUtils::SetBoolean(playlistNode, "shuffle", m_musicPlaylistShuffle);
+
+ XMLUtils::SetInt(pNode, "needsupdate", m_musicNeedsUpdate);
+
+ // myvideos
+ pNode = settings->FirstChild("myvideos");
+ if (pNode == NULL)
+ {
+ TiXmlElement videosNode("myvideos");
+ pNode = settings->InsertEndChild(videosNode);
+ if (pNode == NULL)
+ return false;
+ }
+
+ XMLUtils::SetInt(pNode, "watchmodemovies", m_watchedModes.find("movies")->second);
+ XMLUtils::SetInt(pNode, "watchmodetvshows", m_watchedModes.find("tvshows")->second);
+ XMLUtils::SetInt(pNode, "watchmodemusicvideos", m_watchedModes.find("musicvideos")->second);
+ XMLUtils::SetInt(pNode, "watchmoderecordings", m_watchedModes.find("recordings")->second);
+
+ TiXmlElement videoPlaylistNode("playlist");
+ playlistNode = pNode->InsertEndChild(videoPlaylistNode);
+ if (playlistNode == NULL)
+ return false;
+ XMLUtils::SetBoolean(playlistNode, "repeat", m_videoPlaylistRepeat);
+ XMLUtils::SetBoolean(playlistNode, "shuffle", m_videoPlaylistShuffle);
+
+ XMLUtils::SetInt(pNode, "needsupdate", m_videoNeedsUpdate);
+
+ return true;
+}
+
+void CMediaSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_MUSICLIBRARY_CLEANUP)
+ {
+ if (HELPERS::ShowYesNoDialogText(CVariant{313}, CVariant{333}) == DialogResponse::CHOICE_YES)
+ {
+ if (CMusicLibraryQueue::GetInstance().IsRunning())
+ HELPERS::ShowOKDialogText(CVariant{700}, CVariant{703});
+ else
+ CMusicLibraryQueue::GetInstance().CleanLibrary(true);
+ }
+ }
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_EXPORT)
+ {
+ CLibExportSettings m_musicExportSettings;
+ if (CGUIDialogLibExportSettings::Show(m_musicExportSettings))
+ {
+ // Export music library showing progress dialog
+ CMusicLibraryQueue::GetInstance().ExportLibrary(m_musicExportSettings, true);
+ }
+ }
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_IMPORT)
+ {
+ std::string path;
+ VECSOURCES shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ CServiceBroker::GetMediaManager().GetNetworkLocations(shares);
+ CServiceBroker::GetMediaManager().GetRemovableDrives(shares);
+
+ if (CGUIDialogFileBrowser::ShowAndGetFile(shares, "musicdb.xml", g_localizeStrings.Get(651) , path))
+ {
+ // Import data to music library showing progress dialog
+ CMusicLibraryQueue::GetInstance().ImportLibrary(path, true);
+ }
+ }
+ else if (settingId == CSettings::SETTING_VIDEOLIBRARY_CLEANUP)
+ {
+ if (HELPERS::ShowYesNoDialogText(CVariant{313}, CVariant{333}) == DialogResponse::CHOICE_YES)
+ {
+ if (!CVideoLibraryQueue::GetInstance().CleanLibraryModal())
+ HELPERS::ShowOKDialogText(CVariant{700}, CVariant{703});
+ }
+ }
+ else if (settingId == CSettings::SETTING_VIDEOLIBRARY_EXPORT)
+ CBuiltins::GetInstance().Execute("exportlibrary(video)");
+ else if (settingId == CSettings::SETTING_VIDEOLIBRARY_IMPORT)
+ {
+ std::string path;
+ VECSOURCES shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ CServiceBroker::GetMediaManager().GetNetworkLocations(shares);
+ CServiceBroker::GetMediaManager().GetRemovableDrives(shares);
+
+ if (CGUIDialogFileBrowser::ShowAndGetDirectory(shares, g_localizeStrings.Get(651) , path))
+ {
+ CVideoDatabase videodatabase;
+ videodatabase.Open();
+ videodatabase.ImportFromXML(path);
+ videodatabase.Close();
+ }
+ }
+}
+
+void CMediaSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ if (setting->GetId() == CSettings::SETTING_VIDEOLIBRARY_SHOWUNWATCHEDPLOTS)
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnRefresh");
+}
+
+int CMediaSettings::GetWatchedMode(const std::string &content) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ WatchedModes::const_iterator it = m_watchedModes.find(GetWatchedContent(content));
+ if (it != m_watchedModes.end())
+ return it->second;
+
+ return WatchedModeAll;
+}
+
+void CMediaSettings::SetWatchedMode(const std::string &content, WatchedMode mode)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ WatchedModes::iterator it = m_watchedModes.find(GetWatchedContent(content));
+ if (it != m_watchedModes.end())
+ it->second = mode;
+}
+
+void CMediaSettings::CycleWatchedMode(const std::string &content)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ WatchedModes::iterator it = m_watchedModes.find(GetWatchedContent(content));
+ if (it != m_watchedModes.end())
+ {
+ it->second = (WatchedMode)((int)it->second + 1);
+ if (it->second > WatchedModeWatched)
+ it->second = WatchedModeAll;
+ }
+}
+
+std::string CMediaSettings::GetWatchedContent(const std::string &content)
+{
+ if (content == "seasons" || content == "episodes")
+ return "tvshows";
+
+ return content;
+}
diff --git a/xbmc/settings/MediaSettings.h b/xbmc/settings/MediaSettings.h
new file mode 100644
index 0000000..d239234
--- /dev/null
+++ b/xbmc/settings/MediaSettings.h
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2013-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/VideoSettings.h"
+#include "settings/GameSettings.h"
+#include "settings/ISubSettings.h"
+#include "settings/LibExportSettings.h"
+#include "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <string>
+
+#define VOLUME_DRC_MINIMUM 0 // 0dB
+#define VOLUME_DRC_MAXIMUM 6000 // 60dB
+
+class TiXmlNode;
+
+// Step used to increase/decrease audio delay
+static constexpr float AUDIO_DELAY_STEP = 0.025f;
+
+typedef enum {
+ WatchedModeAll = 0,
+ WatchedModeUnwatched,
+ WatchedModeWatched
+} WatchedMode;
+
+class CMediaSettings : public ISettingCallback, public ISettingsHandler, public ISubSettings
+{
+public:
+ static CMediaSettings& GetInstance();
+
+ bool Load(const TiXmlNode *settings) override;
+ bool Save(TiXmlNode *settings) const override;
+
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ const CVideoSettings& GetDefaultVideoSettings() const { return m_defaultVideoSettings; }
+ CVideoSettings& GetDefaultVideoSettings() { return m_defaultVideoSettings; }
+
+ const CGameSettings& GetDefaultGameSettings() const { return m_defaultGameSettings; }
+ CGameSettings& GetDefaultGameSettings() { return m_defaultGameSettings; }
+ const CGameSettings& GetCurrentGameSettings() const { return m_currentGameSettings; }
+ CGameSettings& GetCurrentGameSettings() { return m_currentGameSettings; }
+
+ /*! \brief Retrieve the watched mode for the given content type
+ \param content Current content type
+ \return the current watch mode for this content type, WATCH_MODE_ALL if the content type is unknown.
+ \sa SetWatchMode
+ */
+ int GetWatchedMode(const std::string &content) const;
+
+ /*! \brief Set the watched mode for the given content type
+ \param content Current content type
+ \param value Watched mode to set
+ \sa GetWatchMode
+ */
+ void SetWatchedMode(const std::string &content, WatchedMode mode);
+
+ /*! \brief Cycle the watched mode for the given content type
+ \param content Current content type
+ \sa GetWatchMode, SetWatchMode
+ */
+ void CycleWatchedMode(const std::string &content);
+
+ void SetMusicPlaylistRepeat(bool repeats) { m_musicPlaylistRepeat = repeats; }
+ void SetMusicPlaylistShuffled(bool shuffled) { m_musicPlaylistShuffle = shuffled; }
+
+ void SetVideoPlaylistRepeat(bool repeats) { m_videoPlaylistRepeat = repeats; }
+ void SetVideoPlaylistShuffled(bool shuffled) { m_videoPlaylistShuffle = shuffled; }
+
+ bool DoesMediaStartWindowed() const { return m_mediaStartWindowed; }
+ void SetMediaStartWindowed(bool windowed) { m_mediaStartWindowed = windowed; }
+ int GetAdditionalSubtitleDirectoryChecked() const { return m_additionalSubtitleDirectoryChecked; }
+ void SetAdditionalSubtitleDirectoryChecked(int checked) { m_additionalSubtitleDirectoryChecked = checked; }
+
+ int GetMusicNeedsUpdate() const { return m_musicNeedsUpdate; }
+ void SetMusicNeedsUpdate(int version) { m_musicNeedsUpdate = version; }
+ int GetVideoNeedsUpdate() const { return m_videoNeedsUpdate; }
+ void SetVideoNeedsUpdate(int version) { m_videoNeedsUpdate = version; }
+
+protected:
+ CMediaSettings();
+ CMediaSettings(const CMediaSettings&) = delete;
+ CMediaSettings& operator=(CMediaSettings const&) = delete;
+ ~CMediaSettings() override;
+
+ static std::string GetWatchedContent(const std::string &content);
+
+private:
+ CVideoSettings m_defaultVideoSettings;
+
+ CGameSettings m_defaultGameSettings;
+ CGameSettings m_currentGameSettings;
+
+ typedef std::map<std::string, WatchedMode> WatchedModes;
+ WatchedModes m_watchedModes;
+
+ bool m_musicPlaylistRepeat;
+ bool m_musicPlaylistShuffle;
+ bool m_videoPlaylistRepeat;
+ bool m_videoPlaylistShuffle;
+
+ bool m_mediaStartWindowed;
+ int m_additionalSubtitleDirectoryChecked;
+
+ int m_musicNeedsUpdate; ///< if a database update means an update is required (set to the version number of the db)
+ int m_videoNeedsUpdate; ///< if a database update means an update is required (set to the version number of the db)
+
+ mutable CCriticalSection m_critical;
+};
diff --git a/xbmc/settings/MediaSourceSettings.cpp b/xbmc/settings/MediaSourceSettings.cpp
new file mode 100644
index 0000000..183131f
--- /dev/null
+++ b/xbmc/settings/MediaSourceSettings.cpp
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2013-2020 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 "MediaSourceSettings.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "media/MediaLockState.h"
+#include "network/WakeOnAccess.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <cstdlib>
+#include <string>
+
+#define SOURCES_FILE "sources.xml"
+#define XML_SOURCES "sources"
+#define XML_SOURCE "source"
+
+CMediaSourceSettings::CMediaSourceSettings()
+{
+ Clear();
+}
+
+CMediaSourceSettings::~CMediaSourceSettings() = default;
+
+CMediaSourceSettings& CMediaSourceSettings::GetInstance()
+{
+ static CMediaSourceSettings sMediaSourceSettings;
+ return sMediaSourceSettings;
+}
+
+std::string CMediaSourceSettings::GetSourcesFile()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::string file;
+ if (profileManager->GetCurrentProfile().hasSources())
+ file = profileManager->GetProfileUserDataFolder();
+ else
+ file = profileManager->GetUserDataFolder();
+
+ return URIUtils::AddFileToFolder(file, SOURCES_FILE);
+}
+
+void CMediaSourceSettings::OnSettingsLoaded()
+{
+ Load();
+}
+
+void CMediaSourceSettings::OnSettingsUnloaded()
+{
+ Clear();
+}
+
+bool CMediaSourceSettings::Load()
+{
+ return Load(GetSourcesFile());
+}
+
+bool CMediaSourceSettings::Load(const std::string &file)
+{
+ Clear();
+
+ if (!CFileUtils::Exists(file))
+ return false;
+
+ CLog::Log(LOGINFO, "CMediaSourceSettings: loading media sources from {}", file);
+
+ // load xml file
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(file))
+ {
+ CLog::Log(LOGERROR, "CMediaSourceSettings: error loading {}: Line {}, {}", file,
+ xmlDoc.ErrorRow(), xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ TiXmlElement *pRootElement = xmlDoc.RootElement();
+ if (pRootElement == NULL || !StringUtils::EqualsNoCase(pRootElement->ValueStr(), XML_SOURCES))
+ CLog::Log(LOGERROR, "CMediaSourceSettings: sources.xml file does not contain <sources>");
+
+ // parse sources
+ std::string dummy;
+ GetSources(pRootElement, "video", m_videoSources, dummy);
+ GetSources(pRootElement, "programs", m_programSources, m_defaultProgramSource);
+ GetSources(pRootElement, "pictures", m_pictureSources, m_defaultPictureSource);
+ GetSources(pRootElement, "files", m_fileSources, m_defaultFileSource);
+ GetSources(pRootElement, "music", m_musicSources, m_defaultMusicSource);
+ GetSources(pRootElement, "games", m_gameSources, dummy);
+
+ return true;
+}
+
+bool CMediaSourceSettings::Save()
+{
+ return Save(GetSourcesFile());
+}
+
+bool CMediaSourceSettings::Save(const std::string &file) const
+{
+ //! @todo Should we be specifying utf8 here??
+ CXBMCTinyXML doc;
+ TiXmlElement xmlRootElement(XML_SOURCES);
+ TiXmlNode *pRoot = doc.InsertEndChild(xmlRootElement);
+ if (pRoot == NULL)
+ return false;
+
+ // ok, now run through and save each sources section
+ SetSources(pRoot, "programs", m_programSources, m_defaultProgramSource);
+ SetSources(pRoot, "video", m_videoSources, "");
+ SetSources(pRoot, "music", m_musicSources, m_defaultMusicSource);
+ SetSources(pRoot, "pictures", m_pictureSources, m_defaultPictureSource);
+ SetSources(pRoot, "files", m_fileSources, m_defaultFileSource);
+ SetSources(pRoot, "games", m_gameSources, "");
+
+ CWakeOnAccess::GetInstance().QueueMACDiscoveryForAllRemotes();
+
+ return doc.SaveFile(file);
+}
+
+void CMediaSourceSettings::Clear()
+{
+ m_programSources.clear();
+ m_pictureSources.clear();
+ m_fileSources.clear();
+ m_musicSources.clear();
+ m_videoSources.clear();
+ m_gameSources.clear();
+}
+
+VECSOURCES* CMediaSourceSettings::GetSources(const std::string &type)
+{
+ if (type == "programs" || type == "myprograms")
+ return &m_programSources;
+ else if (type == "files")
+ return &m_fileSources;
+ else if (type == "music")
+ return &m_musicSources;
+ else if (type == "video" || type == "videos")
+ return &m_videoSources;
+ else if (type == "pictures")
+ return &m_pictureSources;
+ else if (type == "games")
+ return &m_gameSources;
+
+ return NULL;
+}
+
+const std::string& CMediaSourceSettings::GetDefaultSource(const std::string &type) const
+{
+ if (type == "programs" || type == "myprograms")
+ return m_defaultProgramSource;
+ else if (type == "files")
+ return m_defaultFileSource;
+ else if (type == "music")
+ return m_defaultMusicSource;
+ else if (type == "pictures")
+ return m_defaultPictureSource;
+
+ return StringUtils::Empty;
+}
+
+void CMediaSourceSettings::SetDefaultSource(const std::string &type, const std::string &source)
+{
+ if (type == "programs" || type == "myprograms")
+ m_defaultProgramSource = source;
+ else if (type == "files")
+ m_defaultFileSource = source;
+ else if (type == "music")
+ m_defaultMusicSource = source;
+ else if (type == "pictures")
+ m_defaultPictureSource = source;
+}
+
+// NOTE: This function does NOT save the sources.xml file - you need to call SaveSources() separately.
+bool CMediaSourceSettings::UpdateSource(const std::string &strType, const std::string &strOldName, const std::string &strUpdateChild, const std::string &strUpdateValue)
+{
+ VECSOURCES *pShares = GetSources(strType);
+ if (pShares == NULL)
+ return false;
+
+ for (IVECSOURCES it = pShares->begin(); it != pShares->end(); it++)
+ {
+ if (it->strName == strOldName)
+ {
+ if (strUpdateChild == "name")
+ it->strName = strUpdateValue;
+ else if (strUpdateChild == "lockmode")
+ it->m_iLockMode = (LockType)std::strtol(strUpdateValue.c_str(), NULL, 10);
+ else if (strUpdateChild == "lockcode")
+ it->m_strLockCode = strUpdateValue;
+ else if (strUpdateChild == "badpwdcount")
+ it->m_iBadPwdCount = (int)std::strtol(strUpdateValue.c_str(), NULL, 10);
+ else if (strUpdateChild == "thumbnail")
+ it->m_strThumbnailImage = strUpdateValue;
+ else if (strUpdateChild == "path")
+ {
+ it->vecPaths.clear();
+ it->strPath = strUpdateValue;
+ it->vecPaths.push_back(strUpdateValue);
+ }
+ else
+ return false;
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CMediaSourceSettings::DeleteSource(const std::string &strType, const std::string &strName, const std::string &strPath, bool virtualSource /* = false */)
+{
+ VECSOURCES *pShares = GetSources(strType);
+ if (pShares == NULL)
+ return false;
+
+ bool found = false;
+
+ for (IVECSOURCES it = pShares->begin(); it != pShares->end(); it++)
+ {
+ if (it->strName == strName && it->strPath == strPath)
+ {
+ CLog::Log(LOGDEBUG, "CMediaSourceSettings: found share, removing!");
+ pShares->erase(it);
+ found = true;
+ break;
+ }
+ }
+
+ if (virtualSource)
+ return found;
+
+ return Save();
+}
+
+bool CMediaSourceSettings::AddShare(const std::string &type, const CMediaSource &share)
+{
+ VECSOURCES *pShares = GetSources(type);
+ if (pShares == NULL)
+ return false;
+
+ // translate dir and add to our current shares
+ std::string strPath1 = share.strPath;
+ if (strPath1.empty())
+ {
+ CLog::Log(LOGERROR, "CMediaSourceSettings: unable to add empty path");
+ return false;
+ }
+ StringUtils::ToUpper(strPath1);
+
+ CMediaSource shareToAdd = share;
+ if (strPath1.at(0) == '$')
+ {
+ shareToAdd.strPath = CUtil::TranslateSpecialSource(strPath1);
+ if (!share.strPath.empty())
+ CLog::Log(LOGDEBUG, "CMediaSourceSettings: translated ({}) to path ({})", strPath1,
+ shareToAdd.strPath);
+ else
+ {
+ CLog::Log(LOGDEBUG, "CMediaSourceSettings: skipping invalid special directory token ({})",
+ strPath1);
+ return false;
+ }
+ }
+ pShares->push_back(shareToAdd);
+
+ if (!share.m_ignore)
+ return Save();
+
+ return true;
+}
+
+bool CMediaSourceSettings::UpdateShare(const std::string &type, const std::string &oldName, const CMediaSource &share)
+{
+ VECSOURCES *pShares = GetSources(type);
+ if (pShares == NULL)
+ return false;
+
+ // update our current share list
+ CMediaSource* pShare = NULL;
+ for (IVECSOURCES it = pShares->begin(); it != pShares->end(); it++)
+ {
+ if (it->strName == oldName)
+ {
+ it->strName = share.strName;
+ it->strPath = share.strPath;
+ it->vecPaths = share.vecPaths;
+ pShare = &(*it);
+ break;
+ }
+ }
+
+ if (pShare == NULL)
+ return false;
+
+ // Update our XML file as well
+ return Save();
+}
+
+bool CMediaSourceSettings::GetSource(const std::string &category, const TiXmlNode *source, CMediaSource &share)
+{
+ const TiXmlNode *pNodeName = source->FirstChild("name");
+ std::string strName;
+ if (pNodeName && pNodeName->FirstChild())
+ strName = pNodeName->FirstChild()->ValueStr();
+
+ // get multiple paths
+ std::vector<std::string> vecPaths;
+ const TiXmlElement *pPathName = source->FirstChildElement("path");
+ while (pPathName != NULL)
+ {
+ if (pPathName->FirstChild())
+ {
+ std::string strPath = pPathName->FirstChild()->ValueStr();
+
+ // make sure there are no virtualpaths or stack paths defined in sources.xml
+ if (!URIUtils::IsStack(strPath))
+ {
+ // translate special tags
+ if (!strPath.empty() && strPath.at(0) == '$')
+ strPath = CUtil::TranslateSpecialSource(strPath);
+
+ // need to check path validity again as CUtil::TranslateSpecialSource() may have failed
+ if (!strPath.empty())
+ {
+ URIUtils::AddSlashAtEnd(strPath);
+ vecPaths.push_back(strPath);
+ }
+ }
+ else
+ CLog::Log(LOGERROR, "CMediaSourceSettings: invalid path type ({}) in source", strPath);
+ }
+
+ pPathName = pPathName->NextSiblingElement("path");
+ }
+
+ const TiXmlNode *pLockMode = source->FirstChild("lockmode");
+ const TiXmlNode *pLockCode = source->FirstChild("lockcode");
+ const TiXmlNode *pBadPwdCount = source->FirstChild("badpwdcount");
+ const TiXmlNode *pThumbnailNode = source->FirstChild("thumbnail");
+
+ if (strName.empty() || vecPaths.empty())
+ return false;
+
+ std::vector<std::string> verifiedPaths;
+ // disallowed for files, or there's only a single path in the vector
+ if (StringUtils::EqualsNoCase(category, "files") || vecPaths.size() == 1)
+ verifiedPaths.push_back(vecPaths[0]);
+ // multiple paths?
+ else
+ {
+ // validate the paths
+ for (std::vector<std::string>::const_iterator path = vecPaths.begin(); path != vecPaths.end(); ++path)
+ {
+ CURL url(*path);
+ bool bIsInvalid = false;
+
+ // for my programs
+ if (StringUtils::EqualsNoCase(category, "programs") || StringUtils::EqualsNoCase(category, "myprograms"))
+ {
+ // only allow HD and plugins
+ if (url.IsLocal() || url.IsProtocol("plugin"))
+ verifiedPaths.push_back(*path);
+ else
+ bIsInvalid = true;
+ }
+ // for others allow everything (if the user does something silly, we can't stop them)
+ else
+ verifiedPaths.push_back(*path);
+
+ // error message
+ if (bIsInvalid)
+ CLog::Log(LOGERROR, "CMediaSourceSettings: invalid path type ({}) for multipath source",
+ path->c_str());
+ }
+
+ // no valid paths? skip to next source
+ if (verifiedPaths.empty())
+ {
+ CLog::Log(LOGERROR,"CMediaSourceSettings: missing or invalid <name> and/or <path> in source");
+ return false;
+ }
+ }
+
+ share.FromNameAndPaths(category, strName, verifiedPaths);
+
+ share.m_iBadPwdCount = 0;
+ if (pLockMode)
+ {
+ share.m_iLockMode = (LockType)std::strtol(pLockMode->FirstChild()->Value(), NULL, 10);
+ share.m_iHasLock = LOCK_STATE_LOCKED;
+ }
+
+ if (pLockCode && pLockCode->FirstChild())
+ share.m_strLockCode = pLockCode->FirstChild()->Value();
+
+ if (pBadPwdCount && pBadPwdCount->FirstChild())
+ share.m_iBadPwdCount = (int)std::strtol(pBadPwdCount->FirstChild()->Value(), NULL, 10);
+
+ if (pThumbnailNode && pThumbnailNode->FirstChild())
+ share.m_strThumbnailImage = pThumbnailNode->FirstChild()->Value();
+
+ XMLUtils::GetBoolean(source, "allowsharing", share.m_allowSharing);
+
+ return true;
+}
+
+void CMediaSourceSettings::GetSources(const TiXmlNode* pRootElement, const std::string& strTagName, VECSOURCES& items, std::string& strDefault)
+{
+ strDefault = "";
+ items.clear();
+
+ const TiXmlNode *pChild = pRootElement->FirstChild(strTagName.c_str());
+ if (pChild == NULL)
+ {
+ CLog::Log(LOGDEBUG, "CMediaSourceSettings: <{}> tag is missing or sources.xml is malformed",
+ strTagName);
+ return;
+ }
+
+ pChild = pChild->FirstChild();
+ while (pChild != NULL)
+ {
+ std::string strValue = pChild->ValueStr();
+ if (strValue == XML_SOURCE || strValue == "bookmark") // "bookmark" left in for backwards compatibility
+ {
+ CMediaSource share;
+ if (GetSource(strTagName, pChild, share))
+ items.push_back(share);
+ else
+ CLog::Log(LOGERROR, "CMediaSourceSettings: Missing or invalid <name> and/or <path> in source");
+ }
+ else if (strValue == "default")
+ {
+ const TiXmlNode *pValueNode = pChild->FirstChild();
+ if (pValueNode)
+ {
+ std::string pszText = pChild->FirstChild()->ValueStr();
+ if (!pszText.empty())
+ strDefault = pszText;
+ CLog::Log(LOGDEBUG, "CMediaSourceSettings: Setting <default> source to : {}",
+ strDefault);
+ }
+ }
+
+ pChild = pChild->NextSibling();
+ }
+}
+
+bool CMediaSourceSettings::SetSources(TiXmlNode *root, const char *section, const VECSOURCES &shares, const std::string &defaultPath) const
+{
+ TiXmlElement sectionElement(section);
+ TiXmlNode *sectionNode = root->InsertEndChild(sectionElement);
+ if (sectionNode == NULL)
+ return false;
+
+ XMLUtils::SetPath(sectionNode, "default", defaultPath);
+ for (CIVECSOURCES it = shares.begin(); it != shares.end(); it++)
+ {
+ const CMediaSource &share = *it;
+ if (share.m_ignore)
+ continue;
+
+ TiXmlElement source(XML_SOURCE);
+ XMLUtils::SetString(&source, "name", share.strName);
+
+ for (unsigned int i = 0; i < share.vecPaths.size(); i++)
+ XMLUtils::SetPath(&source, "path", share.vecPaths[i]);
+
+ if (share.m_iHasLock)
+ {
+ XMLUtils::SetInt(&source, "lockmode", share.m_iLockMode);
+ XMLUtils::SetString(&source, "lockcode", share.m_strLockCode);
+ XMLUtils::SetInt(&source, "badpwdcount", share.m_iBadPwdCount);
+ }
+
+ if (!share.m_strThumbnailImage.empty())
+ XMLUtils::SetPath(&source, "thumbnail", share.m_strThumbnailImage);
+
+ XMLUtils::SetBoolean(&source, "allowsharing", share.m_allowSharing);
+
+ sectionNode->InsertEndChild(source);
+ }
+
+ return true;
+}
diff --git a/xbmc/settings/MediaSourceSettings.h b/xbmc/settings/MediaSourceSettings.h
new file mode 100644
index 0000000..7fe456e
--- /dev/null
+++ b/xbmc/settings/MediaSourceSettings.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2013-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 "MediaSource.h"
+#include "settings/lib/ISettingsHandler.h"
+
+#include <string>
+
+class CProfileManager;
+class TiXmlNode;
+
+class CMediaSourceSettings : public ISettingsHandler
+{
+public:
+ static CMediaSourceSettings& GetInstance();
+
+ static std::string GetSourcesFile();
+
+ void OnSettingsLoaded() override;
+ void OnSettingsUnloaded() override;
+
+ bool Load();
+ bool Load(const std::string &file);
+ bool Save();
+ bool Save(const std::string &file) const;
+ void Clear();
+
+ VECSOURCES* GetSources(const std::string &type);
+ const std::string& GetDefaultSource(const std::string &type) const;
+ void SetDefaultSource(const std::string &type, const std::string &source);
+
+ bool UpdateSource(const std::string &strType, const std::string &strOldName, const std::string &strUpdateChild, const std::string &strUpdateValue);
+ bool DeleteSource(const std::string &strType, const std::string &strName, const std::string &strPath, bool virtualSource = false);
+ bool AddShare(const std::string &type, const CMediaSource &share);
+ bool UpdateShare(const std::string &type, const std::string &oldName, const CMediaSource &share);
+
+protected:
+ CMediaSourceSettings();
+ CMediaSourceSettings(const CMediaSourceSettings&) = delete;
+ CMediaSourceSettings& operator=(CMediaSourceSettings const&) = delete;
+ ~CMediaSourceSettings() override;
+
+private:
+ bool GetSource(const std::string &category, const TiXmlNode *source, CMediaSource &share);
+ void GetSources(const TiXmlNode* pRootElement, const std::string& strTagName, VECSOURCES& items, std::string& strDefault);
+ bool SetSources(TiXmlNode *root, const char *section, const VECSOURCES &shares, const std::string &defaultPath) const;
+
+ VECSOURCES m_programSources;
+ VECSOURCES m_pictureSources;
+ VECSOURCES m_fileSources;
+ VECSOURCES m_musicSources;
+ VECSOURCES m_videoSources;
+ VECSOURCES m_gameSources;
+
+ std::string m_defaultProgramSource;
+ std::string m_defaultMusicSource;
+ std::string m_defaultPictureSource;
+ std::string m_defaultFileSource;
+};
diff --git a/xbmc/settings/SettingAddon.cpp b/xbmc/settings/SettingAddon.cpp
new file mode 100644
index 0000000..fd39fe9
--- /dev/null
+++ b/xbmc/settings/SettingAddon.cpp
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2013-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 "SettingAddon.h"
+
+#include "addons/Addon.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+CSettingAddon::CSettingAddon(const std::string &id, CSettingsManager *settingsManager /* = nullptr */)
+ : CSettingString(id, settingsManager)
+{ }
+
+CSettingAddon::CSettingAddon(const std::string &id, int label, const std::string &value, CSettingsManager *settingsManager /* = nullptr */)
+ : CSettingString(id, label, value, settingsManager)
+{ }
+
+CSettingAddon::CSettingAddon(const std::string &id, const CSettingAddon &setting)
+ : CSettingString(id, setting)
+{
+ copyaddontype(setting);
+}
+
+SettingPtr CSettingAddon::Clone(const std::string &id) const
+{
+ return std::make_shared<CSettingAddon>(id, *this);
+}
+
+bool CSettingAddon::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if (!CSettingString::Deserialize(node, update))
+ return false;
+
+ if (m_control != nullptr &&
+ (m_control->GetType() != "button" || m_control->GetFormat() != "addon"))
+ {
+ CLog::Log(LOGERROR, "CSettingAddon: invalid <control> of \"{}\"", m_id);
+ return false;
+ }
+
+ bool ok = false;
+ std::string strAddonType;
+ auto constraints = node->FirstChild("constraints");
+ if (constraints != nullptr)
+ {
+ // get the addon type
+ if (XMLUtils::GetString(constraints, "addontype", strAddonType) && !strAddonType.empty())
+ {
+ m_addonType = ADDON::CAddonInfo::TranslateType(strAddonType);
+ if (m_addonType != ADDON::AddonType::UNKNOWN)
+ ok = true;
+ }
+ }
+
+ if (!ok && !update)
+ {
+ CLog::Log(LOGERROR, "CSettingAddon: error reading the addontype value \"{}\" of \"{}\"",
+ strAddonType, m_id);
+ return false;
+ }
+
+ return true;
+}
+
+void CSettingAddon::copyaddontype(const CSettingAddon &setting)
+{
+ CSettingString::Copy(setting);
+
+ std::unique_lock<CSharedSection> lock(m_critical);
+ m_addonType = setting.m_addonType;
+}
diff --git a/xbmc/settings/SettingAddon.h b/xbmc/settings/SettingAddon.h
new file mode 100644
index 0000000..d4aefc8
--- /dev/null
+++ b/xbmc/settings/SettingAddon.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2013-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 "addons/IAddon.h"
+#include "settings/lib/Setting.h"
+
+namespace ADDON
+{
+enum class AddonType;
+}
+
+class CSettingAddon : public CSettingString
+{
+public:
+ CSettingAddon(const std::string &id, CSettingsManager *settingsManager = nullptr);
+ CSettingAddon(const std::string &id, int label, const std::string &value, CSettingsManager *settingsManager = nullptr);
+ CSettingAddon(const std::string &id, const CSettingAddon &setting);
+ ~CSettingAddon() override = default;
+
+ SettingPtr Clone(const std::string &id) const override;
+
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ ADDON::AddonType GetAddonType() const { return m_addonType; }
+ void SetAddonType(ADDON::AddonType addonType) { m_addonType = addonType; }
+
+private:
+ void copyaddontype(const CSettingAddon &setting);
+
+ ADDON::AddonType m_addonType{};
+};
diff --git a/xbmc/settings/SettingConditions.cpp b/xbmc/settings/SettingConditions.cpp
new file mode 100644
index 0000000..bb85e97
--- /dev/null
+++ b/xbmc/settings/SettingConditions.cpp
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2013-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 "SettingConditions.h"
+
+#include "LockType.h"
+#include "addons/AddonManager.h"
+#include "addons/Skin.h"
+#include "addons/addoninfo/AddonType.h"
+#include "application/AppParams.h"
+#include "cores/AudioEngine/Engines/ActiveAE/ActiveAESettings.h"
+#include "ServiceBroker.h"
+#include "GUIPassword.h"
+#if defined(HAS_WEB_SERVER)
+#include "network/WebServer.h"
+#endif
+#include "peripherals/Peripherals.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingAddon.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FontUtils.h"
+#include "utils/StringUtils.h"
+#include "windowing/WinSystem.h"
+
+namespace
+{
+bool AddonHasSettings(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ std::shared_ptr<const CSettingAddon> settingAddon = std::dynamic_pointer_cast<const CSettingAddon>(setting);
+ if (settingAddon == NULL)
+ return false;
+
+ ADDON::AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(settingAddon->GetValue(), addon,
+ settingAddon->GetAddonType(),
+ ADDON::OnlyEnabled::CHOICE_YES) ||
+ addon == NULL)
+ return false;
+
+ if (addon->Type() == ADDON::AddonType::SKIN)
+ return ((ADDON::CSkinInfo*)addon.get())->HasSkinFile("SkinSettings.xml");
+
+ return addon->CanHaveAddonOrInstanceSettings();
+}
+
+bool CheckMasterLock(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return g_passwordManager.IsMasterLockUnlocked(StringUtils::EqualsNoCase(value, "true"));
+}
+
+bool HasPeripherals(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CServiceBroker::GetPeripherals().GetNumberOfPeripherals() > 0;
+}
+
+bool HasPeripheralLibraries(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CServiceBroker::GetAddonMgr().HasInstalledAddons(ADDON::AddonType::PERIPHERALDLL);
+}
+
+bool HasRumbleFeature(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CServiceBroker::GetPeripherals().SupportsFeature(PERIPHERALS::FEATURE_RUMBLE);
+}
+
+bool HasRumbleController(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CServiceBroker::GetPeripherals().HasPeripheralWithFeature(PERIPHERALS::FEATURE_RUMBLE);
+}
+
+bool HasPowerOffFeature(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CServiceBroker::GetPeripherals().SupportsFeature(PERIPHERALS::FEATURE_POWER_OFF);
+}
+
+bool IsHDRDisplay(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CServiceBroker::GetWinSystem()->IsHDRDisplay();
+}
+
+bool IsMasterUser(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return g_passwordManager.bMasterUser;
+}
+
+bool HasSubtitlesFontExtensions(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ auto settingStr = std::dynamic_pointer_cast<const CSettingString>(setting);
+ if (!settingStr)
+ return false;
+
+ return UTILS::FONT::IsSupportedFontExtension(settingStr->GetValue());
+}
+
+bool ProfileCanWriteDatabase(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CSettingConditions::GetCurrentProfile().canWriteDatabases();
+}
+
+bool ProfileCanWriteSources(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CSettingConditions::GetCurrentProfile().canWriteSources();
+}
+
+bool ProfileHasAddons(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CSettingConditions::GetCurrentProfile().hasAddons();
+}
+
+bool ProfileHasDatabase(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CSettingConditions::GetCurrentProfile().hasDatabases();
+}
+
+bool ProfileHasSources(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CSettingConditions::GetCurrentProfile().hasSources();
+}
+
+bool ProfileHasAddonManagerLocked(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CSettingConditions::GetCurrentProfile().addonmanagerLocked();
+}
+
+bool ProfileHasFilesLocked(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CSettingConditions::GetCurrentProfile().filesLocked();
+}
+
+bool ProfileHasMusicLocked(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CSettingConditions::GetCurrentProfile().musicLocked();
+}
+
+bool ProfileHasPicturesLocked(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CSettingConditions::GetCurrentProfile().picturesLocked();
+}
+
+bool ProfileHasProgramsLocked(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CSettingConditions::GetCurrentProfile().programsLocked();
+}
+
+bool ProfileHasSettingsLocked(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ LOCK_LEVEL::SETTINGS_LOCK slValue=LOCK_LEVEL::ALL;
+ if (StringUtils::EqualsNoCase(value, "none"))
+ slValue = LOCK_LEVEL::NONE;
+ else if (StringUtils::EqualsNoCase(value, "standard"))
+ slValue = LOCK_LEVEL::STANDARD;
+ else if (StringUtils::EqualsNoCase(value, "advanced"))
+ slValue = LOCK_LEVEL::ADVANCED;
+ else if (StringUtils::EqualsNoCase(value, "expert"))
+ slValue = LOCK_LEVEL::EXPERT;
+ return slValue <= CSettingConditions::GetCurrentProfile().settingsLockLevel();
+}
+
+bool ProfileHasVideosLocked(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ return CSettingConditions::GetCurrentProfile().videoLocked();
+}
+
+bool ProfileLockMode(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ char* tmp = nullptr;
+ LockType lock = (LockType)strtol(value.c_str(), &tmp, 0);
+ if (tmp != NULL && *tmp != '\0')
+ return false;
+
+ return CSettingConditions::GetCurrentProfile().getLockMode() == lock;
+}
+
+bool GreaterThan(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ std::shared_ptr<const CSettingInt> settingInt = std::dynamic_pointer_cast<const CSettingInt>(setting);
+ if (settingInt == NULL)
+ return false;
+
+ char* tmp = nullptr;
+
+ int lhs = settingInt->GetValue();
+ int rhs = StringUtils::IsInteger(value) ? (int)strtol(value.c_str(), &tmp, 0) : 0;
+
+ return lhs > rhs;
+}
+
+bool GreaterThanOrEqual(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ std::shared_ptr<const CSettingInt> settingInt = std::dynamic_pointer_cast<const CSettingInt>(setting);
+ if (settingInt == NULL)
+ return false;
+
+ char* tmp = nullptr;
+
+ int lhs = settingInt->GetValue();
+ int rhs = StringUtils::IsInteger(value) ? (int)strtol(value.c_str(), &tmp, 0) : 0;
+
+ return lhs >= rhs;
+}
+
+bool LessThan(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ std::shared_ptr<const CSettingInt> settingInt = std::dynamic_pointer_cast<const CSettingInt>(setting);
+ if (settingInt == NULL)
+ return false;
+
+ char* tmp = nullptr;
+
+ int lhs = settingInt->GetValue();
+ int rhs = StringUtils::IsInteger(value) ? (int)strtol(value.c_str(), &tmp, 0) : 0;
+
+ return lhs < rhs;
+}
+
+bool LessThanOrEqual(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ if (setting == NULL)
+ return false;
+
+ std::shared_ptr<const CSettingInt> settingInt = std::dynamic_pointer_cast<const CSettingInt>(setting);
+ if (settingInt == NULL)
+ return false;
+
+ char* tmp = nullptr;
+
+ int lhs = settingInt->GetValue();
+ int rhs = StringUtils::IsInteger(value) ? (int)strtol(value.c_str(), &tmp, 0) : 0;
+
+ return lhs <= rhs;
+}
+}; // anonymous namespace
+
+const CProfileManager* CSettingConditions::m_profileManager = nullptr;
+std::set<std::string> CSettingConditions::m_simpleConditions;
+std::map<std::string, SettingConditionCheck> CSettingConditions::m_complexConditions;
+
+void CSettingConditions::Initialize()
+{
+ if (!m_simpleConditions.empty())
+ return;
+
+ // add simple conditions
+ m_simpleConditions.emplace("true");
+#ifdef HAS_UPNP
+ m_simpleConditions.emplace("has_upnp");
+#endif
+#ifdef HAS_AIRPLAY
+ m_simpleConditions.emplace("has_airplay");
+#endif
+#ifdef HAVE_X11
+ m_simpleConditions.emplace("have_x11");
+#endif
+#ifdef HAVE_WAYLAND
+ m_simpleConditions.emplace("have_wayland");
+#endif
+#ifdef HAS_GL
+ m_simpleConditions.emplace("has_gl");
+#endif
+#ifdef HAS_GLES
+ m_simpleConditions.emplace("has_gles");
+#endif
+#if HAS_GLES >= 2
+ m_simpleConditions.emplace("has_glesv2");
+#endif
+#ifdef HAS_TIME_SERVER
+ m_simpleConditions.emplace("has_time_server");
+#endif
+#ifdef HAS_WEB_SERVER
+ m_simpleConditions.emplace("has_web_server");
+#endif
+#ifdef HAS_FILESYSTEM_SMB
+ m_simpleConditions.emplace("has_filesystem_smb");
+#endif
+#ifdef HAS_FILESYSTEM_NFS
+ m_simpleConditions.insert("has_filesystem_nfs");
+#endif
+#ifdef HAS_ZEROCONF
+ m_simpleConditions.emplace("has_zeroconf");
+#endif
+#ifdef HAVE_LIBVA
+ m_simpleConditions.emplace("have_libva");
+#endif
+#ifdef HAVE_LIBVDPAU
+ m_simpleConditions.emplace("have_libvdpau");
+#endif
+#ifdef TARGET_ANDROID
+ m_simpleConditions.emplace("has_mediacodec");
+#endif
+#ifdef TARGET_DARWIN
+ m_simpleConditions.emplace("HasVTB");
+#endif
+#ifdef TARGET_DARWIN_OSX
+ m_simpleConditions.emplace("have_osx");
+#endif
+#ifdef TARGET_DARWIN_IOS
+ m_simpleConditions.emplace("have_ios");
+#endif
+#ifdef TARGET_DARWIN_TVOS
+ m_simpleConditions.emplace("have_tvos");
+#endif
+#if defined(TARGET_WINDOWS)
+ m_simpleConditions.emplace("has_dx");
+ m_simpleConditions.emplace("hasdxva2");
+#endif
+#ifdef HAVE_LCMS2
+ m_simpleConditions.emplace("have_lcms2");
+#endif
+
+#ifdef TARGET_ANDROID
+ m_simpleConditions.emplace("isstandalone");
+#else
+ if (CServiceBroker::GetAppParams()->IsStandAlone())
+ m_simpleConditions.emplace("isstandalone");
+#endif
+
+ m_simpleConditions.emplace("has_ae_quality_levels");
+
+#ifdef HAS_WEB_SERVER
+ if (CWebServer::WebServerSupportsSSL())
+ m_simpleConditions.emplace("webserver_has_ssl");
+#endif
+
+#ifdef HAVE_LIBBLURAY
+ m_simpleConditions.emplace("have_libbluray");
+#endif
+
+#ifdef HAS_CDDA_RIPPER
+ m_simpleConditions.emplace("has_cdda_ripper");
+#endif
+
+#ifdef HAS_DVD_DRIVE
+ m_simpleConditions.emplace("has_dvd_drive");
+#endif
+
+ // add complex conditions
+ m_complexConditions.emplace("addonhassettings", AddonHasSettings);
+ m_complexConditions.emplace("checkmasterlock", CheckMasterLock);
+ m_complexConditions.emplace("hasperipherals", HasPeripherals);
+ m_complexConditions.emplace("hasperipherallibraries", HasPeripheralLibraries);
+ m_complexConditions.emplace("hasrumblefeature", HasRumbleFeature);
+ m_complexConditions.emplace("hasrumblecontroller", HasRumbleController);
+ m_complexConditions.emplace("haspowerofffeature", HasPowerOffFeature);
+ m_complexConditions.emplace("ishdrdisplay", IsHDRDisplay);
+ m_complexConditions.emplace("ismasteruser", IsMasterUser);
+ m_complexConditions.emplace("hassubtitlesfontextensions", HasSubtitlesFontExtensions);
+ m_complexConditions.emplace("profilecanwritedatabase", ProfileCanWriteDatabase);
+ m_complexConditions.emplace("profilecanwritesources", ProfileCanWriteSources);
+ m_complexConditions.emplace("profilehasaddons", ProfileHasAddons);
+ m_complexConditions.emplace("profilehasdatabase", ProfileHasDatabase);
+ m_complexConditions.emplace("profilehassources", ProfileHasSources);
+ m_complexConditions.emplace("profilehasaddonmanagerlocked", ProfileHasAddonManagerLocked);
+ m_complexConditions.emplace("profilehasfileslocked", ProfileHasFilesLocked);
+ m_complexConditions.emplace("profilehasmusiclocked", ProfileHasMusicLocked);
+ m_complexConditions.emplace("profilehaspictureslocked", ProfileHasPicturesLocked);
+ m_complexConditions.emplace("profilehasprogramslocked", ProfileHasProgramsLocked);
+ m_complexConditions.emplace("profilehassettingslocked", ProfileHasSettingsLocked);
+ m_complexConditions.emplace("profilehasvideoslocked", ProfileHasVideosLocked);
+ m_complexConditions.emplace("profilelockmode", ProfileLockMode);
+ m_complexConditions.emplace("aesettingvisible", ActiveAE::CActiveAESettings::IsSettingVisible);
+ m_complexConditions.emplace("gt", GreaterThan);
+ m_complexConditions.emplace("gte", GreaterThanOrEqual);
+ m_complexConditions.emplace("lt", LessThan);
+ m_complexConditions.emplace("lte", LessThanOrEqual);
+}
+
+void CSettingConditions::Deinitialize()
+{
+ m_profileManager = nullptr;
+}
+
+const CProfile& CSettingConditions::GetCurrentProfile()
+{
+ if (!m_profileManager)
+ m_profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager().get();
+
+ if (m_profileManager)
+ return m_profileManager->GetCurrentProfile();
+
+ static CProfile emptyProfile;
+ return emptyProfile;
+}
+
+bool CSettingConditions::Check(const std::string& condition,
+ const std::string& value /* = "" */,
+ const SettingConstPtr& setting /* = NULL */)
+{
+ if (m_simpleConditions.find(condition) != m_simpleConditions.end())
+ return true;
+
+ std::map<std::string, SettingConditionCheck>::const_iterator itCondition = m_complexConditions.find(condition);
+ if (itCondition != m_complexConditions.end())
+ return itCondition->second(condition, value, setting, NULL);
+
+ return Check(condition);
+}
diff --git a/xbmc/settings/SettingConditions.h b/xbmc/settings/SettingConditions.h
new file mode 100644
index 0000000..f51d9e0
--- /dev/null
+++ b/xbmc/settings/SettingConditions.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014-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/SettingConditions.h"
+
+#include <set>
+#include <string>
+
+class CProfile;
+class CProfileManager;
+
+class CSettingConditions
+{
+public:
+ static void Initialize();
+ static void Deinitialize();
+
+ static const CProfile& GetCurrentProfile();
+
+ static const std::set<std::string>& GetSimpleConditions() { return m_simpleConditions; }
+ static const std::map<std::string, SettingConditionCheck>& GetComplexConditions() { return m_complexConditions; }
+
+ static bool Check(const std::string& condition,
+ const std::string& value = "",
+ const std::shared_ptr<const CSetting>& setting = NULL);
+
+private:
+ // Initialization parameters
+ static const CProfileManager *m_profileManager;
+
+ static std::set<std::string> m_simpleConditions;
+ static std::map<std::string, SettingConditionCheck> m_complexConditions;
+};
diff --git a/xbmc/settings/SettingControl.cpp b/xbmc/settings/SettingControl.cpp
new file mode 100644
index 0000000..3c73b89
--- /dev/null
+++ b/xbmc/settings/SettingControl.cpp
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2013-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 "SettingControl.h"
+
+#include "settings/lib/SettingDefinitions.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <vector>
+
+const char* SHOW_ADDONS_ALL = "all";
+const char* SHOW_ADDONS_INSTALLED = "installed";
+const char* SHOW_ADDONS_INSTALLABLE = "installable";
+
+std::shared_ptr<ISettingControl> CSettingControlCreator::CreateControl(const std::string &controlType) const
+{
+ if (StringUtils::EqualsNoCase(controlType, "toggle"))
+ return std::make_shared<CSettingControlCheckmark>();
+ else if (StringUtils::EqualsNoCase(controlType, "spinner"))
+ return std::make_shared<CSettingControlSpinner>();
+ else if (StringUtils::EqualsNoCase(controlType, "edit"))
+ return std::make_shared<CSettingControlEdit>();
+ else if (StringUtils::EqualsNoCase(controlType, "button"))
+ return std::make_shared<CSettingControlButton>();
+ else if (StringUtils::EqualsNoCase(controlType, "list"))
+ return std::make_shared<CSettingControlList>();
+ else if (StringUtils::EqualsNoCase(controlType, "slider"))
+ return std::make_shared<CSettingControlSlider>();
+ else if (StringUtils::EqualsNoCase(controlType, "range"))
+ return std::make_shared<CSettingControlRange>();
+ else if (StringUtils::EqualsNoCase(controlType, "title"))
+ return std::make_shared<CSettingControlTitle>();
+ else if (StringUtils::EqualsNoCase(controlType, "label"))
+ return std::make_shared<CSettingControlLabel>();
+ else if (StringUtils::EqualsNoCase(controlType, "colorbutton"))
+ return std::make_shared<CSettingControlColorButton>();
+
+ return nullptr;
+}
+
+bool CSettingControlCheckmark::SetFormat(const std::string &format)
+{
+ return format.empty() || StringUtils::EqualsNoCase(format, "boolean");
+}
+
+bool CSettingControlFormattedRange::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ if (!ISettingControl::Deserialize(node, update))
+ return false;
+
+ if (m_format == "string")
+ {
+ XMLUtils::GetInt(node, SETTING_XML_ELM_CONTROL_FORMATLABEL, m_formatLabel);
+
+ // get the minimum label from <setting><constraints><minimum label="X" />
+ auto settingNode = node->Parent();
+ if (settingNode != nullptr)
+ {
+ auto constraintsNode = settingNode->FirstChild(SETTING_XML_ELM_CONSTRAINTS);
+ if (constraintsNode != nullptr)
+ {
+ auto minimumNode = constraintsNode->FirstChild(SETTING_XML_ELM_MINIMUM);
+ if (minimumNode != nullptr)
+ {
+ auto minimumElem = minimumNode->ToElement();
+ if (minimumElem != nullptr)
+ {
+ if (minimumElem->QueryIntAttribute(SETTING_XML_ATTR_LABEL, &m_minimumLabel) != TIXML_SUCCESS)
+ m_minimumLabel = -1;
+ }
+ }
+ }
+ }
+
+ if (m_minimumLabel < 0)
+ {
+ std::string strFormat;
+ if (XMLUtils::GetString(node, SETTING_XML_ATTR_FORMAT, strFormat) && !strFormat.empty())
+ m_formatString = strFormat;
+ }
+ }
+
+ return true;
+}
+
+bool CSettingControlSpinner::SetFormat(const std::string &format)
+{
+ if (!StringUtils::EqualsNoCase(format, "string") &&
+ !StringUtils::EqualsNoCase(format, "integer") &&
+ !StringUtils::EqualsNoCase(format, "number"))
+ return false;
+
+ m_format = format;
+ StringUtils::ToLower(m_format);
+
+ return true;
+}
+
+bool CSettingControlEdit::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ if (!ISettingControl::Deserialize(node, update))
+ return false;
+
+ XMLUtils::GetBoolean(node, SETTING_XML_ELM_CONTROL_HIDDEN, m_hidden);
+ XMLUtils::GetBoolean(node, SETTING_XML_ELM_CONTROL_VERIFYNEW, m_verifyNewValue);
+ XMLUtils::GetInt(node, SETTING_XML_ELM_CONTROL_HEADING, m_heading);
+
+ return true;
+}
+
+bool CSettingControlEdit::SetFormat(const std::string &format)
+{
+ if (!StringUtils::EqualsNoCase(format, "string") &&
+ !StringUtils::EqualsNoCase(format, "integer") &&
+ !StringUtils::EqualsNoCase(format, "number") &&
+ !StringUtils::EqualsNoCase(format, "ip") &&
+ !StringUtils::EqualsNoCase(format, "md5") &&
+ !StringUtils::EqualsNoCase(format, "urlencoded"))
+ return false;
+
+ m_format = format;
+ StringUtils::ToLower(m_format);
+
+ return true;
+}
+
+bool CSettingControlButton::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ if (!ISettingControl::Deserialize(node, update))
+ return false;
+
+ XMLUtils::GetInt(node, SETTING_XML_ELM_CONTROL_HEADING, m_heading);
+ XMLUtils::GetBoolean(node, SETTING_XML_ELM_CONTROL_HIDEVALUE, m_hideValue);
+
+ if (m_format == "action")
+ {
+ bool closeDialog = false;
+ if (XMLUtils::GetBoolean(node, "close", closeDialog))
+ m_closeDialog = closeDialog;
+ std::string strActionData;
+ if (XMLUtils::GetString(node, SETTING_XML_ELM_DATA, strActionData))
+ m_actionData = strActionData;
+ }
+ else if (m_format == "addon")
+ {
+ std::string strShowAddons;
+ if (XMLUtils::GetString(node, "show", strShowAddons) && !strShowAddons.empty())
+ {
+ if (StringUtils::EqualsNoCase(strShowAddons, SHOW_ADDONS_ALL))
+ {
+ m_showInstalledAddons = true;
+ m_showInstallableAddons = true;
+ }
+ else if (StringUtils::EqualsNoCase(strShowAddons, SHOW_ADDONS_INSTALLED))
+ {
+ m_showInstalledAddons = true;
+ m_showInstallableAddons = false;
+ }
+ else if (StringUtils::EqualsNoCase(strShowAddons, SHOW_ADDONS_INSTALLABLE))
+ {
+ m_showInstalledAddons = false;
+ m_showInstallableAddons = true;
+ }
+ else
+ CLog::Log(LOGWARNING, "CSettingControlButton: invalid <show>");
+
+ auto show = node->FirstChildElement("show");
+ if (show != nullptr)
+ {
+ const char *strShowDetails = nullptr;
+ if ((strShowDetails = show->Attribute(SETTING_XML_ATTR_SHOW_DETAILS)) != nullptr)
+ {
+ if (StringUtils::EqualsNoCase(strShowDetails, "false") || StringUtils::EqualsNoCase(strShowDetails, "true"))
+ m_showAddonDetails = StringUtils::EqualsNoCase(strShowDetails, "true");
+ else
+ CLog::Log(LOGWARNING, "CSettingControlButton: error reading \"details\" attribute of <show>");
+ }
+
+ if (!m_showInstallableAddons)
+ {
+ const char *strShowMore = nullptr;
+ if ((strShowMore = show->Attribute(SETTING_XML_ATTR_SHOW_MORE)) != nullptr)
+ {
+ if (StringUtils::EqualsNoCase(strShowMore, "false") || StringUtils::EqualsNoCase(strShowMore, "true"))
+ m_showMoreAddons = StringUtils::EqualsNoCase(strShowMore, "true");
+ else
+ CLog::Log(LOGWARNING, "CSettingControlButton: error reading \"more\" attribute of <show>");
+ }
+ }
+ }
+ }
+ }
+ else if (m_format == "file")
+ {
+ bool useThumbs = false;
+ if (XMLUtils::GetBoolean(node, "usethumbs", useThumbs))
+ m_useImageThumbs = useThumbs;
+ bool useFileDirectories = false;
+ if (XMLUtils::GetBoolean(node, "treatasfolder", useFileDirectories))
+ m_useFileDirectories = useFileDirectories;
+ }
+
+ return true;
+}
+
+bool CSettingControlButton::SetFormat(const std::string &format)
+{
+ if (!StringUtils::EqualsNoCase(format, "path") &&
+ !StringUtils::EqualsNoCase(format, "file") &&
+ !StringUtils::EqualsNoCase(format, "image") &&
+ !StringUtils::EqualsNoCase(format, "addon") &&
+ !StringUtils::EqualsNoCase(format, "action") &&
+ !StringUtils::EqualsNoCase(format, "infolabel") &&
+ !StringUtils::EqualsNoCase(format, "date") &&
+ !StringUtils::EqualsNoCase(format, "time"))
+ return false;
+
+ m_format = format;
+ StringUtils::ToLower(m_format);
+
+ return true;
+}
+
+bool CSettingControlList::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ if (!CSettingControlFormattedRange::Deserialize(node, update))
+ return false;
+
+ XMLUtils::GetInt(node, SETTING_XML_ELM_CONTROL_HEADING, m_heading);
+ XMLUtils::GetBoolean(node, SETTING_XML_ELM_CONTROL_MULTISELECT, m_multiselect);
+ XMLUtils::GetBoolean(node, SETTING_XML_ELM_CONTROL_HIDEVALUE, m_hideValue);
+ XMLUtils::GetInt(node, SETTING_XML_ELM_CONTROL_ADDBUTTONLABEL, m_addButtonLabel);
+
+ return true;
+}
+
+bool CSettingControlList::SetFormat(const std::string &format)
+{
+ if (!StringUtils::EqualsNoCase(format, "string") &&
+ !StringUtils::EqualsNoCase(format, "integer"))
+ return false;
+
+ m_format = format;
+ StringUtils::ToLower(m_format);
+
+ return true;
+}
+
+bool CSettingControlSlider::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ if (!ISettingControl::Deserialize(node, update))
+ return false;
+
+ XMLUtils::GetInt(node, SETTING_XML_ELM_CONTROL_HEADING, m_heading);
+ XMLUtils::GetBoolean(node, SETTING_XML_ELM_CONTROL_POPUP, m_popup);
+
+ XMLUtils::GetInt(node, SETTING_XML_ELM_CONTROL_FORMATLABEL, m_formatLabel);
+ if (m_formatLabel < 0)
+ {
+ std::string strFormat;
+ if (XMLUtils::GetString(node, SETTING_XML_ATTR_FORMAT, strFormat) && !strFormat.empty())
+ m_formatString = strFormat;
+ }
+
+ return true;
+}
+
+bool CSettingControlSlider::SetFormat(const std::string &format)
+{
+ if (!StringUtils::EqualsNoCase(format, "percentage") &&
+ !StringUtils::EqualsNoCase(format, "integer") &&
+ !StringUtils::EqualsNoCase(format, "number"))
+ return false;
+
+ m_format = format;
+ StringUtils::ToLower(m_format);
+ m_formatString = GetDefaultFormatString();
+
+ return true;
+}
+
+std::string CSettingControlSlider::GetDefaultFormatString() const
+{
+ if (m_format == "percentage")
+ return "{} %";
+ if (m_format == "integer")
+ return "{:d}";
+ if (m_format == "number")
+ return "{:.1f}";
+
+ return "{}";
+}
+
+bool CSettingControlRange::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ if (!ISettingControl::Deserialize(node, update))
+ return false;
+
+ auto formatLabel = node->FirstChildElement(SETTING_XML_ELM_CONTROL_FORMATLABEL);
+ if (formatLabel != nullptr)
+ {
+ XMLUtils::GetInt(node, SETTING_XML_ELM_CONTROL_FORMATLABEL, m_formatLabel);
+ if (m_formatLabel < 0)
+ return false;
+
+ auto formatValue = formatLabel->Attribute(SETTING_XML_ELM_CONTROL_FORMATVALUE);
+ if (formatValue != nullptr)
+ {
+ if (StringUtils::IsInteger(formatValue))
+ m_valueFormatLabel = (int)strtol(formatValue, nullptr, 0);
+ else
+ {
+ m_valueFormat = formatValue;
+ if (!m_valueFormat.empty())
+ m_valueFormatLabel = -1;
+ }
+ }
+ }
+
+ return true;
+}
+
+bool CSettingControlRange::SetFormat(const std::string &format)
+{
+ if (StringUtils::EqualsNoCase(format, "percentage"))
+ m_valueFormat = "{} %";
+ else if (StringUtils::EqualsNoCase(format, "integer"))
+ m_valueFormat = "{:d}";
+ else if (StringUtils::EqualsNoCase(format, "number"))
+ m_valueFormat = "{:.1f}";
+ else if (StringUtils::EqualsNoCase(format, "date") ||
+ StringUtils::EqualsNoCase(format, "time"))
+ m_valueFormat.clear();
+ else
+ return false;
+
+ m_format = format;
+ StringUtils::ToLower(m_format);
+
+ return true;
+}
+
+bool CSettingControlTitle::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ if (!ISettingControl::Deserialize(node, update))
+ return false;
+
+ std::string strTmp;
+ if (XMLUtils::GetString(node, SETTING_XML_ATTR_SEPARATOR_POSITION, strTmp))
+ {
+ if (!StringUtils::EqualsNoCase(strTmp, "top") && !StringUtils::EqualsNoCase(strTmp, "bottom"))
+ CLog::Log(LOGWARNING, "CSettingControlTitle: error reading \"value\" attribute of <{}>",
+ SETTING_XML_ATTR_SEPARATOR_POSITION);
+ else
+ m_separatorBelowLabel = StringUtils::EqualsNoCase(strTmp, "bottom");
+ }
+ XMLUtils::GetBoolean(node, SETTING_XML_ATTR_HIDE_SEPARATOR, m_separatorHidden);
+
+ return true;
+}
+
+CSettingControlLabel::CSettingControlLabel()
+{
+ m_format = "string";
+}
+
+bool CSettingControlColorButton::SetFormat(const std::string& format)
+{
+ return format.empty() || StringUtils::EqualsNoCase(format, "string");
+}
diff --git a/xbmc/settings/SettingControl.h b/xbmc/settings/SettingControl.h
new file mode 100644
index 0000000..c86f040
--- /dev/null
+++ b/xbmc/settings/SettingControl.h
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2013-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/ISettingControl.h"
+#include "settings/lib/ISettingControlCreator.h"
+
+#define SETTING_XML_ELM_CONTROL_FORMATLABEL "formatlabel"
+#define SETTING_XML_ELM_CONTROL_HIDDEN "hidden"
+#define SETTING_XML_ELM_CONTROL_VERIFYNEW "verifynew"
+#define SETTING_XML_ELM_CONTROL_HEADING "heading"
+#define SETTING_XML_ELM_CONTROL_HIDEVALUE "hidevalue"
+#define SETTING_XML_ELM_CONTROL_MULTISELECT "multiselect"
+#define SETTING_XML_ELM_CONTROL_POPUP "popup"
+#define SETTING_XML_ELM_CONTROL_FORMATVALUE "value"
+#define SETTING_XML_ELM_CONTROL_ADDBUTTONLABEL "addbuttonlabel"
+#define SETTING_XML_ATTR_SHOW_MORE "more"
+#define SETTING_XML_ATTR_SHOW_DETAILS "details"
+#define SETTING_XML_ATTR_SEPARATOR_POSITION "separatorposition"
+#define SETTING_XML_ATTR_HIDE_SEPARATOR "hideseparator"
+
+class CVariant;
+
+class CSettingControlCreator : public ISettingControlCreator
+{
+public:
+ // implementation of ISettingControlCreator
+ std::shared_ptr<ISettingControl> CreateControl(const std::string &controlType) const override;
+
+protected:
+ CSettingControlCreator() = default;
+ ~CSettingControlCreator() override = default;
+};
+
+class CSettingControlCheckmark : public ISettingControl
+{
+public:
+ CSettingControlCheckmark()
+ {
+ m_format = "boolean";
+ }
+ ~CSettingControlCheckmark() override = default;
+
+ // implementation of ISettingControl
+ std::string GetType() const override { return "toggle"; }
+ bool SetFormat(const std::string &format) override;
+};
+
+class CSettingControlFormattedRange : public ISettingControl
+{
+public:
+ ~CSettingControlFormattedRange() override = default;
+
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ int GetFormatLabel() const { return m_formatLabel; }
+ void SetFormatLabel(int formatLabel) { m_formatLabel = formatLabel; }
+ const std::string& GetFormatString() const { return m_formatString; }
+ void SetFormatString(const std::string &formatString) { m_formatString = formatString; }
+ int GetMinimumLabel() const { return m_minimumLabel; }
+ void SetMinimumLabel(int minimumLabel) { m_minimumLabel = minimumLabel; }
+
+protected:
+ CSettingControlFormattedRange() = default;
+
+ int m_formatLabel = -1;
+ std::string m_formatString = "{}";
+ int m_minimumLabel = -1;
+};
+
+class CSettingControlSpinner : public CSettingControlFormattedRange
+{
+public:
+ CSettingControlSpinner() = default;
+ ~CSettingControlSpinner() override = default;
+
+ // implementation of ISettingControl
+ std::string GetType() const override { return "spinner"; }
+
+ // specialization of CSettingControlFormattedRange
+ bool SetFormat(const std::string &format) override;
+};
+
+class CSettingControlEdit : public ISettingControl
+{
+public:
+ CSettingControlEdit()
+ {
+ m_delayed = true;
+ }
+ ~CSettingControlEdit() override = default;
+
+ // implementation of ISettingControl
+ std::string GetType() const override { return "edit"; }
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+ bool SetFormat(const std::string &format) override;
+
+ bool IsHidden() const { return m_hidden; }
+ void SetHidden(bool hidden) { m_hidden = hidden; }
+ bool VerifyNewValue() const { return m_verifyNewValue; }
+ void SetVerifyNewValue(bool verifyNewValue) { m_verifyNewValue = verifyNewValue; }
+ int GetHeading() const { return m_heading; }
+ void SetHeading(int heading) { m_heading = heading; }
+
+protected:
+ bool m_hidden = false;
+ bool m_verifyNewValue = false;
+ int m_heading = -1;
+};
+
+class CSettingControlButton : public ISettingControl
+{
+public:
+ CSettingControlButton() = default;
+ ~CSettingControlButton() override = default;
+
+ // implementation of ISettingControl
+ std::string GetType() const override { return "button"; }
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+ bool SetFormat(const std::string &format) override;
+
+ int GetHeading() const { return m_heading; }
+ void SetHeading(int heading) { m_heading = heading; }
+ bool HideValue() const { return m_hideValue; }
+ void SetHideValue(bool hideValue) { m_hideValue = hideValue; }
+
+ bool ShowAddonDetails() const { return m_showAddonDetails; }
+ void SetShowAddonDetails(bool showAddonDetails) { m_showAddonDetails = showAddonDetails; }
+ bool ShowInstalledAddons() const { return m_showInstalledAddons; }
+ void SetShowInstalledAddons(bool showInstalledAddons) { m_showInstalledAddons = showInstalledAddons; }
+ bool ShowInstallableAddons() const { return m_showInstallableAddons; }
+ void SetShowInstallableAddons(bool showInstallableAddons) { m_showInstallableAddons = showInstallableAddons; }
+ bool ShowMoreAddons() const { return !m_showInstallableAddons && m_showMoreAddons; }
+ void SetShowMoreAddons(bool showMoreAddons) { m_showMoreAddons = showMoreAddons; }
+
+ bool UseImageThumbs() const { return m_useImageThumbs; }
+ void SetUseImageThumbs(bool useImageThumbs) { m_useImageThumbs = useImageThumbs; }
+ bool UseFileDirectories() const { return m_useFileDirectories; }
+ void SetUseFileDirectories(bool useFileDirectories) { m_useFileDirectories = useFileDirectories; }
+
+ bool HasActionData() const { return !m_actionData.empty(); }
+ const std::string& GetActionData() const { return m_actionData; }
+ void SetActionData(const std::string& actionData) { m_actionData = actionData; }
+
+ bool CloseDialog() const { return m_closeDialog; }
+ void SetCloseDialog(bool closeDialog) { m_closeDialog = closeDialog; }
+
+protected:
+ int m_heading = -1;
+ bool m_hideValue = false;
+
+ bool m_showAddonDetails = true;
+ bool m_showInstalledAddons = true;
+ bool m_showInstallableAddons = false;
+ bool m_showMoreAddons = true;
+
+ bool m_useImageThumbs = false;
+ bool m_useFileDirectories = false;
+
+ std::string m_actionData;
+ bool m_closeDialog = false;
+};
+
+class CSetting;
+using SettingControlListValueFormatter =
+ std::string (*)(const std::shared_ptr<const CSetting>& setting);
+
+class CSettingControlList : public CSettingControlFormattedRange
+{
+public:
+ CSettingControlList() = default;
+ ~CSettingControlList() override = default;
+
+ // implementation of ISettingControl
+ std::string GetType() const override { return "list"; }
+
+ // specialization of CSettingControlFormattedRange
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+ bool SetFormat(const std::string &format) override;
+
+ int GetHeading() const { return m_heading; }
+ void SetHeading(int heading) { m_heading = heading; }
+ bool CanMultiSelect() const { return m_multiselect; }
+ void SetMultiSelect(bool multiselect) { m_multiselect = multiselect; }
+ bool HideValue() const { return m_hideValue; }
+ void SetHideValue(bool hideValue) { m_hideValue = hideValue; }
+ int GetAddButtonLabel() const { return m_addButtonLabel; }
+ void SetAddButtonLabel(int label) { m_addButtonLabel = label; }
+
+ SettingControlListValueFormatter GetFormatter() const { return m_formatter; }
+ void SetFormatter(SettingControlListValueFormatter formatter) { m_formatter = formatter; }
+
+ bool UseDetails() const { return m_useDetails; }
+ void SetUseDetails(bool useDetails) { m_useDetails = useDetails; }
+
+protected:
+ int m_heading = -1;
+ bool m_multiselect = false;
+ bool m_hideValue = false;
+ int m_addButtonLabel = -1;
+ SettingControlListValueFormatter m_formatter = nullptr;
+ bool m_useDetails{false};
+};
+
+class CSettingControlSlider;
+using SettingControlSliderFormatter =
+ std::string (*)(const std::shared_ptr<const CSettingControlSlider>& control,
+ const CVariant& value,
+ const CVariant& minimum,
+ const CVariant& step,
+ const CVariant& maximum);
+
+class CSettingControlSlider : public ISettingControl
+{
+public:
+ CSettingControlSlider() = default;
+ ~CSettingControlSlider() override = default;
+
+ // implementation of ISettingControl
+ std::string GetType() const override { return "slider"; }
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+ bool SetFormat(const std::string &format) override;
+
+ int GetHeading() const { return m_heading; }
+ void SetHeading(int heading) { m_heading = heading; }
+ bool UsePopup() const { return m_popup; }
+ void SetPopup(bool popup) { m_popup = popup; }
+ int GetFormatLabel() const { return m_formatLabel; }
+ void SetFormatLabel(int formatLabel) { m_formatLabel = formatLabel; }
+ const std::string& GetFormatString() const { return m_formatString; }
+ void SetFormatString(const std::string &formatString) { m_formatString = formatString; }
+ std::string GetDefaultFormatString() const;
+
+ SettingControlSliderFormatter GetFormatter() const { return m_formatter; }
+ void SetFormatter(SettingControlSliderFormatter formatter) { m_formatter = formatter; }
+
+protected:
+ int m_heading = -1;
+ bool m_popup = false;
+ int m_formatLabel = -1;
+ std::string m_formatString;
+ SettingControlSliderFormatter m_formatter = nullptr;
+};
+
+class CSettingControlRange : public ISettingControl
+{
+public:
+ CSettingControlRange() = default;
+ ~CSettingControlRange() override = default;
+
+ // implementation of ISettingControl
+ std::string GetType() const override { return "range"; }
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+ bool SetFormat(const std::string &format) override;
+
+ int GetFormatLabel() const { return m_formatLabel; }
+ void SetFormatLabel(int formatLabel) { m_formatLabel = formatLabel; }
+ int GetValueFormatLabel() const { return m_valueFormatLabel; }
+ void SetValueFormatLabel(int valueFormatLabel) { m_valueFormatLabel = valueFormatLabel; }
+ const std::string& GetValueFormat() const { return m_valueFormat; }
+ void SetValueFormat(const std::string &valueFormat) { m_valueFormat = valueFormat; }
+
+protected:
+ int m_formatLabel = 21469;
+ int m_valueFormatLabel = -1;
+ std::string m_valueFormat = "{}";
+};
+
+class CSettingControlTitle : public ISettingControl
+{
+public:
+ CSettingControlTitle() = default;
+ ~CSettingControlTitle() override = default;
+
+ // implementation of ISettingControl
+ std::string GetType() const override { return "title"; }
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ bool IsSeparatorHidden() const { return m_separatorHidden; }
+ void SetSeparatorHidden(bool hidden) { m_separatorHidden = hidden; }
+ bool IsSeparatorBelowLabel() const { return m_separatorBelowLabel; }
+ void SetSeparatorBelowLabel(bool below) { m_separatorBelowLabel = below; }
+
+protected:
+ bool m_separatorHidden = false;
+ bool m_separatorBelowLabel = true;
+};
+
+class CSettingControlLabel : public ISettingControl
+{
+public:
+ CSettingControlLabel();
+ ~CSettingControlLabel() override = default;
+
+ // implementation of ISettingControl
+ std::string GetType() const override { return "label"; }
+};
+
+class CSettingControlColorButton : public ISettingControl
+{
+public:
+ CSettingControlColorButton() { m_format = "string"; }
+ ~CSettingControlColorButton() override = default;
+
+ // implementation of ISettingControl
+ std::string GetType() const override { return "colorbutton"; }
+ bool SetFormat(const std::string& format) override;
+};
diff --git a/xbmc/settings/SettingCreator.cpp b/xbmc/settings/SettingCreator.cpp
new file mode 100644
index 0000000..afe1d89
--- /dev/null
+++ b/xbmc/settings/SettingCreator.cpp
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2013-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 "SettingCreator.h"
+
+#include "settings/SettingAddon.h"
+#include "settings/SettingDateTime.h"
+#include "settings/SettingPath.h"
+#include "utils/StringUtils.h"
+
+std::shared_ptr<CSetting> CSettingCreator::CreateSetting(const std::string &settingType, const std::string &settingId, CSettingsManager *settingsManager /* = nullptr */) const
+{
+ if (StringUtils::EqualsNoCase(settingType, "addon"))
+ return std::make_shared<CSettingAddon>(settingId, settingsManager);
+ else if (StringUtils::EqualsNoCase(settingType, "path"))
+ return std::make_shared<CSettingPath>(settingId, settingsManager);
+ else if (StringUtils::EqualsNoCase(settingType, "date"))
+ return std::make_shared<CSettingDate>(settingId, settingsManager);
+ else if (StringUtils::EqualsNoCase(settingType, "time"))
+ return std::make_shared<CSettingTime>(settingId, settingsManager);
+
+ return nullptr;
+}
diff --git a/xbmc/settings/SettingCreator.h b/xbmc/settings/SettingCreator.h
new file mode 100644
index 0000000..68d4588
--- /dev/null
+++ b/xbmc/settings/SettingCreator.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2014-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/ISettingCreator.h"
+
+class CSettingCreator : public ISettingCreator
+{
+public:
+ // implementation of ISettingCreator
+ std::shared_ptr<CSetting> CreateSetting(const std::string &settingType, const std::string &settingId, CSettingsManager *settingsManager = nullptr) const override;
+
+protected:
+ CSettingCreator() = default;
+ ~CSettingCreator() override = default;
+};
diff --git a/xbmc/settings/SettingDateTime.cpp b/xbmc/settings/SettingDateTime.cpp
new file mode 100644
index 0000000..48a736c
--- /dev/null
+++ b/xbmc/settings/SettingDateTime.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017-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 "SettingDateTime.h"
+
+#include "XBDateTime.h"
+#include "utils/TimeUtils.h"
+
+#include <shared_mutex>
+
+CSettingDate::CSettingDate(const std::string &id, CSettingsManager *settingsManager /* = NULL */)
+ : CSettingString(id, settingsManager)
+{ }
+
+CSettingDate::CSettingDate(const std::string &id, int label, const std::string &value, CSettingsManager *settingsManager /* = NULL */)
+ : CSettingString(id, label, value, settingsManager)
+{ }
+
+CSettingDate::CSettingDate(const std::string &id, const CSettingDate &setting)
+ : CSettingString(id, setting)
+{ }
+
+SettingPtr CSettingDate::Clone(const std::string &id) const
+{
+ return std::make_shared<CSettingDate>(id, *this);
+}
+
+bool CSettingDate::CheckValidity(const std::string &value) const
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+
+ if (!CSettingString::CheckValidity(value))
+ return false;
+
+ return CDateTime::FromDBDate(value).IsValid();
+}
+
+CDateTime CSettingDate::GetDate() const
+{
+ return CDateTime::FromDBDate(GetValue());
+}
+
+bool CSettingDate::SetDate(const CDateTime& date)
+{
+ return SetValue(date.GetAsDBDate());
+}
+
+CSettingTime::CSettingTime(const std::string &id, CSettingsManager *settingsManager /* = NULL */)
+ : CSettingString(id, settingsManager)
+{ }
+
+CSettingTime::CSettingTime(const std::string &id, int label, const std::string &value, CSettingsManager *settingsManager /* = NULL */)
+ : CSettingString(id, label, value, settingsManager)
+{ }
+
+CSettingTime::CSettingTime(const std::string &id, const CSettingTime &setting)
+ : CSettingString(id, setting)
+{ }
+
+SettingPtr CSettingTime::Clone(const std::string &id) const
+{
+ return std::make_shared<CSettingTime>(id, *this);
+}
+
+bool CSettingTime::CheckValidity(const std::string &value) const
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+
+ if (!CSettingString::CheckValidity(value))
+ return false;
+
+ return CDateTime::FromDBTime(value).IsValid();
+}
+
+CDateTime CSettingTime::GetTime() const
+{
+ return CDateTime::FromDBTime(GetValue());
+}
+
+bool CSettingTime::SetTime(const CDateTime& time)
+{
+ return SetValue(CTimeUtils::WithoutSeconds(time.GetAsDBTime()));
+}
diff --git a/xbmc/settings/SettingDateTime.h b/xbmc/settings/SettingDateTime.h
new file mode 100644
index 0000000..5ee481f
--- /dev/null
+++ b/xbmc/settings/SettingDateTime.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017-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/Setting.h"
+
+class CDateTime;
+
+class CSettingDate : public CSettingString
+{
+public:
+ CSettingDate(const std::string &id, CSettingsManager *settingsManager = NULL);
+ CSettingDate(const std::string &id, int label, const std::string &value, CSettingsManager *settingsManager = NULL);
+ CSettingDate(const std::string &id, const CSettingDate &setting);
+ ~CSettingDate() override = default;
+
+ SettingPtr Clone(const std::string &id) const override;
+
+ bool CheckValidity(const std::string &value) const override;
+
+ CDateTime GetDate() const;
+ bool SetDate(const CDateTime& date);
+};
+
+class CSettingTime : public CSettingString
+{
+public:
+ CSettingTime(const std::string &id, CSettingsManager *settingsManager = NULL);
+ CSettingTime(const std::string &id, int label, const std::string &value, CSettingsManager *settingsManager = NULL);
+ CSettingTime(const std::string &id, const CSettingTime &setting);
+ ~CSettingTime() override = default;
+
+ SettingPtr Clone(const std::string &id) const override;
+
+ bool CheckValidity(const std::string &value) const override;
+
+ CDateTime GetTime() const;
+ bool SetTime(const CDateTime& time);
+};
diff --git a/xbmc/settings/SettingPath.cpp b/xbmc/settings/SettingPath.cpp
new file mode 100644
index 0000000..fa215ff
--- /dev/null
+++ b/xbmc/settings/SettingPath.cpp
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2013-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 "SettingPath.h"
+
+#include "settings/lib/SettingsManager.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#define XML_ELM_DEFAULT "default"
+#define XML_ELM_CONSTRAINTS "constraints"
+
+CSettingPath::CSettingPath(const std::string &id, CSettingsManager *settingsManager /* = nullptr */)
+ : CSettingString(id, settingsManager)
+{ }
+
+CSettingPath::CSettingPath(const std::string &id, int label, const std::string &value, CSettingsManager *settingsManager /* = nullptr */)
+ : CSettingString(id, label, value, settingsManager)
+{ }
+
+CSettingPath::CSettingPath(const std::string &id, const CSettingPath &setting)
+ : CSettingString(id, setting)
+{
+ copy(setting);
+}
+
+SettingPtr CSettingPath::Clone(const std::string &id) const
+{
+ return std::make_shared<CSettingPath>(id, *this);
+}
+
+bool CSettingPath::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if (!CSettingString::Deserialize(node, update))
+ return false;
+
+ if (m_control != nullptr &&
+ (m_control->GetType() != "button" || (m_control->GetFormat() != "path" && m_control->GetFormat() != "file" && m_control->GetFormat() != "image")))
+ {
+ CLog::Log(LOGERROR, "CSettingPath: invalid <control> of \"{}\"", m_id);
+ return false;
+ }
+
+ auto constraints = node->FirstChild(XML_ELM_CONSTRAINTS);
+ if (constraints != nullptr)
+ {
+ // get writable
+ XMLUtils::GetBoolean(constraints, "writable", m_writable);
+ // get hide extensions
+ XMLUtils::GetBoolean(constraints, "hideextensions", m_hideExtension);
+
+ // get sources
+ auto sources = constraints->FirstChild("sources");
+ if (sources != nullptr)
+ {
+ m_sources.clear();
+ auto source = sources->FirstChild("source");
+ while (source != nullptr)
+ {
+ auto child = source->FirstChild();
+ if (child != nullptr)
+ {
+ const std::string& strSource = child->ValueStr();
+ if (!strSource.empty())
+ m_sources.push_back(strSource);
+ }
+
+ source = source->NextSibling("source");
+ }
+ }
+
+ // get masking
+ auto masking = constraints->FirstChild("masking");
+ if (masking != nullptr)
+ m_masking = masking->FirstChild()->ValueStr();
+ }
+
+ return true;
+}
+
+bool CSettingPath::SetValue(const std::string &value)
+{
+ // for backwards compatibility to Frodo
+ if (StringUtils::EqualsNoCase(value, "select folder") ||
+ StringUtils::EqualsNoCase(value, "select writable folder"))
+ return CSettingString::SetValue("");
+
+ return CSettingString::SetValue(value);
+}
+
+std::string CSettingPath::GetMasking(const CFileExtensionProvider& fileExtensionProvider) const
+{
+ if (m_masking.empty())
+ return m_masking;
+
+ // setup masking
+ auto audioMask = fileExtensionProvider.GetMusicExtensions();
+ auto videoMask = fileExtensionProvider.GetVideoExtensions();
+ auto imageMask = fileExtensionProvider.GetPictureExtensions();
+ auto execMask = "";
+#if defined(TARGET_WINDOWS)
+ execMask = ".exe|.bat|.cmd|.py";
+#endif // defined(TARGET_WINDOWS)
+
+ std::string masking = m_masking;
+ if (masking == "video")
+ return videoMask;
+ if (masking == "audio")
+ return audioMask;
+ if (masking == "image")
+ return imageMask;
+ if (masking == "executable")
+ return execMask;
+
+ // convert mask qualifiers
+ StringUtils::Replace(masking, "$AUDIO", audioMask);
+ StringUtils::Replace(masking, "$VIDEO", videoMask);
+ StringUtils::Replace(masking, "$IMAGE", imageMask);
+ StringUtils::Replace(masking, "$EXECUTABLE", execMask);
+
+ return masking;
+}
+
+void CSettingPath::copy(const CSettingPath& setting)
+{
+ CSettingString::Copy(setting);
+
+ std::unique_lock<CSharedSection> lock(m_critical);
+ m_writable = setting.m_writable;
+ m_sources = setting.m_sources;
+ m_hideExtension = setting.m_hideExtension;
+ m_masking = setting.m_masking;
+}
diff --git a/xbmc/settings/SettingPath.h b/xbmc/settings/SettingPath.h
new file mode 100644
index 0000000..423782a
--- /dev/null
+++ b/xbmc/settings/SettingPath.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013-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/Setting.h"
+
+#include <vector>
+
+class CFileExtensionProvider;
+
+class CSettingPath : public CSettingString
+{
+public:
+ CSettingPath(const std::string &id, CSettingsManager *settingsManager = nullptr);
+ CSettingPath(const std::string &id, int label, const std::string &value, CSettingsManager *settingsManager = nullptr);
+ CSettingPath(const std::string &id, const CSettingPath &setting);
+ ~CSettingPath() override = default;
+
+ SettingPtr Clone(const std::string &id) const override;
+
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+ bool SetValue(const std::string &value) override;
+
+ bool Writable() const { return m_writable; }
+ void SetWritable(bool writable) { m_writable = writable; }
+ const std::vector<std::string>& GetSources() const { return m_sources; }
+ void SetSources(const std::vector<std::string> &sources) { m_sources = sources; }
+ bool HideExtension() const { return m_hideExtension; }
+ void SetHideExtension(bool hideExtension) { m_hideExtension = hideExtension; }
+ std::string GetMasking(const CFileExtensionProvider& fileExtensionProvider) const;
+ void SetMasking(const std::string& masking) { m_masking = masking; }
+
+private:
+ using CSettingString::copy;
+ void copy(const CSettingPath &setting);
+
+ bool m_writable = true;
+ std::vector<std::string> m_sources;
+ bool m_hideExtension = false;
+ std::string m_masking;
+};
diff --git a/xbmc/settings/SettingUtils.cpp b/xbmc/settings/SettingUtils.cpp
new file mode 100644
index 0000000..c690932
--- /dev/null
+++ b/xbmc/settings/SettingUtils.cpp
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2013-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 "SettingUtils.h"
+
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+
+std::vector<CVariant> CSettingUtils::GetList(const std::shared_ptr<const CSettingList>& settingList)
+{
+ return ListToValues(settingList, settingList->GetValue());
+}
+
+bool CSettingUtils::SetList(const std::shared_ptr<CSettingList>& settingList,
+ const std::vector<CVariant>& value)
+{
+ SettingList newValues;
+ if (!ValuesToList(settingList, value, newValues))
+ return false;
+
+ return settingList->SetValue(newValues);
+}
+
+std::vector<CVariant> CSettingUtils::ListToValues(
+ const std::shared_ptr<const CSettingList>& setting,
+ const std::vector<std::shared_ptr<CSetting>>& values)
+{
+ std::vector<CVariant> realValues;
+
+ if (setting == NULL)
+ return realValues;
+
+ for (const auto& value : values)
+ {
+ switch (setting->GetElementType())
+ {
+ case SettingType::Boolean:
+ realValues.emplace_back(std::static_pointer_cast<const CSettingBool>(value)->GetValue());
+ break;
+
+ case SettingType::Integer:
+ realValues.emplace_back(std::static_pointer_cast<const CSettingInt>(value)->GetValue());
+ break;
+
+ case SettingType::Number:
+ realValues.emplace_back(std::static_pointer_cast<const CSettingNumber>(value)->GetValue());
+ break;
+
+ case SettingType::String:
+ realValues.emplace_back(std::static_pointer_cast<const CSettingString>(value)->GetValue());
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return realValues;
+}
+
+bool CSettingUtils::ValuesToList(const std::shared_ptr<const CSettingList>& setting,
+ const std::vector<CVariant>& values,
+ std::vector<std::shared_ptr<CSetting>>& newValues)
+{
+ if (setting == NULL)
+ return false;
+
+ int index = 0;
+ bool ret = true;
+ for (const auto& value : values)
+ {
+ SettingPtr settingValue =
+ setting->GetDefinition()->Clone(StringUtils::Format("{}.{}", setting->GetId(), index++));
+ if (settingValue == NULL)
+ return false;
+
+ switch (setting->GetElementType())
+ {
+ case SettingType::Boolean:
+ if (!value.isBoolean())
+ ret = false;
+ else
+ ret = std::static_pointer_cast<CSettingBool>(settingValue)->SetValue(value.asBoolean());
+ break;
+
+ case SettingType::Integer:
+ if (!value.isInteger())
+ ret = false;
+ else
+ ret = std::static_pointer_cast<CSettingInt>(settingValue)->SetValue(static_cast<int>(value.asInteger()));
+ break;
+
+ case SettingType::Number:
+ if (!value.isDouble())
+ ret = false;
+ else
+ ret = std::static_pointer_cast<CSettingNumber>(settingValue)->SetValue(value.asDouble());
+ break;
+
+ case SettingType::String:
+ if (!value.isString())
+ ret = false;
+ else
+ ret = std::static_pointer_cast<CSettingString>(settingValue)->SetValue(value.asString());
+ break;
+
+ default:
+ ret = false;
+ break;
+ }
+
+ if (!ret)
+ return false;
+
+ newValues.push_back(std::const_pointer_cast<CSetting>(settingValue));
+ }
+
+ return true;
+}
+
+bool CSettingUtils::FindIntInList(const std::shared_ptr<const CSettingList>& settingList, int value)
+{
+ if (settingList == nullptr || settingList->GetElementType() != SettingType::Integer)
+ return false;
+
+ const auto values = settingList->GetValue();
+ const auto matchingValue =
+ std::find_if(values.begin(), values.end(), [value](const SettingPtr& setting) {
+ return std::static_pointer_cast<CSettingInt>(setting)->GetValue() == value;
+ });
+ return matchingValue != values.end();
+}
diff --git a/xbmc/settings/SettingUtils.h b/xbmc/settings/SettingUtils.h
new file mode 100644
index 0000000..d7e456b
--- /dev/null
+++ b/xbmc/settings/SettingUtils.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2013-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 <memory>
+#include <vector>
+
+class CVariant;
+class CSettingList;
+class CSetting;
+
+class CSettingUtils
+{
+public:
+ /*!
+ \brief Gets the values of the given list setting.
+
+ \param settingList List setting
+ \return List of values of the given list setting
+ */
+ static std::vector<CVariant> GetList(const std::shared_ptr<const CSettingList>& settingList);
+ /*!
+ \brief Sets the values of the given list setting.
+
+ \param settingList List setting
+ \param value Values to set
+ \return True if setting the values was successful, false otherwise
+ */
+ static bool SetList(const std::shared_ptr<CSettingList>& settingList,
+ const std::vector<CVariant>& value);
+
+ static std::vector<CVariant> ListToValues(const std::shared_ptr<const CSettingList>& setting,
+ const std::vector<std::shared_ptr<CSetting>>& values);
+ static bool ValuesToList(const std::shared_ptr<const CSettingList>& setting,
+ const std::vector<CVariant>& values,
+ std::vector<std::shared_ptr<CSetting>>& newValues);
+
+ /*!
+ \brief Search in a list of Ints for a given value.
+
+ \param settingList CSettingList instance
+ \param value value to search for
+ \return True if value was found in list, false otherwise
+ */
+ static bool FindIntInList(const std::shared_ptr<const CSettingList>& settingList, int value);
+};
diff --git a/xbmc/settings/Settings.cpp b/xbmc/settings/Settings.cpp
new file mode 100644
index 0000000..b403b63
--- /dev/null
+++ b/xbmc/settings/Settings.cpp
@@ -0,0 +1,1088 @@
+/*
+ * 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 "Settings.h"
+
+#include "Autorun.h"
+#include "GUIPassword.h"
+#include "LangInfo.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/Skin.h"
+#include "application/AppParams.h"
+#include "cores/VideoPlayer/VideoRenderers/BaseRenderer.h"
+#include "filesystem/File.h"
+#include "guilib/GUIFontManager.h"
+#include "guilib/StereoscopicsManager.h"
+#include "input/KeyboardLayoutManager.h"
+
+#include <mutex>
+#if defined(TARGET_POSIX)
+#include "platform/posix/PosixTimezone.h"
+#endif // defined(TARGET_POSIX)
+#include "network/upnp/UPnPSettings.h"
+#include "network/WakeOnAccess.h"
+#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
+#include "DiscSettings.h"
+#include "SeekHandler.h"
+#include "ServiceBroker.h"
+#include "powermanagement/PowerTypes.h"
+#include "profiles/ProfileManager.h"
+#include "settings/DisplaySettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/SettingConditions.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SkinSettings.h"
+#include "settings/SubtitlesSettings.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/CharsetConverter.h"
+#include "utils/RssManager.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/Variant.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+#include "view/ViewStateSettings.h"
+
+#define SETTINGS_XML_FOLDER "special://xbmc/system/settings/"
+
+using namespace KODI;
+using namespace XFILE;
+
+//! @todo: remove in c++17
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_SKIN;
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_SKINSETTINGS;
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_SKINTHEME;
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_SKINCOLORS;
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_FONT;
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_SKINZOOM;
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_STARTUPACTION;
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_STARTUPWINDOW;
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_SOUNDSKIN;
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_ENABLERSSFEEDS;
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_RSSEDIT;
+constexpr const char* CSettings::SETTING_LOOKANDFEEL_STEREOSTRENGTH;
+constexpr const char* CSettings::SETTING_LOCALE_LANGUAGE;
+constexpr const char* CSettings::SETTING_LOCALE_COUNTRY;
+constexpr const char* CSettings::SETTING_LOCALE_CHARSET;
+constexpr const char* CSettings::SETTING_LOCALE_KEYBOARDLAYOUTS;
+constexpr const char* CSettings::SETTING_LOCALE_ACTIVEKEYBOARDLAYOUT;
+constexpr const char* CSettings::SETTING_LOCALE_TIMEZONECOUNTRY;
+constexpr const char* CSettings::SETTING_LOCALE_TIMEZONE;
+constexpr const char* CSettings::SETTING_LOCALE_SHORTDATEFORMAT;
+constexpr const char* CSettings::SETTING_LOCALE_LONGDATEFORMAT;
+constexpr const char* CSettings::SETTING_LOCALE_TIMEFORMAT;
+constexpr const char* CSettings::SETTING_LOCALE_USE24HOURCLOCK;
+constexpr const char* CSettings::SETTING_LOCALE_TEMPERATUREUNIT;
+constexpr const char* CSettings::SETTING_LOCALE_SPEEDUNIT;
+constexpr const char* CSettings::SETTING_FILELISTS_SHOWPARENTDIRITEMS;
+constexpr const char* CSettings::SETTING_FILELISTS_SHOWEXTENSIONS;
+constexpr const char* CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING;
+constexpr const char* CSettings::SETTING_FILELISTS_ALLOWFILEDELETION;
+constexpr const char* CSettings::SETTING_FILELISTS_SHOWADDSOURCEBUTTONS;
+constexpr const char* CSettings::SETTING_FILELISTS_SHOWHIDDEN;
+constexpr const char* CSettings::SETTING_SCREENSAVER_MODE;
+constexpr const char* CSettings::SETTING_SCREENSAVER_SETTINGS;
+constexpr const char* CSettings::SETTING_SCREENSAVER_PREVIEW;
+constexpr const char* CSettings::SETTING_SCREENSAVER_TIME;
+constexpr const char* CSettings::SETTING_SCREENSAVER_USEMUSICVISINSTEAD;
+constexpr const char* CSettings::SETTING_SCREENSAVER_USEDIMONPAUSE;
+constexpr const char* CSettings::SETTING_WINDOW_WIDTH;
+constexpr const char* CSettings::SETTING_WINDOW_HEIGHT;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_SHOWUNWATCHEDPLOTS;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_ACTORTHUMBS;
+constexpr const char* CSettings::SETTING_MYVIDEOS_FLATTEN;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_FLATTENTVSHOWS;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_TVSHOWSSELECTFIRSTUNWATCHEDITEM;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_TVSHOWSINCLUDEALLSEASONSANDSPECIALS;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_SHOWALLITEMS;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_GROUPMOVIESETS;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_GROUPSINGLEITEMSETS;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_UPDATEONSTARTUP;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_BACKGROUNDUPDATE;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_CLEANUP;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_EXPORT;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_IMPORT;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_SHOWEMPTYTVSHOWS;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_MOVIEART_WHITELIST;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_TVSHOWART_WHITELIST;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_EPISODEART_WHITELIST;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_MUSICVIDEOART_WHITELIST;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_ARTSETTINGS_UPDATED;
+constexpr const char* CSettings::SETTING_VIDEOLIBRARY_SHOWPERFORMERS;
+constexpr const char* CSettings::SETTING_LOCALE_AUDIOLANGUAGE;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_PREFERDEFAULTFLAG;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_AUTOPLAYNEXTITEM;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_SEEKSTEPS;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_SEEKDELAY;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USEDISPLAYASCLOCK;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_ERRORINASPECT;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_STRETCH43;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_TELETEXTENABLED;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_TELETEXTSCALE;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_STEREOSCOPICPLAYBACKMODE;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_QUITSTEREOMODEONSTOP;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_RENDERMETHOD;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_HQSCALERS;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USEMEDIACODEC;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USEMEDIACODECSURFACE;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USEVDPAU;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USEVDPAUMIXER;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USEVDPAUMPEG2;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USEVDPAUMPEG4;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USEVDPAUVC1;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USEDXVA2;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USEVTB;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USEPRIMEDECODER;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_USESTAGEFRIGHT;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_LIMITGUIUPDATE;
+constexpr const char* CSettings::SETTING_VIDEOPLAYER_SUPPORTMVC;
+constexpr const char* CSettings::SETTING_MYVIDEOS_SELECTACTION;
+constexpr const char* CSettings::SETTING_MYVIDEOS_USETAGS;
+constexpr const char* CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS;
+constexpr const char* CSettings::SETTING_MYVIDEOS_EXTRACTCHAPTERTHUMBS;
+constexpr const char* CSettings::SETTING_MYVIDEOS_REPLACELABELS;
+constexpr const char* CSettings::SETTING_MYVIDEOS_EXTRACTTHUMB;
+constexpr const char* CSettings::SETTING_MYVIDEOS_STACKVIDEOS;
+constexpr const char* CSettings::SETTING_LOCALE_SUBTITLELANGUAGE;
+constexpr const char* CSettings::SETTING_SUBTITLES_PARSECAPTIONS;
+constexpr const char* CSettings::SETTING_SUBTITLES_CAPTIONSALIGN;
+constexpr const char* CSettings::SETTING_SUBTITLES_ALIGN;
+constexpr const char* CSettings::SETTING_SUBTITLES_STEREOSCOPICDEPTH;
+constexpr const char* CSettings::SETTING_SUBTITLES_FONTNAME;
+constexpr const char* CSettings::SETTING_SUBTITLES_FONTSIZE;
+constexpr const char* CSettings::SETTING_SUBTITLES_STYLE;
+constexpr const char* CSettings::SETTING_SUBTITLES_COLOR;
+constexpr const char* CSettings::SETTING_SUBTITLES_BORDERSIZE;
+constexpr const char* CSettings::SETTING_SUBTITLES_BORDERCOLOR;
+constexpr const char* CSettings::SETTING_SUBTITLES_BGCOLOR;
+constexpr const char* CSettings::SETTING_SUBTITLES_BGOPACITY;
+constexpr const char* CSettings::SETTING_SUBTITLES_BLUR;
+constexpr const char* CSettings::SETTING_SUBTITLES_BACKGROUNDTYPE;
+constexpr const char* CSettings::SETTING_SUBTITLES_SHADOWCOLOR;
+constexpr const char* CSettings::SETTING_SUBTITLES_SHADOWOPACITY;
+constexpr const char* CSettings::SETTING_SUBTITLES_SHADOWSIZE;
+constexpr const char* CSettings::SETTING_SUBTITLES_CHARSET;
+constexpr const char* CSettings::SETTING_SUBTITLES_OVERRIDEFONTS;
+constexpr const char* CSettings::SETTING_SUBTITLES_OVERRIDESTYLES;
+constexpr const char* CSettings::SETTING_SUBTITLES_LANGUAGES;
+constexpr const char* CSettings::SETTING_SUBTITLES_STORAGEMODE;
+constexpr const char* CSettings::SETTING_SUBTITLES_CUSTOMPATH;
+constexpr const char* CSettings::SETTING_SUBTITLES_PAUSEONSEARCH;
+constexpr const char* CSettings::SETTING_SUBTITLES_DOWNLOADFIRST;
+constexpr const char* CSettings::SETTING_SUBTITLES_TV;
+constexpr const char* CSettings::SETTING_SUBTITLES_MOVIE;
+constexpr const char* CSettings::SETTING_DVDS_AUTORUN;
+constexpr const char* CSettings::SETTING_DVDS_PLAYERREGION;
+constexpr const char* CSettings::SETTING_DVDS_AUTOMENU;
+constexpr const char* CSettings::SETTING_DISC_PLAYBACK;
+constexpr const char* CSettings::SETTING_BLURAY_PLAYERREGION;
+constexpr const char* CSettings::SETTING_ACCESSIBILITY_AUDIOVISUAL;
+constexpr const char* CSettings::SETTING_ACCESSIBILITY_AUDIOHEARING;
+constexpr const char* CSettings::SETTING_ACCESSIBILITY_SUBHEARING;
+constexpr const char* CSettings::SETTING_SCRAPERS_MOVIESDEFAULT;
+constexpr const char* CSettings::SETTING_SCRAPERS_TVSHOWSDEFAULT;
+constexpr const char* CSettings::SETTING_SCRAPERS_MUSICVIDEOSDEFAULT;
+constexpr const char* CSettings::SETTING_PVRMANAGER_PRESELECTPLAYINGCHANNEL;
+constexpr const char* CSettings::SETTING_PVRMANAGER_SYNCCHANNELGROUPS;
+constexpr const char* CSettings::SETTING_PVRMANAGER_BACKENDCHANNELORDER;
+constexpr const char* CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS;
+constexpr const char* CSettings::SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS;
+constexpr const char* CSettings::SETTING_PVRMANAGER_STARTGROUPCHANNELNUMBERSFROMONE;
+constexpr const char* CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES;
+constexpr const char* CSettings::SETTING_PVRMANAGER_CHANNELMANAGER;
+constexpr const char* CSettings::SETTING_PVRMANAGER_GROUPMANAGER;
+constexpr const char* CSettings::SETTING_PVRMANAGER_CHANNELSCAN;
+constexpr const char* CSettings::SETTING_PVRMANAGER_RESETDB;
+constexpr const char* CSettings::SETTING_PVRMANAGER_ADDONS;
+constexpr const char* CSettings::SETTING_PVRMENU_DISPLAYCHANNELINFO;
+constexpr const char* CSettings::SETTING_PVRMENU_CLOSECHANNELOSDONSWITCH;
+constexpr const char* CSettings::SETTING_PVRMENU_ICONPATH;
+constexpr const char* CSettings::SETTING_PVRMENU_SEARCHICONS;
+constexpr const char* CSettings::SETTING_EPG_PAST_DAYSTODISPLAY;
+constexpr const char* CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY;
+constexpr const char* CSettings::SETTING_EPG_SELECTACTION;
+constexpr const char* CSettings::SETTING_EPG_HIDENOINFOAVAILABLE;
+constexpr const char* CSettings::SETTING_EPG_EPGUPDATE;
+constexpr const char* CSettings::SETTING_EPG_PREVENTUPDATESWHILEPLAYINGTV;
+constexpr const char* CSettings::SETTING_EPG_RESETEPG;
+constexpr const char* CSettings::SETTING_PVRPLAYBACK_SWITCHTOFULLSCREENCHANNELTYPES;
+constexpr const char* CSettings::SETTING_PVRPLAYBACK_SIGNALQUALITY;
+constexpr const char* CSettings::SETTING_PVRPLAYBACK_CONFIRMCHANNELSWITCH;
+constexpr const char* CSettings::SETTING_PVRPLAYBACK_CHANNELENTRYTIMEOUT;
+constexpr const char* CSettings::SETTING_PVRPLAYBACK_DELAYMARKLASTWATCHED;
+constexpr const char* CSettings::SETTING_PVRPLAYBACK_FPS;
+constexpr const char* CSettings::SETTING_PVRRECORD_INSTANTRECORDACTION;
+constexpr const char* CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME;
+constexpr const char* CSettings::SETTING_PVRRECORD_MARGINSTART;
+constexpr const char* CSettings::SETTING_PVRRECORD_MARGINEND;
+constexpr const char* CSettings::SETTING_PVRRECORD_TIMERNOTIFICATIONS;
+constexpr const char* CSettings::SETTING_PVRRECORD_GROUPRECORDINGS;
+constexpr const char* CSettings::SETTING_PVRREMINDERS_AUTOCLOSEDELAY;
+constexpr const char* CSettings::SETTING_PVRREMINDERS_AUTORECORD;
+constexpr const char* CSettings::SETTING_PVRREMINDERS_AUTOSWITCH;
+constexpr const char* CSettings::SETTING_PVRPOWERMANAGEMENT_ENABLED;
+constexpr const char* CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME;
+constexpr const char* CSettings::SETTING_PVRPOWERMANAGEMENT_SETWAKEUPCMD;
+constexpr const char* CSettings::SETTING_PVRPOWERMANAGEMENT_PREWAKEUP;
+constexpr const char* CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUP;
+constexpr const char* CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME;
+constexpr const char* CSettings::SETTING_PVRPARENTAL_ENABLED;
+constexpr const char* CSettings::SETTING_PVRPARENTAL_PIN;
+constexpr const char* CSettings::SETTING_PVRPARENTAL_DURATION;
+constexpr const char* CSettings::SETTING_PVRCLIENT_MENUHOOK;
+constexpr const char* CSettings::SETTING_PVRTIMERS_HIDEDISABLEDTIMERS;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_SHOWDISCS;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_USEORIGINALDATE;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_USEARTISTSORTNAME;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_DOWNLOADINFO;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_PREFERONLINEALBUMART;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_ARTWORKLEVEL;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_USEALLLOCALART;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_USEALLREMOTEART;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_ARTISTART_WHITELIST;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_ALBUMART_WHITELIST;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_MUSICTHUMBS;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_ARTSETTINGS_UPDATED;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_ALBUMSSCRAPER;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_ARTISTSSCRAPER;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_OVERRIDETAGS;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_SHOWALLITEMS;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_UPDATEONSTARTUP;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_BACKGROUNDUPDATE;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_CLEANUP;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_EXPORT;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_EXPORT_FILETYPE;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_EXPORT_ITEMS;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_EXPORT_UNSCRAPED;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_EXPORT_OVERWRITE;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_EXPORT_ARTWORK;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_EXPORT_SKIPNFO;
+constexpr const char* CSettings::SETTING_MUSICLIBRARY_IMPORT;
+constexpr const char* CSettings::SETTING_MUSICPLAYER_AUTOPLAYNEXTITEM;
+constexpr const char* CSettings::SETTING_MUSICPLAYER_QUEUEBYDEFAULT;
+constexpr const char* CSettings::SETTING_MUSICPLAYER_SEEKSTEPS;
+constexpr const char* CSettings::SETTING_MUSICPLAYER_SEEKDELAY;
+constexpr const char* CSettings::SETTING_MUSICPLAYER_REPLAYGAINTYPE;
+constexpr const char* CSettings::SETTING_MUSICPLAYER_REPLAYGAINPREAMP;
+constexpr const char* CSettings::SETTING_MUSICPLAYER_REPLAYGAINNOGAINPREAMP;
+constexpr const char* CSettings::SETTING_MUSICPLAYER_REPLAYGAINAVOIDCLIPPING;
+constexpr const char* CSettings::SETTING_MUSICPLAYER_CROSSFADE;
+constexpr const char* CSettings::SETTING_MUSICPLAYER_CROSSFADEALBUMTRACKS;
+constexpr const char* CSettings::SETTING_MUSICPLAYER_VISUALISATION;
+constexpr const char* CSettings::SETTING_MUSICFILES_SELECTACTION;
+constexpr const char* CSettings::SETTING_MUSICFILES_USETAGS;
+constexpr const char* CSettings::SETTING_MUSICFILES_TRACKFORMAT;
+constexpr const char* CSettings::SETTING_MUSICFILES_NOWPLAYINGTRACKFORMAT;
+constexpr const char* CSettings::SETTING_MUSICFILES_LIBRARYTRACKFORMAT;
+constexpr const char* CSettings::SETTING_MUSICFILES_FINDREMOTETHUMBS;
+constexpr const char* CSettings::SETTING_AUDIOCDS_AUTOACTION;
+constexpr const char* CSettings::SETTING_AUDIOCDS_USECDDB;
+constexpr const char* CSettings::SETTING_AUDIOCDS_RECORDINGPATH;
+constexpr const char* CSettings::SETTING_AUDIOCDS_TRACKPATHFORMAT;
+constexpr const char* CSettings::SETTING_AUDIOCDS_ENCODER;
+constexpr const char* CSettings::SETTING_AUDIOCDS_SETTINGS;
+constexpr const char* CSettings::SETTING_AUDIOCDS_EJECTONRIP;
+constexpr const char* CSettings::SETTING_MYMUSIC_SONGTHUMBINVIS;
+constexpr const char* CSettings::SETTING_MYMUSIC_DEFAULTLIBVIEW;
+constexpr const char* CSettings::SETTING_PICTURES_USETAGS;
+constexpr const char* CSettings::SETTING_PICTURES_GENERATETHUMBS;
+constexpr const char* CSettings::SETTING_PICTURES_SHOWVIDEOS;
+constexpr const char* CSettings::SETTING_PICTURES_DISPLAYRESOLUTION;
+constexpr const char* CSettings::SETTING_SLIDESHOW_STAYTIME;
+constexpr const char* CSettings::SETTING_SLIDESHOW_DISPLAYEFFECTS;
+constexpr const char* CSettings::SETTING_SLIDESHOW_SHUFFLE;
+constexpr const char* CSettings::SETTING_SLIDESHOW_HIGHQUALITYDOWNSCALING;
+constexpr const char* CSettings::SETTING_WEATHER_CURRENTLOCATION;
+constexpr const char* CSettings::SETTING_WEATHER_ADDON;
+constexpr const char* CSettings::SETTING_WEATHER_ADDONSETTINGS;
+constexpr const char* CSettings::SETTING_SERVICES_DEVICENAME;
+constexpr const char* CSettings::SETTING_SERVICES_DEVICEUUID;
+constexpr const char* CSettings::SETTING_SERVICES_UPNP;
+constexpr const char* CSettings::SETTING_SERVICES_UPNPSERVER;
+constexpr const char* CSettings::SETTING_SERVICES_UPNPANNOUNCE;
+constexpr const char* CSettings::SETTING_SERVICES_UPNPLOOKFOREXTERNALSUBTITLES;
+constexpr const char* CSettings::SETTING_SERVICES_UPNPCONTROLLER;
+constexpr const char* CSettings::SETTING_SERVICES_UPNPRENDERER;
+constexpr const char* CSettings::SETTING_SERVICES_WEBSERVER;
+constexpr const char* CSettings::SETTING_SERVICES_WEBSERVERPORT;
+constexpr const char* CSettings::SETTING_SERVICES_WEBSERVERAUTHENTICATION;
+constexpr const char* CSettings::SETTING_SERVICES_WEBSERVERUSERNAME;
+constexpr const char* CSettings::SETTING_SERVICES_WEBSERVERPASSWORD;
+constexpr const char* CSettings::SETTING_SERVICES_WEBSERVERSSL;
+constexpr const char* CSettings::SETTING_SERVICES_WEBSKIN;
+constexpr const char* CSettings::SETTING_SERVICES_ESENABLED;
+constexpr const char* CSettings::SETTING_SERVICES_ESPORT;
+constexpr const char* CSettings::SETTING_SERVICES_ESPORTRANGE;
+constexpr const char* CSettings::SETTING_SERVICES_ESMAXCLIENTS;
+constexpr const char* CSettings::SETTING_SERVICES_ESALLINTERFACES;
+constexpr const char* CSettings::SETTING_SERVICES_ESINITIALDELAY;
+constexpr const char* CSettings::SETTING_SERVICES_ESCONTINUOUSDELAY;
+constexpr const char* CSettings::SETTING_SERVICES_ZEROCONF;
+constexpr const char* CSettings::SETTING_SERVICES_AIRPLAY;
+constexpr const char* CSettings::SETTING_SERVICES_AIRPLAYVOLUMECONTROL;
+constexpr const char* CSettings::SETTING_SERVICES_USEAIRPLAYPASSWORD;
+constexpr const char* CSettings::SETTING_SERVICES_AIRPLAYPASSWORD;
+constexpr const char* CSettings::SETTING_SERVICES_AIRPLAYVIDEOSUPPORT;
+constexpr const char* CSettings::SETTING_SMB_WINSSERVER;
+constexpr const char* CSettings::SETTING_SMB_WORKGROUP;
+constexpr const char* CSettings::SETTING_SMB_MINPROTOCOL;
+constexpr const char* CSettings::SETTING_SMB_MAXPROTOCOL;
+constexpr const char* CSettings::SETTING_SMB_LEGACYSECURITY;
+constexpr const char* CSettings::SETTING_SERVICES_WSDISCOVERY;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_MONITOR;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_SCREEN;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_WHITELIST;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_RESOLUTION;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_SCREENMODE;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_FAKEFULLSCREEN;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_BLANKDISPLAYS;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_STEREOSCOPICMODE;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_PREFEREDSTEREOSCOPICMODE;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_NOOFBUFFERS;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_3DLUT;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_DISPLAYPROFILE;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_GUICALIBRATION;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_TESTPATTERN;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_FRAMEPACKING;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_10BITSURFACES;
+constexpr const char* CSettings::SETTING_VIDEOSCREEN_GUISDRPEAKLUMINANCE;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_AUDIODEVICE;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_CHANNELS;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_CONFIG;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_SAMPLERATE;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_STEREOUPMIX;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_MAINTAINORIGINALVOLUME;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_PROCESSQUALITY;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_ATEMPOTHRESHOLD;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_STREAMSILENCE;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_STREAMNOISE;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_GUISOUNDMODE;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_GUISOUNDVOLUME;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGHDEVICE;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_AC3PASSTHROUGH;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_AC3TRANSCODE;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_EAC3PASSTHROUGH;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_DTSPASSTHROUGH;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_TRUEHDPASSTHROUGH;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_DTSHDPASSTHROUGH;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_DTSHDCOREFALLBACK;
+constexpr const char* CSettings::SETTING_AUDIOOUTPUT_VOLUMESTEPS;
+constexpr const char* CSettings::SETTING_INPUT_PERIPHERALS;
+constexpr const char* CSettings::SETTING_INPUT_PERIPHERALLIBRARIES;
+constexpr const char* CSettings::SETTING_INPUT_ENABLEMOUSE;
+constexpr const char* CSettings::SETTING_INPUT_ASKNEWCONTROLLERS;
+constexpr const char* CSettings::SETTING_INPUT_CONTROLLERCONFIG;
+constexpr const char* CSettings::SETTING_INPUT_RUMBLENOTIFY;
+constexpr const char* CSettings::SETTING_INPUT_TESTRUMBLE;
+constexpr const char* CSettings::SETTING_INPUT_CONTROLLERPOWEROFF;
+constexpr const char* CSettings::SETTING_INPUT_APPLEREMOTEMODE;
+constexpr const char* CSettings::SETTING_INPUT_APPLEREMOTEALWAYSON;
+constexpr const char* CSettings::SETTING_INPUT_APPLEREMOTESEQUENCETIME;
+constexpr const char* CSettings::SETTING_INPUT_SIRIREMOTEIDLETIMERENABLED;
+constexpr const char* CSettings::SETTING_INPUT_SIRIREMOTEIDLETIME;
+constexpr const char* CSettings::SETTING_INPUT_SIRIREMOTEHORIZONTALSENSITIVITY;
+constexpr const char* CSettings::SETTING_INPUT_SIRIREMOTEVERTICALSENSITIVITY;
+constexpr const char* CSettings::SETTING_INPUT_TVOSUSEKODIKEYBOARD;
+constexpr const char* CSettings::SETTING_NETWORK_USEHTTPPROXY;
+constexpr const char* CSettings::SETTING_NETWORK_HTTPPROXYTYPE;
+constexpr const char* CSettings::SETTING_NETWORK_HTTPPROXYSERVER;
+constexpr const char* CSettings::SETTING_NETWORK_HTTPPROXYPORT;
+constexpr const char* CSettings::SETTING_NETWORK_HTTPPROXYUSERNAME;
+constexpr const char* CSettings::SETTING_NETWORK_HTTPPROXYPASSWORD;
+constexpr const char* CSettings::SETTING_NETWORK_BANDWIDTH;
+constexpr const char* CSettings::SETTING_POWERMANAGEMENT_DISPLAYSOFF;
+constexpr const char* CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNTIME;
+constexpr const char* CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNSTATE;
+constexpr const char* CSettings::SETTING_POWERMANAGEMENT_WAKEONACCESS;
+constexpr const char* CSettings::SETTING_POWERMANAGEMENT_WAITFORNETWORK;
+constexpr const char* CSettings::SETTING_DEBUG_SHOWLOGINFO;
+constexpr const char* CSettings::SETTING_DEBUG_EXTRALOGGING;
+constexpr const char* CSettings::SETTING_DEBUG_SETEXTRALOGLEVEL;
+constexpr const char* CSettings::SETTING_DEBUG_SCREENSHOTPATH;
+constexpr const char* CSettings::SETTING_DEBUG_SHARE_LOG;
+constexpr const char* CSettings::SETTING_EVENTLOG_ENABLED;
+constexpr const char* CSettings::SETTING_EVENTLOG_ENABLED_NOTIFICATIONS;
+constexpr const char* CSettings::SETTING_EVENTLOG_SHOW;
+constexpr const char* CSettings::SETTING_MASTERLOCK_LOCKCODE;
+constexpr const char* CSettings::SETTING_MASTERLOCK_STARTUPLOCK;
+constexpr const char* CSettings::SETTING_MASTERLOCK_MAXRETRIES;
+constexpr const char* CSettings::SETTING_CACHE_HARDDISK;
+constexpr const char* CSettings::SETTING_CACHEVIDEO_DVDROM;
+constexpr const char* CSettings::SETTING_CACHEVIDEO_LAN;
+constexpr const char* CSettings::SETTING_CACHEVIDEO_INTERNET;
+constexpr const char* CSettings::SETTING_CACHEAUDIO_DVDROM;
+constexpr const char* CSettings::SETTING_CACHEAUDIO_LAN;
+constexpr const char* CSettings::SETTING_CACHEAUDIO_INTERNET;
+constexpr const char* CSettings::SETTING_CACHEDVD_DVDROM;
+constexpr const char* CSettings::SETTING_CACHEDVD_LAN;
+constexpr const char* CSettings::SETTING_CACHEUNKNOWN_INTERNET;
+constexpr const char* CSettings::SETTING_SYSTEM_PLAYLISTSPATH;
+constexpr const char* CSettings::SETTING_ADDONS_AUTOUPDATES;
+constexpr const char* CSettings::SETTING_ADDONS_NOTIFICATIONS;
+constexpr const char* CSettings::SETTING_ADDONS_SHOW_RUNNING;
+constexpr const char* CSettings::SETTING_ADDONS_ALLOW_UNKNOWN_SOURCES;
+constexpr const char* CSettings::SETTING_ADDONS_UPDATEMODE;
+constexpr const char* CSettings::SETTING_ADDONS_MANAGE_DEPENDENCIES;
+constexpr const char* CSettings::SETTING_ADDONS_REMOVE_ORPHANED_DEPENDENCIES;
+constexpr const char* CSettings::SETTING_GENERAL_ADDONFOREIGNFILTER;
+constexpr const char* CSettings::SETTING_GENERAL_ADDONBROKENFILTER;
+constexpr const char* CSettings::SETTING_SOURCE_VIDEOS;
+constexpr const char* CSettings::SETTING_SOURCE_MUSIC;
+constexpr const char* CSettings::SETTING_SOURCE_PICTURES;
+//! @todo: remove in c++17
+
+bool CSettings::Initialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (m_initialized)
+ return false;
+
+ // register custom setting types
+ InitializeSettingTypes();
+ // register custom setting controls
+ InitializeControls();
+
+ // option fillers and conditions need to be
+ // initialized before the setting definitions
+ InitializeOptionFillers();
+ InitializeConditions();
+
+ // load the settings definitions
+ if (!InitializeDefinitions())
+ return false;
+
+ GetSettingsManager()->SetInitialized();
+
+ InitializeISettingsHandlers();
+ InitializeISubSettings();
+ InitializeISettingCallbacks();
+
+ m_initialized = true;
+
+ return true;
+}
+
+void CSettings::RegisterSubSettings(ISubSettings* subSettings)
+{
+ if (subSettings == nullptr)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_subSettings.insert(subSettings);
+}
+
+void CSettings::UnregisterSubSettings(ISubSettings* subSettings)
+{
+ if (subSettings == nullptr)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_subSettings.erase(subSettings);
+}
+
+bool CSettings::Load()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ return Load(profileManager->GetSettingsFile());
+}
+
+bool CSettings::Load(const std::string &file)
+{
+ CXBMCTinyXML xmlDoc;
+ bool updated = false;
+ if (!XFILE::CFile::Exists(file) || !xmlDoc.LoadFile(file) ||
+ !Load(xmlDoc.RootElement(), updated))
+ {
+ CLog::Log(LOGERROR, "CSettings: unable to load settings from {}, creating new default settings",
+ file);
+ if (!Reset())
+ return false;
+
+ if (!Load(file))
+ return false;
+ }
+ // if the settings had to be updated, we need to save the changes
+ else if (updated)
+ return Save(file);
+
+ return true;
+}
+
+bool CSettings::Load(const TiXmlElement* root)
+{
+ bool updated = false;
+ return Load(root, updated);
+}
+
+bool CSettings::Save()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ return Save(profileManager->GetSettingsFile());
+}
+
+bool CSettings::Save(const std::string &file)
+{
+ CXBMCTinyXML xmlDoc;
+ if (!SaveValuesToXml(xmlDoc))
+ return false;
+
+ TiXmlElement* root = xmlDoc.RootElement();
+ if (root == nullptr)
+ return false;
+
+ if (!Save(root))
+ return false;
+
+ return xmlDoc.SaveFile(file);
+}
+
+bool CSettings::Save(TiXmlNode* root) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // save any ISubSettings implementations
+ for (const auto& subSetting : m_subSettings)
+ {
+ if (!subSetting->Save(root))
+ return false;
+ }
+
+ return true;
+}
+
+bool CSettings::LoadSetting(const TiXmlNode *node, const std::string &settingId)
+{
+ return GetSettingsManager()->LoadSetting(node, settingId);
+}
+
+bool CSettings::GetBool(const std::string& id) const
+{
+ // Backward compatibility (skins use this setting)
+ if (StringUtils::EqualsNoCase(id, "lookandfeel.enablemouse"))
+ return CSettingsBase::GetBool(CSettings::SETTING_INPUT_ENABLEMOUSE);
+
+ return CSettingsBase::GetBool(id);
+}
+
+void CSettings::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (!m_initialized)
+ return;
+
+ GetSettingsManager()->Clear();
+
+ for (auto& subSetting : m_subSettings)
+ subSetting->Clear();
+
+ m_initialized = false;
+}
+
+bool CSettings::Load(const TiXmlElement* root, bool& updated)
+{
+ if (root == nullptr)
+ return false;
+
+ if (!CSettingsBase::LoadValuesFromXml(root, updated))
+ return false;
+
+ return Load(static_cast<const TiXmlNode*>(root));
+}
+
+bool CSettings::Load(const TiXmlNode* settings)
+{
+ bool ok = true;
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ for (const auto& subSetting : m_subSettings)
+ ok &= subSetting->Load(settings);
+
+ return ok;
+}
+
+bool CSettings::Initialize(const std::string &file)
+{
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(file.c_str()))
+ {
+ CLog::Log(LOGERROR, "CSettings: error loading settings definition from {}, Line {}\n{}", file,
+ xmlDoc.ErrorRow(), xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "CSettings: loaded settings definition from {}", file);
+
+ return InitializeDefinitionsFromXml(xmlDoc);
+}
+
+bool CSettings::InitializeDefinitions()
+{
+ if (!Initialize(SETTINGS_XML_FOLDER "settings.xml"))
+ {
+ CLog::Log(LOGFATAL, "Unable to load settings definitions");
+ return false;
+ }
+#if defined(TARGET_WINDOWS)
+ if (CFile::Exists(SETTINGS_XML_FOLDER "windows.xml") && !Initialize(SETTINGS_XML_FOLDER "windows.xml"))
+ CLog::Log(LOGFATAL, "Unable to load windows-specific settings definitions");
+#if defined(TARGET_WINDOWS_DESKTOP)
+ if (CFile::Exists(SETTINGS_XML_FOLDER "win32.xml") && !Initialize(SETTINGS_XML_FOLDER "win32.xml"))
+ CLog::Log(LOGFATAL, "Unable to load win32-specific settings definitions");
+#elif defined(TARGET_WINDOWS_STORE)
+ if (CFile::Exists(SETTINGS_XML_FOLDER "win10.xml") && !Initialize(SETTINGS_XML_FOLDER "win10.xml"))
+ CLog::Log(LOGFATAL, "Unable to load win10-specific settings definitions");
+#endif
+#elif defined(TARGET_ANDROID)
+ if (CFile::Exists(SETTINGS_XML_FOLDER "android.xml") && !Initialize(SETTINGS_XML_FOLDER "android.xml"))
+ CLog::Log(LOGFATAL, "Unable to load android-specific settings definitions");
+#elif defined(TARGET_FREEBSD)
+ if (CFile::Exists(SETTINGS_XML_FOLDER "freebsd.xml") && !Initialize(SETTINGS_XML_FOLDER "freebsd.xml"))
+ CLog::Log(LOGFATAL, "Unable to load freebsd-specific settings definitions");
+#elif defined(TARGET_LINUX)
+ if (CFile::Exists(SETTINGS_XML_FOLDER "linux.xml") && !Initialize(SETTINGS_XML_FOLDER "linux.xml"))
+ CLog::Log(LOGFATAL, "Unable to load linux-specific settings definitions");
+#elif defined(TARGET_DARWIN)
+ if (CFile::Exists(SETTINGS_XML_FOLDER "darwin.xml") && !Initialize(SETTINGS_XML_FOLDER "darwin.xml"))
+ CLog::Log(LOGFATAL, "Unable to load darwin-specific settings definitions");
+#if defined(TARGET_DARWIN_OSX)
+ if (CFile::Exists(SETTINGS_XML_FOLDER "darwin_osx.xml") && !Initialize(SETTINGS_XML_FOLDER "darwin_osx.xml"))
+ CLog::Log(LOGFATAL, "Unable to load osx-specific settings definitions");
+#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
+
+#if defined(PLATFORM_SETTINGS_FILE)
+ if (CFile::Exists(SETTINGS_XML_FOLDER DEF_TO_STR_VALUE(PLATFORM_SETTINGS_FILE)) && !Initialize(SETTINGS_XML_FOLDER DEF_TO_STR_VALUE(PLATFORM_SETTINGS_FILE)))
+ CLog::Log(LOGFATAL, "Unable to load platform-specific settings definitions ({})",
+ DEF_TO_STR_VALUE(PLATFORM_SETTINGS_FILE));
+#endif
+
+ // load any custom visibility and default values before loading the special
+ // appliance.xml so that appliances are able to overwrite even those values
+ InitializeVisibility();
+ InitializeDefaults();
+
+ if (CFile::Exists(SETTINGS_XML_FOLDER "appliance.xml") && !Initialize(SETTINGS_XML_FOLDER "appliance.xml"))
+ CLog::Log(LOGFATAL, "Unable to load appliance-specific settings definitions");
+
+ return true;
+}
+
+void CSettings::InitializeSettingTypes()
+{
+ GetSettingsManager()->RegisterSettingType("addon", this);
+ GetSettingsManager()->RegisterSettingType("date", this);
+ GetSettingsManager()->RegisterSettingType("path", this);
+ GetSettingsManager()->RegisterSettingType("time", this);
+}
+
+void CSettings::InitializeControls()
+{
+ GetSettingsManager()->RegisterSettingControl("toggle", this);
+ GetSettingsManager()->RegisterSettingControl("spinner", this);
+ GetSettingsManager()->RegisterSettingControl("edit", this);
+ GetSettingsManager()->RegisterSettingControl("button", this);
+ GetSettingsManager()->RegisterSettingControl("list", this);
+ GetSettingsManager()->RegisterSettingControl("slider", this);
+ GetSettingsManager()->RegisterSettingControl("range", this);
+ GetSettingsManager()->RegisterSettingControl("title", this);
+ GetSettingsManager()->RegisterSettingControl("colorbutton", this);
+}
+
+void CSettings::InitializeVisibility()
+{
+ // hide some settings if necessary
+#if defined(TARGET_DARWIN_EMBEDDED)
+ std::shared_ptr<CSettingString> timezonecountry = std::static_pointer_cast<CSettingString>(GetSettingsManager()->GetSetting(CSettings::SETTING_LOCALE_TIMEZONECOUNTRY));
+ std::shared_ptr<CSettingString> timezone = std::static_pointer_cast<CSettingString>(GetSettingsManager()->GetSetting(CSettings::SETTING_LOCALE_TIMEZONE));
+
+ timezonecountry->SetRequirementsMet(false);
+ timezone->SetRequirementsMet(false);
+#endif
+}
+
+void CSettings::InitializeDefaults()
+{
+ // set some default values if necessary
+#if defined(TARGET_POSIX)
+ std::shared_ptr<CSettingString> timezonecountry = std::static_pointer_cast<CSettingString>(GetSettingsManager()->GetSetting(CSettings::SETTING_LOCALE_TIMEZONECOUNTRY));
+ std::shared_ptr<CSettingString> timezone = std::static_pointer_cast<CSettingString>(GetSettingsManager()->GetSetting(CSettings::SETTING_LOCALE_TIMEZONE));
+
+ if (timezonecountry->IsVisible())
+ timezonecountry->SetDefault(g_timezone.GetCountryByTimezone(g_timezone.GetOSConfiguredTimezone()));
+ if (timezone->IsVisible())
+ timezone->SetDefault(g_timezone.GetOSConfiguredTimezone());
+#endif // defined(TARGET_POSIX)
+
+#if defined(TARGET_WINDOWS)
+ // We prefer a fake fullscreen mode (window covering the screen rather than dedicated fullscreen)
+ // as it works nicer with switching to other applications. However on some systems vsync is broken
+ // when we do this (eg non-Aero on ATI in particular) and on others (AppleTV) we can't get XBMC to
+ // the front
+ if (g_sysinfo.IsAeroDisabled())
+ {
+ auto setting = GetSettingsManager()->GetSetting(CSettings::SETTING_VIDEOSCREEN_FAKEFULLSCREEN);
+ if (!setting)
+ CLog::Log(LOGERROR, "Failed to load setting for: {}",
+ CSettings::SETTING_VIDEOSCREEN_FAKEFULLSCREEN);
+ else
+ std::static_pointer_cast<CSettingBool>(setting)->SetDefault(false);
+ }
+#endif
+
+ if (CServiceBroker::GetAppParams()->IsStandAlone())
+ {
+ auto setting =
+ GetSettingsManager()->GetSetting(CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNSTATE);
+ if (!setting)
+ CLog::Log(LOGERROR, "Failed to load setting for: {}",
+ CSettings::SETTING_POWERMANAGEMENT_SHUTDOWNSTATE);
+ else
+ std::static_pointer_cast<CSettingInt>(setting)->SetDefault(POWERSTATE_SHUTDOWN);
+ }
+
+ // Initialize deviceUUID if not already set, used in zeroconf advertisements.
+ std::shared_ptr<CSettingString> deviceUUID = std::static_pointer_cast<CSettingString>(GetSettingsManager()->GetSetting(CSettings::SETTING_SERVICES_DEVICEUUID));
+ if (deviceUUID->GetValue().empty())
+ {
+ const std::string& uuid = StringUtils::CreateUUID();
+ auto setting = GetSettingsManager()->GetSetting(CSettings::SETTING_SERVICES_DEVICEUUID);
+ if (!setting)
+ CLog::Log(LOGERROR, "Failed to load setting for: {}", CSettings::SETTING_SERVICES_DEVICEUUID);
+ else
+ std::static_pointer_cast<CSettingString>(setting)->SetValue(uuid);
+ }
+}
+
+void CSettings::InitializeOptionFillers()
+{
+ // register setting option fillers
+#ifdef HAS_DVD_DRIVE
+ GetSettingsManager()->RegisterSettingOptionsFiller("audiocdactions", MEDIA_DETECT::CAutorun::SettingOptionAudioCdActionsFiller);
+#endif
+ GetSettingsManager()->RegisterSettingOptionsFiller("charsets", CCharsetConverter::SettingOptionsCharsetsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("fonts", GUIFontManager::SettingOptionsFontsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller(
+ "subtitlesfonts", SUBTITLES::CSubtitlesSettings::SettingOptionsSubtitleFontsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("languagenames", CLangInfo::SettingOptionsLanguageNamesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("refreshchangedelays", CDisplaySettings::SettingOptionsRefreshChangeDelaysFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("refreshrates", CDisplaySettings::SettingOptionsRefreshRatesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("regions", CLangInfo::SettingOptionsRegionsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("shortdateformats", CLangInfo::SettingOptionsShortDateFormatsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("longdateformats", CLangInfo::SettingOptionsLongDateFormatsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("timeformats", CLangInfo::SettingOptionsTimeFormatsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("24hourclockformats", CLangInfo::SettingOptions24HourClockFormatsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("speedunits", CLangInfo::SettingOptionsSpeedUnitsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("temperatureunits", CLangInfo::SettingOptionsTemperatureUnitsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("rendermethods", CBaseRenderer::SettingOptionsRenderMethodsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("modes", CDisplaySettings::SettingOptionsModesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("resolutions", CDisplaySettings::SettingOptionsResolutionsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("screens", CDisplaySettings::SettingOptionsDispModeFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("stereoscopicmodes", CDisplaySettings::SettingOptionsStereoscopicModesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("preferedstereoscopicviewmodes", CDisplaySettings::SettingOptionsPreferredStereoscopicViewModesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("monitors", CDisplaySettings::SettingOptionsMonitorsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("cmsmodes", CDisplaySettings::SettingOptionsCmsModesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("cmswhitepoints", CDisplaySettings::SettingOptionsCmsWhitepointsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("cmsprimaries", CDisplaySettings::SettingOptionsCmsPrimariesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("cmsgammamodes", CDisplaySettings::SettingOptionsCmsGammaModesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("videoseeksteps", CSeekHandler::SettingOptionsSeekStepsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("startupwindows", ADDON::CSkinInfo::SettingOptionsStartupWindowsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("audiostreamlanguages", CLangInfo::SettingOptionsAudioStreamLanguagesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("subtitlestreamlanguages", CLangInfo::SettingOptionsSubtitleStreamLanguagesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("subtitledownloadlanguages", CLangInfo::SettingOptionsSubtitleDownloadlanguagesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("iso6391languages", CLangInfo::SettingOptionsISO6391LanguagesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("skincolors", ADDON::CSkinInfo::SettingOptionsSkinColorsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("skinfonts", ADDON::CSkinInfo::SettingOptionsSkinFontsFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("skinthemes", ADDON::CSkinInfo::SettingOptionsSkinThemesFiller);
+#ifdef TARGET_LINUX
+ GetSettingsManager()->RegisterSettingOptionsFiller("timezonecountries", CPosixTimezone::SettingOptionsTimezoneCountriesFiller);
+ GetSettingsManager()->RegisterSettingOptionsFiller("timezones", CPosixTimezone::SettingOptionsTimezonesFiller);
+#endif
+ GetSettingsManager()->RegisterSettingOptionsFiller("keyboardlayouts", CKeyboardLayoutManager::SettingOptionsKeyboardLayoutsFiller);
+}
+
+void CSettings::UninitializeOptionFillers()
+{
+ GetSettingsManager()->UnregisterSettingOptionsFiller("audiocdactions");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("audiocdencoders");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("charsets");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("fontheights");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("fonts");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("subtitlesfonts");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("languagenames");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("refreshchangedelays");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("refreshrates");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("regions");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("shortdateformats");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("longdateformats");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("timeformats");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("24hourclockformats");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("speedunits");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("temperatureunits");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("rendermethods");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("resolutions");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("screens");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("stereoscopicmodes");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("preferedstereoscopicviewmodes");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("monitors");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("cmsmodes");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("cmswhitepoints");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("cmsprimaries");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("cmsgammamodes");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("videoseeksteps");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("shutdownstates");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("startupwindows");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("audiostreamlanguages");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("subtitlestreamlanguages");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("subtitledownloadlanguages");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("iso6391languages");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("skincolors");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("skinfonts");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("skinthemes");
+#if defined(TARGET_LINUX)
+ GetSettingsManager()->UnregisterSettingOptionsFiller("timezonecountries");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("timezones");
+#endif // defined(TARGET_LINUX)
+ GetSettingsManager()->UnregisterSettingOptionsFiller("verticalsyncs");
+ GetSettingsManager()->UnregisterSettingOptionsFiller("keyboardlayouts");
+}
+
+void CSettings::InitializeConditions()
+{
+ CSettingConditions::Initialize();
+
+ // add basic conditions
+ const std::set<std::string> &simpleConditions = CSettingConditions::GetSimpleConditions();
+ for (std::set<std::string>::const_iterator itCondition = simpleConditions.begin(); itCondition != simpleConditions.end(); ++itCondition)
+ GetSettingsManager()->AddCondition(*itCondition);
+
+ // add more complex conditions
+ const std::map<std::string, SettingConditionCheck> &complexConditions = CSettingConditions::GetComplexConditions();
+ for (std::map<std::string, SettingConditionCheck>::const_iterator itCondition = complexConditions.begin(); itCondition != complexConditions.end(); ++itCondition)
+ GetSettingsManager()->AddDynamicCondition(itCondition->first, itCondition->second);
+}
+
+void CSettings::UninitializeConditions()
+{
+ CSettingConditions::Deinitialize();
+}
+
+void CSettings::InitializeISettingsHandlers()
+{
+ // register ISettingsHandler implementations
+ // The order of these matters! Handlers are processed in the order they were registered.
+ GetSettingsManager()->RegisterSettingsHandler(&CMediaSourceSettings::GetInstance());
+#ifdef HAS_UPNP
+ GetSettingsManager()->RegisterSettingsHandler(&CUPnPSettings::GetInstance());
+#endif
+ GetSettingsManager()->RegisterSettingsHandler(&CWakeOnAccess::GetInstance());
+ GetSettingsManager()->RegisterSettingsHandler(&CRssManager::GetInstance());
+ GetSettingsManager()->RegisterSettingsHandler(&g_langInfo);
+#if defined(TARGET_LINUX) && !defined(TARGET_ANDROID) && !defined(__UCLIBC__)
+ GetSettingsManager()->RegisterSettingsHandler(&g_timezone);
+#endif
+ GetSettingsManager()->RegisterSettingsHandler(&CMediaSettings::GetInstance());
+}
+
+void CSettings::UninitializeISettingsHandlers()
+{
+ // unregister ISettingsHandler implementations
+ GetSettingsManager()->UnregisterSettingsHandler(&CMediaSettings::GetInstance());
+#if defined(TARGET_LINUX)
+ GetSettingsManager()->UnregisterSettingsHandler(&g_timezone);
+#endif // defined(TARGET_LINUX)
+ GetSettingsManager()->UnregisterSettingsHandler(&g_langInfo);
+ GetSettingsManager()->UnregisterSettingsHandler(&CRssManager::GetInstance());
+ GetSettingsManager()->UnregisterSettingsHandler(&CWakeOnAccess::GetInstance());
+#ifdef HAS_UPNP
+ GetSettingsManager()->UnregisterSettingsHandler(&CUPnPSettings::GetInstance());
+#endif
+ GetSettingsManager()->UnregisterSettingsHandler(&CMediaSourceSettings::GetInstance());
+}
+
+void CSettings::InitializeISubSettings()
+{
+ // register ISubSettings implementations
+ RegisterSubSettings(&CDisplaySettings::GetInstance());
+ RegisterSubSettings(&CMediaSettings::GetInstance());
+ RegisterSubSettings(&CSkinSettings::GetInstance());
+ RegisterSubSettings(&g_sysinfo);
+ RegisterSubSettings(&CViewStateSettings::GetInstance());
+}
+
+void CSettings::UninitializeISubSettings()
+{
+ // unregister ISubSettings implementations
+ UnregisterSubSettings(&CDisplaySettings::GetInstance());
+ UnregisterSubSettings(&CMediaSettings::GetInstance());
+ UnregisterSubSettings(&CSkinSettings::GetInstance());
+ UnregisterSubSettings(&g_sysinfo);
+ UnregisterSubSettings(&CViewStateSettings::GetInstance());
+}
+
+void CSettings::InitializeISettingCallbacks()
+{
+ // register any ISettingCallback implementations
+ std::set<std::string> settingSet;
+ settingSet.insert(CSettings::SETTING_MUSICLIBRARY_CLEANUP);
+ settingSet.insert(CSettings::SETTING_MUSICLIBRARY_EXPORT);
+ settingSet.insert(CSettings::SETTING_MUSICLIBRARY_IMPORT);
+ settingSet.insert(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ settingSet.insert(CSettings::SETTING_VIDEOLIBRARY_FLATTENTVSHOWS);
+ settingSet.insert(CSettings::SETTING_VIDEOLIBRARY_GROUPMOVIESETS);
+ settingSet.insert(CSettings::SETTING_VIDEOLIBRARY_CLEANUP);
+ settingSet.insert(CSettings::SETTING_VIDEOLIBRARY_IMPORT);
+ settingSet.insert(CSettings::SETTING_VIDEOLIBRARY_EXPORT);
+ settingSet.insert(CSettings::SETTING_VIDEOLIBRARY_SHOWUNWATCHEDPLOTS);
+ GetSettingsManager()->RegisterCallback(&CMediaSettings::GetInstance(), settingSet);
+
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_VIDEOSCREEN_SCREEN);
+ settingSet.insert(CSettings::SETTING_VIDEOSCREEN_RESOLUTION);
+ settingSet.insert(CSettings::SETTING_VIDEOSCREEN_SCREENMODE);
+ settingSet.insert(CSettings::SETTING_VIDEOSCREEN_MONITOR);
+ settingSet.insert(CSettings::SETTING_VIDEOSCREEN_PREFEREDSTEREOSCOPICMODE);
+ settingSet.insert(CSettings::SETTING_VIDEOSCREEN_3DLUT);
+ settingSet.insert(CSettings::SETTING_VIDEOSCREEN_DISPLAYPROFILE);
+ settingSet.insert(CSettings::SETTING_VIDEOSCREEN_BLANKDISPLAYS);
+ settingSet.insert(CSettings::SETTING_VIDEOSCREEN_WHITELIST);
+ settingSet.insert(CSettings::SETTING_VIDEOSCREEN_10BITSURFACES);
+ GetSettingsManager()->RegisterCallback(&CDisplaySettings::GetInstance(), settingSet);
+
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_SUBTITLES_CHARSET);
+ settingSet.insert(CSettings::SETTING_LOCALE_CHARSET);
+ GetSettingsManager()->RegisterCallback(&g_charsetConverter, settingSet);
+
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_LOCALE_AUDIOLANGUAGE);
+ settingSet.insert(CSettings::SETTING_LOCALE_SUBTITLELANGUAGE);
+ settingSet.insert(CSettings::SETTING_LOCALE_LANGUAGE);
+ settingSet.insert(CSettings::SETTING_LOCALE_COUNTRY);
+ settingSet.insert(CSettings::SETTING_LOCALE_SHORTDATEFORMAT);
+ settingSet.insert(CSettings::SETTING_LOCALE_LONGDATEFORMAT);
+ settingSet.insert(CSettings::SETTING_LOCALE_TIMEFORMAT);
+ settingSet.insert(CSettings::SETTING_LOCALE_USE24HOURCLOCK);
+ settingSet.insert(CSettings::SETTING_LOCALE_TEMPERATUREUNIT);
+ settingSet.insert(CSettings::SETTING_LOCALE_SPEEDUNIT);
+ GetSettingsManager()->RegisterCallback(&g_langInfo, settingSet);
+
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_MASTERLOCK_LOCKCODE);
+ GetSettingsManager()->RegisterCallback(&g_passwordManager, settingSet);
+
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_LOOKANDFEEL_RSSEDIT);
+ GetSettingsManager()->RegisterCallback(&CRssManager::GetInstance(), settingSet);
+
+#if defined(TARGET_LINUX)
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_LOCALE_TIMEZONE);
+ settingSet.insert(CSettings::SETTING_LOCALE_TIMEZONECOUNTRY);
+ GetSettingsManager()->RegisterCallback(&g_timezone, settingSet);
+#endif
+
+#if defined(TARGET_DARWIN_OSX)
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_INPUT_APPLEREMOTEMODE);
+ settingSet.insert(CSettings::SETTING_INPUT_APPLEREMOTEALWAYSON);
+ GetSettingsManager()->RegisterCallback(&XBMCHelper::GetInstance(), settingSet);
+#endif
+
+#if defined(TARGET_DARWIN_TVOS)
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_INPUT_SIRIREMOTEIDLETIMERENABLED);
+ settingSet.insert(CSettings::SETTING_INPUT_SIRIREMOTEIDLETIME);
+ settingSet.insert(CSettings::SETTING_INPUT_SIRIREMOTEHORIZONTALSENSITIVITY);
+ settingSet.insert(CSettings::SETTING_INPUT_SIRIREMOTEVERTICALSENSITIVITY);
+ GetSettingsManager()->RegisterCallback(&CTVOSInputSettings::GetInstance(), settingSet);
+#endif
+
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_ADDONS_SHOW_RUNNING);
+ settingSet.insert(CSettings::SETTING_ADDONS_MANAGE_DEPENDENCIES);
+ settingSet.insert(CSettings::SETTING_ADDONS_REMOVE_ORPHANED_DEPENDENCIES);
+ settingSet.insert(CSettings::SETTING_ADDONS_ALLOW_UNKNOWN_SOURCES);
+ GetSettingsManager()->RegisterCallback(&ADDON::CAddonSystemSettings::GetInstance(), settingSet);
+
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_POWERMANAGEMENT_WAKEONACCESS);
+ GetSettingsManager()->RegisterCallback(&CWakeOnAccess::GetInstance(), settingSet);
+
+#ifdef HAVE_LIBBLURAY
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_DISC_PLAYBACK);
+ GetSettingsManager()->RegisterCallback(&CDiscSettings::GetInstance(), settingSet);
+#endif
+}
+
+void CSettings::UninitializeISettingCallbacks()
+{
+ GetSettingsManager()->UnregisterCallback(&CMediaSettings::GetInstance());
+ GetSettingsManager()->UnregisterCallback(&CDisplaySettings::GetInstance());
+ GetSettingsManager()->UnregisterCallback(&g_charsetConverter);
+ GetSettingsManager()->UnregisterCallback(&g_langInfo);
+ GetSettingsManager()->UnregisterCallback(&g_passwordManager);
+ GetSettingsManager()->UnregisterCallback(&CRssManager::GetInstance());
+#if defined(TARGET_LINUX)
+ GetSettingsManager()->UnregisterCallback(&g_timezone);
+#endif // defined(TARGET_LINUX)
+#if defined(TARGET_DARWIN_OSX)
+ GetSettingsManager()->UnregisterCallback(&XBMCHelper::GetInstance());
+#endif
+ GetSettingsManager()->UnregisterCallback(&CWakeOnAccess::GetInstance());
+#ifdef HAVE_LIBBLURAY
+ GetSettingsManager()->UnregisterCallback(&CDiscSettings::GetInstance());
+#endif
+}
+
+bool CSettings::Reset()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ const std::string settingsFile = profileManager->GetSettingsFile();
+
+ // try to delete the settings file
+ if (XFILE::CFile::Exists(settingsFile, false) && !XFILE::CFile::Delete(settingsFile))
+ CLog::Log(LOGWARNING, "Unable to delete old settings file at {}", settingsFile);
+
+ // unload any loaded settings
+ Unload();
+
+ // try to save the default settings
+ if (!Save())
+ {
+ CLog::Log(LOGWARNING, "Failed to save the default settings to {}", settingsFile);
+ return false;
+ }
+
+ return true;
+}
diff --git a/xbmc/settings/Settings.h b/xbmc/settings/Settings.h
new file mode 100644
index 0000000..cdd2d31
--- /dev/null
+++ b/xbmc/settings/Settings.h
@@ -0,0 +1,598 @@
+/*
+ * 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 "settings/ISubSettings.h"
+#include "settings/SettingControl.h"
+#include "settings/SettingCreator.h"
+#include "settings/SettingsBase.h"
+
+#include <string>
+
+class CSettingList;
+class TiXmlNode;
+
+/*!
+ \brief Wrapper around CSettingsManager responsible for properly setting up
+ the settings manager and registering all the callbacks, handlers and custom
+ setting types.
+ \sa CSettingsManager
+ */
+class CSettings : public CSettingsBase, public CSettingCreator, public CSettingControlCreator
+ , private ISubSettings
+{
+public:
+ static constexpr auto SETTING_LOOKANDFEEL_SKIN = "lookandfeel.skin";
+ static constexpr auto SETTING_LOOKANDFEEL_SKINSETTINGS = "lookandfeel.skinsettings";
+ static constexpr auto SETTING_LOOKANDFEEL_SKINTHEME = "lookandfeel.skintheme";
+ static constexpr auto SETTING_LOOKANDFEEL_SKINCOLORS = "lookandfeel.skincolors";
+ static constexpr auto SETTING_LOOKANDFEEL_FONT = "lookandfeel.font";
+ static constexpr auto SETTING_LOOKANDFEEL_SKINZOOM = "lookandfeel.skinzoom";
+ static constexpr auto SETTING_LOOKANDFEEL_STARTUPACTION = "lookandfeel.startupaction";
+ static constexpr auto SETTING_LOOKANDFEEL_STARTUPWINDOW = "lookandfeel.startupwindow";
+ static constexpr auto SETTING_LOOKANDFEEL_SOUNDSKIN = "lookandfeel.soundskin";
+ static constexpr auto SETTING_LOOKANDFEEL_ENABLERSSFEEDS = "lookandfeel.enablerssfeeds";
+ static constexpr auto SETTING_LOOKANDFEEL_RSSEDIT = "lookandfeel.rssedit";
+ static constexpr auto SETTING_LOOKANDFEEL_STEREOSTRENGTH = "lookandfeel.stereostrength";
+ static constexpr auto SETTING_LOCALE_LANGUAGE = "locale.language";
+ static constexpr auto SETTING_LOCALE_COUNTRY = "locale.country";
+ static constexpr auto SETTING_LOCALE_CHARSET = "locale.charset";
+ static constexpr auto SETTING_LOCALE_KEYBOARDLAYOUTS = "locale.keyboardlayouts";
+ static constexpr auto SETTING_LOCALE_ACTIVEKEYBOARDLAYOUT = "locale.activekeyboardlayout";
+ static constexpr auto SETTING_LOCALE_TIMEZONECOUNTRY = "locale.timezonecountry";
+ static constexpr auto SETTING_LOCALE_TIMEZONE = "locale.timezone";
+ static constexpr auto SETTING_LOCALE_SHORTDATEFORMAT = "locale.shortdateformat";
+ static constexpr auto SETTING_LOCALE_LONGDATEFORMAT = "locale.longdateformat";
+ static constexpr auto SETTING_LOCALE_TIMEFORMAT = "locale.timeformat";
+ static constexpr auto SETTING_LOCALE_USE24HOURCLOCK = "locale.use24hourclock";
+ static constexpr auto SETTING_LOCALE_TEMPERATUREUNIT = "locale.temperatureunit";
+ static constexpr auto SETTING_LOCALE_SPEEDUNIT = "locale.speedunit";
+ static constexpr auto SETTING_FILELISTS_SHOWPARENTDIRITEMS = "filelists.showparentdiritems";
+ static constexpr auto SETTING_FILELISTS_SHOWEXTENSIONS = "filelists.showextensions";
+ static constexpr auto SETTING_FILELISTS_IGNORETHEWHENSORTING = "filelists.ignorethewhensorting";
+ static constexpr auto SETTING_FILELISTS_ALLOWFILEDELETION = "filelists.allowfiledeletion";
+ static constexpr auto SETTING_FILELISTS_SHOWADDSOURCEBUTTONS = "filelists.showaddsourcebuttons";
+ static constexpr auto SETTING_FILELISTS_SHOWHIDDEN = "filelists.showhidden";
+ static constexpr auto SETTING_SCREENSAVER_MODE = "screensaver.mode";
+ static constexpr auto SETTING_SCREENSAVER_SETTINGS = "screensaver.settings";
+ static constexpr auto SETTING_SCREENSAVER_PREVIEW = "screensaver.preview";
+ static constexpr auto SETTING_SCREENSAVER_TIME = "screensaver.time";
+ static constexpr auto SETTING_SCREENSAVER_USEMUSICVISINSTEAD = "screensaver.usemusicvisinstead";
+ static constexpr auto SETTING_SCREENSAVER_USEDIMONPAUSE = "screensaver.usedimonpause";
+ static constexpr auto SETTING_WINDOW_WIDTH = "window.width";
+ static constexpr auto SETTING_WINDOW_HEIGHT = "window.height";
+ static constexpr auto SETTING_VIDEOLIBRARY_SHOWUNWATCHEDPLOTS = "videolibrary.showunwatchedplots";
+ static constexpr auto SETTING_VIDEOLIBRARY_ACTORTHUMBS = "videolibrary.actorthumbs";
+ static constexpr auto SETTING_MYVIDEOS_FLATTEN = "myvideos.flatten";
+ static constexpr auto SETTING_VIDEOLIBRARY_FLATTENTVSHOWS = "videolibrary.flattentvshows";
+ static constexpr auto SETTING_VIDEOLIBRARY_TVSHOWSSELECTFIRSTUNWATCHEDITEM =
+ "videolibrary.tvshowsselectfirstunwatcheditem";
+ static constexpr auto SETTING_VIDEOLIBRARY_TVSHOWSINCLUDEALLSEASONSANDSPECIALS =
+ "videolibrary.tvshowsincludeallseasonsandspecials";
+ static constexpr auto SETTING_VIDEOLIBRARY_SHOWALLITEMS = "videolibrary.showallitems";
+ static constexpr auto SETTING_VIDEOLIBRARY_GROUPMOVIESETS = "videolibrary.groupmoviesets";
+ static constexpr auto SETTING_VIDEOLIBRARY_GROUPSINGLEITEMSETS =
+ "videolibrary.groupsingleitemsets";
+ static constexpr auto SETTING_VIDEOLIBRARY_UPDATEONSTARTUP = "videolibrary.updateonstartup";
+ static constexpr auto SETTING_VIDEOLIBRARY_BACKGROUNDUPDATE = "videolibrary.backgroundupdate";
+ static constexpr auto SETTING_VIDEOLIBRARY_CLEANUP = "videolibrary.cleanup";
+ static constexpr auto SETTING_VIDEOLIBRARY_EXPORT = "videolibrary.export";
+ static constexpr auto SETTING_VIDEOLIBRARY_IMPORT = "videolibrary.import";
+ static constexpr auto SETTING_VIDEOLIBRARY_SHOWEMPTYTVSHOWS = "videolibrary.showemptytvshows";
+ static constexpr auto SETTING_VIDEOLIBRARY_MOVIESETSFOLDER = "videolibrary.moviesetsfolder";
+ static constexpr auto SETTING_VIDEOLIBRARY_ARTWORK_LEVEL = "videolibrary.artworklevel";
+ static constexpr auto SETTING_VIDEOLIBRARY_MOVIEART_WHITELIST = "videolibrary.movieartwhitelist";
+ static constexpr auto SETTING_VIDEOLIBRARY_TVSHOWART_WHITELIST =
+ "videolibrary.tvshowartwhitelist";
+ static constexpr auto SETTING_VIDEOLIBRARY_EPISODEART_WHITELIST =
+ "videolibrary.episodeartwhitelist";
+ static constexpr auto SETTING_VIDEOLIBRARY_MUSICVIDEOART_WHITELIST =
+ "videolibrary.musicvideoartwhitelist";
+ static constexpr auto SETTING_VIDEOLIBRARY_ARTSETTINGS_UPDATED =
+ "videolibrary.artsettingsupdated";
+ static constexpr auto SETTING_VIDEOLIBRARY_SHOWPERFORMERS =
+ "videolibrary.musicvideosallperformers";
+ static constexpr auto SETTING_LOCALE_AUDIOLANGUAGE = "locale.audiolanguage";
+ static constexpr auto SETTING_VIDEOPLAYER_PREFERDEFAULTFLAG = "videoplayer.preferdefaultflag";
+ static constexpr auto SETTING_VIDEOPLAYER_AUTOPLAYNEXTITEM = "videoplayer.autoplaynextitem";
+ static constexpr auto SETTING_VIDEOPLAYER_SEEKSTEPS = "videoplayer.seeksteps";
+ static constexpr auto SETTING_VIDEOPLAYER_SEEKDELAY = "videoplayer.seekdelay";
+ static constexpr auto SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE = "videoplayer.adjustrefreshrate";
+ static constexpr auto SETTING_VIDEOPLAYER_USEDISPLAYASCLOCK = "videoplayer.usedisplayasclock";
+ static constexpr auto SETTING_VIDEOPLAYER_ERRORINASPECT = "videoplayer.errorinaspect";
+ static constexpr auto SETTING_VIDEOPLAYER_STRETCH43 = "videoplayer.stretch43";
+ static constexpr auto SETTING_VIDEOPLAYER_TELETEXTENABLED = "videoplayer.teletextenabled";
+ static constexpr auto SETTING_VIDEOPLAYER_TELETEXTSCALE = "videoplayer.teletextscale";
+ static constexpr auto SETTING_VIDEOPLAYER_STEREOSCOPICPLAYBACKMODE =
+ "videoplayer.stereoscopicplaybackmode";
+ static constexpr auto SETTING_VIDEOPLAYER_QUITSTEREOMODEONSTOP =
+ "videoplayer.quitstereomodeonstop";
+ static constexpr auto SETTING_VIDEOPLAYER_RENDERMETHOD = "videoplayer.rendermethod";
+ static constexpr auto SETTING_VIDEOPLAYER_HQSCALERS = "videoplayer.hqscalers";
+ static constexpr auto SETTING_VIDEOPLAYER_USEMEDIACODEC = "videoplayer.usemediacodec";
+ static constexpr auto SETTING_VIDEOPLAYER_USEMEDIACODECSURFACE =
+ "videoplayer.usemediacodecsurface";
+ static constexpr auto SETTING_VIDEOPLAYER_USEVDPAU = "videoplayer.usevdpau";
+ static constexpr auto SETTING_VIDEOPLAYER_USEVDPAUMIXER = "videoplayer.usevdpaumixer";
+ static constexpr auto SETTING_VIDEOPLAYER_USEVDPAUMPEG2 = "videoplayer.usevdpaumpeg2";
+ static constexpr auto SETTING_VIDEOPLAYER_USEVDPAUMPEG4 = "videoplayer.usevdpaumpeg4";
+ static constexpr auto SETTING_VIDEOPLAYER_USEVDPAUVC1 = "videoplayer.usevdpauvc1";
+ static constexpr auto SETTING_VIDEOPLAYER_USEDXVA2 = "videoplayer.usedxva2";
+ static constexpr auto SETTING_VIDEOPLAYER_USEVTB = "videoplayer.usevtb";
+ static constexpr auto SETTING_VIDEOPLAYER_USEPRIMEDECODER = "videoplayer.useprimedecoder";
+ static constexpr auto SETTING_VIDEOPLAYER_USESTAGEFRIGHT = "videoplayer.usestagefright";
+ static constexpr auto SETTING_VIDEOPLAYER_LIMITGUIUPDATE = "videoplayer.limitguiupdate";
+ static constexpr auto SETTING_VIDEOPLAYER_SUPPORTMVC = "videoplayer.supportmvc";
+ static constexpr auto SETTING_MYVIDEOS_SELECTACTION = "myvideos.selectaction";
+ static constexpr auto SETTING_MYVIDEOS_USETAGS = "myvideos.usetags";
+ static constexpr auto SETTING_MYVIDEOS_EXTRACTFLAGS = "myvideos.extractflags";
+ static constexpr auto SETTING_MYVIDEOS_EXTRACTCHAPTERTHUMBS = "myvideos.extractchapterthumbs";
+ static constexpr auto SETTING_MYVIDEOS_REPLACELABELS = "myvideos.replacelabels";
+ static constexpr auto SETTING_MYVIDEOS_EXTRACTTHUMB = "myvideos.extractthumb";
+ static constexpr auto SETTING_MYVIDEOS_STACKVIDEOS = "myvideos.stackvideos";
+ static constexpr auto SETTING_LOCALE_SUBTITLELANGUAGE = "locale.subtitlelanguage";
+ static constexpr auto SETTING_SUBTITLES_PARSECAPTIONS = "subtitles.parsecaptions";
+ static constexpr auto SETTING_SUBTITLES_CAPTIONSALIGN = "subtitles.captionsalign";
+ static constexpr auto SETTING_SUBTITLES_ALIGN = "subtitles.align";
+ static constexpr auto SETTING_SUBTITLES_STEREOSCOPICDEPTH = "subtitles.stereoscopicdepth";
+ static constexpr auto SETTING_SUBTITLES_FONTNAME = "subtitles.fontname";
+ static constexpr auto SETTING_SUBTITLES_FONTSIZE = "subtitles.fontsize";
+ static constexpr auto SETTING_SUBTITLES_STYLE = "subtitles.style";
+ static constexpr auto SETTING_SUBTITLES_COLOR = "subtitles.colorpick";
+ static constexpr auto SETTING_SUBTITLES_BORDERSIZE = "subtitles.bordersize";
+ static constexpr auto SETTING_SUBTITLES_BORDERCOLOR = "subtitles.bordercolorpick";
+ static constexpr auto SETTING_SUBTITLES_OPACITY = "subtitles.opacity";
+ static constexpr auto SETTING_SUBTITLES_BLUR = "subtitles.blur";
+ static constexpr auto SETTING_SUBTITLES_BACKGROUNDTYPE = "subtitles.backgroundtype";
+ static constexpr auto SETTING_SUBTITLES_SHADOWCOLOR = "subtitles.shadowcolor";
+ static constexpr auto SETTING_SUBTITLES_SHADOWOPACITY = "subtitles.shadowopacity";
+ static constexpr auto SETTING_SUBTITLES_SHADOWSIZE = "subtitles.shadowsize";
+ static constexpr auto SETTING_SUBTITLES_BGCOLOR = "subtitles.bgcolorpick";
+ static constexpr auto SETTING_SUBTITLES_BGOPACITY = "subtitles.bgopacity";
+ static constexpr auto SETTING_SUBTITLES_MARGINVERTICAL = "subtitles.marginvertical";
+ static constexpr auto SETTING_SUBTITLES_CHARSET = "subtitles.charset";
+ static constexpr auto SETTING_SUBTITLES_OVERRIDEFONTS = "subtitles.overridefonts";
+ static constexpr auto SETTING_SUBTITLES_OVERRIDESTYLES = "subtitles.overridestyles";
+ static constexpr auto SETTING_SUBTITLES_LANGUAGES = "subtitles.languages";
+ static constexpr auto SETTING_SUBTITLES_STORAGEMODE = "subtitles.storagemode";
+ static constexpr auto SETTING_SUBTITLES_CUSTOMPATH = "subtitles.custompath";
+ static constexpr auto SETTING_SUBTITLES_PAUSEONSEARCH = "subtitles.pauseonsearch";
+ static constexpr auto SETTING_SUBTITLES_DOWNLOADFIRST = "subtitles.downloadfirst";
+ static constexpr auto SETTING_SUBTITLES_TV = "subtitles.tv";
+ static constexpr auto SETTING_SUBTITLES_MOVIE = "subtitles.movie";
+ static constexpr auto SETTING_DVDS_AUTORUN = "dvds.autorun";
+ static constexpr auto SETTING_DVDS_PLAYERREGION = "dvds.playerregion";
+ static constexpr auto SETTING_DVDS_AUTOMENU = "dvds.automenu";
+ static constexpr auto SETTING_DISC_PLAYBACK = "disc.playback";
+ static constexpr auto SETTING_BLURAY_PLAYERREGION = "bluray.playerregion";
+ static constexpr auto SETTING_ACCESSIBILITY_AUDIOVISUAL = "accessibility.audiovisual";
+ static constexpr auto SETTING_ACCESSIBILITY_AUDIOHEARING = "accessibility.audiohearing";
+ static constexpr auto SETTING_ACCESSIBILITY_SUBHEARING = "accessibility.subhearing";
+ static constexpr auto SETTING_SCRAPERS_MOVIESDEFAULT = "scrapers.moviesdefault";
+ static constexpr auto SETTING_SCRAPERS_TVSHOWSDEFAULT = "scrapers.tvshowsdefault";
+ static constexpr auto SETTING_SCRAPERS_MUSICVIDEOSDEFAULT = "scrapers.musicvideosdefault";
+ static constexpr auto SETTING_PVRMANAGER_PRESELECTPLAYINGCHANNEL =
+ "pvrmanager.preselectplayingchannel";
+ static constexpr auto SETTING_PVRMANAGER_SYNCCHANNELGROUPS = "pvrmanager.syncchannelgroups";
+ static constexpr auto SETTING_PVRMANAGER_BACKENDCHANNELORDER = "pvrmanager.backendchannelorder";
+ static constexpr auto SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERS =
+ "pvrmanager.usebackendchannelnumbers";
+ static constexpr auto SETTING_PVRMANAGER_USEBACKENDCHANNELNUMBERSALWAYS =
+ "pvrmanager.usebackendchannelnumbersalways";
+ static constexpr auto SETTING_PVRMANAGER_STARTGROUPCHANNELNUMBERSFROMONE =
+ "pvrmanager.startgroupchannelnumbersfromone";
+ static constexpr auto SETTING_PVRMANAGER_CLIENTPRIORITIES = "pvrmanager.clientpriorities";
+ static constexpr auto SETTING_PVRMANAGER_CHANNELMANAGER = "pvrmanager.channelmanager";
+ static constexpr auto SETTING_PVRMANAGER_GROUPMANAGER = "pvrmanager.groupmanager";
+ static constexpr auto SETTING_PVRMANAGER_CHANNELSCAN = "pvrmanager.channelscan";
+ static constexpr auto SETTING_PVRMANAGER_RESETDB = "pvrmanager.resetdb";
+ static constexpr auto SETTING_PVRMANAGER_ADDONS = "pvrmanager.addons";
+ static constexpr auto SETTING_PVRMENU_DISPLAYCHANNELINFO = "pvrmenu.displaychannelinfo";
+ static constexpr auto SETTING_PVRMENU_CLOSECHANNELOSDONSWITCH = "pvrmenu.closechannelosdonswitch";
+ static constexpr auto SETTING_PVRMENU_ICONPATH = "pvrmenu.iconpath";
+ static constexpr auto SETTING_PVRMENU_SEARCHICONS = "pvrmenu.searchicons";
+ static constexpr auto SETTING_EPG_PAST_DAYSTODISPLAY = "epg.pastdaystodisplay";
+ static constexpr auto SETTING_EPG_FUTURE_DAYSTODISPLAY = "epg.futuredaystodisplay";
+ static constexpr auto SETTING_EPG_SELECTACTION = "epg.selectaction";
+ static constexpr auto SETTING_EPG_HIDENOINFOAVAILABLE = "epg.hidenoinfoavailable";
+ static constexpr auto SETTING_EPG_EPGUPDATE = "epg.epgupdate";
+ static constexpr auto SETTING_EPG_PREVENTUPDATESWHILEPLAYINGTV =
+ "epg.preventupdateswhileplayingtv";
+ static constexpr auto SETTING_EPG_RESETEPG = "epg.resetepg";
+ static constexpr auto SETTING_PVRPLAYBACK_SWITCHTOFULLSCREENCHANNELTYPES =
+ "pvrplayback.switchtofullscreenchanneltypes";
+ static constexpr auto SETTING_PVRPLAYBACK_SIGNALQUALITY = "pvrplayback.signalquality";
+ static constexpr auto SETTING_PVRPLAYBACK_CONFIRMCHANNELSWITCH =
+ "pvrplayback.confirmchannelswitch";
+ static constexpr auto SETTING_PVRPLAYBACK_CHANNELENTRYTIMEOUT = "pvrplayback.channelentrytimeout";
+ static constexpr auto SETTING_PVRPLAYBACK_DELAYMARKLASTWATCHED =
+ "pvrplayback.delaymarklastwatched";
+ static constexpr auto SETTING_PVRPLAYBACK_FPS = "pvrplayback.fps";
+ static constexpr auto SETTING_PVRRECORD_INSTANTRECORDACTION = "pvrrecord.instantrecordaction";
+ static constexpr auto SETTING_PVRRECORD_INSTANTRECORDTIME = "pvrrecord.instantrecordtime";
+ static constexpr auto SETTING_PVRRECORD_MARGINSTART = "pvrrecord.marginstart";
+ static constexpr auto SETTING_PVRRECORD_MARGINEND = "pvrrecord.marginend";
+ static constexpr auto SETTING_PVRRECORD_TIMERNOTIFICATIONS = "pvrrecord.timernotifications";
+ static constexpr auto SETTING_PVRRECORD_GROUPRECORDINGS = "pvrrecord.grouprecordings";
+ static constexpr auto SETTING_PVRREMINDERS_AUTOCLOSEDELAY = "pvrreminders.autoclosedelay";
+ static constexpr auto SETTING_PVRREMINDERS_AUTORECORD = "pvrreminders.autorecord";
+ static constexpr auto SETTING_PVRREMINDERS_AUTOSWITCH = "pvrreminders.autoswitch";
+ static constexpr auto SETTING_PVRPOWERMANAGEMENT_ENABLED = "pvrpowermanagement.enabled";
+ static constexpr auto SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME =
+ "pvrpowermanagement.backendidletime";
+ static constexpr auto SETTING_PVRPOWERMANAGEMENT_SETWAKEUPCMD = "pvrpowermanagement.setwakeupcmd";
+ static constexpr auto SETTING_PVRPOWERMANAGEMENT_PREWAKEUP = "pvrpowermanagement.prewakeup";
+ static constexpr auto SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUP = "pvrpowermanagement.dailywakeup";
+ static constexpr auto SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME =
+ "pvrpowermanagement.dailywakeuptime";
+ static constexpr auto SETTING_PVRPARENTAL_ENABLED = "pvrparental.enabled";
+ static constexpr auto SETTING_PVRPARENTAL_PIN = "pvrparental.pin";
+ static constexpr auto SETTING_PVRPARENTAL_DURATION = "pvrparental.duration";
+ static constexpr auto SETTING_PVRCLIENT_MENUHOOK = "pvrclient.menuhook";
+ static constexpr auto SETTING_PVRTIMERS_HIDEDISABLEDTIMERS = "pvrtimers.hidedisabledtimers";
+ static constexpr auto SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS =
+ "musiclibrary.showcompilationartists";
+ static constexpr auto SETTING_MUSICLIBRARY_SHOWDISCS = "musiclibrary.showdiscs";
+ static constexpr auto SETTING_MUSICLIBRARY_USEORIGINALDATE = "musiclibrary.useoriginaldate";
+ static constexpr auto SETTING_MUSICLIBRARY_USEARTISTSORTNAME = "musiclibrary.useartistsortname";
+ static constexpr auto SETTING_MUSICLIBRARY_DOWNLOADINFO = "musiclibrary.downloadinfo";
+ static constexpr auto SETTING_MUSICLIBRARY_ARTISTSFOLDER = "musiclibrary.artistsfolder";
+ static constexpr auto SETTING_MUSICLIBRARY_PREFERONLINEALBUMART =
+ "musiclibrary.preferonlinealbumart";
+ static constexpr auto SETTING_MUSICLIBRARY_ARTWORKLEVEL = "musiclibrary.artworklevel";
+ static constexpr auto SETTING_MUSICLIBRARY_USEALLLOCALART = "musiclibrary.usealllocalart";
+ static constexpr auto SETTING_MUSICLIBRARY_USEALLREMOTEART = "musiclibrary.useallremoteart";
+ static constexpr auto SETTING_MUSICLIBRARY_ARTISTART_WHITELIST =
+ "musiclibrary.artistartwhitelist";
+ static constexpr auto SETTING_MUSICLIBRARY_ALBUMART_WHITELIST = "musiclibrary.albumartwhitelist";
+ static constexpr auto SETTING_MUSICLIBRARY_MUSICTHUMBS = "musiclibrary.musicthumbs";
+ static constexpr auto SETTING_MUSICLIBRARY_ARTSETTINGS_UPDATED = "musiclibrary.artsettings";
+ static constexpr auto SETTING_MUSICLIBRARY_ALBUMSSCRAPER = "musiclibrary.albumsscraper";
+ static constexpr auto SETTING_MUSICLIBRARY_ARTISTSSCRAPER = "musiclibrary.artistsscraper";
+ static constexpr auto SETTING_MUSICLIBRARY_OVERRIDETAGS = "musiclibrary.overridetags";
+ static constexpr auto SETTING_MUSICLIBRARY_SHOWALLITEMS = "musiclibrary.showallitems";
+ static constexpr auto SETTING_MUSICLIBRARY_UPDATEONSTARTUP = "musiclibrary.updateonstartup";
+ static constexpr auto SETTING_MUSICLIBRARY_BACKGROUNDUPDATE = "musiclibrary.backgroundupdate";
+ static constexpr auto SETTING_MUSICLIBRARY_CLEANUP = "musiclibrary.cleanup";
+ static constexpr auto SETTING_MUSICLIBRARY_EXPORT = "musiclibrary.export";
+ static constexpr auto SETTING_MUSICLIBRARY_EXPORT_FILETYPE = "musiclibrary.exportfiletype";
+ static constexpr auto SETTING_MUSICLIBRARY_EXPORT_FOLDER = "musiclibrary.exportfolder";
+ static constexpr auto SETTING_MUSICLIBRARY_EXPORT_ITEMS = "musiclibrary.exportitems";
+ static constexpr auto SETTING_MUSICLIBRARY_EXPORT_UNSCRAPED = "musiclibrary.exportunscraped";
+ static constexpr auto SETTING_MUSICLIBRARY_EXPORT_OVERWRITE = "musiclibrary.exportoverwrite";
+ static constexpr auto SETTING_MUSICLIBRARY_EXPORT_ARTWORK = "musiclibrary.exportartwork";
+ static constexpr auto SETTING_MUSICLIBRARY_EXPORT_SKIPNFO = "musiclibrary.exportskipnfo";
+ static constexpr auto SETTING_MUSICLIBRARY_IMPORT = "musiclibrary.import";
+ static constexpr auto SETTING_MUSICPLAYER_AUTOPLAYNEXTITEM = "musicplayer.autoplaynextitem";
+ static constexpr auto SETTING_MUSICPLAYER_QUEUEBYDEFAULT = "musicplayer.queuebydefault";
+ static constexpr auto SETTING_MUSICPLAYER_SEEKSTEPS = "musicplayer.seeksteps";
+ static constexpr auto SETTING_MUSICPLAYER_SEEKDELAY = "musicplayer.seekdelay";
+ static constexpr auto SETTING_MUSICPLAYER_REPLAYGAINTYPE = "musicplayer.replaygaintype";
+ static constexpr auto SETTING_MUSICPLAYER_REPLAYGAINPREAMP = "musicplayer.replaygainpreamp";
+ static constexpr auto SETTING_MUSICPLAYER_REPLAYGAINNOGAINPREAMP =
+ "musicplayer.replaygainnogainpreamp";
+ static constexpr auto SETTING_MUSICPLAYER_REPLAYGAINAVOIDCLIPPING =
+ "musicplayer.replaygainavoidclipping";
+ static constexpr auto SETTING_MUSICPLAYER_CROSSFADE = "musicplayer.crossfade";
+ static constexpr auto SETTING_MUSICPLAYER_CROSSFADEALBUMTRACKS =
+ "musicplayer.crossfadealbumtracks";
+ static constexpr auto SETTING_MUSICPLAYER_VISUALISATION = "musicplayer.visualisation";
+ static constexpr auto SETTING_MUSICFILES_SELECTACTION = "musicfiles.selectaction";
+ static constexpr auto SETTING_MUSICFILES_USETAGS = "musicfiles.usetags";
+ static constexpr auto SETTING_MUSICFILES_TRACKFORMAT = "musicfiles.trackformat";
+ static constexpr auto SETTING_MUSICFILES_NOWPLAYINGTRACKFORMAT =
+ "musicfiles.nowplayingtrackformat";
+ static constexpr auto SETTING_MUSICFILES_LIBRARYTRACKFORMAT = "musicfiles.librarytrackformat";
+ static constexpr auto SETTING_MUSICFILES_FINDREMOTETHUMBS = "musicfiles.findremotethumbs";
+ static constexpr auto SETTING_AUDIOCDS_AUTOACTION = "audiocds.autoaction";
+ static constexpr auto SETTING_AUDIOCDS_USECDDB = "audiocds.usecddb";
+ static constexpr auto SETTING_AUDIOCDS_RECORDINGPATH = "audiocds.recordingpath";
+ static constexpr auto SETTING_AUDIOCDS_TRACKPATHFORMAT = "audiocds.trackpathformat";
+ static constexpr auto SETTING_AUDIOCDS_ENCODER = "audiocds.encoder";
+ static constexpr auto SETTING_AUDIOCDS_SETTINGS = "audiocds.settings";
+ static constexpr auto SETTING_AUDIOCDS_EJECTONRIP = "audiocds.ejectonrip";
+ static constexpr auto SETTING_MYMUSIC_SONGTHUMBINVIS = "mymusic.songthumbinvis";
+ static constexpr auto SETTING_MYMUSIC_DEFAULTLIBVIEW = "mymusic.defaultlibview";
+ static constexpr auto SETTING_PICTURES_USETAGS = "pictures.usetags";
+ static constexpr auto SETTING_PICTURES_GENERATETHUMBS = "pictures.generatethumbs";
+ static constexpr auto SETTING_PICTURES_SHOWVIDEOS = "pictures.showvideos";
+ static constexpr auto SETTING_PICTURES_DISPLAYRESOLUTION = "pictures.displayresolution";
+ static constexpr auto SETTING_SLIDESHOW_STAYTIME = "slideshow.staytime";
+ static constexpr auto SETTING_SLIDESHOW_DISPLAYEFFECTS = "slideshow.displayeffects";
+ static constexpr auto SETTING_SLIDESHOW_SHUFFLE = "slideshow.shuffle";
+ static constexpr auto SETTING_SLIDESHOW_HIGHQUALITYDOWNSCALING =
+ "slideshow.highqualitydownscaling";
+ static constexpr auto SETTING_WEATHER_CURRENTLOCATION = "weather.currentlocation";
+ static constexpr auto SETTING_WEATHER_ADDON = "weather.addon";
+ static constexpr auto SETTING_WEATHER_ADDONSETTINGS = "weather.addonsettings";
+ static constexpr auto SETTING_SERVICES_DEVICENAME = "services.devicename";
+ static constexpr auto SETTING_SERVICES_DEVICEUUID = "services.deviceuuid";
+ static constexpr auto SETTING_SERVICES_UPNP = "services.upnp";
+ static constexpr auto SETTING_SERVICES_UPNPSERVER = "services.upnpserver";
+ static constexpr auto SETTING_SERVICES_UPNPANNOUNCE = "services.upnpannounce";
+ static constexpr auto SETTING_SERVICES_UPNPLOOKFOREXTERNALSUBTITLES =
+ "services.upnplookforexternalsubtitles";
+ static constexpr auto SETTING_SERVICES_UPNPCONTROLLER = "services.upnpcontroller";
+ static constexpr auto SETTING_SERVICES_UPNPRENDERER = "services.upnprenderer";
+ static constexpr auto SETTING_SERVICES_WEBSERVER = "services.webserver";
+ static constexpr auto SETTING_SERVICES_WEBSERVERPORT = "services.webserverport";
+ static constexpr auto SETTING_SERVICES_WEBSERVERAUTHENTICATION =
+ "services.webserverauthentication";
+ static constexpr auto SETTING_SERVICES_WEBSERVERUSERNAME = "services.webserverusername";
+ static constexpr auto SETTING_SERVICES_WEBSERVERPASSWORD = "services.webserverpassword";
+ static constexpr auto SETTING_SERVICES_WEBSERVERSSL = "services.webserverssl";
+ static constexpr auto SETTING_SERVICES_WEBSKIN = "services.webskin";
+ static constexpr auto SETTING_SERVICES_ESENABLED = "services.esenabled";
+ static constexpr auto SETTING_SERVICES_ESPORT = "services.esport";
+ static constexpr auto SETTING_SERVICES_ESPORTRANGE = "services.esportrange";
+ static constexpr auto SETTING_SERVICES_ESMAXCLIENTS = "services.esmaxclients";
+ static constexpr auto SETTING_SERVICES_ESALLINTERFACES = "services.esallinterfaces";
+ static constexpr auto SETTING_SERVICES_ESINITIALDELAY = "services.esinitialdelay";
+ static constexpr auto SETTING_SERVICES_ESCONTINUOUSDELAY = "services.escontinuousdelay";
+ static constexpr auto SETTING_SERVICES_ZEROCONF = "services.zeroconf";
+ static constexpr auto SETTING_SERVICES_AIRPLAY = "services.airplay";
+ static constexpr auto SETTING_SERVICES_AIRPLAYVOLUMECONTROL = "services.airplayvolumecontrol";
+ static constexpr auto SETTING_SERVICES_USEAIRPLAYPASSWORD = "services.useairplaypassword";
+ static constexpr auto SETTING_SERVICES_AIRPLAYPASSWORD = "services.airplaypassword";
+ static constexpr auto SETTING_SERVICES_AIRPLAYVIDEOSUPPORT = "services.airplayvideosupport";
+ static constexpr auto SETTING_SMB_WINSSERVER = "smb.winsserver";
+ static constexpr auto SETTING_SMB_WORKGROUP = "smb.workgroup";
+ static constexpr auto SETTING_SMB_MINPROTOCOL = "smb.minprotocol";
+ static constexpr auto SETTING_SMB_MAXPROTOCOL = "smb.maxprotocol";
+ static constexpr auto SETTING_SMB_LEGACYSECURITY = "smb.legacysecurity";
+ static constexpr auto SETTING_SERVICES_WSDISCOVERY = "services.wsdiscovery";
+ static constexpr auto SETTING_VIDEOSCREEN_MONITOR = "videoscreen.monitor";
+ static constexpr auto SETTING_VIDEOSCREEN_SCREEN = "videoscreen.screen";
+ static constexpr auto SETTING_VIDEOSCREEN_WHITELIST = "videoscreen.whitelist";
+ static constexpr auto SETTING_VIDEOSCREEN_RESOLUTION = "videoscreen.resolution";
+ static constexpr auto SETTING_VIDEOSCREEN_SCREENMODE = "videoscreen.screenmode";
+ static constexpr auto SETTING_VIDEOSCREEN_FAKEFULLSCREEN = "videoscreen.fakefullscreen";
+ static constexpr auto SETTING_VIDEOSCREEN_BLANKDISPLAYS = "videoscreen.blankdisplays";
+ static constexpr auto SETTING_VIDEOSCREEN_STEREOSCOPICMODE = "videoscreen.stereoscopicmode";
+ static constexpr auto SETTING_VIDEOSCREEN_PREFEREDSTEREOSCOPICMODE =
+ "videoscreen.preferedstereoscopicmode";
+ static constexpr auto SETTING_VIDEOSCREEN_NOOFBUFFERS = "videoscreen.noofbuffers";
+ static constexpr auto SETTING_VIDEOSCREEN_3DLUT = "videoscreen.cms3dlut";
+ static constexpr auto SETTING_VIDEOSCREEN_DISPLAYPROFILE = "videoscreen.displayprofile";
+ static constexpr auto SETTING_VIDEOSCREEN_GUICALIBRATION = "videoscreen.guicalibration";
+ static constexpr auto SETTING_VIDEOSCREEN_TESTPATTERN = "videoscreen.testpattern";
+ static constexpr auto SETTING_VIDEOSCREEN_LIMITEDRANGE = "videoscreen.limitedrange";
+ static constexpr auto SETTING_VIDEOSCREEN_FRAMEPACKING = "videoscreen.framepacking";
+ static constexpr auto SETTING_VIDEOSCREEN_10BITSURFACES = "videoscreen.10bitsurfaces";
+ static constexpr auto SETTING_VIDEOSCREEN_GUISDRPEAKLUMINANCE = "videoscreen.guisdrpeakluminance";
+ static constexpr auto SETTING_AUDIOOUTPUT_AUDIODEVICE = "audiooutput.audiodevice";
+ static constexpr auto SETTING_AUDIOOUTPUT_CHANNELS = "audiooutput.channels";
+ static constexpr auto SETTING_AUDIOOUTPUT_CONFIG = "audiooutput.config";
+ static constexpr auto SETTING_AUDIOOUTPUT_SAMPLERATE = "audiooutput.samplerate";
+ static constexpr auto SETTING_AUDIOOUTPUT_STEREOUPMIX = "audiooutput.stereoupmix";
+ static constexpr auto SETTING_AUDIOOUTPUT_MAINTAINORIGINALVOLUME =
+ "audiooutput.maintainoriginalvolume";
+ static constexpr auto SETTING_AUDIOOUTPUT_PROCESSQUALITY = "audiooutput.processquality";
+ static constexpr auto SETTING_AUDIOOUTPUT_ATEMPOTHRESHOLD = "audiooutput.atempothreshold";
+ static constexpr auto SETTING_AUDIOOUTPUT_STREAMSILENCE = "audiooutput.streamsilence";
+ static constexpr auto SETTING_AUDIOOUTPUT_STREAMNOISE = "audiooutput.streamnoise";
+ static constexpr auto SETTING_AUDIOOUTPUT_GUISOUNDMODE = "audiooutput.guisoundmode";
+ static constexpr auto SETTING_AUDIOOUTPUT_GUISOUNDVOLUME = "audiooutput.guisoundvolume";
+ static constexpr auto SETTING_AUDIOOUTPUT_PASSTHROUGH = "audiooutput.passthrough";
+ static constexpr auto SETTING_AUDIOOUTPUT_PASSTHROUGHDEVICE = "audiooutput.passthroughdevice";
+ static constexpr auto SETTING_AUDIOOUTPUT_AC3PASSTHROUGH = "audiooutput.ac3passthrough";
+ static constexpr auto SETTING_AUDIOOUTPUT_AC3TRANSCODE = "audiooutput.ac3transcode";
+ static constexpr auto SETTING_AUDIOOUTPUT_EAC3PASSTHROUGH = "audiooutput.eac3passthrough";
+ static constexpr auto SETTING_AUDIOOUTPUT_DTSPASSTHROUGH = "audiooutput.dtspassthrough";
+ static constexpr auto SETTING_AUDIOOUTPUT_TRUEHDPASSTHROUGH = "audiooutput.truehdpassthrough";
+ static constexpr auto SETTING_AUDIOOUTPUT_DTSHDPASSTHROUGH = "audiooutput.dtshdpassthrough";
+ static constexpr auto SETTING_AUDIOOUTPUT_DTSHDCOREFALLBACK = "audiooutput.dtshdcorefallback";
+ static constexpr auto SETTING_AUDIOOUTPUT_VOLUMESTEPS = "audiooutput.volumesteps";
+ static constexpr auto SETTING_INPUT_PERIPHERALS = "input.peripherals";
+ static constexpr auto SETTING_INPUT_PERIPHERALLIBRARIES = "input.peripherallibraries";
+ static constexpr auto SETTING_INPUT_ENABLEMOUSE = "input.enablemouse";
+ static constexpr auto SETTING_INPUT_ASKNEWCONTROLLERS = "input.asknewcontrollers";
+ static constexpr auto SETTING_INPUT_CONTROLLERCONFIG = "input.controllerconfig";
+ static constexpr auto SETTING_INPUT_RUMBLENOTIFY = "input.rumblenotify";
+ static constexpr auto SETTING_INPUT_TESTRUMBLE = "input.testrumble";
+ static constexpr auto SETTING_INPUT_CONTROLLERPOWEROFF = "input.controllerpoweroff";
+ static constexpr auto SETTING_INPUT_APPLEREMOTEMODE = "input.appleremotemode";
+ static constexpr auto SETTING_INPUT_APPLEREMOTEALWAYSON = "input.appleremotealwayson";
+ static constexpr auto SETTING_INPUT_APPLEREMOTESEQUENCETIME = "input.appleremotesequencetime";
+ static constexpr auto SETTING_INPUT_SIRIREMOTEIDLETIMERENABLED = "input.siriremoteidletimerenabled";
+ static constexpr auto SETTING_INPUT_SIRIREMOTEIDLETIME = "input.siriremoteidletime";
+ static constexpr auto SETTING_INPUT_SIRIREMOTEHORIZONTALSENSITIVITY =
+ "input.siriremotehorizontalsensitivity";
+ static constexpr auto SETTING_INPUT_SIRIREMOTEVERTICALSENSITIVITY =
+ "input.siriremoteverticalsensitivity";
+ static constexpr auto SETTING_INPUT_TVOSUSEKODIKEYBOARD = "input.tvosusekodikeyboard";
+ static constexpr auto SETTING_NETWORK_USEHTTPPROXY = "network.usehttpproxy";
+ static constexpr auto SETTING_NETWORK_HTTPPROXYTYPE = "network.httpproxytype";
+ static constexpr auto SETTING_NETWORK_HTTPPROXYSERVER = "network.httpproxyserver";
+ static constexpr auto SETTING_NETWORK_HTTPPROXYPORT = "network.httpproxyport";
+ static constexpr auto SETTING_NETWORK_HTTPPROXYUSERNAME = "network.httpproxyusername";
+ static constexpr auto SETTING_NETWORK_HTTPPROXYPASSWORD = "network.httpproxypassword";
+ static constexpr auto SETTING_NETWORK_BANDWIDTH = "network.bandwidth";
+ static constexpr auto SETTING_POWERMANAGEMENT_DISPLAYSOFF = "powermanagement.displaysoff";
+ static constexpr auto SETTING_POWERMANAGEMENT_SHUTDOWNTIME = "powermanagement.shutdowntime";
+ static constexpr auto SETTING_POWERMANAGEMENT_SHUTDOWNSTATE = "powermanagement.shutdownstate";
+ static constexpr auto SETTING_POWERMANAGEMENT_WAKEONACCESS = "powermanagement.wakeonaccess";
+ static constexpr auto SETTING_POWERMANAGEMENT_WAITFORNETWORK = "powermanagement.waitfornetwork";
+ static constexpr auto SETTING_DEBUG_SHOWLOGINFO = "debug.showloginfo";
+ static constexpr auto SETTING_DEBUG_EXTRALOGGING = "debug.extralogging";
+ static constexpr auto SETTING_DEBUG_SETEXTRALOGLEVEL = "debug.setextraloglevel";
+ static constexpr auto SETTING_DEBUG_SCREENSHOTPATH = "debug.screenshotpath";
+ static constexpr auto SETTING_DEBUG_SHARE_LOG = "debug.sharelog";
+ static constexpr auto SETTING_EVENTLOG_ENABLED = "eventlog.enabled";
+ static constexpr auto SETTING_EVENTLOG_ENABLED_NOTIFICATIONS = "eventlog.enablednotifications";
+ static constexpr auto SETTING_EVENTLOG_SHOW = "eventlog.show";
+ static constexpr auto SETTING_MASTERLOCK_LOCKCODE = "masterlock.lockcode";
+ static constexpr auto SETTING_MASTERLOCK_STARTUPLOCK = "masterlock.startuplock";
+ static constexpr auto SETTING_MASTERLOCK_MAXRETRIES = "masterlock.maxretries";
+ static constexpr auto SETTING_CACHE_HARDDISK = "cache.harddisk";
+ static constexpr auto SETTING_CACHEVIDEO_DVDROM = "cachevideo.dvdrom";
+ static constexpr auto SETTING_CACHEVIDEO_LAN = "cachevideo.lan";
+ static constexpr auto SETTING_CACHEVIDEO_INTERNET = "cachevideo.internet";
+ static constexpr auto SETTING_CACHEAUDIO_DVDROM = "cacheaudio.dvdrom";
+ static constexpr auto SETTING_CACHEAUDIO_LAN = "cacheaudio.lan";
+ static constexpr auto SETTING_CACHEAUDIO_INTERNET = "cacheaudio.internet";
+ static constexpr auto SETTING_CACHEDVD_DVDROM = "cachedvd.dvdrom";
+ static constexpr auto SETTING_CACHEDVD_LAN = "cachedvd.lan";
+ static constexpr auto SETTING_CACHEUNKNOWN_INTERNET = "cacheunknown.internet";
+ static constexpr auto SETTING_SYSTEM_PLAYLISTSPATH = "system.playlistspath";
+ static constexpr auto SETTING_ADDONS_AUTOUPDATES = "general.addonupdates";
+ static constexpr auto SETTING_ADDONS_NOTIFICATIONS = "general.addonnotifications";
+ static constexpr auto SETTING_ADDONS_SHOW_RUNNING = "addons.showrunning";
+ static constexpr auto SETTING_ADDONS_ALLOW_UNKNOWN_SOURCES = "addons.unknownsources";
+ static constexpr auto SETTING_ADDONS_UPDATEMODE = "addons.updatemode";
+ static constexpr auto SETTING_ADDONS_MANAGE_DEPENDENCIES = "addons.managedependencies";
+ static constexpr auto SETTING_ADDONS_REMOVE_ORPHANED_DEPENDENCIES =
+ "addons.removeorphaneddependencies";
+ static constexpr auto SETTING_GENERAL_ADDONFOREIGNFILTER = "general.addonforeignfilter";
+ static constexpr auto SETTING_GENERAL_ADDONBROKENFILTER = "general.addonbrokenfilter";
+ static constexpr auto SETTING_SOURCE_VIDEOS = "source.videos";
+ static constexpr auto SETTING_SOURCE_MUSIC = "source.music";
+ static constexpr auto SETTING_SOURCE_PICTURES = "source.pictures";
+
+ // values for SETTING_VIDEOLIBRARY_SHOWUNWATCHEDPLOTS
+ static const int VIDEOLIBRARY_PLOTS_SHOW_UNWATCHED_MOVIES = 0;
+ static const int VIDEOLIBRARY_PLOTS_SHOW_UNWATCHED_TVSHOWEPISODES = 1;
+ static const int VIDEOLIBRARY_THUMB_SHOW_UNWATCHED_EPISODE = 2;
+ // values for SETTING_VIDEOLIBRARY_ARTWORK_LEVEL
+ static const int VIDEOLIBRARY_ARTWORK_LEVEL_ALL = 0;
+ static const int VIDEOLIBRARY_ARTWORK_LEVEL_BASIC = 1;
+ static const int VIDEOLIBRARY_ARTWORK_LEVEL_CUSTOM = 2;
+ static const int VIDEOLIBRARY_ARTWORK_LEVEL_NONE = 3;
+
+ // values for SETTING_MUSICLIBRARY_ARTWORKLEVEL
+ static const int MUSICLIBRARY_ARTWORK_LEVEL_ALL = 0;
+ static const int MUSICLIBRARY_ARTWORK_LEVEL_BASIC = 1;
+ static const int MUSICLIBRARY_ARTWORK_LEVEL_CUSTOM = 2;
+ static const int MUSICLIBRARY_ARTWORK_LEVEL_NONE = 3;
+
+ /*!
+ \brief Creates a new settings wrapper around a new settings manager.
+
+ For access to the "global" settings wrapper the static GetInstance() method should
+ be used.
+ */
+ CSettings() = default;
+ ~CSettings() override = default;
+
+ CSettingsManager* GetSettingsManager() const { return m_settingsManager; }
+
+ // specialization of CSettingsBase
+ bool Initialize() override;
+
+ /*!
+ \brief Registers the given ISubSettings implementation.
+
+ \param subSettings ISubSettings implementation
+ */
+ void RegisterSubSettings(ISubSettings* subSettings);
+ /*!
+ \brief Unregisters the given ISubSettings implementation.
+
+ \param subSettings ISubSettings implementation
+ */
+ void UnregisterSubSettings(ISubSettings* subSettings);
+
+ // implementations of CSettingsBase
+ bool Load() override;
+ bool Save() override;
+
+ /*!
+ \brief Loads setting values from the given (XML) file.
+
+ \param file Path to an XML file containing setting values
+ \return True if the setting values were successfully loaded, false otherwise
+ */
+ bool Load(const std::string &file);
+ /*!
+ \brief Loads setting values from the given XML element.
+
+ \param root XML element containing setting values
+ \return True if the setting values were successfully loaded, false otherwise
+ */
+ bool Load(const TiXmlElement* root);
+ /*!
+ \brief Loads setting values from the given XML element.
+
+ \param root XML element containing setting values
+ \param hide Whether to hide the loaded settings or not
+ \return True if the setting values were successfully loaded, false otherwise
+ */
+ bool LoadHidden(const TiXmlElement *root) { return CSettingsBase::LoadHiddenValuesFromXml(root); }
+
+ /*!
+ \brief Saves the setting values to the given (XML) file.
+
+ \param file Path to an XML file
+ \return True if the setting values were successfully saved, false otherwise
+ */
+ bool Save(const std::string &file);
+ /*!
+ \brief Saves the setting values to the given XML node.
+
+ \param root XML node
+ \return True if the setting values were successfully saved, false otherwise
+ */
+ bool Save(TiXmlNode* root) const override;
+
+ /*!
+ \brief Loads the setting being represented by the given XML node with the
+ given identifier.
+
+ \param node XML node representing the setting to load
+ \param settingId Setting identifier
+ \return True if the setting was successfully loaded from the given XML node, false otherwise
+ */
+ bool LoadSetting(const TiXmlNode *node, const std::string &settingId);
+
+ // overwrite (not override) from CSettingsBase
+ bool GetBool(const std::string& id) const;
+
+ /*!
+ \brief Clears the complete settings.
+
+ This removes all initialized settings, groups, categories and sections and
+ returns to the uninitialized state. Any registered callbacks or
+ implementations stay registered.
+ */
+ void Clear() override;
+
+protected:
+ // specializations of CSettingsBase
+ void InitializeSettingTypes() override;
+ void InitializeControls() override;
+ void InitializeOptionFillers() override;
+ void UninitializeOptionFillers() override;
+ void InitializeConditions() override;
+ void UninitializeConditions() override;
+ void InitializeVisibility() override;
+ void InitializeDefaults() override;
+ void InitializeISettingsHandlers() override;
+ void UninitializeISettingsHandlers() override;
+ void InitializeISubSettings() override;
+ void UninitializeISubSettings() override;
+ void InitializeISettingCallbacks() override;
+ void UninitializeISettingCallbacks() override;
+
+ // implementation of CSettingsBase
+ bool InitializeDefinitions() override;
+
+private:
+ CSettings(const CSettings&) = delete;
+ CSettings const& operator=(CSettings const&) = delete;
+
+ bool Load(const TiXmlElement* root, bool& updated);
+
+ // implementation of ISubSettings
+ bool Load(const TiXmlNode* settings) override;
+
+ bool Initialize(const std::string &file);
+ bool Reset();
+
+ std::set<ISubSettings*> m_subSettings;
+};
diff --git a/xbmc/settings/SettingsBase.cpp b/xbmc/settings/SettingsBase.cpp
new file mode 100644
index 0000000..a118a36
--- /dev/null
+++ b/xbmc/settings/SettingsBase.cpp
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2016-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 "SettingsBase.h"
+
+#include "settings/SettingUtils.h"
+#include "settings/SettingsValueXmlSerializer.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/Variant.h"
+#include "utils/XBMCTinyXML.h"
+
+#include <mutex>
+
+#define SETTINGS_XML_ROOT "settings"
+
+CSettingsBase::CSettingsBase()
+ : m_settingsManager(new CSettingsManager())
+{ }
+
+CSettingsBase::~CSettingsBase()
+{
+ Uninitialize();
+
+ delete m_settingsManager;
+}
+
+bool CSettingsBase::Initialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (m_initialized)
+ return false;
+
+ // register custom setting types
+ InitializeSettingTypes();
+ // register custom setting controls
+ InitializeControls();
+
+ // option fillers and conditions need to be
+ // initialized before the setting definitions
+ InitializeOptionFillers();
+ InitializeConditions();
+
+ // load the settings definitions
+ if (!InitializeDefinitions())
+ return false;
+
+ InitializeVisibility();
+ InitializeDefaults();
+
+ m_settingsManager->SetInitialized();
+
+ InitializeISettingsHandlers();
+ InitializeISubSettings();
+ InitializeISettingCallbacks();
+
+ m_initialized = true;
+
+ return true;
+}
+
+bool CSettingsBase::IsInitialized() const
+{
+ return m_initialized && m_settingsManager->IsInitialized();
+}
+
+bool CSettingsBase::LoadValuesFromXml(const CXBMCTinyXML& xml, bool& updated)
+{
+ const TiXmlElement* xmlRoot = xml.RootElement();
+ if (xmlRoot == nullptr || xmlRoot->ValueStr() != SETTINGS_XML_ROOT)
+ return false;
+
+ return m_settingsManager->Load(xmlRoot, updated);
+}
+
+bool CSettingsBase::LoadValuesFromXml(const TiXmlElement* root, bool& updated)
+{
+ if (root == nullptr)
+ return false;
+
+ return m_settingsManager->Load(root, updated);
+}
+
+bool CSettingsBase::LoadHiddenValuesFromXml(const TiXmlElement* root)
+{
+ if (root == nullptr)
+ return false;
+
+ std::map<std::string, std::shared_ptr<CSetting>> loadedSettings;
+
+ bool updated;
+ // don't trigger events for hidden settings
+ bool success = m_settingsManager->Load(root, updated, false, &loadedSettings);
+ if (success)
+ {
+ for(std::map<std::string, std::shared_ptr<CSetting>>::const_iterator setting = loadedSettings.begin(); setting != loadedSettings.end(); ++setting)
+ setting->second->SetVisible(false);
+ }
+
+ return success;
+}
+
+void CSettingsBase::SetLoaded()
+{
+ m_settingsManager->SetLoaded();
+}
+
+bool CSettingsBase::IsLoaded() const
+{
+ return m_settingsManager->IsLoaded();
+}
+
+bool CSettingsBase::SaveValuesToXml(CXBMCTinyXML& xml) const
+{
+ std::string serializedSettings;
+ auto xmlSerializer = std::make_unique<CSettingsValueXmlSerializer>();
+ if (!m_settingsManager->Save(xmlSerializer.get(), serializedSettings))
+ return false;
+
+ if (!xml.Parse(serializedSettings))
+ return false;
+
+ return true;
+}
+
+void CSettingsBase::Unload()
+{
+ m_settingsManager->Unload();
+}
+
+void CSettingsBase::Uninitialize()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (!m_initialized)
+ return;
+
+ // unregister setting option fillers
+ UninitializeOptionFillers();
+
+ // unregister setting conditions
+ UninitializeConditions();
+
+ // unregister ISettingCallback implementations
+ UninitializeISettingCallbacks();
+
+ // cleanup the settings manager
+ m_settingsManager->Clear();
+
+ // unregister ISubSettings implementations
+ UninitializeISubSettings();
+ // unregister ISettingsHandler implementations
+ UninitializeISettingsHandlers();
+
+ m_initialized = false;
+}
+
+void CSettingsBase::RegisterCallback(ISettingCallback* callback, const std::set<std::string>& settingList)
+{
+ m_settingsManager->RegisterCallback(callback, settingList);
+}
+
+void CSettingsBase::UnregisterCallback(ISettingCallback* callback)
+{
+ m_settingsManager->UnregisterCallback(callback);
+}
+
+SettingPtr CSettingsBase::GetSetting(const std::string& id) const
+{
+ if (id.empty())
+ return nullptr;
+
+ return m_settingsManager->GetSetting(id);
+}
+
+std::vector<std::shared_ptr<CSettingSection>> CSettingsBase::GetSections() const
+{
+ return m_settingsManager->GetSections();
+}
+
+std::shared_ptr<CSettingSection> CSettingsBase::GetSection(const std::string& section) const
+{
+ if (section.empty())
+ return nullptr;
+
+ return m_settingsManager->GetSection(section);
+}
+
+bool CSettingsBase::GetBool(const std::string& id) const
+{
+ return m_settingsManager->GetBool(id);
+}
+
+bool CSettingsBase::SetBool(const std::string& id, bool value)
+{
+ return m_settingsManager->SetBool(id, value);
+}
+
+bool CSettingsBase::ToggleBool(const std::string& id)
+{
+ return m_settingsManager->ToggleBool(id);
+}
+
+int CSettingsBase::GetInt(const std::string& id) const
+{
+ return m_settingsManager->GetInt(id);
+}
+
+bool CSettingsBase::SetInt(const std::string& id, int value)
+{
+ return m_settingsManager->SetInt(id, value);
+}
+
+double CSettingsBase::GetNumber(const std::string& id) const
+{
+ return m_settingsManager->GetNumber(id);
+}
+
+bool CSettingsBase::SetNumber(const std::string& id, double value)
+{
+ return m_settingsManager->SetNumber(id, value);
+}
+
+std::string CSettingsBase::GetString(const std::string& id) const
+{
+ return m_settingsManager->GetString(id);
+}
+
+bool CSettingsBase::SetString(const std::string& id, const std::string& value)
+{
+ return m_settingsManager->SetString(id, value);
+}
+
+std::vector<CVariant> CSettingsBase::GetList(const std::string& id) const
+{
+ std::shared_ptr<CSetting> setting = m_settingsManager->GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::List)
+ return std::vector<CVariant>();
+
+ return CSettingUtils::GetList(std::static_pointer_cast<CSettingList>(setting));
+}
+
+bool CSettingsBase::SetList(const std::string& id, const std::vector<CVariant>& value)
+{
+ std::shared_ptr<CSetting> setting = m_settingsManager->GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::List)
+ return false;
+
+ return CSettingUtils::SetList(std::static_pointer_cast<CSettingList>(setting), value);
+}
+
+bool CSettingsBase::SetDefault(const std::string &id)
+{
+ return m_settingsManager->SetDefault(id);
+}
+
+void CSettingsBase::SetDefaults()
+{
+ m_settingsManager->SetDefaults();
+}
+
+bool CSettingsBase::InitializeDefinitionsFromXml(const CXBMCTinyXML& xml)
+{
+ const TiXmlElement* root = xml.RootElement();
+ if (root == nullptr)
+ return false;
+
+ return m_settingsManager->Initialize(root);
+}
diff --git a/xbmc/settings/SettingsBase.h b/xbmc/settings/SettingsBase.h
new file mode 100644
index 0000000..209b6cc
--- /dev/null
+++ b/xbmc/settings/SettingsBase.h
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2016-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"
+#include "threads/CriticalSection.h"
+
+#include <set>
+#include <string>
+#include <vector>
+
+class CSetting;
+class CSettingSection;
+class CSettingsManager;
+class CVariant;
+class CXBMCTinyXML;
+class TiXmlElement;
+
+/*!
+ \brief Basic wrapper around CSettingsManager providing the framework for
+ properly setting up the settings manager and registering all the callbacks,
+ handlers and custom setting types.
+ \sa CSettingsManager
+ */
+class CSettingsBase
+{
+public:
+ virtual ~CSettingsBase();
+
+ CSettingsManager* GetSettingsManager() const { return m_settingsManager; }
+
+ /*!
+ \brief Initializes the setting system with the generic
+ settings definition and platform specific setting definitions.
+
+ \return True if the initialization was successful, false otherwise
+ */
+ virtual bool Initialize();
+ /*!
+ \brief Returns whether the settings system has been initialized or not.
+ */
+ virtual bool IsInitialized() const;
+ /*!
+ \brief Loads the setting values.
+
+ \return True if the setting values are successfully loaded, false otherwise
+ */
+ virtual bool Load() = 0;
+ /*!
+ \brief Tells the settings system that all setting values
+ have been loaded.
+
+ This manual trigger is necessary to enable the ISettingCallback methods
+ being executed.
+ */
+ virtual void SetLoaded();
+ /*!
+ \brief Returns whether the settings system has been loaded or not.
+ */
+ virtual bool IsLoaded() const;
+ /*!
+ \brief Saves the setting values.
+
+ \return True if the setting values were successfully saved, false otherwise
+ */
+ virtual bool Save() = 0;
+ /*!
+ \brief Unloads the previously loaded setting values.
+
+ The values of all the settings are reset to their default values.
+ */
+ virtual void Unload();
+ /*!
+ \brief Uninitializes the settings system.
+
+ Unregisters all previously registered callbacks and destroys all setting
+ objects.
+ */
+ virtual void Uninitialize();
+
+ /*!
+ \brief Registers the given ISettingCallback implementation for the given
+ set of settings.
+
+ \param callback ISettingCallback implementation
+ \param settingList List of setting identifiers for which the given callback shall be triggered
+ */
+ void RegisterCallback(ISettingCallback* callback, const std::set<std::string>& settingList);
+ /*!
+ \brief Unregisters the given ISettingCallback implementation.
+
+ \param callback ISettingCallback implementation
+ */
+ void UnregisterCallback(ISettingCallback* callback);
+
+ /*!
+ \brief Gets the setting with the given identifier.
+
+ \param id Setting identifier
+ \return Setting object with the given identifier or NULL if the identifier is unknown
+ */
+ std::shared_ptr<CSetting> GetSetting(const std::string& id) const;
+ /*!
+ \brief Gets the full list of setting sections.
+
+ \return List of setting sections
+ */
+ std::vector<std::shared_ptr<CSettingSection>> GetSections() const;
+ /*!
+ \brief Gets the setting section with the given identifier.
+
+ \param section Setting section identifier
+ \return Setting section with the given identifier or NULL if the identifier is unknown
+ */
+ std::shared_ptr<CSettingSection> GetSection(const std::string& section) const;
+
+ /*!
+ \brief Gets the boolean value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \return Boolean value of the setting with the given identifier
+ */
+ bool GetBool(const std::string& id) const;
+ /*!
+ \brief Gets the integer value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \return Integer value of the setting with the given identifier
+ */
+ int GetInt(const std::string& id) const;
+ /*!
+ \brief Gets the real number value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \return Real number value of the setting with the given identifier
+ */
+ double GetNumber(const std::string& id) const;
+ /*!
+ \brief Gets the string value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \return String value of the setting with the given identifier
+ */
+ std::string GetString(const std::string& id) const;
+ /*!
+ \brief Gets the values of the list setting with the given identifier.
+
+ \param id Setting identifier
+ \return List of values of the setting with the given identifier
+ */
+ std::vector<CVariant> GetList(const std::string& id) const;
+
+ /*!
+ \brief Sets the boolean value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \param value Boolean value to set
+ \return True if setting the value was successful, false otherwise
+ */
+ bool SetBool(const std::string& id, bool value);
+ /*!
+ \brief Toggles the boolean value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \return True if toggling the boolean value was successful, false otherwise
+ */
+ bool ToggleBool(const std::string& id);
+ /*!
+ \brief Sets the integer value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \param value Integer value to set
+ \return True if setting the value was successful, false otherwise
+ */
+ bool SetInt(const std::string& id, int value);
+ /*!
+ \brief Sets the real number value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \param value Real number value to set
+ \return True if setting the value was successful, false otherwise
+ */
+ bool SetNumber(const std::string& id, double value);
+ /*!
+ \brief Sets the string value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \param value String value to set
+ \return True if setting the value was successful, false otherwise
+ */
+ bool SetString(const std::string& id, const std::string& value);
+ /*!
+ \brief Sets the values of the list setting with the given identifier.
+
+ \param id Setting identifier
+ \param value Values to set
+ \return True if setting the values was successful, false otherwise
+ */
+ bool SetList(const std::string& id, const std::vector<CVariant>& value);
+
+ /*!
+ \brief Sets the value of the setting with the given identifier to its default.
+
+ \param id Setting identifier
+ \return True if setting the value to its default was successful, false otherwise
+ */
+ bool SetDefault(const std::string &id);
+ /*!
+ \brief Sets the value of all settings to their default.
+ */
+ void SetDefaults();
+
+protected:
+ CSettingsBase();
+
+ virtual void InitializeSettingTypes() { }
+ virtual void InitializeControls() { }
+ virtual void InitializeOptionFillers() { }
+ virtual void UninitializeOptionFillers() { }
+ virtual void InitializeConditions() { }
+ virtual void UninitializeConditions() { }
+ virtual bool InitializeDefinitions() = 0;
+ virtual void InitializeVisibility() { }
+ virtual void InitializeDefaults() { }
+ virtual void InitializeISettingsHandlers() { }
+ virtual void UninitializeISettingsHandlers() { }
+ virtual void InitializeISubSettings() { }
+ virtual void UninitializeISubSettings() { }
+ virtual void InitializeISettingCallbacks() { }
+ virtual void UninitializeISettingCallbacks() { }
+
+ bool InitializeDefinitionsFromXml(const CXBMCTinyXML& xml);
+
+ /*!
+ \brief Loads setting values from the given document in XML format
+
+ \param xml Document in XML format from which the settings are loaded
+ \param updated Output parameter indicating whether setting values had to be updated
+ \return True if the setting values were successfully loaded, false otherwise
+ */
+ bool LoadValuesFromXml(const CXBMCTinyXML& xml, bool& updated);
+ /*!
+ \brief Saves the setting values in XML format to the given document.
+
+ \param xml Document to save the setting values in XML format into
+ \return True if the setting values were successfully saved, false otherwise
+ */
+ bool SaveValuesToXml(CXBMCTinyXML& xml) const;
+
+ /*!
+ \brief Loads setting values from the given XML element.
+
+ \param root XML element containing setting values
+ \param updated Output parameter indicating whether setting values had to be updated
+ \return True if the setting values were successfully loaded, false otherwise
+ */
+ bool LoadValuesFromXml(const TiXmlElement* root, bool& updated);
+
+ /*!
+ \brief Loads hidden setting values from the given XML element.
+
+ \param root XML element containing setting values
+ \return True if the setting values were successfully loaded, false otherwise
+ */
+ bool LoadHiddenValuesFromXml(const TiXmlElement* root);
+
+ bool m_initialized = false;
+ CSettingsManager* m_settingsManager;
+ mutable CCriticalSection m_critical;
+private:
+ CSettingsBase(const CSettingsBase&) = delete;
+ CSettingsBase& operator=(const CSettingsBase&) = delete;
+};
diff --git a/xbmc/settings/SettingsComponent.cpp b/xbmc/settings/SettingsComponent.cpp
new file mode 100644
index 0000000..0f5f7ae
--- /dev/null
+++ b/xbmc/settings/SettingsComponent.cpp
@@ -0,0 +1,400 @@
+/*
+ * 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 "SettingsComponent.h"
+
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "application/AppParams.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#ifdef TARGET_DARWIN_EMBEDDED
+#include "platform/darwin/ios-common/DarwinEmbedUtils.h"
+#endif
+#ifdef TARGET_WINDOWS
+#include "platform/Environment.h"
+#endif
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SubtitlesSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#ifdef TARGET_WINDOWS
+#include "win32util.h"
+#endif
+
+CSettingsComponent::CSettingsComponent()
+ : m_settings(new CSettings()),
+ m_advancedSettings(new CAdvancedSettings()),
+ m_subtitlesSettings(new KODI::SUBTITLES::CSubtitlesSettings(m_settings)),
+ m_profileManager(new CProfileManager())
+{
+}
+
+CSettingsComponent::~CSettingsComponent()
+{
+}
+
+void CSettingsComponent::Initialize()
+{
+ if (m_state == State::DEINITED)
+ {
+ const auto params = CServiceBroker::GetAppParams();
+
+ // only the InitDirectories* for the current platform should return true
+ bool inited = InitDirectoriesLinux(params->HasPlatformDirectories());
+ if (!inited)
+ inited = InitDirectoriesOSX(params->HasPlatformDirectories());
+ if (!inited)
+ inited = InitDirectoriesWin32(params->HasPlatformDirectories());
+
+ m_settings->Initialize();
+
+ m_advancedSettings->Initialize(*m_settings->GetSettingsManager());
+ URIUtils::RegisterAdvancedSettings(*m_advancedSettings);
+
+ m_profileManager->Initialize(m_settings);
+
+ m_state = State::INITED;
+ }
+}
+
+bool CSettingsComponent::Load()
+{
+ if (m_state == State::INITED)
+ {
+ if (!m_profileManager->Load())
+ {
+ CLog::Log(LOGFATAL, "unable to load profile");
+ return false;
+ }
+
+ CSpecialProtocol::RegisterProfileManager(*m_profileManager);
+ XFILE::IDirectory::RegisterProfileManager(*m_profileManager);
+
+ if (!m_settings->Load())
+ {
+ CLog::Log(LOGFATAL, "unable to load settings");
+ return false;
+ }
+
+ m_settings->SetLoaded();
+
+ m_state = State::LOADED;
+ return true;
+ }
+ else if (m_state == State::LOADED)
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+void CSettingsComponent::Deinitialize()
+{
+ if (m_state >= State::INITED)
+ {
+ if (m_state == State::LOADED)
+ {
+ m_subtitlesSettings.reset();
+
+ m_settings->Unload();
+
+ XFILE::IDirectory::UnregisterProfileManager();
+ CSpecialProtocol::UnregisterProfileManager();
+ }
+ m_profileManager->Uninitialize();
+
+ URIUtils::UnregisterAdvancedSettings();
+ m_advancedSettings->Uninitialize(*m_settings->GetSettingsManager());
+
+ m_settings->Uninitialize();
+ }
+ m_state = State::DEINITED;
+}
+
+std::shared_ptr<CSettings> CSettingsComponent::GetSettings()
+{
+ return m_settings;
+}
+
+std::shared_ptr<CAdvancedSettings> CSettingsComponent::GetAdvancedSettings()
+{
+ return m_advancedSettings;
+}
+
+std::shared_ptr<KODI::SUBTITLES::CSubtitlesSettings> CSettingsComponent::GetSubtitlesSettings()
+{
+ return m_subtitlesSettings;
+}
+
+std::shared_ptr<CProfileManager> CSettingsComponent::GetProfileManager()
+{
+ return m_profileManager;
+}
+
+bool CSettingsComponent::InitDirectoriesLinux(bool bPlatformDirectories)
+{
+ /*
+ The following is the directory mapping for Platform Specific Mode:
+
+ special://xbmc/ => [read-only] system directory (/usr/share/kodi)
+ special://home/ => [read-write] user's directory that will override special://kodi/ system-wide
+ installations like skins, screensavers, etc.
+ ($HOME/.kodi)
+ NOTE: XBMC will look in both special://xbmc/addons and special://home/addons for addons.
+ special://masterprofile/ => [read-write] userdata of master profile. It will by default be
+ mapped to special://home/userdata ($HOME/.kodi/userdata)
+ special://profile/ => [read-write] current profile's userdata directory.
+ Generally special://masterprofile for the master profile or
+ special://masterprofile/profiles/<profile_name> for other profiles.
+
+ NOTE: All these root directories are lowercase. Some of the sub-directories
+ might be mixed case.
+ */
+
+#if defined(TARGET_POSIX) && !defined(TARGET_DARWIN)
+ std::string appPath;
+ std::string appName = CCompileInfo::GetAppName();
+ std::string dotLowerAppName = "." + appName;
+ StringUtils::ToLower(dotLowerAppName);
+ const char* envAppHome = "KODI_HOME";
+ const char* envAppBinHome = "KODI_BIN_HOME";
+ const char* envAppTemp = "KODI_TEMP";
+
+ std::string userHome;
+ if (getenv("KODI_DATA"))
+ userHome = getenv("KODI_DATA");
+ else if (getenv("HOME"))
+ {
+ userHome = getenv("HOME");
+ userHome.append("/" + dotLowerAppName);
+ }
+ else
+ {
+ userHome = "/root";
+ userHome.append("/" + dotLowerAppName);
+ }
+
+ std::string strTempPath;
+ if (getenv(envAppTemp))
+ strTempPath = getenv(envAppTemp);
+ else
+ strTempPath = userHome + "/temp";
+
+
+ std::string binaddonAltDir;
+ if (getenv("KODI_BINADDON_PATH"))
+ binaddonAltDir = getenv("KODI_BINADDON_PATH");
+
+ auto appBinPath = CUtil::GetHomePath(envAppBinHome);
+ // overridden by user
+ if (getenv(envAppHome))
+ appPath = getenv(envAppHome);
+ else
+ {
+ // use build time default
+ appPath = INSTALL_PATH;
+ /* Check if binaries and arch independent data files are being kept in
+ * separate locations. */
+ if (!XFILE::CDirectory::Exists(URIUtils::AddFileToFolder(appPath, "userdata")))
+ {
+ /* Attempt to locate arch independent data files. */
+ appPath = CUtil::GetHomePath(appBinPath);
+ if (!XFILE::CDirectory::Exists(URIUtils::AddFileToFolder(appPath, "userdata")))
+ {
+ fprintf(stderr, "Unable to find path to %s data files!\n", appName.c_str());
+ exit(1);
+ }
+ }
+ }
+
+ /* Set some environment variables */
+ setenv(envAppBinHome, appBinPath.c_str(), 0);
+ setenv(envAppHome, appPath.c_str(), 0);
+
+ if (bPlatformDirectories)
+ {
+ // map our special drives
+ CSpecialProtocol::SetXBMCBinPath(appBinPath);
+ CSpecialProtocol::SetXBMCAltBinAddonPath(binaddonAltDir);
+ CSpecialProtocol::SetXBMCPath(appPath);
+ CSpecialProtocol::SetHomePath(userHome);
+ CSpecialProtocol::SetMasterProfilePath(userHome + "/userdata");
+ CSpecialProtocol::SetTempPath(strTempPath);
+ CSpecialProtocol::SetLogPath(strTempPath);
+
+ CreateUserDirs();
+
+ }
+ else
+ {
+ URIUtils::AddSlashAtEnd(appPath);
+
+ CSpecialProtocol::SetXBMCBinPath(appBinPath);
+ CSpecialProtocol::SetXBMCAltBinAddonPath(binaddonAltDir);
+ CSpecialProtocol::SetXBMCPath(appPath);
+ CSpecialProtocol::SetHomePath(URIUtils::AddFileToFolder(appPath, "portable_data"));
+ CSpecialProtocol::SetMasterProfilePath(URIUtils::AddFileToFolder(appPath, "portable_data/userdata"));
+
+ std::string strTempPath = appPath;
+ strTempPath = URIUtils::AddFileToFolder(strTempPath, "portable_data/temp");
+ if (getenv(envAppTemp))
+ strTempPath = getenv(envAppTemp);
+ CSpecialProtocol::SetTempPath(strTempPath);
+ CSpecialProtocol::SetLogPath(strTempPath);
+ CreateUserDirs();
+ }
+ CSpecialProtocol::SetXBMCBinAddonPath(appBinPath + "/addons");
+
+ return true;
+#else
+ return false;
+#endif
+}
+
+bool CSettingsComponent::InitDirectoriesOSX(bool bPlatformDirectories)
+{
+#if defined(TARGET_DARWIN)
+ std::string userHome;
+ if (getenv("HOME"))
+ userHome = getenv("HOME");
+ else
+ userHome = "/root";
+
+ std::string binaddonAltDir;
+ if (getenv("KODI_BINADDON_PATH"))
+ binaddonAltDir = getenv("KODI_BINADDON_PATH");
+
+ std::string appPath = CUtil::GetHomePath();
+ setenv("KODI_HOME", appPath.c_str(), 0);
+
+#if defined(TARGET_DARWIN_EMBEDDED)
+ std::string fontconfigPath;
+ fontconfigPath = appPath + "/system/players/VideoPlayer/etc/fonts/fonts.conf";
+ setenv("FONTCONFIG_FILE", fontconfigPath.c_str(), 0);
+#endif
+
+ // setup path to our internal dylibs so loader can find them
+ std::string frameworksPath = CUtil::GetFrameworksPath();
+ CSpecialProtocol::SetXBMCFrameworksPath(frameworksPath);
+
+ if (bPlatformDirectories)
+ {
+ // map our special drives
+ CSpecialProtocol::SetXBMCBinPath(appPath);
+ CSpecialProtocol::SetXBMCAltBinAddonPath(binaddonAltDir);
+ CSpecialProtocol::SetXBMCPath(appPath);
+#if defined(TARGET_DARWIN_EMBEDDED)
+ std::string appName = CCompileInfo::GetAppName();
+ CSpecialProtocol::SetHomePath(userHome + "/" + CDarwinEmbedUtils::GetAppRootFolder() + "/" +
+ appName);
+ CSpecialProtocol::SetMasterProfilePath(userHome + "/" + CDarwinEmbedUtils::GetAppRootFolder() +
+ "/" + appName + "/userdata");
+#else
+ std::string appName = CCompileInfo::GetAppName();
+ CSpecialProtocol::SetHomePath(userHome + "/Library/Application Support/" + appName);
+ CSpecialProtocol::SetMasterProfilePath(userHome + "/Library/Application Support/" + appName + "/userdata");
+#endif
+
+ std::string dotLowerAppName = "." + appName;
+ StringUtils::ToLower(dotLowerAppName);
+ // location for temp files
+#if defined(TARGET_DARWIN_EMBEDDED)
+ std::string strTempPath = URIUtils::AddFileToFolder(
+ userHome, std::string(CDarwinEmbedUtils::GetAppRootFolder()) + "/" + appName + "/temp");
+#else
+ std::string strTempPath = URIUtils::AddFileToFolder(userHome, dotLowerAppName + "/");
+ XFILE::CDirectory::Create(strTempPath);
+ strTempPath = URIUtils::AddFileToFolder(userHome, dotLowerAppName + "/temp");
+#endif
+ CSpecialProtocol::SetTempPath(strTempPath);
+
+ // xbmc.log file location
+#if defined(TARGET_DARWIN_EMBEDDED)
+ strTempPath = userHome + "/" + std::string(CDarwinEmbedUtils::GetAppRootFolder());
+#else
+ strTempPath = userHome + "/Library/Logs";
+#endif
+ CSpecialProtocol::SetLogPath(strTempPath);
+ CreateUserDirs();
+ }
+ else
+ {
+ URIUtils::AddSlashAtEnd(appPath);
+
+ CSpecialProtocol::SetXBMCBinPath(appPath);
+ CSpecialProtocol::SetXBMCAltBinAddonPath(binaddonAltDir);
+ CSpecialProtocol::SetXBMCPath(appPath);
+ CSpecialProtocol::SetHomePath(URIUtils::AddFileToFolder(appPath, "portable_data"));
+ CSpecialProtocol::SetMasterProfilePath(URIUtils::AddFileToFolder(appPath, "portable_data/userdata"));
+
+ std::string strTempPath = URIUtils::AddFileToFolder(appPath, "portable_data/temp");
+ CSpecialProtocol::SetTempPath(strTempPath);
+ CSpecialProtocol::SetLogPath(strTempPath);
+ CreateUserDirs();
+ }
+ CSpecialProtocol::SetXBMCBinAddonPath(appPath + "/addons");
+ return true;
+#else
+ return false;
+#endif
+}
+
+bool CSettingsComponent::InitDirectoriesWin32(bool bPlatformDirectories)
+{
+#ifdef TARGET_WINDOWS
+ std::string xbmcPath = CUtil::GetHomePath();
+ CEnvironment::setenv("KODI_HOME", xbmcPath);
+ CSpecialProtocol::SetXBMCBinPath(xbmcPath);
+ CSpecialProtocol::SetXBMCPath(xbmcPath);
+ CSpecialProtocol::SetXBMCBinAddonPath(xbmcPath + "/addons");
+
+ std::string strWin32UserFolder = CWIN32Util::GetProfilePath(bPlatformDirectories);
+ CSpecialProtocol::SetLogPath(strWin32UserFolder);
+ CSpecialProtocol::SetHomePath(strWin32UserFolder);
+ CSpecialProtocol::SetMasterProfilePath(URIUtils::AddFileToFolder(strWin32UserFolder, "userdata"));
+ CSpecialProtocol::SetTempPath(URIUtils::AddFileToFolder(strWin32UserFolder,"cache"));
+
+ CEnvironment::setenv("KODI_PROFILE_USERDATA", CSpecialProtocol::TranslatePath("special://masterprofile/"));
+
+ CreateUserDirs();
+
+ return true;
+#else
+ return false;
+#endif
+}
+
+void CSettingsComponent::CreateUserDirs() const
+{
+ XFILE::CDirectory::Create("special://home/");
+ XFILE::CDirectory::Create("special://home/addons");
+ XFILE::CDirectory::Create("special://home/addons/packages");
+ XFILE::CDirectory::Create("special://home/addons/temp");
+ XFILE::CDirectory::Create("special://home/media");
+ XFILE::CDirectory::Create("special://home/system");
+ XFILE::CDirectory::Create("special://masterprofile/");
+ XFILE::CDirectory::Create("special://temp/");
+ XFILE::CDirectory::Create("special://logpath");
+ XFILE::CDirectory::Create("special://temp/temp"); // temp directory for python and dllGetTempPathA
+
+ //Let's clear our archive cache before starting up anything more
+ auto archiveCachePath = CSpecialProtocol::TranslatePath("special://temp/archive_cache/");
+ if (XFILE::CDirectory::Exists(archiveCachePath))
+ if (!XFILE::CDirectory::RemoveRecursive(archiveCachePath))
+ CLog::Log(LOGWARNING, "Failed to remove the archive cache at {}", archiveCachePath);
+ XFILE::CDirectory::Create(archiveCachePath);
+
+}
diff --git a/xbmc/settings/SettingsComponent.h b/xbmc/settings/SettingsComponent.h
new file mode 100644
index 0000000..42bb9a8
--- /dev/null
+++ b/xbmc/settings/SettingsComponent.h
@@ -0,0 +1,89 @@
+/*
+ * 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 <memory>
+
+class CAdvancedSettings;
+class CProfileManager;
+class CSettings;
+
+namespace KODI
+{
+namespace SUBTITLES
+{
+class CSubtitlesSettings;
+} // namespace SUBTITLES
+} // namespace KODI
+
+class CSettingsComponent
+{
+public:
+ CSettingsComponent();
+ virtual ~CSettingsComponent();
+
+ /*!
+ * @brief Initialize all subcomponents with system default values (loaded from code, system settings files, ...).
+ */
+ void Initialize();
+
+ /*!
+ * @brief Initialize all subcomponents with user values (loaded from user settings files, according to active profile).
+ * @return true on success, false otherwise.
+ */
+ bool Load();
+
+ /*!
+ * @brief Deinitialize all subcomponents.
+ */
+ void Deinitialize();
+
+ /*!
+ * @brief Get access to the settings subcomponent.
+ * @return the settings subcomponent.
+ */
+ std::shared_ptr<CSettings> GetSettings();
+
+ /*!
+ * @brief Get access to the advanced settings subcomponent.
+ * @return the advanced settings subcomponent.
+ */
+ std::shared_ptr<CAdvancedSettings> GetAdvancedSettings();
+
+ /*!
+ * @brief Get access to the subtitles settings subcomponent.
+ * @return the subtiltles settings subcomponent.
+ */
+ std::shared_ptr<KODI::SUBTITLES::CSubtitlesSettings> GetSubtitlesSettings();
+
+ /*!
+ * @brief Get access to the profiles manager subcomponent.
+ * @return the profiles manager subcomponent.
+ */
+ std::shared_ptr<CProfileManager> GetProfileManager();
+
+private:
+ bool InitDirectoriesLinux(bool bPlatformDirectories);
+ bool InitDirectoriesOSX(bool bPlatformDirectories);
+ bool InitDirectoriesWin32(bool bPlatformDirectories);
+ void CreateUserDirs() const;
+
+ enum class State
+ {
+ DEINITED,
+ INITED,
+ LOADED
+ };
+ State m_state = State::DEINITED;
+
+ std::shared_ptr<CSettings> m_settings;
+ std::shared_ptr<CAdvancedSettings> m_advancedSettings;
+ std::shared_ptr<KODI::SUBTITLES::CSubtitlesSettings> m_subtitlesSettings;
+ std::shared_ptr<CProfileManager> m_profileManager;
+};
diff --git a/xbmc/settings/SettingsValueFlatJsonSerializer.cpp b/xbmc/settings/SettingsValueFlatJsonSerializer.cpp
new file mode 100644
index 0000000..736c37d
--- /dev/null
+++ b/xbmc/settings/SettingsValueFlatJsonSerializer.cpp
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+
+#include "SettingsValueFlatJsonSerializer.h"
+
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingSection.h"
+#include "settings/lib/SettingType.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/JSONVariantWriter.h"
+#include "utils/log.h"
+
+CSettingsValueFlatJsonSerializer::CSettingsValueFlatJsonSerializer(bool compact /* = true */)
+ : m_compact(compact)
+{ }
+
+std::string CSettingsValueFlatJsonSerializer::SerializeValues(
+ const CSettingsManager* settingsManager) const
+{
+ if (settingsManager == nullptr)
+ return "";
+
+ CVariant root(CVariant::VariantTypeObject);
+
+ const auto sections = settingsManager->GetSections();
+ for (const auto& section : sections)
+ SerializeSection(root, section);
+
+ std::string result;
+ if (!CJSONVariantWriter::Write(root, result, m_compact))
+ {
+ CLog::Log(LOGWARNING,
+ "CSettingsValueFlatJsonSerializer: failed to serialize settings into JSON");
+ return "";
+ }
+
+ return result;
+}
+
+void CSettingsValueFlatJsonSerializer::SerializeSection(
+ CVariant& parent, const std::shared_ptr<CSettingSection>& section) const
+{
+ if (section == nullptr)
+ return;
+
+ const auto categories = section->GetCategories();
+ for (const auto& category : categories)
+ SerializeCategory(parent, category);
+}
+
+void CSettingsValueFlatJsonSerializer::SerializeCategory(
+ CVariant& parent, const std::shared_ptr<CSettingCategory>& category) const
+{
+ if (category == nullptr)
+ return;
+
+ const auto groups = category->GetGroups();
+ for (const auto& group : groups)
+ SerializeGroup(parent, group);
+}
+
+void CSettingsValueFlatJsonSerializer::SerializeGroup(
+ CVariant& parent, const std::shared_ptr<CSettingGroup>& group) const
+{
+ if (group == nullptr)
+ return;
+
+ const auto settings = group->GetSettings();
+ for (const auto& setting : settings)
+ SerializeSetting(parent, setting);
+}
+
+void CSettingsValueFlatJsonSerializer::SerializeSetting(
+ CVariant& parent, const std::shared_ptr<CSetting>& setting) const
+{
+ if (setting == nullptr)
+ return;
+
+ // ignore references and action settings (which don't have a value)
+ if (setting->IsReference() || setting->GetType() == SettingType::Action)
+ return;
+
+ const auto valueObj = SerializeSettingValue(setting);
+ if (valueObj.isNull())
+ return;
+
+ parent[setting->GetId()] = valueObj;
+}
+
+CVariant CSettingsValueFlatJsonSerializer::SerializeSettingValue(
+ const std::shared_ptr<CSetting>& setting) const
+{
+ switch (setting->GetType())
+ {
+ case SettingType::Action:
+ return CVariant::ConstNullVariant;
+
+ case SettingType::Boolean:
+ return CVariant(std::static_pointer_cast<CSettingBool>(setting)->GetValue());
+
+ case SettingType::Integer:
+ return CVariant(std::static_pointer_cast<CSettingInt>(setting)->GetValue());
+
+ case SettingType::Number:
+ return CVariant(std::static_pointer_cast<CSettingNumber>(setting)->GetValue());
+
+ case SettingType::String:
+ return CVariant(std::static_pointer_cast<CSettingString>(setting)->GetValue());
+
+ case SettingType::List:
+ {
+ const auto settingList = std::static_pointer_cast<CSettingList>(setting);
+
+ CVariant settingListValuesObj(CVariant::VariantTypeArray);
+ const auto settingListValues = settingList->GetValue();
+ for (const auto& settingListValue : settingListValues)
+ {
+ const auto valueObj = SerializeSettingValue(settingListValue);
+ if (!valueObj.isNull())
+ settingListValuesObj.push_back(valueObj);
+ }
+
+ return settingListValuesObj;
+ }
+
+ case SettingType::Unknown:
+ default:
+ CLog::Log(LOGWARNING,
+ "CSettingsValueFlatJsonSerializer: failed to serialize setting \"{}\" with value \"{}\" " \
+ "of unknown type", setting->GetId(), setting->ToString());
+ return CVariant::ConstNullVariant;
+ }
+}
diff --git a/xbmc/settings/SettingsValueFlatJsonSerializer.h b/xbmc/settings/SettingsValueFlatJsonSerializer.h
new file mode 100644
index 0000000..05214ab
--- /dev/null
+++ b/xbmc/settings/SettingsValueFlatJsonSerializer.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.
+ */
+
+#pragma once
+
+#include "settings/lib/ISettingsValueSerializer.h"
+#include "utils/Variant.h"
+
+#include <memory>
+
+class CSetting;
+class CSettingCategory;
+class CSettingGroup;
+class CSettingSection;
+
+class CSettingsValueFlatJsonSerializer : public ISettingsValueSerializer
+{
+public:
+ explicit CSettingsValueFlatJsonSerializer(bool compact = true);
+ ~CSettingsValueFlatJsonSerializer() = default;
+
+ void SetCompact(bool compact = true) { m_compact = compact; }
+
+ // implementation of ISettingsValueSerializer
+ std::string SerializeValues(const CSettingsManager* settingsManager) const override;
+
+private:
+ void SerializeSection(CVariant& parent, const std::shared_ptr<CSettingSection>& section) const;
+ void SerializeCategory(CVariant& parent, const std::shared_ptr<CSettingCategory>& category) const;
+ void SerializeGroup(CVariant& parent, const std::shared_ptr<CSettingGroup>& group) const;
+ void SerializeSetting(CVariant& parent, const std::shared_ptr<CSetting>& setting) const;
+ CVariant SerializeSettingValue(const std::shared_ptr<CSetting>& setting) const;
+
+ bool m_compact;
+};
diff --git a/xbmc/settings/SettingsValueXmlSerializer.cpp b/xbmc/settings/SettingsValueXmlSerializer.cpp
new file mode 100644
index 0000000..42f83e3
--- /dev/null
+++ b/xbmc/settings/SettingsValueXmlSerializer.cpp
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+#include "SettingsValueXmlSerializer.h"
+
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingSection.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+static constexpr char SETTINGS_XML_ROOT[] = "settings";
+
+std::string CSettingsValueXmlSerializer::SerializeValues(
+ const CSettingsManager* settingsManager) const
+{
+ if (settingsManager == nullptr)
+ return "";
+
+ CXBMCTinyXML xmlDoc;
+ TiXmlElement rootElement(SETTINGS_XML_ROOT);
+ rootElement.SetAttribute(SETTING_XML_ROOT_VERSION, settingsManager->GetVersion());
+ TiXmlNode* xmlRoot = xmlDoc.InsertEndChild(rootElement);
+ if (xmlRoot == nullptr)
+ return "";
+
+ const auto sections = settingsManager->GetSections();
+ for (const auto& section : sections)
+ SerializeSection(xmlRoot, section);
+
+ std::stringstream stream;
+ stream << *xmlDoc.RootElement();
+
+ return stream.str();
+}
+
+void CSettingsValueXmlSerializer::SerializeSection(
+ TiXmlNode* parent, const std::shared_ptr<CSettingSection>& section) const
+{
+ if (section == nullptr)
+ return;
+
+ const auto categories = section->GetCategories();
+ for (const auto& category : categories)
+ SerializeCategory(parent, category);
+}
+
+void CSettingsValueXmlSerializer::SerializeCategory(
+ TiXmlNode* parent, const std::shared_ptr<CSettingCategory>& category) const
+{
+ if (category == nullptr)
+ return;
+
+ const auto groups = category->GetGroups();
+ for (const auto& group : groups)
+ SerializeGroup(parent, group);
+}
+
+void CSettingsValueXmlSerializer::SerializeGroup(TiXmlNode* parent,
+ const std::shared_ptr<CSettingGroup>& group) const
+{
+ if (group == nullptr)
+ return;
+
+ const auto settings = group->GetSettings();
+ for (const auto& setting : settings)
+ SerializeSetting(parent, setting);
+}
+
+void CSettingsValueXmlSerializer::SerializeSetting(TiXmlNode* parent,
+ const std::shared_ptr<CSetting>& setting) const
+{
+ if (setting == nullptr)
+ return;
+
+ // ignore references and action settings (which don't have a value)
+ if (setting->IsReference() || setting->GetType() == SettingType::Action)
+ return;
+
+ TiXmlElement settingElement(SETTING_XML_ELM_SETTING);
+ settingElement.SetAttribute(SETTING_XML_ATTR_ID, setting->GetId());
+
+ // add the default attribute
+ if (setting->IsDefault())
+ settingElement.SetAttribute(SETTING_XML_ELM_DEFAULT, "true");
+
+ // add the value
+ TiXmlText value(setting->ToString());
+ settingElement.InsertEndChild(value);
+
+ if (parent->InsertEndChild(settingElement) == nullptr)
+ CLog::Log(LOGWARNING,
+ "CSettingsValueXmlSerializer: unable to write <" SETTING_XML_ELM_SETTING " id=\"{}\"> tag",
+ setting->GetId());
+}
diff --git a/xbmc/settings/SettingsValueXmlSerializer.h b/xbmc/settings/SettingsValueXmlSerializer.h
new file mode 100644
index 0000000..010c036
--- /dev/null
+++ b/xbmc/settings/SettingsValueXmlSerializer.h
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "settings/lib/ISettingsValueSerializer.h"
+
+#include <memory>
+
+class CSetting;
+class CSettingCategory;
+class CSettingGroup;
+class CSettingSection;
+class TiXmlNode;
+
+class CSettingsValueXmlSerializer : public ISettingsValueSerializer
+{
+public:
+ CSettingsValueXmlSerializer() = default;
+ ~CSettingsValueXmlSerializer() = default;
+
+ // implementation of ISettingsValueSerializer
+ std::string SerializeValues(const CSettingsManager* settingsManager) const override;
+
+private:
+ void SerializeSection(TiXmlNode* parent, const std::shared_ptr<CSettingSection>& section) const;
+ void SerializeCategory(TiXmlNode* parent,
+ const std::shared_ptr<CSettingCategory>& category) const;
+ void SerializeGroup(TiXmlNode* parent, const std::shared_ptr<CSettingGroup>& group) const;
+ void SerializeSetting(TiXmlNode* parent, const std::shared_ptr<CSetting>& setting) const;
+};
diff --git a/xbmc/settings/SkinSettings.cpp b/xbmc/settings/SkinSettings.cpp
new file mode 100644
index 0000000..71ff616
--- /dev/null
+++ b/xbmc/settings/SkinSettings.cpp
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2013-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 "SkinSettings.h"
+
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "guilib/GUIComponent.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <mutex>
+#include <string>
+
+#define XML_SKINSETTINGS "skinsettings"
+
+CSkinSettings::CSkinSettings()
+{
+ Clear();
+}
+
+CSkinSettings::~CSkinSettings() = default;
+
+CSkinSettings& CSkinSettings::GetInstance()
+{
+ static CSkinSettings sSkinSettings;
+ return sSkinSettings;
+}
+
+int CSkinSettings::TranslateString(const std::string &setting)
+{
+ return g_SkinInfo->TranslateString(setting);
+}
+
+const std::string& CSkinSettings::GetString(int setting) const
+{
+ return g_SkinInfo->GetString(setting);
+}
+
+void CSkinSettings::SetString(int setting, const std::string &label)
+{
+ g_SkinInfo->SetString(setting, label);
+}
+
+int CSkinSettings::TranslateBool(const std::string &setting)
+{
+ return g_SkinInfo->TranslateBool(setting);
+}
+
+bool CSkinSettings::GetBool(int setting) const
+{
+ return g_SkinInfo->GetBool(setting);
+}
+
+int CSkinSettings::GetInt(int setting) const
+{
+ return g_SkinInfo->GetInt(setting);
+}
+
+void CSkinSettings::SetBool(int setting, bool set)
+{
+ g_SkinInfo->SetBool(setting, set);
+}
+
+void CSkinSettings::Reset(const std::string &setting)
+{
+ g_SkinInfo->Reset(setting);
+}
+
+std::set<ADDON::CSkinSettingPtr> CSkinSettings::GetSettings() const
+{
+ return g_SkinInfo->GetSkinSettings();
+}
+
+ADDON::CSkinSettingPtr CSkinSettings::GetSetting(const std::string& settingId)
+{
+ return g_SkinInfo->GetSkinSetting(settingId);
+}
+
+std::shared_ptr<const ADDON::CSkinSetting> CSkinSettings::GetSetting(
+ const std::string& settingId) const
+{
+ return g_SkinInfo->GetSkinSetting(settingId);
+}
+
+void CSkinSettings::Reset()
+{
+ g_SkinInfo->Reset();
+
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ infoMgr.ResetCache();
+ infoMgr.GetInfoProviders().GetGUIControlsInfoProvider().ResetContainerMovingCache();
+}
+
+bool CSkinSettings::Load(const TiXmlNode *settings)
+{
+ if (settings == nullptr)
+ return false;
+
+ const TiXmlElement *rootElement = settings->FirstChildElement(XML_SKINSETTINGS);
+
+ // return true in the case skinsettings is missing. It just means that
+ // it's been migrated and it's not an error
+ if (rootElement == nullptr)
+ {
+ CLog::Log(LOGDEBUG, "CSkinSettings: no <skinsettings> tag found");
+ return true;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_settings.clear();
+ m_settings = ADDON::CSkinInfo::ParseSettings(rootElement);
+
+ return true;
+}
+
+bool CSkinSettings::Save(TiXmlNode *settings) const
+{
+ if (settings == nullptr)
+ return false;
+
+ // nothing to do here because skin settings saving has been migrated to CSkinInfo
+
+ return true;
+}
+
+void CSkinSettings::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_settings.clear();
+}
+
+void CSkinSettings::MigrateSettings(const std::shared_ptr<ADDON::CSkinInfo>& skin)
+{
+ if (skin == nullptr)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ bool settingsMigrated = false;
+ const std::string& skinId = skin->ID();
+ std::set<ADDON::CSkinSettingPtr> settingsCopy(m_settings.begin(), m_settings.end());
+ for (const auto& setting : settingsCopy)
+ {
+ if (!StringUtils::StartsWith(setting->name, skinId + "."))
+ continue;
+
+ std::string settingName = setting->name.substr(skinId.size() + 1);
+
+ if (setting->GetType() == "string")
+ {
+ int settingNumber = skin->TranslateString(settingName);
+ if (settingNumber >= 0)
+ skin->SetString(settingNumber, std::dynamic_pointer_cast<ADDON::CSkinSettingString>(setting)->value);
+ }
+ else if (setting->GetType() == "bool")
+ {
+ int settingNumber = skin->TranslateBool(settingName);
+ if (settingNumber >= 0)
+ skin->SetBool(settingNumber, std::dynamic_pointer_cast<ADDON::CSkinSettingBool>(setting)->value);
+ }
+
+ m_settings.erase(setting);
+ settingsMigrated = true;
+ }
+
+ if (settingsMigrated)
+ {
+ // save the skin's settings
+ skin->SaveSettings();
+
+ // save the guisettings.xml
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ }
+}
+
diff --git a/xbmc/settings/SkinSettings.h b/xbmc/settings/SkinSettings.h
new file mode 100644
index 0000000..9ade245
--- /dev/null
+++ b/xbmc/settings/SkinSettings.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2013-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 "addons/Skin.h"
+#include "settings/ISubSettings.h"
+#include "threads/CriticalSection.h"
+
+#include <memory>
+#include <set>
+#include <string>
+
+class TiXmlNode;
+
+class CSkinSettings : public ISubSettings
+{
+public:
+ static CSkinSettings& GetInstance();
+
+ bool Load(const TiXmlNode *settings) override;
+ bool Save(TiXmlNode *settings) const override;
+ void Clear() override;
+
+ void MigrateSettings(const std::shared_ptr<ADDON::CSkinInfo>& skin);
+
+ int TranslateString(const std::string &setting);
+ const std::string& GetString(int setting) const;
+ void SetString(int setting, const std::string &label);
+
+ int TranslateBool(const std::string &setting);
+ bool GetBool(int setting) const;
+ void SetBool(int setting, bool set);
+
+ /*! \brief Get the skin setting value as an integer value
+ * \param setting - the setting id
+ * \return the setting value as an integer, -1 if no conversion is possible
+ */
+ int GetInt(int setting) const;
+
+ std::set<ADDON::CSkinSettingPtr> GetSettings() const;
+ ADDON::CSkinSettingPtr GetSetting(const std::string& settingId);
+ std::shared_ptr<const ADDON::CSkinSetting> GetSetting(const std::string& settingId) const;
+
+ void Reset(const std::string &setting);
+ void Reset();
+
+protected:
+ CSkinSettings();
+ CSkinSettings(const CSkinSettings&) = delete;
+ CSkinSettings& operator=(CSkinSettings const&) = delete;
+ ~CSkinSettings() override;
+
+private:
+ CCriticalSection m_critical;
+ std::set<ADDON::CSkinSettingPtr> m_settings;
+};
diff --git a/xbmc/settings/SubtitlesSettings.cpp b/xbmc/settings/SubtitlesSettings.cpp
new file mode 100644
index 0000000..66ef636
--- /dev/null
+++ b/xbmc/settings/SubtitlesSettings.cpp
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2012-2021 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 "SubtitlesSettings.h"
+
+#include "guilib/GUIFontManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/Settings.h"
+#include "settings/lib/Setting.h"
+#include "utils/FileUtils.h"
+#include "utils/FontUtils.h"
+#include "utils/URIUtils.h"
+
+using namespace KODI;
+using namespace SUBTITLES;
+
+CSubtitlesSettings::CSubtitlesSettings(const std::shared_ptr<CSettings>& settings)
+ : m_settings(settings)
+{
+ m_settings->RegisterCallback(
+ this,
+ {CSettings::SETTING_LOCALE_SUBTITLELANGUAGE, CSettings::SETTING_SUBTITLES_PARSECAPTIONS,
+ CSettings::SETTING_SUBTITLES_ALIGN, CSettings::SETTING_SUBTITLES_STEREOSCOPICDEPTH,
+ CSettings::SETTING_SUBTITLES_FONTNAME, CSettings::SETTING_SUBTITLES_FONTSIZE,
+ CSettings::SETTING_SUBTITLES_STYLE, CSettings::SETTING_SUBTITLES_COLOR,
+ CSettings::SETTING_SUBTITLES_BORDERSIZE, CSettings::SETTING_SUBTITLES_BORDERCOLOR,
+ CSettings::SETTING_SUBTITLES_OPACITY, CSettings::SETTING_SUBTITLES_BGCOLOR,
+ CSettings::SETTING_SUBTITLES_BGOPACITY, CSettings::SETTING_SUBTITLES_BLUR,
+ CSettings::SETTING_SUBTITLES_BACKGROUNDTYPE, CSettings::SETTING_SUBTITLES_SHADOWCOLOR,
+ CSettings::SETTING_SUBTITLES_SHADOWOPACITY, CSettings::SETTING_SUBTITLES_SHADOWSIZE,
+ CSettings::SETTING_SUBTITLES_MARGINVERTICAL, CSettings::SETTING_SUBTITLES_CHARSET,
+ CSettings::SETTING_SUBTITLES_OVERRIDEFONTS, CSettings::SETTING_SUBTITLES_OVERRIDESTYLES,
+ CSettings::SETTING_SUBTITLES_LANGUAGES, CSettings::SETTING_SUBTITLES_STORAGEMODE,
+ CSettings::SETTING_SUBTITLES_CUSTOMPATH, CSettings::SETTING_SUBTITLES_PAUSEONSEARCH,
+ CSettings::SETTING_SUBTITLES_DOWNLOADFIRST, CSettings::SETTING_SUBTITLES_TV,
+ CSettings::SETTING_SUBTITLES_MOVIE});
+}
+
+CSubtitlesSettings::~CSubtitlesSettings()
+{
+ m_settings->UnregisterCallback(this);
+}
+
+void CSubtitlesSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ SetChanged();
+ NotifyObservers(ObservableMessageSettingsChanged);
+ if (setting->GetId() == CSettings::SETTING_SUBTITLES_ALIGN)
+ {
+ SetChanged();
+ NotifyObservers(ObservableMessagePositionChanged);
+ }
+}
+
+Align CSubtitlesSettings::GetAlignment()
+{
+ return static_cast<Align>(m_settings->GetInt(CSettings::SETTING_SUBTITLES_ALIGN));
+}
+
+void CSubtitlesSettings::SetAlignment(Align align)
+{
+ m_settings->SetInt(CSettings::SETTING_SUBTITLES_ALIGN, static_cast<int>(align));
+}
+
+HorizontalAlign CSubtitlesSettings::GetHorizontalAlignment()
+{
+ return static_cast<HorizontalAlign>(
+ m_settings->GetInt(CSettings::SETTING_SUBTITLES_CAPTIONSALIGN));
+}
+
+std::string CSubtitlesSettings::GetFontName()
+{
+ return m_settings->GetString(CSettings::SETTING_SUBTITLES_FONTNAME);
+}
+
+FontStyle CSubtitlesSettings::GetFontStyle()
+{
+ return static_cast<FontStyle>(m_settings->GetInt(CSettings::SETTING_SUBTITLES_STYLE));
+}
+
+int CSubtitlesSettings::GetFontSize()
+{
+ return m_settings->GetInt(CSettings::SETTING_SUBTITLES_FONTSIZE);
+}
+
+UTILS::COLOR::Color CSubtitlesSettings::GetFontColor()
+{
+ return UTILS::COLOR::ConvertHexToColor(m_settings->GetString(CSettings::SETTING_SUBTITLES_COLOR));
+}
+
+int CSubtitlesSettings::GetFontOpacity()
+{
+ return m_settings->GetInt(CSettings::SETTING_SUBTITLES_OPACITY);
+}
+
+int CSubtitlesSettings::GetBorderSize()
+{
+ return m_settings->GetInt(CSettings::SETTING_SUBTITLES_BORDERSIZE);
+}
+
+UTILS::COLOR::Color CSubtitlesSettings::GetBorderColor()
+{
+ return UTILS::COLOR::ConvertHexToColor(
+ m_settings->GetString(CSettings::SETTING_SUBTITLES_BORDERCOLOR));
+}
+
+int CSubtitlesSettings::GetShadowSize()
+{
+ return m_settings->GetInt(CSettings::SETTING_SUBTITLES_SHADOWSIZE);
+}
+
+UTILS::COLOR::Color CSubtitlesSettings::GetShadowColor()
+{
+ return UTILS::COLOR::ConvertHexToColor(
+ m_settings->GetString(CSettings::SETTING_SUBTITLES_SHADOWCOLOR));
+}
+
+int CSubtitlesSettings::GetShadowOpacity()
+{
+ return m_settings->GetInt(CSettings::SETTING_SUBTITLES_SHADOWOPACITY);
+}
+
+int CSubtitlesSettings::GetBlurSize()
+{
+ return m_settings->GetInt(CSettings::SETTING_SUBTITLES_BLUR);
+}
+
+BackgroundType CSubtitlesSettings::GetBackgroundType()
+{
+ return static_cast<BackgroundType>(
+ m_settings->GetInt(CSettings::SETTING_SUBTITLES_BACKGROUNDTYPE));
+}
+
+UTILS::COLOR::Color CSubtitlesSettings::GetBackgroundColor()
+{
+ return UTILS::COLOR::ConvertHexToColor(
+ m_settings->GetString(CSettings::SETTING_SUBTITLES_BGCOLOR));
+}
+
+int CSubtitlesSettings::GetBackgroundOpacity()
+{
+ return m_settings->GetInt(CSettings::SETTING_SUBTITLES_BGOPACITY);
+}
+
+bool CSubtitlesSettings::IsOverrideFonts()
+{
+ return m_settings->GetBool(CSettings::SETTING_SUBTITLES_OVERRIDEFONTS);
+}
+
+OverrideStyles CSubtitlesSettings::GetOverrideStyles()
+{
+ return static_cast<OverrideStyles>(
+ m_settings->GetInt(CSettings::SETTING_SUBTITLES_OVERRIDESTYLES));
+}
+
+float CSubtitlesSettings::GetVerticalMarginPerc()
+{
+ // We return the vertical margin as percentage
+ // to fit the current screen resolution
+ return static_cast<float>(m_settings->GetNumber(CSettings::SETTING_SUBTITLES_MARGINVERTICAL));
+}
+
+void CSubtitlesSettings::SettingOptionsSubtitleFontsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ // From application system fonts folder we add the default font only
+ std::string defaultFontPath =
+ URIUtils::AddFileToFolder("special://xbmc/media/Fonts/", UTILS::FONT::FONT_DEFAULT_FILENAME);
+ if (CFileUtils::Exists(defaultFontPath))
+ {
+ std::string familyName = UTILS::FONT::GetFontFamily(defaultFontPath);
+ if (!familyName.empty())
+ {
+ list.emplace_back(g_localizeStrings.Get(571) + " " + familyName, FONT_DEFAULT_FAMILYNAME);
+ }
+ }
+ // Add additionals fonts from the user fonts folder
+ for (const std::string& familyName : g_fontManager.GetUserFontsFamilyNames())
+ {
+ list.emplace_back(familyName, familyName);
+ }
+}
diff --git a/xbmc/settings/SubtitlesSettings.h b/xbmc/settings/SubtitlesSettings.h
new file mode 100644
index 0000000..3927de1
--- /dev/null
+++ b/xbmc/settings/SubtitlesSettings.h
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2012-2021 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"
+#include "utils/ColorUtils.h"
+#include "utils/Observer.h"
+
+#include <memory>
+#include <string>
+
+class CSetting;
+class CSettings;
+struct StringSettingOption;
+
+namespace KODI
+{
+namespace SUBTITLES
+{
+// This is a placeholder to keep the fontname setting valid
+// even if the default app font could be changed
+constexpr const char* FONT_DEFAULT_FAMILYNAME = "DEFAULT";
+
+enum class Align
+{
+ MANUAL = 0,
+ BOTTOM_INSIDE,
+ BOTTOM_OUTSIDE,
+ TOP_INSIDE,
+ TOP_OUTSIDE
+};
+
+enum class HorizontalAlign
+{
+ LEFT = 0,
+ CENTER,
+ RIGHT
+};
+
+enum class BackgroundType
+{
+ NONE = 0,
+ SHADOW,
+ BOX,
+ SQUAREBOX
+};
+
+enum class FontStyle
+{
+ NORMAL = 0,
+ BOLD,
+ ITALIC,
+ BOLD_ITALIC
+};
+
+enum class OverrideStyles
+{
+ DISABLED = 0,
+ POSITIONS,
+ STYLES,
+ STYLES_POSITIONS
+};
+
+class CSubtitlesSettings : public ISettingCallback, public Observable
+{
+public:
+ explicit CSubtitlesSettings(const std::shared_ptr<CSettings>& settings);
+ ~CSubtitlesSettings() override;
+
+ // Inherited from ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ /*!
+ * \brief Get subtitle alignment
+ * \return The alignment
+ */
+ Align GetAlignment();
+
+ /*!
+ * \brief Set the subtitle alignment
+ * \param align The alignment
+ */
+ void SetAlignment(Align align);
+
+ /*!
+ * \brief Get horizontal text alignment
+ * \return The alignment
+ */
+ HorizontalAlign GetHorizontalAlignment();
+
+ /*!
+ * \brief Get font name
+ * \return The font name
+ */
+ std::string GetFontName();
+
+ /*!
+ * \brief Get font style
+ * \return The font style
+ */
+ FontStyle GetFontStyle();
+
+ /*!
+ * \brief Get font size
+ * \return The font size in PX
+ */
+ int GetFontSize();
+
+ /*!
+ * \brief Get font color
+ * \return The font color
+ */
+ UTILS::COLOR::Color GetFontColor();
+
+ /*!
+ * \brief Get font opacity
+ * \return The font opacity in %
+ */
+ int GetFontOpacity();
+
+ /*!
+ * \brief Get border size
+ * \return The border size in %
+ */
+ int GetBorderSize();
+
+ /*!
+ * \brief Get border color
+ * \return The border color
+ */
+ UTILS::COLOR::Color GetBorderColor();
+
+ /*!
+ * \brief Get shadow size
+ * \return The shadow size in %
+ */
+ int GetShadowSize();
+
+ /*!
+ * \brief Get shadow color
+ * \return The shadow color
+ */
+ UTILS::COLOR::Color GetShadowColor();
+
+ /*!
+ * \brief Get shadow opacity
+ * \return The shadow opacity in %
+ */
+ int GetShadowOpacity();
+
+ /*!
+ * \brief Get blur size
+ * \return The blur size in %
+ */
+ int GetBlurSize();
+
+ /*!
+ * \brief Get background type
+ * \return The background type
+ */
+ BackgroundType GetBackgroundType();
+
+ /*!
+ * \brief Get background color
+ * \return The background color
+ */
+ UTILS::COLOR::Color GetBackgroundColor();
+
+ /*!
+ * \brief Get background opacity
+ * \return The background opacity in %
+ */
+ int GetBackgroundOpacity();
+
+ /*!
+ * \brief Check if font override is enabled
+ * \return True if fonts must be overriden, otherwise false
+ */
+ bool IsOverrideFonts();
+
+ /*!
+ * \brief Get override styles
+ * \return The styles to be overriden
+ */
+ OverrideStyles GetOverrideStyles();
+
+ /*!
+ * \brief Get the subtitle vertical margin
+ * \return The vertical margin in %
+ */
+ float GetVerticalMarginPerc();
+
+ static void SettingOptionsSubtitleFontsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+
+private:
+ CSubtitlesSettings() = delete;
+
+ const std::shared_ptr<CSettings> m_settings;
+};
+
+} // namespace SUBTITLES
+} // namespace KODI
diff --git a/xbmc/settings/dialogs/CMakeLists.txt b/xbmc/settings/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..631e86c
--- /dev/null
+++ b/xbmc/settings/dialogs/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES GUIDialogContentSettings.cpp
+ GUIDialogLibExportSettings.cpp
+ GUIDialogSettingsBase.cpp
+ GUIDialogSettingsManagerBase.cpp
+ GUIDialogSettingsManualBase.cpp)
+
+set(HEADERS GUIDialogContentSettings.h
+ GUIDialogLibExportSettings.h
+ GUIDialogSettingsBase.h
+ GUIDialogSettingsManagerBase.h
+ GUIDialogSettingsManualBase.h)
+
+core_add_library(settings_dialogs)
diff --git a/xbmc/settings/dialogs/GUIDialogContentSettings.cpp b/xbmc/settings/dialogs/GUIDialogContentSettings.cpp
new file mode 100644
index 0000000..da4caf1
--- /dev/null
+++ b/xbmc/settings/dialogs/GUIDialogContentSettings.cpp
@@ -0,0 +1,428 @@
+/*
+ * 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 "GUIDialogContentSettings.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/AddonSystemSettings.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "filesystem/AddonsDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingsManager.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "utils/log.h"
+#include "video/VideoInfoScanner.h"
+
+#include <limits.h>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#define SETTING_CONTENT_TYPE "contenttype"
+#define SETTING_SCRAPER_LIST "scraperlist"
+#define SETTING_SCRAPER_SETTINGS "scrapersettings"
+#define SETTING_SCAN_RECURSIVE "scanrecursive"
+#define SETTING_USE_DIRECTORY_NAMES "usedirectorynames"
+#define SETTING_CONTAINS_SINGLE_ITEM "containssingleitem"
+#define SETTING_EXCLUDE "exclude"
+#define SETTING_NO_UPDATING "noupdating"
+#define SETTING_ALL_EXTERNAL_AUDIO "allexternalaudio"
+
+using namespace ADDON;
+
+
+CGUIDialogContentSettings::CGUIDialogContentSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_CONTENT_SETTINGS, "DialogSettings.xml")
+{ }
+
+void CGUIDialogContentSettings::SetContent(CONTENT_TYPE content)
+{
+ m_content = m_originalContent = content;
+}
+
+void CGUIDialogContentSettings::ResetContent()
+{
+ SetContent(CONTENT_NONE);
+}
+
+void CGUIDialogContentSettings::SetScanSettings(const VIDEO::SScanSettings &scanSettings)
+{
+ m_scanRecursive = (scanSettings.recurse > 0 && !scanSettings.parent_name) ||
+ (scanSettings.recurse > 1 && scanSettings.parent_name);
+ m_useDirectoryNames = scanSettings.parent_name;
+ m_exclude = scanSettings.exclude;
+ m_containsSingleItem = scanSettings.parent_name_root;
+ m_noUpdating = scanSettings.noupdate;
+ m_allExternalAudio = scanSettings.m_allExtAudio;
+}
+
+bool CGUIDialogContentSettings::Show(ADDON::ScraperPtr& scraper, CONTENT_TYPE content /* = CONTENT_NONE */)
+{
+ VIDEO::SScanSettings dummy;
+ return Show(scraper, dummy, content);
+}
+
+bool CGUIDialogContentSettings::Show(ADDON::ScraperPtr& scraper, VIDEO::SScanSettings& settings, CONTENT_TYPE content /* = CONTENT_NONE */)
+{
+ CGUIDialogContentSettings *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogContentSettings>(WINDOW_DIALOG_CONTENT_SETTINGS);
+ if (dialog == NULL)
+ return false;
+
+ if (scraper != NULL)
+ {
+ dialog->SetContent(content != CONTENT_NONE ? content : scraper->Content());
+ dialog->SetScraper(scraper);
+ // toast selected but disabled scrapers
+ if (CServiceBroker::GetAddonMgr().IsAddonDisabled(scraper->ID()))
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(24024), scraper->Name(), 2000, true);
+ }
+
+ dialog->SetScanSettings(settings);
+ dialog->Open();
+
+ bool confirmed = dialog->IsConfirmed();
+ if (confirmed)
+ {
+ scraper = dialog->GetScraper();
+ content = dialog->GetContent();
+
+ settings.m_allExtAudio = dialog->GetUseAllExternalAudio();
+
+ if (scraper == NULL || content == CONTENT_NONE)
+ settings.exclude = dialog->GetExclude();
+ else
+ {
+ settings.exclude = false;
+ settings.noupdate = dialog->GetNoUpdating();
+ scraper->SetPathSettings(content, "");
+
+ if (content == CONTENT_TVSHOWS)
+ {
+ settings.parent_name = settings.parent_name_root = dialog->GetContainsSingleItem();
+ settings.recurse = 0;
+ }
+ else if (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS)
+ {
+ if (dialog->GetUseDirectoryNames())
+ {
+ settings.parent_name = true;
+ settings.parent_name_root = false;
+ settings.recurse = dialog->GetScanRecursive() ? INT_MAX : 1;
+
+ if (dialog->GetContainsSingleItem())
+ {
+ settings.parent_name_root = true;
+ settings.recurse = 0;
+ }
+ }
+ else
+ {
+ settings.parent_name = false;
+ settings.parent_name_root = false;
+ settings.recurse = dialog->GetScanRecursive() ? INT_MAX : 0;
+ }
+ }
+ }
+ }
+
+ // now that we have evaluated all settings we need to reset the content
+ dialog->ResetContent();
+
+ return confirmed;
+}
+
+void CGUIDialogContentSettings::OnInitWindow()
+{
+ CGUIDialogSettingsManualBase::OnInitWindow();
+}
+
+void CGUIDialogContentSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_CONTAINS_SINGLE_ITEM)
+ m_containsSingleItem = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == SETTING_NO_UPDATING)
+ m_noUpdating = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == SETTING_USE_DIRECTORY_NAMES)
+ m_useDirectoryNames = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == SETTING_SCAN_RECURSIVE)
+ {
+ m_scanRecursive = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ GetSettingsManager()->SetBool(SETTING_CONTAINS_SINGLE_ITEM, false);
+ }
+ else if (settingId == SETTING_EXCLUDE)
+ m_exclude = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == SETTING_ALL_EXTERNAL_AUDIO)
+ m_allExternalAudio = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+}
+
+void CGUIDialogContentSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == SETTING_CONTENT_TYPE)
+ {
+ std::vector<std::pair<std::string, int>> labels;
+ if (m_content == CONTENT_ALBUMS || m_content == CONTENT_ARTISTS)
+ {
+ labels.emplace_back(ADDON::TranslateContent(m_content, true), m_content);
+ }
+ else
+ {
+ labels.emplace_back(ADDON::TranslateContent(CONTENT_NONE, true), CONTENT_NONE);
+ labels.emplace_back(ADDON::TranslateContent(CONTENT_MOVIES, true), CONTENT_MOVIES);
+ labels.emplace_back(ADDON::TranslateContent(CONTENT_TVSHOWS, true), CONTENT_TVSHOWS);
+ labels.emplace_back(ADDON::TranslateContent(CONTENT_MUSICVIDEOS, true), CONTENT_MUSICVIDEOS);
+ }
+ std::sort(labels.begin(), labels.end());
+
+ CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (dialog)
+ {
+ dialog->SetHeading(CVariant{ 20344 }); //Label "This directory contains"
+
+ int iIndex = 0;
+ int iSelected = 0;
+ for (const auto &label : labels)
+ {
+ dialog->Add(label.first);
+
+ if (m_content == label.second)
+ iSelected = iIndex;
+ iIndex++;
+ }
+
+ dialog->SetSelected(iSelected);
+
+ dialog->Open();
+ // Selected item has not changes - in case of cancel or the user selecting the same item
+ int newSelected = dialog->GetSelectedItem();
+ if (!dialog->IsConfirmed() || newSelected < 0 || newSelected == iSelected)
+ return;
+
+ auto selected = labels.at(newSelected);
+ m_content = static_cast<CONTENT_TYPE>(selected.second);
+
+ AddonPtr scraperAddon;
+ if (!CAddonSystemSettings::GetInstance().GetActive(ADDON::ScraperTypeFromContent(m_content), scraperAddon) && m_content != CONTENT_NONE)
+ return;
+
+ m_scraper = std::dynamic_pointer_cast<CScraper>(scraperAddon);
+
+ SetupView();
+ SetFocusToSetting(SETTING_CONTENT_TYPE);
+ }
+ }
+ else if (settingId == SETTING_SCRAPER_LIST)
+ {
+ ADDON::AddonType type = ADDON::ScraperTypeFromContent(m_content);
+ std::string currentScraperId;
+ if (m_scraper != nullptr)
+ currentScraperId = m_scraper->ID();
+ std::string selectedAddonId = currentScraperId;
+
+ if (CGUIWindowAddonBrowser::SelectAddonID(type, selectedAddonId, false) == 1
+ && selectedAddonId != currentScraperId)
+ {
+ AddonPtr scraperAddon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(selectedAddonId, scraperAddon,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ m_scraper = std::dynamic_pointer_cast<CScraper>(scraperAddon);
+ SetupView();
+ SetFocusToSetting(SETTING_SCRAPER_LIST);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{} - Could not get settings for addon: {}", __FUNCTION__,
+ selectedAddonId);
+ }
+ }
+ }
+ else if (settingId == SETTING_SCRAPER_SETTINGS)
+ CGUIDialogAddonSettings::ShowForAddon(m_scraper, false);
+}
+
+bool CGUIDialogContentSettings::Save()
+{
+ //Should be saved by caller of ::Show
+ return true;
+}
+
+void CGUIDialogContentSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+ SetHeading(20333);
+
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 186);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+
+ SetLabel2(SETTING_CONTENT_TYPE, ADDON::TranslateContent(m_content, true));
+
+ if (m_content == CONTENT_NONE)
+ {
+ ToggleState(SETTING_SCRAPER_LIST, false);
+ ToggleState(SETTING_SCRAPER_SETTINGS, false);
+ }
+ else
+ {
+ ToggleState(SETTING_SCRAPER_LIST, true);
+ if (m_scraper != NULL && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_scraper->ID()))
+ {
+ SetLabel2(SETTING_SCRAPER_LIST, m_scraper->Name());
+ if (m_scraper && m_scraper->Supports(m_content) && m_scraper->HasSettings())
+ ToggleState(SETTING_SCRAPER_SETTINGS, true);
+ else
+ ToggleState(SETTING_SCRAPER_SETTINGS, false);
+ }
+ else
+ {
+ SetLabel2(SETTING_SCRAPER_LIST, g_localizeStrings.Get(231)); //Set label2 to "None"
+ ToggleState(SETTING_SCRAPER_SETTINGS, false);
+ }
+ }
+}
+
+void CGUIDialogContentSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ if (m_content == CONTENT_NONE)
+ m_showScanSettings = false;
+ else if (m_scraper != NULL && !CServiceBroker::GetAddonMgr().IsAddonDisabled(m_scraper->ID()))
+ m_showScanSettings = true;
+
+ std::shared_ptr<CSettingCategory> category = AddCategory("contentsettings", -1);
+ if (category == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogContentSettings: unable to setup settings");
+ return;
+ }
+
+ std::shared_ptr<CSettingGroup> group = AddGroup(category);
+ if (group == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogContentSettings: unable to setup settings");
+ return;
+ }
+
+ AddButton(group, SETTING_CONTENT_TYPE, 20344, SettingLevel::Basic);
+ AddButton(group, SETTING_SCRAPER_LIST, 38025, SettingLevel::Basic);
+ std::shared_ptr<CSettingAction> subsetting = AddButton(group, SETTING_SCRAPER_SETTINGS, 10004, SettingLevel::Basic);
+ if (subsetting != NULL)
+ subsetting->SetParent(SETTING_SCRAPER_LIST);
+
+ std::shared_ptr<CSettingGroup> groupDetails = AddGroup(category, 20322);
+ if (groupDetails == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogContentSettings: unable to setup scanning settings");
+ return;
+ }
+ switch (m_content)
+ {
+ case CONTENT_TVSHOWS:
+ {
+ AddToggle(groupDetails, SETTING_CONTAINS_SINGLE_ITEM, 20379, SettingLevel::Basic, m_containsSingleItem, false, m_showScanSettings);
+ AddToggle(groupDetails, SETTING_NO_UPDATING, 20432, SettingLevel::Basic, m_noUpdating, false, m_showScanSettings);
+ AddToggle(groupDetails, SETTING_ALL_EXTERNAL_AUDIO, 39120, SettingLevel::Basic,
+ m_allExternalAudio, false, m_showScanSettings);
+ break;
+ }
+
+ case CONTENT_MOVIES:
+ case CONTENT_MUSICVIDEOS:
+ {
+ AddToggle(groupDetails, SETTING_USE_DIRECTORY_NAMES, m_content == CONTENT_MOVIES ? 20329 : 20330, SettingLevel::Basic, m_useDirectoryNames, false, m_showScanSettings);
+ std::shared_ptr<CSettingBool> settingScanRecursive = AddToggle(groupDetails, SETTING_SCAN_RECURSIVE, 20346, SettingLevel::Basic, m_scanRecursive, false, m_showScanSettings);
+ std::shared_ptr<CSettingBool> settingContainsSingleItem = AddToggle(groupDetails, SETTING_CONTAINS_SINGLE_ITEM, 20383, SettingLevel::Basic, m_containsSingleItem, false, m_showScanSettings);
+ AddToggle(groupDetails, SETTING_NO_UPDATING, 20432, SettingLevel::Basic, m_noUpdating, false, m_showScanSettings);
+ AddToggle(groupDetails, SETTING_ALL_EXTERNAL_AUDIO, 39120, SettingLevel::Basic,
+ m_allExternalAudio, false, m_showScanSettings);
+
+ // define an enable dependency with (m_useDirectoryNames && !m_containsSingleItem) || !m_useDirectoryNames
+ CSettingDependency dependencyScanRecursive(SettingDependencyType::Enable, GetSettingsManager());
+ dependencyScanRecursive.Or()
+ ->Add(CSettingDependencyConditionCombinationPtr((new CSettingDependencyConditionCombination(BooleanLogicOperationAnd, GetSettingsManager())) // m_useDirectoryNames && !m_containsSingleItem
+ ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_USE_DIRECTORY_NAMES, "true", SettingDependencyOperator::Equals, false, GetSettingsManager()))) // m_useDirectoryNames
+ ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_CONTAINS_SINGLE_ITEM, "false", SettingDependencyOperator::Equals, false, GetSettingsManager()))))) // !m_containsSingleItem
+ ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_USE_DIRECTORY_NAMES, "false", SettingDependencyOperator::Equals, false, GetSettingsManager()))); // !m_useDirectoryNames
+
+ // define an enable dependency with m_useDirectoryNames && !m_scanRecursive
+ CSettingDependency dependencyContainsSingleItem(SettingDependencyType::Enable, GetSettingsManager());
+ dependencyContainsSingleItem.And()
+ ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_USE_DIRECTORY_NAMES, "true", SettingDependencyOperator::Equals, false, GetSettingsManager()))) // m_useDirectoryNames
+ ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_SCAN_RECURSIVE, "false", SettingDependencyOperator::Equals, false, GetSettingsManager()))); // !m_scanRecursive
+
+ SettingDependencies deps;
+ deps.push_back(dependencyScanRecursive);
+ settingScanRecursive->SetDependencies(deps);
+
+ deps.clear();
+ deps.push_back(dependencyContainsSingleItem);
+ settingContainsSingleItem->SetDependencies(deps);
+ break;
+ }
+
+ case CONTENT_ALBUMS:
+ case CONTENT_ARTISTS:
+ break;
+
+ case CONTENT_NONE:
+ default:
+ AddToggle(groupDetails, SETTING_EXCLUDE, 20380, SettingLevel::Basic, m_exclude, false, !m_showScanSettings);
+ AddToggle(groupDetails, SETTING_ALL_EXTERNAL_AUDIO, 39120, SettingLevel::Basic,
+ m_allExternalAudio, false, !m_showScanSettings);
+ break;
+ }
+}
+
+void CGUIDialogContentSettings::SetLabel2(const std::string &settingid, const std::string &label)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ SET_CONTROL_LABEL2(settingControl->GetID(), label);
+}
+
+void CGUIDialogContentSettings::ToggleState(const std::string &settingid, bool enabled)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ {
+ if (enabled)
+ CONTROL_ENABLE(settingControl->GetID());
+ else
+ CONTROL_DISABLE(settingControl->GetID());
+ }
+}
+
+void CGUIDialogContentSettings::SetFocusToSetting(const std::string& settingid)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ SET_CONTROL_FOCUS(settingControl->GetID(), 0);
+}
diff --git a/xbmc/settings/dialogs/GUIDialogContentSettings.h b/xbmc/settings/dialogs/GUIDialogContentSettings.h
new file mode 100644
index 0000000..c0d7c81
--- /dev/null
+++ b/xbmc/settings/dialogs/GUIDialogContentSettings.h
@@ -0,0 +1,91 @@
+/*
+ * 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 "addons/Scraper.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <map>
+#include <utility>
+
+namespace VIDEO
+{
+ struct SScanSettings;
+}
+class CFileItemList;
+
+class CGUIDialogContentSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogContentSettings();
+
+ // specialization of CGUIWindow
+ bool HasListItems() const override { return true; }
+
+ CONTENT_TYPE GetContent() const { return m_content; }
+ void SetContent(CONTENT_TYPE content);
+ void ResetContent();
+
+ const ADDON::ScraperPtr& GetScraper() const { return m_scraper; }
+ void SetScraper(ADDON::ScraperPtr scraper) { m_scraper = std::move(scraper); }
+
+ void SetScanSettings(const VIDEO::SScanSettings &scanSettings);
+ bool GetScanRecursive() const { return m_scanRecursive; }
+ bool GetUseDirectoryNames() const { return m_useDirectoryNames; }
+ bool GetContainsSingleItem() const { return m_containsSingleItem; }
+ bool GetExclude() const { return m_exclude; }
+ bool GetNoUpdating() const { return m_noUpdating; }
+ bool GetUseAllExternalAudio() const { return m_allExternalAudio; }
+
+ static bool Show(ADDON::ScraperPtr& scraper, CONTENT_TYPE content = CONTENT_NONE);
+ static bool Show(ADDON::ScraperPtr& scraper, VIDEO::SScanSettings& settings, CONTENT_TYPE content = CONTENT_NONE);
+
+protected:
+ // specializations of CGUIWindow
+ void OnInitWindow() override;
+
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+private:
+ void SetLabel2(const std::string &settingid, const std::string &label);
+ void ToggleState(const std::string &settingid, bool enabled);
+ using CGUIDialogSettingsManualBase::SetFocus;
+ void SetFocusToSetting(const std::string& settingid);
+
+ /*!
+ * @brief The currently selected content type
+ */
+ CONTENT_TYPE m_content = CONTENT_NONE;
+ /*!
+ * @brief The selected content type at dialog creation
+ */
+ CONTENT_TYPE m_originalContent = CONTENT_NONE;
+ /*!
+ * @brief The currently selected scraper
+ */
+ ADDON::ScraperPtr m_scraper;
+
+ bool m_showScanSettings = false;
+ bool m_scanRecursive = false;
+ bool m_useDirectoryNames = false;
+ bool m_containsSingleItem = false;
+ bool m_exclude = false;
+ bool m_noUpdating = false;
+ bool m_allExternalAudio = false;
+};
diff --git a/xbmc/settings/dialogs/GUIDialogLibExportSettings.cpp b/xbmc/settings/dialogs/GUIDialogLibExportSettings.cpp
new file mode 100644
index 0000000..7e88553
--- /dev/null
+++ b/xbmc/settings/dialogs/GUIDialogLibExportSettings.cpp
@@ -0,0 +1,452 @@
+/*
+ * 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 "GUIDialogLibExportSettings.h"
+
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "settings/SettingUtils.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "storage/MediaManager.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <limits.h>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+CGUIDialogLibExportSettings::CGUIDialogLibExportSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_LIBEXPORT_SETTINGS, "DialogSettings.xml")
+{ }
+
+bool CGUIDialogLibExportSettings::Show(CLibExportSettings& settings)
+{
+ CGUIDialogLibExportSettings *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogLibExportSettings>(WINDOW_DIALOG_LIBEXPORT_SETTINGS);
+ if (!dialog)
+ return false;
+
+ // Get current export settings from service broker
+ const std::shared_ptr<CSettings> pSettings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ dialog->m_settings.SetExportType(pSettings->GetInt(CSettings::SETTING_MUSICLIBRARY_EXPORT_FILETYPE));
+ dialog->m_settings.m_strPath = pSettings->GetString(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER);
+ dialog->m_settings.SetItemsToExport(pSettings->GetInt(CSettings::SETTING_MUSICLIBRARY_EXPORT_ITEMS));
+ dialog->m_settings.m_unscraped = pSettings->GetBool(CSettings::SETTING_MUSICLIBRARY_EXPORT_UNSCRAPED);
+ dialog->m_settings.m_artwork = pSettings->GetBool(CSettings::SETTING_MUSICLIBRARY_EXPORT_ARTWORK);
+ dialog->m_settings.m_skipnfo = pSettings->GetBool(CSettings::SETTING_MUSICLIBRARY_EXPORT_SKIPNFO);
+ dialog->m_settings.m_overwrite = pSettings->GetBool(CSettings::SETTING_MUSICLIBRARY_EXPORT_OVERWRITE);
+
+ // Ensure NFO or art output enabled when albums exported (adjust old saved settings)
+ if (dialog->m_settings.IsItemExported(ELIBEXPORT_ALBUMS) && (dialog->m_settings.m_skipnfo && !dialog->m_settings.m_artwork))
+ dialog->m_settings.m_skipnfo = false;
+
+ dialog->m_destinationChecked = false;
+ dialog->Open();
+
+ bool confirmed = dialog->IsConfirmed();
+ if (confirmed)
+ {
+ // Return the new settings (saved by service broker but avoids re-reading)
+ settings = dialog->m_settings;
+ }
+ return confirmed;
+}
+
+void CGUIDialogLibExportSettings::OnInitWindow()
+{
+ CGUIDialogSettingsManualBase::OnInitWindow();
+}
+
+void CGUIDialogLibExportSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (!setting)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == CSettings::SETTING_MUSICLIBRARY_EXPORT_FILETYPE)
+ {
+ m_settings.SetExportType(std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ SetupView();
+ SetFocus(CSettings::SETTING_MUSICLIBRARY_EXPORT_FILETYPE);
+ }
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER)
+ {
+ m_settings.m_strPath = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ UpdateButtons();
+ }
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_EXPORT_OVERWRITE)
+ m_settings.m_overwrite = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_EXPORT_ITEMS)
+ {
+ m_settings.SetItemsToExport(GetExportItemsFromSetting(setting));
+ if (m_settings.IsItemExported(ELIBEXPORT_ALBUMS) && (m_settings.m_skipnfo && !m_settings.m_artwork))
+ {
+ m_settings.m_skipnfo = false;
+ m_settingNFO->SetValue(true);
+ UpdateToggles();
+ }
+ UpdateDescription();
+ }
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_EXPORT_ARTWORK)
+ {
+ m_settings.m_artwork = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ if (m_settings.IsItemExported(ELIBEXPORT_ALBUMS) && (m_settings.m_skipnfo && !m_settings.m_artwork))
+ {
+ m_settings.m_skipnfo = false;
+ m_settingNFO->SetValue(true);
+ }
+ UpdateToggles();
+ }
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_EXPORT_UNSCRAPED)
+ m_settings.m_unscraped = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == CSettings::SETTING_MUSICLIBRARY_EXPORT_SKIPNFO)
+ {
+ m_settings.m_skipnfo = !std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ if (m_settings.IsItemExported(ELIBEXPORT_ALBUMS) && (m_settings.m_skipnfo && !m_settings.m_artwork))
+ {
+ m_settings.m_artwork = true;
+ m_settingArt->SetValue(true);
+ }
+ UpdateToggles();
+ }
+}
+
+void CGUIDialogLibExportSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string &settingId = setting->GetId();
+
+ if (settingId == CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER && !m_settings.IsToLibFolders() &&
+ !m_settings.IsArtistFoldersOnly())
+ {
+ VECSOURCES shares;
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ CServiceBroker::GetMediaManager().GetNetworkLocations(shares);
+ CServiceBroker::GetMediaManager().GetRemovableDrives(shares);
+ std::string strDirectory = m_settings.m_strPath;
+ if (!strDirectory.empty())
+ {
+ URIUtils::AddSlashAtEnd(strDirectory);
+ bool bIsSource;
+ if (CUtil::GetMatchingSource(strDirectory, shares, bIsSource) < 0) // path is outside shares - add it as a separate one
+ {
+ CMediaSource share;
+ share.strName = g_localizeStrings.Get(13278);
+ share.strPath = strDirectory;
+ shares.push_back(share);
+ }
+ }
+ else
+ strDirectory = "default location";
+
+ if (CGUIDialogFileBrowser::ShowAndGetDirectory(shares, g_localizeStrings.Get(661), strDirectory, true))
+ {
+ if (!strDirectory.empty())
+ {
+ m_destinationChecked = true;
+ m_settings.m_strPath = strDirectory;
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER, strDirectory);
+ SetFocus(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER);
+ }
+ }
+ UpdateButtons();
+ }
+}
+
+bool CGUIDialogLibExportSettings::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_SETTINGS_OKAY_BUTTON)
+ {
+ OnOK();
+ return true;
+ }
+ }
+ break;
+ }
+ return CGUIDialogSettingsManualBase::OnMessage(message);
+}
+
+void CGUIDialogLibExportSettings::OnOK()
+{
+ // Validate destination folder
+ if (m_settings.IsToLibFolders() || m_settings.IsArtistFoldersOnly())
+ {
+ // Check artist info folder setting
+ std::string path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ if (path.empty())
+ {
+ //"Unable to export to library folders as the system artist information folder setting is empty"
+ //Settings (YES) button takes user to enter the artist info folder setting
+ if (HELPERS::ShowYesNoDialogText(20223, 38317, 186, 10004) == DialogResponse::CHOICE_YES)
+ {
+ m_confirmed = false;
+ Close();
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SETTINGS_MEDIA, CSettings::SETTING_MUSICLIBRARY_ARTISTSFOLDER);
+ }
+ return;
+ }
+ }
+ else if (!m_destinationChecked)
+ {
+ // ELIBEXPORT_SINGLEFILE or LIBEXPORT_SEPARATEFILES
+ // Check that destination folder exists
+ if (!XFILE::CDirectory::Exists(m_settings.m_strPath))
+ {
+ HELPERS::ShowOKDialogText(CVariant{ 38300 }, CVariant{ 38318 });
+ return;
+ }
+ }
+ m_confirmed = true;
+ Save();
+ Close();
+}
+
+bool CGUIDialogLibExportSettings::Save()
+{
+ CLog::Log(LOGINFO, "CGUIDialogMusicExportSettings: Save() called");
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetInt(CSettings::SETTING_MUSICLIBRARY_EXPORT_FILETYPE, m_settings.GetExportType());
+ settings->SetString(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER, m_settings.m_strPath);
+ settings->SetInt(CSettings::SETTING_MUSICLIBRARY_EXPORT_ITEMS, m_settings.GetItemsToExport());
+ settings->SetBool(CSettings::SETTING_MUSICLIBRARY_EXPORT_UNSCRAPED, m_settings.m_unscraped);
+ settings->SetBool(CSettings::SETTING_MUSICLIBRARY_EXPORT_OVERWRITE, m_settings.m_overwrite);
+ settings->SetBool(CSettings::SETTING_MUSICLIBRARY_EXPORT_ARTWORK, m_settings.m_artwork);
+ settings->SetBool(CSettings::SETTING_MUSICLIBRARY_EXPORT_SKIPNFO, m_settings.m_skipnfo);
+ settings->Save();
+
+ return true;
+}
+
+void CGUIDialogLibExportSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+ SetHeading(38300);
+
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_OKAY_BUTTON, 38319);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 222);
+
+ UpdateButtons();
+ UpdateToggles();
+ UpdateDescription();
+}
+
+void CGUIDialogLibExportSettings::UpdateButtons()
+{
+ // Enable Export button when destination folder has a path (but may not exist)
+ bool enableExport(true);
+ if (m_settings.IsSingleFile() ||
+ m_settings.IsSeparateFiles())
+ enableExport = !m_settings.m_strPath.empty();
+
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_SETTINGS_OKAY_BUTTON, enableExport);
+ if (!enableExport)
+ SetFocus(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER);
+}
+
+void CGUIDialogLibExportSettings::UpdateToggles()
+{
+ if (m_settings.IsSeparateFiles())
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_EXPORT_UNSCRAPED, !m_settings.m_skipnfo);
+
+ if (!m_settings.IsItemExported(ELIBEXPORT_ALBUMS) && m_settings.m_skipnfo && !m_settings.m_artwork)
+ {
+ //"Output information to NFO files (currently exporting artist folders only)"
+ SetLabel(CSettings::SETTING_MUSICLIBRARY_EXPORT_SKIPNFO, g_localizeStrings.Get(38310));
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_EXPORT_OVERWRITE, false);
+ }
+ else
+ {
+ //"Output information to NFO files"
+ SetLabel(CSettings::SETTING_MUSICLIBRARY_EXPORT_SKIPNFO, g_localizeStrings.Get(38309));
+ ToggleState(CSettings::SETTING_MUSICLIBRARY_EXPORT_OVERWRITE, true);
+ }
+}
+
+void CGUIDialogLibExportSettings::UpdateDescription()
+{
+ if (m_settings.IsToLibFolders())
+ {
+ // Destination button is description of what to library means
+ SetLabel(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER, "");
+ if (m_settings.IsItemExported(ELIBEXPORT_ALBUMS))
+ if (m_settings.IsArtists())
+ //"Artists exported to Artist Information Folder and albums to music folders"
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER, g_localizeStrings.Get(38322));
+ else
+ //"Albums exported to music folders"
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER, g_localizeStrings.Get(38323));
+ else
+ // "Artists exported to Artist Information Folder"
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER, g_localizeStrings.Get(38324));
+ }
+ else if (m_settings.IsArtistFoldersOnly())
+ {
+ // Destination button is description of what artist folders means
+ SetLabel(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER, "");
+ //"Artists folders created in Artist Information Folder"
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER, g_localizeStrings.Get(38325));
+ }
+ else
+ {
+ SetLabel2(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER, m_settings.m_strPath);
+ SetLabel(CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER, g_localizeStrings.Get(38305));
+ }
+}
+
+void CGUIDialogLibExportSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ std::shared_ptr<CSettingCategory> category = AddCategory("exportsettings", -1);
+ if (!category)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogLibExportSettings: unable to setup settings");
+ return;
+ }
+
+ std::shared_ptr<CSettingGroup> groupDetails = AddGroup(category);
+ if (!groupDetails)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogLibExportSettings: unable to setup settings");
+ return;
+ }
+
+ TranslatableIntegerSettingOptions entries;
+
+ entries.push_back(TranslatableIntegerSettingOption(38301, ELIBEXPORT_SINGLEFILE));
+ entries.push_back(TranslatableIntegerSettingOption(38303, ELIBEXPORT_TOLIBRARYFOLDER));
+ entries.push_back(TranslatableIntegerSettingOption(38302, ELIBEXPORT_SEPARATEFILES));
+ entries.push_back(TranslatableIntegerSettingOption(38321, ELIBEXPORT_ARTISTFOLDERS));
+ AddList(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_FILETYPE, 38304, SettingLevel::Basic, m_settings.GetExportType(), entries, 38304); // "Choose kind of export output"
+ AddButton(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_FOLDER, 38305, SettingLevel::Basic);
+
+ entries.clear();
+ if (!m_settings.IsArtistFoldersOnly())
+ entries.push_back(TranslatableIntegerSettingOption(132, ELIBEXPORT_ALBUMS)); //ablums
+ if (m_settings.IsSingleFile())
+ entries.push_back(TranslatableIntegerSettingOption(134, ELIBEXPORT_SONGS)); //songs
+ entries.push_back(
+ TranslatableIntegerSettingOption(38043, ELIBEXPORT_ALBUMARTISTS)); //album artists
+ entries.push_back(TranslatableIntegerSettingOption(38312, ELIBEXPORT_SONGARTISTS)); //song artists
+ entries.push_back(
+ TranslatableIntegerSettingOption(38313, ELIBEXPORT_OTHERARTISTS)); //other artists
+
+ std::vector<int> items;
+ if (m_settings.IsArtistFoldersOnly())
+ {
+ // Only artists, not albums, at least album artists
+ items = m_settings.GetLimitedItems(ELIBEXPORT_ALBUMARTISTS + ELIBEXPORT_SONGARTISTS + ELIBEXPORT_OTHERARTISTS);
+ if (items.size() == 0)
+ items.emplace_back(ELIBEXPORT_ALBUMARTISTS);
+ }
+ else if (!m_settings.IsSingleFile())
+ {
+ // No songs unless single file export, at least album artists
+ items = m_settings.GetLimitedItems(ELIBEXPORT_ALBUMS + ELIBEXPORT_ALBUMARTISTS + ELIBEXPORT_SONGARTISTS + ELIBEXPORT_OTHERARTISTS);
+ if (items.size() == 0)
+ items.emplace_back(ELIBEXPORT_ALBUMARTISTS);
+ }
+ else
+ items = m_settings.GetExportItems();
+
+ AddList(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_ITEMS, 38306, SettingLevel::Basic, items, entries, 133, 1);
+
+ if (m_settings.IsToLibFolders() || m_settings.IsSeparateFiles())
+ {
+ m_settingNFO = AddToggle(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_SKIPNFO, 38309, SettingLevel::Basic, !m_settings.m_skipnfo);
+ if (m_settings.IsSeparateFiles())
+ AddToggle(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_UNSCRAPED, 38308, SettingLevel::Basic, m_settings.m_unscraped);
+ m_settingArt = AddToggle(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_ARTWORK, 38307, SettingLevel::Basic, m_settings.m_artwork);
+ AddToggle(groupDetails, CSettings::SETTING_MUSICLIBRARY_EXPORT_OVERWRITE, 38311, SettingLevel::Basic, m_settings.m_overwrite);
+ }
+}
+
+void CGUIDialogLibExportSettings::SetLabel2(const std::string &settingid, const std::string &label)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != nullptr && settingControl->GetControl() != nullptr)
+ SET_CONTROL_LABEL2(settingControl->GetID(), label);
+}
+
+void CGUIDialogLibExportSettings::SetLabel(const std::string &settingid, const std::string &label)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != nullptr && settingControl->GetControl() != nullptr)
+ SetControlLabel(settingControl->GetID(), label);
+}
+
+void CGUIDialogLibExportSettings::ToggleState(const std::string & settingid, bool enabled)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != nullptr && settingControl->GetControl() != nullptr)
+ {
+ if (enabled)
+ CONTROL_ENABLE(settingControl->GetID());
+ else
+ CONTROL_DISABLE(settingControl->GetID());
+ }
+}
+
+void CGUIDialogLibExportSettings::SetFocus(const std::string &settingid)
+{
+ BaseSettingControlPtr settingControl = GetSettingControl(settingid);
+ if (settingControl != NULL && settingControl->GetControl() != NULL)
+ SET_CONTROL_FOCUS(settingControl->GetID(), 0);
+}
+
+int CGUIDialogLibExportSettings::GetExportItemsFromSetting(const SettingConstPtr& setting)
+{
+ std::shared_ptr<const CSettingList> settingList = std::static_pointer_cast<const CSettingList>(setting);
+ if (settingList->GetElementType() != SettingType::Integer)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogLibExportSettings::{} - wrong items element type", __FUNCTION__);
+ return 0;
+ }
+ int exportitems = 0;
+ std::vector<CVariant> list = CSettingUtils::GetList(settingList);
+ for (const auto &value : list)
+ {
+ if (!value.isInteger())
+ {
+ CLog::Log(LOGERROR, "CGUIDialogLibExportSettings::{} - wrong items value type", __FUNCTION__);
+ return 0;
+ }
+ exportitems += static_cast<int>(value.asInteger());
+ }
+ return exportitems;
+}
diff --git a/xbmc/settings/dialogs/GUIDialogLibExportSettings.h b/xbmc/settings/dialogs/GUIDialogLibExportSettings.h
new file mode 100644
index 0000000..4c861ba
--- /dev/null
+++ b/xbmc/settings/dialogs/GUIDialogLibExportSettings.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017-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/LibExportSettings.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <map>
+
+class CGUIDialogLibExportSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogLibExportSettings();
+
+ // specialization of CGUIWindow
+ bool HasListItems() const override { return true; }
+ static bool Show(CLibExportSettings& settings);
+
+protected:
+ // specializations of CGUIWindow
+ void OnInitWindow() override;
+
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool OnMessage(CGUIMessage& message) override;
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+ void OnOK();
+ void UpdateButtons();
+
+private:
+ void SetLabel2(const std::string &settingid, const std::string &label);
+ void SetLabel(const std::string &settingid, const std::string &label);
+ void ToggleState(const std::string &settingid, bool enabled);
+
+ using CGUIDialogSettingsManualBase::SetFocus;
+ void SetFocus(const std::string &settingid);
+ static int GetExportItemsFromSetting(const SettingConstPtr& setting);
+ void UpdateToggles();
+ void UpdateDescription();
+
+ CLibExportSettings m_settings;
+ bool m_destinationChecked = false;
+ std::shared_ptr<CSettingBool> m_settingNFO;
+ std::shared_ptr<CSettingBool> m_settingArt;
+};
diff --git a/xbmc/settings/dialogs/GUIDialogSettingsBase.cpp b/xbmc/settings/dialogs/GUIDialogSettingsBase.cpp
new file mode 100644
index 0000000..32d3958
--- /dev/null
+++ b/xbmc/settings/dialogs/GUIDialogSettingsBase.cpp
@@ -0,0 +1,972 @@
+/*
+ * Copyright (C) 2014-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 "GUIDialogSettingsBase.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIColorButtonControl.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIControlGroupList.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIImage.h"
+#include "guilib/GUILabelControl.h"
+#include "guilib/GUIRadioButtonControl.h"
+#include "guilib/GUISettingsSliderControl.h"
+#include "guilib/GUISpinControlEx.h"
+#include "guilib/GUIToggleButtonControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "settings/SettingControl.h"
+#include "settings/lib/SettingSection.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <set>
+#include <string>
+#include <vector>
+
+#if defined(TARGET_WINDOWS) // disable 4355: 'this' used in base member initializer list
+#pragma warning(push)
+#pragma warning(disable : 4355)
+#endif // defined(TARGET_WINDOWS)
+
+#define CATEGORY_GROUP_ID 3
+#define SETTINGS_GROUP_ID 5
+
+#define CONTROL_DEFAULT_BUTTON 7
+#define CONTROL_DEFAULT_RADIOBUTTON 8
+#define CONTROL_DEFAULT_SPIN 9
+#define CONTROL_DEFAULT_CATEGORY_BUTTON 10
+#define CONTROL_DEFAULT_SEPARATOR 11
+#define CONTROL_DEFAULT_EDIT 12
+#define CONTROL_DEFAULT_SLIDER 13
+#define CONTROL_DEFAULT_SETTING_LABEL 14
+#define CONTROL_DEFAULT_COLORBUTTON 15
+
+CGUIDialogSettingsBase::CGUIDialogSettingsBase(int windowId, const std::string& xmlFile)
+ : CGUIDialog(windowId, xmlFile),
+ m_iSetting(0),
+ m_iCategory(0),
+ m_resetSetting(NULL),
+ m_dummyCategory(NULL),
+ m_pOriginalSpin(NULL),
+ m_pOriginalSlider(NULL),
+ m_pOriginalRadioButton(NULL),
+ m_pOriginalColorButton(nullptr),
+ m_pOriginalCategoryButton(NULL),
+ m_pOriginalButton(NULL),
+ m_pOriginalEdit(NULL),
+ m_pOriginalImage(NULL),
+ m_pOriginalGroupTitle(NULL),
+ m_newOriginalEdit(false),
+ m_delayedTimer(this),
+ m_confirmed(false),
+ m_focusedControl(0),
+ m_fadedControl(0)
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogSettingsBase::~CGUIDialogSettingsBase()
+{
+ FreeControls();
+ DeleteControls();
+}
+
+bool CGUIDialogSettingsBase::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_delayedSetting.reset();
+ if (message.GetParam1() != WINDOW_INVALID)
+ { // coming to this window first time (ie not returning back from some other window)
+ // so we reset our section and control states
+ m_iCategory = 0;
+ ResetControlStates();
+ }
+
+ if (AllowResettingSettings())
+ {
+ m_resetSetting = std::make_shared<CSettingAction>(SETTINGS_RESET_SETTING_ID);
+ m_resetSetting->SetLabel(10041);
+ m_resetSetting->SetHelp(10045);
+ m_resetSetting->SetControl(CreateControl("button"));
+ }
+
+ m_dummyCategory = std::make_shared<CSettingCategory>(SETTINGS_EMPTY_CATEGORY_ID);
+ m_dummyCategory->SetLabel(10046);
+ m_dummyCategory->SetHelp(10047);
+ break;
+ }
+
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ // cancel any delayed changes
+ if (m_delayedSetting != NULL)
+ {
+ m_delayedTimer.Stop();
+ CGUIMessage message(GUI_MSG_UPDATE_ITEM, GetID(), m_delayedSetting->GetID());
+ OnMessage(message);
+ }
+
+ CGUIDialog::OnMessage(message);
+ FreeControls();
+ return true;
+ }
+
+ case GUI_MSG_FOCUSED:
+ {
+ CGUIDialog::OnMessage(message);
+ m_focusedControl = GetFocusedControlID();
+
+ // cancel any delayed changes
+ if (m_delayedSetting != NULL && m_delayedSetting->GetID() != m_focusedControl)
+ {
+ m_delayedTimer.Stop();
+ // param1 = 1 for "reset the control if it's invalid"
+ CGUIMessage message(GUI_MSG_UPDATE_ITEM, GetID(), m_delayedSetting->GetID(), 1);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message, GetID());
+ }
+ // update the value of the previous setting (in case it was invalid)
+ else if (m_iSetting >= CONTROL_SETTINGS_START_CONTROL &&
+ m_iSetting < (int)(CONTROL_SETTINGS_START_CONTROL + m_settingControls.size()))
+ {
+ BaseSettingControlPtr control = GetSettingControl(m_iSetting);
+ if (control != NULL && control->GetSetting() != NULL && !control->IsValid())
+ {
+ // param1 = 1 for "reset the control if it's invalid"
+ // param2 = 1 for "only update the current value"
+ CGUIMessage message(GUI_MSG_UPDATE_ITEM, GetID(), m_iSetting, 1, 1);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message, GetID());
+ }
+ }
+
+ CVariant description;
+
+ // check if we have changed the category and need to create new setting controls
+ if (m_focusedControl >= CONTROL_SETTINGS_START_BUTTONS &&
+ m_focusedControl < (int)(CONTROL_SETTINGS_START_BUTTONS + m_categories.size()))
+ {
+ int categoryIndex = m_focusedControl - CONTROL_SETTINGS_START_BUTTONS;
+ SettingCategoryPtr category = m_categories.at(categoryIndex);
+ if (categoryIndex != m_iCategory)
+ {
+ if (!category->CanAccess())
+ {
+ // unable to go to this category - focus the previous one
+ SET_CONTROL_FOCUS(CONTROL_SETTINGS_START_BUTTONS + m_iCategory, 0);
+ return false;
+ }
+
+ m_iCategory = categoryIndex;
+ CreateSettings();
+ }
+
+ description = category->GetHelp();
+ }
+ else if (m_focusedControl >= CONTROL_SETTINGS_START_CONTROL &&
+ m_focusedControl < (int)(CONTROL_SETTINGS_START_CONTROL + m_settingControls.size()))
+ {
+ m_iSetting = m_focusedControl;
+ std::shared_ptr<CSetting> setting = GetSettingControl(m_focusedControl)->GetSetting();
+ if (setting != NULL)
+ description = setting->GetHelp();
+ }
+
+ // set the description of the currently focused category/setting
+ if (description.isInteger() || (description.isString() && !description.empty()))
+ SetDescription(description);
+
+ return true;
+ }
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_SETTINGS_OKAY_BUTTON)
+ {
+ if (OnOkay())
+ {
+ Close();
+ return true;
+ }
+
+ return false;
+ }
+
+ if (iControl == CONTROL_SETTINGS_CANCEL_BUTTON)
+ {
+ OnCancel();
+ Close();
+ return true;
+ }
+
+ BaseSettingControlPtr control = GetSettingControl(iControl);
+ if (control != NULL)
+ OnClick(control);
+
+ break;
+ }
+
+ case GUI_MSG_UPDATE_ITEM:
+ {
+ if (m_delayedSetting != NULL && m_delayedSetting->GetID() == message.GetControlId())
+ {
+ // first get the delayed setting and reset its member variable
+ // to avoid handling the delayed setting twice in case the OnClick()
+ // performed later causes the window to be deinitialized (e.g. when
+ // changing the language)
+ BaseSettingControlPtr delayedSetting = m_delayedSetting;
+ m_delayedSetting.reset();
+
+ // if updating the setting fails and param1 has been specifically set
+ // we need to call OnSettingChanged() to restore a valid value in the
+ // setting control
+ if (!delayedSetting->OnClick() && message.GetParam1() != 0)
+ OnSettingChanged(delayedSetting->GetSetting());
+ return true;
+ }
+
+ if (message.GetControlId() >= CONTROL_SETTINGS_START_CONTROL &&
+ message.GetControlId() < (int)(CONTROL_SETTINGS_START_CONTROL + m_settingControls.size()))
+ {
+ BaseSettingControlPtr settingControl = GetSettingControl(message.GetControlId());
+ if (settingControl.get() != NULL && settingControl->GetSetting() != NULL)
+ {
+ settingControl->UpdateFromSetting(message.GetParam2() != 0);
+ return true;
+ }
+ }
+ break;
+ }
+
+ case GUI_MSG_UPDATE:
+ {
+ if (IsActive() && HasID(message.GetSenderId()))
+ {
+ int focusedControl = GetFocusedControlID();
+ CreateSettings();
+ SET_CONTROL_FOCUS(focusedControl, 0);
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogSettingsBase::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_SETTINGS_RESET:
+ {
+ OnResetSettings();
+ return true;
+ }
+
+ case ACTION_DELETE_ITEM:
+ {
+ if (m_iSetting >= CONTROL_SETTINGS_START_CONTROL &&
+ m_iSetting < (int)(CONTROL_SETTINGS_START_CONTROL + m_settingControls.size()))
+ {
+ auto settingControl = GetSettingControl(m_iSetting);
+ if (settingControl != nullptr)
+ {
+ std::shared_ptr<CSetting> setting = settingControl->GetSetting();
+ if (setting != nullptr)
+ {
+ setting->Reset();
+ return true;
+ }
+ }
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return CGUIDialog::OnAction(action);
+}
+
+bool CGUIDialogSettingsBase::OnBack(int actionID)
+{
+ m_lastControlID = 0; // don't save the control as we go to a different window each time
+
+ // if the setting dialog is not a window but a dialog we need to close differently
+ if (!IsDialog())
+ return CGUIWindow::OnBack(actionID);
+
+ return CGUIDialog::OnBack(actionID);
+}
+
+void CGUIDialogSettingsBase::DoProcess(unsigned int currentTime, CDirtyRegionList& dirtyregions)
+{
+ // update alpha status of current button
+ CGUIControl* control = GetFirstFocusableControl(CONTROL_SETTINGS_START_BUTTONS + m_iCategory);
+ if (control)
+ {
+ if (m_fadedControl &&
+ (m_fadedControl != control->GetID() || m_fadedControl == m_focusedControl))
+ {
+ if (control->GetControlType() == CGUIControl::GUICONTROL_BUTTON)
+ static_cast<CGUIButtonControl*>(control)->SetAlpha(0xFF);
+ else
+ static_cast<CGUIButtonControl*>(control)->SetSelected(false);
+ m_fadedControl = 0;
+ }
+
+ if (!control->HasFocus())
+ {
+ m_fadedControl = control->GetID();
+ if (control->GetControlType() == CGUIControl::GUICONTROL_BUTTON)
+ static_cast<CGUIButtonControl*>(control)->SetAlpha(0x80);
+ else if (control->GetControlType() == CGUIControl::GUICONTROL_TOGGLEBUTTON)
+ static_cast<CGUIButtonControl*>(control)->SetSelected(true);
+ else
+ m_fadedControl = 0;
+
+ if (m_fadedControl)
+ control->SetFocus(true);
+ }
+ }
+ CGUIDialog::DoProcess(currentTime, dirtyregions);
+}
+
+void CGUIDialogSettingsBase::OnInitWindow()
+{
+ m_confirmed = false;
+ SetupView();
+ CGUIDialog::OnInitWindow();
+}
+
+void CGUIDialogSettingsBase::SetupControls(bool createSettings /* = true */)
+{
+ // cleanup first, if necessary
+ FreeControls();
+
+ // get all controls
+ m_pOriginalSpin = dynamic_cast<CGUISpinControlEx*>(GetControl(CONTROL_DEFAULT_SPIN));
+ m_pOriginalSlider = dynamic_cast<CGUISettingsSliderControl*>(GetControl(CONTROL_DEFAULT_SLIDER));
+ m_pOriginalRadioButton =
+ dynamic_cast<CGUIRadioButtonControl*>(GetControl(CONTROL_DEFAULT_RADIOBUTTON));
+ m_pOriginalCategoryButton =
+ dynamic_cast<CGUIButtonControl*>(GetControl(CONTROL_DEFAULT_CATEGORY_BUTTON));
+ m_pOriginalButton = dynamic_cast<CGUIButtonControl*>(GetControl(CONTROL_DEFAULT_BUTTON));
+ m_pOriginalImage = dynamic_cast<CGUIImage*>(GetControl(CONTROL_DEFAULT_SEPARATOR));
+ m_pOriginalEdit = dynamic_cast<CGUIEditControl*>(GetControl(CONTROL_DEFAULT_EDIT));
+ m_pOriginalGroupTitle =
+ dynamic_cast<CGUILabelControl*>(GetControl(CONTROL_DEFAULT_SETTING_LABEL));
+ m_pOriginalColorButton =
+ dynamic_cast<CGUIColorButtonControl*>(GetControl(CONTROL_DEFAULT_COLORBUTTON));
+
+ // if there's no edit control but there's a button control use that instead
+ if (m_pOriginalEdit == nullptr && m_pOriginalButton != nullptr)
+ {
+ m_pOriginalEdit = new CGUIEditControl(*m_pOriginalButton);
+ m_newOriginalEdit = true;
+ }
+
+ // hide all default controls by default
+ if (m_pOriginalSpin != nullptr)
+ m_pOriginalSpin->SetVisible(false);
+ if (m_pOriginalSlider != nullptr)
+ m_pOriginalSlider->SetVisible(false);
+ if (m_pOriginalRadioButton != nullptr)
+ m_pOriginalRadioButton->SetVisible(false);
+ if (m_pOriginalButton != nullptr)
+ m_pOriginalButton->SetVisible(false);
+ if (m_pOriginalCategoryButton != nullptr)
+ m_pOriginalCategoryButton->SetVisible(false);
+ if (m_pOriginalEdit != nullptr)
+ m_pOriginalEdit->SetVisible(false);
+ if (m_pOriginalImage != nullptr)
+ m_pOriginalImage->SetVisible(false);
+ if (m_pOriginalGroupTitle != nullptr)
+ m_pOriginalGroupTitle->SetVisible(false);
+ if (m_pOriginalColorButton != nullptr)
+ m_pOriginalColorButton->SetVisible(false);
+
+ // get the section
+ SettingSectionPtr section = GetSection();
+ if (section == NULL)
+ return;
+
+ // update the screen string
+ if (section->GetLabel() >= 0)
+ SetHeading(section->GetLabel());
+
+ // get the categories we need
+ m_categories = section->GetCategories((SettingLevel)GetSettingLevel());
+ if (m_categories.empty())
+ m_categories.push_back(m_dummyCategory);
+
+ if (m_pOriginalCategoryButton != NULL)
+ {
+ // setup our control groups...
+ CGUIControlGroupList* group =
+ dynamic_cast<CGUIControlGroupList*>(GetControl(CATEGORY_GROUP_ID));
+ if (!group)
+ return;
+
+ // go through the categories and create the necessary buttons
+ int buttonIdOffset = 0;
+ for (SettingCategoryList::const_iterator category = m_categories.begin();
+ category != m_categories.end(); ++category)
+ {
+ CGUIButtonControl* pButton = NULL;
+ if (m_pOriginalCategoryButton->GetControlType() == CGUIControl::GUICONTROL_TOGGLEBUTTON)
+ pButton = new CGUIToggleButtonControl(
+ *static_cast<CGUIToggleButtonControl*>(m_pOriginalCategoryButton));
+ else if (m_pOriginalCategoryButton->GetControlType() == CGUIControl::GUICONTROL_COLORBUTTON)
+ pButton = new CGUIColorButtonControl(
+ *static_cast<CGUIColorButtonControl*>(m_pOriginalCategoryButton));
+ else
+ pButton = new CGUIButtonControl(*m_pOriginalCategoryButton);
+ pButton->SetLabel(GetSettingsLabel(*category));
+ pButton->SetID(CONTROL_SETTINGS_START_BUTTONS + buttonIdOffset);
+ pButton->SetVisible(true);
+ pButton->AllocResources();
+
+ group->AddControl(pButton);
+ buttonIdOffset++;
+ }
+ }
+
+ if (createSettings)
+ CreateSettings();
+
+ // set focus correctly depending on whether there are categories visible or not
+ if (m_pOriginalCategoryButton == NULL &&
+ (m_defaultControl <= 0 || m_defaultControl == CATEGORY_GROUP_ID))
+ m_defaultControl = SETTINGS_GROUP_ID;
+ else if (m_pOriginalCategoryButton != NULL && m_defaultControl <= 0)
+ m_defaultControl = CATEGORY_GROUP_ID;
+}
+
+void CGUIDialogSettingsBase::FreeControls()
+{
+ // clear the category group
+ CGUIControlGroupList* control =
+ dynamic_cast<CGUIControlGroupList*>(GetControl(CATEGORY_GROUP_ID));
+ if (control)
+ {
+ control->FreeResources();
+ control->ClearAll();
+ }
+ m_categories.clear();
+ FreeSettingsControls();
+}
+
+void CGUIDialogSettingsBase::DeleteControls()
+{
+ if (m_newOriginalEdit)
+ {
+ delete m_pOriginalEdit;
+ m_pOriginalEdit = NULL;
+ }
+
+ m_resetSetting.reset();
+ m_dummyCategory.reset();
+}
+
+void CGUIDialogSettingsBase::FreeSettingsControls()
+{
+ // clear the settings group
+ CGUIControlGroupList* control =
+ dynamic_cast<CGUIControlGroupList*>(GetControl(SETTINGS_GROUP_ID));
+ if (control)
+ {
+ control->FreeResources();
+ control->ClearAll();
+ }
+
+ for (std::vector<BaseSettingControlPtr>::iterator control = m_settingControls.begin();
+ control != m_settingControls.end(); ++control)
+ (*control)->Clear();
+
+ m_settingControls.clear();
+}
+
+void CGUIDialogSettingsBase::OnTimeout()
+{
+ UpdateSettingControl(m_delayedSetting, true);
+}
+
+void CGUIDialogSettingsBase::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL || setting->GetType() == SettingType::Unknown ||
+ setting->GetType() == SettingType::Action)
+ return;
+
+ UpdateSettingControl(setting->GetId(), true);
+}
+
+void CGUIDialogSettingsBase::OnSettingPropertyChanged(
+ const std::shared_ptr<const CSetting>& setting, const char* propertyName)
+{
+ if (setting == NULL || propertyName == NULL)
+ return;
+
+ UpdateSettingControl(setting->GetId());
+}
+
+std::string CGUIDialogSettingsBase::GetLocalizedString(uint32_t labelId) const
+{
+ return g_localizeStrings.Get(labelId);
+}
+
+void CGUIDialogSettingsBase::SetupView()
+{
+ SetupControls();
+}
+
+std::set<std::string> CGUIDialogSettingsBase::CreateSettings()
+{
+ FreeSettingsControls();
+
+ std::set<std::string> settingMap;
+
+ if (m_categories.size() <= 0)
+ return settingMap;
+
+ if (m_iCategory < 0 || m_iCategory >= (int)m_categories.size())
+ m_iCategory = 0;
+
+ CGUIControlGroupList* group = dynamic_cast<CGUIControlGroupList*>(GetControl(SETTINGS_GROUP_ID));
+ if (group == NULL)
+ return settingMap;
+
+ SettingCategoryPtr category = m_categories.at(m_iCategory);
+ if (category == NULL)
+ return settingMap;
+
+ // set the description of the current category
+ SetDescription(category->GetHelp());
+
+ const SettingGroupList& groups = category->GetGroups((SettingLevel)GetSettingLevel());
+ int iControlID = CONTROL_SETTINGS_START_CONTROL;
+ bool first = true;
+ for (SettingGroupList::const_iterator groupIt = groups.begin(); groupIt != groups.end();
+ ++groupIt)
+ {
+ if (*groupIt == NULL)
+ continue;
+
+ const SettingList& settings = (*groupIt)->GetSettings((SettingLevel)GetSettingLevel());
+ if (settings.size() <= 0)
+ continue;
+
+ std::shared_ptr<const CSettingControlTitle> title =
+ std::dynamic_pointer_cast<const CSettingControlTitle>((*groupIt)->GetControl());
+ bool hideSeparator = title ? title->IsSeparatorHidden() : false;
+ bool separatorBelowGroupLabel = title ? title->IsSeparatorBelowLabel() : false;
+ int groupLabel = (*groupIt)->GetLabel();
+
+ // hide the separator for the first settings grouplist if it
+ // is the very first item in the list (also above the label)
+ if (first)
+ {
+ first = false;
+ if (groupLabel <= 0)
+ hideSeparator = true;
+ }
+ else if (!separatorBelowGroupLabel && !hideSeparator)
+ AddSeparator(group->GetWidth(), iControlID);
+
+ if (groupLabel > 0)
+ AddGroupLabel(*groupIt, group->GetWidth(), iControlID);
+
+ if (separatorBelowGroupLabel && !hideSeparator)
+ AddSeparator(group->GetWidth(), iControlID);
+
+ for (SettingList::const_iterator settingIt = settings.begin(); settingIt != settings.end();
+ ++settingIt)
+ {
+ const std::shared_ptr<CSetting>& pSetting = *settingIt;
+ settingMap.insert(pSetting->GetId());
+ AddSetting(pSetting, group->GetWidth(), iControlID);
+ }
+ }
+
+ if (AllowResettingSettings() && !settingMap.empty())
+ {
+ // add "Reset" control
+ AddSeparator(group->GetWidth(), iControlID);
+ AddSetting(m_resetSetting, group->GetWidth(), iControlID);
+ }
+
+ // update our settings (turns controls on/off as appropriate)
+ UpdateSettings();
+
+ group->SetInvalid();
+
+ return settingMap;
+}
+
+std::string CGUIDialogSettingsBase::GetSettingsLabel(const std::shared_ptr<ISetting>& pSetting)
+{
+ return GetLocalizedString(pSetting->GetLabel());
+}
+
+void CGUIDialogSettingsBase::UpdateSettings()
+{
+ for (std::vector<BaseSettingControlPtr>::iterator it = m_settingControls.begin();
+ it != m_settingControls.end(); ++it)
+ {
+ BaseSettingControlPtr pSettingControl = *it;
+ std::shared_ptr<CSetting> pSetting = pSettingControl->GetSetting();
+ CGUIControl* pControl = pSettingControl->GetControl();
+ if (pSetting == NULL || pControl == NULL)
+ continue;
+
+ pSettingControl->UpdateFromSetting();
+ }
+}
+
+CGUIControl* CGUIDialogSettingsBase::AddSetting(const std::shared_ptr<CSetting>& pSetting,
+ float width,
+ int& iControlID)
+{
+ if (pSetting == NULL)
+ return NULL;
+
+ BaseSettingControlPtr pSettingControl;
+ CGUIControl* pControl = NULL;
+
+ // determine the label and any possible indentation in case of sub settings
+ std::string label = GetSettingsLabel(pSetting);
+ int parentLevels = 0;
+ std::shared_ptr<CSetting> parentSetting = GetSetting(pSetting->GetParent());
+ while (parentSetting != NULL)
+ {
+ parentLevels++;
+ parentSetting = GetSetting(parentSetting->GetParent());
+ }
+
+ if (parentLevels > 0)
+ {
+ // add additional 2 spaces indentation for anything past one level
+ std::string indentation;
+ for (int index = 1; index < parentLevels; index++)
+ indentation.append(" ");
+ label = StringUtils::Format(g_localizeStrings.Get(168), indentation, label);
+ }
+
+ // create the proper controls
+ if (!pSetting->GetControl())
+ return NULL;
+
+ std::string controlType = pSetting->GetControl()->GetType();
+ if (controlType == "toggle")
+ {
+ if (m_pOriginalRadioButton != NULL)
+ pControl = m_pOriginalRadioButton->Clone();
+ if (pControl == NULL)
+ return NULL;
+
+ static_cast<CGUIRadioButtonControl*>(pControl)->SetLabel(label);
+ pSettingControl.reset(new CGUIControlRadioButtonSetting(
+ static_cast<CGUIRadioButtonControl*>(pControl), iControlID, pSetting, this));
+ }
+ else if (controlType == "spinner")
+ {
+ if (m_pOriginalSpin != NULL)
+ pControl = new CGUISpinControlEx(*m_pOriginalSpin);
+ if (pControl == NULL)
+ return NULL;
+
+ static_cast<CGUISpinControlEx*>(pControl)->SetText(label);
+ pSettingControl.reset(new CGUIControlSpinExSetting(static_cast<CGUISpinControlEx*>(pControl),
+ iControlID, pSetting, this));
+ }
+ else if (controlType == "edit")
+ {
+ if (m_pOriginalEdit != NULL)
+ pControl = new CGUIEditControl(*m_pOriginalEdit);
+ if (pControl == NULL)
+ return NULL;
+
+ static_cast<CGUIEditControl*>(pControl)->SetLabel(label);
+ pSettingControl.reset(new CGUIControlEditSetting(static_cast<CGUIEditControl*>(pControl),
+ iControlID, pSetting, this));
+ }
+ else if (controlType == "list")
+ {
+ if (m_pOriginalButton != NULL)
+ pControl = new CGUIButtonControl(*m_pOriginalButton);
+ if (pControl == NULL)
+ return NULL;
+
+ static_cast<CGUIButtonControl*>(pControl)->SetLabel(label);
+ pSettingControl.reset(new CGUIControlListSetting(static_cast<CGUIButtonControl*>(pControl),
+ iControlID, pSetting, this));
+ }
+ else if (controlType == "button" || controlType == "slider")
+ {
+ if (controlType == "button" ||
+ std::static_pointer_cast<const CSettingControlSlider>(pSetting->GetControl())->UsePopup())
+ {
+ if (m_pOriginalButton != NULL)
+ pControl = new CGUIButtonControl(*m_pOriginalButton);
+ if (pControl == NULL)
+ return NULL;
+
+ static_cast<CGUIButtonControl*>(pControl)->SetLabel(label);
+ pSettingControl.reset(new CGUIControlButtonSetting(static_cast<CGUIButtonControl*>(pControl),
+ iControlID, pSetting, this));
+ }
+ else
+ {
+ if (m_pOriginalSlider != NULL)
+ pControl = m_pOriginalSlider->Clone();
+ if (pControl == NULL)
+ return NULL;
+
+ static_cast<CGUISettingsSliderControl*>(pControl)->SetText(label);
+ pSettingControl.reset(new CGUIControlSliderSetting(
+ static_cast<CGUISettingsSliderControl*>(pControl), iControlID, pSetting, this));
+ }
+ }
+ else if (controlType == "range")
+ {
+ if (m_pOriginalSlider != NULL)
+ pControl = m_pOriginalSlider->Clone();
+ if (pControl == NULL)
+ return NULL;
+
+ static_cast<CGUISettingsSliderControl*>(pControl)->SetText(label);
+ pSettingControl.reset(new CGUIControlRangeSetting(
+ static_cast<CGUISettingsSliderControl*>(pControl), iControlID, pSetting, this));
+ }
+ else if (controlType == "label")
+ {
+ if (m_pOriginalButton != NULL)
+ pControl = new CGUIButtonControl(*m_pOriginalButton);
+ if (pControl == NULL)
+ return NULL;
+
+ static_cast<CGUIButtonControl*>(pControl)->SetLabel(label);
+ pSettingControl.reset(new CGUIControlLabelSetting(static_cast<CGUIButtonControl*>(pControl),
+ iControlID, pSetting, this));
+ }
+ else if (controlType == "colorbutton")
+ {
+ if (m_pOriginalColorButton)
+ pControl = m_pOriginalColorButton->Clone();
+ if (pControl == nullptr)
+ return nullptr;
+
+ static_cast<CGUIColorButtonControl*>(pControl)->SetLabel(label);
+ pSettingControl.reset(new CGUIControlColorButtonSetting(
+ static_cast<CGUIColorButtonControl*>(pControl), iControlID, pSetting, this));
+ }
+ else
+ return nullptr;
+
+ if (pSetting->GetControl()->GetDelayed())
+ pSettingControl->SetDelayed();
+
+ return AddSettingControl(pControl, pSettingControl, width, iControlID);
+}
+
+CGUIControl* CGUIDialogSettingsBase::AddSeparator(float width, int& iControlID)
+{
+ if (m_pOriginalImage == NULL)
+ return NULL;
+
+ CGUIControl* pControl = new CGUIImage(*m_pOriginalImage);
+ if (pControl == NULL)
+ return NULL;
+
+ return AddSettingControl(pControl,
+ BaseSettingControlPtr(new CGUIControlSeparatorSetting(
+ static_cast<CGUIImage*>(pControl), iControlID, this)),
+ width, iControlID);
+}
+
+CGUIControl* CGUIDialogSettingsBase::AddGroupLabel(const std::shared_ptr<CSettingGroup>& group,
+ float width,
+ int& iControlID)
+{
+ if (m_pOriginalGroupTitle == NULL)
+ return NULL;
+
+ CGUIControl* pControl = new CGUILabelControl(*m_pOriginalGroupTitle);
+ if (pControl == NULL)
+ return NULL;
+
+ static_cast<CGUILabelControl*>(pControl)->SetLabel(GetSettingsLabel(group));
+
+ return AddSettingControl(pControl,
+ BaseSettingControlPtr(new CGUIControlGroupTitleSetting(
+ static_cast<CGUILabelControl*>(pControl), iControlID, this)),
+ width, iControlID);
+}
+
+CGUIControl* CGUIDialogSettingsBase::AddSettingControl(CGUIControl* pControl,
+ BaseSettingControlPtr pSettingControl,
+ float width,
+ int& iControlID)
+{
+ if (pControl == NULL)
+ {
+ pSettingControl.reset();
+ return NULL;
+ }
+
+ pControl->SetID(iControlID++);
+ pControl->SetVisible(true);
+ pControl->SetWidth(width);
+
+ CGUIControlGroupList* group = dynamic_cast<CGUIControlGroupList*>(GetControl(SETTINGS_GROUP_ID));
+ if (group != NULL)
+ {
+ pControl->AllocResources();
+ group->AddControl(pControl);
+ }
+ m_settingControls.push_back(pSettingControl);
+
+ return pControl;
+}
+
+void CGUIDialogSettingsBase::SetHeading(const CVariant& label)
+{
+ SetControlLabel(CONTROL_SETTINGS_LABEL, label);
+}
+
+void CGUIDialogSettingsBase::SetDescription(const CVariant& label)
+{
+ SetControlLabel(CONTROL_SETTINGS_DESCRIPTION, label);
+}
+
+void CGUIDialogSettingsBase::OnResetSettings()
+{
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{10041}, CVariant{10042}))
+ {
+ for (std::vector<BaseSettingControlPtr>::iterator it = m_settingControls.begin();
+ it != m_settingControls.end(); ++it)
+ {
+ std::shared_ptr<CSetting> setting = (*it)->GetSetting();
+ if (setting != NULL)
+ setting->Reset();
+ }
+ }
+}
+
+void CGUIDialogSettingsBase::OnClick(const BaseSettingControlPtr& pSettingControl)
+{
+ if (AllowResettingSettings() &&
+ pSettingControl->GetSetting()->GetId() == SETTINGS_RESET_SETTING_ID)
+ {
+ OnAction(CAction(ACTION_SETTINGS_RESET));
+ return;
+ }
+
+ // we need to first set the delayed setting and then execute OnClick()
+ // because OnClick() triggers OnSettingChanged() and there we need to
+ // know if the changed setting is delayed or not
+ if (pSettingControl->IsDelayed())
+ {
+ m_delayedSetting = pSettingControl;
+ // for some controls we need to update its displayed data/text before
+ // OnClick() is called after the delay timer has expired because
+ // otherwise the displayed value of the control does not match with
+ // the user's interaction
+ pSettingControl->UpdateFromControl();
+
+ // either start or restart the delay timer which will result in a call to
+ // the control's OnClick() method to update the setting's value
+ if (m_delayedTimer.IsRunning())
+ m_delayedTimer.Restart();
+ else
+ m_delayedTimer.Start(GetDelayMs());
+
+ return;
+ }
+
+ // if changing the setting fails
+ // we need to restore the proper state
+ if (!pSettingControl->OnClick())
+ pSettingControl->UpdateFromSetting();
+}
+
+void CGUIDialogSettingsBase::UpdateSettingControl(const std::string& settingId,
+ bool updateDisplayOnly /* = false */)
+{
+ if (settingId.empty())
+ return;
+
+ return UpdateSettingControl(GetSettingControl(settingId), updateDisplayOnly);
+}
+
+void CGUIDialogSettingsBase::UpdateSettingControl(const BaseSettingControlPtr& pSettingControl,
+ bool updateDisplayOnly /* = false */)
+{
+ if (pSettingControl == NULL)
+ return;
+
+ // we send a thread message so that it's processed the following frame (some settings won't
+ // like being changed during Render())
+ // param2 = 1 for "only update the current value"
+ CGUIMessage message(GUI_MSG_UPDATE_ITEM, GetID(), pSettingControl->GetID(), 0,
+ updateDisplayOnly ? 1 : 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message, GetID());
+}
+
+void CGUIDialogSettingsBase::SetControlLabel(int controlId, const CVariant& label)
+{
+ if (GetControl(controlId) == NULL)
+ return;
+
+ if (label.isString())
+ SET_CONTROL_LABEL(controlId, label.asString());
+ else if (label.isInteger() && label.asInteger() >= 0)
+ {
+ int labelId = static_cast<uint32_t>(label.asInteger());
+ std::string localizedString = GetLocalizedString(labelId);
+ if (!localizedString.empty())
+ SET_CONTROL_LABEL(controlId, localizedString);
+ else
+ SET_CONTROL_LABEL(controlId, labelId);
+ }
+ else
+ SET_CONTROL_LABEL(controlId, "");
+}
+
+BaseSettingControlPtr CGUIDialogSettingsBase::GetSettingControl(const std::string& strSetting)
+{
+ for (std::vector<BaseSettingControlPtr>::iterator control = m_settingControls.begin();
+ control != m_settingControls.end(); ++control)
+ {
+ if ((*control)->GetSetting() != NULL && (*control)->GetSetting()->GetId() == strSetting)
+ return *control;
+ }
+
+ return BaseSettingControlPtr();
+}
+
+BaseSettingControlPtr CGUIDialogSettingsBase::GetSettingControl(int controlId)
+{
+ if (controlId < CONTROL_SETTINGS_START_CONTROL ||
+ controlId >= (int)(CONTROL_SETTINGS_START_CONTROL + m_settingControls.size()))
+ return BaseSettingControlPtr();
+
+ return m_settingControls[controlId - CONTROL_SETTINGS_START_CONTROL];
+}
diff --git a/xbmc/settings/dialogs/GUIDialogSettingsBase.h b/xbmc/settings/dialogs/GUIDialogSettingsBase.h
new file mode 100644
index 0000000..a633c65
--- /dev/null
+++ b/xbmc/settings/dialogs/GUIDialogSettingsBase.h
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2014-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/GUIDialog.h"
+#include "settings/SettingControl.h"
+#include "settings/lib/ISettingCallback.h"
+#include "threads/Timer.h"
+#include "utils/ILocalizer.h"
+
+#include <set>
+#include <vector>
+
+#define CONTROL_SETTINGS_LABEL 2
+#define CONTROL_SETTINGS_DESCRIPTION 6
+
+#define CONTROL_SETTINGS_OKAY_BUTTON 28
+#define CONTROL_SETTINGS_CANCEL_BUTTON 29
+#define CONTROL_SETTINGS_CUSTOM_BUTTON 30
+
+#define CONTROL_SETTINGS_START_BUTTONS -100
+#define CONTROL_SETTINGS_START_CONTROL -80
+
+#define SETTINGS_RESET_SETTING_ID "settings.reset"
+#define SETTINGS_EMPTY_CATEGORY_ID "categories.empty"
+
+class CGUIControl;
+class CGUIControlBaseSetting;
+class CGUIImage;
+class CGUISpinControlEx;
+class CGUIEditControl;
+class CGUIButtonControl;
+class CGUIRadioButtonControl;
+class CGUISettingsSliderControl;
+class CGUILabelControl;
+class CGUIColorButtonControl;
+
+class CSetting;
+class CSettingAction;
+class CSettingCategory;
+class CSettingGroup;
+class CSettingSection;
+
+class CVariant;
+
+class ISetting;
+
+typedef std::shared_ptr<CGUIControlBaseSetting> BaseSettingControlPtr;
+
+class CGUIDialogSettingsBase : public CGUIDialog,
+ public CSettingControlCreator,
+ public ILocalizer,
+ protected ITimerCallback,
+ protected ISettingCallback
+{
+public:
+ CGUIDialogSettingsBase(int windowId, const std::string& xmlFile);
+ ~CGUIDialogSettingsBase() override;
+
+ // specializations of CGUIControl
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ bool OnBack(int actionID) override;
+ void DoProcess(unsigned int currentTime, CDirtyRegionList& dirtyregions) override;
+
+ virtual bool IsConfirmed() const { return m_confirmed; }
+
+ // implementation of ILocalizer
+ std::string Localize(std::uint32_t code) const override { return GetLocalizedString(code); }
+
+protected:
+ // specializations of CGUIWindow
+ void OnInitWindow() override;
+
+ // implementations of ITimerCallback
+ void OnTimeout() override;
+
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingPropertyChanged(const std::shared_ptr<const CSetting>& setting,
+ const char* propertyName) override;
+
+ // new virtual methods
+ virtual bool AllowResettingSettings() const { return true; }
+ virtual int GetSettingLevel() const { return 0; }
+ virtual std::shared_ptr<CSettingSection> GetSection() = 0;
+ virtual std::shared_ptr<CSetting> GetSetting(const std::string& settingId) = 0;
+ virtual std::chrono::milliseconds GetDelayMs() const { return std::chrono::milliseconds(1500); }
+ virtual std::string GetLocalizedString(uint32_t labelId) const;
+
+ virtual bool OnOkay()
+ {
+ m_confirmed = true;
+ return true;
+ }
+ virtual void OnCancel() {}
+
+ virtual void SetupView();
+ virtual std::set<std::string> CreateSettings();
+ virtual void UpdateSettings();
+
+ /*!
+ \brief Get the name for the setting entry
+
+ Used as virtual to allow related settings dialog to give a std::string name of the setting.
+ If not used on own dialog class it handle the string from int CSetting::GetLabel(),
+ This must also be used if on related dialog no special entry is wanted.
+
+ \param pSetting Base settings class which need the name
+ \return Name used on settings dialog
+ */
+ virtual std::string GetSettingsLabel(const std::shared_ptr<ISetting>& pSetting);
+
+ virtual CGUIControl* AddSetting(const std::shared_ptr<CSetting>& pSetting,
+ float width,
+ int& iControlID);
+ virtual CGUIControl* AddSettingControl(CGUIControl* pControl,
+ BaseSettingControlPtr pSettingControl,
+ float width,
+ int& iControlID);
+
+ virtual void SetupControls(bool createSettings = true);
+ virtual void FreeControls();
+ virtual void DeleteControls();
+ virtual void FreeSettingsControls();
+
+ virtual void SetHeading(const CVariant& label);
+ virtual void SetDescription(const CVariant& label);
+
+ virtual void OnResetSettings();
+
+ /*!
+ \brief A setting control has been interacted with by the user
+
+ This method is called when the user manually interacts (clicks,
+ edits) with a setting control. It contains handling for both
+ delayed and undelayed settings and either starts the delay timer
+ or triggers the setting change which, on success, results in a
+ callback to OnSettingChanged().
+
+ \param pSettingControl Setting control that has been interacted with
+ */
+ virtual void OnClick(const BaseSettingControlPtr& pSettingControl);
+
+ void UpdateSettingControl(const std::string& settingId, bool updateDisplayOnly = false);
+ void UpdateSettingControl(const BaseSettingControlPtr& pSettingControl,
+ bool updateDisplayOnly = false);
+ void SetControlLabel(int controlId, const CVariant& label);
+
+ BaseSettingControlPtr GetSettingControl(const std::string& setting);
+ BaseSettingControlPtr GetSettingControl(int controlId);
+
+ CGUIControl* AddSeparator(float width, int& iControlID);
+ CGUIControl* AddGroupLabel(const std::shared_ptr<CSettingGroup>& group,
+ float width,
+ int& iControlID);
+
+ std::vector<std::shared_ptr<CSettingCategory>> m_categories;
+ std::vector<BaseSettingControlPtr> m_settingControls;
+
+ int m_iSetting;
+ int m_iCategory;
+ std::shared_ptr<CSettingAction> m_resetSetting;
+ std::shared_ptr<CSettingCategory> m_dummyCategory;
+
+ CGUISpinControlEx* m_pOriginalSpin;
+ CGUISettingsSliderControl* m_pOriginalSlider;
+ CGUIRadioButtonControl* m_pOriginalRadioButton;
+ CGUIColorButtonControl* m_pOriginalColorButton;
+ CGUIButtonControl* m_pOriginalCategoryButton;
+ CGUIButtonControl* m_pOriginalButton;
+ CGUIEditControl* m_pOriginalEdit;
+ CGUIImage* m_pOriginalImage;
+ CGUILabelControl* m_pOriginalGroupTitle;
+ bool m_newOriginalEdit;
+
+ BaseSettingControlPtr
+ m_delayedSetting; ///< Current delayed setting \sa CBaseSettingControl::SetDelayed()
+ CTimer m_delayedTimer; ///< Delayed setting timer
+
+ bool m_confirmed;
+ int m_focusedControl, m_fadedControl;
+};
diff --git a/xbmc/settings/dialogs/GUIDialogSettingsManagerBase.cpp b/xbmc/settings/dialogs/GUIDialogSettingsManagerBase.cpp
new file mode 100644
index 0000000..e01edfb
--- /dev/null
+++ b/xbmc/settings/dialogs/GUIDialogSettingsManagerBase.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014-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 "GUIDialogSettingsManagerBase.h"
+
+#include "settings/lib/SettingsManager.h"
+
+#include <cassert>
+
+CGUIDialogSettingsManagerBase::CGUIDialogSettingsManagerBase(int windowId, const std::string &xmlFile)
+ : CGUIDialogSettingsBase(windowId, xmlFile)
+{ }
+
+CGUIDialogSettingsManagerBase::~CGUIDialogSettingsManagerBase() = default;
+
+std::shared_ptr<CSetting> CGUIDialogSettingsManagerBase::GetSetting(const std::string &settingId)
+{
+ assert(GetSettingsManager() != nullptr);
+
+ return GetSettingsManager()->GetSetting(settingId);
+}
+
+bool CGUIDialogSettingsManagerBase::OnOkay()
+{
+ if (Save())
+ {
+ CGUIDialogSettingsBase::OnOkay();
+ return true;
+ }
+
+ return false;
+}
+
+std::set<std::string> CGUIDialogSettingsManagerBase::CreateSettings()
+{
+ assert(GetSettingsManager() != nullptr);
+
+ std::set<std::string> settings = CGUIDialogSettingsBase::CreateSettings();
+
+ if (!settings.empty())
+ GetSettingsManager()->RegisterCallback(this, settings);
+
+ return settings;
+}
+
+void CGUIDialogSettingsManagerBase::FreeSettingsControls()
+{
+ CGUIDialogSettingsBase::FreeSettingsControls();
+
+ if (GetSettingsManager() != nullptr)
+ GetSettingsManager()->UnregisterCallback(this);
+}
+
+std::shared_ptr<ISettingControl> CGUIDialogSettingsManagerBase::CreateControl(const std::string &controlType) const
+{
+ assert(GetSettingsManager() != nullptr);
+
+ return GetSettingsManager()->CreateControl(controlType);
+}
diff --git a/xbmc/settings/dialogs/GUIDialogSettingsManagerBase.h b/xbmc/settings/dialogs/GUIDialogSettingsManagerBase.h
new file mode 100644
index 0000000..42c3c31
--- /dev/null
+++ b/xbmc/settings/dialogs/GUIDialogSettingsManagerBase.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2014-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/dialogs/GUIDialogSettingsBase.h"
+
+class CSettingsManager;
+
+class CGUIDialogSettingsManagerBase : public CGUIDialogSettingsBase
+{
+public:
+ CGUIDialogSettingsManagerBase(int windowId, const std::string &xmlFile);
+ ~CGUIDialogSettingsManagerBase() override;
+
+protected:
+ virtual bool Save() = 0;
+ virtual CSettingsManager* GetSettingsManager() const = 0;
+
+ // implementation of CGUIDialogSettingsBase
+ std::shared_ptr<CSetting> GetSetting(const std::string &settingId) override;
+ bool OnOkay() override;
+
+ std::set<std::string> CreateSettings() override;
+ void FreeSettingsControls() override;
+
+ // implementation of ISettingControlCreator
+ std::shared_ptr<ISettingControl> CreateControl(const std::string &controlType) const override;
+};
diff --git a/xbmc/settings/dialogs/GUIDialogSettingsManualBase.cpp b/xbmc/settings/dialogs/GUIDialogSettingsManualBase.cpp
new file mode 100644
index 0000000..84114db
--- /dev/null
+++ b/xbmc/settings/dialogs/GUIDialogSettingsManualBase.cpp
@@ -0,0 +1,1617 @@
+/*
+ * 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 "GUIDialogSettingsManualBase.h"
+
+#include "settings/SettingAddon.h"
+#include "settings/SettingDateTime.h"
+#include "settings/SettingPath.h"
+#include "settings/SettingUtils.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingSection.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <string>
+#include <vector>
+
+CGUIDialogSettingsManualBase::CGUIDialogSettingsManualBase(int windowId, const std::string &xmlFile)
+ : CGUIDialogSettingsManagerBase(windowId, xmlFile)
+ , m_settingsManager(nullptr)
+{ }
+
+CGUIDialogSettingsManualBase::~CGUIDialogSettingsManualBase()
+{
+ if (GetSettingsManager() != nullptr)
+ {
+ GetSettingsManager()->Clear();
+ m_section = nullptr;
+ delete GetSettingsManager();
+ }
+}
+
+void CGUIDialogSettingsManualBase::SetupView()
+{
+ InitializeSettings();
+
+ if (GetSettingsManager() != nullptr)
+ {
+ // add the created setting section to the settings manager and mark it as ready
+ GetSettingsManager()->AddSection(m_section);
+ GetSettingsManager()->SetInitialized();
+ GetSettingsManager()->SetLoaded();
+ }
+
+ CGUIDialogSettingsBase::SetupView();
+}
+
+CSettingsManager* CGUIDialogSettingsManualBase::GetSettingsManager() const
+{
+ if (m_settingsManager == nullptr)
+ m_settingsManager = new CSettingsManager();
+
+ return m_settingsManager;
+}
+
+void CGUIDialogSettingsManualBase::InitializeSettings()
+{
+ if (GetSettingsManager() != nullptr)
+ {
+ GetSettingsManager()->Clear();
+ m_section = NULL;
+
+ // create a std::make_shared<section
+ m_section = std::make_shared<CSettingSection>(GetProperty("xmlfile").asString(), GetSettingsManager());
+ }
+}
+
+SettingCategoryPtr CGUIDialogSettingsManualBase::AddCategory(const std::string &id, int label, int help /* = -1 */)
+{
+ if (id.empty())
+ return NULL;
+
+ SettingCategoryPtr category = std::make_shared<CSettingCategory>(id, GetSettingsManager());
+ if (category == NULL)
+ return NULL;
+
+ category->SetLabel(label);
+ if (help >= 0)
+ category->SetHelp(help);
+
+ m_section->AddCategory(category);
+ return category;
+}
+
+SettingGroupPtr CGUIDialogSettingsManualBase::AddGroup(const SettingCategoryPtr& category,
+ int label /* = -1 */,
+ int help /* = -1 */,
+ bool separatorBelowLabel /* = true */,
+ bool hideSeparator /* = false */)
+{
+ if (category == NULL)
+ return NULL;
+
+ size_t groups = category->GetGroups().size();
+
+ SettingGroupPtr group = std::make_shared<CSettingGroup>(StringUtils::Format("{0}", groups + 1), GetSettingsManager());
+ if (group == NULL)
+ return NULL;
+
+ if (label >= 0)
+ group->SetLabel(label);
+ if (help >= 0)
+ group->SetHelp(help);
+ group->SetControl(GetTitleControl(separatorBelowLabel, hideSeparator));
+
+ category->AddGroup(group);
+ return group;
+}
+
+std::shared_ptr<CSettingBool> CGUIDialogSettingsManualBase::AddToggle(const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ bool value,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingBool> setting = std::make_shared<CSettingBool>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetCheckmarkControl(delayed));
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddEdit(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ int minimum /* = 0 */,
+ int step /* = 1 */,
+ int maximum /* = 0 */,
+ bool verifyNewValue /* = false */,
+ int heading /* = -1 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, minimum, step, maximum, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetEditControl("integer", delayed, false, verifyNewValue, heading));
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingNumber> CGUIDialogSettingsManualBase::AddEdit(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float value,
+ float minimum /* = 0.0f */,
+ float step /* = 1.0f */,
+ float maximum /* = 0.0f */,
+ bool verifyNewValue /* = false */,
+ int heading /* = -1 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingNumber> setting = std::make_shared<CSettingNumber>(id, label, value, minimum, step, maximum, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetEditControl("number", delayed, false, verifyNewValue, heading));
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingString> CGUIDialogSettingsManualBase::AddEdit(const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool allowEmpty /* = false */,
+ bool hidden /* = false */,
+ int heading /* = -1 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingString> setting = std::make_shared<CSettingString>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetEditControl("string", delayed, hidden, false, heading));
+ setting->SetAllowEmpty(allowEmpty);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingString> CGUIDialogSettingsManualBase::AddIp(const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool allowEmpty /* = false */,
+ int heading /* = -1 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingString> setting = std::make_shared<CSettingString>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetEditControl("ip", delayed, false, false, heading));
+ setting->SetAllowEmpty(allowEmpty);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingString> CGUIDialogSettingsManualBase::AddPasswordMd5(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool allowEmpty /* = false */,
+ int heading /* = -1 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingString> setting = std::make_shared<CSettingString>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetEditControl("md5", delayed, false, false, heading));
+ setting->SetAllowEmpty(allowEmpty);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingAction> CGUIDialogSettingsManualBase::AddButton(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& data /* = "" */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingAction> setting = std::make_shared<CSettingAction>(id, label, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetButtonControl("action", delayed));
+ setting->SetData(data);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingString> CGUIDialogSettingsManualBase::AddInfoLabelButton(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& info,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingString> setting = std::make_shared<CSettingString>(id, label, info, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetButtonControl("infolabel", false));
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingAddon> CGUIDialogSettingsManualBase::AddAddon(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ ADDON::AddonType addonType,
+ bool allowEmpty /* = false */,
+ int heading /* = -1 */,
+ bool hideValue /* = false */,
+ bool showInstalledAddons /* = true */,
+ bool showInstallableAddons /* = false */,
+ bool showMoreAddons /* = true */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingAddon> setting = std::make_shared<CSettingAddon>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetButtonControl("addon", delayed, heading, hideValue, showInstalledAddons, showInstallableAddons, showMoreAddons));
+ setting->SetAddonType(addonType);
+ setting->SetAllowEmpty(allowEmpty);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingPath> CGUIDialogSettingsManualBase::AddPath(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool writable /* = true */,
+ const std::vector<std::string>& sources /* = std::vector<std::string>() */,
+ bool allowEmpty /* = false */,
+ int heading /* = -1 */,
+ bool hideValue /* = false */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingPath> setting = std::make_shared<CSettingPath>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetButtonControl("path", delayed, heading, hideValue));
+ setting->SetWritable(writable);
+ setting->SetSources(sources);
+ setting->SetAllowEmpty(allowEmpty);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingDate> CGUIDialogSettingsManualBase::AddDate(const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool allowEmpty /* = false */,
+ int heading /* = -1 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 || GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingDate> setting = std::make_shared<CSettingDate>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetButtonControl("date", delayed, heading));
+ setting->SetAllowEmpty(allowEmpty);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingTime> CGUIDialogSettingsManualBase::AddTime(const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool allowEmpty /* = false */,
+ int heading /* = -1 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 || GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingTime> setting = std::make_shared<CSettingTime>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetButtonControl("time", delayed, heading));
+ setting->SetAllowEmpty(allowEmpty);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingString> CGUIDialogSettingsManualBase::AddSpinner(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ StringSettingOptionsFiller filler,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 || filler == NULL ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingString> setting = std::make_shared<CSettingString>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSpinnerControl("string", delayed));
+ setting->SetOptionsFiller(filler, this);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddSpinner(const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ int minimum,
+ int step,
+ int maximum,
+ int formatLabel /* = -1 */,
+ int minimumLabel /* = -1 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSpinnerControl("string", delayed, minimumLabel, formatLabel));
+ setting->SetMinimum(minimum);
+ setting->SetStep(step);
+ setting->SetMaximum(maximum);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddSpinner(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ int minimum,
+ int step,
+ int maximum,
+ const std::string& formatString,
+ int minimumLabel /* = -1 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSpinnerControl("string", delayed, minimumLabel, -1, formatString));
+ setting->SetMinimum(minimum);
+ setting->SetStep(step);
+ setting->SetMaximum(maximum);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddSpinner(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const TranslatableIntegerSettingOptions& entries,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 || entries.empty() ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSpinnerControl("string", delayed));
+ setting->SetTranslatableOptions(entries);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddSpinner(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const IntegerSettingOptions& entries,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 || entries.empty() ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSpinnerControl("string", delayed));
+ setting->SetOptions(entries);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddSpinner(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ IntegerSettingOptionsFiller filler,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 || filler == NULL ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSpinnerControl("string", delayed));
+ setting->SetOptionsFiller(filler, this);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingNumber> CGUIDialogSettingsManualBase::AddSpinner(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float value,
+ float minimum,
+ float step,
+ float maximum,
+ int formatLabel /* = -1 */,
+ int minimumLabel /* = -1 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingNumber> setting = std::make_shared<CSettingNumber>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSpinnerControl("number", delayed, minimumLabel, formatLabel));
+ setting->SetMinimum(static_cast<double>(minimum));
+ setting->SetStep(static_cast<double>(step));
+ setting->SetMaximum(static_cast<double>(maximum));
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingNumber> CGUIDialogSettingsManualBase::AddSpinner(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float value,
+ float minimum,
+ float step,
+ float maximum,
+ const std::string& formatString,
+ int minimumLabel /* = -1 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingNumber> setting = std::make_shared<CSettingNumber>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSpinnerControl("number", delayed, minimumLabel, -1, formatString));
+ setting->SetMinimum(static_cast<double>(minimum));
+ setting->SetStep(static_cast<double>(step));
+ setting->SetMaximum(static_cast<double>(maximum));
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingString> CGUIDialogSettingsManualBase::AddList(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ StringSettingOptionsFiller filler,
+ int heading,
+ bool visible /* = true */,
+ int help /* = -1 */,
+ bool details /* = false */)
+{
+ if (group == NULL || id.empty() || label < 0 || filler == NULL ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingString> setting = std::make_shared<CSettingString>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetListControl("string", false, heading, false, nullptr, details));
+ setting->SetOptionsFiller(filler, this);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddList(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const TranslatableIntegerSettingOptions& entries,
+ int heading,
+ bool visible /* = true */,
+ int help /* = -1 */,
+ bool details /* = false */)
+{
+ if (group == NULL || id.empty() || label < 0 || entries.empty() ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetListControl("integer", false, heading, false, nullptr, details));
+ setting->SetTranslatableOptions(entries);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddList(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const IntegerSettingOptions& entries,
+ int heading,
+ bool visible /* = true */,
+ int help /* = -1 */,
+ bool details /* = false */)
+{
+ if (group == NULL || id.empty() || label < 0 || entries.empty() ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetListControl("integer", false, heading, false, nullptr, details));
+ setting->SetOptions(entries);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddList(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ IntegerSettingOptionsFiller filler,
+ int heading,
+ bool visible /* = true */,
+ int help /* = -1 */,
+ bool details /* = false */)
+{
+ if (group == NULL || id.empty() || label < 0 || filler == NULL ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetListControl("integer", false, heading, false, nullptr, details));
+ setting->SetOptionsFiller(filler, this);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddList(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ std::vector<std::string> values,
+ StringSettingOptionsFiller filler,
+ int heading,
+ int minimumItems /* = 0 */,
+ int maximumItems /* = -1 */,
+ bool visible /* = true */,
+ int help /* = -1 */,
+ bool details /* = false */)
+{
+ if (group == NULL || id.empty() || label < 0 || filler == NULL ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingString> settingDefinition = std::make_shared<CSettingString>(id, GetSettingsManager());
+ if (settingDefinition == NULL)
+ return NULL;
+
+ settingDefinition->SetOptionsFiller(filler, this);
+
+ std::shared_ptr<CSettingList> setting = std::make_shared<CSettingList>(id, settingDefinition, label, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ std::vector<CVariant> valueList;
+ for (std::vector<std::string>::const_iterator itValue = values.begin(); itValue != values.end(); ++itValue)
+ valueList.emplace_back(*itValue);
+ SettingList settingValues;
+ if (!CSettingUtils::ValuesToList(setting, valueList, settingValues))
+ return NULL;
+ // setting the default will also set the actual value on an unchanged setting
+ setting->SetDefault(settingValues);
+
+ setting->SetControl(GetListControl("string", false, heading, true, nullptr, details));
+ setting->SetMinimumItems(minimumItems);
+ setting->SetMaximumItems(maximumItems);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddList(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ std::vector<int> values,
+ const TranslatableIntegerSettingOptions& entries,
+ int heading,
+ int minimumItems /* = 0 */,
+ int maximumItems /* = -1 */,
+ bool visible /* = true */,
+ int help /* = -1 */,
+ bool details /* = false */)
+{
+ if (group == NULL || id.empty() || label < 0 || entries.empty() ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> settingDefinition = std::make_shared<CSettingInt>(id, GetSettingsManager());
+ if (settingDefinition == NULL)
+ return NULL;
+
+ settingDefinition->SetTranslatableOptions(entries);
+
+ std::shared_ptr<CSettingList> setting = std::make_shared<CSettingList>(id, settingDefinition, label, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ std::vector<CVariant> valueList;
+ for (std::vector<int>::const_iterator itValue = values.begin(); itValue != values.end(); ++itValue)
+ valueList.emplace_back(*itValue);
+ SettingList settingValues;
+ if (!CSettingUtils::ValuesToList(setting, valueList, settingValues))
+ return NULL;
+ // setting the default will also set the actual value on an unchanged setting
+ setting->SetDefault(settingValues);
+
+ setting->SetControl(GetListControl("integer", false, heading, true, nullptr, details));
+ setting->SetMinimumItems(minimumItems);
+ setting->SetMaximumItems(maximumItems);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddList(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ std::vector<int> values,
+ const IntegerSettingOptions& entries,
+ int heading,
+ int minimumItems /* = 0 */,
+ int maximumItems /* = -1 */,
+ bool visible /* = true */,
+ int help /* = -1 */,
+ bool details /* = false */)
+{
+ if (group == NULL || id.empty() || label < 0 || entries.empty() ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> settingDefinition = std::make_shared<CSettingInt>(id, GetSettingsManager());
+ if (settingDefinition == NULL)
+ return NULL;
+
+ settingDefinition->SetOptions(entries);
+
+ std::shared_ptr<CSettingList> setting = std::make_shared<CSettingList>(id, settingDefinition, label, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ std::vector<CVariant> valueList;
+ for (std::vector<int>::const_iterator itValue = values.begin(); itValue != values.end(); ++itValue)
+ valueList.emplace_back(*itValue);
+ SettingList settingValues;
+ if (!CSettingUtils::ValuesToList(setting, valueList, settingValues))
+ return NULL;
+ // setting the default will also set the actual value on an unchanged setting
+ setting->SetDefault(settingValues);
+
+ setting->SetControl(GetListControl("integer", false, heading, true, nullptr, details));
+ setting->SetMinimumItems(minimumItems);
+ setting->SetMaximumItems(maximumItems);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddList(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ std::vector<int> values,
+ IntegerSettingOptionsFiller filler,
+ int heading,
+ int minimumItems /* = 0 */,
+ int maximumItems /* = -1 */,
+ bool visible /* = true */,
+ int help /* = -1 */,
+ SettingControlListValueFormatter formatter /* = NULL */,
+ bool details /* = false */)
+{
+ if (group == NULL || id.empty() || label < 0 || filler == NULL ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> settingDefinition = std::make_shared<CSettingInt>(id, GetSettingsManager());
+ if (settingDefinition == NULL)
+ return NULL;
+
+ settingDefinition->SetOptionsFiller(filler, this);
+
+ std::shared_ptr<CSettingList> setting = std::make_shared<CSettingList>(id, settingDefinition, label, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ std::vector<CVariant> valueList;
+ for (std::vector<int>::const_iterator itValue = values.begin(); itValue != values.end(); ++itValue)
+ valueList.emplace_back(*itValue);
+ SettingList settingValues;
+ if (!CSettingUtils::ValuesToList(setting, valueList, settingValues))
+ return NULL;
+ // setting the default will also set the actual value on an unchanged setting
+ setting->SetDefault(settingValues);
+
+ setting->SetControl(GetListControl("integer", false, heading, true, formatter, details));
+ setting->SetMinimumItems(minimumItems);
+ setting->SetMaximumItems(maximumItems);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddPercentageSlider(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ int formatLabel,
+ int step /* = 1 */,
+ int heading /* = -1 */,
+ bool usePopup /* = false */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSliderControl("percentage", delayed, heading, usePopup, formatLabel));
+ setting->SetMinimum(0);
+ setting->SetStep(step);
+ setting->SetMaximum(100);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddPercentageSlider(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const std::string& formatString,
+ int step /* = 1 */,
+ int heading /* = -1 */,
+ bool usePopup /* = false */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSliderControl("percentage", delayed, heading, usePopup, -1, formatString));
+ setting->SetMinimum(0);
+ setting->SetStep(step);
+ setting->SetMaximum(100);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddSlider(const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ int formatLabel,
+ int minimum,
+ int step,
+ int maximum,
+ int heading /* = -1 */,
+ bool usePopup /* = false */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSliderControl("integer", delayed, heading, usePopup, formatLabel));
+ setting->SetMinimum(minimum);
+ setting->SetStep(step);
+ setting->SetMaximum(maximum);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingInt> CGUIDialogSettingsManualBase::AddSlider(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const std::string& formatString,
+ int minimum,
+ int step,
+ int maximum,
+ int heading /* = -1 */,
+ bool usePopup /* = false */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> setting = std::make_shared<CSettingInt>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSliderControl("integer", delayed, heading, usePopup, -1, formatString));
+ setting->SetMinimum(minimum);
+ setting->SetStep(step);
+ setting->SetMaximum(maximum);
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingNumber> CGUIDialogSettingsManualBase::AddSlider(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float value,
+ int formatLabel,
+ float minimum,
+ float step,
+ float maximum,
+ int heading /* = -1 */,
+ bool usePopup /* = false */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingNumber> setting = std::make_shared<CSettingNumber>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSliderControl("number", delayed, heading, usePopup, formatLabel));
+ setting->SetMinimum(static_cast<double>(minimum));
+ setting->SetStep(static_cast<double>(step));
+ setting->SetMaximum(static_cast<double>(maximum));
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingNumber> CGUIDialogSettingsManualBase::AddSlider(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float value,
+ const std::string& formatString,
+ float minimum,
+ float step,
+ float maximum,
+ int heading /* = -1 */,
+ bool usePopup /* = false */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingNumber> setting = std::make_shared<CSettingNumber>(id, label, value, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ setting->SetControl(GetSliderControl("number", delayed, heading, usePopup, -1, formatString));
+ setting->SetMinimum(static_cast<double>(minimum));
+ setting->SetStep(static_cast<double>(step));
+ setting->SetMaximum(static_cast<double>(maximum));
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddPercentageRange(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int valueFormatLabel,
+ int step /* = 1 */,
+ int formatLabel /* = 21469 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ return AddRange(group, id, label, level, valueLower, valueUpper, 0, step, 100, "percentage", formatLabel, valueFormatLabel, "", delayed, visible, help);
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddPercentageRange(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ const std::string& valueFormatString /* = "{:d} %" */,
+ int step /* = 1 */,
+ int formatLabel /* = 21469 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ return AddRange(group, id, label, level, valueLower, valueUpper, 0, step, 100, "percentage", formatLabel, -1, valueFormatString, delayed, visible, help);
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddRange(const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ int valueFormatLabel,
+ int formatLabel /* = 21469 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ return AddRange(group, id, label, level, valueLower, valueUpper, minimum, step, maximum, "integer", formatLabel, valueFormatLabel, "", delayed, visible, help);
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddRange(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ const std::string& valueFormatString /* = "{:d}" */,
+ int formatLabel /* = 21469 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ return AddRange(group, id, label, level, valueLower, valueUpper, minimum, step, maximum, "integer", formatLabel, -1, valueFormatString, delayed, visible, help);
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddRange(const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float valueLower,
+ float valueUpper,
+ float minimum,
+ float step,
+ float maximum,
+ int valueFormatLabel,
+ int formatLabel /* = 21469 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ return AddRange(group, id, label, level, valueLower, valueUpper, minimum, step, maximum, "number", formatLabel, valueFormatLabel, "", delayed, visible, help);
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddRange(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float valueLower,
+ float valueUpper,
+ float minimum,
+ float step,
+ float maximum,
+ const std::string& valueFormatString /* = "{:.1f}" */,
+ int formatLabel /* = 21469 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ return AddRange(group, id, label, level, valueLower, valueUpper, minimum, step, maximum, "number", formatLabel, -1, valueFormatString, delayed, visible, help);
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddDateRange(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ int valueFormatLabel,
+ int formatLabel /* = 21469 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ return AddRange(group, id, label, level, valueLower, valueUpper, minimum, step, maximum, "date", formatLabel, valueFormatLabel, "", delayed, visible, help);
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddDateRange(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ const std::string& valueFormatString /* = "" */,
+ int formatLabel /* = 21469 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ return AddRange(group, id, label, level, valueLower, valueUpper, minimum, step, maximum, "date", formatLabel, -1, valueFormatString, delayed, visible, help);
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddTimeRange(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ int valueFormatLabel,
+ int formatLabel /* = 21469 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ return AddRange(group, id, label, level, valueLower, valueUpper, minimum, step, maximum, "time", formatLabel, valueFormatLabel, "", delayed, visible, help);
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddTimeRange(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ const std::string& valueFormatString /* = "mm:ss" */,
+ int formatLabel /* = 21469 */,
+ bool delayed /* = false */,
+ bool visible /* = true */,
+ int help /* = -1 */)
+{
+ return AddRange(group, id, label, level, valueLower, valueUpper, minimum, step, maximum, "time", formatLabel, -1, valueFormatString, delayed, visible, help);
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddRange(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ const std::string& format,
+ int formatLabel,
+ int valueFormatLabel,
+ const std::string& valueFormatString,
+ bool delayed,
+ bool visible,
+ int help)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingInt> settingDefinition = std::make_shared<CSettingInt>(id, GetSettingsManager());
+ if (settingDefinition == NULL)
+ return NULL;
+
+ settingDefinition->SetMinimum(minimum);
+ settingDefinition->SetStep(step);
+ settingDefinition->SetMaximum(maximum);
+
+ std::shared_ptr<CSettingList> setting = std::make_shared<CSettingList>(id, settingDefinition, label, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ std::vector<CVariant> valueList;
+ valueList.emplace_back(valueLower);
+ valueList.emplace_back(valueUpper);
+ SettingList settingValues;
+ if (!CSettingUtils::ValuesToList(setting, valueList, settingValues))
+ return NULL;
+ // setting the default will also set the actual value on an unchanged setting
+ setting->SetDefault(settingValues);
+
+ setting->SetControl(GetRangeControl(format, delayed, formatLabel, valueFormatLabel, valueFormatString));
+ setting->SetMinimumItems(2);
+ setting->SetMaximumItems(2);
+
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+std::shared_ptr<CSettingList> CGUIDialogSettingsManualBase::AddRange(
+ const SettingGroupPtr& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float valueLower,
+ float valueUpper,
+ float minimum,
+ float step,
+ float maximum,
+ const std::string& format,
+ int formatLabel,
+ int valueFormatLabel,
+ const std::string& valueFormatString,
+ bool delayed,
+ bool visible,
+ int help)
+{
+ if (group == NULL || id.empty() || label < 0 ||
+ GetSetting(id) != NULL)
+ return NULL;
+
+ std::shared_ptr<CSettingNumber> settingDefinition = std::make_shared<CSettingNumber>(id, GetSettingsManager());
+ if (settingDefinition == NULL)
+ return NULL;
+
+ settingDefinition->SetMinimum(static_cast<double>(minimum));
+ settingDefinition->SetStep(static_cast<double>(step));
+ settingDefinition->SetMaximum(static_cast<double>(maximum));
+
+ std::shared_ptr<CSettingList> setting = std::make_shared<CSettingList>(id, settingDefinition, label, GetSettingsManager());
+ if (setting == NULL)
+ return NULL;
+
+ std::vector<CVariant> valueList;
+ valueList.emplace_back(valueLower);
+ valueList.emplace_back(valueUpper);
+ SettingList settingValues;
+ if (!CSettingUtils::ValuesToList(setting, valueList, settingValues))
+ return NULL;
+ // setting the default will also set the actual value on an unchanged setting
+ setting->SetDefault(settingValues);
+
+ setting->SetControl(GetRangeControl(format, delayed, formatLabel, valueFormatLabel, valueFormatString));
+ setting->SetMinimumItems(2);
+ setting->SetMaximumItems(2);
+
+ setSettingDetails(setting, level, visible, help);
+
+ group->AddSetting(setting);
+ return setting;
+}
+
+void CGUIDialogSettingsManualBase::setSettingDetails(const std::shared_ptr<CSetting>& setting,
+ SettingLevel level,
+ bool visible,
+ int help)
+{
+ if (setting == NULL)
+ return;
+
+ if (level < SettingLevel::Basic)
+ level = SettingLevel::Basic;
+ else if (level > SettingLevel::Expert)
+ level = SettingLevel::Expert;
+
+ setting->SetLevel(level);
+ setting->SetVisible(visible);
+ if (help >= 0)
+ setting->SetHelp(help);
+}
+
+std::shared_ptr<ISettingControl> CGUIDialogSettingsManualBase::GetCheckmarkControl(bool delayed /* = false */)
+{
+ std::shared_ptr<CSettingControlCheckmark> control = std::make_shared<CSettingControlCheckmark>();
+ control->SetDelayed(delayed);
+
+ return control;
+}
+
+std::shared_ptr<ISettingControl> CGUIDialogSettingsManualBase::GetTitleControl(bool separatorBelowLabel /* = true */, bool hideSeparator /* = false */)
+{
+ std::shared_ptr<CSettingControlTitle> control = std::make_shared<CSettingControlTitle>();
+ control->SetSeparatorBelowLabel(separatorBelowLabel);
+ control->SetSeparatorHidden(hideSeparator);
+
+ return control;
+}
+
+std::shared_ptr<ISettingControl> CGUIDialogSettingsManualBase::GetEditControl(const std::string &format, bool delayed /* = false */, bool hidden /* = false */, bool verifyNewValue /* = false */, int heading /* = -1 */)
+{
+ std::shared_ptr<CSettingControlEdit> control = std::make_shared<CSettingControlEdit>();
+ if (!control->SetFormat(format))
+ return NULL;
+
+ control->SetDelayed(delayed);
+ control->SetHidden(hidden);
+ control->SetVerifyNewValue(verifyNewValue);
+ control->SetHeading(heading);
+
+ return control;
+}
+
+std::shared_ptr<ISettingControl> CGUIDialogSettingsManualBase::GetButtonControl(const std::string &format, bool delayed /* = false */, int heading /* = -1 */, bool hideValue /* = false */,
+ bool showInstalledAddons /* = true */, bool showInstallableAddons /* = false */, bool showMoreAddons /* = true */)
+{
+ std::shared_ptr<CSettingControlButton> control = std::make_shared<CSettingControlButton>();
+ if (!control->SetFormat(format))
+ return NULL;
+
+ control->SetDelayed(delayed);
+ control->SetHeading(heading);
+ control->SetHideValue(hideValue);
+ control->SetShowInstalledAddons(showInstalledAddons);
+ control->SetShowInstallableAddons(showInstallableAddons);
+ control->SetShowMoreAddons(showMoreAddons);
+
+ return control;
+}
+
+std::shared_ptr<ISettingControl> CGUIDialogSettingsManualBase::GetSpinnerControl(const std::string &format, bool delayed /* = false */, int minimumLabel /* = -1 */, int formatLabel /* = -1 */, const std::string &formatString /* = "" */)
+{
+ std::shared_ptr<CSettingControlSpinner> control = std::make_shared<CSettingControlSpinner>();
+ if (!control->SetFormat(format))
+ return NULL;
+
+ control->SetDelayed(delayed);
+ if (formatLabel >= 0)
+ control->SetFormatLabel(formatLabel);
+ if (!formatString.empty())
+ control->SetFormatString(formatString);
+ if (minimumLabel >= 0)
+ control->SetMinimumLabel(minimumLabel);
+
+ return control;
+}
+
+std::shared_ptr<ISettingControl> CGUIDialogSettingsManualBase::GetListControl(
+ const std::string& format,
+ bool delayed /* = false */,
+ int heading /* = -1 */,
+ bool multiselect /* = false */,
+ SettingControlListValueFormatter formatter /* = NULL */,
+ bool details /* = false */)
+{
+ std::shared_ptr<CSettingControlList> control = std::make_shared<CSettingControlList>();
+ if (!control->SetFormat(format))
+ return NULL;
+
+ control->SetDelayed(delayed);
+ control->SetHeading(heading);
+ control->SetMultiSelect(multiselect);
+ control->SetFormatter(formatter);
+ control->SetUseDetails(details);
+
+ return control;
+}
+
+std::shared_ptr<ISettingControl> CGUIDialogSettingsManualBase::GetSliderControl(const std::string &format, bool delayed /* = false */, int heading /* = -1 */, bool usePopup /* = false */,
+ int formatLabel /* = -1 */, const std::string &formatString /* = "" */)
+{
+ std::shared_ptr<CSettingControlSlider> control = std::make_shared<CSettingControlSlider>();
+ if (!control->SetFormat(format))
+ return NULL;
+
+ control->SetDelayed(delayed);
+ if (heading >= 0)
+ control->SetHeading(heading);
+ control->SetPopup(usePopup);
+ if (formatLabel >= 0)
+ control->SetFormatLabel(formatLabel);
+ if (!formatString.empty())
+ control->SetFormatString(formatString);
+
+ return control;
+}
+
+std::shared_ptr<ISettingControl> CGUIDialogSettingsManualBase::GetRangeControl(const std::string &format, bool delayed /* = false */, int formatLabel /* = -1 */,
+ int valueFormatLabel /* = -1 */, const std::string &valueFormatString /* = "" */)
+{
+ std::shared_ptr<CSettingControlRange> control = std::make_shared<CSettingControlRange>();
+ if (!control->SetFormat(format))
+ return NULL;
+
+ control->SetDelayed(delayed);
+ if (formatLabel >= 0)
+ control->SetFormatLabel(formatLabel);
+ if (valueFormatLabel >= 0)
+ control->SetValueFormatLabel(valueFormatLabel);
+ if (!valueFormatString.empty())
+ control->SetValueFormat(valueFormatString);
+
+ return control;
+}
diff --git a/xbmc/settings/dialogs/GUIDialogSettingsManualBase.h b/xbmc/settings/dialogs/GUIDialogSettingsManualBase.h
new file mode 100644
index 0000000..338259c
--- /dev/null
+++ b/xbmc/settings/dialogs/GUIDialogSettingsManualBase.h
@@ -0,0 +1,650 @@
+/*
+ * 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 "addons/IAddon.h"
+#include "settings/dialogs/GUIDialogSettingsManagerBase.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingLevel.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CSetting;
+class CSettingAction;
+class CSettingAddon;
+class CSettingBool;
+class CSettingCategory;
+class CSettingDate;
+class CSettingGroup;
+class CSettingInt;
+class CSettingList;
+class CSettingNumber;
+class CSettingPath;
+class CSettingSection;
+class CSettingString;
+class CSettingsManager;
+class CSettingTime;
+
+class CGUIDialogSettingsManualBase : public CGUIDialogSettingsManagerBase
+{
+public:
+ CGUIDialogSettingsManualBase(int windowId, const std::string &xmlFile);
+ ~CGUIDialogSettingsManualBase() override;
+
+protected:
+ // implementation of CGUIDialogSettingsBase
+ std::shared_ptr<CSettingSection> GetSection() override { return m_section; }
+ void SetupView() override;
+
+ // implementation of CGUIDialogSettingsManagerBase
+ CSettingsManager* GetSettingsManager() const override;
+
+ virtual void InitializeSettings();
+
+ std::shared_ptr<CSettingCategory> AddCategory(const std::string &id, int label, int help = -1);
+ std::shared_ptr<CSettingGroup> AddGroup(const std::shared_ptr<CSettingCategory>& category,
+ int label = -1,
+ int help = -1,
+ bool separatorBelowLabel = true,
+ bool hideSeparator = false);
+ // checkmark control
+ std::shared_ptr<CSettingBool> AddToggle(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ bool value,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ // edit controls
+ std::shared_ptr<CSettingInt> AddEdit(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ int minimum = 0,
+ int step = 1,
+ int maximum = 0,
+ bool verifyNewValue = false,
+ int heading = -1,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingNumber> AddEdit(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float value,
+ float minimum = 0.0f,
+ float step = 1.0f,
+ float maximum = 0.0f,
+ bool verifyNewValue = false,
+ int heading = -1,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingString> AddEdit(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool allowEmpty = false,
+ bool hidden = false,
+ int heading = -1,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingString> AddIp(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool allowEmpty = false,
+ int heading = -1,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingString> AddPasswordMd5(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool allowEmpty = false,
+ int heading = -1,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ // button controls
+ std::shared_ptr<CSettingAction> AddButton(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& data = "",
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingString> AddInfoLabelButton(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& info,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingAddon> AddAddon(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ ADDON::AddonType addonType,
+ bool allowEmpty = false,
+ int heading = -1,
+ bool hideValue = false,
+ bool showInstalledAddons = true,
+ bool showInstallableAddons = false,
+ bool showMoreAddons = true,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingPath> AddPath(
+ const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool writable = true,
+ const std::vector<std::string>& sources = std::vector<std::string>(),
+ bool allowEmpty = false,
+ int heading = -1,
+ bool hideValue = false,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingDate> AddDate(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool allowEmpty = false,
+ int heading = -1,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingTime> AddTime(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ bool allowEmpty = false,
+ int heading = -1,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+
+ // spinner controls
+ std::shared_ptr<CSettingString> AddSpinner(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ StringSettingOptionsFiller filler,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingInt> AddSpinner(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ int minimum,
+ int step,
+ int maximum,
+ int formatLabel = -1,
+ int minimumLabel = -1,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingInt> AddSpinner(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ int minimum,
+ int step,
+ int maximum,
+ const std::string& formatString,
+ int minimumLabel = -1,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingInt> AddSpinner(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const TranslatableIntegerSettingOptions& entries,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingInt> AddSpinner(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const IntegerSettingOptions& entries,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingInt> AddSpinner(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ IntegerSettingOptionsFiller filler,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingNumber> AddSpinner(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float value,
+ float minimum,
+ float step,
+ float maximum,
+ int formatLabel = -1,
+ int minimumLabel = -1,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingNumber> AddSpinner(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float value,
+ float minimum,
+ float step,
+ float maximum,
+ const std::string& formatString,
+ int minimumLabel = -1,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+
+ // list controls
+ std::shared_ptr<CSettingString> AddList(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ const std::string& value,
+ StringSettingOptionsFiller filler,
+ int heading,
+ bool visible = true,
+ int help = -1,
+ bool details = false);
+ std::shared_ptr<CSettingInt> AddList(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const TranslatableIntegerSettingOptions& entries,
+ int heading,
+ bool visible = true,
+ int help = -1,
+ bool details = false);
+ std::shared_ptr<CSettingInt> AddList(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const IntegerSettingOptions& entries,
+ int heading,
+ bool visible = true,
+ int help = -1,
+ bool details = false);
+ std::shared_ptr<CSettingInt> AddList(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ IntegerSettingOptionsFiller filler,
+ int heading,
+ bool visible = true,
+ int help = -1,
+ bool details = false);
+ std::shared_ptr<CSettingList> AddList(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ std::vector<std::string> values,
+ StringSettingOptionsFiller filler,
+ int heading,
+ int minimumItems = 0,
+ int maximumItems = -1,
+ bool visible = true,
+ int help = -1,
+ bool details = false);
+ std::shared_ptr<CSettingList> AddList(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ std::vector<int> values,
+ const TranslatableIntegerSettingOptions& entries,
+ int heading,
+ int minimumItems = 0,
+ int maximumItems = -1,
+ bool visible = true,
+ int help = -1,
+ bool details = false);
+ std::shared_ptr<CSettingList> AddList(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ std::vector<int> values,
+ const IntegerSettingOptions& entries,
+ int heading,
+ int minimumItems = 0,
+ int maximumItems = -1,
+ bool visible = true,
+ int help = -1,
+ bool details = false);
+ std::shared_ptr<CSettingList> AddList(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ std::vector<int> values,
+ IntegerSettingOptionsFiller filler,
+ int heading,
+ int minimumItems = 0,
+ int maximumItems = -1,
+ bool visible = true,
+ int help = -1,
+ SettingControlListValueFormatter formatter = nullptr,
+ bool details = false);
+
+ // slider controls
+ std::shared_ptr<CSettingInt> AddPercentageSlider(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ int formatLabel,
+ int step = 1,
+ int heading = -1,
+ bool usePopup = false,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingInt> AddPercentageSlider(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const std::string& formatString,
+ int step = 1,
+ int heading = -1,
+ bool usePopup = false,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingInt> AddSlider(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ int formatLabel,
+ int minimum,
+ int step,
+ int maximum,
+ int heading = -1,
+ bool usePopup = false,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingInt> AddSlider(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int value,
+ const std::string& formatString,
+ int minimum,
+ int step,
+ int maximum,
+ int heading = -1,
+ bool usePopup = false,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingNumber> AddSlider(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float value,
+ int formatLabel,
+ float minimum,
+ float step,
+ float maximum,
+ int heading = -1,
+ bool usePopup = false,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingNumber> AddSlider(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float value,
+ const std::string& formatString,
+ float minimum,
+ float step,
+ float maximum,
+ int heading = -1,
+ bool usePopup = false,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+
+ // range controls
+ std::shared_ptr<CSettingList> AddPercentageRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int valueFormatLabel,
+ int step = 1,
+ int formatLabel = 21469,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingList> AddPercentageRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ const std::string& valueFormatString = "{:d} %",
+ int step = 1,
+ int formatLabel = 21469,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingList> AddRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ int valueFormatLabel,
+ int formatLabel = 21469,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingList> AddRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ const std::string& valueFormatString = "{:d}",
+ int formatLabel = 21469,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingList> AddRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float valueLower,
+ float valueUpper,
+ float minimum,
+ float step,
+ float maximum,
+ int valueFormatLabel,
+ int formatLabel = 21469,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingList> AddRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float valueLower,
+ float valueUpper,
+ float minimum,
+ float step,
+ float maximum,
+ const std::string& valueFormatString = "{:.1f}",
+ int formatLabel = 21469,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingList> AddDateRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ int valueFormatLabel,
+ int formatLabel = 21469,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingList> AddDateRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ const std::string& valueFormatString = "",
+ int formatLabel = 21469,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingList> AddTimeRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ int valueFormatLabel,
+ int formatLabel = 21469,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+ std::shared_ptr<CSettingList> AddTimeRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ const std::string& valueFormatString = "mm:ss",
+ int formatLabel = 21469,
+ bool delayed = false,
+ bool visible = true,
+ int help = -1);
+
+ std::shared_ptr<ISettingControl> GetTitleControl(bool separatorBelowLabel = true, bool hideSeparator = false);
+ std::shared_ptr<ISettingControl> GetCheckmarkControl(bool delayed = false);
+ std::shared_ptr<ISettingControl> GetEditControl(const std::string &format, bool delayed = false, bool hidden = false, bool verifyNewValue = false, int heading = -1);
+ std::shared_ptr<ISettingControl> GetButtonControl(const std::string &format, bool delayed = false, int heading = -1, bool hideValue = false, bool showInstalledAddons = true,
+ bool showInstallableAddons = false, bool showMoreAddons = true);
+ std::shared_ptr<ISettingControl> GetSpinnerControl(const std::string &format, bool delayed = false, int minimumLabel = -1, int formatLabel = -1, const std::string &formatString = "");
+ std::shared_ptr<ISettingControl> GetListControl(
+ const std::string& format,
+ bool delayed = false,
+ int heading = -1,
+ bool multiselect = false,
+ SettingControlListValueFormatter formatter = nullptr,
+ bool details = false);
+ std::shared_ptr<ISettingControl> GetSliderControl(const std::string &format, bool delayed = false, int heading = -1, bool usePopup = false, int formatLabel = -1, const std::string &formatString = "");
+ std::shared_ptr<ISettingControl> GetRangeControl(const std::string &format, bool delayed = false, int formatLabel = -1, int valueFormatLabel = -1, const std::string &valueFormatString = "");
+
+private:
+ std::shared_ptr<CSettingList> AddRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ int valueLower,
+ int valueUpper,
+ int minimum,
+ int step,
+ int maximum,
+ const std::string& format,
+ int formatLabel,
+ int valueFormatLabel,
+ const std::string& valueFormatString,
+ bool delayed,
+ bool visible,
+ int help);
+ std::shared_ptr<CSettingList> AddRange(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& id,
+ int label,
+ SettingLevel level,
+ float valueLower,
+ float valueUpper,
+ float minimum,
+ float step,
+ float maximum,
+ const std::string& format,
+ int formatLabel,
+ int valueFormatLabel,
+ const std::string& valueFormatString,
+ bool delayed,
+ bool visible,
+ int help);
+
+ void setSettingDetails(const std::shared_ptr<CSetting>& setting,
+ SettingLevel level,
+ bool visible,
+ int help);
+
+ mutable CSettingsManager *m_settingsManager;
+ std::shared_ptr<CSettingSection> m_section;
+};
diff --git a/xbmc/settings/lib/CMakeLists.txt b/xbmc/settings/lib/CMakeLists.txt
new file mode 100644
index 0000000..a39a0ca
--- /dev/null
+++ b/xbmc/settings/lib/CMakeLists.txt
@@ -0,0 +1,31 @@
+set(SOURCES ISetting.cpp
+ ISettingControl.cpp
+ Setting.cpp
+ SettingCategoryAccess.cpp
+ SettingConditions.cpp
+ SettingDependency.cpp
+ SettingRequirement.cpp
+ SettingSection.cpp
+ SettingsManager.cpp
+ SettingUpdate.cpp)
+
+set(HEADERS ISetting.h
+ ISettingCallback.h
+ ISettingControl.h
+ ISettingControlCreator.h
+ ISettingCreator.h
+ ISettingsHandler.h
+ ISettingsValueSerializer.h
+ Setting.h
+ SettingCategoryAccess.h
+ SettingConditions.h
+ SettingDefinitions.h
+ SettingDependency.h
+ SettingLevel.h
+ SettingRequirement.h
+ SettingSection.h
+ SettingsManager.h
+ SettingType.h
+ SettingUpdate.h)
+
+core_add_library(settings_lib)
diff --git a/xbmc/settings/lib/ISetting.cpp b/xbmc/settings/lib/ISetting.cpp
new file mode 100644
index 0000000..a836f99
--- /dev/null
+++ b/xbmc/settings/lib/ISetting.cpp
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2013-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 "ISetting.h"
+
+#include "SettingDefinitions.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+
+#include <string>
+
+ISetting::ISetting(const std::string &id, CSettingsManager *settingsManager /* = nullptr */)
+ : m_id(id)
+ , m_settingsManager(settingsManager)
+ , m_requirementCondition(settingsManager)
+{ }
+
+bool ISetting::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ if (node == nullptr)
+ return false;
+
+ bool value;
+ if (XMLUtils::GetBoolean(node, SETTING_XML_ELM_VISIBLE, value))
+ m_visible = value;
+
+ auto element = node->ToElement();
+ if (element == nullptr)
+ return false;
+
+ int iValue = -1;
+ if (element->QueryIntAttribute(SETTING_XML_ATTR_LABEL, &iValue) == TIXML_SUCCESS && iValue > 0)
+ m_label = iValue;
+ if (element->QueryIntAttribute(SETTING_XML_ATTR_HELP, &iValue) == TIXML_SUCCESS && iValue > 0)
+ m_help = iValue;
+
+ auto requirementNode = node->FirstChild(SETTING_XML_ELM_REQUIREMENT);
+ if (requirementNode == nullptr)
+ return true;
+
+ return m_requirementCondition.Deserialize(requirementNode);
+}
+
+bool ISetting::DeserializeIdentification(const TiXmlNode* node, std::string& identification)
+{
+ return DeserializeIdentificationFromAttribute(node, SETTING_XML_ATTR_ID, identification);
+}
+
+bool ISetting::DeserializeIdentificationFromAttribute(const TiXmlNode* node,
+ const std::string& attribute,
+ std::string& identification)
+{
+ if (node == nullptr)
+ return false;
+
+ auto element = node->ToElement();
+ if (element == nullptr)
+ return false;
+
+ auto idAttribute = element->Attribute(attribute);
+ if (idAttribute == nullptr || idAttribute->empty())
+ return false;
+
+ identification = *idAttribute;
+ return true;
+}
+
+void ISetting::CheckRequirements()
+{
+ m_meetsRequirements = m_requirementCondition.Check();
+}
diff --git a/xbmc/settings/lib/ISetting.h b/xbmc/settings/lib/ISetting.h
new file mode 100644
index 0000000..8a40fe3
--- /dev/null
+++ b/xbmc/settings/lib/ISetting.h
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2013-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 "SettingRequirement.h"
+
+#include <string>
+
+class CSettingsManager;
+class TiXmlNode;
+
+/*!
+ \ingroup settings
+ \brief Interface defining the base of all setting objects
+ */
+class ISetting
+{
+public:
+ /*!
+ \brief Creates a new setting object with the given identifier.
+
+ \param id Identifier of the setting object
+ \param settingsManager Reference to the settings manager
+ */
+ ISetting(const std::string &id, CSettingsManager *settingsManager = nullptr);
+ virtual ~ISetting() = default;
+
+ /*!
+ \brief Deserializes the given XML node into the properties of the setting
+ object.
+
+ If the update parameter is true, the checks for mandatory properties are
+ skipped and values are only updated.
+
+ \param node XML node containing the properties of the setting object
+ \param update Whether to perform checks for mandatory properties or not
+ \return True if deserialization was successful, false otherwise
+ */
+ virtual bool Deserialize(const TiXmlNode *node, bool update = false);
+
+ /*!
+ \brief Gets the identifier of the setting object.
+
+ \return Identifier of the setting object
+ */
+ const std::string& GetId() const { return m_id; }
+ /*!
+ \brief Whether the setting object is visible or hidden.
+
+ \return True if the setting object is visible, false otherwise
+ */
+ virtual bool IsVisible() const { return m_visible; }
+ /*!
+ \brief Sets the visibility state of the setting object.
+
+ \param visible Whether the setting object shall be visible or not
+ */
+ virtual void SetVisible(bool visible) { m_visible = visible; }
+ /*!
+ \brief Gets the localizeable label ID of the setting group.
+
+ \return Localizeable label ID of the setting group
+ */
+ int GetLabel() const { return m_label; }
+ /*!
+ \brief Sets the localizeable label ID of the setting group.
+
+ \param label Localizeable label ID of the setting group
+ */
+ void SetLabel(int label) { m_label = label; }
+ /*!
+ \brief Gets the localizeable help ID of the setting group.
+
+ \return Localizeable help ID of the setting group
+ */
+ int GetHelp() const { return m_help; }
+ /*!
+ \brief Sets the localizeable help ID of the setting group.
+
+ \param label Localizeable help ID of the setting group
+ */
+ void SetHelp(int help) { m_help = help; }
+ /*!
+ \brief Whether the setting object meets all necessary requirements.
+
+ \return True if the setting object meets all necessary requirements, false otherwise
+ */
+ virtual bool MeetsRequirements() const { return m_meetsRequirements; }
+ /*!
+ \brief Checks if the setting object meets all necessary requirements.
+ */
+ virtual void CheckRequirements();
+ /*!
+ \brief Sets whether the setting object meets all necessary requirements.
+
+ \param visible Whether the setting object meets all necessary requirements or not
+ */
+ virtual void SetRequirementsMet(bool requirementsMet) { m_meetsRequirements = requirementsMet; }
+
+ /*!
+ \brief Deserializes the given XML node to retrieve a setting object's
+ identifier.
+
+ \param node XML node containing a setting object's identifier
+ \param identification Will contain the deserialized setting object's identifier
+ \return True if a setting object's identifier was deserialized, false otherwise
+ */
+ static bool DeserializeIdentification(const TiXmlNode *node, std::string &identification);
+
+protected:
+ static constexpr int DefaultLabel = -1;
+ /*!
+ \brief Deserializes the given XML node to retrieve a setting object's identifier from the given attribute.
+
+ \param node XML node containing a setting object's identifier
+ \param attribute Attribute which contains the setting object's identifier
+ \param identification Will contain the deserialized setting object's identifier
+ \return True if a setting object's identifier was deserialized, false otherwise
+ */
+ static bool DeserializeIdentificationFromAttribute(const TiXmlNode* node,
+ const std::string& attribute,
+ std::string& identification);
+
+ std::string m_id;
+ CSettingsManager *m_settingsManager;
+
+private:
+ bool m_visible = true;
+ int m_label = DefaultLabel;
+ int m_help = -1;
+ bool m_meetsRequirements = true;
+ CSettingRequirement m_requirementCondition;
+};
diff --git a/xbmc/settings/lib/ISettingCallback.h b/xbmc/settings/lib/ISettingCallback.h
new file mode 100644
index 0000000..00fc428
--- /dev/null
+++ b/xbmc/settings/lib/ISettingCallback.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2013-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 <memory>
+
+class CSetting;
+class TiXmlNode;
+
+class ISettingCallback
+{
+public:
+ virtual ~ISettingCallback() = default;
+
+ /*!
+ \brief The value of the given setting is being changed.
+
+ This callback is triggered whenever the value of a setting is being
+ changed. The given CSetting already contains the new value and the handler
+ of the callback has the possibility to allow or revert changing the value
+ of the setting. In case of a revert OnSettingChanging() is called again to
+ inform all listeners that the value change has been reverted.
+
+ \param setting The setting whose value is being changed (already containing the changed value)
+ \return True if the new value is acceptable otherwise false
+ */
+ virtual bool OnSettingChanging(const std::shared_ptr<const CSetting>& setting) { return true; }
+
+ /*!
+ \brief The value of the given setting has changed.
+
+ This callback is triggered whenever the value of a setting has been
+ successfully changed (i.e. none of the OnSettingChanging() handlers)
+ has reverted the change.
+
+ \param setting The setting whose value has been changed
+ */
+ virtual void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) {}
+
+ /*!
+ \brief The given setting has been activated.
+
+ This callback is triggered whenever the given setting has been activated.
+ This callback is only fired for CSettingAction settings.
+
+ \param setting The setting which has been activated.
+ */
+ virtual void OnSettingAction(const std::shared_ptr<const CSetting>& setting) {}
+
+ /*!
+ \brief The given setting needs to be updated.
+
+ This callback is triggered when a setting needs to be updated because its
+ value is outdated. This only happens when initially loading the value of a
+ setting and will not be triggered afterwards.
+
+ \param setting The setting which needs to be updated.
+ \param oldSettingId The id of the previous setting.
+ \param oldSettingNode The old setting node
+ \return True if the setting has been successfully updated otherwise false
+ */
+ virtual bool OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode)
+ {
+ return false;
+ }
+
+ /*!
+ \brief The given property of the given setting has changed
+
+ This callback is triggered when a property (e.g. enabled or the list of
+ dynamic options) has changed.
+
+ \param setting The setting which has a changed property
+ \param propertyName The string representation of the changed property
+ */
+ virtual void OnSettingPropertyChanged(const std::shared_ptr<const CSetting>& setting,
+ const char* propertyName)
+ {
+ }
+};
diff --git a/xbmc/settings/lib/ISettingControl.cpp b/xbmc/settings/lib/ISettingControl.cpp
new file mode 100644
index 0000000..3fc51a5
--- /dev/null
+++ b/xbmc/settings/lib/ISettingControl.cpp
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013-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 "ISettingControl.h"
+
+#include "ServiceBroker.h"
+#include "SettingDefinitions.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+Logger ISettingControl::s_logger;
+
+ISettingControl::ISettingControl()
+{
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("ISettingControl");
+}
+
+bool ISettingControl::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ if (node == nullptr)
+ return false;
+
+ auto elem = node->ToElement();
+ if (elem == nullptr)
+ return false;
+
+ auto strTmp = elem->Attribute(SETTING_XML_ATTR_FORMAT);
+ std::string format;
+ if (strTmp != nullptr)
+ format = strTmp;
+ if (!SetFormat(format))
+ {
+ s_logger->error("error reading \"{}\" attribute of <control>", SETTING_XML_ATTR_FORMAT);
+ return false;
+ }
+
+ if ((strTmp = elem->Attribute(SETTING_XML_ATTR_DELAYED)) != nullptr)
+ {
+ if (!StringUtils::EqualsNoCase(strTmp, "false") && !StringUtils::EqualsNoCase(strTmp, "true"))
+ {
+ s_logger->error("error reading \"{}\" attribute of <control>", SETTING_XML_ATTR_DELAYED);
+ return false;
+ }
+ else
+ m_delayed = StringUtils::EqualsNoCase(strTmp, "true");
+ }
+
+ return true;
+}
diff --git a/xbmc/settings/lib/ISettingControl.h b/xbmc/settings/lib/ISettingControl.h
new file mode 100644
index 0000000..293d5ad
--- /dev/null
+++ b/xbmc/settings/lib/ISettingControl.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013-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 "utils/logtypes.h"
+
+#include <string>
+
+class TiXmlNode;
+
+class ISettingControl
+{
+public:
+ ISettingControl();
+ virtual ~ISettingControl() = default;
+
+ virtual std::string GetType() const = 0;
+ const std::string& GetFormat() const { return m_format; }
+ bool GetDelayed() const { return m_delayed; }
+ void SetDelayed(bool delayed) { m_delayed = delayed; }
+
+ virtual bool Deserialize(const TiXmlNode *node, bool update = false);
+ virtual bool SetFormat(const std::string &format) { return true; }
+
+protected:
+ bool m_delayed = false;
+ std::string m_format;
+
+ static Logger s_logger;
+};
diff --git a/xbmc/settings/lib/ISettingControlCreator.h b/xbmc/settings/lib/ISettingControlCreator.h
new file mode 100644
index 0000000..2f77e1c
--- /dev/null
+++ b/xbmc/settings/lib/ISettingControlCreator.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013-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 <memory>
+#include <string>
+
+class ISettingControl;
+
+/*!
+ \ingroup settings
+ \brief Interface for creating a new setting control of a custom setting control type.
+ */
+class ISettingControlCreator
+{
+public:
+ virtual ~ISettingControlCreator() = default;
+
+ /*!
+ \brief Creates a new setting control of the given custom setting control type.
+
+ \param controlType string representation of the setting control type
+ \return A new setting control object of the given (custom) setting control type or nullptr if the setting control type is unknown
+ */
+ virtual std::shared_ptr<ISettingControl> CreateControl(const std::string &controlType) const = 0;
+};
diff --git a/xbmc/settings/lib/ISettingCreator.h b/xbmc/settings/lib/ISettingCreator.h
new file mode 100644
index 0000000..e951c85
--- /dev/null
+++ b/xbmc/settings/lib/ISettingCreator.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013-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 <memory>
+#include <string>
+
+class CSetting;
+class CSettingsManager;
+
+/*!
+ \ingroup settings
+ \brief Interface for creating a new setting of a custom setting type.
+ */
+class ISettingCreator
+{
+public:
+ virtual ~ISettingCreator() = default;
+
+ /*!
+ \brief Creates a new setting of the given custom setting type.
+
+ \param settingType string representation of the setting type
+ \param settingId Identifier of the setting to be created
+ \param settingsManager Reference to the settings manager
+ \return A new setting object of the given (custom) setting type or nullptr if the setting type is unknown
+ */
+ virtual std::shared_ptr<CSetting> CreateSetting(const std::string &settingType, const std::string &settingId, CSettingsManager *settingsManager = nullptr) const = 0;
+};
diff --git a/xbmc/settings/lib/ISettingsHandler.h b/xbmc/settings/lib/ISettingsHandler.h
new file mode 100644
index 0000000..466b55a
--- /dev/null
+++ b/xbmc/settings/lib/ISettingsHandler.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2013-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
+
+/*!
+ \ingroup settings
+ \brief Interface defining methods being called by the settings system if an
+ action is performed on multiple/all settings
+ */
+class ISettingsHandler
+{
+public:
+ virtual ~ISettingsHandler() = default;
+
+ /*!
+ \brief Settings loading has been initiated.
+
+ \return True if the settings should be loaded, false if the loading should be aborted.
+ */
+ virtual bool OnSettingsLoading() { return true; }
+ /*!
+ \brief Settings have been loaded.
+
+ This callback can be used to trigger loading other settings.
+ */
+ virtual void OnSettingsLoaded() { }
+ /*!
+ \brief Settings saving has been initiated.
+
+ \return True if the settings should be saved, false if the saving should be aborted.
+ */
+ virtual bool OnSettingsSaving() const { return true; }
+ /*!
+ \brief Settings have been saved.
+
+ This callback can be used to trigger saving other settings.
+ */
+ virtual void OnSettingsSaved() const { }
+ /*!
+ \brief Setting values have been unloaded.
+
+ This callback can be used to trigger uninitializing any state variables
+ (e.g. before re-loading the settings).
+ */
+ virtual void OnSettingsUnloaded() { }
+ /*!
+ \brief Settings have been cleared.
+
+ This callback can be used to trigger clearing any state variables.
+ */
+ virtual void OnSettingsCleared() { }
+};
diff --git a/xbmc/settings/lib/ISettingsValueSerializer.h b/xbmc/settings/lib/ISettingsValueSerializer.h
new file mode 100644
index 0000000..94bf664
--- /dev/null
+++ b/xbmc/settings/lib/ISettingsValueSerializer.h
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <string>
+
+class CSettingsManager;
+
+class ISettingsValueSerializer
+{
+public:
+ virtual ~ISettingsValueSerializer() = default;
+
+ virtual std::string SerializeValues(const CSettingsManager* settingsManager) const = 0;
+};
diff --git a/xbmc/settings/lib/Setting.cpp b/xbmc/settings/lib/Setting.cpp
new file mode 100644
index 0000000..0802dd8
--- /dev/null
+++ b/xbmc/settings/lib/Setting.cpp
@@ -0,0 +1,1690 @@
+/*
+ * Copyright (C) 2013-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 "Setting.h"
+
+#include "ServiceBroker.h"
+#include "SettingDefinitions.h"
+#include "SettingsManager.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <shared_mutex>
+#include <sstream>
+#include <utility>
+
+template<typename TKey, typename TValue>
+bool CheckSettingOptionsValidity(const TValue& value, const std::vector<std::pair<TKey, TValue>>& options)
+{
+ for (const auto& it : options)
+ {
+ if (it.second == value)
+ return true;
+ }
+
+ return false;
+}
+
+template<typename TKey, typename TValue>
+bool CheckSettingOptionsValidity(const TValue& value, const std::vector<TKey>& options)
+{
+ for (const auto& it : options)
+ {
+ if (it.value == value)
+ return true;
+ }
+
+ return false;
+}
+
+bool DeserializeOptionsSort(const TiXmlElement* optionsElement, SettingOptionsSort& optionsSort)
+{
+ optionsSort = SettingOptionsSort::NoSorting;
+
+ std::string sort;
+ if (optionsElement->QueryStringAttribute("sort", &sort) != TIXML_SUCCESS)
+ return true;
+
+ if (StringUtils::EqualsNoCase(sort, "false") || StringUtils::EqualsNoCase(sort, "off") ||
+ StringUtils::EqualsNoCase(sort, "no") || StringUtils::EqualsNoCase(sort, "disabled"))
+ optionsSort = SettingOptionsSort::NoSorting;
+ else if (StringUtils::EqualsNoCase(sort, "asc") || StringUtils::EqualsNoCase(sort, "ascending") ||
+ StringUtils::EqualsNoCase(sort, "true") || StringUtils::EqualsNoCase(sort, "on") ||
+ StringUtils::EqualsNoCase(sort, "yes") || StringUtils::EqualsNoCase(sort, "enabled"))
+ optionsSort = SettingOptionsSort::Ascending;
+ else if (StringUtils::EqualsNoCase(sort, "desc") || StringUtils::EqualsNoCase(sort, "descending"))
+ optionsSort = SettingOptionsSort::Descending;
+ else
+ return false;
+
+ return true;
+}
+
+Logger CSetting::s_logger;
+
+CSetting::CSetting(const std::string& id, CSettingsManager* settingsManager /* = nullptr */)
+ : ISetting(id, settingsManager)
+{
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSetting");
+}
+
+CSetting::CSetting(const std::string& id, const CSetting& setting)
+ : CSetting(id, setting.m_settingsManager)
+{
+ Copy(setting);
+}
+
+void CSetting::MergeBasics(const CSetting& other)
+{
+ // ISetting
+ SetVisible(other.GetVisible());
+ SetLabel(other.GetLabel());
+ SetHelp(other.GetHelp());
+ SetRequirementsMet(other.MeetsRequirements());
+ // CSetting
+ SetEnabled(other.GetEnabled());
+ SetParent(other.GetParent());
+ SetLevel(other.GetLevel());
+ SetControl(const_cast<CSetting&>(other).GetControl());
+ SetDependencies(other.GetDependencies());
+}
+
+bool CSetting::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ // handle <visible> conditions
+ if (!ISetting::Deserialize(node, update))
+ return false;
+
+ auto element = node->ToElement();
+ if (element == nullptr)
+ return false;
+
+ auto parentSetting = element->Attribute(SETTING_XML_ATTR_PARENT);
+ if (parentSetting != nullptr)
+ m_parentSetting = parentSetting;
+
+ // get <enable>
+ bool value;
+ if (XMLUtils::GetBoolean(node, SETTING_XML_ELM_ENABLED, value))
+ m_enabled = value;
+
+ // get the <level>
+ int level = -1;
+ if (XMLUtils::GetInt(node, SETTING_XML_ELM_LEVEL, level))
+ m_level = static_cast<SettingLevel>(level);
+
+ if (m_level < SettingLevel::Basic || m_level > SettingLevel::Internal)
+ m_level = SettingLevel::Standard;
+
+ auto dependencies = node->FirstChild(SETTING_XML_ELM_DEPENDENCIES);
+ if (dependencies != nullptr)
+ {
+ auto dependencyNode = dependencies->FirstChild(SETTING_XML_ELM_DEPENDENCY);
+ while (dependencyNode != nullptr)
+ {
+ CSettingDependency dependency(m_settingsManager);
+ if (dependency.Deserialize(dependencyNode))
+ m_dependencies.push_back(dependency);
+ else
+ s_logger->warn("error reading <{}> tag of \"{}\"", SETTING_XML_ELM_DEPENDENCY, m_id);
+
+ dependencyNode = dependencyNode->NextSibling(SETTING_XML_ELM_DEPENDENCY);
+ }
+ }
+
+ auto control = node->FirstChildElement(SETTING_XML_ELM_CONTROL);
+ if (control != nullptr)
+ {
+ auto controlType = control->Attribute(SETTING_XML_ATTR_TYPE);
+ if (controlType == nullptr)
+ {
+ s_logger->error("error reading \"{}\" attribute of <control> tag of \"{}\"",
+ SETTING_XML_ATTR_TYPE, m_id);
+ return false;
+ }
+
+ m_control = m_settingsManager->CreateControl(controlType);
+ if (m_control == nullptr || !m_control->Deserialize(control, update))
+ {
+ s_logger->error("error reading <{}> tag of \"{}\"", SETTING_XML_ELM_CONTROL, m_id);
+ return false;
+ }
+ }
+ else if (!update && m_level < SettingLevel::Internal && !IsReference())
+ {
+ s_logger->error("missing <{}> tag of \"{}\"", SETTING_XML_ELM_CONTROL, m_id);
+ return false;
+ }
+
+ auto updates = node->FirstChild(SETTING_XML_ELM_UPDATES);
+ if (updates != nullptr)
+ {
+ auto updateElem = updates->FirstChildElement(SETTING_XML_ELM_UPDATE);
+ while (updateElem != nullptr)
+ {
+ CSettingUpdate settingUpdate;
+ if (settingUpdate.Deserialize(updateElem))
+ {
+ if (!m_updates.insert(settingUpdate).second)
+ s_logger->warn("duplicate <{}> definition for \"{}\"", SETTING_XML_ELM_UPDATE, m_id);
+ }
+ else
+ s_logger->warn("error reading <{}> tag of \"{}\"", SETTING_XML_ELM_UPDATE, m_id);
+
+ updateElem = updateElem->NextSiblingElement(SETTING_XML_ELM_UPDATE);
+ }
+ }
+
+ return true;
+}
+
+bool CSetting::IsEnabled() const
+{
+ if (m_dependencies.empty() && m_parentSetting.empty())
+ return m_enabled;
+
+ // if the setting has a parent setting and that parent setting is disabled
+ // the setting should automatically also be disabled
+ if (!m_parentSetting.empty())
+ {
+ SettingPtr parentSetting = m_settingsManager->GetSetting(m_parentSetting);
+ if (parentSetting != nullptr && !parentSetting->IsEnabled())
+ return false;
+ }
+
+ bool enabled = m_enabled;
+ for (const auto& dep : m_dependencies)
+ {
+ if (dep.GetType() != SettingDependencyType::Enable)
+ continue;
+
+ if (!dep.Check())
+ {
+ enabled = false;
+ break;
+ }
+ }
+
+ return enabled;
+}
+
+void CSetting::SetEnabled(bool enabled)
+{
+ if (!m_dependencies.empty() || m_enabled == enabled)
+ return;
+
+ m_enabled = enabled;
+ OnSettingPropertyChanged(shared_from_this(), "enabled");
+}
+
+void CSetting::MakeReference(const std::string& referencedId /* = "" */)
+{
+ auto tmpReferencedId = referencedId;
+ if (referencedId.empty())
+ tmpReferencedId = m_id;
+
+ m_id = StringUtils::Format("#{}[{}]", tmpReferencedId, StringUtils::CreateUUID());
+ m_referencedId = tmpReferencedId;
+}
+
+bool CSetting::IsVisible() const
+{
+ if (!ISetting::IsVisible())
+ return false;
+
+ bool visible = true;
+ for (const auto& dep : m_dependencies)
+ {
+ if (dep.GetType() != SettingDependencyType::Visible)
+ continue;
+
+ if (!dep.Check())
+ {
+ visible = false;
+ break;
+ }
+ }
+
+ return visible;
+}
+
+bool CSetting::OnSettingChanging(const std::shared_ptr<const CSetting>& setting)
+{
+ if (m_callback == nullptr)
+ return true;
+
+ return m_callback->OnSettingChanging(setting);
+}
+
+void CSetting::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (m_callback == nullptr)
+ return;
+
+ m_callback->OnSettingChanged(setting);
+}
+
+void CSetting::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (m_callback == nullptr)
+ return;
+
+ m_callback->OnSettingAction(setting);
+}
+
+bool CSetting::DeserializeIdentification(const TiXmlNode* node,
+ std::string& identification,
+ bool& isReference)
+{
+ isReference = false;
+
+ // first check if we can simply retrieve the setting's identifier
+ if (ISetting::DeserializeIdentification(node, identification))
+ return true;
+
+ // otherwise try to retrieve a reference to another setting's identifier
+ if (!DeserializeIdentificationFromAttribute(node, SETTING_XML_ATTR_REFERENCE, identification))
+ return false;
+
+ isReference = true;
+ return true;
+}
+
+bool CSetting::OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode)
+{
+ if (m_callback == nullptr)
+ return false;
+
+ return m_callback->OnSettingUpdate(setting, oldSettingId, oldSettingNode);
+}
+
+void CSetting::OnSettingPropertyChanged(const std::shared_ptr<const CSetting>& setting,
+ const char* propertyName)
+{
+ if (m_callback == nullptr)
+ return;
+
+ m_callback->OnSettingPropertyChanged(setting, propertyName);
+}
+
+void CSetting::Copy(const CSetting &setting)
+{
+ SetVisible(setting.IsVisible());
+ SetLabel(setting.GetLabel());
+ SetHelp(setting.GetHelp());
+ SetRequirementsMet(setting.MeetsRequirements());
+ m_callback = setting.m_callback;
+ m_level = setting.m_level;
+
+ if (setting.m_control != nullptr)
+ {
+ m_control = m_settingsManager->CreateControl(setting.m_control->GetType());
+ *m_control = *setting.m_control;
+ }
+ else
+ m_control = nullptr;
+
+ m_dependencies = setting.m_dependencies;
+ m_updates = setting.m_updates;
+ m_changed = setting.m_changed;
+}
+
+Logger CSettingList::s_logger;
+
+CSettingList::CSettingList(const std::string& id,
+ std::shared_ptr<CSetting> settingDefinition,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSetting(id, settingsManager), m_definition(std::move(settingDefinition))
+{
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingList");
+}
+
+CSettingList::CSettingList(const std::string& id,
+ std::shared_ptr<CSetting> settingDefinition,
+ int label,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingList(id, std::move(settingDefinition), settingsManager)
+{
+ SetLabel(label);
+}
+
+CSettingList::CSettingList(const std::string &id, const CSettingList &setting)
+ : CSetting(id, setting)
+{
+ copy(setting);
+}
+
+SettingPtr CSettingList::Clone(const std::string &id) const
+{
+ if (m_definition == nullptr)
+ return nullptr;
+
+ return std::make_shared<CSettingList>(id, *this);
+}
+
+void CSettingList::MergeDetails(const CSetting& other)
+{
+ if (other.GetType() != SettingType::List)
+ return;
+
+ const auto& listSetting = static_cast<const CSettingList&>(other);
+ if (m_definition == nullptr && listSetting.m_definition != nullptr)
+ m_definition = listSetting.m_definition;
+ if (m_defaults.empty() && !listSetting.m_defaults.empty())
+ m_defaults = listSetting.m_defaults;
+ if (m_values.empty() && !listSetting.m_values.empty())
+ m_values = listSetting.m_values;
+ if (m_delimiter == "|" && listSetting.m_delimiter != "|")
+ m_delimiter = listSetting.m_delimiter;
+ if (m_minimumItems == 0 && listSetting.m_minimumItems != 0)
+ m_minimumItems = listSetting.m_minimumItems;
+ if (m_maximumItems == -1 && listSetting.m_maximumItems != -1)
+ m_maximumItems = listSetting.m_maximumItems;
+}
+
+bool CSettingList::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if (m_definition == nullptr)
+ return false;
+
+ if (!CSetting::Deserialize(node, update))
+ return false;
+
+ auto element = node->ToElement();
+ if (element == nullptr)
+ {
+ s_logger->warn("unable to read type of list setting of {}", m_id);
+ return false;
+ }
+
+ // deserialize the setting definition in update mode because we don't care
+ // about an invalid <default> value (which is never used)
+ if (!m_definition->Deserialize(node, true))
+ return false;
+
+ auto constraints = node->FirstChild(SETTING_XML_ELM_CONSTRAINTS);
+ if (constraints != nullptr)
+ {
+ // read the delimiter
+ std::string delimiter;
+ if (XMLUtils::GetString(constraints, SETTING_XML_ELM_DELIMITER, delimiter) && !delimiter.empty())
+ m_delimiter = delimiter;
+
+ XMLUtils::GetInt(constraints, SETTING_XML_ELM_MINIMUM_ITEMS, m_minimumItems);
+ if (m_minimumItems < 0)
+ m_minimumItems = 0;
+ XMLUtils::GetInt(constraints, SETTING_XML_ELM_MAXIMUM_ITEMS, m_maximumItems);
+ if (m_maximumItems <= 0)
+ m_maximumItems = -1;
+ else if (m_maximumItems < m_minimumItems)
+ {
+ s_logger->warn("invalid <{}> ({}) and/or <{}> ({}) of {}", SETTING_XML_ELM_MINIMUM_ITEMS,
+ m_minimumItems, SETTING_XML_ELM_MAXIMUM_ITEMS, m_maximumItems, m_id);
+ return false;
+ }
+ }
+
+ // read the default and initial values
+ std::string values;
+ if (XMLUtils::GetString(node, SETTING_XML_ELM_DEFAULT, values))
+ {
+ if (!fromString(values, m_defaults))
+ {
+ s_logger->warn("invalid <{}> definition \"{}\" of {}", SETTING_XML_ELM_DEFAULT, values, m_id);
+ return false;
+ }
+ Reset();
+ }
+
+ return true;
+}
+
+SettingType CSettingList::GetElementType() const
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+
+ if (m_definition == nullptr)
+ return SettingType::Unknown;
+
+ return m_definition->GetType();
+}
+
+bool CSettingList::FromString(const std::string &value)
+{
+ SettingList values;
+ if (!fromString(value, values))
+ return false;
+
+ return SetValue(values);
+}
+
+std::string CSettingList::ToString() const
+{
+ return toString(m_values);
+}
+
+bool CSettingList::Equals(const std::string &value) const
+{
+ SettingList values;
+ if (!fromString(value, values) || values.size() != m_values.size())
+ return false;
+
+ bool ret = true;
+ for (size_t index = 0; index < values.size(); index++)
+ {
+ if (!m_values[index]->Equals(values[index]->ToString()))
+ {
+ ret = false;
+ break;
+ }
+ }
+
+ return ret;
+}
+
+bool CSettingList::CheckValidity(const std::string &value) const
+{
+ SettingList values;
+ return fromString(value, values);
+}
+
+void CSettingList::Reset()
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+ SettingList values;
+ for (const auto& it : m_defaults)
+ values.push_back(it->Clone(it->GetId()));
+
+ SetValue(values);
+}
+
+bool CSettingList::FromString(const std::vector<std::string> &value)
+{
+ SettingList values;
+ if (!fromValues(value, values))
+ return false;
+
+ return SetValue(values);
+}
+
+bool CSettingList::SetValue(const SettingList &values)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if ((int)values.size() < m_minimumItems ||
+ (m_maximumItems > 0 && (int)values.size() > m_maximumItems))
+ return false;
+
+ bool equal = values.size() == m_values.size();
+ for (size_t index = 0; index < values.size(); index++)
+ {
+ if (values[index]->GetType() != GetElementType())
+ return false;
+
+ if (equal &&
+ !values[index]->Equals(m_values[index]->ToString()))
+ equal = false;
+ }
+
+ if (equal)
+ return true;
+
+ SettingList oldValues = m_values;
+ m_values.clear();
+ m_values.insert(m_values.begin(), values.begin(), values.end());
+
+ if (!OnSettingChanging(shared_from_base<CSettingList>()))
+ {
+ m_values = oldValues;
+
+ // the setting couldn't be changed because one of the
+ // callback handlers failed the OnSettingChanging()
+ // callback so we need to let all the callback handlers
+ // know that the setting hasn't changed
+ OnSettingChanging(shared_from_base<CSettingList>());
+ return false;
+ }
+
+ m_changed = toString(m_values) != toString(m_defaults);
+ OnSettingChanged(shared_from_base<CSettingList>());
+ return true;
+}
+
+void CSettingList::SetDefault(const SettingList &values)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ m_defaults.clear();
+ m_defaults.insert(m_defaults.begin(), values.begin(), values.end());
+
+ if (!m_changed)
+ {
+ m_values.clear();
+ for (const auto& it : m_defaults)
+ m_values.push_back(it->Clone(it->GetId()));
+ }
+}
+
+void CSettingList::copy(const CSettingList &setting)
+{
+ CSetting::Copy(setting);
+
+ copy(setting.m_values, m_values);
+ copy(setting.m_defaults, m_defaults);
+
+ if (setting.m_definition != nullptr)
+ {
+ auto definitionCopy = setting.m_definition->Clone(m_id + ".definition");
+ if (definitionCopy != nullptr)
+ m_definition = definitionCopy;
+ }
+
+ m_delimiter = setting.m_delimiter;
+ m_minimumItems = setting.m_minimumItems;
+ m_maximumItems = setting.m_maximumItems;
+}
+
+void CSettingList::copy(const SettingList &srcValues, SettingList &dstValues)
+{
+ dstValues.clear();
+
+ for (const auto& value : srcValues)
+ {
+ if (value == nullptr)
+ continue;
+
+ SettingPtr valueCopy = value->Clone(value->GetId());
+ if (valueCopy == nullptr)
+ continue;
+
+ dstValues.emplace_back(valueCopy);
+ }
+}
+
+bool CSettingList::fromString(const std::string &strValue, SettingList &values) const
+{
+ return fromValues(StringUtils::Split(strValue, m_delimiter), values);
+}
+
+bool CSettingList::fromValues(const std::vector<std::string> &strValues, SettingList &values) const
+{
+ if ((int)strValues.size() < m_minimumItems ||
+ (m_maximumItems > 0 && (int)strValues.size() > m_maximumItems))
+ return false;
+
+ bool ret = true;
+ int index = 0;
+ for (const auto& value : strValues)
+ {
+ auto settingValue = m_definition->Clone(StringUtils::Format("{}.{}", m_id, index++));
+ if (settingValue == nullptr ||
+ !settingValue->FromString(value))
+ {
+ ret = false;
+ break;
+ }
+
+ values.emplace_back(settingValue);
+ }
+
+ if (!ret)
+ values.clear();
+
+ return ret;
+}
+
+std::string CSettingList::toString(const SettingList &values) const
+{
+ std::vector<std::string> strValues;
+ for (const auto& value : values)
+ {
+ if (value != nullptr)
+ strValues.push_back(value->ToString());
+ }
+
+ return StringUtils::Join(strValues, m_delimiter);
+}
+
+Logger CSettingBool::s_logger;
+
+CSettingBool::CSettingBool(const std::string& id, CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingBool(id, DefaultLabel, DefaultValue, settingsManager)
+{
+}
+
+CSettingBool::CSettingBool(const std::string& id, const CSettingBool& setting)
+ : CSettingBool(id, setting.m_settingsManager)
+{
+ copy(setting);
+}
+
+CSettingBool::CSettingBool(const std::string& id,
+ int label,
+ bool value,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CTraitedSetting(id, settingsManager), m_value(value), m_default(value)
+{
+ SetLabel(label);
+
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingBool");
+}
+
+SettingPtr CSettingBool::Clone(const std::string &id) const
+{
+ return std::make_shared<CSettingBool>(id, *this);
+}
+
+void CSettingBool::MergeDetails(const CSetting& other)
+{
+ if (other.GetType() != SettingType::Boolean)
+ return;
+
+ const auto& boolSetting = static_cast<const CSettingBool&>(other);
+ if (m_default == false && boolSetting.m_default == true)
+ m_default = boolSetting.m_default;
+ if (m_value == m_default && boolSetting.m_value != m_default)
+ m_value = boolSetting.m_value;
+}
+
+bool CSettingBool::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if (!CSetting::Deserialize(node, update))
+ return false;
+
+ // get the default value
+ bool value;
+ if (XMLUtils::GetBoolean(node, SETTING_XML_ELM_DEFAULT, value))
+ m_value = m_default = value;
+ else if (!update)
+ {
+ s_logger->error("error reading the default value of \"{}\"", m_id);
+ return false;
+ }
+
+ return true;
+}
+
+bool CSettingBool::FromString(const std::string &value)
+{
+ bool bValue;
+ if (!fromString(value, bValue))
+ return false;
+
+ return SetValue(bValue);
+}
+
+std::string CSettingBool::ToString() const
+{
+ return m_value ? "true" : "false";
+}
+
+bool CSettingBool::Equals(const std::string &value) const
+{
+ bool bValue;
+ return (fromString(value, bValue) && m_value == bValue);
+}
+
+bool CSettingBool::CheckValidity(const std::string &value) const
+{
+ bool bValue;
+ return fromString(value, bValue);
+}
+
+bool CSettingBool::SetValue(bool value)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if (value == m_value)
+ return true;
+
+ bool oldValue = m_value;
+ m_value = value;
+
+ if (!OnSettingChanging(shared_from_base<CSettingBool>()))
+ {
+ m_value = oldValue;
+
+ // the setting couldn't be changed because one of the
+ // callback handlers failed the OnSettingChanging()
+ // callback so we need to let all the callback handlers
+ // know that the setting hasn't changed
+ OnSettingChanging(shared_from_base<CSettingBool>());
+ return false;
+ }
+
+ m_changed = m_value != m_default;
+ OnSettingChanged(shared_from_base<CSettingBool>());
+ return true;
+}
+
+void CSettingBool::SetDefault(bool value)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ m_default = value;
+ if (!m_changed)
+ m_value = m_default;
+}
+
+void CSettingBool::copy(const CSettingBool &setting)
+{
+ CSetting::Copy(setting);
+
+ m_value = setting.m_value;
+ m_default = setting.m_default;
+}
+
+bool CSettingBool::fromString(const std::string &strValue, bool &value) const
+{
+ if (StringUtils::EqualsNoCase(strValue, "true"))
+ {
+ value = true;
+ return true;
+ }
+ if (StringUtils::EqualsNoCase(strValue, "false"))
+ {
+ value = false;
+ return true;
+ }
+
+ return false;
+}
+
+Logger CSettingInt::s_logger;
+
+CSettingInt::CSettingInt(const std::string& id, CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingInt(id, DefaultLabel, DefaultValue, settingsManager)
+{ }
+
+CSettingInt::CSettingInt(const std::string& id, const CSettingInt& setting)
+ : CSettingInt(id, setting.m_settingsManager)
+{
+ copy(setting);
+}
+
+CSettingInt::CSettingInt(const std::string& id,
+ int label,
+ int value,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingInt(id, label, value, DefaultMin, DefaultStep, DefaultMax, settingsManager)
+{
+ SetLabel(label);
+}
+
+CSettingInt::CSettingInt(const std::string& id,
+ int label,
+ int value,
+ int minimum,
+ int step,
+ int maximum,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CTraitedSetting(id, settingsManager),
+ m_value(value),
+ m_default(value),
+ m_min(minimum),
+ m_step(step),
+ m_max(maximum)
+{
+ SetLabel(label);
+
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingInt");
+}
+
+CSettingInt::CSettingInt(const std::string& id,
+ int label,
+ int value,
+ const TranslatableIntegerSettingOptions& options,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingInt(id, label, value, settingsManager)
+{
+ SetTranslatableOptions(options);
+}
+
+SettingPtr CSettingInt::Clone(const std::string &id) const
+{
+ return std::make_shared<CSettingInt>(id, *this);
+}
+
+void CSettingInt::MergeDetails(const CSetting& other)
+{
+ if (other.GetType() != SettingType::Integer)
+ return;
+
+ const auto& intSetting = static_cast<const CSettingInt&>(other);
+ if (m_default == 0.0 && intSetting.m_default != 0.0)
+ m_default = intSetting.m_default;
+ if (m_value == m_default && intSetting.m_value != m_default)
+ m_value = intSetting.m_value;
+ if (m_min == 0.0 && intSetting.m_min != 0.0)
+ m_min = intSetting.m_min;
+ if (m_step == 1.0 && intSetting.m_step != 1.0)
+ m_step = intSetting.m_step;
+ if (m_max == 0.0 && intSetting.m_max != 0.0)
+ m_max = intSetting.m_max;
+ if (m_translatableOptions.empty() && !intSetting.m_translatableOptions.empty())
+ m_translatableOptions = intSetting.m_translatableOptions;
+ if (m_options.empty() && !intSetting.m_options.empty())
+ m_options = intSetting.m_options;
+ if (m_optionsFillerName.empty() && !intSetting.m_optionsFillerName.empty())
+ m_optionsFillerName = intSetting.m_optionsFillerName;
+ if (m_optionsFiller == nullptr && intSetting.m_optionsFiller != nullptr)
+ m_optionsFiller = intSetting.m_optionsFiller;
+ if (m_optionsFillerData == nullptr && intSetting.m_optionsFillerData != nullptr)
+ m_optionsFillerData = intSetting.m_optionsFillerData;
+ if (m_dynamicOptions.empty() && !intSetting.m_dynamicOptions.empty())
+ m_dynamicOptions = intSetting.m_dynamicOptions;
+ if (m_optionsSort == SettingOptionsSort::NoSorting &&
+ intSetting.m_optionsSort != SettingOptionsSort::NoSorting)
+ m_optionsSort = intSetting.m_optionsSort;
+}
+
+bool CSettingInt::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if (!CSetting::Deserialize(node, update))
+ return false;
+
+ // get the default value
+ int value;
+ if (XMLUtils::GetInt(node, SETTING_XML_ELM_DEFAULT, value))
+ m_value = m_default = value;
+ else if (!update)
+ {
+ s_logger->error("error reading the default value of \"{}\"", m_id);
+ return false;
+ }
+
+ auto constraints = node->FirstChild(SETTING_XML_ELM_CONSTRAINTS);
+ if (constraints != nullptr)
+ {
+ // get the entries
+ auto options = constraints->FirstChildElement(SETTING_XML_ELM_OPTIONS);
+ if (options != nullptr && options->FirstChild() != nullptr)
+ {
+ if (!DeserializeOptionsSort(options, m_optionsSort))
+ s_logger->warn("invalid \"sort\" attribute of <" SETTING_XML_ELM_OPTIONS "> for \"{}\"",
+ m_id);
+
+ if (options->FirstChild()->Type() == TiXmlNode::TINYXML_TEXT)
+ {
+ m_optionsFillerName = options->FirstChild()->ValueStr();
+ if (!m_optionsFillerName.empty())
+ {
+ m_optionsFiller = reinterpret_cast<IntegerSettingOptionsFiller>(m_settingsManager->GetSettingOptionsFiller(shared_from_base<CSettingInt>()));
+ }
+ }
+ else
+ {
+ m_translatableOptions.clear();
+ auto optionElement = options->FirstChildElement(SETTING_XML_ELM_OPTION);
+ while (optionElement != nullptr)
+ {
+ TranslatableIntegerSettingOption entry;
+ if (optionElement->QueryIntAttribute(SETTING_XML_ATTR_LABEL, &entry.label) ==
+ TIXML_SUCCESS &&
+ entry.label > 0)
+ {
+ entry.value = strtol(optionElement->FirstChild()->Value(), nullptr, 10);
+ m_translatableOptions.push_back(entry);
+ }
+ else
+ {
+ std::string label;
+ if (optionElement->QueryStringAttribute(SETTING_XML_ATTR_LABEL, &label) ==
+ TIXML_SUCCESS)
+ {
+ int value = strtol(optionElement->FirstChild()->Value(), nullptr, 10);
+ m_options.emplace_back(label, value);
+ }
+ }
+
+ optionElement = optionElement->NextSiblingElement(SETTING_XML_ELM_OPTION);
+ }
+ }
+ }
+
+ // get minimum
+ XMLUtils::GetInt(constraints, SETTING_XML_ELM_MINIMUM, m_min);
+ // get step
+ XMLUtils::GetInt(constraints, SETTING_XML_ELM_STEP, m_step);
+ // get maximum
+ XMLUtils::GetInt(constraints, SETTING_XML_ELM_MAXIMUM, m_max);
+ }
+
+ return true;
+}
+
+bool CSettingInt::FromString(const std::string &value)
+{
+ int iValue;
+ if (!fromString(value, iValue))
+ return false;
+
+ return SetValue(iValue);
+}
+
+std::string CSettingInt::ToString() const
+{
+ std::ostringstream oss;
+ oss << m_value;
+
+ return oss.str();
+}
+
+bool CSettingInt::Equals(const std::string &value) const
+{
+ int iValue;
+ return (fromString(value, iValue) && m_value == iValue);
+}
+
+bool CSettingInt::CheckValidity(const std::string &value) const
+{
+ int iValue;
+ if (!fromString(value, iValue))
+ return false;
+
+ return CheckValidity(iValue);
+}
+
+bool CSettingInt::CheckValidity(int value) const
+{
+ if (!m_translatableOptions.empty())
+ {
+ if (!CheckSettingOptionsValidity(value, m_translatableOptions))
+ return false;
+ }
+ else if (!m_options.empty())
+ {
+ if (!CheckSettingOptionsValidity(value, m_options))
+ return false;
+ }
+ else if (m_optionsFillerName.empty() && m_optionsFiller == nullptr &&
+ m_min != m_max && (value < m_min || value > m_max))
+ return false;
+
+ return true;
+}
+
+bool CSettingInt::SetValue(int value)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if (value == m_value)
+ return true;
+
+ if (!CheckValidity(value))
+ return false;
+
+ int oldValue = m_value;
+ m_value = value;
+
+ if (!OnSettingChanging(shared_from_base<CSettingInt>()))
+ {
+ m_value = oldValue;
+
+ // the setting couldn't be changed because one of the
+ // callback handlers failed the OnSettingChanging()
+ // callback so we need to let all the callback handlers
+ // know that the setting hasn't changed
+ OnSettingChanging(shared_from_base<CSettingInt>());
+ return false;
+ }
+
+ m_changed = m_value != m_default;
+ OnSettingChanged(shared_from_base<CSettingInt>());
+ return true;
+}
+
+void CSettingInt::SetDefault(int value)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ m_default = value;
+ if (!m_changed)
+ m_value = m_default;
+}
+
+SettingOptionsType CSettingInt::GetOptionsType() const
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ if (!m_translatableOptions.empty())
+ return SettingOptionsType::StaticTranslatable;
+ if (!m_options.empty())
+ return SettingOptionsType::Static;
+ if (!m_optionsFillerName.empty() || m_optionsFiller != nullptr)
+ return SettingOptionsType::Dynamic;
+
+ return SettingOptionsType::Unknown;
+}
+
+IntegerSettingOptions CSettingInt::UpdateDynamicOptions()
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+ IntegerSettingOptions options;
+ if (m_optionsFiller == nullptr &&
+ (m_optionsFillerName.empty() || m_settingsManager == nullptr))
+ return options;
+
+ if (m_optionsFiller == nullptr)
+ {
+ m_optionsFiller = reinterpret_cast<IntegerSettingOptionsFiller>(m_settingsManager->GetSettingOptionsFiller(shared_from_base<CSettingInt>()));
+ if (m_optionsFiller == nullptr)
+ {
+ s_logger->warn("unknown options filler \"{}\" of \"{}\"", m_optionsFillerName, m_id);
+ return options;
+ }
+ }
+
+ int bestMatchingValue = m_value;
+ m_optionsFiller(shared_from_base<CSettingInt>(), options, bestMatchingValue, m_optionsFillerData);
+
+ if (bestMatchingValue != m_value)
+ SetValue(bestMatchingValue);
+
+ bool changed = m_dynamicOptions.size() != options.size();
+ if (!changed)
+ {
+ for (size_t index = 0; index < options.size(); index++)
+ {
+ if (options[index].label.compare(m_dynamicOptions[index].label) != 0 ||
+ options[index].value != m_dynamicOptions[index].value)
+ {
+ changed = true;
+ break;
+ }
+ }
+ }
+
+ if (changed)
+ {
+ m_dynamicOptions = options;
+ OnSettingPropertyChanged(shared_from_base<CSettingInt>(), "options");
+ }
+
+ return options;
+}
+
+void CSettingInt::copy(const CSettingInt &setting)
+{
+ CSetting::Copy(setting);
+
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ m_value = setting.m_value;
+ m_default = setting.m_default;
+ m_min = setting.m_min;
+ m_step = setting.m_step;
+ m_max = setting.m_max;
+ m_translatableOptions = setting.m_translatableOptions;
+ m_options = setting.m_options;
+ m_optionsFillerName = setting.m_optionsFillerName;
+ m_optionsFiller = setting.m_optionsFiller;
+ m_optionsFillerData = setting.m_optionsFillerData;
+ m_dynamicOptions = setting.m_dynamicOptions;
+}
+
+bool CSettingInt::fromString(const std::string &strValue, int &value)
+{
+ if (strValue.empty())
+ return false;
+
+ char *end = nullptr;
+ value = (int)strtol(strValue.c_str(), &end, 10);
+ if (end != nullptr && *end != '\0')
+ return false;
+
+ return true;
+}
+
+Logger CSettingNumber::s_logger;
+
+CSettingNumber::CSettingNumber(const std::string& id,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingNumber(id, DefaultLabel, DefaultValue, settingsManager)
+{ }
+
+CSettingNumber::CSettingNumber(const std::string& id, const CSettingNumber& setting)
+ : CSettingNumber(id, setting.m_settingsManager)
+{
+ copy(setting);
+}
+
+CSettingNumber::CSettingNumber(const std::string& id,
+ int label,
+ float value,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingNumber(id, label, value, DefaultMin, DefaultStep, DefaultMax, settingsManager)
+{
+}
+
+CSettingNumber::CSettingNumber(const std::string& id,
+ int label,
+ float value,
+ float minimum,
+ float step,
+ float maximum,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CTraitedSetting(id, settingsManager),
+ m_value(static_cast<double>(value)),
+ m_default(static_cast<double>(value)),
+ m_min(static_cast<double>(minimum)),
+ m_step(static_cast<double>(step)),
+ m_max(static_cast<double>(maximum))
+{
+ SetLabel(label);
+
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingNumber");
+}
+
+SettingPtr CSettingNumber::Clone(const std::string &id) const
+{
+ return std::make_shared<CSettingNumber>(id, *this);
+}
+
+void CSettingNumber::MergeDetails(const CSetting& other)
+{
+ if (other.GetType() != SettingType::Number)
+ return;
+
+ const auto& numberSetting = static_cast<const CSettingNumber&>(other);
+ if (m_default == 0.0 && numberSetting.m_default != 0.0)
+ m_default = numberSetting.m_default;
+ if (m_value == m_default && numberSetting.m_value != m_default)
+ m_value = numberSetting.m_value;
+ if (m_min == 0.0 && numberSetting.m_min != 0.0)
+ m_min = numberSetting.m_min;
+ if (m_step == 1.0 && numberSetting.m_step != 1.0)
+ m_step = numberSetting.m_step;
+ if (m_max == 0.0 && numberSetting.m_max != 0.0)
+ m_max = numberSetting.m_max;
+}
+
+bool CSettingNumber::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if (!CSetting::Deserialize(node, update))
+ return false;
+
+ // get the default value
+ double value;
+ if (XMLUtils::GetDouble(node, SETTING_XML_ELM_DEFAULT, value))
+ m_value = m_default = value;
+ else if (!update)
+ {
+ s_logger->error("error reading the default value of \"{}\"", m_id);
+ return false;
+ }
+
+ auto constraints = node->FirstChild(SETTING_XML_ELM_CONSTRAINTS);
+ if (constraints != nullptr)
+ {
+ // get the minimum value
+ XMLUtils::GetDouble(constraints, SETTING_XML_ELM_MINIMUM, m_min);
+ // get the step value
+ XMLUtils::GetDouble(constraints, SETTING_XML_ELM_STEP, m_step);
+ // get the maximum value
+ XMLUtils::GetDouble(constraints, SETTING_XML_ELM_MAXIMUM, m_max);
+ }
+
+ return true;
+}
+
+bool CSettingNumber::FromString(const std::string &value)
+{
+ double dValue;
+ if (!fromString(value, dValue))
+ return false;
+
+ return SetValue(dValue);
+}
+
+std::string CSettingNumber::ToString() const
+{
+ std::ostringstream oss;
+ oss << m_value;
+
+ return oss.str();
+}
+
+bool CSettingNumber::Equals(const std::string &value) const
+{
+ double dValue;
+ std::shared_lock<CSharedSection> lock(m_critical);
+ return (fromString(value, dValue) && m_value == dValue);
+}
+
+bool CSettingNumber::CheckValidity(const std::string &value) const
+{
+ double dValue;
+ if (!fromString(value, dValue))
+ return false;
+
+ return CheckValidity(dValue);
+}
+
+bool CSettingNumber::CheckValidity(double value) const
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ if (m_min != m_max &&
+ (value < m_min || value > m_max))
+ return false;
+
+ return true;
+}
+
+bool CSettingNumber::SetValue(double value)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if (value == m_value)
+ return true;
+
+ if (!CheckValidity(value))
+ return false;
+
+ double oldValue = m_value;
+ m_value = value;
+
+ if (!OnSettingChanging(shared_from_base<CSettingNumber>()))
+ {
+ m_value = oldValue;
+
+ // the setting couldn't be changed because one of the
+ // callback handlers failed the OnSettingChanging()
+ // callback so we need to let all the callback handlers
+ // know that the setting hasn't changed
+ OnSettingChanging(shared_from_base<CSettingNumber>());
+ return false;
+ }
+
+ m_changed = m_value != m_default;
+ OnSettingChanged(shared_from_base<CSettingNumber>());
+ return true;
+}
+
+void CSettingNumber::SetDefault(double value)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ m_default = value;
+ if (!m_changed)
+ m_value = m_default;
+}
+
+void CSettingNumber::copy(const CSettingNumber &setting)
+{
+ CSetting::Copy(setting);
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ m_value = setting.m_value;
+ m_default = setting.m_default;
+ m_min = setting.m_min;
+ m_step = setting.m_step;
+ m_max = setting.m_max;
+}
+
+bool CSettingNumber::fromString(const std::string &strValue, double &value)
+{
+ if (strValue.empty())
+ return false;
+
+ char *end = nullptr;
+ value = strtod(strValue.c_str(), &end);
+ if (end != nullptr && *end != '\0')
+ return false;
+
+ return true;
+}
+
+const CSettingString::Value CSettingString::DefaultValue;
+Logger CSettingString::s_logger;
+
+CSettingString::CSettingString(const std::string& id,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingString(id, DefaultLabel, DefaultValue, settingsManager)
+{ }
+
+CSettingString::CSettingString(const std::string& id, const CSettingString& setting)
+ : CSettingString(id, setting.m_settingsManager)
+{
+ copy(setting);
+}
+
+CSettingString::CSettingString(const std::string& id,
+ int label,
+ const std::string& value,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CTraitedSetting(id, settingsManager), m_value(value), m_default(value)
+{
+ SetLabel(label);
+
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingString");
+}
+
+SettingPtr CSettingString::Clone(const std::string &id) const
+{
+ return std::make_shared<CSettingString>(id, *this);
+}
+
+void CSettingString::MergeDetails(const CSetting& other)
+{
+ if (other.GetType() != SettingType::String)
+ return;
+
+ const auto& stringSetting = static_cast<const CSettingString&>(other);
+ if (m_default.empty() && !stringSetting.m_default.empty())
+ m_default = stringSetting.m_default;
+ if (m_value == m_default && stringSetting.m_value != m_default)
+ m_value = stringSetting.m_value;
+ if (m_allowEmpty == false && stringSetting.m_allowEmpty == true)
+ m_allowEmpty = stringSetting.m_allowEmpty;
+ if (m_allowNewOption == false && stringSetting.m_allowNewOption == true)
+ m_allowNewOption = stringSetting.m_allowNewOption;
+ if (m_translatableOptions.empty() && !stringSetting.m_translatableOptions.empty())
+ m_translatableOptions = stringSetting.m_translatableOptions;
+ if (m_options.empty() && !stringSetting.m_options.empty())
+ m_options = stringSetting.m_options;
+ if (m_optionsFillerName.empty() && !stringSetting.m_optionsFillerName.empty())
+ m_optionsFillerName = stringSetting.m_optionsFillerName;
+ if (m_optionsFiller == nullptr && stringSetting.m_optionsFiller != nullptr)
+ m_optionsFiller = stringSetting.m_optionsFiller;
+ if (m_optionsFillerData == nullptr && stringSetting.m_optionsFillerData != nullptr)
+ m_optionsFillerData = stringSetting.m_optionsFillerData;
+ if (m_dynamicOptions.empty() && !stringSetting.m_dynamicOptions.empty())
+ m_dynamicOptions = stringSetting.m_dynamicOptions;
+ if (m_optionsSort == SettingOptionsSort::NoSorting &&
+ stringSetting.m_optionsSort != SettingOptionsSort::NoSorting)
+ m_optionsSort = stringSetting.m_optionsSort;
+}
+
+bool CSettingString::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if (!CSetting::Deserialize(node, update))
+ return false;
+
+ auto constraints = node->FirstChild(SETTING_XML_ELM_CONSTRAINTS);
+ if (constraints != nullptr)
+ {
+ // get allowempty (needs to be parsed before parsing the default value)
+ XMLUtils::GetBoolean(constraints, SETTING_XML_ELM_ALLOWEMPTY, m_allowEmpty);
+
+ // Values other than those in options constraints allowed to be added
+ XMLUtils::GetBoolean(constraints, SETTING_XML_ELM_ALLOWNEWOPTION, m_allowNewOption);
+
+ // get the entries
+ auto options = constraints->FirstChildElement(SETTING_XML_ELM_OPTIONS);
+ if (options != nullptr && options->FirstChild() != nullptr)
+ {
+ if (!DeserializeOptionsSort(options, m_optionsSort))
+ s_logger->warn("invalid \"sort\" attribute of <" SETTING_XML_ELM_OPTIONS "> for \"{}\"",
+ m_id);
+
+ if (options->FirstChild()->Type() == TiXmlNode::TINYXML_TEXT)
+ {
+ m_optionsFillerName = options->FirstChild()->ValueStr();
+ if (!m_optionsFillerName.empty())
+ {
+ m_optionsFiller = reinterpret_cast<StringSettingOptionsFiller>(m_settingsManager->GetSettingOptionsFiller(shared_from_base<CSettingString>()));
+ }
+ }
+ else
+ {
+ m_translatableOptions.clear();
+ auto optionElement = options->FirstChildElement(SETTING_XML_ELM_OPTION);
+ while (optionElement != nullptr)
+ {
+ TranslatableStringSettingOption entry;
+ if (optionElement->QueryIntAttribute(SETTING_XML_ATTR_LABEL, &entry.first) == TIXML_SUCCESS && entry.first > 0)
+ {
+ entry.second = optionElement->FirstChild()->Value();
+ m_translatableOptions.push_back(entry);
+ }
+ else
+ {
+ const std::string value = optionElement->FirstChild()->Value();
+ // if a specific "label" attribute is present use it otherwise use the value as label
+ std::string label = value;
+ optionElement->QueryStringAttribute(SETTING_XML_ATTR_LABEL, &label);
+
+ m_options.emplace_back(label, value);
+ }
+
+ optionElement = optionElement->NextSiblingElement(SETTING_XML_ELM_OPTION);
+ }
+ }
+ }
+ }
+
+ // get the default value
+ std::string value;
+ if (XMLUtils::GetString(node, SETTING_XML_ELM_DEFAULT, value) &&
+ (!value.empty() || m_allowEmpty))
+ m_value = m_default = value;
+ else if (!update && !m_allowEmpty)
+ {
+ s_logger->error("error reading the default value of \"{}\"", m_id);
+ return false;
+ }
+
+ return true;
+}
+
+bool CSettingString::CheckValidity(const std::string &value) const
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ if (!m_allowEmpty && value.empty())
+ return false;
+
+ if (!m_translatableOptions.empty())
+ {
+ if (!CheckSettingOptionsValidity(value, m_translatableOptions))
+ return false;
+ }
+ else if (!m_options.empty() && !m_allowNewOption)
+ {
+ if (!CheckSettingOptionsValidity(value, m_options))
+ return false;
+ }
+
+ return true;
+}
+
+bool CSettingString::SetValue(const std::string &value)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+
+ if (value == m_value)
+ return true;
+
+ if (!CheckValidity(value))
+ return false;
+
+ std::string oldValue = m_value;
+ m_value = value;
+
+ if (!OnSettingChanging(shared_from_base<CSettingString>()))
+ {
+ m_value = oldValue;
+
+ // the setting couldn't be changed because one of the
+ // callback handlers failed the OnSettingChanging()
+ // callback so we need to let all the callback handlers
+ // know that the setting hasn't changed
+ OnSettingChanging(shared_from_base<CSettingString>());
+ return false;
+ }
+
+ m_changed = m_value != m_default;
+ OnSettingChanged(shared_from_base<CSettingString>());
+ return true;
+}
+
+void CSettingString::SetDefault(const std::string &value)
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+
+ m_default = value;
+ if (!m_changed)
+ m_value = m_default;
+}
+
+SettingOptionsType CSettingString::GetOptionsType() const
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ if (!m_translatableOptions.empty())
+ return SettingOptionsType::StaticTranslatable;
+ if (!m_options.empty())
+ return SettingOptionsType::Static;
+ if (!m_optionsFillerName.empty() || m_optionsFiller != nullptr)
+ return SettingOptionsType::Dynamic;
+
+ return SettingOptionsType::Unknown;
+}
+
+StringSettingOptions CSettingString::UpdateDynamicOptions()
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+ StringSettingOptions options;
+ if (m_optionsFiller == nullptr &&
+ (m_optionsFillerName.empty() || m_settingsManager == nullptr))
+ return options;
+
+ if (m_optionsFiller == nullptr)
+ {
+ m_optionsFiller = reinterpret_cast<StringSettingOptionsFiller>(m_settingsManager->GetSettingOptionsFiller(shared_from_base<CSettingString>()));
+ if (m_optionsFiller == nullptr)
+ {
+ s_logger->error("unknown options filler \"{}\" of \"{}\"", m_optionsFillerName, m_id);
+ return options;
+ }
+ }
+
+ std::string bestMatchingValue = m_value;
+ m_optionsFiller(shared_from_base<CSettingString>(), options, bestMatchingValue, m_optionsFillerData);
+
+ if (bestMatchingValue != m_value)
+ SetValue(bestMatchingValue);
+
+ // check if the list of items has changed
+ bool changed = m_dynamicOptions.size() != options.size();
+ if (!changed)
+ {
+ for (size_t index = 0; index < options.size(); index++)
+ {
+ if (options[index].label.compare(m_dynamicOptions[index].label) != 0 ||
+ options[index].value.compare(m_dynamicOptions[index].value) != 0)
+ {
+ changed = true;
+ break;
+ }
+ }
+ }
+
+ if (changed)
+ {
+ m_dynamicOptions = options;
+ OnSettingPropertyChanged(shared_from_base<CSettingString>(), "options");
+ }
+
+ return options;
+}
+
+void CSettingString::copy(const CSettingString &setting)
+{
+ CSetting::Copy(setting);
+
+ std::unique_lock<CSharedSection> lock(m_critical);
+ m_value = setting.m_value;
+ m_default = setting.m_default;
+ m_allowEmpty = setting.m_allowEmpty;
+ m_allowNewOption = setting.m_allowNewOption;
+ m_translatableOptions = setting.m_translatableOptions;
+ m_options = setting.m_options;
+ m_optionsFillerName = setting.m_optionsFillerName;
+ m_optionsFiller = setting.m_optionsFiller;
+ m_optionsFillerData = setting.m_optionsFillerData;
+ m_dynamicOptions = setting.m_dynamicOptions;
+}
+
+Logger CSettingAction::s_logger;
+
+CSettingAction::CSettingAction(const std::string& id,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingAction(id, DefaultLabel, settingsManager)
+{ }
+
+CSettingAction::CSettingAction(const std::string& id,
+ int label,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSetting(id, settingsManager)
+{
+ SetLabel(label);
+
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingAction");
+}
+
+CSettingAction::CSettingAction(const std::string& id, const CSettingAction& setting)
+ : CSettingAction(id, setting.m_settingsManager)
+{
+ copy(setting);
+}
+
+SettingPtr CSettingAction::Clone(const std::string &id) const
+{
+ return std::make_shared<CSettingAction>(id, *this);
+}
+
+void CSettingAction::MergeDetails(const CSetting& other)
+{
+ if (other.GetType() != SettingType::Action)
+ return;
+
+ const auto& actionSetting = static_cast<const CSettingAction&>(other);
+ if (!HasData() && actionSetting.HasData())
+ SetData(actionSetting.GetData());
+}
+
+bool CSettingAction::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+
+ if (!CSetting::Deserialize(node, update))
+ return false;
+
+ m_data = XMLUtils::GetString(node, SETTING_XML_ELM_DATA);
+
+ return true;
+}
+
+void CSettingAction::copy(const CSettingAction& setting)
+{
+ CSetting::Copy(setting);
+
+ std::unique_lock<CSharedSection> lock(m_critical);
+ m_data = setting.m_data;
+}
diff --git a/xbmc/settings/lib/Setting.h b/xbmc/settings/lib/Setting.h
new file mode 100644
index 0000000..9e3ed3f
--- /dev/null
+++ b/xbmc/settings/lib/Setting.h
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2013-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 "ISetting.h"
+#include "ISettingCallback.h"
+#include "ISettingControl.h"
+#include "SettingDefinitions.h"
+#include "SettingDependency.h"
+#include "SettingLevel.h"
+#include "SettingType.h"
+#include "SettingUpdate.h"
+#include "threads/SharedSection.h"
+#include "utils/logtypes.h"
+
+#include <memory>
+#include <set>
+#include <shared_mutex>
+#include <string>
+#include <utility>
+#include <vector>
+
+enum class SettingOptionsType {
+ Unknown = 0,
+ StaticTranslatable,
+ Static,
+ Dynamic
+};
+
+class CSetting;
+using SettingPtr = std::shared_ptr<CSetting>;
+using SettingConstPtr = std::shared_ptr<const CSetting>;
+using SettingList = std::vector<SettingPtr>;
+
+/*!
+ \ingroup settings
+ \brief Setting base class containing all the properties which are common to
+ all settings independent of the setting type.
+ */
+class CSetting : public ISetting,
+ protected ISettingCallback,
+ public std::enable_shared_from_this<CSetting>
+{
+public:
+ CSetting(const std::string& id, CSettingsManager* settingsManager = nullptr);
+ CSetting(const std::string& id, const CSetting& setting);
+ ~CSetting() override = default;
+
+ virtual std::shared_ptr<CSetting> Clone(const std::string &id) const = 0;
+ void MergeBasics(const CSetting& other);
+ virtual void MergeDetails(const CSetting& other) = 0;
+
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ virtual SettingType GetType() const = 0;
+ virtual bool FromString(const std::string &value) = 0;
+ virtual std::string ToString() const = 0;
+ virtual bool Equals(const std::string &value) const = 0;
+ virtual bool CheckValidity(const std::string &value) const = 0;
+ virtual void Reset() = 0;
+
+ bool IsEnabled() const;
+ bool GetEnabled() const { return m_enabled; }
+ void SetEnabled(bool enabled);
+ bool IsDefault() const { return !m_changed; }
+ const std::string& GetParent() const { return m_parentSetting; }
+ void SetParent(const std::string& parentSetting) { m_parentSetting = parentSetting; }
+ SettingLevel GetLevel() const { return m_level; }
+ void SetLevel(SettingLevel level) { m_level = level; }
+ std::shared_ptr<const ISettingControl> GetControl() const { return m_control; }
+ std::shared_ptr<ISettingControl> GetControl() { return m_control; }
+ void SetControl(std::shared_ptr<ISettingControl> control) { m_control = std::move(control); }
+ const SettingDependencies& GetDependencies() const { return m_dependencies; }
+ void SetDependencies(const SettingDependencies &dependencies) { m_dependencies = dependencies; }
+ const std::set<CSettingUpdate>& GetUpdates() const { return m_updates; }
+
+ void SetCallback(ISettingCallback *callback) { m_callback = callback; }
+
+ bool IsReference() const { return !m_referencedId.empty(); }
+ const std::string& GetReferencedId() const { return m_referencedId; }
+ void SetReferencedId(const std::string& referencedId) { m_referencedId = referencedId; }
+ void MakeReference(const std::string& referencedId = "");
+
+ bool GetVisible() const { return ISetting::IsVisible(); }
+ // overrides of ISetting
+ bool IsVisible() const override;
+
+ // implementation of ISettingCallback
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ /*!
+ \brief Deserializes the given XML node to retrieve a setting object's identifier and
+ whether the setting is a reference to another setting or not.
+
+ \param node XML node containing a setting object's identifier
+ \param identification Will contain the deserialized setting object's identifier
+ \param isReference Whether the setting is a reference to the setting with the determined identifier
+ \return True if a setting object's identifier was deserialized, false otherwise
+ */
+ static bool DeserializeIdentification(const TiXmlNode* node,
+ std::string& identification,
+ bool& isReference);
+
+protected:
+ // implementation of ISettingCallback
+ bool OnSettingChanging(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ bool OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode) override;
+ void OnSettingPropertyChanged(const std::shared_ptr<const CSetting>& setting,
+ const char* propertyName) override;
+
+ void Copy(const CSetting &setting);
+
+ template<class TSetting>
+ std::shared_ptr<TSetting> shared_from_base()
+ {
+ return std::static_pointer_cast<TSetting>(shared_from_this());
+ }
+
+ ISettingCallback *m_callback = nullptr;
+ bool m_enabled = true;
+ std::string m_parentSetting;
+ SettingLevel m_level = SettingLevel::Standard;
+ std::shared_ptr<ISettingControl> m_control;
+ SettingDependencies m_dependencies;
+ std::set<CSettingUpdate> m_updates;
+ bool m_changed = false;
+ mutable CSharedSection m_critical;
+
+ std::string m_referencedId;
+
+private:
+ static Logger s_logger;
+};
+
+template<typename TValue, SettingType TSettingType>
+class CTraitedSetting : public CSetting
+{
+public:
+ typedef TValue Value;
+
+ // implementation of CSetting
+ SettingType GetType() const override { return TSettingType; }
+
+ static SettingType Type() { return TSettingType; }
+
+protected:
+ CTraitedSetting(const std::string& id, CSettingsManager* settingsManager = nullptr)
+ : CSetting(id, settingsManager)
+ { }
+ CTraitedSetting(const std::string& id, const CTraitedSetting& setting) : CSetting(id, setting) {}
+ ~CTraitedSetting() override = default;
+};
+
+/*!
+ \ingroup settings
+ \brief List setting implementation
+ \sa CSetting
+ */
+class CSettingList : public CSetting
+{
+public:
+ CSettingList(const std::string &id, std::shared_ptr<CSetting> settingDefinition, CSettingsManager *settingsManager = nullptr);
+ CSettingList(const std::string &id, std::shared_ptr<CSetting> settingDefinition, int label, CSettingsManager *settingsManager = nullptr);
+ CSettingList(const std::string &id, const CSettingList &setting);
+ ~CSettingList() override = default;
+
+ std::shared_ptr<CSetting> Clone(const std::string &id) const override;
+ void MergeDetails(const CSetting& other) override;
+
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ SettingType GetType() const override { return SettingType::List; }
+ bool FromString(const std::string &value) override;
+ std::string ToString() const override;
+ bool Equals(const std::string &value) const override;
+ bool CheckValidity(const std::string &value) const override;
+ void Reset() override;
+
+ SettingType GetElementType() const;
+ std::shared_ptr<CSetting> GetDefinition() { return m_definition; }
+ std::shared_ptr<const CSetting> GetDefinition() const { return m_definition; }
+ void SetDefinition(std::shared_ptr<CSetting> definition) { m_definition = std::move(definition); }
+
+ const std::string& GetDelimiter() const { return m_delimiter; }
+ void SetDelimiter(const std::string &delimiter) { m_delimiter = delimiter; }
+ int GetMinimumItems() const { return m_minimumItems; }
+ void SetMinimumItems(int minimumItems) { m_minimumItems = minimumItems; }
+ int GetMaximumItems() const { return m_maximumItems; }
+ void SetMaximumItems(int maximumItems) { m_maximumItems = maximumItems; }
+
+ bool FromString(const std::vector<std::string> &value);
+
+ const SettingList& GetValue() const { return m_values; }
+ bool SetValue(const SettingList &values);
+ const SettingList& GetDefault() const { return m_defaults; }
+ void SetDefault(const SettingList &values);
+
+protected:
+ void copy(const CSettingList &setting);
+ static void copy(const SettingList &srcValues, SettingList &dstValues);
+ bool fromString(const std::string &strValue, SettingList &values) const;
+ bool fromValues(const std::vector<std::string> &strValues, SettingList &values) const;
+ std::string toString(const SettingList &values) const;
+
+ SettingList m_values;
+ SettingList m_defaults;
+ std::shared_ptr<CSetting> m_definition;
+ std::string m_delimiter = "|";
+ int m_minimumItems = 0;
+ int m_maximumItems = -1;
+
+ static Logger s_logger;
+};
+
+/*!
+ \ingroup settings
+ \brief Boolean setting implementation.
+ \sa CSetting
+ */
+class CSettingBool : public CTraitedSetting<bool, SettingType::Boolean>
+{
+public:
+ CSettingBool(const std::string &id, CSettingsManager *settingsManager = nullptr);
+ CSettingBool(const std::string &id, const CSettingBool &setting);
+ CSettingBool(const std::string &id, int label, bool value, CSettingsManager *settingsManager = nullptr);
+ ~CSettingBool() override = default;
+
+ std::shared_ptr<CSetting> Clone(const std::string &id) const override;
+ void MergeDetails(const CSetting& other) override;
+
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ bool FromString(const std::string &value) override;
+ std::string ToString() const override;
+ bool Equals(const std::string &value) const override;
+ bool CheckValidity(const std::string &value) const override;
+ void Reset() override { SetValue(m_default); }
+
+ bool GetValue() const
+ {
+ std::shared_lock<CSharedSection> lock(m_critical);
+ return m_value;
+ }
+ bool SetValue(bool value);
+ bool GetDefault() const { return m_default; }
+ void SetDefault(bool value);
+
+private:
+ static constexpr Value DefaultValue = false;
+
+ void copy(const CSettingBool &setting);
+ bool fromString(const std::string &strValue, bool &value) const;
+
+ bool m_value = DefaultValue;
+ bool m_default = DefaultValue;
+
+ static Logger s_logger;
+};
+
+/*!
+ \ingroup settings
+ \brief Integer setting implementation
+ \sa CSetting
+ */
+class CSettingInt : public CTraitedSetting<int, SettingType::Integer>
+{
+public:
+ CSettingInt(const std::string &id, CSettingsManager *settingsManager = nullptr);
+ CSettingInt(const std::string &id, const CSettingInt &setting);
+ CSettingInt(const std::string &id, int label, int value, CSettingsManager *settingsManager = nullptr);
+ CSettingInt(const std::string &id, int label, int value, int minimum, int step, int maximum, CSettingsManager *settingsManager = nullptr);
+ CSettingInt(const std::string &id, int label, int value, const TranslatableIntegerSettingOptions &options, CSettingsManager *settingsManager = nullptr);
+ ~CSettingInt() override = default;
+
+ std::shared_ptr<CSetting> Clone(const std::string &id) const override;
+ void MergeDetails(const CSetting& other) override;
+
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ bool FromString(const std::string &value) override;
+ std::string ToString() const override;
+ bool Equals(const std::string &value) const override;
+ bool CheckValidity(const std::string &value) const override;
+ virtual bool CheckValidity(int value) const;
+ void Reset() override { SetValue(m_default); }
+
+ int GetValue() const
+ {
+ std::shared_lock<CSharedSection> lock(m_critical);
+ return m_value;
+ }
+ bool SetValue(int value);
+ int GetDefault() const { return m_default; }
+ void SetDefault(int value);
+
+ int GetMinimum() const { return m_min; }
+ void SetMinimum(int minimum) { m_min = minimum; }
+ int GetStep() const { return m_step; }
+ void SetStep(int step) { m_step = step; }
+ int GetMaximum() const { return m_max; }
+ void SetMaximum(int maximum) { m_max = maximum; }
+
+ SettingOptionsType GetOptionsType() const;
+ const TranslatableIntegerSettingOptions& GetTranslatableOptions() const { return m_translatableOptions; }
+ void SetTranslatableOptions(const TranslatableIntegerSettingOptions &options) { m_translatableOptions = options; }
+ const IntegerSettingOptions& GetOptions() const { return m_options; }
+ void SetOptions(const IntegerSettingOptions &options) { m_options = options; }
+ const std::string& GetOptionsFillerName() const { return m_optionsFillerName; }
+ void SetOptionsFillerName(const std::string &optionsFillerName, void *data = nullptr)
+ {
+ m_optionsFillerName = optionsFillerName;
+ m_optionsFillerData = data;
+ }
+ void SetOptionsFiller(IntegerSettingOptionsFiller optionsFiller, void *data = nullptr)
+ {
+ m_optionsFiller = optionsFiller;
+ m_optionsFillerData = data;
+ }
+ IntegerSettingOptions GetDynamicOptions() const { return m_dynamicOptions; }
+ IntegerSettingOptions UpdateDynamicOptions();
+ SettingOptionsSort GetOptionsSort() const { return m_optionsSort; }
+ void SetOptionsSort(SettingOptionsSort optionsSort) { m_optionsSort = optionsSort; }
+
+private:
+ static constexpr Value DefaultValue = 0;
+ static constexpr Value DefaultMin = DefaultValue;
+ static constexpr Value DefaultStep = 1;
+ static constexpr Value DefaultMax = DefaultValue;
+
+ void copy(const CSettingInt &setting);
+ static bool fromString(const std::string &strValue, int &value);
+
+ int m_value = DefaultValue;
+ int m_default = DefaultValue;
+ int m_min = DefaultMin;
+ int m_step = DefaultStep;
+ int m_max = DefaultMax;
+ TranslatableIntegerSettingOptions m_translatableOptions;
+ IntegerSettingOptions m_options;
+ std::string m_optionsFillerName;
+ IntegerSettingOptionsFiller m_optionsFiller = nullptr;
+ void *m_optionsFillerData = nullptr;
+ IntegerSettingOptions m_dynamicOptions;
+ SettingOptionsSort m_optionsSort = SettingOptionsSort::NoSorting;
+
+ static Logger s_logger;
+};
+
+/*!
+ \ingroup settings
+ \brief Real number setting implementation.
+ \sa CSetting
+ */
+class CSettingNumber : public CTraitedSetting<double, SettingType::Number>
+{
+public:
+ CSettingNumber(const std::string &id, CSettingsManager *settingsManager = nullptr);
+ CSettingNumber(const std::string &id, const CSettingNumber &setting);
+ CSettingNumber(const std::string &id, int label, float value, CSettingsManager *settingsManager = nullptr);
+ CSettingNumber(const std::string &id, int label, float value, float minimum, float step, float maximum, CSettingsManager *settingsManager = nullptr);
+ ~CSettingNumber() override = default;
+
+ std::shared_ptr<CSetting> Clone(const std::string &id) const override;
+ void MergeDetails(const CSetting& other) override;
+
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ bool FromString(const std::string &value) override;
+ std::string ToString() const override;
+ bool Equals(const std::string &value) const override;
+ bool CheckValidity(const std::string &value) const override;
+ virtual bool CheckValidity(double value) const;
+ void Reset() override { SetValue(m_default); }
+
+ double GetValue() const
+ {
+ std::shared_lock<CSharedSection> lock(m_critical);
+ return m_value;
+ }
+ bool SetValue(double value);
+ double GetDefault() const { return m_default; }
+ void SetDefault(double value);
+
+ double GetMinimum() const { return m_min; }
+ void SetMinimum(double minimum) { m_min = minimum; }
+ double GetStep() const { return m_step; }
+ void SetStep(double step) { m_step = step; }
+ double GetMaximum() const { return m_max; }
+ void SetMaximum(double maximum) { m_max = maximum; }
+
+private:
+ static constexpr Value DefaultValue = 0.0;
+ static constexpr Value DefaultMin = DefaultValue;
+ static constexpr Value DefaultStep = 1.0;
+ static constexpr Value DefaultMax = DefaultValue;
+
+ virtual void copy(const CSettingNumber &setting);
+ static bool fromString(const std::string &strValue, double &value);
+
+ double m_value = DefaultValue;
+ double m_default = DefaultValue;
+ double m_min = DefaultMin;
+ double m_step = DefaultStep;
+ double m_max = DefaultMax;
+
+ static Logger s_logger;
+};
+
+/*!
+ \ingroup settings
+ \brief String setting implementation.
+ \sa CSetting
+ */
+class CSettingString : public CTraitedSetting<std::string, SettingType::String>
+{
+public:
+ CSettingString(const std::string &id, CSettingsManager *settingsManager = nullptr);
+ CSettingString(const std::string &id, const CSettingString &setting);
+ CSettingString(const std::string &id, int label, const std::string &value, CSettingsManager *settingsManager = nullptr);
+ ~CSettingString() override = default;
+
+ std::shared_ptr<CSetting> Clone(const std::string &id) const override;
+ void MergeDetails(const CSetting& other) override;
+
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ bool FromString(const std::string &value) override { return SetValue(value); }
+ std::string ToString() const override { return m_value; }
+ bool Equals(const std::string &value) const override { return m_value == value; }
+ bool CheckValidity(const std::string &value) const override;
+ void Reset() override { SetValue(m_default); }
+
+ virtual const std::string& GetValue() const
+ {
+ std::shared_lock<CSharedSection> lock(m_critical);
+ return m_value;
+ }
+ virtual bool SetValue(const std::string &value);
+ virtual const std::string& GetDefault() const { return m_default; }
+ virtual void SetDefault(const std::string &value);
+
+ virtual bool AllowEmpty() const { return m_allowEmpty; }
+ void SetAllowEmpty(bool allowEmpty) { m_allowEmpty = allowEmpty; }
+ virtual bool AllowNewOption() const { return m_allowNewOption; }
+ void SetAllowNewOption(bool allowNewOption) { m_allowNewOption = allowNewOption; }
+
+ SettingOptionsType GetOptionsType() const;
+ const TranslatableStringSettingOptions& GetTranslatableOptions() const { return m_translatableOptions; }
+ void SetTranslatableOptions(const TranslatableStringSettingOptions &options) { m_translatableOptions = options; }
+ const StringSettingOptions& GetOptions() const { return m_options; }
+ void SetOptions(const StringSettingOptions &options) { m_options = options; }
+ const std::string& GetOptionsFillerName() const { return m_optionsFillerName; }
+ void SetOptionsFillerName(const std::string &optionsFillerName, void *data = nullptr)
+ {
+ m_optionsFillerName = optionsFillerName;
+ m_optionsFillerData = data;
+ }
+ void SetOptionsFiller(StringSettingOptionsFiller optionsFiller, void *data = nullptr)
+ {
+ m_optionsFiller = optionsFiller;
+ m_optionsFillerData = data;
+ }
+ StringSettingOptions GetDynamicOptions() const { return m_dynamicOptions; }
+ StringSettingOptions UpdateDynamicOptions();
+ SettingOptionsSort GetOptionsSort() const { return m_optionsSort; }
+ void SetOptionsSort(SettingOptionsSort optionsSort) { m_optionsSort = optionsSort; }
+
+protected:
+ static const Value DefaultValue;
+
+ virtual void copy(const CSettingString &setting);
+
+ std::string m_value;
+ std::string m_default;
+ bool m_allowEmpty = false;
+ bool m_allowNewOption = false;
+ TranslatableStringSettingOptions m_translatableOptions;
+ StringSettingOptions m_options;
+ std::string m_optionsFillerName;
+ StringSettingOptionsFiller m_optionsFiller = nullptr;
+ void *m_optionsFillerData = nullptr;
+ StringSettingOptions m_dynamicOptions;
+ SettingOptionsSort m_optionsSort = SettingOptionsSort::NoSorting;
+
+ static Logger s_logger;
+};
+
+/*!
+ \ingroup settings
+ \brief Action setting implementation.
+
+ A setting action will trigger a call to the OnSettingAction() callback method
+ when activated.
+
+ \sa CSetting
+ */
+class CSettingAction : public CSetting
+{
+public:
+ CSettingAction(const std::string &id, CSettingsManager *settingsManager = nullptr);
+ CSettingAction(const std::string &id, int label, CSettingsManager *settingsManager = nullptr);
+ CSettingAction(const std::string &id, const CSettingAction &setting);
+ ~CSettingAction() override = default;
+
+ std::shared_ptr<CSetting> Clone(const std::string &id) const override;
+ void MergeDetails(const CSetting& other) override;
+
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ SettingType GetType() const override { return SettingType::Action; }
+ bool FromString(const std::string &value) override { return CheckValidity(value); }
+ std::string ToString() const override { return ""; }
+ bool Equals(const std::string &value) const override { return value.empty(); }
+ bool CheckValidity(const std::string &value) const override { return value.empty(); }
+ void Reset() override { }
+
+ bool HasData() const { return !m_data.empty(); }
+ const std::string& GetData() const { return m_data; }
+ void SetData(const std::string& data) { m_data = data; }
+
+protected:
+ virtual void copy(const CSettingAction& setting);
+
+ std::string m_data;
+
+ static Logger s_logger;
+};
diff --git a/xbmc/settings/lib/SettingCategoryAccess.cpp b/xbmc/settings/lib/SettingCategoryAccess.cpp
new file mode 100644
index 0000000..4640586
--- /dev/null
+++ b/xbmc/settings/lib/SettingCategoryAccess.cpp
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013-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 "SettingCategoryAccess.h"
+
+#include "SettingConditions.h"
+#include "SettingsManager.h"
+
+bool CSettingCategoryAccessCondition::Check() const
+{
+ if (m_value.empty())
+ return true;
+
+ if (m_settingsManager == nullptr)
+ return false;
+
+ bool found = m_settingsManager->GetConditions().Check(m_value, "true");
+ if (m_negated)
+ return !found;
+
+ return found;
+}
+
+bool CSettingCategoryAccessConditionCombination::Check() const
+{
+ if (m_operations.empty() && m_values.empty())
+ return true;
+
+ return CSettingConditionCombination::Check();
+}
+
+CSettingCategoryAccess::CSettingCategoryAccess(CSettingsManager *settingsManager /* = nullptr */)
+ : CSettingCondition(settingsManager)
+{
+ m_operation = CBooleanLogicOperationPtr(new CSettingCategoryAccessConditionCombination(m_settingsManager));
+}
diff --git a/xbmc/settings/lib/SettingCategoryAccess.h b/xbmc/settings/lib/SettingCategoryAccess.h
new file mode 100644
index 0000000..5d4ceb8
--- /dev/null
+++ b/xbmc/settings/lib/SettingCategoryAccess.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013-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 "SettingConditions.h"
+
+#include <set>
+#include <string>
+
+class CSettingCategoryAccessCondition : public CSettingConditionItem
+{
+public:
+ explicit CSettingCategoryAccessCondition(CSettingsManager *settingsManager = nullptr)
+ : CSettingConditionItem(settingsManager)
+ { }
+ ~CSettingCategoryAccessCondition() override = default;
+
+ bool Check() const override;
+};
+
+class CSettingCategoryAccessConditionCombination : public CSettingConditionCombination
+{
+public:
+ explicit CSettingCategoryAccessConditionCombination(CSettingsManager *settingsManager = nullptr)
+ : CSettingConditionCombination(settingsManager)
+ { }
+ ~CSettingCategoryAccessConditionCombination() override = default;
+
+ bool Check() const override;
+
+private:
+ CBooleanLogicOperation* newOperation() override { return new CSettingCategoryAccessConditionCombination(m_settingsManager); }
+ CBooleanLogicValue* newValue() override { return new CSettingCategoryAccessCondition(m_settingsManager); }
+};
+
+class CSettingCategoryAccess : public CSettingCondition
+{
+public:
+ explicit CSettingCategoryAccess(CSettingsManager *settingsManager = nullptr);
+ ~CSettingCategoryAccess() override = default;
+};
diff --git a/xbmc/settings/lib/SettingConditions.cpp b/xbmc/settings/lib/SettingConditions.cpp
new file mode 100644
index 0000000..7146834
--- /dev/null
+++ b/xbmc/settings/lib/SettingConditions.cpp
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2013-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 "SettingConditions.h"
+
+#include "SettingDefinitions.h"
+#include "SettingsManager.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+
+bool CSettingConditionItem::Deserialize(const TiXmlNode *node)
+{
+ if (!CBooleanLogicValue::Deserialize(node))
+ return false;
+
+ auto elem = node->ToElement();
+ if (elem == nullptr)
+ return false;
+
+ // get the "name" attribute
+ auto strAttribute = elem->Attribute(SETTING_XML_ATTR_NAME);
+ if (strAttribute != nullptr)
+ m_name = strAttribute;
+
+ // get the "setting" attribute
+ strAttribute = elem->Attribute(SETTING_XML_ATTR_SETTING);
+ if (strAttribute != nullptr)
+ m_setting = strAttribute;
+
+ return true;
+}
+
+bool CSettingConditionItem::Check() const
+{
+ if (m_settingsManager == nullptr)
+ return false;
+
+ return m_settingsManager->GetConditions().Check(m_name, m_value, m_settingsManager->GetSetting(m_setting)) == !m_negated;
+}
+
+bool CSettingConditionCombination::Check() const
+{
+ bool ok = false;
+ for (const auto& operation : m_operations)
+ {
+ if (operation == nullptr)
+ continue;
+
+ const auto combination = std::static_pointer_cast<const CSettingConditionCombination>(operation);
+ if (combination == nullptr)
+ continue;
+
+ if (combination->Check())
+ ok = true;
+ else if (m_operation == BooleanLogicOperationAnd)
+ return false;
+ }
+
+ for (const auto& value : m_values)
+ {
+ if (value == nullptr)
+ continue;
+
+ const auto condition = std::static_pointer_cast<const CSettingConditionItem>(value);
+ if (condition == nullptr)
+ continue;
+
+ if (condition->Check())
+ ok = true;
+ else if (m_operation == BooleanLogicOperationAnd)
+ return false;
+ }
+
+ return ok;
+}
+
+CSettingCondition::CSettingCondition(CSettingsManager *settingsManager /* = nullptr */)
+ : ISettingCondition(settingsManager)
+{
+ m_operation = CBooleanLogicOperationPtr(new CSettingConditionCombination(settingsManager));
+}
+
+bool CSettingCondition::Check() const
+{
+ auto combination = std::static_pointer_cast<CSettingConditionCombination>(m_operation);
+ if (combination == nullptr)
+ return false;
+
+ return combination->Check();
+}
+
+void CSettingConditionsManager::AddCondition(std::string condition)
+{
+ if (condition.empty())
+ return;
+
+ StringUtils::ToLower(condition);
+
+ m_defines.insert(condition);
+}
+
+void CSettingConditionsManager::AddDynamicCondition(std::string identifier, SettingConditionCheck condition, void *data /*= nullptr*/)
+{
+ if (identifier.empty() || condition == nullptr)
+ return;
+
+ StringUtils::ToLower(identifier);
+
+ m_conditions.emplace(identifier, std::make_pair(condition, data));
+}
+
+void CSettingConditionsManager::RemoveDynamicCondition(std::string identifier)
+{
+ if (identifier.empty())
+ return;
+
+ StringUtils::ToLower(identifier);
+
+ auto it = m_conditions.find(identifier);
+ if (it != m_conditions.end())
+ m_conditions.erase(it);
+}
+
+bool CSettingConditionsManager::Check(
+ std::string condition,
+ const std::string& value /* = "" */,
+ const std::shared_ptr<const CSetting>& setting /* = nullptr */) const
+{
+ if (condition.empty())
+ return false;
+
+ StringUtils::ToLower(condition);
+
+ // special handling of "isdefined" conditions
+ if (condition == "isdefined")
+ {
+ std::string tmpValue = value;
+ StringUtils::ToLower(tmpValue);
+
+ return m_defines.find(tmpValue) != m_defines.end();
+ }
+
+ auto conditionIt = m_conditions.find(condition);
+ if (conditionIt == m_conditions.end())
+ return false;
+
+ return conditionIt->second.first(condition, value, setting, conditionIt->second.second);
+}
diff --git a/xbmc/settings/lib/SettingConditions.h b/xbmc/settings/lib/SettingConditions.h
new file mode 100644
index 0000000..aee2c06
--- /dev/null
+++ b/xbmc/settings/lib/SettingConditions.h
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2013-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 "SettingDefinitions.h"
+#include "utils/BooleanLogic.h"
+
+#include <map>
+#include <set>
+#include <string>
+#include <utility>
+
+class CSettingsManager;
+class CSetting;
+
+using SettingConditionCheck = bool (*)(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+class ISettingCondition
+{
+public:
+ explicit ISettingCondition(CSettingsManager *settingsManager)
+ : m_settingsManager(settingsManager)
+ { }
+ virtual ~ISettingCondition() = default;
+
+ virtual bool Check() const = 0;
+
+protected:
+ CSettingsManager *m_settingsManager;
+};
+
+class CSettingConditionItem : public CBooleanLogicValue, public ISettingCondition
+{
+public:
+ explicit CSettingConditionItem(CSettingsManager *settingsManager = nullptr)
+ : ISettingCondition(settingsManager)
+ { }
+ ~CSettingConditionItem() override = default;
+
+ bool Deserialize(const TiXmlNode *node) override;
+ const char* GetTag() const override { return SETTING_XML_ELM_CONDITION; }
+ bool Check() const override;
+
+protected:
+ std::string m_name;
+ std::string m_setting;
+};
+
+class CSettingConditionCombination : public CBooleanLogicOperation, public ISettingCondition
+{
+public:
+ explicit CSettingConditionCombination(CSettingsManager *settingsManager = nullptr)
+ : ISettingCondition(settingsManager)
+ { }
+ ~CSettingConditionCombination() override = default;
+
+ bool Check() const override;
+
+private:
+ CBooleanLogicOperation* newOperation() override { return new CSettingConditionCombination(m_settingsManager); }
+ CBooleanLogicValue* newValue() override { return new CSettingConditionItem(m_settingsManager); }
+};
+
+class CSettingCondition : public CBooleanLogic, public ISettingCondition
+{
+public:
+ explicit CSettingCondition(CSettingsManager *settingsManager = nullptr);
+ ~CSettingCondition() override = default;
+
+ bool Check() const override;
+};
+
+class CSettingConditionsManager
+{
+public:
+ CSettingConditionsManager() = default;
+ CSettingConditionsManager(const CSettingConditionsManager&) = delete;
+ CSettingConditionsManager const& operator=(CSettingConditionsManager const&) = delete;
+ virtual ~CSettingConditionsManager() = default;
+
+ void AddCondition(std::string condition);
+ void AddDynamicCondition(std::string identifier, SettingConditionCheck condition, void *data = nullptr);
+ void RemoveDynamicCondition(std::string identifier);
+
+ bool Check(
+ std::string condition,
+ const std::string& value = "",
+ const std::shared_ptr<const CSetting>& setting = std::shared_ptr<const CSetting>()) const;
+
+private:
+ using SettingConditionPair = std::pair<std::string, std::pair<SettingConditionCheck, void*>>;
+ using SettingConditionMap = std::map<std::string, std::pair<SettingConditionCheck, void*>>;
+
+ SettingConditionMap m_conditions;
+ std::set<std::string> m_defines;
+};
diff --git a/xbmc/settings/lib/SettingDefinitions.h b/xbmc/settings/lib/SettingDefinitions.h
new file mode 100644
index 0000000..ae18347
--- /dev/null
+++ b/xbmc/settings/lib/SettingDefinitions.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2013-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 "utils/Variant.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#define SETTING_XML_ROOT "settings"
+#define SETTING_XML_ROOT_VERSION "version"
+
+#define SETTING_XML_ELM_SECTION "section"
+#define SETTING_XML_ELM_CATEGORY "category"
+#define SETTING_XML_ELM_GROUP "group"
+#define SETTING_XML_ELM_SETTING "setting"
+#define SETTING_XML_ELM_VISIBLE "visible"
+#define SETTING_XML_ELM_REQUIREMENT "requirement"
+#define SETTING_XML_ELM_CONDITION "condition"
+#define SETTING_XML_ELM_ENABLED "enable"
+#define SETTING_XML_ELM_LEVEL "level"
+#define SETTING_XML_ELM_DEFAULT "default"
+#define SETTING_XML_ELM_VALUE "value"
+#define SETTING_XML_ELM_CONTROL "control"
+#define SETTING_XML_ELM_CONSTRAINTS "constraints"
+#define SETTING_XML_ELM_OPTIONS "options"
+#define SETTING_XML_ELM_OPTION "option"
+#define SETTING_XML_ELM_MINIMUM "minimum"
+#define SETTING_XML_ELM_STEP "step"
+#define SETTING_XML_ELM_MAXIMUM "maximum"
+#define SETTING_XML_ELM_ALLOWEMPTY "allowempty"
+#define SETTING_XML_ELM_ALLOWNEWOPTION "allownewoption"
+#define SETTING_XML_ELM_DEPENDENCIES "dependencies"
+#define SETTING_XML_ELM_DEPENDENCY "dependency"
+#define SETTING_XML_ELM_UPDATES "updates"
+#define SETTING_XML_ELM_UPDATE "update"
+#define SETTING_XML_ELM_ACCESS "access"
+#define SETTING_XML_ELM_DELIMITER "delimiter"
+#define SETTING_XML_ELM_MINIMUM_ITEMS "minimumitems"
+#define SETTING_XML_ELM_MAXIMUM_ITEMS "maximumitems"
+#define SETTING_XML_ELM_DATA "data"
+
+#define SETTING_XML_ATTR_ID "id"
+#define SETTING_XML_ATTR_REFERENCE "ref"
+#define SETTING_XML_ATTR_LABEL "label"
+#define SETTING_XML_ATTR_HELP "help"
+#define SETTING_XML_ATTR_TYPE "type"
+#define SETTING_XML_ATTR_PARENT "parent"
+#define SETTING_XML_ATTR_FORMAT "format"
+#define SETTING_XML_ATTR_DELAYED "delayed"
+#define SETTING_XML_ATTR_ON "on"
+#define SETTING_XML_ATTR_OPERATOR "operator"
+#define SETTING_XML_ATTR_NAME "name"
+#define SETTING_XML_ATTR_SETTING "setting"
+#define SETTING_XML_ATTR_BEFORE "before"
+#define SETTING_XML_ATTR_AFTER "after"
+
+struct IntegerSettingOption
+{
+ IntegerSettingOption(const std::string& _label, int _value)
+ : label(_label), value(_value) {}
+
+ IntegerSettingOption(const std::string& _label,
+ const std::string& _label2,
+ int _value,
+ const std::vector<std::pair<std::string, CVariant>>& props)
+ : label(_label), label2(_label2), value(_value), properties(props)
+ {
+ }
+
+ std::string label;
+ std::string label2;
+ int value = 0;
+ std::vector<std::pair<std::string, CVariant>> properties;
+};
+
+struct StringSettingOption
+{
+ StringSettingOption(const std::string& _label, const std::string& _value)
+ : label(_label), value(_value) {}
+
+ StringSettingOption(const std::string& _label,
+ const std::string& _label2,
+ const std::string& _value,
+ const std::vector<std::pair<std::string, CVariant>>& props)
+ : label(_label), label2(_label2), value(_value), properties(props)
+ {
+ }
+
+ std::string label;
+ std::string label2;
+ std::string value;
+ std::vector<std::pair<std::string, CVariant>> properties;
+};
+
+struct TranslatableIntegerSettingOption
+{
+ TranslatableIntegerSettingOption() = default;
+ TranslatableIntegerSettingOption(int _label, int _value, const std::string& _addonId = "")
+ : label(_label), value(_value), addonId(_addonId)
+ {
+ }
+
+ int label = 0;
+ int value = 0;
+ std::string addonId; // Leaved empty for Kodi labels
+};
+
+using TranslatableIntegerSettingOptions = std::vector<TranslatableIntegerSettingOption>;
+using IntegerSettingOptions = std::vector<IntegerSettingOption>;
+using TranslatableStringSettingOption = std::pair<int, std::string>;
+using TranslatableStringSettingOptions = std::vector<TranslatableStringSettingOption>;
+using StringSettingOptions = std::vector<StringSettingOption>;
+
+class CSetting;
+using IntegerSettingOptionsFiller = void (*)(const std::shared_ptr<const CSetting>& setting,
+ IntegerSettingOptions& list,
+ int& current,
+ void* data);
+using StringSettingOptionsFiller = void (*)(const std::shared_ptr<const CSetting>& setting,
+ StringSettingOptions& list,
+ std::string& current,
+ void* data);
+
+enum class SettingOptionsSort
+{
+ NoSorting,
+ Ascending,
+ Descending
+};
diff --git a/xbmc/settings/lib/SettingDependency.cpp b/xbmc/settings/lib/SettingDependency.cpp
new file mode 100644
index 0000000..e3122da
--- /dev/null
+++ b/xbmc/settings/lib/SettingDependency.cpp
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2013-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 "SettingDependency.h"
+
+#include "ServiceBroker.h"
+#include "Setting.h"
+#include "SettingDefinitions.h"
+#include "SettingsManager.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <memory>
+#include <set>
+#include <stdlib.h>
+#include <string>
+
+Logger CSettingDependencyCondition::s_logger;
+
+CSettingDependencyCondition::CSettingDependencyCondition(
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingDependencyCondition(settingsManager, "", "", "")
+{
+}
+
+CSettingDependencyCondition::CSettingDependencyCondition(
+ const std::string& setting,
+ const std::string& value,
+ SettingDependencyOperator op,
+ bool negated /* = false */,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingDependencyCondition(
+ settingsManager, setting, setting, value, SettingDependencyTarget::Setting, op, negated)
+{
+}
+
+CSettingDependencyCondition::CSettingDependencyCondition(
+ const std::string& strProperty,
+ const std::string& value,
+ const std::string& setting /* = "" */,
+ bool negated /* = false */,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingDependencyCondition(settingsManager,
+ strProperty,
+ setting,
+ value,
+ SettingDependencyTarget::Property,
+ SettingDependencyOperator::Equals,
+ negated)
+{
+}
+
+CSettingDependencyCondition::CSettingDependencyCondition(
+ CSettingsManager* settingsManager,
+ const std::string& strProperty,
+ const std::string& setting,
+ const std::string& value,
+ SettingDependencyTarget target /* = SettingDependencyTarget::Unknown */,
+ SettingDependencyOperator op /* = SettingDependencyOperator::Equals */,
+ bool negated /* = false */)
+ : CSettingConditionItem(settingsManager), m_target(target), m_operator(op)
+{
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingDependencyCondition");
+
+ m_name = strProperty;
+ m_setting = setting;
+ m_value = value;
+ m_negated = negated;
+}
+
+bool CSettingDependencyCondition::Deserialize(const TiXmlNode *node)
+{
+ if (!CSettingConditionItem::Deserialize(node))
+ return false;
+
+ auto elem = node->ToElement();
+ if (elem == nullptr)
+ return false;
+
+ m_target = SettingDependencyTarget::Setting;
+ auto strTarget = elem->Attribute(SETTING_XML_ATTR_ON);
+ if (strTarget != nullptr && !setTarget(strTarget))
+ {
+ s_logger->warn("unknown target \"{}\"", strTarget);
+ return false;
+ }
+
+ if (m_target != SettingDependencyTarget::Setting && m_name.empty())
+ {
+ s_logger->warn("missing name for dependency");
+ return false;
+ }
+
+ if (m_target == SettingDependencyTarget::Setting)
+ {
+ if (m_setting.empty())
+ {
+ s_logger->warn("missing setting for dependency");
+ return false;
+ }
+
+ m_name = m_setting;
+ }
+
+ m_operator = SettingDependencyOperator::Equals;
+ auto strOperator = elem->Attribute(SETTING_XML_ATTR_OPERATOR);
+ if (strOperator != nullptr && !setOperator(strOperator))
+ {
+ s_logger->warn("unknown operator \"{}\"", strOperator);
+ return false;
+ }
+
+ return true;
+}
+
+bool CSettingDependencyCondition::Check() const
+{
+ if (m_name.empty() ||
+ m_target == SettingDependencyTarget::Unknown ||
+ m_operator == SettingDependencyOperator::Unknown ||
+ m_settingsManager == nullptr)
+ return false;
+
+ bool result = false;
+ switch (m_target)
+ {
+ case SettingDependencyTarget::Setting:
+ {
+ if (m_setting.empty())
+ return false;
+
+ auto setting = m_settingsManager->GetSetting(m_setting);
+ if (setting == nullptr)
+ {
+ s_logger->warn("unable to check condition on unknown setting \"{}\"", m_setting);
+ return false;
+ }
+
+ switch (m_operator)
+ {
+ case SettingDependencyOperator::Equals:
+ result = setting->Equals(m_value);
+ break;
+
+ case SettingDependencyOperator::LessThan:
+ {
+ const auto value = setting->ToString();
+ if (StringUtils::IsInteger(m_value))
+ result = strtol(value.c_str(), nullptr, 0) < strtol(m_value.c_str(), nullptr, 0);
+ else
+ result = value.compare(m_value) < 0;
+ break;
+ }
+
+ case SettingDependencyOperator::GreaterThan:
+ {
+ const auto value = setting->ToString();
+ if (StringUtils::IsInteger(m_value))
+ result = strtol(value.c_str(), nullptr, 0) > strtol(m_value.c_str(), nullptr, 0);
+ else
+ result = value.compare(m_value) > 0;
+ break;
+ }
+
+ case SettingDependencyOperator::Contains:
+ result = (setting->ToString().find(m_value) != std::string::npos);
+ break;
+
+ case SettingDependencyOperator::Unknown:
+ default:
+ break;
+ }
+
+ break;
+ }
+
+ case SettingDependencyTarget::Property:
+ {
+ SettingConstPtr setting;
+ if (!m_setting.empty())
+ {
+ setting = m_settingsManager->GetSetting(m_setting);
+ if (setting == nullptr)
+ {
+ s_logger->warn("unable to check condition on unknown setting \"{}\"", m_setting);
+ return false;
+ }
+ }
+ result = m_settingsManager->GetConditions().Check(m_name, m_value, setting);
+ break;
+ }
+
+ default:
+ return false;
+ }
+
+ return result == !m_negated;
+}
+
+bool CSettingDependencyCondition::setTarget(const std::string &target)
+{
+ if (StringUtils::EqualsNoCase(target, "setting"))
+ m_target = SettingDependencyTarget::Setting;
+ else if (StringUtils::EqualsNoCase(target, "property"))
+ m_target = SettingDependencyTarget::Property;
+ else
+ return false;
+
+ return true;
+}
+
+bool CSettingDependencyCondition::setOperator(const std::string &op)
+{
+ size_t length = 0;
+ if (StringUtils::EndsWithNoCase(op, "is"))
+ {
+ m_operator = SettingDependencyOperator::Equals;
+ length = 2;
+ }
+ else if (StringUtils::EndsWithNoCase(op, "lessthan"))
+ {
+ m_operator = SettingDependencyOperator::LessThan;
+ length = 8;
+ }
+ else if (StringUtils::EndsWithNoCase(op, "lt"))
+ {
+ m_operator = SettingDependencyOperator::LessThan;
+ length = 2;
+ }
+ else if (StringUtils::EndsWithNoCase(op, "greaterthan"))
+ {
+ m_operator = SettingDependencyOperator::GreaterThan;
+ length = 11;
+ }
+ else if (StringUtils::EndsWithNoCase(op, "gt"))
+ {
+ m_operator = SettingDependencyOperator::GreaterThan;
+ length = 2;
+ }
+ else if (StringUtils::EndsWithNoCase(op, "contains"))
+ {
+ m_operator = SettingDependencyOperator::Contains;
+ length = 8;
+ }
+
+ if (op.size() > length + 1)
+ return false;
+ if (op.size() == length + 1)
+ {
+ if (!StringUtils::StartsWith(op, "!"))
+ return false;
+ m_negated = true;
+ }
+
+ return true;
+}
+
+bool CSettingDependencyConditionCombination::Deserialize(const TiXmlNode *node)
+{
+ if (node == nullptr)
+ return false;
+
+ size_t numOperations = m_operations.size();
+ size_t numValues = m_values.size();
+
+ if (!CSettingConditionCombination::Deserialize(node))
+ return false;
+
+ if (numOperations < m_operations.size())
+ {
+ for (size_t i = numOperations; i < m_operations.size(); i++)
+ {
+ if (m_operations[i] == nullptr)
+ continue;
+
+ auto combination = static_cast<CSettingDependencyConditionCombination*>(m_operations[i].get());
+ if (combination == nullptr)
+ continue;
+
+ const std::set<std::string>& settings = combination->GetSettings();
+ m_settings.insert(settings.begin(), settings.end());
+ }
+ }
+
+ if (numValues < m_values.size())
+ {
+ for (size_t i = numValues; i < m_values.size(); i++)
+ {
+ if (m_values[i] == nullptr)
+ continue;
+
+ auto condition = static_cast<CSettingDependencyCondition*>(m_values[i].get());
+ if (condition == nullptr)
+ continue;
+
+ auto settingId = condition->GetSetting();
+ if (!settingId.empty())
+ m_settings.insert(settingId);
+ }
+ }
+
+ return true;
+}
+
+CSettingDependencyConditionCombination* CSettingDependencyConditionCombination::Add(
+ const CSettingDependencyConditionPtr& condition)
+{
+ if (condition != nullptr)
+ {
+ m_values.push_back(condition);
+
+ auto settingId = condition->GetSetting();
+ if (!settingId.empty())
+ m_settings.insert(settingId);
+ }
+
+ return this;
+}
+
+CSettingDependencyConditionCombination* CSettingDependencyConditionCombination::Add(
+ const CSettingDependencyConditionCombinationPtr& operation)
+{
+ if (operation != nullptr)
+ {
+ m_operations.push_back(operation);
+
+ const auto& settings = operation->GetSettings();
+ m_settings.insert(settings.begin(), settings.end());
+ }
+
+ return this;
+}
+
+Logger CSettingDependency::s_logger;
+
+CSettingDependency::CSettingDependency(CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingDependency(SettingDependencyType::Unknown, settingsManager)
+{
+}
+
+CSettingDependency::CSettingDependency(SettingDependencyType type,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : CSettingCondition(settingsManager), m_type(type)
+{
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingDependency");
+
+ m_operation = CBooleanLogicOperationPtr(new CSettingDependencyConditionCombination(m_settingsManager));
+}
+
+bool CSettingDependency::Deserialize(const TiXmlNode *node)
+{
+ if (node == nullptr)
+ return false;
+
+ auto elem = node->ToElement();
+ if (elem == nullptr)
+ return false;
+
+ auto strType = elem->Attribute(SETTING_XML_ATTR_TYPE);
+ if (strType == nullptr || strlen(strType) <= 0 || !setType(strType))
+ {
+ s_logger->warn("missing or unknown dependency type definition");
+ return false;
+ }
+
+ return CSettingCondition::Deserialize(node);
+}
+
+std::set<std::string> CSettingDependency::GetSettings() const
+{
+ if (m_operation == nullptr)
+ return std::set<std::string>();
+
+ auto combination = static_cast<CSettingDependencyConditionCombination*>(m_operation.get());
+ if (combination == nullptr)
+ return std::set<std::string>();
+
+ return combination->GetSettings();
+}
+
+CSettingDependencyConditionCombinationPtr CSettingDependency::And()
+{
+ if (m_operation == nullptr)
+ m_operation = CBooleanLogicOperationPtr(new CSettingDependencyConditionCombination(m_settingsManager));
+
+ m_operation->SetOperation(BooleanLogicOperationAnd);
+
+ return std::dynamic_pointer_cast<CSettingDependencyConditionCombination>(m_operation);
+}
+
+CSettingDependencyConditionCombinationPtr CSettingDependency::Or()
+{
+ if (m_operation == nullptr)
+ m_operation = CBooleanLogicOperationPtr(new CSettingDependencyConditionCombination(m_settingsManager));
+
+ m_operation->SetOperation(BooleanLogicOperationOr);
+
+ return std::dynamic_pointer_cast<CSettingDependencyConditionCombination>(m_operation);
+}
+
+bool CSettingDependency::setType(const std::string &type)
+{
+ if (StringUtils::EqualsNoCase(type, "enable"))
+ m_type = SettingDependencyType::Enable;
+ else if (StringUtils::EqualsNoCase(type, "update"))
+ m_type = SettingDependencyType::Update;
+ else if (StringUtils::EqualsNoCase(type, "visible"))
+ m_type = SettingDependencyType::Visible;
+ else
+ return false;
+
+ return true;
+}
diff --git a/xbmc/settings/lib/SettingDependency.h b/xbmc/settings/lib/SettingDependency.h
new file mode 100644
index 0000000..5cb34db
--- /dev/null
+++ b/xbmc/settings/lib/SettingDependency.h
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2013-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 "SettingConditions.h"
+#include "utils/BooleanLogic.h"
+#include "utils/logtypes.h"
+
+#include <list>
+#include <set>
+#include <string>
+
+enum class SettingDependencyType {
+ Unknown = 0,
+ Enable,
+ Update,
+ Visible
+};
+
+enum class SettingDependencyOperator {
+ Unknown = 0,
+ Equals,
+ LessThan,
+ GreaterThan,
+ Contains
+};
+
+enum class SettingDependencyTarget {
+ Unknown = 0,
+ Setting,
+ Property
+};
+
+class CSettingDependencyCondition : public CSettingConditionItem
+{
+public:
+ explicit CSettingDependencyCondition(CSettingsManager *settingsManager = nullptr);
+ CSettingDependencyCondition(const std::string &setting, const std::string &value,
+ SettingDependencyOperator op, bool negated = false,
+ CSettingsManager *settingsManager = nullptr);
+ CSettingDependencyCondition(const std::string &strProperty, const std::string &value,
+ const std::string &setting = "", bool negated = false,
+ CSettingsManager *settingsManager = nullptr);
+ ~CSettingDependencyCondition() override = default;
+
+ bool Deserialize(const TiXmlNode *node) override;
+ bool Check() const override;
+
+ const std::string& GetName() const { return m_name; }
+ const std::string& GetSetting() const { return m_setting; }
+ SettingDependencyTarget GetTarget() const { return m_target; }
+ SettingDependencyOperator GetOperator() const { return m_operator; }
+
+private:
+ CSettingDependencyCondition(CSettingsManager* settingsManager,
+ const std::string& strProperty,
+ const std::string& setting,
+ const std::string& value,
+ SettingDependencyTarget target = SettingDependencyTarget::Unknown,
+ SettingDependencyOperator op = SettingDependencyOperator::Equals,
+ bool negated = false);
+
+ bool setTarget(const std::string &target);
+ bool setOperator(const std::string &op);
+
+ SettingDependencyTarget m_target = SettingDependencyTarget::Unknown;
+ SettingDependencyOperator m_operator = SettingDependencyOperator::Equals;
+
+ static Logger s_logger;
+};
+
+using CSettingDependencyConditionPtr = std::shared_ptr<CSettingDependencyCondition>;
+
+class CSettingDependencyConditionCombination;
+using CSettingDependencyConditionCombinationPtr = std::shared_ptr<CSettingDependencyConditionCombination>;
+
+class CSettingDependencyConditionCombination : public CSettingConditionCombination
+{
+public:
+ explicit CSettingDependencyConditionCombination(CSettingsManager *settingsManager = nullptr)
+ : CSettingConditionCombination(settingsManager)
+ { }
+ CSettingDependencyConditionCombination(BooleanLogicOperation op, CSettingsManager *settingsManager = nullptr)
+ : CSettingConditionCombination(settingsManager)
+ {
+ SetOperation(op);
+ }
+ ~CSettingDependencyConditionCombination() override = default;
+
+ bool Deserialize(const TiXmlNode *node) override;
+
+ const std::set<std::string>& GetSettings() const { return m_settings; }
+
+ CSettingDependencyConditionCombination* Add(const CSettingDependencyConditionPtr& condition);
+ CSettingDependencyConditionCombination* Add(
+ const CSettingDependencyConditionCombinationPtr& operation);
+
+private:
+ CBooleanLogicOperation* newOperation() override { return new CSettingDependencyConditionCombination(m_settingsManager); }
+ CBooleanLogicValue* newValue() override { return new CSettingDependencyCondition(m_settingsManager); }
+
+ std::set<std::string> m_settings;
+};
+
+class CSettingDependency : public CSettingCondition
+{
+public:
+ explicit CSettingDependency(CSettingsManager *settingsManager = nullptr);
+ CSettingDependency(SettingDependencyType type, CSettingsManager *settingsManager = nullptr);
+ ~CSettingDependency() override = default;
+
+ bool Deserialize(const TiXmlNode *node) override;
+
+ SettingDependencyType GetType() const { return m_type; }
+ std::set<std::string> GetSettings() const;
+
+ CSettingDependencyConditionCombinationPtr And();
+ CSettingDependencyConditionCombinationPtr Or();
+
+private:
+ bool setType(const std::string &type);
+
+ SettingDependencyType m_type = SettingDependencyType::Unknown;
+
+ static Logger s_logger;
+};
+
+using SettingDependencies = std::list<CSettingDependency>;
+using SettingDependencyMap = std::map<std::string, SettingDependencies>;
diff --git a/xbmc/settings/lib/SettingLevel.h b/xbmc/settings/lib/SettingLevel.h
new file mode 100644
index 0000000..db8e2b5
--- /dev/null
+++ b/xbmc/settings/lib/SettingLevel.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2017-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
+
+/*!
+ \ingroup settings
+ \brief Levels which every setting is assigned to.
+ */
+enum class SettingLevel {
+ Basic = 0,
+ Standard,
+ Advanced,
+ Expert,
+ Internal
+};
diff --git a/xbmc/settings/lib/SettingRequirement.cpp b/xbmc/settings/lib/SettingRequirement.cpp
new file mode 100644
index 0000000..5ba81ab
--- /dev/null
+++ b/xbmc/settings/lib/SettingRequirement.cpp
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2013-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 "SettingRequirement.h"
+
+#include "SettingsManager.h"
+
+bool CSettingRequirementCondition::Check() const
+{
+ if (m_settingsManager == nullptr)
+ return false;
+
+ bool found = m_settingsManager->GetConditions().Check("IsDefined", m_value);
+ if (m_negated)
+ return !found;
+
+ return found;
+}
+
+bool CSettingRequirementConditionCombination::Check() const
+{
+ if (m_operations.empty() && m_values.empty())
+ return true;
+
+ return CSettingConditionCombination::Check();
+}
+
+CSettingRequirement::CSettingRequirement(CSettingsManager *settingsManager /* = nullptr */)
+ : CSettingCondition(settingsManager)
+{
+ m_operation = CBooleanLogicOperationPtr(new CSettingRequirementConditionCombination(m_settingsManager));
+}
diff --git a/xbmc/settings/lib/SettingRequirement.h b/xbmc/settings/lib/SettingRequirement.h
new file mode 100644
index 0000000..f8357a5
--- /dev/null
+++ b/xbmc/settings/lib/SettingRequirement.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013-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 "SettingConditions.h"
+
+#include <set>
+#include <string>
+
+class CSettingRequirementCondition : public CSettingConditionItem
+{
+public:
+ explicit CSettingRequirementCondition(CSettingsManager *settingsManager = nullptr)
+ : CSettingConditionItem(settingsManager)
+ { }
+ ~CSettingRequirementCondition() override = default;
+
+ bool Check() const override;
+};
+
+class CSettingRequirementConditionCombination : public CSettingConditionCombination
+{
+public:
+ explicit CSettingRequirementConditionCombination(CSettingsManager *settingsManager = nullptr)
+ : CSettingConditionCombination(settingsManager)
+ { }
+ ~CSettingRequirementConditionCombination() override = default;
+
+ bool Check() const override;
+
+private:
+ CBooleanLogicOperation* newOperation() override { return new CSettingRequirementConditionCombination(m_settingsManager); }
+ CBooleanLogicValue* newValue() override { return new CSettingRequirementCondition(m_settingsManager); }
+};
+
+class CSettingRequirement : public CSettingCondition
+{
+public:
+ explicit CSettingRequirement(CSettingsManager *settingsManager = nullptr);
+ ~CSettingRequirement() override = default;
+};
diff --git a/xbmc/settings/lib/SettingSection.cpp b/xbmc/settings/lib/SettingSection.cpp
new file mode 100644
index 0000000..4f1b564
--- /dev/null
+++ b/xbmc/settings/lib/SettingSection.cpp
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2013-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 "SettingSection.h"
+
+#include "ServiceBroker.h"
+#include "SettingDefinitions.h"
+#include "SettingsManager.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+template<class T>
+void addISetting(const TiXmlNode* node, const T& item, std::vector<T>& items, bool toBegin = false)
+{
+ if (node != nullptr)
+ {
+ auto element = node->ToElement();
+ if (element != nullptr)
+ {
+ // check if there is a "before" or "after" attribute to place the setting at a specific position
+ int position = -1; // -1 => end, 0 => before, 1 => after
+ auto positionId = element->Attribute(SETTING_XML_ATTR_BEFORE);
+ if (positionId != nullptr && strlen(positionId) > 0)
+ position = 0;
+ else if ((positionId = element->Attribute(SETTING_XML_ATTR_AFTER)) != nullptr && strlen(positionId) > 0)
+ position = 1;
+
+ if (positionId != nullptr && strlen(positionId) > 0 && position >= 0)
+ {
+ for (typename std::vector<T>::iterator it = items.begin(); it != items.end(); ++it)
+ {
+ if (!StringUtils::EqualsNoCase((*it)->GetId(), positionId))
+ continue;
+
+ typename std::vector<T>::iterator positionIt = it;
+ if (position == 1)
+ ++positionIt;
+
+ items.insert(positionIt, item);
+ return;
+ }
+ }
+ }
+ }
+
+ if (!toBegin)
+ items.emplace_back(item);
+ else
+ items.insert(items.begin(), item);
+}
+
+Logger CSettingGroup::s_logger;
+
+CSettingGroup::CSettingGroup(const std::string& id,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : ISetting(id, settingsManager)
+{
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingGroup");
+}
+
+bool CSettingGroup::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ // handle <visible> conditions
+ if (!ISetting::Deserialize(node, update))
+ return false;
+
+ auto controlElement = node->FirstChildElement(SETTING_XML_ELM_CONTROL);
+ if (controlElement != nullptr)
+ {
+ auto controlType = controlElement->Attribute(SETTING_XML_ATTR_TYPE);
+ if (controlType == nullptr || strlen(controlType) <= 0)
+ {
+ s_logger->error("unable to read control type");
+ return false;
+ }
+
+ m_control = m_settingsManager->CreateControl(controlType);
+ if (m_control == nullptr)
+ {
+ s_logger->error("unable to create new control \"{}\"", controlType);
+ return false;
+ }
+
+ if (!m_control->Deserialize(controlElement))
+ {
+ s_logger->warn("unable to read control \"{}\"", controlType);
+ m_control.reset();
+ }
+ }
+
+ auto settingElement = node->FirstChildElement(SETTING_XML_ELM_SETTING);
+ while (settingElement != nullptr)
+ {
+ std::string settingId;
+ bool isReference;
+ if (CSetting::DeserializeIdentification(settingElement, settingId, isReference))
+ {
+ auto settingIt = std::find_if(m_settings.begin(), m_settings.end(),
+ [&settingId](const SettingPtr& setting)
+ {
+ return setting->GetId() == settingId;
+ });
+
+ SettingPtr setting;
+ if (settingIt != m_settings.end())
+ setting = *settingIt;
+
+ update = (setting != nullptr);
+ if (!update)
+ {
+ auto settingType = settingElement->Attribute(SETTING_XML_ATTR_TYPE);
+ if (settingType == nullptr || strlen(settingType) <= 0)
+ {
+ s_logger->error("unable to read setting type of \"{}\"", settingId);
+ return false;
+ }
+
+ setting = m_settingsManager->CreateSetting(settingType, settingId, m_settingsManager);
+ if (setting == nullptr)
+ s_logger->error("unknown setting type \"{}\" of \"{}\"", settingType, settingId);
+ }
+
+ if (setting == nullptr)
+ s_logger->error("unable to create new setting \"{}\"", settingId);
+ else
+ {
+ if (!setting->Deserialize(settingElement, update))
+ s_logger->warn("unable to read setting \"{}\"", settingId);
+ else
+ {
+ // if the setting is a reference turn it into one
+ if (isReference)
+ setting->MakeReference();
+
+ if (!update)
+ addISetting(settingElement, setting, m_settings);
+ }
+ }
+ }
+
+ settingElement = settingElement->NextSiblingElement(SETTING_XML_ELM_SETTING);
+ }
+
+ return true;
+}
+
+SettingList CSettingGroup::GetSettings(SettingLevel level) const
+{
+ SettingList settings;
+ for (const auto& setting : m_settings)
+ {
+ if (setting->GetLevel() <= level && setting->MeetsRequirements())
+ settings.push_back(setting);
+ }
+
+ return settings;
+}
+
+void CSettingGroup::AddSetting(const SettingPtr& setting)
+{
+ addISetting(nullptr, setting, m_settings);
+}
+
+void CSettingGroup::AddSettings(const SettingList &settings)
+{
+ for (const auto& setting : settings)
+ addISetting(nullptr, setting, m_settings);
+}
+
+bool CSettingGroup::ReplaceSetting(const std::shared_ptr<const CSetting>& currentSetting,
+ const std::shared_ptr<CSetting>& newSetting)
+{
+ for (auto itSetting = m_settings.begin(); itSetting != m_settings.end(); ++itSetting)
+ {
+ if (*itSetting == currentSetting)
+ {
+ if (newSetting == nullptr)
+ m_settings.erase(itSetting);
+ else
+ *itSetting = newSetting;
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+Logger CSettingCategory::s_logger;
+
+CSettingCategory::CSettingCategory(const std::string& id,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : ISetting(id, settingsManager),
+ m_accessCondition(settingsManager)
+{
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingCategory");
+}
+
+bool CSettingCategory::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ // handle <visible> conditions
+ if (!ISetting::Deserialize(node, update))
+ return false;
+
+ auto accessNode = node->FirstChild(SETTING_XML_ELM_ACCESS);
+ if (accessNode != nullptr && !m_accessCondition.Deserialize(accessNode))
+ return false;
+
+ auto groupNode = node->FirstChild(SETTING_XML_ELM_GROUP);
+ while (groupNode != nullptr)
+ {
+ std::string groupId;
+ if (CSettingGroup::DeserializeIdentification(groupNode, groupId))
+ {
+ auto groupIt = std::find_if(m_groups.begin(), m_groups.end(),
+ [&groupId](const SettingGroupPtr& group)
+ {
+ return group->GetId() == groupId;
+ });
+
+ SettingGroupPtr group;
+ if (groupIt != m_groups.end())
+ group = *groupIt;
+
+ update = (group != nullptr);
+ if (!update)
+ group = std::make_shared<CSettingGroup>(groupId, m_settingsManager);
+
+ if (group->Deserialize(groupNode, update))
+ {
+ if (!update)
+ addISetting(groupNode, group, m_groups);
+ }
+ else
+ s_logger->warn("unable to read group \"{}\"", groupId);
+ }
+
+ groupNode = groupNode->NextSibling(SETTING_XML_ELM_GROUP);
+ }
+
+ return true;
+}
+
+SettingGroupList CSettingCategory::GetGroups(SettingLevel level) const
+{
+ SettingGroupList groups;
+ for (const auto& group : m_groups)
+ {
+ if (group->MeetsRequirements() && group->IsVisible() && group->GetSettings(level).size() > 0)
+ groups.push_back(group);
+ }
+
+ return groups;
+}
+
+bool CSettingCategory::CanAccess() const
+{
+ return m_accessCondition.Check();
+}
+
+void CSettingCategory::AddGroup(const SettingGroupPtr& group)
+{
+ addISetting(nullptr, group, m_groups, false);
+}
+
+void CSettingCategory::AddGroupToFront(const SettingGroupPtr& group)
+{
+ addISetting(nullptr, group, m_groups, true);
+}
+
+void CSettingCategory::AddGroups(const SettingGroupList &groups)
+{
+ for (const auto& group : groups)
+ addISetting(nullptr, group, m_groups);
+}
+
+Logger CSettingSection::s_logger;
+
+CSettingSection::CSettingSection(const std::string& id,
+ CSettingsManager* settingsManager /* = nullptr */)
+ : ISetting(id, settingsManager)
+{
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingSection");
+}
+
+bool CSettingSection::Deserialize(const TiXmlNode *node, bool update /* = false */)
+{
+ // handle <visible> conditions
+ if (!ISetting::Deserialize(node, update))
+ return false;
+
+ auto categoryNode = node->FirstChild(SETTING_XML_ELM_CATEGORY);
+ while (categoryNode != nullptr)
+ {
+ std::string categoryId;
+ if (CSettingCategory::DeserializeIdentification(categoryNode, categoryId))
+ {
+ auto categoryIt = std::find_if(m_categories.begin(), m_categories.end(),
+ [&categoryId](const SettingCategoryPtr& category)
+ {
+ return category->GetId() == categoryId;
+ });
+
+ SettingCategoryPtr category;
+ if (categoryIt != m_categories.end())
+ category = *categoryIt;
+
+ update = (category != nullptr);
+ if (!update)
+ category = std::make_shared<CSettingCategory>(categoryId, m_settingsManager);
+
+ if (category->Deserialize(categoryNode, update))
+ {
+ if (!update)
+ addISetting(categoryNode, category, m_categories);
+ }
+ else
+ s_logger->warn("unable to read category \"{}\"", categoryId);
+ }
+
+ categoryNode = categoryNode->NextSibling(SETTING_XML_ELM_CATEGORY);
+ }
+
+ return true;
+}
+
+SettingCategoryList CSettingSection::GetCategories(SettingLevel level) const
+{
+ SettingCategoryList categories;
+ for (const auto& category : m_categories)
+ {
+ if (category->MeetsRequirements() && category->IsVisible() && category->GetGroups(level).size() > 0)
+ categories.push_back(category);
+ }
+
+ return categories;
+}
+
+void CSettingSection::AddCategory(const SettingCategoryPtr& category)
+{
+ addISetting(nullptr, category, m_categories);
+}
+
+void CSettingSection::AddCategories(const SettingCategoryList &categories)
+{
+ for (const auto& category : categories)
+ addISetting(nullptr, category, m_categories);
+}
diff --git a/xbmc/settings/lib/SettingSection.h b/xbmc/settings/lib/SettingSection.h
new file mode 100644
index 0000000..f8e06d6
--- /dev/null
+++ b/xbmc/settings/lib/SettingSection.h
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2013-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 "ISetting.h"
+#include "Setting.h"
+#include "SettingCategoryAccess.h"
+#include "utils/logtypes.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+class CSettingsManager;
+
+/*!
+ \ingroup settings
+ \brief Group of settings being part of a category
+ \sa CSettingCategory
+ \sa CSetting
+ */
+class CSettingGroup : public ISetting
+{
+public:
+ /*!
+ \brief Creates a new setting group with the given identifier.
+
+ \param id Identifier of the setting group
+ \param settingsManager Reference to the settings manager
+ */
+ CSettingGroup(const std::string &id, CSettingsManager *settingsManager = nullptr);
+ ~CSettingGroup() override = default;
+
+ // implementation of ISetting
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ /*!
+ \brief Gets the full list of settings belonging to the setting group.
+
+ \return Full list of settings belonging to the setting group
+ */
+ const SettingList& GetSettings() const { return m_settings; }
+ /*!
+ \brief Gets the list of settings assigned to the given setting level (or
+ below) and that meet the requirements conditions belonging to the setting
+ group.
+
+ \param level Level the settings should be assigned to
+ \return List of settings belonging to the setting group
+ */
+ SettingList GetSettings(SettingLevel level) const;
+
+ void AddSetting(const std::shared_ptr<CSetting>& setting);
+ void AddSettings(const SettingList &settings);
+
+ bool ReplaceSetting(const std::shared_ptr<const CSetting>& currentSetting,
+ const std::shared_ptr<CSetting>& newSetting);
+
+ std::shared_ptr<const ISettingControl> GetControl() const { return m_control; }
+ std::shared_ptr<ISettingControl> GetControl() { return m_control; }
+ void SetControl(std::shared_ptr<ISettingControl> control) { m_control = std::move(control); }
+
+private:
+ SettingList m_settings;
+ std::shared_ptr<ISettingControl> m_control;
+
+ static Logger s_logger;
+};
+
+using SettingGroupPtr = std::shared_ptr<CSettingGroup>;
+using SettingGroupList = std::vector<SettingGroupPtr>;
+
+/*!
+ \ingroup settings
+ \brief Category of groups of settings being part of a section
+ \sa CSettingSection
+ \sa CSettingGroup
+ */
+class CSettingCategory : public ISetting
+{
+public:
+ /*!
+ \brief Creates a new setting category with the given identifier.
+
+ \param id Identifier of the setting category
+ \param settingsManager Reference to the settings manager
+ */
+ CSettingCategory(const std::string &id, CSettingsManager *settingsManager = nullptr);
+ ~CSettingCategory() override = default;
+
+ // implementation of ISetting
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ /*!
+ \brief Gets the full list of setting groups belonging to the setting
+ category.
+
+ \return Full list of setting groups belonging to the setting category
+ */
+ const SettingGroupList& GetGroups() const { return m_groups; }
+ /*!
+ \brief Gets the list of setting groups belonging to the setting category
+ that contain settings assigned to the given setting level (or below) and
+ that meet the requirements and visibility conditions.
+
+ \param level Level the settings should be assigned to
+ \return List of setting groups belonging to the setting category
+ */
+ SettingGroupList GetGroups(SettingLevel level) const;
+
+ /*!
+ \brief Whether the setting category can be accessed or not.
+
+ \return True if the setting category can be accessed, false otherwise
+ */
+ bool CanAccess() const;
+
+ void AddGroup(const SettingGroupPtr& group);
+ void AddGroupToFront(const SettingGroupPtr& group);
+ void AddGroups(const SettingGroupList &groups);
+
+private:
+ SettingGroupList m_groups;
+ CSettingCategoryAccess m_accessCondition;
+
+ static Logger s_logger;
+};
+
+using SettingCategoryPtr = std::shared_ptr<CSettingCategory>;
+using SettingCategoryList = std::vector<SettingCategoryPtr>;
+
+/*!
+ \ingroup settings
+ \brief Section of setting categories
+ \sa CSettings
+ \sa CSettingCategory
+ */
+class CSettingSection : public ISetting
+{
+public:
+ /*!
+ \brief Creates a new setting section with the given identifier.
+
+ \param id Identifier of the setting section
+ \param settingsManager Reference to the settings manager
+ */
+ CSettingSection(const std::string &id, CSettingsManager *settingsManager = nullptr);
+ ~CSettingSection() override = default;
+
+ // implementation of ISetting
+ bool Deserialize(const TiXmlNode *node, bool update = false) override;
+
+ /*!
+ \brief Gets the full list of setting categories belonging to the setting
+ section.
+
+ \return Full list of setting categories belonging to the setting section
+ */
+ const SettingCategoryList& GetCategories() const { return m_categories; }
+ /*!
+ \brief Gets the list of setting categories belonging to the setting section
+ that contain settings assigned to the given setting level (or below) and
+ that meet the requirements and visibility conditions.
+
+ \param level Level the settings should be assigned to
+ \return List of setting categories belonging to the setting section
+ */
+ SettingCategoryList GetCategories(SettingLevel level) const;
+
+ void AddCategory(const SettingCategoryPtr& category);
+ void AddCategories(const SettingCategoryList &categories);
+
+private:
+ SettingCategoryList m_categories;
+
+ static Logger s_logger;
+};
+
+using SettingSectionPtr = std::shared_ptr<CSettingSection>;
+using SettingSectionList = std::vector<SettingSectionPtr>;
diff --git a/xbmc/settings/lib/SettingType.h b/xbmc/settings/lib/SettingType.h
new file mode 100644
index 0000000..56838ab
--- /dev/null
+++ b/xbmc/settings/lib/SettingType.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2017-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
+
+/*!
+ \ingroup settings
+ \brief Basic setting types available in the settings system.
+ */
+enum class SettingType {
+ Unknown = 0,
+ Boolean,
+ Integer,
+ Number,
+ String,
+ Action,
+ List
+};
diff --git a/xbmc/settings/lib/SettingUpdate.cpp b/xbmc/settings/lib/SettingUpdate.cpp
new file mode 100644
index 0000000..0201aab
--- /dev/null
+++ b/xbmc/settings/lib/SettingUpdate.cpp
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013-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 "SettingUpdate.h"
+
+#include "ServiceBroker.h"
+#include "SettingDefinitions.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+Logger CSettingUpdate::s_logger;
+
+CSettingUpdate::CSettingUpdate()
+{
+ if (s_logger == nullptr)
+ s_logger = CServiceBroker::GetLogging().GetLogger("CSettingUpdate");
+}
+
+bool CSettingUpdate::Deserialize(const TiXmlNode *node)
+{
+ if (node == nullptr)
+ return false;
+
+ auto elem = node->ToElement();
+ if (elem == nullptr)
+ return false;
+
+ auto strType = elem->Attribute(SETTING_XML_ATTR_TYPE);
+ if (strType == nullptr || strlen(strType) <= 0 || !setType(strType))
+ {
+ s_logger->warn("missing or unknown update type definition");
+ return false;
+ }
+
+ if (m_type == SettingUpdateType::Rename)
+ {
+ if (node->FirstChild() == nullptr || node->FirstChild()->Type() != TiXmlNode::TINYXML_TEXT)
+ {
+ s_logger->warn("missing or invalid setting id for rename update definition");
+ return false;
+ }
+
+ m_value = node->FirstChild()->ValueStr();
+ }
+
+ return true;
+}
+
+bool CSettingUpdate::setType(const std::string &type)
+{
+ if (StringUtils::EqualsNoCase(type, "change"))
+ m_type = SettingUpdateType::Change;
+ else if (StringUtils::EqualsNoCase(type, "rename"))
+ m_type = SettingUpdateType::Rename;
+ else
+ return false;
+
+ return true;
+}
diff --git a/xbmc/settings/lib/SettingUpdate.h b/xbmc/settings/lib/SettingUpdate.h
new file mode 100644
index 0000000..65c3e86
--- /dev/null
+++ b/xbmc/settings/lib/SettingUpdate.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2013-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 "utils/logtypes.h"
+
+#include <string>
+
+class TiXmlNode;
+
+enum class SettingUpdateType {
+ Unknown = 0,
+ Rename,
+ Change
+};
+
+class CSettingUpdate
+{
+public:
+ CSettingUpdate();
+ virtual ~CSettingUpdate() = default;
+
+ inline bool operator<(const CSettingUpdate& rhs) const
+ {
+ return m_type < rhs.m_type && m_value < rhs.m_value;
+ }
+
+ virtual bool Deserialize(const TiXmlNode *node);
+
+ SettingUpdateType GetType() const { return m_type; }
+ const std::string& GetValue() const { return m_value; }
+
+private:
+ bool setType(const std::string &type);
+
+ SettingUpdateType m_type = SettingUpdateType::Unknown;
+ std::string m_value;
+
+ static Logger s_logger;
+};
diff --git a/xbmc/settings/lib/SettingsManager.cpp b/xbmc/settings/lib/SettingsManager.cpp
new file mode 100644
index 0000000..9ff7d61
--- /dev/null
+++ b/xbmc/settings/lib/SettingsManager.cpp
@@ -0,0 +1,1424 @@
+/*
+ * Copyright (C) 2013-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 "SettingsManager.h"
+
+#include "ServiceBroker.h"
+#include "Setting.h"
+#include "SettingDefinitions.h"
+#include "SettingSection.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <map>
+#include <mutex>
+#include <shared_mutex>
+#include <unordered_set>
+#include <utility>
+
+const uint32_t CSettingsManager::Version = 2;
+const uint32_t CSettingsManager::MinimumSupportedVersion = 0;
+
+bool ParseSettingIdentifier(const std::string& settingId, std::string& categoryTag, std::string& settingTag)
+{
+ static const std::string Separator = ".";
+
+ if (settingId.empty())
+ return false;
+
+ auto parts = StringUtils::Split(settingId, Separator);
+ if (parts.size() < 1 || parts.at(0).empty())
+ return false;
+
+ if (parts.size() == 1)
+ {
+ settingTag = parts.at(0);
+ return true;
+ }
+
+ // get the category tag and remove it from the parts
+ categoryTag = parts.at(0);
+ parts.erase(parts.begin());
+
+ // put together the setting tag
+ settingTag = StringUtils::Join(parts, Separator);
+
+ return true;
+}
+
+CSettingsManager::CSettingsManager()
+ : m_logger(CServiceBroker::GetLogging().GetLogger("CSettingsManager"))
+{
+}
+
+CSettingsManager::~CSettingsManager()
+{
+ // first clear all registered settings handler and subsettings
+ // implementations because we can't be sure that they are still valid
+ m_settingsHandlers.clear();
+ m_settingCreators.clear();
+ m_settingControlCreators.clear();
+
+ Clear();
+}
+
+uint32_t CSettingsManager::ParseVersion(const TiXmlElement* root) const
+{
+ // try to get and check the version
+ uint32_t version = 0;
+ root->QueryUnsignedAttribute(SETTING_XML_ROOT_VERSION, &version);
+
+ return version;
+}
+
+bool CSettingsManager::Initialize(const TiXmlElement *root)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+ std::unique_lock<CSharedSection> settingsLock(m_settingsCritical);
+ if (m_initialized || root == nullptr)
+ return false;
+
+ if (!StringUtils::EqualsNoCase(root->ValueStr(), SETTING_XML_ROOT))
+ {
+ m_logger->error("error reading settings definition: doesn't contain <" SETTING_XML_ROOT
+ "> tag");
+ return false;
+ }
+
+ // try to get and check the version
+ uint32_t version = ParseVersion(root);
+ if (version == 0)
+ m_logger->warn("missing " SETTING_XML_ROOT_VERSION " attribute", SETTING_XML_ROOT_VERSION);
+
+ if (MinimumSupportedVersion >= version+1)
+ {
+ m_logger->error("unable to read setting definitions from version {} (minimum version: {})",
+ version, MinimumSupportedVersion);
+ return false;
+ }
+ if (version > Version)
+ {
+ m_logger->error("unable to read setting definitions from version {} (current version: {})",
+ version, Version);
+ return false;
+ }
+
+ auto sectionNode = root->FirstChild(SETTING_XML_ELM_SECTION);
+ while (sectionNode != nullptr)
+ {
+ std::string sectionId;
+ if (CSettingSection::DeserializeIdentification(sectionNode, sectionId))
+ {
+ SettingSectionPtr section = nullptr;
+ auto itSection = m_sections.find(sectionId);
+ bool update = (itSection != m_sections.end());
+ if (!update)
+ section = std::make_shared<CSettingSection>(sectionId, this);
+ else
+ section = itSection->second;
+
+ if (section->Deserialize(sectionNode, update))
+ AddSection(section);
+ else
+ {
+ m_logger->warn("unable to read section \"{}\"", sectionId);
+ }
+ }
+
+ sectionNode = sectionNode->NextSibling(SETTING_XML_ELM_SECTION);
+ }
+
+ return true;
+}
+
+bool CSettingsManager::Load(const TiXmlElement *root, bool &updated, bool triggerEvents /* = true */, std::map<std::string, SettingPtr> *loadedSettings /* = nullptr */)
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ std::unique_lock<CSharedSection> settingsLock(m_settingsCritical);
+ if (m_loaded || root == nullptr)
+ return false;
+
+ if (triggerEvents && !OnSettingsLoading())
+ return false;
+
+ // try to get and check the version
+ uint32_t version = ParseVersion(root);
+ if (version == 0)
+ m_logger->warn("missing {} attribute", SETTING_XML_ROOT_VERSION);
+
+ if (MinimumSupportedVersion >= version+1)
+ {
+ m_logger->error("unable to read setting values from version {} (minimum version: {})", version,
+ MinimumSupportedVersion);
+ return false;
+ }
+ if (version > Version)
+ {
+ m_logger->error("unable to read setting values from version {} (current version: {})", version,
+ Version);
+ return false;
+ }
+
+ if (!Deserialize(root, updated, loadedSettings))
+ return false;
+
+ if (triggerEvents)
+ OnSettingsLoaded();
+
+ return true;
+}
+
+bool CSettingsManager::Save(
+ const ISettingsValueSerializer* serializer, std::string& serializedValues) const
+{
+ if (serializer == nullptr)
+ return false;
+
+ std::shared_lock<CSharedSection> lock(m_critical);
+ std::shared_lock<CSharedSection> settingsLock(m_settingsCritical);
+ if (!m_initialized)
+ return false;
+
+ if (!OnSettingsSaving())
+ return false;
+
+ serializedValues = serializer->SerializeValues(this);
+
+ OnSettingsSaved();
+
+ return true;
+}
+
+void CSettingsManager::Unload()
+{
+ std::unique_lock<CSharedSection> lock(m_settingsCritical);
+ if (!m_loaded)
+ return;
+
+ // needs to be set before calling CSetting::Reset() to avoid calls to
+ // OnSettingChanging() and OnSettingChanged()
+ m_loaded = false;
+
+ for (auto& setting : m_settings)
+ setting.second.setting->Reset();
+
+ OnSettingsUnloaded();
+}
+
+void CSettingsManager::Clear()
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+ Unload();
+
+ m_settings.clear();
+ m_sections.clear();
+
+ OnSettingsCleared();
+
+ m_initialized = false;
+}
+
+bool CSettingsManager::LoadSetting(const TiXmlNode *node, const std::string &settingId)
+{
+ bool updated = false;
+ return LoadSetting(node, settingId, updated);
+}
+
+bool CSettingsManager::LoadSetting(const TiXmlNode *node, const std::string &settingId, bool &updated)
+{
+ updated = false;
+
+ if (node == nullptr)
+ return false;
+
+ auto setting = GetSetting(settingId);
+ if (setting == nullptr)
+ return false;
+
+ return LoadSetting(node, setting, updated);
+}
+
+void CSettingsManager::SetInitialized()
+{
+ std::unique_lock<CSharedSection> lock(m_settingsCritical);
+ if (m_initialized)
+ return;
+
+ m_initialized = true;
+
+ // resolve any reference settings
+ for (const auto& section : m_sections)
+ ResolveReferenceSettings(section.second);
+
+ // remove any incomplete settings
+ CleanupIncompleteSettings();
+
+ // figure out all the dependencies between settings
+ for (const auto& setting : m_settings)
+ ResolveSettingDependencies(setting.second);
+}
+
+void CSettingsManager::AddSection(const SettingSectionPtr& section)
+{
+ if (section == nullptr)
+ return;
+
+ std::unique_lock<CSharedSection> lock(m_critical);
+ std::unique_lock<CSharedSection> settingsLock(m_settingsCritical);
+
+ section->CheckRequirements();
+ m_sections[section->GetId()] = section;
+
+ // get all settings and add them to the settings map
+ std::set<SettingPtr> newSettings;
+ for (const auto& category : section->GetCategories())
+ {
+ category->CheckRequirements();
+ for (auto& group : category->GetGroups())
+ {
+ group->CheckRequirements();
+ for (const auto& setting : group->GetSettings())
+ {
+ AddSetting(setting);
+
+ newSettings.insert(setting);
+ }
+ }
+ }
+
+ if (m_initialized && !newSettings.empty())
+ {
+ // resolve any reference settings in the new section
+ ResolveReferenceSettings(section);
+
+ // cleanup any newly added incomplete settings
+ CleanupIncompleteSettings();
+
+ // resolve any dependencies for the newly added settings
+ for (const auto& setting : newSettings)
+ ResolveSettingDependencies(setting);
+ }
+}
+
+bool CSettingsManager::AddSetting(const std::shared_ptr<CSetting>& setting,
+ const std::shared_ptr<CSettingSection>& section,
+ const std::shared_ptr<CSettingCategory>& category,
+ const std::shared_ptr<CSettingGroup>& group)
+{
+ if (setting == nullptr || section == nullptr || category == nullptr || group == nullptr)
+ return false;
+
+ std::unique_lock<CSharedSection> lock(m_critical);
+ std::unique_lock<CSharedSection> settingsLock(m_settingsCritical);
+
+ // check if a setting with the given ID already exists
+ if (FindSetting(setting->GetId()) != m_settings.end())
+ return false;
+
+ // if the given setting has not been added to the group yet, do it now
+ auto settings = group->GetSettings();
+ if (std::find(settings.begin(), settings.end(), setting) == settings.end())
+ group->AddSetting(setting);
+
+ // if the given group has not been added to the category yet, do it now
+ auto groups = category->GetGroups();
+ if (std::find(groups.begin(), groups.end(), group) == groups.end())
+ category->AddGroup(group);
+
+ // if the given category has not been added to the section yet, do it now
+ auto categories = section->GetCategories();
+ if (std::find(categories.begin(), categories.end(), category) == categories.end())
+ section->AddCategory(category);
+
+ // check if the given section exists and matches
+ auto sectionPtr = GetSection(section->GetId());
+ if (sectionPtr != nullptr && sectionPtr != section)
+ return false;
+
+ // if the section doesn't exist yet, add it
+ if (sectionPtr == nullptr)
+ AddSection(section);
+ else
+ {
+ // add the setting
+ AddSetting(setting);
+
+ if (m_initialized)
+ {
+ // cleanup any newly added incomplete setting
+ CleanupIncompleteSettings();
+
+ // resolve any dependencies for the newly added setting
+ ResolveSettingDependencies(setting);
+ }
+ }
+
+ return true;
+}
+
+void CSettingsManager::RegisterCallback(ISettingCallback *callback, const std::set<std::string> &settingList)
+{
+ std::unique_lock<CSharedSection> lock(m_settingsCritical);
+ if (callback == nullptr)
+ return;
+
+ for (const auto& setting : settingList)
+ {
+ auto itSetting = FindSetting(setting);
+ if (itSetting == m_settings.end())
+ {
+ if (m_initialized)
+ continue;
+
+ Setting tmpSetting = {};
+ std::pair<SettingMap::iterator, bool> tmpIt = InsertSetting(setting, tmpSetting);
+ itSetting = tmpIt.first;
+ }
+
+ itSetting->second.callbacks.insert(callback);
+ }
+}
+
+void CSettingsManager::UnregisterCallback(ISettingCallback *callback)
+{
+ std::unique_lock<CSharedSection> lock(m_settingsCritical);
+ for (auto& setting : m_settings)
+ setting.second.callbacks.erase(callback);
+}
+
+void CSettingsManager::RegisterSettingType(const std::string &settingType, ISettingCreator *settingCreator)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+ if (settingType.empty() || settingCreator == nullptr)
+ return;
+
+ auto creatorIt = m_settingCreators.find(settingType);
+ if (creatorIt == m_settingCreators.end())
+ m_settingCreators.insert(std::make_pair(settingType, settingCreator));
+}
+
+void CSettingsManager::RegisterSettingControl(const std::string &controlType, ISettingControlCreator *settingControlCreator)
+{
+ if (controlType.empty() || settingControlCreator == nullptr)
+ return;
+
+ std::unique_lock<CSharedSection> lock(m_critical);
+ auto creatorIt = m_settingControlCreators.find(controlType);
+ if (creatorIt == m_settingControlCreators.end())
+ m_settingControlCreators.insert(std::make_pair(controlType, settingControlCreator));
+}
+
+void CSettingsManager::RegisterSettingsHandler(ISettingsHandler *settingsHandler, bool bFront /* = false */)
+{
+ if (settingsHandler == nullptr)
+ return;
+
+ std::unique_lock<CSharedSection> lock(m_critical);
+ if (find(m_settingsHandlers.begin(), m_settingsHandlers.end(), settingsHandler) == m_settingsHandlers.end())
+ {
+ if (bFront)
+ m_settingsHandlers.insert(m_settingsHandlers.begin(), settingsHandler);
+ else
+ m_settingsHandlers.emplace_back(settingsHandler);
+ }
+}
+
+void CSettingsManager::UnregisterSettingsHandler(ISettingsHandler *settingsHandler)
+{
+ if (settingsHandler == nullptr)
+ return;
+
+ std::unique_lock<CSharedSection> lock(m_critical);
+ auto it = std::find(m_settingsHandlers.begin(), m_settingsHandlers.end(), settingsHandler);
+ if (it != m_settingsHandlers.end())
+ m_settingsHandlers.erase(it);
+}
+
+void CSettingsManager::RegisterSettingOptionsFiller(const std::string &identifier, IntegerSettingOptionsFiller optionsFiller)
+{
+ if (identifier.empty() || optionsFiller == nullptr)
+ return;
+
+ RegisterSettingOptionsFiller(identifier, reinterpret_cast<void*>(optionsFiller), SettingOptionsFillerType::Integer);
+}
+
+void CSettingsManager::RegisterSettingOptionsFiller(const std::string &identifier, StringSettingOptionsFiller optionsFiller)
+{
+ if (identifier.empty() || optionsFiller == nullptr)
+ return;
+
+ RegisterSettingOptionsFiller(identifier, reinterpret_cast<void*>(optionsFiller), SettingOptionsFillerType::String);
+}
+
+void CSettingsManager::UnregisterSettingOptionsFiller(const std::string &identifier)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+ m_optionsFillers.erase(identifier);
+}
+
+void* CSettingsManager::GetSettingOptionsFiller(const SettingConstPtr& setting)
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ if (setting == nullptr)
+ return nullptr;
+
+ // get the option filler's identifier
+ std::string filler;
+ if (setting->GetType() == SettingType::Integer)
+ filler = std::static_pointer_cast<const CSettingInt>(setting)->GetOptionsFillerName();
+ else if (setting->GetType() == SettingType::String)
+ filler = std::static_pointer_cast<const CSettingString>(setting)->GetOptionsFillerName();
+
+ if (filler.empty())
+ return nullptr;
+
+ // check if such an option filler is known
+ auto fillerIt = m_optionsFillers.find(filler);
+ if (fillerIt == m_optionsFillers.end())
+ return nullptr;
+
+ if (fillerIt->second.filler == nullptr)
+ return nullptr;
+
+ // make sure the option filler's type matches the setting's type
+ switch (fillerIt->second.type)
+ {
+ case SettingOptionsFillerType::Integer:
+ {
+ if (setting->GetType() != SettingType::Integer)
+ return nullptr;
+
+ break;
+ }
+
+ case SettingOptionsFillerType::String:
+ {
+ if (setting->GetType() != SettingType::String)
+ return nullptr;
+
+ break;
+ }
+
+ default:
+ return nullptr;
+ }
+
+ return fillerIt->second.filler;
+}
+
+bool CSettingsManager::HasSettings() const
+{
+ return !m_settings.empty();
+}
+
+SettingPtr CSettingsManager::GetSetting(const std::string &id) const
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ if (id.empty())
+ return nullptr;
+
+ auto setting = FindSetting(id);
+ if (setting != m_settings.end())
+ {
+ if (setting->second.setting->IsReference())
+ return GetSetting(setting->second.setting->GetReferencedId());
+ return setting->second.setting;
+ }
+
+ m_logger->debug("requested setting ({}) was not found.", id);
+ return nullptr;
+}
+
+SettingSectionList CSettingsManager::GetSections() const
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ SettingSectionList sections;
+ for (const auto& section : m_sections)
+ sections.push_back(section.second);
+
+ return sections;
+}
+
+SettingSectionPtr CSettingsManager::GetSection(std::string section) const
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ if (section.empty())
+ return nullptr;
+
+ StringUtils::ToLower(section);
+
+ auto sectionIt = m_sections.find(section);
+ if (sectionIt != m_sections.end())
+ return sectionIt->second;
+
+ m_logger->debug("requested setting section ({}) was not found.", section);
+ return nullptr;
+}
+
+SettingDependencyMap CSettingsManager::GetDependencies(const std::string &id) const
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ auto setting = FindSetting(id);
+ if (setting == m_settings.end())
+ return SettingDependencyMap();
+
+ return setting->second.dependencies;
+}
+
+SettingDependencyMap CSettingsManager::GetDependencies(const SettingConstPtr& setting) const
+{
+ if (setting == nullptr)
+ return SettingDependencyMap();
+
+ return GetDependencies(setting->GetId());
+}
+
+bool CSettingsManager::GetBool(const std::string &id) const
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::Boolean)
+ return false;
+
+ return std::static_pointer_cast<CSettingBool>(setting)->GetValue();
+}
+
+bool CSettingsManager::SetBool(const std::string &id, bool value)
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::Boolean)
+ return false;
+
+ return std::static_pointer_cast<CSettingBool>(setting)->SetValue(value);
+}
+
+bool CSettingsManager::ToggleBool(const std::string &id)
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::Boolean)
+ return false;
+
+ return SetBool(id, !std::static_pointer_cast<CSettingBool>(setting)->GetValue());
+}
+
+int CSettingsManager::GetInt(const std::string &id) const
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::Integer)
+ return 0;
+
+ return std::static_pointer_cast<CSettingInt>(setting)->GetValue();
+}
+
+bool CSettingsManager::SetInt(const std::string &id, int value)
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::Integer)
+ return false;
+
+ return std::static_pointer_cast<CSettingInt>(setting)->SetValue(value);
+}
+
+double CSettingsManager::GetNumber(const std::string &id) const
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::Number)
+ return 0.0;
+
+ return std::static_pointer_cast<CSettingNumber>(setting)->GetValue();
+}
+
+bool CSettingsManager::SetNumber(const std::string &id, double value)
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::Number)
+ return false;
+
+ return std::static_pointer_cast<CSettingNumber>(setting)->SetValue(value);
+}
+
+std::string CSettingsManager::GetString(const std::string &id) const
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::String)
+ return "";
+
+ return std::static_pointer_cast<CSettingString>(setting)->GetValue();
+}
+
+bool CSettingsManager::SetString(const std::string &id, const std::string &value)
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::String)
+ return false;
+
+ return std::static_pointer_cast<CSettingString>(setting)->SetValue(value);
+}
+
+std::vector< std::shared_ptr<CSetting> > CSettingsManager::GetList(const std::string &id) const
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::List)
+ return std::vector< std::shared_ptr<CSetting> >();
+
+ return std::static_pointer_cast<CSettingList>(setting)->GetValue();
+}
+
+bool CSettingsManager::SetList(const std::string &id, const std::vector< std::shared_ptr<CSetting> > &value)
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr || setting->GetType() != SettingType::List)
+ return false;
+
+ return std::static_pointer_cast<CSettingList>(setting)->SetValue(value);
+}
+
+bool CSettingsManager::SetDefault(const std::string &id)
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ SettingPtr setting = GetSetting(id);
+ if (setting == nullptr)
+ return false;
+
+ setting->Reset();
+ return true;
+}
+
+void CSettingsManager::SetDefaults()
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ for (auto& setting : m_settings)
+ setting.second.setting->Reset();
+}
+
+void CSettingsManager::AddCondition(const std::string &condition)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+ if (condition.empty())
+ return;
+
+ m_conditions.AddCondition(condition);
+}
+
+void CSettingsManager::AddDynamicCondition(const std::string &identifier, SettingConditionCheck condition, void *data /*= nullptr*/)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+ if (identifier.empty() || condition == nullptr)
+ return;
+
+ m_conditions.AddDynamicCondition(identifier, condition, data);
+}
+
+void CSettingsManager::RemoveDynamicCondition(const std::string &identifier)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+ if (identifier.empty())
+ return;
+
+ m_conditions.RemoveDynamicCondition(identifier);
+}
+
+bool CSettingsManager::Serialize(TiXmlNode *parent) const
+{
+ if (parent == nullptr)
+ return false;
+
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+
+ for (const auto& setting : m_settings)
+ {
+ if (setting.second.setting->IsReference() ||
+ setting.second.setting->GetType() == SettingType::Action)
+ continue;
+
+ TiXmlElement settingElement(SETTING_XML_ELM_SETTING);
+ settingElement.SetAttribute(SETTING_XML_ATTR_ID, setting.second.setting->GetId());
+
+ // add the default attribute
+ if (setting.second.setting->IsDefault())
+ settingElement.SetAttribute(SETTING_XML_ELM_DEFAULT, "true");
+
+ // add the value
+ TiXmlText value(setting.second.setting->ToString());
+ settingElement.InsertEndChild(value);
+
+ if (parent->InsertEndChild(settingElement) == nullptr)
+ {
+ m_logger->warn("unable to write <" SETTING_XML_ELM_SETTING " id=\"{}\"> tag",
+ setting.second.setting->GetId());
+ continue;
+ }
+ }
+
+ return true;
+}
+
+bool CSettingsManager::Deserialize(const TiXmlNode *node, bool &updated, std::map<std::string, SettingPtr> *loadedSettings /* = nullptr */)
+{
+ updated = false;
+
+ if (node == nullptr)
+ return false;
+
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+
+ // TODO: ideally this would be done by going through all <setting> elements
+ // in node but as long as we have to support the v1- format that's not possible
+ for (auto& setting : m_settings)
+ {
+ bool settingUpdated = false;
+ if (LoadSetting(node, setting.second.setting, settingUpdated))
+ {
+ updated |= settingUpdated;
+ if (loadedSettings != nullptr)
+ loadedSettings->insert(make_pair(setting.first, setting.second.setting));
+ }
+ }
+
+ return true;
+}
+
+bool CSettingsManager::OnSettingChanging(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == nullptr)
+ return false;
+
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ if (!m_loaded)
+ return true;
+
+ auto settingIt = FindSetting(setting->GetId());
+ if (settingIt == m_settings.end())
+ return false;
+
+ Setting settingData = settingIt->second;
+ // now that we have a copy of the setting's data, we can leave the lock
+ lock.unlock();
+
+ for (auto& callback : settingData.callbacks)
+ {
+ if (!callback->OnSettingChanging(setting))
+ return false;
+ }
+
+ // if this is a reference setting apply the same change to the referenced setting
+ if (setting->IsReference())
+ {
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ auto referencedSettingIt = FindSetting(setting->GetReferencedId());
+ if (referencedSettingIt != m_settings.end())
+ {
+ Setting referencedSettingData = referencedSettingIt->second;
+ // now that we have a copy of the setting's data, we can leave the lock
+ lock.unlock();
+
+ referencedSettingData.setting->FromString(setting->ToString());
+ }
+ }
+ else if (!settingData.references.empty())
+ {
+ // if the changed setting is referenced by other settings apply the same change to the referencing settings
+ std::unordered_set<SettingPtr> referenceSettings;
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ for (const auto& reference : settingData.references)
+ {
+ auto referenceSettingIt = FindSetting(reference);
+ if (referenceSettingIt != m_settings.end())
+ referenceSettings.insert(referenceSettingIt->second.setting);
+ }
+ // now that we have a copy of the setting's data, we can leave the lock
+ lock.unlock();
+
+ for (auto& referenceSetting : referenceSettings)
+ referenceSetting->FromString(setting->ToString());
+ }
+
+ return true;
+}
+
+void CSettingsManager::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ if (!m_loaded || setting == nullptr)
+ return;
+
+ auto settingIt = FindSetting(setting->GetId());
+ if (settingIt == m_settings.end())
+ return;
+
+ Setting settingData = settingIt->second;
+ // now that we have a copy of the setting's data, we can leave the lock
+ lock.unlock();
+
+ for (auto& callback : settingData.callbacks)
+ callback->OnSettingChanged(setting);
+
+ // now handle any settings which depend on the changed setting
+ auto dependencies = GetDependencies(setting);
+ for (const auto& deps : dependencies)
+ {
+ for (const auto& dep : deps.second)
+ UpdateSettingByDependency(deps.first, dep);
+ }
+}
+
+void CSettingsManager::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ if (!m_loaded || setting == nullptr)
+ return;
+
+ auto settingIt = FindSetting(setting->GetId());
+ if (settingIt == m_settings.end())
+ return;
+
+ Setting settingData = settingIt->second;
+ // now that we have a copy of the setting's data, we can leave the lock
+ lock.unlock();
+
+ for (auto& callback : settingData.callbacks)
+ callback->OnSettingAction(setting);
+}
+
+bool CSettingsManager::OnSettingUpdate(const SettingPtr& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode)
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ if (setting == nullptr)
+ return false;
+
+ auto settingIt = FindSetting(setting->GetId());
+ if (settingIt == m_settings.end())
+ return false;
+
+ Setting settingData = settingIt->second;
+ // now that we have a copy of the setting's data, we can leave the lock
+ lock.unlock();
+
+ bool ret = false;
+ for (auto& callback : settingData.callbacks)
+ ret |= callback->OnSettingUpdate(setting, oldSettingId, oldSettingNode);
+
+ return ret;
+}
+
+void CSettingsManager::OnSettingPropertyChanged(const std::shared_ptr<const CSetting>& setting,
+ const char* propertyName)
+{
+ std::shared_lock<CSharedSection> lock(m_settingsCritical);
+ if (!m_loaded || setting == nullptr)
+ return;
+
+ auto settingIt = FindSetting(setting->GetId());
+ if (settingIt == m_settings.end())
+ return;
+
+ Setting settingData = settingIt->second;
+ // now that we have a copy of the setting's data, we can leave the lock
+ lock.unlock();
+
+ for (auto& callback : settingData.callbacks)
+ callback->OnSettingPropertyChanged(setting, propertyName);
+
+ // check the changed property and if it may have an influence on the
+ // children of the setting
+ SettingDependencyType dependencyType = SettingDependencyType::Unknown;
+ if (StringUtils::EqualsNoCase(propertyName, "enabled"))
+ dependencyType = SettingDependencyType::Enable;
+ else if (StringUtils::EqualsNoCase(propertyName, "visible"))
+ dependencyType = SettingDependencyType::Visible;
+
+ if (dependencyType != SettingDependencyType::Unknown)
+ {
+ for (const auto& child : settingIt->second.children)
+ UpdateSettingByDependency(child, dependencyType);
+ }
+}
+
+SettingPtr CSettingsManager::CreateSetting(const std::string &settingType, const std::string &settingId, CSettingsManager *settingsManager /* = nullptr */) const
+{
+ if (StringUtils::EqualsNoCase(settingType, "boolean"))
+ return std::make_shared<CSettingBool>(settingId, const_cast<CSettingsManager*>(this));
+ else if (StringUtils::EqualsNoCase(settingType, "integer"))
+ return std::make_shared<CSettingInt>(settingId, const_cast<CSettingsManager*>(this));
+ else if (StringUtils::EqualsNoCase(settingType, "number"))
+ return std::make_shared<CSettingNumber>(settingId, const_cast<CSettingsManager*>(this));
+ else if (StringUtils::EqualsNoCase(settingType, "string"))
+ return std::make_shared<CSettingString>(settingId, const_cast<CSettingsManager*>(this));
+ else if (StringUtils::EqualsNoCase(settingType, "action"))
+ return std::make_shared<CSettingAction>(settingId, const_cast<CSettingsManager*>(this));
+ else if (settingType.size() > 6 &&
+ StringUtils::StartsWith(settingType, "list[") &&
+ StringUtils::EndsWith(settingType, "]"))
+ {
+ std::string elementType = StringUtils::Mid(settingType, 5, settingType.size() - 6);
+ SettingPtr elementSetting = CreateSetting(elementType, settingId + ".definition", const_cast<CSettingsManager*>(this));
+ if (elementSetting != nullptr)
+ return std::make_shared<CSettingList>(settingId, elementSetting, const_cast<CSettingsManager*>(this));
+ }
+
+ std::shared_lock<CSharedSection> lock(m_critical);
+ auto creator = m_settingCreators.find(settingType);
+ if (creator != m_settingCreators.end())
+ return creator->second->CreateSetting(settingType, settingId, const_cast<CSettingsManager*>(this));
+
+ return nullptr;
+}
+
+std::shared_ptr<ISettingControl> CSettingsManager::CreateControl(const std::string &controlType) const
+{
+ if (controlType.empty())
+ return nullptr;
+
+ std::shared_lock<CSharedSection> lock(m_critical);
+ auto creator = m_settingControlCreators.find(controlType);
+ if (creator != m_settingControlCreators.end() && creator->second != nullptr)
+ return creator->second->CreateControl(controlType);
+
+ return nullptr;
+}
+
+bool CSettingsManager::OnSettingsLoading()
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ for (const auto& settingsHandler : m_settingsHandlers)
+ {
+ if (!settingsHandler->OnSettingsLoading())
+ return false;
+ }
+
+ return true;
+}
+
+void CSettingsManager::OnSettingsUnloaded()
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ for (const auto& settingsHandler : m_settingsHandlers)
+ settingsHandler->OnSettingsUnloaded();
+}
+
+void CSettingsManager::OnSettingsLoaded()
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ for (const auto& settingsHandler : m_settingsHandlers)
+ settingsHandler->OnSettingsLoaded();
+}
+
+bool CSettingsManager::OnSettingsSaving() const
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ for (const auto& settingsHandler : m_settingsHandlers)
+ {
+ if (!settingsHandler->OnSettingsSaving())
+ return false;
+ }
+
+ return true;
+}
+
+void CSettingsManager::OnSettingsSaved() const
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ for (const auto& settingsHandler : m_settingsHandlers)
+ settingsHandler->OnSettingsSaved();
+}
+
+void CSettingsManager::OnSettingsCleared()
+{
+ std::shared_lock<CSharedSection> lock(m_critical);
+ for (const auto& settingsHandler : m_settingsHandlers)
+ settingsHandler->OnSettingsCleared();
+}
+
+bool CSettingsManager::LoadSetting(const TiXmlNode* node, const SettingPtr& setting, bool& updated)
+{
+ updated = false;
+
+ if (node == nullptr || setting == nullptr)
+ return false;
+
+ if (setting->GetType() == SettingType::Action)
+ return false;
+
+ auto settingId = setting->GetId();
+ if (setting->IsReference())
+ settingId = setting->GetReferencedId();
+
+ const TiXmlElement* settingElement = nullptr;
+ // try to split the setting identifier into category and subsetting identifier (v1-)
+ std::string categoryTag, settingTag;
+ if (ParseSettingIdentifier(settingId, categoryTag, settingTag))
+ {
+ auto categoryNode = node;
+ if (!categoryTag.empty())
+ categoryNode = node->FirstChild(categoryTag);
+
+ if (categoryNode != nullptr)
+ settingElement = categoryNode->FirstChildElement(settingTag);
+ }
+
+ if (settingElement == nullptr)
+ {
+ // check if the setting is stored using its full setting identifier (v2+)
+ settingElement = node->FirstChildElement(SETTING_XML_ELM_SETTING);
+ while (settingElement != nullptr)
+ {
+ const auto id = settingElement->Attribute(SETTING_XML_ATTR_ID);
+ if (id != nullptr && settingId.compare(id) == 0)
+ break;
+
+ settingElement = settingElement->NextSiblingElement(SETTING_XML_ELM_SETTING);
+ }
+ }
+
+ if (settingElement == nullptr)
+ return false;
+
+ // check if the default="true" attribute is set for the value
+ auto isDefaultAttribute = settingElement->Attribute(SETTING_XML_ELM_DEFAULT);
+ bool isDefault = isDefaultAttribute != nullptr && StringUtils::EqualsNoCase(isDefaultAttribute, "true");
+
+ if (!setting->FromString(settingElement->FirstChild() != nullptr ? settingElement->FirstChild()->ValueStr() : StringUtils::Empty))
+ {
+ m_logger->warn("unable to read value of setting \"{}\"", settingId);
+ return false;
+ }
+
+ // check if we need to perform any update logic for the setting
+ auto updates = setting->GetUpdates();
+ for (const auto& update : updates)
+ updated |= UpdateSetting(node, setting, update);
+
+ // the setting's value hasn't been updated and is the default value
+ // so we can reset it to the default value (in case the default value has changed)
+ if (!updated && isDefault)
+ setting->Reset();
+
+ return true;
+}
+
+bool CSettingsManager::UpdateSetting(const TiXmlNode* node,
+ const SettingPtr& setting,
+ const CSettingUpdate& update)
+{
+ if (node == nullptr || setting == nullptr || update.GetType() == SettingUpdateType::Unknown)
+ return false;
+
+ bool updated = false;
+ const char *oldSetting = nullptr;
+ const TiXmlNode *oldSettingNode = nullptr;
+ if (update.GetType() == SettingUpdateType::Rename)
+ {
+ if (update.GetValue().empty())
+ return false;
+
+ oldSetting = update.GetValue().c_str();
+ std::string categoryTag, settingTag;
+ if (!ParseSettingIdentifier(oldSetting, categoryTag, settingTag))
+ return false;
+
+ auto categoryNode = node;
+ if (!categoryTag.empty())
+ {
+ categoryNode = node->FirstChild(categoryTag);
+ if (categoryNode == nullptr)
+ return false;
+ }
+
+ oldSettingNode = categoryNode->FirstChild(settingTag);
+ if (oldSettingNode == nullptr)
+ return false;
+
+ if (setting->FromString(oldSettingNode->FirstChild() != nullptr ? oldSettingNode->FirstChild()->ValueStr() : StringUtils::Empty))
+ updated = true;
+ else
+ m_logger->warn("unable to update \"{}\" through automatically renaming from \"{}\"",
+ setting->GetId(), oldSetting);
+ }
+
+ updated |= OnSettingUpdate(setting, oldSetting, oldSettingNode);
+ return updated;
+}
+
+void CSettingsManager::UpdateSettingByDependency(const std::string &settingId, const CSettingDependency &dependency)
+{
+ UpdateSettingByDependency(settingId, dependency.GetType());
+}
+
+void CSettingsManager::UpdateSettingByDependency(const std::string &settingId, SettingDependencyType dependencyType)
+{
+ auto settingIt = FindSetting(settingId);
+ if (settingIt == m_settings.end())
+ return;
+ SettingPtr setting = settingIt->second.setting;
+ if (setting == nullptr)
+ return;
+
+ switch (dependencyType)
+ {
+ case SettingDependencyType::Enable:
+ // just trigger the property changed callback and a call to
+ // CSetting::IsEnabled() will automatically determine the new
+ // enabled state
+ OnSettingPropertyChanged(setting, "enabled");
+ break;
+
+ case SettingDependencyType::Update:
+ {
+ SettingType type = setting->GetType();
+ if (type == SettingType::Integer)
+ {
+ auto settingInt = std::static_pointer_cast<CSettingInt>(setting);
+ if (settingInt->GetOptionsType() == SettingOptionsType::Dynamic)
+ settingInt->UpdateDynamicOptions();
+ }
+ else if (type == SettingType::String)
+ {
+ auto settingString = std::static_pointer_cast<CSettingString>(setting);
+ if (settingString->GetOptionsType() == SettingOptionsType::Dynamic)
+ settingString->UpdateDynamicOptions();
+ }
+ break;
+ }
+
+ case SettingDependencyType::Visible:
+ // just trigger the property changed callback and a call to
+ // CSetting::IsVisible() will automatically determine the new
+ // visible state
+ OnSettingPropertyChanged(setting, "visible");
+ break;
+
+ case SettingDependencyType::Unknown:
+ default:
+ break;
+ }
+}
+
+void CSettingsManager::AddSetting(const std::shared_ptr<CSetting>& setting)
+{
+ setting->CheckRequirements();
+
+ auto addedSetting = FindSetting(setting->GetId());
+ if (addedSetting == m_settings.end())
+ {
+ Setting tmpSetting = {};
+ auto tmpIt = InsertSetting(setting->GetId(), tmpSetting);
+ addedSetting = tmpIt.first;
+ }
+
+ if (addedSetting->second.setting == nullptr)
+ {
+ addedSetting->second.setting = setting;
+ setting->SetCallback(this);
+ }
+}
+
+void CSettingsManager::ResolveReferenceSettings(const std::shared_ptr<CSettingSection>& section)
+{
+ struct GroupedReferenceSettings
+ {
+ SettingPtr referencedSetting;
+ std::unordered_set<SettingPtr> referenceSettings;
+ };
+ std::map<std::string, GroupedReferenceSettings> groupedReferenceSettings;
+
+ // collect and group all reference(d) settings
+ auto categories = section->GetCategories();
+ for (const auto& category : categories)
+ {
+ auto groups = category->GetGroups();
+ for (auto& group : groups)
+ {
+ auto settings = group->GetSettings();
+ for (const auto& setting : settings)
+ {
+ if (setting->IsReference())
+ {
+ auto referencedSettingId = setting->GetReferencedId();
+ auto itGroupedReferenceSetting = groupedReferenceSettings.find(referencedSettingId);
+ if (itGroupedReferenceSetting == groupedReferenceSettings.end())
+ {
+ SettingPtr referencedSetting = nullptr;
+ auto itReferencedSetting = FindSetting(referencedSettingId);
+ if (itReferencedSetting == m_settings.end())
+ {
+ m_logger->warn("missing referenced setting \"{}\"", referencedSettingId);
+ continue;
+ }
+
+ GroupedReferenceSettings groupedReferenceSetting;
+ groupedReferenceSetting.referencedSetting = itReferencedSetting->second.setting;
+
+ itGroupedReferenceSetting = groupedReferenceSettings.insert(
+ std::make_pair(referencedSettingId, groupedReferenceSetting)).first;
+ }
+
+ itGroupedReferenceSetting->second.referenceSettings.insert(setting);
+ }
+ }
+ }
+ }
+
+ if (groupedReferenceSettings.empty())
+ return;
+
+ // merge all reference settings into the referenced setting
+ for (const auto& groupedReferenceSetting : groupedReferenceSettings)
+ {
+ auto itReferencedSetting = FindSetting(groupedReferenceSetting.first);
+ if (itReferencedSetting == m_settings.end())
+ continue;
+
+ for (const auto& referenceSetting : groupedReferenceSetting.second.referenceSettings)
+ {
+ groupedReferenceSetting.second.referencedSetting->MergeDetails(*referenceSetting);
+
+ itReferencedSetting->second.references.insert(referenceSetting->GetId());
+ }
+ }
+
+ // resolve any reference settings
+ for (const auto& category : categories)
+ {
+ auto groups = category->GetGroups();
+ for (auto& group : groups)
+ {
+ auto settings = group->GetSettings();
+ for (const auto& setting : settings)
+ {
+ if (setting->IsReference())
+ {
+ auto referencedSettingId = setting->GetReferencedId();
+ auto itGroupedReferenceSetting = groupedReferenceSettings.find(referencedSettingId);
+ if (itGroupedReferenceSetting != groupedReferenceSettings.end())
+ {
+ const auto referencedSetting = itGroupedReferenceSetting->second.referencedSetting;
+
+ // clone the referenced setting and copy the general properties of the reference setting
+ auto clonedReferencedSetting = referencedSetting->Clone(setting->GetId());
+ clonedReferencedSetting->SetReferencedId(referencedSettingId);
+ clonedReferencedSetting->MergeBasics(*setting);
+
+ group->ReplaceSetting(setting, clonedReferencedSetting);
+
+ // update the setting
+ auto itReferenceSetting = FindSetting(setting->GetId());
+ if (itReferenceSetting != m_settings.end())
+ itReferenceSetting->second.setting = clonedReferencedSetting;
+ }
+ }
+ }
+ }
+ }
+}
+
+void CSettingsManager::CleanupIncompleteSettings()
+{
+ // remove any empty and reference settings
+ for (auto setting = m_settings.begin(); setting != m_settings.end(); )
+ {
+ auto tmpIterator = setting++;
+ if (tmpIterator->second.setting == nullptr)
+ {
+ m_logger->warn("removing empty setting \"{}\"", tmpIterator->first);
+ m_settings.erase(tmpIterator);
+ }
+ }
+}
+
+void CSettingsManager::RegisterSettingOptionsFiller(const std::string &identifier, void *filler, SettingOptionsFillerType type)
+{
+ std::unique_lock<CSharedSection> lock(m_critical);
+ auto it = m_optionsFillers.find(identifier);
+ if (it != m_optionsFillers.end())
+ return;
+
+ SettingOptionsFiller optionsFiller = { filler, type };
+ m_optionsFillers.insert(make_pair(identifier, optionsFiller));
+}
+
+void CSettingsManager::ResolveSettingDependencies(const std::shared_ptr<CSetting>& setting)
+{
+ if (setting == nullptr)
+ return;
+
+ ResolveSettingDependencies(FindSetting(setting->GetId())->second);
+}
+
+void CSettingsManager::ResolveSettingDependencies(const Setting& setting)
+{
+ if (setting.setting == nullptr)
+ return;
+
+ // if the setting has a parent setting, add it to its children
+ auto parentSettingId = setting.setting->GetParent();
+ if (!parentSettingId.empty())
+ {
+ auto itParentSetting = FindSetting(parentSettingId);
+ if (itParentSetting != m_settings.end())
+ itParentSetting->second.children.insert(setting.setting->GetId());
+ }
+
+ // handle all dependencies of the setting
+ const auto& dependencies = setting.setting->GetDependencies();
+ for (const auto& deps : dependencies)
+ {
+ const auto settingIds = deps.GetSettings();
+ for (const auto& settingId : settingIds)
+ {
+ auto settingIt = FindSetting(settingId);
+ if (settingIt == m_settings.end())
+ continue;
+
+ bool newDep = true;
+ auto& settingDeps = settingIt->second.dependencies[setting.setting->GetId()];
+ for (const auto& dep : settingDeps)
+ {
+ if (dep.GetType() == deps.GetType())
+ {
+ newDep = false;
+ break;
+ }
+ }
+
+ if (newDep)
+ settingDeps.push_back(deps);
+ }
+ }
+}
+
+CSettingsManager::SettingMap::const_iterator CSettingsManager::FindSetting(std::string settingId) const
+{
+ StringUtils::ToLower(settingId);
+ return m_settings.find(settingId);
+}
+
+CSettingsManager::SettingMap::iterator CSettingsManager::FindSetting(std::string settingId)
+{
+ StringUtils::ToLower(settingId);
+ return m_settings.find(settingId);
+}
+
+std::pair<CSettingsManager::SettingMap::iterator, bool> CSettingsManager::InsertSetting(std::string settingId, const Setting& setting)
+{
+ StringUtils::ToLower(settingId);
+ return m_settings.insert(std::make_pair(settingId, setting));
+}
diff --git a/xbmc/settings/lib/SettingsManager.h b/xbmc/settings/lib/SettingsManager.h
new file mode 100644
index 0000000..161c1a9
--- /dev/null
+++ b/xbmc/settings/lib/SettingsManager.h
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2013-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 "ISettingCallback.h"
+#include "ISettingControlCreator.h"
+#include "ISettingCreator.h"
+#include "ISettingsHandler.h"
+#include "ISettingsValueSerializer.h"
+#include "Setting.h"
+#include "SettingConditions.h"
+#include "SettingDefinitions.h"
+#include "SettingDependency.h"
+#include "threads/SharedSection.h"
+#include "utils/logtypes.h"
+
+#include <map>
+#include <set>
+#include <unordered_set>
+#include <vector>
+
+class CSettingCategory;
+class CSettingGroup;
+class CSettingSection;
+class CSettingUpdate;
+
+class TiXmlElement;
+class TiXmlNode;
+
+/*!
+ \ingroup settings
+ \brief Settings manager responsible for initializing, loading and handling
+ all settings.
+ */
+class CSettingsManager : public ISettingCreator,
+ public ISettingControlCreator,
+ private ISettingCallback,
+ private ISettingsHandler
+{
+public:
+ /*!
+ \brief Creates a new (uninitialized) settings manager.
+ */
+ CSettingsManager();
+ ~CSettingsManager() override;
+
+ static const uint32_t Version;
+ static const uint32_t MinimumSupportedVersion;
+
+ // implementation of ISettingCreator
+ std::shared_ptr<CSetting> CreateSetting(const std::string &settingType, const std::string &settingId, CSettingsManager *settingsManager = nullptr) const override;
+
+ // implementation of ISettingControlCreator
+ std::shared_ptr<ISettingControl> CreateControl(const std::string &controlType) const override;
+
+ uint32_t GetVersion() const { return Version; }
+ uint32_t GetMinimumSupportedVersion() const { return MinimumSupportedVersion; }
+
+ /*!
+ \brief Try to get the version of the setting definitions/values represented by the given XML element.
+
+ \param root XML element representing setting definitions/values
+ \return Version of the setting definitions/values or 0 if no version has been specified
+ */
+ uint32_t ParseVersion(const TiXmlElement* root) const;
+
+ /*!
+ \brief Initializes the settings manager using the setting definitions
+ represented by the given XML element.
+
+ \param root XML element representing setting definitions
+ \return True if the XML element was successfully deserialized into setting definitions, false otherwise
+ */
+ bool Initialize(const TiXmlElement *root);
+ /*!
+ \brief Loads setting values from the given XML element.
+
+ \param root XML element containing setting values
+ \param updated Whether some settings were automatically updated
+ \param triggerEvents Whether to trigger ISettingCallback methods
+ \param loadedSettings A list to fill with all the successfully loaded settings
+ \return True if the setting values were successfully loaded, false otherwise
+ */
+ bool Load(const TiXmlElement *root, bool &updated, bool triggerEvents = true, std::map<std::string, std::shared_ptr<CSetting>> *loadedSettings = nullptr);
+ /*!
+ \brief Saves the setting values using the given serializer.
+
+ \param serializer Settings value serializer to use
+ \return True if the setting values were successfully serialized, false otherwise
+ */
+ bool Save(const ISettingsValueSerializer* serializer, std::string& serializedValues) const;
+ /*!
+ \brief Unloads the previously loaded setting values.
+
+ The values of all the settings are reset to their default values.
+ */
+ void Unload();
+ /*!
+ \brief Clears the complete settings manager.
+
+ This removes all initialized settings, groups, categories and sections and
+ returns to the uninitialized state. Any registered callbacks or
+ implementations stay registered.
+ */
+ void Clear();
+
+ /*!
+ \brief Loads the setting being represented by the given XML node with the
+ given identifier.
+
+ \param node XML node representing the setting to load
+ \param settingId Setting identifier
+ \return True if the setting was successfully loaded from the given XML node, false otherwise
+ */
+ bool LoadSetting(const TiXmlNode *node, const std::string &settingId);
+
+ /*!
+ \brief Loads the setting being represented by the given XML node with the
+ given identifier.
+
+ \param node XML node representing the setting to load
+ \param settingId Setting identifier
+ \param updated Set to true if the setting's value was updated
+ \return True if the setting was successfully loaded from the given XML node, false otherwise
+ */
+ bool LoadSetting(const TiXmlNode *node, const std::string &settingId, bool &updated);
+
+ /*!
+ \brief Tells the settings system that the initialization is complete.
+
+ Setting values can only be loaded after a complete and successful
+ initialization of the settings system.
+ */
+ void SetInitialized();
+ /*!
+ \brief Returns whether the settings system has been initialized or not.
+ */
+ bool IsInitialized() const { return m_initialized; }
+ /*!
+ \brief Tells the settings system that all setting values
+ have been loaded.
+
+ This manual trigger is necessary to enable the ISettingCallback methods
+ being executed.
+ */
+ void SetLoaded() { m_loaded = true; }
+ /*!
+ \brief Returns whether the settings system has been loaded or not.
+ */
+ bool IsLoaded() const { return m_loaded; }
+
+ /*!
+ \brief Adds the given section, its categories, groups and settings.
+
+ This is possible before and after the setting definitions have been
+ initialized.
+ */
+ void AddSection(const std::shared_ptr<CSettingSection>& section);
+
+ /*!
+ \brief Adds the given setting to the given group in the given category in
+ the given section;
+
+ If the given section has not been added yet, it is added. If the given
+ category has not been added to the given section yet, it is added. If the
+ given group has not been added to the given category yet, it is added. If
+ the given setting has not been added to the given group yet, it is added.
+
+ This is possible before and after the setting definitions have been
+ initialized.
+
+ \param setting New setting to be added
+ \param section Section the new setting should be added to
+ \param category Category the new setting should be added to
+ \param group Group the new setting should be added to
+ \return True if the setting has been added, false otherwise
+ */
+ bool AddSetting(const std::shared_ptr<CSetting>& setting,
+ const std::shared_ptr<CSettingSection>& section,
+ const std::shared_ptr<CSettingCategory>& category,
+ const std::shared_ptr<CSettingGroup>& group);
+
+ /*!
+ \brief Registers the given ISettingCallback implementation to be triggered
+ for the given list of settings.
+
+ \param settingsHandler ISettingsHandler implementation
+ \param settingList List of settings to trigger the given ISettingCallback implementation
+ */
+ void RegisterCallback(ISettingCallback *callback, const std::set<std::string> &settingList);
+ /*!
+ \brief Unregisters the given ISettingCallback implementation.
+
+ \param callback ISettingCallback implementation
+ */
+ void UnregisterCallback(ISettingCallback *callback);
+
+ /*!
+ \brief Registers a custom setting type and its ISettingCreator
+ implementation.
+
+ When a setting definition for a registered custom setting type is found its
+ ISettingCreator implementation is called to create and deserialize the
+ setting definition.
+
+ \param settingType String representation of the custom setting type
+ \param settingCreator ISettingCreator implementation
+ */
+ void RegisterSettingType(const std::string &settingType, ISettingCreator *settingCreator);
+
+ /*!
+ \brief Registers a custom setting control type and its
+ ISettingControlCreator implementation
+
+ When a setting control definition for a registered custom setting control
+ type is found its ISettingControlCreator implementation is called to create
+ and deserialize the setting control definition.
+
+ \param controlType String representation of the custom setting control type
+ \param settingControlCreator ISettingControlCreator implementation
+ */
+ void RegisterSettingControl(const std::string &controlType, ISettingControlCreator *settingControlCreator);
+
+ /*!
+ \brief Registers the given ISettingsHandler implementation.
+
+ \param settingsHandler ISettingsHandler implementation
+ \param bFront If True, insert the handler in front of other registered handlers, insert at the end otherwise.
+ */
+ void RegisterSettingsHandler(ISettingsHandler *settingsHandler, bool bFront = false);
+ /*!
+ \brief Unregisters the given ISettingsHandler implementation.
+
+ \param settingsHandler ISettingsHandler implementation
+ */
+ void UnregisterSettingsHandler(ISettingsHandler *settingsHandler);
+
+ /*!
+ \brief Registers the given integer setting options filler under the given identifier.
+
+ \param identifier Setting options filler identifier
+ \param optionsFiller Integer setting options filler implementation
+ */
+ void RegisterSettingOptionsFiller(const std::string &identifier, IntegerSettingOptionsFiller optionsFiller);
+ /*!
+ \brief Registers the given string setting options filler under the given identifier.
+
+ \param identifier Setting options filler identifier
+ \param optionsFiller String setting options filler implementation
+ */
+ void RegisterSettingOptionsFiller(const std::string &identifier, StringSettingOptionsFiller optionsFiller);
+ /*!
+ \brief Unregisters the setting options filler registered under the given identifier.
+
+ \param identifier Setting options filler identifier
+ */
+ void UnregisterSettingOptionsFiller(const std::string &identifier);
+ /*!
+ \brief Gets the implementation of the setting options filler used by the
+ given setting.
+
+ \param setting Setting object
+ \return Implementation of the setting options filler (either IntegerSettingOptionsFiller or StringSettingOptionsFiller)
+ */
+ void* GetSettingOptionsFiller(const std::shared_ptr<const CSetting>& setting);
+
+ /*!
+ \brief Checks whether any settings have been initialized.
+
+ \return True if at least one setting has been initialized, false otherwise*/
+ bool HasSettings() const;
+
+ /*!
+ \brief Gets the setting with the given identifier.
+
+ \param id Setting identifier
+ \return Setting object with the given identifier or nullptr if the identifier is unknown
+ */
+ std::shared_ptr<CSetting> GetSetting(const std::string &id) const;
+ /*!
+ \brief Gets the full list of setting sections.
+
+ \return List of setting sections
+ */
+ std::vector<std::shared_ptr<CSettingSection>> GetSections() const;
+ /*!
+ \brief Gets the setting section with the given identifier.
+
+ \param section Setting section identifier
+ \return Setting section with the given identifier or nullptr if the identifier is unknown
+ */
+ std::shared_ptr<CSettingSection> GetSection(std::string section) const;
+ /*!
+ \brief Gets a map of settings (and their dependencies) which depend on
+ the setting with the given identifier.
+
+ It is important to note that the returned dependencies are not the
+ dependencies of the setting with the given identifier but the settings
+ (and their dependencies) which depend on the setting with the given
+ identifier.
+
+ \param id Setting identifier
+ \return Map of settings (and their dependencies) which depend on the setting with the given identifier
+ */
+ SettingDependencyMap GetDependencies(const std::string &id) const;
+ /*!
+ \brief Gets a map of settings (and their dependencies) which depend on
+ the given setting.
+
+ It is important to note that the returned dependencies are not the
+ dependencies of the given setting but the settings (and their dependencies)
+ which depend on the given setting.
+
+ \param setting Setting object
+ \return Map of settings (and their dependencies) which depend on the given setting
+ */
+ SettingDependencyMap GetDependencies(const std::shared_ptr<const CSetting>& setting) const;
+
+ /*!
+ \brief Gets the boolean value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \return Boolean value of the setting with the given identifier
+ */
+ bool GetBool(const std::string &id) const;
+ /*!
+ \brief Gets the integer value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \return Integer value of the setting with the given identifier
+ */
+ int GetInt(const std::string &id) const;
+ /*!
+ \brief Gets the real number value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \return Real number value of the setting with the given identifier
+ */
+ double GetNumber(const std::string &id) const;
+ /*!
+ \brief Gets the string value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \return String value of the setting with the given identifier
+ */
+ std::string GetString(const std::string &id) const;
+ /*!
+ \brief Gets the values of the list setting with the given identifier.
+
+ \param id Setting identifier
+ \return List of values of the setting with the given identifier
+ */
+ std::vector< std::shared_ptr<CSetting> > GetList(const std::string &id) const;
+
+ /*!
+ \brief Sets the boolean value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \param value Boolean value to set
+ \return True if setting the value was successful, false otherwise
+ */
+ bool SetBool(const std::string &id, bool value);
+ /*!
+ \brief Toggles the boolean value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \return True if toggling the boolean value was successful, false otherwise
+ */
+ bool ToggleBool(const std::string &id);
+ /*!
+ \brief Sets the integer value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \param value Integer value to set
+ \return True if setting the value was successful, false otherwise
+ */
+ bool SetInt(const std::string &id, int value);
+ /*!
+ \brief Sets the real number value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \param value Real number value to set
+ \return True if setting the value was successful, false otherwise
+ */
+ bool SetNumber(const std::string &id, double value);
+ /*!
+ \brief Sets the string value of the setting with the given identifier.
+
+ \param id Setting identifier
+ \param value String value to set
+ \return True if setting the value was successful, false otherwise
+ */
+ bool SetString(const std::string &id, const std::string &value);
+ /*!
+ \brief Sets the values of the list setting with the given identifier.
+
+ \param id Setting identifier
+ \param value Values to set
+ \return True if setting the values was successful, false otherwise
+ */
+ bool SetList(const std::string &id, const std::vector< std::shared_ptr<CSetting> > &value);
+
+ /*!
+ \brief Sets the value of the setting to its default.
+
+ \param id Setting identifier
+ \return True if setting the value to its default was successful, false otherwise
+ */
+ bool SetDefault(const std::string &id);
+ /*!
+ \brief Sets the value of all settings to their default.
+ */
+ void SetDefaults();
+
+ /*!
+ \brief Gets the setting conditions manager used by the settings manager.
+
+ \return Setting conditions manager used by the settings manager.
+ */
+ const CSettingConditionsManager& GetConditions() const { return m_conditions; }
+ /*!
+ \brief Adds the given static condition.
+
+ A static condition is just a string. If a static condition is evaluated,
+ the result depends on whether the condition's value is defined or not.
+
+ \param condition Static condition string/value
+ */
+ void AddCondition(const std::string &condition);
+ /*!
+ \brief Adds the given dynamic condition.
+
+ A dynamic condition has an identifier and an implementation which is
+ triggered when the condition is evaluated.
+
+ \param identifier Identifier of the dynamic condition
+ \param condition Implementation of the dynamic condition
+ \param data Opaque data pointer, will be passed back to SettingConditionCheck function
+ */
+ void AddDynamicCondition(const std::string &identifier, SettingConditionCheck condition, void *data = nullptr);
+
+ /*!
+ \brief Removes the given dynamic condition.
+
+ \param identifier Identifier of the dynamic condition
+ */
+ void RemoveDynamicCondition(const std::string &identifier);
+
+private:
+ // implementation of ISettingCallback
+ bool OnSettingChanging(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+ bool OnSettingUpdate(const std::shared_ptr<CSetting>& setting,
+ const char* oldSettingId,
+ const TiXmlNode* oldSettingNode) override;
+ void OnSettingPropertyChanged(const std::shared_ptr<const CSetting>& setting,
+ const char* propertyName) override;
+
+ // implementation of ISettingsHandler
+ bool OnSettingsLoading() override;
+ void OnSettingsLoaded() override;
+ void OnSettingsUnloaded() override;
+ bool OnSettingsSaving() const override;
+ void OnSettingsSaved() const override;
+ void OnSettingsCleared() override;
+
+ bool Serialize(TiXmlNode *parent) const;
+ bool Deserialize(const TiXmlNode *node, bool &updated, std::map<std::string, std::shared_ptr<CSetting>> *loadedSettings = nullptr);
+
+ bool LoadSetting(const TiXmlNode* node, const std::shared_ptr<CSetting>& setting, bool& updated);
+ bool UpdateSetting(const TiXmlNode* node,
+ const std::shared_ptr<CSetting>& setting,
+ const CSettingUpdate& update);
+ void UpdateSettingByDependency(const std::string &settingId, const CSettingDependency &dependency);
+ void UpdateSettingByDependency(const std::string &settingId, SettingDependencyType dependencyType);
+
+ void AddSetting(const std::shared_ptr<CSetting>& setting);
+
+ void ResolveReferenceSettings(const std::shared_ptr<CSettingSection>& section);
+ void CleanupIncompleteSettings();
+
+ enum class SettingOptionsFillerType {
+ Unknown = 0,
+ Integer,
+ String
+ };
+
+ void RegisterSettingOptionsFiller(const std::string &identifier, void *filler, SettingOptionsFillerType type);
+
+ using CallbackSet = std::set<ISettingCallback *>;
+ struct Setting {
+ std::shared_ptr<CSetting> setting;
+ SettingDependencyMap dependencies;
+ std::set<std::string> children;
+ CallbackSet callbacks;
+ std::unordered_set<std::string> references;
+ };
+
+ using SettingMap = std::map<std::string, Setting>;
+
+ void ResolveSettingDependencies(const std::shared_ptr<CSetting>& setting);
+ void ResolveSettingDependencies(const Setting& setting);
+
+ SettingMap::const_iterator FindSetting(std::string settingId) const;
+ SettingMap::iterator FindSetting(std::string settingId);
+ std::pair<SettingMap::iterator, bool> InsertSetting(std::string settingId, const Setting& setting);
+
+ bool m_initialized = false;
+ bool m_loaded = false;
+
+ SettingMap m_settings;
+ using SettingSectionMap = std::map<std::string, std::shared_ptr<CSettingSection>>;
+ SettingSectionMap m_sections;
+
+ using SettingCreatorMap = std::map<std::string, ISettingCreator*>;
+ SettingCreatorMap m_settingCreators;
+
+ using SettingControlCreatorMap = std::map<std::string, ISettingControlCreator*>;
+ SettingControlCreatorMap m_settingControlCreators;
+
+ using SettingsHandlers = std::vector<ISettingsHandler*>;
+ SettingsHandlers m_settingsHandlers;
+
+ CSettingConditionsManager m_conditions;
+
+ struct SettingOptionsFiller {
+ void *filler;
+ SettingOptionsFillerType type;
+ };
+ using SettingOptionsFillerMap = std::map<std::string, SettingOptionsFiller>;
+ SettingOptionsFillerMap m_optionsFillers;
+
+ mutable CSharedSection m_critical;
+ mutable CSharedSection m_settingsCritical;
+
+ Logger m_logger;
+};
diff --git a/xbmc/settings/windows/CMakeLists.txt b/xbmc/settings/windows/CMakeLists.txt
new file mode 100644
index 0000000..c1b69ac
--- /dev/null
+++ b/xbmc/settings/windows/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES GUIControlSettings.cpp
+ GUIWindowSettings.cpp
+ GUIWindowSettingsCategory.cpp
+ GUIWindowSettingsScreenCalibration.cpp)
+
+set(HEADERS GUIControlSettings.h
+ GUIWindowSettings.h
+ GUIWindowSettingsCategory.h
+ GUIWindowSettingsScreenCalibration.h)
+
+core_add_library(settings_windows)
diff --git a/xbmc/settings/windows/GUIControlSettings.cpp b/xbmc/settings/windows/GUIControlSettings.cpp
new file mode 100644
index 0000000..fcbf1fa
--- /dev/null
+++ b/xbmc/settings/windows/GUIControlSettings.cpp
@@ -0,0 +1,1798 @@
+/*
+ * 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 "GUIControlSettings.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/gui/GUIWindowAddonBrowser.h"
+#include "addons/settings/SettingUrlEncodedString.h"
+#include "dialogs/GUIDialogColorPicker.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogNumeric.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogSlider.h"
+#include "guilib/GUIColorButtonControl.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIImage.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUILabelControl.h"
+#include "guilib/GUIRadioButtonControl.h"
+#include "guilib/GUISettingsSliderControl.h"
+#include "guilib/GUISpinControlEx.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/SettingAddon.h"
+#include "settings/SettingControl.h"
+#include "settings/SettingDateTime.h"
+#include "settings/SettingPath.h"
+#include "settings/SettingUtils.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "storage/MediaManager.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <set>
+#include <utility>
+
+using namespace ADDON;
+
+static std::string Localize(std::uint32_t code,
+ ILocalizer* localizer,
+ const std::string& addonId = "")
+{
+ if (localizer == nullptr)
+ return "";
+
+ if (!addonId.empty())
+ {
+ std::string label = g_localizeStrings.GetAddonString(addonId, code);
+ if (!label.empty())
+ return label;
+ }
+
+ return localizer->Localize(code);
+}
+
+template<typename TValueType>
+static CFileItemPtr GetFileItem(const std::string& label,
+ const std::string& label2,
+ const TValueType& value,
+ const std::vector<std::pair<std::string, CVariant>>& properties,
+ const std::set<TValueType>& selectedValues)
+{
+ CFileItemPtr item(new CFileItem(label));
+ item->SetProperty("value", value);
+ item->SetLabel2(label2);
+
+ for (const auto& prop : properties)
+ item->SetProperty(prop.first, prop.second);
+
+ if (selectedValues.find(value) != selectedValues.end())
+ item->Select(true);
+
+ return item;
+}
+
+template<class SettingOption>
+static bool CompareSettingOptionAseconding(const SettingOption& lhs, const SettingOption& rhs)
+{
+ return StringUtils::CompareNoCase(lhs.label, rhs.label) < 0;
+}
+
+template<class SettingOption>
+static bool CompareSettingOptionDeseconding(const SettingOption& lhs, const SettingOption& rhs)
+{
+ return StringUtils::CompareNoCase(lhs.label, rhs.label) > 0;
+}
+
+static bool GetIntegerOptions(const SettingConstPtr& setting,
+ IntegerSettingOptions& options,
+ std::set<int>& selectedOptions,
+ ILocalizer* localizer,
+ bool updateOptions)
+{
+ std::shared_ptr<const CSettingInt> pSettingInt = NULL;
+ if (setting->GetType() == SettingType::Integer)
+ pSettingInt = std::static_pointer_cast<const CSettingInt>(setting);
+ else if (setting->GetType() == SettingType::List)
+ {
+ std::shared_ptr<const CSettingList> settingList =
+ std::static_pointer_cast<const CSettingList>(setting);
+ if (settingList->GetElementType() != SettingType::Integer)
+ return false;
+
+ pSettingInt = std::static_pointer_cast<const CSettingInt>(settingList->GetDefinition());
+ }
+
+ switch (pSettingInt->GetOptionsType())
+ {
+ case SettingOptionsType::StaticTranslatable:
+ {
+ const TranslatableIntegerSettingOptions& settingOptions =
+ pSettingInt->GetTranslatableOptions();
+ for (const auto& option : settingOptions)
+ options.push_back(
+ IntegerSettingOption(Localize(option.label, localizer, option.addonId), option.value));
+ break;
+ }
+
+ case SettingOptionsType::Static:
+ {
+ const IntegerSettingOptions& settingOptions = pSettingInt->GetOptions();
+ options.insert(options.end(), settingOptions.begin(), settingOptions.end());
+ break;
+ }
+
+ case SettingOptionsType::Dynamic:
+ {
+ IntegerSettingOptions settingOptions;
+ if (updateOptions)
+ settingOptions = std::const_pointer_cast<CSettingInt>(pSettingInt)->UpdateDynamicOptions();
+ else
+ settingOptions = pSettingInt->GetDynamicOptions();
+ options.insert(options.end(), settingOptions.begin(), settingOptions.end());
+ break;
+ }
+
+ case SettingOptionsType::Unknown:
+ default:
+ {
+ std::shared_ptr<const CSettingControlFormattedRange> control =
+ std::static_pointer_cast<const CSettingControlFormattedRange>(pSettingInt->GetControl());
+ for (int i = pSettingInt->GetMinimum(); i <= pSettingInt->GetMaximum();
+ i += pSettingInt->GetStep())
+ {
+ std::string strLabel;
+ if (i == pSettingInt->GetMinimum() && control->GetMinimumLabel() > -1)
+ strLabel = Localize(control->GetMinimumLabel(), localizer);
+ else if (control->GetFormatLabel() > -1)
+ strLabel = StringUtils::Format(Localize(control->GetFormatLabel(), localizer), i);
+ else
+ strLabel = StringUtils::Format(control->GetFormatString(), i);
+
+ options.push_back(IntegerSettingOption(strLabel, i));
+ }
+
+ break;
+ }
+ }
+
+ switch (pSettingInt->GetOptionsSort())
+ {
+ case SettingOptionsSort::Ascending:
+ std::sort(options.begin(), options.end(),
+ CompareSettingOptionAseconding<IntegerSettingOption>);
+ break;
+
+ case SettingOptionsSort::Descending:
+ std::sort(options.begin(), options.end(),
+ CompareSettingOptionDeseconding<IntegerSettingOption>);
+ break;
+
+ case SettingOptionsSort::NoSorting:
+ default:
+ break;
+ }
+
+ // this must be done after potentially calling CSettingInt::UpdateDynamicOptions() because it can
+ // change the value of the setting
+ if (setting->GetType() == SettingType::Integer)
+ selectedOptions.insert(pSettingInt->GetValue());
+ else if (setting->GetType() == SettingType::List)
+ {
+ std::vector<CVariant> list =
+ CSettingUtils::GetList(std::static_pointer_cast<const CSettingList>(setting));
+ for (const auto& itValue : list)
+ selectedOptions.insert((int)itValue.asInteger());
+ }
+ else
+ return false;
+
+ return true;
+}
+
+static bool GetStringOptions(const SettingConstPtr& setting,
+ StringSettingOptions& options,
+ std::set<std::string>& selectedOptions,
+ ILocalizer* localizer,
+ bool updateOptions)
+{
+ std::shared_ptr<const CSettingString> pSettingString = NULL;
+ if (setting->GetType() == SettingType::String)
+ pSettingString = std::static_pointer_cast<const CSettingString>(setting);
+ else if (setting->GetType() == SettingType::List)
+ {
+ std::shared_ptr<const CSettingList> settingList =
+ std::static_pointer_cast<const CSettingList>(setting);
+ if (settingList->GetElementType() != SettingType::String)
+ return false;
+
+ pSettingString = std::static_pointer_cast<const CSettingString>(settingList->GetDefinition());
+ }
+
+ switch (pSettingString->GetOptionsType())
+ {
+ case SettingOptionsType::StaticTranslatable:
+ {
+ const TranslatableStringSettingOptions& settingOptions =
+ pSettingString->GetTranslatableOptions();
+ for (const auto& option : settingOptions)
+ options.push_back(StringSettingOption(Localize(option.first, localizer), option.second));
+ break;
+ }
+
+ case SettingOptionsType::Static:
+ {
+ const StringSettingOptions& settingOptions = pSettingString->GetOptions();
+ options.insert(options.end(), settingOptions.begin(), settingOptions.end());
+ break;
+ }
+
+ case SettingOptionsType::Dynamic:
+ {
+ StringSettingOptions settingOptions;
+ if (updateOptions)
+ settingOptions =
+ std::const_pointer_cast<CSettingString>(pSettingString)->UpdateDynamicOptions();
+ else
+ settingOptions = pSettingString->GetDynamicOptions();
+ options.insert(options.end(), settingOptions.begin(), settingOptions.end());
+ break;
+ }
+
+ case SettingOptionsType::Unknown:
+ default:
+ return false;
+ }
+
+ switch (pSettingString->GetOptionsSort())
+ {
+ case SettingOptionsSort::Ascending:
+ std::sort(options.begin(), options.end(),
+ CompareSettingOptionAseconding<StringSettingOption>);
+ break;
+
+ case SettingOptionsSort::Descending:
+ std::sort(options.begin(), options.end(),
+ CompareSettingOptionDeseconding<StringSettingOption>);
+ break;
+
+ case SettingOptionsSort::NoSorting:
+ default:
+ break;
+ }
+
+ // this must be done after potentially calling CSettingString::UpdateDynamicOptions() because it
+ // can change the value of the setting
+ if (setting->GetType() == SettingType::String)
+ selectedOptions.insert(pSettingString->GetValue());
+ else if (setting->GetType() == SettingType::List)
+ {
+ std::vector<CVariant> list =
+ CSettingUtils::GetList(std::static_pointer_cast<const CSettingList>(setting));
+ for (const auto& itValue : list)
+ selectedOptions.insert(itValue.asString());
+ }
+ else
+ return false;
+
+ return true;
+}
+
+CGUIControlBaseSetting::CGUIControlBaseSetting(int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer)
+ : m_id(id),
+ m_pSetting(std::move(pSetting)),
+ m_localizer(localizer),
+ m_delayed(false),
+ m_valid(true)
+{
+}
+
+bool CGUIControlBaseSetting::IsEnabled() const
+{
+ return m_pSetting != NULL && m_pSetting->IsEnabled();
+}
+
+void CGUIControlBaseSetting::UpdateFromControl()
+{
+ Update(true, true);
+}
+
+void CGUIControlBaseSetting::UpdateFromSetting(bool updateDisplayOnly /* = false */)
+{
+ Update(false, updateDisplayOnly);
+}
+
+std::string CGUIControlBaseSetting::Localize(std::uint32_t code) const
+{
+ return ::Localize(code, m_localizer);
+}
+
+void CGUIControlBaseSetting::Update(bool fromControl, bool updateDisplayOnly)
+{
+ if (fromControl || updateDisplayOnly)
+ return;
+
+ CGUIControl* control = GetControl();
+ if (control == NULL)
+ return;
+
+ control->SetEnabled(IsEnabled());
+ if (m_pSetting)
+ control->SetVisible(m_pSetting->IsVisible());
+ SetValid(true);
+}
+
+CGUIControlRadioButtonSetting::CGUIControlRadioButtonSetting(CGUIRadioButtonControl* pRadioButton,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer)
+ : CGUIControlBaseSetting(id, std::move(pSetting), localizer)
+{
+ m_pRadioButton = pRadioButton;
+ if (m_pRadioButton == NULL)
+ return;
+
+ m_pRadioButton->SetID(id);
+}
+
+CGUIControlRadioButtonSetting::~CGUIControlRadioButtonSetting() = default;
+
+bool CGUIControlRadioButtonSetting::OnClick()
+{
+ SetValid(std::static_pointer_cast<CSettingBool>(m_pSetting)
+ ->SetValue(!std::static_pointer_cast<CSettingBool>(m_pSetting)->GetValue()));
+ return IsValid();
+}
+
+void CGUIControlRadioButtonSetting::Update(bool fromControl, bool updateDisplayOnly)
+{
+ if (fromControl || m_pRadioButton == NULL)
+ return;
+
+ CGUIControlBaseSetting::Update(fromControl, updateDisplayOnly);
+
+ m_pRadioButton->SetSelected(std::static_pointer_cast<CSettingBool>(m_pSetting)->GetValue());
+}
+
+CGUIControlColorButtonSetting::CGUIControlColorButtonSetting(
+ CGUIColorButtonControl* pColorControl,
+ int id,
+ const std::shared_ptr<CSetting>& pSetting,
+ ILocalizer* localizer)
+ : CGUIControlBaseSetting(id, pSetting, localizer)
+{
+ m_pColorButton = pColorControl;
+ if (!m_pColorButton)
+ return;
+
+ m_pColorButton->SetID(id);
+}
+
+CGUIControlColorButtonSetting::~CGUIControlColorButtonSetting() = default;
+
+bool CGUIControlColorButtonSetting::OnClick()
+{
+ if (!m_pColorButton)
+ return false;
+
+ std::shared_ptr<CSettingString> settingHexColor =
+ std::static_pointer_cast<CSettingString>(m_pSetting);
+
+ CGUIDialogColorPicker* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogColorPicker>(
+ WINDOW_DIALOG_COLOR_PICKER);
+ if (!dialog)
+ return false;
+
+ dialog->Reset();
+ dialog->SetHeading(CVariant{Localize(m_pSetting->GetLabel())});
+ dialog->LoadColors();
+ std::string hexColor;
+ if (settingHexColor)
+ hexColor = settingHexColor.get()->GetValue();
+ dialog->SetSelectedColor(hexColor);
+ dialog->Open();
+
+ if (!dialog->IsConfirmed())
+ return false;
+
+ SetValid(
+ std::static_pointer_cast<CSettingString>(m_pSetting)->SetValue(dialog->GetSelectedColor()));
+ return IsValid();
+}
+
+void CGUIControlColorButtonSetting::Update(bool fromControl, bool updateDisplayOnly)
+{
+ if (fromControl || !m_pColorButton)
+ return;
+
+ CGUIControlBaseSetting::Update(fromControl, updateDisplayOnly);
+ // Set the color to apply to the preview color box
+ m_pColorButton->SetImageBoxColor(
+ std::static_pointer_cast<CSettingString>(m_pSetting)->GetValue());
+}
+
+CGUIControlSpinExSetting::CGUIControlSpinExSetting(CGUISpinControlEx* pSpin,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer)
+ : CGUIControlBaseSetting(id, std::move(pSetting), localizer)
+{
+ m_pSpin = pSpin;
+ if (m_pSpin == NULL)
+ return;
+
+ m_pSpin->SetID(id);
+
+ const std::string& controlFormat = m_pSetting->GetControl()->GetFormat();
+ if (controlFormat == "number")
+ {
+ std::shared_ptr<CSettingNumber> pSettingNumber =
+ std::static_pointer_cast<CSettingNumber>(m_pSetting);
+ m_pSpin->SetType(SPIN_CONTROL_TYPE_FLOAT);
+ m_pSpin->SetFloatRange(static_cast<float>(pSettingNumber->GetMinimum()),
+ static_cast<float>(pSettingNumber->GetMaximum()));
+ m_pSpin->SetFloatInterval(static_cast<float>(pSettingNumber->GetStep()));
+ }
+ else if (controlFormat == "integer")
+ m_pSpin->SetType(SPIN_CONTROL_TYPE_TEXT);
+ else if (controlFormat == "string")
+ {
+ m_pSpin->SetType(SPIN_CONTROL_TYPE_TEXT);
+
+ if (m_pSetting->GetType() == SettingType::Integer)
+ FillIntegerSettingControl(false);
+ else if (m_pSetting->GetType() == SettingType::Number)
+ {
+ std::shared_ptr<CSettingNumber> pSettingNumber =
+ std::static_pointer_cast<CSettingNumber>(m_pSetting);
+ std::shared_ptr<const CSettingControlFormattedRange> control =
+ std::static_pointer_cast<const CSettingControlFormattedRange>(m_pSetting->GetControl());
+ int index = 0;
+ for (double value = pSettingNumber->GetMinimum(); value <= pSettingNumber->GetMaximum();
+ value += pSettingNumber->GetStep(), index++)
+ {
+ std::string strLabel;
+ if (value == pSettingNumber->GetMinimum() && control->GetMinimumLabel() > -1)
+ strLabel = Localize(control->GetMinimumLabel());
+ else if (control->GetFormatLabel() > -1)
+ strLabel = StringUtils::Format(Localize(control->GetFormatLabel()), value);
+ else
+ strLabel = StringUtils::Format(control->GetFormatString(), value);
+
+ m_pSpin->AddLabel(strLabel, index);
+ }
+ }
+ }
+}
+
+CGUIControlSpinExSetting::~CGUIControlSpinExSetting() = default;
+
+bool CGUIControlSpinExSetting::OnClick()
+{
+ if (m_pSpin == NULL)
+ return false;
+
+ switch (m_pSetting->GetType())
+ {
+ case SettingType::Integer:
+ SetValid(std::static_pointer_cast<CSettingInt>(m_pSetting)->SetValue(m_pSpin->GetValue()));
+ break;
+
+ case SettingType::Number:
+ {
+ auto pSettingNumber = std::static_pointer_cast<CSettingNumber>(m_pSetting);
+ const auto& controlFormat = m_pSetting->GetControl()->GetFormat();
+ if (controlFormat == "number")
+ SetValid(pSettingNumber->SetValue(static_cast<double>(m_pSpin->GetFloatValue())));
+ else
+ SetValid(pSettingNumber->SetValue(pSettingNumber->GetMinimum() +
+ pSettingNumber->GetStep() * m_pSpin->GetValue()));
+
+ break;
+ }
+
+ case SettingType::String:
+ SetValid(std::static_pointer_cast<CSettingString>(m_pSetting)
+ ->SetValue(m_pSpin->GetStringValue()));
+ break;
+
+ default:
+ return false;
+ }
+
+ return IsValid();
+}
+
+void CGUIControlSpinExSetting::Update(bool fromControl, bool updateDisplayOnly)
+{
+ if (fromControl || m_pSpin == NULL)
+ return;
+
+ CGUIControlBaseSetting::Update(fromControl, updateDisplayOnly);
+
+ FillControl(!updateDisplayOnly);
+
+ if (!updateDisplayOnly)
+ {
+ // disable the spinner if it has less than two items
+ if (!m_pSpin->IsDisabled() && (m_pSpin->GetMaximum() - m_pSpin->GetMinimum()) == 0)
+ m_pSpin->SetEnabled(false);
+ }
+}
+
+void CGUIControlSpinExSetting::FillControl(bool updateValues)
+{
+ if (m_pSpin == NULL)
+ return;
+
+ if (updateValues)
+ m_pSpin->Clear();
+
+ const std::string& controlFormat = m_pSetting->GetControl()->GetFormat();
+ if (controlFormat == "number")
+ {
+ std::shared_ptr<CSettingNumber> pSettingNumber =
+ std::static_pointer_cast<CSettingNumber>(m_pSetting);
+ m_pSpin->SetFloatValue((float)pSettingNumber->GetValue());
+ }
+ else if (controlFormat == "integer")
+ FillIntegerSettingControl(updateValues);
+ else if (controlFormat == "string")
+ {
+ if (m_pSetting->GetType() == SettingType::Integer)
+ FillIntegerSettingControl(updateValues);
+ else if (m_pSetting->GetType() == SettingType::Number)
+ FillFloatSettingControl();
+ else if (m_pSetting->GetType() == SettingType::String)
+ FillStringSettingControl(updateValues);
+ }
+}
+
+void CGUIControlSpinExSetting::FillIntegerSettingControl(bool updateValues)
+{
+ IntegerSettingOptions options;
+ std::set<int> selectedValues;
+ // get the integer options
+ if (!GetIntegerOptions(m_pSetting, options, selectedValues, m_localizer, updateValues) ||
+ selectedValues.size() != 1)
+ return;
+
+ if (updateValues)
+ {
+ // add them to the spinner
+ for (const auto& option : options)
+ m_pSpin->AddLabel(option.label, option.value);
+ }
+
+ // and set the current value
+ m_pSpin->SetValue(*selectedValues.begin());
+}
+
+void CGUIControlSpinExSetting::FillFloatSettingControl()
+{
+ std::shared_ptr<CSettingNumber> pSettingNumber =
+ std::static_pointer_cast<CSettingNumber>(m_pSetting);
+ std::shared_ptr<const CSettingControlFormattedRange> control =
+ std::static_pointer_cast<const CSettingControlFormattedRange>(m_pSetting->GetControl());
+ int index = 0;
+ int currentIndex = 0;
+ for (double value = pSettingNumber->GetMinimum(); value <= pSettingNumber->GetMaximum();
+ value += pSettingNumber->GetStep(), index++)
+ {
+ if (value == pSettingNumber->GetValue())
+ {
+ currentIndex = index;
+ break;
+ }
+ }
+
+ m_pSpin->SetValue(currentIndex);
+}
+
+void CGUIControlSpinExSetting::FillStringSettingControl(bool updateValues)
+{
+ StringSettingOptions options;
+ std::set<std::string> selectedValues;
+ // get the string options
+ if (!GetStringOptions(m_pSetting, options, selectedValues, m_localizer, updateValues) ||
+ selectedValues.size() != 1)
+ return;
+
+ if (updateValues)
+ {
+ // add them to the spinner
+ for (const auto& option : options)
+ m_pSpin->AddLabel(option.label, option.value);
+ }
+
+ // and set the current value
+ m_pSpin->SetStringValue(*selectedValues.begin());
+}
+
+CGUIControlListSetting::CGUIControlListSetting(CGUIButtonControl* pButton,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer)
+ : CGUIControlBaseSetting(id, std::move(pSetting), localizer)
+{
+ m_pButton = pButton;
+ if (m_pButton == NULL)
+ return;
+
+ m_pButton->SetID(id);
+}
+
+CGUIControlListSetting::~CGUIControlListSetting() = default;
+
+bool CGUIControlListSetting::OnClick()
+{
+ if (m_pButton == NULL)
+ return false;
+
+ CGUIDialogSelect* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(
+ WINDOW_DIALOG_SELECT);
+ if (dialog == NULL)
+ return false;
+
+ CFileItemList options;
+ std::shared_ptr<const CSettingControlList> control =
+ std::static_pointer_cast<const CSettingControlList>(m_pSetting->GetControl());
+ bool optionsValid = GetItems(m_pSetting, options, false);
+
+ bool bValueAdded = false;
+ bool bAllowNewOption = false;
+ if (m_pSetting->GetType() == SettingType::List)
+ {
+ std::shared_ptr<const CSettingList> settingList =
+ std::static_pointer_cast<const CSettingList>(m_pSetting);
+ if (settingList->GetElementType() == SettingType::String)
+ {
+ bAllowNewOption = std::static_pointer_cast<const CSettingString>(settingList->GetDefinition())
+ ->AllowNewOption();
+ }
+ }
+ if (!bAllowNewOption)
+ {
+ // Do not show dialog if
+ // there are no items to be chosen
+ if (!optionsValid || options.Size() <= 0)
+ return false;
+
+ dialog->Reset();
+ dialog->SetHeading(CVariant{Localize(m_pSetting->GetLabel())});
+ dialog->SetItems(options);
+ dialog->SetMultiSelection(control->CanMultiSelect());
+ dialog->SetUseDetails(control->UseDetails());
+ dialog->Open();
+
+ if (!dialog->IsConfirmed())
+ return false;
+ }
+ else
+ {
+ // Possible to add items, as well as select from any options given
+ // Add any current values that are not options as items in list
+ std::vector<CVariant> list =
+ CSettingUtils::GetList(std::static_pointer_cast<const CSettingList>(m_pSetting));
+ for (const auto& value : list)
+ {
+ bool found = std::any_of(options.begin(), options.end(), [&](const auto& p) {
+ return p->GetProperty("value").asString() == value.asString();
+ });
+ if (!found)
+ {
+ CFileItemPtr item(new CFileItem(value.asString()));
+ item->SetProperty("value", value.asString());
+ item->Select(true);
+ options.Add(item);
+ }
+ }
+
+ bool bRepeat = true;
+ while (bRepeat)
+ {
+ std::string strAddButton = Localize(control->GetAddButtonLabel());
+ if (strAddButton.empty())
+ strAddButton = Localize(15019); // "ADD"
+ dialog->Reset(); // Clears AddButtonPressed
+ dialog->SetHeading(CVariant{ Localize(m_pSetting->GetLabel()) });
+ dialog->SetItems(options);
+ dialog->SetMultiSelection(control->CanMultiSelect());
+ dialog->EnableButton2(bAllowNewOption, strAddButton);
+
+ dialog->Open();
+
+ if (!dialog->IsConfirmed())
+ return false;
+
+ if (dialog->IsButton2Pressed())
+ {
+ // Get new list value
+ std::string strLabel;
+ bool bValidType = false;
+ while (!bValidType && CGUIKeyboardFactory::ShowAndGetInput(
+ strLabel, CVariant{ Localize(control->GetAddButtonLabel()) }, false))
+ {
+ // Validate new value is unique and truncate at any comma
+ StringUtils::Trim(strLabel);
+ strLabel = strLabel.substr(0, strLabel.find(','));
+ if (!strLabel.empty())
+ {
+ bValidType = !std::any_of(options.begin(), options.end(), [&](const auto& p) {
+ return p->GetProperty("value").asString() == strLabel;
+ });
+ }
+ if (bValidType)
+ { // Add new value to the list of options
+ CFileItemPtr pItem(new CFileItem(strLabel));
+ pItem->Select(true);
+ pItem->SetProperty("value", strLabel);
+ options.Add(pItem);
+ bValueAdded = true;
+ }
+ }
+ }
+ bRepeat = dialog->IsButton2Pressed();
+ }
+ }
+
+ std::vector<CVariant> values;
+ for (int i : dialog->GetSelectedItems())
+ {
+ const CFileItemPtr item = options.Get(i);
+ if (item == NULL || !item->HasProperty("value"))
+ return false;
+
+ values.push_back(item->GetProperty("value"));
+ }
+
+ bool ret = false;
+ switch (m_pSetting->GetType())
+ {
+ case SettingType::Integer:
+ if (values.size() > 1)
+ return false;
+ ret = std::static_pointer_cast<CSettingInt>(m_pSetting)
+ ->SetValue((int)values.at(0).asInteger());
+ break;
+
+ case SettingType::String:
+ if (values.size() > 1)
+ return false;
+ ret = std::static_pointer_cast<CSettingString>(m_pSetting)->SetValue(values.at(0).asString());
+ break;
+
+ case SettingType::List:
+ ret = CSettingUtils::SetList(std::static_pointer_cast<CSettingList>(m_pSetting), values);
+ break;
+
+ default:
+ return false;
+ }
+
+ if (ret)
+ UpdateFromSetting(!bValueAdded);
+ else
+ SetValid(false);
+
+ return IsValid();
+}
+
+void CGUIControlListSetting::Update(bool fromControl, bool updateDisplayOnly)
+{
+ if (fromControl || m_pButton == NULL)
+ return;
+
+ CGUIControlBaseSetting::Update(fromControl, updateDisplayOnly);
+
+ CFileItemList options;
+ std::shared_ptr<const CSettingControlList> control =
+ std::static_pointer_cast<const CSettingControlList>(m_pSetting->GetControl());
+ bool optionsValid = GetItems(m_pSetting, options, !updateDisplayOnly);
+
+ bool bAllowNewOption = false;
+ if (m_pSetting->GetType() == SettingType::List)
+ {
+ std::shared_ptr<const CSettingList> settingList =
+ std::static_pointer_cast<const CSettingList>(m_pSetting);
+ if (settingList->GetElementType() == SettingType::String)
+ {
+ bAllowNewOption = std::static_pointer_cast<const CSettingString>(settingList->GetDefinition())
+ ->AllowNewOption();
+ }
+ }
+
+ std::string label2;
+ if (optionsValid && !control->HideValue())
+ {
+ SettingControlListValueFormatter formatter = control->GetFormatter();
+ if (formatter)
+ label2 = formatter(m_pSetting);
+
+ if (label2.empty() && bAllowNewOption)
+ {
+ const std::shared_ptr<const CSettingList> settingList =
+ std::static_pointer_cast<const CSettingList>(m_pSetting);
+ label2 = settingList->ToString();
+ }
+
+ if (label2.empty())
+ {
+ std::vector<std::string> labels;
+ for (int index = 0; index < options.Size(); index++)
+ {
+ const CFileItemPtr pItem = options.Get(index);
+ if (pItem->IsSelected())
+ labels.push_back(pItem->GetLabel());
+ }
+
+ label2 = StringUtils::Join(labels, ", ");
+ }
+ }
+
+ m_pButton->SetLabel2(label2);
+
+ if (!updateDisplayOnly)
+ {
+ // Disable the control if no items can be added and
+ // there are no items to be chosen
+ if (!m_pButton->IsDisabled() && !bAllowNewOption && (options.Size() <= 0))
+ m_pButton->SetEnabled(false);
+ }
+}
+
+bool CGUIControlListSetting::GetItems(const SettingConstPtr& setting,
+ CFileItemList& items,
+ bool updateItems) const
+{
+ std::shared_ptr<const CSettingControlList> control =
+ std::static_pointer_cast<const CSettingControlList>(setting->GetControl());
+ const std::string& controlFormat = control->GetFormat();
+
+ if (controlFormat == "integer")
+ return GetIntegerItems(setting, items, updateItems);
+ else if (controlFormat == "string")
+ {
+ if (setting->GetType() == SettingType::Integer ||
+ (setting->GetType() == SettingType::List &&
+ std::static_pointer_cast<const CSettingList>(setting)->GetElementType() ==
+ SettingType::Integer))
+ return GetIntegerItems(setting, items, updateItems);
+ else if (setting->GetType() == SettingType::String ||
+ (setting->GetType() == SettingType::List &&
+ std::static_pointer_cast<const CSettingList>(setting)->GetElementType() ==
+ SettingType::String))
+ return GetStringItems(setting, items, updateItems);
+ }
+ else
+ return false;
+
+ return true;
+}
+
+bool CGUIControlListSetting::GetIntegerItems(const SettingConstPtr& setting,
+ CFileItemList& items,
+ bool updateItems) const
+{
+ IntegerSettingOptions options;
+ std::set<int> selectedValues;
+ // get the integer options
+ if (!GetIntegerOptions(setting, options, selectedValues, m_localizer, updateItems))
+ return false;
+
+ // turn them into CFileItems and add them to the item list
+ for (const auto& option : options)
+ items.Add(
+ GetFileItem(option.label, option.label2, option.value, option.properties, selectedValues));
+
+ return true;
+}
+
+bool CGUIControlListSetting::GetStringItems(const SettingConstPtr& setting,
+ CFileItemList& items,
+ bool updateItems) const
+{
+ StringSettingOptions options;
+ std::set<std::string> selectedValues;
+ // get the string options
+ if (!GetStringOptions(setting, options, selectedValues, m_localizer, updateItems))
+ return false;
+
+ // turn them into CFileItems and add them to the item list
+ for (const auto& option : options)
+ items.Add(
+ GetFileItem(option.label, option.label2, option.value, option.properties, selectedValues));
+
+ return true;
+}
+
+CGUIControlButtonSetting::CGUIControlButtonSetting(CGUIButtonControl* pButton,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer)
+ : CGUIControlBaseSetting(id, std::move(pSetting), localizer)
+{
+ m_pButton = pButton;
+ if (m_pButton == NULL)
+ return;
+
+ m_pButton->SetID(id);
+}
+
+CGUIControlButtonSetting::~CGUIControlButtonSetting() = default;
+
+bool CGUIControlButtonSetting::OnClick()
+{
+ if (m_pButton == NULL)
+ return false;
+
+ std::shared_ptr<const ISettingControl> control = m_pSetting->GetControl();
+ const std::string& controlType = control->GetType();
+ const std::string& controlFormat = control->GetFormat();
+ if (controlType == "button")
+ {
+ std::shared_ptr<const CSettingControlButton> buttonControl =
+ std::static_pointer_cast<const CSettingControlButton>(control);
+ if (controlFormat == "addon")
+ {
+ // prompt for the addon
+ std::shared_ptr<CSettingAddon> setting;
+ std::vector<std::string> addonIDs;
+ if (m_pSetting->GetType() == SettingType::List)
+ {
+ std::shared_ptr<CSettingList> settingList =
+ std::static_pointer_cast<CSettingList>(m_pSetting);
+ setting = std::static_pointer_cast<CSettingAddon>(settingList->GetDefinition());
+ for (const SettingPtr& addon : settingList->GetValue())
+ addonIDs.push_back(std::static_pointer_cast<CSettingAddon>(addon)->GetValue());
+ }
+ else
+ {
+ setting = std::static_pointer_cast<CSettingAddon>(m_pSetting);
+ addonIDs.push_back(setting->GetValue());
+ }
+
+ if (CGUIWindowAddonBrowser::SelectAddonID(
+ setting->GetAddonType(), addonIDs, setting->AllowEmpty(),
+ buttonControl->ShowAddonDetails(), m_pSetting->GetType() == SettingType::List,
+ buttonControl->ShowInstalledAddons(), buttonControl->ShowInstallableAddons(),
+ buttonControl->ShowMoreAddons()) != 1)
+ return false;
+
+ if (m_pSetting->GetType() == SettingType::List)
+ std::static_pointer_cast<CSettingList>(m_pSetting)->FromString(addonIDs);
+ else
+ SetValid(setting->SetValue(addonIDs[0]));
+ }
+ else if (controlFormat == "path" || controlFormat == "file" || controlFormat == "image")
+ SetValid(GetPath(std::static_pointer_cast<CSettingPath>(m_pSetting), m_localizer));
+ else if (controlFormat == "date")
+ {
+ std::shared_ptr<CSettingDate> settingDate =
+ std::static_pointer_cast<CSettingDate>(m_pSetting);
+
+ KODI::TIME::SystemTime systemdate;
+ settingDate->GetDate().GetAsSystemTime(systemdate);
+ if (CGUIDialogNumeric::ShowAndGetDate(systemdate, Localize(buttonControl->GetHeading())))
+ SetValid(settingDate->SetDate(CDateTime(systemdate)));
+ }
+ else if (controlFormat == "time")
+ {
+ std::shared_ptr<CSettingTime> settingTime =
+ std::static_pointer_cast<CSettingTime>(m_pSetting);
+
+ KODI::TIME::SystemTime systemtime;
+ settingTime->GetTime().GetAsSystemTime(systemtime);
+
+ if (CGUIDialogNumeric::ShowAndGetTime(systemtime, Localize(buttonControl->GetHeading())))
+ SetValid(settingTime->SetTime(CDateTime(systemtime)));
+ }
+ else if (controlFormat == "action")
+ {
+ // simply call the OnSettingAction callback and whoever knows what to
+ // do can do so (based on the setting's identification)
+ m_pSetting->OnSettingAction(m_pSetting);
+ SetValid(true);
+ }
+ }
+ else if (controlType == "slider")
+ {
+ float value, min, step, max;
+ if (m_pSetting->GetType() == SettingType::Integer)
+ {
+ std::shared_ptr<CSettingInt> settingInt = std::static_pointer_cast<CSettingInt>(m_pSetting);
+ value = (float)settingInt->GetValue();
+ min = (float)settingInt->GetMinimum();
+ step = (float)settingInt->GetStep();
+ max = (float)settingInt->GetMaximum();
+ }
+ else if (m_pSetting->GetType() == SettingType::Number)
+ {
+ std::shared_ptr<CSettingNumber> settingNumber =
+ std::static_pointer_cast<CSettingNumber>(m_pSetting);
+ value = (float)settingNumber->GetValue();
+ min = (float)settingNumber->GetMinimum();
+ step = (float)settingNumber->GetStep();
+ max = (float)settingNumber->GetMaximum();
+ }
+ else
+ return false;
+
+ std::shared_ptr<const CSettingControlSlider> sliderControl =
+ std::static_pointer_cast<const CSettingControlSlider>(control);
+ CGUIDialogSlider::ShowAndGetInput(Localize(sliderControl->GetHeading()), value, min, step, max,
+ this, NULL);
+ SetValid(true);
+ }
+
+ // update the displayed value
+ UpdateFromSetting(true);
+
+ return IsValid();
+}
+
+void CGUIControlButtonSetting::Update(bool fromControl, bool updateDisplayOnly)
+{
+ if (fromControl || m_pButton == NULL)
+ return;
+
+ CGUIControlBaseSetting::Update(fromControl, updateDisplayOnly);
+
+ std::string strText;
+ std::shared_ptr<const ISettingControl> control = m_pSetting->GetControl();
+ const std::string& controlType = control->GetType();
+ const std::string& controlFormat = control->GetFormat();
+
+ if (controlType == "button")
+ {
+ if (!std::static_pointer_cast<const CSettingControlButton>(control)->HideValue())
+ {
+ auto setting = m_pSetting;
+ if (m_pSetting->GetType() == SettingType::List)
+ setting = std::static_pointer_cast<CSettingList>(m_pSetting)->GetDefinition();
+
+ switch (setting->GetType())
+ {
+ case SettingType::String:
+ {
+ if (controlFormat == "addon")
+ {
+ std::vector<std::string> addonIDs;
+ if (m_pSetting->GetType() == SettingType::List)
+ {
+ for (const auto& addonSetting :
+ std::static_pointer_cast<CSettingList>(m_pSetting)->GetValue())
+ addonIDs.push_back(
+ std::static_pointer_cast<CSettingAddon>(addonSetting)->GetValue());
+ }
+ else
+ addonIDs.push_back(std::static_pointer_cast<CSettingString>(setting)->GetValue());
+
+ std::vector<std::string> addonNames;
+ for (const auto& addonID : addonIDs)
+ {
+ ADDON::AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(addonID, addon,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ addonNames.push_back(addon->Name());
+ }
+
+ if (addonNames.empty())
+ strText = g_localizeStrings.Get(231); // None
+ else
+ strText = StringUtils::Join(addonNames, ", ");
+ }
+ else
+ {
+ std::string strValue = std::static_pointer_cast<CSettingString>(setting)->GetValue();
+ if (controlFormat == "path" || controlFormat == "file" || controlFormat == "image")
+ {
+ std::string shortPath;
+ if (CUtil::MakeShortenPath(strValue, shortPath, 30))
+ strText = shortPath;
+ }
+ else if (controlFormat == "infolabel")
+ {
+ strText = strValue;
+ if (strText.empty())
+ strText = g_localizeStrings.Get(231); // None
+ }
+ else
+ strText = strValue;
+ }
+
+ break;
+ }
+
+ case SettingType::Action:
+ {
+ // CSettingAction.
+ // Note: This can be removed once all settings use a proper control & format combination.
+ // CSettingAction is strictly speaking not designed to have a label2, it does not even have a value.
+ strText = m_pButton->GetLabel2();
+
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+ }
+ else if (controlType == "slider")
+ {
+ switch (m_pSetting->GetType())
+ {
+ case SettingType::Integer:
+ {
+ std::shared_ptr<const CSettingInt> settingInt =
+ std::static_pointer_cast<CSettingInt>(m_pSetting);
+ strText = CGUIControlSliderSetting::GetText(m_pSetting, settingInt->GetValue(),
+ settingInt->GetMinimum(), settingInt->GetStep(),
+ settingInt->GetMaximum(), m_localizer);
+ break;
+ }
+
+ case SettingType::Number:
+ {
+ std::shared_ptr<const CSettingNumber> settingNumber =
+ std::static_pointer_cast<CSettingNumber>(m_pSetting);
+ strText = CGUIControlSliderSetting::GetText(
+ m_pSetting, settingNumber->GetValue(), settingNumber->GetMinimum(),
+ settingNumber->GetStep(), settingNumber->GetMaximum(), m_localizer);
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+
+ m_pButton->SetLabel2(strText);
+}
+
+bool CGUIControlButtonSetting::GetPath(const std::shared_ptr<CSettingPath>& pathSetting,
+ ILocalizer* localizer)
+{
+ if (pathSetting == NULL)
+ return false;
+
+ std::string path = pathSetting->GetValue();
+
+ VECSOURCES shares;
+ bool localSharesOnly = false;
+ const std::vector<std::string>& sources = pathSetting->GetSources();
+ for (const auto& source : sources)
+ {
+ if (StringUtils::EqualsNoCase(source, "local"))
+ localSharesOnly = true;
+ else
+ {
+ VECSOURCES* sources = CMediaSourceSettings::GetInstance().GetSources(source);
+ if (sources != NULL)
+ shares.insert(shares.end(), sources->begin(), sources->end());
+ }
+ }
+
+ CServiceBroker::GetMediaManager().GetLocalDrives(shares);
+ if (!localSharesOnly)
+ CServiceBroker::GetMediaManager().GetNetworkLocations(shares);
+
+ bool result = false;
+ std::shared_ptr<const CSettingControlButton> control =
+ std::static_pointer_cast<const CSettingControlButton>(pathSetting->GetControl());
+ const auto heading = ::Localize(control->GetHeading(), localizer);
+ if (control->GetFormat() == "file")
+ result = CGUIDialogFileBrowser::ShowAndGetFile(
+ shares, pathSetting->GetMasking(CServiceBroker::GetFileExtensionProvider()), heading, path,
+ control->UseImageThumbs(), control->UseFileDirectories());
+ else if (control->GetFormat() == "image")
+ {
+ /* Check setting contains own masking, to filter required image type.
+ * e.g. png only needed
+ * <constraints>
+ * <masking>*.png</masking>
+ * </constraints>
+ * <control type="button" format="image">
+ * ...
+ */
+ std::string ext = pathSetting->GetMasking(CServiceBroker::GetFileExtensionProvider());
+ if (ext.empty())
+ ext = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ result = CGUIDialogFileBrowser::ShowAndGetFile(shares, ext, heading, path, true);
+ }
+ else
+ result =
+ CGUIDialogFileBrowser::ShowAndGetDirectory(shares, heading, path, pathSetting->Writable());
+
+ if (!result)
+ return false;
+
+ return pathSetting->SetValue(path);
+}
+
+void CGUIControlButtonSetting::OnSliderChange(void* data, CGUISliderControl* slider)
+{
+ if (slider == NULL)
+ return;
+
+ std::string strText;
+ switch (m_pSetting->GetType())
+ {
+ case SettingType::Integer:
+ {
+ std::shared_ptr<CSettingInt> settingInt = std::static_pointer_cast<CSettingInt>(m_pSetting);
+ if (settingInt->SetValue(slider->GetIntValue()))
+ strText = CGUIControlSliderSetting::GetText(m_pSetting, settingInt->GetValue(),
+ settingInt->GetMinimum(), settingInt->GetStep(),
+ settingInt->GetMaximum(), m_localizer);
+ break;
+ }
+
+ case SettingType::Number:
+ {
+ std::shared_ptr<CSettingNumber> settingNumber =
+ std::static_pointer_cast<CSettingNumber>(m_pSetting);
+ if (settingNumber->SetValue(static_cast<double>(slider->GetFloatValue())))
+ strText = CGUIControlSliderSetting::GetText(
+ m_pSetting, settingNumber->GetValue(), settingNumber->GetMinimum(),
+ settingNumber->GetStep(), settingNumber->GetMaximum(), m_localizer);
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ if (!strText.empty())
+ slider->SetTextValue(strText);
+}
+
+CGUIControlEditSetting::CGUIControlEditSetting(CGUIEditControl* pEdit,
+ int id,
+ const std::shared_ptr<CSetting>& pSetting,
+ ILocalizer* localizer)
+ : CGUIControlBaseSetting(id, pSetting, localizer)
+{
+ std::shared_ptr<const CSettingControlEdit> control =
+ std::static_pointer_cast<const CSettingControlEdit>(pSetting->GetControl());
+ m_pEdit = pEdit;
+ if (m_pEdit == NULL)
+ return;
+
+ m_pEdit->SetID(id);
+ int heading = m_pSetting->GetLabel();
+ if (control->GetHeading() > 0)
+ heading = control->GetHeading();
+ if (heading < 0)
+ heading = 0;
+
+ CGUIEditControl::INPUT_TYPE inputType = CGUIEditControl::INPUT_TYPE_TEXT;
+ const std::string& controlFormat = control->GetFormat();
+ if (controlFormat == "string")
+ {
+ if (control->IsHidden())
+ inputType = CGUIEditControl::INPUT_TYPE_PASSWORD;
+ }
+ else if (controlFormat == "integer" || controlFormat == "number")
+ {
+ if (control->VerifyNewValue())
+ inputType = CGUIEditControl::INPUT_TYPE_PASSWORD_NUMBER_VERIFY_NEW;
+ else
+ inputType = CGUIEditControl::INPUT_TYPE_NUMBER;
+ }
+ else if (controlFormat == "ip")
+ inputType = CGUIEditControl::INPUT_TYPE_IPADDRESS;
+ else if (controlFormat == "md5")
+ inputType = CGUIEditControl::INPUT_TYPE_PASSWORD_MD5;
+
+ m_pEdit->SetInputType(inputType, heading);
+
+ // this will automatically trigger validation so it must be executed after
+ // having set the value of the control based on the value of the setting
+ m_pEdit->SetInputValidation(InputValidation, this);
+}
+
+CGUIControlEditSetting::~CGUIControlEditSetting() = default;
+
+bool CGUIControlEditSetting::OnClick()
+{
+ if (m_pEdit == NULL)
+ return false;
+
+ // update our string
+ if (m_pSetting->GetControl()->GetFormat() == "urlencoded")
+ {
+ std::shared_ptr<CSettingUrlEncodedString> urlEncodedSetting =
+ std::static_pointer_cast<CSettingUrlEncodedString>(m_pSetting);
+ SetValid(urlEncodedSetting->SetDecodedValue(m_pEdit->GetLabel2()));
+ }
+ else
+ SetValid(m_pSetting->FromString(m_pEdit->GetLabel2()));
+
+ return IsValid();
+}
+
+void CGUIControlEditSetting::Update(bool fromControl, bool updateDisplayOnly)
+{
+ if (fromControl || m_pEdit == NULL)
+ return;
+
+ CGUIControlBaseSetting::Update(fromControl, updateDisplayOnly);
+
+ std::shared_ptr<const CSettingControlEdit> control =
+ std::static_pointer_cast<const CSettingControlEdit>(m_pSetting->GetControl());
+
+ if (control->GetFormat() == "urlencoded")
+ {
+ std::shared_ptr<CSettingUrlEncodedString> urlEncodedSetting =
+ std::static_pointer_cast<CSettingUrlEncodedString>(m_pSetting);
+ m_pEdit->SetLabel2(urlEncodedSetting->GetDecodedValue());
+ }
+ else
+ m_pEdit->SetLabel2(m_pSetting->ToString());
+}
+
+bool CGUIControlEditSetting::InputValidation(const std::string& input, void* data)
+{
+ if (data == NULL)
+ return true;
+
+ CGUIControlEditSetting* editControl = reinterpret_cast<CGUIControlEditSetting*>(data);
+ if (editControl->GetSetting() == NULL)
+ return true;
+
+ editControl->SetValid(editControl->GetSetting()->CheckValidity(input));
+ return editControl->IsValid();
+}
+
+CGUIControlSliderSetting::CGUIControlSliderSetting(CGUISettingsSliderControl* pSlider,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer)
+ : CGUIControlBaseSetting(id, std::move(pSetting), localizer)
+{
+ m_pSlider = pSlider;
+ if (m_pSlider == NULL)
+ return;
+
+ m_pSlider->SetID(id);
+
+ switch (m_pSetting->GetType())
+ {
+ case SettingType::Integer:
+ {
+ std::shared_ptr<CSettingInt> settingInt = std::static_pointer_cast<CSettingInt>(m_pSetting);
+ if (m_pSetting->GetControl()->GetFormat() == "percentage")
+ m_pSlider->SetType(SLIDER_CONTROL_TYPE_PERCENTAGE);
+ else
+ {
+ m_pSlider->SetType(SLIDER_CONTROL_TYPE_INT);
+ m_pSlider->SetRange(settingInt->GetMinimum(), settingInt->GetMaximum());
+ }
+ m_pSlider->SetIntInterval(settingInt->GetStep());
+ break;
+ }
+
+ case SettingType::Number:
+ {
+ std::shared_ptr<CSettingNumber> settingNumber =
+ std::static_pointer_cast<CSettingNumber>(m_pSetting);
+ m_pSlider->SetType(SLIDER_CONTROL_TYPE_FLOAT);
+ m_pSlider->SetFloatRange(static_cast<float>(settingNumber->GetMinimum()),
+ static_cast<float>(settingNumber->GetMaximum()));
+ m_pSlider->SetFloatInterval(static_cast<float>(settingNumber->GetStep()));
+ break;
+ }
+
+ default:
+ break;
+ }
+}
+
+CGUIControlSliderSetting::~CGUIControlSliderSetting() = default;
+
+bool CGUIControlSliderSetting::OnClick()
+{
+ if (m_pSlider == NULL)
+ return false;
+
+ switch (m_pSetting->GetType())
+ {
+ case SettingType::Integer:
+ SetValid(
+ std::static_pointer_cast<CSettingInt>(m_pSetting)->SetValue(m_pSlider->GetIntValue()));
+ break;
+
+ case SettingType::Number:
+ SetValid(std::static_pointer_cast<CSettingNumber>(m_pSetting)
+ ->SetValue(static_cast<double>(m_pSlider->GetFloatValue())));
+ break;
+
+ default:
+ return false;
+ }
+
+ return IsValid();
+}
+
+void CGUIControlSliderSetting::Update(bool fromControl, bool updateDisplayOnly)
+{
+ if (m_pSlider == NULL)
+ return;
+
+ CGUIControlBaseSetting::Update(fromControl, updateDisplayOnly);
+
+ std::string strText;
+ switch (m_pSetting->GetType())
+ {
+ case SettingType::Integer:
+ {
+ std::shared_ptr<const CSettingInt> settingInt =
+ std::static_pointer_cast<CSettingInt>(m_pSetting);
+ int value;
+ if (fromControl)
+ value = m_pSlider->GetIntValue();
+ else
+ {
+ value = std::static_pointer_cast<CSettingInt>(m_pSetting)->GetValue();
+ m_pSlider->SetIntValue(value);
+ }
+
+ strText = CGUIControlSliderSetting::GetText(m_pSetting, value, settingInt->GetMinimum(),
+ settingInt->GetStep(), settingInt->GetMaximum(),
+ m_localizer);
+ break;
+ }
+
+ case SettingType::Number:
+ {
+ std::shared_ptr<const CSettingNumber> settingNumber =
+ std::static_pointer_cast<CSettingNumber>(m_pSetting);
+ double value;
+ if (fromControl)
+ value = static_cast<double>(m_pSlider->GetFloatValue());
+ else
+ {
+ value = std::static_pointer_cast<CSettingNumber>(m_pSetting)->GetValue();
+ m_pSlider->SetFloatValue((float)value);
+ }
+
+ strText = CGUIControlSliderSetting::GetText(m_pSetting, value, settingNumber->GetMinimum(),
+ settingNumber->GetStep(),
+ settingNumber->GetMaximum(), m_localizer);
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ if (!strText.empty())
+ m_pSlider->SetTextValue(strText);
+}
+
+std::string CGUIControlSliderSetting::GetText(const std::shared_ptr<CSetting>& setting,
+ const CVariant& value,
+ const CVariant& minimum,
+ const CVariant& step,
+ const CVariant& maximum,
+ ILocalizer* localizer)
+{
+ if (setting == NULL || !(value.isInteger() || value.isDouble()))
+ return "";
+
+ const auto control = std::static_pointer_cast<const CSettingControlSlider>(setting->GetControl());
+ if (control == NULL)
+ return "";
+
+ SettingControlSliderFormatter formatter = control->GetFormatter();
+ if (formatter != NULL)
+ return formatter(control, value, minimum, step, maximum);
+
+ std::string formatString = control->GetFormatString();
+ if (control->GetFormatLabel() > -1)
+ formatString = ::Localize(control->GetFormatLabel(), localizer);
+
+ std::string formattedString;
+ if (FormatText(formatString, value, setting->GetId(), formattedString))
+ return formattedString;
+
+ // fall back to default formatting
+ formatString = control->GetDefaultFormatString();
+ if (FormatText(formatString, value, setting->GetId(), formattedString))
+ return formattedString;
+
+ return "";
+}
+
+bool CGUIControlSliderSetting::FormatText(const std::string& formatString,
+ const CVariant& value,
+ const std::string& settingId,
+ std::string& formattedText)
+{
+ try
+ {
+ if (value.isDouble())
+ formattedText = StringUtils::Format(formatString, value.asDouble());
+ else
+ formattedText = StringUtils::Format(formatString, static_cast<int>(value.asInteger()));
+ }
+ catch (const std::runtime_error& err)
+ {
+ CLog::Log(LOGERROR, "Invalid formatting with string \"{}\" for setting \"{}\": {}",
+ formatString, settingId, err.what());
+ return false;
+ }
+
+ return true;
+}
+
+CGUIControlRangeSetting::CGUIControlRangeSetting(CGUISettingsSliderControl* pSlider,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer)
+ : CGUIControlBaseSetting(id, std::move(pSetting), localizer)
+{
+ m_pSlider = pSlider;
+ if (m_pSlider == NULL)
+ return;
+
+ m_pSlider->SetID(id);
+ m_pSlider->SetRangeSelection(true);
+
+ if (m_pSetting->GetType() == SettingType::List)
+ {
+ std::shared_ptr<CSettingList> settingList = std::static_pointer_cast<CSettingList>(m_pSetting);
+ SettingConstPtr listDefintion = settingList->GetDefinition();
+ switch (listDefintion->GetType())
+ {
+ case SettingType::Integer:
+ {
+ std::shared_ptr<const CSettingInt> listDefintionInt =
+ std::static_pointer_cast<const CSettingInt>(listDefintion);
+ if (m_pSetting->GetControl()->GetFormat() == "percentage")
+ m_pSlider->SetType(SLIDER_CONTROL_TYPE_PERCENTAGE);
+ else
+ {
+ m_pSlider->SetType(SLIDER_CONTROL_TYPE_INT);
+ m_pSlider->SetRange(listDefintionInt->GetMinimum(), listDefintionInt->GetMaximum());
+ }
+ m_pSlider->SetIntInterval(listDefintionInt->GetStep());
+ break;
+ }
+
+ case SettingType::Number:
+ {
+ std::shared_ptr<const CSettingNumber> listDefinitionNumber =
+ std::static_pointer_cast<const CSettingNumber>(listDefintion);
+ m_pSlider->SetType(SLIDER_CONTROL_TYPE_FLOAT);
+ m_pSlider->SetFloatRange(static_cast<float>(listDefinitionNumber->GetMinimum()),
+ static_cast<float>(listDefinitionNumber->GetMaximum()));
+ m_pSlider->SetFloatInterval(static_cast<float>(listDefinitionNumber->GetStep()));
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+}
+
+CGUIControlRangeSetting::~CGUIControlRangeSetting() = default;
+
+bool CGUIControlRangeSetting::OnClick()
+{
+ if (m_pSlider == NULL || m_pSetting->GetType() != SettingType::List)
+ return false;
+
+ std::shared_ptr<CSettingList> settingList = std::static_pointer_cast<CSettingList>(m_pSetting);
+ const SettingList& settingListValues = settingList->GetValue();
+ if (settingListValues.size() != 2)
+ return false;
+
+ std::vector<CVariant> values;
+ SettingConstPtr listDefintion = settingList->GetDefinition();
+ switch (listDefintion->GetType())
+ {
+ case SettingType::Integer:
+ values.emplace_back(m_pSlider->GetIntValue(CGUISliderControl::RangeSelectorLower));
+ values.emplace_back(m_pSlider->GetIntValue(CGUISliderControl::RangeSelectorUpper));
+ break;
+
+ case SettingType::Number:
+ values.emplace_back(m_pSlider->GetFloatValue(CGUISliderControl::RangeSelectorLower));
+ values.emplace_back(m_pSlider->GetFloatValue(CGUISliderControl::RangeSelectorUpper));
+ break;
+
+ default:
+ return false;
+ }
+
+ if (values.size() != 2)
+ return false;
+
+ SetValid(CSettingUtils::SetList(settingList, values));
+ return IsValid();
+}
+
+void CGUIControlRangeSetting::Update(bool fromControl, bool updateDisplayOnly)
+{
+ if (m_pSlider == NULL || m_pSetting->GetType() != SettingType::List)
+ return;
+
+ CGUIControlBaseSetting::Update(fromControl, updateDisplayOnly);
+
+ std::shared_ptr<CSettingList> settingList = std::static_pointer_cast<CSettingList>(m_pSetting);
+ const SettingList& settingListValues = settingList->GetValue();
+ if (settingListValues.size() != 2)
+ return;
+
+ SettingConstPtr listDefintion = settingList->GetDefinition();
+ std::shared_ptr<const CSettingControlRange> controlRange =
+ std::static_pointer_cast<const CSettingControlRange>(m_pSetting->GetControl());
+ const std::string& controlFormat = controlRange->GetFormat();
+
+ std::string strText;
+ std::string strTextLower, strTextUpper;
+ std::string formatString =
+ Localize(controlRange->GetFormatLabel() > -1 ? controlRange->GetFormatLabel() : 21469);
+ std::string valueFormat = controlRange->GetValueFormat();
+ if (controlRange->GetValueFormatLabel() > -1)
+ valueFormat = Localize(controlRange->GetValueFormatLabel());
+
+ switch (listDefintion->GetType())
+ {
+ case SettingType::Integer:
+ {
+ int valueLower, valueUpper;
+ if (fromControl)
+ {
+ valueLower = m_pSlider->GetIntValue(CGUISliderControl::RangeSelectorLower);
+ valueUpper = m_pSlider->GetIntValue(CGUISliderControl::RangeSelectorUpper);
+ }
+ else
+ {
+ valueLower = std::static_pointer_cast<CSettingInt>(settingListValues[0])->GetValue();
+ valueUpper = std::static_pointer_cast<CSettingInt>(settingListValues[1])->GetValue();
+ m_pSlider->SetIntValue(valueLower, CGUISliderControl::RangeSelectorLower);
+ m_pSlider->SetIntValue(valueUpper, CGUISliderControl::RangeSelectorUpper);
+ }
+
+ if (controlFormat == "date" || controlFormat == "time")
+ {
+ CDateTime dateLower((time_t)valueLower);
+ CDateTime dateUpper((time_t)valueUpper);
+
+ if (controlFormat == "date")
+ {
+ if (valueFormat.empty())
+ {
+ strTextLower = dateLower.GetAsLocalizedDate();
+ strTextUpper = dateUpper.GetAsLocalizedDate();
+ }
+ else
+ {
+ strTextLower = dateLower.GetAsLocalizedDate(valueFormat);
+ strTextUpper = dateUpper.GetAsLocalizedDate(valueFormat);
+ }
+ }
+ else
+ {
+ if (valueFormat.empty())
+ valueFormat = "mm:ss";
+
+ strTextLower = dateLower.GetAsLocalizedTime(valueFormat);
+ strTextUpper = dateUpper.GetAsLocalizedTime(valueFormat);
+ }
+ }
+ else
+ {
+ strTextLower = StringUtils::Format(valueFormat, valueLower);
+ strTextUpper = StringUtils::Format(valueFormat, valueUpper);
+ }
+
+ if (valueLower != valueUpper)
+ strText = StringUtils::Format(formatString, strTextLower, strTextUpper);
+ else
+ strText = strTextLower;
+ break;
+ }
+
+ case SettingType::Number:
+ {
+ double valueLower, valueUpper;
+ if (fromControl)
+ {
+ valueLower =
+ static_cast<double>(m_pSlider->GetFloatValue(CGUISliderControl::RangeSelectorLower));
+ valueUpper =
+ static_cast<double>(m_pSlider->GetFloatValue(CGUISliderControl::RangeSelectorUpper));
+ }
+ else
+ {
+ valueLower = std::static_pointer_cast<CSettingNumber>(settingListValues[0])->GetValue();
+ valueUpper = std::static_pointer_cast<CSettingNumber>(settingListValues[1])->GetValue();
+ m_pSlider->SetFloatValue((float)valueLower, CGUISliderControl::RangeSelectorLower);
+ m_pSlider->SetFloatValue((float)valueUpper, CGUISliderControl::RangeSelectorUpper);
+ }
+
+ strTextLower = StringUtils::Format(valueFormat, valueLower);
+ if (valueLower != valueUpper)
+ {
+ strTextUpper = StringUtils::Format(valueFormat, valueUpper);
+ strText = StringUtils::Format(formatString, strTextLower, strTextUpper);
+ }
+ else
+ strText = strTextLower;
+ break;
+ }
+
+ default:
+ strText.clear();
+ break;
+ }
+
+ if (!strText.empty())
+ m_pSlider->SetTextValue(strText);
+}
+
+CGUIControlSeparatorSetting::CGUIControlSeparatorSetting(CGUIImage* pImage,
+ int id,
+ ILocalizer* localizer)
+ : CGUIControlBaseSetting(id, NULL, localizer)
+{
+ m_pImage = pImage;
+ if (m_pImage == NULL)
+ return;
+
+ m_pImage->SetID(id);
+}
+
+CGUIControlSeparatorSetting::~CGUIControlSeparatorSetting() = default;
+
+CGUIControlGroupTitleSetting::CGUIControlGroupTitleSetting(CGUILabelControl* pLabel,
+ int id,
+ ILocalizer* localizer)
+ : CGUIControlBaseSetting(id, NULL, localizer)
+{
+ m_pLabel = pLabel;
+ if (m_pLabel == NULL)
+ return;
+
+ m_pLabel->SetID(id);
+}
+
+CGUIControlGroupTitleSetting::~CGUIControlGroupTitleSetting() = default;
+
+CGUIControlLabelSetting::CGUIControlLabelSetting(CGUIButtonControl* pButton,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer)
+ : CGUIControlBaseSetting(id, std::move(pSetting), localizer)
+{
+ m_pButton = pButton;
+ if (m_pButton == NULL)
+ return;
+
+ m_pButton->SetID(id);
+ UpdateFromSetting();
+}
diff --git a/xbmc/settings/windows/GUIControlSettings.h b/xbmc/settings/windows/GUIControlSettings.h
new file mode 100644
index 0000000..777b60d
--- /dev/null
+++ b/xbmc/settings/windows/GUIControlSettings.h
@@ -0,0 +1,366 @@
+/*
+ * 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 "guilib/ISliderCallback.h"
+#include "utils/ILocalizer.h"
+
+#include <functional>
+#include <memory>
+#include <stdlib.h>
+#include <string>
+
+class CGUIControl;
+class CGUIImage;
+class CGUISpinControlEx;
+class CGUIEditControl;
+class CGUIButtonControl;
+class CGUIRadioButtonControl;
+class CGUISettingsSliderControl;
+class CGUILabelControl;
+class CGUIColorButtonControl;
+
+class CSetting;
+class CSettingControlSlider;
+class CSettingString;
+class CSettingPath;
+
+class CFileItemList;
+class CVariant;
+
+class CGUIControlBaseSetting : protected ILocalizer
+{
+public:
+ CGUIControlBaseSetting(int id, std::shared_ptr<CSetting> pSetting, ILocalizer* localizer);
+ ~CGUIControlBaseSetting() override = default;
+
+ int GetID() const { return m_id; }
+ std::shared_ptr<CSetting> GetSetting() { return m_pSetting; }
+
+ /*!
+ \brief Specifies that this setting should update after a delay
+ Useful for settings that have options to navigate through
+ and may take a while, or require additional input to update
+ once the final setting is chosen. Settings default to updating
+ instantly.
+ \sa IsDelayed()
+ */
+ void SetDelayed() { m_delayed = true; }
+
+ /*!
+ \brief Returns whether this setting should have delayed update
+ \return true if the setting's update should be delayed
+ \sa SetDelayed()
+ */
+ bool IsDelayed() const { return m_delayed; }
+
+ /*!
+ \brief Returns whether this setting is enabled or disabled
+ This state is independent of the real enabled state of a
+ setting control but represents the enabled state of the
+ setting itself based on specific conditions.
+ \return true if the setting is enabled otherwise false
+ \sa SetEnabled()
+ */
+ bool IsEnabled() const;
+
+ /*!
+ \brief Returns whether the setting's value is valid or not
+ */
+ bool IsValid() const { return m_valid; }
+
+ void SetValid(bool valid) { m_valid = valid; }
+
+ virtual CGUIControl* GetControl() { return NULL; }
+ virtual bool OnClick() { return false; }
+ void UpdateFromControl();
+ void UpdateFromSetting(bool updateDisplayOnly = false);
+ virtual void Clear() = 0; ///< Clears the attached control
+protected:
+ // implementation of ILocalizer
+ std::string Localize(std::uint32_t code) const override;
+
+ virtual void Update(bool fromControl, bool updateDisplayOnly);
+
+ int m_id;
+ std::shared_ptr<CSetting> m_pSetting;
+ ILocalizer* m_localizer;
+ bool m_delayed;
+ bool m_valid;
+};
+
+class CGUIControlRadioButtonSetting : public CGUIControlBaseSetting
+{
+public:
+ CGUIControlRadioButtonSetting(CGUIRadioButtonControl* pRadioButton,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer);
+ ~CGUIControlRadioButtonSetting() override;
+
+ void Select(bool bSelect);
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pRadioButton); }
+ bool OnClick() override;
+ void Clear() override { m_pRadioButton = NULL; }
+
+protected:
+ // specialization of CGUIControlBaseSetting
+ void Update(bool fromControl, bool updateDisplayOnly) override;
+
+private:
+ CGUIRadioButtonControl* m_pRadioButton;
+};
+
+class CGUIControlColorButtonSetting : public CGUIControlBaseSetting
+{
+public:
+ CGUIControlColorButtonSetting(CGUIColorButtonControl* pColorControl,
+ int id,
+ const std::shared_ptr<CSetting>& pSetting,
+ ILocalizer* localizer);
+ ~CGUIControlColorButtonSetting() override;
+
+ void Select(bool bSelect);
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pColorButton); }
+ bool OnClick() override;
+ void Clear() override { m_pColorButton = nullptr; }
+
+protected:
+ // specialization of CGUIControlBaseSetting
+ void Update(bool fromControl, bool updateDisplayOnly) override;
+
+private:
+ CGUIColorButtonControl* m_pColorButton;
+};
+
+class CGUIControlSpinExSetting : public CGUIControlBaseSetting
+{
+public:
+ CGUIControlSpinExSetting(CGUISpinControlEx* pSpin,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer);
+ ~CGUIControlSpinExSetting() override;
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pSpin); }
+ bool OnClick() override;
+ void Clear() override { m_pSpin = NULL; }
+
+protected:
+ // specialization of CGUIControlBaseSetting
+ void Update(bool fromControl, bool updateDisplayOnly) override;
+
+private:
+ void FillControl(bool updateDisplayOnly);
+ void FillIntegerSettingControl(bool updateValues);
+ void FillFloatSettingControl();
+ void FillStringSettingControl(bool updateValues);
+ CGUISpinControlEx* m_pSpin;
+};
+
+class CGUIControlListSetting : public CGUIControlBaseSetting
+{
+public:
+ CGUIControlListSetting(CGUIButtonControl* pButton,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer);
+ ~CGUIControlListSetting() override;
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pButton); }
+ bool OnClick() override;
+ void Clear() override { m_pButton = NULL; }
+
+protected:
+ // specialization of CGUIControlBaseSetting
+ void Update(bool fromControl, bool updateDisplayOnly) override;
+
+private:
+ bool GetItems(const std::shared_ptr<const CSetting>& setting,
+ CFileItemList& items,
+ bool updateItems) const;
+ bool GetIntegerItems(const std::shared_ptr<const CSetting>& setting,
+ CFileItemList& items,
+ bool updateItems) const;
+ bool GetStringItems(const std::shared_ptr<const CSetting>& setting,
+ CFileItemList& items,
+ bool updateItems) const;
+
+ CGUIButtonControl* m_pButton;
+};
+
+class CGUIControlListColorSetting : public CGUIControlBaseSetting
+{
+public:
+ CGUIControlListColorSetting(CGUIButtonControl* pButton,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer);
+ ~CGUIControlListColorSetting() override;
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pButton); }
+ bool OnClick() override;
+ void Clear() override { m_pButton = nullptr; }
+
+protected:
+ // specialization of CGUIControlBaseSetting
+ void Update(bool fromControl, bool updateDisplayOnly) override;
+
+private:
+ CGUIButtonControl* m_pButton;
+};
+
+class CGUIControlButtonSetting : public CGUIControlBaseSetting, protected ISliderCallback
+{
+public:
+ CGUIControlButtonSetting(CGUIButtonControl* pButton,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer);
+ ~CGUIControlButtonSetting() override;
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pButton); }
+ bool OnClick() override;
+ void Clear() override { m_pButton = NULL; }
+
+ static bool GetPath(const std::shared_ptr<CSettingPath>& pathSetting, ILocalizer* localizer);
+
+protected:
+ // specialization of CGUIControlBaseSetting
+ void Update(bool fromControl, bool updateDisplayOnly) override;
+
+ // implementations of ISliderCallback
+ void OnSliderChange(void* data, CGUISliderControl* slider) override;
+
+private:
+ CGUIButtonControl* m_pButton;
+};
+
+class CGUIControlEditSetting : public CGUIControlBaseSetting
+{
+public:
+ CGUIControlEditSetting(CGUIEditControl* pButton,
+ int id,
+ const std::shared_ptr<CSetting>& pSetting,
+ ILocalizer* localizer);
+ ~CGUIControlEditSetting() override;
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pEdit); }
+ bool OnClick() override;
+ void Clear() override { m_pEdit = NULL; }
+
+protected:
+ // specialization of CGUIControlBaseSetting
+ void Update(bool fromControl, bool updateDisplayOnly) override;
+
+private:
+ static bool InputValidation(const std::string& input, void* data);
+
+ CGUIEditControl* m_pEdit;
+};
+
+class CGUIControlSliderSetting : public CGUIControlBaseSetting
+{
+public:
+ CGUIControlSliderSetting(CGUISettingsSliderControl* pSlider,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer);
+ ~CGUIControlSliderSetting() override;
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pSlider); }
+ bool OnClick() override;
+ void Clear() override { m_pSlider = NULL; }
+
+ static std::string GetText(const std::shared_ptr<CSetting>& setting,
+ const CVariant& value,
+ const CVariant& minimum,
+ const CVariant& step,
+ const CVariant& maximum,
+ ILocalizer* localizer);
+
+protected:
+ // specialization of CGUIControlBaseSetting
+ void Update(bool fromControl, bool updateDisplayOnly) override;
+
+private:
+ static bool FormatText(const std::string& formatString,
+ const CVariant& value,
+ const std::string& settingId,
+ std::string& formattedText);
+
+ CGUISettingsSliderControl* m_pSlider;
+};
+
+class CGUIControlRangeSetting : public CGUIControlBaseSetting
+{
+public:
+ CGUIControlRangeSetting(CGUISettingsSliderControl* pSlider,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer);
+ ~CGUIControlRangeSetting() override;
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pSlider); }
+ bool OnClick() override;
+ void Clear() override { m_pSlider = NULL; }
+
+protected:
+ // specialization of CGUIControlBaseSetting
+ void Update(bool fromControl, bool updateDisplayOnly) override;
+
+private:
+ CGUISettingsSliderControl* m_pSlider;
+};
+
+class CGUIControlSeparatorSetting : public CGUIControlBaseSetting
+{
+public:
+ CGUIControlSeparatorSetting(CGUIImage* pImage, int id, ILocalizer* localizer);
+ ~CGUIControlSeparatorSetting() override;
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pImage); }
+ bool OnClick() override { return false; }
+ void Clear() override { m_pImage = NULL; }
+
+private:
+ CGUIImage* m_pImage;
+};
+
+class CGUIControlGroupTitleSetting : public CGUIControlBaseSetting
+{
+public:
+ CGUIControlGroupTitleSetting(CGUILabelControl* pLabel, int id, ILocalizer* localizer);
+ ~CGUIControlGroupTitleSetting() override;
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pLabel); }
+ bool OnClick() override { return false; }
+ void Clear() override { m_pLabel = NULL; }
+
+private:
+ CGUILabelControl* m_pLabel;
+};
+
+class CGUIControlLabelSetting : public CGUIControlBaseSetting
+{
+public:
+ CGUIControlLabelSetting(CGUIButtonControl* pButton,
+ int id,
+ std::shared_ptr<CSetting> pSetting,
+ ILocalizer* localizer);
+ ~CGUIControlLabelSetting() override = default;
+
+ CGUIControl* GetControl() override { return reinterpret_cast<CGUIControl*>(m_pButton); }
+ void Clear() override { m_pButton = NULL; }
+
+private:
+ CGUIButtonControl* m_pButton;
+};
diff --git a/xbmc/settings/windows/GUIWindowSettings.cpp b/xbmc/settings/windows/GUIWindowSettings.cpp
new file mode 100644
index 0000000..d8edede
--- /dev/null
+++ b/xbmc/settings/windows/GUIWindowSettings.cpp
@@ -0,0 +1,19 @@
+/*
+ * 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 "GUIWindowSettings.h"
+
+#include "guilib/WindowIDs.h"
+
+CGUIWindowSettings::CGUIWindowSettings(void)
+ : CGUIWindow(WINDOW_SETTINGS_MENU, "Settings.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIWindowSettings::~CGUIWindowSettings(void) = default;
diff --git a/xbmc/settings/windows/GUIWindowSettings.h b/xbmc/settings/windows/GUIWindowSettings.h
new file mode 100644
index 0000000..c658186
--- /dev/null
+++ b/xbmc/settings/windows/GUIWindowSettings.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "guilib/GUIWindow.h"
+
+class CGUIWindowSettings :
+ public CGUIWindow
+{
+public:
+ CGUIWindowSettings(void);
+ ~CGUIWindowSettings(void) override;
+};
diff --git a/xbmc/settings/windows/GUIWindowSettingsCategory.cpp b/xbmc/settings/windows/GUIWindowSettingsCategory.cpp
new file mode 100644
index 0000000..18be9cd
--- /dev/null
+++ b/xbmc/settings/windows/GUIWindowSettingsCategory.cpp
@@ -0,0 +1,244 @@
+/*
+ * 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 "GUIWindowSettingsCategory.h"
+
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "input/Key.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/SettingSection.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "utils/log.h"
+#include "view/ViewStateSettings.h"
+
+#include <string>
+
+#define SETTINGS_SYSTEM WINDOW_SETTINGS_SYSTEM - WINDOW_SETTINGS_START
+#define SETTINGS_SERVICE WINDOW_SETTINGS_SERVICE - WINDOW_SETTINGS_START
+#define SETTINGS_PVR WINDOW_SETTINGS_MYPVR - WINDOW_SETTINGS_START
+#define SETTINGS_PLAYER WINDOW_SETTINGS_PLAYER - WINDOW_SETTINGS_START
+#define SETTINGS_MEDIA WINDOW_SETTINGS_MEDIA - WINDOW_SETTINGS_START
+#define SETTINGS_INTERFACE WINDOW_SETTINGS_INTERFACE - WINDOW_SETTINGS_START
+#define SETTINGS_GAMES WINDOW_SETTINGS_MYGAMES - WINDOW_SETTINGS_START
+
+#define CONTROL_BTN_LEVELS 20
+
+typedef struct {
+ int id;
+ std::string name;
+} SettingGroup;
+
+static const SettingGroup s_settingGroupMap[] = { { SETTINGS_SYSTEM, "system" },
+ { SETTINGS_SERVICE, "services" },
+ { SETTINGS_PVR, "pvr" },
+ { SETTINGS_PLAYER, "player" },
+ { SETTINGS_MEDIA, "media" },
+ { SETTINGS_INTERFACE, "interface" },
+ { SETTINGS_GAMES, "games" } };
+
+#define SettingGroupSize sizeof(s_settingGroupMap) / sizeof(SettingGroup)
+
+CGUIWindowSettingsCategory::CGUIWindowSettingsCategory()
+ : CGUIDialogSettingsManagerBase(WINDOW_SETTINGS_SYSTEM, "SettingsCategory.xml"),
+ m_settings(CServiceBroker::GetSettingsComponent()->GetSettings())
+{
+ // set the correct ID range...
+ m_idRange.clear();
+ m_idRange.push_back(WINDOW_SETTINGS_SYSTEM);
+ m_idRange.push_back(WINDOW_SETTINGS_SERVICE);
+ m_idRange.push_back(WINDOW_SETTINGS_MYPVR);
+ m_idRange.push_back(WINDOW_SETTINGS_PLAYER);
+ m_idRange.push_back(WINDOW_SETTINGS_MEDIA);
+ m_idRange.push_back(WINDOW_SETTINGS_INTERFACE);
+ m_idRange.push_back(WINDOW_SETTINGS_MYGAMES);
+}
+
+CGUIWindowSettingsCategory::~CGUIWindowSettingsCategory() = default;
+
+bool CGUIWindowSettingsCategory::OnMessage(CGUIMessage &message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_iSection = message.GetParam2() - CGUIDialogSettingsManagerBase::GetID();
+ CGUIDialogSettingsManagerBase::OnMessage(message);
+ m_returningFromSkinLoad = false;
+
+ if (!message.GetStringParam(0).empty())
+ FocusElement(message.GetStringParam(0));
+
+ return true;
+ }
+
+ case GUI_MSG_FOCUSED:
+ {
+ if (!m_returningFromSkinLoad)
+ CGUIDialogSettingsManagerBase::OnMessage(message);
+ return true;
+ }
+
+ case GUI_MSG_LOAD_SKIN:
+ {
+ if (IsActive())
+ m_returningFromSkinLoad = true;
+ break;
+ }
+
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (message.GetParam1() == GUI_MSG_WINDOW_RESIZE)
+ {
+ if (IsActive() && CDisplaySettings::GetInstance().GetCurrentResolution() != CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution())
+ {
+ CDisplaySettings::GetInstance().SetCurrentResolution(CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(), true);
+ CreateSettings();
+ }
+ }
+ break;
+ }
+
+ case GUI_MSG_PLAYBACK_STARTED:
+ case GUI_MSG_PLAYBACK_ENDED:
+ case GUI_MSG_PLAYBACK_STOPPED:
+ {
+ if (IsActive())
+ {
+ UpdateSettings();
+ }
+ break;
+ }
+ }
+
+ return CGUIDialogSettingsManagerBase::OnMessage(message);
+}
+
+bool CGUIWindowSettingsCategory::OnAction(const CAction &action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_SETTINGS_LEVEL_CHANGE:
+ {
+ //Test if we can access the new level
+ if (!g_passwordManager.CheckSettingLevelLock(CViewStateSettings::GetInstance().GetNextSettingLevel(), true))
+ return false;
+
+ CViewStateSettings::GetInstance().CycleSettingLevel();
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ // try to keep the current position
+ std::string oldCategory;
+ if (m_iCategory >= 0 && m_iCategory < (int)m_categories.size())
+ oldCategory = m_categories[m_iCategory]->GetId();
+
+ SET_CONTROL_LABEL(CONTROL_BTN_LEVELS, 10036 + (int)CViewStateSettings::GetInstance().GetSettingLevel());
+ // only re-create the categories, the settings will be created later
+ SetupControls(false);
+
+ m_iCategory = 0;
+ // try to find the category that was previously selected
+ if (!oldCategory.empty())
+ {
+ for (int i = 0; i < (int)m_categories.size(); i++)
+ {
+ if (m_categories[i]->GetId() == oldCategory)
+ {
+ m_iCategory = i;
+ break;
+ }
+ }
+ }
+
+ CreateSettings();
+ return true;
+ }
+
+ default:
+ break;
+ }
+
+ return CGUIDialogSettingsManagerBase::OnAction(action);
+}
+
+bool CGUIWindowSettingsCategory::OnBack(int actionID)
+{
+ Save();
+ return CGUIDialogSettingsManagerBase::OnBack(actionID);
+}
+
+void CGUIWindowSettingsCategory::OnWindowLoaded()
+{
+ SET_CONTROL_LABEL(CONTROL_BTN_LEVELS, 10036 + (int)CViewStateSettings::GetInstance().GetSettingLevel());
+ CGUIDialogSettingsManagerBase::OnWindowLoaded();
+}
+
+int CGUIWindowSettingsCategory::GetSettingLevel() const
+{
+ return (int)CViewStateSettings::GetInstance().GetSettingLevel();
+}
+
+SettingSectionPtr CGUIWindowSettingsCategory::GetSection()
+{
+ for (const SettingGroup& settingGroup : s_settingGroupMap)
+ {
+ if (settingGroup.id == m_iSection)
+ return m_settings->GetSection(settingGroup.name);
+ }
+
+ return NULL;
+}
+
+bool CGUIWindowSettingsCategory::Save()
+{
+ m_settings->Save();
+
+ return true;
+}
+
+CSettingsManager* CGUIWindowSettingsCategory::GetSettingsManager() const
+{
+ return m_settings->GetSettingsManager();
+}
+
+void CGUIWindowSettingsCategory::FocusElement(const std::string& elementId)
+{
+ for (size_t i = 0; i < m_categories.size(); ++i)
+ {
+ if (m_categories[i]->GetId() == elementId)
+ {
+ SET_CONTROL_FOCUS(CONTROL_SETTINGS_START_BUTTONS + i, 0);
+ return;
+ }
+ for (const auto& group: m_categories[i]->GetGroups())
+ {
+ for (const auto& setting : group->GetSettings())
+ {
+ if (setting->GetId() == elementId)
+ {
+ SET_CONTROL_FOCUS(CONTROL_SETTINGS_START_BUTTONS + i, 0);
+
+ auto control = GetSettingControl(elementId);
+ if (control)
+ SET_CONTROL_FOCUS(control->GetID(), 0);
+ else
+ CLog::Log(LOGERROR,
+ "CGUIWindowSettingsCategory: failed to get control for setting '{}'.",
+ elementId);
+ return;
+ }
+ }
+ }
+ }
+ CLog::Log(LOGERROR,
+ "CGUIWindowSettingsCategory: failed to set focus. unknown category/setting id '{}'.",
+ elementId);
+}
diff --git a/xbmc/settings/windows/GUIWindowSettingsCategory.h b/xbmc/settings/windows/GUIWindowSettingsCategory.h
new file mode 100644
index 0000000..88c3ac8
--- /dev/null
+++ b/xbmc/settings/windows/GUIWindowSettingsCategory.h
@@ -0,0 +1,51 @@
+/*
+ * 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 "settings/dialogs/GUIDialogSettingsManagerBase.h"
+
+class CSettings;
+
+class CGUIWindowSettingsCategory : public CGUIDialogSettingsManagerBase
+{
+public:
+ CGUIWindowSettingsCategory();
+ ~CGUIWindowSettingsCategory() override;
+
+ // specialization of CGUIControl
+ bool OnMessage(CGUIMessage &message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnBack(int actionID) override;
+ int GetID() const override { return CGUIDialogSettingsManagerBase::GetID() + m_iSection; }
+
+ // specialization of CGUIWindow
+ bool IsDialog() const override { return false; }
+
+protected:
+ // specialization of CGUIWindow
+ void OnWindowLoaded() override;
+
+ // implementation of CGUIDialogSettingsBase
+ int GetSettingLevel() const override;
+ std::shared_ptr<CSettingSection> GetSection() override;
+ bool Save() override;
+
+ // implementation of CGUIDialogSettingsManagerBase
+ CSettingsManager* GetSettingsManager() const override;
+
+ /*!
+ * Set focus to a category or setting in this window. The setting/category must be active in the
+ * current level.
+ */
+ void FocusElement(const std::string& elementId);
+
+ std::shared_ptr<CSettings> m_settings;
+ int m_iSection = 0;
+ bool m_returningFromSkinLoad = false; // true if we are returning from loading the skin
+};
diff --git a/xbmc/settings/windows/GUIWindowSettingsScreenCalibration.cpp b/xbmc/settings/windows/GUIWindowSettingsScreenCalibration.cpp
new file mode 100644
index 0000000..7e67de6
--- /dev/null
+++ b/xbmc/settings/windows/GUIWindowSettingsScreenCalibration.cpp
@@ -0,0 +1,630 @@
+/*
+ * 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 "GUIWindowSettingsScreenCalibration.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMoverControl.h"
+#include "guilib/GUIResizeControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SubtitlesSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+
+#include <string>
+#include <utility>
+
+using namespace KODI;
+
+namespace
+{
+constexpr int CONTROL_LABEL_RES = 2;
+constexpr int CONTROL_LABEL_DESCRIPTION = 3;
+constexpr int CONTROL_LABEL_VALUE = 4;
+constexpr int CONTROL_TOP_LEFT = 8;
+constexpr int CONTROL_BOTTOM_RIGHT = 9;
+constexpr int CONTROL_SUBTITLES = 10;
+constexpr int CONTROL_PIXEL_RATIO = 11;
+constexpr int CONTROL_RESET = 12;
+constexpr int CONTROL_VIDEO = 20;
+
+constexpr int DEFAULT_GUI_HEIGHT = 1080;
+constexpr int DEFAULT_GUI_WIDTH = 1920;
+
+// Fixed transparent space of the subtitle bar (on top + below) for touch screen
+// must match with the space of the skin bar image
+constexpr int CONTROL_SUBTITLES_SPACE = 80;
+} // unnamed namespace
+
+CGUIWindowSettingsScreenCalibration::CGUIWindowSettingsScreenCalibration(void)
+ : CGUIWindow(WINDOW_SCREEN_CALIBRATION, "SettingsScreenCalibration.xml")
+{
+ m_iCurRes = 0;
+ m_iControl = 0;
+ m_fPixelRatioBoxHeight = 0.0f;
+ m_needsScaling = false; // we handle all the scaling
+}
+
+CGUIWindowSettingsScreenCalibration::~CGUIWindowSettingsScreenCalibration(void) = default;
+
+
+void CGUIWindowSettingsScreenCalibration::ResetCalibration()
+{
+ // We ask to reset the calibration
+ // Reset will be applied to: windowed mode or per fullscreen resolution
+ CGUIDialogYesNo* pDialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ pDialog->SetHeading(CVariant{20325});
+ std::string strText = StringUtils::Format(
+ g_localizeStrings.Get(20326),
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(m_Res[m_iCurRes]).strMode);
+ pDialog->SetText(CVariant{std::move(strText)});
+ pDialog->SetChoice(0, CVariant{222});
+ pDialog->SetChoice(1, CVariant{186});
+ pDialog->Open();
+ if (pDialog->IsConfirmed())
+ {
+ CServiceBroker::GetWinSystem()->GetGfxContext().ResetScreenParameters(m_Res[m_iCurRes]);
+ ResetControls();
+ // Send GUI_MSG_WINDOW_RESIZE to rescale font size/aspect for label controls
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(
+ GUI_MSG_NOTIFY_ALL, WINDOW_SCREEN_CALIBRATION, 0, GUI_MSG_WINDOW_RESIZE);
+ }
+}
+
+bool CGUIWindowSettingsScreenCalibration::OnAction(const CAction& action)
+{
+ switch (action.GetID())
+ {
+ case ACTION_CALIBRATE_SWAP_ARROWS:
+ {
+ NextControl();
+ return true;
+ }
+ break;
+
+ case ACTION_CALIBRATE_RESET:
+ {
+ ResetCalibration();
+ return true;
+ }
+ break;
+
+ case ACTION_CHANGE_RESOLUTION:
+ // choose the next resolution in our list
+ {
+ m_iCurRes = (m_iCurRes + 1) % m_Res.size();
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(m_Res[m_iCurRes], false);
+ ResetControls();
+ // Send GUI_MSG_WINDOW_RESIZE to rescale font size/aspect for label controls
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(
+ GUI_MSG_NOTIFY_ALL, WINDOW_SCREEN_CALIBRATION, 0, GUI_MSG_WINDOW_RESIZE);
+ return true;
+ }
+ break;
+
+ // ignore all gesture meta actions
+ case ACTION_GESTURE_BEGIN:
+ case ACTION_GESTURE_END:
+ case ACTION_GESTURE_ABORT:
+ case ACTION_GESTURE_NOTIFY:
+ case ACTION_GESTURE_PAN:
+ case ACTION_GESTURE_ROTATE:
+ case ACTION_GESTURE_ZOOM:
+ return true;
+
+ case ACTION_MOUSE_LEFT_CLICK:
+ case ACTION_TOUCH_TAP:
+ if (GetFocusedControlID() == CONTROL_RESET)
+ {
+ ResetCalibration();
+ return true;
+ }
+ break;
+ }
+
+ // if we see a mouse move event without dx and dy (amount2 and amount3) these
+ // are the focus actions which are generated on touch events and those should
+ // be eaten/ignored here. Else we will switch to the screencalibration controls
+ // which are at that x/y value on each touch/tap/swipe which makes the whole window
+ // unusable for touch screens
+ if (action.GetID() == ACTION_MOUSE_MOVE && action.GetAmount(2) == 0 && action.GetAmount(3) == 0)
+ return true;
+
+ return CGUIWindow::OnAction(action); // base class to handle basic movement etc.
+}
+
+void CGUIWindowSettingsScreenCalibration::AllocResources(bool forceLoad)
+{
+ CGUIWindow::AllocResources(forceLoad);
+}
+
+void CGUIWindowSettingsScreenCalibration::FreeResources(bool forceUnload)
+{
+ CGUIWindow::FreeResources(forceUnload);
+}
+
+
+bool CGUIWindowSettingsScreenCalibration::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ CDisplaySettings::GetInstance().UpdateCalibrations();
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetCalibrating(false);
+ // reset our screen resolution to what it was initially
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(
+ CDisplaySettings::GetInstance().GetCurrentResolution(), false);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(
+ GUI_MSG_NOTIFY_ALL, WINDOW_SCREEN_CALIBRATION, 0, GUI_MSG_WINDOW_RESIZE);
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CGUIWindow::OnMessage(message);
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetCalibrating(true);
+
+ // Get the default XML size values of controls,
+ // we will use these values to scale controls when the resolution change
+ for (int id = CONTROL_TOP_LEFT; id <= CONTROL_RESET; id++)
+ {
+ CGUIControl* control = GetControl(id);
+ if (control)
+ {
+ m_controlsSize.emplace(id, std::make_pair(control->GetHeight(), control->GetWidth()));
+ }
+ }
+
+ // Get the allowable resolutions that we can calibrate...
+ m_Res.clear();
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ bool isPlayingVideo{appPlayer->IsPlayingVideo()};
+ if (isPlayingVideo)
+ { // don't allow resolution switching if we are playing a video
+
+ appPlayer->TriggerUpdateResolution();
+
+ m_iCurRes = 0;
+ m_Res.push_back(CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution());
+ SET_CONTROL_VISIBLE(CONTROL_VIDEO);
+ }
+ else
+ {
+ SET_CONTROL_HIDDEN(CONTROL_VIDEO);
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetAllowedResolutions(m_Res);
+ // find our starting resolution
+ m_iCurRes = FindCurrentResolution();
+ }
+
+ // Setup the first control
+ m_iControl = CONTROL_TOP_LEFT;
+
+ m_isSubtitleBarEnabled =
+ !(CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()->GetAlignment() !=
+ SUBTITLES::Align::MANUAL &&
+ isPlayingVideo);
+
+ ResetControls();
+ return true;
+ }
+ break;
+ case GUI_MSG_CLICKED:
+ {
+ // On click event select the next control
+ NextControl();
+ }
+ break;
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (message.GetParam1() == GUI_MSG_WINDOW_RESIZE &&
+ message.GetSenderId() != WINDOW_SCREEN_CALIBRATION && IsActive())
+ {
+ m_Res.clear();
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetAllowedResolutions(m_Res);
+ m_iCurRes = FindCurrentResolution();
+ ResetControls();
+ }
+ }
+ break;
+ // send before touch for requesting gesture features - we don't want this
+ // it would result in unfocus in the onmessage below ...
+ case GUI_MSG_GESTURE_NOTIFY:
+ // send after touch for unfocussing - we don't want this in this window!
+ case GUI_MSG_UNFOCUS_ALL:
+ return true;
+ break;
+ }
+ return CGUIWindow::OnMessage(message);
+}
+
+unsigned int CGUIWindowSettingsScreenCalibration::FindCurrentResolution()
+{
+ RESOLUTION curRes = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution();
+ for (size_t i = 0; i < m_Res.size(); i++)
+ {
+ // If it's a CUSTOM (monitor) resolution, then CServiceBroker::GetWinSystem()->GetGfxContext().GetAllowedResolutions()
+ // returns just one entry with CUSTOM in it. Update that entry to point to the current
+ // CUSTOM resolution.
+ if (curRes >= RES_CUSTOM)
+ {
+ if (m_Res[i] == RES_CUSTOM)
+ {
+ m_Res[i] = curRes;
+ return i;
+ }
+ }
+ else if (m_Res[i] == CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution())
+ return i;
+ }
+ CLog::Log(LOGERROR, "CALIBRATION: Reported current resolution: {}",
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution());
+ CLog::Log(LOGERROR,
+ "CALIBRATION: Could not determine current resolution, falling back to default");
+ return 0;
+}
+
+void CGUIWindowSettingsScreenCalibration::NextControl()
+{ // set the old control invisible and not focused, and choose the next control
+ CGUIControl* pControl = GetControl(m_iControl);
+ if (pControl)
+ {
+ pControl->SetVisible(false);
+ pControl->SetFocus(false);
+ }
+ // If the current control is the reset button
+ // ask to reset the calibration settings
+ if (m_iControl == CONTROL_RESET)
+ {
+ ResetCalibration();
+ }
+ // switch to the next control
+ m_iControl++;
+ if (m_iControl > CONTROL_RESET)
+ m_iControl = CONTROL_TOP_LEFT;
+ // enable the new control
+ EnableControl(m_iControl);
+}
+
+void CGUIWindowSettingsScreenCalibration::EnableControl(int iControl)
+{
+ SET_CONTROL_VISIBLE(CONTROL_TOP_LEFT);
+ SET_CONTROL_VISIBLE(CONTROL_BOTTOM_RIGHT);
+ SET_CONTROL_VISIBLE(CONTROL_SUBTITLES);
+ SET_CONTROL_VISIBLE(CONTROL_PIXEL_RATIO);
+ SET_CONTROL_VISIBLE(CONTROL_RESET);
+ SET_CONTROL_FOCUS(iControl, 0);
+}
+
+void CGUIWindowSettingsScreenCalibration::ResetControls()
+{
+ // disable the video control, so that our other controls take mouse clicks etc.
+ CONTROL_DISABLE(CONTROL_VIDEO);
+ // disable the UI calibration for our controls
+ // and set their limits
+ // also, set them to invisible if they don't have focus
+ CGUIMoverControl* pControl = dynamic_cast<CGUIMoverControl*>(GetControl(CONTROL_TOP_LEFT));
+ RESOLUTION_INFO info =
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(m_Res[m_iCurRes]);
+
+ m_subtitleVerticalMargin = static_cast<int>(
+ static_cast<float>(info.iHeight) / 100 *
+ CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()->GetVerticalMarginPerc());
+
+ if (pControl)
+ {
+ pControl->SetLimits(-info.iWidth / 4, -info.iHeight / 4, info.iWidth / 4, info.iHeight / 4);
+ auto& size = m_controlsSize[CONTROL_TOP_LEFT];
+ pControl->SetHeight(size.first / DEFAULT_GUI_HEIGHT * info.iHeight);
+ pControl->SetWidth(size.second / DEFAULT_GUI_WIDTH * info.iWidth);
+ pControl->SetPosition(static_cast<float>(info.Overscan.left),
+ static_cast<float>(info.Overscan.top));
+ pControl->SetLocation(info.Overscan.left, info.Overscan.top, false);
+ }
+ pControl = dynamic_cast<CGUIMoverControl*>(GetControl(CONTROL_BOTTOM_RIGHT));
+ if (pControl)
+ {
+ pControl->SetLimits(info.iWidth * 3 / 4, info.iHeight * 3 / 4, info.iWidth * 5 / 4,
+ info.iHeight * 5 / 4);
+ auto& size = m_controlsSize[CONTROL_BOTTOM_RIGHT];
+ pControl->SetHeight(size.first / DEFAULT_GUI_HEIGHT * info.iHeight);
+ pControl->SetWidth(size.second / DEFAULT_GUI_WIDTH * info.iWidth);
+ pControl->SetPosition(static_cast<float>(info.Overscan.right) - pControl->GetWidth(),
+ static_cast<float>(info.Overscan.bottom) - pControl->GetHeight());
+ pControl->SetLocation(info.Overscan.right, info.Overscan.bottom, false);
+ }
+ // Subtitles and OSD controls can only move up and down
+ pControl = dynamic_cast<CGUIMoverControl*>(GetControl(CONTROL_SUBTITLES));
+ if (pControl)
+ {
+ auto& size = m_controlsSize[CONTROL_SUBTITLES];
+ float scaledHeight = size.first / DEFAULT_GUI_HEIGHT * info.iHeight;
+ float scaledSpace =
+ static_cast<float>(CONTROL_SUBTITLES_SPACE) / DEFAULT_GUI_HEIGHT * info.iHeight;
+ m_subtitlesHalfSpace = static_cast<int>(scaledSpace / 2);
+ int barHeight = static_cast<int>(scaledHeight - scaledSpace);
+ pControl->SetLimits(0, m_subtitlesHalfSpace + barHeight + info.Overscan.top, 0,
+ info.Overscan.bottom + m_subtitlesHalfSpace);
+ pControl->SetHeight(scaledHeight);
+ pControl->SetWidth(size.second / DEFAULT_GUI_WIDTH * info.iWidth);
+ // If the vertical margin has been changed from the previous calibration,
+ // the text bar could appear offscreen, then force move to visible area
+ if (info.iSubtitles - m_subtitleVerticalMargin > info.iHeight)
+ info.iSubtitles = info.Overscan.bottom;
+ // We want the text to be at the base of the bar,
+ // then we shift the position to include the vertical margin
+ pControl->SetPosition((info.iWidth - pControl->GetWidth()) * 0.5f,
+ info.iSubtitles - pControl->GetHeight() + m_subtitlesHalfSpace -
+ m_subtitleVerticalMargin);
+ pControl->SetLocation(0, info.iSubtitles + m_subtitlesHalfSpace - m_subtitleVerticalMargin,
+ false);
+ pControl->SetEnabled(m_isSubtitleBarEnabled);
+ }
+ // The pixel ratio control
+ CGUIResizeControl* pResize = dynamic_cast<CGUIResizeControl*>(GetControl(CONTROL_PIXEL_RATIO));
+ if (pResize)
+ {
+ pResize->SetLimits(info.iWidth * 0.25f, info.iHeight * 0.5f, info.iWidth * 0.75f,
+ info.iHeight * 0.5f);
+ pResize->SetHeight(info.iHeight * 0.5f);
+ pResize->SetWidth(pResize->GetHeight() / info.fPixelRatio);
+ pResize->SetPosition((info.iWidth - pResize->GetWidth()) / 2,
+ (info.iHeight - pResize->GetHeight()) / 2);
+ }
+ // The calibration reset
+ pControl = dynamic_cast<CGUIMoverControl*>(GetControl(CONTROL_RESET));
+ if (pControl)
+ {
+ auto& size = m_controlsSize[CONTROL_RESET];
+ pControl->SetHeight(size.first / DEFAULT_GUI_HEIGHT * info.iHeight);
+ pControl->SetWidth(size.second / DEFAULT_GUI_WIDTH * info.iWidth);
+ float posX = 0;
+ float posY = info.iHeight - pControl->GetHeight();
+ pControl->SetLimits(posX, posY, posX, posY);
+ pControl->SetPosition(posX, posY);
+ pControl->SetLocation(posX, posY, false);
+ }
+ // Enable the default control
+ EnableControl(m_iControl);
+}
+
+bool CGUIWindowSettingsScreenCalibration::UpdateFromControl(int iControl)
+{
+ RESOLUTION_INFO info =
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(m_Res[m_iCurRes]);
+ RESOLUTION_INFO infoPrev = info;
+ std::string labelDescription;
+ std::string labelValue;
+
+ if (iControl == CONTROL_PIXEL_RATIO)
+ {
+ CGUIControl* pControl = GetControl(CONTROL_PIXEL_RATIO);
+ if (pControl)
+ {
+ float fWidth = pControl->GetWidth();
+ float fHeight = pControl->GetHeight();
+ info.fPixelRatio = fHeight / fWidth;
+ // recenter our control...
+ pControl->SetPosition((static_cast<float>(info.iWidth) - pControl->GetWidth()) / 2,
+ (static_cast<float>(info.iHeight) - pControl->GetHeight()) / 2);
+ labelDescription = StringUtils::Format("[B]{}[/B][CR]{}", g_localizeStrings.Get(272),
+ g_localizeStrings.Get(273));
+ labelValue = StringUtils::Format("{:5.3f}", info.fPixelRatio);
+ labelValue = StringUtils::Format(g_localizeStrings.Get(20327), labelValue);
+ }
+ }
+ else
+ {
+ CGUIMoverControl* pControl = dynamic_cast<CGUIMoverControl*>(GetControl(iControl));
+ if (pControl)
+ {
+ switch (iControl)
+ {
+ case CONTROL_TOP_LEFT:
+ {
+ info.Overscan.left = pControl->GetXLocation();
+ info.Overscan.top = pControl->GetYLocation();
+ labelDescription = StringUtils::Format("[B]{}[/B][CR]{}", g_localizeStrings.Get(274),
+ g_localizeStrings.Get(276));
+ labelValue =
+ StringUtils::Format("{}, {}", pControl->GetXLocation(), pControl->GetYLocation());
+ labelValue = StringUtils::Format(g_localizeStrings.Get(20327), labelValue);
+ // Update reset control position
+ CGUIMoverControl* pControl = dynamic_cast<CGUIMoverControl*>(GetControl(CONTROL_RESET));
+ if (pControl)
+ {
+ float posX = info.Overscan.left;
+ float posY = info.Overscan.bottom - pControl->GetHeight();
+ pControl->SetLimits(posX, posY, posX, posY);
+ pControl->SetPosition(posX, posY);
+ pControl->SetLocation(posX, posY, false);
+ }
+ }
+ break;
+
+ case CONTROL_BOTTOM_RIGHT:
+ {
+ info.Overscan.right = pControl->GetXLocation();
+ info.Overscan.bottom = pControl->GetYLocation();
+ int iXOff1 = info.iWidth - pControl->GetXLocation();
+ int iYOff1 = info.iHeight - pControl->GetYLocation();
+ labelDescription = StringUtils::Format("[B]{}[/B][CR]{}", g_localizeStrings.Get(275),
+ g_localizeStrings.Get(276));
+ labelValue = StringUtils::Format("{}, {}", iXOff1, iYOff1);
+ labelValue = StringUtils::Format(g_localizeStrings.Get(20327), labelValue);
+ // Update reset control position
+ pControl = dynamic_cast<CGUIMoverControl*>(GetControl(CONTROL_RESET));
+ if (pControl)
+ {
+ float posX = info.Overscan.left;
+ float posY = info.Overscan.bottom - pControl->GetHeight();
+ pControl->SetLimits(posX, posY, posX, posY);
+ pControl->SetPosition(posX, posY);
+ pControl->SetLocation(posX, posY, false);
+ }
+ }
+ break;
+
+ case CONTROL_SUBTITLES:
+ {
+ if (m_isSubtitleBarEnabled)
+ {
+ info.iSubtitles =
+ pControl->GetYLocation() - m_subtitlesHalfSpace + m_subtitleVerticalMargin;
+
+ labelDescription = StringUtils::Format("[B]{}[/B][CR]{}", g_localizeStrings.Get(277),
+ g_localizeStrings.Get(278));
+ labelValue = StringUtils::Format(g_localizeStrings.Get(39184), info.iSubtitles,
+ info.iSubtitles - m_subtitleVerticalMargin);
+ }
+ else
+ {
+ labelDescription = StringUtils::Format("[B]{}[/B][CR]{}", g_localizeStrings.Get(277),
+ g_localizeStrings.Get(39189));
+ }
+ }
+ break;
+
+ case CONTROL_RESET:
+ {
+ labelDescription = g_localizeStrings.Get(20325);
+ }
+ break;
+ }
+ }
+ }
+
+ SET_CONTROL_LABEL(CONTROL_LABEL_DESCRIPTION, labelDescription);
+ SET_CONTROL_LABEL(CONTROL_LABEL_VALUE, labelValue);
+
+ // Set resolution info text
+ std::string resInfo;
+ if (CServiceBroker::GetWinSystem()->IsFullScreen())
+ {
+ resInfo =
+ StringUtils::Format("{} {}x{}@{:.2f} - {}", g_localizeStrings.Get(13287), info.iScreenWidth,
+ info.iScreenHeight, info.fRefreshRate, g_localizeStrings.Get(244));
+ }
+ else
+ {
+ resInfo = StringUtils::Format("{} {}x{} - {}", g_localizeStrings.Get(13287), info.iScreenWidth,
+ info.iScreenHeight, g_localizeStrings.Get(242));
+ }
+ SET_CONTROL_LABEL(CONTROL_LABEL_RES, resInfo);
+
+ // Detect overscan changes
+ bool isOverscanChanged = info.Overscan != infoPrev.Overscan;
+
+ // Adjust subtitle bar position due to overscan changes
+ if (isOverscanChanged)
+ {
+ CGUIMoverControl* pControl = dynamic_cast<CGUIMoverControl*>(GetControl(CONTROL_SUBTITLES));
+ if (pControl)
+ {
+ // Keep the subtitle bar within the overscan boundary
+ if (info.Overscan.bottom + m_subtitleVerticalMargin < info.iSubtitles)
+ {
+ info.iSubtitles = info.Overscan.bottom + m_subtitleVerticalMargin;
+
+ // We want the text to be at the base of the bar,
+ // then we shift the position to include the vertical margin
+ pControl->SetPosition((info.iWidth - pControl->GetWidth()) * 0.5f,
+ info.iSubtitles - pControl->GetHeight() + m_subtitlesHalfSpace -
+ m_subtitleVerticalMargin);
+ pControl->SetLocation(0, info.iSubtitles + m_subtitlesHalfSpace - m_subtitleVerticalMargin,
+ false);
+ }
+
+ // Recalculate limits based on overscan values
+ const auto& size = m_controlsSize[CONTROL_SUBTITLES];
+ const float scaledHeight = size.first / DEFAULT_GUI_HEIGHT * info.iHeight;
+ const float scaledSpace =
+ static_cast<float>(CONTROL_SUBTITLES_SPACE) / DEFAULT_GUI_HEIGHT * info.iHeight;
+
+ m_subtitlesHalfSpace = static_cast<int>(scaledSpace / 2);
+ const int barHeight = static_cast<int>(scaledHeight - scaledSpace);
+
+ pControl->SetLimits(0, m_subtitlesHalfSpace + barHeight + info.Overscan.top, 0,
+ info.Overscan.bottom + m_subtitlesHalfSpace);
+ }
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetResInfo(m_Res[m_iCurRes], info);
+
+ return isOverscanChanged;
+}
+
+void CGUIWindowSettingsScreenCalibration::FrameMove()
+{
+ m_iControl = GetFocusedControlID();
+ if (m_iControl >= 0)
+ {
+ if (UpdateFromControl(m_iControl))
+ {
+ // Send GUI_MSG_WINDOW_RESIZE to rescale font size/aspect for label controls
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(
+ GUI_MSG_NOTIFY_ALL, WINDOW_SCREEN_CALIBRATION, 0, GUI_MSG_WINDOW_RESIZE);
+ }
+ }
+ else
+ {
+ SET_CONTROL_LABEL(CONTROL_LABEL_DESCRIPTION, "");
+ SET_CONTROL_LABEL(CONTROL_LABEL_VALUE, "");
+ SET_CONTROL_LABEL(CONTROL_LABEL_RES, "");
+ }
+ CGUIWindow::FrameMove();
+}
+
+void CGUIWindowSettingsScreenCalibration::DoProcess(unsigned int currentTime,
+ CDirtyRegionList& dirtyregions)
+{
+ MarkDirtyRegion();
+
+ for (int i = CONTROL_TOP_LEFT; i <= CONTROL_RESET; i++)
+ SET_CONTROL_HIDDEN(i);
+ m_needsScaling = true;
+ CGUIWindow::DoProcess(currentTime, dirtyregions);
+ m_needsScaling = false;
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(m_Res[m_iCurRes], false);
+ CServiceBroker::GetWinSystem()->GetGfxContext().AddGUITransform();
+
+ // process the movers etc.
+ for (int i = CONTROL_TOP_LEFT; i <= CONTROL_RESET; i++)
+ {
+ SET_CONTROL_VISIBLE(i);
+ CGUIControl* control = GetControl(i);
+ if (control)
+ control->DoProcess(currentTime, dirtyregions);
+ }
+ CServiceBroker::GetWinSystem()->GetGfxContext().RemoveTransform();
+}
+
+void CGUIWindowSettingsScreenCalibration::DoRender()
+{
+ // we set that we need scaling here to render so that anything else on screen scales correctly
+ m_needsScaling = true;
+ CGUIWindow::DoRender();
+ m_needsScaling = false;
+}
diff --git a/xbmc/settings/windows/GUIWindowSettingsScreenCalibration.h b/xbmc/settings/windows/GUIWindowSettingsScreenCalibration.h
new file mode 100644
index 0000000..dd3e6b1
--- /dev/null
+++ b/xbmc/settings/windows/GUIWindowSettingsScreenCalibration.h
@@ -0,0 +1,47 @@
+/*
+ * 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 "guilib/GUIWindow.h"
+
+#include <map>
+#include <utility>
+#include <vector>
+
+class CGUIWindowSettingsScreenCalibration : public CGUIWindow
+{
+public:
+ CGUIWindowSettingsScreenCalibration(void);
+ ~CGUIWindowSettingsScreenCalibration(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ void DoProcess(unsigned int currentTime, CDirtyRegionList& dirtyregions) override;
+ void FrameMove() override;
+ void DoRender() override;
+ void AllocResources(bool forceLoad = false) override;
+ void FreeResources(bool forceUnLoad = false) override;
+
+protected:
+ unsigned int FindCurrentResolution();
+ void NextControl();
+ void ResetControls();
+ void EnableControl(int iControl);
+ bool UpdateFromControl(int iControl);
+ void ResetCalibration();
+ unsigned int m_iCurRes;
+ std::vector<RESOLUTION> m_Res;
+ int m_iControl;
+ float m_fPixelRatioBoxHeight;
+
+private:
+ std::map<int, std::pair<float, float>> m_controlsSize;
+ int m_subtitlesHalfSpace{0};
+ int m_subtitleVerticalMargin{0};
+ bool m_isSubtitleBarEnabled{false};
+};
diff --git a/xbmc/speech/CMakeLists.txt b/xbmc/speech/CMakeLists.txt
new file mode 100644
index 0000000..1c912dd
--- /dev/null
+++ b/xbmc/speech/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(HEADERS ISpeechRecognition.h
+ ISpeechRecognitionListener.h
+ SpeechRecognitionErrors.h)
+
+if(NOT ENABLE_STATIC_LIBS)
+ core_add_library(speech)
+endif()
diff --git a/xbmc/speech/ISpeechRecognition.h b/xbmc/speech/ISpeechRecognition.h
new file mode 100644
index 0000000..e4c4fcb
--- /dev/null
+++ b/xbmc/speech/ISpeechRecognition.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2005-2022 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 <memory>
+
+namespace speech
+{
+class ISpeechRecognitionListener;
+
+class ISpeechRecognition
+{
+public:
+ static std::shared_ptr<speech::ISpeechRecognition> CreateInstance();
+
+ virtual ~ISpeechRecognition() = default;
+
+ virtual void StartSpeechRecognition(
+ const std::shared_ptr<speech::ISpeechRecognitionListener>& listener) = 0;
+};
+
+} // namespace speech
diff --git a/xbmc/speech/ISpeechRecognitionListener.h b/xbmc/speech/ISpeechRecognitionListener.h
new file mode 100644
index 0000000..39ae0af
--- /dev/null
+++ b/xbmc/speech/ISpeechRecognitionListener.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2005-2022 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 <string>
+#include <vector>
+
+namespace speech
+{
+class ISpeechRecognitionListener
+{
+public:
+ virtual ~ISpeechRecognitionListener() = default;
+
+ virtual void OnReadyForSpeech() {}
+ virtual void OnError(int recognitionError) = 0;
+ virtual void OnResults(const std::vector<std::string>& results) = 0;
+};
+
+} // namespace speech
diff --git a/xbmc/speech/SpeechRecognitionErrors.h b/xbmc/speech/SpeechRecognitionErrors.h
new file mode 100644
index 0000000..140011c
--- /dev/null
+++ b/xbmc/speech/SpeechRecognitionErrors.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2005-2022 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
+
+namespace speech
+{
+namespace RecognitionError
+{
+static constexpr int UNKNOWN = 1;
+static constexpr int NETWORK_TIMEOUT = 2;
+static constexpr int NETWORK = 3;
+static constexpr int AUDIO = 4;
+static constexpr int SERVER = 5;
+static constexpr int CLIENT = 6;
+static constexpr int SPEECH_TIMEOUT = 7;
+static constexpr int NO_MATCH = 8;
+static constexpr int RECOGNIZER_BUSY = 9;
+static constexpr int INSUFFICIENT_PERMISSIONS = 10;
+static constexpr int SERVICE_NOT_AVAILABLE = 11;
+
+} // namespace RecognitionError
+} // namespace speech
diff --git a/xbmc/storage/AutorunMediaJob.cpp b/xbmc/storage/AutorunMediaJob.cpp
new file mode 100644
index 0000000..72e784f
--- /dev/null
+++ b/xbmc/storage/AutorunMediaJob.cpp
@@ -0,0 +1,75 @@
+/*
+ * 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 "AutorunMediaJob.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/builtins/Builtins.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+CAutorunMediaJob::CAutorunMediaJob(const std::string &label, const std::string &path):
+ m_path(path),
+ m_label(label)
+{
+}
+
+bool CAutorunMediaJob::DoWork()
+{
+ CGUIDialogSelect* pDialog= CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+
+ // wake up and turn off the screensaver if it's active
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->WakeUpScreenSaverAndDPMS();
+
+ pDialog->Reset();
+ if (!m_label.empty())
+ pDialog->SetHeading(CVariant{m_label});
+ else
+ pDialog->SetHeading(CVariant{g_localizeStrings.Get(21331)});
+
+ pDialog->Add(g_localizeStrings.Get(21332));
+ pDialog->Add(g_localizeStrings.Get(21333));
+ pDialog->Add(g_localizeStrings.Get(21334));
+ pDialog->Add(g_localizeStrings.Get(21335));
+
+ pDialog->Open();
+
+ int selection = pDialog->GetSelectedItem();
+ if (selection >= 0)
+ {
+ std::string strAction =
+ StringUtils::Format("ActivateWindow({}, {})", GetWindowString(selection), m_path);
+ CBuiltins::GetInstance().Execute(strAction);
+ }
+
+ return true;
+}
+
+const char *CAutorunMediaJob::GetWindowString(int selection)
+{
+ switch (selection)
+ {
+ case 0:
+ return "Videos";
+ case 1:
+ return "Music";
+ case 2:
+ return "Pictures";
+ case 3:
+ return "FileManager";
+ default:
+ return "FileManager";
+ }
+}
diff --git a/xbmc/storage/AutorunMediaJob.h b/xbmc/storage/AutorunMediaJob.h
new file mode 100644
index 0000000..52524f1
--- /dev/null
+++ b/xbmc/storage/AutorunMediaJob.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "utils/Job.h"
+
+#include <string>
+
+class CAutorunMediaJob : public CJob
+{
+public:
+ CAutorunMediaJob(const std::string &label, const std::string &path);
+
+ bool DoWork() override;
+private:
+ const char *GetWindowString(int selection);
+
+ std::string m_path, m_label;
+};
diff --git a/xbmc/storage/CMakeLists.txt b/xbmc/storage/CMakeLists.txt
new file mode 100644
index 0000000..eb282bb
--- /dev/null
+++ b/xbmc/storage/CMakeLists.txt
@@ -0,0 +1,16 @@
+set(SOURCES AutorunMediaJob.cpp
+ MediaManager.cpp)
+
+set(HEADERS AutorunMediaJob.h
+ IStorageProvider.h
+ MediaManager.h)
+
+if(ENABLE_OPTICAL)
+ list(APPEND SOURCES cdioSupport.cpp
+ DetectDVDType.cpp)
+ list(APPEND HEADERS cdioSupport.h
+ DetectDVDType.h
+ discs/IDiscDriveHandler.h)
+endif()
+
+core_add_library(storage)
diff --git a/xbmc/storage/DetectDVDType.cpp b/xbmc/storage/DetectDVDType.cpp
new file mode 100644
index 0000000..2392901
--- /dev/null
+++ b/xbmc/storage/DetectDVDType.cpp
@@ -0,0 +1,446 @@
+/*
+ * 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 "DetectDVDType.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cdioSupport.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace MEDIA_DETECT;
+using namespace std::chrono_literals;
+
+CCriticalSection CDetectDVDMedia::m_muReadingMedia;
+CEvent CDetectDVDMedia::m_evAutorun;
+DriveState CDetectDVDMedia::m_DriveState{DriveState::CLOSED_NO_MEDIA};
+CCdInfo* CDetectDVDMedia::m_pCdInfo = NULL;
+time_t CDetectDVDMedia::m_LastPoll = 0;
+CDetectDVDMedia* CDetectDVDMedia::m_pInstance = NULL;
+std::string CDetectDVDMedia::m_diskLabel = "";
+std::string CDetectDVDMedia::m_diskPath = "";
+UTILS::DISCS::DiscInfo CDetectDVDMedia::m_discInfo;
+
+CDetectDVDMedia::CDetectDVDMedia() : CThread("DetectDVDMedia"),
+ m_cdio(CLibcdio::GetInstance())
+{
+ m_bStop = false;
+ m_pInstance = this;
+}
+
+CDetectDVDMedia::~CDetectDVDMedia() = default;
+
+void CDetectDVDMedia::OnStartup()
+{
+ // SetPriority( ThreadPriority::LOWEST );
+ CLog::Log(LOGDEBUG, "Compiled with libcdio Version 0.{}", LIBCDIO_VERSION_NUM);
+}
+
+void CDetectDVDMedia::Process()
+{
+// for apple - currently disable this check since cdio will return null if no media is loaded
+#if !defined(TARGET_DARWIN)
+ //Before entering loop make sure we actually have a CDrom drive
+ CdIo_t *p_cdio = m_cdio->cdio_open(NULL, DRIVER_DEVICE);
+ if (p_cdio == NULL)
+ return;
+ else
+ m_cdio->cdio_destroy(p_cdio);
+#endif
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ while (!m_bStop)
+ {
+ if (appPlayer->IsPlayingVideo())
+ {
+ CThread::Sleep(10000ms);
+ }
+ else
+ {
+ UpdateDvdrom();
+ m_bStartup = false;
+ CThread::Sleep(2000ms);
+ if (m_bAutorun)
+ {
+ // Media in drive, wait 1.5s more to be sure the device is ready for playback
+ CThread::Sleep(1500ms);
+ m_evAutorun.Set();
+ m_bAutorun = false;
+ }
+ }
+ }
+}
+
+void CDetectDVDMedia::OnExit()
+{
+}
+
+// Gets state of the DVD drive
+void CDetectDVDMedia::UpdateDvdrom()
+{
+ // Signal for WaitMediaReady()
+ // that we are busy detecting the
+ // newly inserted media.
+ {
+ std::unique_lock<CCriticalSection> waitLock(m_muReadingMedia);
+ switch (PollDriveState())
+ {
+ case DriveState::NONE:
+ //! @todo reduce / stop polling for drive updates
+ break;
+
+ case DriveState::OPEN:
+ {
+ // Send Message to GUI that disc been ejected
+ SetNewDVDShareUrl(CServiceBroker::GetMediaManager().TranslateDevicePath(m_diskPath), false,
+ g_localizeStrings.Get(502));
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_REMOVED_MEDIA);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ // Clear all stored info
+ Clear();
+ // Update drive state
+ waitLock.unlock();
+ m_DriveState = DriveState::OPEN;
+ return;
+ }
+ break;
+ case DriveState::NOT_READY:
+ {
+ // Drive is not ready (closing, opening)
+ SetNewDVDShareUrl(CServiceBroker::GetMediaManager().TranslateDevicePath(m_diskPath), false,
+ g_localizeStrings.Get(503));
+ m_DriveState = DriveState::NOT_READY;
+ // DVD-ROM in undefined state
+ // Better delete old CD Information
+ if (m_pCdInfo != nullptr)
+ {
+ delete m_pCdInfo;
+ m_pCdInfo = nullptr;
+ }
+ waitLock.unlock();
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_SOURCES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ // Do we really need sleep here? This will fix: [ 1530771 ] "Open tray" problem
+ // CThread::Sleep(6000ms);
+ return;
+ }
+ break;
+
+ case DriveState::CLOSED_NO_MEDIA:
+ {
+ // Nothing in there...
+ SetNewDVDShareUrl(CServiceBroker::GetMediaManager().TranslateDevicePath(m_diskPath), false,
+ g_localizeStrings.Get(504));
+ m_DriveState = DriveState::CLOSED_NO_MEDIA;
+ // Send Message to GUI that disc has changed
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_SOURCES);
+ waitLock.unlock();
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ return;
+ }
+ break;
+ case DriveState::READY:
+#if !defined(TARGET_DARWIN)
+ return ;
+#endif
+ break;
+ case DriveState::CLOSED_MEDIA_PRESENT:
+ {
+ if (m_DriveState != DriveState::CLOSED_MEDIA_PRESENT)
+ {
+ m_DriveState = DriveState::CLOSED_MEDIA_PRESENT;
+ // Detect ISO9660(mode1/mode2) or CDDA filesystem
+ DetectMediaType();
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_SOURCES);
+ waitLock.unlock();
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ // Tell the application object that a new Cd is inserted
+ // So autorun can be started.
+ if (!m_bStartup)
+ m_bAutorun = true;
+ }
+ return;
+ }
+ default:
+ break;
+ }
+
+ // We have finished media detection
+ // Signal for WaitMediaReady()
+ }
+
+
+}
+
+// Generates the drive url, (like iso9660://)
+// from the CCdInfo class
+void CDetectDVDMedia::DetectMediaType()
+{
+ bool bCDDA(false);
+ CLog::Log(LOGINFO, "Detecting DVD-ROM media filesystem...");
+
+ // Probe and store DiscInfo result
+ // even if no valid tracks are detected we might still be able to play the disc via libdvdnav or libbluray
+ // as long as they can correctly detect the disc
+ UTILS::DISCS::DiscInfo discInfo;
+ if (UTILS::DISCS::GetDiscInfo(discInfo,
+ CServiceBroker::GetMediaManager().TranslateDevicePath(m_diskPath)))
+ {
+ m_discInfo = discInfo;
+ }
+
+ std::string strNewUrl;
+ CCdIoSupport cdio;
+
+ // Delete old CD-Information
+ if ( m_pCdInfo != NULL )
+ {
+ delete m_pCdInfo;
+ m_pCdInfo = NULL;
+ }
+
+ // Detect new CD-Information
+ m_pCdInfo = cdio.GetCdInfo();
+ if (m_pCdInfo == NULL)
+ {
+ CLog::Log(LOGERROR, "Detection of DVD-ROM media failed.");
+ return ;
+ }
+ CLog::Log(LOGINFO, "Tracks overall:{}; Audio tracks:{}; Data tracks:{}",
+ m_pCdInfo->GetTrackCount(), m_pCdInfo->GetAudioTrackCount(),
+ m_pCdInfo->GetDataTrackCount());
+
+ // Detect ISO9660(mode1/mode2), CDDA filesystem or UDF
+ if (m_pCdInfo->IsISOHFS(1) || m_pCdInfo->IsIso9660(1) || m_pCdInfo->IsIso9660Interactive(1))
+ {
+ strNewUrl = "iso9660://";
+ }
+ else
+ {
+ if (m_pCdInfo->IsUDF(1))
+ strNewUrl = CServiceBroker::GetMediaManager().TranslateDevicePath(m_diskPath);
+ else if (m_pCdInfo->IsAudio(1))
+ {
+ strNewUrl = "cdda://local/";
+ bCDDA = true;
+ }
+ else
+ strNewUrl = CServiceBroker::GetMediaManager().TranslateDevicePath(m_diskPath);
+ }
+
+ if (m_pCdInfo->IsISOUDF(1))
+ {
+ if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_detectAsUdf)
+ {
+ strNewUrl = "iso9660://";
+ }
+ else
+ {
+ strNewUrl = CServiceBroker::GetMediaManager().TranslateDevicePath(m_diskPath);
+ }
+ }
+
+ CLog::Log(LOGINFO, "Using protocol {}", strNewUrl);
+
+ if (m_pCdInfo->IsValidFs())
+ {
+ if (!m_pCdInfo->IsAudio(1))
+ CLog::Log(LOGINFO, "Disc label: {}", m_pCdInfo->GetDiscLabel());
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Filesystem is not supported");
+ }
+
+ std::string strLabel;
+ if (bCDDA)
+ {
+ strLabel = "Audio-CD";
+ }
+ else
+ {
+ strLabel = m_pCdInfo->GetDiscLabel();
+ StringUtils::TrimRight(strLabel);
+ }
+
+ SetNewDVDShareUrl( strNewUrl , bCDDA, strLabel);
+}
+
+void CDetectDVDMedia::SetNewDVDShareUrl( const std::string& strNewUrl, bool bCDDA, const std::string& strDiscLabel )
+{
+ std::string strDescription = "DVD";
+ if (bCDDA) strDescription = "CD";
+
+ if (strDiscLabel != "") strDescription = strDiscLabel;
+
+ // Store it in case others want it
+ m_diskLabel = strDescription;
+ m_diskPath = strNewUrl;
+}
+
+DriveState CDetectDVDMedia::PollDriveState()
+{
+ const std::shared_ptr<IDiscDriveHandler> platformDiscDriveHandler =
+ CServiceBroker::GetMediaManager().GetDiscDriveHandler();
+ if (!platformDiscDriveHandler)
+ {
+ return DriveState::NONE;
+ }
+
+ const std::string discPath = CServiceBroker::GetMediaManager().TranslateDevicePath("");
+ const DriveState driveState = platformDiscDriveHandler->GetDriveState(discPath);
+ switch (driveState)
+ {
+ case DriveState::CLOSED_MEDIA_UNDEFINED:
+ // We only poll for new traystatus when driveState has changed or if the last recorded
+ // tray state is undefined
+ if (driveState == DriveState::CLOSED_MEDIA_UNDEFINED &&
+ (m_LastTrayState == TrayState::UNDEFINED || driveState != m_LastDriveState))
+ {
+ m_TrayState = platformDiscDriveHandler->GetTrayState(discPath);
+ }
+ break;
+ case DriveState::OPEN:
+ m_TrayState = TrayState::OPEN;
+ break;
+ default:
+ m_TrayState = TrayState::UNDEFINED;
+ break;
+ }
+ m_LastDriveState = driveState;
+
+ if (m_TrayState == TrayState::CLOSED_MEDIA_PRESENT)
+ {
+ if (m_LastTrayState != TrayState::CLOSED_MEDIA_PRESENT)
+ {
+ m_LastTrayState = m_TrayState;
+ return DriveState::CLOSED_MEDIA_PRESENT;
+ }
+ else
+ {
+ return DriveState::READY;
+ }
+ }
+ else if (m_TrayState == TrayState::CLOSED_NO_MEDIA)
+ {
+ if ((m_LastTrayState != TrayState::CLOSED_NO_MEDIA) &&
+ (m_LastTrayState != TrayState::CLOSED_MEDIA_PRESENT))
+ {
+ m_LastTrayState = m_TrayState;
+ return DriveState::CLOSED_NO_MEDIA;
+ }
+ else
+ {
+ return DriveState::READY;
+ }
+ }
+ else if (m_TrayState == TrayState::OPEN)
+ {
+ if (m_LastTrayState != TrayState::OPEN)
+ {
+ m_LastTrayState = m_TrayState;
+ return DriveState::OPEN;
+ }
+ else
+ {
+ return DriveState::READY;
+ }
+ }
+ else
+ {
+ m_LastTrayState = m_TrayState;
+ }
+
+#ifdef HAS_DVD_DRIVE
+ return DriveState::NOT_READY;
+#else
+ return DriveState::READY;
+#endif
+}
+
+void CDetectDVDMedia::UpdateState()
+{
+ std::unique_lock<CCriticalSection> waitLock(m_muReadingMedia);
+ m_pInstance->DetectMediaType();
+}
+
+// Static function
+// Wait for drive, to finish media detection.
+void CDetectDVDMedia::WaitMediaReady()
+{
+ std::unique_lock<CCriticalSection> waitLock(m_muReadingMedia);
+}
+
+// Static function
+// Returns status of the DVD Drive
+bool CDetectDVDMedia::DriveReady()
+{
+ return m_DriveState == DriveState::READY;
+}
+
+DriveState CDetectDVDMedia::GetDriveState()
+{
+ return m_DriveState;
+}
+
+// Static function
+// Whether a disc is in drive
+bool CDetectDVDMedia::IsDiscInDrive()
+{
+ return m_DriveState == DriveState::CLOSED_MEDIA_PRESENT;
+}
+
+// Static function
+// Returns a CCdInfo class, which contains
+// Media information of the current inserted CD.
+// Can be NULL
+CCdInfo* CDetectDVDMedia::GetCdInfo()
+{
+ std::unique_lock<CCriticalSection> waitLock(m_muReadingMedia);
+ CCdInfo* pCdInfo = m_pCdInfo;
+ return pCdInfo;
+}
+
+const std::string &CDetectDVDMedia::GetDVDLabel()
+{
+ if (!m_discInfo.empty())
+ {
+ return m_discInfo.name;
+ }
+
+ return m_diskLabel;
+}
+
+const std::string &CDetectDVDMedia::GetDVDPath()
+{
+ return m_diskPath;
+}
+
+
+void CDetectDVDMedia::Clear()
+{
+ if (!m_discInfo.empty())
+ {
+ m_discInfo.clear();
+ }
+ m_diskLabel.clear();
+ m_diskPath.clear();
+}
diff --git a/xbmc/storage/DetectDVDType.h b/xbmc/storage/DetectDVDType.h
new file mode 100644
index 0000000..c54272f
--- /dev/null
+++ b/xbmc/storage/DetectDVDType.h
@@ -0,0 +1,88 @@
+/*
+ * 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
+
+// CDetectDVDMedia
+// Thread running in the background to detect a CD change and the filesystem
+//
+// by Bobbin007 in 2003
+
+#include "PlatformDefs.h"
+
+#ifdef HAS_DVD_DRIVE
+
+#include "storage/discs/IDiscDriveHandler.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+#include "utils/DiscsUtils.h"
+
+#include <memory>
+#include <string>
+
+namespace MEDIA_DETECT
+{
+class CCdInfo;
+class CLibcdio;
+
+class CDetectDVDMedia : public CThread
+{
+public:
+ CDetectDVDMedia();
+ ~CDetectDVDMedia() override;
+
+ void OnStartup() override;
+ void OnExit() override;
+ void Process() override;
+
+ static void WaitMediaReady();
+ static bool IsDiscInDrive();
+ static bool DriveReady();
+ static DriveState GetDriveState();
+ static CCdInfo* GetCdInfo();
+ static CEvent m_evAutorun;
+
+ static const std::string &GetDVDLabel();
+ static const std::string &GetDVDPath();
+
+ static void UpdateState();
+protected:
+ void UpdateDvdrom();
+ DriveState PollDriveState();
+
+
+ void DetectMediaType();
+ void SetNewDVDShareUrl( const std::string& strNewUrl, bool bCDDA, const std::string& strDiscLabel );
+
+ void Clear();
+
+private:
+ static CCriticalSection m_muReadingMedia;
+
+ static DriveState m_DriveState;
+ static time_t m_LastPoll;
+ static CDetectDVDMedia* m_pInstance;
+
+ static CCdInfo* m_pCdInfo;
+
+ bool m_bStartup = true; // Do not autorun on startup
+ bool m_bAutorun = false;
+ TrayState m_TrayState{TrayState::UNDEFINED};
+ TrayState m_LastTrayState{TrayState::UNDEFINED};
+ DriveState m_LastDriveState{DriveState::NONE};
+
+ static std::string m_diskLabel;
+ static std::string m_diskPath;
+
+ std::shared_ptr<CLibcdio> m_cdio;
+
+ /*! \brief Stores the DiscInfo of the current disk */
+ static UTILS::DISCS::DiscInfo m_discInfo;
+};
+}
+#endif
diff --git a/xbmc/storage/IStorageProvider.h b/xbmc/storage/IStorageProvider.h
new file mode 100644
index 0000000..aa9ecaf
--- /dev/null
+++ b/xbmc/storage/IStorageProvider.h
@@ -0,0 +1,95 @@
+/*
+ * 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 "MediaSource.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+#ifdef HAS_DVD_DRIVE
+#include "cdioSupport.h"
+#endif
+
+namespace MEDIA_DETECT
+{
+namespace STORAGE
+{
+/*! \brief Abstracts a generic storage device type*/
+enum class Type
+{
+ UNKNOWN, /*!< the storage type is unknown */
+ OPTICAL /*!< an optical device (e.g. DVD or Bluray) */
+};
+
+/*! \brief Abstracts a generic storage device */
+struct StorageDevice
+{
+ /*! Device name/label */
+ std::string label{};
+ /*! Device mountpoint/path */
+ std::string path{};
+ /*! The storage type (e.g. OPTICAL) */
+ STORAGE::Type type{STORAGE::Type::UNKNOWN};
+};
+} // namespace STORAGE
+} // namespace MEDIA_DETECT
+
+class IStorageEventsCallback
+{
+public:
+ virtual ~IStorageEventsCallback() = default;
+
+ /*! \brief Callback executed when a new storage device is added
+ * @param device the storage device
+ */
+ virtual void OnStorageAdded(const MEDIA_DETECT::STORAGE::StorageDevice& device) = 0;
+
+ /*! \brief Callback executed when a new storage device is safely removed
+ * @param device the storage device
+ */
+ virtual void OnStorageSafelyRemoved(const MEDIA_DETECT::STORAGE::StorageDevice& device) = 0;
+
+ /*! \brief Callback executed when a new storage device is unsafely removed
+ * @param device the storage device
+ */
+ virtual void OnStorageUnsafelyRemoved(const MEDIA_DETECT::STORAGE::StorageDevice& device) = 0;
+};
+
+class IStorageProvider
+{
+public:
+ virtual ~IStorageProvider() = default;
+
+ virtual void Initialize() = 0;
+ virtual void Stop() = 0;
+
+ virtual void GetLocalDrives(VECSOURCES &localDrives) = 0;
+ virtual void GetRemovableDrives(VECSOURCES &removableDrives) = 0;
+ virtual std::string GetFirstOpticalDeviceFileName()
+ {
+#ifdef HAS_DVD_DRIVE
+ return std::string(MEDIA_DETECT::CLibcdio::GetInstance()->GetDeviceFileName());
+#else
+ return "";
+#endif
+ }
+
+ virtual bool Eject(const std::string& mountpath) = 0;
+
+ virtual std::vector<std::string> GetDiskUsage() = 0;
+
+ virtual bool PumpDriveChangeEvents(IStorageEventsCallback *callback) = 0;
+
+ /**\brief Called by media manager to create platform storage provider
+ *
+ * This method used to create platform specified storage provider
+ */
+ static std::unique_ptr<IStorageProvider> CreateInstance();
+};
diff --git a/xbmc/storage/MediaManager.cpp b/xbmc/storage/MediaManager.cpp
new file mode 100644
index 0000000..a1efbd9
--- /dev/null
+++ b/xbmc/storage/MediaManager.cpp
@@ -0,0 +1,821 @@
+/*
+ * 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 "MediaManager.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/URIUtils.h"
+
+#include <mutex>
+#ifdef TARGET_WINDOWS
+#include "platform/win32/WIN32Util.h"
+#include "utils/CharsetConverter.h"
+#endif
+#include "guilib/GUIWindowManager.h"
+#ifdef HAS_DVD_DRIVE
+#ifndef TARGET_WINDOWS
+//! @todo switch all ports to use auto sources
+#include <map>
+#include <utility>
+#include "DetectDVDType.h"
+#endif
+#endif
+#include "Autorun.h"
+#include "AutorunMediaJob.h"
+#include "GUIUserMessages.h"
+#include "addons/VFSEntry.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogPlayEject.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <string>
+#include <vector>
+
+#ifdef HAS_DVD_DRIVE
+using namespace MEDIA_DETECT;
+#endif
+
+const char MEDIA_SOURCES_XML[] = { "special://profile/mediasources.xml" };
+
+CMediaManager::CMediaManager()
+{
+ m_bhasoptical = false;
+}
+
+void CMediaManager::Stop()
+{
+ if (m_platformStorage)
+ m_platformStorage->Stop();
+
+ m_platformStorage.reset();
+}
+
+void CMediaManager::Initialize()
+{
+ if (!m_platformStorage)
+ {
+ m_platformStorage = IStorageProvider::CreateInstance();
+ }
+#ifdef HAS_DVD_DRIVE
+ m_platformDiscDriveHander = IDiscDriveHandler::CreateInstance();
+ m_strFirstAvailDrive = m_platformStorage->GetFirstOpticalDeviceFileName();
+#endif
+ m_platformStorage->Initialize();
+}
+
+bool CMediaManager::LoadSources()
+{
+ // clear our location list
+ m_locations.clear();
+
+ // load xml file...
+ CXBMCTinyXML xmlDoc;
+ if ( !xmlDoc.LoadFile( MEDIA_SOURCES_XML ) )
+ return false;
+
+ TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (!pRootElement || StringUtils::CompareNoCase(pRootElement->Value(), "mediasources") != 0)
+ {
+ CLog::Log(LOGERROR, "Error loading {}, Line {} ({})", MEDIA_SOURCES_XML, xmlDoc.ErrorRow(),
+ xmlDoc.ErrorDesc());
+ return false;
+ }
+
+ // load the <network> block
+ TiXmlNode *pNetwork = pRootElement->FirstChild("network");
+ if (pNetwork)
+ {
+ TiXmlElement *pLocation = pNetwork->FirstChildElement("location");
+ while (pLocation)
+ {
+ CNetworkLocation location;
+ pLocation->Attribute("id", &location.id);
+ if (pLocation->FirstChild())
+ {
+ location.path = pLocation->FirstChild()->Value();
+ m_locations.push_back(location);
+ }
+ pLocation = pLocation->NextSiblingElement("location");
+ }
+ }
+ LoadAddonSources();
+ return true;
+}
+
+bool CMediaManager::SaveSources()
+{
+ CXBMCTinyXML xmlDoc;
+ TiXmlElement xmlRootElement("mediasources");
+ TiXmlNode *pRoot = xmlDoc.InsertEndChild(xmlRootElement);
+ if (!pRoot) return false;
+
+ TiXmlElement networkNode("network");
+ TiXmlNode *pNetworkNode = pRoot->InsertEndChild(networkNode);
+ if (pNetworkNode)
+ {
+ for (std::vector<CNetworkLocation>::iterator it = m_locations.begin(); it != m_locations.end(); ++it)
+ {
+ TiXmlElement locationNode("location");
+ locationNode.SetAttribute("id", (*it).id);
+ TiXmlText value((*it).path);
+ locationNode.InsertEndChild(value);
+ pNetworkNode->InsertEndChild(locationNode);
+ }
+ }
+ return xmlDoc.SaveFile(MEDIA_SOURCES_XML);
+}
+
+void CMediaManager::GetLocalDrives(VECSOURCES &localDrives, bool includeQ)
+{
+ std::unique_lock<CCriticalSection> lock(m_CritSecStorageProvider);
+ m_platformStorage->GetLocalDrives(localDrives);
+}
+
+void CMediaManager::GetRemovableDrives(VECSOURCES &removableDrives)
+{
+ std::unique_lock<CCriticalSection> lock(m_CritSecStorageProvider);
+ if (m_platformStorage)
+ m_platformStorage->GetRemovableDrives(removableDrives);
+}
+
+void CMediaManager::GetNetworkLocations(VECSOURCES &locations, bool autolocations)
+{
+ for (unsigned int i = 0; i < m_locations.size(); i++)
+ {
+ CMediaSource share;
+ share.strPath = m_locations[i].path;
+ CURL url(share.strPath);
+ share.strName = url.GetWithoutUserDetails();
+ locations.push_back(share);
+ }
+ if (autolocations)
+ {
+ CMediaSource share;
+ share.m_ignore = true;
+#ifdef HAS_FILESYSTEM_SMB
+ share.strPath = "smb://";
+ share.strName = g_localizeStrings.Get(20171);
+ locations.push_back(share);
+#endif
+
+#ifdef HAS_FILESYSTEM_NFS
+ share.strPath = "nfs://";
+ share.strName = g_localizeStrings.Get(20259);
+ locations.push_back(share);
+#endif// HAS_FILESYSTEM_NFS
+
+#ifdef HAS_UPNP
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SERVICES_UPNP))
+ {
+ const std::string& strDevices = g_localizeStrings.Get(33040); //"% Devices"
+ share.strPath = "upnp://";
+ share.strName = StringUtils::Format(strDevices, "UPnP"); //"UPnP Devices"
+ locations.push_back(share);
+ }
+#endif
+
+#ifdef HAS_ZEROCONF
+ share.strPath = "zeroconf://";
+ share.strName = g_localizeStrings.Get(20262);
+ locations.push_back(share);
+#endif
+
+ if (CServiceBroker::IsAddonInterfaceUp())
+ {
+ for (const auto& addon : CServiceBroker::GetVFSAddonCache().GetAddonInstances())
+ {
+ const auto& info = addon->GetProtocolInfo();
+ if (!info.type.empty() && info.supportBrowsing)
+ {
+ share.strPath = info.type + "://";
+ share.strName = g_localizeStrings.GetAddonString(addon->ID(), info.label);
+ if (share.strName.empty())
+ share.strName = g_localizeStrings.Get(info.label);
+ locations.push_back(share);
+ }
+ }
+ }
+ }
+}
+
+bool CMediaManager::AddNetworkLocation(const std::string &path)
+{
+ CNetworkLocation location;
+ location.path = path;
+ location.id = (int)m_locations.size();
+ m_locations.push_back(location);
+ return SaveSources();
+}
+
+bool CMediaManager::HasLocation(const std::string& path) const
+{
+ for (unsigned int i=0;i<m_locations.size();++i)
+ {
+ if (URIUtils::CompareWithoutSlashAtEnd(m_locations[i].path, path))
+ return true;
+ }
+
+ return false;
+}
+
+
+bool CMediaManager::RemoveLocation(const std::string& path)
+{
+ for (unsigned int i=0;i<m_locations.size();++i)
+ {
+ if (URIUtils::CompareWithoutSlashAtEnd(m_locations[i].path, path))
+ {
+ // prompt for sources, remove, cancel,
+ m_locations.erase(m_locations.begin()+i);
+ return SaveSources();
+ }
+ }
+
+ return false;
+}
+
+bool CMediaManager::SetLocationPath(const std::string& oldPath, const std::string& newPath)
+{
+ for (unsigned int i=0;i<m_locations.size();++i)
+ {
+ if (URIUtils::CompareWithoutSlashAtEnd(m_locations[i].path, oldPath))
+ {
+ m_locations[i].path = newPath;
+ return SaveSources();
+ }
+ }
+
+ return false;
+}
+
+void CMediaManager::LoadAddonSources() const
+{
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVirtualShares)
+ {
+ CMediaSourceSettings::GetInstance().AddShare("video", GetRootAddonTypeSource("video"));
+ CMediaSourceSettings::GetInstance().AddShare("programs", GetRootAddonTypeSource("programs"));
+ CMediaSourceSettings::GetInstance().AddShare("pictures", GetRootAddonTypeSource("pictures"));
+ CMediaSourceSettings::GetInstance().AddShare("music", GetRootAddonTypeSource("music"));
+ CMediaSourceSettings::GetInstance().AddShare("games", GetRootAddonTypeSource("games"));
+ }
+}
+
+CMediaSource CMediaManager::GetRootAddonTypeSource(const std::string& type) const
+{
+ if (type == "programs" || type == "myprograms")
+ {
+ return ComputeRootAddonTypeSource("executable", g_localizeStrings.Get(1043),
+ "DefaultAddonProgram.png");
+ }
+ else if (type == "video" || type == "videos")
+ {
+ return ComputeRootAddonTypeSource("video", g_localizeStrings.Get(1037),
+ "DefaultAddonVideo.png");
+ }
+ else if (type == "music")
+ {
+ return ComputeRootAddonTypeSource("audio", g_localizeStrings.Get(1038),
+ "DefaultAddonMusic.png");
+ }
+ else if (type == "pictures")
+ {
+ return ComputeRootAddonTypeSource("image", g_localizeStrings.Get(1039),
+ "DefaultAddonPicture.png");
+ }
+ else if (type == "games")
+ {
+ return ComputeRootAddonTypeSource("game", g_localizeStrings.Get(35049), "DefaultAddonGame.png");
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Invalid type {} provided", type);
+ return {};
+ }
+}
+
+CMediaSource CMediaManager::ComputeRootAddonTypeSource(const std::string& type,
+ const std::string& label,
+ const std::string& thumb) const
+{
+ CMediaSource source;
+ source.strPath = "addons://sources/" + type + "/";
+ source.strName = label;
+ source.m_strThumbnailImage = thumb;
+ source.m_iDriveType = CMediaSource::SOURCE_TYPE_VPATH;
+ source.m_ignore = true;
+ return source;
+}
+
+void CMediaManager::AddAutoSource(const CMediaSource &share, bool bAutorun)
+{
+ CMediaSourceSettings::GetInstance().AddShare("files", share);
+ CMediaSourceSettings::GetInstance().AddShare("video", share);
+ CMediaSourceSettings::GetInstance().AddShare("pictures", share);
+ CMediaSourceSettings::GetInstance().AddShare("music", share);
+ CMediaSourceSettings::GetInstance().AddShare("programs", share);
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_SOURCES);
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetWindowManager().SendThreadMessage( msg );
+
+#ifdef HAS_DVD_DRIVE
+ if(bAutorun)
+ MEDIA_DETECT::CAutorun::ExecuteAutorun(share.strPath);
+#endif
+}
+
+void CMediaManager::RemoveAutoSource(const CMediaSource &share)
+{
+ CMediaSourceSettings::GetInstance().DeleteSource("files", share.strName, share.strPath, true);
+ CMediaSourceSettings::GetInstance().DeleteSource("video", share.strName, share.strPath, true);
+ CMediaSourceSettings::GetInstance().DeleteSource("pictures", share.strName, share.strPath, true);
+ CMediaSourceSettings::GetInstance().DeleteSource("music", share.strName, share.strPath, true);
+ CMediaSourceSettings::GetInstance().DeleteSource("programs", share.strName, share.strPath, true);
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_SOURCES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage( msg );
+
+#ifdef HAS_DVD_DRIVE
+ // delete cached CdInfo if any
+ RemoveCdInfo(TranslateDevicePath(share.strPath, true));
+ RemoveDiscInfo(TranslateDevicePath(share.strPath, true));
+#endif
+}
+
+/////////////////////////////////////////////////////////////
+// AutoSource status functions:
+//! @todo translate cdda://<device>/
+
+std::string CMediaManager::TranslateDevicePath(const std::string& devicePath, bool bReturnAsDevice)
+{
+ std::unique_lock<CCriticalSection> waitLock(m_muAutoSource);
+ std::string strDevice = devicePath;
+ // fallback for cdda://local/ and empty devicePath
+#ifdef HAS_DVD_DRIVE
+ if(devicePath.empty() || StringUtils::StartsWith(devicePath, "cdda://local"))
+ strDevice = m_strFirstAvailDrive;
+#endif
+
+#ifdef TARGET_WINDOWS
+ if(!m_bhasoptical)
+ return "";
+
+ if(bReturnAsDevice == false)
+ StringUtils::Replace(strDevice, "\\\\.\\","");
+ else if(!strDevice.empty() && strDevice[1]==':')
+ strDevice = StringUtils::Format("\\\\.\\{}:", strDevice[0]);
+
+ URIUtils::RemoveSlashAtEnd(strDevice);
+#endif
+ return strDevice;
+}
+
+bool CMediaManager::IsDiscInDrive(const std::string& devicePath)
+{
+#ifdef HAS_DVD_DRIVE
+#ifdef TARGET_WINDOWS
+ if(!m_bhasoptical)
+ return false;
+
+ std::string strDevice = TranslateDevicePath(devicePath, false);
+ std::map<std::string,CCdInfo*>::iterator it;
+ std::unique_lock<CCriticalSection> waitLock(m_muAutoSource);
+ it = m_mapCdInfo.find(strDevice);
+ if(it != m_mapCdInfo.end())
+ return true;
+ else
+ return false;
+#else
+ if(URIUtils::IsDVD(devicePath) || devicePath.empty())
+ return MEDIA_DETECT::CDetectDVDMedia::IsDiscInDrive(); //! @todo switch all ports to use auto sources
+ else
+ return true; // Assume other paths to be mounted already
+#endif
+#else
+ return false;
+#endif
+}
+
+bool CMediaManager::IsAudio(const std::string& devicePath)
+{
+#ifdef HAS_DVD_DRIVE
+#ifdef TARGET_WINDOWS
+ if(!m_bhasoptical)
+ return false;
+
+ CCdInfo* pCdInfo = GetCdInfo(devicePath);
+ if(pCdInfo != NULL && pCdInfo->IsAudio(1))
+ return true;
+
+ return false;
+#else
+ //! @todo switch all ports to use auto sources
+ MEDIA_DETECT::CCdInfo* pInfo = MEDIA_DETECT::CDetectDVDMedia::GetCdInfo();
+ if (pInfo != NULL && pInfo->IsAudio(1))
+ return true;
+#endif
+#endif
+ return false;
+}
+
+bool CMediaManager::HasOpticalDrive()
+{
+#ifdef HAS_DVD_DRIVE
+ if (!m_strFirstAvailDrive.empty())
+ return true;
+#endif
+ return false;
+}
+
+DriveState CMediaManager::GetDriveStatus(const std::string& devicePath)
+{
+#ifdef HAS_DVD_DRIVE
+#ifdef TARGET_WINDOWS
+ if (!m_bhasoptical || !m_platformDiscDriveHander)
+ return DriveState::NOT_READY;
+
+ std::string translatedDevicePath = TranslateDevicePath(devicePath, true);
+ return m_platformDiscDriveHander->GetDriveState(translatedDevicePath);
+#else
+ return MEDIA_DETECT::CDetectDVDMedia::GetDriveState();
+#endif
+#else
+ return DriveState::NOT_READY;
+#endif
+}
+
+#ifdef HAS_DVD_DRIVE
+CCdInfo* CMediaManager::GetCdInfo(const std::string& devicePath)
+{
+#ifdef TARGET_WINDOWS
+ if(!m_bhasoptical)
+ return NULL;
+
+ std::string strDevice = TranslateDevicePath(devicePath, false);
+ std::map<std::string,CCdInfo*>::iterator it;
+ {
+ std::unique_lock<CCriticalSection> waitLock(m_muAutoSource);
+ it = m_mapCdInfo.find(strDevice);
+ if(it != m_mapCdInfo.end())
+ return it->second;
+ }
+
+ CCdInfo* pCdInfo=NULL;
+ CCdIoSupport cdio;
+ pCdInfo = cdio.GetCdInfo((char*)strDevice.c_str());
+ if(pCdInfo!=NULL)
+ {
+ std::unique_lock<CCriticalSection> waitLock(m_muAutoSource);
+ m_mapCdInfo.insert(std::pair<std::string,CCdInfo*>(strDevice,pCdInfo));
+ }
+
+ return pCdInfo;
+#else
+ return MEDIA_DETECT::CDetectDVDMedia::GetCdInfo();
+#endif
+}
+
+bool CMediaManager::RemoveCdInfo(const std::string& devicePath)
+{
+ if(!m_bhasoptical)
+ return false;
+
+ std::string strDevice = TranslateDevicePath(devicePath, false);
+
+ std::map<std::string,CCdInfo*>::iterator it;
+ std::unique_lock<CCriticalSection> waitLock(m_muAutoSource);
+ it = m_mapCdInfo.find(strDevice);
+ if(it != m_mapCdInfo.end())
+ {
+ if(it->second != NULL)
+ delete it->second;
+
+ m_mapCdInfo.erase(it);
+ return true;
+ }
+ return false;
+}
+
+std::string CMediaManager::GetDiskLabel(const std::string& devicePath)
+{
+#ifdef TARGET_WINDOWS_STORE
+ return ""; // GetVolumeInformationW nut support in UWP app
+#elif defined(TARGET_WINDOWS)
+ if(!m_bhasoptical)
+ return "";
+
+ std::string mediaPath = CServiceBroker::GetMediaManager().TranslateDevicePath(devicePath);
+
+ auto cached = m_mapDiscInfo.find(mediaPath);
+ if (cached != m_mapDiscInfo.end())
+ return cached->second.name;
+
+ // try to minimize the chance of a "device not ready" dialog
+ std::string drivePath = CServiceBroker::GetMediaManager().TranslateDevicePath(devicePath, true);
+ if (CServiceBroker::GetMediaManager().GetDriveStatus(drivePath) !=
+ DriveState::CLOSED_MEDIA_PRESENT)
+ return "";
+
+ UTILS::DISCS::DiscInfo info;
+ info = GetDiscInfo(mediaPath);
+ if (!info.name.empty())
+ {
+ m_mapDiscInfo[mediaPath] = info;
+ return info.name;
+ }
+
+ std::string strDevice = TranslateDevicePath(devicePath);
+ WCHAR cVolumenName[128];
+ WCHAR cFSName[128];
+ URIUtils::AddSlashAtEnd(strDevice);
+ std::wstring strDeviceW;
+ g_charsetConverter.utf8ToW(strDevice, strDeviceW);
+ if(GetVolumeInformationW(strDeviceW.c_str(), cVolumenName, 127, NULL, NULL, NULL, cFSName, 127)==0)
+ return "";
+ g_charsetConverter.wToUTF8(cVolumenName, strDevice);
+ info.name = StringUtils::TrimRight(strDevice, " ");
+ if (!info.name.empty())
+ m_mapDiscInfo[mediaPath] = info;
+
+ return info.name;
+#else
+ return MEDIA_DETECT::CDetectDVDMedia::GetDVDLabel();
+#endif
+}
+
+std::string CMediaManager::GetDiskUniqueId(const std::string& devicePath)
+{
+ std::string mediaPath;
+
+ CCdInfo* pInfo = CServiceBroker::GetMediaManager().GetCdInfo(devicePath);
+ if (pInfo == NULL)
+ return "";
+
+ if (pInfo->IsAudio(1))
+ mediaPath = "cdda://local/";
+
+ if (mediaPath.empty() && (pInfo->IsISOUDF(1) || pInfo->IsISOHFS(1) || pInfo->IsIso9660(1) || pInfo->IsIso9660Interactive(1)))
+ mediaPath = "iso9660://";
+
+ if (mediaPath.empty())
+ mediaPath = devicePath;
+
+#ifdef TARGET_WINDOWS
+ if (mediaPath.empty() || mediaPath == "iso9660://")
+ {
+ mediaPath = CServiceBroker::GetMediaManager().TranslateDevicePath(devicePath);
+ }
+#endif
+
+ UTILS::DISCS::DiscInfo info = GetDiscInfo(mediaPath);
+ if (info.empty())
+ {
+ CLog::Log(LOGDEBUG, "GetDiskUniqueId: Retrieving ID for path {} failed, ID is empty.",
+ CURL::GetRedacted(mediaPath));
+ return "";
+ }
+
+ std::string strID = StringUtils::Format("removable://{}_{}", info.name, info.serial);
+ CLog::Log(LOGDEBUG, "GetDiskUniqueId: Got ID {} for disc with path {}", strID,
+ CURL::GetRedacted(mediaPath));
+
+ return strID;
+}
+
+std::string CMediaManager::GetDiscPath()
+{
+#ifdef TARGET_WINDOWS
+ return CServiceBroker::GetMediaManager().TranslateDevicePath("");
+#else
+
+ std::unique_lock<CCriticalSection> lock(m_CritSecStorageProvider);
+ VECSOURCES drives;
+ m_platformStorage->GetRemovableDrives(drives);
+ for(unsigned i = 0; i < drives.size(); ++i)
+ {
+ if(drives[i].m_iDriveType == CMediaSource::SOURCE_TYPE_DVD && !drives[i].strPath.empty())
+ return drives[i].strPath;
+ }
+
+ // iso9660://, cdda://local/ or D:\ depending on disc type
+ return MEDIA_DETECT::CDetectDVDMedia::GetDVDPath();
+#endif
+}
+
+std::shared_ptr<IDiscDriveHandler> CMediaManager::GetDiscDriveHandler()
+{
+ return m_platformDiscDriveHander;
+}
+#endif
+
+void CMediaManager::SetHasOpticalDrive(bool bstatus)
+{
+ std::unique_lock<CCriticalSection> waitLock(m_muAutoSource);
+ m_bhasoptical = bstatus;
+}
+
+bool CMediaManager::Eject(const std::string& mountpath)
+{
+ std::unique_lock<CCriticalSection> lock(m_CritSecStorageProvider);
+ return m_platformStorage->Eject(mountpath);
+}
+
+void CMediaManager::EjectTray( const bool bEject, const char cDriveLetter )
+{
+#ifdef HAS_DVD_DRIVE
+ if (m_platformDiscDriveHander)
+ {
+ m_platformDiscDriveHander->EjectDriveTray(TranslateDevicePath(""));
+ }
+#endif
+}
+
+void CMediaManager::CloseTray(const char cDriveLetter)
+{
+#ifdef HAS_DVD_DRIVE
+ if (m_platformDiscDriveHander)
+ {
+ m_platformDiscDriveHander->ToggleDriveTray(TranslateDevicePath(""));
+ }
+#endif
+}
+
+void CMediaManager::ToggleTray(const char cDriveLetter)
+{
+#ifdef HAS_DVD_DRIVE
+ if (m_platformDiscDriveHander)
+ {
+ m_platformDiscDriveHander->ToggleDriveTray(TranslateDevicePath(""));
+ }
+#endif
+}
+
+void CMediaManager::ProcessEvents()
+{
+ std::unique_lock<CCriticalSection> lock(m_CritSecStorageProvider);
+ if (m_platformStorage->PumpDriveChangeEvents(this))
+ {
+#if defined(HAS_DVD_DRIVE) && defined(TARGET_DARWIN_OSX)
+ // darwins GetFirstOpticalDeviceFileName only gives us something
+ // when a disc is inserted
+ // so we have to refresh m_strFirstAvailDrive when this happens after Initialize
+ // was called (e.x. the disc was inserted after the start of xbmc)
+ // else TranslateDevicePath wouldn't give the correct device
+ m_strFirstAvailDrive = m_platformStorage->GetFirstOpticalDeviceFileName();
+#endif
+
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_UPDATE_SOURCES);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+}
+
+std::vector<std::string> CMediaManager::GetDiskUsage()
+{
+ std::unique_lock<CCriticalSection> lock(m_CritSecStorageProvider);
+ return m_platformStorage->GetDiskUsage();
+}
+
+void CMediaManager::OnStorageAdded(const MEDIA_DETECT::STORAGE::StorageDevice& device)
+{
+#ifdef HAS_DVD_DRIVE
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetInt(CSettings::SETTING_AUDIOCDS_AUTOACTION) != AUTOCD_NONE || settings->GetBool(CSettings::SETTING_DVDS_AUTORUN))
+ {
+ if (settings->GetInt(CSettings::SETTING_AUDIOCDS_AUTOACTION) == AUTOCD_RIP)
+ {
+ CServiceBroker::GetJobManager()->AddJob(new CAutorunMediaJob(device.label, device.path), this,
+ CJob::PRIORITY_LOW);
+ }
+ else
+ {
+ if (device.type == MEDIA_DETECT::STORAGE::Type::OPTICAL)
+ {
+ if (MEDIA_DETECT::CAutorun::ExecuteAutorun(device.path))
+ {
+ return;
+ }
+ CLog::Log(LOGDEBUG, "{}: Could not execute autorun for optical disc with path {}",
+ __FUNCTION__, device.path);
+ }
+ CServiceBroker::GetJobManager()->AddJob(new CAutorunMediaJob(device.label, device.path), this,
+ CJob::PRIORITY_HIGH);
+ }
+ }
+ else
+ {
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(13021),
+ device.label, TOAST_DISPLAY_TIME, false);
+ }
+#endif
+}
+
+void CMediaManager::OnStorageSafelyRemoved(const MEDIA_DETECT::STORAGE::StorageDevice& device)
+{
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(13023),
+ device.label, TOAST_DISPLAY_TIME, false);
+}
+
+void CMediaManager::OnStorageUnsafelyRemoved(const MEDIA_DETECT::STORAGE::StorageDevice& device)
+{
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning, g_localizeStrings.Get(13022),
+ device.label);
+}
+
+UTILS::DISCS::DiscInfo CMediaManager::GetDiscInfo(const std::string& mediaPath)
+{
+ UTILS::DISCS::DiscInfo info;
+
+ if (mediaPath.empty())
+ return info;
+
+ // Try finding VIDEO_TS/VIDEO_TS.IFO - this indicates a DVD disc is inserted
+ std::string pathVideoTS = URIUtils::AddFileToFolder(mediaPath, "VIDEO_TS", "VIDEO_TS.IFO");
+ // correct the filename if needed
+ if (StringUtils::StartsWith(mediaPath, "dvd://") ||
+ StringUtils::StartsWith(mediaPath, "iso9660://"))
+ {
+ pathVideoTS = TranslateDevicePath("");
+ }
+
+ // check for DVD discs
+ if (CFileUtils::Exists(pathVideoTS))
+ {
+ info = UTILS::DISCS::ProbeDVDDiscInfo(pathVideoTS);
+ if (!info.empty())
+ return info;
+ }
+ // check for Blu-ray discs
+ if (CFileUtils::Exists(URIUtils::AddFileToFolder(mediaPath, "BDMV", "index.bdmv")))
+ {
+ info = UTILS::DISCS::ProbeBlurayDiscInfo(mediaPath);
+ }
+
+ return info;
+}
+
+void CMediaManager::RemoveDiscInfo(const std::string& devicePath)
+{
+ std::string strDevice = TranslateDevicePath(devicePath, false);
+
+ auto it = m_mapDiscInfo.find(strDevice);
+ if (it != m_mapDiscInfo.end())
+ m_mapDiscInfo.erase(it);
+}
+
+bool CMediaManager::playStubFile(const CFileItem& item)
+{
+ // Figure out Lines 1 and 2 of the dialog
+ std::string strLine1, strLine2;
+
+ // use generic message by default
+ strLine1 = g_localizeStrings.Get(435).c_str();
+ strLine2 = g_localizeStrings.Get(436).c_str();
+
+ CXBMCTinyXML discStubXML;
+ if (discStubXML.LoadFile(item.GetPath()))
+ {
+ TiXmlElement* pRootElement = discStubXML.RootElement();
+ if (!pRootElement || StringUtils::CompareNoCase(pRootElement->Value(), "discstub") != 0)
+ CLog::Log(LOGINFO, "No <discstub> node found for {}. Using default info dialog message",
+ item.GetPath());
+ else
+ {
+ XMLUtils::GetString(pRootElement, "title", strLine1);
+ XMLUtils::GetString(pRootElement, "message", strLine2);
+ // no title? use the label of the CFileItem as line 1
+ if (strLine1.empty())
+ strLine1 = item.GetLabel();
+ }
+ }
+
+ if (HasOpticalDrive())
+ {
+#ifdef HAS_DVD_DRIVE
+ if (CGUIDialogPlayEject::ShowAndGetInput(strLine1, strLine2))
+ return MEDIA_DETECT::CAutorun::PlayDiscAskResume();
+#endif
+ }
+ else
+ {
+ KODI::MESSAGING::HELPERS::ShowOKDialogText(strLine1, strLine2);
+ }
+ return true;
+}
diff --git a/xbmc/storage/MediaManager.h b/xbmc/storage/MediaManager.h
new file mode 100644
index 0000000..af76bb6
--- /dev/null
+++ b/xbmc/storage/MediaManager.h
@@ -0,0 +1,147 @@
+/*
+ * 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 "IStorageProvider.h"
+#include "MediaSource.h" // for VECSOURCES
+#include "storage/discs/IDiscDriveHandler.h"
+#include "threads/CriticalSection.h"
+#include "utils/DiscsUtils.h"
+#include "utils/Job.h"
+
+#include <map>
+#include <memory>
+#include <vector>
+
+#include "PlatformDefs.h"
+
+class CFileItem;
+
+class CNetworkLocation
+{
+public:
+ CNetworkLocation() { id = 0; }
+ int id;
+ std::string path;
+};
+
+class CMediaManager : public IStorageEventsCallback, public IJobCallback
+{
+public:
+ CMediaManager();
+
+ void Initialize();
+ void Stop();
+
+ bool LoadSources();
+ bool SaveSources();
+
+ void GetLocalDrives(VECSOURCES &localDrives, bool includeQ = true);
+ void GetRemovableDrives(VECSOURCES &removableDrives);
+ void GetNetworkLocations(VECSOURCES &locations, bool autolocations = true);
+
+ bool AddNetworkLocation(const std::string &path);
+ bool HasLocation(const std::string& path) const;
+ bool RemoveLocation(const std::string& path);
+ bool SetLocationPath(const std::string& oldPath, const std::string& newPath);
+
+ void AddAutoSource(const CMediaSource &share, bool bAutorun=false);
+ void RemoveAutoSource(const CMediaSource &share);
+ bool IsDiscInDrive(const std::string& devicePath="");
+ bool IsAudio(const std::string& devicePath="");
+ bool HasOpticalDrive();
+ std::string TranslateDevicePath(const std::string& devicePath, bool bReturnAsDevice=false);
+ DriveState GetDriveStatus(const std::string& devicePath = "");
+#ifdef HAS_DVD_DRIVE
+ MEDIA_DETECT::CCdInfo* GetCdInfo(const std::string& devicePath="");
+ bool RemoveCdInfo(const std::string& devicePath="");
+ std::string GetDiskLabel(const std::string& devicePath="");
+ std::string GetDiskUniqueId(const std::string& devicePath="");
+
+ /*! \brief Gets the platform disc drive handler
+ * @todo this likely doesn't belong here but in some discsupport component owned by media manager
+ * let's keep it here for now
+ * \return The platform disc drive handler
+ */
+ std::shared_ptr<IDiscDriveHandler> GetDiscDriveHandler();
+#endif
+ std::string GetDiscPath();
+ void SetHasOpticalDrive(bool bstatus);
+
+ bool Eject(const std::string& mountpath);
+ void EjectTray( const bool bEject=true, const char cDriveLetter='\0' );
+ void CloseTray(const char cDriveLetter='\0');
+ void ToggleTray(const char cDriveLetter='\0');
+
+ void ProcessEvents();
+
+ std::vector<std::string> GetDiskUsage();
+
+ /*! \brief Callback executed when a new storage device is added
+ * \sa IStorageEventsCallback
+ * @param device the storage device
+ */
+ void OnStorageAdded(const MEDIA_DETECT::STORAGE::StorageDevice& device) override;
+
+ /*! \brief Callback executed when a new storage device is safely removed
+ * \sa IStorageEventsCallback
+ * @param device the storage device
+ */
+ void OnStorageSafelyRemoved(const MEDIA_DETECT::STORAGE::StorageDevice& device) override;
+
+ /*! \brief Callback executed when a new storage device is unsafely removed
+ * \sa IStorageEventsCallback
+ * @param device the storage device
+ */
+ void OnStorageUnsafelyRemoved(const MEDIA_DETECT::STORAGE::StorageDevice& device) override;
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override { }
+
+ bool playStubFile(const CFileItem& item);
+
+protected:
+ std::vector<CNetworkLocation> m_locations;
+
+ CCriticalSection m_muAutoSource, m_CritSecStorageProvider;
+#ifdef HAS_DVD_DRIVE
+ std::map<std::string,MEDIA_DETECT::CCdInfo*> m_mapCdInfo;
+#endif
+ bool m_bhasoptical;
+ std::string m_strFirstAvailDrive;
+
+private:
+ /*! \brief Loads the addon sources for the different supported browsable addon types
+ */
+ void LoadAddonSources() const;
+
+ /*! \brief Get the addons root source for the given content type
+ \param type the type of addon content desired
+ \return the given CMediaSource for the addon root directory
+ */
+ CMediaSource GetRootAddonTypeSource(const std::string& type) const;
+
+ /*! \brief Generate the addons source for the given content type
+ \param type the type of addon content desired
+ \param label the name of the addons source
+ \param thumb image to use as the icon
+ \return the given CMediaSource for the addon root directory
+ */
+ CMediaSource ComputeRootAddonTypeSource(const std::string& type,
+ const std::string& label,
+ const std::string& thumb) const;
+
+ std::unique_ptr<IStorageProvider> m_platformStorage;
+#ifdef HAS_DVD_DRIVE
+ std::shared_ptr<IDiscDriveHandler> m_platformDiscDriveHander;
+#endif
+
+ UTILS::DISCS::DiscInfo GetDiscInfo(const std::string& mediaPath);
+ void RemoveDiscInfo(const std::string& devicePath);
+ std::map<std::string, UTILS::DISCS::DiscInfo> m_mapDiscInfo;
+};
diff --git a/xbmc/storage/cdioSupport.cpp b/xbmc/storage/cdioSupport.cpp
new file mode 100644
index 0000000..70e2463
--- /dev/null
+++ b/xbmc/storage/cdioSupport.cpp
@@ -0,0 +1,958 @@
+/*
+ * 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 "cdioSupport.h"
+
+#include "platform/Environment.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#include <cdio/cd_types.h>
+#include <cdio/cdio.h>
+#include <cdio/logging.h>
+#include <cdio/mmc.h>
+#include <cdio/util.h>
+
+namespace
+{
+/* Helper constexpr to hide the 0 return code for tray closed. */
+constexpr int CDIO_TRAY_STATUS_CLOSED = 0;
+/* Helper constexpr to hide the 1 return code for tray open */
+constexpr int CDIO_TRAY_STATUS_OPEN = 1;
+/* Helper constexpr to hide the -2 driver code for unsupported tray status get operation */
+constexpr int CDIO_TRAY_STATUS_OP_UNSUPPORTED = -2;
+} // namespace
+
+using namespace MEDIA_DETECT;
+
+std::shared_ptr<CLibcdio> CLibcdio::m_pInstance;
+
+/* Some interesting sector numbers stored in the above buffer. */
+#define ISO_SUPERBLOCK_SECTOR 16 /* buffer[0] */
+#define UFS_SUPERBLOCK_SECTOR 4 /* buffer[2] */
+#define BOOT_SECTOR 17 /* buffer[3] */
+#define VCD_INFO_SECTOR 150 /* buffer[4] */
+#define UDF_ANCHOR_SECTOR 256 /* buffer[5] */
+
+
+signature_t CCdIoSupport::sigs[] = {
+ /*buffer[x] off look for description */
+ {0, 1, "CD001\0", "ISO 9660\0"},
+ {0, 1, "CD-I", "CD-I"},
+ {0, 8, "CDTV", "CDTV"},
+ {0, 8, "CD-RTOS", "CD-RTOS"},
+ {0, 9, "CDROM", "HIGH SIERRA"},
+ {0, 16, "CD-BRIDGE", "BRIDGE"},
+ {0, 1024, "CD-XA001", "XA"},
+ {1, 64, "PPPPHHHHOOOOTTTTOOOO____CCCCDDDD", "PHOTO CD"},
+ {1, 0x438, "\x53\xef", "EXT2 FS"},
+ {2, 1372, "\x54\x19\x01\x0", "UFS"},
+ {3, 7, "EL TORITO", "BOOTABLE"},
+ {4, 0, "VIDEO_CD", "VIDEO CD"},
+ {4, 0, "SUPERVCD", "Chaoji VCD"},
+ {0, 1, "BEA01", "UDF"},
+ {}};
+
+#undef DEBUG_CDIO
+
+static void
+cdio_log_handler (cdio_log_level_t level, const char message[])
+{
+#ifdef DEBUG_CDIO
+ switch (level)
+ {
+ case CDIO_LOG_ERROR:
+ CLog::Log(LOGDEBUG, "**ERROR: {}", message);
+ break;
+ case CDIO_LOG_DEBUG:
+ CLog::Log(LOGDEBUG, "--DEBUG: {}", message);
+ break;
+ case CDIO_LOG_WARN:
+ CLog::Log(LOGDEBUG, "++ WARN: {}", message);
+ break;
+ case CDIO_LOG_INFO:
+ CLog::Log(LOGDEBUG, " INFO: {}", message);
+ break;
+ case CDIO_LOG_ASSERT:
+ CLog::Log(LOGDEBUG, "!ASSERT: {}", message);
+ break;
+ default:
+ //cdio_assert_not_reached ();
+ break;
+ }
+#endif
+}
+
+//////////////////////////////////////////////////////////////////////
+// Construction/Destruction
+//////////////////////////////////////////////////////////////////////
+CLibcdio::CLibcdio(): s_defaultDevice(NULL)
+{
+ cdio_log_set_handler( cdio_log_handler );
+}
+
+CLibcdio::~CLibcdio()
+{
+ free(s_defaultDevice);
+ s_defaultDevice = NULL;
+}
+
+void CLibcdio::ReleaseInstance()
+{
+ m_pInstance.reset();
+}
+
+std::shared_ptr<CLibcdio> CLibcdio::GetInstance()
+{
+ if (!m_pInstance)
+ {
+ m_pInstance = std::shared_ptr<CLibcdio>(new CLibcdio());
+ }
+ return m_pInstance;
+}
+
+CdIo_t* CLibcdio::cdio_open(const char *psz_source, driver_id_t driver_id)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ return( ::cdio_open(psz_source, driver_id) );
+}
+
+CdIo_t* CLibcdio::cdio_open_win32(const char *psz_source)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ return( ::cdio_open_win32(psz_source) );
+}
+
+void CLibcdio::cdio_destroy(CdIo_t *p_cdio)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ ::cdio_destroy(p_cdio);
+}
+
+discmode_t CLibcdio::cdio_get_discmode(CdIo_t *p_cdio)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ return( ::cdio_get_discmode(p_cdio) );
+}
+
+CdioTrayStatus CLibcdio::mmc_get_tray_status(const CdIo_t* p_cdio)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ int status = ::mmc_get_tray_status(p_cdio);
+ switch (status)
+ {
+ case CDIO_TRAY_STATUS_CLOSED:
+ return CdioTrayStatus::CLOSED;
+ case CDIO_TRAY_STATUS_OPEN:
+ return CdioTrayStatus::OPEN;
+ case CDIO_TRAY_STATUS_OP_UNSUPPORTED:
+ return CdioTrayStatus::UNKNOWN;
+ default:
+ break;
+ }
+ return CdioTrayStatus::DRIVER_ERROR;
+}
+
+driver_return_code_t CLibcdio::cdio_eject_media(CdIo_t** p_cdio)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ return( ::cdio_eject_media(p_cdio) );
+}
+
+track_t CLibcdio::cdio_get_last_track_num(const CdIo_t *p_cdio)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ return( ::cdio_get_last_track_num(p_cdio) );
+}
+
+lsn_t CLibcdio::cdio_get_track_lsn(const CdIo_t *p_cdio, track_t i_track)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ return( ::cdio_get_track_lsn(p_cdio, i_track) );
+}
+
+lsn_t CLibcdio::cdio_get_track_last_lsn(const CdIo_t *p_cdio, track_t i_track)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ return( ::cdio_get_track_last_lsn(p_cdio, i_track) );
+}
+
+driver_return_code_t CLibcdio::cdio_read_audio_sectors(
+ const CdIo_t *p_cdio, void *p_buf, lsn_t i_lsn, uint32_t i_blocks)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ return( ::cdio_read_audio_sectors(p_cdio, p_buf, i_lsn, i_blocks) );
+}
+
+driver_return_code_t CLibcdio::cdio_close_tray(const char* psz_source, driver_id_t* driver_id)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ return (::cdio_close_tray(psz_source, driver_id));
+}
+
+const char* CLibcdio::cdio_driver_errmsg(driver_return_code_t drc)
+{
+ return (::cdio_driver_errmsg(drc));
+}
+
+char* CLibcdio::GetDeviceFileName()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ // If We don't have a DVD device initially present (Darwin or a USB DVD drive),
+ // We have to keep checking in case one appears.
+ if (s_defaultDevice && strlen(s_defaultDevice) == 0)
+ {
+ free(s_defaultDevice);
+ s_defaultDevice = NULL;
+ }
+
+ if (s_defaultDevice == NULL)
+ {
+ std::string strEnvDvd = CEnvironment::getenv("KODI_DVD_DEVICE");
+ if (!strEnvDvd.empty())
+ s_defaultDevice = strdup(strEnvDvd.c_str());
+ else
+ {
+ CdIo_t *p_cdio = ::cdio_open(NULL, DRIVER_UNKNOWN);
+ if (p_cdio != NULL)
+ {
+ s_defaultDevice = strdup(::cdio_get_arg(p_cdio, "source"));
+ ::cdio_destroy(p_cdio);
+ }
+ else
+ s_defaultDevice = strdup("");
+ }
+ }
+ return s_defaultDevice;
+}
+
+
+//////////////////////////////////////////////////////////////////////
+// Construction/Destruction
+//////////////////////////////////////////////////////////////////////
+CCdIoSupport::CCdIoSupport()
+: cdio(nullptr)
+{
+ m_cdio = CLibcdio::GetInstance();
+ m_nFirstData = -1; /* # of first data track */
+ m_nNumData = 0; /* # of data tracks */
+ m_nFirstAudio = -1; /* # of first audio track */
+ m_nNumAudio = 0; /* # of audio tracks */
+ m_nIsofsSize = 0; /* size of session */
+ m_nJolietLevel = 0;
+ m_nFs = 0;
+ m_nUDFVerMinor = 0;
+ m_nUDFVerMajor = 0;
+ m_nDataStart = 0;
+ m_nMsOffset = 0;
+ m_nStartTrack = 0;
+}
+
+CCdIoSupport::~CCdIoSupport() = default;
+
+bool CCdIoSupport::EjectTray()
+{
+ return false;
+}
+
+bool CCdIoSupport::CloseTray()
+{
+ return false;
+}
+
+HANDLE CCdIoSupport::OpenCDROM()
+{
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ char* source_name = m_cdio->GetDeviceFileName();
+ CdIo* cdio = ::cdio_open(source_name, DRIVER_UNKNOWN);
+
+ return reinterpret_cast<HANDLE>(cdio);
+}
+
+HANDLE CCdIoSupport::OpenIMAGE( std::string& strFilename )
+{
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ CdIo* cdio = ::cdio_open(strFilename.c_str(), DRIVER_UNKNOWN);
+
+ return reinterpret_cast<HANDLE>(cdio);
+}
+
+int CCdIoSupport::ReadSector(HANDLE hDevice, DWORD dwSector, char* lpczBuffer)
+{
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ CdIo* cdio = (CdIo*) hDevice;
+ if ( cdio == NULL )
+ return -1;
+
+ if ( ::cdio_read_mode1_sector( cdio, lpczBuffer, dwSector, false ) == 0 )
+ return dwSector;
+
+ return -1;
+}
+
+int CCdIoSupport::ReadSectorMode2(HANDLE hDevice, DWORD dwSector, char* lpczBuffer)
+{
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ CdIo* cdio = (CdIo*) hDevice;
+ if ( cdio == NULL )
+ return -1;
+
+ if ( ::cdio_read_mode2_sector( cdio, lpczBuffer, dwSector, false ) == 0 )
+ return dwSector;
+
+ return -1;
+}
+
+int CCdIoSupport::ReadSectorCDDA(HANDLE hDevice, DWORD dwSector, char* lpczBuffer)
+{
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ CdIo* cdio = (CdIo*) hDevice;
+ if ( cdio == NULL )
+ return -1;
+
+ if ( ::cdio_read_audio_sector( cdio, lpczBuffer, dwSector ) == 0 )
+ return dwSector;
+
+ return -1;
+}
+
+void CCdIoSupport::CloseCDROM(HANDLE hDevice)
+{
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ CdIo* cdio = (CdIo*) hDevice;
+
+ if ( cdio == NULL )
+ return ;
+
+ ::cdio_destroy( cdio );
+}
+
+void CCdIoSupport::PrintAnalysis(int fs, int num_audio)
+{
+ switch (fs & FS_MASK)
+ {
+ case FS_UDF:
+ CLog::Log(LOGINFO, "CD-ROM with UDF filesystem");
+ break;
+ case FS_NO_DATA:
+ CLog::Log(LOGINFO, "CD-ROM with audio tracks");
+ break;
+ case FS_ISO_9660:
+ CLog::Log(LOGINFO, "CD-ROM with ISO 9660 filesystem");
+ if (fs & JOLIET)
+ {
+ CLog::Log(LOGINFO, " with joliet extension level {}", m_nJolietLevel);
+ }
+ if (fs & ROCKRIDGE)
+ {
+ CLog::Log(LOGINFO, " and rockridge extensions");
+ }
+ break;
+ case FS_ISO_9660_INTERACTIVE:
+ CLog::Log(LOGINFO, "CD-ROM with CD-RTOS and ISO 9660 filesystem");
+ break;
+ case FS_HIGH_SIERRA:
+ CLog::Log(LOGINFO, "CD-ROM with High Sierra filesystem");
+ break;
+ case FS_INTERACTIVE:
+ CLog::Log(LOGINFO, "CD-Interactive{}", num_audio > 0 ? "/Ready" : "");
+ break;
+ case FS_HFS:
+ CLog::Log(LOGINFO, "CD-ROM with Macintosh HFS");
+ break;
+ case FS_ISO_HFS:
+ CLog::Log(LOGINFO, "CD-ROM with both Macintosh HFS and ISO 9660 filesystem");
+ break;
+ case FS_ISO_UDF:
+ CLog::Log(LOGINFO, "CD-ROM with both UDF and ISO 9660 filesystem");
+ break;
+ case FS_UFS:
+ CLog::Log(LOGINFO, "CD-ROM with Unix UFS");
+ break;
+ case FS_EXT2:
+ CLog::Log(LOGINFO, "CD-ROM with Linux second extended filesystem");
+ break;
+ case FS_3DO:
+ CLog::Log(LOGINFO, "CD-ROM with Panasonic 3DO filesystem");
+ break;
+ case FS_UNKNOWN:
+ CLog::Log(LOGINFO, "CD-ROM with unknown filesystem");
+ break;
+ }
+
+ switch (fs & FS_MASK)
+ {
+ case FS_ISO_9660:
+ case FS_ISO_9660_INTERACTIVE:
+ case FS_ISO_HFS:
+ case FS_ISO_UDF:
+ CLog::Log(LOGINFO, "ISO 9660: {} blocks, label {}", m_nIsofsSize, m_strDiscLabel);
+ break;
+ }
+
+ switch (fs & FS_MASK)
+ {
+ case FS_UDF:
+ case FS_ISO_UDF:
+ CLog::Log(LOGINFO, "UDF: version {:x}.{:02x}", m_nUDFVerMajor, m_nUDFVerMinor);
+ break;
+ }
+
+ if (m_nFirstData == 1 && num_audio > 0)
+ {
+ CLog::Log(LOGINFO, "mixed mode CD ");
+ }
+ if (fs & XA)
+ {
+ CLog::Log(LOGINFO, "XA sectors ");
+ }
+ if (fs & MULTISESSION)
+ {
+ CLog::Log(LOGINFO, "Multisession, offset = {} ", m_nMsOffset);
+ }
+ if (fs & HIDDEN_TRACK)
+ {
+ CLog::Log(LOGINFO, "Hidden Track ");
+ }
+ if (fs & PHOTO_CD)
+ {
+ CLog::Log(LOGINFO, "{}Photo CD ", num_audio > 0 ? " Portfolio " : "");
+ }
+ if (fs & CDTV)
+ {
+ CLog::Log(LOGINFO, "Commodore CDTV ");
+ }
+ if (m_nFirstData > 1)
+ {
+ CLog::Log(LOGINFO, "CD-Plus/Extra ");
+ }
+ if (fs & BOOTABLE)
+ {
+ CLog::Log(LOGINFO, "bootable CD ");
+ }
+ if (fs & VIDEOCDI && num_audio == 0)
+ {
+ CLog::Log(LOGINFO, "Video CD ");
+#if defined(HAVE_VCDINFO) && defined(DEBUG)
+ if (!opts.no_vcd)
+ {
+ printf("\n");
+ print_vcd_info();
+ }
+#endif
+
+ }
+ if (fs & CVD)
+ {
+ CLog::Log(LOGINFO, "Chaoji Video CD");
+ }
+}
+
+int CCdIoSupport::ReadBlock(int superblock, uint32_t offset, uint8_t bufnum, track_t track_num)
+{
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ unsigned int track_sec_count = ::cdio_get_track_sec_count(cdio, track_num);
+ memset(buffer[bufnum], 0, CDIO_CD_FRAMESIZE);
+
+ if ( track_sec_count < static_cast<unsigned int>(superblock))
+ {
+ ::cdio_debug("reading block %u skipped track %d has only %u sectors\n",
+ superblock, track_num, track_sec_count);
+ return -1;
+ }
+
+ ::cdio_debug("about to read sector %lu\n",
+ (long unsigned int) offset + superblock);
+
+ if (::cdio_get_track_green(cdio, track_num))
+ {
+ if (0 < ::cdio_read_mode2_sector(cdio, buffer[bufnum],
+ offset + superblock, false))
+ return -1;
+ }
+ else
+ {
+ if (0 < ::cdio_read_mode1_sector(cdio, buffer[bufnum],
+ offset + superblock, false))
+ return -1;
+ }
+
+ return 0;
+}
+
+bool CCdIoSupport::IsIt(int num)
+{
+ signature_t *sigp = &sigs[num];
+ int len = strlen(sigp->sig_str);
+
+ //! @todo check that num < largest sig.
+ return 0 == memcmp(&buffer[sigp->buf_num][sigp->offset], sigp->sig_str, len);
+}
+
+int CCdIoSupport::IsHFS(void)
+{
+ return (0 == memcmp(&buffer[1][512], "PM", 2)) ||
+ (0 == memcmp(&buffer[1][512], "TS", 2)) ||
+ (0 == memcmp(&buffer[1][1024], "BD", 2));
+}
+
+int CCdIoSupport::Is3DO(void)
+{
+ return (0 == memcmp(&buffer[1][0], "\x01\x5a\x5a\x5a\x5a\x5a\x01", 7)) &&
+ (0 == memcmp(&buffer[1][40], "CD-ROM", 6));
+}
+
+int CCdIoSupport::IsJoliet(void)
+{
+ return 2 == buffer[3][0] && buffer[3][88] == 0x25 && buffer[3][89] == 0x2f;
+}
+
+int CCdIoSupport::IsUDF(void)
+{
+ return 2 == ((uint16_t)buffer[5][0] | ((uint16_t)buffer[5][1] << 8));
+}
+
+/* ISO 9660 volume space in M2F1_SECTOR_SIZE byte units */
+int CCdIoSupport::GetSize(void)
+{
+ return ((buffer[0][80] & 0xff) |
+ ((buffer[0][81] & 0xff) << 8) |
+ ((buffer[0][82] & 0xff) << 16) |
+ ((buffer[0][83] & 0xff) << 24));
+}
+
+int CCdIoSupport::GetJolietLevel( void )
+{
+ switch (buffer[3][90])
+ {
+ case 0x40:
+ return 1;
+ case 0x43:
+ return 2;
+ case 0x45:
+ return 3;
+ }
+ return 0;
+}
+
+#define is_it_dbg(sig) /*\
+ if (is_it(sig)) printf("%s, ", sigs[sig].description)*/
+
+int CCdIoSupport::GuessFilesystem(int start_session, track_t track_num)
+{
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ int ret = FS_UNKNOWN;
+ cdio_iso_analysis_t anal;
+ cdio_fs_anal_t fs;
+ bool udf = false;
+
+ memset(&anal, 0, sizeof(anal));
+ discmode_t mode = ::cdio_get_discmode(cdio);
+ if (::cdio_is_discmode_dvd(mode))
+ {
+ m_strDiscLabel = "";
+ m_nIsofsSize = ::cdio_get_disc_last_lsn(cdio);
+ m_nJolietLevel = ::cdio_get_joliet_level(cdio);
+
+ return FS_ISO_9660;
+ }
+
+ fs = ::cdio_guess_cd_type(cdio, start_session, track_num, &anal);
+
+ switch(CDIO_FSTYPE(fs))
+ {
+ case CDIO_FS_AUDIO:
+ ret = FS_NO_DATA;
+ break;
+
+ case CDIO_FS_HIGH_SIERRA:
+ ret = FS_HIGH_SIERRA;
+ break;
+
+ case CDIO_FS_ISO_9660:
+ ret = FS_ISO_9660;
+ break;
+
+ case CDIO_FS_INTERACTIVE:
+ ret = FS_ISO_9660_INTERACTIVE;
+ break;
+
+ case CDIO_FS_HFS:
+ ret = FS_HFS;
+ break;
+
+ case CDIO_FS_UFS:
+ ret = FS_UFS;
+ break;
+
+ case CDIO_FS_EXT2:
+ ret = FS_EXT2;
+ break;
+
+ case CDIO_FS_UDF:
+ ret = FS_UDF;
+ udf = true;
+ break;
+
+ case CDIO_FS_ISO_UDF:
+ ret = FS_ISO_UDF;
+ udf = true;
+ break;
+
+ default:
+ break;
+ }
+
+ if (udf)
+ {
+ m_nUDFVerMinor = anal.UDFVerMinor;
+ m_nUDFVerMajor = anal.UDFVerMajor;
+ }
+
+ m_strDiscLabel = anal.iso_label;
+ m_nIsofsSize = anal.isofs_size;
+ m_nJolietLevel = anal.joliet_level;
+
+ return ret;
+}
+
+void CCdIoSupport::GetCdTextInfo(xbmc_cdtext_t &xcdt, int trackNum)
+{
+ // cdtext disabled for windows as some setup doesn't like mmc commands
+ // and stall for over a minute in cdio_get_cdtext 83
+#if !defined(TARGET_WINDOWS)
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ // Get the CD-Text , if any
+#if defined(LIBCDIO_VERSION_NUM) && (LIBCDIO_VERSION_NUM >= 84)
+ cdtext_t *pcdtext = static_cast<cdtext_t*>( cdio_get_cdtext(cdio) );
+#else
+ //! @todo - remove after Ubuntu 16.04 (Xenial) is EOL
+ cdtext_t *pcdtext = (cdtext_t *)::cdio_get_cdtext(cdio, trackNum);
+#endif
+
+ if (pcdtext == NULL)
+ return ;
+
+#if defined(LIBCDIO_VERSION_NUM) && (LIBCDIO_VERSION_NUM >= 84)
+ for (int i=0; i < MAX_CDTEXT_FIELDS; i++)
+ if (cdtext_get_const(pcdtext, (cdtext_field_t)i, trackNum))
+ xcdt[(cdtext_field_t)i] = cdtext_field2str((cdtext_field_t)i);
+#else
+ //! @todo - remove after Ubuntu 16.04 (Xenial) is EOL
+ // Same ids used in libcdio and for our structure + the ids are consecutive make this copy loop safe.
+ for (int i = 0; i < MAX_CDTEXT_FIELDS; i++)
+ if (pcdtext->field[i])
+ xcdt[(cdtext_field_t)i] = pcdtext->field[(cdtext_field_t)i];
+#endif
+#endif // TARGET_WINDOWS
+}
+
+CCdInfo* CCdIoSupport::GetCdInfo(char* cDeviceFileName)
+{
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ char* source_name;
+ if(cDeviceFileName == NULL)
+ source_name = m_cdio->GetDeviceFileName();
+ else
+ source_name = cDeviceFileName;
+
+ cdio = ::cdio_open(source_name, DRIVER_UNKNOWN);
+ if (cdio == NULL)
+ {
+ CLog::Log(LOGERROR, "{}: Error in automatically selecting driver with input", __FUNCTION__);
+ return NULL;
+ }
+
+ bool bIsCDRom = true;
+
+ m_nFirstTrackNum = ::cdio_get_first_track_num(cdio);
+ if (m_nFirstTrackNum == CDIO_INVALID_TRACK)
+ {
+#if !defined(TARGET_DARWIN)
+ ::cdio_destroy(cdio);
+ return NULL;
+#else
+ m_nFirstTrackNum = 1;
+ bIsCDRom = false;
+#endif
+ }
+
+ m_nNumTracks = ::cdio_get_num_tracks(cdio);
+ if (m_nNumTracks == CDIO_INVALID_TRACK)
+ {
+#if !defined(TARGET_DARWIN)
+ ::cdio_destroy(cdio);
+ return NULL;
+#else
+ m_nNumTracks = 1;
+ bIsCDRom = false;
+#endif
+ }
+
+ CCdInfo* info = new CCdInfo;
+ info->SetFirstTrack( m_nFirstTrackNum );
+ info->SetTrackCount( m_nNumTracks );
+
+ for (i = m_nFirstTrackNum; i <= CDIO_CDROM_LEADOUT_TRACK; i++)
+ {
+ msf_t msf;
+ if (bIsCDRom && !::cdio_get_track_msf(cdio, i, &msf))
+ {
+ trackinfo ti;
+ ti.nfsInfo = FS_UNKNOWN;
+ ti.ms_offset = 0;
+ ti.isofs_size = 0;
+ ti.nJolietLevel = 0;
+ ti.nFrames = 0;
+ ti.nMins = 0;
+ ti.nSecs = 0;
+ info->SetTrackInformation( i, ti );
+ CLog::Log(LOGDEBUG, "cdio_track_msf for track {} failed, I give up.", i);
+ delete info;
+ ::cdio_destroy(cdio);
+ return NULL;
+ }
+
+ trackinfo ti;
+ if (bIsCDRom && TRACK_FORMAT_AUDIO == ::cdio_get_track_format(cdio, i))
+ {
+ m_nNumAudio++;
+ ti.nfsInfo = FS_NO_DATA;
+ m_nFs = FS_NO_DATA;
+ int temp1 = ::cdio_get_track_lba(cdio, i) - CDIO_PREGAP_SECTORS;
+ int temp2 = ::cdio_get_track_lba(cdio, i + 1) - CDIO_PREGAP_SECTORS;
+ // The length is the address of the second track minus the address of the first track
+ temp2 -= temp1; // temp2 now has length of track1 in frames
+ ti.nMins = temp2 / (60 * 75); // calculate the number of minutes
+ temp2 %= 60 * 75; // calculate the left-over frames
+ ti.nSecs = temp2 / 75; // calculate the number of seconds
+ if ( -1 == m_nFirstAudio)
+ m_nFirstAudio = i;
+
+ // Make sure that we have the Disc related info available
+ if (i == 1)
+ {
+ xbmc_cdtext_t xcdt;
+ GetCdTextInfo(xcdt, 0);
+ info->SetDiscCDTextInformation( xcdt );
+ }
+
+ // Get this tracks info
+ GetCdTextInfo(ti.cdtext, i);
+ }
+ else
+ {
+ m_nNumData++;
+ if ( -1 == m_nFirstData)
+ m_nFirstData = i;
+ }
+ ti.nfsInfo = FS_NO_DATA;
+ ti.ms_offset = 0;
+ ti.isofs_size = 0;
+ ti.nJolietLevel = 0;
+ ti.nFrames = ::cdio_get_track_lba(cdio, i);
+ ti.nMins = 0;
+ ti.nSecs = 0;
+
+ info->SetTrackInformation( i, ti );
+ /* skip to leadout? */
+ if (i == m_nNumTracks)
+ i = CDIO_CDROM_LEADOUT_TRACK;
+ }
+
+ info->SetCddbDiscId( CddbDiscId() );
+ info->SetDiscLength( ::cdio_get_track_lba(cdio, CDIO_CDROM_LEADOUT_TRACK) / CDIO_CD_FRAMES_PER_SEC );
+
+ info->SetAudioTrackCount( m_nNumAudio );
+ info->SetDataTrackCount( m_nNumData );
+ info->SetFirstAudioTrack( m_nFirstAudio );
+ info->SetFirstDataTrack( m_nFirstData );
+
+ CLog::Log(LOGINFO, "CD Analysis Report");
+ CLog::Log(LOGINFO, STRONG);
+
+ /* Try to find out what sort of CD we have */
+ if (0 == m_nNumData)
+ {
+ /* no data track, may be a "real" audio CD or hidden track CD */
+
+ msf_t msf;
+ ::cdio_get_track_msf(cdio, 1, &msf);
+ m_nStartTrack = ::cdio_msf_to_lsn(&msf);
+
+ /* CD-I/Ready says start_track <= 30*75 then CDDA */
+ if (m_nStartTrack > 100 /* 100 is just a guess */)
+ {
+ m_nFs = GuessFilesystem(0, 1);
+ if ((m_nFs & FS_MASK) != FS_UNKNOWN)
+ m_nFs |= HIDDEN_TRACK;
+ else
+ {
+ m_nFs &= ~FS_MASK; /* del filesystem info */
+ CLog::Log(LOGDEBUG, "Oops: {} unused sectors at start, but hidden track check failed.",
+ m_nStartTrack);
+ }
+ }
+ PrintAnalysis(m_nFs, m_nNumAudio);
+ }
+ else
+ {
+ /* We have data track(s) */
+ for (j = 2, i = m_nFirstData; i <= m_nNumTracks; i++)
+ {
+ msf_t msf;
+ track_format_t track_format = ::cdio_get_track_format(cdio, i);
+
+ ::cdio_get_track_msf(cdio, i, &msf);
+
+ switch ( track_format )
+ {
+ case TRACK_FORMAT_AUDIO:
+ {
+ trackinfo ti;
+ ti.nfsInfo = FS_NO_DATA;
+ m_nFs = FS_NO_DATA;
+ ti.ms_offset = 0;
+ ti.isofs_size = 0;
+ ti.nJolietLevel = 0;
+ ti.nFrames = ::cdio_get_track_lba(cdio, i);
+ ti.nMins = 0;
+ ti.nSecs = 0;
+ info->SetTrackInformation( i + 1, ti );
+ }
+ case TRACK_FORMAT_ERROR:
+ break;
+ case TRACK_FORMAT_CDI:
+ case TRACK_FORMAT_XA:
+ case TRACK_FORMAT_DATA:
+ case TRACK_FORMAT_PSX:
+ break;
+ }
+
+ m_nStartTrack = (i == 1) ? 0 : ::cdio_msf_to_lsn(&msf);
+
+ /* Save the start of the data area */
+ if (i == m_nFirstData)
+ m_nDataStart = m_nStartTrack;
+
+ /* Skip tracks which belong to the current walked session */
+ if (m_nStartTrack < m_nDataStart + m_nIsofsSize)
+ continue;
+
+ m_nFs = GuessFilesystem(m_nStartTrack, i);
+ trackinfo ti;
+ ti.nfsInfo = m_nFs;
+ ti.ms_offset = m_nMsOffset;
+ ti.isofs_size = m_nIsofsSize;
+ ti.nJolietLevel = m_nJolietLevel;
+ ti.nFrames = ::cdio_get_track_lba(cdio, i);
+ ti.nMins = 0;
+ ti.nSecs = 0;
+ info->SetDiscLabel(m_strDiscLabel);
+
+
+ if (i > 1)
+ {
+ /* Track is beyond last session -> new session found */
+ m_nMsOffset = m_nStartTrack;
+
+ CLog::Log(LOGINFO,
+ "Session #{} starts at track {:2}, LSN: {:6},"
+ " ISO 9660 blocks: {:6}",
+ j++, i, m_nStartTrack, m_nIsofsSize);
+
+ CLog::Log(LOGINFO, "ISO 9660: {} blocks, label {}", m_nIsofsSize, m_strDiscLabel);
+ m_nFs |= MULTISESSION;
+ ti.nfsInfo = m_nFs;
+ }
+ else
+ {
+ PrintAnalysis(m_nFs, m_nNumAudio);
+ }
+
+ info->SetTrackInformation( i, ti );
+
+ }
+ }
+ ::cdio_destroy( cdio );
+ return info;
+}
+
+
+// Returns the sum of the decimal digits in a number. Eg. 1955 = 20
+int CCdIoSupport::CddbDecDigitSum(int n)
+{
+ int ret = 0;
+
+ for (;;)
+ {
+ ret += n % 10;
+ n = n / 10;
+ if (!n)
+ return ret;
+ }
+}
+
+// Return the number of seconds (discarding frame portion) of an MSF
+unsigned int CCdIoSupport::MsfSeconds(msf_t *msf)
+{
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ return ::cdio_from_bcd8(msf->m)*60 + ::cdio_from_bcd8(msf->s);
+}
+
+
+// Compute the CDDB disk ID for an Audio disk.
+// This is a funny checksum consisting of the concatenation of 3 things:
+// The sum of the decimal digits of sizes of all tracks,
+// The total length of the disk, and
+// The number of tracks.
+
+uint32_t CCdIoSupport::CddbDiscId()
+{
+ std::unique_lock<CCriticalSection> lock(*m_cdio);
+
+ int i, t, n = 0;
+ msf_t start_msf;
+ msf_t msf;
+
+ for (i = 1; i <= m_nNumTracks; i++)
+ {
+ ::cdio_get_track_msf(cdio, i, &msf);
+ n += CddbDecDigitSum(MsfSeconds(&msf));
+ }
+
+ ::cdio_get_track_msf(cdio, 1, &start_msf);
+ ::cdio_get_track_msf(cdio, CDIO_CDROM_LEADOUT_TRACK, &msf);
+
+ t = MsfSeconds(&msf) - MsfSeconds(&start_msf);
+
+ return ((n % 0xff) << 24 | t << 8 | m_nNumTracks);
+}
diff --git a/xbmc/storage/cdioSupport.h b/xbmc/storage/cdioSupport.h
new file mode 100644
index 0000000..b786ff2
--- /dev/null
+++ b/xbmc/storage/cdioSupport.h
@@ -0,0 +1,347 @@
+/*
+ * 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
+
+// CCdInfo - Information about media type of an inserted cd
+// CCdIoSupport - Wrapper class for libcdio with the interface of CIoSupport
+// and detecting the filesystem on the Disc.
+//
+// by Bobbin007 in 2003
+// CD-Text support by Mog - Oct 2004
+
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "PlatformDefs.h" // for ssize_t typedef, used by cdio
+
+#include <cdio/cdio.h>
+
+namespace MEDIA_DETECT
+{
+
+#define STRONG "__________________________________\n"
+//#define NORMAL ""
+
+#define FS_NO_DATA 0 /* audio only */
+#define FS_HIGH_SIERRA 1
+#define FS_ISO_9660 2
+#define FS_INTERACTIVE 3
+#define FS_HFS 4
+#define FS_UFS 5
+#define FS_EXT2 6
+#define FS_ISO_HFS 7 /* both hfs & isofs filesystem */
+#define FS_ISO_9660_INTERACTIVE 8 /* both CD-RTOS and isofs filesystem */
+#define FS_3DO 9
+#define FS_UDF 11
+#define FS_ISO_UDF 12
+#define FS_UNKNOWN 15
+#define FS_MASK 15
+
+#define XA 16
+#define MULTISESSION 32
+#define PHOTO_CD 64
+#define HIDDEN_TRACK 128
+#define CDTV 256
+#define BOOTABLE 512
+#define VIDEOCDI 1024
+#define ROCKRIDGE 2048
+#define JOLIET 4096
+#define CVD 8192 /* Choiji Video CD */
+
+#define IS_ISOFS 0
+#define IS_CD_I 1
+#define IS_CDTV 2
+#define IS_CD_RTOS 3
+#define IS_HS 4
+#define IS_BRIDGE 5
+#define IS_XA 6
+#define IS_PHOTO_CD 7
+#define IS_EXT2 8
+#define IS_UFS 9
+#define IS_BOOTABLE 10
+#define IS_VIDEO_CD 11 /* Video CD */
+#define IS_CVD 12 /* Chinese Video CD - slightly incompatible with SVCD */
+#define IS_UDF 14
+
+typedef struct signature
+{
+ unsigned int buf_num;
+ unsigned int offset;
+ const char *sig_str;
+ const char *description;
+}
+signature_t;
+
+typedef std::map<cdtext_field_t, std::string> xbmc_cdtext_t;
+
+typedef struct TRACKINFO
+{
+ int nfsInfo; // Information of the Tracks Filesystem
+ int nJolietLevel; // Jouliet Level
+ int ms_offset; // Multisession Offset
+ int isofs_size; // Size of the ISO9660 Filesystem
+ int nFrames; // Can be used for cddb query
+ int nMins; // minutes playtime part of Track
+ int nSecs; // seconds playtime part of Track
+ xbmc_cdtext_t cdtext; // CD-Text for this track
+}
+trackinfo;
+
+/*! \brief Helper enum class for the MMC tray state
+*/
+enum class CdioTrayStatus
+{
+ /* The MMC tray state is reported closed */
+ CLOSED,
+ /* The MMC tray state is reported open */
+ OPEN,
+ /* The MMC tray status operation is not supported */
+ UNKNOWN,
+ /* Generic driver error */
+ DRIVER_ERROR
+};
+
+class CCdInfo
+{
+public:
+ CCdInfo()
+ {
+ m_bHasCDDBInfo = true;
+ m_nLength = m_nFirstTrack = m_nNumTrack = m_nNumAudio = m_nFirstAudio = m_nNumData = m_nFirstData = 0;
+ }
+
+ trackinfo GetTrackInformation( int nTrack ) { return m_ti[nTrack -1]; }
+ xbmc_cdtext_t GetDiscCDTextInformation() { return m_cdtext; }
+
+ bool HasDataTracks() { return (m_nNumData > 0); }
+ bool HasAudioTracks() { return (m_nNumAudio > 0); }
+ int GetFirstTrack() { return m_nFirstTrack; }
+ int GetTrackCount() { return m_nNumTrack; }
+ int GetFirstAudioTrack() { return m_nFirstAudio; }
+ int GetFirstDataTrack() { return m_nFirstData; }
+ int GetDataTrackCount() { return m_nNumData; }
+ int GetAudioTrackCount() { return m_nNumAudio; }
+ uint32_t GetCddbDiscId() { return m_ulCddbDiscId; }
+ int GetDiscLength() { return m_nLength; }
+ std::string GetDiscLabel(){ return m_strDiscLabel; }
+
+ // CD-ROM with ISO 9660 filesystem
+ bool IsIso9660( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_ISO_9660); }
+ // CD-ROM with joliet extension
+ bool IsJoliet( int nTrack ) { return (m_ti[nTrack - 1].nfsInfo & JOLIET) ? false : true; }
+ // Joliet extension level
+ int GetJolietLevel( int nTrack ) { return m_ti[nTrack - 1].nJolietLevel; }
+ // ISO filesystem size
+ int GetIsoSize( int nTrack ) { return m_ti[nTrack - 1].isofs_size; }
+ // CD-ROM with rockridge extensions
+ bool IsRockridge( int nTrack ) { return (m_ti[nTrack - 1].nfsInfo & ROCKRIDGE) ? false : true; }
+
+ // CD-ROM with CD-RTOS and ISO 9660 filesystem
+ bool IsIso9660Interactive( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_ISO_9660_INTERACTIVE); }
+
+ // CD-ROM with High Sierra filesystem
+ bool IsHighSierra( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_HIGH_SIERRA); }
+
+ // CD-Interactive, with audiotracks > 0 CD-Interactive/Ready
+ bool IsCDInteractive( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_INTERACTIVE); }
+
+ // CD-ROM with Macintosh HFS
+ bool IsHFS( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_HFS); }
+
+ // CD-ROM with both Macintosh HFS and ISO 9660 filesystem
+ bool IsISOHFS( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_ISO_HFS); }
+
+ // CD-ROM with both UDF and ISO 9660 filesystem
+ bool IsISOUDF( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_ISO_UDF); }
+
+ // CD-ROM with Unix UFS
+ bool IsUFS( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_UFS); }
+
+ // CD-ROM with Linux second extended filesystem
+ bool IsEXT2( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_EXT2); }
+
+ // CD-ROM with Panasonic 3DO filesystem
+ bool Is3DO( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_3DO); }
+
+ // Mixed Mode CD-ROM
+ bool IsMixedMode( int nTrack ) { return (m_nFirstData == 1 && m_nNumAudio > 0); }
+
+ // CD-ROM with XA sectors
+ bool IsXA( int nTrack ) { return (m_ti[nTrack - 1].nfsInfo & XA) ? false : true; }
+
+ // Multisession CD-ROM
+ bool IsMultiSession( int nTrack ) { return (m_ti[nTrack - 1].nfsInfo & MULTISESSION) ? false : true; }
+ // Gets multisession offset
+ int GetMultisessionOffset( int nTrack ) { return m_ti[nTrack - 1].ms_offset; }
+
+ // Hidden Track on Audio CD
+ bool IsHiddenTrack( int nTrack ) { return (m_ti[nTrack - 1].nfsInfo & HIDDEN_TRACK) ? false : true; }
+
+ // Photo CD, with audiotracks > 0 Portfolio Photo CD
+ bool IsPhotoCd( int nTrack ) { return (m_ti[nTrack - 1].nfsInfo & PHOTO_CD) ? false : true; }
+
+ // CD-ROM with Commodore CDTV
+ bool IsCdTv( int nTrack ) { return (m_ti[nTrack - 1].nfsInfo & CDTV) ? false : true; }
+
+ // CD-Plus/Extra
+ bool IsCDExtra( int nTrack ) { return (m_nFirstData > 1); }
+
+ // Bootable CD
+ bool IsBootable( int nTrack ) { return (m_ti[nTrack - 1].nfsInfo & BOOTABLE) ? false : true; }
+
+ // Video CD
+ bool IsVideoCd( int nTrack ) { return (m_ti[nTrack - 1].nfsInfo & VIDEOCDI && m_nNumAudio == 0); }
+
+ // Chaoji Video CD
+ bool IsChaojiVideoCD( int nTrack ) { return (m_ti[nTrack - 1].nfsInfo & CVD) ? false : true; }
+
+ // Audio Track
+ bool IsAudio( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_NO_DATA); }
+
+ // UDF filesystem
+ bool IsUDF( int nTrack ) { return ((m_ti[nTrack - 1].nfsInfo & FS_MASK) == FS_UDF); }
+
+ // Has the cd a filesystem that is readable by the xbox
+ bool IsValidFs() { return (IsISOHFS(1) || IsIso9660(1) || IsIso9660Interactive(1) || IsISOUDF(1) || IsUDF(1) || IsAudio(1)); }
+
+ void SetFirstTrack( int nTrack ) { m_nFirstTrack = nTrack; }
+ void SetTrackCount( int nCount ) { m_nNumTrack = nCount; }
+ void SetFirstAudioTrack( int nTrack ) { m_nFirstAudio = nTrack; }
+ void SetFirstDataTrack( int nTrack ) { m_nFirstData = nTrack; }
+ void SetDataTrackCount( int nCount ) { m_nNumData = nCount; }
+ void SetAudioTrackCount( int nCount ) { m_nNumAudio = nCount; }
+ void SetTrackInformation(int nTrack, trackinfo nInfo)
+ {
+ if (nTrack > 0 && nTrack <= 99)
+ m_ti[nTrack - 1] = std::move(nInfo);
+ }
+ void SetDiscCDTextInformation(xbmc_cdtext_t cdtext) { m_cdtext = std::move(cdtext); }
+
+ void SetCddbDiscId( uint32_t ulCddbDiscId ) { m_ulCddbDiscId = ulCddbDiscId; }
+ void SetDiscLength( int nLength ) { m_nLength = nLength; }
+ bool HasCDDBInfo() { return m_bHasCDDBInfo; }
+ void SetNoCDDBInfo() { m_bHasCDDBInfo = false; }
+
+ void SetDiscLabel(const std::string& strDiscLabel){ m_strDiscLabel = strDiscLabel; }
+
+private:
+ int m_nFirstData; /* # of first data track */
+ int m_nNumData; /* # of data tracks */
+ int m_nFirstAudio; /* # of first audio track */
+ int m_nNumAudio; /* # of audio tracks */
+ int m_nNumTrack;
+ int m_nFirstTrack;
+ trackinfo m_ti[100];
+ uint32_t m_ulCddbDiscId;
+ int m_nLength; // Disclength can be used for cddb query, also see trackinfo.nFrames
+ bool m_bHasCDDBInfo;
+ std::string m_strDiscLabel;
+ xbmc_cdtext_t m_cdtext; // CD-Text for this disc
+};
+
+class CLibcdio : public CCriticalSection
+{
+private:
+ CLibcdio();
+public:
+ virtual ~CLibcdio();
+
+ static void ReleaseInstance();
+ static std::shared_ptr<CLibcdio> GetInstance();
+
+ // libcdio is not thread safe so these are wrappers to libcdio routines
+ CdIo_t* cdio_open(const char *psz_source, driver_id_t driver_id);
+ CdIo_t* cdio_open_win32(const char *psz_source);
+ void cdio_destroy(CdIo_t *p_cdio);
+ discmode_t cdio_get_discmode(CdIo_t *p_cdio);
+ CdioTrayStatus mmc_get_tray_status(const CdIo_t* p_cdio);
+ driver_return_code_t cdio_eject_media(CdIo_t** p_cdio);
+ track_t cdio_get_last_track_num(const CdIo_t *p_cdio);
+ lsn_t cdio_get_track_lsn(const CdIo_t *p_cdio, track_t i_track);
+ lsn_t cdio_get_track_last_lsn(const CdIo_t *p_cdio, track_t i_track);
+ driver_return_code_t cdio_read_audio_sectors(const CdIo_t *p_cdio, void *p_buf, lsn_t i_lsn, uint32_t i_blocks);
+ driver_return_code_t cdio_close_tray(const char* psz_source, driver_id_t* driver_id);
+ const char* cdio_driver_errmsg(driver_return_code_t drc);
+
+ char* GetDeviceFileName();
+
+private:
+ char* s_defaultDevice;
+ CCriticalSection m_critSection;
+ static std::shared_ptr<CLibcdio> m_pInstance;
+};
+
+class CCdIoSupport
+{
+public:
+ CCdIoSupport();
+ virtual ~CCdIoSupport();
+
+ bool EjectTray();
+ bool CloseTray();
+
+ HANDLE OpenCDROM();
+ HANDLE OpenIMAGE( std::string& strFilename );
+ int ReadSector(HANDLE hDevice, DWORD dwSector, char* lpczBuffer);
+ int ReadSectorMode2(HANDLE hDevice, DWORD dwSector, char* lpczBuffer);
+ int ReadSectorCDDA(HANDLE hDevice, DWORD dwSector, char* lpczBuffer);
+ void CloseCDROM(HANDLE hDevice);
+
+ void PrintAnalysis(int fs, int num_audio);
+
+ CCdInfo* GetCdInfo(char* cDeviceFileName=NULL);
+ void GetCdTextInfo(xbmc_cdtext_t &xcdt, int trackNum);
+
+protected:
+ int ReadBlock(int superblock, uint32_t offset, uint8_t bufnum, track_t track_num);
+ bool IsIt(int num);
+ int IsHFS(void);
+ int Is3DO(void);
+ int IsJoliet(void);
+ int IsUDF(void);
+ int GetSize(void);
+ int GetJolietLevel( void );
+ int GuessFilesystem(int start_session, track_t track_num);
+
+ uint32_t CddbDiscId();
+ int CddbDecDigitSum(int n);
+ unsigned int MsfSeconds(msf_t *msf);
+
+private:
+ char buffer[7][CDIO_CD_FRAMESIZE_RAW]; /* for CD-Data */
+ static signature_t sigs[17];
+ int i = 0, j = 0; /* index */
+ int m_nStartTrack; /* first sector of track */
+ int m_nIsofsSize; /* size of session */
+ int m_nJolietLevel;
+ int m_nMsOffset; /* multisession offset found by track-walking */
+ int m_nDataStart; /* start of data area */
+ int m_nFs;
+ int m_nUDFVerMinor;
+ int m_nUDFVerMajor;
+
+ CdIo* cdio;
+ track_t m_nNumTracks = CDIO_INVALID_TRACK;
+ track_t m_nFirstTrackNum = CDIO_INVALID_TRACK;
+
+ std::string m_strDiscLabel;
+
+ int m_nFirstData; /* # of first data track */
+ int m_nNumData; /* # of data tracks */
+ int m_nFirstAudio; /* # of first audio track */
+ int m_nNumAudio; /* # of audio tracks */
+
+ std::shared_ptr<CLibcdio> m_cdio;
+};
+
+}
diff --git a/xbmc/storage/discs/IDiscDriveHandler.h b/xbmc/storage/discs/IDiscDriveHandler.h
new file mode 100644
index 0000000..a3b007a
--- /dev/null
+++ b/xbmc/storage/discs/IDiscDriveHandler.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 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 <memory>
+#include <string>
+
+/*! \brief Represents the state of a disc (optical) drive */
+enum class DriveState
+{
+ /*! The drive is open */
+ OPEN,
+ /*! The drive is not ready (happens when openning or closing) */
+ NOT_READY,
+ /*! The drive is ready */
+ READY,
+ /*! The drive is closed but no media could be detected in the drive */
+ CLOSED_NO_MEDIA,
+ /*! The drive is closed and there is media in the drive */
+ CLOSED_MEDIA_PRESENT,
+ /*! The system does not have an optical drive */
+ NONE,
+ /*! The drive is closed but we don't know yet if there's media there */
+ CLOSED_MEDIA_UNDEFINED
+};
+
+/*! \brief Represents the state of the drive tray */
+enum class TrayState
+{
+ /*! The tray is in an undefined state, we don't know yet */
+ UNDEFINED,
+ /*! The tray is open */
+ OPEN,
+ /*! The tray is closed and doesn't have any optical media */
+ CLOSED_NO_MEDIA,
+ /*! The tray is closed and contains optical media */
+ CLOSED_MEDIA_PRESENT
+};
+
+/*! \brief Generic interface for platform disc drive handling
+*/
+class IDiscDriveHandler
+{
+public:
+ /*! \brief Get the optical drive state provided its device path
+ * \param devicePath the path for the device drive (e.g. /dev/sr0)
+ * \return The drive state
+ */
+ virtual DriveState GetDriveState(const std::string& devicePath) = 0;
+
+ /*! \brief Get the optical drive tray state provided the drive device path
+ * \param devicePath the path for the device drive (e.g. /dev/sr0)
+ * \return The drive state
+ */
+ virtual TrayState GetTrayState(const std::string& devicePath) = 0;
+
+ /*! \brief Eject the provided drive device
+ * \param devicePath the path for the device drive (e.g. /dev/sr0)
+ */
+ virtual void EjectDriveTray(const std::string& devicePath) = 0;
+
+ /*! \brief Close the provided drive device
+ * \note Some drives support closing appart from opening/eject
+ * \param devicePath the path for the device drive (e.g. /dev/sr0)
+ */
+ virtual void CloseDriveTray(const std::string& devicePath) = 0;
+
+ /*! \brief Toggle the state of a given drive device
+ *
+ * Will internally call EjectDriveTray or CloseDriveTray depending on
+ * the internal state of the drive (i.e. if open -> CloseDriveTray /
+ * if closed -> EjectDriveTray)
+ *
+ * \param devicePath the path for the device drive (e.g. /dev/sr0)
+ */
+ virtual void ToggleDriveTray(const std::string& devicePath) = 0;
+
+ /*! \brief Called to create platform-specific disc drive handler
+ *
+ * This method is used to create platform-specific disc drive handler
+ */
+ static std::shared_ptr<IDiscDriveHandler> CreateInstance();
+
+protected:
+ virtual ~IDiscDriveHandler() = default;
+ IDiscDriveHandler() = default;
+};
diff --git a/xbmc/system_egl.h b/xbmc/system_egl.h
new file mode 100644
index 0000000..4a8860b
--- /dev/null
+++ b/xbmc/system_egl.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2005-2020 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
+
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
+
+#if defined(WL_EGL_PLATFORM)
+#undef WL_EGL_PLATFORM
+#define KODI_WL_EGL_PLATFORM
+#endif
+
+#if defined(__GBM__)
+#undef __GBM__
+#define KODI__GBM__
+#endif
+
+#if defined(USE_X11)
+#undef USE_X11
+#define KODI_USE_X11
+#endif
+
+#define EGL_NO_X11
+#define MESA_EGL_NO_X11_HEADERS
+#endif
+
+#include <EGL/egl.h>
+
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
+
+#if defined(KODI_WL_EGL_PLATFORM)
+#undef KODI_WL_EGL_PLATFORM
+#define WL_EGL_PLATFORM
+#endif
+
+#if defined(KODI__GBM__)
+#undef KODI__GBM__
+#define __GBM__
+#endif
+
+#if defined(KODI_USE_X11)
+#undef KODI_USE_X11
+#define USE_X11
+#endif
+
+#endif
diff --git a/xbmc/system_gl.h b/xbmc/system_gl.h
new file mode 100644
index 0000000..04c4d55
--- /dev/null
+++ b/xbmc/system_gl.h
@@ -0,0 +1,37 @@
+/*
+ * 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
+
+#ifdef HAS_GL
+// always define GL_GLEXT_PROTOTYPES before include gl headers
+#if !defined(GL_GLEXT_PROTOTYPES)
+#define GL_GLEXT_PROTOTYPES
+#endif
+#if defined(TARGET_LINUX)
+#include <GL/gl.h>
+#include <GL/glext.h>
+#elif defined(TARGET_FREEBSD)
+#include <GL/gl.h>
+#elif defined(TARGET_DARWIN)
+#include <OpenGL/gl.h>
+#include <OpenGL/gl3.h>
+#include <OpenGL/gl3ext.h>
+#endif
+#elif HAS_GLES >= 2
+#if defined(TARGET_DARWIN)
+#include <OpenGLES/ES2/gl.h>
+#include <OpenGLES/ES2/glext.h>
+#else
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+#endif
+#if HAS_GLES == 3
+#include <GLES3/gl3.h>
+#endif
+#endif
diff --git a/xbmc/test/CMakeLists.txt b/xbmc/test/CMakeLists.txt
new file mode 100644
index 0000000..7187270
--- /dev/null
+++ b/xbmc/test/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(SOURCES TestBasicEnvironment.cpp
+ TestFileItem.cpp
+ TestTextureUtils.cpp
+ TestURL.cpp
+ TestUtil.cpp
+ TestUtils.cpp
+ TestDateTime.cpp
+ TestDateTimeSpan.cpp)
+
+set(HEADERS TestBasicEnvironment.h
+ TestUtils.h)
+
+core_add_test_library(xbmc_test)
diff --git a/xbmc/test/MtTestUtils.h b/xbmc/test/MtTestUtils.h
new file mode 100644
index 0000000..e41dd5e
--- /dev/null
+++ b/xbmc/test/MtTestUtils.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2005-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 "threads/SystemClock.h"
+
+#include <chrono>
+#include <thread>
+
+namespace ConditionPoll
+{
+/**
+ * This is usually enough time for the condition to have occurred in a test.
+ */
+constexpr unsigned int defaultTimeout{20000};
+
+/**
+ * poll until the lambda returns true or the timeout occurs. If the timeout occurs then
+ * the function will return false. Otherwise it will return true.
+ */
+template<typename L> inline bool poll(unsigned int timeoutMillis, L lambda)
+{
+ XbmcThreads::EndTime<> endTime{std::chrono::milliseconds(timeoutMillis)};
+ bool lastValue = false;
+ while (!endTime.IsTimePast() && (lastValue = lambda()) == false)
+ std::this_thread::sleep_for(std::chrono::milliseconds(50));
+ return lastValue;
+}
+
+/**
+ * poll until the lambda returns true or the defaultTimeout occurs. If the timeout occurs then
+ * the function will return false. Otherwise it will return true.
+ */
+template<typename L> inline bool poll(L lambda)
+{
+ return poll(defaultTimeout, lambda);
+}
+
+}
diff --git a/xbmc/test/TestBasicEnvironment.cpp b/xbmc/test/TestBasicEnvironment.cpp
new file mode 100644
index 0000000..7a35776
--- /dev/null
+++ b/xbmc/test/TestBasicEnvironment.cpp
@@ -0,0 +1,122 @@
+/*
+ * 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 "TestBasicEnvironment.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "ServiceManager.h"
+#include "TestUtils.h"
+#include "application/AppEnvironment.h"
+#include "application/AppParams.h"
+#include "application/Application.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "messaging/ApplicationMessenger.h"
+#include "platform/Filesystem.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "windowing/WinSystem.h"
+
+#ifdef TARGET_DARWIN
+#include "Util.h"
+#endif
+
+#include <cstdio>
+#include <cstdlib>
+#include <climits>
+#include <system_error>
+
+namespace fs = KODI::PLATFORM::FILESYSTEM;
+
+TestBasicEnvironment::TestBasicEnvironment() = default;
+
+void TestBasicEnvironment::SetUp()
+{
+ const auto params = std::make_shared<CAppParams>();
+ params->SetPlatformDirectories(false);
+
+ CAppEnvironment::SetUp(params);
+
+ CServiceBroker::RegisterAppMessenger(std::make_shared<KODI::MESSAGING::CApplicationMessenger>());
+
+ XFILE::CFile *f;
+
+ g_application.m_ServiceManager.reset(new CServiceManager());
+
+ if (!CXBMCTestUtils::Instance().SetReferenceFileBasePath())
+ SetUpError();
+ CXBMCTestUtils::Instance().setTestFileFactoryWriteInputFile(
+ XBMC_REF_FILE_PATH("xbmc/filesystem/test/reffile.txt")
+ );
+
+//for darwin set framework path - else we get assert
+//in guisettings init below
+#ifdef TARGET_DARWIN
+ std::string frameworksPath = CUtil::GetFrameworksPath();
+ CSpecialProtocol::SetXBMCFrameworksPath(frameworksPath);
+#endif
+ /**
+ * @todo Something should be done about all the asserts in GUISettings so
+ * that the initialization of these components won't be needed.
+ */
+
+ /* Create a temporary directory and set it to be used throughout the
+ * test suite run.
+ */
+
+ std::error_code ec;
+ m_tempPath = fs::create_temp_directory(ec);
+ if (ec)
+ {
+ TearDown();
+ SetUpError();
+ }
+
+ CSpecialProtocol::SetTempPath(m_tempPath);
+ CSpecialProtocol::SetProfilePath(m_tempPath);
+
+ /* Create and delete a tempfile to initialize the VFS (really to initialize
+ * CLibcdio). This is done so that the initialization of the VFS does not
+ * affect the performance results of the test cases.
+ */
+ /** @todo Make the initialization of the VFS here optional so it can be
+ * testable in a test case.
+ */
+ f = XBMC_CREATETEMPFILE("");
+ if (!f || !XBMC_DELETETEMPFILE(f))
+ {
+ TearDown();
+ SetUpError();
+ }
+
+ const CProfile profile("special://temp");
+ CServiceBroker::GetSettingsComponent()->GetProfileManager()->AddProfile(profile);
+ CServiceBroker::GetSettingsComponent()->GetProfileManager()->CreateProfileFolders();
+
+ if (!g_application.m_ServiceManager->InitForTesting())
+ exit(1);
+}
+
+void TestBasicEnvironment::TearDown()
+{
+ XFILE::CDirectory::RemoveRecursive(m_tempPath);
+
+ g_application.m_ServiceManager->DeinitTesting();
+
+ CServiceBroker::UnregisterAppMessenger();
+
+ CAppEnvironment::TearDown();
+}
+
+void TestBasicEnvironment::SetUpError()
+{
+ fprintf(stderr, "Setup of basic environment failed.\n");
+ exit(EXIT_FAILURE);
+}
diff --git a/xbmc/test/TestBasicEnvironment.h b/xbmc/test/TestBasicEnvironment.h
new file mode 100644
index 0000000..2b88dcc
--- /dev/null
+++ b/xbmc/test/TestBasicEnvironment.h
@@ -0,0 +1,25 @@
+/*
+ * 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 <string>
+
+#include <gtest/gtest.h>
+
+class TestBasicEnvironment : public testing::Environment
+{
+public:
+ TestBasicEnvironment();
+
+ void SetUp() override;
+ void TearDown() override;
+private:
+ void SetUpError();
+ std::string m_tempPath;
+};
diff --git a/xbmc/test/TestDateTime.cpp b/xbmc/test/TestDateTime.cpp
new file mode 100644
index 0000000..7e51df8
--- /dev/null
+++ b/xbmc/test/TestDateTime.cpp
@@ -0,0 +1,660 @@
+/*
+ * 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 "LangInfo.h"
+#include "XBDateTime.h"
+#include "guilib/LocalizeStrings.h"
+
+#include <array>
+#include <iostream>
+
+#include <gtest/gtest.h>
+
+class TestDateTime : public testing::Test
+{
+protected:
+ TestDateTime() = default;
+ ~TestDateTime() override = default;
+};
+
+TEST_F(TestDateTime, DateTimeOperators)
+{
+ CDateTime dateTime1(1991, 5, 14, 12, 34, 56);
+ CDateTime dateTime2(1991, 5, 14, 12, 34, 57);
+
+ EXPECT_TRUE(dateTime1 < dateTime2);
+ EXPECT_FALSE(dateTime1 > dateTime2);
+ EXPECT_FALSE(dateTime1 == dateTime2);
+}
+
+TEST_F(TestDateTime, FileTimeOperators)
+{
+ CDateTime dateTime1(1991, 5, 14, 12, 34, 56);
+ CDateTime dateTime2(1991, 5, 14, 12, 34, 57);
+
+ KODI::TIME::FileTime fileTime1;
+ KODI::TIME::FileTime fileTime2;
+
+ dateTime1.GetAsTimeStamp(fileTime1);
+ dateTime2.GetAsTimeStamp(fileTime2);
+
+ CDateTime dateTime3(fileTime1);
+
+ EXPECT_TRUE(dateTime3 < fileTime2);
+ EXPECT_FALSE(dateTime3 > fileTime2);
+ EXPECT_FALSE(dateTime3 == fileTime2);
+}
+
+TEST_F(TestDateTime, SystemTimeOperators)
+{
+ CDateTime dateTime1(1991, 5, 14, 12, 34, 56);
+ CDateTime dateTime2(1991, 5, 14, 12, 34, 57);
+
+ KODI::TIME::SystemTime systemTime;
+ dateTime2.GetAsSystemTime(systemTime);
+
+ EXPECT_TRUE(dateTime1 < systemTime);
+ EXPECT_FALSE(dateTime1 > systemTime);
+ EXPECT_FALSE(dateTime1 == systemTime);
+}
+
+TEST_F(TestDateTime, TimeTOperators)
+{
+ CDateTime dateTime1(1991, 5, 14, 12, 34, 56);
+ CDateTime dateTime2(1991, 5, 14, 12, 34, 57);
+
+ time_t time;
+ dateTime2.GetAsTime(time);
+
+ EXPECT_TRUE(dateTime1 < time);
+ EXPECT_FALSE(dateTime1 > time);
+ EXPECT_FALSE(dateTime1 == time);
+}
+
+TEST_F(TestDateTime, TmOperators)
+{
+ {
+ CDateTime dateTime1(1991, 5, 14, 12, 34, 56);
+
+ tm t1;
+ dateTime1.GetAsTm(t1);
+
+ EXPECT_FALSE(dateTime1 < t1);
+ EXPECT_FALSE(dateTime1 > t1);
+ EXPECT_TRUE(dateTime1 == t1);
+
+ CDateTime dateTime2(1991, 5, 14, 12, 34, 57);
+
+ tm t2;
+ dateTime2.GetAsTm(t2);
+
+ EXPECT_TRUE(dateTime1 < t2);
+ EXPECT_FALSE(dateTime1 > t2);
+ EXPECT_FALSE(dateTime1 == t2);
+ }
+
+ // same test but opposite daylight saving
+ {
+ CDateTime dateTime1(1991, 1, 14, 12, 34, 56);
+
+ tm t1;
+ dateTime1.GetAsTm(t1);
+
+ EXPECT_FALSE(dateTime1 < t1);
+ EXPECT_FALSE(dateTime1 > t1);
+ EXPECT_TRUE(dateTime1 == t1);
+
+ CDateTime dateTime2(1991, 1, 14, 12, 34, 57);
+
+ tm t2;
+ dateTime2.GetAsTm(t2);
+
+ EXPECT_TRUE(dateTime1 < t2);
+ EXPECT_FALSE(dateTime1 > t2);
+ EXPECT_FALSE(dateTime1 == t2);
+ }
+}
+
+// no way to test this platform agnostically (for now) so just log it.
+TEST_F(TestDateTime, GetCurrentDateTime)
+{
+ auto date = CDateTime::GetCurrentDateTime();
+ std::cout << "Current Date: " << date.GetAsDBDateTime() << std::endl;
+}
+
+// no way to test this platform agnostically (for now) so just log it.
+TEST_F(TestDateTime, GetUTCDateTime)
+{
+ auto date = CDateTime::GetUTCDateTime();
+ std::cout << "Current Date UTC: " << date.GetAsDBDateTime() << std::endl;
+}
+
+TEST_F(TestDateTime, MonthStringToMonthNum)
+{
+ std::array<std::pair<std::string, std::string>, 12> months = {{
+ {"Jan", "January"},
+ {"Feb", "February"},
+ {"Mar", "March"},
+ {"Apr", "April"},
+ {"May", "May"},
+ {"Jun", "June"},
+ {"Jul", "July"},
+ {"Aug", "August"},
+ {"Sep", "September"},
+ {"Oct", "October"},
+ {"Nov", "November"},
+ {"Dec", "December"},
+ }};
+
+ int i = 1;
+ for (const auto& month : months)
+ {
+ EXPECT_EQ(CDateTime::MonthStringToMonthNum(month.first), i);
+ EXPECT_EQ(CDateTime::MonthStringToMonthNum(month.second), i);
+ i++;
+ }
+}
+
+// this method is broken as SetFromDBDate() will return true
+TEST_F(TestDateTime, DISABLED_SetFromDateString)
+{
+ CDateTime dateTime;
+ EXPECT_TRUE(dateTime.SetFromDateString("tuesday may 14, 1991"));
+
+ std::cout << "year: " << dateTime.GetYear() << std::endl;
+ std::cout << "month: " << dateTime.GetMonth() << std::endl;
+ std::cout << "day: " << dateTime.GetDay() << std::endl;
+
+ EXPECT_EQ(dateTime.GetYear(), 1991);
+ EXPECT_EQ(dateTime.GetMonth(), 5);
+ EXPECT_EQ(dateTime.GetDay(), 14);
+}
+
+TEST_F(TestDateTime, SetFromDBDate)
+{
+ CDateTime dateTime;
+ EXPECT_TRUE(dateTime.SetFromDBDate("1991-05-14"));
+ EXPECT_EQ(dateTime.GetYear(), 1991);
+ EXPECT_EQ(dateTime.GetMonth(), 5);
+ EXPECT_EQ(dateTime.GetDay(), 14);
+
+ dateTime.Reset();
+ EXPECT_TRUE(dateTime.SetFromDBDate("02-01-1993"));
+ EXPECT_EQ(dateTime.GetYear(), 1993);
+ EXPECT_EQ(dateTime.GetMonth(), 1);
+ EXPECT_EQ(dateTime.GetDay(), 2);
+}
+
+// disabled on osx and freebsd as their mktime functions
+// don't work for dates before 1900
+#if defined(TARGET_DARWIN_OSX) || defined(TARGET_FREEBSD)
+TEST_F(TestDateTime, DISABLED_SetFromDBTime)
+#else
+TEST_F(TestDateTime, SetFromDBTime)
+#endif
+{
+ CDateTime dateTime1;
+ EXPECT_TRUE(dateTime1.SetFromDBTime("12:34"));
+ EXPECT_EQ(dateTime1.GetHour(), 12);
+ EXPECT_EQ(dateTime1.GetMinute(), 34);
+ EXPECT_EQ(dateTime1.GetSecond(), 0);
+
+ CDateTime dateTime2;
+ EXPECT_TRUE(dateTime2.SetFromDBTime("12:34:56"));
+ EXPECT_EQ(dateTime2.GetHour(), 12);
+ EXPECT_EQ(dateTime2.GetMinute(), 34);
+ EXPECT_EQ(dateTime2.GetSecond(), 56);
+}
+
+TEST_F(TestDateTime, SetFromDBDateTime)
+{
+ CDateTime dateTime;
+ EXPECT_TRUE(dateTime.SetFromDBDateTime("1991-05-14 12:34:56"));
+ EXPECT_EQ(dateTime.GetYear(), 1991);
+ EXPECT_EQ(dateTime.GetMonth(), 5);
+ EXPECT_EQ(dateTime.GetDay(), 14);
+ EXPECT_EQ(dateTime.GetHour(), 12);
+ EXPECT_EQ(dateTime.GetMinute(), 34);
+ EXPECT_EQ(dateTime.GetSecond(), 56);
+}
+
+TEST_F(TestDateTime, SetFromW3CDate)
+{
+ CDateTime dateTime;
+ EXPECT_TRUE(dateTime.SetFromW3CDate("1994-11-05T13:15:30Z"));
+ EXPECT_EQ(dateTime.GetYear(), 1994);
+ EXPECT_EQ(dateTime.GetMonth(), 11);
+ EXPECT_EQ(dateTime.GetDay(), 5);
+ EXPECT_EQ(dateTime.GetHour(), 0);
+ EXPECT_EQ(dateTime.GetMinute(), 0);
+ EXPECT_EQ(dateTime.GetSecond(), 0);
+}
+
+TEST_F(TestDateTime, SetFromW3CDateTime)
+{
+ CDateTimeSpan bias = CDateTime::GetTimezoneBias();
+ CDateTime dateTime;
+ dateTime.SetFromDBDateTime("1994-11-05 13:15:30");
+ dateTime += bias;
+ std::string dateTimeStr = dateTime.GetAsDBDate() + "T" + dateTime.GetAsDBTime() + "Z";
+
+ CDateTime dateTime1;
+ EXPECT_TRUE(dateTime1.SetFromW3CDateTime(dateTimeStr));
+ EXPECT_EQ(dateTime1.GetYear(), 1994);
+ EXPECT_EQ(dateTime1.GetMonth(), 11);
+ EXPECT_EQ(dateTime1.GetDay(), 5);
+ EXPECT_EQ(dateTime1.GetHour(), 13);
+ EXPECT_EQ(dateTime1.GetMinute(), 15);
+ EXPECT_EQ(dateTime1.GetSecond(), 30);
+
+ CDateTime dateTime2;
+ EXPECT_TRUE(dateTime2.SetFromW3CDateTime("1994-11-05T08:15:30-05:00"));
+ EXPECT_EQ(dateTime2.GetYear(), 1994);
+ EXPECT_EQ(dateTime2.GetMonth(), 11);
+ EXPECT_EQ(dateTime2.GetDay(), 5);
+ EXPECT_EQ(dateTime2.GetHour(), 13);
+ EXPECT_EQ(dateTime2.GetMinute(), 15);
+ EXPECT_EQ(dateTime2.GetSecond(), 30);
+}
+
+TEST_F(TestDateTime, SetFromUTCDateTime)
+{
+ CDateTimeSpan bias = CDateTime::GetTimezoneBias();
+
+ CDateTime dateTime1;
+ dateTime1.SetFromDBDateTime("1991-05-14 12:34:56");
+ dateTime1 += bias;
+
+ CDateTime dateTime2;
+ EXPECT_TRUE(dateTime2.SetFromUTCDateTime(dateTime1));
+ EXPECT_EQ(dateTime2.GetYear(), 1991);
+ EXPECT_EQ(dateTime2.GetMonth(), 5);
+ EXPECT_EQ(dateTime2.GetDay(), 14);
+ EXPECT_EQ(dateTime2.GetHour(), 12);
+ EXPECT_EQ(dateTime2.GetMinute(), 34);
+ EXPECT_EQ(dateTime2.GetSecond(), 56);
+
+ const time_t time = 674224496 + bias.GetSecondsTotal();
+
+ CDateTime dateTime3;
+ EXPECT_TRUE(dateTime3.SetFromUTCDateTime(time));
+ EXPECT_EQ(dateTime3.GetYear(), 1991);
+ EXPECT_EQ(dateTime3.GetMonth(), 5);
+ EXPECT_EQ(dateTime3.GetDay(), 14);
+ EXPECT_EQ(dateTime3.GetHour(), 12);
+ EXPECT_EQ(dateTime3.GetMinute(), 34);
+ EXPECT_EQ(dateTime3.GetSecond(), 56);
+}
+
+TEST_F(TestDateTime, SetFromRFC1123DateTime)
+{
+ std::string dateTime1("Mon, 21 Oct 2018 12:16:24 GMT");
+
+ CDateTime dateTime2;
+ EXPECT_TRUE(dateTime2.SetFromRFC1123DateTime(dateTime1));
+ EXPECT_EQ(dateTime2.GetYear(), 2018);
+ EXPECT_EQ(dateTime2.GetMonth(), 10);
+ EXPECT_EQ(dateTime2.GetDay(), 21);
+ EXPECT_EQ(dateTime2.GetHour(), 12);
+ EXPECT_EQ(dateTime2.GetMinute(), 16);
+ EXPECT_EQ(dateTime2.GetSecond(), 24);
+}
+
+TEST_F(TestDateTime, SetDateTime)
+{
+ CDateTime dateTime1;
+ EXPECT_TRUE(dateTime1.SetDateTime(1991, 05, 14, 12, 34, 56));
+ EXPECT_EQ(dateTime1.GetYear(), 1991);
+ EXPECT_EQ(dateTime1.GetMonth(), 5);
+ EXPECT_EQ(dateTime1.GetDay(), 14);
+ EXPECT_EQ(dateTime1.GetHour(), 12);
+ EXPECT_EQ(dateTime1.GetMinute(), 34);
+ EXPECT_EQ(dateTime1.GetSecond(), 56);
+
+ CDateTime dateTime2;
+ EXPECT_TRUE(dateTime2.SetDate(1991, 05, 14));
+ EXPECT_EQ(dateTime2.GetYear(), 1991);
+ EXPECT_EQ(dateTime2.GetMonth(), 5);
+ EXPECT_EQ(dateTime2.GetDay(), 14);
+ EXPECT_EQ(dateTime2.GetHour(), 0);
+ EXPECT_EQ(dateTime2.GetMinute(), 0);
+ EXPECT_EQ(dateTime2.GetSecond(), 0);
+
+// disabled on osx and freebsd as their mktime functions
+// don't work for dates before 1900
+#if !defined(TARGET_DARWIN_OSX) && !defined(TARGET_FREEBSD)
+ CDateTime dateTime3;
+ EXPECT_TRUE(dateTime3.SetTime(12, 34, 56));
+ EXPECT_EQ(dateTime3.GetYear(), 1601);
+ EXPECT_EQ(dateTime3.GetMonth(), 1);
+ EXPECT_EQ(dateTime3.GetDay(), 1);
+ EXPECT_EQ(dateTime3.GetHour(), 12);
+ EXPECT_EQ(dateTime3.GetMinute(), 34);
+ EXPECT_EQ(dateTime3.GetSecond(), 56);
+#endif
+}
+
+TEST_F(TestDateTime, GetAsStrings)
+{
+ CDateTime dateTime;
+ dateTime.SetDateTime(1991, 05, 14, 12, 34, 56);
+
+ EXPECT_EQ(dateTime.GetAsSaveString(), "19910514_123456");
+ EXPECT_EQ(dateTime.GetAsDBDateTime(), "1991-05-14 12:34:56");
+ EXPECT_EQ(dateTime.GetAsDBDate(), "1991-05-14");
+ EXPECT_EQ(dateTime.GetAsDBTime(), "12:34:56");
+ EXPECT_EQ(dateTime.GetAsW3CDate(), "1991-05-14");
+}
+
+// disabled because we have no way to validate these values
+// GetTimezoneBias() always returns a positive value so
+// there is no way to detect the direction of the offset
+TEST_F(TestDateTime, DISABLED_GetAsStringsWithBias)
+{
+ CDateTimeSpan bias = CDateTime::GetTimezoneBias();
+
+ CDateTime dateTime;
+ dateTime.SetDateTime(1991, 05, 14, 12, 34, 56);
+
+ CDateTime dateTimeWithBias(dateTime);
+ dateTimeWithBias += bias;
+
+ EXPECT_EQ(dateTime.GetAsRFC1123DateTime(), "Tue, 14 May 1991 20:34:56 GMT");
+ EXPECT_EQ(dateTime.GetAsW3CDateTime(false), "1991-05-14T12:34:56+08:00");
+ EXPECT_EQ(dateTime.GetAsW3CDateTime(true), "1991-05-14T20:34:56Z");
+}
+
+TEST_F(TestDateTime, GetAsLocalized)
+{
+ // short date formats using "/"
+ // "DD/MM/YYYY",
+ // "MM/DD/YYYY",
+ // "YYYY/MM/DD",
+ // "D/M/YYYY",
+ // short date formats using "-"
+ // "DD-MM-YYYY",
+ // "MM-DD-YYYY",
+ // "YYYY-MM-DD",
+ // "YYYY-M-D",
+ // short date formats using "."
+ // "DD.MM.YYYY",
+ // "DD.M.YYYY",
+ // "D.M.YYYY",
+ // "D. M. YYYY",
+ // "YYYY.MM.DD"
+
+ // "DDDD, D MMMM YYYY",
+ // "DDDD, DD MMMM YYYY",
+ // "DDDD, D. MMMM YYYY",
+ // "DDDD, DD. MMMM YYYY",
+ // "DDDD, MMMM D, YYYY",
+ // "DDDD, MMMM DD, YYYY",
+ // "DDDD D MMMM YYYY",
+ // "DDDD DD MMMM YYYY",
+ // "DDDD D. MMMM YYYY",
+ // "DDDD DD. MMMM YYYY",
+ // "D. MMMM YYYY",
+ // "DD. MMMM YYYY",
+ // "D. MMMM. YYYY",
+ // "DD. MMMM. YYYY",
+ // "YYYY. MMMM. D"
+
+ ASSERT_TRUE(g_localizeStrings.Load(g_langInfo.GetLanguagePath(), "resource.language.en_gb"));
+
+ // 24 hour clock must be set before time format
+ g_langInfo.Set24HourClock(false);
+ g_langInfo.SetTimeFormat("hh:mm:ss");
+
+ g_langInfo.SetShortDateFormat("MM/DD/YYYY");
+ g_langInfo.SetLongDateFormat("DDDD, DD MMMM YYYY");
+
+ CDateTime dateTime1;
+ dateTime1.SetDateTime(1991, 05, 14, 12, 34, 56);
+
+ // std::cout << "GetAsLocalizedDate: " << dateTime1.GetAsLocalizedDate(false) << std::endl;
+ // std::cout << "GetAsLocalizedDate: " << dateTime1.GetAsLocalizedDate(true) << std::endl;
+ // std::cout << "GetAsLocalizedDate: " << dateTime1.GetAsLocalizedDate(std::string("dd-mm-yyyy")) << std::endl;
+ // std::cout << "GetAsLocalizedTime: " << dateTime1.GetAsLocalizedTime("hh-mm-ss", true) << std::endl;
+ // std::cout << "GetAsLocalizedTime: " << dateTime1.GetAsLocalizedTime("hh-mm-ss", false)
+ // << std::endl;
+ // std::cout << "GetAsLocalizedDateTime: " << dateTime1.GetAsLocalizedDateTime(false, false)
+ // << std::endl;
+ // std::cout << "GetAsLocalizedDateTime: " << dateTime1.GetAsLocalizedDateTime(true, true)
+ // << std::endl;
+ // std::cout << "GetAsLocalizedTime: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(0), false)
+ // << std::endl;
+ // std::cout << "GetAsLocalizedTime: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(0), true)
+ // << std::endl;
+
+ // std::cout << "1: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(1)) << std::endl;
+ // std::cout << "2: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(2)) << std::endl;
+ // std::cout << "3: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(3)) << std::endl;
+ // std::cout << "4: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(4)) << std::endl;
+ // std::cout << "5: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(5)) << std::endl;
+ // std::cout << "6: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(6)) << std::endl;
+ // std::cout << "7: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(7)) << std::endl;
+ // std::cout << "8: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(8)) << std::endl;
+ // std::cout << "14: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(14)) << std::endl;
+ // std::cout << "15: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(15)) << std::endl;
+ // std::cout << "16: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(16)) << std::endl;
+ // std::cout << "19: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(19)) << std::endl;
+ // std::cout << "27: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(27)) << std::endl;
+ // std::cout << "32: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(32)) << std::endl;
+ // std::cout << "64: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(64)) << std::endl;
+ // std::cout << "128: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(128)) << std::endl;
+ // std::cout << "256: " << dateTime1.GetAsLocalizedTime(TIME_FORMAT(256)) << std::endl;
+
+ EXPECT_EQ(dateTime1.GetAsLocalizedDate(false), "05/14/1991");
+ EXPECT_EQ(dateTime1.GetAsLocalizedDate(true), "Tuesday, 14 May 1991");
+ EXPECT_EQ(dateTime1.GetAsLocalizedDate(std::string("dd-mm-yyyy")),
+ "14-05-1991"); // need to force overload function
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime("hh-mm-ss", true), "12-34-56");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime("hh-mm-ss", false), "12-34");
+ EXPECT_EQ(dateTime1.GetAsLocalizedDateTime(false, false), "05/14/1991 12:34");
+ EXPECT_EQ(dateTime1.GetAsLocalizedDateTime(true, true), "Tuesday, 14 May 1991 12:34:56");
+
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(0), false), "12:34");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(0), true), "12:34:56");
+
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(1)), "56");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(2)), "34");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(3)), "34:56");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(4)), "12");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(5)), "12:56");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(6)), "12:34");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(7)), "12:34:56");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(8)), "PM");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(14)), "12:34 PM");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(15)), "12:34:56 PM");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(16)), "12");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(19)), "12:34:56");
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(27)), "12:34:56 PM");
+
+ // not possible to use these three
+ // EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(32)), "");
+ // EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(64)), "");
+ // EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(128)), "");
+
+ EXPECT_EQ(dateTime1.GetAsLocalizedTime(TIME_FORMAT(256)), "34");
+
+
+ // 24 hour clock must be set before time format
+ g_langInfo.Set24HourClock(true);
+ g_langInfo.SetTimeFormat("h:m:s");
+
+ g_langInfo.SetShortDateFormat("YYYY-M-D");
+ g_langInfo.SetLongDateFormat("DDDD, MMMM D, YYYY");
+
+ CDateTime dateTime2;
+ dateTime2.SetDateTime(2020, 2, 3, 4, 5, 6);
+
+ // std::cout << "GetAsLocalizedDate: " << dateTime2.GetAsLocalizedDate(false) << std::endl;
+ // std::cout << "GetAsLocalizedDate: " << dateTime2.GetAsLocalizedDate(true) << std::endl;
+ // std::cout << "GetAsLocalizedDate: " << dateTime2.GetAsLocalizedDate(std::string("dd-mm-yyyy")) << std::endl;
+ // std::cout << "GetAsLocalizedTime: " << dateTime2.GetAsLocalizedTime("hh-mm-ss", true) << std::endl;
+ // std::cout << "GetAsLocalizedTime: " << dateTime2.GetAsLocalizedTime("hh-mm-ss", false)
+ // << std::endl;
+ // std::cout << "GetAsLocalizedDateTime: " << dateTime2.GetAsLocalizedDateTime(false, false)
+ // << std::endl;
+ // std::cout << "GetAsLocalizedDateTime: " << dateTime2.GetAsLocalizedDateTime(true, true)
+ // << std::endl;
+ // std::cout << "GetAsLocalizedTime: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(0), false)
+ // << std::endl;
+ // std::cout << "GetAsLocalizedTime: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(0), true)
+ // << std::endl;
+
+ // std::cout << "1: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(1)) << std::endl;
+ // std::cout << "2: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(2)) << std::endl;
+ // std::cout << "3: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(3)) << std::endl;
+ // std::cout << "4: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(4)) << std::endl;
+ // std::cout << "5: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(5)) << std::endl;
+ // std::cout << "6: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(6)) << std::endl;
+ // std::cout << "7: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(7)) << std::endl;
+ // std::cout << "8: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(8)) << std::endl;
+ // std::cout << "14: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(14)) << std::endl;
+ // std::cout << "15: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(15)) << std::endl;
+ // std::cout << "16: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(16)) << std::endl;
+ // std::cout << "19: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(19)) << std::endl;
+ // std::cout << "27: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(27)) << std::endl;
+ // std::cout << "32: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(32)) << std::endl;
+ // std::cout << "64: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(64)) << std::endl;
+ // std::cout << "128: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(128)) << std::endl;
+ // std::cout << "256: " << dateTime2.GetAsLocalizedTime(TIME_FORMAT(256)) << std::endl;
+
+ EXPECT_EQ(dateTime2.GetAsLocalizedDate(false), "2020-2-3");
+ EXPECT_EQ(dateTime2.GetAsLocalizedDate(true), "Monday, February 3, 2020");
+ EXPECT_EQ(dateTime2.GetAsLocalizedDate(std::string("dd-mm-yyyy")),
+ "03-02-2020"); // need to force overload function
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime("hh-mm-ss", true), "04-05-06");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime("hh-mm-ss", false), "04-05");
+ EXPECT_EQ(dateTime2.GetAsLocalizedDateTime(false, false), "2020-2-3 4:5");
+ EXPECT_EQ(dateTime2.GetAsLocalizedDateTime(true, true), "Monday, February 3, 2020 4:5:6");
+
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(0), false), "4:5");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(0), true), "4:5:6");
+
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(1)), "06");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(2)), "05");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(3)), "05:06");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(4)), "04");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(5)), "04:06");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(6)), "04:05");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(7)), "04:05:06");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(8)), "");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(14)), "04:05");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(15)), "04:05:06");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(16)), "4");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(19)), "4:05:06");
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(27)), "4:05:06 AM");
+
+ // not possible to use these three
+ // EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(32)), "");
+ // EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(64)), "");
+ // EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(128)), "");
+
+ EXPECT_EQ(dateTime2.GetAsLocalizedTime(TIME_FORMAT(256)), "5");
+}
+
+TEST_F(TestDateTime, GetAsSystemTime)
+{
+ CDateTime dateTime;
+ dateTime.SetDateTime(1991, 05, 14, 12, 34, 56);
+
+ KODI::TIME::SystemTime systemTime;
+ dateTime.GetAsSystemTime(systemTime);
+
+ EXPECT_TRUE(dateTime == systemTime);
+}
+
+TEST_F(TestDateTime, GetAsTime)
+{
+ CDateTime dateTime;
+ dateTime.SetDateTime(1991, 05, 14, 12, 34, 56);
+
+ time_t time;
+ dateTime.GetAsTime(time);
+
+ EXPECT_TRUE(dateTime == time);
+}
+
+TEST_F(TestDateTime, GetAsTm)
+{
+ {
+ CDateTime dateTime;
+ dateTime.SetDateTime(1991, 05, 14, 12, 34, 56);
+
+ tm time;
+ dateTime.GetAsTm(time);
+ EXPECT_TRUE(dateTime == time);
+ }
+
+ // same test but opposite daylight saving
+ {
+ CDateTime dateTime;
+ dateTime.SetDateTime(1991, 01, 14, 12, 34, 56);
+
+ tm time;
+ dateTime.GetAsTm(time);
+ EXPECT_TRUE(dateTime == time);
+ }
+}
+
+// Disabled pending std::chrono and std::date changes.
+TEST_F(TestDateTime, DISABLED_GetAsTimeStamp)
+{
+ CDateTimeSpan bias = CDateTime::GetTimezoneBias();
+
+ CDateTime dateTime;
+ dateTime.SetDateTime(1991, 05, 14, 12, 34, 56);
+
+ KODI::TIME::FileTime fileTime;
+ dateTime.GetAsTimeStamp(fileTime);
+ dateTime += bias;
+
+ EXPECT_TRUE(dateTime == fileTime);
+}
+
+TEST_F(TestDateTime, GetAsUTCDateTime)
+{
+ CDateTimeSpan bias = CDateTime::GetTimezoneBias();
+
+ CDateTime dateTime1;
+ dateTime1.SetDateTime(1991, 05, 14, 12, 34, 56);
+
+ CDateTime dateTime2;
+ dateTime2 = dateTime1.GetAsUTCDateTime();
+ dateTime2 -= bias;
+
+ EXPECT_EQ(dateTime2.GetYear(), 1991);
+ EXPECT_EQ(dateTime2.GetMonth(), 5);
+ EXPECT_EQ(dateTime2.GetDay(), 14);
+ EXPECT_EQ(dateTime2.GetHour(), 12);
+ EXPECT_EQ(dateTime2.GetMinute(), 34);
+ EXPECT_EQ(dateTime2.GetSecond(), 56);
+}
+
+// disabled on osx and freebsd as their mktime functions
+// don't work for dates before 1900
+#if defined(TARGET_DARWIN_OSX) || defined(TARGET_FREEBSD)
+TEST_F(TestDateTime, DISABLED_Reset)
+#else
+TEST_F(TestDateTime, Reset)
+#endif
+{
+ CDateTime dateTime;
+ dateTime.SetDateTime(1991, 05, 14, 12, 34, 56);
+
+ dateTime.Reset();
+
+ EXPECT_EQ(dateTime.GetYear(), 1601);
+ EXPECT_EQ(dateTime.GetMonth(), 1);
+ EXPECT_EQ(dateTime.GetDay(), 1);
+ EXPECT_EQ(dateTime.GetHour(), 0);
+ EXPECT_EQ(dateTime.GetMinute(), 0);
+ EXPECT_EQ(dateTime.GetSecond(), 0);
+}
diff --git a/xbmc/test/TestDateTimeSpan.cpp b/xbmc/test/TestDateTimeSpan.cpp
new file mode 100644
index 0000000..2ebdc59
--- /dev/null
+++ b/xbmc/test/TestDateTimeSpan.cpp
@@ -0,0 +1,80 @@
+/*
+ * 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 "XBDateTime.h"
+
+#include <gtest/gtest.h>
+
+class TestDateTimeSpan : public testing::Test
+{
+protected:
+ TestDateTimeSpan() = default;
+ ~TestDateTimeSpan() override = default;
+};
+
+TEST_F(TestDateTimeSpan, Operators)
+{
+ CDateTimeSpan timeSpan1(1, 1, 1, 1);
+ CDateTimeSpan timeSpan2(2, 2, 2, 2);
+
+ EXPECT_FALSE(timeSpan1 > timeSpan2);
+ EXPECT_TRUE(timeSpan1 < timeSpan2);
+
+ CDateTimeSpan timeSpan3(timeSpan1);
+ EXPECT_TRUE(timeSpan1 == timeSpan3);
+
+ EXPECT_TRUE((timeSpan1 + timeSpan3) == timeSpan2);
+ EXPECT_TRUE((timeSpan2 - timeSpan3) == timeSpan1);
+
+ timeSpan1 += timeSpan3;
+ EXPECT_TRUE(timeSpan1 == timeSpan2);
+
+ timeSpan1 -= timeSpan3;
+ EXPECT_TRUE(timeSpan1 == timeSpan3);
+}
+
+TEST_F(TestDateTimeSpan, SetDateTimeSpan)
+{
+ CDateTimeSpan timeSpan;
+ int days = 1;
+ int hours = 2;
+ int minutes = 3;
+ int seconds = 4;
+
+ int secondsTotal = (days * 24 * 60 * 60) + (hours * 60 * 60) + (minutes * 60) + seconds;
+
+ timeSpan.SetDateTimeSpan(days, hours, minutes, seconds);
+ EXPECT_EQ(timeSpan.GetDays(), days);
+ EXPECT_EQ(timeSpan.GetHours(), hours);
+ EXPECT_EQ(timeSpan.GetMinutes(), minutes);
+ EXPECT_EQ(timeSpan.GetSeconds(), seconds);
+ EXPECT_EQ(timeSpan.GetSecondsTotal(), secondsTotal);
+}
+
+TEST_F(TestDateTimeSpan, SetFromPeriod)
+{
+ CDateTimeSpan timeSpan;
+
+ timeSpan.SetFromPeriod("3");
+ EXPECT_EQ(timeSpan.GetDays(), 3);
+
+ timeSpan.SetFromPeriod("3weeks");
+ EXPECT_EQ(timeSpan.GetDays(), 21);
+
+ timeSpan.SetFromPeriod("3months");
+ EXPECT_EQ(timeSpan.GetDays(), 93);
+}
+
+TEST_F(TestDateTimeSpan, SetFromTimeString)
+{
+ CDateTimeSpan timeSpan;
+
+ timeSpan.SetFromTimeString("12:34");
+ EXPECT_EQ(timeSpan.GetHours(), 12);
+ EXPECT_EQ(timeSpan.GetMinutes(), 34);
+}
diff --git a/xbmc/test/TestFileItem.cpp b/xbmc/test/TestFileItem.cpp
new file mode 100644
index 0000000..e78ba27
--- /dev/null
+++ b/xbmc/test/TestFileItem.cpp
@@ -0,0 +1,132 @@
+/*
+ * 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 "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/SettingsManager.h"
+
+#include <gtest/gtest.h>
+
+using ::testing::Test;
+using ::testing::WithParamInterface;
+using ::testing::ValuesIn;
+
+struct TestFileData
+{
+ const char *file;
+ bool use_folder;
+ const char *base;
+};
+
+class AdvancedSettingsResetBase : public Test
+{
+public:
+ AdvancedSettingsResetBase();
+};
+
+AdvancedSettingsResetBase::AdvancedSettingsResetBase()
+{
+ // Force all advanced settings to be reset to defaults
+ const auto settings = CServiceBroker::GetSettingsComponent();
+ CSettingsManager* settingsMgr = settings->GetSettings()->GetSettingsManager();
+ settings->GetAdvancedSettings()->Uninitialize(*settingsMgr);
+ settings->GetAdvancedSettings()->Initialize(*settingsMgr);
+}
+
+class TestFileItemSpecifiedArtJpg : public AdvancedSettingsResetBase,
+ public WithParamInterface<TestFileData>
+{
+};
+
+
+TEST_P(TestFileItemSpecifiedArtJpg, GetLocalArt)
+{
+ CFileItem item;
+ item.SetPath(GetParam().file);
+ std::string path = CURL(item.GetLocalArt("art.jpg", GetParam().use_folder)).Get();
+ std::string compare = CURL(GetParam().base).Get();
+ EXPECT_EQ(compare, path);
+}
+
+const TestFileData MovieFiles[] = {{ "c:\\dir\\filename.avi", false, "c:\\dir\\filename-art.jpg" },
+ { "c:\\dir\\filename.avi", true, "c:\\dir\\art.jpg" },
+ { "/dir/filename.avi", false, "/dir/filename-art.jpg" },
+ { "/dir/filename.avi", true, "/dir/art.jpg" },
+ { "smb://somepath/file.avi", false, "smb://somepath/file-art.jpg" },
+ { "smb://somepath/file.avi", true, "smb://somepath/art.jpg" },
+ { "stack:///path/to/movie-cd1.avi , /path/to/movie-cd2.avi", false, "/path/to/movie-art.jpg" },
+ { "stack:///path/to/movie-cd1.avi , /path/to/movie-cd2.avi", true, "/path/to/art.jpg" },
+ { "stack:///path/to/movie_name/cd1/some_file1.avi , /path/to/movie_name/cd2/some_file2.avi", true, "/path/to/movie_name/art.jpg" },
+ { "/home/user/TV Shows/Dexter/S1/1x01.avi", false, "/home/user/TV Shows/Dexter/S1/1x01-art.jpg" },
+ { "/home/user/TV Shows/Dexter/S1/1x01.avi", true, "/home/user/TV Shows/Dexter/S1/art.jpg" },
+ { "zip://g%3a%5cmultimedia%5cmovies%5cSphere%2ezip/Sphere.avi", false, "g:\\multimedia\\movies\\Sphere-art.jpg" },
+ { "zip://g%3a%5cmultimedia%5cmovies%5cSphere%2ezip/Sphere.avi", true, "g:\\multimedia\\movies\\art.jpg" },
+ { "/home/user/movies/movie_name/video_ts/VIDEO_TS.IFO", false, "/home/user/movies/movie_name/art.jpg" },
+ { "/home/user/movies/movie_name/video_ts/VIDEO_TS.IFO", true, "/home/user/movies/movie_name/art.jpg" },
+ { "/home/user/movies/movie_name/BDMV/index.bdmv", false, "/home/user/movies/movie_name/art.jpg" },
+ { "/home/user/movies/movie_name/BDMV/index.bdmv", true, "/home/user/movies/movie_name/art.jpg" }};
+
+INSTANTIATE_TEST_SUITE_P(MovieFiles, TestFileItemSpecifiedArtJpg, ValuesIn(MovieFiles));
+
+class TestFileItemFallbackArt : public AdvancedSettingsResetBase,
+ public WithParamInterface<TestFileData>
+{
+};
+
+TEST_P(TestFileItemFallbackArt, GetLocalArt)
+{
+ CFileItem item;
+ item.SetPath(GetParam().file);
+ std::string path = CURL(item.GetLocalArt("", GetParam().use_folder)).Get();
+ std::string compare = CURL(GetParam().base).Get();
+ EXPECT_EQ(compare, path);
+}
+
+const TestFileData NoArtFiles[] = {{ "c:\\dir\\filename.avi", false, "c:\\dir\\filename.tbn" },
+ { "/dir/filename.avi", false, "/dir/filename.tbn" },
+ { "smb://somepath/file.avi", false, "smb://somepath/file.tbn" },
+ { "/home/user/TV Shows/Dexter/S1/1x01.avi", false, "/home/user/TV Shows/Dexter/S1/1x01.tbn" },
+ { "zip://g%3a%5cmultimedia%5cmovies%5cSphere%2ezip/Sphere.avi", false, "g:\\multimedia\\movies\\Sphere.tbn" }};
+
+INSTANTIATE_TEST_SUITE_P(NoArt, TestFileItemFallbackArt, ValuesIn(NoArtFiles));
+
+class TestFileItemBasePath : public AdvancedSettingsResetBase,
+ public WithParamInterface<TestFileData>
+{
+};
+
+TEST_P(TestFileItemBasePath, GetBaseMoviePath)
+{
+ CFileItem item;
+ item.SetPath(GetParam().file);
+ std::string path = CURL(item.GetBaseMoviePath(GetParam().use_folder)).Get();
+ std::string compare = CURL(GetParam().base).Get();
+ EXPECT_EQ(compare, path);
+}
+
+const TestFileData BaseMovies[] = {{ "c:\\dir\\filename.avi", false, "c:\\dir\\filename.avi" },
+ { "c:\\dir\\filename.avi", true, "c:\\dir\\" },
+ { "/dir/filename.avi", false, "/dir/filename.avi" },
+ { "/dir/filename.avi", true, "/dir/" },
+ { "smb://somepath/file.avi", false, "smb://somepath/file.avi" },
+ { "smb://somepath/file.avi", true, "smb://somepath/" },
+ { "stack:///path/to/movie_name/cd1/some_file1.avi , /path/to/movie_name/cd2/some_file2.avi", false, "stack:///path/to/movie_name/cd1/some_file1.avi , /path/to/movie_name/cd2/some_file2.avi" },
+ { "stack:///path/to/movie_name/cd1/some_file1.avi , /path/to/movie_name/cd2/some_file2.avi", true, "/path/to/movie_name/" },
+ { "/home/user/TV Shows/Dexter/S1/1x01.avi", false, "/home/user/TV Shows/Dexter/S1/1x01.avi" },
+ { "/home/user/TV Shows/Dexter/S1/1x01.avi", true, "/home/user/TV Shows/Dexter/S1/" },
+ { "zip://g%3a%5cmultimedia%5cmovies%5cSphere%2ezip/Sphere.avi", true, "g:\\multimedia\\movies\\" },
+ { "/home/user/movies/movie_name/video_ts/VIDEO_TS.IFO", false, "/home/user/movies/movie_name/" },
+ { "/home/user/movies/movie_name/video_ts/VIDEO_TS.IFO", true, "/home/user/movies/movie_name/" },
+ { "/home/user/movies/movie_name/BDMV/index.bdmv", false, "/home/user/movies/movie_name/" },
+ { "/home/user/movies/movie_name/BDMV/index.bdmv", true, "/home/user/movies/movie_name/" }};
+
+INSTANTIATE_TEST_SUITE_P(BaseNameMovies, TestFileItemBasePath, ValuesIn(BaseMovies));
diff --git a/xbmc/test/TestTextureUtils.cpp b/xbmc/test/TestTextureUtils.cpp
new file mode 100644
index 0000000..a925e38
--- /dev/null
+++ b/xbmc/test/TestTextureUtils.cpp
@@ -0,0 +1,51 @@
+/*
+ * 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 "TextureDatabase.h"
+#include "URL.h"
+
+#include <gtest/gtest.h>
+
+using ::testing::ValuesIn;
+
+namespace
+{
+typedef struct
+{
+ const char *in;
+ const char *type;
+ const char *options;
+ const char *out;
+} TestFiles;
+
+const TestFiles test_files[] = {{ "/path/to/image/file.jpg", "", "", "image://%2fpath%2fto%2fimage%2ffile.jpg/" },
+ { "/path/to/image/file.jpg", "", "size=thumb", "image://%2fpath%2fto%2fimage%2ffile.jpg/transform?size=thumb" },
+ { "/path/to/video/file.mkv", "video", "", "image://video@%2fpath%2fto%2fvideo%2ffile.mkv/" },
+ { "/path/to/music/file.mp3", "music", "", "image://music@%2fpath%2fto%2fmusic%2ffile.mp3/" },
+ { "image://%2fpath%2fto%2fimage%2ffile.jpg/", "", "", "image://%2fpath%2fto%2fimage%2ffile.jpg/" },
+ { "image://%2fpath%2fto%2fimage%2ffile.jpg/transform?size=thumb", "", "size=thumb", "image://%2fpath%2fto%2fimage%2ffile.jpg/transform?size=thumb" }};
+
+
+class TestTextureUtils :
+ public ::testing::TestWithParam<TestFiles>
+{
+};
+
+TEST_P(TestTextureUtils, GetWrappedImageURL)
+{
+ const TestFiles &testFiles(GetParam());
+
+ std::string expected = testFiles.out;
+ std::string out = CTextureUtils::GetWrappedImageURL(testFiles.in,
+ testFiles.type,
+ testFiles.options);
+ EXPECT_EQ(expected, out);
+}
+
+INSTANTIATE_TEST_SUITE_P(SampleFiles, TestTextureUtils, ValuesIn(test_files));
+}
diff --git a/xbmc/test/TestURL.cpp b/xbmc/test/TestURL.cpp
new file mode 100644
index 0000000..e74d3be
--- /dev/null
+++ b/xbmc/test/TestURL.cpp
@@ -0,0 +1,72 @@
+/*
+ * 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 "URL.h"
+
+#include <gtest/gtest.h>
+
+using ::testing::Test;
+using ::testing::WithParamInterface;
+using ::testing::ValuesIn;
+
+struct TestURLGetWithoutUserDetailsData
+{
+ std::string input;
+ std::string expected;
+ bool redact;
+};
+
+std::ostream& operator<<(std::ostream& os,
+ const TestURLGetWithoutUserDetailsData& rhs)
+{
+ return os << "(Input: " << rhs.input <<
+ "; Redact: " << (rhs.redact?"true":"false") <<
+ "; Expected: " << rhs.expected << ")";
+}
+
+class TestURLGetWithoutUserDetails : public Test,
+ public WithParamInterface<TestURLGetWithoutUserDetailsData>
+{
+};
+
+TEST_P(TestURLGetWithoutUserDetails, GetWithoutUserDetails)
+{
+ CURL input(GetParam().input);
+ std::string result = input.GetWithoutUserDetails(GetParam().redact);
+ EXPECT_EQ(result, GetParam().expected);
+}
+
+const TestURLGetWithoutUserDetailsData values[] = {
+ { std::string("smb://example.com/example"), std::string("smb://example.com/example"), false },
+ { std::string("smb://example.com/example"), std::string("smb://example.com/example"), true },
+ { std::string("smb://god:universe@example.com/example"), std::string("smb://example.com/example"), false },
+ { std::string("smb://god@example.com/example"), std::string("smb://USERNAME@example.com/example"), true },
+ { std::string("smb://god:universe@example.com/example"), std::string("smb://USERNAME:PASSWORD@example.com/example"), true },
+ { std::string("http://god:universe@example.com:8448/example|auth=digest"), std::string("http://USERNAME:PASSWORD@example.com:8448/example|auth=digest"), true },
+ { std::string("smb://fd00::1/example"), std::string("smb://fd00::1/example"), false },
+ { std::string("smb://fd00::1/example"), std::string("smb://fd00::1/example"), true },
+ { std::string("smb://[fd00::1]:8080/example"), std::string("smb://[fd00::1]:8080/example"), false },
+ { std::string("smb://[fd00::1]:8080/example"), std::string("smb://[fd00::1]:8080/example"), true },
+ { std::string("smb://god:universe@[fd00::1]:8080/example"), std::string("smb://[fd00::1]:8080/example"), false },
+ { std::string("smb://god@[fd00::1]:8080/example"), std::string("smb://USERNAME@[fd00::1]:8080/example"), true },
+ { std::string("smb://god:universe@fd00::1/example"), std::string("smb://USERNAME:PASSWORD@fd00::1/example"), true },
+ { std::string("http://god:universe@[fd00::1]:8448/example|auth=digest"), std::string("http://USERNAME:PASSWORD@[fd00::1]:8448/example|auth=digest"), true },
+ { std::string("smb://00ff:1:0000:abde::/example"), std::string("smb://00ff:1:0000:abde::/example"), true },
+ { std::string("smb://god:universe@[00ff:1:0000:abde::]:8080/example"), std::string("smb://[00ff:1:0000:abde::]:8080/example"), false },
+ { std::string("smb://god@[00ff:1:0000:abde::]:8080/example"), std::string("smb://USERNAME@[00ff:1:0000:abde::]:8080/example"), true },
+ { std::string("smb://god:universe@00ff:1:0000:abde::/example"), std::string("smb://USERNAME:PASSWORD@00ff:1:0000:abde::/example"), true },
+ { std::string("http://god:universe@[00ff:1:0000:abde::]:8448/example|auth=digest"), std::string("http://USERNAME:PASSWORD@[00ff:1:0000:abde::]:8448/example|auth=digest"), true },
+ { std::string("smb://milkyway;god:universe@example.com/example"), std::string("smb://DOMAIN;USERNAME:PASSWORD@example.com/example"), true },
+ { std::string("smb://milkyway;god@example.com/example"), std::string("smb://DOMAIN;USERNAME@example.com/example"), true },
+ { std::string("smb://milkyway;@example.com/example"), std::string("smb://example.com/example"), true },
+ { std::string("smb://milkyway;god:universe@example.com/example"), std::string("smb://example.com/example"), false },
+ { std::string("smb://milkyway;god@example.com/example"), std::string("smb://example.com/example"), false },
+ { std::string("smb://milkyway;@example.com/example"), std::string("smb://example.com/example"), false },
+};
+
+INSTANTIATE_TEST_SUITE_P(URL, TestURLGetWithoutUserDetails, ValuesIn(values));
diff --git a/xbmc/test/TestUtil.cpp b/xbmc/test/TestUtil.cpp
new file mode 100644
index 0000000..827c310
--- /dev/null
+++ b/xbmc/test/TestUtil.cpp
@@ -0,0 +1,87 @@
+/*
+ * 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 "Util.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestUtil, GetQualifiedFilename)
+{
+ std::string file = "../foo";
+ CUtil::GetQualifiedFilename("smb://", file);
+ EXPECT_EQ(file, "foo");
+ file = "C:\\foo\\bar";
+ CUtil::GetQualifiedFilename("smb://", file);
+ EXPECT_EQ(file, "C:\\foo\\bar");
+ file = "../foo/./bar";
+ CUtil::GetQualifiedFilename("smb://my/path", file);
+ EXPECT_EQ(file, "smb://my/foo/bar");
+ file = "smb://foo/bar/";
+ CUtil::GetQualifiedFilename("upnp://", file);
+ EXPECT_EQ(file, "smb://foo/bar/");
+}
+
+TEST(TestUtil, MakeLegalPath)
+{
+ std::string path;
+#ifdef TARGET_WINDOWS
+ path = "C:\\foo\\bar";
+ EXPECT_EQ(CUtil::MakeLegalPath(path), "C:\\foo\\bar");
+ path = "C:\\foo:\\bar\\";
+ EXPECT_EQ(CUtil::MakeLegalPath(path), "C:\\foo_\\bar\\");
+#else
+ path = "/foo/bar/";
+ EXPECT_EQ(CUtil::MakeLegalPath(path),"/foo/bar/");
+ path = "/foo?/bar";
+ EXPECT_EQ(CUtil::MakeLegalPath(path),"/foo_/bar");
+#endif
+ path = "smb://foo/bar";
+ EXPECT_EQ(CUtil::MakeLegalPath(path), "smb://foo/bar");
+ path = "smb://foo/bar?/";
+ EXPECT_EQ(CUtil::MakeLegalPath(path), "smb://foo/bar_/");
+}
+
+TEST(TestUtil, MakeShortenPath)
+{
+ std::string result;
+ EXPECT_EQ(true, CUtil::MakeShortenPath("smb://test/string/is/long/and/very/much/so", result, 10));
+ EXPECT_EQ("smb:/../so", result);
+
+ EXPECT_EQ(true, CUtil::MakeShortenPath("smb://test/string/is/long/and/very/much/so", result, 30));
+ EXPECT_EQ("smb://../../../../../../../so", result);
+
+ EXPECT_EQ(true, CUtil::MakeShortenPath("smb://test//string/is/long/and/very//much/so", result, 30));
+ EXPECT_EQ("smb:/../../../../../so", result);
+
+ EXPECT_EQ(true, CUtil::MakeShortenPath("//test//string/is/long/and/very//much/so", result, 30));
+ EXPECT_EQ("/../../../../../so", result);
+}
+
+TEST(TestUtil, ValidatePath)
+{
+ std::string path;
+#ifdef TARGET_WINDOWS
+ path = "C:/foo/bar/";
+ EXPECT_EQ(CUtil::ValidatePath(path), "C:\\foo\\bar\\");
+ path = "C:\\\\foo\\\\bar\\";
+ EXPECT_EQ(CUtil::ValidatePath(path, true), "C:\\foo\\bar\\");
+ path = "\\\\foo\\\\bar\\";
+ EXPECT_EQ(CUtil::ValidatePath(path, true), "\\\\foo\\bar\\");
+#else
+ path = "\\foo\\bar\\";
+ EXPECT_EQ(CUtil::ValidatePath(path), "/foo/bar/");
+ path = "/foo//bar/";
+ EXPECT_EQ(CUtil::ValidatePath(path, true), "/foo/bar/");
+#endif
+ path = "smb://foo/bar/";
+ EXPECT_EQ(CUtil::ValidatePath(path), "smb://foo/bar/");
+ path = "smb://foo//bar/";
+ EXPECT_EQ(CUtil::ValidatePath(path, true), "smb://foo/bar/");
+ path = "smb:\\\\foo\\\\bar\\";
+ EXPECT_EQ(CUtil::ValidatePath(path, true), "smb://foo/bar/");
+}
diff --git a/xbmc/test/TestUtils.cpp b/xbmc/test/TestUtils.cpp
new file mode 100644
index 0000000..1289c89
--- /dev/null
+++ b/xbmc/test/TestUtils.cpp
@@ -0,0 +1,324 @@
+/*
+ * 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 "TestUtils.h"
+#include "Util.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "platform/Filesystem.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#ifdef TARGET_WINDOWS
+#include <windows.h>
+#else
+#include <cstdlib>
+#include <climits>
+#include <ctime>
+#endif
+
+#include <system_error>
+
+namespace fs = KODI::PLATFORM::FILESYSTEM;
+
+class CTempFile : public XFILE::CFile
+{
+public:
+ CTempFile() = default;
+ ~CTempFile()
+ {
+ Delete();
+ }
+ bool Create(const std::string &suffix)
+ {
+ std::error_code ec;
+ m_ptempFilePath = fs::temp_file_path(suffix, ec);
+ if (ec)
+ return false;
+
+ if (m_ptempFilePath.empty())
+ return false;
+
+ OpenForWrite(m_ptempFilePath, true);
+ return true;
+ }
+ bool Delete()
+ {
+ Close();
+ return CFile::Delete(m_ptempFilePath);
+ };
+ std::string getTempFilePath() const
+ {
+ return m_ptempFilePath;
+ }
+ std::string getTempFileDirectory() const
+ {
+ return URIUtils::GetDirectory(m_ptempFilePath);
+ }
+private:
+ std::string m_ptempFilePath;
+};
+
+CXBMCTestUtils::CXBMCTestUtils()
+{
+ probability = 0.01;
+}
+
+CXBMCTestUtils &CXBMCTestUtils::Instance()
+{
+ static CXBMCTestUtils instance;
+ return instance;
+}
+
+std::string CXBMCTestUtils::ReferenceFilePath(const std::string& path)
+{
+ return CSpecialProtocol::TranslatePath(URIUtils::AddFileToFolder("special://xbmc", path));
+}
+
+bool CXBMCTestUtils::SetReferenceFileBasePath()
+{
+ std::string xbmcPath = CUtil::GetHomePath();
+ if (xbmcPath.empty())
+ return false;
+
+ /* Set xbmc, xbmcbin and home path */
+ CSpecialProtocol::SetXBMCPath(xbmcPath);
+ CSpecialProtocol::SetXBMCBinPath(xbmcPath);
+ CSpecialProtocol::SetHomePath(URIUtils::AddFileToFolder(xbmcPath, "portable_data"));
+
+ return true;
+}
+
+XFILE::CFile *CXBMCTestUtils::CreateTempFile(std::string const& suffix)
+{
+ CTempFile *f = new CTempFile();
+ if (f->Create(suffix))
+ return f;
+ delete f;
+ return NULL;
+}
+
+bool CXBMCTestUtils::DeleteTempFile(XFILE::CFile *tempfile)
+{
+ if (!tempfile)
+ return true;
+ CTempFile *f = static_cast<CTempFile*>(tempfile);
+ bool retval = f->Delete();
+ delete f;
+ return retval;
+}
+
+std::string CXBMCTestUtils::TempFilePath(XFILE::CFile const* const tempfile)
+{
+ if (!tempfile)
+ return "";
+ CTempFile const* const f = static_cast<CTempFile const*>(tempfile);
+ return f->getTempFilePath();
+}
+
+std::string CXBMCTestUtils::TempFileDirectory(XFILE::CFile const* const tempfile)
+{
+ if (!tempfile)
+ return "";
+ CTempFile const* const f = static_cast<CTempFile const*>(tempfile);
+ return f->getTempFileDirectory();
+}
+
+XFILE::CFile *CXBMCTestUtils::CreateCorruptedFile(std::string const& strFileName,
+ std::string const& suffix)
+{
+ XFILE::CFile inputfile, *tmpfile = CreateTempFile(suffix);
+ unsigned char buf[20], tmpchar;
+ ssize_t size, i;
+
+ if (tmpfile && inputfile.Open(strFileName))
+ {
+ srand(time(NULL));
+ while ((size = inputfile.Read(buf, sizeof(buf))) > 0)
+ {
+ for (i = 0; i < size; i++)
+ {
+ if ((rand() % RAND_MAX) < (probability * RAND_MAX))
+ {
+ tmpchar = buf[i];
+ do
+ {
+ buf[i] = (rand() % 256);
+ } while (buf[i] == tmpchar);
+ }
+ }
+ if (tmpfile->Write(buf, size) < 0)
+ {
+ inputfile.Close();
+ tmpfile->Close();
+ DeleteTempFile(tmpfile);
+ return NULL;
+ }
+ }
+ inputfile.Close();
+ tmpfile->Close();
+ return tmpfile;
+ }
+ delete tmpfile;
+ return NULL;
+}
+
+
+std::vector<std::string> &CXBMCTestUtils::getTestFileFactoryReadUrls()
+{
+ return TestFileFactoryReadUrls;
+}
+
+std::vector<std::string> &CXBMCTestUtils::getTestFileFactoryWriteUrls()
+{
+ return TestFileFactoryWriteUrls;
+}
+
+std::string &CXBMCTestUtils::getTestFileFactoryWriteInputFile()
+{
+ return TestFileFactoryWriteInputFile;
+}
+
+void CXBMCTestUtils::setTestFileFactoryWriteInputFile(std::string const& file)
+{
+ TestFileFactoryWriteInputFile = file;
+}
+
+std::vector<std::string> &CXBMCTestUtils::getAdvancedSettingsFiles()
+{
+ return AdvancedSettingsFiles;
+}
+
+std::vector<std::string> &CXBMCTestUtils::getGUISettingsFiles()
+{
+ return GUISettingsFiles;
+}
+
+static const char usage[] =
+"Kodi Test Suite\n"
+"Usage: kodi-test [options]\n"
+"\n"
+"The following options are recognized by the kodi-test program.\n"
+"\n"
+" --add-testfilefactory-readurl [URL]\n"
+" Add a url to be used int the TestFileFactory read tests.\n"
+"\n"
+" --add-testfilefactory-readurls [URLS]\n"
+" Add multiple urls from a ',' delimited string of urls to be used\n"
+" in the TestFileFactory read tests.\n"
+"\n"
+" --add-testfilefactory-writeurl [URL]\n"
+" Add a url to be used int the TestFileFactory write tests.\n"
+"\n"
+" --add-testfilefactory-writeurls [URLS]\n"
+" Add multiple urls from a ',' delimited string of urls to be used\n"
+" in the TestFileFactory write tests.\n"
+"\n"
+" --set-testfilefactory-writeinputfile [FILE]\n"
+" Set the path to the input file used in the TestFileFactory write tests.\n"
+"\n"
+" --add-advancedsettings-file [FILE]\n"
+" Add an advanced settings file to be loaded in test cases that use them.\n"
+"\n"
+" --add-advancedsettings-files [FILES]\n"
+" Add multiple advanced settings files from a ',' delimited string of\n"
+" files to be loaded in test cases that use them.\n"
+"\n"
+" --add-guisettings-file [FILE]\n"
+" Add a GUI settings file to be loaded in test cases that use them.\n"
+"\n"
+" --add-guisettings-files [FILES]\n"
+" Add multiple GUI settings files from a ',' delimited string of\n"
+" files to be loaded in test cases that use them.\n"
+"\n"
+" --set-probability [PROBABILITY]\n"
+" Set the probability variable used by the file corrupting functions.\n"
+" The variable should be a double type from 0.0 to 1.0. Values given\n"
+" less than 0.0 are treated as 0.0. Values greater than 1.0 are treated\n"
+" as 1.0. The default probability is 0.01.\n"
+;
+
+void CXBMCTestUtils::ParseArgs(int argc, char **argv)
+{
+ int i;
+ std::string arg;
+ for (i = 1; i < argc; i++)
+ {
+ arg = argv[i];
+ if (arg == "--add-testfilefactory-readurl")
+ {
+ TestFileFactoryReadUrls.emplace_back(argv[++i]);
+ }
+ else if (arg == "--add-testfilefactory-readurls")
+ {
+ arg = argv[++i];
+ std::vector<std::string> urls = StringUtils::Split(arg, ",");
+ for (const auto& it : urls)
+ TestFileFactoryReadUrls.push_back(it);
+ }
+ else if (arg == "--add-testfilefactory-writeurl")
+ {
+ TestFileFactoryWriteUrls.emplace_back(argv[++i]);
+ }
+ else if (arg == "--add-testfilefactory-writeurls")
+ {
+ arg = argv[++i];
+ std::vector<std::string> urls = StringUtils::Split(arg, ",");
+ for (const auto& it : urls)
+ TestFileFactoryWriteUrls.push_back(it);
+ }
+ else if (arg == "--set-testfilefactory-writeinputfile")
+ {
+ TestFileFactoryWriteInputFile = argv[++i];
+ }
+ else if (arg == "--add-advancedsettings-file")
+ {
+ AdvancedSettingsFiles.emplace_back(argv[++i]);
+ }
+ else if (arg == "--add-advancedsettings-files")
+ {
+ arg = argv[++i];
+ std::vector<std::string> urls = StringUtils::Split(arg, ",");
+ for (const auto& it : urls)
+ AdvancedSettingsFiles.push_back(it);
+ }
+ else if (arg == "--add-guisettings-file")
+ {
+ GUISettingsFiles.emplace_back(argv[++i]);
+ }
+ else if (arg == "--add-guisettings-files")
+ {
+ arg = argv[++i];
+ std::vector<std::string> urls = StringUtils::Split(arg, ",");
+ for (const auto& it : urls)
+ GUISettingsFiles.push_back(it);
+ }
+ else if (arg == "--set-probability")
+ {
+ probability = atof(argv[++i]);
+ if (probability < 0.0)
+ probability = 0.0;
+ else if (probability > 1.0)
+ probability = 1.0;
+ }
+ else
+ {
+ std::cerr << usage;
+ exit(EXIT_FAILURE);
+ }
+ }
+}
+
+std::string CXBMCTestUtils::getNewLineCharacters() const
+{
+#ifdef TARGET_WINDOWS
+ return "\r\n";
+#else
+ return "\n";
+#endif
+}
diff --git a/xbmc/test/TestUtils.h b/xbmc/test/TestUtils.h
new file mode 100644
index 0000000..b0dbcf6
--- /dev/null
+++ b/xbmc/test/TestUtils.h
@@ -0,0 +1,102 @@
+/*
+ * 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 <string>
+#include <vector>
+
+namespace XFILE
+{
+ class CFile;
+}
+
+class CXBMCTestUtils
+{
+public:
+ static CXBMCTestUtils &Instance();
+
+ /* ReferenceFilePath() is used to prepend a path with the location to the
+ * xbmc-test binary. It's assumed the test suite program will only be run
+ * with xbmc-test residing in the source tree.
+ */
+ std::string ReferenceFilePath(const std::string& path);
+
+ /* Function to set the reference file base path. */
+ bool SetReferenceFileBasePath();
+
+ /* Function used in creating a temporary file. It accepts a parameter
+ * 'suffix' to append to the end of the tempfile path. The temporary
+ * file is return as a XFILE::CFile object.
+ */
+ XFILE::CFile *CreateTempFile(std::string const& suffix);
+
+ /* Function used to close and delete a temporary file previously created
+ * using CreateTempFile().
+ */
+ bool DeleteTempFile(XFILE::CFile *tempfile);
+
+ /* Function to get path of a tempfile */
+ std::string TempFilePath(XFILE::CFile const* const tempfile);
+
+ /* Get the containing directory of a tempfile */
+ std::string TempFileDirectory(XFILE::CFile const* const tempfile);
+
+ /* Functions to get variables used in the TestFileFactory tests. */
+ std::vector<std::string> &getTestFileFactoryReadUrls();
+
+ /* Function to get variables used in the TestFileFactory tests. */
+ std::vector<std::string> &getTestFileFactoryWriteUrls();
+
+ /* Function to get the input file used in the TestFileFactory.Write tests. */
+ std::string &getTestFileFactoryWriteInputFile();
+
+ /* Function to set the input file used in the TestFileFactory.Write tests */
+ void setTestFileFactoryWriteInputFile(std::string const& file);
+
+ /* Function to get advanced settings files. */
+ std::vector<std::string> &getAdvancedSettingsFiles();
+
+ /* Function to get GUI settings files. */
+ std::vector<std::string> &getGUISettingsFiles();
+
+ /* Function used in creating a corrupted file. The parameters are a URL
+ * to the original file to be corrupted and a suffix to append to the
+ * path of the newly created file. This will return a XFILE::CFile
+ * object which is itself a tempfile object which can be used with the
+ * tempfile functions of this utility class.
+ */
+ XFILE::CFile *CreateCorruptedFile(std::string const& strFileName,
+ std::string const& suffix);
+
+ /* Function to parse command line options */
+ void ParseArgs(int argc, char **argv);
+
+ /* Function to return the newline characters for this platform */
+ std::string getNewLineCharacters() const;
+private:
+ CXBMCTestUtils();
+ CXBMCTestUtils(CXBMCTestUtils const&) = delete;
+ CXBMCTestUtils& operator=(CXBMCTestUtils const&) = delete;
+
+ std::vector<std::string> TestFileFactoryReadUrls;
+ std::vector<std::string> TestFileFactoryWriteUrls;
+ std::string TestFileFactoryWriteInputFile;
+
+ std::vector<std::string> AdvancedSettingsFiles;
+ std::vector<std::string> GUISettingsFiles;
+
+ double probability;
+};
+
+#define XBMC_REF_FILE_PATH(s) CXBMCTestUtils::Instance().ReferenceFilePath(s)
+#define XBMC_CREATETEMPFILE(a) CXBMCTestUtils::Instance().CreateTempFile(a)
+#define XBMC_DELETETEMPFILE(a) CXBMCTestUtils::Instance().DeleteTempFile(a)
+#define XBMC_TEMPFILEPATH(a) CXBMCTestUtils::Instance().TempFilePath(a)
+#define XBMC_CREATECORRUPTEDFILE(a, b) \
+ CXBMCTestUtils::Instance().CreateCorruptedFile(a, b)
diff --git a/xbmc/test/xbmc-test.cpp b/xbmc/test/xbmc-test.cpp
new file mode 100644
index 0000000..faafd5a
--- /dev/null
+++ b/xbmc/test/xbmc-test.cpp
@@ -0,0 +1,32 @@
+/*
+ * 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 "TestBasicEnvironment.h"
+#include "TestUtils.h"
+#include "commons/ilog.h"
+#include "threads/Thread.h"
+
+#include <cstdio>
+#include <cstdlib>
+
+#include <gtest/gtest.h>
+
+int main(int argc, char **argv)
+{
+ testing::InitGoogleTest(&argc, argv);
+ CXBMCTestUtils::Instance().ParseArgs(argc, argv);
+
+ if (!testing::AddGlobalTestEnvironment(new TestBasicEnvironment()))
+ {
+ fprintf(stderr, "Unable to add basic test environment.\n");
+ exit(EXIT_FAILURE);
+ }
+ int ret = RUN_ALL_TESTS();
+
+ return ret;
+}
diff --git a/xbmc/threads/CMakeLists.txt b/xbmc/threads/CMakeLists.txt
new file mode 100644
index 0000000..02e2016
--- /dev/null
+++ b/xbmc/threads/CMakeLists.txt
@@ -0,0 +1,17 @@
+set(SOURCES Event.cpp
+ Thread.cpp
+ Timer.cpp)
+
+set(HEADERS Condition.h
+ CriticalSection.h
+ Event.h
+ Lockables.h
+ SharedSection.h
+ SingleLock.h
+ SystemClock.h
+ Thread.h
+ Timer.h
+ IThreadImpl.h
+ IRunnable.h)
+
+core_add_library(threads)
diff --git a/xbmc/threads/Condition.h b/xbmc/threads/Condition.h
new file mode 100644
index 0000000..3738352
--- /dev/null
+++ b/xbmc/threads/Condition.h
@@ -0,0 +1,108 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+#include <chrono>
+#include <condition_variable>
+#include <functional>
+#include <mutex>
+#include <utility>
+
+namespace XbmcThreads
+{
+
+ /**
+ * This is a thin wrapper around std::condition_variable_any. It is subject
+ * to "spurious returns"
+ */
+ class ConditionVariable
+ {
+ private:
+ std::condition_variable_any cond;
+ ConditionVariable(const ConditionVariable&) = delete;
+ ConditionVariable& operator=(const ConditionVariable&) = delete;
+
+ public:
+ ConditionVariable() = default;
+
+ inline void wait(CCriticalSection& lock, std::function<bool()> predicate)
+ {
+ int count = lock.count;
+ lock.count = 0;
+ cond.wait(lock.get_underlying(), std::move(predicate));
+ lock.count = count;
+ }
+
+ inline void wait(CCriticalSection& lock)
+ {
+ int count = lock.count;
+ lock.count = 0;
+ cond.wait(lock.get_underlying());
+ lock.count = count;
+ }
+
+ template<typename Rep, typename Period>
+ inline bool wait(CCriticalSection& lock,
+ std::chrono::duration<Rep, Period> duration,
+ std::function<bool()> predicate)
+ {
+ int count = lock.count;
+ lock.count = 0;
+ bool ret = cond.wait_for(lock.get_underlying(), duration, predicate);
+ lock.count = count;
+ return ret;
+ }
+
+ template<typename Rep, typename Period>
+ inline bool wait(CCriticalSection& lock, std::chrono::duration<Rep, Period> duration)
+ {
+ int count = lock.count;
+ lock.count = 0;
+ std::cv_status res = cond.wait_for(lock.get_underlying(), duration);
+ lock.count = count;
+ return res == std::cv_status::no_timeout;
+ }
+
+ inline void wait(std::unique_lock<CCriticalSection>& lock, std::function<bool()> predicate)
+ {
+ cond.wait(*lock.mutex(), std::move(predicate));
+ }
+
+ inline void wait(std::unique_lock<CCriticalSection>& lock) { wait(*lock.mutex()); }
+
+ template<typename Rep, typename Period>
+ inline bool wait(std::unique_lock<CCriticalSection>& lock,
+ std::chrono::duration<Rep, Period> duration,
+ std::function<bool()> predicate)
+ {
+ return wait(*lock.mutex(), duration, predicate);
+ }
+
+ template<typename Rep, typename Period>
+ inline bool wait(std::unique_lock<CCriticalSection>& lock,
+ std::chrono::duration<Rep, Period> duration)
+ {
+ return wait(*lock.mutex(), duration);
+ }
+
+ inline void notifyAll()
+ {
+ cond.notify_all();
+ }
+
+ inline void notify()
+ {
+ cond.notify_one();
+ }
+ };
+
+}
+
diff --git a/xbmc/threads/CriticalSection.h b/xbmc/threads/CriticalSection.h
new file mode 100644
index 0000000..5aaf8aa
--- /dev/null
+++ b/xbmc/threads/CriticalSection.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "threads/Lockables.h"
+
+#if defined(TARGET_POSIX)
+#include "platform/posix/threads/RecursiveMutex.h"
+
+class CCriticalSection : public XbmcThreads::CountingLockable<XbmcThreads::CRecursiveMutex>
+{
+};
+
+#elif defined(TARGET_WINDOWS)
+#include <mutex>
+
+class CCriticalSection : public XbmcThreads::CountingLockable<std::recursive_mutex>
+{
+};
+
+#endif
diff --git a/xbmc/threads/Event.cpp b/xbmc/threads/Event.cpp
new file mode 100644
index 0000000..1c67f4e
--- /dev/null
+++ b/xbmc/threads/Event.cpp
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2002 Frodo
+ * Portions Copyright (c) by the authors of ffmpeg and xvid
+ * Copyright (C) 2002-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 "Event.h"
+
+#include <algorithm>
+#include <limits>
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+void CEvent::addGroup(XbmcThreads::CEventGroup* group)
+{
+ std::unique_lock<CCriticalSection> lock(groupListMutex);
+ if (!groups)
+ groups.reset(new std::vector<XbmcThreads::CEventGroup*>);
+
+ groups->push_back(group);
+}
+
+void CEvent::removeGroup(XbmcThreads::CEventGroup* group)
+{
+ std::unique_lock<CCriticalSection> lock(groupListMutex);
+ if (groups)
+ {
+ groups->erase(std::remove(groups->begin(), groups->end(), group), groups->end());
+ if (groups->empty())
+ {
+ groups.reset();
+ }
+ }
+}
+
+// locking is ALWAYS done in this order:
+// CEvent::groupListMutex -> CEventGroup::mutex -> CEvent::mutex
+void CEvent::Set()
+{
+ // Originally I had this without locking. Thanks to FernetMenta who
+ // pointed out that this creates a race condition between setting
+ // checking the signal and calling wait() on the Wait call in the
+ // CEvent class. This now perfectly matches the boost example here:
+ // http://www.boost.org/doc/libs/1_41_0/doc/html/thread/synchronization.html#thread.synchronization.condvar_ref
+ {
+ std::unique_lock<CCriticalSection> slock(mutex);
+ signaled = true;
+ }
+
+ actualCv.notifyAll();
+
+ std::unique_lock<CCriticalSection> l(groupListMutex);
+ if (groups)
+ {
+ for (auto* group : *groups)
+ group->Set(this);
+ }
+}
+
+namespace XbmcThreads
+{
+ /**
+ * This will block until any one of the CEvents in the group are
+ * signaled at which point a pointer to that CEvents will be
+ * returned.
+ */
+ CEvent* CEventGroup::wait()
+ {
+ return wait(std::chrono::milliseconds::max());
+ }
+
+ CEventGroup::CEventGroup(std::initializer_list<CEvent*> eventsList)
+ : events{eventsList}
+ {
+ // we preping for a wait, so we need to set the group value on
+ // all of the CEvents.
+ for (auto* event : events)
+ {
+ event->addGroup(this);
+ }
+ }
+
+ CEventGroup::~CEventGroup()
+ {
+ for (auto* event : events)
+ {
+ event->removeGroup(this);
+ }
+ }
+}
diff --git a/xbmc/threads/Event.h b/xbmc/threads/Event.h
new file mode 100644
index 0000000..72ce754
--- /dev/null
+++ b/xbmc/threads/Event.h
@@ -0,0 +1,239 @@
+/*
+ * 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 "threads/Condition.h"
+
+#include <initializer_list>
+#include <memory>
+#include <mutex>
+#include <vector>
+
+// forward declare the CEventGroup
+namespace XbmcThreads
+{
+class CEventGroup;
+}
+
+/**
+ * @brief This is an Event class built from a ConditionVariable. The Event adds the state
+ * that the condition is gating as well as the mutex/lock.
+ *
+ * This Event can be 'interruptible' (even though there is only a single place
+ * in the code that uses this behavior).
+ *
+ * This class manages 'spurious returns' from the condition variable.
+ *
+ */
+
+class CEvent
+{
+ bool manualReset;
+ volatile bool signaled;
+ unsigned int numWaits = 0;
+
+ CCriticalSection groupListMutex; // lock for the groups list
+ std::unique_ptr<std::vector<XbmcThreads::CEventGroup*>> groups;
+
+ XbmcThreads::ConditionVariable actualCv;
+ CCriticalSection mutex;
+
+ friend class XbmcThreads::CEventGroup;
+
+ void addGroup(XbmcThreads::CEventGroup* group);
+ void removeGroup(XbmcThreads::CEventGroup* group);
+
+ // helper for the two wait methods
+ inline bool prepReturn()
+ {
+ bool ret = signaled;
+ if (!manualReset && numWaits == 0)
+ signaled = false;
+ return ret;
+ }
+
+ CEvent(const CEvent&) = delete;
+ CEvent& operator=(const CEvent&) = delete;
+
+public:
+ inline CEvent(bool manual = false, bool signaled_ = false)
+ : manualReset(manual), signaled(signaled_)
+ {
+ }
+
+ inline void Reset()
+ {
+ std::unique_lock<CCriticalSection> lock(mutex);
+ signaled = false;
+ }
+ void Set();
+
+ /**
+ * @brief Returns true if Event has been triggered and not reset, false otherwise.
+ *
+ */
+ inline bool Signaled()
+ {
+ std::unique_lock<CCriticalSection> lock(mutex);
+ return signaled;
+ }
+
+ /**
+ * @brief This will wait up to 'duration' for the Event to be
+ * triggered. The method will return 'true' if the Event
+ * was triggered. Otherwise it will return false.
+ *
+ */
+ template<typename Rep, typename Period>
+ inline bool Wait(std::chrono::duration<Rep, Period> duration)
+ {
+ std::unique_lock<CCriticalSection> lock(mutex);
+ numWaits++;
+ actualCv.wait(mutex, duration, std::bind(&CEvent::Signaled, this));
+ numWaits--;
+ return prepReturn();
+ }
+
+ /**
+ * @brief This will wait for the Event to be triggered. The method will return
+ * 'true' if the Event was triggered. If it was either interrupted
+ * it will return false. Otherwise it will return false.
+ *
+ */
+ inline bool Wait()
+ {
+ std::unique_lock<CCriticalSection> lock(mutex);
+ numWaits++;
+ actualCv.wait(mutex, std::bind(&CEvent::Signaled, this));
+ numWaits--;
+ return prepReturn();
+ }
+
+ /**
+ * @brief This is mostly for testing. It allows a thread to make sure there are
+ * the right amount of other threads waiting.
+ *
+ */
+ inline int getNumWaits()
+ {
+ std::unique_lock<CCriticalSection> lock(mutex);
+ return numWaits;
+ }
+};
+
+namespace XbmcThreads
+{
+/**
+ * @brief CEventGroup is a means of grouping CEvents to wait on them together.
+ * It is equivalent to WaitOnMultipleObject that returns when "any" Event
+ * in the group signaled.
+ *
+ */
+class CEventGroup
+{
+ std::vector<CEvent*> events;
+ CEvent* signaled{};
+ XbmcThreads::ConditionVariable actualCv;
+ CCriticalSection mutex;
+
+ unsigned int numWaits{0};
+
+ // This is ONLY called from CEvent::Set.
+ inline void Set(CEvent* child)
+ {
+ std::unique_lock<CCriticalSection> l(mutex);
+ signaled = child;
+ actualCv.notifyAll();
+ }
+
+ friend class ::CEvent;
+
+ CEventGroup(const CEventGroup&) = delete;
+ CEventGroup& operator=(const CEventGroup&) = delete;
+
+public:
+ /**
+ * @brief Create a CEventGroup from a number of CEvents.
+ *
+ */
+ CEventGroup(std::initializer_list<CEvent*> events);
+
+ ~CEventGroup();
+
+ /**
+ * @brief This will block until any one of the CEvents in the group are
+ * signaled at which point a pointer to that CEvents will be
+ * returned.
+ *
+ */
+ CEvent* wait();
+
+ /**
+ * @brief locking is ALWAYS done in this order:
+ * CEvent::groupListMutex -> CEventGroup::mutex -> CEvent::mutex
+ *
+ * Notice that this method doesn't grab the CEvent::groupListMutex at all. This
+ * is fine. It just grabs the CEventGroup::mutex and THEN the individual
+ *
+ */
+ template<typename Rep, typename Period>
+ CEvent* wait(std::chrono::duration<Rep, Period> duration)
+ {
+ std::unique_lock<CCriticalSection> lock(mutex); // grab CEventGroup::mutex
+ numWaits++;
+
+ // ==================================================
+ // This block checks to see if any child events are
+ // signaled and sets 'signaled' to the first one it
+ // finds.
+ // ==================================================
+ signaled = nullptr;
+ for (auto* cur : events)
+ {
+ std::unique_lock<CCriticalSection> lock2(cur->mutex);
+ if (cur->signaled)
+ signaled = cur;
+ }
+ // ==================================================
+
+ if (!signaled)
+ {
+ // both of these release the CEventGroup::mutex
+ if (duration == std::chrono::duration<Rep, Period>::max())
+ actualCv.wait(mutex, [this]() { return signaled != nullptr; });
+ else
+ actualCv.wait(mutex, duration, [this]() { return signaled != nullptr; });
+ } // at this point the CEventGroup::mutex is reacquired
+ numWaits--;
+
+ // signaled should have been set by a call to CEventGroup::Set
+ CEvent* ret = signaled;
+ if (numWaits == 0)
+ {
+ if (signaled)
+ // This acquires and releases the CEvent::mutex. This is fine since the
+ // CEventGroup::mutex is already being held
+ signaled->Wait(std::chrono::duration<Rep, Period>::zero()); // reset the event if needed
+ signaled = nullptr; // clear the signaled if all the waiters are gone
+ }
+ return ret;
+ }
+
+ /**
+ * @brief This is mostly for testing. It allows a thread to make sure there are
+ * the right amount of other threads waiting.
+ *
+ */
+ inline int getNumWaits()
+ {
+ std::unique_lock<CCriticalSection> lock(mutex);
+ return numWaits;
+ }
+};
+} // namespace XbmcThreads
diff --git a/xbmc/threads/IRunnable.h b/xbmc/threads/IRunnable.h
new file mode 100644
index 0000000..32c5b8f
--- /dev/null
+++ b/xbmc/threads/IRunnable.h
@@ -0,0 +1,17 @@
+/*
+ * 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
+
+class IRunnable
+{
+public:
+ virtual void Run()=0;
+ virtual void Cancel() {}
+ virtual ~IRunnable() = default;
+};
diff --git a/xbmc/threads/IThreadImpl.h b/xbmc/threads/IThreadImpl.h
new file mode 100644
index 0000000..f631c79
--- /dev/null
+++ b/xbmc/threads/IThreadImpl.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2005-2022 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/Thread.h"
+
+#include <memory>
+#include <string>
+#include <thread>
+
+class IThreadImpl
+{
+public:
+ virtual ~IThreadImpl() = default;
+
+ static std::unique_ptr<IThreadImpl> CreateThreadImpl(std::thread::native_handle_type handle);
+
+ /*!
+ * \brief Set the thread name and other info (platform dependent)
+ *
+ */
+ virtual void SetThreadInfo(const std::string& name) = 0;
+
+ /*!
+ * \brief Set the thread priority via the native threading library
+ *
+ */
+ virtual bool SetPriority(const ThreadPriority& priority) = 0;
+
+protected:
+ IThreadImpl(std::thread::native_handle_type handle) : m_handle(handle) {}
+
+ std::thread::native_handle_type m_handle;
+
+private:
+ IThreadImpl() = delete;
+};
diff --git a/xbmc/threads/Lockables.h b/xbmc/threads/Lockables.h
new file mode 100644
index 0000000..18509b1
--- /dev/null
+++ b/xbmc/threads/Lockables.h
@@ -0,0 +1,107 @@
+/*
+ * 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
+
+namespace XbmcThreads
+{
+
+ /**
+ * This template will take any implementation of the "Lockable" concept
+ * and allow it to be used as an "Exitable Lockable."
+ *
+ * Something that implements the "Lockable concept" simply means that
+ * it has the three methods:
+ *
+ * lock();
+ * try_lock();
+ * unlock();
+ * IsLocked();
+ *
+ * "Exitable" specifically means that, no matter how deep the recursion
+ * on the mutex/critical section, we can exit from it and then restore
+ * the state.
+ *
+ * This requires us to extend the Lockable so that we can keep track of the
+ * number of locks that have been recursively acquired so that we can
+ * undo it, and then restore that (See class CSingleExit).
+ *
+ * All xbmc code expects Lockables to be recursive.
+ */
+ template<class L> class CountingLockable
+ {
+ friend class ConditionVariable;
+
+ CountingLockable(const CountingLockable&) = delete;
+ CountingLockable& operator=(const CountingLockable&) = delete;
+ protected:
+ L mutex;
+ unsigned int count = 0;
+
+ public:
+ inline CountingLockable() = default;
+
+ // STL Lockable concept
+ inline void lock() { mutex.lock(); count++; }
+ inline bool try_lock() { return mutex.try_lock() ? count++, true : false; }
+ inline void unlock() { count--; mutex.unlock(); }
+
+ /*!
+ * \brief Check if have a lock owned
+ * \return True if have a lock, otherwise false
+ */
+ inline bool IsLocked() const { return count > 0; }
+
+ /**
+ * This implements the "exitable" behavior mentioned above.
+ */
+ inline unsigned int exit(unsigned int leave = 0)
+ {
+ // it's possible we don't actually own the lock
+ // so we will try it.
+ unsigned int ret = 0;
+ if (try_lock())
+ {
+ if (leave < (count - 1))
+ {
+ ret = count - 1 - leave; // The -1 is because we don't want
+ // to count the try_lock increment.
+ // We must NOT compare "count" in this loop since
+ // as soon as the last unlock is called another thread
+ // can modify it.
+ for (unsigned int i = 0; i < ret; i++)
+ unlock();
+ }
+ unlock(); // undo the try_lock before returning
+ }
+
+ return ret;
+ }
+
+ /**
+ * Restore a previous exit to the provided level.
+ */
+ inline void restore(unsigned int restoreCount)
+ {
+ for (unsigned int i = 0; i < restoreCount; i++)
+ lock();
+ }
+
+ /**
+ * Some implementations (see pthreads) require access to the underlying
+ * CCriticalSection, which is also implementation specific. This
+ * provides access to it through the same method on the guard classes
+ * UniqueLock, and SharedLock.
+ *
+ * There really should be no need for the users of the threading library
+ * to call this method.
+ */
+ inline L& get_underlying() { return mutex; }
+ };
+
+}
diff --git a/xbmc/threads/SharedSection.h b/xbmc/threads/SharedSection.h
new file mode 100644
index 0000000..dce371d
--- /dev/null
+++ b/xbmc/threads/SharedSection.h
@@ -0,0 +1,54 @@
+/*
+ * 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 "threads/Condition.h"
+
+#include <mutex>
+#include <shared_mutex>
+
+/**
+ * A CSharedSection is a mutex that satisfies the Shared Lockable concept (see Lockables.h).
+ */
+class CSharedSection
+{
+ CCriticalSection sec;
+ XbmcThreads::ConditionVariable actualCv;
+
+ unsigned int sharedCount = 0;
+
+public:
+ inline CSharedSection() = default;
+
+ inline void lock()
+ {
+ std::unique_lock<CCriticalSection> l(sec);
+ while (sharedCount)
+ actualCv.wait(l, [this]() { return sharedCount == 0; });
+ sec.lock();
+ }
+ inline bool try_lock() { return (sec.try_lock() ? ((sharedCount == 0) ? true : (sec.unlock(), false)) : false); }
+ inline void unlock() { sec.unlock(); }
+
+ inline void lock_shared()
+ {
+ std::unique_lock<CCriticalSection> l(sec);
+ sharedCount++;
+ }
+ inline bool try_lock_shared() { return (sec.try_lock() ? sharedCount++, sec.unlock(), true : false); }
+ inline void unlock_shared()
+ {
+ std::unique_lock<CCriticalSection> l(sec);
+ sharedCount--;
+ if (!sharedCount)
+ {
+ actualCv.notifyAll();
+ }
+ }
+};
diff --git a/xbmc/threads/SingleLock.h b/xbmc/threads/SingleLock.h
new file mode 100644
index 0000000..cacb691
--- /dev/null
+++ b/xbmc/threads/SingleLock.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2002 Frodo
+ * Portions Copyright (c) by the authors of ffmpeg and xvid
+ * Copyright (C) 2002-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 <mutex>
+
+/**
+ * This implements a "guard" pattern for exiting all locks
+ * currently being held by the current thread and restoring
+ * those locks on destruction.
+ *
+ * This class can be used on a CCriticalSection that isn't owned
+ * by this thread in which case it will do nothing.
+ */
+class CSingleExit
+{
+ CCriticalSection& sec;
+ unsigned int count;
+public:
+ inline explicit CSingleExit(CCriticalSection& cs) : sec(cs), count(cs.exit()) { }
+ inline ~CSingleExit() { sec.restore(count); }
+};
diff --git a/xbmc/threads/SystemClock.h b/xbmc/threads/SystemClock.h
new file mode 100644
index 0000000..92c4901
--- /dev/null
+++ b/xbmc/threads/SystemClock.h
@@ -0,0 +1,96 @@
+/*
+ * 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 "utils/log.h"
+
+#include <chrono>
+#include <limits>
+#include <thread>
+
+namespace XbmcThreads
+{
+
+template<typename>
+struct is_chrono_duration : std::false_type
+{
+};
+
+template<typename Rep, typename Period>
+struct is_chrono_duration<std::chrono::duration<Rep, Period>> : std::true_type
+{
+};
+
+template<typename T = std::chrono::milliseconds, bool = is_chrono_duration<T>::value>
+class EndTime;
+
+template<typename T>
+class EndTime<T, true>
+{
+public:
+ explicit EndTime(const T duration) { Set(duration); }
+
+ EndTime() = default;
+ EndTime(const EndTime& right) = delete;
+ ~EndTime() = default;
+
+ static constexpr T Max() { return m_max; }
+
+ void Set(const T duration)
+ {
+ m_startTime = std::chrono::steady_clock::now();
+
+ if (duration > m_max)
+ {
+ m_totalWaitTime = m_max;
+ CLog::Log(LOGWARNING, "duration ({}) greater than max ({}) - duration will be truncated!",
+ duration.count(), m_max.count());
+ }
+ else
+ {
+ m_totalWaitTime = duration;
+ }
+ }
+
+ bool IsTimePast() const
+ {
+ const auto now = std::chrono::steady_clock::now();
+
+ return ((now - m_startTime) >= m_totalWaitTime);
+ }
+
+ T GetTimeLeft() const
+ {
+ const auto now = std::chrono::steady_clock::now();
+
+ const auto left = ((m_startTime + m_totalWaitTime) - now);
+
+ if (left < T::zero())
+ return T::zero();
+
+ return std::chrono::duration_cast<T>(left);
+ }
+
+ void SetExpired() { m_totalWaitTime = T::zero(); }
+
+ void SetInfinite() { m_totalWaitTime = m_max; }
+
+ T GetInitialTimeoutValue() const { return m_totalWaitTime; }
+
+ std::chrono::steady_clock::time_point GetStartTime() const { return m_startTime; }
+
+private:
+ std::chrono::steady_clock::time_point m_startTime;
+ T m_totalWaitTime = T::zero();
+
+ static constexpr T m_max =
+ std::chrono::duration_cast<T>(std::chrono::steady_clock::duration::max());
+};
+
+} // namespace XbmcThreads
diff --git a/xbmc/threads/Thread.cpp b/xbmc/threads/Thread.cpp
new file mode 100644
index 0000000..e23baa7
--- /dev/null
+++ b/xbmc/threads/Thread.cpp
@@ -0,0 +1,282 @@
+/*
+ * Copyright (c) 2002 Frodo
+ * Portions Copyright (c) by the authors of ffmpeg and xvid
+ * Copyright (C) 2002-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 "Thread.h"
+
+#include "IRunnable.h"
+#include "commons/Exception.h"
+#include "threads/IThreadImpl.h"
+#include "threads/SingleLock.h"
+#include "utils/log.h"
+
+#include <atomic>
+#include <inttypes.h>
+#include <iostream>
+#include <mutex>
+#include <stdlib.h>
+
+static thread_local CThread* currentThread;
+
+//////////////////////////////////////////////////////////////////////
+// Construction/Destruction
+//////////////////////////////////////////////////////////////////////
+
+CThread::CThread(const char* ThreadName)
+:
+ m_bStop(false), m_StopEvent(true, true), m_StartEvent(true, true), m_pRunnable(nullptr)
+{
+ if (ThreadName)
+ m_ThreadName = ThreadName;
+}
+
+CThread::CThread(IRunnable* pRunnable, const char* ThreadName)
+:
+ m_bStop(false), m_StopEvent(true, true), m_StartEvent(true, true), m_pRunnable(pRunnable)
+{
+ if (ThreadName)
+ m_ThreadName = ThreadName;
+}
+
+CThread::~CThread()
+{
+ StopThread();
+ if (m_thread != nullptr)
+ {
+ m_thread->detach();
+ delete m_thread;
+ }
+}
+
+void CThread::Create(bool bAutoDelete)
+{
+ if (m_thread != nullptr)
+ {
+ // if the thread exited on it's own, without a call to StopThread, then we can get here
+ // incorrectly. We should be able to determine this by checking the promise.
+ std::future_status stat = m_future.wait_for(std::chrono::milliseconds(0));
+ // a status of 'ready' means the future contains the value so the thread has exited
+ // since the thread can't exit without setting the future.
+ if (stat == std::future_status::ready) // this is an indication the thread has exited.
+ StopThread(true); // so let's just clean up
+ else
+ { // otherwise we have a problem.
+ CLog::Log(LOGERROR, "{} - fatal error creating thread {} - old thread id not null",
+ __FUNCTION__, m_ThreadName);
+ exit(1);
+ }
+ }
+
+ m_bAutoDelete = bAutoDelete;
+ m_bStop = false;
+ m_StopEvent.Reset();
+ m_StartEvent.Reset();
+
+ // lock?
+ //std::unique_lock<CCriticalSection> l(m_CriticalSection);
+
+ std::promise<bool> prom;
+ m_future = prom.get_future();
+
+ {
+ // The std::thread internals must be set prior to the lambda doing
+ // any work. This will cause the lambda to wait until m_thread
+ // is fully initialized. Interestingly, using a std::atomic doesn't
+ // have the appropriate memory barrier behavior to accomplish the
+ // same thing so a full system mutex needs to be used.
+ std::unique_lock<CCriticalSection> blockLambdaTillDone(m_CriticalSection);
+ m_thread = new std::thread([](CThread* pThread, std::promise<bool> promise)
+ {
+ try
+ {
+
+ {
+ // Wait for the pThread->m_thread internals to be set. Otherwise we could
+ // get to a place where we're reading, say, the thread id inside this
+ // lambda's call stack prior to the thread that kicked off this lambda
+ // having it set. Once this lock is released, the CThread::Create function
+ // that kicked this off is done so everything should be set.
+ std::unique_lock<CCriticalSection> waitForThreadInternalsToBeSet(
+ pThread->m_CriticalSection);
+ }
+
+ // This is used in various helper methods like GetCurrentThread so it needs
+ // to be set before anything else is done.
+ currentThread = pThread;
+
+ std::string name;
+ bool autodelete;
+
+ if (pThread == nullptr)
+ {
+ CLog::Log(LOGERROR, "{}, sanity failed. thread is NULL.", __FUNCTION__);
+ promise.set_value(false);
+ return;
+ }
+
+ name = pThread->m_ThreadName;
+
+ std::stringstream ss;
+ ss << std::this_thread::get_id();
+ std::string id = ss.str();
+ autodelete = pThread->m_bAutoDelete;
+
+ pThread->m_impl = IThreadImpl::CreateThreadImpl(pThread->m_thread->native_handle());
+ pThread->m_impl->SetThreadInfo(name);
+
+ CLog::Log(LOGDEBUG, "Thread {} start, auto delete: {}", name,
+ (autodelete ? "true" : "false"));
+
+ pThread->m_StartEvent.Set();
+
+ pThread->Action();
+
+ if (autodelete)
+ {
+ CLog::Log(LOGDEBUG, "Thread {} {} terminating (autodelete)", name, id);
+ delete pThread;
+ pThread = NULL;
+ }
+ else
+ CLog::Log(LOGDEBUG, "Thread {} {} terminating", name, id);
+ }
+ catch (const std::exception& e)
+ {
+ CLog::Log(LOGDEBUG, "Thread Terminating with Exception: {}", e.what());
+ }
+ catch (...)
+ {
+ CLog::Log(LOGDEBUG,"Thread Terminating with Exception");
+ }
+
+ promise.set_value(true);
+ }, this, std::move(prom));
+ } // let the lambda proceed
+
+ m_StartEvent.Wait(); // wait for the thread just spawned to set its internals
+}
+
+bool CThread::IsRunning() const
+{
+ if (m_thread != nullptr) {
+ // it's possible that the thread exited on it's own without a call to StopThread. If so then
+ // the promise should be fulfilled.
+ std::future_status stat = m_future.wait_for(std::chrono::milliseconds(0));
+ // a status of 'ready' means the future contains the value so the thread has exited
+ // since the thread can't exit without setting the future.
+ if (stat == std::future_status::ready) // this is an indication the thread has exited.
+ return false;
+ return true; // otherwise the thread is still active.
+ } else
+ return false;
+}
+
+bool CThread::SetPriority(const ThreadPriority& priority)
+{
+ return m_impl->SetPriority(priority);
+}
+
+bool CThread::IsAutoDelete() const
+{
+ return m_bAutoDelete;
+}
+
+void CThread::StopThread(bool bWait /*= true*/)
+{
+ m_StartEvent.Wait();
+
+ m_bStop = true;
+ m_StopEvent.Set();
+ std::unique_lock<CCriticalSection> lock(m_CriticalSection);
+ std::thread* lthread = m_thread;
+ if (lthread != nullptr && bWait && !IsCurrentThread())
+ {
+ lock.unlock();
+ if (!Join(std::chrono::milliseconds::max())) // eh?
+ lthread->join();
+ m_thread = nullptr;
+ }
+}
+
+void CThread::Process()
+{
+ if (m_pRunnable)
+ m_pRunnable->Run();
+}
+
+bool CThread::IsCurrentThread() const
+{
+ CThread* pThread = currentThread;
+ if (pThread != nullptr)
+ return pThread == this;
+ else
+ return false;
+}
+
+CThread* CThread::GetCurrentThread()
+{
+ return currentThread;
+}
+
+bool CThread::Join(std::chrono::milliseconds duration)
+{
+ std::unique_lock<CCriticalSection> l(m_CriticalSection);
+ std::thread* lthread = m_thread;
+ if (lthread != nullptr)
+ {
+ if (IsCurrentThread())
+ return false;
+
+ {
+ CSingleExit exit(m_CriticalSection); // don't hold the thread lock while we're waiting
+ std::future_status stat = m_future.wait_for(duration);
+ if (stat != std::future_status::ready)
+ return false;
+ }
+
+ // it's possible it's already joined since we released the lock above.
+ if (lthread->joinable())
+ m_thread->join();
+ return true;
+ }
+ else
+ return false;
+}
+
+void CThread::Action()
+{
+ try
+ {
+ OnStartup();
+ }
+ catch (const XbmcCommons::UncheckedException &e)
+ {
+ e.LogThrowMessage("OnStartup");
+ if (IsAutoDelete())
+ return;
+ }
+
+ try
+ {
+ Process();
+ }
+ catch (const XbmcCommons::UncheckedException &e)
+ {
+ e.LogThrowMessage("Process");
+ }
+
+ try
+ {
+ OnExit();
+ }
+ catch (const XbmcCommons::UncheckedException &e)
+ {
+ e.LogThrowMessage("OnExit");
+ }
+}
diff --git a/xbmc/threads/Thread.h b/xbmc/threads/Thread.h
new file mode 100644
index 0000000..c657d1b
--- /dev/null
+++ b/xbmc/threads/Thread.h
@@ -0,0 +1,125 @@
+/*
+ * 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
+
+// Thread.h: interface for the CThread class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "Event.h"
+
+#include <atomic>
+#include <future>
+#ifdef TARGET_DARWIN
+#include <mach/mach.h>
+#endif
+#include <stdint.h>
+#include <string>
+#include <thread>
+
+enum class ThreadPriority
+{
+ LOWEST,
+ BELOW_NORMAL,
+ NORMAL,
+ ABOVE_NORMAL,
+ HIGHEST,
+
+ /*!
+ * \brief Do not use this for priority. It is only needed to count the
+ * amount of values in the ThreadPriority enum.
+ *
+ */
+ PRIORITY_COUNT,
+};
+
+class IRunnable;
+class IThreadImpl;
+class CThread
+{
+protected:
+ explicit CThread(const char* ThreadName);
+
+public:
+ CThread(IRunnable* pRunnable, const char* ThreadName);
+ virtual ~CThread();
+ void Create(bool bAutoDelete = false);
+
+ template<typename Rep, typename Period>
+ void Sleep(std::chrono::duration<Rep, Period> duration)
+ {
+ if (duration > std::chrono::milliseconds(10) && IsCurrentThread())
+ m_StopEvent.Wait(duration);
+ else
+ std::this_thread::sleep_for(duration);
+ }
+
+ bool IsAutoDelete() const;
+ virtual void StopThread(bool bWait = true);
+ bool IsRunning() const;
+
+ bool IsCurrentThread() const;
+ bool Join(std::chrono::milliseconds duration);
+
+ inline static const std::thread::id GetCurrentThreadId()
+ {
+ return std::this_thread::get_id();
+ }
+
+ /*!
+ * \brief Set the threads priority. This uses the platforms
+ * native threading library to do so.
+ *
+ */
+ bool SetPriority(const ThreadPriority& priority);
+
+ static CThread* GetCurrentThread();
+
+ virtual void OnException(){} // signal termination handler
+
+protected:
+ virtual void OnStartup() {}
+ virtual void OnExit() {}
+ virtual void Process();
+
+ std::atomic<bool> m_bStop;
+
+ enum WaitResponse { WAIT_INTERRUPTED = -1, WAIT_SIGNALED = 0, WAIT_TIMEDOUT = 1 };
+
+ /**
+ * This call will wait on a CEvent in an interruptible way such that if
+ * stop is called on the thread the wait will return with a response
+ * indicating what happened.
+ */
+ inline WaitResponse AbortableWait(CEvent& event,
+ std::chrono::milliseconds duration =
+ std::chrono::milliseconds(-1) /* indicates wait forever*/)
+ {
+ XbmcThreads::CEventGroup group{&event, &m_StopEvent};
+ const CEvent* result =
+ duration < std::chrono::milliseconds::zero() ? group.wait() : group.wait(duration);
+ return result == &event ? WAIT_SIGNALED :
+ (result == NULL ? WAIT_TIMEDOUT : WAIT_INTERRUPTED);
+ }
+
+private:
+ void Action();
+
+ bool m_bAutoDelete = false;
+ CEvent m_StopEvent;
+ CEvent m_StartEvent;
+ CCriticalSection m_CriticalSection;
+ IRunnable* m_pRunnable;
+
+ std::string m_ThreadName;
+ std::thread* m_thread = nullptr;
+ std::future<bool> m_future;
+
+ std::unique_ptr<IThreadImpl> m_impl;
+};
diff --git a/xbmc/threads/Timer.cpp b/xbmc/threads/Timer.cpp
new file mode 100644
index 0000000..d06ba40
--- /dev/null
+++ b/xbmc/threads/Timer.cpp
@@ -0,0 +1,109 @@
+/*
+ * 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 "Timer.h"
+
+#include <algorithm>
+
+using namespace std::chrono_literals;
+
+CTimer::CTimer(std::function<void()> const& callback)
+ : CThread("Timer"), m_callback(callback), m_timeout(0ms), m_interval(false)
+{ }
+
+CTimer::CTimer(ITimerCallback *callback)
+ : CTimer(std::bind(&ITimerCallback::OnTimeout, callback))
+{ }
+
+CTimer::~CTimer()
+{
+ Stop(true);
+}
+
+bool CTimer::Start(std::chrono::milliseconds timeout, bool interval /* = false */)
+{
+ if (m_callback == NULL || timeout == 0ms || IsRunning())
+ return false;
+
+ m_timeout = timeout;
+ m_interval = interval;
+
+ Create();
+ return true;
+}
+
+bool CTimer::Stop(bool wait /* = false */)
+{
+ if (!IsRunning())
+ return false;
+
+ m_bStop = true;
+ m_eventTimeout.Set();
+ StopThread(wait);
+
+ return true;
+}
+
+void CTimer::RestartAsync(std::chrono::milliseconds timeout)
+{
+ m_timeout = timeout;
+ m_endTime = std::chrono::steady_clock::now() + timeout;
+ m_eventTimeout.Set();
+}
+
+bool CTimer::Restart()
+{
+ if (!IsRunning())
+ return false;
+
+ Stop(true);
+
+ return Start(m_timeout, m_interval);
+}
+
+float CTimer::GetElapsedSeconds() const
+{
+ return GetElapsedMilliseconds() / 1000.0f;
+}
+
+float CTimer::GetElapsedMilliseconds() const
+{
+ if (!IsRunning())
+ return 0.0f;
+
+ auto now = std::chrono::steady_clock::now();
+ std::chrono::duration<float, std::milli> duration = (now - (m_endTime - m_timeout));
+
+ return duration.count();
+}
+
+void CTimer::Process()
+{
+ while (!m_bStop)
+ {
+ auto currentTime = std::chrono::steady_clock::now();
+ m_endTime = currentTime + m_timeout;
+
+ // wait the necessary time
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(m_endTime - currentTime);
+
+ if (!m_eventTimeout.Wait(duration))
+ {
+ currentTime = std::chrono::steady_clock::now();
+ if (m_endTime <= currentTime)
+ {
+ // execute OnTimeout() callback
+ m_callback();
+
+ // continue if this is an interval timer, or if it was restarted during callback
+ if (!m_interval && m_endTime <= currentTime)
+ break;
+ }
+ }
+ }
+}
diff --git a/xbmc/threads/Timer.h b/xbmc/threads/Timer.h
new file mode 100644
index 0000000..f434a38
--- /dev/null
+++ b/xbmc/threads/Timer.h
@@ -0,0 +1,51 @@
+/*
+ * 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 "Event.h"
+#include "Thread.h"
+
+#include <chrono>
+#include <functional>
+
+class ITimerCallback
+{
+public:
+ virtual ~ITimerCallback() = default;
+
+ virtual void OnTimeout() = 0;
+};
+
+class CTimer : protected CThread
+{
+public:
+ explicit CTimer(ITimerCallback *callback);
+ explicit CTimer(std::function<void()> const& callback);
+ ~CTimer() override;
+
+ bool Start(std::chrono::milliseconds timeout, bool interval = false);
+ bool Stop(bool wait = false);
+ bool Restart();
+ void RestartAsync(std::chrono::milliseconds timeout);
+
+ bool IsRunning() const { return CThread::IsRunning(); }
+
+ float GetElapsedSeconds() const;
+ float GetElapsedMilliseconds() const;
+
+protected:
+ void Process() override;
+
+private:
+ std::function<void()> m_callback;
+ std::chrono::milliseconds m_timeout;
+ bool m_interval;
+ std::chrono::time_point<std::chrono::steady_clock> m_endTime;
+ CEvent m_eventTimeout;
+};
diff --git a/xbmc/threads/test/CMakeLists.txt b/xbmc/threads/test/CMakeLists.txt
new file mode 100644
index 0000000..136e972
--- /dev/null
+++ b/xbmc/threads/test/CMakeLists.txt
@@ -0,0 +1,7 @@
+set(SOURCES TestEvent.cpp
+ TestSharedSection.cpp
+ TestEndTime.cpp)
+
+set(HEADERS TestHelpers.h)
+
+core_add_test_library(threads_test)
diff --git a/xbmc/threads/test/TestEndTime.cpp b/xbmc/threads/test/TestEndTime.cpp
new file mode 100644
index 0000000..fb1c6d8
--- /dev/null
+++ b/xbmc/threads/test/TestEndTime.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2005-2020 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 "threads/SystemClock.h"
+
+#include <gtest/gtest.h>
+
+using namespace std::chrono_literals;
+
+namespace
+{
+
+template<typename T = std::chrono::milliseconds>
+void CommonTests(XbmcThreads::EndTime<T>& endTime)
+{
+ EXPECT_EQ(100ms, endTime.GetInitialTimeoutValue());
+ EXPECT_LT(T::zero(), endTime.GetStartTime().time_since_epoch());
+
+ EXPECT_FALSE(endTime.IsTimePast());
+ EXPECT_LT(T::zero(), endTime.GetTimeLeft());
+
+ std::this_thread::sleep_for(100ms);
+
+ EXPECT_TRUE(endTime.IsTimePast());
+ EXPECT_EQ(T::zero(), endTime.GetTimeLeft());
+
+ endTime.SetInfinite();
+ EXPECT_GE(T::max(), endTime.GetInitialTimeoutValue());
+ EXPECT_EQ(XbmcThreads::EndTime<T>::Max(), endTime.GetInitialTimeoutValue());
+ endTime.SetExpired();
+ EXPECT_EQ(T::zero(), endTime.GetInitialTimeoutValue());
+}
+
+} // namespace
+
+TEST(TestEndTime, DefaultConstructor)
+{
+ XbmcThreads::EndTime<> endTime;
+ endTime.Set(100ms);
+
+ CommonTests(endTime);
+}
+
+TEST(TestEndTime, ExplicitConstructor)
+{
+ XbmcThreads::EndTime<> endTime(100ms);
+
+ CommonTests(endTime);
+}
+
+TEST(TestEndTime, DoubleMicroSeconds)
+{
+ XbmcThreads::EndTime<std::chrono::duration<double, std::micro>> endTime(100ms);
+
+ CommonTests(endTime);
+}
+
+TEST(TestEndTime, SteadyClockDuration)
+{
+ XbmcThreads::EndTime<std::chrono::steady_clock::duration> endTime(100ms);
+
+ CommonTests(endTime);
+
+ endTime.SetInfinite();
+ EXPECT_EQ(std::chrono::steady_clock::duration::max(), endTime.GetInitialTimeoutValue());
+
+ endTime.SetExpired();
+ EXPECT_EQ(std::chrono::steady_clock::duration::zero(), endTime.GetInitialTimeoutValue());
+
+ endTime.Set(endTime.Max());
+ EXPECT_EQ(std::chrono::steady_clock::duration::max(), endTime.GetInitialTimeoutValue());
+}
diff --git a/xbmc/threads/test/TestEvent.cpp b/xbmc/threads/test/TestEvent.cpp
new file mode 100644
index 0000000..c4526b3
--- /dev/null
+++ b/xbmc/threads/test/TestEvent.cpp
@@ -0,0 +1,628 @@
+/*
+ * 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 "threads/Event.h"
+#include "threads/IRunnable.h"
+#include "threads/test/TestHelpers.h"
+
+#include <memory>
+#include <stdio.h>
+
+using namespace XbmcThreads;
+using namespace std::chrono_literals;
+
+//=============================================================================
+// Helper classes
+//=============================================================================
+
+class waiter : public IRunnable
+{
+ CEvent& event;
+public:
+ bool& result;
+
+ volatile bool waiting;
+
+ waiter(CEvent& o, bool& flag) : event(o), result(flag), waiting(false) {}
+
+ void Run() override
+ {
+ waiting = true;
+ result = event.Wait();
+ waiting = false;
+ }
+};
+
+class timed_waiter : public IRunnable
+{
+ CEvent& event;
+ std::chrono::milliseconds waitTime;
+
+public:
+ int& result;
+
+ volatile bool waiting;
+
+ timed_waiter(CEvent& o, int& flag, std::chrono::milliseconds waitTimeMillis)
+ : event(o), waitTime(waitTimeMillis), result(flag), waiting(false)
+ {
+ }
+
+ void Run() override
+ {
+ waiting = true;
+ result = 0;
+ result = event.Wait(waitTime) ? 1 : -1;
+ waiting = false;
+ }
+};
+
+class group_wait : public IRunnable
+{
+ CEventGroup& event;
+ std::chrono::milliseconds timeout;
+
+public:
+ CEvent* result;
+ bool waiting;
+
+ group_wait(CEventGroup& o) : event(o), timeout(-1ms), result(NULL), waiting(false) {}
+
+ group_wait(CEventGroup& o, std::chrono::milliseconds timeout_)
+ : event(o), timeout(timeout_), result(NULL), waiting(false)
+ {
+ }
+
+ void Run() override
+ {
+ waiting = true;
+ if (timeout == -1ms)
+ result = event.wait();
+ else
+ result = event.wait(timeout);
+ waiting = false;
+ }
+};
+
+//=============================================================================
+
+TEST(TestEvent, General)
+{
+ CEvent event;
+ bool result = false;
+ waiter w1(event,result);
+ thread waitThread(w1);
+
+ EXPECT_TRUE(waitForWaiters(event, 1, 10000ms));
+
+ EXPECT_TRUE(!result);
+
+ event.Set();
+
+ EXPECT_TRUE(waitThread.timed_join(10000ms));
+
+ EXPECT_TRUE(result);
+}
+
+TEST(TestEvent, TwoWaits)
+{
+ CEvent event;
+ bool result1 = false;
+ bool result2 = false;
+ waiter w1(event,result1);
+ waiter w2(event,result2);
+ thread waitThread1(w1);
+ thread waitThread2(w2);
+
+ EXPECT_TRUE(waitForWaiters(event, 2, 10000ms));
+
+ EXPECT_TRUE(!result1);
+ EXPECT_TRUE(!result2);
+
+ event.Set();
+
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+ EXPECT_TRUE(waitThread2.timed_join(10000ms));
+
+ EXPECT_TRUE(result1);
+ EXPECT_TRUE(result2);
+
+}
+
+TEST(TestEvent, TimedWaits)
+{
+ CEvent event;
+ int result1 = 10;
+ timed_waiter w1(event, result1, 100ms);
+ thread waitThread1(w1);
+
+ EXPECT_TRUE(waitForWaiters(event, 1, 10000ms));
+
+ EXPECT_TRUE(result1 == 0);
+
+ event.Set();
+
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+
+ EXPECT_TRUE(result1 == 1);
+}
+
+TEST(TestEvent, TimedWaitsTimeout)
+{
+ CEvent event;
+ int result1 = 10;
+ timed_waiter w1(event, result1, 50ms);
+ thread waitThread1(w1);
+
+ EXPECT_TRUE(waitForWaiters(event, 1, 100ms));
+
+ EXPECT_TRUE(result1 == 0);
+
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+
+ EXPECT_TRUE(result1 == -1);
+}
+
+TEST(TestEvent, Group)
+{
+ CEvent event1;
+ CEvent event2;
+
+ CEventGroup group{&event1,&event2};
+
+ bool result1 = false;
+ bool result2 = false;
+
+ waiter w1(event1,result1);
+ waiter w2(event2,result2);
+ group_wait w3(group);
+
+ thread waitThread1(w1);
+ thread waitThread2(w2);
+ thread waitThread3(w3);
+
+ EXPECT_TRUE(waitForWaiters(event1, 1, 10000ms));
+ EXPECT_TRUE(waitForWaiters(event2, 1, 10000ms));
+ EXPECT_TRUE(waitForWaiters(group, 1, 10000ms));
+
+ EXPECT_TRUE(!result1);
+ EXPECT_TRUE(!result2);
+
+ EXPECT_TRUE(w3.waiting);
+ EXPECT_TRUE(w3.result == NULL);
+
+ event1.Set();
+
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+ EXPECT_TRUE(waitThread3.timed_join(10000ms));
+ std::this_thread::sleep_for(10ms);
+
+ EXPECT_TRUE(result1);
+ EXPECT_TRUE(!w1.waiting);
+ EXPECT_TRUE(!result2);
+ EXPECT_TRUE(w2.waiting);
+ EXPECT_TRUE(!w3.waiting);
+ EXPECT_TRUE(w3.result == &event1);
+
+ event2.Set();
+
+ EXPECT_TRUE(waitThread2.timed_join(10000ms));
+}
+
+/* Test disabled for now, because it deadlocks
+TEST(TestEvent, GroupLimitedGroupScope)
+{
+ CEvent event1;
+ CEvent event2;
+
+ {
+ CEventGroup group(&event1,&event2,NULL);
+
+ bool result1 = false;
+ bool result2 = false;
+
+ waiter w1(event1,result1);
+ waiter w2(event2,result2);
+ group_wait w3(group);
+
+ thread waitThread1(w1);
+ thread waitThread2(w2);
+ thread waitThread3(w3);
+
+ EXPECT_TRUE(waitForWaiters(event1,1,10000ms));
+ EXPECT_TRUE(waitForWaiters(event2,1,10000ms));
+ EXPECT_TRUE(waitForWaiters(group,1,10000ms));
+
+ EXPECT_TRUE(!result1);
+ EXPECT_TRUE(!result2);
+
+ EXPECT_TRUE(w3.waiting);
+ EXPECT_TRUE(w3.result == NULL);
+
+ event1.Set();
+
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+ EXPECT_TRUE(waitThread3.timed_join(10000ms));
+ std::this_thread::sleep_for(10ms);
+
+ EXPECT_TRUE(result1);
+ EXPECT_TRUE(!w1.waiting);
+ EXPECT_TRUE(!result2);
+ EXPECT_TRUE(w2.waiting);
+ EXPECT_TRUE(!w3.waiting);
+ EXPECT_TRUE(w3.result == &event1);
+ }
+
+ event2.Set();
+
+ std::this_thread::sleep_for(50ms); // give thread 2 a chance to exit
+}*/
+
+TEST(TestEvent, TwoGroups)
+{
+ CEvent event1;
+ CEvent event2;
+
+ CEventGroup group1{&event1,&event2};
+ CEventGroup group2{&event1,&event2};
+
+ bool result1 = false;
+ bool result2 = false;
+
+ waiter w1(event1,result1);
+ waiter w2(event2,result2);
+ group_wait w3(group1);
+ group_wait w4(group2);
+
+ thread waitThread1(w1);
+ thread waitThread2(w2);
+ thread waitThread3(w3);
+ thread waitThread4(w4);
+
+ EXPECT_TRUE(waitForWaiters(event1, 1, 10000ms));
+ EXPECT_TRUE(waitForWaiters(event2, 1, 10000ms));
+ EXPECT_TRUE(waitForWaiters(group1, 1, 10000ms));
+ EXPECT_TRUE(waitForWaiters(group2, 1, 10000ms));
+
+ EXPECT_TRUE(!result1);
+ EXPECT_TRUE(!result2);
+
+ EXPECT_TRUE(w3.waiting);
+ EXPECT_EQ(w3.result,(void*)NULL);
+ EXPECT_TRUE(w4.waiting);
+ EXPECT_EQ(w4.result,(void*)NULL);
+
+ event1.Set();
+
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+ EXPECT_TRUE(waitThread3.timed_join(10000ms));
+ EXPECT_TRUE(waitThread4.timed_join(10000ms));
+ std::this_thread::sleep_for(10ms);
+
+ EXPECT_TRUE(result1);
+ EXPECT_TRUE(!w1.waiting);
+ EXPECT_TRUE(!result2);
+ EXPECT_TRUE(w2.waiting);
+ EXPECT_TRUE(!w3.waiting);
+ EXPECT_TRUE(w3.result == &event1);
+ EXPECT_TRUE(!w4.waiting);
+ EXPECT_TRUE(w4.result == &event1);
+
+ event2.Set();
+
+ EXPECT_TRUE(waitThread2.timed_join(10000ms));
+}
+
+TEST(TestEvent, AutoResetBehavior)
+{
+ CEvent event;
+
+ EXPECT_TRUE(!event.Wait(1ms));
+
+ event.Set(); // event will remain signaled if there are no waits
+
+ EXPECT_TRUE(event.Wait(1ms));
+}
+
+TEST(TestEvent, ManualReset)
+{
+ CEvent event(true);
+ bool result = false;
+ waiter w1(event,result);
+ thread waitThread(w1);
+
+ EXPECT_TRUE(waitForWaiters(event, 1, 10000ms));
+
+ EXPECT_TRUE(!result);
+
+ event.Set();
+
+ EXPECT_TRUE(waitThread.timed_join(10000ms));
+
+ EXPECT_TRUE(result);
+
+ // with manual reset, the state should remain signaled
+ EXPECT_TRUE(event.Wait(1ms));
+
+ event.Reset();
+
+ EXPECT_TRUE(!event.Wait(1ms));
+}
+
+TEST(TestEvent, InitVal)
+{
+ CEvent event(false,true);
+ EXPECT_TRUE(event.Wait(50ms));
+}
+
+TEST(TestEvent, SimpleTimeout)
+{
+ CEvent event;
+ EXPECT_TRUE(!event.Wait(50ms));
+}
+
+TEST(TestEvent, GroupChildSet)
+{
+ CEvent event1(true);
+ CEvent event2;
+
+ event1.Set();
+ CEventGroup group{&event1,&event2};
+
+ bool result1 = false;
+ bool result2 = false;
+
+ waiter w1(event1,result1);
+ waiter w2(event2,result2);
+ group_wait w3(group);
+
+ thread waitThread1(w1);
+ thread waitThread2(w2);
+ thread waitThread3(w3);
+
+ EXPECT_TRUE(waitForWaiters(event2, 1, 10000ms));
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+ EXPECT_TRUE(waitThread3.timed_join(10000ms));
+ std::this_thread::sleep_for(10ms);
+
+ EXPECT_TRUE(result1);
+ EXPECT_TRUE(!result2);
+
+ EXPECT_TRUE(!w3.waiting);
+ EXPECT_TRUE(w3.result == &event1);
+
+ event2.Set();
+
+ EXPECT_TRUE(waitThread2.timed_join(10000ms));
+}
+
+TEST(TestEvent, GroupChildSet2)
+{
+ CEvent event1(true,true);
+ CEvent event2;
+
+ CEventGroup group{&event1,&event2};
+
+ bool result1 = false;
+ bool result2 = false;
+
+ waiter w1(event1,result1);
+ waiter w2(event2,result2);
+ group_wait w3(group);
+
+ thread waitThread1(w1);
+ thread waitThread2(w2);
+ thread waitThread3(w3);
+
+ EXPECT_TRUE(waitForWaiters(event2, 1, 10000ms));
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+ EXPECT_TRUE(waitThread3.timed_join(10000ms));
+ std::this_thread::sleep_for(10ms);
+
+ EXPECT_TRUE(result1);
+ EXPECT_TRUE(!result2);
+
+ EXPECT_TRUE(!w3.waiting);
+ EXPECT_TRUE(w3.result == &event1);
+
+ event2.Set();
+
+ EXPECT_TRUE(waitThread2.timed_join(10000ms));
+}
+
+TEST(TestEvent, GroupWaitResetsChild)
+{
+ CEvent event1;
+ CEvent event2;
+
+ CEventGroup group{&event1,&event2};
+
+ group_wait w3(group);
+
+ thread waitThread3(w3);
+
+ EXPECT_TRUE(waitForWaiters(group, 1, 10000ms));
+
+ EXPECT_TRUE(w3.waiting);
+ EXPECT_TRUE(w3.result == NULL);
+
+ event2.Set();
+
+ EXPECT_TRUE(waitThread3.timed_join(10000ms));
+
+ EXPECT_TRUE(!w3.waiting);
+ EXPECT_TRUE(w3.result == &event2);
+ // event2 should have been reset.
+ EXPECT_TRUE(event2.Wait(1ms) == false);
+}
+
+TEST(TestEvent, GroupTimedWait)
+{
+ CEvent event1;
+ CEvent event2;
+ CEventGroup group{&event1,&event2};
+
+ bool result1 = false;
+ bool result2 = false;
+
+ waiter w1(event1,result1);
+ waiter w2(event2,result2);
+
+ thread waitThread1(w1);
+ thread waitThread2(w2);
+
+ EXPECT_TRUE(waitForWaiters(event1, 1, 10000ms));
+ EXPECT_TRUE(waitForWaiters(event2, 1, 10000ms));
+
+ EXPECT_TRUE(group.wait(20ms) == NULL); // waited ... got nothing
+
+ group_wait w3(group, 50ms);
+ thread waitThread3(w3);
+
+ EXPECT_TRUE(waitForWaiters(group, 1, 10000ms));
+
+ EXPECT_TRUE(!result1);
+ EXPECT_TRUE(!result2);
+
+ EXPECT_TRUE(w3.waiting);
+ EXPECT_TRUE(w3.result == NULL);
+
+ // this should end given the wait is for only 50 millis
+ EXPECT_TRUE(waitThread3.timed_join(200ms));
+
+ EXPECT_TRUE(!w3.waiting);
+ EXPECT_TRUE(w3.result == NULL);
+
+ group_wait w4(group, 50ms);
+ thread waitThread4(w4);
+
+ EXPECT_TRUE(waitForWaiters(group, 1, 10000ms));
+
+ EXPECT_TRUE(w4.waiting);
+ EXPECT_TRUE(w4.result == NULL);
+
+ event1.Set();
+
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+ EXPECT_TRUE(waitThread4.timed_join(10000ms));
+ std::this_thread::sleep_for(10ms);
+
+ EXPECT_TRUE(result1);
+ EXPECT_TRUE(!result2);
+
+ EXPECT_TRUE(!w4.waiting);
+ EXPECT_TRUE(w4.result == &event1);
+
+ event2.Set();
+
+ EXPECT_TRUE(waitThread2.timed_join(10000ms));
+}
+
+#define TESTNUM 100000l
+#define NUMTHREADS 100l
+
+CEvent* g_event = NULL;
+std::atomic<long> g_mutex;
+
+class mass_waiter : public IRunnable
+{
+public:
+ CEvent& event;
+ bool result;
+
+ volatile bool waiting = false;
+
+ mass_waiter() : event(*g_event) {}
+
+ void Run() override
+ {
+ waiting = true;
+ AtomicGuard g(&g_mutex);
+ result = event.Wait();
+ waiting = false;
+ }
+};
+
+class poll_mass_waiter : public IRunnable
+{
+public:
+ CEvent& event;
+ bool result;
+
+ volatile bool waiting = false;
+
+ poll_mass_waiter() : event(*g_event) {}
+
+ void Run() override
+ {
+ waiting = true;
+ AtomicGuard g(&g_mutex);
+ while ((result = event.Wait(0ms)) == false)
+ ;
+ waiting = false;
+ }
+};
+
+template <class W> void RunMassEventTest(std::vector<std::shared_ptr<W>>& m, bool canWaitOnEvent)
+{
+ std::vector<std::shared_ptr<thread>> t(NUMTHREADS);
+ for(size_t i=0; i<NUMTHREADS; i++)
+ t[i].reset(new thread(*m[i]));
+
+ EXPECT_TRUE(waitForThread(g_mutex, NUMTHREADS, 10000ms));
+ if (canWaitOnEvent)
+ {
+ EXPECT_TRUE(waitForWaiters(*g_event, NUMTHREADS, 10000ms));
+ }
+
+ std::this_thread::sleep_for(100ms); // give them a little more time
+
+ for(size_t i=0; i<NUMTHREADS; i++)
+ {
+ EXPECT_TRUE(m[i]->waiting);
+ }
+
+ g_event->Set();
+
+ for(size_t i=0; i<NUMTHREADS; i++)
+ {
+ EXPECT_TRUE(t[i]->timed_join(10000ms));
+ }
+
+ for(size_t i=0; i<NUMTHREADS; i++)
+ {
+ EXPECT_TRUE(!m[i]->waiting);
+ EXPECT_TRUE(m[i]->result);
+ }
+}
+
+
+TEST(TestMassEvent, General)
+{
+ g_event = new CEvent();
+
+ std::vector<std::shared_ptr<mass_waiter>> m(NUMTHREADS);
+ for(size_t i=0; i<NUMTHREADS; i++)
+ m[i].reset(new mass_waiter());
+
+ RunMassEventTest(m,true);
+ delete g_event;
+}
+
+TEST(TestMassEvent, Polling)
+{
+ g_event = new CEvent(true); // polling needs to avoid the auto-reset
+
+ std::vector<std::shared_ptr<poll_mass_waiter>> m(NUMTHREADS);
+ for(size_t i=0; i<NUMTHREADS; i++)
+ m[i].reset(new poll_mass_waiter());
+
+ RunMassEventTest(m,false);
+ delete g_event;
+}
+
diff --git a/xbmc/threads/test/TestHelpers.h b/xbmc/threads/test/TestHelpers.h
new file mode 100644
index 0000000..4d8752c
--- /dev/null
+++ b/xbmc/threads/test/TestHelpers.h
@@ -0,0 +1,75 @@
+/*
+ * 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 "threads/Thread.h"
+
+#include <memory>
+#include <mutex>
+
+#include <gtest/gtest.h>
+
+template<class E>
+inline static bool waitForWaiters(E& event, int numWaiters, std::chrono::milliseconds duration)
+{
+ for (auto i = std::chrono::milliseconds::zero(); i < duration; i++)
+ {
+ if (event.getNumWaits() == numWaiters)
+ return true;
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+
+ return false;
+}
+
+inline static bool waitForThread(std::atomic<long>& mutex,
+ int numWaiters,
+ std::chrono::milliseconds duration)
+{
+ CCriticalSection sec;
+ for (auto i = std::chrono::milliseconds::zero(); i < duration; i++)
+ {
+ if (mutex == (long)numWaiters)
+ return true;
+
+ {
+ std::unique_lock<CCriticalSection> tmplock(sec); // kick any memory syncs
+ }
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ }
+
+ return false;
+}
+
+class AtomicGuard
+{
+ std::atomic<long>* val;
+public:
+ inline AtomicGuard(std::atomic<long>* val_) : val(val_) { if (val) ++(*val); }
+ inline ~AtomicGuard() { if (val) --(*val); }
+};
+
+class thread
+{
+ std::unique_ptr<CThread> cthread;
+
+public:
+ inline explicit thread(IRunnable& runnable)
+ : cthread(std::make_unique<CThread>(&runnable, "DumbThread"))
+ {
+ cthread->Create();
+ }
+
+ void join() { cthread->Join(std::chrono::milliseconds::max()); }
+
+ bool timed_join(std::chrono::milliseconds duration) { return cthread->Join(duration); }
+};
+
diff --git a/xbmc/threads/test/TestSharedSection.cpp b/xbmc/threads/test/TestSharedSection.cpp
new file mode 100644
index 0000000..fc2ca08
--- /dev/null
+++ b/xbmc/threads/test/TestSharedSection.cpp
@@ -0,0 +1,215 @@
+/*
+ * 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 "threads/Event.h"
+#include "threads/IRunnable.h"
+#include "threads/SharedSection.h"
+#include "threads/test/TestHelpers.h"
+
+#include <mutex>
+#include <shared_mutex>
+#include <stdio.h>
+
+using namespace std::chrono_literals;
+
+//=============================================================================
+// Helper classes
+//=============================================================================
+
+template<class L>
+class locker : public IRunnable
+{
+ CSharedSection& sec;
+ CEvent* wait;
+
+ std::atomic<long>* mutex;
+public:
+ volatile bool haslock;
+ volatile bool obtainedlock;
+
+ inline locker(CSharedSection& o, std::atomic<long>* mutex_ = NULL, CEvent* wait_ = NULL) :
+ sec(o), wait(wait_), mutex(mutex_), haslock(false), obtainedlock(false) {}
+
+ inline locker(CSharedSection& o, CEvent* wait_ = NULL) :
+ sec(o), wait(wait_), mutex(NULL), haslock(false), obtainedlock(false) {}
+
+ void Run() override
+ {
+ AtomicGuard g(mutex);
+ L lock(sec);
+ haslock = true;
+ obtainedlock = true;
+ if (wait)
+ wait->Wait();
+ haslock = false;
+ }
+};
+
+TEST(TestCritSection, General)
+{
+ CCriticalSection sec;
+
+ std::unique_lock<CCriticalSection> l1(sec);
+ std::unique_lock<CCriticalSection> l2(sec);
+}
+
+TEST(TestSharedSection, General)
+{
+ CSharedSection sec;
+
+ std::shared_lock<CSharedSection> l1(sec);
+ std::shared_lock<CSharedSection> l2(sec);
+}
+
+TEST(TestSharedSection, GetSharedLockWhileTryingExclusiveLock)
+{
+ std::atomic<long> mutex(0L);
+ CEvent event;
+
+ CSharedSection sec;
+
+ std::shared_lock<CSharedSection> l1(sec); // get a shared lock
+
+ locker<std::unique_lock<CSharedSection>> l2(sec, &mutex);
+ thread waitThread1(l2); // try to get an exclusive lock
+
+ EXPECT_TRUE(waitForThread(mutex, 1, 10000ms));
+ std::this_thread::sleep_for(10ms); // still need to give it a chance to move ahead
+
+ EXPECT_TRUE(!l2.haslock); // this thread is waiting ...
+ EXPECT_TRUE(!l2.obtainedlock); // this thread is waiting ...
+
+ // now try and get a SharedLock
+ locker<std::shared_lock<CSharedSection>> l3(sec, &mutex, &event);
+ thread waitThread3(l3); // try to get a shared lock
+ EXPECT_TRUE(waitForThread(mutex, 2, 10000ms));
+ std::this_thread::sleep_for(10ms);
+ EXPECT_TRUE(l3.haslock);
+
+ event.Set();
+ EXPECT_TRUE(waitThread3.timed_join(10000ms));
+
+ // l3 should have released.
+ EXPECT_TRUE(!l3.haslock);
+
+ // but the exclusive lock should still not have happened
+ EXPECT_TRUE(!l2.haslock); // this thread is waiting ...
+ EXPECT_TRUE(!l2.obtainedlock); // this thread is waiting ...
+
+ // let it go
+ l1.unlock(); // the last shared lock leaves.
+
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+
+ EXPECT_TRUE(l2.obtainedlock); // the exclusive lock was captured
+ EXPECT_TRUE(!l2.haslock); // ... but it doesn't have it anymore
+}
+
+TEST(TestSharedSection, TwoCase)
+{
+ CSharedSection sec;
+
+ CEvent event;
+ std::atomic<long> mutex(0L);
+
+ locker<std::shared_lock<CSharedSection>> l1(sec, &mutex, &event);
+
+ {
+ std::shared_lock<CSharedSection> lock(sec);
+ thread waitThread1(l1);
+
+ EXPECT_TRUE(waitForWaiters(event, 1, 10000ms));
+ EXPECT_TRUE(l1.haslock);
+
+ event.Set();
+
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+ }
+
+ locker<std::shared_lock<CSharedSection>> l2(sec, &mutex, &event);
+ {
+ std::unique_lock<CSharedSection> lock(sec); // get exclusive lock
+ thread waitThread2(l2); // thread should block
+
+ EXPECT_TRUE(waitForThread(mutex, 1, 10000ms));
+ std::this_thread::sleep_for(10ms);
+
+ EXPECT_TRUE(!l2.haslock);
+
+ lock.unlock();
+
+ EXPECT_TRUE(waitForWaiters(event, 1, 10000ms));
+ std::this_thread::sleep_for(10ms);
+ EXPECT_TRUE(l2.haslock);
+
+ event.Set();
+
+ EXPECT_TRUE(waitThread2.timed_join(10000ms));
+ }
+}
+
+TEST(TestMultipleSharedSection, General)
+{
+ CSharedSection sec;
+
+ CEvent event;
+ std::atomic<long> mutex(0L);
+
+ locker<std::shared_lock<CSharedSection>> l1(sec, &mutex, &event);
+
+ {
+ std::shared_lock<CSharedSection> lock(sec);
+ thread waitThread1(l1);
+
+ EXPECT_TRUE(waitForThread(mutex, 1, 10000ms));
+ std::this_thread::sleep_for(10ms);
+
+ EXPECT_TRUE(l1.haslock);
+
+ event.Set();
+
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+ }
+
+ locker<std::shared_lock<CSharedSection>> l2(sec, &mutex, &event);
+ locker<std::shared_lock<CSharedSection>> l3(sec, &mutex, &event);
+ locker<std::shared_lock<CSharedSection>> l4(sec, &mutex, &event);
+ locker<std::shared_lock<CSharedSection>> l5(sec, &mutex, &event);
+ {
+ std::unique_lock<CSharedSection> lock(sec);
+ thread waitThread1(l2);
+ thread waitThread2(l3);
+ thread waitThread3(l4);
+ thread waitThread4(l5);
+
+ EXPECT_TRUE(waitForThread(mutex, 4, 10000ms));
+ std::this_thread::sleep_for(10ms);
+
+ EXPECT_TRUE(!l2.haslock);
+ EXPECT_TRUE(!l3.haslock);
+ EXPECT_TRUE(!l4.haslock);
+ EXPECT_TRUE(!l5.haslock);
+
+ lock.unlock();
+
+ EXPECT_TRUE(waitForWaiters(event, 4, 10000ms));
+
+ EXPECT_TRUE(l2.haslock);
+ EXPECT_TRUE(l3.haslock);
+ EXPECT_TRUE(l4.haslock);
+ EXPECT_TRUE(l5.haslock);
+
+ event.Set();
+
+ EXPECT_TRUE(waitThread1.timed_join(10000ms));
+ EXPECT_TRUE(waitThread2.timed_join(10000ms));
+ EXPECT_TRUE(waitThread3.timed_join(10000ms));
+ EXPECT_TRUE(waitThread4.timed_join(10000ms));
+ }
+}
+
diff --git a/xbmc/utils/ActorProtocol.cpp b/xbmc/utils/ActorProtocol.cpp
new file mode 100644
index 0000000..8798853
--- /dev/null
+++ b/xbmc/utils/ActorProtocol.cpp
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "ActorProtocol.h"
+
+#include "threads/Event.h"
+
+#include <cstring>
+#include <mutex>
+
+using namespace Actor;
+
+void Message::Release()
+{
+ bool skip;
+ origin.Lock();
+ skip = isSync ? !isSyncFini : false;
+ isSyncFini = true;
+ origin.Unlock();
+
+ if (skip)
+ return;
+
+ // free data buffer
+ if (data != buffer)
+ delete [] data;
+
+ payloadObj.reset();
+
+ // delete event in case of sync message
+ delete event;
+
+ origin.ReturnMessage(this);
+}
+
+bool Message::Reply(int sig, void *data /* = NULL*/, size_t size /* = 0 */)
+{
+ if (!isSync)
+ {
+ if (isOut)
+ return origin.SendInMessage(sig, data, size);
+ else
+ return origin.SendOutMessage(sig, data, size);
+ }
+
+ origin.Lock();
+
+ if (!isSyncTimeout)
+ {
+ Message *msg = origin.GetMessage();
+ msg->signal = sig;
+ msg->isOut = !isOut;
+ replyMessage = msg;
+ if (data)
+ {
+ if (size > sizeof(msg->buffer))
+ msg->data = new uint8_t[size];
+ else
+ msg->data = msg->buffer;
+ memcpy(msg->data, data, size);
+ }
+ }
+
+ origin.Unlock();
+
+ if (event)
+ event->Set();
+
+ return true;
+}
+
+Protocol::~Protocol()
+{
+ Message *msg;
+ Purge();
+ while (!freeMessageQueue.empty())
+ {
+ msg = freeMessageQueue.front();
+ freeMessageQueue.pop();
+ delete msg;
+ }
+}
+
+Message *Protocol::GetMessage()
+{
+ Message *msg;
+
+ std::unique_lock<CCriticalSection> lock(criticalSection);
+
+ if (!freeMessageQueue.empty())
+ {
+ msg = freeMessageQueue.front();
+ freeMessageQueue.pop();
+ }
+ else
+ msg = new Message(*this);
+
+ msg->isSync = false;
+ msg->isSyncFini = false;
+ msg->isSyncTimeout = false;
+ msg->event = NULL;
+ msg->data = NULL;
+ msg->payloadSize = 0;
+ msg->replyMessage = NULL;
+
+ return msg;
+}
+
+void Protocol::ReturnMessage(Message *msg)
+{
+ std::unique_lock<CCriticalSection> lock(criticalSection);
+
+ freeMessageQueue.push(msg);
+}
+
+bool Protocol::SendOutMessage(int signal,
+ const void* data /* = NULL */,
+ size_t size /* = 0 */,
+ Message* outMsg /* = NULL */)
+{
+ Message *msg;
+ if (outMsg)
+ msg = outMsg;
+ else
+ msg = GetMessage();
+
+ msg->signal = signal;
+ msg->isOut = true;
+
+ if (data)
+ {
+ if (size > sizeof(msg->buffer))
+ msg->data = new uint8_t[size];
+ else
+ msg->data = msg->buffer;
+ memcpy(msg->data, data, size);
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(criticalSection);
+ outMessages.push(msg);
+ }
+ if (containerOutEvent)
+ containerOutEvent->Set();
+
+ return true;
+}
+
+bool Protocol::SendOutMessage(int signal, CPayloadWrapBase *payload, Message *outMsg)
+{
+ Message *msg;
+ if (outMsg)
+ msg = outMsg;
+ else
+ msg = GetMessage();
+
+ msg->signal = signal;
+ msg->isOut = true;
+
+ msg->payloadObj.reset(payload);
+
+ {
+ std::unique_lock<CCriticalSection> lock(criticalSection);
+ outMessages.push(msg);
+ }
+ if (containerOutEvent)
+ containerOutEvent->Set();
+
+ return true;
+}
+
+bool Protocol::SendInMessage(int signal,
+ const void* data /* = NULL */,
+ size_t size /* = 0 */,
+ Message* outMsg /* = NULL */)
+{
+ Message *msg;
+ if (outMsg)
+ msg = outMsg;
+ else
+ msg = GetMessage();
+
+ msg->signal = signal;
+ msg->isOut = false;
+
+ if (data)
+ {
+ if (size > sizeof(msg->data))
+ msg->data = new uint8_t[size];
+ else
+ msg->data = msg->buffer;
+ memcpy(msg->data, data, size);
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(criticalSection);
+ inMessages.push(msg);
+ }
+ if (containerInEvent)
+ containerInEvent->Set();
+
+ return true;
+}
+
+bool Protocol::SendInMessage(int signal, CPayloadWrapBase *payload, Message *outMsg)
+{
+ Message *msg;
+ if (outMsg)
+ msg = outMsg;
+ else
+ msg = GetMessage();
+
+ msg->signal = signal;
+ msg->isOut = false;
+
+ msg->payloadObj.reset(payload);
+
+ {
+ std::unique_lock<CCriticalSection> lock(criticalSection);
+ inMessages.push(msg);
+ }
+ if (containerInEvent)
+ containerInEvent->Set();
+
+ return true;
+}
+
+bool Protocol::SendOutMessageSync(
+ int signal, Message** retMsg, int timeout, const void* data /* = NULL */, size_t size /* = 0 */)
+{
+ Message *msg = GetMessage();
+ msg->isOut = true;
+ msg->isSync = true;
+ msg->event = new CEvent;
+ msg->event->Reset();
+ SendOutMessage(signal, data, size, msg);
+
+ if (!msg->event->Wait(std::chrono::milliseconds(timeout)))
+ {
+ const std::unique_lock<CCriticalSection> lock(criticalSection);
+ if (msg->replyMessage)
+ *retMsg = msg->replyMessage;
+ else
+ {
+ *retMsg = NULL;
+ msg->isSyncTimeout = true;
+ }
+ }
+ else
+ *retMsg = msg->replyMessage;
+
+ msg->Release();
+
+ if (*retMsg)
+ return true;
+ else
+ return false;
+}
+
+bool Protocol::SendOutMessageSync(int signal, Message **retMsg, int timeout, CPayloadWrapBase *payload)
+{
+ Message *msg = GetMessage();
+ msg->isOut = true;
+ msg->isSync = true;
+ msg->event = new CEvent;
+ msg->event->Reset();
+ SendOutMessage(signal, payload, msg);
+
+ if (!msg->event->Wait(std::chrono::milliseconds(timeout)))
+ {
+ const std::unique_lock<CCriticalSection> lock(criticalSection);
+ if (msg->replyMessage)
+ *retMsg = msg->replyMessage;
+ else
+ {
+ *retMsg = NULL;
+ msg->isSyncTimeout = true;
+ }
+ }
+ else
+ *retMsg = msg->replyMessage;
+
+ msg->Release();
+
+ if (*retMsg)
+ return true;
+ else
+ return false;
+}
+
+bool Protocol::ReceiveOutMessage(Message **msg)
+{
+ std::unique_lock<CCriticalSection> lock(criticalSection);
+
+ if (outMessages.empty() || outDefered)
+ return false;
+
+ *msg = outMessages.front();
+ outMessages.pop();
+
+ return true;
+}
+
+bool Protocol::ReceiveInMessage(Message **msg)
+{
+ std::unique_lock<CCriticalSection> lock(criticalSection);
+
+ if (inMessages.empty() || inDefered)
+ return false;
+
+ *msg = inMessages.front();
+ inMessages.pop();
+
+ return true;
+}
+
+
+void Protocol::Purge()
+{
+ Message *msg;
+
+ while (ReceiveInMessage(&msg))
+ msg->Release();
+
+ while (ReceiveOutMessage(&msg))
+ msg->Release();
+}
+
+void Protocol::PurgeIn(int signal)
+{
+ Message *msg;
+ std::queue<Message*> msgs;
+
+ std::unique_lock<CCriticalSection> lock(criticalSection);
+
+ while (!inMessages.empty())
+ {
+ msg = inMessages.front();
+ inMessages.pop();
+ if (msg->signal != signal)
+ msgs.push(msg);
+ }
+ while (!msgs.empty())
+ {
+ msg = msgs.front();
+ msgs.pop();
+ inMessages.push(msg);
+ }
+}
+
+void Protocol::PurgeOut(int signal)
+{
+ Message *msg;
+ std::queue<Message*> msgs;
+
+ std::unique_lock<CCriticalSection> lock(criticalSection);
+
+ while (!outMessages.empty())
+ {
+ msg = outMessages.front();
+ outMessages.pop();
+ if (msg->signal != signal)
+ msgs.push(msg);
+ }
+ while (!msgs.empty())
+ {
+ msg = msgs.front();
+ msgs.pop();
+ outMessages.push(msg);
+ }
+}
diff --git a/xbmc/utils/ActorProtocol.h b/xbmc/utils/ActorProtocol.h
new file mode 100644
index 0000000..77f19b9
--- /dev/null
+++ b/xbmc/utils/ActorProtocol.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+
+#include <cstddef>
+#include <memory>
+#include <queue>
+#include <string>
+#include <utility>
+
+class CEvent;
+
+namespace Actor
+{
+
+class CPayloadWrapBase
+{
+public:
+ virtual ~CPayloadWrapBase() = default;
+};
+
+template<typename Payload>
+class CPayloadWrap : public CPayloadWrapBase
+{
+public:
+ ~CPayloadWrap() override = default;
+ CPayloadWrap(Payload* data) { m_pPayload.reset(data); }
+ CPayloadWrap(Payload& data) { m_pPayload.reset(new Payload(data)); }
+ Payload* GetPlayload() { return m_pPayload.get(); }
+
+protected:
+ std::unique_ptr<Payload> m_pPayload;
+};
+
+class Protocol;
+
+class Message
+{
+ friend class Protocol;
+
+ static constexpr size_t MSG_INTERNAL_BUFFER_SIZE = 32;
+
+public:
+ int signal;
+ bool isSync = false;
+ bool isSyncFini;
+ bool isOut;
+ bool isSyncTimeout;
+ size_t payloadSize;
+ uint8_t buffer[MSG_INTERNAL_BUFFER_SIZE];
+ uint8_t *data = nullptr;
+ std::unique_ptr<CPayloadWrapBase> payloadObj;
+ Message *replyMessage = nullptr;
+ Protocol &origin;
+ CEvent *event = nullptr;
+
+ void Release();
+ bool Reply(int sig, void *data = nullptr, size_t size = 0);
+
+private:
+ explicit Message(Protocol &_origin) noexcept
+ :origin(_origin) {}
+};
+
+class Protocol
+{
+public:
+ Protocol(std::string name, CEvent* inEvent, CEvent* outEvent)
+ : portName(std::move(name)), containerInEvent(inEvent), containerOutEvent(outEvent)
+ {
+ }
+ Protocol(std::string name) : Protocol(std::move(name), nullptr, nullptr) {}
+ ~Protocol();
+ Message *GetMessage();
+ void ReturnMessage(Message *msg);
+ bool SendOutMessage(int signal,
+ const void* data = nullptr,
+ size_t size = 0,
+ Message* outMsg = nullptr);
+ bool SendOutMessage(int signal, CPayloadWrapBase *payload, Message *outMsg = nullptr);
+ bool SendInMessage(int signal,
+ const void* data = nullptr,
+ size_t size = 0,
+ Message* outMsg = nullptr);
+ bool SendInMessage(int signal, CPayloadWrapBase *payload, Message *outMsg = nullptr);
+ bool SendOutMessageSync(
+ int signal, Message** retMsg, int timeout, const void* data = nullptr, size_t size = 0);
+ bool SendOutMessageSync(int signal, Message **retMsg, int timeout, CPayloadWrapBase *payload);
+ bool ReceiveOutMessage(Message **msg);
+ bool ReceiveInMessage(Message **msg);
+ void Purge();
+ void PurgeIn(int signal);
+ void PurgeOut(int signal);
+ void DeferIn(bool value) { inDefered = value; }
+ void DeferOut(bool value) { outDefered = value; }
+ void Lock() { criticalSection.lock(); }
+ void Unlock() { criticalSection.unlock(); }
+ std::string portName;
+
+protected:
+ CEvent *containerInEvent, *containerOutEvent;
+ CCriticalSection criticalSection;
+ std::queue<Message*> outMessages;
+ std::queue<Message*> inMessages;
+ std::queue<Message*> freeMessageQueue;
+ bool inDefered = false, outDefered = false;
+};
+
+}
diff --git a/xbmc/utils/AlarmClock.cpp b/xbmc/utils/AlarmClock.cpp
new file mode 100644
index 0000000..f37f828
--- /dev/null
+++ b/xbmc/utils/AlarmClock.cpp
@@ -0,0 +1,161 @@
+/*
+ * 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 "AlarmClock.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "events/EventLog.h"
+#include "events/NotificationEvent.h"
+#include "guilib/LocalizeStrings.h"
+#include "log.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/StringUtils.h"
+
+#include <mutex>
+#include <utility>
+
+using namespace std::chrono_literals;
+
+CAlarmClock::CAlarmClock() : CThread("AlarmClock")
+{
+}
+
+CAlarmClock::~CAlarmClock() = default;
+
+void CAlarmClock::Start(const std::string& strName, float n_secs, const std::string& strCommand, bool bSilent /* false */, bool bLoop /* false */)
+{
+ // make lower case so that lookups are case-insensitive
+ std::string lowerName(strName);
+ StringUtils::ToLower(lowerName);
+ Stop(lowerName);
+ SAlarmClockEvent event;
+ event.m_fSecs = static_cast<double>(n_secs);
+ event.m_strCommand = strCommand;
+ event.m_loop = bLoop;
+ if (!m_bIsRunning)
+ {
+ StopThread();
+ Create();
+ m_bIsRunning = true;
+ }
+
+ uint32_t labelAlarmClock;
+ uint32_t labelStarted;
+ if (StringUtils::EqualsNoCase(strName, "shutdowntimer"))
+ {
+ labelAlarmClock = 20144;
+ labelStarted = 20146;
+ }
+ else
+ {
+ labelAlarmClock = 13208;
+ labelStarted = 13210;
+ }
+
+ EventPtr alarmClockActivity(new CNotificationEvent(
+ labelAlarmClock,
+ StringUtils::Format(g_localizeStrings.Get(labelStarted), static_cast<int>(event.m_fSecs) / 60,
+ static_cast<int>(event.m_fSecs) % 60)));
+
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ {
+ if (bSilent)
+ eventLog->Add(alarmClockActivity);
+ else
+ eventLog->AddWithNotification(alarmClockActivity);
+ }
+
+ event.watch.StartZero();
+ std::unique_lock<CCriticalSection> lock(m_events);
+ m_event.insert(make_pair(lowerName,event));
+ CLog::Log(LOGDEBUG, "started alarm with name: {}", lowerName);
+}
+
+void CAlarmClock::Stop(const std::string& strName, bool bSilent /* false */)
+{
+ std::unique_lock<CCriticalSection> lock(m_events);
+
+ std::string lowerName(strName);
+ StringUtils::ToLower(lowerName); // lookup as lowercase only
+ std::map<std::string,SAlarmClockEvent>::iterator iter = m_event.find(lowerName);
+
+ if (iter == m_event.end())
+ return;
+
+ uint32_t labelAlarmClock;
+ if (StringUtils::EqualsNoCase(strName, "shutdowntimer"))
+ labelAlarmClock = 20144;
+ else
+ labelAlarmClock = 13208;
+
+ std::string strMessage;
+ float elapsed = 0.f;
+
+ if (iter->second.watch.IsRunning())
+ elapsed = iter->second.watch.GetElapsedSeconds();
+
+ if (elapsed > static_cast<float>(iter->second.m_fSecs))
+ strMessage = g_localizeStrings.Get(13211);
+ else
+ {
+ float remaining = static_cast<float>(iter->second.m_fSecs) - elapsed;
+ strMessage = StringUtils::Format(g_localizeStrings.Get(13212), static_cast<int>(remaining) / 60,
+ static_cast<int>(remaining) % 60);
+ }
+
+ if (iter->second.m_strCommand.empty() || static_cast<float>(iter->second.m_fSecs) > elapsed)
+ {
+ EventPtr alarmClockActivity(new CNotificationEvent(labelAlarmClock, strMessage));
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ {
+ if (bSilent)
+ eventLog->Add(alarmClockActivity);
+ else
+ eventLog->AddWithNotification(alarmClockActivity);
+ }
+ }
+ else
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ iter->second.m_strCommand);
+ if (iter->second.m_loop)
+ {
+ iter->second.watch.Reset();
+ return;
+ }
+ }
+
+ iter->second.watch.Stop();
+ m_event.erase(iter);
+}
+
+void CAlarmClock::Process()
+{
+ while( !m_bStop)
+ {
+ std::string strLast;
+ {
+ std::unique_lock<CCriticalSection> lock(m_events);
+ for (std::map<std::string,SAlarmClockEvent>::iterator iter=m_event.begin();iter != m_event.end(); ++iter)
+ if (iter->second.watch.IsRunning() &&
+ iter->second.watch.GetElapsedSeconds() >= static_cast<float>(iter->second.m_fSecs))
+ {
+ Stop(iter->first);
+ if ((iter = m_event.find(strLast)) == m_event.end())
+ break;
+ }
+ else
+ strLast = iter->first;
+ }
+ CThread::Sleep(100ms);
+ }
+}
+
diff --git a/xbmc/utils/AlarmClock.h b/xbmc/utils/AlarmClock.h
new file mode 100644
index 0000000..392ba02
--- /dev/null
+++ b/xbmc/utils/AlarmClock.h
@@ -0,0 +1,69 @@
+/*
+ * 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 "Stopwatch.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <map>
+#include <string>
+
+struct SAlarmClockEvent
+{
+ CStopWatch watch;
+ double m_fSecs;
+ std::string m_strCommand;
+ bool m_loop;
+};
+
+class CAlarmClock : public CThread
+{
+public:
+ CAlarmClock();
+ ~CAlarmClock() override;
+ void Start(const std::string& strName, float n_secs, const std::string& strCommand, bool bSilent = false, bool bLoop = false);
+ inline bool IsRunning() const
+ {
+ return m_bIsRunning;
+ }
+
+ inline bool HasAlarm(const std::string& strName)
+ {
+ // note: strName should be lower case only here
+ // No point checking it at the moment due to it only being called
+ // from GUIInfoManager (which is always lowercase)
+ // CLog::Log(LOGDEBUG,"checking for {}",strName);
+ return (m_event.find(strName) != m_event.end());
+ }
+
+ double GetRemaining(const std::string& strName)
+ {
+ std::map<std::string,SAlarmClockEvent>::iterator iter;
+ if ((iter=m_event.find(strName)) != m_event.end())
+ {
+ return iter->second.m_fSecs - static_cast<double>(iter->second.watch.IsRunning()
+ ? iter->second.watch.GetElapsedSeconds()
+ : 0.f);
+ }
+
+ return 0.0;
+ }
+
+ void Stop(const std::string& strName, bool bSilent = false);
+ void Process() override;
+private:
+ std::map<std::string,SAlarmClockEvent> m_event;
+ CCriticalSection m_events;
+
+ bool m_bIsRunning = false;
+};
+
+extern CAlarmClock g_alarmClock;
+
diff --git a/xbmc/utils/AliasShortcutUtils.cpp b/xbmc/utils/AliasShortcutUtils.cpp
new file mode 100644
index 0000000..6eed427
--- /dev/null
+++ b/xbmc/utils/AliasShortcutUtils.cpp
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2009-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.
+ */
+
+#if defined(TARGET_DARWIN_OSX)
+#include "utils/URIUtils.h"
+#include "platform/darwin/DarwinUtils.h"
+#elif defined(TARGET_POSIX)
+#else
+#endif
+
+#include "AliasShortcutUtils.h"
+#include "utils/log.h"
+
+bool IsAliasShortcut(const std::string& path, bool isdirectory)
+{
+ bool rtn = false;
+
+#if defined(TARGET_DARWIN_OSX)
+ // Note: regular files that have an .alias extension can be
+ // reported as an alias when clearly, they are not. Trap them out.
+ if (!URIUtils::HasExtension(path, ".alias"))//! @todo - check if this is still needed with the new API
+ {
+ rtn = CDarwinUtils::IsAliasShortcut(path, isdirectory);
+ }
+#elif defined(TARGET_POSIX)
+ // Linux does not use alias or shortcut methods
+#elif defined(TARGET_WINDOWS)
+/* Needs testing under Windows platform so ignore shortcuts for now
+ if (CUtil::GetExtension(path) == ".lnk")
+ {
+ rtn = true;
+ }
+*/
+#endif
+ return(rtn);
+}
+
+void TranslateAliasShortcut(std::string& path)
+{
+#if defined(TARGET_DARWIN_OSX)
+ CDarwinUtils::TranslateAliasShortcut(path);
+#elif defined(TARGET_POSIX)
+ // Linux does not use alias or shortcut methods
+#elif defined(TARGET_WINDOWS_STORE)
+ // Win10 does not use alias or shortcut methods
+ CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__);
+#elif defined(TARGET_WINDOWS)
+/* Needs testing under Windows platform so ignore shortcuts for now
+ CComPtr<IShellLink> ipShellLink;
+
+ // Get a pointer to the IShellLink interface
+ if (NOERROR == CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (void**)&ipShellLink))
+ WCHAR wszTemp[MAX_PATH];
+
+ // Get a pointer to the IPersistFile interface
+ CComQIPtr<IPersistFile> ipPersistFile(ipShellLink);
+
+ // IPersistFile is using LPCOLESTR so make sure that the string is Unicode
+#if !defined _UNICODE
+ MultiByteToWideChar(CP_ACP, 0, lpszShortcutPath, -1, wszTemp, MAX_PATH);
+#else
+ wcsncpy(wszTemp, lpszShortcutPath, MAX_PATH);
+#endif
+
+ // Open the shortcut file and initialize it from its contents
+ if (NOERROR == ipPersistFile->Load(wszTemp, STGM_READ))
+ {
+ // Try to find the target of a shortcut even if it has been moved or renamed
+ if (NOERROR == ipShellLink->Resolve(NULL, SLR_UPDATE))
+ {
+ WIN32_FIND_DATA wfd;
+ TCHAR real_path[PATH_MAX];
+ // Get the path to the shortcut target
+ if (NOERROR == ipShellLink->GetPath(real_path, MAX_PATH, &wfd, SLGP_RAWPATH))
+ {
+ // Get the description of the target
+ TCHAR szDesc[MAX_PATH];
+ if (NOERROR == ipShellLink->GetDescription(szDesc, MAX_PATH))
+ {
+ path = real_path;
+ }
+ }
+ }
+ }
+ }
+*/
+#endif
+}
diff --git a/xbmc/utils/AliasShortcutUtils.h b/xbmc/utils/AliasShortcutUtils.h
new file mode 100644
index 0000000..524c8f0
--- /dev/null
+++ b/xbmc/utils/AliasShortcutUtils.h
@@ -0,0 +1,14 @@
+/*
+ * Copyright (C) 2009-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 <string>
+
+bool IsAliasShortcut(const std::string& path, bool isdirectory);
+void TranslateAliasShortcut(std::string &path);
diff --git a/xbmc/utils/Archive.cpp b/xbmc/utils/Archive.cpp
new file mode 100644
index 0000000..4f69929
--- /dev/null
+++ b/xbmc/utils/Archive.cpp
@@ -0,0 +1,463 @@
+/*
+ * 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 "Archive.h"
+
+#include "IArchivable.h"
+#include "filesystem/File.h"
+#include "utils/Variant.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <stdexcept>
+
+#ifdef __GNUC__
+#pragma GCC diagnostic ignored "-Wlong-long"
+#endif
+
+using namespace XFILE;
+
+//arbitrarily chosen, should be plenty big enough for our strings
+//without causing random bad things happening
+//not very bad, just tiny bad
+#define MAX_STRING_SIZE 100*1024*1024
+
+CArchive::CArchive(CFile* pFile, int mode)
+{
+ m_pFile = pFile;
+ m_iMode = mode;
+
+ m_pBuffer = std::unique_ptr<uint8_t[]>(new uint8_t[CARCHIVE_BUFFER_MAX]);
+ memset(m_pBuffer.get(), 0, CARCHIVE_BUFFER_MAX);
+ if (mode == load)
+ {
+ m_BufferPos = m_pBuffer.get() + CARCHIVE_BUFFER_MAX;
+ m_BufferRemain = 0;
+ }
+ else
+ {
+ m_BufferPos = m_pBuffer.get();
+ m_BufferRemain = CARCHIVE_BUFFER_MAX;
+ }
+}
+
+CArchive::~CArchive()
+{
+ FlushBuffer();
+}
+
+void CArchive::Close()
+{
+ FlushBuffer();
+}
+
+bool CArchive::IsLoading() const
+{
+ return (m_iMode == load);
+}
+
+bool CArchive::IsStoring() const
+{
+ return (m_iMode == store);
+}
+
+CArchive& CArchive::operator<<(float f)
+{
+ return streamout(&f, sizeof(f));
+}
+
+CArchive& CArchive::operator<<(double d)
+{
+ return streamout(&d, sizeof(d));
+}
+
+CArchive& CArchive::operator<<(short int s)
+{
+ return streamout(&s, sizeof(s));
+}
+
+CArchive& CArchive::operator<<(unsigned short int us)
+{
+ return streamout(&us, sizeof(us));
+}
+
+CArchive& CArchive::operator<<(int i)
+{
+ return streamout(&i, sizeof(i));
+}
+
+CArchive& CArchive::operator<<(unsigned int ui)
+{
+ return streamout(&ui, sizeof(ui));
+}
+
+CArchive& CArchive::operator<<(long int l)
+{
+ return streamout(&l, sizeof(l));
+}
+
+CArchive& CArchive::operator<<(unsigned long int ul)
+{
+ return streamout(&ul, sizeof(ul));
+}
+
+CArchive& CArchive::operator<<(long long int ll)
+{
+ return streamout(&ll, sizeof(ll));
+}
+
+CArchive& CArchive::operator<<(unsigned long long int ull)
+{
+ return streamout(&ull, sizeof(ull));
+}
+
+CArchive& CArchive::operator<<(bool b)
+{
+ return streamout(&b, sizeof(b));
+}
+
+CArchive& CArchive::operator<<(char c)
+{
+ return streamout(&c, sizeof(c));
+}
+
+CArchive& CArchive::operator<<(const std::string& str)
+{
+ auto size = static_cast<uint32_t>(str.size());
+ if (size > MAX_STRING_SIZE)
+ throw std::out_of_range("String too large, over 100MB");
+
+ *this << size;
+
+ return streamout(str.data(), size * sizeof(char));
+}
+
+CArchive& CArchive::operator<<(const std::wstring& wstr)
+{
+ if (wstr.size() > MAX_STRING_SIZE)
+ throw std::out_of_range("String too large, over 100MB");
+
+ auto size = static_cast<uint32_t>(wstr.size());
+
+ *this << size;
+
+ return streamout(wstr.data(), size * sizeof(wchar_t));
+}
+
+CArchive& CArchive::operator<<(const KODI::TIME::SystemTime& time)
+{
+ return streamout(&time, sizeof(KODI::TIME::SystemTime));
+}
+
+CArchive& CArchive::operator<<(IArchivable& obj)
+{
+ obj.Archive(*this);
+
+ return *this;
+}
+
+CArchive& CArchive::operator<<(const CVariant& variant)
+{
+ *this << static_cast<int>(variant.type());
+ switch (variant.type())
+ {
+ case CVariant::VariantTypeInteger:
+ *this << variant.asInteger();
+ break;
+ case CVariant::VariantTypeUnsignedInteger:
+ *this << variant.asUnsignedInteger();
+ break;
+ case CVariant::VariantTypeBoolean:
+ *this << variant.asBoolean();
+ break;
+ case CVariant::VariantTypeString:
+ *this << variant.asString();
+ break;
+ case CVariant::VariantTypeWideString:
+ *this << variant.asWideString();
+ break;
+ case CVariant::VariantTypeDouble:
+ *this << variant.asDouble();
+ break;
+ case CVariant::VariantTypeArray:
+ *this << variant.size();
+ for (auto i = variant.begin_array(); i != variant.end_array(); ++i)
+ *this << *i;
+ break;
+ case CVariant::VariantTypeObject:
+ *this << variant.size();
+ for (auto itr = variant.begin_map(); itr != variant.end_map(); ++itr)
+ {
+ *this << itr->first;
+ *this << itr->second;
+ }
+ break;
+ case CVariant::VariantTypeNull:
+ case CVariant::VariantTypeConstNull:
+ default:
+ break;
+ }
+
+ return *this;
+}
+
+CArchive& CArchive::operator<<(const std::vector<std::string>& strArray)
+{
+ if (std::numeric_limits<uint32_t>::max() < strArray.size())
+ throw std::out_of_range("Array too large, over 2^32 in size");
+
+ *this << static_cast<uint32_t>(strArray.size());
+
+ for (auto&& item : strArray)
+ *this << item;
+
+ return *this;
+}
+
+CArchive& CArchive::operator<<(const std::vector<int>& iArray)
+{
+ if (std::numeric_limits<uint32_t>::max() < iArray.size())
+ throw std::out_of_range("Array too large, over 2^32 in size");
+
+ *this << static_cast<uint32_t>(iArray.size());
+
+ for (auto&& item : iArray)
+ *this << item;
+
+ return *this;
+}
+
+CArchive& CArchive::operator>>(std::string& str)
+{
+ uint32_t iLength = 0;
+ *this >> iLength;
+
+ if (iLength > MAX_STRING_SIZE)
+ throw std::out_of_range("String too large, over 100MB");
+
+ auto s = std::unique_ptr<char[]>(new char[iLength]);
+ streamin(s.get(), iLength * sizeof(char));
+ str.assign(s.get(), iLength);
+
+ return *this;
+}
+
+CArchive& CArchive::operator>>(std::wstring& wstr)
+{
+ uint32_t iLength = 0;
+ *this >> iLength;
+
+ if (iLength > MAX_STRING_SIZE)
+ throw std::out_of_range("String too large, over 100MB");
+
+ auto p = std::unique_ptr<wchar_t[]>(new wchar_t[iLength]);
+ streamin(p.get(), iLength * sizeof(wchar_t));
+ wstr.assign(p.get(), iLength);
+
+ return *this;
+}
+
+CArchive& CArchive::operator>>(KODI::TIME::SystemTime& time)
+{
+ return streamin(&time, sizeof(KODI::TIME::SystemTime));
+}
+
+CArchive& CArchive::operator>>(IArchivable& obj)
+{
+ obj.Archive(*this);
+
+ return *this;
+}
+
+CArchive& CArchive::operator>>(CVariant& variant)
+{
+ int type;
+ *this >> type;
+ variant = CVariant(static_cast<CVariant::VariantType>(type));
+
+ switch (variant.type())
+ {
+ case CVariant::VariantTypeInteger:
+ {
+ int64_t value;
+ *this >> value;
+ variant = value;
+ break;
+ }
+ case CVariant::VariantTypeUnsignedInteger:
+ {
+ uint64_t value;
+ *this >> value;
+ variant = value;
+ break;
+ }
+ case CVariant::VariantTypeBoolean:
+ {
+ bool value;
+ *this >> value;
+ variant = value;
+ break;
+ }
+ case CVariant::VariantTypeString:
+ {
+ std::string value;
+ *this >> value;
+ variant = value;
+ break;
+ }
+ case CVariant::VariantTypeWideString:
+ {
+ std::wstring value;
+ *this >> value;
+ variant = value;
+ break;
+ }
+ case CVariant::VariantTypeDouble:
+ {
+ double value;
+ *this >> value;
+ variant = value;
+ break;
+ }
+ case CVariant::VariantTypeArray:
+ {
+ unsigned int size;
+ *this >> size;
+ for (; size > 0; size--)
+ {
+ CVariant value;
+ *this >> value;
+ variant.append(value);
+ }
+ break;
+ }
+ case CVariant::VariantTypeObject:
+ {
+ unsigned int size;
+ *this >> size;
+ for (; size > 0; size--)
+ {
+ std::string name;
+ CVariant value;
+ *this >> name;
+ *this >> value;
+ variant[name] = value;
+ }
+ break;
+ }
+ case CVariant::VariantTypeNull:
+ case CVariant::VariantTypeConstNull:
+ default:
+ break;
+ }
+
+ return *this;
+}
+
+CArchive& CArchive::operator>>(std::vector<std::string>& strArray)
+{
+ uint32_t size;
+ *this >> size;
+ strArray.clear();
+ for (uint32_t index = 0; index < size; index++)
+ {
+ std::string str;
+ *this >> str;
+ strArray.push_back(std::move(str));
+ }
+
+ return *this;
+}
+
+CArchive& CArchive::operator>>(std::vector<int>& iArray)
+{
+ uint32_t size;
+ *this >> size;
+ iArray.clear();
+ for (uint32_t index = 0; index < size; index++)
+ {
+ int i;
+ *this >> i;
+ iArray.push_back(i);
+ }
+
+ return *this;
+}
+
+void CArchive::FlushBuffer()
+{
+ if (m_iMode == store && m_BufferPos != m_pBuffer.get())
+ {
+ if (m_pFile->Write(m_pBuffer.get(), m_BufferPos - m_pBuffer.get()) != m_BufferPos - m_pBuffer.get())
+ CLog::Log(LOGERROR, "{}: Error flushing buffer", __FUNCTION__);
+ else
+ {
+ m_BufferPos = m_pBuffer.get();
+ m_BufferRemain = CARCHIVE_BUFFER_MAX;
+ }
+ }
+}
+
+CArchive &CArchive::streamout_bufferwrap(const uint8_t *ptr, size_t size)
+{
+ do
+ {
+ auto chunkSize = std::min(size, m_BufferRemain);
+ m_BufferPos = std::copy(ptr, ptr + chunkSize, m_BufferPos);
+ ptr += chunkSize;
+ size -= chunkSize;
+ m_BufferRemain -= chunkSize;
+ if (m_BufferRemain == 0)
+ FlushBuffer();
+ } while (size > 0);
+ return *this;
+}
+
+void CArchive::FillBuffer()
+{
+ if (m_iMode == load && m_BufferRemain == 0)
+ {
+ auto read = m_pFile->Read(m_pBuffer.get(), CARCHIVE_BUFFER_MAX);
+ if (read > 0)
+ {
+ m_BufferRemain = read;
+ m_BufferPos = m_pBuffer.get();
+ }
+ }
+}
+
+CArchive &CArchive::streamin_bufferwrap(uint8_t *ptr, size_t size)
+{
+ auto orig_ptr = ptr;
+ auto orig_size = size;
+ do
+ {
+ if (m_BufferRemain == 0)
+ {
+ FillBuffer();
+ if (m_BufferRemain < CARCHIVE_BUFFER_MAX && m_BufferRemain < size)
+ {
+ CLog::Log(LOGERROR, "{}: can't stream in: requested {} bytes, was read {} bytes",
+ __FUNCTION__, static_cast<unsigned long>(orig_size),
+ static_cast<unsigned long>(ptr - orig_ptr + m_BufferRemain));
+
+ memset(orig_ptr, 0, orig_size);
+ return *this;
+ }
+ }
+ auto chunkSize = std::min(size, m_BufferRemain);
+ ptr = std::copy(m_BufferPos, m_BufferPos + chunkSize, ptr);
+ m_BufferPos += chunkSize;
+ m_BufferRemain -= chunkSize;
+ size -= chunkSize;
+ } while (size > 0);
+ return *this;
+}
diff --git a/xbmc/utils/Archive.h b/xbmc/utils/Archive.h
new file mode 100644
index 0000000..a1af0c3
--- /dev/null
+++ b/xbmc/utils/Archive.h
@@ -0,0 +1,185 @@
+/*
+ * 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 <cstring>
+#include <memory>
+#include <string>
+#include <vector>
+
+#define CARCHIVE_BUFFER_MAX 4096
+
+namespace XFILE
+{
+ class CFile;
+}
+class CVariant;
+class IArchivable;
+namespace KODI::TIME
+{
+struct SystemTime;
+}
+
+class CArchive
+{
+public:
+ CArchive(XFILE::CFile* pFile, int mode);
+ ~CArchive();
+
+ /* CArchive support storing and loading of all C basic integer types
+ * C basic types was chosen instead of fixed size ints (int16_t - int64_t) to support all integer typedefs
+ * For example size_t can be typedef of unsigned int, long or long long depending on platform
+ * while int32_t and int64_t are usually unsigned short, int or long long, but not long
+ * and even if int and long can have same binary representation they are different types for compiler
+ * According to section 5.2.4.2.1 of C99 http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf
+ * minimal size of short int is 16 bits
+ * minimal size of int is 16 bits (usually 32 or 64 bits, larger or equal to short int)
+ * minimal size of long int is 32 bits (larger or equal to int)
+ * minimal size of long long int is 64 bits (larger or equal to long int) */
+ // storing
+ CArchive& operator<<(float f);
+ CArchive& operator<<(double d);
+ CArchive& operator<<(short int s);
+ CArchive& operator<<(unsigned short int us);
+ CArchive& operator<<(int i);
+ CArchive& operator<<(unsigned int ui);
+ CArchive& operator<<(long int l);
+ CArchive& operator<<(unsigned long int ul);
+ CArchive& operator<<(long long int ll);
+ CArchive& operator<<(unsigned long long int ull);
+ CArchive& operator<<(bool b);
+ CArchive& operator<<(char c);
+ CArchive& operator<<(const std::string &str);
+ CArchive& operator<<(const std::wstring& wstr);
+ CArchive& operator<<(const KODI::TIME::SystemTime& time);
+ CArchive& operator<<(IArchivable& obj);
+ CArchive& operator<<(const CVariant& variant);
+ CArchive& operator<<(const std::vector<std::string>& strArray);
+ CArchive& operator<<(const std::vector<int>& iArray);
+
+ // loading
+ inline CArchive& operator>>(float& f)
+ {
+ return streamin(&f, sizeof(f));
+ }
+
+ inline CArchive& operator>>(double& d)
+ {
+ return streamin(&d, sizeof(d));
+ }
+
+ inline CArchive& operator>>(short int& s)
+ {
+ return streamin(&s, sizeof(s));
+ }
+
+ inline CArchive& operator>>(unsigned short int& us)
+ {
+ return streamin(&us, sizeof(us));
+ }
+
+ inline CArchive& operator>>(int& i)
+ {
+ return streamin(&i, sizeof(i));
+ }
+
+ inline CArchive& operator>>(unsigned int& ui)
+ {
+ return streamin(&ui, sizeof(ui));
+ }
+
+ inline CArchive& operator>>(long int& l)
+ {
+ return streamin(&l, sizeof(l));
+ }
+
+ inline CArchive& operator>>(unsigned long int& ul)
+ {
+ return streamin(&ul, sizeof(ul));
+ }
+
+ inline CArchive& operator>>(long long int& ll)
+ {
+ return streamin(&ll, sizeof(ll));
+ }
+
+ inline CArchive& operator>>(unsigned long long int& ull)
+ {
+ return streamin(&ull, sizeof(ull));
+ }
+
+ inline CArchive& operator>>(bool& b)
+ {
+ return streamin(&b, sizeof(b));
+ }
+
+ inline CArchive& operator>>(char& c)
+ {
+ return streamin(&c, sizeof(c));
+ }
+
+ CArchive& operator>>(std::string &str);
+ CArchive& operator>>(std::wstring& wstr);
+ CArchive& operator>>(KODI::TIME::SystemTime& time);
+ CArchive& operator>>(IArchivable& obj);
+ CArchive& operator>>(CVariant& variant);
+ CArchive& operator>>(std::vector<std::string>& strArray);
+ CArchive& operator>>(std::vector<int>& iArray);
+
+ bool IsLoading() const;
+ bool IsStoring() const;
+
+ void Close();
+
+ enum Mode {load = 0, store};
+
+protected:
+ inline CArchive &streamout(const void *dataPtr, size_t size)
+ {
+ auto ptr = static_cast<const uint8_t *>(dataPtr);
+ /* Note, the buffer is flushed as soon as it is full (m_BufferRemain == size) rather
+ * than waiting until we attempt to put more data into an already full buffer */
+ if (m_BufferRemain > size)
+ {
+ memcpy(m_BufferPos, ptr, size);
+ m_BufferPos += size;
+ m_BufferRemain -= size;
+ return *this;
+ }
+
+ return streamout_bufferwrap(ptr, size);
+ }
+
+ inline CArchive &streamin(void *dataPtr, size_t size)
+ {
+ auto ptr = static_cast<uint8_t *>(dataPtr);
+ /* Note, refilling the buffer is deferred until we know we need to read more from it */
+ if (m_BufferRemain >= size)
+ {
+ memcpy(ptr, m_BufferPos, size);
+ m_BufferPos += size;
+ m_BufferRemain -= size;
+ return *this;
+ }
+
+ return streamin_bufferwrap(ptr, size);
+ }
+
+ XFILE::CFile* m_pFile; //non-owning
+ int m_iMode;
+ std::unique_ptr<uint8_t[]> m_pBuffer;
+ uint8_t *m_BufferPos;
+ size_t m_BufferRemain;
+
+private:
+ void FlushBuffer();
+ CArchive &streamout_bufferwrap(const uint8_t *ptr, size_t size);
+ void FillBuffer();
+ CArchive &streamin_bufferwrap(uint8_t *ptr, size_t size);
+};
diff --git a/xbmc/utils/Base64.cpp b/xbmc/utils/Base64.cpp
new file mode 100644
index 0000000..6b41519
--- /dev/null
+++ b/xbmc/utils/Base64.cpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2011-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 "Base64.h"
+
+#define PADDING '='
+
+const std::string Base64::m_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz"
+ "0123456789+/";
+
+void Base64::Encode(const char* input, unsigned int length, std::string &output)
+{
+ if (input == NULL || length == 0)
+ return;
+
+ long l;
+ output.clear();
+ output.reserve(((length + 2) / 3) * 4);
+
+ for (unsigned int i = 0; i < length; i += 3)
+ {
+ l = ((((unsigned long) input[i]) << 16) & 0xFFFFFF) |
+ ((((i + 1) < length) ? (((unsigned long) input[i + 1]) << 8) : 0) & 0xFFFF) |
+ ((((i + 2) < length) ? (((unsigned long) input[i + 2]) << 0) : 0) & 0x00FF);
+
+ output.push_back(m_characters[(l >> 18) & 0x3F]);
+ output.push_back(m_characters[(l >> 12) & 0x3F]);
+
+ if (i + 1 < length)
+ output.push_back(m_characters[(l >> 6) & 0x3F]);
+ if (i + 2 < length)
+ output.push_back(m_characters[(l >> 0) & 0x3F]);
+ }
+
+ int left = 3 - (length % 3);
+
+ if (length % 3)
+ {
+ for (int i = 0; i < left; i++)
+ output.push_back(PADDING);
+ }
+}
+
+std::string Base64::Encode(const char* input, unsigned int length)
+{
+ std::string output;
+ Encode(input, length, output);
+
+ return output;
+}
+
+void Base64::Encode(const std::string &input, std::string &output)
+{
+ Encode(input.c_str(), input.size(), output);
+}
+
+std::string Base64::Encode(const std::string &input)
+{
+ std::string output;
+ Encode(input, output);
+
+ return output;
+}
+
+void Base64::Decode(const char* input, unsigned int length, std::string &output)
+{
+ if (input == NULL || length == 0)
+ return;
+
+ long l;
+ output.clear();
+
+ for (unsigned int index = 0; index < length; index++)
+ {
+ if (input[index] == '=')
+ {
+ length = index;
+ break;
+ }
+ }
+
+ output.reserve(length - ((length + 2) / 4));
+
+ for (unsigned int i = 0; i < length; i += 4)
+ {
+ l = ((((unsigned long) m_characters.find(input[i])) & 0x3F) << 18);
+ l |= (((i + 1) < length) ? ((((unsigned long) m_characters.find(input[i + 1])) & 0x3F) << 12) : 0);
+ l |= (((i + 2) < length) ? ((((unsigned long) m_characters.find(input[i + 2])) & 0x3F) << 6) : 0);
+ l |= (((i + 3) < length) ? ((((unsigned long) m_characters.find(input[i + 3])) & 0x3F) << 0) : 0);
+
+ output.push_back((char)((l >> 16) & 0xFF));
+ if (i + 2 < length)
+ output.push_back((char)((l >> 8) & 0xFF));
+ if (i + 3 < length)
+ output.push_back((char)((l >> 0) & 0xFF));
+ }
+}
+
+std::string Base64::Decode(const char* input, unsigned int length)
+{
+ std::string output;
+ Decode(input, length, output);
+
+ return output;
+}
+
+void Base64::Decode(const std::string &input, std::string &output)
+{
+ size_t length = input.find_first_of(PADDING);
+ if (length == std::string::npos)
+ length = input.size();
+
+ Decode(input.c_str(), length, output);
+}
+
+std::string Base64::Decode(const std::string &input)
+{
+ std::string output;
+ Decode(input, output);
+
+ return output;
+}
diff --git a/xbmc/utils/Base64.h b/xbmc/utils/Base64.h
new file mode 100644
index 0000000..4b645ee
--- /dev/null
+++ b/xbmc/utils/Base64.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2011-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 <string>
+
+class Base64
+{
+public:
+ static void Encode(const char* input, unsigned int length, std::string &output);
+ static std::string Encode(const char* input, unsigned int length);
+ static void Encode(const std::string &input, std::string &output);
+ static std::string Encode(const std::string &input);
+ static void Decode(const char* input, unsigned int length, std::string &output);
+ static std::string Decode(const char* input, unsigned int length);
+ static void Decode(const std::string &input, std::string &output);
+ static std::string Decode(const std::string &input);
+
+private:
+ static const std::string m_characters;
+};
diff --git a/xbmc/utils/BitstreamConverter.cpp b/xbmc/utils/BitstreamConverter.cpp
new file mode 100644
index 0000000..f2224a5
--- /dev/null
+++ b/xbmc/utils/BitstreamConverter.cpp
@@ -0,0 +1,1237 @@
+/*
+ * 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 "utils/log.h"
+
+#include <assert.h>
+
+#ifndef UINT16_MAX
+#define UINT16_MAX (65535U)
+#endif
+
+#include "BitstreamConverter.h"
+#include "BitstreamReader.h"
+#include "BitstreamWriter.h"
+
+#include <algorithm>
+
+enum {
+ AVC_NAL_SLICE=1,
+ AVC_NAL_DPA,
+ AVC_NAL_DPB,
+ AVC_NAL_DPC,
+ AVC_NAL_IDR_SLICE,
+ AVC_NAL_SEI,
+ AVC_NAL_SPS,
+ AVC_NAL_PPS,
+ AVC_NAL_AUD,
+ AVC_NAL_END_SEQUENCE,
+ AVC_NAL_END_STREAM,
+ AVC_NAL_FILLER_DATA,
+ AVC_NAL_SPS_EXT,
+ AVC_NAL_AUXILIARY_SLICE=19
+};
+
+enum
+{
+ HEVC_NAL_TRAIL_N = 0,
+ HEVC_NAL_TRAIL_R = 1,
+ HEVC_NAL_TSA_N = 2,
+ HEVC_NAL_TSA_R = 3,
+ HEVC_NAL_STSA_N = 4,
+ HEVC_NAL_STSA_R = 5,
+ HEVC_NAL_RADL_N = 6,
+ HEVC_NAL_RADL_R = 7,
+ HEVC_NAL_RASL_N = 8,
+ HEVC_NAL_RASL_R = 9,
+ HEVC_NAL_BLA_W_LP = 16,
+ HEVC_NAL_BLA_W_RADL = 17,
+ HEVC_NAL_BLA_N_LP = 18,
+ HEVC_NAL_IDR_W_RADL = 19,
+ HEVC_NAL_IDR_N_LP = 20,
+ HEVC_NAL_CRA_NUT = 21,
+ HEVC_NAL_VPS = 32,
+ HEVC_NAL_SPS = 33,
+ HEVC_NAL_PPS = 34,
+ HEVC_NAL_AUD = 35,
+ HEVC_NAL_EOS_NUT = 36,
+ HEVC_NAL_EOB_NUT = 37,
+ HEVC_NAL_FD_NUT = 38,
+ HEVC_NAL_SEI_PREFIX = 39,
+ HEVC_NAL_SEI_SUFFIX = 40,
+ HEVC_NAL_UNSPEC62 = 62, // Dolby Vision RPU
+ HEVC_NAL_UNSPEC63 = 63 // Dolby Vision EL
+};
+
+enum {
+ SEI_BUFFERING_PERIOD = 0,
+ SEI_PIC_TIMING,
+ SEI_PAN_SCAN_RECT,
+ SEI_FILLER_PAYLOAD,
+ SEI_USER_DATA_REGISTERED_ITU_T_T35,
+ SEI_USER_DATA_UNREGISTERED,
+ SEI_RECOVERY_POINT,
+ SEI_DEC_REF_PIC_MARKING_REPETITION,
+ SEI_SPARE_PIC,
+ SEI_SCENE_INFO,
+ SEI_SUB_SEQ_INFO,
+ SEI_SUB_SEQ_LAYER_CHARACTERISTICS,
+ SEI_SUB_SEQ_CHARACTERISTICS,
+ SEI_FULL_FRAME_FREEZE,
+ SEI_FULL_FRAME_FREEZE_RELEASE,
+ SEI_FULL_FRAME_SNAPSHOT,
+ SEI_PROGRESSIVE_REFINEMENT_SEGMENT_START,
+ SEI_PROGRESSIVE_REFINEMENT_SEGMENT_END,
+ SEI_MOTION_CONSTRAINED_SLICE_GROUP_SET,
+ SEI_FILM_GRAIN_CHARACTERISTICS,
+ SEI_DEBLOCKING_FILTER_DISPLAY_PREFERENCE,
+ SEI_STEREO_VIDEO_INFO,
+ SEI_POST_FILTER_HINTS,
+ SEI_TONE_MAPPING
+};
+
+/*
+ * GStreamer h264 parser
+ * Copyright (C) 2005 Michal Benes <michal.benes@itonis.tv>
+ * (C) 2008 Wim Taymans <wim.taymans@gmail.com>
+ * gsth264parse.c
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+static void nal_bs_init(nal_bitstream *bs, const uint8_t *data, size_t size)
+{
+ bs->data = data;
+ bs->end = data + size;
+ bs->head = 0;
+ // fill with something other than 0 to detect
+ // emulation prevention bytes
+ bs->cache = 0xffffffff;
+}
+
+static uint32_t nal_bs_read(nal_bitstream *bs, int n)
+{
+ uint32_t res = 0;
+ int shift;
+
+ if (n == 0)
+ return res;
+
+ // fill up the cache if we need to
+ while (bs->head < n)
+ {
+ uint8_t a_byte;
+ bool check_three_byte;
+
+ check_three_byte = true;
+next_byte:
+ if (bs->data >= bs->end)
+ {
+ // we're at the end, can't produce more than head number of bits
+ n = bs->head;
+ break;
+ }
+ // get the byte, this can be an emulation_prevention_three_byte that we need
+ // to ignore.
+ a_byte = *bs->data++;
+ if (check_three_byte && a_byte == 0x03 && ((bs->cache & 0xffff) == 0))
+ {
+ // next byte goes unconditionally to the cache, even if it's 0x03
+ check_three_byte = false;
+ goto next_byte;
+ }
+ // shift bytes in cache, moving the head bits of the cache left
+ bs->cache = (bs->cache << 8) | a_byte;
+ bs->head += 8;
+ }
+
+ // bring the required bits down and truncate
+ if ((shift = bs->head - n) > 0)
+ res = static_cast<uint32_t>(bs->cache >> shift);
+ else
+ res = static_cast<uint32_t>(bs->cache);
+
+ // mask out required bits
+ if (n < 32)
+ res &= (1 << n) - 1;
+ bs->head = shift;
+
+ return res;
+}
+
+static bool nal_bs_eos(nal_bitstream *bs)
+{
+ return (bs->data >= bs->end) && (bs->head == 0);
+}
+
+// read unsigned Exp-Golomb code
+static int nal_bs_read_ue(nal_bitstream *bs)
+{
+ int i = 0;
+
+ while (nal_bs_read(bs, 1) == 0 && !nal_bs_eos(bs) && i < 31)
+ i++;
+
+ return ((1 << i) - 1 + nal_bs_read(bs, i));
+}
+
+static const uint8_t* avc_find_startcode_internal(const uint8_t *p, const uint8_t *end)
+{
+ const uint8_t *a = p + 4 - ((intptr_t)p & 3);
+
+ for (end -= 3; p < a && p < end; p++)
+ {
+ if (p[0] == 0 && p[1] == 0 && p[2] == 1)
+ return p;
+ }
+
+ for (end -= 3; p < end; p += 4)
+ {
+ uint32_t x = *(const uint32_t*)p;
+ if ((x - 0x01010101) & (~x) & 0x80808080) // generic
+ {
+ if (p[1] == 0)
+ {
+ if (p[0] == 0 && p[2] == 1)
+ return p;
+ if (p[2] == 0 && p[3] == 1)
+ return p+1;
+ }
+ if (p[3] == 0)
+ {
+ if (p[2] == 0 && p[4] == 1)
+ return p+2;
+ if (p[4] == 0 && p[5] == 1)
+ return p+3;
+ }
+ }
+ }
+
+ for (end += 3; p < end; p++)
+ {
+ if (p[0] == 0 && p[1] == 0 && p[2] == 1)
+ return p;
+ }
+
+ return end + 3;
+}
+
+static const uint8_t* avc_find_startcode(const uint8_t *p, const uint8_t *end)
+{
+ const uint8_t *out = avc_find_startcode_internal(p, end);
+ if (p<out && out<end && !out[-1])
+ out--;
+ return out;
+}
+
+static bool has_sei_recovery_point(const uint8_t *p, const uint8_t *end)
+{
+ int pt(0), ps(0), offset(1);
+
+ do
+ {
+ pt = 0;
+ do {
+ pt += p[offset];
+ } while (p[offset++] == 0xFF);
+
+ ps = 0;
+ do {
+ ps += p[offset];
+ } while (p[offset++] == 0xFF);
+
+ if (pt == SEI_RECOVERY_POINT)
+ {
+ nal_bitstream bs;
+ nal_bs_init(&bs, p + offset, ps);
+ return nal_bs_read_ue(&bs) >= 0;
+ }
+ offset += ps;
+ } while (p + offset < end && p[offset] != 0x80);
+
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////////
+CBitstreamParser::CBitstreamParser() = default;
+
+void CBitstreamParser::Close()
+{
+}
+
+bool CBitstreamParser::CanStartDecode(const uint8_t *buf, int buf_size)
+{
+ if (!buf)
+ return false;
+
+ bool rtn = false;
+ uint32_t state = -1;
+ const uint8_t *buf_begin, *buf_end = buf + buf_size;
+
+ for (; rtn == false;)
+ {
+ buf = find_start_code(buf, buf_end, &state);
+ if (buf >= buf_end)
+ {
+ break;
+ }
+
+ switch (state & 0x1f)
+ {
+ case AVC_NAL_SLICE:
+ break;
+ case AVC_NAL_IDR_SLICE:
+ rtn = true;
+ break;
+ case AVC_NAL_SEI:
+ buf_begin = buf - 1;
+ buf = find_start_code(buf, buf_end, &state) - 4;
+ if (has_sei_recovery_point(buf_begin, buf))
+ rtn = true;
+ break;
+ case AVC_NAL_SPS:
+ rtn = true;
+ break;
+ case AVC_NAL_PPS:
+ break;
+ default:
+ break;
+ }
+ }
+
+ return rtn;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////////
+CBitstreamConverter::CBitstreamConverter()
+{
+ m_convert_bitstream = false;
+ m_convertBuffer = NULL;
+ m_convertSize = 0;
+ m_inputBuffer = NULL;
+ m_inputSize = 0;
+ m_to_annexb = false;
+ m_extradata = NULL;
+ m_extrasize = 0;
+ m_convert_3byteTo4byteNALSize = false;
+ m_convert_bytestream = false;
+ m_sps_pps_context.sps_pps_data = NULL;
+ m_start_decode = true;
+}
+
+CBitstreamConverter::~CBitstreamConverter()
+{
+ Close();
+}
+
+bool CBitstreamConverter::Open(enum AVCodecID codec, uint8_t *in_extradata, int in_extrasize, bool to_annexb)
+{
+ m_to_annexb = to_annexb;
+
+ m_codec = codec;
+ switch(m_codec)
+ {
+ case AV_CODEC_ID_H264:
+ if (in_extrasize < 7 || in_extradata == NULL)
+ {
+ CLog::Log(LOGERROR, "CBitstreamConverter::Open avcC data too small or missing");
+ return false;
+ }
+ // valid avcC data (bitstream) always starts with the value 1 (version)
+ if(m_to_annexb)
+ {
+ if ( in_extradata[0] == 1 )
+ {
+ CLog::Log(LOGINFO, "CBitstreamConverter::Open bitstream to annexb init");
+ m_extrasize = in_extrasize;
+ m_extradata = (uint8_t*)av_malloc(in_extrasize);
+ memcpy(m_extradata, in_extradata, in_extrasize);
+ m_convert_bitstream = BitstreamConvertInitAVC(m_extradata, m_extrasize);
+ return true;
+ }
+ else
+ CLog::Log(LOGINFO, "CBitstreamConverter::Open Invalid avcC");
+ }
+ else
+ {
+ // valid avcC atom data always starts with the value 1 (version)
+ if ( in_extradata[0] != 1 )
+ {
+ if ( (in_extradata[0] == 0 && in_extradata[1] == 0 && in_extradata[2] == 0 && in_extradata[3] == 1) ||
+ (in_extradata[0] == 0 && in_extradata[1] == 0 && in_extradata[2] == 1) )
+ {
+ CLog::Log(LOGINFO, "CBitstreamConverter::Open annexb to bitstream init");
+ // video content is from x264 or from bytestream h264 (AnnexB format)
+ // NAL reformatting to bitstream format needed
+ AVIOContext *pb;
+ if (avio_open_dyn_buf(&pb) < 0)
+ return false;
+ m_convert_bytestream = true;
+ // create a valid avcC atom data from ffmpeg's extradata
+ isom_write_avcc(pb, in_extradata, in_extrasize);
+ // unhook from ffmpeg's extradata
+ in_extradata = NULL;
+ // extract the avcC atom data into extradata then write it into avcCData for VDADecoder
+ in_extrasize = avio_close_dyn_buf(pb, &in_extradata);
+ // make a copy of extradata contents
+ m_extradata = (uint8_t *)av_malloc(in_extrasize);
+ memcpy(m_extradata, in_extradata, in_extrasize);
+ m_extrasize = in_extrasize;
+ // done with the converted extradata, we MUST free using av_free
+ av_free(in_extradata);
+ return true;
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "CBitstreamConverter::Open invalid avcC atom data");
+ return false;
+ }
+ }
+ else
+ {
+ if (in_extradata[4] == 0xFE)
+ {
+ CLog::Log(LOGINFO, "CBitstreamConverter::Open annexb to bitstream init 3 byte to 4 byte nal");
+ // video content is from so silly encoder that think 3 byte NAL sizes
+ // are valid, setup to convert 3 byte NAL sizes to 4 byte.
+ in_extradata[4] = 0xFF;
+ m_convert_3byteTo4byteNALSize = true;
+
+ m_extradata = (uint8_t *)av_malloc(in_extrasize);
+ memcpy(m_extradata, in_extradata, in_extrasize);
+ m_extrasize = in_extrasize;
+ return true;
+ }
+ }
+ // valid avcC atom
+ m_extradata = (uint8_t*)av_malloc(in_extrasize);
+ memcpy(m_extradata, in_extradata, in_extrasize);
+ m_extrasize = in_extrasize;
+ return true;
+ }
+ return false;
+ break;
+ case AV_CODEC_ID_HEVC:
+ if (in_extrasize < 23 || in_extradata == NULL)
+ {
+ CLog::Log(LOGERROR, "CBitstreamConverter::Open hvcC data too small or missing");
+ return false;
+ }
+ // valid hvcC data (bitstream) always starts with the value 1 (version)
+ if(m_to_annexb)
+ {
+ /**
+ * It seems the extradata is encoded as hvcC format.
+ * Temporarily, we support configurationVersion==0 until 14496-15 3rd
+ * is finalized. When finalized, configurationVersion will be 1 and we
+ * can recognize hvcC by checking if extradata[0]==1 or not.
+ */
+
+ if (in_extradata[0] || in_extradata[1] || in_extradata[2] > 1)
+ {
+ CLog::Log(LOGINFO, "CBitstreamConverter::Open bitstream to annexb init");
+ m_extrasize = in_extrasize;
+ m_extradata = (uint8_t*)av_malloc(in_extrasize);
+ memcpy(m_extradata, in_extradata, in_extrasize);
+ m_convert_bitstream = BitstreamConvertInitHEVC(m_extradata, m_extrasize);
+ return true;
+ }
+ else
+ CLog::Log(LOGINFO, "CBitstreamConverter::Open Invalid hvcC");
+ }
+ else
+ {
+ // valid hvcC atom data always starts with the value 1 (version)
+ if ( in_extradata[0] != 1 )
+ {
+ if ( (in_extradata[0] == 0 && in_extradata[1] == 0 && in_extradata[2] == 0 && in_extradata[3] == 1) ||
+ (in_extradata[0] == 0 && in_extradata[1] == 0 && in_extradata[2] == 1) )
+ {
+ CLog::Log(LOGINFO, "CBitstreamConverter::Open annexb to bitstream init");
+ //! @todo convert annexb to bitstream format
+ return false;
+ }
+ else
+ {
+ CLog::Log(LOGINFO, "CBitstreamConverter::Open invalid hvcC atom data");
+ return false;
+ }
+ }
+ else
+ {
+ if ((in_extradata[4] & 0x3) == 2)
+ {
+ CLog::Log(LOGINFO, "CBitstreamConverter::Open annexb to bitstream init 3 byte to 4 byte nal");
+ // video content is from so silly encoder that think 3 byte NAL sizes
+ // are valid, setup to convert 3 byte NAL sizes to 4 byte.
+ in_extradata[4] |= 0x03;
+ m_convert_3byteTo4byteNALSize = true;
+ }
+ }
+ // valid hvcC atom
+ m_extradata = (uint8_t*)av_malloc(in_extrasize);
+ memcpy(m_extradata, in_extradata, in_extrasize);
+ m_extrasize = in_extrasize;
+ return true;
+ }
+ return false;
+ break;
+ default:
+ return false;
+ break;
+ }
+ return false;
+}
+
+void CBitstreamConverter::Close(void)
+{
+ if (m_sps_pps_context.sps_pps_data)
+ av_free(m_sps_pps_context.sps_pps_data), m_sps_pps_context.sps_pps_data = NULL;
+
+ if (m_convertBuffer)
+ av_free(m_convertBuffer), m_convertBuffer = NULL;
+ m_convertSize = 0;
+
+ if (m_extradata)
+ av_free(m_extradata), m_extradata = NULL;
+ m_extrasize = 0;
+
+ m_inputSize = 0;
+ m_inputBuffer = NULL;
+
+ m_convert_bitstream = false;
+ m_convert_bytestream = false;
+ m_convert_3byteTo4byteNALSize = false;
+}
+
+bool CBitstreamConverter::Convert(uint8_t *pData, int iSize)
+{
+ if (m_convertBuffer)
+ {
+ av_free(m_convertBuffer);
+ m_convertBuffer = NULL;
+ }
+ m_inputSize = 0;
+ m_convertSize = 0;
+ m_inputBuffer = NULL;
+
+ if (pData)
+ {
+ if (m_codec == AV_CODEC_ID_H264 ||
+ m_codec == AV_CODEC_ID_HEVC)
+ {
+ if (m_to_annexb)
+ {
+ int demuxer_bytes = iSize;
+ uint8_t *demuxer_content = pData;
+
+ if (m_convert_bitstream)
+ {
+ // convert demuxer packet from bitstream to bytestream (AnnexB)
+ int bytestream_size = 0;
+ uint8_t *bytestream_buff = NULL;
+
+ BitstreamConvert(demuxer_content, demuxer_bytes, &bytestream_buff, &bytestream_size);
+ if (bytestream_buff && (bytestream_size > 0))
+ {
+ m_convertSize = bytestream_size;
+ m_convertBuffer = bytestream_buff;
+ return true;
+ }
+ else
+ {
+ m_convertSize = 0;
+ m_convertBuffer = NULL;
+ CLog::Log(LOGERROR, "CBitstreamConverter::Convert: error converting.");
+ return false;
+ }
+ }
+ else
+ {
+ m_inputSize = iSize;
+ m_inputBuffer = pData;
+ return true;
+ }
+ }
+ else
+ {
+ m_inputSize = iSize;
+ m_inputBuffer = pData;
+
+ if (m_convert_bytestream)
+ {
+ if(m_convertBuffer)
+ {
+ av_free(m_convertBuffer);
+ m_convertBuffer = NULL;
+ }
+ m_convertSize = 0;
+
+ // convert demuxer packet from bytestream (AnnexB) to bitstream
+ AVIOContext *pb;
+
+ if(avio_open_dyn_buf(&pb) < 0)
+ {
+ return false;
+ }
+ m_convertSize = avc_parse_nal_units(pb, pData, iSize);
+ m_convertSize = avio_close_dyn_buf(pb, &m_convertBuffer);
+ }
+ else if (m_convert_3byteTo4byteNALSize)
+ {
+ if(m_convertBuffer)
+ {
+ av_free(m_convertBuffer);
+ m_convertBuffer = NULL;
+ }
+ m_convertSize = 0;
+
+ // convert demuxer packet from 3 byte NAL sizes to 4 byte
+ AVIOContext *pb;
+ if (avio_open_dyn_buf(&pb) < 0)
+ return false;
+
+ uint32_t nal_size;
+ uint8_t *end = pData + iSize;
+ uint8_t *nal_start = pData;
+ while (nal_start < end)
+ {
+ nal_size = BS_RB24(nal_start);
+ avio_wb32(pb, nal_size);
+ nal_start += 3;
+ avio_write(pb, nal_start, nal_size);
+ nal_start += nal_size;
+ }
+
+ m_convertSize = avio_close_dyn_buf(pb, &m_convertBuffer);
+ }
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+uint8_t *CBitstreamConverter::GetConvertBuffer() const
+{
+ if((m_convert_bitstream || m_convert_bytestream || m_convert_3byteTo4byteNALSize) && m_convertBuffer != NULL)
+ return m_convertBuffer;
+ else
+ return m_inputBuffer;
+}
+
+int CBitstreamConverter::GetConvertSize() const
+{
+ if((m_convert_bitstream || m_convert_bytestream || m_convert_3byteTo4byteNALSize) && m_convertBuffer != NULL)
+ return m_convertSize;
+ else
+ return m_inputSize;
+}
+
+uint8_t *CBitstreamConverter::GetExtraData() const
+{
+ if(m_convert_bitstream)
+ return m_sps_pps_context.sps_pps_data;
+ else
+ return m_extradata;
+}
+int CBitstreamConverter::GetExtraSize() const
+{
+ if(m_convert_bitstream)
+ return m_sps_pps_context.size;
+ else
+ return m_extrasize;
+}
+
+void CBitstreamConverter::ResetStartDecode(void)
+{
+ m_start_decode = false;
+}
+
+bool CBitstreamConverter::CanStartDecode() const
+{
+ return m_start_decode;
+}
+
+bool CBitstreamConverter::BitstreamConvertInitAVC(void *in_extradata, int in_extrasize)
+{
+ // based on h264_mp4toannexb_bsf.c (ffmpeg)
+ // which is Copyright (c) 2007 Benoit Fouet <benoit.fouet@free.fr>
+ // and Licensed GPL 2.1 or greater
+
+ m_sps_pps_size = 0;
+ m_sps_pps_context.sps_pps_data = NULL;
+
+ // nothing to filter
+ if (!in_extradata || in_extrasize < 6)
+ return false;
+
+ uint16_t unit_size;
+ uint32_t total_size = 0;
+ uint8_t *out = NULL, unit_nb, sps_done = 0, sps_seen = 0, pps_seen = 0;
+ const uint8_t *extradata = (uint8_t*)in_extradata + 4;
+ static const uint8_t nalu_header[4] = {0, 0, 0, 1};
+
+ // retrieve length coded size
+ m_sps_pps_context.length_size = (*extradata++ & 0x3) + 1;
+
+ // retrieve sps and pps unit(s)
+ unit_nb = *extradata++ & 0x1f; // number of sps unit(s)
+ if (!unit_nb)
+ {
+ goto pps;
+ }
+ else
+ {
+ sps_seen = 1;
+ }
+
+ while (unit_nb--)
+ {
+ void *tmp;
+
+ unit_size = extradata[0] << 8 | extradata[1];
+ total_size += unit_size + 4;
+
+ if (total_size > INT_MAX - AV_INPUT_BUFFER_PADDING_SIZE ||
+ (extradata + 2 + unit_size) > ((uint8_t*)in_extradata + in_extrasize))
+ {
+ av_free(out);
+ return false;
+ }
+ tmp = av_realloc(out, total_size + AV_INPUT_BUFFER_PADDING_SIZE);
+ if (!tmp)
+ {
+ av_free(out);
+ return false;
+ }
+ out = (uint8_t*)tmp;
+ memcpy(out + total_size - unit_size - 4, nalu_header, 4);
+ memcpy(out + total_size - unit_size, extradata + 2, unit_size);
+ extradata += 2 + unit_size;
+
+pps:
+ if (!unit_nb && !sps_done++)
+ {
+ unit_nb = *extradata++; // number of pps unit(s)
+ if (unit_nb)
+ pps_seen = 1;
+ }
+ }
+
+ if (out)
+ memset(out + total_size, 0, AV_INPUT_BUFFER_PADDING_SIZE);
+
+ if (!sps_seen)
+ CLog::Log(LOGDEBUG, "SPS NALU missing or invalid. The resulting stream may not play");
+ if (!pps_seen)
+ CLog::Log(LOGDEBUG, "PPS NALU missing or invalid. The resulting stream may not play");
+
+ m_sps_pps_context.sps_pps_data = out;
+ m_sps_pps_context.size = total_size;
+ m_sps_pps_context.first_idr = 1;
+ m_sps_pps_context.idr_sps_pps_seen = 0;
+
+ return true;
+}
+
+bool CBitstreamConverter::BitstreamConvertInitHEVC(void *in_extradata, int in_extrasize)
+{
+ m_sps_pps_size = 0;
+ m_sps_pps_context.sps_pps_data = NULL;
+
+ // nothing to filter
+ if (!in_extradata || in_extrasize < 23)
+ return false;
+
+ uint16_t unit_nb, unit_size;
+ uint32_t total_size = 0;
+ uint8_t *out = NULL, array_nb, nal_type, sps_seen = 0, pps_seen = 0;
+ const uint8_t *extradata = (uint8_t*)in_extradata + 21;
+ static const uint8_t nalu_header[4] = {0, 0, 0, 1};
+
+ // retrieve length coded size
+ m_sps_pps_context.length_size = (*extradata++ & 0x3) + 1;
+
+ array_nb = *extradata++;
+ while (array_nb--)
+ {
+ nal_type = *extradata++ & 0x3f;
+ unit_nb = extradata[0] << 8 | extradata[1];
+ extradata += 2;
+
+ if (nal_type == HEVC_NAL_SPS && unit_nb)
+ {
+ sps_seen = 1;
+ }
+ else if (nal_type == HEVC_NAL_PPS && unit_nb)
+ {
+ pps_seen = 1;
+ }
+ while (unit_nb--)
+ {
+ void *tmp;
+
+ unit_size = extradata[0] << 8 | extradata[1];
+ extradata += 2;
+ if (nal_type != HEVC_NAL_SPS &&
+ nal_type != HEVC_NAL_PPS &&
+ nal_type != HEVC_NAL_VPS)
+ {
+ extradata += unit_size;
+ continue;
+ }
+ total_size += unit_size + 4;
+
+ if (total_size > INT_MAX - AV_INPUT_BUFFER_PADDING_SIZE ||
+ (extradata + unit_size) > ((uint8_t*)in_extradata + in_extrasize))
+ {
+ av_free(out);
+ return false;
+ }
+ tmp = av_realloc(out, total_size + AV_INPUT_BUFFER_PADDING_SIZE);
+ if (!tmp)
+ {
+ av_free(out);
+ return false;
+ }
+ out = (uint8_t*)tmp;
+ memcpy(out + total_size - unit_size - 4, nalu_header, 4);
+ memcpy(out + total_size - unit_size, extradata, unit_size);
+ extradata += unit_size;
+ }
+ }
+
+ if (out)
+ memset(out + total_size, 0, AV_INPUT_BUFFER_PADDING_SIZE);
+
+ if (!sps_seen)
+ CLog::Log(LOGDEBUG, "SPS NALU missing or invalid. The resulting stream may not play");
+ if (!pps_seen)
+ CLog::Log(LOGDEBUG, "PPS NALU missing or invalid. The resulting stream may not play");
+
+ m_sps_pps_context.sps_pps_data = out;
+ m_sps_pps_context.size = total_size;
+ m_sps_pps_context.first_idr = 1;
+ m_sps_pps_context.idr_sps_pps_seen = 0;
+
+ return true;
+}
+
+bool CBitstreamConverter::IsIDR(uint8_t unit_type)
+{
+ switch (m_codec)
+ {
+ case AV_CODEC_ID_H264:
+ return unit_type == AVC_NAL_IDR_SLICE;
+ case AV_CODEC_ID_HEVC:
+ return unit_type == HEVC_NAL_IDR_W_RADL ||
+ unit_type == HEVC_NAL_IDR_N_LP ||
+ unit_type == HEVC_NAL_CRA_NUT;
+ default:
+ return false;
+ }
+}
+
+bool CBitstreamConverter::IsSlice(uint8_t unit_type)
+{
+ switch (m_codec)
+ {
+ case AV_CODEC_ID_H264:
+ return unit_type == AVC_NAL_SLICE;
+ case AV_CODEC_ID_HEVC:
+ return unit_type == HEVC_NAL_TRAIL_R ||
+ unit_type == HEVC_NAL_TRAIL_N ||
+ unit_type == HEVC_NAL_TSA_N ||
+ unit_type == HEVC_NAL_TSA_R ||
+ unit_type == HEVC_NAL_STSA_N ||
+ unit_type == HEVC_NAL_STSA_R ||
+ unit_type == HEVC_NAL_BLA_W_LP ||
+ unit_type == HEVC_NAL_BLA_W_RADL ||
+ unit_type == HEVC_NAL_BLA_N_LP ||
+ unit_type == HEVC_NAL_CRA_NUT ||
+ unit_type == HEVC_NAL_RADL_N ||
+ unit_type == HEVC_NAL_RADL_R ||
+ unit_type == HEVC_NAL_RASL_N ||
+ unit_type == HEVC_NAL_RASL_R;
+ default:
+ return false;
+ }
+}
+
+bool CBitstreamConverter::BitstreamConvert(uint8_t* pData, int iSize, uint8_t **poutbuf, int *poutbuf_size)
+{
+ // based on h264_mp4toannexb_bsf.c (ffmpeg)
+ // which is Copyright (c) 2007 Benoit Fouet <benoit.fouet@free.fr>
+ // and Licensed GPL 2.1 or greater
+
+ int i;
+ uint8_t *buf = pData;
+ uint32_t buf_size = iSize;
+ uint8_t unit_type, nal_sps, nal_pps, nal_sei;
+ int32_t nal_size;
+ uint32_t cumul_size = 0;
+ const uint8_t *buf_end = buf + buf_size;
+
+ switch (m_codec)
+ {
+ case AV_CODEC_ID_H264:
+ nal_sps = AVC_NAL_SPS;
+ nal_pps = AVC_NAL_PPS;
+ nal_sei = AVC_NAL_SEI;
+ break;
+ case AV_CODEC_ID_HEVC:
+ nal_sps = HEVC_NAL_SPS;
+ nal_pps = HEVC_NAL_PPS;
+ nal_sei = HEVC_NAL_SEI_PREFIX;
+ break;
+ default:
+ return false;
+ }
+
+ do
+ {
+ if (buf + m_sps_pps_context.length_size > buf_end)
+ goto fail;
+
+ for (nal_size = 0, i = 0; i < m_sps_pps_context.length_size; i++)
+ nal_size = (nal_size << 8) | buf[i];
+
+ buf += m_sps_pps_context.length_size;
+ if (m_codec == AV_CODEC_ID_H264)
+ {
+ unit_type = *buf & 0x1f;
+ }
+ else
+ {
+ unit_type = (*buf >> 1) & 0x3f;
+ }
+
+ if (buf + nal_size > buf_end || nal_size <= 0)
+ goto fail;
+
+ // Don't add sps/pps if the unit already contain them
+ if (m_sps_pps_context.first_idr && (unit_type == nal_sps || unit_type == nal_pps))
+ m_sps_pps_context.idr_sps_pps_seen = 1;
+
+ if (!m_start_decode && (unit_type == nal_sps || IsIDR(unit_type) || (unit_type == nal_sei && has_sei_recovery_point(buf, buf + nal_size))))
+ m_start_decode = true;
+
+ // prepend only to the first access unit of an IDR picture, if no sps/pps already present
+ if (m_sps_pps_context.first_idr && IsIDR(unit_type) && !m_sps_pps_context.idr_sps_pps_seen)
+ {
+ BitstreamAllocAndCopy(poutbuf, poutbuf_size, m_sps_pps_context.sps_pps_data,
+ m_sps_pps_context.size, buf, nal_size, unit_type);
+ m_sps_pps_context.first_idr = 0;
+ }
+ else
+ {
+ BitstreamAllocAndCopy(poutbuf, poutbuf_size, NULL, 0, buf, nal_size, unit_type);
+ if (!m_sps_pps_context.first_idr && IsSlice(unit_type))
+ {
+ m_sps_pps_context.first_idr = 1;
+ m_sps_pps_context.idr_sps_pps_seen = 0;
+ }
+ }
+
+ buf += nal_size;
+ cumul_size += nal_size + m_sps_pps_context.length_size;
+ } while (cumul_size < buf_size);
+
+ return true;
+
+fail:
+ av_free(*poutbuf), *poutbuf = NULL;
+ *poutbuf_size = 0;
+ return false;
+}
+
+void CBitstreamConverter::BitstreamAllocAndCopy(uint8_t** poutbuf,
+ int* poutbuf_size,
+ const uint8_t* sps_pps,
+ uint32_t sps_pps_size,
+ const uint8_t* in,
+ uint32_t in_size,
+ uint8_t nal_type)
+{
+ // based on h264_mp4toannexb_bsf.c (ffmpeg)
+ // which is Copyright (c) 2007 Benoit Fouet <benoit.fouet@free.fr>
+ // and Licensed GPL 2.1 or greater
+
+ uint32_t offset = *poutbuf_size;
+ uint8_t nal_header_size = offset ? 3 : 4;
+ void *tmp;
+
+ // According to x265, this type is always encoded with four-sized header
+ // https://bitbucket.org/multicoreware/x265_git/src/4bf31dc15fb6d1f93d12ecf21fad5e695f0db5c0/source/encoder/nal.cpp#lines-100
+ if (nal_type == HEVC_NAL_UNSPEC62)
+ nal_header_size = 4;
+
+ *poutbuf_size += sps_pps_size + in_size + nal_header_size;
+ tmp = av_realloc(*poutbuf, *poutbuf_size);
+ if (!tmp)
+ return;
+ *poutbuf = (uint8_t*)tmp;
+ if (sps_pps)
+ memcpy(*poutbuf + offset, sps_pps, sps_pps_size);
+
+ memcpy(*poutbuf + sps_pps_size + nal_header_size + offset, in, in_size);
+ if (!offset)
+ {
+ BS_WB32(*poutbuf + sps_pps_size, 1);
+ }
+ else if (nal_header_size == 4)
+ {
+ (*poutbuf + offset + sps_pps_size)[0] = 0;
+ (*poutbuf + offset + sps_pps_size)[1] = 0;
+ (*poutbuf + offset + sps_pps_size)[2] = 0;
+ (*poutbuf + offset + sps_pps_size)[3] = 1;
+ }
+ else
+ {
+ (*poutbuf + offset + sps_pps_size)[0] = 0;
+ (*poutbuf + offset + sps_pps_size)[1] = 0;
+ (*poutbuf + offset + sps_pps_size)[2] = 1;
+ }
+}
+
+int CBitstreamConverter::avc_parse_nal_units(AVIOContext *pb, const uint8_t *buf_in, int size)
+{
+ const uint8_t *p = buf_in;
+ const uint8_t *end = p + size;
+ const uint8_t *nal_start, *nal_end;
+
+ size = 0;
+ nal_start = avc_find_startcode(p, end);
+
+ for (;;) {
+ while (nal_start < end && !*(nal_start++));
+ if (nal_start == end)
+ break;
+
+ nal_end = avc_find_startcode(nal_start, end);
+ avio_wb32(pb, nal_end - nal_start);
+ avio_write(pb, nal_start, nal_end - nal_start);
+ size += 4 + nal_end - nal_start;
+ nal_start = nal_end;
+ }
+ return size;
+}
+
+int CBitstreamConverter::avc_parse_nal_units_buf(const uint8_t *buf_in, uint8_t **buf, int *size)
+{
+ AVIOContext *pb;
+ int ret = avio_open_dyn_buf(&pb);
+ if (ret < 0)
+ return ret;
+
+ avc_parse_nal_units(pb, buf_in, *size);
+
+ av_freep(buf);
+ *size = avio_close_dyn_buf(pb, buf);
+ return 0;
+}
+
+int CBitstreamConverter::isom_write_avcc(AVIOContext *pb, const uint8_t *data, int len)
+{
+ // extradata from bytestream h264, convert to avcC atom data for bitstream
+ if (len > 6)
+ {
+ /* check for h264 start code */
+ if (BS_RB32(data) == 0x00000001 || BS_RB24(data) == 0x000001)
+ {
+ uint8_t *buf=NULL, *end, *start;
+ uint32_t sps_size=0, pps_size=0;
+ uint8_t *sps=0, *pps=0;
+
+ int ret = avc_parse_nal_units_buf(data, &buf, &len);
+ if (ret < 0)
+ return ret;
+ start = buf;
+ end = buf + len;
+
+ /* look for sps and pps */
+ while (end - buf > 4)
+ {
+ uint32_t size;
+ uint8_t nal_type;
+ size = std::min<uint32_t>(BS_RB32(buf), end - buf - 4);
+ buf += 4;
+ nal_type = buf[0] & 0x1f;
+ if (nal_type == 7) /* SPS */
+ {
+ sps = buf;
+ sps_size = size;
+ }
+ else if (nal_type == 8) /* PPS */
+ {
+ pps = buf;
+ pps_size = size;
+ }
+ buf += size;
+ }
+ if (!sps || !pps || sps_size < 4 || sps_size > UINT16_MAX || pps_size > UINT16_MAX)
+ assert(0);
+
+ avio_w8(pb, 1); /* version */
+ avio_w8(pb, sps[1]); /* profile */
+ avio_w8(pb, sps[2]); /* profile compat */
+ avio_w8(pb, sps[3]); /* level */
+ avio_w8(pb, 0xff); /* 6 bits reserved (111111) + 2 bits nal size length - 1 (11) */
+ avio_w8(pb, 0xe1); /* 3 bits reserved (111) + 5 bits number of sps (00001) */
+
+ avio_wb16(pb, sps_size);
+ avio_write(pb, sps, sps_size);
+ if (pps)
+ {
+ avio_w8(pb, 1); /* number of pps */
+ avio_wb16(pb, pps_size);
+ avio_write(pb, pps, pps_size);
+ }
+ av_free(start);
+ }
+ else
+ {
+ avio_write(pb, data, len);
+ }
+ }
+ return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////////
+bool CBitstreamConverter::mpeg2_sequence_header(const uint8_t *data, const uint32_t size, mpeg2_sequence *sequence)
+{
+ // parse nal's until a sequence_header_code is found
+ // and return the width, height, aspect ratio and frame rate if changed.
+ bool changed = false;
+
+ if (!data)
+ return changed;
+
+ const uint8_t *p = data;
+ const uint8_t *end = p + size;
+ const uint8_t *nal_start, *nal_end;
+
+ nal_start = avc_find_startcode(p, end);
+ while (nal_start < end)
+ {
+ while (!*(nal_start++));
+ nal_end = avc_find_startcode(nal_start, end);
+ if (*nal_start == 0xB3)
+ {
+ nal_bitstream bs;
+ nal_bs_init(&bs, nal_start, end - nal_start);
+
+ // sequence_header_code
+ nal_bs_read(&bs, 8);
+
+ // width
+ // nal_start + 12 bits == horizontal_size_value
+ uint32_t width = nal_bs_read(&bs, 12);
+ if (width != sequence->width)
+ {
+ changed = true;
+ sequence->width = width;
+ }
+ // height
+ // nal_start + 24 bits == vertical_size_value
+ uint32_t height = nal_bs_read(&bs, 12);
+ if (height != sequence->height)
+ {
+ changed = true;
+ sequence->height = height;
+ }
+
+ // aspect ratio
+ // nal_start + 28 bits == aspect_ratio_information
+ float ratio = sequence->ratio;
+ uint32_t ratio_info = nal_bs_read(&bs, 4);
+ switch(ratio_info)
+ {
+ case 0x01:
+ ratio = 1.0f;
+ break;
+ default:
+ case 0x02:
+ ratio = 4.0f/3;
+ break;
+ case 0x03:
+ ratio = 16.0f/9;
+ break;
+ case 0x04:
+ ratio = 2.21f;
+ break;
+ }
+ if (ratio_info != sequence->ratio_info)
+ {
+ changed = true;
+ sequence->ratio = ratio;
+ sequence->ratio_info = ratio_info;
+ }
+
+ // frame rate
+ // nal_start + 32 bits == frame_rate_code
+ uint32_t fpsrate = sequence->fps_rate;
+ uint32_t fpsscale = sequence->fps_scale;
+ uint32_t rate_info = nal_bs_read(&bs, 4);
+
+ switch(rate_info)
+ {
+ default:
+ case 0x01:
+ fpsrate = 24000;
+ fpsscale = 1001;
+ break;
+ case 0x02:
+ fpsrate = 24000;
+ fpsscale = 1000;
+ break;
+ case 0x03:
+ fpsrate = 25000;
+ fpsscale = 1000;
+ break;
+ case 0x04:
+ fpsrate = 30000;
+ fpsscale = 1001;
+ break;
+ case 0x05:
+ fpsrate = 30000;
+ fpsscale = 1000;
+ break;
+ case 0x06:
+ fpsrate = 50000;
+ fpsscale = 1000;
+ break;
+ case 0x07:
+ fpsrate = 60000;
+ fpsscale = 1001;
+ break;
+ case 0x08:
+ fpsrate = 60000;
+ fpsscale = 1000;
+ break;
+ }
+
+ if (fpsscale != sequence->fps_scale || fpsrate != sequence->fps_rate)
+ {
+ changed = true;
+ sequence->fps_rate = fpsrate;
+ sequence->fps_scale = fpsscale;
+ }
+ }
+ nal_start = nal_end;
+ }
+
+ return changed;
+}
+
diff --git a/xbmc/utils/BitstreamConverter.h b/xbmc/utils/BitstreamConverter.h
new file mode 100644
index 0000000..3c57e14
--- /dev/null
+++ b/xbmc/utils/BitstreamConverter.h
@@ -0,0 +1,144 @@
+/*
+ * 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 <stdint.h>
+
+extern "C" {
+#include <libavutil/avutil.h>
+#include <libavformat/avformat.h>
+#include <libavfilter/avfilter.h>
+#include <libavcodec/avcodec.h>
+}
+
+typedef struct
+{
+ const uint8_t *data;
+ const uint8_t *end;
+ int head;
+ uint64_t cache;
+} nal_bitstream;
+
+typedef struct mpeg2_sequence
+{
+ uint32_t width;
+ uint32_t height;
+ uint32_t fps_rate;
+ uint32_t fps_scale;
+ float ratio;
+ uint32_t ratio_info;
+} mpeg2_sequence;
+
+typedef struct
+{
+ int profile_idc;
+ int level_idc;
+ int sps_id;
+
+ int chroma_format_idc;
+ int separate_colour_plane_flag;
+ int bit_depth_luma_minus8;
+ int bit_depth_chroma_minus8;
+ int qpprime_y_zero_transform_bypass_flag;
+ int seq_scaling_matrix_present_flag;
+
+ int log2_max_frame_num_minus4;
+ int pic_order_cnt_type;
+ int log2_max_pic_order_cnt_lsb_minus4;
+
+ int max_num_ref_frames;
+ int gaps_in_frame_num_value_allowed_flag;
+ int pic_width_in_mbs_minus1;
+ int pic_height_in_map_units_minus1;
+
+ int frame_mbs_only_flag;
+ int mb_adaptive_frame_field_flag;
+
+ int direct_8x8_inference_flag;
+
+ int frame_cropping_flag;
+ int frame_crop_left_offset;
+ int frame_crop_right_offset;
+ int frame_crop_top_offset;
+ int frame_crop_bottom_offset;
+} sps_info_struct;
+
+class CBitstreamParser
+{
+public:
+ CBitstreamParser();
+ ~CBitstreamParser() = default;
+
+ static bool Open() { return true; }
+ static void Close();
+ static bool CanStartDecode(const uint8_t *buf, int buf_size);
+};
+
+class CBitstreamConverter
+{
+public:
+ CBitstreamConverter();
+ ~CBitstreamConverter();
+
+ bool Open(enum AVCodecID codec, uint8_t *in_extradata, int in_extrasize, bool to_annexb);
+ void Close(void);
+ bool NeedConvert(void) const { return m_convert_bitstream; }
+ bool Convert(uint8_t *pData, int iSize);
+ uint8_t* GetConvertBuffer(void) const;
+ int GetConvertSize() const;
+ uint8_t* GetExtraData(void) const;
+ int GetExtraSize() const;
+ void ResetStartDecode(void);
+ bool CanStartDecode() const;
+
+ static bool mpeg2_sequence_header(const uint8_t *data, const uint32_t size, mpeg2_sequence *sequence);
+
+protected:
+ static int avc_parse_nal_units(AVIOContext *pb, const uint8_t *buf_in, int size);
+ static int avc_parse_nal_units_buf(const uint8_t *buf_in, uint8_t **buf, int *size);
+ int isom_write_avcc(AVIOContext *pb, const uint8_t *data, int len);
+ // bitstream to bytestream (Annex B) conversion support.
+ bool IsIDR(uint8_t unit_type);
+ bool IsSlice(uint8_t unit_type);
+ bool BitstreamConvertInitAVC(void *in_extradata, int in_extrasize);
+ bool BitstreamConvertInitHEVC(void *in_extradata, int in_extrasize);
+ bool BitstreamConvert(uint8_t* pData, int iSize, uint8_t **poutbuf, int *poutbuf_size);
+ static void BitstreamAllocAndCopy(uint8_t** poutbuf,
+ int* poutbuf_size,
+ const uint8_t* sps_pps,
+ uint32_t sps_pps_size,
+ const uint8_t* in,
+ uint32_t in_size,
+ uint8_t nal_type);
+
+ typedef struct omx_bitstream_ctx {
+ uint8_t length_size;
+ uint8_t first_idr;
+ uint8_t idr_sps_pps_seen;
+ uint8_t *sps_pps_data;
+ uint32_t size;
+ } omx_bitstream_ctx;
+
+ uint8_t *m_convertBuffer;
+ int m_convertSize;
+ uint8_t *m_inputBuffer;
+ int m_inputSize;
+
+ uint32_t m_sps_pps_size;
+ omx_bitstream_ctx m_sps_pps_context;
+ bool m_convert_bitstream;
+ bool m_to_annexb;
+
+ uint8_t *m_extradata;
+ int m_extrasize;
+ bool m_convert_3byteTo4byteNALSize;
+ bool m_convert_bytestream;
+ AVCodecID m_codec;
+ bool m_start_decode;
+};
diff --git a/xbmc/utils/BitstreamReader.cpp b/xbmc/utils/BitstreamReader.cpp
new file mode 100644
index 0000000..6f2a7ff
--- /dev/null
+++ b/xbmc/utils/BitstreamReader.cpp
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017-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 "BitstreamReader.h"
+
+CBitstreamReader::CBitstreamReader(const uint8_t *buf, int len)
+ : buffer(buf)
+ , start(buf)
+ , offbits(0)
+ , length(len)
+ , oflow(0)
+{
+}
+
+uint32_t CBitstreamReader::ReadBits(int nbits)
+{
+ uint32_t ret = GetBits(nbits);
+
+ offbits += nbits;
+ buffer += offbits / 8;
+ offbits %= 8;
+
+ return ret;
+}
+
+void CBitstreamReader::SkipBits(int nbits)
+{
+ offbits += nbits;
+ buffer += offbits / 8;
+ offbits %= 8;
+
+ if (buffer > (start + length))
+ oflow = 1;
+}
+
+uint32_t CBitstreamReader::GetBits(int nbits)
+{
+ int i, nbytes;
+ uint32_t ret = 0;
+ const uint8_t *buf;
+
+ buf = buffer;
+ nbytes = (offbits + nbits) / 8;
+
+ if (((offbits + nbits) % 8) > 0)
+ nbytes++;
+
+ if ((buf + nbytes) > (start + length))
+ {
+ oflow = 1;
+ return 0;
+ }
+ for (i = 0; i<nbytes; i++)
+ ret += buf[i] << ((nbytes - i - 1) * 8);
+
+ i = (4 - nbytes) * 8 + offbits;
+
+ ret = ((ret << i) >> i) >> ((nbytes * 8) - nbits - offbits);
+
+ return ret;
+}
+
+const uint8_t* find_start_code(const uint8_t *p, const uint8_t *end, uint32_t *state)
+{
+ if (p >= end)
+ return end;
+
+ for (int i = 0; i < 3; i++)
+ {
+ uint32_t tmp = *state << 8;
+ *state = tmp + *(p++);
+ if (tmp == 0x100 || p == end)
+ return p;
+ }
+
+ while (p < end)
+ {
+ if (p[-1] > 1) p += 3;
+ else if (p[-2]) p += 2;
+ else if (p[-3] | (p[-1] - 1)) p++;
+ else {
+ p++;
+ break;
+ }
+ }
+
+ p = (p < end)? p - 4 : end - 4;
+ *state = BS_RB32(p);
+
+ return p + 4;
+}
diff --git a/xbmc/utils/BitstreamReader.h b/xbmc/utils/BitstreamReader.h
new file mode 100644
index 0000000..89fa2b2
--- /dev/null
+++ b/xbmc/utils/BitstreamReader.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017-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 <stdint.h>
+
+class CBitstreamReader
+{
+public:
+ CBitstreamReader(const uint8_t *buf, int len);
+ uint32_t ReadBits(int nbits);
+ void SkipBits(int nbits);
+ uint32_t GetBits(int nbits);
+
+private:
+ const uint8_t *buffer, *start;
+ int offbits, length, oflow;
+};
+
+const uint8_t* find_start_code(const uint8_t *p, const uint8_t *end, uint32_t *state);
+
+////////////////////////////////////////////////////////////////////////////////////////////
+//! @todo refactor this so as not to need these ffmpeg routines.
+//! These are not exposed in ffmpeg's API so we dupe them here.
+
+/*
+ * AVC helper functions for muxers
+ * Copyright (c) 2006 Baptiste Coudurier <baptiste.coudurier@smartjog.com>
+ * This is part of FFmpeg
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+constexpr uint32_t BS_RB24(const uint8_t* x)
+{
+ return (x[0] << 16) | (x[1] << 8) | x[2];
+}
+
+constexpr uint32_t BS_RB32(const uint8_t* x)
+{
+ return (x[1] << 24) | (x[1] << 16) | (x[2] << 8) | x[3];
+}
+
diff --git a/xbmc/utils/BitstreamStats.cpp b/xbmc/utils/BitstreamStats.cpp
new file mode 100644
index 0000000..a35a757
--- /dev/null
+++ b/xbmc/utils/BitstreamStats.cpp
@@ -0,0 +1,70 @@
+/*
+ * 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 "BitstreamStats.h"
+
+#include "utils/TimeUtils.h"
+
+int64_t BitstreamStats::m_tmFreq;
+
+BitstreamStats::BitstreamStats(unsigned int nEstimatedBitrate)
+{
+ m_dBitrate = 0.0;
+ m_dMaxBitrate = 0.0;
+ m_dMinBitrate = -1.0;
+
+ m_nBitCount = 0;
+ m_nEstimatedBitrate = nEstimatedBitrate;
+ m_tmStart = 0LL;
+
+ if (m_tmFreq == 0LL)
+ m_tmFreq = CurrentHostFrequency();
+}
+
+void BitstreamStats::AddSampleBytes(unsigned int nBytes)
+{
+ AddSampleBits(nBytes*8);
+}
+
+void BitstreamStats::AddSampleBits(unsigned int nBits)
+{
+ m_nBitCount += nBits;
+ if (m_nBitCount >= m_nEstimatedBitrate)
+ CalculateBitrate();
+}
+
+void BitstreamStats::Start()
+{
+ m_nBitCount = 0;
+ m_tmStart = CurrentHostCounter();
+}
+
+void BitstreamStats::CalculateBitrate()
+{
+ int64_t tmNow;
+ tmNow = CurrentHostCounter();
+
+ double elapsed = (double)(tmNow - m_tmStart) / (double)m_tmFreq;
+ // only update once every 2 seconds
+ if (elapsed >= 2)
+ {
+ m_dBitrate = (double)m_nBitCount / elapsed;
+
+ if (m_dBitrate > m_dMaxBitrate)
+ m_dMaxBitrate = m_dBitrate;
+
+ if (m_dBitrate < m_dMinBitrate || m_dMinBitrate == -1)
+ m_dMinBitrate = m_dBitrate;
+
+ Start();
+ }
+}
+
+
+
+
diff --git a/xbmc/utils/BitstreamStats.h b/xbmc/utils/BitstreamStats.h
new file mode 100644
index 0000000..13413c6
--- /dev/null
+++ b/xbmc/utils/BitstreamStats.h
@@ -0,0 +1,40 @@
+/*
+ * 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 <stdint.h>
+
+class BitstreamStats final
+{
+public:
+ // in order not to cause a performance hit, we should only check the clock when
+ // we reach m_nEstimatedBitrate bits.
+ // if this value is 1, we will calculate bitrate on every sample.
+ explicit BitstreamStats(unsigned int nEstimatedBitrate=(10240*8) /*10Kbit*/);
+
+ void AddSampleBytes(unsigned int nBytes);
+ void AddSampleBits(unsigned int nBits);
+
+ inline double GetBitrate() const { return m_dBitrate; }
+ inline double GetMaxBitrate() const { return m_dMaxBitrate; }
+ inline double GetMinBitrate() const { return m_dMinBitrate; }
+
+ void Start();
+ void CalculateBitrate();
+
+private:
+ double m_dBitrate;
+ double m_dMaxBitrate;
+ double m_dMinBitrate;
+ unsigned int m_nBitCount;
+ unsigned int m_nEstimatedBitrate; // when we reach this amount of bits we check current bitrate.
+ int64_t m_tmStart;
+ static int64_t m_tmFreq;
+};
+
diff --git a/xbmc/utils/BitstreamWriter.cpp b/xbmc/utils/BitstreamWriter.cpp
new file mode 100644
index 0000000..43c0788
--- /dev/null
+++ b/xbmc/utils/BitstreamWriter.cpp
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2017-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 "BitstreamWriter.h"
+
+CBitstreamWriter::CBitstreamWriter(uint8_t *buffer, unsigned int buffer_size, int writer_le)
+ : writer_le(writer_le)
+ , bit_buf(0)
+ , bit_left(32)
+ , buf(buffer)
+ , buf_ptr(buf)
+{
+}
+
+void CBitstreamWriter::WriteBits(int n, unsigned int value)
+{
+ // Write up to 32 bits into a bitstream.
+ unsigned int bit_buf;
+ int bit_left;
+
+ if (n == 32)
+ {
+ // Write exactly 32 bits into a bitstream.
+ // danger, recursion in play.
+ int lo = value & 0xffff;
+ int hi = value >> 16;
+ if (writer_le)
+ {
+ WriteBits(16, lo);
+ WriteBits(16, hi);
+ }
+ else
+ {
+ WriteBits(16, hi);
+ WriteBits(16, lo);
+ }
+ return;
+ }
+
+ bit_buf = this->bit_buf;
+ bit_left = this->bit_left;
+
+ if (writer_le)
+ {
+ bit_buf |= value << (32 - bit_left);
+ if (n >= bit_left) {
+ BS_WL32(buf_ptr, bit_buf);
+ buf_ptr += 4;
+ bit_buf = (bit_left == 32) ? 0 : value >> bit_left;
+ bit_left += 32;
+ }
+ bit_left -= n;
+ }
+ else
+ {
+ if (n < bit_left) {
+ bit_buf = (bit_buf << n) | value;
+ bit_left -= n;
+ }
+ else {
+ bit_buf <<= bit_left;
+ bit_buf |= value >> (n - bit_left);
+ BS_WB32(buf_ptr, bit_buf);
+ buf_ptr += 4;
+ bit_left += 32 - n;
+ bit_buf = value;
+ }
+ }
+
+ this->bit_buf = bit_buf;
+ this->bit_left = bit_left;
+}
+
+void CBitstreamWriter::SkipBits(int n)
+{
+ // Skip the given number of bits.
+ // Must only be used if the actual values in the bitstream do not matter.
+ // If n is 0 the behavior is undefined.
+ bit_left -= n;
+ buf_ptr -= 4 * (bit_left >> 5);
+ bit_left &= 31;
+}
+
+void CBitstreamWriter::FlushBits()
+{
+ if (!writer_le)
+ {
+ if (bit_left < 32)
+ bit_buf <<= bit_left;
+ }
+ while (bit_left < 32)
+ {
+
+ if (writer_le)
+ {
+ *buf_ptr++ = bit_buf;
+ bit_buf >>= 8;
+ }
+ else
+ {
+ *buf_ptr++ = bit_buf >> 24;
+ bit_buf <<= 8;
+ }
+ bit_left += 8;
+ }
+ bit_left = 32;
+ bit_buf = 0;
+}
diff --git a/xbmc/utils/BitstreamWriter.h b/xbmc/utils/BitstreamWriter.h
new file mode 100644
index 0000000..91257b2
--- /dev/null
+++ b/xbmc/utils/BitstreamWriter.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017-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 <stdint.h>
+
+class CBitstreamWriter
+{
+public:
+ CBitstreamWriter(uint8_t *buffer, unsigned int buffer_size, int writer_le);
+ void WriteBits(int n, unsigned int value);
+ void SkipBits(int n);
+ void FlushBits();
+
+private:
+ int writer_le;
+ uint32_t bit_buf;
+ int bit_left;
+ uint8_t *buf, *buf_ptr;
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////
+//! @todo refactor this so as not to need these ffmpeg routines.
+//! These are not exposed in ffmpeg's API so we dupe them here.
+
+/*
+ * AVC helper functions for muxers
+ * Copyright (c) 2006 Baptiste Coudurier <baptiste.coudurier@smartjog.com>
+ * This is part of FFmpeg
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+#define BS_WB32(p, d) { \
+ ((uint8_t*)(p))[3] = (d); \
+ ((uint8_t*)(p))[2] = (d) >> 8; \
+ ((uint8_t*)(p))[1] = (d) >> 16; \
+ ((uint8_t*)(p))[0] = (d) >> 24; }
+
+#define BS_WL32(p, d) { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d) >> 8; \
+ ((uint8_t*)(p))[2] = (d) >> 16; \
+ ((uint8_t*)(p))[3] = (d) >> 24; }
diff --git a/xbmc/utils/BooleanLogic.cpp b/xbmc/utils/BooleanLogic.cpp
new file mode 100644
index 0000000..7ccc547
--- /dev/null
+++ b/xbmc/utils/BooleanLogic.cpp
@@ -0,0 +1,122 @@
+/*
+ * 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 "BooleanLogic.h"
+
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+bool CBooleanLogicValue::Deserialize(const TiXmlNode *node)
+{
+ if (node == NULL)
+ return false;
+
+ const TiXmlElement *elem = node->ToElement();
+ if (elem == NULL)
+ return false;
+
+ if (node->FirstChild() != NULL && node->FirstChild()->Type() == TiXmlNode::TINYXML_TEXT)
+ m_value = node->FirstChild()->ValueStr();
+
+ m_negated = false;
+ const char *strNegated = elem->Attribute("negated");
+ if (strNegated != NULL)
+ {
+ if (StringUtils::EqualsNoCase(strNegated, "true"))
+ m_negated = true;
+ else if (!StringUtils::EqualsNoCase(strNegated, "false"))
+ {
+ CLog::Log(LOGDEBUG, "CBooleanLogicValue: invalid negated value \"{}\"", strNegated);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool CBooleanLogicOperation::Deserialize(const TiXmlNode *node)
+{
+ if (node == NULL)
+ return false;
+
+ // check if this is a simple operation with a single value directly expressed
+ // in the parent tag
+ if (node->FirstChild() == NULL || node->FirstChild()->Type() == TiXmlNode::TINYXML_TEXT)
+ {
+ CBooleanLogicValuePtr value = CBooleanLogicValuePtr(newValue());
+ if (value == NULL || !value->Deserialize(node))
+ {
+ CLog::Log(LOGDEBUG, "CBooleanLogicOperation: failed to deserialize implicit boolean value definition");
+ return false;
+ }
+
+ m_values.push_back(value);
+ return true;
+ }
+
+ const TiXmlNode *operationNode = node->FirstChild();
+ while (operationNode != NULL)
+ {
+ std::string tag = operationNode->ValueStr();
+ if (StringUtils::EqualsNoCase(tag, "and") || StringUtils::EqualsNoCase(tag, "or"))
+ {
+ CBooleanLogicOperationPtr operation = CBooleanLogicOperationPtr(newOperation());
+ if (operation == NULL)
+ return false;
+
+ operation->SetOperation(StringUtils::EqualsNoCase(tag, "and") ? BooleanLogicOperationAnd : BooleanLogicOperationOr);
+ if (!operation->Deserialize(operationNode))
+ {
+ CLog::Log(LOGDEBUG, "CBooleanLogicOperation: failed to deserialize <{}> definition", tag);
+ return false;
+ }
+
+ m_operations.push_back(operation);
+ }
+ else
+ {
+ CBooleanLogicValuePtr value = CBooleanLogicValuePtr(newValue());
+ if (value == NULL)
+ return false;
+
+ if (StringUtils::EqualsNoCase(tag, value->GetTag()))
+ {
+ if (!value->Deserialize(operationNode))
+ {
+ CLog::Log(LOGDEBUG, "CBooleanLogicOperation: failed to deserialize <{}> definition", tag);
+ return false;
+ }
+
+ m_values.push_back(value);
+ }
+ else if (operationNode->Type() == TiXmlNode::TINYXML_ELEMENT)
+ CLog::Log(LOGDEBUG, "CBooleanLogicOperation: unknown <{}> definition encountered", tag);
+ }
+
+ operationNode = operationNode->NextSibling();
+ }
+
+ return true;
+}
+
+bool CBooleanLogic::Deserialize(const TiXmlNode *node)
+{
+ if (node == NULL)
+ return false;
+
+ if (m_operation == NULL)
+ {
+ m_operation = CBooleanLogicOperationPtr(new CBooleanLogicOperation());
+
+ if (m_operation == NULL)
+ return false;
+ }
+
+ return m_operation->Deserialize(node);
+}
diff --git a/xbmc/utils/BooleanLogic.h b/xbmc/utils/BooleanLogic.h
new file mode 100644
index 0000000..0355134
--- /dev/null
+++ b/xbmc/utils/BooleanLogic.h
@@ -0,0 +1,90 @@
+/*
+ * 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 "utils/IXmlDeserializable.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+typedef enum {
+ BooleanLogicOperationOr = 0,
+ BooleanLogicOperationAnd
+} BooleanLogicOperation;
+
+class CBooleanLogicValue : public IXmlDeserializable
+{
+public:
+ CBooleanLogicValue(const std::string &value = "", bool negated = false)
+ : m_value(value), m_negated(negated)
+ { }
+ ~CBooleanLogicValue() override = default;
+
+ bool Deserialize(const TiXmlNode *node) override;
+
+ virtual const std::string& GetValue() const { return m_value; }
+ virtual bool IsNegated() const { return m_negated; }
+ virtual const char* GetTag() const { return "value"; }
+
+ virtual void SetValue(const std::string &value) { m_value = value; }
+ virtual void SetNegated(bool negated) { m_negated = negated; }
+
+protected:
+ std::string m_value;
+ bool m_negated;
+};
+
+typedef std::shared_ptr<CBooleanLogicValue> CBooleanLogicValuePtr;
+typedef std::vector<CBooleanLogicValuePtr> CBooleanLogicValues;
+
+class CBooleanLogicOperation;
+typedef std::shared_ptr<CBooleanLogicOperation> CBooleanLogicOperationPtr;
+typedef std::vector<CBooleanLogicOperationPtr> CBooleanLogicOperations;
+
+class CBooleanLogicOperation : public IXmlDeserializable
+{
+public:
+ explicit CBooleanLogicOperation(BooleanLogicOperation op = BooleanLogicOperationAnd)
+ : m_operation(op)
+ { }
+ ~CBooleanLogicOperation() override = default;
+
+ bool Deserialize(const TiXmlNode *node) override;
+
+ virtual BooleanLogicOperation GetOperation() const { return m_operation; }
+ virtual const CBooleanLogicOperations& GetOperations() const { return m_operations; }
+ virtual const CBooleanLogicValues& GetValues() const { return m_values; }
+
+ virtual void SetOperation(BooleanLogicOperation op) { m_operation = op; }
+
+protected:
+ virtual CBooleanLogicOperation* newOperation() { return new CBooleanLogicOperation(); }
+ virtual CBooleanLogicValue* newValue() { return new CBooleanLogicValue(); }
+
+ BooleanLogicOperation m_operation;
+ CBooleanLogicOperations m_operations;
+ CBooleanLogicValues m_values;
+};
+
+class CBooleanLogic : public IXmlDeserializable
+{
+protected:
+ /* make sure nobody deletes a pointer to this class */
+ ~CBooleanLogic() override = default;
+
+public:
+ bool Deserialize(const TiXmlNode *node) override;
+
+ const CBooleanLogicOperationPtr& Get() const { return m_operation; }
+ CBooleanLogicOperationPtr Get() { return m_operation; }
+
+protected:
+ CBooleanLogicOperationPtr m_operation;
+};
diff --git a/xbmc/utils/BufferObject.cpp b/xbmc/utils/BufferObject.cpp
new file mode 100644
index 0000000..1bf338e
--- /dev/null
+++ b/xbmc/utils/BufferObject.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2005-2020 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 "BufferObject.h"
+
+#include "BufferObjectFactory.h"
+#include "utils/log.h"
+
+#if defined(HAVE_LINUX_DMA_BUF)
+#include <linux/dma-buf.h>
+#include <sys/ioctl.h>
+#endif
+
+std::unique_ptr<CBufferObject> CBufferObject::GetBufferObject(bool needsCreateBySize)
+{
+ return CBufferObjectFactory::CreateBufferObject(needsCreateBySize);
+}
+
+int CBufferObject::GetFd()
+{
+ return m_fd;
+}
+
+uint32_t CBufferObject::GetStride()
+{
+ return m_stride;
+}
+
+uint64_t CBufferObject::GetModifier()
+{
+ return 0; // linear
+}
+
+void CBufferObject::SyncStart()
+{
+#if defined(HAVE_LINUX_DMA_BUF)
+ struct dma_buf_sync sync;
+ sync.flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_RW;
+
+ int ret = ioctl(m_fd, DMA_BUF_IOCTL_SYNC, &sync);
+ if (ret < 0)
+ CLog::LogF(LOGERROR, "ioctl DMA_BUF_IOCTL_SYNC failed, ret={} errno={}", ret, strerror(errno));
+#endif
+}
+
+void CBufferObject::SyncEnd()
+{
+#if defined(HAVE_LINUX_DMA_BUF)
+ struct dma_buf_sync sync;
+ sync.flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_RW;
+
+ int ret = ioctl(m_fd, DMA_BUF_IOCTL_SYNC, &sync);
+ if (ret < 0)
+ CLog::LogF(LOGERROR, "ioctl DMA_BUF_IOCTL_SYNC failed, ret={} errno={}", ret, strerror(errno));
+#endif
+}
diff --git a/xbmc/utils/BufferObject.h b/xbmc/utils/BufferObject.h
new file mode 100644
index 0000000..4603d9b
--- /dev/null
+++ b/xbmc/utils/BufferObject.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2005-2020 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 "IBufferObject.h"
+
+#include <memory>
+#include <stdint.h>
+
+/**
+ * @brief base class for using the IBufferObject interface. Derived classes
+ * should be based on this class.
+ *
+ */
+class CBufferObject : public IBufferObject
+{
+public:
+ /**
+ * @brief Get a BufferObject from CBufferObjectFactory
+ *
+ * @return std::unique_ptr<CBufferObject>
+ */
+ static std::unique_ptr<CBufferObject> GetBufferObject(bool needsCreateBySize);
+
+ virtual bool CreateBufferObject(uint32_t format, uint32_t width, uint32_t height) override = 0;
+ bool CreateBufferObject(uint64_t size) override { return false; }
+
+ int GetFd() override;
+ uint32_t GetStride() override;
+ uint64_t GetModifier() override;
+
+ void SyncStart() override;
+ void SyncEnd() override;
+
+protected:
+ int m_fd{-1};
+ uint32_t m_stride{0};
+};
diff --git a/xbmc/utils/BufferObjectFactory.cpp b/xbmc/utils/BufferObjectFactory.cpp
new file mode 100644
index 0000000..13ada4b
--- /dev/null
+++ b/xbmc/utils/BufferObjectFactory.cpp
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2005-2020 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 "BufferObjectFactory.h"
+
+std::list<std::function<std::unique_ptr<CBufferObject>()>> CBufferObjectFactory::m_bufferObjects;
+
+std::unique_ptr<CBufferObject> CBufferObjectFactory::CreateBufferObject(bool needsCreateBySize)
+{
+ for (const auto& bufferObject : m_bufferObjects)
+ {
+ auto bo = bufferObject();
+
+ if (needsCreateBySize)
+ {
+ if (!bo->CreateBufferObject(1))
+ continue;
+
+ bo->DestroyBufferObject();
+ }
+
+ return bo;
+ }
+
+ return nullptr;
+}
+
+void CBufferObjectFactory::RegisterBufferObject(
+ const std::function<std::unique_ptr<CBufferObject>()>& createFunc)
+{
+ m_bufferObjects.emplace_front(createFunc);
+}
+
+void CBufferObjectFactory::ClearBufferObjects()
+{
+ m_bufferObjects.clear();
+}
diff --git a/xbmc/utils/BufferObjectFactory.h b/xbmc/utils/BufferObjectFactory.h
new file mode 100644
index 0000000..1420129
--- /dev/null
+++ b/xbmc/utils/BufferObjectFactory.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2005-2020 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 "BufferObject.h"
+
+#include <functional>
+#include <list>
+#include <memory>
+
+/**
+ * @brief Factory that provides CBufferObject registration and creation
+ *
+ */
+class CBufferObjectFactory
+{
+public:
+ /**
+ * @brief Create a CBufferObject from the registered BufferObject types.
+ * In the future this may include some criteria for selecting a specific
+ * CBufferObject derived type. Currently it returns the CBufferObject
+ * implementation that was registered last.
+ *
+ * @return std::unique_ptr<CBufferObject>
+ */
+ static std::unique_ptr<CBufferObject> CreateBufferObject(bool needsCreateBySize);
+
+ /**
+ * @brief Registers a CBufferObject class to class to the factory.
+ *
+ */
+ static void RegisterBufferObject(const std::function<std::unique_ptr<CBufferObject>()>&);
+
+ /**
+ * @brief Clears the list of registered CBufferObject types
+ *
+ */
+ static void ClearBufferObjects();
+
+protected:
+ static std::list<std::function<std::unique_ptr<CBufferObject>()>> m_bufferObjects;
+};
diff --git a/xbmc/utils/CMakeLists.txt b/xbmc/utils/CMakeLists.txt
new file mode 100644
index 0000000..8e13289
--- /dev/null
+++ b/xbmc/utils/CMakeLists.txt
@@ -0,0 +1,248 @@
+set(SOURCES ActorProtocol.cpp
+ AlarmClock.cpp
+ AliasShortcutUtils.cpp
+ Archive.cpp
+ Base64.cpp
+ BitstreamConverter.cpp
+ BitstreamReader.cpp
+ BitstreamStats.cpp
+ BitstreamWriter.cpp
+ BooleanLogic.cpp
+ CharArrayParser.cpp
+ CharsetConverter.cpp
+ CharsetDetection.cpp
+ ColorUtils.cpp
+ ContentUtils.cpp
+ CPUInfo.cpp
+ Crc32.cpp
+ CSSUtils.cpp
+ DatabaseUtils.cpp
+ Digest.cpp
+ DiscsUtils.cpp
+ EndianSwap.cpp
+ EmbeddedArt.cpp
+ ExecString.cpp
+ FileExtensionProvider.cpp
+ Fanart.cpp
+ FileOperationJob.cpp
+ FileUtils.cpp
+ FontUtils.cpp
+ GroupUtils.cpp
+ HTMLUtil.cpp
+ HttpHeader.cpp
+ HttpParser.cpp
+ HttpRangeUtils.cpp
+ HttpResponse.cpp
+ InfoLoader.cpp
+ JobManager.cpp
+ JSONVariantParser.cpp
+ JSONVariantWriter.cpp
+ LabelFormatter.cpp
+ LangCodeExpander.cpp
+ LegacyPathTranslation.cpp
+ Locale.cpp
+ log.cpp
+ Mime.cpp
+ MovingSpeed.cpp
+ Observer.cpp
+ POUtils.cpp
+ PlayerUtils.cpp
+ RecentlyAddedJob.cpp
+ RegExp.cpp
+ rfft.cpp
+ RingBuffer.cpp
+ RssManager.cpp
+ RssReader.cpp
+ ProgressJob.cpp
+ SaveFileStateJob.cpp
+ ScraperParser.cpp
+ ScraperUrl.cpp
+ Screenshot.cpp
+ SortUtils.cpp
+ Speed.cpp
+ StreamDetails.cpp
+ StreamUtils.cpp
+ StringUtils.cpp
+ StringValidation.cpp
+ SystemInfo.cpp
+ Temperature.cpp
+ TextSearch.cpp
+ TimeUtils.cpp
+ URIUtils.cpp
+ UrlOptions.cpp
+ Utf8Utils.cpp
+ Variant.cpp
+ VC1BitstreamParser.cpp
+ Vector.cpp
+ XBMCTinyXML.cpp
+ XMLUtils.cpp)
+
+set(HEADERS ActorProtocol.h
+ AlarmClock.h
+ AliasShortcutUtils.h
+ Archive.h
+ Base64.h
+ BitstreamConverter.h
+ BitstreamReader.h
+ BitstreamStats.h
+ BitstreamWriter.h
+ BooleanLogic.h
+ CharArrayParser.h
+ CharsetConverter.h
+ CharsetDetection.h
+ CPUInfo.h
+ ColorUtils.h
+ ComponentContainer.h
+ ContentUtils.h
+ Crc32.h
+ CSSUtils.h
+ DatabaseUtils.h
+ Digest.h
+ DiscsUtils.h
+ EndianSwap.h
+ EventStream.h
+ EventStreamDetail.h
+ ExecString.h
+ FileExtensionProvider.h
+ Fanart.h
+ FileOperationJob.h
+ FileUtils.h
+ FontUtils.h
+ Geometry.h
+ GlobalsHandling.h
+ GroupUtils.h
+ HDRCapabilities.h
+ HTMLUtil.h
+ HttpHeader.h
+ HttpParser.h
+ HttpRangeUtils.h
+ HttpResponse.h
+ IArchivable.h
+ IBufferObject.h
+ ILocalizer.h
+ InfoLoader.h
+ IPlatformLog.h
+ IRssObserver.h
+ IScreenshotSurface.h
+ ISerializable.h
+ ISortable.h
+ IXmlDeserializable.h
+ Job.h
+ JobManager.h
+ JSONVariantParser.h
+ JSONVariantWriter.h
+ LabelFormatter.h
+ LangCodeExpander.h
+ LegacyPathTranslation.h
+ Locale.h
+ log.h
+ logtypes.h
+ Map.h
+ MathUtils.h
+ MemUtils.h
+ Mime.h
+ MovingSpeed.h
+ Observer.h
+ params_check_macros.h
+ POUtils.h
+ PlayerUtils.h
+ ProgressJob.h
+ RecentlyAddedJob.h
+ RegExp.h
+ rfft.h
+ RingBuffer.h
+ RssManager.h
+ RssReader.h
+ SaveFileStateJob.h
+ ScopeGuard.h
+ ScraperParser.h
+ ScraperUrl.h
+ Screenshot.h
+ SortUtils.h
+ Speed.h
+ Stopwatch.h
+ StreamDetails.h
+ StreamUtils.h
+ StringUtils.h
+ StringValidation.h
+ SystemInfo.h
+ Temperature.h
+ TextSearch.h
+ TimeFormat.h
+ TimeUtils.h
+ TransformMatrix.h
+ URIUtils.h
+ UrlOptions.h
+ Utf8Utils.h
+ Variant.h
+ VC1BitstreamParser.h
+ Vector.h
+ XBMCTinyXML.h
+ XMLUtils.h
+ XTimeUtils.h)
+
+if(XSLT_FOUND)
+ list(APPEND SOURCES XSLTUtils.cpp)
+ list(APPEND HEADERS XSLTUtils.h)
+endif()
+if(EGL_FOUND)
+ list(APPEND SOURCES EGLUtils.cpp
+ EGLFence.cpp)
+ list(APPEND HEADERS EGLUtils.h
+ EGLFence.h)
+endif()
+
+# The large map trips the clang optimizer
+if(CMAKE_CXX_COMPILER_ID STREQUAL Clang)
+ set_source_files_properties(Mime.cpp PROPERTIES COMPILE_FLAGS -O0)
+endif()
+
+if(OPENGL_FOUND OR OPENGLES_FOUND)
+ list(APPEND SOURCES GLUtils.cpp)
+ list(APPEND HEADERS GLUtils.h)
+endif()
+
+if("gbm" IN_LIST CORE_PLATFORM_NAME_LC OR "wayland" IN_LIST CORE_PLATFORM_NAME_LC)
+ list(APPEND SOURCES BufferObject.cpp
+ BufferObjectFactory.cpp)
+ list(APPEND HEADERS BufferObject.h
+ BufferObjectFactory.h)
+
+ if("gbm" IN_LIST CORE_PLATFORM_NAME_LC)
+ list(APPEND SOURCES DumbBufferObject.cpp)
+ list(APPEND SOURCES DumbBufferObject.h)
+ endif()
+
+ if(HAVE_LINUX_MEMFD AND HAVE_LINUX_UDMABUF)
+ list(APPEND SOURCES UDMABufferObject.cpp)
+ list(APPEND HEADERS UDMABufferObject.h)
+ endif()
+
+ if(HAVE_LINUX_DMA_HEAP)
+ list(APPEND SOURCES DMAHeapBufferObject.cpp)
+ list(APPEND HEADERS DMAHeapBufferObject.h)
+ endif()
+
+ if(GBM_HAS_BO_MAP AND "gbm" IN_LIST CORE_PLATFORM_NAME_LC)
+ list(APPEND SOURCES GBMBufferObject.cpp)
+ list(APPEND HEADERS GBMBufferObject.h)
+ endif()
+
+ if(EGL_FOUND)
+ list(APPEND SOURCES EGLImage.cpp)
+ list(APPEND HEADERS EGLImage.h)
+ endif()
+
+ if(LIBDRM_FOUND)
+ list(APPEND SOURCES DRMHelpers.cpp)
+ list(APPEND HEADERS DRMHelpers.h)
+ endif()
+endif()
+
+core_add_library(utils)
+
+if(NOT CORE_SYSTEM_NAME STREQUAL windows AND NOT CORE_SYSTEM_NAME STREQUAL windowsstore)
+ if(HAVE_SSE2)
+ target_compile_options(${CORE_LIBRARY} PRIVATE -msse2)
+ endif()
+endif()
diff --git a/xbmc/utils/CPUInfo.cpp b/xbmc/utils/CPUInfo.cpp
new file mode 100644
index 0000000..9f43c10
--- /dev/null
+++ b/xbmc/utils/CPUInfo.cpp
@@ -0,0 +1,64 @@
+/*
+ * 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 "CPUInfo.h"
+
+#include "utils/StringUtils.h"
+
+bool CCPUInfo::HasCoreId(int coreId) const
+{
+ for (const auto& core : m_cores)
+ {
+ if (core.m_id == coreId)
+ return true;
+ }
+
+ return false;
+}
+
+const CoreInfo CCPUInfo::GetCoreInfo(int coreId)
+{
+ CoreInfo coreInfo;
+
+ for (auto& core : m_cores)
+ {
+ if (core.m_id == coreId)
+ coreInfo = core;
+ }
+
+ return coreInfo;
+}
+
+std::string CCPUInfo::GetCoresUsageString()
+{
+ std::string strCores;
+
+ if (SupportsCPUUsage())
+ {
+ GetUsedPercentage(); // must call it to recalculate pct values
+
+ if (!m_cores.empty())
+ {
+ for (const auto& core : m_cores)
+ {
+ if (!strCores.empty())
+ strCores += ' ';
+ if (core.m_usagePercent < 10.0)
+ strCores += StringUtils::Format("#{}: {:1.1f}%", core.m_id, core.m_usagePercent);
+ else
+ strCores += StringUtils::Format("#{}: {:3.0f}%", core.m_id, core.m_usagePercent);
+ }
+ }
+ else
+ {
+ strCores += StringUtils::Format("{:3.0f}%", static_cast<double>(m_lastUsedPercentage));
+ }
+ }
+
+ return strCores;
+}
diff --git a/xbmc/utils/CPUInfo.h b/xbmc/utils/CPUInfo.h
new file mode 100644
index 0000000..e11384e
--- /dev/null
+++ b/xbmc/utils/CPUInfo.h
@@ -0,0 +1,121 @@
+/*
+ * 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 "threads/SystemClock.h"
+#include "utils/Temperature.h"
+
+#include <chrono>
+#include <memory>
+#include <string>
+#include <vector>
+
+enum CpuFeature
+{
+ CPU_FEATURE_MMX = 1 << 0,
+ CPU_FEATURE_MMX2 = 1 << 1,
+ CPU_FEATURE_SSE = 1 << 2,
+ CPU_FEATURE_SSE2 = 1 << 3,
+ CPU_FEATURE_SSE3 = 1 << 4,
+ CPU_FEATURE_SSSE3 = 1 << 5,
+ CPU_FEATURE_SSE4 = 1 << 6,
+ CPU_FEATURE_SSE42 = 1 << 7,
+ CPU_FEATURE_3DNOW = 1 << 8,
+ CPU_FEATURE_3DNOWEXT = 1 << 9,
+ CPU_FEATURE_ALTIVEC = 1 << 10,
+ CPU_FEATURE_NEON = 1 << 11,
+};
+
+struct CoreInfo
+{
+ int m_id = 0;
+ double m_usagePercent = 0.0;
+ std::size_t m_activeTime = 0;
+ std::size_t m_idleTime = 0;
+ std::size_t m_totalTime = 0;
+};
+
+class CCPUInfo
+{
+public:
+ // Defines to help with calls to CPUID
+ const unsigned int CPUID_INFOTYPE_MANUFACTURER = 0x00000000;
+ const unsigned int CPUID_INFOTYPE_STANDARD = 0x00000001;
+ const unsigned int CPUID_INFOTYPE_EXTENDED_IMPLEMENTED = 0x80000000;
+ const unsigned int CPUID_INFOTYPE_EXTENDED = 0x80000001;
+ const unsigned int CPUID_INFOTYPE_PROCESSOR_1 = 0x80000002;
+ const unsigned int CPUID_INFOTYPE_PROCESSOR_2 = 0x80000003;
+ const unsigned int CPUID_INFOTYPE_PROCESSOR_3 = 0x80000004;
+
+ // Standard Features
+ // Bitmasks for the values returned by a call to cpuid with eax=0x00000001
+ const unsigned int CPUID_00000001_ECX_SSE3 = (1 << 0);
+ const unsigned int CPUID_00000001_ECX_SSSE3 = (1 << 9);
+ const unsigned int CPUID_00000001_ECX_SSE4 = (1 << 19);
+ const unsigned int CPUID_00000001_ECX_SSE42 = (1 << 20);
+
+ const unsigned int CPUID_00000001_EDX_MMX = (1 << 23);
+ const unsigned int CPUID_00000001_EDX_SSE = (1 << 25);
+ const unsigned int CPUID_00000001_EDX_SSE2 = (1 << 26);
+
+ // Extended Features
+ // Bitmasks for the values returned by a call to cpuid with eax=0x80000001
+ const unsigned int CPUID_80000001_EDX_MMX2 = (1 << 22);
+ const unsigned int CPUID_80000001_EDX_MMX = (1 << 23);
+ const unsigned int CPUID_80000001_EDX_3DNOWEXT = (1 << 30);
+ const unsigned int CPUID_80000001_EDX_3DNOW = (1U << 31);
+
+ // In milliseconds
+ const std::chrono::milliseconds MINIMUM_TIME_BETWEEN_READS{500};
+
+ static std::shared_ptr<CCPUInfo> GetCPUInfo();
+
+ virtual bool SupportsCPUUsage() const { return true; }
+
+ virtual int GetUsedPercentage() = 0;
+ virtual float GetCPUFrequency() = 0;
+ virtual bool GetTemperature(CTemperature& temperature) = 0;
+
+ bool HasCoreId(int coreId) const;
+ const CoreInfo GetCoreInfo(int coreId);
+ std::string GetCoresUsageString();
+
+ unsigned int GetCPUFeatures() const { return m_cpuFeatures; }
+ int GetCPUCount() const { return m_cpuCount; }
+ std::string GetCPUModel() { return m_cpuModel; }
+ std::string GetCPUBogoMips() { return m_cpuBogoMips; }
+ std::string GetCPUSoC() { return m_cpuSoC; }
+ std::string GetCPUHardware() { return m_cpuHardware; }
+ std::string GetCPURevision() { return m_cpuRevision; }
+ std::string GetCPUSerial() { return m_cpuSerial; }
+
+protected:
+ CCPUInfo() = default;
+ virtual ~CCPUInfo() = default;
+
+ int m_lastUsedPercentage;
+ XbmcThreads::EndTime<> m_nextUsedReadTime;
+ std::string m_cpuVendor;
+ std::string m_cpuModel;
+ std::string m_cpuBogoMips;
+ std::string m_cpuSoC;
+ std::string m_cpuHardware;
+ std::string m_cpuRevision;
+ std::string m_cpuSerial;
+
+ double m_usagePercent{0.0};
+ std::size_t m_activeTime{0};
+ std::size_t m_idleTime{0};
+ std::size_t m_totalTime{0};
+
+ int m_cpuCount;
+ unsigned int m_cpuFeatures;
+
+ std::vector<CoreInfo> m_cores;
+};
diff --git a/xbmc/utils/CSSUtils.cpp b/xbmc/utils/CSSUtils.cpp
new file mode 100644
index 0000000..329d70c
--- /dev/null
+++ b/xbmc/utils/CSSUtils.cpp
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2005-2021 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 "CSSUtils.h"
+
+#include <cstdint>
+#include <string>
+
+namespace
+{
+// https://www.w3.org/TR/css-syntax-3/#hex-digit
+bool isHexDigit(char c)
+{
+ return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
+}
+
+// https://www.w3.org/TR/css-syntax-3/#hex-digit
+uint32_t convertHexDigit(char c)
+{
+ if (c >= '0' && c <= '9')
+ {
+ return c - '0';
+ }
+ else if (c >= 'A' && c <= 'F')
+ {
+ return 10 + c - 'A';
+ }
+ else
+ {
+ return 10 + c - 'a';
+ }
+}
+
+// https://infra.spec.whatwg.org/#surrogate
+bool isSurrogateCodePoint(uint32_t c)
+{
+ return c >= 0xD800 && c <= 0xDFFF;
+}
+
+// https://www.w3.org/TR/css-syntax-3/#maximum-allowed-code-point
+bool isGreaterThanMaximumAllowedCodePoint(uint32_t c)
+{
+ return c > 0x10FFFF;
+}
+
+// https://www.w3.org/TR/css-syntax-3/#consume-escaped-code-point
+std::string escapeStringChunk(std::string& str, size_t& pos)
+{
+ if (str.size() < pos + 1)
+ return "";
+
+ uint32_t codePoint = convertHexDigit(str[pos + 1]);
+
+ if (str.size() >= pos + 2)
+ pos += 2;
+ else
+ return "";
+
+ int numDigits = 1;
+ while (numDigits < 6 && isHexDigit(str[pos]))
+ {
+ codePoint = 16 * codePoint + convertHexDigit(str[pos]);
+ if (str.size() >= pos + 1)
+ {
+ pos += 1;
+ numDigits += 1;
+ }
+ else
+ break;
+ }
+
+ std::string result;
+
+ // Convert code point to UTF-8 bytes
+ if (codePoint == 0 || isSurrogateCodePoint(codePoint) ||
+ isGreaterThanMaximumAllowedCodePoint(codePoint))
+ {
+ result += u8"\uFFFD";
+ }
+ else if (codePoint < 0x80)
+ {
+ // 1-byte UTF-8: 0xxxxxxx
+ result += static_cast<char>(codePoint);
+ }
+ else if (codePoint < 0x800)
+ {
+ // 2-byte UTF-8: 110xxxxx 10xxxxxx
+ uint32_t x1 = codePoint >> 6; // 6 = num of x's in 2nd byte
+ uint32_t x2 = codePoint - (x1 << 6); // 6 = num of x's in 2nd byte
+ uint32_t b1 = (6 << 5) + x1; // 6 = 0b110 ; 5 = num of x's in 1st byte
+ uint32_t b2 = (2 << 6) + x2; // 2 = 0b10 ; 6 = num of x's in 2nd byte
+ result += static_cast<char>(b1);
+ result += static_cast<char>(b2);
+ }
+ else if (codePoint < 0x10000)
+ {
+ // 3-byte UTF-8: 1110xxxx 10xxxxxx 10xxxxxx
+ uint32_t y1 = codePoint >> 6;
+ uint32_t x3 = codePoint - (y1 << 6);
+ uint32_t x1 = y1 >> 6;
+ uint32_t x2 = y1 - (x1 << 6);
+ uint32_t b1 = (14 << 4) + x1;
+ uint32_t b2 = (2 << 6) + x2;
+ uint32_t b3 = (2 << 6) + x3;
+ result += static_cast<char>(b1);
+ result += static_cast<char>(b2);
+ result += static_cast<char>(b3);
+ }
+ else
+ {
+ // 4-byte UTF-8: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ uint32_t y2 = codePoint >> 6;
+ uint32_t x4 = codePoint - (y2 << 6);
+ uint32_t y1 = y2 >> 6;
+ uint32_t x3 = y2 - (y1 << 6);
+ uint32_t x1 = y1 >> 6;
+ uint32_t x2 = y1 - (x1 << 6);
+ uint32_t b1 = (30 << 3) + x1;
+ uint32_t b2 = (2 << 6) + x2;
+ uint32_t b3 = (2 << 6) + x3;
+ uint32_t b4 = (2 << 6) + x4;
+ result += static_cast<char>(b1);
+ result += static_cast<char>(b2);
+ result += static_cast<char>(b3);
+ result += static_cast<char>(b4);
+ }
+
+ return result;
+}
+
+} // unnamed namespace
+
+void UTILS::CSS::Escape(std::string& str)
+{
+ std::string result;
+
+ for (size_t pos = 0; pos < str.size(); pos++)
+ {
+ if (str[pos] == '\\')
+ result += escapeStringChunk(str, pos);
+ else
+ result += str[pos];
+ }
+
+ str = result;
+}
diff --git a/xbmc/utils/CSSUtils.h b/xbmc/utils/CSSUtils.h
new file mode 100644
index 0000000..9d610bb
--- /dev/null
+++ b/xbmc/utils/CSSUtils.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2005-2021 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 <string>
+
+namespace UTILS
+{
+namespace CSS
+{
+
+/*! \brief Escape a CSS string
+ * \param str the string to be escaped
+ */
+void Escape(std::string& str);
+
+} // namespace CSS
+} // namespace UTILS
diff --git a/xbmc/utils/CharArrayParser.cpp b/xbmc/utils/CharArrayParser.cpp
new file mode 100644
index 0000000..5aeec20
--- /dev/null
+++ b/xbmc/utils/CharArrayParser.cpp
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2005-2021 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 "CharArrayParser.h"
+
+#include "utils/log.h"
+
+#include <cstdint>
+#include <cstring>
+
+void CCharArrayParser::Reset()
+{
+ m_limit = 0;
+ m_position = 0;
+}
+
+void CCharArrayParser::Reset(const char* data, int limit)
+{
+ m_data = data;
+ m_limit = limit;
+ m_position = 0;
+}
+
+int CCharArrayParser::CharsLeft()
+{
+ return m_limit - m_position;
+}
+
+int CCharArrayParser::GetPosition()
+{
+ return m_position;
+}
+
+bool CCharArrayParser::SetPosition(int position)
+{
+ if (position >= 0 && position <= m_limit)
+ m_position = position;
+ else
+ {
+ CLog::Log(LOGERROR, "{} - Position out of range", __FUNCTION__);
+ return false;
+ }
+ return true;
+}
+
+bool CCharArrayParser::SkipChars(int nChars)
+{
+ return SetPosition(m_position + nChars);
+}
+
+uint8_t CCharArrayParser::ReadNextUnsignedChar()
+{
+ m_position++;
+ if (!m_data)
+ {
+ CLog::Log(LOGERROR, "{} - No data to read", __FUNCTION__);
+ return 0;
+ }
+ if (m_position > m_limit)
+ CLog::Log(LOGERROR, "{} - Position out of range", __FUNCTION__);
+ return static_cast<uint8_t>(m_data[m_position - 1]) & 0xFF;
+}
+
+uint16_t CCharArrayParser::ReadNextUnsignedShort()
+{
+ if (!m_data)
+ {
+ CLog::Log(LOGERROR, "{} - No data to read", __FUNCTION__);
+ return 0;
+ }
+ m_position += 2;
+ if (m_position > m_limit)
+ CLog::Log(LOGERROR, "{} - Position out of range", __FUNCTION__);
+ return (static_cast<uint16_t>(m_data[m_position - 2]) & 0xFF) << 8 |
+ (static_cast<uint16_t>(m_data[m_position - 1]) & 0xFF);
+}
+
+uint32_t CCharArrayParser::ReadNextUnsignedInt()
+{
+ if (!m_data)
+ {
+ CLog::Log(LOGERROR, "{} - No data to read", __FUNCTION__);
+ return 0;
+ }
+ m_position += 4;
+ if (m_position > m_limit)
+ CLog::Log(LOGERROR, "{} - Position out of range", __FUNCTION__);
+ return (static_cast<uint32_t>(m_data[m_position - 4]) & 0xFF) << 24 |
+ (static_cast<uint32_t>(m_data[m_position - 3]) & 0xFF) << 16 |
+ (static_cast<uint32_t>(m_data[m_position - 2]) & 0xFF) << 8 |
+ (static_cast<uint32_t>(m_data[m_position - 1]) & 0xFF);
+}
+
+std::string CCharArrayParser::ReadNextString(int length)
+{
+ if (!m_data)
+ {
+ CLog::Log(LOGERROR, "{} - No data to read", __FUNCTION__);
+ return "";
+ }
+ std::string str(m_data + m_position, length);
+ m_position += length;
+ if (m_position > m_limit)
+ CLog::Log(LOGERROR, "{} - Position out of range", __FUNCTION__);
+ return str;
+}
+
+bool CCharArrayParser::ReadNextArray(int length, char* data)
+{
+ if (!m_data)
+ {
+ CLog::Log(LOGERROR, "{} - No data to read", __FUNCTION__);
+ return false;
+ }
+ if (m_position + length > m_limit)
+ {
+ CLog::Log(LOGERROR, "{} - Position out of range", __FUNCTION__);
+ return false;
+ }
+ std::strncpy(data, m_data + m_position, length);
+ data[length] = '\0';
+ return true;
+}
+
+bool CCharArrayParser::ReadNextLine(std::string& line)
+{
+ if (!m_data)
+ {
+ CLog::Log(LOGERROR, "{} - No data to read", __FUNCTION__);
+ return false;
+ }
+ if (CharsLeft() == 0)
+ {
+ line.clear();
+ return false;
+ }
+
+ int lineLimit = m_position;
+ while (lineLimit < m_limit && !(m_data[lineLimit] == '\n' || m_data[lineLimit] == '\r'))
+ {
+ lineLimit++;
+ }
+
+ if (lineLimit - m_position >= 3 && m_data[m_position] == '\xEF' &&
+ m_data[m_position + 1] == '\xBB' && m_data[m_position + 2] == '\xBF')
+ {
+ // There's a UTF-8 byte order mark at the start of the line. Discard it.
+ m_position += 3;
+ }
+
+ line.assign(m_data + m_position, lineLimit - m_position);
+ m_position = lineLimit;
+
+ if (m_data[m_position] == '\r')
+ {
+ m_position++;
+ }
+ if (m_data[m_position] == '\n')
+ {
+ m_position++;
+ }
+
+ return true;
+}
diff --git a/xbmc/utils/CharArrayParser.h b/xbmc/utils/CharArrayParser.h
new file mode 100644
index 0000000..3430946
--- /dev/null
+++ b/xbmc/utils/CharArrayParser.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2005-2021 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 <cstdint>
+#include <string>
+
+/*!
+ *\brief Wraps a char array, providing a set of methods for parsing data from it.
+ */
+class CCharArrayParser
+{
+public:
+ CCharArrayParser() = default;
+ ~CCharArrayParser() = default;
+
+ /*!
+ * \brief Sets the position and limit to zero
+ */
+ void Reset();
+
+ /*!
+ * \brief Updates the instance to wrap the specified data and resets the position to zero
+ * \param data The data
+ * \param limit The limit of length of the data
+ */
+ void Reset(const char* data, int limit);
+
+ /*!
+ * \brief Return the number of chars yet to be read
+ */
+ int CharsLeft();
+
+ /*!
+ * \brief Returns the current offset in the array
+ */
+ int GetPosition();
+
+ /*!
+ * \brief Set the reading offset in the array
+ * \param position The new offset position
+ * \return True if success, otherwise false
+ */
+ bool SetPosition(int position);
+
+ /*!
+ * \brief Skip a specified number of chars
+ * \param nChars The number of chars
+ * \return True if success, otherwise false
+ */
+ bool SkipChars(int nChars);
+
+ /*!
+ * \brief Reads the next unsigned char (it is assumed that the caller has
+ * already checked the availability of the data for its length)
+ * \return The unsigned char value
+ */
+ uint8_t ReadNextUnsignedChar();
+
+ /*!
+ * \brief Reads the next two chars as unsigned short value (it is assumed
+ * that the caller has already checked the availability of the data for its length)
+ * \return The unsigned short value
+ */
+ uint16_t ReadNextUnsignedShort();
+
+ /*!
+ * \brief Reads the next four chars as unsigned int value (it is assumed
+ * that the caller has already checked the availability of the data for its length)
+ * \return The unsigned int value
+ */
+ uint32_t ReadNextUnsignedInt();
+
+ /*!
+ * \brief Reads the next string of specified length (it is assumed that
+ * the caller has already checked the availability of the data for its length)
+ * \param length The length to be read
+ * \return The string value
+ */
+ std::string ReadNextString(int length);
+
+ /*!
+ * \brief Reads the next chars array of specified length (it is assumed that
+ * the caller has already checked the availability of the data for its length)
+ * \param length The length to be read
+ * \param data[OUT] The data read
+ * \return True if success, otherwise false
+ */
+ bool ReadNextArray(int length, char* data);
+
+ /*!
+ * \brief Reads a line of text.
+ * A line is considered to be terminated by any one of a carriage return ('\\r'),
+ * a line feed ('\\n'), or a carriage return followed by a line feed ('\\r\\n'),
+ * this method discards leading UTF-8 byte order marks, if present.
+ * \param line [OUT] The line read without line-termination characters
+ * \return True if read, otherwise false if the end of the data has already
+ * been reached
+ */
+ bool ReadNextLine(std::string& line);
+
+ /*!
+ * \brief Get the current data
+ * \return The char pointer to the current data
+ */
+ const char* GetData() { return m_data; };
+
+private:
+ const char* m_data{nullptr};
+ int m_position{0};
+ int m_limit{0};
+};
diff --git a/xbmc/utils/CharsetConverter.cpp b/xbmc/utils/CharsetConverter.cpp
new file mode 100644
index 0000000..89976ee
--- /dev/null
+++ b/xbmc/utils/CharsetConverter.cpp
@@ -0,0 +1,910 @@
+/*
+ * 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 "CharsetConverter.h"
+
+#include "LangInfo.h"
+#include "guilib/LocalizeStrings.h"
+#include "log.h"
+#include "settings/Settings.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "utils/StringUtils.h"
+#include "utils/Utf8Utils.h"
+
+#include <algorithm>
+#include <mutex>
+
+#include <fribidi.h>
+#include <iconv.h>
+
+#ifdef WORDS_BIGENDIAN
+ #define ENDIAN_SUFFIX "BE"
+#else
+ #define ENDIAN_SUFFIX "LE"
+#endif
+
+#if defined(TARGET_DARWIN)
+ #define WCHAR_IS_UCS_4 1
+ #define UTF16_CHARSET "UTF-16" ENDIAN_SUFFIX
+ #define UTF32_CHARSET "UTF-32" ENDIAN_SUFFIX
+ #define UTF8_SOURCE "UTF-8-MAC"
+ #define WCHAR_CHARSET UTF32_CHARSET
+#elif defined(TARGET_WINDOWS)
+ #define WCHAR_IS_UTF16 1
+ #define UTF16_CHARSET "UTF-16" ENDIAN_SUFFIX
+ #define UTF32_CHARSET "UTF-32" ENDIAN_SUFFIX
+ #define UTF8_SOURCE "UTF-8"
+ #define WCHAR_CHARSET UTF16_CHARSET
+#elif defined(TARGET_FREEBSD)
+ #define WCHAR_IS_UCS_4 1
+ #define UTF16_CHARSET "UTF-16" ENDIAN_SUFFIX
+ #define UTF32_CHARSET "UTF-32" ENDIAN_SUFFIX
+ #define UTF8_SOURCE "UTF-8"
+ #define WCHAR_CHARSET UTF32_CHARSET
+#elif defined(TARGET_ANDROID)
+ #define WCHAR_IS_UCS_4 1
+ #define UTF16_CHARSET "UTF-16" ENDIAN_SUFFIX
+ #define UTF32_CHARSET "UTF-32" ENDIAN_SUFFIX
+ #define UTF8_SOURCE "UTF-8"
+ #define WCHAR_CHARSET UTF32_CHARSET
+#else
+ #define UTF16_CHARSET "UTF-16" ENDIAN_SUFFIX
+ #define UTF32_CHARSET "UTF-32" ENDIAN_SUFFIX
+ #define UTF8_SOURCE "UTF-8"
+ #define WCHAR_CHARSET "WCHAR_T"
+ #if __STDC_ISO_10646__
+ #ifdef SIZEOF_WCHAR_T
+ #if SIZEOF_WCHAR_T == 4
+ #define WCHAR_IS_UCS_4 1
+ #elif SIZEOF_WCHAR_T == 2
+ #define WCHAR_IS_UCS_2 1
+ #endif
+ #endif
+ #endif
+#endif
+
+#define NO_ICONV ((iconv_t)-1)
+
+enum SpecialCharset
+{
+ NotSpecialCharset = 0,
+ SystemCharset,
+ UserCharset /* locale.charset */,
+ SubtitleCharset /* subtitles.charset */,
+};
+
+class CConverterType : public CCriticalSection
+{
+public:
+ CConverterType(const std::string& sourceCharset, const std::string& targetCharset, unsigned int targetSingleCharMaxLen = 1);
+ CConverterType(enum SpecialCharset sourceSpecialCharset, const std::string& targetCharset, unsigned int targetSingleCharMaxLen = 1);
+ CConverterType(const std::string& sourceCharset, enum SpecialCharset targetSpecialCharset, unsigned int targetSingleCharMaxLen = 1);
+ CConverterType(enum SpecialCharset sourceSpecialCharset, enum SpecialCharset targetSpecialCharset, unsigned int targetSingleCharMaxLen = 1);
+ CConverterType(const CConverterType& other);
+ ~CConverterType();
+
+ iconv_t GetConverter(std::unique_lock<CCriticalSection>& converterLock);
+
+ void Reset(void);
+ void ReinitTo(const std::string& sourceCharset, const std::string& targetCharset, unsigned int targetSingleCharMaxLen = 1);
+ std::string GetSourceCharset(void) const { return m_sourceCharset; }
+ std::string GetTargetCharset(void) const { return m_targetCharset; }
+ unsigned int GetTargetSingleCharMaxLen(void) const { return m_targetSingleCharMaxLen; }
+
+private:
+ static std::string ResolveSpecialCharset(enum SpecialCharset charset);
+
+ enum SpecialCharset m_sourceSpecialCharset;
+ std::string m_sourceCharset;
+ enum SpecialCharset m_targetSpecialCharset;
+ std::string m_targetCharset;
+ iconv_t m_iconv;
+ unsigned int m_targetSingleCharMaxLen;
+};
+
+CConverterType::CConverterType(const std::string& sourceCharset, const std::string& targetCharset, unsigned int targetSingleCharMaxLen /*= 1*/) : CCriticalSection(),
+ m_sourceSpecialCharset(NotSpecialCharset),
+ m_sourceCharset(sourceCharset),
+ m_targetSpecialCharset(NotSpecialCharset),
+ m_targetCharset(targetCharset),
+ m_iconv(NO_ICONV),
+ m_targetSingleCharMaxLen(targetSingleCharMaxLen)
+{
+}
+
+CConverterType::CConverterType(enum SpecialCharset sourceSpecialCharset, const std::string& targetCharset, unsigned int targetSingleCharMaxLen /*= 1*/) : CCriticalSection(),
+ m_sourceSpecialCharset(sourceSpecialCharset),
+ m_sourceCharset(),
+ m_targetSpecialCharset(NotSpecialCharset),
+ m_targetCharset(targetCharset),
+ m_iconv(NO_ICONV),
+ m_targetSingleCharMaxLen(targetSingleCharMaxLen)
+{
+}
+
+CConverterType::CConverterType(const std::string& sourceCharset, enum SpecialCharset targetSpecialCharset, unsigned int targetSingleCharMaxLen /*= 1*/) : CCriticalSection(),
+ m_sourceSpecialCharset(NotSpecialCharset),
+ m_sourceCharset(sourceCharset),
+ m_targetSpecialCharset(targetSpecialCharset),
+ m_targetCharset(),
+ m_iconv(NO_ICONV),
+ m_targetSingleCharMaxLen(targetSingleCharMaxLen)
+{
+}
+
+CConverterType::CConverterType(enum SpecialCharset sourceSpecialCharset, enum SpecialCharset targetSpecialCharset, unsigned int targetSingleCharMaxLen /*= 1*/) : CCriticalSection(),
+ m_sourceSpecialCharset(sourceSpecialCharset),
+ m_sourceCharset(),
+ m_targetSpecialCharset(targetSpecialCharset),
+ m_targetCharset(),
+ m_iconv(NO_ICONV),
+ m_targetSingleCharMaxLen(targetSingleCharMaxLen)
+{
+}
+
+CConverterType::CConverterType(const CConverterType& other) : CCriticalSection(),
+ m_sourceSpecialCharset(other.m_sourceSpecialCharset),
+ m_sourceCharset(other.m_sourceCharset),
+ m_targetSpecialCharset(other.m_targetSpecialCharset),
+ m_targetCharset(other.m_targetCharset),
+ m_iconv(NO_ICONV),
+ m_targetSingleCharMaxLen(other.m_targetSingleCharMaxLen)
+{
+}
+
+CConverterType::~CConverterType()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ if (m_iconv != NO_ICONV)
+ iconv_close(m_iconv);
+ lock.unlock(); // ensure unlocking before final destruction
+}
+
+iconv_t CConverterType::GetConverter(std::unique_lock<CCriticalSection>& converterLock)
+{
+ // ensure that this unique instance is locked externally
+ if (converterLock.mutex() != this)
+ return NO_ICONV;
+
+ if (m_iconv == NO_ICONV)
+ {
+ if (m_sourceSpecialCharset)
+ m_sourceCharset = ResolveSpecialCharset(m_sourceSpecialCharset);
+ if (m_targetSpecialCharset)
+ m_targetCharset = ResolveSpecialCharset(m_targetSpecialCharset);
+
+ m_iconv = iconv_open(m_targetCharset.c_str(), m_sourceCharset.c_str());
+
+ if (m_iconv == NO_ICONV)
+ CLog::Log(LOGERROR, "{}: iconv_open() for \"{}\" -> \"{}\" failed, errno = {} ({})",
+ __FUNCTION__, m_sourceCharset, m_targetCharset, errno, strerror(errno));
+ }
+
+ return m_iconv;
+}
+
+void CConverterType::Reset(void)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ if (m_iconv != NO_ICONV)
+ {
+ iconv_close(m_iconv);
+ m_iconv = NO_ICONV;
+ }
+
+ if (m_sourceSpecialCharset)
+ m_sourceCharset.clear();
+ if (m_targetSpecialCharset)
+ m_targetCharset.clear();
+
+}
+
+void CConverterType::ReinitTo(const std::string& sourceCharset, const std::string& targetCharset, unsigned int targetSingleCharMaxLen /*= 1*/)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ if (sourceCharset != m_sourceCharset || targetCharset != m_targetCharset)
+ {
+ if (m_iconv != NO_ICONV)
+ {
+ iconv_close(m_iconv);
+ m_iconv = NO_ICONV;
+ }
+
+ m_sourceSpecialCharset = NotSpecialCharset;
+ m_sourceCharset = sourceCharset;
+ m_targetSpecialCharset = NotSpecialCharset;
+ m_targetCharset = targetCharset;
+ m_targetSingleCharMaxLen = targetSingleCharMaxLen;
+ }
+}
+
+std::string CConverterType::ResolveSpecialCharset(enum SpecialCharset charset)
+{
+ switch (charset)
+ {
+ case SystemCharset:
+ return "";
+ case UserCharset:
+ return g_langInfo.GetGuiCharSet();
+ case SubtitleCharset:
+ return g_langInfo.GetSubtitleCharSet();
+ case NotSpecialCharset:
+ default:
+ return "UTF-8"; /* dummy value */
+ }
+}
+
+enum StdConversionType /* Keep it in sync with CCharsetConverter::CInnerConverter::m_stdConversion */
+{
+ NoConversion = -1,
+ Utf8ToUtf32 = 0,
+ Utf32ToUtf8,
+ Utf32ToW,
+ WToUtf32,
+ SubtitleCharsetToUtf8,
+ Utf8ToUserCharset,
+ UserCharsetToUtf8,
+ Utf32ToUserCharset,
+ WtoUtf8,
+ Utf16LEtoW,
+ Utf16BEtoUtf8,
+ Utf16LEtoUtf8,
+ Utf8toW,
+ Utf8ToSystem,
+ SystemToUtf8,
+ Ucs2CharsetToUtf8,
+ MacintoshToUtf8,
+ NumberOfStdConversionTypes /* Dummy sentinel entry */
+};
+
+/* We don't want to pollute header file with many additional includes and definitions, so put
+ here all staff that require usage of types defined in this file or in additional headers */
+class CCharsetConverter::CInnerConverter
+{
+public:
+ static bool logicalToVisualBiDi(const std::u32string& stringSrc,
+ std::u32string& stringDst,
+ FriBidiCharType base = FRIBIDI_TYPE_LTR,
+ const bool failOnBadString = false,
+ int* visualToLogicalMap = nullptr);
+ static bool isBidiDirectionRTL(const std::string& stringSrc);
+
+ template<class INPUT,class OUTPUT>
+ static bool stdConvert(StdConversionType convertType, const INPUT& strSource, OUTPUT& strDest, bool failOnInvalidChar = false);
+ template<class INPUT,class OUTPUT>
+ static bool customConvert(const std::string& sourceCharset, const std::string& targetCharset, const INPUT& strSource, OUTPUT& strDest, bool failOnInvalidChar = false);
+
+ template<class INPUT,class OUTPUT>
+ static bool convert(iconv_t type, int multiplier, const INPUT& strSource, OUTPUT& strDest, bool failOnInvalidChar = false);
+
+ static CConverterType m_stdConversion[NumberOfStdConversionTypes];
+ static CCriticalSection m_critSectionFriBiDi;
+};
+
+/* single symbol sizes in chars */
+const int CCharsetConverter::m_Utf8CharMinSize = 1;
+const int CCharsetConverter::m_Utf8CharMaxSize = 4;
+
+// clang-format off
+CConverterType CCharsetConverter::CInnerConverter::m_stdConversion[NumberOfStdConversionTypes] = /* keep it in sync with enum StdConversionType */
+{
+ /* Utf8ToUtf32 */ CConverterType(UTF8_SOURCE, UTF32_CHARSET),
+ /* Utf32ToUtf8 */ CConverterType(UTF32_CHARSET, "UTF-8", CCharsetConverter::m_Utf8CharMaxSize),
+ /* Utf32ToW */ CConverterType(UTF32_CHARSET, WCHAR_CHARSET),
+ /* WToUtf32 */ CConverterType(WCHAR_CHARSET, UTF32_CHARSET),
+ /* SubtitleCharsetToUtf8*/CConverterType(SubtitleCharset, "UTF-8", CCharsetConverter::m_Utf8CharMaxSize),
+ /* Utf8ToUserCharset */ CConverterType(UTF8_SOURCE, UserCharset),
+ /* UserCharsetToUtf8 */ CConverterType(UserCharset, "UTF-8", CCharsetConverter::m_Utf8CharMaxSize),
+ /* Utf32ToUserCharset */ CConverterType(UTF32_CHARSET, UserCharset),
+ /* WtoUtf8 */ CConverterType(WCHAR_CHARSET, "UTF-8", CCharsetConverter::m_Utf8CharMaxSize),
+ /* Utf16LEtoW */ CConverterType("UTF-16LE", WCHAR_CHARSET),
+ /* Utf16BEtoUtf8 */ CConverterType("UTF-16BE", "UTF-8", CCharsetConverter::m_Utf8CharMaxSize),
+ /* Utf16LEtoUtf8 */ CConverterType("UTF-16LE", "UTF-8", CCharsetConverter::m_Utf8CharMaxSize),
+ /* Utf8toW */ CConverterType(UTF8_SOURCE, WCHAR_CHARSET),
+ /* Utf8ToSystem */ CConverterType(UTF8_SOURCE, SystemCharset),
+ /* SystemToUtf8 */ CConverterType(SystemCharset, UTF8_SOURCE),
+ /* Ucs2CharsetToUtf8 */ CConverterType("UCS-2LE", "UTF-8", CCharsetConverter::m_Utf8CharMaxSize),
+ /* MacintoshToUtf8 */ CConverterType("macintosh", "UTF-8", CCharsetConverter::m_Utf8CharMaxSize)
+};
+// clang-format on
+
+CCriticalSection CCharsetConverter::CInnerConverter::m_critSectionFriBiDi;
+
+template<class INPUT,class OUTPUT>
+bool CCharsetConverter::CInnerConverter::stdConvert(StdConversionType convertType, const INPUT& strSource, OUTPUT& strDest, bool failOnInvalidChar /*= false*/)
+{
+ strDest.clear();
+ if (strSource.empty())
+ return true;
+
+ if (convertType < 0 || convertType >= NumberOfStdConversionTypes)
+ return false;
+
+ CConverterType& convType = m_stdConversion[convertType];
+ std::unique_lock<CCriticalSection> converterLock(convType);
+
+ return convert(convType.GetConverter(converterLock), convType.GetTargetSingleCharMaxLen(), strSource, strDest, failOnInvalidChar);
+}
+
+template<class INPUT,class OUTPUT>
+bool CCharsetConverter::CInnerConverter::customConvert(const std::string& sourceCharset, const std::string& targetCharset, const INPUT& strSource, OUTPUT& strDest, bool failOnInvalidChar /*= false*/)
+{
+ strDest.clear();
+ if (strSource.empty())
+ return true;
+
+ iconv_t conv = iconv_open(targetCharset.c_str(), sourceCharset.c_str());
+ if (conv == NO_ICONV)
+ {
+ CLog::Log(LOGERROR, "{}: iconv_open() for \"{}\" -> \"{}\" failed, errno = {} ({})",
+ __FUNCTION__, sourceCharset, targetCharset, errno, strerror(errno));
+ return false;
+ }
+ const int dstMultp = (targetCharset.compare(0, 5, "UTF-8") == 0) ? CCharsetConverter::m_Utf8CharMaxSize : 1;
+ const bool result = convert(conv, dstMultp, strSource, strDest, failOnInvalidChar);
+ iconv_close(conv);
+
+ return result;
+}
+
+/* iconv may declare inbuf to be char** rather than const char** depending on platform and version,
+ so provide a wrapper that handles both */
+struct charPtrPtrAdapter
+{
+ const char** pointer;
+ explicit charPtrPtrAdapter(const char** p) :
+ pointer(p) { }
+ operator char**()
+ { return const_cast<char**>(pointer); }
+ operator const char**()
+ { return pointer; }
+};
+
+template<class INPUT,class OUTPUT>
+bool CCharsetConverter::CInnerConverter::convert(iconv_t type, int multiplier, const INPUT& strSource, OUTPUT& strDest, bool failOnInvalidChar /*= false*/)
+{
+ if (type == NO_ICONV)
+ return false;
+
+ //input buffer for iconv() is the buffer from strSource
+ size_t inBufSize = (strSource.length() + 1) * sizeof(typename INPUT::value_type);
+ const char* inBuf = (const char*)strSource.c_str();
+
+ //allocate output buffer for iconv()
+ size_t outBufSize = (strSource.length() + 1) * sizeof(typename OUTPUT::value_type) * multiplier;
+ char* outBuf = (char*)malloc(outBufSize);
+ if (outBuf == NULL)
+ {
+ CLog::Log(LOGFATAL, "{}: malloc failed", __FUNCTION__);
+ return false;
+ }
+
+ size_t inBytesAvail = inBufSize; //how many bytes iconv() can read
+ size_t outBytesAvail = outBufSize; //how many bytes iconv() can write
+ const char* inBufStart = inBuf; //where in our input buffer iconv() should start reading
+ char* outBufStart = outBuf; //where in out output buffer iconv() should start writing
+
+ size_t returnV;
+ while(true)
+ {
+ //iconv() will update inBufStart, inBytesAvail, outBufStart and outBytesAvail
+ returnV = iconv(type, charPtrPtrAdapter(&inBufStart), &inBytesAvail, &outBufStart, &outBytesAvail);
+
+ if (returnV == (size_t)-1)
+ {
+ if (errno == E2BIG) //output buffer is not big enough
+ {
+ //save where iconv() ended converting, realloc might make outBufStart invalid
+ size_t bytesConverted = outBufSize - outBytesAvail;
+
+ //make buffer twice as big
+ outBufSize *= 2;
+ char* newBuf = (char*)realloc(outBuf, outBufSize);
+ if (!newBuf)
+ {
+ CLog::Log(LOGFATAL, "{} realloc failed with errno={}({})", __FUNCTION__, errno,
+ strerror(errno));
+ break;
+ }
+ outBuf = newBuf;
+
+ //update the buffer pointer and counter
+ outBufStart = outBuf + bytesConverted;
+ outBytesAvail = outBufSize - bytesConverted;
+
+ //continue in the loop and convert the rest
+ continue;
+ }
+ else if (errno == EILSEQ) //An invalid multibyte sequence has been encountered in the input
+ {
+ if (failOnInvalidChar)
+ break;
+
+ //skip invalid byte
+ inBufStart++;
+ inBytesAvail--;
+ //continue in the loop and convert the rest
+ continue;
+ }
+ else if (errno == EINVAL) /* Invalid sequence at the end of input buffer */
+ {
+ if (!failOnInvalidChar)
+ returnV = 0; /* reset error status to use converted part */
+
+ break;
+ }
+ else //iconv() had some other error
+ {
+ CLog::Log(LOGERROR, "{}: iconv() failed, errno={} ({})", __FUNCTION__, errno,
+ strerror(errno));
+ }
+ }
+ break;
+ }
+
+ //complete the conversion (reset buffers), otherwise the current data will prefix the data on the next call
+ if (iconv(type, NULL, NULL, &outBufStart, &outBytesAvail) == (size_t)-1)
+ CLog::Log(LOGERROR, "{} failed cleanup errno={}({})", __FUNCTION__, errno, strerror(errno));
+
+ if (returnV == (size_t)-1)
+ {
+ free(outBuf);
+ return false;
+ }
+ //we're done
+
+ const typename OUTPUT::size_type sizeInChars = (typename OUTPUT::size_type) (outBufSize - outBytesAvail) / sizeof(typename OUTPUT::value_type);
+ typename OUTPUT::const_pointer strPtr = (typename OUTPUT::const_pointer) outBuf;
+ /* Make sure that all buffer is assigned and string is stopped at end of buffer */
+ if (strPtr[sizeInChars-1] == 0 && strSource[strSource.length()-1] != 0)
+ strDest.assign(strPtr, sizeInChars-1);
+ else
+ strDest.assign(strPtr, sizeInChars);
+
+ free(outBuf);
+
+ return true;
+}
+
+bool CCharsetConverter::CInnerConverter::logicalToVisualBiDi(
+ const std::u32string& stringSrc,
+ std::u32string& stringDst,
+ FriBidiCharType base /*= FRIBIDI_TYPE_LTR*/,
+ const bool failOnBadString /*= false*/,
+ int* visualToLogicalMap /*= nullptr*/)
+{
+ stringDst.clear();
+
+ const size_t srcLen = stringSrc.length();
+ if (srcLen == 0)
+ return true;
+
+ stringDst.reserve(srcLen);
+ size_t lineStart = 0;
+
+ // libfribidi is not threadsafe, so make sure we make it so
+ std::unique_lock<CCriticalSection> lock(m_critSectionFriBiDi);
+ do
+ {
+ size_t lineEnd = stringSrc.find('\n', lineStart);
+ if (lineEnd >= srcLen) // equal to 'lineEnd == std::string::npos'
+ lineEnd = srcLen;
+ else
+ lineEnd++; // include '\n'
+
+ const size_t lineLen = lineEnd - lineStart;
+
+ FriBidiChar* visual = (FriBidiChar*) malloc((lineLen + 1) * sizeof(FriBidiChar));
+ if (visual == NULL)
+ {
+ free(visual);
+ CLog::Log(LOGFATAL, "{}: can't allocate memory", __FUNCTION__);
+ return false;
+ }
+
+ bool bidiFailed = false;
+ FriBidiCharType baseCopy = base; // preserve same value for all lines, required because fribidi_log2vis will modify parameter value
+ if (fribidi_log2vis(reinterpret_cast<const FriBidiChar*>(stringSrc.c_str() + lineStart),
+ lineLen, &baseCopy, visual, nullptr,
+ !visualToLogicalMap ? nullptr : visualToLogicalMap + lineStart, nullptr))
+ {
+ // Removes bidirectional marks
+ const int newLen = fribidi_remove_bidi_marks(
+ visual, lineLen, nullptr, !visualToLogicalMap ? nullptr : visualToLogicalMap + lineStart,
+ nullptr);
+ if (newLen > 0)
+ stringDst.append((const char32_t*)visual, (size_t)newLen);
+ else if (newLen < 0)
+ bidiFailed = failOnBadString;
+ }
+ else
+ bidiFailed = failOnBadString;
+
+ free(visual);
+
+ if (bidiFailed)
+ return false;
+
+ lineStart = lineEnd;
+ } while (lineStart < srcLen);
+
+ return !stringDst.empty();
+}
+
+bool CCharsetConverter::CInnerConverter::isBidiDirectionRTL(const std::string& str)
+{
+ std::u32string converted;
+ if (!CInnerConverter::stdConvert(Utf8ToUtf32, str, converted, true))
+ return false;
+
+ int lineLen = static_cast<int>(str.size());
+ FriBidiCharType* charTypes = new FriBidiCharType[lineLen];
+ fribidi_get_bidi_types(reinterpret_cast<const FriBidiChar*>(converted.c_str()),
+ (FriBidiStrIndex)lineLen, charTypes);
+ FriBidiCharType charType = fribidi_get_par_direction(charTypes, (FriBidiStrIndex)lineLen);
+ delete[] charTypes;
+ return charType == FRIBIDI_PAR_RTL;
+}
+
+static struct SCharsetMapping
+{
+ const char* charset;
+ const char* caption;
+} g_charsets[] = {
+ { "ISO-8859-1", "Western Europe (ISO)" }
+ , { "ISO-8859-2", "Central Europe (ISO)" }
+ , { "ISO-8859-3", "South Europe (ISO)" }
+ , { "ISO-8859-4", "Baltic (ISO)" }
+ , { "ISO-8859-5", "Cyrillic (ISO)" }
+ , { "ISO-8859-6", "Arabic (ISO)" }
+ , { "ISO-8859-7", "Greek (ISO)" }
+ , { "ISO-8859-8", "Hebrew (ISO)" }
+ , { "ISO-8859-9", "Turkish (ISO)" }
+ , { "CP1250", "Central Europe (Windows)" }
+ , { "CP1251", "Cyrillic (Windows)" }
+ , { "CP1252", "Western Europe (Windows)" }
+ , { "CP1253", "Greek (Windows)" }
+ , { "CP1254", "Turkish (Windows)" }
+ , { "CP1255", "Hebrew (Windows)" }
+ , { "CP1256", "Arabic (Windows)" }
+ , { "CP1257", "Baltic (Windows)" }
+ , { "CP1258", "Vietnamese (Windows)" }
+ , { "CP874", "Thai (Windows)" }
+ , { "BIG5", "Chinese Traditional (Big5)" }
+ , { "GBK", "Chinese Simplified (GBK)" }
+ , { "SHIFT_JIS", "Japanese (Shift-JIS)" }
+ , { "CP949", "Korean" }
+ , { "BIG5-HKSCS", "Hong Kong (Big5-HKSCS)" }
+ , { NULL, NULL }
+};
+
+CCharsetConverter::CCharsetConverter() = default;
+
+void CCharsetConverter::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_LOCALE_CHARSET)
+ resetUserCharset();
+ else if (settingId == CSettings::SETTING_SUBTITLES_CHARSET)
+ resetSubtitleCharset();
+}
+
+void CCharsetConverter::clear()
+{
+}
+
+std::vector<std::string> CCharsetConverter::getCharsetLabels()
+{
+ std::vector<std::string> lab;
+ for(SCharsetMapping* c = g_charsets; c->charset; c++)
+ lab.emplace_back(c->caption);
+
+ return lab;
+}
+
+std::string CCharsetConverter::getCharsetLabelByName(const std::string& charsetName)
+{
+ for(SCharsetMapping* c = g_charsets; c->charset; c++)
+ {
+ if (StringUtils::EqualsNoCase(charsetName,c->charset))
+ return c->caption;
+ }
+
+ return "";
+}
+
+std::string CCharsetConverter::getCharsetNameByLabel(const std::string& charsetLabel)
+{
+ for(SCharsetMapping* c = g_charsets; c->charset; c++)
+ {
+ if (StringUtils::EqualsNoCase(charsetLabel, c->caption))
+ return c->charset;
+ }
+
+ return "";
+}
+
+void CCharsetConverter::reset(void)
+{
+ for (CConverterType& conversion : CInnerConverter::m_stdConversion)
+ conversion.Reset();
+}
+
+void CCharsetConverter::resetSystemCharset(void)
+{
+ CInnerConverter::m_stdConversion[Utf8ToSystem].Reset();
+ CInnerConverter::m_stdConversion[SystemToUtf8].Reset();
+}
+
+void CCharsetConverter::resetUserCharset(void)
+{
+ CInnerConverter::m_stdConversion[UserCharsetToUtf8].Reset();
+ CInnerConverter::m_stdConversion[UserCharsetToUtf8].Reset();
+ CInnerConverter::m_stdConversion[Utf32ToUserCharset].Reset();
+ resetSubtitleCharset();
+}
+
+void CCharsetConverter::resetSubtitleCharset(void)
+{
+ CInnerConverter::m_stdConversion[SubtitleCharsetToUtf8].Reset();
+}
+
+void CCharsetConverter::reinitCharsetsFromSettings(void)
+{
+ resetUserCharset(); // this will also reinit Subtitle charsets
+}
+
+bool CCharsetConverter::utf8ToUtf32(const std::string& utf8StringSrc, std::u32string& utf32StringDst, bool failOnBadChar /*= true*/)
+{
+ return CInnerConverter::stdConvert(Utf8ToUtf32, utf8StringSrc, utf32StringDst, failOnBadChar);
+}
+
+std::u32string CCharsetConverter::utf8ToUtf32(const std::string& utf8StringSrc, bool failOnBadChar /*= true*/)
+{
+ std::u32string converted;
+ utf8ToUtf32(utf8StringSrc, converted, failOnBadChar);
+ return converted;
+}
+
+bool CCharsetConverter::utf8ToUtf32Visual(const std::string& utf8StringSrc, std::u32string& utf32StringDst, bool bVisualBiDiFlip /*= false*/, bool forceLTRReadingOrder /*= false*/, bool failOnBadChar /*= false*/)
+{
+ if (bVisualBiDiFlip)
+ {
+ std::u32string converted;
+ if (!CInnerConverter::stdConvert(Utf8ToUtf32, utf8StringSrc, converted, failOnBadChar))
+ return false;
+
+ return CInnerConverter::logicalToVisualBiDi(converted, utf32StringDst, forceLTRReadingOrder ? FRIBIDI_TYPE_LTR : FRIBIDI_TYPE_PDF, failOnBadChar);
+ }
+ return CInnerConverter::stdConvert(Utf8ToUtf32, utf8StringSrc, utf32StringDst, failOnBadChar);
+}
+
+bool CCharsetConverter::utf32ToUtf8(const std::u32string& utf32StringSrc, std::string& utf8StringDst, bool failOnBadChar /*= true*/)
+{
+ return CInnerConverter::stdConvert(Utf32ToUtf8, utf32StringSrc, utf8StringDst, failOnBadChar);
+}
+
+std::string CCharsetConverter::utf32ToUtf8(const std::u32string& utf32StringSrc, bool failOnBadChar /*= false*/)
+{
+ std::string converted;
+ utf32ToUtf8(utf32StringSrc, converted, failOnBadChar);
+ return converted;
+}
+
+bool CCharsetConverter::utf32ToW(const std::u32string& utf32StringSrc, std::wstring& wStringDst, bool failOnBadChar /*= true*/)
+{
+#ifdef WCHAR_IS_UCS_4
+ wStringDst.assign((const wchar_t*)utf32StringSrc.c_str(), utf32StringSrc.length());
+ return true;
+#else // !WCHAR_IS_UCS_4
+ return CInnerConverter::stdConvert(Utf32ToW, utf32StringSrc, wStringDst, failOnBadChar);
+#endif // !WCHAR_IS_UCS_4
+}
+
+bool CCharsetConverter::utf32logicalToVisualBiDi(const std::u32string& logicalStringSrc,
+ std::u32string& visualStringDst,
+ bool forceLTRReadingOrder /*= false*/,
+ bool failOnBadString /*= false*/,
+ int* visualToLogicalMap /*= nullptr*/)
+{
+ return CInnerConverter::logicalToVisualBiDi(
+ logicalStringSrc, visualStringDst, forceLTRReadingOrder ? FRIBIDI_TYPE_LTR : FRIBIDI_TYPE_PDF,
+ failOnBadString, visualToLogicalMap);
+}
+
+bool CCharsetConverter::wToUtf32(const std::wstring& wStringSrc, std::u32string& utf32StringDst, bool failOnBadChar /*= true*/)
+{
+#ifdef WCHAR_IS_UCS_4
+ /* UCS-4 is almost equal to UTF-32, but UTF-32 has strict limits on possible values, while UCS-4 is usually unchecked.
+ * With this "conversion" we ensure that output will be valid UTF-32 string. */
+#endif
+ return CInnerConverter::stdConvert(WToUtf32, wStringSrc, utf32StringDst, failOnBadChar);
+}
+
+// The bVisualBiDiFlip forces a flip of characters for hebrew/arabic languages, only set to false if the flipping
+// of the string is already made or the string is not displayed in the GUI
+bool CCharsetConverter::utf8ToW(const std::string& utf8StringSrc, std::wstring& wStringDst, bool bVisualBiDiFlip /*= true*/,
+ bool forceLTRReadingOrder /*= false*/, bool failOnBadChar /*= false*/)
+{
+ // Try to flip hebrew/arabic characters, if any
+ if (bVisualBiDiFlip)
+ {
+ wStringDst.clear();
+ std::u32string utf32str;
+ if (!CInnerConverter::stdConvert(Utf8ToUtf32, utf8StringSrc, utf32str, failOnBadChar))
+ return false;
+
+ std::u32string utf32flipped;
+ const bool bidiResult = CInnerConverter::logicalToVisualBiDi(utf32str, utf32flipped, forceLTRReadingOrder ? FRIBIDI_TYPE_LTR : FRIBIDI_TYPE_PDF, failOnBadChar);
+
+ return CInnerConverter::stdConvert(Utf32ToW, utf32flipped, wStringDst, failOnBadChar) && bidiResult;
+ }
+
+ return CInnerConverter::stdConvert(Utf8toW, utf8StringSrc, wStringDst, failOnBadChar);
+}
+
+bool CCharsetConverter::subtitleCharsetToUtf8(const std::string& stringSrc, std::string& utf8StringDst)
+{
+ return CInnerConverter::stdConvert(SubtitleCharsetToUtf8, stringSrc, utf8StringDst, false);
+}
+
+bool CCharsetConverter::fromW(const std::wstring& wStringSrc,
+ std::string& stringDst, const std::string& enc)
+{
+ return CInnerConverter::customConvert(WCHAR_CHARSET, enc, wStringSrc, stringDst);
+}
+
+bool CCharsetConverter::toW(const std::string& stringSrc,
+ std::wstring& wStringDst, const std::string& enc)
+{
+ return CInnerConverter::customConvert(enc, WCHAR_CHARSET, stringSrc, wStringDst);
+}
+
+bool CCharsetConverter::utf8ToStringCharset(const std::string& utf8StringSrc, std::string& stringDst)
+{
+ return CInnerConverter::stdConvert(Utf8ToUserCharset, utf8StringSrc, stringDst);
+}
+
+bool CCharsetConverter::utf8ToStringCharset(std::string& stringSrcDst)
+{
+ std::string strSrc(stringSrcDst);
+ return utf8ToStringCharset(strSrc, stringSrcDst);
+}
+
+bool CCharsetConverter::ToUtf8(const std::string& strSourceCharset, const std::string& stringSrc, std::string& utf8StringDst, bool failOnBadChar /*= false*/)
+{
+ if (strSourceCharset == "UTF-8")
+ { // simple case - no conversion necessary
+ utf8StringDst = stringSrc;
+ return true;
+ }
+
+ return CInnerConverter::customConvert(strSourceCharset, "UTF-8", stringSrc, utf8StringDst, failOnBadChar);
+}
+
+bool CCharsetConverter::utf8To(const std::string& strDestCharset, const std::string& utf8StringSrc, std::string& stringDst)
+{
+ if (strDestCharset == "UTF-8")
+ { // simple case - no conversion necessary
+ stringDst = utf8StringSrc;
+ return true;
+ }
+
+ return CInnerConverter::customConvert(UTF8_SOURCE, strDestCharset, utf8StringSrc, stringDst);
+}
+
+bool CCharsetConverter::utf8To(const std::string& strDestCharset, const std::string& utf8StringSrc, std::u16string& utf16StringDst)
+{
+ return CInnerConverter::customConvert(UTF8_SOURCE, strDestCharset, utf8StringSrc, utf16StringDst);
+}
+
+bool CCharsetConverter::utf8To(const std::string& strDestCharset, const std::string& utf8StringSrc, std::u32string& utf32StringDst)
+{
+ return CInnerConverter::customConvert(UTF8_SOURCE, strDestCharset, utf8StringSrc, utf32StringDst);
+}
+
+bool CCharsetConverter::unknownToUTF8(std::string& stringSrcDst)
+{
+ std::string source(stringSrcDst);
+ return unknownToUTF8(source, stringSrcDst);
+}
+
+bool CCharsetConverter::unknownToUTF8(const std::string& stringSrc, std::string& utf8StringDst, bool failOnBadChar /*= false*/)
+{
+ // checks whether it's utf8 already, and if not converts using the sourceCharset if given, else the string charset
+ if (CUtf8Utils::isValidUtf8(stringSrc))
+ {
+ utf8StringDst = stringSrc;
+ return true;
+ }
+ return CInnerConverter::stdConvert(UserCharsetToUtf8, stringSrc, utf8StringDst, failOnBadChar);
+}
+
+bool CCharsetConverter::wToUTF8(const std::wstring& wStringSrc, std::string& utf8StringDst, bool failOnBadChar /*= false*/)
+{
+ return CInnerConverter::stdConvert(WtoUtf8, wStringSrc, utf8StringDst, failOnBadChar);
+}
+
+bool CCharsetConverter::utf16BEtoUTF8(const std::u16string& utf16StringSrc, std::string& utf8StringDst)
+{
+ return CInnerConverter::stdConvert(Utf16BEtoUtf8, utf16StringSrc, utf8StringDst);
+}
+
+bool CCharsetConverter::utf16BEtoUTF8(const std::string& utf16StringSrc, std::string& utf8StringDst)
+{
+ return CInnerConverter::stdConvert(Utf16BEtoUtf8, utf16StringSrc, utf8StringDst);
+}
+
+bool CCharsetConverter::utf16LEtoUTF8(const std::u16string& utf16StringSrc,
+ std::string& utf8StringDst)
+{
+ return CInnerConverter::stdConvert(Utf16LEtoUtf8, utf16StringSrc, utf8StringDst);
+}
+
+bool CCharsetConverter::ucs2ToUTF8(const std::u16string& ucs2StringSrc, std::string& utf8StringDst)
+{
+ return CInnerConverter::stdConvert(Ucs2CharsetToUtf8, ucs2StringSrc,utf8StringDst);
+}
+
+bool CCharsetConverter::utf16LEtoW(const std::u16string& utf16String, std::wstring& wString)
+{
+ return CInnerConverter::stdConvert(Utf16LEtoW, utf16String, wString);
+}
+
+bool CCharsetConverter::utf32ToStringCharset(const std::u32string& utf32StringSrc, std::string& stringDst)
+{
+ return CInnerConverter::stdConvert(Utf32ToUserCharset, utf32StringSrc, stringDst);
+}
+
+bool CCharsetConverter::utf8ToSystem(std::string& stringSrcDst, bool failOnBadChar /*= false*/)
+{
+ std::string strSrc(stringSrcDst);
+ return CInnerConverter::stdConvert(Utf8ToSystem, strSrc, stringSrcDst, failOnBadChar);
+}
+
+bool CCharsetConverter::systemToUtf8(const std::string& sysStringSrc, std::string& utf8StringDst, bool failOnBadChar /*= false*/)
+{
+ return CInnerConverter::stdConvert(SystemToUtf8, sysStringSrc, utf8StringDst, failOnBadChar);
+}
+
+bool CCharsetConverter::MacintoshToUTF8(const std::string& macStringSrc, std::string& utf8StringDst)
+{
+ return CInnerConverter::stdConvert(MacintoshToUtf8, macStringSrc, utf8StringDst);
+}
+
+bool CCharsetConverter::utf8logicalToVisualBiDi(const std::string& utf8StringSrc, std::string& utf8StringDst, bool failOnBadString /*= false*/)
+{
+ utf8StringDst.clear();
+ std::u32string utf32flipped;
+ if (!utf8ToUtf32Visual(utf8StringSrc, utf32flipped, true, true, failOnBadString))
+ return false;
+
+ return CInnerConverter::stdConvert(Utf32ToUtf8, utf32flipped, utf8StringDst, failOnBadString);
+}
+
+bool CCharsetConverter::utf8IsRTLBidiDirection(const std::string& utf8String)
+{
+ return CInnerConverter::isBidiDirectionRTL(utf8String);
+}
+
+void CCharsetConverter::SettingOptionsCharsetsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ std::vector<std::string> vecCharsets = g_charsetConverter.getCharsetLabels();
+ sort(vecCharsets.begin(), vecCharsets.end(), sortstringbyname());
+
+ list.emplace_back(g_localizeStrings.Get(13278), "DEFAULT"); // "Default"
+ for (int i = 0; i < (int) vecCharsets.size(); ++i)
+ list.emplace_back(vecCharsets[i], g_charsetConverter.getCharsetNameByLabel(vecCharsets[i]));
+}
diff --git a/xbmc/utils/CharsetConverter.h b/xbmc/utils/CharsetConverter.h
new file mode 100644
index 0000000..ae956c3
--- /dev/null
+++ b/xbmc/utils/CharsetConverter.h
@@ -0,0 +1,207 @@
+/*
+ * 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 "settings/lib/ISettingCallback.h"
+#include "utils/GlobalsHandling.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+class CSetting;
+struct StringSettingOption;
+
+class CCharsetConverter : public ISettingCallback
+{
+public:
+ CCharsetConverter();
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ static void reset();
+ static void resetSystemCharset();
+ static void reinitCharsetsFromSettings(void);
+
+ static void clear();
+
+ /**
+ * Convert UTF-8 string to UTF-32 string.
+ * No RTL logical-visual transformation is performed.
+ * @param utf8StringSrc is source UTF-8 string to convert
+ * @param utf32StringDst is output UTF-32 string, empty on any error
+ * @param failOnBadChar if set to true function will fail on invalid character,
+ * otherwise invalid character will be skipped
+ * @return true on successful conversion, false on any error
+ */
+ static bool utf8ToUtf32(const std::string& utf8StringSrc, std::u32string& utf32StringDst, bool failOnBadChar = true);
+ /**
+ * Convert UTF-8 string to UTF-32 string.
+ * No RTL logical-visual transformation is performed.
+ * @param utf8StringSrc is source UTF-8 string to convert
+ * @param failOnBadChar if set to true function will fail on invalid character,
+ * otherwise invalid character will be skipped
+ * @return converted string on successful conversion, empty string on any error
+ */
+ static std::u32string utf8ToUtf32(const std::string& utf8StringSrc, bool failOnBadChar = true);
+ /**
+ * Convert UTF-8 string to UTF-32 string.
+ * RTL logical-visual transformation is optionally performed.
+ * Use it for readable text, GUI strings etc.
+ * @param utf8StringSrc is source UTF-8 string to convert
+ * @param utf32StringDst is output UTF-32 string, empty on any error
+ * @param bVisualBiDiFlip allow RTL visual-logical transformation if set to true, must be set
+ * to false is logical-visual transformation is already done
+ * @param forceLTRReadingOrder force LTR reading order
+ * @param failOnBadChar if set to true function will fail on invalid character,
+ * otherwise invalid character will be skipped
+ * @return true on successful conversion, false on any error
+ */
+ static bool utf8ToUtf32Visual(const std::string& utf8StringSrc, std::u32string& utf32StringDst, bool bVisualBiDiFlip = false, bool forceLTRReadingOrder = false, bool failOnBadChar = false);
+ /**
+ * Convert UTF-32 string to UTF-8 string.
+ * No RTL visual-logical transformation is performed.
+ * @param utf32StringSrc is source UTF-32 string to convert
+ * @param utf8StringDst is output UTF-8 string, empty on any error
+ * @param failOnBadChar if set to true function will fail on invalid character,
+ * otherwise invalid character will be skipped
+ * @return true on successful conversion, false on any error
+ */
+ static bool utf32ToUtf8(const std::u32string& utf32StringSrc, std::string& utf8StringDst, bool failOnBadChar = false);
+ /**
+ * Convert UTF-32 string to UTF-8 string.
+ * No RTL visual-logical transformation is performed.
+ * @param utf32StringSrc is source UTF-32 string to convert
+ * @param failOnBadChar if set to true function will fail on invalid character,
+ * otherwise invalid character will be skipped
+ * @return converted string on successful conversion, empty string on any error
+ */
+ static std::string utf32ToUtf8(const std::u32string& utf32StringSrc, bool failOnBadChar = false);
+ /**
+ * Convert UTF-32 string to wchar_t string (wstring).
+ * No RTL visual-logical transformation is performed.
+ * @param utf32StringSrc is source UTF-32 string to convert
+ * @param wStringDst is output wchar_t string, empty on any error
+ * @param failOnBadChar if set to true function will fail on invalid character,
+ * otherwise invalid character will be skipped
+ * @return true on successful conversion, false on any error
+ */
+ static bool utf32ToW(const std::u32string& utf32StringSrc, std::wstring& wStringDst, bool failOnBadChar = false);
+ /**
+ * Perform logical to visual flip.
+ * @param logicalStringSrc is source string with logical characters order
+ * @param visualStringDst is output string with visual characters order, empty on any error
+ * @param forceLTRReadingOrder force LTR reading order
+ * @param visualToLogicalMap is output mapping of positions in the visual string to the logical string
+ * @return true on success, false otherwise
+ */
+ static bool utf32logicalToVisualBiDi(const std::u32string& logicalStringSrc,
+ std::u32string& visualStringDst,
+ bool forceLTRReadingOrder = false,
+ bool failOnBadString = false,
+ int* visualToLogicalMap = nullptr);
+ /**
+ * Strictly convert wchar_t string (wstring) to UTF-32 string.
+ * No RTL visual-logical transformation is performed.
+ * @param wStringSrc is source wchar_t string to convert
+ * @param utf32StringDst is output UTF-32 string, empty on any error
+ * @param failOnBadChar if set to true function will fail on invalid character,
+ * otherwise invalid character will be skipped
+ * @return true on successful conversion, false on any error
+ */
+ static bool wToUtf32(const std::wstring& wStringSrc, std::u32string& utf32StringDst, bool failOnBadChar = false);
+
+ static bool utf8ToW(const std::string& utf8StringSrc, std::wstring& wStringDst,
+ bool bVisualBiDiFlip = true, bool forceLTRReadingOrder = false,
+ bool failOnBadChar = false);
+
+ static bool utf16LEtoW(const std::u16string& utf16String, std::wstring& wString);
+
+ static bool subtitleCharsetToUtf8(const std::string& stringSrc, std::string& utf8StringDst);
+
+ static bool utf8ToStringCharset(const std::string& utf8StringSrc, std::string& stringDst);
+
+ static bool utf8ToStringCharset(std::string& stringSrcDst);
+ static bool utf8ToSystem(std::string& stringSrcDst, bool failOnBadChar = false);
+ static bool systemToUtf8(const std::string& sysStringSrc, std::string& utf8StringDst, bool failOnBadChar = false);
+
+ static bool utf8To(const std::string& strDestCharset, const std::string& utf8StringSrc, std::string& stringDst);
+ static bool utf8To(const std::string& strDestCharset, const std::string& utf8StringSrc, std::u16string& utf16StringDst);
+ static bool utf8To(const std::string& strDestCharset, const std::string& utf8StringSrc, std::u32string& utf32StringDst);
+
+ static bool ToUtf8(const std::string& strSourceCharset, const std::string& stringSrc, std::string& utf8StringDst, bool failOnBadChar = false);
+
+ static bool wToUTF8(const std::wstring& wStringSrc, std::string& utf8StringDst, bool failOnBadChar = false);
+
+ /*!
+ * \brief Convert UTF-16BE (u16string) string to UTF-8 string.
+ * No RTL visual-logical transformation is performed.
+ * \param utf16StringSrc Is source UTF-16BE u16string string to convert
+ * \param utf8StringDst Is output UTF-8 string, empty on any error
+ * \return True on successful conversion, false on any error
+ */
+ static bool utf16BEtoUTF8(const std::u16string& utf16StringSrc, std::string& utf8StringDst);
+
+ /*!
+ * \brief Convert UTF-16BE (string) string to UTF-8 string.
+ * No RTL visual-logical transformation is performed.
+ * \param utf16StringSrc Is source UTF-16BE string to convert
+ * \param utf8StringDst Is output UTF-8 string, empty on any error
+ * \return True on successful conversion, false on any error
+ */
+ static bool utf16BEtoUTF8(const std::string& utf16StringSrc, std::string& utf8StringDst);
+
+ static bool utf16LEtoUTF8(const std::u16string& utf16StringSrc, std::string& utf8StringDst);
+ static bool ucs2ToUTF8(const std::u16string& ucs2StringSrc, std::string& utf8StringDst);
+
+ /*!
+ * \brief Convert Macintosh (string) string to UTF-8 string.
+ * No RTL visual-logical transformation is performed.
+ * \param macStringSrc Is source Macintosh string to convert
+ * \param utf8StringDst Is output UTF-8 string, empty on any error
+ * \return True on successful conversion, false on any error
+ */
+ static bool MacintoshToUTF8(const std::string& macStringSrc, std::string& utf8StringDst);
+
+ static bool utf8logicalToVisualBiDi(const std::string& utf8StringSrc, std::string& utf8StringDst, bool failOnBadString = false);
+
+ /**
+ * Check if a string has RTL direction.
+ * @param utf8StringSrc the string
+ * @return true if the string is RTL, otherwise false
+ */
+ static bool utf8IsRTLBidiDirection(const std::string& utf8String);
+
+ static bool utf32ToStringCharset(const std::u32string& utf32StringSrc, std::string& stringDst);
+
+ static std::vector<std::string> getCharsetLabels();
+ static std::string getCharsetLabelByName(const std::string& charsetName);
+ static std::string getCharsetNameByLabel(const std::string& charsetLabel);
+
+ static bool unknownToUTF8(std::string& stringSrcDst);
+ static bool unknownToUTF8(const std::string& stringSrc, std::string& utf8StringDst, bool failOnBadChar = false);
+
+ static bool toW(const std::string& stringSrc, std::wstring& wStringDst, const std::string& enc);
+ static bool fromW(const std::wstring& wStringSrc, std::string& stringDst, const std::string& enc);
+
+ static void SettingOptionsCharsetsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+
+private:
+ static void resetUserCharset(void);
+ static void resetSubtitleCharset(void);
+
+ static const int m_Utf8CharMinSize, m_Utf8CharMaxSize;
+ class CInnerConverter;
+};
+
+XBMC_GLOBAL_REF(CCharsetConverter,g_charsetConverter);
+#define g_charsetConverter XBMC_GLOBAL_USE(CCharsetConverter)
diff --git a/xbmc/utils/CharsetDetection.cpp b/xbmc/utils/CharsetDetection.cpp
new file mode 100644
index 0000000..965af0a
--- /dev/null
+++ b/xbmc/utils/CharsetDetection.cpp
@@ -0,0 +1,642 @@
+/*
+ * Copyright (C) 2013-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 "CharsetDetection.h"
+
+#include "LangInfo.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/Utf8Utils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+
+/* XML declaration can be virtually any size (with many-many whitespaces)
+ * but for in real world we don't need to process megabytes of data
+ * so limit search for XML declaration to reasonable value */
+const size_t CCharsetDetection::m_XmlDeclarationMaxLength = 250;
+
+/* According to http://www.w3.org/TR/2013/CR-html5-20130806/single-page.html#charset
+ * encoding must be placed in first 1024 bytes of document */
+const size_t CCharsetDetection::m_HtmlCharsetEndSearchPos = 1024;
+
+/* According to http://www.w3.org/TR/2013/CR-html5-20130806/single-page.html#space-character
+ * tab, LF, FF, CR or space can be used as whitespace */
+const std::string CCharsetDetection::m_HtmlWhitespaceChars("\x09\x0A\x0C\x0D\x20"); // tab, LF, FF, CR and space
+
+std::string CCharsetDetection::GetBomEncoding(const char* const content, const size_t contentLength)
+{
+ if (contentLength < 2)
+ return "";
+ if (content[0] == (char)0xFE && content[1] == (char)0xFF)
+ return "UTF-16BE";
+ if (contentLength >= 4 && content[0] == (char)0xFF && content[1] == (char)0xFE && content[2] == (char)0x00 && content[3] == (char)0x00)
+ return "UTF-32LE"; /* first two bytes are same for UTF-16LE and UTF-32LE, so first check for full UTF-32LE mark */
+ if (content[0] == (char)0xFF && content[1] == (char)0xFE)
+ return "UTF-16LE";
+ if (contentLength < 3)
+ return "";
+ if (content[0] == (char)0xEF && content[1] == (char)0xBB && content[2] == (char)0xBF)
+ return "UTF-8";
+ if (contentLength < 4)
+ return "";
+ if (content[0] == (char)0x00 && content[1] == (char)0x00 && content[2] == (char)0xFE && content[3] == (char)0xFF)
+ return "UTF-32BE";
+ if (contentLength >= 5 && content[0] == (char)0x2B && content[1] == (char)0x2F && content[2] == (char)0x76 &&
+ (content[4] == (char)0x32 || content[4] == (char)0x39 || content[4] == (char)0x2B || content[4] == (char)0x2F))
+ return "UTF-7";
+ if (content[0] == (char)0x84 && content[1] == (char)0x31 && content[2] == (char)0x95 && content[3] == (char)0x33)
+ return "GB18030";
+
+ return "";
+}
+
+bool CCharsetDetection::DetectXmlEncoding(const char* const xmlContent, const size_t contentLength, std::string& detectedEncoding)
+{
+ detectedEncoding.clear();
+
+ if (contentLength < 2)
+ return false; // too short for any detection
+
+ /* Byte Order Mark has priority over "encoding=" parameter */
+ detectedEncoding = GetBomEncoding(xmlContent, contentLength);
+ if (!detectedEncoding.empty())
+ return true;
+
+ /* try to read encoding from XML declaration */
+ if (GetXmlEncodingFromDeclaration(xmlContent, contentLength, detectedEncoding))
+ {
+ StringUtils::ToUpper(detectedEncoding);
+
+ /* make some safety checks */
+ if (detectedEncoding == "UTF-8")
+ return true; // fast track for most common case
+
+ if (StringUtils::StartsWith(detectedEncoding, "UCS-") || StringUtils::StartsWith(detectedEncoding, "UTF-"))
+ {
+ if (detectedEncoding == "UTF-7")
+ return true;
+
+ /* XML declaration was detected in UTF-8 mode (by 'GetXmlEncodingFromDeclaration') so we know
+ * that text in single byte encoding, but declaration itself wrongly specify multibyte encoding */
+ detectedEncoding.clear();
+ return false;
+ }
+ return true;
+ }
+
+ /* try to detect basic encoding */
+ std::string guessedEncoding;
+ if (!GuessXmlEncoding(xmlContent, contentLength, guessedEncoding))
+ return false; /* can't detect any encoding */
+
+ /* have some guessed encoding, try to use it */
+ std::string convertedXml;
+ /* use 'm_XmlDeclarationMaxLength * 4' below for UTF-32-like encodings */
+ if (!g_charsetConverter.ToUtf8(guessedEncoding, std::string(xmlContent, std::min(contentLength, m_XmlDeclarationMaxLength * 4)), convertedXml)
+ || convertedXml.empty())
+ return false; /* can't convert, guessed encoding is wrong */
+
+ /* text converted, hopefully at least XML declaration is in UTF-8 now */
+ std::string declaredEncoding;
+ /* try to read real encoding from converted XML declaration */
+ if (!GetXmlEncodingFromDeclaration(convertedXml.c_str(), convertedXml.length(), declaredEncoding))
+ { /* did not find real encoding in XML declaration, use guessed encoding */
+ detectedEncoding = guessedEncoding;
+ return true;
+ }
+
+ /* found encoding in converted XML declaration, we know correct endianness and number of bytes per char */
+ /* make some safety checks */
+ StringUtils::ToUpper(declaredEncoding);
+ if (declaredEncoding == guessedEncoding)
+ return true;
+
+ if (StringUtils::StartsWith(guessedEncoding, "UCS-4"))
+ {
+ if (declaredEncoding.length() < 5 ||
+ (!StringUtils::StartsWith(declaredEncoding, "UTF-32") && !StringUtils::StartsWith(declaredEncoding, "UCS-4")))
+ { /* Guessed encoding was correct because we can convert and read XML declaration, but declaration itself is wrong (not 4-bytes encoding) */
+ detectedEncoding = guessedEncoding;
+ return true;
+ }
+ }
+ else if (StringUtils::StartsWith(guessedEncoding, "UTF-16"))
+ {
+ if (declaredEncoding.length() < 5 ||
+ (!StringUtils::StartsWith(declaredEncoding, "UTF-16") && !StringUtils::StartsWith(declaredEncoding, "UCS-2")))
+ { /* Guessed encoding was correct because we can read XML declaration, but declaration is wrong (not 2-bytes encoding) */
+ detectedEncoding = guessedEncoding;
+ return true;
+ }
+ }
+
+ if (StringUtils::StartsWith(guessedEncoding, "UCS-4") || StringUtils::StartsWith(guessedEncoding, "UTF-16"))
+ {
+ /* Check endianness in declared encoding. We already know correct endianness as XML declaration was detected after conversion. */
+ /* Guessed UTF/UCS encoding always ends with endianness */
+ std::string guessedEndianness(guessedEncoding, guessedEncoding.length() - 2);
+
+ if (!StringUtils::EndsWith(declaredEncoding, "BE") && !StringUtils::EndsWith(declaredEncoding, "LE")) /* Declared encoding without endianness */
+ detectedEncoding = declaredEncoding + guessedEndianness; /* add guessed endianness */
+ else if (!StringUtils::EndsWith(declaredEncoding, guessedEndianness)) /* Wrong endianness in declared encoding */
+ detectedEncoding = declaredEncoding.substr(0, declaredEncoding.length() - 2) + guessedEndianness; /* replace endianness by guessed endianness */
+ else
+ detectedEncoding = declaredEncoding; /* declared encoding with correct endianness */
+
+ return true;
+ }
+ else if (StringUtils::StartsWith(guessedEncoding, "EBCDIC"))
+ {
+ if (declaredEncoding.find("EBCDIC") != std::string::npos)
+ detectedEncoding = declaredEncoding; /* Declared encoding is some specific EBCDIC encoding */
+ else
+ detectedEncoding = guessedEncoding;
+
+ return true;
+ }
+
+ /* should be unreachable */
+ return false;
+}
+
+bool CCharsetDetection::GetXmlEncodingFromDeclaration(const char* const xmlContent, const size_t contentLength, std::string& declaredEncoding)
+{
+ // following code is std::string-processing analog of regular expression-processing
+ // regular expression: "<\\?xml([ \n\r\t]+[^ \n\t\r>]+)*[ \n\r\t]+encoding[ \n\r\t]*=[ \n\r\t]*('[^ \n\t\r>']+'|\"[^ \n\t\r>\"]+\")"
+ // on win32 x86 machine regular expression is slower that std::string 20-40 times and can slowdown XML processing for several times
+ // seems that this regular expression is too slow due to many variable length parts, regexp for '&amp;'-fixing is much faster
+
+ declaredEncoding.clear();
+
+ // avoid extra large search
+ std::string strXml(xmlContent, std::min(contentLength, m_XmlDeclarationMaxLength));
+
+ size_t pos = strXml.find("<?xml");
+ if (pos == std::string::npos || pos + 6 > strXml.length() || pos > strXml.find('<'))
+ return false; // no "<?xml" declaration, "<?xml" is not first element or "<?xml" is incomplete
+
+ pos += 5; // 5 is length of "<?xml"
+
+ const size_t declLength = std::min(std::min(m_XmlDeclarationMaxLength, contentLength - pos), strXml.find('>', pos) - pos);
+ const std::string xmlDecl(xmlContent + pos, declLength);
+ const char* const xmlDeclC = xmlDecl.c_str(); // for faster processing of [] and for null-termination
+
+ static const char* const whiteSpaceChars = " \n\r\t"; // according to W3C Recommendation for XML, any of them can be used as separator
+ pos = 0;
+
+ while (pos + 12 <= declLength) // 12 is minimal length of "encoding='x'"
+ {
+ pos = xmlDecl.find_first_of(whiteSpaceChars, pos);
+ if (pos == std::string::npos)
+ return false; // no " encoding=" in declaration
+
+ pos = xmlDecl.find_first_not_of(whiteSpaceChars, pos);
+ if (pos == std::string::npos)
+ return false; // no "encoding=" in declaration
+
+ if (xmlDecl.compare(pos, 8, "encoding", 8) != 0)
+ continue; // not "encoding" parameter
+ pos += 8; // length of "encoding"
+
+ if (xmlDeclC[pos] == ' ' || xmlDeclC[pos] == '\n' || xmlDeclC[pos] == '\r' || xmlDeclC[pos] == '\t') // no buffer overrun as string is null-terminated
+ {
+ pos = xmlDecl.find_first_not_of(whiteSpaceChars, pos);
+ if (pos == std::string::npos)
+ return false; // this " encoding" is incomplete, only whitespace chars remains
+ }
+ if (xmlDeclC[pos] != '=')
+ { // "encoding" without "=", try to find other
+ pos--; // step back to whitespace
+ continue;
+ }
+
+ pos++; // skip '='
+ if (xmlDeclC[pos] == ' ' || xmlDeclC[pos] == '\n' || xmlDeclC[pos] == '\r' || xmlDeclC[pos] == '\t') // no buffer overrun as string is null-terminated
+ {
+ pos = xmlDecl.find_first_not_of(whiteSpaceChars, pos);
+ if (pos == std::string::npos)
+ return false; // this " encoding" is incomplete, only whitespace chars remains
+ }
+ size_t encNameEndPos;
+ if (xmlDeclC[pos] == '"')
+ encNameEndPos = xmlDecl.find('"', ++pos);
+ else if (xmlDeclC[pos] == '\'')
+ encNameEndPos = xmlDecl.find('\'', ++pos);
+ else
+ continue; // no quote or double quote after 'encoding=', try to find other
+
+ if (encNameEndPos != std::string::npos)
+ {
+ declaredEncoding.assign(xmlDecl, pos, encNameEndPos - pos);
+ return true;
+ }
+ // no closing quote or double quote after 'encoding="x', try to find other
+ }
+
+ return false;
+}
+
+bool CCharsetDetection::GuessXmlEncoding(const char* const xmlContent, const size_t contentLength, std::string& supposedEncoding)
+{
+ supposedEncoding.clear();
+ if (contentLength < 4)
+ return false; // too little data to guess
+
+ if (xmlContent[0] == 0 && xmlContent[1] == 0 && xmlContent[2] == 0 && xmlContent[3] == (char)0x3C) // '<' == '00 00 00 3C' in UCS-4 (UTF-32) big-endian
+ supposedEncoding = "UCS-4BE"; // use UCS-4 according to W3C recommendation
+ else if (xmlContent[0] == (char)0x3C && xmlContent[1] == 0 && xmlContent[2] == 0 && xmlContent[3] == 0) // '<' == '3C 00 00 00' in UCS-4 (UTF-32) little-endian
+ supposedEncoding = "UCS-4LE"; // use UCS-4 according to W3C recommendation
+ else if (xmlContent[0] == 0 && xmlContent[1] == (char)0x3C && xmlContent[2] == 0 && xmlContent[3] == (char)0x3F) // "<?" == "00 3C 00 3F" in UTF-16 (UCS-2) big-endian
+ supposedEncoding = "UTF-16BE";
+ else if (xmlContent[0] == (char)0x3C && xmlContent[1] == 0 && xmlContent[2] == (char)0x3F && xmlContent[3] == 0) // "<?" == "3C 00 3F 00" in UTF-16 (UCS-2) little-endian
+ supposedEncoding = "UTF-16LE";
+ else if (xmlContent[0] == (char)0x4C && xmlContent[1] == (char)0x6F && xmlContent[2] == (char)0xA7 && xmlContent[3] == (char)0x94) // "<?xm" == "4C 6F A7 94" in most EBCDIC encodings
+ supposedEncoding = "EBCDIC-CP-US"; // guessed value, real value must be read from declaration
+ else
+ return false;
+
+ return true;
+}
+
+bool CCharsetDetection::ConvertHtmlToUtf8(const std::string& htmlContent, std::string& converted, const std::string& serverReportedCharset, std::string& usedHtmlCharset)
+{
+ converted.clear();
+ usedHtmlCharset.clear();
+ if (htmlContent.empty())
+ {
+ usedHtmlCharset = "UTF-8"; // any charset can be used for empty content, use UTF-8 as default
+ return false;
+ }
+
+ // this is relaxed implementation of http://www.w3.org/TR/2013/CR-html5-20130806/single-page.html#determining-the-character-encoding
+
+ // try to get charset from Byte Order Mark
+ std::string bomCharset(GetBomEncoding(htmlContent));
+ if (checkConversion(bomCharset, htmlContent, converted))
+ {
+ usedHtmlCharset = bomCharset;
+ return true;
+ }
+
+ // try charset from HTTP header (or from other out-of-band source)
+ if (checkConversion(serverReportedCharset, htmlContent, converted))
+ {
+ usedHtmlCharset = serverReportedCharset;
+ return true;
+ }
+
+ // try to find charset in HTML
+ std::string declaredCharset(GetHtmlEncodingFromHead(htmlContent));
+ if (!declaredCharset.empty())
+ {
+ if (declaredCharset.compare(0, 3, "UTF", 3) == 0)
+ declaredCharset = "UTF-8"; // charset string was found in singlebyte mode, charset can't be multibyte encoding
+ if (checkConversion(declaredCharset, htmlContent, converted))
+ {
+ usedHtmlCharset = declaredCharset;
+ return true;
+ }
+ }
+
+ // try UTF-8 if not tried before
+ if (bomCharset != "UTF-8" && serverReportedCharset != "UTF-8" && declaredCharset != "UTF-8" && checkConversion("UTF-8", htmlContent, converted))
+ {
+ usedHtmlCharset = "UTF-8";
+ return false; // only guessed value
+ }
+
+ // try user charset
+ std::string userCharset(g_langInfo.GetGuiCharSet());
+ if (checkConversion(userCharset, htmlContent, converted))
+ {
+ usedHtmlCharset = userCharset;
+ return false; // only guessed value
+ }
+
+ // try WINDOWS-1252
+ if (checkConversion("WINDOWS-1252", htmlContent, converted))
+ {
+ usedHtmlCharset = "WINDOWS-1252";
+ return false; // only guessed value
+ }
+
+ // can't find exact charset
+ // use one of detected as fallback
+ if (!bomCharset.empty())
+ usedHtmlCharset = bomCharset;
+ else if (!serverReportedCharset.empty())
+ usedHtmlCharset = serverReportedCharset;
+ else if (!declaredCharset.empty())
+ usedHtmlCharset = declaredCharset;
+ else if (!userCharset.empty())
+ usedHtmlCharset = userCharset;
+ else
+ usedHtmlCharset = "WINDOWS-1252";
+
+ CLog::Log(LOGWARNING, "{}: Can't correctly convert to UTF-8 charset, converting as \"{}\"",
+ __FUNCTION__, usedHtmlCharset);
+ g_charsetConverter.ToUtf8(usedHtmlCharset, htmlContent, converted, false);
+
+ return false;
+}
+
+bool CCharsetDetection::ConvertPlainTextToUtf8(const std::string& textContent, std::string& converted, const std::string& serverReportedCharset, std::string& usedCharset)
+{
+ converted.clear();
+ usedCharset.clear();
+ if (textContent.empty())
+ {
+ usedCharset = "UTF-8"; // any charset can be used for empty content, use UTF-8 as default
+ return true;
+ }
+
+ // try to get charset from Byte Order Mark
+ std::string bomCharset(GetBomEncoding(textContent));
+ if (checkConversion(bomCharset, textContent, converted))
+ {
+ usedCharset = bomCharset;
+ return true;
+ }
+
+ // try charset from HTTP header (or from other out-of-band source)
+ if (checkConversion(serverReportedCharset, textContent, converted))
+ {
+ usedCharset = serverReportedCharset;
+ return true;
+ }
+
+ // try UTF-8 if not tried before
+ if (bomCharset != "UTF-8" && serverReportedCharset != "UTF-8" && checkConversion("UTF-8", textContent, converted))
+ {
+ usedCharset = "UTF-8";
+ return true;
+ }
+
+ // try user charset
+ std::string userCharset(g_langInfo.GetGuiCharSet());
+ if (checkConversion(userCharset, textContent, converted))
+ {
+ usedCharset = userCharset;
+ return true;
+ }
+
+ // try system default charset
+ if (g_charsetConverter.systemToUtf8(textContent, converted, true))
+ {
+ usedCharset = "char"; // synonym to system charset
+ return true;
+ }
+
+ // try WINDOWS-1252
+ if (checkConversion("WINDOWS-1252", textContent, converted))
+ {
+ usedCharset = "WINDOWS-1252";
+ return true;
+ }
+
+ // can't find correct charset
+ // use one of detected as fallback
+ if (!serverReportedCharset.empty())
+ usedCharset = serverReportedCharset;
+ else if (!bomCharset.empty())
+ usedCharset = bomCharset;
+ else if (!userCharset.empty())
+ usedCharset = userCharset;
+ else
+ usedCharset = "WINDOWS-1252";
+
+ CLog::Log(LOGWARNING, "{}: Can't correctly convert to UTF-8 charset, converting as \"{}\"",
+ __FUNCTION__, usedCharset);
+ g_charsetConverter.ToUtf8(usedCharset, textContent, converted, false);
+
+ return false;
+}
+
+
+bool CCharsetDetection::checkConversion(const std::string& srcCharset, const std::string& src, std::string& dst)
+{
+ if (srcCharset.empty())
+ return false;
+
+ if (srcCharset != "UTF-8")
+ {
+ if (g_charsetConverter.ToUtf8(srcCharset, src, dst, true))
+ return true;
+ }
+ else if (CUtf8Utils::isValidUtf8(src))
+ {
+ dst = src;
+ return true;
+ }
+
+ return false;
+}
+
+std::string CCharsetDetection::GetHtmlEncodingFromHead(const std::string& htmlContent)
+{
+ std::string smallerHtmlContent;
+ if (htmlContent.length() > 2 * m_HtmlCharsetEndSearchPos)
+ smallerHtmlContent.assign(htmlContent, 0, 2 * m_HtmlCharsetEndSearchPos); // use twice more bytes to search for charset for safety
+
+ const std::string& html = smallerHtmlContent.empty() ? htmlContent : smallerHtmlContent; // limit search
+ const char* const htmlC = html.c_str(); // for null-termination
+ const size_t len = html.length();
+
+ // this is an implementation of http://www.w3.org/TR/2013/CR-html5-20130806/single-page.html#prescan-a-byte-stream-to-determine-its-encoding
+ // labels in comments correspond to the labels in HTML5 standard
+ // note: opposite to standard, everything is converted to uppercase instead of lower case
+ size_t pos = 0;
+ while (pos < len) // "loop" label
+ {
+ if (html.compare(pos, 4, "<!--", 4) == 0)
+ {
+ pos = html.find("-->", pos + 2);
+ if (pos == std::string::npos)
+ return "";
+ pos += 2;
+ }
+ else if (htmlC[pos] == '<' && (htmlC[pos + 1] == 'm' || htmlC[pos + 1] == 'M') && (htmlC[pos + 2] == 'e' || htmlC[pos + 2] == 'E')
+ && (htmlC[pos + 3] == 't' || htmlC[pos + 3] == 'T') && (htmlC[pos + 4] == 'a' || htmlC[pos + 4] == 'A')
+ && (htmlC[pos + 5] == 0x09 || htmlC[pos + 5] == 0x0A || htmlC[pos + 5] == 0x0C || htmlC[pos + 5] == 0x0D || htmlC[pos + 5] == 0x20 || htmlC[pos + 5] == 0x2F))
+ { // this is case insensitive "<meta" and one of tab, LF, FF, CR, space or slash
+ pos += 5; // "pos" points to symbol after "<meta"
+ std::string attrName, attrValue;
+ bool gotPragma = false;
+ std::string contentCharset;
+ do // "attributes" label
+ {
+ pos = GetHtmlAttribute(html, pos, attrName, attrValue);
+ if (attrName == "HTTP-EQUIV" && attrValue == "CONTENT-TYPE")
+ gotPragma = true;
+ else if (attrName == "CONTENT")
+ contentCharset = ExtractEncodingFromHtmlMeta(attrValue);
+ else if (attrName == "CHARSET")
+ {
+ StringUtils::Trim(attrValue, m_HtmlWhitespaceChars.c_str()); // tab, LF, FF, CR, space
+ if (!attrValue.empty())
+ return attrValue;
+ }
+ } while (!attrName.empty() && pos < len);
+
+ // "processing" label
+ if (gotPragma && !contentCharset.empty())
+ return contentCharset;
+ }
+ else if (htmlC[pos] == '<' && ((htmlC[pos + 1] >= 'A' && htmlC[pos + 1] <= 'Z') || (htmlC[pos + 1] >= 'a' && htmlC[pos + 1] <= 'z')))
+ {
+ pos = html.find_first_of("\x09\x0A\x0C\x0D >", pos); // tab, LF, FF, CR, space or '>'
+ std::string attrName, attrValue;
+ do
+ {
+ pos = GetHtmlAttribute(html, pos, attrName, attrValue);
+ } while (pos < len && !attrName.empty());
+ }
+ else if (html.compare(pos, 2, "<!", 2) == 0 || html.compare(pos, 2, "</", 2) == 0 || html.compare(pos, 2, "<?", 2) == 0)
+ pos = html.find('>', pos);
+
+ if (pos == std::string::npos)
+ return "";
+
+ // "next byte" label
+ pos++;
+ }
+
+ return ""; // no charset was found
+}
+
+size_t CCharsetDetection::GetHtmlAttribute(const std::string& htmlContent, size_t pos, std::string& attrName, std::string& attrValue)
+{
+ attrName.clear();
+ attrValue.clear();
+ static const char* const htmlWhitespaceSlash = "\x09\x0A\x0C\x0D\x20\x2F"; // tab, LF, FF, CR, space or slash
+ const char* const htmlC = htmlContent.c_str();
+ const size_t len = htmlContent.length();
+
+ // this is an implementation of http://www.w3.org/TR/2013/CR-html5-20130806/single-page.html#concept-get-attributes-when-sniffing
+ // labels in comments correspond to the labels in HTML5 standard
+ // note: opposite to standard, everything is converted to uppercase instead of lower case
+ pos = htmlContent.find_first_not_of(htmlWhitespaceSlash, pos);
+ if (pos == std::string::npos || htmlC[pos] == '>')
+ return pos; // only white spaces or slashes up to the end of the htmlContent or no more attributes
+
+ while (pos < len && htmlC[pos] != '=')
+ {
+ const char chr = htmlC[pos];
+ if (chr == '/' || chr == '>')
+ return pos; // no attributes or empty attribute value
+ else if (m_HtmlWhitespaceChars.find(chr) != std::string::npos) // chr is one of whitespaces
+ {
+ pos = htmlContent.find_first_not_of(m_HtmlWhitespaceChars, pos); // "spaces" label
+ if (pos == std::string::npos || htmlC[pos] != '=')
+ return pos; // only white spaces up to the end or no attribute value
+ break;
+ }
+ else
+ appendCharAsAsciiUpperCase(attrName, chr);
+
+ pos++;
+ }
+
+ if (pos >= len)
+ return std::string::npos; // no '=', '/' or '>' were found up to the end of htmlContent
+
+ pos++; // advance pos to character after '='
+
+ pos = htmlContent.find_first_not_of(m_HtmlWhitespaceChars, pos); // "value" label
+ if (pos == std::string::npos)
+ return pos; // only white spaces remain in htmlContent
+
+ if (htmlC[pos] == '>')
+ return pos; // empty attribute value
+ else if (htmlC[pos] == '"' || htmlC[pos] == '\'')
+ {
+ const char qChr = htmlC[pos];
+ // "quote loop" label
+ while (++pos < len)
+ {
+ const char chr = htmlC[pos];
+ if (chr == qChr)
+ return pos + 1;
+ else
+ appendCharAsAsciiUpperCase(attrValue, chr);
+ }
+ return std::string::npos; // no closing quote is found
+ }
+
+ appendCharAsAsciiUpperCase(attrValue, htmlC[pos]);
+ pos++;
+
+ while (pos < len)
+ {
+ const char chr = htmlC[pos];
+ if (m_HtmlWhitespaceChars.find(chr) != std::string::npos || chr == '>')
+ return pos;
+ else
+ appendCharAsAsciiUpperCase(attrValue, chr);
+
+ pos++;
+ }
+
+ return std::string::npos; // rest of htmlContent was attribute value
+}
+
+std::string CCharsetDetection::ExtractEncodingFromHtmlMeta(const std::string& metaContent,
+ size_t pos /*= 0*/)
+{
+ size_t len = metaContent.length();
+ if (pos >= len)
+ return "";
+
+ const char* const metaContentC = metaContent.c_str();
+
+ // this is an implementation of http://www.w3.org/TR/2013/CR-html5-20130806/single-page.html#algorithm-for-extracting-a-character-encoding-from-a-meta-element
+ // labels in comments correspond to the labels in HTML5 standard
+ // note: opposite to standard, case sensitive match is used as argument is always in uppercase
+ std::string charset;
+ do
+ {
+ // "loop" label
+ pos = metaContent.find("CHARSET", pos);
+ if (pos == std::string::npos)
+ return "";
+
+ pos = metaContent.find_first_not_of(m_HtmlWhitespaceChars, pos + 7); // '7' is the length of 'CHARSET'
+ if (pos != std::string::npos && metaContentC[pos] == '=')
+ {
+ pos = metaContent.find_first_not_of(m_HtmlWhitespaceChars, pos + 1);
+ if (pos != std::string::npos)
+ {
+ if (metaContentC[pos] == '\'' || metaContentC[pos] == '"')
+ {
+ const char qChr = metaContentC[pos];
+ pos++;
+ const size_t closeQpos = metaContent.find(qChr, pos);
+ if (closeQpos != std::string::npos)
+ charset.assign(metaContent, pos, closeQpos - pos);
+ }
+ else
+ charset.assign(metaContent, pos, metaContent.find("\x09\x0A\x0C\x0D ;", pos) - pos); // assign content up to the next tab, LF, FF, CR, space, semicolon or end of string
+ }
+ break;
+ }
+ } while (pos < len);
+
+ static const char* const htmlWhitespaceCharsC = m_HtmlWhitespaceChars.c_str();
+ StringUtils::Trim(charset, htmlWhitespaceCharsC);
+
+ return charset;
+}
+
+inline void CCharsetDetection::appendCharAsAsciiUpperCase(std::string& str, const char chr)
+{
+ if (chr >= 'a' && chr <= 'z')
+ str.push_back(chr - ('a' - 'A')); // convert to upper case
+ else
+ str.push_back(chr);
+}
diff --git a/xbmc/utils/CharsetDetection.h b/xbmc/utils/CharsetDetection.h
new file mode 100644
index 0000000..d1b9ba9
--- /dev/null
+++ b/xbmc/utils/CharsetDetection.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2013-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 <string>
+
+
+class CCharsetDetection
+{
+public:
+ /**
+ * Detect text encoding by Byte Order Mark
+ * Multibyte encodings (UTF-16/32) always ends with explicit endianness (LE/BE)
+ * @param content pointer to text to analyze
+ * @param contentLength length of text
+ * @return detected encoding or empty string if BOM not detected
+ */
+ static std::string GetBomEncoding(const char* const content, const size_t contentLength);
+ /**
+ * Detect text encoding by Byte Order Mark
+ * Multibyte encodings (UTF-16/32) always ends with explicit endianness (LE/BE)
+ * @param content the text to analyze
+ * @return detected encoding or empty string if BOM not detected
+ */
+ static inline std::string GetBomEncoding(const std::string& content)
+ { return GetBomEncoding(content.c_str(), content.length()); }
+
+ static inline bool DetectXmlEncoding(const std::string& xmlContent, std::string& detectedEncoding)
+ { return DetectXmlEncoding(xmlContent.c_str(), xmlContent.length(), detectedEncoding); }
+
+ static bool DetectXmlEncoding(const char* const xmlContent, const size_t contentLength, std::string& detectedEncoding);
+
+ /**
+ * Detect HTML charset and HTML convert to UTF-8
+ * @param htmlContent content of HTML file
+ * @param converted receive result of conversion
+ * @param serverReportedCharset charset from HTTP header or from other out-of-band source, empty if unknown or unset
+ * @return true if charset is properly detected and HTML is correctly converted, false if charset is only guessed
+ */
+ static inline bool ConvertHtmlToUtf8(const std::string& htmlContent, std::string& converted, const std::string& serverReportedCharset = "")
+ {
+ std::string usedHtmlCharset;
+ return ConvertHtmlToUtf8(htmlContent, converted, serverReportedCharset, usedHtmlCharset);
+ }
+ /**
+ * Detect HTML charset and HTML convert to UTF-8
+ * @param htmlContent content of HTML file
+ * @param converted receive result of conversion
+ * @param serverReportedCharset charset from HTTP header or from other out-of-band source, empty if unknown or unset
+ * @param usedHtmlCharset receive charset used for conversion
+ * @return true if charset is properly detected and HTML is correctly converted, false if charset is only guessed
+ */
+ static bool ConvertHtmlToUtf8(const std::string& htmlContent, std::string& converted, const std::string& serverReportedCharset, std::string& usedHtmlCharset);
+
+ /**
+ * Try to convert plain text to UTF-8 using best suitable charset
+ * @param textContent text to convert
+ * @param converted receive result of conversion
+ * @param serverReportedCharset charset from HTTP header or from other out-of-band source, empty if unknown or unset
+ * @param usedCharset receive charset used for conversion
+ * @return true if converted without errors, false otherwise
+ */
+ static bool ConvertPlainTextToUtf8(const std::string& textContent, std::string& converted, const std::string& serverReportedCharset, std::string& usedCharset);
+
+private:
+ static bool GetXmlEncodingFromDeclaration(const char* const xmlContent, const size_t contentLength, std::string& declaredEncoding);
+ /**
+ * Try to guess text encoding by searching for '<?xml' mark in different encodings
+ * Multibyte encodings (UTF/UCS) always ends with explicit endianness (LE/BE)
+ * @param content pointer to text to analyze
+ * @param contentLength length of text
+ * @param detectedEncoding reference to variable that receive supposed encoding
+ * @return true if any encoding supposed, false otherwise
+ */
+ static bool GuessXmlEncoding(const char* const xmlContent, const size_t contentLength, std::string& supposedEncoding);
+
+ static std::string GetHtmlEncodingFromHead(const std::string& htmlContent);
+ static size_t GetHtmlAttribute(const std::string& htmlContent, size_t pos, std::string& atrName, std::string& strValue);
+ static std::string ExtractEncodingFromHtmlMeta(const std::string& metaContent, size_t pos = 0);
+
+ static bool checkConversion(const std::string& srcCharset, const std::string& src, std::string& dst);
+ static void appendCharAsAsciiUpperCase(std::string& str, const char chr);
+
+ static const size_t m_XmlDeclarationMaxLength;
+ static const size_t m_HtmlCharsetEndSearchPos;
+
+ static const std::string m_HtmlWhitespaceChars;
+};
diff --git a/xbmc/utils/ColorUtils.cpp b/xbmc/utils/ColorUtils.cpp
new file mode 100644
index 0000000..d153bc4
--- /dev/null
+++ b/xbmc/utils/ColorUtils.cpp
@@ -0,0 +1,150 @@
+/*
+ * 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 "ColorUtils.h"
+
+#include "StringUtils.h"
+
+#include <algorithm>
+#include <cmath>
+#include <cstdio>
+
+using namespace UTILS::COLOR;
+
+namespace
+{
+
+void GetHSLValues(ColorInfo& colorInfo)
+{
+ double r = (colorInfo.colorARGB & 0x00FF0000) >> 16;
+ double g = (colorInfo.colorARGB & 0x0000FF00) >> 8;
+ double b = (colorInfo.colorARGB & 0x000000FF);
+ r /= 255;
+ g /= 255;
+ b /= 255;
+ const double& maxVal = std::max<double>({r, g, b});
+ const double& minVal = std::min<double>({r, g, b});
+ double h = 0;
+ double s = 0;
+ double l = (minVal + maxVal) / 2;
+ double d = maxVal - minVal;
+
+ if (d == 0)
+ {
+ h = s = 0; // achromatic
+ }
+ else
+ {
+ s = l > 0.5 ? d / (2 - maxVal - minVal) : d / (maxVal + minVal);
+ if (maxVal == r)
+ {
+ h = (g - b) / d + (g < b ? 6 : 0);
+ }
+ else if (maxVal == g)
+ {
+ h = (b - r) / d + 2;
+ }
+ else if (maxVal == b)
+ {
+ h = (r - g) / d + 4;
+ }
+ h /= 6;
+ }
+
+ colorInfo.hue = h;
+ colorInfo.saturation = s;
+ colorInfo.lightness = l;
+}
+
+} // unnamed namespace
+
+Color UTILS::COLOR::ChangeOpacity(const Color argb, const float opacity)
+{
+ int newAlpha = static_cast<int>(std::ceil(((argb >> 24) & 0xff) * opacity));
+ return (argb & 0x00FFFFFF) | (newAlpha << 24);
+};
+
+Color UTILS::COLOR::ConvertToRGBA(const Color argb)
+{
+ return ((argb & 0x00FF0000) << 8) | //RR______
+ ((argb & 0x0000FF00) << 8) | //__GG____
+ ((argb & 0x000000FF) << 8) | //____BB__
+ ((argb & 0xFF000000) >> 24); //______AA
+}
+
+Color UTILS::COLOR::ConvertToARGB(const Color rgba)
+{
+ return ((rgba & 0x000000FF) << 24) | //AA_____
+ ((rgba & 0xFF000000) >> 8) | //__RR____
+ ((rgba & 0x00FF0000) >> 8) | //____GG__
+ ((rgba & 0x0000FF00) >> 8); //______BB
+}
+
+Color UTILS::COLOR::ConvertToBGR(const Color argb)
+{
+ return (argb & 0x00FF0000) >> 16 | //____RR
+ (argb & 0x0000FF00) | //__GG__
+ (argb & 0x000000FF) << 16; //BB____
+}
+
+Color UTILS::COLOR::ConvertHexToColor(const std::string& hexColor)
+{
+ Color value = 0;
+ std::sscanf(hexColor.c_str(), "%x", &value);
+ return value;
+}
+
+Color UTILS::COLOR::ConvertIntToRGB(int r, int g, int b)
+{
+ return ((r & 0xff) << 16) + ((g & 0xff) << 8) + (b & 0xff);
+}
+
+ColorInfo UTILS::COLOR::MakeColorInfo(const Color& argb)
+{
+ ColorInfo colorInfo;
+ colorInfo.colorARGB = argb;
+ GetHSLValues(colorInfo);
+ return colorInfo;
+}
+
+ColorInfo UTILS::COLOR::MakeColorInfo(const std::string& hexColor)
+{
+ ColorInfo colorInfo;
+ colorInfo.colorARGB = ConvertHexToColor(hexColor);
+ GetHSLValues(colorInfo);
+ return colorInfo;
+}
+
+bool UTILS::COLOR::comparePairColorInfo(const std::pair<std::string, ColorInfo>& a,
+ const std::pair<std::string, ColorInfo>& b)
+{
+ if (a.second.hue == b.second.hue)
+ {
+ if (a.second.saturation == b.second.saturation)
+ return (a.second.lightness < b.second.lightness);
+ else
+ return (a.second.saturation < b.second.saturation);
+ }
+ else
+ return (a.second.hue < b.second.hue);
+}
+
+ColorFloats UTILS::COLOR::ConvertToFloats(const Color argb)
+{
+ ColorFloats c;
+ c.alpha = static_cast<float>((argb >> 24) & 0xFF) * (1.0f / 255.0f);
+ c.red = static_cast<float>((argb >> 16) & 0xFF) * (1.0f / 255.0f);
+ c.green = static_cast<float>((argb >> 8) & 0xFF) * (1.0f / 255.0f);
+ c.blue = static_cast<float>(argb & 0xFF) * (1.0f / 255.0f);
+ return c;
+}
+
+std::string UTILS::COLOR::ConvertoToHexRGB(const Color argb)
+{
+ return StringUtils::Format("{:06X}", argb & ~0xFF000000);
+}
diff --git a/xbmc/utils/ColorUtils.h b/xbmc/utils/ColorUtils.h
new file mode 100644
index 0000000..f157835
--- /dev/null
+++ b/xbmc/utils/ColorUtils.h
@@ -0,0 +1,167 @@
+/*
+ * 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 "utils/Map.h"
+
+#include <cstdint>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace UTILS
+{
+namespace COLOR
+{
+
+typedef uint32_t Color;
+
+// Custom colors
+
+constexpr Color NONE = 0x00000000;
+constexpr Color LIMITED_BLACK = 0xFF101010;
+
+// W3C HTML color list
+
+constexpr Color WHITE = 0xFFFFFFFF;
+constexpr Color SILVER = 0xFFC0C0C0;
+constexpr Color GRAY = 0xFF808080;
+constexpr Color BLACK = 0xFF000000;
+constexpr Color RED = 0xFFFF0000;
+constexpr Color MAROON = 0xFF800000;
+constexpr Color YELLOW = 0xFFFFFF00;
+constexpr Color OLIVE = 0xFF808000;
+constexpr Color LIME = 0xFF00FF00;
+constexpr Color GREEN = 0xFF008000;
+constexpr Color AQUA = 0xFF00FFFF;
+constexpr Color TEAL = 0xFF008080;
+constexpr Color BLUE = 0xFF0000FF;
+constexpr Color NAVY = 0xFF000080;
+constexpr Color FUCHSIA = 0xFFFF00FF;
+constexpr Color PURPLE = 0xFF800080;
+
+struct ColorInfo
+{
+ Color colorARGB;
+ double hue;
+ double saturation;
+ double lightness;
+};
+
+struct ColorFloats
+{
+ float red;
+ float green;
+ float blue;
+ float alpha;
+};
+
+//! \brief W3C HTML 16 basic color list
+constexpr auto HTML_BASIC_COLORS = make_map<std::string_view, Color>({{"white", WHITE},
+ {"silver", SILVER},
+ {"gray", GRAY},
+ {"black", BLACK},
+ {"red", RED},
+ {"maroon", MAROON},
+ {"yellow", YELLOW},
+ {"olive", OLIVE},
+ {"lime", LIME},
+ {"green", GREEN},
+ {"aqua", AQUA},
+ {"teal", TEAL},
+ {"blue", BLUE},
+ {"navy", NAVY},
+ {"fuchsia", FUCHSIA},
+ {"purple", PURPLE}});
+
+/*!
+ * \brief Change the opacity of a given ARGB color
+ * \param color The original color
+ * \param opacity The opacity value as a float
+ * \return the original color with the changed opacity/alpha value
+ */
+Color ChangeOpacity(const Color argb, const float opacity);
+
+/*!
+ * \brief Convert given ARGB color to RGBA color value
+ * \param color The original color
+ * \return the original color converted to RGBA value
+ */
+Color ConvertToRGBA(const Color argb);
+
+/*!
+ * \brief Convert given RGBA color to ARGB color value
+ * \param color The original color
+ * \return the original color converted to ARGB value
+ */
+Color ConvertToARGB(const Color rgba);
+
+/*!
+ * \brief Convert given ARGB color to BGR color value
+ * \param color The original color
+ * \return the original color converted to BGR value
+*/
+Color ConvertToBGR(const Color argb);
+
+/*!
+ * \brief Convert given hex value to Color value
+ * \param hexColor The original hex color
+ * \return the original hex color converted to Color value
+ */
+Color ConvertHexToColor(const std::string& hexColor);
+
+/*!
+ * \brief Convert given RGB int values to RGB color value
+ * \param r The red value
+ * \param g The green value
+ * \param b The blue value
+ * \return the color as RGB value
+ */
+Color ConvertIntToRGB(int r, int g, int b);
+
+/*!
+ * \brief Create a ColorInfo from an ARGB Color to
+ * get additional information of the color
+ * and allow to be sorted with a color comparer
+ * \param argb The original ARGB color
+ * \return the ColorInfo
+ */
+ColorInfo MakeColorInfo(const Color& argb);
+
+/*!
+ * \brief Create a ColorInfo from an HEX color value to
+ * get additional information of the color
+ * and allow to be sorted with a color comparer
+ * \param hexColor The original ARGB color
+ * \return the ColorInfo
+ */
+ColorInfo MakeColorInfo(const std::string& hexColor);
+
+/*!
+ * \brief Comparer for pair string/ColorInfo to sort colors in a hue scale
+ */
+bool comparePairColorInfo(const std::pair<std::string, ColorInfo>& a,
+ const std::pair<std::string, ColorInfo>& b);
+
+/*!
+ * \brief Convert given ARGB color to ColorFloats
+ * \param color The original color
+ * \return the original color converted to ColorFloats
+ */
+ColorFloats ConvertToFloats(const Color argb);
+
+/*!
+ * \brief Convert given ARGB color to hex RGB color value
+ * \param color The original color
+ * \return The original color converted to hex RGB
+ */
+std::string ConvertoToHexRGB(const Color argb);
+
+} // namespace COLOR
+} // namespace UTILS
diff --git a/xbmc/utils/ComponentContainer.h b/xbmc/utils/ComponentContainer.h
new file mode 100644
index 0000000..3aa826a
--- /dev/null
+++ b/xbmc/utils/ComponentContainer.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 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 <cstddef>
+#include <memory>
+#include <mutex>
+#include <stdexcept>
+#include <typeindex>
+#include <unordered_map>
+#include <utility>
+
+//! \brief A generic container for components.
+//! \details A component has to be derived from the BaseType.
+//! Only a single instance of each derived type can be registered.
+//! Intended use is through inheritance.
+template<class BaseType>
+class CComponentContainer
+{
+public:
+ //! \brief Obtain a component.
+ template<class T>
+ std::shared_ptr<T> GetComponent()
+ {
+ return std::const_pointer_cast<T>(std::as_const(*this).template GetComponent<T>());
+ }
+
+ //! \brief Obtain a component.
+ template<class T>
+ std::shared_ptr<const T> GetComponent() const
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ const auto it = m_components.find(std::type_index(typeid(T)));
+ if (it != m_components.end())
+ return std::static_pointer_cast<const T>((*it).second);
+
+ throw std::logic_error("ComponentContainer: Attempt to obtain non-existent component");
+ }
+
+ //! \brief Returns number of registered components.
+ std::size_t size() const { return m_components.size(); }
+
+protected:
+ //! \brief Register a new component instance.
+ void RegisterComponent(const std::shared_ptr<BaseType>& component)
+ {
+ if (!component)
+ return;
+
+ // Note: Extra var needed to avoid clang warning
+ // "Expression with side effects will be evaluated despite being used as an operand to 'typeid'"
+ // https://stackoverflow.com/questions/46494928/clang-warning-on-expression-side-effects
+ const auto& componentRef = *component;
+
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_components.insert({std::type_index(typeid(componentRef)), component});
+ }
+
+ //! \brief Deregister a component.
+ void DeregisterComponent(const std::type_info& typeInfo)
+ {
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_components.erase(typeInfo);
+ }
+
+private:
+ mutable CCriticalSection m_critSection; //!< Critical section for map updates
+ std::unordered_map<std::type_index, std::shared_ptr<BaseType>>
+ m_components; //!< Map of components
+};
diff --git a/xbmc/utils/ContentUtils.cpp b/xbmc/utils/ContentUtils.cpp
new file mode 100644
index 0000000..b0ebc67
--- /dev/null
+++ b/xbmc/utils/ContentUtils.cpp
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2005-2020 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 "ContentUtils.h"
+
+#include "FileItem.h"
+#include "utils/StringUtils.h"
+#include "video/VideoInfoTag.h"
+
+namespace
+{
+bool HasPreferredArtType(const CFileItem& item)
+{
+ return item.HasVideoInfoTag() && (item.GetVideoInfoTag()->m_type == MediaTypeMovie ||
+ item.GetVideoInfoTag()->m_type == MediaTypeTvShow ||
+ item.GetVideoInfoTag()->m_type == MediaTypeSeason ||
+ item.GetVideoInfoTag()->m_type == MediaTypeVideoCollection);
+}
+
+std::string GetPreferredArtType(const MediaType& type)
+{
+ if (type == MediaTypeMovie || type == MediaTypeTvShow || type == MediaTypeSeason ||
+ type == MediaTypeVideoCollection)
+ {
+ return "poster";
+ }
+ return "thumb";
+}
+} // namespace
+
+const std::string ContentUtils::GetPreferredArtImage(const CFileItem& item)
+{
+ if (HasPreferredArtType(item))
+ {
+ auto preferredArtType = GetPreferredArtType(item.GetVideoInfoTag()->m_type);
+ if (item.HasArt(preferredArtType))
+ {
+ return item.GetArt(preferredArtType);
+ }
+ }
+ return item.GetArt("thumb");
+}
+
+std::unique_ptr<CFileItem> ContentUtils::GeneratePlayableTrailerItem(const CFileItem& item,
+ const std::string& label)
+{
+ std::unique_ptr<CFileItem> trailerItem = std::make_unique<CFileItem>();
+ trailerItem->SetPath(item.GetVideoInfoTag()->m_strTrailer);
+ CVideoInfoTag* videoInfoTag = trailerItem->GetVideoInfoTag();
+ *videoInfoTag = *item.GetVideoInfoTag();
+ videoInfoTag->m_streamDetails.Reset();
+ videoInfoTag->m_strTitle = StringUtils::Format("{} ({})", videoInfoTag->m_strTitle, label);
+ trailerItem->SetArt(item.GetArt());
+ videoInfoTag->m_iDbId = -1;
+ videoInfoTag->m_iFileId = -1;
+ return trailerItem;
+}
diff --git a/xbmc/utils/ContentUtils.h b/xbmc/utils/ContentUtils.h
new file mode 100644
index 0000000..b9037a7
--- /dev/null
+++ b/xbmc/utils/ContentUtils.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2005-2020 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 "media/MediaType.h"
+
+#include <memory>
+#include <string>
+
+class CFileItem;
+
+class ContentUtils
+{
+public:
+ /*! \brief Gets the preferred art image for a given item (depending on its media type). Provided a CFileItem
+ with many art types on its art map, this method will return a default preferred image (content dependent) for situations where the
+ the client expects a single image to represent the item. For instance, for movies and shows this will return the poster, while for
+ episodes it will return the thumb.
+ \param item The CFileItem to process
+ \return the preferred art image
+ */
+ static const std::string GetPreferredArtImage(const CFileItem& item);
+
+ /*! \brief Gets a trailer playable file item for a given item. Essentially this creates a new item which contains
+ the original item infotag and has the playable path and label changed.
+ \param item The CFileItem to process
+ \param label The label for the new item
+ \return a pointer to the trailer item
+ */
+ static std::unique_ptr<CFileItem> GeneratePlayableTrailerItem(const CFileItem& item,
+ const std::string& label);
+};
diff --git a/xbmc/utils/Crc32.cpp b/xbmc/utils/Crc32.cpp
new file mode 100644
index 0000000..e10f9c9
--- /dev/null
+++ b/xbmc/utils/Crc32.cpp
@@ -0,0 +1,110 @@
+/*
+ * 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 "Crc32.h"
+
+#include "utils/StringUtils.h"
+
+uint32_t crc_tab[256] =
+{
+ 0x00000000L, 0x04C11DB7L, 0x09823B6EL, 0x0D4326D9L,
+ 0x130476DCL, 0x17C56B6BL, 0x1A864DB2L, 0x1E475005L,
+ 0x2608EDB8L, 0x22C9F00FL, 0x2F8AD6D6L, 0x2B4BCB61L,
+ 0x350C9B64L, 0x31CD86D3L, 0x3C8EA00AL, 0x384FBDBDL,
+ 0x4C11DB70L, 0x48D0C6C7L, 0x4593E01EL, 0x4152FDA9L,
+ 0x5F15ADACL, 0x5BD4B01BL, 0x569796C2L, 0x52568B75L,
+ 0x6A1936C8L, 0x6ED82B7FL, 0x639B0DA6L, 0x675A1011L,
+ 0x791D4014L, 0x7DDC5DA3L, 0x709F7B7AL, 0x745E66CDL,
+ 0x9823B6E0L, 0x9CE2AB57L, 0x91A18D8EL, 0x95609039L,
+ 0x8B27C03CL, 0x8FE6DD8BL, 0x82A5FB52L, 0x8664E6E5L,
+ 0xBE2B5B58L, 0xBAEA46EFL, 0xB7A96036L, 0xB3687D81L,
+ 0xAD2F2D84L, 0xA9EE3033L, 0xA4AD16EAL, 0xA06C0B5DL,
+ 0xD4326D90L, 0xD0F37027L, 0xDDB056FEL, 0xD9714B49L,
+ 0xC7361B4CL, 0xC3F706FBL, 0xCEB42022L, 0xCA753D95L,
+ 0xF23A8028L, 0xF6FB9D9FL, 0xFBB8BB46L, 0xFF79A6F1L,
+ 0xE13EF6F4L, 0xE5FFEB43L, 0xE8BCCD9AL, 0xEC7DD02DL,
+ 0x34867077L, 0x30476DC0L, 0x3D044B19L, 0x39C556AEL,
+ 0x278206ABL, 0x23431B1CL, 0x2E003DC5L, 0x2AC12072L,
+ 0x128E9DCFL, 0x164F8078L, 0x1B0CA6A1L, 0x1FCDBB16L,
+ 0x018AEB13L, 0x054BF6A4L, 0x0808D07DL, 0x0CC9CDCAL,
+ 0x7897AB07L, 0x7C56B6B0L, 0x71159069L, 0x75D48DDEL,
+ 0x6B93DDDBL, 0x6F52C06CL, 0x6211E6B5L, 0x66D0FB02L,
+ 0x5E9F46BFL, 0x5A5E5B08L, 0x571D7DD1L, 0x53DC6066L,
+ 0x4D9B3063L, 0x495A2DD4L, 0x44190B0DL, 0x40D816BAL,
+ 0xACA5C697L, 0xA864DB20L, 0xA527FDF9L, 0xA1E6E04EL,
+ 0xBFA1B04BL, 0xBB60ADFCL, 0xB6238B25L, 0xB2E29692L,
+ 0x8AAD2B2FL, 0x8E6C3698L, 0x832F1041L, 0x87EE0DF6L,
+ 0x99A95DF3L, 0x9D684044L, 0x902B669DL, 0x94EA7B2AL,
+ 0xE0B41DE7L, 0xE4750050L, 0xE9362689L, 0xEDF73B3EL,
+ 0xF3B06B3BL, 0xF771768CL, 0xFA325055L, 0xFEF34DE2L,
+ 0xC6BCF05FL, 0xC27DEDE8L, 0xCF3ECB31L, 0xCBFFD686L,
+ 0xD5B88683L, 0xD1799B34L, 0xDC3ABDEDL, 0xD8FBA05AL,
+ 0x690CE0EEL, 0x6DCDFD59L, 0x608EDB80L, 0x644FC637L,
+ 0x7A089632L, 0x7EC98B85L, 0x738AAD5CL, 0x774BB0EBL,
+ 0x4F040D56L, 0x4BC510E1L, 0x46863638L, 0x42472B8FL,
+ 0x5C007B8AL, 0x58C1663DL, 0x558240E4L, 0x51435D53L,
+ 0x251D3B9EL, 0x21DC2629L, 0x2C9F00F0L, 0x285E1D47L,
+ 0x36194D42L, 0x32D850F5L, 0x3F9B762CL, 0x3B5A6B9BL,
+ 0x0315D626L, 0x07D4CB91L, 0x0A97ED48L, 0x0E56F0FFL,
+ 0x1011A0FAL, 0x14D0BD4DL, 0x19939B94L, 0x1D528623L,
+ 0xF12F560EL, 0xF5EE4BB9L, 0xF8AD6D60L, 0xFC6C70D7L,
+ 0xE22B20D2L, 0xE6EA3D65L, 0xEBA91BBCL, 0xEF68060BL,
+ 0xD727BBB6L, 0xD3E6A601L, 0xDEA580D8L, 0xDA649D6FL,
+ 0xC423CD6AL, 0xC0E2D0DDL, 0xCDA1F604L, 0xC960EBB3L,
+ 0xBD3E8D7EL, 0xB9FF90C9L, 0xB4BCB610L, 0xB07DABA7L,
+ 0xAE3AFBA2L, 0xAAFBE615L, 0xA7B8C0CCL, 0xA379DD7BL,
+ 0x9B3660C6L, 0x9FF77D71L, 0x92B45BA8L, 0x9675461FL,
+ 0x8832161AL, 0x8CF30BADL, 0x81B02D74L, 0x857130C3L,
+ 0x5D8A9099L, 0x594B8D2EL, 0x5408ABF7L, 0x50C9B640L,
+ 0x4E8EE645L, 0x4A4FFBF2L, 0x470CDD2BL, 0x43CDC09CL,
+ 0x7B827D21L, 0x7F436096L, 0x7200464FL, 0x76C15BF8L,
+ 0x68860BFDL, 0x6C47164AL, 0x61043093L, 0x65C52D24L,
+ 0x119B4BE9L, 0x155A565EL, 0x18197087L, 0x1CD86D30L,
+ 0x029F3D35L, 0x065E2082L, 0x0B1D065BL, 0x0FDC1BECL,
+ 0x3793A651L, 0x3352BBE6L, 0x3E119D3FL, 0x3AD08088L,
+ 0x2497D08DL, 0x2056CD3AL, 0x2D15EBE3L, 0x29D4F654L,
+ 0xC5A92679L, 0xC1683BCEL, 0xCC2B1D17L, 0xC8EA00A0L,
+ 0xD6AD50A5L, 0xD26C4D12L, 0xDF2F6BCBL, 0xDBEE767CL,
+ 0xE3A1CBC1L, 0xE760D676L, 0xEA23F0AFL, 0xEEE2ED18L,
+ 0xF0A5BD1DL, 0xF464A0AAL, 0xF9278673L, 0xFDE69BC4L,
+ 0x89B8FD09L, 0x8D79E0BEL, 0x803AC667L, 0x84FBDBD0L,
+ 0x9ABC8BD5L, 0x9E7D9662L, 0x933EB0BBL, 0x97FFAD0CL,
+ 0xAFB010B1L, 0xAB710D06L, 0xA6322BDFL, 0xA2F33668L,
+ 0xBCB4666DL, 0xB8757BDAL, 0xB5365D03L, 0xB1F740B4L
+};
+
+Crc32::Crc32()
+{
+ Reset();
+}
+
+void Crc32::Reset()
+{
+ m_crc = 0xFFFFFFFF;
+}
+
+void Crc32::Compute(const char* buffer, size_t count)
+{
+ while (count--)
+ m_crc = (m_crc << 8) ^ crc_tab[((m_crc >> 24) ^ *buffer++) & 0xFF];
+}
+
+uint32_t Crc32::Compute(const std::string& strValue)
+{
+ Crc32 crc;
+ crc.Compute(strValue.c_str(), strValue.size());
+ return crc;
+}
+
+uint32_t Crc32::ComputeFromLowerCase(const std::string& strValue)
+{
+ std::string strLower = strValue;
+ StringUtils::ToLower(strLower);
+ return Compute(strLower);
+}
+
diff --git a/xbmc/utils/Crc32.h b/xbmc/utils/Crc32.h
new file mode 100644
index 0000000..f4a3588
--- /dev/null
+++ b/xbmc/utils/Crc32.h
@@ -0,0 +1,31 @@
+/*
+ * 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 <stdint.h>
+#include <string>
+
+class Crc32
+{
+public:
+ Crc32();
+ void Reset();
+ void Compute(const char* buffer, size_t count);
+ static uint32_t Compute(const std::string& strValue);
+ static uint32_t ComputeFromLowerCase(const std::string& strValue);
+
+ operator uint32_t () const
+ {
+ return m_crc;
+ }
+
+private:
+ uint32_t m_crc;
+};
+
diff --git a/xbmc/utils/DMAHeapBufferObject.cpp b/xbmc/utils/DMAHeapBufferObject.cpp
new file mode 100644
index 0000000..ad05aa8
--- /dev/null
+++ b/xbmc/utils/DMAHeapBufferObject.cpp
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2005-2020 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 "DMAHeapBufferObject.h"
+
+#include "ServiceBroker.h"
+#include "utils/BufferObjectFactory.h"
+#include "utils/log.h"
+
+#include <array>
+
+#include <drm_fourcc.h>
+#include <fcntl.h>
+#include <linux/dma-heap.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+namespace
+{
+
+std::array<const char*, 3> DMA_HEAP_PATHS = {
+ "/dev/dma_heap/reserved",
+ "/dev/dma_heap/linux,cma",
+ "/dev/dma_heap/system",
+};
+
+static const char* DMA_HEAP_PATH;
+
+} // namespace
+
+std::unique_ptr<CBufferObject> CDMAHeapBufferObject::Create()
+{
+ return std::make_unique<CDMAHeapBufferObject>();
+}
+
+void CDMAHeapBufferObject::Register()
+{
+ for (auto path : DMA_HEAP_PATHS)
+ {
+ int fd = open(path, O_RDWR);
+ if (fd < 0)
+ {
+ CLog::Log(LOGDEBUG, "CDMAHeapBufferObject::{} unable to open {}: {}", __FUNCTION__, path,
+ strerror(errno));
+ continue;
+ }
+
+ close(fd);
+ DMA_HEAP_PATH = path;
+ break;
+ }
+
+ if (!DMA_HEAP_PATH)
+ return;
+
+ CLog::Log(LOGDEBUG, "CDMAHeapBufferObject::{} - using {}", __FUNCTION__, DMA_HEAP_PATH);
+
+ CBufferObjectFactory::RegisterBufferObject(CDMAHeapBufferObject::Create);
+}
+
+CDMAHeapBufferObject::~CDMAHeapBufferObject()
+{
+ ReleaseMemory();
+ DestroyBufferObject();
+
+ close(m_dmaheapfd);
+ m_dmaheapfd = -1;
+}
+
+bool CDMAHeapBufferObject::CreateBufferObject(uint32_t format, uint32_t width, uint32_t height)
+{
+ if (m_fd >= 0)
+ return true;
+
+ uint32_t bpp{1};
+
+ switch (format)
+ {
+ case DRM_FORMAT_ARGB8888:
+ bpp = 4;
+ break;
+ case DRM_FORMAT_ARGB1555:
+ case DRM_FORMAT_RGB565:
+ bpp = 2;
+ break;
+ default:
+ throw std::runtime_error("CDMAHeapBufferObject: pixel format not implemented");
+ }
+
+ m_stride = width * bpp;
+
+ return CreateBufferObject(width * height * bpp);
+}
+
+bool CDMAHeapBufferObject::CreateBufferObject(uint64_t size)
+{
+ m_size = size;
+
+ if (m_dmaheapfd < 0)
+ {
+ m_dmaheapfd = open(DMA_HEAP_PATH, O_RDWR);
+ if (m_dmaheapfd < 0)
+ {
+ CLog::LogF(LOGERROR, "failed to open {}:", DMA_HEAP_PATH, strerror(errno));
+ return false;
+ }
+ }
+
+ struct dma_heap_allocation_data allocData{};
+ allocData.len = m_size;
+ allocData.fd_flags = (O_CLOEXEC | O_RDWR);
+ allocData.heap_flags = 0;
+
+ int ret = ioctl(m_dmaheapfd, DMA_HEAP_IOCTL_ALLOC, &allocData);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CDMAHeapBufferObject::{} - ioctl DMA_HEAP_IOCTL_ALLOC failed, errno={}",
+ __FUNCTION__, strerror(errno));
+ return false;
+ }
+
+ m_fd = allocData.fd;
+ m_size = allocData.len;
+
+ if (m_fd < 0 || m_size <= 0)
+ {
+ CLog::Log(LOGERROR, "CDMAHeapBufferObject::{} - invalid allocation data: fd={} len={}",
+ __FUNCTION__, m_fd, m_size);
+ return false;
+ }
+
+ return true;
+}
+
+void CDMAHeapBufferObject::DestroyBufferObject()
+{
+ if (m_fd < 0)
+ return;
+
+ int ret = close(m_fd);
+ if (ret < 0)
+ CLog::Log(LOGERROR, "CDMAHeapBufferObject::{} - close failed, errno={}", __FUNCTION__,
+ strerror(errno));
+
+ m_fd = -1;
+ m_stride = 0;
+ m_size = 0;
+}
+
+uint8_t* CDMAHeapBufferObject::GetMemory()
+{
+ if (m_fd < 0)
+ return nullptr;
+
+ if (m_map)
+ {
+ CLog::Log(LOGDEBUG, "CDMAHeapBufferObject::{} - already mapped fd={} map={}", __FUNCTION__,
+ m_fd, fmt::ptr(m_map));
+ return m_map;
+ }
+
+ m_map = static_cast<uint8_t*>(mmap(nullptr, m_size, PROT_WRITE, MAP_SHARED, m_fd, 0));
+ if (m_map == MAP_FAILED)
+ {
+ CLog::Log(LOGERROR, "CDMAHeapBufferObject::{} - mmap failed, errno={}", __FUNCTION__,
+ strerror(errno));
+ return nullptr;
+ }
+
+ return m_map;
+}
+
+void CDMAHeapBufferObject::ReleaseMemory()
+{
+ if (!m_map)
+ return;
+
+ int ret = munmap(m_map, m_size);
+ if (ret < 0)
+ CLog::Log(LOGERROR, "CDMAHeapBufferObject::{} - munmap failed, errno={}", __FUNCTION__,
+ strerror(errno));
+
+ m_map = nullptr;
+}
diff --git a/xbmc/utils/DMAHeapBufferObject.h b/xbmc/utils/DMAHeapBufferObject.h
new file mode 100644
index 0000000..eb7a6fe
--- /dev/null
+++ b/xbmc/utils/DMAHeapBufferObject.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2005-2020 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 "utils/BufferObject.h"
+
+#include <memory>
+#include <stdint.h>
+
+class CDMAHeapBufferObject : public CBufferObject
+{
+public:
+ CDMAHeapBufferObject() = default;
+ virtual ~CDMAHeapBufferObject() override;
+
+ // Registration
+ static std::unique_ptr<CBufferObject> Create();
+ static void Register();
+
+ // IBufferObject overrides via CBufferObject
+ bool CreateBufferObject(uint32_t format, uint32_t width, uint32_t height) override;
+ bool CreateBufferObject(uint64_t size) override;
+ void DestroyBufferObject() override;
+ uint8_t* GetMemory() override;
+ void ReleaseMemory() override;
+ std::string GetName() const override { return "CDMAHeapBufferObject"; }
+
+private:
+ int m_dmaheapfd{-1};
+ uint64_t m_size{0};
+ uint8_t* m_map{nullptr};
+};
diff --git a/xbmc/utils/DRMHelpers.cpp b/xbmc/utils/DRMHelpers.cpp
new file mode 100644
index 0000000..0b4b4e4
--- /dev/null
+++ b/xbmc/utils/DRMHelpers.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2005-2021 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 "DRMHelpers.h"
+
+#include "utils/StringUtils.h"
+
+#include <sstream>
+
+#include <drm_fourcc.h>
+#include <xf86drm.h>
+
+namespace DRMHELPERS
+{
+
+std::string FourCCToString(uint32_t fourcc)
+{
+ std::stringstream ss;
+ ss << static_cast<char>((fourcc & 0x000000FF));
+ ss << static_cast<char>((fourcc & 0x0000FF00) >> 8);
+ ss << static_cast<char>((fourcc & 0x00FF0000) >> 16);
+ ss << static_cast<char>((fourcc & 0xFF000000) >> 24);
+
+ return ss.str();
+}
+
+std::string ModifierToString(uint64_t modifier)
+{
+#if defined(HAVE_DRM_MODIFIER_NAME)
+ std::string modifierVendorStr{"UNKNOWN_VENDOR"};
+
+ const char* vendorName = drmGetFormatModifierVendor(modifier);
+ if (vendorName)
+ modifierVendorStr = std::string(vendorName);
+
+ free(const_cast<char*>(vendorName));
+
+ std::string modifierNameStr{"UNKNOWN_MODIFIER"};
+
+ const char* modifierName = drmGetFormatModifierName(modifier);
+ if (modifierName)
+ modifierNameStr = std::string(modifierName);
+
+ free(const_cast<char*>(modifierName));
+
+ if (modifier == DRM_FORMAT_MOD_LINEAR)
+ return modifierNameStr;
+
+ return modifierVendorStr + "_" + modifierNameStr;
+#else
+ return StringUtils::Format("{:#x}", modifier);
+#endif
+}
+
+} // namespace DRMHELPERS
diff --git a/xbmc/utils/DRMHelpers.h b/xbmc/utils/DRMHelpers.h
new file mode 100644
index 0000000..dcc7f50
--- /dev/null
+++ b/xbmc/utils/DRMHelpers.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2005-2021 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 <cstdint>
+#include <string>
+
+namespace DRMHELPERS
+{
+
+std::string FourCCToString(uint32_t fourcc);
+
+std::string ModifierToString(uint64_t modifier);
+
+} // namespace DRMHELPERS
diff --git a/xbmc/utils/DatabaseUtils.cpp b/xbmc/utils/DatabaseUtils.cpp
new file mode 100644
index 0000000..7397516
--- /dev/null
+++ b/xbmc/utils/DatabaseUtils.cpp
@@ -0,0 +1,797 @@
+/*
+ * 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 "DatabaseUtils.h"
+
+#include "dbwrappers/dataset.h"
+#include "music/MusicDatabase.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <sstream>
+
+MediaType DatabaseUtils::MediaTypeFromVideoContentType(VideoDbContentType videoContentType)
+{
+ switch (videoContentType)
+ {
+ case VideoDbContentType::MOVIES:
+ return MediaTypeMovie;
+
+ case VideoDbContentType::MOVIE_SETS:
+ return MediaTypeVideoCollection;
+
+ case VideoDbContentType::TVSHOWS:
+ return MediaTypeTvShow;
+
+ case VideoDbContentType::EPISODES:
+ return MediaTypeEpisode;
+
+ case VideoDbContentType::MUSICVIDEOS:
+ return MediaTypeMusicVideo;
+
+ default:
+ break;
+ }
+
+ return MediaTypeNone;
+}
+
+std::string DatabaseUtils::GetField(Field field, const MediaType &mediaType, DatabaseQueryPart queryPart)
+{
+ if (field == FieldNone || mediaType == MediaTypeNone)
+ return "";
+
+ if (mediaType == MediaTypeAlbum)
+ {
+ if (field == FieldId) return "albumview.idAlbum";
+ else if (field == FieldAlbum) return "albumview.strAlbum";
+ else if (field == FieldArtist || field == FieldAlbumArtist) return "albumview.strArtists";
+ else if (field == FieldGenre)
+ return "albumview.strGenres";
+ else if (field == FieldYear)
+ return "albumview.strReleaseDate";
+ else if (field == FieldOrigYear || field == FieldOrigDate)
+ return "albumview.strOrigReleaseDate";
+ else if (field == FieldMoods) return "albumview.strMoods";
+ else if (field == FieldStyles) return "albumview.strStyles";
+ else if (field == FieldThemes) return "albumview.strThemes";
+ else if (field == FieldReview) return "albumview.strReview";
+ else if (field == FieldMusicLabel) return "albumview.strLabel";
+ else if (field == FieldAlbumType) return "albumview.strType";
+ else if (field == FieldCompilation) return "albumview.bCompilation";
+ else if (field == FieldRating) return "albumview.fRating";
+ else if (field == FieldVotes) return "albumview.iVotes";
+ else if (field == FieldUserRating) return "albumview.iUserrating";
+ else if (field == FieldDateAdded) return "albumview.dateAdded";
+ else if (field == FieldDateNew) return "albumview.dateNew";
+ else if (field == FieldDateModified) return "albumview.dateModified";
+ else if (field == FieldPlaycount) return "albumview.iTimesPlayed";
+ else if (field == FieldLastPlayed) return "albumview.lastPlayed";
+ else if (field == FieldTotalDiscs)
+ return "albumview.iDiscTotal";
+ else if (field == FieldAlbumStatus)
+ return "albumview.strReleaseStatus";
+ else if (field == FieldAlbumDuration)
+ return "albumview.iAlbumDuration";
+ }
+ else if (mediaType == MediaTypeSong)
+ {
+ if (field == FieldId) return "songview.idSong";
+ else if (field == FieldTitle) return "songview.strTitle";
+ else if (field == FieldTrackNumber) return "songview.iTrack";
+ else if (field == FieldTime) return "songview.iDuration";
+ else if (field == FieldYear)
+ return "songview.strReleaseDate";
+ else if (field == FieldOrigYear || field == FieldOrigDate)
+ return "songview.strOrigReleaseDate";
+ else if (field == FieldFilename) return "songview.strFilename";
+ else if (field == FieldPlaycount) return "songview.iTimesPlayed";
+ else if (field == FieldStartOffset) return "songview.iStartOffset";
+ else if (field == FieldEndOffset) return "songview.iEndOffset";
+ else if (field == FieldLastPlayed) return "songview.lastPlayed";
+ else if (field == FieldRating) return "songview.rating";
+ else if (field == FieldVotes) return "songview.votes";
+ else if (field == FieldUserRating) return "songview.userrating";
+ else if (field == FieldComment) return "songview.comment";
+ else if (field == FieldMoods) return "songview.mood";
+ else if (field == FieldAlbum) return "songview.strAlbum";
+ else if (field == FieldPath) return "songview.strPath";
+ else if (field == FieldArtist || field == FieldAlbumArtist) return "songview.strArtists";
+ else if (field == FieldGenre)
+ return "songview.strGenres";
+ else if (field == FieldDateAdded) return "songview.dateAdded";
+ else if (field == FieldDateNew) return "songview.dateNew";
+ else if (field == FieldDateModified) return "songview.dateModified";
+
+ else if (field == FieldDiscTitle)
+ return "songview.strDiscSubtitle";
+ else if (field == FieldBPM)
+ return "songview.iBPM";
+ else if (field == FieldMusicBitRate)
+ return "songview.iBitRate";
+ else if (field == FieldSampleRate)
+ return "songview.iSampleRate";
+ else if (field == FieldNoOfChannels)
+ return "songview.iChannels";
+ }
+ else if (mediaType == MediaTypeArtist)
+ {
+ if (field == FieldId) return "artistview.idArtist";
+ else if (field == FieldArtistSort) return "artistview.strSortName";
+ else if (field == FieldArtist) return "artistview.strArtist";
+ else if (field == FieldArtistType) return "artistview.strType";
+ else if (field == FieldGender) return "artistview.strGender";
+ else if (field == FieldDisambiguation) return "artistview.strDisambiguation";
+ else if (field == FieldGenre) return "artistview.strGenres";
+ else if (field == FieldMoods) return "artistview.strMoods";
+ else if (field == FieldStyles) return "artistview.strStyles";
+ else if (field == FieldInstruments) return "artistview.strInstruments";
+ else if (field == FieldBiography) return "artistview.strBiography";
+ else if (field == FieldBorn) return "artistview.strBorn";
+ else if (field == FieldBandFormed) return "artistview.strFormed";
+ else if (field == FieldDisbanded) return "artistview.strDisbanded";
+ else if (field == FieldDied) return "artistview.strDied";
+ else if (field == FieldDateAdded) return "artistview.dateAdded";
+ else if (field == FieldDateNew) return "artistview.dateNew";
+ else if (field == FieldDateModified) return "artistview.dateModified";
+ }
+ else if (mediaType == MediaTypeMusicVideo)
+ {
+ std::string result;
+ if (field == FieldId) return "musicvideo_view.idMVideo";
+ else if (field == FieldTitle)
+ result = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_TITLE);
+ else if (field == FieldTime)
+ result = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_RUNTIME);
+ else if (field == FieldDirector)
+ result = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_DIRECTOR);
+ else if (field == FieldStudio)
+ result = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_STUDIOS);
+ else if (field == FieldYear) return "musicvideo_view.premiered";
+ else if (field == FieldPlot)
+ result = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_PLOT);
+ else if (field == FieldAlbum)
+ result = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_ALBUM);
+ else if (field == FieldArtist)
+ result = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_ARTIST);
+ else if (field == FieldGenre)
+ result = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_GENRE);
+ else if (field == FieldTrackNumber)
+ result = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_TRACK);
+ else if (field == FieldFilename) return "musicvideo_view.strFilename";
+ else if (field == FieldPath) return "musicvideo_view.strPath";
+ else if (field == FieldPlaycount) return "musicvideo_view.playCount";
+ else if (field == FieldLastPlayed) return "musicvideo_view.lastPlayed";
+ else if (field == FieldDateAdded) return "musicvideo_view.dateAdded";
+ else if (field == FieldUserRating) return "musicvideo_view.userrating";
+
+ if (!result.empty())
+ return result;
+ }
+ else if (mediaType == MediaTypeMovie)
+ {
+ std::string result;
+ if (field == FieldId) return "movie_view.idMovie";
+ else if (field == FieldTitle)
+ {
+ // We need some extra logic to get the title value if sorttitle isn't set
+ if (queryPart == DatabaseQueryPartOrderBy)
+ result = StringUtils::Format("CASE WHEN length(movie_view.c{:02}) > 0 THEN "
+ "movie_view.c{:02} ELSE movie_view.c{:02} END",
+ VIDEODB_ID_SORTTITLE, VIDEODB_ID_SORTTITLE, VIDEODB_ID_TITLE);
+ else
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_TITLE);
+ }
+ else if (field == FieldPlot)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_PLOT);
+ else if (field == FieldPlotOutline)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_PLOTOUTLINE);
+ else if (field == FieldTagline)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_TAGLINE);
+ else if (field == FieldVotes) return "movie_view.votes";
+ else if (field == FieldRating) return "movie_view.rating";
+ else if (field == FieldWriter)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_CREDITS);
+ else if (field == FieldYear) return "movie_view.premiered";
+ else if (field == FieldSortTitle)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_SORTTITLE);
+ else if (field == FieldOriginalTitle)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_ORIGINALTITLE);
+ else if (field == FieldTime)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_RUNTIME);
+ else if (field == FieldMPAA)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_MPAA);
+ else if (field == FieldTop250)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_TOP250);
+ else if (field == FieldSet) return "movie_view.strSet";
+ else if (field == FieldGenre)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_GENRE);
+ else if (field == FieldDirector)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_DIRECTOR);
+ else if (field == FieldStudio)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_STUDIOS);
+ else if (field == FieldTrailer)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_TRAILER);
+ else if (field == FieldCountry)
+ result = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_COUNTRY);
+ else if (field == FieldFilename) return "movie_view.strFilename";
+ else if (field == FieldPath) return "movie_view.strPath";
+ else if (field == FieldPlaycount) return "movie_view.playCount";
+ else if (field == FieldLastPlayed) return "movie_view.lastPlayed";
+ else if (field == FieldDateAdded) return "movie_view.dateAdded";
+ else if (field == FieldUserRating) return "movie_view.userrating";
+
+ if (!result.empty())
+ return result;
+ }
+ else if (mediaType == MediaTypeTvShow)
+ {
+ std::string result;
+ if (field == FieldId) return "tvshow_view.idShow";
+ else if (field == FieldTitle)
+ {
+ // We need some extra logic to get the title value if sorttitle isn't set
+ if (queryPart == DatabaseQueryPartOrderBy)
+ result = StringUtils::Format("CASE WHEN length(tvshow_view.c{:02}) > 0 THEN "
+ "tvshow_view.c{:02} ELSE tvshow_view.c{:02} END",
+ VIDEODB_ID_TV_SORTTITLE, VIDEODB_ID_TV_SORTTITLE,
+ VIDEODB_ID_TV_TITLE);
+ else
+ result = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_TITLE);
+ }
+ else if (field == FieldPlot)
+ result = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_PLOT);
+ else if (field == FieldTvShowStatus)
+ result = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_STATUS);
+ else if (field == FieldVotes) return "tvshow_view.votes";
+ else if (field == FieldRating) return "tvshow_view.rating";
+ else if (field == FieldYear)
+ result = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_PREMIERED);
+ else if (field == FieldGenre)
+ result = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_GENRE);
+ else if (field == FieldMPAA)
+ result = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_MPAA);
+ else if (field == FieldStudio)
+ result = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_STUDIOS);
+ else if (field == FieldSortTitle)
+ result = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_SORTTITLE);
+ else if (field == FieldOriginalTitle)
+ result = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_ORIGINALTITLE);
+ else if (field == FieldPath) return "tvshow_view.strPath";
+ else if (field == FieldDateAdded) return "tvshow_view.dateAdded";
+ else if (field == FieldLastPlayed) return "tvshow_view.lastPlayed";
+ else if (field == FieldSeason) return "tvshow_view.totalSeasons";
+ else if (field == FieldNumberOfEpisodes) return "tvshow_view.totalCount";
+ else if (field == FieldNumberOfWatchedEpisodes) return "tvshow_view.watchedcount";
+ else if (field == FieldUserRating) return "tvshow_view.userrating";
+
+ if (!result.empty())
+ return result;
+ }
+ else if (mediaType == MediaTypeEpisode)
+ {
+ std::string result;
+ if (field == FieldId) return "episode_view.idEpisode";
+ else if (field == FieldTitle)
+ result = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_TITLE);
+ else if (field == FieldPlot)
+ result = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_PLOT);
+ else if (field == FieldVotes) return "episode_view.votes";
+ else if (field == FieldRating) return "episode_view.rating";
+ else if (field == FieldWriter)
+ result = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_CREDITS);
+ else if (field == FieldAirDate)
+ result = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_AIRED);
+ else if (field == FieldTime)
+ result = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_RUNTIME);
+ else if (field == FieldDirector)
+ result = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_DIRECTOR);
+ else if (field == FieldSeason)
+ result = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_SEASON);
+ else if (field == FieldEpisodeNumber)
+ result = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_EPISODE);
+ else if (field == FieldUniqueId)
+ result = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_IDENT_ID);
+ else if (field == FieldEpisodeNumberSpecialSort)
+ result = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_SORTEPISODE);
+ else if (field == FieldSeasonSpecialSort)
+ result = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_SORTSEASON);
+ else if (field == FieldFilename) return "episode_view.strFilename";
+ else if (field == FieldPath) return "episode_view.strPath";
+ else if (field == FieldPlaycount) return "episode_view.playCount";
+ else if (field == FieldLastPlayed) return "episode_view.lastPlayed";
+ else if (field == FieldDateAdded) return "episode_view.dateAdded";
+ else if (field == FieldTvShowTitle) return "episode_view.strTitle";
+ else if (field == FieldYear) return "episode_view.premiered";
+ else if (field == FieldMPAA) return "episode_view.mpaa";
+ else if (field == FieldStudio) return "episode_view.strStudio";
+ else if (field == FieldUserRating) return "episode_view.userrating";
+
+ if (!result.empty())
+ return result;
+ }
+
+ if (field == FieldRandom && queryPart == DatabaseQueryPartOrderBy)
+ return "RANDOM()";
+
+ return "";
+}
+
+int DatabaseUtils::GetField(Field field, const MediaType &mediaType)
+{
+ if (field == FieldNone || mediaType == MediaTypeNone)
+ return -1;
+
+ return GetField(field, mediaType, false);
+}
+
+int DatabaseUtils::GetFieldIndex(Field field, const MediaType &mediaType)
+{
+ if (field == FieldNone || mediaType == MediaTypeNone)
+ return -1;
+
+ return GetField(field, mediaType, true);
+}
+
+bool DatabaseUtils::GetSelectFields(const Fields &fields, const MediaType &mediaType, FieldList &selectFields)
+{
+ if (mediaType == MediaTypeNone || fields.empty())
+ return false;
+
+ Fields sortFields = fields;
+
+ // add necessary fields to create the label
+ if (mediaType == MediaTypeSong || mediaType == MediaTypeVideo || mediaType == MediaTypeVideoCollection ||
+ mediaType == MediaTypeMusicVideo || mediaType == MediaTypeMovie || mediaType == MediaTypeTvShow || mediaType == MediaTypeEpisode)
+ sortFields.insert(FieldTitle);
+ if (mediaType == MediaTypeEpisode)
+ {
+ sortFields.insert(FieldSeason);
+ sortFields.insert(FieldEpisodeNumber);
+ }
+ else if (mediaType == MediaTypeAlbum)
+ sortFields.insert(FieldAlbum);
+ else if (mediaType == MediaTypeSong)
+ sortFields.insert(FieldTrackNumber);
+ else if (mediaType == MediaTypeArtist)
+ sortFields.insert(FieldArtist);
+
+ selectFields.clear();
+ for (Fields::const_iterator it = sortFields.begin(); it != sortFields.end(); ++it)
+ {
+ // ignore FieldLabel because it needs special handling (see further up)
+ if (*it == FieldLabel)
+ continue;
+
+ if (GetField(*it, mediaType, DatabaseQueryPartSelect).empty())
+ {
+ CLog::Log(LOGDEBUG, "DatabaseUtils::GetSortFieldList: unknown field {}", *it);
+ continue;
+ }
+ selectFields.push_back(*it);
+ }
+
+ return !selectFields.empty();
+}
+
+bool DatabaseUtils::GetFieldValue(const dbiplus::field_value &fieldValue, CVariant &variantValue)
+{
+ if (fieldValue.get_isNull())
+ {
+ variantValue = CVariant::ConstNullVariant;
+ return true;
+ }
+
+ switch (fieldValue.get_fType())
+ {
+ case dbiplus::ft_String:
+ case dbiplus::ft_WideString:
+ case dbiplus::ft_Object:
+ variantValue = fieldValue.get_asString();
+ return true;
+ case dbiplus::ft_Char:
+ case dbiplus::ft_WChar:
+ variantValue = fieldValue.get_asChar();
+ return true;
+ case dbiplus::ft_Boolean:
+ variantValue = fieldValue.get_asBool();
+ return true;
+ case dbiplus::ft_Short:
+ variantValue = fieldValue.get_asShort();
+ return true;
+ case dbiplus::ft_UShort:
+ variantValue = fieldValue.get_asShort();
+ return true;
+ case dbiplus::ft_Int:
+ variantValue = fieldValue.get_asInt();
+ return true;
+ case dbiplus::ft_UInt:
+ variantValue = fieldValue.get_asUInt();
+ return true;
+ case dbiplus::ft_Float:
+ variantValue = fieldValue.get_asFloat();
+ return true;
+ case dbiplus::ft_Double:
+ case dbiplus::ft_LongDouble:
+ variantValue = fieldValue.get_asDouble();
+ return true;
+ case dbiplus::ft_Int64:
+ variantValue = fieldValue.get_asInt64();
+ return true;
+ }
+
+ return false;
+}
+
+bool DatabaseUtils::GetDatabaseResults(const MediaType &mediaType, const FieldList &fields, const std::unique_ptr<dbiplus::Dataset> &dataset, DatabaseResults &results)
+{
+ if (dataset->num_rows() == 0)
+ return true;
+
+ const dbiplus::result_set &resultSet = dataset->get_result_set();
+ unsigned int offset = results.size();
+
+ if (fields.empty())
+ {
+ DatabaseResult result;
+ for (unsigned int index = 0; index < resultSet.records.size(); index++)
+ {
+ result[FieldRow] = index + offset;
+ results.push_back(result);
+ }
+
+ return true;
+ }
+
+ if (resultSet.record_header.size() < fields.size())
+ return false;
+
+ std::vector<int> fieldIndexLookup;
+ fieldIndexLookup.reserve(fields.size());
+ for (FieldList::const_iterator it = fields.begin(); it != fields.end(); ++it)
+ fieldIndexLookup.push_back(GetFieldIndex(*it, mediaType));
+
+ results.reserve(resultSet.records.size() + offset);
+ for (unsigned int index = 0; index < resultSet.records.size(); index++)
+ {
+ DatabaseResult result;
+ result[FieldRow] = index + offset;
+
+ unsigned int lookupIndex = 0;
+ for (FieldList::const_iterator it = fields.begin(); it != fields.end(); ++it)
+ {
+ int fieldIndex = fieldIndexLookup[lookupIndex++];
+ if (fieldIndex < 0)
+ return false;
+
+ std::pair<Field, CVariant> value;
+ value.first = *it;
+ if (!GetFieldValue(resultSet.records[index]->at(fieldIndex), value.second))
+ CLog::Log(LOGWARNING, "GetDatabaseResults: unable to retrieve value of field {}",
+ resultSet.record_header[fieldIndex].name);
+
+ if (value.first == FieldYear &&
+ (mediaType == MediaTypeTvShow || mediaType == MediaTypeEpisode ||
+ mediaType == MediaTypeMovie))
+ {
+ CDateTime dateTime;
+ dateTime.SetFromDBDate(value.second.asString());
+ if (dateTime.IsValid())
+ {
+ value.second.clear();
+ value.second = dateTime.GetYear();
+ }
+ }
+
+ result.insert(value);
+ }
+
+ result[FieldMediaType] = mediaType;
+ if (mediaType == MediaTypeMovie || mediaType == MediaTypeVideoCollection ||
+ mediaType == MediaTypeTvShow || mediaType == MediaTypeMusicVideo)
+ result[FieldLabel] = result.at(FieldTitle).asString();
+ else if (mediaType == MediaTypeEpisode)
+ {
+ std::ostringstream label;
+ label << (int)(result.at(FieldSeason).asInteger() * 100 + result.at(FieldEpisodeNumber).asInteger());
+ label << ". ";
+ label << result.at(FieldTitle).asString();
+ result[FieldLabel] = label.str();
+ }
+ else if (mediaType == MediaTypeAlbum)
+ result[FieldLabel] = result.at(FieldAlbum).asString();
+ else if (mediaType == MediaTypeSong)
+ {
+ std::ostringstream label;
+ label << (int)result.at(FieldTrackNumber).asInteger();
+ label << ". ";
+ label << result.at(FieldTitle).asString();
+ result[FieldLabel] = label.str();
+ }
+ else if (mediaType == MediaTypeArtist)
+ result[FieldLabel] = result.at(FieldArtist).asString();
+
+ results.push_back(result);
+ }
+
+ return true;
+}
+
+std::string DatabaseUtils::BuildLimitClause(int end, int start /* = 0 */)
+{
+ return " LIMIT " + BuildLimitClauseOnly(end, start);
+}
+
+std::string DatabaseUtils::BuildLimitClauseOnly(int end, int start /* = 0 */)
+{
+ std::ostringstream sql;
+ if (start > 0)
+ {
+ if (end > 0)
+ {
+ end = end - start;
+ if (end < 0)
+ end = 0;
+ }
+
+ sql << start << "," << end;
+ }
+ else
+ sql << end;
+
+ return sql.str();
+}
+
+size_t DatabaseUtils::GetLimitCount(int end, int start)
+{
+ if (start > 0)
+ {
+ if (end - start < 0)
+ return 0;
+ else
+ return static_cast<size_t>(end - start);
+ }
+ else if (end > 0)
+ return static_cast<size_t>(end);
+ return 0;
+}
+
+int DatabaseUtils::GetField(Field field, const MediaType &mediaType, bool asIndex)
+{
+ if (field == FieldNone || mediaType == MediaTypeNone)
+ return -1;
+
+ int index = -1;
+
+ if (mediaType == MediaTypeAlbum)
+ {
+ if (field == FieldId) return CMusicDatabase::album_idAlbum;
+ else if (field == FieldAlbum) return CMusicDatabase::album_strAlbum;
+ else if (field == FieldArtist || field == FieldAlbumArtist) return CMusicDatabase::album_strArtists;
+ else if (field == FieldGenre) return CMusicDatabase::album_strGenres;
+ else if (field == FieldYear) return CMusicDatabase::album_strReleaseDate;
+ else if (field == FieldMoods) return CMusicDatabase::album_strMoods;
+ else if (field == FieldStyles) return CMusicDatabase::album_strStyles;
+ else if (field == FieldThemes) return CMusicDatabase::album_strThemes;
+ else if (field == FieldReview) return CMusicDatabase::album_strReview;
+ else if (field == FieldMusicLabel) return CMusicDatabase::album_strLabel;
+ else if (field == FieldAlbumType) return CMusicDatabase::album_strType;
+ else if (field == FieldRating) return CMusicDatabase::album_fRating;
+ else if (field == FieldVotes) return CMusicDatabase::album_iVotes;
+ else if (field == FieldUserRating) return CMusicDatabase::album_iUserrating;
+ else if (field == FieldPlaycount) return CMusicDatabase::album_iTimesPlayed;
+ else if (field == FieldLastPlayed) return CMusicDatabase::album_dtLastPlayed;
+ else if (field == FieldDateAdded) return CMusicDatabase::album_dateAdded;
+ else if (field == FieldDateNew) return CMusicDatabase::album_dateNew;
+ else if (field == FieldDateModified) return CMusicDatabase::album_dateModified;
+ else if (field == FieldTotalDiscs)
+ return CMusicDatabase::album_iTotalDiscs;
+ else if (field == FieldOrigYear || field == FieldOrigDate)
+ return CMusicDatabase::album_strOrigReleaseDate;
+ else if (field == FieldAlbumStatus)
+ return CMusicDatabase::album_strReleaseStatus;
+ else if (field == FieldAlbumDuration)
+ return CMusicDatabase::album_iAlbumDuration;
+ }
+ else if (mediaType == MediaTypeSong)
+ {
+ if (field == FieldId) return CMusicDatabase::song_idSong;
+ else if (field == FieldTitle) return CMusicDatabase::song_strTitle;
+ else if (field == FieldTrackNumber) return CMusicDatabase::song_iTrack;
+ else if (field == FieldTime) return CMusicDatabase::song_iDuration;
+ else if (field == FieldYear) return CMusicDatabase::song_strReleaseDate;
+ else if (field == FieldFilename) return CMusicDatabase::song_strFileName;
+ else if (field == FieldPlaycount) return CMusicDatabase::song_iTimesPlayed;
+ else if (field == FieldStartOffset) return CMusicDatabase::song_iStartOffset;
+ else if (field == FieldEndOffset) return CMusicDatabase::song_iEndOffset;
+ else if (field == FieldLastPlayed) return CMusicDatabase::song_lastplayed;
+ else if (field == FieldRating) return CMusicDatabase::song_rating;
+ else if (field == FieldUserRating) return CMusicDatabase::song_userrating;
+ else if (field == FieldVotes) return CMusicDatabase::song_votes;
+ else if (field == FieldComment) return CMusicDatabase::song_comment;
+ else if (field == FieldMoods) return CMusicDatabase::song_mood;
+ else if (field == FieldAlbum) return CMusicDatabase::song_strAlbum;
+ else if (field == FieldPath) return CMusicDatabase::song_strPath;
+ else if (field == FieldGenre) return CMusicDatabase::song_strGenres;
+ else if (field == FieldArtist || field == FieldAlbumArtist) return CMusicDatabase::song_strArtists;
+ else if (field == FieldDateAdded) return CMusicDatabase::song_dateAdded;
+ else if (field == FieldDateNew) return CMusicDatabase::song_dateNew;
+ else if (field == FieldDateModified) return CMusicDatabase::song_dateModified;
+ else if (field == FieldBPM)
+ return CMusicDatabase::song_iBPM;
+ else if (field == FieldMusicBitRate)
+ return CMusicDatabase::song_iBitRate;
+ else if (field == FieldSampleRate)
+ return CMusicDatabase::song_iSampleRate;
+ else if (field == FieldNoOfChannels)
+ return CMusicDatabase::song_iChannels;
+ }
+ else if (mediaType == MediaTypeArtist)
+ {
+ if (field == FieldId) return CMusicDatabase::artist_idArtist;
+ else if (field == FieldArtist) return CMusicDatabase::artist_strArtist;
+ else if (field == FieldArtistSort) return CMusicDatabase::artist_strSortName;
+ else if (field == FieldArtistType) return CMusicDatabase::artist_strType;
+ else if (field == FieldGender) return CMusicDatabase::artist_strGender;
+ else if (field == FieldDisambiguation) return CMusicDatabase::artist_strDisambiguation;
+ else if (field == FieldGenre) return CMusicDatabase::artist_strGenres;
+ else if (field == FieldMoods) return CMusicDatabase::artist_strMoods;
+ else if (field == FieldStyles) return CMusicDatabase::artist_strStyles;
+ else if (field == FieldInstruments) return CMusicDatabase::artist_strInstruments;
+ else if (field == FieldBiography) return CMusicDatabase::artist_strBiography;
+ else if (field == FieldBorn) return CMusicDatabase::artist_strBorn;
+ else if (field == FieldBandFormed) return CMusicDatabase::artist_strFormed;
+ else if (field == FieldDisbanded) return CMusicDatabase::artist_strDisbanded;
+ else if (field == FieldDied) return CMusicDatabase::artist_strDied;
+ else if (field == FieldDateAdded) return CMusicDatabase::artist_dateAdded;
+ else if (field == FieldDateNew) return CMusicDatabase::artist_dateNew;
+ else if (field == FieldDateModified) return CMusicDatabase::artist_dateModified;
+ }
+ else if (mediaType == MediaTypeMusicVideo)
+ {
+ if (field == FieldId) return 0;
+ else if (field == FieldTitle) index = VIDEODB_ID_MUSICVIDEO_TITLE;
+ else if (field == FieldTime) index = VIDEODB_ID_MUSICVIDEO_RUNTIME;
+ else if (field == FieldDirector) index = VIDEODB_ID_MUSICVIDEO_DIRECTOR;
+ else if (field == FieldStudio) index = VIDEODB_ID_MUSICVIDEO_STUDIOS;
+ else if (field == FieldYear) return VIDEODB_DETAILS_MUSICVIDEO_PREMIERED;
+ else if (field == FieldPlot) index = VIDEODB_ID_MUSICVIDEO_PLOT;
+ else if (field == FieldAlbum) index = VIDEODB_ID_MUSICVIDEO_ALBUM;
+ else if (field == FieldArtist) index = VIDEODB_ID_MUSICVIDEO_ARTIST;
+ else if (field == FieldGenre) index = VIDEODB_ID_MUSICVIDEO_GENRE;
+ else if (field == FieldTrackNumber) index = VIDEODB_ID_MUSICVIDEO_TRACK;
+ else if (field == FieldFilename) return VIDEODB_DETAILS_MUSICVIDEO_FILE;
+ else if (field == FieldPath) return VIDEODB_DETAILS_MUSICVIDEO_PATH;
+ else if (field == FieldPlaycount) return VIDEODB_DETAILS_MUSICVIDEO_PLAYCOUNT;
+ else if (field == FieldLastPlayed) return VIDEODB_DETAILS_MUSICVIDEO_LASTPLAYED;
+ else if (field == FieldDateAdded) return VIDEODB_DETAILS_MUSICVIDEO_DATEADDED;
+ else if (field == FieldUserRating) return VIDEODB_DETAILS_MUSICVIDEO_USER_RATING;
+
+ if (index < 0)
+ return index;
+
+ if (asIndex)
+ {
+ // see VideoDatabase.h
+ // the first field is the item's ID and the second is the item's file ID
+ index += 2;
+ }
+ }
+ else if (mediaType == MediaTypeMovie)
+ {
+ if (field == FieldId) return 0;
+ else if (field == FieldTitle) index = VIDEODB_ID_TITLE;
+ else if (field == FieldSortTitle) index = VIDEODB_ID_SORTTITLE;
+ else if (field == FieldOriginalTitle) index = VIDEODB_ID_ORIGINALTITLE;
+ else if (field == FieldPlot) index = VIDEODB_ID_PLOT;
+ else if (field == FieldPlotOutline) index = VIDEODB_ID_PLOTOUTLINE;
+ else if (field == FieldTagline) index = VIDEODB_ID_TAGLINE;
+ else if (field == FieldVotes) return VIDEODB_DETAILS_MOVIE_VOTES;
+ else if (field == FieldRating) return VIDEODB_DETAILS_MOVIE_RATING;
+ else if (field == FieldWriter) index = VIDEODB_ID_CREDITS;
+ else if (field == FieldYear) return VIDEODB_DETAILS_MOVIE_PREMIERED;
+ else if (field == FieldTime) index = VIDEODB_ID_RUNTIME;
+ else if (field == FieldMPAA) index = VIDEODB_ID_MPAA;
+ else if (field == FieldTop250) index = VIDEODB_ID_TOP250;
+ else if (field == FieldSet) return VIDEODB_DETAILS_MOVIE_SET_NAME;
+ else if (field == FieldGenre) index = VIDEODB_ID_GENRE;
+ else if (field == FieldDirector) index = VIDEODB_ID_DIRECTOR;
+ else if (field == FieldStudio) index = VIDEODB_ID_STUDIOS;
+ else if (field == FieldTrailer) index = VIDEODB_ID_TRAILER;
+ else if (field == FieldCountry) index = VIDEODB_ID_COUNTRY;
+ else if (field == FieldFilename) index = VIDEODB_DETAILS_MOVIE_FILE;
+ else if (field == FieldPath) return VIDEODB_DETAILS_MOVIE_PATH;
+ else if (field == FieldPlaycount) return VIDEODB_DETAILS_MOVIE_PLAYCOUNT;
+ else if (field == FieldLastPlayed) return VIDEODB_DETAILS_MOVIE_LASTPLAYED;
+ else if (field == FieldDateAdded) return VIDEODB_DETAILS_MOVIE_DATEADDED;
+ else if (field == FieldUserRating) return VIDEODB_DETAILS_MOVIE_USER_RATING;
+
+ if (index < 0)
+ return index;
+
+ if (asIndex)
+ {
+ // see VideoDatabase.h
+ // the first field is the item's ID and the second is the item's file ID
+ index += 2;
+ }
+ }
+ else if (mediaType == MediaTypeTvShow)
+ {
+ if (field == FieldId) return 0;
+ else if (field == FieldTitle) index = VIDEODB_ID_TV_TITLE;
+ else if (field == FieldSortTitle) index = VIDEODB_ID_TV_SORTTITLE;
+ else if (field == FieldOriginalTitle) index = VIDEODB_ID_TV_ORIGINALTITLE;
+ else if (field == FieldPlot) index = VIDEODB_ID_TV_PLOT;
+ else if (field == FieldTvShowStatus) index = VIDEODB_ID_TV_STATUS;
+ else if (field == FieldVotes) return VIDEODB_DETAILS_TVSHOW_VOTES;
+ else if (field == FieldRating) return VIDEODB_DETAILS_TVSHOW_RATING;
+ else if (field == FieldYear) index = VIDEODB_ID_TV_PREMIERED;
+ else if (field == FieldGenre) index = VIDEODB_ID_TV_GENRE;
+ else if (field == FieldMPAA) index = VIDEODB_ID_TV_MPAA;
+ else if (field == FieldStudio) index = VIDEODB_ID_TV_STUDIOS;
+ else if (field == FieldPath) return VIDEODB_DETAILS_TVSHOW_PATH;
+ else if (field == FieldDateAdded) return VIDEODB_DETAILS_TVSHOW_DATEADDED;
+ else if (field == FieldLastPlayed) return VIDEODB_DETAILS_TVSHOW_LASTPLAYED;
+ else if (field == FieldNumberOfEpisodes) return VIDEODB_DETAILS_TVSHOW_NUM_EPISODES;
+ else if (field == FieldNumberOfWatchedEpisodes) return VIDEODB_DETAILS_TVSHOW_NUM_WATCHED;
+ else if (field == FieldSeason) return VIDEODB_DETAILS_TVSHOW_NUM_SEASONS;
+ else if (field == FieldUserRating) return VIDEODB_DETAILS_TVSHOW_USER_RATING;
+
+ if (index < 0)
+ return index;
+
+ if (asIndex)
+ {
+ // see VideoDatabase.h
+ // the first field is the item's ID
+ index += 1;
+ }
+ }
+ else if (mediaType == MediaTypeEpisode)
+ {
+ if (field == FieldId) return 0;
+ else if (field == FieldTitle) index = VIDEODB_ID_EPISODE_TITLE;
+ else if (field == FieldPlot) index = VIDEODB_ID_EPISODE_PLOT;
+ else if (field == FieldVotes) return VIDEODB_DETAILS_EPISODE_VOTES;
+ else if (field == FieldRating) return VIDEODB_DETAILS_EPISODE_RATING;
+ else if (field == FieldWriter) index = VIDEODB_ID_EPISODE_CREDITS;
+ else if (field == FieldAirDate) index = VIDEODB_ID_EPISODE_AIRED;
+ else if (field == FieldTime) index = VIDEODB_ID_EPISODE_RUNTIME;
+ else if (field == FieldDirector) index = VIDEODB_ID_EPISODE_DIRECTOR;
+ else if (field == FieldSeason) index = VIDEODB_ID_EPISODE_SEASON;
+ else if (field == FieldEpisodeNumber) index = VIDEODB_ID_EPISODE_EPISODE;
+ else if (field == FieldUniqueId) index = VIDEODB_ID_EPISODE_IDENT_ID;
+ else if (field == FieldEpisodeNumberSpecialSort) index = VIDEODB_ID_EPISODE_SORTEPISODE;
+ else if (field == FieldSeasonSpecialSort) index = VIDEODB_ID_EPISODE_SORTSEASON;
+ else if (field == FieldFilename) return VIDEODB_DETAILS_EPISODE_FILE;
+ else if (field == FieldPath) return VIDEODB_DETAILS_EPISODE_PATH;
+ else if (field == FieldPlaycount) return VIDEODB_DETAILS_EPISODE_PLAYCOUNT;
+ else if (field == FieldLastPlayed) return VIDEODB_DETAILS_EPISODE_LASTPLAYED;
+ else if (field == FieldDateAdded) return VIDEODB_DETAILS_EPISODE_DATEADDED;
+ else if (field == FieldTvShowTitle) return VIDEODB_DETAILS_EPISODE_TVSHOW_NAME;
+ else if (field == FieldStudio) return VIDEODB_DETAILS_EPISODE_TVSHOW_STUDIO;
+ else if (field == FieldYear) return VIDEODB_DETAILS_EPISODE_TVSHOW_AIRED;
+ else if (field == FieldMPAA) return VIDEODB_DETAILS_EPISODE_TVSHOW_MPAA;
+ else if (field == FieldUserRating) return VIDEODB_DETAILS_EPISODE_USER_RATING;
+
+ if (index < 0)
+ return index;
+
+ if (asIndex)
+ {
+ // see VideoDatabase.h
+ // the first field is the item's ID and the second is the item's file ID
+ index += 2;
+ }
+ }
+
+ return index;
+}
diff --git a/xbmc/utils/DatabaseUtils.h b/xbmc/utils/DatabaseUtils.h
new file mode 100644
index 0000000..c9bf6a6
--- /dev/null
+++ b/xbmc/utils/DatabaseUtils.h
@@ -0,0 +1,187 @@
+/*
+ * 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 "media/MediaType.h"
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+class CVariant;
+enum class VideoDbContentType;
+
+namespace dbiplus
+{
+ class Dataset;
+ class field_value;
+}
+
+typedef enum
+{
+ // special fields used during sorting
+ FieldUnknown = -1,
+ FieldNone = 0,
+ FieldSort, // used to store the string to use for sorting
+ FieldSortSpecial, // whether the item needs special handling (0 = no, 1 = sort on top, 2 = sort on bottom)
+ FieldLabel,
+ FieldFolder,
+ FieldMediaType,
+ FieldRow, // the row number in a dataset
+
+ // special fields not retrieved from the database
+ FieldSize,
+ FieldDate,
+ FieldDriveType,
+ FieldStartOffset,
+ FieldEndOffset,
+ FieldProgramCount,
+ FieldBitrate,
+ FieldListeners,
+ FieldPlaylist,
+ FieldVirtualFolder,
+ FieldRandom,
+ FieldDateTaken,
+ FieldAudioCount,
+ FieldSubtitleCount,
+
+ FieldInstallDate,
+ FieldLastUpdated,
+ FieldLastUsed,
+
+ // fields retrievable from the database
+ FieldId,
+ FieldGenre,
+ FieldAlbum,
+ FieldDiscTitle,
+ FieldIsBoxset,
+ FieldTotalDiscs,
+ FieldOrigYear,
+ FieldOrigDate,
+ FieldArtist,
+ FieldArtistSort,
+ FieldAlbumArtist,
+ FieldTitle,
+ FieldSortTitle,
+ FieldOriginalTitle,
+ FieldYear,
+ FieldTime,
+ FieldTrackNumber,
+ FieldFilename,
+ FieldPath,
+ FieldPlaycount,
+ FieldLastPlayed,
+ FieldInProgress,
+ FieldRating,
+ FieldComment,
+ FieldRole,
+ FieldDateAdded,
+ FieldDateModified,
+ FieldDateNew,
+ FieldTvShowTitle,
+ FieldPlot,
+ FieldPlotOutline,
+ FieldTagline,
+ FieldTvShowStatus,
+ FieldVotes,
+ FieldDirector,
+ FieldActor,
+ FieldStudio,
+ FieldCountry,
+ FieldMPAA,
+ FieldTop250,
+ FieldSet,
+ FieldNumberOfEpisodes,
+ FieldNumberOfWatchedEpisodes,
+ FieldWriter,
+ FieldAirDate,
+ FieldEpisodeNumber,
+ FieldUniqueId,
+ FieldSeason,
+ FieldEpisodeNumberSpecialSort,
+ FieldSeasonSpecialSort,
+ FieldReview,
+ FieldThemes,
+ FieldMoods,
+ FieldStyles,
+ FieldAlbumType,
+ FieldMusicLabel,
+ FieldCompilation,
+ FieldSource,
+ FieldTrailer,
+ FieldVideoResolution,
+ FieldVideoAspectRatio,
+ FieldVideoCodec,
+ FieldAudioChannels,
+ FieldAudioCodec,
+ FieldAudioLanguage,
+ FieldSubtitleLanguage,
+ FieldProductionCode,
+ FieldTag,
+ FieldChannelName,
+ FieldChannelNumber,
+ FieldInstruments,
+ FieldBiography,
+ FieldArtistType,
+ FieldGender,
+ FieldDisambiguation,
+ FieldBorn,
+ FieldBandFormed,
+ FieldDisbanded,
+ FieldDied,
+ FieldStereoMode,
+ FieldUserRating,
+ FieldRelevance, // Used for actors' appearances
+ FieldClientChannelOrder,
+ FieldBPM,
+ FieldMusicBitRate,
+ FieldSampleRate,
+ FieldNoOfChannels,
+ FieldAlbumStatus,
+ FieldAlbumDuration,
+ FieldHdrType,
+ FieldProvider,
+ FieldUserPreference,
+ FieldMax
+} Field;
+
+typedef std::set<Field> Fields;
+typedef std::vector<Field> FieldList;
+
+typedef enum {
+ DatabaseQueryPartSelect,
+ DatabaseQueryPartWhere,
+ DatabaseQueryPartOrderBy,
+} DatabaseQueryPart;
+
+typedef std::map<Field, CVariant> DatabaseResult;
+typedef std::vector<DatabaseResult> DatabaseResults;
+
+class DatabaseUtils
+{
+public:
+ static MediaType MediaTypeFromVideoContentType(VideoDbContentType videoContentType);
+
+ static std::string GetField(Field field, const MediaType &mediaType, DatabaseQueryPart queryPart);
+ static int GetField(Field field, const MediaType &mediaType);
+ static int GetFieldIndex(Field field, const MediaType &mediaType);
+ static bool GetSelectFields(const Fields &fields, const MediaType &mediaType, FieldList &selectFields);
+
+ static bool GetFieldValue(const dbiplus::field_value &fieldValue, CVariant &variantValue);
+ static bool GetDatabaseResults(const MediaType &mediaType, const FieldList &fields, const std::unique_ptr<dbiplus::Dataset> &dataset, DatabaseResults &results);
+
+ static std::string BuildLimitClause(int end, int start = 0);
+ static std::string BuildLimitClauseOnly(int end, int start = 0);
+ static size_t GetLimitCount(int end, int start);
+
+private:
+ static int GetField(Field field, const MediaType &mediaType, bool asIndex);
+};
diff --git a/xbmc/utils/Digest.cpp b/xbmc/utils/Digest.cpp
new file mode 100644
index 0000000..445a755
--- /dev/null
+++ b/xbmc/utils/Digest.cpp
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 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 "Digest.h"
+
+#include "StringUtils.h"
+
+#include <array>
+#include <stdexcept>
+
+#include <openssl/evp.h>
+
+namespace KODI
+{
+namespace UTILITY
+{
+
+namespace
+{
+
+EVP_MD const * TypeToEVPMD(CDigest::Type type)
+{
+ switch (type)
+ {
+ case CDigest::Type::MD5:
+ return EVP_md5();
+ case CDigest::Type::SHA1:
+ return EVP_sha1();
+ case CDigest::Type::SHA256:
+ return EVP_sha256();
+ case CDigest::Type::SHA512:
+ return EVP_sha512();
+ default:
+ throw std::invalid_argument("Unknown digest type");
+ }
+}
+
+}
+
+std::ostream& operator<<(std::ostream& os, TypedDigest const& digest)
+{
+ return os << "{" << CDigest::TypeToString(digest.type) << "}" << digest.value;
+}
+
+std::string CDigest::TypeToString(Type type)
+{
+ switch (type)
+ {
+ case Type::MD5:
+ return "md5";
+ case Type::SHA1:
+ return "sha1";
+ case Type::SHA256:
+ return "sha256";
+ case Type::SHA512:
+ return "sha512";
+ case Type::INVALID:
+ return "invalid";
+ default:
+ throw std::invalid_argument("Unknown digest type");
+ }
+}
+
+CDigest::Type CDigest::TypeFromString(std::string const& type)
+{
+ std::string typeLower{type};
+ StringUtils::ToLower(typeLower);
+ if (type == "md5")
+ {
+ return Type::MD5;
+ }
+ else if (type == "sha1")
+ {
+ return Type::SHA1;
+ }
+ else if (type == "sha256")
+ {
+ return Type::SHA256;
+ }
+ else if (type == "sha512")
+ {
+ return Type::SHA512;
+ }
+ else
+ {
+ throw std::invalid_argument(std::string("Unknown digest type \"") + type + "\"");
+ }
+}
+
+void CDigest::MdCtxDeleter::operator()(EVP_MD_CTX* context)
+{
+ EVP_MD_CTX_destroy(context);
+}
+
+CDigest::CDigest(Type type)
+: m_context{EVP_MD_CTX_create()}, m_md(TypeToEVPMD(type))
+{
+ if (1 != EVP_DigestInit_ex(m_context.get(), m_md, nullptr))
+ {
+ throw std::runtime_error("EVP_DigestInit_ex failed");
+ }
+}
+
+void CDigest::Update(std::string const& data)
+{
+ Update(data.c_str(), data.size());
+}
+
+void CDigest::Update(void const* data, std::size_t size)
+{
+ if (m_finalized)
+ {
+ throw std::logic_error("Finalized digest cannot be updated any more");
+ }
+
+ if (1 != EVP_DigestUpdate(m_context.get(), data, size))
+ {
+ throw std::runtime_error("EVP_DigestUpdate failed");
+ }
+}
+
+std::string CDigest::FinalizeRaw()
+{
+ if (m_finalized)
+ {
+ throw std::logic_error("Digest can only be finalized once");
+ }
+
+ m_finalized = true;
+
+ std::array<unsigned char, 64> digest;
+ std::size_t size = EVP_MD_size(m_md);
+ if (size > digest.size())
+ {
+ throw std::runtime_error("Digest unexpectedly long");
+ }
+ if (1 != EVP_DigestFinal_ex(m_context.get(), digest.data(), nullptr))
+ {
+ throw std::runtime_error("EVP_DigestFinal_ex failed");
+ }
+ return {reinterpret_cast<char*> (digest.data()), size};
+}
+
+std::string CDigest::Finalize()
+{
+ return StringUtils::ToHexadecimal(FinalizeRaw());
+}
+
+std::string CDigest::Calculate(Type type, std::string const& data)
+{
+ CDigest digest{type};
+ digest.Update(data);
+ return digest.Finalize();
+}
+
+std::string CDigest::Calculate(Type type, void const* data, std::size_t size)
+{
+ CDigest digest{type};
+ digest.Update(data, size);
+ return digest.Finalize();
+}
+
+}
+}
diff --git a/xbmc/utils/Digest.h b/xbmc/utils/Digest.h
new file mode 100644
index 0000000..6452857
--- /dev/null
+++ b/xbmc/utils/Digest.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 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 "StringUtils.h"
+
+#include <iostream>
+#include <memory>
+#include <stdexcept>
+#include <string>
+
+#include <openssl/evp.h>
+
+namespace KODI
+{
+namespace UTILITY
+{
+
+/**
+ * Utility class for calculating message digests/hashes, currently using OpenSSL
+ */
+class CDigest
+{
+public:
+ enum class Type
+ {
+ MD5,
+ SHA1,
+ SHA256,
+ SHA512,
+ INVALID
+ };
+
+ /**
+ * Convert type enumeration value to lower-case string representation
+ */
+ static std::string TypeToString(Type type);
+ /**
+ * Convert digest type string representation to enumeration value
+ */
+ static Type TypeFromString(std::string const& type);
+
+ /**
+ * Create a digest calculation object
+ */
+ CDigest(Type type);
+ /**
+ * Update digest with data
+ *
+ * Cannot be called after \ref Finalize has been called
+ */
+ void Update(std::string const& data);
+ /**
+ * Update digest with data
+ *
+ * Cannot be called after \ref Finalize has been called
+ */
+ void Update(void const* data, std::size_t size);
+ /**
+ * Finalize and return the digest
+ *
+ * The digest object cannot be used any more after this function
+ * has been called.
+ *
+ * \return digest value as string in lower-case hexadecimal notation
+ */
+ std::string Finalize();
+ /**
+ * Finalize and return the digest
+ *
+ * The digest object cannot be used any more after this
+ * function has been called.
+ *
+ * \return digest value as binary std::string
+ */
+ std::string FinalizeRaw();
+
+ /**
+ * Calculate message digest
+ */
+ static std::string Calculate(Type type, std::string const& data);
+ /**
+ * Calculate message digest
+ */
+ static std::string Calculate(Type type, void const* data, std::size_t size);
+
+private:
+ struct MdCtxDeleter
+ {
+ void operator()(EVP_MD_CTX* context);
+ };
+
+ bool m_finalized{false};
+ std::unique_ptr<EVP_MD_CTX, MdCtxDeleter> m_context;
+ EVP_MD const* m_md;
+};
+
+struct TypedDigest
+{
+ CDigest::Type type{CDigest::Type::INVALID};
+ std::string value;
+
+ TypedDigest() = default;
+
+ TypedDigest(CDigest::Type type, std::string const& value)
+ : type(type), value(value)
+ {}
+
+ bool Empty() const
+ {
+ return (type == CDigest::Type::INVALID || value.empty());
+ }
+};
+
+inline bool operator==(TypedDigest const& left, TypedDigest const& right)
+{
+ if (left.type != right.type)
+ {
+ throw std::logic_error("Cannot compare digests of different type");
+ }
+ return StringUtils::EqualsNoCase(left.value, right.value);
+}
+
+inline bool operator!=(TypedDigest const& left, TypedDigest const& right)
+{
+ return !(left == right);
+}
+
+std::ostream& operator<<(std::ostream& os, TypedDigest const& digest);
+
+}
+}
diff --git a/xbmc/utils/DiscsUtils.cpp b/xbmc/utils/DiscsUtils.cpp
new file mode 100644
index 0000000..87d06a3
--- /dev/null
+++ b/xbmc/utils/DiscsUtils.cpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 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 "DiscsUtils.h"
+
+#include "FileItem.h"
+//! @todo it's wrong to include videoplayer scoped files, refactor
+// dvd inputstream so they can be used by other components. Or just use libdvdnav directly.
+#include "cores/VideoPlayer/DVDInputStreams/DVDInputStreamNavigator.h"
+#ifdef HAVE_LIBBLURAY
+//! @todo it's wrong to include vfs scoped files in a utils class, refactor
+// to use libbluray directly.
+#include "filesystem/BlurayDirectory.h"
+#endif
+
+bool UTILS::DISCS::GetDiscInfo(UTILS::DISCS::DiscInfo& info, const std::string& mediaPath)
+{
+ // try to probe as a DVD
+ info = ProbeDVDDiscInfo(mediaPath);
+ if (!info.empty())
+ return true;
+
+ // try to probe as Blu-ray
+ info = ProbeBlurayDiscInfo(mediaPath);
+ if (!info.empty())
+ return true;
+
+ return false;
+}
+
+UTILS::DISCS::DiscInfo UTILS::DISCS::ProbeDVDDiscInfo(const std::string& mediaPath)
+{
+ DiscInfo info;
+ CFileItem item{mediaPath, false};
+ CDVDInputStreamNavigator dvdNavigator{nullptr, item};
+ if (dvdNavigator.Open())
+ {
+ info.type = DiscType::DVD;
+ info.name = dvdNavigator.GetDVDTitleString();
+ // fallback to DVD volume id
+ if (info.name.empty())
+ {
+ info.name = dvdNavigator.GetDVDVolIdString();
+ }
+ info.serial = dvdNavigator.GetDVDSerialString();
+ }
+ return info;
+}
+
+UTILS::DISCS::DiscInfo UTILS::DISCS::ProbeBlurayDiscInfo(const std::string& mediaPath)
+{
+ DiscInfo info;
+#ifdef HAVE_LIBBLURAY
+ XFILE::CBlurayDirectory bdDir;
+ if (!bdDir.InitializeBluray(mediaPath))
+ return info;
+
+ info.type = DiscType::BLURAY;
+ info.name = bdDir.GetBlurayTitle();
+ info.serial = bdDir.GetBlurayID();
+#endif
+ return info;
+}
diff --git a/xbmc/utils/DiscsUtils.h b/xbmc/utils/DiscsUtils.h
new file mode 100644
index 0000000..08752ea
--- /dev/null
+++ b/xbmc/utils/DiscsUtils.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2022 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 <string>
+
+namespace UTILS
+{
+namespace DISCS
+{
+
+/*! \brief Abstracts a disc type
+*/
+enum class DiscType
+{
+ UNKNOWN, ///< the default value, the application doesn't know what the device is
+ DVD, ///< dvd disc
+ BLURAY ///< blu-ray disc
+};
+
+/*! \brief Abstracts the info the app knows about a disc (type, name, serial)
+*/
+struct DiscInfo
+{
+ /*! \brief The disc type, \sa DiscType */
+ DiscType type{DiscType::UNKNOWN};
+ /*! \brief The disc serial number */
+ std::string serial;
+ /*! \brief The disc label (equivalent to the mount point label) */
+ std::string name;
+
+ /*! \brief Check if the info is empty (e.g. after probing)
+ \return true if the info is empty, false otherwise
+ */
+ bool empty() { return (type == DiscType::UNKNOWN && name.empty() && serial.empty()); }
+
+ /*! \brief Clears all the DiscInfo members
+ */
+ void clear()
+ {
+ type = DiscType::UNKNOWN;
+ name.clear();
+ serial.clear();
+ }
+};
+
+/*! \brief Try to obtain the disc info (type, name, serial) of a given media path
+ \param[in, out] info The disc info struct
+ \param mediaPath The disc mediapath (e.g. /dev/cdrom, D\://, etc)
+ \return true if getting the disc info was successfull
+*/
+bool GetDiscInfo(DiscInfo& info, const std::string& mediaPath);
+
+/*! \brief Try to probe the provided media path as a DVD
+ \param mediaPath The disc mediapath (e.g. /dev/cdrom, D\://, etc)
+ \return the DiscInfo for the given media path (might be an empty struct)
+*/
+DiscInfo ProbeDVDDiscInfo(const std::string& mediaPath);
+
+/*! \brief Try to probe the provided media path as a Bluray
+ \param mediaPath The disc mediapath (e.g. /dev/cdrom, D\://, etc)
+ \return the DiscInfo for the given media path (might be an empty struct)
+*/
+DiscInfo ProbeBlurayDiscInfo(const std::string& mediaPath);
+
+} // namespace DISCS
+} // namespace UTILS
diff --git a/xbmc/utils/DumbBufferObject.cpp b/xbmc/utils/DumbBufferObject.cpp
new file mode 100644
index 0000000..51e518a
--- /dev/null
+++ b/xbmc/utils/DumbBufferObject.cpp
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2005-2020 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 "DumbBufferObject.h"
+
+#include "ServiceBroker.h"
+#include "utils/BufferObjectFactory.h"
+#include "utils/log.h"
+#include "windowing/gbm/WinSystemGbm.h"
+#include "windowing/gbm/WinSystemGbmEGLContext.h"
+
+#include <drm_fourcc.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "PlatformDefs.h"
+
+using namespace KODI::WINDOWING::GBM;
+
+std::unique_ptr<CBufferObject> CDumbBufferObject::Create()
+{
+ return std::make_unique<CDumbBufferObject>();
+}
+
+void CDumbBufferObject::Register()
+{
+ CBufferObjectFactory::RegisterBufferObject(CDumbBufferObject::Create);
+}
+
+CDumbBufferObject::CDumbBufferObject()
+{
+ auto winSystem = static_cast<CWinSystemGbmEGLContext*>(CServiceBroker::GetWinSystem());
+
+ m_device = winSystem->GetDrm()->GetFileDescriptor();
+}
+
+CDumbBufferObject::~CDumbBufferObject()
+{
+ ReleaseMemory();
+ DestroyBufferObject();
+}
+
+bool CDumbBufferObject::CreateBufferObject(uint32_t format, uint32_t width, uint32_t height)
+{
+ if (m_fd >= 0)
+ return true;
+
+ uint32_t bpp;
+
+ switch (format)
+ {
+ case DRM_FORMAT_ARGB1555:
+ case DRM_FORMAT_RGB565:
+ bpp = 16;
+ break;
+ case DRM_FORMAT_ARGB8888:
+ bpp = 32;
+ break;
+ default:
+ throw std::runtime_error("CDumbBufferObject: pixel format not implemented");
+ }
+
+ struct drm_mode_create_dumb create_dumb{};
+ create_dumb.height = height;
+ create_dumb.width = width;
+ create_dumb.bpp = bpp;
+
+ int ret = drmIoctl(m_device, DRM_IOCTL_MODE_CREATE_DUMB, &create_dumb);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CDumbBufferObject::{} - ioctl DRM_IOCTL_MODE_CREATE_DUMB failed, errno={}",
+ __FUNCTION__, strerror(errno));
+ return false;
+ }
+
+ m_size = create_dumb.size;
+ m_stride = create_dumb.pitch;
+
+ ret = drmPrimeHandleToFD(m_device, create_dumb.handle, 0, &m_fd);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CDumbBufferObject::{} - failed to get fd from prime handle, errno={}",
+ __FUNCTION__, strerror(errno));
+ return false;
+ }
+
+ return true;
+}
+
+void CDumbBufferObject::DestroyBufferObject()
+{
+ if (m_fd < 0)
+ return;
+
+ int ret = close(m_fd);
+ if (ret < 0)
+ CLog::Log(LOGERROR, "CDumbBufferObject::{} - close failed, errno={}", __FUNCTION__,
+ strerror(errno));
+
+ m_fd = -1;
+ m_stride = 0;
+ m_size = 0;
+}
+
+uint8_t* CDumbBufferObject::GetMemory()
+{
+ if (m_fd < 0)
+ return nullptr;
+
+ if (m_map)
+ {
+ CLog::Log(LOGDEBUG, "CDumbBufferObject::{} - already mapped fd={} map={}", __FUNCTION__, m_fd,
+ fmt::ptr(m_map));
+ return m_map;
+ }
+
+ uint32_t handle;
+ int ret = drmPrimeFDToHandle(m_device, m_fd, &handle);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CDumbBufferObject::{} - failed to get handle from prime fd, errno={}",
+ __FUNCTION__, strerror(errno));
+ return nullptr;
+ }
+
+ struct drm_mode_map_dumb map_dumb{};
+ map_dumb.handle = handle;
+
+ ret = drmIoctl(m_device, DRM_IOCTL_MODE_MAP_DUMB, &map_dumb);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CDumbBufferObject::{} - ioctl DRM_IOCTL_MODE_MAP_DUMB failed, errno={}",
+ __FUNCTION__, strerror(errno));
+ return nullptr;
+ }
+
+ m_offset = map_dumb.offset;
+
+ m_map = static_cast<uint8_t*>(mmap(nullptr, m_size, PROT_WRITE, MAP_SHARED, m_device, m_offset));
+ if (m_map == MAP_FAILED)
+ {
+ CLog::Log(LOGERROR, "CDumbBufferObject::{} - mmap failed, errno={}", __FUNCTION__,
+ strerror(errno));
+ return nullptr;
+ }
+
+ return m_map;
+}
+
+void CDumbBufferObject::ReleaseMemory()
+{
+ if (!m_map)
+ return;
+
+ int ret = munmap(m_map, m_size);
+ if (ret < 0)
+ CLog::Log(LOGERROR, "CDumbBufferObject::{} - munmap failed, errno={}", __FUNCTION__,
+ strerror(errno));
+
+ m_map = nullptr;
+ m_offset = 0;
+}
diff --git a/xbmc/utils/DumbBufferObject.h b/xbmc/utils/DumbBufferObject.h
new file mode 100644
index 0000000..2dec611
--- /dev/null
+++ b/xbmc/utils/DumbBufferObject.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2005-2020 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 "utils/BufferObject.h"
+
+#include <memory>
+#include <stdint.h>
+
+class CDumbBufferObject : public CBufferObject
+{
+public:
+ CDumbBufferObject();
+ virtual ~CDumbBufferObject() override;
+
+ // Registration
+ static std::unique_ptr<CBufferObject> Create();
+ static void Register();
+
+ // IBufferObject overrides via CBufferObject
+ bool CreateBufferObject(uint32_t format, uint32_t width, uint32_t height) override;
+ void DestroyBufferObject() override;
+ uint8_t* GetMemory() override;
+ void ReleaseMemory() override;
+ std::string GetName() const override { return "CDumbBufferObject"; }
+
+private:
+ int m_device{-1};
+ uint64_t m_size{0};
+ uint64_t m_offset{0};
+ uint8_t* m_map{nullptr};
+};
diff --git a/xbmc/utils/EGLFence.cpp b/xbmc/utils/EGLFence.cpp
new file mode 100644
index 0000000..369c40a
--- /dev/null
+++ b/xbmc/utils/EGLFence.cpp
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017-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 "EGLFence.h"
+
+#include "EGLUtils.h"
+#include "utils/log.h"
+
+using namespace KODI::UTILS::EGL;
+
+CEGLFence::CEGLFence(EGLDisplay display) :
+ m_display(display)
+{
+ m_eglCreateSyncKHR = CEGLUtils::GetRequiredProcAddress<PFNEGLCREATESYNCKHRPROC>("eglCreateSyncKHR");
+ m_eglDestroySyncKHR = CEGLUtils::GetRequiredProcAddress<PFNEGLDESTROYSYNCKHRPROC>("eglDestroySyncKHR");
+ m_eglGetSyncAttribKHR = CEGLUtils::GetRequiredProcAddress<PFNEGLGETSYNCATTRIBKHRPROC>("eglGetSyncAttribKHR");
+}
+
+void CEGLFence::CreateFence()
+{
+ m_fence = m_eglCreateSyncKHR(m_display, EGL_SYNC_FENCE_KHR, nullptr);
+ if (m_fence == EGL_NO_SYNC_KHR)
+ {
+ CEGLUtils::Log(LOGERROR, "failed to create egl sync fence");
+ throw std::runtime_error("failed to create egl sync fence");
+ }
+}
+
+void CEGLFence::DestroyFence()
+{
+ if (m_fence == EGL_NO_SYNC_KHR)
+ {
+ return;
+ }
+
+ if (m_eglDestroySyncKHR(m_display, m_fence) != EGL_TRUE)
+ {
+ CEGLUtils::Log(LOGERROR, "failed to destroy egl sync fence");
+ }
+
+ m_fence = EGL_NO_SYNC_KHR;
+}
+
+bool CEGLFence::IsSignaled()
+{
+ // fence has been destroyed so return true immediately so buffer can be used
+ if (m_fence == EGL_NO_SYNC_KHR)
+ {
+ return true;
+ }
+
+ EGLint status = EGL_UNSIGNALED_KHR;
+ if (m_eglGetSyncAttribKHR(m_display, m_fence, EGL_SYNC_STATUS_KHR, &status) != EGL_TRUE)
+ {
+ CEGLUtils::Log(LOGERROR, "failed to query egl sync fence");
+ return false;
+ }
+
+ if (status == EGL_SIGNALED_KHR)
+ {
+ return true;
+ }
+
+ return false;
+}
diff --git a/xbmc/utils/EGLFence.h b/xbmc/utils/EGLFence.h
new file mode 100644
index 0000000..bd96444
--- /dev/null
+++ b/xbmc/utils/EGLFence.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017-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 "system_egl.h"
+
+#include <EGL/eglext.h>
+
+namespace KODI
+{
+namespace UTILS
+{
+namespace EGL
+{
+
+class CEGLFence
+{
+public:
+ explicit CEGLFence(EGLDisplay display);
+ CEGLFence(CEGLFence const& other) = delete;
+ CEGLFence& operator=(CEGLFence const& other) = delete;
+
+ void CreateFence();
+ void DestroyFence();
+ bool IsSignaled();
+
+private:
+ EGLDisplay m_display{nullptr};
+ EGLSyncKHR m_fence{nullptr};
+
+ PFNEGLCREATESYNCKHRPROC m_eglCreateSyncKHR{nullptr};
+ PFNEGLDESTROYSYNCKHRPROC m_eglDestroySyncKHR{nullptr};
+ PFNEGLGETSYNCATTRIBKHRPROC m_eglGetSyncAttribKHR{nullptr};
+};
+
+}
+}
+}
diff --git a/xbmc/utils/EGLImage.cpp b/xbmc/utils/EGLImage.cpp
new file mode 100644
index 0000000..24f9708
--- /dev/null
+++ b/xbmc/utils/EGLImage.cpp
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2017-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 "EGLImage.h"
+
+#include "ServiceBroker.h"
+#include "utils/DRMHelpers.h"
+#include "utils/EGLUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <map>
+
+namespace
+{
+ const EGLint eglDmabufPlaneFdAttr[CEGLImage::MAX_NUM_PLANES] =
+ {
+ EGL_DMA_BUF_PLANE0_FD_EXT,
+ EGL_DMA_BUF_PLANE1_FD_EXT,
+ EGL_DMA_BUF_PLANE2_FD_EXT,
+ };
+
+ const EGLint eglDmabufPlaneOffsetAttr[CEGLImage::MAX_NUM_PLANES] =
+ {
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT,
+ EGL_DMA_BUF_PLANE1_OFFSET_EXT,
+ EGL_DMA_BUF_PLANE2_OFFSET_EXT,
+ };
+
+ const EGLint eglDmabufPlanePitchAttr[CEGLImage::MAX_NUM_PLANES] =
+ {
+ EGL_DMA_BUF_PLANE0_PITCH_EXT,
+ EGL_DMA_BUF_PLANE1_PITCH_EXT,
+ EGL_DMA_BUF_PLANE2_PITCH_EXT,
+ };
+
+#if defined(EGL_EXT_image_dma_buf_import_modifiers)
+ const EGLint eglDmabufPlaneModifierLoAttr[CEGLImage::MAX_NUM_PLANES] =
+ {
+ EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
+ EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT,
+ EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT,
+ };
+
+ const EGLint eglDmabufPlaneModifierHiAttr[CEGLImage::MAX_NUM_PLANES] =
+ {
+ EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
+ EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT,
+ EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT,
+ };
+#endif
+
+#define X(VAL) std::make_pair(VAL, #VAL)
+std::map<EGLint, const char*> eglAttributes =
+{
+ X(EGL_WIDTH),
+ X(EGL_HEIGHT),
+
+ // please keep attributes in accordance to:
+ // https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_image_dma_buf_import.txt
+ X(EGL_LINUX_DRM_FOURCC_EXT),
+ X(EGL_DMA_BUF_PLANE0_FD_EXT),
+ X(EGL_DMA_BUF_PLANE0_OFFSET_EXT),
+ X(EGL_DMA_BUF_PLANE0_PITCH_EXT),
+ X(EGL_DMA_BUF_PLANE1_FD_EXT),
+ X(EGL_DMA_BUF_PLANE1_OFFSET_EXT),
+ X(EGL_DMA_BUF_PLANE1_PITCH_EXT),
+ X(EGL_DMA_BUF_PLANE2_FD_EXT),
+ X(EGL_DMA_BUF_PLANE2_OFFSET_EXT),
+ X(EGL_DMA_BUF_PLANE2_PITCH_EXT),
+ X(EGL_YUV_COLOR_SPACE_HINT_EXT),
+ X(EGL_SAMPLE_RANGE_HINT_EXT),
+ X(EGL_YUV_CHROMA_VERTICAL_SITING_HINT_EXT),
+ X(EGL_YUV_CHROMA_HORIZONTAL_SITING_HINT_EXT),
+ X(EGL_ITU_REC601_EXT),
+ X(EGL_ITU_REC709_EXT),
+ X(EGL_ITU_REC2020_EXT),
+ X(EGL_YUV_FULL_RANGE_EXT),
+ X(EGL_YUV_NARROW_RANGE_EXT),
+ X(EGL_YUV_CHROMA_SITING_0_EXT),
+ X(EGL_YUV_CHROMA_SITING_0_5_EXT),
+
+#if defined(EGL_EXT_image_dma_buf_import_modifiers)
+ // please keep attributes in accordance to:
+ // https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_image_dma_buf_import_modifiers.txt
+ X(EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT),
+ X(EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT),
+ X(EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT),
+ X(EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT),
+ X(EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT),
+ X(EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT),
+ X(EGL_DMA_BUF_PLANE3_FD_EXT),
+ X(EGL_DMA_BUF_PLANE3_OFFSET_EXT),
+ X(EGL_DMA_BUF_PLANE3_PITCH_EXT),
+ X(EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT),
+ X(EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT),
+#endif
+};
+
+} // namespace
+
+CEGLImage::CEGLImage(EGLDisplay display) :
+ m_display(display)
+{
+ m_eglCreateImageKHR = CEGLUtils::GetRequiredProcAddress<PFNEGLCREATEIMAGEKHRPROC>("eglCreateImageKHR");
+ m_eglDestroyImageKHR = CEGLUtils::GetRequiredProcAddress<PFNEGLDESTROYIMAGEKHRPROC>("eglDestroyImageKHR");
+ m_glEGLImageTargetTexture2DOES = CEGLUtils::GetRequiredProcAddress<PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>("glEGLImageTargetTexture2DOES");
+}
+
+bool CEGLImage::CreateImage(EglAttrs imageAttrs)
+{
+ CEGLAttributes<22> attribs;
+ attribs.Add({{EGL_WIDTH, imageAttrs.width},
+ {EGL_HEIGHT, imageAttrs.height},
+ {EGL_LINUX_DRM_FOURCC_EXT, static_cast<EGLint>(imageAttrs.format)}});
+
+ if (imageAttrs.colorSpace != 0 && imageAttrs.colorRange != 0)
+ {
+ attribs.Add({{EGL_YUV_COLOR_SPACE_HINT_EXT, imageAttrs.colorSpace},
+ {EGL_SAMPLE_RANGE_HINT_EXT, imageAttrs.colorRange},
+ {EGL_YUV_CHROMA_VERTICAL_SITING_HINT_EXT, EGL_YUV_CHROMA_SITING_0_EXT},
+ {EGL_YUV_CHROMA_HORIZONTAL_SITING_HINT_EXT, EGL_YUV_CHROMA_SITING_0_EXT}});
+ }
+
+ for (int i = 0; i < MAX_NUM_PLANES; i++)
+ {
+ if (imageAttrs.planes[i].fd != 0)
+ {
+ attribs.Add({{eglDmabufPlaneFdAttr[i], imageAttrs.planes[i].fd},
+ {eglDmabufPlaneOffsetAttr[i], imageAttrs.planes[i].offset},
+ {eglDmabufPlanePitchAttr[i], imageAttrs.planes[i].pitch}});
+
+#if defined(EGL_EXT_image_dma_buf_import_modifiers)
+ if (imageAttrs.planes[i].modifier != DRM_FORMAT_MOD_INVALID && imageAttrs.planes[i].modifier != DRM_FORMAT_MOD_LINEAR)
+ attribs.Add({{eglDmabufPlaneModifierLoAttr[i], static_cast<EGLint>(imageAttrs.planes[i].modifier & 0xFFFFFFFF)},
+ {eglDmabufPlaneModifierHiAttr[i], static_cast<EGLint>(imageAttrs.planes[i].modifier >> 32)}});
+#endif
+ }
+ }
+
+ m_image = m_eglCreateImageKHR(m_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.Get());
+
+ if (!m_image || CServiceBroker::GetLogging().CanLogComponent(LOGVIDEO))
+ {
+ const EGLint* attrs = attribs.Get();
+
+ std::string eglString;
+
+ for (int i = 0; i < (attribs.Size()); i += 2)
+ {
+ std::string keyStr;
+ std::string valueStr;
+
+ auto eglAttrKey = eglAttributes.find(attrs[i]);
+ if (eglAttrKey != eglAttributes.end())
+ {
+ keyStr = eglAttrKey->second;
+ }
+ else
+ {
+ keyStr = std::to_string(attrs[i]);
+ }
+
+ auto eglAttrValue = eglAttributes.find(attrs[i + 1]);
+ if (eglAttrValue != eglAttributes.end())
+ {
+ valueStr = eglAttrValue->second;
+ }
+ else
+ {
+ if (eglAttrKey != eglAttributes.end() && eglAttrKey->first == EGL_LINUX_DRM_FOURCC_EXT)
+ valueStr = DRMHELPERS::FourCCToString(attrs[i + 1]);
+ else
+ valueStr = std::to_string(attrs[i + 1]);
+ }
+
+ eglString.append(StringUtils::Format("{}: {}\n", keyStr, valueStr));
+ }
+
+ CLog::Log(LOGDEBUG, "CEGLImage::{} - attributes:\n{}", __FUNCTION__, eglString);
+ }
+
+ if (!m_image)
+ {
+ CLog::Log(LOGERROR, "CEGLImage::{} - failed to import buffer into EGL image: {:#4x}",
+ __FUNCTION__, eglGetError());
+ return false;
+ }
+
+ return true;
+}
+
+void CEGLImage::UploadImage(GLenum textureTarget)
+{
+ m_glEGLImageTargetTexture2DOES(textureTarget, m_image);
+}
+
+void CEGLImage::DestroyImage()
+{
+ m_eglDestroyImageKHR(m_display, m_image);
+}
+
+#if defined(EGL_EXT_image_dma_buf_import_modifiers)
+bool CEGLImage::SupportsFormat(uint32_t format)
+{
+ auto eglQueryDmaBufFormatsEXT =
+ CEGLUtils::GetRequiredProcAddress<PFNEGLQUERYDMABUFFORMATSEXTPROC>(
+ "eglQueryDmaBufFormatsEXT");
+
+ EGLint numFormats;
+ if (eglQueryDmaBufFormatsEXT(m_display, 0, nullptr, &numFormats) != EGL_TRUE)
+ {
+ CLog::Log(LOGERROR,
+ "CEGLImage::{} - failed to query the max number of EGL dma-buf formats: {:#4x}",
+ __FUNCTION__, eglGetError());
+ return false;
+ }
+
+ std::vector<EGLint> formats(numFormats);
+ if (eglQueryDmaBufFormatsEXT(m_display, numFormats, formats.data(), &numFormats) != EGL_TRUE)
+ {
+ CLog::Log(LOGERROR, "CEGLImage::{} - failed to query EGL dma-buf formats: {:#4x}", __FUNCTION__,
+ eglGetError());
+ return false;
+ }
+
+ auto foundFormat = std::find(formats.begin(), formats.end(), format);
+ if (foundFormat == formats.end() || CServiceBroker::GetLogging().CanLogComponent(LOGVIDEO))
+ {
+ std::string formatStr;
+ for (const auto& supportedFormat : formats)
+ formatStr.append("\n" + DRMHELPERS::FourCCToString(supportedFormat));
+
+ CLog::Log(LOGDEBUG, "CEGLImage::{} - supported formats:{}", __FUNCTION__, formatStr);
+ }
+
+ if (foundFormat != formats.end())
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CEGLImage::{} - supported format: {}", __FUNCTION__,
+ DRMHELPERS::FourCCToString(format));
+ return true;
+ }
+
+ CLog::Log(LOGERROR, "CEGLImage::{} - format not supported: {}", __FUNCTION__,
+ DRMHELPERS::FourCCToString(format));
+
+ return false;
+}
+
+bool CEGLImage::SupportsFormatAndModifier(uint32_t format, uint64_t modifier)
+{
+ if (!SupportsFormat(format))
+ return false;
+
+ if (modifier == DRM_FORMAT_MOD_LINEAR)
+ return true;
+
+ /*
+ * Some broadcom modifiers have parameters encoded which need to be
+ * masked out before comparing with reported modifiers.
+ */
+ if (modifier >> 56 == DRM_FORMAT_MOD_VENDOR_BROADCOM)
+ modifier = fourcc_mod_broadcom_mod(modifier);
+
+ auto eglQueryDmaBufModifiersEXT =
+ CEGLUtils::GetRequiredProcAddress<PFNEGLQUERYDMABUFMODIFIERSEXTPROC>(
+ "eglQueryDmaBufModifiersEXT");
+
+ EGLint numFormats;
+ if (eglQueryDmaBufModifiersEXT(m_display, format, 0, nullptr, nullptr, &numFormats) != EGL_TRUE)
+ {
+ CLog::Log(LOGERROR,
+ "CEGLImage::{} - failed to query the max number of EGL dma-buf format modifiers for "
+ "format: {} - {:#4x}",
+ __FUNCTION__, DRMHELPERS::FourCCToString(format), eglGetError());
+ return false;
+ }
+
+ std::vector<EGLuint64KHR> modifiers(numFormats);
+
+ if (eglQueryDmaBufModifiersEXT(m_display, format, numFormats, modifiers.data(), nullptr,
+ &numFormats) != EGL_TRUE)
+ {
+ CLog::Log(
+ LOGERROR,
+ "CEGLImage::{} - failed to query EGL dma-buf format modifiers for format: {} - {:#4x}",
+ __FUNCTION__, DRMHELPERS::FourCCToString(format), eglGetError());
+ return false;
+ }
+
+ auto foundModifier = std::find(modifiers.begin(), modifiers.end(), modifier);
+ if (foundModifier == modifiers.end() || CServiceBroker::GetLogging().CanLogComponent(LOGVIDEO))
+ {
+ std::string modifierStr;
+ for (const auto& supportedModifier : modifiers)
+ modifierStr.append("\n" + DRMHELPERS::ModifierToString(supportedModifier));
+
+ CLog::Log(LOGDEBUG, "CEGLImage::{} - supported modifiers:{}", __FUNCTION__, modifierStr);
+ }
+
+ if (foundModifier != modifiers.end())
+ {
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CEGLImage::{} - supported modifier: {}", __FUNCTION__,
+ DRMHELPERS::ModifierToString(modifier));
+ return true;
+ }
+
+ CLog::Log(LOGERROR, "CEGLImage::{} - modifier ({}) not supported for format ({})", __FUNCTION__,
+ DRMHELPERS::ModifierToString(modifier), DRMHELPERS::FourCCToString(format));
+
+ return false;
+}
+#endif
diff --git a/xbmc/utils/EGLImage.h b/xbmc/utils/EGLImage.h
new file mode 100644
index 0000000..5e86155
--- /dev/null
+++ b/xbmc/utils/EGLImage.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017-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 "system_egl.h"
+
+#include <array>
+
+#include <EGL/eglext.h>
+#include <drm_fourcc.h>
+
+#include "system_gl.h"
+
+class CEGLImage
+{
+public:
+ static const int MAX_NUM_PLANES{3};
+
+ struct EglPlane
+ {
+ int fd{0};
+ int offset{0};
+ int pitch{0};
+ uint64_t modifier{DRM_FORMAT_MOD_INVALID};
+ };
+
+ struct EglAttrs
+ {
+ int width{0};
+ int height{0};
+ uint32_t format{0};
+ int colorSpace{0};
+ int colorRange{0};
+ std::array<EglPlane, MAX_NUM_PLANES> planes;
+ };
+
+ explicit CEGLImage(EGLDisplay display);
+
+ CEGLImage(CEGLImage const& other) = delete;
+ CEGLImage& operator=(CEGLImage const& other) = delete;
+
+ bool CreateImage(EglAttrs imageAttrs);
+ void UploadImage(GLenum textureTarget);
+ void DestroyImage();
+
+#if defined(EGL_EXT_image_dma_buf_import_modifiers)
+ bool SupportsFormatAndModifier(uint32_t format, uint64_t modifier);
+
+private:
+ bool SupportsFormat(uint32_t format);
+#else
+private:
+#endif
+ EGLDisplay m_display{nullptr};
+ EGLImageKHR m_image{nullptr};
+
+ PFNEGLCREATEIMAGEKHRPROC m_eglCreateImageKHR{nullptr};
+ PFNEGLDESTROYIMAGEKHRPROC m_eglDestroyImageKHR{nullptr};
+ PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES{nullptr};
+};
diff --git a/xbmc/utils/EGLUtils.cpp b/xbmc/utils/EGLUtils.cpp
new file mode 100644
index 0000000..7c5e709
--- /dev/null
+++ b/xbmc/utils/EGLUtils.cpp
@@ -0,0 +1,598 @@
+/*
+ * Copyright (C) 2017-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 "EGLUtils.h"
+
+#include "ServiceBroker.h"
+#include "StringUtils.h"
+#include "guilib/IDirtyRegionSolver.h"
+#include "log.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+
+#include <map>
+
+#include <EGL/eglext.h>
+
+namespace
+{
+
+#define X(VAL) std::make_pair(VAL, #VAL)
+std::map<EGLint, const char*> eglAttributes =
+{
+ // please keep attributes in accordance to:
+ // https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglGetConfigAttrib.xhtml
+ X(EGL_ALPHA_SIZE),
+ X(EGL_ALPHA_MASK_SIZE),
+ X(EGL_BIND_TO_TEXTURE_RGB),
+ X(EGL_BIND_TO_TEXTURE_RGBA),
+ X(EGL_BLUE_SIZE),
+ X(EGL_BUFFER_SIZE),
+ X(EGL_COLOR_BUFFER_TYPE),
+ X(EGL_CONFIG_CAVEAT),
+ X(EGL_CONFIG_ID),
+ X(EGL_CONFORMANT),
+ X(EGL_DEPTH_SIZE),
+ X(EGL_GREEN_SIZE),
+ X(EGL_LEVEL),
+ X(EGL_LUMINANCE_SIZE),
+ X(EGL_MAX_PBUFFER_WIDTH),
+ X(EGL_MAX_PBUFFER_HEIGHT),
+ X(EGL_MAX_PBUFFER_PIXELS),
+ X(EGL_MAX_SWAP_INTERVAL),
+ X(EGL_MIN_SWAP_INTERVAL),
+ X(EGL_NATIVE_RENDERABLE),
+ X(EGL_NATIVE_VISUAL_ID),
+ X(EGL_NATIVE_VISUAL_TYPE),
+ X(EGL_RED_SIZE),
+ X(EGL_RENDERABLE_TYPE),
+ X(EGL_SAMPLE_BUFFERS),
+ X(EGL_SAMPLES),
+ X(EGL_STENCIL_SIZE),
+ X(EGL_SURFACE_TYPE),
+ X(EGL_TRANSPARENT_TYPE),
+ X(EGL_TRANSPARENT_RED_VALUE),
+ X(EGL_TRANSPARENT_GREEN_VALUE),
+ X(EGL_TRANSPARENT_BLUE_VALUE)
+};
+
+std::map<EGLenum, const char*> eglErrors =
+{
+ // please keep errors in accordance to:
+ // https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglGetError.xhtml
+ X(EGL_SUCCESS),
+ X(EGL_NOT_INITIALIZED),
+ X(EGL_BAD_ACCESS),
+ X(EGL_BAD_ALLOC),
+ X(EGL_BAD_ATTRIBUTE),
+ X(EGL_BAD_CONFIG),
+ X(EGL_BAD_CONTEXT),
+ X(EGL_BAD_CURRENT_SURFACE),
+ X(EGL_BAD_DISPLAY),
+ X(EGL_BAD_MATCH),
+ X(EGL_BAD_NATIVE_PIXMAP),
+ X(EGL_BAD_NATIVE_WINDOW),
+ X(EGL_BAD_PARAMETER),
+ X(EGL_BAD_SURFACE),
+ X(EGL_CONTEXT_LOST),
+};
+
+std::map<EGLint, const char*> eglErrorType =
+{
+ X(EGL_DEBUG_MSG_CRITICAL_KHR),
+ X(EGL_DEBUG_MSG_ERROR_KHR),
+ X(EGL_DEBUG_MSG_WARN_KHR),
+ X(EGL_DEBUG_MSG_INFO_KHR),
+};
+#undef X
+
+} // namespace
+
+void EglErrorCallback(EGLenum error,
+ const char* command,
+ EGLint messageType,
+ EGLLabelKHR threadLabel,
+ EGLLabelKHR objectLabel,
+ const char* message)
+{
+ std::string errorStr;
+ std::string typeStr;
+
+ auto eglError = eglErrors.find(error);
+ if (eglError != eglErrors.end())
+ {
+ errorStr = eglError->second;
+ }
+
+ auto eglType = eglErrorType.find(messageType);
+ if (eglType != eglErrorType.end())
+ {
+ typeStr = eglType->second;
+ }
+
+ CLog::Log(LOGDEBUG, "EGL Debugging:\nError: {}\nCommand: {}\nType: {}\nMessage: {}", errorStr, command, typeStr, message);
+}
+
+std::set<std::string> CEGLUtils::GetClientExtensions()
+{
+ const char* extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
+ if (!extensions)
+ {
+ return {};
+ }
+ std::set<std::string> result;
+ StringUtils::SplitTo(std::inserter(result, result.begin()), extensions, " ");
+ return result;
+}
+
+std::set<std::string> CEGLUtils::GetExtensions(EGLDisplay eglDisplay)
+{
+ const char* extensions = eglQueryString(eglDisplay, EGL_EXTENSIONS);
+ if (!extensions)
+ {
+ throw std::runtime_error("Could not query EGL for extensions");
+ }
+ std::set<std::string> result;
+ StringUtils::SplitTo(std::inserter(result, result.begin()), extensions, " ");
+ return result;
+}
+
+bool CEGLUtils::HasExtension(EGLDisplay eglDisplay, const std::string& name)
+{
+ auto exts = GetExtensions(eglDisplay);
+ return (exts.find(name) != exts.end());
+}
+
+bool CEGLUtils::HasClientExtension(const std::string& name)
+{
+ auto exts = GetClientExtensions();
+ return (exts.find(name) != exts.end());
+}
+
+void CEGLUtils::Log(int logLevel, const std::string& what)
+{
+ EGLenum error = eglGetError();
+ std::string errorStr = StringUtils::Format("{:#04X}", error);
+
+ auto eglError = eglErrors.find(error);
+ if (eglError != eglErrors.end())
+ {
+ errorStr = eglError->second;
+ }
+
+ CLog::Log(logLevel, "{} ({})", what, errorStr);
+}
+
+CEGLContextUtils::CEGLContextUtils(EGLenum platform, std::string const& platformExtension)
+: m_platform{platform}
+{
+ if (CEGLUtils::HasClientExtension("EGL_KHR_debug"))
+ {
+ auto eglDebugMessageControl = CEGLUtils::GetRequiredProcAddress<PFNEGLDEBUGMESSAGECONTROLKHRPROC>("eglDebugMessageControlKHR");
+
+ EGLAttrib eglDebugAttribs[] = {EGL_DEBUG_MSG_CRITICAL_KHR, EGL_TRUE,
+ EGL_DEBUG_MSG_ERROR_KHR, EGL_TRUE,
+ EGL_DEBUG_MSG_WARN_KHR, EGL_TRUE,
+ EGL_DEBUG_MSG_INFO_KHR, EGL_TRUE,
+ EGL_NONE};
+
+ eglDebugMessageControl(EglErrorCallback, eglDebugAttribs);
+ }
+
+ m_platformSupported = CEGLUtils::HasClientExtension("EGL_EXT_platform_base") && CEGLUtils::HasClientExtension(platformExtension);
+}
+
+bool CEGLContextUtils::IsPlatformSupported() const
+{
+ return m_platformSupported;
+}
+
+CEGLContextUtils::~CEGLContextUtils()
+{
+ Destroy();
+}
+
+bool CEGLContextUtils::CreateDisplay(EGLNativeDisplayType nativeDisplay)
+{
+ if (m_eglDisplay != EGL_NO_DISPLAY)
+ {
+ throw std::logic_error("Do not call CreateDisplay when display has already been created");
+ }
+
+ m_eglDisplay = eglGetDisplay(nativeDisplay);
+ if (m_eglDisplay == EGL_NO_DISPLAY)
+ {
+ CEGLUtils::Log(LOGERROR, "failed to get EGL display");
+ return false;
+ }
+
+ return true;
+}
+
+bool CEGLContextUtils::CreatePlatformDisplay(void* nativeDisplay, EGLNativeDisplayType nativeDisplayLegacy)
+{
+ if (m_eglDisplay != EGL_NO_DISPLAY)
+ {
+ throw std::logic_error("Do not call CreateDisplay when display has already been created");
+ }
+
+#if defined(EGL_EXT_platform_base)
+ if (IsPlatformSupported())
+ {
+ // Theoretically it is possible to use eglGetDisplay() and eglCreateWindowSurface,
+ // but then the EGL library basically has to guess which platform we want
+ // if it supports multiple which is usually the case -
+ // it's better and safer to make it explicit
+
+ auto getPlatformDisplayEXT = CEGLUtils::GetRequiredProcAddress<PFNEGLGETPLATFORMDISPLAYEXTPROC>("eglGetPlatformDisplayEXT");
+ m_eglDisplay = getPlatformDisplayEXT(m_platform, nativeDisplay, nullptr);
+
+ if (m_eglDisplay == EGL_NO_DISPLAY)
+ {
+ CEGLUtils::Log(LOGERROR, "failed to get platform display");
+ return false;
+ }
+ }
+#endif
+
+ if (m_eglDisplay == EGL_NO_DISPLAY)
+ {
+ return CreateDisplay(nativeDisplayLegacy);
+ }
+
+ return true;
+}
+
+bool CEGLContextUtils::InitializeDisplay(EGLint renderingApi)
+{
+ if (!eglInitialize(m_eglDisplay, nullptr, nullptr))
+ {
+ CEGLUtils::Log(LOGERROR, "failed to initialize EGL display");
+ Destroy();
+ return false;
+ }
+
+ const char* value;
+ value = eglQueryString(m_eglDisplay, EGL_VERSION);
+ CLog::Log(LOGINFO, "EGL_VERSION = {}", value ? value : "NULL");
+
+ value = eglQueryString(m_eglDisplay, EGL_VENDOR);
+ CLog::Log(LOGINFO, "EGL_VENDOR = {}", value ? value : "NULL");
+
+ value = eglQueryString(m_eglDisplay, EGL_EXTENSIONS);
+ CLog::Log(LOGINFO, "EGL_EXTENSIONS = {}", value ? value : "NULL");
+
+ value = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
+ CLog::Log(LOGINFO, "EGL_CLIENT_EXTENSIONS = {}", value ? value : "NULL");
+
+ if (eglBindAPI(renderingApi) != EGL_TRUE)
+ {
+ CEGLUtils::Log(LOGERROR, "failed to bind EGL API");
+ Destroy();
+ return false;
+ }
+
+ return true;
+}
+
+bool CEGLContextUtils::ChooseConfig(EGLint renderableType, EGLint visualId, bool hdr)
+{
+ EGLint numMatched{0};
+
+ if (m_eglDisplay == EGL_NO_DISPLAY)
+ {
+ throw std::logic_error("Choosing an EGLConfig requires an EGL display");
+ }
+
+ EGLint surfaceType = EGL_WINDOW_BIT;
+ // for the non-trivial dirty region modes, we need the EGL buffer to be preserved across updates
+ int guiAlgorithmDirtyRegions = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_guiAlgorithmDirtyRegions;
+ if (guiAlgorithmDirtyRegions == DIRTYREGION_SOLVER_COST_REDUCTION ||
+ guiAlgorithmDirtyRegions == DIRTYREGION_SOLVER_UNION)
+ surfaceType |= EGL_SWAP_BEHAVIOR_PRESERVED_BIT;
+
+ CEGLAttributesVec attribs;
+ attribs.Add({{EGL_RED_SIZE, 8},
+ {EGL_GREEN_SIZE, 8},
+ {EGL_BLUE_SIZE, 8},
+ {EGL_ALPHA_SIZE, 2},
+ {EGL_DEPTH_SIZE, 16},
+ {EGL_STENCIL_SIZE, 0},
+ {EGL_SAMPLE_BUFFERS, 0},
+ {EGL_SAMPLES, 0},
+ {EGL_SURFACE_TYPE, surfaceType},
+ {EGL_RENDERABLE_TYPE, renderableType}});
+
+ EGLConfig* currentConfig(hdr ? &m_eglHDRConfig : &m_eglConfig);
+
+ if (hdr)
+#if EGL_EXT_pixel_format_float
+ attribs.Add({{EGL_COLOR_COMPONENT_TYPE_EXT, EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT}});
+#else
+ return false;
+#endif
+
+ const char* errorMsg = nullptr;
+
+ if (eglChooseConfig(m_eglDisplay, attribs.Get(), nullptr, 0, &numMatched) != EGL_TRUE)
+ errorMsg = "failed to query number of EGL configs";
+
+ std::vector<EGLConfig> eglConfigs(numMatched);
+ if (eglChooseConfig(m_eglDisplay, attribs.Get(), eglConfigs.data(), numMatched, &numMatched) != EGL_TRUE)
+ errorMsg = "failed to find EGL configs with appropriate attributes";
+
+ if (errorMsg)
+ {
+ if (!hdr)
+ {
+ CEGLUtils::Log(LOGERROR, errorMsg);
+ Destroy();
+ }
+ else
+ CEGLUtils::Log(LOGINFO, errorMsg);
+ return false;
+ }
+
+ EGLint id{0};
+ for (const auto &eglConfig: eglConfigs)
+ {
+ *currentConfig = eglConfig;
+
+ if (visualId == 0)
+ break;
+
+ if (eglGetConfigAttrib(m_eglDisplay, *currentConfig, EGL_NATIVE_VISUAL_ID, &id) != EGL_TRUE)
+ CEGLUtils::Log(LOGERROR, "failed to query EGL attribute EGL_NATIVE_VISUAL_ID");
+
+ if (visualId == id)
+ break;
+ }
+
+ if (visualId != 0 && visualId != id)
+ {
+ CLog::Log(LOGDEBUG, "failed to find EGL config with EGL_NATIVE_VISUAL_ID={}", visualId);
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "EGL {}Config Attributes:", hdr ? "HDR " : "");
+
+ for (const auto &eglAttribute : eglAttributes)
+ {
+ EGLint value{0};
+ if (eglGetConfigAttrib(m_eglDisplay, *currentConfig, eglAttribute.first, &value) != EGL_TRUE)
+ CEGLUtils::Log(LOGERROR,
+ StringUtils::Format("failed to query EGL attribute {}", eglAttribute.second));
+
+ // we only need to print the hex value if it's an actual EGL define
+ CLog::Log(LOGDEBUG, " {}: {}", eglAttribute.second,
+ (value >= 0x3000 && value <= 0x3200) ? StringUtils::Format("{:#04x}", value)
+ : std::to_string(value));
+ }
+
+ return true;
+}
+
+EGLint CEGLContextUtils::GetConfigAttrib(EGLint attribute) const
+{
+ EGLint value{0};
+ if (eglGetConfigAttrib(m_eglDisplay, m_eglConfig, attribute, &value) != EGL_TRUE)
+ CEGLUtils::Log(LOGERROR, "failed to query EGL attribute");
+ return value;
+}
+
+bool CEGLContextUtils::CreateContext(CEGLAttributesVec contextAttribs)
+{
+ if (m_eglContext != EGL_NO_CONTEXT)
+ {
+ throw std::logic_error("Do not call CreateContext when context has already been created");
+ }
+
+ EGLConfig eglConfig{m_eglConfig};
+
+ if (CEGLUtils::HasExtension(m_eglDisplay, "EGL_KHR_no_config_context"))
+ eglConfig = EGL_NO_CONFIG_KHR;
+
+ if (CEGLUtils::HasExtension(m_eglDisplay, "EGL_IMG_context_priority"))
+ contextAttribs.Add({{EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG}});
+
+ if (CEGLUtils::HasExtension(m_eglDisplay, "EGL_KHR_create_context") &&
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_openGlDebugging)
+ {
+ contextAttribs.Add({{EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR}});
+ }
+
+ m_eglContext = eglCreateContext(m_eglDisplay, eglConfig,
+ EGL_NO_CONTEXT, contextAttribs.Get());
+
+ if (CEGLUtils::HasExtension(m_eglDisplay, "EGL_IMG_context_priority"))
+ {
+ EGLint value{EGL_CONTEXT_PRIORITY_MEDIUM_IMG};
+
+ if (eglQueryContext(m_eglDisplay, m_eglContext, EGL_CONTEXT_PRIORITY_LEVEL_IMG, &value) != EGL_TRUE)
+ CEGLUtils::Log(LOGERROR, "failed to query EGL context attribute EGL_CONTEXT_PRIORITY_LEVEL_IMG");
+
+ if (value != EGL_CONTEXT_PRIORITY_HIGH_IMG)
+ CLog::Log(LOGDEBUG, "Failed to obtain a high priority EGL context");
+ }
+
+ if (m_eglContext == EGL_NO_CONTEXT)
+ {
+ // This is expected to fail under some circumstances, so log as debug
+ CLog::Log(LOGDEBUG, "Failed to create EGL context (EGL error {})", eglGetError());
+ return false;
+ }
+
+ return true;
+}
+
+bool CEGLContextUtils::BindContext()
+{
+ if (m_eglDisplay == EGL_NO_DISPLAY || m_eglSurface == EGL_NO_SURFACE || m_eglContext == EGL_NO_CONTEXT)
+ {
+ throw std::logic_error("Activating an EGLContext requires display, surface, and context");
+ }
+
+ if (eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext) != EGL_TRUE)
+ {
+ CLog::Log(LOGERROR, "Failed to make context current {} {} {}", fmt::ptr(m_eglDisplay),
+ fmt::ptr(m_eglSurface), fmt::ptr(m_eglContext));
+ return false;
+ }
+
+ return true;
+}
+
+void CEGLContextUtils::SurfaceAttrib()
+{
+ if (m_eglDisplay == EGL_NO_DISPLAY || m_eglSurface == EGL_NO_SURFACE)
+ {
+ throw std::logic_error("Setting surface attributes requires a surface");
+ }
+
+ // for the non-trivial dirty region modes, we need the EGL buffer to be preserved across updates
+ int guiAlgorithmDirtyRegions = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_guiAlgorithmDirtyRegions;
+ if (guiAlgorithmDirtyRegions == DIRTYREGION_SOLVER_COST_REDUCTION ||
+ guiAlgorithmDirtyRegions == DIRTYREGION_SOLVER_UNION)
+ {
+ if (eglSurfaceAttrib(m_eglDisplay, m_eglSurface, EGL_SWAP_BEHAVIOR, EGL_BUFFER_PRESERVED) != EGL_TRUE)
+ {
+ CEGLUtils::Log(LOGERROR, "failed to set EGL_BUFFER_PRESERVED swap behavior");
+ }
+ }
+}
+
+void CEGLContextUtils::SurfaceAttrib(EGLint attribute, EGLint value)
+{
+ if (eglSurfaceAttrib(m_eglDisplay, m_eglSurface, attribute, value) != EGL_TRUE)
+ {
+ CEGLUtils::Log(LOGERROR, "failed to set EGL_BUFFER_PRESERVED swap behavior");
+ }
+}
+
+bool CEGLContextUtils::CreateSurface(EGLNativeWindowType nativeWindow, EGLint HDRcolorSpace /* = EGL_NONE */)
+{
+ if (m_eglDisplay == EGL_NO_DISPLAY)
+ {
+ throw std::logic_error("Creating a surface requires a display");
+ }
+ if (m_eglSurface != EGL_NO_SURFACE)
+ {
+ throw std::logic_error("Do not call CreateSurface when surface has already been created");
+ }
+
+ CEGLAttributesVec attribs;
+ EGLConfig config = m_eglConfig;
+
+#ifdef EGL_GL_COLORSPACE
+ if (HDRcolorSpace != EGL_NONE)
+ {
+ attribs.Add({{EGL_GL_COLORSPACE, HDRcolorSpace}});
+ config = m_eglHDRConfig;
+ }
+#endif
+
+ m_eglSurface = eglCreateWindowSurface(m_eglDisplay, config, nativeWindow, attribs.Get());
+
+ if (m_eglSurface == EGL_NO_SURFACE)
+ {
+ CEGLUtils::Log(LOGERROR, "failed to create window surface");
+ return false;
+ }
+
+ SurfaceAttrib();
+
+ return true;
+}
+
+bool CEGLContextUtils::CreatePlatformSurface(void* nativeWindow, EGLNativeWindowType nativeWindowLegacy)
+{
+ if (m_eglDisplay == EGL_NO_DISPLAY)
+ {
+ throw std::logic_error("Creating a surface requires a display");
+ }
+ if (m_eglSurface != EGL_NO_SURFACE)
+ {
+ throw std::logic_error("Do not call CreateSurface when surface has already been created");
+ }
+
+#if defined(EGL_EXT_platform_base)
+ if (IsPlatformSupported())
+ {
+ auto createPlatformWindowSurfaceEXT = CEGLUtils::GetRequiredProcAddress<PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC>("eglCreatePlatformWindowSurfaceEXT");
+ m_eglSurface = createPlatformWindowSurfaceEXT(m_eglDisplay, m_eglConfig, nativeWindow, nullptr);
+
+ if (m_eglSurface == EGL_NO_SURFACE)
+ {
+ CEGLUtils::Log(LOGERROR, "failed to create platform window surface");
+ return false;
+ }
+ }
+#endif
+
+ if (m_eglSurface == EGL_NO_SURFACE)
+ {
+ return CreateSurface(nativeWindowLegacy);
+ }
+
+ SurfaceAttrib();
+
+ return true;
+}
+
+void CEGLContextUtils::Destroy()
+{
+ DestroyContext();
+ DestroySurface();
+
+ if (m_eglDisplay != EGL_NO_DISPLAY)
+ {
+ eglTerminate(m_eglDisplay);
+ m_eglDisplay = EGL_NO_DISPLAY;
+ }
+}
+
+void CEGLContextUtils::DestroyContext()
+{
+ if (m_eglContext != EGL_NO_CONTEXT)
+ {
+ eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+ eglDestroyContext(m_eglDisplay, m_eglContext);
+ m_eglContext = EGL_NO_CONTEXT;
+ }
+}
+
+void CEGLContextUtils::DestroySurface()
+{
+ if (m_eglSurface != EGL_NO_SURFACE)
+ {
+ eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+ eglDestroySurface(m_eglDisplay, m_eglSurface);
+ m_eglSurface = EGL_NO_SURFACE;
+ }
+}
+
+
+bool CEGLContextUtils::SetVSync(bool enable)
+{
+ if (m_eglDisplay == EGL_NO_DISPLAY)
+ {
+ return false;
+ }
+
+ return (eglSwapInterval(m_eglDisplay, enable) == EGL_TRUE);
+}
+
+bool CEGLContextUtils::TrySwapBuffers()
+{
+ if (m_eglDisplay == EGL_NO_DISPLAY || m_eglSurface == EGL_NO_SURFACE)
+ {
+ return false;
+ }
+
+ return (eglSwapBuffers(m_eglDisplay, m_eglSurface) == EGL_TRUE);
+}
diff --git a/xbmc/utils/EGLUtils.h b/xbmc/utils/EGLUtils.h
new file mode 100644
index 0000000..2db1628
--- /dev/null
+++ b/xbmc/utils/EGLUtils.h
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2017-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 <array>
+#include <set>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+#include "system_egl.h"
+
+class CEGLUtils
+{
+public:
+ static std::set<std::string> GetClientExtensions();
+ static std::set<std::string> GetExtensions(EGLDisplay eglDisplay);
+ static bool HasExtension(EGLDisplay eglDisplay, std::string const & name);
+ static bool HasClientExtension(std::string const& name);
+ static void Log(int logLevel, std::string const& what);
+ template<typename T>
+ static T GetRequiredProcAddress(const char * procname)
+ {
+ T p = reinterpret_cast<T>(eglGetProcAddress(procname));
+ if (!p)
+ {
+ throw std::runtime_error(std::string("Could not get EGL function \"") + procname + "\" - maybe a required extension is not supported?");
+ }
+ return p;
+ }
+
+private:
+ CEGLUtils();
+};
+
+/**
+ * Convenience wrapper for heap-allocated EGL attribute arrays
+ *
+ * The wrapper makes sure that the key/value pairs are always written in actual
+ * pairs and that the array is always terminated with EGL_NONE.
+ */
+class CEGLAttributesVec
+{
+public:
+ struct EGLAttribute
+ {
+ EGLint key;
+ EGLint value;
+ };
+
+ /**
+ * Add multiple attributes
+ *
+ * The array is automatically terminated with EGL_NONE
+ */
+ void Add(std::initializer_list<EGLAttribute> const& attributes)
+ {
+ for (auto const& attribute : attributes)
+ {
+ m_attributes.insert(m_attributes.begin(), attribute.value);
+ m_attributes.insert(m_attributes.begin(), attribute.key);
+ }
+ }
+
+ /**
+ * Add one attribute
+ *
+ * The array is automatically terminated with EGL_NONE
+ */
+ void Add(EGLAttribute const& attribute)
+ {
+ Add({attribute});
+ }
+
+ EGLint const * Get() const
+ {
+ return m_attributes.data();
+ }
+
+private:
+ std::vector<EGLint> m_attributes{EGL_NONE};
+};
+
+/**
+ * Convenience wrapper for stack-allocated EGL attribute arrays
+ *
+ * The wrapper makes sure that the key/value pairs are always written in actual
+ * pairs, that the array is always terminated with EGL_NONE, and that the bounds
+ * of the array are not exceeded (checked on runtime).
+ *
+ * \tparam AttributeCount maximum number of attributes that can be added.
+ * Determines the size of the storage array.
+ */
+template<std::size_t AttributeCount>
+class CEGLAttributes
+{
+public:
+ struct EGLAttribute
+ {
+ EGLint key;
+ EGLint value;
+ };
+
+ CEGLAttributes()
+ {
+ m_attributes[0] = EGL_NONE;
+ }
+
+ /**
+ * Add multiple attributes
+ *
+ * The array is automatically terminated with EGL_NONE
+ *
+ * \throws std::out_of_range if more than AttributeCount attributes are added
+ * in total
+ */
+ void Add(std::initializer_list<EGLAttribute> const& attributes)
+ {
+ if (m_writePosition + attributes.size() * 2 + 1 > m_attributes.size())
+ {
+ throw std::out_of_range("CEGLAttributes::Add");
+ }
+
+ for (auto const& attribute : attributes)
+ {
+ m_attributes[m_writePosition++] = attribute.key;
+ m_attributes[m_writePosition++] = attribute.value;
+ }
+ m_attributes[m_writePosition] = EGL_NONE;
+ }
+
+ /**
+ * Add one attribute
+ *
+ * The array is automatically terminated with EGL_NONE
+ *
+ * \throws std::out_of_range if more than AttributeCount attributes are added
+ * in total
+ */
+ void Add(EGLAttribute const& attribute)
+ {
+ Add({attribute});
+ }
+
+ EGLint const * Get() const
+ {
+ return m_attributes.data();
+ }
+
+ int Size() const
+ {
+ return m_writePosition;
+ }
+
+private:
+ std::array<EGLint, AttributeCount * 2 + 1> m_attributes;
+ int m_writePosition{};
+};
+
+class CEGLContextUtils final
+{
+public:
+ CEGLContextUtils() = default;
+ /**
+ * \param platform platform as constant from an extension building on EGL_EXT_platform_base
+ */
+ CEGLContextUtils(EGLenum platform, std::string const& platformExtension);
+ ~CEGLContextUtils();
+
+ bool CreateDisplay(EGLNativeDisplayType nativeDisplay);
+ /**
+ * Create EGLDisplay with EGL_EXT_platform_base
+ *
+ * Falls back to \ref CreateDisplay (with nativeDisplayLegacy) on failure.
+ * The native displays to use with the platform-based and the legacy approach
+ * may be defined to have different types and/or semantics, so this function takes
+ * both as separate parameters.
+ *
+ * \param nativeDisplay native display to use with eglGetPlatformDisplayEXT
+ * \param nativeDisplayLegacy native display to use with eglGetDisplay
+ */
+ bool CreatePlatformDisplay(void* nativeDisplay, EGLNativeDisplayType nativeDisplayLegacy);
+
+ void SurfaceAttrib(EGLint attribute, EGLint value);
+ bool CreateSurface(EGLNativeWindowType nativeWindow, EGLint HDRcolorSpace = EGL_NONE);
+ bool CreatePlatformSurface(void* nativeWindow, EGLNativeWindowType nativeWindowLegacy);
+ bool InitializeDisplay(EGLint renderingApi);
+ bool ChooseConfig(EGLint renderableType, EGLint visualId = 0, bool hdr = false);
+ bool CreateContext(CEGLAttributesVec contextAttribs);
+ bool BindContext();
+ void Destroy();
+ void DestroySurface();
+ void DestroyContext();
+ bool SetVSync(bool enable);
+ bool TrySwapBuffers();
+ bool IsPlatformSupported() const;
+ EGLint GetConfigAttrib(EGLint attribute) const;
+
+ EGLDisplay GetEGLDisplay() const
+ {
+ return m_eglDisplay;
+ }
+ EGLSurface GetEGLSurface() const
+ {
+ return m_eglSurface;
+ }
+ EGLContext GetEGLContext() const
+ {
+ return m_eglContext;
+ }
+ EGLConfig GetEGLConfig() const
+ {
+ return m_eglConfig;
+ }
+
+private:
+ void SurfaceAttrib();
+
+ EGLenum m_platform{EGL_NONE};
+ bool m_platformSupported{false};
+
+ EGLDisplay m_eglDisplay{EGL_NO_DISPLAY};
+ EGLSurface m_eglSurface{EGL_NO_SURFACE};
+ EGLContext m_eglContext{EGL_NO_CONTEXT};
+ EGLConfig m_eglConfig{}, m_eglHDRConfig{};
+};
diff --git a/xbmc/utils/EmbeddedArt.cpp b/xbmc/utils/EmbeddedArt.cpp
new file mode 100644
index 0000000..3af2259
--- /dev/null
+++ b/xbmc/utils/EmbeddedArt.cpp
@@ -0,0 +1,72 @@
+/*
+ * 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 "EmbeddedArt.h"
+
+#include "Archive.h"
+
+EmbeddedArtInfo::EmbeddedArtInfo(size_t size,
+ const std::string &mime, const std::string& type)
+{
+ Set(size, mime, type);
+}
+
+void EmbeddedArtInfo::Set(size_t size, const std::string &mime, const std::string& type)
+{
+ m_size = size;
+ m_mime = mime;
+ m_type = type;
+}
+
+void EmbeddedArtInfo::Clear()
+{
+ m_mime.clear();
+ m_size = 0;
+}
+
+bool EmbeddedArtInfo::Empty() const
+{
+ return m_size == 0;
+}
+
+bool EmbeddedArtInfo::Matches(const EmbeddedArtInfo &right) const
+{
+ return (m_size == right.m_size &&
+ m_mime == right.m_mime &&
+ m_type == right.m_type);
+}
+
+void EmbeddedArtInfo::Archive(CArchive &ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_size;
+ ar << m_mime;
+ ar << m_type;
+ }
+ else
+ {
+ ar >> m_size;
+ ar >> m_mime;
+ ar >> m_type;
+ }
+}
+
+EmbeddedArt::EmbeddedArt(const uint8_t *data, size_t size,
+ const std::string &mime, const std::string& type)
+{
+ Set(data, size, mime, type);
+}
+
+void EmbeddedArt::Set(const uint8_t *data, size_t size,
+ const std::string &mime, const std::string& type)
+{
+ EmbeddedArtInfo::Set(size, mime, type);
+ m_data.resize(size);
+ m_data.assign(data, data+size);
+}
diff --git a/xbmc/utils/EmbeddedArt.h b/xbmc/utils/EmbeddedArt.h
new file mode 100644
index 0000000..b752bac
--- /dev/null
+++ b/xbmc/utils/EmbeddedArt.h
@@ -0,0 +1,49 @@
+/*
+ * 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 "IArchivable.h"
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+class EmbeddedArtInfo : public IArchivable
+{
+public:
+ EmbeddedArtInfo() = default;
+ EmbeddedArtInfo(size_t size, const std::string &mime, const std::string& type = "");
+ virtual ~EmbeddedArtInfo() = default;
+
+ // implementation of IArchivable
+ void Archive(CArchive& ar) override;
+
+ void Set(size_t size, const std::string &mime, const std::string& type = "");
+ void Clear();
+ bool Empty() const;
+ bool Matches(const EmbeddedArtInfo &right) const;
+ void SetType(const std::string& type) { m_type = type; }
+
+ size_t m_size = 0;
+ std::string m_mime;
+ std::string m_type;
+};
+
+class EmbeddedArt : public EmbeddedArtInfo
+{
+public:
+ EmbeddedArt() = default;
+ EmbeddedArt(const uint8_t *data, size_t size,
+ const std::string &mime, const std::string& type = "");
+
+ void Set(const uint8_t *data, size_t size,
+ const std::string &mime, const std::string& type = "");
+
+ std::vector<uint8_t> m_data;
+};
diff --git a/xbmc/utils/EndianSwap.cpp b/xbmc/utils/EndianSwap.cpp
new file mode 100644
index 0000000..3f645d9
--- /dev/null
+++ b/xbmc/utils/EndianSwap.cpp
@@ -0,0 +1,30 @@
+/*
+ * 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 "EndianSwap.h"
+
+/* based on libavformat/spdif.c */
+void Endian_Swap16_buf(uint16_t *dst, uint16_t *src, int w)
+{
+ int i;
+
+ for (i = 0; i + 8 <= w; i += 8) {
+ dst[i + 0] = Endian_Swap16(src[i + 0]);
+ dst[i + 1] = Endian_Swap16(src[i + 1]);
+ dst[i + 2] = Endian_Swap16(src[i + 2]);
+ dst[i + 3] = Endian_Swap16(src[i + 3]);
+ dst[i + 4] = Endian_Swap16(src[i + 4]);
+ dst[i + 5] = Endian_Swap16(src[i + 5]);
+ dst[i + 6] = Endian_Swap16(src[i + 6]);
+ dst[i + 7] = Endian_Swap16(src[i + 7]);
+ }
+
+ for (; i < w; i++)
+ dst[i + 0] = Endian_Swap16(src[i + 0]);
+}
+
diff --git a/xbmc/utils/EndianSwap.h b/xbmc/utils/EndianSwap.h
new file mode 100644
index 0000000..0b02689
--- /dev/null
+++ b/xbmc/utils/EndianSwap.h
@@ -0,0 +1,90 @@
+/*
+ * 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
+
+ /* Endian_SwapXX functions taken from SDL (SDL_endian.h) */
+
+#ifdef TARGET_POSIX
+#include <inttypes.h>
+#elif TARGET_WINDOWS
+#define __inline__ __inline
+#include <stdint.h>
+#endif
+
+
+/* Set up for C function definitions, even when using C++ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if defined(__GNUC__) && (defined(__powerpc__) || defined(__ppc__))
+static __inline__ uint16_t Endian_Swap16(uint16_t x)
+{
+ uint16_t result;
+
+ __asm__("rlwimi %0,%2,8,16,23" : "=&r" (result) : "0" (x >> 8), "r" (x));
+ return result;
+}
+
+static __inline__ uint32_t Endian_Swap32(uint32_t x)
+{
+ uint32_t result;
+
+ __asm__("rlwimi %0,%2,24,16,23" : "=&r" (result) : "0" (x>>24), "r" (x));
+ __asm__("rlwimi %0,%2,8,8,15" : "=&r" (result) : "0" (result), "r" (x));
+ __asm__("rlwimi %0,%2,24,0,7" : "=&r" (result) : "0" (result), "r" (x));
+ return result;
+}
+#else
+static __inline__ uint16_t Endian_Swap16(uint16_t x) {
+ return((x<<8)|(x>>8));
+}
+
+static __inline__ uint32_t Endian_Swap32(uint32_t x) {
+ return((x<<24)|((x<<8)&0x00FF0000)|((x>>8)&0x0000FF00)|(x>>24));
+}
+#endif
+
+static __inline__ uint64_t Endian_Swap64(uint64_t x) {
+ uint32_t hi, lo;
+
+ /* Separate into high and low 32-bit values and swap them */
+ lo = (uint32_t)(x&0xFFFFFFFF);
+ x >>= 32;
+ hi = (uint32_t)(x&0xFFFFFFFF);
+ x = Endian_Swap32(lo);
+ x <<= 32;
+ x |= Endian_Swap32(hi);
+ return(x);
+
+}
+
+void Endian_Swap16_buf(uint16_t *dst, uint16_t *src, int w);
+
+#ifndef WORDS_BIGENDIAN
+#define Endian_SwapLE16(X) (X)
+#define Endian_SwapLE32(X) (X)
+#define Endian_SwapLE64(X) (X)
+#define Endian_SwapBE16(X) Endian_Swap16(X)
+#define Endian_SwapBE32(X) Endian_Swap32(X)
+#define Endian_SwapBE64(X) Endian_Swap64(X)
+#else
+#define Endian_SwapLE16(X) Endian_Swap16(X)
+#define Endian_SwapLE32(X) Endian_Swap32(X)
+#define Endian_SwapLE64(X) Endian_Swap64(X)
+#define Endian_SwapBE16(X) (X)
+#define Endian_SwapBE32(X) (X)
+#define Endian_SwapBE64(X) (X)
+#endif
+
+/* Ends C function definitions when using C++ */
+#ifdef __cplusplus
+}
+#endif
+
diff --git a/xbmc/utils/EventStream.h b/xbmc/utils/EventStream.h
new file mode 100644
index 0000000..6fcb651
--- /dev/null
+++ b/xbmc/utils/EventStream.h
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016-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 "EventStreamDetail.h"
+#include "JobManager.h"
+#include "threads/CriticalSection.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <vector>
+
+
+template<typename Event>
+class CEventStream
+{
+public:
+
+ template<typename A>
+ void Subscribe(A* owner, void (A::*fn)(const Event&))
+ {
+ auto subscription = std::make_shared<detail::CSubscription<Event, A>>(owner, fn);
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ m_subscriptions.emplace_back(std::move(subscription));
+ }
+
+ template<typename A>
+ void Unsubscribe(A* obj)
+ {
+ std::vector<std::shared_ptr<detail::ISubscription<Event>>> toCancel;
+ {
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ auto it = m_subscriptions.begin();
+ while (it != m_subscriptions.end())
+ {
+ if ((*it)->IsOwnedBy(obj))
+ {
+ toCancel.push_back(*it);
+ it = m_subscriptions.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+ for (auto& subscription : toCancel)
+ subscription->Cancel();
+ }
+
+protected:
+ std::vector<std::shared_ptr<detail::ISubscription<Event>>> m_subscriptions;
+ CCriticalSection m_criticalSection;
+};
+
+
+template<typename Event>
+class CEventSource : public CEventStream<Event>
+{
+public:
+ explicit CEventSource() : m_queue(false, 1, CJob::PRIORITY_HIGH) {}
+
+ template<typename A>
+ void Publish(A event)
+ {
+ std::unique_lock<CCriticalSection> lock(this->m_criticalSection);
+ auto& subscriptions = this->m_subscriptions;
+ auto task = [subscriptions, event](){
+ for (auto& s: subscriptions)
+ s->HandleEvent(event);
+ };
+ lock.unlock();
+ m_queue.Submit(std::move(task));
+ }
+
+private:
+ CJobQueue m_queue;
+};
+
+template<typename Event>
+class CBlockingEventSource : public CEventStream<Event>
+{
+public:
+ template<typename A>
+ void HandleEvent(A event)
+ {
+ std::unique_lock<CCriticalSection> lock(this->m_criticalSection);
+ for (const auto& subscription : this->m_subscriptions)
+ {
+ subscription->HandleEvent(event);
+ }
+ }
+};
diff --git a/xbmc/utils/EventStreamDetail.h b/xbmc/utils/EventStreamDetail.h
new file mode 100644
index 0000000..31cd0b5
--- /dev/null
+++ b/xbmc/utils/EventStreamDetail.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016-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 <mutex>
+
+namespace detail
+{
+
+template<typename Event>
+class ISubscription
+{
+public:
+ virtual void HandleEvent(const Event& event) = 0;
+ virtual void Cancel() = 0;
+ virtual bool IsOwnedBy(void* obj) = 0;
+ virtual ~ISubscription() = default;
+};
+
+template<typename Event, typename Owner>
+class CSubscription : public ISubscription<Event>
+{
+public:
+ typedef void (Owner::*Fn)(const Event&);
+ CSubscription(Owner* owner, Fn fn);
+ void HandleEvent(const Event& event) override;
+ void Cancel() override;
+ bool IsOwnedBy(void *obj) override;
+
+private:
+ Owner* m_owner;
+ Fn m_eventHandler;
+ CCriticalSection m_criticalSection;
+};
+
+template<typename Event, typename Owner>
+CSubscription<Event, Owner>::CSubscription(Owner* owner, Fn fn)
+ : m_owner(owner), m_eventHandler(fn)
+{}
+
+template<typename Event, typename Owner>
+bool CSubscription<Event, Owner>::IsOwnedBy(void* obj)
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ return obj != nullptr && obj == m_owner;
+}
+
+template<typename Event, typename Owner>
+void CSubscription<Event, Owner>::Cancel()
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ m_owner = nullptr;
+}
+
+template<typename Event, typename Owner>
+void CSubscription<Event, Owner>::HandleEvent(const Event& event)
+{
+ std::unique_lock<CCriticalSection> lock(m_criticalSection);
+ if (m_owner)
+ (m_owner->*m_eventHandler)(event);
+}
+}
diff --git a/xbmc/utils/ExecString.cpp b/xbmc/utils/ExecString.cpp
new file mode 100644
index 0000000..37650e0
--- /dev/null
+++ b/xbmc/utils/ExecString.cpp
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2022 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 "ExecString.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+CExecString::CExecString(const std::string& execString)
+{
+ m_valid = Parse(execString);
+}
+
+CExecString::CExecString(const std::string& function, const std::vector<std::string>& params)
+ : m_function(function), m_params(params)
+{
+ m_valid = !m_function.empty();
+
+ if (m_valid)
+ SetExecString();
+}
+
+CExecString::CExecString(const CFileItem& item, const std::string& contextWindow)
+{
+ m_valid = Parse(item, contextWindow);
+}
+
+namespace
+{
+void SplitParams(const std::string& paramString, std::vector<std::string>& parameters)
+{
+ bool inQuotes = false;
+ bool lastEscaped = false; // only every second character can be escaped
+ int inFunction = 0;
+ size_t whiteSpacePos = 0;
+ std::string parameter;
+ parameters.clear();
+ for (size_t pos = 0; pos < paramString.size(); pos++)
+ {
+ char ch = paramString[pos];
+ bool escaped = (pos > 0 && paramString[pos - 1] == '\\' && !lastEscaped);
+ lastEscaped = escaped;
+ if (inQuotes)
+ { // if we're in a quote, we accept everything until the closing quote
+ if (ch == '"' && !escaped)
+ { // finished a quote - no need to add the end quote to our string
+ inQuotes = false;
+ }
+ }
+ else
+ { // not in a quote, so check if we should be starting one
+ if (ch == '"' && !escaped)
+ { // start of quote - no need to add the quote to our string
+ inQuotes = true;
+ }
+ if (inFunction && ch == ')')
+ { // end of a function
+ inFunction--;
+ }
+ if (ch == '(')
+ { // start of function
+ inFunction++;
+ }
+ if (!inFunction && ch == ',')
+ { // not in a function, so a comma signifies the end of this parameter
+ if (whiteSpacePos)
+ parameter = parameter.substr(0, whiteSpacePos);
+ // trim off start and end quotes
+ if (parameter.length() > 1 && parameter[0] == '"' &&
+ parameter[parameter.length() - 1] == '"')
+ parameter = parameter.substr(1, parameter.length() - 2);
+ else if (parameter.length() > 3 && parameter[parameter.length() - 1] == '"')
+ {
+ // check name="value" style param.
+ size_t quotaPos = parameter.find('"');
+ if (quotaPos > 1 && quotaPos < parameter.length() - 1 && parameter[quotaPos - 1] == '=')
+ {
+ parameter.erase(parameter.length() - 1);
+ parameter.erase(quotaPos);
+ }
+ }
+ parameters.push_back(parameter);
+ parameter.clear();
+ whiteSpacePos = 0;
+ continue;
+ }
+ }
+ if ((ch == '"' || ch == '\\') && escaped)
+ { // escaped quote or backslash
+ parameter[parameter.size() - 1] = ch;
+ continue;
+ }
+ // whitespace handling - we skip any whitespace at the left or right of an unquoted parameter
+ if (ch == ' ' && !inQuotes)
+ {
+ if (parameter.empty()) // skip whitespace on left
+ continue;
+ if (!whiteSpacePos) // make a note of where whitespace starts on the right
+ whiteSpacePos = parameter.size();
+ }
+ else
+ whiteSpacePos = 0;
+ parameter += ch;
+ }
+ if (inFunction || inQuotes)
+ CLog::Log(LOGWARNING, "{}({}) - end of string while searching for ) or \"", __FUNCTION__,
+ paramString);
+ if (whiteSpacePos)
+ parameter.erase(whiteSpacePos);
+ // trim off start and end quotes
+ if (parameter.size() > 1 && parameter[0] == '"' && parameter[parameter.size() - 1] == '"')
+ parameter = parameter.substr(1, parameter.size() - 2);
+ else if (parameter.size() > 3 && parameter[parameter.size() - 1] == '"')
+ {
+ // check name="value" style param.
+ size_t quotaPos = parameter.find('"');
+ if (quotaPos > 1 && quotaPos < parameter.length() - 1 && parameter[quotaPos - 1] == '=')
+ {
+ parameter.erase(parameter.length() - 1);
+ parameter.erase(quotaPos);
+ }
+ }
+ if (!parameter.empty() || parameters.size())
+ parameters.push_back(parameter);
+}
+
+void SplitExecFunction(const std::string& execString,
+ std::string& function,
+ std::vector<std::string>& parameters)
+{
+ std::string paramString;
+
+ size_t iPos = execString.find('(');
+ size_t iPos2 = execString.rfind(')');
+ if (iPos != std::string::npos && iPos2 != std::string::npos)
+ {
+ paramString = execString.substr(iPos + 1, iPos2 - iPos - 1);
+ function = execString.substr(0, iPos);
+ }
+ else
+ function = execString;
+
+ // remove any whitespace, and the standard prefix (if it exists)
+ StringUtils::Trim(function);
+
+ SplitParams(paramString, parameters);
+}
+} // namespace
+
+bool CExecString::Parse(const std::string& execString)
+{
+ m_execString = execString;
+ SplitExecFunction(m_execString, m_function, m_params);
+
+ // Keep original function case in execstring, lowercase it in function
+ StringUtils::ToLower(m_function);
+ return true;
+}
+
+bool CExecString::Parse(const CFileItem& item, const std::string& contextWindow)
+{
+ if (item.IsFavourite())
+ {
+ const CURL url(item.GetPath());
+ Parse(CURL::Decode(url.GetHostName()));
+ }
+ else if (item.m_bIsFolder &&
+ (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_playlistAsFolders ||
+ !(item.IsSmartPlayList() || item.IsPlayList())))
+ {
+ if (!contextWindow.empty())
+ Build("ActivateWindow", {contextWindow, StringUtils::Paramify(item.GetPath()), "return"});
+ }
+ else if (item.IsScript() && item.GetPath().size() > 9) // script://<foo>
+ Build("RunScript", {StringUtils::Paramify(item.GetPath().substr(9))});
+ else if (item.IsAddonsPath() && item.GetPath().size() > 9) // addons://<foo>
+ {
+ const CURL url(item.GetPath());
+ if (url.GetHostName() == "install")
+ Build("InstallFromZip", {});
+ else if (url.GetHostName() == "check_for_updates")
+ Build("UpdateAddonRepos", {"showProgress"});
+ else
+ Build("RunAddon", {StringUtils::Paramify(url.GetFileName())});
+ }
+ else if (item.IsAndroidApp() && item.GetPath().size() > 26) // androidapp://sources/apps/<foo>
+ Build("StartAndroidActivity", {StringUtils::Paramify(item.GetPath().substr(26))});
+ else // assume a media file
+ {
+ if (item.IsVideoDb() && item.HasVideoInfoTag())
+ BuildPlayMedia(item, StringUtils::Paramify(item.GetVideoInfoTag()->m_strFileNameAndPath));
+ else if (item.IsMusicDb() && item.HasMusicInfoTag())
+ BuildPlayMedia(item, StringUtils::Paramify(item.GetMusicInfoTag()->GetURL()));
+ else if (item.IsPicture())
+ Build("ShowPicture", {StringUtils::Paramify(item.GetPath())});
+ else
+ {
+ // Everything else will be treated as PlayMedia for item's path
+ BuildPlayMedia(item, StringUtils::Paramify(item.GetPath()));
+ }
+ }
+ return true;
+}
+
+void CExecString::Build(const std::string& function, const std::vector<std::string>& params)
+{
+ m_function = function;
+ m_params = params;
+ SetExecString();
+}
+
+void CExecString::BuildPlayMedia(const CFileItem& item, const std::string& target)
+{
+ std::vector<std::string> params{target};
+
+ if (item.HasProperty("playlist_type_hint"))
+ params.emplace_back("playlist_type_hint=" + item.GetProperty("playlist_type_hint").asString());
+
+ Build("PlayMedia", params);
+}
+
+void CExecString::SetExecString()
+{
+ if (m_params.empty())
+ m_execString = m_function;
+ else
+ m_execString = StringUtils::Format("{}({})", m_function, StringUtils::Join(m_params, ","));
+
+ // Keep original function case in execstring, lowercase it in function
+ StringUtils::ToLower(m_function);
+}
diff --git a/xbmc/utils/ExecString.h b/xbmc/utils/ExecString.h
new file mode 100644
index 0000000..fda234a
--- /dev/null
+++ b/xbmc/utils/ExecString.h
@@ -0,0 +1,46 @@
+/*
+ * 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 <string>
+#include <vector>
+
+class CFileItem;
+
+class CExecString
+{
+public:
+ CExecString() = default;
+ explicit CExecString(const std::string& execString);
+ CExecString(const std::string& function, const std::vector<std::string>& params);
+ CExecString(const CFileItem& item, const std::string& contextWindow);
+
+ virtual ~CExecString() = default;
+
+ std::string GetExecString() const { return m_execString; }
+
+ bool IsValid() const { return m_valid; }
+
+ std::string GetFunction() const { return m_function; }
+ std::vector<std::string> GetParams() const { return m_params; }
+
+private:
+ bool Parse(const std::string& execString);
+ bool Parse(const CFileItem& item, const std::string& contextWindow);
+
+ void Build(const std::string& function, const std::vector<std::string>& params);
+ void BuildPlayMedia(const CFileItem& item, const std::string& target);
+
+ void SetExecString();
+
+ bool m_valid{false};
+ std::string m_function;
+ std::vector<std::string> m_params;
+ std::string m_execString;
+};
diff --git a/xbmc/utils/Fanart.cpp b/xbmc/utils/Fanart.cpp
new file mode 100644
index 0000000..5e385ee
--- /dev/null
+++ b/xbmc/utils/Fanart.cpp
@@ -0,0 +1,176 @@
+/*
+ * 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 "Fanart.h"
+
+#include "StringUtils.h"
+#include "URIUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+
+#include <algorithm>
+#include <functional>
+
+const unsigned int CFanart::max_fanart_colors=3;
+
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+/// CFanart Functions
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+CFanart::CFanart() = default;
+
+void CFanart::Pack()
+{
+ // Take our data and pack it into the m_xml string
+ m_xml.clear();
+ TiXmlElement fanart("fanart");
+ for (std::vector<SFanartData>::const_iterator it = m_fanart.begin(); it != m_fanart.end(); ++it)
+ {
+ TiXmlElement thumb("thumb");
+ thumb.SetAttribute("colors", it->strColors.c_str());
+ thumb.SetAttribute("preview", it->strPreview.c_str());
+ TiXmlText text(it->strImage);
+ thumb.InsertEndChild(text);
+ fanart.InsertEndChild(thumb);
+ }
+ m_xml << fanart;
+}
+
+void CFanart::AddFanart(const std::string& image, const std::string& preview, const std::string& colors)
+{
+ SFanartData info;
+ info.strPreview = preview;
+ info.strImage = image;
+ ParseColors(colors, info.strColors);
+ m_fanart.push_back(std::move(info));
+}
+
+void CFanart::Clear()
+{
+ m_fanart.clear();
+ m_xml.clear();
+}
+
+bool CFanart::Unpack()
+{
+ CXBMCTinyXML doc;
+ doc.Parse(m_xml);
+
+ m_fanart.clear();
+
+ TiXmlElement *fanart = doc.FirstChildElement("fanart");
+ while (fanart)
+ {
+ std::string url = XMLUtils::GetAttribute(fanart, "url");
+ TiXmlElement *fanartThumb = fanart->FirstChildElement("thumb");
+ while (fanartThumb)
+ {
+ if (!fanartThumb->NoChildren())
+ {
+ SFanartData data;
+ if (url.empty())
+ {
+ data.strImage = fanartThumb->FirstChild()->ValueStr();
+ data.strPreview = XMLUtils::GetAttribute(fanartThumb, "preview");
+ }
+ else
+ {
+ data.strImage = URIUtils::AddFileToFolder(url, fanartThumb->FirstChild()->ValueStr());
+ if (fanartThumb->Attribute("preview"))
+ data.strPreview = URIUtils::AddFileToFolder(url, fanartThumb->Attribute("preview"));
+ }
+ ParseColors(XMLUtils::GetAttribute(fanartThumb, "colors"), data.strColors);
+ m_fanart.push_back(data);
+ }
+ fanartThumb = fanartThumb->NextSiblingElement("thumb");
+ }
+ fanart = fanart->NextSiblingElement("fanart");
+ }
+ return true;
+}
+
+std::string CFanart::GetImageURL(unsigned int index) const
+{
+ if (index >= m_fanart.size())
+ return "";
+
+ return m_fanart[index].strImage;
+}
+
+std::string CFanart::GetPreviewURL(unsigned int index) const
+{
+ if (index >= m_fanart.size())
+ return "";
+
+ return m_fanart[index].strPreview.empty() ? m_fanart[index].strImage : m_fanart[index].strPreview;
+}
+
+const std::string CFanart::GetColor(unsigned int index) const
+{
+ if (index >= max_fanart_colors || m_fanart.empty() ||
+ m_fanart[0].strColors.size() < index*9+8)
+ return "FFFFFFFF";
+
+ // format is AARRGGBB,AARRGGBB etc.
+ return m_fanart[0].strColors.substr(index*9, 8);
+}
+
+bool CFanart::SetPrimaryFanart(unsigned int index)
+{
+ if (index >= m_fanart.size())
+ return false;
+
+ std::iter_swap(m_fanart.begin()+index, m_fanart.begin());
+
+ // repack our data
+ Pack();
+
+ return true;
+}
+
+unsigned int CFanart::GetNumFanarts() const
+{
+ return m_fanart.size();
+}
+
+bool CFanart::ParseColors(const std::string &colorsIn, std::string &colorsOut)
+{
+ // Formats:
+ // 0: XBMC ARGB Hexadecimal string comma separated "FFFFFFFF,DDDDDDDD,AAAAAAAA"
+ // 1: The TVDB RGB Int Triplets, pipe separate with leading/trailing pipes "|68,69,59|69,70,58|78,78,68|"
+
+ // Essentially we read the colors in using the proper format, and store them in our own fixed temporary format (3 DWORDS), and then
+ // write them back in the specified format.
+
+ if (colorsIn.empty())
+ return false;
+
+ // check for the TVDB RGB triplets "|68,69,59|69,70,58|78,78,68|"
+ if (colorsIn[0] == '|')
+ { // need conversion
+ colorsOut.clear();
+ std::vector<std::string> strColors = StringUtils::Split(colorsIn, "|");
+ for (int i = 0; i < std::min((int)strColors.size()-1, (int)max_fanart_colors); i++)
+ { // split up each color
+ std::vector<std::string> strTriplets = StringUtils::Split(strColors[i+1], ",");
+ if (strTriplets.size() == 3)
+ { // convert
+ if (colorsOut.size())
+ colorsOut += ",";
+ colorsOut += StringUtils::Format("FF{:2x}{:2x}{:2x}", std::stol(strTriplets[0]),
+ std::stol(strTriplets[1]), std::stol(strTriplets[2]));
+ }
+ }
+ }
+ else
+ { // assume is our format
+ colorsOut = colorsIn;
+ }
+ return true;
+}
diff --git a/xbmc/utils/Fanart.h b/xbmc/utils/Fanart.h
new file mode 100644
index 0000000..c47d2df
--- /dev/null
+++ b/xbmc/utils/Fanart.h
@@ -0,0 +1,107 @@
+/*
+ * 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
+
+// Fanart.h
+//////////////////////////////////////////////////////////////////////
+
+#include <string>
+#include <vector>
+
+///
+/// /brief CFanart is the core of fanart support and contains all fanart data for a specific show
+///
+/// CFanart stores all data related to all available fanarts for a given TV show and provides
+/// functions required to manipulate and access that data.
+/// In order to provide an interface between the fanart data and the XBMC database, all data
+/// is stored internally it its own form, as well as packed into an XML formatted string
+/// stored in the member variable m_xml.
+/// Information on multiple fanarts for a given show is stored, but XBMC only cares about the
+/// very first fanart stored. These interfaces provide a means to access the data in that first
+/// fanart record, as well as to set which fanart is the first record. Externally, all XBMC needs
+/// to care about is getting and setting that first record. Everything else is maintained internally
+/// by CFanart. This point is key to using the interface properly.
+class CFanart
+{
+public:
+ ///
+ /// Standard constructor doesn't need to do anything
+ CFanart();
+ ///
+ /// Takes the internal fanart data and packs it into an XML formatted string in m_xml
+ /// \sa m_xml
+ void Pack();
+ ///
+ /// Takes the XML formatted string m_xml and unpacks the fanart data contained into the internal data
+ /// \return A boolean indicating success or failure
+ /// \sa m_xml
+ bool Unpack();
+ ///
+ /// Retrieves the fanart full res image URL
+ /// \param index - index of image to retrieve (defaults to 0)
+ /// \return A string containing the full URL to the full resolution fanart image
+ std::string GetImageURL(unsigned int index = 0) const;
+ ///
+ /// Retrieves the fanart preview image URL, or full res image URL if that doesn't exist
+ /// \param index - index of image to retrieve (defaults to 0)
+ /// \return A string containing the full URL to the full resolution fanart image
+ std::string GetPreviewURL(unsigned int index = 0) const;
+ ///
+ /// Used to return a specified fanart theme color value
+ /// \param index: 0 based index of the color to retrieve. A fanart theme contains 3 colors, indices 0-2, arranged from darkest to lightest.
+ const std::string GetColor(unsigned int index) const;
+ ///
+ /// Sets a particular fanart to be the "primary" fanart, or in other words, sets which fanart is actually used by XBMC
+ ///
+ /// This is the one of the only instances in the public interface where there is any hint that more than one fanart exists, but its by necessity.
+ /// \param index: 0 based index of which fanart to set as the primary fanart
+ /// \return A boolean value indicating success or failure. This should only return false if the specified index is not a valid fanart
+ bool SetPrimaryFanart(unsigned int index);
+ ///
+ /// Returns how many fanarts are stored
+ /// \return An integer indicating how many fanarts are stored in the class. Fanart indices are 0 to (GetNumFanarts() - 1)
+ unsigned int GetNumFanarts() const;
+ /// Adds an image to internal fanart data
+ void AddFanart(const std::string& image, const std::string& preview, const std::string& colors);
+ /// Clear all internal fanart data
+ void Clear();
+ ///
+ /// m_xml contains an XML formatted string which is all fanart packed into one string.
+ ///
+ /// This string is the "interface" as it were to the XBMC database, and MUST be kept in sync with the rest of the class. Therefore
+ /// anytime this string is changed, the change should be followed up by a call to CFanart::UnPack(). This XML formatted string is
+ /// also the interface used to pass the fanart data from the scraper to CFanart.
+ std::string m_xml;
+private:
+ static const unsigned int max_fanart_colors;
+ ///
+ /// Parse various color formats as returned by the sites scraped into a format we recognize
+ ///
+ /// Supported Formats:
+ ///
+ /// * The TVDB RGB Int Triplets, pipe separate with leading/trailing pipes "|68,69,59|69,70,58|78,78,68|"
+ /// * XBMC ARGB Hexadecimal string comma separated "FFFFFFFF,DDDDDDDD,AAAAAAAA"
+ ///
+ /// \param colorsIn: string containing colors in some format to be converted
+ /// \param colorsOut: XBMC ARGB Hexadecimal string comma separated "FFFFFFFF,DDDDDDDD,AAAAAAAA"
+ /// \return boolean indicating success or failure.
+ static bool ParseColors(const std::string&colorsIn, std::string&colorsOut);
+
+ struct SFanartData
+ {
+ std::string strImage;
+ std::string strColors;
+ std::string strPreview;
+ };
+
+ ///
+ /// std::vector that stores all our fanart data
+ std::vector<SFanartData> m_fanart;
+};
+
diff --git a/xbmc/utils/FileExtensionProvider.cpp b/xbmc/utils/FileExtensionProvider.cpp
new file mode 100644
index 0000000..79ce46c
--- /dev/null
+++ b/xbmc/utils/FileExtensionProvider.cpp
@@ -0,0 +1,263 @@
+/*
+ * 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 "FileExtensionProvider.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonEvents.h"
+#include "addons/AddonManager.h"
+#include "addons/AudioDecoder.h"
+#include "addons/ExtsMimeSupportList.h"
+#include "addons/ImageDecoder.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+
+#include <string>
+#include <vector>
+
+using namespace ADDON;
+using namespace KODI::ADDONS;
+
+const std::vector<AddonType> ADDON_TYPES = {AddonType::VFS, AddonType::IMAGEDECODER,
+ AddonType::AUDIODECODER};
+
+CFileExtensionProvider::CFileExtensionProvider(ADDON::CAddonMgr& addonManager)
+ : m_advancedSettings(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()),
+ m_addonManager(addonManager)
+{
+ SetAddonExtensions();
+
+ m_addonManager.Events().Subscribe(this, &CFileExtensionProvider::OnAddonEvent);
+}
+
+CFileExtensionProvider::~CFileExtensionProvider()
+{
+ m_addonManager.Events().Unsubscribe(this);
+
+ m_advancedSettings.reset();
+ m_addonExtensions.clear();
+}
+
+std::string CFileExtensionProvider::GetDiscStubExtensions() const
+{
+ return m_advancedSettings->m_discStubExtensions;
+}
+
+std::string CFileExtensionProvider::GetMusicExtensions() const
+{
+ std::string extensions(m_advancedSettings->m_musicExtensions);
+ extensions += '|' + GetAddonExtensions(AddonType::VFS);
+ extensions += '|' + GetAddonExtensions(AddonType::AUDIODECODER);
+
+ return extensions;
+}
+
+std::string CFileExtensionProvider::GetPictureExtensions() const
+{
+ std::string extensions(m_advancedSettings->m_pictureExtensions);
+ extensions += '|' + GetAddonExtensions(AddonType::VFS);
+ extensions += '|' + GetAddonExtensions(AddonType::IMAGEDECODER);
+
+ return extensions;
+}
+
+std::string CFileExtensionProvider::GetSubtitleExtensions() const
+{
+ std::string extensions(m_advancedSettings->m_subtitlesExtensions);
+ extensions += '|' + GetAddonExtensions(AddonType::VFS);
+
+ return extensions;
+}
+
+std::string CFileExtensionProvider::GetVideoExtensions() const
+{
+ std::string extensions(m_advancedSettings->m_videoExtensions);
+ if (!extensions.empty())
+ extensions += '|';
+ extensions += GetAddonExtensions(AddonType::VFS);
+
+ return extensions;
+}
+
+std::string CFileExtensionProvider::GetFileFolderExtensions() const
+{
+ std::string extensions(GetAddonFileFolderExtensions(AddonType::VFS));
+ if (!extensions.empty())
+ extensions += '|';
+ extensions += GetAddonFileFolderExtensions(AddonType::AUDIODECODER);
+
+ return extensions;
+}
+
+bool CFileExtensionProvider::CanOperateExtension(const std::string& path) const
+{
+ /*!
+ * @todo Improve this function to support all cases and not only audio decoder.
+ */
+
+ // Get file extensions to find addon related to it.
+ std::string strExtension = URIUtils::GetExtension(path);
+ StringUtils::ToLower(strExtension);
+ if (!strExtension.empty() && CServiceBroker::IsAddonInterfaceUp())
+ {
+ std::vector<std::unique_ptr<KODI::ADDONS::IAddonSupportCheck>> supportList;
+
+ auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetExtensionSupportedAddonInfos(
+ strExtension, CExtsMimeSupportList::FilterSelect::all);
+ for (const auto& addonInfo : addonInfos)
+ {
+ switch (addonInfo.first)
+ {
+ case AddonType::AUDIODECODER:
+ supportList.emplace_back(new CAudioDecoder(addonInfo.second));
+ break;
+ case AddonType::IMAGEDECODER:
+ supportList.emplace_back(new CImageDecoder(addonInfo.second, ""));
+ break;
+ default:
+ break;
+ }
+ }
+
+ /*!
+ * We expect that other addons can support the file, and return true if
+ * list empty.
+ *
+ * @todo Check addons can also be types in conflict with Kodi's
+ * supported parts!
+ *
+ * @warning This part is really big ugly at the moment and as soon as possible
+ * add about other addons where works with extensions!!!
+ * Due to @ref GetFileFolderExtensions() call from outside place before here, becomes
+ * it usable in this way, as there limited to AudioDecoder and VFS addons.
+ */
+ if (supportList.empty())
+ {
+ return true;
+ }
+
+ /*!
+ * Check all found addons about support of asked file.
+ */
+ for (const auto& addon : supportList)
+ {
+ if (addon->SupportsFile(path))
+ return true;
+ }
+ }
+
+ /*!
+ * If no file extensions present, mark it as not supported.
+ */
+ return false;
+}
+
+std::string CFileExtensionProvider::GetAddonExtensions(AddonType type) const
+{
+ auto it = m_addonExtensions.find(type);
+ if (it != m_addonExtensions.end())
+ return it->second;
+
+ return "";
+}
+
+std::string CFileExtensionProvider::GetAddonFileFolderExtensions(AddonType type) const
+{
+ auto it = m_addonFileFolderExtensions.find(type);
+ if (it != m_addonFileFolderExtensions.end())
+ return it->second;
+
+ return "";
+}
+
+void CFileExtensionProvider::SetAddonExtensions()
+{
+ for (auto const type : ADDON_TYPES)
+ {
+ SetAddonExtensions(type);
+ }
+}
+
+void CFileExtensionProvider::SetAddonExtensions(AddonType type)
+{
+ std::vector<std::string> extensions;
+ std::vector<std::string> fileFolderExtensions;
+
+ if (type == AddonType::AUDIODECODER || type == AddonType::IMAGEDECODER)
+ {
+ auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetSupportedAddonInfos(
+ CExtsMimeSupportList::FilterSelect::all);
+ for (const auto& addonInfo : addonInfos)
+ {
+ if (addonInfo.m_addonType != type)
+ continue;
+
+ for (const auto& ext : addonInfo.m_supportedExtensions)
+ {
+ extensions.push_back(ext.first);
+ if (addonInfo.m_hasTracks)
+ fileFolderExtensions.push_back(ext.first);
+ }
+ }
+ }
+ else if (type == AddonType::VFS)
+ {
+ std::vector<AddonInfoPtr> addonInfos;
+ m_addonManager.GetAddonInfos(addonInfos, true, type);
+ for (const auto& addonInfo : addonInfos)
+ {
+ std::string ext = addonInfo->Type(type)->GetValue("@extensions").asString();
+ if (!ext.empty())
+ {
+ extensions.push_back(ext);
+ if (addonInfo->Type(type)->GetValue("@filedirectories").asBoolean())
+ fileFolderExtensions.push_back(ext);
+
+ if (addonInfo->Type(type)->GetValue("@encodedhostname").asBoolean())
+ {
+ std::string prot = addonInfo->Type(type)->GetValue("@protocols").asString();
+ auto prots = StringUtils::Split(prot, "|");
+ for (const std::string& it : prots)
+ m_encoded.push_back(it);
+ }
+ }
+ }
+ }
+
+ m_addonExtensions[type] = StringUtils::Join(extensions, "|");
+ m_addonFileFolderExtensions[type] = StringUtils::Join(fileFolderExtensions, "|");
+}
+
+void CFileExtensionProvider::OnAddonEvent(const AddonEvent& event)
+{
+ if (typeid(event) == typeid(AddonEvents::Enabled) ||
+ typeid(event) == typeid(AddonEvents::Disabled) ||
+ typeid(event) == typeid(AddonEvents::ReInstalled))
+ {
+ for (auto &type : ADDON_TYPES)
+ {
+ if (m_addonManager.HasType(event.addonId, type))
+ {
+ SetAddonExtensions(type);
+ break;
+ }
+ }
+ }
+ else if (typeid(event) == typeid(AddonEvents::UnInstalled))
+ {
+ SetAddonExtensions();
+ }
+}
+
+bool CFileExtensionProvider::EncodedHostName(const std::string& protocol) const
+{
+ return std::find(m_encoded.begin(),m_encoded.end(),protocol) != m_encoded.end();
+}
diff --git a/xbmc/utils/FileExtensionProvider.h b/xbmc/utils/FileExtensionProvider.h
new file mode 100644
index 0000000..586f155
--- /dev/null
+++ b/xbmc/utils/FileExtensionProvider.h
@@ -0,0 +1,91 @@
+/*
+ * 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 <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace ADDON
+{
+enum class AddonType;
+class CAddonMgr;
+struct AddonEvent;
+}
+
+class CAdvancedSettings;
+
+class CFileExtensionProvider
+{
+public:
+ CFileExtensionProvider(ADDON::CAddonMgr& addonManager);
+ ~CFileExtensionProvider();
+
+ /*!
+ * @brief Returns a list of picture extensions
+ */
+ std::string GetPictureExtensions() const;
+
+ /*!
+ * @brief Returns a list of music extensions
+ */
+ std::string GetMusicExtensions() const;
+
+ /*!
+ * @brief Returns a list of video extensions
+ */
+ std::string GetVideoExtensions() const;
+
+ /*!
+ * @brief Returns a list of subtitle extensions
+ */
+ std::string GetSubtitleExtensions() const;
+
+ /*!
+ * @brief Returns a list of disc stub extensions
+ */
+ std::string GetDiscStubExtensions() const;
+
+ /*!
+ * @brief Returns a file folder extensions
+ */
+ std::string GetFileFolderExtensions() const;
+
+ /*!
+ * @brief Returns whether a url protocol from add-ons use encoded hostnames
+ */
+ bool EncodedHostName(const std::string& protocol) const;
+
+ /*!
+ * @brief Returns true if related provider can operate related file
+ *
+ * @note Thought for cases e.g. by ISO, where can be a video or also a SACD.
+ */
+ bool CanOperateExtension(const std::string& path) const;
+
+private:
+ std::string GetAddonExtensions(ADDON::AddonType type) const;
+ std::string GetAddonFileFolderExtensions(ADDON::AddonType type) const;
+ void SetAddonExtensions();
+ void SetAddonExtensions(ADDON::AddonType type);
+
+ void OnAddonEvent(const ADDON::AddonEvent& event);
+
+ // Construction properties
+ std::shared_ptr<CAdvancedSettings> m_advancedSettings;
+ ADDON::CAddonMgr &m_addonManager;
+
+ // File extension properties
+ std::map<ADDON::AddonType, std::string> m_addonExtensions;
+ std::map<ADDON::AddonType, std::string> m_addonFileFolderExtensions;
+
+ // Protocols from add-ons with encoded host names
+ std::vector<std::string> m_encoded;
+};
diff --git a/xbmc/utils/FileOperationJob.cpp b/xbmc/utils/FileOperationJob.cpp
new file mode 100644
index 0000000..7313a18
--- /dev/null
+++ b/xbmc/utils/FileOperationJob.cpp
@@ -0,0 +1,358 @@
+/*
+ * 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 "FileOperationJob.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/FileDirectoryFactory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+CFileOperationJob::CFileOperationJob()
+ : m_items(),
+ m_strDestFile(),
+ m_avgSpeed(),
+ m_currentOperation(),
+ m_currentFile()
+{ }
+
+CFileOperationJob::CFileOperationJob(FileAction action, CFileItemList & items,
+ const std::string& strDestFile,
+ bool displayProgress /* = false */,
+ int heading /* = 0 */, int line /* = 0 */)
+ : m_action(action),
+ m_items(),
+ m_strDestFile(strDestFile),
+ m_avgSpeed(),
+ m_currentOperation(),
+ m_currentFile(),
+ m_displayProgress(displayProgress),
+ m_heading(heading),
+ m_line(line)
+{
+ SetFileOperation(action, items, strDestFile);
+}
+
+void CFileOperationJob::SetFileOperation(FileAction action,
+ const CFileItemList& items,
+ const std::string& strDestFile)
+{
+ m_action = action;
+ m_strDestFile = strDestFile;
+
+ m_items.Clear();
+ for (int i = 0; i < items.Size(); i++)
+ m_items.Add(CFileItemPtr(new CFileItem(*items[i])));
+}
+
+bool CFileOperationJob::DoWork()
+{
+ FileOperationList ops;
+ double totalTime = 0.0;
+
+ if (m_displayProgress && GetProgressDialog() == NULL)
+ {
+ CGUIDialogExtendedProgressBar* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
+ SetProgressBar(dialog->GetHandle(GetActionString(m_action)));
+ }
+
+ bool success = DoProcess(m_action, m_items, m_strDestFile, ops, totalTime);
+
+ unsigned int size = ops.size();
+
+ double opWeight = 100.0 / totalTime;
+ double current = 0.0;
+
+ for (unsigned int i = 0; i < size && success; i++)
+ success &= ops[i].ExecuteOperation(this, current, opWeight);
+
+ MarkFinished();
+
+ return success;
+}
+
+bool CFileOperationJob::DoProcessFile(FileAction action, const std::string& strFileA, const std::string& strFileB, FileOperationList &fileOperations, double &totalTime)
+{
+ int64_t time = 1;
+
+ if (action == ActionCopy || action == ActionReplace || (action == ActionMove && !CanBeRenamed(strFileA, strFileB)))
+ {
+ struct __stat64 data;
+ if (CFile::Stat(strFileA, &data) == 0)
+ time += data.st_size;
+ }
+
+ fileOperations.push_back(CFileOperation(action, strFileA, strFileB, time));
+
+ totalTime += time;
+
+ return true;
+}
+
+bool CFileOperationJob::DoProcessFolder(FileAction action, const std::string& strPath, const std::string& strDestFile, FileOperationList &fileOperations, double &totalTime)
+{
+ // check whether this folder is a filedirectory - if so, we don't process it's contents
+ CFileItem item(strPath, false);
+ IFileDirectory *file = CFileDirectoryFactory::Create(item.GetURL(), &item);
+ if (file)
+ {
+ delete file;
+ return true;
+ }
+
+ CFileItemList items;
+ CDirectory::GetDirectory(strPath, items, "", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_GET_HIDDEN);
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr pItem = items[i];
+ pItem->Select(true);
+ }
+
+ if (!DoProcess(action, items, strDestFile, fileOperations, totalTime))
+ {
+ CLog::Log(LOGERROR, "FileManager: error while processing folder: {}", strPath);
+ return false;
+ }
+
+ if (action == ActionMove)
+ {
+ fileOperations.push_back(CFileOperation(ActionDeleteFolder, strPath, "", 1));
+ totalTime += 1.0;
+ }
+
+ return true;
+}
+
+bool CFileOperationJob::DoProcess(FileAction action,
+ const CFileItemList& items,
+ const std::string& strDestFile,
+ FileOperationList& fileOperations,
+ double& totalTime)
+{
+ for (int iItem = 0; iItem < items.Size(); ++iItem)
+ {
+ CFileItemPtr pItem = items[iItem];
+ if (pItem->IsSelected())
+ {
+ std::string strNoSlash = pItem->GetPath();
+ URIUtils::RemoveSlashAtEnd(strNoSlash);
+ std::string strFileName = URIUtils::GetFileName(strNoSlash);
+
+ // special case for upnp
+ if (URIUtils::IsUPnP(items.GetPath()) || URIUtils::IsUPnP(pItem->GetPath()))
+ {
+ // get filename from label instead of path
+ strFileName = pItem->GetLabel();
+
+ if (!pItem->m_bIsFolder && !URIUtils::HasExtension(strFileName))
+ {
+ // FIXME: for now we only work well if the url has the extension
+ // we should map the content type to the extension otherwise
+ strFileName += URIUtils::GetExtension(pItem->GetPath());
+ }
+
+ strFileName = CUtil::MakeLegalFileName(strFileName);
+ }
+
+ std::string strnewDestFile;
+ if (!strDestFile.empty()) // only do this if we have a destination
+ strnewDestFile = URIUtils::ChangeBasePath(pItem->GetPath(), strFileName, strDestFile); // Convert (URL) encoding + slashes (if source / target differ)
+
+ if (pItem->m_bIsFolder)
+ {
+ // in ActionReplace mode all subdirectories will be removed by the below
+ // DoProcessFolder(ActionDelete) call as well, so ActionCopy is enough when
+ // processing those
+ FileAction subdirAction = (action == ActionReplace) ? ActionCopy : action;
+ // create folder on dest. drive
+ if (action != ActionDelete && action != ActionDeleteFolder)
+ DoProcessFile(ActionCreateFolder, strnewDestFile, "", fileOperations, totalTime);
+
+ if (action == ActionReplace && CDirectory::Exists(strnewDestFile))
+ DoProcessFolder(ActionDelete, strnewDestFile, "", fileOperations, totalTime);
+
+ if (!DoProcessFolder(subdirAction, pItem->GetPath(), strnewDestFile, fileOperations, totalTime))
+ return false;
+
+ if (action == ActionDelete || action == ActionDeleteFolder)
+ DoProcessFile(ActionDeleteFolder, pItem->GetPath(), "", fileOperations, totalTime);
+ }
+ else
+ DoProcessFile(action, pItem->GetPath(), strnewDestFile, fileOperations, totalTime);
+ }
+ }
+
+ return true;
+}
+
+CFileOperationJob::CFileOperation::CFileOperation(FileAction action, const std::string &strFileA, const std::string &strFileB, int64_t time)
+ : m_action(action),
+ m_strFileA(strFileA),
+ m_strFileB(strFileB),
+ m_time(time)
+{ }
+
+struct DataHolder
+{
+ CFileOperationJob *base;
+ double current;
+ double opWeight;
+};
+
+std::string CFileOperationJob::GetActionString(FileAction action)
+{
+ std::string result;
+ switch (action)
+ {
+ case ActionCopy:
+ case ActionReplace:
+ result = g_localizeStrings.Get(115);
+ break;
+
+ case ActionMove:
+ result = g_localizeStrings.Get(116);
+ break;
+
+ case ActionDelete:
+ case ActionDeleteFolder:
+ result = g_localizeStrings.Get(117);
+ break;
+
+ case ActionCreateFolder:
+ result = g_localizeStrings.Get(119);
+ break;
+
+ default:
+ break;
+ }
+
+ return result;
+}
+
+bool CFileOperationJob::CFileOperation::ExecuteOperation(CFileOperationJob *base, double &current, double opWeight)
+{
+ bool bResult = true;
+
+ base->m_currentFile = CURL(m_strFileA).GetFileNameWithoutPath();
+ base->m_currentOperation = GetActionString(m_action);
+
+ if (base->ShouldCancel((unsigned int)current, 100))
+ return false;
+
+ base->SetText(base->GetCurrentFile());
+
+ DataHolder data = {base, current, opWeight};
+
+ switch (m_action)
+ {
+ case ActionCopy:
+ case ActionReplace:
+ bResult = CFile::Copy(m_strFileA, m_strFileB, this, &data);
+ break;
+
+ case ActionMove:
+ if (CanBeRenamed(m_strFileA, m_strFileB))
+ bResult = CFile::Rename(m_strFileA, m_strFileB);
+ else if (CFile::Copy(m_strFileA, m_strFileB, this, &data))
+ bResult = CFile::Delete(m_strFileA);
+ else
+ bResult = false;
+ break;
+
+ case ActionDelete:
+ bResult = CFile::Delete(m_strFileA);
+ break;
+
+ case ActionDeleteFolder:
+ bResult = CDirectory::Remove(m_strFileA);
+ break;
+
+ case ActionCreateFolder:
+ bResult = CDirectory::Create(m_strFileA);
+ break;
+
+ default:
+ CLog::Log(LOGERROR, "FileManager: unknown operation");
+ bResult = false;
+ break;
+ }
+
+ current += (double)m_time * opWeight;
+
+ return bResult;
+}
+
+inline bool CFileOperationJob::CanBeRenamed(const std::string &strFileA, const std::string &strFileB)
+{
+#ifndef TARGET_POSIX
+ if (strFileA[1] == ':' && strFileA[0] == strFileB[0])
+ return true;
+#else
+ if (URIUtils::IsHD(strFileA) && URIUtils::IsHD(strFileB))
+ return true;
+ else if (URIUtils::IsSmb(strFileA) && URIUtils::IsSmb(strFileB)) {
+ CURL smbFileA(strFileA), smbFileB(strFileB);
+ return smbFileA.GetHostName() == smbFileB.GetHostName() &&
+ smbFileA.GetShareName() == smbFileB.GetShareName();
+ }
+#endif
+ return false;
+}
+
+bool CFileOperationJob::CFileOperation::OnFileCallback(void* pContext, int ipercent, float avgSpeed)
+{
+ DataHolder *data = static_cast<DataHolder*>(pContext);
+ double current = data->current + ((double)ipercent * data->opWeight * (double)m_time)/ 100.0;
+
+ if (avgSpeed > 1000000.0f)
+ data->base->m_avgSpeed = StringUtils::Format("{:.1f} MB/s", avgSpeed / 1000000.0f);
+ else
+ data->base->m_avgSpeed = StringUtils::Format("{:.1f} KB/s", avgSpeed / 1000.0f);
+
+ std::string line;
+ line =
+ StringUtils::Format("{} ({})", data->base->GetCurrentFile(), data->base->GetAverageSpeed());
+ data->base->SetText(line);
+ return !data->base->ShouldCancel((unsigned)current, 100);
+}
+
+bool CFileOperationJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CFileOperationJob* rjob = dynamic_cast<const CFileOperationJob*>(job);
+ if (rjob == NULL)
+ return false;
+
+ if (GetAction() != rjob->GetAction() ||
+ m_strDestFile != rjob->m_strDestFile ||
+ m_items.Size() != rjob->m_items.Size())
+ return false;
+
+ for (int i = 0; i < m_items.Size(); i++)
+ {
+ if (m_items[i]->GetPath() != rjob->m_items[i]->GetPath() ||
+ m_items[i]->IsSelected() != rjob->m_items[i]->IsSelected())
+ return false;
+ }
+
+ return true;
+}
diff --git a/xbmc/utils/FileOperationJob.h b/xbmc/utils/FileOperationJob.h
new file mode 100644
index 0000000..7284c62
--- /dev/null
+++ b/xbmc/utils/FileOperationJob.h
@@ -0,0 +1,91 @@
+/*
+ * 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 "FileItem.h"
+#include "filesystem/IFileTypes.h"
+#include "utils/ProgressJob.h"
+
+#include <string>
+#include <vector>
+
+class CFileOperationJob : public CProgressJob
+{
+public:
+ enum FileAction
+ {
+ ActionCopy = 1,
+ ActionMove,
+ ActionDelete,
+ ActionReplace, ///< Copy, emptying any existing destination directories first
+ ActionCreateFolder,
+ ActionDeleteFolder,
+ };
+
+ CFileOperationJob();
+ CFileOperationJob(FileAction action, CFileItemList & items,
+ const std::string& strDestFile,
+ bool displayProgress = false,
+ int errorHeading = 0, int errorLine = 0);
+
+ static std::string GetActionString(FileAction action);
+
+ // implementations of CJob
+ bool DoWork() override;
+ const char* GetType() const override { return m_displayProgress ? "filemanager" : ""; }
+ bool operator==(const CJob *job) const override;
+
+ void SetFileOperation(FileAction action,
+ const CFileItemList& items,
+ const std::string& strDestFile);
+
+ const std::string &GetAverageSpeed() const { return m_avgSpeed; }
+ const std::string &GetCurrentOperation() const { return m_currentOperation; }
+ const std::string &GetCurrentFile() const { return m_currentFile; }
+ const CFileItemList &GetItems() const { return m_items; }
+ FileAction GetAction() const { return m_action; }
+ int GetHeading() const { return m_heading; }
+ int GetLine() const { return m_line; }
+
+private:
+ class CFileOperation : public XFILE::IFileCallback
+ {
+ public:
+ CFileOperation(FileAction action, const std::string &strFileA, const std::string &strFileB, int64_t time);
+
+ bool OnFileCallback(void* pContext, int ipercent, float avgSpeed) override;
+
+ bool ExecuteOperation(CFileOperationJob *base, double &current, double opWeight);
+
+ private:
+ FileAction m_action;
+ std::string m_strFileA, m_strFileB;
+ int64_t m_time;
+ };
+ friend class CFileOperation;
+
+ typedef std::vector<CFileOperation> FileOperationList;
+ bool DoProcess(FileAction action,
+ const CFileItemList& items,
+ const std::string& strDestFile,
+ FileOperationList& fileOperations,
+ double& totalTime);
+ bool DoProcessFolder(FileAction action, const std::string& strPath, const std::string& strDestFile, FileOperationList &fileOperations, double &totalTime);
+ bool DoProcessFile(FileAction action, const std::string& strFileA, const std::string& strFileB, FileOperationList &fileOperations, double &totalTime);
+
+ static inline bool CanBeRenamed(const std::string &strFileA, const std::string &strFileB);
+
+ FileAction m_action = ActionCopy;
+ CFileItemList m_items;
+ std::string m_strDestFile;
+ std::string m_avgSpeed, m_currentOperation, m_currentFile;
+ bool m_displayProgress = false;
+ int m_heading = 0;
+ int m_line = 0;
+};
diff --git a/xbmc/utils/FileUtils.cpp b/xbmc/utils/FileUtils.cpp
new file mode 100644
index 0000000..b39f75a
--- /dev/null
+++ b/xbmc/utils/FileUtils.cpp
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2010-2020 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 "FileUtils.h"
+
+#include "CompileInfo.h"
+#include "FileOperationJob.h"
+#include "ServiceBroker.h"
+#include "StringUtils.h"
+#include "URIUtils.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/File.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "filesystem/StackDirectory.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/WIN32Util.h"
+#include "utils/CharsetConverter.h"
+#endif
+
+#include <vector>
+
+using namespace XFILE;
+
+bool CFileUtils::DeleteItem(const std::string &strPath)
+{
+ CFileItemPtr item(new CFileItem(strPath));
+ item->SetPath(strPath);
+ item->m_bIsFolder = URIUtils::HasSlashAtEnd(strPath);
+ item->Select(true);
+ return DeleteItem(item);
+}
+
+bool CFileUtils::DeleteItem(const std::shared_ptr<CFileItem>& item)
+{
+ if (!item || item->IsParentFolder())
+ return false;
+
+ // Create a temporary item list containing the file/folder for deletion
+ CFileItemPtr pItemTemp(new CFileItem(*item));
+ pItemTemp->Select(true);
+ CFileItemList items;
+ items.Add(pItemTemp);
+
+ // grab the real filemanager window, set up the progress bar,
+ // and process the delete action
+ CFileOperationJob op(CFileOperationJob::ActionDelete, items, "");
+
+ return op.DoWork();
+}
+
+bool CFileUtils::RenameFile(const std::string &strFile)
+{
+ std::string strFileAndPath(strFile);
+ URIUtils::RemoveSlashAtEnd(strFileAndPath);
+ std::string strFileName = URIUtils::GetFileName(strFileAndPath);
+ std::string strPath = URIUtils::GetDirectory(strFileAndPath);
+ if (CGUIKeyboardFactory::ShowAndGetInput(strFileName, CVariant{g_localizeStrings.Get(16013)}, false))
+ {
+ strPath = URIUtils::AddFileToFolder(strPath, strFileName);
+ CLog::Log(LOGINFO, "FileUtils: rename {}->{}", strFileAndPath, strPath);
+ if (URIUtils::IsMultiPath(strFileAndPath))
+ { // special case for multipath renames - rename all the paths.
+ std::vector<std::string> paths;
+ CMultiPathDirectory::GetPaths(strFileAndPath, paths);
+ bool success = false;
+ for (unsigned int i = 0; i < paths.size(); ++i)
+ {
+ std::string filePath(paths[i]);
+ URIUtils::RemoveSlashAtEnd(filePath);
+ filePath = URIUtils::GetDirectory(filePath);
+ filePath = URIUtils::AddFileToFolder(filePath, strFileName);
+ if (CFile::Rename(paths[i], filePath))
+ success = true;
+ }
+ return success;
+ }
+ return CFile::Rename(strFileAndPath, strPath);
+ }
+ return false;
+}
+
+bool CFileUtils::RemoteAccessAllowed(const std::string &strPath)
+{
+ std::string SourceNames[] = { "programs", "files", "video", "music", "pictures" };
+
+ std::string realPath = URIUtils::GetRealPath(strPath);
+ // for rar:// and zip:// paths we need to extract the path to the archive
+ // instead of using the VFS path
+ while (URIUtils::IsInArchive(realPath))
+ realPath = CURL(realPath).GetHostName();
+
+ if (StringUtils::StartsWithNoCase(realPath, "virtualpath://upnproot/"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "musicdb://"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "videodb://"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "library://video"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "library://music"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "sources://video"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "special://musicplaylists"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "special://profile/playlists"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "special://videoplaylists"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "special://skin"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "special://profile/addon_data"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "addons://sources"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "upnp://"))
+ return true;
+ else if (StringUtils::StartsWithNoCase(realPath, "plugin://"))
+ return true;
+ else
+ {
+ std::string strPlaylistsPath = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH);
+ URIUtils::RemoveSlashAtEnd(strPlaylistsPath);
+ if (StringUtils::StartsWithNoCase(realPath, strPlaylistsPath))
+ return true;
+ }
+ bool isSource;
+ // Check manually added sources (held in sources.xml)
+ for (const std::string& sourceName : SourceNames)
+ {
+ VECSOURCES* sources = CMediaSourceSettings::GetInstance().GetSources(sourceName);
+ int sourceIndex = CUtil::GetMatchingSource(realPath, *sources, isSource);
+ if (sourceIndex >= 0 && sourceIndex < static_cast<int>(sources->size()) &&
+ sources->at(sourceIndex).m_iHasLock != LOCK_STATE_LOCKED &&
+ sources->at(sourceIndex).m_allowSharing)
+ return true;
+ }
+ // Check auto-mounted sources
+ VECSOURCES sources;
+ CServiceBroker::GetMediaManager().GetRemovableDrives(
+ sources); // Sources returned always have m_allowsharing = true
+ //! @todo Make sharing of auto-mounted sources user configurable
+ int sourceIndex = CUtil::GetMatchingSource(realPath, sources, isSource);
+ if (sourceIndex >= 0 && sourceIndex < static_cast<int>(sources.size()) &&
+ sources.at(sourceIndex).m_iHasLock != LOCK_STATE_LOCKED &&
+ sources.at(sourceIndex).m_allowSharing)
+ return true;
+
+ return false;
+}
+
+CDateTime CFileUtils::GetModificationDate(const std::string& strFileNameAndPath,
+ const bool& bUseLatestDate)
+{
+ if (bUseLatestDate)
+ return GetModificationDate(1, strFileNameAndPath);
+ else
+ return GetModificationDate(0, strFileNameAndPath);
+}
+
+CDateTime CFileUtils::GetModificationDate(const int& code, const std::string& strFileNameAndPath)
+{
+ CDateTime dateAdded;
+ if (strFileNameAndPath.empty())
+ {
+ CLog::Log(LOGDEBUG, "{} empty strFileNameAndPath variable", __FUNCTION__);
+ return dateAdded;
+ }
+
+ try
+ {
+ std::string file = strFileNameAndPath;
+ if (URIUtils::IsStack(strFileNameAndPath))
+ file = CStackDirectory::GetFirstStackedFile(strFileNameAndPath);
+
+ if (URIUtils::IsInArchive(file))
+ file = CURL(file).GetHostName();
+
+ // Try to get ctime (creation on Windows, metadata change on Linux) and mtime (modification)
+ struct __stat64 buffer;
+ if (CFile::Stat(file, &buffer) == 0 && (buffer.st_mtime != 0 || buffer.st_ctime != 0))
+ {
+ time_t now = time(NULL);
+ time_t addedTime;
+ // Prefer the modification time if it's valid, fallback to ctime
+ if (code == 0)
+ {
+ if (buffer.st_mtime != 0 && static_cast<time_t>(buffer.st_mtime) <= now)
+ addedTime = static_cast<time_t>(buffer.st_mtime);
+ else
+ addedTime = static_cast<time_t>(buffer.st_ctime);
+ }
+ // Use the later of the ctime and mtime
+ else if (code == 1)
+ {
+ addedTime =
+ std::max(static_cast<time_t>(buffer.st_ctime), static_cast<time_t>(buffer.st_mtime));
+ // if the newer of the two dates is in the future, we try it with the older one
+ if (addedTime > now)
+ addedTime =
+ std::min(static_cast<time_t>(buffer.st_ctime), static_cast<time_t>(buffer.st_mtime));
+ }
+ // Prefer the earliest of ctime and mtime, fallback to other
+ else
+ {
+ addedTime =
+ std::min(static_cast<time_t>(buffer.st_ctime), static_cast<time_t>(buffer.st_mtime));
+ // if the older of the two dates is invalid, we try it with the newer one
+ if (addedTime == 0)
+ addedTime =
+ std::max(static_cast<time_t>(buffer.st_ctime), static_cast<time_t>(buffer.st_mtime));
+ }
+
+
+ // make sure the datetime does is not in the future
+ if (addedTime <= now)
+ {
+ struct tm* time;
+#ifdef HAVE_LOCALTIME_R
+ struct tm result = {};
+ time = localtime_r(&addedTime, &result);
+#else
+ time = localtime(&addedTime);
+#endif
+ if (time)
+ dateAdded = *time;
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} unable to extract modification date for file ({})", __FUNCTION__,
+ strFileNameAndPath);
+ }
+ return dateAdded;
+}
+
+bool CFileUtils::CheckFileAccessAllowed(const std::string &filePath)
+{
+ // DENY access to paths matching
+ const std::vector<std::string> blacklist = {
+ "passwords.xml",
+ "sources.xml",
+ "guisettings.xml",
+ "advancedsettings.xml",
+ "server.key",
+ "/.ssh/",
+ };
+ // ALLOW kodi paths
+ std::vector<std::string> whitelist = {
+ CSpecialProtocol::TranslatePath("special://home"),
+ CSpecialProtocol::TranslatePath("special://xbmc"),
+ CSpecialProtocol::TranslatePath("special://musicartistsinfo"),
+ };
+
+ auto kodiExtraWhitelist = CCompileInfo::GetWebserverExtraWhitelist();
+ whitelist.insert(whitelist.end(), kodiExtraWhitelist.begin(), kodiExtraWhitelist.end());
+
+ // image urls come in the form of image://... sometimes with a / appended at the end
+ // and can be embedded in a music or video file image://music@...
+ // strip this off to get the real file path
+ bool isImage = false;
+ std::string decodePath = CURL::Decode(filePath);
+ size_t pos = decodePath.find("image://");
+ if (pos != std::string::npos)
+ {
+ isImage = true;
+ decodePath.erase(pos, 8);
+ URIUtils::RemoveSlashAtEnd(decodePath);
+ if (StringUtils::StartsWith(decodePath, "music@") || StringUtils::StartsWith(decodePath, "video@"))
+ decodePath.erase(pos, 6);
+ }
+
+ // check blacklist
+ for (const auto &b : blacklist)
+ {
+ if (decodePath.find(b) != std::string::npos)
+ {
+ CLog::Log(LOGERROR, "{} denied access to {}", __FUNCTION__, decodePath);
+ return false;
+ }
+ }
+
+#if defined(TARGET_POSIX)
+ std::string whiteEntry;
+ char *fullpath = realpath(decodePath.c_str(), nullptr);
+
+ // if this is a locally existing file, check access permissions
+ if (fullpath)
+ {
+ const std::string realPath = fullpath;
+ free(fullpath);
+
+ // check whitelist
+ for (const auto &w : whitelist)
+ {
+ char *realtemp = realpath(w.c_str(), nullptr);
+ if (realtemp)
+ {
+ whiteEntry = realtemp;
+ free(realtemp);
+ }
+ if (StringUtils::StartsWith(realPath, whiteEntry))
+ return true;
+ }
+ // check sources with realPath
+ return CFileUtils::RemoteAccessAllowed(realPath);
+ }
+#elif defined(TARGET_WINDOWS)
+ CURL url(decodePath);
+ if (url.GetProtocol().empty())
+ {
+ std::wstring decodePathW;
+ g_charsetConverter.utf8ToW(decodePath, decodePathW, false);
+ CWIN32Util::AddExtraLongPathPrefix(decodePathW);
+ DWORD bufSize = GetFullPathNameW(decodePathW.c_str(), 0, nullptr, nullptr);
+ if (bufSize > 0)
+ {
+ std::wstring fullpathW;
+ fullpathW.resize(bufSize);
+ if (GetFullPathNameW(decodePathW.c_str(), bufSize, const_cast<wchar_t*>(fullpathW.c_str()), nullptr) <= bufSize - 1)
+ {
+ CWIN32Util::RemoveExtraLongPathPrefix(fullpathW);
+ std::string fullpath;
+ g_charsetConverter.wToUTF8(fullpathW, fullpath, false);
+ for (const std::string& whiteEntry : whitelist)
+ {
+ if (StringUtils::StartsWith(fullpath, whiteEntry))
+ return true;
+ }
+ return CFileUtils::RemoteAccessAllowed(fullpath);
+ }
+ }
+ }
+#endif
+ // if it isn't a local file, it must be a vfs entry
+ if (! isImage)
+ return CFileUtils::RemoteAccessAllowed(decodePath);
+ return true;
+}
+
+bool CFileUtils::Exists(const std::string& strFileName, bool bUseCache)
+{
+ return CFile::Exists(strFileName, bUseCache);
+}
diff --git a/xbmc/utils/FileUtils.h b/xbmc/utils/FileUtils.h
new file mode 100644
index 0000000..667d4b3
--- /dev/null
+++ b/xbmc/utils/FileUtils.h
@@ -0,0 +1,34 @@
+/*
+ * 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 <memory>
+#include <string>
+
+class CFileItem;
+class CDateTime;
+
+class CFileUtils
+{
+public:
+ static bool CheckFileAccessAllowed(const std::string &filePath);
+ static bool DeleteItem(const std::shared_ptr<CFileItem>& item);
+ static bool DeleteItem(const std::string &strPath);
+ static bool Exists(const std::string& strFileName, bool bUseCache = true);
+ static bool RenameFile(const std::string &strFile);
+ static bool RemoteAccessAllowed(const std::string &strPath);
+ static unsigned int LoadFile(const std::string &filename, void* &outputBuffer);
+ /*! \brief Get the modified date of a file if its invalid it returns the creation date - this behavior changes when you set bUseLatestDate
+ \param strFileNameAndPath path to the file
+ \param bUseLatestDate use the newer datetime of the files mtime and ctime
+ \return Returns the file date, can return a invalid date if problems occur
+ */
+ static CDateTime GetModificationDate(const std::string& strFileNameAndPath, const bool& bUseLatestDate);
+ static CDateTime GetModificationDate(const int& code, const std::string& strFileNameAndPath);
+};
diff --git a/xbmc/utils/FontUtils.cpp b/xbmc/utils/FontUtils.cpp
new file mode 100644
index 0000000..4abf510
--- /dev/null
+++ b/xbmc/utils/FontUtils.cpp
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2022 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 "FontUtils.h"
+
+#include "FileItem.h"
+#include "StringUtils.h"
+#include "URIUtils.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/CharsetConverter.h"
+#include "utils/log.h"
+
+#include <ft2build.h>
+
+#include FT_FREETYPE_H
+#include FT_SFNT_NAMES_H
+#include FT_TRUETYPE_IDS_H
+
+using namespace XFILE;
+
+namespace
+{
+// \brief Get font family from SFNT table entries
+std::string GetFamilyNameFromSfnt(FT_Face face)
+{
+ std::string familyName;
+
+ for (FT_UInt index = 0; index < FT_Get_Sfnt_Name_Count(face); ++index)
+ {
+ FT_SfntName name;
+ if (FT_Get_Sfnt_Name(face, index, &name) != 0)
+ {
+ CLog::LogF(LOGWARNING, "Failed to get SFNT name at index {}", index);
+ continue;
+ }
+
+ // Get the unicode font family name (format-specific interface)
+ // In properties there may be one or more font family names encoded for each platform.
+ // NOTE: we give preference to MS/APPLE platform data, then fallback to MAC
+ // because has been found some fonts that provide names not convertible MAC text to UTF8
+ if (name.name_id == TT_NAME_ID_FONT_FAMILY)
+ {
+ const std::string nameEnc{reinterpret_cast<const char*>(name.string), name.string_len};
+
+ if (name.platform_id == TT_PLATFORM_MICROSOFT ||
+ name.platform_id == TT_PLATFORM_APPLE_UNICODE)
+ {
+ if (name.language_id != TT_MAC_LANGID_ENGLISH &&
+ name.language_id != TT_MS_LANGID_ENGLISH_UNITED_STATES &&
+ name.language_id != TT_MS_LANGID_ENGLISH_UNITED_KINGDOM)
+ continue;
+
+ if (CCharsetConverter::utf16BEtoUTF8(nameEnc, familyName))
+ break; // Stop here to prefer the name given with this platform
+ else
+ CLog::LogF(LOGERROR, "Failed to convert the font name string encoded as \"UTF-16BE\"");
+ }
+ else if (name.platform_id == TT_PLATFORM_MACINTOSH && familyName.empty())
+ {
+ if (name.language_id != TT_MAC_LANGID_ENGLISH || name.encoding_id != TT_MAC_ID_ROMAN)
+ continue;
+
+ if (!CCharsetConverter::MacintoshToUTF8(nameEnc, familyName))
+ CLog::LogF(LOGERROR, "Failed to convert the font name string encoded as \"macintosh\"");
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Unsupported font SFNT name platform \"{}\"", name.platform_id);
+ }
+ }
+ }
+ return familyName;
+}
+} // unnamed namespace
+
+std::string UTILS::FONT::GetFontFamily(std::vector<uint8_t>& buffer)
+{
+ FT_Library m_library{nullptr};
+ FT_Init_FreeType(&m_library);
+ if (!m_library)
+ {
+ CLog::LogF(LOGERROR, "Unable to initialize freetype library");
+ return "";
+ }
+
+ // Load the font face
+ FT_Face face;
+ std::string familyName;
+ if (FT_New_Memory_Face(m_library, reinterpret_cast<const FT_Byte*>(buffer.data()), buffer.size(),
+ 0, &face) == 0)
+ {
+ familyName = GetFamilyNameFromSfnt(face);
+ if (familyName.empty())
+ {
+ CLog::LogF(LOGWARNING, "Failed to get the unicode family name for \"{}\", fallback to ASCII",
+ face->family_name);
+ // ASCII font family name may differ from the unicode one, use this as fallback only
+ familyName = std::string{face->family_name};
+ if (familyName.empty())
+ CLog::LogF(LOGERROR, "Family name missing in the font");
+ }
+ }
+ else
+ {
+ CLog::LogF(LOGERROR, "Failed to process font memory buffer");
+ }
+
+ FT_Done_Face(face);
+ FT_Done_FreeType(m_library);
+ return familyName;
+}
+
+std::string UTILS::FONT::GetFontFamily(const std::string& filepath)
+{
+ std::vector<uint8_t> buffer;
+ if (filepath.empty())
+ return "";
+ if (XFILE::CFile().LoadFile(filepath, buffer) <= 0)
+ {
+ CLog::LogF(LOGERROR, "Failed to load file {}", filepath);
+ return "";
+ }
+ return GetFontFamily(buffer);
+}
+
+bool UTILS::FONT::IsSupportedFontExtension(const std::string& filepath)
+{
+ return URIUtils::HasExtension(filepath, UTILS::FONT::SUPPORTED_EXTENSIONS_MASK);
+}
+
+void UTILS::FONT::ClearTemporaryFonts()
+{
+ if (!CDirectory::Exists(UTILS::FONT::FONTPATH::TEMP))
+ return;
+
+ CFileItemList items;
+ CDirectory::GetDirectory(UTILS::FONT::FONTPATH::TEMP, items,
+ UTILS::FONT::SUPPORTED_EXTENSIONS_MASK,
+ DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_BYPASS_CACHE | DIR_FLAG_GET_HIDDEN);
+ for (const auto& item : items)
+ {
+ if (item->m_bIsFolder)
+ continue;
+
+ CFile::Delete(item->GetPath());
+ }
+}
+
+std::string UTILS::FONT::FONTPATH::GetSystemFontPath(const std::string& filename)
+{
+ std::string fontPath = URIUtils::AddFileToFolder(
+ CSpecialProtocol::TranslatePath(UTILS::FONT::FONTPATH::SYSTEM), filename);
+ if (XFILE::CFile::Exists(fontPath))
+ {
+ return CSpecialProtocol::TranslatePath(fontPath);
+ }
+
+ CLog::LogF(LOGERROR, "Could not find application system font {}", filename);
+ return "";
+}
diff --git a/xbmc/utils/FontUtils.h b/xbmc/utils/FontUtils.h
new file mode 100644
index 0000000..d4b92dd
--- /dev/null
+++ b/xbmc/utils/FontUtils.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 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 <stdint.h>
+#include <string>
+#include <vector>
+
+namespace UTILS
+{
+namespace FONT
+{
+constexpr const char* SUPPORTED_EXTENSIONS_MASK = ".ttf|.otf";
+
+// The default application font
+constexpr const char* FONT_DEFAULT_FILENAME = "arial.ttf";
+
+namespace FONTPATH
+{
+// Directory where Kodi bundled fonts files are located
+constexpr const char* SYSTEM = "special://xbmc/media/Fonts/";
+// Directory where user defined fonts are located
+constexpr const char* USER = "special://home/media/Fonts/";
+// Temporary font path (where MKV fonts are extracted and temporarily stored)
+constexpr const char* TEMP = "special://temp/fonts/";
+
+/*!
+ * \brief Provided a font filename returns the complete path for the font in
+ * the system font folder (if it exists) or an empty string otherwise
+ * \param filename The font file name
+ * \return The path for the font or an empty string if the path does not exist
+ */
+std::string GetSystemFontPath(const std::string& filename);
+}; // namespace FONTPATH
+
+/*!
+ * \brief Get the font family name from a font file
+ * \param buffer The font data
+ * \return The font family name, otherwise empty if fails
+ */
+std::string GetFontFamily(std::vector<uint8_t>& buffer);
+
+/*!
+ * \brief Get the font family name from a font file
+ * \param filepath The path where read the font data
+ * \return The font family name, otherwise empty if fails
+ */
+std::string GetFontFamily(const std::string& filepath);
+
+/*!
+ * \brief Check if a filename have a supported font extension.
+ * \param filepath The font file path
+ * \return True if it has a supported extension, otherwise false
+ */
+bool IsSupportedFontExtension(const std::string& filepath);
+
+/*!
+ * \brief Removes all temporary fonts, e.g.those extract from MKV containers
+ * that are only available during playback
+ */
+void ClearTemporaryFonts();
+
+} // namespace FONT
+} // namespace UTILS
diff --git a/xbmc/utils/GBMBufferObject.cpp b/xbmc/utils/GBMBufferObject.cpp
new file mode 100644
index 0000000..90c4017
--- /dev/null
+++ b/xbmc/utils/GBMBufferObject.cpp
@@ -0,0 +1,100 @@
+/*
+ * 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 "GBMBufferObject.h"
+
+#include "BufferObjectFactory.h"
+#include "ServiceBroker.h"
+#include "windowing/gbm/WinSystemGbmEGLContext.h"
+
+#include <gbm.h>
+#include <unistd.h>
+
+using namespace KODI::WINDOWING::GBM;
+
+std::unique_ptr<CBufferObject> CGBMBufferObject::Create()
+{
+ return std::make_unique<CGBMBufferObject>();
+}
+
+void CGBMBufferObject::Register()
+{
+ CBufferObjectFactory::RegisterBufferObject(CGBMBufferObject::Create);
+}
+
+CGBMBufferObject::CGBMBufferObject()
+{
+ m_device =
+ static_cast<CWinSystemGbmEGLContext*>(CServiceBroker::GetWinSystem())->GetGBMDevice()->Get();
+}
+
+CGBMBufferObject::~CGBMBufferObject()
+{
+ ReleaseMemory();
+ DestroyBufferObject();
+}
+
+bool CGBMBufferObject::CreateBufferObject(uint32_t format, uint32_t width, uint32_t height)
+{
+ if (m_fd >= 0)
+ return true;
+
+ m_width = width;
+ m_height = height;
+
+ m_bo = gbm_bo_create(m_device, m_width, m_height, format, GBM_BO_USE_LINEAR);
+
+ if (!m_bo)
+ return false;
+
+ m_fd = gbm_bo_get_fd(m_bo);
+
+ return true;
+}
+
+void CGBMBufferObject::DestroyBufferObject()
+{
+ close(m_fd);
+
+ if (m_bo)
+ gbm_bo_destroy(m_bo);
+
+ m_bo = nullptr;
+ m_fd = -1;
+}
+
+uint8_t* CGBMBufferObject::GetMemory()
+{
+ if (m_bo)
+ {
+ m_map = static_cast<uint8_t*>(gbm_bo_map(m_bo, 0, 0, m_width, m_height, GBM_BO_TRANSFER_WRITE, &m_stride, &m_map_data));
+ if (m_map)
+ return m_map;
+ }
+
+ return nullptr;
+}
+
+void CGBMBufferObject::ReleaseMemory()
+{
+ if (m_bo && m_map)
+ {
+ gbm_bo_unmap(m_bo, m_map_data);
+ m_map_data = nullptr;
+ m_map = nullptr;
+ }
+}
+
+uint64_t CGBMBufferObject::GetModifier()
+{
+#if defined(HAS_GBM_MODIFIERS)
+ return gbm_bo_get_modifier(m_bo);
+#else
+ return 0;
+#endif
+}
diff --git a/xbmc/utils/GBMBufferObject.h b/xbmc/utils/GBMBufferObject.h
new file mode 100644
index 0000000..ae8de58
--- /dev/null
+++ b/xbmc/utils/GBMBufferObject.h
@@ -0,0 +1,48 @@
+/*
+ * 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 "utils/BufferObject.h"
+
+#include <memory>
+#include <stdint.h>
+
+struct gbm_bo;
+struct gbm_device;
+
+class CGBMBufferObject : public CBufferObject
+{
+public:
+ CGBMBufferObject();
+ ~CGBMBufferObject() override;
+
+ // Registration
+ static std::unique_ptr<CBufferObject> Create();
+ static void Register();
+
+ // IBufferObject overrides via CBufferObject
+ bool CreateBufferObject(uint32_t format, uint32_t width, uint32_t height) override;
+ void DestroyBufferObject() override;
+ uint8_t* GetMemory() override;
+ void ReleaseMemory() override;
+ std::string GetName() const override { return "CGBMBufferObject"; }
+
+ // CBufferObject overrides
+ uint64_t GetModifier() override;
+
+private:
+ gbm_device* m_device{nullptr};
+ gbm_bo* m_bo{nullptr};
+
+ uint32_t m_width{0};
+ uint32_t m_height{0};
+
+ uint8_t* m_map{nullptr};
+ void* m_map_data{nullptr};
+};
diff --git a/xbmc/utils/GLUtils.cpp b/xbmc/utils/GLUtils.cpp
new file mode 100644
index 0000000..df8921e
--- /dev/null
+++ b/xbmc/utils/GLUtils.cpp
@@ -0,0 +1,280 @@
+/*
+ * 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 "GLUtils.h"
+
+#include "ServiceBroker.h"
+#include "log.h"
+#include "rendering/MatrixGL.h"
+#include "rendering/RenderSystem.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+
+#include <map>
+#include <stdexcept>
+#include <utility>
+
+namespace
+{
+
+#define X(VAL) std::make_pair(VAL, #VAL)
+std::map<GLenum, const char*> glErrors =
+{
+ // please keep attributes in accordance to:
+ // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glGetError.xhtml
+ X(GL_NO_ERROR),
+ X(GL_INVALID_ENUM),
+ X(GL_INVALID_VALUE),
+ X(GL_INVALID_OPERATION),
+ X(GL_INVALID_FRAMEBUFFER_OPERATION),
+ X(GL_OUT_OF_MEMORY),
+#if defined(HAS_GL)
+ X(GL_STACK_UNDERFLOW),
+ X(GL_STACK_OVERFLOW),
+#endif
+};
+
+std::map<GLenum, const char*> glErrorSource = {
+#if defined(HAS_GLES) && defined(TARGET_LINUX)
+ X(GL_DEBUG_SOURCE_API_KHR),
+ X(GL_DEBUG_SOURCE_WINDOW_SYSTEM_KHR),
+ X(GL_DEBUG_SOURCE_SHADER_COMPILER_KHR),
+ X(GL_DEBUG_SOURCE_THIRD_PARTY_KHR),
+ X(GL_DEBUG_SOURCE_APPLICATION_KHR),
+ X(GL_DEBUG_SOURCE_OTHER_KHR),
+#endif
+};
+
+std::map<GLenum, const char*> glErrorType = {
+#if defined(HAS_GLES) && defined(TARGET_LINUX)
+ X(GL_DEBUG_TYPE_ERROR_KHR),
+ X(GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR_KHR),
+ X(GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR_KHR),
+ X(GL_DEBUG_TYPE_PORTABILITY_KHR),
+ X(GL_DEBUG_TYPE_PERFORMANCE_KHR),
+ X(GL_DEBUG_TYPE_OTHER_KHR),
+ X(GL_DEBUG_TYPE_MARKER_KHR),
+#endif
+};
+
+std::map<GLenum, const char*> glErrorSeverity = {
+#if defined(HAS_GLES) && defined(TARGET_LINUX)
+ X(GL_DEBUG_SEVERITY_HIGH_KHR),
+ X(GL_DEBUG_SEVERITY_MEDIUM_KHR),
+ X(GL_DEBUG_SEVERITY_LOW_KHR),
+ X(GL_DEBUG_SEVERITY_NOTIFICATION_KHR),
+#endif
+};
+#undef X
+
+} // namespace
+
+void KODI::UTILS::GL::GlErrorCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam)
+{
+ std::string sourceStr;
+ std::string typeStr;
+ std::string severityStr;
+
+ auto glSource = glErrorSource.find(source);
+ if (glSource != glErrorSource.end())
+ {
+ sourceStr = glSource->second;
+ }
+
+ auto glType = glErrorType.find(type);
+ if (glType != glErrorType.end())
+ {
+ typeStr = glType->second;
+ }
+
+ auto glSeverity = glErrorSeverity.find(severity);
+ if (glSeverity != glErrorSeverity.end())
+ {
+ severityStr = glSeverity->second;
+ }
+
+ CLog::Log(LOGDEBUG, "OpenGL(ES) Debugging:\nSource: {}\nType: {}\nSeverity: {}\nID: {}\nMessage: {}", sourceStr, typeStr, severityStr, id, message);
+}
+
+static void PrintMatrix(const GLfloat* matrix, const std::string& matrixName)
+{
+ CLog::Log(LOGDEBUG, "{}:\n{:> 10.3f} {:> 10.3f} {:> 10.3f} {:> 10.3f}\n{:> 10.3f} {:> 10.3f} {:> 10.3f} {:> 10.3f}\n{:> 10.3f} {:> 10.3f} {:> 10.3f} {:> 10.3f}\n{:> 10.3f} {:> 10.3f} {:> 10.3f} {:> 10.3f}",
+ matrixName,
+ matrix[0], matrix[1], matrix[2], matrix[3],
+ matrix[4], matrix[5], matrix[6], matrix[7],
+ matrix[8], matrix[9], matrix[10], matrix[11],
+ matrix[12], matrix[13], matrix[14], matrix[15]);
+}
+
+void _VerifyGLState(const char* szfile, const char* szfunction, int lineno)
+{
+ GLenum err = glGetError();
+ if (err == GL_NO_ERROR)
+ {
+ return;
+ }
+
+ auto error = glErrors.find(err);
+ if (error != glErrors.end())
+ {
+ CLog::Log(LOGERROR, "GL(ES) ERROR: {}", error->second);
+ }
+
+ if (szfile && szfunction)
+ {
+ CLog::Log(LOGERROR, "In file: {} function: {} line: {}", szfile, szfunction, lineno);
+ }
+
+ GLboolean scissors;
+ glGetBooleanv(GL_SCISSOR_TEST, &scissors);
+ CLog::Log(LOGDEBUG, "Scissor test enabled: {}", scissors == GL_TRUE ? "True" : "False");
+
+ GLfloat matrix[16];
+ glGetFloatv(GL_SCISSOR_BOX, matrix);
+ CLog::Log(LOGDEBUG, "Scissor box: {}, {}, {}, {}", matrix[0], matrix[1], matrix[2], matrix[3]);
+
+ glGetFloatv(GL_VIEWPORT, matrix);
+ CLog::Log(LOGDEBUG, "Viewport: {}, {}, {}, {}", matrix[0], matrix[1], matrix[2], matrix[3]);
+
+ PrintMatrix(glMatrixProject.Get(), "Projection Matrix");
+ PrintMatrix(glMatrixModview.Get(), "Modelview Matrix");
+}
+
+void LogGraphicsInfo()
+{
+#if defined(HAS_GL) || defined(HAS_GLES)
+ const char* s;
+
+ s = reinterpret_cast<const char*>(glGetString(GL_VENDOR));
+ if (s)
+ CLog::Log(LOGINFO, "GL_VENDOR = {}", s);
+ else
+ CLog::Log(LOGINFO, "GL_VENDOR = NULL");
+
+ s = reinterpret_cast<const char*>(glGetString(GL_RENDERER));
+ if (s)
+ CLog::Log(LOGINFO, "GL_RENDERER = {}", s);
+ else
+ CLog::Log(LOGINFO, "GL_RENDERER = NULL");
+
+ s = reinterpret_cast<const char*>(glGetString(GL_VERSION));
+ if (s)
+ CLog::Log(LOGINFO, "GL_VERSION = {}", s);
+ else
+ CLog::Log(LOGINFO, "GL_VERSION = NULL");
+
+ s = reinterpret_cast<const char*>(glGetString(GL_SHADING_LANGUAGE_VERSION));
+ if (s)
+ CLog::Log(LOGINFO, "GL_SHADING_LANGUAGE_VERSION = {}", s);
+ else
+ CLog::Log(LOGINFO, "GL_SHADING_LANGUAGE_VERSION = NULL");
+
+ //GL_NVX_gpu_memory_info extension
+#define GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX 0x9047
+#define GL_GPU_MEMORY_INFO_TOTAL_AVAILABLE_MEMORY_NVX 0x9048
+#define GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX 0x9049
+#define GL_GPU_MEMORY_INFO_EVICTION_COUNT_NVX 0x904A
+#define GL_GPU_MEMORY_INFO_EVICTED_MEMORY_NVX 0x904B
+
+ if (CServiceBroker::GetRenderSystem()->IsExtSupported("GL_NVX_gpu_memory_info"))
+ {
+ GLint mem = 0;
+
+ glGetIntegerv(GL_GPU_MEMORY_INFO_TOTAL_AVAILABLE_MEMORY_NVX, &mem);
+ CLog::Log(LOGINFO, "GL_GPU_MEMORY_INFO_TOTAL_AVAILABLE_MEMORY_NVX = {}", mem);
+
+ //this seems to be the amount of ram on the videocard
+ glGetIntegerv(GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX, &mem);
+ CLog::Log(LOGINFO, "GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX = {}", mem);
+ }
+
+ std::string extensions;
+#if defined(HAS_GL)
+ unsigned int renderVersionMajor, renderVersionMinor;
+ CServiceBroker::GetRenderSystem()->GetRenderVersion(renderVersionMajor, renderVersionMinor);
+ if (renderVersionMajor > 3 ||
+ (renderVersionMajor == 3 && renderVersionMinor >= 2))
+ {
+ GLint n;
+ glGetIntegerv(GL_NUM_EXTENSIONS, &n);
+ if (n > 0)
+ {
+ GLint i;
+ for (i = 0; i < n; i++)
+ {
+ extensions += (const char*)glGetStringi(GL_EXTENSIONS, i);
+ extensions += " ";
+ }
+ }
+ }
+ else
+#endif
+ {
+ extensions += (const char*) glGetString(GL_EXTENSIONS);
+ }
+
+ if (!extensions.empty())
+ CLog::Log(LOGINFO, "GL_EXTENSIONS = {}", extensions);
+ else
+ CLog::Log(LOGINFO, "GL_EXTENSIONS = NULL");
+
+
+#else /* !HAS_GL */
+ CLog::Log(LOGINFO, "Please define LogGraphicsInfo for your chosen graphics library");
+#endif /* !HAS_GL */
+}
+
+int KODI::UTILS::GL::glFormatElementByteCount(GLenum format)
+{
+ switch (format)
+ {
+#ifdef HAS_GL
+ case GL_BGRA:
+ return 4;
+ case GL_RED:
+ return 1;
+ case GL_GREEN:
+ return 1;
+ case GL_RG:
+ return 2;
+ case GL_BGR:
+ return 3;
+#endif
+ case GL_RGBA:
+ return 4;
+ case GL_RGB:
+ return 3;
+ case GL_LUMINANCE_ALPHA:
+ return 2;
+ case GL_LUMINANCE:
+ case GL_ALPHA:
+ return 1;
+ default:
+ CLog::Log(LOGERROR, "glFormatElementByteCount - Unknown format {}", format);
+ return 1;
+ }
+}
+
+uint8_t KODI::UTILS::GL::GetChannelFromARGB(const KODI::UTILS::GL::ColorChannel colorChannel,
+ const uint32_t argb)
+{
+ switch (colorChannel)
+ {
+ case KODI::UTILS::GL::ColorChannel::A:
+ return (argb >> 24) & 0xFF;
+ case KODI::UTILS::GL::ColorChannel::R:
+ return (argb >> 16) & 0xFF;
+ case KODI::UTILS::GL::ColorChannel::G:
+ return (argb >> 8) & 0xFF;
+ case KODI::UTILS::GL::ColorChannel::B:
+ return (argb >> 0) & 0xFF;
+ default:
+ throw std::runtime_error("KODI::UTILS::GL::GetChannelFromARGB: ColorChannel not handled");
+ };
+}
diff --git a/xbmc/utils/GLUtils.h b/xbmc/utils/GLUtils.h
new file mode 100644
index 0000000..5c36030
--- /dev/null
+++ b/xbmc/utils/GLUtils.h
@@ -0,0 +1,54 @@
+/*
+ * 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
+
+// GL Error checking macro
+// this function is useful for tracking down GL errors, which otherwise
+// just result in undefined behavior and can be difficult to track down.
+//
+// Just call it 'VerifyGLState()' after a sequence of GL calls
+//
+// if GL_DEBUGGING and HAS_GL are defined, the function checks
+// for GL errors and prints the current state of the various matrices;
+// if not it's just an empty inline stub, and thus won't affect performance
+// and will be optimized out.
+
+#include "system_gl.h"
+
+namespace KODI
+{
+namespace UTILS
+{
+namespace GL
+{
+void GlErrorCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam);
+
+int glFormatElementByteCount(GLenum format);
+
+enum class ColorChannel
+{
+ A,
+ R,
+ G,
+ B,
+};
+
+uint8_t GetChannelFromARGB(const ColorChannel colorChannel, const uint32_t argb);
+}
+}
+}
+
+void _VerifyGLState(const char* szfile, const char* szfunction, int lineno);
+#if defined(GL_DEBUGGING) && (defined(HAS_GL) || defined(HAS_GLES))
+#define VerifyGLState() _VerifyGLState(__FILE__, __FUNCTION__, __LINE__)
+#else
+#define VerifyGLState()
+#endif
+
+void LogGraphicsInfo();
diff --git a/xbmc/utils/Geometry.h b/xbmc/utils/Geometry.h
new file mode 100644
index 0000000..878905b
--- /dev/null
+++ b/xbmc/utils/Geometry.h
@@ -0,0 +1,484 @@
+/*
+ * 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
+
+#ifdef __GNUC__
+// under gcc, inline will only take place if optimizations are applied (-O). this will force inline even with optimizations.
+#define XBMC_FORCE_INLINE __attribute__((always_inline))
+#else
+#define XBMC_FORCE_INLINE
+#endif
+
+#include <algorithm>
+#include <stdexcept>
+#include <vector>
+
+template <typename T> class CPointGen
+{
+public:
+ typedef CPointGen<T> this_type;
+
+ CPointGen() noexcept = default;
+
+ constexpr CPointGen(T a, T b)
+ : x{a}, y{b}
+ {}
+
+ template<class U> explicit constexpr CPointGen(const CPointGen<U>& rhs)
+ : x{static_cast<T> (rhs.x)}, y{static_cast<T> (rhs.y)}
+ {}
+
+ constexpr this_type operator+(const this_type &point) const
+ {
+ return {x + point.x, y + point.y};
+ };
+
+ this_type& operator+=(const this_type &point)
+ {
+ x += point.x;
+ y += point.y;
+ return *this;
+ };
+
+ constexpr this_type operator-(const this_type &point) const
+ {
+ return {x - point.x, y - point.y};
+ };
+
+ this_type& operator-=(const this_type &point)
+ {
+ x -= point.x;
+ y -= point.y;
+ return *this;
+ };
+
+ constexpr this_type operator*(T factor) const
+ {
+ return {x * factor, y * factor};
+ }
+
+ this_type& operator*=(T factor)
+ {
+ x *= factor;
+ y *= factor;
+ return *this;
+ }
+
+ constexpr this_type operator/(T factor) const
+ {
+ return {x / factor, y / factor};
+ }
+
+ this_type& operator/=(T factor)
+ {
+ x /= factor;
+ y /= factor;
+ return *this;
+ }
+
+ T x{}, y{};
+};
+
+template<typename T>
+constexpr bool operator==(const CPointGen<T> &point1, const CPointGen<T> &point2) noexcept
+{
+ return (point1.x == point2.x && point1.y == point2.y);
+}
+
+template<typename T>
+constexpr bool operator!=(const CPointGen<T> &point1, const CPointGen<T> &point2) noexcept
+{
+ return !(point1 == point2);
+}
+
+using CPoint = CPointGen<float>;
+using CPointInt = CPointGen<int>;
+
+
+/**
+ * Generic two-dimensional size representation
+ *
+ * Class invariant: width and height are both non-negative
+ * Throws std::out_of_range if invariant would be violated. The class
+ * is exception-safe. If modification would violate the invariant, the size
+ * is not changed.
+ */
+template <typename T> class CSizeGen
+{
+ T m_w{}, m_h{};
+
+ void CheckSet(T width, T height)
+ {
+ if (width < 0)
+ {
+ throw std::out_of_range("Size may not have negative width");
+ }
+ if (height < 0)
+ {
+ throw std::out_of_range("Size may not have negative height");
+ }
+ m_w = width;
+ m_h = height;
+ }
+
+public:
+ typedef CSizeGen<T> this_type;
+
+ CSizeGen() noexcept = default;
+
+ CSizeGen(T width, T height)
+ {
+ CheckSet(width, height);
+ }
+
+ T Width() const
+ {
+ return m_w;
+ }
+
+ T Height() const
+ {
+ return m_h;
+ }
+
+ void SetWidth(T width)
+ {
+ CheckSet(width, m_h);
+ }
+
+ void SetHeight(T height)
+ {
+ CheckSet(m_w, height);
+ }
+
+ void Set(T width, T height)
+ {
+ CheckSet(width, height);
+ }
+
+ bool IsZero() const
+ {
+ return (m_w == static_cast<T> (0) && m_h == static_cast<T> (0));
+ }
+
+ T Area() const
+ {
+ return m_w * m_h;
+ }
+
+ CPointGen<T> ToPoint() const
+ {
+ return {m_w, m_h};
+ }
+
+ template<class U> explicit CSizeGen<T>(const CSizeGen<U>& rhs)
+ {
+ CheckSet(static_cast<T> (rhs.m_w), static_cast<T> (rhs.m_h));
+ }
+
+ this_type operator+(const this_type& size) const
+ {
+ return {m_w + size.m_w, m_h + size.m_h};
+ };
+
+ this_type& operator+=(const this_type& size)
+ {
+ CheckSet(m_w + size.m_w, m_h + size.m_h);
+ return *this;
+ };
+
+ this_type operator-(const this_type& size) const
+ {
+ return {m_w - size.m_w, m_h - size.m_h};
+ };
+
+ this_type& operator-=(const this_type& size)
+ {
+ CheckSet(m_w - size.m_w, m_h - size.m_h);
+ return *this;
+ };
+
+ this_type operator*(T factor) const
+ {
+ return {m_w * factor, m_h * factor};
+ }
+
+ this_type& operator*=(T factor)
+ {
+ CheckSet(m_w * factor, m_h * factor);
+ return *this;
+ }
+
+ this_type operator/(T factor) const
+ {
+ return {m_w / factor, m_h / factor};
+ }
+
+ this_type& operator/=(T factor)
+ {
+ CheckSet(m_w / factor, m_h / factor);
+ return *this;
+ }
+};
+
+template<typename T>
+inline bool operator==(const CSizeGen<T>& size1, const CSizeGen<T>& size2) noexcept
+{
+ return (size1.Width() == size2.Width() && size1.Height() == size2.Height());
+}
+
+template<typename T>
+inline bool operator!=(const CSizeGen<T>& size1, const CSizeGen<T>& size2) noexcept
+{
+ return !(size1 == size2);
+}
+
+using CSize = CSizeGen<float>;
+using CSizeInt = CSizeGen<int>;
+
+
+template <typename T> class CRectGen
+{
+public:
+ typedef CRectGen<T> this_type;
+ typedef CPointGen<T> point_type;
+ typedef CSizeGen<T> size_type;
+
+ CRectGen() noexcept = default;
+
+ constexpr CRectGen(T left, T top, T right, T bottom)
+ : x1{left}, y1{top}, x2{right}, y2{bottom}
+ {}
+
+ constexpr CRectGen(const point_type &p1, const point_type &p2)
+ : x1{p1.x}, y1{p1.y}, x2{p2.x}, y2{p2.y}
+ {}
+
+ constexpr CRectGen(const point_type &origin, const size_type &size)
+ : x1{origin.x}, y1{origin.y}, x2{x1 + size.Width()}, y2{y1 + size.Height()}
+ {}
+
+ template<class U> explicit constexpr CRectGen(const CRectGen<U>& rhs)
+ : x1{static_cast<T> (rhs.x1)}, y1{static_cast<T> (rhs.y1)}, x2{static_cast<T> (rhs.x2)}, y2{static_cast<T> (rhs.y2)}
+ {}
+
+ void SetRect(T left, T top, T right, T bottom)
+ {
+ x1 = left;
+ y1 = top;
+ x2 = right;
+ y2 = bottom;
+ }
+
+ constexpr bool PtInRect(const point_type &point) const
+ {
+ return (x1 <= point.x && point.x <= x2 && y1 <= point.y && point.y <= y2);
+ };
+
+ this_type& operator-=(const point_type &point) XBMC_FORCE_INLINE
+ {
+ x1 -= point.x;
+ y1 -= point.y;
+ x2 -= point.x;
+ y2 -= point.y;
+ return *this;
+ };
+
+ constexpr this_type operator-(const point_type &point) const
+ {
+ return {x1 - point.x, y1 - point.y, x2 - point.x, y2 - point.y};
+ }
+
+ this_type& operator+=(const point_type &point) XBMC_FORCE_INLINE
+ {
+ x1 += point.x;
+ y1 += point.y;
+ x2 += point.x;
+ y2 += point.y;
+ return *this;
+ };
+
+ constexpr this_type operator+(const point_type &point) const
+ {
+ return {x1 + point.x, y1 + point.y, x2 + point.x, y2 + point.y};
+ }
+
+ this_type& operator-=(const size_type &size)
+ {
+ x2 -= size.Width();
+ y2 -= size.Height();
+ return *this;
+ };
+
+ constexpr this_type operator-(const size_type &size) const
+ {
+ return {x1, y1, x2 - size.Width(), y2 - size.Height()};
+ }
+
+ this_type& operator+=(const size_type &size)
+ {
+ x2 += size.Width();
+ y2 += size.Height();
+ return *this;
+ };
+
+ constexpr this_type operator+(const size_type &size) const
+ {
+ return {x1, y1, x2 + size.Width(), y2 + size.Height()};
+ }
+
+ this_type& Intersect(const this_type &rect)
+ {
+ x1 = clamp_range(x1, rect.x1, rect.x2);
+ x2 = clamp_range(x2, rect.x1, rect.x2);
+ y1 = clamp_range(y1, rect.y1, rect.y2);
+ y2 = clamp_range(y2, rect.y1, rect.y2);
+ return *this;
+ };
+
+ this_type& Union(const this_type &rect)
+ {
+ if (IsEmpty())
+ *this = rect;
+ else if (!rect.IsEmpty())
+ {
+ x1 = std::min(x1,rect.x1);
+ y1 = std::min(y1,rect.y1);
+
+ x2 = std::max(x2,rect.x2);
+ y2 = std::max(y2,rect.y2);
+ }
+
+ return *this;
+ };
+
+ constexpr bool IsEmpty() const XBMC_FORCE_INLINE
+ {
+ return (x2 - x1) * (y2 - y1) == 0;
+ };
+
+ constexpr point_type P1() const XBMC_FORCE_INLINE
+ {
+ return {x1, y1};
+ }
+
+ constexpr point_type P2() const XBMC_FORCE_INLINE
+ {
+ return {x2, y2};
+ }
+
+ constexpr T Width() const XBMC_FORCE_INLINE
+ {
+ return x2 - x1;
+ };
+
+ constexpr T Height() const XBMC_FORCE_INLINE
+ {
+ return y2 - y1;
+ };
+
+ constexpr T Area() const XBMC_FORCE_INLINE
+ {
+ return Width() * Height();
+ };
+
+ size_type ToSize() const
+ {
+ return {Width(), Height()};
+ };
+
+ std::vector<this_type> SubtractRect(this_type splitterRect)
+ {
+ std::vector<this_type> newRectanglesList;
+ this_type intersection = splitterRect.Intersect(*this);
+
+ if (!intersection.IsEmpty())
+ {
+ this_type add;
+
+ // add rect above intersection if not empty
+ add = this_type(x1, y1, x2, intersection.y1);
+ if (!add.IsEmpty())
+ newRectanglesList.push_back(add);
+
+ // add rect below intersection if not empty
+ add = this_type(x1, intersection.y2, x2, y2);
+ if (!add.IsEmpty())
+ newRectanglesList.push_back(add);
+
+ // add rect left intersection if not empty
+ add = this_type(x1, intersection.y1, intersection.x1, intersection.y2);
+ if (!add.IsEmpty())
+ newRectanglesList.push_back(add);
+
+ // add rect right intersection if not empty
+ add = this_type(intersection.x2, intersection.y1, x2, intersection.y2);
+ if (!add.IsEmpty())
+ newRectanglesList.push_back(add);
+ }
+ else
+ {
+ newRectanglesList.push_back(*this);
+ }
+
+ return newRectanglesList;
+ }
+
+ std::vector<this_type> SubtractRects(std::vector<this_type> intersectionList)
+ {
+ std::vector<this_type> fragmentsList;
+ fragmentsList.push_back(*this);
+
+ for (typename std::vector<this_type>::iterator splitter = intersectionList.begin(); splitter != intersectionList.end(); ++splitter)
+ {
+ typename std::vector<this_type> toAddList;
+
+ for (typename std::vector<this_type>::iterator fragment = fragmentsList.begin(); fragment != fragmentsList.end(); ++fragment)
+ {
+ std::vector<this_type> newFragmentsList = fragment->SubtractRect(*splitter);
+ toAddList.insert(toAddList.end(), newFragmentsList.begin(), newFragmentsList.end());
+ }
+
+ fragmentsList.clear();
+ fragmentsList.insert(fragmentsList.end(), toAddList.begin(), toAddList.end());
+ }
+
+ return fragmentsList;
+ }
+
+ void GetQuad(point_type (&points)[4])
+ {
+ points[0] = { x1, y1 };
+ points[1] = { x2, y1 };
+ points[2] = { x2, y2 };
+ points[3] = { x1, y2 };
+ }
+
+ T x1{}, y1{}, x2{}, y2{};
+private:
+ static constexpr T clamp_range(T x, T l, T h) XBMC_FORCE_INLINE
+ {
+ return (x > h) ? h : ((x < l) ? l : x);
+ }
+};
+
+template<typename T>
+constexpr bool operator==(const CRectGen<T> &rect1, const CRectGen<T> &rect2) noexcept
+{
+ return (rect1.x1 == rect2.x1 && rect1.y1 == rect2.y1 && rect1.x2 == rect2.x2 && rect1.y2 == rect2.y2);
+}
+
+template<typename T>
+constexpr bool operator!=(const CRectGen<T> &rect1, const CRectGen<T> &rect2) noexcept
+{
+ return !(rect1 == rect2);
+}
+
+using CRect = CRectGen<float>;
+using CRectInt = CRectGen<int>;
diff --git a/xbmc/utils/GlobalsHandling.h b/xbmc/utils/GlobalsHandling.h
new file mode 100644
index 0000000..a51cc08
--- /dev/null
+++ b/xbmc/utils/GlobalsHandling.h
@@ -0,0 +1,202 @@
+/*
+ * 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 <memory>
+
+/**
+ * This file contains the pattern for moving "globals" from the BSS Segment to the heap.
+ * A note on usage of this pattern for globals replacement:
+ *
+ * This pattern uses a singleton pattern and some compiler/C preprocessor sugar to allow
+ * "global" variables to be lazy instantiated and initialized and moved from the BSS segment
+ * to the heap (that is, they are instantiated on the heap when they are first used rather
+ * than relying on the startup code to initialize the BSS segment). This eliminates the
+ * problem associated with global variable dependencies across compilation units.
+ *
+ * Reference counting from the BSS segment is used to destruct these globals at the time the
+ * last compilation unit that knows about it is finalized by the post-main shutdown. The book
+ * keeping is done by smuggling a smart pointer into every file that references a particular
+ * "global class" through the use of a 'static' declaration of an instance of that smart
+ * pointer in the header file of the global class (did you ever think you'd see a file scope
+ * 'static' variable in a header file - on purpose?)
+ *
+ * There are two different ways to use this pattern when replacing global variables.
+ * The selection of which one to use depends on whether or not there is a possibility
+ * that the code in the .cpp file for the global can be executed from a static method
+ * somewhere. This may take some explanation.
+ *
+ * The (at least) two ways to do this:
+ *
+ * 1) You can use the reference object std::shared_ptr to access the global variable.
+ *
+ * This would be the preferred means since it is (very slightly) more efficient than
+ * the alternative. To use this pattern you replace standard static references to
+ * the global with access through the reference. If you use the C preprocessor to
+ * do this for you can put the following code in the header file where the global's
+ * class is declared:
+ *
+ * static std::shared_ptr<GlobalVariableClass> g_globalVariableRef(xbmcutil::GlobalsSingleton<GlobalVariableClass>::getInstance());
+ * #define g_globalVariable (*(g_globalVariableRef.get()))
+ *
+ * Note what this does. In every file that includes this header there will be a *static*
+ * instance of the std::shared_ptr<GlobalVariableClass> smart pointer. This effectively
+ * reference counts the singleton from every compilation unit (ie, object code file that
+ * results from a compilation of a .c/.cpp file) that references this global directly.
+ *
+ * There is a problem with this, however. Keep in mind that the instance of the smart pointer
+ * (being in the BSS segment of the compilation unit) is ITSELF an object that depends on
+ * the BSS segment initialization in order to be initialized with an instance from the
+ * singleton. That means, depending on the code structure, it is possible to get into a
+ * circumstance where the above #define could be exercised PRIOR TO the setting of the
+ * value of the smart pointer.
+ *
+ * Some reflection on this should lead you to the conclusion that the only way for this to
+ * happen is if access to this global can take place through a static/global method, directly
+ * or indirectly (ie, the static/global method can call another method that uses the
+ * reference), where that static is called from initialization code exercised prior to
+ * the start of 'main.'
+ *
+ * Because of the "indirectly" in the above statement, this situation can be difficult to
+ * determine beforehand.
+ *
+ * 2) Alternatively, when you KNOW that the global variable can suffer from the above described
+ * problem, you can restrict all access to the variable to the singleton by changing
+ * the #define to:
+ *
+ * #define g_globalVariable (*(xbmcutil::Singleton<GlobalVariableClass>::getInstance()))
+ *
+ * A few things to note about this. First, this separates the reference counting aspect
+ * from the access aspect of this solution. The smart pointers are no longer used for
+ * access, only for reference counting. Secondly, all access is through the singleton directly
+ * so there is no reliance on the state of the BSS segment for the code to operate
+ * correctly.
+ *
+ * This solution is required for g_Windowing because it's accessed (both directly and
+ * indirectly) from the static methods of CLog which are called repeatedly from
+ * code exercised during the initialization of the BSS segment.
+ */
+
+namespace xbmcutil
+{
+ /**
+ * This class is an implementation detail of the macros defined below and
+ * is NOT meant to be used as a general purpose utility. IOW, DO NOT USE THIS
+ * CLASS to support a general singleton design pattern, it's specialized
+ * for solving the initialization/finalization order/dependency problem
+ * with global variables and should only be used via the macros below.
+ *
+ * Currently THIS IS NOT THREAD SAFE! Why not just add a lock you ask?
+ * Because this singleton is used to initialize global variables and
+ * there is an issue with having the lock used prior to its
+ * initialization. No matter what, if this class is used as a replacement
+ * for global variables there's going to be a race condition if it's used
+ * anywhere else. So currently this is the only prescribed use.
+ *
+ * Therefore this hack depends on the fact that compilation unit global/static
+ * initialization is done in a single thread.
+ */
+ template <class T> class GlobalsSingleton
+ {
+ /**
+ * This thing just deletes the shared_ptr when the 'instance'
+ * goes out of scope (when the bss segment of the compilation unit
+ * that 'instance' is sitting in is deinitialized). See the comment
+ * on 'instance' for more information.
+ */
+ template <class K> class Deleter
+ {
+ public:
+ K* guarded;
+ ~Deleter() { if (guarded) delete guarded; }
+ };
+
+ /**
+ * Is it possible that getInstance can be called prior to the shared_ptr 'instance'
+ * being initialized as a global? If so, then the shared_ptr constructor would
+ * effectively 'reset' the shared pointer after it had been set by the prior
+ * getInstance call, and a second instance would be created. We really don't
+ * want this to happen so 'instance' is a pointer to a smart pointer so that
+ * we can deterministically handle its construction. It is guarded by the
+ * Deleter class above so that when the bss segment that this static is
+ * sitting in is deinitialized, the shared_ptr pointer will be cleaned up.
+ */
+ static Deleter<std::shared_ptr<T> > instance;
+
+ /**
+ * See 'getQuick' below.
+ */
+ static T* quick;
+ public:
+
+ /**
+ * Retrieve an instance of the singleton using a shared pointer for
+ * reference counting.
+ */
+ inline static std::shared_ptr<T> getInstance()
+ {
+ if (!instance.guarded)
+ {
+ if (!quick)
+ quick = new T;
+ instance.guarded = new std::shared_ptr<T>(quick);
+ }
+ return *(instance.guarded);
+ }
+
+ /**
+ * This is for quick access when using form (2) of the pattern. Before 'mdd' points
+ * it out, this might be a case of 'solving problems we don't have' but this access
+ * is used frequently within the event loop so any help here should benefit the
+ * overall performance and there is nothing complicated or tricky here and not
+ * a lot of code to maintain.
+ */
+ inline static T* getQuick()
+ {
+ if (!quick)
+ quick = new T;
+
+ return quick;
+ }
+
+ };
+
+ template <class T> typename GlobalsSingleton<T>::template Deleter<std::shared_ptr<T> > GlobalsSingleton<T>::instance;
+ template <class T> T* GlobalsSingleton<T>::quick;
+
+ /**
+ * This is another bit of hackery that will act as a flag for
+ * whether or not a global/static has been initialized yet. An instance
+ * should be placed in the cpp file after the static/global it's meant to
+ * monitor.
+ */
+ class InitFlag { public: explicit InitFlag(bool& flag) { flag = true; } };
+}
+
+/**
+ * For pattern (2) above, you can use the following macro. This pattern is safe to
+ * use in all cases but may be very slightly less efficient.
+ *
+ * Also, you must also use a #define to replace the actual global variable since
+ * there's no way to use a macro to add a #define. An example would be:
+ *
+ * XBMC_GLOBAL_REF(CWinSystemWin32DX, g_Windowing);
+ * #define g_Windowing XBMC_GLOBAL_USE(CWinSystemWin32DX)
+ *
+ */
+#define XBMC_GLOBAL_REF(classname,g_variable) \
+ static std::shared_ptr<classname> g_variable##Ref(xbmcutil::GlobalsSingleton<classname>::getInstance())
+
+/**
+ * This declares the actual use of the variable. It needs to be used in another #define
+ * of the form:
+ *
+ * #define g_variable XBMC_GLOBAL_USE(classname)
+ */
+#define XBMC_GLOBAL_USE(classname) (*(xbmcutil::GlobalsSingleton<classname>::getQuick()))
diff --git a/xbmc/utils/GroupUtils.cpp b/xbmc/utils/GroupUtils.cpp
new file mode 100644
index 0000000..a51399f
--- /dev/null
+++ b/xbmc/utils/GroupUtils.cpp
@@ -0,0 +1,157 @@
+/*
+ * 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 "GroupUtils.h"
+
+#include "FileItem.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "video/VideoDbUrl.h"
+#include "video/VideoInfoTag.h"
+
+#include <map>
+#include <set>
+
+using SetMap = std::map<int, std::set<CFileItemPtr> >;
+
+bool GroupUtils::Group(GroupBy groupBy, const std::string &baseDir, const CFileItemList &items, CFileItemList &groupedItems, GroupAttribute groupAttributes /* = GroupAttributeNone */)
+{
+ CFileItemList ungroupedItems;
+ return Group(groupBy, baseDir, items, groupedItems, ungroupedItems, groupAttributes);
+}
+
+bool GroupUtils::Group(GroupBy groupBy, const std::string &baseDir, const CFileItemList &items, CFileItemList &groupedItems, CFileItemList &ungroupedItems, GroupAttribute groupAttributes /* = GroupAttributeNone */)
+{
+ if (groupBy == GroupByNone)
+ return false;
+
+ // nothing to do if there are no items to group
+ if (items.Size() <= 0)
+ return true;
+
+ SetMap setMap;
+ for (int index = 0; index < items.Size(); index++)
+ {
+ bool ungrouped = true;
+ const CFileItemPtr item = items.Get(index);
+
+ // group by sets
+ if ((groupBy & GroupBySet) &&
+ item->HasVideoInfoTag() && item->GetVideoInfoTag()->m_set.id > 0)
+ {
+ ungrouped = false;
+ setMap[item->GetVideoInfoTag()->m_set.id].insert(item);
+ }
+
+ if (ungrouped)
+ ungroupedItems.Add(item);
+ }
+
+ if ((groupBy & GroupBySet) && !setMap.empty())
+ {
+ CVideoDbUrl itemsUrl;
+ if (!itemsUrl.FromString(baseDir))
+ return false;
+
+ for (SetMap::const_iterator set = setMap.begin(); set != setMap.end(); ++set)
+ {
+ // only one item in the set, so add it to the ungrouped items
+ if (set->second.size() == 1 && (groupAttributes & GroupAttributeIgnoreSingleItems))
+ {
+ ungroupedItems.Add(*set->second.begin());
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem((*set->second.begin())->GetVideoInfoTag()->m_set.title));
+ pItem->GetVideoInfoTag()->m_iDbId = set->first;
+ pItem->GetVideoInfoTag()->m_type = MediaTypeVideoCollection;
+
+ std::string basePath = StringUtils::Format("videodb://movies/sets/{}/", set->first);
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(basePath))
+ pItem->SetPath(basePath);
+ else
+ {
+ videoUrl.AddOptions((*set->second.begin())->GetURL().GetOptions());
+ pItem->SetPath(videoUrl.ToString());
+ }
+ pItem->m_bIsFolder = true;
+
+ CVideoInfoTag* setInfo = pItem->GetVideoInfoTag();
+ setInfo->m_strPath = pItem->GetPath();
+ setInfo->m_strTitle = pItem->GetLabel();
+ setInfo->m_strPlot = (*set->second.begin())->GetVideoInfoTag()->m_set.overview;
+
+ int ratings = 0;
+ float totalRatings = 0;
+ int iWatched = 0; // have all the movies been played at least once?
+ std::set<std::string> pathSet;
+ for (std::set<CFileItemPtr>::const_iterator movie = set->second.begin(); movie != set->second.end(); ++movie)
+ {
+ CVideoInfoTag* movieInfo = (*movie)->GetVideoInfoTag();
+ // handle rating
+ if (movieInfo->GetRating().rating > 0.0f)
+ {
+ ratings++;
+ totalRatings += movieInfo->GetRating().rating;
+ }
+
+ // handle year
+ if (movieInfo->GetYear() > setInfo->GetYear())
+ setInfo->SetYear(movieInfo->GetYear());
+
+ // handle lastplayed
+ if (movieInfo->m_lastPlayed.IsValid() && movieInfo->m_lastPlayed > setInfo->m_lastPlayed)
+ setInfo->m_lastPlayed = movieInfo->m_lastPlayed;
+
+ // handle dateadded
+ if (movieInfo->m_dateAdded.IsValid() && movieInfo->m_dateAdded > setInfo->m_dateAdded)
+ setInfo->m_dateAdded = movieInfo->m_dateAdded;
+
+ // handle playcount/watched
+ setInfo->SetPlayCount(setInfo->GetPlayCount() + movieInfo->GetPlayCount());
+ if (movieInfo->GetPlayCount() > 0)
+ iWatched++;
+
+ //accumulate the path for a multipath construction
+ CFileItem video(movieInfo->m_basePath, false);
+ if (video.IsVideo())
+ pathSet.insert(URIUtils::GetParentPath(movieInfo->m_basePath));
+ else
+ pathSet.insert(movieInfo->m_basePath);
+ }
+ setInfo->m_basePath = XFILE::CMultiPathDirectory::ConstructMultiPath(pathSet);
+
+ if (ratings > 0)
+ pItem->GetVideoInfoTag()->SetRating(totalRatings / ratings);
+
+ setInfo->SetPlayCount(iWatched >= static_cast<int>(set->second.size()) ? (setInfo->GetPlayCount() / set->second.size()) : 0);
+ pItem->SetProperty("total", (int)set->second.size());
+ pItem->SetProperty("watched", iWatched);
+ pItem->SetProperty("unwatched", (int)set->second.size() - iWatched);
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, setInfo->GetPlayCount() > 0);
+
+ groupedItems.Add(pItem);
+ }
+ }
+
+ return true;
+}
+
+bool GroupUtils::GroupAndMix(GroupBy groupBy, const std::string &baseDir, const CFileItemList &items, CFileItemList &groupedItemsMixed, GroupAttribute groupAttributes /* = GroupAttributeNone */)
+{
+ CFileItemList ungroupedItems;
+ if (!Group(groupBy, baseDir, items, groupedItemsMixed, ungroupedItems, groupAttributes))
+ return false;
+
+ // add all the ungrouped items as well
+ groupedItemsMixed.Append(ungroupedItems);
+
+ return true;
+}
diff --git a/xbmc/utils/GroupUtils.h b/xbmc/utils/GroupUtils.h
new file mode 100644
index 0000000..2ea7083
--- /dev/null
+++ b/xbmc/utils/GroupUtils.h
@@ -0,0 +1,32 @@
+/*
+ * 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 <string>
+
+class CFileItemList;
+
+// can be used as a flag
+typedef enum {
+ GroupByNone = 0x0,
+ GroupBySet = 0x1
+} GroupBy;
+
+typedef enum {
+ GroupAttributeNone = 0x0,
+ GroupAttributeIgnoreSingleItems = 0x1
+} GroupAttribute;
+
+class GroupUtils
+{
+public:
+ static bool Group(GroupBy groupBy, const std::string &baseDir, const CFileItemList &items, CFileItemList &groupedItems, GroupAttribute groupAttributes = GroupAttributeNone);
+ static bool Group(GroupBy groupBy, const std::string &baseDir, const CFileItemList &items, CFileItemList &groupedItems, CFileItemList &ungroupedItems, GroupAttribute groupAttributes = GroupAttributeNone);
+ static bool GroupAndMix(GroupBy groupBy, const std::string &baseDir, const CFileItemList &items, CFileItemList &groupedItemsMixed, GroupAttribute groupAttributes = GroupAttributeNone);
+};
diff --git a/xbmc/utils/HDRCapabilities.h b/xbmc/utils/HDRCapabilities.h
new file mode 100644
index 0000000..4802b78
--- /dev/null
+++ b/xbmc/utils/HDRCapabilities.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2005-2021 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 CHDRCapabilities
+{
+public:
+ CHDRCapabilities() = default;
+ ~CHDRCapabilities() = default;
+
+ bool SupportsHDR10() const { return m_hdr10; }
+ bool SupportsHLG() const { return m_hlg; }
+ bool SupportsHDR10Plus() const { return m_hdr10_plus; }
+ bool SupportsDolbyVision() const { return m_dolby_vision; }
+
+ void SetHDR10() { m_hdr10 = true; }
+ void SetHLG() { m_hlg = true; }
+ void SetHDR10Plus() { m_hdr10_plus = true; }
+ void SetDolbyVision() { m_dolby_vision = true; }
+
+private:
+ bool m_hdr10 = false;
+ bool m_hlg = false;
+ bool m_hdr10_plus = false;
+ bool m_dolby_vision = false;
+};
diff --git a/xbmc/utils/HTMLUtil.cpp b/xbmc/utils/HTMLUtil.cpp
new file mode 100644
index 0000000..8687ffe
--- /dev/null
+++ b/xbmc/utils/HTMLUtil.cpp
@@ -0,0 +1,229 @@
+/*
+ * 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 "HTMLUtil.h"
+
+#include "utils/StringUtils.h"
+
+#include <wctype.h>
+
+using namespace HTML;
+
+CHTMLUtil::CHTMLUtil(void) = default;
+
+CHTMLUtil::~CHTMLUtil(void) = default;
+
+void CHTMLUtil::RemoveTags(std::string& strHTML)
+{
+ int iNested = 0;
+ std::string strReturn = "";
+ for (int i = 0; i < (int) strHTML.size(); ++i)
+ {
+ if (strHTML[i] == '<') iNested++;
+ else if (strHTML[i] == '>') iNested--;
+ else
+ {
+ if (!iNested)
+ {
+ strReturn += strHTML[i];
+ }
+ }
+ }
+
+ strHTML = strReturn;
+}
+
+typedef struct
+{
+ const wchar_t* html;
+ const wchar_t w;
+} HTMLMapping;
+
+static const HTMLMapping mappings[] =
+ {{L"&amp;", 0x0026},
+ {L"&apos;", 0x0027},
+ {L"&acute;", 0x00B4},
+ {L"&agrave;", 0x00E0},
+ {L"&aacute;", 0x00E1},
+ {L"&acirc;", 0x00E2},
+ {L"&atilde;", 0x00E3},
+ {L"&auml;", 0x00E4},
+ {L"&aring;", 0x00E5},
+ {L"&aelig;", 0x00E6},
+ {L"&Agrave;", 0x00C0},
+ {L"&Aacute;", 0x00C1},
+ {L"&Acirc;", 0x00C2},
+ {L"&Atilde;", 0x00C3},
+ {L"&Auml;", 0x00C4},
+ {L"&Aring;", 0x00C5},
+ {L"&AElig;", 0x00C6},
+ {L"&bdquo;", 0x201E},
+ {L"&brvbar;", 0x00A6},
+ {L"&bull;", 0x2022},
+ {L"&bullet;", 0x2022},
+ {L"&cent;", 0x00A2},
+ {L"&circ;", 0x02C6},
+ {L"&curren;", 0x00A4},
+ {L"&copy;", 0x00A9},
+ {L"&cedil;", 0x00B8},
+ {L"&Ccedil;", 0x00C7},
+ {L"&ccedil;", 0x00E7},
+ {L"&dagger;", 0x2020},
+ {L"&deg;", 0x00B0},
+ {L"&divide;", 0x00F7},
+ {L"&Dagger;", 0x2021},
+ {L"&egrave;", 0x00E8},
+ {L"&eacute;", 0x00E9},
+ {L"&ecirc;", 0x00EA},
+ {L"&emsp;", 0x2003},
+ {L"&ensp;", 0x2002},
+ {L"&euml;", 0x00EB},
+ {L"&eth;", 0x00F0},
+ {L"&euro;", 0x20AC},
+ {L"&Egrave;", 0x00C8},
+ {L"&Eacute;", 0x00C9},
+ {L"&Ecirc;", 0x00CA},
+ {L"&Euml;", 0x00CB},
+ {L"&ETH;", 0x00D0},
+ {L"&quot;", 0x0022},
+ {L"&frasl;", 0x2044},
+ {L"&frac14;", 0x00BC},
+ {L"&frac12;", 0x00BD},
+ {L"&frac34;", 0x00BE},
+ {L"&gt;", 0x003E},
+ {L"&hellip;", 0x2026},
+ {L"&iexcl;", 0x00A1},
+ {L"&iquest;", 0x00BF},
+ {L"&igrave;", 0x00EC},
+ {L"&iacute;", 0x00ED},
+ {L"&icirc;", 0x00EE},
+ {L"&iuml;", 0x00EF},
+ {L"&Igrave;", 0x00CC},
+ {L"&Iacute;", 0x00CD},
+ {L"&Icirc;", 0x00CE},
+ {L"&Iuml;", 0x00CF},
+ {L"&lrm;", 0x200E},
+ {L"&lt;", 0x003C},
+ {L"&laquo;", 0x00AB},
+ {L"&ldquo;", 0x201C},
+ {L"&lsaquo;", 0x2039},
+ {L"&lsquo;", 0x2018},
+ {L"&macr;", 0x00AF},
+ {L"&micro;", 0x00B5},
+ {L"&middot;", 0x00B7},
+ {L"&mdash;", 0x2014},
+ {L"&nbsp;", 0x00A0},
+ {L"&ndash;", 0x2013},
+ {L"&ntilde;", 0x00F1},
+ {L"&not;", 0x00AC},
+ {L"&Ntilde;", 0x00D1},
+ {L"&ordf;", 0x00AA},
+ {L"&ordm;", 0x00BA},
+ {L"&oelig;", 0x0153},
+ {L"&ograve;", 0x00F2},
+ {L"&oacute;", 0x00F3},
+ {L"&ocirc;", 0x00F4},
+ {L"&otilde;", 0x00F5},
+ {L"&ouml;", 0x00F6},
+ {L"&oslash;", 0x00F8},
+ {L"&OElig;", 0x0152},
+ {L"&Ograve;", 0x00D2},
+ {L"&Oacute;", 0x00D3},
+ {L"&Ocirc;", 0x00D4},
+ {L"&Otilde;", 0x00D5},
+ {L"&Ouml;", 0x00D6},
+ {L"&Oslash;", 0x00D8},
+ {L"&para;", 0x00B6},
+ {L"&permil;", 0x2030},
+ {L"&plusmn;", 0x00B1},
+ {L"&pound;", 0x00A3},
+ {L"&raquo;", 0x00BB},
+ {L"&rdquo;", 0x201D},
+ {L"&reg;", 0x00AE},
+ {L"&rlm;", 0x200F},
+ {L"&rsaquo;", 0x203A},
+ {L"&rsquo;", 0x2019},
+ {L"&sbquo;", 0x201A},
+ {L"&scaron;", 0x0161},
+ {L"&sect;", 0x00A7},
+ {L"&shy;", 0x00AD},
+ {L"&sup1;", 0x00B9},
+ {L"&sup2;", 0x00B2},
+ {L"&sup3;", 0x00B3},
+ {L"&szlig;", 0x00DF},
+ {L"&Scaron;", 0x0160},
+ {L"&thinsp;", 0x2009},
+ {L"&thorn;", 0x00FE},
+ {L"&tilde;", 0x02DC},
+ {L"&times;", 0x00D7},
+ {L"&trade;", 0x2122},
+ {L"&THORN;", 0x00DE},
+ {L"&uml;", 0x00A8},
+ {L"&ugrave;", 0x00F9},
+ {L"&uacute;", 0x00FA},
+ {L"&ucirc;", 0x00FB},
+ {L"&uuml;", 0x00FC},
+ {L"&Ugrave;", 0x00D9},
+ {L"&Uacute;", 0x00DA},
+ {L"&Ucirc;", 0x00DB},
+ {L"&Uuml;", 0x00DC},
+ {L"&yen;", 0x00A5},
+ {L"&yuml;", 0x00FF},
+ {L"&yacute;", 0x00FD},
+ {L"&Yacute;", 0x00DD},
+ {L"&Yuml;", 0x0178},
+ {L"&zwj;", 0x200D},
+ {L"&zwnj;", 0x200C},
+ {NULL, L'\0'}};
+
+void CHTMLUtil::ConvertHTMLToW(const std::wstring& strHTML, std::wstring& strStripped)
+{
+ //! @todo STRING_CLEANUP
+ if (strHTML.empty())
+ {
+ strStripped.clear();
+ return ;
+ }
+ size_t iPos = 0;
+ strStripped = strHTML;
+ while (mappings[iPos].html)
+ {
+ StringUtils::Replace(strStripped, mappings[iPos].html,std::wstring(1, mappings[iPos].w));
+ iPos++;
+ }
+
+ iPos = strStripped.find(L"&#");
+ while (iPos > 0 && iPos < strStripped.size() - 4)
+ {
+ size_t iStart = iPos + 1;
+ iPos += 2;
+ std::wstring num;
+ int base = 10;
+ if (strStripped[iPos] == L'x')
+ {
+ base = 16;
+ iPos++;
+ }
+
+ size_t i = iPos;
+ while (iPos < strStripped.size() &&
+ (base == 16 ? iswxdigit(strStripped[iPos]) : iswdigit(strStripped[iPos])))
+ iPos++;
+
+ num = strStripped.substr(i, iPos-i);
+ wchar_t val = (wchar_t)wcstol(num.c_str(),NULL,base);
+ if (base == 10)
+ num = StringUtils::Format(L"&#{};", num);
+ else
+ num = StringUtils::Format(L"&#x{};", num);
+
+ StringUtils::Replace(strStripped, num,std::wstring(1,val));
+ iPos = strStripped.find(L"&#", iStart);
+ }
+}
+
diff --git a/xbmc/utils/HTMLUtil.h b/xbmc/utils/HTMLUtil.h
new file mode 100644
index 0000000..5295a9c
--- /dev/null
+++ b/xbmc/utils/HTMLUtil.h
@@ -0,0 +1,23 @@
+/*
+ * 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 <string>
+
+namespace HTML
+{
+class CHTMLUtil
+{
+public:
+ CHTMLUtil(void);
+ virtual ~CHTMLUtil(void);
+ static void RemoveTags(std::string& strHTML);
+ static void ConvertHTMLToW(const std::wstring& strHTML, std::wstring& strStripped);
+};
+}
diff --git a/xbmc/utils/HttpHeader.cpp b/xbmc/utils/HttpHeader.cpp
new file mode 100644
index 0000000..ad73bb2
--- /dev/null
+++ b/xbmc/utils/HttpHeader.cpp
@@ -0,0 +1,239 @@
+/*
+ * 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 "HttpHeader.h"
+
+#include "utils/StringUtils.h"
+
+// header white space characters according to RFC 2616
+const char* const CHttpHeader::m_whitespaceChars = " \t";
+
+
+CHttpHeader::CHttpHeader()
+{
+ m_headerdone = false;
+}
+
+CHttpHeader::~CHttpHeader() = default;
+
+void CHttpHeader::Parse(const std::string& strData)
+{
+ size_t pos = 0;
+ const size_t len = strData.length();
+ const char* const strDataC = strData.c_str();
+
+ // According to RFC 2616 any header line can have continuation on next line, if next line is started from whitespace char
+ // This code at first checks for whitespace char at the begging of the line, and if found, then current line is appended to m_lastHeaderLine
+ // If current line is NOT started from whitespace char, then previously stored (and completed) m_lastHeaderLine is parsed and current line is assigned to m_lastHeaderLine (to be parsed later)
+ while (pos < len)
+ {
+ size_t lineEnd = strData.find('\x0a', pos); // use '\x0a' instead of '\n' to be platform independent
+
+ if (lineEnd == std::string::npos)
+ return; // error: expected only complete lines
+
+ const size_t nextLine = lineEnd + 1;
+ if (lineEnd > pos && strDataC[lineEnd - 1] == '\x0d') // use '\x0d' instead of '\r' to be platform independent
+ lineEnd--;
+
+ if (m_headerdone)
+ Clear(); // clear previous header and process new one
+
+ if (strDataC[pos] == ' ' || strDataC[pos] == '\t') // same chars as in CHttpHeader::m_whitespaceChars
+ { // line is started from whitespace char: this is continuation of previous line
+ pos = strData.find_first_not_of(m_whitespaceChars, pos);
+
+ m_lastHeaderLine.push_back(' '); // replace all whitespace chars at start of the line with single space
+ m_lastHeaderLine.append(strData, pos, lineEnd - pos); // append current line
+ }
+ else
+ { // this line is NOT continuation, this line is new header line
+ if (!m_lastHeaderLine.empty())
+ ParseLine(m_lastHeaderLine); // process previously stored completed line (if any)
+
+ m_lastHeaderLine.assign(strData, pos, lineEnd - pos); // store current line to (possibly) complete later. Will be parsed on next turns.
+
+ if (pos == lineEnd)
+ m_headerdone = true; // current line is bare "\r\n" (or "\n"), means end of header; no need to process current m_lastHeaderLine
+ }
+
+ pos = nextLine; // go to next line (if any)
+ }
+}
+
+bool CHttpHeader::ParseLine(const std::string& headerLine)
+{
+ const size_t valueStart = headerLine.find(':');
+
+ if (valueStart != std::string::npos)
+ {
+ std::string strParam(headerLine, 0, valueStart);
+ std::string strValue(headerLine, valueStart + 1);
+
+ StringUtils::Trim(strParam, m_whitespaceChars);
+ StringUtils::ToLower(strParam);
+
+ StringUtils::Trim(strValue, m_whitespaceChars);
+
+ if (!strParam.empty() && !strValue.empty())
+ m_params.push_back(HeaderParams::value_type(strParam, strValue));
+ else
+ return false;
+ }
+ else if (m_protoLine.empty())
+ m_protoLine = headerLine;
+
+ return true;
+}
+
+void CHttpHeader::AddParam(const std::string& param, const std::string& value, const bool overwrite /*= false*/)
+{
+ std::string paramLower(param);
+ StringUtils::ToLower(paramLower);
+ StringUtils::Trim(paramLower, m_whitespaceChars);
+ if (paramLower.empty())
+ return;
+
+ if (overwrite)
+ { // delete ALL parameters with the same name
+ // note: 'GetValue' always returns last added parameter,
+ // so you probably don't need to overwrite
+ for (size_t i = 0; i < m_params.size();)
+ {
+ if (m_params[i].first == paramLower)
+ m_params.erase(m_params.begin() + i);
+ else
+ ++i;
+ }
+ }
+
+ std::string valueTrim(value);
+ StringUtils::Trim(valueTrim, m_whitespaceChars);
+ if (valueTrim.empty())
+ return;
+
+ m_params.push_back(HeaderParams::value_type(paramLower, valueTrim));
+}
+
+std::string CHttpHeader::GetValue(const std::string& strParam) const
+{
+ std::string paramLower(strParam);
+ StringUtils::ToLower(paramLower);
+
+ return GetValueRaw(paramLower);
+}
+
+std::string CHttpHeader::GetValueRaw(const std::string& strParam) const
+{
+ // look in reverse to find last parameter (probably most important)
+ for (HeaderParams::const_reverse_iterator iter = m_params.rbegin(); iter != m_params.rend(); ++iter)
+ {
+ if (iter->first == strParam)
+ return iter->second;
+ }
+
+ return "";
+}
+
+std::vector<std::string> CHttpHeader::GetValues(std::string strParam) const
+{
+ StringUtils::ToLower(strParam);
+ std::vector<std::string> values;
+
+ for (HeaderParams::const_iterator iter = m_params.begin(); iter != m_params.end(); ++iter)
+ {
+ if (iter->first == strParam)
+ values.push_back(iter->second);
+ }
+
+ return values;
+}
+
+std::string CHttpHeader::GetHeader(void) const
+{
+ if (m_protoLine.empty() && m_params.empty())
+ return "";
+
+ std::string strHeader(m_protoLine + "\r\n");
+
+ for (HeaderParams::const_iterator iter = m_params.begin(); iter != m_params.end(); ++iter)
+ strHeader += ((*iter).first + ": " + (*iter).second + "\r\n");
+
+ strHeader += "\r\n";
+ return strHeader;
+}
+
+std::string CHttpHeader::GetMimeType(void) const
+{
+ std::string strValue(GetValueRaw("content-type"));
+
+ std::string mimeType(strValue, 0, strValue.find(';'));
+ StringUtils::TrimRight(mimeType, m_whitespaceChars);
+
+ return mimeType;
+}
+
+std::string CHttpHeader::GetCharset(void) const
+{
+ std::string strValue(GetValueRaw("content-type"));
+ if (strValue.empty())
+ return strValue;
+
+ StringUtils::ToUpper(strValue);
+ const size_t len = strValue.length();
+
+ // extract charset value from 'contenttype/contentsubtype;pram1=param1Val ; charset=XXXX\t;param2=param2Val'
+ // most common form: 'text/html; charset=XXXX'
+ // charset value can be in double quotes: 'text/xml; charset="XXX XX"'
+
+ size_t pos = strValue.find(';');
+ while (pos < len)
+ {
+ // move to the next non-whitespace character
+ pos = strValue.find_first_not_of(m_whitespaceChars, pos + 1);
+
+ if (pos != std::string::npos)
+ {
+ if (strValue.compare(pos, 8, "CHARSET=", 8) == 0)
+ {
+ pos += 8; // move position to char after 'CHARSET='
+ size_t len = strValue.find(';', pos);
+ if (len != std::string::npos)
+ len -= pos;
+ std::string charset(strValue, pos, len); // intentionally ignoring possible ';' inside quoted string
+ // as we don't support any charset with ';' in name
+ StringUtils::Trim(charset, m_whitespaceChars);
+ if (!charset.empty())
+ {
+ if (charset[0] != '"')
+ return charset;
+ else
+ { // charset contains quoted string (allowed according to RFC 2616)
+ StringUtils::Replace(charset, "\\", ""); // unescape chars, ignoring possible '\"' and '\\'
+ const size_t closingQ = charset.find('"', 1);
+ if (closingQ == std::string::npos)
+ return ""; // no closing quote
+
+ return charset.substr(1, closingQ - 1);
+ }
+ }
+ }
+ pos = strValue.find(';', pos); // find next parameter
+ }
+ }
+
+ return ""; // no charset is detected
+}
+
+void CHttpHeader::Clear()
+{
+ m_params.clear();
+ m_protoLine.clear();
+ m_headerdone = false;
+ m_lastHeaderLine.clear();
+}
diff --git a/xbmc/utils/HttpHeader.h b/xbmc/utils/HttpHeader.h
new file mode 100644
index 0000000..6d8b543
--- /dev/null
+++ b/xbmc/utils/HttpHeader.h
@@ -0,0 +1,53 @@
+/*
+ * 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 <string>
+#include <utility>
+#include <vector>
+
+class CHttpHeader
+{
+public:
+ typedef std::pair<std::string, std::string> HeaderParamValue;
+ typedef std::vector<HeaderParamValue> HeaderParams;
+ typedef HeaderParams::iterator HeaderParamsIter;
+
+ CHttpHeader();
+ ~CHttpHeader();
+
+ void Parse(const std::string& strData);
+ void AddParam(const std::string& param, const std::string& value, const bool overwrite = false);
+
+ std::string GetValue(const std::string& strParam) const;
+ std::vector<std::string> GetValues(std::string strParam) const;
+
+ std::string GetHeader(void) const;
+
+ std::string GetMimeType(void) const;
+ std::string GetCharset(void) const;
+ inline std::string GetProtoLine() const
+ { return m_protoLine; }
+
+ inline bool IsHeaderDone(void) const
+ { return m_headerdone; }
+
+ void Clear();
+
+protected:
+ std::string GetValueRaw(const std::string& strParam) const;
+ bool ParseLine(const std::string& headerLine);
+
+ HeaderParams m_params;
+ std::string m_protoLine;
+ bool m_headerdone;
+ std::string m_lastHeaderLine;
+ static const char* const m_whitespaceChars;
+};
+
diff --git a/xbmc/utils/HttpParser.cpp b/xbmc/utils/HttpParser.cpp
new file mode 100644
index 0000000..9276d4c
--- /dev/null
+++ b/xbmc/utils/HttpParser.cpp
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2011-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.
+ *
+ * This code implements parsing of HTTP requests.
+ * This code was written by Steve Hanov in 2009, no copyright is claimed.
+ * This code is in the public domain.
+ * Code was taken from http://refactormycode.com/codes/778-an-efficient-http-parser
+ */
+
+#include "HttpParser.h"
+
+HttpParser::~HttpParser() = default;
+
+void
+HttpParser::parseHeader()
+{
+ // run the fsm.
+ const int CR = 13;
+ const int LF = 10;
+ const int ANY = 256;
+
+ enum Action {
+ // make lower case
+ LOWER = 0x1,
+
+ // convert current character to null.
+ NULLIFY = 0x2,
+
+ // set the header index to the current position
+ SET_HEADER_START = 0x4,
+
+ // set the key index to the current position
+ SET_KEY = 0x8,
+
+ // set value index to the current position.
+ SET_VALUE = 0x10,
+
+ // store current key/value pair.
+ STORE_KEY_VALUE = 0x20,
+
+ // sets content start to current position + 1
+ SET_CONTENT_START = 0x40
+ };
+
+ static const struct FSM {
+ State curState;
+ int c;
+ State nextState;
+ unsigned actions;
+ } fsm[] = {
+ { p_request_line, CR, p_request_line_cr, NULLIFY },
+ { p_request_line, ANY, p_request_line, 0 },
+ { p_request_line_cr, LF, p_request_line_crlf, 0 },
+ { p_request_line_crlf, CR, p_request_line_crlfcr, 0 },
+ { p_request_line_crlf, ANY, p_key, SET_HEADER_START | SET_KEY | LOWER },
+ { p_request_line_crlfcr, LF, p_content, SET_CONTENT_START },
+ { p_key, ':', p_key_colon, NULLIFY },
+ { p_key, ANY, p_key, LOWER },
+ { p_key_colon, ' ', p_key_colon_sp, 0 },
+ { p_key_colon_sp, ANY, p_value, SET_VALUE },
+ { p_value, CR, p_value_cr, NULLIFY | STORE_KEY_VALUE },
+ { p_value, ANY, p_value, 0 },
+ { p_value_cr, LF, p_value_crlf, 0 },
+ { p_value_crlf, CR, p_value_crlfcr, 0 },
+ { p_value_crlf, ANY, p_key, SET_KEY | LOWER },
+ { p_value_crlfcr, LF, p_content, SET_CONTENT_START },
+ { p_error, ANY, p_error, 0 }
+ };
+
+ for( unsigned i = _parsedTo; i < _data.length(); ++i) {
+ char c = _data[i];
+ State nextState = p_error;
+
+ for (const FSM& f : fsm) {
+ if ( f.curState == _state &&
+ ( c == f.c || f.c == ANY ) ) {
+
+ nextState = f.nextState;
+
+ if ( f.actions & LOWER ) {
+ _data[i] = tolower( _data[i] );
+ }
+
+ if ( f.actions & NULLIFY ) {
+ _data[i] = 0;
+ }
+
+ if ( f.actions & SET_HEADER_START ) {
+ _headerStart = i;
+ }
+
+ if ( f.actions & SET_KEY ) {
+ _keyIndex = i;
+ }
+
+ if ( f.actions & SET_VALUE ) {
+ _valueIndex = i;
+ }
+
+ if ( f.actions & SET_CONTENT_START ) {
+ _contentStart = i + 1;
+ }
+
+ if ( f.actions & STORE_KEY_VALUE ) {
+ // store position of first character of key.
+ _keys.push_back( _keyIndex );
+ }
+
+ break;
+ }
+ }
+
+ _state = nextState;
+
+ if ( _state == p_content ) {
+ const char* str = getValue("content-length");
+ if ( str ) {
+ _contentLength = atoi( str );
+ }
+ break;
+ }
+ }
+
+ _parsedTo = _data.length();
+
+}
+
+bool
+HttpParser::parseRequestLine()
+{
+ size_t sp1;
+ size_t sp2;
+
+ sp1 = _data.find( ' ', 0 );
+ if ( sp1 == std::string::npos ) return false;
+ sp2 = _data.find( ' ', sp1 + 1 );
+ if ( sp2 == std::string::npos ) return false;
+
+ _data[sp1] = 0;
+ _data[sp2] = 0;
+ _uriIndex = sp1 + 1;
+ return true;
+}
+
+HttpParser::status_t
+HttpParser::addBytes( const char* bytes, unsigned len )
+{
+ if ( _status != Incomplete ) {
+ return _status;
+ }
+
+ // append the bytes to data.
+ _data.append( bytes, len );
+
+ if ( _state < p_content ) {
+ parseHeader();
+ }
+
+ if ( _state == p_error ) {
+ _status = Error;
+ } else if ( _state == p_content ) {
+ if ( _contentLength == 0 || _data.length() - _contentStart >= _contentLength ) {
+ if ( parseRequestLine() ) {
+ _status = Done;
+ } else {
+ _status = Error;
+ }
+ }
+ }
+
+ return _status;
+}
+
+const char*
+HttpParser::getMethod() const
+{
+ return &_data[0];
+}
+
+const char*
+HttpParser::getUri() const
+{
+ return &_data[_uriIndex];
+}
+
+const char*
+HttpParser::getQueryString() const
+{
+ const char* pos = getUri();
+ while( *pos ) {
+ if ( *pos == '?' ) {
+ pos++;
+ break;
+ }
+ pos++;
+ }
+ return pos;
+}
+
+const char*
+HttpParser::getBody() const
+{
+ if ( _contentLength > 0 ) {
+ return &_data[_contentStart];
+ } else {
+ return NULL;
+ }
+}
+
+// key should be in lower case.
+const char*
+HttpParser::getValue( const char* key ) const
+{
+ for( IntArray::const_iterator iter = _keys.begin();
+ iter != _keys.end(); ++iter )
+ {
+ unsigned index = *iter;
+ if ( strcmp( &_data[index], key ) == 0 ) {
+ return &_data[index + strlen(key) + 2];
+ }
+
+ }
+
+ return NULL;
+}
+
+unsigned
+HttpParser::getContentLength() const
+{
+ return _contentLength;
+}
+
diff --git a/xbmc/utils/HttpParser.h b/xbmc/utils/HttpParser.h
new file mode 100644
index 0000000..47a3a9f
--- /dev/null
+++ b/xbmc/utils/HttpParser.h
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2011-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.
+ *
+ * This code implements parsing of HTTP requests.
+ * This code was written by Steve Hanov in 2009, no copyright is claimed.
+ * This code is in the public domain.
+ * Code was taken from http://refactormycode.com/codes/778-an-efficient-http-parser
+ */
+
+#pragma once
+
+#include <stdlib.h>
+#include <string.h>
+#include <string>
+#include <vector>
+
+// A class to incrementally parse an HTTP header as it comes in. It
+// lets you know when it has received all required bytes, as specified
+// by the content-length header (if present). If there is no content-length,
+// it will stop reading after the final "\n\r".
+//
+// Example usage:
+//
+// HttpParser parser;
+// HttpParser::status_t status;
+//
+// for( ;; ) {
+// // read bytes from socket into buffer, break on error
+// status = parser.addBytes( buffer, length );
+// if ( status != HttpParser::Incomplete ) break;
+// }
+//
+// if ( status == HttpParser::Done ) {
+// // parse fully formed http message.
+// }
+
+
+class HttpParser
+{
+public:
+ ~HttpParser();
+
+ enum status_t {
+ Done,
+ Error,
+ Incomplete
+ };
+
+ status_t addBytes( const char* bytes, unsigned len );
+
+ const char* getMethod() const;
+ const char* getUri() const;
+ const char* getQueryString() const;
+ const char* getBody() const;
+ // key should be in lower case when looking up.
+ const char* getValue( const char* key ) const;
+ unsigned getContentLength() const;
+
+private:
+ void parseHeader();
+ bool parseRequestLine();
+
+ std::string _data;
+ unsigned _headerStart = 0;
+ unsigned _parsedTo = 0 ;
+ int _state = 0 ;
+ unsigned _keyIndex = 0;
+ unsigned _valueIndex = 0;
+ unsigned _contentLength = 0;
+ unsigned _contentStart = 0;
+ unsigned _uriIndex = 0;
+
+ typedef std::vector<unsigned> IntArray;
+ IntArray _keys;
+
+ enum State {
+ p_request_line=0,
+ p_request_line_cr=1,
+ p_request_line_crlf=2,
+ p_request_line_crlfcr=3,
+ p_key=4,
+ p_key_colon=5,
+ p_key_colon_sp=6,
+ p_value=7,
+ p_value_cr=8,
+ p_value_crlf=9,
+ p_value_crlfcr=10,
+ p_content=11, // here we are done parsing the header.
+ p_error=12 // here an error has occurred and the parse failed.
+ };
+
+ status_t _status = Incomplete ;
+};
+
diff --git a/xbmc/utils/HttpRangeUtils.cpp b/xbmc/utils/HttpRangeUtils.cpp
new file mode 100644
index 0000000..18ad32b
--- /dev/null
+++ b/xbmc/utils/HttpRangeUtils.cpp
@@ -0,0 +1,424 @@
+/*
+ * 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 <algorithm>
+
+#include "HttpRangeUtils.h"
+#include "Util.h"
+#ifdef HAS_WEB_SERVER
+#include "network/httprequesthandler/IHTTPRequestHandler.h"
+#endif // HAS_WEB_SERVER
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <inttypes.h>
+
+#define HEADER_NEWLINE "\r\n"
+#define HEADER_SEPARATOR HEADER_NEWLINE HEADER_NEWLINE
+#define HEADER_BOUNDARY "--"
+
+#define HEADER_CONTENT_RANGE_VALUE "{}"
+#define HEADER_CONTENT_RANGE_VALUE_UNKNOWN "*"
+#define HEADER_CONTENT_RANGE_FORMAT_BYTES "bytes " HEADER_CONTENT_RANGE_VALUE "-" HEADER_CONTENT_RANGE_VALUE "/"
+#define CONTENT_RANGE_FORMAT_TOTAL HEADER_CONTENT_RANGE_FORMAT_BYTES HEADER_CONTENT_RANGE_VALUE
+#define CONTENT_RANGE_FORMAT_TOTAL_UNKNOWN HEADER_CONTENT_RANGE_FORMAT_BYTES HEADER_CONTENT_RANGE_VALUE_UNKNOWN
+
+CHttpRange::CHttpRange(uint64_t firstPosition, uint64_t lastPosition)
+ : m_first(firstPosition),
+ m_last(lastPosition)
+{ }
+
+bool CHttpRange::operator<(const CHttpRange &other) const
+{
+ return (m_first < other.m_first) ||
+ (m_first == other.m_first && m_last < other.m_last);
+}
+
+bool CHttpRange::operator==(const CHttpRange &other) const
+{
+ return m_first == other.m_first && m_last == other.m_last;
+}
+
+bool CHttpRange::operator!=(const CHttpRange &other) const
+{
+ return !(*this == other);
+}
+
+uint64_t CHttpRange::GetLength() const
+{
+ if (!IsValid())
+ return 0;
+
+ return m_last - m_first + 1;
+}
+
+void CHttpRange::SetLength(uint64_t length)
+{
+ m_last = m_first + length - 1;
+}
+
+bool CHttpRange::IsValid() const
+{
+ return m_last >= m_first;
+}
+
+CHttpResponseRange::CHttpResponseRange()
+ : CHttpRange(),
+ m_data(NULL)
+{ }
+
+CHttpResponseRange::CHttpResponseRange(uint64_t firstPosition, uint64_t lastPosition)
+ : CHttpRange(firstPosition, lastPosition),
+ m_data(NULL)
+{ }
+
+CHttpResponseRange::CHttpResponseRange(const void* data, uint64_t firstPosition, uint64_t lastPosition)
+ : CHttpRange(firstPosition, lastPosition),
+ m_data(data)
+{ }
+
+CHttpResponseRange::CHttpResponseRange(const void* data, uint64_t length)
+ : CHttpRange(0, length - 1),
+ m_data(data)
+{ }
+
+bool CHttpResponseRange::operator==(const CHttpResponseRange &other) const
+{
+ if (!CHttpRange::operator==(other))
+ return false;
+
+ return m_data == other.m_data;
+}
+
+bool CHttpResponseRange::operator!=(const CHttpResponseRange &other) const
+{
+ return !(*this == other);
+}
+
+void CHttpResponseRange::SetData(const void* data, uint64_t length)
+{
+ if (length == 0)
+ return;
+
+ m_first = 0;
+
+ SetData(data);
+ SetLength(length);
+}
+
+void CHttpResponseRange::SetData(const void* data, uint64_t firstPosition, uint64_t lastPosition)
+{
+ SetData(data);
+ SetFirstPosition(firstPosition);
+ SetLastPosition(lastPosition);
+}
+
+bool CHttpResponseRange::IsValid() const
+{
+ if (!CHttpRange::IsValid())
+ return false;
+
+ return m_data != NULL;
+}
+
+CHttpRanges::CHttpRanges()
+: m_ranges()
+{ }
+
+CHttpRanges::CHttpRanges(const HttpRanges& httpRanges)
+: m_ranges(httpRanges)
+{
+ SortAndCleanup();
+}
+
+bool CHttpRanges::Get(size_t index, CHttpRange& range) const
+{
+ if (index >= Size())
+ return false;
+
+ range = m_ranges.at(index);
+ return true;
+}
+
+bool CHttpRanges::GetFirst(CHttpRange& range) const
+{
+ if (m_ranges.empty())
+ return false;
+
+ range = m_ranges.front();
+ return true;
+}
+
+bool CHttpRanges::GetLast(CHttpRange& range) const
+{
+ if (m_ranges.empty())
+ return false;
+
+ range = m_ranges.back();
+ return true;
+}
+
+bool CHttpRanges::GetFirstPosition(uint64_t& position) const
+{
+ if (m_ranges.empty())
+ return false;
+
+ position = m_ranges.front().GetFirstPosition();
+ return true;
+}
+
+bool CHttpRanges::GetLastPosition(uint64_t& position) const
+{
+ if (m_ranges.empty())
+ return false;
+
+ position = m_ranges.back().GetLastPosition();
+ return true;
+}
+
+uint64_t CHttpRanges::GetLength() const
+{
+ uint64_t length = 0;
+ for (HttpRanges::const_iterator range = m_ranges.begin(); range != m_ranges.end(); ++range)
+ length += range->GetLength();
+
+ return length;
+}
+
+bool CHttpRanges::GetTotalRange(CHttpRange& range) const
+{
+ if (m_ranges.empty())
+ return false;
+
+ uint64_t firstPosition, lastPosition;
+ if (!GetFirstPosition(firstPosition) || !GetLastPosition(lastPosition))
+ return false;
+
+ range.SetFirstPosition(firstPosition);
+ range.SetLastPosition(lastPosition);
+
+ return range.IsValid();
+}
+
+void CHttpRanges::Add(const CHttpRange& range)
+{
+ if (!range.IsValid())
+ return;
+
+ m_ranges.push_back(range);
+
+ SortAndCleanup();
+}
+
+void CHttpRanges::Remove(size_t index)
+{
+ if (index >= Size())
+ return;
+
+ m_ranges.erase(m_ranges.begin() + index);
+}
+
+void CHttpRanges::Clear()
+{
+ m_ranges.clear();
+}
+
+bool CHttpRanges::Parse(const std::string& header)
+{
+ return Parse(header, std::numeric_limits<uint64_t>::max());
+}
+
+bool CHttpRanges::Parse(const std::string& header, uint64_t totalLength)
+{
+ m_ranges.clear();
+
+ if (header.empty() || totalLength == 0 || !StringUtils::StartsWithNoCase(header, "bytes="))
+ return false;
+
+ uint64_t lastPossiblePosition = totalLength - 1;
+
+ // remove "bytes=" from the beginning
+ std::string rangesValue = header.substr(6);
+
+ // split the value of the "Range" header by ","
+ std::vector<std::string> rangeValues = StringUtils::Split(rangesValue, ",");
+
+ for (std::vector<std::string>::const_iterator range = rangeValues.begin(); range != rangeValues.end(); ++range)
+ {
+ // there must be a "-" in the range definition
+ if (range->find("-") == std::string::npos)
+ return false;
+
+ std::vector<std::string> positions = StringUtils::Split(*range, "-");
+ if (positions.size() != 2)
+ return false;
+
+ bool hasStart = false;
+ uint64_t start = 0;
+ bool hasEnd = false;
+ uint64_t end = 0;
+
+ // parse the start and end positions
+ if (!positions.front().empty())
+ {
+ if (!StringUtils::IsNaturalNumber(positions.front()))
+ return false;
+
+ start = str2uint64(positions.front(), 0);
+ hasStart = true;
+ }
+ if (!positions.back().empty())
+ {
+ if (!StringUtils::IsNaturalNumber(positions.back()))
+ return false;
+
+ end = str2uint64(positions.back(), 0);
+ hasEnd = true;
+ }
+
+ // nothing defined at all
+ if (!hasStart && !hasEnd)
+ return false;
+
+ // make sure that the end position makes sense
+ if (hasEnd)
+ end = std::min(end, lastPossiblePosition);
+
+ if (!hasStart && hasEnd)
+ {
+ // the range is defined as the number of bytes from the end
+ start = totalLength - end;
+ end = lastPossiblePosition;
+ }
+ else if (hasStart && !hasEnd)
+ end = lastPossiblePosition;
+
+ // make sure the start position makes sense
+ if (start > lastPossiblePosition)
+ return false;
+
+ // make sure that the start position is smaller or equal to the end position
+ if (end < start)
+ return false;
+
+ m_ranges.push_back(CHttpRange(start, end));
+ }
+
+ if (m_ranges.empty())
+ return false;
+
+ SortAndCleanup();
+ return !m_ranges.empty();
+}
+
+void CHttpRanges::SortAndCleanup()
+{
+ // sort the ranges by their first position
+ std::sort(m_ranges.begin(), m_ranges.end());
+
+ // check for overlapping ranges
+ for (HttpRanges::iterator range = m_ranges.begin() + 1; range != m_ranges.end();)
+ {
+ HttpRanges::iterator previous = range - 1;
+
+ // check if the current and the previous range overlap
+ if (previous->GetLastPosition() + 1 >= range->GetFirstPosition())
+ {
+ // combine the previous and the current ranges by setting the last position of the previous range
+ // to the last position of the current range
+ previous->SetLastPosition(range->GetLastPosition());
+
+ // then remove the current range which is not needed anymore
+ range = m_ranges.erase(range);
+ }
+ else
+ ++range;
+ }
+}
+
+std::string HttpRangeUtils::GenerateContentRangeHeaderValue(const CHttpRange* range)
+{
+ if (range == NULL)
+ return "";
+
+ return StringUtils::Format(CONTENT_RANGE_FORMAT_TOTAL, range->GetFirstPosition(), range->GetLastPosition(), range->GetLength());
+}
+
+std::string HttpRangeUtils::GenerateContentRangeHeaderValue(uint64_t start, uint64_t end, uint64_t total)
+{
+ if (total > 0)
+ return StringUtils::Format(CONTENT_RANGE_FORMAT_TOTAL, start, end, total);
+
+ return StringUtils::Format(CONTENT_RANGE_FORMAT_TOTAL_UNKNOWN, start, end);
+}
+
+#ifdef HAS_WEB_SERVER
+
+std::string HttpRangeUtils::GenerateMultipartBoundary()
+{
+ static char chars[] = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ // create a string of length 30 to 40 and pre-fill it with "-"
+ size_t count = static_cast<size_t>(CUtil::GetRandomNumber()) % 11 + 30;
+ std::string boundary(count, '-');
+
+ for (size_t i = static_cast<size_t>(CUtil::GetRandomNumber()) % 5 + 8; i < count; i++)
+ boundary.replace(i, 1, 1, chars[static_cast<size_t>(CUtil::GetRandomNumber()) % 64]);
+
+ return boundary;
+}
+
+std::string HttpRangeUtils::GenerateMultipartBoundaryContentType(const std::string& multipartBoundary)
+{
+ if (multipartBoundary.empty())
+ return "";
+
+ return "multipart/byteranges; boundary=" + multipartBoundary;
+}
+
+std::string HttpRangeUtils::GenerateMultipartBoundaryWithHeader(const std::string& multipartBoundary, const std::string& contentType)
+{
+ if (multipartBoundary.empty())
+ return "";
+
+ std::string boundaryWithHeader = HEADER_BOUNDARY + multipartBoundary + HEADER_NEWLINE;
+ if (!contentType.empty())
+ boundaryWithHeader += MHD_HTTP_HEADER_CONTENT_TYPE ": " + contentType + HEADER_NEWLINE;
+
+ return boundaryWithHeader;
+}
+
+std::string HttpRangeUtils::GenerateMultipartBoundaryWithHeader(const std::string& multipartBoundary, const std::string& contentType, const CHttpRange* range)
+{
+ if (multipartBoundary.empty() || range == NULL)
+ return "";
+
+ return GenerateMultipartBoundaryWithHeader(GenerateMultipartBoundaryWithHeader(multipartBoundary, contentType), range);
+}
+
+std::string HttpRangeUtils::GenerateMultipartBoundaryWithHeader(const std::string& multipartBoundaryWithContentType, const CHttpRange* range)
+{
+ if (multipartBoundaryWithContentType.empty() || range == NULL)
+ return "";
+
+ std::string boundaryWithHeader = multipartBoundaryWithContentType;
+ boundaryWithHeader += MHD_HTTP_HEADER_CONTENT_RANGE ": " + GenerateContentRangeHeaderValue(range);
+ boundaryWithHeader += HEADER_SEPARATOR;
+
+ return boundaryWithHeader;
+}
+
+std::string HttpRangeUtils::GenerateMultipartBoundaryEnd(const std::string& multipartBoundary)
+{
+ if (multipartBoundary.empty())
+ return "";
+
+ return HEADER_NEWLINE HEADER_BOUNDARY + multipartBoundary + HEADER_BOUNDARY;
+}
+
+#endif // HAS_WEB_SERVER
diff --git a/xbmc/utils/HttpRangeUtils.h b/xbmc/utils/HttpRangeUtils.h
new file mode 100644
index 0000000..7e0b66d
--- /dev/null
+++ b/xbmc/utils/HttpRangeUtils.h
@@ -0,0 +1,187 @@
+/*
+ * 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 <stdint.h>
+#include <string>
+#include <vector>
+
+class CHttpRange
+{
+public:
+ CHttpRange() = default;
+ CHttpRange(uint64_t firstPosition, uint64_t lastPosition);
+ virtual ~CHttpRange() = default;
+
+ bool operator<(const CHttpRange &other) const;
+ bool operator==(const CHttpRange &other) const;
+ bool operator!=(const CHttpRange &other) const;
+
+ virtual uint64_t GetFirstPosition() const { return m_first; }
+ virtual void SetFirstPosition(uint64_t firstPosition) { m_first = firstPosition; }
+ virtual uint64_t GetLastPosition() const { return m_last; }
+ virtual void SetLastPosition(uint64_t lastPosition) { m_last = lastPosition; }
+
+ virtual uint64_t GetLength() const;
+ virtual void SetLength(uint64_t length);
+
+ virtual bool IsValid() const;
+
+protected:
+ uint64_t m_first = 1;
+ uint64_t m_last = 0;
+};
+
+typedef std::vector<CHttpRange> HttpRanges;
+
+class CHttpResponseRange : public CHttpRange
+{
+public:
+ CHttpResponseRange();
+ CHttpResponseRange(uint64_t firstPosition, uint64_t lastPosition);
+ CHttpResponseRange(const void* data, uint64_t firstPosition, uint64_t lastPosition);
+ CHttpResponseRange(const void* data, uint64_t length);
+ ~CHttpResponseRange() override = default;
+
+ bool operator==(const CHttpResponseRange &other) const;
+ bool operator!=(const CHttpResponseRange &other) const;
+
+ const void* GetData() const { return m_data; }
+ void SetData(const void* data) { m_data = data; }
+ void SetData(const void* data, uint64_t length);
+ void SetData(const void* data, uint64_t firstPosition, uint64_t lastPosition);
+
+ bool IsValid() const override;
+
+protected:
+ const void* m_data;
+};
+
+typedef std::vector<CHttpResponseRange> HttpResponseRanges;
+
+class CHttpRanges final
+{
+public:
+ CHttpRanges();
+ explicit CHttpRanges(const HttpRanges& httpRanges);
+
+ const HttpRanges& Get() const { return m_ranges; }
+ bool Get(size_t index, CHttpRange& range) const;
+ bool GetFirst(CHttpRange& range) const;
+ bool GetLast(CHttpRange& range) const;
+ size_t Size() const { return m_ranges.size(); }
+ bool IsEmpty() const { return m_ranges.empty(); }
+
+ bool GetFirstPosition(uint64_t& position) const;
+ bool GetLastPosition(uint64_t& position) const;
+ uint64_t GetLength() const;
+
+ bool GetTotalRange(CHttpRange& range) const;
+
+ void Add(const CHttpRange& range);
+ void Remove(size_t index);
+ void Clear();
+
+ HttpRanges::const_iterator Begin() const { return m_ranges.begin(); }
+ HttpRanges::const_iterator End() const { return m_ranges.end(); }
+
+ bool Parse(const std::string& header);
+ bool Parse(const std::string& header, uint64_t totalLength);
+
+protected:
+ void SortAndCleanup();
+
+ HttpRanges m_ranges;
+};
+
+class HttpRangeUtils
+{
+public:
+ /*!
+ * \brief Generates a valid Content-Range HTTP header value for the given HTTP
+ * range definition.
+ *
+ * \param range HTTP range definition used to generate the Content-Range HTTP header
+ * \return Content-Range HTTP header value
+ */
+ static std::string GenerateContentRangeHeaderValue(const CHttpRange* range);
+
+ /*!
+ * \brief Generates a valid Content-Range HTTP header value for the given HTTP
+ * range properties.
+ *
+ * \param start Start position of the HTTP range
+ * \param end Last/End position of the HTTP range
+ * \param total Total length of original content (not just the range)
+ * \return Content-Range HTTP header value
+ */
+ static std::string GenerateContentRangeHeaderValue(uint64_t start, uint64_t end, uint64_t total);
+
+#ifdef HAS_WEB_SERVER
+ /*!
+ * \brief Generates a multipart boundary that can be used in ranged HTTP
+ * responses.
+ *
+ * \return Multipart boundary that can be used in ranged HTTP responses
+ */
+ static std::string GenerateMultipartBoundary();
+
+ /*!
+ * \brief Generates the multipart/byteranges Content-Type HTTP header value
+ * containing the given multipart boundary for a ranged HTTP response.
+ *
+ * \param multipartBoundary Multipart boundary to be used in the ranged HTTP response
+ * \return multipart/byteranges Content-Type HTTP header value
+ */
+ static std::string GenerateMultipartBoundaryContentType(const std::string& multipartBoundary);
+
+ /*!
+ * \brief Generates a multipart boundary including the Content-Type HTTP
+ * header value with the (actual) given content type of the original
+ * content.
+ *
+ * \param multipartBoundary Multipart boundary to be used in the ranged HTTP response
+ * \param contentType (Actual) Content type of the original content
+ * \return Multipart boundary (including the Content-Type HTTP header) value that can be used in ranged HTTP responses
+ */
+ static std::string GenerateMultipartBoundaryWithHeader(const std::string& multipartBoundary, const std::string& contentType);
+
+ /*!
+ * \brief Generates a multipart boundary including the Content-Type HTTP
+ * header value with the (actual) given content type of the original
+ * content and the Content-Range HTTP header value for the given range.
+ *
+ * \param multipartBoundary Multipart boundary to be used in the ranged HTTP response
+ * \param contentType (Actual) Content type of the original content
+ * \param range HTTP range definition used to generate the Content-Range HTTP header
+ * \return Multipart boundary (including the Content-Type and Content-Range HTTP headers) value that can be used in ranged HTTP responses
+ */
+ static std::string GenerateMultipartBoundaryWithHeader(const std::string& multipartBoundary, const std::string& contentType, const CHttpRange* range);
+
+ /*!
+ * \brief Generates a multipart boundary including the Content-Type HTTP
+ * header value with the (actual) given content type of the original
+ * content and the Content-Range HTTP header value for the given range.
+ *
+ * \param multipartBoundaryWithContentType Multipart boundary (already including the Content-Type HTTP header value) to be used in the ranged HTTP response
+ * \param range HTTP range definition used to generate the Content-Range HTTP header
+ * \return Multipart boundary (including the Content-Type and Content-Range HTTP headers) value that can be used in ranged HTTP responses
+ */
+ static std::string GenerateMultipartBoundaryWithHeader(const std::string& multipartBoundaryWithContentType, const CHttpRange* range);
+
+ /*!
+ * \brief Generates a multipart boundary end that can be used in ranged HTTP
+ * responses.
+ *
+ * \param multipartBoundary Multipart boundary to be used in the ranged HTTP response
+ * \return Multipart boundary end that can be used in a ranged HTTP response
+ */
+ static std::string GenerateMultipartBoundaryEnd(const std::string& multipartBoundary);
+#endif // HAS_WEB_SERVER
+};
diff --git a/xbmc/utils/HttpResponse.cpp b/xbmc/utils/HttpResponse.cpp
new file mode 100644
index 0000000..33c7c27
--- /dev/null
+++ b/xbmc/utils/HttpResponse.cpp
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2011-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 "HttpResponse.h"
+
+#include <stdio.h>
+
+#define SPACE " "
+#define SEPARATOR ": "
+#define LINEBREAK "\r\n"
+
+#define HEADER_CONTENT_LENGTH "Content-Length"
+
+std::map<HTTP::StatusCode, std::string> CHttpResponse::m_statusCodeText = CHttpResponse::createStatusCodes();
+
+CHttpResponse::CHttpResponse(HTTP::Method method, HTTP::StatusCode status, HTTP::Version version /* = HTTPVersion1_1 */)
+{
+ m_method = method;
+ m_status = status;
+ m_version = version;
+
+ m_content = NULL;
+ m_contentLength = 0;
+}
+
+void CHttpResponse::AddHeader(const std::string &field, const std::string &value)
+{
+ if (field.empty())
+ return;
+
+ m_headers.emplace_back(field, value);
+}
+
+void CHttpResponse::SetContent(const char* data, unsigned int length)
+{
+ m_content = data;
+
+ if (m_content == NULL)
+ m_contentLength = 0;
+ else
+ m_contentLength = length;
+}
+
+std::string CHttpResponse::Create()
+{
+ m_buffer.clear();
+
+ m_buffer.append("HTTP/");
+ switch (m_version)
+ {
+ case HTTP::Version1_0:
+ m_buffer.append("1.0");
+ break;
+
+ case HTTP::Version1_1:
+ m_buffer.append("1.1");
+ break;
+
+ default:
+ return 0;
+ }
+
+ char statusBuffer[4];
+ sprintf(statusBuffer, "%d", (int)m_status);
+ m_buffer.append(SPACE);
+ m_buffer.append(statusBuffer);
+
+ m_buffer.append(SPACE);
+ m_buffer.append(m_statusCodeText.find(m_status)->second);
+ m_buffer.append(LINEBREAK);
+
+ bool hasContentLengthHeader = false;
+ for (unsigned int index = 0; index < m_headers.size(); index++)
+ {
+ m_buffer.append(m_headers[index].first);
+ m_buffer.append(SEPARATOR);
+ m_buffer.append(m_headers[index].second);
+ m_buffer.append(LINEBREAK);
+
+ if (m_headers[index].first.compare(HEADER_CONTENT_LENGTH) == 0)
+ hasContentLengthHeader = true;
+ }
+
+ if (!hasContentLengthHeader && m_content != NULL && m_contentLength > 0)
+ {
+ m_buffer.append(HEADER_CONTENT_LENGTH);
+ m_buffer.append(SEPARATOR);
+ char lengthBuffer[11];
+ sprintf(lengthBuffer, "%u", m_contentLength);
+ m_buffer.append(lengthBuffer);
+ m_buffer.append(LINEBREAK);
+ }
+
+ m_buffer.append(LINEBREAK);
+ if (m_content != NULL && m_contentLength > 0)
+ m_buffer.append(m_content, m_contentLength);
+
+ return m_buffer;
+}
+
+std::map<HTTP::StatusCode, std::string> CHttpResponse::createStatusCodes()
+{
+ std::map<HTTP::StatusCode, std::string> map;
+ map[HTTP::Continue] = "Continue";
+ map[HTTP::SwitchingProtocols] = "Switching Protocols";
+ map[HTTP::Processing] = "Processing";
+ map[HTTP::ConnectionTimedOut] = "Connection timed out";
+ map[HTTP::OK] = "OK";
+ map[HTTP::Created] = "Created";
+ map[HTTP::Accepted] = "Accepted";
+ map[HTTP::NonAuthoritativeInformation] = "Non-Authoritative Information";
+ map[HTTP::NoContent] = "No Content";
+ map[HTTP::ResetContent] = "Reset Content";
+ map[HTTP::PartialContent] = "Partial Content";
+ map[HTTP::MultiStatus] = "Multi-Status";
+ map[HTTP::MultipleChoices] = "Multiple Choices";
+ map[HTTP::MovedPermanently] = "Moved Permanently";
+ map[HTTP::Found] = "Found";
+ map[HTTP::SeeOther] = "See Other";
+ map[HTTP::NotModified] = "Not Modified";
+ map[HTTP::UseProxy] = "Use Proxy";
+ //map[HTTP::SwitchProxy] = "Switch Proxy";
+ map[HTTP::TemporaryRedirect] = "Temporary Redirect";
+ map[HTTP::BadRequest] = "Bad Request";
+ map[HTTP::Unauthorized] = "Unauthorized";
+ map[HTTP::PaymentRequired] = "Payment Required";
+ map[HTTP::Forbidden] = "Forbidden";
+ map[HTTP::NotFound] = "Not Found";
+ map[HTTP::MethodNotAllowed] = "Method Not Allowed";
+ map[HTTP::NotAcceptable] = "Not Acceptable";
+ map[HTTP::ProxyAuthenticationRequired] = "Proxy Authentication Required";
+ map[HTTP::RequestTimeout] = "Request Time-out";
+ map[HTTP::Conflict] = "Conflict";
+ map[HTTP::Gone] = "Gone";
+ map[HTTP::LengthRequired] = "Length Required";
+ map[HTTP::PreconditionFailed] = "Precondition Failed";
+ map[HTTP::RequestEntityTooLarge] = "Request Entity Too Large";
+ map[HTTP::RequestURITooLong] = "Request-URI Too Long";
+ map[HTTP::UnsupportedMediaType] = "Unsupported Media Type";
+ map[HTTP::RequestedRangeNotSatisfiable] = "Requested range not satisfiable";
+ map[HTTP::ExpectationFailed] = "Expectation Failed";
+ map[HTTP::ImATeapot] = "I'm a Teapot";
+ map[HTTP::TooManyConnections] = "There are too many connections from your internet address";
+ map[HTTP::UnprocessableEntity] = "Unprocessable Entity";
+ map[HTTP::Locked] = "Locked";
+ map[HTTP::FailedDependency] = "Failed Dependency";
+ map[HTTP::UnorderedCollection] = "UnorderedCollection";
+ map[HTTP::UpgradeRequired] = "Upgrade Required";
+ map[HTTP::InternalServerError] = "Internal Server Error";
+ map[HTTP::NotImplemented] = "Not Implemented";
+ map[HTTP::BadGateway] = "Bad Gateway";
+ map[HTTP::ServiceUnavailable] = "Service Unavailable";
+ map[HTTP::GatewayTimeout] = "Gateway Time-out";
+ map[HTTP::HTTPVersionNotSupported] = "HTTP Version not supported";
+ map[HTTP::VariantAlsoNegotiates] = "Variant Also Negotiates";
+ map[HTTP::InsufficientStorage] = "Insufficient Storage";
+ map[HTTP::BandwidthLimitExceeded] = "Bandwidth Limit Exceeded";
+ map[HTTP::NotExtended] = "Not Extended";
+
+ return map;
+}
diff --git a/xbmc/utils/HttpResponse.h b/xbmc/utils/HttpResponse.h
new file mode 100644
index 0000000..50fc739
--- /dev/null
+++ b/xbmc/utils/HttpResponse.h
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2011-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 <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace HTTP
+{
+ enum Version
+ {
+ Version1_0,
+ Version1_1
+ };
+
+ enum Method
+ {
+ Get,
+ Head,
+ POST,
+ PUT,
+ Delete,
+ Trace,
+ Connect
+ };
+
+ enum StatusCode
+ {
+ // Information 1xx
+ Continue = 100,
+ SwitchingProtocols = 101,
+ Processing = 102,
+ ConnectionTimedOut = 103,
+
+ // Success 2xx
+ OK = 200,
+ Created = 201,
+ Accepted = 202,
+ NonAuthoritativeInformation = 203,
+ NoContent = 204,
+ ResetContent = 205,
+ PartialContent = 206,
+ MultiStatus = 207,
+
+ // Redirects 3xx
+ MultipleChoices = 300,
+ MovedPermanently = 301,
+ Found = 302,
+ SeeOther = 303,
+ NotModified = 304,
+ UseProxy = 305,
+ //SwitchProxy = 306,
+ TemporaryRedirect = 307,
+
+ // Client errors 4xx
+ BadRequest = 400,
+ Unauthorized = 401,
+ PaymentRequired = 402,
+ Forbidden = 403,
+ NotFound = 404,
+ MethodNotAllowed = 405,
+ NotAcceptable = 406,
+ ProxyAuthenticationRequired = 407,
+ RequestTimeout = 408,
+ Conflict = 409,
+ Gone = 410,
+ LengthRequired = 411,
+ PreconditionFailed = 412,
+ RequestEntityTooLarge = 413,
+ RequestURITooLong = 414,
+ UnsupportedMediaType = 415,
+ RequestedRangeNotSatisfiable = 416,
+ ExpectationFailed = 417,
+ ImATeapot = 418,
+ TooManyConnections = 421,
+ UnprocessableEntity = 422,
+ Locked = 423,
+ FailedDependency = 424,
+ UnorderedCollection = 425,
+ UpgradeRequired = 426,
+
+ // Server errors 5xx
+ InternalServerError = 500,
+ NotImplemented = 501,
+ BadGateway = 502,
+ ServiceUnavailable = 503,
+ GatewayTimeout = 504,
+ HTTPVersionNotSupported = 505,
+ VariantAlsoNegotiates = 506,
+ InsufficientStorage = 507,
+ BandwidthLimitExceeded = 509,
+ NotExtended = 510
+ };
+}
+
+class CHttpResponse
+{
+public:
+ CHttpResponse(HTTP::Method method, HTTP::StatusCode status, HTTP::Version version = HTTP::Version1_1);
+
+ void AddHeader(const std::string &field, const std::string &value);
+ void SetContent(const char* data, unsigned int length);
+
+ std::string Create();
+
+private:
+ HTTP::Method m_method;
+ HTTP::StatusCode m_status;
+ HTTP::Version m_version;
+ std::vector< std::pair<std::string, std::string> > m_headers;
+ const char* m_content;
+ unsigned int m_contentLength;
+ std::string m_buffer;
+
+ static std::map<HTTP::StatusCode, std::string> m_statusCodeText;
+ static std::map<HTTP::StatusCode, std::string> createStatusCodes();
+};
diff --git a/xbmc/utils/IArchivable.h b/xbmc/utils/IArchivable.h
new file mode 100644
index 0000000..c481e7b
--- /dev/null
+++ b/xbmc/utils/IArchivable.h
@@ -0,0 +1,22 @@
+/*
+ * 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
+
+class CArchive;
+
+class IArchivable
+{
+protected:
+ /* make sure nobody deletes a pointer to this class */
+ virtual ~IArchivable() = default;
+
+public:
+ virtual void Archive(CArchive& ar) = 0;
+};
+
diff --git a/xbmc/utils/IBufferObject.h b/xbmc/utils/IBufferObject.h
new file mode 100644
index 0000000..4588aff
--- /dev/null
+++ b/xbmc/utils/IBufferObject.h
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2017-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 <stdint.h>
+#include <string>
+
+/**
+ * @brief Interface to describe CBufferObjects.
+ *
+ * BufferObjects are used to abstract various memory types and present them
+ * with a generic interface. Typically on a posix system exists the concept
+ * of Direct Memory Access (DMA) which allows various systems to interact
+ * with a memory location directly without having to copy the data into
+ * userspace (ie. from kernel space). These DMA buffers are presented as
+ * file descriptors (fds) which can be passed around to gain access to
+ * the buffers memory location.
+ *
+ * In order to write to these buffer types typically the memory location has
+ * to be mapped into userspace via a call to mmap (or similar). This presents
+ * userspace with a memory location that can be initially written to (ie. by
+ * using memcpy or similar). Depending on the underlying implementation a
+ * stride might be specified if the memory type requires padding to a certain
+ * size (such as page size). The stride must be used when copying into the buffer.
+ *
+ * Some memory implementation may provide special memory layouts in which case
+ * modifiers are provided that describe tiling or compression. This should be
+ * transparent to the caller as the data copied into a BufferObject will likely
+ * be linear. The modifier will be needed when presenting the buffer via DRM or
+ * EGL even if it is linear.
+ *
+ */
+class IBufferObject
+{
+public:
+ virtual ~IBufferObject() = default;
+
+ /**
+ * @brief Create a BufferObject based on the format, width, and height of the desired buffer
+ *
+ * @param format framebuffer pixel formats are described using the fourcc codes defined in
+ * https://github.com/torvalds/linux/blob/master/include/uapi/drm/drm_fourcc.h
+ * @param width width of the requested buffer.
+ * @param height height of the requested buffer.
+ * @return true BufferObject creation was successful.
+ * @return false BufferObject creation was unsuccessful.
+ */
+ virtual bool CreateBufferObject(uint32_t format, uint32_t width, uint32_t height) = 0;
+
+ /**
+ * @brief Create a BufferObject based only on the size of the desired buffer. Not all
+ * CBufferObject implementations may support this. This method is required for
+ * use with the CAddonVideoCodec as it only knows the decoded buffer size.
+ *
+ * @param size of the requested buffer.
+ * @return true BufferObject creation was successful.
+ * @return false BufferObject creation was unsuccessful.
+ */
+ virtual bool CreateBufferObject(uint64_t size) = 0;
+
+ /**
+ * @brief Destroy a BufferObject.
+ *
+ */
+ virtual void DestroyBufferObject() = 0;
+
+ /**
+ * @brief Get the Memory location of the BufferObject. This method needs to be
+ * called to be able to copy data into the BufferObject.
+ *
+ * @return uint8_t* pointer to the memory location of the BufferObject.
+ */
+ virtual uint8_t *GetMemory() = 0;
+
+ /**
+ * @brief Release the mapped memory of the BufferObject. After calling this the memory
+ * location pointed to by GetMemory() will be invalid.
+ *
+ */
+ virtual void ReleaseMemory() = 0;
+
+ /**
+ * @brief Get the File Descriptor of the BufferObject. The fd is guaranteed to be
+ * available after calling CreateBufferObject().
+ *
+ * @return int fd for the BufferObject. Invalid if -1.
+ */
+ virtual int GetFd() = 0;
+
+ /**
+ * @brief Get the Stride of the BufferObject. The stride is guaranteed to be
+ * available after calling GetMemory().
+ *
+ * @return uint32_t stride of the BufferObject.
+ */
+ virtual uint32_t GetStride() = 0;
+
+ /**
+ * @brief Get the Modifier of the BufferObject. Format Modifiers further describe
+ * the buffer's format such as for tiling or compression.
+ * see https://github.com/torvalds/linux/blob/master/include/uapi/drm/drm_fourcc.h
+ *
+ * @return uint64_t modifier of the BufferObject. 0 means the layout is linear (default).
+ */
+ virtual uint64_t GetModifier() = 0;
+
+ /**
+ * @brief Must be called before reading/writing data to the BufferObject.
+ *
+ */
+ virtual void SyncStart() = 0;
+
+ /**
+ * @brief Must be called after reading/writing data to the BufferObject.
+ *
+ */
+ virtual void SyncEnd() = 0;
+
+ /**
+ * @brief Get the Name of the BufferObject type in use
+ *
+ * @return std::string name of the BufferObject type in use
+ */
+ virtual std::string GetName() const = 0;
+};
diff --git a/xbmc/utils/ILocalizer.h b/xbmc/utils/ILocalizer.h
new file mode 100644
index 0000000..a81f7ce
--- /dev/null
+++ b/xbmc/utils/ILocalizer.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2017-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 <cstdint>
+#include <string>
+
+class ILocalizer
+{
+public:
+ virtual ~ILocalizer() = default;
+
+ virtual std::string Localize(std::uint32_t code) const = 0;
+
+protected:
+ ILocalizer() = default;
+};
diff --git a/xbmc/utils/IPlatformLog.h b/xbmc/utils/IPlatformLog.h
new file mode 100644
index 0000000..6ccf98d
--- /dev/null
+++ b/xbmc/utils/IPlatformLog.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 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 <memory>
+#include <mutex>
+#include <string>
+
+#ifdef TARGET_WINDOWS
+using spdlog_filename_t = std::wstring;
+#else
+using spdlog_filename_t = std::string;
+#endif
+
+namespace spdlog
+{
+namespace sinks
+{
+template<typename Mutex>
+class dist_sink;
+}
+} // namespace spdlog
+
+class IPlatformLog
+{
+public:
+ virtual ~IPlatformLog() = default;
+
+ static std::unique_ptr<IPlatformLog> CreatePlatformLog();
+
+ virtual spdlog_filename_t GetLogFilename(const std::string& filename) const = 0;
+ virtual void AddSinks(
+ std::shared_ptr<spdlog::sinks::dist_sink<std::mutex>> distributionSink) const = 0;
+};
diff --git a/xbmc/utils/IRssObserver.h b/xbmc/utils/IRssObserver.h
new file mode 100644
index 0000000..fae240c
--- /dev/null
+++ b/xbmc/utils/IRssObserver.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013-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 <vector>
+
+typedef uint32_t character_t;
+typedef std::vector<character_t> vecText;
+
+class IRssObserver
+{
+protected:
+ /* make sure nobody deletes a pointer to this class */
+ ~IRssObserver() = default;
+
+public:
+ virtual void OnFeedUpdate(const vecText &feed) = 0;
+ virtual void OnFeedRelease() = 0;
+};
diff --git a/xbmc/utils/IScreenshotSurface.h b/xbmc/utils/IScreenshotSurface.h
new file mode 100644
index 0000000..ba3fa6a
--- /dev/null
+++ b/xbmc/utils/IScreenshotSurface.h
@@ -0,0 +1,36 @@
+/*
+ * 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
+
+class IScreenshotSurface
+{
+public:
+ virtual ~IScreenshotSurface() = default;
+ virtual bool Capture() { return false; }
+ virtual void CaptureVideo(bool blendToBuffer) {}
+
+ int GetWidth() const { return m_width; }
+ int GetHeight() const { return m_height; }
+ int GetStride() const { return m_stride; }
+ unsigned char* GetBuffer() const { return m_buffer; }
+ void ReleaseBuffer()
+ {
+ if (m_buffer)
+ {
+ delete m_buffer;
+ m_buffer = nullptr;
+ }
+ };
+
+protected:
+ int m_width{0};
+ int m_height{0};
+ int m_stride{0};
+ unsigned char* m_buffer{nullptr};
+};
diff --git a/xbmc/utils/ISerializable.h b/xbmc/utils/ISerializable.h
new file mode 100644
index 0000000..12f0fba
--- /dev/null
+++ b/xbmc/utils/ISerializable.h
@@ -0,0 +1,21 @@
+/*
+ * 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
+
+class CVariant;
+
+class ISerializable
+{
+protected:
+ /* make sure nobody deletes a pointer to this class */
+ ~ISerializable() = default;
+
+ public:
+ virtual void Serialize(CVariant& value) const = 0;
+};
diff --git a/xbmc/utils/ISortable.h b/xbmc/utils/ISortable.h
new file mode 100644
index 0000000..ea4a0d3
--- /dev/null
+++ b/xbmc/utils/ISortable.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "SortUtils.h"
+
+#include <map>
+
+class ISortable
+{
+protected:
+ /* make sure nobody deletes a pointer to this class */
+ ~ISortable() = default;
+
+public:
+ virtual void ToSortable(SortItem& sortable, Field field) const = 0;
+};
diff --git a/xbmc/utils/IXmlDeserializable.h b/xbmc/utils/IXmlDeserializable.h
new file mode 100644
index 0000000..edeec25
--- /dev/null
+++ b/xbmc/utils/IXmlDeserializable.h
@@ -0,0 +1,19 @@
+/*
+ * 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
+
+class TiXmlNode;
+
+class IXmlDeserializable
+{
+public:
+ virtual ~IXmlDeserializable() = default;
+
+ virtual bool Deserialize(const TiXmlNode *node) = 0;
+};
diff --git a/xbmc/utils/InfoLoader.cpp b/xbmc/utils/InfoLoader.cpp
new file mode 100644
index 0000000..be4697c
--- /dev/null
+++ b/xbmc/utils/InfoLoader.cpp
@@ -0,0 +1,60 @@
+/*
+ * 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 "InfoLoader.h"
+
+#include "JobManager.h"
+#include "ServiceBroker.h"
+#include "TimeUtils.h"
+#include "guilib/LocalizeStrings.h"
+
+CInfoLoader::CInfoLoader(unsigned int timeToRefresh)
+{
+ m_refreshTime = 0;
+ m_timeToRefresh = timeToRefresh;
+ m_busy = false;
+}
+
+CInfoLoader::~CInfoLoader() = default;
+
+void CInfoLoader::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ m_refreshTime = CTimeUtils::GetFrameTime() + m_timeToRefresh;
+ m_busy = false;
+}
+
+std::string CInfoLoader::GetInfo(int info)
+{
+ // Refresh if need be
+ if (m_refreshTime < CTimeUtils::GetFrameTime() && !m_busy)
+ { // queue up the job
+ m_busy = true;
+ CServiceBroker::GetJobManager()->AddJob(GetJob(), this);
+ }
+ if (m_busy && CTimeUtils::GetFrameTime() - m_refreshTime > 1000)
+ {
+ return BusyInfo(info);
+ }
+ return TranslateInfo(info);
+}
+
+std::string CInfoLoader::BusyInfo(int info) const
+{
+ return g_localizeStrings.Get(503);
+}
+
+std::string CInfoLoader::TranslateInfo(int info) const
+{
+ return "";
+}
+
+void CInfoLoader::Refresh()
+{
+ m_refreshTime = CTimeUtils::GetFrameTime();
+}
+
diff --git a/xbmc/utils/InfoLoader.h b/xbmc/utils/InfoLoader.h
new file mode 100644
index 0000000..720f0d7
--- /dev/null
+++ b/xbmc/utils/InfoLoader.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "Job.h"
+
+#include <string>
+
+class CInfoLoader : public IJobCallback
+{
+public:
+ explicit CInfoLoader(unsigned int timeToRefresh = 5 * 60 * 1000);
+ ~CInfoLoader() override;
+
+ std::string GetInfo(int info);
+ void Refresh();
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+protected:
+ virtual CJob *GetJob() const=0;
+ virtual std::string TranslateInfo(int info) const;
+ virtual std::string BusyInfo(int info) const;
+private:
+ unsigned int m_refreshTime;
+ unsigned int m_timeToRefresh;
+ bool m_busy;
+};
diff --git a/xbmc/utils/JSONVariantParser.cpp b/xbmc/utils/JSONVariantParser.cpp
new file mode 100644
index 0000000..493abfd
--- /dev/null
+++ b/xbmc/utils/JSONVariantParser.cpp
@@ -0,0 +1,219 @@
+/*
+ * 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 "JSONVariantParser.h"
+
+#include <rapidjson/reader.h>
+
+class CJSONVariantParserHandler
+{
+public:
+ explicit CJSONVariantParserHandler(CVariant& parsedObject);
+
+ bool Null();
+ bool Bool(bool b);
+ bool Int(int i);
+ bool Uint(unsigned u);
+ bool Int64(int64_t i);
+ bool Uint64(uint64_t u);
+ bool Double(double d);
+ bool RawNumber(const char* str, rapidjson::SizeType length, bool copy);
+ bool String(const char* str, rapidjson::SizeType length, bool copy);
+ bool StartObject();
+ bool Key(const char* str, rapidjson::SizeType length, bool copy);
+ bool EndObject(rapidjson::SizeType memberCount);
+ bool StartArray();
+ bool EndArray(rapidjson::SizeType elementCount);
+
+private:
+ template <typename... TArgs>
+ bool Primitive(TArgs... args)
+ {
+ PushObject(CVariant(std::forward<TArgs>(args)...));
+ PopObject();
+
+ return true;
+ }
+
+ void PushObject(const CVariant& variant);
+ void PopObject();
+
+ CVariant& m_parsedObject;
+ std::vector<CVariant *> m_parse;
+ std::string m_key;
+ CVariant m_root;
+
+ enum class PARSE_STATUS
+ {
+ Variable,
+ Array,
+ Object
+ };
+ PARSE_STATUS m_status;
+};
+
+CJSONVariantParserHandler::CJSONVariantParserHandler(CVariant& parsedObject)
+ : m_parsedObject(parsedObject),
+ m_parse(),
+ m_key(),
+ m_status(PARSE_STATUS::Variable)
+{ }
+
+bool CJSONVariantParserHandler::Null()
+{
+ PushObject(CVariant::ConstNullVariant);
+ PopObject();
+
+ return true;
+}
+
+bool CJSONVariantParserHandler::Bool(bool b)
+{
+ return Primitive(b);
+}
+
+bool CJSONVariantParserHandler::Int(int i)
+{
+ return Primitive(i);
+}
+
+bool CJSONVariantParserHandler::Uint(unsigned u)
+{
+ return Primitive(u);
+}
+
+bool CJSONVariantParserHandler::Int64(int64_t i)
+{
+ return Primitive(i);
+}
+
+bool CJSONVariantParserHandler::Uint64(uint64_t u)
+{
+ return Primitive(u);
+}
+
+bool CJSONVariantParserHandler::Double(double d)
+{
+ return Primitive(d);
+}
+
+bool CJSONVariantParserHandler::RawNumber(const char* str, rapidjson::SizeType length, bool copy)
+{
+ return Primitive(str, length);
+}
+
+bool CJSONVariantParserHandler::String(const char* str, rapidjson::SizeType length, bool copy)
+{
+ return Primitive(str, length);
+}
+
+bool CJSONVariantParserHandler::StartObject()
+{
+ PushObject(CVariant::VariantTypeObject);
+
+ return true;
+}
+
+bool CJSONVariantParserHandler::Key(const char* str, rapidjson::SizeType length, bool copy)
+{
+ m_key = std::string(str, 0, length);
+
+ return true;
+}
+
+bool CJSONVariantParserHandler::EndObject(rapidjson::SizeType memberCount)
+{
+ PopObject();
+
+ return true;
+}
+
+bool CJSONVariantParserHandler::StartArray()
+{
+ PushObject(CVariant::VariantTypeArray);
+
+ return true;
+}
+
+bool CJSONVariantParserHandler::EndArray(rapidjson::SizeType elementCount)
+{
+ PopObject();
+
+ return true;
+}
+
+void CJSONVariantParserHandler::PushObject(const CVariant& variant)
+{
+ if (m_status == PARSE_STATUS::Object)
+ {
+ (*m_parse[m_parse.size() - 1])[m_key] = variant;
+ m_parse.push_back(&(*m_parse[m_parse.size() - 1])[m_key]);
+ }
+ else if (m_status == PARSE_STATUS::Array)
+ {
+ CVariant *temp = m_parse[m_parse.size() - 1];
+ temp->push_back(variant);
+ m_parse.push_back(&(*temp)[temp->size() - 1]);
+ }
+ else if (m_parse.empty())
+ {
+ m_root = variant;
+ m_parse.push_back(&m_root);
+ }
+
+ if (variant.isObject())
+ m_status = PARSE_STATUS::Object;
+ else if (variant.isArray())
+ m_status = PARSE_STATUS::Array;
+ else
+ m_status = PARSE_STATUS::Variable;
+}
+
+void CJSONVariantParserHandler::PopObject()
+{
+ CVariant *variant = m_parse[m_parse.size() - 1];
+ m_parse.pop_back();
+
+ if (!m_parse.empty())
+ {
+ variant = m_parse[m_parse.size() - 1];
+ if (variant->isObject())
+ m_status = PARSE_STATUS::Object;
+ else if (variant->isArray())
+ m_status = PARSE_STATUS::Array;
+ else
+ m_status = PARSE_STATUS::Variable;
+ }
+ else
+ {
+ m_parsedObject = *variant;
+ m_status = PARSE_STATUS::Variable;
+ }
+}
+
+bool CJSONVariantParser::Parse(const char* json, CVariant& data)
+{
+ if (json == nullptr)
+ return false;
+
+ rapidjson::Reader reader;
+ rapidjson::StringStream stringStream(json);
+
+ CJSONVariantParserHandler handler(data);
+ // use kParseIterativeFlag to eliminate possible stack overflow
+ // from json parsing via reentrant calls
+ if (reader.Parse<rapidjson::kParseIterativeFlag>(stringStream, handler))
+ return true;
+
+ return false;
+}
+
+bool CJSONVariantParser::Parse(const std::string& json, CVariant& data)
+{
+ return Parse(json.c_str(), data);
+}
diff --git a/xbmc/utils/JSONVariantParser.h b/xbmc/utils/JSONVariantParser.h
new file mode 100644
index 0000000..17cfb61
--- /dev/null
+++ b/xbmc/utils/JSONVariantParser.h
@@ -0,0 +1,22 @@
+/*
+ * 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 "utils/Variant.h"
+
+#include <string>
+
+class CJSONVariantParser
+{
+public:
+ CJSONVariantParser() = delete;
+
+ static bool Parse(const char* json, CVariant& data);
+ static bool Parse(const std::string& json, CVariant& data);
+};
diff --git a/xbmc/utils/JSONVariantWriter.cpp b/xbmc/utils/JSONVariantWriter.cpp
new file mode 100644
index 0000000..b48a8ae
--- /dev/null
+++ b/xbmc/utils/JSONVariantWriter.cpp
@@ -0,0 +1,92 @@
+/*
+ * 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 "JSONVariantWriter.h"
+
+#include "utils/Variant.h"
+
+#include <rapidjson/prettywriter.h>
+#include <rapidjson/stringbuffer.h>
+#include <rapidjson/writer.h>
+
+template<class TWriter>
+bool InternalWrite(TWriter& writer, const CVariant &value)
+{
+ switch (value.type())
+ {
+ case CVariant::VariantTypeInteger:
+ return writer.Int64(value.asInteger());
+
+ case CVariant::VariantTypeUnsignedInteger:
+ return writer.Uint64(value.asUnsignedInteger());
+
+ case CVariant::VariantTypeDouble:
+ return writer.Double(value.asDouble());
+
+ case CVariant::VariantTypeBoolean:
+ return writer.Bool(value.asBoolean());
+
+ case CVariant::VariantTypeString:
+ return writer.String(value.c_str(), value.size());
+
+ case CVariant::VariantTypeArray:
+ if (!writer.StartArray())
+ return false;
+
+ for (CVariant::const_iterator_array itr = value.begin_array(); itr != value.end_array(); ++itr)
+ {
+ if (!InternalWrite(writer, *itr))
+ return false;
+ }
+
+ return writer.EndArray(value.size());
+
+ case CVariant::VariantTypeObject:
+ if (!writer.StartObject())
+ return false;
+
+ for (CVariant::const_iterator_map itr = value.begin_map(); itr != value.end_map(); ++itr)
+ {
+ if (!writer.Key(itr->first.c_str()) ||
+ !InternalWrite(writer, itr->second))
+ return false;
+ }
+
+ return writer.EndObject(value.size());
+
+ case CVariant::VariantTypeConstNull:
+ case CVariant::VariantTypeNull:
+ default:
+ return writer.Null();
+ }
+
+ return false;
+}
+
+bool CJSONVariantWriter::Write(const CVariant &value, std::string& output, bool compact)
+{
+ rapidjson::StringBuffer stringBuffer;
+ if (compact)
+ {
+ rapidjson::Writer<rapidjson::StringBuffer> writer(stringBuffer);
+
+ if (!InternalWrite(writer, value) || !writer.IsComplete())
+ return false;
+ }
+ else
+ {
+ rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(stringBuffer);
+ writer.SetIndent('\t', 1);
+
+ if (!InternalWrite(writer, value) || !writer.IsComplete())
+ return false;
+ }
+
+ output = stringBuffer.GetString();
+ return true;
+}
diff --git a/xbmc/utils/JSONVariantWriter.h b/xbmc/utils/JSONVariantWriter.h
new file mode 100644
index 0000000..e1f5bd6
--- /dev/null
+++ b/xbmc/utils/JSONVariantWriter.h
@@ -0,0 +1,21 @@
+/*
+ * 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 <string>
+
+class CVariant;
+
+class CJSONVariantWriter
+{
+public:
+ CJSONVariantWriter() = delete;
+
+ static bool Write(const CVariant &value, std::string& output, bool compact);
+};
diff --git a/xbmc/utils/Job.h b/xbmc/utils/Job.h
new file mode 100644
index 0000000..b4d3f7f
--- /dev/null
+++ b/xbmc/utils/Job.h
@@ -0,0 +1,179 @@
+/*
+ * 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
+
+class CJob;
+
+#include <stddef.h>
+
+#define kJobTypeMediaFlags "mediaflags"
+#define kJobTypeCacheImage "cacheimage"
+#define kJobTypeDDSCompress "ddscompress"
+
+/*!
+ \ingroup jobs
+ \brief Callback interface for asynchronous jobs.
+
+ Used by clients of the CJobManager to receive progress, abort and completion notification of jobs.
+ Clients of small jobs wishing to perform actions on job completion or abort should implement the
+ IJobCallback::OnJobComplete() and/or IJobCallback::OnJobAbort() function. Clients of larger jobs
+ may choose to implement the IJobCallback::OnJobProgress() function in order to be kept informed of
+ progress.
+
+ \sa CJobManager and CJob
+ */
+class IJobCallback
+{
+public:
+ /*!
+ \brief Destructor for job call back objects.
+
+ \sa CJobManager and CJob
+ */
+ virtual ~IJobCallback() = default;
+
+ /*!
+ \brief The callback used when a job completes.
+
+ OnJobComplete is called at the completion of the job's DoWork() function, and is used
+ to return information to the caller on the result of the job. On returning form this function
+ the CJobManager will destroy this job.
+
+ \param jobID the unique id of the job (as retrieved from CJobManager::AddJob)
+ \param success the result from the DoWork call
+ \param job the job that has been processed. The job will be destroyed after this function returns
+ \sa CJobManager and CJob
+ */
+ virtual void OnJobComplete(unsigned int jobID, bool success, CJob *job)=0;
+
+ /*!
+ \brief An optional callback function used when a job will be aborted.
+
+ OnJobAbort is called whenever a job gets aborted before or while being executed.
+ Job's DoWork method will not be called, OnJobComplete will not be called. The job instance will
+ be destroyed by the caller after calling this function.
+
+ \param jobID the unique id of the job (as retrieved from CJobManager::AddJob)
+ \param job the job that has been aborted.
+ \sa CJobManager and CJob
+ */
+ virtual void OnJobAbort(unsigned int jobID, CJob* job) {}
+
+ /*!
+ \brief An optional callback function that a job may call while processing.
+
+ OnJobProgress may be called periodically by a job during it's DoWork() function. It is used
+ by the job to report on progress.
+
+ \param jobID the unique id of the job (as retrieved from CJobManager::AddJob)
+ \param progress the current progress of the job, out of total.
+ \param total the total amount of work to be processed.
+ \param job the job that has been processed.
+ \sa CJobManager and CJob
+ */
+ virtual void OnJobProgress(unsigned int jobID,
+ unsigned int progress,
+ unsigned int total,
+ const CJob* job)
+ {
+ }
+};
+
+class CJobManager;
+
+/*!
+ \ingroup jobs
+ \brief Base class for jobs that are executed asynchronously.
+
+ Clients of the CJobManager should subclass CJob and provide the DoWork() function. Data should be
+ passed to the job on creation, and any data sharing between the job and the client should be kept to within
+ the callback functions if possible, and guarded with critical sections as appropriate.
+
+ Jobs typically fall into two groups: small jobs that perform a single function, and larger jobs that perform a
+ sequence of functions. Clients with small jobs should implement the IJobCallback::OnJobComplete() callback to receive results.
+ Clients with larger jobs may wish to implement both the IJobCallback::OnJobComplete() and IJobCallback::OnJobProgress()
+ callbacks to receive updates. Jobs may be cancelled at any point by the client via CJobManager::CancelJob(), however
+ effort should be taken to ensure that any callbacks and cancellation is suitably guarded against simultaneous thread access.
+
+ Handling cancellation of jobs within the OnJobProgress callback is a threadsafe operation, as all execution is
+ then in the Job thread.
+
+ \sa CJobManager and IJobCallback
+ */
+class CJob
+{
+public:
+ /*!
+ \brief Priority levels for jobs, specified by clients when adding jobs to the CJobManager.
+ \sa CJobManager
+ */
+ enum PRIORITY {
+ PRIORITY_LOW_PAUSABLE = 0,
+ PRIORITY_LOW,
+ PRIORITY_NORMAL,
+ PRIORITY_HIGH,
+ PRIORITY_DEDICATED, // will create a new worker if no worker is available at queue time
+ };
+ CJob() { m_callback = NULL; }
+
+ /*!
+ \brief Destructor for job objects.
+
+ Jobs are destroyed by the CJobManager after the OnJobComplete() or OnJobAbort() callback is
+ complete. CJob subclasses should therefore supply a virtual destructor to cleanup any memory
+ allocated by complete or cancelled jobs.
+
+ \sa CJobManager
+ */
+ virtual ~CJob() = default;
+
+ /*!
+ \brief Main workhorse function of CJob instances
+
+ All CJob subclasses must implement this function, performing all processing. Once this function
+ is complete, the OnJobComplete() callback is called, and the job is then destroyed.
+
+ \sa CJobManager, IJobCallback::OnJobComplete()
+ */
+ virtual bool DoWork() = 0; // function to do the work
+
+ /*!
+ \brief Function that returns the type of job.
+
+ CJob subclasses may optionally implement this function to specify the type of job.
+ This is useful for the CJobManager::AddLIFOJob() routine, which preempts similar jobs
+ with the new job.
+
+ \return a unique character string describing the job.
+ \sa CJobManager
+ */
+ virtual const char* GetType() const { return ""; }
+
+ virtual bool operator==(const CJob* job) const
+ {
+ return false;
+ }
+
+ /*!
+ \brief Function for longer jobs to report progress and check whether they have been cancelled.
+
+ Jobs that contain loops that may take time should check this routine each iteration of the loop,
+ both to (optionally) report progress, and to check for cancellation.
+
+ \param progress the amount of the job performed, out of total.
+ \param total the total amount of processing to be performed
+ \return if true, the job has been asked to cancel.
+
+ \sa IJobCallback::OnJobProgress()
+ */
+ virtual bool ShouldCancel(unsigned int progress, unsigned int total) const;
+private:
+ friend class CJobManager;
+ CJobManager *m_callback;
+};
diff --git a/xbmc/utils/JobManager.cpp b/xbmc/utils/JobManager.cpp
new file mode 100644
index 0000000..a1ca34b
--- /dev/null
+++ b/xbmc/utils/JobManager.cpp
@@ -0,0 +1,440 @@
+/*
+ * 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 "JobManager.h"
+
+#include "ServiceBroker.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <functional>
+#include <mutex>
+#include <stdexcept>
+
+using namespace std::chrono_literals;
+
+bool CJob::ShouldCancel(unsigned int progress, unsigned int total) const
+{
+ if (m_callback)
+ return m_callback->OnJobProgress(progress, total, this);
+ return false;
+}
+
+CJobWorker::CJobWorker(CJobManager *manager) : CThread("JobWorker")
+{
+ m_jobManager = manager;
+ Create(true); // start work immediately, and kill ourselves when we're done
+}
+
+CJobWorker::~CJobWorker()
+{
+ m_jobManager->RemoveWorker(this);
+ if(!IsAutoDelete())
+ StopThread();
+}
+
+void CJobWorker::Process()
+{
+ SetPriority(ThreadPriority::LOWEST);
+ while (true)
+ {
+ // request an item from our manager (this call is blocking)
+ CJob* job = m_jobManager->GetNextJob();
+ if (!job)
+ break;
+
+ bool success = false;
+ try
+ {
+ success = job->DoWork();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} error processing job {}", __FUNCTION__, job->GetType());
+ }
+ m_jobManager->OnJobComplete(success, job);
+ }
+}
+
+void CJobQueue::CJobPointer::CancelJob()
+{
+ CServiceBroker::GetJobManager()->CancelJob(m_id);
+ m_id = 0;
+}
+
+CJobQueue::CJobQueue(bool lifo, unsigned int jobsAtOnce, CJob::PRIORITY priority)
+: m_jobsAtOnce(jobsAtOnce), m_priority(priority), m_lifo(lifo)
+{
+}
+
+CJobQueue::~CJobQueue()
+{
+ CancelJobs();
+}
+
+void CJobQueue::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ OnJobNotify(job);
+}
+
+void CJobQueue::OnJobAbort(unsigned int jobID, CJob* job)
+{
+ OnJobNotify(job);
+}
+
+void CJobQueue::CancelJob(const CJob *job)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ Processing::iterator i = find(m_processing.begin(), m_processing.end(), job);
+ if (i != m_processing.end())
+ {
+ i->CancelJob();
+ m_processing.erase(i);
+ return;
+ }
+ Queue::iterator j = find(m_jobQueue.begin(), m_jobQueue.end(), job);
+ if (j != m_jobQueue.end())
+ {
+ j->FreeJob();
+ m_jobQueue.erase(j);
+ }
+}
+
+bool CJobQueue::AddJob(CJob *job)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ // check if we have this job already. If so, we're done.
+ if (find(m_jobQueue.begin(), m_jobQueue.end(), job) != m_jobQueue.end() ||
+ find(m_processing.begin(), m_processing.end(), job) != m_processing.end())
+ {
+ delete job;
+ return false;
+ }
+
+ if (m_lifo)
+ m_jobQueue.push_back(CJobPointer(job));
+ else
+ m_jobQueue.push_front(CJobPointer(job));
+ QueueNextJob();
+
+ return true;
+}
+
+void CJobQueue::OnJobNotify(CJob* job)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ // check if this job is in our processing list
+ const auto it = std::find(m_processing.begin(), m_processing.end(), job);
+ if (it != m_processing.end())
+ m_processing.erase(it);
+ // request a new job be queued
+ QueueNextJob();
+}
+
+void CJobQueue::QueueNextJob()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ while (m_jobQueue.size() && m_processing.size() < m_jobsAtOnce)
+ {
+ CJobPointer &job = m_jobQueue.back();
+ job.m_id = CServiceBroker::GetJobManager()->AddJob(job.m_job, this, m_priority);
+ if (job.m_id > 0)
+ {
+ m_processing.emplace_back(job);
+ m_jobQueue.pop_back();
+ return;
+ }
+ m_jobQueue.pop_back();
+ }
+}
+
+void CJobQueue::CancelJobs()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for_each(m_processing.begin(), m_processing.end(), [](CJobPointer& jp) { jp.CancelJob(); });
+ for_each(m_jobQueue.begin(), m_jobQueue.end(), [](CJobPointer& jp) { jp.FreeJob(); });
+ m_jobQueue.clear();
+ m_processing.clear();
+}
+
+bool CJobQueue::IsProcessing() const
+{
+ return CServiceBroker::GetJobManager()->m_running &&
+ (!m_processing.empty() || !m_jobQueue.empty());
+}
+
+bool CJobQueue::QueueEmpty() const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ return m_jobQueue.empty();
+}
+
+CJobManager::CJobManager()
+{
+ m_jobCounter = 0;
+ m_running = true;
+ m_pauseJobs = false;
+}
+
+void CJobManager::Restart()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (m_running)
+ throw std::logic_error("CJobManager already running");
+ m_running = true;
+}
+
+void CJobManager::CancelJobs()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_running = false;
+
+ // clear any pending jobs
+ for (unsigned int priority = CJob::PRIORITY_LOW_PAUSABLE; priority <= CJob::PRIORITY_DEDICATED; ++priority)
+ {
+ std::for_each(m_jobQueue[priority].begin(), m_jobQueue[priority].end(), [](CWorkItem& wi) {
+ if (wi.m_callback)
+ wi.m_callback->OnJobAbort(wi.m_id, wi.m_job);
+ wi.FreeJob();
+ });
+ m_jobQueue[priority].clear();
+ }
+
+ // cancel any callbacks on jobs still processing
+ std::for_each(m_processing.begin(), m_processing.end(), [](CWorkItem& wi) {
+ if (wi.m_callback)
+ wi.m_callback->OnJobAbort(wi.m_id, wi.m_job);
+ wi.Cancel();
+ });
+
+ // tell our workers to finish
+ while (m_workers.size())
+ {
+ lock.unlock();
+ m_jobEvent.Set();
+ std::this_thread::yield(); // yield after setting the event to give the workers some time to die
+ lock.lock();
+ }
+}
+
+unsigned int CJobManager::AddJob(CJob *job, IJobCallback *callback, CJob::PRIORITY priority)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (!m_running)
+ {
+ delete job;
+ return 0;
+ }
+
+ // increment the job counter, ensuring 0 (invalid job) is never hit
+ m_jobCounter++;
+ if (m_jobCounter == 0)
+ m_jobCounter++;
+
+ // create a work item for this job
+ CWorkItem work(job, m_jobCounter, priority, callback);
+ m_jobQueue[priority].push_back(work);
+
+ StartWorkers(priority);
+ return work.m_id;
+}
+
+void CJobManager::CancelJob(unsigned int jobID)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ // check whether we have this job in the queue
+ for (unsigned int priority = CJob::PRIORITY_LOW_PAUSABLE; priority <= CJob::PRIORITY_DEDICATED; ++priority)
+ {
+ JobQueue::iterator i = find(m_jobQueue[priority].begin(), m_jobQueue[priority].end(), jobID);
+ if (i != m_jobQueue[priority].end())
+ {
+ delete i->m_job;
+ m_jobQueue[priority].erase(i);
+ return;
+ }
+ }
+ // or if we're processing it
+ Processing::iterator it = find(m_processing.begin(), m_processing.end(), jobID);
+ if (it != m_processing.end())
+ it->m_callback = NULL; // job is in progress, so only thing to do is to remove callback
+}
+
+void CJobManager::StartWorkers(CJob::PRIORITY priority)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ // check how many free threads we have
+ if (m_processing.size() >= GetMaxWorkers(priority))
+ return;
+
+ // do we have any sleeping threads?
+ if (m_processing.size() < m_workers.size())
+ {
+ m_jobEvent.Set();
+ return;
+ }
+
+ // everyone is busy - we need more workers
+ m_workers.push_back(new CJobWorker(this));
+}
+
+CJob *CJobManager::PopJob()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ for (int priority = CJob::PRIORITY_DEDICATED; priority >= CJob::PRIORITY_LOW_PAUSABLE; --priority)
+ {
+ // Check whether we're pausing pausable jobs
+ if (priority == CJob::PRIORITY_LOW_PAUSABLE && m_pauseJobs)
+ continue;
+
+ if (m_jobQueue[priority].size() && m_processing.size() < GetMaxWorkers(CJob::PRIORITY(priority)))
+ {
+ // pop the job off the queue
+ CWorkItem job = m_jobQueue[priority].front();
+ m_jobQueue[priority].pop_front();
+
+ // add to the processing vector
+ m_processing.push_back(job);
+ job.m_job->m_callback = this;
+ return job.m_job;
+ }
+ }
+ return NULL;
+}
+
+void CJobManager::PauseJobs()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_pauseJobs = true;
+}
+
+void CJobManager::UnPauseJobs()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ m_pauseJobs = false;
+}
+
+bool CJobManager::IsProcessing(const CJob::PRIORITY &priority) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (m_pauseJobs)
+ return false;
+
+ for(Processing::const_iterator it = m_processing.begin(); it < m_processing.end(); ++it)
+ {
+ if (priority == it->m_priority)
+ return true;
+ }
+ return false;
+}
+
+int CJobManager::IsProcessing(const std::string &type) const
+{
+ int jobsMatched = 0;
+ std::unique_lock<CCriticalSection> lock(m_section);
+
+ if (m_pauseJobs)
+ return 0;
+
+ for(Processing::const_iterator it = m_processing.begin(); it < m_processing.end(); ++it)
+ {
+ if (type == std::string(it->m_job->GetType()))
+ jobsMatched++;
+ }
+ return jobsMatched;
+}
+
+CJob* CJobManager::GetNextJob()
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ while (m_running)
+ {
+ // grab a job off the queue if we have one
+ CJob *job = PopJob();
+ if (job)
+ return job;
+ // no jobs are left - sleep for 30 seconds to allow new jobs to come in
+ lock.unlock();
+ bool newJob = m_jobEvent.Wait(30000ms);
+ lock.lock();
+ if (!newJob)
+ break;
+ }
+ // ensure no jobs have come in during the period after
+ // timeout and before we held the lock
+ return PopJob();
+}
+
+bool CJobManager::OnJobProgress(unsigned int progress, unsigned int total, const CJob *job) const
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ // find the job in the processing queue, and check whether it's cancelled (no callback)
+ Processing::const_iterator i = find(m_processing.begin(), m_processing.end(), job);
+ if (i != m_processing.end())
+ {
+ CWorkItem item(*i);
+ lock.unlock(); // leave section prior to call
+ if (item.m_callback)
+ {
+ item.m_callback->OnJobProgress(item.m_id, progress, total, job);
+ return false;
+ }
+ }
+ return true; // couldn't find the job, or it's been cancelled
+}
+
+void CJobManager::OnJobComplete(bool success, CJob *job)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ // remove the job from the processing queue
+ Processing::iterator i = find(m_processing.begin(), m_processing.end(), job);
+ if (i != m_processing.end())
+ {
+ // tell any listeners we're done with the job, then delete it
+ CWorkItem item(*i);
+ lock.unlock();
+ try
+ {
+ if (item.m_callback)
+ item.m_callback->OnJobComplete(item.m_id, success, item.m_job);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} error processing job {}", __FUNCTION__, item.m_job->GetType());
+ }
+ lock.lock();
+ Processing::iterator j = find(m_processing.begin(), m_processing.end(), job);
+ if (j != m_processing.end())
+ m_processing.erase(j);
+ lock.unlock();
+ item.FreeJob();
+ }
+}
+
+void CJobManager::RemoveWorker(const CJobWorker *worker)
+{
+ std::unique_lock<CCriticalSection> lock(m_section);
+ // remove our worker
+ Workers::iterator i = find(m_workers.begin(), m_workers.end(), worker);
+ if (i != m_workers.end())
+ m_workers.erase(i); // workers auto-delete
+}
+
+unsigned int CJobManager::GetMaxWorkers(CJob::PRIORITY priority)
+{
+ static const unsigned int max_workers = 5;
+ if (priority == CJob::PRIORITY_DEDICATED)
+ return 10000; // A large number..
+ return max_workers - (CJob::PRIORITY_HIGH - priority);
+}
diff --git a/xbmc/utils/JobManager.h b/xbmc/utils/JobManager.h
new file mode 100644
index 0000000..545e5a1
--- /dev/null
+++ b/xbmc/utils/JobManager.h
@@ -0,0 +1,381 @@
+/*
+ * 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 "Job.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+
+#include <queue>
+#include <string>
+#include <vector>
+
+class CJobManager;
+
+class CJobWorker : public CThread
+{
+public:
+ explicit CJobWorker(CJobManager *manager);
+ ~CJobWorker() override;
+
+ void Process() override;
+private:
+ CJobManager *m_jobManager;
+};
+
+template<typename F>
+class CLambdaJob : public CJob
+{
+public:
+ CLambdaJob(F&& f) : m_f(std::forward<F>(f)) {}
+ bool DoWork() override
+ {
+ m_f();
+ return true;
+ }
+ bool operator==(const CJob *job) const override
+ {
+ return this == job;
+ };
+private:
+ F m_f;
+};
+
+/*!
+ \ingroup jobs
+ \brief Job Queue class to handle a queue of unique jobs to be processed sequentially
+
+ Holds a queue of jobs to be processed sequentially, either first in,first out
+ or last in, first out. Jobs are unique, so queueing multiple copies of the same job
+ (based on the CJob::operator==) will not add additional jobs.
+
+ Classes should subclass this class and override OnJobCallback should they require
+ information from the job.
+
+ \sa CJob and IJobCallback
+ */
+class CJobQueue: public IJobCallback
+{
+ class CJobPointer
+ {
+ public:
+ explicit CJobPointer(CJob *job)
+ {
+ m_job = job;
+ m_id = 0;
+ };
+ void CancelJob();
+ void FreeJob()
+ {
+ delete m_job;
+ m_job = NULL;
+ };
+ bool operator==(const CJob *job) const
+ {
+ if (m_job)
+ return *m_job == job;
+ return false;
+ };
+ CJob *m_job;
+ unsigned int m_id;
+ };
+public:
+ /*!
+ \brief CJobQueue constructor
+ \param lifo whether the queue should be processed last in first out or first in first out. Defaults to false (first in first out)
+ \param jobsAtOnce number of jobs at once to process. Defaults to 1.
+ \param priority priority of this queue.
+ \sa CJob
+ */
+ CJobQueue(bool lifo = false, unsigned int jobsAtOnce = 1, CJob::PRIORITY priority = CJob::PRIORITY_LOW);
+
+ /*!
+ \brief CJobQueue destructor
+ Cancels any in-process jobs, and destroys the job queue.
+ \sa CJob
+ */
+ ~CJobQueue() override;
+
+ /*!
+ \brief Add a job to the queue
+ On completion of the job, destruction of the job queue or in case the job could not be added successfully, the CJob object will be destroyed.
+ \param job a pointer to the job to add. The job should be subclassed from CJob.
+ \return True if the job was added successfully, false otherwise.
+ In case of failure, the passed CJob object will be deleted before returning from this method.
+ \sa CJob
+ */
+ bool AddJob(CJob *job);
+
+ /*!
+ \brief Add a function f to this job queue
+ */
+ template<typename F>
+ void Submit(F&& f)
+ {
+ AddJob(new CLambdaJob<F>(std::forward<F>(f)));
+ }
+
+ /*!
+ \brief Cancel a job in the queue
+ Cancels a job in the queue. Any job currently being processed may complete after this
+ call has completed, but OnJobComplete will not be performed. If the job is only queued
+ then it will be removed from the queue and deleted.
+ \param job a pointer to the job to cancel. The job should be subclassed from CJob.
+ \sa CJob
+ */
+ void CancelJob(const CJob *job);
+
+ /*!
+ \brief Cancel all jobs in the queue
+ Removes all jobs from the queue. Any job currently being processed may complete after this
+ call has completed, but OnJobComplete will not be performed.
+ \sa CJob
+ */
+ void CancelJobs();
+
+ /*!
+ \brief Check whether the queue is processing a job
+ */
+ bool IsProcessing() const;
+
+ /*!
+ \brief The callback used when a job completes.
+
+ CJobQueue implementation will cleanup the internal processing queue and then queue the next
+ job at the job manager, if any.
+
+ \param jobID the unique id of the job (as retrieved from CJobManager::AddJob)
+ \param success the result from the DoWork call
+ \param job the job that has been processed.
+ \sa CJobManager, IJobCallback and CJob
+ */
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ /*!
+ \brief The callback used when a job will be aborted.
+
+ CJobQueue implementation will cleanup the internal processing queue and then queue the next
+ job at the job manager, if any.
+
+ \param jobID the unique id of the job (as retrieved from CJobManager::AddJob)
+ \param job the job that has been aborted.
+ \sa CJobManager, IJobCallback and CJob
+ */
+ void OnJobAbort(unsigned int jobID, CJob* job) override;
+
+protected:
+ /*!
+ \brief Returns if we still have jobs waiting to be processed
+ NOTE: This function does not take into account the jobs that are currently processing
+ */
+ bool QueueEmpty() const;
+
+private:
+ void OnJobNotify(CJob* job);
+ void QueueNextJob();
+
+ typedef std::deque<CJobPointer> Queue;
+ typedef std::vector<CJobPointer> Processing;
+ Queue m_jobQueue;
+ Processing m_processing;
+
+ unsigned int m_jobsAtOnce;
+ CJob::PRIORITY m_priority;
+ mutable CCriticalSection m_section;
+ bool m_lifo;
+};
+
+/*!
+ \ingroup jobs
+ \brief Job Manager class for scheduling asynchronous jobs.
+
+ Controls asynchronous job execution, by allowing clients to add and cancel jobs.
+ Should be accessed via CServiceBroker::GetJobManager(). Jobs are allocated based
+ on priority levels. Lower priority jobs are executed only if there are sufficient
+ spare worker threads free to allow for higher priority jobs that may arise.
+
+ \sa CJob and IJobCallback
+ */
+class CJobManager final
+{
+ class CWorkItem
+ {
+ public:
+ CWorkItem(CJob *job, unsigned int id, CJob::PRIORITY priority, IJobCallback *callback)
+ {
+ m_job = job;
+ m_id = id;
+ m_callback = callback;
+ m_priority = priority;
+ }
+ bool operator==(unsigned int jobID) const
+ {
+ return m_id == jobID;
+ };
+ bool operator==(const CJob *job) const
+ {
+ return m_job == job;
+ };
+ void FreeJob()
+ {
+ delete m_job;
+ m_job = NULL;
+ };
+ void Cancel()
+ {
+ m_callback = NULL;
+ };
+ CJob *m_job;
+ unsigned int m_id;
+ IJobCallback *m_callback;
+ CJob::PRIORITY m_priority;
+ };
+
+public:
+ CJobManager();
+
+ /*!
+ \brief Add a job to the threaded job manager.
+ On completion or abort of the job or in case the job could not be added successfully, the CJob object will be destroyed.
+ \param job a pointer to the job to add. The job should be subclassed from CJob
+ \param callback a pointer to an IJobCallback instance to receive job progress and completion notices.
+ \param priority the priority that this job should run at.
+ \return On success, a unique identifier for this job, to be used with other interaction, 0 otherwise.
+ In case of failure, the passed CJob object will be deleted before returning from this method.
+ \sa CJob, IJobCallback, CancelJob()
+ */
+ unsigned int AddJob(CJob *job, IJobCallback *callback, CJob::PRIORITY priority = CJob::PRIORITY_LOW);
+
+ /*!
+ \brief Add a function f to this job manager for asynchronously execution.
+ */
+ template<typename F>
+ void Submit(F&& f, CJob::PRIORITY priority = CJob::PRIORITY_LOW)
+ {
+ AddJob(new CLambdaJob<F>(std::forward<F>(f)), nullptr, priority);
+ }
+
+ /*!
+ \brief Add a function f to this job manager for asynchronously execution.
+ */
+ template<typename F>
+ void Submit(F&& f, IJobCallback *callback, CJob::PRIORITY priority = CJob::PRIORITY_LOW)
+ {
+ AddJob(new CLambdaJob<F>(std::forward<F>(f)), callback, priority);
+ }
+
+ /*!
+ \brief Cancel a job with the given id.
+ \param jobID the id of the job to cancel, retrieved previously from AddJob()
+ \sa AddJob()
+ */
+ void CancelJob(unsigned int jobID);
+
+ /*!
+ \brief Cancel all remaining jobs, preparing for shutdown
+ Should be called prior to destroying any objects that may be being used as callbacks
+ \sa CancelJob(), AddJob()
+ */
+ void CancelJobs();
+
+ /*!
+ \brief Re-start accepting jobs again
+ Called after calling CancelJobs() to allow this manager to accept more jobs
+ \throws std::logic_error if the manager was not previously cancelled
+ \sa CancelJobs()
+ */
+ void Restart();
+
+ /*!
+ \brief Checks to see if any jobs of a specific type are currently processing.
+ \param type Job type to search for
+ \return Number of matching jobs
+ */
+ int IsProcessing(const std::string &type) const;
+
+ /*!
+ \brief Suspends queueing of jobs with priority PRIORITY_LOW_PAUSABLE until unpaused
+ Useful to (for ex) stop queuing thumb jobs during video start/playback.
+ Does not affect currently processing jobs, use IsProcessing to see if any need to be waited on
+ \sa UnPauseJobs()
+ */
+ void PauseJobs();
+
+ /*!
+ \brief Resumes queueing of (previously paused) jobs with priority PRIORITY_LOW_PAUSABLE
+ \sa PauseJobs()
+ */
+ void UnPauseJobs();
+
+ /*!
+ \brief Checks to see if any jobs with specific priority are currently processing.
+ \param priority to search for
+ \return true if processing jobs, else returns false
+ */
+ bool IsProcessing(const CJob::PRIORITY &priority) const;
+
+protected:
+ friend class CJobWorker;
+ friend class CJob;
+ friend class CJobQueue;
+
+ /*!
+ \brief Get a new job to process. Blocks until a new job is available, or a timeout has occurred.
+ \sa CJob
+ */
+ CJob* GetNextJob();
+
+ /*!
+ \brief Callback from CJobWorker after a job has completed.
+ Calls IJobCallback::OnJobComplete(), and then destroys job.
+ \param job a pointer to the calling subclassed CJob instance.
+ \param success the result from the DoWork call
+ \sa IJobCallback, CJob
+ */
+ void OnJobComplete(bool success, CJob *job);
+
+ /*!
+ \brief Callback from CJob to report progress and check for cancellation.
+ Checks for cancellation, and calls IJobCallback::OnJobProgress().
+ \param progress amount of processing performed to date, out of total.
+ \param total total amount of processing.
+ \param job pointer to the calling subclassed CJob instance.
+ \return true if the job has been cancelled, else returns false.
+ \sa IJobCallback, CJob
+ */
+ bool OnJobProgress(unsigned int progress, unsigned int total, const CJob *job) const;
+
+private:
+ CJobManager(const CJobManager&) = delete;
+ CJobManager const& operator=(CJobManager const&) = delete;
+
+ /*! \brief Pop a job off the job queue and add to the processing queue ready to process
+ \return the job to process, NULL if no jobs are available
+ */
+ CJob *PopJob();
+
+ void StartWorkers(CJob::PRIORITY priority);
+ void RemoveWorker(const CJobWorker *worker);
+ static unsigned int GetMaxWorkers(CJob::PRIORITY priority);
+
+ unsigned int m_jobCounter;
+
+ typedef std::deque<CWorkItem> JobQueue;
+ typedef std::vector<CWorkItem> Processing;
+ typedef std::vector<CJobWorker*> Workers;
+
+ JobQueue m_jobQueue[CJob::PRIORITY_DEDICATED + 1];
+ bool m_pauseJobs;
+ Processing m_processing;
+ Workers m_workers;
+
+ mutable CCriticalSection m_section;
+ CEvent m_jobEvent;
+ bool m_running;
+};
diff --git a/xbmc/utils/LabelFormatter.cpp b/xbmc/utils/LabelFormatter.cpp
new file mode 100644
index 0000000..d66cc63
--- /dev/null
+++ b/xbmc/utils/LabelFormatter.cpp
@@ -0,0 +1,479 @@
+/*
+ * 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 "LabelFormatter.h"
+
+#include "FileItem.h"
+#include "RegExp.h"
+#include "ServiceBroker.h"
+#include "StringUtils.h"
+#include "URIUtils.h"
+#include "Util.h"
+#include "Variant.h"
+#include "addons/IAddon.h"
+#include "guilib/LocalizeStrings.h"
+#include "music/tags/MusicInfoTag.h"
+#include "pictures/PictureInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "video/VideoInfoTag.h"
+
+#include <cassert>
+#include <cstdlib>
+#include <inttypes.h>
+
+using namespace MUSIC_INFO;
+
+/* LabelFormatter
+ * ==============
+ *
+ * The purpose of this class is to parse a mask string of the form
+ *
+ * [%N. ][%T] - [%A][ (%Y)]
+ *
+ * and provide methods to format up a CFileItem's label(s).
+ *
+ * The %N/%A/%B masks are replaced with the corresponding metadata (if available).
+ *
+ * Square brackets are treated as a metadata block. Anything inside the block other
+ * than the metadata mask is treated as either a prefix or postfix to the metadata. This
+ * information is only included in the formatted string when the metadata is non-empty.
+ *
+ * Any metadata tags not enclosed with square brackets are treated as if it were immediately
+ * enclosed - i.e. with no prefix or postfix.
+ *
+ * The special characters %, [, and ] can be produced using %%, %[, and %] respectively.
+ *
+ * Any static text outside of the metadata blocks is only shown if the blocks on either side
+ * (or just one side in the case of an end) are both non-empty.
+ *
+ * Examples (using the above expression):
+ *
+ * Track Title Artist Year Resulting Label
+ * ----- ----- ------ ---- ---------------
+ * 10 "40" U2 1983 10. "40" - U2 (1983)
+ * "40" U2 1983 "40" - U2 (1983)
+ * 10 U2 1983 10. U2 (1983)
+ * 10 "40" 1983 "40" (1983)
+ * 10 "40" U2 10. "40" - U2
+ * 10 "40" 10. "40"
+ *
+ * Available metadata masks:
+ *
+ * %A - Artist
+ * %B - Album
+ * %C - Programs count
+ * %D - Duration
+ * %E - episode number
+ * %F - FileName
+ * %G - Genre
+ * %H - season*100+episode
+ * %I - Size
+ * %J - Date
+ * %K - Movie/Game title
+ * %L - existing Label
+ * %M - number of episodes
+ * %N - Track Number
+ * %O - mpaa rating
+ * %P - production code
+ * %Q - file time
+ * %R - Movie rating
+ * %S - Disc Number
+ * %T - Title
+ * %U - studio
+ * %V - Playcount
+ * %W - Listeners
+ * %X - Bitrate
+ * %Y - Year
+ * %Z - tvshow title
+ * %a - Date Added
+ * %b - Total number of discs
+ * %c - Relevance - Used for actors' appearances
+ * %d - Date and Time
+ * %e - Original release date
+ * %f - bpm
+ * %p - Last Played
+ * %r - User Rating
+ * *t - Date Taken (suitable for Pictures)
+ */
+
+#define MASK_CHARS "NSATBGYFLDIJRCKMEPHZOQUVXWabcdefiprstuv"
+
+CLabelFormatter::CLabelFormatter(const std::string &mask, const std::string &mask2)
+{
+ // assemble our label masks
+ AssembleMask(0, mask);
+ AssembleMask(1, mask2);
+ // save a bool for faster lookups
+ m_hideFileExtensions = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWEXTENSIONS);
+}
+
+std::string CLabelFormatter::GetContent(unsigned int label, const CFileItem *item) const
+{
+ assert(label < 2);
+ assert(m_staticContent[label].size() == m_dynamicContent[label].size() + 1);
+
+ if (!item) return "";
+
+ std::string strLabel, dynamicLeft, dynamicRight;
+ for (unsigned int i = 0; i < m_dynamicContent[label].size(); i++)
+ {
+ dynamicRight = GetMaskContent(m_dynamicContent[label][i], item);
+ if ((i == 0 || !dynamicLeft.empty()) && !dynamicRight.empty())
+ strLabel += m_staticContent[label][i];
+ strLabel += dynamicRight;
+ dynamicLeft = dynamicRight;
+ }
+ if (!dynamicLeft.empty())
+ strLabel += m_staticContent[label][m_dynamicContent[label].size()];
+
+ return strLabel;
+}
+
+void CLabelFormatter::FormatLabel(CFileItem *item) const
+{
+ std::string maskedLabel = GetContent(0, item);
+ if (!maskedLabel.empty())
+ item->SetLabel(maskedLabel);
+ else if (!item->m_bIsFolder && m_hideFileExtensions)
+ item->RemoveExtension();
+}
+
+void CLabelFormatter::FormatLabel2(CFileItem *item) const
+{
+ item->SetLabel2(GetContent(1, item));
+}
+
+std::string CLabelFormatter::GetMaskContent(const CMaskString &mask, const CFileItem *item) const
+{
+ if (!item) return "";
+ const CMusicInfoTag *music = item->GetMusicInfoTag();
+ const CVideoInfoTag *movie = item->GetVideoInfoTag();
+ const CPictureInfoTag *pic = item->GetPictureInfoTag();
+ std::string value;
+ switch (mask.m_content)
+ {
+ case 'N':
+ if (music && music->GetTrackNumber() > 0)
+ value = StringUtils::Format("{:02}", music->GetTrackNumber());
+ if (movie&& movie->m_iTrack > 0)
+ value = StringUtils::Format("{:02}", movie->m_iTrack);
+ break;
+ case 'S':
+ if (music && music->GetDiscNumber() > 0)
+ value = StringUtils::Format("{:02}", music->GetDiscNumber());
+ break;
+ case 'A':
+ if (music && music->GetArtistString().size())
+ value = music->GetArtistString();
+ if (movie && movie->m_artist.size())
+ value = StringUtils::Join(movie->m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ break;
+ case 'T':
+ if (music && music->GetTitle().size())
+ value = music->GetTitle();
+ if (movie && movie->m_strTitle.size())
+ value = movie->m_strTitle;
+ break;
+ case 'Z':
+ if (movie && !movie->m_strShowTitle.empty())
+ value = movie->m_strShowTitle;
+ break;
+ case 'B':
+ if (music && music->GetAlbum().size())
+ value = music->GetAlbum();
+ else if (movie)
+ value = movie->m_strAlbum;
+ break;
+ case 'G':
+ if (music && music->GetGenre().size())
+ value = StringUtils::Join(music->GetGenre(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_musicItemSeparator);
+ if (movie && movie->m_genre.size())
+ value = StringUtils::Join(movie->m_genre, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ break;
+ case 'Y':
+ if (music)
+ value = music->GetYearString();
+ if (movie)
+ {
+ if (movie->m_firstAired.IsValid())
+ value = movie->m_firstAired.GetAsLocalizedDate();
+ else if (movie->HasYear())
+ value = std::to_string(movie->GetYear());
+ }
+ break;
+ case 'F': // filename
+ value = CUtil::GetTitleFromPath(item->GetPath(), item->m_bIsFolder && !item->IsFileFolder());
+ break;
+ case 'L':
+ value = item->GetLabel();
+ // is the label the actual file or folder name?
+ if (value == URIUtils::GetFileName(item->GetPath()))
+ { // label is the same as filename, clean it up as appropriate
+ value = CUtil::GetTitleFromPath(item->GetPath(), item->m_bIsFolder && !item->IsFileFolder());
+ }
+ break;
+ case 'D':
+ { // duration
+ int nDuration=0;
+ if (music)
+ nDuration = music->GetDuration();
+ if (movie)
+ nDuration = movie->GetDuration();
+ if (nDuration > 0)
+ value = StringUtils::SecondsToTimeString(nDuration, (nDuration >= 3600) ? TIME_FORMAT_H_MM_SS : TIME_FORMAT_MM_SS);
+ else if (item->m_dwSize > 0)
+ value = StringUtils::SizeToString(item->m_dwSize);
+ }
+ break;
+ case 'I': // size
+ if( (item->m_bIsFolder && item->m_dwSize != 0) || item->m_dwSize >= 0 )
+ value = StringUtils::SizeToString(item->m_dwSize);
+ break;
+ case 'J': // date
+ if (item->m_dateTime.IsValid())
+ value = item->m_dateTime.GetAsLocalizedDate();
+ break;
+ case 'Q': // time
+ if (item->m_dateTime.IsValid())
+ value = item->m_dateTime.GetAsLocalizedTime("", false);
+ break;
+ case 'R': // rating
+ if (music && music->GetRating() != 0.f)
+ value = StringUtils::Format("{:.1f}", music->GetRating());
+ else if (movie && movie->GetRating().rating != 0.f)
+ value = StringUtils::Format("{:.1f}", movie->GetRating().rating);
+ break;
+ case 'C': // programs count
+ value = std::to_string(item->m_iprogramCount);
+ break;
+ case 'c': // relevance
+ value = std::to_string(movie->m_relevance);
+ break;
+ case 'K':
+ value = item->m_strTitle;
+ break;
+ case 'M':
+ if (movie && movie->m_iEpisode > 0)
+ value = StringUtils::Format("{} {}", movie->m_iEpisode,
+ g_localizeStrings.Get(movie->m_iEpisode == 1 ? 20452 : 20453));
+ break;
+ case 'E':
+ if (movie && movie->m_iEpisode > 0)
+ { // episode number
+ if (movie->m_iSeason == 0)
+ value = StringUtils::Format("S{:02}", movie->m_iEpisode);
+ else
+ value = StringUtils::Format("{:02}", movie->m_iEpisode);
+ }
+ break;
+ case 'P':
+ if (movie) // tvshow production code
+ value = movie->m_strProductionCode;
+ break;
+ case 'H':
+ if (movie && movie->m_iEpisode > 0)
+ { // season*100+episode number
+ if (movie->m_iSeason == 0)
+ value = StringUtils::Format("S{:02}", movie->m_iEpisode);
+ else
+ value = StringUtils::Format("{}x{:02}", movie->m_iSeason, movie->m_iEpisode);
+ }
+ break;
+ case 'O':
+ if (movie)
+ {// MPAA Rating
+ value = movie->m_strMPAARating;
+ }
+ break;
+ case 'U':
+ if (movie && !movie->m_studio.empty())
+ {// Studios
+ value = StringUtils::Join(movie ->m_studio, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ }
+ break;
+ case 'V': // Playcount
+ if (music)
+ value = std::to_string(music->GetPlayCount());
+ if (movie)
+ value = std::to_string(movie->GetPlayCount());
+ break;
+ case 'X': // Bitrate
+ if( !item->m_bIsFolder && item->m_dwSize != 0 )
+ value = StringUtils::Format("{} kbps", item->m_dwSize);
+ break;
+ case 'W': // Listeners
+ if( !item->m_bIsFolder && music && music->GetListeners() != 0 )
+ value =
+ StringUtils::Format("{} {}", music->GetListeners(),
+ g_localizeStrings.Get(music->GetListeners() == 1 ? 20454 : 20455));
+ break;
+ case 'a': // Date Added
+ if (movie && movie->m_dateAdded.IsValid())
+ value = movie->m_dateAdded.GetAsLocalizedDate();
+ if (music && music->GetDateAdded().IsValid())
+ value = music->GetDateAdded().GetAsLocalizedDate();
+ break;
+ case 'b': // Total number of discs
+ if (music)
+ value = std::to_string(music->GetTotalDiscs());
+ break;
+ case 'e': // Original release date
+ if (music)
+ {
+ value = music->GetOriginalDate();
+ if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bMusicLibraryUseISODates)
+ value = StringUtils::ISODateToLocalizedDate(value);
+ }
+ break;
+ case 'd': // date and time
+ if (item->m_dateTime.IsValid())
+ value = item->m_dateTime.GetAsLocalizedDateTime();
+ break;
+ case 'p': // Last played
+ if (movie && movie->m_lastPlayed.IsValid())
+ value = movie->m_lastPlayed.GetAsLocalizedDate();
+ if (music && music->GetLastPlayed().IsValid())
+ value = music->GetLastPlayed().GetAsLocalizedDate();
+ break;
+ case 'r': // userrating
+ if (movie && movie->m_iUserRating != 0)
+ value = std::to_string(movie->m_iUserRating);
+ if (music && music->GetUserrating() != 0)
+ value = std::to_string(music->GetUserrating());
+ break;
+ case 't': // Date Taken
+ if (pic && pic->GetDateTimeTaken().IsValid())
+ value = pic->GetDateTimeTaken().GetAsLocalizedDate();
+ break;
+ case 's': // Addon status
+ if (item->HasProperty("Addon.Status"))
+ value = item->GetProperty("Addon.Status").asString();
+ break;
+ case 'i': // Install date
+ if (item->HasAddonInfo() && item->GetAddonInfo()->InstallDate().IsValid())
+ value = item->GetAddonInfo()->InstallDate().GetAsLocalizedDate();
+ break;
+ case 'u': // Last used
+ if (item->HasAddonInfo() && item->GetAddonInfo()->LastUsed().IsValid())
+ value = item->GetAddonInfo()->LastUsed().GetAsLocalizedDate();
+ break;
+ case 'v': // Last updated
+ if (item->HasAddonInfo() && item->GetAddonInfo()->LastUpdated().IsValid())
+ value = item->GetAddonInfo()->LastUpdated().GetAsLocalizedDate();
+ break;
+ case 'f': // BPM
+ if (music)
+ value = std::to_string(music->GetBPM());
+ break;
+ }
+ if (!value.empty())
+ return mask.m_prefix + value + mask.m_postfix;
+ return "";
+}
+
+void CLabelFormatter::SplitMask(unsigned int label, const std::string &mask)
+{
+ assert(label < 2);
+ CRegExp reg;
+ reg.RegComp("%([" MASK_CHARS "])");
+ std::string work(mask);
+ int findStart = -1;
+ while ((findStart = reg.RegFind(work.c_str())) >= 0)
+ { // we've found a match
+ m_staticContent[label].push_back(work.substr(0, findStart));
+ m_dynamicContent[label].emplace_back("", reg.GetMatch(1)[0], "");
+ work = work.substr(findStart + reg.GetFindLen());
+ }
+ m_staticContent[label].push_back(work);
+}
+
+void CLabelFormatter::AssembleMask(unsigned int label, const std::string& mask)
+{
+ assert(label < 2);
+ m_staticContent[label].clear();
+ m_dynamicContent[label].clear();
+
+ // we want to match [<prefix>%A<postfix]
+ // but allow %%, %[, %] to be in the prefix and postfix. Anything before the first [
+ // could be a mask that's not surrounded with [], so pass to SplitMask.
+ CRegExp reg;
+ reg.RegComp("(^|[^%])\\[(([^%]|%%|%\\]|%\\[)*)%([" MASK_CHARS "])(([^%]|%%|%\\]|%\\[)*)\\]");
+ std::string work(mask);
+ int findStart = -1;
+ while ((findStart = reg.RegFind(work.c_str())) >= 0)
+ { // we've found a match for a pre/postfixed string
+ // send anything
+ SplitMask(label, work.substr(0, findStart) + reg.GetMatch(1));
+ m_dynamicContent[label].emplace_back(reg.GetMatch(2), reg.GetMatch(4)[0], reg.GetMatch(5));
+ work = work.substr(findStart + reg.GetFindLen());
+ }
+ SplitMask(label, work);
+ assert(m_staticContent[label].size() == m_dynamicContent[label].size() + 1);
+}
+
+bool CLabelFormatter::FillMusicTag(const std::string &fileName, CMusicInfoTag *tag) const
+{
+ // run through and find static content to split the string up
+ size_t pos1 = fileName.find(m_staticContent[0][0], 0);
+ if (pos1 == std::string::npos)
+ return false;
+ for (unsigned int i = 1; i < m_staticContent[0].size(); i++)
+ {
+ size_t pos2 = m_staticContent[0][i].size() ? fileName.find(m_staticContent[0][i], pos1) : fileName.size();
+ if (pos2 == std::string::npos)
+ return false;
+ // found static content - thus we have the dynamic content surrounded
+ FillMusicMaskContent(m_dynamicContent[0][i - 1].m_content, fileName.substr(pos1, pos2 - pos1), tag);
+ pos1 = pos2 + m_staticContent[0][i].size();
+ }
+ return true;
+}
+
+void CLabelFormatter::FillMusicMaskContent(const char mask, const std::string &value, CMusicInfoTag *tag) const
+{
+ if (!tag) return;
+ switch (mask)
+ {
+ case 'N':
+ tag->SetTrackNumber(atol(value.c_str()));
+ break;
+ case 'S':
+ tag->SetDiscNumber(atol(value.c_str()));
+ break;
+ case 'A':
+ tag->SetArtist(value);
+ break;
+ case 'T':
+ tag->SetTitle(value);
+ break;
+ case 'B':
+ tag->SetAlbum(value);
+ break;
+ case 'G':
+ tag->SetGenre(value);
+ break;
+ case 'Y':
+ tag->SetYear(atol(value.c_str()));
+ break;
+ case 'D':
+ tag->SetDuration(StringUtils::TimeStringToSeconds(value));
+ break;
+ case 'R': // rating
+ tag->SetRating(value[0]);
+ break;
+ case 'r': // userrating
+ tag->SetUserrating(value[0]);
+ break;
+ case 'b': // total discs
+ tag->SetTotalDiscs(atol(value.c_str()));
+ break;
+ }
+}
+
diff --git a/xbmc/utils/LabelFormatter.h b/xbmc/utils/LabelFormatter.h
new file mode 100644
index 0000000..a10eae6
--- /dev/null
+++ b/xbmc/utils/LabelFormatter.h
@@ -0,0 +1,76 @@
+/*
+ * 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 <string>
+#include <vector>
+
+namespace MUSIC_INFO
+{
+ class CMusicInfoTag;
+}
+
+class CFileItem; // forward
+
+struct LABEL_MASKS
+{
+ LABEL_MASKS(const std::string& strLabelFile="", const std::string& strLabel2File="", const std::string& strLabelFolder="", const std::string& strLabel2Folder="") :
+ m_strLabelFile(strLabelFile),
+ m_strLabel2File(strLabel2File),
+ m_strLabelFolder(strLabelFolder),
+ m_strLabel2Folder(strLabel2Folder)
+ {}
+ std::string m_strLabelFile;
+ std::string m_strLabel2File;
+ std::string m_strLabelFolder;
+ std::string m_strLabel2Folder;
+};
+
+class CLabelFormatter
+{
+public:
+ CLabelFormatter(const std::string &mask, const std::string &mask2);
+
+ void FormatLabel(CFileItem *item) const;
+ void FormatLabel2(CFileItem *item) const;
+ void FormatLabels(CFileItem *item) const // convenient shorthand
+ {
+ FormatLabel(item);
+ FormatLabel2(item);
+ }
+
+ bool FillMusicTag(const std::string &fileName, MUSIC_INFO::CMusicInfoTag *tag) const;
+
+private:
+ class CMaskString
+ {
+ public:
+ CMaskString(const std::string &prefix, char content, const std::string &postfix) :
+ m_prefix(prefix),
+ m_postfix(postfix),
+ m_content(content)
+ {};
+ std::string m_prefix;
+ std::string m_postfix;
+ char m_content;
+ };
+
+ // functions for assembling the mask vectors
+ void AssembleMask(unsigned int label, const std::string &mask);
+ void SplitMask(unsigned int label, const std::string &mask);
+
+ // functions for retrieving content based on our mask vectors
+ std::string GetContent(unsigned int label, const CFileItem *item) const;
+ std::string GetMaskContent(const CMaskString &mask, const CFileItem *item) const;
+ void FillMusicMaskContent(const char mask, const std::string &value, MUSIC_INFO::CMusicInfoTag *tag) const;
+
+ std::vector<std::string> m_staticContent[2];
+ std::vector<CMaskString> m_dynamicContent[2];
+ bool m_hideFileExtensions;
+};
diff --git a/xbmc/utils/LangCodeExpander.cpp b/xbmc/utils/LangCodeExpander.cpp
new file mode 100644
index 0000000..f683905
--- /dev/null
+++ b/xbmc/utils/LangCodeExpander.cpp
@@ -0,0 +1,1792 @@
+/*
+ * Copyright (C) 2005-2020 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 "LangCodeExpander.h"
+
+#include "LangInfo.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+
+#include <algorithm>
+#include <array>
+
+#define MAKECODE(a, b, c, d) \
+ ((((long)(a)) << 24) | (((long)(b)) << 16) | (((long)(c)) << 8) | (long)(d))
+#define MAKETWOCHARCODE(a, b) ((((long)(a)) << 8) | (long)(b))
+
+typedef struct LCENTRY
+{
+ long code;
+ const char* name;
+} LCENTRY;
+
+extern const std::array<struct LCENTRY, 186> g_iso639_1;
+extern const std::array<struct LCENTRY, 540> g_iso639_2;
+
+struct ISO639
+{
+ const char* iso639_1;
+ const char* iso639_2b;
+ const char* iso639_2t;
+ const char* win_id;
+};
+
+struct ISO3166_1
+{
+ const char* alpha2;
+ const char* alpha3;
+};
+
+// declared as extern to allow forward declaration
+extern const std::array<ISO639, 190> LanguageCodes;
+extern const std::array<ISO3166_1, 245> RegionCodes;
+
+CLangCodeExpander::CLangCodeExpander() = default;
+
+CLangCodeExpander::~CLangCodeExpander() = default;
+
+void CLangCodeExpander::Clear()
+{
+ m_mapUser.clear();
+}
+
+void CLangCodeExpander::LoadUserCodes(const TiXmlElement* pRootElement)
+{
+ if (pRootElement != NULL)
+ {
+ m_mapUser.clear();
+
+ std::string sShort, sLong;
+
+ const TiXmlNode* pLangCode = pRootElement->FirstChild("code");
+ while (pLangCode != NULL)
+ {
+ const TiXmlNode* pShort = pLangCode->FirstChildElement("short");
+ const TiXmlNode* pLong = pLangCode->FirstChildElement("long");
+ if (pShort != NULL && pLong != NULL)
+ {
+ sShort = pShort->FirstChild()->Value();
+ sLong = pLong->FirstChild()->Value();
+ StringUtils::ToLower(sShort);
+
+ m_mapUser[sShort] = sLong;
+ }
+
+ pLangCode = pLangCode->NextSibling();
+ }
+ }
+}
+
+bool CLangCodeExpander::Lookup(const std::string& code, std::string& desc)
+{
+ if (LookupInUserMap(code, desc))
+ return true;
+
+ if (LookupInISO639Tables(code, desc))
+ return true;
+
+ if (LookupInLangAddons(code, desc))
+ return true;
+
+ // Language code with subtag is supported only with language addons
+ // or with user defined map, then if not found we fallback by obtaining
+ // the primary code description only and appending the remaining
+ int iSplit = code.find('-');
+ if (iSplit > 0)
+ {
+ std::string primaryTagDesc;
+ const bool hasPrimaryTagDesc = Lookup(code.substr(0, iSplit), primaryTagDesc);
+ std::string subtagCode = code.substr(iSplit + 1);
+ if (hasPrimaryTagDesc)
+ {
+ if (primaryTagDesc.length() > 0)
+ desc = primaryTagDesc;
+ else
+ desc = code.substr(0, iSplit);
+
+ if (subtagCode.length() > 0)
+ desc += " - " + subtagCode;
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CLangCodeExpander::Lookup(const int code, std::string& desc)
+{
+ char lang[3];
+ lang[2] = 0;
+ lang[1] = (code & 0xFF);
+ lang[0] = (code >> 8) & 0xFF;
+
+ return Lookup(lang, desc);
+}
+
+bool CLangCodeExpander::ConvertISO6391ToISO6392B(const std::string& strISO6391,
+ std::string& strISO6392B,
+ bool checkWin32Locales /*= false*/)
+{
+ // not a 2 char code
+ if (strISO6391.length() != 2)
+ return false;
+
+ std::string strISO6391Lower(strISO6391);
+ StringUtils::ToLower(strISO6391Lower);
+ StringUtils::Trim(strISO6391Lower);
+
+ for (const auto& codes : LanguageCodes)
+ {
+ if (strISO6391Lower == codes.iso639_1)
+ {
+ if (checkWin32Locales && codes.win_id)
+ {
+ strISO6392B = codes.win_id;
+ return true;
+ }
+
+ strISO6392B = codes.iso639_2b;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool CLangCodeExpander::ConvertToISO6392B(const std::string& strCharCode,
+ std::string& strISO6392B,
+ bool checkWin32Locales /* = false */)
+{
+
+ //first search in the user defined map
+ if (LookupUserCode(strCharCode, strISO6392B))
+ return true;
+
+ if (strCharCode.size() == 2)
+ return g_LangCodeExpander.ConvertISO6391ToISO6392B(strCharCode, strISO6392B, checkWin32Locales);
+
+ if (strCharCode.size() == 3)
+ {
+ std::string charCode(strCharCode);
+ StringUtils::ToLower(charCode);
+ for (const auto& codes : LanguageCodes)
+ {
+ if (charCode == codes.iso639_2b ||
+ (checkWin32Locales && codes.win_id != NULL && charCode == codes.win_id))
+ {
+ strISO6392B = charCode;
+ return true;
+ }
+ }
+
+ for (const auto& codes : RegionCodes)
+ {
+ if (charCode == codes.alpha3)
+ {
+ strISO6392B = charCode;
+ return true;
+ }
+ }
+ }
+ else if (strCharCode.size() > 3)
+ {
+ for (const auto& codes : g_iso639_2)
+ {
+ if (StringUtils::EqualsNoCase(strCharCode, codes.name))
+ {
+ strISO6392B = CodeToString(codes.code);
+ return true;
+ }
+ }
+
+ // Try search on language addons
+ strISO6392B = g_langInfo.ConvertEnglishNameToAddonLocale(strCharCode);
+ if (!strISO6392B.empty())
+ return true;
+ }
+ return false;
+}
+
+bool CLangCodeExpander::ConvertToISO6392T(const std::string& strCharCode,
+ std::string& strISO6392T,
+ bool checkWin32Locales /* = false */)
+{
+ if (!ConvertToISO6392B(strCharCode, strISO6392T, checkWin32Locales))
+ return false;
+
+ for (const auto& codes : LanguageCodes)
+ {
+ if (strISO6392T == codes.iso639_2b ||
+ (checkWin32Locales && codes.win_id != NULL && strISO6392T == codes.win_id))
+ {
+ if (codes.iso639_2t != nullptr)
+ strISO6392T = codes.iso639_2t;
+ return true;
+ }
+ }
+ return false;
+}
+
+
+bool CLangCodeExpander::LookupUserCode(const std::string& desc, std::string& userCode)
+{
+ for (STRINGLOOKUPTABLE::const_iterator it = m_mapUser.begin(); it != m_mapUser.end(); ++it)
+ {
+ if (StringUtils::EqualsNoCase(desc, it->first) || StringUtils::EqualsNoCase(desc, it->second))
+ {
+ userCode = it->first;
+ return true;
+ }
+ }
+ return false;
+}
+
+#ifdef TARGET_WINDOWS
+bool CLangCodeExpander::ConvertISO31661Alpha2ToISO31661Alpha3(const std::string& strISO31661Alpha2,
+ std::string& strISO31661Alpha3)
+{
+ if (strISO31661Alpha2.length() != 2)
+ return false;
+
+ std::string strLower(strISO31661Alpha2);
+ StringUtils::ToLower(strLower);
+ StringUtils::Trim(strLower);
+ for (const auto& codes : RegionCodes)
+ {
+ if (strLower == codes.alpha2)
+ {
+ strISO31661Alpha3 = codes.alpha3;
+ return true;
+ }
+ }
+
+ return true;
+}
+
+bool CLangCodeExpander::ConvertWindowsLanguageCodeToISO6392B(
+ const std::string& strWindowsLanguageCode, std::string& strISO6392B)
+{
+ if (strWindowsLanguageCode.length() != 3)
+ return false;
+
+ std::string strLower(strWindowsLanguageCode);
+ StringUtils::ToLower(strLower);
+ for (const auto& codes : LanguageCodes)
+ {
+ if ((codes.win_id && strLower == codes.win_id) || strLower == codes.iso639_2b)
+ {
+ strISO6392B = codes.iso639_2b;
+ return true;
+ }
+ }
+
+ return false;
+}
+#endif
+
+bool CLangCodeExpander::ConvertToISO6391(const std::string& lang, std::string& code)
+{
+ if (lang.empty())
+ return false;
+
+ //first search in the user defined map
+ if (LookupUserCode(lang, code))
+ return true;
+
+ if (lang.length() == 2)
+ {
+ std::string tmp;
+ if (Lookup(lang, tmp))
+ {
+ code = lang;
+ return true;
+ }
+ }
+ else if (lang.length() == 3)
+ {
+ std::string lower(lang);
+ StringUtils::ToLower(lower);
+ for (const auto& codes : LanguageCodes)
+ {
+ if (lower == codes.iso639_2b || (codes.win_id && lower == codes.win_id))
+ {
+ code = codes.iso639_1;
+ return true;
+ }
+ }
+
+ for (const auto& codes : RegionCodes)
+ {
+ if (lower == codes.alpha3)
+ {
+ code = codes.alpha2;
+ return true;
+ }
+ }
+ }
+
+ // check if lang is full language name
+ std::string tmp;
+ if (ReverseLookup(lang, tmp))
+ {
+ if (tmp.length() == 2)
+ {
+ code = tmp;
+ return true;
+ }
+
+ if (tmp.length() == 3)
+ {
+ // there's only an iso639-2 code that is identical to the language name, e.g. Yao
+ if (StringUtils::EqualsNoCase(tmp, lang))
+ return false;
+
+ return ConvertToISO6391(tmp, code);
+ }
+ }
+
+ return false;
+}
+
+bool CLangCodeExpander::ReverseLookup(const std::string& desc, std::string& code)
+{
+ if (desc.empty())
+ return false;
+
+ std::string descTmp(desc);
+ StringUtils::Trim(descTmp);
+
+ // First find to user-defined languages
+ for (STRINGLOOKUPTABLE::const_iterator it = m_mapUser.begin(); it != m_mapUser.end(); ++it)
+ {
+ if (StringUtils::EqualsNoCase(descTmp, it->second))
+ {
+ code = it->first;
+ return true;
+ }
+ }
+
+ for (const auto& codes : g_iso639_1)
+ {
+ if (StringUtils::EqualsNoCase(descTmp, codes.name))
+ {
+ code = CodeToString(codes.code);
+ return true;
+ }
+ }
+
+ for (const auto& codes : g_iso639_2)
+ {
+ if (StringUtils::EqualsNoCase(descTmp, codes.name))
+ {
+ code = CodeToString(codes.code);
+ return true;
+ }
+ }
+
+ // Find on language addons
+ code = g_langInfo.ConvertEnglishNameToAddonLocale(descTmp);
+ if (!code.empty())
+ return true;
+
+ return false;
+}
+
+bool CLangCodeExpander::LookupInUserMap(const std::string& code, std::string& desc)
+{
+ if (code.empty())
+ return false;
+
+ // make sure we convert to lowercase before trying to find it
+ std::string sCode(code);
+ StringUtils::ToLower(sCode);
+ StringUtils::Trim(sCode);
+
+ STRINGLOOKUPTABLE::iterator it = m_mapUser.find(sCode);
+ if (it != m_mapUser.end())
+ {
+ desc = it->second;
+ return true;
+ }
+
+ return false;
+}
+
+bool CLangCodeExpander::LookupInLangAddons(const std::string& code, std::string& desc)
+{
+ if (code.empty())
+ return false;
+
+ std::string sCode{code};
+ StringUtils::Trim(sCode);
+ StringUtils::ToLower(sCode);
+ StringUtils::Replace(sCode, '-', '_');
+
+ desc = g_langInfo.GetEnglishLanguageName(sCode);
+ return !desc.empty();
+}
+
+bool CLangCodeExpander::LookupInISO639Tables(const std::string& code, std::string& desc)
+{
+ if (code.empty())
+ return false;
+
+ long longcode;
+ std::string sCode(code);
+ StringUtils::ToLower(sCode);
+ StringUtils::Trim(sCode);
+
+ if (sCode.length() == 2)
+ {
+ longcode = MAKECODE('\0', '\0', sCode[0], sCode[1]);
+ for (const auto& codes : g_iso639_1)
+ {
+ if (codes.code == longcode)
+ {
+ desc = codes.name;
+ return true;
+ }
+ }
+ }
+ else if (sCode.length() == 3)
+ {
+ longcode = MAKECODE('\0', sCode[0], sCode[1], sCode[2]);
+ for (const auto& codes : g_iso639_2)
+ {
+ if (codes.code == longcode)
+ {
+ desc = codes.name;
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+std::string CLangCodeExpander::CodeToString(long code)
+{
+ std::string ret;
+ for (unsigned int j = 0; j < 4; j++)
+ {
+ char c = (char)code & 0xFF;
+ if (c == '\0')
+ break;
+
+ ret.insert(0, 1, c);
+ code >>= 8;
+ }
+ return ret;
+}
+
+bool CLangCodeExpander::CompareFullLanguageNames(const std::string& lang1, const std::string& lang2)
+{
+ if (StringUtils::EqualsNoCase(lang1, lang2))
+ return true;
+
+ std::string expandedLang1, expandedLang2, code1, code2;
+
+ if (!ReverseLookup(lang1, code1))
+ return false;
+
+ code1 = lang1;
+ if (!ReverseLookup(lang2, code2))
+ return false;
+
+ code2 = lang2;
+ Lookup(expandedLang1, code1);
+ Lookup(expandedLang2, code2);
+
+ return StringUtils::EqualsNoCase(expandedLang1, expandedLang2);
+}
+
+std::vector<std::string> CLangCodeExpander::GetLanguageNames(
+ LANGFORMATS format /* = CLangCodeExpander::ISO_639_1 */,
+ LANG_LIST list /* = LANG_LIST::DEFAULT */)
+{
+ std::map<std::string, std::string> langMap;
+
+ if (format == CLangCodeExpander::ISO_639_2)
+ std::transform(g_iso639_2.begin(), g_iso639_2.end(), std::inserter(langMap, langMap.end()),
+ [](const LCENTRY& e) { return std::make_pair(CodeToString(e.code), e.name); });
+ else
+ std::transform(g_iso639_1.begin(), g_iso639_1.end(), std::inserter(langMap, langMap.end()),
+ [](const LCENTRY& e) { return std::make_pair(CodeToString(e.code), e.name); });
+
+ if (list == LANG_LIST::INCLUDE_ADDONS || list == LANG_LIST::INCLUDE_ADDONS_USERDEFINED)
+ {
+ g_langInfo.GetAddonsLanguageCodes(langMap);
+ }
+
+ // User-defined languages can override existing ones
+ if (list == LANG_LIST::INCLUDE_USERDEFINED || list == LANG_LIST::INCLUDE_ADDONS_USERDEFINED)
+ {
+ for (const auto& value : m_mapUser)
+ {
+ langMap[value.first] = value.second;
+ }
+ }
+
+ // Sort by name and remove duplicates
+ std::set<std::string, sortstringbyname> languages;
+ for (const auto& lang : langMap)
+ {
+ languages.insert(lang.second);
+ }
+
+ return std::vector<std::string>(languages.begin(), languages.end());
+}
+
+bool CLangCodeExpander::CompareISO639Codes(const std::string& code1, const std::string& code2)
+{
+ if (StringUtils::EqualsNoCase(code1, code2))
+ return true;
+
+ std::string expandedLang1;
+ if (!Lookup(code1, expandedLang1))
+ return false;
+
+ std::string expandedLang2;
+ if (!Lookup(code2, expandedLang2))
+ return false;
+
+ return StringUtils::EqualsNoCase(expandedLang1, expandedLang2);
+}
+
+std::string CLangCodeExpander::ConvertToISO6392B(const std::string& lang)
+{
+ if (lang.empty())
+ return lang;
+
+ std::string two, three;
+ if (ConvertToISO6391(lang, two))
+ {
+ if (ConvertToISO6392B(two, three))
+ return three;
+ }
+
+ return lang;
+}
+
+std::string CLangCodeExpander::ConvertToISO6392T(const std::string& lang)
+{
+ if (lang.empty())
+ return lang;
+
+ std::string two, three;
+ if (ConvertToISO6391(lang, two))
+ {
+ if (ConvertToISO6392T(two, three))
+ return three;
+ }
+
+ return lang;
+}
+
+std::string CLangCodeExpander::FindLanguageCodeWithSubtag(const std::string& str)
+{
+ CRegExp regLangCode;
+ if (regLangCode.RegComp(
+ "(?:^|\\s|\\()(([A-Za-z]{2,3})-([A-Za-z]{2}|[0-9]{3}|[A-Za-z]{4}))(?:$|\\s|\\))") &&
+ regLangCode.RegFind(str) >= 0)
+ {
+ return regLangCode.GetMatch(1);
+ }
+ return "";
+}
+
+// clang-format off
+const std::array<struct LCENTRY, 186> g_iso639_1 = {{
+ {MAKECODE('\0', '\0', 'a', 'a'), "Afar"},
+ {MAKECODE('\0', '\0', 'a', 'b'), "Abkhazian"},
+ {MAKECODE('\0', '\0', 'a', 'e'), "Avestan"},
+ {MAKECODE('\0', '\0', 'a', 'f'), "Afrikaans"},
+ {MAKECODE('\0', '\0', 'a', 'k'), "Akan"},
+ {MAKECODE('\0', '\0', 'a', 'm'), "Amharic"},
+ {MAKECODE('\0', '\0', 'a', 'n'), "Aragonese"},
+ {MAKECODE('\0', '\0', 'a', 'r'), "Arabic"},
+ {MAKECODE('\0', '\0', 'a', 's'), "Assamese"},
+ {MAKECODE('\0', '\0', 'a', 'v'), "Avaric"},
+ {MAKECODE('\0', '\0', 'a', 'y'), "Aymara"},
+ {MAKECODE('\0', '\0', 'a', 'z'), "Azerbaijani"},
+ {MAKECODE('\0', '\0', 'b', 'a'), "Bashkir"},
+ {MAKECODE('\0', '\0', 'b', 'e'), "Belarusian"},
+ {MAKECODE('\0', '\0', 'b', 'g'), "Bulgarian"},
+ {MAKECODE('\0', '\0', 'b', 'h'), "Bihari"},
+ {MAKECODE('\0', '\0', 'b', 'i'), "Bislama"},
+ {MAKECODE('\0', '\0', 'b', 'm'), "Bambara"},
+ {MAKECODE('\0', '\0', 'b', 'n'), "Bengali; Bangla"},
+ {MAKECODE('\0', '\0', 'b', 'o'), "Tibetan"},
+ {MAKECODE('\0', '\0', 'b', 'r'), "Breton"},
+ {MAKECODE('\0', '\0', 'b', 's'), "Bosnian"},
+ {MAKECODE('\0', '\0', 'c', 'a'), "Catalan"},
+ {MAKECODE('\0', '\0', 'c', 'e'), "Chechen"},
+ {MAKECODE('\0', '\0', 'c', 'h'), "Chamorro"},
+ {MAKECODE('\0', '\0', 'c', 'o'), "Corsican"},
+ {MAKECODE('\0', '\0', 'c', 'r'), "Cree"},
+ {MAKECODE('\0', '\0', 'c', 's'), "Czech"},
+ {MAKECODE('\0', '\0', 'c', 'u'), "Church Slavic"},
+ {MAKECODE('\0', '\0', 'c', 'v'), "Chuvash"},
+ {MAKECODE('\0', '\0', 'c', 'y'), "Welsh"},
+ {MAKECODE('\0', '\0', 'd', 'a'), "Danish"},
+ {MAKECODE('\0', '\0', 'd', 'e'), "German"},
+ {MAKECODE('\0', '\0', 'd', 'v'), "Dhivehi"},
+ {MAKECODE('\0', '\0', 'd', 'z'), "Dzongkha"},
+ {MAKECODE('\0', '\0', 'e', 'e'), "Ewe"},
+ {MAKECODE('\0', '\0', 'e', 'l'), "Greek"},
+ {MAKECODE('\0', '\0', 'e', 'n'), "English"},
+ {MAKECODE('\0', '\0', 'e', 'o'), "Esperanto"},
+ {MAKECODE('\0', '\0', 'e', 's'), "Spanish"},
+ {MAKECODE('\0', '\0', 'e', 't'), "Estonian"},
+ {MAKECODE('\0', '\0', 'e', 'u'), "Basque"},
+ {MAKECODE('\0', '\0', 'f', 'a'), "Persian"},
+ {MAKECODE('\0', '\0', 'f', 'f'), "Fulah"},
+ {MAKECODE('\0', '\0', 'f', 'i'), "Finnish"},
+ {MAKECODE('\0', '\0', 'f', 'j'), "Fijian"},
+ {MAKECODE('\0', '\0', 'f', 'o'), "Faroese"},
+ {MAKECODE('\0', '\0', 'f', 'r'), "French"},
+ {MAKECODE('\0', '\0', 'f', 'y'), "Western Frisian"},
+ {MAKECODE('\0', '\0', 'g', 'a'), "Irish"},
+ {MAKECODE('\0', '\0', 'g', 'd'), "Scottish Gaelic"},
+ {MAKECODE('\0', '\0', 'g', 'l'), "Galician"},
+ {MAKECODE('\0', '\0', 'g', 'n'), "Guarani"},
+ {MAKECODE('\0', '\0', 'g', 'u'), "Gujarati"},
+ {MAKECODE('\0', '\0', 'g', 'v'), "Manx"},
+ {MAKECODE('\0', '\0', 'h', 'a'), "Hausa"},
+ {MAKECODE('\0', '\0', 'h', 'e'), "Hebrew"},
+ {MAKECODE('\0', '\0', 'h', 'i'), "Hindi"},
+ {MAKECODE('\0', '\0', 'h', 'o'), "Hiri Motu"},
+ {MAKECODE('\0', '\0', 'h', 'r'), "Croatian"},
+ {MAKECODE('\0', '\0', 'h', 't'), "Haitian"},
+ {MAKECODE('\0', '\0', 'h', 'u'), "Hungarian"},
+ {MAKECODE('\0', '\0', 'h', 'y'), "Armenian"},
+ {MAKECODE('\0', '\0', 'h', 'z'), "Herero"},
+ {MAKECODE('\0', '\0', 'i', 'a'), "Interlingua"},
+ {MAKECODE('\0', '\0', 'i', 'd'), "Indonesian"},
+ {MAKECODE('\0', '\0', 'i', 'e'), "Interlingue"},
+ {MAKECODE('\0', '\0', 'i', 'g'), "Igbo"},
+ {MAKECODE('\0', '\0', 'i', 'i'), "Sichuan Yi"},
+ {MAKECODE('\0', '\0', 'i', 'k'), "Inupiat"},
+ {MAKECODE('\0', '\0', 'i', 'o'), "Ido"},
+ {MAKECODE('\0', '\0', 'i', 's'), "Icelandic"},
+ {MAKECODE('\0', '\0', 'i', 't'), "Italian"},
+ {MAKECODE('\0', '\0', 'i', 'u'), "Inuktitut"},
+ {MAKECODE('\0', '\0', 'j', 'a'), "Japanese"},
+ {MAKECODE('\0', '\0', 'j', 'v'), "Javanese"},
+ {MAKECODE('\0', '\0', 'k', 'a'), "Georgian"},
+ {MAKECODE('\0', '\0', 'k', 'g'), "Kongo"},
+ {MAKECODE('\0', '\0', 'k', 'i'), "Kikuyu"},
+ {MAKECODE('\0', '\0', 'k', 'j'), "Kuanyama"},
+ {MAKECODE('\0', '\0', 'k', 'k'), "Kazakh"},
+ {MAKECODE('\0', '\0', 'k', 'l'), "Kalaallisut"},
+ {MAKECODE('\0', '\0', 'k', 'm'), "Khmer"},
+ {MAKECODE('\0', '\0', 'k', 'n'), "Kannada"},
+ {MAKECODE('\0', '\0', 'k', 'o'), "Korean"},
+ {MAKECODE('\0', '\0', 'k', 'r'), "Kanuri"},
+ {MAKECODE('\0', '\0', 'k', 's'), "Kashmiri"},
+ {MAKECODE('\0', '\0', 'k', 'u'), "Kurdish"},
+ {MAKECODE('\0', '\0', 'k', 'v'), "Komi"},
+ {MAKECODE('\0', '\0', 'k', 'w'), "Cornish"},
+ {MAKECODE('\0', '\0', 'k', 'y'), "Kirghiz"},
+ {MAKECODE('\0', '\0', 'l', 'a'), "Latin"},
+ {MAKECODE('\0', '\0', 'l', 'b'), "Luxembourgish"},
+ {MAKECODE('\0', '\0', 'l', 'g'), "Ganda"},
+ {MAKECODE('\0', '\0', 'l', 'i'), "Limburgan"},
+ {MAKECODE('\0', '\0', 'l', 'n'), "Lingala"},
+ {MAKECODE('\0', '\0', 'l', 'o'), "Lao"},
+ {MAKECODE('\0', '\0', 'l', 't'), "Lithuanian"},
+ {MAKECODE('\0', '\0', 'l', 'u'), "Luba-Katanga"},
+ {MAKECODE('\0', '\0', 'l', 'v'), "Latvian, Lettish"},
+ {MAKECODE('\0', '\0', 'm', 'g'), "Malagasy"},
+ {MAKECODE('\0', '\0', 'm', 'h'), "Marshallese"},
+ {MAKECODE('\0', '\0', 'm', 'i'), "Maori"},
+ {MAKECODE('\0', '\0', 'm', 'k'), "Macedonian"},
+ {MAKECODE('\0', '\0', 'm', 'l'), "Malayalam"},
+ {MAKECODE('\0', '\0', 'm', 'n'), "Mongolian"},
+ {MAKECODE('\0', '\0', 'm', 'r'), "Marathi"},
+ {MAKECODE('\0', '\0', 'm', 's'), "Malay"},
+ {MAKECODE('\0', '\0', 'm', 't'), "Maltese"},
+ {MAKECODE('\0', '\0', 'm', 'y'), "Burmese"},
+ {MAKECODE('\0', '\0', 'n', 'a'), "Nauru"},
+ {MAKECODE('\0', '\0', 'n', 'b'), "Norwegian Bokm\xC3\xA5l"},
+ {MAKECODE('\0', '\0', 'n', 'd'), "Ndebele, North"},
+ {MAKECODE('\0', '\0', 'n', 'e'), "Nepali"},
+ {MAKECODE('\0', '\0', 'n', 'g'), "Ndonga"},
+ {MAKECODE('\0', '\0', 'n', 'l'), "Dutch"},
+ {MAKECODE('\0', '\0', 'n', 'n'), "Norwegian Nynorsk"},
+ {MAKECODE('\0', '\0', 'n', 'o'), "Norwegian"},
+ {MAKECODE('\0', '\0', 'n', 'r'), "Ndebele, South"},
+ {MAKECODE('\0', '\0', 'n', 'v'), "Navajo"},
+ {MAKECODE('\0', '\0', 'n', 'y'), "Chichewa"},
+ {MAKECODE('\0', '\0', 'o', 'c'), "Occitan"},
+ {MAKECODE('\0', '\0', 'o', 'j'), "Ojibwa"},
+ {MAKECODE('\0', '\0', 'o', 'm'), "Oromo"},
+ {MAKECODE('\0', '\0', 'o', 'r'), "Oriya"},
+ {MAKECODE('\0', '\0', 'o', 's'), "Ossetic"},
+ {MAKECODE('\0', '\0', 'p', 'a'), "Punjabi"},
+ {MAKECODE('\0', '\0', 'p', 'i'), "Pali"},
+ {MAKECODE('\0', '\0', 'p', 'l'), "Polish"},
+ {MAKECODE('\0', '\0', 'p', 's'), "Pashto, Pushto"},
+ {MAKECODE('\0', '\0', 'p', 't'), "Portuguese"},
+ // pb = unofficial language code for Brazilian Portuguese
+ {MAKECODE('\0', '\0', 'p', 'b'), "Portuguese (Brazil)"},
+ {MAKECODE('\0', '\0', 'q', 'u'), "Quechua"},
+ {MAKECODE('\0', '\0', 'r', 'm'), "Romansh"},
+ {MAKECODE('\0', '\0', 'r', 'n'), "Kirundi"},
+ {MAKECODE('\0', '\0', 'r', 'o'), "Romanian"},
+ {MAKECODE('\0', '\0', 'r', 'u'), "Russian"},
+ {MAKECODE('\0', '\0', 'r', 'w'), "Kinyarwanda"},
+ {MAKECODE('\0', '\0', 's', 'a'), "Sanskrit"},
+ {MAKECODE('\0', '\0', 's', 'c'), "Sardinian"},
+ {MAKECODE('\0', '\0', 's', 'd'), "Sindhi"},
+ {MAKECODE('\0', '\0', 's', 'e'), "Northern Sami"},
+ {MAKECODE('\0', '\0', 's', 'g'), "Sangho"},
+ {MAKECODE('\0', '\0', 's', 'h'), "Serbo-Croatian"},
+ {MAKECODE('\0', '\0', 's', 'i'), "Sinhalese"},
+ {MAKECODE('\0', '\0', 's', 'k'), "Slovak"},
+ {MAKECODE('\0', '\0', 's', 'l'), "Slovenian"},
+ {MAKECODE('\0', '\0', 's', 'm'), "Samoan"},
+ {MAKECODE('\0', '\0', 's', 'n'), "Shona"},
+ {MAKECODE('\0', '\0', 's', 'o'), "Somali"},
+ {MAKECODE('\0', '\0', 's', 'q'), "Albanian"},
+ {MAKECODE('\0', '\0', 's', 'r'), "Serbian"},
+ {MAKECODE('\0', '\0', 's', 's'), "Swati"},
+ {MAKECODE('\0', '\0', 's', 't'), "Sesotho"},
+ {MAKECODE('\0', '\0', 's', 'u'), "Sundanese"},
+ {MAKECODE('\0', '\0', 's', 'v'), "Swedish"},
+ {MAKECODE('\0', '\0', 's', 'w'), "Swahili"},
+ {MAKECODE('\0', '\0', 't', 'a'), "Tamil"},
+ {MAKECODE('\0', '\0', 't', 'e'), "Telugu"},
+ {MAKECODE('\0', '\0', 't', 'g'), "Tajik"},
+ {MAKECODE('\0', '\0', 't', 'h'), "Thai"},
+ {MAKECODE('\0', '\0', 't', 'i'), "Tigrinya"},
+ {MAKECODE('\0', '\0', 't', 'k'), "Turkmen"},
+ {MAKECODE('\0', '\0', 't', 'l'), "Tagalog"},
+ {MAKECODE('\0', '\0', 't', 'n'), "Tswana"},
+ {MAKECODE('\0', '\0', 't', 'o'), "Tonga"},
+ {MAKECODE('\0', '\0', 't', 'r'), "Turkish"},
+ {MAKECODE('\0', '\0', 't', 's'), "Tsonga"},
+ {MAKECODE('\0', '\0', 't', 't'), "Tatar"},
+ {MAKECODE('\0', '\0', 't', 'w'), "Twi"},
+ {MAKECODE('\0', '\0', 't', 'y'), "Tahitian"},
+ {MAKECODE('\0', '\0', 'u', 'g'), "Uighur"},
+ {MAKECODE('\0', '\0', 'u', 'k'), "Ukrainian"},
+ {MAKECODE('\0', '\0', 'u', 'r'), "Urdu"},
+ {MAKECODE('\0', '\0', 'u', 'z'), "Uzbek"},
+ {MAKECODE('\0', '\0', 'v', 'e'), "Venda"},
+ {MAKECODE('\0', '\0', 'v', 'i'), "Vietnamese"},
+ {MAKECODE('\0', '\0', 'v', 'o'), "Volapuk"},
+ {MAKECODE('\0', '\0', 'w', 'a'), "Walloon"},
+ {MAKECODE('\0', '\0', 'w', 'o'), "Wolof"},
+ {MAKECODE('\0', '\0', 'x', 'h'), "Xhosa"},
+ {MAKECODE('\0', '\0', 'y', 'i'), "Yiddish"},
+ {MAKECODE('\0', '\0', 'y', 'o'), "Yoruba"},
+ {MAKECODE('\0', '\0', 'z', 'a'), "Zhuang"},
+ {MAKECODE('\0', '\0', 'z', 'h'), "Chinese"},
+ {MAKECODE('\0', '\0', 'z', 'u'), "Zulu"},
+}};
+// clang-format on
+
+// clang-format off
+const std::array<struct LCENTRY, 540> g_iso639_2 = {{
+ {MAKECODE('\0', 'a', 'b', 'k'), "Abkhaz"},
+ {MAKECODE('\0', 'a', 'b', 'k'), "Abkhazian"},
+ {MAKECODE('\0', 'a', 'c', 'e'), "Achinese"},
+ {MAKECODE('\0', 'a', 'c', 'h'), "Acoli"},
+ {MAKECODE('\0', 'a', 'd', 'a'), "Adangme"},
+ {MAKECODE('\0', 'a', 'd', 'y'), "Adygei"},
+ {MAKECODE('\0', 'a', 'd', 'y'), "Adyghe"},
+ {MAKECODE('\0', 'a', 'a', 'r'), "Afar"},
+ {MAKECODE('\0', 'a', 'f', 'h'), "Afrihili"},
+ {MAKECODE('\0', 'a', 'f', 'r'), "Afrikaans"},
+ {MAKECODE('\0', 'a', 'f', 'a'), "Afro-Asiatic (Other)"},
+ {MAKECODE('\0', 'a', 'k', 'a'), "Akan"},
+ {MAKECODE('\0', 'a', 'k', 'k'), "Akkadian"},
+ {MAKECODE('\0', 'a', 'l', 'b'), "Albanian"},
+ {MAKECODE('\0', 's', 'q', 'i'), "Albanian"},
+ {MAKECODE('\0', 'a', 'l', 'e'), "Aleut"},
+ {MAKECODE('\0', 'a', 'l', 'g'), "Algonquian languages"},
+ {MAKECODE('\0', 't', 'u', 't'), "Altaic (Other)"},
+ {MAKECODE('\0', 'a', 'm', 'h'), "Amharic"},
+ {MAKECODE('\0', 'a', 'p', 'a'), "Apache languages"},
+ {MAKECODE('\0', 'a', 'r', 'a'), "Arabic"},
+ {MAKECODE('\0', 'a', 'r', 'g'), "Aragonese"},
+ {MAKECODE('\0', 'a', 'r', 'c'), "Aramaic"},
+ {MAKECODE('\0', 'a', 'r', 'p'), "Arapaho"},
+ {MAKECODE('\0', 'a', 'r', 'n'), "Araucanian"},
+ {MAKECODE('\0', 'a', 'r', 'w'), "Arawak"},
+ {MAKECODE('\0', 'a', 'r', 'm'), "Armenian"},
+ {MAKECODE('\0', 'h', 'y', 'e'), "Armenian"},
+ {MAKECODE('\0', 'a', 'r', 't'), "Artificial (Other)"},
+ {MAKECODE('\0', 'a', 's', 'm'), "Assamese"},
+ {MAKECODE('\0', 'a', 's', 't'), "Asturian"},
+ {MAKECODE('\0', 'a', 't', 'h'), "Athapascan languages"},
+ {MAKECODE('\0', 'a', 'u', 's'), "Australian languages"},
+ {MAKECODE('\0', 'm', 'a', 'p'), "Austronesian (Other)"},
+ {MAKECODE('\0', 'a', 'v', 'a'), "Avaric"},
+ {MAKECODE('\0', 'a', 'v', 'e'), "Avestan"},
+ {MAKECODE('\0', 'a', 'w', 'a'), "Awadhi"},
+ {MAKECODE('\0', 'a', 'y', 'm'), "Aymara"},
+ {MAKECODE('\0', 'a', 'z', 'e'), "Azerbaijani"},
+ {MAKECODE('\0', 'a', 's', 't'), "Bable"},
+ {MAKECODE('\0', 'b', 'a', 'n'), "Balinese"},
+ {MAKECODE('\0', 'b', 'a', 't'), "Baltic (Other)"},
+ {MAKECODE('\0', 'b', 'a', 'l'), "Baluchi"},
+ {MAKECODE('\0', 'b', 'a', 'm'), "Bambara"},
+ {MAKECODE('\0', 'b', 'a', 'i'), "Bamileke languages"},
+ {MAKECODE('\0', 'b', 'a', 'd'), "Banda"},
+ {MAKECODE('\0', 'b', 'n', 't'), "Bantu (Other)"},
+ {MAKECODE('\0', 'b', 'a', 's'), "Basa"},
+ {MAKECODE('\0', 'b', 'a', 'k'), "Bashkir"},
+ {MAKECODE('\0', 'b', 'a', 'q'), "Basque"},
+ {MAKECODE('\0', 'e', 'u', 's'), "Basque"},
+ {MAKECODE('\0', 'b', 't', 'k'), "Batak (Indonesia)"},
+ {MAKECODE('\0', 'b', 'e', 'j'), "Beja"},
+ {MAKECODE('\0', 'b', 'e', 'l'), "Belarusian"},
+ {MAKECODE('\0', 'b', 'e', 'm'), "Bemba"},
+ {MAKECODE('\0', 'b', 'e', 'n'), "Bengali"},
+ {MAKECODE('\0', 'b', 'e', 'r'), "Berber (Other)"},
+ {MAKECODE('\0', 'b', 'h', 'o'), "Bhojpuri"},
+ {MAKECODE('\0', 'b', 'i', 'h'), "Bihari"},
+ {MAKECODE('\0', 'b', 'i', 'k'), "Bikol"},
+ {MAKECODE('\0', 'b', 'y', 'n'), "Bilin"},
+ {MAKECODE('\0', 'b', 'i', 'n'), "Bini"},
+ {MAKECODE('\0', 'b', 'i', 's'), "Bislama"},
+ {MAKECODE('\0', 'b', 'y', 'n'), "Blin"},
+ {MAKECODE('\0', 'n', 'o', 'b'), "Bokm\xC3\xA5l, Norwegian"},
+ {MAKECODE('\0', 'b', 'o', 's'), "Bosnian"},
+ {MAKECODE('\0', 'b', 'r', 'a'), "Braj"},
+ {MAKECODE('\0', 'b', 'r', 'e'), "Breton"},
+ {MAKECODE('\0', 'b', 'u', 'g'), "Buginese"},
+ {MAKECODE('\0', 'b', 'u', 'l'), "Bulgarian"},
+ {MAKECODE('\0', 'b', 'u', 'a'), "Buriat"},
+ {MAKECODE('\0', 'b', 'u', 'r'), "Burmese"},
+ {MAKECODE('\0', 'm', 'y', 'a'), "Burmese"},
+ {MAKECODE('\0', 'c', 'a', 'd'), "Caddo"},
+ {MAKECODE('\0', 'c', 'a', 'r'), "Carib"},
+ {MAKECODE('\0', 's', 'p', 'a'), "Spanish"},
+ {MAKECODE('\0', 'c', 'a', 't'), "Catalan"},
+ {MAKECODE('\0', 'c', 'a', 'u'), "Caucasian (Other)"},
+ {MAKECODE('\0', 'c', 'e', 'b'), "Cebuano"},
+ {MAKECODE('\0', 'c', 'e', 'l'), "Celtic (Other)"},
+ {MAKECODE('\0', 'c', 'h', 'g'), "Chagatai"},
+ {MAKECODE('\0', 'c', 'm', 'c'), "Chamic languages"},
+ {MAKECODE('\0', 'c', 'h', 'a'), "Chamorro"},
+ {MAKECODE('\0', 'c', 'h', 'e'), "Chechen"},
+ {MAKECODE('\0', 'c', 'h', 'r'), "Cherokee"},
+ {MAKECODE('\0', 'n', 'y', 'a'), "Chewa"},
+ {MAKECODE('\0', 'c', 'h', 'y'), "Cheyenne"},
+ {MAKECODE('\0', 'c', 'h', 'b'), "Chibcha"},
+ {MAKECODE('\0', 'n', 'y', 'a'), "Chichewa"},
+ {MAKECODE('\0', 'c', 'h', 'i'), "Chinese"},
+ {MAKECODE('\0', 'z', 'h', 'o'), "Chinese"},
+ {MAKECODE('\0', 'c', 'h', 'n'), "Chinook jargon"},
+ {MAKECODE('\0', 'c', 'h', 'p'), "Chipewyan"},
+ {MAKECODE('\0', 'c', 'h', 'o'), "Choctaw"},
+ {MAKECODE('\0', 'z', 'h', 'a'), "Chuang"},
+ {MAKECODE('\0', 'c', 'h', 'u'), "Church Slavonic"},
+ {MAKECODE('\0', 'c', 'h', 'k'), "Chuukese"},
+ {MAKECODE('\0', 'c', 'h', 'v'), "Chuvash"},
+ {MAKECODE('\0', 'n', 'w', 'c'), "Classical Nepal Bhasa"},
+ {MAKECODE('\0', 'n', 'w', 'c'), "Classical Newari"},
+ {MAKECODE('\0', 'c', 'o', 'p'), "Coptic"},
+ {MAKECODE('\0', 'c', 'o', 'r'), "Cornish"},
+ {MAKECODE('\0', 'c', 'o', 's'), "Corsican"},
+ {MAKECODE('\0', 'c', 'r', 'e'), "Cree"},
+ {MAKECODE('\0', 'm', 'u', 's'), "Creek"},
+ {MAKECODE('\0', 'c', 'r', 'p'), "Creoles and pidgins (Other)"},
+ {MAKECODE('\0', 'c', 'p', 'e'), "English-based (Other)"},
+ {MAKECODE('\0', 'c', 'p', 'f'), "French-based (Other)"},
+ {MAKECODE('\0', 'c', 'p', 'p'), "Portuguese-based (Other)"},
+ {MAKECODE('\0', 'c', 'r', 'h'), "Crimean Tatar"},
+ {MAKECODE('\0', 'c', 'r', 'h'), "Crimean Turkish"},
+ {MAKECODE('\0', 'h', 'r', 'v'), "Croatian"},
+ {MAKECODE('\0', 's', 'c', 'r'), "Croatian"},
+ {MAKECODE('\0', 'c', 'u', 's'), "Cushitic (Other)"},
+ {MAKECODE('\0', 'c', 'z', 'e'), "Czech"},
+ {MAKECODE('\0', 'c', 'e', 's'), "Czech"},
+ {MAKECODE('\0', 'd', 'a', 'k'), "Dakota"},
+ {MAKECODE('\0', 'd', 'a', 'n'), "Danish"},
+ {MAKECODE('\0', 'd', 'a', 'r'), "Dargwa"},
+ {MAKECODE('\0', 'd', 'a', 'y'), "Dayak"},
+ {MAKECODE('\0', 'd', 'e', 'l'), "Delaware"},
+ {MAKECODE('\0', 'd', 'i', 'n'), "Dinka"},
+ {MAKECODE('\0', 'd', 'i', 'v'), "Divehi"},
+ {MAKECODE('\0', 'd', 'o', 'i'), "Dogri"},
+ {MAKECODE('\0', 'd', 'g', 'r'), "Dogrib"},
+ {MAKECODE('\0', 'd', 'r', 'a'), "Dravidian (Other)"},
+ {MAKECODE('\0', 'd', 'u', 'a'), "Duala"},
+ {MAKECODE('\0', 'd', 'u', 't'), "Dutch"},
+ {MAKECODE('\0', 'n', 'l', 'd'), "Dutch"},
+ {MAKECODE('\0', 'd', 'u', 'm'), "Dutch, Middle (ca. 1050-1350)"},
+ {MAKECODE('\0', 'd', 'y', 'u'), "Dyula"},
+ {MAKECODE('\0', 'd', 'z', 'o'), "Dzongkha"},
+ {MAKECODE('\0', 'e', 'f', 'i'), "Efik"},
+ {MAKECODE('\0', 'e', 'g', 'y'), "Egyptian (Ancient)"},
+ {MAKECODE('\0', 'e', 'k', 'a'), "Ekajuk"},
+ {MAKECODE('\0', 'e', 'l', 'x'), "Elamite"},
+ {MAKECODE('\0', 'e', 'n', 'g'), "English"},
+ {MAKECODE('\0', 'e', 'n', 'm'), "English, Middle (1100-1500)"},
+ {MAKECODE('\0', 'a', 'n', 'g'), "English, Old (ca.450-1100)"},
+ {MAKECODE('\0', 'm', 'y', 'v'), "Erzya"},
+ {MAKECODE('\0', 'e', 'p', 'o'), "Esperanto"},
+ {MAKECODE('\0', 'e', 's', 't'), "Estonian"},
+ {MAKECODE('\0', 'e', 'w', 'e'), "Ewe"},
+ {MAKECODE('\0', 'e', 'w', 'o'), "Ewondo"},
+ {MAKECODE('\0', 'f', 'a', 'n'), "Fang"},
+ {MAKECODE('\0', 'f', 'a', 't'), "Fanti"},
+ {MAKECODE('\0', 'f', 'a', 'o'), "Faroese"},
+ {MAKECODE('\0', 'f', 'i', 'j'), "Fijian"},
+ {MAKECODE('\0', 'f', 'i', 'l'), "Filipino"},
+ {MAKECODE('\0', 'f', 'i', 'n'), "Finnish"},
+ {MAKECODE('\0', 'f', 'i', 'u'), "Finno-Ugrian (Other)"},
+ {MAKECODE('\0', 'd', 'u', 't'), "Flemish"},
+ {MAKECODE('\0', 'n', 'l', 'd'), "Flemish"},
+ {MAKECODE('\0', 'f', 'o', 'n'), "Fon"},
+ {MAKECODE('\0', 'f', 'r', 'e'), "French"},
+ {MAKECODE('\0', 'f', 'r', 'a'), "French"},
+ {MAKECODE('\0', 'f', 'r', 'm'), "French, Middle (ca.1400-1600)"},
+ {MAKECODE('\0', 'f', 'r', 'o'), "French, Old (842-ca.1400)"},
+ {MAKECODE('\0', 'f', 'r', 'y'), "Frisian"},
+ {MAKECODE('\0', 'f', 'u', 'r'), "Friulian"},
+ {MAKECODE('\0', 'f', 'u', 'l'), "Fulah"},
+ {MAKECODE('\0', 'g', 'a', 'a'), "Ga"},
+ {MAKECODE('\0', 'g', 'l', 'a'), "Gaelic"},
+ {MAKECODE('\0', 'g', 'l', 'g'), "Gallegan"},
+ {MAKECODE('\0', 'l', 'u', 'g'), "Ganda"},
+ {MAKECODE('\0', 'g', 'a', 'y'), "Gayo"},
+ {MAKECODE('\0', 'g', 'b', 'a'), "Gbaya"},
+ {MAKECODE('\0', 'g', 'e', 'z'), "Geez"},
+ {MAKECODE('\0', 'g', 'e', 'o'), "Georgian"},
+ {MAKECODE('\0', 'k', 'a', 't'), "Georgian"},
+ {MAKECODE('\0', 'g', 'e', 'r'), "German"},
+ {MAKECODE('\0', 'd', 'e', 'u'), "German"},
+ {MAKECODE('\0', 'n', 'd', 's'), "German, Low"},
+ {MAKECODE('\0', 'g', 'm', 'h'), "German, Middle High (ca.1050-1500)"},
+ {MAKECODE('\0', 'g', 'o', 'h'), "German, Old High (ca.750-1050)"},
+ {MAKECODE('\0', 'g', 's', 'w'), "German, Swiss German"},
+ {MAKECODE('\0', 'g', 'e', 'm'), "Germanic (Other)"},
+ {MAKECODE('\0', 'k', 'i', 'k'), "Gikuyu"},
+ {MAKECODE('\0', 'g', 'i', 'l'), "Gilbertese"},
+ {MAKECODE('\0', 'g', 'o', 'n'), "Gondi"},
+ {MAKECODE('\0', 'g', 'o', 'r'), "Gorontalo"},
+ {MAKECODE('\0', 'g', 'o', 't'), "Gothic"},
+ {MAKECODE('\0', 'g', 'r', 'b'), "Grebo"},
+ {MAKECODE('\0', 'g', 'r', 'c'), "Greek, Ancient (to 1453)"},
+ {MAKECODE('\0', 'g', 'r', 'e'), "Greek, Modern (1453-)"},
+ {MAKECODE('\0', 'e', 'l', 'l'), "Greek, Modern (1453-)"},
+ {MAKECODE('\0', 'k', 'a', 'l'), "Greenlandic"},
+ {MAKECODE('\0', 'g', 'r', 'n'), "Guarani"},
+ {MAKECODE('\0', 'g', 'u', 'j'), "Gujarati"},
+ {MAKECODE('\0', 'g', 'w', 'i'), "Gwich\xC2\xB4in"},
+ {MAKECODE('\0', 'h', 'a', 'i'), "Haida"},
+ {MAKECODE('\0', 'h', 'a', 't'), "Haitian"},
+ {MAKECODE('\0', 'h', 'a', 't'), "Haitian Creole"},
+ {MAKECODE('\0', 'h', 'a', 'u'), "Hausa"},
+ {MAKECODE('\0', 'h', 'a', 'w'), "Hawaiian"},
+ {MAKECODE('\0', 'h', 'e', 'b'), "Hebrew"},
+ {MAKECODE('\0', 'h', 'e', 'r'), "Herero"},
+ {MAKECODE('\0', 'h', 'i', 'l'), "Hiligaynon"},
+ {MAKECODE('\0', 'h', 'i', 'm'), "Himachali"},
+ {MAKECODE('\0', 'h', 'i', 'n'), "Hindi"},
+ {MAKECODE('\0', 'h', 'm', 'o'), "Hiri Motu"},
+ {MAKECODE('\0', 'h', 'i', 't'), "Hittite"},
+ {MAKECODE('\0', 'h', 'm', 'n'), "Hmong"},
+ {MAKECODE('\0', 'h', 'u', 'n'), "Hungarian"},
+ {MAKECODE('\0', 'h', 'u', 'p'), "Hupa"},
+ {MAKECODE('\0', 'i', 'b', 'a'), "Iban"},
+ {MAKECODE('\0', 'i', 'c', 'e'), "Icelandic"},
+ {MAKECODE('\0', 'i', 's', 'l'), "Icelandic"},
+ {MAKECODE('\0', 'i', 'd', 'o'), "Ido"},
+ {MAKECODE('\0', 'i', 'b', 'o'), "Igbo"},
+ {MAKECODE('\0', 'i', 'j', 'o'), "Ijo"},
+ {MAKECODE('\0', 'i', 'l', 'o'), "Iloko"},
+ {MAKECODE('\0', 's', 'm', 'n'), "Inari Sami"},
+ {MAKECODE('\0', 'i', 'n', 'c'), "Indic (Other)"},
+ {MAKECODE('\0', 'i', 'n', 'e'), "Indo-European (Other)"},
+ {MAKECODE('\0', 'i', 'n', 'd'), "Indonesian"},
+ {MAKECODE('\0', 'i', 'n', 'h'), "Ingush"},
+ {MAKECODE('\0', 'i', 'n', 'a'), "Auxiliary Language Association)"},
+ {MAKECODE('\0', 'i', 'l', 'e'), "Interlingue"},
+ {MAKECODE('\0', 'i', 'k', 'u'), "Inuktitut"},
+ {MAKECODE('\0', 'i', 'p', 'k'), "Inupiaq"},
+ {MAKECODE('\0', 'i', 'r', 'a'), "Iranian (Other)"},
+ {MAKECODE('\0', 'g', 'l', 'e'), "Irish"},
+ {MAKECODE('\0', 'm', 'g', 'a'), "Irish, Middle (900-1200)"},
+ {MAKECODE('\0', 's', 'g', 'a'), "Irish, Old (to 900)"},
+ {MAKECODE('\0', 'i', 'r', 'o'), "Iroquoian languages"},
+ {MAKECODE('\0', 'i', 't', 'a'), "Italian"},
+ {MAKECODE('\0', 'j', 'p', 'n'), "Japanese"},
+ {MAKECODE('\0', 'j', 'a', 'v'), "Javanese"},
+ {MAKECODE('\0', 'j', 'r', 'b'), "Judeo-Arabic"},
+ {MAKECODE('\0', 'j', 'p', 'r'), "Judeo-Persian"},
+ {MAKECODE('\0', 'k', 'b', 'd'), "Kabardian"},
+ {MAKECODE('\0', 'k', 'a', 'b'), "Kabyle"},
+ {MAKECODE('\0', 'k', 'a', 'c'), "Kachin"},
+ {MAKECODE('\0', 'k', 'a', 'l'), "Kalaallisut"},
+ {MAKECODE('\0', 'x', 'a', 'l'), "Kalmyk"},
+ {MAKECODE('\0', 'k', 'a', 'm'), "Kamba"},
+ {MAKECODE('\0', 'k', 'a', 'n'), "Kannada"},
+ {MAKECODE('\0', 'k', 'a', 'u'), "Kanuri"},
+ {MAKECODE('\0', 'k', 'r', 'c'), "Karachay-Balkar"},
+ {MAKECODE('\0', 'k', 'a', 'a'), "Kara-Kalpak"},
+ {MAKECODE('\0', 'k', 'a', 'r'), "Karen"},
+ {MAKECODE('\0', 'k', 'a', 's'), "Kashmiri"},
+ {MAKECODE('\0', 'c', 's', 'b'), "Kashubian"},
+ {MAKECODE('\0', 'k', 'a', 'w'), "Kawi"},
+ {MAKECODE('\0', 'k', 'a', 'z'), "Kazakh"},
+ {MAKECODE('\0', 'k', 'h', 'a'), "Khasi"},
+ {MAKECODE('\0', 'k', 'h', 'm'), "Khmer"},
+ {MAKECODE('\0', 'k', 'h', 'i'), "Khoisan (Other)"},
+ {MAKECODE('\0', 'k', 'h', 'o'), "Khotanese"},
+ {MAKECODE('\0', 'k', 'i', 'k'), "Kikuyu"},
+ {MAKECODE('\0', 'k', 'm', 'b'), "Kimbundu"},
+ {MAKECODE('\0', 'k', 'i', 'n'), "Kinyarwanda"},
+ {MAKECODE('\0', 'k', 'i', 'r'), "Kirghiz"},
+ {MAKECODE('\0', 't', 'l', 'h'), "Klingon"},
+ {MAKECODE('\0', 'k', 'o', 'm'), "Komi"},
+ {MAKECODE('\0', 'k', 'o', 'n'), "Kongo"},
+ {MAKECODE('\0', 'k', 'o', 'k'), "Konkani"},
+ {MAKECODE('\0', 'k', 'o', 'r'), "Korean"},
+ {MAKECODE('\0', 'k', 'o', 's'), "Kosraean"},
+ {MAKECODE('\0', 'k', 'p', 'e'), "Kpelle"},
+ {MAKECODE('\0', 'k', 'r', 'o'), "Kru"},
+ {MAKECODE('\0', 'k', 'u', 'a'), "Kuanyama"},
+ {MAKECODE('\0', 'k', 'u', 'm'), "Kumyk"},
+ {MAKECODE('\0', 'k', 'u', 'r'), "Kurdish"},
+ {MAKECODE('\0', 'k', 'r', 'u'), "Kurukh"},
+ {MAKECODE('\0', 'k', 'u', 't'), "Kutenai"},
+ {MAKECODE('\0', 'k', 'u', 'a'), "Kwanyama, Kuanyama"},
+ {MAKECODE('\0', 'l', 'a', 'd'), "Ladino"},
+ {MAKECODE('\0', 'l', 'a', 'h'), "Lahnda"},
+ {MAKECODE('\0', 'l', 'a', 'm'), "Lamba"},
+ {MAKECODE('\0', 'l', 'a', 'o'), "Lao"},
+ {MAKECODE('\0', 'l', 'a', 't'), "Latin"},
+ {MAKECODE('\0', 'l', 'a', 'v'), "Latvian"},
+ {MAKECODE('\0', 'l', 't', 'z'), "Letzeburgesch"},
+ {MAKECODE('\0', 'l', 'e', 'z'), "Lezghian"},
+ {MAKECODE('\0', 'l', 'i', 'm'), "Limburgan"},
+ {MAKECODE('\0', 'l', 'i', 'm'), "Limburger"},
+ {MAKECODE('\0', 'l', 'i', 'm'), "Limburgish"},
+ {MAKECODE('\0', 'l', 'i', 'n'), "Lingala"},
+ {MAKECODE('\0', 'l', 'i', 't'), "Lithuanian"},
+ {MAKECODE('\0', 'j', 'b', 'o'), "Lojban"},
+ {MAKECODE('\0', 'n', 'd', 's'), "Low German"},
+ {MAKECODE('\0', 'n', 'd', 's'), "Low Saxon"},
+ {MAKECODE('\0', 'd', 's', 'b'), "Lower Sorbian"},
+ {MAKECODE('\0', 'l', 'o', 'z'), "Lozi"},
+ {MAKECODE('\0', 'l', 'u', 'b'), "Luba-Katanga"},
+ {MAKECODE('\0', 'l', 'u', 'a'), "Luba-Lulua"},
+ {MAKECODE('\0', 'l', 'u', 'i'), "Luiseno"},
+ {MAKECODE('\0', 's', 'm', 'j'), "Lule Sami"},
+ {MAKECODE('\0', 'l', 'u', 'n'), "Lunda"},
+ {MAKECODE('\0', 'l', 'u', 'o'), "Luo (Kenya and Tanzania)"},
+ {MAKECODE('\0', 'l', 'u', 's'), "Lushai"},
+ {MAKECODE('\0', 'l', 't', 'z'), "Luxembourgish"},
+ {MAKECODE('\0', 'm', 'a', 'c'), "Macedonian"},
+ {MAKECODE('\0', 'm', 'k', 'd'), "Macedonian"},
+ {MAKECODE('\0', 'm', 'a', 'd'), "Madurese"},
+ {MAKECODE('\0', 'm', 'a', 'g'), "Magahi"},
+ {MAKECODE('\0', 'm', 'a', 'i'), "Maithili"},
+ {MAKECODE('\0', 'm', 'a', 'k'), "Makasar"},
+ {MAKECODE('\0', 'm', 'l', 'g'), "Malagasy"},
+ {MAKECODE('\0', 'm', 'a', 'y'), "Malay"},
+ {MAKECODE('\0', 'm', 's', 'a'), "Malay"},
+ {MAKECODE('\0', 'm', 'a', 'l'), "Malayalam"},
+ {MAKECODE('\0', 'm', 'l', 't'), "Maltese"},
+ {MAKECODE('\0', 'm', 'n', 'c'), "Manchu"},
+ {MAKECODE('\0', 'm', 'd', 'r'), "Mandar"},
+ {MAKECODE('\0', 'm', 'a', 'n'), "Mandingo"},
+ {MAKECODE('\0', 'm', 'n', 'i'), "Manipuri"},
+ {MAKECODE('\0', 'm', 'n', 'o'), "Manobo languages"},
+ {MAKECODE('\0', 'g', 'l', 'v'), "Manx"},
+ {MAKECODE('\0', 'm', 'a', 'o'), "Maori"},
+ {MAKECODE('\0', 'm', 'r', 'i'), "Maori"},
+ {MAKECODE('\0', 'm', 'a', 'r'), "Marathi"},
+ {MAKECODE('\0', 'c', 'h', 'm'), "Mari"},
+ {MAKECODE('\0', 'm', 'a', 'h'), "Marshallese"},
+ {MAKECODE('\0', 'm', 'w', 'r'), "Marwari"},
+ {MAKECODE('\0', 'm', 'a', 's'), "Masai"},
+ {MAKECODE('\0', 'm', 'y', 'n'), "Mayan languages"},
+ {MAKECODE('\0', 'm', 'e', 'n'), "Mende"},
+ {MAKECODE('\0', 'm', 'i', 'c'), "Micmac"},
+ {MAKECODE('\0', 'm', 'i', 'c'), "Mi'kmaq"},
+ {MAKECODE('\0', 'm', 'i', 'n'), "Minangkabau"},
+ {MAKECODE('\0', 'm', 'w', 'l'), "Mirandese"},
+ {MAKECODE('\0', 'm', 'i', 's'), "Miscellaneous languages"},
+ {MAKECODE('\0', 'm', 'o', 'h'), "Mohawk"},
+ {MAKECODE('\0', 'm', 'd', 'f'), "Moksha"},
+ {MAKECODE('\0', 'm', 'o', 'l'), "Moldavian"},
+ {MAKECODE('\0', 'm', 'k', 'h'), "Mon-Khmer (Other)"},
+ {MAKECODE('\0', 'l', 'o', 'l'), "Mongo"},
+ {MAKECODE('\0', 'm', 'o', 'n'), "Mongolian"},
+ {MAKECODE('\0', 'm', 'o', 's'), "Mossi"},
+ {MAKECODE('\0', 'm', 'u', 'l'), "Multiple languages"},
+ {MAKECODE('\0', 'm', 'u', 'n'), "Munda languages"},
+ {MAKECODE('\0', 'n', 'a', 'h'), "Nahuatl"},
+ {MAKECODE('\0', 'n', 'a', 'u'), "Nauru"},
+ {MAKECODE('\0', 'n', 'a', 'v'), "Navaho, Navajo"},
+ {MAKECODE('\0', 'n', 'a', 'v'), "Navajo"},
+ {MAKECODE('\0', 'n', 'd', 'e'), "Ndebele, North"},
+ {MAKECODE('\0', 'n', 'b', 'l'), "Ndebele, South"},
+ {MAKECODE('\0', 'n', 'd', 'o'), "Ndonga"},
+ {MAKECODE('\0', 'n', 'a', 'p'), "Neapolitan"},
+ {MAKECODE('\0', 'n', 'e', 'w'), "Nepal Bhasa"},
+ {MAKECODE('\0', 'n', 'e', 'p'), "Nepali"},
+ {MAKECODE('\0', 'n', 'e', 'w'), "Newari"},
+ {MAKECODE('\0', 'n', 'i', 'a'), "Nias"},
+ {MAKECODE('\0', 'n', 'i', 'c'), "Niger-Kordofanian (Other)"},
+ {MAKECODE('\0', 's', 's', 'a'), "Nilo-Saharan (Other)"},
+ {MAKECODE('\0', 'n', 'i', 'u'), "Niuean"},
+ {MAKECODE('\0', 'z', 'x', 'x'), "No linguistic content"},
+ {MAKECODE('\0', 'n', 'o', 'g'), "Nogai"},
+ {MAKECODE('\0', 'n', 'o', 'n'), "Norse, Old"},
+ {MAKECODE('\0', 'n', 'a', 'i'), "North American Indian (Other)"},
+ {MAKECODE('\0', 's', 'm', 'e'), "Northern Sami"},
+ {MAKECODE('\0', 'n', 's', 'o'), "Northern Sotho"},
+ {MAKECODE('\0', 'n', 'd', 'e'), "North Ndebele"},
+ {MAKECODE('\0', 'n', 'o', 'r'), "Norwegian"},
+ {MAKECODE('\0', 'n', 'o', 'b'), "Norwegian Bokm\xC3\xA5l"},
+ {MAKECODE('\0', 'n', 'n', 'o'), "Norwegian Nynorsk"},
+ {MAKECODE('\0', 'n', 'u', 'b'), "Nubian languages"},
+ {MAKECODE('\0', 'n', 'y', 'm'), "Nyamwezi"},
+ {MAKECODE('\0', 'n', 'y', 'a'), "Nyanja"},
+ {MAKECODE('\0', 'n', 'y', 'n'), "Nyankole"},
+ {MAKECODE('\0', 'n', 'n', 'o'), "Nynorsk, Norwegian"},
+ {MAKECODE('\0', 'n', 'y', 'o'), "Nyoro"},
+ {MAKECODE('\0', 'n', 'z', 'i'), "Nzima"},
+ {MAKECODE('\0', 'o', 'c', 'i'), "Occitan (post 1500)"},
+ {MAKECODE('\0', 'o', 'j', 'i'), "Ojibwa"},
+ {MAKECODE('\0', 'c', 'h', 'u'), "Old Bulgarian"},
+ {MAKECODE('\0', 'c', 'h', 'u'), "Old Church Slavonic"},
+ {MAKECODE('\0', 'n', 'w', 'c'), "Old Newari"},
+ {MAKECODE('\0', 'c', 'h', 'u'), "Old Slavonic"},
+ {MAKECODE('\0', 'o', 'r', 'i'), "Oriya"},
+ {MAKECODE('\0', 'o', 'r', 'm'), "Oromo"},
+ {MAKECODE('\0', 'o', 's', 'a'), "Osage"},
+ {MAKECODE('\0', 'o', 's', 's'), "Ossetian"},
+ {MAKECODE('\0', 'o', 's', 's'), "Ossetic"},
+ {MAKECODE('\0', 'o', 't', 'o'), "Otomian languages"},
+ {MAKECODE('\0', 'p', 'a', 'l'), "Pahlavi"},
+ {MAKECODE('\0', 'p', 'a', 'u'), "Palauan"},
+ {MAKECODE('\0', 'p', 'l', 'i'), "Pali"},
+ {MAKECODE('\0', 'p', 'a', 'm'), "Pampanga"},
+ {MAKECODE('\0', 'p', 'a', 'g'), "Pangasinan"},
+ {MAKECODE('\0', 'p', 'a', 'n'), "Panjabi"},
+ {MAKECODE('\0', 'p', 'a', 'p'), "Papiamento"},
+ {MAKECODE('\0', 'p', 'a', 'a'), "Papuan (Other)"},
+ {MAKECODE('\0', 'n', 's', 'o'), "Pedi"},
+ {MAKECODE('\0', 'p', 'e', 'r'), "Persian"},
+ {MAKECODE('\0', 'f', 'a', 's'), "Persian"},
+ {MAKECODE('\0', 'p', 'e', 'o'), "Persian, Old (ca.600-400 B.C.)"},
+ {MAKECODE('\0', 'p', 'h', 'i'), "Philippine (Other)"},
+ {MAKECODE('\0', 'p', 'h', 'n'), "Phoenician"},
+ {MAKECODE('\0', 'f', 'i', 'l'), "Pilipino"},
+ {MAKECODE('\0', 'p', 'o', 'n'), "Pohnpeian"},
+ {MAKECODE('\0', 'p', 'o', 'l'), "Polish"},
+ {MAKECODE('\0', 'p', 'o', 'r'), "Portuguese"},
+ // pob = unofficial language code for Brazilian Portuguese
+ {MAKECODE('\0', 'p', 'o', 'b'), "Portuguese (Brazil)"},
+ {MAKECODE('\0', 'p', 'r', 'a'), "Prakrit languages"},
+ {MAKECODE('\0', 'o', 'c', 'i'), "Proven\xC3\xA7"
+ "al"},
+ {MAKECODE('\0', 'p', 'r', 'o'), "Proven\xC3\xA7"
+ "al, Old (to 1500)"},
+ {MAKECODE('\0', 'p', 'a', 'n'), "Punjabi"},
+ {MAKECODE('\0', 'p', 'u', 's'), "Pushto"},
+ {MAKECODE('\0', 'q', 'u', 'e'), "Quechua"},
+ {MAKECODE('\0', 'r', 'o', 'h'), "Raeto-Romance"},
+ {MAKECODE('\0', 'r', 'a', 'j'), "Rajasthani"},
+ {MAKECODE('\0', 'r', 'a', 'p'), "Rapanui"},
+ {MAKECODE('\0', 'r', 'a', 'r'), "Rarotongan"},
+ // { "qaa-qtz", "Reserved for local use" },
+ {MAKECODE('\0', 'r', 'o', 'a'), "Romance (Other)"},
+ {MAKECODE('\0', 'r', 'u', 'm'), "Romanian"},
+ {MAKECODE('\0', 'r', 'o', 'n'), "Romanian"},
+ {MAKECODE('\0', 'r', 'o', 'm'), "Romany"},
+ {MAKECODE('\0', 'r', 'u', 'n'), "Rundi"},
+ {MAKECODE('\0', 'r', 'u', 's'), "Russian"},
+ {MAKECODE('\0', 's', 'a', 'l'), "Salishan languages"},
+ {MAKECODE('\0', 's', 'a', 'm'), "Samaritan Aramaic"},
+ {MAKECODE('\0', 's', 'm', 'i'), "Sami languages (Other)"},
+ {MAKECODE('\0', 's', 'm', 'o'), "Samoan"},
+ {MAKECODE('\0', 's', 'a', 'd'), "Sandawe"},
+ {MAKECODE('\0', 's', 'a', 'g'), "Sango"},
+ {MAKECODE('\0', 's', 'a', 'n'), "Sanskrit"},
+ {MAKECODE('\0', 's', 'a', 't'), "Santali"},
+ {MAKECODE('\0', 's', 'r', 'd'), "Sardinian"},
+ {MAKECODE('\0', 's', 'a', 's'), "Sasak"},
+ {MAKECODE('\0', 'n', 'd', 's'), "Saxon, Low"},
+ {MAKECODE('\0', 's', 'c', 'o'), "Scots"},
+ {MAKECODE('\0', 'g', 'l', 'a'), "Scottish Gaelic"},
+ {MAKECODE('\0', 's', 'e', 'l'), "Selkup"},
+ {MAKECODE('\0', 's', 'e', 'm'), "Semitic (Other)"},
+ {MAKECODE('\0', 'n', 's', 'o'), "Sepedi"},
+ {MAKECODE('\0', 's', 'c', 'c'), "Serbian"},
+ {MAKECODE('\0', 's', 'r', 'p'), "Serbian"},
+ {MAKECODE('\0', 's', 'r', 'r'), "Serer"},
+ {MAKECODE('\0', 's', 'h', 'n'), "Shan"},
+ {MAKECODE('\0', 's', 'n', 'a'), "Shona"},
+ {MAKECODE('\0', 'i', 'i', 'i'), "Sichuan Yi"},
+ {MAKECODE('\0', 's', 'c', 'n'), "Sicilian"},
+ {MAKECODE('\0', 's', 'i', 'd'), "Sidamo"},
+ {MAKECODE('\0', 's', 'g', 'n'), "Sign languages"},
+ {MAKECODE('\0', 'b', 'l', 'a'), "Siksika"},
+ {MAKECODE('\0', 's', 'n', 'd'), "Sindhi"},
+ {MAKECODE('\0', 's', 'i', 'n'), "Sinhala"},
+ {MAKECODE('\0', 's', 'i', 'n'), "Sinhalese"},
+ {MAKECODE('\0', 's', 'i', 't'), "Sino-Tibetan (Other)"},
+ {MAKECODE('\0', 's', 'i', 'o'), "Siouan languages"},
+ {MAKECODE('\0', 's', 'm', 's'), "Skolt Sami"},
+ {MAKECODE('\0', 'd', 'e', 'n'), "Slave (Athapascan)"},
+ {MAKECODE('\0', 's', 'l', 'a'), "Slavic (Other)"},
+ {MAKECODE('\0', 's', 'l', 'o'), "Slovak"},
+ {MAKECODE('\0', 's', 'l', 'k'), "Slovak"},
+ {MAKECODE('\0', 's', 'l', 'v'), "Slovenian"},
+ {MAKECODE('\0', 's', 'o', 'g'), "Sogdian"},
+ {MAKECODE('\0', 's', 'o', 'm'), "Somali"},
+ {MAKECODE('\0', 's', 'o', 'n'), "Songhai"},
+ {MAKECODE('\0', 's', 'n', 'k'), "Soninke"},
+ {MAKECODE('\0', 'w', 'e', 'n'), "Sorbian languages"},
+ {MAKECODE('\0', 'n', 's', 'o'), "Sotho, Northern"},
+ {MAKECODE('\0', 's', 'o', 't'), "Sotho, Southern"},
+ {MAKECODE('\0', 's', 'a', 'i'), "South American Indian (Other)"},
+ {MAKECODE('\0', 's', 'm', 'a'), "Southern Sami"},
+ {MAKECODE('\0', 'n', 'b', 'l'), "South Ndebele"},
+ {MAKECODE('\0', 's', 'p', 'a'), "Castilian"},
+ {MAKECODE('\0', 's', 'u', 'k'), "Sukuma"},
+ {MAKECODE('\0', 's', 'u', 'x'), "Sumerian"},
+ {MAKECODE('\0', 's', 'u', 'n'), "Sundanese"},
+ {MAKECODE('\0', 's', 'u', 's'), "Susu"},
+ {MAKECODE('\0', 's', 'w', 'a'), "Swahili"},
+ {MAKECODE('\0', 's', 's', 'w'), "Swati"},
+ {MAKECODE('\0', 's', 'w', 'e'), "Swedish"},
+ {MAKECODE('\0', 's', 'y', 'r'), "Syriac"},
+ {MAKECODE('\0', 't', 'g', 'l'), "Tagalog"},
+ {MAKECODE('\0', 't', 'a', 'h'), "Tahitian"},
+ {MAKECODE('\0', 't', 'a', 'i'), "Tai (Other)"},
+ {MAKECODE('\0', 't', 'g', 'k'), "Tajik"},
+ {MAKECODE('\0', 't', 'm', 'h'), "Tamashek"},
+ {MAKECODE('\0', 't', 'a', 'm'), "Tamil"},
+ {MAKECODE('\0', 't', 'a', 't'), "Tatar"},
+ {MAKECODE('\0', 't', 'e', 'l'), "Telugu"},
+ {MAKECODE('\0', 't', 'e', 'r'), "Tereno"},
+ {MAKECODE('\0', 't', 'e', 't'), "Tetum"},
+ {MAKECODE('\0', 't', 'h', 'a'), "Thai"},
+ {MAKECODE('\0', 't', 'i', 'b'), "Tibetan"},
+ {MAKECODE('\0', 'b', 'o', 'd'), "Tibetan"},
+ {MAKECODE('\0', 't', 'i', 'g'), "Tigre"},
+ {MAKECODE('\0', 't', 'i', 'r'), "Tigrinya"},
+ {MAKECODE('\0', 't', 'e', 'm'), "Timne"},
+ {MAKECODE('\0', 't', 'i', 'v'), "Tiv"},
+ {MAKECODE('\0', 't', 'l', 'h'), "tlhIngan-Hol"},
+ {MAKECODE('\0', 't', 'l', 'i'), "Tlingit"},
+ {MAKECODE('\0', 't', 'p', 'i'), "Tok Pisin"},
+ {MAKECODE('\0', 't', 'k', 'l'), "Tokelau"},
+ {MAKECODE('\0', 't', 'o', 'g'), "Tonga (Nyasa)"},
+ {MAKECODE('\0', 't', 'o', 'n'), "Tonga (Tonga Islands)"},
+ {MAKECODE('\0', 't', 's', 'i'), "Tsimshian"},
+ {MAKECODE('\0', 't', 's', 'o'), "Tsonga"},
+ {MAKECODE('\0', 't', 's', 'n'), "Tswana"},
+ {MAKECODE('\0', 't', 'u', 'm'), "Tumbuka"},
+ {MAKECODE('\0', 't', 'u', 'p'), "Tupi languages"},
+ {MAKECODE('\0', 't', 'u', 'r'), "Turkish"},
+ {MAKECODE('\0', 'o', 't', 'a'), "Turkish, Ottoman (1500-1928)"},
+ {MAKECODE('\0', 't', 'u', 'k'), "Turkmen"},
+ {MAKECODE('\0', 't', 'v', 'l'), "Tuvalu"},
+ {MAKECODE('\0', 't', 'y', 'v'), "Tuvinian"},
+ {MAKECODE('\0', 't', 'w', 'i'), "Twi"},
+ {MAKECODE('\0', 'u', 'd', 'm'), "Udmurt"},
+ {MAKECODE('\0', 'u', 'g', 'a'), "Ugaritic"},
+ {MAKECODE('\0', 'u', 'i', 'g'), "Uighur"},
+ {MAKECODE('\0', 'u', 'k', 'r'), "Ukrainian"},
+ {MAKECODE('\0', 'u', 'm', 'b'), "Umbundu"},
+ {MAKECODE('\0', 'u', 'n', 'd'), "Undetermined"},
+ {MAKECODE('\0', 'h', 's', 'b'), "Upper Sorbian"},
+ {MAKECODE('\0', 'u', 'r', 'd'), "Urdu"},
+ {MAKECODE('\0', 'u', 'i', 'g'), "Uyghur"},
+ {MAKECODE('\0', 'u', 'z', 'b'), "Uzbek"},
+ {MAKECODE('\0', 'v', 'a', 'i'), "Vai"},
+ {MAKECODE('\0', 'c', 'a', 't'), "Valencian"},
+ {MAKECODE('\0', 'v', 'e', 'n'), "Venda"},
+ {MAKECODE('\0', 'v', 'i', 'e'), "Vietnamese"},
+ {MAKECODE('\0', 'v', 'o', 'l'), "Volap\xC3\xBCk"},
+ {MAKECODE('\0', 'v', 'o', 't'), "Votic"},
+ {MAKECODE('\0', 'w', 'a', 'k'), "Wakashan languages"},
+ {MAKECODE('\0', 'w', 'a', 'l'), "Walamo"},
+ {MAKECODE('\0', 'w', 'l', 'n'), "Walloon"},
+ {MAKECODE('\0', 'w', 'a', 'r'), "Waray"},
+ {MAKECODE('\0', 'w', 'a', 's'), "Washo"},
+ {MAKECODE('\0', 'w', 'e', 'l'), "Welsh"},
+ {MAKECODE('\0', 'c', 'y', 'm'), "Welsh"},
+ {MAKECODE('\0', 'w', 'o', 'l'), "Wolof"},
+ {MAKECODE('\0', 'x', 'h', 'o'), "Xhosa"},
+ {MAKECODE('\0', 's', 'a', 'h'), "Yakut"},
+ {MAKECODE('\0', 'y', 'a', 'o'), "Yao"},
+ {MAKECODE('\0', 'y', 'a', 'p'), "Yapese"},
+ {MAKECODE('\0', 'y', 'i', 'd'), "Yiddish"},
+ {MAKECODE('\0', 'y', 'o', 'r'), "Yoruba"},
+ {MAKECODE('\0', 'y', 'p', 'k'), "Yupik languages"},
+ {MAKECODE('\0', 'z', 'n', 'd'), "Zande"},
+ {MAKECODE('\0', 'z', 'a', 'p'), "Zapotec"},
+ {MAKECODE('\0', 'z', 'e', 'n'), "Zenaga"},
+ {MAKECODE('\0', 'z', 'h', 'a'), "Zhuang"},
+ {MAKECODE('\0', 'z', 'u', 'l'), "Zulu"},
+ {MAKECODE('\0', 'z', 'u', 'n'), "Zuni"},
+}};
+// clang-format on
+
+// clang-format off
+const std::array<ISO639, 190> LanguageCodes = {{
+ {"aa", "aar", NULL, NULL},
+ {"ab", "abk", NULL, NULL},
+ {"af", "afr", NULL, NULL},
+ {"ak", "aka", NULL, NULL},
+ {"am", "amh", NULL, NULL},
+ {"ar", "ara", NULL, NULL},
+ {"an", "arg", NULL, NULL},
+ {"as", "asm", NULL, NULL},
+ {"av", "ava", NULL, NULL},
+ {"ae", "ave", NULL, NULL},
+ {"ay", "aym", NULL, NULL},
+ {"az", "aze", NULL, NULL},
+ {"ba", "bak", NULL, NULL},
+ {"bm", "bam", NULL, NULL},
+ {"be", "bel", NULL, NULL},
+ {"bn", "ben", NULL, NULL},
+ {"bh", "bih", NULL, NULL},
+ {"bi", "bis", NULL, NULL},
+ {"bo", "tib", NULL, "bod"},
+ {"bs", "bos", NULL, NULL},
+ {"br", "bre", NULL, NULL},
+ {"bg", "bul", NULL, NULL},
+ {"ca", "cat", NULL, NULL},
+ {"cs", "cze", "ces", "ces"},
+ {"ch", "cha", NULL, NULL},
+ {"ce", "che", NULL, NULL},
+ {"cu", "chu", NULL, NULL},
+ {"cv", "chv", NULL, NULL},
+ {"kw", "cor", NULL, NULL},
+ {"co", "cos", NULL, NULL},
+ {"cr", "cre", NULL, NULL},
+ {"cy", "wel", NULL, "cym"},
+ {"da", "dan", NULL, NULL},
+ {"de", "ger", "deu", "deu"},
+ {"dv", "div", NULL, NULL},
+ {"dz", "dzo", NULL, NULL},
+ {"el", "gre", "ell", "ell"},
+ {"en", "eng", NULL, NULL},
+ {"eo", "epo", NULL, NULL},
+ {"et", "est", NULL, NULL},
+ {"eu", "baq", NULL, "eus"},
+ {"ee", "ewe", NULL, NULL},
+ {"fo", "fao", NULL, NULL},
+ {"fa", "per", NULL, "fas"},
+ {"fj", "fij", NULL, NULL},
+ {"fi", "fin", NULL, NULL},
+ {"fr", "fre", "fra", "fra"},
+ {"fy", "fry", NULL, NULL},
+ {"ff", "ful", NULL, NULL},
+ {"gd", "gla", NULL, NULL},
+ {"ga", "gle", NULL, NULL},
+ {"gl", "glg", NULL, NULL},
+ {"gv", "glv", NULL, NULL},
+ {"gn", "grn", NULL, NULL},
+ {"gu", "guj", NULL, NULL},
+ {"ht", "hat", NULL, NULL},
+ {"ha", "hau", NULL, NULL},
+ {"he", "heb", NULL, NULL},
+ {"hz", "her", NULL, NULL},
+ {"hi", "hin", NULL, NULL},
+ {"ho", "hmo", NULL, NULL},
+ {"hr", "hrv", NULL, NULL},
+ {"hu", "hun", NULL, NULL},
+ {"hy", "arm", NULL, "hye"},
+ {"ig", "ibo", NULL, NULL},
+ {"io", "ido", NULL, NULL},
+ {"ii", "iii", NULL, NULL},
+ {"iu", "iku", NULL, NULL},
+ {"ie", "ile", NULL, NULL},
+ {"ia", "ina", NULL, NULL},
+ {"id", "ind", NULL, NULL},
+ {"ik", "ipk", NULL, NULL},
+ {"is", "ice", "isl", "isl"},
+ {"it", "ita", NULL, NULL},
+ {"jv", "jav", NULL, NULL},
+ {"ja", "jpn", NULL, NULL},
+ {"kl", "kal", NULL, NULL},
+ {"kn", "kan", NULL, NULL},
+ {"ks", "kas", NULL, NULL},
+ {"ka", "geo", NULL, "kat"},
+ {"kr", "kau", NULL, NULL},
+ {"kk", "kaz", NULL, NULL},
+ {"km", "khm", NULL, NULL},
+ {"ki", "kik", NULL, NULL},
+ {"rw", "kin", NULL, NULL},
+ {"ky", "kir", NULL, NULL},
+ {"kv", "kom", NULL, NULL},
+ {"kg", "kon", NULL, NULL},
+ {"ko", "kor", NULL, NULL},
+ {"kj", "kua", NULL, NULL},
+ {"ku", "kur", NULL, NULL},
+ {"lo", "lao", NULL, NULL},
+ {"la", "lat", NULL, NULL},
+ {"lv", "lav", NULL, NULL},
+ {"li", "lim", NULL, NULL},
+ {"ln", "lin", NULL, NULL},
+ {"lt", "lit", NULL, NULL},
+ {"lb", "ltz", NULL, NULL},
+ {"lu", "lub", NULL, NULL},
+ {"lg", "lug", NULL, NULL},
+ {"mk", "mac", NULL, "mdk"},
+ {"mh", "mah", NULL, NULL},
+ {"ml", "mal", NULL, NULL},
+ {"mi", "mao", NULL, "mri"},
+ {"mr", "mar", NULL, NULL},
+ {"ms", "may", NULL, "msa"},
+ {"mg", "mlg", NULL, NULL},
+ {"mt", "mlt", NULL, NULL},
+ {"mn", "mon", NULL, NULL},
+ {"my", "bur", NULL, "mya"},
+ {"na", "nau", NULL, NULL},
+ {"nv", "nav", NULL, NULL},
+ {"nr", "nbl", NULL, NULL},
+ {"nd", "nde", NULL, NULL},
+ {"ng", "ndo", NULL, NULL},
+ {"ne", "nep", NULL, NULL},
+ {"nl", "dut", "nld", "nld"},
+ {"nn", "nno", NULL, NULL},
+ {"nb", "nob", NULL, NULL},
+ {"no", "nor", NULL, NULL},
+ {"ny", "nya", NULL, NULL},
+ {"oc", "oci", NULL, NULL},
+ {"oj", "oji", NULL, NULL},
+ {"or", "ori", NULL, NULL},
+ {"om", "orm", NULL, NULL},
+ {"os", "oss", NULL, NULL},
+ {"pa", "pan", NULL, NULL},
+ // pb / pob = unofficial language code for Brazilian Portuguese
+ {"pb", "pob", NULL, NULL},
+ {"pi", "pli", NULL, NULL},
+ {"pl", "pol", "plk", NULL},
+ {"pt", "por", "ptg", NULL},
+ {"ps", "pus", NULL, NULL},
+ {"qu", "que", NULL, NULL},
+ {"rm", "roh", NULL, NULL},
+ {"ro", "rum", "ron", "ron"},
+ {"rn", "run", NULL, NULL},
+ {"ru", "rus", NULL, NULL},
+ {"sh", "scr", NULL, NULL},
+ {"sg", "sag", NULL, NULL},
+ {"sa", "san", NULL, NULL},
+ {"si", "sin", NULL, NULL},
+ {"sk", "slo", "sky", "slk"},
+ {"sl", "slv", NULL, NULL},
+ {"se", "sme", NULL, NULL},
+ {"sm", "smo", NULL, NULL},
+ {"sn", "sna", NULL, NULL},
+ {"sd", "snd", NULL, NULL},
+ {"so", "som", NULL, NULL},
+ {"st", "sot", NULL, NULL},
+ {"es", "spa", "esp", NULL},
+ {"sq", "alb", NULL, "sqi"},
+ {"sc", "srd", NULL, NULL},
+ {"sr", "srp", NULL, NULL},
+ {"ss", "ssw", NULL, NULL},
+ {"su", "sun", NULL, NULL},
+ {"sw", "swa", NULL, NULL},
+ {"sv", "swe", "sve", NULL},
+ {"ty", "tah", NULL, NULL},
+ {"ta", "tam", NULL, NULL},
+ {"tt", "tat", NULL, NULL},
+ {"te", "tel", NULL, NULL},
+ {"tg", "tgk", NULL, NULL},
+ {"tl", "tgl", NULL, NULL},
+ {"th", "tha", NULL, NULL},
+ {"ti", "tir", NULL, NULL},
+ {"to", "ton", NULL, NULL},
+ {"tn", "tsn", NULL, NULL},
+ {"ts", "tso", NULL, NULL},
+ {"tk", "tuk", NULL, NULL},
+ {"tr", "tur", "trk", NULL},
+ {"tw", "twi", NULL, NULL},
+ {"ug", "uig", NULL, NULL},
+ {"uk", "ukr", NULL, NULL},
+ {"ur", "urd", NULL, NULL},
+ {"uz", "uzb", NULL, NULL},
+ {"ve", "ven", NULL, NULL},
+ {"vi", "vie", NULL, NULL},
+ {"vo", "vol", NULL, NULL},
+ {"wa", "wln", NULL, NULL},
+ {"wo", "wol", NULL, NULL},
+ {"xh", "xho", NULL, NULL},
+ {"yi", "yid", NULL, NULL},
+ {"yo", "yor", NULL, NULL},
+ {"za", "zha", NULL, NULL},
+ {"zh", "chi", "zho", "zho"},
+ {"zu", "zul", NULL, NULL},
+ {"zv", "und", NULL, NULL}, // Kodi intern mapping for missing "Undetermined" iso639-1 code
+ {"zx", "zxx", NULL,
+ NULL}, // Kodi intern mapping for missing "No linguistic content" iso639-1 code
+ {"zy", "mis", NULL,
+ NULL}, // Kodi intern mapping for missing "Miscellaneous languages" iso639-1 code
+ {"zz", "mul", NULL, NULL} // Kodi intern mapping for missing "Multiple languages" iso639-1 code
+}};
+// clang-format on
+
+// Based on ISO 3166
+// clang-format off
+const std::array<ISO3166_1, 245> RegionCodes = {{
+ {"af", "afg"},
+ {"ax", "ala"},
+ {"al", "alb"},
+ {"dz", "dza"},
+ {"as", "asm"},
+ {"ad", "and"},
+ {"ao", "ago"},
+ {"ai", "aia"},
+ {"aq", "ata"},
+ {"ag", "atg"},
+ {"ar", "arg"},
+ {"am", "arm"},
+ {"aw", "abw"},
+ {"au", "aus"},
+ {"at", "aut"},
+ {"az", "aze"},
+ {"bs", "bhs"},
+ {"bh", "bhr"},
+ {"bd", "bgd"},
+ {"bb", "brb"},
+ {"by", "blr"},
+ {"be", "bel"},
+ {"bz", "blz"},
+ {"bj", "ben"},
+ {"bm", "bmu"},
+ {"bt", "btn"},
+ {"bo", "bol"},
+ {"ba", "bih"},
+ {"bw", "bwa"},
+ {"bv", "bvt"},
+ {"br", "bra"},
+ {"io", "iot"},
+ {"bn", "brn"},
+ {"bg", "bgr"},
+ {"bf", "bfa"},
+ {"bi", "bdi"},
+ {"kh", "khm"},
+ {"cm", "cmr"},
+ {"ca", "can"},
+ {"cv", "cpv"},
+ {"ky", "cym"},
+ {"cf", "caf"},
+ {"td", "tcd"},
+ {"cl", "chl"},
+ {"cn", "chn"},
+ {"cx", "cxr"},
+ {"co", "col"},
+ {"km", "com"},
+ {"cg", "cog"},
+ {"cd", "cod"},
+ {"ck", "cok"},
+ {"cr", "cri"},
+ {"ci", "civ"},
+ {"hr", "hrv"},
+ {"cu", "cub"},
+ {"cy", "cyp"},
+ {"cz", "cze"},
+ {"dk", "dnk"},
+ {"dj", "dji"},
+ {"dm", "dma"},
+ {"do", "dom"},
+ {"ec", "ecu"},
+ {"eg", "egy"},
+ {"sv", "slv"},
+ {"gq", "gnq"},
+ {"er", "eri"},
+ {"ee", "est"},
+ {"et", "eth"},
+ {"fk", "flk"},
+ {"fo", "fro"},
+ {"fj", "fji"},
+ {"fi", "fin"},
+ {"fr", "fra"},
+ {"gf", "guf"},
+ {"pf", "pyf"},
+ {"tf", "atf"},
+ {"ga", "gab"},
+ {"gm", "gmb"},
+ {"ge", "geo"},
+ {"de", "deu"},
+ {"gh", "gha"},
+ {"gi", "gib"},
+ {"gr", "grc"},
+ {"gl", "grl"},
+ {"gd", "grd"},
+ {"gp", "glp"},
+ {"gu", "gum"},
+ {"gt", "gtm"},
+ {"gg", "ggy"},
+ {"gn", "gin"},
+ {"gw", "gnb"},
+ {"gy", "guy"},
+ {"ht", "hti"},
+ {"hm", "hmd"},
+ {"va", "vat"},
+ {"hn", "hnd"},
+ {"hk", "hkg"},
+ {"hu", "hun"},
+ {"is", "isl"},
+ {"in", "ind"},
+ {"id", "idn"},
+ {"ir", "irn"},
+ {"iq", "irq"},
+ {"ie", "irl"},
+ {"im", "imn"},
+ {"il", "isr"},
+ {"it", "ita"},
+ {"jm", "jam"},
+ {"jp", "jpn"},
+ {"je", "jey"},
+ {"jo", "jor"},
+ {"kz", "kaz"},
+ {"ke", "ken"},
+ {"ki", "kir"},
+ {"kp", "prk"},
+ {"kr", "kor"},
+ {"kw", "kwt"},
+ {"kg", "kgz"},
+ {"la", "lao"},
+ {"lv", "lva"},
+ {"lb", "lbn"},
+ {"ls", "lso"},
+ {"lr", "lbr"},
+ {"ly", "lby"},
+ {"li", "lie"},
+ {"lt", "ltu"},
+ {"lu", "lux"},
+ {"mo", "mac"},
+ {"mk", "mkd"},
+ {"mg", "mdg"},
+ {"mw", "mwi"},
+ {"my", "mys"},
+ {"mv", "mdv"},
+ {"ml", "mli"},
+ {"mt", "mlt"},
+ {"mh", "mhl"},
+ {"mq", "mtq"},
+ {"mr", "mrt"},
+ {"mu", "mus"},
+ {"yt", "myt"},
+ {"mx", "mex"},
+ {"fm", "fsm"},
+ {"md", "mda"},
+ {"mc", "mco"},
+ {"mn", "mng"},
+ {"me", "mne"},
+ {"ms", "msr"},
+ {"ma", "mar"},
+ {"mz", "moz"},
+ {"mm", "mmr"},
+ {"na", "nam"},
+ {"nr", "nru"},
+ {"np", "npl"},
+ {"nl", "nld"},
+ {"an", "ant"},
+ {"nc", "ncl"},
+ {"nz", "nzl"},
+ {"ni", "nic"},
+ {"ne", "ner"},
+ {"ng", "nga"},
+ {"nu", "niu"},
+ {"nf", "nfk"},
+ {"mp", "mnp"},
+ {"no", "nor"},
+ {"om", "omn"},
+ {"pk", "pak"},
+ {"pw", "plw"},
+ {"ps", "pse"},
+ {"pa", "pan"},
+ {"pg", "png"},
+ {"py", "pry"},
+ {"pe", "per"},
+ {"ph", "phl"},
+ {"pn", "pcn"},
+ {"pl", "pol"},
+ {"pt", "prt"},
+ {"pr", "pri"},
+ {"qa", "qat"},
+ {"re", "reu"},
+ {"ro", "rou"},
+ {"ru", "rus"},
+ {"rw", "rwa"},
+ {"bl", "blm"},
+ {"sh", "shn"},
+ {"kn", "kna"},
+ {"lc", "lca"},
+ {"mf", "maf"},
+ {"pm", "spm"},
+ {"vc", "vct"},
+ {"ws", "wsm"},
+ {"sm", "smr"},
+ {"st", "stp"},
+ {"sa", "sau"},
+ {"sn", "sen"},
+ {"rs", "srb"},
+ {"sc", "syc"},
+ {"sl", "sle"},
+ {"sg", "sgp"},
+ {"sk", "svk"},
+ {"si", "svn"},
+ {"sb", "slb"},
+ {"so", "som"},
+ {"za", "zaf"},
+ {"gs", "sgs"},
+ {"es", "esp"},
+ {"lk", "lka"},
+ {"sd", "sdn"},
+ {"sr", "sur"},
+ {"sj", "sjm"},
+ {"sz", "swz"},
+ {"se", "swe"},
+ {"ch", "che"},
+ {"sy", "syr"},
+ {"tw", "twn"},
+ {"tj", "tjk"},
+ {"tz", "tza"},
+ {"th", "tha"},
+ {"tl", "tls"},
+ {"tg", "tgo"},
+ {"tk", "tkl"},
+ {"to", "ton"},
+ {"tt", "tto"},
+ {"tn", "tun"},
+ {"tr", "tur"},
+ {"tm", "tkm"},
+ {"tc", "tca"},
+ {"tv", "tuv"},
+ {"ug", "uga"},
+ {"ua", "ukr"},
+ {"ae", "are"},
+ {"gb", "gbr"},
+ {"us", "usa"},
+ {"um", "umi"},
+ {"uy", "ury"},
+ {"uz", "uzb"},
+ {"vu", "vut"},
+ {"ve", "ven"},
+ {"vn", "vnm"},
+ {"vg", "vgb"},
+ {"vi", "vir"},
+ {"wf", "wlf"},
+ {"eh", "esh"},
+ {"ye", "yem"},
+ {"zm", "zmb"},
+ {"zw", "zwe"}
+}};
+// clang-format on
diff --git a/xbmc/utils/LangCodeExpander.h b/xbmc/utils/LangCodeExpander.h
new file mode 100644
index 0000000..dc9e5dc
--- /dev/null
+++ b/xbmc/utils/LangCodeExpander.h
@@ -0,0 +1,185 @@
+/*
+ * 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 <map>
+#include <string>
+#include <vector>
+
+class TiXmlElement;
+
+class CLangCodeExpander
+{
+public:
+ CLangCodeExpander();
+ ~CLangCodeExpander();
+
+ enum LANGFORMATS
+ {
+ ISO_639_1,
+ ISO_639_2,
+ ENGLISH_NAME
+ };
+
+ enum class LANG_LIST
+ {
+ // Standard ISO
+ DEFAULT,
+ // Standard ISO + Language addons
+ INCLUDE_ADDONS,
+ // Standard ISO + User defined
+ // (User defined can override language name of existing codes)
+ INCLUDE_USERDEFINED,
+ // Standard ISO + Language addons + User defined
+ // (User defined can override language name of existing codes)
+ INCLUDE_ADDONS_USERDEFINED,
+ };
+
+ void LoadUserCodes(const TiXmlElement* pRootElement);
+ void Clear();
+
+ bool Lookup(const std::string& code, std::string& desc);
+ bool Lookup(const int code, std::string& desc);
+
+ /** \brief Determines if two english language names represent the same language.
+ * \param[in] lang1 The first language string to compare given as english language name.
+ * \param[in] lang2 The second language string to compare given as english language name.
+ * \return true if the two language strings represent the same language, false otherwise.
+ * For example "Abkhaz" and "Abkhazian" represent the same language.
+ */
+ bool CompareFullLanguageNames(const std::string& lang1, const std::string& lang2);
+
+ /** \brief Determines if two languages given as ISO 639-1, ISO 639-2/T, or ISO 639-2/B codes represent the same language.
+ * \param[in] code1 The first language to compare given as ISO 639-1, ISO 639-2/T, or ISO 639-2/B code.
+ * \param[in] code2 The second language to compare given as ISO 639-1, ISO 639-2/T, or ISO 639-2/B code.
+ * \return true if the two language codes represent the same language, false otherwise.
+ * For example "ger", "deu" and "de" represent the same language.
+ */
+ bool CompareISO639Codes(const std::string& code1, const std::string& code2);
+
+ /** \brief Converts a language given as 2-Char (ISO 639-1),
+ * 3-Char (ISO 639-2/T or ISO 639-2/B),
+ * or full english name string to a 2-Char (ISO 639-1) code.
+ * \param[out] code The 2-Char language code of the given language lang.
+ * \param[in] lang The language that should be converted.
+ * \return true if the conversion succeeded, false otherwise.
+ */
+ bool ConvertToISO6391(const std::string& lang, std::string& code);
+
+ /** \brief Converts a language given as 2-Char (ISO 639-1),
+ * 3-Char (ISO 639-2/T or ISO 639-2/B),
+ * or full english name string to a 3-Char ISO 639-2/B code.
+ * \param[in] lang The language that should be converted.
+ * \return The 3-Char ISO 639-2/B code of lang if that code exists, lang otherwise.
+ */
+ std::string ConvertToISO6392B(const std::string& lang);
+
+ /** \brief Converts a language given as 2-Char (ISO 639-1) to a 3-Char (ISO 639-2/T) code.
+ * \param[in] strISO6391 The language that should be converted.
+ * \param[out] strISO6392B The 3-Char (ISO 639-2/B) language code of the given language strISO6391.
+ * \param[in] checkWin32Locales Whether to also check WIN32 specific language codes.
+ * \return true if the conversion succeeded, false otherwise.
+ */
+ static bool ConvertISO6391ToISO6392B(const std::string& strISO6391, std::string& strISO6392B, bool checkWin32Locales = false);
+
+ /** \brief Converts a language given as 2-Char (ISO 639-1),
+ * 3-Char (ISO 639-2/T or ISO 639-2/B),
+ * or full english name string to a 3-Char ISO 639-2/T code.
+ * \param[in] strCharCode The language that should be converted.
+ * \param[out] strISO6392B The 3-Char (ISO 639-2/B) language code of the given language strISO6391.
+ * \param[in] checkWin32Locales Whether to also check WIN32 specific language codes.
+ * \return true if the conversion succeeded, false otherwise.
+ */
+ bool ConvertToISO6392B(const std::string& strCharCode, std::string& strISO6392B, bool checkWin32Locales = false);
+
+ /** \brief Converts a language given as 2-Char (ISO 639-1),
+ * 3-Char (ISO 639-2/T or ISO 639-2/B),
+ * or full english name string to a 3-Char ISO 639-2/T code.
+ * \param[in] strCharCode The language that should be converted.
+ * \param[out] strISO6392T The 3-Char (ISO 639-2/T) language code of the given language strISO6391.
+ * \param[in] checkWin32Locales Whether to also check WIN32 specific language codes.
+ * \return true if the conversion succeeded, false otherwise.
+ */
+ bool ConvertToISO6392T(const std::string& strCharCode, std::string& strISO6392T, bool checkWin32Locales = false);
+
+ /** \brief Converts a language given as 2-Char (ISO 639-1),
+ * 3-Char (ISO 639-2/T or ISO 639-2/B),
+ * or full english name string to a 3-Char ISO 639-2/T code.
+ * \param[in] lang The language that should be converted.
+ * \return The 3-Char ISO 639-2/T code of lang if that code exists, lang otherwise.
+ */
+ std::string ConvertToISO6392T(const std::string& lang);
+
+ /*
+ * \brief Find a language code with subtag (e.g. zh-tw, zh-Hans) in to a string.
+ * This function find a limited set of IETF BCP47 specs, so:
+ * language tag + region subtag, or, language tag + script subtag.
+ * The language code can be found also if wrapped with round brackets.
+ * \param str The string where find the language code.
+ * \return The language code found in the string, otherwise empty string
+ */
+ static std::string FindLanguageCodeWithSubtag(const std::string& str);
+
+#ifdef TARGET_WINDOWS
+ static bool ConvertISO31661Alpha2ToISO31661Alpha3(const std::string& strISO31661Alpha2, std::string& strISO31661Alpha3);
+ static bool ConvertWindowsLanguageCodeToISO6392B(const std::string& strWindowsLanguageCode, std::string& strISO6392B);
+#endif
+
+ /*
+ * \brief Get the list of language names.
+ * \param format [OPT] The format type.
+ * \param list [OPT] The type of language list to retrieve.
+ * \return The languages
+ */
+ std::vector<std::string> GetLanguageNames(LANGFORMATS format = ISO_639_1,
+ LANG_LIST list = LANG_LIST::DEFAULT);
+
+protected:
+ /*
+ * \brief Converts a language code given as a long, see #MAKECODE(a, b, c, d)
+ * to its string representation.
+ * \param[in] code The language code given as a long, see #MAKECODE(a, b, c, d).
+ * \return The string representation of the given language code code.
+ */
+ static std::string CodeToString(long code);
+
+ static bool LookupInISO639Tables(const std::string& code, std::string& desc);
+
+ /*
+ * \brief Looks up the language description for given language code
+ * in to the installed language addons.
+ * \param[in] code The language code for which description is looked for.
+ * \param[out] desc The english language name.
+ * \return true if the language description was found, false otherwise.
+ */
+ static bool LookupInLangAddons(const std::string& code, std::string& desc);
+
+ bool LookupInUserMap(const std::string& code, std::string& desc);
+
+ /** \brief Looks up the ISO 639-1, ISO 639-2/T, or ISO 639-2/B, whichever it finds first,
+ * code of the given english language name.
+ * \param[in] desc The english language name for which a code is looked for.
+ * \param[out] code The ISO 639-1, ISO 639-2/T, or ISO 639-2/B code of the given language desc.
+ * \return true if the a code was found, false otherwise.
+ */
+ bool ReverseLookup(const std::string& desc, std::string& code);
+
+
+ /** \brief Looks up the user defined code of the given code or language name.
+ * \param[in] desc The language code or name that should be converted.
+ * \param[out] userCode The user defined language code of the given language desc.
+ * \return true if desc was found, false otherwise.
+ */
+ bool LookupUserCode(const std::string& desc, std::string &userCode);
+
+ typedef std::map<std::string, std::string> STRINGLOOKUPTABLE;
+ STRINGLOOKUPTABLE m_mapUser;
+};
+
+extern CLangCodeExpander g_LangCodeExpander;
diff --git a/xbmc/utils/LegacyPathTranslation.cpp b/xbmc/utils/LegacyPathTranslation.cpp
new file mode 100644
index 0000000..0069339
--- /dev/null
+++ b/xbmc/utils/LegacyPathTranslation.cpp
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2013-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 "LegacyPathTranslation.h"
+
+#include "URL.h"
+#include "utils/StringUtils.h"
+
+typedef struct Translator {
+ const char *legacyPath;
+ const char *newPath;
+} Translator;
+
+// ATTENTION: Make sure the longer match strings go first
+// because the string match is performed with StringUtils::StartsWith()
+static Translator s_videoDbTranslator[] = {
+ { "videodb://1/1", "videodb://movies/genres" },
+ { "videodb://1/2", "videodb://movies/titles" },
+ { "videodb://1/3", "videodb://movies/years" },
+ { "videodb://1/4", "videodb://movies/actors" },
+ { "videodb://1/5", "videodb://movies/directors" },
+ { "videodb://1/6", "videodb://movies/studios" },
+ { "videodb://1/7", "videodb://movies/sets" },
+ { "videodb://1/8", "videodb://movies/countries" },
+ { "videodb://1/9", "videodb://movies/tags" },
+ { "videodb://1", "videodb://movies" },
+ { "videodb://2/1", "videodb://tvshows/genres" },
+ { "videodb://2/2", "videodb://tvshows/titles" },
+ { "videodb://2/3", "videodb://tvshows/years" },
+ { "videodb://2/4", "videodb://tvshows/actors" },
+ { "videodb://2/5", "videodb://tvshows/studios" },
+ { "videodb://2/9", "videodb://tvshows/tags" },
+ { "videodb://2", "videodb://tvshows" },
+ { "videodb://3/1", "videodb://musicvideos/genres" },
+ { "videodb://3/2", "videodb://musicvideos/titles" },
+ { "videodb://3/3", "videodb://musicvideos/years" },
+ { "videodb://3/4", "videodb://musicvideos/artists" },
+ { "videodb://3/5", "videodb://musicvideos/albums" },
+ { "videodb://3/9", "videodb://musicvideos/tags" },
+ { "videodb://3", "videodb://musicvideos" },
+ { "videodb://4", "videodb://recentlyaddedmovies" },
+ { "videodb://5", "videodb://recentlyaddedepisodes" },
+ { "videodb://6", "videodb://recentlyaddedmusicvideos" }
+};
+
+#define VideoDbTranslatorSize sizeof(s_videoDbTranslator) / sizeof(Translator)
+
+// ATTENTION: Make sure the longer match strings go first
+// because the string match is performed with StringUtils::StartsWith()
+static Translator s_musicDbTranslator[] = {
+ { "musicdb://10", "musicdb://singles" },
+ { "musicdb://1", "musicdb://genres" },
+ { "musicdb://2", "musicdb://artists" },
+ { "musicdb://3", "musicdb://albums" },
+ { "musicdb://4", "musicdb://songs" },
+ { "musicdb://5/1", "musicdb://top100/albums" },
+ { "musicdb://5/2", "musicdb://top100/songs" },
+ { "musicdb://5", "musicdb://top100" },
+ { "musicdb://6", "musicdb://recentlyaddedalbums" },
+ { "musicdb://7", "musicdb://recentlyplayedalbums" },
+ { "musicdb://8", "musicdb://compilations" },
+ { "musicdb://9", "musicdb://years" }
+};
+
+#define MusicDbTranslatorSize sizeof(s_musicDbTranslator) / sizeof(Translator)
+
+std::string CLegacyPathTranslation::TranslateVideoDbPath(const CURL &legacyPath)
+{
+ return TranslatePath(legacyPath.Get(), s_videoDbTranslator, VideoDbTranslatorSize);
+}
+
+std::string CLegacyPathTranslation::TranslateMusicDbPath(const CURL &legacyPath)
+{
+ return TranslatePath(legacyPath.Get(), s_musicDbTranslator, MusicDbTranslatorSize);
+}
+
+std::string CLegacyPathTranslation::TranslateVideoDbPath(const std::string &legacyPath)
+{
+ return TranslatePath(legacyPath, s_videoDbTranslator, VideoDbTranslatorSize);
+}
+
+std::string CLegacyPathTranslation::TranslateMusicDbPath(const std::string &legacyPath)
+{
+ return TranslatePath(legacyPath, s_musicDbTranslator, MusicDbTranslatorSize);
+}
+
+std::string CLegacyPathTranslation::TranslatePath(const std::string &legacyPath, Translator *translationMap, size_t translationMapSize)
+{
+ std::string newPath = legacyPath;
+ for (size_t index = 0; index < translationMapSize; index++)
+ {
+ if (StringUtils::StartsWithNoCase(newPath, translationMap[index].legacyPath))
+ {
+ StringUtils::Replace(newPath, translationMap[index].legacyPath, translationMap[index].newPath);
+ break;
+ }
+ }
+
+ return newPath;
+}
diff --git a/xbmc/utils/LegacyPathTranslation.h b/xbmc/utils/LegacyPathTranslation.h
new file mode 100644
index 0000000..ba6450b
--- /dev/null
+++ b/xbmc/utils/LegacyPathTranslation.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013-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 <string>
+
+typedef struct Translator Translator;
+
+class CURL;
+
+/*!
+ \brief Translates old internal paths into new ones
+
+ Translates old videodb:// and musicdb:// paths which used numbers
+ to indicate a specific category to new paths using more descriptive
+ strings to indicate categories.
+ */
+class CLegacyPathTranslation
+{
+public:
+ /*!
+ \brief Translates old videodb:// paths to new ones
+
+ \param legacyPath Path in the old videodb:// format using numbers
+ \return Path in the new videodb:// format using descriptive strings
+ */
+ static std::string TranslateVideoDbPath(const CURL &legacyPath);
+ static std::string TranslateVideoDbPath(const std::string &legacyPath);
+
+ /*!
+ \brief Translates old musicdb:// paths to new ones
+
+ \param legacyPath Path in the old musicdb:// format using numbers
+ \return Path in the new musicdb:// format using descriptive strings
+ */
+ static std::string TranslateMusicDbPath(const CURL &legacyPath);
+ static std::string TranslateMusicDbPath(const std::string &legacyPath);
+
+private:
+ static std::string TranslatePath(const std::string &legacyPath, Translator *translationMap, size_t translationMapSize);
+};
diff --git a/xbmc/utils/Literals.h b/xbmc/utils/Literals.h
new file mode 100644
index 0000000..ce567d5
--- /dev/null
+++ b/xbmc/utils/Literals.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2014-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
+
+constexpr unsigned long long int operator"" _kib (unsigned long long int val)
+{
+ return val * 1024ull;
+}
+
+constexpr unsigned long long int operator"" _kb (unsigned long long int val)
+{
+ return val * 1000ull;
+}
+
+constexpr unsigned long long int operator"" _mib (unsigned long long int val)
+{
+ return val * 1024ull * 1024ull;
+}
+
+constexpr unsigned long long int operator"" _mb (unsigned long long int val)
+{
+ return val * 1000ull * 1000ull;
+}
diff --git a/xbmc/utils/Locale.cpp b/xbmc/utils/Locale.cpp
new file mode 100644
index 0000000..ff63ed4
--- /dev/null
+++ b/xbmc/utils/Locale.cpp
@@ -0,0 +1,284 @@
+/*
+ * 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 "Locale.h"
+
+#include "utils/StringUtils.h"
+
+const CLocale CLocale::Empty;
+
+CLocale::CLocale()
+ : m_language(),
+ m_territory(),
+ m_codeset(),
+ m_modifier()
+{ }
+
+CLocale::CLocale(const std::string& language)
+ : m_language(),
+ m_territory(),
+ m_codeset(),
+ m_modifier()
+{
+ m_valid = ParseLocale(language, m_language, m_territory, m_codeset, m_modifier);
+}
+
+CLocale::CLocale(const std::string& language, const std::string& territory)
+ : m_language(language),
+ m_territory(territory),
+ m_codeset(),
+ m_modifier()
+{
+ Initialize();
+}
+
+CLocale::CLocale(const std::string& language, const std::string& territory, const std::string& codeset)
+ : m_language(language),
+ m_territory(territory),
+ m_codeset(codeset),
+ m_modifier()
+{
+ Initialize();
+}
+
+CLocale::CLocale(const std::string& language, const std::string& territory, const std::string& codeset, const std::string& modifier)
+ : m_language(language),
+ m_territory(territory),
+ m_codeset(codeset),
+ m_modifier(modifier)
+{
+ Initialize();
+}
+
+CLocale::~CLocale() = default;
+
+CLocale CLocale::FromString(const std::string& locale)
+{
+ return CLocale(locale);
+}
+
+bool CLocale::operator==(const CLocale& other) const
+{
+ if (!m_valid && !other.m_valid)
+ return true;
+
+ return m_valid == other.m_valid &&
+ StringUtils::EqualsNoCase(m_language, other.m_language) &&
+ StringUtils::EqualsNoCase(m_territory, other.m_territory) &&
+ StringUtils::EqualsNoCase(m_codeset, other.m_codeset) &&
+ StringUtils::EqualsNoCase(m_modifier, other.m_modifier);
+}
+
+std::string CLocale::ToString() const
+{
+ if (!m_valid)
+ return "";
+
+ std::string locale = ToShortString();
+
+ if (!m_codeset.empty())
+ locale += "." + m_codeset;
+
+ if (!m_modifier.empty())
+ locale += "@" + m_modifier;
+
+ return locale;
+}
+
+std::string CLocale::ToStringLC() const
+{
+ if (!m_valid)
+ return "";
+
+ std::string locale = ToString();
+ StringUtils::ToLower(locale);
+
+ return locale;
+}
+
+std::string CLocale::ToShortString() const
+{
+ if (!m_valid)
+ return "";
+
+ std::string locale = m_language;
+
+ if (!m_territory.empty())
+ locale += "_" + m_territory;
+
+ return locale;
+}
+
+std::string CLocale::ToShortStringLC() const
+{
+ if (!m_valid)
+ return "";
+
+ std::string locale = ToShortString();
+ StringUtils::ToLower(locale);
+
+ return locale;
+}
+
+bool CLocale::Equals(const std::string& locale) const
+{
+ CLocale other = FromString(locale);
+
+ return *this == other;
+}
+
+bool CLocale::Matches(const std::string& locale) const
+{
+ CLocale other = FromString(locale);
+
+ if (!m_valid && !other.m_valid)
+ return true;
+ if (!m_valid || !other.m_valid)
+ return false;
+
+ if (!StringUtils::EqualsNoCase(m_language, other.m_language))
+ return false;
+ if (!m_territory.empty() && !other.m_territory.empty() && !StringUtils::EqualsNoCase(m_territory, other.m_territory))
+ return false;
+ if (!m_codeset.empty() && !other.m_codeset.empty() && !StringUtils::EqualsNoCase(m_codeset, other.m_codeset))
+ return false;
+ if (!m_modifier.empty() && !other.m_modifier.empty() && !StringUtils::EqualsNoCase(m_modifier, other.m_modifier))
+ return false;
+
+ return true;
+}
+
+std::string CLocale::FindBestMatch(const std::set<std::string>& locales) const
+{
+ std::string bestMatch = "";
+ int bestMatchRank = -1;
+
+ for (auto const& locale : locales)
+ {
+ // check if there is an exact match
+ if (Equals(locale))
+ return locale;
+
+ int matchRank = GetMatchRank(locale);
+ if (matchRank > bestMatchRank)
+ {
+ bestMatchRank = matchRank;
+ bestMatch = locale;
+ }
+ }
+
+ return bestMatch;
+}
+
+std::string CLocale::FindBestMatch(const std::unordered_map<std::string, std::string>& locales) const
+{
+ std::string bestMatch = "";
+ int bestMatchRank = -1;
+
+ for (auto const& locale : locales)
+ {
+ // check if there is an exact match
+ if (Equals(locale.first))
+ return locale.first;
+
+ int matchRank = GetMatchRank(locale.first);
+ if (matchRank > bestMatchRank)
+ {
+ bestMatchRank = matchRank;
+ bestMatch = locale.first;
+ }
+ }
+
+ return bestMatch;
+}
+
+bool CLocale::CheckValidity(const std::string& language, const std::string& territory, const std::string& codeset, const std::string& modifier)
+{
+ static_cast<void>(territory);
+ static_cast<void>(codeset);
+ static_cast<void>(modifier);
+
+ return !language.empty();
+}
+
+bool CLocale::ParseLocale(const std::string &locale, std::string &language, std::string &territory, std::string &codeset, std::string &modifier)
+{
+ if (locale.empty())
+ return false;
+
+ language.clear();
+ territory.clear();
+ codeset.clear();
+ modifier.clear();
+
+ // the format for a locale is [language[_territory][.codeset][@modifier]]
+ std::string tmp = locale;
+
+ // look for the modifier after @
+ size_t pos = tmp.find('@');
+ if (pos != std::string::npos)
+ {
+ modifier = tmp.substr(pos + 1);
+ tmp = tmp.substr(0, pos);
+ }
+
+ // look for the codeset after .
+ pos = tmp.find('.');
+ if (pos != std::string::npos)
+ {
+ codeset = tmp.substr(pos + 1);
+ tmp = tmp.substr(0, pos);
+ }
+
+ // look for the codeset after _
+ pos = tmp.find('_');
+ if (pos != std::string::npos)
+ {
+ territory = tmp.substr(pos + 1);
+ StringUtils::ToUpper(territory);
+ tmp = tmp.substr(0, pos);
+ }
+
+ // what remains is the language
+ language = tmp;
+ StringUtils::ToLower(language);
+
+ return CheckValidity(language, territory, codeset, modifier);
+}
+
+void CLocale::Initialize()
+{
+ m_valid = CheckValidity(m_language, m_territory, m_codeset, m_modifier);
+ if (m_valid)
+ {
+ StringUtils::ToLower(m_language);
+ StringUtils::ToUpper(m_territory);
+ }
+}
+
+int CLocale::GetMatchRank(const std::string& locale) const
+{
+ CLocale other = FromString(locale);
+
+ // both locales must be valid and match in language
+ if (!m_valid || !other.m_valid ||
+ !StringUtils::EqualsNoCase(m_language, other.m_language))
+ return -1;
+
+ int rank = 0;
+ // matching in territory is considered more important than matching in
+ // codeset and/or modifier
+ if (!m_territory.empty() && !other.m_territory.empty() && StringUtils::EqualsNoCase(m_territory, other.m_territory))
+ rank += 3;
+ if (!m_codeset.empty() && !other.m_codeset.empty() && StringUtils::EqualsNoCase(m_codeset, other.m_codeset))
+ rank += 1;
+ if (!m_modifier.empty() && !other.m_modifier.empty() && StringUtils::EqualsNoCase(m_modifier, other.m_modifier))
+ rank += 1;
+
+ return rank;
+}
diff --git a/xbmc/utils/Locale.h b/xbmc/utils/Locale.h
new file mode 100644
index 0000000..4f68af8
--- /dev/null
+++ b/xbmc/utils/Locale.h
@@ -0,0 +1,161 @@
+/*
+ * 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 <set>
+#include <string>
+#include <unordered_map>
+
+/*!
+ \brief Class representing a full locale of the form `[language[_territory][.codeset][@modifier]]`.
+ */
+class CLocale
+{
+public:
+ CLocale();
+ explicit CLocale(const std::string& language);
+ CLocale(const std::string& language, const std::string& territory);
+ CLocale(const std::string& language, const std::string& territory, const std::string& codeset);
+ CLocale(const std::string& language, const std::string& territory, const std::string& codeset, const std::string& modifier);
+ ~CLocale();
+
+ /*!
+ \brief Empty (and invalid) CLocale instance.
+ */
+ static const CLocale Empty;
+
+ /*!
+ \brief Parses the given string representation and turns it into a locale.
+
+ \param locale String representation of a locale
+ */
+ static CLocale FromString(const std::string& locale);
+
+ bool operator==(const CLocale& other) const;
+ inline bool operator!=(const CLocale& other) const { return !(*this == other); }
+
+ /*!
+ \brief Whether the locale is valid or not.
+
+ \details A locale is considered valid if at least the language code is set.
+ */
+ bool IsValid() const { return m_valid; }
+
+ /*!
+ \brief Returns the (lower-case) ISO 639-1 language code of the locale.
+ */
+ const std::string& GetLanguageCode() const { return m_language; }
+ /*!
+ \brief Returns the (upper-case) ISO 3166-1 Alpha-2 territory code of the locale.
+ */
+ const std::string& GetTerritoryCode() const { return m_territory; }
+ /*!
+ \brief Returns the codeset of the locale.
+ */
+ const std::string& GetCodeset() const { return m_codeset; }
+ /*!
+ \brief Returns the modifier of the locale.
+ */
+ const std::string& GetModifier() const { return m_modifier; }
+
+ /*!
+ \brief Returns the full string representation of the locale.
+
+ \details The format of the string representation is
+ `[language[_territory][.codeset][@modifier]]` where the language is
+ represented as a (lower-case) two character ISO 639-1 code and the territory
+ is represented as a (upper-case) two character ISO 3166-1 Alpha-2 code.
+ */
+ std::string ToString() const;
+ /*!
+ \brief Returns the full string representation of the locale in lowercase.
+
+ \details The format of the string representation is
+ `language[_territory][.codeset][@modifier]]` where the language is
+ represented as a two character ISO 639-1 code and the territory is
+ represented as a two character ISO 3166-1 Alpha-2 code.
+ */
+ std::string ToStringLC() const;
+ /*!
+ \brief Returns the short string representation of the locale.
+
+ \details The format of the short string representation is
+ `[language[_territory]` where the language is represented as a (lower-case)
+ two character ISO 639-1 code and the territory is represented as a
+ (upper-case) two character ISO 3166-1 Alpha-2 code.
+ */
+ std::string ToShortString() const;
+ /*!
+ \brief Returns the short string representation of the locale in lowercase.
+
+ \details The format of the short string representation is
+ `[language[_territory]` where the language is represented as a two character
+ ISO 639-1 code and the territory is represented as a two character
+ ISO 3166-1 Alpha-2 code.
+ */
+ std::string ToShortStringLC() const;
+
+ /*!
+ \brief Checks if the given string representation of a locale exactly matches
+ the locale.
+
+ \param locale String representation of a locale
+ \return True if the string representation matches the locale, false otherwise.
+ */
+ bool Equals(const std::string& locale) const;
+
+ /*!
+ \brief Checks if the given string representation of a locale partly matches
+ the locale.
+
+ \details Partial matching means that every available locale part needs to
+ match the same locale part of the other locale if present.
+
+ \param locale String representation of a locale
+ \return True if the string representation matches the locale, false otherwise.
+ */
+ bool Matches(const std::string& locale) const;
+
+ /*!
+ \brief Tries to find the locale in the given list that matches this locale
+ best.
+
+ \param locales List of string representations of locales
+ \return Best matching locale from the given list or empty string.
+ */
+ std::string FindBestMatch(const std::set<std::string>& locales) const;
+
+ /*!
+ \brief Tries to find the locale in the given list that matches this locale
+ best.
+
+ \param locales Map list of string representations of locales with first as
+ locale identifier
+ \return Best matching locale from the given list or empty string.
+
+ \remark Used from \ref CAddonInfo::GetTranslatedText to prevent copy from map
+ to set.
+ */
+ std::string FindBestMatch(const std::unordered_map<std::string, std::string>& locales) const;
+
+private:
+ static bool CheckValidity(const std::string& language, const std::string& territory, const std::string& codeset, const std::string& modifier);
+ static bool ParseLocale(const std::string &locale, std::string &language, std::string &territory, std::string &codeset, std::string &modifier);
+
+ void Initialize();
+
+ int GetMatchRank(const std::string& locale) const;
+
+ bool m_valid = false;
+ std::string m_language;
+ std::string m_territory;
+ std::string m_codeset;
+ std::string m_modifier;
+};
+
diff --git a/xbmc/utils/Map.h b/xbmc/utils/Map.h
new file mode 100644
index 0000000..17af545
--- /dev/null
+++ b/xbmc/utils/Map.h
@@ -0,0 +1,102 @@
+
+/*
+ * Copyright (C) 2005-2022 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 <algorithm>
+#include <array>
+#include <stdexcept>
+
+/*!
+ * \brief This class is designed to implement a constexpr version of std::map.
+ * The standard library std::map doesn't allow constexpr (and it
+ * doesn't look like it will be implemented in the future). This class
+ * utilizes std::array and std::pair as they allow constexpr.
+ *
+ * When using this class you should use the helper make_map instead of
+ * constructing this class directly. For example:
+ * constexpr auto myMap = make_map<int, std::string_view>({{1, "one"}});
+ *
+ * This class is useful for mapping enum values to strings that can be
+ * compile time checked. This also helps with heap usage.
+ *
+ * Lookups have linear complexity, so should not be used for "big" maps.
+ */
+template<typename Key, typename Value, size_t Size>
+class CMap
+{
+public:
+ template<typename Iterable>
+ constexpr CMap(Iterable begin, Iterable end)
+ {
+ size_t index = 0;
+ while (begin != end)
+ {
+ // c++17 doesn't have constexpr assignment operator for std::pair
+ auto& first = m_map[index].first;
+ auto& second = m_map[index].second;
+ ++index;
+
+ first = std::move(begin->first);
+ second = std::move(begin->second);
+ ++begin;
+
+ //! @todo: c++20 can use constexpr assignment operator instead
+ // auto& p = data[index];
+ // ++index;
+
+ // p = std::move(*begin);
+ // ++begin;
+ //
+ }
+ }
+
+ ~CMap() = default;
+
+ constexpr const Value& at(const Key& key) const
+ {
+ const auto it = find(key);
+ if (it != m_map.cend())
+ {
+ return it->second;
+ }
+ else
+ {
+ throw std::range_error("Not Found");
+ }
+ }
+
+ constexpr auto find(const Key& key) const
+ {
+ return std::find_if(m_map.cbegin(), m_map.cend(),
+ [&key](const auto& pair) { return pair.first == key; });
+ }
+
+ constexpr size_t size() const { return Size; }
+
+ constexpr auto cbegin() const { return m_map.cbegin(); }
+ constexpr auto cend() const { return m_map.cend(); }
+
+private:
+ CMap() = delete;
+
+ std::array<std::pair<Key, Value>, Size> m_map;
+};
+
+/*!
+ * \brief Use this helper when wanting to use CMap. This is needed to allow
+ * deducing the size of the map from the initializer list without
+ * needing to explicitly give the size of the map (similar to std::map).
+ *
+ */
+template<typename Key, typename Value, std::size_t Size>
+constexpr auto make_map(std::pair<Key, Value>(&&m)[Size]) -> CMap<Key, Value, Size>
+{
+ return CMap<Key, Value, Size>(std::begin(m), std::end(m));
+}
diff --git a/xbmc/utils/MathUtils.h b/xbmc/utils/MathUtils.h
new file mode 100644
index 0000000..2b1dbcc
--- /dev/null
+++ b/xbmc/utils/MathUtils.h
@@ -0,0 +1,239 @@
+/*
+ * 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 <assert.h>
+#include <climits>
+#include <cmath>
+#include <stdint.h>
+#include <type_traits>
+
+#if defined(HAVE_SSE2) && defined(__SSE2__)
+#include <emmintrin.h>
+#endif
+
+// use real compiler defines in here as we want to
+// avoid including system.h or other magic includes.
+// use 'gcc -dM -E - < /dev/null' or similar to find them.
+
+// clang-format off
+#if defined(__aarch64__) || \
+ defined(__alpha__) || \
+ defined(__arc__) || \
+ defined(__arm__) || \
+ defined(__loongarch__) || \
+ defined(_M_ARM) || \
+ defined(__mips__) || \
+ defined(__or1k__) || \
+ defined(__powerpc__) || \
+ defined(__ppc__) || \
+ defined(__riscv) || \
+ defined(__SH4__) || \
+ defined(__s390x__) || \
+ defined(__sparc__) || \
+ defined(__xtensa__)
+#define DISABLE_MATHUTILS_ASM_ROUND_INT
+#endif
+// clang-format on
+
+/*! \brief Math utility class.
+ Note that the test() routine should return true for all implementations
+
+ See http://ldesoras.free.fr/doc/articles/rounding_en.pdf for an explanation
+ of the technique used on x86.
+ */
+namespace MathUtils
+{
+ // GCC does something stupid with optimization on release builds if we try
+ // to assert in these functions
+
+ /*! \brief Round to nearest integer.
+ This routine does fast rounding to the nearest integer.
+ In the case (k + 0.5 for any integer k) we round up to k+1, and in all other
+ instances we should return the nearest integer.
+ Thus, { -1.5, -0.5, 0.5, 1.5 } is rounded to { -1, 0, 1, 2 }.
+ It preserves the property that round(k) - round(k-1) = 1 for all doubles k.
+
+ Make sure MathUtils::test() returns true for each implementation.
+ \sa truncate_int, test
+ */
+ inline int round_int(double x)
+ {
+ assert(x > static_cast<double>((int) (INT_MIN / 2)) - 1.0);
+ assert(x < static_cast<double>((int) (INT_MAX / 2)) + 1.0);
+
+#if defined(DISABLE_MATHUTILS_ASM_ROUND_INT)
+ /* This implementation warrants some further explanation.
+ *
+ * First, a couple of notes on rounding:
+ * 1) C casts from float/double to integer round towards zero.
+ * 2) Float/double additions are rounded according to the normal rules,
+ * in other words: on some architectures, it's fixed at compile-time,
+ * and on others it can be set using fesetround()). The following
+ * analysis assumes round-to-nearest with ties rounding to even. This
+ * is a fairly sensible choice, and is the default with ARM VFP.
+ *
+ * What this function wants is round-to-nearest with ties rounding to
+ * +infinity. This isn't an IEEE rounding mode, even if we could guarantee
+ * that all architectures supported fesetround(), which they don't. Instead,
+ * this adds an offset of 2147483648.5 (= 0x80000000.8p0), then casts to
+ * an unsigned int (crucially, all possible inputs are now in a range where
+ * round to zero acts the same as round to -infinity) and then subtracts
+ * 0x80000000 in the integer domain. The 0.5 component of the offset
+ * converts what is effectively a round down into a round to nearest, with
+ * ties rounding up, as desired.
+ *
+ * There is a catch, that because there is a double rounding, there is a
+ * small region where the input falls just *below* a tie, where the addition
+ * of the offset causes a round *up* to an exact integer, due to the finite
+ * level of precision available in floating point. You need to be aware of
+ * this when calling this function, although at present it is not believed
+ * that XBMC ever attempts to round numbers in this window.
+ *
+ * It is worth proving the size of the affected window. Recall that double
+ * precision employs a mantissa of 52 bits.
+ * 1) For all inputs -0.5 <= x <= INT_MAX
+ * Once the offset is applied, the most significant binary digit in the
+ * floating-point representation is +2^31.
+ * At this magnitude, the smallest step representable in double precision
+ * is 2^31 / 2^52 = 0.000000476837158203125
+ * So the size of the range which is rounded up due to the addition is
+ * half the size of this step, or 0.0000002384185791015625
+ *
+ * 2) For all inputs INT_MIN/2 < x < -0.5
+ * Once the offset is applied, the most significant binary digit in the
+ * floating-point representation is +2^30.
+ * At this magnitude, the smallest step representable in double precision
+ * is 2^30 / 2^52 = 0.0000002384185791015625
+ * So the size of the range which is rounded up due to the addition is
+ * half the size of this step, or 0.00000011920928955078125
+ *
+ * 3) For all inputs INT_MIN <= x <= INT_MIN/2
+ * The representation once the offset is applied has equal or greater
+ * precision than the input, so the addition does not cause rounding.
+ */
+ return ((unsigned int) (x + 2147483648.5)) - 0x80000000;
+
+#else
+ const float round_to_nearest = 0.5f;
+ int i;
+#if defined(HAVE_SSE2) && defined(__SSE2__)
+ const float round_dn_to_nearest = 0.4999999f;
+ i = (x > 0) ? _mm_cvttsd_si32(_mm_set_sd(x + static_cast<double>(round_to_nearest)))
+ : _mm_cvttsd_si32(_mm_set_sd(x - static_cast<double>(round_dn_to_nearest)));
+
+#elif defined(TARGET_WINDOWS)
+ __asm
+ {
+ fld x
+ fadd st, st (0)
+ fadd round_to_nearest
+ fistp i
+ sar i, 1
+ }
+
+#else
+ __asm__ __volatile__ (
+ "fadd %%st\n\t"
+ "fadd %%st(1)\n\t"
+ "fistpl %0\n\t"
+ "sarl $1, %0\n"
+ : "=m"(i) : "u"(round_to_nearest), "t"(x) : "st"
+ );
+
+#endif
+ return i;
+#endif
+ }
+
+ /*! \brief Truncate to nearest integer.
+ This routine does fast truncation to an integer.
+ It should simply drop the fractional portion of the floating point number.
+
+ Make sure MathUtils::test() returns true for each implementation.
+ \sa round_int, test
+ */
+ inline int truncate_int(double x)
+ {
+ assert(x > static_cast<double>(INT_MIN / 2) - 1.0);
+ assert(x < static_cast<double>(INT_MAX / 2) + 1.0);
+ return static_cast<int>(x);
+ }
+
+ inline int64_t abs(int64_t a)
+ {
+ return (a < 0) ? -a : a;
+ }
+
+ inline unsigned bitcount(unsigned v)
+ {
+ unsigned c = 0;
+ for (c = 0; v; c++)
+ v &= v - 1; // clear the least significant bit set
+ return c;
+ }
+
+ inline void hack()
+ {
+ // stupid hack to keep compiler from dropping these
+ // functions as unused
+ MathUtils::round_int(0.0);
+ MathUtils::truncate_int(0.0);
+ MathUtils::abs(0);
+ }
+
+ /**
+ * Compare two floating-point numbers for equality and regard them
+ * as equal if their difference is below a given threshold.
+ *
+ * It is usually not useful to compare float numbers for equality with
+ * the standard operator== since very close numbers might have different
+ * representations.
+ */
+ template<typename FloatT>
+ inline bool FloatEquals(FloatT f1, FloatT f2, FloatT maxDelta)
+ {
+ return (std::abs(f2 - f1) < maxDelta);
+ }
+
+ /*!
+ * \brief Round a floating point number to nearest multiple
+ * \param value The value to round
+ * \param multiple The multiple
+ * \return The rounded value
+ */
+ template<typename T, std::enable_if_t<std::is_floating_point<T>::value, bool> = true>
+ inline T RoundF(const T value, const T multiple)
+ {
+ if (multiple == 0)
+ return value;
+
+ return static_cast<T>(std::round(static_cast<double>(value) / static_cast<double>(multiple)) *
+ static_cast<double>(multiple));
+ }
+
+#if 0
+ /*! \brief test routine for round_int and truncate_int
+ Must return true on all platforms.
+ */
+ inline bool test()
+ {
+ for (int i = -8; i < 8; ++i)
+ {
+ double d = 0.25*i;
+ int r = (i < 0) ? (i - 1) / 4 : (i + 2) / 4;
+ int t = i / 4;
+ if (round_int(d) != r || truncate_int(d) != t)
+ return false;
+ }
+ return true;
+ }
+#endif
+} // namespace MathUtils
+
diff --git a/xbmc/utils/MemUtils.h b/xbmc/utils/MemUtils.h
new file mode 100644
index 0000000..0266908
--- /dev/null
+++ b/xbmc/utils/MemUtils.h
@@ -0,0 +1,30 @@
+/*
+ * 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 <cstdint>
+#include <memory>
+
+namespace KODI
+{
+namespace MEMORY
+{
+struct MemoryStatus
+{
+ unsigned int memoryLoad;
+
+ uint64_t totalPhys;
+ uint64_t availPhys;
+};
+
+void* AlignedMalloc(size_t s, size_t alignTo);
+void AlignedFree(void* p);
+void GetMemoryStatus(MemoryStatus* buffer);
+}
+}
diff --git a/xbmc/utils/Mime.cpp b/xbmc/utils/Mime.cpp
new file mode 100644
index 0000000..576e499
--- /dev/null
+++ b/xbmc/utils/Mime.cpp
@@ -0,0 +1,700 @@
+/*
+ * 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 "Mime.h"
+
+#include "FileItem.h"
+#include "URIUtils.h"
+#include "URL.h"
+#include "filesystem/CurlFile.h"
+#include "music/tags/MusicInfoTag.h"
+#include "utils/StringUtils.h"
+#include "video/VideoInfoTag.h"
+
+#include <algorithm>
+
+const std::map<std::string, std::string> CMime::m_mimetypes = {
+ {{"3dm", "x-world/x-3dmf"},
+ {"3dmf", "x-world/x-3dmf"},
+ {"3fr", "image/3fr"},
+ {"a", "application/octet-stream"},
+ {"aab", "application/x-authorware-bin"},
+ {"aam", "application/x-authorware-map"},
+ {"aas", "application/x-authorware-seg"},
+ {"abc", "text/vnd.abc"},
+ {"acgi", "text/html"},
+ {"afl", "video/animaflex"},
+ {"ai", "application/postscript"},
+ {"aif", "audio/aiff"},
+ {"aifc", "audio/x-aiff"},
+ {"aiff", "audio/aiff"},
+ {"aim", "application/x-aim"},
+ {"aip", "text/x-audiosoft-intra"},
+ {"ani", "application/x-navi-animation"},
+ {"aos", "application/x-nokia-9000-communicator-add-on-software"},
+ {"apng", "image/apng"},
+ {"aps", "application/mime"},
+ {"arc", "application/octet-stream"},
+ {"arj", "application/arj"},
+ {"art", "image/x-jg"},
+ {"arw", "image/arw"},
+ {"asf", "video/x-ms-asf"},
+ {"asm", "text/x-asm"},
+ {"asp", "text/asp"},
+ {"asx", "video/x-ms-asf"},
+ {"au", "audio/basic"},
+ {"avi", "video/avi"},
+ {"avs", "video/avs-video"},
+ {"bcpio", "application/x-bcpio"},
+ {"bin", "application/octet-stream"},
+ {"bm", "image/bmp"},
+ {"bmp", "image/bmp"},
+ {"boo", "application/book"},
+ {"book", "application/book"},
+ {"boz", "application/x-bzip2"},
+ {"bsh", "application/x-bsh"},
+ {"bz", "application/x-bzip"},
+ {"bz2", "application/x-bzip2"},
+ {"c", "text/plain"},
+ {"c++", "text/plain"},
+ {"cat", "application/vnd.ms-pki.seccat"},
+ {"cc", "text/plain"},
+ {"ccad", "application/clariscad"},
+ {"cco", "application/x-cocoa"},
+ {"cdf", "application/cdf"},
+ {"cer", "application/pkix-cert"},
+ {"cer", "application/x-x509-ca-cert"},
+ {"cha", "application/x-chat"},
+ {"chat", "application/x-chat"},
+ {"class", "application/java"},
+ {"com", "application/octet-stream"},
+ {"conf", "text/plain"},
+ {"cpio", "application/x-cpio"},
+ {"cpp", "text/x-c"},
+ {"cpt", "application/x-cpt"},
+ {"crl", "application/pkcs-crl"},
+ {"crt", "application/pkix-cert"},
+ {"cr2", "image/cr2"},
+ {"crw", "image/crw"},
+ {"csh", "application/x-csh"},
+ {"css", "text/css"},
+ {"cxx", "text/plain"},
+ {"dcr", "application/x-director"},
+ {"deepv", "application/x-deepv"},
+ {"def", "text/plain"},
+ {"der", "application/x-x509-ca-cert"},
+ {"dif", "video/x-dv"},
+ {"dir", "application/x-director"},
+ {"dl", "video/dl"},
+ {"divx", "video/x-msvideo"},
+ {"dng", "image/dng"},
+ {"doc", "application/msword"},
+ {"docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
+ {"dot", "application/msword"},
+ {"dp", "application/commonground"},
+ {"drw", "application/drafting"},
+ {"dump", "application/octet-stream"},
+ {"dv", "video/x-dv"},
+ {"dvi", "application/x-dvi"},
+ {"dwf", "model/vnd.dwf"},
+ {"dwg", "image/vnd.dwg"},
+ {"dxf", "image/vnd.dwg"},
+ {"dxr", "application/x-director"},
+ {"el", "text/x-script.elisp"},
+ {"elc", "application/x-elc"},
+ {"env", "application/x-envoy"},
+ {"eps", "application/postscript"},
+ {"erf", "image/erf"},
+ {"es", "application/x-esrehber"},
+ {"etx", "text/x-setext"},
+ {"evy", "application/envoy"},
+ {"exe", "application/octet-stream"},
+ {"f", "text/x-fortran"},
+ {"f77", "text/x-fortran"},
+ {"f90", "text/x-fortran"},
+ {"fdf", "application/vnd.fdf"},
+ {"fif", "image/fif"},
+ {"flac", "audio/flac"},
+ {"fli", "video/fli"},
+ {"flo", "image/florian"},
+ {"flv", "video/x-flv"},
+ {"flx", "text/vnd.fmi.flexstor"},
+ {"fmf", "video/x-atomic3d-feature"},
+ {"for", "text/plain"},
+ {"for", "text/x-fortran"},
+ {"fpx", "image/vnd.fpx"},
+ {"frl", "application/freeloader"},
+ {"funk", "audio/make"},
+ {"g", "text/plain"},
+ {"g3", "image/g3fax"},
+ {"gif", "image/gif"},
+ {"gl", "video/x-gl"},
+ {"gsd", "audio/x-gsm"},
+ {"gsm", "audio/x-gsm"},
+ {"gsp", "application/x-gsp"},
+ {"gss", "application/x-gss"},
+ {"gtar", "application/x-gtar"},
+ {"gz", "application/x-compressed"},
+ {"gzip", "application/x-gzip"},
+ {"h", "text/plain"},
+ {"hdf", "application/x-hdf"},
+ {"heic", "image/heic"},
+ {"heif", "image/heif"},
+ {"help", "application/x-helpfile"},
+ {"hgl", "application/vnd.hp-hpgl"},
+ {"hh", "text/plain"},
+ {"hlb", "text/x-script"},
+ {"hlp", "application/hlp"},
+ {"hpg", "application/vnd.hp-hpgl"},
+ {"hpgl", "application/vnd.hp-hpgl"},
+ {"hqx", "application/binhex"},
+ {"hta", "application/hta"},
+ {"htc", "text/x-component"},
+ {"htm", "text/html"},
+ {"html", "text/html"},
+ {"htmls", "text/html"},
+ {"htt", "text/webviewhtml"},
+ {"htx", "text/html"},
+ {"ice", "x-conference/x-cooltalk"},
+ {"ico", "image/x-icon"},
+ {"idc", "text/plain"},
+ {"ief", "image/ief"},
+ {"iefs", "image/ief"},
+ {"iges", "application/iges"},
+ {"igs", "application/iges"},
+ {"ima", "application/x-ima"},
+ {"imap", "application/x-httpd-imap"},
+ {"inf", "application/inf"},
+ {"ins", "application/x-internet-signup"},
+ {"ip", "application/x-ip2"},
+ {"isu", "video/x-isvideo"},
+ {"it", "audio/it"},
+ {"iv", "application/x-inventor"},
+ {"ivr", "i-world/i-vrml"},
+ {"ivy", "application/x-livescreen"},
+ {"jam", "audio/x-jam"},
+ {"jav", "text/x-java-source"},
+ {"java", "text/x-java-source"},
+ {"jcm", "application/x-java-commerce"},
+ {"jfif", "image/jpeg"},
+ {"jp2", "image/jp2"},
+ {"jfif-tbnl", "image/jpeg"},
+ {"jpe", "image/jpeg"},
+ {"jpeg", "image/jpeg"},
+ {"jpg", "image/jpeg"},
+ {"jps", "image/x-jps"},
+ {"js", "application/javascript"},
+ {"json", "application/json"},
+ {"jut", "image/jutvision"},
+ {"kar", "music/x-karaoke"},
+ {"kdc", "image/kdc"},
+ {"ksh", "text/x-script.ksh"},
+ {"la", "audio/nspaudio"},
+ {"lam", "audio/x-liveaudio"},
+ {"latex", "application/x-latex"},
+ {"lha", "application/lha"},
+ {"lhx", "application/octet-stream"},
+ {"list", "text/plain"},
+ {"lma", "audio/nspaudio"},
+ {"log", "text/plain"},
+ {"lsp", "application/x-lisp"},
+ {"lst", "text/plain"},
+ {"lsx", "text/x-la-asf"},
+ {"ltx", "application/x-latex"},
+ {"lzh", "application/x-lzh"},
+ {"lzx", "application/lzx"},
+ {"m", "text/x-m"},
+ {"m1v", "video/mpeg"},
+ {"m2a", "audio/mpeg"},
+ {"m2v", "video/mpeg"},
+ {"m3u", "audio/x-mpegurl"},
+ {"man", "application/x-troff-man"},
+ {"map", "application/x-navimap"},
+ {"mar", "text/plain"},
+ {"mbd", "application/mbedlet"},
+ {"mc$", "application/x-magic-cap-package-1.0"},
+ {"mcd", "application/x-mathcad"},
+ {"mcf", "text/mcf"},
+ {"mcp", "application/netmc"},
+ {"mdc", "image/mdc"},
+ {"me", "application/x-troff-me"},
+ {"mef", "image/mef"},
+ {"mht", "message/rfc822"},
+ {"mhtml", "message/rfc822"},
+ {"mid", "audio/midi"},
+ {"midi", "audio/midi"},
+ {"mif", "application/x-mif"},
+ {"mime", "message/rfc822"},
+ {"mjf", "audio/x-vnd.audioexplosion.mjuicemediafile"},
+ {"mjpg", "video/x-motion-jpeg"},
+ {"mka", "audio/x-matroska"},
+ {"mkv", "video/x-matroska"},
+ {"mk3d", "video/x-matroska-3d"},
+ {"mm", "application/x-meme"},
+ {"mme", "application/base64"},
+ {"mod", "audio/mod"},
+ {"moov", "video/quicktime"},
+ {"mov", "video/quicktime"},
+ {"movie", "video/x-sgi-movie"},
+ {"mos", "image/mos"},
+ {"mp2", "audio/mpeg"},
+ {"mp3", "audio/mpeg3"},
+ {"mp4", "video/mp4"},
+ {"mpa", "audio/mpeg"},
+ {"mpc", "application/x-project"},
+ {"mpe", "video/mpeg"},
+ {"mpeg", "video/mpeg"},
+ {"mpg", "video/mpeg"},
+ {"mpga", "audio/mpeg"},
+ {"mpp", "application/vnd.ms-project"},
+ {"mpt", "application/x-project"},
+ {"mpv", "application/x-project"},
+ {"mpx", "application/x-project"},
+ {"mrc", "application/marc"},
+ {"mrw", "image/mrw"},
+ {"ms", "application/x-troff-ms"},
+ {"mv", "video/x-sgi-movie"},
+ {"my", "audio/make"},
+ {"mzz", "application/x-vnd.audioexplosion.mzz"},
+ {"nap", "image/naplps"},
+ {"naplps", "image/naplps"},
+ {"nc", "application/x-netcdf"},
+ {"ncm", "application/vnd.nokia.configuration-message"},
+ {"nef", "image/nef"},
+ {"nfo", "text/xml"},
+ {"nif", "image/x-niff"},
+ {"niff", "image/x-niff"},
+ {"nix", "application/x-mix-transfer"},
+ {"nrw", "image/nrw"},
+ {"nsc", "application/x-conference"},
+ {"nvd", "application/x-navidoc"},
+ {"o", "application/octet-stream"},
+ {"oda", "application/oda"},
+ {"ogg", "audio/ogg"},
+ {"omc", "application/x-omc"},
+ {"omcd", "application/x-omcdatamaker"},
+ {"omcr", "application/x-omcregerator"},
+ {"orf", "image/orf"},
+ {"p", "text/x-pascal"},
+ {"p10", "application/pkcs10"},
+ {"p12", "application/pkcs-12"},
+ {"p7a", "application/x-pkcs7-signature"},
+ {"p7c", "application/pkcs7-mime"},
+ {"p7m", "application/pkcs7-mime"},
+ {"p7r", "application/x-pkcs7-certreqresp"},
+ {"p7s", "application/pkcs7-signature"},
+ {"part", "application/pro_eng"},
+ {"pas", "text/pascal"},
+ {"pbm", "image/x-portable-bitmap"},
+ {"pcl", "application/vnd.hp-pcl"},
+ {"pct", "image/x-pict"},
+ {"pcx", "image/x-pcx"},
+ {"pdb", "chemical/x-pdb"},
+ {"pdf", "application/pdf"},
+ {"pef", "image/pef"},
+ {"pfunk", "audio/make.my.funk"},
+ {"pgm", "image/x-portable-greymap"},
+ {"pic", "image/pict"},
+ {"pict", "image/pict"},
+ {"pkg", "application/x-newton-compatible-pkg"},
+ {"pko", "application/vnd.ms-pki.pko"},
+ {"pl", "text/x-script.perl"},
+ {"plx", "application/x-pixclscript"},
+ {"pm", "text/x-script.perl-module"},
+ {"pm4", "application/x-pagemaker"},
+ {"pm5", "application/x-pagemaker"},
+ {"png", "image/png"},
+ {"pnm", "application/x-portable-anymap"},
+ {"pot", "application/vnd.ms-powerpoint"},
+ {"pov", "model/x-pov"},
+ {"ppa", "application/vnd.ms-powerpoint"},
+ {"ppm", "image/x-portable-pixmap"},
+ {"pps", "application/mspowerpoint"},
+ {"ppt", "application/mspowerpoint"},
+ {"ppz", "application/mspowerpoint"},
+ {"pre", "application/x-freelance"},
+ {"prt", "application/pro_eng"},
+ {"ps", "application/postscript"},
+ {"psd", "application/octet-stream"},
+ {"pvu", "paleovu/x-pv"},
+ {"pwz", "application/vnd.ms-powerpoint"},
+ {"py", "text/x-script.python"},
+ {"pyc", "application/x-bytecode.python"},
+ {"qcp", "audio/vnd.qcelp"},
+ {"qd3", "x-world/x-3dmf"},
+ {"qd3d", "x-world/x-3dmf"},
+ {"qif", "image/x-quicktime"},
+ {"qt", "video/quicktime"},
+ {"qtc", "video/x-qtc"},
+ {"qti", "image/x-quicktime"},
+ {"qtif", "image/x-quicktime"},
+ {"ra", "audio/x-realaudio"},
+ {"raf", "image/raf"},
+ {"ram", "audio/x-pn-realaudio"},
+ {"ras", "image/cmu-raster"},
+ {"rast", "image/cmu-raster"},
+ {"raw", "image/raw"},
+ {"rexx", "text/x-script.rexx"},
+ {"rf", "image/vnd.rn-realflash"},
+ {"rgb", "image/x-rgb"},
+ {"rm", "audio/x-pn-realaudio"},
+ {"rmi", "audio/mid"},
+ {"rmm", "audio/x-pn-realaudio"},
+ {"rmp", "audio/x-pn-realaudio"},
+ {"rng", "application/ringing-tones"},
+ {"rnx", "application/vnd.rn-realplayer"},
+ {"roff", "application/x-troff"},
+ {"rp", "image/vnd.rn-realpix"},
+ {"rpm", "audio/x-pn-realaudio-plugin"},
+ {"rt", "text/richtext"},
+ {"rtf", "text/richtext"},
+ {"rtx", "text/richtext"},
+ {"rv", "video/vnd.rn-realvideo"},
+ {"rw2", "image/rw2"},
+ {"s", "text/x-asm"},
+ {"s3m", "audio/s3m"},
+ {"saveme", "application/octet-stream"},
+ {"sbk", "application/x-tbook"},
+ {"scm", "video/x-scm"},
+ {"sdml", "text/plain"},
+ {"sdp", "application/sdp"},
+ {"sdr", "application/sounder"},
+ {"sea", "application/sea"},
+ {"set", "application/set"},
+ {"sgm", "text/sgml"},
+ {"sgml", "text/sgml"},
+ {"sh", "text/x-script.sh"},
+ {"shar", "application/x-bsh"},
+ {"shtml", "text/x-server-parsed-html"},
+ {"sid", "audio/x-psid"},
+ {"sit", "application/x-stuffit"},
+ {"skd", "application/x-koan"},
+ {"skm", "application/x-koan"},
+ {"skp", "application/x-koan"},
+ {"skt", "application/x-koan"},
+ {"sl", "application/x-seelogo"},
+ {"smi", "application/smil"},
+ {"smil", "application/smil"},
+ {"snd", "audio/basic"},
+ {"sol", "application/solids"},
+ {"spc", "text/x-speech"},
+ {"spl", "application/futuresplash"},
+ {"spr", "application/x-sprite"},
+ {"sprite", "application/x-sprite"},
+ {"src", "application/x-wais-source"},
+ {"srw", "image/srw"},
+ {"ssi", "text/x-server-parsed-html"},
+ {"ssm", "application/streamingmedia"},
+ {"sst", "application/vnd.ms-pki.certstore"},
+ {"step", "application/step"},
+ {"stl", "application/sla"},
+ {"stp", "application/step"},
+ {"sup", "application/x-pgs"},
+ {"sv4cpio", "application/x-sv4cpio"},
+ {"sv4crc", "application/x-sv4crc"},
+ {"svf", "image/vnd.dwg"},
+ {"svg", "image/svg+xml"},
+ {"svr", "application/x-world"},
+ {"swf", "application/x-shockwave-flash"},
+ {"t", "application/x-troff"},
+ {"talk", "text/x-speech"},
+ {"tar", "application/x-tar"},
+ {"tbk", "application/toolbook"},
+ {"tcl", "text/x-script.tcl"},
+ {"tcsh", "text/x-script.tcsh"},
+ {"tex", "application/x-tex"},
+ {"texi", "application/x-texinfo"},
+ {"texinfo", "application/x-texinfo"},
+ {"text", "text/plain"},
+ {"tgz", "application/x-compressed"},
+ {"tif", "image/tiff"},
+ {"tiff", "image/tiff"},
+ {"tr", "application/x-troff"},
+ {"ts", "video/mp2t"},
+ {"tsi", "audio/tsp-audio"},
+ {"tsp", "audio/tsplayer"},
+ {"tsv", "text/tab-separated-values"},
+ {"turbot", "image/florian"},
+ {"txt", "text/plain"},
+ {"uil", "text/x-uil"},
+ {"uni", "text/uri-list"},
+ {"unis", "text/uri-list"},
+ {"unv", "application/i-deas"},
+ {"uri", "text/uri-list"},
+ {"uris", "text/uri-list"},
+ {"ustar", "application/x-ustar"},
+ {"uu", "text/x-uuencode"},
+ {"uue", "text/x-uuencode"},
+ {"vcd", "application/x-cdlink"},
+ {"vcs", "text/x-vcalendar"},
+ {"vda", "application/vda"},
+ {"vdo", "video/vdo"},
+ {"vew", "application/groupwise"},
+ {"viv", "video/vivo"},
+ {"vivo", "video/vivo"},
+ {"vmd", "application/vocaltec-media-desc"},
+ {"vmf", "application/vocaltec-media-file"},
+ {"voc", "audio/voc"},
+ {"vos", "video/vosaic"},
+ {"vox", "audio/voxware"},
+ {"vqe", "audio/x-twinvq-plugin"},
+ {"vqf", "audio/x-twinvq"},
+ {"vql", "audio/x-twinvq-plugin"},
+ {"vrml", "application/x-vrml"},
+ {"vrt", "x-world/x-vrt"},
+ {"vsd", "application/x-visio"},
+ {"vst", "application/x-visio"},
+ {"vsw", "application/x-visio"},
+ {"vtt", "text/vtt"},
+ {"w60", "application/wordperfect6.0"},
+ {"w61", "application/wordperfect6.1"},
+ {"w6w", "application/msword"},
+ {"wav", "audio/wav"},
+ {"wb1", "application/x-qpro"},
+ {"wbmp", "image/vnd.wap.wbmp"},
+ {"web", "application/vnd.xara"},
+ {"webp", "image/webp"},
+ {"wiz", "application/msword"},
+ {"wk1", "application/x-123"},
+ {"wma", "audio/x-ms-wma"},
+ {"wmf", "windows/metafile"},
+ {"wml", "text/vnd.wap.wml"},
+ {"wmlc", "application/vnd.wap.wmlc"},
+ {"wmls", "text/vnd.wap.wmlscript"},
+ {"wmlsc", "application/vnd.wap.wmlscriptc"},
+ {"wmv", "video/x-ms-wmv"},
+ {"word", "application/msword"},
+ {"wp", "application/wordperfect"},
+ {"wp5", "application/wordperfect"},
+ {"wp6", "application/wordperfect"},
+ {"wpd", "application/wordperfect"},
+ {"wq1", "application/x-lotus"},
+ {"wri", "application/mswrite"},
+ {"wrl", "model/vrml"},
+ {"wrz", "model/vrml"},
+ {"wsc", "text/scriplet"},
+ {"wsrc", "application/x-wais-source"},
+ {"wtk", "application/x-wintalk"},
+ {"x3f", "image/x3f"},
+ {"xbm", "image/xbm"},
+ {"xdr", "video/x-amt-demorun"},
+ {"xgz", "xgl/drawing"},
+ {"xif", "image/vnd.xiff"},
+ {"xl", "application/excel"},
+ {"xla", "application/excel"},
+ {"xlb", "application/excel"},
+ {"xlc", "application/excel"},
+ {"xld", "application/excel"},
+ {"xlk", "application/excel"},
+ {"xll", "application/excel"},
+ {"xlm", "application/excel"},
+ {"xls", "application/excel"},
+ {"xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
+ {"xlt", "application/excel"},
+ {"xlv", "application/excel"},
+ {"xlw", "application/excel"},
+ {"xm", "audio/xm"},
+ {"xml", "text/xml"},
+ {"xmz", "xgl/movie"},
+ {"xpix", "application/x-vnd.ls-xpix"},
+ {"xpm", "image/xpm"},
+ {"x-png", "image/png"},
+ {"xspf", "application/xspf+xml"},
+ {"xsr", "video/x-amt-showrun"},
+ {"xvid", "video/x-msvideo"},
+ {"xwd", "image/x-xwd"},
+ {"xyz", "chemical/x-pdb"},
+ {"z", "application/x-compressed"},
+ {"zip", "application/zip"},
+ {"zoo", "application/octet-stream"},
+ {"zsh", "text/x-script.zsh"}}};
+
+std::string CMime::GetMimeType(const std::string &extension)
+{
+ if (extension.empty())
+ return "";
+
+ std::string ext = extension;
+ size_t posNotPoint = ext.find_first_not_of('.');
+ if (posNotPoint != std::string::npos && posNotPoint > 0)
+ ext = extension.substr(posNotPoint);
+ transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+
+ std::map<std::string, std::string>::const_iterator it = m_mimetypes.find(ext);
+ if (it != m_mimetypes.end())
+ return it->second;
+
+ return "";
+}
+
+std::string CMime::GetMimeType(const CFileItem &item)
+{
+ std::string path = item.GetDynPath();
+ if (item.HasVideoInfoTag() && !item.GetVideoInfoTag()->GetPath().empty())
+ path = item.GetVideoInfoTag()->GetPath();
+ else if (item.HasMusicInfoTag() && !item.GetMusicInfoTag()->GetURL().empty())
+ path = item.GetMusicInfoTag()->GetURL();
+
+ return GetMimeType(URIUtils::GetExtension(path));
+}
+
+std::string CMime::GetMimeType(const CURL &url, bool lookup)
+{
+
+ std::string strMimeType;
+
+ if( url.IsProtocol("shout") || url.IsProtocol("http") || url.IsProtocol("https"))
+ {
+ // If lookup is false, bail out early to leave mime type empty
+ if (!lookup)
+ return strMimeType;
+
+ std::string strmime;
+ XFILE::CCurlFile::GetMimeType(url, strmime);
+
+ // try to get mime-type again but with an NSPlayer User-Agent
+ // in order for server to provide correct mime-type. Allows us
+ // to properly detect an MMS stream
+ if (StringUtils::StartsWithNoCase(strmime, "video/x-ms-"))
+ XFILE::CCurlFile::GetMimeType(url, strmime, "NSPlayer/11.00.6001.7000");
+
+ // make sure there are no options set in mime-type
+ // mime-type can look like "video/x-ms-asf ; charset=utf8"
+ size_t i = strmime.find(';');
+ if(i != std::string::npos)
+ strmime.erase(i, strmime.length() - i);
+ StringUtils::Trim(strmime);
+ strMimeType = strmime;
+ }
+ else
+ strMimeType = GetMimeType(url.GetFileType());
+
+ // if it's still empty set to an unknown type
+ if (strMimeType.empty())
+ strMimeType = "application/octet-stream";
+
+ return strMimeType;
+}
+
+CMime::EFileType CMime::GetFileTypeFromMime(const std::string& mimeType)
+{
+ // based on http://mimesniff.spec.whatwg.org/
+
+ std::string type, subtype;
+ if (!parseMimeType(mimeType, type, subtype))
+ return FileTypeUnknown;
+
+ if (type == "application")
+ {
+ if (subtype == "zip")
+ return FileTypeZip;
+ if (subtype == "x-gzip")
+ return FileTypeGZip;
+ if (subtype == "x-rar-compressed")
+ return FileTypeRar;
+
+ if (subtype == "xml")
+ return FileTypeXml;
+ }
+ else if (type == "text")
+ {
+ if (subtype == "xml")
+ return FileTypeXml;
+ if (subtype == "html")
+ return FileTypeHtml;
+ if (subtype == "plain")
+ return FileTypePlainText;
+ }
+ else if (type == "image")
+ {
+ if (subtype == "bmp")
+ return FileTypeBmp;
+ if (subtype == "gif")
+ return FileTypeGif;
+ if (subtype == "png")
+ return FileTypePng;
+ if (subtype == "jpeg" || subtype == "pjpeg")
+ return FileTypeJpeg;
+ }
+
+ if (StringUtils::EndsWith(subtype, "+zip"))
+ return FileTypeZip;
+ if (StringUtils::EndsWith(subtype, "+xml"))
+ return FileTypeXml;
+
+ return FileTypeUnknown;
+}
+
+CMime::EFileType CMime::GetFileTypeFromContent(const std::string& fileContent)
+{
+ // based on http://mimesniff.spec.whatwg.org/#matching-a-mime-type-pattern
+
+ const size_t len = fileContent.length();
+ if (len < 2)
+ return FileTypeUnknown;
+
+ const unsigned char* const b = (const unsigned char*)fileContent.c_str();
+
+ //! @todo add detection for text types
+
+ // check image types
+ if (b[0] == 'B' && b[1] == 'M')
+ return FileTypeBmp;
+ if (len >= 6 && b[0] == 'G' && b[1] == 'I' && b[2] == 'F' && b[3] == '8' && (b[4] == '7' || b[4] == '9') && b[5] == 'a')
+ return FileTypeGif;
+ if (len >= 8 && b[0] == 0x89 && b[1] == 'P' && b[2] == 'N' && b[3] == 'G' && b[4] == 0x0D && b[5] == 0x0A && b[6] == 0x1A && b[7] == 0x0A)
+ return FileTypePng;
+ if (len >= 3 && b[0] == 0xFF && b[1] == 0xD8 && b[2] == 0xFF)
+ return FileTypeJpeg;
+
+ // check archive types
+ if (len >= 3 && b[0] == 0x1F && b[1] == 0x8B && b[2] == 0x08)
+ return FileTypeGZip;
+ if (len >= 4 && b[0] == 'P' && b[1] == 'K' && b[2] == 0x03 && b[3] == 0x04)
+ return FileTypeZip;
+ if (len >= 7 && b[0] == 'R' && b[1] == 'a' && b[2] == 'r' && b[3] == ' ' && b[4] == 0x1A && b[5] == 0x07 && b[6] == 0x00)
+ return FileTypeRar;
+
+ //! @todo add detection for other types if required
+
+ return FileTypeUnknown;
+}
+
+bool CMime::parseMimeType(const std::string& mimeType, std::string& type, std::string& subtype)
+{
+ static const char* const whitespaceChars = "\x09\x0A\x0C\x0D\x20"; // tab, LF, FF, CR and space
+
+ type.clear();
+ subtype.clear();
+
+ const size_t slashPos = mimeType.find('/');
+ if (slashPos == std::string::npos)
+ return false;
+
+ type.assign(mimeType, 0, slashPos);
+ subtype.assign(mimeType, slashPos + 1, std::string::npos);
+
+ const size_t semicolonPos = subtype.find(';');
+ if (semicolonPos != std::string::npos)
+ subtype.erase(semicolonPos);
+
+ StringUtils::Trim(type, whitespaceChars);
+ StringUtils::Trim(subtype, whitespaceChars);
+
+ if (type.empty() || subtype.empty())
+ {
+ type.clear();
+ subtype.clear();
+ return false;
+ }
+
+ StringUtils::ToLower(type);
+ StringUtils::ToLower(subtype);
+
+ return true;
+}
diff --git a/xbmc/utils/Mime.h b/xbmc/utils/Mime.h
new file mode 100644
index 0000000..d3554b9
--- /dev/null
+++ b/xbmc/utils/Mime.h
@@ -0,0 +1,46 @@
+/*
+ * 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 <map>
+#include <string>
+
+class CURL;
+
+class CFileItem;
+
+class CMime
+{
+public:
+ static std::string GetMimeType(const std::string &extension);
+ static std::string GetMimeType(const CFileItem &item);
+ static std::string GetMimeType(const CURL &url, bool lookup = true);
+
+ enum EFileType
+ {
+ FileTypeUnknown = 0,
+ FileTypeHtml,
+ FileTypeXml,
+ FileTypePlainText,
+ FileTypeZip,
+ FileTypeGZip,
+ FileTypeRar,
+ FileTypeBmp,
+ FileTypeGif,
+ FileTypePng,
+ FileTypeJpeg,
+ };
+ static EFileType GetFileTypeFromMime(const std::string& mimeType);
+ static EFileType GetFileTypeFromContent(const std::string& fileContent);
+
+private:
+ static bool parseMimeType(const std::string& mimeType, std::string& type, std::string& subtype);
+
+ static const std::map<std::string, std::string> m_mimetypes;
+};
diff --git a/xbmc/utils/MovingSpeed.cpp b/xbmc/utils/MovingSpeed.cpp
new file mode 100644
index 0000000..1e4269e
--- /dev/null
+++ b/xbmc/utils/MovingSpeed.cpp
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 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 "MovingSpeed.h"
+
+#include "utils/MathUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+
+void UTILS::MOVING_SPEED::CMovingSpeed::AddEventConfig(uint32_t eventId,
+ float acceleration,
+ float maxVelocity,
+ uint32_t resetTimeout)
+{
+ EventCfg eventCfg{acceleration, maxVelocity, resetTimeout};
+ m_eventsData.emplace(eventId, EventData{eventCfg});
+}
+
+void UTILS::MOVING_SPEED::CMovingSpeed::AddEventConfig(uint32_t eventId, EventCfg event)
+{
+ m_eventsData.emplace(eventId, EventData{event});
+}
+
+void UTILS::MOVING_SPEED::CMovingSpeed::AddEventMapConfig(MapEventConfig& configs)
+{
+ for (auto& cfg : configs)
+ {
+ AddEventConfig(static_cast<uint32_t>(cfg.first), cfg.second);
+ }
+}
+
+void UTILS::MOVING_SPEED::CMovingSpeed::Reset()
+{
+ m_currentEventId = 0;
+ for (auto& eventPair : m_eventsData)
+ {
+ Reset(eventPair.first);
+ }
+}
+
+void UTILS::MOVING_SPEED::CMovingSpeed::Reset(uint32_t eventId)
+{
+ auto mapIt = m_eventsData.find(eventId);
+ if (mapIt == m_eventsData.end())
+ {
+ CLog::LogF(LOGWARNING, "Cannot reset Event ID {} configuration", eventId);
+ }
+ else
+ {
+ EventData& event = mapIt->second;
+ event.m_currentVelocity = 1.0f;
+ event.m_lastFrameTime = 0;
+ }
+}
+
+float UTILS::MOVING_SPEED::CMovingSpeed::GetUpdatedDistance(uint32_t eventId)
+{
+ auto mapEventIt = m_eventsData.find(eventId);
+
+ if (mapEventIt == m_eventsData.end())
+ {
+ CLog::LogF(LOGDEBUG, "No event set for event ID {}", eventId);
+ return 0;
+ }
+
+ EventData& eventData = mapEventIt->second;
+ EventCfg& eventCfg = eventData.m_config;
+
+ uint32_t currentFrameTime{CTimeUtils::GetFrameTime()};
+ uint32_t deltaFrameTime{currentFrameTime - eventData.m_lastFrameTime};
+ float distance = (eventCfg.m_delta == 0.0f) ? 1.0f : eventCfg.m_delta;
+
+ if (eventData.m_lastFrameTime != 0 && deltaFrameTime > eventCfg.m_resetTimeout)
+ {
+ // If the delta time exceed the timeout then reset values
+ Reset(eventId);
+ }
+ else if (m_currentEventId != eventId)
+ {
+ // If the event id is changed then reset values
+ Reset(eventId);
+ }
+ else if (eventData.m_lastFrameTime != 0)
+ {
+ // Calculate the new speed based on time so as not to depend on the frame rate
+ eventData.m_currentVelocity +=
+ eventCfg.m_acceleration * (static_cast<float>(deltaFrameTime) / 1000);
+
+ if (eventCfg.m_maxVelocity > 0 && eventData.m_currentVelocity > eventCfg.m_maxVelocity)
+ eventData.m_currentVelocity = eventCfg.m_maxVelocity;
+
+ distance = eventData.m_currentVelocity * (static_cast<float>(deltaFrameTime) / 1000);
+ if (eventCfg.m_delta > 0.0f)
+ distance = MathUtils::RoundF(distance, eventCfg.m_delta);
+ }
+
+ m_currentEventId = eventId;
+ eventData.m_lastFrameTime = currentFrameTime;
+ return distance;
+}
+
+UTILS::MOVING_SPEED::EventType UTILS::MOVING_SPEED::ParseEventType(std::string_view eventType)
+{
+ if (eventType == "up")
+ return EventType::UP;
+ else if (eventType == "down")
+ return EventType::DOWN;
+ else if (eventType == "left")
+ return EventType::LEFT;
+ else if (eventType == "right")
+ return EventType::RIGHT;
+ else
+ {
+ CLog::LogF(LOGERROR, "Unsupported event type \"{}\"", eventType);
+ return EventType::NONE;
+ }
+}
diff --git a/xbmc/utils/MovingSpeed.h b/xbmc/utils/MovingSpeed.h
new file mode 100644
index 0000000..70214a6
--- /dev/null
+++ b/xbmc/utils/MovingSpeed.h
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2022 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 <map>
+#include <stdint.h>
+#include <string_view>
+
+namespace UTILS
+{
+namespace MOVING_SPEED
+{
+
+struct EventCfg
+{
+ /*!
+ * \param acceleration Acceleration in pixels per second (px/sec)
+ * \param maxVelocity Max movement speed, it depends from acceleration value,
+ * a suitable value could be (acceleration value)*3. Set 0 to disable it
+ * \param resetTimeout Resets acceleration speed if idle for specified millisecs
+ */
+ EventCfg(float acceleration, float maxVelocity, uint32_t resetTimeout)
+ : m_acceleration{acceleration}, m_maxVelocity{maxVelocity}, m_resetTimeout{resetTimeout}
+ {
+ }
+
+ /*!
+ * \param acceleration Acceleration in pixels per second (px/sec)
+ * \param maxVelocity Max movement speed, it depends from acceleration value,
+ * a suitable value could be (acceleration value)*3. Set 0 to disable it
+ * \param resetTimeout Resets acceleration speed if idle for specified millisecs
+ * \param delta Specify the minimal increment step, and the result of distance
+ value will be rounded by delta. Set 0 to disable it
+ */
+ EventCfg(float acceleration, float maxVelocity, uint32_t resetTimeout, float delta)
+ : m_acceleration{acceleration},
+ m_maxVelocity{maxVelocity},
+ m_resetTimeout{resetTimeout},
+ m_delta{delta}
+ {
+ }
+
+ float m_acceleration;
+ float m_maxVelocity;
+ uint32_t m_resetTimeout;
+ float m_delta{0};
+};
+
+enum class EventType
+{
+ NONE = 0,
+ UP,
+ DOWN,
+ LEFT,
+ RIGHT
+};
+
+typedef std::map<EventType, EventCfg> MapEventConfig;
+
+/*!
+ * \brief Class to calculate the velocity for a motion effect.
+ * To ensure it works, the GetUpdatedDistance method must be called at each
+ * input received (e.g. continuous key press of same key on the keyboard).
+ * The motion effect will stop at the event ID change (different key pressed).
+ */
+class CMovingSpeed
+{
+public:
+ /*!
+ * \brief Add the configuration for an event
+ * \param eventId The id for the event, must be unique
+ * \param acceleration Acceleration in pixels per second (px/sec)
+ * \param maxVelocity Max movement speed, it depends from acceleration value,
+ * a suitable value could be (acceleration value)*3. Set 0 to disable it
+ * \param resetTimeout Resets acceleration speed if idle for specified millisecs
+ */
+ void AddEventConfig(uint32_t eventId,
+ float acceleration,
+ float maxVelocity,
+ uint32_t resetTimeout);
+
+ /*!
+ * \brief Add the configuration for an event
+ * \param eventId The id for the event, must be unique
+ * \param event The event configuration
+ */
+ void AddEventConfig(uint32_t eventId, EventCfg event);
+
+ /*!
+ * \brief Add a map of events configuration
+ * \param configs The map of events configuration where key value is event id,
+ */
+ void AddEventMapConfig(MapEventConfig& configs);
+
+ /*!
+ * \brief Reset stored velocity to all events
+ * \param event The event configuration
+ */
+ void Reset();
+
+ /*!
+ * \brief Reset stored velocity for a specific event
+ * \param event The event ID
+ */
+ void Reset(uint32_t eventId);
+
+ /*!
+ * \brief Get the updated distance based on acceleration speed
+ * \param eventId The id for the event to handle
+ * \return The distance
+ */
+ float GetUpdatedDistance(uint32_t eventId);
+
+ /*!
+ * \brief Get the updated distance based on acceleration speed
+ * \param eventType The event type to handle
+ * \return The distance
+ */
+ float GetUpdatedDistance(EventType eventType)
+ {
+ return GetUpdatedDistance(static_cast<uint32_t>(eventType));
+ }
+
+private:
+ struct EventData
+ {
+ EventData(EventCfg config) : m_config{config} {}
+
+ EventCfg m_config;
+ float m_currentVelocity{1.0f};
+ uint32_t m_lastFrameTime{0};
+ };
+
+ uint32_t m_currentEventId{0};
+ std::map<uint32_t, EventData> m_eventsData;
+};
+
+/*!
+ * \brief Parse a string event type to enum EventType.
+ * \param eventType The event type as string
+ * \return The EventType if has success, otherwise EventType::NONE
+ */
+EventType ParseEventType(std::string_view eventType);
+
+} // namespace MOVING_SPEED
+} // namespace UTILS
diff --git a/xbmc/utils/Observer.cpp b/xbmc/utils/Observer.cpp
new file mode 100644
index 0000000..5c60a4b
--- /dev/null
+++ b/xbmc/utils/Observer.cpp
@@ -0,0 +1,71 @@
+/*
+ * 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 "Observer.h"
+
+#include <algorithm>
+#include <mutex>
+
+Observable &Observable::operator=(const Observable &observable)
+{
+ std::unique_lock<CCriticalSection> lock(m_obsCritSection);
+
+ m_bObservableChanged = static_cast<bool>(observable.m_bObservableChanged);
+ m_observers = observable.m_observers;
+
+ return *this;
+}
+
+bool Observable::IsObserving(const Observer &obs) const
+{
+ std::unique_lock<CCriticalSection> lock(m_obsCritSection);
+ return std::find(m_observers.begin(), m_observers.end(), &obs) != m_observers.end();
+}
+
+void Observable::RegisterObserver(Observer *obs)
+{
+ std::unique_lock<CCriticalSection> lock(m_obsCritSection);
+ if (!IsObserving(*obs))
+ {
+ m_observers.push_back(obs);
+ }
+}
+
+void Observable::UnregisterObserver(Observer *obs)
+{
+ std::unique_lock<CCriticalSection> lock(m_obsCritSection);
+ auto iter = std::remove(m_observers.begin(), m_observers.end(), obs);
+ if (iter != m_observers.end())
+ m_observers.erase(iter);
+}
+
+void Observable::NotifyObservers(const ObservableMessage message /* = ObservableMessageNone */)
+{
+ // Make sure the set/compare is atomic
+ // so we don't clobber the variable in a race condition
+ auto bNotify = m_bObservableChanged.exchange(false);
+
+ if (bNotify)
+ SendMessage(message);
+}
+
+void Observable::SetChanged(bool SetTo)
+{
+ m_bObservableChanged = SetTo;
+}
+
+void Observable::SendMessage(const ObservableMessage message)
+{
+ std::unique_lock<CCriticalSection> lock(m_obsCritSection);
+
+ for (auto& observer : m_observers)
+ {
+ observer->Notify(*this, message);
+ }
+}
diff --git a/xbmc/utils/Observer.h b/xbmc/utils/Observer.h
new file mode 100644
index 0000000..49c9b3c
--- /dev/null
+++ b/xbmc/utils/Observer.h
@@ -0,0 +1,95 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+#include <atomic>
+#include <vector>
+
+class Observable;
+class ObservableMessageJob;
+
+typedef enum
+{
+ ObservableMessageNone,
+ ObservableMessagePeripheralsChanged,
+ ObservableMessageSettingsChanged,
+ ObservableMessageButtonMapsChanged,
+ // Used for example when the subtitle alignment position change
+ ObservableMessagePositionChanged,
+ ObservableMessageGamePortsChanged,
+ ObservableMessageGameAgentsChanged,
+} ObservableMessage;
+
+class Observer
+{
+public:
+ Observer() = default;
+ virtual ~Observer() = default;
+ /*!
+ * @brief Process a message from an observable.
+ * @param obs The observable that sends the message.
+ * @param msg The message.
+ */
+ virtual void Notify(const Observable &obs, const ObservableMessage msg) = 0;
+};
+
+class Observable
+{
+ friend class ObservableMessageJob;
+
+public:
+ Observable() = default;
+ virtual ~Observable() = default;
+ Observable& operator=(const Observable& observable);
+
+ /*!
+ * @brief Register an observer.
+ * @param obs The observer to register.
+ */
+ virtual void RegisterObserver(Observer *obs);
+
+ /*!
+ * @brief Unregister an observer.
+ * @param obs The observer to unregister.
+ */
+ virtual void UnregisterObserver(Observer *obs);
+
+ /*!
+ * @brief Send a message to all observers when m_bObservableChanged is true.
+ * @param message The message to send.
+ */
+ virtual void NotifyObservers(const ObservableMessage message = ObservableMessageNone);
+
+ /*!
+ * @brief Mark an observable changed.
+ * @param bSetTo True to mark the observable changed, false to mark it as unchanged.
+ */
+ virtual void SetChanged(bool bSetTo = true);
+
+ /*!
+ * @brief Check whether this observable is being observed by an observer.
+ * @param obs The observer to check.
+ * @return True if this observable is being observed by the given observer, false otherwise.
+ */
+ virtual bool IsObserving(const Observer &obs) const;
+
+protected:
+ /*!
+ * @brief Send a message to all observer when m_bObservableChanged is true.
+ * @param obs The observer that sends the message.
+ * @param message The message to send.
+ */
+ void SendMessage(const ObservableMessage message);
+
+ std::atomic<bool> m_bObservableChanged{false}; /*!< true when the observable is marked as changed, false otherwise */
+ std::vector<Observer *> m_observers; /*!< all observers */
+ mutable CCriticalSection m_obsCritSection; /*!< mutex */
+};
diff --git a/xbmc/utils/POUtils.cpp b/xbmc/utils/POUtils.cpp
new file mode 100644
index 0000000..830336d
--- /dev/null
+++ b/xbmc/utils/POUtils.cpp
@@ -0,0 +1,313 @@
+/*
+ * 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 "utils/POUtils.h"
+
+#include "URL.h"
+#include "filesystem/File.h"
+#include "utils/log.h"
+
+#include <stdlib.h>
+
+CPODocument::CPODocument()
+{
+ m_CursorPos = 0;
+ m_nextEntryPos = 0;
+ m_POfilelength = 0;
+ m_Entry.msgStrPlural.clear();
+ m_Entry.msgStrPlural.resize(1);
+}
+
+CPODocument::~CPODocument() = default;
+
+bool CPODocument::LoadFile(const std::string &pofilename)
+{
+ CURL poFileUrl(pofilename);
+ if (!XFILE::CFile::Exists(poFileUrl))
+ return false;
+
+ XFILE::CFile file;
+ std::vector<uint8_t> buf;
+ if (file.LoadFile(poFileUrl, buf) < 18) // at least a size of a minimalistic header
+ {
+ CLog::Log(LOGERROR, "{}: can't load file \"{}\" or file is too small", __FUNCTION__,
+ pofilename);
+ return false;
+ }
+
+ m_strBuffer = '\n';
+ m_strBuffer.append(reinterpret_cast<char*>(buf.data()), buf.size());
+ buf.clear();
+
+ ConvertLineEnds(pofilename);
+
+ // we make sure, to have an LF at the end of buffer
+ if (*m_strBuffer.rbegin() != '\n')
+ {
+ m_strBuffer += "\n";
+ }
+
+ m_POfilelength = m_strBuffer.size();
+
+ if (GetNextEntry() && m_Entry.Type == MSGID_FOUND)
+ return true;
+
+ CLog::Log(LOGERROR, "POParser: unable to read PO file header from file: {}", pofilename);
+ return false;
+}
+
+bool CPODocument::GetNextEntry()
+{
+ do
+ {
+ // if we don't find LFLF, we reached the end of the buffer and the last entry to check
+ // we indicate this with setting m_nextEntryPos to the end of the buffer
+ if ((m_nextEntryPos = m_strBuffer.find("\n\n", m_CursorPos)) == std::string::npos)
+ m_nextEntryPos = m_POfilelength-1;
+
+ // now we read the actual entry into a temp string for further processing
+ m_Entry.Content.assign(m_strBuffer, m_CursorPos, m_nextEntryPos - m_CursorPos +1);
+ m_CursorPos = m_nextEntryPos+1; // jump cursor to the second LF character
+
+ if (FindLineStart ("\nmsgid ", m_Entry.msgID.Pos))
+ {
+ if (FindLineStart ("\nmsgctxt \"#", m_Entry.xIDPos) && ParseNumID())
+ {
+ m_Entry.Type = ID_FOUND; // we found an entry with a valid numeric id
+ return true;
+ }
+
+ size_t plurPos;
+ if (FindLineStart ("\nmsgid_plural ", plurPos))
+ {
+ m_Entry.Type = MSGID_PLURAL_FOUND; // we found a pluralized entry
+ return true;
+ }
+
+ m_Entry.Type = MSGID_FOUND; // we found a normal entry, with no numeric id
+ return true;
+ }
+ }
+ while (m_nextEntryPos != m_POfilelength-1);
+ // we reached the end of buffer AND we have not found a valid entry
+
+ return false;
+}
+
+void CPODocument::ParseEntry(bool bisSourceLang)
+{
+ if (bisSourceLang)
+ {
+ if (m_Entry.Type == ID_FOUND)
+ GetString(m_Entry.msgID);
+ else
+ m_Entry.msgID.Str.clear();
+ return;
+ }
+
+ if (m_Entry.Type != ID_FOUND)
+ {
+ GetString(m_Entry.msgID);
+ if (FindLineStart ("\nmsgctxt ", m_Entry.msgCtxt.Pos))
+ GetString(m_Entry.msgCtxt);
+ else
+ m_Entry.msgCtxt.Str.clear();
+ }
+
+ if (m_Entry.Type != MSGID_PLURAL_FOUND)
+ {
+ if (FindLineStart ("\nmsgstr ", m_Entry.msgStr.Pos))
+ {
+ GetString(m_Entry.msgStr);
+ GetString(m_Entry.msgID);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "POParser: missing msgstr line in entry. Failed entry: {}",
+ m_Entry.Content);
+ m_Entry.msgStr.Str.clear();
+ }
+ return;
+ }
+
+ // We found a plural form entry. We read it into a vector of CStrEntry types
+ m_Entry.msgStrPlural.clear();
+ std::string strPattern = "\nmsgstr[0] ";
+ CStrEntry strEntry;
+
+ for (int n=0; n<7 ; n++)
+ {
+ strPattern[8] = static_cast<char>(n+'0');
+ if (FindLineStart (strPattern, strEntry.Pos))
+ {
+ GetString(strEntry);
+ if (strEntry.Str.empty())
+ break;
+ m_Entry.msgStrPlural.push_back(strEntry);
+ }
+ else
+ break;
+ }
+
+ if (m_Entry.msgStrPlural.empty())
+ {
+ CLog::Log(LOGERROR,
+ "POParser: msgstr[] plural lines have zero valid strings. "
+ "Failed entry: {}",
+ m_Entry.Content);
+ m_Entry.msgStrPlural.resize(1); // Put 1 element with an empty string into the vector
+ }
+}
+
+const std::string& CPODocument::GetPlurMsgstr(size_t plural) const
+{
+ if (m_Entry.msgStrPlural.size() < plural+1)
+ {
+ CLog::Log(LOGERROR,
+ "POParser: msgstr[{}] plural field requested, but not found in PO file. "
+ "Failed entry: {}",
+ static_cast<int>(plural), m_Entry.Content);
+ plural = m_Entry.msgStrPlural.size()-1;
+ }
+ return m_Entry.msgStrPlural[plural].Str;
+}
+
+std::string CPODocument::UnescapeString(const std::string &strInput)
+{
+ std::string strOutput;
+ if (strInput.empty())
+ return strOutput;
+
+ char oescchar;
+ strOutput.reserve(strInput.size());
+ std::string::const_iterator it = strInput.begin();
+ while (it < strInput.end())
+ {
+ oescchar = *it++;
+ if (oescchar == '\\')
+ {
+ if (it == strInput.end())
+ {
+ CLog::Log(LOGERROR,
+ "POParser: warning, unhandled escape character "
+ "at line-end. Problematic entry: {}",
+ m_Entry.Content);
+ break;
+ }
+ switch (*it++)
+ {
+ case 'a': oescchar = '\a'; break;
+ case 'b': oescchar = '\b'; break;
+ case 'v': oescchar = '\v'; break;
+ case 'n': oescchar = '\n'; break;
+ case 't': oescchar = '\t'; break;
+ case 'r': oescchar = '\r'; break;
+ case '"': oescchar = '"' ; break;
+ case '0': oescchar = '\0'; break;
+ case 'f': oescchar = '\f'; break;
+ case '?': oescchar = '\?'; break;
+ case '\'': oescchar = '\''; break;
+ case '\\': oescchar = '\\'; break;
+
+ default:
+ {
+ CLog::Log(LOGERROR,
+ "POParser: warning, unhandled escape character. Problematic entry: {}",
+ m_Entry.Content);
+ continue;
+ }
+ }
+ }
+ strOutput.push_back(oescchar);
+ }
+ return strOutput;
+}
+
+bool CPODocument::FindLineStart(const std::string &strToFind, size_t &FoundPos)
+{
+
+ FoundPos = m_Entry.Content.find(strToFind);
+
+ if (FoundPos == std::string::npos || FoundPos + strToFind.size() + 2 > m_Entry.Content.size())
+ return false; // if we don't find the string or if we don't have at least one char after it
+
+ FoundPos += strToFind.size(); // to set the pos marker to the exact start of the real data
+ return true;
+}
+
+bool CPODocument::ParseNumID()
+{
+ if (isdigit(m_Entry.Content.at(m_Entry.xIDPos))) // verify if the first char is digit
+ {
+ // we check for the numeric id for the fist 10 chars (uint32)
+ m_Entry.xID = strtol(&m_Entry.Content[m_Entry.xIDPos], NULL, 10);
+ return true;
+ }
+
+ CLog::Log(LOGERROR, "POParser: found numeric id descriptor, but no valid id can be read, "
+ "entry was handled as normal msgid entry");
+ CLog::Log(LOGERROR, "POParser: The problematic entry: {}", m_Entry.Content);
+ return false;
+}
+
+void CPODocument::GetString(CStrEntry &strEntry)
+{
+ size_t nextLFPos;
+ size_t startPos = strEntry.Pos;
+ strEntry.Str.clear();
+
+ while (startPos < m_Entry.Content.size())
+ {
+ nextLFPos = m_Entry.Content.find('\n', startPos);
+ if (nextLFPos == std::string::npos)
+ nextLFPos = m_Entry.Content.size();
+
+ // check syntax, if it really is a valid quoted string line
+ if (nextLFPos-startPos < 2 || m_Entry.Content[startPos] != '\"' ||
+ m_Entry.Content[nextLFPos-1] != '\"')
+ break;
+
+ strEntry.Str.append(m_Entry.Content, startPos+1, nextLFPos-2-startPos);
+ startPos = nextLFPos+1;
+ }
+
+ strEntry.Str = UnescapeString(strEntry.Str);
+}
+
+void CPODocument::ConvertLineEnds(const std::string &filename)
+{
+ size_t foundPos = m_strBuffer.find_first_of('\r');
+ if (foundPos == std::string::npos)
+ return; // We have only Linux style line endings in the file, nothing to do
+
+ if (foundPos+1 >= m_strBuffer.size() || m_strBuffer[foundPos+1] != '\n')
+ CLog::Log(LOGDEBUG,
+ "POParser: PO file has Mac Style Line Endings. "
+ "Converted in memory to Linux LF for file: {}",
+ filename);
+ else
+ CLog::Log(LOGDEBUG,
+ "POParser: PO file has Win Style Line Endings. "
+ "Converted in memory to Linux LF for file: {}",
+ filename);
+
+ std::string strTemp;
+ strTemp.reserve(m_strBuffer.size());
+ for (std::string::const_iterator it = m_strBuffer.begin(); it < m_strBuffer.end(); ++it)
+ {
+ if (*it == '\r')
+ {
+ if (it+1 == m_strBuffer.end() || *(it+1) != '\n')
+ strTemp.push_back('\n'); // convert Mac style line ending and continue
+ continue; // we have Win style line ending so we exclude this CR now
+ }
+ strTemp.push_back(*it);
+ }
+ m_strBuffer.swap(strTemp);
+ m_POfilelength = m_strBuffer.size();
+}
diff --git a/xbmc/utils/POUtils.h b/xbmc/utils/POUtils.h
new file mode 100644
index 0000000..1752b79
--- /dev/null
+++ b/xbmc/utils/POUtils.h
@@ -0,0 +1,162 @@
+/*
+ * 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 <stdint.h>
+#include <string>
+#include <vector>
+
+typedef enum
+{
+ ID_FOUND = 0, // We have an entry with a numeric (previously XML) identification number.
+ MSGID_FOUND = 1, // We have a classic gettext entry with textual msgid. No numeric ID.
+ MSGID_PLURAL_FOUND = 2 // We have a classic gettext entry with textual msgid in plural form.
+} POIdType;
+
+enum
+{
+ ISSOURCELANG=true
+};
+
+// Struct to hold current position and text of the string field in the main PO entry.
+struct CStrEntry
+{
+ size_t Pos;
+ std::string Str;
+};
+
+// Struct to collect all important data of the current processed entry.
+struct CPOEntry
+{
+ int Type;
+ uint32_t xID;
+ size_t xIDPos;
+ std::string Content;
+ CStrEntry msgCtxt;
+ CStrEntry msgID;
+ CStrEntry msgStr;
+ std::vector<CStrEntry> msgStrPlural;
+};
+
+class CPODocument
+{
+public:
+ CPODocument();
+ ~CPODocument();
+
+ /*! \brief Tries to load a PO file into a temporary memory buffer.
+ * It also tries to parse the header of the PO file.
+ \param pofilename filename of the PO file to load.
+ \return true if the load was successful, unless return false
+ */
+ bool LoadFile(const std::string &pofilename);
+
+ /*! \brief Fast jumps to the next entry in PO buffer.
+ * Finds next entry started with "#: id:" or msgctx or msgid.
+ * to be as fast as possible this does not even get the id number
+ * just the type of the entry found. GetEntryID() has to be called
+ * for getting the id. After that ParseEntry() needs a call for
+ * actually getting the msg strings. The reason for this is to
+ * have calls and checks as fast as possible generally and specially
+ * for parsing weather tokens and to parse only the needed strings from
+ * the fallback language (missing from the gui language translation)
+ \return true if there was an entry found, false if reached the end of buffer
+ */
+ bool GetNextEntry();
+
+ /*! \brief Gets the type of entry found with GetNextEntry.
+ \return the type of entry: ID_FOUND || MSGID_FOUND || MSGID_PLURAL_FOUND
+ */
+ int GetEntryType() const {return m_Entry.Type;}
+
+ /*! \brief Parses the numeric ID from current entry.
+ * This function can only be called right after GetNextEntry()
+ * to make sure that we have a valid entry detected.
+ \return parsed ID number
+ */
+ uint32_t GetEntryID() const {return m_Entry.xID;}
+
+ /*! \brief Parses current entry.
+ * Reads msgid, msgstr, msgstr[x], msgctxt strings.
+ * Note that this function also back-converts the c++ style escape sequences.
+ * The function only parses the needed strings, considering if it is a source language file.
+ \param bisSourceLang if we parse a source English file.
+ */
+ void ParseEntry(bool bisSourceLang);
+
+ /*! \brief Gets the msgctxt string previously parsed by ParseEntry().
+ \return string* containing the msgctxt string, unescaped and linked together.
+ */
+ const std::string& GetMsgctxt() const {return m_Entry.msgCtxt.Str;}
+
+ /*! \brief Gets the msgid string previously parsed by ParseEntry().
+ \return string* containing the msgid string, unescaped and linked together.
+ */
+ const std::string& GetMsgid() const {return m_Entry.msgID.Str;}
+
+ /*! \brief Gets the msgstr string previously parsed by ParseEntry().
+ \return string* containing the msgstr string, unescaped and linked together.
+ */
+ const std::string& GetMsgstr() const {return m_Entry.msgStr.Str;}
+
+ /*! \brief Gets the msgstr[x] string previously parsed by ParseEntry().
+ \param plural the number of plural-form expected to get (0-6).
+ \return string* containing the msgstr string, unescaped and linked together.
+ */
+ const std::string& GetPlurMsgstr (size_t plural) const;
+
+protected:
+
+ /*! \brief Converts c++ style char escape sequences back to char.
+ * Supports: \a \v \n \t \r \" \0 \f \? \' \\
+ \param strInput string contains the string to be unescaped.
+ \return unescaped string.
+ */
+ std::string UnescapeString(const std::string &strInput);
+
+ /*! \brief Finds the position of line, starting with a given string in current entry.
+ * This function can only be called after GetNextEntry()
+ \param strToFind a string what we look for, at beginning of the lines.
+ \param FoundPos will get the position where we found the line starting with the string.
+ \return false if no line like that can be found in the entry (m_Entry)
+ */
+ bool FindLineStart(const std::string &strToFind, size_t &FoundPos);
+
+ /*! \brief Reads, and links together the quoted strings found with ParseEntry().
+ * This function can only be called after GetNextEntry() called.
+ \param strEntry.Str a string where we get the appended string lines.
+ \param strEntry.Pos the position in m_Entry.Content to start reading the string.
+ */
+ void GetString(CStrEntry &strEntry);
+
+ /*! \brief Parses the numeric id and checks if it is valid.
+ * This function can only be called after GetNextEntry()
+ * It checks m_Entry.Content at position m_Entry.xIDPos for the numeric id.
+ * The converted ID number goes into m_Entry.xID for public read out.
+ \return false, if parse and convert of the id number was unsuccessful.
+ */
+ bool ParseNumID();
+
+ /*! \brief If we have Windows or Mac line-end chars in PO file, convert them to Unix LFs
+ */
+ void ConvertLineEnds(const std::string &filename);
+
+ // Temporary string buffer to read file in.
+ std::string m_strBuffer;
+ // Size of the string buffer.
+ size_t m_POfilelength;
+
+ // Current cursor position in m_strBuffer.
+ size_t m_CursorPos;
+ // The next PO entry position in m_strBuffer.
+ size_t m_nextEntryPos;
+
+ // Variable to hold all data of currently processed entry.
+ CPOEntry m_Entry;
+};
diff --git a/xbmc/utils/PlayerUtils.cpp b/xbmc/utils/PlayerUtils.cpp
new file mode 100644
index 0000000..3fd6847
--- /dev/null
+++ b/xbmc/utils/PlayerUtils.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 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 "PlayerUtils.h"
+
+#include "FileItem.h"
+#include "music/MusicUtils.h"
+#include "utils/Variant.h"
+#include "video/VideoUtils.h"
+
+bool CPlayerUtils::IsItemPlayable(const CFileItem& itemIn)
+{
+ const CFileItem item(itemIn.GetItemToPlay());
+
+ // General
+ if (item.IsParentFolder())
+ return false;
+
+ // Plugins
+ if (item.IsPlugin() && item.GetProperty("isplayable").asBoolean())
+ return true;
+
+ // Music
+ if (MUSIC_UTILS::IsItemPlayable(item))
+ return true;
+
+ // Movies / TV Shows / Music Videos
+ if (VIDEO_UTILS::IsItemPlayable(item))
+ return true;
+
+ //! @todo add more types on demand.
+
+ return false;
+}
diff --git a/xbmc/utils/PlayerUtils.h b/xbmc/utils/PlayerUtils.h
new file mode 100644
index 0000000..f62d891
--- /dev/null
+++ b/xbmc/utils/PlayerUtils.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) 2022 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 CFileItem;
+
+class CPlayerUtils
+{
+public:
+ static bool IsItemPlayable(const CFileItem& item);
+};
diff --git a/xbmc/utils/ProgressJob.cpp b/xbmc/utils/ProgressJob.cpp
new file mode 100644
index 0000000..6ef1f24
--- /dev/null
+++ b/xbmc/utils/ProgressJob.cpp
@@ -0,0 +1,185 @@
+/*
+ * 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 "ProgressJob.h"
+
+#include "ServiceBroker.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/Variant.h"
+
+#include <math.h>
+
+CProgressJob::CProgressJob()
+ : m_progress(NULL),
+ m_progressDialog(NULL)
+{ }
+
+CProgressJob::CProgressJob(CGUIDialogProgressBarHandle* progressBar)
+ : m_progress(progressBar),
+ m_progressDialog(NULL)
+{ }
+
+CProgressJob::~CProgressJob()
+{
+ MarkFinished();
+
+ m_progress = NULL;
+ m_progressDialog = NULL;
+}
+
+bool CProgressJob::ShouldCancel(unsigned int progress, unsigned int total) const
+{
+ if (IsCancelled())
+ return true;
+
+ SetProgress(progress, total);
+
+ return CJob::ShouldCancel(progress, total);
+}
+
+bool CProgressJob::DoModal()
+{
+ m_progress = NULL;
+
+ // get a progress dialog if we don't already have one
+ if (m_progressDialog == NULL)
+ {
+ m_progressDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+
+ if (m_progressDialog == NULL)
+ return false;
+ }
+
+ m_modal = true;
+
+ // do the work
+ bool result = DoWork();
+
+ // mark the progress dialog as finished (will close it)
+ MarkFinished();
+ m_modal = false;
+
+ return result;
+}
+
+void CProgressJob::SetProgressIndicators(CGUIDialogProgressBarHandle* progressBar, CGUIDialogProgress* progressDialog, bool updateProgress /* = true */, bool updateInformation /* = true */)
+{
+ SetProgressBar(progressBar);
+ SetProgressDialog(progressDialog);
+ SetUpdateProgress(updateProgress);
+ SetUpdateInformation(updateInformation);
+
+ // disable auto-closing
+ SetAutoClose(false);
+}
+
+void CProgressJob::ShowProgressDialog() const
+{
+ if (!IsModal() || m_progressDialog == NULL ||
+ m_progressDialog->IsDialogRunning())
+ return;
+
+ // show the progress dialog as a modal dialog with a progress bar
+ m_progressDialog->Open();
+ m_progressDialog->ShowProgressBar(true);
+}
+
+void CProgressJob::SetTitle(const std::string &title)
+{
+ if (!m_updateInformation)
+ return;
+
+ if (m_progress != NULL)
+ m_progress->SetTitle(title);
+ else if (m_progressDialog != NULL)
+ {
+ m_progressDialog->SetHeading(CVariant{title});
+
+ ShowProgressDialog();
+ }
+}
+
+void CProgressJob::SetText(const std::string &text)
+{
+ if (!m_updateInformation)
+ return;
+
+ if (m_progress != NULL)
+ m_progress->SetText(text);
+ else if (m_progressDialog != NULL)
+ {
+ m_progressDialog->SetText(CVariant{text});
+
+ ShowProgressDialog();
+ }
+}
+
+void CProgressJob::SetProgress(float percentage) const
+{
+ if (!m_updateProgress)
+ return;
+
+ if (m_progress != NULL)
+ m_progress->SetPercentage(percentage);
+ else if (m_progressDialog != NULL)
+ {
+ ShowProgressDialog();
+
+ int iPercentage = static_cast<int>(ceil(percentage));
+ // only change and update the progress bar if its percentage value changed
+ // (this can have a huge impact on performance if it's called a lot)
+ if (iPercentage != m_progressDialog->GetPercentage())
+ {
+ m_progressDialog->SetPercentage(iPercentage);
+ m_progressDialog->Progress();
+ }
+ }
+}
+
+void CProgressJob::SetProgress(int currentStep, int totalSteps) const
+{
+ if (!m_updateProgress)
+ return;
+
+ if (m_progress != NULL)
+ m_progress->SetProgress(currentStep, totalSteps);
+ else if (m_progressDialog != NULL)
+ SetProgress((static_cast<float>(currentStep) * 100.0f) / totalSteps);
+}
+
+void CProgressJob::MarkFinished()
+{
+ if (m_progress != NULL)
+ {
+ if (m_updateProgress)
+ {
+ m_progress->MarkFinished();
+ // We don't own this pointer and it will be deleted after it's marked finished
+ // just set it to nullptr so we don't try to use it again
+ m_progress = nullptr;
+ }
+ }
+ else if (m_progressDialog != NULL && m_autoClose)
+ m_progressDialog->Close();
+}
+
+bool CProgressJob::IsCancelled() const
+{
+ if (m_progressDialog != NULL)
+ return m_progressDialog->IsCanceled();
+
+ return false;
+}
+
+bool CProgressJob::HasProgressIndicator() const
+{
+ return m_progress != nullptr || m_progressDialog != nullptr;
+}
diff --git a/xbmc/utils/ProgressJob.h b/xbmc/utils/ProgressJob.h
new file mode 100644
index 0000000..f1117aa
--- /dev/null
+++ b/xbmc/utils/ProgressJob.h
@@ -0,0 +1,163 @@
+/*
+ * 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 "utils/Job.h"
+
+#include <string>
+
+class CGUIDialogProgress;
+class CGUIDialogProgressBarHandle;
+
+/*!
+ \brief Basic implementation of a CJob with a progress bar to indicate the
+ progress of the job being processed.
+ */
+class CProgressJob : public CJob
+{
+public:
+ ~CProgressJob() override;
+
+ // implementation of CJob
+ const char *GetType() const override { return "ProgressJob"; }
+ bool operator==(const CJob* job) const override { return false; }
+ bool ShouldCancel(unsigned int progress, unsigned int total) const override;
+
+ /*!
+ \brief Executes the job showing a modal progress dialog.
+ */
+ bool DoModal();
+
+ /*!
+ \brief Sets the given progress indicators to be used during execution of
+ the job.
+
+ \details This automatically disables auto-closing the given progress
+ indicators once the job has been finished.
+
+ \param progressBar Progress bar handle to be used.
+ \param progressDialog Progress dialog to be used.
+ \param updateProgress (optional) Whether to show progress updates.
+ \param updateInformation (optional) Whether to show progress information.
+ */
+ void SetProgressIndicators(CGUIDialogProgressBarHandle* progressBar, CGUIDialogProgress* progressDialog, bool updateProgress = true, bool updateInformation = true);
+
+ bool HasProgressIndicator() const;
+
+protected:
+ CProgressJob();
+ explicit CProgressJob(CGUIDialogProgressBarHandle* progressBar);
+
+ /*!
+ \brief Whether the job is being run modally or in the background.
+ */
+ bool IsModal() const { return m_modal; }
+
+ /*!
+ \brief Returns the progress bar indicating the progress of the job.
+ */
+ CGUIDialogProgressBarHandle* GetProgressBar() const { return m_progress; }
+
+ /*!
+ \brief Sets the progress bar indicating the progress of the job.
+ */
+ void SetProgressBar(CGUIDialogProgressBarHandle* progress) { m_progress = progress; }
+
+ /*!
+ \brief Returns the progress dialog indicating the progress of the job.
+ */
+ CGUIDialogProgress* GetProgressDialog() const { return m_progressDialog; }
+
+ /*!
+ \brief Sets the progress bar indicating the progress of the job.
+ */
+ void SetProgressDialog(CGUIDialogProgress* progressDialog) { m_progressDialog = progressDialog; }
+
+ /*!
+ \brief Whether to automatically close the progress indicator in MarkFinished().
+ */
+ bool GetAutoClose() { return m_autoClose; }
+
+ /*!
+ \brief Set whether to automatically close the progress indicator in MarkFinished().
+ */
+ void SetAutoClose(bool autoClose) { m_autoClose = autoClose; }
+
+ /*!
+ \brief Whether to update the progress bar or not.
+ */
+ bool GetUpdateProgress() { return m_updateProgress; }
+
+ /*!
+ \brief Set whether to update the progress bar or not.
+ */
+ void SetUpdateProgress(bool updateProgress) { m_updateProgress = updateProgress; }
+
+ /*!
+ \brief Whether to update the progress information or not.
+ */
+ bool GetUpdateInformation() { return m_updateInformation; }
+
+ /*!
+ \brief Set whether to update the progress information or not.
+ */
+ void SetUpdateInformation(bool updateInformation) { m_updateInformation = updateInformation; }
+
+ /*!
+ \brief Makes sure that the modal dialog is being shown.
+ */
+ void ShowProgressDialog() const;
+
+ /*!
+ \brief Sets the given title as the title of the progress bar.
+
+ \param[in] title Title to be set
+ */
+ void SetTitle(const std::string &title);
+
+ /*!
+ \brief Sets the given text as the description of the progress bar.
+
+ \param[in] text Text to be set
+ */
+ void SetText(const std::string &text);
+
+ /*!
+ \brief Sets the progress of the progress bar to the given value in percentage.
+
+ \param[in] percentage Percentage to be set as the current progress
+ */
+ void SetProgress(float percentage) const;
+
+ /*!
+ \brief Sets the progress of the progress bar to the given value.
+
+ \param[in] currentStep Current step being processed
+ \param[in] totalSteps Total steps to be processed
+ */
+ void SetProgress(int currentStep, int totalSteps) const;
+
+ /*!
+ \brief Marks the progress as finished by setting it to 100%.
+ */
+ void MarkFinished();
+
+ /*!
+ \brief Checks if the progress dialog has been cancelled.
+ */
+ bool IsCancelled() const;
+
+private:
+ bool m_modal = false;
+ bool m_autoClose = true;
+ bool m_updateProgress = true;
+ bool m_updateInformation = true;
+ mutable CGUIDialogProgressBarHandle* m_progress;
+ mutable CGUIDialogProgress* m_progressDialog;
+};
diff --git a/xbmc/utils/Random.h b/xbmc/utils/Random.h
new file mode 100644
index 0000000..ac2a073
--- /dev/null
+++ b/xbmc/utils/Random.h
@@ -0,0 +1,26 @@
+/*
+ * 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 <algorithm>
+#include <random>
+
+namespace KODI
+{
+namespace UTILS
+{
+template<class TIterator>
+void RandomShuffle(TIterator begin, TIterator end)
+{
+ std::random_device rd;
+ std::mt19937 mt(rd());
+ std::shuffle(begin, end, mt);
+}
+}
+}
diff --git a/xbmc/utils/RecentlyAddedJob.cpp b/xbmc/utils/RecentlyAddedJob.cpp
new file mode 100644
index 0000000..fca1f6d
--- /dev/null
+++ b/xbmc/utils/RecentlyAddedJob.cpp
@@ -0,0 +1,396 @@
+/*
+ * 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 "RecentlyAddedJob.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindow.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "music/MusicDatabase.h"
+#include "music/MusicDbUrl.h"
+#include "music/MusicThumbLoader.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#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)
+{
+ m_flag = flag;
+}
+
+bool CRecentlyAddedJob::UpdateVideo()
+{
+ auto home = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_HOME);
+
+ if ( home == nullptr )
+ return false;
+
+ CLog::Log(LOGDEBUG, "CRecentlyAddedJob::UpdateVideos() - Running RecentlyAdded home screen update");
+
+ int i = 0;
+ CFileItemList items;
+ CVideoDatabase videodatabase;
+ CVideoThumbLoader loader;
+ loader.OnLoaderStart();
+
+ videodatabase.Open();
+
+ if (videodatabase.GetRecentlyAddedMoviesNav("videodb://recentlyaddedmovies/", items, NUM_ITEMS))
+ {
+ for (; i < items.Size(); ++i)
+ {
+ auto item = items.Get(i);
+ std::string value = std::to_string(i + 1);
+ std::string strRating =
+ StringUtils::Format("{:.1f}", item->GetVideoInfoTag()->GetRating().rating);
+
+ home->SetProperty("LatestMovie." + value + ".Title" , item->GetLabel());
+ home->SetProperty("LatestMovie." + value + ".Rating" , strRating);
+ home->SetProperty("LatestMovie." + value + ".Year" , item->GetVideoInfoTag()->GetYear());
+ home->SetProperty("LatestMovie." + value + ".Plot" , item->GetVideoInfoTag()->m_strPlot);
+ home->SetProperty("LatestMovie." + value + ".RunningTime" , item->GetVideoInfoTag()->GetDuration() / 60);
+ home->SetProperty("LatestMovie." + value + ".Path" , item->GetVideoInfoTag()->m_strFileNameAndPath);
+ home->SetProperty("LatestMovie." + value + ".Trailer" , item->GetVideoInfoTag()->m_strTrailer);
+
+ if (!item->HasArt("thumb"))
+ loader.LoadItem(item.get());
+
+ home->SetProperty("LatestMovie." + value + ".Thumb" , item->GetArt("thumb"));
+ home->SetProperty("LatestMovie." + value + ".Fanart" , item->GetArt("fanart"));
+ home->SetProperty("LatestMovie." + value + ".Poster" , item->GetArt("poster"));
+ }
+ }
+ for (; i < NUM_ITEMS; ++i)
+ {
+ std::string value = std::to_string(i + 1);
+ home->SetProperty("LatestMovie." + value + ".Title" , "");
+ home->SetProperty("LatestMovie." + value + ".Thumb" , "");
+ home->SetProperty("LatestMovie." + value + ".Rating" , "");
+ home->SetProperty("LatestMovie." + value + ".Year" , "");
+ home->SetProperty("LatestMovie." + value + ".Plot" , "");
+ home->SetProperty("LatestMovie." + value + ".RunningTime" , "");
+ home->SetProperty("LatestMovie." + value + ".Path" , "");
+ home->SetProperty("LatestMovie." + value + ".Trailer" , "");
+ home->SetProperty("LatestMovie." + value + ".Fanart" , "");
+ home->SetProperty("LatestMovie." + value + ".Poster" , "");
+ }
+
+ i = 0;
+ CFileItemList TVShowItems;
+
+ if (videodatabase.GetRecentlyAddedEpisodesNav("videodb://recentlyaddedepisodes/", TVShowItems, NUM_ITEMS))
+ {
+ for (; i < TVShowItems.Size(); ++i)
+ {
+ auto item = TVShowItems.Get(i);
+ int EpisodeSeason = item->GetVideoInfoTag()->m_iSeason;
+ int EpisodeNumber = item->GetVideoInfoTag()->m_iEpisode;
+ std::string EpisodeNo = StringUtils::Format("s{:02}e{:02}", EpisodeSeason, EpisodeNumber);
+ std::string value = std::to_string(i + 1);
+ std::string strRating =
+ StringUtils::Format("{:.1f}", item->GetVideoInfoTag()->GetRating().rating);
+
+ home->SetProperty("LatestEpisode." + value + ".ShowTitle" , item->GetVideoInfoTag()->m_strShowTitle);
+ home->SetProperty("LatestEpisode." + value + ".EpisodeTitle" , item->GetVideoInfoTag()->m_strTitle);
+ home->SetProperty("LatestEpisode." + value + ".Rating" , strRating);
+ home->SetProperty("LatestEpisode." + value + ".Plot" , item->GetVideoInfoTag()->m_strPlot);
+ home->SetProperty("LatestEpisode." + value + ".EpisodeNo" , EpisodeNo);
+ home->SetProperty("LatestEpisode." + value + ".EpisodeSeason" , EpisodeSeason);
+ home->SetProperty("LatestEpisode." + value + ".EpisodeNumber" , EpisodeNumber);
+ home->SetProperty("LatestEpisode." + value + ".Path" , item->GetVideoInfoTag()->m_strFileNameAndPath);
+
+ if (!item->HasArt("thumb"))
+ loader.LoadItem(item.get());
+
+ std::string seasonThumb;
+ if (item->GetVideoInfoTag()->m_iIdSeason > 0)
+ seasonThumb = videodatabase.GetArtForItem(item->GetVideoInfoTag()->m_iIdSeason, MediaTypeSeason, "thumb");
+
+ home->SetProperty("LatestEpisode." + value + ".Thumb" , item->GetArt("thumb"));
+ home->SetProperty("LatestEpisode." + value + ".ShowThumb" , item->GetArt("tvshow.thumb"));
+ home->SetProperty("LatestEpisode." + value + ".SeasonThumb" , seasonThumb);
+ home->SetProperty("LatestEpisode." + value + ".Fanart" , item->GetArt("fanart"));
+ }
+ }
+ for (; i < NUM_ITEMS; ++i)
+ {
+ std::string value = std::to_string(i + 1);
+ home->SetProperty("LatestEpisode." + value + ".ShowTitle" , "");
+ home->SetProperty("LatestEpisode." + value + ".EpisodeTitle" , "");
+ home->SetProperty("LatestEpisode." + value + ".Rating" , "");
+ home->SetProperty("LatestEpisode." + value + ".Plot" , "");
+ home->SetProperty("LatestEpisode." + value + ".EpisodeNo" , "");
+ home->SetProperty("LatestEpisode." + value + ".EpisodeSeason" , "");
+ home->SetProperty("LatestEpisode." + value + ".EpisodeNumber" , "");
+ home->SetProperty("LatestEpisode." + value + ".Path" , "");
+ home->SetProperty("LatestEpisode." + value + ".Thumb" , "");
+ home->SetProperty("LatestEpisode." + value + ".ShowThumb" , "");
+ home->SetProperty("LatestEpisode." + value + ".SeasonThumb" , "");
+ home->SetProperty("LatestEpisode." + value + ".Fanart" , "");
+ }
+
+#if defined(TARGET_DARWIN_TVOS)
+ // Add recently added Movies and TvShows items on tvOS Kodi TopShelf
+ CTVOSTopShelf::GetInstance().SetTopShelfItems(items, TVOSTopShelfItemsCategory::MOVIES);
+ CTVOSTopShelf::GetInstance().SetTopShelfItems(TVShowItems, TVOSTopShelfItemsCategory::TV_SHOWS);
+#endif
+
+ i = 0;
+ CFileItemList MusicVideoItems;
+
+ if (videodatabase.GetRecentlyAddedMusicVideosNav("videodb://recentlyaddedmusicvideos/", MusicVideoItems, NUM_ITEMS))
+ {
+ for (; i < MusicVideoItems.Size(); ++i)
+ {
+ auto item = MusicVideoItems.Get(i);
+ std::string value = std::to_string(i + 1);
+
+ home->SetProperty("LatestMusicVideo." + value + ".Title" , item->GetLabel());
+ home->SetProperty("LatestMusicVideo." + value + ".Year" , item->GetVideoInfoTag()->GetYear());
+ home->SetProperty("LatestMusicVideo." + value + ".Plot" , item->GetVideoInfoTag()->m_strPlot);
+ home->SetProperty("LatestMusicVideo." + value + ".RunningTime" , item->GetVideoInfoTag()->GetDuration() / 60);
+ home->SetProperty("LatestMusicVideo." + value + ".Path" , item->GetVideoInfoTag()->m_strFileNameAndPath);
+ home->SetProperty("LatestMusicVideo." + value + ".Artist" , StringUtils::Join(item->GetVideoInfoTag()->m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator));
+
+ if (!item->HasArt("thumb"))
+ loader.LoadItem(item.get());
+
+ home->SetProperty("LatestMusicVideo." + value + ".Thumb" , item->GetArt("thumb"));
+ home->SetProperty("LatestMusicVideo." + value + ".Fanart" , item->GetArt("fanart"));
+ }
+ }
+ for (; i < NUM_ITEMS; ++i)
+ {
+ std::string value = std::to_string(i + 1);
+ home->SetProperty("LatestMusicVideo." + value + ".Title" , "");
+ home->SetProperty("LatestMusicVideo." + value + ".Thumb" , "");
+ home->SetProperty("LatestMusicVideo." + value + ".Year" , "");
+ home->SetProperty("LatestMusicVideo." + value + ".Plot" , "");
+ home->SetProperty("LatestMusicVideo." + value + ".RunningTime" , "");
+ home->SetProperty("LatestMusicVideo." + value + ".Path" , "");
+ home->SetProperty("LatestMusicVideo." + value + ".Artist" , "");
+ home->SetProperty("LatestMusicVideo." + value + ".Fanart" , "");
+ }
+
+ videodatabase.Close();
+ return true;
+}
+
+bool CRecentlyAddedJob::UpdateMusic()
+{
+ auto home = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_HOME);
+
+ if ( home == nullptr )
+ return false;
+
+ CLog::Log(LOGDEBUG, "CRecentlyAddedJob::UpdateMusic() - Running RecentlyAdded home screen update");
+
+ int i = 0;
+ CFileItemList musicItems;
+ CMusicDatabase musicdatabase;
+ CMusicThumbLoader loader;
+ loader.OnLoaderStart();
+
+ musicdatabase.Open();
+
+ if (musicdatabase.GetRecentlyAddedAlbumSongs("musicdb://songs/", musicItems, NUM_ITEMS))
+ {
+ int idAlbum = -1;
+ std::string strAlbumThumb;
+ std::string strAlbumFanart;
+ for (; i < musicItems.Size(); ++i)
+ {
+ auto item = musicItems.Get(i);
+ std::string value = std::to_string(i + 1);
+
+ std::string strRating;
+ std::string strAlbum = item->GetMusicInfoTag()->GetAlbum();
+ std::string strArtist = item->GetMusicInfoTag()->GetArtistString();
+
+ if (idAlbum != item->GetMusicInfoTag()->GetAlbumId())
+ {
+ strAlbumThumb.clear();
+ strAlbumFanart.clear();
+ idAlbum = item->GetMusicInfoTag()->GetAlbumId();
+
+ if (loader.LoadItem(item.get()))
+ {
+ strAlbumThumb = item->GetArt("thumb");
+ strAlbumFanart = item->GetArt("fanart");
+ }
+ }
+
+ strRating = std::to_string(item->GetMusicInfoTag()->GetUserrating());
+
+ home->SetProperty("LatestSong." + value + ".Title" , item->GetMusicInfoTag()->GetTitle());
+ home->SetProperty("LatestSong." + value + ".Year" , item->GetMusicInfoTag()->GetYear());
+ home->SetProperty("LatestSong." + value + ".Artist" , strArtist);
+ home->SetProperty("LatestSong." + value + ".Album" , strAlbum);
+ home->SetProperty("LatestSong." + value + ".Rating" , strRating);
+ home->SetProperty("LatestSong." + value + ".Path" , item->GetMusicInfoTag()->GetURL());
+ home->SetProperty("LatestSong." + value + ".Thumb" , strAlbumThumb);
+ home->SetProperty("LatestSong." + value + ".Fanart" , strAlbumFanart);
+ }
+ }
+ for (; i < NUM_ITEMS; ++i)
+ {
+ std::string value = std::to_string(i + 1);
+ home->SetProperty("LatestSong." + value + ".Title" , "");
+ home->SetProperty("LatestSong." + value + ".Year" , "");
+ home->SetProperty("LatestSong." + value + ".Artist" , "");
+ home->SetProperty("LatestSong." + value + ".Album" , "");
+ home->SetProperty("LatestSong." + value + ".Rating" , "");
+ home->SetProperty("LatestSong." + value + ".Path" , "");
+ home->SetProperty("LatestSong." + value + ".Thumb" , "");
+ home->SetProperty("LatestSong." + value + ".Fanart" , "");
+ }
+
+ i = 0;
+ VECALBUMS albums;
+
+ if (musicdatabase.GetRecentlyAddedAlbums(albums, NUM_ITEMS))
+ {
+ size_t j = 0;
+ for (; j < albums.size(); ++j)
+ {
+ auto& album=albums[j];
+ std::string value = std::to_string(j + 1);
+ std::string strThumb;
+ std::string strFanart;
+ bool artfound = false;
+ std::vector<ArtForThumbLoader> art;
+ // Get album thumb and fanart for first album artist
+ artfound = musicdatabase.GetArtForItem(-1, album.idAlbum, -1, true, art);
+ if (artfound)
+ {
+ for (const auto& artitem : art)
+ {
+ if (artitem.mediaType == MediaTypeAlbum && artitem.artType == "thumb")
+ strThumb = artitem.url;
+ else if (artitem.mediaType == MediaTypeArtist && artitem.artType == "fanart")
+ strFanart = artitem.url;
+ }
+ }
+
+ std::string strDBpath = StringUtils::Format("musicdb://albums/{}/", album.idAlbum);
+
+ home->SetProperty("LatestAlbum." + value + ".Title" , album.strAlbum);
+ home->SetProperty("LatestAlbum." + value + ".Year" , album.strReleaseDate);
+ home->SetProperty("LatestAlbum." + value + ".Artist" , album.GetAlbumArtistString());
+ home->SetProperty("LatestAlbum." + value + ".Rating" , album.fRating);
+ home->SetProperty("LatestAlbum." + value + ".Path" , strDBpath);
+ home->SetProperty("LatestAlbum." + value + ".Thumb" , strThumb);
+ home->SetProperty("LatestAlbum." + value + ".Fanart" , strFanart);
+ }
+ i = j;
+ }
+ for (; i < NUM_ITEMS; ++i)
+ {
+ std::string value = std::to_string(i + 1);
+ home->SetProperty("LatestAlbum." + value + ".Title" , "");
+ home->SetProperty("LatestAlbum." + value + ".Year" , "");
+ home->SetProperty("LatestAlbum." + value + ".Artist" , "");
+ home->SetProperty("LatestAlbum." + value + ".Rating" , "");
+ home->SetProperty("LatestAlbum." + value + ".Path" , "");
+ home->SetProperty("LatestAlbum." + value + ".Thumb" , "");
+ home->SetProperty("LatestAlbum." + value + ".Fanart" , "");
+ }
+
+ musicdatabase.Close();
+ return true;
+}
+
+bool CRecentlyAddedJob::UpdateTotal()
+{
+ auto home = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_HOME);
+
+ if ( home == nullptr )
+ return false;
+
+ CLog::Log(LOGDEBUG, "CRecentlyAddedJob::UpdateTotal() - Running RecentlyAdded home screen update");
+
+ CVideoDatabase videodatabase;
+ CMusicDatabase musicdatabase;
+
+ musicdatabase.Open();
+
+ CMusicDbUrl musicUrl;
+ musicUrl.FromString("musicdb://artists/");
+ musicUrl.AddOption("albumartistsonly", !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICLIBRARY_SHOWCOMPILATIONARTISTS));
+
+ CFileItemList items;
+ CDatabase::Filter filter;
+ musicdatabase.GetArtistsByWhere(musicUrl.ToString(), filter, items, SortDescription(), true);
+ int MusArtistTotals = 0;
+ if (items.Size() == 1 && items.Get(0)->HasProperty("total"))
+ MusArtistTotals = static_cast<int>(items.Get(0)->GetProperty("total").asInteger());
+
+ int MusSongTotals = atoi(musicdatabase.GetSingleValue("songview" , "count(1)").c_str());
+ int MusAlbumTotals = atoi(musicdatabase.GetSingleValue("songview" , "count(distinct strAlbum)").c_str());
+ musicdatabase.Close();
+
+ videodatabase.Open();
+ int tvShowCount = atoi(videodatabase.GetSingleValue("tvshow_view" , "count(1)").c_str());
+ int movieTotals = atoi(videodatabase.GetSingleValue("movie_view" , "count(1)").c_str());
+ int movieWatched = atoi(videodatabase.GetSingleValue("movie_view" , "count(playCount)").c_str());
+ int MusVidTotals = atoi(videodatabase.GetSingleValue("musicvideo_view" , "count(1)").c_str());
+ int MusVidWatched = atoi(videodatabase.GetSingleValue("musicvideo_view" , "count(playCount)").c_str());
+ int EpWatched = atoi(videodatabase.GetSingleValue("tvshow_view" , "sum(watchedcount)").c_str());
+ int EpCount = atoi(videodatabase.GetSingleValue("tvshow_view" , "sum(totalcount)").c_str());
+ int TvShowsWatched = atoi(videodatabase.GetSingleValue("tvshow_view" , "sum(watchedcount = totalcount)").c_str());
+ videodatabase.Close();
+
+ home->SetProperty("TVShows.Count" , tvShowCount);
+ home->SetProperty("TVShows.Watched" , TvShowsWatched);
+ home->SetProperty("TVShows.UnWatched" , tvShowCount - TvShowsWatched);
+ home->SetProperty("Episodes.Count" , EpCount);
+ home->SetProperty("Episodes.Watched" , EpWatched);
+ home->SetProperty("Episodes.UnWatched" , EpCount-EpWatched);
+ home->SetProperty("Movies.Count" , movieTotals);
+ home->SetProperty("Movies.Watched" , movieWatched);
+ home->SetProperty("Movies.UnWatched" , movieTotals - movieWatched);
+ home->SetProperty("MusicVideos.Count" , MusVidTotals);
+ home->SetProperty("MusicVideos.Watched" , MusVidWatched);
+ home->SetProperty("MusicVideos.UnWatched" , MusVidTotals - MusVidWatched);
+ home->SetProperty("Music.SongsCount" , MusSongTotals);
+ home->SetProperty("Music.AlbumsCount" , MusAlbumTotals);
+ home->SetProperty("Music.ArtistsCount" , MusArtistTotals);
+
+ return true;
+}
+
+
+bool CRecentlyAddedJob::DoWork()
+{
+ bool ret = true;
+ if (m_flag & Audio)
+ ret &= UpdateMusic();
+
+ if (m_flag & Video)
+ ret &= UpdateVideo();
+
+ if (m_flag & Totals)
+ ret &= UpdateTotal();
+
+ return ret;
+}
diff --git a/xbmc/utils/RecentlyAddedJob.h b/xbmc/utils/RecentlyAddedJob.h
new file mode 100644
index 0000000..f61b60b
--- /dev/null
+++ b/xbmc/utils/RecentlyAddedJob.h
@@ -0,0 +1,30 @@
+/*
+ * 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 "Job.h"
+
+enum ERecentlyAddedFlag
+{
+ Audio = 0x1,
+ Video = 0x2,
+ Totals = 0x4
+};
+
+class CRecentlyAddedJob : public CJob
+{
+public:
+ explicit CRecentlyAddedJob(int flag);
+ static bool UpdateVideo();
+ static bool UpdateMusic();
+ static bool UpdateTotal();
+ bool DoWork() override;
+private:
+ int m_flag;
+};
diff --git a/xbmc/utils/RegExp.cpp b/xbmc/utils/RegExp.cpp
new file mode 100644
index 0000000..9667b64
--- /dev/null
+++ b/xbmc/utils/RegExp.cpp
@@ -0,0 +1,651 @@
+/*
+ * 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 "RegExp.h"
+
+#include "log.h"
+#include "utils/StringUtils.h"
+#include "utils/Utf8Utils.h"
+
+#include <algorithm>
+#include <stdlib.h>
+#include <string.h>
+
+using namespace PCRE;
+
+#ifndef PCRE_UCP
+#define PCRE_UCP 0
+#endif // PCRE_UCP
+
+#ifdef PCRE_CONFIG_JIT
+#define PCRE_HAS_JIT_CODE 1
+#endif
+
+#ifndef PCRE_STUDY_JIT_COMPILE
+#define PCRE_STUDY_JIT_COMPILE 0
+#endif
+#ifndef PCRE_INFO_JIT
+// some unused number
+#define PCRE_INFO_JIT 2048
+#endif
+#ifndef PCRE_HAS_JIT_CODE
+#define pcre_free_study(x) pcre_free((x))
+#endif
+
+int CRegExp::m_Utf8Supported = -1;
+int CRegExp::m_UcpSupported = -1;
+int CRegExp::m_JitSupported = -1;
+
+
+CRegExp::CRegExp(bool caseless /*= false*/, CRegExp::utf8Mode utf8 /*= asciiOnly*/)
+{
+ InitValues(caseless, utf8);
+}
+
+void CRegExp::InitValues(bool caseless /*= false*/, CRegExp::utf8Mode utf8 /*= asciiOnly*/)
+{
+ m_utf8Mode = utf8;
+ m_re = NULL;
+ m_sd = NULL;
+ m_iOptions = PCRE_DOTALL | PCRE_NEWLINE_ANY;
+ if(caseless)
+ m_iOptions |= PCRE_CASELESS;
+ if (m_utf8Mode == forceUtf8)
+ {
+ if (IsUtf8Supported())
+ m_iOptions |= PCRE_UTF8;
+ if (AreUnicodePropertiesSupported())
+ m_iOptions |= PCRE_UCP;
+ }
+
+ m_offset = 0;
+ m_jitCompiled = false;
+ m_bMatched = false;
+ m_iMatchCount = 0;
+ m_jitStack = NULL;
+
+ memset(m_iOvector, 0, sizeof(m_iOvector));
+}
+
+CRegExp::CRegExp(bool caseless, CRegExp::utf8Mode utf8, const char *re, studyMode study /*= NoStudy*/)
+{
+ if (utf8 == autoUtf8)
+ utf8 = requireUtf8(re) ? forceUtf8 : asciiOnly;
+
+ InitValues(caseless, utf8);
+ RegComp(re, study);
+}
+
+bool CRegExp::requireUtf8(const std::string& regexp)
+{
+ // enable UTF-8 mode if regexp string has UTF-8 multibyte sequences
+ if (CUtf8Utils::checkStrForUtf8(regexp) == CUtf8Utils::utf8string)
+ return true;
+
+ // check for explicit Unicode Properties (\p, \P, \X) and for Unicode character codes (greater than 0xFF) in form \x{hhh..}
+ // note: PCRE change meaning of \w, \s, \d (and \W, \S, \D) when Unicode Properties are enabled,
+ // but in auto mode we enable UNP for US-ASCII regexp only if regexp contains explicit \p, \P, \X or Unicode character code
+ const char* const regexpC = regexp.c_str();
+ const size_t len = regexp.length();
+ size_t pos = 0;
+
+ while (pos < len)
+ {
+ const char chr = regexpC[pos];
+ if (chr == '\\')
+ {
+ const char nextChr = regexpC[pos + 1];
+
+ if (nextChr == 'p' || nextChr == 'P' || nextChr == 'X')
+ return true; // found Unicode Properties
+ else if (nextChr == 'Q')
+ pos = regexp.find("\\E", pos + 2); // skip all literals in "\Q...\E"
+ else if (nextChr == 'x' && regexpC[pos + 2] == '{')
+ { // Unicode character with hex code
+ if (readCharXCode(regexp, pos) >= 0x100)
+ return true; // found Unicode character code
+ }
+ else if (nextChr == '\\' || nextChr == '(' || nextChr == ')'
+ || nextChr == '[' || nextChr == ']')
+ pos++; // exclude next character from analyze
+
+ } // chr != '\\'
+ else if (chr == '(' && regexpC[pos + 1] == '?' && regexpC[pos + 2] == '#') // comment in regexp
+ pos = regexp.find(')', pos); // skip comment
+ else if (chr == '[')
+ {
+ if (isCharClassWithUnicode(regexp, pos))
+ return true;
+ }
+
+ if (pos == std::string::npos) // check results of regexp.find() and isCharClassWithUnicode
+ return false;
+
+ pos++;
+ }
+
+ // no Unicode Properties was found
+ return false;
+}
+
+inline int CRegExp::readCharXCode(const std::string& regexp, size_t& pos)
+{
+ // read hex character code in form "\x{hh..}"
+ // 'pos' must point to '\'
+ if (pos >= regexp.length())
+ return -1;
+ const char* const regexpC = regexp.c_str();
+ if (regexpC[pos] != '\\' || regexpC[pos + 1] != 'x' || regexpC[pos + 2] != '{')
+ return -1;
+
+ pos++;
+ const size_t startPos = pos; // 'startPos' points to 'x'
+ const size_t closingBracketPos = regexp.find('}', startPos + 2);
+ if (closingBracketPos == std::string::npos)
+ return 0; // return character zero code, leave 'pos' at 'x'
+
+ pos++; // 'pos' points to '{'
+ int chCode = 0;
+ while (++pos < closingBracketPos)
+ {
+ const int xdigitVal = StringUtils::asciixdigitvalue(regexpC[pos]);
+ if (xdigitVal >= 0)
+ chCode = chCode * 16 + xdigitVal;
+ else
+ { // found non-hexdigit
+ pos = startPos; // reset 'pos' to 'startPos', process "{hh..}" as non-code
+ return 0; // return character zero code
+ }
+ }
+
+ return chCode;
+}
+
+bool CRegExp::isCharClassWithUnicode(const std::string& regexp, size_t& pos)
+{
+ const char* const regexpC = regexp.c_str();
+ const size_t len = regexp.length();
+ if (pos > len || regexpC[pos] != '[')
+ return false;
+
+ // look for Unicode character code "\x{hhh..}" and Unicode properties "\P", "\p" and "\X"
+ // find end (terminating ']') of character class (like "[a-h45]")
+ // detect nested POSIX classes like "[[:lower:]]" and escaped brackets like "[\]]"
+ bool needUnicode = false;
+ while (++pos < len)
+ {
+ if (regexpC[pos] == '[' && regexpC[pos + 1] == ':')
+ { // possible POSIX character class, like "[:alpha:]"
+ const size_t nextClosingBracketPos = regexp.find(']', pos + 2); // don't care about "\]", as it produce error if used inside POSIX char class
+
+ if (nextClosingBracketPos == std::string::npos)
+ { // error in regexp: no closing ']' for character class
+ pos = std::string::npos;
+ return needUnicode;
+ }
+ else if (regexpC[nextClosingBracketPos - 1] == ':')
+ pos = nextClosingBracketPos; // skip POSIX character class
+ // if ":]" is not found, process "[:..." as part of normal character class
+ }
+ else if (regexpC[pos] == ']')
+ return needUnicode; // end of character class
+ else if (regexpC[pos] == '\\')
+ {
+ const char nextChar = regexpC[pos + 1];
+ if (nextChar == ']' || nextChar == '[')
+ pos++; // skip next character
+ else if (nextChar == 'Q')
+ {
+ pos = regexp.find("\\E", pos + 2);
+ if (pos == std::string::npos)
+ return needUnicode; // error in regexp: no closing "\E" after "\Q" in character class
+ else
+ pos++; // skip "\E"
+ }
+ else if (nextChar == 'p' || nextChar == 'P' || nextChar == 'X')
+ needUnicode = true; // don't care about property name as it can contain only ASCII chars
+ else if (nextChar == 'x')
+ {
+ if (readCharXCode(regexp, pos) >= 0x100)
+ needUnicode = true;
+ }
+ }
+ }
+ pos = std::string::npos; // closing square bracket was not found
+
+ return needUnicode;
+}
+
+
+CRegExp::CRegExp(const CRegExp& re)
+{
+ m_re = NULL;
+ m_sd = NULL;
+ m_jitStack = NULL;
+ m_utf8Mode = re.m_utf8Mode;
+ m_iOptions = re.m_iOptions;
+ *this = re;
+}
+
+CRegExp& CRegExp::operator=(const CRegExp& re)
+{
+ size_t size;
+ Cleanup();
+ m_jitCompiled = false;
+ m_pattern = re.m_pattern;
+ if (re.m_re)
+ {
+ if (pcre_fullinfo(re.m_re, NULL, PCRE_INFO_SIZE, &size) >= 0)
+ {
+ if ((m_re = (pcre*)malloc(size)))
+ {
+ memcpy(m_re, re.m_re, size);
+ memcpy(m_iOvector, re.m_iOvector, OVECCOUNT*sizeof(int));
+ m_offset = re.m_offset;
+ m_iMatchCount = re.m_iMatchCount;
+ m_bMatched = re.m_bMatched;
+ m_subject = re.m_subject;
+ m_iOptions = re.m_iOptions;
+ }
+ else
+ CLog::Log(LOGFATAL, "{}: Failed to allocate memory", __FUNCTION__);
+ }
+ }
+ return *this;
+}
+
+CRegExp::~CRegExp()
+{
+ Cleanup();
+}
+
+bool CRegExp::RegComp(const char *re, studyMode study /*= NoStudy*/)
+{
+ if (!re)
+ return false;
+
+ m_offset = 0;
+ m_jitCompiled = false;
+ m_bMatched = false;
+ m_iMatchCount = 0;
+ const char *errMsg = NULL;
+ int errOffset = 0;
+ int options = m_iOptions;
+ if (m_utf8Mode == autoUtf8 && requireUtf8(re))
+ options |= (IsUtf8Supported() ? PCRE_UTF8 : 0) | (AreUnicodePropertiesSupported() ? PCRE_UCP : 0);
+
+ Cleanup();
+
+ m_re = pcre_compile(re, options, &errMsg, &errOffset, NULL);
+ if (!m_re)
+ {
+ m_pattern.clear();
+ CLog::Log(LOGERROR, "PCRE: {}. Compilation failed at offset {} in expression '{}'", errMsg,
+ errOffset, re);
+ return false;
+ }
+
+ m_pattern = re;
+
+ if (study)
+ {
+ const bool jitCompile = (study == StudyWithJitComp) && IsJitSupported();
+ const int studyOptions = jitCompile ? PCRE_STUDY_JIT_COMPILE : 0;
+
+ m_sd = pcre_study(m_re, studyOptions, &errMsg);
+ if (errMsg != NULL)
+ {
+ CLog::Log(LOGWARNING, "{}: PCRE error \"{}\" while studying expression", __FUNCTION__,
+ errMsg);
+ if (m_sd != NULL)
+ {
+ pcre_free_study(m_sd);
+ m_sd = NULL;
+ }
+ }
+ else if (jitCompile)
+ {
+ int jitPresent = 0;
+ m_jitCompiled = (pcre_fullinfo(m_re, m_sd, PCRE_INFO_JIT, &jitPresent) == 0 && jitPresent == 1);
+ }
+ }
+
+ return true;
+}
+
+int CRegExp::RegFind(const char *str, unsigned int startoffset /*= 0*/, int maxNumberOfCharsToTest /*= -1*/)
+{
+ return PrivateRegFind(strlen(str), str, startoffset, maxNumberOfCharsToTest);
+}
+
+int CRegExp::PrivateRegFind(size_t bufferLen, const char *str, unsigned int startoffset /* = 0*/, int maxNumberOfCharsToTest /*= -1*/)
+{
+ m_offset = 0;
+ m_bMatched = false;
+ m_iMatchCount = 0;
+
+ if (!m_re)
+ {
+ CLog::Log(LOGERROR, "PCRE: Called before compilation");
+ return -1;
+ }
+
+ if (!str)
+ {
+ CLog::Log(LOGERROR, "PCRE: Called without a string to match");
+ return -1;
+ }
+
+ if (startoffset > bufferLen)
+ {
+ CLog::Log(LOGERROR, "{}: startoffset is beyond end of string to match", __FUNCTION__);
+ return -1;
+ }
+
+#ifdef PCRE_HAS_JIT_CODE
+ if (m_jitCompiled && !m_jitStack)
+ {
+ m_jitStack = pcre_jit_stack_alloc(32*1024, 512*1024);
+ if (m_jitStack == NULL)
+ CLog::Log(LOGWARNING, "{}: can't allocate address space for JIT stack", __FUNCTION__);
+
+ pcre_assign_jit_stack(m_sd, NULL, m_jitStack);
+ }
+#endif
+
+ if (maxNumberOfCharsToTest >= 0)
+ bufferLen = std::min<size_t>(bufferLen, startoffset + maxNumberOfCharsToTest);
+
+ m_subject.assign(str + startoffset, bufferLen - startoffset);
+ int rc = pcre_exec(m_re, NULL, m_subject.c_str(), m_subject.length(), 0, 0, m_iOvector, OVECCOUNT);
+
+ if (rc<1)
+ {
+ static const int fragmentLen = 80; // length of excerpt before erroneous char for log
+ switch(rc)
+ {
+ case PCRE_ERROR_NOMATCH:
+ return -1;
+
+ case PCRE_ERROR_MATCHLIMIT:
+ CLog::Log(LOGERROR, "PCRE: Match limit reached");
+ return -1;
+
+#ifdef PCRE_ERROR_SHORTUTF8
+ case PCRE_ERROR_SHORTUTF8:
+ {
+ const size_t startPos = (m_subject.length() > fragmentLen) ? CUtf8Utils::RFindValidUtf8Char(m_subject, m_subject.length() - fragmentLen) : 0;
+ if (startPos != std::string::npos)
+ CLog::Log(
+ LOGERROR,
+ "PCRE: Bad UTF-8 character at the end of string. Text before bad character: \"{}\"",
+ m_subject.substr(startPos));
+ else
+ CLog::Log(LOGERROR, "PCRE: Bad UTF-8 character at the end of string");
+ return -1;
+ }
+#endif
+ case PCRE_ERROR_BADUTF8:
+ {
+ const size_t startPos = (m_iOvector[0] > fragmentLen) ? CUtf8Utils::RFindValidUtf8Char(m_subject, m_iOvector[0] - fragmentLen) : 0;
+ if (m_iOvector[0] >= 0 && startPos != std::string::npos)
+ CLog::Log(LOGERROR,
+ "PCRE: Bad UTF-8 character, error code: {}, position: {}. Text before bad "
+ "char: \"{}\"",
+ m_iOvector[1], m_iOvector[0],
+ m_subject.substr(startPos, m_iOvector[0] - startPos + 1));
+ else
+ CLog::Log(LOGERROR, "PCRE: Bad UTF-8 character, error code: {}, position: {}",
+ m_iOvector[1], m_iOvector[0]);
+ return -1;
+ }
+ case PCRE_ERROR_BADUTF8_OFFSET:
+ CLog::Log(LOGERROR, "PCRE: Offset is pointing to the middle of UTF-8 character");
+ return -1;
+
+ default:
+ CLog::Log(LOGERROR, "PCRE: Unknown error: {}", rc);
+ return -1;
+ }
+ }
+ m_offset = startoffset;
+ m_bMatched = true;
+ m_iMatchCount = rc;
+ return m_iOvector[0] + m_offset;
+}
+
+int CRegExp::GetCaptureTotal() const
+{
+ int c = -1;
+ if (m_re)
+ pcre_fullinfo(m_re, NULL, PCRE_INFO_CAPTURECOUNT, &c);
+ return c;
+}
+
+std::string CRegExp::GetReplaceString(const std::string& sReplaceExp) const
+{
+ if (!m_bMatched || sReplaceExp.empty())
+ return "";
+
+ const char* const expr = sReplaceExp.c_str();
+
+ size_t pos = sReplaceExp.find_first_of("\\&");
+ std::string result(sReplaceExp, 0, pos);
+ result.reserve(sReplaceExp.size()); // very rough estimate
+
+ while(pos != std::string::npos)
+ {
+ if (expr[pos] == '\\')
+ {
+ // string is null-terminated and current char isn't null, so it's safe to advance to next char
+ pos++; // advance to next char
+ const char nextChar = expr[pos];
+ if (nextChar == '&' || nextChar == '\\')
+ { // this is "\&" or "\\" combination
+ result.push_back(nextChar); // add '&' or '\' to result
+ pos++;
+ }
+ else if (isdigit(nextChar))
+ { // this is "\0" - "\9" combination
+ int subNum = nextChar - '0';
+ pos++; // advance to second next char
+ const char secondNextChar = expr[pos];
+ if (isdigit(secondNextChar))
+ { // this is "\00" - "\99" combination
+ subNum = subNum * 10 + (secondNextChar - '0');
+ pos++;
+ }
+ result.append(GetMatch(subNum));
+ }
+ }
+ else
+ { // '&' char
+ result.append(GetMatch(0));
+ pos++;
+ }
+
+ const size_t nextPos = sReplaceExp.find_first_of("\\&", pos);
+ result.append(sReplaceExp, pos, nextPos - pos);
+ pos = nextPos;
+ }
+
+ return result;
+}
+
+int CRegExp::GetSubStart(int iSub) const
+{
+ if (!IsValidSubNumber(iSub))
+ return -1;
+
+ return m_iOvector[iSub*2] + m_offset;
+}
+
+int CRegExp::GetSubStart(const std::string& subName) const
+{
+ return GetSubStart(GetNamedSubPatternNumber(subName.c_str()));
+}
+
+int CRegExp::GetSubLength(int iSub) const
+{
+ if (!IsValidSubNumber(iSub))
+ return -1;
+
+ return m_iOvector[(iSub*2)+1] - m_iOvector[(iSub*2)];
+}
+
+int CRegExp::GetSubLength(const std::string& subName) const
+{
+ return GetSubLength(GetNamedSubPatternNumber(subName.c_str()));
+}
+
+std::string CRegExp::GetMatch(int iSub /* = 0 */) const
+{
+ if (!IsValidSubNumber(iSub))
+ return "";
+
+ int pos = m_iOvector[(iSub*2)];
+ int len = m_iOvector[(iSub*2)+1] - pos;
+ if (pos < 0 || len <= 0)
+ return "";
+
+ return m_subject.substr(pos, len);
+}
+
+std::string CRegExp::GetMatch(const std::string& subName) const
+{
+ return GetMatch(GetNamedSubPatternNumber(subName.c_str()));
+}
+
+bool CRegExp::GetNamedSubPattern(const char* strName, std::string& strMatch) const
+{
+ strMatch.clear();
+ int iSub = pcre_get_stringnumber(m_re, strName);
+ if (!IsValidSubNumber(iSub))
+ return false;
+ strMatch = GetMatch(iSub);
+ return true;
+}
+
+int CRegExp::GetNamedSubPatternNumber(const char* strName) const
+{
+ return pcre_get_stringnumber(m_re, strName);
+}
+
+void CRegExp::DumpOvector(int iLog /* = LOGDEBUG */)
+{
+ if (iLog < LOGDEBUG || iLog > LOGNONE)
+ return;
+
+ std::string str = "{";
+ int size = GetSubCount(); // past the subpatterns is junk
+ for (int i = 0; i <= size; i++)
+ {
+ std::string t = StringUtils::Format("[{},{}]", m_iOvector[(i * 2)], m_iOvector[(i * 2) + 1]);
+ if (i != size)
+ t += ",";
+ str += t;
+ }
+ str += "}";
+ CLog::Log(iLog, "regexp ovector={}", str);
+}
+
+void CRegExp::Cleanup()
+{
+ if (m_re)
+ {
+ pcre_free(m_re);
+ m_re = NULL;
+ }
+
+ if (m_sd)
+ {
+ pcre_free_study(m_sd);
+ m_sd = NULL;
+ }
+
+#ifdef PCRE_HAS_JIT_CODE
+ if (m_jitStack)
+ {
+ pcre_jit_stack_free(m_jitStack);
+ m_jitStack = NULL;
+ }
+#endif
+}
+
+inline bool CRegExp::IsValidSubNumber(int iSub) const
+{
+ return iSub >= 0 && iSub <= m_iMatchCount && iSub <= m_MaxNumOfBackrefrences;
+}
+
+
+bool CRegExp::IsUtf8Supported(void)
+{
+ if (m_Utf8Supported == -1)
+ {
+ if (pcre_config(PCRE_CONFIG_UTF8, &m_Utf8Supported) != 0)
+ m_Utf8Supported = 0;
+ }
+
+ return m_Utf8Supported == 1;
+}
+
+bool CRegExp::AreUnicodePropertiesSupported(void)
+{
+#if defined(PCRE_CONFIG_UNICODE_PROPERTIES) && PCRE_UCP != 0
+ if (m_UcpSupported == -1)
+ {
+ if (pcre_config(PCRE_CONFIG_UNICODE_PROPERTIES, &m_UcpSupported) != 0)
+ m_UcpSupported = 0;
+ }
+#endif
+
+ return m_UcpSupported == 1;
+}
+
+bool CRegExp::LogCheckUtf8Support(void)
+{
+ bool utf8FullSupport = true;
+
+ if (!CRegExp::IsUtf8Supported())
+ {
+ utf8FullSupport = false;
+ CLog::Log(LOGWARNING, "UTF-8 is not supported in PCRE lib, support for national symbols is limited!");
+ }
+
+ if (!CRegExp::AreUnicodePropertiesSupported())
+ {
+ utf8FullSupport = false;
+ CLog::Log(LOGWARNING, "Unicode properties are not enabled in PCRE lib, support for national symbols may be limited!");
+ }
+
+ if (!utf8FullSupport)
+ {
+ CLog::Log(LOGINFO,
+ "Consider installing PCRE lib version 8.10 or later with enabled Unicode properties "
+ "and UTF-8 support. Your PCRE lib version: {}",
+ PCRE::pcre_version());
+#if PCRE_UCP == 0
+ CLog::Log(LOGINFO, "You will need to rebuild XBMC after PCRE lib update.");
+#endif
+ }
+
+ return utf8FullSupport;
+}
+
+bool CRegExp::IsJitSupported(void)
+{
+ if (m_JitSupported == -1)
+ {
+#ifdef PCRE_HAS_JIT_CODE
+ if (pcre_config(PCRE_CONFIG_JIT, &m_JitSupported) != 0)
+#endif
+ m_JitSupported = 0;
+ }
+
+ return m_JitSupported == 1;
+}
diff --git a/xbmc/utils/RegExp.h b/xbmc/utils/RegExp.h
new file mode 100644
index 0000000..53f6019
--- /dev/null
+++ b/xbmc/utils/RegExp.h
@@ -0,0 +1,165 @@
+/*
+ * 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
+
+//! @todo - move to std::regex (after switching to gcc 4.9 or higher) and get rid of CRegExp
+
+#include <string>
+#include <vector>
+
+/* make sure stdlib.h is included before including pcre.h inside the
+ namespace; this works around stdlib.h definitions also living in
+ the PCRE namespace */
+#include <stdlib.h>
+
+namespace PCRE {
+struct real_pcre_jit_stack; // forward declaration for PCRE without JIT
+typedef struct real_pcre_jit_stack pcre_jit_stack;
+#include <pcre.h>
+}
+
+class CRegExp
+{
+public:
+ enum studyMode
+ {
+ NoStudy = 0, // do not study expression
+ StudyRegExp = 1, // study expression (slower compilation, faster find)
+ StudyWithJitComp // study expression and JIT-compile it, if possible (heavyweight optimization)
+ };
+ enum utf8Mode
+ {
+ autoUtf8 = -1, // analyze regexp for UTF-8 multi-byte chars, for Unicode codes > 0xFF
+ // or explicit Unicode properties (\p, \P and \X), enable UTF-8 mode if any of them are found
+ asciiOnly = 0, // process regexp and strings as single-byte encoded strings
+ forceUtf8 = 1 // enable UTF-8 mode (with Unicode properties)
+ };
+
+ static const int m_MaxNumOfBackrefrences = 20;
+ /**
+ * @param caseless (optional) Matching will be case insensitive if set to true
+ * or case sensitive if set to false
+ * @param utf8 (optional) Control UTF-8 processing
+ */
+ CRegExp(bool caseless = false, utf8Mode utf8 = asciiOnly);
+ /**
+ * Create new CRegExp object and compile regexp expression in one step
+ * @warning Use only with hardcoded regexp when you're sure that regexp is compiled without errors
+ * @param caseless Matching will be case insensitive if set to true
+ * or case sensitive if set to false
+ * @param utf8 Control UTF-8 processing
+ * @param re The regular expression
+ * @param study (optional) Controls study of expression, useful if expression will be used
+ * several times
+ */
+ CRegExp(bool caseless, utf8Mode utf8, const char *re, studyMode study = NoStudy);
+
+ CRegExp(const CRegExp& re);
+ ~CRegExp();
+
+ /**
+ * Compile (prepare) regular expression
+ * @param re The regular expression
+ * @param study (optional) Controls study of expression, useful if expression will be used
+ * several times
+ * @return true on success, false on any error
+ */
+ bool RegComp(const char *re, studyMode study = NoStudy);
+
+ /**
+ * Compile (prepare) regular expression
+ * @param re The regular expression
+ * @param study (optional) Controls study of expression, useful if expression will be used
+ * several times
+ * @return true on success, false on any error
+ */
+ bool RegComp(const std::string& re, studyMode study = NoStudy)
+ { return RegComp(re.c_str(), study); }
+
+ /**
+ * Find first match of regular expression in given string
+ * @param str The string to match against regular expression
+ * @param startoffset (optional) The string offset to start matching
+ * @param maxNumberOfCharsToTest (optional) The maximum number of characters to test (match) in
+ * string. If set to -1 string checked up to the end.
+ * @return staring position of match in string, negative value in case of error or no match
+ */
+ int RegFind(const char* str, unsigned int startoffset = 0, int maxNumberOfCharsToTest = -1);
+ /**
+ * Find first match of regular expression in given string
+ * @param str The string to match against regular expression
+ * @param startoffset (optional) The string offset to start matching
+ * @param maxNumberOfCharsToTest (optional) The maximum number of characters to test (match) in
+ * string. If set to -1 string checked up to the end.
+ * @return staring position of match in string, negative value in case of error or no match
+ */
+ int RegFind(const std::string& str, unsigned int startoffset = 0, int maxNumberOfCharsToTest = -1)
+ { return PrivateRegFind(str.length(), str.c_str(), startoffset, maxNumberOfCharsToTest); }
+ std::string GetReplaceString(const std::string& sReplaceExp) const;
+ int GetFindLen() const
+ {
+ if (!m_re || !m_bMatched)
+ return 0;
+
+ return (m_iOvector[1] - m_iOvector[0]);
+ };
+ int GetSubCount() const { return m_iMatchCount - 1; } // PCRE returns the number of sub-patterns + 1
+ int GetSubStart(int iSub) const;
+ int GetSubStart(const std::string& subName) const;
+ int GetSubLength(int iSub) const;
+ int GetSubLength(const std::string& subName) const;
+ int GetCaptureTotal() const;
+ std::string GetMatch(int iSub = 0) const;
+ std::string GetMatch(const std::string& subName) const;
+ const std::string& GetPattern() const { return m_pattern; }
+ bool GetNamedSubPattern(const char* strName, std::string& strMatch) const;
+ int GetNamedSubPatternNumber(const char* strName) const;
+ void DumpOvector(int iLog);
+ /**
+ * Check is RegExp object is ready for matching
+ * @return true if RegExp object is ready for matching, false otherwise
+ */
+ inline bool IsCompiled(void) const
+ { return !m_pattern.empty(); }
+ CRegExp& operator= (const CRegExp& re);
+ static bool IsUtf8Supported(void);
+ static bool AreUnicodePropertiesSupported(void);
+ static bool LogCheckUtf8Support(void);
+ static bool IsJitSupported(void);
+
+private:
+ int PrivateRegFind(size_t bufferLen, const char *str, unsigned int startoffset = 0, int maxNumberOfCharsToTest = -1);
+ void InitValues(bool caseless = false, CRegExp::utf8Mode utf8 = asciiOnly);
+ static bool requireUtf8(const std::string& regexp);
+ static int readCharXCode(const std::string& regexp, size_t& pos);
+ static bool isCharClassWithUnicode(const std::string& regexp, size_t& pos);
+
+ void Cleanup();
+ inline bool IsValidSubNumber(int iSub) const;
+
+ PCRE::pcre* m_re;
+ PCRE::pcre_extra* m_sd;
+ static const int OVECCOUNT=(m_MaxNumOfBackrefrences + 1) * 3;
+ unsigned int m_offset;
+ int m_iOvector[OVECCOUNT];
+ utf8Mode m_utf8Mode;
+ int m_iMatchCount;
+ int m_iOptions;
+ bool m_jitCompiled;
+ bool m_bMatched;
+ PCRE::pcre_jit_stack* m_jitStack;
+ std::string m_subject;
+ std::string m_pattern;
+ static int m_Utf8Supported;
+ static int m_UcpSupported;
+ static int m_JitSupported;
+};
+
+typedef std::vector<CRegExp> VECCREGEXP;
+
diff --git a/xbmc/utils/RingBuffer.cpp b/xbmc/utils/RingBuffer.cpp
new file mode 100644
index 0000000..454f1f9
--- /dev/null
+++ b/xbmc/utils/RingBuffer.cpp
@@ -0,0 +1,245 @@
+/*
+ * 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 "RingBuffer.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <cstring>
+#include <mutex>
+
+/* Constructor */
+CRingBuffer::CRingBuffer()
+{
+ m_buffer = NULL;
+ m_size = 0;
+ m_readPtr = 0;
+ m_writePtr = 0;
+ m_fillCount = 0;
+}
+
+/* Destructor */
+CRingBuffer::~CRingBuffer()
+{
+ Destroy();
+}
+
+/* Create a ring buffer with the specified 'size' */
+bool CRingBuffer::Create(unsigned int size)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_buffer = (char*)malloc(size);
+ if (m_buffer != NULL)
+ {
+ m_size = size;
+ return true;
+ }
+ return false;
+}
+
+/* Free the ring buffer and set all values to NULL or 0 */
+void CRingBuffer::Destroy()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_buffer != NULL)
+ {
+ free(m_buffer);
+ m_buffer = NULL;
+ }
+ m_size = 0;
+ m_readPtr = 0;
+ m_writePtr = 0;
+ m_fillCount = 0;
+}
+
+/* Clear the ring buffer */
+void CRingBuffer::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ m_readPtr = 0;
+ m_writePtr = 0;
+ m_fillCount = 0;
+}
+
+/* Read in data from the ring buffer to the supplied buffer 'buf'. The amount
+ * read in is specified by 'size'.
+ */
+bool CRingBuffer::ReadData(char *buf, unsigned int size)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (size > m_fillCount)
+ {
+ return false;
+ }
+ if (size + m_readPtr > m_size)
+ {
+ unsigned int chunk = m_size - m_readPtr;
+ memcpy(buf, m_buffer + m_readPtr, chunk);
+ memcpy(buf + chunk, m_buffer, size - chunk);
+ m_readPtr = size - chunk;
+ }
+ else
+ {
+ memcpy(buf, m_buffer + m_readPtr, size);
+ m_readPtr += size;
+ }
+ if (m_readPtr == m_size)
+ m_readPtr = 0;
+ m_fillCount -= size;
+ return true;
+}
+
+/* Read in data from the ring buffer to another ring buffer object specified by
+ * 'rBuf'.
+ */
+bool CRingBuffer::ReadData(CRingBuffer &rBuf, unsigned int size)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (rBuf.getBuffer() == NULL)
+ rBuf.Create(size);
+
+ bool bOk = size <= rBuf.getMaxWriteSize() && size <= getMaxReadSize();
+ if (bOk)
+ {
+ unsigned int chunksize = std::min(size, m_size - m_readPtr);
+ bOk = rBuf.WriteData(&getBuffer()[m_readPtr], chunksize);
+ if (bOk && chunksize < size)
+ bOk = rBuf.WriteData(&getBuffer()[0], size - chunksize);
+ if (bOk)
+ SkipBytes(size);
+ }
+
+ return bOk;
+}
+
+/* Write data to ring buffer from buffer specified in 'buf'. Amount read in is
+ * specified by 'size'.
+ */
+bool CRingBuffer::WriteData(const char *buf, unsigned int size)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (size > m_size - m_fillCount)
+ {
+ return false;
+ }
+ if (size + m_writePtr > m_size)
+ {
+ unsigned int chunk = m_size - m_writePtr;
+ memcpy(m_buffer + m_writePtr, buf, chunk);
+ memcpy(m_buffer, buf + chunk, size - chunk);
+ m_writePtr = size - chunk;
+ }
+ else
+ {
+ memcpy(m_buffer + m_writePtr, buf, size);
+ m_writePtr += size;
+ }
+ if (m_writePtr == m_size)
+ m_writePtr = 0;
+ m_fillCount += size;
+ return true;
+}
+
+/* Write data to ring buffer from another ring buffer object specified by
+ * 'rBuf'.
+ */
+bool CRingBuffer::WriteData(CRingBuffer &rBuf, unsigned int size)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (m_buffer == NULL)
+ Create(size);
+
+ bool bOk = size <= rBuf.getMaxReadSize() && size <= getMaxWriteSize();
+ if (bOk)
+ {
+ unsigned int readpos = rBuf.getReadPtr();
+ unsigned int chunksize = std::min(size, rBuf.getSize() - readpos);
+ bOk = WriteData(&rBuf.getBuffer()[readpos], chunksize);
+ if (bOk && chunksize < size)
+ bOk = WriteData(&rBuf.getBuffer()[0], size - chunksize);
+ }
+
+ return bOk;
+}
+
+/* Skip bytes in buffer to be read */
+bool CRingBuffer::SkipBytes(int skipSize)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ if (skipSize < 0)
+ {
+ return false; // skipping backwards is not supported
+ }
+
+ unsigned int size = skipSize;
+ if (size > m_fillCount)
+ {
+ return false;
+ }
+ if (size + m_readPtr > m_size)
+ {
+ unsigned int chunk = m_size - m_readPtr;
+ m_readPtr = size - chunk;
+ }
+ else
+ {
+ m_readPtr += size;
+ }
+ if (m_readPtr == m_size)
+ m_readPtr = 0;
+ m_fillCount -= size;
+ return true;
+}
+
+/* Append all content from ring buffer 'rBuf' to this ring buffer */
+bool CRingBuffer::Append(CRingBuffer &rBuf)
+{
+ return WriteData(rBuf, rBuf.getMaxReadSize());
+}
+
+/* Copy all content from ring buffer 'rBuf' to this ring buffer overwriting any existing data */
+bool CRingBuffer::Copy(CRingBuffer &rBuf)
+{
+ Clear();
+ return Append(rBuf);
+}
+
+/* Our various 'get' methods */
+char *CRingBuffer::getBuffer()
+{
+ return m_buffer;
+}
+
+unsigned int CRingBuffer::getSize()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_size;
+}
+
+unsigned int CRingBuffer::getReadPtr() const
+{
+ return m_readPtr;
+}
+
+unsigned int CRingBuffer::getWritePtr()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_writePtr;
+}
+
+unsigned int CRingBuffer::getMaxReadSize()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_fillCount;
+}
+
+unsigned int CRingBuffer::getMaxWriteSize()
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+ return m_size - m_fillCount;
+}
diff --git a/xbmc/utils/RingBuffer.h b/xbmc/utils/RingBuffer.h
new file mode 100644
index 0000000..8cdb971
--- /dev/null
+++ b/xbmc/utils/RingBuffer.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+class CRingBuffer
+{
+ CCriticalSection m_critSection;
+ char *m_buffer;
+ unsigned int m_size;
+ unsigned int m_readPtr;
+ unsigned int m_writePtr;
+ unsigned int m_fillCount;
+public:
+ CRingBuffer();
+ ~CRingBuffer();
+ bool Create(unsigned int size);
+ void Destroy();
+ void Clear();
+ bool ReadData(char *buf, unsigned int size);
+ bool ReadData(CRingBuffer &rBuf, unsigned int size);
+ bool WriteData(const char *buf, unsigned int size);
+ bool WriteData(CRingBuffer &rBuf, unsigned int size);
+ bool SkipBytes(int skipSize);
+ bool Append(CRingBuffer &rBuf);
+ bool Copy(CRingBuffer &rBuf);
+ char *getBuffer();
+ unsigned int getSize();
+ unsigned int getReadPtr() const;
+ unsigned int getWritePtr();
+ unsigned int getMaxReadSize();
+ unsigned int getMaxWriteSize();
+};
diff --git a/xbmc/utils/RssManager.cpp b/xbmc/utils/RssManager.cpp
new file mode 100644
index 0000000..b8d350c
--- /dev/null
+++ b/xbmc/utils/RssManager.cpp
@@ -0,0 +1,199 @@
+/*
+ * 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 "RssManager.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonInstaller.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "interfaces/builtins/Builtins.h"
+#include "profiles/ProfileManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/FileUtils.h"
+#include "utils/RssReader.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <mutex>
+#include <utility>
+
+using namespace KODI::MESSAGING;
+
+
+CRssManager::CRssManager()
+{
+ m_bActive = false;
+}
+
+CRssManager::~CRssManager()
+{
+ Stop();
+}
+
+CRssManager& CRssManager::GetInstance()
+{
+ static CRssManager sRssManager;
+ return sRssManager;
+}
+
+void CRssManager::OnSettingsLoaded()
+{
+ Load();
+}
+
+void CRssManager::OnSettingsUnloaded()
+{
+ Clear();
+}
+
+void CRssManager::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_LOOKANDFEEL_RSSEDIT)
+ {
+ ADDON::AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon("script.rss.editor", addon,
+ ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ if (!ADDON::CAddonInstaller::GetInstance().InstallModal(
+ "script.rss.editor", addon, ADDON::InstallModalPrompt::CHOICE_YES))
+ return;
+ }
+ CBuiltins::GetInstance().Execute("RunScript(script.rss.editor)");
+ }
+}
+
+void CRssManager::Start()
+ {
+ m_bActive = true;
+}
+
+void CRssManager::Stop()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_bActive = false;
+ for (unsigned int i = 0; i < m_readers.size(); i++)
+ {
+ if (m_readers[i].reader)
+ delete m_readers[i].reader;
+ }
+ m_readers.clear();
+}
+
+bool CRssManager::Load()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ std::string rssXML = profileManager->GetUserDataItem("RssFeeds.xml");
+ if (!CFileUtils::Exists(rssXML))
+ return false;
+
+ CXBMCTinyXML rssDoc;
+ if (!rssDoc.LoadFile(rssXML))
+ {
+ CLog::Log(LOGERROR, "CRssManager: error loading {}, Line {}\n{}", rssXML, rssDoc.ErrorRow(),
+ rssDoc.ErrorDesc());
+ return false;
+ }
+
+ const TiXmlElement *pRootElement = rssDoc.RootElement();
+ if (pRootElement == NULL || !StringUtils::EqualsNoCase(pRootElement->ValueStr(), "rssfeeds"))
+ {
+ CLog::Log(LOGERROR, "CRssManager: error loading {}, no <rssfeeds> node", rssXML);
+ return false;
+ }
+
+ m_mapRssUrls.clear();
+ const TiXmlElement* pSet = pRootElement->FirstChildElement("set");
+ while (pSet != NULL)
+ {
+ int iId;
+ if (pSet->QueryIntAttribute("id", &iId) == TIXML_SUCCESS)
+ {
+ RssSet set;
+ set.rtl = pSet->Attribute("rtl") != NULL &&
+ StringUtils::CompareNoCase(pSet->Attribute("rtl"), "true") == 0;
+ const TiXmlElement* pFeed = pSet->FirstChildElement("feed");
+ while (pFeed != NULL)
+ {
+ int iInterval;
+ if (pFeed->QueryIntAttribute("updateinterval", &iInterval) != TIXML_SUCCESS)
+ {
+ iInterval = 30; // default to 30 min
+ CLog::Log(LOGDEBUG, "CRssManager: no interval set, default to 30!");
+ }
+
+ if (pFeed->FirstChild() != NULL)
+ {
+ //! @todo UTF-8: Do these URLs need to be converted to UTF-8?
+ //! What about the xml encoding?
+ std::string strUrl = pFeed->FirstChild()->ValueStr();
+ set.url.push_back(strUrl);
+ set.interval.push_back(iInterval);
+ }
+ pFeed = pFeed->NextSiblingElement("feed");
+ }
+
+ m_mapRssUrls.insert(std::make_pair(iId,set));
+ }
+ else
+ CLog::Log(LOGERROR, "CRssManager: found rss url set with no id in RssFeeds.xml, ignored");
+
+ pSet = pSet->NextSiblingElement("set");
+ }
+
+ return true;
+}
+
+bool CRssManager::Reload()
+{
+ Stop();
+ if (!Load())
+ return false;
+ Start();
+
+ return true;
+}
+
+void CRssManager::Clear()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_mapRssUrls.clear();
+}
+
+// returns true if the reader doesn't need creating, false otherwise
+bool CRssManager::GetReader(int controlID, int windowID, IRssObserver* observer, CRssReader *&reader)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // check to see if we've already created this reader
+ for (unsigned int i = 0; i < m_readers.size(); i++)
+ {
+ if (m_readers[i].controlID == controlID && m_readers[i].windowID == windowID)
+ {
+ reader = m_readers[i].reader;
+ reader->SetObserver(observer);
+ reader->UpdateObserver();
+ return true;
+ }
+ }
+ // need to create a new one
+ READERCONTROL readerControl;
+ readerControl.controlID = controlID;
+ readerControl.windowID = windowID;
+ reader = readerControl.reader = new CRssReader;
+ m_readers.push_back(readerControl);
+ return false;
+}
diff --git a/xbmc/utils/RssManager.h b/xbmc/utils/RssManager.h
new file mode 100644
index 0000000..2b807d7
--- /dev/null
+++ b/xbmc/utils/RssManager.h
@@ -0,0 +1,68 @@
+/*
+ * 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 "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+#include "threads/CriticalSection.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+class CRssReader;
+class IRssObserver;
+
+typedef struct
+{
+ bool rtl;
+ std::vector<int> interval;
+ std::vector<std::string> url;
+} RssSet;
+typedef std::map<int, RssSet> RssUrls;
+
+class CRssManager : public ISettingCallback, public ISettingsHandler
+{
+public:
+ static CRssManager& GetInstance();
+
+ void OnSettingsLoaded() override;
+ void OnSettingsUnloaded() override;
+
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ void Start();
+ void Stop();
+ bool Load();
+ bool Reload();
+ void Clear();
+ bool IsActive() const { return m_bActive; }
+
+ bool GetReader(int controlID, int windowID, IRssObserver* observer, CRssReader *&reader);
+ const RssUrls& GetUrls() const { return m_mapRssUrls; }
+
+protected:
+ CRssManager();
+ ~CRssManager() override;
+
+private:
+ CRssManager(const CRssManager&) = delete;
+ CRssManager& operator=(const CRssManager&) = delete;
+ struct READERCONTROL
+ {
+ int controlID;
+ int windowID;
+ CRssReader *reader;
+ };
+
+ std::vector<READERCONTROL> m_readers;
+ RssUrls m_mapRssUrls;
+ bool m_bActive;
+ CCriticalSection m_critical;
+};
diff --git a/xbmc/utils/RssReader.cpp b/xbmc/utils/RssReader.cpp
new file mode 100644
index 0000000..0b227b6
--- /dev/null
+++ b/xbmc/utils/RssReader.cpp
@@ -0,0 +1,415 @@
+/*
+ * 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 "RssReader.h"
+
+#include "CharsetConverter.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/File.h"
+#include "guilib/GUIRSSControl.h"
+#include "guilib/LocalizeStrings.h"
+#include "log.h"
+#include "network/Network.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SystemClock.h"
+#include "utils/HTMLUtil.h"
+#include "utils/XTimeUtils.h"
+
+#include <mutex>
+
+#define RSS_COLOR_BODY 0
+#define RSS_COLOR_HEADLINE 1
+#define RSS_COLOR_CHANNEL 2
+
+using namespace XFILE;
+using namespace std::chrono_literals;
+
+//////////////////////////////////////////////////////////////////////
+// Construction/Destruction
+//////////////////////////////////////////////////////////////////////
+
+CRssReader::CRssReader() : CThread("RSSReader")
+{
+ m_pObserver = NULL;
+ m_spacesBetweenFeeds = 0;
+ m_bIsRunning = false;
+ m_savedScrollPixelPos = 0;
+ m_rtlText = false;
+ m_requestRefresh = false;
+}
+
+CRssReader::~CRssReader()
+{
+ if (m_pObserver)
+ m_pObserver->OnFeedRelease();
+ StopThread();
+ for (unsigned int i = 0; i < m_vecTimeStamps.size(); i++)
+ delete m_vecTimeStamps[i];
+}
+
+void CRssReader::Create(IRssObserver* aObserver, const std::vector<std::string>& aUrls, const std::vector<int> &times, int spacesBetweenFeeds, bool rtl)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ m_pObserver = aObserver;
+ m_spacesBetweenFeeds = spacesBetweenFeeds;
+ m_vecUrls = aUrls;
+ m_strFeed.resize(aUrls.size());
+ m_strColors.resize(aUrls.size());
+ // set update times
+ m_vecUpdateTimes = times;
+ m_rtlText = rtl;
+ m_requestRefresh = false;
+
+ // update each feed on creation
+ for (unsigned int i = 0; i < m_vecUpdateTimes.size(); ++i)
+ {
+ AddToQueue(i);
+ KODI::TIME::SystemTime* time = new KODI::TIME::SystemTime;
+ KODI::TIME::GetLocalTime(time);
+ m_vecTimeStamps.push_back(time);
+ }
+}
+
+void CRssReader::requestRefresh()
+{
+ m_requestRefresh = true;
+}
+
+void CRssReader::AddToQueue(int iAdd)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (iAdd < (int)m_vecUrls.size())
+ m_vecQueue.push_back(iAdd);
+ if (!m_bIsRunning)
+ {
+ StopThread();
+ m_bIsRunning = true;
+ CThread::Create(false);
+ }
+}
+
+void CRssReader::OnExit()
+{
+ m_bIsRunning = false;
+}
+
+int CRssReader::GetQueueSize()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ return m_vecQueue.size();
+}
+
+void CRssReader::Process()
+{
+ while (GetQueueSize())
+ {
+ std::unique_lock<CCriticalSection> lock(m_critical);
+
+ int iFeed = m_vecQueue.front();
+ m_vecQueue.erase(m_vecQueue.begin());
+
+ m_strFeed[iFeed].clear();
+ m_strColors[iFeed].clear();
+
+ CCurlFile http;
+ http.SetUserAgent(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_userAgent);
+ http.SetTimeout(2);
+ std::string strXML;
+ std::string strUrl = m_vecUrls[iFeed];
+ lock.unlock();
+
+ int nRetries = 3;
+ CURL url(strUrl);
+ std::string fileCharset;
+
+ // we wait for the network to come up
+ if ((url.IsProtocol("http") || url.IsProtocol("https")) &&
+ !CServiceBroker::GetNetwork().IsAvailable())
+ {
+ CLog::Log(LOGWARNING, "RSS: No network connection");
+ strXML = "<rss><item><title>"+g_localizeStrings.Get(15301)+"</title></item></rss>";
+ }
+ else
+ {
+ XbmcThreads::EndTime<> timeout(15s);
+ while (!m_bStop && nRetries > 0)
+ {
+ if (timeout.IsTimePast())
+ {
+ CLog::Log(LOGERROR, "Timeout while retrieving rss feed: {}", strUrl);
+ break;
+ }
+ nRetries--;
+
+ if (!url.IsProtocol("http") && !url.IsProtocol("https"))
+ {
+ CFile file;
+ std::vector<uint8_t> buffer;
+ if (file.LoadFile(strUrl, buffer) > 0)
+ {
+ strXML.assign(reinterpret_cast<char*>(buffer.data()), buffer.size());
+ break;
+ }
+ }
+ else
+ {
+ if (http.Get(strUrl, strXML))
+ {
+ fileCharset = http.GetProperty(XFILE::FILE_PROPERTY_CONTENT_CHARSET);
+ CLog::Log(LOGDEBUG, "Got rss feed: {}", strUrl);
+ break;
+ }
+ else if (nRetries > 0)
+ CThread::Sleep(5000ms); // Network problems? Retry, but not immediately.
+ else
+ CLog::Log(LOGERROR, "Unable to obtain rss feed: {}", strUrl);
+ }
+ }
+ http.Cancel();
+ }
+ if (!strXML.empty() && m_pObserver)
+ {
+ // erase any <content:encoded> tags (also unsupported by tinyxml)
+ size_t iStart = strXML.find("<content:encoded>");
+ size_t iEnd = 0;
+ while (iStart != std::string::npos)
+ {
+ // get <content:encoded> end position
+ iEnd = strXML.find("</content:encoded>", iStart) + 18;
+
+ // erase the section
+ strXML = strXML.erase(iStart, iEnd - iStart);
+
+ iStart = strXML.find("<content:encoded>");
+ }
+
+ if (Parse(strXML, iFeed, fileCharset))
+ CLog::Log(LOGDEBUG, "Parsed rss feed: {}", strUrl);
+ }
+ }
+ UpdateObserver();
+}
+
+void CRssReader::getFeed(vecText &text)
+{
+ text.clear();
+ // double the spaces at the start of the set
+ for (int j = 0; j < m_spacesBetweenFeeds; j++)
+ text.push_back(L' ');
+ for (unsigned int i = 0; i < m_strFeed.size(); i++)
+ {
+ for (int j = 0; j < m_spacesBetweenFeeds; j++)
+ text.push_back(L' ');
+
+ for (unsigned int j = 0; j < m_strFeed[i].size(); j++)
+ {
+ character_t letter = m_strFeed[i][j] | ((m_strColors[i][j] - 48) << 16);
+ text.push_back(letter);
+ }
+ }
+}
+
+void CRssReader::AddTag(const std::string &aString)
+{
+ m_tagSet.push_back(aString);
+}
+
+void CRssReader::AddString(std::wstring aString, int aColour, int iFeed)
+{
+ if (m_rtlText)
+ m_strFeed[iFeed] = aString + m_strFeed[iFeed];
+ else
+ m_strFeed[iFeed] += aString;
+
+ size_t nStringLength = aString.size();
+
+ for (size_t i = 0;i < nStringLength;i++)
+ aString[i] = static_cast<char>(48 + aColour);
+
+ if (m_rtlText)
+ m_strColors[iFeed] = aString + m_strColors[iFeed];
+ else
+ m_strColors[iFeed] += aString;
+}
+
+void CRssReader::GetNewsItems(TiXmlElement* channelXmlNode, int iFeed)
+{
+ HTML::CHTMLUtil html;
+
+ TiXmlElement * itemNode = channelXmlNode->FirstChildElement("item");
+ std::map<std::string, std::wstring> mTagElements;
+ typedef std::pair<std::string, std::wstring> StrPair;
+ std::list<std::string>::iterator i;
+
+ // Add the title tag in if we didn't pass any tags in at all
+ // Represents default behaviour before configurability
+
+ if (m_tagSet.empty())
+ AddTag("title");
+
+ while (itemNode != nullptr)
+ {
+ TiXmlNode* childNode = itemNode->FirstChild();
+ mTagElements.clear();
+ while (childNode != nullptr)
+ {
+ std::string strName = childNode->ValueStr();
+
+ for (i = m_tagSet.begin(); i != m_tagSet.end(); ++i)
+ {
+ if (!childNode->NoChildren() && *i == strName)
+ {
+ std::string htmlText = childNode->FirstChild()->ValueStr();
+
+ // This usually happens in right-to-left languages where they want to
+ // specify in the RSS body that the text should be RTL.
+ // <title>
+ // <div dir="RTL">��� ����: ���� �� �����</div>
+ // </title>
+ if (htmlText == "div" || htmlText == "span")
+ htmlText = childNode->FirstChild()->FirstChild()->ValueStr();
+
+ std::wstring unicodeText, unicodeText2;
+
+ g_charsetConverter.utf8ToW(htmlText, unicodeText2, m_rtlText);
+ html.ConvertHTMLToW(unicodeText2, unicodeText);
+
+ mTagElements.insert(StrPair(*i, unicodeText));
+ }
+ }
+ childNode = childNode->NextSibling();
+ }
+
+ int rsscolour = RSS_COLOR_HEADLINE;
+ for (i = m_tagSet.begin(); i != m_tagSet.end(); ++i)
+ {
+ std::map<std::string, std::wstring>::iterator j = mTagElements.find(*i);
+
+ if (j == mTagElements.end())
+ continue;
+
+ std::wstring& text = j->second;
+ AddString(text, rsscolour, iFeed);
+ rsscolour = RSS_COLOR_BODY;
+ text = L" - ";
+ AddString(text, rsscolour, iFeed);
+ }
+ itemNode = itemNode->NextSiblingElement("item");
+ }
+}
+
+bool CRssReader::Parse(const std::string& data, int iFeed, const std::string& charset)
+{
+ m_xml.Clear();
+ m_xml.Parse(data, charset);
+
+ CLog::Log(LOGDEBUG, "RSS feed encoding: {}", m_xml.GetUsedCharset());
+
+ return Parse(iFeed);
+}
+
+bool CRssReader::Parse(int iFeed)
+{
+ TiXmlElement* rootXmlNode = m_xml.RootElement();
+
+ if (!rootXmlNode)
+ return false;
+
+ TiXmlElement* rssXmlNode = NULL;
+
+ std::string strValue = rootXmlNode->ValueStr();
+ if (strValue.find("rss") != std::string::npos ||
+ strValue.find("rdf") != std::string::npos)
+ rssXmlNode = rootXmlNode;
+ else
+ {
+ // Unable to find root <rss> or <rdf> node
+ return false;
+ }
+
+ TiXmlElement* channelXmlNode = rssXmlNode->FirstChildElement("channel");
+ if (channelXmlNode)
+ {
+ TiXmlElement* titleNode = channelXmlNode->FirstChildElement("title");
+ if (titleNode && !titleNode->NoChildren())
+ {
+ std::string strChannel = titleNode->FirstChild()->Value();
+ std::wstring strChannelUnicode;
+ g_charsetConverter.utf8ToW(strChannel, strChannelUnicode, m_rtlText);
+ AddString(strChannelUnicode, RSS_COLOR_CHANNEL, iFeed);
+
+ AddString(L":", RSS_COLOR_CHANNEL, iFeed);
+ AddString(L" ", RSS_COLOR_CHANNEL, iFeed);
+ }
+
+ GetNewsItems(channelXmlNode,iFeed);
+ }
+
+ GetNewsItems(rssXmlNode,iFeed);
+
+ // avoid trailing ' - '
+ if (m_strFeed[iFeed].size() > 3 && m_strFeed[iFeed].substr(m_strFeed[iFeed].size() - 3) == L" - ")
+ {
+ if (m_rtlText)
+ {
+ m_strFeed[iFeed].erase(0, 3);
+ m_strColors[iFeed].erase(0, 3);
+ }
+ else
+ {
+ m_strFeed[iFeed].erase(m_strFeed[iFeed].length() - 3);
+ m_strColors[iFeed].erase(m_strColors[iFeed].length() - 3);
+ }
+ }
+ return true;
+}
+
+void CRssReader::SetObserver(IRssObserver *observer)
+{
+ m_pObserver = observer;
+}
+
+void CRssReader::UpdateObserver()
+{
+ if (!m_pObserver)
+ return;
+
+ vecText feed;
+ getFeed(feed);
+ if (!feed.empty())
+ {
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ if (m_pObserver) // need to check again when locked to make sure observer wasnt removed
+ m_pObserver->OnFeedUpdate(feed);
+ }
+}
+
+void CRssReader::CheckForUpdates()
+{
+ KODI::TIME::SystemTime time;
+ KODI::TIME::GetLocalTime(&time);
+
+ for (unsigned int i = 0;i < m_vecUpdateTimes.size(); ++i )
+ {
+ if (m_requestRefresh || ((time.day * 24 * 60) + (time.hour * 60) + time.minute) -
+ ((m_vecTimeStamps[i]->day * 24 * 60) +
+ (m_vecTimeStamps[i]->hour * 60) + m_vecTimeStamps[i]->minute) >
+ m_vecUpdateTimes[i])
+ {
+ CLog::Log(LOGDEBUG, "Updating RSS");
+ KODI::TIME::GetLocalTime(m_vecTimeStamps[i]);
+ AddToQueue(i);
+ }
+ }
+
+ m_requestRefresh = false;
+}
diff --git a/xbmc/utils/RssReader.h b/xbmc/utils/RssReader.h
new file mode 100644
index 0000000..ae9d2f7
--- /dev/null
+++ b/xbmc/utils/RssReader.h
@@ -0,0 +1,67 @@
+/*
+ * 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 "threads/CriticalSection.h"
+#include "threads/Thread.h"
+#include "utils/IRssObserver.h"
+#include "utils/XBMCTinyXML.h"
+
+#include <list>
+#include <string>
+#include <vector>
+
+namespace KODI::TIME
+{
+struct SystemTime;
+}
+
+class CRssReader : public CThread
+{
+public:
+ CRssReader();
+ ~CRssReader() override;
+
+ void Create(IRssObserver* aObserver, const std::vector<std::string>& aUrl, const std::vector<int>& times, int spacesBetweenFeeds, bool rtl);
+ bool Parse(const std::string& data, int iFeed, const std::string& charset);
+ void getFeed(vecText &text);
+ void AddTag(const std::string &addTag);
+ void AddToQueue(int iAdd);
+ void UpdateObserver();
+ void SetObserver(IRssObserver* observer);
+ void CheckForUpdates();
+ void requestRefresh();
+ float m_savedScrollPixelPos;
+
+private:
+ void Process() override;
+ bool Parse(int iFeed);
+ void GetNewsItems(TiXmlElement* channelXmlNode, int iFeed);
+ void AddString(std::wstring aString, int aColour, int iFeed);
+ void UpdateFeed();
+ void OnExit() override;
+ int GetQueueSize();
+
+ IRssObserver* m_pObserver;
+
+ std::vector<std::wstring> m_strFeed;
+ std::vector<std::wstring> m_strColors;
+ std::vector<KODI::TIME::SystemTime*> m_vecTimeStamps;
+ std::vector<int> m_vecUpdateTimes;
+ int m_spacesBetweenFeeds;
+ CXBMCTinyXML m_xml;
+ std::list<std::string> m_tagSet;
+ std::vector<std::string> m_vecUrls;
+ std::vector<int> m_vecQueue;
+ bool m_bIsRunning;
+ bool m_rtlText;
+ bool m_requestRefresh;
+
+ CCriticalSection m_critical;
+};
diff --git a/xbmc/utils/SaveFileStateJob.cpp b/xbmc/utils/SaveFileStateJob.cpp
new file mode 100644
index 0000000..f7da601
--- /dev/null
+++ b/xbmc/utils/SaveFileStateJob.cpp
@@ -0,0 +1,235 @@
+/*
+ * 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 "SaveFileStateJob.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "StringUtils.h"
+#include "URIUtils.h"
+#include "URL.h"
+#include "Util.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationStackHelper.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "interfaces/AnnouncementManager.h"
+#include "log.h"
+#include "music/MusicDatabase.h"
+#include "music/tags/MusicInfoTag.h"
+#include "network/upnp/UPnP.h"
+#include "utils/Variant.h"
+#include "video/Bookmark.h"
+#include "video/VideoDatabase.h"
+
+void CSaveFileState::DoWork(CFileItem& item,
+ CBookmark& bookmark,
+ bool updatePlayCount)
+{
+ std::string progressTrackingFile = item.GetPath();
+
+ if (item.HasVideoInfoTag() && StringUtils::StartsWith(item.GetVideoInfoTag()->m_strFileNameAndPath, "removable://"))
+ progressTrackingFile = item.GetVideoInfoTag()->m_strFileNameAndPath; // this variable contains removable:// suffixed by disc label+uniqueid or is empty if label not uniquely identified
+ else if (item.HasVideoInfoTag() && item.IsVideoDb())
+ progressTrackingFile = item.GetVideoInfoTag()->m_strFileNameAndPath; // we need the file url of the video db item to create the bookmark
+ else if (item.HasProperty("original_listitem_url"))
+ {
+ // only use original_listitem_url for Python, UPnP and Bluray sources
+ std::string original = item.GetProperty("original_listitem_url").asString();
+ if (URIUtils::IsPlugin(original) || URIUtils::IsUPnP(original) || URIUtils::IsBluray(item.GetPath()))
+ progressTrackingFile = original;
+ }
+
+ if (!progressTrackingFile.empty())
+ {
+#ifdef HAS_UPNP
+ // checks if UPnP server of this file is available and supports updating
+ if (URIUtils::IsUPnP(progressTrackingFile)
+ && UPNP::CUPnP::SaveFileState(item, bookmark, updatePlayCount))
+ {
+ return;
+ }
+#endif
+ if (item.IsVideo())
+ {
+ std::string redactPath = CURL::GetRedacted(progressTrackingFile);
+ CLog::Log(LOGDEBUG, "{} - Saving file state for video item {}", __FUNCTION__, redactPath);
+
+ CVideoDatabase videodatabase;
+ if (!videodatabase.Open())
+ {
+ CLog::Log(LOGWARNING, "{} - Unable to open video database. Can not save file state!",
+ __FUNCTION__);
+ }
+ else
+ {
+ videodatabase.BeginTransaction();
+
+ if (URIUtils::IsPlugin(progressTrackingFile) && !(item.HasVideoInfoTag() && item.GetVideoInfoTag()->m_iDbId >= 0))
+ {
+ // FileItem from plugin can lack information, make sure all needed fields are set
+ CVideoInfoTag *tag = item.GetVideoInfoTag();
+ CStreamDetails streams = tag->m_streamDetails;
+ if (videodatabase.LoadVideoInfo(progressTrackingFile, *tag))
+ {
+ item.SetPath(progressTrackingFile);
+ item.ClearProperty("original_listitem_url");
+ tag->m_streamDetails = streams;
+ }
+ }
+
+ bool updateListing = false;
+ // No resume & watched status for livetv
+ if (!item.IsLiveTV())
+ {
+ if (updatePlayCount)
+ {
+ // no watched for not yet finished pvr recordings
+ if (!item.IsInProgressPVRRecording())
+ {
+ CLog::Log(LOGDEBUG, "{} - Marking video item {} as watched", __FUNCTION__,
+ redactPath);
+
+ // consider this item as played
+ const CDateTime newLastPlayed = videodatabase.IncrementPlayCount(item);
+
+ item.SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, true);
+ updateListing = true;
+
+ if (item.HasVideoInfoTag())
+ {
+ item.GetVideoInfoTag()->IncrementPlayCount();
+
+ if (newLastPlayed.IsValid())
+ item.GetVideoInfoTag()->m_lastPlayed = newLastPlayed;
+
+ CVariant data;
+ data["id"] = item.GetVideoInfoTag()->m_iDbId;
+ data["type"] = item.GetVideoInfoTag()->m_type;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
+ "OnUpdate", data);
+ }
+ }
+ }
+ else
+ {
+ const CDateTime newLastPlayed = videodatabase.UpdateLastPlayed(item);
+
+ if (item.HasVideoInfoTag() && newLastPlayed.IsValid())
+ item.GetVideoInfoTag()->m_lastPlayed = newLastPlayed;
+ }
+
+ if (!item.HasVideoInfoTag() ||
+ item.GetVideoInfoTag()->GetResumePoint().timeInSeconds != bookmark.timeInSeconds)
+ {
+ if (bookmark.timeInSeconds <= 0.0)
+ videodatabase.ClearBookMarksOfFile(progressTrackingFile, CBookmark::RESUME);
+ else
+ videodatabase.AddBookMarkToFile(progressTrackingFile, bookmark, CBookmark::RESUME);
+ if (item.HasVideoInfoTag())
+ item.GetVideoInfoTag()->SetResumePoint(bookmark);
+
+ // UPnP announce resume point changes to clients
+ // however not if playcount is modified as that already announces
+ if (item.HasVideoInfoTag() && !updatePlayCount)
+ {
+ CVariant data;
+ data["id"] = item.GetVideoInfoTag()->m_iDbId;
+ data["type"] = item.GetVideoInfoTag()->m_type;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
+ "OnUpdate", data);
+ }
+
+ updateListing = true;
+ }
+ }
+
+ if (item.HasVideoInfoTag() && item.GetVideoInfoTag()->HasStreamDetails())
+ {
+ CFileItem dbItem(item);
+
+ // Check whether the item's db streamdetails need updating
+ if (!videodatabase.GetStreamDetails(dbItem) ||
+ dbItem.GetVideoInfoTag()->m_streamDetails != item.GetVideoInfoTag()->m_streamDetails)
+ {
+ videodatabase.SetStreamDetailsForFile(item.GetVideoInfoTag()->m_streamDetails, progressTrackingFile);
+ updateListing = true;
+ }
+ }
+
+ videodatabase.CommitTransaction();
+
+ if (updateListing)
+ {
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ CFileItemPtr msgItem(new CFileItem(item));
+ if (item.HasProperty("original_listitem_url"))
+ msgItem->SetPath(item.GetProperty("original_listitem_url").asString());
+
+ // Could be part of an ISO stack. In this case the bookmark is saved onto the part.
+ // In order to properly update the list, we need to refresh the stack's resume point
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto stackHelper = components.GetComponent<CApplicationStackHelper>();
+ if (stackHelper->HasRegisteredStack(item) &&
+ stackHelper->GetRegisteredStackTotalTimeMs(item) == 0)
+ videodatabase.GetResumePoint(*(msgItem->GetVideoInfoTag()));
+
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0, GUI_MSG_UPDATE_ITEM, 0, msgItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
+ }
+
+ videodatabase.Close();
+ }
+ }
+
+ if (item.IsAudio())
+ {
+ std::string redactPath = CURL::GetRedacted(progressTrackingFile);
+ CLog::Log(LOGDEBUG, "{} - Saving file state for audio item {}", __FUNCTION__, redactPath);
+
+ CMusicDatabase musicdatabase;
+ if (updatePlayCount)
+ {
+ if (!musicdatabase.Open())
+ {
+ CLog::Log(LOGWARNING, "{} - Unable to open music database. Can not save file state!",
+ __FUNCTION__);
+ }
+ else
+ {
+ // consider this item as played
+ CLog::Log(LOGDEBUG, "{} - Marking audio item {} as listened", __FUNCTION__, redactPath);
+
+ musicdatabase.IncrementPlayCount(item);
+ musicdatabase.Close();
+
+ // UPnP announce resume point changes to clients
+ // however not if playcount is modified as that already announces
+ if (item.IsMusicDb())
+ {
+ CVariant data;
+ data["id"] = item.GetMusicInfoTag()->GetDatabaseId();
+ data["type"] = item.GetMusicInfoTag()->GetType();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::AudioLibrary,
+ "OnUpdate", data);
+ }
+ }
+ }
+
+ if (item.IsAudioBook())
+ {
+ musicdatabase.Open();
+ musicdatabase.SetResumeBookmarkForAudioBook(
+ item, item.GetStartOffset() + CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds));
+ musicdatabase.Close();
+ }
+ }
+ }
+}
diff --git a/xbmc/utils/SaveFileStateJob.h b/xbmc/utils/SaveFileStateJob.h
new file mode 100644
index 0000000..b7bb0cc
--- /dev/null
+++ b/xbmc/utils/SaveFileStateJob.h
@@ -0,0 +1,21 @@
+/*
+ * 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 CBookmark;
+class CFileItem;
+
+class CSaveFileState
+{
+public:
+ static void DoWork(CFileItem& item,
+ CBookmark& bookmark,
+ bool updatePlayCount);
+};
+
diff --git a/xbmc/utils/ScopeGuard.h b/xbmc/utils/ScopeGuard.h
new file mode 100644
index 0000000..2f731fb
--- /dev/null
+++ b/xbmc/utils/ScopeGuard.h
@@ -0,0 +1,111 @@
+/*
+ * 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 <functional>
+
+namespace KODI
+{
+namespace UTILS
+{
+
+/*! \class CScopeGuard
+ \brief Generic scopeguard designed to handle any type of handle
+
+ This is not necessary but recommended to cut down on some typing
+ using CSocketHandle = CScopeGuard<SOCKET, INVALID_SOCKET, closesocket>;
+
+ CSocketHandle sh(closesocket, open(thingy));
+ */
+template<typename Handle, Handle invalid, typename Deleter>
+class CScopeGuard
+{
+
+public:
+
+ CScopeGuard(std::function<Deleter> del, Handle handle = invalid)
+ : m_handle{handle}
+ , m_deleter{del}
+ { };
+
+ ~CScopeGuard() noexcept
+ {
+ reset();
+ }
+
+ operator Handle() const
+ {
+ return m_handle;
+ }
+
+ operator bool() const
+ {
+ return m_handle != invalid;
+ }
+
+ /*! \brief attach a new handle to this instance, if there's
+ already a handle it will be closed.
+
+ \param[in] handle The handle to manage
+ */
+ void attach(Handle handle)
+ {
+ reset();
+
+ m_handle = handle;
+ }
+
+ /*! \brief release the managed handle so that it won't be auto closed
+
+ \return The handle being managed by the guard
+ */
+ Handle release()
+ {
+ Handle h = m_handle;
+ m_handle = invalid;
+ return h;
+ }
+
+ /*! \brief reset the instance, closing any managed handle and setting it to invalid
+ */
+ void reset()
+ {
+ if (m_handle != invalid)
+ {
+ m_deleter(m_handle);
+ m_handle = invalid;
+ }
+ }
+
+ //Disallow default construction and copying
+ CScopeGuard() = delete;
+ CScopeGuard(const CScopeGuard& rhs) = delete;
+ CScopeGuard& operator= (const CScopeGuard& rhs) = delete;
+
+ //Allow moving
+ CScopeGuard(CScopeGuard&& rhs) noexcept
+ : m_handle{std::move(rhs.m_handle)}, m_deleter{std::move(rhs.m_deleter)}
+ {
+ // Bring moved-from object into released state so destructor will not do anything
+ rhs.release();
+ }
+ CScopeGuard& operator=(CScopeGuard&& rhs) noexcept
+ {
+ attach(rhs.release());
+ m_deleter = std::move(rhs.m_deleter);
+ return *this;
+ }
+
+private:
+ Handle m_handle;
+ std::function<Deleter> m_deleter;
+};
+
+}
+}
diff --git a/xbmc/utils/ScraperParser.cpp b/xbmc/utils/ScraperParser.cpp
new file mode 100644
index 0000000..b6f85b9
--- /dev/null
+++ b/xbmc/utils/ScraperParser.cpp
@@ -0,0 +1,615 @@
+/*
+ * 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 "ScraperParser.h"
+
+#include "guilib/LocalizeStrings.h"
+#include "RegExp.h"
+#include "HTMLUtil.h"
+#include "addons/Scraper.h"
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "log.h"
+#include "CharsetConverter.h"
+#ifdef HAVE_LIBXSLT
+#include "utils/XSLTUtils.h"
+#endif
+#include "utils/XMLUtils.h"
+#include <sstream>
+#include <cstring>
+
+using namespace ADDON;
+using namespace XFILE;
+
+CScraperParser::CScraperParser()
+{
+ m_pRootElement = NULL;
+ m_document = NULL;
+ m_SearchStringEncoding = "UTF-8";
+ m_scraper = NULL;
+ m_isNoop = true;
+}
+
+CScraperParser::CScraperParser(const CScraperParser& parser)
+{
+ m_pRootElement = NULL;
+ m_document = NULL;
+ m_SearchStringEncoding = "UTF-8";
+ m_scraper = NULL;
+ m_isNoop = true;
+ *this = parser;
+}
+
+CScraperParser &CScraperParser::operator=(const CScraperParser &parser)
+{
+ if (this != &parser)
+ {
+ Clear();
+ if (parser.m_document)
+ {
+ m_scraper = parser.m_scraper;
+ m_document = new CXBMCTinyXML(*parser.m_document);
+ LoadFromXML();
+ }
+ else
+ m_scraper = NULL;
+ }
+ return *this;
+}
+
+CScraperParser::~CScraperParser()
+{
+ Clear();
+}
+
+void CScraperParser::Clear()
+{
+ m_pRootElement = NULL;
+ delete m_document;
+
+ m_document = NULL;
+ m_strFile.clear();
+}
+
+bool CScraperParser::Load(const std::string& strXMLFile)
+{
+ Clear();
+
+ m_document = new CXBMCTinyXML();
+
+ if (!m_document)
+ return false;
+
+ m_strFile = strXMLFile;
+
+ if (m_document->LoadFile(strXMLFile))
+ return LoadFromXML();
+
+ delete m_document;
+ m_document = NULL;
+ return false;
+}
+
+bool CScraperParser::LoadFromXML()
+{
+ if (!m_document)
+ return false;
+
+ m_pRootElement = m_document->RootElement();
+ std::string strValue = m_pRootElement->ValueStr();
+ if (strValue == "scraper")
+ {
+ TiXmlElement* pChildElement = m_pRootElement->FirstChildElement("CreateSearchUrl");
+ if (pChildElement)
+ {
+ m_isNoop = false;
+ if (!(m_SearchStringEncoding = pChildElement->Attribute("SearchStringEncoding")))
+ m_SearchStringEncoding = "UTF-8";
+ }
+
+ pChildElement = m_pRootElement->FirstChildElement("CreateArtistSearchUrl");
+ if (pChildElement)
+ {
+ m_isNoop = false;
+ if (!(m_SearchStringEncoding = pChildElement->Attribute("SearchStringEncoding")))
+ m_SearchStringEncoding = "UTF-8";
+ }
+ pChildElement = m_pRootElement->FirstChildElement("CreateAlbumSearchUrl");
+ if (pChildElement)
+ {
+ m_isNoop = false;
+ if (!(m_SearchStringEncoding = pChildElement->Attribute("SearchStringEncoding")))
+ m_SearchStringEncoding = "UTF-8";
+ }
+
+ return true;
+ }
+
+ delete m_document;
+ m_document = NULL;
+ m_pRootElement = NULL;
+ return false;
+}
+
+void CScraperParser::ReplaceBuffers(std::string& strDest)
+{
+ // insert buffers
+ size_t iIndex;
+ for (int i=MAX_SCRAPER_BUFFERS-1; i>=0; i--)
+ {
+ iIndex = 0;
+ std::string temp = StringUtils::Format("$${}", i + 1);
+ while ((iIndex = strDest.find(temp,iIndex)) != std::string::npos)
+ {
+ strDest.replace(strDest.begin()+iIndex,strDest.begin()+iIndex+temp.size(),m_param[i]);
+ iIndex += m_param[i].length();
+ }
+ }
+ // insert settings
+ iIndex = 0;
+ while ((iIndex = strDest.find("$INFO[", iIndex)) != std::string::npos)
+ {
+ size_t iEnd = strDest.find(']', iIndex);
+ std::string strInfo = strDest.substr(iIndex+6, iEnd - iIndex - 6);
+ std::string strReplace;
+ if (m_scraper)
+ strReplace = m_scraper->GetSetting(strInfo);
+ strDest.replace(strDest.begin()+iIndex,strDest.begin()+iEnd+1,strReplace);
+ iIndex += strReplace.length();
+ }
+ // insert localize strings
+ iIndex = 0;
+ while ((iIndex = strDest.find("$LOCALIZE[", iIndex)) != std::string::npos)
+ {
+ size_t iEnd = strDest.find(']', iIndex);
+ std::string strInfo = strDest.substr(iIndex+10, iEnd - iIndex - 10);
+ std::string strReplace;
+ if (m_scraper)
+ strReplace = g_localizeStrings.GetAddonString(m_scraper->ID(), strtol(strInfo.c_str(),NULL,10));
+ strDest.replace(strDest.begin()+iIndex,strDest.begin()+iEnd+1,strReplace);
+ iIndex += strReplace.length();
+ }
+ iIndex = 0;
+ while ((iIndex = strDest.find("\\n",iIndex)) != std::string::npos)
+ strDest.replace(strDest.begin()+iIndex,strDest.begin()+iIndex+2,"\n");
+}
+
+void CScraperParser::ParseExpression(const std::string& input, std::string& dest, TiXmlElement* element, bool bAppend)
+{
+ std::string strOutput = XMLUtils::GetAttribute(element, "output");
+
+ TiXmlElement* pExpression = element->FirstChildElement("expression");
+ if (pExpression)
+ {
+ bool bInsensitive=true;
+ const char* sensitive = pExpression->Attribute("cs");
+ if (sensitive)
+ if (StringUtils::CompareNoCase(sensitive, "yes") == 0)
+ bInsensitive=false; // match case sensitive
+
+ CRegExp::utf8Mode eUtf8 = CRegExp::autoUtf8;
+ const char* const strUtf8 = pExpression->Attribute("utf8");
+ if (strUtf8)
+ {
+ if (StringUtils::CompareNoCase(strUtf8, "yes") == 0)
+ eUtf8 = CRegExp::forceUtf8;
+ else if (StringUtils::CompareNoCase(strUtf8, "no") == 0)
+ eUtf8 = CRegExp::asciiOnly;
+ else if (StringUtils::CompareNoCase(strUtf8, "auto") == 0)
+ eUtf8 = CRegExp::autoUtf8;
+ }
+
+ CRegExp reg(bInsensitive, eUtf8);
+ std::string strExpression;
+ if (pExpression->FirstChild())
+ strExpression = pExpression->FirstChild()->Value();
+ else
+ strExpression = "(.*)";
+ ReplaceBuffers(strExpression);
+ ReplaceBuffers(strOutput);
+
+ if (!reg.RegComp(strExpression.c_str()))
+ {
+ return;
+ }
+
+ bool bRepeat = false;
+ const char* szRepeat = pExpression->Attribute("repeat");
+ if (szRepeat)
+ if (StringUtils::CompareNoCase(szRepeat, "yes") == 0)
+ bRepeat = true;
+
+ const char* szClear = pExpression->Attribute("clear");
+ if (szClear)
+ if (StringUtils::CompareNoCase(szClear, "yes") == 0)
+ dest=""; // clear no matter if regexp fails
+
+ bool bClean[MAX_SCRAPER_BUFFERS];
+ GetBufferParams(bClean,pExpression->Attribute("noclean"),true);
+
+ bool bTrim[MAX_SCRAPER_BUFFERS];
+ GetBufferParams(bTrim,pExpression->Attribute("trim"),false);
+
+ bool bFixChars[MAX_SCRAPER_BUFFERS];
+ GetBufferParams(bFixChars,pExpression->Attribute("fixchars"),false);
+
+ bool bEncode[MAX_SCRAPER_BUFFERS];
+ GetBufferParams(bEncode,pExpression->Attribute("encode"),false);
+
+ int iOptional = -1;
+ pExpression->QueryIntAttribute("optional",&iOptional);
+
+ int iCompare = -1;
+ pExpression->QueryIntAttribute("compare",&iCompare);
+ if (iCompare > -1)
+ StringUtils::ToLower(m_param[iCompare-1]);
+ std::string curInput = input;
+ for (int iBuf=0;iBuf<MAX_SCRAPER_BUFFERS;++iBuf)
+ {
+ if (bClean[iBuf])
+ InsertToken(strOutput,iBuf+1,"!!!CLEAN!!!");
+ if (bTrim[iBuf])
+ InsertToken(strOutput,iBuf+1,"!!!TRIM!!!");
+ if (bFixChars[iBuf])
+ InsertToken(strOutput,iBuf+1,"!!!FIXCHARS!!!");
+ if (bEncode[iBuf])
+ InsertToken(strOutput,iBuf+1,"!!!ENCODE!!!");
+ }
+ int i = reg.RegFind(curInput.c_str());
+ while (i > -1 && (i < (int)curInput.size() || curInput.empty()))
+ {
+ if (!bAppend)
+ {
+ dest = "";
+ bAppend = true;
+ }
+ std::string strCurOutput=strOutput;
+
+ if (iOptional > -1) // check that required param is there
+ {
+ char temp[12];
+ sprintf(temp,"\\%i",iOptional);
+ std::string szParam = reg.GetReplaceString(temp);
+ CRegExp reg2;
+ reg2.RegComp("(.*)(\\\\\\(.*\\\\2.*)\\\\\\)(.*)");
+ int i2=reg2.RegFind(strCurOutput.c_str());
+ while (i2 > -1)
+ {
+ std::string szRemove(reg2.GetMatch(2));
+ int iRemove = szRemove.size();
+ int i3 = strCurOutput.find(szRemove);
+ if (!szParam.empty())
+ {
+ strCurOutput.erase(i3+iRemove,2);
+ strCurOutput.erase(i3,2);
+ }
+ else
+ strCurOutput.replace(strCurOutput.begin()+i3,strCurOutput.begin()+i3+iRemove+2,"");
+
+ i2 = reg2.RegFind(strCurOutput.c_str());
+ }
+ }
+
+ int iLen = reg.GetFindLen();
+ // nasty hack #1 - & means \0 in a replace string
+ StringUtils::Replace(strCurOutput, "&","!!!AMPAMP!!!");
+ std::string result = reg.GetReplaceString(strCurOutput);
+ if (!result.empty())
+ {
+ std::string strResult(result);
+ StringUtils::Replace(strResult, "!!!AMPAMP!!!","&");
+ Clean(strResult);
+ ReplaceBuffers(strResult);
+ if (iCompare > -1)
+ {
+ std::string strResultNoCase = strResult;
+ StringUtils::ToLower(strResultNoCase);
+ if (strResultNoCase.find(m_param[iCompare-1]) != std::string::npos)
+ dest += strResult;
+ }
+ else
+ dest += strResult;
+ }
+ if (bRepeat && iLen > 0)
+ {
+ curInput.erase(0,i+iLen>(int)curInput.size()?curInput.size():i+iLen);
+ i = reg.RegFind(curInput.c_str());
+ }
+ else
+ i = -1;
+ }
+ }
+}
+
+void CScraperParser::ParseXSLT(const std::string& input, std::string& dest, TiXmlElement* element, bool bAppend)
+{
+#ifdef HAVE_LIBXSLT
+ TiXmlElement* pSheet = element->FirstChildElement();
+ if (pSheet)
+ {
+ XSLTUtils xsltUtils;
+ std::string strXslt;
+ strXslt << *pSheet;
+ ReplaceBuffers(strXslt);
+
+ if (!xsltUtils.SetInput(input))
+ CLog::Log(LOGDEBUG, "could not parse input XML");
+
+ if (!xsltUtils.SetStylesheet(strXslt))
+ CLog::Log(LOGDEBUG, "could not parse stylesheet XML");
+
+ xsltUtils.XSLTTransform(dest);
+ }
+#endif
+}
+
+TiXmlElement *FirstChildScraperElement(TiXmlElement *element)
+{
+ for (TiXmlElement *child = element->FirstChildElement(); child; child = child->NextSiblingElement())
+ {
+#ifdef HAVE_LIBXSLT
+ if (child->ValueStr() == "XSLT")
+ return child;
+#endif
+ if (child->ValueStr() == "RegExp")
+ return child;
+ }
+ return NULL;
+}
+
+TiXmlElement *NextSiblingScraperElement(TiXmlElement *element)
+{
+ for (TiXmlElement *next = element->NextSiblingElement(); next; next = next->NextSiblingElement())
+ {
+#ifdef HAVE_LIBXSLT
+ if (next->ValueStr() == "XSLT")
+ return next;
+#endif
+ if (next->ValueStr() == "RegExp")
+ return next;
+ }
+ return NULL;
+}
+
+void CScraperParser::ParseNext(TiXmlElement* element)
+{
+ TiXmlElement* pReg = element;
+ while (pReg)
+ {
+ TiXmlElement* pChildReg = FirstChildScraperElement(pReg);
+ if (pChildReg)
+ ParseNext(pChildReg);
+ else
+ {
+ TiXmlElement* pChildReg = pReg->FirstChildElement("clear");
+ if (pChildReg)
+ ParseNext(pChildReg);
+ }
+
+ int iDest = 1;
+ bool bAppend = false;
+ const char* szDest = pReg->Attribute("dest");
+ if (szDest && strlen(szDest))
+ {
+ if (szDest[strlen(szDest)-1] == '+')
+ bAppend = true;
+
+ iDest = atoi(szDest);
+ }
+
+ const char *szInput = pReg->Attribute("input");
+ std::string strInput;
+ if (szInput)
+ {
+ strInput = szInput;
+ ReplaceBuffers(strInput);
+ }
+ else
+ strInput = m_param[0];
+
+ const char* szConditional = pReg->Attribute("conditional");
+ bool bExecute = true;
+ if (szConditional)
+ {
+ bool bInverse=false;
+ if (szConditional[0] == '!')
+ {
+ bInverse = true;
+ szConditional++;
+ }
+ std::string strSetting;
+ if (m_scraper && m_scraper->HasSettings())
+ strSetting = m_scraper->GetSetting(szConditional);
+ bExecute = bInverse != (strSetting == "true");
+ }
+
+ if (bExecute)
+ {
+ if (iDest-1 < MAX_SCRAPER_BUFFERS && iDest-1 > -1)
+ {
+#ifdef HAVE_LIBXSLT
+ if (pReg->ValueStr() == "XSLT")
+ ParseXSLT(strInput, m_param[iDest - 1], pReg, bAppend);
+ else
+#endif
+ ParseExpression(strInput, m_param[iDest - 1],pReg,bAppend);
+ }
+ else
+ CLog::Log(LOGERROR,"CScraperParser::ParseNext: destination buffer "
+ "out of bounds, skipping expression");
+ }
+ pReg = NextSiblingScraperElement(pReg);
+ }
+}
+
+const std::string CScraperParser::Parse(const std::string& strTag,
+ CScraper* scraper)
+{
+ TiXmlElement* pChildElement = m_pRootElement->FirstChildElement(strTag.c_str());
+ if(pChildElement == NULL)
+ {
+ CLog::Log(LOGERROR, "{}: Could not find scraper function {}", __FUNCTION__, strTag);
+ return "";
+ }
+ int iResult = 1; // default to param 1
+ pChildElement->QueryIntAttribute("dest",&iResult);
+ TiXmlElement* pChildStart = FirstChildScraperElement(pChildElement);
+ m_scraper = scraper;
+ ParseNext(pChildStart);
+ std::string tmp = m_param[iResult-1];
+
+ const char* szClearBuffers = pChildElement->Attribute("clearbuffers");
+ if (!szClearBuffers || StringUtils::CompareNoCase(szClearBuffers, "no") != 0)
+ ClearBuffers();
+
+ return tmp;
+}
+
+void CScraperParser::Clean(std::string& strDirty)
+{
+ size_t i = 0;
+ std::string strBuffer;
+ while ((i = strDirty.find("!!!CLEAN!!!",i)) != std::string::npos)
+ {
+ size_t i2;
+ if ((i2 = strDirty.find("!!!CLEAN!!!",i+11)) != std::string::npos)
+ {
+ strBuffer = strDirty.substr(i+11,i2-i-11);
+ std::string strConverted(strBuffer);
+ HTML::CHTMLUtil::RemoveTags(strConverted);
+ StringUtils::Trim(strConverted);
+ strDirty.replace(i, i2-i+11, strConverted);
+ i += strConverted.size();
+ }
+ else
+ break;
+ }
+ i=0;
+ while ((i = strDirty.find("!!!TRIM!!!",i)) != std::string::npos)
+ {
+ size_t i2;
+ if ((i2 = strDirty.find("!!!TRIM!!!",i+10)) != std::string::npos)
+ {
+ strBuffer = strDirty.substr(i+10,i2-i-10);
+ StringUtils::Trim(strBuffer);
+ strDirty.replace(i, i2-i+10, strBuffer);
+ i += strBuffer.size();
+ }
+ else
+ break;
+ }
+ i=0;
+ while ((i = strDirty.find("!!!FIXCHARS!!!",i)) != std::string::npos)
+ {
+ size_t i2;
+ if ((i2 = strDirty.find("!!!FIXCHARS!!!",i+14)) != std::string::npos)
+ {
+ strBuffer = strDirty.substr(i+14,i2-i-14);
+ std::wstring wbuffer;
+ g_charsetConverter.utf8ToW(strBuffer, wbuffer, false, false, false);
+ std::wstring wConverted;
+ HTML::CHTMLUtil::ConvertHTMLToW(wbuffer,wConverted);
+ g_charsetConverter.wToUTF8(wConverted, strBuffer, false);
+ StringUtils::Trim(strBuffer);
+ ConvertJSON(strBuffer);
+ strDirty.replace(i, i2-i+14, strBuffer);
+ i += strBuffer.size();
+ }
+ else
+ break;
+ }
+ i=0;
+ while ((i=strDirty.find("!!!ENCODE!!!",i)) != std::string::npos)
+ {
+ size_t i2;
+ if ((i2 = strDirty.find("!!!ENCODE!!!",i+12)) != std::string::npos)
+ {
+ strBuffer = CURL::Encode(strDirty.substr(i + 12, i2 - i - 12));
+ strDirty.replace(i, i2-i+12, strBuffer);
+ i += strBuffer.size();
+ }
+ else
+ break;
+ }
+}
+
+void CScraperParser::ConvertJSON(std::string &string)
+{
+ CRegExp reg;
+ reg.RegComp("\\\\u([0-f]{4})");
+ while (reg.RegFind(string.c_str()) > -1)
+ {
+ int pos = reg.GetSubStart(1);
+ std::string szReplace(reg.GetMatch(1));
+
+ std::string replace = StringUtils::Format("&#x{};", szReplace);
+ string.replace(string.begin()+pos-2, string.begin()+pos+4, replace);
+ }
+
+ CRegExp reg2;
+ reg2.RegComp("\\\\x([0-9]{2})([^\\\\]+;)");
+ while (reg2.RegFind(string.c_str()) > -1)
+ {
+ int pos1 = reg2.GetSubStart(1);
+ int pos2 = reg2.GetSubStart(2);
+ std::string szHexValue(reg2.GetMatch(1));
+
+ std::string replace = std::to_string(std::stol(szHexValue, NULL, 16));
+ string.replace(string.begin()+pos1-2, string.begin()+pos2+reg2.GetSubLength(2), replace);
+ }
+
+ StringUtils::Replace(string, "\\\"","\"");
+}
+
+void CScraperParser::ClearBuffers()
+{
+ //clear all m_param strings
+ for (std::string& param : m_param)
+ param.clear();
+}
+
+void CScraperParser::GetBufferParams(bool* result, const char* attribute, bool defvalue)
+{
+ for (int iBuf=0;iBuf<MAX_SCRAPER_BUFFERS;++iBuf)
+ result[iBuf] = defvalue;
+ if (attribute)
+ {
+ std::vector<std::string> vecBufs;
+ StringUtils::Tokenize(attribute,vecBufs,",");
+ for (size_t nToken=0; nToken < vecBufs.size(); nToken++)
+ {
+ int index = atoi(vecBufs[nToken].c_str())-1;
+ if (index < MAX_SCRAPER_BUFFERS)
+ result[index] = !defvalue;
+ }
+ }
+}
+
+void CScraperParser::InsertToken(std::string& strOutput, int buf, const char* token)
+{
+ char temp[4];
+ sprintf(temp,"\\%i",buf);
+ size_t i2=0;
+ while ((i2 = strOutput.find(temp,i2)) != std::string::npos)
+ {
+ strOutput.insert(i2,token);
+ i2 += strlen(token) + strlen(temp);
+ strOutput.insert(i2,token);
+ }
+}
+
+void CScraperParser::AddDocument(const CXBMCTinyXML* doc)
+{
+ const TiXmlNode* node = doc->RootElement()->FirstChild();
+ while (node)
+ {
+ m_pRootElement->InsertEndChild(*node);
+ node = node->NextSibling();
+ }
+}
+
diff --git a/xbmc/utils/ScraperParser.h b/xbmc/utils/ScraperParser.h
new file mode 100644
index 0000000..ec1cfd2
--- /dev/null
+++ b/xbmc/utils/ScraperParser.h
@@ -0,0 +1,78 @@
+/*
+ * 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 <string>
+#include <vector>
+
+#define MAX_SCRAPER_BUFFERS 20
+
+namespace ADDON
+{
+ class CScraper;
+}
+
+class TiXmlElement;
+class CXBMCTinyXML;
+
+class CScraperSettings;
+
+class CScraperParser
+{
+public:
+ CScraperParser();
+ CScraperParser(const CScraperParser& parser);
+ ~CScraperParser();
+ CScraperParser& operator= (const CScraperParser& parser);
+ bool Load(const std::string& strXMLFile);
+ bool IsNoop() const { return m_isNoop; }
+
+ void Clear();
+ const std::string& GetFilename() const { return m_strFile; }
+ std::string GetSearchStringEncoding() const
+ { return m_SearchStringEncoding; }
+ const std::string Parse(const std::string& strTag,
+ ADDON::CScraper* scraper);
+
+ void AddDocument(const CXBMCTinyXML* doc);
+
+ std::string m_param[MAX_SCRAPER_BUFFERS];
+
+private:
+ bool LoadFromXML();
+ void ReplaceBuffers(std::string& strDest);
+ void ParseExpression(const std::string& input, std::string& dest, TiXmlElement* element, bool bAppend);
+
+ /*! \brief Parse an 'XSLT' declaration from the scraper
+ This allow us to transform an inbound XML document using XSLT
+ to a different type of XML document, ready to be output direct
+ to the album loaders or similar
+ \param input the input document
+ \param dest the output destination for the conversion
+ \param element the current XML element
+ \param bAppend append or clear the buffer
+ */
+ void ParseXSLT(const std::string& input, std::string& dest, TiXmlElement* element, bool bAppend);
+ void ParseNext(TiXmlElement* element);
+ void Clean(std::string& strDirty);
+ void ConvertJSON(std::string &string);
+ void ClearBuffers();
+ void GetBufferParams(bool* result, const char* attribute, bool defvalue);
+ void InsertToken(std::string& strOutput, int buf, const char* token);
+
+ CXBMCTinyXML* m_document;
+ TiXmlElement* m_pRootElement;
+
+ const char* m_SearchStringEncoding;
+ bool m_isNoop;
+
+ std::string m_strFile;
+ ADDON::CScraper* m_scraper;
+};
+
diff --git a/xbmc/utils/ScraperUrl.cpp b/xbmc/utils/ScraperUrl.cpp
new file mode 100644
index 0000000..f131b16
--- /dev/null
+++ b/xbmc/utils/ScraperUrl.cpp
@@ -0,0 +1,434 @@
+/*
+ * 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 "ScraperUrl.h"
+
+#include "CharsetConverter.h"
+#include "ServiceBroker.h"
+#include "URIUtils.h"
+#include "URL.h"
+#include "XMLUtils.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/ZipFile.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CharsetDetection.h"
+#include "utils/Mime.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cstring>
+#include <sstream>
+
+CScraperUrl::CScraperUrl() : m_relevance(0.0), m_parsed(false)
+{
+}
+
+CScraperUrl::CScraperUrl(const std::string& strUrl) : CScraperUrl()
+{
+ ParseFromData(strUrl);
+}
+
+CScraperUrl::CScraperUrl(const TiXmlElement* element) : CScraperUrl()
+{
+ ParseAndAppendUrl(element);
+}
+
+CScraperUrl::~CScraperUrl() = default;
+
+void CScraperUrl::Clear()
+{
+ m_urls.clear();
+ m_data.clear();
+ m_relevance = 0.0;
+ m_parsed = false;
+}
+
+void CScraperUrl::SetData(std::string data)
+{
+ m_data = std::move(data);
+ m_parsed = false;
+}
+
+const CScraperUrl::SUrlEntry CScraperUrl::GetFirstUrlByType(const std::string& type) const
+{
+ const auto url = std::find_if(m_urls.begin(), m_urls.end(), [type](const SUrlEntry& url) {
+ return url.m_type == UrlType::General && (type.empty() || url.m_aspect == type);
+ });
+ if (url != m_urls.end())
+ return *url;
+
+ return SUrlEntry();
+}
+
+const CScraperUrl::SUrlEntry CScraperUrl::GetSeasonUrl(int season, const std::string& type) const
+{
+ const auto url = std::find_if(m_urls.begin(), m_urls.end(), [season, type](const SUrlEntry& url) {
+ return url.m_type == UrlType::Season && url.m_season == season &&
+ (type.empty() || type == "thumb" || url.m_aspect == type);
+ });
+ if (url != m_urls.end())
+ return *url;
+
+ return SUrlEntry();
+}
+
+unsigned int CScraperUrl::GetMaxSeasonUrl() const
+{
+ unsigned int maxSeason = 0;
+ for (const auto& url : m_urls)
+ {
+ if (url.m_type == UrlType::Season && url.m_season > 0 &&
+ static_cast<unsigned int>(url.m_season) > maxSeason)
+ maxSeason = url.m_season;
+ }
+ return maxSeason;
+}
+
+std::string CScraperUrl::GetFirstThumbUrl() const
+{
+ if (m_urls.empty())
+ return {};
+
+ return GetThumbUrl(m_urls.front());
+}
+
+void CScraperUrl::GetThumbUrls(std::vector<std::string>& thumbs,
+ const std::string& type,
+ int season,
+ bool unique) const
+{
+ for (const auto& url : m_urls)
+ {
+ if (url.m_aspect == type || type.empty() || url.m_aspect.empty())
+ {
+ if ((url.m_type == CScraperUrl::UrlType::General && season == -1) ||
+ (url.m_type == CScraperUrl::UrlType::Season && url.m_season == season))
+ {
+ std::string thumbUrl = GetThumbUrl(url);
+ if (!unique || std::find(thumbs.begin(), thumbs.end(), thumbUrl) == thumbs.end())
+ thumbs.push_back(thumbUrl);
+ }
+ }
+ }
+}
+
+bool CScraperUrl::Parse()
+{
+ if (m_parsed)
+ return true;
+
+ auto dataToParse = m_data;
+ m_data.clear();
+ return ParseFromData(dataToParse);
+}
+
+bool CScraperUrl::ParseFromData(const std::string& data)
+{
+ if (data.empty())
+ return false;
+
+ CXBMCTinyXML doc;
+ /* strUrl is coming from internal sources (usually generated by scraper or from database)
+ * so strUrl is always in UTF-8 */
+ doc.Parse(data, TIXML_ENCODING_UTF8);
+
+ auto pElement = doc.RootElement();
+ if (pElement == nullptr)
+ {
+ m_urls.emplace_back(data);
+ m_data = data;
+ }
+ else
+ {
+ while (pElement != nullptr)
+ {
+ ParseAndAppendUrl(pElement);
+ pElement = pElement->NextSiblingElement(pElement->Value());
+ }
+ }
+
+ m_parsed = true;
+ return true;
+}
+
+bool CScraperUrl::ParseAndAppendUrl(const TiXmlElement* element)
+{
+ if (element == nullptr || element->FirstChild() == nullptr ||
+ element->FirstChild()->Value() == nullptr)
+ return false;
+
+ bool wasEmpty = m_data.empty();
+
+ std::stringstream stream;
+ stream << *element;
+ m_data += stream.str();
+
+ SUrlEntry url(element->FirstChild()->ValueStr());
+ url.m_spoof = XMLUtils::GetAttribute(element, "spoof");
+
+ const char* szPost = element->Attribute("post");
+ if (szPost && StringUtils::CompareNoCase(szPost, "yes") == 0)
+ url.m_post = true;
+ else
+ url.m_post = false;
+
+ const char* szIsGz = element->Attribute("gzip");
+ if (szIsGz && StringUtils::CompareNoCase(szIsGz, "yes") == 0)
+ url.m_isgz = true;
+ else
+ url.m_isgz = false;
+
+ url.m_cache = XMLUtils::GetAttribute(element, "cache");
+
+ const char* szType = element->Attribute("type");
+ if (szType && StringUtils::CompareNoCase(szType, "season") == 0)
+ {
+ url.m_type = UrlType::Season;
+ const char* szSeason = element->Attribute("season");
+ if (szSeason)
+ url.m_season = atoi(szSeason);
+ }
+
+ url.m_aspect = XMLUtils::GetAttribute(element, "aspect");
+ url.m_preview = XMLUtils::GetAttribute(element, "preview");
+
+ m_urls.push_back(url);
+
+ if (wasEmpty)
+ m_parsed = true;
+
+ return true;
+}
+
+// XML format is of strUrls is:
+// <TAG><url>...</url>...</TAG> (parsed by ParseElement) or <url>...</url> (ditto)
+bool CScraperUrl::ParseAndAppendUrlsFromEpisodeGuide(const std::string& episodeGuide)
+{
+ if (episodeGuide.empty())
+ return false;
+
+ // ok, now parse the xml file
+ CXBMCTinyXML doc;
+ /* strUrls is coming from internal sources so strUrls is always in UTF-8 */
+ doc.Parse(episodeGuide, TIXML_ENCODING_UTF8);
+ if (doc.RootElement() == nullptr)
+ return false;
+
+ bool wasEmpty = m_data.empty();
+
+ TiXmlHandle docHandle(&doc);
+ auto link = docHandle.FirstChild("episodeguide").Element();
+ if (link->FirstChildElement("url"))
+ {
+ for (link = link->FirstChildElement("url"); link; link = link->NextSiblingElement("url"))
+ ParseAndAppendUrl(link);
+ }
+ else if (link->FirstChild() && link->FirstChild()->Value())
+ ParseAndAppendUrl(link);
+
+ if (wasEmpty)
+ m_parsed = true;
+
+ return true;
+}
+
+void CScraperUrl::AddParsedUrl(const std::string& url,
+ const std::string& aspect,
+ const std::string& preview,
+ const std::string& referrer,
+ const std::string& cache,
+ bool post,
+ bool isgz,
+ int season)
+{
+ bool wasEmpty = m_data.empty();
+
+ TiXmlElement thumb("thumb");
+ thumb.SetAttribute("spoof", referrer);
+ thumb.SetAttribute("cache", cache);
+ if (post)
+ thumb.SetAttribute("post", "yes");
+ if (isgz)
+ thumb.SetAttribute("gzip", "yes");
+ if (season >= 0)
+ {
+ thumb.SetAttribute("season", std::to_string(season));
+ thumb.SetAttribute("type", "season");
+ }
+ thumb.SetAttribute("aspect", aspect);
+ thumb.SetAttribute("preview", preview);
+ TiXmlText text(url);
+ thumb.InsertEndChild(text);
+
+ m_data << thumb;
+
+ SUrlEntry nUrl(url);
+ nUrl.m_spoof = referrer;
+ nUrl.m_post = post;
+ nUrl.m_isgz = isgz;
+ nUrl.m_cache = cache;
+ nUrl.m_preview = preview;
+ if (season >= 0)
+ {
+ nUrl.m_type = UrlType::Season;
+ nUrl.m_season = season;
+ }
+ nUrl.m_aspect = aspect;
+
+ m_urls.push_back(nUrl);
+
+ if (wasEmpty)
+ m_parsed = true;
+}
+
+std::string CScraperUrl::GetThumbUrl(const CScraperUrl::SUrlEntry& entry)
+{
+ if (entry.m_spoof.empty())
+ return entry.m_url;
+
+ return entry.m_url + "|Referer=" + CURL::Encode(entry.m_spoof);
+}
+
+bool CScraperUrl::Get(const SUrlEntry& scrURL,
+ std::string& strHTML,
+ XFILE::CCurlFile& http,
+ const std::string& cacheContext)
+{
+ CURL url(scrURL.m_url);
+ http.SetReferer(scrURL.m_spoof);
+ std::string strCachePath;
+
+ if (!scrURL.m_cache.empty())
+ {
+ strCachePath = URIUtils::AddFileToFolder(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cachePath, "scrapers",
+ cacheContext, scrURL.m_cache);
+ if (XFILE::CFile::Exists(strCachePath))
+ {
+ XFILE::CFile file;
+ std::vector<uint8_t> buffer;
+ if (file.LoadFile(strCachePath, buffer) > 0)
+ {
+ strHTML.assign(reinterpret_cast<char*>(buffer.data()), buffer.size());
+ return true;
+ }
+ }
+ }
+
+ auto strHTML1 = strHTML;
+
+ if (scrURL.m_post)
+ {
+ std::string strOptions = url.GetOptions();
+ strOptions = strOptions.substr(1);
+ url.SetOptions("");
+
+ if (!http.Post(url.Get(), strOptions, strHTML1))
+ return false;
+ }
+ else if (!http.Get(url.Get(), strHTML1))
+ return false;
+
+ strHTML = strHTML1;
+
+ const auto mimeType = http.GetProperty(XFILE::FILE_PROPERTY_MIME_TYPE);
+ CMime::EFileType ftype = CMime::GetFileTypeFromMime(mimeType);
+ if (ftype == CMime::FileTypeUnknown)
+ ftype = CMime::GetFileTypeFromContent(strHTML);
+
+ if (ftype == CMime::FileTypeZip || ftype == CMime::FileTypeGZip)
+ {
+ XFILE::CZipFile file;
+ std::string strBuffer;
+ auto iSize = file.UnpackFromMemory(
+ strBuffer, strHTML, scrURL.m_isgz); // FIXME: use FileTypeGZip instead of scrURL.m_isgz?
+ if (iSize > 0)
+ {
+ strHTML = strBuffer;
+ CLog::Log(LOGDEBUG, "{}: Archive \"{}\" was unpacked in memory", __FUNCTION__, scrURL.m_url);
+ }
+ else
+ CLog::Log(LOGWARNING, "{}: \"{}\" looks like archive but cannot be unpacked", __FUNCTION__,
+ scrURL.m_url);
+ }
+
+ const auto reportedCharset = http.GetProperty(XFILE::FILE_PROPERTY_CONTENT_CHARSET);
+ if (ftype == CMime::FileTypeHtml)
+ {
+ std::string realHtmlCharset, converted;
+ if (!CCharsetDetection::ConvertHtmlToUtf8(strHTML, converted, reportedCharset, realHtmlCharset))
+ CLog::Log(LOGWARNING,
+ "{}: Can't find precise charset for HTML \"{}\", using \"{}\" as fallback",
+ __FUNCTION__, scrURL.m_url, realHtmlCharset);
+ else
+ CLog::Log(LOGDEBUG, "{}: Using \"{}\" charset for HTML \"{}\"", __FUNCTION__, realHtmlCharset,
+ scrURL.m_url);
+
+ strHTML = converted;
+ }
+ else if (ftype == CMime::FileTypeXml)
+ {
+ CXBMCTinyXML xmlDoc;
+ xmlDoc.Parse(strHTML, reportedCharset);
+
+ const auto realXmlCharset = xmlDoc.GetUsedCharset();
+ if (!realXmlCharset.empty())
+ {
+ CLog::Log(LOGDEBUG, "{}: Using \"{}\" charset for XML \"{}\"", __FUNCTION__, realXmlCharset,
+ scrURL.m_url);
+ std::string converted;
+ g_charsetConverter.ToUtf8(realXmlCharset, strHTML, converted);
+ strHTML = converted;
+ }
+ }
+ else if (ftype == CMime::FileTypePlainText ||
+ StringUtils::EqualsNoCase(mimeType.substr(0, 5), "text/"))
+ {
+ std::string realTextCharset;
+ std::string converted;
+ CCharsetDetection::ConvertPlainTextToUtf8(strHTML, converted, reportedCharset, realTextCharset);
+ strHTML = converted;
+ if (reportedCharset != realTextCharset)
+ CLog::Log(LOGWARNING,
+ "{}: Using \"{}\" charset for plain text \"{}\" instead of server reported \"{}\" "
+ "charset",
+ __FUNCTION__, realTextCharset, scrURL.m_url, reportedCharset);
+ else
+ CLog::Log(LOGDEBUG, "{}: Using \"{}\" charset for plain text \"{}\"", __FUNCTION__,
+ realTextCharset, scrURL.m_url);
+ }
+ else if (!reportedCharset.empty())
+ {
+ CLog::Log(LOGDEBUG, "{}: Using \"{}\" charset for \"{}\"", __FUNCTION__, reportedCharset,
+ scrURL.m_url);
+ if (reportedCharset != "UTF-8")
+ {
+ std::string converted;
+ g_charsetConverter.ToUtf8(reportedCharset, strHTML, converted);
+ strHTML = converted;
+ }
+ }
+ else
+ CLog::Log(LOGDEBUG, "{}: Using content of \"{}\" as binary or text with \"UTF-8\" charset",
+ __FUNCTION__, scrURL.m_url);
+
+ if (!scrURL.m_cache.empty())
+ {
+ const auto strCachePath = URIUtils::AddFileToFolder(
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cachePath, "scrapers",
+ cacheContext, scrURL.m_cache);
+ XFILE::CFile file;
+ if (!file.OpenForWrite(strCachePath, true) ||
+ file.Write(strHTML.data(), strHTML.size()) != static_cast<ssize_t>(strHTML.size()))
+ return false;
+ }
+ return true;
+}
diff --git a/xbmc/utils/ScraperUrl.h b/xbmc/utils/ScraperUrl.h
new file mode 100644
index 0000000..9ff416a
--- /dev/null
+++ b/xbmc/utils/ScraperUrl.h
@@ -0,0 +1,123 @@
+/*
+ * 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 <map>
+#include <string>
+#include <vector>
+
+class TiXmlElement;
+namespace XFILE
+{
+class CCurlFile;
+}
+
+class CScraperUrl
+{
+public:
+ enum class UrlType
+ {
+ General = 1,
+ Season = 2
+ };
+
+ struct SUrlEntry
+ {
+ explicit SUrlEntry(std::string url = "")
+ : m_url(std::move(url)), m_type(UrlType::General), m_post(false), m_isgz(false), m_season(-1)
+ {
+ }
+
+ std::string m_spoof;
+ std::string m_url;
+ std::string m_cache;
+ std::string m_aspect;
+ std::string m_preview;
+ UrlType m_type;
+ bool m_post;
+ bool m_isgz;
+ int m_season;
+ };
+
+ CScraperUrl();
+ explicit CScraperUrl(const std::string& strUrl);
+ explicit CScraperUrl(const TiXmlElement* element);
+ ~CScraperUrl();
+
+ void Clear();
+
+ bool HasData() const { return !m_data.empty(); }
+ const std::string& GetData() const { return m_data; }
+ void SetData(std::string data);
+
+ const std::string& GetTitle() const { return m_title; }
+ void SetTitle(std::string title) { m_title = std::move(title); }
+
+ const std::string& GetId() const { return m_id; }
+ void SetId(std::string id) { m_id = std::move(id); }
+
+ double GetRelevance() const { return m_relevance; }
+ void SetRelevance(double relevance) { m_relevance = relevance; }
+
+ bool HasUrls() const { return !m_urls.empty(); }
+ const std::vector<SUrlEntry>& GetUrls() const { return m_urls; }
+ void SetUrls(std::vector<SUrlEntry> urls) { m_urls = std::move(urls); }
+ void AppendUrl(SUrlEntry url) { m_urls.push_back(std::move(url)); }
+
+ const SUrlEntry GetFirstUrlByType(const std::string& type = "") const;
+ const SUrlEntry GetSeasonUrl(int season, const std::string& type = "") const;
+ unsigned int GetMaxSeasonUrl() const;
+
+ std::string GetFirstThumbUrl() const;
+
+ /*! \brief fetch the full URLs (including referrer) of thumbs
+ \param thumbs [out] vector of thumb URLs to fill
+ \param type the type of thumb URLs to fetch, if empty (the default) picks any
+ \param season number of season that we want thumbs for, -1 indicates no season (the default)
+ \param unique avoid adding duplicate URLs when adding to a thumbs vector with existing items
+ */
+ void GetThumbUrls(std::vector<std::string>& thumbs,
+ const std::string& type = "",
+ int season = -1,
+ bool unique = false) const;
+
+ bool Parse();
+ bool ParseFromData(const std::string& data); // copies by intention
+ bool ParseAndAppendUrl(const TiXmlElement* element);
+ bool ParseAndAppendUrlsFromEpisodeGuide(const std::string& episodeGuide); // copies by intention
+ void AddParsedUrl(const std::string& url,
+ const std::string& aspect = "",
+ const std::string& preview = "",
+ const std::string& referrer = "",
+ const std::string& cache = "",
+ bool post = false,
+ bool isgz = false,
+ int season = -1);
+
+ /*! \brief fetch the full URL (including referrer) of a thumb
+ \param URL entry to use to create the full URL
+ \return the full URL, including referrer
+ */
+ static std::string GetThumbUrl(const CScraperUrl::SUrlEntry& entry);
+
+ static bool Get(const SUrlEntry& scrURL,
+ std::string& strHTML,
+ XFILE::CCurlFile& http,
+ const std::string& cacheContext);
+
+ // ATTENTION: this member MUST NOT be used directly except from databases
+ std::string m_data;
+
+private:
+ std::string m_title;
+ std::string m_id;
+ double m_relevance;
+ std::vector<SUrlEntry> m_urls;
+ bool m_parsed;
+};
diff --git a/xbmc/utils/Screenshot.cpp b/xbmc/utils/Screenshot.cpp
new file mode 100644
index 0000000..25ecbac
--- /dev/null
+++ b/xbmc/utils/Screenshot.cpp
@@ -0,0 +1,117 @@
+/*
+ * 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 "Screenshot.h"
+
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "filesystem/File.h"
+#include "guilib/LocalizeStrings.h"
+#include "pictures/Picture.h"
+#include "settings/SettingPath.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/windows/GUIControlSettings.h"
+#include "utils/JobManager.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+
+std::vector<std::function<std::unique_ptr<IScreenshotSurface>()>> CScreenShot::m_screenShotSurfaces;
+
+void CScreenShot::Register(const std::function<std::unique_ptr<IScreenshotSurface>()>& createFunc)
+{
+ m_screenShotSurfaces.emplace_back(createFunc);
+}
+
+void CScreenShot::TakeScreenshot(const std::string& filename, bool sync)
+{
+ auto surface = m_screenShotSurfaces.back()();
+
+ if (!surface)
+ {
+ CLog::Log(LOGERROR, "failed to create screenshot surface");
+ return;
+ }
+
+ if (!surface->Capture())
+ {
+ CLog::Log(LOGERROR, "Screenshot {} failed", CURL::GetRedacted(filename));
+ return;
+ }
+
+ surface->CaptureVideo(true);
+
+ CLog::Log(LOGDEBUG, "Saving screenshot {}", CURL::GetRedacted(filename));
+
+ //set alpha byte to 0xFF
+ for (int y = 0; y < surface->GetHeight(); y++)
+ {
+ unsigned char* alphaptr = surface->GetBuffer() - 1 + y * surface->GetStride();
+ for (int x = 0; x < surface->GetWidth(); x++)
+ *(alphaptr += 4) = 0xFF;
+ }
+
+ //if sync is true, the png file needs to be completely written when this function returns
+ if (sync)
+ {
+ if (!CPicture::CreateThumbnailFromSurface(surface->GetBuffer(), surface->GetWidth(), surface->GetHeight(), surface->GetStride(), filename))
+ CLog::Log(LOGERROR, "Unable to write screenshot {}", CURL::GetRedacted(filename));
+
+ surface->ReleaseBuffer();
+ }
+ else
+ {
+ //make sure the file exists to avoid concurrency issues
+ XFILE::CFile file;
+ if (file.OpenForWrite(filename))
+ file.Close();
+ else
+ CLog::Log(LOGERROR, "Unable to create file {}", CURL::GetRedacted(filename));
+
+ //write .png file asynchronous with CThumbnailWriter, prevents stalling of the render thread
+ //buffer is deleted from CThumbnailWriter
+ CThumbnailWriter* thumbnailwriter = new CThumbnailWriter(surface->GetBuffer(), surface->GetWidth(), surface->GetHeight(), surface->GetStride(), filename);
+ CServiceBroker::GetJobManager()->AddJob(thumbnailwriter, nullptr);
+ }
+}
+
+void CScreenShot::TakeScreenshot()
+{
+ std::shared_ptr<CSettingPath> screenshotSetting = std::static_pointer_cast<CSettingPath>(CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(CSettings::SETTING_DEBUG_SCREENSHOTPATH));
+ if (!screenshotSetting)
+ return;
+
+ std::string strDir = screenshotSetting->GetValue();
+ if (strDir.empty())
+ {
+ if (!CGUIControlButtonSetting::GetPath(screenshotSetting, &g_localizeStrings))
+ return;
+
+ strDir = screenshotSetting->GetValue();
+ }
+
+ URIUtils::RemoveSlashAtEnd(strDir);
+
+ if (!strDir.empty())
+ {
+ std::string file =
+ CUtil::GetNextFilename(URIUtils::AddFileToFolder(strDir, "screenshot{:05}.png"), 65535);
+
+ if (!file.empty())
+ {
+ TakeScreenshot(file, false);
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Too many screen shots or invalid folder");
+ }
+ }
+}
diff --git a/xbmc/utils/Screenshot.h b/xbmc/utils/Screenshot.h
new file mode 100644
index 0000000..6c44558
--- /dev/null
+++ b/xbmc/utils/Screenshot.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "IScreenshotSurface.h"
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+class CScreenShot
+{
+public:
+ static void Register(const std::function<std::unique_ptr<IScreenshotSurface>()>& createFunc);
+
+ static void TakeScreenshot();
+ static void TakeScreenshot(const std::string &filename, bool sync);
+
+private:
+ static std::vector<std::function<std::unique_ptr<IScreenshotSurface>()>> m_screenShotSurfaces;
+};
diff --git a/xbmc/utils/SortUtils.cpp b/xbmc/utils/SortUtils.cpp
new file mode 100644
index 0000000..b6b2c21
--- /dev/null
+++ b/xbmc/utils/SortUtils.cpp
@@ -0,0 +1,1385 @@
+/*
+ * 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 "SortUtils.h"
+
+#include "LangInfo.h"
+#include "URL.h"
+#include "Util.h"
+#include "utils/CharsetConverter.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+#include <algorithm>
+#include <inttypes.h>
+
+std::string ArrayToString(SortAttribute attributes, const CVariant &variant, const std::string &separator = " / ")
+{
+ std::vector<std::string> strArray;
+ if (variant.isArray())
+ {
+ for (CVariant::const_iterator_array it = variant.begin_array(); it != variant.end_array(); ++it)
+ {
+ if (attributes & SortAttributeIgnoreArticle)
+ strArray.push_back(SortUtils::RemoveArticles(it->asString()));
+ else
+ strArray.push_back(it->asString());
+ }
+
+ return StringUtils::Join(strArray, separator);
+ }
+ else if (variant.isString())
+ {
+ if (attributes & SortAttributeIgnoreArticle)
+ return SortUtils::RemoveArticles(variant.asString());
+ else
+ return variant.asString();
+ }
+
+ return "";
+}
+
+std::string ByLabel(SortAttribute attributes, const SortItem &values)
+{
+ if (attributes & SortAttributeIgnoreArticle)
+ return SortUtils::RemoveArticles(values.at(FieldLabel).asString());
+
+ return values.at(FieldLabel).asString();
+}
+
+std::string ByFile(SortAttribute attributes, const SortItem &values)
+{
+ CURL url(values.at(FieldPath).asString());
+
+ return StringUtils::Format("{} {}", url.GetFileNameWithoutPath(),
+ values.at(FieldStartOffset).asInteger());
+}
+
+std::string ByPath(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", values.at(FieldPath).asString(),
+ values.at(FieldStartOffset).asInteger());
+}
+
+std::string ByLastPlayed(SortAttribute attributes, const SortItem &values)
+{
+ if (attributes & SortAttributeIgnoreLabel)
+ return values.at(FieldLastPlayed).asString();
+
+ return StringUtils::Format("{} {}", values.at(FieldLastPlayed).asString(),
+ ByLabel(attributes, values));
+}
+
+std::string ByPlaycount(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", (int)values.at(FieldPlaycount).asInteger(),
+ ByLabel(attributes, values));
+}
+
+std::string ByDate(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldDate).asString() + " " + ByLabel(attributes, values);
+}
+
+std::string ByDateAdded(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", values.at(FieldDateAdded).asString(),
+ (int)values.at(FieldId).asInteger());
+}
+
+std::string BySize(SortAttribute attributes, const SortItem &values)
+{
+ return std::to_string(values.at(FieldSize).asInteger());
+}
+
+std::string ByDriveType(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", (int)values.at(FieldDriveType).asInteger(),
+ ByLabel(attributes, values));
+}
+
+std::string ByTitle(SortAttribute attributes, const SortItem &values)
+{
+ if (attributes & SortAttributeIgnoreArticle)
+ return SortUtils::RemoveArticles(values.at(FieldTitle).asString());
+
+ return values.at(FieldTitle).asString();
+}
+
+std::string ByAlbum(SortAttribute attributes, const SortItem &values)
+{
+ std::string album = values.at(FieldAlbum).asString();
+ if (attributes & SortAttributeIgnoreArticle)
+ album = SortUtils::RemoveArticles(album);
+
+ std::string label =
+ StringUtils::Format("{} {}", album, ArrayToString(attributes, values.at(FieldArtist)));
+
+ const CVariant &track = values.at(FieldTrackNumber);
+ if (!track.isNull())
+ label += StringUtils::Format(" {}", (int)track.asInteger());
+
+ return label;
+}
+
+std::string ByAlbumType(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldAlbumType).asString() + " " + ByLabel(attributes, values);
+}
+
+std::string ByArtist(SortAttribute attributes, const SortItem &values)
+{
+ std::string label;
+ if (attributes & SortAttributeUseArtistSortName)
+ {
+ const CVariant &artistsort = values.at(FieldArtistSort);
+ if (!artistsort.isNull())
+ label = artistsort.asString();
+ }
+ if (label.empty())
+ label = ArrayToString(attributes, values.at(FieldArtist));
+
+ const CVariant &album = values.at(FieldAlbum);
+ if (!album.isNull())
+ label += " " + SortUtils::RemoveArticles(album.asString());
+
+ const CVariant &track = values.at(FieldTrackNumber);
+ if (!track.isNull())
+ label += StringUtils::Format(" {}", (int)track.asInteger());
+
+ return label;
+}
+
+std::string ByArtistThenYear(SortAttribute attributes, const SortItem &values)
+{
+ std::string label;
+ if (attributes & SortAttributeUseArtistSortName)
+ {
+ const CVariant &artistsort = values.at(FieldArtistSort);
+ if (!artistsort.isNull())
+ label = artistsort.asString();
+ }
+ if (label.empty())
+ label = ArrayToString(attributes, values.at(FieldArtist));
+
+ const CVariant &year = values.at(FieldYear);
+ if (!year.isNull())
+ label += StringUtils::Format(" {}", static_cast<int>(year.asInteger()));
+
+ const CVariant &album = values.at(FieldAlbum);
+ if (!album.isNull())
+ label += " " + SortUtils::RemoveArticles(album.asString());
+
+ const CVariant &track = values.at(FieldTrackNumber);
+ if (!track.isNull())
+ label += StringUtils::Format(" {}", (int)track.asInteger());
+
+ return label;
+}
+
+std::string ByTrackNumber(SortAttribute attributes, const SortItem &values)
+{
+ return std::to_string((int)values.at(FieldTrackNumber).asInteger());
+}
+
+std::string ByTotalDiscs(SortAttribute attributes, const SortItem& values)
+{
+ return StringUtils::Format("{} {}", static_cast<int>(values.at(FieldTotalDiscs).asInteger()),
+ ByLabel(attributes, values));
+}
+std::string ByTime(SortAttribute attributes, const SortItem &values)
+{
+ std::string label;
+ const CVariant &time = values.at(FieldTime);
+ if (time.isInteger())
+ label = std::to_string((int)time.asInteger());
+ else
+ label = time.asString();
+ return label;
+}
+
+std::string ByProgramCount(SortAttribute attributes, const SortItem &values)
+{
+ return std::to_string((int)values.at(FieldProgramCount).asInteger());
+}
+
+std::string ByPlaylistOrder(SortAttribute attributes, const SortItem &values)
+{
+ //! @todo Playlist order is hacked into program count variable (not nice, but ok until 2.0)
+ return ByProgramCount(attributes, values);
+}
+
+std::string ByGenre(SortAttribute attributes, const SortItem &values)
+{
+ return ArrayToString(attributes, values.at(FieldGenre));
+}
+
+std::string ByCountry(SortAttribute attributes, const SortItem &values)
+{
+ return ArrayToString(attributes, values.at(FieldCountry));
+}
+
+std::string ByYear(SortAttribute attributes, const SortItem &values)
+{
+ std::string label;
+ const CVariant &airDate = values.at(FieldAirDate);
+ if (!airDate.isNull() && !airDate.asString().empty())
+ label = airDate.asString() + " ";
+
+ label += std::to_string((int)values.at(FieldYear).asInteger());
+
+ const CVariant &album = values.at(FieldAlbum);
+ if (!album.isNull())
+ label += " " + SortUtils::RemoveArticles(album.asString());
+
+ const CVariant &track = values.at(FieldTrackNumber);
+ if (!track.isNull())
+ label += StringUtils::Format(" {}", (int)track.asInteger());
+
+ label += " " + ByLabel(attributes, values);
+
+ return label;
+}
+
+std::string ByOrigDate(SortAttribute attributes, const SortItem& values)
+{
+ std::string label;
+ label = values.at(FieldOrigDate).asString();
+
+ const CVariant& album = values.at(FieldAlbum);
+ if (!album.isNull())
+ label += " " + SortUtils::RemoveArticles(album.asString());
+
+ const CVariant &track = values.at(FieldTrackNumber);
+ if (!track.isNull())
+ label += StringUtils::Format(" {}", static_cast<int>(track.asInteger()));
+
+ label += " " + ByLabel(attributes, values);
+
+ return label;
+}
+
+std::string BySortTitle(SortAttribute attributes, const SortItem &values)
+{
+ std::string title = values.at(FieldSortTitle).asString();
+ if (title.empty())
+ title = values.at(FieldTitle).asString();
+
+ if (attributes & SortAttributeIgnoreArticle)
+ title = SortUtils::RemoveArticles(title);
+
+ return title;
+}
+
+std::string ByOriginalTitle(SortAttribute attributes, const SortItem& values)
+{
+
+ std::string title = values.at(FieldOriginalTitle).asString();
+ if (title.empty())
+ title = values.at(FieldSortTitle).asString();
+
+ if (title.empty())
+ title = values.at(FieldTitle).asString();
+
+ if (attributes & SortAttributeIgnoreArticle)
+ title = SortUtils::RemoveArticles(title);
+
+ return title;
+}
+
+std::string ByRating(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{:f} {}", values.at(FieldRating).asFloat(),
+ ByLabel(attributes, values));
+}
+
+std::string ByUserRating(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", static_cast<int>(values.at(FieldUserRating).asInteger()),
+ ByLabel(attributes, values));
+}
+
+std::string ByVotes(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", (int)values.at(FieldVotes).asInteger(),
+ ByLabel(attributes, values));
+}
+
+std::string ByTop250(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", (int)values.at(FieldTop250).asInteger(),
+ ByLabel(attributes, values));
+}
+
+std::string ByMPAA(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldMPAA).asString() + " " + ByLabel(attributes, values);
+}
+
+std::string ByStudio(SortAttribute attributes, const SortItem &values)
+{
+ return ArrayToString(attributes, values.at(FieldStudio));
+}
+
+std::string ByEpisodeNumber(SortAttribute attributes, const SortItem &values)
+{
+ // we calculate an offset number based on the episode's
+ // sort season and episode values. in addition
+ // we include specials 'episode' numbers to get proper
+ // sorting of multiple specials in a row. each
+ // of these are given their particular ranges to semi-ensure uniqueness.
+ // theoretical problem: if a show has > 2^15 specials and two of these are placed
+ // after each other they will sort backwards. if a show has > 2^32-1 seasons
+ // or if a season has > 2^16-1 episodes strange things will happen (overflow)
+ uint64_t num;
+ const CVariant &episodeSpecial = values.at(FieldEpisodeNumberSpecialSort);
+ const CVariant &seasonSpecial = values.at(FieldSeasonSpecialSort);
+ if (!episodeSpecial.isNull() && !seasonSpecial.isNull() &&
+ (episodeSpecial.asInteger() > 0 || seasonSpecial.asInteger() > 0))
+ num = ((uint64_t)seasonSpecial.asInteger() << 32) + (episodeSpecial.asInteger() << 16) - ((2 << 15) - values.at(FieldEpisodeNumber).asInteger());
+ else
+ num = ((uint64_t)values.at(FieldSeason).asInteger() << 32) + (values.at(FieldEpisodeNumber).asInteger() << 16);
+
+ std::string title;
+ if (values.find(FieldMediaType) != values.end() && values.at(FieldMediaType).asString() == MediaTypeMovie)
+ title = BySortTitle(attributes, values);
+ if (title.empty())
+ title = ByLabel(attributes, values);
+
+ return StringUtils::Format("{} {}", num, title);
+}
+
+std::string BySeason(SortAttribute attributes, const SortItem &values)
+{
+ int season = (int)values.at(FieldSeason).asInteger();
+ const CVariant &specialSeason = values.at(FieldSeasonSpecialSort);
+ if (!specialSeason.isNull())
+ season = (int)specialSeason.asInteger();
+
+ return StringUtils::Format("{} {}", season, ByLabel(attributes, values));
+}
+
+std::string ByNumberOfEpisodes(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", (int)values.at(FieldNumberOfEpisodes).asInteger(),
+ ByLabel(attributes, values));
+}
+
+std::string ByNumberOfWatchedEpisodes(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", (int)values.at(FieldNumberOfWatchedEpisodes).asInteger(),
+ ByLabel(attributes, values));
+}
+
+std::string ByTvShowStatus(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldTvShowStatus).asString() + " " + ByLabel(attributes, values);
+}
+
+std::string ByTvShowTitle(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldTvShowTitle).asString() + " " + ByLabel(attributes, values);
+}
+
+std::string ByProductionCode(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldProductionCode).asString();
+}
+
+std::string ByVideoResolution(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", (int)values.at(FieldVideoResolution).asInteger(),
+ ByLabel(attributes, values));
+}
+
+std::string ByVideoCodec(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", values.at(FieldVideoCodec).asString(),
+ ByLabel(attributes, values));
+}
+
+std::string ByVideoAspectRatio(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{:.3f} {}", values.at(FieldVideoAspectRatio).asFloat(),
+ ByLabel(attributes, values));
+}
+
+std::string ByAudioChannels(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", (int)values.at(FieldAudioChannels).asInteger(),
+ ByLabel(attributes, values));
+}
+
+std::string ByAudioCodec(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", values.at(FieldAudioCodec).asString(),
+ ByLabel(attributes, values));
+}
+
+std::string ByAudioLanguage(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", values.at(FieldAudioLanguage).asString(),
+ ByLabel(attributes, values));
+}
+
+std::string BySubtitleLanguage(SortAttribute attributes, const SortItem &values)
+{
+ return StringUtils::Format("{} {}", values.at(FieldSubtitleLanguage).asString(),
+ ByLabel(attributes, values));
+}
+
+std::string ByBitrate(SortAttribute attributes, const SortItem &values)
+{
+ return std::to_string(values.at(FieldBitrate).asInteger());
+}
+
+std::string ByListeners(SortAttribute attributes, const SortItem &values)
+{
+ return std::to_string(values.at(FieldListeners).asInteger());
+}
+
+std::string ByRandom(SortAttribute attributes, const SortItem &values)
+{
+ return std::to_string(CUtil::GetRandomNumber());
+}
+
+std::string ByChannel(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldChannelName).asString();
+}
+
+std::string ByChannelNumber(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldChannelNumber).asString();
+}
+
+std::string ByClientChannelOrder(SortAttribute attributes, const SortItem& values)
+{
+ return values.at(FieldClientChannelOrder).asString();
+}
+
+std::string ByProvider(SortAttribute attributes, const SortItem& values)
+{
+ return values.at(FieldProvider).asString();
+}
+
+std::string ByUserPreference(SortAttribute attributes, const SortItem& values)
+{
+ return values.at(FieldUserPreference).asString();
+}
+
+std::string ByDateTaken(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldDateTaken).asString();
+}
+
+std::string ByRelevance(SortAttribute attributes, const SortItem &values)
+{
+ return std::to_string((int)values.at(FieldRelevance).asInteger());
+}
+
+std::string ByInstallDate(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldInstallDate).asString();
+}
+
+std::string ByLastUpdated(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldLastUpdated).asString();
+}
+
+std::string ByLastUsed(SortAttribute attributes, const SortItem &values)
+{
+ return values.at(FieldLastUsed).asString();
+}
+
+std::string ByBPM(SortAttribute attributes, const SortItem& values)
+{
+ return StringUtils::Format("{} {}", static_cast<int>(values.at(FieldBPM).asInteger()),
+ ByLabel(attributes, values));
+}
+
+bool preliminarySort(const SortItem &left, const SortItem &right, bool handleFolder, bool &result, std::wstring &labelLeft, std::wstring &labelRight)
+{
+ // make sure both items have the necessary data to do the sorting
+ SortItem::const_iterator itLeftSort, itRightSort;
+ if ((itLeftSort = left.find(FieldSort)) == left.end())
+ {
+ result = false;
+ return true;
+ }
+ if ((itRightSort = right.find(FieldSort)) == right.end())
+ {
+ result = true;
+ return true;
+ }
+
+ // look at special sorting behaviour
+ SortItem::const_iterator itLeft, itRight;
+ SortSpecial leftSortSpecial = SortSpecialNone;
+ SortSpecial rightSortSpecial = SortSpecialNone;
+ if ((itLeft = left.find(FieldSortSpecial)) != left.end() && itLeft->second.asInteger() <= (int64_t)SortSpecialOnBottom)
+ leftSortSpecial = (SortSpecial)itLeft->second.asInteger();
+ if ((itRight = right.find(FieldSortSpecial)) != right.end() && itRight->second.asInteger() <= (int64_t)SortSpecialOnBottom)
+ rightSortSpecial = (SortSpecial)itRight->second.asInteger();
+
+ // one has a special sort
+ if (leftSortSpecial != rightSortSpecial)
+ {
+ // left should be sorted on top
+ // or right should be sorted on bottom
+ // => left is sorted above right
+ if (leftSortSpecial == SortSpecialOnTop ||
+ rightSortSpecial == SortSpecialOnBottom)
+ {
+ result = true;
+ return true;
+ }
+
+ // otherwise right is sorted above left
+ result = false;
+ return true;
+ }
+ // both have either sort on top or sort on bottom -> leave as-is
+ else if (leftSortSpecial != SortSpecialNone)
+ {
+ result = false;
+ return true;
+ }
+
+ if (handleFolder)
+ {
+ itLeft = left.find(FieldFolder);
+ itRight = right.find(FieldFolder);
+ if (itLeft != left.end() && itRight != right.end() &&
+ itLeft->second.asBoolean() != itRight->second.asBoolean())
+ {
+ result = itLeft->second.asBoolean();
+ return true;
+ }
+ }
+
+ labelLeft = itLeftSort->second.asWideString();
+ labelRight = itRightSort->second.asWideString();
+
+ return false;
+}
+
+bool SorterAscending(const SortItem &left, const SortItem &right)
+{
+ bool result;
+ std::wstring labelLeft, labelRight;
+ if (preliminarySort(left, right, true, result, labelLeft, labelRight))
+ return result;
+
+ return StringUtils::AlphaNumericCompare(labelLeft.c_str(), labelRight.c_str()) < 0;
+}
+
+bool SorterDescending(const SortItem &left, const SortItem &right)
+{
+ bool result;
+ std::wstring labelLeft, labelRight;
+ if (preliminarySort(left, right, true, result, labelLeft, labelRight))
+ return result;
+
+ return StringUtils::AlphaNumericCompare(labelLeft.c_str(), labelRight.c_str()) > 0;
+}
+
+bool SorterIgnoreFoldersAscending(const SortItem &left, const SortItem &right)
+{
+ bool result;
+ std::wstring labelLeft, labelRight;
+ if (preliminarySort(left, right, false, result, labelLeft, labelRight))
+ return result;
+
+ return StringUtils::AlphaNumericCompare(labelLeft.c_str(), labelRight.c_str()) < 0;
+}
+
+bool SorterIgnoreFoldersDescending(const SortItem &left, const SortItem &right)
+{
+ bool result;
+ std::wstring labelLeft, labelRight;
+ if (preliminarySort(left, right, false, result, labelLeft, labelRight))
+ return result;
+
+ return StringUtils::AlphaNumericCompare(labelLeft.c_str(), labelRight.c_str()) > 0;
+}
+
+bool SorterIndirectAscending(const SortItemPtr &left, const SortItemPtr &right)
+{
+ return SorterAscending(*left, *right);
+}
+
+bool SorterIndirectDescending(const SortItemPtr &left, const SortItemPtr &right)
+{
+ return SorterDescending(*left, *right);
+}
+
+bool SorterIndirectIgnoreFoldersAscending(const SortItemPtr &left, const SortItemPtr &right)
+{
+ return SorterIgnoreFoldersAscending(*left, *right);
+}
+
+bool SorterIndirectIgnoreFoldersDescending(const SortItemPtr &left, const SortItemPtr &right)
+{
+ return SorterIgnoreFoldersDescending(*left, *right);
+}
+
+// clang-format off
+std::map<SortBy, SortUtils::SortPreparator> fillPreparators()
+{
+ std::map<SortBy, SortUtils::SortPreparator> preparators;
+
+ preparators[SortByNone] = NULL;
+ preparators[SortByLabel] = ByLabel;
+ preparators[SortByDate] = ByDate;
+ preparators[SortBySize] = BySize;
+ preparators[SortByFile] = ByFile;
+ preparators[SortByPath] = ByPath;
+ preparators[SortByDriveType] = ByDriveType;
+ preparators[SortByTitle] = ByTitle;
+ preparators[SortByTrackNumber] = ByTrackNumber;
+ preparators[SortByTime] = ByTime;
+ preparators[SortByArtist] = ByArtist;
+ preparators[SortByArtistThenYear] = ByArtistThenYear;
+ preparators[SortByAlbum] = ByAlbum;
+ preparators[SortByAlbumType] = ByAlbumType;
+ preparators[SortByGenre] = ByGenre;
+ preparators[SortByCountry] = ByCountry;
+ preparators[SortByYear] = ByYear;
+ preparators[SortByRating] = ByRating;
+ preparators[SortByUserRating] = ByUserRating;
+ preparators[SortByVotes] = ByVotes;
+ preparators[SortByTop250] = ByTop250;
+ preparators[SortByProgramCount] = ByProgramCount;
+ preparators[SortByPlaylistOrder] = ByPlaylistOrder;
+ preparators[SortByEpisodeNumber] = ByEpisodeNumber;
+ preparators[SortBySeason] = BySeason;
+ preparators[SortByNumberOfEpisodes] = ByNumberOfEpisodes;
+ preparators[SortByNumberOfWatchedEpisodes] = ByNumberOfWatchedEpisodes;
+ preparators[SortByTvShowStatus] = ByTvShowStatus;
+ preparators[SortByTvShowTitle] = ByTvShowTitle;
+ preparators[SortBySortTitle] = BySortTitle;
+ preparators[SortByProductionCode] = ByProductionCode;
+ preparators[SortByMPAA] = ByMPAA;
+ preparators[SortByVideoResolution] = ByVideoResolution;
+ preparators[SortByVideoCodec] = ByVideoCodec;
+ preparators[SortByVideoAspectRatio] = ByVideoAspectRatio;
+ preparators[SortByAudioChannels] = ByAudioChannels;
+ preparators[SortByAudioCodec] = ByAudioCodec;
+ preparators[SortByAudioLanguage] = ByAudioLanguage;
+ preparators[SortBySubtitleLanguage] = BySubtitleLanguage;
+ preparators[SortByStudio] = ByStudio;
+ preparators[SortByDateAdded] = ByDateAdded;
+ preparators[SortByLastPlayed] = ByLastPlayed;
+ preparators[SortByPlaycount] = ByPlaycount;
+ preparators[SortByListeners] = ByListeners;
+ preparators[SortByBitrate] = ByBitrate;
+ preparators[SortByRandom] = ByRandom;
+ preparators[SortByChannel] = ByChannel;
+ preparators[SortByChannelNumber] = ByChannelNumber;
+ preparators[SortByClientChannelOrder] = ByClientChannelOrder;
+ preparators[SortByProvider] = ByProvider;
+ preparators[SortByUserPreference] = ByUserPreference;
+ preparators[SortByDateTaken] = ByDateTaken;
+ preparators[SortByRelevance] = ByRelevance;
+ preparators[SortByInstallDate] = ByInstallDate;
+ preparators[SortByLastUpdated] = ByLastUpdated;
+ preparators[SortByLastUsed] = ByLastUsed;
+ preparators[SortByTotalDiscs] = ByTotalDiscs;
+ preparators[SortByOrigDate] = ByOrigDate;
+ preparators[SortByBPM] = ByBPM;
+ preparators[SortByOriginalTitle] = ByOriginalTitle;
+
+ return preparators;
+}
+// clang-format on
+
+std::map<SortBy, Fields> fillSortingFields()
+{
+ std::map<SortBy, Fields> sortingFields;
+
+ sortingFields.insert(std::pair<SortBy, Fields>(SortByNone, Fields()));
+
+ sortingFields[SortByLabel].insert(FieldLabel);
+ sortingFields[SortByDate].insert(FieldDate);
+ sortingFields[SortBySize].insert(FieldSize);
+ sortingFields[SortByFile].insert(FieldPath);
+ sortingFields[SortByFile].insert(FieldStartOffset);
+ sortingFields[SortByPath].insert(FieldPath);
+ sortingFields[SortByPath].insert(FieldStartOffset);
+ sortingFields[SortByDriveType].insert(FieldDriveType);
+ sortingFields[SortByTitle].insert(FieldTitle);
+ sortingFields[SortByTrackNumber].insert(FieldTrackNumber);
+ sortingFields[SortByTime].insert(FieldTime);
+ sortingFields[SortByArtist].insert(FieldArtist);
+ sortingFields[SortByArtist].insert(FieldArtistSort);
+ sortingFields[SortByArtist].insert(FieldYear);
+ sortingFields[SortByArtist].insert(FieldAlbum);
+ sortingFields[SortByArtist].insert(FieldTrackNumber);
+ sortingFields[SortByArtistThenYear].insert(FieldArtist);
+ sortingFields[SortByArtistThenYear].insert(FieldArtistSort);
+ sortingFields[SortByArtistThenYear].insert(FieldYear);
+ sortingFields[SortByArtistThenYear].insert(FieldOrigDate);
+ sortingFields[SortByArtistThenYear].insert(FieldAlbum);
+ sortingFields[SortByArtistThenYear].insert(FieldTrackNumber);
+ sortingFields[SortByAlbum].insert(FieldAlbum);
+ sortingFields[SortByAlbum].insert(FieldArtist);
+ sortingFields[SortByAlbum].insert(FieldArtistSort);
+ sortingFields[SortByAlbum].insert(FieldTrackNumber);
+ sortingFields[SortByAlbumType].insert(FieldAlbumType);
+ sortingFields[SortByGenre].insert(FieldGenre);
+ sortingFields[SortByCountry].insert(FieldCountry);
+ sortingFields[SortByYear].insert(FieldYear);
+ sortingFields[SortByYear].insert(FieldAirDate);
+ sortingFields[SortByYear].insert(FieldAlbum);
+ sortingFields[SortByYear].insert(FieldTrackNumber);
+ sortingFields[SortByYear].insert(FieldOrigDate);
+ sortingFields[SortByRating].insert(FieldRating);
+ sortingFields[SortByUserRating].insert(FieldUserRating);
+ sortingFields[SortByVotes].insert(FieldVotes);
+ sortingFields[SortByTop250].insert(FieldTop250);
+ sortingFields[SortByProgramCount].insert(FieldProgramCount);
+ sortingFields[SortByPlaylistOrder].insert(FieldProgramCount);
+ sortingFields[SortByEpisodeNumber].insert(FieldEpisodeNumber);
+ sortingFields[SortByEpisodeNumber].insert(FieldSeason);
+ sortingFields[SortByEpisodeNumber].insert(FieldEpisodeNumberSpecialSort);
+ sortingFields[SortByEpisodeNumber].insert(FieldSeasonSpecialSort);
+ sortingFields[SortByEpisodeNumber].insert(FieldTitle);
+ sortingFields[SortByEpisodeNumber].insert(FieldSortTitle);
+ sortingFields[SortBySeason].insert(FieldSeason);
+ sortingFields[SortBySeason].insert(FieldSeasonSpecialSort);
+ sortingFields[SortByNumberOfEpisodes].insert(FieldNumberOfEpisodes);
+ sortingFields[SortByNumberOfWatchedEpisodes].insert(FieldNumberOfWatchedEpisodes);
+ sortingFields[SortByTvShowStatus].insert(FieldTvShowStatus);
+ sortingFields[SortByTvShowTitle].insert(FieldTvShowTitle);
+ sortingFields[SortBySortTitle].insert(FieldSortTitle);
+ sortingFields[SortBySortTitle].insert(FieldTitle);
+ sortingFields[SortByProductionCode].insert(FieldProductionCode);
+ sortingFields[SortByMPAA].insert(FieldMPAA);
+ sortingFields[SortByVideoResolution].insert(FieldVideoResolution);
+ sortingFields[SortByVideoCodec].insert(FieldVideoCodec);
+ sortingFields[SortByVideoAspectRatio].insert(FieldVideoAspectRatio);
+ sortingFields[SortByAudioChannels].insert(FieldAudioChannels);
+ sortingFields[SortByAudioCodec].insert(FieldAudioCodec);
+ sortingFields[SortByAudioLanguage].insert(FieldAudioLanguage);
+ sortingFields[SortBySubtitleLanguage].insert(FieldSubtitleLanguage);
+ sortingFields[SortByStudio].insert(FieldStudio);
+ sortingFields[SortByDateAdded].insert(FieldDateAdded);
+ sortingFields[SortByDateAdded].insert(FieldId);
+ sortingFields[SortByLastPlayed].insert(FieldLastPlayed);
+ sortingFields[SortByPlaycount].insert(FieldPlaycount);
+ sortingFields[SortByListeners].insert(FieldListeners);
+ sortingFields[SortByBitrate].insert(FieldBitrate);
+ sortingFields[SortByChannel].insert(FieldChannelName);
+ sortingFields[SortByChannelNumber].insert(FieldChannelNumber);
+ sortingFields[SortByClientChannelOrder].insert(FieldClientChannelOrder);
+ sortingFields[SortByProvider].insert(FieldProvider);
+ sortingFields[SortByUserPreference].insert(FieldUserPreference);
+ sortingFields[SortByDateTaken].insert(FieldDateTaken);
+ sortingFields[SortByRelevance].insert(FieldRelevance);
+ sortingFields[SortByInstallDate].insert(FieldInstallDate);
+ sortingFields[SortByLastUpdated].insert(FieldLastUpdated);
+ sortingFields[SortByLastUsed].insert(FieldLastUsed);
+ sortingFields[SortByTotalDiscs].insert(FieldTotalDiscs);
+ sortingFields[SortByOrigDate].insert(FieldOrigDate);
+ sortingFields[SortByOrigDate].insert(FieldAlbum);
+ sortingFields[SortByOrigDate].insert(FieldTrackNumber);
+ sortingFields[SortByBPM].insert(FieldBPM);
+ sortingFields[SortByOriginalTitle].insert(FieldOriginalTitle);
+ sortingFields[SortByOriginalTitle].insert(FieldTitle);
+ sortingFields[SortByOriginalTitle].insert(FieldSortTitle);
+ sortingFields.insert(std::pair<SortBy, Fields>(SortByRandom, Fields()));
+
+ return sortingFields;
+}
+
+std::map<SortBy, SortUtils::SortPreparator> SortUtils::m_preparators = fillPreparators();
+std::map<SortBy, Fields> SortUtils::m_sortingFields = fillSortingFields();
+
+void SortUtils::GetFieldsForSQLSort(const MediaType& mediaType,
+ SortBy sortMethod,
+ FieldList& fields)
+{
+ fields.clear();
+ if (mediaType == MediaTypeNone)
+ return;
+
+ if (mediaType == MediaTypeAlbum)
+ {
+ if (sortMethod == SortByLabel || sortMethod == SortByAlbum || sortMethod == SortByTitle)
+ {
+ fields.emplace_back(FieldAlbum);
+ fields.emplace_back(FieldArtist);
+ }
+ else if (sortMethod == SortByAlbumType)
+ {
+ fields.emplace_back(FieldAlbumType);
+ fields.emplace_back(FieldAlbum);
+ fields.emplace_back(FieldArtist);
+ }
+ else if (sortMethod == SortByArtist)
+ {
+ fields.emplace_back(FieldArtist);
+ fields.emplace_back(FieldAlbum);
+ }
+ else if (sortMethod == SortByArtistThenYear)
+ {
+ fields.emplace_back(FieldArtist);
+ fields.emplace_back(FieldYear);
+ fields.emplace_back(FieldAlbum);
+ }
+ else if (sortMethod == SortByYear)
+ {
+ fields.emplace_back(FieldYear);
+ fields.emplace_back(FieldAlbum);
+ }
+ else if (sortMethod == SortByGenre)
+ {
+ fields.emplace_back(FieldGenre);
+ fields.emplace_back(FieldAlbum);
+ }
+ else if (sortMethod == SortByDateAdded)
+ fields.emplace_back(FieldDateAdded);
+ else if (sortMethod == SortByPlaycount)
+ {
+ fields.emplace_back(FieldPlaycount);
+ fields.emplace_back(FieldAlbum);
+ }
+ else if (sortMethod == SortByLastPlayed)
+ {
+ fields.emplace_back(FieldLastPlayed);
+ fields.emplace_back(FieldAlbum);
+ }
+ else if (sortMethod == SortByRating)
+ {
+ fields.emplace_back(FieldRating);
+ fields.emplace_back(FieldAlbum);
+ }
+ else if (sortMethod == SortByVotes)
+ {
+ fields.emplace_back(FieldVotes);
+ fields.emplace_back(FieldAlbum);
+ }
+ else if (sortMethod == SortByUserRating)
+ {
+ fields.emplace_back(FieldUserRating);
+ fields.emplace_back(FieldAlbum);
+ }
+ else if (sortMethod == SortByTotalDiscs)
+ {
+ fields.emplace_back(FieldTotalDiscs);
+ fields.emplace_back(FieldAlbum);
+ }
+ else if (sortMethod == SortByOrigDate)
+ {
+ fields.emplace_back(FieldOrigDate);
+ fields.emplace_back(FieldAlbum);
+ }
+ }
+ else if (mediaType == MediaTypeSong)
+ {
+ if (sortMethod == SortByLabel || sortMethod == SortByTrackNumber)
+ fields.emplace_back(FieldTrackNumber);
+ else if (sortMethod == SortByTitle)
+ fields.emplace_back(FieldTitle);
+ else if (sortMethod == SortByAlbum)
+ {
+ fields.emplace_back(FieldAlbum);
+ fields.emplace_back(FieldAlbumArtist);
+ fields.emplace_back(FieldTrackNumber);
+ }
+ else if (sortMethod == SortByArtist)
+ {
+ fields.emplace_back(FieldArtist);
+ fields.emplace_back(FieldAlbum);
+ fields.emplace_back(FieldTrackNumber);
+ }
+ else if (sortMethod == SortByArtistThenYear)
+ {
+ fields.emplace_back(FieldArtist);
+ fields.emplace_back(FieldYear);
+ fields.emplace_back(FieldAlbum);
+ fields.emplace_back(FieldTrackNumber);
+ }
+ else if (sortMethod == SortByYear)
+ {
+ fields.emplace_back(FieldYear);
+ fields.emplace_back(FieldAlbum);
+ fields.emplace_back(FieldTrackNumber);
+ }
+ else if (sortMethod == SortByGenre)
+ {
+ fields.emplace_back(FieldGenre);
+ fields.emplace_back(FieldAlbum);
+ }
+ else if (sortMethod == SortByDateAdded)
+ fields.emplace_back(FieldDateAdded);
+ else if (sortMethod == SortByPlaycount)
+ {
+ fields.emplace_back(FieldPlaycount);
+ fields.emplace_back(FieldTrackNumber);
+ }
+ else if (sortMethod == SortByLastPlayed)
+ {
+ fields.emplace_back(FieldLastPlayed);
+ fields.emplace_back(FieldTrackNumber);
+ }
+ else if (sortMethod == SortByRating)
+ {
+ fields.emplace_back(FieldRating);
+ fields.emplace_back(FieldTrackNumber);
+ }
+ else if (sortMethod == SortByVotes)
+ {
+ fields.emplace_back(FieldVotes);
+ fields.emplace_back(FieldTrackNumber);
+ }
+ else if (sortMethod == SortByUserRating)
+ {
+ fields.emplace_back(FieldUserRating);
+ fields.emplace_back(FieldTrackNumber);
+ }
+ else if (sortMethod == SortByFile)
+ {
+ fields.emplace_back(FieldPath);
+ fields.emplace_back(FieldFilename);
+ fields.emplace_back(FieldStartOffset);
+ }
+ else if (sortMethod == SortByTime)
+ fields.emplace_back(FieldTime);
+ else if (sortMethod == SortByAlbumType)
+ {
+ fields.emplace_back(FieldAlbumType);
+ fields.emplace_back(FieldAlbum);
+ fields.emplace_back(FieldTrackNumber);
+ }
+ else if (sortMethod == SortByOrigDate)
+ {
+ fields.emplace_back(FieldOrigDate);
+ fields.emplace_back(FieldAlbum);
+ fields.emplace_back(FieldTrackNumber);
+ }
+ else if (sortMethod == SortByBPM)
+ fields.emplace_back(FieldBPM);
+ }
+ else if (mediaType == MediaTypeArtist)
+ {
+ if (sortMethod == SortByLabel || sortMethod == SortByTitle || sortMethod == SortByArtist)
+ fields.emplace_back(FieldArtist);
+ else if (sortMethod == SortByGenre)
+ fields.emplace_back(FieldGenre);
+ else if (sortMethod == SortByDateAdded)
+ fields.emplace_back(FieldDateAdded);
+ }
+
+ // Add sort by id to define order when other fields same or sort none
+ fields.emplace_back(FieldId);
+ return;
+}
+
+
+void SortUtils::Sort(SortBy sortBy, SortOrder sortOrder, SortAttribute attributes, DatabaseResults& items, int limitEnd /* = -1 */, int limitStart /* = 0 */)
+{
+ if (sortBy != SortByNone)
+ {
+ // get the matching SortPreparator
+ SortPreparator preparator = getPreparator(sortBy);
+ if (preparator != NULL)
+ {
+ Fields sortingFields = GetFieldsForSorting(sortBy);
+
+ // Prepare the string used for sorting and store it under FieldSort
+ for (DatabaseResults::iterator item = items.begin(); item != items.end(); ++item)
+ {
+ // add all fields to the item that are required for sorting if they are currently missing
+ for (Fields::const_iterator field = sortingFields.begin(); field != sortingFields.end(); ++field)
+ {
+ if (item->find(*field) == item->end())
+ item->insert(std::pair<Field, CVariant>(*field, CVariant::ConstNullVariant));
+ }
+
+ std::wstring sortLabel;
+ g_charsetConverter.utf8ToW(preparator(attributes, *item), sortLabel, false);
+ item->insert(std::pair<Field, CVariant>(FieldSort, CVariant(sortLabel)));
+ }
+
+ // Do the sorting
+ std::stable_sort(items.begin(), items.end(), getSorter(sortOrder, attributes));
+ }
+ }
+
+ if (limitStart > 0 && (size_t)limitStart < items.size())
+ {
+ items.erase(items.begin(), items.begin() + limitStart);
+ limitEnd -= limitStart;
+ }
+ if (limitEnd > 0 && (size_t)limitEnd < items.size())
+ items.erase(items.begin() + limitEnd, items.end());
+}
+
+void SortUtils::Sort(SortBy sortBy, SortOrder sortOrder, SortAttribute attributes, SortItems& items, int limitEnd /* = -1 */, int limitStart /* = 0 */)
+{
+ if (sortBy != SortByNone)
+ {
+ // get the matching SortPreparator
+ SortPreparator preparator = getPreparator(sortBy);
+ if (preparator != NULL)
+ {
+ Fields sortingFields = GetFieldsForSorting(sortBy);
+
+ // Prepare the string used for sorting and store it under FieldSort
+ for (SortItems::iterator item = items.begin(); item != items.end(); ++item)
+ {
+ // add all fields to the item that are required for sorting if they are currently missing
+ for (Fields::const_iterator field = sortingFields.begin(); field != sortingFields.end(); ++field)
+ {
+ if ((*item)->find(*field) == (*item)->end())
+ (*item)->insert(std::pair<Field, CVariant>(*field, CVariant::ConstNullVariant));
+ }
+
+ std::wstring sortLabel;
+ g_charsetConverter.utf8ToW(preparator(attributes, **item), sortLabel, false);
+ (*item)->insert(std::pair<Field, CVariant>(FieldSort, CVariant(sortLabel)));
+ }
+
+ // Do the sorting
+ std::stable_sort(items.begin(), items.end(), getSorterIndirect(sortOrder, attributes));
+ }
+ }
+
+ if (limitStart > 0 && (size_t)limitStart < items.size())
+ {
+ items.erase(items.begin(), items.begin() + limitStart);
+ limitEnd -= limitStart;
+ }
+ if (limitEnd > 0 && (size_t)limitEnd < items.size())
+ items.erase(items.begin() + limitEnd, items.end());
+}
+
+void SortUtils::Sort(const SortDescription &sortDescription, DatabaseResults& items)
+{
+ Sort(sortDescription.sortBy, sortDescription.sortOrder, sortDescription.sortAttributes, items, sortDescription.limitEnd, sortDescription.limitStart);
+}
+
+void SortUtils::Sort(const SortDescription &sortDescription, SortItems& items)
+{
+ Sort(sortDescription.sortBy, sortDescription.sortOrder, sortDescription.sortAttributes, items, sortDescription.limitEnd, sortDescription.limitStart);
+}
+
+bool SortUtils::SortFromDataset(const SortDescription &sortDescription, const MediaType &mediaType, const std::unique_ptr<dbiplus::Dataset> &dataset, DatabaseResults &results)
+{
+ FieldList fields;
+ if (!DatabaseUtils::GetSelectFields(SortUtils::GetFieldsForSorting(sortDescription.sortBy), mediaType, fields))
+ fields.clear();
+
+ if (!DatabaseUtils::GetDatabaseResults(mediaType, fields, dataset, results))
+ return false;
+
+ SortDescription sorting = sortDescription;
+ if (sortDescription.sortBy == SortByNone)
+ {
+ sorting.limitStart = 0;
+ sorting.limitEnd = -1;
+ }
+
+ Sort(sorting, results);
+
+ return true;
+}
+
+const SortUtils::SortPreparator& SortUtils::getPreparator(SortBy sortBy)
+{
+ std::map<SortBy, SortPreparator>::const_iterator it = m_preparators.find(sortBy);
+ if (it != m_preparators.end())
+ return it->second;
+
+ return m_preparators[SortByNone];
+}
+
+SortUtils::Sorter SortUtils::getSorter(SortOrder sortOrder, SortAttribute attributes)
+{
+ if (attributes & SortAttributeIgnoreFolders)
+ return sortOrder == SortOrderDescending ? SorterIgnoreFoldersDescending : SorterIgnoreFoldersAscending;
+
+ return sortOrder == SortOrderDescending ? SorterDescending : SorterAscending;
+}
+
+SortUtils::SorterIndirect SortUtils::getSorterIndirect(SortOrder sortOrder, SortAttribute attributes)
+{
+ if (attributes & SortAttributeIgnoreFolders)
+ return sortOrder == SortOrderDescending ? SorterIndirectIgnoreFoldersDescending : SorterIndirectIgnoreFoldersAscending;
+
+ return sortOrder == SortOrderDescending ? SorterIndirectDescending : SorterIndirectAscending;
+}
+
+const Fields& SortUtils::GetFieldsForSorting(SortBy sortBy)
+{
+ std::map<SortBy, Fields>::const_iterator it = m_sortingFields.find(sortBy);
+ if (it != m_sortingFields.end())
+ return it->second;
+
+ return m_sortingFields[SortByNone];
+}
+
+std::string SortUtils::RemoveArticles(const std::string &label)
+{
+ std::set<std::string> sortTokens = g_langInfo.GetSortTokens();
+ for (std::set<std::string>::const_iterator token = sortTokens.begin(); token != sortTokens.end(); ++token)
+ {
+ if (token->size() < label.size() && StringUtils::StartsWithNoCase(label, *token))
+ return label.substr(token->size());
+ }
+
+ return label;
+}
+
+typedef struct
+{
+ SortBy sort;
+ SORT_METHOD old;
+ SortAttribute flags;
+ int label;
+} sort_map;
+
+// clang-format off
+const sort_map table[] = {
+ { SortByLabel, SORT_METHOD_LABEL, SortAttributeNone, 551 },
+ { SortByLabel, SORT_METHOD_LABEL_IGNORE_THE, SortAttributeIgnoreArticle, 551 },
+ { SortByLabel, SORT_METHOD_LABEL_IGNORE_FOLDERS, SortAttributeIgnoreFolders, 551 },
+ { SortByDate, SORT_METHOD_DATE, SortAttributeNone, 552 },
+ { SortBySize, SORT_METHOD_SIZE, SortAttributeNone, 553 },
+ { SortByBitrate, SORT_METHOD_BITRATE, SortAttributeNone, 623 },
+ { SortByDriveType, SORT_METHOD_DRIVE_TYPE, SortAttributeNone, 564 },
+ { SortByTrackNumber, SORT_METHOD_TRACKNUM, SortAttributeNone, 554 },
+ { SortByEpisodeNumber, SORT_METHOD_EPISODE, SortAttributeNone, 20359 },// 20360 "Episodes" used for SORT_METHOD_EPISODE for sorting tvshows by episode count
+ { SortByTime, SORT_METHOD_DURATION, SortAttributeNone, 180 },
+ { SortByTime, SORT_METHOD_VIDEO_RUNTIME, SortAttributeNone, 180 },
+ { SortByTitle, SORT_METHOD_TITLE, SortAttributeNone, 556 },
+ { SortByTitle, SORT_METHOD_TITLE_IGNORE_THE, SortAttributeIgnoreArticle, 556 },
+ { SortByTitle, SORT_METHOD_VIDEO_TITLE, SortAttributeNone, 556 },
+ { SortByArtist, SORT_METHOD_ARTIST, SortAttributeNone, 557 },
+ { SortByArtistThenYear, SORT_METHOD_ARTIST_AND_YEAR, SortAttributeNone, 578 },
+ { SortByArtist, SORT_METHOD_ARTIST_IGNORE_THE, SortAttributeIgnoreArticle, 557 },
+ { SortByAlbum, SORT_METHOD_ALBUM, SortAttributeNone, 558 },
+ { SortByAlbum, SORT_METHOD_ALBUM_IGNORE_THE, SortAttributeIgnoreArticle, 558 },
+ { SortByGenre, SORT_METHOD_GENRE, SortAttributeNone, 515 },
+ { SortByCountry, SORT_METHOD_COUNTRY, SortAttributeNone, 574 },
+ { SortByDateAdded, SORT_METHOD_DATEADDED, SortAttributeIgnoreFolders, 570 },
+ { SortByFile, SORT_METHOD_FILE, SortAttributeIgnoreFolders, 561 },
+ { SortByRating, SORT_METHOD_SONG_RATING, SortAttributeNone, 563 },
+ { SortByRating, SORT_METHOD_VIDEO_RATING, SortAttributeIgnoreFolders, 563 },
+ { SortByUserRating, SORT_METHOD_SONG_USER_RATING, SortAttributeIgnoreFolders, 38018 },
+ { SortByUserRating, SORT_METHOD_VIDEO_USER_RATING, SortAttributeIgnoreFolders, 38018 },
+ { SortBySortTitle, SORT_METHOD_VIDEO_SORT_TITLE, SortAttributeIgnoreFolders, 171 },
+ { SortBySortTitle, SORT_METHOD_VIDEO_SORT_TITLE_IGNORE_THE, (SortAttribute)(SortAttributeIgnoreFolders | SortAttributeIgnoreArticle), 171 },
+ { SortByOriginalTitle, SORT_METHOD_VIDEO_ORIGINAL_TITLE, SortAttributeIgnoreFolders, 20376 },
+ { SortByOriginalTitle, SORT_METHOD_VIDEO_ORIGINAL_TITLE_IGNORE_THE, (SortAttribute)(SortAttributeIgnoreFolders | SortAttributeIgnoreArticle), 20376 },
+ { SortByYear, SORT_METHOD_YEAR, SortAttributeIgnoreFolders, 562 },
+ { SortByProductionCode, SORT_METHOD_PRODUCTIONCODE, SortAttributeNone, 20368 },
+ { SortByProgramCount, SORT_METHOD_PROGRAM_COUNT, SortAttributeNone, 567 }, // label is "play count"
+ { SortByPlaylistOrder, SORT_METHOD_PLAYLIST_ORDER, SortAttributeIgnoreFolders, 559 },
+ { SortByMPAA, SORT_METHOD_MPAA_RATING, SortAttributeNone, 20074 },
+ { SortByStudio, SORT_METHOD_STUDIO, SortAttributeNone, 572 },
+ { SortByStudio, SORT_METHOD_STUDIO_IGNORE_THE, SortAttributeIgnoreArticle, 572 },
+ { SortByPath, SORT_METHOD_FULLPATH, SortAttributeNone, 573 },
+ { SortByLastPlayed, SORT_METHOD_LASTPLAYED, SortAttributeIgnoreFolders, 568 },
+ { SortByPlaycount, SORT_METHOD_PLAYCOUNT, SortAttributeIgnoreFolders, 567 },
+ { SortByListeners, SORT_METHOD_LISTENERS, SortAttributeNone, 20455 },
+ { SortByChannel, SORT_METHOD_CHANNEL, SortAttributeNone, 19029 },
+ { SortByChannel, SORT_METHOD_CHANNEL_NUMBER, SortAttributeNone, 549 },
+ { SortByChannel, SORT_METHOD_CLIENT_CHANNEL_ORDER, SortAttributeNone, 19315 },
+ { SortByProvider, SORT_METHOD_PROVIDER, SortAttributeNone, 19348 },
+ { SortByUserPreference, SORT_METHOD_USER_PREFERENCE, SortAttributeNone, 19349 },
+ { SortByDateTaken, SORT_METHOD_DATE_TAKEN, SortAttributeIgnoreFolders, 577 },
+ { SortByNone, SORT_METHOD_NONE, SortAttributeNone, 16018 },
+ { SortByTotalDiscs, SORT_METHOD_TOTAL_DISCS, SortAttributeNone, 38077 },
+ { SortByOrigDate, SORT_METHOD_ORIG_DATE, SortAttributeNone, 38079 },
+ { SortByBPM, SORT_METHOD_BPM, SortAttributeNone, 38080 },
+
+ // the following have no corresponding SORT_METHOD_*
+ { SortByAlbumType, SORT_METHOD_NONE, SortAttributeNone, 564 },
+ { SortByVotes, SORT_METHOD_NONE, SortAttributeNone, 205 },
+ { SortByTop250, SORT_METHOD_NONE, SortAttributeNone, 13409 },
+ { SortByMPAA, SORT_METHOD_NONE, SortAttributeNone, 20074 },
+ { SortByDateAdded, SORT_METHOD_NONE, SortAttributeNone, 570 },
+ { SortByTvShowTitle, SORT_METHOD_NONE, SortAttributeNone, 20364 },
+ { SortByTvShowStatus, SORT_METHOD_NONE, SortAttributeNone, 126 },
+ { SortBySeason, SORT_METHOD_NONE, SortAttributeNone, 20373 },
+ { SortByNumberOfEpisodes, SORT_METHOD_NONE, SortAttributeNone, 20360 },
+ { SortByNumberOfWatchedEpisodes, SORT_METHOD_NONE, SortAttributeNone, 21441 },
+ { SortByVideoResolution, SORT_METHOD_NONE, SortAttributeNone, 21443 },
+ { SortByVideoCodec, SORT_METHOD_NONE, SortAttributeNone, 21445 },
+ { SortByVideoAspectRatio, SORT_METHOD_NONE, SortAttributeNone, 21374 },
+ { SortByAudioChannels, SORT_METHOD_NONE, SortAttributeNone, 21444 },
+ { SortByAudioCodec, SORT_METHOD_NONE, SortAttributeNone, 21446 },
+ { SortByAudioLanguage, SORT_METHOD_NONE, SortAttributeNone, 21447 },
+ { SortBySubtitleLanguage, SORT_METHOD_NONE, SortAttributeNone, 21448 },
+ { SortByRandom, SORT_METHOD_NONE, SortAttributeNone, 590 }
+};
+// clang-format on
+
+SORT_METHOD SortUtils::TranslateOldSortMethod(SortBy sortBy, bool ignoreArticle)
+{
+ for (const sort_map& t : table)
+ {
+ if (t.sort == sortBy)
+ {
+ if (ignoreArticle == ((t.flags & SortAttributeIgnoreArticle) == SortAttributeIgnoreArticle))
+ return t.old;
+ }
+ }
+ for (const sort_map& t : table)
+ {
+ if (t.sort == sortBy)
+ return t.old;
+ }
+ return SORT_METHOD_NONE;
+}
+
+SortDescription SortUtils::TranslateOldSortMethod(SORT_METHOD sortBy)
+{
+ SortDescription description;
+ for (const sort_map& t : table)
+ {
+ if (t.old == sortBy)
+ {
+ description.sortBy = t.sort;
+ description.sortAttributes = t.flags;
+ break;
+ }
+ }
+ return description;
+}
+
+int SortUtils::GetSortLabel(SortBy sortBy)
+{
+ for (const sort_map& t : table)
+ {
+ if (t.sort == sortBy)
+ return t.label;
+ }
+ return 16018; // None
+}
+
+template<typename T>
+T TypeFromString(const std::map<std::string, T>& typeMap, const std::string& name, const T& defaultType)
+{
+ auto it = typeMap.find(name);
+ if (it == typeMap.end())
+ return defaultType;
+
+ return it->second;
+}
+
+template<typename T>
+const std::string& TypeToString(const std::map<std::string, T>& typeMap, const T& value)
+{
+ auto it = std::find_if(typeMap.begin(), typeMap.end(),
+ [&value](const std::pair<std::string, T>& pair)
+ {
+ return pair.second == value;
+ });
+
+ if (it == typeMap.end())
+ return StringUtils::Empty;
+
+ return it->first;
+}
+
+/**
+ * @brief Sort methods to translate string values to enum values.
+ *
+ * @warning On string changes, edit __SortBy__ enumerator to have strings right
+ * for documentation!
+ */
+const std::map<std::string, SortBy> sortMethods = {
+ {"label", SortByLabel},
+ {"date", SortByDate},
+ {"size", SortBySize},
+ {"file", SortByFile},
+ {"path", SortByPath},
+ {"drivetype", SortByDriveType},
+ {"title", SortByTitle},
+ {"track", SortByTrackNumber},
+ {"time", SortByTime},
+ {"artist", SortByArtist},
+ {"artistyear", SortByArtistThenYear},
+ {"album", SortByAlbum},
+ {"albumtype", SortByAlbumType},
+ {"genre", SortByGenre},
+ {"country", SortByCountry},
+ {"year", SortByYear},
+ {"rating", SortByRating},
+ {"votes", SortByVotes},
+ {"top250", SortByTop250},
+ {"programcount", SortByProgramCount},
+ {"playlist", SortByPlaylistOrder},
+ {"episode", SortByEpisodeNumber},
+ {"season", SortBySeason},
+ {"totalepisodes", SortByNumberOfEpisodes},
+ {"watchedepisodes", SortByNumberOfWatchedEpisodes},
+ {"tvshowstatus", SortByTvShowStatus},
+ {"tvshowtitle", SortByTvShowTitle},
+ {"sorttitle", SortBySortTitle},
+ {"productioncode", SortByProductionCode},
+ {"mpaa", SortByMPAA},
+ {"videoresolution", SortByVideoResolution},
+ {"videocodec", SortByVideoCodec},
+ {"videoaspectratio", SortByVideoAspectRatio},
+ {"audiochannels", SortByAudioChannels},
+ {"audiocodec", SortByAudioCodec},
+ {"audiolanguage", SortByAudioLanguage},
+ {"subtitlelanguage", SortBySubtitleLanguage},
+ {"studio", SortByStudio},
+ {"dateadded", SortByDateAdded},
+ {"lastplayed", SortByLastPlayed},
+ {"playcount", SortByPlaycount},
+ {"listeners", SortByListeners},
+ {"bitrate", SortByBitrate},
+ {"random", SortByRandom},
+ {"channel", SortByChannel},
+ {"channelnumber", SortByChannelNumber},
+ {"clientchannelorder", SortByClientChannelOrder},
+ {"provider", SortByProvider},
+ {"userpreference", SortByUserPreference},
+ {"datetaken", SortByDateTaken},
+ {"userrating", SortByUserRating},
+ {"installdate", SortByInstallDate},
+ {"lastupdated", SortByLastUpdated},
+ {"lastused", SortByLastUsed},
+ {"totaldiscs", SortByTotalDiscs},
+ {"originaldate", SortByOrigDate},
+ {"bpm", SortByBPM},
+ {"originaltitle", SortByOriginalTitle},
+};
+
+SortBy SortUtils::SortMethodFromString(const std::string& sortMethod)
+{
+ return TypeFromString<SortBy>(sortMethods, sortMethod, SortByNone);
+}
+
+const std::string& SortUtils::SortMethodToString(SortBy sortMethod)
+{
+ return TypeToString<SortBy>(sortMethods, sortMethod);
+}
+
+const std::map<std::string, SortOrder> sortOrders = {
+ { "ascending", SortOrderAscending },
+ { "descending", SortOrderDescending }
+};
+
+SortOrder SortUtils::SortOrderFromString(const std::string& sortOrder)
+{
+ return TypeFromString<SortOrder>(sortOrders, sortOrder, SortOrderNone);
+}
+
+const std::string& SortUtils::SortOrderToString(SortOrder sortOrder)
+{
+ return TypeToString<SortOrder>(sortOrders, sortOrder);
+}
diff --git a/xbmc/utils/SortUtils.h b/xbmc/utils/SortUtils.h
new file mode 100644
index 0000000..19a7d07
--- /dev/null
+++ b/xbmc/utils/SortUtils.h
@@ -0,0 +1,233 @@
+/*
+ * 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 "DatabaseUtils.h"
+#include "LabelFormatter.h"
+#include "SortFileItem.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+typedef enum {
+ SortOrderNone = 0,
+ SortOrderAscending,
+ SortOrderDescending
+} SortOrder;
+
+typedef enum {
+ SortAttributeNone = 0x0,
+ SortAttributeIgnoreArticle = 0x1,
+ SortAttributeIgnoreFolders = 0x2,
+ SortAttributeUseArtistSortName = 0x4,
+ SortAttributeIgnoreLabel = 0x8
+} SortAttribute;
+
+typedef enum {
+ SortSpecialNone = 0,
+ SortSpecialOnTop = 1,
+ SortSpecialOnBottom = 2
+} SortSpecial;
+
+///
+/// \defgroup List_of_sort_methods List of sort methods
+/// \addtogroup List_of_sort_methods
+///
+/// \brief These ID's can be used with the \ref built_in_functions_6 "Container.SetSortMethod(id)" function
+/// \note The on field named part with <b>String</b> shows the string used on
+/// GUI to set this sort type.
+///
+///@{
+typedef enum
+{
+ /// __0__ :
+ SortByNone = 0,
+ /// __1__ : Sort by Name <em>(String: <b><c>Label</c></b>)</em>
+ SortByLabel,
+ /// __2__ : Sort by Date <em>(String: <b><c>Date</c></b>)</em>
+ SortByDate,
+ /// __3__ : Sort by Size <em>(String: <b><c>Size</c></b>)</em>
+ SortBySize,
+ /// __4__ : Sort by filename <em>(String: <b><c>File</c></b>)</em>
+ SortByFile,
+ /// __5__ : Sort by path <em>(String: <b><c>Path</c></b>)</em>
+ SortByPath,
+ /// __6__ : Sort by drive type <em>(String: <b><c>DriveType</c></b>)</em>
+ SortByDriveType,
+ /// __7__ : Sort by title <em>(String: <b><c>Title</c></b>)</em>
+ SortByTitle,
+ /// __8__ : Sort by track number <em>(String: <b><c>TrackNumber</c></b>)</em>
+ SortByTrackNumber,
+ /// __9__ : Sort by time <em>(String: <b><c>Time</c></b>)</em>
+ SortByTime,
+ /// __10__ : Sort by artist <em>(String: <b><c>Artist</c></b>)</em>
+ SortByArtist,
+ /// __11__ : Sort by first artist then year <em>(String: <b><c>ArtistYear</c></b>)</em>
+ SortByArtistThenYear,
+ /// __12__ : Sort by album <em>(String: <b><c>Album</c></b>)</em>
+ SortByAlbum,
+ /// __13__ : Sort by album type <em>(String: <b><c>AlbumType</c></b>)</em>
+ SortByAlbumType,
+ /// __14__ : Sort by genre <em>(String: <b><c>Genre</c></b>)</em>
+ SortByGenre,
+ /// __15__ : Sort by country <em>(String: <b><c>Country</c></b>)</em>
+ SortByCountry,
+ /// __16__ : Sort by year <em>(String: <b><c>Year</c></b>)</em>
+ SortByYear,
+ /// __17__ : Sort by rating <em>(String: <b><c>Rating</c></b>)</em>
+ SortByRating,
+ /// __18__ : Sort by user rating <em>(String: <b><c>UserRating</c></b>)</em>
+ SortByUserRating,
+ /// __19__ : Sort by votes <em>(String: <b><c>Votes</c></b>)</em>
+ SortByVotes,
+ /// __20__ : Sort by top 250 <em>(String: <b><c>Top250</c></b>)</em>
+ SortByTop250,
+ /// __21__ : Sort by program count <em>(String: <b><c>ProgramCount</c></b>)</em>
+ SortByProgramCount,
+ /// __22__ : Sort by playlist order <em>(String: <b><c>Playlist</c></b>)</em>
+ SortByPlaylistOrder,
+ /// __23__ : Sort by episode number <em>(String: <b><c>Episode</c></b>)</em>
+ SortByEpisodeNumber,
+ /// __24__ : Sort by season <em>(String: <b><c>Season</c></b>)</em>
+ SortBySeason,
+ /// __25__ : Sort by number of episodes <em>(String: <b><c>TotalEpisodes</c></b>)</em>
+ SortByNumberOfEpisodes,
+ /// __26__ : Sort by number of watched episodes <em>(String: <b><c>WatchedEpisodes</c></b>)</em>
+ SortByNumberOfWatchedEpisodes,
+ /// __27__ : Sort by TV show status <em>(String: <b><c>TvShowStatus</c></b>)</em>
+ SortByTvShowStatus,
+ /// __28__ : Sort by TV show title <em>(String: <b><c>TvShowTitle</c></b>)</em>
+ SortByTvShowTitle,
+ /// __29__ : Sort by sort title <em>(String: <b><c>SortTitle</c></b>)</em>
+ SortBySortTitle,
+ /// __30__ : Sort by production code <em>(String: <b><c>ProductionCode</c></b>)</em>
+ SortByProductionCode,
+ /// __31__ : Sort by MPAA <em>(String: <b><c>MPAA</c></b>)</em>
+ SortByMPAA,
+ /// __32__ : Sort by video resolution <em>(String: <b><c>VideoResolution</c></b>)</em>
+ SortByVideoResolution,
+ /// __33__ : Sort by video codec <em>(String: <b><c>VideoCodec</c></b>)</em>
+ SortByVideoCodec,
+ /// __34__ : Sort by video aspect ratio <em>(String: <b><c>VideoAspectRatio</c></b>)</em>
+ SortByVideoAspectRatio,
+ /// __35__ : Sort by audio channels <em>(String: <b><c>AudioChannels</c></b>)</em>
+ SortByAudioChannels,
+ /// __36__ : Sort by audio codec <em>(String: <b><c>AudioCodec</c></b>)</em>
+ SortByAudioCodec,
+ /// __37__ : Sort by audio language <em>(String: <b><c>AudioLanguage</c></b>)</em>
+ SortByAudioLanguage,
+ /// __38__ : Sort by subtitle language <em>(String: <b><c>SubtitleLanguage</c></b>)</em>
+ SortBySubtitleLanguage,
+ /// __39__ : Sort by studio <em>(String: <b><c>Studio</c></b>)</em>
+ SortByStudio,
+ /// __40__ : Sort by date added <em>(String: <b><c>DateAdded</c></b>)</em>
+ SortByDateAdded,
+ /// __41__ : Sort by last played <em>(String: <b><c>LastPlayed</c></b>)</em>
+ SortByLastPlayed,
+ /// __42__ : Sort by playcount <em>(String: <b><c>PlayCount</c></b>)</em>
+ SortByPlaycount,
+ /// __43__ : Sort by listener <em>(String: <b><c>Listeners</c></b>)</em>
+ SortByListeners,
+ /// __44__ : Sort by bitrate <em>(String: <b><c>Bitrate</c></b>)</em>
+ SortByBitrate,
+ /// __45__ : Sort by random <em>(String: <b><c>Random</c></b>)</em>
+ SortByRandom,
+ /// __46__ : Sort by channel <em>(String: <b><c>Channel</c></b>)</em>
+ SortByChannel,
+ /// __47__ : Sort by channel number <em>(String: <b><c>ChannelNumber</c></b>)</em>
+ SortByChannelNumber,
+ /// __48__ : Sort by date taken <em>(String: <b><c>DateTaken</c></b>)</em>
+ SortByDateTaken,
+ /// __49__ : Sort by relevance
+ SortByRelevance,
+ /// __50__ : Sort by installation date <en>(String: <b><c>installdate</c></b>)</em>
+ SortByInstallDate,
+ /// __51__ : Sort by last updated <en>(String: <b><c>lastupdated</c></b>)</em>
+ SortByLastUpdated,
+ /// __52__ : Sort by last used <em>(String: <b><c>lastused</c></b>)</em>
+ SortByLastUsed,
+ /// __53__ : Sort by client channel order <em>(String: <b><c>ClientChannelOrder</c></b>)</em>
+ SortByClientChannelOrder,
+ /// __54__ : Sort by total number of discs <em>(String: <b><c>totaldiscs</c></b>)</em>
+ SortByTotalDiscs,
+ /// __55__ : Sort by original release date <em>(String: <b><c>Originaldate</c></b>)</em>
+ SortByOrigDate,
+ /// __56__ : Sort by BPM <em>(String: <b><c>bpm</c></b>)</em>
+ SortByBPM,
+ /// __57__ : Sort by original title <em>(String: <b><c>OriginalTitle</c></b>)</em>
+ SortByOriginalTitle,
+ /// __58__ : Sort by provider <em>(String: <b><c>Provider</c></b>)</em>
+ /// @skinning_v20 <b>SortByProvider</b> New sort method added.
+ SortByProvider,
+ /// __59__ : Sort by user preference <em>(String: <b><c>UserPreference</c></b>)</em>
+ /// @skinning_v20 <b>SortByUserPreference</b> New sort method added.
+ SortByUserPreference,
+} SortBy;
+///@}
+
+typedef struct SortDescription {
+ SortBy sortBy = SortByNone;
+ SortOrder sortOrder = SortOrderAscending;
+ SortAttribute sortAttributes = SortAttributeNone;
+ int limitStart = 0;
+ int limitEnd = -1;
+} SortDescription;
+
+typedef struct GUIViewSortDetails
+{
+ SortDescription m_sortDescription;
+ int m_buttonLabel;
+ LABEL_MASKS m_labelMasks;
+} GUIViewSortDetails;
+
+typedef DatabaseResult SortItem;
+typedef std::shared_ptr<SortItem> SortItemPtr;
+typedef std::vector<SortItemPtr> SortItems;
+
+class SortUtils
+{
+public:
+ static SORT_METHOD TranslateOldSortMethod(SortBy sortBy, bool ignoreArticle);
+ static SortDescription TranslateOldSortMethod(SORT_METHOD sortBy);
+
+ static SortBy SortMethodFromString(const std::string& sortMethod);
+ static const std::string& SortMethodToString(SortBy sortMethod);
+ static SortOrder SortOrderFromString(const std::string& sortOrder);
+ static const std::string& SortOrderToString(SortOrder sortOrder);
+
+ /*! \brief retrieve the label id associated with a sort method for displaying in the UI.
+ \param sortBy the sort method in question.
+ \return the label id of the sort method.
+ */
+ static int GetSortLabel(SortBy sortBy);
+
+ static void Sort(SortBy sortBy, SortOrder sortOrder, SortAttribute attributes, DatabaseResults& items, int limitEnd = -1, int limitStart = 0);
+ static void Sort(SortBy sortBy, SortOrder sortOrder, SortAttribute attributes, SortItems& items, int limitEnd = -1, int limitStart = 0);
+ static void Sort(const SortDescription &sortDescription, DatabaseResults& items);
+ static void Sort(const SortDescription &sortDescription, SortItems& items);
+ static bool SortFromDataset(const SortDescription &sortDescription, const MediaType &mediaType, const std::unique_ptr<dbiplus::Dataset> &dataset, DatabaseResults &results);
+
+ static void GetFieldsForSQLSort(const MediaType& mediaType, SortBy sortMethod, FieldList& fields);
+ static const Fields& GetFieldsForSorting(SortBy sortBy);
+ static std::string RemoveArticles(const std::string &label);
+
+ typedef std::string (*SortPreparator) (SortAttribute, const SortItem&);
+ typedef bool (*Sorter) (const DatabaseResult &, const DatabaseResult &);
+ typedef bool (*SorterIndirect) (const SortItemPtr &, const SortItemPtr &);
+
+private:
+ static const SortPreparator& getPreparator(SortBy sortBy);
+ static Sorter getSorter(SortOrder sortOrder, SortAttribute attributes);
+ static SorterIndirect getSorterIndirect(SortOrder sortOrder, SortAttribute attributes);
+
+ static std::map<SortBy, SortPreparator> m_preparators;
+ static std::map<SortBy, Fields> m_sortingFields;
+};
diff --git a/xbmc/utils/Speed.cpp b/xbmc/utils/Speed.cpp
new file mode 100644
index 0000000..d1326c7
--- /dev/null
+++ b/xbmc/utils/Speed.cpp
@@ -0,0 +1,582 @@
+/*
+ * 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 "Speed.h"
+
+#include "utils/Archive.h"
+#include "utils/StringUtils.h"
+
+#include <assert.h>
+
+CSpeed::CSpeed()
+{
+ m_value = 0.0;
+ m_valid = false;
+}
+
+CSpeed::CSpeed(const CSpeed& speed)
+{
+ m_value = speed.m_value;
+ m_valid = speed.m_valid;
+}
+
+CSpeed::CSpeed(double value)
+{
+ m_value = value;
+ m_valid = true;
+}
+
+bool CSpeed::operator >(const CSpeed& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ if (!IsValid() || !right.IsValid())
+ return false;
+
+ if (this == &right)
+ return false;
+
+ return (m_value > right.m_value);
+}
+
+bool CSpeed::operator >=(const CSpeed& right) const
+{
+ return operator >(right) || operator ==(right);
+}
+
+bool CSpeed::operator <(const CSpeed& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ if (!IsValid() || !right.IsValid())
+ return false;
+
+ if (this == &right)
+ return false;
+
+ return (m_value < right.m_value);
+}
+
+bool CSpeed::operator <=(const CSpeed& right) const
+{
+ return operator <(right) || operator ==(right);
+}
+
+bool CSpeed::operator ==(const CSpeed& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ if (!IsValid() || !right.IsValid())
+ return false;
+
+ if (this == &right)
+ return true;
+
+ return (m_value == right.m_value);
+}
+
+bool CSpeed::operator !=(const CSpeed& right) const
+{
+ return !operator ==(right.m_value);
+}
+
+CSpeed& CSpeed::operator =(const CSpeed& right)
+{
+ m_valid = right.m_valid;
+ m_value = right.m_value;
+ return *this;
+}
+
+const CSpeed& CSpeed::operator +=(const CSpeed& right)
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ m_value += right.m_value;
+ return *this;
+}
+
+const CSpeed& CSpeed::operator -=(const CSpeed& right)
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ m_value -= right.m_value;
+ return *this;
+}
+
+const CSpeed& CSpeed::operator *=(const CSpeed& right)
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ m_value *= right.m_value;
+ return *this;
+}
+
+const CSpeed& CSpeed::operator /=(const CSpeed& right)
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ m_value /= right.m_value;
+ return *this;
+}
+
+CSpeed CSpeed::operator +(const CSpeed& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ CSpeed temp(*this);
+
+ if (!IsValid() || !right.IsValid())
+ temp.SetValid(false);
+ else
+ temp.m_value += right.m_value;
+
+ return temp;
+}
+
+CSpeed CSpeed::operator -(const CSpeed& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ CSpeed temp(*this);
+ if (!IsValid() || !right.IsValid())
+ temp.SetValid(false);
+ else
+ temp.m_value -= right.m_value;
+
+ return temp;
+}
+
+CSpeed CSpeed::operator *(const CSpeed& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ CSpeed temp(*this);
+ if (!IsValid() || !right.IsValid())
+ temp.SetValid(false);
+ else
+ temp.m_value *= right.m_value;
+ return temp;
+}
+
+CSpeed CSpeed::operator /(const CSpeed& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ CSpeed temp(*this);
+ if (!IsValid() || !right.IsValid())
+ temp.SetValid(false);
+ else
+ temp.m_value /= right.m_value;
+ return temp;
+}
+
+CSpeed& CSpeed::operator ++()
+{
+ assert(IsValid());
+
+ m_value++;
+ return *this;
+}
+
+CSpeed& CSpeed::operator --()
+{
+ assert(IsValid());
+
+ m_value--;
+ return *this;
+}
+
+CSpeed CSpeed::operator ++(int)
+{
+ assert(IsValid());
+
+ CSpeed temp(*this);
+ m_value++;
+ return temp;
+}
+
+CSpeed CSpeed::operator --(int)
+{
+ assert(IsValid());
+
+ CSpeed temp(*this);
+ m_value--;
+ return temp;
+}
+
+bool CSpeed::operator >(double right) const
+{
+ assert(IsValid());
+
+ if (!IsValid())
+ return false;
+
+ return (m_value > right);
+}
+
+bool CSpeed::operator >=(double right) const
+{
+ return operator >(right) || operator ==(right);
+}
+
+bool CSpeed::operator <(double right) const
+{
+ assert(IsValid());
+
+ if (!IsValid())
+ return false;
+
+ return (m_value < right);
+}
+
+bool CSpeed::operator <=(double right) const
+{
+ return operator <(right) || operator ==(right);
+}
+
+bool CSpeed::operator ==(double right) const
+{
+ if (!IsValid())
+ return false;
+
+ return (m_value == right);
+}
+
+bool CSpeed::operator !=(double right) const
+{
+ return !operator ==(right);
+}
+
+const CSpeed& CSpeed::operator +=(double right)
+{
+ assert(IsValid());
+
+ m_value += right;
+ return *this;
+}
+
+const CSpeed& CSpeed::operator -=(double right)
+{
+ assert(IsValid());
+
+ m_value -= right;
+ return *this;
+}
+
+const CSpeed& CSpeed::operator *=(double right)
+{
+ assert(IsValid());
+
+ m_value *= right;
+ return *this;
+}
+
+const CSpeed& CSpeed::operator /=(double right)
+{
+ assert(IsValid());
+
+ m_value /= right;
+ return *this;
+}
+
+CSpeed CSpeed::operator +(double right) const
+{
+ assert(IsValid());
+
+ CSpeed temp(*this);
+ temp.m_value += right;
+ return temp;
+}
+
+CSpeed CSpeed::operator -(double right) const
+{
+ assert(IsValid());
+
+ CSpeed temp(*this);
+ temp.m_value -= right;
+ return temp;
+}
+
+CSpeed CSpeed::operator *(double right) const
+{
+ assert(IsValid());
+
+ CSpeed temp(*this);
+ temp.m_value *= right;
+ return temp;
+}
+
+CSpeed CSpeed::operator /(double right) const
+{
+ assert(IsValid());
+
+ CSpeed temp(*this);
+ temp.m_value /= right;
+ return temp;
+}
+
+CSpeed CSpeed::CreateFromKilometresPerHour(double value)
+{
+ return CSpeed(value / 3.6);
+}
+
+CSpeed CSpeed::CreateFromMetresPerMinute(double value)
+{
+ return CSpeed(value / 60.0);
+}
+
+CSpeed CSpeed::CreateFromMetresPerSecond(double value)
+{
+ return CSpeed(value);
+}
+
+CSpeed CSpeed::CreateFromFeetPerHour(double value)
+{
+ return CreateFromFeetPerMinute(value / 60.0);
+}
+
+CSpeed CSpeed::CreateFromFeetPerMinute(double value)
+{
+ return CreateFromFeetPerSecond(value / 60.0);
+}
+
+CSpeed CSpeed::CreateFromFeetPerSecond(double value)
+{
+ return CSpeed(value / 3.280839895);
+}
+
+CSpeed CSpeed::CreateFromMilesPerHour(double value)
+{
+ return CSpeed(value / 2.236936292);
+}
+
+CSpeed CSpeed::CreateFromKnots(double value)
+{
+ return CSpeed(value / 1.943846172);
+}
+
+CSpeed CSpeed::CreateFromBeaufort(unsigned int value)
+{
+ if (value == 0)
+ return CSpeed(0.15);
+ if (value == 1)
+ return CSpeed(0.9);
+ if (value == 2)
+ return CSpeed(2.4);
+ if (value == 3)
+ return CSpeed(4.4);
+ if (value == 4)
+ return CSpeed(6.75);
+ if (value == 5)
+ return CSpeed(9.4);
+ if (value == 6)
+ return CSpeed(12.35);
+ if (value == 7)
+ return CSpeed(15.55);
+ if (value == 8)
+ return CSpeed(18.95);
+ if (value == 9)
+ return CSpeed(22.6);
+ if (value == 10)
+ return CSpeed(26.45);
+ if (value == 11)
+ return CSpeed(30.5);
+
+ return CSpeed(32.6);
+}
+
+CSpeed CSpeed::CreateFromInchPerSecond(double value)
+{
+ return CSpeed(value / 39.37007874);
+}
+
+CSpeed CSpeed::CreateFromYardPerSecond(double value)
+{
+ return CSpeed(value / 1.093613298);
+}
+
+CSpeed CSpeed::CreateFromFurlongPerFortnight(double value)
+{
+ return CSpeed(value / 6012.885613871);
+}
+
+void CSpeed::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_value;
+ ar << m_valid;
+ }
+ else
+ {
+ ar >> m_value;
+ ar >> m_valid;
+ }
+}
+
+bool CSpeed::IsValid() const
+{
+ return m_valid;
+}
+
+double CSpeed::ToKilometresPerHour() const
+{
+ return m_value * 3.6;
+}
+
+double CSpeed::ToMetresPerMinute() const
+{
+ return m_value * 60.0;
+}
+
+double CSpeed::ToMetresPerSecond() const
+{
+ return m_value;
+}
+
+double CSpeed::ToFeetPerHour() const
+{
+ return ToFeetPerMinute() * 60.0;
+}
+
+double CSpeed::ToFeetPerMinute() const
+{
+ return ToFeetPerSecond() * 60.0;
+}
+
+double CSpeed::ToFeetPerSecond() const
+{
+ return m_value * 3.280839895;
+}
+
+double CSpeed::ToMilesPerHour() const
+{
+ return m_value * 2.236936292;
+}
+
+double CSpeed::ToKnots() const
+{
+ return m_value * 1.943846172;
+}
+
+double CSpeed::ToBeaufort() const
+{
+ if (m_value < 0.3)
+ return 0;
+ if (m_value >= 0.3 && m_value < 1.5)
+ return 1;
+ if (m_value >= 1.5 && m_value < 3.3)
+ return 2;
+ if (m_value >= 3.3 && m_value < 5.5)
+ return 3;
+ if (m_value >= 5.5 && m_value < 8.0)
+ return 4;
+ if (m_value >= 8.0 && m_value < 10.8)
+ return 5;
+ if (m_value >= 10.8 && m_value < 13.9)
+ return 6;
+ if (m_value >= 13.9 && m_value < 17.2)
+ return 7;
+ if (m_value >= 17.2 && m_value < 20.7)
+ return 8;
+ if (m_value >= 20.7 && m_value < 24.5)
+ return 9;
+ if (m_value >= 24.5 && m_value < 28.4)
+ return 10;
+ if (m_value >= 28.4 && m_value < 32.6)
+ return 11;
+
+ return 12;
+}
+
+double CSpeed::ToInchPerSecond() const
+{
+ return m_value * 39.37007874;
+}
+
+double CSpeed::ToYardPerSecond() const
+{
+ return m_value * 1.093613298;
+}
+
+double CSpeed::ToFurlongPerFortnight() const
+{
+ return m_value * 6012.885613871;
+}
+
+double CSpeed::To(Unit speedUnit) const
+{
+ if (!IsValid())
+ return 0;
+
+ double value = 0.0;
+
+ switch (speedUnit)
+ {
+ case UnitKilometresPerHour:
+ value = ToKilometresPerHour();
+ break;
+ case UnitMetresPerMinute:
+ value = ToMetresPerMinute();
+ break;
+ case UnitMetresPerSecond:
+ value = ToMetresPerSecond();
+ break;
+ case UnitFeetPerHour:
+ value = ToFeetPerHour();
+ break;
+ case UnitFeetPerMinute:
+ value = ToFeetPerMinute();
+ break;
+ case UnitFeetPerSecond:
+ value = ToFeetPerSecond();
+ break;
+ case UnitMilesPerHour:
+ value = ToMilesPerHour();
+ break;
+ case UnitKnots:
+ value = ToKnots();
+ break;
+ case UnitBeaufort:
+ value = ToBeaufort();
+ break;
+ case UnitInchPerSecond:
+ value = ToInchPerSecond();
+ break;
+ case UnitYardPerSecond:
+ value = ToYardPerSecond();
+ break;
+ case UnitFurlongPerFortnight:
+ value = ToFurlongPerFortnight();
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ return value;
+}
+
+// Returns temperature as localized string
+std::string CSpeed::ToString(Unit speedUnit) const
+{
+ if (!IsValid())
+ return "";
+
+ return StringUtils::Format("{:2.0f}", To(speedUnit));
+}
diff --git a/xbmc/utils/Speed.h b/xbmc/utils/Speed.h
new file mode 100644
index 0000000..8ad5d05
--- /dev/null
+++ b/xbmc/utils/Speed.h
@@ -0,0 +1,116 @@
+/*
+ * 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 "utils/IArchivable.h"
+
+#include <string>
+
+class CSpeed : public IArchivable
+{
+public:
+ CSpeed();
+ CSpeed(const CSpeed& speed);
+
+ typedef enum Unit
+ {
+ UnitKilometresPerHour = 0,
+ UnitMetresPerMinute,
+ UnitMetresPerSecond,
+ UnitFeetPerHour,
+ UnitFeetPerMinute,
+ UnitFeetPerSecond,
+ UnitMilesPerHour,
+ UnitKnots,
+ UnitBeaufort,
+ UnitInchPerSecond,
+ UnitYardPerSecond,
+ UnitFurlongPerFortnight
+ } Unit;
+
+ static CSpeed CreateFromKilometresPerHour(double value);
+ static CSpeed CreateFromMetresPerMinute(double value);
+ static CSpeed CreateFromMetresPerSecond(double value);
+ static CSpeed CreateFromFeetPerHour(double value);
+ static CSpeed CreateFromFeetPerMinute(double value);
+ static CSpeed CreateFromFeetPerSecond(double value);
+ static CSpeed CreateFromMilesPerHour(double value);
+ static CSpeed CreateFromKnots(double value);
+ static CSpeed CreateFromBeaufort(unsigned int value);
+ static CSpeed CreateFromInchPerSecond(double value);
+ static CSpeed CreateFromYardPerSecond(double value);
+ static CSpeed CreateFromFurlongPerFortnight(double value);
+
+ bool operator >(const CSpeed& right) const;
+ bool operator >=(const CSpeed& right) const;
+ bool operator <(const CSpeed& right) const;
+ bool operator <=(const CSpeed& right) const;
+ bool operator ==(const CSpeed& right) const;
+ bool operator !=(const CSpeed& right) const;
+
+ CSpeed& operator =(const CSpeed& right);
+ const CSpeed& operator +=(const CSpeed& right);
+ const CSpeed& operator -=(const CSpeed& right);
+ const CSpeed& operator *=(const CSpeed& right);
+ const CSpeed& operator /=(const CSpeed& right);
+ CSpeed operator +(const CSpeed& right) const;
+ CSpeed operator -(const CSpeed& right) const;
+ CSpeed operator *(const CSpeed& right) const;
+ CSpeed operator /(const CSpeed& right) const;
+
+ bool operator >(double right) const;
+ bool operator >=(double right) const;
+ bool operator <(double right) const;
+ bool operator <=(double right) const;
+ bool operator ==(double right) const;
+ bool operator !=(double right) const;
+
+ const CSpeed& operator +=(double right);
+ const CSpeed& operator -=(double right);
+ const CSpeed& operator *=(double right);
+ const CSpeed& operator /=(double right);
+ CSpeed operator +(double right) const;
+ CSpeed operator -(double right) const;
+ CSpeed operator *(double right) const;
+ CSpeed operator /(double right) const;
+
+ CSpeed& operator ++();
+ CSpeed& operator --();
+ CSpeed operator ++(int);
+ CSpeed operator --(int);
+
+ void Archive(CArchive& ar) override;
+
+ bool IsValid() const;
+
+ double ToKilometresPerHour() const;
+ double ToMetresPerMinute() const;
+ double ToMetresPerSecond() const;
+ double ToFeetPerHour() const;
+ double ToFeetPerMinute() const;
+ double ToFeetPerSecond() const;
+ double ToMilesPerHour() const;
+ double ToKnots() const;
+ double ToBeaufort() const;
+ double ToInchPerSecond() const;
+ double ToYardPerSecond() const;
+ double ToFurlongPerFortnight() const;
+
+ double To(Unit speedUnit) const;
+ std::string ToString(Unit speedUnit) const;
+
+protected:
+ explicit CSpeed(double value);
+
+ void SetValid(bool valid) { m_valid = valid; }
+
+ double m_value; // we store in m/s
+ bool m_valid;
+};
+
diff --git a/xbmc/utils/Stopwatch.h b/xbmc/utils/Stopwatch.h
new file mode 100644
index 0000000..9ec2983
--- /dev/null
+++ b/xbmc/utils/Stopwatch.h
@@ -0,0 +1,111 @@
+/*
+ * 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 <chrono>
+#include <stdint.h>
+
+class CStopWatch
+{
+public:
+ CStopWatch() = default;
+ ~CStopWatch() = default;
+
+ /*!
+ \brief Retrieve the running state of the stopwatch.
+
+ \return True if stopwatch has been started but not stopped.
+ */
+ inline bool IsRunning() const
+ {
+ return m_isRunning;
+ }
+
+ /*!
+ \brief Record start time and change state to running.
+ */
+ inline void StartZero()
+ {
+ m_startTick = std::chrono::steady_clock::now();
+ m_isRunning = true;
+ }
+
+ /*!
+ \brief Record start time and change state to running, only if the stopwatch is stopped.
+ */
+ inline void Start()
+ {
+ if (!m_isRunning)
+ StartZero();
+ }
+
+ /*!
+ \brief Record stop time and change state to not running.
+ */
+ inline void Stop()
+ {
+ if(m_isRunning)
+ {
+ m_stopTick = std::chrono::steady_clock::now();
+ m_isRunning = false;
+ }
+ }
+
+ /*!
+ \brief Set the start time such that time elapsed is now zero.
+ */
+ void Reset()
+ {
+ if (m_isRunning)
+ m_startTick = std::chrono::steady_clock::now();
+ else
+ m_startTick = m_stopTick;
+ }
+
+ /*!
+ \brief Retrieve time elapsed between the last call to Start(), StartZero()
+ or Reset() and; if running, now; if stopped, the last call to Stop().
+
+ \return Elapsed time, in seconds, as a float.
+ */
+ float GetElapsedSeconds() const
+ {
+ std::chrono::duration<float> elapsed;
+
+ if (m_isRunning)
+ elapsed = std::chrono::steady_clock::now() - m_startTick;
+ else
+ elapsed = m_stopTick - m_startTick;
+
+ return elapsed.count();
+ }
+
+ /*!
+ \brief Retrieve time elapsed between the last call to Start(), StartZero()
+ or Reset() and; if running, now; if stopped, the last call to Stop().
+
+ \return Elapsed time, in milliseconds, as a float.
+ */
+ float GetElapsedMilliseconds() const
+ {
+ std::chrono::duration<float, std::milli> elapsed;
+
+ if (m_isRunning)
+ elapsed = std::chrono::steady_clock::now() - m_startTick;
+ else
+ elapsed = m_stopTick - m_startTick;
+
+ return elapsed.count();
+ }
+
+private:
+ std::chrono::time_point<std::chrono::steady_clock> m_startTick;
+ std::chrono::time_point<std::chrono::steady_clock> m_stopTick;
+ bool m_isRunning = false;
+};
diff --git a/xbmc/utils/StreamDetails.cpp b/xbmc/utils/StreamDetails.cpp
new file mode 100644
index 0000000..90f5b4a
--- /dev/null
+++ b/xbmc/utils/StreamDetails.cpp
@@ -0,0 +1,674 @@
+/*
+ * 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 "StreamDetails.h"
+
+#include "LangInfo.h"
+#include "StreamUtils.h"
+#include "utils/Archive.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/Variant.h"
+
+#include <math.h>
+
+const float VIDEOASPECT_EPSILON = 0.025f;
+
+CStreamDetailVideo::CStreamDetailVideo() :
+ CStreamDetail(CStreamDetail::VIDEO)
+{
+}
+
+CStreamDetailVideo::CStreamDetailVideo(const VideoStreamInfo &info, int duration) :
+ CStreamDetail(CStreamDetail::VIDEO),
+ m_iWidth(info.width),
+ m_iHeight(info.height),
+ m_fAspect(info.videoAspectRatio),
+ m_iDuration(duration),
+ m_strCodec(info.codecName),
+ m_strStereoMode(info.stereoMode),
+ m_strLanguage(info.language),
+ m_strHdrType(CStreamDetails::HdrTypeToString(info.hdrType))
+{
+}
+
+void CStreamDetailVideo::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_strCodec;
+ ar << m_fAspect;
+ ar << m_iHeight;
+ ar << m_iWidth;
+ ar << m_iDuration;
+ ar << m_strStereoMode;
+ ar << m_strLanguage;
+ ar << m_strHdrType;
+ }
+ else
+ {
+ ar >> m_strCodec;
+ ar >> m_fAspect;
+ ar >> m_iHeight;
+ ar >> m_iWidth;
+ ar >> m_iDuration;
+ ar >> m_strStereoMode;
+ ar >> m_strLanguage;
+ ar >> m_strHdrType;
+ }
+}
+void CStreamDetailVideo::Serialize(CVariant& value) const
+{
+ value["codec"] = m_strCodec;
+ value["aspect"] = m_fAspect;
+ value["height"] = m_iHeight;
+ value["width"] = m_iWidth;
+ value["duration"] = m_iDuration;
+ value["stereomode"] = m_strStereoMode;
+ value["language"] = m_strLanguage;
+ value["hdrtype"] = m_strHdrType;
+}
+
+bool CStreamDetailVideo::IsWorseThan(const CStreamDetail &that) const
+{
+ if (that.m_eType != CStreamDetail::VIDEO)
+ return true;
+
+ // Best video stream is that with the most pixels
+ const auto& sdv = static_cast<const CStreamDetailVideo&>(that);
+ return (sdv.m_iWidth * sdv.m_iHeight) > (m_iWidth * m_iHeight);
+}
+
+CStreamDetailAudio::CStreamDetailAudio() :
+ CStreamDetail(CStreamDetail::AUDIO)
+{
+}
+
+CStreamDetailAudio::CStreamDetailAudio(const AudioStreamInfo &info) :
+ CStreamDetail(CStreamDetail::AUDIO),
+ m_iChannels(info.channels),
+ m_strCodec(info.codecName),
+ m_strLanguage(info.language)
+{
+}
+
+void CStreamDetailAudio::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_strCodec;
+ ar << m_strLanguage;
+ ar << m_iChannels;
+ }
+ else
+ {
+ ar >> m_strCodec;
+ ar >> m_strLanguage;
+ ar >> m_iChannels;
+ }
+}
+void CStreamDetailAudio::Serialize(CVariant& value) const
+{
+ value["codec"] = m_strCodec;
+ value["language"] = m_strLanguage;
+ value["channels"] = m_iChannels;
+}
+
+bool CStreamDetailAudio::IsWorseThan(const CStreamDetail &that) const
+{
+ if (that.m_eType != CStreamDetail::AUDIO)
+ return true;
+
+ const auto& sda = static_cast<const CStreamDetailAudio&>(that);
+ // First choice is the thing with the most channels
+ if (sda.m_iChannels > m_iChannels)
+ return true;
+ if (m_iChannels > sda.m_iChannels)
+ return false;
+
+ // In case of a tie, revert to codec priority
+ return StreamUtils::GetCodecPriority(sda.m_strCodec) > StreamUtils::GetCodecPriority(m_strCodec);
+}
+
+CStreamDetailSubtitle::CStreamDetailSubtitle() :
+ CStreamDetail(CStreamDetail::SUBTITLE)
+{
+}
+
+CStreamDetailSubtitle::CStreamDetailSubtitle(const SubtitleStreamInfo &info) :
+ CStreamDetail(CStreamDetail::SUBTITLE),
+ m_strLanguage(info.language)
+{
+}
+
+void CStreamDetailSubtitle::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_strLanguage;
+ }
+ else
+ {
+ ar >> m_strLanguage;
+ }
+}
+void CStreamDetailSubtitle::Serialize(CVariant& value) const
+{
+ value["language"] = m_strLanguage;
+}
+
+bool CStreamDetailSubtitle::IsWorseThan(const CStreamDetail &that) const
+{
+ if (that.m_eType != CStreamDetail::SUBTITLE)
+ return true;
+
+ if (g_LangCodeExpander.CompareISO639Codes(m_strLanguage, static_cast<const CStreamDetailSubtitle &>(that).m_strLanguage))
+ return false;
+
+ // the best subtitle should be the one in the user's preferred language
+ // If preferred language is set to "original" this is "eng"
+ return m_strLanguage.empty() ||
+ g_LangCodeExpander.CompareISO639Codes(static_cast<const CStreamDetailSubtitle &>(that).m_strLanguage, g_langInfo.GetSubtitleLanguage());
+}
+
+CStreamDetailSubtitle& CStreamDetailSubtitle::operator=(const CStreamDetailSubtitle &that)
+{
+ if (this != &that)
+ {
+ this->m_pParent = that.m_pParent;
+ this->m_strLanguage = that.m_strLanguage;
+ }
+ return *this;
+}
+
+CStreamDetails& CStreamDetails::operator=(const CStreamDetails &that)
+{
+ if (this != &that)
+ {
+ Reset();
+ for (const auto &iter : that.m_vecItems)
+ {
+ switch (iter->m_eType)
+ {
+ case CStreamDetail::VIDEO:
+ AddStream(new CStreamDetailVideo(static_cast<const CStreamDetailVideo&>(*iter)));
+ break;
+ case CStreamDetail::AUDIO:
+ AddStream(new CStreamDetailAudio(static_cast<const CStreamDetailAudio&>(*iter)));
+ break;
+ case CStreamDetail::SUBTITLE:
+ AddStream(new CStreamDetailSubtitle(static_cast<const CStreamDetailSubtitle&>(*iter)));
+ break;
+ }
+ }
+
+ DetermineBestStreams();
+ } /* if this != that */
+
+ return *this;
+}
+
+bool CStreamDetails::operator ==(const CStreamDetails &right) const
+{
+ if (this == &right) return true;
+
+ if (GetVideoStreamCount() != right.GetVideoStreamCount() ||
+ GetAudioStreamCount() != right.GetAudioStreamCount() ||
+ GetSubtitleStreamCount() != right.GetSubtitleStreamCount())
+ return false;
+
+ for (int iStream=1; iStream<=GetVideoStreamCount(); iStream++)
+ {
+ if (GetVideoCodec(iStream) != right.GetVideoCodec(iStream) ||
+ GetVideoWidth(iStream) != right.GetVideoWidth(iStream) ||
+ GetVideoHeight(iStream) != right.GetVideoHeight(iStream) ||
+ GetVideoDuration(iStream) != right.GetVideoDuration(iStream) ||
+ fabs(GetVideoAspect(iStream) - right.GetVideoAspect(iStream)) > VIDEOASPECT_EPSILON)
+ return false;
+ }
+
+ for (int iStream=1; iStream<=GetAudioStreamCount(); iStream++)
+ {
+ if (GetAudioCodec(iStream) != right.GetAudioCodec(iStream) ||
+ GetAudioLanguage(iStream) != right.GetAudioLanguage(iStream) ||
+ GetAudioChannels(iStream) != right.GetAudioChannels(iStream) )
+ return false;
+ }
+
+ for (int iStream=1; iStream<=GetSubtitleStreamCount(); iStream++)
+ {
+ if (GetSubtitleLanguage(iStream) != right.GetSubtitleLanguage(iStream) )
+ return false;
+ }
+
+ return true;
+}
+
+bool CStreamDetails::operator !=(const CStreamDetails &right) const
+{
+ if (this == &right) return false;
+
+ return !(*this == right);
+}
+
+CStreamDetail *CStreamDetails::NewStream(CStreamDetail::StreamType type)
+{
+ CStreamDetail *retVal = NULL;
+ switch (type)
+ {
+ case CStreamDetail::VIDEO:
+ retVal = new CStreamDetailVideo();
+ break;
+ case CStreamDetail::AUDIO:
+ retVal = new CStreamDetailAudio();
+ break;
+ case CStreamDetail::SUBTITLE:
+ retVal = new CStreamDetailSubtitle();
+ break;
+ }
+
+ if (retVal)
+ AddStream(retVal);
+
+ return retVal;
+}
+
+std::string CStreamDetails::GetVideoLanguage(int idx) const
+{
+ const CStreamDetailVideo* item =
+ dynamic_cast<const CStreamDetailVideo*>(GetNthStream(CStreamDetail::VIDEO, idx));
+ if (item)
+ return item->m_strLanguage;
+ else
+ return "";
+}
+
+int CStreamDetails::GetStreamCount(CStreamDetail::StreamType type) const
+{
+ int retVal = 0;
+ for (const auto &iter : m_vecItems)
+ if (iter->m_eType == type)
+ retVal++;
+ return retVal;
+}
+
+int CStreamDetails::GetVideoStreamCount(void) const
+{
+ return GetStreamCount(CStreamDetail::VIDEO);
+}
+
+int CStreamDetails::GetAudioStreamCount(void) const
+{
+ return GetStreamCount(CStreamDetail::AUDIO);
+}
+
+int CStreamDetails::GetSubtitleStreamCount(void) const
+{
+ return GetStreamCount(CStreamDetail::SUBTITLE);
+}
+
+CStreamDetails::CStreamDetails(const CStreamDetails &that)
+{
+ m_pBestVideo = nullptr;
+ m_pBestAudio = nullptr;
+ m_pBestSubtitle = nullptr;
+ *this = that;
+}
+
+void CStreamDetails::AddStream(CStreamDetail *item)
+{
+ item->m_pParent = this;
+ m_vecItems.emplace_back(item);
+}
+
+void CStreamDetails::Reset(void)
+{
+ m_pBestVideo = nullptr;
+ m_pBestAudio = nullptr;
+ m_pBestSubtitle = nullptr;
+
+ m_vecItems.clear();
+}
+
+const CStreamDetail* CStreamDetails::GetNthStream(CStreamDetail::StreamType type, int idx) const
+{
+ if (idx == 0)
+ {
+ switch (type)
+ {
+ case CStreamDetail::VIDEO:
+ return m_pBestVideo;
+ break;
+ case CStreamDetail::AUDIO:
+ return m_pBestAudio;
+ break;
+ case CStreamDetail::SUBTITLE:
+ return m_pBestSubtitle;
+ break;
+ default:
+ return NULL;
+ break;
+ }
+ }
+
+ for (const auto &iter : m_vecItems)
+ if (iter->m_eType == type)
+ {
+ idx--;
+ if (idx < 1)
+ return iter.get();
+ }
+
+ return NULL;
+}
+
+std::string CStreamDetails::GetVideoCodec(int idx) const
+{
+ const CStreamDetailVideo* item =
+ dynamic_cast<const CStreamDetailVideo*>(GetNthStream(CStreamDetail::VIDEO, idx));
+ if (item)
+ return item->m_strCodec;
+ else
+ return "";
+}
+
+float CStreamDetails::GetVideoAspect(int idx) const
+{
+ const CStreamDetailVideo* item =
+ dynamic_cast<const CStreamDetailVideo*>(GetNthStream(CStreamDetail::VIDEO, idx));
+ if (item)
+ return item->m_fAspect;
+ else
+ return 0.0;
+}
+
+int CStreamDetails::GetVideoWidth(int idx) const
+{
+ const CStreamDetailVideo* item =
+ dynamic_cast<const CStreamDetailVideo*>(GetNthStream(CStreamDetail::VIDEO, idx));
+ if (item)
+ return item->m_iWidth;
+ else
+ return 0;
+}
+
+int CStreamDetails::GetVideoHeight(int idx) const
+{
+ const CStreamDetailVideo* item =
+ dynamic_cast<const CStreamDetailVideo*>(GetNthStream(CStreamDetail::VIDEO, idx));
+ if (item)
+ return item->m_iHeight;
+ else
+ return 0;
+}
+
+std::string CStreamDetails::GetVideoHdrType( int idx) const
+{
+ const CStreamDetailVideo* item =
+ dynamic_cast<const CStreamDetailVideo*>(GetNthStream(CStreamDetail::VIDEO, idx));
+ if (item)
+ return item->m_strHdrType;
+ else
+ return "";
+}
+
+int CStreamDetails::GetVideoDuration(int idx) const
+{
+ const CStreamDetailVideo* item =
+ dynamic_cast<const CStreamDetailVideo*>(GetNthStream(CStreamDetail::VIDEO, idx));
+ if (item)
+ return item->m_iDuration;
+ else
+ return 0;
+}
+
+void CStreamDetails::SetVideoDuration(int idx, const int duration)
+{
+ CStreamDetailVideo* item = const_cast<CStreamDetailVideo*>(
+ dynamic_cast<const CStreamDetailVideo*>(GetNthStream(CStreamDetail::VIDEO, idx)));
+ if (item)
+ item->m_iDuration = duration;
+}
+
+std::string CStreamDetails::GetStereoMode(int idx) const
+{
+ const CStreamDetailVideo* item =
+ dynamic_cast<const CStreamDetailVideo*>(GetNthStream(CStreamDetail::VIDEO, idx));
+ if (item)
+ return item->m_strStereoMode;
+ else
+ return "";
+}
+
+std::string CStreamDetails::GetAudioCodec(int idx) const
+{
+ const CStreamDetailAudio* item =
+ dynamic_cast<const CStreamDetailAudio*>(GetNthStream(CStreamDetail::AUDIO, idx));
+ if (item)
+ return item->m_strCodec;
+ else
+ return "";
+}
+
+std::string CStreamDetails::GetAudioLanguage(int idx) const
+{
+ const CStreamDetailAudio* item =
+ dynamic_cast<const CStreamDetailAudio*>(GetNthStream(CStreamDetail::AUDIO, idx));
+ if (item)
+ return item->m_strLanguage;
+ else
+ return "";
+}
+
+int CStreamDetails::GetAudioChannels(int idx) const
+{
+ const CStreamDetailAudio* item =
+ dynamic_cast<const CStreamDetailAudio*>(GetNthStream(CStreamDetail::AUDIO, idx));
+ if (item)
+ return item->m_iChannels;
+ else
+ return -1;
+}
+
+std::string CStreamDetails::GetSubtitleLanguage(int idx) const
+{
+ const CStreamDetailSubtitle* item =
+ dynamic_cast<const CStreamDetailSubtitle*>(GetNthStream(CStreamDetail::SUBTITLE, idx));
+ if (item)
+ return item->m_strLanguage;
+ else
+ return "";
+}
+
+void CStreamDetails::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << (int)m_vecItems.size();
+
+ for (auto &iter : m_vecItems)
+ {
+ // the type goes before the actual item. When loading we need
+ // to know the type before we can construct an instance to serialize
+ ar << (int)iter->m_eType;
+ ar << (*iter);
+ }
+ }
+ else
+ {
+ int count;
+ ar >> count;
+
+ Reset();
+ for (int i=0; i<count; i++)
+ {
+ int type;
+ CStreamDetail *p = NULL;
+
+ ar >> type;
+ p = NewStream(CStreamDetail::StreamType(type));
+ if (p)
+ ar >> (*p);
+ }
+
+ DetermineBestStreams();
+ }
+}
+void CStreamDetails::Serialize(CVariant& value) const
+{
+ // make sure these properties are always present
+ value["audio"] = CVariant(CVariant::VariantTypeArray);
+ value["video"] = CVariant(CVariant::VariantTypeArray);
+ value["subtitle"] = CVariant(CVariant::VariantTypeArray);
+
+ CVariant v;
+ for (const auto &iter : m_vecItems)
+ {
+ v.clear();
+ iter->Serialize(v);
+ switch (iter->m_eType)
+ {
+ case CStreamDetail::AUDIO:
+ value["audio"].push_back(v);
+ break;
+ case CStreamDetail::VIDEO:
+ value["video"].push_back(v);
+ break;
+ case CStreamDetail::SUBTITLE:
+ value["subtitle"].push_back(v);
+ break;
+ }
+ }
+}
+
+void CStreamDetails::DetermineBestStreams(void)
+{
+ m_pBestVideo = NULL;
+ m_pBestAudio = NULL;
+ m_pBestSubtitle = NULL;
+
+ for (const auto &iter : m_vecItems)
+ {
+ const CStreamDetail **champion;
+ switch (iter->m_eType)
+ {
+ case CStreamDetail::VIDEO:
+ champion = (const CStreamDetail **)&m_pBestVideo;
+ break;
+ case CStreamDetail::AUDIO:
+ champion = (const CStreamDetail **)&m_pBestAudio;
+ break;
+ case CStreamDetail::SUBTITLE:
+ champion = (const CStreamDetail **)&m_pBestSubtitle;
+ break;
+ default:
+ champion = NULL;
+ } /* switch type */
+
+ if (!champion)
+ continue;
+
+ if ((*champion == NULL) || (*champion)->IsWorseThan(*iter))
+ *champion = iter.get();
+ } /* for each */
+}
+
+std::string CStreamDetails::VideoDimsToResolutionDescription(int iWidth, int iHeight)
+{
+ if (iWidth == 0 || iHeight == 0)
+ return "";
+
+ else if (iWidth <= 720 && iHeight <= 480)
+ return "480";
+ // 720x576 (PAL) (768 when rescaled for square pixels)
+ else if (iWidth <= 768 && iHeight <= 576)
+ return "576";
+ // 960x540 (sometimes 544 which is multiple of 16)
+ else if (iWidth <= 960 && iHeight <= 544)
+ return "540";
+ // 1280x720
+ else if (iWidth <= 1280 && iHeight <= 962)
+ return "720";
+ // 1920x1080
+ else if (iWidth <= 1920 && iHeight <= 1440)
+ return "1080";
+ // 4K
+ else if (iWidth <= 4096 && iHeight <= 3072)
+ return "4K";
+ // 8K
+ else if (iWidth <= 8192 && iHeight <= 6144)
+ return "8K";
+ else
+ return "";
+}
+
+std::string CStreamDetails::VideoAspectToAspectDescription(float fAspect)
+{
+ if (fAspect == 0.0f)
+ return "";
+
+ // Given that we're never going to be able to handle every single possibility in
+ // aspect ratios, particularly when cropping prior to video encoding is taken into account
+ // the best we can do is take the "common" aspect ratios, and return the closest one available.
+ // The cutoffs are the geometric mean of the two aspect ratios either side.
+ if (fAspect < 1.0909f) // sqrt(1.00*1.19)
+ return "1.00";
+ else if (fAspect < 1.2581f) // sqrt(1.19*1.33)
+ return "1.19";
+ else if (fAspect < 1.3499f) // sqrt(1.33*1.37)
+ return "1.33";
+ else if (fAspect < 1.5080f) // sqrt(1.37*1.66)
+ return "1.37";
+ else if (fAspect < 1.7190f) // sqrt(1.66*1.78)
+ return "1.66";
+ else if (fAspect < 1.8147f) // sqrt(1.78*1.85)
+ return "1.78";
+ else if (fAspect < 1.9235f) // sqrt(1.85*2.00)
+ return "1.85";
+ else if (fAspect < 2.0976f) // sqrt(2.00*2.20)
+ return "2.00";
+ else if (fAspect < 2.2738f) // sqrt(2.20*2.35)
+ return "2.20";
+ else if (fAspect < 2.3749f) // sqrt(2.35*2.40)
+ return "2.35";
+ else if (fAspect < 2.4739f) // sqrt(2.40*2.55)
+ return "2.40";
+ else if (fAspect < 2.6529f) // sqrt(2.55*2.76)
+ return "2.55";
+ return "2.76";
+}
+
+bool CStreamDetails::SetStreams(const VideoStreamInfo& videoInfo, int videoDuration, const AudioStreamInfo& audioInfo, const SubtitleStreamInfo& subtitleInfo)
+{
+ if (!videoInfo.valid && !audioInfo.valid && !subtitleInfo.valid)
+ return false;
+ Reset();
+ if (videoInfo.valid)
+ AddStream(new CStreamDetailVideo(videoInfo, videoDuration));
+ if (audioInfo.valid)
+ AddStream(new CStreamDetailAudio(audioInfo));
+ if (subtitleInfo.valid)
+ AddStream(new CStreamDetailSubtitle(subtitleInfo));
+ DetermineBestStreams();
+ return true;
+}
+
+std::string CStreamDetails::HdrTypeToString(StreamHdrType hdrType)
+{
+ switch (hdrType)
+ {
+ case StreamHdrType::HDR_TYPE_DOLBYVISION:
+ return "dolbyvision";
+ case StreamHdrType::HDR_TYPE_HDR10:
+ return "hdr10";
+ case StreamHdrType::HDR_TYPE_HLG:
+ return "hlg";
+ case StreamHdrType::HDR_TYPE_NONE:
+ default:
+ return "";
+ }
+}
diff --git a/xbmc/utils/StreamDetails.h b/xbmc/utils/StreamDetails.h
new file mode 100644
index 0000000..bc3aacd
--- /dev/null
+++ b/xbmc/utils/StreamDetails.h
@@ -0,0 +1,142 @@
+/*
+ * 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 "ISerializable.h"
+#include "cores/VideoPlayer/Interface/StreamInfo.h"
+#include "utils/IArchivable.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CStreamDetails;
+class CVariant;
+struct VideoStreamInfo;
+struct AudioStreamInfo;
+struct SubtitleStreamInfo;
+
+class CStreamDetail : public IArchivable, public ISerializable
+{
+public:
+ enum StreamType {
+ VIDEO,
+ AUDIO,
+ SUBTITLE
+ };
+
+ explicit CStreamDetail(StreamType type) : m_eType(type), m_pParent(NULL) {}
+ virtual ~CStreamDetail() = default;
+ virtual bool IsWorseThan(const CStreamDetail &that) const = 0;
+
+ const StreamType m_eType;
+
+protected:
+ CStreamDetails *m_pParent;
+ friend class CStreamDetails;
+};
+
+class CStreamDetailVideo final : public CStreamDetail
+{
+public:
+ CStreamDetailVideo();
+ CStreamDetailVideo(const VideoStreamInfo &info, int duration = 0);
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& value) const override;
+ bool IsWorseThan(const CStreamDetail &that) const override;
+
+ int m_iWidth = 0;
+ int m_iHeight = 0;
+ float m_fAspect = 0.0;
+ int m_iDuration = 0;
+ std::string m_strCodec;
+ std::string m_strStereoMode;
+ std::string m_strLanguage;
+ std::string m_strHdrType;
+};
+
+class CStreamDetailAudio final : public CStreamDetail
+{
+public:
+ CStreamDetailAudio();
+ CStreamDetailAudio(const AudioStreamInfo &info);
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& value) const override;
+ bool IsWorseThan(const CStreamDetail &that) const override;
+
+ int m_iChannels = -1;
+ std::string m_strCodec;
+ std::string m_strLanguage;
+};
+
+class CStreamDetailSubtitle final : public CStreamDetail
+{
+public:
+ CStreamDetailSubtitle();
+ CStreamDetailSubtitle(const SubtitleStreamInfo &info);
+ CStreamDetailSubtitle(const CStreamDetailSubtitle&) = default;
+ CStreamDetailSubtitle& operator=(const CStreamDetailSubtitle &that);
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& value) const override;
+ bool IsWorseThan(const CStreamDetail &that) const override;
+
+ std::string m_strLanguage;
+};
+
+class CStreamDetails final : public IArchivable, public ISerializable
+{
+public:
+ CStreamDetails() { Reset(); }
+ CStreamDetails(const CStreamDetails &that);
+ CStreamDetails& operator=(const CStreamDetails &that);
+ bool operator ==(const CStreamDetails &that) const;
+ bool operator !=(const CStreamDetails &that) const;
+
+ static std::string VideoDimsToResolutionDescription(int iWidth, int iHeight);
+ static std::string VideoAspectToAspectDescription(float fAspect);
+
+ bool HasItems(void) const { return m_vecItems.size() > 0; }
+ int GetStreamCount(CStreamDetail::StreamType type) const;
+ int GetVideoStreamCount(void) const;
+ int GetAudioStreamCount(void) const;
+ int GetSubtitleStreamCount(void) const;
+ static std::string HdrTypeToString(StreamHdrType hdrType);
+ const CStreamDetail* GetNthStream(CStreamDetail::StreamType type, int idx) const;
+
+ std::string GetVideoCodec(int idx = 0) const;
+ float GetVideoAspect(int idx = 0) const;
+ int GetVideoWidth(int idx = 0) const;
+ int GetVideoHeight(int idx = 0) const;
+ std::string GetVideoHdrType (int idx = 0) const;
+ int GetVideoDuration(int idx = 0) const;
+ void SetVideoDuration(int idx, const int duration);
+ std::string GetStereoMode(int idx = 0) const;
+ std::string GetVideoLanguage(int idx = 0) const;
+
+ std::string GetAudioCodec(int idx = 0) const;
+ std::string GetAudioLanguage(int idx = 0) const;
+ int GetAudioChannels(int idx = 0) const;
+
+ std::string GetSubtitleLanguage(int idx = 0) const;
+
+ void AddStream(CStreamDetail *item);
+ void Reset(void);
+ void DetermineBestStreams(void);
+
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& value) const override;
+
+ bool SetStreams(const VideoStreamInfo& videoInfo, int videoDuration, const AudioStreamInfo& audioInfo, const SubtitleStreamInfo& subtitleInfo);
+private:
+ CStreamDetail *NewStream(CStreamDetail::StreamType type);
+ std::vector<std::unique_ptr<CStreamDetail>> m_vecItems;
+ const CStreamDetailVideo *m_pBestVideo;
+ const CStreamDetailAudio *m_pBestAudio;
+ const CStreamDetailSubtitle *m_pBestSubtitle;
+};
diff --git a/xbmc/utils/StreamUtils.cpp b/xbmc/utils/StreamUtils.cpp
new file mode 100644
index 0000000..bd34fec
--- /dev/null
+++ b/xbmc/utils/StreamUtils.cpp
@@ -0,0 +1,32 @@
+/*
+ * 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 "StreamUtils.h"
+
+int StreamUtils::GetCodecPriority(const std::string &codec)
+{
+ /*
+ * Technically flac, truehd, and dtshd_ma are equivalently good as they're all lossless. However,
+ * ffmpeg can't decode dtshd_ma losslessy yet.
+ */
+ if (codec == "flac") // Lossless FLAC
+ return 7;
+ if (codec == "truehd") // Dolby TrueHD
+ return 6;
+ if (codec == "dtshd_ma") // DTS-HD Master Audio (previously known as DTS++)
+ return 5;
+ if (codec == "dtshd_hra") // DTS-HD High Resolution Audio
+ return 4;
+ if (codec == "eac3") // Dolby Digital Plus
+ return 3;
+ if (codec == "dca") // DTS
+ return 2;
+ if (codec == "ac3") // Dolby Digital
+ return 1;
+ return 0;
+}
diff --git a/xbmc/utils/StreamUtils.h b/xbmc/utils/StreamUtils.h
new file mode 100644
index 0000000..3d16e8a
--- /dev/null
+++ b/xbmc/utils/StreamUtils.h
@@ -0,0 +1,34 @@
+/*
+ * 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 <cstdint>
+#include <string>
+
+static constexpr int MP4_BOX_HEADER_SIZE = 8;
+
+class StreamUtils
+{
+public:
+ static int GetCodecPriority(const std::string& codec);
+
+ /*!
+ * \brief Make a FourCC code as unsigned integer value
+ * \param c1 The first FourCC char
+ * \param c2 The second FourCC char
+ * \param c3 The third FourCC char
+ * \param c4 The fourth FourCC char
+ * \return The FourCC as unsigned integer value
+ */
+ static constexpr uint32_t MakeFourCC(char c1, char c2, char c3, char c4)
+ {
+ return ((static_cast<uint32_t>(c1) << 24) | (static_cast<uint32_t>(c2) << 16) |
+ (static_cast<uint32_t>(c3) << 8) | (static_cast<uint32_t>(c4)));
+ }
+};
diff --git a/xbmc/utils/StringUtils.cpp b/xbmc/utils/StringUtils.cpp
new file mode 100644
index 0000000..7429223
--- /dev/null
+++ b/xbmc/utils/StringUtils.cpp
@@ -0,0 +1,1900 @@
+/*
+ * 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.
+ */
+//-----------------------------------------------------------------------
+//
+// File: StringUtils.cpp
+//
+// Purpose: ATL split string utility
+// Author: Paul J. Weiss
+//
+// Modified to use J O'Leary's std::string class by kraqh3d
+//
+//------------------------------------------------------------------------
+
+#ifdef HAVE_NEW_CROSSGUID
+#include <crossguid/guid.hpp>
+#else
+#include <guid.h>
+#endif
+
+#if defined(TARGET_ANDROID)
+#include <androidjni/JNIThreading.h>
+#endif
+
+#include "CharsetConverter.h"
+#include "LangInfo.h"
+#include "StringUtils.h"
+#include "XBDateTime.h"
+
+#include <algorithm>
+#include <array>
+#include <assert.h>
+#include <functional>
+#include <inttypes.h>
+#include <iomanip>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <fstrcmp.h>
+#include <memory.h>
+
+// don't move or std functions end up in PCRE namespace
+// clang-format off
+#include "utils/RegExp.h"
+// clang-format on
+
+#define FORMAT_BLOCK_SIZE 512 // # of bytes for initial allocation for printf
+
+namespace
+{
+/*!
+ * \brief Converts a string to a number of a specified type, by using istringstream.
+ * \param str The string to convert
+ * \param fallback [OPT] The number to return when the conversion fails
+ * \return The converted number, otherwise fallback if conversion fails
+ */
+template<typename T>
+T NumberFromSS(std::string_view str, T fallback) noexcept
+{
+ std::istringstream iss{str.data()};
+ T result{fallback};
+ iss >> result;
+ return result;
+}
+} // unnamed namespace
+
+static constexpr const char* ADDON_GUID_RE = "^(\\{){0,1}[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{12}(\\}){0,1}$";
+
+/* empty string for use in returns by ref */
+const std::string StringUtils::Empty = "";
+
+// Copyright (c) Leigh Brasington 2012. All rights reserved.
+// This code may be used and reproduced without written permission.
+// http://www.leighb.com/tounicupper.htm
+//
+// The tables were constructed from
+// http://publib.boulder.ibm.com/infocenter/iseries/v7r1m0/index.jsp?topic=%2Fnls%2Frbagslowtoupmaptable.htm
+
+static constexpr wchar_t unicode_lowers[] = {
+ (wchar_t)0x0061, (wchar_t)0x0062, (wchar_t)0x0063, (wchar_t)0x0064, (wchar_t)0x0065, (wchar_t)0x0066, (wchar_t)0x0067, (wchar_t)0x0068, (wchar_t)0x0069,
+ (wchar_t)0x006A, (wchar_t)0x006B, (wchar_t)0x006C, (wchar_t)0x006D, (wchar_t)0x006E, (wchar_t)0x006F, (wchar_t)0x0070, (wchar_t)0x0071, (wchar_t)0x0072,
+ (wchar_t)0x0073, (wchar_t)0x0074, (wchar_t)0x0075, (wchar_t)0x0076, (wchar_t)0x0077, (wchar_t)0x0078, (wchar_t)0x0079, (wchar_t)0x007A, (wchar_t)0x00E0,
+ (wchar_t)0x00E1, (wchar_t)0x00E2, (wchar_t)0x00E3, (wchar_t)0x00E4, (wchar_t)0x00E5, (wchar_t)0x00E6, (wchar_t)0x00E7, (wchar_t)0x00E8, (wchar_t)0x00E9,
+ (wchar_t)0x00EA, (wchar_t)0x00EB, (wchar_t)0x00EC, (wchar_t)0x00ED, (wchar_t)0x00EE, (wchar_t)0x00EF, (wchar_t)0x00F0, (wchar_t)0x00F1, (wchar_t)0x00F2,
+ (wchar_t)0x00F3, (wchar_t)0x00F4, (wchar_t)0x00F5, (wchar_t)0x00F6, (wchar_t)0x00F8, (wchar_t)0x00F9, (wchar_t)0x00FA, (wchar_t)0x00FB, (wchar_t)0x00FC,
+ (wchar_t)0x00FD, (wchar_t)0x00FE, (wchar_t)0x00FF, (wchar_t)0x0101, (wchar_t)0x0103, (wchar_t)0x0105, (wchar_t)0x0107, (wchar_t)0x0109, (wchar_t)0x010B,
+ (wchar_t)0x010D, (wchar_t)0x010F, (wchar_t)0x0111, (wchar_t)0x0113, (wchar_t)0x0115, (wchar_t)0x0117, (wchar_t)0x0119, (wchar_t)0x011B, (wchar_t)0x011D,
+ (wchar_t)0x011F, (wchar_t)0x0121, (wchar_t)0x0123, (wchar_t)0x0125, (wchar_t)0x0127, (wchar_t)0x0129, (wchar_t)0x012B, (wchar_t)0x012D, (wchar_t)0x012F,
+ (wchar_t)0x0131, (wchar_t)0x0133, (wchar_t)0x0135, (wchar_t)0x0137, (wchar_t)0x013A, (wchar_t)0x013C, (wchar_t)0x013E, (wchar_t)0x0140, (wchar_t)0x0142,
+ (wchar_t)0x0144, (wchar_t)0x0146, (wchar_t)0x0148, (wchar_t)0x014B, (wchar_t)0x014D, (wchar_t)0x014F, (wchar_t)0x0151, (wchar_t)0x0153, (wchar_t)0x0155,
+ (wchar_t)0x0157, (wchar_t)0x0159, (wchar_t)0x015B, (wchar_t)0x015D, (wchar_t)0x015F, (wchar_t)0x0161, (wchar_t)0x0163, (wchar_t)0x0165, (wchar_t)0x0167,
+ (wchar_t)0x0169, (wchar_t)0x016B, (wchar_t)0x016D, (wchar_t)0x016F, (wchar_t)0x0171, (wchar_t)0x0173, (wchar_t)0x0175, (wchar_t)0x0177, (wchar_t)0x017A,
+ (wchar_t)0x017C, (wchar_t)0x017E, (wchar_t)0x0183, (wchar_t)0x0185, (wchar_t)0x0188, (wchar_t)0x018C, (wchar_t)0x0192, (wchar_t)0x0199, (wchar_t)0x01A1,
+ (wchar_t)0x01A3, (wchar_t)0x01A5, (wchar_t)0x01A8, (wchar_t)0x01AD, (wchar_t)0x01B0, (wchar_t)0x01B4, (wchar_t)0x01B6, (wchar_t)0x01B9, (wchar_t)0x01BD,
+ (wchar_t)0x01C6, (wchar_t)0x01C9, (wchar_t)0x01CC, (wchar_t)0x01CE, (wchar_t)0x01D0, (wchar_t)0x01D2, (wchar_t)0x01D4, (wchar_t)0x01D6, (wchar_t)0x01D8,
+ (wchar_t)0x01DA, (wchar_t)0x01DC, (wchar_t)0x01DF, (wchar_t)0x01E1, (wchar_t)0x01E3, (wchar_t)0x01E5, (wchar_t)0x01E7, (wchar_t)0x01E9, (wchar_t)0x01EB,
+ (wchar_t)0x01ED, (wchar_t)0x01EF, (wchar_t)0x01F3, (wchar_t)0x01F5, (wchar_t)0x01FB, (wchar_t)0x01FD, (wchar_t)0x01FF, (wchar_t)0x0201, (wchar_t)0x0203,
+ (wchar_t)0x0205, (wchar_t)0x0207, (wchar_t)0x0209, (wchar_t)0x020B, (wchar_t)0x020D, (wchar_t)0x020F, (wchar_t)0x0211, (wchar_t)0x0213, (wchar_t)0x0215,
+ (wchar_t)0x0217, (wchar_t)0x0253, (wchar_t)0x0254, (wchar_t)0x0257, (wchar_t)0x0258, (wchar_t)0x0259, (wchar_t)0x025B, (wchar_t)0x0260, (wchar_t)0x0263,
+ (wchar_t)0x0268, (wchar_t)0x0269, (wchar_t)0x026F, (wchar_t)0x0272, (wchar_t)0x0275, (wchar_t)0x0283, (wchar_t)0x0288, (wchar_t)0x028A, (wchar_t)0x028B,
+ (wchar_t)0x0292, (wchar_t)0x03AC, (wchar_t)0x03AD, (wchar_t)0x03AE, (wchar_t)0x03AF, (wchar_t)0x03B1, (wchar_t)0x03B2, (wchar_t)0x03B3, (wchar_t)0x03B4,
+ (wchar_t)0x03B5, (wchar_t)0x03B6, (wchar_t)0x03B7, (wchar_t)0x03B8, (wchar_t)0x03B9, (wchar_t)0x03BA, (wchar_t)0x03BB, (wchar_t)0x03BC, (wchar_t)0x03BD,
+ (wchar_t)0x03BE, (wchar_t)0x03BF, (wchar_t)0x03C0, (wchar_t)0x03C1, (wchar_t)0x03C3, (wchar_t)0x03C4, (wchar_t)0x03C5, (wchar_t)0x03C6, (wchar_t)0x03C7,
+ (wchar_t)0x03C8, (wchar_t)0x03C9, (wchar_t)0x03CA, (wchar_t)0x03CB, (wchar_t)0x03CC, (wchar_t)0x03CD, (wchar_t)0x03CE, (wchar_t)0x03E3, (wchar_t)0x03E5,
+ (wchar_t)0x03E7, (wchar_t)0x03E9, (wchar_t)0x03EB, (wchar_t)0x03ED, (wchar_t)0x03EF, (wchar_t)0x0430, (wchar_t)0x0431, (wchar_t)0x0432, (wchar_t)0x0433,
+ (wchar_t)0x0434, (wchar_t)0x0435, (wchar_t)0x0436, (wchar_t)0x0437, (wchar_t)0x0438, (wchar_t)0x0439, (wchar_t)0x043A, (wchar_t)0x043B, (wchar_t)0x043C,
+ (wchar_t)0x043D, (wchar_t)0x043E, (wchar_t)0x043F, (wchar_t)0x0440, (wchar_t)0x0441, (wchar_t)0x0442, (wchar_t)0x0443, (wchar_t)0x0444, (wchar_t)0x0445,
+ (wchar_t)0x0446, (wchar_t)0x0447, (wchar_t)0x0448, (wchar_t)0x0449, (wchar_t)0x044A, (wchar_t)0x044B, (wchar_t)0x044C, (wchar_t)0x044D, (wchar_t)0x044E,
+ (wchar_t)0x044F, (wchar_t)0x0451, (wchar_t)0x0452, (wchar_t)0x0453, (wchar_t)0x0454, (wchar_t)0x0455, (wchar_t)0x0456, (wchar_t)0x0457, (wchar_t)0x0458,
+ (wchar_t)0x0459, (wchar_t)0x045A, (wchar_t)0x045B, (wchar_t)0x045C, (wchar_t)0x045E, (wchar_t)0x045F, (wchar_t)0x0461, (wchar_t)0x0463, (wchar_t)0x0465,
+ (wchar_t)0x0467, (wchar_t)0x0469, (wchar_t)0x046B, (wchar_t)0x046D, (wchar_t)0x046F, (wchar_t)0x0471, (wchar_t)0x0473, (wchar_t)0x0475, (wchar_t)0x0477,
+ (wchar_t)0x0479, (wchar_t)0x047B, (wchar_t)0x047D, (wchar_t)0x047F, (wchar_t)0x0481, (wchar_t)0x0491, (wchar_t)0x0493, (wchar_t)0x0495, (wchar_t)0x0497,
+ (wchar_t)0x0499, (wchar_t)0x049B, (wchar_t)0x049D, (wchar_t)0x049F, (wchar_t)0x04A1, (wchar_t)0x04A3, (wchar_t)0x04A5, (wchar_t)0x04A7, (wchar_t)0x04A9,
+ (wchar_t)0x04AB, (wchar_t)0x04AD, (wchar_t)0x04AF, (wchar_t)0x04B1, (wchar_t)0x04B3, (wchar_t)0x04B5, (wchar_t)0x04B7, (wchar_t)0x04B9, (wchar_t)0x04BB,
+ (wchar_t)0x04BD, (wchar_t)0x04BF, (wchar_t)0x04C2, (wchar_t)0x04C4, (wchar_t)0x04C8, (wchar_t)0x04CC, (wchar_t)0x04D1, (wchar_t)0x04D3, (wchar_t)0x04D5,
+ (wchar_t)0x04D7, (wchar_t)0x04D9, (wchar_t)0x04DB, (wchar_t)0x04DD, (wchar_t)0x04DF, (wchar_t)0x04E1, (wchar_t)0x04E3, (wchar_t)0x04E5, (wchar_t)0x04E7,
+ (wchar_t)0x04E9, (wchar_t)0x04EB, (wchar_t)0x04EF, (wchar_t)0x04F1, (wchar_t)0x04F3, (wchar_t)0x04F5, (wchar_t)0x04F9, (wchar_t)0x0561, (wchar_t)0x0562,
+ (wchar_t)0x0563, (wchar_t)0x0564, (wchar_t)0x0565, (wchar_t)0x0566, (wchar_t)0x0567, (wchar_t)0x0568, (wchar_t)0x0569, (wchar_t)0x056A, (wchar_t)0x056B,
+ (wchar_t)0x056C, (wchar_t)0x056D, (wchar_t)0x056E, (wchar_t)0x056F, (wchar_t)0x0570, (wchar_t)0x0571, (wchar_t)0x0572, (wchar_t)0x0573, (wchar_t)0x0574,
+ (wchar_t)0x0575, (wchar_t)0x0576, (wchar_t)0x0577, (wchar_t)0x0578, (wchar_t)0x0579, (wchar_t)0x057A, (wchar_t)0x057B, (wchar_t)0x057C, (wchar_t)0x057D,
+ (wchar_t)0x057E, (wchar_t)0x057F, (wchar_t)0x0580, (wchar_t)0x0581, (wchar_t)0x0582, (wchar_t)0x0583, (wchar_t)0x0584, (wchar_t)0x0585, (wchar_t)0x0586,
+ (wchar_t)0x10D0, (wchar_t)0x10D1, (wchar_t)0x10D2, (wchar_t)0x10D3, (wchar_t)0x10D4, (wchar_t)0x10D5, (wchar_t)0x10D6, (wchar_t)0x10D7, (wchar_t)0x10D8,
+ (wchar_t)0x10D9, (wchar_t)0x10DA, (wchar_t)0x10DB, (wchar_t)0x10DC, (wchar_t)0x10DD, (wchar_t)0x10DE, (wchar_t)0x10DF, (wchar_t)0x10E0, (wchar_t)0x10E1,
+ (wchar_t)0x10E2, (wchar_t)0x10E3, (wchar_t)0x10E4, (wchar_t)0x10E5, (wchar_t)0x10E6, (wchar_t)0x10E7, (wchar_t)0x10E8, (wchar_t)0x10E9, (wchar_t)0x10EA,
+ (wchar_t)0x10EB, (wchar_t)0x10EC, (wchar_t)0x10ED, (wchar_t)0x10EE, (wchar_t)0x10EF, (wchar_t)0x10F0, (wchar_t)0x10F1, (wchar_t)0x10F2, (wchar_t)0x10F3,
+ (wchar_t)0x10F4, (wchar_t)0x10F5, (wchar_t)0x1E01, (wchar_t)0x1E03, (wchar_t)0x1E05, (wchar_t)0x1E07, (wchar_t)0x1E09, (wchar_t)0x1E0B, (wchar_t)0x1E0D,
+ (wchar_t)0x1E0F, (wchar_t)0x1E11, (wchar_t)0x1E13, (wchar_t)0x1E15, (wchar_t)0x1E17, (wchar_t)0x1E19, (wchar_t)0x1E1B, (wchar_t)0x1E1D, (wchar_t)0x1E1F,
+ (wchar_t)0x1E21, (wchar_t)0x1E23, (wchar_t)0x1E25, (wchar_t)0x1E27, (wchar_t)0x1E29, (wchar_t)0x1E2B, (wchar_t)0x1E2D, (wchar_t)0x1E2F, (wchar_t)0x1E31,
+ (wchar_t)0x1E33, (wchar_t)0x1E35, (wchar_t)0x1E37, (wchar_t)0x1E39, (wchar_t)0x1E3B, (wchar_t)0x1E3D, (wchar_t)0x1E3F, (wchar_t)0x1E41, (wchar_t)0x1E43,
+ (wchar_t)0x1E45, (wchar_t)0x1E47, (wchar_t)0x1E49, (wchar_t)0x1E4B, (wchar_t)0x1E4D, (wchar_t)0x1E4F, (wchar_t)0x1E51, (wchar_t)0x1E53, (wchar_t)0x1E55,
+ (wchar_t)0x1E57, (wchar_t)0x1E59, (wchar_t)0x1E5B, (wchar_t)0x1E5D, (wchar_t)0x1E5F, (wchar_t)0x1E61, (wchar_t)0x1E63, (wchar_t)0x1E65, (wchar_t)0x1E67,
+ (wchar_t)0x1E69, (wchar_t)0x1E6B, (wchar_t)0x1E6D, (wchar_t)0x1E6F, (wchar_t)0x1E71, (wchar_t)0x1E73, (wchar_t)0x1E75, (wchar_t)0x1E77, (wchar_t)0x1E79,
+ (wchar_t)0x1E7B, (wchar_t)0x1E7D, (wchar_t)0x1E7F, (wchar_t)0x1E81, (wchar_t)0x1E83, (wchar_t)0x1E85, (wchar_t)0x1E87, (wchar_t)0x1E89, (wchar_t)0x1E8B,
+ (wchar_t)0x1E8D, (wchar_t)0x1E8F, (wchar_t)0x1E91, (wchar_t)0x1E93, (wchar_t)0x1E95, (wchar_t)0x1EA1, (wchar_t)0x1EA3, (wchar_t)0x1EA5, (wchar_t)0x1EA7,
+ (wchar_t)0x1EA9, (wchar_t)0x1EAB, (wchar_t)0x1EAD, (wchar_t)0x1EAF, (wchar_t)0x1EB1, (wchar_t)0x1EB3, (wchar_t)0x1EB5, (wchar_t)0x1EB7, (wchar_t)0x1EB9,
+ (wchar_t)0x1EBB, (wchar_t)0x1EBD, (wchar_t)0x1EBF, (wchar_t)0x1EC1, (wchar_t)0x1EC3, (wchar_t)0x1EC5, (wchar_t)0x1EC7, (wchar_t)0x1EC9, (wchar_t)0x1ECB,
+ (wchar_t)0x1ECD, (wchar_t)0x1ECF, (wchar_t)0x1ED1, (wchar_t)0x1ED3, (wchar_t)0x1ED5, (wchar_t)0x1ED7, (wchar_t)0x1ED9, (wchar_t)0x1EDB, (wchar_t)0x1EDD,
+ (wchar_t)0x1EDF, (wchar_t)0x1EE1, (wchar_t)0x1EE3, (wchar_t)0x1EE5, (wchar_t)0x1EE7, (wchar_t)0x1EE9, (wchar_t)0x1EEB, (wchar_t)0x1EED, (wchar_t)0x1EEF,
+ (wchar_t)0x1EF1, (wchar_t)0x1EF3, (wchar_t)0x1EF5, (wchar_t)0x1EF7, (wchar_t)0x1EF9, (wchar_t)0x1F00, (wchar_t)0x1F01, (wchar_t)0x1F02, (wchar_t)0x1F03,
+ (wchar_t)0x1F04, (wchar_t)0x1F05, (wchar_t)0x1F06, (wchar_t)0x1F07, (wchar_t)0x1F10, (wchar_t)0x1F11, (wchar_t)0x1F12, (wchar_t)0x1F13, (wchar_t)0x1F14,
+ (wchar_t)0x1F15, (wchar_t)0x1F20, (wchar_t)0x1F21, (wchar_t)0x1F22, (wchar_t)0x1F23, (wchar_t)0x1F24, (wchar_t)0x1F25, (wchar_t)0x1F26, (wchar_t)0x1F27,
+ (wchar_t)0x1F30, (wchar_t)0x1F31, (wchar_t)0x1F32, (wchar_t)0x1F33, (wchar_t)0x1F34, (wchar_t)0x1F35, (wchar_t)0x1F36, (wchar_t)0x1F37, (wchar_t)0x1F40,
+ (wchar_t)0x1F41, (wchar_t)0x1F42, (wchar_t)0x1F43, (wchar_t)0x1F44, (wchar_t)0x1F45, (wchar_t)0x1F51, (wchar_t)0x1F53, (wchar_t)0x1F55, (wchar_t)0x1F57,
+ (wchar_t)0x1F60, (wchar_t)0x1F61, (wchar_t)0x1F62, (wchar_t)0x1F63, (wchar_t)0x1F64, (wchar_t)0x1F65, (wchar_t)0x1F66, (wchar_t)0x1F67, (wchar_t)0x1F80,
+ (wchar_t)0x1F81, (wchar_t)0x1F82, (wchar_t)0x1F83, (wchar_t)0x1F84, (wchar_t)0x1F85, (wchar_t)0x1F86, (wchar_t)0x1F87, (wchar_t)0x1F90, (wchar_t)0x1F91,
+ (wchar_t)0x1F92, (wchar_t)0x1F93, (wchar_t)0x1F94, (wchar_t)0x1F95, (wchar_t)0x1F96, (wchar_t)0x1F97, (wchar_t)0x1FA0, (wchar_t)0x1FA1, (wchar_t)0x1FA2,
+ (wchar_t)0x1FA3, (wchar_t)0x1FA4, (wchar_t)0x1FA5, (wchar_t)0x1FA6, (wchar_t)0x1FA7, (wchar_t)0x1FB0, (wchar_t)0x1FB1, (wchar_t)0x1FD0, (wchar_t)0x1FD1,
+ (wchar_t)0x1FE0, (wchar_t)0x1FE1, (wchar_t)0x24D0, (wchar_t)0x24D1, (wchar_t)0x24D2, (wchar_t)0x24D3, (wchar_t)0x24D4, (wchar_t)0x24D5, (wchar_t)0x24D6,
+ (wchar_t)0x24D7, (wchar_t)0x24D8, (wchar_t)0x24D9, (wchar_t)0x24DA, (wchar_t)0x24DB, (wchar_t)0x24DC, (wchar_t)0x24DD, (wchar_t)0x24DE, (wchar_t)0x24DF,
+ (wchar_t)0x24E0, (wchar_t)0x24E1, (wchar_t)0x24E2, (wchar_t)0x24E3, (wchar_t)0x24E4, (wchar_t)0x24E5, (wchar_t)0x24E6, (wchar_t)0x24E7, (wchar_t)0x24E8,
+ (wchar_t)0x24E9, (wchar_t)0xFF41, (wchar_t)0xFF42, (wchar_t)0xFF43, (wchar_t)0xFF44, (wchar_t)0xFF45, (wchar_t)0xFF46, (wchar_t)0xFF47, (wchar_t)0xFF48,
+ (wchar_t)0xFF49, (wchar_t)0xFF4A, (wchar_t)0xFF4B, (wchar_t)0xFF4C, (wchar_t)0xFF4D, (wchar_t)0xFF4E, (wchar_t)0xFF4F, (wchar_t)0xFF50, (wchar_t)0xFF51,
+ (wchar_t)0xFF52, (wchar_t)0xFF53, (wchar_t)0xFF54, (wchar_t)0xFF55, (wchar_t)0xFF56, (wchar_t)0xFF57, (wchar_t)0xFF58, (wchar_t)0xFF59, (wchar_t)0xFF5A
+};
+
+static const wchar_t unicode_uppers[] = {
+ (wchar_t)0x0041, (wchar_t)0x0042, (wchar_t)0x0043, (wchar_t)0x0044, (wchar_t)0x0045, (wchar_t)0x0046, (wchar_t)0x0047, (wchar_t)0x0048, (wchar_t)0x0049,
+ (wchar_t)0x004A, (wchar_t)0x004B, (wchar_t)0x004C, (wchar_t)0x004D, (wchar_t)0x004E, (wchar_t)0x004F, (wchar_t)0x0050, (wchar_t)0x0051, (wchar_t)0x0052,
+ (wchar_t)0x0053, (wchar_t)0x0054, (wchar_t)0x0055, (wchar_t)0x0056, (wchar_t)0x0057, (wchar_t)0x0058, (wchar_t)0x0059, (wchar_t)0x005A, (wchar_t)0x00C0,
+ (wchar_t)0x00C1, (wchar_t)0x00C2, (wchar_t)0x00C3, (wchar_t)0x00C4, (wchar_t)0x00C5, (wchar_t)0x00C6, (wchar_t)0x00C7, (wchar_t)0x00C8, (wchar_t)0x00C9,
+ (wchar_t)0x00CA, (wchar_t)0x00CB, (wchar_t)0x00CC, (wchar_t)0x00CD, (wchar_t)0x00CE, (wchar_t)0x00CF, (wchar_t)0x00D0, (wchar_t)0x00D1, (wchar_t)0x00D2,
+ (wchar_t)0x00D3, (wchar_t)0x00D4, (wchar_t)0x00D5, (wchar_t)0x00D6, (wchar_t)0x00D8, (wchar_t)0x00D9, (wchar_t)0x00DA, (wchar_t)0x00DB, (wchar_t)0x00DC,
+ (wchar_t)0x00DD, (wchar_t)0x00DE, (wchar_t)0x0178, (wchar_t)0x0100, (wchar_t)0x0102, (wchar_t)0x0104, (wchar_t)0x0106, (wchar_t)0x0108, (wchar_t)0x010A,
+ (wchar_t)0x010C, (wchar_t)0x010E, (wchar_t)0x0110, (wchar_t)0x0112, (wchar_t)0x0114, (wchar_t)0x0116, (wchar_t)0x0118, (wchar_t)0x011A, (wchar_t)0x011C,
+ (wchar_t)0x011E, (wchar_t)0x0120, (wchar_t)0x0122, (wchar_t)0x0124, (wchar_t)0x0126, (wchar_t)0x0128, (wchar_t)0x012A, (wchar_t)0x012C, (wchar_t)0x012E,
+ (wchar_t)0x0049, (wchar_t)0x0132, (wchar_t)0x0134, (wchar_t)0x0136, (wchar_t)0x0139, (wchar_t)0x013B, (wchar_t)0x013D, (wchar_t)0x013F, (wchar_t)0x0141,
+ (wchar_t)0x0143, (wchar_t)0x0145, (wchar_t)0x0147, (wchar_t)0x014A, (wchar_t)0x014C, (wchar_t)0x014E, (wchar_t)0x0150, (wchar_t)0x0152, (wchar_t)0x0154,
+ (wchar_t)0x0156, (wchar_t)0x0158, (wchar_t)0x015A, (wchar_t)0x015C, (wchar_t)0x015E, (wchar_t)0x0160, (wchar_t)0x0162, (wchar_t)0x0164, (wchar_t)0x0166,
+ (wchar_t)0x0168, (wchar_t)0x016A, (wchar_t)0x016C, (wchar_t)0x016E, (wchar_t)0x0170, (wchar_t)0x0172, (wchar_t)0x0174, (wchar_t)0x0176, (wchar_t)0x0179,
+ (wchar_t)0x017B, (wchar_t)0x017D, (wchar_t)0x0182, (wchar_t)0x0184, (wchar_t)0x0187, (wchar_t)0x018B, (wchar_t)0x0191, (wchar_t)0x0198, (wchar_t)0x01A0,
+ (wchar_t)0x01A2, (wchar_t)0x01A4, (wchar_t)0x01A7, (wchar_t)0x01AC, (wchar_t)0x01AF, (wchar_t)0x01B3, (wchar_t)0x01B5, (wchar_t)0x01B8, (wchar_t)0x01BC,
+ (wchar_t)0x01C4, (wchar_t)0x01C7, (wchar_t)0x01CA, (wchar_t)0x01CD, (wchar_t)0x01CF, (wchar_t)0x01D1, (wchar_t)0x01D3, (wchar_t)0x01D5, (wchar_t)0x01D7,
+ (wchar_t)0x01D9, (wchar_t)0x01DB, (wchar_t)0x01DE, (wchar_t)0x01E0, (wchar_t)0x01E2, (wchar_t)0x01E4, (wchar_t)0x01E6, (wchar_t)0x01E8, (wchar_t)0x01EA,
+ (wchar_t)0x01EC, (wchar_t)0x01EE, (wchar_t)0x01F1, (wchar_t)0x01F4, (wchar_t)0x01FA, (wchar_t)0x01FC, (wchar_t)0x01FE, (wchar_t)0x0200, (wchar_t)0x0202,
+ (wchar_t)0x0204, (wchar_t)0x0206, (wchar_t)0x0208, (wchar_t)0x020A, (wchar_t)0x020C, (wchar_t)0x020E, (wchar_t)0x0210, (wchar_t)0x0212, (wchar_t)0x0214,
+ (wchar_t)0x0216, (wchar_t)0x0181, (wchar_t)0x0186, (wchar_t)0x018A, (wchar_t)0x018E, (wchar_t)0x018F, (wchar_t)0x0190, (wchar_t)0x0193, (wchar_t)0x0194,
+ (wchar_t)0x0197, (wchar_t)0x0196, (wchar_t)0x019C, (wchar_t)0x019D, (wchar_t)0x019F, (wchar_t)0x01A9, (wchar_t)0x01AE, (wchar_t)0x01B1, (wchar_t)0x01B2,
+ (wchar_t)0x01B7, (wchar_t)0x0386, (wchar_t)0x0388, (wchar_t)0x0389, (wchar_t)0x038A, (wchar_t)0x0391, (wchar_t)0x0392, (wchar_t)0x0393, (wchar_t)0x0394,
+ (wchar_t)0x0395, (wchar_t)0x0396, (wchar_t)0x0397, (wchar_t)0x0398, (wchar_t)0x0399, (wchar_t)0x039A, (wchar_t)0x039B, (wchar_t)0x039C, (wchar_t)0x039D,
+ (wchar_t)0x039E, (wchar_t)0x039F, (wchar_t)0x03A0, (wchar_t)0x03A1, (wchar_t)0x03A3, (wchar_t)0x03A4, (wchar_t)0x03A5, (wchar_t)0x03A6, (wchar_t)0x03A7,
+ (wchar_t)0x03A8, (wchar_t)0x03A9, (wchar_t)0x03AA, (wchar_t)0x03AB, (wchar_t)0x038C, (wchar_t)0x038E, (wchar_t)0x038F, (wchar_t)0x03E2, (wchar_t)0x03E4,
+ (wchar_t)0x03E6, (wchar_t)0x03E8, (wchar_t)0x03EA, (wchar_t)0x03EC, (wchar_t)0x03EE, (wchar_t)0x0410, (wchar_t)0x0411, (wchar_t)0x0412, (wchar_t)0x0413,
+ (wchar_t)0x0414, (wchar_t)0x0415, (wchar_t)0x0416, (wchar_t)0x0417, (wchar_t)0x0418, (wchar_t)0x0419, (wchar_t)0x041A, (wchar_t)0x041B, (wchar_t)0x041C,
+ (wchar_t)0x041D, (wchar_t)0x041E, (wchar_t)0x041F, (wchar_t)0x0420, (wchar_t)0x0421, (wchar_t)0x0422, (wchar_t)0x0423, (wchar_t)0x0424, (wchar_t)0x0425,
+ (wchar_t)0x0426, (wchar_t)0x0427, (wchar_t)0x0428, (wchar_t)0x0429, (wchar_t)0x042A, (wchar_t)0x042B, (wchar_t)0x042C, (wchar_t)0x042D, (wchar_t)0x042E,
+ (wchar_t)0x042F, (wchar_t)0x0401, (wchar_t)0x0402, (wchar_t)0x0403, (wchar_t)0x0404, (wchar_t)0x0405, (wchar_t)0x0406, (wchar_t)0x0407, (wchar_t)0x0408,
+ (wchar_t)0x0409, (wchar_t)0x040A, (wchar_t)0x040B, (wchar_t)0x040C, (wchar_t)0x040E, (wchar_t)0x040F, (wchar_t)0x0460, (wchar_t)0x0462, (wchar_t)0x0464,
+ (wchar_t)0x0466, (wchar_t)0x0468, (wchar_t)0x046A, (wchar_t)0x046C, (wchar_t)0x046E, (wchar_t)0x0470, (wchar_t)0x0472, (wchar_t)0x0474, (wchar_t)0x0476,
+ (wchar_t)0x0478, (wchar_t)0x047A, (wchar_t)0x047C, (wchar_t)0x047E, (wchar_t)0x0480, (wchar_t)0x0490, (wchar_t)0x0492, (wchar_t)0x0494, (wchar_t)0x0496,
+ (wchar_t)0x0498, (wchar_t)0x049A, (wchar_t)0x049C, (wchar_t)0x049E, (wchar_t)0x04A0, (wchar_t)0x04A2, (wchar_t)0x04A4, (wchar_t)0x04A6, (wchar_t)0x04A8,
+ (wchar_t)0x04AA, (wchar_t)0x04AC, (wchar_t)0x04AE, (wchar_t)0x04B0, (wchar_t)0x04B2, (wchar_t)0x04B4, (wchar_t)0x04B6, (wchar_t)0x04B8, (wchar_t)0x04BA,
+ (wchar_t)0x04BC, (wchar_t)0x04BE, (wchar_t)0x04C1, (wchar_t)0x04C3, (wchar_t)0x04C7, (wchar_t)0x04CB, (wchar_t)0x04D0, (wchar_t)0x04D2, (wchar_t)0x04D4,
+ (wchar_t)0x04D6, (wchar_t)0x04D8, (wchar_t)0x04DA, (wchar_t)0x04DC, (wchar_t)0x04DE, (wchar_t)0x04E0, (wchar_t)0x04E2, (wchar_t)0x04E4, (wchar_t)0x04E6,
+ (wchar_t)0x04E8, (wchar_t)0x04EA, (wchar_t)0x04EE, (wchar_t)0x04F0, (wchar_t)0x04F2, (wchar_t)0x04F4, (wchar_t)0x04F8, (wchar_t)0x0531, (wchar_t)0x0532,
+ (wchar_t)0x0533, (wchar_t)0x0534, (wchar_t)0x0535, (wchar_t)0x0536, (wchar_t)0x0537, (wchar_t)0x0538, (wchar_t)0x0539, (wchar_t)0x053A, (wchar_t)0x053B,
+ (wchar_t)0x053C, (wchar_t)0x053D, (wchar_t)0x053E, (wchar_t)0x053F, (wchar_t)0x0540, (wchar_t)0x0541, (wchar_t)0x0542, (wchar_t)0x0543, (wchar_t)0x0544,
+ (wchar_t)0x0545, (wchar_t)0x0546, (wchar_t)0x0547, (wchar_t)0x0548, (wchar_t)0x0549, (wchar_t)0x054A, (wchar_t)0x054B, (wchar_t)0x054C, (wchar_t)0x054D,
+ (wchar_t)0x054E, (wchar_t)0x054F, (wchar_t)0x0550, (wchar_t)0x0551, (wchar_t)0x0552, (wchar_t)0x0553, (wchar_t)0x0554, (wchar_t)0x0555, (wchar_t)0x0556,
+ (wchar_t)0x10A0, (wchar_t)0x10A1, (wchar_t)0x10A2, (wchar_t)0x10A3, (wchar_t)0x10A4, (wchar_t)0x10A5, (wchar_t)0x10A6, (wchar_t)0x10A7, (wchar_t)0x10A8,
+ (wchar_t)0x10A9, (wchar_t)0x10AA, (wchar_t)0x10AB, (wchar_t)0x10AC, (wchar_t)0x10AD, (wchar_t)0x10AE, (wchar_t)0x10AF, (wchar_t)0x10B0, (wchar_t)0x10B1,
+ (wchar_t)0x10B2, (wchar_t)0x10B3, (wchar_t)0x10B4, (wchar_t)0x10B5, (wchar_t)0x10B6, (wchar_t)0x10B7, (wchar_t)0x10B8, (wchar_t)0x10B9, (wchar_t)0x10BA,
+ (wchar_t)0x10BB, (wchar_t)0x10BC, (wchar_t)0x10BD, (wchar_t)0x10BE, (wchar_t)0x10BF, (wchar_t)0x10C0, (wchar_t)0x10C1, (wchar_t)0x10C2, (wchar_t)0x10C3,
+ (wchar_t)0x10C4, (wchar_t)0x10C5, (wchar_t)0x1E00, (wchar_t)0x1E02, (wchar_t)0x1E04, (wchar_t)0x1E06, (wchar_t)0x1E08, (wchar_t)0x1E0A, (wchar_t)0x1E0C,
+ (wchar_t)0x1E0E, (wchar_t)0x1E10, (wchar_t)0x1E12, (wchar_t)0x1E14, (wchar_t)0x1E16, (wchar_t)0x1E18, (wchar_t)0x1E1A, (wchar_t)0x1E1C, (wchar_t)0x1E1E,
+ (wchar_t)0x1E20, (wchar_t)0x1E22, (wchar_t)0x1E24, (wchar_t)0x1E26, (wchar_t)0x1E28, (wchar_t)0x1E2A, (wchar_t)0x1E2C, (wchar_t)0x1E2E, (wchar_t)0x1E30,
+ (wchar_t)0x1E32, (wchar_t)0x1E34, (wchar_t)0x1E36, (wchar_t)0x1E38, (wchar_t)0x1E3A, (wchar_t)0x1E3C, (wchar_t)0x1E3E, (wchar_t)0x1E40, (wchar_t)0x1E42,
+ (wchar_t)0x1E44, (wchar_t)0x1E46, (wchar_t)0x1E48, (wchar_t)0x1E4A, (wchar_t)0x1E4C, (wchar_t)0x1E4E, (wchar_t)0x1E50, (wchar_t)0x1E52, (wchar_t)0x1E54,
+ (wchar_t)0x1E56, (wchar_t)0x1E58, (wchar_t)0x1E5A, (wchar_t)0x1E5C, (wchar_t)0x1E5E, (wchar_t)0x1E60, (wchar_t)0x1E62, (wchar_t)0x1E64, (wchar_t)0x1E66,
+ (wchar_t)0x1E68, (wchar_t)0x1E6A, (wchar_t)0x1E6C, (wchar_t)0x1E6E, (wchar_t)0x1E70, (wchar_t)0x1E72, (wchar_t)0x1E74, (wchar_t)0x1E76, (wchar_t)0x1E78,
+ (wchar_t)0x1E7A, (wchar_t)0x1E7C, (wchar_t)0x1E7E, (wchar_t)0x1E80, (wchar_t)0x1E82, (wchar_t)0x1E84, (wchar_t)0x1E86, (wchar_t)0x1E88, (wchar_t)0x1E8A,
+ (wchar_t)0x1E8C, (wchar_t)0x1E8E, (wchar_t)0x1E90, (wchar_t)0x1E92, (wchar_t)0x1E94, (wchar_t)0x1EA0, (wchar_t)0x1EA2, (wchar_t)0x1EA4, (wchar_t)0x1EA6,
+ (wchar_t)0x1EA8, (wchar_t)0x1EAA, (wchar_t)0x1EAC, (wchar_t)0x1EAE, (wchar_t)0x1EB0, (wchar_t)0x1EB2, (wchar_t)0x1EB4, (wchar_t)0x1EB6, (wchar_t)0x1EB8,
+ (wchar_t)0x1EBA, (wchar_t)0x1EBC, (wchar_t)0x1EBE, (wchar_t)0x1EC0, (wchar_t)0x1EC2, (wchar_t)0x1EC4, (wchar_t)0x1EC6, (wchar_t)0x1EC8, (wchar_t)0x1ECA,
+ (wchar_t)0x1ECC, (wchar_t)0x1ECE, (wchar_t)0x1ED0, (wchar_t)0x1ED2, (wchar_t)0x1ED4, (wchar_t)0x1ED6, (wchar_t)0x1ED8, (wchar_t)0x1EDA, (wchar_t)0x1EDC,
+ (wchar_t)0x1EDE, (wchar_t)0x1EE0, (wchar_t)0x1EE2, (wchar_t)0x1EE4, (wchar_t)0x1EE6, (wchar_t)0x1EE8, (wchar_t)0x1EEA, (wchar_t)0x1EEC, (wchar_t)0x1EEE,
+ (wchar_t)0x1EF0, (wchar_t)0x1EF2, (wchar_t)0x1EF4, (wchar_t)0x1EF6, (wchar_t)0x1EF8, (wchar_t)0x1F08, (wchar_t)0x1F09, (wchar_t)0x1F0A, (wchar_t)0x1F0B,
+ (wchar_t)0x1F0C, (wchar_t)0x1F0D, (wchar_t)0x1F0E, (wchar_t)0x1F0F, (wchar_t)0x1F18, (wchar_t)0x1F19, (wchar_t)0x1F1A, (wchar_t)0x1F1B, (wchar_t)0x1F1C,
+ (wchar_t)0x1F1D, (wchar_t)0x1F28, (wchar_t)0x1F29, (wchar_t)0x1F2A, (wchar_t)0x1F2B, (wchar_t)0x1F2C, (wchar_t)0x1F2D, (wchar_t)0x1F2E, (wchar_t)0x1F2F,
+ (wchar_t)0x1F38, (wchar_t)0x1F39, (wchar_t)0x1F3A, (wchar_t)0x1F3B, (wchar_t)0x1F3C, (wchar_t)0x1F3D, (wchar_t)0x1F3E, (wchar_t)0x1F3F, (wchar_t)0x1F48,
+ (wchar_t)0x1F49, (wchar_t)0x1F4A, (wchar_t)0x1F4B, (wchar_t)0x1F4C, (wchar_t)0x1F4D, (wchar_t)0x1F59, (wchar_t)0x1F5B, (wchar_t)0x1F5D, (wchar_t)0x1F5F,
+ (wchar_t)0x1F68, (wchar_t)0x1F69, (wchar_t)0x1F6A, (wchar_t)0x1F6B, (wchar_t)0x1F6C, (wchar_t)0x1F6D, (wchar_t)0x1F6E, (wchar_t)0x1F6F, (wchar_t)0x1F88,
+ (wchar_t)0x1F89, (wchar_t)0x1F8A, (wchar_t)0x1F8B, (wchar_t)0x1F8C, (wchar_t)0x1F8D, (wchar_t)0x1F8E, (wchar_t)0x1F8F, (wchar_t)0x1F98, (wchar_t)0x1F99,
+ (wchar_t)0x1F9A, (wchar_t)0x1F9B, (wchar_t)0x1F9C, (wchar_t)0x1F9D, (wchar_t)0x1F9E, (wchar_t)0x1F9F, (wchar_t)0x1FA8, (wchar_t)0x1FA9, (wchar_t)0x1FAA,
+ (wchar_t)0x1FAB, (wchar_t)0x1FAC, (wchar_t)0x1FAD, (wchar_t)0x1FAE, (wchar_t)0x1FAF, (wchar_t)0x1FB8, (wchar_t)0x1FB9, (wchar_t)0x1FD8, (wchar_t)0x1FD9,
+ (wchar_t)0x1FE8, (wchar_t)0x1FE9, (wchar_t)0x24B6, (wchar_t)0x24B7, (wchar_t)0x24B8, (wchar_t)0x24B9, (wchar_t)0x24BA, (wchar_t)0x24BB, (wchar_t)0x24BC,
+ (wchar_t)0x24BD, (wchar_t)0x24BE, (wchar_t)0x24BF, (wchar_t)0x24C0, (wchar_t)0x24C1, (wchar_t)0x24C2, (wchar_t)0x24C3, (wchar_t)0x24C4, (wchar_t)0x24C5,
+ (wchar_t)0x24C6, (wchar_t)0x24C7, (wchar_t)0x24C8, (wchar_t)0x24C9, (wchar_t)0x24CA, (wchar_t)0x24CB, (wchar_t)0x24CC, (wchar_t)0x24CD, (wchar_t)0x24CE,
+ (wchar_t)0x24CF, (wchar_t)0xFF21, (wchar_t)0xFF22, (wchar_t)0xFF23, (wchar_t)0xFF24, (wchar_t)0xFF25, (wchar_t)0xFF26, (wchar_t)0xFF27, (wchar_t)0xFF28,
+ (wchar_t)0xFF29, (wchar_t)0xFF2A, (wchar_t)0xFF2B, (wchar_t)0xFF2C, (wchar_t)0xFF2D, (wchar_t)0xFF2E, (wchar_t)0xFF2F, (wchar_t)0xFF30, (wchar_t)0xFF31,
+ (wchar_t)0xFF32, (wchar_t)0xFF33, (wchar_t)0xFF34, (wchar_t)0xFF35, (wchar_t)0xFF36, (wchar_t)0xFF37, (wchar_t)0xFF38, (wchar_t)0xFF39, (wchar_t)0xFF3A
+};
+
+
+std::string StringUtils::FormatV(const char *fmt, va_list args)
+{
+ if (!fmt || !fmt[0])
+ return "";
+
+ int size = FORMAT_BLOCK_SIZE;
+ va_list argCopy;
+
+ while (true)
+ {
+ char *cstr = reinterpret_cast<char*>(malloc(sizeof(char) * size));
+ if (!cstr)
+ return "";
+
+ va_copy(argCopy, args);
+ int nActual = vsnprintf(cstr, size, fmt, argCopy);
+ va_end(argCopy);
+
+ if (nActual > -1 && nActual < size) // We got a valid result
+ {
+ std::string str(cstr, nActual);
+ free(cstr);
+ return str;
+ }
+ free(cstr);
+#ifndef TARGET_WINDOWS
+ if (nActual > -1) // Exactly what we will need (glibc 2.1)
+ size = nActual + 1;
+ else // Let's try to double the size (glibc 2.0)
+ size *= 2;
+#else // TARGET_WINDOWS
+ va_copy(argCopy, args);
+ size = _vscprintf(fmt, argCopy);
+ va_end(argCopy);
+ if (size < 0)
+ return "";
+ else
+ size++; // increment for null-termination
+#endif // TARGET_WINDOWS
+ }
+
+ return ""; // unreachable
+}
+
+std::wstring StringUtils::FormatV(const wchar_t *fmt, va_list args)
+{
+ if (!fmt || !fmt[0])
+ return L"";
+
+ int size = FORMAT_BLOCK_SIZE;
+ va_list argCopy;
+
+ while (true)
+ {
+ wchar_t *cstr = reinterpret_cast<wchar_t*>(malloc(sizeof(wchar_t) * size));
+ if (!cstr)
+ return L"";
+
+ va_copy(argCopy, args);
+ int nActual = vswprintf(cstr, size, fmt, argCopy);
+ va_end(argCopy);
+
+ if (nActual > -1 && nActual < size) // We got a valid result
+ {
+ std::wstring str(cstr, nActual);
+ free(cstr);
+ return str;
+ }
+ free(cstr);
+
+#ifndef TARGET_WINDOWS
+ if (nActual > -1) // Exactly what we will need (glibc 2.1)
+ size = nActual + 1;
+ else // Let's try to double the size (glibc 2.0)
+ size *= 2;
+#else // TARGET_WINDOWS
+ va_copy(argCopy, args);
+ size = _vscwprintf(fmt, argCopy);
+ va_end(argCopy);
+ if (size < 0)
+ return L"";
+ else
+ size++; // increment for null-termination
+#endif // TARGET_WINDOWS
+ }
+
+ return L"";
+}
+
+int compareWchar (const void* a, const void* b)
+{
+ if (*(const wchar_t*)a < *(const wchar_t*)b)
+ return -1;
+ else if (*(const wchar_t*)a > *(const wchar_t*)b)
+ return 1;
+ return 0;
+}
+
+wchar_t tolowerUnicode(const wchar_t& c)
+{
+ wchar_t* p = (wchar_t*) bsearch (&c, unicode_uppers, sizeof(unicode_uppers) / sizeof(wchar_t), sizeof(wchar_t), compareWchar);
+ if (p)
+ return *(unicode_lowers + (p - unicode_uppers));
+
+ return c;
+}
+
+wchar_t toupperUnicode(const wchar_t& c)
+{
+ wchar_t* p = (wchar_t*) bsearch (&c, unicode_lowers, sizeof(unicode_lowers) / sizeof(wchar_t), sizeof(wchar_t), compareWchar);
+ if (p)
+ return *(unicode_uppers + (p - unicode_lowers));
+
+ return c;
+}
+
+template<typename Str, typename Fn>
+void transformString(const Str& input, Str& output, Fn fn)
+{
+ std::transform(input.begin(), input.end(), output.begin(), fn);
+}
+
+std::string StringUtils::ToUpper(const std::string& str)
+{
+ std::string result(str.size(), '\0');
+ transformString(str, result, ::toupper);
+ return result;
+}
+
+std::wstring StringUtils::ToUpper(const std::wstring& str)
+{
+ std::wstring result(str.size(), '\0');
+ transformString(str, result, toupperUnicode);
+ return result;
+}
+
+void StringUtils::ToUpper(std::string &str)
+{
+ transformString(str, str, ::toupper);
+}
+
+void StringUtils::ToUpper(std::wstring &str)
+{
+ transformString(str, str, toupperUnicode);
+}
+
+std::string StringUtils::ToLower(const std::string& str)
+{
+ std::string result(str.size(), '\0');
+ transformString(str, result, ::tolower);
+ return result;
+}
+
+std::wstring StringUtils::ToLower(const std::wstring& str)
+{
+ std::wstring result(str.size(), '\0');
+ transformString(str, result, tolowerUnicode);
+ return result;
+}
+
+void StringUtils::ToLower(std::string &str)
+{
+ transformString(str, str, ::tolower);
+}
+
+void StringUtils::ToLower(std::wstring &str)
+{
+ transformString(str, str, tolowerUnicode);
+}
+
+void StringUtils::ToCapitalize(std::string &str)
+{
+ std::wstring wstr;
+ g_charsetConverter.utf8ToW(str, wstr);
+ ToCapitalize(wstr);
+ g_charsetConverter.wToUTF8(wstr, str);
+}
+
+void StringUtils::ToCapitalize(std::wstring &str)
+{
+ const std::locale& loc = g_langInfo.GetSystemLocale();
+ bool isFirstLetter = true;
+ for (std::wstring::iterator it = str.begin(); it < str.end(); ++it)
+ {
+ // capitalize after spaces and punctuation characters (except apostrophes)
+ if (std::isspace(*it, loc) || (std::ispunct(*it, loc) && *it != '\''))
+ isFirstLetter = true;
+ else if (isFirstLetter)
+ {
+ *it = std::toupper(*it, loc);
+ isFirstLetter = false;
+ }
+ }
+}
+
+bool StringUtils::EqualsNoCase(const std::string &str1, const std::string &str2)
+{
+ // before we do the char-by-char comparison, first compare sizes of both strings.
+ // This led to a 33% improvement in benchmarking on average. (size() just returns a member of std::string)
+ if (str1.size() != str2.size())
+ return false;
+ return EqualsNoCase(str1.c_str(), str2.c_str());
+}
+
+bool StringUtils::EqualsNoCase(const std::string &str1, const char *s2)
+{
+ return EqualsNoCase(str1.c_str(), s2);
+}
+
+bool StringUtils::EqualsNoCase(const char *s1, const char *s2)
+{
+ char c2; // we need only one char outside the loop
+ do
+ {
+ const char c1 = *s1++; // const local variable should help compiler to optimize
+ c2 = *s2++;
+ if (c1 != c2 && ::tolower(c1) != ::tolower(c2)) // This includes the possibility that one of the characters is the null-terminator, which implies a string mismatch.
+ return false;
+ } while (c2 != '\0'); // At this point, we know c1 == c2, so there's no need to test them both.
+ return true;
+}
+
+int StringUtils::CompareNoCase(const std::string& str1, const std::string& str2, size_t n /* = 0 */)
+{
+ return CompareNoCase(str1.c_str(), str2.c_str(), n);
+}
+
+int StringUtils::CompareNoCase(const char* s1, const char* s2, size_t n /* = 0 */)
+{
+ char c2; // we need only one char outside the loop
+ size_t index = 0;
+ do
+ {
+ const char c1 = *s1++; // const local variable should help compiler to optimize
+ c2 = *s2++;
+ index++;
+ if (c1 != c2 && ::tolower(c1) != ::tolower(c2)) // This includes the possibility that one of the characters is the null-terminator, which implies a string mismatch.
+ return ::tolower(c1) - ::tolower(c2);
+ } while (c2 != '\0' &&
+ index != n); // At this point, we know c1 == c2, so there's no need to test them both.
+ return 0;
+}
+
+std::string StringUtils::Left(const std::string &str, size_t count)
+{
+ count = std::max((size_t)0, std::min(count, str.size()));
+ return str.substr(0, count);
+}
+
+std::string StringUtils::Mid(const std::string &str, size_t first, size_t count /* = string::npos */)
+{
+ if (first + count > str.size())
+ count = str.size() - first;
+
+ if (first > str.size())
+ return std::string();
+
+ assert(first + count <= str.size());
+
+ return str.substr(first, count);
+}
+
+std::string StringUtils::Right(const std::string &str, size_t count)
+{
+ count = std::max((size_t)0, std::min(count, str.size()));
+ return str.substr(str.size() - count);
+}
+
+std::string& StringUtils::Trim(std::string &str)
+{
+ TrimLeft(str);
+ return TrimRight(str);
+}
+
+std::string& StringUtils::Trim(std::string &str, const char* const chars)
+{
+ TrimLeft(str, chars);
+ return TrimRight(str, chars);
+}
+
+// hack to check only first byte of UTF-8 character
+// without this hack "TrimX" functions failed on Win32 and OS X with UTF-8 strings
+static int isspace_c(char c)
+{
+ return (c & 0x80) == 0 && ::isspace(c);
+}
+
+std::string& StringUtils::TrimLeft(std::string &str)
+{
+ str.erase(str.begin(),
+ std::find_if(str.begin(), str.end(), [](char s) { return isspace_c(s) == 0; }));
+ return str;
+}
+
+std::string& StringUtils::TrimLeft(std::string &str, const char* const chars)
+{
+ size_t nidx = str.find_first_not_of(chars);
+ str.erase(0, nidx);
+ return str;
+}
+
+std::string& StringUtils::TrimRight(std::string &str)
+{
+ str.erase(std::find_if(str.rbegin(), str.rend(), [](char s) { return isspace_c(s) == 0; }).base(),
+ str.end());
+ return str;
+}
+
+std::string& StringUtils::TrimRight(std::string &str, const char* const chars)
+{
+ size_t nidx = str.find_last_not_of(chars);
+ str.erase(str.npos == nidx ? 0 : ++nidx);
+ return str;
+}
+
+int StringUtils::ReturnDigits(const std::string& str)
+{
+ std::stringstream ss;
+ for (const auto& character : str)
+ {
+ if (isdigit(character))
+ ss << character;
+ }
+ return atoi(ss.str().c_str());
+}
+
+std::string& StringUtils::RemoveDuplicatedSpacesAndTabs(std::string& str)
+{
+ std::string::iterator it = str.begin();
+ bool onSpace = false;
+ while(it != str.end())
+ {
+ if (*it == '\t')
+ *it = ' ';
+
+ if (*it == ' ')
+ {
+ if (onSpace)
+ {
+ it = str.erase(it);
+ continue;
+ }
+ else
+ onSpace = true;
+ }
+ else
+ onSpace = false;
+
+ ++it;
+ }
+ return str;
+}
+
+int StringUtils::Replace(std::string &str, char oldChar, char newChar)
+{
+ int replacedChars = 0;
+ for (std::string::iterator it = str.begin(); it != str.end(); ++it)
+ {
+ if (*it == oldChar)
+ {
+ *it = newChar;
+ replacedChars++;
+ }
+ }
+
+ return replacedChars;
+}
+
+int StringUtils::Replace(std::string &str, const std::string &oldStr, const std::string &newStr)
+{
+ if (oldStr.empty())
+ return 0;
+
+ int replacedChars = 0;
+ size_t index = 0;
+
+ while (index < str.size() && (index = str.find(oldStr, index)) != std::string::npos)
+ {
+ str.replace(index, oldStr.size(), newStr);
+ index += newStr.size();
+ replacedChars++;
+ }
+
+ return replacedChars;
+}
+
+int StringUtils::Replace(std::wstring &str, const std::wstring &oldStr, const std::wstring &newStr)
+{
+ if (oldStr.empty())
+ return 0;
+
+ int replacedChars = 0;
+ size_t index = 0;
+
+ while (index < str.size() && (index = str.find(oldStr, index)) != std::string::npos)
+ {
+ str.replace(index, oldStr.size(), newStr);
+ index += newStr.size();
+ replacedChars++;
+ }
+
+ return replacedChars;
+}
+
+bool StringUtils::StartsWith(const std::string &str1, const std::string &str2)
+{
+ return str1.compare(0, str2.size(), str2) == 0;
+}
+
+bool StringUtils::StartsWith(const std::string &str1, const char *s2)
+{
+ return StartsWith(str1.c_str(), s2);
+}
+
+bool StringUtils::StartsWith(const char *s1, const char *s2)
+{
+ while (*s2 != '\0')
+ {
+ if (*s1 != *s2)
+ return false;
+ s1++;
+ s2++;
+ }
+ return true;
+}
+
+bool StringUtils::StartsWithNoCase(const std::string &str1, const std::string &str2)
+{
+ return StartsWithNoCase(str1.c_str(), str2.c_str());
+}
+
+bool StringUtils::StartsWithNoCase(const std::string &str1, const char *s2)
+{
+ return StartsWithNoCase(str1.c_str(), s2);
+}
+
+bool StringUtils::StartsWithNoCase(const char *s1, const char *s2)
+{
+ while (*s2 != '\0')
+ {
+ if (::tolower(*s1) != ::tolower(*s2))
+ return false;
+ s1++;
+ s2++;
+ }
+ return true;
+}
+
+bool StringUtils::EndsWith(const std::string &str1, const std::string &str2)
+{
+ if (str1.size() < str2.size())
+ return false;
+ return str1.compare(str1.size() - str2.size(), str2.size(), str2) == 0;
+}
+
+bool StringUtils::EndsWith(const std::string &str1, const char *s2)
+{
+ size_t len2 = strlen(s2);
+ if (str1.size() < len2)
+ return false;
+ return str1.compare(str1.size() - len2, len2, s2) == 0;
+}
+
+bool StringUtils::EndsWithNoCase(const std::string &str1, const std::string &str2)
+{
+ if (str1.size() < str2.size())
+ return false;
+ const char *s1 = str1.c_str() + str1.size() - str2.size();
+ const char *s2 = str2.c_str();
+ while (*s2 != '\0')
+ {
+ if (::tolower(*s1) != ::tolower(*s2))
+ return false;
+ s1++;
+ s2++;
+ }
+ return true;
+}
+
+bool StringUtils::EndsWithNoCase(const std::string &str1, const char *s2)
+{
+ size_t len2 = strlen(s2);
+ if (str1.size() < len2)
+ return false;
+ const char *s1 = str1.c_str() + str1.size() - len2;
+ while (*s2 != '\0')
+ {
+ if (::tolower(*s1) != ::tolower(*s2))
+ return false;
+ s1++;
+ s2++;
+ }
+ return true;
+}
+
+std::vector<std::string> StringUtils::Split(const std::string& input, const std::string& delimiter, unsigned int iMaxStrings)
+{
+ std::vector<std::string> result;
+ SplitTo(std::back_inserter(result), input, delimiter, iMaxStrings);
+ return result;
+}
+
+std::vector<std::string> StringUtils::Split(const std::string& input, const char delimiter, size_t iMaxStrings)
+{
+ std::vector<std::string> result;
+ SplitTo(std::back_inserter(result), input, delimiter, iMaxStrings);
+ return result;
+}
+
+std::vector<std::string> StringUtils::Split(const std::string& input, const std::vector<std::string>& delimiters)
+{
+ std::vector<std::string> result;
+ SplitTo(std::back_inserter(result), input, delimiters);
+ return result;
+}
+
+std::vector<std::string> StringUtils::SplitMulti(const std::vector<std::string>& input,
+ const std::vector<std::string>& delimiters,
+ size_t iMaxStrings /* = 0 */)
+{
+ if (input.empty())
+ return std::vector<std::string>();
+
+ std::vector<std::string> results(input);
+
+ if (delimiters.empty() || (iMaxStrings > 0 && iMaxStrings <= input.size()))
+ return results;
+
+ std::vector<std::string> strings1;
+ if (iMaxStrings == 0)
+ {
+ for (size_t di = 0; di < delimiters.size(); di++)
+ {
+ for (size_t i = 0; i < results.size(); i++)
+ {
+ std::vector<std::string> substrings = StringUtils::Split(results[i], delimiters[di]);
+ for (size_t j = 0; j < substrings.size(); j++)
+ strings1.push_back(substrings[j]);
+ }
+ results = strings1;
+ strings1.clear();
+ }
+ return results;
+ }
+
+ // Control the number of strings input is split into, keeping the original strings.
+ // Note iMaxStrings > input.size()
+ int64_t iNew = iMaxStrings - results.size();
+ for (size_t di = 0; di < delimiters.size(); di++)
+ {
+ for (size_t i = 0; i < results.size(); i++)
+ {
+ if (iNew > 0)
+ {
+ std::vector<std::string> substrings = StringUtils::Split(results[i], delimiters[di], iNew + 1);
+ iNew = iNew - substrings.size() + 1;
+ for (size_t j = 0; j < substrings.size(); j++)
+ strings1.push_back(substrings[j]);
+ }
+ else
+ strings1.push_back(results[i]);
+ }
+ results = strings1;
+ iNew = iMaxStrings - results.size();
+ strings1.clear();
+ if ((iNew <= 0))
+ break; //Stop trying any more delimiters
+ }
+ return results;
+}
+
+// returns the number of occurrences of strFind in strInput.
+int StringUtils::FindNumber(const std::string& strInput, const std::string &strFind)
+{
+ size_t pos = strInput.find(strFind, 0);
+ int numfound = 0;
+ while (pos != std::string::npos)
+ {
+ numfound++;
+ pos = strInput.find(strFind, pos + 1);
+ }
+ return numfound;
+}
+
+// Plane maps for MySQL utf8_general_ci (now known as utf8mb3_general_ci) collation
+// Derived from https://github.com/MariaDB/server/blob/10.5/strings/ctype-utf8.c
+
+// clang-format off
+static const uint16_t plane00[] = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
+ 0x0060, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F,
+ 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F,
+ 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F,
+ 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF,
+ 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x039C, 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF,
+ 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00C6, 0x0043, 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049,
+ 0x00D0, 0x004E, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x00D7, 0x00D8, 0x0055, 0x0055, 0x0055, 0x0055, 0x0059, 0x00DE, 0x0053,
+ 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00C6, 0x0043, 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049,
+ 0x00D0, 0x004E, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x00F7, 0x00D8, 0x0055, 0x0055, 0x0055, 0x0055, 0x0059, 0x00DE, 0x0059
+};
+
+static const uint16_t plane01[] = {
+ 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0043, 0x0043, 0x0043, 0x0043, 0x0043, 0x0043, 0x0043, 0x0043, 0x0044, 0x0044,
+ 0x0110, 0x0110, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0047, 0x0047, 0x0047, 0x0047,
+ 0x0047, 0x0047, 0x0047, 0x0047, 0x0048, 0x0048, 0x0126, 0x0126, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049, 0x0049,
+ 0x0049, 0x0049, 0x0132, 0x0132, 0x004A, 0x004A, 0x004B, 0x004B, 0x0138, 0x004C, 0x004C, 0x004C, 0x004C, 0x004C, 0x004C, 0x013F,
+ 0x013F, 0x0141, 0x0141, 0x004E, 0x004E, 0x004E, 0x004E, 0x004E, 0x004E, 0x0149, 0x014A, 0x014A, 0x004F, 0x004F, 0x004F, 0x004F,
+ 0x004F, 0x004F, 0x0152, 0x0152, 0x0052, 0x0052, 0x0052, 0x0052, 0x0052, 0x0052, 0x0053, 0x0053, 0x0053, 0x0053, 0x0053, 0x0053,
+ 0x0053, 0x0053, 0x0054, 0x0054, 0x0054, 0x0054, 0x0166, 0x0166, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055,
+ 0x0055, 0x0055, 0x0055, 0x0055, 0x0057, 0x0057, 0x0059, 0x0059, 0x0059, 0x005A, 0x005A, 0x005A, 0x005A, 0x005A, 0x005A, 0x0053,
+ 0x0180, 0x0181, 0x0182, 0x0182, 0x0184, 0x0184, 0x0186, 0x0187, 0x0187, 0x0189, 0x018A, 0x018B, 0x018B, 0x018D, 0x018E, 0x018F,
+ 0x0190, 0x0191, 0x0191, 0x0193, 0x0194, 0x01F6, 0x0196, 0x0197, 0x0198, 0x0198, 0x019A, 0x019B, 0x019C, 0x019D, 0x019E, 0x019F,
+ 0x004F, 0x004F, 0x01A2, 0x01A2, 0x01A4, 0x01A4, 0x01A6, 0x01A7, 0x01A7, 0x01A9, 0x01AA, 0x01AB, 0x01AC, 0x01AC, 0x01AE, 0x0055,
+ 0x0055, 0x01B1, 0x01B2, 0x01B3, 0x01B3, 0x01B5, 0x01B5, 0x01B7, 0x01B8, 0x01B8, 0x01BA, 0x01BB, 0x01BC, 0x01BC, 0x01BE, 0x01F7,
+ 0x01C0, 0x01C1, 0x01C2, 0x01C3, 0x01C4, 0x01C4, 0x01C4, 0x01C7, 0x01C7, 0x01C7, 0x01CA, 0x01CA, 0x01CA, 0x0041, 0x0041, 0x0049,
+ 0x0049, 0x004F, 0x004F, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x018E, 0x0041, 0x0041,
+ 0x0041, 0x0041, 0x00C6, 0x00C6, 0x01E4, 0x01E4, 0x0047, 0x0047, 0x004B, 0x004B, 0x004F, 0x004F, 0x004F, 0x004F, 0x01B7, 0x01B7,
+ 0x004A, 0x01F1, 0x01F1, 0x01F1, 0x0047, 0x0047, 0x01F6, 0x01F7, 0x004E, 0x004E, 0x0041, 0x0041, 0x00C6, 0x00C6, 0x00D8, 0x00D8
+};
+
+static const uint16_t plane02[] = {
+ 0x0041, 0x0041, 0x0041, 0x0041, 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049, 0x004F, 0x004F, 0x004F, 0x004F,
+ 0x0052, 0x0052, 0x0052, 0x0052, 0x0055, 0x0055, 0x0055, 0x0055, 0x0053, 0x0053, 0x0054, 0x0054, 0x021C, 0x021C, 0x0048, 0x0048,
+ 0x0220, 0x0221, 0x0222, 0x0222, 0x0224, 0x0224, 0x0041, 0x0041, 0x0045, 0x0045, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F,
+ 0x004F, 0x004F, 0x0059, 0x0059, 0x0234, 0x0235, 0x0236, 0x0237, 0x0238, 0x0239, 0x023A, 0x023B, 0x023C, 0x023D, 0x023E, 0x023F,
+ 0x0240, 0x0241, 0x0242, 0x0243, 0x0244, 0x0245, 0x0246, 0x0247, 0x0248, 0x0249, 0x024A, 0x024B, 0x024C, 0x024D, 0x024E, 0x024F,
+ 0x0250, 0x0251, 0x0252, 0x0181, 0x0186, 0x0255, 0x0189, 0x018A, 0x0258, 0x018F, 0x025A, 0x0190, 0x025C, 0x025D, 0x025E, 0x025F,
+ 0x0193, 0x0261, 0x0262, 0x0194, 0x0264, 0x0265, 0x0266, 0x0267, 0x0197, 0x0196, 0x026A, 0x026B, 0x026C, 0x026D, 0x026E, 0x019C,
+ 0x0270, 0x0271, 0x019D, 0x0273, 0x0274, 0x019F, 0x0276, 0x0277, 0x0278, 0x0279, 0x027A, 0x027B, 0x027C, 0x027D, 0x027E, 0x027F,
+ 0x01A6, 0x0281, 0x0282, 0x01A9, 0x0284, 0x0285, 0x0286, 0x0287, 0x01AE, 0x0289, 0x01B1, 0x01B2, 0x028C, 0x028D, 0x028E, 0x028F,
+ 0x0290, 0x0291, 0x01B7, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297, 0x0298, 0x0299, 0x029A, 0x029B, 0x029C, 0x029D, 0x029E, 0x029F,
+ 0x02A0, 0x02A1, 0x02A2, 0x02A3, 0x02A4, 0x02A5, 0x02A6, 0x02A7, 0x02A8, 0x02A9, 0x02AA, 0x02AB, 0x02AC, 0x02AD, 0x02AE, 0x02AF,
+ 0x02B0, 0x02B1, 0x02B2, 0x02B3, 0x02B4, 0x02B5, 0x02B6, 0x02B7, 0x02B8, 0x02B9, 0x02BA, 0x02BB, 0x02BC, 0x02BD, 0x02BE, 0x02BF,
+ 0x02C0, 0x02C1, 0x02C2, 0x02C3, 0x02C4, 0x02C5, 0x02C6, 0x02C7, 0x02C8, 0x02C9, 0x02CA, 0x02CB, 0x02CC, 0x02CD, 0x02CE, 0x02CF,
+ 0x02D0, 0x02D1, 0x02D2, 0x02D3, 0x02D4, 0x02D5, 0x02D6, 0x02D7, 0x02D8, 0x02D9, 0x02DA, 0x02DB, 0x02DC, 0x02DD, 0x02DE, 0x02DF,
+ 0x02E0, 0x02E1, 0x02E2, 0x02E3, 0x02E4, 0x02E5, 0x02E6, 0x02E7, 0x02E8, 0x02E9, 0x02EA, 0x02EB, 0x02EC, 0x02ED, 0x02EE, 0x02EF,
+ 0x02F0, 0x02F1, 0x02F2, 0x02F3, 0x02F4, 0x02F5, 0x02F6, 0x02F7, 0x02F8, 0x02F9, 0x02FA, 0x02FB, 0x02FC, 0x02FD, 0x02FE, 0x02FF
+};
+
+static const uint16_t plane03[] = {
+ 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307, 0x0308, 0x0309, 0x030A, 0x030B, 0x030C, 0x030D, 0x030E, 0x030F,
+ 0x0310, 0x0311, 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317, 0x0318, 0x0319, 0x031A, 0x031B, 0x031C, 0x031D, 0x031E, 0x031F,
+ 0x0320, 0x0321, 0x0322, 0x0323, 0x0324, 0x0325, 0x0326, 0x0327, 0x0328, 0x0329, 0x032A, 0x032B, 0x032C, 0x032D, 0x032E, 0x032F,
+ 0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, 0x0336, 0x0337, 0x0338, 0x0339, 0x033A, 0x033B, 0x033C, 0x033D, 0x033E, 0x033F,
+ 0x0340, 0x0341, 0x0342, 0x0343, 0x0344, 0x0399, 0x0346, 0x0347, 0x0348, 0x0349, 0x034A, 0x034B, 0x034C, 0x034D, 0x034E, 0x034F,
+ 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357, 0x0358, 0x0359, 0x035A, 0x035B, 0x035C, 0x035D, 0x035E, 0x035F,
+ 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, 0x0368, 0x0369, 0x036A, 0x036B, 0x036C, 0x036D, 0x036E, 0x036F,
+ 0x0370, 0x0371, 0x0372, 0x0373, 0x0374, 0x0375, 0x0376, 0x0377, 0x0378, 0x0379, 0x037A, 0x037B, 0x037C, 0x037D, 0x037E, 0x037F,
+ 0x0380, 0x0381, 0x0382, 0x0383, 0x0384, 0x0385, 0x0391, 0x0387, 0x0395, 0x0397, 0x0399, 0x038B, 0x039F, 0x038D, 0x03A5, 0x03A9,
+ 0x0399, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F,
+ 0x03A0, 0x03A1, 0x03A2, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x0399, 0x03A5, 0x0391, 0x0395, 0x0397, 0x0399,
+ 0x03A5, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F,
+ 0x03A0, 0x03A1, 0x03A3, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x0399, 0x03A5, 0x039F, 0x03A5, 0x03A9, 0x03CF,
+ 0x0392, 0x0398, 0x03D2, 0x03D2, 0x03D2, 0x03A6, 0x03A0, 0x03D7, 0x03D8, 0x03D9, 0x03DA, 0x03DA, 0x03DC, 0x03DC, 0x03DE, 0x03DE,
+ 0x03E0, 0x03E0, 0x03E2, 0x03E2, 0x03E4, 0x03E4, 0x03E6, 0x03E6, 0x03E8, 0x03E8, 0x03EA, 0x03EA, 0x03EC, 0x03EC, 0x03EE, 0x03EE,
+ 0x039A, 0x03A1, 0x03A3, 0x03F3, 0x03F4, 0x03F5, 0x03F6, 0x03F7, 0x03F8, 0x03F9, 0x03FA, 0x03FB, 0x03FC, 0x03FD, 0x03FE, 0x03FF
+};
+
+static const uint16_t plane04[] = {
+ 0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406, 0x0408, 0x0409, 0x040A, 0x040B, 0x041A, 0x0418, 0x0423, 0x040F,
+ 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F,
+ 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F,
+ 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F,
+ 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F,
+ 0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406, 0x0408, 0x0409, 0x040A, 0x040B, 0x041A, 0x0418, 0x0423, 0x040F,
+ 0x0460, 0x0460, 0x0462, 0x0462, 0x0464, 0x0464, 0x0466, 0x0466, 0x0468, 0x0468, 0x046A, 0x046A, 0x046C, 0x046C, 0x046E, 0x046E,
+ 0x0470, 0x0470, 0x0472, 0x0472, 0x0474, 0x0474, 0x0474, 0x0474, 0x0478, 0x0478, 0x047A, 0x047A, 0x047C, 0x047C, 0x047E, 0x047E,
+ 0x0480, 0x0480, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487, 0x0488, 0x0489, 0x048A, 0x048B, 0x048C, 0x048C, 0x048E, 0x048E,
+ 0x0490, 0x0490, 0x0492, 0x0492, 0x0494, 0x0494, 0x0496, 0x0496, 0x0498, 0x0498, 0x049A, 0x049A, 0x049C, 0x049C, 0x049E, 0x049E,
+ 0x04A0, 0x04A0, 0x04A2, 0x04A2, 0x04A4, 0x04A4, 0x04A6, 0x04A6, 0x04A8, 0x04A8, 0x04AA, 0x04AA, 0x04AC, 0x04AC, 0x04AE, 0x04AE,
+ 0x04B0, 0x04B0, 0x04B2, 0x04B2, 0x04B4, 0x04B4, 0x04B6, 0x04B6, 0x04B8, 0x04B8, 0x04BA, 0x04BA, 0x04BC, 0x04BC, 0x04BE, 0x04BE,
+ 0x04C0, 0x0416, 0x0416, 0x04C3, 0x04C3, 0x04C5, 0x04C6, 0x04C7, 0x04C7, 0x04C9, 0x04CA, 0x04CB, 0x04CB, 0x04CD, 0x04CE, 0x04CF,
+ 0x0410, 0x0410, 0x0410, 0x0410, 0x04D4, 0x04D4, 0x0415, 0x0415, 0x04D8, 0x04D8, 0x04D8, 0x04D8, 0x0416, 0x0416, 0x0417, 0x0417,
+ 0x04E0, 0x04E0, 0x0418, 0x0418, 0x0418, 0x0418, 0x041E, 0x041E, 0x04E8, 0x04E8, 0x04E8, 0x04E8, 0x042D, 0x042D, 0x0423, 0x0423,
+ 0x0423, 0x0423, 0x0423, 0x0423, 0x0427, 0x0427, 0x04F6, 0x04F7, 0x042B, 0x042B, 0x04FA, 0x04FB, 0x04FC, 0x04FD, 0x04FE, 0x04FF
+};
+
+static const uint16_t plane05[] = {
+ 0x0500, 0x0501, 0x0502, 0x0503, 0x0504, 0x0505, 0x0506, 0x0507, 0x0508, 0x0509, 0x050A, 0x050B, 0x050C, 0x050D, 0x050E, 0x050F,
+ 0x0510, 0x0511, 0x0512, 0x0513, 0x0514, 0x0515, 0x0516, 0x0517, 0x0518, 0x0519, 0x051A, 0x051B, 0x051C, 0x051D, 0x051E, 0x051F,
+ 0x0520, 0x0521, 0x0522, 0x0523, 0x0524, 0x0525, 0x0526, 0x0527, 0x0528, 0x0529, 0x052A, 0x052B, 0x052C, 0x052D, 0x052E, 0x052F,
+ 0x0530, 0x0531, 0x0532, 0x0533, 0x0534, 0x0535, 0x0536, 0x0537, 0x0538, 0x0539, 0x053A, 0x053B, 0x053C, 0x053D, 0x053E, 0x053F,
+ 0x0540, 0x0541, 0x0542, 0x0543, 0x0544, 0x0545, 0x0546, 0x0547, 0x0548, 0x0549, 0x054A, 0x054B, 0x054C, 0x054D, 0x054E, 0x054F,
+ 0x0550, 0x0551, 0x0552, 0x0553, 0x0554, 0x0555, 0x0556, 0x0557, 0x0558, 0x0559, 0x055A, 0x055B, 0x055C, 0x055D, 0x055E, 0x055F,
+ 0x0560, 0x0531, 0x0532, 0x0533, 0x0534, 0x0535, 0x0536, 0x0537, 0x0538, 0x0539, 0x053A, 0x053B, 0x053C, 0x053D, 0x053E, 0x053F,
+ 0x0540, 0x0541, 0x0542, 0x0543, 0x0544, 0x0545, 0x0546, 0x0547, 0x0548, 0x0549, 0x054A, 0x054B, 0x054C, 0x054D, 0x054E, 0x054F,
+ 0x0550, 0x0551, 0x0552, 0x0553, 0x0554, 0x0555, 0x0556, 0x0587, 0x0588, 0x0589, 0x058A, 0x058B, 0x058C, 0x058D, 0x058E, 0x058F,
+ 0x0590, 0x0591, 0x0592, 0x0593, 0x0594, 0x0595, 0x0596, 0x0597, 0x0598, 0x0599, 0x059A, 0x059B, 0x059C, 0x059D, 0x059E, 0x059F,
+ 0x05A0, 0x05A1, 0x05A2, 0x05A3, 0x05A4, 0x05A5, 0x05A6, 0x05A7, 0x05A8, 0x05A9, 0x05AA, 0x05AB, 0x05AC, 0x05AD, 0x05AE, 0x05AF,
+ 0x05B0, 0x05B1, 0x05B2, 0x05B3, 0x05B4, 0x05B5, 0x05B6, 0x05B7, 0x05B8, 0x05B9, 0x05BA, 0x05BB, 0x05BC, 0x05BD, 0x05BE, 0x05BF,
+ 0x05C0, 0x05C1, 0x05C2, 0x05C3, 0x05C4, 0x05C5, 0x05C6, 0x05C7, 0x05C8, 0x05C9, 0x05CA, 0x05CB, 0x05CC, 0x05CD, 0x05CE, 0x05CF,
+ 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6, 0x05D7, 0x05D8, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF,
+ 0x05E0, 0x05E1, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7, 0x05E8, 0x05E9, 0x05EA, 0x05EB, 0x05EC, 0x05ED, 0x05EE, 0x05EF,
+ 0x05F0, 0x05F1, 0x05F2, 0x05F3, 0x05F4, 0x05F5, 0x05F6, 0x05F7, 0x05F8, 0x05F9, 0x05FA, 0x05FB, 0x05FC, 0x05FD, 0x05FE, 0x05FF
+};
+
+static const uint16_t plane1E[] = {
+ 0x0041, 0x0041, 0x0042, 0x0042, 0x0042, 0x0042, 0x0042, 0x0042, 0x0043, 0x0043, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044, 0x0044,
+ 0x0044, 0x0044, 0x0044, 0x0044, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0046, 0x0046,
+ 0x0047, 0x0047, 0x0048, 0x0048, 0x0048, 0x0048, 0x0048, 0x0048, 0x0048, 0x0048, 0x0048, 0x0048, 0x0049, 0x0049, 0x0049, 0x0049,
+ 0x004B, 0x004B, 0x004B, 0x004B, 0x004B, 0x004B, 0x004C, 0x004C, 0x004C, 0x004C, 0x004C, 0x004C, 0x004C, 0x004C, 0x004D, 0x004D,
+ 0x004D, 0x004D, 0x004D, 0x004D, 0x004E, 0x004E, 0x004E, 0x004E, 0x004E, 0x004E, 0x004E, 0x004E, 0x004F, 0x004F, 0x004F, 0x004F,
+ 0x004F, 0x004F, 0x004F, 0x004F, 0x0050, 0x0050, 0x0050, 0x0050, 0x0052, 0x0052, 0x0052, 0x0052, 0x0052, 0x0052, 0x0052, 0x0052,
+ 0x0053, 0x0053, 0x0053, 0x0053, 0x0053, 0x0053, 0x0053, 0x0053, 0x0053, 0x0053, 0x0054, 0x0054, 0x0054, 0x0054, 0x0054, 0x0054,
+ 0x0054, 0x0054, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0056, 0x0056, 0x0056, 0x0056,
+ 0x0057, 0x0057, 0x0057, 0x0057, 0x0057, 0x0057, 0x0057, 0x0057, 0x0057, 0x0057, 0x0058, 0x0058, 0x0058, 0x0058, 0x0059, 0x0059,
+ 0x005A, 0x005A, 0x005A, 0x005A, 0x005A, 0x005A, 0x0048, 0x0054, 0x0057, 0x0059, 0x1E9A, 0x0053, 0x1E9C, 0x1E9D, 0x1E9E, 0x1E9F,
+ 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041,
+ 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045,
+ 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049, 0x004F, 0x004F, 0x004F, 0x004F,
+ 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F,
+ 0x004F, 0x004F, 0x004F, 0x004F, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055, 0x0055,
+ 0x0055, 0x0055, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x0059, 0x1EFA, 0x1EFB, 0x1EFC, 0x1EFD, 0x1EFE, 0x1EFF
+};
+
+static const uint16_t plane1F[] = {
+ 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391,
+ 0x0395, 0x0395, 0x0395, 0x0395, 0x0395, 0x0395, 0x1F16, 0x1F17, 0x0395, 0x0395, 0x0395, 0x0395, 0x0395, 0x0395, 0x1F1E, 0x1F1F,
+ 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397,
+ 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399,
+ 0x039F, 0x039F, 0x039F, 0x039F, 0x039F, 0x039F, 0x1F46, 0x1F47, 0x039F, 0x039F, 0x039F, 0x039F, 0x039F, 0x039F, 0x1F4E, 0x1F4F,
+ 0x03A5, 0x03A5, 0x03A5, 0x03A5, 0x03A5, 0x03A5, 0x03A5, 0x03A5, 0x1F58, 0x03A5, 0x1F5A, 0x03A5, 0x1F5C, 0x03A5, 0x1F5E, 0x03A5,
+ 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9,
+ 0x0391, 0x1FBB, 0x0395, 0x1FC9, 0x0397, 0x1FCB, 0x0399, 0x1FDB, 0x039F, 0x1FF9, 0x03A5, 0x1FEB, 0x03A9, 0x1FFB, 0x1F7E, 0x1F7F,
+ 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391,
+ 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397, 0x0397,
+ 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9, 0x03A9,
+ 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x1FB5, 0x0391, 0x0391, 0x0391, 0x0391, 0x0391, 0x1FBB, 0x0391, 0x1FBD, 0x0399, 0x1FBF,
+ 0x1FC0, 0x1FC1, 0x0397, 0x0397, 0x0397, 0x1FC5, 0x0397, 0x0397, 0x0395, 0x1FC9, 0x0397, 0x1FCB, 0x0397, 0x1FCD, 0x1FCE, 0x1FCF,
+ 0x0399, 0x0399, 0x0399, 0x1FD3, 0x1FD4, 0x1FD5, 0x0399, 0x0399, 0x0399, 0x0399, 0x0399, 0x1FDB, 0x1FDC, 0x1FDD, 0x1FDE, 0x1FDF,
+ 0x03A5, 0x03A5, 0x03A5, 0x1FE3, 0x03A1, 0x03A1, 0x03A5, 0x03A5, 0x03A5, 0x03A5, 0x03A5, 0x1FEB, 0x03A1, 0x1FED, 0x1FEE, 0x1FEF,
+ 0x1FF0, 0x1FF1, 0x03A9, 0x03A9, 0x03A9, 0x1FF5, 0x03A9, 0x03A9, 0x039F, 0x1FF9, 0x03A9, 0x1FFB, 0x03A9, 0x1FFD, 0x1FFE, 0x1FFF
+};
+
+static const uint16_t plane21[] = {
+ 0x2100, 0x2101, 0x2102, 0x2103, 0x2104, 0x2105, 0x2106, 0x2107, 0x2108, 0x2109, 0x210A, 0x210B, 0x210C, 0x210D, 0x210E, 0x210F,
+ 0x2110, 0x2111, 0x2112, 0x2113, 0x2114, 0x2115, 0x2116, 0x2117, 0x2118, 0x2119, 0x211A, 0x211B, 0x211C, 0x211D, 0x211E, 0x211F,
+ 0x2120, 0x2121, 0x2122, 0x2123, 0x2124, 0x2125, 0x2126, 0x2127, 0x2128, 0x2129, 0x212A, 0x212B, 0x212C, 0x212D, 0x212E, 0x212F,
+ 0x2130, 0x2131, 0x2132, 0x2133, 0x2134, 0x2135, 0x2136, 0x2137, 0x2138, 0x2139, 0x213A, 0x213B, 0x213C, 0x213D, 0x213E, 0x213F,
+ 0x2140, 0x2141, 0x2142, 0x2143, 0x2144, 0x2145, 0x2146, 0x2147, 0x2148, 0x2149, 0x214A, 0x214B, 0x214C, 0x214D, 0x214E, 0x214F,
+ 0x2150, 0x2151, 0x2152, 0x2153, 0x2154, 0x2155, 0x2156, 0x2157, 0x2158, 0x2159, 0x215A, 0x215B, 0x215C, 0x215D, 0x215E, 0x215F,
+ 0x2160, 0x2161, 0x2162, 0x2163, 0x2164, 0x2165, 0x2166, 0x2167, 0x2168, 0x2169, 0x216A, 0x216B, 0x216C, 0x216D, 0x216E, 0x216F,
+ 0x2160, 0x2161, 0x2162, 0x2163, 0x2164, 0x2165, 0x2166, 0x2167, 0x2168, 0x2169, 0x216A, 0x216B, 0x216C, 0x216D, 0x216E, 0x216F,
+ 0x2180, 0x2181, 0x2182, 0x2183, 0x2184, 0x2185, 0x2186, 0x2187, 0x2188, 0x2189, 0x218A, 0x218B, 0x218C, 0x218D, 0x218E, 0x218F,
+ 0x2190, 0x2191, 0x2192, 0x2193, 0x2194, 0x2195, 0x2196, 0x2197, 0x2198, 0x2199, 0x219A, 0x219B, 0x219C, 0x219D, 0x219E, 0x219F,
+ 0x21A0, 0x21A1, 0x21A2, 0x21A3, 0x21A4, 0x21A5, 0x21A6, 0x21A7, 0x21A8, 0x21A9, 0x21AA, 0x21AB, 0x21AC, 0x21AD, 0x21AE, 0x21AF,
+ 0x21B0, 0x21B1, 0x21B2, 0x21B3, 0x21B4, 0x21B5, 0x21B6, 0x21B7, 0x21B8, 0x21B9, 0x21BA, 0x21BB, 0x21BC, 0x21BD, 0x21BE, 0x21BF,
+ 0x21C0, 0x21C1, 0x21C2, 0x21C3, 0x21C4, 0x21C5, 0x21C6, 0x21C7, 0x21C8, 0x21C9, 0x21CA, 0x21CB, 0x21CC, 0x21CD, 0x21CE, 0x21CF,
+ 0x21D0, 0x21D1, 0x21D2, 0x21D3, 0x21D4, 0x21D5, 0x21D6, 0x21D7, 0x21D8, 0x21D9, 0x21DA, 0x21DB, 0x21DC, 0x21DD, 0x21DE, 0x21DF,
+ 0x21E0, 0x21E1, 0x21E2, 0x21E3, 0x21E4, 0x21E5, 0x21E6, 0x21E7, 0x21E8, 0x21E9, 0x21EA, 0x21EB, 0x21EC, 0x21ED, 0x21EE, 0x21EF,
+ 0x21F0, 0x21F1, 0x21F2, 0x21F3, 0x21F4, 0x21F5, 0x21F6, 0x21F7, 0x21F8, 0x21F9, 0x21FA, 0x21FB, 0x21FC, 0x21FD, 0x21FE, 0x21FF
+};
+
+static const uint16_t plane24[] = {
+ 0x2400, 0x2401, 0x2402, 0x2403, 0x2404, 0x2405, 0x2406, 0x2407, 0x2408, 0x2409, 0x240A, 0x240B, 0x240C, 0x240D, 0x240E, 0x240F,
+ 0x2410, 0x2411, 0x2412, 0x2413, 0x2414, 0x2415, 0x2416, 0x2417, 0x2418, 0x2419, 0x241A, 0x241B, 0x241C, 0x241D, 0x241E, 0x241F,
+ 0x2420, 0x2421, 0x2422, 0x2423, 0x2424, 0x2425, 0x2426, 0x2427, 0x2428, 0x2429, 0x242A, 0x242B, 0x242C, 0x242D, 0x242E, 0x242F,
+ 0x2430, 0x2431, 0x2432, 0x2433, 0x2434, 0x2435, 0x2436, 0x2437, 0x2438, 0x2439, 0x243A, 0x243B, 0x243C, 0x243D, 0x243E, 0x243F,
+ 0x2440, 0x2441, 0x2442, 0x2443, 0x2444, 0x2445, 0x2446, 0x2447, 0x2448, 0x2449, 0x244A, 0x244B, 0x244C, 0x244D, 0x244E, 0x244F,
+ 0x2450, 0x2451, 0x2452, 0x2453, 0x2454, 0x2455, 0x2456, 0x2457, 0x2458, 0x2459, 0x245A, 0x245B, 0x245C, 0x245D, 0x245E, 0x245F,
+ 0x2460, 0x2461, 0x2462, 0x2463, 0x2464, 0x2465, 0x2466, 0x2467, 0x2468, 0x2469, 0x246A, 0x246B, 0x246C, 0x246D, 0x246E, 0x246F,
+ 0x2470, 0x2471, 0x2472, 0x2473, 0x2474, 0x2475, 0x2476, 0x2477, 0x2478, 0x2479, 0x247A, 0x247B, 0x247C, 0x247D, 0x247E, 0x247F,
+ 0x2480, 0x2481, 0x2482, 0x2483, 0x2484, 0x2485, 0x2486, 0x2487, 0x2488, 0x2489, 0x248A, 0x248B, 0x248C, 0x248D, 0x248E, 0x248F,
+ 0x2490, 0x2491, 0x2492, 0x2493, 0x2494, 0x2495, 0x2496, 0x2497, 0x2498, 0x2499, 0x249A, 0x249B, 0x249C, 0x249D, 0x249E, 0x249F,
+ 0x24A0, 0x24A1, 0x24A2, 0x24A3, 0x24A4, 0x24A5, 0x24A6, 0x24A7, 0x24A8, 0x24A9, 0x24AA, 0x24AB, 0x24AC, 0x24AD, 0x24AE, 0x24AF,
+ 0x24B0, 0x24B1, 0x24B2, 0x24B3, 0x24B4, 0x24B5, 0x24B6, 0x24B7, 0x24B8, 0x24B9, 0x24BA, 0x24BB, 0x24BC, 0x24BD, 0x24BE, 0x24BF,
+ 0x24C0, 0x24C1, 0x24C2, 0x24C3, 0x24C4, 0x24C5, 0x24C6, 0x24C7, 0x24C8, 0x24C9, 0x24CA, 0x24CB, 0x24CC, 0x24CD, 0x24CE, 0x24CF,
+ 0x24B6, 0x24B7, 0x24B8, 0x24B9, 0x24BA, 0x24BB, 0x24BC, 0x24BD, 0x24BE, 0x24BF, 0x24C0, 0x24C1, 0x24C2, 0x24C3, 0x24C4, 0x24C5,
+ 0x24C6, 0x24C7, 0x24C8, 0x24C9, 0x24CA, 0x24CB, 0x24CC, 0x24CD, 0x24CE, 0x24CF, 0x24EA, 0x24EB, 0x24EC, 0x24ED, 0x24EE, 0x24EF,
+ 0x24F0, 0x24F1, 0x24F2, 0x24F3, 0x24F4, 0x24F5, 0x24F6, 0x24F7, 0x24F8, 0x24F9, 0x24FA, 0x24FB, 0x24FC, 0x24FD, 0x24FE, 0x24FF
+};
+
+static const uint16_t planeFF[] = {
+ 0xFF00, 0xFF01, 0xFF02, 0xFF03, 0xFF04, 0xFF05, 0xFF06, 0xFF07, 0xFF08, 0xFF09, 0xFF0A, 0xFF0B, 0xFF0C, 0xFF0D, 0xFF0E, 0xFF0F,
+ 0xFF10, 0xFF11, 0xFF12, 0xFF13, 0xFF14, 0xFF15, 0xFF16, 0xFF17, 0xFF18, 0xFF19, 0xFF1A, 0xFF1B, 0xFF1C, 0xFF1D, 0xFF1E, 0xFF1F,
+ 0xFF20, 0xFF21, 0xFF22, 0xFF23, 0xFF24, 0xFF25, 0xFF26, 0xFF27, 0xFF28, 0xFF29, 0xFF2A, 0xFF2B, 0xFF2C, 0xFF2D, 0xFF2E, 0xFF2F,
+ 0xFF30, 0xFF31, 0xFF32, 0xFF33, 0xFF34, 0xFF35, 0xFF36, 0xFF37, 0xFF38, 0xFF39, 0xFF3A, 0xFF3B, 0xFF3C, 0xFF3D, 0xFF3E, 0xFF3F,
+ 0xFF40, 0xFF21, 0xFF22, 0xFF23, 0xFF24, 0xFF25, 0xFF26, 0xFF27, 0xFF28, 0xFF29, 0xFF2A, 0xFF2B, 0xFF2C, 0xFF2D, 0xFF2E, 0xFF2F,
+ 0xFF30, 0xFF31, 0xFF32, 0xFF33, 0xFF34, 0xFF35, 0xFF36, 0xFF37, 0xFF38, 0xFF39, 0xFF3A, 0xFF5B, 0xFF5C, 0xFF5D, 0xFF5E, 0xFF5F,
+ 0xFF60, 0xFF61, 0xFF62, 0xFF63, 0xFF64, 0xFF65, 0xFF66, 0xFF67, 0xFF68, 0xFF69, 0xFF6A, 0xFF6B, 0xFF6C, 0xFF6D, 0xFF6E, 0xFF6F,
+ 0xFF70, 0xFF71, 0xFF72, 0xFF73, 0xFF74, 0xFF75, 0xFF76, 0xFF77, 0xFF78, 0xFF79, 0xFF7A, 0xFF7B, 0xFF7C, 0xFF7D, 0xFF7E, 0xFF7F,
+ 0xFF80, 0xFF81, 0xFF82, 0xFF83, 0xFF84, 0xFF85, 0xFF86, 0xFF87, 0xFF88, 0xFF89, 0xFF8A, 0xFF8B, 0xFF8C, 0xFF8D, 0xFF8E, 0xFF8F,
+ 0xFF90, 0xFF91, 0xFF92, 0xFF93, 0xFF94, 0xFF95, 0xFF96, 0xFF97, 0xFF98, 0xFF99, 0xFF9A, 0xFF9B, 0xFF9C, 0xFF9D, 0xFF9E, 0xFF9F,
+ 0xFFA0, 0xFFA1, 0xFFA2, 0xFFA3, 0xFFA4, 0xFFA5, 0xFFA6, 0xFFA7, 0xFFA8, 0xFFA9, 0xFFAA, 0xFFAB, 0xFFAC, 0xFFAD, 0xFFAE, 0xFFAF,
+ 0xFFB0, 0xFFB1, 0xFFB2, 0xFFB3, 0xFFB4, 0xFFB5, 0xFFB6, 0xFFB7, 0xFFB8, 0xFFB9, 0xFFBA, 0xFFBB, 0xFFBC, 0xFFBD, 0xFFBE, 0xFFBF,
+ 0xFFC0, 0xFFC1, 0xFFC2, 0xFFC3, 0xFFC4, 0xFFC5, 0xFFC6, 0xFFC7, 0xFFC8, 0xFFC9, 0xFFCA, 0xFFCB, 0xFFCC, 0xFFCD, 0xFFCE, 0xFFCF,
+ 0xFFD0, 0xFFD1, 0xFFD2, 0xFFD3, 0xFFD4, 0xFFD5, 0xFFD6, 0xFFD7, 0xFFD8, 0xFFD9, 0xFFDA, 0xFFDB, 0xFFDC, 0xFFDD, 0xFFDE, 0xFFDF,
+ 0xFFE0, 0xFFE1, 0xFFE2, 0xFFE3, 0xFFE4, 0xFFE5, 0xFFE6, 0xFFE7, 0xFFE8, 0xFFE9, 0xFFEA, 0xFFEB, 0xFFEC, 0xFFED, 0xFFEE, 0xFFEF,
+ 0xFFF0, 0xFFF1, 0xFFF2, 0xFFF3, 0xFFF4, 0xFFF5, 0xFFF6, 0xFFF7, 0xFFF8, 0xFFF9, 0xFFFA, 0xFFFB, 0xFFFC, 0xFFFD, 0xFFFE, 0xFFFF
+};
+
+static const uint16_t* const planemap[256] = {
+ plane00, plane01, plane02, plane03, plane04, plane05, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, plane1E, plane1F, NULL,
+ plane21, NULL, NULL, plane24, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ NULL, NULL, planeFF
+};
+// clang-format on
+
+static wchar_t GetCollationWeight(const wchar_t& r)
+{
+ // Lookup the "weight" of a UTF8 char, equivalent lowercase ascii letter, in the plane map,
+ // the character comparison value used by using "accent folding" collation utf8_general_ci
+ // in MySQL (AKA utf8mb3_general_ci in MariaDB 10)
+ auto index = r >> 8;
+ if (index > 255)
+ return 0xFFFD;
+ auto plane = planemap[index];
+ if (plane == nullptr)
+ return r;
+ return static_cast<wchar_t>(plane[r & 0xFF]);
+}
+
+// Compares separately the numeric and alphabetic parts of a wide string.
+// returns negative if left < right, positive if left > right
+// and 0 if they are identical.
+// See also the equivalent StringUtils::AlphaNumericCollation() for UFT8 data
+int64_t StringUtils::AlphaNumericCompare(const wchar_t* left, const wchar_t* right)
+{
+ const wchar_t *l = left;
+ const wchar_t *r = right;
+ const wchar_t *ld, *rd;
+ wchar_t lc, rc;
+ int64_t lnum, rnum;
+ bool lsym, rsym;
+ while (*l != 0 && *r != 0)
+ {
+ // check if we have a numerical value
+ if (*l >= L'0' && *l <= L'9' && *r >= L'0' && *r <= L'9')
+ {
+ ld = l;
+ lnum = *ld++ - L'0';
+ while (*ld >= L'0' && *ld <= L'9' && ld < l + 15)
+ { // compare only up to 15 digits
+ lnum *= 10;
+ lnum += *ld++ - L'0';
+ }
+ rd = r;
+ rnum = *rd++ - L'0';
+ while (*rd >= L'0' && *rd <= L'9' && rd < r + 15)
+ { // compare only up to 15 digits
+ rnum *= 10;
+ rnum += *rd++ - L'0';
+ }
+ // do we have numbers?
+ if (lnum != rnum)
+ { // yes - and they're different!
+ return lnum - rnum;
+ }
+ l = ld;
+ r = rd;
+ continue;
+ }
+
+ lc = *l;
+ rc = *r;
+ // Put ascii punctuation and symbols e.g. !#$&()*+,-./:;<=>?@[\]^_ `{|}~ above the other
+ // alphanumeric ascii, rather than some being mixed between the numbers and letters, and
+ // above all other unicode letters, symbols and punctuation.
+ // (Locale collation of these chars varies across platforms)
+ lsym = (lc >= 32 && lc < L'0') || (lc > L'9' && lc < L'A') || (lc > L'Z' && lc < L'a') ||
+ (lc > L'z' && lc < 128);
+ rsym = (rc >= 32 && rc < L'0') || (rc > L'9' && rc < L'A') || (rc > L'Z' && rc < L'a') ||
+ (rc > L'z' && rc < 128);
+ if (lsym && !rsym)
+ return -1;
+ if (!lsym && rsym)
+ return 1;
+ if (lsym && rsym)
+ {
+ if (lc != rc)
+ return static_cast<int64_t>(lc) - static_cast<int64_t>(rc);
+ else
+ { // Same symbol advance to next wchar
+ l++;
+ r++;
+ continue;
+ }
+ }
+ if (!g_langInfo.UseLocaleCollation())
+ {
+ // Apply case sensitive accent folding collation to non-ascii chars.
+ // This mimics utf8_general_ci collation, and provides simple collation of LATIN-1 chars
+ // for any platformthat doesn't have a language specific collate facet implemented
+ if (lc > 128)
+ lc = GetCollationWeight(lc);
+ if (rc > 128)
+ rc = GetCollationWeight(rc);
+ }
+ // Do case less comparison, convert ascii upper case to lower case
+ if (lc >= L'A' && lc <= L'Z')
+ lc += L'a' - L'A';
+ if (rc >= L'A' && rc <= L'Z')
+ rc += L'a' - L'A';
+
+ if (lc != rc)
+ {
+ if (!g_langInfo.UseLocaleCollation())
+ {
+ // Compare unicode (having applied accent folding collation to non-ascii chars).
+ int i = wcsncmp(&lc, &rc, 1);
+ return i;
+ }
+ else
+ {
+ // Fetch collation facet from locale to do comparison of wide char although on some
+ // platforms this is not language specific but just compares unicode
+ const std::collate<wchar_t>& coll =
+ std::use_facet<std::collate<wchar_t>>(g_langInfo.GetSystemLocale());
+ int cmp_res = coll.compare(&lc, &lc + 1, &rc, &rc + 1);
+ if (cmp_res != 0)
+ return cmp_res;
+ }
+ }
+ l++; r++;
+ }
+ if (*r)
+ { // r is longer
+ return -1;
+ }
+ else if (*l)
+ { // l is longer
+ return 1;
+ }
+ return 0; // files are the same
+}
+
+/*
+ Convert the UTF8 character to which z points into a 31-bit Unicode point.
+ Return how many bytes (0 to 3) of UTF8 data encode the character.
+ This only works right if z points to a well-formed UTF8 string.
+ Byte-0 Byte-1 Byte-2 Byte-3 Value
+ 0xxxxxxx 00000000 00000000 0xxxxxxx
+ 110yyyyy 10xxxxxx 00000000 00000yyy yyxxxxxx
+ 1110zzzz 10yyyyyy 10xxxxxx 00000000 zzzzyyyy yyxxxxxx
+ 11110uuu 10uuzzzz 10yyyyyy 10xxxxxx 000uuuuu zzzzyyyy yyxxxxxx
+*/
+static uint32_t UTF8ToUnicode(const unsigned char* z, int nKey, unsigned char& bytes)
+{
+ // Lookup table used decode the first byte of a multi-byte UTF8 character
+ // clang-format off
+ static const unsigned char utf8Trans1[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x00, 0x00,
+ };
+ // clang-format on
+
+ uint32_t c;
+ bytes = 0;
+ c = z[0];
+ if (c >= 0xc0)
+ {
+ c = utf8Trans1[c - 0xc0];
+ int index = 1;
+ while (index < nKey && (z[index] & 0xc0) == 0x80)
+ {
+ c = (c << 6) + (0x3f & z[index]);
+ index++;
+ }
+ if (c < 0x80 || (c & 0xFFFFF800) == 0xD800 || (c & 0xFFFFFFFE) == 0xFFFE)
+ c = 0xFFFD;
+ bytes = static_cast<unsigned char>(index - 1);
+ }
+ return c;
+}
+
+/*
+ SQLite collating function, see sqlite3_create_collation
+ The equivalent of AlphaNumericCompare() but for comparing UTF8 encoded data
+
+ This only processes enough data to find a difference, and avoids expensive data conversions.
+ When sorting in memory item data is converted once to wstring in advance prior to sorting, the
+ SQLite callback function can not do that kind of preparation. Instead, in order to use
+ AlphaNumericCompare(), it would have to repeatedly convert the full input data to wstring for
+ every pair comparison made. That approach was found to be 10 times slower than using this
+ separate routine.
+*/
+int StringUtils::AlphaNumericCollation(int nKey1, const void* pKey1, int nKey2, const void* pKey2)
+{
+ // Get exact matches of shorter text to start of larger test fast
+ int n = std::min(nKey1, nKey2);
+ int r = memcmp(pKey1, pKey2, n);
+ if (r == 0)
+ return nKey1 - nKey2;
+
+ //Not a binary match, so process character at a time
+ const unsigned char* zA = static_cast<const unsigned char*>(pKey1);
+ const unsigned char* zB = static_cast<const unsigned char*>(pKey2);
+ wchar_t lc, rc;
+ unsigned char bytes;
+ int64_t lnum, rnum;
+ bool lsym, rsym;
+ int ld, rd;
+ int i = 0;
+ int j = 0;
+ // Looping Unicode point at a time through potentially 1 to 4 multi-byte encoded UTF8 data
+ while (i < nKey1 && j < nKey2)
+ {
+ // Check if we have numerical values, compare only up to 15 digits
+ if (isdigit(zA[i]) && isdigit(zB[j]))
+ {
+ lnum = zA[i] - '0';
+ ld = i + 1;
+ while (ld < nKey1 && isdigit(zA[ld]) && ld < i + 15)
+ {
+ lnum *= 10;
+ lnum += zA[ld] - '0';
+ ld++;
+ }
+ rnum = zB[j] - '0';
+ rd = j + 1;
+ while (rd < nKey2 && isdigit(zB[rd]) && rd < j + 15)
+ {
+ rnum *= 10;
+ rnum += zB[rd] - '0';
+ rd++;
+ }
+ // do we have numbers?
+ if (lnum != rnum)
+ { // yes - and they're different!
+ return static_cast<int>(lnum - rnum);
+ }
+ // Advance to after digits
+ i = ld;
+ j = rd;
+ continue;
+ }
+ // Put ascii punctuation and symbols e.g. !#$&()*+,-./:;<=>?@[\]^_ `{|}~ before the other
+ // alphanumeric ascii, rather than some being mixed between the numbers and letters, and
+ // above all other unicode letters, symbols and punctuation.
+ // (Locale collation of these chars varies across platforms)
+ lsym = (zA[i] >= 32 && zA[i] < '0') || (zA[i] > '9' && zA[i] < 'A') ||
+ (zA[i] > 'Z' && zA[i] < 'a') || (zA[i] > 'z' && zA[i] < 128);
+ rsym = (zB[j] >= 32 && zB[j] < '0') || (zB[j] > '9' && zB[j] < 'A') ||
+ (zB[j] > 'Z' && zB[j] < 'a') || (zB[j] > 'z' && zB[j] < 128);
+ if (lsym && !rsym)
+ return -1;
+ if (!lsym && rsym)
+ return 1;
+ if (lsym && rsym)
+ {
+ if (zA[i] != zB[j])
+ return static_cast<int>(zA[i]) - static_cast<int>(zB[j]);
+ else
+ { // Same symbol advance to next
+ i++;
+ j++;
+ continue;
+ }
+ }
+ //Decode single (1 to 4 bytes) UTF8 character to Unicode
+ lc = UTF8ToUnicode(&zA[i], nKey1 - i, bytes);
+ i += bytes;
+ rc = UTF8ToUnicode(&zB[j], nKey2 - j, bytes);
+ j += bytes;
+ if (!g_langInfo.UseLocaleCollation())
+ {
+ // Apply case sensitive accent folding collation to non-ascii chars.
+ // This mimics utf8_general_ci collation, and provides simple collation of LATIN-1 chars
+ // for any platform that doesn't have a language specific collate facet implemented
+ if (lc > 128)
+ lc = GetCollationWeight(lc);
+ if (rc > 128)
+ rc = GetCollationWeight(rc);
+ }
+ // Caseless comparison so convert ascii upper case to lower case
+ if (lc >= 'A' && lc <= 'Z')
+ lc += 'a' - 'A';
+ if (rc >= 'A' && rc <= 'Z')
+ rc += 'a' - 'A';
+
+ if (lc != rc)
+ {
+ if (!g_langInfo.UseLocaleCollation() || (lc <= 128 && rc <= 128))
+ // Compare unicode (having applied accent folding collation to non-ascii chars).
+ return static_cast<int>(lc) - static_cast<int>(rc);
+ else
+ {
+ // Fetch collation facet from locale to do comparison of wide char although on some
+ // platforms this is not language specific but just compares unicode
+ const std::collate<wchar_t>& coll =
+ std::use_facet<std::collate<wchar_t>>(g_langInfo.GetSystemLocale());
+ int cmp_res = coll.compare(&lc, &lc + 1, &rc, &rc + 1);
+ if (cmp_res != 0)
+ return cmp_res;
+ }
+ }
+ i++;
+ j++;
+ }
+ // Compared characters of shortest are the same as longest, length determines order
+ return (nKey1 - nKey2);
+}
+
+int StringUtils::DateStringToYYYYMMDD(const std::string &dateString)
+{
+ std::vector<std::string> days = StringUtils::Split(dateString, '-');
+ if (days.size() == 1)
+ return atoi(days[0].c_str());
+ else if (days.size() == 2)
+ return atoi(days[0].c_str())*100+atoi(days[1].c_str());
+ else if (days.size() == 3)
+ return atoi(days[0].c_str())*10000+atoi(days[1].c_str())*100+atoi(days[2].c_str());
+ else
+ return -1;
+}
+
+std::string StringUtils::ISODateToLocalizedDate(const std::string& strIsoDate)
+{
+ // Convert ISO8601 date strings YYYY, YYYY-MM, or YYYY-MM-DD to (partial) localized date strings
+ CDateTime date;
+ std::string formattedDate = strIsoDate;
+ if (formattedDate.size() == 10)
+ {
+ date.SetFromDBDate(strIsoDate);
+ formattedDate = date.GetAsLocalizedDate();
+ }
+ else if (formattedDate.size() == 7)
+ {
+ std::string strFormat = date.GetAsLocalizedDate(false);
+ std::string tempdate;
+ // find which date separator we are using. Can be -./
+ size_t pos = strFormat.find_first_of("-./");
+ if (pos != std::string::npos)
+ {
+ bool yearFirst = strFormat.find("1601") == 0; // true if year comes first
+ std::string sep = strFormat.substr(pos, 1);
+ if (yearFirst)
+ { // build formatted date with year first, then separator and month
+ tempdate = formattedDate.substr(0, 4);
+ tempdate += sep;
+ tempdate += formattedDate.substr(5, 2);
+ }
+ else
+ {
+ tempdate = formattedDate.substr(5, 2);
+ tempdate += sep;
+ tempdate += formattedDate.substr(0, 4);
+ }
+ formattedDate = tempdate;
+ }
+ // return either just the year or the locally formatted version of the ISO date
+ }
+ return formattedDate;
+}
+
+long StringUtils::TimeStringToSeconds(const std::string &timeString)
+{
+ std::string strCopy(timeString);
+ StringUtils::Trim(strCopy);
+ if(StringUtils::EndsWithNoCase(strCopy, " min"))
+ {
+ // this is imdb format of "XXX min"
+ return 60 * atoi(strCopy.c_str());
+ }
+ else
+ {
+ std::vector<std::string> secs = StringUtils::Split(strCopy, ':');
+ int timeInSecs = 0;
+ for (unsigned int i = 0; i < 3 && i < secs.size(); i++)
+ {
+ timeInSecs *= 60;
+ timeInSecs += atoi(secs[i].c_str());
+ }
+ return timeInSecs;
+ }
+}
+
+std::string StringUtils::SecondsToTimeString(long lSeconds, TIME_FORMAT format)
+{
+ bool isNegative = lSeconds < 0;
+ lSeconds = std::abs(lSeconds);
+
+ std::string strHMS;
+ if (format == TIME_FORMAT_SECS)
+ strHMS = std::to_string(lSeconds);
+ else if (format == TIME_FORMAT_MINS)
+ strHMS = std::to_string(lrintf(static_cast<float>(lSeconds) / 60.0f));
+ else if (format == TIME_FORMAT_HOURS)
+ strHMS = std::to_string(lrintf(static_cast<float>(lSeconds) / 3600.0f));
+ else if (format & TIME_FORMAT_M)
+ strHMS += std::to_string(lSeconds % 3600 / 60);
+ else
+ {
+ int hh = lSeconds / 3600;
+ lSeconds = lSeconds % 3600;
+ int mm = lSeconds / 60;
+ int ss = lSeconds % 60;
+
+ if (format == TIME_FORMAT_GUESS)
+ format = (hh >= 1) ? TIME_FORMAT_HH_MM_SS : TIME_FORMAT_MM_SS;
+ if (format & TIME_FORMAT_HH)
+ strHMS += StringUtils::Format("{:02}", hh);
+ else if (format & TIME_FORMAT_H)
+ strHMS += std::to_string(hh);
+ if (format & TIME_FORMAT_MM)
+ strHMS += StringUtils::Format(strHMS.empty() ? "{:02}" : ":{:02}", mm);
+ if (format & TIME_FORMAT_SS)
+ strHMS += StringUtils::Format(strHMS.empty() ? "{:02}" : ":{:02}", ss);
+ }
+
+ if (isNegative)
+ strHMS = "-" + strHMS;
+
+ return strHMS;
+}
+
+bool StringUtils::IsNaturalNumber(const std::string& str)
+{
+ size_t i = 0, n = 0;
+ // allow whitespace,digits,whitespace
+ while (i < str.size() && isspace((unsigned char) str[i]))
+ i++;
+ while (i < str.size() && isdigit((unsigned char) str[i]))
+ {
+ i++; n++;
+ }
+ while (i < str.size() && isspace((unsigned char) str[i]))
+ i++;
+ return i == str.size() && n > 0;
+}
+
+bool StringUtils::IsInteger(const std::string& str)
+{
+ size_t i = 0, n = 0;
+ // allow whitespace,-,digits,whitespace
+ while (i < str.size() && isspace((unsigned char) str[i]))
+ i++;
+ if (i < str.size() && str[i] == '-')
+ i++;
+ while (i < str.size() && isdigit((unsigned char) str[i]))
+ {
+ i++; n++;
+ }
+ while (i < str.size() && isspace((unsigned char) str[i]))
+ i++;
+ return i == str.size() && n > 0;
+}
+
+int StringUtils::asciidigitvalue(char chr)
+{
+ if (!isasciidigit(chr))
+ return -1;
+
+ return chr - '0';
+}
+
+int StringUtils::asciixdigitvalue(char chr)
+{
+ int v = asciidigitvalue(chr);
+ if (v >= 0)
+ return v;
+ if (chr >= 'a' && chr <= 'f')
+ return chr - 'a' + 10;
+ if (chr >= 'A' && chr <= 'F')
+ return chr - 'A' + 10;
+
+ return -1;
+}
+
+
+void StringUtils::RemoveCRLF(std::string& strLine)
+{
+ StringUtils::TrimRight(strLine, "\n\r");
+}
+
+std::string StringUtils::SizeToString(int64_t size)
+{
+ std::string strLabel;
+ constexpr std::array<char, 9> prefixes = {' ', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'};
+ unsigned int i = 0;
+ double s = (double)size;
+ while (i < prefixes.size() && s >= 1000.0)
+ {
+ s /= 1024.0;
+ i++;
+ }
+
+ if (!i)
+ strLabel = StringUtils::Format("{:.2f} B", s);
+ else if (i == prefixes.size())
+ {
+ if (s >= 1000.0)
+ strLabel = StringUtils::Format(">999.99 {}B", prefixes[i - 1]);
+ else
+ strLabel = StringUtils::Format("{:.2f} {}B", s, prefixes[i - 1]);
+ }
+ else if (s >= 100.0)
+ strLabel = StringUtils::Format("{:.1f} {}B", s, prefixes[i]);
+ else
+ strLabel = StringUtils::Format("{:.2f} {}B", s, prefixes[i]);
+
+ return strLabel;
+}
+
+std::string StringUtils::BinaryStringToString(const std::string& in)
+{
+ std::string out;
+ out.reserve(in.size() / 2);
+ for (const char *cur = in.c_str(), *end = cur + in.size(); cur != end; ++cur) {
+ if (*cur == '\\') {
+ ++cur;
+ if (cur == end) {
+ break;
+ }
+ if (isdigit(*cur)) {
+ char* end;
+ unsigned long num = strtol(cur, &end, 10);
+ cur = end - 1;
+ out.push_back(num);
+ continue;
+ }
+ }
+ out.push_back(*cur);
+ }
+ return out;
+}
+
+std::string StringUtils::ToHexadecimal(const std::string& in)
+{
+ std::ostringstream ss;
+ ss << std::hex;
+ for (unsigned char ch : in) {
+ ss << std::setw(2) << std::setfill('0') << static_cast<unsigned long> (ch);
+ }
+ return ss.str();
+}
+
+// return -1 if not, else return the utf8 char length.
+int IsUTF8Letter(const unsigned char *str)
+{
+ // reference:
+ // unicode -> utf8 table: http://www.utf8-chartable.de/
+ // latin characters in unicode: http://en.wikipedia.org/wiki/Latin_characters_in_Unicode
+ unsigned char ch = str[0];
+ if (!ch)
+ return -1;
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'))
+ return 1;
+ if (!(ch & 0x80))
+ return -1;
+ unsigned char ch2 = str[1];
+ if (!ch2)
+ return -1;
+ // check latin 1 letter table: http://en.wikipedia.org/wiki/C1_Controls_and_Latin-1_Supplement
+ if (ch == 0xC3 && ch2 >= 0x80 && ch2 <= 0xBF && ch2 != 0x97 && ch2 != 0xB7)
+ return 2;
+ // check latin extended A table: http://en.wikipedia.org/wiki/Latin_Extended-A
+ if (ch >= 0xC4 && ch <= 0xC7 && ch2 >= 0x80 && ch2 <= 0xBF)
+ return 2;
+ // check latin extended B table: http://en.wikipedia.org/wiki/Latin_Extended-B
+ // and International Phonetic Alphabet: http://en.wikipedia.org/wiki/IPA_Extensions_(Unicode_block)
+ if (((ch == 0xC8 || ch == 0xC9) && ch2 >= 0x80 && ch2 <= 0xBF)
+ || (ch == 0xCA && ch2 >= 0x80 && ch2 <= 0xAF))
+ return 2;
+ return -1;
+}
+
+size_t StringUtils::FindWords(const char *str, const char *wordLowerCase)
+{
+ // NOTE: This assumes word is lowercase!
+ const unsigned char *s = (const unsigned char *)str;
+ do
+ {
+ // start with a compare
+ const unsigned char *c = s;
+ const unsigned char *w = (const unsigned char *)wordLowerCase;
+ bool same = true;
+ while (same && *c && *w)
+ {
+ unsigned char lc = *c++;
+ if (lc >= 'A' && lc <= 'Z')
+ lc += 'a'-'A';
+
+ if (lc != *w++) // different
+ same = false;
+ }
+ if (same && *w == 0) // only the same if word has been exhausted
+ return (const char *)s - str;
+
+ // otherwise, skip current word (composed by latin letters) or number
+ int l;
+ if (*s >= '0' && *s <= '9')
+ {
+ ++s;
+ while (*s >= '0' && *s <= '9') ++s;
+ }
+ else if ((l = IsUTF8Letter(s)) > 0)
+ {
+ s += l;
+ while ((l = IsUTF8Letter(s)) > 0) s += l;
+ }
+ else
+ ++s;
+ while (*s && *s == ' ') s++;
+
+ // and repeat until we're done
+ } while (*s);
+
+ return std::string::npos;
+}
+
+// assumes it is called from after the first open bracket is found
+int StringUtils::FindEndBracket(const std::string &str, char opener, char closer, int startPos)
+{
+ int blocks = 1;
+ for (unsigned int i = startPos; i < str.size(); i++)
+ {
+ if (str[i] == opener)
+ blocks++;
+ else if (str[i] == closer)
+ {
+ blocks--;
+ if (!blocks)
+ return i;
+ }
+ }
+
+ return (int)std::string::npos;
+}
+
+void StringUtils::WordToDigits(std::string &word)
+{
+ static const char word_to_letter[] = "22233344455566677778889999";
+ StringUtils::ToLower(word);
+ for (unsigned int i = 0; i < word.size(); ++i)
+ { // NB: This assumes ascii, which probably needs extending at some point.
+ char letter = word[i];
+ if ((letter >= 'a' && letter <= 'z')) // assume contiguous letter range
+ {
+ word[i] = word_to_letter[letter-'a'];
+ }
+ else if (letter < '0' || letter > '9') // We want to keep 0-9!
+ {
+ word[i] = ' '; // replace everything else with a space
+ }
+ }
+}
+
+std::string StringUtils::CreateUUID()
+{
+#ifdef HAVE_NEW_CROSSGUID
+#ifdef TARGET_ANDROID
+ JNIEnv* env = xbmc_jnienv();
+ return xg::newGuid(env).str();
+#else
+ return xg::newGuid().str();
+#endif /* TARGET_ANDROID */
+#else
+ static GuidGenerator guidGenerator;
+ auto guid = guidGenerator.newGuid();
+
+ std::stringstream strGuid; strGuid << guid;
+ return strGuid.str();
+#endif
+}
+
+bool StringUtils::ValidateUUID(const std::string &uuid)
+{
+ CRegExp guidRE;
+ guidRE.RegComp(ADDON_GUID_RE);
+ return (guidRE.RegFind(uuid.c_str()) == 0);
+}
+
+double StringUtils::CompareFuzzy(const std::string &left, const std::string &right)
+{
+ return (0.5 + fstrcmp(left.c_str(), right.c_str()) * (left.length() + right.length())) / 2.0;
+}
+
+int StringUtils::FindBestMatch(const std::string &str, const std::vector<std::string> &strings, double &matchscore)
+{
+ int best = -1;
+ matchscore = 0;
+
+ int i = 0;
+ for (std::vector<std::string>::const_iterator it = strings.begin(); it != strings.end(); ++it, i++)
+ {
+ int maxlength = std::max(str.length(), it->length());
+ double score = StringUtils::CompareFuzzy(str, *it) / maxlength;
+ if (score > matchscore)
+ {
+ matchscore = score;
+ best = i;
+ }
+ }
+ return best;
+}
+
+bool StringUtils::ContainsKeyword(const std::string &str, const std::vector<std::string> &keywords)
+{
+ for (std::vector<std::string>::const_iterator it = keywords.begin(); it != keywords.end(); ++it)
+ {
+ if (str.find(*it) != str.npos)
+ return true;
+ }
+ return false;
+}
+
+size_t StringUtils::utf8_strlen(const char *s)
+{
+ size_t length = 0;
+ while (*s)
+ {
+ if ((*s++ & 0xC0) != 0x80)
+ length++;
+ }
+ return length;
+}
+
+std::string StringUtils::Paramify(const std::string &param)
+{
+ std::string result = param;
+ // escape backspaces
+ StringUtils::Replace(result, "\\", "\\\\");
+ // escape double quotes
+ StringUtils::Replace(result, "\"", "\\\"");
+
+ // add double quotes around the whole string
+ return "\"" + result + "\"";
+}
+
+std::string StringUtils::DeParamify(const std::string& param)
+{
+ std::string result = param;
+
+ // remove double quotes around the whole string
+ if (StringUtils::StartsWith(result, "\"") && StringUtils::EndsWith(result, "\""))
+ {
+ result.erase(0, 1);
+ result.pop_back();
+
+ // unescape double quotes
+ StringUtils::Replace(result, "\\\"", "\"");
+
+ // unescape backspaces
+ StringUtils::Replace(result, "\\\\", "\\");
+ }
+
+ return result;
+}
+
+std::vector<std::string> StringUtils::Tokenize(const std::string &input, const std::string &delimiters)
+{
+ std::vector<std::string> tokens;
+ Tokenize(input, tokens, delimiters);
+ return tokens;
+}
+
+void StringUtils::Tokenize(const std::string& input, std::vector<std::string>& tokens, const std::string& delimiters)
+{
+ tokens.clear();
+ // Skip delimiters at beginning.
+ std::string::size_type dataPos = input.find_first_not_of(delimiters);
+ while (dataPos != std::string::npos)
+ {
+ // Find next delimiter
+ const std::string::size_type nextDelimPos = input.find_first_of(delimiters, dataPos);
+ // Found a token, add it to the vector.
+ tokens.push_back(input.substr(dataPos, nextDelimPos - dataPos));
+ // Skip delimiters. Note the "not_of"
+ dataPos = input.find_first_not_of(delimiters, nextDelimPos);
+ }
+}
+
+std::vector<std::string> StringUtils::Tokenize(const std::string &input, const char delimiter)
+{
+ std::vector<std::string> tokens;
+ Tokenize(input, tokens, delimiter);
+ return tokens;
+}
+
+void StringUtils::Tokenize(const std::string& input, std::vector<std::string>& tokens, const char delimiter)
+{
+ tokens.clear();
+ // Skip delimiters at beginning.
+ std::string::size_type dataPos = input.find_first_not_of(delimiter);
+ while (dataPos != std::string::npos)
+ {
+ // Find next delimiter
+ const std::string::size_type nextDelimPos = input.find(delimiter, dataPos);
+ // Found a token, add it to the vector.
+ tokens.push_back(input.substr(dataPos, nextDelimPos - dataPos));
+ // Skip delimiters. Note the "not_of"
+ dataPos = input.find_first_not_of(delimiter, nextDelimPos);
+ }
+}
+
+uint32_t StringUtils::ToUint32(std::string_view str, uint32_t fallback /* = 0 */) noexcept
+{
+ return NumberFromSS(str, fallback);
+}
+
+uint64_t StringUtils::ToUint64(std::string_view str, uint64_t fallback /* = 0 */) noexcept
+{
+ return NumberFromSS(str, fallback);
+}
+
+float StringUtils::ToFloat(std::string_view str, float fallback /* = 0.0f */) noexcept
+{
+ return NumberFromSS(str, fallback);
+}
+
+std::string StringUtils::FormatFileSize(uint64_t bytes)
+{
+ const std::array<std::string, 6> units{{"B", "kB", "MB", "GB", "TB", "PB"}};
+ if (bytes < 1000)
+ return Format("{}B", bytes);
+
+ size_t i = 0;
+ double value = static_cast<double>(bytes);
+ while (i + 1 < units.size() && value >= 999.5)
+ {
+ ++i;
+ value /= 1024.0;
+ }
+ unsigned int decimals = value < 9.995 ? 2 : (value < 99.95 ? 1 : 0);
+ return Format("{:.{}f}{}", value, decimals, units[i]);
+}
+
+const std::locale& StringUtils::GetOriginalLocale() noexcept
+{
+ return g_langInfo.GetOriginalLocale();
+}
+
+std::string StringUtils::CreateFromCString(const char* cstr)
+{
+ return cstr != nullptr ? std::string(cstr) : std::string();
+}
diff --git a/xbmc/utils/StringUtils.h b/xbmc/utils/StringUtils.h
new file mode 100644
index 0000000..29d9985
--- /dev/null
+++ b/xbmc/utils/StringUtils.h
@@ -0,0 +1,434 @@
+/*
+ * 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
+
+//-----------------------------------------------------------------------
+//
+// File: StringUtils.h
+//
+// Purpose: ATL split string utility
+// Author: Paul J. Weiss
+//
+// Modified to support J O'Leary's std::string class by kraqh3d
+//
+//------------------------------------------------------------------------
+
+#include <stdarg.h>
+#include <stdint.h>
+#include <string>
+#include <vector>
+#include <sstream>
+#include <locale>
+
+// workaround for broken [[deprecated]] in coverity
+#if defined(__COVERITY__)
+#undef FMT_DEPRECATED
+#define FMT_DEPRECATED
+#endif
+#include "utils/TimeFormat.h"
+#include "utils/params_check_macros.h"
+
+#include <fmt/format.h>
+#if FMT_VERSION >= 80000
+#include <fmt/xchar.h>
+#endif
+
+/*! \brief C-processor Token stringification
+
+The following macros can be used to stringify definitions to
+C style strings.
+
+Example:
+
+#define foo 4
+DEF_TO_STR_NAME(foo) // outputs "foo"
+DEF_TO_STR_VALUE(foo) // outputs "4"
+
+*/
+
+#define DEF_TO_STR_NAME(x) #x
+#define DEF_TO_STR_VALUE(x) DEF_TO_STR_NAME(x)
+
+template<typename T, std::enable_if_t<!std::is_enum<T>::value, int> = 0>
+constexpr auto&& EnumToInt(T&& arg) noexcept
+{
+ return arg;
+}
+template<typename T, std::enable_if_t<std::is_enum<T>::value, int> = 0>
+constexpr auto EnumToInt(T&& arg) noexcept
+{
+ return static_cast<int>(arg);
+}
+
+class StringUtils
+{
+public:
+ /*! \brief Get a formatted string similar to sprintf
+
+ \param fmt Format of the resulting string
+ \param ... variable number of value type arguments
+ \return Formatted string
+ */
+ template<typename... Args>
+ static std::string Format(const std::string& fmt, Args&&... args)
+ {
+ // coverity[fun_call_w_exception : FALSE]
+ return ::fmt::format(fmt, EnumToInt(std::forward<Args>(args))...);
+ }
+ template<typename... Args>
+ static std::wstring Format(const std::wstring& fmt, Args&&... args)
+ {
+ // coverity[fun_call_w_exception : FALSE]
+ return ::fmt::format(fmt, EnumToInt(std::forward<Args>(args))...);
+ }
+
+ static std::string FormatV(PRINTF_FORMAT_STRING const char *fmt, va_list args);
+ static std::wstring FormatV(PRINTF_FORMAT_STRING const wchar_t *fmt, va_list args);
+ static std::string ToUpper(const std::string& str);
+ static std::wstring ToUpper(const std::wstring& str);
+ static void ToUpper(std::string &str);
+ static void ToUpper(std::wstring &str);
+ static std::string ToLower(const std::string& str);
+ static std::wstring ToLower(const std::wstring& str);
+ static void ToLower(std::string &str);
+ static void ToLower(std::wstring &str);
+ static void ToCapitalize(std::string &str);
+ static void ToCapitalize(std::wstring &str);
+ static bool EqualsNoCase(const std::string &str1, const std::string &str2);
+ static bool EqualsNoCase(const std::string &str1, const char *s2);
+ static bool EqualsNoCase(const char *s1, const char *s2);
+ static int CompareNoCase(const std::string& str1, const std::string& str2, size_t n = 0);
+ static int CompareNoCase(const char* s1, const char* s2, size_t n = 0);
+ static int ReturnDigits(const std::string &str);
+ static std::string Left(const std::string &str, size_t count);
+ static std::string Mid(const std::string &str, size_t first, size_t count = std::string::npos);
+ static std::string Right(const std::string &str, size_t count);
+ static std::string& Trim(std::string &str);
+ static std::string& Trim(std::string &str, const char* const chars);
+ static std::string& TrimLeft(std::string &str);
+ static std::string& TrimLeft(std::string &str, const char* const chars);
+ static std::string& TrimRight(std::string &str);
+ static std::string& TrimRight(std::string &str, const char* const chars);
+ static std::string& RemoveDuplicatedSpacesAndTabs(std::string& str);
+ static int Replace(std::string &str, char oldChar, char newChar);
+ static int Replace(std::string &str, const std::string &oldStr, const std::string &newStr);
+ static int Replace(std::wstring &str, const std::wstring &oldStr, const std::wstring &newStr);
+ static bool StartsWith(const std::string &str1, const std::string &str2);
+ static bool StartsWith(const std::string &str1, const char *s2);
+ static bool StartsWith(const char *s1, const char *s2);
+ static bool StartsWithNoCase(const std::string &str1, const std::string &str2);
+ static bool StartsWithNoCase(const std::string &str1, const char *s2);
+ static bool StartsWithNoCase(const char *s1, const char *s2);
+ static bool EndsWith(const std::string &str1, const std::string &str2);
+ static bool EndsWith(const std::string &str1, const char *s2);
+ static bool EndsWithNoCase(const std::string &str1, const std::string &str2);
+ static bool EndsWithNoCase(const std::string &str1, const char *s2);
+
+ template<typename CONTAINER>
+ static std::string Join(const CONTAINER &strings, const std::string& delimiter)
+ {
+ std::string result;
+ for (const auto& str : strings)
+ result += str + delimiter;
+
+ if (!result.empty())
+ result.erase(result.size() - delimiter.size());
+ return result;
+ }
+
+ /*! \brief Splits the given input string using the given delimiter into separate strings.
+
+ If the given input string is empty the result will be an empty array (not
+ an array containing an empty string).
+
+ \param input Input string to be split
+ \param delimiter Delimiter to be used to split the input string
+ \param iMaxStrings (optional) Maximum number of splitted strings
+ */
+ static std::vector<std::string> Split(const std::string& input, const std::string& delimiter, unsigned int iMaxStrings = 0);
+ static std::vector<std::string> Split(const std::string& input, const char delimiter, size_t iMaxStrings = 0);
+ static std::vector<std::string> Split(const std::string& input, const std::vector<std::string> &delimiters);
+ /*! \brief Splits the given input string using the given delimiter into separate strings.
+
+ If the given input string is empty nothing will be put into the target iterator.
+
+ \param d_first the beginning of the destination range
+ \param input Input string to be split
+ \param delimiter Delimiter to be used to split the input string
+ \param iMaxStrings (optional) Maximum number of splitted strings
+ \return output iterator to the element in the destination range, one past the last element
+ * that was put there
+ */
+ template<typename OutputIt>
+ static OutputIt SplitTo(OutputIt d_first, const std::string& input, const std::string& delimiter, unsigned int iMaxStrings = 0)
+ {
+ OutputIt dest = d_first;
+
+ if (input.empty())
+ return dest;
+ if (delimiter.empty())
+ {
+ *d_first++ = input;
+ return dest;
+ }
+
+ const size_t delimLen = delimiter.length();
+ size_t nextDelim;
+ size_t textPos = 0;
+ do
+ {
+ if (--iMaxStrings == 0)
+ {
+ *dest++ = input.substr(textPos);
+ break;
+ }
+ nextDelim = input.find(delimiter, textPos);
+ *dest++ = input.substr(textPos, nextDelim - textPos);
+ textPos = nextDelim + delimLen;
+ } while (nextDelim != std::string::npos);
+
+ return dest;
+ }
+ template<typename OutputIt>
+ static OutputIt SplitTo(OutputIt d_first, const std::string& input, const char delimiter, size_t iMaxStrings = 0)
+ {
+ return SplitTo(d_first, input, std::string(1, delimiter), iMaxStrings);
+ }
+ template<typename OutputIt>
+ static OutputIt SplitTo(OutputIt d_first, const std::string& input, const std::vector<std::string> &delimiters)
+ {
+ OutputIt dest = d_first;
+ if (input.empty())
+ return dest;
+
+ if (delimiters.empty())
+ {
+ *dest++ = input;
+ return dest;
+ }
+ std::string str = input;
+ for (size_t di = 1; di < delimiters.size(); di++)
+ StringUtils::Replace(str, delimiters[di], delimiters[0]);
+ return SplitTo(dest, str, delimiters[0]);
+ }
+
+ /*! \brief Splits the given input strings using the given delimiters into further separate strings.
+
+ If the given input string vector is empty the result will be an empty array (not
+ an array containing an empty string).
+
+ Delimiter strings are applied in order, so once the (optional) maximum number of
+ items is produced no other delimiters are applied. This produces different results
+ to applying all delimiters at once e.g. "a/b#c/d" becomes "a", "b#c", "d" rather
+ than "a", "b", "c/d"
+
+ \param input Input vector of strings each to be split
+ \param delimiters Delimiter strings to be used to split the input strings
+ \param iMaxStrings (optional) Maximum number of resulting split strings
+ */
+ static std::vector<std::string> SplitMulti(const std::vector<std::string>& input,
+ const std::vector<std::string>& delimiters,
+ size_t iMaxStrings = 0);
+ static int FindNumber(const std::string& strInput, const std::string &strFind);
+ static int64_t AlphaNumericCompare(const wchar_t *left, const wchar_t *right);
+ static int AlphaNumericCollation(int nKey1, const void* pKey1, int nKey2, const void* pKey2);
+ static long TimeStringToSeconds(const std::string &timeString);
+ static void RemoveCRLF(std::string& strLine);
+
+ /*! \brief utf8 version of strlen - skips any non-starting bytes in the count, thus returning the number of utf8 characters
+ \param s c-string to find the length of.
+ \return the number of utf8 characters in the string.
+ */
+ static size_t utf8_strlen(const char *s);
+
+ /*! \brief convert a time in seconds to a string based on the given time format
+ \param seconds time in seconds
+ \param format the format we want the time in.
+ \return the formatted time
+ \sa TIME_FORMAT
+ */
+ static std::string SecondsToTimeString(long seconds, TIME_FORMAT format = TIME_FORMAT_GUESS);
+
+ /*! \brief check whether a string is a natural number.
+ Matches [ \t]*[0-9]+[ \t]*
+ \param str the string to check
+ \return true if the string is a natural number, false otherwise.
+ */
+ static bool IsNaturalNumber(const std::string& str);
+
+ /*! \brief check whether a string is an integer.
+ Matches [ \t]*[\-]*[0-9]+[ \t]*
+ \param str the string to check
+ \return true if the string is an integer, false otherwise.
+ */
+ static bool IsInteger(const std::string& str);
+
+ /* The next several isasciiXX and asciiXXvalue functions are locale independent (US-ASCII only),
+ * as opposed to standard ::isXX (::isalpha, ::isdigit...) which are locale dependent.
+ * Next functions get parameter as char and don't need double cast ((int)(unsigned char) is required for standard functions). */
+ inline static bool isasciidigit(char chr) // locale independent
+ {
+ return chr >= '0' && chr <= '9';
+ }
+ inline static bool isasciixdigit(char chr) // locale independent
+ {
+ return (chr >= '0' && chr <= '9') || (chr >= 'a' && chr <= 'f') || (chr >= 'A' && chr <= 'F');
+ }
+ static int asciidigitvalue(char chr); // locale independent
+ static int asciixdigitvalue(char chr); // locale independent
+ inline static bool isasciiuppercaseletter(char chr) // locale independent
+ {
+ return (chr >= 'A' && chr <= 'Z');
+ }
+ inline static bool isasciilowercaseletter(char chr) // locale independent
+ {
+ return (chr >= 'a' && chr <= 'z');
+ }
+ inline static bool isasciialphanum(char chr) // locale independent
+ {
+ return isasciiuppercaseletter(chr) || isasciilowercaseletter(chr) || isasciidigit(chr);
+ }
+ static std::string SizeToString(int64_t size);
+ static const std::string Empty;
+ static size_t FindWords(const char *str, const char *wordLowerCase);
+ static int FindEndBracket(const std::string &str, char opener, char closer, int startPos = 0);
+ static int DateStringToYYYYMMDD(const std::string &dateString);
+ static std::string ISODateToLocalizedDate (const std::string& strIsoDate);
+ static void WordToDigits(std::string &word);
+ static std::string CreateUUID();
+ static bool ValidateUUID(const std::string &uuid); // NB only validates syntax
+ static double CompareFuzzy(const std::string &left, const std::string &right);
+ static int FindBestMatch(const std::string &str, const std::vector<std::string> &strings, double &matchscore);
+ static bool ContainsKeyword(const std::string &str, const std::vector<std::string> &keywords);
+
+ /*! \brief Convert the string of binary chars to the actual string.
+
+ Convert the string representation of binary chars to the actual string.
+ For example \1\2\3 is converted to a string with binary char \1, \2 and \3
+
+ \param param String to convert
+ \return Converted string
+ */
+ static std::string BinaryStringToString(const std::string& in);
+ /**
+ * Convert each character in the string to its hexadecimal
+ * representation and return the concatenated result
+ *
+ * example: "abc\n" -> "6162630a"
+ */
+ static std::string ToHexadecimal(const std::string& in);
+ /*! \brief Format the string with locale separators.
+
+ Format the string with locale separators.
+ For example 10000.57 in en-us is '10,000.57' but in italian is '10.000,57'
+
+ \param param String to format
+ \return Formatted string
+ */
+ template<typename T>
+ static std::string FormatNumber(T num)
+ {
+ std::stringstream ss;
+// ifdef is needed because when you set _ITERATOR_DEBUG_LEVEL=0 and you use custom numpunct you will get runtime error in debug mode
+// for more info https://connect.microsoft.com/VisualStudio/feedback/details/2655363
+#if !(defined(_DEBUG) && defined(TARGET_WINDOWS))
+ ss.imbue(GetOriginalLocale());
+#endif
+ ss.precision(1);
+ ss << std::fixed << num;
+ return ss.str();
+ }
+
+ /*! \brief Escapes the given string to be able to be used as a parameter.
+
+ Escapes backslashes and double-quotes with an additional backslash and
+ adds double-quotes around the whole string.
+
+ \param param String to escape/paramify
+ \return Escaped/Paramified string
+ */
+ static std::string Paramify(const std::string &param);
+
+ /*! \brief Unescapes the given string.
+
+ Unescapes backslashes and double-quotes and removes double-quotes around the whole string.
+
+ \param param String to unescape/deparamify
+ \return Unescaped/Deparamified string
+ */
+ static std::string DeParamify(const std::string& param);
+
+ /*! \brief Split a string by the specified delimiters.
+ Splits a string using one or more delimiting characters, ignoring empty tokens.
+ Differs from Split() in two ways:
+ 1. The delimiters are treated as individual characters, rather than a single delimiting string.
+ 2. Empty tokens are ignored.
+ \return a vector of tokens
+ */
+ static std::vector<std::string> Tokenize(const std::string& input, const std::string& delimiters);
+ static void Tokenize(const std::string& input, std::vector<std::string>& tokens, const std::string& delimiters);
+ static std::vector<std::string> Tokenize(const std::string& input, const char delimiter);
+ static void Tokenize(const std::string& input, std::vector<std::string>& tokens, const char delimiter);
+
+ /*!
+ * \brief Converts a string to a unsigned int number.
+ * \param str The string to convert
+ * \param fallback [OPT] The number to return when the conversion fails
+ * \return The converted number, otherwise fallback if conversion fails
+ */
+ static uint32_t ToUint32(std::string_view str, uint32_t fallback = 0) noexcept;
+
+ /*!
+ * \brief Converts a string to a unsigned long long number.
+ * \param str The string to convert
+ * \param fallback [OPT] The number to return when the conversion fails
+ * \return The converted number, otherwise fallback if conversion fails
+ */
+ static uint64_t ToUint64(std::string_view str, uint64_t fallback = 0) noexcept;
+
+ /*!
+ * \brief Converts a string to a float number.
+ * \param str The string to convert
+ * \param fallback [OPT] The number to return when the conversion fails
+ * \return The converted number, otherwise fallback if conversion fails
+ */
+ static float ToFloat(std::string_view str, float fallback = 0.0f) noexcept;
+
+ /*!
+ * Returns bytes in a human readable format using the smallest unit that will fit `bytes` in at
+ * most three digits. The number of decimals are adjusted with significance such that 'small'
+ * numbers will have more decimals than larger ones.
+ *
+ * For example: 1024 bytes will be formatted as "1.00kB", 10240 bytes as "10.0kB" and
+ * 102400 bytes as "100kB". See TestStringUtils for more examples.
+ */
+ static std::string FormatFileSize(uint64_t bytes);
+
+ /*! \brief Converts a cstring pointer (const char*) to a std::string.
+ In case nullptr is passed the result is an empty string.
+ \param cstr the const pointer to char
+ \return the resulting std::string or ""
+ */
+ static std::string CreateFromCString(const char* cstr);
+
+private:
+ /*!
+ * Wrapper for CLangInfo::GetOriginalLocale() which allows us to
+ * avoid including LangInfo.h from this header.
+ */
+ static const std::locale& GetOriginalLocale() noexcept;
+};
+
+struct sortstringbyname
+{
+ bool operator()(const std::string& strItem1, const std::string& strItem2) const
+ {
+ return StringUtils::CompareNoCase(strItem1, strItem2) < 0;
+ }
+};
diff --git a/xbmc/utils/StringValidation.cpp b/xbmc/utils/StringValidation.cpp
new file mode 100644
index 0000000..386bfb9
--- /dev/null
+++ b/xbmc/utils/StringValidation.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2013-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 "StringValidation.h"
+
+#include "utils/StringUtils.h"
+
+bool StringValidation::IsInteger(const std::string &input, void *data)
+{
+ return StringUtils::IsInteger(input);
+}
+
+bool StringValidation::IsPositiveInteger(const std::string &input, void *data)
+{
+ return StringUtils::IsNaturalNumber(input);
+}
+
+bool StringValidation::IsTime(const std::string &input, void *data)
+{
+ std::string strTime = input;
+ StringUtils::Trim(strTime);
+
+ if (StringUtils::EndsWithNoCase(strTime, " min"))
+ {
+ strTime = StringUtils::Left(strTime, strTime.size() - 4);
+ StringUtils::TrimRight(strTime);
+
+ return IsPositiveInteger(strTime, NULL);
+ }
+ else
+ {
+ // support [[HH:]MM:]SS
+ std::vector<std::string> bits = StringUtils::Split(input, ":");
+ if (bits.size() > 3)
+ return false;
+
+ for (std::vector<std::string>::const_iterator i = bits.begin(); i != bits.end(); ++i)
+ if (!IsPositiveInteger(*i, NULL))
+ return false;
+
+ return true;
+ }
+ return false;
+}
diff --git a/xbmc/utils/StringValidation.h b/xbmc/utils/StringValidation.h
new file mode 100644
index 0000000..34d54e8
--- /dev/null
+++ b/xbmc/utils/StringValidation.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013-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 <string>
+
+class StringValidation
+{
+public:
+ typedef bool (*Validator)(const std::string &input, void *data);
+
+ static bool NonEmpty(const std::string &input, void *data) { return !input.empty(); }
+ static bool IsInteger(const std::string &input, void *data);
+ static bool IsPositiveInteger(const std::string &input, void *data);
+ static bool IsTime(const std::string &input, void *data);
+
+private:
+ StringValidation() = default;
+};
diff --git a/xbmc/utils/SystemInfo.cpp b/xbmc/utils/SystemInfo.cpp
new file mode 100644
index 0000000..e85c415
--- /dev/null
+++ b/xbmc/utils/SystemInfo.cpp
@@ -0,0 +1,1469 @@
+/*
+ * 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 <limits.h>
+
+#include "SystemInfo.h"
+#ifndef TARGET_POSIX
+#include <conio.h>
+#else
+#include <sys/utsname.h>
+#endif
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "filesystem/CurlFile.h"
+#include "filesystem/File.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "network/Network.h"
+#include "platform/Filesystem.h"
+#include "rendering/RenderSystem.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CPUInfo.h"
+#include "utils/log.h"
+
+#ifdef TARGET_WINDOWS
+#include <dwmapi.h>
+#include "utils/CharsetConverter.h"
+#include <VersionHelpers.h>
+
+#ifdef TARGET_WINDOWS_STORE
+#include <winrt/Windows.Security.ExchangeActiveSyncProvisioning.h>
+#include <winrt/Windows.System.Profile.h>
+
+using namespace winrt::Windows::ApplicationModel;
+using namespace winrt::Windows::Security::ExchangeActiveSyncProvisioning;
+using namespace winrt::Windows::System;
+using namespace winrt::Windows::System::Profile;
+#endif
+#include <wincrypt.h>
+#include "platform/win32/CharsetConverter.h"
+#endif
+#if defined(TARGET_DARWIN)
+#include "platform/darwin/DarwinUtils.h"
+#endif
+#include "powermanagement/PowerManager.h"
+#include "utils/StringUtils.h"
+#include "utils/XMLUtils.h"
+#if defined(TARGET_ANDROID)
+#include <androidjni/Build.h>
+#include <androidjni/Context.h>
+#include <androidjni/PackageManager.h>
+#endif
+
+/* Platform identification */
+#if defined(TARGET_DARWIN)
+#include <Availability.h>
+#include <mach-o/arch.h>
+#include <sys/sysctl.h>
+#elif defined(TARGET_ANDROID)
+#include <android/api-level.h>
+#include <sys/system_properties.h>
+#elif defined(TARGET_FREEBSD)
+#include <sys/param.h>
+#elif defined(TARGET_LINUX)
+#include "platform/linux/SysfsPath.h"
+
+#include <linux/version.h>
+#endif
+
+#include <system_error>
+
+/* Expand macro before stringify */
+#define STR_MACRO(x) #x
+#define XSTR_MACRO(x) STR_MACRO(x)
+
+namespace
+{
+auto startTime = std::chrono::steady_clock::now();
+}
+
+using namespace XFILE;
+
+#ifdef TARGET_WINDOWS_DESKTOP
+static bool sysGetVersionExWByRef(OSVERSIONINFOEXW& osVerInfo)
+{
+ osVerInfo.dwOSVersionInfoSize = sizeof(osVerInfo);
+
+ typedef NTSTATUS(__stdcall *RtlGetVersionPtr)(RTL_OSVERSIONINFOEXW* pOsInfo);
+ static HMODULE hNtDll = GetModuleHandleW(L"ntdll.dll");
+ if (hNtDll != NULL)
+ {
+ static RtlGetVersionPtr RtlGetVer = (RtlGetVersionPtr) GetProcAddress(hNtDll, "RtlGetVersion");
+ if (RtlGetVer && RtlGetVer(&osVerInfo) == 0)
+ return true;
+ }
+ // failed to get OS information directly from ntdll.dll
+ // use GetVersionExW() as fallback
+ // note: starting from Windows 8.1 GetVersionExW() may return unfaithful information
+ if (GetVersionExW((OSVERSIONINFOW*) &osVerInfo) != 0)
+ return true;
+
+ ZeroMemory(&osVerInfo, sizeof(osVerInfo));
+ return false;
+}
+
+static bool appendWindows10NameVersion(std::string& osNameVer)
+{
+ wchar_t versionW[32] = {};
+ DWORD len = sizeof(versionW);
+ bool obtained = false;
+ if (ERROR_SUCCESS == RegGetValueW(HKEY_LOCAL_MACHINE, REG_CURRENT_VERSION, L"DisplayVersion",
+ RRF_RT_REG_SZ, nullptr, &versionW, &len))
+ {
+ obtained = true;
+ }
+ else if (ERROR_SUCCESS == RegGetValueW(HKEY_LOCAL_MACHINE, REG_CURRENT_VERSION, L"ReleaseId",
+ RRF_RT_REG_SZ, nullptr, &versionW, &len))
+ {
+ obtained = true;
+ }
+ if (obtained)
+ osNameVer.append(StringUtils::Format(" {}", KODI::PLATFORM::WINDOWS::FromW(versionW)));
+
+ return obtained;
+}
+#endif // TARGET_WINDOWS_DESKTOP
+
+#if defined(TARGET_LINUX) && !defined(TARGET_ANDROID)
+static std::string getValueFromOs_release(std::string key)
+{
+ FILE* os_rel = fopen("/etc/os-release", "r");
+ if (!os_rel)
+ return "";
+
+ char* buf = new char[10 * 1024]; // more than enough
+ size_t len = fread(buf, 1, 10 * 1024, os_rel);
+ fclose(os_rel);
+ if (len == 0)
+ {
+ delete[] buf;
+ return "";
+ }
+
+ std::string content(buf, len);
+ delete[] buf;
+
+ // find begin of value string
+ size_t valStart = 0, seachPos;
+ key += '=';
+ if (content.compare(0, key.length(), key) == 0)
+ valStart = key.length();
+ else
+ {
+ key = "\n" + key;
+ seachPos = 0;
+ do
+ {
+ seachPos = content.find(key, seachPos);
+ if (seachPos == std::string::npos)
+ return "";
+ if (seachPos == 0 || content[seachPos - 1] != '\\')
+ valStart = seachPos + key.length();
+ else
+ seachPos++;
+ } while (valStart == 0);
+ }
+
+ if (content[valStart] == '\n')
+ return "";
+
+ // find end of value string
+ seachPos = valStart;
+ do
+ {
+ seachPos = content.find('\n', seachPos + 1);
+ } while (seachPos != std::string::npos && content[seachPos - 1] == '\\');
+ size_t const valEnd = seachPos;
+
+ std::string value(content, valStart, valEnd - valStart);
+ if (value.empty())
+ return value;
+
+ // remove quotes
+ if (value[0] == '\'' || value[0] == '"')
+ {
+ if (value.length() < 2)
+ return value;
+ size_t qEnd = value.rfind(value[0]);
+ if (qEnd != std::string::npos)
+ {
+ value.erase(qEnd);
+ value.erase(0, 1);
+ }
+ }
+
+ // unescape characters
+ for (size_t slashPos = value.find('\\'); slashPos < value.length() - 1; slashPos = value.find('\\', slashPos))
+ {
+ if (value[slashPos + 1] == '\n')
+ value.erase(slashPos, 2);
+ else
+ {
+ value.erase(slashPos, 1);
+ slashPos++; // skip unescaped character
+ }
+ }
+
+ return value;
+}
+
+enum lsb_rel_info_type
+{
+ lsb_rel_distributor,
+ lsb_rel_description,
+ lsb_rel_release,
+ lsb_rel_codename
+};
+
+static std::string getValueFromLsb_release(enum lsb_rel_info_type infoType)
+{
+ std::string key, command("unset PYTHONHOME; unset PYTHONPATH; lsb_release ");
+ switch (infoType)
+ {
+ case lsb_rel_distributor:
+ command += "-i";
+ key = "Distributor ID:\t";
+ break;
+ case lsb_rel_description:
+ command += "-d";
+ key = "Description:\t";
+ break;
+ case lsb_rel_release:
+ command += "-r";
+ key = "Release:\t";
+ break;
+ case lsb_rel_codename:
+ command += "-c";
+ key = "Codename:\t";
+ break;
+ default:
+ return "";
+ }
+ command += " 2>/dev/null";
+ FILE* lsb_rel = popen(command.c_str(), "r");
+ if (lsb_rel == NULL)
+ return "";
+
+ char buf[300]; // more than enough
+ if (fgets(buf, 300, lsb_rel) == NULL)
+ {
+ pclose(lsb_rel);
+ return "";
+ }
+ pclose(lsb_rel);
+
+ std::string response(buf);
+ if (response.compare(0, key.length(), key) != 0)
+ return "";
+
+ return response.substr(key.length(), response.find('\n') - key.length());
+}
+#endif // TARGET_LINUX && !TARGET_ANDROID
+
+CSysInfo g_sysinfo;
+
+CSysInfoJob::CSysInfoJob() = default;
+
+bool CSysInfoJob::DoWork()
+{
+ m_info.systemUptime = GetSystemUpTime(false);
+ m_info.systemTotalUptime = GetSystemUpTime(true);
+ m_info.internetState = GetInternetState();
+ m_info.videoEncoder = GetVideoEncoder();
+ m_info.cpuFrequency =
+ StringUtils::Format("{:4.0f} MHz", CServiceBroker::GetCPUInfo()->GetCPUFrequency());
+ m_info.osVersionInfo = CSysInfo::GetOsPrettyNameWithVersion() + " (kernel: " + CSysInfo::GetKernelName() + " " + CSysInfo::GetKernelVersionFull() + ")";
+ m_info.macAddress = GetMACAddress();
+ m_info.batteryLevel = GetBatteryLevel();
+ return true;
+}
+
+const CSysData &CSysInfoJob::GetData() const
+{
+ return m_info;
+}
+
+CSysData::INTERNET_STATE CSysInfoJob::GetInternetState()
+{
+ // Internet connection state!
+ XFILE::CCurlFile http;
+ if (http.IsInternet())
+ return CSysData::CONNECTED;
+ return CSysData::DISCONNECTED;
+}
+
+std::string CSysInfoJob::GetMACAddress()
+{
+ CNetworkInterface* iface = CServiceBroker::GetNetwork().GetFirstConnectedInterface();
+ if (iface)
+ return iface->GetMacAddress();
+
+ return "";
+}
+
+std::string CSysInfoJob::GetVideoEncoder()
+{
+ return "GPU: " + CServiceBroker::GetRenderSystem()->GetRenderRenderer();
+}
+
+std::string CSysInfoJob::GetBatteryLevel()
+{
+ return StringUtils::Format("{}%", CServiceBroker::GetPowerManager().BatteryLevel());
+}
+
+bool CSysInfoJob::SystemUpTime(int iInputMinutes, int &iMinutes, int &iHours, int &iDays)
+{
+ iHours = 0; iDays = 0;
+ iMinutes = iInputMinutes;
+ if (iMinutes >= 60) // Hour's
+ {
+ iHours = iMinutes / 60;
+ iMinutes = iMinutes - (iHours *60);
+ }
+ if (iHours >= 24) // Days
+ {
+ iDays = iHours / 24;
+ iHours = iHours - (iDays * 24);
+ }
+ return true;
+}
+
+std::string CSysInfoJob::GetSystemUpTime(bool bTotalUptime)
+{
+ std::string strSystemUptime;
+ int iInputMinutes, iMinutes,iHours,iDays;
+
+ auto now = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::minutes>(now - startTime);
+
+ if(bTotalUptime)
+ {
+ //Total Uptime
+ iInputMinutes = g_sysinfo.GetTotalUptime() + duration.count();
+ }
+ else
+ {
+ //Current UpTime
+ iInputMinutes = duration.count();
+ }
+
+ SystemUpTime(iInputMinutes,iMinutes, iHours, iDays);
+ if (iDays > 0)
+ {
+ strSystemUptime =
+ StringUtils::Format("{} {}, {} {}, {} {}", iDays, g_localizeStrings.Get(12393), iHours,
+ g_localizeStrings.Get(12392), iMinutes, g_localizeStrings.Get(12391));
+ }
+ else if (iDays == 0 && iHours >= 1 )
+ {
+ strSystemUptime = StringUtils::Format("{} {}, {} {}", iHours, g_localizeStrings.Get(12392),
+ iMinutes, g_localizeStrings.Get(12391));
+ }
+ else if (iDays == 0 && iHours == 0 && iMinutes >= 0)
+ {
+ strSystemUptime = StringUtils::Format("{} {}", iMinutes, g_localizeStrings.Get(12391));
+ }
+ return strSystemUptime;
+}
+
+std::string CSysInfo::TranslateInfo(int info) const
+{
+ switch(info)
+ {
+ case SYSTEM_VIDEO_ENCODER_INFO:
+ return m_info.videoEncoder;
+ case NETWORK_MAC_ADDRESS:
+ return m_info.macAddress;
+ case SYSTEM_OS_VERSION_INFO:
+ return m_info.osVersionInfo;
+ case SYSTEM_CPUFREQUENCY:
+ return m_info.cpuFrequency;
+ case SYSTEM_UPTIME:
+ return m_info.systemUptime;
+ case SYSTEM_TOTALUPTIME:
+ return m_info.systemTotalUptime;
+ case SYSTEM_INTERNET_STATE:
+ if (m_info.internetState == CSysData::CONNECTED)
+ return g_localizeStrings.Get(13296);
+ else
+ return g_localizeStrings.Get(13297);
+ case SYSTEM_BATTERY_LEVEL:
+ return m_info.batteryLevel;
+ default:
+ return "";
+ }
+}
+
+void CSysInfo::Reset()
+{
+ m_info.Reset();
+}
+
+CSysInfo::CSysInfo(void) : CInfoLoader(15 * 1000)
+{
+ memset(MD5_Sign, 0, sizeof(MD5_Sign));
+ m_iSystemTimeTotalUp = 0;
+}
+
+CSysInfo::~CSysInfo() = default;
+
+bool CSysInfo::Load(const TiXmlNode *settings)
+{
+ if (settings == NULL)
+ return false;
+
+ const TiXmlElement *pElement = settings->FirstChildElement("general");
+ if (pElement)
+ XMLUtils::GetInt(pElement, "systemtotaluptime", m_iSystemTimeTotalUp, 0, INT_MAX);
+
+ return true;
+}
+
+bool CSysInfo::Save(TiXmlNode *settings) const
+{
+ if (settings == NULL)
+ return false;
+
+ TiXmlNode *generalNode = settings->FirstChild("general");
+ if (generalNode == NULL)
+ {
+ TiXmlElement generalNodeNew("general");
+ generalNode = settings->InsertEndChild(generalNodeNew);
+ if (generalNode == NULL)
+ return false;
+ }
+ XMLUtils::SetInt(generalNode, "systemtotaluptime", m_iSystemTimeTotalUp);
+
+ return true;
+}
+
+const std::string& CSysInfo::GetAppName(void)
+{
+ assert(CCompileInfo::GetAppName() != NULL);
+ static const std::string appName(CCompileInfo::GetAppName());
+
+ return appName;
+}
+
+bool CSysInfo::GetDiskSpace(std::string drive,int& iTotal, int& iTotalFree, int& iTotalUsed, int& iPercentFree, int& iPercentUsed)
+{
+ using namespace KODI::PLATFORM::FILESYSTEM;
+
+ space_info total = {};
+ std::error_code ec;
+
+ // None of this makes sense but the idea of total space
+ // makes no sense on any system really.
+ // Return space for / or for C: as it's correct in a sense
+ // and not much worse than trying to count a total for different
+ // drives/mounts
+ if (drive.empty() || drive == "*")
+ {
+#if defined(TARGET_WINDOWS)
+ drive = "C";
+#elif defined(TARGET_POSIX)
+ drive = "/";
+#endif
+ }
+
+#ifdef TARGET_WINDOWS_DESKTOP
+ using KODI::PLATFORM::WINDOWS::ToW;
+ UINT uidriveType = GetDriveType(ToW(drive + ":\\").c_str());
+ if (uidriveType != DRIVE_UNKNOWN && uidriveType != DRIVE_NO_ROOT_DIR)
+ total = space(drive + ":\\", ec);
+#elif defined(TARGET_POSIX)
+ total = space(drive, ec);
+#endif
+ if (ec.value() != 0)
+ return false;
+
+ iTotal = static_cast<int>(total.capacity / MB);
+ iTotalFree = static_cast<int>(total.free / MB);
+ iTotalUsed = iTotal - iTotalFree;
+ if (total.capacity > 0)
+ iPercentUsed = static_cast<int>(100.0f * (total.capacity - total.free) / total.capacity + 0.5f);
+ else
+ iPercentUsed = 0;
+
+ iPercentFree = 100 - iPercentUsed;
+
+ return true;
+}
+
+std::string CSysInfo::GetKernelName(bool emptyIfUnknown /*= false*/)
+{
+ static std::string kernelName;
+ if (kernelName.empty())
+ {
+#if defined(TARGET_WINDOWS_DESKTOP)
+ OSVERSIONINFOEXW osvi = {};
+ if (sysGetVersionExWByRef(osvi) && osvi.dwPlatformId == VER_PLATFORM_WIN32_NT)
+ kernelName = "Windows NT";
+#elif defined(TARGET_WINDOWS_STORE)
+ auto e = EasClientDeviceInformation();
+ auto os = e.OperatingSystem();
+ g_charsetConverter.wToUTF8(std::wstring(os.c_str()), kernelName);
+#elif defined(TARGET_POSIX)
+ struct utsname un;
+ if (uname(&un) == 0)
+ kernelName.assign(un.sysname);
+#endif // defined(TARGET_POSIX)
+
+ if (kernelName.empty())
+ kernelName = "Unknown kernel"; // can't detect
+ }
+
+ if (emptyIfUnknown && kernelName == "Unknown kernel")
+ return "";
+
+ return kernelName;
+}
+
+std::string CSysInfo::GetKernelVersionFull(void)
+{
+ static std::string kernelVersionFull;
+ if (!kernelVersionFull.empty())
+ return kernelVersionFull;
+
+#if defined(TARGET_WINDOWS_DESKTOP)
+ OSVERSIONINFOEXW osvi = {};
+ DWORD dwBuildRevision = 0;
+ DWORD len = sizeof(DWORD);
+
+ if (sysGetVersionExWByRef(osvi))
+ kernelVersionFull = StringUtils::Format("{}.{}.{}", osvi.dwMajorVersion, osvi.dwMinorVersion,
+ osvi.dwBuildNumber);
+ // get UBR (updates build revision)
+ if (ERROR_SUCCESS == RegGetValueW(HKEY_LOCAL_MACHINE, REG_CURRENT_VERSION, L"UBR",
+ RRF_RT_REG_DWORD, nullptr, &dwBuildRevision, &len))
+ {
+ kernelVersionFull += StringUtils::Format(".{}", dwBuildRevision);
+ }
+
+#elif defined(TARGET_WINDOWS_STORE)
+ // get the system version number
+ auto sv = AnalyticsInfo::VersionInfo().DeviceFamilyVersion();
+ wchar_t* end;
+ unsigned long long v = wcstoull(sv.c_str(), &end, 10);
+ unsigned long long v1 = (v & 0xFFFF000000000000L) >> 48;
+ unsigned long long v2 = (v & 0x0000FFFF00000000L) >> 32;
+ unsigned long long v3 = (v & 0x00000000FFFF0000L) >> 16;
+ unsigned long long v4 = (v & 0x000000000000FFFFL);
+ kernelVersionFull = StringUtils::Format("{}.{}.{}", v1, v2, v3);
+ if (v4)
+ kernelVersionFull += StringUtils::Format(".{}", v4);
+
+#elif defined(TARGET_POSIX)
+ struct utsname un;
+ if (uname(&un) == 0)
+ kernelVersionFull.assign(un.release);
+#endif // defined(TARGET_POSIX)
+
+ if (kernelVersionFull.empty())
+ kernelVersionFull = "0.0.0"; // can't detect
+
+ return kernelVersionFull;
+}
+
+std::string CSysInfo::GetKernelVersion(void)
+{
+ static std::string kernelVersionClear;
+ if (kernelVersionClear.empty())
+ {
+ kernelVersionClear = GetKernelVersionFull();
+ const size_t erasePos = kernelVersionClear.find_first_not_of("0123456789.");
+ if (erasePos != std::string::npos)
+ kernelVersionClear.erase(erasePos);
+ }
+
+ return kernelVersionClear;
+}
+
+std::string CSysInfo::GetOsName(bool emptyIfUnknown /* = false*/)
+{
+ static std::string osName;
+ if (osName.empty())
+ {
+#if defined (TARGET_WINDOWS)
+ osName = GetKernelName() + "-based OS";
+#elif defined(TARGET_FREEBSD)
+ 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 = "macOS";
+#elif defined (TARGET_ANDROID)
+ if (CJNIContext::GetPackageManager().hasSystemFeature("android.software.leanback"))
+ osName = "Android TV";
+ else
+ osName = "Android";
+#elif defined(TARGET_LINUX)
+ osName = getValueFromOs_release("NAME");
+ if (osName.empty())
+ osName = getValueFromLsb_release(lsb_rel_distributor);
+ if (osName.empty())
+ osName = getValueFromOs_release("ID");
+#endif // defined(TARGET_LINUX)
+
+ if (osName.empty())
+ osName = "Unknown OS";
+ }
+
+ if (emptyIfUnknown && osName == "Unknown OS")
+ return "";
+
+ return osName;
+}
+
+std::string CSysInfo::GetOsVersion(void)
+{
+ static std::string osVersion;
+ if (!osVersion.empty())
+ return osVersion;
+
+#if defined(TARGET_WINDOWS) || defined(TARGET_FREEBSD)
+ osVersion = GetKernelVersion(); // FIXME: for Win32 and FreeBSD OS version is a kernel version
+#elif defined(TARGET_DARWIN)
+ osVersion = CDarwinUtils::GetVersionString();
+#elif defined(TARGET_ANDROID)
+ char versionCStr[PROP_VALUE_MAX];
+ int propLen = __system_property_get("ro.build.version.release", versionCStr);
+ osVersion.assign(versionCStr, (propLen > 0 && propLen <= PROP_VALUE_MAX) ? propLen : 0);
+
+ if (osVersion.empty() || std::string("0123456789").find(versionCStr[0]) == std::string::npos)
+ osVersion.clear(); // can't correctly detect Android version
+ else
+ {
+ size_t pointPos = osVersion.find('.');
+ if (pointPos == std::string::npos)
+ osVersion += ".0.0";
+ else if (osVersion.find('.', pointPos + 1) == std::string::npos)
+ osVersion += ".0";
+ }
+#elif defined(TARGET_LINUX)
+ osVersion = getValueFromOs_release("VERSION_ID");
+ if (osVersion.empty())
+ osVersion = getValueFromLsb_release(lsb_rel_release);
+#endif // defined(TARGET_LINUX)
+
+ if (osVersion.empty())
+ osVersion = "0.0";
+
+ return osVersion;
+}
+
+std::string CSysInfo::GetOsPrettyNameWithVersion(void)
+{
+ static std::string osNameVer;
+ if (!osNameVer.empty())
+ return osNameVer;
+
+#if defined (TARGET_WINDOWS_DESKTOP)
+ OSVERSIONINFOEXW osvi = {};
+
+ osNameVer = "Windows ";
+ if (sysGetVersionExWByRef(osvi))
+ {
+ switch (GetWindowsVersion())
+ {
+ case WindowsVersionWin7:
+ if (osvi.wProductType == VER_NT_WORKSTATION)
+ osNameVer.append("7");
+ else
+ osNameVer.append("Server 2008 R2");
+ break;
+ case WindowsVersionWin8:
+ if (osvi.wProductType == VER_NT_WORKSTATION)
+ osNameVer.append("8");
+ else
+ osNameVer.append("Server 2012");
+ break;
+ case WindowsVersionWin8_1:
+ if (osvi.wProductType == VER_NT_WORKSTATION)
+ osNameVer.append("8.1");
+ else
+ osNameVer.append("Server 2012 R2");
+ break;
+ case WindowsVersionWin10:
+ case WindowsVersionWin10_1709:
+ case WindowsVersionWin10_1803:
+ case WindowsVersionWin10_1809:
+ case WindowsVersionWin10_1903:
+ case WindowsVersionWin10_1909:
+ case WindowsVersionWin10_2004:
+ case WindowsVersionWin10_Future:
+ osNameVer.append("10");
+ appendWindows10NameVersion(osNameVer);
+ break;
+ case WindowsVersionWin11:
+ osNameVer.append("11");
+ appendWindows10NameVersion(osNameVer);
+ break;
+ case WindowsVersionFuture:
+ osNameVer.append("Unknown future version");
+ break;
+ default:
+ osNameVer.append("Unknown version");
+ break;
+ }
+
+ // Append Service Pack version if any
+ if (osvi.wServicePackMajor > 0 || osvi.wServicePackMinor > 0)
+ {
+ osNameVer.append(StringUtils::Format(" SP{}", osvi.wServicePackMajor));
+ if (osvi.wServicePackMinor > 0)
+ {
+ osNameVer.append(StringUtils::Format(".{}", osvi.wServicePackMinor));
+ }
+ }
+ }
+ else
+ osNameVer.append(" unknown");
+#elif defined(TARGET_WINDOWS_STORE)
+ osNameVer = GetKernelName() + " " + GetOsVersion();
+#elif defined(TARGET_FREEBSD)
+ osNameVer = GetOsName() + " " + GetOsVersion();
+#elif defined(TARGET_DARWIN)
+ osNameVer = StringUtils::Format("{} {} ({})", GetOsName(), GetOsVersion(),
+ CDarwinUtils::GetOSVersionString());
+#elif defined(TARGET_ANDROID)
+ osNameVer =
+ GetOsName() + " " + GetOsVersion() + " API level " + std::to_string(CJNIBuild::SDK_INT);
+#elif defined(TARGET_LINUX)
+ osNameVer = getValueFromOs_release("PRETTY_NAME");
+ if (osNameVer.empty())
+ {
+ osNameVer = getValueFromLsb_release(lsb_rel_description);
+ std::string osName(GetOsName(true));
+ if (!osName.empty() && osNameVer.find(osName) == std::string::npos)
+ osNameVer = osName + osNameVer;
+ if (osNameVer.empty())
+ osNameVer = "Unknown Linux Distribution";
+ }
+
+ if (osNameVer.find(GetOsVersion()) == std::string::npos)
+ osNameVer += " " + GetOsVersion();
+#endif // defined(TARGET_LINUX)
+
+ if (osNameVer.empty())
+ osNameVer = "Unknown OS Unknown version";
+
+ return osNameVer;
+
+}
+
+std::string CSysInfo::GetManufacturerName(void)
+{
+ static std::string manufName;
+ static bool inited = false;
+ if (!inited)
+ {
+#if defined(TARGET_ANDROID)
+ char deviceCStr[PROP_VALUE_MAX];
+ int propLen = __system_property_get("ro.product.manufacturer", deviceCStr);
+ manufName.assign(deviceCStr, (propLen > 0 && propLen <= PROP_VALUE_MAX) ? propLen : 0);
+#elif defined(TARGET_DARWIN)
+ manufName = CDarwinUtils::GetManufacturer();
+#elif defined(TARGET_WINDOWS_STORE)
+ auto eas = EasClientDeviceInformation();
+ auto manufacturer = eas.SystemManufacturer();
+ g_charsetConverter.wToUTF8(std::wstring(manufacturer.c_str()), manufName);
+#elif defined(TARGET_LINUX)
+
+ auto cpuInfo = CServiceBroker::GetCPUInfo();
+ manufName = cpuInfo->GetCPUSoC();
+
+#elif defined(TARGET_WINDOWS)
+ // We just don't care, might be useful on embedded
+#endif
+ inited = true;
+ }
+
+ return manufName;
+}
+
+std::string CSysInfo::GetModelName(void)
+{
+ static std::string modelName;
+ static bool inited = false;
+ if (!inited)
+ {
+#if defined(TARGET_ANDROID)
+ char deviceCStr[PROP_VALUE_MAX];
+ int propLen = __system_property_get("ro.product.model", deviceCStr);
+ modelName.assign(deviceCStr, (propLen > 0 && propLen <= PROP_VALUE_MAX) ? propLen : 0);
+#elif defined(TARGET_DARWIN_EMBEDDED)
+ modelName = CDarwinUtils::getIosPlatformString();
+#elif defined(TARGET_DARWIN_OSX)
+ size_t nameLen = 0; // 'nameLen' should include terminating null
+ if (sysctlbyname("hw.model", NULL, &nameLen, NULL, 0) == 0 && nameLen > 1)
+ {
+ std::vector<char> buf(nameLen);
+ if (sysctlbyname("hw.model", buf.data(), &nameLen, NULL, 0) == 0 && nameLen == buf.size())
+ modelName.assign(buf.data(),
+ nameLen - 1); // assign exactly 'nameLen-1' characters to 'modelName'
+ }
+#elif defined(TARGET_WINDOWS_STORE)
+ auto eas = EasClientDeviceInformation();
+ auto manufacturer = eas.SystemProductName();
+ g_charsetConverter.wToUTF8(std::wstring(manufacturer.c_str()), modelName);
+#elif defined(TARGET_LINUX)
+ auto cpuInfo = CServiceBroker::GetCPUInfo();
+ modelName = cpuInfo->GetCPUHardware();
+#elif defined(TARGET_WINDOWS)
+ // We just don't care, might be useful on embedded
+#endif
+ inited = true;
+ }
+
+ return modelName;
+}
+
+bool CSysInfo::IsAeroDisabled()
+{
+#ifdef TARGET_WINDOWS_STORE
+ return true; // need to review https://msdn.microsoft.com/en-us/library/windows/desktop/aa969518(v=vs.85).aspx
+#elif defined(TARGET_WINDOWS)
+ BOOL aeroEnabled = FALSE;
+ HRESULT res = DwmIsCompositionEnabled(&aeroEnabled);
+ if (SUCCEEDED(res))
+ return !aeroEnabled;
+#endif
+ return false;
+}
+
+CSysInfo::WindowsVersion CSysInfo::m_WinVer = WindowsVersionUnknown;
+
+bool CSysInfo::IsWindowsVersion(WindowsVersion ver)
+{
+ if (ver == WindowsVersionUnknown)
+ return false;
+ return GetWindowsVersion() == ver;
+}
+
+bool CSysInfo::IsWindowsVersionAtLeast(WindowsVersion ver)
+{
+ if (ver == WindowsVersionUnknown)
+ return false;
+ return GetWindowsVersion() >= ver;
+}
+
+CSysInfo::WindowsVersion CSysInfo::GetWindowsVersion()
+{
+#ifdef TARGET_WINDOWS_DESKTOP
+ if (m_WinVer == WindowsVersionUnknown)
+ {
+ OSVERSIONINFOEXW osvi = {};
+ if (sysGetVersionExWByRef(osvi))
+ {
+ if (osvi.dwMajorVersion == 6 && osvi.dwMinorVersion == 1)
+ m_WinVer = WindowsVersionWin7;
+ else if (osvi.dwMajorVersion == 6 && osvi.dwMinorVersion == 2)
+ m_WinVer = WindowsVersionWin8;
+ else if (osvi.dwMajorVersion == 6 && osvi.dwMinorVersion == 3)
+ m_WinVer = WindowsVersionWin8_1;
+ else if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0 && osvi.dwBuildNumber < 16299)
+ m_WinVer = WindowsVersionWin10;
+ else if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0 && osvi.dwBuildNumber == 16299)
+ m_WinVer = WindowsVersionWin10_1709;
+ else if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0 && osvi.dwBuildNumber == 17134)
+ m_WinVer = WindowsVersionWin10_1803;
+ else if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0 && osvi.dwBuildNumber == 17763)
+ m_WinVer = WindowsVersionWin10_1809;
+ else if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0 && osvi.dwBuildNumber == 18362)
+ m_WinVer = WindowsVersionWin10_1903;
+ else if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0 && osvi.dwBuildNumber == 18363)
+ m_WinVer = WindowsVersionWin10_1909;
+ else if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0 && osvi.dwBuildNumber == 19041)
+ m_WinVer = WindowsVersionWin10_2004;
+ else if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0 && osvi.dwBuildNumber >= 22000)
+ m_WinVer = WindowsVersionWin11;
+ else if (osvi.dwMajorVersion == 10 && osvi.dwMinorVersion == 0 && osvi.dwBuildNumber > 19041)
+ m_WinVer = WindowsVersionWin10_Future;
+ /* Insert checks for new Windows versions here */
+ else if ( (osvi.dwMajorVersion == 6 && osvi.dwMinorVersion > 3) || osvi.dwMajorVersion > 10)
+ m_WinVer = WindowsVersionFuture;
+ }
+ }
+#elif defined(TARGET_WINDOWS_STORE)
+ m_WinVer = WindowsVersionWin10;
+#endif // TARGET_WINDOWS
+ return m_WinVer;
+}
+
+int CSysInfo::GetKernelBitness(void)
+{
+ static int kernelBitness = -1;
+ if (kernelBitness == -1)
+ {
+#ifdef TARGET_WINDOWS_STORE
+ Package package = Package::Current();
+ auto arch = package.Id().Architecture();
+ switch (arch)
+ {
+ case ProcessorArchitecture::X86:
+ kernelBitness = 32;
+ break;
+ case ProcessorArchitecture::X64:
+ kernelBitness = 64;
+ break;
+ case ProcessorArchitecture::Arm:
+ kernelBitness = 32;
+ break;
+ case ProcessorArchitecture::Unknown: // not sure what to do here. guess 32 for now
+ case ProcessorArchitecture::Neutral:
+ kernelBitness = 32;
+ break;
+ }
+#elif defined(TARGET_WINDOWS_DESKTOP)
+ SYSTEM_INFO si;
+ GetNativeSystemInfo(&si);
+ if (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_INTEL || si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_ARM)
+ kernelBitness = 32;
+ else if (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64)
+ kernelBitness = 64;
+ else
+ {
+ BOOL isWow64 = FALSE;
+ if (IsWow64Process(GetCurrentProcess(), &isWow64) && isWow64) // fallback
+ kernelBitness = 64;
+ }
+#elif defined(TARGET_DARWIN_EMBEDDED)
+ // Note: OS X return x86 CPU type without CPU_ARCH_ABI64 flag
+ const NXArchInfo* archInfo = NXGetLocalArchInfo();
+ if (archInfo)
+ kernelBitness = ((archInfo->cputype & CPU_ARCH_ABI64) != 0) ? 64 : 32;
+#elif defined(TARGET_POSIX)
+ struct utsname un;
+ if (uname(&un) == 0)
+ {
+ std::string machine(un.machine);
+ if (machine == "x86_64" || machine == "amd64" || machine == "arm64" || machine == "aarch64" ||
+ machine == "ppc64" || machine == "ppc64el" || machine == "ppc64le" || machine == "ia64" ||
+ machine == "mips64" || machine == "s390x" || machine == "riscv64")
+ kernelBitness = 64;
+ else
+ kernelBitness = 32;
+ }
+#endif
+ if (kernelBitness == -1)
+ kernelBitness = 0; // can't detect
+ }
+
+ return kernelBitness;
+}
+
+const std::string& CSysInfo::GetKernelCpuFamily(void)
+{
+ static std::string kernelCpuFamily;
+ if (kernelCpuFamily.empty())
+ {
+#ifdef TARGET_WINDOWS
+ SYSTEM_INFO si;
+ GetNativeSystemInfo(&si);
+ if (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_INTEL ||
+ si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64)
+ kernelCpuFamily = "x86";
+ else if (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_ARM)
+ kernelCpuFamily = "ARM";
+#elif defined(TARGET_DARWIN)
+ const NXArchInfo* archInfo = NXGetLocalArchInfo();
+ if (archInfo)
+ {
+ const cpu_type_t cpuType = (archInfo->cputype & ~CPU_ARCH_ABI64); // get CPU family without 64-bit ABI flag
+ if (cpuType == CPU_TYPE_I386)
+ kernelCpuFamily = "x86";
+ else if (cpuType == CPU_TYPE_ARM)
+ kernelCpuFamily = "ARM";
+#ifdef CPU_TYPE_MIPS
+ else if (cpuType == CPU_TYPE_MIPS)
+ kernelCpuFamily = "MIPS";
+#endif // CPU_TYPE_MIPS
+ }
+#elif defined(TARGET_POSIX)
+ struct utsname un;
+ if (uname(&un) == 0)
+ {
+ std::string machine(un.machine);
+ if (machine.compare(0, 3, "arm", 3) == 0 || machine.compare(0, 7, "aarch64", 7) == 0)
+ kernelCpuFamily = "ARM";
+ else if (machine.compare(0, 4, "mips", 4) == 0)
+ kernelCpuFamily = "MIPS";
+ else if (machine.compare(0, 4, "i686", 4) == 0 || machine == "i386" || machine == "amd64" || machine.compare(0, 3, "x86", 3) == 0)
+ kernelCpuFamily = "x86";
+ else if (machine.compare(0, 4, "s390", 4) == 0)
+ kernelCpuFamily = "s390";
+ else if (machine.compare(0, 3, "ppc", 3) == 0 || machine.compare(0, 5, "power", 5) == 0)
+ kernelCpuFamily = "PowerPC";
+ else if (machine.compare(0, 5, "riscv", 5) == 0)
+ kernelCpuFamily = "RISC-V";
+ }
+#endif
+ if (kernelCpuFamily.empty())
+ kernelCpuFamily = "unknown CPU family";
+ }
+ return kernelCpuFamily;
+}
+
+int CSysInfo::GetXbmcBitness(void)
+{
+ return static_cast<int>(sizeof(void*) * 8);
+}
+
+bool CSysInfo::HasInternet()
+{
+ if (m_info.internetState != CSysData::UNKNOWN)
+ return m_info.internetState == CSysData::CONNECTED;
+ return (m_info.internetState = CSysInfoJob::GetInternetState()) == CSysData::CONNECTED;
+}
+
+std::string CSysInfo::GetHddSpaceInfo(int drive, bool shortText)
+{
+ int percent;
+ return GetHddSpaceInfo( percent, drive, shortText);
+}
+
+std::string CSysInfo::GetHddSpaceInfo(int& percent, int drive, bool shortText)
+{
+ int total, totalFree, totalUsed, percentFree, percentused;
+ std::string strRet;
+ percent = 0;
+ if (g_sysinfo.GetDiskSpace("", total, totalFree, totalUsed, percentFree, percentused))
+ {
+ if (shortText)
+ {
+ switch(drive)
+ {
+ case SYSTEM_FREE_SPACE:
+ percent = percentFree;
+ break;
+ case SYSTEM_USED_SPACE:
+ percent = percentused;
+ break;
+ }
+ }
+ else
+ {
+ switch(drive)
+ {
+ case SYSTEM_FREE_SPACE:
+ strRet = StringUtils::Format("{} MB {}", totalFree, g_localizeStrings.Get(160));
+ break;
+ case SYSTEM_USED_SPACE:
+ strRet = StringUtils::Format("{} MB {}", totalUsed, g_localizeStrings.Get(20162));
+ break;
+ case SYSTEM_TOTAL_SPACE:
+ strRet = StringUtils::Format("{} MB {}", total, g_localizeStrings.Get(20161));
+ break;
+ case SYSTEM_FREE_SPACE_PERCENT:
+ strRet = StringUtils::Format("{} % {}", percentFree, g_localizeStrings.Get(160));
+ break;
+ case SYSTEM_USED_SPACE_PERCENT:
+ strRet = StringUtils::Format("{} % {}", percentused, g_localizeStrings.Get(20162));
+ break;
+ }
+ }
+ }
+ else
+ {
+ if (shortText)
+ strRet = g_localizeStrings.Get(10006); // N/A
+ else
+ strRet = g_localizeStrings.Get(10005); // Not available
+ }
+ return strRet;
+}
+
+std::string CSysInfo::GetUserAgent()
+{
+ static std::string result;
+ if (!result.empty())
+ return result;
+
+ result = GetAppName() + "/" + CSysInfo::GetVersionShort() + " (";
+#if defined(TARGET_WINDOWS)
+ result += GetKernelName() + " " + GetKernelVersion();
+#ifndef TARGET_WINDOWS_STORE
+ BOOL bIsWow = FALSE;
+ if (IsWow64Process(GetCurrentProcess(), &bIsWow) && bIsWow)
+ result.append("; WOW64");
+ else
+#endif
+ {
+ SYSTEM_INFO si = {};
+ GetSystemInfo(&si);
+ if (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64)
+ result.append("; Win64; x64");
+ else if (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_IA64)
+ result.append("; Win64; IA64");
+ else if (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_ARM)
+ result.append("; ARM");
+ }
+#elif defined(TARGET_DARWIN)
+#if defined(TARGET_DARWIN_EMBEDDED)
+ std::string iDevStr(GetModelName()); // device model name with number of model version
+ size_t iDevStrDigit = iDevStr.find_first_of("0123456789");
+ std::string iDev(iDevStr, 0, iDevStrDigit); // device model name without number
+ if (iDevStrDigit == 0)
+ iDev = "unknown";
+ result += iDev + "; ";
+ std::string iOSVersion(GetOsVersion());
+ size_t lastDotPos = iOSVersion.rfind('.');
+ if (lastDotPos != std::string::npos && iOSVersion.find('.') != lastDotPos
+ && iOSVersion.find_first_not_of('0', lastDotPos + 1) == std::string::npos)
+ iOSVersion.erase(lastDotPos);
+ StringUtils::Replace(iOSVersion, '.', '_');
+ 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";
+#else
+ result += "Macintosh; ";
+ std::string cpuFam(GetBuildTargetCpuFamily());
+ if (cpuFam == "x86")
+ result += "Intel ";
+ result += "Mac OS X ";
+ std::string OSXVersion(GetOsVersion());
+ StringUtils::Replace(OSXVersion, '.', '_');
+ result += OSXVersion;
+#endif
+#elif defined(TARGET_ANDROID)
+ result += "Linux; Android ";
+ std::string versionStr(GetOsVersion());
+ const size_t verLen = versionStr.length();
+ if (verLen >= 2 && versionStr.compare(verLen - 2, 2, ".0", 2) == 0)
+ versionStr.erase(verLen - 2); // remove last ".0" if any
+ result += versionStr;
+ std::string deviceInfo(GetModelName());
+
+ char buildId[PROP_VALUE_MAX];
+ int propLen = __system_property_get("ro.build.id", buildId);
+ if (propLen > 0 && propLen <= PROP_VALUE_MAX)
+ {
+ if (!deviceInfo.empty())
+ deviceInfo += " ";
+ deviceInfo += "Build/";
+ deviceInfo.append(buildId, propLen);
+ }
+
+ if (!deviceInfo.empty())
+ result += "; " + deviceInfo;
+#elif defined(TARGET_POSIX)
+ result += "X11; ";
+ struct utsname un;
+ if (uname(&un) == 0)
+ {
+ std::string cpuStr(un.machine);
+ if (cpuStr == "x86_64" && GetXbmcBitness() == 32)
+ cpuStr = "i686 on x86_64";
+ result += un.sysname;
+ result += " ";
+ result += cpuStr;
+ }
+ else
+ result += "Unknown";
+#else
+ result += "Unknown";
+#endif
+ result += ")";
+
+ if (GetAppName() != "Kodi")
+ result += " Kodi_Fork_" + GetAppName() + "/1.0"; // default fork number is '1.0', replace it with actual number if necessary
+
+#ifdef TARGET_LINUX
+ // Add distribution name
+ std::string linuxOSName(GetOsName(true));
+ if (!linuxOSName.empty())
+ result += " " + linuxOSName + "/" + GetOsVersion();
+#endif
+
+#if defined(TARGET_DARWIN_IOS)
+ std::string iDevVer;
+ if (iDevStrDigit == std::string::npos)
+ iDevVer = "0.0";
+ else
+ iDevVer.assign(iDevStr, iDevStrDigit, std::string::npos);
+ StringUtils::Replace(iDevVer, ',', '.');
+ result += " HW_" + iDev + "/" + iDevVer;
+#endif
+ // add more device IDs here if needed.
+ // keep only one device ID in result! Form:
+ // result += " HW_" + "deviceID" + "/" + "1.0"; // '1.0' if device has no version
+
+#if defined(TARGET_ANDROID)
+ // Android has no CPU string by default, so add it as additional parameter
+ struct utsname un1;
+ if (uname(&un1) == 0)
+ {
+ std::string cpuStr(un1.machine);
+ StringUtils::Replace(cpuStr, ' ', '_');
+ result += " Sys_CPU/" + cpuStr;
+ }
+#endif
+
+ result += " App_Bitness/" + std::to_string(GetXbmcBitness());
+
+ std::string fullVer(CSysInfo::GetVersion());
+ StringUtils::Replace(fullVer, ' ', '-');
+ result += " Version/" + fullVer;
+
+ return result;
+}
+
+std::string CSysInfo::GetDeviceName()
+{
+ std::string friendlyName = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SERVICES_DEVICENAME);
+ if (StringUtils::EqualsNoCase(friendlyName, CCompileInfo::GetAppName()))
+ {
+ std::string hostname("[unknown]");
+ CServiceBroker::GetNetwork().GetHostName(hostname);
+ return StringUtils::Format("{} ({})", friendlyName, hostname);
+ }
+
+ return friendlyName;
+}
+
+// Version string MUST NOT contain spaces. It is used
+// in the HTTP request user agent.
+std::string CSysInfo::GetVersionShort()
+{
+ if (strlen(CCompileInfo::GetSuffix()) == 0)
+ return StringUtils::Format("{}.{}", CCompileInfo::GetMajor(), CCompileInfo::GetMinor());
+ else
+ return StringUtils::Format("{}.{}-{}", CCompileInfo::GetMajor(), CCompileInfo::GetMinor(),
+ CCompileInfo::GetSuffix());
+}
+
+std::string CSysInfo::GetVersion()
+{
+ return GetVersionShort() + " (" + CCompileInfo::GetVersionCode() + ")" +
+ " Git:" + CCompileInfo::GetSCMID();
+}
+
+std::string CSysInfo::GetVersionCode()
+{
+ return CCompileInfo::GetVersionCode();
+}
+
+std::string CSysInfo::GetVersionGit()
+{
+ return CCompileInfo::GetSCMID();
+}
+
+std::string CSysInfo::GetBuildDate()
+{
+ return CCompileInfo::GetBuildDate();
+}
+
+std::string CSysInfo::GetBuildTargetPlatformName(void)
+{
+#if defined(TARGET_DARWIN_OSX)
+ return "macOS";
+#elif defined(TARGET_DARWIN_IOS)
+ return "iOS";
+#elif defined(TARGET_DARWIN_TVOS)
+ return "tvOS";
+#elif defined(TARGET_FREEBSD)
+ return "FreeBSD";
+#elif defined(TARGET_ANDROID)
+ return "Android";
+#elif defined(TARGET_LINUX)
+ return "Linux";
+#elif defined(TARGET_WINDOWS)
+#ifdef NTDDI_VERSION
+ return "Windows NT";
+#else // !NTDDI_VERSION
+ return "unknown Win32 platform";
+#endif // !NTDDI_VERSION
+#else
+ return "unknown platform";
+#endif
+}
+
+std::string CSysInfo::GetBuildTargetPlatformVersion(void)
+{
+#if defined(TARGET_DARWIN_OSX)
+ 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)
+ return "API level " XSTR_MACRO(__ANDROID_API__);
+#elif defined(TARGET_LINUX)
+ return XSTR_MACRO(LINUX_VERSION_CODE);
+#elif defined(TARGET_WINDOWS)
+#ifdef NTDDI_VERSION
+ return XSTR_MACRO(NTDDI_VERSION);
+#else // !NTDDI_VERSION
+ return "(unknown Win32 platform)";
+#endif // !NTDDI_VERSION
+#else
+ return "(unknown platform)";
+#endif
+}
+
+std::string CSysInfo::GetBuildTargetPlatformVersionDecoded(void)
+{
+#if defined(TARGET_DARWIN_OSX)
+ if (__MAC_OS_X_VERSION_MIN_REQUIRED % 100)
+ return StringUtils::Format("version {}.{}.{}", __MAC_OS_X_VERSION_MIN_REQUIRED / 10000,
+ (__MAC_OS_X_VERSION_MIN_REQUIRED / 100) % 100,
+ __MAC_OS_X_VERSION_MIN_REQUIRED % 100);
+ else
+ return StringUtils::Format("version {}.{}", __MAC_OS_X_VERSION_MIN_REQUIRED / 10000,
+ (__MAC_OS_X_VERSION_MIN_REQUIRED / 100) % 100);
+#elif defined(TARGET_DARWIN_EMBEDDED)
+ std::string versionStr = GetBuildTargetPlatformVersion();
+ static const int major = (std::stoi(versionStr) / 10000) % 100;
+ static const int minor = (std::stoi(versionStr) / 100) % 100;
+ static const int rev = std::stoi(versionStr) % 100;
+ return StringUtils::Format("version {}.{}.{}", major, minor, rev);
+#elif defined(TARGET_FREEBSD)
+ // FIXME: should works well starting from FreeBSD 8.1
+ static const int major = (__FreeBSD_version / 100000) % 100;
+ static const int minor = (__FreeBSD_version / 1000) % 100;
+ static const int Rxx = __FreeBSD_version % 1000;
+ if ((major < 9 && Rxx == 0))
+ return StringUtils::Format("version {}.{}-RELEASE", major, minor);
+ if (Rxx >= 500)
+ return StringUtils::Format("version {}.{}-STABLE", major, minor);
+
+ return StringUtils::Format("version {}.{}-CURRENT", major, minor);
+#elif defined(TARGET_ANDROID)
+ return "API level " XSTR_MACRO(__ANDROID_API__);
+#elif defined(TARGET_LINUX)
+ return StringUtils::Format("version {}.{}.{}", (LINUX_VERSION_CODE >> 16) & 0xFF,
+ (LINUX_VERSION_CODE >> 8) & 0xFF, LINUX_VERSION_CODE & 0xFF);
+#elif defined(TARGET_WINDOWS)
+#ifdef NTDDI_VERSION
+ std::string version(StringUtils::Format("version {}.{}", int(NTDDI_VERSION >> 24) & 0xFF,
+ int(NTDDI_VERSION >> 16) & 0xFF));
+ if (SPVER(NTDDI_VERSION))
+ version += StringUtils::Format(" SP{}", int(SPVER(NTDDI_VERSION)));
+ return version;
+#else // !NTDDI_VERSION
+ return "(unknown Win32 platform)";
+#endif // !NTDDI_VERSION
+#else
+ return "(unknown platform)";
+#endif
+}
+
+std::string CSysInfo::GetBuildTargetCpuFamily(void)
+{
+#if defined(__thumb__) || defined(_M_ARMT)
+ return "ARM (Thumb)";
+#elif defined(__arm__) || defined(_M_ARM) || defined (__aarch64__)
+ return "ARM";
+#elif defined(__mips__) || defined(mips) || defined(__mips)
+ return "MIPS";
+#elif defined(__amd64__) || defined(__amd64) || defined(__x86_64__) || defined(__x86_64) || defined(_M_X64) || defined(_M_AMD64) || \
+ defined(i386) || defined(__i386) || defined(__i386__) || defined(__i486__) || defined(__i586__) || defined(__i686__) || defined(_M_IX86) || defined(_X86_)
+ return "x86";
+#elif defined(__s390x__)
+ return "s390";
+#elif defined(__powerpc) || defined(__powerpc__) || defined(__powerpc64__) || defined(__ppc__) || defined(__ppc64__) || defined(_M_PPC)
+ return "PowerPC";
+#elif defined(__riscv)
+ return "RISC-V";
+#else
+ return "unknown CPU family";
+#endif
+}
+
+std::string CSysInfo::GetUsedCompilerNameAndVer(void)
+{
+#if defined(__clang__)
+#ifdef __clang_version__
+ return "Clang " __clang_version__;
+#else // ! __clang_version__
+ return "Clang " XSTR_MACRO(__clang_major__) "." XSTR_MACRO(__clang_minor__) "." XSTR_MACRO(__clang_patchlevel__);
+#endif //! __clang_version__
+#elif defined (__INTEL_COMPILER)
+ return "Intel Compiler " XSTR_MACRO(__INTEL_COMPILER);
+#elif defined (__GNUC__)
+ std::string compilerStr;
+#ifdef __llvm__
+ /* Note: this will not detect GCC + DragonEgg */
+ compilerStr = "llvm-gcc ";
+#else // __llvm__
+ compilerStr = "GCC ";
+#endif // !__llvm__
+ compilerStr += XSTR_MACRO(__GNUC__) "." XSTR_MACRO(__GNUC_MINOR__) "." XSTR_MACRO(__GNUC_PATCHLEVEL__);
+ return compilerStr;
+#elif defined (_MSC_VER)
+ return "MSVC " XSTR_MACRO(_MSC_FULL_VER);
+#else
+ return "unknown compiler";
+#endif
+}
+
+std::string CSysInfo::GetPrivacyPolicy()
+{
+ if (m_privacyPolicy.empty())
+ {
+ CFile file;
+ std::vector<uint8_t> buf;
+ if (file.LoadFile("special://xbmc/privacy-policy.txt", buf) > 0)
+ {
+ m_privacyPolicy = std::string(reinterpret_cast<char*>(buf.data()), buf.size());
+ }
+ else
+ m_privacyPolicy = g_localizeStrings.Get(19055);
+ }
+ return m_privacyPolicy;
+}
+
+CSysInfo::WindowsDeviceFamily CSysInfo::GetWindowsDeviceFamily()
+{
+#ifdef TARGET_WINDOWS_STORE
+ auto familyName = AnalyticsInfo::VersionInfo().DeviceFamily();
+ if (familyName == L"Windows.Desktop")
+ return WindowsDeviceFamily::Desktop;
+ else if (familyName == L"Windows.Mobile")
+ return WindowsDeviceFamily::Mobile;
+ else if (familyName == L"Windows.Universal")
+ return WindowsDeviceFamily::IoT;
+ else if (familyName == L"Windows.Team")
+ return WindowsDeviceFamily::Surface;
+ else if (familyName == L"Windows.Xbox")
+ return WindowsDeviceFamily::Xbox;
+ else
+ return WindowsDeviceFamily::Other;
+#endif // TARGET_WINDOWS_STORE
+ return WindowsDeviceFamily::Desktop;
+}
+
+CJob *CSysInfo::GetJob() const
+{
+ return new CSysInfoJob();
+}
+
+void CSysInfo::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ m_info = static_cast<CSysInfoJob*>(job)->GetData();
+ CInfoLoader::OnJobComplete(jobID, success, job);
+}
diff --git a/xbmc/utils/SystemInfo.h b/xbmc/utils/SystemInfo.h
new file mode 100644
index 0000000..0facf9d
--- /dev/null
+++ b/xbmc/utils/SystemInfo.h
@@ -0,0 +1,165 @@
+/*
+ * 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 "InfoLoader.h"
+#include "settings/ISubSettings.h"
+
+#include <string>
+
+#define KB (1024) // 1 KiloByte (1KB) 1024 Byte (2^10 Byte)
+#define MB (1024*KB) // 1 MegaByte (1MB) 1024 KB (2^10 KB)
+#define GB (1024*MB) // 1 GigaByte (1GB) 1024 MB (2^10 MB)
+#define TB (1024*GB) // 1 TerraByte (1TB) 1024 GB (2^10 GB)
+
+#define MAX_KNOWN_ATTRIBUTES 46
+
+#define REG_CURRENT_VERSION L"Software\\Microsoft\\Windows NT\\CurrentVersion"
+
+
+class CSysData
+{
+public:
+ enum INTERNET_STATE { UNKNOWN, CONNECTED, DISCONNECTED };
+ CSysData()
+ {
+ Reset();
+ };
+
+ void Reset()
+ {
+ internetState = UNKNOWN;
+ };
+
+ std::string systemUptime;
+ std::string systemTotalUptime;
+ INTERNET_STATE internetState;
+ std::string videoEncoder;
+ std::string cpuFrequency;
+ std::string osVersionInfo;
+ std::string macAddress;
+ std::string batteryLevel;
+};
+
+class CSysInfoJob : public CJob
+{
+public:
+ CSysInfoJob();
+
+ bool DoWork() override;
+ const CSysData &GetData() const;
+
+ static CSysData::INTERNET_STATE GetInternetState();
+private:
+ static bool SystemUpTime(int iInputMinutes, int &iMinutes, int &iHours, int &iDays);
+ static std::string GetSystemUpTime(bool bTotalUptime);
+ static std::string GetMACAddress();
+ static std::string GetVideoEncoder();
+ static std::string GetBatteryLevel();
+
+ CSysData m_info;
+};
+
+class CSysInfo : public CInfoLoader, public ISubSettings
+{
+public:
+ enum WindowsVersion
+ {
+ WindowsVersionUnknown = -1, // Undetected, unsupported Windows version or OS in not Windows
+ WindowsVersionWin7, // Windows 7, Windows Server 2008 R2
+ WindowsVersionWin8, // Windows 8, Windows Server 2012
+ WindowsVersionWin8_1, // Windows 8.1
+ WindowsVersionWin10, // Windows 10
+ WindowsVersionWin10_1709, // Windows 10 1709 (FCU)
+ WindowsVersionWin10_1803, // Windows 10 1803
+ WindowsVersionWin10_1809, // Windows 10 1809
+ WindowsVersionWin10_1903, // Windows 10 1903
+ WindowsVersionWin10_1909, // Windows 10 1909
+ WindowsVersionWin10_2004, // Windows 10 2004
+ WindowsVersionWin10_Future, // Windows 10 future build
+ WindowsVersionWin11, // Windows 11
+ /* Insert new Windows versions here, when they'll be known */
+ WindowsVersionFuture = 100 // Future Windows version, not known to code
+ };
+ enum WindowsDeviceFamily
+ {
+ Mobile = 1,
+ Desktop = 2,
+ IoT = 3,
+ Xbox = 4,
+ Surface = 5,
+ Other = 100
+ };
+
+ CSysInfo(void);
+ ~CSysInfo() override;
+
+ bool Load(const TiXmlNode *settings) override;
+ bool Save(TiXmlNode *settings) const override;
+
+ char MD5_Sign[32 + 1];
+
+ static const std::string& GetAppName(void); // the same name as CCompileInfo::GetAppName(), but const ref to std::string
+
+ static std::string GetKernelName(bool emptyIfUnknown = false);
+ static std::string GetKernelVersionFull(void); // full version string, including "-generic", "-RELEASE" etc.
+ static std::string GetKernelVersion(void); // only digits with dots
+ static std::string GetOsName(bool emptyIfUnknown = false);
+ static std::string GetOsVersion(void);
+ static std::string GetOsPrettyNameWithVersion(void);
+ static std::string GetUserAgent();
+ static std::string GetDeviceName();
+ static std::string GetVersion();
+ static std::string GetVersionShort();
+ static std::string GetVersionCode();
+ static std::string GetVersionGit();
+ static std::string GetBuildDate();
+
+ bool HasInternet();
+ bool IsAeroDisabled();
+ static bool IsWindowsVersion(WindowsVersion ver);
+ static bool IsWindowsVersionAtLeast(WindowsVersion ver);
+ static WindowsVersion GetWindowsVersion();
+ static int GetKernelBitness(void);
+ static int GetXbmcBitness(void);
+ static const std::string& GetKernelCpuFamily(void);
+ static std::string GetManufacturerName(void);
+ static std::string GetModelName(void);
+ bool GetDiskSpace(std::string drive,int& iTotal, int& iTotalFree, int& iTotalUsed, int& iPercentFree, int& iPercentUsed);
+ std::string GetHddSpaceInfo(int& percent, int drive, bool shortText=false);
+ std::string GetHddSpaceInfo(int drive, bool shortText=false);
+
+ int GetTotalUptime() const { return m_iSystemTimeTotalUp; }
+ void SetTotalUptime(int uptime) { m_iSystemTimeTotalUp = uptime; }
+
+ static std::string GetBuildTargetPlatformName(void);
+ static std::string GetBuildTargetPlatformVersion(void);
+ static std::string GetBuildTargetPlatformVersionDecoded(void);
+ static std::string GetBuildTargetCpuFamily(void);
+
+ static std::string GetUsedCompilerNameAndVer(void);
+ std::string GetPrivacyPolicy();
+
+ static WindowsDeviceFamily GetWindowsDeviceFamily();
+
+protected:
+ CJob *GetJob() const override;
+ std::string TranslateInfo(int info) const override;
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+private:
+ CSysData m_info;
+ std::string m_privacyPolicy;
+ static WindowsVersion m_WinVer;
+ int m_iSystemTimeTotalUp; // Uptime in minutes!
+ void Reset();
+};
+
+extern CSysInfo g_sysinfo;
+
diff --git a/xbmc/utils/Temperature.cpp b/xbmc/utils/Temperature.cpp
new file mode 100644
index 0000000..15aad3a
--- /dev/null
+++ b/xbmc/utils/Temperature.cpp
@@ -0,0 +1,481 @@
+/*
+ * 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 "Temperature.h"
+
+#include "utils/Archive.h"
+#include "utils/StringUtils.h"
+
+#include <assert.h>
+
+CTemperature::CTemperature()
+{
+ m_value = 0.0;
+ m_valid=false;
+}
+
+CTemperature::CTemperature(const CTemperature& temperature)
+{
+ m_value=temperature.m_value;
+ m_valid=temperature.m_valid;
+}
+
+CTemperature::CTemperature(double value)
+{
+ m_value=value;
+ m_valid=true;
+}
+
+bool CTemperature::operator >(const CTemperature& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ if (!IsValid() || !right.IsValid())
+ return false;
+
+ if (this==&right)
+ return false;
+
+ return (m_value>right.m_value);
+}
+
+bool CTemperature::operator >=(const CTemperature& right) const
+{
+ return operator >(right) || operator ==(right);
+}
+
+bool CTemperature::operator <(const CTemperature& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ if (!IsValid() || !right.IsValid())
+ return false;
+
+ if (this==&right)
+ return false;
+
+ return (m_value<right.m_value);
+}
+
+bool CTemperature::operator <=(const CTemperature& right) const
+{
+ return operator <(right) || operator ==(right);
+}
+
+bool CTemperature::operator ==(const CTemperature& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ if (!IsValid() || !right.IsValid())
+ return false;
+
+ if (this==&right)
+ return true;
+
+ return (m_value==right.m_value);
+}
+
+bool CTemperature::operator !=(const CTemperature& right) const
+{
+ return !operator ==(right.m_value);
+}
+
+CTemperature& CTemperature::operator =(const CTemperature& right)
+{
+ m_valid=right.m_valid;
+ m_value=right.m_value;
+ return *this;
+}
+
+const CTemperature& CTemperature::operator +=(const CTemperature& right)
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ m_value+=right.m_value;
+ return *this;
+}
+
+const CTemperature& CTemperature::operator -=(const CTemperature& right)
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ m_value-=right.m_value;
+ return *this;
+}
+
+const CTemperature& CTemperature::operator *=(const CTemperature& right)
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ m_value*=right.m_value;
+ return *this;
+}
+
+const CTemperature& CTemperature::operator /=(const CTemperature& right)
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ m_value/=right.m_value;
+ return *this;
+}
+
+CTemperature CTemperature::operator +(const CTemperature& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ CTemperature temp(*this);
+
+ if (!IsValid() || !right.IsValid())
+ temp.SetValid(false);
+ else
+ temp.m_value+=right.m_value;
+
+ return temp;
+}
+
+CTemperature CTemperature::operator -(const CTemperature& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ CTemperature temp(*this);
+ if (!IsValid() || !right.IsValid())
+ temp.SetValid(false);
+ else
+ temp.m_value-=right.m_value;
+
+ return temp;
+}
+
+CTemperature CTemperature::operator *(const CTemperature& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ CTemperature temp(*this);
+ if (!IsValid() || !right.IsValid())
+ temp.SetValid(false);
+ else
+ temp.m_value*=right.m_value;
+ return temp;
+}
+
+CTemperature CTemperature::operator /(const CTemperature& right) const
+{
+ assert(IsValid());
+ assert(right.IsValid());
+
+ CTemperature temp(*this);
+ if (!IsValid() || !right.IsValid())
+ temp.SetValid(false);
+ else
+ temp.m_value/=right.m_value;
+ return temp;
+}
+
+CTemperature& CTemperature::operator ++()
+{
+ assert(IsValid());
+
+ m_value++;
+ return *this;
+}
+
+CTemperature& CTemperature::operator --()
+{
+ assert(IsValid());
+
+ m_value--;
+ return *this;
+}
+
+CTemperature CTemperature::operator ++(int)
+{
+ assert(IsValid());
+
+ CTemperature temp(*this);
+ m_value++;
+ return temp;
+}
+
+CTemperature CTemperature::operator --(int)
+{
+ assert(IsValid());
+
+ CTemperature temp(*this);
+ m_value--;
+ return temp;
+}
+
+bool CTemperature::operator >(double right) const
+{
+ assert(IsValid());
+
+ if (!IsValid())
+ return false;
+
+ return (m_value>right);
+}
+
+bool CTemperature::operator >=(double right) const
+{
+ return operator >(right) || operator ==(right);
+}
+
+bool CTemperature::operator <(double right) const
+{
+ assert(IsValid());
+
+ if (!IsValid())
+ return false;
+
+ return (m_value<right);
+}
+
+bool CTemperature::operator <=(double right) const
+{
+ return operator <(right) || operator ==(right);
+}
+
+bool CTemperature::operator ==(double right) const
+{
+ if (!IsValid())
+ return false;
+
+ return (m_value==right);
+}
+
+bool CTemperature::operator !=(double right) const
+{
+ return !operator ==(right);
+}
+
+const CTemperature& CTemperature::operator +=(double right)
+{
+ assert(IsValid());
+
+ m_value+=right;
+ return *this;
+}
+
+const CTemperature& CTemperature::operator -=(double right)
+{
+ assert(IsValid());
+
+ m_value-=right;
+ return *this;
+}
+
+const CTemperature& CTemperature::operator *=(double right)
+{
+ assert(IsValid());
+
+ m_value*=right;
+ return *this;
+}
+
+const CTemperature& CTemperature::operator /=(double right)
+{
+ assert(IsValid());
+
+ m_value/=right;
+ return *this;
+}
+
+CTemperature CTemperature::operator +(double right) const
+{
+ assert(IsValid());
+
+ CTemperature temp(*this);
+ temp.m_value+=right;
+ return temp;
+}
+
+CTemperature CTemperature::operator -(double right) const
+{
+ assert(IsValid());
+
+ CTemperature temp(*this);
+ temp.m_value-=right;
+ return temp;
+}
+
+CTemperature CTemperature::operator *(double right) const
+{
+ assert(IsValid());
+
+ CTemperature temp(*this);
+ temp.m_value*=right;
+ return temp;
+}
+
+CTemperature CTemperature::operator /(double right) const
+{
+ assert(IsValid());
+
+ CTemperature temp(*this);
+ temp.m_value/=right;
+ return temp;
+}
+
+CTemperature CTemperature::CreateFromFahrenheit(double value)
+{
+ return CTemperature(value);
+}
+
+CTemperature CTemperature::CreateFromReaumur(double value)
+{
+ return CTemperature(value * 2.25 + 32.0);
+}
+
+CTemperature CTemperature::CreateFromRankine(double value)
+{
+ return CTemperature(value - 459.67);
+}
+
+CTemperature CTemperature::CreateFromRomer(double value)
+{
+ return CTemperature((value - 7.5) * 24.0 / 7.0 + 32.0);
+}
+
+CTemperature CTemperature::CreateFromDelisle(double value)
+{
+ CTemperature temp(212.0 - value * 1.2);
+ return temp;
+}
+
+CTemperature CTemperature::CreateFromNewton(double value)
+{
+ return CTemperature(value * 60.0 / 11.0 + 32.0);
+}
+
+CTemperature CTemperature::CreateFromCelsius(double value)
+{
+ return CTemperature(value * 1.8 + 32.0);
+}
+
+CTemperature CTemperature::CreateFromKelvin(double value)
+{
+ return CTemperature((value - 273.15) * 1.8 + 32.0);
+}
+
+void CTemperature::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar<<m_value;
+ ar<<m_valid;
+ }
+ else
+ {
+ ar>>m_value;
+ ar>>m_valid;
+ }
+}
+
+bool CTemperature::IsValid() const
+{
+ return m_valid;
+}
+
+double CTemperature::ToFahrenheit() const
+{
+ return m_value;
+}
+
+double CTemperature::ToKelvin() const
+{
+ return (m_value + 459.67) / 1.8;
+}
+
+double CTemperature::ToCelsius() const
+{
+ return (m_value - 32.0) / 1.8;
+}
+
+double CTemperature::ToReaumur() const
+{
+ return (m_value - 32.0) / 2.25;
+}
+
+double CTemperature::ToRankine() const
+{
+ return m_value + 459.67;
+}
+
+double CTemperature::ToRomer() const
+{
+ return (m_value - 32.0) * 7.0 / 24.0 + 7.5;
+}
+
+double CTemperature::ToDelisle() const
+{
+ return (212.0 - m_value) * 5.0 / 6.0;
+}
+
+double CTemperature::ToNewton() const
+{
+ return (m_value - 32.0) * 11.0 / 60.0;
+}
+
+double CTemperature::To(Unit temperatureUnit) const
+{
+ if (!IsValid())
+ return 0;
+
+ double value = 0.0;
+
+ switch (temperatureUnit)
+ {
+ case UnitFahrenheit:
+ value=ToFahrenheit();
+ break;
+ case UnitKelvin:
+ value=ToKelvin();
+ break;
+ case UnitCelsius:
+ value=ToCelsius();
+ break;
+ case UnitReaumur:
+ value=ToReaumur();
+ break;
+ case UnitRankine:
+ value=ToRankine();
+ break;
+ case UnitRomer:
+ value=ToRomer();
+ break;
+ case UnitDelisle:
+ value=ToDelisle();
+ break;
+ case UnitNewton:
+ value=ToNewton();
+ break;
+ default:
+ assert(false);
+ break;
+ }
+ return value;
+}
+
+// Returns temperature as localized string
+std::string CTemperature::ToString(Unit temperatureUnit) const
+{
+ if (!IsValid())
+ return "";
+
+ return StringUtils::Format("{:2.0f}", To(temperatureUnit));
+}
diff --git a/xbmc/utils/Temperature.h b/xbmc/utils/Temperature.h
new file mode 100644
index 0000000..9d2a019
--- /dev/null
+++ b/xbmc/utils/Temperature.h
@@ -0,0 +1,103 @@
+/*
+ * 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 "utils/IArchivable.h"
+
+#include <string>
+
+class CTemperature : public IArchivable
+{
+public:
+ CTemperature();
+ CTemperature(const CTemperature& temperature);
+
+ typedef enum Unit
+ {
+ UnitFahrenheit = 0,
+ UnitKelvin,
+ UnitCelsius,
+ UnitReaumur,
+ UnitRankine,
+ UnitRomer,
+ UnitDelisle,
+ UnitNewton
+ } Unit;
+
+ static CTemperature CreateFromFahrenheit(double value);
+ static CTemperature CreateFromKelvin(double value);
+ static CTemperature CreateFromCelsius(double value);
+ static CTemperature CreateFromReaumur(double value);
+ static CTemperature CreateFromRankine(double value);
+ static CTemperature CreateFromRomer(double value);
+ static CTemperature CreateFromDelisle(double value);
+ static CTemperature CreateFromNewton(double value);
+
+ bool operator >(const CTemperature& right) const;
+ bool operator >=(const CTemperature& right) const;
+ bool operator <(const CTemperature& right) const;
+ bool operator <=(const CTemperature& right) const;
+ bool operator ==(const CTemperature& right) const;
+ bool operator !=(const CTemperature& right) const;
+
+ CTemperature& operator =(const CTemperature& right);
+ const CTemperature& operator +=(const CTemperature& right);
+ const CTemperature& operator -=(const CTemperature& right);
+ const CTemperature& operator *=(const CTemperature& right);
+ const CTemperature& operator /=(const CTemperature& right);
+ CTemperature operator +(const CTemperature& right) const;
+ CTemperature operator -(const CTemperature& right) const;
+ CTemperature operator *(const CTemperature& right) const;
+ CTemperature operator /(const CTemperature& right) const;
+
+ bool operator >(double right) const;
+ bool operator >=(double right) const;
+ bool operator <(double right) const;
+ bool operator <=(double right) const;
+ bool operator ==(double right) const;
+ bool operator !=(double right) const;
+
+ const CTemperature& operator +=(double right);
+ const CTemperature& operator -=(double right);
+ const CTemperature& operator *=(double right);
+ const CTemperature& operator /=(double right);
+ CTemperature operator +(double right) const;
+ CTemperature operator -(double right) const;
+ CTemperature operator *(double right) const;
+ CTemperature operator /(double right) const;
+
+ CTemperature& operator ++();
+ CTemperature& operator --();
+ CTemperature operator ++(int);
+ CTemperature operator --(int);
+
+ void Archive(CArchive& ar) override;
+
+ bool IsValid() const;
+ void SetValid(bool valid) { m_valid = valid; }
+
+ double ToFahrenheit() const;
+ double ToKelvin() const;
+ double ToCelsius() const;
+ double ToReaumur() const;
+ double ToRankine() const;
+ double ToRomer() const;
+ double ToDelisle() const;
+ double ToNewton() const;
+
+ double To(Unit temperatureUnit) const;
+ std::string ToString(Unit temperatureUnit) const;
+
+protected:
+ explicit CTemperature(double value);
+
+ double m_value; // we store as fahrenheit
+ bool m_valid;
+};
+
diff --git a/xbmc/utils/TextSearch.cpp b/xbmc/utils/TextSearch.cpp
new file mode 100644
index 0000000..1ada61d
--- /dev/null
+++ b/xbmc/utils/TextSearch.cpp
@@ -0,0 +1,146 @@
+/*
+ * 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 "TextSearch.h"
+
+#include "StringUtils.h"
+
+CTextSearch::CTextSearch(const std::string &strSearchTerms, bool bCaseSensitive /* = false */, TextSearchDefault defaultSearchMode /* = SEARCH_DEFAULT_OR */)
+{
+ m_bCaseSensitive = bCaseSensitive;
+ ExtractSearchTerms(strSearchTerms, defaultSearchMode);
+}
+
+bool CTextSearch::IsValid(void) const
+{
+ return m_AND.size() > 0 || m_OR.size() > 0 || m_NOT.size() > 0;
+}
+
+bool CTextSearch::Search(const std::string &strHaystack) const
+{
+ if (strHaystack.empty() || !IsValid())
+ return false;
+
+ std::string strSearch(strHaystack);
+ if (!m_bCaseSensitive)
+ StringUtils::ToLower(strSearch);
+
+ /* check whether any of the NOT terms matches and return false if there's a match */
+ for (unsigned int iNotPtr = 0; iNotPtr < m_NOT.size(); iNotPtr++)
+ {
+ if (strSearch.find(m_NOT.at(iNotPtr)) != std::string::npos)
+ return false;
+ }
+
+ /* check whether at least one of the OR terms matches and return false if there's no match found */
+ bool bFound(m_OR.empty());
+ for (unsigned int iOrPtr = 0; iOrPtr < m_OR.size(); iOrPtr++)
+ {
+ if (strSearch.find(m_OR.at(iOrPtr)) != std::string::npos)
+ {
+ bFound = true;
+ break;
+ }
+ }
+ if (!bFound)
+ return false;
+
+ /* check whether all of the AND terms match and return false if one of them wasn't found */
+ for (unsigned int iAndPtr = 0; iAndPtr < m_AND.size(); iAndPtr++)
+ {
+ if (strSearch.find(m_AND[iAndPtr]) == std::string::npos)
+ return false;
+ }
+
+ /* all ok, return true */
+ return true;
+}
+
+void CTextSearch::GetAndCutNextTerm(std::string &strSearchTerm, std::string &strNextTerm)
+{
+ std::string strFindNext(" ");
+
+ if (StringUtils::EndsWith(strSearchTerm, "\""))
+ {
+ strSearchTerm.erase(0, 1);
+ strFindNext = "\"";
+ }
+
+ size_t iNextPos = strSearchTerm.find(strFindNext);
+ if (iNextPos != std::string::npos)
+ {
+ strNextTerm = strSearchTerm.substr(0, iNextPos);
+ strSearchTerm.erase(0, iNextPos + 1);
+ }
+ else
+ {
+ strNextTerm = strSearchTerm;
+ strSearchTerm.clear();
+ }
+}
+
+void CTextSearch::ExtractSearchTerms(const std::string &strSearchTerm, TextSearchDefault defaultSearchMode)
+{
+ std::string strParsedSearchTerm(strSearchTerm);
+ StringUtils::Trim(strParsedSearchTerm);
+
+ if (!m_bCaseSensitive)
+ StringUtils::ToLower(strParsedSearchTerm);
+
+ bool bNextAND(defaultSearchMode == SEARCH_DEFAULT_AND);
+ bool bNextOR(defaultSearchMode == SEARCH_DEFAULT_OR);
+ bool bNextNOT(defaultSearchMode == SEARCH_DEFAULT_NOT);
+
+ while (strParsedSearchTerm.length() > 0)
+ {
+ StringUtils::TrimLeft(strParsedSearchTerm);
+
+ if (StringUtils::StartsWith(strParsedSearchTerm, "!") || StringUtils::StartsWithNoCase(strParsedSearchTerm, "not"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ bNextNOT = true;
+ }
+ else if (StringUtils::StartsWith(strParsedSearchTerm, "+") || StringUtils::StartsWithNoCase(strParsedSearchTerm, "and"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ bNextAND = true;
+ }
+ else if (StringUtils::StartsWith(strParsedSearchTerm, "|") || StringUtils::StartsWithNoCase(strParsedSearchTerm, "or"))
+ {
+ std::string strDummy;
+ GetAndCutNextTerm(strParsedSearchTerm, strDummy);
+ bNextOR = true;
+ }
+ else
+ {
+ std::string strTerm;
+ GetAndCutNextTerm(strParsedSearchTerm, strTerm);
+ if (strTerm.length() > 0)
+ {
+ if (bNextAND)
+ m_AND.push_back(strTerm);
+ else if (bNextOR)
+ m_OR.push_back(strTerm);
+ else if (bNextNOT)
+ m_NOT.push_back(strTerm);
+ }
+ else
+ {
+ break;
+ }
+
+ bNextAND = (defaultSearchMode == SEARCH_DEFAULT_AND);
+ bNextOR = (defaultSearchMode == SEARCH_DEFAULT_OR);
+ bNextNOT = (defaultSearchMode == SEARCH_DEFAULT_NOT);
+ }
+
+ StringUtils::TrimLeft(strParsedSearchTerm);
+ }
+}
diff --git a/xbmc/utils/TextSearch.h b/xbmc/utils/TextSearch.h
new file mode 100644
index 0000000..f2d1fdb
--- /dev/null
+++ b/xbmc/utils/TextSearch.h
@@ -0,0 +1,37 @@
+/*
+ * 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 <string>
+#include <vector>
+
+typedef enum TextSearchDefault
+{
+ SEARCH_DEFAULT_AND = 0,
+ SEARCH_DEFAULT_OR,
+ SEARCH_DEFAULT_NOT
+} TextSearchDefault;
+
+class CTextSearch final
+{
+public:
+ CTextSearch(const std::string &strSearchTerms, bool bCaseSensitive = false, TextSearchDefault defaultSearchMode = SEARCH_DEFAULT_OR);
+
+ bool Search(const std::string &strHaystack) const;
+ bool IsValid(void) const;
+
+private:
+ static void GetAndCutNextTerm(std::string &strSearchTerm, std::string &strNextTerm);
+ void ExtractSearchTerms(const std::string &strSearchTerm, TextSearchDefault defaultSearchMode);
+
+ bool m_bCaseSensitive;
+ std::vector<std::string> m_AND;
+ std::vector<std::string> m_OR;
+ std::vector<std::string> m_NOT;
+};
diff --git a/xbmc/utils/TimeFormat.h b/xbmc/utils/TimeFormat.h
new file mode 100644
index 0000000..595f532
--- /dev/null
+++ b/xbmc/utils/TimeFormat.h
@@ -0,0 +1,67 @@
+/*
+ * 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
+
+/*! \brief TIME_FORMAT enum/bitmask used for formatting time strings
+ Note the use of bitmasking, e.g.
+ TIME_FORMAT_HH_MM_SS = TIME_FORMAT_HH | TIME_FORMAT_MM | TIME_FORMAT_SS
+ \sa StringUtils::SecondsToTimeString
+ \note For InfoLabels use the equivalent value listed (bold)
+ on the description of each enum value.
+ \note<b>Example:</b> 3661 seconds => h=1, hh=01, m=1, mm=01, ss=01, hours=1, mins=61, secs=3661
+ <p><hr>
+ @skinning_v18 **[Infolabels Updated]** Added <b>secs</b>, <b>mins</b>, <b>hours</b> (total time) and **m** as possible formats for
+ InfoLabels that support the definition of a time format. Examples are:
+ - \link Player_SeekOffset_format `Player.SeekOffset(format)`\endlink
+ - \link Player_TimeRemaining_format `Player.TimeRemaining(format)`\endlink
+ - \link Player_Time_format `Player.Time(format)`\endlink
+ - \link Player_Duration_format `Player.Duration(format)`\endlink
+ - \link Player_FinishTime_format `Player.FinishTime(format)`\endlink
+ - \link Player_StartTime_format `Player.StartTime(format)` \endlink
+ - \link Player_SeekNumeric_format `Player.SeekNumeric(format)`\endlink
+ - \link ListItem_Duration_format `ListItem.Duration(format)`\endlink
+ - \link PVR_EpgEventDuration_format `PVR.EpgEventDuration(format)`\endlink
+ - \link PVR_EpgEventElapsedTime_format `PVR.EpgEventElapsedTime(format)`\endlink
+ - \link PVR_EpgEventRemainingTime_format `PVR.EpgEventRemainingTime(format)`\endlink
+ - \link PVR_EpgEventSeekTime_format `PVR.EpgEventSeekTime(format)`\endlink
+ - \link PVR_EpgEventFinishTime_format `PVR.EpgEventFinishTime(format)`\endlink
+ - \link PVR_TimeShiftStart_format `PVR.TimeShiftStart(format)`\endlink
+ - \link PVR_TimeShiftEnd_format `PVR.TimeShiftEnd(format)`\endlink
+ - \link PVR_TimeShiftCur_format `PVR.TimeShiftCur(format)`\endlink
+ - \link PVR_TimeShiftOffset_format `PVR.TimeShiftOffset(format)`\endlink
+ - \link PVR_TimeshiftProgressDuration_format `PVR.TimeshiftProgressDuration(format)`\endlink
+ - \link PVR_TimeshiftProgressEndTime `PVR.TimeshiftProgressEndTime`\endlink
+ - \link PVR_TimeshiftProgressEndTime_format `PVR.TimeshiftProgressEndTime(format)`\endlink
+ - \link ListItem_NextDuration_format `ListItem.NextDuration(format)` \endlink
+ <p>
+ */
+enum TIME_FORMAT
+{
+ TIME_FORMAT_GUESS = 0, ///< usually used as the fallback value if the format value is empty
+ TIME_FORMAT_SS = 1, ///< <b>ss</b> - seconds only
+ TIME_FORMAT_MM = 2, ///< <b>mm</b> - minutes only (2-digit)
+ TIME_FORMAT_MM_SS = 3, ///< <b>mm:ss</b> - minutes and seconds
+ TIME_FORMAT_HH = 4, ///< <b>hh</b> - hours only (2-digit)
+ TIME_FORMAT_HH_SS = 5, ///< <b>hh:ss</b> - hours and seconds (this is not particularly useful)
+ TIME_FORMAT_HH_MM = 6, ///< <b>hh:mm</b> - hours and minutes
+ TIME_FORMAT_HH_MM_SS = 7, ///< <b>hh:mm:ss</b> - hours, minutes and seconds
+ TIME_FORMAT_XX = 8, ///< <b>xx</b> - returns AM/PM for a 12-hour clock
+ TIME_FORMAT_HH_MM_XX =
+ 14, ///< <b>hh:mm xx</b> - returns hours and minutes in a 12-hour clock format (AM/PM)
+ TIME_FORMAT_HH_MM_SS_XX =
+ 15, ///< <b>hh:mm:ss xx</b> - returns hours (2-digit), minutes and seconds in a 12-hour clock format (AM/PM)
+ TIME_FORMAT_H = 16, ///< <b>h</b> - hours only (1-digit)
+ TIME_FORMAT_H_MM_SS = 19, ///< <b>hh:mm:ss</b> - hours, minutes and seconds
+ TIME_FORMAT_H_MM_SS_XX =
+ 27, ///< <b>hh:mm:ss xx</b> - returns hours (1-digit), minutes and seconds in a 12-hour clock format (AM/PM)
+ TIME_FORMAT_SECS = 32, ///< <b>secs</b> - total time in seconds
+ TIME_FORMAT_MINS = 64, ///< <b>mins</b> - total time in minutes
+ TIME_FORMAT_HOURS = 128, ///< <b>hours</b> - total time in hours
+ TIME_FORMAT_M = 256 ///< <b>m</b> - minutes only (1-digit)
+};
diff --git a/xbmc/utils/TimeUtils.cpp b/xbmc/utils/TimeUtils.cpp
new file mode 100644
index 0000000..95c5069
--- /dev/null
+++ b/xbmc/utils/TimeUtils.cpp
@@ -0,0 +1,108 @@
+/*
+ * 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 "TimeUtils.h"
+#include "XBDateTime.h"
+#include "windowing/GraphicContext.h"
+
+#if defined(TARGET_DARWIN)
+#include <mach/mach_time.h>
+#include <CoreVideo/CVHostTime.h>
+#elif defined(TARGET_WINDOWS)
+#include <windows.h>
+#else
+#include <time.h>
+#endif
+
+namespace
+{
+auto startTime = std::chrono::steady_clock::now();
+}
+
+int64_t CurrentHostCounter(void)
+{
+#if defined(TARGET_DARWIN)
+ return( (int64_t)CVGetCurrentHostTime() );
+#elif defined(TARGET_WINDOWS)
+ LARGE_INTEGER PerformanceCount;
+ QueryPerformanceCounter(&PerformanceCount);
+ return( (int64_t)PerformanceCount.QuadPart );
+#else
+ struct timespec now;
+#if defined(CLOCK_MONOTONIC_RAW) && !defined(TARGET_ANDROID)
+ clock_gettime(CLOCK_MONOTONIC_RAW, &now);
+#else
+ clock_gettime(CLOCK_MONOTONIC, &now);
+#endif // CLOCK_MONOTONIC_RAW && !TARGET_ANDROID
+ return( ((int64_t)now.tv_sec * 1000000000L) + now.tv_nsec );
+#endif
+}
+
+int64_t CurrentHostFrequency(void)
+{
+#if defined(TARGET_DARWIN)
+ return( (int64_t)CVGetHostClockFrequency() );
+#elif defined(TARGET_WINDOWS)
+ LARGE_INTEGER Frequency;
+ QueryPerformanceFrequency(&Frequency);
+ return( (int64_t)Frequency.QuadPart );
+#else
+ return( (int64_t)1000000000L );
+#endif
+}
+
+unsigned int CTimeUtils::frameTime = 0;
+
+void CTimeUtils::UpdateFrameTime(bool flip)
+{
+ auto now = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - startTime);
+
+ unsigned int currentTime = duration.count();
+ unsigned int last = frameTime;
+ while (frameTime < currentTime)
+ {
+ frameTime += (unsigned int)(1000 / CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS());
+ // observe wrap around
+ if (frameTime < last)
+ break;
+ }
+}
+
+unsigned int CTimeUtils::GetFrameTime()
+{
+ return frameTime;
+}
+
+CDateTime CTimeUtils::GetLocalTime(time_t time)
+{
+ CDateTime result;
+
+ tm *local;
+#ifdef HAVE_LOCALTIME_R
+ tm res = {};
+ local = localtime_r(&time, &res); // Conversion to local time
+#else
+ local = localtime(&time); // Conversion to local time
+#endif
+ /*
+ * Microsoft implementation of localtime returns NULL if on or before epoch.
+ * http://msdn.microsoft.com/en-us/library/bf12f0hc(VS.80).aspx
+ */
+ if (local)
+ result = *local;
+ else
+ result = time; // Use the original time as close enough.
+
+ return result;
+}
+
+std::string CTimeUtils::WithoutSeconds(const std::string& hhmmss)
+{
+ return hhmmss.substr(0, 5);
+}
diff --git a/xbmc/utils/TimeUtils.h b/xbmc/utils/TimeUtils.h
new file mode 100644
index 0000000..d7740d7
--- /dev/null
+++ b/xbmc/utils/TimeUtils.h
@@ -0,0 +1,46 @@
+/*
+ * 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 <stdint.h>
+#include <string>
+#include <time.h>
+
+class CDateTime;
+
+int64_t CurrentHostCounter(void);
+int64_t CurrentHostFrequency(void);
+
+class CTimeUtils
+{
+public:
+
+ /*!
+ * @brief Update the time frame
+ * @note Not threadsafe
+ */
+ static void UpdateFrameTime(bool flip);
+
+ /*!
+ * @brief Returns the frame time in MS
+ * @note Not threadsafe
+ */
+ static unsigned int GetFrameTime();
+ static CDateTime GetLocalTime(time_t time);
+
+ /*!
+ * @brief Returns a time string without seconds, i.e: HH:MM
+ * @param hhmmss Time string in the format HH:MM:SS
+ */
+ static std::string WithoutSeconds(const std::string& hhmmss);
+
+private:
+ static unsigned int frameTime;
+};
+
diff --git a/xbmc/utils/TransformMatrix.h b/xbmc/utils/TransformMatrix.h
new file mode 100644
index 0000000..a9bf8fd
--- /dev/null
+++ b/xbmc/utils/TransformMatrix.h
@@ -0,0 +1,296 @@
+/*
+ * 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 "utils/ColorUtils.h"
+
+#include <algorithm>
+#include <math.h>
+#include <memory>
+#include <string.h>
+
+#ifdef __GNUC__
+// under gcc, inline will only take place if optimizations are applied (-O). this will force inline even with optimizations.
+#define XBMC_FORCE_INLINE __attribute__((always_inline))
+#else
+#define XBMC_FORCE_INLINE
+#endif
+
+class TransformMatrix
+{
+public:
+ TransformMatrix()
+ {
+ Reset();
+ };
+ void Reset()
+ {
+ m[0][0] = 1.0f; m[0][1] = m[0][2] = m[0][3] = 0.0f;
+ m[1][0] = m[1][2] = m[1][3] = 0.0f; m[1][1] = 1.0f;
+ m[2][0] = m[2][1] = m[2][3] = 0.0f; m[2][2] = 1.0f;
+ alpha = red = green = blue = 1.0f;
+ identity = true;
+ };
+ static TransformMatrix CreateTranslation(float transX, float transY, float transZ = 0)
+ {
+ TransformMatrix translation;
+ translation.SetTranslation(transX, transY, transZ);
+ return translation;
+ }
+ void SetTranslation(float transX, float transY, float transZ)
+ {
+ m[0][1] = m[0][2] = 0.0f; m[0][0] = 1.0f; m[0][3] = transX;
+ m[1][0] = m[1][2] = 0.0f; m[1][1] = 1.0f; m[1][3] = transY;
+ m[2][0] = m[2][1] = 0.0f; m[2][2] = 1.0f; m[2][3] = transZ;
+ alpha = red = green = blue = 1.0f;
+ identity = (transX == 0 && transY == 0 && transZ == 0);
+ }
+ static TransformMatrix CreateScaler(float scaleX, float scaleY, float scaleZ = 1.0f)
+ {
+ TransformMatrix scaler;
+ scaler.m[0][0] = scaleX;
+ scaler.m[1][1] = scaleY;
+ scaler.m[2][2] = scaleZ;
+ scaler.identity = (scaleX == 1 && scaleY == 1 && scaleZ == 1);
+ return scaler;
+ };
+ void SetScaler(float scaleX, float scaleY, float centerX, float centerY)
+ {
+ // Trans(centerX,centerY,centerZ)*Scale(scaleX,scaleY,scaleZ)*Trans(-centerX,-centerY,-centerZ)
+ float centerZ = 0.0f, scaleZ = 1.0f;
+ m[0][0] = scaleX; m[0][1] = 0.0f; m[0][2] = 0.0f; m[0][3] = centerX*(1-scaleX);
+ m[1][0] = 0.0f; m[1][1] = scaleY; m[1][2] = 0.0f; m[1][3] = centerY*(1-scaleY);
+ m[2][0] = 0.0f; m[2][1] = 0.0f; m[2][2] = scaleZ; m[2][3] = centerZ*(1-scaleZ);
+ alpha = red = green = blue = 1.0f;
+ identity = (scaleX == 1 && scaleY == 1);
+ };
+ void SetXRotation(float angle, float y, float z, float ar = 1.0f)
+ { // angle about the X axis, centered at y,z where our coordinate system has aspect ratio ar.
+ // Trans(0,y,z)*Scale(1,1/ar,1)*RotateX(angle)*Scale(ar,1,1)*Trans(0,-y,-z);
+ float c = cos(angle); float s = sin(angle);
+ m[0][0] = ar; m[0][1] = 0.0f; m[0][2] = 0.0f; m[0][3] = 0.0f;
+ m[1][0] = 0.0f; m[1][1] = c/ar; m[1][2] = -s/ar; m[1][3] = (-y*c+s*z)/ar + y;
+ m[2][0] = 0.0f; m[2][1] = s; m[2][2] = c; m[2][3] = (-y*s-c*z) + z;
+ alpha = red = green = blue = 1.0f;
+ identity = (angle == 0);
+ }
+ void SetYRotation(float angle, float x, float z, float ar = 1.0f)
+ { // angle about the Y axis, centered at x,z where our coordinate system has aspect ratio ar.
+ // Trans(x,0,z)*Scale(1/ar,1,1)*RotateY(angle)*Scale(ar,1,1)*Trans(-x,0,-z);
+ float c = cos(angle); float s = sin(angle);
+ m[0][0] = c; m[0][1] = 0.0f; m[0][2] = -s/ar; m[0][3] = -x*c + s*z/ar + x;
+ m[1][0] = 0.0f; m[1][1] = 1.0f; m[1][2] = 0.0f; m[1][3] = 0.0f;
+ m[2][0] = ar*s; m[2][1] = 0.0f; m[2][2] = c; m[2][3] = -ar*x*s - c*z + z;
+ alpha = red = green = blue = 1.0f;
+ identity = (angle == 0);
+ }
+ static TransformMatrix CreateZRotation(float angle, float x, float y, float ar = 1.0f)
+ { // angle about the Z axis, centered at x,y where our coordinate system has aspect ratio ar.
+ // Trans(x,y,0)*Scale(1/ar,1,1)*RotateZ(angle)*Scale(ar,1,1)*Trans(-x,-y,0)
+ TransformMatrix rot;
+ rot.SetZRotation(angle, x, y, ar);
+ return rot;
+ }
+ void SetZRotation(float angle, float x, float y, float ar = 1.0f)
+ { // angle about the Z axis, centered at x,y where our coordinate system has aspect ratio ar.
+ // Trans(x,y,0)*Scale(1/ar,1,1)*RotateZ(angle)*Scale(ar,1,1)*Trans(-x,-y,0)
+ float c = cos(angle); float s = sin(angle);
+ m[0][0] = c; m[0][1] = -s/ar; m[0][2] = 0.0f; m[0][3] = -x*c + s*y/ar + x;
+ m[1][0] = s*ar; m[1][1] = c; m[1][2] = 0.0f; m[1][3] = -ar*x*s - c*y + y;
+ m[2][0] = 0.0f; m[2][1] = 0.0f; m[2][2] = 1.0f; m[2][3] = 0.0f;
+ alpha = red = green = blue = 1.0f;
+ identity = (angle == 0);
+ }
+ static TransformMatrix CreateFader(float a)
+ {
+ TransformMatrix fader;
+ fader.SetFader(a);
+ return fader;
+ }
+ static TransformMatrix CreateFader(float a, float r, float g, float b)
+ {
+ TransformMatrix fader;
+ fader.SetFader(a, r, g, b);
+ return fader;
+ }
+ void SetFader(float a)
+ {
+ m[0][0] = 1.0f; m[0][1] = 0.0f; m[0][2] = 0.0f; m[0][3] = 0.0f;
+ m[1][0] = 0.0f; m[1][1] = 1.0f; m[1][2] = 0.0f; m[1][3] = 0.0f;
+ m[2][0] = 0.0f; m[2][1] = 0.0f; m[2][2] = 1.0f; m[2][3] = 0.0f;
+ alpha = a;
+ red = green = blue = 1.0f;
+ identity = (a == 1.0f);
+ }
+
+ void SetFader(float a, float r, float g, float b)
+ {
+ m[0][0] = 1.0f; m[0][1] = 0.0f; m[0][2] = 0.0f; m[0][3] = 0.0f;
+ m[1][0] = 0.0f; m[1][1] = 1.0f; m[1][2] = 0.0f; m[1][3] = 0.0f;
+ m[2][0] = 0.0f; m[2][1] = 0.0f; m[2][2] = 1.0f; m[2][3] = 0.0f;
+ alpha = a;
+ red = r;
+ green = g;
+ blue = b;
+ identity = ((a == 1.0f) && (r == 1.0f) && (g == 1.0f) && (b == 1.0f));
+ }
+
+ // multiplication operators
+ const TransformMatrix &operator *=(const TransformMatrix &right)
+ {
+ if (right.identity)
+ return *this;
+ if (identity)
+ {
+ *this = right;
+ return *this;
+ }
+ float t00 = m[0][0] * right.m[0][0] + m[0][1] * right.m[1][0] + m[0][2] * right.m[2][0];
+ float t01 = m[0][0] * right.m[0][1] + m[0][1] * right.m[1][1] + m[0][2] * right.m[2][1];
+ float t02 = m[0][0] * right.m[0][2] + m[0][1] * right.m[1][2] + m[0][2] * right.m[2][2];
+ m[0][3] = m[0][0] * right.m[0][3] + m[0][1] * right.m[1][3] + m[0][2] * right.m[2][3] + m[0][3];
+ m[0][0] = t00; m[0][1] = t01; m[0][2] = t02;
+ t00 = m[1][0] * right.m[0][0] + m[1][1] * right.m[1][0] + m[1][2] * right.m[2][0];
+ t01 = m[1][0] * right.m[0][1] + m[1][1] * right.m[1][1] + m[1][2] * right.m[2][1];
+ t02 = m[1][0] * right.m[0][2] + m[1][1] * right.m[1][2] + m[1][2] * right.m[2][2];
+ m[1][3] = m[1][0] * right.m[0][3] + m[1][1] * right.m[1][3] + m[1][2] * right.m[2][3] + m[1][3];
+ m[1][0] = t00; m[1][1] = t01; m[1][2] = t02;
+ t00 = m[2][0] * right.m[0][0] + m[2][1] * right.m[1][0] + m[2][2] * right.m[2][0];
+ t01 = m[2][0] * right.m[0][1] + m[2][1] * right.m[1][1] + m[2][2] * right.m[2][1];
+ t02 = m[2][0] * right.m[0][2] + m[2][1] * right.m[1][2] + m[2][2] * right.m[2][2];
+ m[2][3] = m[2][0] * right.m[0][3] + m[2][1] * right.m[1][3] + m[2][2] * right.m[2][3] + m[2][3];
+ m[2][0] = t00; m[2][1] = t01; m[2][2] = t02;
+ alpha *= right.alpha;
+ red *= right.red;
+ green *= right.green;
+ blue *= right.blue;
+ identity = false;
+ return *this;
+ }
+
+ TransformMatrix operator *(const TransformMatrix &right) const
+ {
+ if (right.identity)
+ return *this;
+ if (identity)
+ return right;
+ TransformMatrix result;
+ result.m[0][0] = m[0][0] * right.m[0][0] + m[0][1] * right.m[1][0] + m[0][2] * right.m[2][0];
+ result.m[0][1] = m[0][0] * right.m[0][1] + m[0][1] * right.m[1][1] + m[0][2] * right.m[2][1];
+ result.m[0][2] = m[0][0] * right.m[0][2] + m[0][1] * right.m[1][2] + m[0][2] * right.m[2][2];
+ result.m[0][3] = m[0][0] * right.m[0][3] + m[0][1] * right.m[1][3] + m[0][2] * right.m[2][3] + m[0][3];
+ result.m[1][0] = m[1][0] * right.m[0][0] + m[1][1] * right.m[1][0] + m[1][2] * right.m[2][0];
+ result.m[1][1] = m[1][0] * right.m[0][1] + m[1][1] * right.m[1][1] + m[1][2] * right.m[2][1];
+ result.m[1][2] = m[1][0] * right.m[0][2] + m[1][1] * right.m[1][2] + m[1][2] * right.m[2][2];
+ result.m[1][3] = m[1][0] * right.m[0][3] + m[1][1] * right.m[1][3] + m[1][2] * right.m[2][3] + m[1][3];
+ result.m[2][0] = m[2][0] * right.m[0][0] + m[2][1] * right.m[1][0] + m[2][2] * right.m[2][0];
+ result.m[2][1] = m[2][0] * right.m[0][1] + m[2][1] * right.m[1][1] + m[2][2] * right.m[2][1];
+ result.m[2][2] = m[2][0] * right.m[0][2] + m[2][1] * right.m[1][2] + m[2][2] * right.m[2][2];
+ result.m[2][3] = m[2][0] * right.m[0][3] + m[2][1] * right.m[1][3] + m[2][2] * right.m[2][3] + m[2][3];
+ result.alpha = alpha * right.alpha;
+ result.red = red * right.red;
+ result.green = green * right.green;
+ result.blue = blue * right.blue;
+ result.identity = false;
+ return result;
+ }
+
+ inline void TransformPosition(float &x, float &y, float &z) const XBMC_FORCE_INLINE
+ {
+ float newX = m[0][0] * x + m[0][1] * y + m[0][2] * z + m[0][3];
+ float newY = m[1][0] * x + m[1][1] * y + m[1][2] * z + m[1][3];
+ z = m[2][0] * x + m[2][1] * y + m[2][2] * z + m[2][3];
+ y = newY;
+ x = newX;
+ }
+
+ inline void TransformPositionUnscaled(float &x, float &y, float &z) const XBMC_FORCE_INLINE
+ {
+ float n;
+ // calculate the norm of the transformed (but not translated) vectors involved
+ n = sqrt(m[0][0]*m[0][0] + m[0][1]*m[0][1] + m[0][2]*m[0][2]);
+ float newX = (m[0][0] * x + m[0][1] * y + m[0][2] * z)/n + m[0][3];
+ n = sqrt(m[1][0]*m[1][0] + m[1][1]*m[1][1] + m[1][2]*m[1][2]);
+ float newY = (m[1][0] * x + m[1][1] * y + m[1][2] * z)/n + m[1][3];
+ n = sqrt(m[2][0]*m[2][0] + m[2][1]*m[2][1] + m[2][2]*m[2][2]);
+ float newZ = (m[2][0] * x + m[2][1] * y + m[2][2] * z)/n + m[2][3];
+ z = newZ;
+ y = newY;
+ x = newX;
+ }
+
+ inline void InverseTransformPosition(float &x, float &y) const XBMC_FORCE_INLINE
+ { // used for mouse - no way to find z
+ x -= m[0][3]; y -= m[1][3];
+ float detM = m[0][0]*m[1][1] - m[0][1]*m[1][0];
+ float newX = (m[1][1] * x - m[0][1] * y)/detM;
+ y = (-m[1][0] * x + m[0][0] * y)/detM;
+ x = newX;
+ }
+
+ inline float TransformXCoord(float x, float y, float z) const XBMC_FORCE_INLINE
+ {
+ return m[0][0] * x + m[0][1] * y + m[0][2] * z + m[0][3];
+ }
+
+ inline float TransformYCoord(float x, float y, float z) const XBMC_FORCE_INLINE
+ {
+ return m[1][0] * x + m[1][1] * y + m[1][2] * z + m[1][3];
+ }
+
+ inline float TransformZCoord(float x, float y, float z) const XBMC_FORCE_INLINE
+ {
+ return m[2][0] * x + m[2][1] * y + m[2][2] * z + m[2][3];
+ }
+
+ inline UTILS::COLOR::Color TransformAlpha(UTILS::COLOR::Color color) const XBMC_FORCE_INLINE
+ {
+ return static_cast<UTILS::COLOR::Color>(color * alpha);
+ }
+
+ inline UTILS::COLOR::Color TransformColor(UTILS::COLOR::Color color) const XBMC_FORCE_INLINE
+ {
+ UTILS::COLOR::Color a = static_cast<UTILS::COLOR::Color>(((color >> 24) & 0xff) * alpha);
+ UTILS::COLOR::Color r = static_cast<UTILS::COLOR::Color>(((color >> 16) & 0xff) * red);
+ UTILS::COLOR::Color g = static_cast<UTILS::COLOR::Color>(((color >> 8) & 0xff) * green);
+ UTILS::COLOR::Color b = static_cast<UTILS::COLOR::Color>(((color)&0xff) * blue);
+ if (a > 255)
+ a = 255;
+ if (r > 255)
+ r = 255;
+ if (g > 255)
+ g = 255;
+ if (b > 255)
+ b = 255;
+
+ return ((a << 24) & 0xff000000) | ((r << 16) & 0xff0000) | ((g << 8) & 0xff00) | (b & 0xff);
+ }
+
+ float m[3][4];
+ float alpha;
+ float red;
+ float green;
+ float blue;
+ bool identity;
+};
+
+inline bool operator==(const TransformMatrix &a, const TransformMatrix &b)
+{
+ bool comparison =
+ a.alpha == b.alpha && a.red == b.red && a.green == b.green && a.blue == b.blue &&
+ ((a.identity && b.identity) ||
+ (!a.identity && !b.identity &&
+ std::equal(&a.m[0][0], &a.m[0][0] + sizeof(a.m) / sizeof(a.m[0][0]), &b.m[0][0])));
+ return comparison;
+}
+
+inline bool operator!=(const TransformMatrix &a, const TransformMatrix &b)
+{
+ return !operator==(a, b);
+}
diff --git a/xbmc/utils/UDMABufferObject.cpp b/xbmc/utils/UDMABufferObject.cpp
new file mode 100644
index 0000000..2b8336b
--- /dev/null
+++ b/xbmc/utils/UDMABufferObject.cpp
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2005-2020 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 "UDMABufferObject.h"
+
+#include "utils/BufferObjectFactory.h"
+#include "utils/log.h"
+
+#include <drm_fourcc.h>
+#include <fcntl.h>
+#include <linux/udmabuf.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "PlatformDefs.h"
+
+namespace
+{
+
+const auto PAGESIZE = getpagesize();
+
+int RoundUp(int num, int factor)
+{
+ return num + factor - 1 - (num - 1) % factor;
+}
+
+} // namespace
+
+std::unique_ptr<CBufferObject> CUDMABufferObject::Create()
+{
+ return std::make_unique<CUDMABufferObject>();
+}
+
+void CUDMABufferObject::Register()
+{
+ int fd = open("/dev/udmabuf", O_RDWR);
+ if (fd < 0)
+ {
+ CLog::Log(LOGDEBUG, "CUDMABufferObject::{} - unable to open /dev/udmabuf: {}", __FUNCTION__,
+ strerror(errno));
+ return;
+ }
+
+ close(fd);
+
+ CBufferObjectFactory::RegisterBufferObject(CUDMABufferObject::Create);
+}
+
+CUDMABufferObject::~CUDMABufferObject()
+{
+ ReleaseMemory();
+ DestroyBufferObject();
+
+ int ret = close(m_udmafd);
+ if (ret < 0)
+ CLog::Log(LOGERROR, "CUDMABufferObject::{} - close /dev/udmabuf failed, errno={}", __FUNCTION__,
+ strerror(errno));
+
+ m_udmafd = -1;
+}
+
+bool CUDMABufferObject::CreateBufferObject(uint32_t format, uint32_t width, uint32_t height)
+{
+ if (m_fd >= 0)
+ return true;
+
+ uint32_t bpp{1};
+
+ switch (format)
+ {
+ case DRM_FORMAT_ARGB8888:
+ bpp = 4;
+ break;
+ case DRM_FORMAT_ARGB1555:
+ case DRM_FORMAT_RGB565:
+ bpp = 2;
+ break;
+ default:
+ throw std::runtime_error("CUDMABufferObject: pixel format not implemented");
+ }
+
+ m_stride = width * bpp;
+
+ return CreateBufferObject(width * height * bpp);
+}
+
+bool CUDMABufferObject::CreateBufferObject(uint64_t size)
+{
+ // Must be rounded to the system page size
+ m_size = RoundUp(size, PAGESIZE);
+
+ m_memfd = memfd_create("kodi", MFD_CLOEXEC | MFD_ALLOW_SEALING);
+ if (m_memfd < 0)
+ {
+ CLog::Log(LOGERROR, "CUDMABufferObject::{} - memfd_create failed: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ if (ftruncate(m_memfd, m_size) < 0)
+ {
+ CLog::Log(LOGERROR, "CUDMABufferObject::{} - ftruncate failed: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ if (fcntl(m_memfd, F_ADD_SEALS, F_SEAL_SHRINK) < 0)
+ {
+ CLog::Log(LOGERROR, "CUDMABufferObject::{} - fcntl failed: {}", __FUNCTION__, strerror(errno));
+ close(m_memfd);
+ return false;
+ }
+
+ if (m_udmafd < 0)
+ {
+ m_udmafd = open("/dev/udmabuf", O_RDWR);
+ if (m_udmafd < 0)
+ {
+ CLog::Log(LOGERROR, "CUDMABufferObject::{} - unable to open /dev/udmabuf: {}", __FUNCTION__,
+ strerror(errno));
+ close(m_memfd);
+ return false;
+ }
+ }
+
+ struct udmabuf_create_item create{};
+ create.memfd = static_cast<uint32_t>(m_memfd);
+ create.offset = 0;
+ create.size = m_size;
+
+ m_fd = ioctl(m_udmafd, UDMABUF_CREATE, &create);
+ if (m_fd < 0)
+ {
+ CLog::Log(LOGERROR, "CUDMABufferObject::{} - ioctl UDMABUF_CREATE failed: {}", __FUNCTION__,
+ strerror(errno));
+ close(m_memfd);
+ return false;
+ }
+
+ return true;
+}
+
+void CUDMABufferObject::DestroyBufferObject()
+{
+ if (m_fd < 0)
+ return;
+
+ int ret = close(m_fd);
+ if (ret < 0)
+ CLog::Log(LOGERROR, "CUDMABufferObject::{} - close fd failed, errno={}", __FUNCTION__,
+ strerror(errno));
+
+ ret = close(m_memfd);
+ if (ret < 0)
+ CLog::Log(LOGERROR, "CUDMABufferObject::{} - close memfd failed, errno={}", __FUNCTION__,
+ strerror(errno));
+
+ m_memfd = -1;
+ m_fd = -1;
+ m_stride = 0;
+ m_size = 0;
+}
+
+uint8_t* CUDMABufferObject::GetMemory()
+{
+ if (m_fd < 0)
+ return nullptr;
+
+ if (m_map)
+ {
+ CLog::Log(LOGDEBUG, "CUDMABufferObject::{} - already mapped fd={} map={}", __FUNCTION__, m_fd,
+ fmt::ptr(m_map));
+ return m_map;
+ }
+
+ m_map = static_cast<uint8_t*>(mmap(nullptr, m_size, PROT_WRITE, MAP_SHARED, m_memfd, 0));
+ if (m_map == MAP_FAILED)
+ {
+ CLog::Log(LOGERROR, "CUDMABufferObject::{} - mmap failed, errno={}", __FUNCTION__,
+ strerror(errno));
+ return nullptr;
+ }
+
+ return m_map;
+}
+
+void CUDMABufferObject::ReleaseMemory()
+{
+ if (!m_map)
+ return;
+
+ int ret = munmap(m_map, m_size);
+ if (ret < 0)
+ CLog::Log(LOGERROR, "CUDMABufferObject::{} - munmap failed, errno={}", __FUNCTION__,
+ strerror(errno));
+
+ m_map = nullptr;
+}
diff --git a/xbmc/utils/UDMABufferObject.h b/xbmc/utils/UDMABufferObject.h
new file mode 100644
index 0000000..a842560
--- /dev/null
+++ b/xbmc/utils/UDMABufferObject.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2005-2020 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 "utils/BufferObject.h"
+
+#include <memory>
+#include <stdint.h>
+
+class CUDMABufferObject : public CBufferObject
+{
+public:
+ CUDMABufferObject() = default;
+ virtual ~CUDMABufferObject() override;
+
+ // Registration
+ static std::unique_ptr<CBufferObject> Create();
+ static void Register();
+
+ // IBufferObject overrides via CBufferObject
+ bool CreateBufferObject(uint32_t format, uint32_t width, uint32_t height) override;
+ bool CreateBufferObject(uint64_t size) override;
+ void DestroyBufferObject() override;
+ uint8_t* GetMemory() override;
+ void ReleaseMemory() override;
+ std::string GetName() const override { return "CUDMABufferObject"; }
+
+private:
+ int m_memfd{-1};
+ int m_udmafd{-1};
+ uint64_t m_size{0};
+ uint8_t* m_map{nullptr};
+};
diff --git a/xbmc/utils/URIUtils.cpp b/xbmc/utils/URIUtils.cpp
new file mode 100644
index 0000000..b2b9b23
--- /dev/null
+++ b/xbmc/utils/URIUtils.cpp
@@ -0,0 +1,1493 @@
+/*
+ * 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 "network/Network.h"
+#include "URIUtils.h"
+#include "FileItem.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "filesystem/StackDirectory.h"
+#include "network/DNSNameCache.h"
+#include "pvr/channels/PVRChannelsPath.h"
+#include "settings/AdvancedSettings.h"
+#include "URL.h"
+#include "utils/FileExtensionProvider.h"
+#include "ServiceBroker.h"
+#include "StringUtils.h"
+#include "utils/log.h"
+
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/CharsetConverter.h"
+#endif
+
+#include <algorithm>
+#include <cassert>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+using namespace PVR;
+using namespace XFILE;
+
+const CAdvancedSettings* URIUtils::m_advancedSettings = nullptr;
+
+void URIUtils::RegisterAdvancedSettings(const CAdvancedSettings& advancedSettings)
+{
+ m_advancedSettings = &advancedSettings;
+}
+
+void URIUtils::UnregisterAdvancedSettings()
+{
+ m_advancedSettings = nullptr;
+}
+
+/* returns filename extension including period of filename */
+std::string URIUtils::GetExtension(const CURL& url)
+{
+ return URIUtils::GetExtension(url.GetFileName());
+}
+
+std::string URIUtils::GetExtension(const std::string& strFileName)
+{
+ if (IsURL(strFileName))
+ {
+ CURL url(strFileName);
+ return GetExtension(url.GetFileName());
+ }
+
+ size_t period = strFileName.find_last_of("./\\");
+ if (period == std::string::npos || strFileName[period] != '.')
+ return std::string();
+
+ return strFileName.substr(period);
+}
+
+bool URIUtils::HasPluginPath(const CFileItem& item)
+{
+ return IsPlugin(item.GetPath()) || IsPlugin(item.GetDynPath());
+}
+
+bool URIUtils::HasExtension(const std::string& strFileName)
+{
+ if (IsURL(strFileName))
+ {
+ CURL url(strFileName);
+ return HasExtension(url.GetFileName());
+ }
+
+ size_t iPeriod = strFileName.find_last_of("./\\");
+ return iPeriod != std::string::npos && strFileName[iPeriod] == '.';
+}
+
+bool URIUtils::HasExtension(const CURL& url, const std::string& strExtensions)
+{
+ return HasExtension(url.GetFileName(), strExtensions);
+}
+
+bool URIUtils::HasExtension(const std::string& strFileName, const std::string& strExtensions)
+{
+ if (IsURL(strFileName))
+ {
+ const CURL url(strFileName);
+ return HasExtension(url.GetFileName(), strExtensions);
+ }
+
+ const size_t pos = strFileName.find_last_of("./\\");
+ if (pos == std::string::npos || strFileName[pos] != '.')
+ return false;
+
+ const std::string extensionLower = StringUtils::ToLower(strFileName.substr(pos));
+
+ const std::vector<std::string> extensionsLower =
+ StringUtils::Split(StringUtils::ToLower(strExtensions), '|');
+
+ for (const auto& ext : extensionsLower)
+ {
+ if (StringUtils::EndsWith(ext, extensionLower))
+ return true;
+ }
+
+ return false;
+}
+
+void URIUtils::RemoveExtension(std::string& strFileName)
+{
+ if(IsURL(strFileName))
+ {
+ CURL url(strFileName);
+ strFileName = url.GetFileName();
+ RemoveExtension(strFileName);
+ url.SetFileName(strFileName);
+ strFileName = url.Get();
+ return;
+ }
+
+ size_t period = strFileName.find_last_of("./\\");
+ if (period != std::string::npos && strFileName[period] == '.')
+ {
+ std::string strExtension = strFileName.substr(period);
+ StringUtils::ToLower(strExtension);
+ strExtension += "|";
+
+ std::string strFileMask;
+ strFileMask = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ strFileMask += "|" + CServiceBroker::GetFileExtensionProvider().GetMusicExtensions();
+ strFileMask += "|" + CServiceBroker::GetFileExtensionProvider().GetVideoExtensions();
+ strFileMask += "|" + CServiceBroker::GetFileExtensionProvider().GetSubtitleExtensions();
+#if defined(TARGET_DARWIN)
+ strFileMask += "|.py|.xml|.milk|.xbt|.cdg|.app|.applescript|.workflow";
+#else
+ strFileMask += "|.py|.xml|.milk|.xbt|.cdg";
+#endif
+ strFileMask += "|";
+
+ if (strFileMask.find(strExtension) != std::string::npos)
+ strFileName.erase(period);
+ }
+}
+
+std::string URIUtils::ReplaceExtension(const std::string& strFile,
+ const std::string& strNewExtension)
+{
+ if(IsURL(strFile))
+ {
+ CURL url(strFile);
+ url.SetFileName(ReplaceExtension(url.GetFileName(), strNewExtension));
+ return url.Get();
+ }
+
+ std::string strChangedFile;
+ std::string strExtension = GetExtension(strFile);
+ if ( strExtension.size() )
+ {
+ strChangedFile = strFile.substr(0, strFile.size() - strExtension.size()) ;
+ strChangedFile += strNewExtension;
+ }
+ else
+ {
+ strChangedFile = strFile;
+ strChangedFile += strNewExtension;
+ }
+ return strChangedFile;
+}
+
+std::string URIUtils::GetFileName(const CURL& url)
+{
+ return GetFileName(url.GetFileName());
+}
+
+/* returns a filename given an url */
+/* handles both / and \, and options in urls*/
+std::string URIUtils::GetFileName(const std::string& strFileNameAndPath)
+{
+ if(IsURL(strFileNameAndPath))
+ {
+ CURL url(strFileNameAndPath);
+ return GetFileName(url.GetFileName());
+ }
+
+ /* find the last slash */
+ const size_t slash = strFileNameAndPath.find_last_of("/\\");
+ return strFileNameAndPath.substr(slash+1);
+}
+
+void URIUtils::Split(const std::string& strFileNameAndPath,
+ std::string& strPath, std::string& strFileName)
+{
+ //Splits a full filename in path and file.
+ //ex. smb://computer/share/directory/filename.ext -> strPath:smb://computer/share/directory/ and strFileName:filename.ext
+ //Trailing slash will be preserved
+ strFileName = "";
+ strPath = "";
+ int i = strFileNameAndPath.size() - 1;
+ while (i > 0)
+ {
+ char ch = strFileNameAndPath[i];
+ // Only break on ':' if it's a drive separator for DOS (ie d:foo)
+ if (ch == '/' || ch == '\\' || (ch == ':' && i == 1)) break;
+ else i--;
+ }
+ if (i == 0)
+ i--;
+
+ // take left including the directory separator
+ strPath = strFileNameAndPath.substr(0, i+1);
+ // everything to the right of the directory separator
+ strFileName = strFileNameAndPath.substr(i+1);
+
+ // if actual uri, ignore options
+ if (IsURL(strFileNameAndPath))
+ {
+ i = strFileName.size() - 1;
+ while (i > 0)
+ {
+ char ch = strFileName[i];
+ if (ch == '?' || ch == '|') break;
+ else i--;
+ }
+ if (i > 0)
+ strFileName = strFileName.substr(0, i);
+ }
+}
+
+std::vector<std::string> URIUtils::SplitPath(const std::string& strPath)
+{
+ CURL url(strPath);
+
+ // silly std::string can't take a char in the constructor
+ std::string sep(1, url.GetDirectorySeparator());
+
+ // split the filename portion of the URL up into separate dirs
+ std::vector<std::string> dirs = StringUtils::Split(url.GetFileName(), sep);
+
+ // we start with the root path
+ std::string dir = url.GetWithoutFilename();
+
+ if (!dir.empty())
+ dirs.insert(dirs.begin(), dir);
+
+ // we don't need empty token on the end
+ if (dirs.size() > 1 && dirs.back().empty())
+ dirs.erase(dirs.end() - 1);
+
+ return dirs;
+}
+
+void URIUtils::GetCommonPath(std::string& strParent, const std::string& strPath)
+{
+ // find the common path of parent and path
+ unsigned int j = 1;
+ while (j <= std::min(strParent.size(), strPath.size()) &&
+ StringUtils::CompareNoCase(strParent, strPath, j) == 0)
+ j++;
+ strParent.erase(j - 1);
+ // they should at least share a / at the end, though for things such as path/cd1 and path/cd2 there won't be
+ if (!HasSlashAtEnd(strParent))
+ {
+ strParent = GetDirectory(strParent);
+ AddSlashAtEnd(strParent);
+ }
+}
+
+bool URIUtils::HasParentInHostname(const CURL& url)
+{
+ return url.IsProtocol("zip") || url.IsProtocol("apk") || url.IsProtocol("bluray") ||
+ url.IsProtocol("udf") || url.IsProtocol("iso9660") || url.IsProtocol("xbt") ||
+ (CServiceBroker::IsAddonInterfaceUp() &&
+ CServiceBroker::GetFileExtensionProvider().EncodedHostName(url.GetProtocol()));
+}
+
+bool URIUtils::HasEncodedHostname(const CURL& url)
+{
+ return HasParentInHostname(url)
+ || url.IsProtocol("musicsearch")
+ || url.IsProtocol( "image");
+}
+
+bool URIUtils::HasEncodedFilename(const CURL& url)
+{
+ const std::string prot2 = url.GetTranslatedProtocol();
+
+ // For now assume only (quasi) http internet streams use URL encoding
+ return CURL::IsProtocolEqual(prot2, "http") ||
+ CURL::IsProtocolEqual(prot2, "https");
+}
+
+std::string URIUtils::GetParentPath(const std::string& strPath)
+{
+ std::string strReturn;
+ GetParentPath(strPath, strReturn);
+ return strReturn;
+}
+
+bool URIUtils::GetParentPath(const std::string& strPath, std::string& strParent)
+{
+ strParent.clear();
+
+ CURL url(strPath);
+ std::string strFile = url.GetFileName();
+ if ( URIUtils::HasParentInHostname(url) && strFile.empty())
+ {
+ strFile = url.GetHostName();
+ return GetParentPath(strFile, strParent);
+ }
+ else if (url.IsProtocol("stack"))
+ {
+ CStackDirectory dir;
+ CFileItemList items;
+ if (!dir.GetDirectory(url, items))
+ return false;
+ CURL url2(GetDirectory(items[0]->GetPath()));
+ if (HasParentInHostname(url2))
+ GetParentPath(url2.Get(), strParent);
+ else
+ strParent = url2.Get();
+ for( int i=1;i<items.Size();++i)
+ {
+ items[i]->m_strDVDLabel = GetDirectory(items[i]->GetPath());
+ if (HasParentInHostname(url2))
+ items[i]->SetPath(GetParentPath(items[i]->m_strDVDLabel));
+ else
+ items[i]->SetPath(items[i]->m_strDVDLabel);
+
+ GetCommonPath(strParent,items[i]->GetPath());
+ }
+ return true;
+ }
+ else if (url.IsProtocol("multipath"))
+ {
+ // get the parent path of the first item
+ return GetParentPath(CMultiPathDirectory::GetFirstPath(strPath), strParent);
+ }
+ else if (url.IsProtocol("plugin"))
+ {
+ if (!url.GetOptions().empty())
+ {
+ //! @todo Make a new python call to get the plugin content type and remove this temporary hack
+ // When a plugin provides multiple types, it has "plugin://addon.id/?content_type=xxx" root URL
+ if (url.GetFileName().empty() && url.HasOption("content_type") && url.GetOptions().find('&') == std::string::npos)
+ url.SetHostName("");
+ //
+ url.SetOptions("");
+ strParent = url.Get();
+ return true;
+ }
+ if (!url.GetFileName().empty())
+ {
+ url.SetFileName("");
+ strParent = url.Get();
+ return true;
+ }
+ if (!url.GetHostName().empty())
+ {
+ url.SetHostName("");
+ strParent = url.Get();
+ return true;
+ }
+ return true; // already at root
+ }
+ else if (url.IsProtocol("special"))
+ {
+ if (HasSlashAtEnd(strFile))
+ strFile.erase(strFile.size() - 1);
+ if(strFile.rfind('/') == std::string::npos)
+ return false;
+ }
+ else if (strFile.empty())
+ {
+ if (!url.GetHostName().empty())
+ {
+ // we have an share with only server or workgroup name
+ // set hostname to "" and return true to get back to root
+ url.SetHostName("");
+ strParent = url.Get();
+ return true;
+ }
+ return false;
+ }
+
+ if (HasSlashAtEnd(strFile) )
+ {
+ strFile.erase(strFile.size() - 1);
+ }
+
+ size_t iPos = strFile.rfind('/');
+#ifndef TARGET_POSIX
+ if (iPos == std::string::npos)
+ {
+ iPos = strFile.rfind('\\');
+ }
+#endif
+ if (iPos == std::string::npos)
+ {
+ url.SetFileName("");
+ strParent = url.Get();
+ return true;
+ }
+
+ strFile.erase(iPos);
+
+ AddSlashAtEnd(strFile);
+
+ url.SetFileName(strFile);
+ strParent = url.Get();
+ return true;
+}
+
+std::string URIUtils::GetBasePath(const std::string& strPath)
+{
+ std::string strCheck(strPath);
+ if (IsStack(strPath))
+ strCheck = CStackDirectory::GetFirstStackedFile(strPath);
+
+ std::string strDirectory = GetDirectory(strCheck);
+ if (IsInRAR(strCheck))
+ {
+ std::string strPath=strDirectory;
+ GetParentPath(strPath, strDirectory);
+ }
+ if (IsStack(strPath))
+ {
+ strCheck = strDirectory;
+ RemoveSlashAtEnd(strCheck);
+ if (GetFileName(strCheck).size() == 3 && StringUtils::StartsWithNoCase(GetFileName(strCheck), "cd"))
+ strDirectory = GetDirectory(strCheck);
+ }
+ return strDirectory;
+}
+
+std::string URLEncodePath(const std::string& strPath)
+{
+ std::vector<std::string> segments = StringUtils::Split(strPath, "/");
+ for (std::vector<std::string>::iterator i = segments.begin(); i != segments.end(); ++i)
+ *i = CURL::Encode(*i);
+
+ return StringUtils::Join(segments, "/");
+}
+
+std::string URLDecodePath(const std::string& strPath)
+{
+ std::vector<std::string> segments = StringUtils::Split(strPath, "/");
+ for (std::vector<std::string>::iterator i = segments.begin(); i != segments.end(); ++i)
+ *i = CURL::Decode(*i);
+
+ return StringUtils::Join(segments, "/");
+}
+
+std::string URIUtils::ChangeBasePath(const std::string &fromPath, const std::string &fromFile, const std::string &toPath, const bool &bAddPath /* = true */)
+{
+ std::string toFile = fromFile;
+
+ // Convert back slashes to forward slashes, if required
+ if (IsDOSPath(fromPath) && !IsDOSPath(toPath))
+ StringUtils::Replace(toFile, "\\", "/");
+
+ // Handle difference in URL encoded vs. not encoded
+ if ( HasEncodedFilename(CURL(fromPath))
+ && !HasEncodedFilename(CURL(toPath)) )
+ {
+ toFile = URLDecodePath(toFile); // Decode path
+ }
+ else if (!HasEncodedFilename(CURL(fromPath))
+ && HasEncodedFilename(CURL(toPath)) )
+ {
+ toFile = URLEncodePath(toFile); // Encode path
+ }
+
+ // Convert forward slashes to back slashes, if required
+ if (!IsDOSPath(fromPath) && IsDOSPath(toPath))
+ StringUtils::Replace(toFile, "/", "\\");
+
+ if (bAddPath)
+ return AddFileToFolder(toPath, toFile);
+
+ return toFile;
+}
+
+CURL URIUtils::SubstitutePath(const CURL& url, bool reverse /* = false */)
+{
+ const std::string pathToUrl = url.Get();
+ return CURL(SubstitutePath(pathToUrl, reverse));
+}
+
+std::string URIUtils::SubstitutePath(const std::string& strPath, bool reverse /* = false */)
+{
+ if (!m_advancedSettings)
+ {
+ // path substitution not needed / not working during Kodi bootstrap.
+ return strPath;
+ }
+
+ for (const auto& pathPair : m_advancedSettings->m_pathSubstitutions)
+ {
+ const std::string fromPath = reverse ? pathPair.second : pathPair.first;
+ std::string toPath = reverse ? pathPair.first : pathPair.second;
+
+ if (strncmp(strPath.c_str(), fromPath.c_str(), HasSlashAtEnd(fromPath) ? fromPath.size() - 1 : fromPath.size()) == 0)
+ {
+ if (strPath.size() > fromPath.size())
+ {
+ std::string strSubPathAndFileName = strPath.substr(fromPath.size());
+ return ChangeBasePath(fromPath, strSubPathAndFileName, toPath); // Fix encoding + slash direction
+ }
+ else
+ {
+ return toPath;
+ }
+ }
+ }
+ return strPath;
+}
+
+bool URIUtils::IsProtocol(const std::string& url, const std::string &type)
+{
+ return StringUtils::StartsWithNoCase(url, type + "://");
+}
+
+bool URIUtils::PathHasParent(std::string path, std::string parent, bool translate /* = false */)
+{
+ if (translate)
+ {
+ path = CSpecialProtocol::TranslatePath(path);
+ parent = CSpecialProtocol::TranslatePath(parent);
+ }
+
+ if (parent.empty())
+ return false;
+
+ if (path == parent)
+ return true;
+
+ // Make sure parent has a trailing slash
+ AddSlashAtEnd(parent);
+
+ return StringUtils::StartsWith(path, parent);
+}
+
+bool URIUtils::PathEquals(std::string path1, std::string path2, bool ignoreTrailingSlash /* = false */, bool ignoreURLOptions /* = false */)
+{
+ if (ignoreURLOptions)
+ {
+ path1 = CURL(path1).GetWithoutOptions();
+ path2 = CURL(path2).GetWithoutOptions();
+ }
+
+ if (ignoreTrailingSlash)
+ {
+ RemoveSlashAtEnd(path1);
+ RemoveSlashAtEnd(path2);
+ }
+
+ return (path1 == path2);
+}
+
+bool URIUtils::IsRemote(const std::string& strFile)
+{
+ if (IsCDDA(strFile) || IsISO9660(strFile))
+ return false;
+
+ if (IsStack(strFile))
+ return IsRemote(CStackDirectory::GetFirstStackedFile(strFile));
+
+ if (IsSpecial(strFile))
+ return IsRemote(CSpecialProtocol::TranslatePath(strFile));
+
+ if(IsMultiPath(strFile))
+ { // virtual paths need to be checked separately
+ std::vector<std::string> paths;
+ if (CMultiPathDirectory::GetPaths(strFile, paths))
+ {
+ for (unsigned int i = 0; i < paths.size(); i++)
+ if (IsRemote(paths[i])) return true;
+ }
+ return false;
+ }
+
+ CURL url(strFile);
+ if(HasParentInHostname(url))
+ return IsRemote(url.GetHostName());
+
+ if (IsAddonsPath(strFile))
+ return false;
+
+ if (IsSourcesPath(strFile))
+ return false;
+
+ if (IsVideoDb(strFile) || IsMusicDb(strFile))
+ return false;
+
+ if (IsLibraryFolder(strFile))
+ return false;
+
+ if (IsPlugin(strFile))
+ return false;
+
+ if (IsAndroidApp(strFile))
+ return false;
+
+ if (!url.IsLocal())
+ return true;
+
+ return false;
+}
+
+bool URIUtils::IsOnDVD(const std::string& strFile)
+{
+ if (IsProtocol(strFile, "dvd"))
+ return true;
+
+ if (IsProtocol(strFile, "udf"))
+ return true;
+
+ if (IsProtocol(strFile, "iso9660"))
+ return true;
+
+ if (IsProtocol(strFile, "cdda"))
+ return true;
+
+#if defined(TARGET_WINDOWS_STORE)
+ CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__);
+#elif defined(TARGET_WINDOWS_DESKTOP)
+ using KODI::PLATFORM::WINDOWS::ToW;
+ if (strFile.size() >= 2 && strFile.substr(1, 1) == ":")
+ return (GetDriveType(ToW(strFile.substr(0, 3)).c_str()) == DRIVE_CDROM);
+#endif
+ return false;
+}
+
+bool URIUtils::IsOnLAN(const std::string& strPath)
+{
+ if(IsMultiPath(strPath))
+ return IsOnLAN(CMultiPathDirectory::GetFirstPath(strPath));
+
+ if(IsStack(strPath))
+ return IsOnLAN(CStackDirectory::GetFirstStackedFile(strPath));
+
+ if(IsSpecial(strPath))
+ return IsOnLAN(CSpecialProtocol::TranslatePath(strPath));
+
+ if(IsPlugin(strPath))
+ return false;
+
+ if(IsUPnP(strPath))
+ return true;
+
+ CURL url(strPath);
+ if (HasParentInHostname(url))
+ return IsOnLAN(url.GetHostName());
+
+ if(!IsRemote(strPath))
+ return false;
+
+ const std::string& host = url.GetHostName();
+
+ return IsHostOnLAN(host);
+}
+
+static bool addr_match(uint32_t addr, const char* target, const char* submask)
+{
+ uint32_t addr2 = ntohl(inet_addr(target));
+ uint32_t mask = ntohl(inet_addr(submask));
+ return (addr & mask) == (addr2 & mask);
+}
+
+bool URIUtils::IsHostOnLAN(const std::string& host, bool offLineCheck)
+{
+ if(host.length() == 0)
+ return false;
+
+ // assume a hostname without dot's
+ // is local (smb netbios hostnames)
+ if(host.find('.') == std::string::npos)
+ return true;
+
+ uint32_t address = ntohl(inet_addr(host.c_str()));
+ if(address == INADDR_NONE)
+ {
+ std::string ip;
+ if(CDNSNameCache::Lookup(host, ip))
+ address = ntohl(inet_addr(ip.c_str()));
+ }
+
+ if(address != INADDR_NONE)
+ {
+ if (offLineCheck) // check if in private range, ref https://en.wikipedia.org/wiki/Private_network
+ {
+ if (
+ addr_match(address, "192.168.0.0", "255.255.0.0") ||
+ addr_match(address, "10.0.0.0", "255.0.0.0") ||
+ addr_match(address, "172.16.0.0", "255.240.0.0")
+ )
+ return true;
+ }
+ // check if we are on the local subnet
+ if (!CServiceBroker::GetNetwork().GetFirstConnectedInterface())
+ return false;
+
+ if (CServiceBroker::GetNetwork().HasInterfaceForIP(address))
+ return true;
+ }
+
+ return false;
+}
+
+bool URIUtils::IsMultiPath(const std::string& strPath)
+{
+ return IsProtocol(strPath, "multipath");
+}
+
+bool URIUtils::IsHD(const std::string& strFileName)
+{
+ CURL url(strFileName);
+
+ if (IsStack(strFileName))
+ return IsHD(CStackDirectory::GetFirstStackedFile(strFileName));
+
+ if (IsSpecial(strFileName))
+ return IsHD(CSpecialProtocol::TranslatePath(strFileName));
+
+ if (HasParentInHostname(url))
+ return IsHD(url.GetHostName());
+
+ return url.GetProtocol().empty() || url.IsProtocol("file") || url.IsProtocol("win-lib");
+}
+
+bool URIUtils::IsDVD(const std::string& strFile)
+{
+ std::string strFileLow = strFile;
+ StringUtils::ToLower(strFileLow);
+ if (strFileLow.find("video_ts.ifo") != std::string::npos && IsOnDVD(strFile))
+ return true;
+
+#if defined(TARGET_WINDOWS)
+ if (IsProtocol(strFile, "dvd"))
+ return true;
+
+ if(strFile.size() < 2 || (strFile.substr(1) != ":\\" && strFile.substr(1) != ":"))
+ return false;
+
+#ifndef TARGET_WINDOWS_STORE
+ if(GetDriveType(KODI::PLATFORM::WINDOWS::ToW(strFile).c_str()) == DRIVE_CDROM)
+ return true;
+#endif
+#else
+ if (strFileLow == "iso9660://" || strFileLow == "udf://" || strFileLow == "dvd://1" )
+ return true;
+#endif
+
+ return false;
+}
+
+bool URIUtils::IsStack(const std::string& strFile)
+{
+ return IsProtocol(strFile, "stack");
+}
+
+bool URIUtils::IsFavourite(const std::string& strFile)
+{
+ return IsProtocol(strFile, "favourites");
+}
+
+bool URIUtils::IsRAR(const std::string& strFile)
+{
+ std::string strExtension = GetExtension(strFile);
+
+ if (strExtension == ".001" && !StringUtils::EndsWithNoCase(strFile, ".ts.001"))
+ return true;
+
+ if (StringUtils::EqualsNoCase(strExtension, ".cbr"))
+ return true;
+
+ if (StringUtils::EqualsNoCase(strExtension, ".rar"))
+ return true;
+
+ return false;
+}
+
+bool URIUtils::IsInArchive(const std::string &strFile)
+{
+ CURL url(strFile);
+
+ bool archiveProto = url.IsProtocol("archive") && !url.GetFileName().empty();
+ return archiveProto || IsInZIP(strFile) || IsInRAR(strFile) || IsInAPK(strFile);
+}
+
+bool URIUtils::IsInAPK(const std::string& strFile)
+{
+ CURL url(strFile);
+
+ return url.IsProtocol("apk") && !url.GetFileName().empty();
+}
+
+bool URIUtils::IsInZIP(const std::string& strFile)
+{
+ CURL url(strFile);
+
+ if (url.GetFileName().empty())
+ return false;
+
+ if (url.IsProtocol("archive"))
+ return IsZIP(url.GetHostName());
+
+ return url.IsProtocol("zip");
+}
+
+bool URIUtils::IsInRAR(const std::string& strFile)
+{
+ CURL url(strFile);
+
+ if (url.GetFileName().empty())
+ return false;
+
+ if (url.IsProtocol("archive"))
+ return IsRAR(url.GetHostName());
+
+ return url.IsProtocol("rar");
+}
+
+bool URIUtils::IsAPK(const std::string& strFile)
+{
+ return HasExtension(strFile, ".apk");
+}
+
+bool URIUtils::IsZIP(const std::string& strFile) // also checks for comic books!
+{
+ return HasExtension(strFile, ".zip|.cbz");
+}
+
+bool URIUtils::IsArchive(const std::string& strFile)
+{
+ return HasExtension(strFile, ".zip|.rar|.apk|.cbz|.cbr");
+}
+
+bool URIUtils::IsSpecial(const std::string& strFile)
+{
+ if (IsStack(strFile))
+ return IsSpecial(CStackDirectory::GetFirstStackedFile(strFile));
+
+ return IsProtocol(strFile, "special");
+}
+
+bool URIUtils::IsPlugin(const std::string& strFile)
+{
+ CURL url(strFile);
+ return url.IsProtocol("plugin");
+}
+
+bool URIUtils::IsScript(const std::string& strFile)
+{
+ CURL url(strFile);
+ return url.IsProtocol("script");
+}
+
+bool URIUtils::IsAddonsPath(const std::string& strFile)
+{
+ CURL url(strFile);
+ return url.IsProtocol("addons");
+}
+
+bool URIUtils::IsSourcesPath(const std::string& strPath)
+{
+ CURL url(strPath);
+ return url.IsProtocol("sources");
+}
+
+bool URIUtils::IsCDDA(const std::string& strFile)
+{
+ return IsProtocol(strFile, "cdda");
+}
+
+bool URIUtils::IsISO9660(const std::string& strFile)
+{
+ return IsProtocol(strFile, "iso9660");
+}
+
+bool URIUtils::IsSmb(const std::string& strFile)
+{
+ if (IsStack(strFile))
+ return IsSmb(CStackDirectory::GetFirstStackedFile(strFile));
+
+ if (IsSpecial(strFile))
+ return IsSmb(CSpecialProtocol::TranslatePath(strFile));
+
+ CURL url(strFile);
+ if (HasParentInHostname(url))
+ return IsSmb(url.GetHostName());
+
+ return IsProtocol(strFile, "smb");
+}
+
+bool URIUtils::IsURL(const std::string& strFile)
+{
+ return strFile.find("://") != std::string::npos;
+}
+
+bool URIUtils::IsFTP(const std::string& strFile)
+{
+ if (IsStack(strFile))
+ return IsFTP(CStackDirectory::GetFirstStackedFile(strFile));
+
+ if (IsSpecial(strFile))
+ return IsFTP(CSpecialProtocol::TranslatePath(strFile));
+
+ CURL url(strFile);
+ if (HasParentInHostname(url))
+ return IsFTP(url.GetHostName());
+
+ return IsProtocol(strFile, "ftp") ||
+ IsProtocol(strFile, "ftps");
+}
+
+bool URIUtils::IsHTTP(const std::string& strFile, bool bTranslate /* = false */)
+{
+ if (IsStack(strFile))
+ return IsHTTP(CStackDirectory::GetFirstStackedFile(strFile));
+
+ if (IsSpecial(strFile))
+ return IsHTTP(CSpecialProtocol::TranslatePath(strFile));
+
+ CURL url(strFile);
+ if (HasParentInHostname(url))
+ return IsHTTP(url.GetHostName());
+
+ const std::string strProtocol = (bTranslate ? url.GetTranslatedProtocol() : url.GetProtocol());
+
+ return (strProtocol == "http" || strProtocol == "https");
+}
+
+bool URIUtils::IsUDP(const std::string& strFile)
+{
+ if (IsStack(strFile))
+ return IsUDP(CStackDirectory::GetFirstStackedFile(strFile));
+
+ return IsProtocol(strFile, "udp");
+}
+
+bool URIUtils::IsTCP(const std::string& strFile)
+{
+ if (IsStack(strFile))
+ return IsTCP(CStackDirectory::GetFirstStackedFile(strFile));
+
+ return IsProtocol(strFile, "tcp");
+}
+
+bool URIUtils::IsPVR(const std::string& strFile)
+{
+ if (IsStack(strFile))
+ return IsPVR(CStackDirectory::GetFirstStackedFile(strFile));
+
+ return IsProtocol(strFile, "pvr");
+}
+
+bool URIUtils::IsPVRChannel(const std::string& strFile)
+{
+ if (IsStack(strFile))
+ return IsPVRChannel(CStackDirectory::GetFirstStackedFile(strFile));
+
+ return IsProtocol(strFile, "pvr") && CPVRChannelsPath(strFile).IsChannel();
+}
+
+bool URIUtils::IsPVRChannelGroup(const std::string& strFile)
+{
+ if (IsStack(strFile))
+ return IsPVRChannelGroup(CStackDirectory::GetFirstStackedFile(strFile));
+
+ return IsProtocol(strFile, "pvr") && CPVRChannelsPath(strFile).IsChannelGroup();
+}
+
+bool URIUtils::IsPVRGuideItem(const std::string& strFile)
+{
+ if (IsStack(strFile))
+ return IsPVRGuideItem(CStackDirectory::GetFirstStackedFile(strFile));
+
+ return StringUtils::StartsWithNoCase(strFile, "pvr://guide");
+}
+
+bool URIUtils::IsDAV(const std::string& strFile)
+{
+ if (IsStack(strFile))
+ return IsDAV(CStackDirectory::GetFirstStackedFile(strFile));
+
+ if (IsSpecial(strFile))
+ return IsDAV(CSpecialProtocol::TranslatePath(strFile));
+
+ CURL url(strFile);
+ if (HasParentInHostname(url))
+ return IsDAV(url.GetHostName());
+
+ return IsProtocol(strFile, "dav") ||
+ IsProtocol(strFile, "davs");
+}
+
+bool URIUtils::IsInternetStream(const std::string &path, bool bStrictCheck /* = false */)
+{
+ const CURL pathToUrl(path);
+ return IsInternetStream(pathToUrl, bStrictCheck);
+}
+
+bool URIUtils::IsInternetStream(const CURL& url, bool bStrictCheck /* = false */)
+{
+ if (url.GetProtocol().empty())
+ return false;
+
+ // there's nothing to stop internet streams from being stacked
+ if (url.IsProtocol("stack"))
+ return IsInternetStream(CStackDirectory::GetFirstStackedFile(url.Get()), bStrictCheck);
+
+ // Only consider "streamed" filesystems internet streams when being strict
+ if (bStrictCheck && IsStreamedFilesystem(url.Get()))
+ return true;
+
+ // Check for true internetstreams
+ const std::string& protocol = url.GetProtocol();
+ if (CURL::IsProtocolEqual(protocol, "http") || CURL::IsProtocolEqual(protocol, "https") ||
+ CURL::IsProtocolEqual(protocol, "tcp") || CURL::IsProtocolEqual(protocol, "udp") ||
+ CURL::IsProtocolEqual(protocol, "rtp") || CURL::IsProtocolEqual(protocol, "sdp") ||
+ CURL::IsProtocolEqual(protocol, "mms") || CURL::IsProtocolEqual(protocol, "mmst") ||
+ CURL::IsProtocolEqual(protocol, "mmsh") || CURL::IsProtocolEqual(protocol, "rtsp") ||
+ CURL::IsProtocolEqual(protocol, "rtmp") || CURL::IsProtocolEqual(protocol, "rtmpt") ||
+ CURL::IsProtocolEqual(protocol, "rtmpe") || CURL::IsProtocolEqual(protocol, "rtmpte") ||
+ CURL::IsProtocolEqual(protocol, "rtmps") || CURL::IsProtocolEqual(protocol, "shout") ||
+ CURL::IsProtocolEqual(protocol, "rss") || CURL::IsProtocolEqual(protocol, "rsss"))
+ return true;
+
+ return false;
+}
+
+bool URIUtils::IsStreamedFilesystem(const std::string& strPath)
+{
+ CURL url(strPath);
+
+ if (url.GetProtocol().empty())
+ return false;
+
+ if (url.IsProtocol("stack"))
+ return IsStreamedFilesystem(CStackDirectory::GetFirstStackedFile(strPath));
+
+ if (IsUPnP(strPath) || IsFTP(strPath) || IsHTTP(strPath, true))
+ return true;
+
+ //! @todo sftp/ssh special case has to be handled by vfs addon
+ if (url.IsProtocol("sftp") || url.IsProtocol("ssh"))
+ return true;
+
+ return false;
+}
+
+bool URIUtils::IsNetworkFilesystem(const std::string& strPath)
+{
+ CURL url(strPath);
+
+ if (url.GetProtocol().empty())
+ return false;
+
+ if (url.IsProtocol("stack"))
+ return IsNetworkFilesystem(CStackDirectory::GetFirstStackedFile(strPath));
+
+ if (IsStreamedFilesystem(strPath))
+ return true;
+
+ if (IsSmb(strPath) || IsNfs(strPath))
+ return true;
+
+ return false;
+}
+
+bool URIUtils::IsUPnP(const std::string& strFile)
+{
+ return IsProtocol(strFile, "upnp");
+}
+
+bool URIUtils::IsLiveTV(const std::string& strFile)
+{
+ std::string strFileWithoutSlash(strFile);
+ RemoveSlashAtEnd(strFileWithoutSlash);
+
+ if (StringUtils::EndsWithNoCase(strFileWithoutSlash, ".pvr") &&
+ !StringUtils::StartsWith(strFileWithoutSlash, "pvr://recordings"))
+ return true;
+
+ return false;
+}
+
+bool URIUtils::IsPVRRecording(const std::string& strFile)
+{
+ std::string strFileWithoutSlash(strFile);
+ RemoveSlashAtEnd(strFileWithoutSlash);
+
+ return StringUtils::EndsWithNoCase(strFileWithoutSlash, ".pvr") &&
+ StringUtils::StartsWith(strFile, "pvr://recordings");
+}
+
+bool URIUtils::IsPVRRecordingFileOrFolder(const std::string& strFile)
+{
+ return StringUtils::StartsWith(strFile, "pvr://recordings");
+}
+
+bool URIUtils::IsPVRTVRecordingFileOrFolder(const std::string& strFile)
+{
+ return StringUtils::StartsWith(strFile, "pvr://recordings/tv");
+}
+
+bool URIUtils::IsPVRRadioRecordingFileOrFolder(const std::string& strFile)
+{
+ return StringUtils::StartsWith(strFile, "pvr://recordings/radio");
+}
+
+bool URIUtils::IsMusicDb(const std::string& strFile)
+{
+ return IsProtocol(strFile, "musicdb");
+}
+
+bool URIUtils::IsNfs(const std::string& strFile)
+{
+ if (IsStack(strFile))
+ return IsNfs(CStackDirectory::GetFirstStackedFile(strFile));
+
+ if (IsSpecial(strFile))
+ return IsNfs(CSpecialProtocol::TranslatePath(strFile));
+
+ CURL url(strFile);
+ if (HasParentInHostname(url))
+ return IsNfs(url.GetHostName());
+
+ return IsProtocol(strFile, "nfs");
+}
+
+bool URIUtils::IsVideoDb(const std::string& strFile)
+{
+ return IsProtocol(strFile, "videodb");
+}
+
+bool URIUtils::IsBluray(const std::string& strFile)
+{
+ return IsProtocol(strFile, "bluray");
+}
+
+bool URIUtils::IsAndroidApp(const std::string &path)
+{
+ return IsProtocol(path, "androidapp");
+}
+
+bool URIUtils::IsLibraryFolder(const std::string& strFile)
+{
+ CURL url(strFile);
+ return url.IsProtocol("library");
+}
+
+bool URIUtils::IsLibraryContent(const std::string &strFile)
+{
+ return (IsProtocol(strFile, "library") ||
+ IsProtocol(strFile, "videodb") ||
+ IsProtocol(strFile, "musicdb") ||
+ StringUtils::EndsWith(strFile, ".xsp"));
+}
+
+bool URIUtils::IsDOSPath(const std::string &path)
+{
+ if (path.size() > 1 && path[1] == ':' && isalpha(path[0]))
+ return true;
+
+ // windows network drives
+ if (path.size() > 1 && path[0] == '\\' && path[1] == '\\')
+ return true;
+
+ return false;
+}
+
+std::string URIUtils::AppendSlash(std::string strFolder)
+{
+ AddSlashAtEnd(strFolder);
+ return strFolder;
+}
+
+void URIUtils::AddSlashAtEnd(std::string& strFolder)
+{
+ if (IsURL(strFolder))
+ {
+ CURL url(strFolder);
+ std::string file = url.GetFileName();
+ if(!file.empty() && file != strFolder)
+ {
+ AddSlashAtEnd(file);
+ url.SetFileName(file);
+ strFolder = url.Get();
+ }
+ return;
+ }
+
+ if (!HasSlashAtEnd(strFolder))
+ {
+ if (IsDOSPath(strFolder))
+ strFolder += '\\';
+ else
+ strFolder += '/';
+ }
+}
+
+bool URIUtils::HasSlashAtEnd(const std::string& strFile, bool checkURL /* = false */)
+{
+ if (strFile.empty()) return false;
+ if (checkURL && IsURL(strFile))
+ {
+ CURL url(strFile);
+ const std::string& file = url.GetFileName();
+ return file.empty() || HasSlashAtEnd(file, false);
+ }
+ char kar = strFile.c_str()[strFile.size() - 1];
+
+ if (kar == '/' || kar == '\\')
+ return true;
+
+ return false;
+}
+
+void URIUtils::RemoveSlashAtEnd(std::string& strFolder)
+{
+ // performance optimization. pvr guide items are mass objects, uri never has a slash at end, and this method is quite expensive...
+ if (IsPVRGuideItem(strFolder))
+ return;
+
+ if (IsURL(strFolder))
+ {
+ CURL url(strFolder);
+ std::string file = url.GetFileName();
+ if (!file.empty() && file != strFolder)
+ {
+ RemoveSlashAtEnd(file);
+ url.SetFileName(file);
+ strFolder = url.Get();
+ return;
+ }
+ if(url.GetHostName().empty())
+ return;
+ }
+
+ while (HasSlashAtEnd(strFolder))
+ strFolder.erase(strFolder.size()-1, 1);
+}
+
+bool URIUtils::CompareWithoutSlashAtEnd(const std::string& strPath1, const std::string& strPath2)
+{
+ std::string strc1 = strPath1, strc2 = strPath2;
+ RemoveSlashAtEnd(strc1);
+ RemoveSlashAtEnd(strc2);
+ return StringUtils::EqualsNoCase(strc1, strc2);
+}
+
+
+std::string URIUtils::FixSlashesAndDups(const std::string& path, const char slashCharacter /* = '/' */, const size_t startFrom /*= 0*/)
+{
+ const size_t len = path.length();
+ if (startFrom >= len)
+ return path;
+
+ std::string result(path, 0, startFrom);
+ result.reserve(len);
+
+ const char* const str = path.c_str();
+ size_t pos = startFrom;
+ do
+ {
+ if (str[pos] == '\\' || str[pos] == '/')
+ {
+ result.push_back(slashCharacter); // append one slash
+ pos++;
+ // skip any following slashes
+ while (str[pos] == '\\' || str[pos] == '/') // str is null-terminated, no need to check for buffer overrun
+ pos++;
+ }
+ else
+ result.push_back(str[pos++]); // append current char and advance pos to next char
+
+ } while (pos < len);
+
+ return result;
+}
+
+
+std::string URIUtils::CanonicalizePath(const std::string& path, const char slashCharacter /*= '\\'*/)
+{
+ assert(slashCharacter == '\\' || slashCharacter == '/');
+
+ if (path.empty())
+ return path;
+
+ const std::string slashStr(1, slashCharacter);
+ std::vector<std::string> pathVec, resultVec;
+ StringUtils::Tokenize(path, pathVec, slashStr);
+
+ for (std::vector<std::string>::const_iterator it = pathVec.begin(); it != pathVec.end(); ++it)
+ {
+ if (*it == ".")
+ { /* skip - do nothing */ }
+ else if (*it == ".." && !resultVec.empty() && resultVec.back() != "..")
+ resultVec.pop_back();
+ else
+ resultVec.push_back(*it);
+ }
+
+ std::string result;
+ if (path[0] == slashCharacter)
+ result.push_back(slashCharacter); // add slash at the begin
+
+ result += StringUtils::Join(resultVec, slashStr);
+
+ if (path[path.length() - 1] == slashCharacter && !result.empty() && result[result.length() - 1] != slashCharacter)
+ result.push_back(slashCharacter); // add slash at the end if result isn't empty and result isn't "/"
+
+ return result;
+}
+
+std::string URIUtils::AddFileToFolder(const std::string& strFolder,
+ const std::string& strFile)
+{
+ if (IsURL(strFolder))
+ {
+ CURL url(strFolder);
+ if (url.GetFileName() != strFolder)
+ {
+ url.SetFileName(AddFileToFolder(url.GetFileName(), strFile));
+ return url.Get();
+ }
+ }
+
+ std::string strResult = strFolder;
+ if (!strResult.empty())
+ AddSlashAtEnd(strResult);
+
+ // Remove any slash at the start of the file
+ if (strFile.size() && (strFile[0] == '/' || strFile[0] == '\\'))
+ strResult += strFile.substr(1);
+ else
+ strResult += strFile;
+
+ // correct any slash directions
+ if (!IsDOSPath(strFolder))
+ StringUtils::Replace(strResult, '\\', '/');
+ else
+ StringUtils::Replace(strResult, '/', '\\');
+
+ return strResult;
+}
+
+std::string URIUtils::GetDirectory(const std::string &strFilePath)
+{
+ // Will from a full filename return the directory the file resides in.
+ // Keeps the final slash at end and possible |option=foo options.
+
+ size_t iPosSlash = strFilePath.find_last_of("/\\");
+ if (iPosSlash == std::string::npos)
+ return ""; // No slash, so no path (ignore any options)
+
+ size_t iPosBar = strFilePath.rfind('|');
+ if (iPosBar == std::string::npos)
+ return strFilePath.substr(0, iPosSlash + 1); // Only path
+
+ return strFilePath.substr(0, iPosSlash + 1) + strFilePath.substr(iPosBar); // Path + options
+}
+
+CURL URIUtils::CreateArchivePath(const std::string& type,
+ const CURL& archiveUrl,
+ const std::string& pathInArchive,
+ const std::string& password)
+{
+ CURL url;
+ url.SetProtocol(type);
+ if (!password.empty())
+ url.SetUserName(password);
+ url.SetHostName(archiveUrl.Get());
+
+ /* NOTE: on posix systems, the replacement of \ with / is incorrect.
+ Ideally this would not be done. We need to check that the ZipManager
+ code (and elsewhere) doesn't pass in non-posix paths.
+ */
+ std::string strBuffer(pathInArchive);
+ StringUtils::Replace(strBuffer, '\\', '/');
+ StringUtils::TrimLeft(strBuffer, "/");
+ url.SetFileName(strBuffer);
+
+ return url;
+}
+
+std::string URIUtils::GetRealPath(const std::string &path)
+{
+ if (path.empty())
+ return path;
+
+ CURL url(path);
+ url.SetHostName(GetRealPath(url.GetHostName()));
+ url.SetFileName(resolvePath(url.GetFileName()));
+
+ return url.Get();
+}
+
+std::string URIUtils::resolvePath(const std::string &path)
+{
+ if (path.empty())
+ return path;
+
+ size_t posSlash = path.find('/');
+ size_t posBackslash = path.find('\\');
+ std::string delim = posSlash < posBackslash ? "/" : "\\";
+ std::vector<std::string> parts = StringUtils::Split(path, delim);
+ std::vector<std::string> realParts;
+
+ for (std::vector<std::string>::const_iterator part = parts.begin(); part != parts.end(); ++part)
+ {
+ if (part->empty() || part->compare(".") == 0)
+ continue;
+
+ // go one level back up
+ if (part->compare("..") == 0)
+ {
+ if (!realParts.empty())
+ realParts.pop_back();
+ continue;
+ }
+
+ realParts.push_back(*part);
+ }
+
+ std::string realPath;
+ // re-add any / or \ at the beginning
+ for (std::string::const_iterator itPath = path.begin(); itPath != path.end(); ++itPath)
+ {
+ if (*itPath != delim.at(0))
+ break;
+
+ realPath += delim;
+ }
+ // put together the path
+ realPath += StringUtils::Join(realParts, delim);
+ // re-add any / or \ at the end
+ if (path.at(path.size() - 1) == delim.at(0) &&
+ realPath.size() > 0 && realPath.at(realPath.size() - 1) != delim.at(0))
+ realPath += delim;
+
+ return realPath;
+}
+
+bool URIUtils::UpdateUrlEncoding(std::string &strFilename)
+{
+ if (strFilename.empty())
+ return false;
+
+ CURL url(strFilename);
+ // if this is a stack:// URL we need to work with its filename
+ if (URIUtils::IsStack(strFilename))
+ {
+ std::vector<std::string> files;
+ if (!CStackDirectory::GetPaths(strFilename, files))
+ return false;
+
+ for (std::vector<std::string>::iterator file = files.begin(); file != files.end(); ++file)
+ UpdateUrlEncoding(*file);
+
+ std::string stackPath;
+ if (!CStackDirectory::ConstructStackPath(files, stackPath))
+ return false;
+
+ url.Parse(stackPath);
+ }
+ // if the protocol has an encoded hostname we need to work with its hostname
+ else if (URIUtils::HasEncodedHostname(url))
+ {
+ std::string hostname = url.GetHostName();
+ UpdateUrlEncoding(hostname);
+ url.SetHostName(hostname);
+ }
+ else
+ return false;
+
+ std::string newFilename = url.Get();
+ if (newFilename == strFilename)
+ return false;
+
+ strFilename = newFilename;
+ return true;
+}
diff --git a/xbmc/utils/URIUtils.h b/xbmc/utils/URIUtils.h
new file mode 100644
index 0000000..85ba81b
--- /dev/null
+++ b/xbmc/utils/URIUtils.h
@@ -0,0 +1,241 @@
+/*
+ * 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 <string>
+#include <vector>
+
+class CURL;
+class CAdvancedSettings;
+class CFileItem;
+
+class URIUtils
+{
+public:
+ static void RegisterAdvancedSettings(const CAdvancedSettings& advancedSettings);
+ static void UnregisterAdvancedSettings();
+
+ static std::string GetDirectory(const std::string &strFilePath);
+
+ static std::string GetFileName(const CURL& url);
+ static std::string GetFileName(const std::string& strFileNameAndPath);
+
+ static std::string GetExtension(const CURL& url);
+ static std::string GetExtension(const std::string& strFileName);
+
+
+ /*! \brief Check if the CFileItem has a plugin path.
+ \param item The CFileItem.
+ \return true if there is a plugin path, false otherwise.
+ */
+ static bool HasPluginPath(const CFileItem& item);
+
+ /*!
+ \brief Check if there is a file extension
+ \param strFileName Path or URL to check
+ \return \e true if strFileName have an extension.
+ \note Returns false when strFileName is empty.
+ \sa GetExtension
+ */
+ static bool HasExtension(const std::string& strFileName);
+
+ /*!
+ \brief Check if filename have any of the listed extensions
+ \param strFileName Path or URL to check
+ \param strExtensions List of '.' prefixed lowercase extensions separated with '|'
+ \return \e true if strFileName have any one of the extensions.
+ \note The check is case insensitive for strFileName, but requires
+ strExtensions to be lowercase. Returns false when strFileName or
+ strExtensions is empty.
+ \sa GetExtension
+ */
+ static bool HasExtension(const std::string& strFileName, const std::string& strExtensions);
+ static bool HasExtension(const CURL& url, const std::string& strExtensions);
+
+ static void RemoveExtension(std::string& strFileName);
+ static std::string ReplaceExtension(const std::string& strFile,
+ const std::string& strNewExtension);
+ static void Split(const std::string& strFileNameAndPath,
+ std::string& strPath, std::string& strFileName);
+ static std::vector<std::string> SplitPath(const std::string& strPath);
+
+ static void GetCommonPath(std::string& strParent, const std::string& strPath);
+ static std::string GetParentPath(const std::string& strPath);
+ static bool GetParentPath(const std::string& strPath, std::string& strParent);
+
+ /*! \brief Retrieve the base path, accounting for stacks and files in rars.
+ \param strPath path.
+ \return the folder that contains the item.
+ */
+ static std::string GetBasePath(const std::string& strPath);
+
+ /* \brief Change the base path of a URL: fromPath/fromFile -> toPath/toFile
+ Handles changes in path separator and filename URL encoding if necessary to derive toFile.
+ \param fromPath the base path of the original URL
+ \param fromFile the filename portion of the original URL
+ \param toPath the base path of the resulting URL
+ \return the full path.
+ */
+ static std::string ChangeBasePath(const std::string &fromPath, const std::string &fromFile, const std::string &toPath, const bool &bAddPath = true);
+
+ static CURL SubstitutePath(const CURL& url, bool reverse = false);
+ static std::string SubstitutePath(const std::string& strPath, bool reverse = false);
+
+ /*! \brief Check whether a URL is a given URL scheme.
+ Comparison is case-insensitive as per RFC1738
+ \param url a std::string path.
+ \param type a lower-case scheme name, e.g. "smb".
+ \return true if the url is of the given scheme, false otherwise.
+ \sa PathHasParent, PathEquals
+ */
+ static bool IsProtocol(const std::string& url, const std::string& type);
+
+ /*! \brief Check whether a path has a given parent.
+ Comparison is case-sensitive.
+ Use IsProtocol() to compare the protocol portion only.
+ \param path a std::string path.
+ \param parent the string the parent of the path should be compared against.
+ \param translate whether to translate any special paths into real paths
+ \return true if the path has the given parent string, false otherwise.
+ \sa IsProtocol, PathEquals
+ */
+ static bool PathHasParent(std::string path, std::string parent, bool translate = false);
+
+ /*! \brief Check whether a path equals another path.
+ Comparison is case-sensitive.
+ \param path1 a std::string path.
+ \param path2 the second path the path should be compared against.
+ \param ignoreTrailingSlash ignore any trailing slashes in both paths
+ \return true if the paths are equal, false otherwise.
+ \sa IsProtocol, PathHasParent
+ */
+ static bool PathEquals(std::string path1, std::string path2, bool ignoreTrailingSlash = false, bool ignoreURLOptions = false);
+
+ static bool IsAddonsPath(const std::string& strFile);
+ static bool IsSourcesPath(const std::string& strFile);
+ static bool IsCDDA(const std::string& strFile);
+ static bool IsDAV(const std::string& strFile);
+ static bool IsDOSPath(const std::string &path);
+ static bool IsDVD(const std::string& strFile);
+ static bool IsFTP(const std::string& strFile);
+ static bool IsHTTP(const std::string& strFile, bool bTranslate = false);
+ static bool IsUDP(const std::string& strFile);
+ static bool IsTCP(const std::string& strFile);
+ static bool IsHD(const std::string& strFileName);
+ static bool IsInArchive(const std::string& strFile);
+ static bool IsInRAR(const std::string& strFile);
+ static bool IsInternetStream(const std::string& path, bool bStrictCheck = false);
+ static bool IsInternetStream(const CURL& url, bool bStrictCheck = false);
+ static bool IsStreamedFilesystem(const std::string& strPath);
+ static bool IsNetworkFilesystem(const std::string& strPath);
+ static bool IsInAPK(const std::string& strFile);
+ static bool IsInZIP(const std::string& strFile);
+ static bool IsISO9660(const std::string& strFile);
+ static bool IsLiveTV(const std::string& strFile);
+ static bool IsPVRRecording(const std::string& strFile);
+ static bool IsPVRRecordingFileOrFolder(const std::string& strFile);
+ static bool IsPVRTVRecordingFileOrFolder(const std::string& strFile);
+ static bool IsPVRRadioRecordingFileOrFolder(const std::string& strFile);
+ static bool IsMultiPath(const std::string& strPath);
+ static bool IsMusicDb(const std::string& strFile);
+ static bool IsNfs(const std::string& strFile);
+ static bool IsOnDVD(const std::string& strFile);
+ static bool IsOnLAN(const std::string& strFile);
+ static bool IsHostOnLAN(const std::string& hostName, bool offLineCheck = false);
+ static bool IsPlugin(const std::string& strFile);
+ static bool IsScript(const std::string& strFile);
+ static bool IsRAR(const std::string& strFile);
+ static bool IsRemote(const std::string& strFile);
+ static bool IsSmb(const std::string& strFile);
+ static bool IsSpecial(const std::string& strFile);
+ static bool IsStack(const std::string& strFile);
+ static bool IsFavourite(const std::string& strFile);
+ static bool IsUPnP(const std::string& strFile);
+ static bool IsURL(const std::string& strFile);
+ static bool IsVideoDb(const std::string& strFile);
+ static bool IsAPK(const std::string& strFile);
+ static bool IsZIP(const std::string& strFile);
+ static bool IsArchive(const std::string& strFile);
+ static bool IsBluray(const std::string& strFile);
+ static bool IsAndroidApp(const std::string& strFile);
+ static bool IsLibraryFolder(const std::string& strFile);
+ static bool IsLibraryContent(const std::string& strFile);
+ static bool IsPVR(const std::string& strFile);
+ static bool IsPVRChannel(const std::string& strFile);
+ static bool IsPVRChannelGroup(const std::string& strFile);
+ static bool IsPVRGuideItem(const std::string& strFile);
+
+ static std::string AppendSlash(std::string strFolder);
+ static void AddSlashAtEnd(std::string& strFolder);
+ static bool HasSlashAtEnd(const std::string& strFile, bool checkURL = false);
+ static void RemoveSlashAtEnd(std::string& strFolder);
+ static bool CompareWithoutSlashAtEnd(const std::string& strPath1, const std::string& strPath2);
+ static std::string FixSlashesAndDups(const std::string& path, const char slashCharacter = '/', const size_t startFrom = 0);
+ /**
+ * Convert path to form without duplicated slashes and without relative directories
+ * Strip duplicated slashes
+ * Resolve and remove relative directories ("/../" and "/./")
+ * Will ignore slashes with other direction than specified
+ * Will not resolve path starting from relative directory
+ * @warning Don't use with "protocol://path"-style URLs
+ * @param path string to process
+ * @param slashCharacter character to use as directory delimiter
+ * @return transformed path
+ */
+ static std::string CanonicalizePath(const std::string& path, const char slashCharacter = '\\');
+
+ static CURL CreateArchivePath(const std::string& type,
+ const CURL& archiveUrl,
+ const std::string& pathInArchive = "",
+ const std::string& password = "");
+
+ static std::string AddFileToFolder(const std::string& strFolder, const std::string& strFile);
+ template <typename... T>
+ static std::string AddFileToFolder(const std::string& strFolder, const std::string& strFile, T... args)
+ {
+ auto newPath = AddFileToFolder(strFolder, strFile);
+ return AddFileToFolder(newPath, args...);
+ }
+
+ static bool HasParentInHostname(const CURL& url);
+ static bool HasEncodedHostname(const CURL& url);
+ static bool HasEncodedFilename(const CURL& url);
+
+ /*!
+ \brief Cleans up the given path by resolving "." and ".."
+ and returns it.
+
+ This methods goes through the given path and removes any "."
+ (as it states "this directory") and resolves any ".." by
+ removing the previous directory from the path. This is done
+ for file paths and host names (in case of VFS paths).
+
+ \param path Path to be cleaned up
+ \return Actual path without any "." or ".."
+ */
+ static std::string GetRealPath(const std::string &path);
+
+ /*!
+ \brief Updates the URL encoded hostname of the given path
+
+ This method must only be used to update paths encoded with
+ the old (Eden) URL encoding implementation to the new (Frodo)
+ URL encoding implementation (which does not URL encode -_.!().
+
+ \param strFilename Path to update
+ \return True if the path has been updated/changed otherwise false
+ */
+ static bool UpdateUrlEncoding(std::string &strFilename);
+
+private:
+ static std::string resolvePath(const std::string &path);
+
+ static const CAdvancedSettings* m_advancedSettings;
+};
+
diff --git a/xbmc/utils/UrlOptions.cpp b/xbmc/utils/UrlOptions.cpp
new file mode 100644
index 0000000..389d08d
--- /dev/null
+++ b/xbmc/utils/UrlOptions.cpp
@@ -0,0 +1,170 @@
+/*
+ * 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 "UrlOptions.h"
+
+#include "URL.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+CUrlOptions::CUrlOptions() = default;
+
+CUrlOptions::CUrlOptions(const std::string &options, const char *strLead /* = "" */)
+ : m_strLead(strLead)
+{
+ AddOptions(options);
+}
+
+CUrlOptions::~CUrlOptions() = default;
+
+std::string CUrlOptions::GetOptionsString(bool withLeadingSeparator /* = false */) const
+{
+ std::string options;
+ for (const auto &opt : m_options)
+ {
+ if (!options.empty())
+ options += "&";
+
+ options += CURL::Encode(opt.first);
+ if (!opt.second.empty())
+ options += "=" + CURL::Encode(opt.second.asString());
+ }
+
+ if (withLeadingSeparator && !options.empty())
+ {
+ if (m_strLead.empty())
+ options = "?" + options;
+ else
+ options = m_strLead + options;
+ }
+
+ return options;
+}
+
+void CUrlOptions::AddOption(const std::string &key, const char *value)
+{
+ if (key.empty() || value == NULL)
+ return;
+
+ return AddOption(key, std::string(value));
+}
+
+void CUrlOptions::AddOption(const std::string &key, const std::string &value)
+{
+ if (key.empty())
+ return;
+
+ m_options[key] = value;
+}
+
+void CUrlOptions::AddOption(const std::string &key, int value)
+{
+ if (key.empty())
+ return;
+
+ m_options[key] = value;
+}
+
+void CUrlOptions::AddOption(const std::string &key, float value)
+{
+ if (key.empty())
+ return;
+
+ m_options[key] = value;
+}
+
+void CUrlOptions::AddOption(const std::string &key, double value)
+{
+ if (key.empty())
+ return;
+
+ m_options[key] = value;
+}
+
+void CUrlOptions::AddOption(const std::string &key, bool value)
+{
+ if (key.empty())
+ return;
+
+ m_options[key] = value;
+}
+
+void CUrlOptions::AddOptions(const std::string &options)
+{
+ if (options.empty())
+ return;
+
+ std::string strOptions = options;
+
+ // if matching the preset leading str, remove from options.
+ if (!m_strLead.empty() && strOptions.compare(0, m_strLead.length(), m_strLead) == 0)
+ strOptions.erase(0, m_strLead.length());
+ else if (strOptions.at(0) == '?' || strOptions.at(0) == '#' || strOptions.at(0) == ';' || strOptions.at(0) == '|')
+ {
+ // remove leading ?, #, ; or | if present
+ if (!m_strLead.empty())
+ CLog::Log(LOGWARNING, "{}: original leading str {} overridden by {}", __FUNCTION__, m_strLead,
+ strOptions.at(0));
+ m_strLead = strOptions.at(0);
+ strOptions.erase(0, 1);
+ }
+
+ // split the options by & and process them one by one
+ for (const auto &option : StringUtils::Split(strOptions, "&"))
+ {
+ if (option.empty())
+ continue;
+
+ std::string key, value;
+
+ size_t pos = option.find('=');
+ key = CURL::Decode(option.substr(0, pos));
+ if (pos != std::string::npos)
+ value = CURL::Decode(option.substr(pos + 1));
+
+ // the key cannot be empty
+ if (!key.empty())
+ AddOption(key, value);
+ }
+}
+
+void CUrlOptions::AddOptions(const CUrlOptions &options)
+{
+ m_options.insert(options.m_options.begin(), options.m_options.end());
+}
+
+void CUrlOptions::RemoveOption(const std::string &key)
+{
+ if (key.empty())
+ return;
+
+ auto option = m_options.find(key);
+ if (option != m_options.end())
+ m_options.erase(option);
+}
+
+bool CUrlOptions::HasOption(const std::string &key) const
+{
+ if (key.empty())
+ return false;
+
+ return m_options.find(key) != m_options.end();
+}
+
+bool CUrlOptions::GetOption(const std::string &key, CVariant &value) const
+{
+ if (key.empty())
+ return false;
+
+ auto option = m_options.find(key);
+ if (option == m_options.end())
+ return false;
+
+ value = option->second;
+ return true;
+}
diff --git a/xbmc/utils/UrlOptions.h b/xbmc/utils/UrlOptions.h
new file mode 100644
index 0000000..1fa7ac6
--- /dev/null
+++ b/xbmc/utils/UrlOptions.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "utils/Variant.h"
+
+#include <map>
+#include <string>
+
+class CUrlOptions
+{
+public:
+ typedef std::map<std::string, CVariant> UrlOptions;
+
+ CUrlOptions();
+ CUrlOptions(const std::string &options, const char *strLead = "");
+ virtual ~CUrlOptions();
+
+ void Clear() { m_options.clear(); m_strLead.clear(); }
+
+ const UrlOptions& GetOptions() const { return m_options; }
+ std::string GetOptionsString(bool withLeadingSeparator = false) const;
+
+ virtual void AddOption(const std::string &key, const char *value);
+ virtual void AddOption(const std::string &key, const std::string &value);
+ virtual void AddOption(const std::string &key, int value);
+ virtual void AddOption(const std::string &key, float value);
+ virtual void AddOption(const std::string &key, double value);
+ virtual void AddOption(const std::string &key, bool value);
+ virtual void AddOptions(const std::string &options);
+ virtual void AddOptions(const CUrlOptions &options);
+ virtual void RemoveOption(const std::string &key);
+
+ bool HasOption(const std::string &key) const;
+ bool GetOption(const std::string &key, CVariant &value) const;
+
+protected:
+ UrlOptions m_options;
+ std::string m_strLead;
+};
diff --git a/xbmc/utils/Utf8Utils.cpp b/xbmc/utils/Utf8Utils.cpp
new file mode 100644
index 0000000..a45002a
--- /dev/null
+++ b/xbmc/utils/Utf8Utils.cpp
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2013-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 "Utf8Utils.h"
+
+
+CUtf8Utils::utf8CheckResult CUtf8Utils::checkStrForUtf8(const std::string& str)
+{
+ const char* const strC = str.c_str();
+ const size_t len = str.length();
+ size_t pos = 0;
+ bool isPlainAscii = true;
+
+ while (pos < len)
+ {
+ const size_t chrLen = SizeOfUtf8Char(strC + pos);
+ if (chrLen == 0)
+ return hiAscii; // non valid UTF-8 sequence
+ else if (chrLen > 1)
+ isPlainAscii = false;
+
+ pos += chrLen;
+ }
+
+ if (isPlainAscii)
+ return plainAscii; // only single-byte characters (valid for US-ASCII and for UTF-8)
+
+ return utf8string; // valid UTF-8 with at least one valid UTF-8 multi-byte sequence
+}
+
+
+
+size_t CUtf8Utils::FindValidUtf8Char(const std::string& str, const size_t startPos /*= 0*/)
+{
+ const char* strC = str.c_str();
+ const size_t len = str.length();
+
+ size_t pos = startPos;
+ while (pos < len)
+ {
+ if (SizeOfUtf8Char(strC + pos))
+ return pos;
+
+ pos++;
+ }
+
+ return std::string::npos;
+}
+
+size_t CUtf8Utils::RFindValidUtf8Char(const std::string& str, const size_t startPos)
+{
+ const size_t len = str.length();
+ if (!len)
+ return std::string::npos;
+
+ const char* strC = str.c_str();
+ size_t pos = (startPos >= len) ? len - 1 : startPos;
+ while (pos < len) // pos is unsigned, after zero pos becomes large then len
+ {
+ if (SizeOfUtf8Char(strC + pos))
+ return pos;
+
+ pos--;
+ }
+
+ return std::string::npos;
+}
+
+inline size_t CUtf8Utils::SizeOfUtf8Char(const std::string& str, const size_t charStart /*= 0*/)
+{
+ if (charStart >= str.length())
+ return std::string::npos;
+
+ return SizeOfUtf8Char(str.c_str() + charStart);
+}
+
+// must be used only internally in class!
+// str must be null-terminated
+inline size_t CUtf8Utils::SizeOfUtf8Char(const char* const str)
+{
+ if (!str)
+ return 0;
+
+ const unsigned char* const strU = (const unsigned char*)str;
+ const unsigned char chr = strU[0];
+
+ /* this is an implementation of http://www.unicode.org/versions/Unicode6.2.0/ch03.pdf#G27506 */
+
+ /* U+0000 - U+007F in UTF-8 */
+ if (chr <= 0x7F)
+ return 1;
+
+ /* U+0080 - U+07FF in UTF-8 */ /* binary representation and range */
+ if (chr >= 0xC2 && chr <= 0xDF /* C2=1100 0010 - DF=1101 1111 */
+ // as str is null terminated,
+ && ((strU[1] & 0xC0) == 0x80)) /* C0=1100 0000, 80=1000 0000 - BF=1011 1111 */
+ return 2; // valid UTF-8 2 bytes sequence
+
+ /* U+0800 - U+0FFF in UTF-8 */
+ if (chr == 0xE0 /* E0=1110 0000 */
+ && (strU[1] & 0xE0) == 0xA0 /* E0=1110 0000, A0=1010 0000 - BF=1011 1111 */
+ && (strU[2] & 0xC0) == 0x80) /* C0=1100 0000, 80=1000 0000 - BF=1011 1111 */
+ return 3; // valid UTF-8 3 bytes sequence
+
+ /* U+1000 - U+CFFF in UTF-8 */
+ /* skip U+D000 - U+DFFF (handled later) */
+ /* U+E000 - U+FFFF in UTF-8 */
+ if (((chr >= 0xE1 && chr <= 0xEC) /* E1=1110 0001 - EC=1110 1100 */
+ || chr == 0xEE || chr == 0xEF) /* EE=1110 1110 - EF=1110 1111 */
+ && (strU[1] & 0xC0) == 0x80 /* C0=1100 0000, 80=1000 0000 - BF=1011 1111 */
+ && (strU[2] & 0xC0) == 0x80) /* C0=1100 0000, 80=1000 0000 - BF=1011 1111 */
+ return 3; // valid UTF-8 3 bytes sequence
+
+ /* U+D000 - U+D7FF in UTF-8 */
+ /* note: range U+D800 - U+DFFF is reserved and invalid */
+ if (chr == 0xED /* ED=1110 1101 */
+ && (strU[1] & 0xE0) == 0x80 /* E0=1110 0000, 80=1000 0000 - 9F=1001 1111 */
+ && (strU[2] & 0xC0) == 0x80) /* C0=1100 0000, 80=1000 0000 - BF=1011 1111 */
+ return 3; // valid UTF-8 3 bytes sequence
+
+ /* U+10000 - U+3FFFF in UTF-8 */
+ if (chr == 0xF0 /* F0=1111 0000 */
+ && (strU[1] & 0xE0) == 0x80 /* E0=1110 0000, 80=1000 0000 - 9F=1001 1111 */
+ && strU[2] >= 0x90 && strU[2] <= 0xBF /* 90=1001 0000 - BF=1011 1111 */
+ && (strU[3] & 0xC0) == 0x80) /* C0=1100 0000, 80=1000 0000 - BF=1011 1111 */
+ return 4; // valid UTF-8 4 bytes sequence
+
+ /* U+40000 - U+FFFFF in UTF-8 */
+ if (chr >= 0xF1 && chr <= 0xF3 /* F1=1111 0001 - F3=1111 0011 */
+ && (strU[1] & 0xC0) == 0x80 /* C0=1100 0000, 80=1000 0000 - BF=1011 1111 */
+ && (strU[2] & 0xC0) == 0x80 /* C0=1100 0000, 80=1000 0000 - BF=1011 1111 */
+ && (strU[3] & 0xC0) == 0x80) /* C0=1100 0000, 80=1000 0000 - BF=1011 1111 */
+ return 4; // valid UTF-8 4 bytes sequence
+
+ /* U+100000 - U+10FFFF in UTF-8 */
+ if (chr == 0xF4 /* F4=1111 0100 */
+ && (strU[1] & 0xF0) == 0x80 /* F0=1111 0000, 80=1000 0000 - 8F=1000 1111 */
+ && (strU[2] & 0xC0) == 0x80 /* C0=1100 0000, 80=1000 0000 - BF=1011 1111 */
+ && (strU[3] & 0xC0) == 0x80) /* C0=1100 0000, 80=1000 0000 - BF=1011 1111 */
+ return 4; // valid UTF-8 4 bytes sequence
+
+ return 0; // invalid UTF-8 char sequence
+}
diff --git a/xbmc/utils/Utf8Utils.h b/xbmc/utils/Utf8Utils.h
new file mode 100644
index 0000000..a29f64a
--- /dev/null
+++ b/xbmc/utils/Utf8Utils.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2013-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 <string>
+
+class CUtf8Utils
+{
+public:
+ enum utf8CheckResult
+ {
+ plainAscii = -1, // only US-ASCII characters (valid for UTF-8 too)
+ hiAscii = 0, // non-UTF-8 sequence with high ASCII characters
+ // (possible single-byte national encoding like WINDOWS-1251, multi-byte encoding like UTF-32 or invalid UTF-8)
+ utf8string = 1 // valid UTF-8 sequences, but not US-ASCII only
+ };
+
+ /**
+ * Check given string for valid UTF-8 sequences
+ * @param str string to check
+ * @return result of check, "plainAscii" for empty string
+ */
+ static utf8CheckResult checkStrForUtf8(const std::string& str);
+
+ static inline bool isValidUtf8(const std::string& str)
+ {
+ return checkStrForUtf8(str) != hiAscii;
+ }
+
+ static size_t FindValidUtf8Char(const std::string& str, const size_t startPos = 0);
+ static size_t RFindValidUtf8Char(const std::string& str, const size_t startPos);
+
+ static size_t SizeOfUtf8Char(const std::string& str, const size_t charStart = 0);
+private:
+ static size_t SizeOfUtf8Char(const char* const str);
+};
diff --git a/xbmc/utils/VC1BitstreamParser.cpp b/xbmc/utils/VC1BitstreamParser.cpp
new file mode 100644
index 0000000..8ac1b6e
--- /dev/null
+++ b/xbmc/utils/VC1BitstreamParser.cpp
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2017-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 "VC1BitstreamParser.h"
+
+#include "BitstreamReader.h"
+
+enum
+{
+ VC1_PROFILE_SIMPLE,
+ VC1_PROFILE_MAIN,
+ VC1_PROFILE_RESERVED,
+ VC1_PROFILE_ADVANCED,
+ VC1_PROFILE_NOPROFILE
+};
+
+enum
+{
+ VC1_END_OF_SEQ = 0x0A,
+ VC1_SLICE = 0x0B,
+ VC1_FIELD = 0x0C,
+ VC1_FRAME = 0x0D,
+ VC1_ENTRYPOINT = 0x0E,
+ VC1_SEQUENCE = 0x0F,
+ VC1_SLICE_USER = 0x1B,
+ VC1_FIELD_USER = 0x1C,
+ VC1_FRAME_USER = 0x1D,
+ VC1_ENTRY_POINT_USER = 0x1E,
+ VC1_SEQUENCE_USER = 0x1F
+};
+
+enum
+{
+ VC1_FRAME_PROGRESSIVE = 0x0,
+ VC1_FRAME_INTERLACE = 0x10,
+ VC1_FIELD_INTERLACE = 0x11
+};
+
+CVC1BitstreamParser::CVC1BitstreamParser()
+{
+ Reset();
+}
+
+void CVC1BitstreamParser::Reset()
+{
+ m_Profile = VC1_PROFILE_NOPROFILE;
+}
+
+bool CVC1BitstreamParser::IsRecoveryPoint(const uint8_t *buf, int buf_size)
+{
+ return vc1_parse_frame(buf, buf + buf_size, true);
+};
+
+bool CVC1BitstreamParser::IsIFrame(const uint8_t *buf, int buf_size)
+{
+ return vc1_parse_frame(buf, buf + buf_size, false);
+};
+
+bool CVC1BitstreamParser::vc1_parse_frame(const uint8_t *buf, const uint8_t *buf_end, bool sequence_only)
+{
+ uint32_t state = -1;
+ for (;;)
+ {
+ buf = find_start_code(buf, buf_end, &state);
+ if (buf >= buf_end)
+ break;
+ if (buf[-1] == VC1_SEQUENCE)
+ {
+ if (m_Profile != VC1_PROFILE_NOPROFILE)
+ return false;
+ CBitstreamReader br(buf, buf_end - buf);
+ // Read the profile
+ m_Profile = static_cast<uint8_t>(br.ReadBits(2));
+ if (m_Profile == VC1_PROFILE_ADVANCED)
+ {
+ br.SkipBits(39);
+ m_AdvInterlace = br.ReadBits(1);
+ }
+ else
+ {
+ br.SkipBits(22);
+
+ m_SimpleSkipBits = 2;
+ if (br.ReadBits(1)) //rangered
+ ++m_SimpleSkipBits;
+
+ m_MaxBFrames = br.ReadBits(3);
+
+ br.SkipBits(2); // quantizer
+ if (br.ReadBits(1)) //finterpflag
+ ++m_SimpleSkipBits;
+ }
+ if (sequence_only)
+ return true;
+ }
+ else if (buf[-1] == VC1_FRAME)
+ {
+ CBitstreamReader br(buf, buf_end - buf);
+
+ if (sequence_only)
+ return false;
+ if (m_Profile == VC1_PROFILE_ADVANCED)
+ {
+ uint8_t fcm;
+ if (m_AdvInterlace) {
+ fcm = br.ReadBits(1);
+ if (fcm)
+ fcm = br.ReadBits(1) + 1;
+ }
+ else
+ fcm = VC1_FRAME_PROGRESSIVE;
+ if (fcm == VC1_FIELD_INTERLACE) {
+ uint8_t pic = br.ReadBits(3);
+ return pic == 0x00 || pic == 0x01;
+ }
+ else
+ {
+ uint8_t pic(0);
+ while (pic < 4 && br.ReadBits(1))++pic;
+ return pic == 2;
+ }
+ return false;
+ }
+ else if (m_Profile != VC1_PROFILE_NOPROFILE)
+ {
+ br.SkipBits(m_SimpleSkipBits); // quantizer
+ uint8_t pic(br.ReadBits(1));
+ if (m_MaxBFrames) {
+ if (!pic) {
+ pic = br.ReadBits(1);
+ return pic != 0;
+ }
+ else
+ return false;
+ }
+ else
+ return pic != 0;
+ }
+ else
+ break;
+ }
+ }
+ return false;
+}
diff --git a/xbmc/utils/VC1BitstreamParser.h b/xbmc/utils/VC1BitstreamParser.h
new file mode 100644
index 0000000..882160c
--- /dev/null
+++ b/xbmc/utils/VC1BitstreamParser.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017-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 <stdint.h>
+
+class CVC1BitstreamParser
+{
+public:
+ CVC1BitstreamParser();
+ ~CVC1BitstreamParser() = default;
+
+ void Reset();
+
+ inline bool IsRecoveryPoint(const uint8_t *buf, int buf_size);
+ inline bool IsIFrame(const uint8_t *buf, int buf_size);
+
+protected:
+ bool vc1_parse_frame(const uint8_t *buf, const uint8_t *buf_end, bool sequenceOnly);
+private:
+ uint8_t m_Profile;
+ uint8_t m_MaxBFrames;
+ uint8_t m_SimpleSkipBits;
+ uint8_t m_AdvInterlace;
+};
diff --git a/xbmc/utils/Variant.cpp b/xbmc/utils/Variant.cpp
new file mode 100644
index 0000000..a9327c9
--- /dev/null
+++ b/xbmc/utils/Variant.cpp
@@ -0,0 +1,885 @@
+/*
+ * 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 "Variant.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <utility>
+
+#ifndef strtoll
+#ifdef TARGET_WINDOWS
+#define strtoll _strtoi64
+#define strtoull _strtoui64
+#define wcstoll _wcstoi64
+#define wcstoull _wcstoui64
+#else // TARGET_WINDOWS
+#if !defined(TARGET_DARWIN)
+#define strtoll(str, endptr, base) (int64_t)strtod(str, endptr)
+#define strtoull(str, endptr, base) (uint64_t)strtod(str, endptr)
+#define wcstoll(str, endptr, base) (int64_t)wcstod(str, endptr)
+#define wcstoull(str, endptr, base) (uint64_t)wcstod(str, endptr)
+#endif
+#endif // TARGET_WINDOWS
+#endif // strtoll
+
+std::string trimRight(const std::string &str)
+{
+ std::string tmp = str;
+ // find_last_not_of will return string::npos (which is defined as -1)
+ // or a value between 0 and size() - 1 => find_last_not_of() + 1 will
+ // always result in a valid index between 0 and size()
+ tmp.erase(tmp.find_last_not_of(" \n\r\t") + 1);
+
+ return tmp;
+}
+
+std::wstring trimRight(const std::wstring &str)
+{
+ std::wstring tmp = str;
+ // find_last_not_of will return string::npos (which is defined as -1)
+ // or a value between 0 and size() - 1 => find_last_not_of() + 1 will
+ // always result in a valid index between 0 and size()
+ tmp.erase(tmp.find_last_not_of(L" \n\r\t") + 1);
+
+ return tmp;
+}
+
+int64_t str2int64(const std::string &str, int64_t fallback /* = 0 */)
+{
+ char *end = NULL;
+ std::string tmp = trimRight(str);
+ int64_t result = strtoll(tmp.c_str(), &end, 0);
+ if (end == NULL || *end == '\0')
+ return result;
+
+ return fallback;
+}
+
+int64_t str2int64(const std::wstring &str, int64_t fallback /* = 0 */)
+{
+ wchar_t *end = NULL;
+ std::wstring tmp = trimRight(str);
+ int64_t result = wcstoll(tmp.c_str(), &end, 0);
+ if (end == NULL || *end == '\0')
+ return result;
+
+ return fallback;
+}
+
+uint64_t str2uint64(const std::string &str, uint64_t fallback /* = 0 */)
+{
+ char *end = NULL;
+ std::string tmp = trimRight(str);
+ uint64_t result = strtoull(tmp.c_str(), &end, 0);
+ if (end == NULL || *end == '\0')
+ return result;
+
+ return fallback;
+}
+
+uint64_t str2uint64(const std::wstring &str, uint64_t fallback /* = 0 */)
+{
+ wchar_t *end = NULL;
+ std::wstring tmp = trimRight(str);
+ uint64_t result = wcstoull(tmp.c_str(), &end, 0);
+ if (end == NULL || *end == '\0')
+ return result;
+
+ return fallback;
+}
+
+double str2double(const std::string &str, double fallback /* = 0.0 */)
+{
+ char *end = NULL;
+ std::string tmp = trimRight(str);
+ double result = strtod(tmp.c_str(), &end);
+ if (end == NULL || *end == '\0')
+ return result;
+
+ return fallback;
+}
+
+double str2double(const std::wstring &str, double fallback /* = 0.0 */)
+{
+ wchar_t *end = NULL;
+ std::wstring tmp = trimRight(str);
+ double result = wcstod(tmp.c_str(), &end);
+ if (end == NULL || *end == '\0')
+ return result;
+
+ return fallback;
+}
+
+CVariant::CVariant()
+ : CVariant(VariantTypeNull)
+{
+}
+
+CVariant CVariant::ConstNullVariant = CVariant::VariantTypeConstNull;
+CVariant::VariantArray CVariant::EMPTY_ARRAY;
+CVariant::VariantMap CVariant::EMPTY_MAP;
+
+CVariant::CVariant(VariantType type)
+{
+ m_type = type;
+
+ switch (type)
+ {
+ case VariantTypeInteger:
+ m_data.integer = 0;
+ break;
+ case VariantTypeUnsignedInteger:
+ m_data.unsignedinteger = 0;
+ break;
+ case VariantTypeBoolean:
+ m_data.boolean = false;
+ break;
+ case VariantTypeDouble:
+ m_data.dvalue = 0.0;
+ break;
+ case VariantTypeString:
+ m_data.string = new std::string();
+ break;
+ case VariantTypeWideString:
+ m_data.wstring = new std::wstring();
+ break;
+ case VariantTypeArray:
+ m_data.array = new VariantArray();
+ break;
+ case VariantTypeObject:
+ m_data.map = new VariantMap();
+ break;
+ default:
+#ifndef TARGET_WINDOWS_STORE // this corrupts the heap in Win10 UWP version
+ memset(&m_data, 0, sizeof(m_data));
+#endif
+ break;
+ }
+}
+
+CVariant::CVariant(int integer)
+{
+ m_type = VariantTypeInteger;
+ m_data.integer = integer;
+}
+
+CVariant::CVariant(int64_t integer)
+{
+ m_type = VariantTypeInteger;
+ m_data.integer = integer;
+}
+
+CVariant::CVariant(unsigned int unsignedinteger)
+{
+ m_type = VariantTypeUnsignedInteger;
+ m_data.unsignedinteger = unsignedinteger;
+}
+
+CVariant::CVariant(uint64_t unsignedinteger)
+{
+ m_type = VariantTypeUnsignedInteger;
+ m_data.unsignedinteger = unsignedinteger;
+}
+
+CVariant::CVariant(double value)
+{
+ m_type = VariantTypeDouble;
+ m_data.dvalue = value;
+}
+
+CVariant::CVariant(float value)
+{
+ m_type = VariantTypeDouble;
+ m_data.dvalue = (double)value;
+}
+
+CVariant::CVariant(bool boolean)
+{
+ m_type = VariantTypeBoolean;
+ m_data.boolean = boolean;
+}
+
+CVariant::CVariant(const char *str)
+{
+ m_type = VariantTypeString;
+ m_data.string = new std::string(str);
+}
+
+CVariant::CVariant(const char *str, unsigned int length)
+{
+ m_type = VariantTypeString;
+ m_data.string = new std::string(str, length);
+}
+
+CVariant::CVariant(const std::string &str)
+{
+ m_type = VariantTypeString;
+ m_data.string = new std::string(str);
+}
+
+CVariant::CVariant(std::string &&str)
+{
+ m_type = VariantTypeString;
+ m_data.string = new std::string(std::move(str));
+}
+
+CVariant::CVariant(const wchar_t *str)
+{
+ m_type = VariantTypeWideString;
+ m_data.wstring = new std::wstring(str);
+}
+
+CVariant::CVariant(const wchar_t *str, unsigned int length)
+{
+ m_type = VariantTypeWideString;
+ m_data.wstring = new std::wstring(str, length);
+}
+
+CVariant::CVariant(const std::wstring &str)
+{
+ m_type = VariantTypeWideString;
+ m_data.wstring = new std::wstring(str);
+}
+
+CVariant::CVariant(std::wstring &&str)
+{
+ m_type = VariantTypeWideString;
+ m_data.wstring = new std::wstring(std::move(str));
+}
+
+CVariant::CVariant(const std::vector<std::string> &strArray)
+{
+ m_type = VariantTypeArray;
+ m_data.array = new VariantArray;
+ m_data.array->reserve(strArray.size());
+ for (const auto& item : strArray)
+ m_data.array->push_back(CVariant(item));
+}
+
+CVariant::CVariant(const std::map<std::string, std::string> &strMap)
+{
+ m_type = VariantTypeObject;
+ m_data.map = new VariantMap;
+ for (std::map<std::string, std::string>::const_iterator it = strMap.begin(); it != strMap.end(); ++it)
+ m_data.map->insert(make_pair(it->first, CVariant(it->second)));
+}
+
+CVariant::CVariant(const std::map<std::string, CVariant> &variantMap)
+{
+ m_type = VariantTypeObject;
+ m_data.map = new VariantMap(variantMap.begin(), variantMap.end());
+}
+
+CVariant::CVariant(const CVariant &variant)
+{
+ m_type = VariantTypeNull;
+ *this = variant;
+}
+
+CVariant::CVariant(CVariant&& rhs) noexcept
+{
+ //Set this so that operator= don't try and run cleanup
+ //when we're not initialized.
+ m_type = VariantTypeNull;
+
+ *this = std::move(rhs);
+}
+
+CVariant::~CVariant()
+{
+ cleanup();
+}
+
+void CVariant::cleanup()
+{
+ switch (m_type)
+ {
+ case VariantTypeString:
+ delete m_data.string;
+ m_data.string = nullptr;
+ break;
+
+ case VariantTypeWideString:
+ delete m_data.wstring;
+ m_data.wstring = nullptr;
+ break;
+
+ case VariantTypeArray:
+ delete m_data.array;
+ m_data.array = nullptr;
+ break;
+
+ case VariantTypeObject:
+ delete m_data.map;
+ m_data.map = nullptr;
+ break;
+ default:
+ break;
+ }
+ m_type = VariantTypeNull;
+}
+
+bool CVariant::isInteger() const
+{
+ return isSignedInteger() || isUnsignedInteger();
+}
+
+bool CVariant::isSignedInteger() const
+{
+ return m_type == VariantTypeInteger;
+}
+
+bool CVariant::isUnsignedInteger() const
+{
+ return m_type == VariantTypeUnsignedInteger;
+}
+
+bool CVariant::isBoolean() const
+{
+ return m_type == VariantTypeBoolean;
+}
+
+bool CVariant::isDouble() const
+{
+ return m_type == VariantTypeDouble;
+}
+
+bool CVariant::isString() const
+{
+ return m_type == VariantTypeString;
+}
+
+bool CVariant::isWideString() const
+{
+ return m_type == VariantTypeWideString;
+}
+
+bool CVariant::isArray() const
+{
+ return m_type == VariantTypeArray;
+}
+
+bool CVariant::isObject() const
+{
+ return m_type == VariantTypeObject;
+}
+
+bool CVariant::isNull() const
+{
+ return m_type == VariantTypeNull || m_type == VariantTypeConstNull;
+}
+
+CVariant::VariantType CVariant::type() const
+{
+ return m_type;
+}
+
+int64_t CVariant::asInteger(int64_t fallback) const
+{
+ switch (m_type)
+ {
+ case VariantTypeInteger:
+ return m_data.integer;
+ case VariantTypeUnsignedInteger:
+ return (int64_t)m_data.unsignedinteger;
+ case VariantTypeDouble:
+ return (int64_t)m_data.dvalue;
+ case VariantTypeString:
+ return str2int64(*m_data.string, fallback);
+ case VariantTypeWideString:
+ return str2int64(*m_data.wstring, fallback);
+ default:
+ return fallback;
+ }
+
+ return fallback;
+}
+
+int32_t CVariant::asInteger32(int32_t fallback) const
+{
+ return static_cast<int32_t>(asInteger(fallback));
+}
+
+uint64_t CVariant::asUnsignedInteger(uint64_t fallback) const
+{
+ switch (m_type)
+ {
+ case VariantTypeUnsignedInteger:
+ return m_data.unsignedinteger;
+ case VariantTypeInteger:
+ return (uint64_t)m_data.integer;
+ case VariantTypeDouble:
+ return (uint64_t)m_data.dvalue;
+ case VariantTypeString:
+ return str2uint64(*m_data.string, fallback);
+ case VariantTypeWideString:
+ return str2uint64(*m_data.wstring, fallback);
+ default:
+ return fallback;
+ }
+
+ return fallback;
+}
+
+uint32_t CVariant::asUnsignedInteger32(uint32_t fallback) const
+{
+ return static_cast<uint32_t>(asUnsignedInteger(fallback));
+}
+
+double CVariant::asDouble(double fallback) const
+{
+ switch (m_type)
+ {
+ case VariantTypeDouble:
+ return m_data.dvalue;
+ case VariantTypeInteger:
+ return (double)m_data.integer;
+ case VariantTypeUnsignedInteger:
+ return (double)m_data.unsignedinteger;
+ case VariantTypeString:
+ return str2double(*m_data.string, fallback);
+ case VariantTypeWideString:
+ return str2double(*m_data.wstring, fallback);
+ default:
+ return fallback;
+ }
+
+ return fallback;
+}
+
+float CVariant::asFloat(float fallback) const
+{
+ switch (m_type)
+ {
+ case VariantTypeDouble:
+ return (float)m_data.dvalue;
+ case VariantTypeInteger:
+ return (float)m_data.integer;
+ case VariantTypeUnsignedInteger:
+ return (float)m_data.unsignedinteger;
+ case VariantTypeString:
+ return (float)str2double(*m_data.string, static_cast<double>(fallback));
+ case VariantTypeWideString:
+ return (float)str2double(*m_data.wstring, static_cast<double>(fallback));
+ default:
+ return fallback;
+ }
+
+ return fallback;
+}
+
+bool CVariant::asBoolean(bool fallback) const
+{
+ switch (m_type)
+ {
+ case VariantTypeBoolean:
+ return m_data.boolean;
+ case VariantTypeInteger:
+ return (m_data.integer != 0);
+ case VariantTypeUnsignedInteger:
+ return (m_data.unsignedinteger != 0);
+ case VariantTypeDouble:
+ return (m_data.dvalue != 0);
+ case VariantTypeString:
+ if (m_data.string->empty() || m_data.string->compare("0") == 0 || m_data.string->compare("false") == 0)
+ return false;
+ return true;
+ case VariantTypeWideString:
+ if (m_data.wstring->empty() || m_data.wstring->compare(L"0") == 0 || m_data.wstring->compare(L"false") == 0)
+ return false;
+ return true;
+ default:
+ return fallback;
+ }
+
+ return fallback;
+}
+
+std::string CVariant::asString(const std::string &fallback /* = "" */) const
+{
+ switch (m_type)
+ {
+ case VariantTypeString:
+ return *m_data.string;
+ case VariantTypeBoolean:
+ return m_data.boolean ? "true" : "false";
+ case VariantTypeInteger:
+ return std::to_string(m_data.integer);
+ case VariantTypeUnsignedInteger:
+ return std::to_string(m_data.unsignedinteger);
+ case VariantTypeDouble:
+ return std::to_string(m_data.dvalue);
+ default:
+ return fallback;
+ }
+
+ return fallback;
+}
+
+std::wstring CVariant::asWideString(const std::wstring &fallback /* = L"" */) const
+{
+ switch (m_type)
+ {
+ case VariantTypeWideString:
+ return *m_data.wstring;
+ case VariantTypeBoolean:
+ return m_data.boolean ? L"true" : L"false";
+ case VariantTypeInteger:
+ return std::to_wstring(m_data.integer);
+ case VariantTypeUnsignedInteger:
+ return std::to_wstring(m_data.unsignedinteger);
+ case VariantTypeDouble:
+ return std::to_wstring(m_data.dvalue);
+ default:
+ return fallback;
+ }
+
+ return fallback;
+}
+
+CVariant &CVariant::operator[](const std::string &key)
+{
+ if (m_type == VariantTypeNull)
+ {
+ m_type = VariantTypeObject;
+ m_data.map = new VariantMap;
+ }
+
+ if (m_type == VariantTypeObject)
+ return (*m_data.map)[key];
+ else
+ return ConstNullVariant;
+}
+
+const CVariant &CVariant::operator[](const std::string &key) const
+{
+ VariantMap::const_iterator it;
+ if (m_type == VariantTypeObject && (it = m_data.map->find(key)) != m_data.map->end())
+ return it->second;
+ else
+ return ConstNullVariant;
+}
+
+CVariant &CVariant::operator[](unsigned int position)
+{
+ if (m_type == VariantTypeArray && size() > position)
+ return m_data.array->at(position);
+ else
+ return ConstNullVariant;
+}
+
+const CVariant &CVariant::operator[](unsigned int position) const
+{
+ if (m_type == VariantTypeArray && size() > position)
+ return m_data.array->at(position);
+ else
+ return ConstNullVariant;
+}
+
+CVariant &CVariant::operator=(const CVariant &rhs)
+{
+ if (m_type == VariantTypeConstNull || this == &rhs)
+ return *this;
+
+ cleanup();
+
+ m_type = rhs.m_type;
+
+ switch (m_type)
+ {
+ case VariantTypeInteger:
+ m_data.integer = rhs.m_data.integer;
+ break;
+ case VariantTypeUnsignedInteger:
+ m_data.unsignedinteger = rhs.m_data.unsignedinteger;
+ break;
+ case VariantTypeBoolean:
+ m_data.boolean = rhs.m_data.boolean;
+ break;
+ case VariantTypeDouble:
+ m_data.dvalue = rhs.m_data.dvalue;
+ break;
+ case VariantTypeString:
+ m_data.string = new std::string(*rhs.m_data.string);
+ break;
+ case VariantTypeWideString:
+ m_data.wstring = new std::wstring(*rhs.m_data.wstring);
+ break;
+ case VariantTypeArray:
+ m_data.array = new VariantArray(rhs.m_data.array->begin(), rhs.m_data.array->end());
+ break;
+ case VariantTypeObject:
+ m_data.map = new VariantMap(rhs.m_data.map->begin(), rhs.m_data.map->end());
+ break;
+ default:
+ break;
+ }
+
+ return *this;
+}
+
+CVariant& CVariant::operator=(CVariant&& rhs) noexcept
+{
+ if (m_type == VariantTypeConstNull || this == &rhs)
+ return *this;
+
+ //Make sure that if we're moved into we don't leak any pointers
+ if (m_type != VariantTypeNull)
+ cleanup();
+
+ m_type = rhs.m_type;
+ m_data = rhs.m_data;
+
+ //Should be enough to just set m_type here
+ //but better safe than sorry, could probably lead to coverity warnings
+ if (rhs.m_type == VariantTypeString)
+ rhs.m_data.string = nullptr;
+ else if (rhs.m_type == VariantTypeWideString)
+ rhs.m_data.wstring = nullptr;
+ else if (rhs.m_type == VariantTypeArray)
+ rhs.m_data.array = nullptr;
+ else if (rhs.m_type == VariantTypeObject)
+ rhs.m_data.map = nullptr;
+
+ rhs.m_type = VariantTypeNull;
+
+ return *this;
+}
+
+bool CVariant::operator==(const CVariant &rhs) const
+{
+ if (m_type == rhs.m_type)
+ {
+ switch (m_type)
+ {
+ case VariantTypeInteger:
+ return m_data.integer == rhs.m_data.integer;
+ case VariantTypeUnsignedInteger:
+ return m_data.unsignedinteger == rhs.m_data.unsignedinteger;
+ case VariantTypeBoolean:
+ return m_data.boolean == rhs.m_data.boolean;
+ case VariantTypeDouble:
+ return m_data.dvalue == rhs.m_data.dvalue;
+ case VariantTypeString:
+ return *m_data.string == *rhs.m_data.string;
+ case VariantTypeWideString:
+ return *m_data.wstring == *rhs.m_data.wstring;
+ case VariantTypeArray:
+ return *m_data.array == *rhs.m_data.array;
+ case VariantTypeObject:
+ return *m_data.map == *rhs.m_data.map;
+ default:
+ break;
+ }
+ }
+
+ return false;
+}
+
+void CVariant::reserve(size_t length)
+{
+ if (m_type == VariantTypeNull)
+ {
+ m_type = VariantTypeArray;
+ m_data.array = new VariantArray;
+ }
+ if (m_type == VariantTypeArray)
+ m_data.array->reserve(length);
+}
+
+void CVariant::push_back(const CVariant &variant)
+{
+ if (m_type == VariantTypeNull)
+ {
+ m_type = VariantTypeArray;
+ m_data.array = new VariantArray;
+ }
+
+ if (m_type == VariantTypeArray)
+ m_data.array->push_back(variant);
+}
+
+void CVariant::push_back(CVariant &&variant)
+{
+ if (m_type == VariantTypeNull)
+ {
+ m_type = VariantTypeArray;
+ m_data.array = new VariantArray;
+ }
+
+ if (m_type == VariantTypeArray)
+ m_data.array->push_back(std::move(variant));
+}
+
+void CVariant::append(const CVariant &variant)
+{
+ push_back(variant);
+}
+
+void CVariant::append(CVariant&& variant)
+{
+ push_back(std::move(variant));
+}
+
+const char *CVariant::c_str() const
+{
+ if (m_type == VariantTypeString)
+ return m_data.string->c_str();
+ else
+ return NULL;
+}
+
+void CVariant::swap(CVariant &rhs)
+{
+ VariantType temp_type = m_type;
+ VariantUnion temp_data = m_data;
+
+ m_type = rhs.m_type;
+ m_data = rhs.m_data;
+
+ rhs.m_type = temp_type;
+ rhs.m_data = temp_data;
+}
+
+CVariant::iterator_array CVariant::begin_array()
+{
+ if (m_type == VariantTypeArray)
+ return m_data.array->begin();
+ else
+ return EMPTY_ARRAY.begin();
+}
+
+CVariant::const_iterator_array CVariant::begin_array() const
+{
+ if (m_type == VariantTypeArray)
+ return m_data.array->begin();
+ else
+ return EMPTY_ARRAY.begin();
+}
+
+CVariant::iterator_array CVariant::end_array()
+{
+ if (m_type == VariantTypeArray)
+ return m_data.array->end();
+ else
+ return EMPTY_ARRAY.end();
+}
+
+CVariant::const_iterator_array CVariant::end_array() const
+{
+ if (m_type == VariantTypeArray)
+ return m_data.array->end();
+ else
+ return EMPTY_ARRAY.end();
+}
+
+CVariant::iterator_map CVariant::begin_map()
+{
+ if (m_type == VariantTypeObject)
+ return m_data.map->begin();
+ else
+ return EMPTY_MAP.begin();
+}
+
+CVariant::const_iterator_map CVariant::begin_map() const
+{
+ if (m_type == VariantTypeObject)
+ return m_data.map->begin();
+ else
+ return EMPTY_MAP.begin();
+}
+
+CVariant::iterator_map CVariant::end_map()
+{
+ if (m_type == VariantTypeObject)
+ return m_data.map->end();
+ else
+ return EMPTY_MAP.end();
+}
+
+CVariant::const_iterator_map CVariant::end_map() const
+{
+ if (m_type == VariantTypeObject)
+ return m_data.map->end();
+ else
+ return EMPTY_MAP.end();
+}
+
+unsigned int CVariant::size() const
+{
+ if (m_type == VariantTypeObject)
+ return m_data.map->size();
+ else if (m_type == VariantTypeArray)
+ return m_data.array->size();
+ else if (m_type == VariantTypeString)
+ return m_data.string->size();
+ else if (m_type == VariantTypeWideString)
+ return m_data.wstring->size();
+ else
+ return 0;
+}
+
+bool CVariant::empty() const
+{
+ if (m_type == VariantTypeObject)
+ return m_data.map->empty();
+ else if (m_type == VariantTypeArray)
+ return m_data.array->empty();
+ else if (m_type == VariantTypeString)
+ return m_data.string->empty();
+ else if (m_type == VariantTypeWideString)
+ return m_data.wstring->empty();
+ else if (m_type == VariantTypeNull)
+ return true;
+
+ return false;
+}
+
+void CVariant::clear()
+{
+ if (m_type == VariantTypeObject)
+ m_data.map->clear();
+ else if (m_type == VariantTypeArray)
+ m_data.array->clear();
+ else if (m_type == VariantTypeString)
+ m_data.string->clear();
+ else if (m_type == VariantTypeWideString)
+ m_data.wstring->clear();
+}
+
+void CVariant::erase(const std::string &key)
+{
+ if (m_type == VariantTypeNull)
+ {
+ m_type = VariantTypeObject;
+ m_data.map = new VariantMap;
+ }
+ else if (m_type == VariantTypeObject)
+ m_data.map->erase(key);
+}
+
+void CVariant::erase(unsigned int position)
+{
+ if (m_type == VariantTypeNull)
+ {
+ m_type = VariantTypeArray;
+ m_data.array = new VariantArray;
+ }
+
+ if (m_type == VariantTypeArray && position < size())
+ m_data.array->erase(m_data.array->begin() + position);
+}
+
+bool CVariant::isMember(const std::string &key) const
+{
+ if (m_type == VariantTypeObject)
+ return m_data.map->find(key) != m_data.map->end();
+
+ return false;
+}
diff --git a/xbmc/utils/Variant.h b/xbmc/utils/Variant.h
new file mode 100644
index 0000000..9d48a3d
--- /dev/null
+++ b/xbmc/utils/Variant.h
@@ -0,0 +1,170 @@
+/*
+ * 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 <map>
+#include <stdint.h>
+#include <string>
+#include <vector>
+#include <wchar.h>
+
+int64_t str2int64(const std::string &str, int64_t fallback = 0);
+int64_t str2int64(const std::wstring &str, int64_t fallback = 0);
+uint64_t str2uint64(const std::string &str, uint64_t fallback = 0);
+uint64_t str2uint64(const std::wstring &str, uint64_t fallback = 0);
+double str2double(const std::string &str, double fallback = 0.0);
+double str2double(const std::wstring &str, double fallback = 0.0);
+
+#ifdef TARGET_WINDOWS_STORE
+#pragma pack(push)
+#pragma pack(8)
+#endif
+
+class CVariant
+{
+public:
+ enum VariantType
+ {
+ VariantTypeInteger,
+ VariantTypeUnsignedInteger,
+ VariantTypeBoolean,
+ VariantTypeString,
+ VariantTypeWideString,
+ VariantTypeDouble,
+ VariantTypeArray,
+ VariantTypeObject,
+ VariantTypeNull,
+ VariantTypeConstNull
+ };
+
+ CVariant();
+ CVariant(VariantType type);
+ CVariant(int integer);
+ CVariant(int64_t integer);
+ CVariant(unsigned int unsignedinteger);
+ CVariant(uint64_t unsignedinteger);
+ CVariant(double value);
+ CVariant(float value);
+ CVariant(bool boolean);
+ CVariant(const char *str);
+ CVariant(const char *str, unsigned int length);
+ CVariant(const std::string &str);
+ CVariant(std::string &&str);
+ CVariant(const wchar_t *str);
+ CVariant(const wchar_t *str, unsigned int length);
+ CVariant(const std::wstring &str);
+ CVariant(std::wstring &&str);
+ CVariant(const std::vector<std::string> &strArray);
+ CVariant(const std::map<std::string, std::string> &strMap);
+ CVariant(const std::map<std::string, CVariant> &variantMap);
+ CVariant(const CVariant &variant);
+ CVariant(CVariant&& rhs) noexcept;
+ ~CVariant();
+
+
+
+ bool isInteger() const;
+ bool isSignedInteger() const;
+ bool isUnsignedInteger() const;
+ bool isBoolean() const;
+ bool isString() const;
+ bool isWideString() const;
+ bool isDouble() const;
+ bool isArray() const;
+ bool isObject() const;
+ bool isNull() const;
+
+ VariantType type() const;
+
+ int64_t asInteger(int64_t fallback = 0) const;
+ int32_t asInteger32(int32_t fallback = 0) const;
+ uint64_t asUnsignedInteger(uint64_t fallback = 0u) const;
+ uint32_t asUnsignedInteger32(uint32_t fallback = 0u) const;
+ bool asBoolean(bool fallback = false) const;
+ std::string asString(const std::string &fallback = "") const;
+ std::wstring asWideString(const std::wstring &fallback = L"") const;
+ double asDouble(double fallback = 0.0) const;
+ float asFloat(float fallback = 0.0f) const;
+
+ CVariant &operator[](const std::string &key);
+ const CVariant &operator[](const std::string &key) const;
+ CVariant &operator[](unsigned int position);
+ const CVariant &operator[](unsigned int position) const;
+
+ CVariant &operator=(const CVariant &rhs);
+ CVariant& operator=(CVariant&& rhs) noexcept;
+ bool operator==(const CVariant &rhs) const;
+ bool operator!=(const CVariant &rhs) const { return !(*this == rhs); }
+
+ void reserve(size_t length);
+ void push_back(const CVariant &variant);
+ void push_back(CVariant &&variant);
+ void append(const CVariant &variant);
+ void append(CVariant &&variant);
+
+ const char *c_str() const;
+
+ void swap(CVariant &rhs);
+
+private:
+ typedef std::vector<CVariant> VariantArray;
+ typedef std::map<std::string, CVariant> VariantMap;
+
+public:
+ typedef VariantArray::iterator iterator_array;
+ typedef VariantArray::const_iterator const_iterator_array;
+
+ typedef VariantMap::iterator iterator_map;
+ typedef VariantMap::const_iterator const_iterator_map;
+
+ iterator_array begin_array();
+ const_iterator_array begin_array() const;
+ iterator_array end_array();
+ const_iterator_array end_array() const;
+
+ iterator_map begin_map();
+ const_iterator_map begin_map() const;
+ iterator_map end_map();
+ const_iterator_map end_map() const;
+
+ unsigned int size() const;
+ bool empty() const;
+ void clear();
+ void erase(const std::string &key);
+ void erase(unsigned int position);
+
+ bool isMember(const std::string &key) const;
+
+ static CVariant ConstNullVariant;
+
+private:
+ void cleanup();
+ union VariantUnion
+ {
+ int64_t integer;
+ uint64_t unsignedinteger;
+ bool boolean;
+ double dvalue;
+ std::string *string;
+ std::wstring *wstring;
+ VariantArray *array;
+ VariantMap *map;
+ };
+
+ VariantType m_type;
+ VariantUnion m_data;
+
+ static VariantArray EMPTY_ARRAY;
+ static VariantMap EMPTY_MAP;
+};
+
+#ifdef TARGET_WINDOWS_STORE
+#pragma pack(pop)
+#endif
+
diff --git a/xbmc/utils/Vector.cpp b/xbmc/utils/Vector.cpp
new file mode 100644
index 0000000..1f3a2fd
--- /dev/null
+++ b/xbmc/utils/Vector.cpp
@@ -0,0 +1,32 @@
+/*
+ * 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 "Vector.h"
+
+#include <math.h>
+
+CVector& CVector::operator+=(const CVector &other)
+{
+ x += other.x;
+ y += other.y;
+
+ return *this;
+}
+
+CVector& CVector::operator-=(const CVector &other)
+{
+ x -= other.x;
+ y -= other.y;
+
+ return *this;
+}
+
+float CVector::length() const
+{
+ return sqrt(pow(x, 2) + pow(y, 2));
+}
diff --git a/xbmc/utils/Vector.h b/xbmc/utils/Vector.h
new file mode 100644
index 0000000..f427ec9
--- /dev/null
+++ b/xbmc/utils/Vector.h
@@ -0,0 +1,39 @@
+/*
+ * 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
+
+class CVector
+{
+public:
+ CVector() = default;
+ constexpr CVector(float xCoord, float yCoord):x(xCoord), y(yCoord) {}
+
+ constexpr CVector operator+(const CVector &other) const
+ {
+ return CVector(x + other.x, y + other.y);
+ }
+
+ constexpr CVector operator-(const CVector &other) const
+ {
+ return CVector(x - other.x, y - other.y);
+ }
+
+ CVector& operator+=(const CVector &other);
+ CVector& operator-=(const CVector &other);
+
+ constexpr float scalar(const CVector &other) const
+ {
+ return x * other.x + y * other.y;
+ }
+
+ float length() const;
+
+ float x = 0;
+ float y = 0;
+};
diff --git a/xbmc/utils/XBMCTinyXML.cpp b/xbmc/utils/XBMCTinyXML.cpp
new file mode 100644
index 0000000..612ddf2
--- /dev/null
+++ b/xbmc/utils/XBMCTinyXML.cpp
@@ -0,0 +1,273 @@
+/*
+ * 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 "XBMCTinyXML.h"
+
+#include "LangInfo.h"
+#include "RegExp.h"
+#include "filesystem/File.h"
+#include "utils/CharsetConverter.h"
+#include "utils/CharsetDetection.h"
+#include "utils/StringUtils.h"
+#include "utils/Utf8Utils.h"
+#include "utils/log.h"
+
+#define MAX_ENTITY_LENGTH 8 // size of largest entity "&#xNNNN;"
+#define BUFFER_SIZE 4096
+
+CXBMCTinyXML::CXBMCTinyXML()
+: TiXmlDocument()
+{
+}
+
+CXBMCTinyXML::CXBMCTinyXML(const char *documentName)
+: TiXmlDocument(documentName)
+{
+}
+
+CXBMCTinyXML::CXBMCTinyXML(const std::string& documentName)
+: TiXmlDocument(documentName)
+{
+}
+
+CXBMCTinyXML::CXBMCTinyXML(const std::string& documentName, const std::string& documentCharset)
+: TiXmlDocument(documentName), m_SuggestedCharset(documentCharset)
+{
+ StringUtils::ToUpper(m_SuggestedCharset);
+}
+
+bool CXBMCTinyXML::LoadFile(TiXmlEncoding encoding)
+{
+ return LoadFile(value, encoding);
+}
+
+bool CXBMCTinyXML::LoadFile(const char *_filename, TiXmlEncoding encoding)
+{
+ return LoadFile(std::string(_filename), encoding);
+}
+
+bool CXBMCTinyXML::LoadFile(const std::string& _filename, TiXmlEncoding encoding)
+{
+ value = _filename.c_str();
+
+ XFILE::CFile file;
+ std::vector<uint8_t> buffer;
+
+ if (file.LoadFile(value, buffer) <= 0)
+ {
+ SetError(TIXML_ERROR_OPENING_FILE, NULL, NULL, TIXML_ENCODING_UNKNOWN);
+ return false;
+ }
+
+ // Delete the existing data:
+ Clear();
+ location.Clear();
+
+ std::string data(reinterpret_cast<char*>(buffer.data()), buffer.size());
+ buffer.clear(); // free memory early
+
+ if (encoding == TIXML_ENCODING_UNKNOWN)
+ Parse(data, file.GetProperty(XFILE::FILE_PROPERTY_CONTENT_CHARSET));
+ else
+ Parse(data, encoding);
+
+ if (Error())
+ return false;
+ return true;
+}
+
+bool CXBMCTinyXML::LoadFile(const std::string& _filename, const std::string& documentCharset)
+{
+ m_SuggestedCharset = documentCharset;
+ StringUtils::ToUpper(m_SuggestedCharset);
+ return LoadFile(_filename, TIXML_ENCODING_UNKNOWN);
+}
+
+bool CXBMCTinyXML::LoadFile(FILE *f, TiXmlEncoding encoding)
+{
+ std::string data;
+ char buf[BUFFER_SIZE] = {};
+ int result;
+ while ((result = fread(buf, 1, BUFFER_SIZE, f)) > 0)
+ data.append(buf, result);
+ return Parse(data, encoding);
+}
+
+bool CXBMCTinyXML::SaveFile(const char *_filename) const
+{
+ return SaveFile(std::string(_filename));
+}
+
+bool CXBMCTinyXML::SaveFile(const std::string& filename) const
+{
+ XFILE::CFile file;
+ if (file.OpenForWrite(filename, true))
+ {
+ TiXmlPrinter printer;
+ Accept(&printer);
+ bool suc = file.Write(printer.CStr(), printer.Size()) == static_cast<ssize_t>(printer.Size());
+ if (suc)
+ file.Flush();
+
+ return suc;
+ }
+ return false;
+}
+
+bool CXBMCTinyXML::Parse(const std::string& data, const std::string& dataCharset)
+{
+ m_SuggestedCharset = dataCharset;
+ StringUtils::ToUpper(m_SuggestedCharset);
+ return Parse(data, TIXML_ENCODING_UNKNOWN);
+}
+
+bool CXBMCTinyXML::Parse(const std::string& data, TiXmlEncoding encoding /*= TIXML_DEFAULT_ENCODING */)
+{
+ m_UsedCharset.clear();
+ if (encoding != TIXML_ENCODING_UNKNOWN)
+ { // encoding != TIXML_ENCODING_UNKNOWN means "do not use m_SuggestedCharset and charset detection"
+ m_SuggestedCharset.clear();
+ if (encoding == TIXML_ENCODING_UTF8)
+ m_UsedCharset = "UTF-8";
+
+ return InternalParse(data, encoding);
+ }
+
+ if (!m_SuggestedCharset.empty() && TryParse(data, m_SuggestedCharset))
+ return true;
+
+ std::string detectedCharset;
+ if (CCharsetDetection::DetectXmlEncoding(data, detectedCharset) && TryParse(data, detectedCharset))
+ {
+ if (!m_SuggestedCharset.empty())
+ CLog::Log(LOGWARNING,
+ "{}: \"{}\" charset was used instead of suggested charset \"{}\" for {}",
+ __FUNCTION__, m_UsedCharset, m_SuggestedCharset,
+ (value.empty() ? "XML data" : ("file \"" + value + "\"")));
+
+ return true;
+ }
+
+ // check for valid UTF-8
+ if (m_SuggestedCharset != "UTF-8" && detectedCharset != "UTF-8" && CUtf8Utils::isValidUtf8(data) &&
+ TryParse(data, "UTF-8"))
+ {
+ if (!m_SuggestedCharset.empty())
+ CLog::Log(LOGWARNING,
+ "{}: \"{}\" charset was used instead of suggested charset \"{}\" for {}",
+ __FUNCTION__, m_UsedCharset, m_SuggestedCharset,
+ (value.empty() ? "XML data" : ("file \"" + value + "\"")));
+ else if (!detectedCharset.empty())
+ CLog::Log(LOGWARNING, "{}: \"{}\" charset was used instead of detected charset \"{}\" for {}",
+ __FUNCTION__, m_UsedCharset, detectedCharset,
+ (value.empty() ? "XML data" : ("file \"" + value + "\"")));
+ return true;
+ }
+
+ // fallback: try user GUI charset
+ if (TryParse(data, g_langInfo.GetGuiCharSet()))
+ {
+ if (!m_SuggestedCharset.empty())
+ CLog::Log(LOGWARNING,
+ "{}: \"{}\" charset was used instead of suggested charset \"{}\" for {}",
+ __FUNCTION__, m_UsedCharset, m_SuggestedCharset,
+ (value.empty() ? "XML data" : ("file \"" + value + "\"")));
+ else if (!detectedCharset.empty())
+ CLog::Log(LOGWARNING, "{}: \"{}\" charset was used instead of detected charset \"{}\" for {}",
+ __FUNCTION__, m_UsedCharset, detectedCharset,
+ (value.empty() ? "XML data" : ("file \"" + value + "\"")));
+ return true;
+ }
+
+ // can't detect correct data charset, try to process data as is
+ if (InternalParse(data, TIXML_ENCODING_UNKNOWN))
+ {
+ if (!m_SuggestedCharset.empty())
+ CLog::Log(LOGWARNING, "{}: Processed {} as unknown encoding instead of suggested \"{}\"",
+ __FUNCTION__, (value.empty() ? "XML data" : ("file \"" + value + "\"")),
+ m_SuggestedCharset);
+ else if (!detectedCharset.empty())
+ CLog::Log(LOGWARNING, "{}: Processed {} as unknown encoding instead of detected \"{}\"",
+ __FUNCTION__, (value.empty() ? "XML data" : ("file \"" + value + "\"")),
+ detectedCharset);
+ return true;
+ }
+
+ return false;
+}
+
+bool CXBMCTinyXML::TryParse(const std::string& data, const std::string& tryDataCharset)
+{
+ if (tryDataCharset == "UTF-8")
+ InternalParse(data, TIXML_ENCODING_UTF8); // process data without conversion
+ else if (!tryDataCharset.empty())
+ {
+ std::string converted;
+ /* some wrong conversions can leave US-ASCII XML header and structure untouched but break non-English data
+ * so conversion must fail on wrong character and then other encodings will be tried */
+ if (!g_charsetConverter.ToUtf8(tryDataCharset, data, converted, true) || converted.empty())
+ return false; // can't convert data
+
+ InternalParse(converted, TIXML_ENCODING_UTF8);
+ }
+ else
+ InternalParse(data, TIXML_ENCODING_LEGACY);
+
+ // 'Error()' contains result of last run of 'TiXmlDocument::Parse()'
+ if (Error())
+ {
+ Clear();
+ location.Clear();
+
+ return false;
+ }
+
+ m_UsedCharset = tryDataCharset;
+ return true;
+}
+
+bool CXBMCTinyXML::InternalParse(const std::string& rawdata, TiXmlEncoding encoding /*= TIXML_DEFAULT_ENCODING */)
+{
+ // Preprocess string, replacing '&' with '&amp; for invalid XML entities
+ size_t pos = rawdata.find('&');
+ if (pos == std::string::npos)
+ return (TiXmlDocument::Parse(rawdata.c_str(), NULL, encoding) != NULL); // nothing to fix, process data directly
+
+ std::string data(rawdata);
+ CRegExp re(false, CRegExp::asciiOnly, "^&(amp|lt|gt|quot|apos|#x[a-fA-F0-9]{1,4}|#[0-9]{1,5});.*");
+ do
+ {
+ if (re.RegFind(data, pos, MAX_ENTITY_LENGTH) < 0)
+ data.insert(pos + 1, "amp;");
+ pos = data.find('&', pos + 1);
+ } while (pos != std::string::npos);
+
+ return (TiXmlDocument::Parse(data.c_str(), NULL, encoding) != NULL);
+}
+
+bool CXBMCTinyXML::Test()
+{
+ // scraper results with unescaped &
+ CXBMCTinyXML doc;
+ std::string data("<details><url function=\"ParseTMDBRating\" "
+ "cache=\"tmdb-en-12244.json\">"
+ "http://api.themoviedb.org/3/movie/12244"
+ "?api_key=57983e31fb435df4df77afb854740ea9"
+ "&language=en&#x3f;&#x003F;&#0063;</url></details>");
+ doc.Parse(data, TIXML_DEFAULT_ENCODING);
+ TiXmlNode *root = doc.RootElement();
+ if (root && root->ValueStr() == "details")
+ {
+ TiXmlElement *url = root->FirstChildElement("url");
+ if (url && url->FirstChild())
+ {
+ return (url->FirstChild()->ValueStr() == "http://api.themoviedb.org/3/movie/12244?api_key=57983e31fb435df4df77afb854740ea9&language=en???");
+ }
+ }
+ return false;
+}
diff --git a/xbmc/utils/XBMCTinyXML.h b/xbmc/utils/XBMCTinyXML.h
new file mode 100644
index 0000000..2f4e188
--- /dev/null
+++ b/xbmc/utils/XBMCTinyXML.h
@@ -0,0 +1,59 @@
+/*
+ * 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
+
+#ifndef TARGET_WINDOWS
+//compile fix for TinyXml < 2.6.0
+#define DOCUMENT TINYXML_DOCUMENT
+#define ELEMENT TINYXML_ELEMENT
+#define COMMENT TINYXML_COMMENT
+#define UNKNOWN TINYXML_UNKNOWN
+#define TEXT TINYXML_TEXT
+#define DECLARATION TINYXML_DECLARATION
+#define TYPECOUNT TINYXML_TYPECOUNT
+#endif
+
+#include <tinyxml.h>
+#include <string>
+
+#undef DOCUMENT
+#undef ELEMENT
+#undef COMMENT
+#undef UNKNOWN
+//#undef TEXT
+#undef DECLARATION
+#undef TYPECOUNT
+
+class CXBMCTinyXML : public TiXmlDocument
+{
+public:
+ CXBMCTinyXML();
+ explicit CXBMCTinyXML(const char*);
+ explicit CXBMCTinyXML(const std::string& documentName);
+ CXBMCTinyXML(const std::string& documentName, const std::string& documentCharset);
+ bool LoadFile(TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING);
+ bool LoadFile(const char*, TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING);
+ bool LoadFile(const std::string& _filename, TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING);
+ bool LoadFile(const std::string& _filename, const std::string& documentCharset);
+ bool LoadFile(FILE*, TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING);
+ bool SaveFile(const char*) const;
+ bool SaveFile(const std::string& filename) const;
+ bool Parse(const std::string& data, TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING);
+ bool Parse(const std::string& data, const std::string& dataCharset);
+ inline std::string GetSuggestedCharset(void) const { return m_SuggestedCharset; }
+ inline std::string GetUsedCharset(void) const { return m_UsedCharset; }
+ static bool Test();
+protected:
+ using TiXmlDocument::Parse;
+ bool TryParse(const std::string& data, const std::string& tryDataCharset);
+ bool InternalParse(const std::string& rawdata, TiXmlEncoding encoding = TIXML_DEFAULT_ENCODING);
+
+ std::string m_SuggestedCharset;
+ std::string m_UsedCharset;
+};
diff --git a/xbmc/utils/XMLUtils.cpp b/xbmc/utils/XMLUtils.cpp
new file mode 100644
index 0000000..8e8603e
--- /dev/null
+++ b/xbmc/utils/XMLUtils.cpp
@@ -0,0 +1,345 @@
+/*
+ * 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 "XMLUtils.h"
+
+#include "StringUtils.h"
+#include "URL.h"
+#include "XBDateTime.h"
+
+bool XMLUtils::GetHex(const TiXmlNode* pRootNode, const char* strTag, uint32_t& hexValue)
+{
+ const TiXmlNode* pNode = pRootNode->FirstChild(strTag );
+ if (!pNode || !pNode->FirstChild()) return false;
+ return sscanf(pNode->FirstChild()->Value(), "%x", &hexValue) == 1;
+}
+
+
+bool XMLUtils::GetUInt(const TiXmlNode* pRootNode, const char* strTag, uint32_t& uintValue)
+{
+ const TiXmlNode* pNode = pRootNode->FirstChild(strTag );
+ if (!pNode || !pNode->FirstChild()) return false;
+ uintValue = atol(pNode->FirstChild()->Value());
+ return true;
+}
+
+bool XMLUtils::GetUInt(const TiXmlNode* pRootNode, const char* strTag, uint32_t &value, const uint32_t min, const uint32_t max)
+{
+ if (GetUInt(pRootNode, strTag, value))
+ {
+ if (value < min) value = min;
+ if (value > max) value = max;
+ return true;
+ }
+ return false;
+}
+
+bool XMLUtils::GetLong(const TiXmlNode* pRootNode, const char* strTag, long& lLongValue)
+{
+ const TiXmlNode* pNode = pRootNode->FirstChild(strTag );
+ if (!pNode || !pNode->FirstChild()) return false;
+ lLongValue = atol(pNode->FirstChild()->Value());
+ return true;
+}
+
+bool XMLUtils::GetInt(const TiXmlNode* pRootNode, const char* strTag, int& iIntValue)
+{
+ const TiXmlNode* pNode = pRootNode->FirstChild(strTag );
+ if (!pNode || !pNode->FirstChild()) return false;
+ iIntValue = atoi(pNode->FirstChild()->Value());
+ return true;
+}
+
+bool XMLUtils::GetInt(const TiXmlNode* pRootNode, const char* strTag, int &value, const int min, const int max)
+{
+ if (GetInt(pRootNode, strTag, value))
+ {
+ if (value < min) value = min;
+ if (value > max) value = max;
+ return true;
+ }
+ return false;
+}
+
+bool XMLUtils::GetDouble(const TiXmlNode* root, const char* tag, double& value)
+{
+ const TiXmlNode* node = root->FirstChild(tag);
+ if (!node || !node->FirstChild()) return false;
+ value = atof(node->FirstChild()->Value());
+ return true;
+}
+
+bool XMLUtils::GetFloat(const TiXmlNode* pRootNode, const char* strTag, float& value)
+{
+ const TiXmlNode* pNode = pRootNode->FirstChild(strTag );
+ if (!pNode || !pNode->FirstChild()) return false;
+ value = (float)atof(pNode->FirstChild()->Value());
+ return true;
+}
+
+bool XMLUtils::GetFloat(const TiXmlNode* pRootElement, const char *tagName, float& fValue, const float fMin, const float fMax)
+{
+ if (GetFloat(pRootElement, tagName, fValue))
+ { // check range
+ if (fValue < fMin) fValue = fMin;
+ if (fValue > fMax) fValue = fMax;
+ return true;
+ }
+ return false;
+}
+
+bool XMLUtils::GetBoolean(const TiXmlNode* pRootNode, const char* strTag, bool& bBoolValue)
+{
+ const TiXmlNode* pNode = pRootNode->FirstChild(strTag );
+ if (!pNode || !pNode->FirstChild()) return false;
+ std::string strEnabled = pNode->FirstChild()->ValueStr();
+ StringUtils::ToLower(strEnabled);
+ if (strEnabled == "off" || strEnabled == "no" || strEnabled == "disabled" || strEnabled == "false" || strEnabled == "0" )
+ bBoolValue = false;
+ else
+ {
+ bBoolValue = true;
+ if (strEnabled != "on" && strEnabled != "yes" && strEnabled != "enabled" && strEnabled != "true")
+ return false; // invalid bool switch - it's probably some other string.
+ }
+ return true;
+}
+
+bool XMLUtils::GetString(const TiXmlNode* pRootNode, const char* strTag, std::string& strStringValue)
+{
+ const TiXmlElement* pElement = pRootNode->FirstChildElement(strTag);
+ if (!pElement) return false;
+
+ const char* encoded = pElement->Attribute("urlencoded");
+ const TiXmlNode* pNode = pElement->FirstChild();
+ if (pNode != NULL)
+ {
+ strStringValue = pNode->ValueStr();
+ if (encoded && StringUtils::CompareNoCase(encoded, "yes") == 0)
+ strStringValue = CURL::Decode(strStringValue);
+ return true;
+ }
+ strStringValue.clear();
+ return true;
+}
+
+std::string XMLUtils::GetString(const TiXmlNode* pRootNode, const char* strTag)
+{
+ std::string temp;
+ GetString(pRootNode, strTag, temp);
+ return temp;
+}
+
+bool XMLUtils::HasChild(const TiXmlNode* pRootNode, const char* strTag)
+{
+ const TiXmlElement* pElement = pRootNode->FirstChildElement(strTag);
+ if (!pElement) return false;
+ const TiXmlNode* pNode = pElement->FirstChild();
+ return (pNode != NULL);
+}
+
+bool XMLUtils::GetAdditiveString(const TiXmlNode* pRootNode, const char* strTag,
+ const std::string& strSeparator, std::string& strStringValue,
+ bool clear)
+{
+ std::string strTemp;
+ const TiXmlElement* node = pRootNode->FirstChildElement(strTag);
+ bool bResult=false;
+ if (node && node->FirstChild() && clear)
+ strStringValue.clear();
+ while (node)
+ {
+ if (node->FirstChild())
+ {
+ bResult = true;
+ strTemp = node->FirstChild()->Value();
+ const char* clear=node->Attribute("clear");
+ if (strStringValue.empty() || (clear && StringUtils::CompareNoCase(clear, "true") == 0))
+ strStringValue = strTemp;
+ else
+ strStringValue += strSeparator+strTemp;
+ }
+ node = node->NextSiblingElement(strTag);
+ }
+
+ return bResult;
+}
+
+/*!
+ Parses the XML for multiple tags of the given name.
+ Does not clear the array to support chaining.
+*/
+bool XMLUtils::GetStringArray(const TiXmlNode* pRootNode, const char* strTag, std::vector<std::string>& arrayValue, bool clear /* = false */, const std::string& separator /* = "" */)
+{
+ std::string strTemp;
+ const TiXmlElement* node = pRootNode->FirstChildElement(strTag);
+ bool bResult=false;
+ if (node && node->FirstChild() && clear)
+ arrayValue.clear();
+ while (node)
+ {
+ if (node->FirstChild())
+ {
+ bResult = true;
+ strTemp = node->FirstChild()->ValueStr();
+
+ const char* clearAttr = node->Attribute("clear");
+ if (clearAttr && StringUtils::CompareNoCase(clearAttr, "true") == 0)
+ arrayValue.clear();
+
+ if (strTemp.empty())
+ continue;
+
+ if (separator.empty())
+ arrayValue.push_back(strTemp);
+ else
+ {
+ std::vector<std::string> tempArray = StringUtils::Split(strTemp, separator);
+ arrayValue.insert(arrayValue.end(), tempArray.begin(), tempArray.end());
+ }
+ }
+ node = node->NextSiblingElement(strTag);
+ }
+
+ return bResult;
+}
+
+bool XMLUtils::GetPath(const TiXmlNode* pRootNode, const char* strTag, std::string& strStringValue)
+{
+ const TiXmlElement* pElement = pRootNode->FirstChildElement(strTag);
+ if (!pElement) return false;
+
+ const char* encoded = pElement->Attribute("urlencoded");
+ const TiXmlNode* pNode = pElement->FirstChild();
+ if (pNode != NULL)
+ {
+ strStringValue = pNode->Value();
+ if (encoded && StringUtils::CompareNoCase(encoded, "yes") == 0)
+ strStringValue = CURL::Decode(strStringValue);
+ return true;
+ }
+ strStringValue.clear();
+ return false;
+}
+
+bool XMLUtils::GetDate(const TiXmlNode* pRootNode, const char* strTag, CDateTime& date)
+{
+ std::string strDate;
+ if (GetString(pRootNode, strTag, strDate) && !strDate.empty())
+ {
+ date.SetFromDBDate(strDate);
+ return true;
+ }
+
+ return false;
+}
+
+bool XMLUtils::GetDateTime(const TiXmlNode* pRootNode, const char* strTag, CDateTime& dateTime)
+{
+ std::string strDateTime;
+ if (GetString(pRootNode, strTag, strDateTime) && !strDateTime.empty())
+ {
+ dateTime.SetFromDBDateTime(strDateTime);
+ return true;
+ }
+
+ return false;
+}
+
+std::string XMLUtils::GetAttribute(const TiXmlElement *element, const char *tag)
+{
+ if (element)
+ {
+ const char *attribute = element->Attribute(tag);
+ if (attribute)
+ return attribute;
+ }
+ return "";
+}
+
+void XMLUtils::SetAdditiveString(TiXmlNode* pRootNode, const char *strTag, const std::string& strSeparator, const std::string& strValue)
+{
+ std::vector<std::string> list = StringUtils::Split(strValue, strSeparator);
+ for (std::vector<std::string>::const_iterator i = list.begin(); i != list.end(); ++i)
+ SetString(pRootNode, strTag, *i);
+}
+
+void XMLUtils::SetStringArray(TiXmlNode* pRootNode, const char *strTag, const std::vector<std::string>& arrayValue)
+{
+ for (unsigned int i = 0; i < arrayValue.size(); i++)
+ SetString(pRootNode, strTag, arrayValue.at(i));
+}
+
+TiXmlNode* XMLUtils::SetString(TiXmlNode* pRootNode, const char *strTag, const std::string& strValue)
+{
+ TiXmlElement newElement(strTag);
+ TiXmlNode *pNewNode = pRootNode->InsertEndChild(newElement);
+ if (pNewNode)
+ {
+ TiXmlText value(strValue);
+ pNewNode->InsertEndChild(value);
+ }
+ return pNewNode;
+}
+
+TiXmlNode* XMLUtils::SetInt(TiXmlNode* pRootNode, const char *strTag, int value)
+{
+ std::string strValue = std::to_string(value);
+ return SetString(pRootNode, strTag, strValue);
+}
+
+void XMLUtils::SetLong(TiXmlNode* pRootNode, const char *strTag, long value)
+{
+ std::string strValue = std::to_string(value);
+ SetString(pRootNode, strTag, strValue);
+}
+
+TiXmlNode* XMLUtils::SetFloat(TiXmlNode* pRootNode, const char *strTag, float value)
+{
+ std::string strValue = StringUtils::Format("{:f}", value);
+ return SetString(pRootNode, strTag, strValue);
+}
+
+TiXmlNode* XMLUtils::SetDouble(TiXmlNode* pRootNode, const char* strTag, double value)
+{
+ std::string strValue = StringUtils::Format("{:f}", value);
+ return SetString(pRootNode, strTag, strValue);
+}
+
+void XMLUtils::SetBoolean(TiXmlNode* pRootNode, const char *strTag, bool value)
+{
+ SetString(pRootNode, strTag, value ? "true" : "false");
+}
+
+void XMLUtils::SetHex(TiXmlNode* pRootNode, const char *strTag, uint32_t value)
+{
+ std::string strValue = StringUtils::Format("{:x}", value);
+ SetString(pRootNode, strTag, strValue);
+}
+
+void XMLUtils::SetPath(TiXmlNode* pRootNode, const char *strTag, const std::string& strValue)
+{
+ TiXmlElement newElement(strTag);
+ newElement.SetAttribute("pathversion", path_version);
+ TiXmlNode *pNewNode = pRootNode->InsertEndChild(newElement);
+ if (pNewNode)
+ {
+ TiXmlText value(strValue);
+ pNewNode->InsertEndChild(value);
+ }
+}
+
+void XMLUtils::SetDate(TiXmlNode* pRootNode, const char *strTag, const CDateTime& date)
+{
+ SetString(pRootNode, strTag, date.IsValid() ? date.GetAsDBDate() : "");
+}
+
+void XMLUtils::SetDateTime(TiXmlNode* pRootNode, const char *strTag, const CDateTime& dateTime)
+{
+ SetString(pRootNode, strTag, dateTime.IsValid() ? dateTime.GetAsDBDateTime() : "");
+}
diff --git a/xbmc/utils/XMLUtils.h b/xbmc/utils/XMLUtils.h
new file mode 100644
index 0000000..fcd23bd
--- /dev/null
+++ b/xbmc/utils/XMLUtils.h
@@ -0,0 +1,95 @@
+/*
+ * 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 "utils/XBMCTinyXML.h"
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+class CDateTime;
+
+class XMLUtils
+{
+public:
+ static bool HasChild(const TiXmlNode* pRootNode, const char* strTag);
+
+ static bool GetHex(const TiXmlNode* pRootNode, const char* strTag, uint32_t& dwHexValue);
+ static bool GetUInt(const TiXmlNode* pRootNode, const char* strTag, uint32_t& dwUIntValue);
+ static bool GetLong(const TiXmlNode* pRootNode, const char* strTag, long& lLongValue);
+ static bool GetFloat(const TiXmlNode* pRootNode, const char* strTag, float& value);
+ static bool GetDouble(const TiXmlNode* pRootNode, const char* strTag, double& value);
+ static bool GetInt(const TiXmlNode* pRootNode, const char* strTag, int& iIntValue);
+ static bool GetBoolean(const TiXmlNode* pRootNode, const char* strTag, bool& bBoolValue);
+
+ /*! \brief Get a string value from the xml tag
+ If the specified tag isn't found strStringvalue is not modified and will contain whatever
+ value it had before the method call.
+
+ \param[in] pRootNode the xml node that contains the tag
+ \param[in] strTag the xml tag to read from
+ \param[in,out] strStringValue where to store the read string
+ \return true on success, false if the tag isn't found
+ */
+ static bool GetString(const TiXmlNode* pRootNode, const char* strTag, std::string& strStringValue);
+
+ /*! \brief Get a string value from the xml tag
+
+ \param[in] pRootNode the xml node that contains the tag
+ \param[in] strTag the tag to read from
+
+ \return the value in the specified tag or an empty string if the tag isn't found
+ */
+ static std::string GetString(const TiXmlNode* pRootNode, const char* strTag);
+ /*! \brief Get multiple tags, concatenating the values together.
+ Transforms
+ <tag>value1</tag>
+ <tag clear="true">value2</tag>
+ ...
+ <tag>valuen</tag>
+ into value2<sep>...<sep>valuen, appending it to the value string. Note that <value1> is overwritten by the clear="true" tag.
+
+ \param rootNode the parent containing the <tag>'s.
+ \param tag the <tag> in question.
+ \param separator the separator to use when concatenating values.
+ \param value [out] the resulting string. Remains untouched if no <tag> is available, else is appended (or cleared based on the clear parameter).
+ \param clear if true, clears the string prior to adding tags, if tags are available. Defaults to false.
+ */
+ static bool GetAdditiveString(const TiXmlNode* rootNode, const char* tag, const std::string& separator, std::string& value, bool clear = false);
+ static bool GetStringArray(const TiXmlNode* rootNode, const char* tag, std::vector<std::string>& arrayValue, bool clear = false, const std::string& separator = "");
+ static bool GetPath(const TiXmlNode* pRootNode, const char* strTag, std::string& strStringValue);
+ static bool GetFloat(const TiXmlNode* pRootNode, const char* strTag, float& value, const float min, const float max);
+ static bool GetUInt(const TiXmlNode* pRootNode, const char* strTag, uint32_t& dwUIntValue, const uint32_t min, const uint32_t max);
+ static bool GetInt(const TiXmlNode* pRootNode, const char* strTag, int& iIntValue, const int min, const int max);
+ static bool GetDate(const TiXmlNode* pRootNode, const char* strTag, CDateTime& date);
+ static bool GetDateTime(const TiXmlNode* pRootNode, const char* strTag, CDateTime& dateTime);
+ /*! \brief Fetch a std::string copy of an attribute, if it exists. Cannot distinguish between empty and non-existent attributes.
+ \param element the element to query.
+ \param tag the name of the attribute.
+ \return the attribute, if it exists, else an empty string
+ */
+ static std::string GetAttribute(const TiXmlElement *element, const char *tag);
+
+ static TiXmlNode* SetString(TiXmlNode* pRootNode, const char *strTag, const std::string& strValue);
+ static void SetAdditiveString(TiXmlNode* pRootNode, const char *strTag, const std::string& strSeparator, const std::string& strValue);
+ static void SetStringArray(TiXmlNode* pRootNode, const char *strTag, const std::vector<std::string>& arrayValue);
+ static TiXmlNode* SetInt(TiXmlNode* pRootNode, const char *strTag, int value);
+ static TiXmlNode* SetFloat(TiXmlNode* pRootNode, const char *strTag, float value);
+ static TiXmlNode* SetDouble(TiXmlNode* pRootNode, const char* strTag, double value);
+ static void SetBoolean(TiXmlNode* pRootNode, const char *strTag, bool value);
+ static void SetHex(TiXmlNode* pRootNode, const char *strTag, uint32_t value);
+ static void SetPath(TiXmlNode* pRootNode, const char *strTag, const std::string& strValue);
+ static void SetLong(TiXmlNode* pRootNode, const char *strTag, long iValue);
+ static void SetDate(TiXmlNode* pRootNode, const char *strTag, const CDateTime& date);
+ static void SetDateTime(TiXmlNode* pRootNode, const char *strTag, const CDateTime& dateTime);
+
+ static const int path_version = 1;
+};
+
diff --git a/xbmc/utils/XSLTUtils.cpp b/xbmc/utils/XSLTUtils.cpp
new file mode 100644
index 0000000..ba069ba
--- /dev/null
+++ b/xbmc/utils/XSLTUtils.cpp
@@ -0,0 +1,103 @@
+/*
+ * 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 "XSLTUtils.h"
+#include "log.h"
+#include <libxslt/xslt.h>
+#include <libxslt/transform.h>
+
+#ifndef TARGET_WINDOWS
+#include <iostream>
+#endif
+
+#define TMP_BUF_SIZE 512
+void err(void *ctx, const char *msg, ...) {
+ char string[TMP_BUF_SIZE];
+ va_list arg_ptr;
+ va_start(arg_ptr, msg);
+ vsnprintf(string, TMP_BUF_SIZE, msg, arg_ptr);
+ va_end(arg_ptr);
+ CLog::Log(LOGDEBUG, "XSLT: {}", string);
+}
+
+XSLTUtils::XSLTUtils()
+{
+ // initialize libxslt
+ xmlSubstituteEntitiesDefault(1);
+ xmlLoadExtDtdDefaultValue = 0;
+ xsltSetGenericErrorFunc(NULL, err);
+}
+
+XSLTUtils::~XSLTUtils()
+{
+ if (m_xmlInput)
+ xmlFreeDoc(m_xmlInput);
+ if (m_xmlOutput)
+ xmlFreeDoc(m_xmlOutput);
+ if (m_xsltStylesheet)
+ xsltFreeStylesheet(m_xsltStylesheet);
+}
+
+bool XSLTUtils::XSLTTransform(std::string& output)
+{
+ const char *params[16+1];
+ params[0] = NULL;
+ m_xmlOutput = xsltApplyStylesheet(m_xsltStylesheet, m_xmlInput, params);
+ if (!m_xmlOutput)
+ {
+ CLog::Log(LOGDEBUG, "XSLT: xslt transformation failed");
+ return false;
+ }
+
+ xmlChar *xmlResultBuffer = NULL;
+ int xmlResultLength = 0;
+ int res = xsltSaveResultToString(&xmlResultBuffer, &xmlResultLength, m_xmlOutput, m_xsltStylesheet);
+ if (res == -1)
+ {
+ xmlFree(xmlResultBuffer);
+ return false;
+ }
+
+ output.append((const char *)xmlResultBuffer, xmlResultLength);
+ xmlFree(xmlResultBuffer);
+
+ return true;
+}
+
+bool XSLTUtils::SetInput(const std::string& input)
+{
+ m_xmlInput = xmlParseMemory(input.c_str(), input.size());
+ if (!m_xmlInput)
+ return false;
+ return true;
+}
+
+bool XSLTUtils::SetStylesheet(const std::string& stylesheet)
+{
+ if (m_xsltStylesheet) {
+ xsltFreeStylesheet(m_xsltStylesheet);
+ m_xsltStylesheet = NULL;
+ }
+
+ m_xmlStylesheet = xmlParseMemory(stylesheet.c_str(), stylesheet.size());
+ if (!m_xmlStylesheet)
+ {
+ CLog::Log(LOGDEBUG, "could not xmlParseMemory stylesheetdoc");
+ return false;
+ }
+
+ m_xsltStylesheet = xsltParseStylesheetDoc(m_xmlStylesheet);
+ if (!m_xsltStylesheet) {
+ CLog::Log(LOGDEBUG, "could not parse stylesheetdoc");
+ xmlFree(m_xmlStylesheet);
+ m_xmlStylesheet = NULL;
+ return false;
+ }
+
+ return true;
+}
diff --git a/xbmc/utils/XSLTUtils.h b/xbmc/utils/XSLTUtils.h
new file mode 100644
index 0000000..78221b9
--- /dev/null
+++ b/xbmc/utils/XSLTUtils.h
@@ -0,0 +1,51 @@
+/*
+ * 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 <string>
+
+#include <libxslt/xslt.h>
+#include <libxslt/xsltutils.h>
+
+class XSLTUtils
+{
+public:
+ XSLTUtils();
+ ~XSLTUtils();
+
+ /*! \brief Set the input XML for an XSLT transform from a string.
+ This sets up the XSLT transformer with some input XML from a string in memory.
+ The input XML should be well formed.
+ \param input the XML document to be transformed.
+ */
+ bool SetInput(const std::string& input);
+
+ /*! \brief Set the stylesheet (XSL) for an XSLT transform from a string.
+ This sets up the XSLT transformer with some stylesheet XML from a string in memory.
+ The input XSL should be well formed.
+ \param input the XSL document to be transformed.
+ */
+ bool SetStylesheet(const std::string& stylesheet);
+
+ /*! \brief Perform an XSLT transform on an inbound XML document.
+ This will apply an XSLT transformation on an input XML document,
+ giving an output XML document, using the specified XSLT document
+ as the transformer.
+ \param input the parent containing the <tag>'s.
+ \param filename the <tag> in question.
+ */
+ bool XSLTTransform(std::string& output);
+
+
+private:
+ xmlDocPtr m_xmlInput = nullptr;
+ xmlDocPtr m_xmlOutput = nullptr;
+ xmlDocPtr m_xmlStylesheet = nullptr;
+ xsltStylesheetPtr m_xsltStylesheet = nullptr;
+};
diff --git a/xbmc/utils/XTimeUtils.h b/xbmc/utils/XTimeUtils.h
new file mode 100644
index 0000000..91cda40
--- /dev/null
+++ b/xbmc/utils/XTimeUtils.h
@@ -0,0 +1,88 @@
+/*
+ * 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 <chrono>
+#include <string>
+#include <thread>
+
+#if !defined(TARGET_WINDOWS)
+#include "PlatformDefs.h"
+#else
+// This is needed, a forward declaration of FILETIME
+// breaks everything
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include <Windows.h>
+#endif
+
+namespace KODI
+{
+namespace TIME
+{
+struct SystemTime
+{
+ unsigned short year;
+ unsigned short month;
+ unsigned short dayOfWeek;
+ unsigned short day;
+ unsigned short hour;
+ unsigned short minute;
+ unsigned short second;
+ unsigned short milliseconds;
+};
+
+struct TimeZoneInformation
+{
+ long bias;
+ std::string standardName;
+ SystemTime standardDate;
+ long standardBias;
+ std::string daylightName;
+ SystemTime daylightDate;
+ long daylightBias;
+};
+
+constexpr int KODI_TIME_ZONE_ID_INVALID{-1};
+constexpr int KODI_TIME_ZONE_ID_UNKNOWN{0};
+constexpr int KODI_TIME_ZONE_ID_STANDARD{1};
+constexpr int KODI_TIME_ZONE_ID_DAYLIGHT{2};
+
+struct FileTime
+{
+ unsigned int lowDateTime;
+ unsigned int highDateTime;
+};
+
+void GetLocalTime(SystemTime* systemTime);
+uint32_t GetTimeZoneInformation(TimeZoneInformation* timeZoneInformation);
+
+template<typename Rep, typename Period>
+void Sleep(std::chrono::duration<Rep, Period> duration)
+{
+ if (duration == std::chrono::duration<Rep, Period>::zero())
+ {
+ std::this_thread::yield();
+ return;
+ }
+
+ std::this_thread::sleep_for(duration);
+}
+
+int FileTimeToLocalFileTime(const FileTime* fileTime, FileTime* localFileTime);
+int SystemTimeToFileTime(const SystemTime* systemTime, FileTime* fileTime);
+long CompareFileTime(const FileTime* fileTime1, const FileTime* fileTime2);
+int FileTimeToSystemTime(const FileTime* fileTime, SystemTime* systemTime);
+int LocalFileTimeToFileTime(const FileTime* LocalFileTime, FileTime* fileTime);
+
+int FileTimeToTimeT(const FileTime* localFileTime, time_t* pTimeT);
+int TimeTToFileTime(time_t timeT, FileTime* localFileTime);
+} // namespace TIME
+} // namespace KODI
diff --git a/xbmc/utils/log.cpp b/xbmc/utils/log.cpp
new file mode 100644
index 0000000..da5771e
--- /dev/null
+++ b/xbmc/utils/log.cpp
@@ -0,0 +1,302 @@
+/*
+ * 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 "log.h"
+
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/SettingUtils.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <cstring>
+#include <set>
+
+#include <spdlog/sinks/basic_file_sink.h>
+#include <spdlog/sinks/dist_sink.h>
+#include <spdlog/sinks/dup_filter_sink.h>
+
+namespace
+{
+static constexpr unsigned char Utf8Bom[3] = {0xEF, 0xBB, 0xBF};
+static const std::string LogFileExtension = ".log";
+static const std::string LogPattern = "%Y-%m-%d %T.%e T:%-5t %7l <%n>: %v";
+} // namespace
+
+CLog::CLog()
+ : m_platform(IPlatformLog::CreatePlatformLog()),
+ m_sinks(std::make_shared<spdlog::sinks::dist_sink_mt>()),
+ m_defaultLogger(CreateLogger("general")),
+ m_logLevel(LOG_LEVEL_DEBUG),
+ m_componentLogEnabled(false),
+ m_componentLogLevels(0)
+{
+ // add platform-specific debug sinks
+ m_platform->AddSinks(m_sinks);
+
+ // register the default logger with spdlog
+ spdlog::set_default_logger(m_defaultLogger);
+
+ // set the formatting pattern globally
+ spdlog::set_pattern(LogPattern);
+
+ // flush on debug logs
+ spdlog::flush_on(spdlog::level::debug);
+
+ // set the log level
+ SetLogLevel(m_logLevel);
+}
+
+CLog::~CLog()
+{
+ spdlog::drop("general");
+}
+
+void CLog::OnSettingsLoaded()
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ m_componentLogEnabled = settings->GetBool(CSettings::SETTING_DEBUG_EXTRALOGGING);
+ SetComponentLogLevel(settings->GetList(CSettings::SETTING_DEBUG_SETEXTRALOGLEVEL));
+}
+
+void CLog::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_DEBUG_EXTRALOGGING)
+ m_componentLogEnabled = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ else if (settingId == CSettings::SETTING_DEBUG_SETEXTRALOGLEVEL)
+ SetComponentLogLevel(
+ CSettingUtils::GetList(std::static_pointer_cast<const CSettingList>(setting)));
+}
+
+void CLog::Initialize(const std::string& path)
+{
+ if (m_fileSink != nullptr)
+ return;
+
+ // register setting callbacks
+ auto settingsManager =
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSettingsManager();
+ settingsManager->RegisterSettingOptionsFiller("loggingcomponents",
+ SettingOptionsLoggingComponentsFiller);
+ settingsManager->RegisterSettingsHandler(this);
+ std::set<std::string> settingSet;
+ settingSet.insert(CSettings::SETTING_DEBUG_EXTRALOGGING);
+ settingSet.insert(CSettings::SETTING_DEBUG_SETEXTRALOGLEVEL);
+ settingsManager->RegisterCallback(this, settingSet);
+
+ if (path.empty())
+ return;
+
+ // put together the path to the log file(s)
+ std::string appName = CCompileInfo::GetAppName();
+ StringUtils::ToLower(appName);
+ const std::string filePathBase = URIUtils::AddFileToFolder(path, appName);
+ const std::string filePath = filePathBase + LogFileExtension;
+ const std::string oldFilePath = filePathBase + ".old" + LogFileExtension;
+
+ // handle old.log by deleting an existing old.log and renaming the last log to old.log
+ XFILE::CFile::Delete(oldFilePath);
+ XFILE::CFile::Rename(filePath, oldFilePath);
+
+ // write UTF-8 BOM
+ {
+ XFILE::CFile file;
+ if (file.OpenForWrite(filePath, true))
+ file.Write(Utf8Bom, sizeof(Utf8Bom));
+ }
+
+ // create the file sink within a duplicate filter sink
+ auto duplicateFilterSink =
+ std::make_shared<spdlog::sinks::dup_filter_sink_st>(std::chrono::seconds(10));
+ auto basicFileSink = std::make_shared<spdlog::sinks::basic_file_sink_st>(
+ m_platform->GetLogFilename(filePath), false);
+ basicFileSink->set_pattern(LogPattern);
+ duplicateFilterSink->add_sink(basicFileSink);
+ m_fileSink = duplicateFilterSink;
+
+ // add it to the existing sinks
+ m_sinks->add_sink(m_fileSink);
+}
+
+void CLog::UnregisterFromSettings()
+{
+ // unregister setting callbacks
+ auto settingsManager =
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSettingsManager();
+ settingsManager->UnregisterSettingOptionsFiller("loggingcomponents");
+ settingsManager->UnregisterSettingsHandler(this);
+ settingsManager->UnregisterCallback(this);
+}
+
+void CLog::Deinitialize()
+{
+ if (m_fileSink == nullptr)
+ return;
+
+ // flush all loggers
+ spdlog::apply_all([](const std::shared_ptr<spdlog::logger>& logger) { logger->flush(); });
+
+ // flush the file sink
+ m_fileSink->flush();
+
+ // remove and destroy the file sink
+ m_sinks->remove_sink(m_fileSink);
+ m_fileSink.reset();
+}
+
+void CLog::SetLogLevel(int level)
+{
+ if (level < LOG_LEVEL_NONE || level > LOG_LEVEL_MAX)
+ return;
+
+ m_logLevel = level;
+
+ auto spdLevel = spdlog::level::info;
+ if (level <= LOG_LEVEL_NONE)
+ spdLevel = spdlog::level::off;
+ else if (level >= LOG_LEVEL_DEBUG)
+ spdLevel = spdlog::level::trace;
+
+ if (m_defaultLogger != nullptr && m_defaultLogger->level() == spdLevel)
+ return;
+
+ spdlog::set_level(spdLevel);
+ FormatAndLogInternal(spdlog::level::info, "Log level changed to \"{}\"",
+ spdlog::level::to_string_view(spdLevel));
+}
+
+bool CLog::IsLogLevelLogged(int loglevel)
+{
+ if (m_logLevel >= LOG_LEVEL_DEBUG)
+ return true;
+ if (m_logLevel <= LOG_LEVEL_NONE)
+ return false;
+
+ return (loglevel & LOGMASK) >= LOGINFO;
+}
+
+bool CLog::CanLogComponent(uint32_t component) const
+{
+ if (!m_componentLogEnabled || component == 0)
+ return false;
+
+ return ((m_componentLogLevels & component) == component);
+}
+
+void CLog::SettingOptionsLoggingComponentsFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(669), LOGSAMBA);
+ list.emplace_back(g_localizeStrings.Get(670), LOGCURL);
+ list.emplace_back(g_localizeStrings.Get(672), LOGFFMPEG);
+ list.emplace_back(g_localizeStrings.Get(675), LOGJSONRPC);
+ list.emplace_back(g_localizeStrings.Get(676), LOGAUDIO);
+ list.emplace_back(g_localizeStrings.Get(680), LOGVIDEO);
+ list.emplace_back(g_localizeStrings.Get(683), LOGAVTIMING);
+ list.emplace_back(g_localizeStrings.Get(684), LOGWINDOWING);
+ list.emplace_back(g_localizeStrings.Get(685), LOGPVR);
+ list.emplace_back(g_localizeStrings.Get(686), LOGEPG);
+ list.emplace_back(g_localizeStrings.Get(39117), LOGANNOUNCE);
+#ifdef HAS_DBUS
+ list.emplace_back(g_localizeStrings.Get(674), LOGDBUS);
+#endif
+#ifdef HAS_WEB_SERVER
+ list.emplace_back(g_localizeStrings.Get(681), LOGWEBSERVER);
+#endif
+#ifdef HAS_AIRTUNES
+ list.emplace_back(g_localizeStrings.Get(677), LOGAIRTUNES);
+#endif
+#ifdef HAS_UPNP
+ list.emplace_back(g_localizeStrings.Get(678), LOGUPNP);
+#endif
+#ifdef HAVE_LIBCEC
+ list.emplace_back(g_localizeStrings.Get(679), LOGCEC);
+#endif
+ list.emplace_back(g_localizeStrings.Get(682), LOGDATABASE);
+#if defined(HAS_FILESYSTEM_SMB)
+ list.emplace_back(g_localizeStrings.Get(37050), LOGWSDISCOVERY);
+#endif
+}
+
+Logger CLog::GetLogger(const std::string& loggerName)
+{
+ auto logger = spdlog::get(loggerName);
+ if (logger == nullptr)
+ logger = CreateLogger(loggerName);
+
+ return logger;
+}
+
+CLog& CLog::GetInstance()
+{
+ return CServiceBroker::GetLogging();
+}
+
+spdlog::level::level_enum CLog::MapLogLevel(int level)
+{
+ switch (level)
+ {
+ case LOGDEBUG:
+ return spdlog::level::debug;
+ case LOGINFO:
+ return spdlog::level::info;
+ case LOGWARNING:
+ return spdlog::level::warn;
+ case LOGERROR:
+ return spdlog::level::err;
+ case LOGFATAL:
+ return spdlog::level::critical;
+ case LOGNONE:
+ return spdlog::level::off;
+
+ default:
+ break;
+ }
+
+ return spdlog::level::info;
+}
+
+Logger CLog::CreateLogger(const std::string& loggerName)
+{
+ // create the logger
+ auto logger = std::make_shared<spdlog::logger>(loggerName, m_sinks);
+
+ // initialize the logger
+ spdlog::initialize_logger(logger);
+
+ return logger;
+}
+
+void CLog::SetComponentLogLevel(const std::vector<CVariant>& components)
+{
+ m_componentLogLevels = 0;
+ for (const auto& component : components)
+ {
+ if (!component.isInteger())
+ continue;
+
+ m_componentLogLevels |= static_cast<uint32_t>(component.asInteger());
+ }
+}
+
+void CLog::FormatLineBreaks(std::string& message)
+{
+ StringUtils::Replace(message, "\n", "\n ");
+}
diff --git a/xbmc/utils/log.h b/xbmc/utils/log.h
new file mode 100644
index 0000000..1c42c88
--- /dev/null
+++ b/xbmc/utils/log.h
@@ -0,0 +1,165 @@
+/*
+ * 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
+
+// spdlog specific defines
+// clang-format off
+#include <string_view>
+#define SPDLOG_LEVEL_NAMES \
+{ \
+ std::string_view{"TRACE"}, \
+ std::string_view{"DEBUG"}, \
+ std::string_view{"INFO"}, \
+ std::string_view{"WARNING"}, \
+ std::string_view{"ERROR"}, \
+ std::string_view{"FATAL"}, \
+ std::string_view{"OFF"} \
+};
+// clang-format on
+
+#include "commons/ilog.h"
+#include "settings/lib/ISettingCallback.h"
+#include "settings/lib/ISettingsHandler.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "utils/IPlatformLog.h"
+#include "utils/logtypes.h"
+
+#include <string>
+#include <vector>
+
+#include <spdlog/spdlog.h>
+
+namespace spdlog
+{
+namespace sinks
+{
+class sink;
+
+template<typename Mutex>
+class dist_sink;
+} // namespace sinks
+} // namespace spdlog
+
+#if FMT_VERSION >= 100000
+using fmt::enums::format_as;
+
+namespace fmt
+{
+template<typename T, typename Char>
+struct formatter<std::atomic<T>, Char> : formatter<T, Char>
+{
+};
+} // namespace fmt
+#endif
+
+class CLog : public ISettingsHandler, public ISettingCallback
+{
+public:
+ CLog();
+ ~CLog();
+
+ // implementation of ISettingsHandler
+ void OnSettingsLoaded() override;
+
+ // implementation of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ void Initialize(const std::string& path);
+ void UnregisterFromSettings();
+ void Deinitialize();
+
+ void SetLogLevel(int level);
+ int GetLogLevel() { return m_logLevel; }
+ bool IsLogLevelLogged(int loglevel);
+
+ bool CanLogComponent(uint32_t component) const;
+ static void SettingOptionsLoggingComponentsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ Logger GetLogger(const std::string& loggerName);
+
+ template<typename... Args>
+ static inline void Log(int level, const std::string_view& format, Args&&... args)
+ {
+ Log(MapLogLevel(level), format, std::forward<Args>(args)...);
+ }
+
+ template<typename... Args>
+ static inline void Log(int level,
+ uint32_t component,
+ const std::string_view& format,
+ Args&&... args)
+ {
+ if (!GetInstance().CanLogComponent(component))
+ return;
+
+ Log(level, format, std::forward<Args>(args)...);
+ }
+
+ template<typename... Args>
+ static inline void Log(spdlog::level::level_enum level,
+ const std::string_view& format,
+ Args&&... args)
+ {
+ GetInstance().FormatAndLogInternal(level, format, std::forward<Args>(args)...);
+ }
+
+ template<typename... Args>
+ static inline void Log(spdlog::level::level_enum level,
+ uint32_t component,
+ const std::string_view& format,
+ Args&&... args)
+ {
+ if (!GetInstance().CanLogComponent(component))
+ return;
+
+ Log(level, format, std::forward<Args>(args)...);
+ }
+
+#define LogF(level, format, ...) Log((level), ("{}: " format), __FUNCTION__, ##__VA_ARGS__)
+#define LogFC(level, component, format, ...) \
+ Log((level), (component), ("{}: " format), __FUNCTION__, ##__VA_ARGS__)
+
+private:
+ static CLog& GetInstance();
+
+ static spdlog::level::level_enum MapLogLevel(int level);
+
+ template<typename... Args>
+ inline void FormatAndLogInternal(spdlog::level::level_enum level,
+ const std::string_view& format,
+ Args&&... args)
+ {
+ auto message = fmt::format(format, std::forward<Args>(args)...);
+
+ // fixup newline alignment, number of spaces should equal prefix length
+ FormatLineBreaks(message);
+
+ m_defaultLogger->log(level, message);
+ }
+
+ Logger CreateLogger(const std::string& loggerName);
+
+ void SetComponentLogLevel(const std::vector<CVariant>& components);
+
+ void FormatLineBreaks(std::string& message);
+
+ std::unique_ptr<IPlatformLog> m_platform;
+ std::shared_ptr<spdlog::sinks::dist_sink<std::mutex>> m_sinks;
+ Logger m_defaultLogger;
+
+ std::shared_ptr<spdlog::sinks::sink> m_fileSink;
+
+ int m_logLevel;
+
+ bool m_componentLogEnabled;
+ uint32_t m_componentLogLevels;
+};
diff --git a/xbmc/utils/logtypes.h b/xbmc/utils/logtypes.h
new file mode 100644
index 0000000..f41aa7e
--- /dev/null
+++ b/xbmc/utils/logtypes.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2020 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 <memory>
+
+namespace spdlog
+{
+class logger;
+}
+
+using Logger = std::shared_ptr<spdlog::logger>;
diff --git a/xbmc/utils/params_check_macros.h b/xbmc/utils/params_check_macros.h
new file mode 100644
index 0000000..550e229
--- /dev/null
+++ b/xbmc/utils/params_check_macros.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2014-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
+
+// macros for gcc, clang & others
+#ifndef PARAM1_PRINTF_FORMAT
+#ifdef __GNUC__
+// for use in functions that take printf format string as first parameter and additional printf parameters as second parameter
+// for example: int myprintf(const char* format, ...) PARAM1_PRINTF_FORMAT;
+#define PARAM1_PRINTF_FORMAT __attribute__((format(printf,1,2)))
+
+// for use in functions that take printf format string as second parameter and additional printf parameters as third parameter
+// for example: bool log_string(int logLevel, const char* format, ...) PARAM2_PRINTF_FORMAT;
+// note: all non-static class member functions take pointer to class object as hidden first parameter
+#define PARAM2_PRINTF_FORMAT __attribute__((format(printf,2,3)))
+
+// for use in functions that take printf format string as third parameter and additional printf parameters as fourth parameter
+// note: all non-static class member functions take pointer to class object as hidden first parameter
+// for example: class A { bool log_string(int logLevel, const char* functionName, const char* format, ...) PARAM3_PRINTF_FORMAT; }
+#define PARAM3_PRINTF_FORMAT __attribute__((format(printf,3,4)))
+
+// for use in functions that take printf format string as fourth parameter and additional printf parameters as fifth parameter
+// note: all non-static class member functions take pointer to class object as hidden first parameter
+// for example: class A { bool log_string(int logLevel, const char* functionName, int component, const char* format, ...) PARAM4_PRINTF_FORMAT; }
+#define PARAM4_PRINTF_FORMAT __attribute__((format(printf,4,5)))
+#else // ! __GNUC__
+#define PARAM1_PRINTF_FORMAT
+#define PARAM2_PRINTF_FORMAT
+#define PARAM3_PRINTF_FORMAT
+#define PARAM4_PRINTF_FORMAT
+#endif // ! __GNUC__
+#endif // PARAM1_PRINTF_FORMAT
+
+// macros for VC
+// VC check parameters only when "Code Analysis" is called
+#ifndef PRINTF_FORMAT_STRING
+#ifdef _MSC_VER
+#include <sal.h>
+
+// for use in any function that take printf format string and parameters
+// for example: bool log_string(int logLevel, PRINTF_FORMAT_STRING const char* format, ...);
+#define PRINTF_FORMAT_STRING _In_z_ _Printf_format_string_
+
+// specify that parameter must be zero-terminated string
+// for example: void SetName(IN_STRING const char* newName);
+#define IN_STRING _In_z_
+
+// specify that parameter must be zero-terminated string or NULL
+// for example: bool SetAdditionalName(IN_OPT_STRING const char* addName);
+#define IN_OPT_STRING _In_opt_z_
+#else // ! _MSC_VER
+#define PRINTF_FORMAT_STRING
+#define IN_STRING
+#define IN_OPT_STRING
+#endif // ! _MSC_VER
+#endif // PRINTF_FORMAT_STRING
diff --git a/xbmc/utils/rfft.cpp b/xbmc/utils/rfft.cpp
new file mode 100644
index 0000000..e0ae383
--- /dev/null
+++ b/xbmc/utils/rfft.cpp
@@ -0,0 +1,73 @@
+/*
+ * 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 "rfft.h"
+
+#if defined(TARGET_WINDOWS) && !defined(_USE_MATH_DEFINES)
+#define _USE_MATH_DEFINES
+#endif
+#include <math.h>
+
+RFFT::RFFT(int size, bool windowed) :
+ m_size(size), m_windowed(windowed)
+{
+ m_cfg = kiss_fftr_alloc(m_size,0,nullptr,nullptr);
+}
+
+RFFT::~RFFT()
+{
+ // we don' use kiss_fftr_free here because
+ // its hardcoded to free and doesn't pay attention
+ // to SIMD (which might be used during kiss_fftr_alloc
+ //in the C'tor).
+ KISS_FFT_FREE(m_cfg);
+}
+
+void RFFT::calc(const float* input, float* output)
+{
+ // temporary buffers
+ std::vector<kiss_fft_scalar> linput(m_size), rinput(m_size);
+ std::vector<kiss_fft_cpx> loutput(m_size), routput(m_size);
+
+ for (size_t i=0;i<m_size;++i)
+ {
+ linput[i] = input[2*i];
+ rinput[i] = input[2*i+1];
+ }
+
+ if (m_windowed)
+ {
+ hann(linput);
+ hann(rinput);
+ }
+
+ // transform channels
+ kiss_fftr(m_cfg, &linput[0], &loutput[0]);
+ kiss_fftr(m_cfg, &rinput[0], &routput[0]);
+
+ auto&& filter = [&](kiss_fft_cpx& data)
+ {
+ return static_cast<double>(sqrt(data.r * data.r + data.i * data.i)) * 2.0 / m_size *
+ (m_windowed ? sqrt(8.0 / 3.0) : 1.0);
+ };
+
+ // interleave while taking magnitudes and normalizing
+ for (size_t i=0;i<m_size/2;++i)
+ {
+ output[2*i] = filter(loutput[i]);
+ output[2*i+1] = filter(routput[i]);
+ }
+}
+
+#include <iostream>
+
+void RFFT::hann(std::vector<kiss_fft_scalar>& data)
+{
+ for (size_t i=0;i<data.size();++i)
+ data[i] *= 0.5f * (1.0f - cos(2.0f * static_cast<float>(M_PI) * i / (data.size() - 1)));
+}
diff --git a/xbmc/utils/rfft.h b/xbmc/utils/rfft.h
new file mode 100644
index 0000000..ce54d63
--- /dev/null
+++ b/xbmc/utils/rfft.h
@@ -0,0 +1,39 @@
+/*
+ * 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 <vector>
+
+#include <kissfft/kiss_fftr.h>
+
+//! \brief Class performing a RFFT of interleaved stereo data.
+class RFFT
+{
+public:
+ //! \brief The constructor creates a RFFT plan.
+ //! \brief size Length of time data for a single channel.
+ //! \brief windowed Whether or not to apply a Hann window to data.
+ RFFT(int size, bool windowed=false);
+
+ //! \brief Free the RFFT plan
+ ~RFFT();
+
+ //! \brief Calculate FFTs
+ //! \param input Input data of size 2*m_size
+ //! \param output Output data of size m_size.
+ void calc(const float* input, float* output);
+protected:
+ //! \brief Apply a Hann window to a buffer.
+ //! \param data Vector with data to apply window to.
+ static void hann(std::vector<kiss_fft_scalar>& data);
+
+ size_t m_size; //!< Size for a single channel.
+ bool m_windowed; //!< Whether or not a Hann window is applied.
+ kiss_fftr_cfg m_cfg; //!< FFT plan
+};
diff --git a/xbmc/utils/test/CMakeLists.txt b/xbmc/utils/test/CMakeLists.txt
new file mode 100644
index 0000000..a5ba095
--- /dev/null
+++ b/xbmc/utils/test/CMakeLists.txt
@@ -0,0 +1,51 @@
+set(SOURCES TestAlarmClock.cpp
+ TestAliasShortcutUtils.cpp
+ TestArchive.cpp
+ TestBase64.cpp
+ TestBitstreamStats.cpp
+ TestCharsetConverter.cpp
+ TestCPUInfo.cpp
+ TestComponentContainer.cpp
+ TestCrc32.cpp
+ TestDatabaseUtils.cpp
+ TestDigest.cpp
+ TestEndianSwap.cpp
+ TestExecString.cpp
+ TestFileOperationJob.cpp
+ TestFileUtils.cpp
+ TestGlobalsHandling.cpp
+ TestHTMLUtil.cpp
+ TestHttpHeader.cpp
+ TestHttpParser.cpp
+ TestHttpRangeUtils.cpp
+ TestHttpResponse.cpp
+ TestJobManager.cpp
+ TestJSONVariantParser.cpp
+ TestJSONVariantWriter.cpp
+ TestLabelFormatter.cpp
+ TestLangCodeExpander.cpp
+ TestLocale.cpp
+ Testlog.cpp
+ TestMathUtils.cpp
+ TestMime.cpp
+ TestPOUtils.cpp
+ TestRegExp.cpp
+ Testrfft.cpp
+ TestRingBuffer.cpp
+ TestScraperParser.cpp
+ TestScraperUrl.cpp
+ TestSortUtils.cpp
+ TestStopwatch.cpp
+ TestStreamDetails.cpp
+ TestStreamUtils.cpp
+ TestStringUtils.cpp
+ TestSystemInfo.cpp
+ TestURIUtils.cpp
+ TestUrlOptions.cpp
+ TestVariant.cpp
+ TestXBMCTinyXML.cpp
+ TestXMLUtils.cpp)
+
+set(HEADERS TestGlobalsHandlingPattern1.h)
+
+core_add_test_library(utils_test)
diff --git a/xbmc/utils/test/CXBMCTinyXML-test.xml b/xbmc/utils/test/CXBMCTinyXML-test.xml
new file mode 100644
index 0000000..9444dc8
--- /dev/null
+++ b/xbmc/utils/test/CXBMCTinyXML-test.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<details>
+ <url function="ParseTMDBRating" cache="tmdb-en-12244.json">
+ http://api.themoviedb.org/3/movie/12244?api_key=57983e31fb435df4df77afb854740ea9&language=en&#x3f;&#x003F;&#0063;
+ </url>
+</details>
diff --git a/xbmc/utils/test/TestAlarmClock.cpp b/xbmc/utils/test/TestAlarmClock.cpp
new file mode 100644
index 0000000..75ea84a
--- /dev/null
+++ b/xbmc/utils/test/TestAlarmClock.cpp
@@ -0,0 +1,25 @@
+/*
+ * 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 "utils/AlarmClock.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestAlarmClock, General)
+{
+ CAlarmClock a;
+ EXPECT_FALSE(a.IsRunning());
+ EXPECT_FALSE(a.HasAlarm("test"));
+ a.Start("test", 100.f, "test");
+ EXPECT_TRUE(a.IsRunning());
+ EXPECT_TRUE(a.HasAlarm("test"));
+ EXPECT_FALSE(a.HasAlarm("test2"));
+ EXPECT_NE(0.f, a.GetRemaining("test"));
+ EXPECT_EQ(0.f, a.GetRemaining("test2"));
+ a.Stop("test");
+}
diff --git a/xbmc/utils/test/TestAliasShortcutUtils.cpp b/xbmc/utils/test/TestAliasShortcutUtils.cpp
new file mode 100644
index 0000000..d36fd41
--- /dev/null
+++ b/xbmc/utils/test/TestAliasShortcutUtils.cpp
@@ -0,0 +1,91 @@
+/*
+ * 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 "utils/AliasShortcutUtils.h"
+#include "filesystem/File.h"
+#include "test/TestUtils.h"
+
+#if defined(TARGET_DARWIN_OSX)
+#include "platform/darwin/DarwinUtils.h"
+#endif
+#include <gtest/gtest.h>
+
+TEST(TestAliasShortcutUtils, IsAliasShortcut)
+{
+ XFILE::CFile *tmpFile = XBMC_CREATETEMPFILE("noaliastest");
+ std::string noalias = XBMC_TEMPFILEPATH(tmpFile);
+
+#if defined(TARGET_DARWIN_OSX)
+ XFILE::CFile *aliasDestFile = XBMC_CREATETEMPFILE("aliastest");
+ std::string alias = XBMC_TEMPFILEPATH(aliasDestFile);
+
+ //we only need the path here so delete the alias file
+ //which will be recreated as shortcut later:
+ XBMC_DELETETEMPFILE(aliasDestFile);
+
+ // create alias from a pointing to /Volumes
+ CDarwinUtils::CreateAliasShortcut(alias, "/Volumes");
+ EXPECT_TRUE(IsAliasShortcut(alias, true));
+ XFILE::CFile::Delete(alias);
+
+ // volumes is not a shortcut but a dir
+ EXPECT_FALSE(IsAliasShortcut("/Volumes", true));
+#endif
+
+ // a regular file is not a shortcut
+ EXPECT_FALSE(IsAliasShortcut(noalias, false));
+ XBMC_DELETETEMPFILE(tmpFile);
+
+ // empty string is not an alias
+ std::string emptyString;
+ EXPECT_FALSE(IsAliasShortcut(emptyString, false));
+
+ // non-existent file is no alias
+ std::string nonExistingFile="/IDontExistsNormally/somefile.txt";
+ EXPECT_FALSE(IsAliasShortcut(nonExistingFile, false));
+}
+
+TEST(TestAliasShortcutUtils, TranslateAliasShortcut)
+{
+ XFILE::CFile *tmpFile = XBMC_CREATETEMPFILE("noaliastest");
+ std::string noalias = XBMC_TEMPFILEPATH(tmpFile);
+ std::string noaliastemp = noalias;
+
+#if defined(TARGET_DARWIN_OSX)
+ XFILE::CFile *aliasDestFile = XBMC_CREATETEMPFILE("aliastest");
+ std::string alias = XBMC_TEMPFILEPATH(aliasDestFile);
+
+ //we only need the path here so delete the alias file
+ //which will be recreated as shortcut later:
+ XBMC_DELETETEMPFILE(aliasDestFile);
+
+ // create alias from a pointing to /Volumes
+ CDarwinUtils::CreateAliasShortcut(alias, "/Volumes");
+
+ // resolve the shortcut
+ TranslateAliasShortcut(alias);
+ EXPECT_STREQ("/Volumes", alias.c_str());
+ XFILE::CFile::Delete(alias);
+#endif
+
+ // translating a non-shortcut url should result in no change...
+ TranslateAliasShortcut(noaliastemp);
+ EXPECT_STREQ(noaliastemp.c_str(), noalias.c_str());
+ XBMC_DELETETEMPFILE(tmpFile);
+
+ //translate empty should stay empty
+ std::string emptyString;
+ TranslateAliasShortcut(emptyString);
+ EXPECT_STREQ("", emptyString.c_str());
+
+ // translate non-existent file should result in no change...
+ std::string nonExistingFile="/IDontExistsNormally/somefile.txt";
+ std::string resolvedNonExistingFile=nonExistingFile;
+ TranslateAliasShortcut(resolvedNonExistingFile);
+ EXPECT_STREQ(resolvedNonExistingFile.c_str(), nonExistingFile.c_str());
+}
diff --git a/xbmc/utils/test/TestArchive.cpp b/xbmc/utils/test/TestArchive.cpp
new file mode 100644
index 0000000..90628ea
--- /dev/null
+++ b/xbmc/utils/test/TestArchive.cpp
@@ -0,0 +1,411 @@
+/*
+ * 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.
+ */
+
+#if defined(TARGET_WINDOWS)
+# include <windows.h>
+#endif
+
+#include "filesystem/File.h"
+#include "test/TestUtils.h"
+#include "utils/Archive.h"
+#include "utils/Variant.h"
+#include "utils/XTimeUtils.h"
+
+#include <gtest/gtest.h>
+
+class TestArchive : public testing::Test
+{
+protected:
+ TestArchive()
+ {
+ file = XBMC_CREATETEMPFILE(".ar");
+ }
+ ~TestArchive() override
+ {
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(file));
+ }
+ XFILE::CFile *file;
+};
+
+TEST_F(TestArchive, IsStoring)
+{
+ ASSERT_NE(nullptr, file);
+ CArchive arstore(file, CArchive::store);
+ EXPECT_TRUE(arstore.IsStoring());
+ EXPECT_FALSE(arstore.IsLoading());
+ arstore.Close();
+}
+
+TEST_F(TestArchive, IsLoading)
+{
+ ASSERT_NE(nullptr, file);
+ CArchive arload(file, CArchive::load);
+ EXPECT_TRUE(arload.IsLoading());
+ EXPECT_FALSE(arload.IsStoring());
+ arload.Close();
+}
+
+TEST_F(TestArchive, FloatArchive)
+{
+ ASSERT_NE(nullptr, file);
+ float float_ref = 1, float_var = 0;
+
+ CArchive arstore(file, CArchive::store);
+ arstore << float_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> float_var;
+ arload.Close();
+
+ EXPECT_EQ(float_ref, float_var);
+}
+
+TEST_F(TestArchive, DoubleArchive)
+{
+ ASSERT_NE(nullptr, file);
+ double double_ref = 2, double_var = 0;
+
+ CArchive arstore(file, CArchive::store);
+ arstore << double_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> double_var;
+ arload.Close();
+
+ EXPECT_EQ(double_ref, double_var);
+}
+
+TEST_F(TestArchive, IntegerArchive)
+{
+ ASSERT_NE(nullptr, file);
+ int int_ref = 3, int_var = 0;
+
+ CArchive arstore(file, CArchive::store);
+ arstore << int_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> int_var;
+ arload.Close();
+
+ EXPECT_EQ(int_ref, int_var);
+}
+
+TEST_F(TestArchive, UnsignedIntegerArchive)
+{
+ ASSERT_NE(nullptr, file);
+ unsigned int unsigned_int_ref = 4, unsigned_int_var = 0;
+
+ CArchive arstore(file, CArchive::store);
+ arstore << unsigned_int_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> unsigned_int_var;
+ arload.Close();
+
+ EXPECT_EQ(unsigned_int_ref, unsigned_int_var);
+}
+
+TEST_F(TestArchive, Int64tArchive)
+{
+ ASSERT_NE(nullptr, file);
+ int64_t int64_t_ref = 5, int64_t_var = 0;
+
+ CArchive arstore(file, CArchive::store);
+ arstore << int64_t_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> int64_t_var;
+ arload.Close();
+
+ EXPECT_EQ(int64_t_ref, int64_t_var);
+}
+
+TEST_F(TestArchive, UInt64tArchive)
+{
+ ASSERT_NE(nullptr, file);
+ uint64_t uint64_t_ref = 6, uint64_t_var = 0;
+
+ CArchive arstore(file, CArchive::store);
+ arstore << uint64_t_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> uint64_t_var;
+ arload.Close();
+
+ EXPECT_EQ(uint64_t_ref, uint64_t_var);
+}
+
+TEST_F(TestArchive, BoolArchive)
+{
+ ASSERT_NE(nullptr, file);
+ bool bool_ref = true, bool_var = false;
+
+ CArchive arstore(file, CArchive::store);
+ arstore << bool_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> bool_var;
+ arload.Close();
+
+ EXPECT_EQ(bool_ref, bool_var);
+}
+
+TEST_F(TestArchive, CharArchive)
+{
+ ASSERT_NE(nullptr, file);
+ char char_ref = 'A', char_var = '\0';
+
+ CArchive arstore(file, CArchive::store);
+ arstore << char_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> char_var;
+ arload.Close();
+
+ EXPECT_EQ(char_ref, char_var);
+}
+
+TEST_F(TestArchive, WStringArchive)
+{
+ ASSERT_NE(nullptr, file);
+ std::wstring wstring_ref = L"test wstring", wstring_var;
+
+ CArchive arstore(file, CArchive::store);
+ arstore << wstring_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> wstring_var;
+ arload.Close();
+
+ EXPECT_STREQ(wstring_ref.c_str(), wstring_var.c_str());
+}
+
+TEST_F(TestArchive, StringArchive)
+{
+ ASSERT_NE(nullptr, file);
+ std::string string_ref = "test string", string_var;
+
+ CArchive arstore(file, CArchive::store);
+ arstore << string_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> string_var;
+ arload.Close();
+
+ EXPECT_STREQ(string_ref.c_str(), string_var.c_str());
+}
+
+TEST_F(TestArchive, SystemTimeArchive)
+{
+ ASSERT_NE(nullptr, file);
+ KODI::TIME::SystemTime SystemTime_ref = {1, 2, 3, 4, 5, 6, 7, 8};
+ KODI::TIME::SystemTime SystemTime_var = {0, 0, 0, 0, 0, 0, 0, 0};
+
+ CArchive arstore(file, CArchive::store);
+ arstore << SystemTime_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> SystemTime_var;
+ arload.Close();
+
+ EXPECT_TRUE(!memcmp(&SystemTime_ref, &SystemTime_var, sizeof(KODI::TIME::SystemTime)));
+}
+
+TEST_F(TestArchive, CVariantArchive)
+{
+ ASSERT_NE(nullptr, file);
+ CVariant CVariant_ref((int)1), CVariant_var;
+
+ CArchive arstore(file, CArchive::store);
+ arstore << CVariant_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> CVariant_var;
+ arload.Close();
+
+ EXPECT_TRUE(CVariant_var.isInteger());
+ EXPECT_EQ(1, CVariant_var.asInteger());
+}
+
+TEST_F(TestArchive, CVariantArchiveString)
+{
+ ASSERT_NE(nullptr, file);
+ CVariant CVariant_ref("teststring"), CVariant_var;
+
+ CArchive arstore(file, CArchive::store);
+ arstore << CVariant_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> CVariant_var;
+ arload.Close();
+
+ EXPECT_TRUE(CVariant_var.isString());
+ EXPECT_STREQ("teststring", CVariant_var.asString().c_str());
+}
+
+TEST_F(TestArchive, StringVectorArchive)
+{
+ ASSERT_NE(nullptr, file);
+ std::vector<std::string> strArray_ref, strArray_var;
+ strArray_ref.emplace_back("test strArray_ref 0");
+ strArray_ref.emplace_back("test strArray_ref 1");
+ strArray_ref.emplace_back("test strArray_ref 2");
+ strArray_ref.emplace_back("test strArray_ref 3");
+
+ CArchive arstore(file, CArchive::store);
+ arstore << strArray_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> strArray_var;
+ arload.Close();
+
+ EXPECT_STREQ("test strArray_ref 0", strArray_var.at(0).c_str());
+ EXPECT_STREQ("test strArray_ref 1", strArray_var.at(1).c_str());
+ EXPECT_STREQ("test strArray_ref 2", strArray_var.at(2).c_str());
+ EXPECT_STREQ("test strArray_ref 3", strArray_var.at(3).c_str());
+}
+
+TEST_F(TestArchive, IntegerVectorArchive)
+{
+ ASSERT_NE(nullptr, file);
+ std::vector<int> iArray_ref, iArray_var;
+ iArray_ref.push_back(0);
+ iArray_ref.push_back(1);
+ iArray_ref.push_back(2);
+ iArray_ref.push_back(3);
+
+ CArchive arstore(file, CArchive::store);
+ arstore << iArray_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ arload >> iArray_var;
+ arload.Close();
+
+ EXPECT_EQ(0, iArray_var.at(0));
+ EXPECT_EQ(1, iArray_var.at(1));
+ EXPECT_EQ(2, iArray_var.at(2));
+ EXPECT_EQ(3, iArray_var.at(3));
+}
+
+TEST_F(TestArchive, MultiTypeArchive)
+{
+ ASSERT_NE(nullptr, file);
+ float float_ref = 1, float_var = 0;
+ double double_ref = 2, double_var = 0;
+ int int_ref = 3, int_var = 0;
+ unsigned int unsigned_int_ref = 4, unsigned_int_var = 0;
+ int64_t int64_t_ref = 5, int64_t_var = 0;
+ uint64_t uint64_t_ref = 6, uint64_t_var = 0;
+ bool bool_ref = true, bool_var = false;
+ char char_ref = 'A', char_var = '\0';
+ std::string string_ref = "test string", string_var;
+ std::wstring wstring_ref = L"test wstring", wstring_var;
+ KODI::TIME::SystemTime SystemTime_ref = {1, 2, 3, 4, 5, 6, 7, 8};
+ KODI::TIME::SystemTime SystemTime_var = {0, 0, 0, 0, 0, 0, 0, 0};
+ CVariant CVariant_ref((int)1), CVariant_var;
+ std::vector<std::string> strArray_ref, strArray_var;
+ strArray_ref.emplace_back("test strArray_ref 0");
+ strArray_ref.emplace_back("test strArray_ref 1");
+ strArray_ref.emplace_back("test strArray_ref 2");
+ strArray_ref.emplace_back("test strArray_ref 3");
+ std::vector<int> iArray_ref, iArray_var;
+ iArray_ref.push_back(0);
+ iArray_ref.push_back(1);
+ iArray_ref.push_back(2);
+ iArray_ref.push_back(3);
+
+ CArchive arstore(file, CArchive::store);
+ EXPECT_TRUE(arstore.IsStoring());
+ EXPECT_FALSE(arstore.IsLoading());
+ arstore << float_ref;
+ arstore << double_ref;
+ arstore << int_ref;
+ arstore << unsigned_int_ref;
+ arstore << int64_t_ref;
+ arstore << uint64_t_ref;
+ arstore << bool_ref;
+ arstore << char_ref;
+ arstore << string_ref;
+ arstore << wstring_ref;
+ arstore << SystemTime_ref;
+ arstore << CVariant_ref;
+ arstore << strArray_ref;
+ arstore << iArray_ref;
+ arstore.Close();
+
+ ASSERT_EQ(0, file->Seek(0, SEEK_SET));
+ CArchive arload(file, CArchive::load);
+ EXPECT_TRUE(arload.IsLoading());
+ EXPECT_FALSE(arload.IsStoring());
+ arload >> float_var;
+ arload >> double_var;
+ arload >> int_var;
+ arload >> unsigned_int_var;
+ arload >> int64_t_var;
+ arload >> uint64_t_var;
+ arload >> bool_var;
+ arload >> char_var;
+ arload >> string_var;
+ arload >> wstring_var;
+ arload >> SystemTime_var;
+ arload >> CVariant_var;
+ arload >> strArray_var;
+ arload >> iArray_var;
+ arload.Close();
+
+ EXPECT_EQ(float_ref, float_var);
+ EXPECT_EQ(double_ref, double_var);
+ EXPECT_EQ(int_ref, int_var);
+ EXPECT_EQ(unsigned_int_ref, unsigned_int_var);
+ EXPECT_EQ(int64_t_ref, int64_t_var);
+ EXPECT_EQ(uint64_t_ref, uint64_t_var);
+ EXPECT_EQ(bool_ref, bool_var);
+ EXPECT_EQ(char_ref, char_var);
+ EXPECT_STREQ(string_ref.c_str(), string_var.c_str());
+ EXPECT_STREQ(wstring_ref.c_str(), wstring_var.c_str());
+ EXPECT_TRUE(!memcmp(&SystemTime_ref, &SystemTime_var, sizeof(KODI::TIME::SystemTime)));
+ EXPECT_TRUE(CVariant_var.isInteger());
+ EXPECT_STREQ("test strArray_ref 0", strArray_var.at(0).c_str());
+ EXPECT_STREQ("test strArray_ref 1", strArray_var.at(1).c_str());
+ EXPECT_STREQ("test strArray_ref 2", strArray_var.at(2).c_str());
+ EXPECT_STREQ("test strArray_ref 3", strArray_var.at(3).c_str());
+ EXPECT_EQ(0, iArray_var.at(0));
+ EXPECT_EQ(1, iArray_var.at(1));
+ EXPECT_EQ(2, iArray_var.at(2));
+ EXPECT_EQ(3, iArray_var.at(3));
+}
diff --git a/xbmc/utils/test/TestBase64.cpp b/xbmc/utils/test/TestBase64.cpp
new file mode 100644
index 0000000..8416378
--- /dev/null
+++ b/xbmc/utils/test/TestBase64.cpp
@@ -0,0 +1,77 @@
+/*
+ * 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 "utils/Base64.h"
+
+#include <gtest/gtest.h>
+
+static const char refdata[] = "\x01\x02\x03\x04\x05\x06\x07\x08"
+ "\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
+ "\x11\x12\x13\x14\x15\x16\x17\x18"
+ "\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
+ "\x21\x22\x23\x24\x25\x26\x27\x28"
+ "\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30";
+
+static const char refbase64data[] = "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcY"
+ "GRobHB0eHyAhIiMkJSYnKCkqKywtLi8w";
+
+TEST(TestBase64, Encode_1)
+{
+ std::string a;
+ Base64::Encode(refdata, sizeof(refdata) - 1, a);
+ EXPECT_STREQ(refbase64data, a.c_str());
+}
+
+TEST(TestBase64, Encode_2)
+{
+ std::string a;
+ a = Base64::Encode(refdata, sizeof(refdata) - 1);
+ EXPECT_STREQ(refbase64data, a.c_str());
+}
+
+TEST(TestBase64, Encode_3)
+{
+ std::string a;
+ Base64::Encode(refdata, a);
+ EXPECT_STREQ(refbase64data, a.c_str());
+}
+
+TEST(TestBase64, Encode_4)
+{
+ std::string a;
+ a = Base64::Encode(refdata);
+ EXPECT_STREQ(refbase64data, a.c_str());
+}
+
+TEST(TestBase64, Decode_1)
+{
+ std::string a;
+ Base64::Decode(refbase64data, sizeof(refbase64data) - 1, a);
+ EXPECT_STREQ(refdata, a.c_str());
+}
+
+TEST(TestBase64, Decode_2)
+{
+ std::string a;
+ a = Base64::Decode(refbase64data, sizeof(refbase64data) - 1);
+ EXPECT_STREQ(refdata, a.c_str());
+}
+
+TEST(TestBase64, Decode_3)
+{
+ std::string a;
+ Base64::Decode(refbase64data, a);
+ EXPECT_STREQ(refdata, a.c_str());
+}
+
+TEST(TestBase64, Decode_4)
+{
+ std::string a;
+ a = Base64::Decode(refbase64data);
+ EXPECT_STREQ(refdata, a.c_str());
+}
diff --git a/xbmc/utils/test/TestBitstreamStats.cpp b/xbmc/utils/test/TestBitstreamStats.cpp
new file mode 100644
index 0000000..200a633
--- /dev/null
+++ b/xbmc/utils/test/TestBitstreamStats.cpp
@@ -0,0 +1,60 @@
+/*
+ * 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 "threads/Thread.h"
+#include "utils/BitstreamStats.h"
+
+#include <gtest/gtest.h>
+
+using namespace std::chrono_literals;
+
+#define BITS (256 * 8)
+#define BYTES (256)
+
+class CTestBitstreamStatsThread : public CThread
+{
+public:
+ CTestBitstreamStatsThread() :
+ CThread("TestBitstreamStats"){}
+
+};
+
+TEST(TestBitstreamStats, General)
+{
+ int i;
+ BitstreamStats a;
+ CTestBitstreamStatsThread t;
+
+ i = 0;
+ a.Start();
+ EXPECT_EQ(0.0, a.GetBitrate());
+ EXPECT_EQ(0.0, a.GetMaxBitrate());
+ EXPECT_EQ(-1.0, a.GetMinBitrate());
+ while (i <= BITS)
+ {
+ a.AddSampleBits(1);
+ i++;
+ t.Sleep(1ms);
+ }
+ a.CalculateBitrate();
+ EXPECT_GT(a.GetBitrate(), 0.0);
+ EXPECT_GT(a.GetMaxBitrate(), 0.0);
+ EXPECT_GT(a.GetMinBitrate(), 0.0);
+
+ i = 0;
+ while (i <= BYTES)
+ {
+ a.AddSampleBytes(1);
+ t.Sleep(2ms);
+ i++;
+ }
+ a.CalculateBitrate();
+ EXPECT_GT(a.GetBitrate(), 0.0);
+ EXPECT_GT(a.GetMaxBitrate(), 0.0);
+ EXPECT_LE(a.GetMinBitrate(), a.GetMaxBitrate());
+}
diff --git a/xbmc/utils/test/TestCPUInfo.cpp b/xbmc/utils/test/TestCPUInfo.cpp
new file mode 100644
index 0000000..bd9572a
--- /dev/null
+++ b/xbmc/utils/test/TestCPUInfo.cpp
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+#if defined(TARGET_WINDOWS)
+# include <windows.h>
+#endif
+
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CPUInfo.h"
+#include "utils/Temperature.h"
+#include "utils/XTimeUtils.h"
+
+#include <gtest/gtest.h>
+
+struct TestCPUInfo : public ::testing::Test
+{
+ TestCPUInfo() { CServiceBroker::RegisterCPUInfo(CCPUInfo::GetCPUInfo()); }
+
+ ~TestCPUInfo() { CServiceBroker::UnregisterCPUInfo(); }
+};
+
+TEST_F(TestCPUInfo, GetUsedPercentage)
+{
+ EXPECT_GE(CServiceBroker::GetCPUInfo()->GetUsedPercentage(), 0);
+}
+
+TEST_F(TestCPUInfo, GetCPUCount)
+{
+ EXPECT_GT(CServiceBroker::GetCPUInfo()->GetCPUCount(), 0);
+}
+
+TEST_F(TestCPUInfo, GetCPUFrequency)
+{
+ EXPECT_GE(CServiceBroker::GetCPUInfo()->GetCPUFrequency(), 0.f);
+}
+
+#if defined(TARGET_WINDOWS)
+TEST_F(TestCPUInfo, DISABLED_GetTemperature)
+#else
+TEST_F(TestCPUInfo, GetTemperature)
+#endif
+{
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cpuTempCmd = "echo '50 c'";
+ CTemperature t;
+ EXPECT_TRUE(CServiceBroker::GetCPUInfo()->GetTemperature(t));
+ EXPECT_TRUE(t.IsValid());
+}
+
+TEST_F(TestCPUInfo, CoreInfo)
+{
+ ASSERT_TRUE(CServiceBroker::GetCPUInfo()->HasCoreId(0));
+ const CoreInfo c = CServiceBroker::GetCPUInfo()->GetCoreInfo(0);
+ EXPECT_TRUE(c.m_id == 0);
+}
+
+TEST_F(TestCPUInfo, GetCoresUsageString)
+{
+ EXPECT_STRNE("", CServiceBroker::GetCPUInfo()->GetCoresUsageString().c_str());
+}
+
+TEST_F(TestCPUInfo, GetCPUFeatures)
+{
+ unsigned int a = CServiceBroker::GetCPUInfo()->GetCPUFeatures();
+ (void)a;
+}
diff --git a/xbmc/utils/test/TestCharsetConverter.cpp b/xbmc/utils/test/TestCharsetConverter.cpp
new file mode 100644
index 0000000..f8736b7
--- /dev/null
+++ b/xbmc/utils/test/TestCharsetConverter.cpp
@@ -0,0 +1,401 @@
+/*
+ * 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 "ServiceBroker.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CharsetConverter.h"
+#include "utils/Utf8Utils.h"
+
+#include <gtest/gtest.h>
+
+#if 0
+static const uint16_t refutf16LE1[] = { 0xff54, 0xff45, 0xff53, 0xff54,
+ 0xff3f, 0xff55, 0xff54, 0xff46,
+ 0xff11, 0xff16, 0xff2c, 0xff25,
+ 0xff54, 0xff4f, 0xff57, 0x0 };
+
+static const uint16_t refutf16LE2[] = { 0xff54, 0xff45, 0xff53, 0xff54,
+ 0xff3f, 0xff55, 0xff54, 0xff46,
+ 0xff18, 0xff34, 0xff4f, 0xff1a,
+ 0xff3f, 0xff43, 0xff48, 0xff41,
+ 0xff52, 0xff53, 0xff45, 0xff54,
+ 0xff3f, 0xff35, 0xff34, 0xff26,
+ 0xff0d, 0xff11, 0xff16, 0xff2c,
+ 0xff25, 0xff0c, 0xff3f, 0xff23,
+ 0xff33, 0xff54, 0xff44, 0xff33,
+ 0xff54, 0xff52, 0xff49, 0xff4e,
+ 0xff47, 0xff11, 0xff16, 0x0 };
+#endif
+
+static const char refutf16LE3[] = "T\377E\377S\377T\377?\377S\377T\377"
+ "R\377I\377N\377G\377#\377H\377A\377"
+ "R\377S\377E\377T\377\064\377O\377\065"
+ "\377T\377F\377\030\377";
+
+#if 0
+static const uint16_t refutf16LE4[] = { 0xff54, 0xff45, 0xff53, 0xff54,
+ 0xff3f, 0xff55, 0xff54, 0xff46,
+ 0xff11, 0xff16, 0xff2c, 0xff25,
+ 0xff54, 0xff4f, 0xff35, 0xff34,
+ 0xff26, 0xff18, 0x0 };
+
+static const uint32_t refutf32LE1[] = { 0xff54, 0xff45, 0xff53, 0xff54,
+ 0xff3f, 0xff55, 0xff54, 0xff46,
+ 0xff18, 0xff34, 0xff4f, 0xff1a,
+ 0xff3f, 0xff43, 0xff48, 0xff41,
+ 0xff52, 0xff53, 0xff45, 0xff54,
+ 0xff3f, 0xff35, 0xff34, 0xff26,
+ 0xff0d, 0xff13, 0xff12, 0xff2c,
+ 0xff25, 0xff0c, 0xff3f, 0xff23,
+ 0xff33, 0xff54, 0xff44, 0xff33,
+ 0xff54, 0xff52, 0xff49, 0xff4e,
+ 0xff47, 0xff13, 0xff12, 0xff3f,
+#ifdef TARGET_DARWIN
+ 0x0 };
+#else
+ 0x1f42d, 0x1f42e, 0x0 };
+#endif
+
+static const uint16_t refutf16BE[] = { 0x54ff, 0x45ff, 0x53ff, 0x54ff,
+ 0x3fff, 0x55ff, 0x54ff, 0x46ff,
+ 0x11ff, 0x16ff, 0x22ff, 0x25ff,
+ 0x54ff, 0x4fff, 0x35ff, 0x34ff,
+ 0x26ff, 0x18ff, 0x0};
+
+static const uint16_t refucs2[] = { 0xff54, 0xff45, 0xff53, 0xff54,
+ 0xff3f, 0xff55, 0xff43, 0xff53,
+ 0xff12, 0xff54, 0xff4f, 0xff35,
+ 0xff34, 0xff26, 0xff18, 0x0 };
+#endif
+
+class TestCharsetConverter : public testing::Test
+{
+protected:
+ TestCharsetConverter()
+ {
+ /* Add default settings for locale.
+ * Settings here are taken from CGUISettings::Initialize()
+ */
+ /*
+ //! @todo implement
+ CSettingsCategory *loc = CServiceBroker::GetSettingsComponent()->GetSettings()->AddCategory(7, "locale", 14090);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->AddString(loc, CSettings::SETTING_LOCALE_LANGUAGE,248,"english",
+ SPIN_CONTROL_TEXT);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->AddString(loc, CSettings::SETTING_LOCALE_COUNTRY, 20026, "USA",
+ SPIN_CONTROL_TEXT);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->AddString(loc, CSettings::SETTING_LOCALE_CHARSET, 14091, "DEFAULT",
+ SPIN_CONTROL_TEXT); // charset is set by the
+ // language file
+
+ // Add default settings for subtitles
+ CSettingsCategory *sub = CServiceBroker::GetSettingsComponent()->GetSettings()->AddCategory(5, "subtitles", 287);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->AddString(sub, CSettings::SETTING_SUBTITLES_CHARSET, 735, "DEFAULT",
+ SPIN_CONTROL_TEXT);
+ */
+ g_charsetConverter.reset();
+ g_charsetConverter.clear();
+ }
+
+ ~TestCharsetConverter() override
+ {
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Unload();
+ }
+
+ std::string refstra1, refstra2, varstra1;
+ std::wstring refstrw1, varstrw1;
+ std::string refstr1;
+};
+
+TEST_F(TestCharsetConverter, utf8ToW)
+{
+ refstra1 = "test utf8ToW";
+ refstrw1 = L"test utf8ToW";
+ varstrw1.clear();
+ g_charsetConverter.utf8ToW(refstra1, varstrw1, true, false, false);
+ EXPECT_STREQ(refstrw1.c_str(), varstrw1.c_str());
+}
+
+
+//TEST_F(TestCharsetConverter, utf16LEtoW)
+//{
+// refstrw1 = L"test_utf16LEtow";
+// //! @todo Should be able to use '=' operator instead of assign()
+// std::wstring refstr16_1;
+// refstr16_1.assign(refutf16LE1);
+// varstrw1.clear();
+// g_charsetConverter.utf16LEtoW(refstr16_1, varstrw1);
+// EXPECT_STREQ(refstrw1.c_str(), varstrw1.c_str());
+//}
+
+TEST_F(TestCharsetConverter, subtitleCharsetToUtf8)
+{
+ refstra1 = "test subtitleCharsetToW";
+ varstra1.clear();
+ g_charsetConverter.subtitleCharsetToUtf8(refstra1, varstra1);
+
+ /* Assign refstra1 to refstrw1 so that we can compare */
+ EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+}
+
+TEST_F(TestCharsetConverter, utf8ToStringCharset_1)
+{
+ refstra1 = "test utf8ToStringCharset";
+ varstra1.clear();
+ g_charsetConverter.utf8ToStringCharset(refstra1, varstra1);
+ EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+}
+
+TEST_F(TestCharsetConverter, utf8ToStringCharset_2)
+{
+ refstra1 = "test utf8ToStringCharset";
+ varstra1 = "test utf8ToStringCharset";
+ g_charsetConverter.utf8ToStringCharset(varstra1);
+ EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+}
+
+TEST_F(TestCharsetConverter, utf8ToSystem)
+{
+ refstra1 = "test utf8ToSystem";
+ varstra1 = "test utf8ToSystem";
+ g_charsetConverter.utf8ToSystem(varstra1);
+ EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+}
+
+TEST_F(TestCharsetConverter, utf8To_ASCII)
+{
+ refstra1 = "test utf8To: charset ASCII, std::string";
+ varstra1.clear();
+ g_charsetConverter.utf8To("ASCII", refstra1, varstra1);
+ EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+}
+
+/*
+TEST_F(TestCharsetConverter, utf8To_UTF16LE)
+{
+ refstra1 = "test_utf8To:_charset_UTF-16LE,_"
+ "CStdString16";
+ refstr16_1.assign(refutf16LE2);
+ varstr16_1.clear();
+ g_charsetConverter.utf8To("UTF-16LE", refstra1, varstr16_1);
+ EXPECT_TRUE(!memcmp(refstr16_1.c_str(), varstr16_1.c_str(),
+ refstr16_1.length() * sizeof(uint16_t)));
+}
+*/
+
+//TEST_F(TestCharsetConverter, utf8To_UTF32LE)
+//{
+// refstra1 = "test_utf8To:_charset_UTF-32LE,_"
+//#ifdef TARGET_DARWIN
+///* OSX has its own 'special' utf-8 charset which we use (see UTF8_SOURCE in CharsetConverter.cpp)
+// which is basically NFD (decomposed) utf-8. The trouble is, it fails on the COW FACE and MOUSE FACE
+// characters for some reason (possibly anything over 0x100000, or maybe there's a decomposed form of these
+// that I couldn't find???) If UTF8_SOURCE is switched to UTF-8 then this test would pass as-is, but then
+// some filenames stored in utf8-mac wouldn't display correctly in the UI. */
+// "CStdString32_";
+//#else
+// "CStdString32_🐭🐮";
+//#endif
+// refstr32_1.assign(refutf32LE1);
+// varstr32_1.clear();
+// g_charsetConverter.utf8To("UTF-32LE", refstra1, varstr32_1);
+// EXPECT_TRUE(!memcmp(refstr32_1.c_str(), varstr32_1.c_str(),
+// sizeof(refutf32LE1)));
+//}
+
+TEST_F(TestCharsetConverter, stringCharsetToUtf8)
+{
+ refstra1 = "test_stringCharsetToUtf8";
+ varstra1.clear();
+ g_charsetConverter.ToUtf8("UTF-16LE", refutf16LE3, varstra1);
+ EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+}
+
+TEST_F(TestCharsetConverter, isValidUtf8_1)
+{
+ varstra1.clear();
+ g_charsetConverter.ToUtf8("UTF-16LE", refutf16LE3, varstra1);
+ EXPECT_TRUE(CUtf8Utils::isValidUtf8(varstra1.c_str()));
+}
+
+TEST_F(TestCharsetConverter, isValidUtf8_2)
+{
+ refstr1 = refutf16LE3;
+ EXPECT_FALSE(CUtf8Utils::isValidUtf8(refstr1));
+}
+
+TEST_F(TestCharsetConverter, isValidUtf8_3)
+{
+ varstra1.clear();
+ g_charsetConverter.ToUtf8("UTF-16LE", refutf16LE3, varstra1);
+ EXPECT_TRUE(CUtf8Utils::isValidUtf8(varstra1.c_str()));
+}
+
+TEST_F(TestCharsetConverter, isValidUtf8_4)
+{
+ EXPECT_FALSE(CUtf8Utils::isValidUtf8(refutf16LE3));
+}
+
+//! @todo Resolve correct input/output for this function
+// TEST_F(TestCharsetConverter, ucs2CharsetToStringCharset)
+// {
+// void ucs2CharsetToStringCharset(const std::wstring& strSource,
+// std::string& strDest, bool swap = false);
+// }
+
+TEST_F(TestCharsetConverter, wToUTF8)
+{
+ refstrw1 = L"test_wToUTF8";
+ refstra1 = u8"test_wToUTF8";
+ varstra1.clear();
+ g_charsetConverter.wToUTF8(refstrw1, varstra1);
+ EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+}
+
+//TEST_F(TestCharsetConverter, utf16BEtoUTF8)
+//{
+// refstr16_1.assign(refutf16BE);
+// refstra1 = "test_utf16BEtoUTF8";
+// varstra1.clear();
+// g_charsetConverter.utf16BEtoUTF8(refstr16_1, varstra1);
+// EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+//}
+
+//TEST_F(TestCharsetConverter, utf16LEtoUTF8)
+//{
+// refstr16_1.assign(refutf16LE4);
+// refstra1 = "test_utf16LEtoUTF8";
+// varstra1.clear();
+// g_charsetConverter.utf16LEtoUTF8(refstr16_1, varstra1);
+// EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+//}
+
+//TEST_F(TestCharsetConverter, ucs2ToUTF8)
+//{
+// refstr16_1.assign(refucs2);
+// refstra1 = "test_ucs2toUTF8";
+// varstra1.clear();
+// g_charsetConverter.ucs2ToUTF8(refstr16_1, varstra1);
+// EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+//}
+
+TEST_F(TestCharsetConverter, utf8logicalToVisualBiDi)
+{
+ refstra1 = "test_utf8logicalToVisualBiDi";
+ refstra2 = "test_utf8logicalToVisualBiDi";
+ varstra1.clear();
+ g_charsetConverter.utf8logicalToVisualBiDi(refstra1, varstra1);
+ EXPECT_STREQ(refstra2.c_str(), varstra1.c_str());
+}
+
+//! @todo Resolve correct input/output for this function
+// TEST_F(TestCharsetConverter, utf32ToStringCharset)
+// {
+// void utf32ToStringCharset(const unsigned long* strSource, std::string& strDest);
+// }
+
+TEST_F(TestCharsetConverter, getCharsetLabels)
+{
+ std::vector<std::string> reflabels;
+ reflabels.emplace_back("Western Europe (ISO)");
+ reflabels.emplace_back("Central Europe (ISO)");
+ reflabels.emplace_back("South Europe (ISO)");
+ reflabels.emplace_back("Baltic (ISO)");
+ reflabels.emplace_back("Cyrillic (ISO)");
+ reflabels.emplace_back("Arabic (ISO)");
+ reflabels.emplace_back("Greek (ISO)");
+ reflabels.emplace_back("Hebrew (ISO)");
+ reflabels.emplace_back("Turkish (ISO)");
+ reflabels.emplace_back("Central Europe (Windows)");
+ reflabels.emplace_back("Cyrillic (Windows)");
+ reflabels.emplace_back("Western Europe (Windows)");
+ reflabels.emplace_back("Greek (Windows)");
+ reflabels.emplace_back("Turkish (Windows)");
+ reflabels.emplace_back("Hebrew (Windows)");
+ reflabels.emplace_back("Arabic (Windows)");
+ reflabels.emplace_back("Baltic (Windows)");
+ reflabels.emplace_back("Vietnamese (Windows)");
+ reflabels.emplace_back("Thai (Windows)");
+ reflabels.emplace_back("Chinese Traditional (Big5)");
+ reflabels.emplace_back("Chinese Simplified (GBK)");
+ reflabels.emplace_back("Japanese (Shift-JIS)");
+ reflabels.emplace_back("Korean");
+ reflabels.emplace_back("Hong Kong (Big5-HKSCS)");
+
+ std::vector<std::string> varlabels = g_charsetConverter.getCharsetLabels();
+ ASSERT_EQ(reflabels.size(), varlabels.size());
+
+ size_t pos = 0;
+ for (const auto& it : varlabels)
+ {
+ EXPECT_STREQ((reflabels.at(pos++)).c_str(), it.c_str());
+ }
+}
+
+TEST_F(TestCharsetConverter, getCharsetLabelByName)
+{
+ std::string varstr =
+ g_charsetConverter.getCharsetLabelByName("ISO-8859-1");
+ EXPECT_STREQ("Western Europe (ISO)", varstr.c_str());
+ varstr.clear();
+ varstr = g_charsetConverter.getCharsetLabelByName("Bogus");
+ EXPECT_STREQ("", varstr.c_str());
+}
+
+TEST_F(TestCharsetConverter, getCharsetNameByLabel)
+{
+ std::string varstr =
+ g_charsetConverter.getCharsetNameByLabel("Western Europe (ISO)");
+ EXPECT_STREQ("ISO-8859-1", varstr.c_str());
+ varstr.clear();
+ varstr = g_charsetConverter.getCharsetNameByLabel("Bogus");
+ EXPECT_STREQ("", varstr.c_str());
+}
+
+TEST_F(TestCharsetConverter, unknownToUTF8_1)
+{
+ refstra1 = "test_unknownToUTF8";
+ varstra1 = "test_unknownToUTF8";
+ g_charsetConverter.unknownToUTF8(varstra1);
+ EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+}
+
+TEST_F(TestCharsetConverter, unknownToUTF8_2)
+{
+ refstra1 = "test_unknownToUTF8";
+ varstra1.clear();
+ g_charsetConverter.unknownToUTF8(refstra1, varstra1);
+ EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+}
+
+TEST_F(TestCharsetConverter, toW)
+{
+ refstra1 = "test_toW:_charset_UTF-16LE";
+ refstrw1 = L"\xBDEF\xEF94\x85BD\xBDEF\xEF93\x94BD\xBCEF\xEFBF"
+ L"\x94BD\xBDEF\xEF8F\xB7BC\xBCEF\xEF9A\xBFBC\xBDEF"
+ L"\xEF83\x88BD\xBDEF\xEF81\x92BD\xBDEF\xEF93\x85BD"
+ L"\xBDEF\xEF94\xBFBC\xBCEF\xEFB5\xB4BC\xBCEF\xEFA6"
+ L"\x8DBC\xBCEF\xEF91\x96BC\xBCEF\xEFAC\xA5BC";
+ varstrw1.clear();
+ g_charsetConverter.toW(refstra1, varstrw1, "UTF-16LE");
+ EXPECT_STREQ(refstrw1.c_str(), varstrw1.c_str());
+}
+
+TEST_F(TestCharsetConverter, fromW)
+{
+ refstrw1 = L"\xBDEF\xEF94\x85BD\xBDEF\xEF93\x94BD\xBCEF\xEFBF"
+ L"\x86BD\xBDEF\xEF92\x8FBD\xBDEF\xEF8D\xB7BC\xBCEF"
+ L"\xEF9A\xBFBC\xBDEF\xEF83\x88BD\xBDEF\xEF81\x92BD"
+ L"\xBDEF\xEF93\x85BD\xBDEF\xEF94\xBFBC\xBCEF\xEFB5"
+ L"\xB4BC\xBCEF\xEFA6\x8DBC\xBCEF\xEF91\x96BC\xBCEF"
+ L"\xEFAC\xA5BC";
+ refstra1 = "test_fromW:_charset_UTF-16LE";
+ varstra1.clear();
+ g_charsetConverter.fromW(refstrw1, varstra1, "UTF-16LE");
+ EXPECT_STREQ(refstra1.c_str(), varstra1.c_str());
+}
diff --git a/xbmc/utils/test/TestComponentContainer.cpp b/xbmc/utils/test/TestComponentContainer.cpp
new file mode 100644
index 0000000..d7246be
--- /dev/null
+++ b/xbmc/utils/test/TestComponentContainer.cpp
@@ -0,0 +1,76 @@
+/*
+ * 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 "utils/ComponentContainer.h"
+
+#include <utility>
+
+#include <gtest/gtest.h>
+
+class BaseTestType
+{
+public:
+ virtual ~BaseTestType() = default;
+};
+
+struct DerivedType1 : public BaseTestType
+{
+ int a = 1;
+};
+
+struct DerivedType2 : public BaseTestType
+{
+ int a = 2;
+};
+
+struct DerivedType3 : public BaseTestType
+{
+ int a = 3;
+};
+
+class TestContainer : public CComponentContainer<BaseTestType>
+{
+ FRIEND_TEST(TestComponentContainer, Generic);
+};
+
+TEST(TestComponentContainer, Generic)
+{
+ TestContainer container;
+
+ // check that we can register types
+ container.RegisterComponent(std::make_shared<DerivedType1>());
+ EXPECT_EQ(container.size(), 1u);
+ container.RegisterComponent(std::make_shared<DerivedType2>());
+ EXPECT_EQ(container.size(), 2u);
+
+ // check that trying to register a component twice does nothing
+ container.RegisterComponent(std::make_shared<DerivedType2>());
+ EXPECT_EQ(container.size(), 2u);
+
+ // check that first component is valid
+ const auto t1 = container.GetComponent<DerivedType1>();
+ EXPECT_TRUE(t1 != nullptr);
+ EXPECT_EQ(t1->a, 1);
+
+ // check that second component is valid
+ const auto t2 = container.GetComponent<DerivedType2>();
+ EXPECT_TRUE(t2 != nullptr);
+ EXPECT_EQ(t2->a, 2);
+
+ // check that third component is not there
+ EXPECT_THROW(container.GetComponent<DerivedType3>(), std::logic_error);
+
+ // check that component instance is constant
+ const auto t4 = container.GetComponent<DerivedType1>();
+ EXPECT_EQ(t1.get(), t4.get());
+
+ // check we can call the const overload for GetComponent
+ // and that the returned type is const
+ const auto t5 = const_cast<const TestContainer&>(container).GetComponent<DerivedType1>();
+ EXPECT_TRUE(std::is_const_v<typename decltype(t5)::element_type>);
+}
diff --git a/xbmc/utils/test/TestCrc32.cpp b/xbmc/utils/test/TestCrc32.cpp
new file mode 100644
index 0000000..99a2dd5
--- /dev/null
+++ b/xbmc/utils/test/TestCrc32.cpp
@@ -0,0 +1,50 @@
+/*
+ * 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 "utils/Crc32.h"
+
+#include <gtest/gtest.h>
+
+static const char refdata[] = "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "01234567890!@#$%^&*()";
+
+TEST(TestCrc32, Compute_1)
+{
+ Crc32 a;
+ uint32_t varcrc;
+ a.Compute(refdata, sizeof(refdata) - 1);
+ varcrc = a;
+ EXPECT_EQ(0xa4eb60e3, varcrc);
+}
+
+TEST(TestCrc32, Compute_2)
+{
+ uint32_t varcrc;
+ std::string s = refdata;
+ varcrc = Crc32::Compute(s);
+ EXPECT_EQ(0xa4eb60e3, varcrc);
+}
+
+TEST(TestCrc32, ComputeFromLowerCase)
+{
+ std::string s = refdata;
+ uint32_t varcrc = Crc32::ComputeFromLowerCase(s);
+ EXPECT_EQ((uint32_t)0x7f045b3e, varcrc);
+}
+
+TEST(TestCrc32, Reset)
+{
+ Crc32 a;
+ uint32_t varcrc;
+ std::string s = refdata;
+ a.Compute(s.c_str(), s.length());
+ a.Reset();
+ varcrc = a;
+ EXPECT_EQ(0xffffffff, varcrc);
+}
diff --git a/xbmc/utils/test/TestDatabaseUtils.cpp b/xbmc/utils/test/TestDatabaseUtils.cpp
new file mode 100644
index 0000000..ddb986c
--- /dev/null
+++ b/xbmc/utils/test/TestDatabaseUtils.cpp
@@ -0,0 +1,1377 @@
+/*
+ * 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 "dbwrappers/qry_dat.h"
+#include "music/MusicDatabase.h"
+#include "utils/DatabaseUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "video/VideoDatabase.h"
+
+#include <gtest/gtest.h>
+
+class TestDatabaseUtilsHelper
+{
+public:
+ TestDatabaseUtilsHelper()
+ {
+ album_idAlbum = CMusicDatabase::album_idAlbum;
+ album_strAlbum = CMusicDatabase::album_strAlbum;
+ album_strArtists = CMusicDatabase::album_strArtists;
+ album_strGenres = CMusicDatabase::album_strGenres;
+ album_strMoods = CMusicDatabase::album_strMoods;
+ album_strReleaseDate = CMusicDatabase::album_strReleaseDate;
+ album_strOrigReleaseDate = CMusicDatabase::album_strOrigReleaseDate;
+ album_strStyles = CMusicDatabase::album_strStyles;
+ album_strThemes = CMusicDatabase::album_strThemes;
+ album_strReview = CMusicDatabase::album_strReview;
+ album_strLabel = CMusicDatabase::album_strLabel;
+ album_strType = CMusicDatabase::album_strType;
+ album_fRating = CMusicDatabase::album_fRating;
+ album_iVotes = CMusicDatabase::album_iVotes;
+ album_iUserrating = CMusicDatabase::album_iUserrating;
+ album_dtDateAdded = CMusicDatabase::album_dateAdded;
+
+ song_idSong = CMusicDatabase::song_idSong;
+ song_strTitle = CMusicDatabase::song_strTitle;
+ song_iTrack = CMusicDatabase::song_iTrack;
+ song_iDuration = CMusicDatabase::song_iDuration;
+ song_strReleaseDate = CMusicDatabase::song_strReleaseDate;
+ song_strOrigReleaseDate = CMusicDatabase::song_strOrigReleaseDate;
+ song_strFileName = CMusicDatabase::song_strFileName;
+ song_iTimesPlayed = CMusicDatabase::song_iTimesPlayed;
+ song_iStartOffset = CMusicDatabase::song_iStartOffset;
+ song_iEndOffset = CMusicDatabase::song_iEndOffset;
+ song_lastplayed = CMusicDatabase::song_lastplayed;
+ song_rating = CMusicDatabase::song_rating;
+ song_votes = CMusicDatabase::song_votes;
+ song_userrating = CMusicDatabase::song_userrating;
+ song_comment = CMusicDatabase::song_comment;
+ song_strAlbum = CMusicDatabase::song_strAlbum;
+ song_strPath = CMusicDatabase::song_strPath;
+ song_strGenres = CMusicDatabase::song_strGenres;
+ song_strArtists = CMusicDatabase::song_strArtists;
+ }
+
+ int album_idAlbum;
+ int album_strAlbum;
+ int album_strArtists;
+ int album_strGenres;
+ int album_strMoods;
+ int album_strReleaseDate;
+ int album_strOrigReleaseDate;
+ int album_strStyles;
+ int album_strThemes;
+ int album_strReview;
+ int album_strLabel;
+ int album_strType;
+ int album_fRating;
+ int album_iVotes;
+ int album_iUserrating;
+ int album_dtDateAdded;
+
+ int song_idSong;
+ int song_strTitle;
+ int song_iTrack;
+ int song_iDuration;
+ int song_strReleaseDate;
+ int song_strOrigReleaseDate;
+ int song_strFileName;
+ int song_iTimesPlayed;
+ int song_iStartOffset;
+ int song_iEndOffset;
+ int song_lastplayed;
+ int song_rating;
+ int song_votes;
+ int song_userrating;
+ int song_comment;
+ int song_strAlbum;
+ int song_strPath;
+ int song_strGenres;
+ int song_strArtists;
+};
+
+TEST(TestDatabaseUtils, GetField_None)
+{
+ std::string refstr, varstr;
+
+ refstr = "";
+ varstr = DatabaseUtils::GetField(FieldNone, MediaTypeNone,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ varstr = DatabaseUtils::GetField(FieldNone, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestDatabaseUtils, GetField_MediaTypeAlbum)
+{
+ std::string refstr, varstr;
+
+ refstr = "albumview.idAlbum";
+ varstr = DatabaseUtils::GetField(FieldId, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strAlbum";
+ varstr = DatabaseUtils::GetField(FieldAlbum, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strArtists";
+ varstr = DatabaseUtils::GetField(FieldArtist, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strArtists";
+ varstr = DatabaseUtils::GetField(FieldAlbumArtist, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strGenres";
+ varstr = DatabaseUtils::GetField(FieldGenre, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strReleaseDate";
+ varstr = DatabaseUtils::GetField(FieldYear, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+refstr = "albumview.strOrigReleaseDate";
+ varstr = DatabaseUtils::GetField(FieldOrigYear, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strMoods";
+ varstr = DatabaseUtils::GetField(FieldMoods, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strStyles";
+ varstr = DatabaseUtils::GetField(FieldStyles, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strThemes";
+ varstr = DatabaseUtils::GetField(FieldThemes, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strReview";
+ varstr = DatabaseUtils::GetField(FieldReview, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strLabel";
+ varstr = DatabaseUtils::GetField(FieldMusicLabel, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strType";
+ varstr = DatabaseUtils::GetField(FieldAlbumType, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.fRating";
+ varstr = DatabaseUtils::GetField(FieldRating, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.iVotes";
+ varstr = DatabaseUtils::GetField(FieldVotes, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.iUserrating";
+ varstr = DatabaseUtils::GetField(FieldUserRating, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.dateAdded";
+ varstr = DatabaseUtils::GetField(FieldDateAdded, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "";
+ varstr = DatabaseUtils::GetField(FieldNone, MediaTypeAlbum,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "albumview.strAlbum";
+ varstr = DatabaseUtils::GetField(FieldAlbum, MediaTypeAlbum,
+ DatabaseQueryPartWhere);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ varstr = DatabaseUtils::GetField(FieldAlbum, MediaTypeAlbum,
+ DatabaseQueryPartOrderBy);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestDatabaseUtils, GetField_MediaTypeSong)
+{
+ std::string refstr, varstr;
+
+ refstr = "songview.idSong";
+ varstr = DatabaseUtils::GetField(FieldId, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.strTitle";
+ varstr = DatabaseUtils::GetField(FieldTitle, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.iTrack";
+ varstr = DatabaseUtils::GetField(FieldTrackNumber, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.iDuration";
+ varstr = DatabaseUtils::GetField(FieldTime, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.strFilename";
+ varstr = DatabaseUtils::GetField(FieldFilename, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.iTimesPlayed";
+ varstr = DatabaseUtils::GetField(FieldPlaycount, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.iStartOffset";
+ varstr = DatabaseUtils::GetField(FieldStartOffset, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.iEndOffset";
+ varstr = DatabaseUtils::GetField(FieldEndOffset, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.lastPlayed";
+ varstr = DatabaseUtils::GetField(FieldLastPlayed, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.rating";
+ varstr = DatabaseUtils::GetField(FieldRating, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.votes";
+ varstr = DatabaseUtils::GetField(FieldVotes, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.userrating";
+ varstr = DatabaseUtils::GetField(FieldUserRating, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.comment";
+ varstr = DatabaseUtils::GetField(FieldComment, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.strReleaseDate";
+ varstr = DatabaseUtils::GetField(FieldYear, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.strOrigReleaseDate";
+ varstr = DatabaseUtils::GetField(FieldOrigYear, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.strAlbum";
+ varstr = DatabaseUtils::GetField(FieldAlbum, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.strPath";
+ varstr = DatabaseUtils::GetField(FieldPath, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.strArtists";
+ varstr = DatabaseUtils::GetField(FieldArtist, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.strArtists";
+ varstr = DatabaseUtils::GetField(FieldAlbumArtist, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.strGenres";
+ varstr = DatabaseUtils::GetField(FieldGenre, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.dateAdded";
+ varstr = DatabaseUtils::GetField(FieldDateAdded, MediaTypeSong,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "songview.strPath";
+ varstr = DatabaseUtils::GetField(FieldPath, MediaTypeSong,
+ DatabaseQueryPartWhere);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ varstr = DatabaseUtils::GetField(FieldPath, MediaTypeSong,
+ DatabaseQueryPartOrderBy);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestDatabaseUtils, GetField_MediaTypeMusicVideo)
+{
+ std::string refstr, varstr;
+
+ refstr = "musicvideo_view.idMVideo";
+ varstr = DatabaseUtils::GetField(FieldId, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_TITLE);
+ varstr = DatabaseUtils::GetField(FieldTitle, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_RUNTIME);
+ varstr = DatabaseUtils::GetField(FieldTime, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_DIRECTOR);
+ varstr = DatabaseUtils::GetField(FieldDirector, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_STUDIOS);
+ varstr = DatabaseUtils::GetField(FieldStudio, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_PLOT);
+ varstr = DatabaseUtils::GetField(FieldPlot, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_ALBUM);
+ varstr = DatabaseUtils::GetField(FieldAlbum, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_ARTIST);
+ varstr = DatabaseUtils::GetField(FieldArtist, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_GENRE);
+ varstr = DatabaseUtils::GetField(FieldGenre, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("musicvideo_view.c{:02}", VIDEODB_ID_MUSICVIDEO_TRACK);
+ varstr = DatabaseUtils::GetField(FieldTrackNumber, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "musicvideo_view.strFilename";
+ varstr = DatabaseUtils::GetField(FieldFilename, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "musicvideo_view.strPath";
+ varstr = DatabaseUtils::GetField(FieldPath, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "musicvideo_view.playCount";
+ varstr = DatabaseUtils::GetField(FieldPlaycount, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "musicvideo_view.lastPlayed";
+ varstr = DatabaseUtils::GetField(FieldLastPlayed, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "musicvideo_view.dateAdded";
+ varstr = DatabaseUtils::GetField(FieldDateAdded, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "";
+ varstr = DatabaseUtils::GetField(FieldVideoResolution, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "musicvideo_view.strPath";
+ varstr = DatabaseUtils::GetField(FieldPath, MediaTypeMusicVideo,
+ DatabaseQueryPartWhere);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "musicvideo_view.strPath";
+ varstr = DatabaseUtils::GetField(FieldPath, MediaTypeMusicVideo,
+ DatabaseQueryPartOrderBy);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "musicvideo_view.userrating";
+ varstr = DatabaseUtils::GetField(FieldUserRating, MediaTypeMusicVideo,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestDatabaseUtils, GetField_MediaTypeMovie)
+{
+ std::string refstr, varstr;
+
+ refstr = "movie_view.idMovie";
+ varstr = DatabaseUtils::GetField(FieldId, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_TITLE);
+ varstr = DatabaseUtils::GetField(FieldTitle, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("CASE WHEN length(movie_view.c{:02}) > 0 THEN movie_view.c{:02} "
+ "ELSE movie_view.c{:02} END",
+ VIDEODB_ID_SORTTITLE, VIDEODB_ID_SORTTITLE, VIDEODB_ID_TITLE);
+ varstr = DatabaseUtils::GetField(FieldTitle, MediaTypeMovie,
+ DatabaseQueryPartOrderBy);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_PLOT);
+ varstr = DatabaseUtils::GetField(FieldPlot, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_PLOTOUTLINE);
+ varstr = DatabaseUtils::GetField(FieldPlotOutline, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_TAGLINE);
+ varstr = DatabaseUtils::GetField(FieldTagline, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "movie_view.votes";
+ varstr = DatabaseUtils::GetField(FieldVotes, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "movie_view.rating";
+ varstr = DatabaseUtils::GetField(FieldRating, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_CREDITS);
+ varstr = DatabaseUtils::GetField(FieldWriter, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_SORTTITLE);
+ varstr = DatabaseUtils::GetField(FieldSortTitle, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_RUNTIME);
+ varstr = DatabaseUtils::GetField(FieldTime, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_MPAA);
+ varstr = DatabaseUtils::GetField(FieldMPAA, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_TOP250);
+ varstr = DatabaseUtils::GetField(FieldTop250, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_GENRE);
+ varstr = DatabaseUtils::GetField(FieldGenre, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_DIRECTOR);
+ varstr = DatabaseUtils::GetField(FieldDirector, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_STUDIOS);
+ varstr = DatabaseUtils::GetField(FieldStudio, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_TRAILER);
+ varstr = DatabaseUtils::GetField(FieldTrailer, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("movie_view.c{:02}", VIDEODB_ID_COUNTRY);
+ varstr = DatabaseUtils::GetField(FieldCountry, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "movie_view.strFilename";
+ varstr = DatabaseUtils::GetField(FieldFilename, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "movie_view.strPath";
+ varstr = DatabaseUtils::GetField(FieldPath, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "movie_view.playCount";
+ varstr = DatabaseUtils::GetField(FieldPlaycount, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "movie_view.lastPlayed";
+ varstr = DatabaseUtils::GetField(FieldLastPlayed, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "movie_view.dateAdded";
+ varstr = DatabaseUtils::GetField(FieldDateAdded, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "movie_view.userrating";
+ varstr = DatabaseUtils::GetField(FieldUserRating, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "";
+ varstr = DatabaseUtils::GetField(FieldRandom, MediaTypeMovie,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestDatabaseUtils, GetField_MediaTypeTvShow)
+{
+ std::string refstr, varstr;
+
+ refstr = "tvshow_view.idShow";
+ varstr = DatabaseUtils::GetField(FieldId, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr =
+ StringUtils::Format("CASE WHEN length(tvshow_view.c{:02}) > 0 THEN tvshow_view.c{:02} "
+ "ELSE tvshow_view.c{:02} END",
+ VIDEODB_ID_TV_SORTTITLE, VIDEODB_ID_TV_SORTTITLE, VIDEODB_ID_TV_TITLE);
+ varstr = DatabaseUtils::GetField(FieldTitle, MediaTypeTvShow,
+ DatabaseQueryPartOrderBy);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_TITLE);
+ varstr = DatabaseUtils::GetField(FieldTitle, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_PLOT);
+ varstr = DatabaseUtils::GetField(FieldPlot, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_STATUS);
+ varstr = DatabaseUtils::GetField(FieldTvShowStatus, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "tvshow_view.votes";
+ varstr = DatabaseUtils::GetField(FieldVotes, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "tvshow_view.rating";
+ varstr = DatabaseUtils::GetField(FieldRating, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_PREMIERED);
+ varstr = DatabaseUtils::GetField(FieldYear, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_GENRE);
+ varstr = DatabaseUtils::GetField(FieldGenre, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_MPAA);
+ varstr = DatabaseUtils::GetField(FieldMPAA, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_STUDIOS);
+ varstr = DatabaseUtils::GetField(FieldStudio, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("tvshow_view.c{:02}", VIDEODB_ID_TV_SORTTITLE);
+ varstr = DatabaseUtils::GetField(FieldSortTitle, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "tvshow_view.strPath";
+ varstr = DatabaseUtils::GetField(FieldPath, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "tvshow_view.dateAdded";
+ varstr = DatabaseUtils::GetField(FieldDateAdded, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "tvshow_view.totalSeasons";
+ varstr = DatabaseUtils::GetField(FieldSeason, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "tvshow_view.totalCount";
+ varstr = DatabaseUtils::GetField(FieldNumberOfEpisodes, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "tvshow_view.watchedcount";
+ varstr = DatabaseUtils::GetField(FieldNumberOfWatchedEpisodes,
+ MediaTypeTvShow, DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "tvshow_view.userrating";
+ varstr = DatabaseUtils::GetField(FieldUserRating, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "";
+ varstr = DatabaseUtils::GetField(FieldRandom, MediaTypeTvShow,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestDatabaseUtils, GetField_MediaTypeEpisode)
+{
+ std::string refstr, varstr;
+
+ refstr = "episode_view.idEpisode";
+ varstr = DatabaseUtils::GetField(FieldId, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_TITLE);
+ varstr = DatabaseUtils::GetField(FieldTitle, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_PLOT);
+ varstr = DatabaseUtils::GetField(FieldPlot, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.votes";
+ varstr = DatabaseUtils::GetField(FieldVotes, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.rating";
+ varstr = DatabaseUtils::GetField(FieldRating, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_CREDITS);
+ varstr = DatabaseUtils::GetField(FieldWriter, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_AIRED);
+ varstr = DatabaseUtils::GetField(FieldAirDate, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_RUNTIME);
+ varstr = DatabaseUtils::GetField(FieldTime, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_DIRECTOR);
+ varstr = DatabaseUtils::GetField(FieldDirector, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_SEASON);
+ varstr = DatabaseUtils::GetField(FieldSeason, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = StringUtils::Format("episode_view.c{:02}", VIDEODB_ID_EPISODE_EPISODE);
+ varstr = DatabaseUtils::GetField(FieldEpisodeNumber, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.strFilename";
+ varstr = DatabaseUtils::GetField(FieldFilename, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.strPath";
+ varstr = DatabaseUtils::GetField(FieldPath, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.playCount";
+ varstr = DatabaseUtils::GetField(FieldPlaycount, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.lastPlayed";
+ varstr = DatabaseUtils::GetField(FieldLastPlayed, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.dateAdded";
+ varstr = DatabaseUtils::GetField(FieldDateAdded, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.strTitle";
+ varstr = DatabaseUtils::GetField(FieldTvShowTitle, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.premiered";
+ varstr = DatabaseUtils::GetField(FieldYear, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.mpaa";
+ varstr = DatabaseUtils::GetField(FieldMPAA, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.strStudio";
+ varstr = DatabaseUtils::GetField(FieldStudio, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "episode_view.userrating";
+ varstr = DatabaseUtils::GetField(FieldUserRating, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "";
+ varstr = DatabaseUtils::GetField(FieldRandom, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestDatabaseUtils, GetField_FieldRandom)
+{
+ std::string refstr, varstr;
+
+ refstr = "";
+ varstr = DatabaseUtils::GetField(FieldRandom, MediaTypeEpisode,
+ DatabaseQueryPartSelect);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "";
+ varstr = DatabaseUtils::GetField(FieldRandom, MediaTypeEpisode,
+ DatabaseQueryPartWhere);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "RANDOM()";
+ varstr = DatabaseUtils::GetField(FieldRandom, MediaTypeEpisode,
+ DatabaseQueryPartOrderBy);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestDatabaseUtils, GetFieldIndex_None)
+{
+ int refindex, varindex;
+
+ refindex = -1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRandom, MediaTypeNone);
+ EXPECT_EQ(refindex, varindex);
+
+ varindex = DatabaseUtils::GetFieldIndex(FieldNone, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+}
+
+//! @todo Should enums in CMusicDatabase be made public instead?
+TEST(TestDatabaseUtils, GetFieldIndex_MediaTypeAlbum)
+{
+ int refindex, varindex;
+ TestDatabaseUtilsHelper a;
+
+ refindex = a.album_idAlbum;
+ varindex = DatabaseUtils::GetFieldIndex(FieldId, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strAlbum;
+ varindex = DatabaseUtils::GetFieldIndex(FieldAlbum, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strArtists;
+ varindex = DatabaseUtils::GetFieldIndex(FieldArtist, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strArtists;
+ varindex = DatabaseUtils::GetFieldIndex(FieldAlbumArtist, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strGenres;
+ varindex = DatabaseUtils::GetFieldIndex(FieldGenre, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strReleaseDate;
+ varindex = DatabaseUtils::GetFieldIndex(FieldYear, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strOrigReleaseDate;
+ varindex = DatabaseUtils::GetFieldIndex(FieldOrigYear, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strMoods;
+ varindex = DatabaseUtils::GetFieldIndex(FieldMoods, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strStyles;
+ varindex = DatabaseUtils::GetFieldIndex(FieldStyles, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strThemes;
+ varindex = DatabaseUtils::GetFieldIndex(FieldThemes, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strReview;
+ varindex = DatabaseUtils::GetFieldIndex(FieldReview, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strLabel;
+ varindex = DatabaseUtils::GetFieldIndex(FieldMusicLabel, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_strType;
+ varindex = DatabaseUtils::GetFieldIndex(FieldAlbumType, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_fRating;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRating, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.album_dtDateAdded;
+ varindex = DatabaseUtils::GetFieldIndex(FieldDateAdded, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = -1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRandom, MediaTypeAlbum);
+ EXPECT_EQ(refindex, varindex);
+}
+
+TEST(TestDatabaseUtils, GetFieldIndex_MediaTypeSong)
+{
+ int refindex, varindex;
+ TestDatabaseUtilsHelper a;
+
+ refindex = a.song_idSong;
+ varindex = DatabaseUtils::GetFieldIndex(FieldId, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_strTitle;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTitle, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_iTrack;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTrackNumber, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_iDuration;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTime, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_strReleaseDate;
+ varindex = DatabaseUtils::GetFieldIndex(FieldYear, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_strFileName;
+ varindex = DatabaseUtils::GetFieldIndex(FieldFilename, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_iTimesPlayed;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPlaycount, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_iStartOffset;
+ varindex = DatabaseUtils::GetFieldIndex(FieldStartOffset, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_iEndOffset;
+ varindex = DatabaseUtils::GetFieldIndex(FieldEndOffset, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_lastplayed;
+ varindex = DatabaseUtils::GetFieldIndex(FieldLastPlayed, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_rating;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRating, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_votes;
+ varindex = DatabaseUtils::GetFieldIndex(FieldVotes, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_userrating;
+ varindex = DatabaseUtils::GetFieldIndex(FieldUserRating, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_comment;
+ varindex = DatabaseUtils::GetFieldIndex(FieldComment, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_strAlbum;
+ varindex = DatabaseUtils::GetFieldIndex(FieldAlbum, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_strPath;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPath, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_strArtists;
+ varindex = DatabaseUtils::GetFieldIndex(FieldArtist, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = a.song_strGenres;
+ varindex = DatabaseUtils::GetFieldIndex(FieldGenre, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = -1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRandom, MediaTypeSong);
+ EXPECT_EQ(refindex, varindex);
+}
+
+TEST(TestDatabaseUtils, GetFieldIndex_MediaTypeMusicVideo)
+{
+ int refindex, varindex;
+
+ refindex = 0;
+ varindex = DatabaseUtils::GetFieldIndex(FieldId, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_MUSICVIDEO_TITLE + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTitle, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_MUSICVIDEO_RUNTIME + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTime, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_MUSICVIDEO_DIRECTOR + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldDirector, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_MUSICVIDEO_STUDIOS + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldStudio, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_MUSICVIDEO_PLOT + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPlot, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_MUSICVIDEO_ALBUM + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldAlbum, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_MUSICVIDEO_ARTIST + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldArtist, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_MUSICVIDEO_GENRE + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldGenre, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_MUSICVIDEO_TRACK + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTrackNumber, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MUSICVIDEO_FILE;
+ varindex = DatabaseUtils::GetFieldIndex(FieldFilename, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MUSICVIDEO_PATH;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPath, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MUSICVIDEO_PLAYCOUNT;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPlaycount, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MUSICVIDEO_LASTPLAYED;
+ varindex = DatabaseUtils::GetFieldIndex(FieldLastPlayed, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MUSICVIDEO_DATEADDED;
+ varindex = DatabaseUtils::GetFieldIndex(FieldDateAdded, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MUSICVIDEO_USER_RATING;
+ varindex = DatabaseUtils::GetFieldIndex(FieldUserRating, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MUSICVIDEO_PREMIERED;
+ varindex = DatabaseUtils::GetFieldIndex(FieldYear, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = -1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRandom, MediaTypeMusicVideo);
+ EXPECT_EQ(refindex, varindex);
+}
+
+TEST(TestDatabaseUtils, GetFieldIndex_MediaTypeMovie)
+{
+ int refindex, varindex;
+
+ refindex = 0;
+ varindex = DatabaseUtils::GetFieldIndex(FieldId, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TITLE + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTitle, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_SORTTITLE + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldSortTitle, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_PLOT + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPlot, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_PLOTOUTLINE + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPlotOutline, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TAGLINE + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTagline, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_CREDITS + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldWriter, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_RUNTIME + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTime, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_MPAA + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldMPAA, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TOP250 + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTop250, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_GENRE + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldGenre, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_DIRECTOR + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldDirector, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_STUDIOS + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldStudio, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TRAILER + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTrailer, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_COUNTRY + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldCountry, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MOVIE_FILE + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldFilename, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MOVIE_PATH;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPath, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MOVIE_PLAYCOUNT;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPlaycount, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MOVIE_LASTPLAYED;
+ varindex = DatabaseUtils::GetFieldIndex(FieldLastPlayed, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MOVIE_DATEADDED;
+ varindex = DatabaseUtils::GetFieldIndex(FieldDateAdded, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MOVIE_USER_RATING;
+ varindex = DatabaseUtils::GetFieldIndex(FieldUserRating, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MOVIE_VOTES;
+ varindex = DatabaseUtils::GetFieldIndex(FieldVotes, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MOVIE_RATING;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRating, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_MOVIE_PREMIERED;
+ varindex = DatabaseUtils::GetFieldIndex(FieldYear, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = -1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRandom, MediaTypeMovie);
+ EXPECT_EQ(refindex, varindex);
+}
+
+TEST(TestDatabaseUtils, GetFieldIndex_MediaTypeTvShow)
+{
+ int refindex, varindex;
+
+ refindex = 0;
+ varindex = DatabaseUtils::GetFieldIndex(FieldId, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TV_TITLE + 1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTitle, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TV_SORTTITLE + 1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldSortTitle, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TV_PLOT + 1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPlot, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TV_STATUS + 1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTvShowStatus, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TV_PREMIERED + 1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldYear, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TV_GENRE + 1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldGenre, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TV_MPAA + 1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldMPAA, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_TV_STUDIOS + 1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldStudio, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_TVSHOW_PATH;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPath, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_TVSHOW_DATEADDED;
+ varindex = DatabaseUtils::GetFieldIndex(FieldDateAdded, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_TVSHOW_NUM_EPISODES;
+ varindex = DatabaseUtils::GetFieldIndex(FieldNumberOfEpisodes, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_TVSHOW_NUM_WATCHED;
+ varindex = DatabaseUtils::GetFieldIndex(FieldNumberOfWatchedEpisodes, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_TVSHOW_NUM_SEASONS;
+ varindex = DatabaseUtils::GetFieldIndex(FieldSeason, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_TVSHOW_USER_RATING;
+ varindex = DatabaseUtils::GetFieldIndex(FieldUserRating, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_TVSHOW_VOTES;
+ varindex = DatabaseUtils::GetFieldIndex(FieldVotes, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_TVSHOW_RATING;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRating, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = -1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRandom, MediaTypeTvShow);
+ EXPECT_EQ(refindex, varindex);
+}
+
+TEST(TestDatabaseUtils, GetFieldIndex_MediaTypeEpisode)
+{
+ int refindex, varindex;
+
+ refindex = 0;
+ varindex = DatabaseUtils::GetFieldIndex(FieldId, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_EPISODE_TITLE + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTitle, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_EPISODE_PLOT + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPlot, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_EPISODE_CREDITS + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldWriter, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_EPISODE_AIRED + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldAirDate, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_EPISODE_RUNTIME + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTime, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_EPISODE_DIRECTOR + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldDirector, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_EPISODE_SEASON + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldSeason, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_ID_EPISODE_EPISODE + 2;
+ varindex = DatabaseUtils::GetFieldIndex(FieldEpisodeNumber, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_FILE;
+ varindex = DatabaseUtils::GetFieldIndex(FieldFilename, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_PATH;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPath, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_PLAYCOUNT;
+ varindex = DatabaseUtils::GetFieldIndex(FieldPlaycount, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_LASTPLAYED;
+ varindex = DatabaseUtils::GetFieldIndex(FieldLastPlayed, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_DATEADDED;
+ varindex = DatabaseUtils::GetFieldIndex(FieldDateAdded, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_TVSHOW_NAME;
+ varindex = DatabaseUtils::GetFieldIndex(FieldTvShowTitle, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_TVSHOW_STUDIO;
+ varindex = DatabaseUtils::GetFieldIndex(FieldStudio, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_TVSHOW_AIRED;
+ varindex = DatabaseUtils::GetFieldIndex(FieldYear, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_TVSHOW_MPAA;
+ varindex = DatabaseUtils::GetFieldIndex(FieldMPAA, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_USER_RATING;
+ varindex = DatabaseUtils::GetFieldIndex(FieldUserRating, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_VOTES;
+ varindex = DatabaseUtils::GetFieldIndex(FieldVotes, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = VIDEODB_DETAILS_EPISODE_RATING;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRating, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+
+ refindex = -1;
+ varindex = DatabaseUtils::GetFieldIndex(FieldRandom, MediaTypeEpisode);
+ EXPECT_EQ(refindex, varindex);
+}
+
+TEST(TestDatabaseUtils, GetSelectFields)
+{
+ Fields fields;
+ FieldList fieldlist;
+
+ EXPECT_FALSE(DatabaseUtils::GetSelectFields(fields, MediaTypeAlbum,
+ fieldlist));
+
+ fields.insert(FieldId);
+ fields.insert(FieldGenre);
+ fields.insert(FieldAlbum);
+ fields.insert(FieldArtist);
+ fields.insert(FieldTitle);
+ EXPECT_FALSE(DatabaseUtils::GetSelectFields(fields, MediaTypeNone,
+ fieldlist));
+ EXPECT_TRUE(DatabaseUtils::GetSelectFields(fields, MediaTypeAlbum,
+ fieldlist));
+ EXPECT_FALSE(fieldlist.empty());
+}
+
+TEST(TestDatabaseUtils, GetFieldValue)
+{
+ CVariant v_null, v_string;
+ dbiplus::field_value f_null, f_string("test");
+
+ f_null.set_isNull();
+ EXPECT_TRUE(DatabaseUtils::GetFieldValue(f_null, v_null));
+ EXPECT_TRUE(v_null.isNull());
+
+ EXPECT_TRUE(DatabaseUtils::GetFieldValue(f_string, v_string));
+ EXPECT_FALSE(v_string.isNull());
+ EXPECT_TRUE(v_string.isString());
+}
+
+//! @todo Need some way to test this function
+// TEST(TestDatabaseUtils, GetDatabaseResults)
+// {
+// static bool GetDatabaseResults(MediaType mediaType, const FieldList &fields,
+// const std::unique_ptr<dbiplus::Dataset> &dataset,
+// DatabaseResults &results);
+// }
+
+TEST(TestDatabaseUtils, BuildLimitClause)
+{
+ std::string a = DatabaseUtils::BuildLimitClause(100);
+ EXPECT_STREQ(" LIMIT 100", a.c_str());
+}
+
+// class DatabaseUtils
+// {
+// public:
+//
+//
+// static std::string BuildLimitClause(int end, int start = 0);
+// };
diff --git a/xbmc/utils/test/TestDigest.cpp b/xbmc/utils/test/TestDigest.cpp
new file mode 100644
index 0000000..96d0529
--- /dev/null
+++ b/xbmc/utils/test/TestDigest.cpp
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 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 "utils/Digest.h"
+
+#include <gtest/gtest.h>
+
+using KODI::UTILITY::CDigest;
+using KODI::UTILITY::TypedDigest;
+
+TEST(TestDigest, Digest_Empty)
+{
+ EXPECT_STREQ(CDigest::Calculate(CDigest::Type::MD5, "").c_str(), "d41d8cd98f00b204e9800998ecf8427e");
+ EXPECT_STREQ(CDigest::Calculate(CDigest::Type::MD5, nullptr, 0).c_str(), "d41d8cd98f00b204e9800998ecf8427e");
+ {
+ CDigest digest{CDigest::Type::MD5};
+ EXPECT_STREQ(digest.Finalize().c_str(), "d41d8cd98f00b204e9800998ecf8427e");
+ }
+ {
+ CDigest digest{CDigest::Type::MD5};
+ digest.Update("");
+ digest.Update(nullptr, 0);
+ EXPECT_STREQ(digest.Finalize().c_str(), "d41d8cd98f00b204e9800998ecf8427e");
+ }
+}
+
+TEST(TestDigest, Digest_Basic)
+{
+ EXPECT_STREQ(CDigest::Calculate(CDigest::Type::MD5, "asdf").c_str(), "912ec803b2ce49e4a541068d495ab570");
+ EXPECT_STREQ(CDigest::Calculate(CDigest::Type::MD5, "asdf", 4).c_str(), "912ec803b2ce49e4a541068d495ab570");
+ {
+ CDigest digest{CDigest::Type::MD5};
+ digest.Update("as");
+ digest.Update("df", 2);
+ EXPECT_STREQ(digest.Finalize().c_str(), "912ec803b2ce49e4a541068d495ab570");
+ }
+}
+
+TEST(TestDigest, Digest_SHA1)
+{
+ EXPECT_STREQ(CDigest::Calculate(CDigest::Type::SHA1, "").c_str(), "da39a3ee5e6b4b0d3255bfef95601890afd80709");
+ EXPECT_STREQ(CDigest::Calculate(CDigest::Type::SHA1, "asdf").c_str(), "3da541559918a808c2402bba5012f6c60b27661c");
+}
+
+TEST(TestDigest, Digest_SHA256)
+{
+ EXPECT_STREQ(CDigest::Calculate(CDigest::Type::SHA256, "").c_str(), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
+ EXPECT_STREQ(CDigest::Calculate(CDigest::Type::SHA256, "asdf").c_str(), "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b");
+}
+
+TEST(TestDigest, Digest_SHA512)
+{
+ EXPECT_STREQ(CDigest::Calculate(CDigest::Type::SHA512, "").c_str(), "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e");
+ EXPECT_STREQ(CDigest::Calculate(CDigest::Type::SHA512, "asdf").c_str(), "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1");
+}
+
+TEST(TestDigest, TypedDigest_Empty)
+{
+ TypedDigest t1, t2;
+ EXPECT_EQ(t1, t2);
+ EXPECT_EQ(t1.type, CDigest::Type::INVALID);
+ EXPECT_EQ(t1.value, "");
+ EXPECT_TRUE(t1.Empty());
+ t1.type = CDigest::Type::SHA1;
+ EXPECT_TRUE(t1.Empty());
+}
+
+TEST(TestDigest, TypedDigest_SameType)
+{
+ TypedDigest t1{CDigest::Type::SHA1, "da39a3ee5e6b4b0d3255bfef95601890afd80709"};
+ TypedDigest t2{CDigest::Type::SHA1, "da39a3ee5e6b4b0d3255bfef95601890afd80708"};
+ EXPECT_NE(t1, t2);
+ EXPECT_FALSE(t1.Empty());
+}
+
+TEST(TestDigest, TypedDigest_CompareCase)
+{
+ TypedDigest t1{CDigest::Type::SHA1, "da39a3ee5e6b4b0d3255bfef95601890afd80708"};
+ TypedDigest t2{CDigest::Type::SHA1, "da39A3EE5e6b4b0d3255bfef95601890afd80708"};
+ EXPECT_EQ(t1, t2);
+}
+
+TEST(TestDigest, TypedDigest_DifferingType)
+{
+ TypedDigest t1{CDigest::Type::SHA1, "da39a3ee5e6b4b0d3255bfef95601890afd80709"};
+ TypedDigest t2{CDigest::Type::SHA256, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"};
+ // Silence "unused expression" warning
+ bool a;
+ EXPECT_THROW(a = (t1 == t2), std::logic_error);
+ // Silence "unused variable" warning
+ (void)a;
+ EXPECT_THROW(a = (t1 != t2), std::logic_error);
+ (void)a;
+}
diff --git a/xbmc/utils/test/TestEndianSwap.cpp b/xbmc/utils/test/TestEndianSwap.cpp
new file mode 100644
index 0000000..70d3cf0
--- /dev/null
+++ b/xbmc/utils/test/TestEndianSwap.cpp
@@ -0,0 +1,133 @@
+/*
+ * 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 "utils/EndianSwap.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestEndianSwap, Endian_Swap16)
+{
+ uint16_t ref, var;
+ ref = 0x00FF;
+ var = Endian_Swap16(0xFF00);
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_Swap32)
+{
+ uint32_t ref, var;
+ ref = 0x00FF00FF;
+ var = Endian_Swap32(0xFF00FF00);
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_Swap64)
+{
+ uint64_t ref, var;
+ ref = UINT64_C(0x00FF00FF00FF00FF);
+ var = Endian_Swap64(UINT64_C(0xFF00FF00FF00FF00));
+ EXPECT_EQ(ref, var);
+}
+
+#ifndef WORDS_BIGENDIAN
+TEST(TestEndianSwap, Endian_SwapLE16)
+{
+ uint16_t ref, var;
+ ref = 0x00FF;
+ var = Endian_SwapLE16(0x00FF);
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_SwapLE32)
+{
+ uint32_t ref, var;
+ ref = 0x00FF00FF;
+ var = Endian_SwapLE32(0x00FF00FF);
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_SwapLE64)
+{
+ uint64_t ref, var;
+ ref = UINT64_C(0x00FF00FF00FF00FF);
+ var = Endian_SwapLE64(UINT64_C(0x00FF00FF00FF00FF));
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_SwapBE16)
+{
+ uint16_t ref, var;
+ ref = 0x00FF;
+ var = Endian_SwapBE16(0xFF00);
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_SwapBE32)
+{
+ uint32_t ref, var;
+ ref = 0x00FF00FF;
+ var = Endian_SwapBE32(0xFF00FF00);
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_SwapBE64)
+{
+ uint64_t ref, var;
+ ref = UINT64_C(0x00FF00FF00FF00FF);
+ var = Endian_SwapBE64(UINT64_C(0xFF00FF00FF00FF00));
+ EXPECT_EQ(ref, var);
+}
+#else
+TEST(TestEndianSwap, Endian_SwapLE16)
+{
+ uint16_t ref, var;
+ ref = 0x00FF;
+ var = Endian_SwapLE16(0xFF00);
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_SwapLE32)
+{
+ uint32_t ref, var;
+ ref = 0x00FF00FF;
+ var = Endian_SwapLE32(0xFF00FF00);
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_SwapLE64)
+{
+ uint64_t ref, var;
+ ref = UINT64_C(0x00FF00FF00FF00FF);
+ var = Endian_SwapLE64(UINT64_C(0xFF00FF00FF00FF00));
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_SwapBE16)
+{
+ uint16_t ref, var;
+ ref = 0x00FF;
+ var = Endian_SwapBE16(0x00FF);
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_SwapBE32)
+{
+ uint32_t ref, var;
+ ref = 0x00FF00FF;
+ var = Endian_SwapBE32(0x00FF00FF);
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestEndianSwap, Endian_SwapBE64)
+{
+ uint64_t ref, var;
+ ref = UINT64_C(0x00FF00FF00FF00FF);
+ var = Endian_SwapBE64(UINT64_C(0x00FF00FF00FF00FF));
+ EXPECT_EQ(ref, var);
+}
+#endif
diff --git a/xbmc/utils/test/TestExecString.cpp b/xbmc/utils/test/TestExecString.cpp
new file mode 100644
index 0000000..4577b87
--- /dev/null
+++ b/xbmc/utils/test/TestExecString.cpp
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2022 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 "FileItem.h"
+#include "utils/ExecString.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestExecString, ctor_1)
+{
+ {
+ const CExecString exec("ActivateWindow(Video, \"C:\\test\\foo\")");
+ EXPECT_EQ(exec.IsValid(), true);
+ EXPECT_EQ(exec.GetFunction(), "activatewindow");
+ EXPECT_EQ(exec.GetParams().size(), 2U);
+ EXPECT_EQ(exec.GetParams()[0], "Video");
+ EXPECT_EQ(exec.GetParams()[1], "C:\\test\\foo");
+ EXPECT_EQ(exec.GetExecString(), "ActivateWindow(Video, \"C:\\test\\foo\")");
+ }
+ {
+ const CExecString exec("ActivateWindow(Video, \"C:\\test\\foo\\\")");
+ EXPECT_EQ(exec.IsValid(), true);
+ EXPECT_EQ(exec.GetFunction(), "activatewindow");
+ EXPECT_EQ(exec.GetParams().size(), 2U);
+ EXPECT_EQ(exec.GetParams()[0], "Video");
+ EXPECT_EQ(exec.GetParams()[1], "C:\\test\\foo");
+ EXPECT_EQ(exec.GetExecString(), "ActivateWindow(Video, \"C:\\test\\foo\\\")");
+ }
+ {
+ const CExecString exec("ActivateWindow(Video, \"C:\\\\test\\\\foo\\\\\")");
+ EXPECT_EQ(exec.IsValid(), true);
+ EXPECT_EQ(exec.GetFunction(), "activatewindow");
+ EXPECT_EQ(exec.GetParams().size(), 2U);
+ EXPECT_EQ(exec.GetParams()[0], "Video");
+ EXPECT_EQ(exec.GetParams()[1], "C:\\test\\foo\\");
+ EXPECT_EQ(exec.GetExecString(), "ActivateWindow(Video, \"C:\\\\test\\\\foo\\\\\")");
+ }
+ {
+ const CExecString exec("ActivateWindow(Video, \"C:\\\\\\\\test\\\\\\foo\\\\\")");
+ EXPECT_EQ(exec.IsValid(), true);
+ EXPECT_EQ(exec.GetFunction(), "activatewindow");
+ EXPECT_EQ(exec.GetParams().size(), 2U);
+ EXPECT_EQ(exec.GetParams()[0], "Video");
+ EXPECT_EQ(exec.GetParams()[1], "C:\\\\test\\\\foo\\");
+ EXPECT_EQ(exec.GetExecString(), "ActivateWindow(Video, \"C:\\\\\\\\test\\\\\\foo\\\\\")");
+ }
+ {
+ const CExecString exec("SetProperty(Foo,\"\")");
+ EXPECT_EQ(exec.IsValid(), true);
+ EXPECT_EQ(exec.GetFunction(), "setproperty");
+ EXPECT_EQ(exec.GetParams().size(), 2U);
+ EXPECT_EQ(exec.GetParams()[0], "Foo");
+ EXPECT_EQ(exec.GetParams()[1], "");
+ EXPECT_EQ(exec.GetExecString(), "SetProperty(Foo,\"\")");
+ }
+ {
+ const CExecString exec("SetProperty(foo,ba(\"ba black )\",sheep))");
+ EXPECT_EQ(exec.IsValid(), true);
+ EXPECT_EQ(exec.GetFunction(), "setproperty");
+ EXPECT_EQ(exec.GetParams().size(), 2U);
+ EXPECT_EQ(exec.GetParams()[0], "foo");
+ EXPECT_EQ(exec.GetParams()[1], "ba(\"ba black )\",sheep)");
+ EXPECT_EQ(exec.GetExecString(), "SetProperty(foo,ba(\"ba black )\",sheep))");
+ }
+}
+
+TEST(TestExecString, ctor_2)
+{
+ {
+ const CExecString exec("ActivateWindow", {"Video", "C:\\test\\foo"});
+ EXPECT_EQ(exec.IsValid(), true);
+ EXPECT_EQ(exec.GetFunction(), "activatewindow");
+ EXPECT_EQ(exec.GetParams().size(), 2U);
+ EXPECT_EQ(exec.GetParams()[0], "Video");
+ EXPECT_EQ(exec.GetParams()[1], "C:\\test\\foo");
+ EXPECT_EQ(exec.GetExecString(), "ActivateWindow(Video,C:\\test\\foo)");
+ }
+}
+
+TEST(TestExecString, ctor_3)
+{
+ {
+ const CFileItem item("C:\\test\\foo", true);
+ const CExecString exec(item, "Video");
+ EXPECT_EQ(exec.IsValid(), true);
+ EXPECT_EQ(exec.GetFunction(), "activatewindow");
+ EXPECT_EQ(exec.GetParams().size(), 3U);
+ EXPECT_EQ(exec.GetParams()[0], "Video");
+ EXPECT_EQ(exec.GetParams()[1], "\"C:\\\\test\\\\foo\\\\\"");
+ EXPECT_EQ(exec.GetParams()[2], "return");
+ EXPECT_EQ(exec.GetExecString(), "ActivateWindow(Video,\"C:\\\\test\\\\foo\\\\\",return)");
+ }
+ {
+ const CFileItem item("C:\\test\\foo\\", true);
+ const CExecString exec(item, "Video");
+ EXPECT_EQ(exec.IsValid(), true);
+ EXPECT_EQ(exec.GetFunction(), "activatewindow");
+ EXPECT_EQ(exec.GetParams().size(), 3U);
+ EXPECT_EQ(exec.GetParams()[0], "Video");
+ EXPECT_EQ(exec.GetParams()[1], "\"C:\\\\test\\\\foo\\\\\"");
+ EXPECT_EQ(exec.GetParams()[2], "return");
+ EXPECT_EQ(exec.GetExecString(), "ActivateWindow(Video,\"C:\\\\test\\\\foo\\\\\",return)");
+ }
+ {
+ const CFileItem item("C:\\test\\foo", false);
+ const CExecString exec(item, "Video");
+ EXPECT_EQ(exec.IsValid(), true);
+ EXPECT_EQ(exec.GetFunction(), "playmedia");
+ EXPECT_EQ(exec.GetParams().size(), 1U);
+ EXPECT_EQ(exec.GetParams()[0], "\"C:\\\\test\\\\foo\"");
+ EXPECT_EQ(exec.GetExecString(), "PlayMedia(\"C:\\\\test\\\\foo\")");
+ }
+}
diff --git a/xbmc/utils/test/TestFileOperationJob.cpp b/xbmc/utils/test/TestFileOperationJob.cpp
new file mode 100644
index 0000000..1df243e
--- /dev/null
+++ b/xbmc/utils/test/TestFileOperationJob.cpp
@@ -0,0 +1,292 @@
+/*
+ * 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 "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "test/TestUtils.h"
+#include "utils/FileOperationJob.h"
+#include "utils/URIUtils.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestFileOperationJob, ActionCopy)
+{
+ XFILE::CFile *tmpfile;
+ std::string tmpfilepath, destfile;
+ CFileItemList items;
+ CFileOperationJob job;
+
+ ASSERT_NE(nullptr, (tmpfile = XBMC_CREATETEMPFILE("")));
+ tmpfilepath = XBMC_TEMPFILEPATH(tmpfile);
+ tmpfile->Close();
+
+ CFileItemPtr item(new CFileItem(tmpfilepath));
+ item->SetPath(tmpfilepath);
+ item->m_bIsFolder = false;
+ item->Select(true);
+ items.Add(item);
+
+ std::string destpath = URIUtils::GetDirectory(tmpfilepath);
+ destpath = URIUtils::AddFileToFolder(destpath, "copy");
+ destfile = URIUtils::AddFileToFolder(destpath, URIUtils::GetFileName(tmpfilepath));
+ ASSERT_FALSE(XFILE::CFile::Exists(destfile));
+
+ job.SetFileOperation(CFileOperationJob::ActionCopy, items, destpath);
+ EXPECT_EQ(CFileOperationJob::ActionCopy, job.GetAction());
+
+ EXPECT_TRUE(job.DoWork());
+ EXPECT_TRUE(XFILE::CFile::Exists(tmpfilepath));
+ EXPECT_TRUE(XFILE::CFile::Exists(destfile));
+
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(tmpfile));
+ EXPECT_TRUE(XFILE::CFile::Delete(destfile));
+ EXPECT_TRUE(XFILE::CDirectory::Remove(destpath));
+}
+
+TEST(TestFileOperationJob, ActionMove)
+{
+ XFILE::CFile *tmpfile;
+ std::string tmpfilepath, destfile;
+ CFileItemList items;
+ CFileOperationJob job;
+
+ ASSERT_NE(nullptr, (tmpfile = XBMC_CREATETEMPFILE("")));
+ tmpfilepath = XBMC_TEMPFILEPATH(tmpfile);
+ tmpfile->Close();
+
+ CFileItemPtr item(new CFileItem(tmpfilepath));
+ item->SetPath(tmpfilepath);
+ item->m_bIsFolder = false;
+ item->Select(true);
+ items.Add(item);
+
+ std::string destpath = URIUtils::GetDirectory(tmpfilepath);
+ destpath = URIUtils::AddFileToFolder(destpath, "move");
+ destfile = URIUtils::AddFileToFolder(destpath, URIUtils::GetFileName(tmpfilepath));
+ ASSERT_FALSE(XFILE::CFile::Exists(destfile));
+ ASSERT_TRUE(XFILE::CDirectory::Create(destpath));
+
+ job.SetFileOperation(CFileOperationJob::ActionMove, items, destpath);
+ EXPECT_EQ(CFileOperationJob::ActionMove, job.GetAction());
+
+ EXPECT_TRUE(job.DoWork());
+ EXPECT_FALSE(XFILE::CFile::Exists(tmpfilepath));
+ EXPECT_TRUE(XFILE::CFile::Exists(destfile));
+
+ EXPECT_TRUE(XFILE::CFile::Delete(destfile));
+ EXPECT_TRUE(XFILE::CDirectory::Remove(destpath));
+
+ EXPECT_FALSE(XBMC_DELETETEMPFILE(tmpfile));
+}
+
+TEST(TestFileOperationJob, ActionDelete)
+{
+ XFILE::CFile *tmpfile;
+ std::string tmpfilepath, destfile;
+ CFileItemList items;
+ CFileOperationJob job;
+
+ ASSERT_NE(nullptr, (tmpfile = XBMC_CREATETEMPFILE("")));
+ tmpfilepath = XBMC_TEMPFILEPATH(tmpfile);
+ tmpfile->Close();
+
+ CFileItemPtr item(new CFileItem(tmpfilepath));
+ item->SetPath(tmpfilepath);
+ item->m_bIsFolder = false;
+ item->Select(true);
+ items.Add(item);
+
+ std::string destpath = URIUtils::GetDirectory(tmpfilepath);
+ destpath = URIUtils::AddFileToFolder(destpath, "delete");
+ destfile = URIUtils::AddFileToFolder(destpath, URIUtils::GetFileName(tmpfilepath));
+ ASSERT_FALSE(XFILE::CFile::Exists(destfile));
+
+ job.SetFileOperation(CFileOperationJob::ActionCopy, items, destpath);
+ EXPECT_EQ(CFileOperationJob::ActionCopy, job.GetAction());
+
+ EXPECT_TRUE(job.DoWork());
+ EXPECT_TRUE(XFILE::CFile::Exists(tmpfilepath));
+ EXPECT_TRUE(XFILE::CFile::Exists(destfile));
+
+ job.SetFileOperation(CFileOperationJob::ActionDelete, items, "");
+ EXPECT_EQ(CFileOperationJob::ActionDelete, job.GetAction());
+
+ EXPECT_TRUE(job.DoWork());
+ EXPECT_FALSE(XFILE::CFile::Exists(tmpfilepath));
+
+ items.Clear();
+ CFileItemPtr item2(new CFileItem(destfile));
+ item2->SetPath(destfile);
+ item2->m_bIsFolder = false;
+ item2->Select(true);
+ items.Add(item2);
+
+ job.SetFileOperation(CFileOperationJob::ActionDelete, items, "");
+ EXPECT_EQ(CFileOperationJob::ActionDelete, job.GetAction());
+
+ EXPECT_TRUE(job.DoWork());
+ EXPECT_FALSE(XFILE::CFile::Exists(destfile));
+ EXPECT_TRUE(XFILE::CDirectory::Remove(destpath));
+
+ EXPECT_FALSE(XBMC_DELETETEMPFILE(tmpfile));
+}
+
+TEST(TestFileOperationJob, ActionReplace)
+{
+ XFILE::CFile *tmpfile;
+ std::string tmpfilepath, destfile;
+ CFileItemList items;
+ CFileOperationJob job;
+
+ ASSERT_NE(nullptr, (tmpfile = XBMC_CREATETEMPFILE("")));
+ tmpfilepath = XBMC_TEMPFILEPATH(tmpfile);
+ tmpfile->Close();
+
+ CFileItemPtr item(new CFileItem(tmpfilepath));
+ item->SetPath(tmpfilepath);
+ item->m_bIsFolder = false;
+ item->Select(true);
+ items.Add(item);
+
+ std::string destpath = URIUtils::GetDirectory(tmpfilepath);
+ destpath = URIUtils::AddFileToFolder(destpath, "replace");
+ destfile = URIUtils::AddFileToFolder(destpath, URIUtils::GetFileName(tmpfilepath));
+ ASSERT_FALSE(XFILE::CFile::Exists(destfile));
+
+ job.SetFileOperation(CFileOperationJob::ActionCopy, items, destpath);
+ EXPECT_EQ(CFileOperationJob::ActionCopy, job.GetAction());
+
+ EXPECT_TRUE(job.DoWork());
+ EXPECT_TRUE(XFILE::CFile::Exists(tmpfilepath));
+ EXPECT_TRUE(XFILE::CFile::Exists(destfile));
+
+ job.SetFileOperation(CFileOperationJob::ActionReplace, items, destpath);
+ EXPECT_EQ(CFileOperationJob::ActionReplace, job.GetAction());
+
+ EXPECT_TRUE(job.DoWork());
+ EXPECT_TRUE(XFILE::CFile::Exists(destfile));
+
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(tmpfile));
+ EXPECT_TRUE(XFILE::CFile::Delete(destfile));
+ EXPECT_TRUE(XFILE::CDirectory::Remove(destpath));
+}
+
+TEST(TestFileOperationJob, ActionCreateFolder)
+{
+ XFILE::CFile *tmpfile;
+ std::string tmpfilepath, destpath;
+ CFileItemList items;
+ CFileOperationJob job;
+
+ ASSERT_NE(nullptr, (tmpfile = XBMC_CREATETEMPFILE("")));
+ tmpfilepath = XBMC_TEMPFILEPATH(tmpfile);
+
+ std::string tmpfiledirectory =
+ CXBMCTestUtils::Instance().TempFileDirectory(tmpfile);
+
+ tmpfile->Close();
+
+ destpath = tmpfilepath;
+ destpath += ".createfolder";
+ ASSERT_FALSE(XFILE::CFile::Exists(destpath));
+
+ CFileItemPtr item(new CFileItem(destpath));
+ item->SetPath(destpath);
+ item->m_bIsFolder = true;
+ item->Select(true);
+ items.Add(item);
+
+ job.SetFileOperation(CFileOperationJob::ActionCreateFolder, items, tmpfiledirectory);
+ EXPECT_EQ(CFileOperationJob::ActionCreateFolder, job.GetAction());
+
+ EXPECT_TRUE(job.DoWork());
+ EXPECT_TRUE(XFILE::CDirectory::Exists(destpath));
+
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(tmpfile));
+ EXPECT_TRUE(XFILE::CDirectory::Remove(destpath));
+}
+
+// This test will fail until ActionDeleteFolder has a proper implementation
+TEST(TestFileOperationJob, ActionDeleteFolder)
+{
+ XFILE::CFile *tmpfile;
+ std::string tmpfilepath, destpath;
+ CFileItemList items;
+ CFileOperationJob job;
+
+ ASSERT_NE(nullptr, (tmpfile = XBMC_CREATETEMPFILE("")));
+ tmpfilepath = XBMC_TEMPFILEPATH(tmpfile);
+
+ std::string tmpfiledirectory =
+ CXBMCTestUtils::Instance().TempFileDirectory(tmpfile);
+
+ tmpfile->Close();
+
+ destpath = tmpfilepath;
+ destpath += ".deletefolder";
+ ASSERT_FALSE(XFILE::CFile::Exists(destpath));
+
+ CFileItemPtr item(new CFileItem(destpath));
+ item->SetPath(destpath);
+ item->m_bIsFolder = true;
+ item->Select(true);
+ items.Add(item);
+
+ job.SetFileOperation(CFileOperationJob::ActionCreateFolder, items, tmpfiledirectory);
+ EXPECT_EQ(CFileOperationJob::ActionCreateFolder, job.GetAction());
+
+ EXPECT_TRUE(job.DoWork());
+ EXPECT_TRUE(XFILE::CDirectory::Exists(destpath));
+
+ job.SetFileOperation(CFileOperationJob::ActionDeleteFolder, items, tmpfiledirectory);
+ EXPECT_EQ(CFileOperationJob::ActionDeleteFolder, job.GetAction());
+
+ EXPECT_TRUE(job.DoWork());
+ EXPECT_FALSE(XFILE::CDirectory::Exists(destpath));
+
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(tmpfile));
+}
+
+TEST(TestFileOperationJob, GetFunctions)
+{
+ XFILE::CFile *tmpfile;
+ std::string tmpfilepath, destfile;
+ CFileItemList items;
+ CFileOperationJob job;
+
+ ASSERT_NE(nullptr, (tmpfile = XBMC_CREATETEMPFILE("")));
+ tmpfilepath = XBMC_TEMPFILEPATH(tmpfile);
+ tmpfile->Close();
+
+ CFileItemPtr item(new CFileItem(tmpfilepath));
+ item->SetPath(tmpfilepath);
+ item->m_bIsFolder = false;
+ item->Select(true);
+ items.Add(item);
+
+ std::string destpath = URIUtils::GetDirectory(tmpfilepath);
+ destpath = URIUtils::AddFileToFolder(destpath, "getfunctions");
+ destfile = URIUtils::AddFileToFolder(destpath, URIUtils::GetFileName(tmpfilepath));
+ ASSERT_FALSE(XFILE::CFile::Exists(destfile));
+
+ job.SetFileOperation(CFileOperationJob::ActionCopy, items, destpath);
+ EXPECT_EQ(CFileOperationJob::ActionCopy, job.GetAction());
+
+ EXPECT_TRUE(job.DoWork());
+ EXPECT_TRUE(XFILE::CFile::Exists(tmpfilepath));
+ EXPECT_TRUE(XFILE::CFile::Exists(destfile));
+
+ std::cout << "GetAverageSpeed(): " << job.GetAverageSpeed() << std::endl;
+ std::cout << "GetCurrentOperation(): " << job.GetCurrentOperation() << std::endl;
+ std::cout << "GetCurrentFile(): " << job.GetCurrentFile() << std::endl;
+ EXPECT_FALSE(job.GetItems().IsEmpty());
+
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(tmpfile));
+ EXPECT_TRUE(XFILE::CFile::Delete(destfile));
+ EXPECT_TRUE(XFILE::CDirectory::Remove(destpath));
+}
diff --git a/xbmc/utils/test/TestFileUtils.cpp b/xbmc/utils/test/TestFileUtils.cpp
new file mode 100644
index 0000000..bb9b8b6
--- /dev/null
+++ b/xbmc/utils/test/TestFileUtils.cpp
@@ -0,0 +1,44 @@
+/*
+ * 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 "FileItem.h"
+#include "filesystem/File.h"
+#include "test/TestUtils.h"
+#include "utils/FileUtils.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestFileUtils, DeleteItem_CFileItemPtr)
+{
+ XFILE::CFile *tmpfile;
+ std::string tmpfilepath;
+
+ ASSERT_NE(nullptr, (tmpfile = XBMC_CREATETEMPFILE("")));
+ tmpfilepath = XBMC_TEMPFILEPATH(tmpfile);
+
+ CFileItemPtr item(new CFileItem(tmpfilepath));
+ item->SetPath(tmpfilepath);
+ item->m_bIsFolder = false;
+ item->Select(true);
+ tmpfile->Close(); //Close tmpfile before we try to delete it
+ EXPECT_TRUE(CFileUtils::DeleteItem(item));
+ EXPECT_FALSE(XBMC_DELETETEMPFILE(tmpfile));
+}
+
+TEST(TestFileUtils, DeleteItemString)
+{
+ XFILE::CFile *tmpfile;
+
+ ASSERT_NE(nullptr, (tmpfile = XBMC_CREATETEMPFILE("")));
+ tmpfile->Close(); //Close tmpfile before we try to delete it
+ EXPECT_TRUE(CFileUtils::DeleteItem(XBMC_TEMPFILEPATH(tmpfile)));
+ EXPECT_FALSE(XBMC_DELETETEMPFILE(tmpfile));
+}
+
+/* Executing RenameFile() requires input from the user */
+// static bool RenameFile(const std::string &strFile);
diff --git a/xbmc/utils/test/TestGlobalsHandling.cpp b/xbmc/utils/test/TestGlobalsHandling.cpp
new file mode 100644
index 0000000..5b8d26a
--- /dev/null
+++ b/xbmc/utils/test/TestGlobalsHandling.cpp
@@ -0,0 +1,25 @@
+/*
+ * 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 "utils/test/TestGlobalsHandlingPattern1.h"
+
+#include <gtest/gtest.h>
+
+using namespace xbmcutil;
+using namespace test;
+
+bool TestGlobalPattern1::ctorCalled = false;
+bool TestGlobalPattern1::dtorCalled = false;
+
+TEST(TestGlobal, Pattern1)
+{
+ EXPECT_TRUE(TestGlobalPattern1::ctorCalled);
+ {
+ std::shared_ptr<TestGlobalPattern1> ptr = g_testGlobalPattern1Ref;
+ }
+}
diff --git a/xbmc/utils/test/TestGlobalsHandlingPattern1.h b/xbmc/utils/test/TestGlobalsHandlingPattern1.h
new file mode 100644
index 0000000..92088b8
--- /dev/null
+++ b/xbmc/utils/test/TestGlobalsHandlingPattern1.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "utils/GlobalsHandling.h"
+
+#include <iostream>
+
+namespace xbmcutil
+{
+ namespace test
+ {
+ class TestGlobalPattern1
+ {
+ public:
+ static bool ctorCalled;
+ static bool dtorCalled;
+
+ int somethingToAccess = 0;
+
+ TestGlobalPattern1() { ctorCalled = true; }
+ ~TestGlobalPattern1()
+ {
+ std::cout << "Clean shutdown of TestGlobalPattern1" << std::endl << std::flush;
+ dtorCalled = true;
+ }
+
+ void beHappy() { if (somethingToAccess) throw somethingToAccess; }
+ };
+ }
+}
+
+XBMC_GLOBAL_REF(xbmcutil::test::TestGlobalPattern1,g_testGlobalPattern1);
+#define g_testGlobalPattern1 XBMC_GLOBAL_USE(xbmcutil::test::TestGlobalPattern1)
diff --git a/xbmc/utils/test/TestHTMLUtil.cpp b/xbmc/utils/test/TestHTMLUtil.cpp
new file mode 100644
index 0000000..7d0e515
--- /dev/null
+++ b/xbmc/utils/test/TestHTMLUtil.cpp
@@ -0,0 +1,36 @@
+/*
+ * 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 "utils/HTMLUtil.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestHTMLUtil, RemoveTags)
+{
+ std::string str;
+ str = "<!DOCTYPE html>\n"
+ "<html>\n"
+ " <head class=\"someclass\">\n"
+ " <body>\n"
+ " <p>blah blah blah</p>\n"
+ " </body>\n"
+ " </head>\n"
+ "</html>\n";
+ HTML::CHTMLUtil::RemoveTags(str);
+ EXPECT_STREQ("\n\n \n \n blah blah blah\n \n \n\n",
+ str.c_str());
+}
+
+TEST(TestHTMLUtil, ConvertHTMLToW)
+{
+ std::wstring inw, refstrw, varstrw;
+ inw = L"&aring;&amp;&euro;";
+ refstrw = L"\u00e5&\u20ac";
+ HTML::CHTMLUtil::ConvertHTMLToW(inw, varstrw);
+ EXPECT_STREQ(refstrw.c_str(), varstrw.c_str());
+}
diff --git a/xbmc/utils/test/TestHttpHeader.cpp b/xbmc/utils/test/TestHttpHeader.cpp
new file mode 100644
index 0000000..1aeecc7
--- /dev/null
+++ b/xbmc/utils/test/TestHttpHeader.cpp
@@ -0,0 +1,505 @@
+/*
+ * 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 "utils/HttpHeader.h"
+
+#include <string.h>
+
+#include <gtest/gtest.h>
+
+#define CHECK_CNT_TYPE_NAME "Content-Type"
+#define CHECK_CONTENT_TYPE_HTML "text/html"
+#define CHECK_CONTENT_TYPE_HTML_CHRS "text/html; charset=WINDOWS-1251"
+#define CHECK_CONTENT_TYPE_XML_CHRS "text/xml; charset=uTf-8"
+#define CHECK_CONTENT_TYPE_TEXT "text/plain"
+#define CHECK_DATE_NAME "Date"
+#define CHECK_DATE_VALUE1 "Thu, 09 Jan 2014 17:58:30 GMT"
+#define CHECK_DATE_VALUE2 "Thu, 09 Jan 2014 20:21:20 GMT"
+#define CHECK_DATE_VALUE3 "Thu, 09 Jan 2014 20:25:02 GMT"
+#define CHECK_PROT_LINE_200 "HTTP/1.1 200 OK"
+#define CHECK_PROT_LINE_301 "HTTP/1.1 301 Moved Permanently"
+
+#define CHECK_HEADER_SMPL CHECK_PROT_LINE_200 "\r\n" \
+ CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_HTML "\r\n" \
+ "\r\n"
+
+#define CHECK_HEADER_L1 CHECK_PROT_LINE_200 "\r\n" \
+ "Server: nginx/1.4.4\r\n" \
+ CHECK_DATE_NAME ": " CHECK_DATE_VALUE1 "\r\n" \
+ CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_HTML_CHRS "\r\n" \
+ "Transfer-Encoding: chunked\r\n" \
+ "Connection: close\r\n" \
+ "Set-Cookie: PHPSESSID=90857d437518db8f0944ca012761048a; path=/; domain=example.com\r\n" \
+ "Expires: Thu, 19 Nov 1981 08:52:00 GMT\r\n" \
+ "Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\n" \
+ "Pragma: no-cache\r\n" \
+ "Set-Cookie: user_country=ot; expires=Thu, 09-Jan-2014 18:58:30 GMT; path=/; domain=.example.com\r\n" \
+ "\r\n"
+
+#define CHECK_HEADER_R CHECK_PROT_LINE_301 "\r\n" \
+ "Server: nginx/1.4.4\r\n" \
+ CHECK_DATE_NAME ": " CHECK_DATE_VALUE2 "\r\n" \
+ CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_HTML "\r\n" \
+ "Content-Length: 150\r\n" \
+ "Connection: close\r\n" \
+ "Location: http://www.Example.Com\r\n" \
+ "\r\n"
+
+#define CHECK_HEADER_L2 CHECK_PROT_LINE_200 "\r\n" \
+ CHECK_DATE_NAME ": " CHECK_DATE_VALUE3 "\r\n" \
+ "Server: Apache/2.4.7 (Unix) mod_wsgi/3.4 Python/2.7.5 OpenSSL/1.0.1e\r\n" \
+ "Last-Modified: Thu, 09 Jan 2014 20:10:28 GMT\r\n" \
+ "ETag: \"9a97-4ef8f335ebd10\"\r\n" \
+ "Accept-Ranges: bytes\r\n" \
+ "Content-Length: 33355\r\n" \
+ "Vary: Accept-Encoding\r\n" \
+ "Cache-Control: max-age=3600\r\n" \
+ "Expires: Thu, 09 Jan 2014 21:25:02 GMT\r\n" \
+ "Connection: close\r\n" \
+ CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_XML_CHRS "\r\n" \
+ "\r\n"
+
+// local helper function: replace substrings
+std::string strReplace(const std::string& str, const std::string& from, const std::string& to)
+{
+ std::string result;
+ size_t prevPos = 0;
+ size_t pos;
+ const size_t len = str.length();
+
+ do
+ {
+ pos = str.find(from, prevPos);
+ result.append(str, prevPos, pos - prevPos);
+ if (pos >= len)
+ break;
+ result.append(to);
+ prevPos = pos + from.length();
+ } while (true);
+
+ return result;
+}
+
+TEST(TestHttpHeader, General)
+{
+ /* check freshly created object */
+ CHttpHeader testHdr;
+ EXPECT_TRUE(testHdr.GetHeader().empty()) << "Newly created object is not empty";
+ EXPECT_TRUE(testHdr.GetProtoLine().empty()) << "Newly created object has non-empty protocol line";
+ EXPECT_TRUE(testHdr.GetMimeType().empty()) << "Newly created object has non-empty MIME-type";
+ EXPECT_TRUE(testHdr.GetCharset().empty()) << "Newly created object has non-empty charset";
+ EXPECT_TRUE(testHdr.GetValue("foo").empty()) << "Newly created object has some parameter";
+ EXPECT_TRUE(testHdr.GetValues("bar").empty()) << "Newly created object has some parameters";
+ EXPECT_FALSE(testHdr.IsHeaderDone()) << "Newly created object has \"parsing finished\" state";
+
+ /* check general functions in simple case */
+ testHdr.Parse(CHECK_HEADER_SMPL);
+ EXPECT_FALSE(testHdr.GetHeader().empty()) << "Parsed header is empty";
+ EXPECT_FALSE(testHdr.GetProtoLine().empty()) << "Parsed header has empty protocol line";
+ EXPECT_FALSE(testHdr.GetMimeType().empty()) << "Parsed header has empty MIME-type";
+ EXPECT_FALSE(testHdr.GetValue(CHECK_CNT_TYPE_NAME).empty()) << "Parsed header has empty \"" CHECK_CNT_TYPE_NAME "\" parameter";
+ EXPECT_FALSE(testHdr.GetValues(CHECK_CNT_TYPE_NAME).empty()) << "Parsed header has no \"" CHECK_CNT_TYPE_NAME "\" parameters";
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Parsed header has \"parsing not finished\" state";
+
+ /* check clearing of object */
+ testHdr.Clear();
+ EXPECT_TRUE(testHdr.GetHeader().empty()) << "Cleared object is not empty";
+ EXPECT_TRUE(testHdr.GetProtoLine().empty()) << "Cleared object has non-empty protocol line";
+ EXPECT_TRUE(testHdr.GetMimeType().empty()) << "Cleared object has non-empty MIME-type";
+ EXPECT_TRUE(testHdr.GetCharset().empty()) << "Cleared object has non-empty charset";
+ EXPECT_TRUE(testHdr.GetValue(CHECK_CNT_TYPE_NAME).empty()) << "Cleared object has some parameter";
+ EXPECT_TRUE(testHdr.GetValues(CHECK_CNT_TYPE_NAME).empty()) << "Cleared object has some parameters";
+ EXPECT_FALSE(testHdr.IsHeaderDone()) << "Cleared object has \"parsing finished\" state";
+
+ /* check general functions after object clearing */
+ testHdr.Parse(CHECK_HEADER_R);
+ EXPECT_FALSE(testHdr.GetHeader().empty()) << "Parsed header is empty";
+ EXPECT_FALSE(testHdr.GetProtoLine().empty()) << "Parsed header has empty protocol line";
+ EXPECT_FALSE(testHdr.GetMimeType().empty()) << "Parsed header has empty MIME-type";
+ EXPECT_FALSE(testHdr.GetValue(CHECK_CNT_TYPE_NAME).empty()) << "Parsed header has empty \"" CHECK_CNT_TYPE_NAME "\" parameter";
+ EXPECT_FALSE(testHdr.GetValues(CHECK_CNT_TYPE_NAME).empty()) << "Parsed header has no \"" CHECK_CNT_TYPE_NAME "\" parameters";
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Parsed header has \"parsing not finished\" state";
+}
+
+TEST(TestHttpHeader, Parse)
+{
+ CHttpHeader testHdr;
+
+ /* check parsing line-by-line */
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n");
+ EXPECT_FALSE(testHdr.IsHeaderDone()) << "Not completed header has \"parsing finished\" state";
+ testHdr.Parse(CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_HTML "\r\n");
+ EXPECT_FALSE(testHdr.IsHeaderDone()) << "Not completed header has \"parsing finished\" state";
+ testHdr.Parse("\r\n");
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ(CHECK_PROT_LINE_200, testHdr.GetProtoLine().c_str()) << "Wrong protocol line";
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_HTML, testHdr.GetValue(CHECK_CNT_TYPE_NAME).c_str()) << "Wrong value of parameter \"" CHECK_CNT_TYPE_NAME "\"";
+
+ /* check autoclearing when new header is parsed */
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n");
+ EXPECT_FALSE(testHdr.IsHeaderDone()) << "Not completed header has \"parsing finished\" state";
+ EXPECT_TRUE(testHdr.GetValues(CHECK_CNT_TYPE_NAME).empty()) << "Cleared header has some parameters";
+ testHdr.Clear();
+ EXPECT_TRUE(testHdr.GetHeader().empty()) << "Cleared object is not empty";
+
+ /* general check parsing */
+ testHdr.Parse(CHECK_HEADER_SMPL);
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STRCASEEQ(CHECK_HEADER_SMPL, testHdr.GetHeader().c_str()) << "Parsed header mismatch the original header";
+ testHdr.Parse(CHECK_HEADER_L1);
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STRCASEEQ(CHECK_HEADER_L1, testHdr.GetHeader().c_str()) << "Parsed header mismatch the original header";
+ EXPECT_STREQ("Thu, 09 Jan 2014 17:58:30 GMT", testHdr.GetValue("Date").c_str()); // case-sensitive match of value
+ testHdr.Parse(CHECK_HEADER_L2);
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STRCASEEQ(CHECK_HEADER_L2, testHdr.GetHeader().c_str()) << "Parsed header mismatch the original header";
+ EXPECT_STREQ("Thu, 09 Jan 2014 20:10:28 GMT", testHdr.GetValue("Last-Modified").c_str()); // case-sensitive match of value
+ testHdr.Parse(CHECK_HEADER_R);
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STRCASEEQ(CHECK_HEADER_R, testHdr.GetHeader().c_str()) << "Parsed header mismatch the original header";
+ EXPECT_STREQ("http://www.Example.Com", testHdr.GetValue("Location").c_str()); // case-sensitive match of value
+
+ /* check support for '\n' line endings */
+ testHdr.Parse(strReplace(CHECK_HEADER_SMPL, "\r\n", "\n"));
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STRCASEEQ(CHECK_HEADER_SMPL, testHdr.GetHeader().c_str()) << "Parsed header mismatch the original header";
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ testHdr.Parse(strReplace(CHECK_HEADER_L1, "\r\n", "\n"));
+ EXPECT_STRCASEEQ(CHECK_HEADER_L1, testHdr.GetHeader().c_str()) << "Parsed header mismatch the original header";
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ testHdr.Parse(strReplace(CHECK_HEADER_L2, "\r\n", "\n"));
+ EXPECT_STRCASEEQ(CHECK_HEADER_L2, testHdr.GetHeader().c_str()) << "Parsed header mismatch the original header";
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ testHdr.Parse(CHECK_PROT_LINE_200 "\n" CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_HTML "\r\n"); // mixed "\n" and "\r\n"
+ testHdr.Parse("\n");
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STRCASEEQ(CHECK_PROT_LINE_200 "\r\n" CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_HTML "\r\n\r\n", testHdr.GetHeader().c_str()) << "Parsed header mismatch the original header";
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_HTML, testHdr.GetValue(CHECK_CNT_TYPE_NAME).c_str()) << "Wrong value of parameter \"" CHECK_CNT_TYPE_NAME "\"";
+
+ /* check trimming of whitespaces for parameter name and value */
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n" CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_HTML "\r\n\r\n");
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_HTML, testHdr.GetValue(CHECK_CNT_TYPE_NAME).c_str()) << "Wrong value of parameter \"" CHECK_CNT_TYPE_NAME "\"";
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n" CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_HTML " \r\n\r\n");
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_HTML, testHdr.GetValue(CHECK_CNT_TYPE_NAME).c_str()) << "Wrong value of parameter \"" CHECK_CNT_TYPE_NAME "\"";
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n" CHECK_CNT_TYPE_NAME ":" CHECK_CONTENT_TYPE_HTML " \r\n\r\n");
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_HTML, testHdr.GetValue(CHECK_CNT_TYPE_NAME).c_str()) << "Wrong value of parameter \"" CHECK_CNT_TYPE_NAME "\"";
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n" CHECK_CNT_TYPE_NAME ":\t" CHECK_CONTENT_TYPE_HTML " \t \r\n\r\n");
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_HTML, testHdr.GetValue(CHECK_CNT_TYPE_NAME).c_str()) << "Wrong value of parameter \"" CHECK_CNT_TYPE_NAME "\"";
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n" CHECK_CNT_TYPE_NAME ":\t " CHECK_CONTENT_TYPE_HTML " \t \r\n\r\n");
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_HTML, testHdr.GetValue(CHECK_CNT_TYPE_NAME).c_str()) << "Wrong value of parameter \"" CHECK_CNT_TYPE_NAME "\"";
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n" CHECK_CNT_TYPE_NAME "\t:" CHECK_CONTENT_TYPE_HTML " \t \r\n\r\n");
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_HTML, testHdr.GetValue(CHECK_CNT_TYPE_NAME).c_str()) << "Wrong value of parameter \"" CHECK_CNT_TYPE_NAME "\"";
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n" CHECK_CNT_TYPE_NAME " \t : " CHECK_CONTENT_TYPE_HTML " \t \r\n\r\n");
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_HTML, testHdr.GetValue(CHECK_CNT_TYPE_NAME).c_str()) << "Wrong value of parameter \"" CHECK_CNT_TYPE_NAME "\"";
+}
+
+TEST(TestHttpHeader, Parse_Multiline)
+{
+ CHttpHeader testHdr;
+
+ /* Check multiline parameter parsing line-by-line */
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n");
+ testHdr.Parse(CHECK_DATE_NAME ": " CHECK_DATE_VALUE3 "\r\n");
+ testHdr.Parse("X-Comment: This\r\n"); // between singleline parameters
+ testHdr.Parse(" is\r\n");
+ testHdr.Parse(" multi\r\n");
+ testHdr.Parse(" line\r\n");
+ testHdr.Parse(" value\r\n");
+ testHdr.Parse(CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_TEXT "\r\n");
+ testHdr.Parse("\r\n");
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is multi line value", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n");
+ testHdr.Parse("X-Comment: This\r\n"); // first parameter
+ testHdr.Parse(" is\r\n");
+ testHdr.Parse(" multi\r\n");
+ testHdr.Parse(" line\r\n");
+ testHdr.Parse(" value\r\n");
+ testHdr.Parse(CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_TEXT "\r\n");
+ testHdr.Parse("\r\n");
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is multi line value", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n");
+ testHdr.Parse(CHECK_DATE_NAME ": " CHECK_DATE_VALUE3 "\r\n");
+ testHdr.Parse("X-Comment: This\r\n"); // last parameter
+ testHdr.Parse(" is\r\n");
+ testHdr.Parse(" multi\r\n");
+ testHdr.Parse(" line\r\n");
+ testHdr.Parse(" value\r\n");
+ testHdr.Parse("\r\n");
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is multi line value", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n");
+ testHdr.Parse("X-Comment: This\r\n"); // the only parameter
+ testHdr.Parse(" is\r\n");
+ testHdr.Parse(" multi\r\n");
+ testHdr.Parse(" line\r\n");
+ testHdr.Parse(" value\r\n");
+ testHdr.Parse("\r\n");
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is multi line value", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n");
+ testHdr.Parse("X-Comment: This\n"); // the only parameter with mixed ending style
+ testHdr.Parse(" is\r\n");
+ testHdr.Parse(" multi\n");
+ testHdr.Parse(" line\r\n");
+ testHdr.Parse(" value\n");
+ testHdr.Parse("\r\n");
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is multi line value", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ /* Check multiline parameter parsing as one line */
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n" CHECK_DATE_NAME ": " CHECK_DATE_VALUE3 "\r\nX-Comment: This\r\n is\r\n multi\r\n line\r\n value\r\n" \
+ CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_TEXT "\r\n\r\n"); // between singleline parameters
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is multi line value", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\nX-Comment: This\r\n is\r\n multi\r\n line\r\n value\r\n" \
+ CHECK_CNT_TYPE_NAME ": " CHECK_CONTENT_TYPE_TEXT "\r\n\r\n"); // first parameter
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is multi line value", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n" CHECK_DATE_NAME ": " CHECK_DATE_VALUE3 "\r\nX-Comment: This\r\n is\r\n multi\r\n line\r\n value\r\n\r\n"); // last parameter
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is multi line value", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\nX-Comment: This\r\n is\r\n multi\r\n line\r\n value\r\n\r\n"); // the only parameter
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is multi line value", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\nX-Comment: This\n is\r\n multi\r\n line\n value\r\n\n"); // the only parameter with mixed ending style
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is multi line value", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ /* Check multiline parameter parsing as mixed one/many lines */
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\nX-Comment: This\n is\r\n multi\r\n");
+ testHdr.Parse(" line\n value\r\n\n"); // the only parameter with mixed ending style
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is multi line value", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ /* Check parsing of multiline parameter with ':' in value */
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\nX-Comment: This\r\n is:\r\n mul:ti\r\n");
+ testHdr.Parse(" :line\r\n valu:e\r\n\n"); // the only parameter
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("This is: mul:ti :line valu:e", testHdr.GetValue("X-Comment").c_str()) << "Wrong multiline value";
+
+ /* Check multiline parameter parsing with trimming */
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n");
+ testHdr.Parse(CHECK_DATE_NAME ": " CHECK_DATE_VALUE3 "\r\n");
+ testHdr.Parse("Server: Apache/2.4.7 (Unix)\r\n"); // last parameter, line-by-line parsing
+ testHdr.Parse(" mod_wsgi/3.4 \r\n");
+ testHdr.Parse("\tPython/2.7.5\r\n");
+ testHdr.Parse("\t \t \tOpenSSL/1.0.1e\r\n");
+ testHdr.Parse("\r\n");
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_GE(strlen("Apache/2.4.7 (Unix) mod_wsgi/3.4 \tPython/2.7.5\t \t \tOpenSSL/1.0.1e"), testHdr.GetValue("Server").length()) << "Length of miltiline value is greater than length of original string";
+ EXPECT_LE(strlen("Apache/2.4.7 (Unix) mod_wsgi/3.4 Python/2.7.5 OpenSSL/1.0.1e"), testHdr.GetValue("Server").length()) << "Length of miltiline value is less than length of trimmed original string";
+ EXPECT_STREQ("Apache/2.4.7(Unix)mod_wsgi/3.4Python/2.7.5OpenSSL/1.0.1e", strReplace(strReplace(testHdr.GetValue("Server"), " ", ""), "\t", "").c_str()) << "Multiline value with removed whitespaces does not match original string with removed whitespaces";
+
+ testHdr.Clear();
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\n");
+ testHdr.Parse(CHECK_DATE_NAME ": " CHECK_DATE_VALUE3 "\r\n");
+ testHdr.Parse("Server: Apache/2.4.7 (Unix)\r\n mod_wsgi/3.4 \n"); // last parameter, mixed line-by-line/one line parsing, mixed line ending
+ testHdr.Parse("\tPython/2.7.5\n\t \t \tOpenSSL/1.0.1e\r\n");
+ testHdr.Parse("\r\n");
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_GE(strlen("Apache/2.4.7 (Unix) mod_wsgi/3.4 \tPython/2.7.5\t \t \tOpenSSL/1.0.1e"), testHdr.GetValue("Server").length()) << "Length of miltiline value is greater than length of original string";
+ EXPECT_LE(strlen("Apache/2.4.7 (Unix) mod_wsgi/3.4 Python/2.7.5 OpenSSL/1.0.1e"), testHdr.GetValue("Server").length()) << "Length of miltiline value is less than length of trimmed original string";
+ EXPECT_STREQ("Apache/2.4.7(Unix)mod_wsgi/3.4Python/2.7.5OpenSSL/1.0.1e", strReplace(strReplace(testHdr.GetValue("Server"), " ", ""), "\t", "").c_str()) << "Multiline value with removed whitespaces does not match original string with removed whitespaces";
+}
+
+TEST(TestHttpHeader, GetValue)
+{
+ CHttpHeader testHdr;
+
+ /* Check that all parameters values can be retrieved */
+ testHdr.Parse(CHECK_HEADER_R);
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("nginx/1.4.4", testHdr.GetValue("Server").c_str()) << "Wrong parameter value";
+ EXPECT_STREQ(CHECK_DATE_VALUE2, testHdr.GetValue(CHECK_DATE_NAME).c_str()) << "Wrong parameter value";
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_HTML, testHdr.GetValue(CHECK_CNT_TYPE_NAME).c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("150", testHdr.GetValue("Content-Length").c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("close", testHdr.GetValue("Connection").c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("http://www.Example.Com", testHdr.GetValue("Location").c_str()) << "Wrong parameter value";
+ EXPECT_TRUE(testHdr.GetValue("foo").empty()) << "Some value is returned for non-existed parameter";
+
+ /* Check that all parameters values can be retrieved in random order */
+ testHdr.Parse(CHECK_HEADER_R);
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Completed header has \"parsing not finished\" state";
+ EXPECT_STREQ("http://www.Example.Com", testHdr.GetValue("Location").c_str()) << "Wrong parameter value";
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_HTML, testHdr.GetValue(CHECK_CNT_TYPE_NAME).c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("http://www.Example.Com", testHdr.GetValue("Location").c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("close", testHdr.GetValue("Connection").c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("nginx/1.4.4", testHdr.GetValue("Server").c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("150", testHdr.GetValue("Content-Length").c_str()) << "Wrong parameter value";
+ EXPECT_STREQ(CHECK_DATE_VALUE2, testHdr.GetValue(CHECK_DATE_NAME).c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("nginx/1.4.4", testHdr.GetValue("Server").c_str()) << "Wrong parameter value";
+ EXPECT_TRUE(testHdr.GetValue("foo").empty()) << "Some value is returned for non-existed parameter";
+
+ /* Check that parameters name is case-insensitive and value is case-sensitive*/
+ EXPECT_STREQ("http://www.Example.Com", testHdr.GetValue("location").c_str()) << "Wrong parameter value for lowercase name";
+ EXPECT_STREQ("http://www.Example.Com", testHdr.GetValue("LOCATION").c_str()) << "Wrong parameter value for UPPERCASE name";
+ EXPECT_STREQ("http://www.Example.Com", testHdr.GetValue("LoCAtIOn").c_str()) << "Wrong parameter value for MiXEdcASe name";
+
+ /* Check value of last added parameter with the same name is returned */
+ testHdr.Parse(CHECK_HEADER_L1);
+ EXPECT_STREQ("close", testHdr.GetValue("Connection").c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("user_country=ot; expires=Thu, 09-Jan-2014 18:58:30 GMT; path=/; domain=.example.com", testHdr.GetValue("Set-Cookie").c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("user_country=ot; expires=Thu, 09-Jan-2014 18:58:30 GMT; path=/; domain=.example.com", testHdr.GetValue("set-cookie").c_str()) << "Wrong parameter value for lowercase name";
+}
+
+TEST(TestHttpHeader, GetValues)
+{
+ CHttpHeader testHdr;
+
+ /* Check that all parameter values can be retrieved and order of values is correct */
+ testHdr.Parse(CHECK_HEADER_L1);
+ EXPECT_EQ(1U, testHdr.GetValues("Server").size()) << "Wrong number of values for parameter \"Server\"";
+ EXPECT_STREQ("nginx/1.4.4", testHdr.GetValues("Server")[0].c_str()) << "Wrong parameter value";
+ EXPECT_EQ(2U, testHdr.GetValues("Set-Cookie").size()) << "Wrong number of values for parameter \"Set-Cookie\"";
+ EXPECT_STREQ("PHPSESSID=90857d437518db8f0944ca012761048a; path=/; domain=example.com", testHdr.GetValues("Set-Cookie")[0].c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("user_country=ot; expires=Thu, 09-Jan-2014 18:58:30 GMT; path=/; domain=.example.com", testHdr.GetValues("Set-Cookie")[1].c_str()) << "Wrong parameter value";
+ EXPECT_TRUE(testHdr.GetValues("foo").empty()) << "Some values are returned for non-existed parameter";
+}
+
+TEST(TestHttpHeader, AddParam)
+{
+ CHttpHeader testHdr;
+
+ /* General functionality */
+ testHdr.AddParam("server", "Microsoft-IIS/8.0");
+ EXPECT_STREQ("Microsoft-IIS/8.0", testHdr.GetValue("Server").c_str()) << "Wrong parameter value";
+
+ /* Interfere with parsing */
+ EXPECT_FALSE(testHdr.IsHeaderDone()) << "\"AddParam\" set \"parsing finished\" state";
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\nServer: nginx/1.4.4\r\n\r\n");
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Parsed header has \"parsing not finished\" state";
+ EXPECT_STREQ("nginx/1.4.4", testHdr.GetValue("Server").c_str()) << "Wrong parameter value";
+ testHdr.AddParam("server", "Apache/2.4.7");
+ EXPECT_STREQ("Apache/2.4.7", testHdr.GetValue("Server").c_str()) << "Wrong parameter value";
+ EXPECT_EQ(3U, testHdr.GetValues("Server").size()) << "Wrong number of values for parameter \"Server\"";
+
+ /* Multiple values */
+ testHdr.AddParam("X-foo", "bar1");
+ testHdr.AddParam("x-foo", "bar2");
+ testHdr.AddParam("x-fOO", "bar3");
+ EXPECT_EQ(3U, testHdr.GetValues("X-FOO").size()) << "Wrong number of values for parameter \"X-foo\"";
+ EXPECT_STREQ("bar1", testHdr.GetValues("X-FOo")[0].c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("bar2", testHdr.GetValues("X-fOo")[1].c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("bar3", testHdr.GetValues("x-fOo")[2].c_str()) << "Wrong parameter value";
+ EXPECT_STREQ("bar3", testHdr.GetValue("x-foo").c_str()) << "Wrong parameter value";
+
+ /* Overwrite value */
+ EXPECT_TRUE(testHdr.IsHeaderDone()) << "Parsed header has \"parsing not finished\" state";
+ testHdr.AddParam("x-fOO", "superbar", true);
+ EXPECT_EQ(1U, testHdr.GetValues("X-FoO").size()) << "Wrong number of values for parameter \"X-foo\"";
+ EXPECT_STREQ("superbar", testHdr.GetValue("x-foo").c_str()) << "Wrong parameter value";
+
+ /* Check name trimming */
+ testHdr.AddParam("\tx-fOO\t ", "bar");
+ EXPECT_EQ(2U, testHdr.GetValues("X-FoO").size()) << "Wrong number of values for parameter \"X-foo\"";
+ EXPECT_STREQ("bar", testHdr.GetValue("x-foo").c_str()) << "Wrong parameter value";
+ testHdr.AddParam(" SerVer \t ", "fakeSrv", true);
+ EXPECT_EQ(1U, testHdr.GetValues("serveR").size()) << "Wrong number of values for parameter \"Server\"";
+ EXPECT_STREQ("fakeSrv", testHdr.GetValue("Server").c_str()) << "Wrong parameter value";
+
+ /* Check value trimming */
+ testHdr.AddParam("X-TestParam", " testValue1");
+ EXPECT_STREQ("testValue1", testHdr.GetValue("X-TestParam").c_str()) << "Wrong parameter value";
+ testHdr.AddParam("X-TestParam", "\ttestValue2 and more \t ");
+ EXPECT_STREQ("testValue2 and more", testHdr.GetValue("X-TestParam").c_str()) << "Wrong parameter value";
+
+ /* Empty name or value */
+ testHdr.Clear();
+ testHdr.AddParam("X-TestParam", " ");
+ EXPECT_TRUE(testHdr.GetHeader().empty()) << "Parameter with empty value was added";
+ testHdr.AddParam("\t\t", "value");
+ EXPECT_TRUE(testHdr.GetHeader().empty());
+ testHdr.AddParam(" ", "\t");
+ EXPECT_TRUE(testHdr.GetHeader().empty());
+}
+
+TEST(TestHttpHeader, GetMimeType)
+{
+ CHttpHeader testHdr;
+
+ /* General functionality */
+ EXPECT_TRUE(testHdr.GetMimeType().empty()) << "Newly created object has non-empty MIME-type";
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\nServer: nginx/1.4.4\r\n\r\n");
+ EXPECT_TRUE(testHdr.GetMimeType().empty()) << "Non-empty MIME-type for header without MIME-type";
+ testHdr.Parse(CHECK_HEADER_SMPL);
+ EXPECT_STREQ("text/html", testHdr.GetMimeType().c_str()) << "Wrong MIME-type";
+ testHdr.Parse(CHECK_HEADER_L1);
+ EXPECT_STREQ("text/html", testHdr.GetMimeType().c_str()) << "Wrong MIME-type";
+ testHdr.Parse(CHECK_HEADER_L2);
+ EXPECT_STREQ("text/xml", testHdr.GetMimeType().c_str()) << "Wrong MIME-type";
+ testHdr.Parse(CHECK_HEADER_R);
+ EXPECT_STREQ("text/html", testHdr.GetMimeType().c_str()) << "Wrong MIME-type";
+
+ /* Overwrite by AddParam */
+ testHdr.AddParam(CHECK_CNT_TYPE_NAME, CHECK_CONTENT_TYPE_TEXT);
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_TEXT, testHdr.GetMimeType().c_str()) << "MIME-type was not overwritten by \"AddParam\"";
+
+ /* Correct trimming */
+ testHdr.AddParam(CHECK_CNT_TYPE_NAME, " " CHECK_CONTENT_TYPE_TEXT " \t ;foo=bar");
+ EXPECT_STREQ(CHECK_CONTENT_TYPE_TEXT, testHdr.GetMimeType().c_str()) << "MIME-type is not trimmed correctly";
+}
+
+
+TEST(TestHttpHeader, GetCharset)
+{
+ CHttpHeader testHdr;
+
+ /* General functionality */
+ EXPECT_TRUE(testHdr.GetCharset().empty()) << "Newly created object has non-empty charset";
+ testHdr.Parse(CHECK_PROT_LINE_200 "\r\nServer: nginx/1.4.4\r\n\r\n");
+ EXPECT_TRUE(testHdr.GetCharset().empty()) << "Non-empty charset for header without charset";
+ testHdr.Parse(CHECK_HEADER_SMPL);
+ EXPECT_TRUE(testHdr.GetCharset().empty()) << "Non-empty charset for header without charset";
+ testHdr.Parse(CHECK_HEADER_L1);
+ EXPECT_STREQ("WINDOWS-1251", testHdr.GetCharset().c_str()) << "Wrong charset value";
+ testHdr.Parse(CHECK_HEADER_L2);
+ EXPECT_STREQ("UTF-8", testHdr.GetCharset().c_str()) << "Wrong charset value";
+
+ /* Overwrite by AddParam */
+ testHdr.AddParam(CHECK_CNT_TYPE_NAME, CHECK_CONTENT_TYPE_TEXT "; charset=WINDOWS-1252");
+ EXPECT_STREQ("WINDOWS-1252", testHdr.GetCharset().c_str()) << "Charset was not overwritten by \"AddParam\"";
+
+ /* Correct trimming */
+ testHdr.AddParam(CHECK_CNT_TYPE_NAME, "text/plain;charset=WINDOWS-1251");
+ EXPECT_STREQ("WINDOWS-1251", testHdr.GetCharset().c_str()) << "Wrong charset value";
+ testHdr.AddParam(CHECK_CNT_TYPE_NAME, "text/plain ;\tcharset=US-AScII\t");
+ EXPECT_STREQ("US-ASCII", testHdr.GetCharset().c_str()) << "Wrong charset value";
+ testHdr.AddParam(CHECK_CNT_TYPE_NAME, "text/html ; \tcharset=\"uTF-8\"\t");
+ EXPECT_STREQ("UTF-8", testHdr.GetCharset().c_str()) << "Wrong charset value";
+ testHdr.AddParam(CHECK_CNT_TYPE_NAME, " \ttext/xml\t;\tcharset=uTF-16 ");
+ EXPECT_STREQ("UTF-16", testHdr.GetCharset().c_str()) << "Wrong charset value";
+}
diff --git a/xbmc/utils/test/TestHttpParser.cpp b/xbmc/utils/test/TestHttpParser.cpp
new file mode 100644
index 0000000..1eb2932
--- /dev/null
+++ b/xbmc/utils/test/TestHttpParser.cpp
@@ -0,0 +1,49 @@
+/*
+ * 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 "utils/HttpParser.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestHttpParser, General)
+{
+ HttpParser a;
+ std::string str = "POST /path/script.cgi HTTP/1.0\r\n"
+ "From: amejia@xbmc.org\r\n"
+ "User-Agent: XBMC/snapshot (compatible; MSIE 5.5; Windows NT"
+ " 4.0)\r\n"
+ "Content-Type: application/x-www-form-urlencoded\r\n"
+ "Content-Length: 35\r\n"
+ "\r\n"
+ "home=amejia&favorite+flavor=orange\r\n";
+ std::string refstr, varstr;
+
+ EXPECT_EQ(a.Done, a.addBytes(str.c_str(), str.length()));
+
+ refstr = "POST";
+ varstr = a.getMethod();
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "/path/script.cgi";
+ varstr = a.getUri();
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "";
+ varstr = a.getQueryString();
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "home=amejia&favorite+flavor=orange\r\n";
+ varstr = a.getBody();
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "application/x-www-form-urlencoded";
+ varstr = a.getValue("content-type");
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ EXPECT_EQ((unsigned)35, a.getContentLength());
+}
diff --git a/xbmc/utils/test/TestHttpRangeUtils.cpp b/xbmc/utils/test/TestHttpRangeUtils.cpp
new file mode 100644
index 0000000..f988f10
--- /dev/null
+++ b/xbmc/utils/test/TestHttpRangeUtils.cpp
@@ -0,0 +1,887 @@
+/*
+ * 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 "utils/HttpRangeUtils.h"
+
+#include <gtest/gtest.h>
+
+#define RANGES_START "bytes="
+
+static const uint64_t DefaultFirstPosition = 1;
+static const uint64_t DefaultLastPosition = 0;
+static const uint64_t DefaultLength = 0;
+static const void* DefaultData = NULL;
+
+TEST(TestHttpRange, FirstPosition)
+{
+ const uint64_t expectedFirstPosition = 25;
+
+ CHttpRange range;
+ EXPECT_EQ(DefaultFirstPosition, range.GetFirstPosition());
+
+ range.SetFirstPosition(expectedFirstPosition);
+ EXPECT_EQ(expectedFirstPosition, range.GetFirstPosition());
+}
+
+TEST(TestHttpRange, LastPosition)
+{
+ const uint64_t expectedLastPosition = 25;
+
+ CHttpRange range;
+ EXPECT_EQ(DefaultLastPosition, range.GetLastPosition());
+
+ range.SetLastPosition(expectedLastPosition);
+ EXPECT_EQ(expectedLastPosition, range.GetLastPosition());
+}
+
+TEST(TestHttpRange, Length)
+{
+ const uint64_t expectedFirstPosition = 10;
+ const uint64_t expectedLastPosition = 25;
+ const uint64_t expectedLength = expectedLastPosition - expectedFirstPosition + 1;
+
+ CHttpRange range;
+ EXPECT_EQ(DefaultLength, range.GetLength());
+
+ range.SetFirstPosition(expectedFirstPosition);
+ range.SetLastPosition(expectedLastPosition);
+ EXPECT_EQ(expectedLength, range.GetLength());
+
+ CHttpRange range_length;
+ range.SetFirstPosition(expectedFirstPosition);
+ range.SetLength(expectedLength);
+ EXPECT_EQ(expectedLastPosition, range.GetLastPosition());
+ EXPECT_EQ(expectedLength, range.GetLength());
+}
+
+TEST(TestHttpRange, IsValid)
+{
+ const uint64_t validFirstPosition = 10;
+ const uint64_t validLastPosition = 25;
+ const uint64_t invalidLastPosition = 5;
+
+ CHttpRange range;
+ EXPECT_FALSE(range.IsValid());
+
+ range.SetFirstPosition(validFirstPosition);
+ EXPECT_FALSE(range.IsValid());
+
+ range.SetLastPosition(invalidLastPosition);
+ EXPECT_FALSE(range.IsValid());
+
+ range.SetLastPosition(validLastPosition);
+ EXPECT_TRUE(range.IsValid());
+}
+
+TEST(TestHttpRange, Ctor)
+{
+ const uint64_t validFirstPosition = 10;
+ const uint64_t validLastPosition = 25;
+ const uint64_t invalidLastPosition = 5;
+ const uint64_t validLength = validLastPosition - validFirstPosition + 1;
+
+ CHttpRange range_invalid(validFirstPosition, invalidLastPosition);
+ EXPECT_EQ(validFirstPosition, range_invalid.GetFirstPosition());
+ EXPECT_EQ(invalidLastPosition, range_invalid.GetLastPosition());
+ EXPECT_EQ(DefaultLength, range_invalid.GetLength());
+ EXPECT_FALSE(range_invalid.IsValid());
+
+ CHttpRange range_valid(validFirstPosition, validLastPosition);
+ EXPECT_EQ(validFirstPosition, range_valid.GetFirstPosition());
+ EXPECT_EQ(validLastPosition, range_valid.GetLastPosition());
+ EXPECT_EQ(validLength, range_valid.GetLength());
+ EXPECT_TRUE(range_valid.IsValid());
+}
+
+TEST(TestHttpResponseRange, SetData)
+{
+ const uint64_t validFirstPosition = 1;
+ const uint64_t validLastPosition = 2;
+ const uint64_t validLength = validLastPosition - validFirstPosition + 1;
+ const char* validData = "test";
+ const void* invalidData = DefaultData;
+ const size_t validDataLength = strlen(validData);
+ const size_t invalidDataLength = 1;
+
+ CHttpResponseRange range;
+ EXPECT_EQ(DefaultData, range.GetData());
+ EXPECT_FALSE(range.IsValid());
+
+ range.SetData(invalidData);
+ EXPECT_EQ(invalidData, range.GetData());
+ EXPECT_FALSE(range.IsValid());
+
+ range.SetData(validData);
+ EXPECT_EQ(validData, range.GetData());
+ EXPECT_FALSE(range.IsValid());
+
+ range.SetData(invalidData, 0);
+ EXPECT_EQ(validData, range.GetData());
+ EXPECT_FALSE(range.IsValid());
+
+ range.SetData(invalidData, invalidDataLength);
+ EXPECT_EQ(invalidData, range.GetData());
+ EXPECT_FALSE(range.IsValid());
+
+ range.SetData(validData, validDataLength);
+ EXPECT_EQ(validData, range.GetData());
+ EXPECT_EQ(0U, range.GetFirstPosition());
+ EXPECT_EQ(validDataLength - 1, range.GetLastPosition());
+ EXPECT_EQ(validDataLength, range.GetLength());
+ EXPECT_TRUE(range.IsValid());
+
+ range.SetData(invalidData, 0, 0);
+ EXPECT_EQ(invalidData, range.GetData());
+ EXPECT_FALSE(range.IsValid());
+
+ range.SetData(validData, validFirstPosition, validLastPosition);
+ EXPECT_EQ(validData, range.GetData());
+ EXPECT_EQ(validFirstPosition, range.GetFirstPosition());
+ EXPECT_EQ(validLastPosition, range.GetLastPosition());
+ EXPECT_EQ(validLength, range.GetLength());
+ EXPECT_TRUE(range.IsValid());
+}
+
+TEST(TestHttpRanges, Ctor)
+{
+ CHttpRange range;
+ uint64_t position;
+
+ CHttpRanges ranges_empty;
+
+ EXPECT_EQ(0U, ranges_empty.Size());
+ EXPECT_TRUE(ranges_empty.Get().empty());
+
+ EXPECT_FALSE(ranges_empty.Get(0, range));
+ EXPECT_FALSE(ranges_empty.GetFirst(range));
+ EXPECT_FALSE(ranges_empty.GetLast(range));
+
+ EXPECT_FALSE(ranges_empty.GetFirstPosition(position));
+ EXPECT_FALSE(ranges_empty.GetLastPosition(position));
+ EXPECT_EQ(0U, ranges_empty.GetLength());
+ EXPECT_FALSE(ranges_empty.GetTotalRange(range));
+}
+
+TEST(TestHttpRanges, GetAll)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+
+ HttpRanges ranges_raw;
+ ranges_raw.push_back(range_0);
+ ranges_raw.push_back(range_1);
+ ranges_raw.push_back(range_2);
+
+ CHttpRanges ranges(ranges_raw);
+
+ const HttpRanges& ranges_raw_get = ranges.Get();
+ ASSERT_EQ(ranges_raw.size(), ranges_raw_get.size());
+
+ for (size_t i = 0; i < ranges_raw.size(); ++i)
+ EXPECT_EQ(ranges_raw.at(i), ranges_raw_get.at(i));
+}
+
+TEST(TestHttpRanges, GetIndex)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+
+ HttpRanges ranges_raw;
+ ranges_raw.push_back(range_0);
+ ranges_raw.push_back(range_1);
+ ranges_raw.push_back(range_2);
+
+ CHttpRanges ranges(ranges_raw);
+
+ CHttpRange range;
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range_0, range);
+
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range_1, range);
+
+ EXPECT_TRUE(ranges.Get(2, range));
+ EXPECT_EQ(range_2, range);
+
+ EXPECT_FALSE(ranges.Get(3, range));
+}
+
+TEST(TestHttpRanges, GetFirst)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+
+ HttpRanges ranges_raw;
+ ranges_raw.push_back(range_0);
+ ranges_raw.push_back(range_1);
+ ranges_raw.push_back(range_2);
+
+ CHttpRanges ranges(ranges_raw);
+
+ CHttpRange range;
+ EXPECT_TRUE(ranges.GetFirst(range));
+ EXPECT_EQ(range_0, range);
+}
+
+TEST(TestHttpRanges, GetLast)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+
+ HttpRanges ranges_raw;
+ ranges_raw.push_back(range_0);
+ ranges_raw.push_back(range_1);
+ ranges_raw.push_back(range_2);
+
+ CHttpRanges ranges(ranges_raw);
+
+ CHttpRange range;
+ EXPECT_TRUE(ranges.GetLast(range));
+ EXPECT_EQ(range_2, range);
+}
+
+TEST(TestHttpRanges, Size)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+
+ HttpRanges ranges_raw;
+ ranges_raw.push_back(range_0);
+ ranges_raw.push_back(range_1);
+ ranges_raw.push_back(range_2);
+
+ CHttpRanges ranges_empty;
+ EXPECT_EQ(0U, ranges_empty.Size());
+
+ CHttpRanges ranges(ranges_raw);
+ EXPECT_EQ(ranges_raw.size(), ranges.Size());
+}
+
+TEST(TestHttpRanges, GetFirstPosition)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+
+ HttpRanges ranges_raw;
+ ranges_raw.push_back(range_0);
+ ranges_raw.push_back(range_1);
+ ranges_raw.push_back(range_2);
+
+ CHttpRanges ranges(ranges_raw);
+
+ uint64_t position;
+ EXPECT_TRUE(ranges.GetFirstPosition(position));
+ EXPECT_EQ(range_0.GetFirstPosition(), position);
+}
+
+TEST(TestHttpRanges, GetLastPosition)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+
+ HttpRanges ranges_raw;
+ ranges_raw.push_back(range_0);
+ ranges_raw.push_back(range_1);
+ ranges_raw.push_back(range_2);
+
+ CHttpRanges ranges(ranges_raw);
+
+ uint64_t position;
+ EXPECT_TRUE(ranges.GetLastPosition(position));
+ EXPECT_EQ(range_2.GetLastPosition(), position);
+}
+
+TEST(TestHttpRanges, GetLength)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+ const uint64_t expectedLength = range_0.GetLength() + range_1.GetLength() + range_2.GetLength();
+
+ HttpRanges ranges_raw;
+ ranges_raw.push_back(range_0);
+ ranges_raw.push_back(range_1);
+ ranges_raw.push_back(range_2);
+
+ CHttpRanges ranges(ranges_raw);
+
+ EXPECT_EQ(expectedLength, ranges.GetLength());
+}
+
+TEST(TestHttpRanges, GetTotalRange)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+ CHttpRange range_total_expected(range_0.GetFirstPosition(), range_2.GetLastPosition());
+
+ HttpRanges ranges_raw;
+ ranges_raw.push_back(range_0);
+ ranges_raw.push_back(range_1);
+ ranges_raw.push_back(range_2);
+
+ CHttpRanges ranges(ranges_raw);
+
+ CHttpRange range_total;
+ EXPECT_TRUE(ranges.GetTotalRange(range_total));
+ EXPECT_EQ(range_total_expected, range_total);
+}
+
+TEST(TestHttpRanges, Add)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+
+ CHttpRanges ranges;
+ CHttpRange range;
+
+ ranges.Add(range_0);
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.GetFirst(range));
+ EXPECT_EQ(range_0, range);
+ EXPECT_TRUE(ranges.GetLast(range));
+ EXPECT_EQ(range_0, range);
+
+ ranges.Add(range_1);
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.GetFirst(range));
+ EXPECT_EQ(range_0, range);
+ EXPECT_TRUE(ranges.GetLast(range));
+ EXPECT_EQ(range_1, range);
+
+ ranges.Add(range_2);
+ EXPECT_EQ(3U, ranges.Size());
+ EXPECT_TRUE(ranges.GetFirst(range));
+ EXPECT_EQ(range_0, range);
+ EXPECT_TRUE(ranges.GetLast(range));
+ EXPECT_EQ(range_2, range);
+}
+
+TEST(TestHttpRanges, Remove)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+
+ HttpRanges ranges_raw;
+ ranges_raw.push_back(range_0);
+ ranges_raw.push_back(range_1);
+ ranges_raw.push_back(range_2);
+
+ CHttpRanges ranges(ranges_raw);
+
+ CHttpRange range;
+ EXPECT_EQ(3U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range_1, range);
+ EXPECT_TRUE(ranges.Get(2, range));
+ EXPECT_EQ(range_2, range);
+
+ // remove non-existing range
+ ranges.Remove(ranges.Size());
+ EXPECT_EQ(3U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range_1, range);
+ EXPECT_TRUE(ranges.Get(2, range));
+ EXPECT_EQ(range_2, range);
+
+ // remove first range
+ ranges.Remove(0);
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range_1, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range_2, range);
+
+ // remove last range
+ ranges.Remove(1);
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range_1, range);
+
+ // remove remaining range
+ ranges.Remove(0);
+ EXPECT_EQ(0U, ranges.Size());
+}
+
+TEST(TestHttpRanges, Clear)
+{
+ CHttpRange range_0(0, 2);
+ CHttpRange range_1(4, 6);
+ CHttpRange range_2(8, 10);
+
+ HttpRanges ranges_raw;
+ ranges_raw.push_back(range_0);
+ ranges_raw.push_back(range_1);
+ ranges_raw.push_back(range_2);
+
+ CHttpRanges ranges(ranges_raw);
+
+ CHttpRange range;
+ EXPECT_EQ(3U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range_1, range);
+ EXPECT_TRUE(ranges.Get(2, range));
+ EXPECT_EQ(range_2, range);
+
+ ranges.Clear();
+ EXPECT_EQ(0U, ranges.Size());
+}
+
+TEST(TestHttpRanges, ParseInvalid)
+{
+ CHttpRanges ranges;
+
+ // combinations of invalid string and invalid total length
+ EXPECT_FALSE(ranges.Parse(""));
+ EXPECT_FALSE(ranges.Parse("", 0));
+ EXPECT_FALSE(ranges.Parse("", 1));
+ EXPECT_FALSE(ranges.Parse("test", 0));
+ EXPECT_FALSE(ranges.Parse(RANGES_START, 0));
+
+ // empty range definition
+ EXPECT_FALSE(ranges.Parse(RANGES_START));
+ EXPECT_FALSE(ranges.Parse(RANGES_START "-"));
+
+ // bad characters in range definition
+ EXPECT_FALSE(ranges.Parse(RANGES_START "a"));
+ EXPECT_FALSE(ranges.Parse(RANGES_START "1a"));
+ EXPECT_FALSE(ranges.Parse(RANGES_START "1-a"));
+ EXPECT_FALSE(ranges.Parse(RANGES_START "a-a"));
+ EXPECT_FALSE(ranges.Parse(RANGES_START "a-1"));
+ EXPECT_FALSE(ranges.Parse(RANGES_START "--"));
+ EXPECT_FALSE(ranges.Parse(RANGES_START "1--"));
+ EXPECT_FALSE(ranges.Parse(RANGES_START "1--2"));
+ EXPECT_FALSE(ranges.Parse(RANGES_START "--2"));
+
+ // combination of valid and empty range definitions
+ EXPECT_FALSE(ranges.Parse(RANGES_START "0-1,"));
+ EXPECT_FALSE(ranges.Parse(RANGES_START ",0-1"));
+
+ // too big start position
+ EXPECT_FALSE(ranges.Parse(RANGES_START "10-11", 5));
+
+ // end position smaller than start position
+ EXPECT_FALSE(ranges.Parse(RANGES_START "1-0"));
+}
+
+TEST(TestHttpRanges, ParseStartOnly)
+{
+ const uint64_t totalLength = 5;
+ const CHttpRange range0_(0, totalLength - 1);
+ const CHttpRange range2_(2, totalLength - 1);
+
+ CHttpRange range;
+
+ CHttpRanges ranges_all;
+ EXPECT_TRUE(ranges_all.Parse(RANGES_START "0-", totalLength));
+ EXPECT_EQ(1U, ranges_all.Size());
+ EXPECT_TRUE(ranges_all.Get(0, range));
+ EXPECT_EQ(range0_, range);
+
+ CHttpRanges ranges_some;
+ EXPECT_TRUE(ranges_some.Parse(RANGES_START "2-", totalLength));
+ EXPECT_EQ(1U, ranges_some.Size());
+ EXPECT_TRUE(ranges_some.Get(0, range));
+ EXPECT_EQ(range2_, range);
+}
+
+TEST(TestHttpRanges, ParseFromEnd)
+{
+ const uint64_t totalLength = 5;
+ const CHttpRange range_1(totalLength - 1, totalLength - 1);
+ const CHttpRange range_3(totalLength - 3, totalLength - 1);
+
+ CHttpRange range;
+
+ CHttpRanges ranges_1;
+ EXPECT_TRUE(ranges_1.Parse(RANGES_START "-1", totalLength));
+ EXPECT_EQ(1U, ranges_1.Size());
+ EXPECT_TRUE(ranges_1.Get(0, range));
+ EXPECT_EQ(range_1, range);
+
+ CHttpRanges ranges_3;
+ EXPECT_TRUE(ranges_3.Parse(RANGES_START "-3", totalLength));
+ EXPECT_EQ(1U, ranges_3.Size());
+ EXPECT_TRUE(ranges_3.Get(0, range));
+ EXPECT_EQ(range_3, range);
+}
+
+TEST(TestHttpRanges, ParseSingle)
+{
+ const uint64_t totalLength = 5;
+ const CHttpRange range0_0(0, 0);
+ const CHttpRange range0_1(0, 1);
+ const CHttpRange range0_5(0, totalLength - 1);
+ const CHttpRange range1_1(1, 1);
+ const CHttpRange range1_3(1, 3);
+ const CHttpRange range3_4(3, 4);
+ const CHttpRange range4_4(4, 4);
+
+ CHttpRange range;
+
+ CHttpRanges ranges;
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_0, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-1", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_1, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-5", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_5, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "1-1", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range1_1, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "1-3", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range1_3, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "3-4", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range3_4, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "4-4", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range4_4, range);
+}
+
+TEST(TestHttpRanges, ParseMulti)
+{
+ const uint64_t totalLength = 6;
+ const CHttpRange range0_0(0, 0);
+ const CHttpRange range0_1(0, 1);
+ const CHttpRange range1_3(1, 3);
+ const CHttpRange range2_2(2, 2);
+ const CHttpRange range4_5(4, 5);
+ const CHttpRange range5_5(5, 5);
+
+ CHttpRange range;
+
+ CHttpRanges ranges;
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,2-2", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range2_2, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,2-2,4-5", totalLength));
+ EXPECT_EQ(3U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range2_2, range);
+ EXPECT_TRUE(ranges.Get(2, range));
+ EXPECT_EQ(range4_5, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-1,5-5", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_1, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range5_5, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "1-3,5-5", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range1_3, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range5_5, range);
+}
+
+TEST(TestHttpRanges, ParseOrderedNotOverlapping)
+{
+ const uint64_t totalLength = 5;
+ const CHttpRange range0_0(0, 0);
+ const CHttpRange range0_1(0, 1);
+ const CHttpRange range2_2(2, 2);
+ const CHttpRange range2_(2, totalLength - 1);
+ const CHttpRange range_1(totalLength - 1, totalLength - 1);
+
+ CHttpRange range;
+
+ CHttpRanges ranges;
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,-1", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range_1, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,2-2,-1", totalLength));
+ EXPECT_EQ(3U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range2_2, range);
+ EXPECT_TRUE(ranges.Get(2, range));
+ EXPECT_EQ(range_1, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,2-", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range2_, range);
+}
+
+TEST(TestHttpRanges, ParseOrderedBackToBack)
+{
+ const uint64_t totalLength = 5;
+ const CHttpRange range0_1(0, 1);
+ const CHttpRange range0_2(0, 2);
+ const CHttpRange range1_2(1, 2);
+ const CHttpRange range0_3(0, 3);
+ const CHttpRange range4_4(4, 4);
+ const CHttpRange range0_4(0, 4);
+ const CHttpRange range3_4(3, 4);
+
+ CHttpRange range;
+
+ CHttpRanges ranges;
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,1-1", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_1, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,1-1,2-2", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_2, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,1-1,2-2,3-3", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_3, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,1-1,2-2,3-3,4-4", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_4, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,1-1,3-3,4-4", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_1, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range3_4, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "1-1,2-2,4-4", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range1_2, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range4_4, range);
+}
+
+TEST(TestHttpRanges, ParseOrderedOverlapping)
+{
+ const uint64_t totalLength = 5;
+ const CHttpRange range0_0(0, 0);
+ const CHttpRange range0_1(0, 1);
+ const CHttpRange range0_2(0, 2);
+ const CHttpRange range0_3(0, 3);
+ const CHttpRange range0_4(0, 4);
+ const CHttpRange range2_4(2, 4);
+
+ CHttpRange range;
+
+ CHttpRanges ranges;
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,0-1", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_1, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,0-1,0-2", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_2, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,0-1,1-2", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_2, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,0-2,1-3", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_3, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-1,1-2,2-3,3-4", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_4, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-0,2-3,2-4,4-4", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range2_4, range);
+}
+
+TEST(TestHttpRanges, ParseUnorderedNotOverlapping)
+{
+ const uint64_t totalLength = 5;
+ const CHttpRange range0_0(0, 0);
+ const CHttpRange range0_1(0, 1);
+ const CHttpRange range2_2(2, 2);
+ const CHttpRange range2_(2, totalLength - 1);
+ const CHttpRange range_1(totalLength - 1, totalLength - 1);
+
+ CHttpRange range;
+
+ CHttpRanges ranges;
+ EXPECT_TRUE(ranges.Parse(RANGES_START "-1,0-0", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range_1, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "2-2,-1,0-0", totalLength));
+ EXPECT_EQ(3U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range2_2, range);
+ EXPECT_TRUE(ranges.Get(2, range));
+ EXPECT_EQ(range_1, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "2-,0-0", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range2_, range);
+}
+
+TEST(TestHttpRanges, ParseUnorderedBackToBack)
+{
+ const uint64_t totalLength = 5;
+ const CHttpRange range0_0(0, 0);
+ const CHttpRange range1_1(1, 1);
+ const CHttpRange range0_1(0, 1);
+ const CHttpRange range2_2(2, 2);
+ const CHttpRange range0_2(0, 2);
+ const CHttpRange range1_2(1, 2);
+ const CHttpRange range3_3(3, 3);
+ const CHttpRange range0_3(0, 3);
+ const CHttpRange range4_4(4, 4);
+ const CHttpRange range0_4(0, 4);
+ const CHttpRange range3_4(3, 4);
+
+ CHttpRange range;
+
+ CHttpRanges ranges;
+ EXPECT_TRUE(ranges.Parse(RANGES_START "1-1,0-0", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_1, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "1-1,0-0,2-2", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_2, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "2-2,1-1,3-3,0-0", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_3, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "4-4,1-1,0-0,2-2,3-3", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_4, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "3-3,0-0,4-4,1-1", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_1, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range3_4, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "4-4,1-1,2-2", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range1_2, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range4_4, range);
+}
+
+TEST(TestHttpRanges, ParseUnorderedOverlapping)
+{
+ const uint64_t totalLength = 5;
+ const CHttpRange range0_0(0, 0);
+ const CHttpRange range0_1(0, 1);
+ const CHttpRange range0_2(0, 2);
+ const CHttpRange range0_3(0, 3);
+ const CHttpRange range0_4(0, 4);
+ const CHttpRange range2_4(2, 4);
+
+ CHttpRange range;
+
+ CHttpRanges ranges;
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-1,0-0", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_1, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-2,0-0,0-1", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_2, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-1,1-2,0-0", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_2, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "0-2,0-0,1-3", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_3, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "2-3,1-2,0-1,3-4", totalLength));
+ EXPECT_EQ(1U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_4, range);
+
+ EXPECT_TRUE(ranges.Parse(RANGES_START "4-4,0-0,2-4,2-3", totalLength));
+ EXPECT_EQ(2U, ranges.Size());
+ EXPECT_TRUE(ranges.Get(0, range));
+ EXPECT_EQ(range0_0, range);
+ EXPECT_TRUE(ranges.Get(1, range));
+ EXPECT_EQ(range2_4, range);
+}
diff --git a/xbmc/utils/test/TestHttpResponse.cpp b/xbmc/utils/test/TestHttpResponse.cpp
new file mode 100644
index 0000000..1f66285
--- /dev/null
+++ b/xbmc/utils/test/TestHttpResponse.cpp
@@ -0,0 +1,43 @@
+/*
+ * 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 "utils/HttpResponse.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestHttpResponse, General)
+{
+ CHttpResponse a(HTTP::POST, HTTP::OK);
+ std::string response, content, refstr;
+
+ a.AddHeader("date", "Sun, 01 Jul 2012 00:00:00 -0400");
+ a.AddHeader("content-type", "text/html");
+ content = "<html>\r\n"
+ " <body>\r\n"
+ " <h1>XBMC TestHttpResponse Page</h1>\r\n"
+ " <p>blah blah blah</p>\r\n"
+ " </body>\r\n"
+ "</html>\r\n";
+ a.SetContent(content.c_str(), content.length());
+
+ response = a.Create();;
+ EXPECT_EQ((unsigned int)210, response.size());
+
+ refstr = "HTTP/1.1 200 OK\r\n"
+ "date: Sun, 01 Jul 2012 00:00:00 -0400\r\n"
+ "content-type: text/html\r\n"
+ "Content-Length: 106\r\n"
+ "\r\n"
+ "<html>\r\n"
+ " <body>\r\n"
+ " <h1>XBMC TestHttpResponse Page</h1>\r\n"
+ " <p>blah blah blah</p>\r\n"
+ " </body>\r\n"
+ "</html>\r\n";
+ EXPECT_STREQ(refstr.c_str(), response.c_str());
+}
diff --git a/xbmc/utils/test/TestJSONVariantParser.cpp b/xbmc/utils/test/TestJSONVariantParser.cpp
new file mode 100644
index 0000000..b8556b0
--- /dev/null
+++ b/xbmc/utils/test/TestJSONVariantParser.cpp
@@ -0,0 +1,189 @@
+/*
+ * 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 "utils/JSONVariantParser.h"
+#include "utils/Variant.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestJSONVariantParser, CannotParseNullptr)
+{
+ CVariant variant;
+ ASSERT_FALSE(CJSONVariantParser::Parse(nullptr, variant));
+}
+
+TEST(TestJSONVariantParser, CannotParseEmptyString)
+{
+ CVariant variant;
+ ASSERT_FALSE(CJSONVariantParser::Parse("", variant));
+ ASSERT_FALSE(CJSONVariantParser::Parse(std::string(), variant));
+}
+
+TEST(TestJSONVariantParser, CannotParseInvalidJson)
+{
+ CVariant variant;
+ ASSERT_FALSE(CJSONVariantParser::Parse("{", variant));
+ ASSERT_FALSE(CJSONVariantParser::Parse("}", variant));
+ ASSERT_FALSE(CJSONVariantParser::Parse("[", variant));
+ ASSERT_FALSE(CJSONVariantParser::Parse("]", variant));
+ ASSERT_FALSE(CJSONVariantParser::Parse("foo", variant));
+}
+
+TEST(TestJSONVariantParser, CanParseNull)
+{
+ CVariant variant;
+ ASSERT_TRUE(CJSONVariantParser::Parse("null", variant));
+ ASSERT_TRUE(variant.isNull());
+}
+
+TEST(TestJSONVariantParser, CanParseBoolean)
+{
+ CVariant variant;
+ ASSERT_TRUE(CJSONVariantParser::Parse("true", variant));
+ ASSERT_TRUE(variant.isBoolean());
+ ASSERT_TRUE(variant.asBoolean());
+
+ ASSERT_TRUE(CJSONVariantParser::Parse("false", variant));
+ ASSERT_TRUE(variant.isBoolean());
+ ASSERT_FALSE(variant.asBoolean());
+}
+
+TEST(TestJSONVariantParser, CanParseSignedInteger)
+{
+ CVariant variant;
+ ASSERT_TRUE(CJSONVariantParser::Parse("-1", variant));
+ ASSERT_TRUE(variant.isInteger());
+ ASSERT_EQ(-1, variant.asInteger());
+}
+
+TEST(TestJSONVariantParser, CanParseUnsignedInteger)
+{
+ CVariant variant;
+ ASSERT_TRUE(CJSONVariantParser::Parse("0", variant));
+ ASSERT_TRUE(variant.isUnsignedInteger());
+ ASSERT_EQ(0U, variant.asUnsignedInteger());
+
+ ASSERT_TRUE(CJSONVariantParser::Parse("1", variant));
+ ASSERT_TRUE(variant.isUnsignedInteger());
+ ASSERT_EQ(1U, variant.asUnsignedInteger());
+}
+
+TEST(TestJSONVariantParser, CanParseSignedInteger64)
+{
+ CVariant variant;
+ ASSERT_TRUE(CJSONVariantParser::Parse("-4294967296", variant));
+ ASSERT_TRUE(variant.isInteger());
+ ASSERT_EQ(-4294967296, variant.asInteger());
+}
+
+TEST(TestJSONVariantParser, CanParseUnsignedInteger64)
+{
+ CVariant variant;
+ ASSERT_TRUE(CJSONVariantParser::Parse("4294967296", variant));
+ ASSERT_TRUE(variant.isUnsignedInteger());
+ ASSERT_EQ(4294967296U, variant.asUnsignedInteger());
+}
+
+TEST(TestJSONVariantParser, CanParseDouble)
+{
+ CVariant variant;
+ ASSERT_TRUE(CJSONVariantParser::Parse("0.0", variant));
+ ASSERT_TRUE(variant.isDouble());
+ ASSERT_EQ(0.0, variant.asDouble());
+
+ ASSERT_TRUE(CJSONVariantParser::Parse("1.0", variant));
+ ASSERT_TRUE(variant.isDouble());
+ ASSERT_EQ(1.0, variant.asDouble());
+
+ ASSERT_TRUE(CJSONVariantParser::Parse("-1.0", variant));
+ ASSERT_TRUE(variant.isDouble());
+ ASSERT_EQ(-1.0, variant.asDouble());
+}
+
+TEST(TestJSONVariantParser, CanParseString)
+{
+ CVariant variant;
+ ASSERT_TRUE(CJSONVariantParser::Parse("\"\"", variant));
+ ASSERT_TRUE(variant.isString());
+ ASSERT_TRUE(variant.empty());
+
+ ASSERT_TRUE(CJSONVariantParser::Parse("\"foo\"", variant));
+ ASSERT_TRUE(variant.isString());
+ ASSERT_STREQ("foo", variant.asString().c_str());
+
+ ASSERT_TRUE(CJSONVariantParser::Parse("\"foo bar\"", variant));
+ ASSERT_TRUE(variant.isString());
+ ASSERT_STREQ("foo bar", variant.asString().c_str());
+}
+
+TEST(TestJSONVariantParser, CanParseObject)
+{
+ CVariant variant;
+ ASSERT_TRUE(CJSONVariantParser::Parse("{}", variant));
+ ASSERT_TRUE(variant.isObject());
+ ASSERT_TRUE(variant.empty());
+
+ variant.clear();
+ ASSERT_TRUE(CJSONVariantParser::Parse("{ \"foo\": \"bar\" }", variant));
+ ASSERT_TRUE(variant.isObject());
+ ASSERT_TRUE(variant.isMember("foo"));
+ ASSERT_TRUE(variant["foo"].isString());
+ ASSERT_STREQ("bar", variant["foo"].asString().c_str());
+
+ variant.clear();
+ ASSERT_TRUE(CJSONVariantParser::Parse("{ \"foo\": \"bar\", \"bar\": true }", variant));
+ ASSERT_TRUE(variant.isObject());
+ ASSERT_TRUE(variant.isMember("foo"));
+ ASSERT_TRUE(variant["foo"].isString());
+ ASSERT_STREQ("bar", variant["foo"].asString().c_str());
+ ASSERT_TRUE(variant.isMember("bar"));
+ ASSERT_TRUE(variant["bar"].isBoolean());
+ ASSERT_TRUE(variant["bar"].asBoolean());
+
+ variant.clear();
+ ASSERT_TRUE(CJSONVariantParser::Parse("{ \"foo\": { \"sub-foo\": \"bar\" } }", variant));
+ ASSERT_TRUE(variant.isObject());
+ ASSERT_TRUE(variant.isMember("foo"));
+ ASSERT_TRUE(variant["foo"].isObject());
+ ASSERT_TRUE(variant["foo"].isMember("sub-foo"));
+ ASSERT_TRUE(variant["foo"]["sub-foo"].isString());
+ ASSERT_STREQ("bar", variant["foo"]["sub-foo"].asString().c_str());
+}
+
+TEST(TestJSONVariantParser, CanParseArray)
+{
+ CVariant variant;
+ ASSERT_TRUE(CJSONVariantParser::Parse("[]", variant));
+ ASSERT_TRUE(variant.isArray());
+ ASSERT_TRUE(variant.empty());
+
+ variant.clear();
+ ASSERT_TRUE(CJSONVariantParser::Parse("[ true ]", variant));
+ ASSERT_TRUE(variant.isArray());
+ ASSERT_EQ(1U, variant.size());
+ ASSERT_TRUE(variant[0].isBoolean());
+ ASSERT_TRUE(variant[0].asBoolean());
+
+ variant.clear();
+ ASSERT_TRUE(CJSONVariantParser::Parse("[ true, \"foo\" ]", variant));
+ ASSERT_TRUE(variant.isArray());
+ ASSERT_EQ(2U, variant.size());
+ ASSERT_TRUE(variant[0].isBoolean());
+ ASSERT_TRUE(variant[0].asBoolean());
+ ASSERT_TRUE(variant[1].isString());
+ ASSERT_STREQ("foo", variant[1].asString().c_str());
+
+ variant.clear();
+ ASSERT_TRUE(CJSONVariantParser::Parse("[ { \"foo\": \"bar\" } ]", variant));
+ ASSERT_TRUE(variant.isArray());
+ ASSERT_EQ(1U, variant.size());
+ ASSERT_TRUE(variant[0].isObject());
+ ASSERT_TRUE(variant[0].isMember("foo"));
+ ASSERT_TRUE(variant[0]["foo"].isString());
+ ASSERT_STREQ("bar", variant[0]["foo"].asString().c_str());
+}
diff --git a/xbmc/utils/test/TestJSONVariantWriter.cpp b/xbmc/utils/test/TestJSONVariantWriter.cpp
new file mode 100644
index 0000000..0772a4d
--- /dev/null
+++ b/xbmc/utils/test/TestJSONVariantWriter.cpp
@@ -0,0 +1,151 @@
+/*
+ * 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 "utils/JSONVariantWriter.h"
+#include "utils/Variant.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestJSONVariantWriter, CanWriteNull)
+{
+ CVariant variant;
+ std::string str;
+
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("null", str.c_str());
+}
+
+TEST(TestJSONVariantWriter, CanWriteBoolean)
+{
+ CVariant variant(true);
+ std::string str;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("true", str.c_str());
+
+ variant = false;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("false", str.c_str());
+}
+
+TEST(TestJSONVariantWriter, CanWriteSignedInteger)
+{
+ CVariant variant(-1);
+ std::string str;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("-1", str.c_str());
+}
+
+TEST(TestJSONVariantWriter, CanWriteUnsignedInteger)
+{
+ CVariant variant(0);
+ std::string str;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("0", str.c_str());
+
+ variant = 1;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("1", str.c_str());
+}
+
+TEST(TestJSONVariantWriter, CanWriteSignedInteger64)
+{
+ CVariant variant(static_cast<int64_t>(-4294967296LL));
+ std::string str;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("-4294967296", str.c_str());
+}
+
+TEST(TestJSONVariantWriter, CanWriteUnsignedInteger64)
+{
+ CVariant variant(static_cast<int64_t>(4294967296LL));
+ std::string str;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("4294967296", str.c_str());
+}
+
+TEST(TestJSONVariantWriter, CanWriteDouble)
+{
+ CVariant variant(0.0);
+ std::string str;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("0.0", str.c_str());
+
+ variant = 1.0;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("1.0", str.c_str());
+
+ variant = -1.0;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("-1.0", str.c_str());
+}
+
+TEST(TestJSONVariantWriter, CanWriteString)
+{
+ CVariant variant("");
+ std::string str;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("\"\"", str.c_str());
+
+ variant = "foo";
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("\"foo\"", str.c_str());
+
+ variant = "foo bar";
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("\"foo bar\"", str.c_str());
+}
+
+TEST(TestJSONVariantWriter, CanWriteObject)
+{
+ CVariant variant(CVariant::VariantTypeObject);
+ std::string str;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("{}", str.c_str());
+
+ variant.clear();
+ variant["foo"] = "bar";
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("{\n\t\"foo\": \"bar\"\n}", str.c_str());
+
+ variant.clear();
+ variant["foo"] = "bar";
+ variant["bar"] = true;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("{\n\t\"bar\": true,\n\t\"foo\": \"bar\"\n}", str.c_str());
+
+ variant.clear();
+ variant["foo"]["sub-foo"] = "bar";
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("{\n\t\"foo\": {\n\t\t\"sub-foo\": \"bar\"\n\t}\n}", str.c_str());
+}
+
+TEST(TestJSONVariantWriter, CanWriteArray)
+{
+ CVariant variant(CVariant::VariantTypeArray);
+ std::string str;
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("[]", str.c_str());
+
+ variant.clear();
+ variant.push_back(true);
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("[\n\ttrue\n]", str.c_str());
+
+ variant.clear();
+ variant.push_back(true);
+ variant.push_back("foo");
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("[\n\ttrue,\n\t\"foo\"\n]", str.c_str());
+
+ variant.clear();
+ CVariant obj(CVariant::VariantTypeObject);
+ obj["foo"] = "bar";
+ variant.push_back(obj);
+ ASSERT_TRUE(CJSONVariantWriter::Write(variant, str, false));
+ ASSERT_STREQ("[\n\t{\n\t\t\"foo\": \"bar\"\n\t}\n]", str.c_str());
+}
diff --git a/xbmc/utils/test/TestJobManager.cpp b/xbmc/utils/test/TestJobManager.cpp
new file mode 100644
index 0000000..86f0af9
--- /dev/null
+++ b/xbmc/utils/test/TestJobManager.cpp
@@ -0,0 +1,218 @@
+/*
+ * 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 "ServiceBroker.h"
+#include "test/MtTestUtils.h"
+#include "utils/Job.h"
+#include "utils/JobManager.h"
+#include "utils/XTimeUtils.h"
+
+#include <atomic>
+#include <mutex>
+
+#include <gtest/gtest.h>
+
+using namespace ConditionPoll;
+
+struct Flags
+{
+ std::atomic<bool> lingerAtWork{true};
+ std::atomic<bool> started{false};
+ std::atomic<bool> finished{false};
+ std::atomic<bool> wasCanceled{false};
+};
+
+class DummyJob : public CJob
+{
+ Flags* m_flags;
+public:
+ inline DummyJob(Flags* flags) : m_flags(flags)
+ {
+ }
+
+ bool DoWork() override
+ {
+ m_flags->started = true;
+ while (m_flags->lingerAtWork)
+ std::this_thread::yield();
+
+ if (ShouldCancel(0,0))
+ m_flags->wasCanceled = true;
+
+ m_flags->finished = true;
+ return true;
+ }
+};
+
+class ReallyDumbJob : public CJob
+{
+ Flags* m_flags;
+public:
+ inline ReallyDumbJob(Flags* flags) : m_flags(flags) {}
+
+ bool DoWork() override
+ {
+ m_flags->finished = true;
+ return true;
+ }
+};
+
+class TestJobManager : public testing::Test
+{
+protected:
+ TestJobManager() { CServiceBroker::RegisterJobManager(std::make_shared<CJobManager>()); }
+
+ ~TestJobManager() override
+ {
+ /* Always cancel jobs test completion */
+ CServiceBroker::GetJobManager()->CancelJobs();
+ CServiceBroker::GetJobManager()->Restart();
+ CServiceBroker::UnregisterJobManager();
+ }
+};
+
+TEST_F(TestJobManager, AddJob)
+{
+ Flags* flags = new Flags();
+ ReallyDumbJob* job = new ReallyDumbJob(flags);
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+ ASSERT_TRUE(poll([flags]() -> bool { return flags->finished; }));
+ delete flags;
+}
+
+TEST_F(TestJobManager, CancelJob)
+{
+ unsigned int id;
+ Flags* flags = new Flags();
+ DummyJob* job = new DummyJob(flags);
+ id = CServiceBroker::GetJobManager()->AddJob(job, nullptr);
+
+ // wait for the worker thread to be entered
+ ASSERT_TRUE(poll([flags]() -> bool { return flags->started; }));
+
+ // cancel the job
+ CServiceBroker::GetJobManager()->CancelJob(id);
+
+ // let the worker thread continue
+ flags->lingerAtWork = false;
+
+ // make sure the job finished.
+ ASSERT_TRUE(poll([flags]() -> bool { return flags->finished; }));
+
+ // ... and that it was canceled.
+ EXPECT_TRUE(flags->wasCanceled);
+ delete flags;
+}
+
+namespace
+{
+struct JobControlPackage
+{
+ JobControlPackage()
+ {
+ // We're not ready to wait yet
+ jobCreatedMutex.lock();
+ }
+
+ ~JobControlPackage()
+ {
+ jobCreatedMutex.unlock();
+ }
+
+ bool ready = false;
+ XbmcThreads::ConditionVariable jobCreatedCond;
+ CCriticalSection jobCreatedMutex;
+};
+
+class BroadcastingJob :
+ public CJob
+{
+public:
+
+ BroadcastingJob(JobControlPackage &package) :
+ m_package(package),
+ m_finish(false)
+ {
+ }
+
+ void FinishAndStopBlocking()
+ {
+ std::unique_lock<CCriticalSection> lock(m_blockMutex);
+
+ m_finish = true;
+ m_block.notifyAll();
+ }
+
+ const char * GetType() const override
+ {
+ return "BroadcastingJob";
+ }
+
+ bool DoWork() override
+ {
+ {
+ std::unique_lock<CCriticalSection> lock(m_package.jobCreatedMutex);
+
+ m_package.ready = true;
+ m_package.jobCreatedCond.notifyAll();
+ }
+
+ std::unique_lock<CCriticalSection> blockLock(m_blockMutex);
+
+ // Block until we're told to go away
+ while (!m_finish)
+ m_block.wait(m_blockMutex);
+ return true;
+ }
+
+private:
+
+ JobControlPackage &m_package;
+
+ XbmcThreads::ConditionVariable m_block;
+ CCriticalSection m_blockMutex;
+ bool m_finish;
+};
+
+BroadcastingJob *
+WaitForJobToStartProcessing(CJob::PRIORITY priority, JobControlPackage &package)
+{
+ BroadcastingJob* job = new BroadcastingJob(package);
+ CServiceBroker::GetJobManager()->AddJob(job, nullptr, priority);
+
+ // We're now ready to wait, wait and then unblock once ready
+ while (!package.ready)
+ package.jobCreatedCond.wait(package.jobCreatedMutex);
+
+ return job;
+}
+}
+
+TEST_F(TestJobManager, PauseLowPriorityJob)
+{
+ JobControlPackage package;
+ BroadcastingJob *job (WaitForJobToStartProcessing(CJob::PRIORITY_LOW_PAUSABLE, package));
+
+ EXPECT_TRUE(CServiceBroker::GetJobManager()->IsProcessing(CJob::PRIORITY_LOW_PAUSABLE));
+ CServiceBroker::GetJobManager()->PauseJobs();
+ EXPECT_FALSE(CServiceBroker::GetJobManager()->IsProcessing(CJob::PRIORITY_LOW_PAUSABLE));
+ CServiceBroker::GetJobManager()->UnPauseJobs();
+ EXPECT_TRUE(CServiceBroker::GetJobManager()->IsProcessing(CJob::PRIORITY_LOW_PAUSABLE));
+
+ job->FinishAndStopBlocking();
+}
+
+TEST_F(TestJobManager, IsProcessing)
+{
+ JobControlPackage package;
+ BroadcastingJob *job (WaitForJobToStartProcessing(CJob::PRIORITY_LOW_PAUSABLE, package));
+
+ EXPECT_EQ(0, CServiceBroker::GetJobManager()->IsProcessing(""));
+
+ job->FinishAndStopBlocking();
+}
diff --git a/xbmc/utils/test/TestLabelFormatter.cpp b/xbmc/utils/test/TestLabelFormatter.cpp
new file mode 100644
index 0000000..633e89a
--- /dev/null
+++ b/xbmc/utils/test/TestLabelFormatter.cpp
@@ -0,0 +1,69 @@
+/*
+ * 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 "FileItem.h"
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "test/TestUtils.h"
+#include "utils/LabelFormatter.h"
+
+#include <gtest/gtest.h>
+
+/* Set default settings used by CLabelFormatter. */
+class TestLabelFormatter : public testing::Test
+{
+protected:
+ TestLabelFormatter() = default;
+
+ ~TestLabelFormatter() override
+ {
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Unload();
+ }
+};
+
+TEST_F(TestLabelFormatter, FormatLabel)
+{
+ XFILE::CFile *tmpfile;
+ std::string tmpfilepath;
+ LABEL_MASKS labelMasks;
+ CLabelFormatter formatter("", labelMasks.m_strLabel2File);
+
+ ASSERT_NE(nullptr, (tmpfile = XBMC_CREATETEMPFILE("")));
+ tmpfilepath = XBMC_TEMPFILEPATH(tmpfile);
+
+ CFileItemPtr item(new CFileItem(tmpfilepath));
+ item->SetPath(tmpfilepath);
+ item->m_bIsFolder = false;
+ item->Select(true);
+
+ formatter.FormatLabel(item.get());
+
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(tmpfile));
+}
+
+TEST_F(TestLabelFormatter, FormatLabel2)
+{
+ XFILE::CFile *tmpfile;
+ std::string tmpfilepath;
+ LABEL_MASKS labelMasks;
+ CLabelFormatter formatter("", labelMasks.m_strLabel2File);
+
+ ASSERT_NE(nullptr, (tmpfile = XBMC_CREATETEMPFILE("")));
+ tmpfilepath = XBMC_TEMPFILEPATH(tmpfile);
+
+ CFileItemPtr item(new CFileItem(tmpfilepath));
+ item->SetPath(tmpfilepath);
+ item->m_bIsFolder = false;
+ item->Select(true);
+
+ formatter.FormatLabel2(item.get());
+
+ EXPECT_TRUE(XBMC_DELETETEMPFILE(tmpfile));
+}
diff --git a/xbmc/utils/test/TestLangCodeExpander.cpp b/xbmc/utils/test/TestLangCodeExpander.cpp
new file mode 100644
index 0000000..7a6dde1
--- /dev/null
+++ b/xbmc/utils/test/TestLangCodeExpander.cpp
@@ -0,0 +1,29 @@
+/*
+ * 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 "utils/LangCodeExpander.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestLangCodeExpander, ConvertISO6391ToISO6392B)
+{
+ std::string refstr, varstr;
+
+ refstr = "eng";
+ g_LangCodeExpander.ConvertISO6391ToISO6392B("en", varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestLangCodeExpander, ConvertToISO6392B)
+{
+ std::string refstr, varstr;
+
+ refstr = "eng";
+ g_LangCodeExpander.ConvertToISO6392B("en", varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
diff --git a/xbmc/utils/test/TestLocale.cpp b/xbmc/utils/test/TestLocale.cpp
new file mode 100644
index 0000000..f5193ed
--- /dev/null
+++ b/xbmc/utils/test/TestLocale.cpp
@@ -0,0 +1,272 @@
+/*
+ * 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 "utils/Locale.h"
+#include "utils/StringUtils.h"
+
+#include <gtest/gtest.h>
+
+static const std::string TerritorySeparator = "_";
+static const std::string CodesetSeparator = ".";
+static const std::string ModifierSeparator = "@";
+
+static const std::string LanguageCodeEnglish = "en";
+static const std::string TerritoryCodeBritain = "GB";
+static const std::string CodesetUtf8 = "UTF-8";
+static const std::string ModifierLatin = "latin";
+
+TEST(TestLocale, DefaultLocale)
+{
+ CLocale locale;
+ ASSERT_FALSE(locale.IsValid());
+ ASSERT_STREQ("", locale.GetLanguageCode().c_str());
+ ASSERT_STREQ("", locale.GetTerritoryCode().c_str());
+ ASSERT_STREQ("", locale.GetCodeset().c_str());
+ ASSERT_STREQ("", locale.GetModifier().c_str());
+ ASSERT_STREQ("", locale.ToString().c_str());
+}
+
+TEST(TestLocale, LanguageLocale)
+{
+ CLocale locale(LanguageCodeEnglish);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.GetLanguageCode().c_str());
+ ASSERT_STREQ("", locale.GetTerritoryCode().c_str());
+ ASSERT_STREQ("", locale.GetCodeset().c_str());
+ ASSERT_STREQ("", locale.GetModifier().c_str());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.ToString().c_str());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.ToStringLC().c_str());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.ToShortString().c_str());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.ToShortStringLC().c_str());
+}
+
+TEST(TestLocale, LanguageTerritoryLocale)
+{
+ const std::string strLocale = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain;
+ std::string strLocaleLC = strLocale;
+ StringUtils::ToLower(strLocaleLC);
+
+ CLocale locale(LanguageCodeEnglish, TerritoryCodeBritain);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.GetLanguageCode().c_str());
+ ASSERT_STREQ(TerritoryCodeBritain.c_str(), locale.GetTerritoryCode().c_str());
+ ASSERT_STREQ("", locale.GetCodeset().c_str());
+ ASSERT_STREQ("", locale.GetModifier().c_str());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+ ASSERT_STREQ(strLocaleLC.c_str(), locale.ToStringLC().c_str());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToShortString().c_str());
+ ASSERT_STREQ(strLocaleLC.c_str(), locale.ToShortStringLC().c_str());
+}
+
+TEST(TestLocale, LanguageCodesetLocale)
+{
+ const std::string strLocale = LanguageCodeEnglish + CodesetSeparator + CodesetUtf8;
+ std::string strLocaleLC = strLocale;
+ StringUtils::ToLower(strLocaleLC);
+
+ CLocale locale(LanguageCodeEnglish, "", CodesetUtf8);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.GetLanguageCode().c_str());
+ ASSERT_STREQ("", locale.GetTerritoryCode().c_str());
+ ASSERT_STREQ(CodesetUtf8.c_str(), locale.GetCodeset().c_str());
+ ASSERT_STREQ("", locale.GetModifier().c_str());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+ ASSERT_STREQ(strLocaleLC.c_str(), locale.ToStringLC().c_str());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.ToShortString().c_str());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.ToShortStringLC().c_str());
+}
+
+TEST(TestLocale, LanguageModifierLocale)
+{
+ const std::string strLocale = LanguageCodeEnglish + ModifierSeparator + ModifierLatin;
+ std::string strLocaleLC = strLocale;
+ StringUtils::ToLower(strLocaleLC);
+
+ CLocale locale(LanguageCodeEnglish, "", "", ModifierLatin);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.GetLanguageCode().c_str());
+ ASSERT_STREQ("", locale.GetTerritoryCode().c_str());
+ ASSERT_STREQ("", locale.GetCodeset().c_str());
+ ASSERT_STREQ(ModifierLatin.c_str(), locale.GetModifier().c_str());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+ ASSERT_STREQ(strLocaleLC.c_str(), locale.ToStringLC().c_str());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.ToShortString().c_str());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.ToShortStringLC().c_str());
+}
+
+TEST(TestLocale, LanguageTerritoryCodesetLocale)
+{
+ const std::string strLocaleShort = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain;
+ std::string strLocaleShortLC = strLocaleShort;
+ StringUtils::ToLower(strLocaleShortLC);
+ const std::string strLocale = strLocaleShort + CodesetSeparator + CodesetUtf8;
+ std::string strLocaleLC = strLocale;
+ StringUtils::ToLower(strLocaleLC);
+
+ CLocale locale(LanguageCodeEnglish, TerritoryCodeBritain, CodesetUtf8);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.GetLanguageCode().c_str());
+ ASSERT_STREQ(TerritoryCodeBritain.c_str(), locale.GetTerritoryCode().c_str());
+ ASSERT_STREQ(CodesetUtf8.c_str(), locale.GetCodeset().c_str());
+ ASSERT_STREQ("", locale.GetModifier().c_str());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+ ASSERT_STREQ(strLocaleLC.c_str(), locale.ToStringLC().c_str());
+ ASSERT_STREQ(strLocaleShort.c_str(), locale.ToShortString().c_str());
+ ASSERT_STREQ(strLocaleShortLC.c_str(), locale.ToShortStringLC().c_str());
+}
+
+TEST(TestLocale, LanguageTerritoryModifierLocale)
+{
+ const std::string strLocaleShort = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain;
+ std::string strLocaleShortLC = strLocaleShort;
+ StringUtils::ToLower(strLocaleShortLC);
+ const std::string strLocale = strLocaleShort + ModifierSeparator + ModifierLatin;
+ std::string strLocaleLC = strLocale;
+ StringUtils::ToLower(strLocaleLC);
+
+ CLocale locale(LanguageCodeEnglish, TerritoryCodeBritain, "", ModifierLatin);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.GetLanguageCode().c_str());
+ ASSERT_STREQ(TerritoryCodeBritain.c_str(), locale.GetTerritoryCode().c_str());
+ ASSERT_STREQ("", locale.GetCodeset().c_str());
+ ASSERT_STREQ(ModifierLatin.c_str(), locale.GetModifier().c_str());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+ ASSERT_STREQ(strLocaleLC.c_str(), locale.ToStringLC().c_str());
+ ASSERT_STREQ(strLocaleShort.c_str(), locale.ToShortString().c_str());
+ ASSERT_STREQ(strLocaleShortLC.c_str(), locale.ToShortStringLC().c_str());
+}
+
+TEST(TestLocale, LanguageTerritoryCodesetModifierLocale)
+{
+ const std::string strLocaleShort = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain;
+ std::string strLocaleShortLC = strLocaleShort;
+ StringUtils::ToLower(strLocaleShortLC);
+ const std::string strLocale = strLocaleShort + CodesetSeparator + CodesetUtf8 + ModifierSeparator + ModifierLatin;
+ std::string strLocaleLC = strLocale;
+ StringUtils::ToLower(strLocaleLC);
+
+ CLocale locale(LanguageCodeEnglish, TerritoryCodeBritain, CodesetUtf8, ModifierLatin);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.GetLanguageCode().c_str());
+ ASSERT_STREQ(TerritoryCodeBritain.c_str(), locale.GetTerritoryCode().c_str());
+ ASSERT_STREQ(CodesetUtf8.c_str(), locale.GetCodeset().c_str());
+ ASSERT_STREQ(ModifierLatin.c_str(), locale.GetModifier().c_str());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+ ASSERT_STREQ(strLocaleLC.c_str(), locale.ToStringLC().c_str());
+ ASSERT_STREQ(strLocaleShort.c_str(), locale.ToShortString().c_str());
+ ASSERT_STREQ(strLocaleShortLC.c_str(), locale.ToShortStringLC().c_str());
+}
+
+TEST(TestLocale, FullStringLocale)
+{
+ const std::string strLocaleShort = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain;
+ std::string strLocaleShortLC = strLocaleShort;
+ StringUtils::ToLower(strLocaleShortLC);
+ const std::string strLocale = strLocaleShort + CodesetSeparator + CodesetUtf8 + ModifierSeparator + ModifierLatin;
+ std::string strLocaleLC = strLocale;
+ StringUtils::ToLower(strLocaleLC);
+
+ CLocale locale(strLocale);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(LanguageCodeEnglish.c_str(), locale.GetLanguageCode().c_str());
+ ASSERT_STREQ(TerritoryCodeBritain.c_str(), locale.GetTerritoryCode().c_str());
+ ASSERT_STREQ(CodesetUtf8.c_str(), locale.GetCodeset().c_str());
+ ASSERT_STREQ(ModifierLatin.c_str(), locale.GetModifier().c_str());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+ ASSERT_STREQ(strLocaleLC.c_str(), locale.ToStringLC().c_str());
+ ASSERT_STREQ(strLocaleShort.c_str(), locale.ToShortString().c_str());
+ ASSERT_STREQ(strLocaleShortLC.c_str(), locale.ToShortStringLC().c_str());
+}
+
+TEST(TestLocale, FromString)
+{
+ std::string strLocale = "";
+ CLocale locale = CLocale::FromString(strLocale);
+ ASSERT_FALSE(locale.IsValid());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+
+ strLocale = LanguageCodeEnglish;
+ locale = CLocale::FromString(strLocale);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+
+ strLocale = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain;
+ locale = CLocale::FromString(strLocale);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+
+ strLocale = LanguageCodeEnglish + CodesetSeparator + CodesetUtf8;
+ locale = CLocale::FromString(strLocale);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+
+ strLocale = LanguageCodeEnglish + ModifierSeparator + ModifierLatin;
+ locale = CLocale::FromString(strLocale);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+
+ strLocale = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain + CodesetSeparator + CodesetUtf8;
+ locale = CLocale::FromString(strLocale);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+
+ strLocale = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain + ModifierSeparator + ModifierLatin;
+ locale = CLocale::FromString(strLocale);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+
+ strLocale = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain + CodesetSeparator + CodesetUtf8 + ModifierSeparator + ModifierLatin;
+ locale = CLocale::FromString(strLocale);
+ ASSERT_TRUE(locale.IsValid());
+ ASSERT_STREQ(strLocale.c_str(), locale.ToString().c_str());
+}
+
+TEST(TestLocale, EmptyLocale)
+{
+ ASSERT_FALSE(CLocale::Empty.IsValid());
+ ASSERT_STREQ("", CLocale::Empty.GetLanguageCode().c_str());
+ ASSERT_STREQ("", CLocale::Empty.GetTerritoryCode().c_str());
+ ASSERT_STREQ("", CLocale::Empty.GetCodeset().c_str());
+ ASSERT_STREQ("", CLocale::Empty.GetModifier().c_str());
+ ASSERT_STREQ("", CLocale::Empty.ToString().c_str());
+}
+
+TEST(TestLocale, Equals)
+{
+ std::string strLocale = "";
+ CLocale locale;
+ ASSERT_TRUE(locale.Equals(strLocale));
+
+ locale = CLocale(LanguageCodeEnglish);
+ strLocale = LanguageCodeEnglish;
+ ASSERT_TRUE(locale.Equals(strLocale));
+
+ locale = CLocale(LanguageCodeEnglish, TerritoryCodeBritain);
+ strLocale = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain;
+ ASSERT_TRUE(locale.Equals(strLocale));
+
+ locale = CLocale(LanguageCodeEnglish, "", CodesetUtf8);
+ strLocale = LanguageCodeEnglish + CodesetSeparator + CodesetUtf8;
+ ASSERT_TRUE(locale.Equals(strLocale));
+
+ locale = CLocale(LanguageCodeEnglish, "", "", ModifierLatin);
+ strLocale = LanguageCodeEnglish + ModifierSeparator + ModifierLatin;
+ ASSERT_TRUE(locale.Equals(strLocale));
+
+ locale = CLocale(LanguageCodeEnglish, TerritoryCodeBritain, CodesetUtf8);
+ strLocale = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain + CodesetSeparator + CodesetUtf8;
+ ASSERT_TRUE(locale.Equals(strLocale));
+
+ locale = CLocale(LanguageCodeEnglish, TerritoryCodeBritain, "", ModifierLatin);
+ strLocale = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain + ModifierSeparator + ModifierLatin;
+ ASSERT_TRUE(locale.Equals(strLocale));
+
+ locale = CLocale(LanguageCodeEnglish, TerritoryCodeBritain, CodesetUtf8, ModifierLatin);
+ strLocale = LanguageCodeEnglish + TerritorySeparator + TerritoryCodeBritain + CodesetSeparator + CodesetUtf8 + ModifierSeparator + ModifierLatin;
+ ASSERT_TRUE(locale.Equals(strLocale));
+}
diff --git a/xbmc/utils/test/TestMathUtils.cpp b/xbmc/utils/test/TestMathUtils.cpp
new file mode 100644
index 0000000..d60cc3f
--- /dev/null
+++ b/xbmc/utils/test/TestMathUtils.cpp
@@ -0,0 +1,59 @@
+/*
+ * 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 "utils/MathUtils.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestMathUtils, round_int)
+{
+ int refval, varval, i;
+
+ for (i = -8; i < 8; ++i)
+ {
+ double d = 0.25*i;
+ refval = (i < 0) ? (i - 1) / 4 : (i + 2) / 4;
+ varval = MathUtils::round_int(d);
+ EXPECT_EQ(refval, varval);
+ }
+}
+
+TEST(TestMathUtils, truncate_int)
+{
+ int refval, varval, i;
+
+ for (i = -8; i < 8; ++i)
+ {
+ double d = 0.25*i;
+ refval = i / 4;
+ varval = MathUtils::truncate_int(d);
+ EXPECT_EQ(refval, varval);
+ }
+}
+
+TEST(TestMathUtils, abs)
+{
+ int64_t refval, varval;
+
+ refval = 5;
+ varval = MathUtils::abs(-5);
+ EXPECT_EQ(refval, varval);
+}
+
+TEST(TestMathUtils, bitcount)
+{
+ unsigned refval, varval;
+
+ refval = 10;
+ varval = MathUtils::bitcount(0x03FF);
+ EXPECT_EQ(refval, varval);
+
+ refval = 8;
+ varval = MathUtils::bitcount(0x2AD5);
+ EXPECT_EQ(refval, varval);
+}
diff --git a/xbmc/utils/test/TestMime.cpp b/xbmc/utils/test/TestMime.cpp
new file mode 100644
index 0000000..7ef82c3
--- /dev/null
+++ b/xbmc/utils/test/TestMime.cpp
@@ -0,0 +1,29 @@
+/*
+ * 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 "FileItem.h"
+#include "utils/Mime.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestMime, GetMimeType_string)
+{
+ EXPECT_STREQ("video/avi", CMime::GetMimeType("avi").c_str());
+ EXPECT_STRNE("video/x-msvideo", CMime::GetMimeType("avi").c_str());
+ EXPECT_STRNE("video/avi", CMime::GetMimeType("xvid").c_str());
+}
+
+TEST(TestMime, GetMimeType_CFileItem)
+{
+ std::string refstr, varstr;
+ CFileItem item("testfile.mp4", false);
+
+ refstr = "video/mp4";
+ varstr = CMime::GetMimeType(item);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
diff --git a/xbmc/utils/test/TestPOUtils.cpp b/xbmc/utils/test/TestPOUtils.cpp
new file mode 100644
index 0000000..5808c31
--- /dev/null
+++ b/xbmc/utils/test/TestPOUtils.cpp
@@ -0,0 +1,47 @@
+/*
+ * 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 "test/TestUtils.h"
+#include "utils/POUtils.h"
+
+#include <gtest/gtest.h>
+
+
+TEST(TestPOUtils, General)
+{
+ CPODocument a;
+
+ EXPECT_TRUE(a.LoadFile(XBMC_REF_FILE_PATH("xbmc/utils/test/data/language/Spanish/strings.po")));
+
+ EXPECT_TRUE(a.GetNextEntry());
+ EXPECT_EQ(ID_FOUND, a.GetEntryType());
+ EXPECT_EQ((uint32_t)0, a.GetEntryID());
+ a.ParseEntry(false);
+ EXPECT_STREQ("", a.GetMsgctxt().c_str());
+ EXPECT_STREQ("Programs", a.GetMsgid().c_str());
+ EXPECT_STREQ("Programas", a.GetMsgstr().c_str());
+ EXPECT_STREQ("", a.GetPlurMsgstr(0).c_str());
+
+ EXPECT_TRUE(a.GetNextEntry());
+ EXPECT_EQ(ID_FOUND, a.GetEntryType());
+ EXPECT_EQ((uint32_t)1, a.GetEntryID());
+ a.ParseEntry(false);
+ EXPECT_STREQ("", a.GetMsgctxt().c_str());
+ EXPECT_STREQ("Pictures", a.GetMsgid().c_str());
+ EXPECT_STREQ("Imágenes", a.GetMsgstr().c_str());
+ EXPECT_STREQ("", a.GetPlurMsgstr(0).c_str());
+
+ EXPECT_TRUE(a.GetNextEntry());
+ EXPECT_EQ(ID_FOUND, a.GetEntryType());
+ EXPECT_EQ((uint32_t)2, a.GetEntryID());
+ a.ParseEntry(false);
+ EXPECT_STREQ("", a.GetMsgctxt().c_str());
+ EXPECT_STREQ("Music", a.GetMsgid().c_str());
+ EXPECT_STREQ("Música", a.GetMsgstr().c_str());
+ EXPECT_STREQ("", a.GetPlurMsgstr(0).c_str());
+}
diff --git a/xbmc/utils/test/TestRegExp.cpp b/xbmc/utils/test/TestRegExp.cpp
new file mode 100644
index 0000000..1cd3939
--- /dev/null
+++ b/xbmc/utils/test/TestRegExp.cpp
@@ -0,0 +1,169 @@
+/*
+ * 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.
+ */
+
+/** @todo gtest/gtest.h needs to come in before utils/RegExp.h.
+ * Investigate why.
+ */
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestRegExp, RegFind)
+{
+ CRegExp regex;
+
+ EXPECT_TRUE(regex.RegComp("^Test.*"));
+ EXPECT_EQ(0, regex.RegFind("Test string."));
+
+ EXPECT_TRUE(regex.RegComp("^string.*"));
+ EXPECT_EQ(-1, regex.RegFind("Test string."));
+}
+
+TEST(TestRegExp, GetReplaceString)
+{
+ CRegExp regex;
+
+ EXPECT_TRUE(regex.RegComp("^(Test)\\s*(.*)\\."));
+ EXPECT_EQ(0, regex.RegFind("Test string."));
+ EXPECT_STREQ("string", regex.GetReplaceString("\\2").c_str());
+}
+
+TEST(TestRegExp, GetFindLen)
+{
+ CRegExp regex;
+
+ EXPECT_TRUE(regex.RegComp("^(Test)\\s*(.*)\\."));
+ EXPECT_EQ(0, regex.RegFind("Test string."));
+ EXPECT_EQ(12, regex.GetFindLen());
+}
+
+TEST(TestRegExp, GetSubCount)
+{
+ CRegExp regex;
+
+ EXPECT_TRUE(regex.RegComp("^(Test)\\s*(.*)\\."));
+ EXPECT_EQ(0, regex.RegFind("Test string."));
+ EXPECT_EQ(2, regex.GetSubCount());
+}
+
+TEST(TestRegExp, GetSubStart)
+{
+ CRegExp regex;
+
+ EXPECT_TRUE(regex.RegComp("^(Test)\\s*(.*)\\."));
+ EXPECT_EQ(0, regex.RegFind("Test string."));
+ EXPECT_EQ(0, regex.GetSubStart(0));
+ EXPECT_EQ(0, regex.GetSubStart(1));
+ EXPECT_EQ(5, regex.GetSubStart(2));
+}
+
+TEST(TestRegExp, GetCaptureTotal)
+{
+ CRegExp regex;
+
+ EXPECT_TRUE(regex.RegComp("^(Test)\\s*(.*)\\."));
+ EXPECT_EQ(0, regex.RegFind("Test string."));
+ EXPECT_EQ(2, regex.GetCaptureTotal());
+}
+
+TEST(TestRegExp, GetMatch)
+{
+ CRegExp regex;
+
+ EXPECT_TRUE(regex.RegComp("^(Test)\\s*(.*)\\."));
+ EXPECT_EQ(0, regex.RegFind("Test string."));
+ EXPECT_STREQ("Test string.", regex.GetMatch(0).c_str());
+ EXPECT_STREQ("Test", regex.GetMatch(1).c_str());
+ EXPECT_STREQ("string", regex.GetMatch(2).c_str());
+}
+
+TEST(TestRegExp, GetPattern)
+{
+ CRegExp regex;
+
+ EXPECT_TRUE(regex.RegComp("^(Test)\\s*(.*)\\."));
+ EXPECT_STREQ("^(Test)\\s*(.*)\\.", regex.GetPattern().c_str());
+}
+
+TEST(TestRegExp, GetNamedSubPattern)
+{
+ CRegExp regex;
+ std::string match;
+
+ EXPECT_TRUE(regex.RegComp("^(?<first>Test)\\s*(?<second>.*)\\."));
+ EXPECT_EQ(0, regex.RegFind("Test string."));
+ EXPECT_TRUE(regex.GetNamedSubPattern("first", match));
+ EXPECT_STREQ("Test", match.c_str());
+ EXPECT_TRUE(regex.GetNamedSubPattern("second", match));
+ EXPECT_STREQ("string", match.c_str());
+}
+
+TEST(TestRegExp, operatorEqual)
+{
+ CRegExp regex, regexcopy;
+ std::string match;
+
+ EXPECT_TRUE(regex.RegComp("^(?<first>Test)\\s*(?<second>.*)\\."));
+ regexcopy = regex;
+ EXPECT_EQ(0, regexcopy.RegFind("Test string."));
+ EXPECT_TRUE(regexcopy.GetNamedSubPattern("first", match));
+ EXPECT_STREQ("Test", match.c_str());
+ EXPECT_TRUE(regexcopy.GetNamedSubPattern("second", match));
+ EXPECT_STREQ("string", match.c_str());
+}
+
+class TestRegExpLog : public testing::Test
+{
+protected:
+ TestRegExpLog() = default;
+ ~TestRegExpLog() override { CServiceBroker::GetLogging().Deinitialize(); }
+};
+
+TEST_F(TestRegExpLog, DumpOvector)
+{
+ CRegExp regex;
+ std::string logfile, logstring;
+ char buf[100];
+ ssize_t bytesread;
+ XFILE::CFile file;
+
+ std::string appName = CCompileInfo::GetAppName();
+ StringUtils::ToLower(appName);
+ logfile = CSpecialProtocol::TranslatePath("special://temp/") + appName + ".log";
+ CServiceBroker::GetLogging().Initialize(
+ CSpecialProtocol::TranslatePath("special://temp/").c_str());
+ EXPECT_TRUE(XFILE::CFile::Exists(logfile));
+
+ EXPECT_TRUE(regex.RegComp("^(?<first>Test)\\s*(?<second>.*)\\."));
+ EXPECT_EQ(0, regex.RegFind("Test string."));
+ regex.DumpOvector(LOGDEBUG);
+ CServiceBroker::GetLogging().Deinitialize();
+
+ EXPECT_TRUE(file.Open(logfile));
+ while ((bytesread = file.Read(buf, sizeof(buf) - 1)) > 0)
+ {
+ buf[bytesread] = '\0';
+ logstring.append(buf);
+ }
+ file.Close();
+ EXPECT_FALSE(logstring.empty());
+
+ EXPECT_STREQ("\xEF\xBB\xBF", logstring.substr(0, 3).c_str());
+
+ EXPECT_TRUE(regex.RegComp(".*(debug|DEBUG) <general>: regexp ovector=\\{\\[0,12\\],\\[0,4\\],"
+ "\\[5,11\\]\\}.*"));
+ EXPECT_GE(regex.RegFind(logstring), 0);
+
+ EXPECT_TRUE(XFILE::CFile::Delete(logfile));
+}
diff --git a/xbmc/utils/test/TestRingBuffer.cpp b/xbmc/utils/test/TestRingBuffer.cpp
new file mode 100644
index 0000000..e2fd2d5
--- /dev/null
+++ b/xbmc/utils/test/TestRingBuffer.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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 "utils/RingBuffer.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestRingBuffer, General)
+{
+ CRingBuffer a;
+ char data[20];
+ unsigned int i;
+
+ EXPECT_TRUE(a.Create(20));
+ EXPECT_EQ((unsigned int)20, a.getSize());
+ memset(data, 0, sizeof(data));
+ for (i = 0; i < a.getSize(); i++)
+ EXPECT_TRUE(a.WriteData(data, 1));
+ a.Clear();
+
+ memcpy(data, "0123456789", sizeof("0123456789"));
+ EXPECT_TRUE(a.WriteData(data, sizeof("0123456789")));
+ EXPECT_STREQ("0123456789", a.getBuffer());
+
+ memset(data, 0, sizeof(data));
+ EXPECT_TRUE(a.ReadData(data, 5));
+ EXPECT_STREQ("01234", data);
+}
diff --git a/xbmc/utils/test/TestScraperParser.cpp b/xbmc/utils/test/TestScraperParser.cpp
new file mode 100644
index 0000000..4ff4b06
--- /dev/null
+++ b/xbmc/utils/test/TestScraperParser.cpp
@@ -0,0 +1,24 @@
+/*
+ * 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 "test/TestUtils.h"
+#include "utils/ScraperParser.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestScraperParser, General)
+{
+ CScraperParser a;
+
+ a.Clear();
+ EXPECT_TRUE(a.Load(XBMC_REF_FILE_PATH("/addons/metadata.local/local.xml")));
+
+ EXPECT_STREQ(XBMC_REF_FILE_PATH("/addons/metadata.local/local.xml").c_str(),
+ a.GetFilename().c_str());
+ EXPECT_STREQ("UTF-8", a.GetSearchStringEncoding().c_str());
+}
diff --git a/xbmc/utils/test/TestScraperUrl.cpp b/xbmc/utils/test/TestScraperUrl.cpp
new file mode 100644
index 0000000..1feb181
--- /dev/null
+++ b/xbmc/utils/test/TestScraperUrl.cpp
@@ -0,0 +1,34 @@
+/*
+ * 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 "utils/ScraperUrl.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestScraperUrl, General)
+{
+ CScraperUrl a;
+ std::string xmlstring;
+
+ xmlstring = "<data spoof=\"blah\" gzip=\"yes\">\n"
+ " <someurl>\n"
+ " </someurl>\n"
+ " <someotherurl>\n"
+ " </someotherurl>\n"
+ "</data>\n";
+ EXPECT_TRUE(a.ParseFromData(xmlstring));
+
+ const auto url = a.GetFirstUrlByType();
+ EXPECT_STREQ("blah", url.m_spoof.c_str());
+ EXPECT_STREQ("someurl", url.m_url.c_str());
+ EXPECT_STREQ("", url.m_cache.c_str());
+ EXPECT_EQ(CScraperUrl::UrlType::General, url.m_type);
+ EXPECT_FALSE(url.m_post);
+ EXPECT_TRUE(url.m_isgz);
+ EXPECT_EQ(-1, url.m_season);
+}
diff --git a/xbmc/utils/test/TestSortUtils.cpp b/xbmc/utils/test/TestSortUtils.cpp
new file mode 100644
index 0000000..dac3c62
--- /dev/null
+++ b/xbmc/utils/test/TestSortUtils.cpp
@@ -0,0 +1,123 @@
+/*
+ * 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 "utils/SortUtils.h"
+#include "utils/Variant.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestSortUtils, Sort_SortBy)
+{
+ SortItems items;
+
+ CVariant variant1("M Artist");
+ SortItemPtr item1(new SortItem());
+ (*item1)[FieldArtist] = variant1;
+ CVariant variant2("B Artist");
+ SortItemPtr item2(new SortItem());
+ (*item2)[FieldArtist] = variant2;
+ CVariant variant3("R Artist");
+ SortItemPtr item3(new SortItem());
+ (*item3)[FieldArtist] = variant3;
+ CVariant variant4("R Artist");
+ SortItemPtr item4(new SortItem());
+ (*item4)[FieldArtist] = variant4;
+ CVariant variant5("I Artist");
+ SortItemPtr item5(new SortItem());
+ (*item5)[FieldArtist] = variant5;
+ CVariant variant6("A Artist");
+ SortItemPtr item6(new SortItem());
+ (*item6)[FieldArtist] = variant6;
+ CVariant variant7("G Artist");
+ SortItemPtr item7(new SortItem());
+ (*item7)[FieldArtist] = variant7;
+
+ items.push_back(item1);
+ items.push_back(item2);
+ items.push_back(item3);
+ items.push_back(item4);
+ items.push_back(item5);
+ items.push_back(item6);
+ items.push_back(item7);
+
+ SortUtils::Sort(SortByArtist, SortOrderAscending, SortAttributeNone, items);
+
+ EXPECT_STREQ("A Artist", (*items.at(0))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("B Artist", (*items.at(1))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("G Artist", (*items.at(2))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("I Artist", (*items.at(3))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("M Artist", (*items.at(4))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("R Artist", (*items.at(5))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("R Artist", (*items.at(6))[FieldArtist].asString().c_str());
+}
+
+TEST(TestSortUtils, Sort_SortDescription)
+{
+ SortItems items;
+
+ CVariant variant1("M Artist");
+ SortItemPtr item1(new SortItem());
+ (*item1)[FieldArtist] = variant1;
+ CVariant variant2("B Artist");
+ SortItemPtr item2(new SortItem());
+ (*item2)[FieldArtist] = variant2;
+ CVariant variant3("R Artist");
+ SortItemPtr item3(new SortItem());
+ (*item3)[FieldArtist] = variant3;
+ CVariant variant4("R Artist");
+ SortItemPtr item4(new SortItem());
+ (*item4)[FieldArtist] = variant4;
+ CVariant variant5("I Artist");
+ SortItemPtr item5(new SortItem());
+ (*item5)[FieldArtist] = variant5;
+ CVariant variant6("A Artist");
+ SortItemPtr item6(new SortItem());
+ (*item6)[FieldArtist] = variant6;
+ CVariant variant7("G Artist");
+ SortItemPtr item7(new SortItem());
+ (*item7)[FieldArtist] = variant7;
+
+ items.push_back(item1);
+ items.push_back(item2);
+ items.push_back(item3);
+ items.push_back(item4);
+ items.push_back(item5);
+ items.push_back(item6);
+ items.push_back(item7);
+
+ SortDescription desc;
+ desc.sortBy = SortByArtist;
+ SortUtils::Sort(desc, items);
+
+ EXPECT_STREQ("A Artist", (*items.at(0))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("B Artist", (*items.at(1))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("G Artist", (*items.at(2))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("I Artist", (*items.at(3))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("M Artist", (*items.at(4))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("R Artist", (*items.at(5))[FieldArtist].asString().c_str());
+ EXPECT_STREQ("R Artist", (*items.at(6))[FieldArtist].asString().c_str());
+}
+
+TEST(TestSortUtils, GetFieldsForSorting)
+{
+ Fields fields;
+
+ fields = SortUtils::GetFieldsForSorting(SortByArtist);
+ Fields::iterator it;
+ it = fields.find(FieldAlbum);
+ EXPECT_EQ(FieldAlbum, *it);
+ it = fields.find(FieldArtist);
+ EXPECT_EQ(FieldArtist, *it);
+ it = fields.find(FieldArtistSort);
+ EXPECT_EQ(FieldArtistSort, *it);
+ it = fields.find(FieldYear);
+ EXPECT_EQ(FieldYear, *it);
+ it = fields.find(FieldTrackNumber);
+ EXPECT_EQ(FieldTrackNumber, *it);
+ EXPECT_EQ((unsigned int)5, fields.size());
+}
diff --git a/xbmc/utils/test/TestStopwatch.cpp b/xbmc/utils/test/TestStopwatch.cpp
new file mode 100644
index 0000000..82f555d
--- /dev/null
+++ b/xbmc/utils/test/TestStopwatch.cpp
@@ -0,0 +1,66 @@
+/*
+ * 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 "threads/Thread.h"
+#include "utils/Stopwatch.h"
+
+#include <gtest/gtest.h>
+
+using namespace std::chrono_literals;
+
+class CTestStopWatchThread : public CThread
+{
+public:
+ CTestStopWatchThread() :
+ CThread("TestStopWatch"){}
+};
+
+TEST(TestStopWatch, Initialization)
+{
+ CStopWatch a;
+ EXPECT_FALSE(a.IsRunning());
+ EXPECT_EQ(0.0f, a.GetElapsedSeconds());
+ EXPECT_EQ(0.0f, a.GetElapsedMilliseconds());
+}
+
+TEST(TestStopWatch, Start)
+{
+ CStopWatch a;
+ a.Start();
+ EXPECT_TRUE(a.IsRunning());
+}
+
+TEST(TestStopWatch, Stop)
+{
+ CStopWatch a;
+ a.Start();
+ a.Stop();
+ EXPECT_FALSE(a.IsRunning());
+}
+
+TEST(TestStopWatch, ElapsedTime)
+{
+ CStopWatch a;
+ CTestStopWatchThread thread;
+ a.Start();
+ thread.Sleep(1ms);
+ EXPECT_GT(a.GetElapsedSeconds(), 0.0f);
+ EXPECT_GT(a.GetElapsedMilliseconds(), 0.0f);
+}
+
+TEST(TestStopWatch, Reset)
+{
+ CStopWatch a;
+ CTestStopWatchThread thread;
+ a.StartZero();
+ thread.Sleep(2ms);
+ EXPECT_GT(a.GetElapsedMilliseconds(), 1);
+ thread.Sleep(3ms);
+ a.Reset();
+ EXPECT_LT(a.GetElapsedMilliseconds(), 5);
+}
diff --git a/xbmc/utils/test/TestStreamDetails.cpp b/xbmc/utils/test/TestStreamDetails.cpp
new file mode 100644
index 0000000..7842eee
--- /dev/null
+++ b/xbmc/utils/test/TestStreamDetails.cpp
@@ -0,0 +1,76 @@
+/*
+ * 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 "utils/StreamDetails.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestStreamDetails, General)
+{
+ CStreamDetails a;
+ CStreamDetailVideo *video = new CStreamDetailVideo();
+ CStreamDetailAudio *audio = new CStreamDetailAudio();
+ CStreamDetailSubtitle *subtitle = new CStreamDetailSubtitle();
+
+ video->m_iWidth = 1920;
+ video->m_iHeight = 1080;
+ video->m_fAspect = 2.39f;
+ video->m_iDuration = 30;
+ video->m_strCodec = "h264";
+ video->m_strStereoMode = "left_right";
+ video->m_strLanguage = "eng";
+
+ audio->m_iChannels = 2;
+ audio->m_strCodec = "aac";
+ audio->m_strLanguage = "eng";
+
+ subtitle->m_strLanguage = "eng";
+
+ a.AddStream(video);
+ a.AddStream(audio);
+
+ EXPECT_TRUE(a.HasItems());
+
+ EXPECT_EQ(1, a.GetStreamCount(CStreamDetail::VIDEO));
+ EXPECT_EQ(1, a.GetVideoStreamCount());
+ EXPECT_STREQ("", a.GetVideoCodec().c_str());
+ EXPECT_EQ(0.0f, a.GetVideoAspect());
+ EXPECT_EQ(0, a.GetVideoWidth());
+ EXPECT_EQ(0, a.GetVideoHeight());
+ EXPECT_EQ(0, a.GetVideoDuration());
+ EXPECT_STREQ("", a.GetStereoMode().c_str());
+
+ EXPECT_EQ(1, a.GetStreamCount(CStreamDetail::AUDIO));
+ EXPECT_EQ(1, a.GetAudioStreamCount());
+
+ EXPECT_EQ(0, a.GetStreamCount(CStreamDetail::SUBTITLE));
+ EXPECT_EQ(0, a.GetSubtitleStreamCount());
+
+ a.AddStream(subtitle);
+ EXPECT_EQ(1, a.GetStreamCount(CStreamDetail::SUBTITLE));
+ EXPECT_EQ(1, a.GetSubtitleStreamCount());
+
+ a.DetermineBestStreams();
+ EXPECT_STREQ("h264", a.GetVideoCodec().c_str());
+ EXPECT_EQ(2.39f, a.GetVideoAspect());
+ EXPECT_EQ(1920, a.GetVideoWidth());
+ EXPECT_EQ(1080, a.GetVideoHeight());
+ EXPECT_EQ(30, a.GetVideoDuration());
+ EXPECT_STREQ("left_right", a.GetStereoMode().c_str());
+}
+
+TEST(TestStreamDetails, VideoDimsToResolutionDescription)
+{
+ EXPECT_STREQ("1080",
+ CStreamDetails::VideoDimsToResolutionDescription(1920, 1080).c_str());
+}
+
+TEST(TestStreamDetails, VideoAspectToAspectDescription)
+{
+ EXPECT_STREQ("2.40", CStreamDetails::VideoAspectToAspectDescription(2.39f).c_str());
+}
diff --git a/xbmc/utils/test/TestStreamUtils.cpp b/xbmc/utils/test/TestStreamUtils.cpp
new file mode 100644
index 0000000..e23f958
--- /dev/null
+++ b/xbmc/utils/test/TestStreamUtils.cpp
@@ -0,0 +1,23 @@
+/*
+ * 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 "utils/StreamUtils.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestStreamUtils, General)
+{
+ EXPECT_EQ(0, StreamUtils::GetCodecPriority(""));
+ EXPECT_EQ(1, StreamUtils::GetCodecPriority("ac3"));
+ EXPECT_EQ(2, StreamUtils::GetCodecPriority("dca"));
+ EXPECT_EQ(3, StreamUtils::GetCodecPriority("eac3"));
+ EXPECT_EQ(4, StreamUtils::GetCodecPriority("dtshd_hra"));
+ EXPECT_EQ(5, StreamUtils::GetCodecPriority("dtshd_ma"));
+ EXPECT_EQ(6, StreamUtils::GetCodecPriority("truehd"));
+ EXPECT_EQ(7, StreamUtils::GetCodecPriority("flac"));
+}
diff --git a/xbmc/utils/test/TestStringUtils.cpp b/xbmc/utils/test/TestStringUtils.cpp
new file mode 100644
index 0000000..82a78b1
--- /dev/null
+++ b/xbmc/utils/test/TestStringUtils.cpp
@@ -0,0 +1,609 @@
+/*
+ * 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 "utils/StringUtils.h"
+
+#include <algorithm>
+
+#include <gtest/gtest.h>
+enum class ECG
+{
+ A,
+ B
+};
+
+enum EG
+{
+ C,
+ D
+};
+
+namespace test_enum
+{
+enum class ECN
+{
+ A = 1,
+ B
+};
+enum EN
+{
+ C = 1,
+ D
+};
+}
+TEST(TestStringUtils, Format)
+{
+ std::string refstr = "test 25 2.7 ff FF";
+
+ std::string varstr =
+ StringUtils::Format("{} {} {:.1f} {:x} {:02X}", "test", 25, 2.743f, 0x00ff, 0x00ff);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ varstr = StringUtils::Format("", "test", 25, 2.743f, 0x00ff, 0x00ff);
+ EXPECT_STREQ("", varstr.c_str());
+}
+
+TEST(TestStringUtils, FormatEnum)
+{
+ const char* zero = "0";
+ const char* one = "1";
+
+ std::string varstr = StringUtils::Format("{}", ECG::A);
+ EXPECT_STREQ(zero, varstr.c_str());
+
+ varstr = StringUtils::Format("{}", EG::C);
+ EXPECT_STREQ(zero, varstr.c_str());
+
+ varstr = StringUtils::Format("{}", test_enum::ECN::A);
+ EXPECT_STREQ(one, varstr.c_str());
+
+ varstr = StringUtils::Format("{}", test_enum::EN::C);
+ EXPECT_STREQ(one, varstr.c_str());
+}
+
+TEST(TestStringUtils, FormatEnumWidth)
+{
+ const char* one = "01";
+
+ std::string varstr = StringUtils::Format("{:02d}", ECG::B);
+ EXPECT_STREQ(one, varstr.c_str());
+
+ varstr = StringUtils::Format("{:02}", EG::D);
+ EXPECT_STREQ(one, varstr.c_str());
+}
+
+TEST(TestStringUtils, ToUpper)
+{
+ std::string refstr = "TEST";
+
+ std::string varstr = "TeSt";
+ StringUtils::ToUpper(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, ToLower)
+{
+ std::string refstr = "test";
+
+ std::string varstr = "TeSt";
+ StringUtils::ToLower(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, ToCapitalize)
+{
+ std::string refstr = "Test";
+ std::string varstr = "test";
+ StringUtils::ToCapitalize(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "Just A Test";
+ varstr = "just a test";
+ StringUtils::ToCapitalize(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "Test -1;2:3, String For Case";
+ varstr = "test -1;2:3, string for Case";
+ StringUtils::ToCapitalize(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = " JuST Another\t\tTEst:\nWoRKs ";
+ varstr = " juST another\t\ttEst:\nwoRKs ";
+ StringUtils::ToCapitalize(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "N.Y.P.D";
+ varstr = "n.y.p.d";
+ StringUtils::ToCapitalize(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "N-Y-P-D";
+ varstr = "n-y-p-d";
+ StringUtils::ToCapitalize(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, EqualsNoCase)
+{
+ std::string refstr = "TeSt";
+
+ EXPECT_TRUE(StringUtils::EqualsNoCase(refstr, "TeSt"));
+ EXPECT_TRUE(StringUtils::EqualsNoCase(refstr, "tEsT"));
+}
+
+TEST(TestStringUtils, Left)
+{
+ std::string refstr, varstr;
+ std::string origstr = "test";
+
+ refstr = "";
+ varstr = StringUtils::Left(origstr, 0);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "te";
+ varstr = StringUtils::Left(origstr, 2);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "test";
+ varstr = StringUtils::Left(origstr, 10);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, Mid)
+{
+ std::string refstr, varstr;
+ std::string origstr = "test";
+
+ refstr = "";
+ varstr = StringUtils::Mid(origstr, 0, 0);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "te";
+ varstr = StringUtils::Mid(origstr, 0, 2);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "test";
+ varstr = StringUtils::Mid(origstr, 0, 10);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "st";
+ varstr = StringUtils::Mid(origstr, 2);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "st";
+ varstr = StringUtils::Mid(origstr, 2, 2);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "es";
+ varstr = StringUtils::Mid(origstr, 1, 2);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, Right)
+{
+ std::string refstr, varstr;
+ std::string origstr = "test";
+
+ refstr = "";
+ varstr = StringUtils::Right(origstr, 0);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "st";
+ varstr = StringUtils::Right(origstr, 2);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ refstr = "test";
+ varstr = StringUtils::Right(origstr, 10);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, Trim)
+{
+ std::string refstr = "test test";
+
+ std::string varstr = " test test ";
+ StringUtils::Trim(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, TrimLeft)
+{
+ std::string refstr = "test test ";
+
+ std::string varstr = " test test ";
+ StringUtils::TrimLeft(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, TrimRight)
+{
+ std::string refstr = " test test";
+
+ std::string varstr = " test test ";
+ StringUtils::TrimRight(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, Replace)
+{
+ std::string refstr = "text text";
+
+ std::string varstr = "test test";
+ EXPECT_EQ(StringUtils::Replace(varstr, 's', 'x'), 2);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ EXPECT_EQ(StringUtils::Replace(varstr, 's', 'x'), 0);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ varstr = "test test";
+ EXPECT_EQ(StringUtils::Replace(varstr, "s", "x"), 2);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+
+ EXPECT_EQ(StringUtils::Replace(varstr, "s", "x"), 0);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, StartsWith)
+{
+ std::string refstr = "test";
+
+ EXPECT_FALSE(StringUtils::StartsWithNoCase(refstr, "x"));
+
+ EXPECT_TRUE(StringUtils::StartsWith(refstr, "te"));
+ EXPECT_TRUE(StringUtils::StartsWith(refstr, "test"));
+ EXPECT_FALSE(StringUtils::StartsWith(refstr, "Te"));
+
+ EXPECT_TRUE(StringUtils::StartsWithNoCase(refstr, "Te"));
+ EXPECT_TRUE(StringUtils::StartsWithNoCase(refstr, "TesT"));
+}
+
+TEST(TestStringUtils, EndsWith)
+{
+ std::string refstr = "test";
+
+ EXPECT_FALSE(StringUtils::EndsWithNoCase(refstr, "x"));
+
+ EXPECT_TRUE(StringUtils::EndsWith(refstr, "st"));
+ EXPECT_TRUE(StringUtils::EndsWith(refstr, "test"));
+ EXPECT_FALSE(StringUtils::EndsWith(refstr, "sT"));
+
+ EXPECT_TRUE(StringUtils::EndsWithNoCase(refstr, "sT"));
+ EXPECT_TRUE(StringUtils::EndsWithNoCase(refstr, "TesT"));
+}
+
+TEST(TestStringUtils, Join)
+{
+ std::string refstr, varstr;
+ std::vector<std::string> strarray;
+
+ strarray.emplace_back("a");
+ strarray.emplace_back("b");
+ strarray.emplace_back("c");
+ strarray.emplace_back("de");
+ strarray.emplace_back(",");
+ strarray.emplace_back("fg");
+ strarray.emplace_back(",");
+ refstr = "a,b,c,de,,,fg,,";
+ varstr = StringUtils::Join(strarray, ",");
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, Split)
+{
+ std::vector<std::string> varresults;
+
+ // test overload with string as delimiter
+ varresults = StringUtils::Split("g,h,ij,k,lm,,n", ",");
+ EXPECT_STREQ("g", varresults.at(0).c_str());
+ EXPECT_STREQ("h", varresults.at(1).c_str());
+ EXPECT_STREQ("ij", varresults.at(2).c_str());
+ EXPECT_STREQ("k", varresults.at(3).c_str());
+ EXPECT_STREQ("lm", varresults.at(4).c_str());
+ EXPECT_STREQ("", varresults.at(5).c_str());
+ EXPECT_STREQ("n", varresults.at(6).c_str());
+
+ EXPECT_TRUE(StringUtils::Split("", "|").empty());
+
+ EXPECT_EQ(4U, StringUtils::Split("a bc d ef ghi ", " ", 4).size());
+ EXPECT_STREQ("d ef ghi ", StringUtils::Split("a bc d ef ghi ", " ", 4).at(3).c_str()) << "Last part must include rest of the input string";
+ EXPECT_EQ(7U, StringUtils::Split("a bc d ef ghi ", " ").size()) << "Result must be 7 strings including two empty strings";
+ EXPECT_STREQ("bc", StringUtils::Split("a bc d ef ghi ", " ").at(1).c_str());
+ EXPECT_STREQ("", StringUtils::Split("a bc d ef ghi ", " ").at(2).c_str());
+ EXPECT_STREQ("", StringUtils::Split("a bc d ef ghi ", " ").at(6).c_str());
+
+ EXPECT_EQ(2U, StringUtils::Split("a bc d ef ghi ", " ").size());
+ EXPECT_EQ(2U, StringUtils::Split("a bc d ef ghi ", " ", 10).size());
+ EXPECT_STREQ("a bc", StringUtils::Split("a bc d ef ghi ", " ", 10).at(0).c_str());
+
+ EXPECT_EQ(1U, StringUtils::Split("a bc d ef ghi ", " z").size());
+ EXPECT_STREQ("a bc d ef ghi ", StringUtils::Split("a bc d ef ghi ", " z").at(0).c_str());
+
+ EXPECT_EQ(1U, StringUtils::Split("a bc d ef ghi ", "").size());
+ EXPECT_STREQ("a bc d ef ghi ", StringUtils::Split("a bc d ef ghi ", "").at(0).c_str());
+
+ // test overload with char as delimiter
+ EXPECT_EQ(4U, StringUtils::Split("a bc d ef ghi ", ' ', 4).size());
+ EXPECT_STREQ("d ef ghi ", StringUtils::Split("a bc d ef ghi ", ' ', 4).at(3).c_str());
+ EXPECT_EQ(7U, StringUtils::Split("a bc d ef ghi ", ' ').size()) << "Result must be 7 strings including two empty strings";
+ EXPECT_STREQ("bc", StringUtils::Split("a bc d ef ghi ", ' ').at(1).c_str());
+ EXPECT_STREQ("", StringUtils::Split("a bc d ef ghi ", ' ').at(2).c_str());
+ EXPECT_STREQ("", StringUtils::Split("a bc d ef ghi ", ' ').at(6).c_str());
+
+ EXPECT_EQ(1U, StringUtils::Split("a bc d ef ghi ", 'z').size());
+ EXPECT_STREQ("a bc d ef ghi ", StringUtils::Split("a bc d ef ghi ", 'z').at(0).c_str());
+
+ EXPECT_EQ(1U, StringUtils::Split("a bc d ef ghi ", "").size());
+ EXPECT_STREQ("a bc d ef ghi ", StringUtils::Split("a bc d ef ghi ", 'z').at(0).c_str());
+}
+
+TEST(TestStringUtils, FindNumber)
+{
+ EXPECT_EQ(3, StringUtils::FindNumber("aabcaadeaa", "aa"));
+ EXPECT_EQ(1, StringUtils::FindNumber("aabcaadeaa", "b"));
+}
+
+TEST(TestStringUtils, AlphaNumericCompare)
+{
+ int64_t ref, var;
+
+ ref = 0;
+ var = StringUtils::AlphaNumericCompare(L"123abc", L"abc123");
+ EXPECT_LT(var, ref);
+}
+
+TEST(TestStringUtils, TimeStringToSeconds)
+{
+ EXPECT_EQ(77455, StringUtils::TimeStringToSeconds("21:30:55"));
+ EXPECT_EQ(7*60, StringUtils::TimeStringToSeconds("7 min"));
+ EXPECT_EQ(7*60, StringUtils::TimeStringToSeconds("7 min\t"));
+ EXPECT_EQ(154*60, StringUtils::TimeStringToSeconds(" 154 min"));
+ EXPECT_EQ(1*60+1, StringUtils::TimeStringToSeconds("1:01"));
+ EXPECT_EQ(4*60+3, StringUtils::TimeStringToSeconds("4:03"));
+ EXPECT_EQ(2*3600+4*60+3, StringUtils::TimeStringToSeconds("2:04:03"));
+ EXPECT_EQ(2*3600+4*60+3, StringUtils::TimeStringToSeconds(" 2:4:3"));
+ EXPECT_EQ(2*3600+4*60+3, StringUtils::TimeStringToSeconds(" \t\t 02:04:03 \n "));
+ EXPECT_EQ(1*3600+5*60+2, StringUtils::TimeStringToSeconds("01:05:02:04:03 \n "));
+ EXPECT_EQ(0, StringUtils::TimeStringToSeconds("blah"));
+ EXPECT_EQ(0, StringUtils::TimeStringToSeconds("ля-ля"));
+}
+
+TEST(TestStringUtils, RemoveCRLF)
+{
+ std::string refstr, varstr;
+
+ refstr = "test\r\nstring\nblah blah";
+ varstr = "test\r\nstring\nblah blah\n";
+ StringUtils::RemoveCRLF(varstr);
+ EXPECT_STREQ(refstr.c_str(), varstr.c_str());
+}
+
+TEST(TestStringUtils, utf8_strlen)
+{
+ size_t ref, var;
+
+ ref = 9;
+ var = StringUtils::utf8_strlen("test_UTF8");
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestStringUtils, SecondsToTimeString)
+{
+ std::string ref, var;
+
+ ref = "21:30:55";
+ var = StringUtils::SecondsToTimeString(77455);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST(TestStringUtils, IsNaturalNumber)
+{
+ EXPECT_TRUE(StringUtils::IsNaturalNumber("10"));
+ EXPECT_TRUE(StringUtils::IsNaturalNumber(" 10"));
+ EXPECT_TRUE(StringUtils::IsNaturalNumber("0"));
+ EXPECT_FALSE(StringUtils::IsNaturalNumber(" 1 0"));
+ EXPECT_FALSE(StringUtils::IsNaturalNumber("1.0"));
+ EXPECT_FALSE(StringUtils::IsNaturalNumber("1.1"));
+ EXPECT_FALSE(StringUtils::IsNaturalNumber("0x1"));
+ EXPECT_FALSE(StringUtils::IsNaturalNumber("blah"));
+ EXPECT_FALSE(StringUtils::IsNaturalNumber("120 h"));
+ EXPECT_FALSE(StringUtils::IsNaturalNumber(" "));
+ EXPECT_FALSE(StringUtils::IsNaturalNumber(""));
+}
+
+TEST(TestStringUtils, IsInteger)
+{
+ EXPECT_TRUE(StringUtils::IsInteger("10"));
+ EXPECT_TRUE(StringUtils::IsInteger(" -10"));
+ EXPECT_TRUE(StringUtils::IsInteger("0"));
+ EXPECT_FALSE(StringUtils::IsInteger(" 1 0"));
+ EXPECT_FALSE(StringUtils::IsInteger("1.0"));
+ EXPECT_FALSE(StringUtils::IsInteger("1.1"));
+ EXPECT_FALSE(StringUtils::IsInteger("0x1"));
+ EXPECT_FALSE(StringUtils::IsInteger("blah"));
+ EXPECT_FALSE(StringUtils::IsInteger("120 h"));
+ EXPECT_FALSE(StringUtils::IsInteger(" "));
+ EXPECT_FALSE(StringUtils::IsInteger(""));
+}
+
+TEST(TestStringUtils, SizeToString)
+{
+ std::string ref, var;
+
+ ref = "2.00 GB";
+ var = StringUtils::SizeToString(2147483647);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ ref = "0.00 B";
+ var = StringUtils::SizeToString(0);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST(TestStringUtils, EmptyString)
+{
+ EXPECT_STREQ("", StringUtils::Empty.c_str());
+}
+
+TEST(TestStringUtils, FindWords)
+{
+ size_t ref, var;
+
+ ref = 5;
+ var = StringUtils::FindWords("test string", "string");
+ EXPECT_EQ(ref, var);
+ var = StringUtils::FindWords("12345string", "string");
+ EXPECT_EQ(ref, var);
+ var = StringUtils::FindWords("apple2012", "2012");
+ EXPECT_EQ(ref, var);
+ ref = -1;
+ var = StringUtils::FindWords("12345string", "ring");
+ EXPECT_EQ(ref, var);
+ var = StringUtils::FindWords("12345string", "345");
+ EXPECT_EQ(ref, var);
+ var = StringUtils::FindWords("apple2012", "e2012");
+ EXPECT_EQ(ref, var);
+ var = StringUtils::FindWords("apple2012", "12");
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestStringUtils, FindWords_NonAscii)
+{
+ size_t ref, var;
+
+ ref = 6;
+ var = StringUtils::FindWords("我的视频", "视频");
+ EXPECT_EQ(ref, var);
+ var = StringUtils::FindWords("我的视频", "视");
+ EXPECT_EQ(ref, var);
+ var = StringUtils::FindWords("Apple ple", "ple");
+ EXPECT_EQ(ref, var);
+ ref = 7;
+ var = StringUtils::FindWords("Äpfel.pfel", "pfel");
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestStringUtils, FindEndBracket)
+{
+ int ref, var;
+
+ ref = 11;
+ var = StringUtils::FindEndBracket("atest testbb test", 'a', 'b');
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestStringUtils, DateStringToYYYYMMDD)
+{
+ int ref, var;
+
+ ref = 20120706;
+ var = StringUtils::DateStringToYYYYMMDD("2012-07-06");
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestStringUtils, WordToDigits)
+{
+ std::string ref, var;
+
+ ref = "8378 787464";
+ var = "test string";
+ StringUtils::WordToDigits(var);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST(TestStringUtils, CreateUUID)
+{
+ std::cout << "CreateUUID(): " << StringUtils::CreateUUID() << std::endl;
+}
+
+TEST(TestStringUtils, ValidateUUID)
+{
+ EXPECT_TRUE(StringUtils::ValidateUUID(StringUtils::CreateUUID()));
+}
+
+TEST(TestStringUtils, CompareFuzzy)
+{
+ double ref, var;
+
+ ref = 6.25;
+ var = StringUtils::CompareFuzzy("test string", "string test");
+ EXPECT_EQ(ref, var);
+}
+
+TEST(TestStringUtils, FindBestMatch)
+{
+ double refdouble, vardouble;
+ int refint, varint;
+ std::vector<std::string> strarray;
+
+ refint = 3;
+ refdouble = 0.5625;
+ strarray.emplace_back("");
+ strarray.emplace_back("a");
+ strarray.emplace_back("e");
+ strarray.emplace_back("es");
+ strarray.emplace_back("t");
+ varint = StringUtils::FindBestMatch("test", strarray, vardouble);
+ EXPECT_EQ(refint, varint);
+ EXPECT_EQ(refdouble, vardouble);
+}
+
+TEST(TestStringUtils, Paramify)
+{
+ const char *input = "some, very \\ odd \"string\"";
+ const char *ref = "\"some, very \\\\ odd \\\"string\\\"\"";
+
+ std::string result = StringUtils::Paramify(input);
+ EXPECT_STREQ(ref, result.c_str());
+}
+
+TEST(TestStringUtils, sortstringbyname)
+{
+ std::vector<std::string> strarray;
+ strarray.emplace_back("B");
+ strarray.emplace_back("c");
+ strarray.emplace_back("a");
+ std::sort(strarray.begin(), strarray.end(), sortstringbyname());
+
+ EXPECT_STREQ("a", strarray[0].c_str());
+ EXPECT_STREQ("B", strarray[1].c_str());
+ EXPECT_STREQ("c", strarray[2].c_str());
+}
+
+TEST(TestStringUtils, FileSizeFormat)
+{
+ EXPECT_STREQ("0B", StringUtils::FormatFileSize(0).c_str());
+
+ EXPECT_STREQ("999B", StringUtils::FormatFileSize(999).c_str());
+ EXPECT_STREQ("0.98kB", StringUtils::FormatFileSize(1000).c_str());
+
+ EXPECT_STREQ("1.00kB", StringUtils::FormatFileSize(1024).c_str());
+ EXPECT_STREQ("9.99kB", StringUtils::FormatFileSize(10229).c_str());
+
+ EXPECT_STREQ("10.1kB", StringUtils::FormatFileSize(10387).c_str());
+ EXPECT_STREQ("99.9kB", StringUtils::FormatFileSize(102297).c_str());
+
+ EXPECT_STREQ("100kB", StringUtils::FormatFileSize(102400).c_str());
+ EXPECT_STREQ("999kB", StringUtils::FormatFileSize(1023431).c_str());
+
+ EXPECT_STREQ("0.98MB", StringUtils::FormatFileSize(1023897).c_str());
+ EXPECT_STREQ("0.98MB", StringUtils::FormatFileSize(1024000).c_str());
+
+ //Last unit should overflow the 3 digit limit
+ EXPECT_STREQ("5432PB", StringUtils::FormatFileSize(6115888293969133568).c_str());
+}
+
+TEST(TestStringUtils, ToHexadecimal)
+{
+ EXPECT_STREQ("", StringUtils::ToHexadecimal("").c_str());
+ EXPECT_STREQ("616263", StringUtils::ToHexadecimal("abc").c_str());
+ std::string a{"a\0b\n", 4};
+ EXPECT_STREQ("6100620a", StringUtils::ToHexadecimal(a).c_str());
+ std::string nul{"\0", 1};
+ EXPECT_STREQ("00", StringUtils::ToHexadecimal(nul).c_str());
+ std::string ff{"\xFF", 1};
+ EXPECT_STREQ("ff", StringUtils::ToHexadecimal(ff).c_str());
+}
diff --git a/xbmc/utils/test/TestSystemInfo.cpp b/xbmc/utils/test/TestSystemInfo.cpp
new file mode 100644
index 0000000..d14a474
--- /dev/null
+++ b/xbmc/utils/test/TestSystemInfo.cpp
@@ -0,0 +1,326 @@
+/*
+ * 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 "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "settings/Settings.h"
+#include "utils/CPUInfo.h"
+#include "utils/SystemInfo.h"
+#if defined(TARGET_WINDOWS)
+#include "platform/win32/CharsetConverter.h"
+#endif
+
+#include <gtest/gtest.h>
+
+#include "PlatformDefs.h"
+
+class TestSystemInfo : public testing::Test
+{
+protected:
+ TestSystemInfo() { CServiceBroker::RegisterCPUInfo(CCPUInfo::GetCPUInfo()); }
+ ~TestSystemInfo() { CServiceBroker::UnregisterCPUInfo(); }
+};
+
+TEST_F(TestSystemInfo, Print_System_Info)
+{
+ std::cout << "'GetKernelName(false)': \"" << g_sysinfo.GetKernelName(true) << "\"\n";
+ std::cout << "'GetKernelVersion()': \"" << g_sysinfo.GetKernelVersion() << "\"\n";
+ std::cout << "'GetKernelVersionFull()': \"" << g_sysinfo.GetKernelVersionFull() << "\"\n";
+ std::cout << "'GetOsPrettyNameWithVersion()': \"" << g_sysinfo.GetOsPrettyNameWithVersion() << "\"\n";
+ std::cout << "'GetOsName(false)': \"" << g_sysinfo.GetOsName(false) << "\"\n";
+ std::cout << "'GetOsVersion()': \"" << g_sysinfo.GetOsVersion() << "\"\n";
+ std::cout << "'GetKernelCpuFamily()': \"" << g_sysinfo.GetKernelCpuFamily() << "\"\n";
+ std::cout << "'GetKernelBitness()': \"" << g_sysinfo.GetKernelBitness() << "\"\n";
+ std::cout << "'GetBuildTargetPlatformName()': \"" << g_sysinfo.GetBuildTargetPlatformName() << "\"\n";
+ std::cout << "'GetBuildTargetPlatformVersionDecoded()': \"" << g_sysinfo.GetBuildTargetPlatformVersionDecoded() << "\"\n";
+ std::cout << "'GetBuildTargetPlatformVersion()': \"" << g_sysinfo.GetBuildTargetPlatformVersion() << "\"\n";
+ std::cout << "'GetBuildTargetCpuFamily()': \"" << g_sysinfo.GetBuildTargetCpuFamily() << "\"\n";
+ std::cout << "'GetXbmcBitness()': \"" << g_sysinfo.GetXbmcBitness() << "\"\n";
+ std::cout << "'GetUsedCompilerNameAndVer()': \"" << g_sysinfo.GetUsedCompilerNameAndVer() << "\"\n";
+ std::cout << "'GetManufacturerName()': \"" << g_sysinfo.GetManufacturerName() << "\"\n";
+ std::cout << "'GetModelName()': \"" << g_sysinfo.GetModelName() << "\"\n";
+ std::cout << "'GetUserAgent()': \"" << g_sysinfo.GetUserAgent() << "\"\n";
+}
+
+TEST_F(TestSystemInfo, GetKernelName)
+{
+ EXPECT_FALSE(g_sysinfo.GetKernelName(true).empty()) << "'GetKernelName(true)' must not return empty kernel name";
+ EXPECT_FALSE(g_sysinfo.GetKernelName(false).empty()) << "'GetKernelName(false)' must not return empty kernel name";
+ EXPECT_STRCASENE("Unknown kernel", g_sysinfo.GetKernelName(true).c_str()) << "'GetKernelName(true)' must not return 'Unknown kernel'";
+ EXPECT_STRCASENE("Unknown kernel", g_sysinfo.GetKernelName(false).c_str()) << "'GetKernelName(false)' must not return 'Unknown kernel'";
+#ifndef TARGET_DARWIN
+ EXPECT_EQ(g_sysinfo.GetBuildTargetPlatformName(), g_sysinfo.GetKernelName(true)) << "'GetKernelName(true)' must match GetBuildTargetPlatformName()";
+ EXPECT_EQ(g_sysinfo.GetBuildTargetPlatformName(), g_sysinfo.GetKernelName(false)) << "'GetKernelName(false)' must match GetBuildTargetPlatformName()";
+#endif // !TARGET_DARWIN
+#if defined(TARGET_WINDOWS)
+ EXPECT_NE(std::string::npos, g_sysinfo.GetKernelName(true).find("Windows")) << "'GetKernelName(true)' must contain 'Windows'";
+ EXPECT_NE(std::string::npos, g_sysinfo.GetKernelName(false).find("Windows")) << "'GetKernelName(false)' must contain 'Windows'";
+#elif defined(TARGET_FREEBSD)
+ EXPECT_STREQ("FreeBSD", g_sysinfo.GetKernelName(true).c_str()) << "'GetKernelName(true)' must return 'FreeBSD'";
+ EXPECT_STREQ("FreeBSD", g_sysinfo.GetKernelName(false).c_str()) << "'GetKernelName(false)' must return 'FreeBSD'";
+#elif defined(TARGET_DARWIN)
+ EXPECT_STREQ("Darwin", g_sysinfo.GetKernelName(true).c_str()) << "'GetKernelName(true)' must return 'Darwin'";
+ EXPECT_STREQ("Darwin", g_sysinfo.GetKernelName(false).c_str()) << "'GetKernelName(false)' must return 'Darwin'";
+#elif defined(TARGET_LINUX)
+ EXPECT_STREQ("Linux", g_sysinfo.GetKernelName(true).c_str()) << "'GetKernelName(true)' must return 'Linux'";
+ EXPECT_STREQ("Linux", g_sysinfo.GetKernelName(false).c_str()) << "'GetKernelName(false)' must return 'Linux'";
+#endif
+}
+
+TEST_F(TestSystemInfo, GetKernelVersionFull)
+{
+ EXPECT_FALSE(g_sysinfo.GetKernelVersionFull().empty()) << "'GetKernelVersionFull()' must not return empty string";
+ EXPECT_STRNE("0.0.0", g_sysinfo.GetKernelVersionFull().c_str()) << "'GetKernelVersionFull()' must not return '0.0.0'";
+ EXPECT_STRNE("0.0", g_sysinfo.GetKernelVersionFull().c_str()) << "'GetKernelVersionFull()' must not return '0.0'";
+ EXPECT_EQ(0U, g_sysinfo.GetKernelVersionFull().find_first_of("0123456789")) << "'GetKernelVersionFull()' must not return version not starting from digit";
+}
+
+TEST_F(TestSystemInfo, GetKernelVersion)
+{
+ EXPECT_FALSE(g_sysinfo.GetKernelVersion().empty()) << "'GetKernelVersion()' must not return empty string";
+ EXPECT_STRNE("0.0.0", g_sysinfo.GetKernelVersion().c_str()) << "'GetKernelVersion()' must not return '0.0.0'";
+ EXPECT_STRNE("0.0", g_sysinfo.GetKernelVersion().c_str()) << "'GetKernelVersion()' must not return '0.0'";
+ EXPECT_EQ(0U, g_sysinfo.GetKernelVersion().find_first_of("0123456789")) << "'GetKernelVersion()' must not return version not starting from digit";
+ EXPECT_EQ(std::string::npos, g_sysinfo.GetKernelVersion().find_first_not_of("0123456789.")) << "'GetKernelVersion()' must not return version with not only digits and dots";
+}
+
+TEST_F(TestSystemInfo, GetOsName)
+{
+ EXPECT_FALSE(g_sysinfo.GetOsName(true).empty()) << "'GetOsName(true)' must not return empty OS name";
+ EXPECT_FALSE(g_sysinfo.GetOsName(false).empty()) << "'GetOsName(false)' must not return empty OS name";
+ EXPECT_STRCASENE("Unknown OS", g_sysinfo.GetOsName(true).c_str()) << "'GetOsName(true)' must not return 'Unknown OS'";
+ EXPECT_STRCASENE("Unknown OS", g_sysinfo.GetOsName(false).c_str()) << "'GetOsName(false)' must not return 'Unknown OS'";
+#if defined(TARGET_WINDOWS)
+ EXPECT_NE(std::string::npos, g_sysinfo.GetOsName(true).find("Windows")) << "'GetOsName(true)' must contain 'Windows'";
+ EXPECT_NE(std::string::npos, g_sysinfo.GetOsName(false).find("Windows")) << "'GetOsName(false)' must contain 'Windows'";
+#elif defined(TARGET_FREEBSD)
+ EXPECT_STREQ("FreeBSD", g_sysinfo.GetOsName(true).c_str()) << "'GetOsName(true)' must return 'FreeBSD'";
+ EXPECT_STREQ("FreeBSD", g_sysinfo.GetOsName(false).c_str()) << "'GetOsName(false)' must return 'FreeBSD'";
+#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("macOS", g_sysinfo.GetOsName(true).c_str())
+ << "'GetOsName(true)' must return 'macOS'";
+ EXPECT_STREQ("macOS", g_sysinfo.GetOsName(false).c_str())
+ << "'GetOsName(false)' must return 'macOS'";
+#elif defined(TARGET_ANDROID)
+ EXPECT_STREQ("Android", g_sysinfo.GetOsName(true).c_str()) << "'GetOsName(true)' must return 'Android'";
+ EXPECT_STREQ("Android", g_sysinfo.GetOsName(false).c_str()) << "'GetOsName(false)' must return 'Android'";
+#endif
+#ifdef TARGET_DARWIN
+ EXPECT_EQ(g_sysinfo.GetBuildTargetPlatformName(), g_sysinfo.GetOsName(true)) << "'GetOsName(true)' must match GetBuildTargetPlatformName()";
+ EXPECT_EQ(g_sysinfo.GetBuildTargetPlatformName(), g_sysinfo.GetOsName(false)) << "'GetOsName(false)' must match GetBuildTargetPlatformName()";
+#endif // TARGET_DARWIN
+}
+
+TEST_F(TestSystemInfo, DISABLED_GetOsVersion)
+{
+ EXPECT_FALSE(g_sysinfo.GetOsVersion().empty()) << "'GetOsVersion()' must not return empty string";
+ EXPECT_STRNE("0.0.0", g_sysinfo.GetOsVersion().c_str()) << "'GetOsVersion()' must not return '0.0.0'";
+ EXPECT_STRNE("0.0", g_sysinfo.GetOsVersion().c_str()) << "'GetOsVersion()' must not return '0.0'";
+ EXPECT_EQ(0U, g_sysinfo.GetOsVersion().find_first_of("0123456789")) << "'GetOsVersion()' must not return version not starting from digit";
+ EXPECT_EQ(std::string::npos, g_sysinfo.GetOsVersion().find_first_not_of("0123456789.")) << "'GetOsVersion()' must not return version with not only digits and dots";
+}
+
+TEST_F(TestSystemInfo, GetOsPrettyNameWithVersion)
+{
+ EXPECT_FALSE(g_sysinfo.GetOsPrettyNameWithVersion().empty()) << "'GetOsPrettyNameWithVersion()' must not return empty string";
+ EXPECT_EQ(std::string::npos, g_sysinfo.GetOsPrettyNameWithVersion().find("Unknown")) << "'GetOsPrettyNameWithVersion()' must not contain 'Unknown'";
+ EXPECT_EQ(std::string::npos, g_sysinfo.GetOsPrettyNameWithVersion().find("unknown")) << "'GetOsPrettyNameWithVersion()' must not contain 'unknown'";
+#ifdef TARGET_WINDOWS
+ EXPECT_NE(std::string::npos, g_sysinfo.GetOsPrettyNameWithVersion().find("Windows")) << "'GetOsPrettyNameWithVersion()' must contain 'Windows'";
+#else // ! TARGET_WINDOWS
+ EXPECT_NE(std::string::npos, g_sysinfo.GetOsPrettyNameWithVersion().find(g_sysinfo.GetOsVersion())) << "'GetOsPrettyNameWithVersion()' must contain OS version";
+#endif // ! TARGET_WINDOWS
+}
+
+TEST_F(TestSystemInfo, GetManufacturerName)
+{
+ EXPECT_STRCASENE("unknown", g_sysinfo.GetManufacturerName().c_str()) << "'GetManufacturerName()' must return empty string instead of 'Unknown'";
+}
+
+TEST_F(TestSystemInfo, GetModelName)
+{
+ EXPECT_STRCASENE("unknown", g_sysinfo.GetModelName().c_str()) << "'GetModelName()' must return empty string instead of 'Unknown'";
+}
+
+#ifndef TARGET_WINDOWS
+TEST_F(TestSystemInfo, IsAeroDisabled)
+{
+ EXPECT_FALSE(g_sysinfo.IsAeroDisabled()) << "'IsAeroDisabled()' must return 'false'";
+}
+#endif // ! TARGET_WINDOWS
+
+TEST_F(TestSystemInfo, IsWindowsVersion)
+{
+ EXPECT_FALSE(g_sysinfo.IsWindowsVersion(CSysInfo::WindowsVersionUnknown)) << "'IsWindowsVersion()' must return 'false' for 'WindowsVersionUnknown'";
+#ifndef TARGET_WINDOWS
+ EXPECT_FALSE(g_sysinfo.IsWindowsVersion(CSysInfo::WindowsVersionWin7)) << "'IsWindowsVersion()' must return 'false'";
+#endif // ! TARGET_WINDOWS
+}
+
+TEST_F(TestSystemInfo, IsWindowsVersionAtLeast)
+{
+ EXPECT_FALSE(g_sysinfo.IsWindowsVersionAtLeast(CSysInfo::WindowsVersionUnknown)) << "'IsWindowsVersionAtLeast()' must return 'false' for 'WindowsVersionUnknown'";
+ EXPECT_FALSE(g_sysinfo.IsWindowsVersionAtLeast(CSysInfo::WindowsVersionFuture)) << "'IsWindowsVersionAtLeast()' must return 'false' for 'WindowsVersionFuture'";
+#ifndef TARGET_WINDOWS
+ EXPECT_FALSE(g_sysinfo.IsWindowsVersion(CSysInfo::WindowsVersionWin7)) << "'IsWindowsVersionAtLeast()' must return 'false'";
+#endif // ! TARGET_WINDOWS
+}
+
+TEST_F(TestSystemInfo, GetWindowsVersion)
+{
+#ifdef TARGET_WINDOWS
+ EXPECT_NE(CSysInfo::WindowsVersionUnknown, g_sysinfo.GetWindowsVersion()) << "'GetWindowsVersion()' must not return 'WindowsVersionUnknown'";
+ EXPECT_NE(CSysInfo::WindowsVersionFuture, g_sysinfo.GetWindowsVersion()) << "'GetWindowsVersion()' must not return 'WindowsVersionFuture'";
+#else // ! TARGET_WINDOWS
+ EXPECT_EQ(CSysInfo::WindowsVersionUnknown, g_sysinfo.GetWindowsVersion()) << "'GetWindowsVersion()' must return 'WindowsVersionUnknown'";
+#endif // ! TARGET_WINDOWS
+}
+
+TEST_F(TestSystemInfo, GetKernelBitness)
+{
+ EXPECT_TRUE(g_sysinfo.GetKernelBitness() == 32 || g_sysinfo.GetKernelBitness() == 64) << "'GetKernelBitness()' must return '32' or '64', but not '" << g_sysinfo.GetKernelBitness() << "'";
+ EXPECT_LE(g_sysinfo.GetXbmcBitness(), g_sysinfo.GetKernelBitness()) << "'GetKernelBitness()' must be greater or equal to 'GetXbmcBitness()'";
+}
+
+TEST_F(TestSystemInfo, GetKernelCpuFamily)
+{
+ EXPECT_STRNE("unknown CPU family", g_sysinfo.GetKernelCpuFamily().c_str()) << "'GetKernelCpuFamily()' must not return 'unknown CPU family'";
+#if defined(__thumb__) || defined(_M_ARMT) || defined(__arm__) || defined(_M_ARM) || defined (__aarch64__)
+ EXPECT_STREQ("ARM", g_sysinfo.GetKernelCpuFamily().c_str()) << "'GetKernelCpuFamily()' must return 'ARM'";
+#else // ! ARM
+ EXPECT_EQ(g_sysinfo.GetBuildTargetCpuFamily(), g_sysinfo.GetKernelCpuFamily()) << "'GetKernelCpuFamily()' must match 'GetBuildTargetCpuFamily()'";
+#endif // ! ARM
+}
+
+TEST_F(TestSystemInfo, GetXbmcBitness)
+{
+ EXPECT_TRUE(g_sysinfo.GetXbmcBitness() == 32 || g_sysinfo.GetXbmcBitness() == 64) << "'GetXbmcBitness()' must return '32' or '64', but not '" << g_sysinfo.GetXbmcBitness() << "'";
+ EXPECT_GE(g_sysinfo.GetKernelBitness(), g_sysinfo.GetXbmcBitness()) << "'GetXbmcBitness()' must be not greater than 'GetKernelBitness()'";
+}
+
+TEST_F(TestSystemInfo, GetUserAgent)
+{
+ EXPECT_STREQ(g_sysinfo.GetAppName().c_str(), g_sysinfo.GetUserAgent().substr(0, g_sysinfo.GetAppName().size()).c_str()) << "'GetUserAgent()' string must start with app name'";
+ EXPECT_NE(std::string::npos, g_sysinfo.GetUserAgent().find('(')) << "'GetUserAgent()' must contain brackets around second parameter";
+ EXPECT_NE(std::string::npos, g_sysinfo.GetUserAgent().find(')')) << "'GetUserAgent()' must contain brackets around second parameter";
+ EXPECT_EQ(g_sysinfo.GetUserAgent().find(' '), g_sysinfo.GetUserAgent().find(" (")) << "Second parameter in 'GetUserAgent()' string must be in brackets";
+ EXPECT_EQ(g_sysinfo.GetUserAgent().find(" (") + 1, g_sysinfo.GetUserAgent().find('(')) << "'GetUserAgent()' string must not contain any opening brackets before second parameter";
+ EXPECT_GT(g_sysinfo.GetUserAgent().find(')'), g_sysinfo.GetUserAgent().find('(')) << "'GetUserAgent()' string must not contain any closing brackets before second parameter";
+ EXPECT_EQ(g_sysinfo.GetUserAgent().find(") "), g_sysinfo.GetUserAgent().find(')')) << "'GetUserAgent()' string must not contain any closing brackets before end of second parameter";
+#if defined(TARGET_WINDOWS)
+ EXPECT_EQ(g_sysinfo.GetUserAgent().find('('), g_sysinfo.GetUserAgent().find("(Windows")) << "Second parameter in 'GetUserAgent()' string must start from `Windows`";
+ EXPECT_NE(std::string::npos, g_sysinfo.GetUserAgent().find("Windows")) << "'GetUserAgent()' must contain 'Windows'";
+#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)
+ EXPECT_EQ(g_sysinfo.GetUserAgent().find('('), g_sysinfo.GetUserAgent().find("(Linux; Android ")) << "Second parameter in 'GetUserAgent()' string must start from 'Linux; Android '";
+#elif defined(TARGET_POSIX)
+ EXPECT_EQ(g_sysinfo.GetUserAgent().find('('), g_sysinfo.GetUserAgent().find("(X11; ")) << "Second parameter in 'GetUserAgent()' string must start from 'X11; '";
+#if defined(TARGET_FREEBSD)
+ EXPECT_EQ(g_sysinfo.GetUserAgent().find('('), g_sysinfo.GetUserAgent().find("(X11; FreeBSD ")) << "Second parameter in 'GetUserAgent()' string must start from 'X11; FreeBSD '";
+#elif defined(TARGET_LINUX)
+ EXPECT_EQ(g_sysinfo.GetUserAgent().find('('), g_sysinfo.GetUserAgent().find("(X11; Linux ")) << "Second parameter in 'GetUserAgent()' string must start from 'X11; Linux '";
+#endif // defined(TARGET_LINUX)
+#endif // defined(TARGET_POSIX)
+
+ EXPECT_NE(std::string::npos, g_sysinfo.GetUserAgent().find(" App_Bitness/")) << "'GetUserAgent()' must contain ' App_Bitness/'";
+ EXPECT_NE(std::string::npos, g_sysinfo.GetUserAgent().find(" Version/")) << "'GetUserAgent()' must contain ' Version/'";
+}
+
+TEST_F(TestSystemInfo, GetBuildTargetPlatformName)
+{
+ EXPECT_EQ(std::string::npos, g_sysinfo.GetBuildTargetPlatformName().find("Unknown")) << "'GetBuildTargetPlatformName()' must not contain 'Unknown', actual value: '" << g_sysinfo.GetBuildTargetPlatformName() << "'";
+ EXPECT_EQ(std::string::npos, g_sysinfo.GetBuildTargetPlatformName().find("unknown")) << "'GetBuildTargetPlatformName()' must not contain 'unknown', actual value: '" << g_sysinfo.GetBuildTargetPlatformName() << "'";
+}
+
+TEST_F(TestSystemInfo, GetBuildTargetPlatformVersion)
+{
+ EXPECT_EQ(std::string::npos, g_sysinfo.GetBuildTargetPlatformVersion().find("Unknown")) << "'GetBuildTargetPlatformVersion()' must not contain 'Unknown', actual value: '" << g_sysinfo.GetBuildTargetPlatformVersion() << "'";
+ EXPECT_EQ(std::string::npos, g_sysinfo.GetBuildTargetPlatformVersion().find("unknown")) << "'GetBuildTargetPlatformVersion()' must not contain 'unknown', actual value: '" << g_sysinfo.GetBuildTargetPlatformVersion() << "'";
+}
+
+TEST_F(TestSystemInfo, GetBuildTargetPlatformVersionDecoded)
+{
+ EXPECT_EQ(std::string::npos, g_sysinfo.GetBuildTargetPlatformVersionDecoded().find("Unknown")) << "'GetBuildTargetPlatformVersionDecoded()' must not contain 'Unknown', actual value: '" << g_sysinfo.GetBuildTargetPlatformVersion() << "'";
+ EXPECT_EQ(std::string::npos, g_sysinfo.GetBuildTargetPlatformVersionDecoded().find("unknown")) << "'GetBuildTargetPlatformVersionDecoded()' must not contain 'unknown', actual value: '" << g_sysinfo.GetBuildTargetPlatformVersion() << "'";
+#ifdef TARGET_ANDROID
+ EXPECT_STREQ("API level ", g_sysinfo.GetBuildTargetPlatformVersionDecoded().substr(0, 10).c_str()) << "'GetBuildTargetPlatformVersionDecoded()' must start from 'API level '";
+#else
+ EXPECT_STREQ("version ", g_sysinfo.GetBuildTargetPlatformVersionDecoded().substr(0, 8).c_str()) << "'GetBuildTargetPlatformVersionDecoded()' must start from 'version'";
+#endif
+}
+
+TEST_F(TestSystemInfo, GetBuildTargetCpuFamily)
+{
+ EXPECT_STRNE("unknown CPU family", g_sysinfo.GetBuildTargetCpuFamily().c_str()) << "'GetBuildTargetCpuFamily()' must not return 'unknown CPU family'";
+#if defined(__thumb__) || defined(_M_ARMT) || defined(__arm__) || defined(_M_ARM) || defined (__aarch64__)
+ EXPECT_STREQ("ARM", g_sysinfo.GetBuildTargetCpuFamily().substr(0, 3).c_str()) << "'GetKernelCpuFamily()' string must start from 'ARM'";
+#else // ! ARM
+ EXPECT_EQ(g_sysinfo.GetKernelCpuFamily(), g_sysinfo.GetBuildTargetCpuFamily()) << "'GetBuildTargetCpuFamily()' must match 'GetKernelCpuFamily()'";
+#endif // ! ARM
+}
+
+TEST_F(TestSystemInfo, GetUsedCompilerNameAndVer)
+{
+ EXPECT_STRNE("unknown compiler", g_sysinfo.GetUsedCompilerNameAndVer().c_str()) << "'GetUsedCompilerNameAndVer()' must not return 'unknown compiler'";
+}
+
+TEST_F(TestSystemInfo, GetDiskSpace)
+{
+ int iTotal, iTotalFree, iTotalUsed, iPercentFree, iPercentUsed;
+
+ iTotal = iTotalFree = iTotalUsed = iPercentFree = iPercentUsed = 0;
+ EXPECT_TRUE(g_sysinfo.GetDiskSpace("*", iTotal, iTotalFree, iTotalUsed, iPercentFree, iPercentUsed)) << "'GetDiskSpace()' return 'false' for disk '*'";
+ EXPECT_NE(0, iTotal) << "'GetDiskSpace()' return zero total space for disk '*'";
+ EXPECT_EQ(iTotal, iTotalFree + iTotalUsed) << "'GetDiskSpace()' return 'TotalFree + TotalUsed' not equal to 'Total' for disk '*'";
+ EXPECT_EQ(100, iPercentFree + iPercentUsed) << "'GetDiskSpace()' return 'PercentFree + PercentUsed' not equal to '100' for disk '*'";
+
+ iTotal = iTotalFree = iTotalUsed = iPercentFree = iPercentUsed = 0;
+ EXPECT_TRUE(g_sysinfo.GetDiskSpace("", iTotal, iTotalFree, iTotalUsed, iPercentFree, iPercentUsed)) << "'GetDiskSpace()' return 'false' for disk ''";
+ EXPECT_NE(0, iTotal) << "'GetDiskSpace()' return zero total space for disk ''";
+ EXPECT_EQ(iTotal, iTotalFree + iTotalUsed) << "'GetDiskSpace()' return 'TotalFree + TotalUsed' not equal to 'Total' for disk ''";
+ EXPECT_EQ(100, iPercentFree + iPercentUsed) << "'GetDiskSpace()' return 'PercentFree + PercentUsed' not equal to '100' for disk ''";
+
+#ifdef TARGET_WINDOWS
+ using KODI::PLATFORM::WINDOWS::FromW;
+ wchar_t sysDrive[300];
+ DWORD res = GetEnvironmentVariableW(L"SystemDrive", sysDrive, sizeof(sysDrive) / sizeof(wchar_t));
+ std::string sysDriveLtr;
+ if (res != 0 && res <= sizeof(sysDrive) / sizeof(wchar_t))
+ sysDriveLtr.assign(FromW(sysDrive), 0, 1);
+ else
+ sysDriveLtr = "C"; // fallback
+
+ iTotal = iTotalFree = iTotalUsed = iPercentFree = iPercentUsed = 0;
+ EXPECT_TRUE(g_sysinfo.GetDiskSpace(sysDriveLtr, iTotal, iTotalFree, iTotalUsed, iPercentFree, iPercentUsed)) << "'GetDiskSpace()' return 'false' for disk '" << sysDriveLtr << ":'";
+ EXPECT_NE(0, iTotal) << "'GetDiskSpace()' return zero total space for disk '" << sysDriveLtr << ":'";
+ EXPECT_EQ(iTotal, iTotalFree + iTotalUsed) << "'GetDiskSpace()' return 'TotalFree + TotalUsed' not equal to 'Total' for disk '" << sysDriveLtr << ":'";
+ EXPECT_EQ(100, iPercentFree + iPercentUsed) << "'GetDiskSpace()' return 'PercentFree + PercentUsed' not equal to '100' for disk '" << sysDriveLtr << ":'";
+#elif defined(TARGET_POSIX)
+ iTotal = iTotalFree = iTotalUsed = iPercentFree = iPercentUsed = 0;
+ EXPECT_TRUE(g_sysinfo.GetDiskSpace("/", iTotal, iTotalFree, iTotalUsed, iPercentFree, iPercentUsed)) << "'GetDiskSpace()' return 'false' for directory '/'";
+ EXPECT_NE(0, iTotal) << "'GetDiskSpace()' return zero total space for directory '/'";
+ EXPECT_EQ(iTotal, iTotalFree + iTotalUsed) << "'GetDiskSpace()' return 'TotalFree + TotalUsed' not equal to 'Total' for directory '/'";
+ EXPECT_EQ(100, iPercentFree + iPercentUsed) << "'GetDiskSpace()' return 'PercentFree + PercentUsed' not equal to '100' for directory '/'";
+#endif
+}
diff --git a/xbmc/utils/test/TestURIUtils.cpp b/xbmc/utils/test/TestURIUtils.cpp
new file mode 100644
index 0000000..7122fe9
--- /dev/null
+++ b/xbmc/utils/test/TestURIUtils.cpp
@@ -0,0 +1,585 @@
+/*
+ * 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 "ServiceBroker.h"
+#include "URL.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+
+#include <utility>
+
+#include <gtest/gtest.h>
+
+using namespace XFILE;
+
+class TestURIUtils : public testing::Test
+{
+protected:
+ TestURIUtils() = default;
+ ~TestURIUtils() override
+ {
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_pathSubstitutions.clear();
+ }
+};
+
+TEST_F(TestURIUtils, PathHasParent)
+{
+ EXPECT_TRUE(URIUtils::PathHasParent("/path/to/movie.avi", "/path/to/"));
+ EXPECT_FALSE(URIUtils::PathHasParent("/path/to/movie.avi", "/path/2/"));
+}
+
+TEST_F(TestURIUtils, GetDirectory)
+{
+ EXPECT_STREQ("/path/to/", URIUtils::GetDirectory("/path/to/movie.avi").c_str());
+ EXPECT_STREQ("/path/to/", URIUtils::GetDirectory("/path/to/").c_str());
+ EXPECT_STREQ("/path/to/|option=foo", URIUtils::GetDirectory("/path/to/movie.avi|option=foo").c_str());
+ EXPECT_STREQ("/path/to/|option=foo", URIUtils::GetDirectory("/path/to/|option=foo").c_str());
+ EXPECT_STREQ("", URIUtils::GetDirectory("movie.avi").c_str());
+ EXPECT_STREQ("", URIUtils::GetDirectory("movie.avi|option=foo").c_str());
+ EXPECT_STREQ("", URIUtils::GetDirectory("").c_str());
+
+ // Make sure it works when assigning to the same str as the reference parameter
+ std::string var = "/path/to/movie.avi|option=foo";
+ var = URIUtils::GetDirectory(var);
+ EXPECT_STREQ("/path/to/|option=foo", var.c_str());
+}
+
+TEST_F(TestURIUtils, GetExtension)
+{
+ EXPECT_STREQ(".avi",
+ URIUtils::GetExtension("/path/to/movie.avi").c_str());
+}
+
+TEST_F(TestURIUtils, HasExtension)
+{
+ EXPECT_TRUE (URIUtils::HasExtension("/path/to/movie.AvI"));
+ EXPECT_FALSE(URIUtils::HasExtension("/path/to/movie"));
+ EXPECT_FALSE(URIUtils::HasExtension("/path/.to/movie"));
+ EXPECT_FALSE(URIUtils::HasExtension(""));
+
+ EXPECT_TRUE (URIUtils::HasExtension("/path/to/movie.AvI", ".avi"));
+ EXPECT_FALSE(URIUtils::HasExtension("/path/to/movie.AvI", ".mkv"));
+ EXPECT_FALSE(URIUtils::HasExtension("/path/.avi/movie", ".avi"));
+ EXPECT_FALSE(URIUtils::HasExtension("", ".avi"));
+
+ EXPECT_TRUE (URIUtils::HasExtension("/path/movie.AvI", ".avi|.mkv|.mp4"));
+ EXPECT_TRUE (URIUtils::HasExtension("/path/movie.AvI", ".mkv|.avi|.mp4"));
+ EXPECT_FALSE(URIUtils::HasExtension("/path/movie.AvI", ".mpg|.mkv|.mp4"));
+ EXPECT_FALSE(URIUtils::HasExtension("/path.mkv/movie.AvI", ".mpg|.mkv|.mp4"));
+ EXPECT_FALSE(URIUtils::HasExtension("", ".avi|.mkv|.mp4"));
+}
+
+TEST_F(TestURIUtils, GetFileName)
+{
+ EXPECT_STREQ("movie.avi",
+ URIUtils::GetFileName("/path/to/movie.avi").c_str());
+}
+
+TEST_F(TestURIUtils, RemoveExtension)
+{
+ std::string ref, var;
+
+ /* NOTE: CSettings need to be set to find other extensions. */
+ ref = "/path/to/file";
+ var = "/path/to/file.xml";
+ URIUtils::RemoveExtension(var);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST_F(TestURIUtils, ReplaceExtension)
+{
+ std::string ref, var;
+
+ ref = "/path/to/file.xsd";
+ var = URIUtils::ReplaceExtension("/path/to/file.xml", ".xsd");
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST_F(TestURIUtils, Split)
+{
+ std::string refpath, reffile, varpath, varfile;
+
+ refpath = "/path/to/";
+ reffile = "movie.avi";
+ URIUtils::Split("/path/to/movie.avi", varpath, varfile);
+ EXPECT_STREQ(refpath.c_str(), varpath.c_str());
+ EXPECT_STREQ(reffile.c_str(), varfile.c_str());
+
+ std::string varpathOptional, varfileOptional;
+
+ refpath = "/path/to/";
+ reffile = "movie?movie.avi";
+ URIUtils::Split("/path/to/movie?movie.avi", varpathOptional, varfileOptional);
+ EXPECT_STREQ(refpath.c_str(), varpathOptional.c_str());
+ EXPECT_STREQ(reffile.c_str(), varfileOptional.c_str());
+
+ refpath = "file:///path/to/";
+ reffile = "movie.avi";
+ URIUtils::Split("file:///path/to/movie.avi?showinfo=true", varpathOptional, varfileOptional);
+ EXPECT_STREQ(refpath.c_str(), varpathOptional.c_str());
+ EXPECT_STREQ(reffile.c_str(), varfileOptional.c_str());
+}
+
+TEST_F(TestURIUtils, SplitPath)
+{
+ std::vector<std::string> strarray;
+
+ strarray = URIUtils::SplitPath("http://www.test.com/path/to/movie.avi");
+
+ EXPECT_STREQ("http://www.test.com/", strarray.at(0).c_str());
+ EXPECT_STREQ("path", strarray.at(1).c_str());
+ EXPECT_STREQ("to", strarray.at(2).c_str());
+ EXPECT_STREQ("movie.avi", strarray.at(3).c_str());
+}
+
+TEST_F(TestURIUtils, SplitPathLocal)
+{
+#ifndef TARGET_LINUX
+ const char *path = "C:\\path\\to\\movie.avi";
+#else
+ const char *path = "/path/to/movie.avi";
+#endif
+ std::vector<std::string> strarray;
+
+ strarray = URIUtils::SplitPath(path);
+
+#ifndef TARGET_LINUX
+ EXPECT_STREQ("C:", strarray.at(0).c_str());
+#else
+ EXPECT_STREQ("", strarray.at(0).c_str());
+#endif
+ EXPECT_STREQ("path", strarray.at(1).c_str());
+ EXPECT_STREQ("to", strarray.at(2).c_str());
+ EXPECT_STREQ("movie.avi", strarray.at(3).c_str());
+}
+
+TEST_F(TestURIUtils, GetCommonPath)
+{
+ std::string ref, var;
+
+ ref = "/path/";
+ var = "/path/2/movie.avi";
+ URIUtils::GetCommonPath(var, "/path/to/movie.avi");
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST_F(TestURIUtils, GetParentPath)
+{
+ std::string ref, var;
+
+ ref = "/path/to/";
+ var = URIUtils::GetParentPath("/path/to/movie.avi");
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ var.clear();
+ EXPECT_TRUE(URIUtils::GetParentPath("/path/to/movie.avi", var));
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST_F(TestURIUtils, SubstitutePath)
+{
+ std::string from, to, ref, var;
+
+ from = "C:\\My Videos";
+ to = "https://myserver/some%20other%20path";
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_pathSubstitutions.push_back(std::make_pair(from, to));
+
+ from = "/this/path1";
+ to = "/some/other/path2";
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_pathSubstitutions.push_back(std::make_pair(from, to));
+
+ from = "davs://otherserver/my%20music%20path";
+ to = "D:\\Local Music\\MP3 Collection";
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_pathSubstitutions.push_back(std::make_pair(from, to));
+
+ ref = "https://myserver/some%20other%20path/sub%20dir/movie%20name.avi";
+ var = URIUtils::SubstitutePath("C:\\My Videos\\sub dir\\movie name.avi");
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ ref = "C:\\My Videos\\sub dir\\movie name.avi";
+ var = URIUtils::SubstitutePath("https://myserver/some%20other%20path/sub%20dir/movie%20name.avi", true);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ ref = "D:\\Local Music\\MP3 Collection\\Phil Collins\\Some CD\\01 - Two Hearts.mp3";
+ var = URIUtils::SubstitutePath("davs://otherserver/my%20music%20path/Phil%20Collins/Some%20CD/01%20-%20Two%20Hearts.mp3");
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ ref = "davs://otherserver/my%20music%20path/Phil%20Collins/Some%20CD/01%20-%20Two%20Hearts.mp3";
+ var = URIUtils::SubstitutePath("D:\\Local Music\\MP3 Collection\\Phil Collins\\Some CD\\01 - Two Hearts.mp3", true);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ ref = "/some/other/path2/to/movie.avi";
+ var = URIUtils::SubstitutePath("/this/path1/to/movie.avi");
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ ref = "/this/path1/to/movie.avi";
+ var = URIUtils::SubstitutePath("/some/other/path2/to/movie.avi", true);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ ref = "/no/translation path/";
+ var = URIUtils::SubstitutePath(ref);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ ref = "/no/translation path/";
+ var = URIUtils::SubstitutePath(ref, true);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ ref = "c:\\no\\translation path";
+ var = URIUtils::SubstitutePath(ref);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ ref = "c:\\no\\translation path";
+ var = URIUtils::SubstitutePath(ref, true);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST_F(TestURIUtils, IsAddonsPath)
+{
+ EXPECT_TRUE(URIUtils::IsAddonsPath("addons://path/to/addons"));
+}
+
+TEST_F(TestURIUtils, IsSourcesPath)
+{
+ EXPECT_TRUE(URIUtils::IsSourcesPath("sources://path/to/sources"));
+}
+
+TEST_F(TestURIUtils, IsCDDA)
+{
+ EXPECT_TRUE(URIUtils::IsCDDA("cdda://path/to/cdda"));
+}
+
+TEST_F(TestURIUtils, IsDOSPath)
+{
+ EXPECT_TRUE(URIUtils::IsDOSPath("C://path/to/dosfile"));
+}
+
+TEST_F(TestURIUtils, IsDVD)
+{
+ EXPECT_TRUE(URIUtils::IsDVD("dvd://path/in/video_ts.ifo"));
+#if defined(TARGET_WINDOWS)
+ EXPECT_TRUE(URIUtils::IsDVD("dvd://path/in/file"));
+#else
+ EXPECT_TRUE(URIUtils::IsDVD("iso9660://path/in/video_ts.ifo"));
+ EXPECT_TRUE(URIUtils::IsDVD("udf://path/in/video_ts.ifo"));
+ EXPECT_TRUE(URIUtils::IsDVD("dvd://1"));
+#endif
+}
+
+TEST_F(TestURIUtils, IsFTP)
+{
+ EXPECT_TRUE(URIUtils::IsFTP("ftp://path/in/ftp"));
+}
+
+TEST_F(TestURIUtils, IsHD)
+{
+ EXPECT_TRUE(URIUtils::IsHD("/path/to/file"));
+ EXPECT_TRUE(URIUtils::IsHD("file:///path/to/file"));
+ EXPECT_TRUE(URIUtils::IsHD("special://path/to/file"));
+ EXPECT_TRUE(URIUtils::IsHD("stack://path/to/file"));
+ EXPECT_TRUE(URIUtils::IsHD("zip://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsInArchive)
+{
+ EXPECT_TRUE(URIUtils::IsInArchive("zip://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsInRAR)
+{
+ EXPECT_TRUE(URIUtils::IsInRAR("rar://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsInternetStream)
+{
+ CURL url1("http://path/to/file");
+ CURL url2("https://path/to/file");
+ EXPECT_TRUE(URIUtils::IsInternetStream(url1));
+ EXPECT_TRUE(URIUtils::IsInternetStream(url2));
+}
+
+TEST_F(TestURIUtils, IsInZIP)
+{
+ EXPECT_TRUE(URIUtils::IsInZIP("zip://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsISO9660)
+{
+ EXPECT_TRUE(URIUtils::IsISO9660("iso9660://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsLiveTV)
+{
+ EXPECT_TRUE(URIUtils::IsLiveTV("whatever://path/to/file.pvr"));
+}
+
+TEST_F(TestURIUtils, IsMultiPath)
+{
+ EXPECT_TRUE(URIUtils::IsMultiPath("multipath://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsMusicDb)
+{
+ EXPECT_TRUE(URIUtils::IsMusicDb("musicdb://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsNfs)
+{
+ EXPECT_TRUE(URIUtils::IsNfs("nfs://path/to/file"));
+ EXPECT_TRUE(URIUtils::IsNfs("stack://nfs://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsOnDVD)
+{
+ EXPECT_TRUE(URIUtils::IsOnDVD("dvd://path/to/file"));
+ EXPECT_TRUE(URIUtils::IsOnDVD("udf://path/to/file"));
+ EXPECT_TRUE(URIUtils::IsOnDVD("iso9660://path/to/file"));
+ EXPECT_TRUE(URIUtils::IsOnDVD("cdda://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsOnLAN)
+{
+ std::vector<std::string> multiVec;
+ multiVec.emplace_back("smb://path/to/file");
+ EXPECT_TRUE(URIUtils::IsOnLAN(CMultiPathDirectory::ConstructMultiPath(multiVec)));
+ EXPECT_TRUE(URIUtils::IsOnLAN("stack://smb://path/to/file"));
+ EXPECT_TRUE(URIUtils::IsOnLAN("smb://path/to/file"));
+ EXPECT_FALSE(URIUtils::IsOnLAN("plugin://path/to/file"));
+ EXPECT_TRUE(URIUtils::IsOnLAN("upnp://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsPlugin)
+{
+ EXPECT_TRUE(URIUtils::IsPlugin("plugin://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsScript)
+{
+ EXPECT_TRUE(URIUtils::IsScript("script://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsRAR)
+{
+ EXPECT_TRUE(URIUtils::IsRAR("/path/to/rarfile.rar"));
+ EXPECT_TRUE(URIUtils::IsRAR("/path/to/rarfile.cbr"));
+ EXPECT_FALSE(URIUtils::IsRAR("/path/to/file"));
+ EXPECT_FALSE(URIUtils::IsRAR("rar://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsRemote)
+{
+ EXPECT_TRUE(URIUtils::IsRemote("http://path/to/file"));
+ EXPECT_TRUE(URIUtils::IsRemote("https://path/to/file"));
+ EXPECT_FALSE(URIUtils::IsRemote("addons://user/"));
+ EXPECT_FALSE(URIUtils::IsRemote("sources://video/"));
+ EXPECT_FALSE(URIUtils::IsRemote("videodb://movies/titles"));
+ EXPECT_FALSE(URIUtils::IsRemote("musicdb://genres/"));
+ EXPECT_FALSE(URIUtils::IsRemote("library://video/"));
+ EXPECT_FALSE(URIUtils::IsRemote("androidapp://app"));
+ EXPECT_FALSE(URIUtils::IsRemote("plugin://plugin.video.id"));
+}
+
+TEST_F(TestURIUtils, IsSmb)
+{
+ EXPECT_TRUE(URIUtils::IsSmb("smb://path/to/file"));
+ EXPECT_TRUE(URIUtils::IsSmb("stack://smb://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsSpecial)
+{
+ EXPECT_TRUE(URIUtils::IsSpecial("special://path/to/file"));
+ EXPECT_TRUE(URIUtils::IsSpecial("stack://special://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsStack)
+{
+ EXPECT_TRUE(URIUtils::IsStack("stack://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsUPnP)
+{
+ EXPECT_TRUE(URIUtils::IsUPnP("upnp://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsURL)
+{
+ EXPECT_TRUE(URIUtils::IsURL("someprotocol://path/to/file"));
+ EXPECT_FALSE(URIUtils::IsURL("/path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsVideoDb)
+{
+ EXPECT_TRUE(URIUtils::IsVideoDb("videodb://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsZIP)
+{
+ EXPECT_TRUE(URIUtils::IsZIP("/path/to/zipfile.zip"));
+ EXPECT_TRUE(URIUtils::IsZIP("/path/to/zipfile.cbz"));
+ EXPECT_FALSE(URIUtils::IsZIP("/path/to/file"));
+ EXPECT_FALSE(URIUtils::IsZIP("zip://path/to/file"));
+}
+
+TEST_F(TestURIUtils, IsBluray)
+{
+ EXPECT_TRUE(URIUtils::IsBluray("bluray://path/to/file"));
+}
+
+TEST_F(TestURIUtils, AddSlashAtEnd)
+{
+ std::string ref, var;
+
+ ref = "bluray://path/to/file/";
+ var = "bluray://path/to/file/";
+ URIUtils::AddSlashAtEnd(var);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST_F(TestURIUtils, HasSlashAtEnd)
+{
+ EXPECT_TRUE(URIUtils::HasSlashAtEnd("bluray://path/to/file/"));
+ EXPECT_FALSE(URIUtils::HasSlashAtEnd("bluray://path/to/file"));
+}
+
+TEST_F(TestURIUtils, RemoveSlashAtEnd)
+{
+ std::string ref, var;
+
+ ref = "bluray://path/to/file";
+ var = "bluray://path/to/file/";
+ URIUtils::RemoveSlashAtEnd(var);
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST_F(TestURIUtils, CreateArchivePath)
+{
+ std::string ref, var;
+
+ ref = "zip://%2fpath%2fto%2f/file";
+ var = URIUtils::CreateArchivePath("zip", CURL("/path/to/"), "file").Get();
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST_F(TestURIUtils, AddFileToFolder)
+{
+ std::string ref = "/path/to/file";
+ std::string var = URIUtils::AddFileToFolder("/path/to", "file");
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+
+ ref = "/path/to/file/and/more";
+ var = URIUtils::AddFileToFolder("/path", "to", "file", "and", "more");
+ EXPECT_STREQ(ref.c_str(), var.c_str());
+}
+
+TEST_F(TestURIUtils, HasParentInHostname)
+{
+ EXPECT_TRUE(URIUtils::HasParentInHostname(CURL("zip://")));
+ EXPECT_TRUE(URIUtils::HasParentInHostname(CURL("bluray://")));
+}
+
+TEST_F(TestURIUtils, HasEncodedHostname)
+{
+ EXPECT_TRUE(URIUtils::HasEncodedHostname(CURL("zip://")));
+ EXPECT_TRUE(URIUtils::HasEncodedHostname(CURL("bluray://")));
+ EXPECT_TRUE(URIUtils::HasEncodedHostname(CURL("musicsearch://")));
+}
+
+TEST_F(TestURIUtils, HasEncodedFilename)
+{
+ EXPECT_TRUE(URIUtils::HasEncodedFilename(CURL("shout://")));
+ EXPECT_TRUE(URIUtils::HasEncodedFilename(CURL("dav://")));
+ EXPECT_TRUE(URIUtils::HasEncodedFilename(CURL("rss://")));
+ EXPECT_TRUE(URIUtils::HasEncodedFilename(CURL("davs://")));
+}
+
+TEST_F(TestURIUtils, GetRealPath)
+{
+ std::string ref;
+
+ ref = "/path/to/file/";
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath(ref).c_str());
+
+ ref = "path/to/file";
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("../path/to/file").c_str());
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("./path/to/file").c_str());
+
+ ref = "/path/to/file";
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath(ref).c_str());
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("/path/to/./file").c_str());
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("/./path/to/./file").c_str());
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("/path/to/some/../file").c_str());
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("/../path/to/some/../file").c_str());
+
+ ref = "/path/to";
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("/path/to/some/../file/..").c_str());
+
+#ifdef TARGET_WINDOWS
+ ref = "\\\\path\\to\\file\\";
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath(ref).c_str());
+
+ ref = "path\\to\\file";
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("..\\path\\to\\file").c_str());
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath(".\\path\\to\\file").c_str());
+
+ ref = "\\\\path\\to\\file";
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath(ref).c_str());
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("\\\\path\\to\\.\\file").c_str());
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("\\\\.\\path/to\\.\\file").c_str());
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("\\\\path\\to\\some\\..\\file").c_str());
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("\\\\..\\path\\to\\some\\..\\file").c_str());
+
+ ref = "\\\\path\\to";
+ EXPECT_STREQ(ref.c_str(), URIUtils::GetRealPath("\\\\path\\to\\some\\..\\file\\..").c_str());
+#endif
+
+ // test rar/zip paths
+ ref = "zip://%2fpath%2fto%2fzip/subpath/to/file";
+ EXPECT_STRCASEEQ(ref.c_str(), URIUtils::GetRealPath(ref).c_str());
+
+ // test rar/zip paths
+ ref = "zip://%2fpath%2fto%2fzip/subpath/to/file";
+ EXPECT_STRCASEEQ(ref.c_str(), URIUtils::GetRealPath("zip://%2fpath%2fto%2fzip/../subpath/to/file").c_str());
+ EXPECT_STRCASEEQ(ref.c_str(), URIUtils::GetRealPath("zip://%2fpath%2fto%2fzip/./subpath/to/file").c_str());
+ EXPECT_STRCASEEQ(ref.c_str(), URIUtils::GetRealPath("zip://%2fpath%2fto%2fzip/subpath/to/./file").c_str());
+ EXPECT_STRCASEEQ(ref.c_str(), URIUtils::GetRealPath("zip://%2fpath%2fto%2fzip/subpath/to/some/../file").c_str());
+
+ EXPECT_STRCASEEQ(ref.c_str(), URIUtils::GetRealPath("zip://%2fpath%2fto%2f.%2fzip/subpath/to/file").c_str());
+ EXPECT_STRCASEEQ(ref.c_str(), URIUtils::GetRealPath("zip://%2fpath%2fto%2fsome%2f..%2fzip/subpath/to/file").c_str());
+
+ // test zip/zip path
+ ref ="zip://zip%3a%2f%2f%252Fpath%252Fto%252Fzip%2fpath%2fto%2fzip/subpath/to/file";
+ EXPECT_STRCASEEQ(ref.c_str(), URIUtils::GetRealPath("zip://zip%3a%2f%2f%252Fpath%252Fto%252Fsome%252F..%252Fzip%2fpath%2fto%2fsome%2f..%2fzip/subpath/to/some/../file").c_str());
+}
+
+TEST_F(TestURIUtils, UpdateUrlEncoding)
+{
+ std::string oldUrl = "stack://zip://%2fpath%2fto%2farchive%2fsome%2darchive%2dfile%2eCD1%2ezip/video.avi , zip://%2fpath%2fto%2farchive%2fsome%2darchive%2dfile%2eCD2%2ezip/video.avi";
+ std::string newUrl = "stack://zip://%2fpath%2fto%2farchive%2fsome-archive-file.CD1.zip/video.avi , zip://%2fpath%2fto%2farchive%2fsome-archive-file.CD2.zip/video.avi";
+
+ EXPECT_TRUE(URIUtils::UpdateUrlEncoding(oldUrl));
+ EXPECT_STRCASEEQ(newUrl.c_str(), oldUrl.c_str());
+
+ oldUrl = "zip://%2fpath%2fto%2farchive%2fsome%2darchive%2efile%2ezip/video.avi";
+ newUrl = "zip://%2fpath%2fto%2farchive%2fsome-archive.file.zip/video.avi";
+
+ EXPECT_TRUE(URIUtils::UpdateUrlEncoding(oldUrl));
+ EXPECT_STRCASEEQ(newUrl.c_str(), oldUrl.c_str());
+
+ oldUrl = "/path/to/some/long%2dnamed%2efile";
+ newUrl = "/path/to/some/long%2dnamed%2efile";
+
+ EXPECT_FALSE(URIUtils::UpdateUrlEncoding(oldUrl));
+ EXPECT_STRCASEEQ(newUrl.c_str(), oldUrl.c_str());
+
+ oldUrl = "/path/to/some/long-named.file";
+ newUrl = "/path/to/some/long-named.file";
+
+ EXPECT_FALSE(URIUtils::UpdateUrlEncoding(oldUrl));
+ EXPECT_STRCASEEQ(newUrl.c_str(), oldUrl.c_str());
+}
diff --git a/xbmc/utils/test/TestUrlOptions.cpp b/xbmc/utils/test/TestUrlOptions.cpp
new file mode 100644
index 0000000..f684fe5
--- /dev/null
+++ b/xbmc/utils/test/TestUrlOptions.cpp
@@ -0,0 +1,193 @@
+/*
+ * 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 "utils/UrlOptions.h"
+#include "utils/Variant.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestUrlOptions, Clear)
+{
+ const char *key = "foo";
+
+ CUrlOptions urlOptions;
+ urlOptions.AddOption(key, "bar");
+ EXPECT_TRUE(urlOptions.HasOption(key));
+
+ urlOptions.Clear();
+ EXPECT_FALSE(urlOptions.HasOption(key));
+}
+
+TEST(TestUrlOptions, AddOption)
+{
+ const char *keyChar = "char";
+ const char *keyString = "string";
+ const char *keyEmpty = "empty";
+ const char *keyInt = "int";
+ const char *keyFloat = "float";
+ const char *keyDouble = "double";
+ const char *keyBool = "bool";
+
+ const char *valueChar = "valueChar";
+ const std::string valueString = "valueString";
+ const char *valueEmpty = "";
+ int valueInt = 1;
+ float valueFloat = 1.0f;
+ double valueDouble = 1.0;
+ bool valueBool = true;
+
+ CVariant variantValue;
+
+ CUrlOptions urlOptions;
+ urlOptions.AddOption(keyChar, valueChar);
+ {
+ CVariant variantValue;
+ EXPECT_TRUE(urlOptions.GetOption(keyChar, variantValue));
+ EXPECT_TRUE(variantValue.isString());
+ EXPECT_STREQ(valueChar, variantValue.asString().c_str());
+ }
+
+ urlOptions.AddOption(keyString, valueString);
+ {
+ CVariant variantValue;
+ EXPECT_TRUE(urlOptions.GetOption(keyString, variantValue));
+ EXPECT_TRUE(variantValue.isString());
+ EXPECT_STREQ(valueString.c_str(), variantValue.asString().c_str());
+ }
+
+ urlOptions.AddOption(keyEmpty, valueEmpty);
+ {
+ CVariant variantValue;
+ EXPECT_TRUE(urlOptions.GetOption(keyEmpty, variantValue));
+ EXPECT_TRUE(variantValue.isString());
+ EXPECT_STREQ(valueEmpty, variantValue.asString().c_str());
+ }
+
+ urlOptions.AddOption(keyInt, valueInt);
+ {
+ CVariant variantValue;
+ EXPECT_TRUE(urlOptions.GetOption(keyInt, variantValue));
+ EXPECT_TRUE(variantValue.isInteger());
+ EXPECT_EQ(valueInt, (int)variantValue.asInteger());
+ }
+
+ urlOptions.AddOption(keyFloat, valueFloat);
+ {
+ CVariant variantValue;
+ EXPECT_TRUE(urlOptions.GetOption(keyFloat, variantValue));
+ EXPECT_TRUE(variantValue.isDouble());
+ EXPECT_EQ(valueFloat, variantValue.asFloat());
+ }
+
+ urlOptions.AddOption(keyDouble, valueDouble);
+ {
+ CVariant variantValue;
+ EXPECT_TRUE(urlOptions.GetOption(keyDouble, variantValue));
+ EXPECT_TRUE(variantValue.isDouble());
+ EXPECT_EQ(valueDouble, variantValue.asDouble());
+ }
+
+ urlOptions.AddOption(keyBool, valueBool);
+ {
+ CVariant variantValue;
+ EXPECT_TRUE(urlOptions.GetOption(keyBool, variantValue));
+ EXPECT_TRUE(variantValue.isBoolean());
+ EXPECT_EQ(valueBool, variantValue.asBoolean());
+ }
+}
+
+TEST(TestUrlOptions, AddOptions)
+{
+ std::string ref = "foo=bar&key=value";
+
+ CUrlOptions urlOptions(ref);
+ {
+ CVariant value;
+ EXPECT_TRUE(urlOptions.GetOption("foo", value));
+ EXPECT_TRUE(value.isString());
+ EXPECT_STREQ("bar", value.asString().c_str());
+ }
+ {
+ CVariant value;
+ EXPECT_TRUE(urlOptions.GetOption("key", value));
+ EXPECT_TRUE(value.isString());
+ EXPECT_STREQ("value", value.asString().c_str());
+ }
+
+ ref = "foo=bar&key";
+ urlOptions.Clear();
+ urlOptions.AddOptions(ref);
+ {
+ CVariant value;
+ EXPECT_TRUE(urlOptions.GetOption("foo", value));
+ EXPECT_TRUE(value.isString());
+ EXPECT_STREQ("bar", value.asString().c_str());
+ }
+ {
+ CVariant value;
+ EXPECT_TRUE(urlOptions.GetOption("key", value));
+ EXPECT_TRUE(value.isString());
+ EXPECT_TRUE(value.empty());
+ }
+}
+
+TEST(TestUrlOptions, RemoveOption)
+{
+ const char *key = "foo";
+
+ CUrlOptions urlOptions;
+ urlOptions.AddOption(key, "bar");
+ EXPECT_TRUE(urlOptions.HasOption(key));
+
+ urlOptions.RemoveOption(key);
+ EXPECT_FALSE(urlOptions.HasOption(key));
+}
+
+TEST(TestUrlOptions, HasOption)
+{
+ const char *key = "foo";
+
+ CUrlOptions urlOptions;
+ urlOptions.AddOption(key, "bar");
+ EXPECT_TRUE(urlOptions.HasOption(key));
+ EXPECT_FALSE(urlOptions.HasOption("bar"));
+}
+
+TEST(TestUrlOptions, GetOptions)
+{
+ const char *key1 = "foo";
+ const char *key2 = "key";
+ const char *value1 = "bar";
+ const char *value2 = "value";
+
+ CUrlOptions urlOptions;
+ urlOptions.AddOption(key1, value1);
+ urlOptions.AddOption(key2, value2);
+ const CUrlOptions::UrlOptions &options = urlOptions.GetOptions();
+ EXPECT_FALSE(options.empty());
+ EXPECT_EQ(2U, options.size());
+
+ CUrlOptions::UrlOptions::const_iterator it1 = options.find(key1);
+ EXPECT_TRUE(it1 != options.end());
+ CUrlOptions::UrlOptions::const_iterator it2 = options.find(key2);
+ EXPECT_TRUE(it2 != options.end());
+ EXPECT_FALSE(options.find("wrong") != options.end());
+ EXPECT_TRUE(it1->second.isString());
+ EXPECT_TRUE(it2->second.isString());
+ EXPECT_STREQ(value1, it1->second.asString().c_str());
+ EXPECT_STREQ(value2, it2->second.asString().c_str());
+}
+
+TEST(TestUrlOptions, GetOptionsString)
+{
+ const char *ref = "foo=bar&key";
+
+ CUrlOptions urlOptions(ref);
+ std::string value = urlOptions.GetOptionsString();
+ EXPECT_STREQ(ref, value.c_str());
+}
diff --git a/xbmc/utils/test/TestVariant.cpp b/xbmc/utils/test/TestVariant.cpp
new file mode 100644
index 0000000..3c96cd0
--- /dev/null
+++ b/xbmc/utils/test/TestVariant.cpp
@@ -0,0 +1,334 @@
+/*
+ * 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 "utils/Variant.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestVariant, VariantTypeInteger)
+{
+ CVariant a((int)0), b((int64_t)1);
+
+ EXPECT_TRUE(a.isInteger());
+ EXPECT_EQ(CVariant::VariantTypeInteger, a.type());
+ EXPECT_TRUE(b.isInteger());
+ EXPECT_EQ(CVariant::VariantTypeInteger, b.type());
+
+ EXPECT_EQ((int64_t)1, b.asInteger());
+}
+
+TEST(TestVariant, VariantTypeUnsignedInteger)
+{
+ CVariant a((unsigned int)0), b((uint64_t)1);
+
+ EXPECT_TRUE(a.isUnsignedInteger());
+ EXPECT_EQ(CVariant::VariantTypeUnsignedInteger, a.type());
+ EXPECT_TRUE(b.isUnsignedInteger());
+ EXPECT_EQ(CVariant::VariantTypeUnsignedInteger, b.type());
+
+ EXPECT_EQ((uint64_t)1, b.asUnsignedInteger());
+}
+
+TEST(TestVariant, VariantTypeBoolean)
+{
+ CVariant a(true);
+
+ EXPECT_TRUE(a.isBoolean());
+ EXPECT_EQ(CVariant::VariantTypeBoolean, a.type());
+
+ EXPECT_TRUE(a.asBoolean());
+}
+
+TEST(TestVariant, VariantTypeString)
+{
+ CVariant a("VariantTypeString");
+ CVariant b("VariantTypeString2", sizeof("VariantTypeString2") - 1);
+ std::string str("VariantTypeString3");
+ CVariant c(str);
+
+ EXPECT_TRUE(a.isString());
+ EXPECT_EQ(CVariant::VariantTypeString, a.type());
+ EXPECT_TRUE(b.isString());
+ EXPECT_EQ(CVariant::VariantTypeString, b.type());
+ EXPECT_TRUE(c.isString());
+ EXPECT_EQ(CVariant::VariantTypeString, c.type());
+
+ EXPECT_STREQ("VariantTypeString", a.asString().c_str());
+ EXPECT_STREQ("VariantTypeString2", b.asString().c_str());
+ EXPECT_STREQ("VariantTypeString3", c.asString().c_str());
+}
+
+TEST(TestVariant, VariantTypeWideString)
+{
+ CVariant a(L"VariantTypeWideString");
+ CVariant b(L"VariantTypeWideString2", sizeof(L"VariantTypeWideString2") - 1);
+ std::wstring str(L"VariantTypeWideString3");
+ CVariant c(str);
+
+ EXPECT_TRUE(a.isWideString());
+ EXPECT_EQ(CVariant::VariantTypeWideString, a.type());
+ EXPECT_TRUE(b.isWideString());
+ EXPECT_EQ(CVariant::VariantTypeWideString, b.type());
+ EXPECT_TRUE(c.isWideString());
+ EXPECT_EQ(CVariant::VariantTypeWideString, c.type());
+
+ EXPECT_STREQ(L"VariantTypeWideString", a.asWideString().c_str());
+ EXPECT_STREQ(L"VariantTypeWideString2", b.asWideString().c_str());
+ EXPECT_STREQ(L"VariantTypeWideString3", c.asWideString().c_str());
+}
+
+TEST(TestVariant, VariantTypeDouble)
+{
+ CVariant a((float)0.0f), b((double)0.1f);
+
+ EXPECT_TRUE(a.isDouble());
+ EXPECT_EQ(CVariant::VariantTypeDouble, a.type());
+ EXPECT_TRUE(b.isDouble());
+ EXPECT_EQ(CVariant::VariantTypeDouble, b.type());
+
+ EXPECT_EQ((float)0.0f, a.asDouble());
+ EXPECT_EQ((double)0.1f, b.asDouble());
+}
+
+TEST(TestVariant, VariantTypeArray)
+{
+ std::vector<std::string> strarray;
+ strarray.emplace_back("string1");
+ strarray.emplace_back("string2");
+ strarray.emplace_back("string3");
+ strarray.emplace_back("string4");
+ CVariant a(strarray);
+
+ EXPECT_TRUE(a.isArray());
+ EXPECT_EQ(CVariant::VariantTypeArray, a.type());
+}
+
+TEST(TestVariant, VariantTypeObject)
+{
+ CVariant a;
+ a["key"] = "value";
+
+ EXPECT_TRUE(a.isObject());
+ EXPECT_EQ(CVariant::VariantTypeObject, a.type());
+}
+
+TEST(TestVariant, VariantTypeNull)
+{
+ CVariant a;
+
+ EXPECT_TRUE(a.isNull());
+ EXPECT_EQ(CVariant::VariantTypeNull, a.type());
+}
+
+TEST(TestVariant, VariantFromMap)
+{
+ std::map<std::string, std::string> strMap;
+ strMap["key"] = "value";
+ CVariant a = strMap;
+
+ EXPECT_TRUE(a.isObject());
+ EXPECT_TRUE(a.size() == 1);
+ EXPECT_EQ(CVariant::VariantTypeObject, a.type());
+ EXPECT_TRUE(a.isMember("key"));
+ EXPECT_TRUE(a["key"].isString());
+ EXPECT_STREQ(a["key"].asString().c_str(), "value");
+
+ std::map<std::string, CVariant> variantMap;
+ variantMap["key"] = CVariant("value");
+ CVariant b = variantMap;
+
+ EXPECT_TRUE(b.isObject());
+ EXPECT_TRUE(b.size() == 1);
+ EXPECT_EQ(CVariant::VariantTypeObject, b.type());
+ EXPECT_TRUE(b.isMember("key"));
+ EXPECT_TRUE(b["key"].isString());
+ EXPECT_STREQ(b["key"].asString().c_str(), "value");
+}
+
+TEST(TestVariant, operatorTest)
+{
+ std::vector<std::string> strarray;
+ strarray.emplace_back("string1");
+ CVariant a, b, c(strarray), d;
+ a["key"] = "value";
+ b = a;
+ c[0] = "value2";
+ d = c;
+
+ EXPECT_TRUE(a.isObject());
+ EXPECT_EQ(CVariant::VariantTypeObject, a.type());
+ EXPECT_TRUE(b.isObject());
+ EXPECT_EQ(CVariant::VariantTypeObject, b.type());
+ EXPECT_TRUE(c.isArray());
+ EXPECT_EQ(CVariant::VariantTypeArray, c.type());
+ EXPECT_TRUE(d.isArray());
+ EXPECT_EQ(CVariant::VariantTypeArray, d.type());
+
+ EXPECT_TRUE(a == b);
+ EXPECT_TRUE(c == d);
+ EXPECT_FALSE(a == d);
+
+ EXPECT_STREQ("value", a["key"].asString().c_str());
+ EXPECT_STREQ("value2", c[0].asString().c_str());
+}
+
+TEST(TestVariant, push_back)
+{
+ CVariant a, b("variant1"), c("variant2"), d("variant3");
+ a.push_back(b);
+ a.push_back(c);
+ a.push_back(d);
+
+ EXPECT_TRUE(a.isArray());
+ EXPECT_EQ(CVariant::VariantTypeArray, a.type());
+ EXPECT_STREQ("variant1", a[0].asString().c_str());
+ EXPECT_STREQ("variant2", a[1].asString().c_str());
+ EXPECT_STREQ("variant3", a[2].asString().c_str());
+}
+
+TEST(TestVariant, append)
+{
+ CVariant a, b("variant1"), c("variant2"), d("variant3");
+ a.append(b);
+ a.append(c);
+ a.append(d);
+
+ EXPECT_TRUE(a.isArray());
+ EXPECT_EQ(CVariant::VariantTypeArray, a.type());
+ EXPECT_STREQ("variant1", a[0].asString().c_str());
+ EXPECT_STREQ("variant2", a[1].asString().c_str());
+ EXPECT_STREQ("variant3", a[2].asString().c_str());
+}
+
+TEST(TestVariant, c_str)
+{
+ CVariant a("variant");
+
+ EXPECT_STREQ("variant", a.c_str());
+}
+
+TEST(TestVariant, swap)
+{
+ CVariant a((int)0), b("variant");
+
+ EXPECT_TRUE(a.isInteger());
+ EXPECT_TRUE(b.isString());
+
+ a.swap(b);
+ EXPECT_TRUE(b.isInteger());
+ EXPECT_TRUE(a.isString());
+}
+
+TEST(TestVariant, iterator_array)
+{
+ std::vector<std::string> strarray;
+ strarray.emplace_back("string");
+ strarray.emplace_back("string");
+ strarray.emplace_back("string");
+ strarray.emplace_back("string");
+ CVariant a(strarray);
+
+ EXPECT_TRUE(a.isArray());
+ EXPECT_EQ(CVariant::VariantTypeArray, a.type());
+
+ for (auto it = a.begin_array(); it != a.end_array(); it++)
+ {
+ EXPECT_STREQ("string", it->c_str());
+ }
+
+ for (auto const_it = a.begin_array(); const_it != a.end_array(); const_it++)
+ {
+ EXPECT_STREQ("string", const_it->c_str());
+ }
+}
+
+TEST(TestVariant, iterator_map)
+{
+ CVariant a;
+ a["key1"] = "string";
+ a["key2"] = "string";
+ a["key3"] = "string";
+ a["key4"] = "string";
+
+ EXPECT_TRUE(a.isObject());
+ EXPECT_EQ(CVariant::VariantTypeObject, a.type());
+
+ for (auto it = a.begin_map(); it != a.end_map(); it++)
+ {
+ EXPECT_STREQ("string", it->second.c_str());
+ }
+
+ for (auto const_it = a.begin_map(); const_it != a.end_map(); const_it++)
+ {
+ EXPECT_STREQ("string", const_it->second.c_str());
+ }
+}
+
+TEST(TestVariant, size)
+{
+ std::vector<std::string> strarray;
+ strarray.emplace_back("string");
+ strarray.emplace_back("string");
+ strarray.emplace_back("string");
+ strarray.emplace_back("string");
+ CVariant a(strarray);
+
+ EXPECT_EQ((unsigned int)4, a.size());
+}
+
+TEST(TestVariant, empty)
+{
+ std::vector<std::string> strarray;
+ CVariant a(strarray);
+
+ EXPECT_TRUE(a.empty());
+}
+
+TEST(TestVariant, clear)
+{
+ std::vector<std::string> strarray;
+ strarray.emplace_back("string");
+ strarray.emplace_back("string");
+ strarray.emplace_back("string");
+ strarray.emplace_back("string");
+ CVariant a(strarray);
+
+ EXPECT_FALSE(a.empty());
+ a.clear();
+ EXPECT_TRUE(a.empty());
+}
+
+TEST(TestVariant, erase)
+{
+ std::vector<std::string> strarray;
+ strarray.emplace_back("string1");
+ strarray.emplace_back("string2");
+ strarray.emplace_back("string3");
+ strarray.emplace_back("string4");
+ CVariant a, b(strarray);
+ a["key1"] = "string1";
+ a["key2"] = "string2";
+ a["key3"] = "string3";
+ a["key4"] = "string4";
+
+ EXPECT_STREQ("string2", a["key2"].c_str());
+ EXPECT_STREQ("string2", b[1].c_str());
+ a.erase("key2");
+ b.erase(1);
+ EXPECT_FALSE(a["key2"].c_str());
+ EXPECT_STREQ("string3", b[1].c_str());
+}
+
+TEST(TestVariant, isMember)
+{
+ CVariant a;
+ a["key1"] = "string1";
+
+ EXPECT_TRUE(a.isMember("key1"));
+ EXPECT_FALSE(a.isMember("key2"));
+}
diff --git a/xbmc/utils/test/TestXBMCTinyXML.cpp b/xbmc/utils/test/TestXBMCTinyXML.cpp
new file mode 100644
index 0000000..b3f84eb
--- /dev/null
+++ b/xbmc/utils/test/TestXBMCTinyXML.cpp
@@ -0,0 +1,58 @@
+/*
+ * 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 "test/TestUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestXBMCTinyXML, ParseFromString)
+{
+ bool retval = false;
+ // scraper results with unescaped &
+ CXBMCTinyXML doc;
+ std::string data("<details><url function=\"ParseTMDBRating\" "
+ "cache=\"tmdb-en-12244.json\">"
+ "http://api.themoviedb.org/3/movie/12244"
+ "?api_key=57983e31fb435df4df77afb854740ea9"
+ "&language=en&#x3f;&#x003F;&#0063;</url></details>");
+ doc.Parse(data);
+ TiXmlNode *root = doc.RootElement();
+ if (root && root->ValueStr() == "details")
+ {
+ TiXmlElement *url = root->FirstChildElement("url");
+ if (url && url->FirstChild())
+ {
+ retval = (url->FirstChild()->ValueStr() == "http://api.themoviedb.org/3/movie/12244?api_key=57983e31fb435df4df77afb854740ea9&language=en???");
+ }
+ }
+ EXPECT_TRUE(retval);
+}
+
+TEST(TestXBMCTinyXML, ParseFromFileHandle)
+{
+ bool retval = false;
+ // scraper results with unescaped &
+ CXBMCTinyXML doc;
+ FILE *f = fopen(XBMC_REF_FILE_PATH("/xbmc/utils/test/CXBMCTinyXML-test.xml").c_str(), "r");
+ ASSERT_NE(nullptr, f);
+ doc.LoadFile(f);
+ fclose(f);
+ TiXmlNode *root = doc.RootElement();
+ if (root && root->ValueStr() == "details")
+ {
+ TiXmlElement *url = root->FirstChildElement("url");
+ if (url && url->FirstChild())
+ {
+ std::string str = url->FirstChild()->ValueStr();
+ retval = (StringUtils::Trim(str) == "http://api.themoviedb.org/3/movie/12244?api_key=57983e31fb435df4df77afb854740ea9&language=en???");
+ }
+ }
+ EXPECT_TRUE(retval);
+}
diff --git a/xbmc/utils/test/TestXMLUtils.cpp b/xbmc/utils/test/TestXMLUtils.cpp
new file mode 100644
index 0000000..ba4c87c
--- /dev/null
+++ b/xbmc/utils/test/TestXMLUtils.cpp
@@ -0,0 +1,356 @@
+/*
+ * 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 "XBDateTime.h"
+#include "utils/StringUtils.h"
+#include "utils/XMLUtils.h"
+
+#include <gtest/gtest.h>
+
+TEST(TestXMLUtils, GetHex)
+{
+ CXBMCTinyXML a;
+ uint32_t ref, val;
+
+ a.Parse(std::string("<root><node>0xFF</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetHex(a.RootElement(), "node", val));
+
+ ref = 0xFF;
+ EXPECT_EQ(ref, val);
+}
+
+TEST(TestXMLUtils, GetUInt)
+{
+ CXBMCTinyXML a;
+ uint32_t ref, val;
+
+ a.Parse(std::string("<root><node>1000</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetUInt(a.RootElement(), "node", val));
+
+ ref = 1000;
+ EXPECT_EQ(ref, val);
+}
+
+TEST(TestXMLUtils, GetLong)
+{
+ CXBMCTinyXML a;
+ long ref, val;
+
+ a.Parse(std::string("<root><node>1000</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetLong(a.RootElement(), "node", val));
+
+ ref = 1000;
+ EXPECT_EQ(ref, val);
+}
+
+TEST(TestXMLUtils, GetFloat)
+{
+ CXBMCTinyXML a;
+ float ref, val;
+
+ a.Parse(std::string("<root><node>1000.1f</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetFloat(a.RootElement(), "node", val));
+ EXPECT_TRUE(XMLUtils::GetFloat(a.RootElement(), "node", val, 1000.0f,
+ 1000.2f));
+ ref = 1000.1f;
+ EXPECT_EQ(ref, val);
+}
+
+TEST(TestXMLUtils, GetDouble)
+{
+ CXBMCTinyXML a;
+ double val;
+ std::string refstr, valstr;
+
+ a.Parse(std::string("<root><node>1000.1f</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetDouble(a.RootElement(), "node", val));
+
+ refstr = "1000.100000";
+ valstr = StringUtils::Format("{:f}", val);
+ EXPECT_STREQ(refstr.c_str(), valstr.c_str());
+}
+
+TEST(TestXMLUtils, GetInt)
+{
+ CXBMCTinyXML a;
+ int ref, val;
+
+ a.Parse(std::string("<root><node>1000</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetInt(a.RootElement(), "node", val));
+ EXPECT_TRUE(XMLUtils::GetInt(a.RootElement(), "node", val, 999, 1001));
+
+ ref = 1000;
+ EXPECT_EQ(ref, val);
+}
+
+TEST(TestXMLUtils, GetBoolean)
+{
+ CXBMCTinyXML a;
+ bool ref, val;
+
+ a.Parse(std::string("<root><node>true</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetBoolean(a.RootElement(), "node", val));
+
+ ref = true;
+ EXPECT_EQ(ref, val);
+}
+
+TEST(TestXMLUtils, GetString)
+{
+ CXBMCTinyXML a;
+ std::string ref, val;
+
+ a.Parse(std::string("<root><node>some string</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetString(a.RootElement(), "node", val));
+
+ ref = "some string";
+ EXPECT_STREQ(ref.c_str(), val.c_str());
+}
+
+TEST(TestXMLUtils, GetAdditiveString)
+{
+ CXBMCTinyXML a, b;
+ std::string ref, val;
+
+ a.Parse(std::string("<root>\n"
+ " <node>some string1</node>\n"
+ " <node>some string2</node>\n"
+ " <node>some string3</node>\n"
+ " <node>some string4</node>\n"
+ " <node>some string5</node>\n"
+ "</root>\n"));
+ EXPECT_TRUE(XMLUtils::GetAdditiveString(a.RootElement(), "node", ",", val));
+
+ ref = "some string1,some string2,some string3,some string4,some string5";
+ EXPECT_STREQ(ref.c_str(), val.c_str());
+
+ val.clear();
+ b.Parse(std::string("<root>\n"
+ " <node>some string1</node>\n"
+ " <node>some string2</node>\n"
+ " <node clear=\"true\">some string3</node>\n"
+ " <node>some string4</node>\n"
+ " <node>some string5</node>\n"
+ "</root>\n"));
+ EXPECT_TRUE(XMLUtils::GetAdditiveString(b.RootElement(), "node", ",", val));
+
+ ref = "some string3,some string4,some string5";
+ EXPECT_STREQ(ref.c_str(), val.c_str());
+}
+
+TEST(TestXMLUtils, GetStringArray)
+{
+ CXBMCTinyXML a;
+ std::vector<std::string> strarray;
+
+ a.Parse(std::string("<root>\n"
+ " <node>some string1</node>\n"
+ " <node>some string2</node>\n"
+ " <node>some string3</node>\n"
+ " <node>some string4</node>\n"
+ " <node>some string5</node>\n"
+ "</root>\n"));
+ EXPECT_TRUE(XMLUtils::GetStringArray(a.RootElement(), "node", strarray));
+
+ EXPECT_STREQ("some string1", strarray.at(0).c_str());
+ EXPECT_STREQ("some string2", strarray.at(1).c_str());
+ EXPECT_STREQ("some string3", strarray.at(2).c_str());
+ EXPECT_STREQ("some string4", strarray.at(3).c_str());
+ EXPECT_STREQ("some string5", strarray.at(4).c_str());
+}
+
+TEST(TestXMLUtils, GetPath)
+{
+ CXBMCTinyXML a, b;
+ std::string ref, val;
+
+ a.Parse(std::string("<root><node urlencoded=\"yes\">special://xbmc/</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetPath(a.RootElement(), "node", val));
+
+ ref = "special://xbmc/";
+ EXPECT_STREQ(ref.c_str(), val.c_str());
+
+ val.clear();
+ b.Parse(std::string("<root><node>special://xbmcbin/</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetPath(b.RootElement(), "node", val));
+
+ ref = "special://xbmcbin/";
+ EXPECT_STREQ(ref.c_str(), val.c_str());
+}
+
+TEST(TestXMLUtils, GetDate)
+{
+ CXBMCTinyXML a;
+ CDateTime ref, val;
+
+ a.Parse(std::string("<root><node>2012-07-08</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetDate(a.RootElement(), "node", val));
+ ref.SetDate(2012, 7, 8);
+ EXPECT_TRUE(ref == val);
+}
+
+TEST(TestXMLUtils, GetDateTime)
+{
+ CXBMCTinyXML a;
+ CDateTime ref, val;
+
+ a.Parse(std::string("<root><node>2012-07-08 01:02:03</node></root>"));
+ EXPECT_TRUE(XMLUtils::GetDateTime(a.RootElement(), "node", val));
+ ref.SetDateTime(2012, 7, 8, 1, 2, 3);
+ EXPECT_TRUE(ref == val);
+}
+
+TEST(TestXMLUtils, SetString)
+{
+ CXBMCTinyXML a;
+ std::string ref, val;
+
+ a.Parse(std::string("<root></root>"));
+ XMLUtils::SetString(a.RootElement(), "node", "some string");
+ EXPECT_TRUE(XMLUtils::GetString(a.RootElement(), "node", val));
+
+ ref = "some string";
+ EXPECT_STREQ(ref.c_str(), val.c_str());
+}
+
+TEST(TestXMLUtils, SetAdditiveString)
+{
+ CXBMCTinyXML a;
+ std::string ref, val;
+
+ a.Parse(std::string("<root></root>"));
+ XMLUtils::SetAdditiveString(a.RootElement(), "node", ",",
+ "some string1,some string2,some string3,some string4,some string5");
+ EXPECT_TRUE(XMLUtils::GetAdditiveString(a.RootElement(), "node", ",", val));
+
+ ref = "some string1,some string2,some string3,some string4,some string5";
+ EXPECT_STREQ(ref.c_str(), val.c_str());
+}
+
+TEST(TestXMLUtils, SetStringArray)
+{
+ CXBMCTinyXML a;
+ std::vector<std::string> strarray;
+ strarray.emplace_back("some string1");
+ strarray.emplace_back("some string2");
+ strarray.emplace_back("some string3");
+ strarray.emplace_back("some string4");
+ strarray.emplace_back("some string5");
+
+ a.Parse(std::string("<root></root>"));
+ XMLUtils::SetStringArray(a.RootElement(), "node", strarray);
+ EXPECT_TRUE(XMLUtils::GetStringArray(a.RootElement(), "node", strarray));
+
+ EXPECT_STREQ("some string1", strarray.at(0).c_str());
+ EXPECT_STREQ("some string2", strarray.at(1).c_str());
+ EXPECT_STREQ("some string3", strarray.at(2).c_str());
+ EXPECT_STREQ("some string4", strarray.at(3).c_str());
+ EXPECT_STREQ("some string5", strarray.at(4).c_str());
+}
+
+TEST(TestXMLUtils, SetInt)
+{
+ CXBMCTinyXML a;
+ int ref, val;
+
+ a.Parse(std::string("<root></root>"));
+ XMLUtils::SetInt(a.RootElement(), "node", 1000);
+ EXPECT_TRUE(XMLUtils::GetInt(a.RootElement(), "node", val));
+
+ ref = 1000;
+ EXPECT_EQ(ref, val);
+}
+
+TEST(TestXMLUtils, SetFloat)
+{
+ CXBMCTinyXML a;
+ float ref, val;
+
+ a.Parse(std::string("<root></root>"));
+ XMLUtils::SetFloat(a.RootElement(), "node", 1000.1f);
+ EXPECT_TRUE(XMLUtils::GetFloat(a.RootElement(), "node", val));
+
+ ref = 1000.1f;
+ EXPECT_EQ(ref, val);
+}
+
+TEST(TestXMLUtils, SetBoolean)
+{
+ CXBMCTinyXML a;
+ bool ref, val;
+
+ a.Parse(std::string("<root></root>"));
+ XMLUtils::SetBoolean(a.RootElement(), "node", true);
+ EXPECT_TRUE(XMLUtils::GetBoolean(a.RootElement(), "node", val));
+
+ ref = true;
+ EXPECT_EQ(ref, val);
+}
+
+TEST(TestXMLUtils, SetHex)
+{
+ CXBMCTinyXML a;
+ uint32_t ref, val;
+
+ a.Parse(std::string("<root></root>"));
+ XMLUtils::SetHex(a.RootElement(), "node", 0xFF);
+ EXPECT_TRUE(XMLUtils::GetHex(a.RootElement(), "node", val));
+
+ ref = 0xFF;
+ EXPECT_EQ(ref, val);
+}
+
+TEST(TestXMLUtils, SetPath)
+{
+ CXBMCTinyXML a;
+ std::string ref, val;
+
+ a.Parse(std::string("<root></root>"));
+ XMLUtils::SetPath(a.RootElement(), "node", "special://xbmc/");
+ EXPECT_TRUE(XMLUtils::GetPath(a.RootElement(), "node", val));
+
+ ref = "special://xbmc/";
+ EXPECT_STREQ(ref.c_str(), val.c_str());
+}
+
+TEST(TestXMLUtils, SetLong)
+{
+ CXBMCTinyXML a;
+ long ref, val;
+
+ a.Parse(std::string("<root></root>"));
+ XMLUtils::SetLong(a.RootElement(), "node", 1000);
+ EXPECT_TRUE(XMLUtils::GetLong(a.RootElement(), "node", val));
+
+ ref = 1000;
+ EXPECT_EQ(ref, val);
+}
+
+TEST(TestXMLUtils, SetDate)
+{
+ CXBMCTinyXML a;
+ CDateTime ref, val;
+
+ a.Parse(std::string("<root></root>"));
+ ref.SetDate(2012, 7, 8);
+ XMLUtils::SetDate(a.RootElement(), "node", ref);
+ EXPECT_TRUE(XMLUtils::GetDate(a.RootElement(), "node", val));
+ EXPECT_TRUE(ref == val);
+}
+
+TEST(TestXMLUtils, SetDateTime)
+{
+ CXBMCTinyXML a;
+ CDateTime ref, val;
+
+ a.Parse(std::string("<root></root>"));
+ ref.SetDateTime(2012, 7, 8, 1, 2, 3);
+ XMLUtils::SetDateTime(a.RootElement(), "node", ref);
+ EXPECT_TRUE(XMLUtils::GetDateTime(a.RootElement(), "node", val));
+ EXPECT_TRUE(ref == val);
+}
diff --git a/xbmc/utils/test/Testlog.cpp b/xbmc/utils/test/Testlog.cpp
new file mode 100644
index 0000000..a700d2a
--- /dev/null
+++ b/xbmc/utils/test/Testlog.cpp
@@ -0,0 +1,96 @@
+/*
+ * 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 "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "test/TestUtils.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include <stdlib.h>
+
+#include <gtest/gtest.h>
+
+class Testlog : public testing::Test
+{
+protected:
+ Testlog() = default;
+ ~Testlog() override { CServiceBroker::GetLogging().Deinitialize(); }
+};
+
+TEST_F(Testlog, Log)
+{
+ std::string logfile, logstring;
+ char buf[100];
+ ssize_t bytesread;
+ XFILE::CFile file;
+ CRegExp regex;
+
+ std::string appName = CCompileInfo::GetAppName();
+ StringUtils::ToLower(appName);
+ logfile = CSpecialProtocol::TranslatePath("special://temp/") + appName + ".log";
+ CServiceBroker::GetLogging().Initialize(
+ CSpecialProtocol::TranslatePath("special://temp/").c_str());
+ EXPECT_TRUE(XFILE::CFile::Exists(logfile));
+
+ CLog::Log(LOGDEBUG, "debug log message");
+ CLog::Log(LOGINFO, "info log message");
+ CLog::Log(LOGWARNING, "warning log message");
+ CLog::Log(LOGERROR, "error log message");
+ CLog::Log(LOGFATAL, "fatal log message");
+ CLog::Log(LOGNONE, "none type log message");
+ CServiceBroker::GetLogging().Deinitialize();
+
+ EXPECT_TRUE(file.Open(logfile));
+ while ((bytesread = file.Read(buf, sizeof(buf) - 1)) > 0)
+ {
+ buf[bytesread] = '\0';
+ logstring.append(buf);
+ }
+ file.Close();
+ EXPECT_FALSE(logstring.empty());
+
+ EXPECT_STREQ("\xEF\xBB\xBF", logstring.substr(0, 3).c_str());
+
+ EXPECT_TRUE(regex.RegComp(".*(debug|DEBUG) <general>: debug log message.*"));
+ EXPECT_GE(regex.RegFind(logstring), 0);
+ EXPECT_TRUE(regex.RegComp(".*(info|INFO) <general>: info log message.*"));
+ EXPECT_GE(regex.RegFind(logstring), 0);
+ EXPECT_TRUE(regex.RegComp(".*(warning|WARNING) <general>: warning log message.*"));
+ EXPECT_GE(regex.RegFind(logstring), 0);
+ EXPECT_TRUE(regex.RegComp(".*(error|ERROR) <general>: error log message.*"));
+ EXPECT_GE(regex.RegFind(logstring), 0);
+ EXPECT_TRUE(regex.RegComp(".*(critical|CRITICAL|fatal|FATAL) <general>: fatal log message.*"));
+ EXPECT_GE(regex.RegFind(logstring), 0);
+ EXPECT_TRUE(regex.RegComp(".*(off|OFF) <general>: none type log message.*"));
+ EXPECT_GE(regex.RegFind(logstring), 0);
+
+ EXPECT_TRUE(XFILE::CFile::Delete(logfile));
+}
+
+TEST_F(Testlog, SetLogLevel)
+{
+ std::string logfile;
+
+ std::string appName = CCompileInfo::GetAppName();
+ StringUtils::ToLower(appName);
+ logfile = CSpecialProtocol::TranslatePath("special://temp/") + appName + ".log";
+ CServiceBroker::GetLogging().Initialize(
+ CSpecialProtocol::TranslatePath("special://temp/").c_str());
+ EXPECT_TRUE(XFILE::CFile::Exists(logfile));
+
+ EXPECT_EQ(LOG_LEVEL_DEBUG, CServiceBroker::GetLogging().GetLogLevel());
+ CServiceBroker::GetLogging().SetLogLevel(LOG_LEVEL_MAX);
+ EXPECT_EQ(LOG_LEVEL_MAX, CServiceBroker::GetLogging().GetLogLevel());
+
+ CServiceBroker::GetLogging().Deinitialize();
+ EXPECT_TRUE(XFILE::CFile::Delete(logfile));
+}
diff --git a/xbmc/utils/test/Testrfft.cpp b/xbmc/utils/test/Testrfft.cpp
new file mode 100644
index 0000000..a6c859d
--- /dev/null
+++ b/xbmc/utils/test/Testrfft.cpp
@@ -0,0 +1,41 @@
+/*
+ * 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 "utils/rfft.h"
+
+#include <gtest/gtest.h>
+
+#if defined(TARGET_WINDOWS) && !defined(_USE_MATH_DEFINES)
+#define _USE_MATH_DEFINES
+#endif
+
+#include <math.h>
+
+
+TEST(TestRFFT, SimpleSignal)
+{
+ const int size = 32;
+ const int freq1 = 5;
+ const int freq2[] = {1,7};
+ std::vector<float> input(2*size);
+ std::vector<float> output(size);
+ for (size_t i=0;i<size;++i)
+ {
+ input[2*i] = cos(freq1*2.0*M_PI*i/size);
+ input[2*i+1] = cos(freq2[0]*2.0*M_PI*i/size)+cos(freq2[1]*2.0*M_PI*i/size);
+ }
+ RFFT transform(size, false);
+
+ transform.calc(&input[0], &output[0]);
+
+ for (int i=0;i<size/2;++i)
+ {
+ EXPECT_NEAR(output[2*i],(i==freq1?1.0:0.0), 1e-7);
+ EXPECT_NEAR(output[2*i+1], ((i==freq2[0]||i==freq2[1])?1.0:0.0), 1e-7);
+ }
+}
diff --git a/xbmc/utils/test/data/language/Spanish/strings.po b/xbmc/utils/test/data/language/Spanish/strings.po
new file mode 100644
index 0000000..8ee8d02
--- /dev/null
+++ b/xbmc/utils/test/data/language/Spanish/strings.po
@@ -0,0 +1,26 @@
+# Kodi Media Center language file
+msgid ""
+msgstr ""
+"Project-Id-Version: XBMC Main\n"
+"Report-Msgid-Bugs-To: http://trac.xbmc.org/\n"
+"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Kodi Translation Team\n"
+"Language-Team: Spanish (http://www.transifex.com/projects/p/xbmc-main/language/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "#0"
+msgid "Programs"
+msgstr "Programas"
+
+msgctxt "#1"
+msgid "Pictures"
+msgstr "Imágenes"
+
+msgctxt "#2"
+msgid "Music"
+msgstr "Música"
diff --git a/xbmc/video/Bookmark.cpp b/xbmc/video/Bookmark.cpp
new file mode 100644
index 0000000..d18c00e
--- /dev/null
+++ b/xbmc/video/Bookmark.cpp
@@ -0,0 +1,39 @@
+/*
+ * 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 "Bookmark.h"
+
+CBookmark::CBookmark()
+{
+ Reset();
+}
+
+void CBookmark::Reset()
+{
+ episodeNumber = 0;
+ seasonNumber = 0;
+ timeInSeconds = 0.0;
+ totalTimeInSeconds = 0.0;
+ partNumber = 0;
+ type = STANDARD;
+}
+
+bool CBookmark::IsSet() const
+{
+ return totalTimeInSeconds > 0.0;
+}
+
+bool CBookmark::IsPartWay() const
+{
+ return totalTimeInSeconds > 0.0 && timeInSeconds > 0.0;
+}
+
+bool CBookmark::HasSavedPlayerState() const
+{
+ return !playerState.empty();
+}
diff --git a/xbmc/video/Bookmark.h b/xbmc/video/Bookmark.h
new file mode 100644
index 0000000..6c118b1
--- /dev/null
+++ b/xbmc/video/Bookmark.h
@@ -0,0 +1,53 @@
+/*
+ * 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 <string>
+#include <vector>
+
+class CBookmark
+{
+public:
+ CBookmark();
+ void Reset();
+
+ /*! \brief returns true if this bookmark has been set.
+ \return true if totalTimeInSeconds is positive.
+ */
+ bool IsSet() const;
+
+ /*! \brief returns true if this bookmark is part way through the video file
+ \return true if both totalTimeInSeconds and timeInSeconds are positive.
+ */
+ bool IsPartWay() const;
+
+ /*! \brief returns true if this bookmark has a stored serialized player state
+ \return true if playerState is not empty.
+ */
+ bool HasSavedPlayerState() const;
+
+ double timeInSeconds;
+ double totalTimeInSeconds;
+ long partNumber;
+ std::string thumbNailImage;
+ std::string playerState;
+ std::string player;
+ long seasonNumber;
+ long episodeNumber;
+
+ enum EType
+ {
+ STANDARD = 0,
+ RESUME = 1,
+ EPISODE = 2
+ } type;
+};
+
+typedef std::vector<CBookmark> VECBOOKMARKS;
+
diff --git a/xbmc/video/CMakeLists.txt b/xbmc/video/CMakeLists.txt
new file mode 100644
index 0000000..00f9baf
--- /dev/null
+++ b/xbmc/video/CMakeLists.txt
@@ -0,0 +1,33 @@
+set(SOURCES Bookmark.cpp
+ ContextMenus.cpp
+ GUIViewStateVideo.cpp
+ PlayerController.cpp
+ Teletext.cpp
+ VideoDatabase.cpp
+ VideoDbUrl.cpp
+ VideoInfoDownloader.cpp
+ VideoInfoScanner.cpp
+ VideoInfoTag.cpp
+ VideoLibraryQueue.cpp
+ VideoThumbLoader.cpp
+ VideoUtils.cpp
+ ViewModeSettings.cpp)
+
+set(HEADERS Bookmark.h
+ ContextMenus.h
+ Episode.h
+ GUIViewStateVideo.h
+ PlayerController.h
+ Teletext.h
+ TeletextDefines.h
+ VideoDatabase.h
+ VideoDbUrl.h
+ VideoInfoDownloader.h
+ VideoInfoScanner.h
+ VideoInfoTag.h
+ VideoLibraryQueue.h
+ VideoThumbLoader.h
+ VideoUtils.h
+ ViewModeSettings.h)
+
+core_add_library(video)
diff --git a/xbmc/video/ContextMenus.cpp b/xbmc/video/ContextMenus.cpp
new file mode 100644
index 0000000..c1f4c43
--- /dev/null
+++ b/xbmc/video/ContextMenus.cpp
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenus.h"
+
+#include "Autorun.h"
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "playlists/PlayList.h"
+#include "settings/MediaSettings.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "video/VideoUtils.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+#include "video/windows/GUIWindowVideoBase.h"
+#include "view/GUIViewState.h"
+
+#include <utility>
+
+namespace CONTEXTMENU
+{
+
+CVideoInfo::CVideoInfo(MediaType mediaType)
+ : CStaticContextMenuAction(19033), m_mediaType(std::move(mediaType))
+{
+}
+
+bool CVideoInfo::IsVisible(const CFileItem& item) const
+{
+ if (!item.HasVideoInfoTag())
+ return false;
+
+ if (item.IsPVRRecording())
+ return false; // pvr recordings have its own implementation for this
+
+ return item.GetVideoInfoTag()->m_type == m_mediaType;
+}
+
+bool CVideoInfo::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ CGUIDialogVideoInfo::ShowFor(*item);
+ return true;
+}
+
+bool CVideoRemoveResumePoint::IsVisible(const CFileItem& itemIn) const
+{
+ CFileItem item(itemIn.GetItemToPlay());
+ if (item.IsDeleted()) // e.g. trashed pvr recording
+ return false;
+
+ // Folders don't have a resume point
+ return !item.m_bIsFolder && VIDEO_UTILS::GetItemResumeInformation(item).isResumable;
+}
+
+bool CVideoRemoveResumePoint::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ CVideoLibraryQueue::GetInstance().ResetResumePoint(item);
+ return true;
+}
+
+bool CVideoMarkWatched::IsVisible(const CFileItem& item) const
+{
+ if (item.IsDeleted()) // e.g. trashed pvr recording
+ return false;
+
+ if (item.m_bIsFolder && item.IsPlugin()) // we cannot manage plugin folder's watched state
+ return false;
+
+ if (item.m_bIsFolder) // Only allow video db content, video and recording folders to be updated recursively
+ {
+ if (item.HasVideoInfoTag())
+ return item.IsVideoDb();
+ else if (item.GetProperty("IsVideoFolder").asBoolean())
+ return true;
+ else
+ return !item.IsParentFolder() && URIUtils::IsPVRRecordingFileOrFolder(item.GetPath());
+ }
+ else if (!item.HasVideoInfoTag())
+ return false;
+
+ return item.GetVideoInfoTag()->GetPlayCount() == 0;
+}
+
+bool CVideoMarkWatched::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ CVideoLibraryQueue::GetInstance().MarkAsWatched(item, true);
+ return true;
+}
+
+bool CVideoMarkUnWatched::IsVisible(const CFileItem& item) const
+{
+ if (item.IsDeleted()) // e.g. trashed pvr recording
+ return false;
+
+ if (item.m_bIsFolder && item.IsPlugin()) // we cannot manage plugin folder's watched state
+ return false;
+
+ if (item.m_bIsFolder) // Only allow video db content, video and recording folders to be updated recursively
+ {
+ if (item.HasVideoInfoTag())
+ return item.IsVideoDb();
+ else if (item.GetProperty("IsVideoFolder").asBoolean())
+ return true;
+ else
+ return !item.IsParentFolder() && URIUtils::IsPVRRecordingFileOrFolder(item.GetPath());
+ }
+ else if (!item.HasVideoInfoTag())
+ return false;
+
+ return item.GetVideoInfoTag()->GetPlayCount() > 0;
+}
+
+bool CVideoMarkUnWatched::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ CVideoLibraryQueue::GetInstance().MarkAsWatched(item, false);
+ return true;
+}
+
+bool CVideoBrowse::IsVisible(const CFileItem& item) const
+{
+ if (item.IsFileFolder(EFILEFOLDER_MASK_ONBROWSE))
+ return false; // handled by CMediaWindow
+
+ return item.m_bIsFolder && VIDEO_UTILS::IsItemPlayable(item);
+}
+
+bool CVideoBrowse::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ int target = WINDOW_INVALID;
+ if (URIUtils::IsPVRRadioRecordingFileOrFolder(item->GetPath()))
+ target = WINDOW_RADIO_RECORDINGS;
+ else if (URIUtils::IsPVRTVRecordingFileOrFolder(item->GetPath()))
+ target = WINDOW_TV_RECORDINGS;
+ else
+ target = WINDOW_VIDEO_NAV;
+
+ auto& windowMgr = CServiceBroker::GetGUI()->GetWindowManager();
+
+ if (target == windowMgr.GetActiveWindow())
+ {
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, target, 0, GUI_MSG_UPDATE);
+ msg.SetStringParam(item->GetPath());
+ windowMgr.SendMessage(msg);
+ }
+ else
+ {
+ windowMgr.ActivateWindow(target, {item->GetPath(), "return"});
+ }
+ return true;
+}
+
+std::string CVideoResume::GetLabel(const CFileItem& item) const
+{
+ return CGUIWindowVideoBase::GetResumeString(item.GetItemToPlay());
+}
+
+bool CVideoResume::IsVisible(const CFileItem& itemIn) const
+{
+ CFileItem item(itemIn.GetItemToPlay());
+ if (item.IsDeleted()) // e.g. trashed pvr recording
+ return false;
+
+ return VIDEO_UTILS::GetItemResumeInformation(item).isResumable;
+}
+
+namespace
+{
+
+void AddRecordingsToPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems)
+{
+ if (item->m_bIsFolder)
+ {
+ CFileItemList items;
+ XFILE::CDirectory::GetDirectory(item->GetPath(), items, "", XFILE::DIR_FLAG_DEFAULTS);
+
+ const int watchedMode = CMediaSettings::GetInstance().GetWatchedMode("recordings");
+ const bool unwatchedOnly = watchedMode == WatchedModeUnwatched;
+ const bool watchedOnly = watchedMode == WatchedModeWatched;
+ for (const auto& currItem : items)
+ {
+ if (currItem->HasVideoInfoTag() &&
+ ((unwatchedOnly && currItem->GetVideoInfoTag()->GetPlayCount() > 0) ||
+ (watchedOnly && currItem->GetVideoInfoTag()->GetPlayCount() <= 0)))
+ continue;
+
+ AddRecordingsToPlayList(currItem, queuedItems);
+ }
+ }
+ else
+ {
+ queuedItems.Add(item);
+ }
+}
+
+void AddRecordingsToPlayListAndSort(const std::shared_ptr<CFileItem>& item,
+ CFileItemList& queuedItems)
+{
+ queuedItems.SetPath(item->GetPath());
+ AddRecordingsToPlayList(item, queuedItems);
+
+ if (!queuedItems.IsEmpty())
+ {
+ const int windowId = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ if (windowId == WINDOW_TV_RECORDINGS || windowId == WINDOW_RADIO_RECORDINGS)
+ {
+ std::unique_ptr<CGUIViewState> viewState(CGUIViewState::GetViewState(windowId, queuedItems));
+ if (viewState)
+ queuedItems.Sort(viewState->GetSortMethod());
+ }
+ }
+}
+
+void PlayAndQueueRecordings(const std::shared_ptr<CFileItem>& item, int windowId)
+{
+ const std::shared_ptr<CFileItem> parentFolderItem =
+ std::make_shared<CFileItem>(URIUtils::GetParentPath(item->GetPath()), true);
+
+ // add all items of given item's directory to a temporary playlist, start playback of given item
+ CFileItemList queuedItems;
+ AddRecordingsToPlayListAndSort(parentFolderItem, queuedItems);
+
+ PLAYLIST::CPlayListPlayer& player = CServiceBroker::GetPlaylistPlayer();
+
+ player.ClearPlaylist(PLAYLIST::TYPE_VIDEO);
+ player.Reset();
+ player.Add(PLAYLIST::TYPE_VIDEO, queuedItems);
+
+ // figure out where to start playback
+ PLAYLIST::CPlayList& playList = player.GetPlaylist(PLAYLIST::TYPE_VIDEO);
+ int itemToPlay = 0;
+
+ for (int i = 0; i < queuedItems.Size(); ++i)
+ {
+ if (item->IsSamePath(queuedItems.Get(i).get()))
+ {
+ itemToPlay = i;
+ break;
+ }
+ }
+
+ if (player.IsShuffled(PLAYLIST::TYPE_VIDEO))
+ {
+ playList.Swap(0, playList.FindOrder(itemToPlay));
+ itemToPlay = 0;
+ }
+
+ player.SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ player.Play(itemToPlay, "");
+}
+
+void SetPathAndPlay(CFileItem& item)
+{
+ if (!item.m_bIsFolder && item.IsVideoDb())
+ {
+ item.SetProperty("original_listitem_url", item.GetPath());
+ item.SetPath(item.GetVideoInfoTag()->m_strFileNameAndPath);
+ }
+ item.SetProperty("check_resume", false);
+
+ if (item.IsLiveTV()) // pvr tv or pvr radio?
+ {
+ g_application.PlayMedia(item, "", PLAYLIST::TYPE_VIDEO);
+ }
+ else
+ {
+ item.SetProperty("playlist_type_hint", PLAYLIST::TYPE_VIDEO);
+ VIDEO_UTILS::PlayItem(std::make_shared<CFileItem>(item));
+ }
+}
+
+} // unnamed namespace
+
+bool CVideoResume::Execute(const std::shared_ptr<CFileItem>& itemIn) const
+{
+ CFileItem item(itemIn->GetItemToPlay());
+#ifdef HAS_DVD_DRIVE
+ if (item.IsDVD() || item.IsCDDA())
+ return MEDIA_DETECT::CAutorun::PlayDisc(item.GetPath(), true, false);
+#endif
+
+ item.SetStartOffset(STARTOFFSET_RESUME);
+ SetPathAndPlay(item);
+ return true;
+};
+
+std::string CVideoPlay::GetLabel(const CFileItem& itemIn) const
+{
+ CFileItem item(itemIn.GetItemToPlay());
+ if (item.IsLiveTV())
+ return g_localizeStrings.Get(19000); // Switch to channel
+ if (VIDEO_UTILS::GetItemResumeInformation(item).isResumable)
+ return g_localizeStrings.Get(12021); // Play from beginning
+ return g_localizeStrings.Get(208); // Play
+}
+
+bool CVideoPlay::IsVisible(const CFileItem& item) const
+{
+ return VIDEO_UTILS::IsItemPlayable(item);
+}
+
+bool CVideoPlay::Execute(const std::shared_ptr<CFileItem>& itemIn) const
+{
+ CFileItem item(itemIn->GetItemToPlay());
+#ifdef HAS_DVD_DRIVE
+ if (item.IsDVD() || item.IsCDDA())
+ return MEDIA_DETECT::CAutorun::PlayDisc(item.GetPath(), true, true);
+#endif
+ SetPathAndPlay(item);
+ return true;
+};
+
+namespace
+{
+void SelectNextItem(int windowID)
+{
+ auto& windowMgr = CServiceBroker::GetGUI()->GetWindowManager();
+ CGUIWindow* window = windowMgr.GetWindow(windowID);
+ if (window)
+ {
+ const int viewContainerID = window->GetViewContainerID();
+ if (viewContainerID > 0)
+ {
+ CGUIMessage msg1(GUI_MSG_ITEM_SELECTED, windowID, viewContainerID);
+ windowMgr.SendMessage(msg1, windowID);
+
+ CGUIMessage msg2(GUI_MSG_ITEM_SELECT, windowID, viewContainerID, msg1.GetParam1() + 1);
+ windowMgr.SendMessage(msg2, windowID);
+ }
+ }
+}
+} // unnamed namespace
+
+bool CVideoQueue::IsVisible(const CFileItem& item) const
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_PLAYLIST)
+ return false; // Already queued
+
+ if (!item.CanQueue())
+ return false;
+
+ return VIDEO_UTILS::IsItemPlayable(item);
+}
+
+bool CVideoQueue::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ const int windowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ if (windowID == WINDOW_VIDEO_PLAYLIST)
+ return false; // Already queued
+
+ VIDEO_UTILS::QueueItem(item, VIDEO_UTILS::QueuePosition::POSITION_END);
+
+ // Set selection to next item in active window's view.
+ SelectNextItem(windowID);
+
+ return true;
+};
+
+bool CVideoPlayNext::IsVisible(const CFileItem& item) const
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_PLAYLIST)
+ return false; // Already queued
+
+ if (!item.CanQueue())
+ return false;
+
+ return VIDEO_UTILS::IsItemPlayable(item);
+}
+
+bool CVideoPlayNext::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_PLAYLIST)
+ return false; // Already queued
+
+ VIDEO_UTILS::QueueItem(item, VIDEO_UTILS::QueuePosition::POSITION_BEGIN);
+ return true;
+};
+
+bool CVideoPlayAndQueue::IsVisible(const CFileItem& item) const
+{
+ const int windowId = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ if (windowId == WINDOW_VIDEO_PLAYLIST)
+ return false; // Already queued
+
+ if ((windowId == WINDOW_TV_RECORDINGS || windowId == WINDOW_RADIO_RECORDINGS) &&
+ item.IsUsablePVRRecording())
+ return true;
+
+ return false; //! @todo implement
+}
+
+bool CVideoPlayAndQueue::Execute(const std::shared_ptr<CFileItem>& item) const
+{
+ const int windowId = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+ if (windowId == WINDOW_VIDEO_PLAYLIST)
+ return false; // Already queued
+
+ if ((windowId == WINDOW_TV_RECORDINGS || windowId == WINDOW_RADIO_RECORDINGS) &&
+ item->IsUsablePVRRecording())
+ {
+ // recursively add items located in the same folder as item to play list, starting with item
+ PlayAndQueueRecordings(item, windowId);
+ return true;
+ }
+
+ return true; //! @todo implement
+};
+
+}
diff --git a/xbmc/video/ContextMenus.h b/xbmc/video/ContextMenus.h
new file mode 100644
index 0000000..1cbffe4
--- /dev/null
+++ b/xbmc/video/ContextMenus.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016-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 "ContextMenuItem.h"
+#include "VideoLibraryQueue.h"
+#include "media/MediaType.h"
+
+#include <memory>
+
+namespace CONTEXTMENU
+{
+
+class CVideoInfo : public CStaticContextMenuAction
+{
+public:
+ explicit CVideoInfo(MediaType mediaType);
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+
+private:
+ const MediaType m_mediaType;
+};
+
+struct CTVShowInfo : CVideoInfo
+{
+ CTVShowInfo() : CVideoInfo(MediaTypeTvShow) {}
+};
+
+struct CEpisodeInfo : CVideoInfo
+{
+ CEpisodeInfo() : CVideoInfo(MediaTypeEpisode) {}
+};
+
+struct CMusicVideoInfo : CVideoInfo
+{
+ CMusicVideoInfo() : CVideoInfo(MediaTypeMusicVideo) {}
+};
+
+struct CMovieInfo : CVideoInfo
+{
+ CMovieInfo() : CVideoInfo(MediaTypeMovie) {}
+};
+
+struct CVideoRemoveResumePoint : CStaticContextMenuAction
+{
+ CVideoRemoveResumePoint() : CStaticContextMenuAction(38209) {}
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CVideoMarkWatched : CStaticContextMenuAction
+{
+ CVideoMarkWatched() : CStaticContextMenuAction(16103) {}
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CVideoMarkUnWatched : CStaticContextMenuAction
+{
+ CVideoMarkUnWatched() : CStaticContextMenuAction(16104) {}
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CVideoBrowse : CStaticContextMenuAction
+{
+ CVideoBrowse() : CStaticContextMenuAction(37015) {} // Browse into
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CVideoResume : IContextMenuItem
+{
+ std::string GetLabel(const CFileItem& item) const override;
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& _item) const override;
+};
+
+struct CVideoPlay : IContextMenuItem
+{
+ std::string GetLabel(const CFileItem& item) const override;
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& _item) const override;
+};
+
+struct CVideoQueue : CStaticContextMenuAction
+{
+ CVideoQueue() : CStaticContextMenuAction(13347) {} // Queue item
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CVideoPlayNext : CStaticContextMenuAction
+{
+ CVideoPlayNext() : CStaticContextMenuAction(10008) {} // Play next
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+struct CVideoPlayAndQueue : CStaticContextMenuAction
+{
+ CVideoPlayAndQueue() : CStaticContextMenuAction(13412) {} // Play from here
+ bool IsVisible(const CFileItem& item) const override;
+ bool Execute(const std::shared_ptr<CFileItem>& item) const override;
+};
+
+}
diff --git a/xbmc/video/Episode.h b/xbmc/video/Episode.h
new file mode 100644
index 0000000..833f9e3
--- /dev/null
+++ b/xbmc/video/Episode.h
@@ -0,0 +1,51 @@
+/*
+ * 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 "XBDateTime.h"
+#include "utils/ScraperUrl.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+class CFileItem;
+
+// single episode information
+namespace VIDEO
+{
+ struct EPISODE
+ {
+ bool isFolder;
+ int iSeason;
+ int iEpisode;
+ int iSubepisode;
+ std::string strPath;
+ std::string strTitle;
+ CDateTime cDate;
+ CScraperUrl cScraperUrl;
+ std::shared_ptr<CFileItem> item;
+ EPISODE(int Season = -1, int Episode = -1, int Subepisode = 0, bool Folder = false)
+ {
+ iSeason = Season;
+ iEpisode = Episode;
+ iSubepisode = Subepisode;
+ isFolder = Folder;
+ }
+ bool operator==(const struct EPISODE& rhs) const
+ {
+ return (iSeason == rhs.iSeason &&
+ iEpisode == rhs.iEpisode &&
+ iSubepisode == rhs.iSubepisode);
+ }
+ };
+
+ typedef std::vector<EPISODE> EPISODELIST;
+}
+
diff --git a/xbmc/video/GUIViewStateVideo.cpp b/xbmc/video/GUIViewStateVideo.cpp
new file mode 100644
index 0000000..3065272
--- /dev/null
+++ b/xbmc/video/GUIViewStateVideo.cpp
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2016-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 "GUIViewStateVideo.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "VideoDatabase.h"
+#include "filesystem/Directory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "guilib/WindowIDs.h"
+#include "playlists/PlayListTypes.h"
+#include "settings/MediaSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/SortUtils.h"
+#include "view/ViewStateSettings.h"
+
+using namespace XFILE;
+using namespace VIDEODATABASEDIRECTORY;
+
+std::string CGUIViewStateWindowVideo::GetLockType()
+{
+ return "video";
+}
+
+std::string CGUIViewStateWindowVideo::GetExtensions()
+{
+ return CServiceBroker::GetFileExtensionProvider().GetVideoExtensions();
+}
+
+PLAYLIST::Id CGUIViewStateWindowVideo::GetPlaylist() const
+{
+ return PLAYLIST::TYPE_VIDEO;
+}
+
+VECSOURCES& CGUIViewStateWindowVideo::GetSources()
+{
+ AddLiveTVSources();
+ return CGUIViewState::GetSources();
+}
+
+bool CGUIViewStateWindowVideo::AutoPlayNextItem()
+{
+ return AutoPlayNextVideoItem();
+}
+
+/***************************/
+
+CGUIViewStateWindowVideoNav::CGUIViewStateWindowVideoNav(const CFileItemList& items) : CGUIViewStateWindowVideo(items)
+{
+ SortAttribute sortAttributes = SortAttributeNone;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttributes = SortAttributeIgnoreArticle;
+
+ if (items.IsVirtualDirectoryRoot())
+ {
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Label, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+ }
+ else if (items.IsVideoDb())
+ {
+ NODE_TYPE NodeType=CVideoDatabaseDirectory::GetDirectoryChildType(items.GetPath());
+ CQueryParams params;
+ CVideoDatabaseDirectory::GetQueryParams(items.GetPath(),params);
+
+ switch (NodeType)
+ {
+ case NODE_TYPE_MOVIES_OVERVIEW:
+ case NODE_TYPE_TVSHOWS_OVERVIEW:
+ case NODE_TYPE_MUSICVIDEOS_OVERVIEW:
+ case NODE_TYPE_OVERVIEW:
+ {
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Label, empty
+
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_DIRECTOR:
+ case NODE_TYPE_ACTOR:
+ {
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS("%T", "%R", "%L", "")); // Title, Rating | Label, empty
+ AddSortMethod(SortByRelevance, 38026, LABEL_MASKS("%T", "%c", "%L", "%c")); // Title, Actor's appearances (Relevance) | Label, Actor's appearances (Relevance)
+ SetSortMethod(SortByLabel);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavactors");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_YEAR:
+ {
+ AddSortMethod(SortByLabel, 562, LABEL_MASKS("%T", "%R", "%L", "")); // Title, Rating | Label, empty
+ SetSortMethod(SortByLabel);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavyears");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_SEASONS:
+ {
+ AddSortMethod(SortBySortTitle, 556, LABEL_MASKS("%L", "","%L","")); // Label, empty | Label, empty
+ SetSortMethod(SortBySortTitle);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavseasons");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_TITLE_TVSHOWS:
+ case NODE_TYPE_INPROGRESS_TVSHOWS:
+ {
+ AddSortMethod(SortBySortTitle, sortAttributes, 556, LABEL_MASKS("%T", "%M", "%T", "%M")); // Title, #Episodes | Title, #Episodes
+ AddSortMethod(SortByOriginalTitle, sortAttributes, 20376,
+ LABEL_MASKS("%T", "%M", "%T", "%M")); // Title, #Episodes | Title, #Episodes
+
+ AddSortMethod(SortByNumberOfEpisodes, sortAttributes, 20360,
+ LABEL_MASKS("%L", "%M", "%L", "%M")); // Label, #Episodes | Label, #Episodes
+ AddSortMethod(
+ SortByLastPlayed, sortAttributes, 568,
+ LABEL_MASKS("%T", "%p", "%T", "%p")); // Title, #Last played | Title, #Last played
+ AddSortMethod(SortByDateAdded, sortAttributes, 570,
+ LABEL_MASKS("%T", "%a", "%T", "%a")); // Title, DateAdded | Title, DateAdded
+ AddSortMethod(SortByYear, sortAttributes, 562,
+ LABEL_MASKS("%L", "%Y", "%L", "%Y")); // Label, Year | Label, Year
+ AddSortMethod(SortByRating, sortAttributes, 563,
+ LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating
+ AddSortMethod(SortByUserRating, sortAttributes, 38018,
+ LABEL_MASKS("%T", "%r", "%T", "%r")); // Title, Userrating | Title, Userrating
+ SetSortMethod(SortByLabel);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavtvshows");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_MUSICVIDEOS_ALBUM:
+ case NODE_TYPE_GENRE:
+ case NODE_TYPE_COUNTRY:
+ case NODE_TYPE_STUDIO:
+ {
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS("%T", "%R", "%L", "")); // Title, Rating | Label, empty
+ SetSortMethod(SortByLabel);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavgenres");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_SETS:
+ {
+ AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%T","%R", "%T","%R")); // Title, Rating | Title, Rating
+
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y", "%T", "%Y")); // Title, Year | Title, Year
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating
+ AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T", "%a", "%T", "%a")); // Title, DateAdded | Title, DateAdded
+ AddSortMethod(SortByPlaycount, 567,
+ LABEL_MASKS("%T", "%V", "%T", "%V")); // Title, Playcount | Title, Playcount
+
+ SetSortMethod(SortByLabel);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavgenres");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_TAGS:
+ {
+ AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%T","", "%T","")); // Title, empty | Title, empty
+ SetSortMethod(SortByLabel);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavgenres");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_EPISODES:
+ {
+ if (params.GetSeason() > -1)
+ {
+ AddSortMethod(SortByEpisodeNumber, 20359, LABEL_MASKS("%E. %T","%R")); // Episode. Title, Rating | empty, empty
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%E. %T", "%R")); // Episode. Title, Rating | empty, empty
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%E. %T", "%r")); // Episode. Title, Userrating | empty, empty
+ AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%E. %T", "%O")); // Episode. Title, MPAA | empty, empty
+ AddSortMethod(SortByProductionCode, 20368, LABEL_MASKS("%E. %T","%P", "%E. %T","%P")); // Episode. Title, ProductionCode | Episode. Title, ProductionCode
+ AddSortMethod(SortByDate, 552, LABEL_MASKS("%E. %T","%J","%E. %T","%J")); // Episode. Title, Date | Episode. Title, Date
+ AddSortMethod(SortByPlaycount, 567,
+ LABEL_MASKS("%E. %T", "%V")); // Episode. Title, Playcount | empty, empty
+ }
+ else
+ {
+ AddSortMethod(SortByEpisodeNumber, 20359, LABEL_MASKS("%H. %T","%R")); // Order. Title, Rating | empty, empty
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%H. %T", "%R")); // Order. Title, Rating | empty, empty
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%H. %T", "%r")); // Order. Title, Userrating | empty, empty
+ AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%H. %T", "%O")); // Order. Title, MPAA | empty, empty
+ AddSortMethod(SortByProductionCode, 20368, LABEL_MASKS("%H. %T","%P", "%H. %T","%P")); // Order. Title, ProductionCode | Episode. Title, ProductionCode
+ AddSortMethod(SortByDate, 552, LABEL_MASKS("%H. %T","%J","%H. %T","%J")); // Order. Title, Date | Episode. Title, Date
+ AddSortMethod(SortByPlaycount, 567,
+ LABEL_MASKS("%H. %T", "%V")); // Order. Title, Playcount | empty, empty
+ }
+ AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%T","%R")); // Title, Rating | empty, empty
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavepisodes");
+ SetSortMethod(viewState->m_sortDescription);
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ break;
+ }
+ case NODE_TYPE_RECENTLY_ADDED_EPISODES:
+ {
+ AddSortMethod(SortByNone, 552, LABEL_MASKS("%Z - %H. %T", "%R")); // TvShow - Order. Title, Rating | empty, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("videonavepisodes")->m_viewMode);
+ SetSortOrder(SortOrderNone);
+
+ break;
+ }
+ case NODE_TYPE_TITLE_MOVIES:
+ {
+ if (params.GetSetId() > -1) // Is this a listing within a set?
+ {
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y")); // Title, Year | empty, empty
+ AddSortMethod(SortBySortTitle, sortAttributes, 556, LABEL_MASKS("%T", "%R")); // Title, Rating | empty, empty
+ AddSortMethod(SortByOriginalTitle, sortAttributes, 20376,
+ LABEL_MASKS("%T", "%R")); // Title, Rating | empty, empty
+ }
+ else
+ {
+ AddSortMethod(SortBySortTitle, sortAttributes, 556, LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating
+ AddSortMethod(SortByOriginalTitle, sortAttributes, 20376,
+ LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y", "%T", "%Y")); // Title, Year | Title, Year
+ }
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T", "%r", "%T", "%r")); // Title, Userrating | Title, Userrating
+ AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%T", "%O")); // Title, MPAA | empty, empty
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T", "%D")); // Title, Duration | empty, empty
+ AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T", "%a", "%T", "%a")); // Title, DateAdded | Title, DateAdded
+ AddSortMethod(SortByLastPlayed, 568, LABEL_MASKS("%T", "%p", "%T", "%p")); // Title, #Last played | Title, #Last played
+ AddSortMethod(SortByPlaycount, 567,
+ LABEL_MASKS("%T", "%V", "%T", "%V")); // Title, Playcount | Title, Playcount
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavtitles");
+ if (params.GetSetId() > -1)
+ {
+ SetSortMethod(SortByYear);
+ SetSortOrder(SortOrderAscending);
+ }
+ else
+ {
+ SetSortMethod(viewState->m_sortDescription);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+
+ SetViewAsControl(viewState->m_viewMode);
+ }
+ break;
+ case NODE_TYPE_TITLE_MUSICVIDEOS:
+ {
+ AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year | empty, empty
+ AddSortMethod(SortByAlbum, sortAttributes, 558, LABEL_MASKS("%B - %T - %A", "%Y")); // Album, Title, Artist, Year | empty, empty
+ AddSortMethod(SortByArtist, sortAttributes, 557, LABEL_MASKS("%A - %T", "%Y")); // Artist - Title, Year | empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttributes, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year| empty, empty
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount
+ AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%T - %A", "%O")); // Title - Artist, MPAARating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating
+
+ std::string strTrack=CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%N")); // Userdefined, Track Number | empty, empty
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavmusicvideos");
+ SetSortMethod(viewState->m_sortDescription);
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ break;
+ case NODE_TYPE_RECENTLY_ADDED_MOVIES:
+ {
+ AddSortMethod(SortByNone, 552, LABEL_MASKS("%T", "%R")); // Title, Rating | empty, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("videonavtitles")->m_viewMode);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ case NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS:
+ {
+ AddSortMethod(SortByNone, 552, LABEL_MASKS("%A - %T", "%Y")); // Artist - Title, Year | empty, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(CViewStateSettings::GetInstance().Get("videonavmusicvideos")->m_viewMode);
+
+ SetSortOrder(SortOrderNone);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ else
+ {
+ AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%L", "%I", "%L", "")); // Label, Size | Label, empty
+ AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I")); // Label, Size | Label, Size
+ AddSortMethod(SortByDate, 552, LABEL_MASKS("%L", "%J", "%L", "%J")); // Label, Date | Label, Date
+ AddSortMethod(SortByFile, 561, LABEL_MASKS("%L", "%I", "%L", "")); // Label, Size | Label, empty
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videofiles");
+ SetSortMethod(viewState->m_sortDescription);
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+ LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV);
+}
+
+void CGUIViewStateWindowVideoNav::SaveViewState()
+{
+ if (m_items.IsVideoDb())
+ {
+ NODE_TYPE NodeType = CVideoDatabaseDirectory::GetDirectoryChildType(m_items.GetPath());
+ CQueryParams params;
+ CVideoDatabaseDirectory::GetQueryParams(m_items.GetPath(),params);
+ switch (NodeType)
+ {
+ case NODE_TYPE_ACTOR:
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavactors"));
+ break;
+ case NODE_TYPE_YEAR:
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavyears"));
+ break;
+ case NODE_TYPE_GENRE:
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavgenres"));
+ break;
+ case NODE_TYPE_TITLE_MOVIES:
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, params.GetSetId() > -1 ? NULL : CViewStateSettings::GetInstance().Get("videonavtitles"));
+ break;
+ case NODE_TYPE_EPISODES:
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavepisodes"));
+ break;
+ case NODE_TYPE_TITLE_TVSHOWS:
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavtvshows"));
+ break;
+ case NODE_TYPE_SEASONS:
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavseasons"));
+ break;
+ case NODE_TYPE_TITLE_MUSICVIDEOS:
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavmusicvideos"));
+ break;
+ default:
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV);
+ break;
+ }
+ }
+ else
+ {
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videofiles"));
+ }
+}
+
+VECSOURCES& CGUIViewStateWindowVideoNav::GetSources()
+{
+ // Setup shares we want to have
+ m_sources.clear();
+ CFileItemList items;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN))
+ CDirectory::GetDirectory("library://video_flat/", items, "", DIR_FLAG_DEFAULTS);
+ else
+ CDirectory::GetDirectory("library://video/", items, "", DIR_FLAG_DEFAULTS);
+ for (int i=0; i<items.Size(); ++i)
+ {
+ CFileItemPtr item=items[i];
+ CMediaSource share;
+ share.strName=item->GetLabel();
+ share.strPath = item->GetPath();
+ share.m_strThumbnailImage = item->GetArt("icon");
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ m_sources.push_back(share);
+ }
+ return CGUIViewStateWindowVideo::GetSources();
+}
+
+bool CGUIViewStateWindowVideoNav::AutoPlayNextItem()
+{
+ CQueryParams params;
+ CVideoDatabaseDirectory::GetQueryParams(m_items.GetPath(),params);
+ if (static_cast<VideoDbContentType>(params.GetContentType()) == VideoDbContentType::MUSICVIDEOS)
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MUSICPLAYER_AUTOPLAYNEXTITEM);
+
+ return CGUIViewStateWindowVideo::AutoPlayNextItem();
+}
+
+CGUIViewStateWindowVideoPlaylist::CGUIViewStateWindowVideoPlaylist(const CFileItemList& items) : CGUIViewStateWindowVideo(items)
+{
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%L", "", "%L", "")); // Label, empty | Label, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ SetSortOrder(SortOrderNone);
+
+ LoadViewState(items.GetPath(), WINDOW_VIDEO_PLAYLIST);
+}
+
+void CGUIViewStateWindowVideoPlaylist::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_PLAYLIST);
+}
+
+bool CGUIViewStateWindowVideoPlaylist::HideExtensions()
+{
+ return true;
+}
+
+bool CGUIViewStateWindowVideoPlaylist::HideParentDirItems()
+{
+ return true;
+}
+
+VECSOURCES& CGUIViewStateWindowVideoPlaylist::GetSources()
+{
+ m_sources.clear();
+ // Playlist share
+ CMediaSource share;
+ share.strPath= "playlistvideo://";
+ share.m_iDriveType = CMediaSource::SOURCE_TYPE_LOCAL;
+ m_sources.push_back(share);
+
+ // no plugins in playlist window
+ return m_sources;
+}
+
+CGUIViewStateVideoMovies::CGUIViewStateVideoMovies(const CFileItemList& items) : CGUIViewStateWindowVideo(items)
+{
+ SortAttribute sortAttributes = SortAttributeNone;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttributes = SortAttributeIgnoreArticle;
+
+ AddSortMethod(SortBySortTitle, sortAttributes, 556,
+ LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating
+ AddSortMethod(SortByOriginalTitle, sortAttributes, 20376,
+ LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T", "%Y", "%T", "%Y")); // Title, Year | Title, Year
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T", "%r", "%T", "%r")); // Title, Userrating | Title, Userrating
+ AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%T", "%O")); // Title, MPAA | empty, empty
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T", "%D")); // Title, Duration | empty, empty
+ AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T", "%a", "%T", "%a")); // Title, DateAdded | Title, DateAdded
+ AddSortMethod(SortByPlaycount, 567,
+ LABEL_MASKS("%T", "%V", "%T", "%V")); // Title, Playcount | Title, Playcount
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavtitles");
+ if (items.IsSmartPlayList() || items.IsLibraryFolder())
+ AddPlaylistOrder(items, LABEL_MASKS("%T", "%R", "%T", "%R")); // Title, Rating | Title, Rating
+ else
+ {
+ SetSortMethod(viewState->m_sortDescription);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+
+ SetViewAsControl(viewState->m_viewMode);
+
+ LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV);
+}
+
+void CGUIViewStateVideoMovies::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavtitles"));
+}
+
+CGUIViewStateVideoMusicVideos::CGUIViewStateVideoMusicVideos(const CFileItemList& items) : CGUIViewStateWindowVideo(items)
+{
+ SortAttribute sortAttributes = SortAttributeNone;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttributes = SortAttributeIgnoreArticle;
+
+ AddSortMethod(SortByLabel, sortAttributes, 551, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year | empty, empty
+ AddSortMethod(SortByAlbum, sortAttributes, 558, LABEL_MASKS("%B - %T - %A", "%Y")); // Album, Title, Artist, Year | empty, empty
+ AddSortMethod(SortByArtist, sortAttributes, 557, LABEL_MASKS("%A - %T", "%Y")); // Artist - Title, Year | empty, empty
+ AddSortMethod(SortByArtistThenYear, sortAttributes, 578, LABEL_MASKS("%A - %T", "%Y")); // Artist, Title, Year| empty, empty
+ AddSortMethod(SortByYear, 562, LABEL_MASKS("%T - %A", "%Y")); // Title, Artist, Year| empty, empty
+ AddSortMethod(SortByTime, 180, LABEL_MASKS("%T - %A", "%D")); // Title, Artist, Duration| empty, empty
+ AddSortMethod(SortByDateAdded, 570, LABEL_MASKS("%T - %A", "%a")); // Title - Artist, DateAdded | empty, empty
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%T - %A", "%V")); // Title - Artist, PlayCount
+ AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%T - %A", "%O")); // Title - Artist, MPAARating
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%T - %A", "%r")); // Title - Artist, UserRating
+
+ std::string strTrack = settings->GetString(CSettings::SETTING_MUSICFILES_TRACKFORMAT);
+ AddSortMethod(SortByTrackNumber, 554, LABEL_MASKS(strTrack, "%N")); // Userdefined, Track Number | empty, empty
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavmusicvideos");
+ if (items.IsSmartPlayList() || items.IsLibraryFolder())
+ AddPlaylistOrder(items, LABEL_MASKS("%A - %T", "%Y")); // Artist - Title, Year | empty, empty
+ else
+ {
+ SetSortMethod(viewState->m_sortDescription);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+
+ SetViewAsControl(viewState->m_viewMode);
+
+ LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV);
+}
+
+void CGUIViewStateVideoMusicVideos::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavmusicvideos"));
+}
+
+CGUIViewStateVideoTVShows::CGUIViewStateVideoTVShows(const CFileItemList& items) : CGUIViewStateWindowVideo(items)
+{
+ SortAttribute sortAttributes = SortAttributeNone;
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttributes = SortAttributeIgnoreArticle;
+
+ AddSortMethod(SortBySortTitle, sortAttributes, 556,
+ LABEL_MASKS("%T", "%M", "%T", "%M")); // Title, #Episodes | Title, #Episodes
+ AddSortMethod(SortByNumberOfEpisodes, sortAttributes, 20360,
+ LABEL_MASKS("%L", "%M", "%L", "%M")); // Label, #Episodes | Label, #Episodes
+ AddSortMethod(SortByLastPlayed, sortAttributes, 568,
+ LABEL_MASKS("%T", "%p", "%T", "%p")); // Title, #Last played | Title, #Last played
+ AddSortMethod(SortByDateAdded, sortAttributes, 570,
+ LABEL_MASKS("%T", "%a", "%T", "%a")); // Title, DateAdded | Title, DateAdded
+ AddSortMethod(SortByYear, sortAttributes, 562,
+ LABEL_MASKS("%T", "%Y", "%T", "%Y")); // Title, Year | Title, Year
+ AddSortMethod(SortByUserRating, sortAttributes, 38018,
+ LABEL_MASKS("%T", "%r", "%T", "%r")); // Title, Userrating | Title, Userrating
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavtvshows");
+ if (items.IsSmartPlayList() || items.IsLibraryFolder())
+ AddPlaylistOrder(items, LABEL_MASKS("%T", "%M", "%T", "%M")); // Title, #Episodes | Title, #Episodes
+ else
+ {
+ SetSortMethod(viewState->m_sortDescription);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+
+ SetViewAsControl(viewState->m_viewMode);
+
+ LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV);
+}
+
+void CGUIViewStateVideoTVShows::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavtvshows"));
+}
+
+CGUIViewStateVideoEpisodes::CGUIViewStateVideoEpisodes(const CFileItemList& items) : CGUIViewStateWindowVideo(items)
+{
+ // TvShow - Order. Title, Rating | empty, empty
+ AddSortMethod(SortByEpisodeNumber, 20359, LABEL_MASKS("%Z - %H. %T","%R"));
+ // TvShow - Order. Title, Rating | empty, empty
+ AddSortMethod(SortByRating, 563, LABEL_MASKS("%Z - %H. %T", "%R"));
+ // TvShow - Order. Title, Userrating | empty, empty
+ AddSortMethod(SortByUserRating, 38018, LABEL_MASKS("%Z - %H. %T", "%r"));
+ // TvShow - Order. Title, MPAA | empty, empty
+ AddSortMethod(SortByMPAA, 20074, LABEL_MASKS("%Z - %H. %T", "%O"));
+ // TvShow - Order. Title, Production Code | empty, empty
+ AddSortMethod(SortByProductionCode, 20368, LABEL_MASKS("%Z - %H. %T","%P"));
+ // TvShow - Order. Title, Date | empty, empty
+ AddSortMethod(SortByDate, 552, LABEL_MASKS("%Z - %H. %T","%J"));
+ // TvShow - Order. Title, Playcount | empty, empty
+ AddSortMethod(SortByPlaycount, 567, LABEL_MASKS("%H. %T", "%V"));
+
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS("%Z - %H. %T","%R"), // TvShow - Order. Title, Rating | empty, empty
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+
+ const CViewState *viewState = CViewStateSettings::GetInstance().Get("videonavepisodes");
+ if (items.IsSmartPlayList() || items.IsLibraryFolder())
+ AddPlaylistOrder(items, LABEL_MASKS("%Z - %H. %T", "%R")); // TvShow - Order. Title, Rating | empty, empty
+ else
+ {
+ SetSortMethod(viewState->m_sortDescription);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+ }
+
+ SetViewAsControl(viewState->m_viewMode);
+
+ LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV);
+}
+
+void CGUIViewStateVideoEpisodes::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV, CViewStateSettings::GetInstance().Get("videonavepisodes"));
+}
+
+CGUIViewStateVideoPlaylist::CGUIViewStateVideoPlaylist(const CFileItemList& items)
+ : CGUIViewStateWindowVideo(items)
+{
+ SortAttribute sortAttributes = SortAttributeNone;
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sortAttributes = SortAttributeIgnoreArticle;
+
+ AddSortMethod(SortByPlaylistOrder, 559, LABEL_MASKS("%L", "")); // Label, empty
+ AddSortMethod(SortByLabel, sortAttributes, 551,
+ LABEL_MASKS("%L", "%I", "%L", "")); // Label, Size | Label, empty
+ AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I")); // Label, Size | Label, Size
+ AddSortMethod(SortByDate, 552, LABEL_MASKS("%L", "%J", "%L", "%J")); // Label, Date | Label, Date
+ AddSortMethod(SortByFile, 561, LABEL_MASKS("%L", "%I", "%L", "")); // Label, Size | Label, empty
+
+ SetSortMethod(SortByPlaylistOrder);
+
+ const CViewState* viewState = CViewStateSettings::GetInstance().Get("videofiles");
+ SetViewAsControl(viewState->m_viewMode);
+ SetSortOrder(viewState->m_sortDescription.sortOrder);
+
+ LoadViewState(items.GetPath(), WINDOW_VIDEO_NAV);
+}
+
+void CGUIViewStateVideoPlaylist::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), WINDOW_VIDEO_NAV);
+}
diff --git a/xbmc/video/GUIViewStateVideo.h b/xbmc/video/GUIViewStateVideo.h
new file mode 100644
index 0000000..4a7a8e6
--- /dev/null
+++ b/xbmc/video/GUIViewStateVideo.h
@@ -0,0 +1,90 @@
+/*
+ * 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 "view/GUIViewState.h"
+
+class CGUIViewStateWindowVideo : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateWindowVideo(const CFileItemList& items) : CGUIViewState(items) {}
+
+protected:
+ VECSOURCES& GetSources() override;
+ std::string GetLockType() override;
+ PLAYLIST::Id GetPlaylist() const override;
+ std::string GetExtensions() override;
+ bool AutoPlayNextItem() override;
+};
+
+class CGUIViewStateVideoPlaylist : public CGUIViewStateWindowVideo
+{
+public:
+ explicit CGUIViewStateVideoPlaylist(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateWindowVideoNav : public CGUIViewStateWindowVideo
+{
+public:
+ explicit CGUIViewStateWindowVideoNav(const CFileItemList& items);
+ bool AutoPlayNextItem() override;
+
+protected:
+ void SaveViewState() override;
+ VECSOURCES& GetSources() override;
+};
+
+class CGUIViewStateWindowVideoPlaylist : public CGUIViewStateWindowVideo
+{
+public:
+ explicit CGUIViewStateWindowVideoPlaylist(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+ bool HideExtensions() override;
+ bool HideParentDirItems() override;
+ VECSOURCES& GetSources() override;
+ bool AutoPlayNextItem() override { return false; }
+};
+
+class CGUIViewStateVideoMovies : public CGUIViewStateWindowVideo
+{
+public:
+ explicit CGUIViewStateVideoMovies(const CFileItemList& items);
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateVideoMusicVideos : public CGUIViewStateWindowVideo
+{
+public:
+ explicit CGUIViewStateVideoMusicVideos(const CFileItemList& items);
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateVideoTVShows : public CGUIViewStateWindowVideo
+{
+public:
+ explicit CGUIViewStateVideoTVShows(const CFileItemList& items);
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateVideoEpisodes : public CGUIViewStateWindowVideo
+{
+public:
+ explicit CGUIViewStateVideoEpisodes(const CFileItemList& items);
+protected:
+ void SaveViewState() override;
+};
+
diff --git a/xbmc/video/PlayerController.cpp b/xbmc/video/PlayerController.cpp
new file mode 100644
index 0000000..036fbae
--- /dev/null
+++ b/xbmc/video/PlayerController.cpp
@@ -0,0 +1,582 @@
+/*
+ * 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 "PlayerController.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/IPlayer.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogSlider.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUISliderControl.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/DisplaySettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/SubtitlesSettings.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "video/dialogs/GUIDialogAudioSettings.h"
+
+using namespace KODI;
+using namespace UTILS;
+
+CPlayerController::CPlayerController()
+{
+ MOVING_SPEED::EventCfg eventCfg{100.0f, 300.0f, 200};
+ m_movingSpeed.AddEventConfig(ACTION_SUBTITLE_VSHIFT_UP, eventCfg);
+ m_movingSpeed.AddEventConfig(ACTION_SUBTITLE_VSHIFT_DOWN, eventCfg);
+}
+
+CPlayerController::~CPlayerController() = default;
+
+CPlayerController& CPlayerController::GetInstance()
+{
+ static CPlayerController instance;
+ return instance;
+}
+
+bool CPlayerController::OnAction(const CAction &action)
+{
+ const unsigned int MsgTime = 300;
+ const unsigned int DisplTime = 2000;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ if (appPlayer->IsPlayingVideo())
+ {
+ switch (action.GetID())
+ {
+ case ACTION_SHOW_SUBTITLES:
+ {
+ if (appPlayer->GetSubtitleCount() == 0)
+ {
+ CGUIDialogKaiToast::QueueNotification(
+ CGUIDialogKaiToast::Info, g_localizeStrings.Get(287), g_localizeStrings.Get(10005),
+ DisplTime, false, MsgTime);
+ return true;
+ }
+
+ bool subsOn = !appPlayer->GetSubtitleVisible();
+ appPlayer->SetSubtitleVisible(subsOn);
+ std::string sub;
+ if (subsOn)
+ {
+ std::string lang;
+ SubtitleStreamInfo info;
+ appPlayer->GetSubtitleStreamInfo(CURRENT_STREAM, info);
+ if (!g_LangCodeExpander.Lookup(info.language, lang))
+ lang = g_localizeStrings.Get(13205); // Unknown
+
+ if (info.name.length() == 0)
+ sub = lang;
+ else
+ sub = StringUtils::Format("{} - {}", lang, info.name);
+ }
+ else
+ sub = g_localizeStrings.Get(1223);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info,
+ g_localizeStrings.Get(287), sub, DisplTime, false, MsgTime);
+ return true;
+ }
+
+ case ACTION_NEXT_SUBTITLE:
+ case ACTION_CYCLE_SUBTITLE:
+ {
+ if (appPlayer->GetSubtitleCount() == 0)
+ return true;
+
+ int currentSub = appPlayer->GetSubtitle();
+ bool currentSubVisible = true;
+
+ if (appPlayer->GetSubtitleVisible())
+ {
+ if (++currentSub >= appPlayer->GetSubtitleCount())
+ {
+ currentSub = 0;
+ if (action.GetID() == ACTION_NEXT_SUBTITLE)
+ {
+ appPlayer->SetSubtitleVisible(false);
+ currentSubVisible = false;
+ }
+ }
+ appPlayer->SetSubtitle(currentSub);
+ }
+ else if (action.GetID() == ACTION_NEXT_SUBTITLE)
+ {
+ appPlayer->SetSubtitleVisible(true);
+ }
+
+ std::string sub, lang;
+ if (currentSubVisible)
+ {
+ SubtitleStreamInfo info;
+ appPlayer->GetSubtitleStreamInfo(currentSub, info);
+ if (!g_LangCodeExpander.Lookup(info.language, lang))
+ lang = g_localizeStrings.Get(13205); // Unknown
+
+ if (info.name.length() == 0)
+ sub = lang;
+ else
+ sub = StringUtils::Format("{} - {}", lang, info.name);
+ }
+ else
+ sub = g_localizeStrings.Get(1223);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(287), sub, DisplTime, false, MsgTime);
+ return true;
+ }
+
+ case ACTION_SUBTITLE_DELAY_MIN:
+ {
+ float videoSubsDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoSubsDelayRange;
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_SubtitleDelay -= 0.1f;
+ if (vs.m_SubtitleDelay < -videoSubsDelayRange)
+ vs.m_SubtitleDelay = -videoSubsDelayRange;
+ appPlayer->SetSubTitleDelay(vs.m_SubtitleDelay);
+
+ ShowSlider(action.GetID(), 22006, appPlayer->GetVideoSettings().m_SubtitleDelay,
+ -videoSubsDelayRange, 0.1f, videoSubsDelayRange);
+ return true;
+ }
+
+ case ACTION_SUBTITLE_DELAY_PLUS:
+ {
+ float videoSubsDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoSubsDelayRange;
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_SubtitleDelay += 0.1f;
+ if (vs.m_SubtitleDelay > videoSubsDelayRange)
+ vs.m_SubtitleDelay = videoSubsDelayRange;
+ appPlayer->SetSubTitleDelay(vs.m_SubtitleDelay);
+
+ ShowSlider(action.GetID(), 22006, appPlayer->GetVideoSettings().m_SubtitleDelay,
+ -videoSubsDelayRange, 0.1f, videoSubsDelayRange);
+ return true;
+ }
+
+ case ACTION_SUBTITLE_DELAY:
+ {
+ float videoSubsDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoSubsDelayRange;
+ ShowSlider(action.GetID(), 22006, appPlayer->GetVideoSettings().m_SubtitleDelay,
+ -videoSubsDelayRange, 0.1f, videoSubsDelayRange, true);
+ return true;
+ }
+
+ case ACTION_AUDIO_DELAY:
+ {
+ float videoAudioDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAudioDelayRange;
+ ShowSlider(action.GetID(), 297, appPlayer->GetVideoSettings().m_AudioDelay,
+ -videoAudioDelayRange, AUDIO_DELAY_STEP, videoAudioDelayRange, true);
+ return true;
+ }
+
+ case ACTION_AUDIO_DELAY_MIN:
+ {
+ float videoAudioDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAudioDelayRange;
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_AudioDelay -= AUDIO_DELAY_STEP;
+ if (vs.m_AudioDelay < -videoAudioDelayRange)
+ vs.m_AudioDelay = -videoAudioDelayRange;
+ appPlayer->SetAVDelay(vs.m_AudioDelay);
+
+ ShowSlider(action.GetID(), 297, appPlayer->GetVideoSettings().m_AudioDelay,
+ -videoAudioDelayRange, AUDIO_DELAY_STEP, videoAudioDelayRange);
+ return true;
+ }
+
+ case ACTION_AUDIO_DELAY_PLUS:
+ {
+ float videoAudioDelayRange = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAudioDelayRange;
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_AudioDelay += AUDIO_DELAY_STEP;
+ if (vs.m_AudioDelay > videoAudioDelayRange)
+ vs.m_AudioDelay = videoAudioDelayRange;
+ appPlayer->SetAVDelay(vs.m_AudioDelay);
+
+ ShowSlider(action.GetID(), 297, appPlayer->GetVideoSettings().m_AudioDelay,
+ -videoAudioDelayRange, AUDIO_DELAY_STEP, videoAudioDelayRange);
+ return true;
+ }
+
+ case ACTION_AUDIO_NEXT_LANGUAGE:
+ {
+ if (appPlayer->GetAudioStreamCount() == 1)
+ return true;
+
+ int currentAudio = appPlayer->GetAudioStream();
+ int audioStreamCount = appPlayer->GetAudioStreamCount();
+
+ if (++currentAudio >= audioStreamCount)
+ currentAudio = 0;
+ appPlayer->SetAudioStream(currentAudio); // Set the audio stream to the one selected
+ std::string aud;
+ std::string lan;
+ AudioStreamInfo info;
+ appPlayer->GetAudioStreamInfo(currentAudio, info);
+ if (!g_LangCodeExpander.Lookup(info.language, lan))
+ lan = g_localizeStrings.Get(13205); // Unknown
+ if (info.name.empty())
+ aud = lan;
+ else
+ aud = StringUtils::Format("{} - {}", lan, info.name);
+ std::string caption = g_localizeStrings.Get(460);
+ caption += StringUtils::Format(" ({}/{})", currentAudio + 1, audioStreamCount);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, caption, aud, DisplTime, false, MsgTime);
+ return true;
+ }
+
+ case ACTION_VIDEO_NEXT_STREAM:
+ {
+ if (appPlayer->GetVideoStreamCount() == 1)
+ return true;
+
+ int currentVideo = appPlayer->GetVideoStream();
+ int videoStreamCount = appPlayer->GetVideoStreamCount();
+
+ if (++currentVideo >= videoStreamCount)
+ currentVideo = 0;
+ appPlayer->SetVideoStream(currentVideo);
+ VideoStreamInfo info;
+ appPlayer->GetVideoStreamInfo(currentVideo, info);
+ std::string caption = g_localizeStrings.Get(38031);
+ caption += StringUtils::Format(" ({}/{})", currentVideo + 1, videoStreamCount);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, caption, info.name, DisplTime, false, MsgTime);
+ return true;
+ }
+
+ case ACTION_ZOOM_IN:
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_CustomZoomAmount += 0.01f;
+ if (vs.m_CustomZoomAmount > 2.f)
+ vs.m_CustomZoomAmount = 2.f;
+ vs.m_ViewMode = ViewModeCustom;
+ appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio,
+ vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch);
+ ShowSlider(action.GetID(), 216, vs.m_CustomZoomAmount, 0.5f, 0.1f, 2.0f);
+ return true;
+ }
+
+ case ACTION_ZOOM_OUT:
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_CustomZoomAmount -= 0.01f;
+ if (vs.m_CustomZoomAmount < 0.5f)
+ vs.m_CustomZoomAmount = 0.5f;
+ vs.m_ViewMode = ViewModeCustom;
+ appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio,
+ vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch);
+ ShowSlider(action.GetID(), 216, vs.m_CustomZoomAmount, 0.5f, 0.1f, 2.0f);
+ return true;
+ }
+
+ case ACTION_INCREASE_PAR:
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_CustomPixelRatio += 0.01f;
+ if (vs.m_CustomPixelRatio > 2.f)
+ vs.m_CustomPixelRatio = 2.f;
+ vs.m_ViewMode = ViewModeCustom;
+ appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio,
+ vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch);
+ ShowSlider(action.GetID(), 217, vs.m_CustomPixelRatio, 0.5f, 0.1f, 2.0f);
+ return true;
+ }
+
+ case ACTION_DECREASE_PAR:
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_CustomPixelRatio -= 0.01f;
+ if (vs.m_CustomPixelRatio < 0.5f)
+ vs.m_CustomPixelRatio = 0.5f;
+ vs.m_ViewMode = ViewModeCustom;
+ appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio,
+ vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch);
+ ShowSlider(action.GetID(), 217, vs.m_CustomPixelRatio, 0.5f, 0.1f, 2.0f);
+ return true;
+ }
+
+ case ACTION_VSHIFT_UP:
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_CustomVerticalShift -= 0.01f;
+ if (vs.m_CustomVerticalShift < -2.0f)
+ vs.m_CustomVerticalShift = -2.0f;
+ vs.m_ViewMode = ViewModeCustom;
+ appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio,
+ vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch);
+ ShowSlider(action.GetID(), 225, vs.m_CustomVerticalShift, -2.0f, 0.1f, 2.0f);
+ return true;
+ }
+
+ case ACTION_VSHIFT_DOWN:
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_CustomVerticalShift += 0.01f;
+ if (vs.m_CustomVerticalShift > 2.0f)
+ vs.m_CustomVerticalShift = 2.0f;
+ vs.m_ViewMode = ViewModeCustom;
+ appPlayer->SetRenderViewMode(ViewModeCustom, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio,
+ vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch);
+ ShowSlider(action.GetID(), 225, vs.m_CustomVerticalShift, -2.0f, 0.1f, 2.0f);
+ return true;
+ }
+
+ case ACTION_SUBTITLE_VSHIFT_UP:
+ {
+ const auto settings{CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()};
+ SUBTITLES::Align subAlign{settings->GetAlignment()};
+ if (subAlign != SUBTITLES::Align::BOTTOM_OUTSIDE && subAlign != SUBTITLES::Align::MANUAL)
+ return true;
+
+ RESOLUTION_INFO resInfo = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+
+ int maxPos = resInfo.Overscan.bottom;
+ if (subAlign == SUBTITLES::Align::BOTTOM_OUTSIDE)
+ {
+ maxPos =
+ resInfo.Overscan.bottom + static_cast<int>(static_cast<float>(resInfo.iHeight) / 100 *
+ settings->GetVerticalMarginPerc());
+ }
+
+ vs.m_subtitleVerticalPosition -=
+ static_cast<int>(m_movingSpeed.GetUpdatedDistance(ACTION_SUBTITLE_VSHIFT_UP));
+ if (vs.m_subtitleVerticalPosition < resInfo.Overscan.top)
+ vs.m_subtitleVerticalPosition = resInfo.Overscan.top;
+ appPlayer->SetSubtitleVerticalPosition(vs.m_subtitleVerticalPosition,
+ action.GetText() == "save");
+
+ ShowSlider(action.GetID(), 277, static_cast<float>(vs.m_subtitleVerticalPosition),
+ static_cast<float>(resInfo.Overscan.top), 1.0f, static_cast<float>(maxPos));
+ return true;
+ }
+
+ case ACTION_SUBTITLE_VSHIFT_DOWN:
+ {
+ const auto settings{CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()};
+ SUBTITLES::Align subAlign{settings->GetAlignment()};
+ if (subAlign != SUBTITLES::Align::BOTTOM_OUTSIDE && subAlign != SUBTITLES::Align::MANUAL)
+ return true;
+
+ RESOLUTION_INFO resInfo = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+
+ int maxPos = resInfo.Overscan.bottom;
+ if (subAlign == SUBTITLES::Align::BOTTOM_OUTSIDE)
+ {
+ // In this case the position not includes the vertical margin,
+ // so to be able to move the text to the bottom of the screen
+ // we must extend the maximum position with the vertical margin.
+ // Note that the text may go also slightly off-screen, this is
+ // caused by Libass see "displacement compensation" on OverlayRenderer
+ maxPos =
+ resInfo.Overscan.bottom + static_cast<int>(static_cast<float>(resInfo.iHeight) / 100 *
+ settings->GetVerticalMarginPerc());
+ }
+
+ vs.m_subtitleVerticalPosition +=
+ static_cast<int>(m_movingSpeed.GetUpdatedDistance(ACTION_SUBTITLE_VSHIFT_DOWN));
+ if (vs.m_subtitleVerticalPosition > maxPos)
+ vs.m_subtitleVerticalPosition = maxPos;
+ appPlayer->SetSubtitleVerticalPosition(vs.m_subtitleVerticalPosition,
+ action.GetText() == "save");
+
+ ShowSlider(action.GetID(), 277, static_cast<float>(vs.m_subtitleVerticalPosition),
+ static_cast<float>(resInfo.Overscan.top), 1.0f, static_cast<float>(maxPos));
+ return true;
+ }
+
+ case ACTION_SUBTITLE_ALIGN:
+ {
+ const auto settings{CServiceBroker::GetSettingsComponent()->GetSubtitlesSettings()};
+ SUBTITLES::Align align{settings->GetAlignment()};
+
+ align = static_cast<SUBTITLES::Align>(static_cast<int>(align) + 1);
+
+ if (align != SUBTITLES::Align::MANUAL && align != SUBTITLES::Align::BOTTOM_INSIDE &&
+ align != SUBTITLES::Align::BOTTOM_OUTSIDE && align != SUBTITLES::Align::TOP_INSIDE &&
+ align != SUBTITLES::Align::TOP_OUTSIDE)
+ {
+ align = SUBTITLES::Align::MANUAL;
+ }
+
+ settings->SetAlignment(align);
+ CGUIDialogKaiToast::QueueNotification(
+ CGUIDialogKaiToast::Info, g_localizeStrings.Get(21460),
+ g_localizeStrings.Get(21461 + static_cast<int>(align)), TOAST_DISPLAY_TIME, false);
+ return true;
+ }
+
+ case ACTION_VOLAMP_UP:
+ case ACTION_VOLAMP_DOWN:
+ {
+ // Don't allow change with passthrough audio
+ if (appPlayer->IsPassthrough())
+ {
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Warning,
+ g_localizeStrings.Get(660),
+ g_localizeStrings.Get(29802),
+ TOAST_DISPLAY_TIME, false);
+ return false;
+ }
+
+ float sliderMax = VOLUME_DRC_MAXIMUM / 100.0f;
+ float sliderMin = VOLUME_DRC_MINIMUM / 100.0f;
+
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ if (action.GetID() == ACTION_VOLAMP_UP)
+ vs.m_VolumeAmplification += 1.0f;
+ else
+ vs.m_VolumeAmplification -= 1.0f;
+
+ vs.m_VolumeAmplification =
+ std::max(std::min(vs.m_VolumeAmplification, sliderMax), sliderMin);
+
+ appPlayer->SetDynamicRangeCompression((long)(vs.m_VolumeAmplification * 100));
+
+ ShowSlider(action.GetID(), 660, vs.m_VolumeAmplification, sliderMin, 1.0f, sliderMax);
+ return true;
+ }
+
+ case ACTION_VOLAMP:
+ {
+ float sliderMax = VOLUME_DRC_MAXIMUM / 100.0f;
+ float sliderMin = VOLUME_DRC_MINIMUM / 100.0f;
+ ShowSlider(action.GetID(), 660, appPlayer->GetVideoSettings().m_VolumeAmplification,
+ sliderMin, 1.0f, sliderMax, true);
+ return true;
+ }
+
+ case ACTION_PLAYER_PROGRAM_SELECT:
+ {
+ std::vector<ProgramInfo> programs;
+ appPlayer->GetPrograms(programs);
+ CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (dialog)
+ {
+ int playing = 0;
+ int idx = 0;
+ for (const auto& prog : programs)
+ {
+ dialog->Add(prog.name);
+ if (prog.playing)
+ playing = idx;
+ idx++;
+ }
+ dialog->SetHeading(CVariant{g_localizeStrings.Get(39109)});
+ dialog->SetSelected(playing);
+ dialog->Open();
+ idx = dialog->GetSelectedItem();
+ if (idx > 0)
+ appPlayer->SetProgram(programs[idx].id);
+ }
+ return true;
+ }
+
+ case ACTION_PLAYER_RESOLUTION_SELECT:
+ {
+ std::vector<CVariant> indexList = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(CSettings::SETTING_VIDEOSCREEN_WHITELIST);
+
+ CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (dialog)
+ {
+ int current = 0;
+ int idx = 0;
+ auto currentRes = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution();
+ for (const CVariant &mode : indexList)
+ {
+ auto res = CDisplaySettings::GetInstance().GetResFromString(mode.asString());
+ const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(res);
+ dialog->Add(info.strMode);
+ if (res == currentRes)
+ current = idx;
+ idx++;
+ }
+ dialog->SetHeading(CVariant{g_localizeStrings.Get(39110)});
+ dialog->SetSelected(current);
+ dialog->Open();
+ idx = dialog->GetSelectedItem();
+ if (idx >= 0)
+ {
+ auto res = CDisplaySettings::GetInstance().GetResFromString(indexList[idx].asString());
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(res, false);
+ }
+ }
+ return true;
+ }
+
+ default:
+ break;
+ }
+ }
+ return false;
+}
+
+void CPlayerController::ShowSlider(int action, int label, float value, float min, float delta, float max, bool modal)
+{
+ m_sliderAction = action;
+ if (modal)
+ CGUIDialogSlider::ShowAndGetInput(g_localizeStrings.Get(label), value, min, delta, max, this);
+ else
+ CGUIDialogSlider::Display(label, value, min, delta, max, this);
+}
+
+void CPlayerController::OnSliderChange(void *data, CGUISliderControl *slider)
+{
+ if (!slider)
+ return;
+
+ if (m_sliderAction == ACTION_ZOOM_OUT || m_sliderAction == ACTION_ZOOM_IN ||
+ m_sliderAction == ACTION_INCREASE_PAR || m_sliderAction == ACTION_DECREASE_PAR ||
+ m_sliderAction == ACTION_VSHIFT_UP || m_sliderAction == ACTION_VSHIFT_DOWN)
+ {
+ std::string strValue = StringUtils::Format("{:1.2f}", slider->GetFloatValue());
+ slider->SetTextValue(strValue);
+ }
+ else if (m_sliderAction == ACTION_SUBTITLE_VSHIFT_UP ||
+ m_sliderAction == ACTION_SUBTITLE_VSHIFT_DOWN)
+ {
+ std::string strValue = StringUtils::Format("{:.0f}px", slider->GetFloatValue());
+ slider->SetTextValue(strValue);
+ }
+ else if (m_sliderAction == ACTION_VOLAMP_UP ||
+ m_sliderAction == ACTION_VOLAMP_DOWN ||
+ m_sliderAction == ACTION_VOLAMP)
+ slider->SetTextValue(CGUIDialogAudioSettings::FormatDecibel(slider->GetFloatValue()));
+ else
+ slider->SetTextValue(
+ CGUIDialogAudioSettings::FormatDelay(slider->GetFloatValue(), AUDIO_DELAY_STEP));
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ if (appPlayer->HasPlayer())
+ {
+ if (m_sliderAction == ACTION_AUDIO_DELAY)
+ {
+ appPlayer->SetAVDelay(slider->GetFloatValue());
+ }
+ else if (m_sliderAction == ACTION_SUBTITLE_DELAY)
+ {
+ appPlayer->SetSubTitleDelay(slider->GetFloatValue());
+ }
+ else if (m_sliderAction == ACTION_VOLAMP)
+ {
+ appPlayer->SetDynamicRangeCompression((long)(slider->GetFloatValue() * 100));
+ }
+ }
+}
diff --git a/xbmc/video/PlayerController.h b/xbmc/video/PlayerController.h
new file mode 100644
index 0000000..688c46c
--- /dev/null
+++ b/xbmc/video/PlayerController.h
@@ -0,0 +1,58 @@
+/*
+ * 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 "guilib/ISliderCallback.h"
+#include "input/Key.h"
+#include "interfaces/IActionListener.h"
+#include "utils/MovingSpeed.h"
+
+/*! \brief Player controller class to handle user actions.
+
+ Handles actions that are normally suited to fullscreen playback, such as
+ altering subtitles and audio tracks, changing aspect ratio, subtitle placement,
+ and placement of the video on screen.
+ */
+class CPlayerController : public ISliderCallback, public IActionListener
+{
+public:
+ static CPlayerController& GetInstance();
+
+ /*! \brief Perform a player control action if appropriate.
+ \param action the action to perform.
+ \return true if the action is considered handled, false if it should be handled elsewhere.
+ */
+ bool OnAction(const CAction &action) override;
+
+ /*! \brief Callback from the slider dialog.
+ \sa CGUIDialogSlider
+ */
+ void OnSliderChange(void *data, CGUISliderControl *slider) override;
+
+protected:
+ CPlayerController();
+ CPlayerController(const CPlayerController&) = delete;
+ CPlayerController& operator=(CPlayerController const&) = delete;
+ ~CPlayerController() override;
+
+private:
+ /*! \brief pop up a slider dialog for a particular action
+ \param action id of the action the slider responds to
+ \param label id of the label to display
+ \param value value to set on the slider
+ \param min minimum value the slider may take
+ \param delta change value to advance the slider by with each click
+ \param max maximal value the slider may take
+ \param modal true if we should wait for the slider to finish. Defaults to false
+ */
+ void ShowSlider(int action, int label, float value, float min, float delta, float max, bool modal = false);
+
+ int m_sliderAction = 0; ///< \brief set to the action id for a slider being displayed \sa ShowSlider
+ UTILS::MOVING_SPEED::CMovingSpeed m_movingSpeed;
+};
diff --git a/xbmc/video/Teletext.cpp b/xbmc/video/Teletext.cpp
new file mode 100644
index 0000000..3cbff16
--- /dev/null
+++ b/xbmc/video/Teletext.cpp
@@ -0,0 +1,4163 @@
+/*
+ * 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.
+ */
+
+/*
+ * Most Codeparts are taken from the TuxBox Teletext plugin which is based
+ * upon videotext-0.6.19991029 and written by Thomas Loewe (LazyT),
+ * Roland Meier and DBLuelle. See http://www.tuxtxt.net/ for more information.
+ * Many thanks to the TuxBox Teletext Team for this great work.
+ */
+
+#include "Teletext.h"
+
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "filesystem/SpecialProtocol.h"
+#include "input/Key.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <harfbuzz/hb-ft.h>
+
+using namespace std::chrono_literals;
+
+static inline void SDL_memset4(uint32_t* dst, uint32_t val, size_t len)
+{
+ for (; len > 0; --len)
+ *dst++ = val;
+}
+#define SDL_memcpy4(dst, src, len) memcpy(dst, src, (len) << 2)
+
+static const char *TeletextFont = "special://xbmc/media/Fonts/teletext.ttf";
+
+/* spacing attributes */
+#define alpha_black 0x00
+#define alpha_red 0x01
+#define alpha_green 0x02
+#define alpha_yellow 0x03
+#define alpha_blue 0x04
+#define alpha_magenta 0x05
+#define alpha_cyan 0x06
+#define alpha_white 0x07
+#define flash 0x08
+#define steady 0x09
+#define end_box 0x0A
+#define start_box 0x0B
+#define normal_size 0x0C
+#define double_height 0x0D
+#define double_width 0x0E
+#define double_size 0x0F
+#define mosaic_black 0x10
+#define mosaic_red 0x11
+#define mosaic_green 0x12
+#define mosaic_yellow 0x13
+#define mosaic_blue 0x14
+#define mosaic_magenta 0x15
+#define mosaic_cyan 0x16
+#define mosaic_white 0x17
+#define conceal 0x18
+#define contiguous_mosaic 0x19
+#define separated_mosaic 0x1A
+#define esc 0x1B
+#define black_background 0x1C
+#define new_background 0x1D
+#define hold_mosaic 0x1E
+#define release_mosaic 0x1F
+
+#define RowAddress2Row(row) ((row == 40) ? 24 : (row - 40))
+
+// G2 Set as defined in ETS 300 706
+const unsigned short int G2table[5][6*16] =
+{
+ // Latin G2 Supplementary Set
+ { 0x0020, 0x00A1, 0x00A2, 0x00A3, 0x0024, 0x00A5, 0x0023, 0x00A7, 0x00A4, 0x2018, 0x201C, 0x00AB, 0x2190, 0x2191, 0x2192, 0x2193,
+ 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00D7, 0x00B5, 0x00B6, 0x00B7, 0x00F7, 0x2019, 0x201D, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF,
+ 0x0020, 0x0300, 0x0301, 0x02C6, 0x0303, 0x02C9, 0x02D8, 0x02D9, 0x00A8, 0x002E, 0x02DA, 0x00B8, 0x005F, 0x02DD, 0x02DB, 0x02C7,
+ 0x2014, 0x00B9, 0x00AE, 0x00A9, 0x2122, 0x266A, 0x20AC, 0x2030, 0x03B1, 0x0020, 0x0020, 0x0020, 0x215B, 0x215C, 0x215D, 0x215E,
+ 0x2126, 0x00C6, 0x00D0, 0x00AA, 0x0126, 0x0020, 0x0132, 0x013F, 0x0141, 0x00D8, 0x0152, 0x00BA, 0x00DE, 0x0166, 0x014A, 0x0149,
+ 0x0138, 0x00E6, 0x0111, 0x00F0, 0x0127, 0x0131, 0x0133, 0x0140, 0x0142, 0x00F8, 0x0153, 0x00DF, 0x00FE, 0x0167, 0x014B, 0x25A0},
+ // Cyrillic G2 Supplementary Set
+ { 0x0020, 0x00A1, 0x00A2, 0x00A3, 0x0024, 0x00A5, 0x0020, 0x00A7, 0x0020, 0x2018, 0x201C, 0x00AB, 0x2190, 0x2191, 0x2192, 0x2193,
+ 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00D7, 0x00B5, 0x00B6, 0x00B7, 0x00F7, 0x2019, 0x201D, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF,
+ 0x0020, 0x0300, 0x0301, 0x02C6, 0x02DC, 0x02C9, 0x02D8, 0x02D9, 0x00A8, 0x002E, 0x02DA, 0x00B8, 0x005F, 0x02DD, 0x02DB, 0x02C7,
+ 0x2014, 0x00B9, 0x00AE, 0x00A9, 0x2122, 0x266A, 0x20AC, 0x2030, 0x03B1, 0x0141, 0x0142, 0x00DF, 0x215B, 0x215C, 0x215D, 0x215E,
+ 0x0044, 0x0045, 0x0046, 0x0047, 0x0049, 0x004A, 0x004B, 0x004C, 0x004E, 0x0051, 0x0052, 0x0053, 0x0055, 0x0056, 0x0057, 0x005A,
+ 0x0064, 0x0065, 0x0066, 0x0067, 0x0069, 0x006A, 0x006B, 0x006C, 0x006E, 0x0071, 0x0072, 0x0073, 0x0075, 0x0076, 0x0077, 0x007A},
+ // Greek G2 Supplementary Set
+ { 0x0020, 0x0061, 0x0062, 0x00A3, 0x0065, 0x0068, 0x0069, 0x00A7, 0x003A, 0x2018, 0x201C, 0x006B, 0x2190, 0x2191, 0x2192, 0x2193,
+ 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00D7, 0x006D, 0x006E, 0x0070, 0x00F7, 0x2019, 0x201D, 0x0074, 0x00BC, 0x00BD, 0x00BE, 0x0078,
+ 0x0020, 0x0300, 0x0301, 0x02C6, 0x02DC, 0x02C9, 0x02D8, 0x02D9, 0x00A8, 0x002E, 0x02DA, 0x00B8, 0x005F, 0x02DD, 0x02DB, 0x02C7,
+ 0x003F, 0x00B9, 0x00AE, 0x00A9, 0x2122, 0x266A, 0x20AC, 0x2030, 0x03B1, 0x038A, 0x038E, 0x038F, 0x215B, 0x215C, 0x215D, 0x215E,
+ 0x0043, 0x0044, 0x0046, 0x0047, 0x004A, 0x004C, 0x0051, 0x0052, 0x0053, 0x0055, 0x0056, 0x0057, 0x0059, 0x005A, 0x0386, 0x0389,
+ 0x0063, 0x0064, 0x0066, 0x0067, 0x006A, 0x006C, 0x0071, 0x0072, 0x0073, 0x0075, 0x0076, 0x0077, 0x0079, 0x007A, 0x0388, 0x25A0},
+ // Arabic G2 Set
+ { 0x0020, 0x0639, 0xFEC9, 0xFE83, 0xFE85, 0xFE87, 0xFE8B, 0xFE89, 0xFB7C, 0xFB7D, 0xFB7A, 0xFB58, 0xFB59, 0xFB56, 0xFB6D, 0xFB8E,
+ 0x0660, 0x0661, 0x0662, 0x0663, 0x0664, 0x0665, 0x0666, 0x0667, 0x0668, 0x0669, 0xFECE, 0xFECD, 0xFEFC, 0xFEEC, 0xFEEA, 0xFEE9,
+ 0x00E0, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005A, 0x00EB, 0x00EA, 0x00F9, 0x00EE, 0xFECA,
+ 0x00E9, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007A, 0x00E2, 0x00F4, 0x00FB, 0x00E7, 0x25A0}
+};
+
+//const (avoid warnings :<)
+TextPageAttr_t Text_AtrTable[] =
+{
+ { TXT_ColorWhite , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_WB */
+ { TXT_ColorWhite , TXT_ColorBlack , C_G0P, 0, 0, 1 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_PassiveDefault */
+ { TXT_ColorWhite , TXT_ColorRed , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_L250 */
+ { TXT_ColorBlack , TXT_ColorGreen , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_L251 */
+ { TXT_ColorBlack , TXT_ColorYellow, C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_L252 */
+ { TXT_ColorWhite , TXT_ColorBlue , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_L253 */
+ { TXT_ColorMagenta, TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_TOPMENU0 */
+ { TXT_ColorGreen , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_TOPMENU1 */
+ { TXT_ColorYellow , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_TOPMENU2 */
+ { TXT_ColorCyan , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_TOPMENU3 */
+ { TXT_ColorMenu2 , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSG0 */
+ { TXT_ColorYellow , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSG1 */
+ { TXT_ColorMenu2 , TXT_ColorTransp, C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSG2 */
+ { TXT_ColorWhite , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSG3 */
+ { TXT_ColorMenu2 , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSGDRM0 */
+ { TXT_ColorYellow , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSGDRM1 */
+ { TXT_ColorMenu2 , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSGDRM2 */
+ { TXT_ColorWhite , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MSGDRM3 */
+ { TXT_ColorMenu1 , TXT_ColorBlue , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENUHIL0 5a Z */
+ { TXT_ColorWhite , TXT_ColorBlue , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENUHIL1 58 X */
+ { TXT_ColorMenu2 , TXT_ColorTransp, C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENUHIL2 9b õ */
+ { TXT_ColorMenu2 , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU0 ab ´ */
+ { TXT_ColorYellow , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU1 a4 § */
+ { TXT_ColorMenu2 , TXT_ColorTransp, C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU2 9b õ */
+ { TXT_ColorMenu2 , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU3 cb À */
+ { TXT_ColorCyan , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU4 c7 « */
+ { TXT_ColorWhite , TXT_ColorMenu3 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU5 c8 » */
+ { TXT_ColorWhite , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_MENU6 a8 ® */
+ { TXT_ColorYellow , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f}, /* ATR_CATCHMENU0 a4 § */
+ { TXT_ColorWhite , TXT_ColorMenu1 , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f} /* ATR_CATCHMENU1 a8 ® */
+};
+
+/* shapes */
+enum
+{
+ S_END = 0,
+ S_FHL, /* full horizontal line: y-offset */
+ S_FVL, /* full vertical line: x-offset */
+ S_BOX, /* rectangle: x-offset, y-offset, width, height */
+ S_TRA, /* trapez: x0, y0, l0, x1, y1, l1 */
+ S_BTR, /* trapez in bgcolor: x0, y0, l0, x1, y1, l1 */
+ S_INV, /* invert */
+ S_LNK, /* call other shape: shapenumber */
+ S_CHR, /* Character from freetype hibyte, lowbyte */
+ S_ADT, /* Character 2F alternating raster */
+ S_FLH, /* flip horizontal */
+ S_FLV /* flip vertical */
+};
+
+/* shape coordinates */
+enum
+{
+ S_W13 = 5, /* width*1/3 */
+ S_W12, /* width*1/2 */
+ S_W23, /* width*2/3 */
+ S_W11, /* width */
+ S_WM3, /* width-3 */
+ S_H13, /* height*1/3 */
+ S_H12, /* height*1/2 */
+ S_H23, /* height*2/3 */
+ S_H11, /* height */
+ S_NrShCoord
+};
+
+/* G3 characters */
+unsigned char aG3_20[] = { S_TRA, 0, S_H23, 1, 0, S_H11, S_W12, S_END };
+unsigned char aG3_21[] = { S_TRA, 0, S_H23, 1, 0, S_H11, S_W11, S_END };
+unsigned char aG3_22[] = { S_TRA, 0, S_H12, 1, 0, S_H11, S_W12, S_END };
+unsigned char aG3_23[] = { S_TRA, 0, S_H12, 1, 0, S_H11, S_W11, S_END };
+unsigned char aG3_24[] = { S_TRA, 0, 0, 1, 0, S_H11, S_W12, S_END };
+unsigned char aG3_25[] = { S_TRA, 0, 0, 1, 0, S_H11, S_W11, S_END };
+unsigned char aG3_26[] = { S_INV, S_LNK, 0x66, S_END };
+unsigned char aG3_27[] = { S_INV, S_LNK, 0x67, S_END };
+unsigned char aG3_28[] = { S_INV, S_LNK, 0x68, S_END };
+unsigned char aG3_29[] = { S_INV, S_LNK, 0x69, S_END };
+unsigned char aG3_2a[] = { S_INV, S_LNK, 0x6a, S_END };
+unsigned char aG3_2b[] = { S_INV, S_LNK, 0x6b, S_END };
+unsigned char aG3_2c[] = { S_INV, S_LNK, 0x6c, S_END };
+unsigned char aG3_2d[] = { S_INV, S_LNK, 0x6d, S_END };
+unsigned char aG3_2e[] = { S_BOX, 2, 0, 3, S_H11, S_END };
+unsigned char aG3_2f[] = { S_ADT };
+unsigned char aG3_30[] = { S_LNK, 0x20, S_FLH, S_END };
+unsigned char aG3_31[] = { S_LNK, 0x21, S_FLH, S_END };
+unsigned char aG3_32[] = { S_LNK, 0x22, S_FLH, S_END };
+unsigned char aG3_33[] = { S_LNK, 0x23, S_FLH, S_END };
+unsigned char aG3_34[] = { S_LNK, 0x24, S_FLH, S_END };
+unsigned char aG3_35[] = { S_LNK, 0x25, S_FLH, S_END };
+unsigned char aG3_36[] = { S_INV, S_LNK, 0x76, S_END };
+unsigned char aG3_37[] = { S_INV, S_LNK, 0x77, S_END };
+unsigned char aG3_38[] = { S_INV, S_LNK, 0x78, S_END };
+unsigned char aG3_39[] = { S_INV, S_LNK, 0x79, S_END };
+unsigned char aG3_3a[] = { S_INV, S_LNK, 0x7a, S_END };
+unsigned char aG3_3b[] = { S_INV, S_LNK, 0x7b, S_END };
+unsigned char aG3_3c[] = { S_INV, S_LNK, 0x7c, S_END };
+unsigned char aG3_3d[] = { S_INV, S_LNK, 0x7d, S_END };
+unsigned char aG3_3e[] = { S_LNK, 0x2e, S_FLH, S_END };
+unsigned char aG3_3f[] = { S_BOX, 0, 0, S_W11, S_H11, S_END };
+unsigned char aG3_40[] = { S_BOX, 0, S_H13, S_W11, S_H13, S_LNK, 0x7e, S_END };
+unsigned char aG3_41[] = { S_BOX, 0, S_H13, S_W11, S_H13, S_LNK, 0x7e, S_FLV, S_END };
+unsigned char aG3_42[] = { S_LNK, 0x50, S_BOX, S_W12, S_H13, S_W12, S_H13, S_END };
+unsigned char aG3_43[] = { S_LNK, 0x50, S_BOX, 0, S_H13, S_W12, S_H13, S_END };
+unsigned char aG3_44[] = { S_LNK, 0x48, S_FLV, S_LNK, 0x48, S_END };
+unsigned char aG3_45[] = { S_LNK, 0x44, S_FLH, S_END };
+unsigned char aG3_46[] = { S_LNK, 0x47, S_FLV, S_END };
+unsigned char aG3_47[] = { S_LNK, 0x48, S_FLH, S_LNK, 0x48, S_END };
+unsigned char aG3_48[] = { S_TRA, 0, 0, S_W23, 0, S_H23, 0, S_BTR, 0, 0, S_W13, 0, S_H13, 0, S_END };
+unsigned char aG3_49[] = { S_LNK, 0x48, S_FLH, S_END };
+unsigned char aG3_4a[] = { S_LNK, 0x48, S_FLV, S_END };
+unsigned char aG3_4b[] = { S_LNK, 0x48, S_FLH, S_FLV, S_END };
+unsigned char aG3_4c[] = { S_LNK, 0x50, S_BOX, 0, S_H13, S_W11, S_H13, S_END };
+unsigned char aG3_4d[] = { S_CHR, 0x25, 0xE6 };
+unsigned char aG3_4e[] = { S_CHR, 0x25, 0xCF };
+unsigned char aG3_4f[] = { S_CHR, 0x25, 0xCB };
+unsigned char aG3_50[] = { S_BOX, S_W12, 0, 2, S_H11, S_FLH, S_BOX, S_W12, 0, 2, S_H11,S_END };
+unsigned char aG3_51[] = { S_BOX, 0, S_H12, S_W11, 2, S_FLV, S_BOX, 0, S_H12, S_W11, 2,S_END };
+unsigned char aG3_52[] = { S_LNK, 0x55, S_FLH, S_FLV, S_END };
+unsigned char aG3_53[] = { S_LNK, 0x55, S_FLV, S_END };
+unsigned char aG3_54[] = { S_LNK, 0x55, S_FLH, S_END };
+unsigned char aG3_55[] = { S_LNK, 0x7e, S_FLV, S_BOX, 0, S_H12, S_W12, 2, S_FLV, S_BOX, 0, S_H12, S_W12, 2, S_END };
+unsigned char aG3_56[] = { S_LNK, 0x57, S_FLH, S_END};
+unsigned char aG3_57[] = { S_LNK, 0x55, S_LNK, 0x50 , S_END};
+unsigned char aG3_58[] = { S_LNK, 0x59, S_FLV, S_END};
+unsigned char aG3_59[] = { S_LNK, 0x7e, S_LNK, 0x51 , S_END};
+unsigned char aG3_5a[] = { S_LNK, 0x50, S_LNK, 0x51 , S_END};
+unsigned char aG3_5b[] = { S_CHR, 0x21, 0x92};
+unsigned char aG3_5c[] = { S_CHR, 0x21, 0x90};
+unsigned char aG3_5d[] = { S_CHR, 0x21, 0x91};
+unsigned char aG3_5e[] = { S_CHR, 0x21, 0x93};
+unsigned char aG3_5f[] = { S_CHR, 0x00, 0x20};
+unsigned char aG3_60[] = { S_INV, S_LNK, 0x20, S_END };
+unsigned char aG3_61[] = { S_INV, S_LNK, 0x21, S_END };
+unsigned char aG3_62[] = { S_INV, S_LNK, 0x22, S_END };
+unsigned char aG3_63[] = { S_INV, S_LNK, 0x23, S_END };
+unsigned char aG3_64[] = { S_INV, S_LNK, 0x24, S_END };
+unsigned char aG3_65[] = { S_INV, S_LNK, 0x25, S_END };
+unsigned char aG3_66[] = { S_LNK, 0x20, S_FLV, S_END };
+unsigned char aG3_67[] = { S_LNK, 0x21, S_FLV, S_END };
+unsigned char aG3_68[] = { S_LNK, 0x22, S_FLV, S_END };
+unsigned char aG3_69[] = { S_LNK, 0x23, S_FLV, S_END };
+unsigned char aG3_6a[] = { S_LNK, 0x24, S_FLV, S_END };
+unsigned char aG3_6b[] = { S_BOX, 0, 0, S_W11, S_H13, S_TRA, 0, S_H13, S_W11, 0, S_H23, 1, S_END };
+unsigned char aG3_6c[] = { S_TRA, 0, 0, 1, 0, S_H12, S_W12, S_FLV, S_TRA, 0, 0, 1, 0, S_H12, S_W12, S_BOX, 0, S_H12, S_W12,1, S_END };
+unsigned char aG3_6d[] = { S_TRA, 0, 0, S_W12, S_W12, S_H12, 0, S_FLH, S_TRA, 0, 0, S_W12, S_W12, S_H12, 0, S_END };
+unsigned char aG3_6e[] = { S_CHR, 0x00, 0x20};
+unsigned char aG3_6f[] = { S_CHR, 0x00, 0x20};
+unsigned char aG3_70[] = { S_INV, S_LNK, 0x30, S_END };
+unsigned char aG3_71[] = { S_INV, S_LNK, 0x31, S_END };
+unsigned char aG3_72[] = { S_INV, S_LNK, 0x32, S_END };
+unsigned char aG3_73[] = { S_INV, S_LNK, 0x33, S_END };
+unsigned char aG3_74[] = { S_INV, S_LNK, 0x34, S_END };
+unsigned char aG3_75[] = { S_INV, S_LNK, 0x35, S_END };
+unsigned char aG3_76[] = { S_LNK, 0x66, S_FLH, S_END };
+unsigned char aG3_77[] = { S_LNK, 0x67, S_FLH, S_END };
+unsigned char aG3_78[] = { S_LNK, 0x68, S_FLH, S_END };
+unsigned char aG3_79[] = { S_LNK, 0x69, S_FLH, S_END };
+unsigned char aG3_7a[] = { S_LNK, 0x6a, S_FLH, S_END };
+unsigned char aG3_7b[] = { S_LNK, 0x6b, S_FLH, S_END };
+unsigned char aG3_7c[] = { S_LNK, 0x6c, S_FLH, S_END };
+unsigned char aG3_7d[] = { S_LNK, 0x6d, S_FLV, S_END };
+unsigned char aG3_7e[] = { S_BOX, S_W12, 0, 2, S_H12, S_FLH, S_BOX, S_W12, 0, 2, S_H12, S_END };// help char, not printed directly (only by S_LNK)
+
+unsigned char *aShapes[] =
+{
+ aG3_20, aG3_21, aG3_22, aG3_23, aG3_24, aG3_25, aG3_26, aG3_27, aG3_28, aG3_29, aG3_2a, aG3_2b, aG3_2c, aG3_2d, aG3_2e, aG3_2f,
+ aG3_30, aG3_31, aG3_32, aG3_33, aG3_34, aG3_35, aG3_36, aG3_37, aG3_38, aG3_39, aG3_3a, aG3_3b, aG3_3c, aG3_3d, aG3_3e, aG3_3f,
+ aG3_40, aG3_41, aG3_42, aG3_43, aG3_44, aG3_45, aG3_46, aG3_47, aG3_48, aG3_49, aG3_4a, aG3_4b, aG3_4c, aG3_4d, aG3_4e, aG3_4f,
+ aG3_50, aG3_51, aG3_52, aG3_53, aG3_54, aG3_55, aG3_56, aG3_57, aG3_58, aG3_59, aG3_5a, aG3_5b, aG3_5c, aG3_5d, aG3_5e, aG3_5f,
+ aG3_60, aG3_61, aG3_62, aG3_63, aG3_64, aG3_65, aG3_66, aG3_67, aG3_68, aG3_69, aG3_6a, aG3_6b, aG3_6c, aG3_6d, aG3_6e, aG3_6f,
+ aG3_70, aG3_71, aG3_72, aG3_73, aG3_74, aG3_75, aG3_76, aG3_77, aG3_78, aG3_79, aG3_7a, aG3_7b, aG3_7c, aG3_7d, aG3_7e
+};
+
+// G0 Table as defined in ETS 300 706
+// cyrillic G0 Charset (0 = Serbian/Croatian, 1 = Russian/Bulgarian, 2 = Ukrainian)
+const unsigned short int G0table[6][6*16] =
+{
+ // Cyrillic G0 Set - Option 1 - Serbian/Croatian
+ { ' ', '!', '\"', '#', '$', '%', '&', '\'', '(' , ')' , '*', '+', ',', '-', '.', '/',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
+ 0x0427, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 0x0425, 0x0418, 0x0408, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E,
+ 0x041F, 0x040C, 0x0420, 0x0421, 0x0422, 0x0423, 0x0412, 0x0403, 0x0409, 0x040A, 0x0417, 0x040B, 0x0416, 0x0402, 0x0428, 0x040F,
+ 0x0447, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 0x0445, 0x0438, 0x0458, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E,
+ 0x043F, 0x045C, 0x0440, 0x0441, 0x0442, 0x0443, 0x0432, 0x0453, 0x0459, 0x045A, 0x0437, 0x045B, 0x0436, 0x0452, 0x0448, 0x25A0},
+ // Cyrillic G0 Set - Option 2 - Russian/Bulgarian
+ { ' ', '!', '\"', '#', '$', '%', 0x044B, '\'', '(' , ')' , '*', '+', ',', '-', '.', '/',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
+ 0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E,
+ 0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412, 0x042C, 0x042A, 0x0417, 0x0428, 0x042D, 0x0429, 0x0427, 0x042B,
+ 0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E,
+ 0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432, 0x044C, 0x044A, 0x0437, 0x0448, 0x044D, 0x0449, 0x0447, 0x25A0},
+ // Cyrillic G0 Set - Option 3 - Ukrainian
+ { ' ', '!', '\"', '#', '$', '%', 0x0457, '\'', '(' , ')' , '*', '+', ',', '-', '.', '/',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
+ 0x042E, 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 0x0425, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E,
+ 0x041F, 0x042F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412, 0x042C, 0x0406, 0x0417, 0x0428, 0x0404, 0x0429, 0x0427, 0x0407,
+ 0x044E, 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 0x0445, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E,
+ 0x043F, 0x044F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432, 0x044C, 0x0456, 0x0437, 0x0448, 0x0454, 0x0449, 0x0447, 0x25A0},
+ // Greek G0 Set
+ { ' ', '!', '\"', '#', '$', '%', '&', '\'', '(' , ')' , '*', '+', ',', '-', '.', '/',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', 0x00AB, '=', 0x00BB, '?',
+ 0x0390, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F,
+ 0x03A0, 0x03A1, 0x0384, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x03AA, 0x03AB, 0x03AC, 0x03AD, 0x03AE, 0x03AF,
+ 0x03B0, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF,
+ 0x03C0, 0x03C1, 0x03C2, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x03CA, 0x03CB, 0x03CC, 0x03CD, 0x03CE, 0x25A0},
+ // Hebrew G0 Set
+ { ' ', '!', 0x05F2, 0x00A3, '$', '%', '&', '\'', '(' , ')' , '*', '+', ',', '-', '.', '/',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?',
+ '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 0x2190, 0x00BD, 0x2192, 0x2191, '#',
+ 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6, 0x05D7, 0x05D8, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF,
+ 0x05E0, 0x05E1, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7, 0x05E8, 0x05E9, 0x05EA, 0x20AA, 0x2551, 0x00BE, 0x00F7, 0x25A0},
+ // Arabic G0 Set - Thanks to Habib2006(fannansat)
+ { ' ', '!', 0x05F2, 0x00A3, '$', 0x066A, 0xFEF0, 0xFEF2, 0xFD3F, 0xFD3E, '*', '+', ',', '-', '.', '/',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', 0x061B, '>', '=', '<', 0x061F,
+ 0xFE94, 0x0621, 0xFE92, 0x0628, 0xFE98, 0x062A, 0xFE8E, 0xFE8D, 0xFE91, 0xFE93, 0xFE97, 0xFE9B, 0xFE9F, 0xFEA3, 0xFEA7, 0xFEA9,
+ 0x0630, 0xFEAD, 0xFEAF, 0xFEB3, 0xFEB7, 0xFEBB, 0xFEBF, 0xFEC1, 0xFEC5, 0xFECB, 0xFECF, 0xFE9C, 0xFEA0, 0xFEA4, 0xFEA8, 0x0023,
+ 0x0640, 0xFED3, 0xFED7, 0xFEDB, 0xFEDF, 0xFEE3, 0xFEE7, 0xFEEB, 0xFEED, 0xFEEF, 0xFEF3, 0xFE99, 0xFE9D, 0xFEA1, 0xFEA5, 0xFEF4,
+ 0xFEF0, 0xFECC, 0xFED0, 0xFED4, 0xFED1, 0xFED8, 0xFED5, 0xFED9, 0xFEE0, 0xFEDD, 0xFEE4, 0xFEE1, 0xFEE8, 0xFEE5, 0xFEFB, 0x25A0}
+};
+
+const unsigned short int nationaltable23[14][2] =
+{
+ { '#', 0x00A4 }, /* 0 */
+ { '#', 0x016F }, /* 1 CS/SK */
+ { 0x00A3, '$' }, /* 2 EN */
+ { '#', 0x00F5 }, /* 3 ET */
+ { 0x00E9, 0x0457 }, /* 4 FR */
+ { '#', '$' }, /* 5 DE */
+ { 0x00A3, '$' }, /* 6 IT */
+ { '#', '$' }, /* 7 LV/LT */
+ { '#', 0x0144 }, /* 8 PL */
+ { 0x00E7, '$' }, /* 9 PT/ES */
+ { '#', 0x00A4 }, /* A RO */
+ { '#', 0x00CB }, /* B SR/HR/SL */
+ { '#', 0x00A4 }, /* C SV/FI/HU */
+ { 0x20A4, 0x011F }, /* D TR */
+};
+const unsigned short int nationaltable40[14] =
+{
+ '@', /* 0 */
+ 0x010D, /* 1 CS/SK */
+ '@', /* 2 EN */
+ 0x0161, /* 3 ET */
+ 0x00E0, /* 4 FR */
+ 0x00A7, /* 5 DE */
+ 0x00E9, /* 6 IT */
+ 0x0161, /* 7 LV/LT */
+ 0x0105, /* 8 PL */
+ 0x00A1, /* 9 PT/ES */
+ 0x0162, /* A RO */
+ 0x010C, /* B SR/HR/SL */
+ 0x00C9, /* C SV/FI/HU */
+ 0x0130, /* D TR */
+};
+const unsigned short int nationaltable5b[14][6] =
+{
+ { '[', '\\', ']', '^', '_', '`' }, /* 0 */
+ { 0x0165, 0x017E, 0x00FD, 0x00ED, 0x0159, 0x00E9 }, /* 1 CS/SK */
+ { 0x2190, 0x00BD, 0x2192, 0x2191, '#', 0x00AD }, /* 2 EN */
+ { 0x00C4, 0x00D6, 0x017D, 0x00DC, 0x00D5, 0x0161 }, /* 3 ET */
+ { 0x0451, 0x00EA, 0x00F9, 0x00EE, '#', 0x00E8 }, /* 4 FR */
+ { 0x00C4, 0x00D6, 0x00DC, '^', '_', 0x00B0 }, /* 5 DE */
+ { 0x00B0, 0x00E7, 0x2192, 0x2191, '#', 0x00F9 }, /* 6 IT */
+ { 0x0117, 0x0119, 0x017D, 0x010D, 0x016B, 0x0161 }, /* 7 LV/LT */
+ { 0x017B, 0x015A, 0x0141, 0x0107, 0x00F3, 0x0119 }, /* 8 PL */
+ { 0x00E1, 0x00E9, 0x00ED, 0x00F3, 0x00FA, 0x00BF }, /* 9 PT/ES */
+ { 0x00C2, 0x015E, 0x01CD, 0x01CF, 0x0131, 0x0163 }, /* A RO */
+ { 0x0106, 0x017D, 0x00D0, 0x0160, 0x0451, 0x010D }, /* B SR/HR/SL */
+ { 0x00C4, 0x00D6, 0x00C5, 0x00DC, '_', 0x00E9 }, /* C SV/FI/HU */
+ { 0x015E, 0x00D6, 0x00C7, 0x00DC, 0x011E, 0x0131 }, /* D TR */
+};
+const unsigned short int nationaltable7b[14][4] =
+{
+ { '{', '|', '}', '~' }, /* 0 */
+ { 0x00E1, 0x011B, 0x00FA, 0x0161 }, /* 1 CS/SK */
+ { 0x00BC, 0x2551, 0x00BE, 0x00F7 }, /* 2 EN */
+ { 0x00E4, 0x00F6, 0x017E, 0x00FC }, /* 3 ET */
+ { 0x00E2, 0x00F4, 0x00FB, 0x00E7 }, /* 4 FR */
+ { 0x00E4, 0x00F6, 0x00FC, 0x00DF }, /* 5 DE */
+ { 0x00E0, 0x00F3, 0x00E8, 0x00EC }, /* 6 IT */
+ { 0x0105, 0x0173, 0x017E, 0x012F }, /* 7 LV/LT */
+ { 0x017C, 0x015B, 0x0142, 0x017A }, /* 8 PL */
+ { 0x00FC, 0x00F1, 0x00E8, 0x00E0 }, /* 9 PT/ES */
+ { 0x00E2, 0x015F, 0x01CE, 0x00EE }, /* A RO */
+ { 0x0107, 0x017E, 0x0111, 0x0161 }, /* B SR/HR/SL */
+ { 0x00E4, 0x00F6, 0x00E5, 0x00FC }, /* C SV/FI/HU */
+ { 0x015F, 0x00F6, 0x00E7, 0x00FC }, /* D TR */
+};
+const unsigned short int arrowtable[] =
+{
+ 8592, 8594, 8593, 8595, 'O', 'K', 8592, 8592
+};
+
+CTeletextDecoder::CTeletextDecoder()
+{
+ memset(&m_RenderInfo, 0, sizeof(TextRenderInfo_t));
+
+ m_teletextFont = CSpecialProtocol::TranslatePath(TeletextFont);
+ m_TextureBuffer = NULL;
+ m_txtCache = NULL;
+ m_Manager = NULL;
+ m_Library = NULL;
+ m_RenderInfo.ShowFlof = true;
+ m_RenderInfo.Show39 = false;
+ m_RenderInfo.Showl25 = true;
+ m_RenderInfo.Prev_100 = 0x100;
+ m_RenderInfo.Prev_10 = 0x100;
+ m_RenderInfo.Next_100 = 0x100;
+ m_RenderInfo.Next_10 = 0x100;
+ m_RenderInfo.InputCounter = 2;
+
+ unsigned short rd0[] = {0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0x00<<8, 0x00<<8, 0x00<<8, 0, 0 };
+ unsigned short gn0[] = {0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0x20<<8, 0x10<<8, 0x20<<8, 0, 0 };
+ unsigned short bl0[] = {0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0x40<<8, 0x20<<8, 0x40<<8, 0, 0 };
+ unsigned short tr0[] = {0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,
+ 0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,
+ 0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,
+ 0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,
+ 0x0000 , 0x0000 , 0x0A0A , 0xFFFF, 0x3030 };
+
+ memcpy(m_RenderInfo.rd0,rd0,TXT_Color_SIZECOLTABLE*sizeof(unsigned short));
+ memcpy(m_RenderInfo.gn0,gn0,TXT_Color_SIZECOLTABLE*sizeof(unsigned short));
+ memcpy(m_RenderInfo.bl0,bl0,TXT_Color_SIZECOLTABLE*sizeof(unsigned short));
+ memcpy(m_RenderInfo.tr0,tr0,TXT_Color_SIZECOLTABLE*sizeof(unsigned short));
+
+ m_LastPage = 0;
+ m_TempPage = 0;
+ m_Ascender = 0;
+ m_PCOldCol = 0;
+ m_PCOldRow = 0;
+ m_CatchedPage = 0;
+ m_CatchCol = 0;
+ m_CatchRow = 0;
+ prevTimeSec = 0;
+ prevHeaderPage = 0;
+ m_updateTexture = false;
+ m_YOffset = 0;
+}
+
+CTeletextDecoder::~CTeletextDecoder() = default;
+
+bool CTeletextDecoder::HandleAction(const CAction &action)
+{
+ if (m_txtCache == NULL)
+ {
+ CLog::Log(LOGERROR, "CTeletextDecoder::HandleAction called without teletext cache");
+ return false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ if (action.GetID() == ACTION_MOVE_UP)
+ {
+ if (m_RenderInfo.PageCatching)
+ CatchNextPage(-1, -1);
+ else
+ GetNextPageOne(true);
+ return true;
+ }
+ else if (action.GetID() == ACTION_MOVE_DOWN)
+ {
+ if (m_RenderInfo.PageCatching)
+ CatchNextPage(1, 1);
+ else
+ GetNextPageOne(false);
+ return true;
+ }
+ else if (action.GetID() == ACTION_MOVE_RIGHT)
+ {
+ if (m_RenderInfo.PageCatching)
+ CatchNextPage(0, 1);
+ else if (m_RenderInfo.Boxed)
+ {
+ m_RenderInfo.SubtitleDelay++;
+ // display SubtitleDelay
+ m_RenderInfo.PosY = 0;
+ char ns[10];
+ SetPosX(1);
+ sprintf(ns,"+%d ", m_RenderInfo.SubtitleDelay);
+ RenderCharFB(ns[0], &Text_AtrTable[ATR_WB]);
+ RenderCharFB(ns[1], &Text_AtrTable[ATR_WB]);
+ RenderCharFB(ns[2], &Text_AtrTable[ATR_WB]);
+ RenderCharFB(ns[4], &Text_AtrTable[ATR_WB]);
+ }
+ else
+ {
+ GetNextSubPage(1);
+ }
+ return true;
+ }
+ else if (action.GetID() == ACTION_MOVE_LEFT)
+ {
+ if (m_RenderInfo.PageCatching)
+ CatchNextPage(0, -1);
+ else if (m_RenderInfo.Boxed)
+ {
+ m_RenderInfo.SubtitleDelay--;
+
+ // display subtitledelay
+ m_RenderInfo.PosY = 0;
+ char ns[10];
+ SetPosX(1);
+ sprintf(ns,"+%d ", m_RenderInfo.SubtitleDelay);
+ RenderCharFB(ns[0], &Text_AtrTable[ATR_WB]);
+ RenderCharFB(ns[1], &Text_AtrTable[ATR_WB]);
+ RenderCharFB(ns[2], &Text_AtrTable[ATR_WB]);
+ RenderCharFB(ns[4], &Text_AtrTable[ATR_WB]);
+ }
+ else
+ {
+ GetNextSubPage(-1);
+ }
+ return true;
+ }
+ else if (action.GetID() >= REMOTE_0 && action.GetID() <= REMOTE_9)
+ {
+ PageInput(action.GetID() - REMOTE_0);
+ return true;
+ }
+ else if (action.GetID() == KEY_UNICODE)
+ { // input from the keyboard
+ if (action.GetUnicode() >= 48 && action.GetUnicode() < 58)
+ {
+ PageInput(action.GetUnicode() - 48);
+ return true;
+ }
+ return false;
+ }
+ else if (action.GetID() == ACTION_PAGE_UP)
+ {
+ SwitchZoomMode();
+ return true;
+ }
+ else if (action.GetID() == ACTION_PAGE_DOWN)
+ {
+ SwitchTranspMode();
+ return true;
+ }
+ else if (action.GetID() == ACTION_SELECT_ITEM)
+ {
+ if (m_txtCache->SubPageTable[m_txtCache->Page] == 0xFF)
+ return false;
+
+ if (!m_RenderInfo.PageCatching)
+ StartPageCatching();
+ else
+ StopPageCatching();
+
+ return true;
+ }
+
+ if (m_RenderInfo.PageCatching)
+ {
+ m_txtCache->PageUpdate = true;
+ m_RenderInfo.PageCatching = false;
+ return true;
+ }
+
+ if (action.GetID() == ACTION_SHOW_INFO)
+ {
+ SwitchHintMode();
+ return true;
+ }
+ else if (action.GetID() == ACTION_TELETEXT_RED)
+ {
+ ColorKey(m_RenderInfo.Prev_100);
+ return true;
+ }
+ else if (action.GetID() == ACTION_TELETEXT_GREEN)
+ {
+ ColorKey(m_RenderInfo.Prev_10);
+ return true;
+ }
+ else if (action.GetID() == ACTION_TELETEXT_YELLOW)
+ {
+ ColorKey(m_RenderInfo.Next_10);
+ return true;
+ }
+ else if (action.GetID() == ACTION_TELETEXT_BLUE)
+ {
+ ColorKey(m_RenderInfo.Next_100);
+ return true;
+ }
+
+ return false;
+}
+
+bool CTeletextDecoder::InitDecoder()
+{
+ int error;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ m_txtCache = appPlayer->GetTeletextCache();
+ if (m_txtCache == nullptr)
+ {
+ CLog::Log(LOGERROR, "{}: called without teletext cache", __FUNCTION__);
+ return false;
+ }
+
+ /* init fontlibrary */
+ if ((error = FT_Init_FreeType(&m_Library)))
+ {
+ CLog::Log(LOGERROR, "{}: <FT_Init_FreeType: {:#2X}>", __FUNCTION__, error);
+ m_Library = NULL;
+ return false;
+ }
+
+ if ((error = FTC_Manager_New(m_Library, 7, 2, 0, &MyFaceRequester, NULL, &m_Manager)))
+ {
+ FT_Done_FreeType(m_Library);
+ m_Library = NULL;
+ m_Manager = NULL;
+ CLog::Log(LOGERROR, "{}: <FTC_Manager_New: {:#2X}>", __FUNCTION__, error);
+ return false;
+ }
+
+ if ((error = FTC_SBitCache_New(m_Manager, &m_Cache)))
+ {
+ FTC_Manager_Done(m_Manager);
+ FT_Done_FreeType(m_Library);
+ m_Manager = NULL;
+ m_Library = NULL;
+ CLog::Log(LOGERROR, "{}: <FTC_SBit_Cache_New: {:#2X}>", __FUNCTION__, error);
+ return false;
+ }
+
+ /* calculate font dimensions */
+ m_RenderInfo.Width = (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth()*CServiceBroker::GetWinSystem()->GetGfxContext().GetGUIScaleX());
+ m_RenderInfo.Height = (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight()*CServiceBroker::GetWinSystem()->GetGfxContext().GetGUIScaleY());
+ m_RenderInfo.FontHeight = m_RenderInfo.Height / 25;
+ m_RenderInfo.FontWidth_Normal = m_RenderInfo.Width / (m_RenderInfo.Show39 ? 39 : 40);
+ SetFontWidth(m_RenderInfo.FontWidth_Normal);
+ for (int i = 0; i <= 10; i++)
+ m_RenderInfo.axdrcs[i+12+1] = (m_RenderInfo.FontHeight * i + 6) / 10;
+
+ /* center screen */
+ m_TypeTTF.face_id = (FTC_FaceID) const_cast<char*>(m_teletextFont.c_str());
+ m_TypeTTF.height = (FT_UShort) m_RenderInfo.FontHeight;
+ m_TypeTTF.flags = FT_LOAD_MONOCHROME;
+ if (FTC_Manager_LookupFace(m_Manager, m_TypeTTF.face_id, &m_Face))
+ {
+ m_TypeTTF.face_id = (FTC_FaceID) const_cast<char*>(m_teletextFont.c_str());
+ if ((error = FTC_Manager_LookupFace(m_Manager, m_TypeTTF.face_id, &m_Face)))
+ {
+ CLog::Log(LOGERROR, "{}: <FTC_Manager_Lookup_Face failed with Errorcode {:#2X}>",
+ __FUNCTION__, error);
+ FTC_Manager_Done(m_Manager);
+ FT_Done_FreeType(m_Library);
+ m_Manager = NULL;
+ m_Library = NULL;
+ return false;
+ }
+ }
+ m_Ascender = m_RenderInfo.FontHeight * m_Face->ascender / m_Face->units_per_EM;
+
+ /* set variable screeninfo for double buffering */
+ m_YOffset = 0;
+ m_TextureBuffer = new UTILS::COLOR::Color[4 * m_RenderInfo.Height * m_RenderInfo.Width];
+
+ ClearFB(GetColorRGB(TXT_ColorTransp));
+ ClearBB(GetColorRGB(TXT_ColorTransp)); /* initialize backbuffer */
+ /* set new colormap */
+ SetColors(DefaultColors, 0, TXT_Color_SIZECOLTABLE);
+
+ for (int i = 0; i < 40 * 25; i++)
+ {
+ m_RenderInfo.PageChar[i] = ' ';
+ m_RenderInfo.PageAtrb[i].fg = TXT_ColorTransp;
+ m_RenderInfo.PageAtrb[i].bg = TXT_ColorTransp;
+ m_RenderInfo.PageAtrb[i].charset = C_G0P;
+ m_RenderInfo.PageAtrb[i].doubleh = 0;
+ m_RenderInfo.PageAtrb[i].doublew = 0;
+ m_RenderInfo.PageAtrb[i].IgnoreAtBlackBgSubst = 0;
+ }
+
+ m_RenderInfo.TranspMode = false;
+ m_LastPage = 0x100;
+
+ return true;
+}
+
+void CTeletextDecoder::EndDecoder()
+{
+ /* clear SubtitleCache */
+ for (TextSubtitleCache_t*& subtitleCache : m_RenderInfo.SubtitleCache)
+ {
+ if (subtitleCache != NULL)
+ {
+ delete subtitleCache;
+ subtitleCache = NULL;
+ }
+ }
+
+ if (m_TextureBuffer)
+ {
+ delete[] m_TextureBuffer;
+ m_TextureBuffer = NULL;
+ }
+
+ /* close freetype */
+ if (m_Manager)
+ {
+ FTC_Node_Unref(m_anode, m_Manager);
+ FTC_Manager_Done(m_Manager);
+ }
+ if (m_Library)
+ {
+ FT_Done_FreeType(m_Library);
+ }
+
+ m_Manager = NULL;
+ m_Library = NULL;
+
+ if (!m_txtCache)
+ {
+ CLog::Log(LOGINFO, "{}: called without cache", __FUNCTION__);
+ }
+ else
+ {
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+ m_txtCache->PageUpdate = true;
+ CLog::Log(LOGDEBUG, "Teletext: Rendering ended");
+ }
+}
+
+void CTeletextDecoder::PageInput(int Number)
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ m_updateTexture = true;
+
+ /* clear m_TempPage */
+ if (m_RenderInfo.InputCounter == 2)
+ m_TempPage = 0;
+
+ /* check for 0 & 9 on first position */
+ if (Number == 0 && m_RenderInfo.InputCounter == 2)
+ {
+ /* set page */
+ m_TempPage = m_LastPage; /* 0 toggles to last page as in program switching */
+ m_RenderInfo.InputCounter = -1;
+ }
+ else if (Number == 9 && m_RenderInfo.InputCounter == 2)
+ {
+ return;
+ }
+
+ /* show pageinput */
+ if (m_RenderInfo.ZoomMode == 2)
+ {
+ m_RenderInfo.ZoomMode = 1;
+ CopyBB2FB();
+ }
+
+ m_RenderInfo.PosY = 0;
+
+ switch (m_RenderInfo.InputCounter)
+ {
+ case 2:
+ SetPosX(1);
+ RenderCharFB(Number | '0', &Text_AtrTable[ATR_WB]);
+ RenderCharFB('-', &Text_AtrTable[ATR_WB]);
+ RenderCharFB('-', &Text_AtrTable[ATR_WB]);
+ break;
+
+ case 1:
+ SetPosX(2);
+ RenderCharFB(Number | '0', &Text_AtrTable[ATR_WB]);
+ break;
+
+ case 0:
+ SetPosX(3);
+ RenderCharFB(Number | '0', &Text_AtrTable[ATR_WB]);
+ break;
+ }
+
+ /* generate pagenumber */
+ m_TempPage |= Number << (m_RenderInfo.InputCounter*4);
+
+ m_RenderInfo.InputCounter--;
+
+ if (m_RenderInfo.InputCounter < 0)
+ {
+ /* disable SubPage zapping */
+ m_txtCache->ZapSubpageManual = false;
+
+ /* reset input */
+ m_RenderInfo.InputCounter = 2;
+
+ /* set new page */
+ m_LastPage = m_txtCache->Page;
+
+ m_txtCache->Page = m_TempPage;
+ m_RenderInfo.HintMode = false;
+
+ /* check cache */
+ int subp = m_txtCache->SubPageTable[m_txtCache->Page];
+ if (subp != 0xFF)
+ {
+ m_txtCache->SubPage = subp;
+ m_txtCache->PageUpdate = true;
+ }
+ else
+ {
+ m_txtCache->SubPage = 0;
+// RenderMessage(PageNotFound);
+ }
+ }
+}
+
+void CTeletextDecoder::GetNextPageOne(bool up)
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ /* disable subpage zapping */
+ m_txtCache->ZapSubpageManual = false;
+
+ /* abort pageinput */
+ m_RenderInfo.InputCounter = 2;
+
+ /* find next cached page */
+ m_LastPage = m_txtCache->Page;
+
+ int subp;
+ do {
+ if (up)
+ CDVDTeletextTools::NextDec(&m_txtCache->Page);
+ else
+ CDVDTeletextTools::PrevDec(&m_txtCache->Page);
+ subp = m_txtCache->SubPageTable[m_txtCache->Page];
+ } while (subp == 0xFF && m_txtCache->Page != m_LastPage);
+
+ /* update Page */
+ if (m_txtCache->Page != m_LastPage)
+ {
+ if (m_RenderInfo.ZoomMode == 2)
+ m_RenderInfo.ZoomMode = 1;
+
+ m_txtCache->SubPage = subp;
+ m_RenderInfo.HintMode = false;
+ m_txtCache->PageUpdate = true;
+ }
+}
+
+void CTeletextDecoder::GetNextSubPage(int offset)
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ /* abort pageinput */
+ m_RenderInfo.InputCounter = 2;
+
+ for (int loop = m_txtCache->SubPage + offset; loop != m_txtCache->SubPage; loop += offset)
+ {
+ if (loop < 0)
+ loop = 0x79;
+ else if (loop > 0x79)
+ loop = 0;
+ if (loop == m_txtCache->SubPage)
+ break;
+
+ if (m_txtCache->astCachetable[m_txtCache->Page][loop])
+ {
+ /* enable manual SubPage zapping */
+ m_txtCache->ZapSubpageManual = true;
+
+ /* update page */
+ if (m_RenderInfo.ZoomMode == 2) /* if zoomed to lower half */
+ m_RenderInfo.ZoomMode = 1; /* activate upper half */
+
+ m_txtCache->SubPage = loop;
+ m_RenderInfo.HintMode = false;
+ m_txtCache->PageUpdate = true;
+
+ return;
+ }
+ }
+}
+
+void CTeletextDecoder::SwitchZoomMode()
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ if (m_txtCache->SubPageTable[m_txtCache->Page] != 0xFF)
+ {
+ /* toggle mode */
+ m_RenderInfo.ZoomMode++;
+
+ if (m_RenderInfo.ZoomMode == 3)
+ m_RenderInfo.ZoomMode = 0;
+
+ /* update page */
+ m_txtCache->PageUpdate = true;
+ }
+}
+
+void CTeletextDecoder::SwitchTranspMode()
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ /* toggle mode */
+ if (!m_RenderInfo.TranspMode)
+ m_RenderInfo.TranspMode = true;
+ else
+ m_RenderInfo.TranspMode = false; /* backward to immediately switch to TV-screen */
+
+ /* set mode */
+ if (!m_RenderInfo.TranspMode) /* normal text-only */
+ {
+ ClearBB(m_txtCache->FullScrColor);
+ m_txtCache->PageUpdate = true;
+ }
+ else /* semi-transparent BG with FG text */
+ {
+ ClearBB(TXT_ColorTransp);
+ m_txtCache->PageUpdate = true;
+ }
+}
+
+void CTeletextDecoder::SwitchHintMode()
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ /* toggle mode */
+ m_RenderInfo.HintMode ^= true;
+
+ if (!m_RenderInfo.HintMode) /* toggle evaluation of level 2.5 information by explicitly switching off HintMode */
+ {
+ m_RenderInfo.Showl25 ^= true;
+ }
+ /* update page */
+ m_txtCache->PageUpdate = true;
+}
+
+void CTeletextDecoder::ColorKey(int target)
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ if (!target)
+ return;
+
+ if (m_RenderInfo.ZoomMode == 2)
+ m_RenderInfo.ZoomMode = 1;
+
+ m_LastPage = m_txtCache->Page;
+ m_txtCache->Page = target;
+ m_txtCache->SubPage = m_txtCache->SubPageTable[m_txtCache->Page];
+ m_RenderInfo.InputCounter = 2;
+ m_RenderInfo.HintMode = false;
+ m_txtCache->PageUpdate = true;
+}
+
+void CTeletextDecoder::StartPageCatching()
+{
+ m_RenderInfo.PageCatching = true;
+
+ /* abort pageinput */
+ m_RenderInfo.InputCounter = 2;
+
+ /* show info line */
+ m_RenderInfo.ZoomMode = 0;
+ m_RenderInfo.PosX = 0;
+ m_RenderInfo.PosY = 24*m_RenderInfo.FontHeight;
+
+ /* check for pagenumber(s) */
+ m_CatchRow = 1;
+ m_CatchCol = 0;
+ m_CatchedPage = 0;
+ m_PCOldRow = 0;
+ m_PCOldCol = 0; /* no inverted page number to restore yet */
+ CatchNextPage(0, 1);
+
+ if (!m_CatchedPage)
+ {
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ m_RenderInfo.PageCatching = false;
+ m_txtCache->PageUpdate = true;
+ return;
+ }
+}
+
+void CTeletextDecoder::StopPageCatching()
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ /* set new page */
+ if (m_RenderInfo.ZoomMode == 2)
+ m_RenderInfo.ZoomMode = 1;
+
+ m_LastPage = m_txtCache->Page;
+ m_txtCache->Page = m_CatchedPage;
+ m_RenderInfo.HintMode = false;
+ m_txtCache->PageUpdate = true;
+ m_RenderInfo.PageCatching = false;
+
+ int subp = m_txtCache->SubPageTable[m_txtCache->Page];
+ if (subp != 0xFF)
+ m_txtCache->SubPage = subp;
+ else
+ m_txtCache->SubPage = 0;
+}
+
+void CTeletextDecoder::CatchNextPage(int firstlineinc, int inc)
+{
+ int tmp_page, allowwrap = 1; /* allow first wrap around */
+
+ /* catch next page */
+ for(;;)
+ {
+ unsigned char *p = &(m_RenderInfo.PageChar[m_CatchRow*40 + m_CatchCol]);
+ TextPageAttr_t a = m_RenderInfo.PageAtrb[m_CatchRow*40 + m_CatchCol];
+
+ if (!(a.charset == C_G1C || a.charset == C_G1S) && /* no mosaic */
+ (a.fg != a.bg) && /* not hidden */
+ (*p >= '1' && *p <= '8' && /* valid page number */
+ *(p+1) >= '0' && *(p+1) <= '9' &&
+ *(p+2) >= '0' && *(p+2) <= '9') &&
+ (m_CatchRow == 0 || (*(p-1) < '0' || *(p-1) > '9')) && /* non-numeric char before and behind */
+ (m_CatchRow == 37 || (*(p+3) < '0' || *(p+3) > '9')))
+ {
+ tmp_page = ((*p - '0')<<8) | ((*(p+1) - '0')<<4) | (*(p+2) - '0');
+
+#if 0
+ if (tmp_page != m_CatchedPage) /* confusing to skip identical page numbers - I want to reach what I aim to */
+#endif
+ {
+ m_CatchedPage = tmp_page;
+ RenderCatchedPage();
+ m_CatchCol += inc; /* FIXME: limit */
+ return;
+ }
+ }
+
+ if (firstlineinc > 0)
+ {
+ m_CatchRow++;
+ m_CatchCol = 0;
+ firstlineinc = 0;
+ }
+ else if (firstlineinc < 0)
+ {
+ m_CatchRow--;
+ m_CatchCol = 37;
+ firstlineinc = 0;
+ }
+ else
+ m_CatchCol += inc;
+
+ if (m_CatchCol > 37)
+ {
+ m_CatchRow++;
+ m_CatchCol = 0;
+ }
+ else if (m_CatchCol < 0)
+ {
+ m_CatchRow--;
+ m_CatchCol = 37;
+ }
+
+ if (m_CatchRow > 23)
+ {
+ if (allowwrap)
+ {
+ allowwrap = 0;
+ m_CatchRow = 1;
+ m_CatchCol = 0;
+ }
+ else
+ {
+ return;
+ }
+ }
+ else if (m_CatchRow < 1)
+ {
+ if (allowwrap)
+ {
+ allowwrap = 0;
+ m_CatchRow = 23;
+ m_CatchCol =37;
+ }
+ else
+ {
+ return;
+ }
+ }
+ }
+}
+
+void CTeletextDecoder::RenderCatchedPage()
+{
+ int zoom = 0;
+ m_updateTexture = true;
+
+ /* handle zoom */
+ if (m_RenderInfo.ZoomMode)
+ zoom = 1<<10;
+
+ if (m_PCOldRow || m_PCOldCol) /* not at first call */
+ {
+ /* restore pagenumber */
+ SetPosX(m_PCOldCol);
+
+ if (m_RenderInfo.ZoomMode == 2)
+ m_RenderInfo.PosY = (m_PCOldRow-12)*m_RenderInfo.FontHeight*((zoom>>10)+1);
+ else
+ m_RenderInfo.PosY = m_PCOldRow*m_RenderInfo.FontHeight*((zoom>>10)+1);
+
+ RenderCharFB(m_RenderInfo.PageChar[m_PCOldRow*40 + m_PCOldCol ], &m_RenderInfo.PageAtrb[m_PCOldRow*40 + m_PCOldCol ]);
+ RenderCharFB(m_RenderInfo.PageChar[m_PCOldRow*40 + m_PCOldCol + 1], &m_RenderInfo.PageAtrb[m_PCOldRow*40 + m_PCOldCol + 1]);
+ RenderCharFB(m_RenderInfo.PageChar[m_PCOldRow*40 + m_PCOldCol + 2], &m_RenderInfo.PageAtrb[m_PCOldRow*40 + m_PCOldCol + 2]);
+ }
+
+ m_PCOldRow = m_CatchRow;
+ m_PCOldCol = m_CatchCol;
+
+ /* mark pagenumber */
+ if (m_RenderInfo.ZoomMode == 1 && m_CatchRow > 11)
+ {
+ m_RenderInfo.ZoomMode = 2;
+ CopyBB2FB();
+ }
+ else if (m_RenderInfo.ZoomMode == 2 && m_CatchRow < 12)
+ {
+ m_RenderInfo.ZoomMode = 1;
+ CopyBB2FB();
+ }
+ SetPosX(m_CatchCol);
+
+ if (m_RenderInfo.ZoomMode == 2)
+ m_RenderInfo.PosY = (m_CatchRow-12)*m_RenderInfo.FontHeight*((zoom>>10)+1);
+ else
+ m_RenderInfo.PosY = m_CatchRow*m_RenderInfo.FontHeight*((zoom>>10)+1);
+
+ TextPageAttr_t a0 = m_RenderInfo.PageAtrb[m_CatchRow*40 + m_CatchCol ];
+ TextPageAttr_t a1 = m_RenderInfo.PageAtrb[m_CatchRow*40 + m_CatchCol + 1];
+ TextPageAttr_t a2 = m_RenderInfo.PageAtrb[m_CatchRow*40 + m_CatchCol + 2];
+ int t;
+
+ /* exchange colors */
+ t = a0.fg; a0.fg = a0.bg; a0.bg = t;
+ t = a1.fg; a1.fg = a1.bg; a1.bg = t;
+ t = a2.fg; a2.fg = a2.bg; a2.bg = t;
+
+ RenderCharFB(m_RenderInfo.PageChar[m_CatchRow*40 + m_CatchCol ], &a0);
+ RenderCharFB(m_RenderInfo.PageChar[m_CatchRow*40 + m_CatchCol + 1], &a1);
+ RenderCharFB(m_RenderInfo.PageChar[m_CatchRow*40 + m_CatchCol + 2], &a2);
+}
+
+void CTeletextDecoder::RenderPage()
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ int StartRow = 0;
+ int national_subset_bak = m_txtCache->NationalSubset;
+
+ if (m_txtCache->PageUpdate)
+ m_updateTexture = true;
+
+ /* update page or timestring */
+ if (m_txtCache->PageUpdate && m_txtCache->PageReceiving != m_txtCache->Page && m_RenderInfo.InputCounter == 2)
+ {
+ /* reset update flag */
+ m_txtCache->PageUpdate = false;
+ if (m_RenderInfo.Boxed && m_RenderInfo.SubtitleDelay)
+ {
+ TextSubtitleCache_t* c = NULL;
+ int j = -1;
+ for (int i = 0; i < SUBTITLE_CACHESIZE; i++)
+ {
+ if (j == -1 && !m_RenderInfo.SubtitleCache[i])
+ j = i;
+ if (m_RenderInfo.SubtitleCache[i] && !m_RenderInfo.SubtitleCache[i]->Valid)
+ {
+ c = m_RenderInfo.SubtitleCache[i];
+ break;
+ }
+ }
+ if (c == NULL)
+ {
+ if (j == -1) // no more space in SubtitleCache
+ return;
+
+ c = new TextSubtitleCache_t;
+ if (c == NULL)
+ return;
+
+ *c = {};
+ m_RenderInfo.SubtitleCache[j] = c;
+ }
+ c->Valid = true;
+ c->Timestamp = std::chrono::steady_clock::now();
+
+ if (m_txtCache->SubPageTable[m_txtCache->Page] != 0xFF)
+ {
+ TextPageinfo_t * p = DecodePage(m_RenderInfo.Showl25, c->PageChar, c->PageAtrb, m_RenderInfo.HintMode, m_RenderInfo.ShowFlof);
+ if (p)
+ {
+ m_RenderInfo.Boxed = p->boxed;
+ }
+ }
+ m_RenderInfo.DelayStarted = true;
+ return;
+ }
+ m_RenderInfo.DelayStarted = false;
+ /* decode page */
+ if (m_txtCache->SubPageTable[m_txtCache->Page] != 0xFF)
+ {
+ TextPageinfo_t * p = DecodePage(m_RenderInfo.Showl25, m_RenderInfo.PageChar, m_RenderInfo.PageAtrb, m_RenderInfo.HintMode, m_RenderInfo.ShowFlof);
+ if (p)
+ {
+ m_RenderInfo.PageInfo = p;
+ m_RenderInfo.Boxed = p->boxed;
+ }
+ if (m_RenderInfo.Boxed || m_RenderInfo.TranspMode)
+ FillBorder(GetColorRGB(TXT_ColorTransp));
+ else
+ FillBorder(GetColorRGB((enumTeletextColor)m_txtCache->FullScrColor));
+
+ if (m_txtCache->ColorTable) /* as late as possible to shorten the time the old page is displayed with the new colors */
+ SetColors(m_txtCache->ColorTable, 16, 16); /* set colors for CLUTs 2+3 */
+ }
+ else
+ StartRow = 1;
+
+ DoRenderPage(StartRow, national_subset_bak);
+ }
+ else
+ {
+ if (m_RenderInfo.DelayStarted)
+ {
+ auto now = std::chrono::steady_clock::now();
+ for (TextSubtitleCache_t* const subtitleCache : m_RenderInfo.SubtitleCache)
+ {
+ if (subtitleCache && subtitleCache->Valid &&
+ std::chrono::duration_cast<std::chrono::seconds>(now - subtitleCache->Timestamp)
+ .count() >= m_RenderInfo.SubtitleDelay)
+ {
+ memcpy(m_RenderInfo.PageChar, subtitleCache->PageChar, 40 * 25);
+ memcpy(m_RenderInfo.PageAtrb, subtitleCache->PageAtrb, 40 * 25 * sizeof(TextPageAttr_t));
+ DoRenderPage(StartRow, national_subset_bak);
+ subtitleCache->Valid = false;
+ return;
+ }
+ }
+ }
+ if (m_RenderInfo.ZoomMode != 2)
+ {
+ m_RenderInfo.PosY = 0;
+ if (m_txtCache->SubPageTable[m_txtCache->Page] == 0xff)
+ {
+ m_RenderInfo.PageAtrb[32].fg = TXT_ColorYellow;
+ m_RenderInfo.PageAtrb[32].bg = TXT_ColorMenu1;
+ int showpage = m_txtCache->PageReceiving;
+ int showsubpage;
+
+ // Verify that showpage is positive before any access to the array
+ if (showpage >= 0 && (showsubpage = m_txtCache->SubPageTable[showpage]) != 0xff)
+ {
+ TextCachedPage_t *pCachedPage;
+ pCachedPage = m_txtCache->astCachetable[showpage][showsubpage];
+ if (pCachedPage && IsDec(showpage))
+ {
+ m_RenderInfo.PosX = 0;
+ if (m_RenderInfo.InputCounter == 2)
+ {
+ if (m_txtCache->BTTok && !m_txtCache->BasicTop[m_txtCache->Page]) /* page non-existent according to TOP (continue search anyway) */
+ {
+ m_RenderInfo.PageAtrb[0].fg = TXT_ColorWhite;
+ m_RenderInfo.PageAtrb[0].bg = TXT_ColorRed;
+ }
+ else
+ {
+ m_RenderInfo.PageAtrb[0].fg = TXT_ColorYellow;
+ m_RenderInfo.PageAtrb[0].bg = TXT_ColorMenu1;
+ }
+ CDVDTeletextTools::Hex2Str((char*)m_RenderInfo.PageChar+3, m_txtCache->Page);
+
+ int col;
+ for (col = m_RenderInfo.nofirst; col < 7; col++) // selected page
+ {
+ RenderCharFB(m_RenderInfo.PageChar[col], &m_RenderInfo.PageAtrb[0]);
+ }
+ RenderCharFB(m_RenderInfo.PageChar[col], &m_RenderInfo.PageAtrb[32]);
+ }
+ else
+ SetPosX(8);
+
+ memcpy(&m_RenderInfo.PageChar[8], pCachedPage->p0, 24); /* header line without timestring */
+ for (unsigned char i : pCachedPage->p0)
+ {
+ RenderCharFB(i, &m_RenderInfo.PageAtrb[32]);
+ }
+
+ /* Update on every Header number change */
+ if (pCachedPage->p0[2] != prevHeaderPage)
+ {
+ prevHeaderPage = pCachedPage->p0[2];
+ m_updateTexture = true;
+ }
+ }
+ }
+ }
+
+ /* update timestring */
+ SetPosX(32);
+ for (int i = 0; i < 8; i++)
+ {
+ if (!m_RenderInfo.PageAtrb[32+i].flashing)
+ RenderCharFB(m_txtCache->TimeString[i], &m_RenderInfo.PageAtrb[32]);
+ else
+ {
+ SetPosX(33+i);
+ }
+ }
+
+ if (!IsSubtitlePage(m_txtCache->Page))
+ {
+ /* Update on every changed second */
+ if (m_txtCache->TimeString[7] != prevTimeSec)
+ {
+ prevTimeSec = m_txtCache->TimeString[7];
+ m_updateTexture = true;
+ }
+ }
+ else
+ {
+ m_updateTexture = true;
+ }
+ }
+ DoFlashing(StartRow);
+ m_txtCache->NationalSubset = national_subset_bak;
+ }
+}
+
+bool CTeletextDecoder::IsSubtitlePage(int pageNumber) const
+{
+ if (!m_txtCache)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ for (const auto subPage : m_txtCache->SubtitlePages)
+ {
+ if (subPage.page == pageNumber)
+ return true;
+ }
+
+ return false;
+}
+
+void CTeletextDecoder::DoFlashing(int startrow)
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ TextCachedPage_t* textCachepage =
+ m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPage];
+
+ // Verify that the page is not deleted by the other thread: CDVDTeletextData::ResetTeletextCache()
+ if (!textCachepage || m_RenderInfo.PageInfo != &textCachepage->pageinfo)
+ m_RenderInfo.PageInfo = nullptr;
+
+ /* get national subset */
+ if (m_txtCache->NationalSubset <= NAT_MAX_FROM_HEADER && /* not for GR/RU as long as line28 is not evaluated */
+ m_RenderInfo.PageInfo && m_RenderInfo.PageInfo->nationalvalid) /* individual subset according to page header */
+ {
+ m_txtCache->NationalSubset = CountryConversionTable[m_RenderInfo.PageInfo->national];
+ }
+
+ /* Flashing */
+ TextPageAttr_t flashattr;
+ char flashchar;
+ std::chrono::milliseconds flashphase = std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::steady_clock::now().time_since_epoch()) %
+ 1000;
+
+ int srow = startrow;
+ int erow = 24;
+ int factor=1;
+
+ switch (m_RenderInfo.ZoomMode)
+ {
+ case 1: erow = 12; factor=2;break;
+ case 2: srow = 12; factor=2;break;
+ }
+
+ m_RenderInfo.PosY = startrow*m_RenderInfo.FontHeight*factor;
+ for (int row = srow; row < erow; row++)
+ {
+ int index = row * 40;
+ int dhset = 0;
+ int incflash = 3;
+ int decflash = 2;
+
+ m_RenderInfo.PosX = 0;
+ for (int col = m_RenderInfo.nofirst; col < 40; col++)
+ {
+ if (m_RenderInfo.PageAtrb[index + col].flashing && m_RenderInfo.PageChar[index + col] > 0x20 && m_RenderInfo.PageChar[index + col] != 0xff )
+ {
+ SetPosX(col);
+ flashchar = m_RenderInfo.PageChar[index + col];
+ bool doflash = false;
+ memcpy(&flashattr, &m_RenderInfo.PageAtrb[index + col], sizeof(TextPageAttr_t));
+ switch (flashattr.flashing &0x1c) // Flash Rate
+ {
+ case 0x00 : // 1 Hz
+ if (flashphase > 500ms)
+ doflash = true;
+ break;
+ case 0x04 : // 2 Hz Phase 1
+ if (flashphase < 250ms)
+ doflash = true;
+ break;
+ case 0x08 : // 2 Hz Phase 2
+ if (flashphase >= 250ms && flashphase < 500ms)
+ doflash = true;
+ break;
+ case 0x0c : // 2 Hz Phase 3
+ if (flashphase >= 500ms && flashphase < 750ms)
+ doflash = true;
+ break;
+ case 0x10 : // incremental flash
+ incflash++;
+ if (incflash>3) incflash = 1;
+ switch (incflash)
+ {
+ case 1:
+ if (flashphase < 250ms)
+ doflash = true;
+ break;
+ case 2:
+ if (flashphase >= 250ms && flashphase < 500ms)
+ doflash = true;
+ break;
+ case 3:
+ if (flashphase >= 500ms && flashphase < 750ms)
+ doflash = true;
+ break;
+ }
+ break;
+ case 0x14 : // decremental flash
+ decflash--;
+ if (decflash<1) decflash = 3;
+ switch (decflash)
+ {
+ case 1:
+ if (flashphase < 250ms)
+ doflash = true;
+ break;
+ case 2:
+ if (flashphase >= 250ms && flashphase < 500ms)
+ doflash = true;
+ break;
+ case 3:
+ if (flashphase >= 500ms && flashphase < 750ms)
+ doflash = true;
+ break;
+ }
+ break;
+
+ }
+
+ switch (flashattr.flashing &0x03) // Flash Mode
+ {
+ case 0x01 : // normal Flashing
+ if (doflash) flashattr.fg = flashattr.bg;
+ break;
+ case 0x02 : // inverted Flashing
+ doflash = !doflash;
+ if (doflash) flashattr.fg = flashattr.bg;
+ break;
+ case 0x03 : // color Flashing
+ if (doflash) flashattr.fg = flashattr.fg + (flashattr.fg > 7 ? (-8) : 8);
+ break;
+
+ }
+ RenderCharFB(flashchar, &flashattr);
+ if (flashattr.doublew) col++;
+ if (flashattr.doubleh) dhset = 1;
+
+ m_updateTexture = true;
+ }
+ }
+ if (dhset)
+ {
+ row++;
+ m_RenderInfo.PosY += m_RenderInfo.FontHeight*factor;
+ }
+ m_RenderInfo.PosY += m_RenderInfo.FontHeight*factor;
+ }
+}
+
+void CTeletextDecoder::DoRenderPage(int startrow, int national_subset_bak)
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ /* display first column? */
+ m_RenderInfo.nofirst = m_RenderInfo.Show39;
+ for (int row = 1; row < 24; row++)
+ {
+ int Byte = m_RenderInfo.PageChar[row*40];
+ if (Byte != ' ' && Byte != 0x00 && Byte != 0xFF && m_RenderInfo.PageAtrb[row*40].fg != m_RenderInfo.PageAtrb[row*40].bg)
+ {
+ m_RenderInfo.nofirst = 0;
+ break;
+ }
+ }
+ m_RenderInfo.FontWidth_Normal = m_RenderInfo.Width / (m_RenderInfo.nofirst ? 39 : 40);
+ SetFontWidth(m_RenderInfo.FontWidth_Normal);
+
+ if (m_RenderInfo.TranspMode || m_RenderInfo.Boxed)
+ {
+ FillBorder(GetColorRGB(TXT_ColorTransp));//ClearBB(transp);
+ m_RenderInfo.ClearBBColor = TXT_ColorTransp;
+ }
+
+ /* get national subset */
+ if (m_txtCache->NationalSubset <= NAT_MAX_FROM_HEADER && /* not for GR/RU as long as line28 is not evaluated */
+ m_RenderInfo.PageInfo && m_RenderInfo.PageInfo->nationalvalid) /* individual subset according to page header */
+ {
+ m_txtCache->NationalSubset = CountryConversionTable[m_RenderInfo.PageInfo->national];
+ }
+ /* render page */
+ if (m_RenderInfo.PageInfo && (m_RenderInfo.PageInfo->function == FUNC_GDRCS || m_RenderInfo.PageInfo->function == FUNC_DRCS)) /* character definitions */
+ {
+ #define DRCSROWS 8
+ #define DRCSCOLS (48/DRCSROWS)
+ #define DRCSZOOMX 3
+ #define DRCSZOOMY 5
+ #define DRCSXSPC (12*DRCSZOOMX + 2)
+ #define DRCSYSPC (10*DRCSZOOMY + 2)
+
+ unsigned char ax[] = { /* array[0..12] of x-offsets, array[0..10] of y-offsets for each pixel */
+ DRCSZOOMX * 0,
+ DRCSZOOMX * 1,
+ DRCSZOOMX * 2,
+ DRCSZOOMX * 3,
+ DRCSZOOMX * 4,
+ DRCSZOOMX * 5,
+ DRCSZOOMX * 6,
+ DRCSZOOMX * 7,
+ DRCSZOOMX * 8,
+ DRCSZOOMX * 9,
+ DRCSZOOMX * 10,
+ DRCSZOOMX * 11,
+ DRCSZOOMX * 12,
+ DRCSZOOMY * 0,
+ DRCSZOOMY * 1,
+ DRCSZOOMY * 2,
+ DRCSZOOMY * 3,
+ DRCSZOOMY * 4,
+ DRCSZOOMY * 5,
+ DRCSZOOMY * 6,
+ DRCSZOOMY * 7,
+ DRCSZOOMY * 8,
+ DRCSZOOMY * 9,
+ DRCSZOOMY * 10
+ };
+
+ ClearBB(TXT_ColorBlack);
+ for (int col = 0; col < 24*40; col++)
+ m_RenderInfo.PageAtrb[col] = Text_AtrTable[ATR_WB];
+
+ for (int row = 0; row < DRCSROWS; row++)
+ {
+ for (int col = 0; col < DRCSCOLS; col++)
+ {
+ RenderDRCS(m_RenderInfo.Width,
+ m_RenderInfo.PageChar + 20 * (DRCSCOLS * row + col + 2),
+ m_TextureBuffer
+ + (m_RenderInfo.FontHeight + DRCSYSPC * row + m_RenderInfo.Height) * m_RenderInfo.Width
+ + DRCSXSPC * col,
+ ax, GetColorRGB(TXT_ColorWhite), GetColorRGB(TXT_ColorBlack));
+ }
+ }
+ memset(m_RenderInfo.PageChar + 40, 0xff, 24*40); /* don't render any char below row 0 */
+ }
+ m_RenderInfo.PosY = startrow*m_RenderInfo.FontHeight;
+ for (int row = startrow; row < 24; row++)
+ {
+ int index = row * 40;
+
+ m_RenderInfo.PosX = 0;
+ for (int col = m_RenderInfo.nofirst; col < 40; col++)
+ {
+ RenderCharBB(m_RenderInfo.PageChar[index + col], &m_RenderInfo.PageAtrb[index + col]);
+
+ if (m_RenderInfo.PageAtrb[index + col].doubleh && m_RenderInfo.PageChar[index + col] != 0xff && row < 24-1) /* disable lower char in case of doubleh setting in l25 objects */
+ m_RenderInfo.PageChar[index + col + 40] = 0xff;
+ if (m_RenderInfo.PageAtrb[index + col].doublew && col < 40-1) /* skip next column if double width */
+ {
+ col++;
+ if (m_RenderInfo.PageAtrb[index + col - 1].doubleh && m_RenderInfo.PageChar[index + col] != 0xff && row < 24-1) /* disable lower char in case of doubleh setting in l25 objects */
+ m_RenderInfo.PageChar[index + col + 40] = 0xff;
+ }
+ }
+ m_RenderInfo.PosY += m_RenderInfo.FontHeight;
+ }
+ DoFlashing(startrow);
+
+ /* update framebuffer */
+ CopyBB2FB();
+ m_txtCache->NationalSubset = national_subset_bak;
+}
+
+void CTeletextDecoder::Decode_BTT()
+{
+ /* basic top table */
+ int current, b1, b2, b3, b4;
+ unsigned char btt[23*40];
+
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ if (m_txtCache->SubPageTable[0x1f0] == 0xff || 0 == m_txtCache->astCachetable[0x1f0][m_txtCache->SubPageTable[0x1f0]]) /* not yet received */
+ return;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ appPlayer->LoadPage(0x1f0, m_txtCache->SubPageTable[0x1f0], btt);
+ if (btt[799] == ' ') /* not completely received or error */
+ return;
+
+ current = 0x100;
+ for (int i = 0; i < 800; i++)
+ {
+ b1 = btt[i];
+ if (b1 == ' ')
+ b1 = 0;
+ else
+ {
+ b1 = dehamming[b1];
+ if (b1 == 0xFF) /* hamming error in btt */
+ {
+ btt[799] = ' '; /* mark btt as not received */
+ return;
+ }
+ }
+ m_txtCache->BasicTop[current] = b1;
+ CDVDTeletextTools::NextDec(&current);
+ }
+ /* page linking table */
+ m_txtCache->ADIP_PgMax = -1; /* rebuild table of adip pages */
+ for (int i = 0; i < 10; i++)
+ {
+ b1 = dehamming[btt[800 + 8*i +0]];
+
+ if (b1 == 0xE)
+ continue; /* unused */
+ else if (b1 == 0xF)
+ break; /* end */
+
+ b4 = dehamming[btt[800 + 8*i +7]];
+
+ if (b4 != 2) /* only adip, ignore multipage (1) */
+ continue;
+
+ b2 = dehamming[btt[800 + 8*i +1]];
+ b3 = dehamming[btt[800 + 8*i +2]];
+
+ if (b1 == 0xFF || b2 == 0xFF || b3 == 0xFF)
+ {
+ CLog::Log(LOGERROR, "CTeletextDecoder::Decode_BTT <Biterror in btt/plt index {}>", i);
+ btt[799] = ' '; /* mark btt as not received */
+ return;
+ }
+
+ b1 = b1<<8 | b2<<4 | b3; /* page number */
+ m_txtCache->ADIP_Pg[++m_txtCache->ADIP_PgMax] = b1;
+ }
+
+ m_txtCache->BTTok = true;
+}
+
+void CTeletextDecoder::Decode_ADIP() /* additional information table */
+{
+ int i, p, j, b1, b2, b3, charfound;
+ unsigned char padip[23*40];
+
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ for (i = 0; i <= m_txtCache->ADIP_PgMax; i++)
+ {
+ p = m_txtCache->ADIP_Pg[i];
+ if (!p || m_txtCache->SubPageTable[p] == 0xff || 0 == m_txtCache->astCachetable[p][m_txtCache->SubPageTable[p]]) /* not cached (avoid segfault) */
+ continue;
+
+ appPlayer->LoadPage(p, m_txtCache->SubPageTable[p], padip);
+ for (j = 0; j < 44; j++)
+ {
+ b1 = dehamming[padip[20*j+0]];
+ if (b1 == 0xE)
+ continue; /* unused */
+
+ if (b1 == 0xF)
+ break; /* end */
+
+ b2 = dehamming[padip[20*j+1]];
+ b3 = dehamming[padip[20*j+2]];
+
+ if (b1 == 0xFF || b2 == 0xFF || b3 == 0xFF)
+ {
+ CLog::Log(LOGERROR,
+ "CTeletextDecoder::Decode_BTT <Biterror in ait {:03x} {} {:02x} {:02x} {:02x} "
+ "{:02x} {:02x} {:02x}>",
+ p, j, padip[20 * j + 0], padip[20 * j + 1], padip[20 * j + 2], b1, b2, b3);
+ return;
+ }
+
+ if (b1>8 || b2>9 || b3>9) /* ignore entries with invalid or hex page numbers */
+ {
+ continue;
+ }
+
+ b1 = b1<<8 | b2<<4 | b3; /* page number */
+ charfound = 0; /* flag: no printable char found */
+
+ for (b2 = 11; b2 >= 0; b2--)
+ {
+ b3 = deparity[padip[20*j + 8 + b2]];
+ if (b3 < ' ')
+ b3 = ' ';
+
+ if (b3 == ' ' && !charfound)
+ m_txtCache->ADIPTable[b1][b2] = '\0';
+ else
+ {
+ m_txtCache->ADIPTable[b1][b2] = b3;
+ charfound = 1;
+ }
+ }
+ } /* next link j */
+
+ m_txtCache->ADIP_Pg[i] = 0; /* completely decoded: clear entry */
+ } /* next adip page i */
+
+ while ((m_txtCache->ADIP_PgMax >= 0) && !m_txtCache->ADIP_Pg[m_txtCache->ADIP_PgMax]) /* and shrink table */
+ m_txtCache->ADIP_PgMax--;
+}
+
+int CTeletextDecoder::TopText_GetNext(int startpage, int up, int findgroup)
+{
+ int current, nextgrp, nextblk;
+
+ int stoppage = (IsDec(startpage) ? startpage : startpage & 0xF00); // avoid endless loop in hexmode
+ nextgrp = nextblk = 0;
+ current = startpage;
+
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ do {
+ if (up)
+ CDVDTeletextTools::NextDec(&current);
+ else
+ CDVDTeletextTools::PrevDec(&current);
+
+ if (!m_txtCache->BTTok || m_txtCache->BasicTop[current]) /* only if existent */
+ {
+ if (findgroup)
+ {
+ if (m_txtCache->BasicTop[current] >= 6 && m_txtCache->BasicTop[current] <= 7)
+ return current;
+ if (!nextgrp && (current&0x00F) == 0)
+ nextgrp = current;
+ }
+ if (m_txtCache->BasicTop[current] >= 2 && m_txtCache->BasicTop[current] <= 5) /* always find block */
+ return current;
+
+ if (!nextblk && (current&0x0FF) == 0)
+ nextblk = current;
+ }
+ } while (current != stoppage);
+
+ if (nextgrp)
+ return nextgrp;
+ else if (nextblk)
+ return nextblk;
+ else
+ return current;
+}
+
+void CTeletextDecoder::Showlink(int column, int linkpage)
+{
+ unsigned char line[] = " >??? ";
+ int oldfontwidth = m_RenderInfo.FontWidth;
+ int yoffset;
+
+ if (m_YOffset)
+ yoffset = 0;
+ else
+ yoffset = m_RenderInfo.Height;
+
+ int abx = ((m_RenderInfo.Width)%(40-m_RenderInfo.nofirst) == 0 ? m_RenderInfo.Width+1 : (m_RenderInfo.Width)/(((m_RenderInfo.Width)%(40-m_RenderInfo.nofirst)))+1);// distance between 'inserted' pixels
+ int width = m_RenderInfo.Width /4;
+
+ m_RenderInfo.PosY = 24*m_RenderInfo.FontHeight;
+
+ if (m_RenderInfo.Boxed)
+ {
+ m_RenderInfo.PosX = column*width;
+ FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY+yoffset, m_RenderInfo.Width, m_RenderInfo.FontHeight, GetColorRGB(TXT_ColorTransp));
+ return;
+ }
+
+ if (m_txtCache->ADIPTable[linkpage][0])
+ {
+ m_RenderInfo.PosX = column*width;
+ int l = strlen(m_txtCache->ADIPTable[linkpage]);
+
+ if (l > 9) /* smaller font, if no space for one half space at front and end */
+ SetFontWidth(oldfontwidth * 10 / (l+1));
+
+ FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY+yoffset, width+(m_RenderInfo.Width%4), m_RenderInfo.FontHeight, GetColorRGB((enumTeletextColor)Text_AtrTable[ATR_L250 + column].bg));
+ m_RenderInfo.PosX += ((width) - (l*m_RenderInfo.FontWidth+l*m_RenderInfo.FontWidth/abx))/2; /* center */
+
+ for (char *p = m_txtCache->ADIPTable[linkpage]; *p; p++)
+ RenderCharBB(*p, &Text_AtrTable[ATR_L250 + column]);
+
+ SetFontWidth(oldfontwidth);
+ }
+ else /* display number */
+ {
+ m_RenderInfo.PosX = column*width;
+ FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY+yoffset, m_RenderInfo.Width-m_RenderInfo.PosX, m_RenderInfo.FontHeight, GetColorRGB((enumTeletextColor)Text_AtrTable[ATR_L250 + column].bg));
+ if (linkpage < m_txtCache->Page)
+ {
+ line[6] = '<';
+ CDVDTeletextTools::Hex2Str((char*)line + 5, linkpage);
+ }
+ else
+ CDVDTeletextTools::Hex2Str((char*)line + 6, linkpage);
+
+ for (unsigned char *p = line; p < line+9; p++)
+ RenderCharBB(*p, &Text_AtrTable[ATR_L250 + column]);
+ }
+}
+
+void CTeletextDecoder::CreateLine25()
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ /* btt completely received and not yet decoded */
+ if (!m_txtCache->BTTok)
+ Decode_BTT();
+
+ if (m_txtCache->ADIP_PgMax >= 0)
+ Decode_ADIP();
+
+ if (!m_RenderInfo.ShowHex && m_RenderInfo.ShowFlof &&
+ (m_txtCache->FlofPages[m_txtCache->Page][0] || m_txtCache->FlofPages[m_txtCache->Page][1] || m_txtCache->FlofPages[m_txtCache->Page][2] || m_txtCache->FlofPages[m_txtCache->Page][3])) // FLOF-Navigation present
+ {
+ m_RenderInfo.Prev_100 = m_txtCache->FlofPages[m_txtCache->Page][0];
+ m_RenderInfo.Prev_10 = m_txtCache->FlofPages[m_txtCache->Page][1];
+ m_RenderInfo.Next_10 = m_txtCache->FlofPages[m_txtCache->Page][2];
+ m_RenderInfo.Next_100 = m_txtCache->FlofPages[m_txtCache->Page][3];
+
+ m_RenderInfo.PosY = 24*m_RenderInfo.FontHeight;
+ m_RenderInfo.PosX = 0;
+ for (int i=m_RenderInfo.nofirst; i<40; i++)
+ RenderCharBB(m_RenderInfo.PageChar[24*40 + i], &m_RenderInfo.PageAtrb[24*40 + i]);
+ }
+ else
+ {
+ /* normal: blk-1, grp+1, grp+2, blk+1 */
+ /* hex: hex+1, blk-1, grp+1, blk+1 */
+ if (m_RenderInfo.ShowHex)
+ {
+ /* arguments: startpage, up, findgroup */
+ m_RenderInfo.Prev_100 = NextHex(m_txtCache->Page);
+ m_RenderInfo.Prev_10 = TopText_GetNext(m_txtCache->Page, 0, 0);
+ m_RenderInfo.Next_10 = TopText_GetNext(m_txtCache->Page, 1, 1);
+ }
+ else
+ {
+ m_RenderInfo.Prev_100 = TopText_GetNext(m_txtCache->Page, 0, 0);
+ m_RenderInfo.Prev_10 = TopText_GetNext(m_txtCache->Page, 1, 1);
+ m_RenderInfo.Next_10 = TopText_GetNext(m_RenderInfo.Prev_10, 1, 1);
+ }
+ m_RenderInfo.Next_100 = TopText_GetNext(m_RenderInfo.Next_10, 1, 0);
+ Showlink(0, m_RenderInfo.Prev_100);
+ Showlink(1, m_RenderInfo.Prev_10);
+ Showlink(2, m_RenderInfo.Next_10);
+ Showlink(3, m_RenderInfo.Next_100);
+ }
+}
+
+void CTeletextDecoder::RenderCharFB(int Char, TextPageAttr_t *Attribute)
+{
+ RenderCharIntern(&m_RenderInfo, Char, Attribute, m_RenderInfo.ZoomMode, m_YOffset);
+}
+
+void CTeletextDecoder::RenderCharBB(int Char, TextPageAttr_t *Attribute)
+{
+ RenderCharIntern(&m_RenderInfo, Char, Attribute, 0, m_RenderInfo.Height-m_YOffset);
+}
+
+void CTeletextDecoder::CopyBB2FB()
+{
+ UTILS::COLOR::Color *src, *dst, *topsrc;
+ int screenwidth;
+ UTILS::COLOR::Color fillcolor;
+
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ /* line 25 */
+ if (!m_RenderInfo.PageCatching)
+ CreateLine25();
+
+ /* copy backbuffer to framebuffer */
+ if (!m_RenderInfo.ZoomMode)
+ {
+ if (m_YOffset)
+ m_YOffset = 0;
+ else
+ m_YOffset = m_RenderInfo.Height;
+
+ if (m_RenderInfo.ClearBBColor >= 0)
+ {
+ m_RenderInfo.ClearBBColor = -1;
+ }
+ return;
+ }
+
+ src = dst = topsrc = m_TextureBuffer + m_RenderInfo.Width;
+
+ if (m_YOffset)
+ {
+ dst += m_RenderInfo.Width * m_RenderInfo.Height;
+ }
+ else
+ {
+ src += m_RenderInfo.Width * m_RenderInfo.Height;
+ topsrc += m_RenderInfo.Width * m_RenderInfo.Height;
+ }
+
+ if (!m_RenderInfo.PageCatching)
+ SDL_memcpy4(dst+(24*m_RenderInfo.FontHeight)*m_RenderInfo.Width, src + (24*m_RenderInfo.FontHeight)*m_RenderInfo.Width, m_RenderInfo.Width*m_RenderInfo.FontHeight); /* copy line25 in normal height */
+
+ if (m_RenderInfo.TranspMode)
+ fillcolor = GetColorRGB(TXT_ColorTransp);
+ else
+ fillcolor = GetColorRGB((enumTeletextColor)m_txtCache->FullScrColor);
+
+ if (m_RenderInfo.ZoomMode == 2)
+ src += 12*m_RenderInfo.FontHeight*m_RenderInfo.Width;
+
+ screenwidth = m_RenderInfo.Width;
+
+ for (int i = 12*m_RenderInfo.FontHeight; i; i--)
+ {
+ SDL_memcpy4(dst, src, screenwidth);
+ dst += m_RenderInfo.Width;
+ SDL_memcpy4(dst, src, screenwidth);
+ dst += m_RenderInfo.Width;
+ src += m_RenderInfo.Width;
+ }
+
+ for (int i = m_RenderInfo.Height - 25*m_RenderInfo.FontHeight; i >= 0;i--)
+ {
+ SDL_memset4(dst + m_RenderInfo.Width*(m_RenderInfo.FontHeight+i), fillcolor, screenwidth);
+ }
+}
+
+FT_Error CTeletextDecoder::MyFaceRequester(FTC_FaceID face_id, FT_Library library, FT_Pointer request_data, FT_Face *aface)
+{
+ FT_Error result = FT_New_Face(library, (const char*)face_id, 0, aface);
+
+ if (!result)
+ CLog::Log(LOGINFO, "Teletext font {} loaded", (char*)face_id);
+ else
+ CLog::Log(LOGERROR, "Opening of Teletext font {} failed", (char*)face_id);
+
+ return result;
+}
+
+void CTeletextDecoder::SetFontWidth(int newWidth)
+{
+ if (m_RenderInfo.FontWidth != newWidth)
+ {
+ m_RenderInfo.FontWidth = newWidth;
+ m_TypeTTF.width = (FT_UShort) m_RenderInfo.FontWidth;
+
+ for (int i = 0; i <= 12; i++)
+ m_RenderInfo.axdrcs[i] = (m_RenderInfo.FontWidth * i + 6) / 12;
+ }
+}
+
+int CTeletextDecoder::GetCurFontWidth()
+{
+ int mx = (m_RenderInfo.Width)%(40-m_RenderInfo.nofirst); // # of unused pixels
+ int abx = (mx == 0 ? m_RenderInfo.Width+1 : (m_RenderInfo.Width)/(mx+1)); // distance between 'inserted' pixels
+ int nx = abx+1-(m_RenderInfo.PosX % (abx+1)); // # of pixels to next insert
+ return m_RenderInfo.FontWidth+(((m_RenderInfo.PosX+m_RenderInfo.FontWidth+1) <= m_RenderInfo.Width && nx <= m_RenderInfo.FontWidth+1) ? 1 : 0);
+}
+
+void CTeletextDecoder::SetPosX(int column)
+{
+ m_RenderInfo.PosX = 0;
+
+ for (int i = 0; i < column-m_RenderInfo.nofirst; i++)
+ m_RenderInfo.PosX += GetCurFontWidth();
+}
+
+void CTeletextDecoder::ClearBB(UTILS::COLOR::Color Color)
+{
+ SDL_memset4(m_TextureBuffer + (m_RenderInfo.Height-m_YOffset)*m_RenderInfo.Width, Color, m_RenderInfo.Width*m_RenderInfo.Height);
+}
+
+void CTeletextDecoder::ClearFB(UTILS::COLOR::Color Color)
+{
+ SDL_memset4(m_TextureBuffer + m_RenderInfo.Width*m_YOffset, Color, m_RenderInfo.Width*m_RenderInfo.Height);
+}
+
+void CTeletextDecoder::FillBorder(UTILS::COLOR::Color Color)
+{
+ FillRect(m_TextureBuffer + (m_RenderInfo.Height-m_YOffset)*m_RenderInfo.Width, m_RenderInfo.Width, 0, 25*m_RenderInfo.FontHeight, m_RenderInfo.Width, m_RenderInfo.Height-(25*m_RenderInfo.FontHeight), Color);
+ FillRect(m_TextureBuffer + m_RenderInfo.Width*m_YOffset, m_RenderInfo.Width, 0, 25*m_RenderInfo.FontHeight, m_RenderInfo.Width, m_RenderInfo.Height-(25*m_RenderInfo.FontHeight), Color);
+}
+
+void CTeletextDecoder::FillRect(
+ UTILS::COLOR::Color* buffer, int xres, int x, int y, int w, int h, UTILS::COLOR::Color Color)
+{
+ if (!buffer) return;
+
+ UTILS::COLOR::Color* p = buffer + x + y * xres;
+
+ if (w > 0)
+ {
+ for ( ; h > 0 ; h--)
+ {
+ SDL_memset4(p, Color, w);
+ p += xres;
+ }
+ }
+}
+
+void CTeletextDecoder::DrawVLine(
+ UTILS::COLOR::Color* lfb, int xres, int x, int y, int l, UTILS::COLOR::Color color)
+{
+ if (!lfb) return;
+ UTILS::COLOR::Color* p = lfb + x + y * xres;
+
+ for ( ; l > 0 ; l--)
+ {
+ *p = color;
+ p += xres;
+ }
+}
+
+void CTeletextDecoder::DrawHLine(
+ UTILS::COLOR::Color* lfb, int xres, int x, int y, int l, UTILS::COLOR::Color color)
+{
+ if (!lfb) return;
+ if (l > 0)
+ SDL_memset4(lfb + x + y * xres, color, l);
+}
+
+void CTeletextDecoder::RenderDRCS(
+ int xres,
+ unsigned char* s, /* pointer to char data, parity undecoded */
+ UTILS::COLOR::Color* d, /* pointer to frame buffer of top left pixel */
+ unsigned char* ax, /* array[0..12] of x-offsets, array[0..10] of y-offsets for each pixel */
+ UTILS::COLOR::Color fgcolor,
+ UTILS::COLOR::Color bgcolor)
+{
+ if (d == NULL) return;
+
+ unsigned char *ay = ax + 13; /* array[0..10] of y-offsets for each pixel */
+
+ for (int y = 0; y < 10; y++) /* 10*2 bytes a 6 pixels per char definition */
+ {
+ unsigned char c1 = deparity[*s++];
+ unsigned char c2 = deparity[*s++];
+ int h = ay[y+1] - ay[y];
+
+ if (!h)
+ continue;
+ if (((c1 == ' ') && (*(s-2) != ' ')) || ((c2 == ' ') && (*(s-1) != ' '))) /* parity error: stop decoding FIXME */
+ return;
+ for (int bit = 0x20, x = 0;
+ bit;
+ bit >>= 1, x++) /* bit mask (MSB left), column counter */
+ {
+ UTILS::COLOR::Color f1 = (c1 & bit) ? fgcolor : bgcolor;
+ UTILS::COLOR::Color f2 = (c2 & bit) ? fgcolor : bgcolor;
+ for (int i = 0; i < h; i++)
+ {
+ if (ax[x+1] > ax[x])
+ SDL_memset4(d + ax[x], f1, ax[x+1] - ax[x]);
+ if (ax[x+7] > ax[x+6])
+ SDL_memset4(d + ax[x+6], f2, ax[x+7] - ax[x+6]); /* 2nd byte 6 pixels to the right */
+ d += xres;
+ }
+ d -= h * xres;
+ }
+ d += h * xres;
+ }
+}
+
+void CTeletextDecoder::FillRectMosaicSeparated(UTILS::COLOR::Color* lfb,
+ int xres,
+ int x,
+ int y,
+ int w,
+ int h,
+ UTILS::COLOR::Color fgcolor,
+ UTILS::COLOR::Color bgcolor,
+ int set)
+{
+ if (!lfb) return;
+ FillRect(lfb,xres,x, y, w, h, bgcolor);
+ if (set)
+ {
+ FillRect(lfb,xres,x+1, y+1, w-2, h-2, fgcolor);
+ }
+}
+
+void CTeletextDecoder::FillTrapez(UTILS::COLOR::Color* lfb,
+ int xres,
+ int x0,
+ int y0,
+ int l0,
+ int xoffset1,
+ int h,
+ int l1,
+ UTILS::COLOR::Color color)
+{
+ UTILS::COLOR::Color* p = lfb + x0 + y0 * xres;
+ int xoffset, l;
+
+ for (int yoffset = 0; yoffset < h; yoffset++)
+ {
+ l = l0 + ((l1-l0) * yoffset + h/2) / h;
+ xoffset = (xoffset1 * yoffset + h/2) / h;
+ if (l > 0)
+ SDL_memset4(p + xoffset, color, l);
+ p += xres;
+ }
+}
+
+void CTeletextDecoder::FlipHorz(UTILS::COLOR::Color* lfb, int xres, int x, int y, int w, int h)
+{
+ UTILS::COLOR::Color buf[2048];
+ UTILS::COLOR::Color* p = lfb + x + y * xres;
+ int w1,h1;
+
+ for (h1 = 0 ; h1 < h ; h1++)
+ {
+ SDL_memcpy4(buf,p,w);
+ for (w1 = 0 ; w1 < w ; w1++)
+ {
+ *(p+w1) = buf[w-(w1+1)];
+ }
+ p += xres;
+ }
+}
+
+void CTeletextDecoder::FlipVert(UTILS::COLOR::Color* lfb, int xres, int x, int y, int w, int h)
+{
+ UTILS::COLOR::Color buf[2048];
+ UTILS::COLOR::Color *p = lfb + x + y * xres, *p1, *p2;
+ int h1;
+
+ for (h1 = 0 ; h1 < h/2 ; h1++)
+ {
+ p1 = (p+(h1*xres));
+ p2 = (p+(h-(h1+1))*xres);
+ SDL_memcpy4(buf, p1, w);
+ SDL_memcpy4(p1, p2, w);
+ SDL_memcpy4(p2, buf, w);
+ }
+}
+
+int CTeletextDecoder::ShapeCoord(int param, int curfontwidth, int curFontHeight)
+{
+ switch (param)
+ {
+ case S_W13:
+ return curfontwidth/3;
+ case S_W12:
+ return curfontwidth/2;
+ case S_W23:
+ return curfontwidth*2/3;
+ case S_W11:
+ return curfontwidth;
+ case S_WM3:
+ return curfontwidth-3;
+ case S_H13:
+ return curFontHeight/3;
+ case S_H12:
+ return curFontHeight/2;
+ case S_H23:
+ return curFontHeight*2/3;
+ case S_H11:
+ return curFontHeight;
+ default:
+ return param;
+ }
+}
+
+void CTeletextDecoder::DrawShape(UTILS::COLOR::Color* lfb,
+ int xres,
+ int x,
+ int y,
+ int shapenumber,
+ int curfontwidth,
+ int FontHeight,
+ int curFontHeight,
+ UTILS::COLOR::Color fgcolor,
+ UTILS::COLOR::Color bgcolor,
+ bool clear)
+{
+ if (!lfb || shapenumber < 0x20 || shapenumber > 0x7e || (shapenumber == 0x7e && clear))
+ return;
+
+ unsigned char *p = aShapes[shapenumber - 0x20];
+
+ if (*p == S_INV)
+ {
+ int t = fgcolor;
+ fgcolor = bgcolor;
+ bgcolor = t;
+ p++;
+ }
+
+ if (clear)
+ FillRect(lfb, xres, x, y, curfontwidth, FontHeight, bgcolor);
+
+ while (*p != S_END)
+ {
+ switch (*p++)
+ {
+ case S_FHL:
+ {
+ int offset = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ DrawHLine(lfb, xres, x, y + offset, curfontwidth, fgcolor);
+ break;
+ }
+ case S_FVL:
+ {
+ int offset = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ DrawVLine(lfb,xres,x + offset, y, FontHeight, fgcolor);
+ break;
+ }
+ case S_FLH:
+ FlipHorz(lfb,xres,x,y,curfontwidth, FontHeight);
+ break;
+ case S_FLV:
+ FlipVert(lfb,xres,x,y,curfontwidth, FontHeight);
+ break;
+ case S_BOX:
+ {
+ int xo = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int yo = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int w = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int h = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ FillRect(lfb,xres,x + xo, y + yo, w, h, fgcolor);
+ break;
+ }
+ case S_TRA:
+ {
+ int x0 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int y0 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int l0 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int x1 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int y1 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int l1 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ FillTrapez(lfb, xres,x + x0, y + y0, l0, x1-x0, y1-y0, l1, fgcolor);
+ break;
+ }
+ case S_BTR:
+ {
+ int x0 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int y0 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int l0 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int x1 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int y1 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ int l1 = ShapeCoord(*p++, curfontwidth, curFontHeight);
+ FillTrapez(lfb, xres, x + x0, y + y0, l0, x1-x0, y1-y0, l1, bgcolor);
+ break;
+ }
+ case S_LNK:
+ {
+ DrawShape(lfb,xres,x, y, ShapeCoord(*p, curfontwidth, curFontHeight), curfontwidth, FontHeight, curFontHeight, fgcolor, bgcolor, false);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+}
+
+void CTeletextDecoder::RenderCharIntern(TextRenderInfo_t* RenderInfo, int Char, TextPageAttr_t *Attribute, int zoom, int yoffset)
+{
+ int Row, Pitch;
+ int glyph;
+ UTILS::COLOR::Color bgcolor, fgcolor;
+ int factor, xfactor;
+ unsigned char *sbitbuffer;
+
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ int national_subset_local = m_txtCache->NationalSubset;
+ int curfontwidth = GetCurFontWidth();
+ int t = curfontwidth;
+ m_RenderInfo.PosX += t;
+ int curfontwidth2 = GetCurFontWidth();
+ m_RenderInfo.PosX -= t;
+ int alphachar = RenderChar(m_TextureBuffer+(yoffset)*m_RenderInfo.Width, m_RenderInfo.Width, Char, &m_RenderInfo.PosX, m_RenderInfo.PosY, Attribute, zoom > 0, curfontwidth, curfontwidth2, m_RenderInfo.FontHeight, m_RenderInfo.TranspMode, m_RenderInfo.axdrcs, m_Ascender);
+ if (alphachar <= 0) return;
+
+ if (zoom && Attribute->doubleh)
+ factor = 4;
+ else if (zoom || Attribute->doubleh)
+ factor = 2;
+ else
+ factor = 1;
+
+ fgcolor = GetColorRGB((enumTeletextColor)Attribute->fg);
+ if (m_RenderInfo.TranspMode && m_RenderInfo.PosY < 24*m_RenderInfo.FontHeight)
+ {
+ bgcolor = GetColorRGB(TXT_ColorTransp);
+ }
+ else
+ {
+ bgcolor = GetColorRGB((enumTeletextColor)Attribute->bg);
+ }
+
+ if (Attribute->doublew)
+ {
+ curfontwidth += curfontwidth2;
+ xfactor = 2;
+ }
+ else
+ xfactor = 1;
+
+ // Check if the alphanumeric char has diacritical marks (or results from composing chars) or
+ // on the other hand it is just a simple alphanumeric char
+ if (!Attribute->diacrit)
+ {
+ Char = alphachar;
+ }
+ else
+ {
+ if ((national_subset_local == NAT_SC) || (national_subset_local == NAT_RB) ||
+ (national_subset_local == NAT_UA))
+ Char = G2table[1][0x20 + Attribute->diacrit];
+ else if (national_subset_local == NAT_GR)
+ Char = G2table[2][0x20 + Attribute->diacrit];
+ else if (national_subset_local == NAT_HB)
+ Char = G2table[3][0x20 + Attribute->diacrit];
+ else if (national_subset_local == NAT_AR)
+ Char = G2table[4][0x20 + Attribute->diacrit];
+ else
+ Char = G2table[0][0x20 + Attribute->diacrit];
+
+ // use harfbuzz to combine the diacritical mark with the alphanumeric char
+ // fallback to the alphanumeric char if composition fails
+ hb_unicode_funcs_t* ufuncs = hb_unicode_funcs_get_default();
+ hb_codepoint_t composedChar;
+ const hb_bool_t isComposed = hb_unicode_compose(ufuncs, alphachar, Char, &composedChar);
+ Char = isComposed ? composedChar : alphachar;
+ }
+
+ /* render char */
+ if (!(glyph = FT_Get_Char_Index(m_Face, Char)))
+ {
+ CLog::Log(LOGERROR, "{}: <FT_Get_Char_Index for Char {:x} \"{}\" failed", __FUNCTION__,
+ alphachar, alphachar);
+
+ FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY + yoffset, curfontwidth, factor*m_RenderInfo.FontHeight, bgcolor);
+ m_RenderInfo.PosX += curfontwidth;
+ return;
+ }
+
+ if (FTC_SBitCache_Lookup(m_Cache, &m_TypeTTF, glyph, &m_sBit, &m_anode) != 0)
+ {
+ FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY + yoffset, curfontwidth, m_RenderInfo.FontHeight, bgcolor);
+ m_RenderInfo.PosX += curfontwidth;
+ return;
+ }
+
+ sbitbuffer = m_sBit->buffer;
+
+ int backupTTFshiftY = m_RenderInfo.TTFShiftY;
+ if (national_subset_local == NAT_AR)
+ m_RenderInfo.TTFShiftY = backupTTFshiftY - 2; // for arabic TTF font should be shifted up slightly
+
+ UTILS::COLOR::Color* p;
+ int f; /* running counter for zoom factor */
+ int he = m_sBit->height; // sbit->height should not be altered, I guess
+ Row = factor * (m_Ascender - m_sBit->top + m_RenderInfo.TTFShiftY);
+ if (Row < 0)
+ {
+ sbitbuffer -= m_sBit->pitch*Row;
+ he += Row;
+ Row = 0;
+ }
+ else
+ {
+ FillRect(m_TextureBuffer, m_RenderInfo.Width, m_RenderInfo.PosX, m_RenderInfo.PosY + yoffset, curfontwidth, Row, bgcolor); /* fill upper margin */
+ }
+
+ if (m_Ascender - m_sBit->top + m_RenderInfo.TTFShiftY + he > m_RenderInfo.FontHeight)
+ he = m_RenderInfo.FontHeight - m_Ascender + m_sBit->top - m_RenderInfo.TTFShiftY; /* limit char height to defined/calculated FontHeight */
+ if (he < 0) he = m_RenderInfo.FontHeight;
+
+ p = m_TextureBuffer + m_RenderInfo.PosX + (yoffset + m_RenderInfo.PosY + Row) * m_RenderInfo.Width; /* running pointer into framebuffer */
+ for (Row = he; Row; Row--) /* row counts up, but down may be a little faster :) */
+ {
+ int pixtodo = m_sBit->width;
+ UTILS::COLOR::Color* pstart = p;
+
+ for (int Bit = xfactor * (m_sBit->left + m_RenderInfo.TTFShiftX); Bit > 0; Bit--) /* fill left margin */
+ {
+ for (f = factor-1; f >= 0; f--)
+ *(p + f*m_RenderInfo.Width) = bgcolor;
+ p++;
+ }
+
+ for (Pitch = m_sBit->pitch; Pitch; Pitch--)
+ {
+ for (int Bit = 0x80; Bit; Bit >>= 1)
+ {
+ UTILS::COLOR::Color color;
+
+ if (--pixtodo < 0)
+ break;
+
+ if (*sbitbuffer & Bit) /* bit set -> foreground */
+ color = fgcolor;
+ else /* bit not set -> background */
+ color = bgcolor;
+
+ for (f = factor-1; f >= 0; f--)
+ *(p + f*m_RenderInfo.Width) = color;
+ p++;
+
+ if (xfactor > 1) /* double width */
+ {
+ for (f = factor-1; f >= 0; f--)
+ *(p + f*m_RenderInfo.Width) = color;
+ p++;
+ }
+ }
+ sbitbuffer++;
+ }
+ for (int Bit = (curfontwidth - xfactor*(m_sBit->width + m_sBit->left + m_RenderInfo.TTFShiftX));
+ Bit > 0; Bit--) /* fill rest of char width */
+ {
+ for (f = factor-1; f >= 0; f--)
+ *(p + f*m_RenderInfo.Width) = bgcolor;
+ p++;
+ }
+
+ p = pstart + factor*m_RenderInfo.Width;
+ }
+
+ Row = m_Ascender - m_sBit->top + he + m_RenderInfo.TTFShiftY;
+ FillRect(m_TextureBuffer,
+ m_RenderInfo.Width,
+ m_RenderInfo.PosX,
+ m_RenderInfo.PosY + yoffset + Row * factor,
+ curfontwidth,
+ (m_RenderInfo.FontHeight - Row) * factor,
+ bgcolor); /* fill lower margin */
+
+ if (Attribute->underline)
+ FillRect(m_TextureBuffer,
+ m_RenderInfo.Width,
+ m_RenderInfo.PosX,
+ m_RenderInfo.PosY + yoffset + (m_RenderInfo.FontHeight-2)* factor,
+ curfontwidth,
+ 2*factor,
+ fgcolor); /* underline char */
+
+ m_RenderInfo.PosX += curfontwidth;
+ m_RenderInfo.TTFShiftY = backupTTFshiftY; // restore TTFShiftY
+}
+
+int CTeletextDecoder::RenderChar(
+ UTILS::COLOR::Color* buffer, // pointer to render buffer, min. FontHeight*2*xres
+ int xres, // length of 1 line in render buffer
+ int Char, // character to render
+ int*
+ pPosX, // left border for rendering relative to *buffer, will be set to right border after rendering
+ int PosY, // vertical position of char in *buffer
+ TextPageAttr_t* Attribute, // Attributes of Char
+ bool zoom, // 1= character will be rendered in double height
+ int curfontwidth, // rendering width of character
+ int curfontwidth2, // rendering width of next character (needed for doublewidth)
+ int FontHeight, // height of character
+ bool transpmode, // 1= transparent display
+ unsigned char* axdrcs, // width and height of DRCS-chars
+ int Ascender) // Ascender of font
+{
+ UTILS::COLOR::Color bgcolor, fgcolor;
+ int factor, xfactor;
+
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ int national_subset_local = m_txtCache->NationalSubset;
+ int ymosaic[4];
+ ymosaic[0] = 0; /* y-offsets for 2*3 mosaic */
+ ymosaic[1] = (FontHeight + 1) / 3;
+ ymosaic[2] = (FontHeight * 2 + 1) / 3;
+ ymosaic[3] = FontHeight;
+
+ if (Attribute->setX26)
+ {
+ national_subset_local = 0; // no national subset
+ }
+
+ // G0+G2 set designation
+ if (Attribute->setG0G2 != 0x3f)
+ {
+ switch (Attribute->setG0G2)
+ {
+ case 0x20 :
+ national_subset_local = NAT_SC;
+ break;
+ case 0x24 :
+ national_subset_local = NAT_RB;
+ break;
+ case 0x25 :
+ national_subset_local = NAT_UA;
+ break;
+ case 0x37:
+ national_subset_local = NAT_GR;
+ break;
+ case 0x55:
+ national_subset_local = NAT_HB;
+ break;
+ case 0x47:
+ case 0x57:
+ national_subset_local = NAT_AR;
+ break;
+ default:
+ national_subset_local = CountryConversionTable[Attribute->setG0G2 & 0x07];
+ break;
+ }
+ }
+
+ if (Attribute->charset == C_G0S) // use secondary charset
+ national_subset_local = m_txtCache->NationalSubsetSecondary;
+ if (zoom && Attribute->doubleh)
+ factor = 4;
+ else if (zoom || Attribute->doubleh)
+ factor = 2;
+ else
+ factor = 1;
+
+ if (Attribute->doublew)
+ {
+ curfontwidth += curfontwidth2;
+ xfactor = 2;
+ }
+ else
+ xfactor = 1;
+
+ if (Char == 0xFF) /* skip doubleheight chars in lower line */
+ {
+ *pPosX += curfontwidth;
+ return -1;
+ }
+
+ /* get colors */
+ if (Attribute->inverted)
+ {
+ int t = Attribute->fg;
+ Attribute->fg = Attribute->bg;
+ Attribute->bg = t;
+ }
+ fgcolor = GetColorRGB((enumTeletextColor)Attribute->fg);
+ if (transpmode == true && PosY < 24*FontHeight)
+ {
+ bgcolor = GetColorRGB(TXT_ColorTransp);
+ }
+ else
+ {
+ bgcolor = GetColorRGB((enumTeletextColor)Attribute->bg);
+ }
+
+ /* handle mosaic */
+ if ((Attribute->charset == C_G1C || Attribute->charset == C_G1S) &&
+ ((Char&0xA0) == 0x20))
+ {
+ int w1 = (curfontwidth / 2 ) *xfactor;
+ int w2 = (curfontwidth - w1) *xfactor;
+
+ Char = (Char & 0x1f) | ((Char & 0x40) >> 1);
+ if (Attribute->charset == C_G1S) /* separated mosaic */
+ {
+ for (int y = 0; y < 3; y++)
+ {
+ FillRectMosaicSeparated(buffer, xres,*pPosX, PosY + ymosaic[y]*factor, w1, (ymosaic[y+1] - ymosaic[y])*factor, fgcolor, bgcolor, Char & 0x01);
+ FillRectMosaicSeparated(buffer, xres,*pPosX + w1, PosY + ymosaic[y]*factor, w2, (ymosaic[y+1] - ymosaic[y])*factor, fgcolor, bgcolor, Char & 0x02);
+ Char >>= 2;
+ }
+ }
+ else
+ {
+ for (int y = 0; y < 3; y++)
+ {
+ FillRect(buffer, xres, *pPosX, PosY + ymosaic[y]*factor, w1, (ymosaic[y+1] - ymosaic[y])*factor, (Char & 0x01) ? fgcolor : bgcolor);
+ FillRect(buffer, xres, *pPosX + w1, PosY + ymosaic[y]*factor, w2, (ymosaic[y+1] - ymosaic[y])*factor, (Char & 0x02) ? fgcolor : bgcolor);
+ Char >>= 2;
+ }
+ }
+
+ *pPosX += curfontwidth;
+ return 0;
+ }
+
+ if (Attribute->charset == C_G3)
+ {
+ if (Char < 0x20 || Char > 0x7d)
+ {
+ Char = 0x20;
+ }
+ else
+ {
+ if (*aShapes[Char - 0x20] == S_CHR)
+ {
+ unsigned char *p = aShapes[Char - 0x20];
+ Char = (*(p+1) <<8) + (*(p+2));
+ }
+ else if (*aShapes[Char - 0x20] == S_ADT)
+ {
+ if (buffer)
+ {
+ int x,y,f,c;
+ UTILS::COLOR::Color* p = buffer + *pPosX + PosY * xres;
+ for (y=0; y<FontHeight;y++)
+ {
+ for (f=0; f<factor; f++)
+ {
+ for (x=0; x<curfontwidth*xfactor;x++)
+ {
+ c = (y&4 ? (x/3)&1 :((x+3)/3)&1);
+ *(p+x) = (c ? fgcolor : bgcolor);
+ }
+ p += xres;
+ }
+ }
+ }
+ *pPosX += curfontwidth;
+ return 0;
+ }
+ else
+ {
+ DrawShape(buffer, xres,*pPosX, PosY, Char, curfontwidth, FontHeight, factor*FontHeight, fgcolor, bgcolor, true);
+ *pPosX += curfontwidth;
+ return 0;
+ }
+ }
+ }
+ else if (Attribute->charset >= C_OFFSET_DRCS)
+ {
+ TextCachedPage_t *pcache = m_txtCache->astCachetable[(Attribute->charset & 0x10) ? m_txtCache->drcs : m_txtCache->gdrcs][Attribute->charset & 0x0f];
+ if (pcache)
+ {
+ unsigned char drcs_data[23*40];
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->LoadPage((Attribute->charset & 0x10) ? m_txtCache->drcs : m_txtCache->gdrcs,
+ Attribute->charset & 0x0f, drcs_data);
+ unsigned char *p;
+ if (Char < 23*2)
+ p = drcs_data + 20*Char;
+ else if (pcache->pageinfo.p24)
+ p = pcache->pageinfo.p24 + 20*(Char - 23*2);
+ else
+ {
+ FillRect(buffer, xres,*pPosX, PosY, curfontwidth, factor*FontHeight, bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ }
+ axdrcs[12] = curfontwidth; /* adjust last x-offset according to position, FIXME: double width */
+ RenderDRCS(xres, p, buffer + *pPosX + PosY * xres, axdrcs, fgcolor, bgcolor);
+ }
+ else
+ {
+ FillRect(buffer,xres,*pPosX, PosY, curfontwidth, factor*FontHeight, bgcolor);
+ }
+ *pPosX += curfontwidth;
+ return 0;
+ }
+ else if (Attribute->charset == C_G2 && Char >= 0x20 && Char <= 0x7F)
+ {
+ if ((national_subset_local == NAT_SC) || (national_subset_local == NAT_RB) || (national_subset_local == NAT_UA))
+ Char = G2table[1][Char-0x20];
+ else if (national_subset_local == NAT_GR)
+ Char = G2table[2][Char-0x20];
+ else if (national_subset_local == NAT_AR)
+ Char = G2table[3][Char-0x20];
+ else
+ Char = G2table[0][Char-0x20];
+
+ //if (Char == 0x7F)
+ //{
+ // FillRect(buffer,xres,*pPosX, PosY, curfontwidth, factor*Ascender, fgcolor);
+ // FillRect(buffer,xres,*pPosX, PosY + factor*Ascender, curfontwidth, factor*(FontHeight-Ascender), bgcolor);
+ // *pPosX += curfontwidth;
+ // return 0;
+ //}
+ }
+ else if (national_subset_local == NAT_SC && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for serbian/croatian */
+ Char = G0table[0][Char-0x20];
+ else if (national_subset_local == NAT_RB && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for russian/bulgarian */
+ Char = G0table[1][Char-0x20];
+ else if (national_subset_local == NAT_UA && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for ukrainian */
+ Char = G0table[2][Char-0x20];
+ else if (national_subset_local == NAT_GR && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for greek */
+ Char = G0table[3][Char-0x20];
+ else if (national_subset_local == NAT_HB && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for hebrew */
+ Char = G0table[4][Char-0x20];
+ else if (national_subset_local == NAT_AR && Char >= 0x20 && Char <= 0x7F) /* remap complete areas for arabic */
+ Char = G0table[5][Char-0x20];
+ else
+ {
+ /* load char */
+ switch (Char)
+ {
+ case 0x00:
+ case 0x20:
+ FillRect(buffer, xres, *pPosX, PosY, curfontwidth, factor*FontHeight, bgcolor);
+ *pPosX += curfontwidth;
+ return -3;
+ case 0x23:
+ case 0x24:
+ Char = nationaltable23[national_subset_local][Char-0x23];
+ break;
+ case 0x40:
+ Char = nationaltable40[national_subset_local];
+ break;
+ case 0x5B:
+ case 0x5C:
+ case 0x5D:
+ case 0x5E:
+ case 0x5F:
+ case 0x60:
+ Char = nationaltable5b[national_subset_local][Char-0x5B];
+ break;
+ case 0x7B:
+ case 0x7C:
+ case 0x7D:
+ case 0x7E:
+ Char = nationaltable7b[national_subset_local][Char-0x7B];
+ break;
+ case 0x7F:
+ FillRect(buffer,xres,*pPosX, PosY , curfontwidth, factor*Ascender, fgcolor);
+ FillRect(buffer,xres,*pPosX, PosY + factor*Ascender, curfontwidth, factor*(FontHeight-Ascender), bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xE0: /* |- */
+ DrawHLine(buffer,xres,*pPosX, PosY, curfontwidth, fgcolor);
+ DrawVLine(buffer,xres,*pPosX, PosY +1, FontHeight -1, fgcolor);
+ FillRect(buffer,xres,*pPosX +1, PosY +1, curfontwidth-1, FontHeight-1, bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xE1: /* - */
+ DrawHLine(buffer,xres,*pPosX, PosY, curfontwidth, fgcolor);
+ FillRect(buffer,xres,*pPosX, PosY +1, curfontwidth, FontHeight-1, bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xE2: /* -| */
+ DrawHLine(buffer,xres,*pPosX, PosY, curfontwidth, fgcolor);
+ DrawVLine(buffer,xres,*pPosX + curfontwidth -1, PosY +1, FontHeight -1, fgcolor);
+ FillRect(buffer,xres,*pPosX, PosY +1, curfontwidth-1, FontHeight-1, bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xE3: /* | */
+ DrawVLine(buffer,xres,*pPosX, PosY, FontHeight, fgcolor);
+ FillRect(buffer,xres,*pPosX +1, PosY, curfontwidth -1, FontHeight, bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xE4: /* | */
+ DrawVLine(buffer,xres,*pPosX + curfontwidth -1, PosY, FontHeight, fgcolor);
+ FillRect(buffer,xres,*pPosX, PosY, curfontwidth -1, FontHeight, bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xE5: /* |_ */
+ DrawHLine(buffer,xres,*pPosX, PosY + FontHeight -1, curfontwidth, fgcolor);
+ DrawVLine(buffer,xres,*pPosX, PosY, FontHeight -1, fgcolor);
+ FillRect(buffer,xres,*pPosX +1, PosY, curfontwidth-1, FontHeight-1, bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xE6: /* _ */
+ DrawHLine(buffer,xres,*pPosX, PosY + FontHeight -1, curfontwidth, fgcolor);
+ FillRect(buffer,xres,*pPosX, PosY, curfontwidth, FontHeight-1, bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xE7: /* _| */
+ DrawHLine(buffer,xres,*pPosX, PosY + FontHeight -1, curfontwidth, fgcolor);
+ DrawVLine(buffer,xres,*pPosX + curfontwidth -1, PosY, FontHeight -1, fgcolor);
+ FillRect(buffer,xres,*pPosX, PosY, curfontwidth-1, FontHeight-1, bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xE8: /* Ii */
+ FillRect(buffer,xres,*pPosX +1, PosY, curfontwidth -1, FontHeight, bgcolor);
+ for (int Row=0; Row < curfontwidth/2; Row++)
+ DrawVLine(buffer,xres,*pPosX + Row, PosY + Row, FontHeight - Row, fgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xE9: /* II */
+ FillRect(buffer,xres,*pPosX, PosY, curfontwidth/2, FontHeight, fgcolor);
+ FillRect(buffer,xres,*pPosX + curfontwidth/2, PosY, (curfontwidth+1)/2, FontHeight, bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xEA: /* ∞ */
+ FillRect(buffer,xres,*pPosX, PosY, curfontwidth, FontHeight, bgcolor);
+ FillRect(buffer,xres,*pPosX, PosY, curfontwidth/2, curfontwidth/2, fgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xEB: /* ¨ */
+ FillRect(buffer,xres,*pPosX, PosY +1, curfontwidth, FontHeight -1, bgcolor);
+ for (int Row=0; Row < curfontwidth/2; Row++)
+ DrawHLine(buffer,xres,*pPosX + Row, PosY + Row, curfontwidth - Row, fgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xEC: /* -- */
+ FillRect(buffer, xres,*pPosX, PosY, curfontwidth, curfontwidth/2, fgcolor);
+ FillRect(buffer, xres,*pPosX, PosY + curfontwidth/2, curfontwidth, FontHeight - curfontwidth/2, bgcolor);
+ *pPosX += curfontwidth;
+ return 0;
+ case 0xED:
+ case 0xEE:
+ case 0xEF:
+ case 0xF0:
+ case 0xF1:
+ case 0xF2:
+ case 0xF3:
+ case 0xF4:
+ Char = arrowtable[Char - 0xED];
+ break;
+ default:
+ break;
+ }
+ }
+ if (Char <= 0x20)
+ {
+ FillRect(buffer, xres, *pPosX, PosY, curfontwidth, factor*FontHeight, bgcolor);
+ *pPosX += curfontwidth;
+ return -2;
+ }
+ return Char; // Char is an alphanumeric unicode character
+}
+
+TextPageinfo_t* CTeletextDecoder::DecodePage(bool showl25, // 1=decode Level2.5-graphics
+ unsigned char* PageChar, // page buffer, min. 25*40
+ TextPageAttr_t *PageAtrb, // attribute buffer, min 25*40
+ bool HintMode, // 1=show hidden information
+ bool showflof) // 1=decode FLOF-line
+{
+ int col;
+ int hold, dhset;
+ int foreground, background, doubleheight, doublewidth, charset, previous_charset, mosaictype, IgnoreAtBlackBgSubst, concealed, flashmode, boxwin;
+ unsigned char held_mosaic, *p;
+ TextCachedPage_t *pCachedPage;
+
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ /* copy page to decode buffer */
+ if (m_txtCache->SubPageTable[m_txtCache->Page] == 0xff) /* not cached: do nothing */
+ return NULL;
+
+ if (m_txtCache->ZapSubpageManual)
+ pCachedPage = m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPage];
+ else
+ pCachedPage = m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPageTable[m_txtCache->Page]];
+ if (!pCachedPage) /* not cached: do nothing */
+ return nullptr;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->LoadPage(m_txtCache->Page, m_txtCache->SubPage, &PageChar[40]);
+
+ memcpy(&PageChar[8], pCachedPage->p0, 24); /* header line without TimeString */
+
+ TextPageinfo_t* PageInfo = &(pCachedPage->pageinfo);
+ if (PageInfo->p24)
+ memcpy(&PageChar[24*40], PageInfo->p24, 40); /* line 25 for FLOF */
+
+ /* copy TimeString */
+ memcpy(&PageChar[32], &m_txtCache->TimeString, 8);
+
+ bool boxed;
+ /* check for newsflash & subtitle */
+ if (PageInfo->boxed && IsDec(m_txtCache->Page))
+ boxed = true;
+ else
+ boxed = false;
+
+
+ /* modify header */
+ if (boxed)
+ {
+ memset(PageChar, ' ', 40);
+ }
+ else
+ {
+ memset(PageChar, ' ', 8);
+ CDVDTeletextTools::Hex2Str((char*)PageChar+3, m_txtCache->Page);
+ if (m_txtCache->SubPage)
+ {
+ *(PageChar+4) ='/';
+ *(PageChar+5) ='0';
+ CDVDTeletextTools::Hex2Str((char*)PageChar+6, m_txtCache->SubPage);
+ }
+ }
+
+ if (!IsDec(m_txtCache->Page))
+ {
+ TextPageAttr_t atr = { TXT_ColorWhite , TXT_ColorBlack , C_G0P, 0, 0, 0 ,0, 0, 0, 0, 0, 0, 0, 0x3f};
+ if (PageInfo->function == FUNC_MOT) /* magazine organization table */
+ {
+ for (col = 0; col < 24*40; col++)
+ PageAtrb[col] = atr;
+ for (col = 40; col < 24*40; col++)
+ PageChar[col] = number2char(PageChar[col]);
+ return PageInfo; /* don't interpret irregular pages */
+ }
+ else if (PageInfo->function == FUNC_GPOP || PageInfo->function == FUNC_POP) /* object definitions */
+ {
+ for (int col = 0; col < 24*40; col++)
+ PageAtrb[col] = atr;
+
+ p = PageChar + 40;
+ for (int row = 1; row < 12; row++)
+ {
+ *p++ = number2char(row); /* first column: number (0-9, A-..) */
+ for (int col = 1; col < 40; col += 3)
+ {
+ int d = CDVDTeletextTools::deh24(p);
+ if (d < 0)
+ {
+ memcpy(p, "???", 3);
+ p += 3;
+ }
+ else
+ {
+ *p++ = number2char((d >> 6) & 0x1f); /* mode */
+ *p++ = number2char(d & 0x3f); /* address */
+ *p++ = number2char((d >> 11) & 0x7f); /* data */
+ }
+ }
+ }
+ return PageInfo; /* don't interpret irregular pages */
+ }
+ else if (PageInfo->function == FUNC_GDRCS || PageInfo->function == FUNC_DRCS) /* character definitions */
+ {
+ return PageInfo; /* don't interpret irregular pages */
+ }
+ else
+ {
+ int h, parityerror = 0;
+
+ for (int i = 0; i < 8; i++)
+ PageAtrb[i] = atr;
+
+ /* decode parity/hamming */
+ for (unsigned int i = 40; i < TELETEXT_PAGE_SIZE; i++)
+ {
+ PageAtrb[i] = atr;
+ p = PageChar + i;
+ h = dehamming[*p];
+ if (parityerror && h != 0xFF) /* if no regular page (after any parity error) */
+ CDVDTeletextTools::Hex2Str((char*)p, h); /* first try dehamming */
+ else
+ {
+ if (*p == ' ' || deparity[*p] != ' ') /* correct parity */
+ *p &= 127;
+ else
+ {
+ parityerror = 1;
+ if (h != 0xFF) /* first parity error: try dehamming */
+ CDVDTeletextTools::Hex2Str((char*)p, h);
+ else
+ *p = ' ';
+ }
+ }
+ }
+ if (parityerror)
+ {
+ return PageInfo; /* don't interpret irregular pages */
+ }
+ }
+ }
+ int mosaic_pending,esc_pending;
+ /* decode */
+ for (int row = 0; row < ((showflof && PageInfo->p24) ? 25 : 24); row++)
+ {
+ /* start-of-row default conditions */
+ foreground = TXT_ColorWhite;
+ background = TXT_ColorBlack;
+ doubleheight = 0;
+ doublewidth = 0;
+ charset = previous_charset = C_G0P; // remember charset for switching back after mosaic charset was used
+ mosaictype = 0;
+ concealed = 0;
+ flashmode = 0;
+ hold = 0;
+ boxwin = 0;
+ held_mosaic = ' ';
+ dhset = 0;
+ IgnoreAtBlackBgSubst = 0;
+ mosaic_pending = esc_pending = 0; // we need to render at least one mosaic char if 'esc' is received immediately after mosaic charset switch on
+
+ if (boxed && memchr(&PageChar[row*40], start_box, 40) == 0)
+ {
+ foreground = TXT_ColorTransp;
+ background = TXT_ColorTransp;
+ }
+
+ for (int col = 0; col < 40; col++)
+ {
+ int index = row*40 + col;
+
+ PageAtrb[index].fg = foreground;
+ PageAtrb[index].bg = background;
+ PageAtrb[index].charset = charset;
+ PageAtrb[index].doubleh = doubleheight;
+ PageAtrb[index].doublew = (col < 39 ? doublewidth : 0);
+ PageAtrb[index].IgnoreAtBlackBgSubst = IgnoreAtBlackBgSubst;
+ PageAtrb[index].concealed = concealed;
+ PageAtrb[index].flashing = flashmode;
+ PageAtrb[index].boxwin = boxwin;
+ PageAtrb[index].inverted = 0; // only relevant for Level 2.5
+ PageAtrb[index].underline = 0; // only relevant for Level 2.5
+ PageAtrb[index].diacrit = 0; // only relevant for Level 2.5
+ PageAtrb[index].setX26 = 0; // only relevant for Level 2.5
+ PageAtrb[index].setG0G2 = 0x3f; // only relevant for Level 2.5
+
+ if (PageChar[index] < ' ')
+ {
+ if (esc_pending) { // mosaic char has been rendered and we can switch charsets
+ charset = previous_charset;
+ if (charset == C_G0P)
+ charset = previous_charset = C_G0S;
+ else if (charset == C_G0S)
+ charset = previous_charset = C_G0P;
+ esc_pending = 0;
+ }
+ switch (PageChar[index])
+ {
+ case alpha_black:
+ case alpha_red:
+ case alpha_green:
+ case alpha_yellow:
+ case alpha_blue:
+ case alpha_magenta:
+ case alpha_cyan:
+ case alpha_white:
+ concealed = 0;
+ foreground = PageChar[index] - alpha_black + TXT_ColorBlack;
+ if (col == 0 && PageChar[index] == alpha_white)
+ PageAtrb[index].fg = TXT_ColorBlack; // indicate level 1 color change on column 0; (hack)
+ if ((charset!=C_G0P) && (charset!=C_G0S)) // we need to change charset to state it was before mosaic
+ charset = previous_charset;
+ break;
+
+ case flash:
+ flashmode = 1;
+ break;
+
+ case steady:
+ flashmode = 0;
+ PageAtrb[index].flashing = 0;
+ break;
+
+ case end_box:
+ boxwin = 0;
+ IgnoreAtBlackBgSubst = 0;
+ break;
+
+ case start_box:
+ if (!boxwin)
+ boxwin = 1;
+ break;
+
+ case normal_size:
+ doubleheight = 0;
+ doublewidth = 0;
+ PageAtrb[index].doubleh = doubleheight;
+ PageAtrb[index].doublew = doublewidth;
+ break;
+
+ case double_height:
+ if (row < 23)
+ {
+ doubleheight = 1;
+ dhset = 1;
+ }
+ doublewidth = 0;
+
+ break;
+
+ case double_width:
+ if (col < 39)
+ doublewidth = 1;
+ doubleheight = 0;
+ break;
+
+ case double_size:
+ if (row < 23)
+ {
+ doubleheight = 1;
+ dhset = 1;
+ }
+ if (col < 39)
+ doublewidth = 1;
+ break;
+
+ case mosaic_black:
+ case mosaic_red:
+ case mosaic_green:
+ case mosaic_yellow:
+ case mosaic_blue:
+ case mosaic_magenta:
+ case mosaic_cyan:
+ case mosaic_white:
+ concealed = 0;
+ foreground = PageChar[index] - mosaic_black + TXT_ColorBlack;
+ if ((charset==C_G0P) || (charset==C_G0S))
+ previous_charset=charset;
+ charset = mosaictype ? C_G1S : C_G1C;
+ mosaic_pending = 1;
+ break;
+
+ case conceal:
+ PageAtrb[index].concealed = 1;
+ concealed = 1;
+ if (!HintMode)
+ {
+ foreground = background;
+ PageAtrb[index].fg = foreground;
+ }
+ break;
+
+ case contiguous_mosaic:
+ mosaictype = 0;
+ if (charset == C_G1S)
+ {
+ charset = C_G1C;
+ PageAtrb[index].charset = charset;
+ }
+ break;
+
+ case separated_mosaic:
+ mosaictype = 1;
+ if (charset == C_G1C)
+ {
+ charset = C_G1S;
+ PageAtrb[index].charset = charset;
+ }
+ break;
+
+ case esc:
+ if (!mosaic_pending) { // if mosaic is pending we need to wait before mosaic arrives
+ if ((charset != C_G0P) && (charset != C_G0S)) // we need to switch to charset which was active before mosaic
+ charset = previous_charset;
+ if (charset == C_G0P)
+ charset = previous_charset = C_G0S;
+ else if (charset == C_G0S)
+ charset = previous_charset = C_G0P;
+ } else esc_pending = 1;
+ break;
+
+ case black_background:
+ background = TXT_ColorBlack;
+ IgnoreAtBlackBgSubst = 0;
+ PageAtrb[index].bg = background;
+ PageAtrb[index].IgnoreAtBlackBgSubst = IgnoreAtBlackBgSubst;
+ break;
+
+ case new_background:
+ background = foreground;
+ if (background == TXT_ColorBlack)
+ IgnoreAtBlackBgSubst = 1;
+ else
+ IgnoreAtBlackBgSubst = 0;
+ PageAtrb[index].bg = background;
+ PageAtrb[index].IgnoreAtBlackBgSubst = IgnoreAtBlackBgSubst;
+ break;
+
+ case hold_mosaic:
+ hold = 1;
+ break;
+
+ case release_mosaic:
+ hold = 2;
+ break;
+ }
+
+ /* handle spacing attributes */
+ if (hold && (PageAtrb[index].charset == C_G1C || PageAtrb[index].charset == C_G1S))
+ PageChar[index] = held_mosaic;
+ else
+ PageChar[index] = ' ';
+
+ if (hold == 2)
+ hold = 0;
+ }
+ else /* char >= ' ' */
+ {
+ mosaic_pending = 0; // charset will be switched next if esc_pending
+ /* set new held-mosaic char */
+ if ((charset == C_G1C || charset == C_G1S) &&
+ ((PageChar[index]&0xA0) == 0x20))
+ held_mosaic = PageChar[index];
+ if (PageAtrb[index].doubleh)
+ PageChar[index + 40] = 0xFF;
+
+ }
+ if (!(charset == C_G1C || charset == C_G1S))
+ held_mosaic = ' '; /* forget if outside mosaic */
+
+ } /* for col */
+
+ /* skip row if doubleheight */
+ if (row < 23 && dhset)
+ {
+ for (int col = 0; col < 40; col++)
+ {
+ int index = row*40 + col;
+ PageAtrb[index+40].bg = PageAtrb[index].bg;
+ PageAtrb[index+40].fg = TXT_ColorWhite;
+ if (!PageAtrb[index].doubleh)
+ PageChar[index+40] = ' ';
+ PageAtrb[index+40].flashing = 0;
+ PageAtrb[index+40].charset = C_G0P;
+ PageAtrb[index+40].doubleh = 0;
+ PageAtrb[index+40].doublew = 0;
+ PageAtrb[index+40].IgnoreAtBlackBgSubst = 0;
+ PageAtrb[index+40].concealed = 0;
+ PageAtrb[index+40].flashing = 0;
+ PageAtrb[index+40].boxwin = PageAtrb[index].boxwin;
+ }
+ row++;
+ }
+ } /* for row */
+ m_txtCache->FullScrColor = TXT_ColorBlack;
+
+ if (showl25)
+ Eval_l25(PageChar, PageAtrb, HintMode);
+
+ /* handle Black Background Color Substitution and transparency (CLUT1#0) */
+ {
+ int o = 0;
+ char bitmask ;
+
+ for (unsigned char row : m_txtCache->FullRowColor)
+ {
+ for (int c = 0; c < 40; c++)
+ {
+ bitmask = (PageAtrb[o].bg == 0x08 ? 0x08 : 0x00) | (row == 0x08 ? 0x04 : 0x00) | (PageAtrb[o].boxwin <<1) | (int)boxed;
+ switch (bitmask)
+ {
+ case 0x08:
+ case 0x0b:
+ if (row == 0x08)
+ PageAtrb[o].bg = m_txtCache->FullScrColor;
+ else
+ PageAtrb[o].bg = row;
+ break;
+ case 0x01:
+ case 0x05:
+ case 0x09:
+ case 0x0a:
+ case 0x0c:
+ case 0x0d:
+ case 0x0e:
+ case 0x0f:
+ PageAtrb[o].bg = TXT_ColorTransp;
+ break;
+ }
+ bitmask = (PageAtrb[o].fg == 0x08 ? 0x08 : 0x00) | (row == 0x08 ? 0x04 : 0x00) | (PageAtrb[o].boxwin <<1) | (int)boxed;
+ switch (bitmask)
+ {
+ case 0x08:
+ case 0x0b:
+ if (row == 0x08)
+ PageAtrb[o].fg = m_txtCache->FullScrColor;
+ else
+ PageAtrb[o].fg = row;
+ break;
+ case 0x01:
+ case 0x05:
+ case 0x09:
+ case 0x0a:
+ case 0x0c:
+ case 0x0d:
+ case 0x0e:
+ case 0x0f:
+ PageAtrb[o].fg = TXT_ColorTransp;
+ break;
+ }
+ o++;
+ }
+ }
+ }
+ return PageInfo;
+}
+
+void CTeletextDecoder::Eval_l25(unsigned char* PageChar, TextPageAttr_t *PageAtrb, bool HintMode)
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ memset(m_txtCache->FullRowColor, 0, sizeof(m_txtCache->FullRowColor));
+ m_txtCache->FullScrColor = TXT_ColorBlack;
+ m_txtCache->ColorTable = NULL;
+
+ if (!m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPage])
+ return;
+
+ /* normal page */
+ if (IsDec(m_txtCache->Page))
+ {
+ unsigned char APx0, APy0, APx, APy;
+ TextPageinfo_t *pi = &(m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPage]->pageinfo);
+ TextCachedPage_t *pmot = m_txtCache->astCachetable[(m_txtCache->Page & 0xf00) | 0xfe][0];
+ int p26Received = 0;
+ int BlackBgSubst = 0;
+ int ColorTableRemapping = 0;
+
+ m_txtCache->pop = m_txtCache->gpop = m_txtCache->drcs = m_txtCache->gdrcs = 0;
+
+ if (pi->ext)
+ {
+ TextExtData_t *e = pi->ext;
+
+ if (e->p26[0])
+ p26Received = 1;
+
+ if (e->p27)
+ {
+ Textp27_t *p27 = e->p27;
+ if (p27[0].l25)
+ m_txtCache->gpop = p27[0].page;
+ if (p27[1].l25)
+ m_txtCache->pop = p27[1].page;
+ if (p27[2].l25)
+ m_txtCache->gdrcs = p27[2].page;
+ if (p27[3].l25)
+ m_txtCache->drcs = p27[3].page;
+ }
+
+ if (e->p28Received)
+ {
+ m_txtCache->ColorTable = e->bgr;
+ BlackBgSubst = e->BlackBgSubst;
+ ColorTableRemapping = e->ColorTableRemapping;
+ memset(m_txtCache->FullRowColor, e->DefRowColor, sizeof(m_txtCache->FullRowColor));
+ m_txtCache->FullScrColor = e->DefScreenColor;
+ m_txtCache->NationalSubset = SetNational(e->DefaultCharset);
+ m_txtCache->NationalSubsetSecondary = SetNational(e->SecondCharset);
+ } /* e->p28Received */
+ }
+
+ if (!m_txtCache->ColorTable && m_txtCache->astP29[m_txtCache->Page >> 8])
+ {
+ TextExtData_t *e = m_txtCache->astP29[m_txtCache->Page >> 8];
+ m_txtCache->ColorTable = e->bgr;
+ BlackBgSubst = e->BlackBgSubst;
+ ColorTableRemapping = e->ColorTableRemapping;
+ memset(m_txtCache->FullRowColor, e->DefRowColor, sizeof(m_txtCache->FullRowColor));
+ m_txtCache->FullScrColor = e->DefScreenColor;
+ m_txtCache->NationalSubset = SetNational(e->DefaultCharset);
+ m_txtCache->NationalSubsetSecondary = SetNational(e->SecondCharset);
+ }
+
+ if (ColorTableRemapping)
+ {
+ for (int i = 0; i < 25*40; i++)
+ {
+ PageAtrb[i].fg += MapTblFG[ColorTableRemapping - 1];
+ if (!BlackBgSubst || PageAtrb[i].bg != TXT_ColorBlack || PageAtrb[i].IgnoreAtBlackBgSubst)
+ PageAtrb[i].bg += MapTblBG[ColorTableRemapping - 1];
+ }
+ }
+
+ /* determine ?pop/?drcs from MOT */
+ if (pmot)
+ {
+ unsigned char pmot_data[23*40];
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->LoadPage((m_txtCache->Page & 0xf00) | 0xfe, 0, pmot_data);
+
+ unsigned char *p = pmot_data; /* start of link data */
+ int o = 2 * (((m_txtCache->Page & 0xf0) >> 4) * 10 + (m_txtCache->Page & 0x0f)); /* offset of links for current page */
+ int opop = p[o] & 0x07; /* index of POP link */
+ int odrcs = p[o+1] & 0x07; /* index of DRCS link */
+ unsigned char obj[3*4*4]; /* types* objects * (triplet,packet,subp,high) */
+ unsigned char type,ct, tstart = 4*4;
+ memset(obj,0,sizeof(obj));
+
+ if (p[o] & 0x08) /* GPOP data used */
+ {
+ if (!m_txtCache->gpop || !(p[18*40] & 0x08)) /* no p27 data or higher prio of MOT link */
+ {
+ m_txtCache->gpop = ((p[18*40] << 8) | (p[18*40+1] << 4) | p[18*40+2]) & 0x7ff;
+ if ((m_txtCache->gpop & 0xff) == 0xff)
+ m_txtCache->gpop = 0;
+ else
+ {
+ if (m_txtCache->gpop < 0x100)
+ m_txtCache->gpop += 0x800;
+ if (!p26Received)
+ {
+ ct = 2;
+ while (ct)
+ {
+ ct--;
+ type = (p[18*40+5] >> 2*ct) & 0x03;
+
+ if (type == 0) continue;
+ obj[(type-1)*(tstart)+ct*4 ] = 3 * ((p[18*40+7+ct*2] >> 1) & 0x03) + type; //triplet
+ obj[(type-1)*(tstart)+ct*4+1] = ((p[18*40+7+ct*2] & 0x08) >> 3) + 1 ; //packet
+ obj[(type-1)*(tstart)+ct*4+2] = p[18*40+6+ct*2] & 0x0f ; //subp
+ obj[(type-1)*(tstart)+ct*4+3] = p[18*40+7+ct*2] & 0x01 ; //high
+ }
+ }
+ }
+ }
+ }
+ if (opop) /* POP data used */
+ {
+ opop = 18*40 + 10*opop; /* offset to POP link */
+ if (!m_txtCache->pop || !(p[opop] & 0x08)) /* no p27 data or higher prio of MOT link */
+ {
+ m_txtCache->pop = ((p[opop] << 8) | (p[opop+1] << 4) | p[opop+2]) & 0x7ff;
+ if ((m_txtCache->pop & 0xff) == 0xff)
+ m_txtCache->pop = 0;
+ else
+ {
+ if (m_txtCache->pop < 0x100)
+ m_txtCache->pop += 0x800;
+ if (!p26Received)
+ {
+ ct = 2;
+ while (ct)
+ {
+ ct--;
+ type = (p[opop+5] >> 2*ct) & 0x03;
+
+ if (type == 0) continue;
+ obj[(type-1)*(tstart)+(ct+2)*4 ] = 3 * ((p[opop+7+ct*2] >> 1) & 0x03) + type; //triplet
+ obj[(type-1)*(tstart)+(ct+2)*4+1] = ((p[opop+7+ct*2] & 0x08) >> 3) + 1 ; //packet
+ obj[(type-1)*(tstart)+(ct+2)*4+2] = p[opop+6+ct*2] ; //subp
+ obj[(type-1)*(tstart)+(ct+2)*4+3] = p[opop+7+ct*2] & 0x01 ; //high
+ }
+ }
+ }
+ }
+ }
+ // eval default objects in correct order
+ for (int i = 0; i < 12; i++)
+ {
+ if (obj[i*4] != 0)
+ {
+ APx0 = APy0 = APx = APy = m_txtCache->tAPx = m_txtCache->tAPy = 0;
+ Eval_NumberedObject(i % 4 > 1 ? m_txtCache->pop : m_txtCache->gpop, obj[i*4+2], obj[i*4+1], obj[i*4], obj[i*4+3], &APx, &APy, &APx0, &APy0, PageChar, PageAtrb);
+ }
+ }
+
+ if (p[o+1] & 0x08) /* GDRCS data used */
+ {
+ if (!m_txtCache->gdrcs || !(p[20*40] & 0x08)) /* no p27 data or higher prio of MOT link */
+ {
+ m_txtCache->gdrcs = ((p[20*40] << 8) | (p[20*40+1] << 4) | p[20*40+2]) & 0x7ff;
+ if ((m_txtCache->gdrcs & 0xff) == 0xff)
+ m_txtCache->gdrcs = 0;
+ else if (m_txtCache->gdrcs < 0x100)
+ m_txtCache->gdrcs += 0x800;
+ }
+ }
+ if (odrcs) /* DRCS data used */
+ {
+ odrcs = 20*40 + 4*odrcs; /* offset to DRCS link */
+ if (!m_txtCache->drcs || !(p[odrcs] & 0x08)) /* no p27 data or higher prio of MOT link */
+ {
+ m_txtCache->drcs = ((p[odrcs] << 8) | (p[odrcs+1] << 4) | p[odrcs+2]) & 0x7ff;
+ if ((m_txtCache->drcs & 0xff) == 0xff)
+ m_txtCache->drcs = 0;
+ else if (m_txtCache->drcs < 0x100)
+ m_txtCache->drcs += 0x800;
+ }
+ }
+ if (m_txtCache->astCachetable[m_txtCache->gpop][0])
+ m_txtCache->astCachetable[m_txtCache->gpop][0]->pageinfo.function = FUNC_GPOP;
+ if (m_txtCache->astCachetable[m_txtCache->pop][0])
+ m_txtCache->astCachetable[m_txtCache->pop][0]->pageinfo.function = FUNC_POP;
+ if (m_txtCache->astCachetable[m_txtCache->gdrcs][0])
+ m_txtCache->astCachetable[m_txtCache->gdrcs][0]->pageinfo.function = FUNC_GDRCS;
+ if (m_txtCache->astCachetable[m_txtCache->drcs][0])
+ m_txtCache->astCachetable[m_txtCache->drcs][0]->pageinfo.function = FUNC_DRCS;
+ } /* if mot */
+
+ /* evaluate local extension data from p26 */
+ if (p26Received)
+ {
+ APx0 = APy0 = APx = APy = m_txtCache->tAPx = m_txtCache->tAPy = 0;
+ Eval_Object(13 * (23-2 + 2), m_txtCache->astCachetable[m_txtCache->Page][m_txtCache->SubPage], &APx, &APy, &APx0, &APy0, OBJ_ACTIVE, &PageChar[40], PageChar, PageAtrb); /* 1st triplet p26/0 */
+ }
+
+ {
+ int o = 0;
+ for (unsigned char row : m_txtCache->FullRowColor)
+ {
+ for (int c = 0; c < 40; c++)
+ {
+ if (BlackBgSubst && PageAtrb[o].bg == TXT_ColorBlack && !(PageAtrb[o].IgnoreAtBlackBgSubst))
+ {
+ if (row == 0x08)
+ PageAtrb[o].bg = m_txtCache->FullScrColor;
+ else
+ PageAtrb[o].bg = row;
+ }
+ o++;
+ }
+ }
+ }
+
+ if (!HintMode)
+ {
+ for (int i = 0; i < 25*40; i++)
+ {
+ if (PageAtrb[i].concealed) PageAtrb[i].fg = PageAtrb[i].bg;
+ }
+ }
+ } /* is_dec(page) */
+}
+
+/* dump interpreted object data to stdout */
+/* in: 18 bit object data */
+/* out: termination info, >0 if end of object */
+void CTeletextDecoder::Eval_Object(int iONr, TextCachedPage_t *pstCachedPage,
+ unsigned char *pAPx, unsigned char *pAPy,
+ unsigned char *pAPx0, unsigned char *pAPy0,
+ tObjType ObjType, unsigned char* pagedata, unsigned char* PageChar, TextPageAttr_t* PageAtrb)
+{
+ int iOData;
+ int iONr1 = iONr + 1; /* don't terminate after first triplet */
+ unsigned char drcssubp=0, gdrcssubp=0;
+ signed char endcol = -1; /* last column to which to extend attribute changes */
+ TextPageAttr_t attrPassive = { TXT_ColorWhite , TXT_ColorBlack , C_G0P, 0, 0, 1 ,0, 0, 0, 0, 0, 0, 0, 0x3f}; /* current attribute for passive objects */
+
+ do
+ {
+ iOData = iTripletNumber2Data(iONr, pstCachedPage, pagedata); /* get triplet data, next triplet */
+ if (iOData < 0) /* invalid number, not cached, or hamming error: terminate */
+ break;
+
+ if (endcol < 0)
+ {
+ if (ObjType == OBJ_ACTIVE)
+ {
+ endcol = 40;
+ }
+ else if (ObjType == OBJ_ADAPTIVE) /* search end of line */
+ {
+ for (int i = iONr; i <= 506; i++)
+ {
+ int iTempOData = iTripletNumber2Data(i, pstCachedPage, pagedata); /* get triplet data, next triplet */
+ int iAddress = (iTempOData ) & 0x3f;
+ int iMode = (iTempOData >> 6) & 0x1f;
+ //int iData = (iTempOData >> 11) & 0x7f;
+ if (iTempOData < 0 || /* invalid number, not cached, or hamming error: terminate */
+ (iAddress >= 40 /* new row: row address and */
+ && (iMode == 0x01 || /* Full Row Color or */
+ iMode == 0x04 || /* Set Active Position */
+ (iMode >= 0x15 && iMode <= 0x17) || /* Object Definition */
+ iMode == 0x17))) /* Object Termination */
+ break;
+ if (iAddress < 40 && iMode != 0x06)
+ endcol = iAddress;
+ }
+ }
+ }
+ iONr++;
+ }
+ while (0 == Eval_Triplet(iOData, pstCachedPage, pAPx, pAPy, pAPx0, pAPy0, &drcssubp, &gdrcssubp, &endcol, &attrPassive, pagedata, PageChar, PageAtrb) || iONr1 == iONr); /* repeat until termination reached */
+}
+
+void CTeletextDecoder::Eval_NumberedObject(int p, int s, int packet, int triplet, int high,
+ unsigned char *pAPx, unsigned char *pAPy,
+ unsigned char *pAPx0, unsigned char *pAPy0, unsigned char* PageChar, TextPageAttr_t* PageAtrb)
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ if (!packet || 0 == m_txtCache->astCachetable[p][s])
+ return;
+
+ unsigned char pagedata[23*40];
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->LoadPage(p, s, pagedata);
+
+ int idata = CDVDTeletextTools::deh24(pagedata + 40*(packet-1) + 1 + 3*triplet);
+ int iONr;
+
+ if (idata < 0) /* hamming error: ignore triplet */
+ return;
+ if (high)
+ iONr = idata >> 9; /* triplet number of odd object data */
+ else
+ iONr = idata & 0x1ff; /* triplet number of even object data */
+ if (iONr <= 506)
+ {
+ Eval_Object(iONr, m_txtCache->astCachetable[p][s], pAPx, pAPy, pAPx0, pAPy0, (tObjType)(triplet % 3),pagedata, PageChar, PageAtrb);
+ }
+}
+
+int CTeletextDecoder::Eval_Triplet(int iOData, TextCachedPage_t *pstCachedPage,
+ unsigned char *pAPx, unsigned char *pAPy,
+ unsigned char *pAPx0, unsigned char *pAPy0,
+ unsigned char *drcssubp, unsigned char *gdrcssubp,
+ signed char *endcol, TextPageAttr_t *attrPassive, unsigned char* pagedata, unsigned char* PageChar, TextPageAttr_t* PageAtrb)
+{
+ int iAddress = (iOData ) & 0x3f;
+ int iMode = (iOData >> 6) & 0x1f;
+ int iData = (iOData >> 11) & 0x7f;
+
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ if (iAddress < 40) /* column addresses */
+ {
+ int offset; /* offset to PageChar and PageAtrb */
+
+ if (iMode != 0x06)
+ *pAPx = iAddress; /* new Active Column */
+ offset = (*pAPy0 + *pAPy) * 40 + *pAPx0 + *pAPx; /* offset to PageChar and PageAtrb */
+
+ switch (iMode)
+ {
+ case 0x00:
+ if (0 == (iData>>5))
+ {
+ int newcolor = iData & 0x1f;
+ if (*endcol < 0) /* passive object */
+ attrPassive->fg = newcolor;
+ else if (*endcol == 40) /* active object */
+ {
+ TextPageAttr_t *p = &PageAtrb[offset];
+ int oldcolor = (p)->fg; /* current color (set-after) */
+ int c = *pAPx0 + *pAPx; /* current column absolute */
+ do
+ {
+ p->fg = newcolor;
+ p++;
+ c++;
+ } while (c < 40 && p->fg == oldcolor); /* stop at change by level 1 page */
+ }
+ else /* adaptive object */
+ {
+ TextPageAttr_t *p = &PageAtrb[offset];
+ int c = *pAPx; /* current column relative to object origin */
+ do
+ {
+ p->fg = newcolor;
+ p++;
+ c++;
+ } while (c <= *endcol);
+ }
+ }
+ break;
+ case 0x01:
+ if (iData >= 0x20)
+ {
+ PageChar[offset] = iData;
+ if (*endcol < 0) /* passive object */
+ {
+ attrPassive->charset = C_G1C; /* FIXME: separated? */
+ PageAtrb[offset] = *attrPassive;
+ }
+ else if (PageAtrb[offset].charset != C_G1S)
+ PageAtrb[offset].charset = C_G1C; /* FIXME: separated? */
+ }
+ break;
+ case 0x02:
+ case 0x0b:
+ PageChar[offset] = iData;
+ if (*endcol < 0) /* passive object */
+ {
+ attrPassive->charset = C_G3;
+ PageAtrb[offset] = *attrPassive;
+ }
+ else
+ PageAtrb[offset].charset = C_G3;
+ break;
+ case 0x03:
+ if (0 == (iData>>5))
+ {
+ int newcolor = iData & 0x1f;
+ if (*endcol < 0) /* passive object */
+ attrPassive->bg = newcolor;
+ else if (*endcol == 40) /* active object */
+ {
+ TextPageAttr_t *p = &PageAtrb[offset];
+ int oldcolor = (p)->bg; /* current color (set-after) */
+ int c = *pAPx0 + *pAPx; /* current column absolute */
+ do
+ {
+ p->bg = newcolor;
+ if (newcolor == TXT_ColorBlack)
+ p->IgnoreAtBlackBgSubst = 1;
+ p++;
+ c++;
+ } while (c < 40 && p->bg == oldcolor); /* stop at change by level 1 page */
+ }
+ else /* adaptive object */
+ {
+ TextPageAttr_t *p = &PageAtrb[offset];
+ int c = *pAPx; /* current column relative to object origin */
+ do
+ {
+ p->bg = newcolor;
+ if (newcolor == TXT_ColorBlack)
+ p->IgnoreAtBlackBgSubst = 1;
+ p++;
+ c++;
+ } while (c <= *endcol);
+ }
+ }
+ break;
+ case 0x06:
+ /* ignore */
+ break;
+ case 0x07:
+ if ((iData & 0x60) != 0) break; // reserved data field
+ if (*endcol < 0) /* passive object */
+ {
+ attrPassive->flashing=iData & 0x1f;
+ PageAtrb[offset] = *attrPassive;
+ }
+ else
+ PageAtrb[offset].flashing=iData & 0x1f;
+ break;
+ case 0x08:
+ if (*endcol < 0) /* passive object */
+ {
+ attrPassive->setG0G2=iData & 0x3f;
+ PageAtrb[offset] = *attrPassive;
+ }
+ else
+ PageAtrb[offset].setG0G2=iData & 0x3f;
+ break;
+ case 0x09:
+ PageChar[offset] = iData;
+ if (*endcol < 0) /* passive object */
+ {
+ attrPassive->charset = C_G0P; /* FIXME: secondary? */
+ attrPassive->setX26 = 1;
+ PageAtrb[offset] = *attrPassive;
+ }
+ else
+ {
+ PageAtrb[offset].charset = C_G0P; /* FIXME: secondary? */
+ PageAtrb[offset].setX26 = 1;
+ }
+ break;
+// case 0x0b: (see 0x02)
+ case 0x0c:
+ {
+ int conc = (iData & 0x04);
+ int inv = (iData & 0x10);
+ int dw = (iData & 0x40 ?1:0);
+ int dh = (iData & 0x01 ?1:0);
+ int sep = (iData & 0x20);
+ int bw = (iData & 0x02 ?1:0);
+ if (*endcol < 0) /* passive object */
+ {
+ if (conc)
+ {
+ attrPassive->concealed = 1;
+ attrPassive->fg = attrPassive->bg;
+ }
+ attrPassive->inverted = (inv ? 1- attrPassive->inverted : 0);
+ attrPassive->doubleh = dh;
+ attrPassive->doublew = dw;
+ attrPassive->boxwin = bw;
+ if (bw) attrPassive->IgnoreAtBlackBgSubst = 0;
+ if (sep)
+ {
+ if (attrPassive->charset == C_G1C)
+ attrPassive->charset = C_G1S;
+ else
+ attrPassive->underline = 1;
+ }
+ else
+ {
+ if (attrPassive->charset == C_G1S)
+ attrPassive->charset = C_G1C;
+ else
+ attrPassive->underline = 0;
+ }
+ }
+ else
+ {
+
+ int c = *pAPx0 + (*endcol == 40 ? *pAPx : 0); /* current column */
+ int c1 = offset;
+ TextPageAttr_t *p = &PageAtrb[offset];
+ do
+ {
+ p->inverted = (inv ? 1- p->inverted : 0);
+ if (conc)
+ {
+ p->concealed = 1;
+ p->fg = p->bg;
+ }
+ if (sep)
+ {
+ if (p->charset == C_G1C)
+ p->charset = C_G1S;
+ else
+ p->underline = 1;
+ }
+ else
+ {
+ if (p->charset == C_G1S)
+ p->charset = C_G1C;
+ else
+ p->underline = 0;
+ }
+ p->doublew = dw;
+ p->doubleh = dh;
+ p->boxwin = bw;
+ if (bw) p->IgnoreAtBlackBgSubst = 0;
+ p++;
+ c++;
+ c1++;
+ } while (c < *endcol);
+ }
+ break;
+ }
+ case 0x0d:
+ PageChar[offset] = iData & 0x3f;
+ if (*endcol < 0) /* passive object */
+ {
+ attrPassive->charset = C_OFFSET_DRCS + ((iData & 0x40) ? (0x10 + *drcssubp) : *gdrcssubp);
+ PageAtrb[offset] = *attrPassive;
+ }
+ else
+ PageAtrb[offset].charset = C_OFFSET_DRCS + ((iData & 0x40) ? (0x10 + *drcssubp) : *gdrcssubp);
+ break;
+ case 0x0f:
+ PageChar[offset] = iData;
+ if (*endcol < 0) /* passive object */
+ {
+ attrPassive->charset = C_G2;
+ PageAtrb[offset] = *attrPassive;
+ }
+ else
+ PageAtrb[offset].charset = C_G2;
+ break;
+ default:
+ if (iMode == 0x10 && iData == 0x2a)
+ iData = '@';
+ if (iMode >= 0x10)
+ {
+ PageChar[offset] = iData;
+ if (*endcol < 0) /* passive object */
+ {
+ attrPassive->charset = C_G0P;
+ attrPassive->diacrit = iMode & 0x0f;
+ attrPassive->setX26 = 1;
+ PageAtrb[offset] = *attrPassive;
+ }
+ else
+ {
+ PageAtrb[offset].charset = C_G0P;
+ PageAtrb[offset].diacrit = iMode & 0x0f;
+ PageAtrb[offset].setX26 = 1;
+ }
+ }
+ break; /* unsupported or not yet implemented mode: ignore */
+ } /* switch (iMode) */
+ }
+ else /* ================= (iAddress >= 40): row addresses ====================== */
+ {
+ switch (iMode)
+ {
+ case 0x00:
+ if (0 == (iData>>5))
+ {
+ m_txtCache->FullScrColor = iData & 0x1f;
+ }
+ break;
+ case 0x01:
+ if (*endcol == 40) /* active object */
+ {
+ *pAPy = RowAddress2Row(iAddress); /* new Active Row */
+
+ int color = iData & 0x1f;
+ int row = *pAPy0 + *pAPy;
+ int maxrow;
+
+ if (row <= 24 && 0 == (iData>>5))
+ maxrow = row;
+ else if (3 == (iData>>5))
+ maxrow = 24;
+ else
+ maxrow = -1;
+ for (; row <= maxrow; row++)
+ m_txtCache->FullRowColor[row] = color;
+ *endcol = -1;
+ }
+ break;
+ case 0x04:
+ *pAPy = RowAddress2Row(iAddress); /* new Active Row */
+ if (iData < 40)
+ *pAPx = iData; /* new Active Column */
+ *endcol = -1; /* FIXME: check if row changed? */
+ break;
+ case 0x07:
+ if (iAddress == 0x3f)
+ {
+ *pAPx = *pAPy = 0; /* new Active Position 0,0 */
+ if (*endcol == 40) /* active object */
+ {
+ int color = iData & 0x1f;
+ int row = *pAPy0; // + *pAPy;
+ int maxrow;
+
+ if (row <= 24 && 0 == (iData>>5))
+ maxrow = row;
+ else if (3 == (iData>>5))
+ maxrow = 24;
+ else
+ maxrow = -1;
+ for (; row <= maxrow; row++)
+ m_txtCache->FullRowColor[row] = color;
+ }
+ *endcol = -1;
+ }
+ break;
+ case 0x08:
+ case 0x09:
+ case 0x0a:
+ case 0x0b:
+ case 0x0c:
+ case 0x0d:
+ case 0x0e:
+ case 0x0f:
+ /* ignore */
+ break;
+ case 0x10:
+ m_txtCache->tAPy = iAddress - 40;
+ m_txtCache->tAPx = iData;
+ break;
+ case 0x11:
+ case 0x12:
+ case 0x13:
+ if (iAddress & 0x10) /* POP or GPOP */
+ {
+ unsigned char APx = 0, APy = 0;
+ unsigned char APx0 = *pAPx0 + *pAPx + m_txtCache->tAPx, APy0 = *pAPy0 + *pAPy + m_txtCache->tAPy;
+ int triplet = 3 * ((iData >> 5) & 0x03) + (iMode & 0x03);
+ int packet = (iAddress & 0x03) + 1;
+ int subp = iData & 0x0f;
+ int high = (iData >> 4) & 0x01;
+
+
+ if (APx0 < 40) /* not in side panel */
+ {
+ Eval_NumberedObject((iAddress & 0x08) ? m_txtCache->gpop : m_txtCache->pop, subp, packet, triplet, high, &APx, &APy, &APx0, &APy0, PageChar,PageAtrb);
+ }
+ }
+ else if (iAddress & 0x08) /* local: eval invoked object */
+ {
+ unsigned char APx = 0, APy = 0;
+ unsigned char APx0 = *pAPx0 + *pAPx + m_txtCache->tAPx, APy0 = *pAPy0 + *pAPy + m_txtCache->tAPy;
+ int descode = ((iAddress & 0x01) << 3) | (iData >> 4);
+ int triplet = iData & 0x0f;
+
+ if (APx0 < 40) /* not in side panel */
+ {
+ Eval_Object(13 * 23 + 13 * descode + triplet, pstCachedPage, &APx, &APy, &APx0, &APy0, (tObjType)(triplet % 3), pagedata, PageChar, PageAtrb);
+ }
+ }
+ break;
+ case 0x15:
+ case 0x16:
+ case 0x17:
+ if (0 == (iAddress & 0x08)) /* Object Definition illegal or only level 3.5 */
+ break; /* ignore */
+
+ m_txtCache->tAPx = m_txtCache->tAPy = 0;
+ *endcol = -1;
+ return 0xFF; /* termination by object definition */
+ break;
+ case 0x18:
+ if (0 == (iData & 0x10)) /* DRCS Mode reserved or only level 3.5 */
+ break; /* ignore */
+
+ if (iData & 0x40)
+ *drcssubp = iData & 0x0f;
+ else
+ *gdrcssubp = iData & 0x0f;
+ break;
+ case 0x1f:
+ m_txtCache->tAPx = m_txtCache->tAPy = 0;
+ *endcol = -1;
+ return 0x80 | iData; /* explicit termination */
+ break;
+ default:
+ break; /* unsupported or not yet implemented mode: ignore */
+ } /* switch (iMode) */
+ } /* (iAddress >= 40): row addresses */
+
+ if (iAddress < 40 || iMode != 0x10) /* leave temp. AP-Offset unchanged only immediately after definition */
+ m_txtCache->tAPx = m_txtCache->tAPy = 0;
+
+ return 0; /* normal exit, no termination */
+}
+
+/* get object data */
+/* in: absolute triplet number (0..506, start at packet 3 byte 1) */
+/* in: pointer to cache struct of page data */
+/* out: 18 bit triplet data, <0 if invalid number, not cached, or hamming error */
+int CTeletextDecoder::iTripletNumber2Data(int iONr, TextCachedPage_t *pstCachedPage, unsigned char* pagedata)
+{
+ if (iONr > 506 || 0 == pstCachedPage)
+ return -1;
+
+ unsigned char *p;
+ int packet = (iONr / 13) + 3;
+ int packetoffset = 3 * (iONr % 13);
+
+ if (packet <= 23)
+ p = pagedata + 40*(packet-1) + packetoffset + 1;
+ else if (packet <= 25)
+ {
+ if (0 == pstCachedPage->pageinfo.p24)
+ return -1;
+ p = pstCachedPage->pageinfo.p24 + 40*(packet-24) + packetoffset + 1;
+ }
+ else
+ {
+ int descode = packet - 26;
+ if (0 == pstCachedPage->pageinfo.ext)
+ return -1;
+ if (0 == pstCachedPage->pageinfo.ext->p26[descode])
+ return -1;
+ p = pstCachedPage->pageinfo.ext->p26[descode] + packetoffset; /* first byte (=designation code) is not cached */
+ }
+ return CDVDTeletextTools::deh24(p);
+}
+
+int CTeletextDecoder::SetNational(unsigned char sec)
+{
+ std::unique_lock<CCriticalSection> lock(m_txtCache->m_critSection);
+
+ switch (sec)
+ {
+ case 0x08:
+ return NAT_PL; //polish
+ case 0x16:
+ case 0x36:
+ return NAT_TR; //turkish
+ case 0x1d:
+ return NAT_SR; //serbian, croatian, slovenian
+ case 0x20:
+ return NAT_SC; // serbian, croatian
+ case 0x24:
+ return NAT_RB; // russian, bulgarian
+ case 0x25:
+ return NAT_UA; // ukrainian
+ case 0x22:
+ return NAT_ET; // estonian
+ case 0x23:
+ return NAT_LV; // latvian, lithuanian
+ case 0x37:
+ return NAT_GR; // greek
+ case 0x55:
+ return NAT_HB; // hebrew
+ case 0x47:
+ case 0x57:
+ return NAT_AR; // arabic
+ }
+ return CountryConversionTable[sec & 0x07];
+}
+
+int CTeletextDecoder::NextHex(int i) /* return next existing non-decimal page number */
+{
+ int startpage = i;
+ if (startpage < 0x100)
+ startpage = 0x100;
+
+ do
+ {
+ i++;
+ if (i > 0x8FF)
+ i = 0x100;
+ if (i == startpage)
+ break;
+ } while ((m_txtCache->SubPageTable[i] == 0xFF) || IsDec(i));
+ return i;
+}
+
+void CTeletextDecoder::SetColors(const unsigned short *pcolormap, int offset, int number)
+{
+ int j = offset; /* index in global color table */
+
+ for (int i = 0; i < number; i++)
+ {
+ int r = ((pcolormap[i] >> 8) & 0xf) << 4;
+ int g = ((pcolormap[i] >> 4) & 0xf) << 4;
+ int b = ((pcolormap[i]) & 0xf) << 4;
+
+ if (m_RenderInfo.rd0[j] != r)
+ {
+ m_RenderInfo.rd0[j] = r;
+ }
+ if (m_RenderInfo.gn0[j] != g)
+ {
+ m_RenderInfo.gn0[j] = g;
+ }
+ if (m_RenderInfo.bl0[j] != b)
+ {
+ m_RenderInfo.bl0[j] = b;
+ }
+ j++;
+ }
+}
+
+UTILS::COLOR::Color CTeletextDecoder::GetColorRGB(enumTeletextColor ttc)
+{
+ switch (ttc)
+ {
+ case TXT_ColorBlack: return 0xFF000000;
+ case TXT_ColorRed: return 0xFFFC1414;
+ case TXT_ColorGreen: return 0xFF24FC24;
+ case TXT_ColorYellow: return 0xFFFCC024;
+ case TXT_ColorBlue: return 0xFF0000FC;
+ case TXT_ColorMagenta: return 0xFFB000FC;
+ case TXT_ColorCyan: return 0xFF00FCFC;
+ case TXT_ColorWhite: return 0xFFFCFCFC;
+ case TXT_ColorTransp: return 0x00000000;
+ default: break;
+ }
+
+ /* Get colors for CLUTs 2+3 */
+ int index = (int)ttc;
+ UTILS::COLOR::Color color = (m_RenderInfo.tr0[index] << 24) | (m_RenderInfo.bl0[index] << 16) |
+ (m_RenderInfo.gn0[index] << 8) | m_RenderInfo.rd0[index];
+ return color;
+}
+
diff --git a/xbmc/video/Teletext.h b/xbmc/video/Teletext.h
new file mode 100644
index 0000000..105d842
--- /dev/null
+++ b/xbmc/video/Teletext.h
@@ -0,0 +1,197 @@
+/*
+ * 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 "TeletextDefines.h"
+#include "guilib/GUITexture.h"
+#include "utils/ColorUtils.h"
+
+// stuff for freetype
+#include <ft2build.h>
+
+#ifdef TARGET_WINDOWS_STORE
+#define generic GenericFromFreeTypeLibrary
+#endif
+
+#include FT_FREETYPE_H
+#include FT_CACHE_H
+#include FT_CACHE_SMALL_BITMAPS_H
+
+class CAction;
+
+typedef enum /* object type */
+{
+ OBJ_PASSIVE,
+ OBJ_ACTIVE,
+ OBJ_ADAPTIVE
+} tObjType;
+
+class CTeletextDecoder
+{
+public:
+ CTeletextDecoder();
+ virtual ~CTeletextDecoder(void);
+
+ bool NeedRendering() { return m_updateTexture; }
+ void RenderingDone() { m_updateTexture = false; }
+ UTILS::COLOR::Color* GetTextureBuffer()
+ {
+ return m_TextureBuffer + (m_RenderInfo.Width * m_YOffset);
+ }
+ int GetHeight() { return m_RenderInfo.Height; }
+ int GetWidth() { return m_RenderInfo.Width; }
+ bool InitDecoder();
+ void EndDecoder();
+ void RenderPage();
+ bool HandleAction(const CAction &action);
+
+private:
+ void PageInput(int Number);
+ void GetNextPageOne(bool up);
+ void GetNextSubPage(int offset);
+ bool IsSubtitlePage(int pageNumber) const;
+ void SwitchZoomMode();
+ void SwitchTranspMode();
+ void SwitchHintMode();
+ void ColorKey(int target);
+ void StartPageCatching();
+ void StopPageCatching();
+ void CatchNextPage(int firstlineinc, int inc);
+ void RenderCatchedPage();
+ void DoFlashing(int startrow);
+ void DoRenderPage(int startrow, int national_subset_bak);
+ void Decode_BTT();
+ void Decode_ADIP();
+ int TopText_GetNext(int startpage, int up, int findgroup);
+ void Showlink(int column, int linkpage);
+ void CreateLine25();
+ void RenderCharFB(int Char, TextPageAttr_t *Attribute);
+ void RenderCharBB(int Char, TextPageAttr_t *Attribute);
+ void CopyBB2FB();
+ void SetFontWidth(int newWidth);
+ int GetCurFontWidth();
+ void SetPosX(int column);
+ void ClearBB(UTILS::COLOR::Color Color);
+ void ClearFB(UTILS::COLOR::Color Color);
+ void FillBorder(UTILS::COLOR::Color Color);
+ void FillRect(
+ UTILS::COLOR::Color* buffer, int xres, int x, int y, int w, int h, UTILS::COLOR::Color Color);
+ void DrawVLine(
+ UTILS::COLOR::Color* lfb, int xres, int x, int y, int l, UTILS::COLOR::Color color);
+ void DrawHLine(
+ UTILS::COLOR::Color* lfb, int xres, int x, int y, int l, UTILS::COLOR::Color color);
+ void FillRectMosaicSeparated(UTILS::COLOR::Color* lfb,
+ int xres,
+ int x,
+ int y,
+ int w,
+ int h,
+ UTILS::COLOR::Color fgcolor,
+ UTILS::COLOR::Color bgcolor,
+ int set);
+ void FillTrapez(UTILS::COLOR::Color* lfb,
+ int xres,
+ int x0,
+ int y0,
+ int l0,
+ int xoffset1,
+ int h,
+ int l1,
+ UTILS::COLOR::Color color);
+ void FlipHorz(UTILS::COLOR::Color* lfb, int xres, int x, int y, int w, int h);
+ void FlipVert(UTILS::COLOR::Color* lfb, int xres, int x, int y, int w, int h);
+ int ShapeCoord(int param, int curfontwidth, int curfontheight);
+ void DrawShape(UTILS::COLOR::Color* lfb,
+ int xres,
+ int x,
+ int y,
+ int shapenumber,
+ int curfontwidth,
+ int fontheight,
+ int curfontheight,
+ UTILS::COLOR::Color fgcolor,
+ UTILS::COLOR::Color bgcolor,
+ bool clear);
+ void RenderDRCS(
+ int xres,
+ unsigned char* s, /* pointer to char data, parity undecoded */
+ UTILS::COLOR::Color* d, /* pointer to frame buffer of top left pixel */
+ unsigned char* ax, /* array[0..12] of x-offsets, array[0..10] of y-offsets for each pixel */
+ UTILS::COLOR::Color fgcolor,
+ UTILS::COLOR::Color bgcolor);
+ void RenderCharIntern(TextRenderInfo_t* RenderInfo, int Char, TextPageAttr_t *Attribute, int zoom, int yoffset);
+ int RenderChar(
+ UTILS::COLOR::Color* buffer, // pointer to render buffer, min. fontheight*2*xres
+ int xres, // length of 1 line in render buffer
+ int Char, // character to render
+ int*
+ pPosX, // left border for rendering relative to *buffer, will be set to right border after rendering
+ int PosY, // vertical position of char in *buffer
+ TextPageAttr_t* Attribute, // Attributes of Char
+ bool zoom, // 1= character will be rendered in double height
+ int curfontwidth, // rendering width of character
+ int curfontwidth2, // rendering width of next character (needed for doublewidth)
+ int fontheight, // height of character
+ bool transpmode, // 1= transparent display
+ unsigned char* axdrcs, // width and height of DRCS-chars
+ int Ascender);
+ TextPageinfo_t* DecodePage(bool showl25, // 1=decode Level2.5-graphics
+ unsigned char* PageChar, // page buffer, min. 25*40
+ TextPageAttr_t *PageAtrb, // attribute buffer, min 25*40
+ bool HintMode, // 1=show hidden information
+ bool showflof); // 1=decode FLOF-line
+ void Eval_l25(unsigned char* page_char, TextPageAttr_t *PageAtrb, bool HintMode);
+ void Eval_Object(int iONr, TextCachedPage_t *pstCachedPage,
+ unsigned char *pAPx, unsigned char *pAPy,
+ unsigned char *pAPx0, unsigned char *pAPy0,
+ tObjType ObjType, unsigned char* pagedata, unsigned char* page_char, TextPageAttr_t* PageAtrb);
+ void Eval_NumberedObject(int p, int s, int packet, int triplet, int high,
+ unsigned char *pAPx, unsigned char *pAPy,
+ unsigned char *pAPx0, unsigned char *pAPy0, unsigned char* page_char, TextPageAttr_t* PageAtrb);
+ int Eval_Triplet(int iOData, TextCachedPage_t *pstCachedPage,
+ unsigned char *pAPx, unsigned char *pAPy,
+ unsigned char *pAPx0, unsigned char *pAPy0,
+ unsigned char *drcssubp, unsigned char *gdrcssubp,
+ signed char *endcol, TextPageAttr_t *attrPassive, unsigned char* pagedata, unsigned char* page_char, TextPageAttr_t* PageAtrb);
+ int iTripletNumber2Data(int iONr, TextCachedPage_t *pstCachedPage, unsigned char* pagedata);
+ int SetNational(unsigned char sec);
+ int NextHex(int i);
+ void SetColors(const unsigned short *pcolormap, int offset, int number);
+ UTILS::COLOR::Color GetColorRGB(enumTeletextColor ttc);
+
+ static FT_Error MyFaceRequester(FTC_FaceID face_id, FT_Library library, FT_Pointer request_data, FT_Face *aface);
+
+ std::string m_teletextFont; /* Path to teletext font */
+ int m_YOffset; /* Swap position for Front buffer and Back buffer */
+ UTILS::COLOR::Color* m_TextureBuffer; /* Texture buffer to hold generated data */
+ bool m_updateTexture; /* Update the texture if set */
+ char prevHeaderPage; /* Needed for texture update if header is changed */
+ char prevTimeSec; /* Needed for Time string update */
+
+ int m_CatchRow; /* for page catching */
+ int m_CatchCol; /* " " " */
+ int m_CatchedPage; /* " " " */
+ int m_PCOldRow; /* " " " */
+ int m_PCOldCol; /* " " " */
+
+ FT_Library m_Library; /* FreeType 2 data */
+ FTC_Manager m_Manager; /* " " " */
+ FTC_SBitCache m_Cache; /* " " " */
+ FTC_SBit m_sBit; /* " " " */
+ FT_Face m_Face; /* " " " */
+ /*! An opaque handle to a cache node object. Each cache node is reference-counted. */
+ FTC_Node m_anode;
+ FTC_ImageTypeRec m_TypeTTF; /* " " " */
+ int m_Ascender; /* " " " */
+
+ int m_TempPage; /* Temporary page number for number input */
+ int m_LastPage; /* Last selected Page */
+ std::shared_ptr<TextCacheStruct_t> m_txtCache; /* Text cache generated by the VideoPlayer if Teletext present */
+ TextRenderInfo_t m_RenderInfo; /* Rendering information of displayed Teletext page */
+};
diff --git a/xbmc/video/TeletextDefines.h b/xbmc/video/TeletextDefines.h
new file mode 100644
index 0000000..1c4884e
--- /dev/null
+++ b/xbmc/video/TeletextDefines.h
@@ -0,0 +1,478 @@
+/*
+ * 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 "threads/CriticalSection.h"
+
+#include <chrono>
+#include <string>
+
+#define FLOFSIZE 4
+#define SUBTITLE_CACHESIZE 50
+#define TELETEXT_PAGE_SIZE (40 * 25)
+
+#define number2char(c) ((c) + (((c) <= 9) ? '0' : ('A' - 10)))
+
+enum /* indices in atrtable */
+{
+ ATR_WB, /* white on black */
+ ATR_PassiveDefault, /* Default for passive objects: white on black, ignore at Black Background Color Substitution */
+ ATR_L250, /* line25 */
+ ATR_L251, /* line25 */
+ ATR_L252, /* line25 */
+ ATR_L253, /* line25 */
+ ATR_TOPMENU0, /* topmenu */
+ ATR_TOPMENU1, /* topmenu */
+ ATR_TOPMENU2, /* topmenu */
+ ATR_TOPMENU3, /* topmenu */
+ ATR_MSG0, /* message */
+ ATR_MSG1, /* message */
+ ATR_MSG2, /* message */
+ ATR_MSG3, /* message */
+ ATR_MSGDRM0, /* message */
+ ATR_MSGDRM1, /* message */
+ ATR_MSGDRM2, /* message */
+ ATR_MSGDRM3, /* message */
+ ATR_MENUHIL0, /* highlight menu line */
+ ATR_MENUHIL1, /* highlight menu line */
+ ATR_MENUHIL2, /* highlight menu line */
+ ATR_MENU0, /* menu line */
+ ATR_MENU1, /* menu line */
+ ATR_MENU2, /* menu line */
+ ATR_MENU3, /* menu line */
+ ATR_MENU4, /* menu line */
+ ATR_MENU5, /* menu line */
+ ATR_MENU6, /* menu line */
+ ATR_CATCHMENU0, /* catch menu line */
+ ATR_CATCHMENU1 /* catch menu line */
+};
+
+/* colortable */
+enum enumTeletextColor
+{
+ TXT_ColorBlack = 0,
+ TXT_ColorRed, /* 1 */
+ TXT_ColorGreen, /* 2 */
+ TXT_ColorYellow, /* 3 */
+ TXT_ColorBlue, /* 4 */
+ TXT_ColorMagenta, /* 5 */
+ TXT_ColorCyan, /* 6 */
+ TXT_ColorWhite, /* 7 */
+ TXT_ColorMenu1 = (4*8),
+ TXT_ColorMenu2,
+ TXT_ColorMenu3,
+ TXT_ColorTransp,
+ TXT_ColorTransp2,
+ TXT_Color_SIZECOLTABLE
+};
+
+enum /* options for charset */
+{
+ C_G0P = 0, /* primary G0 */
+ C_G0S, /* secondary G0 */
+ C_G1C, /* G1 contiguous */
+ C_G1S, /* G1 separate */
+ C_G2,
+ C_G3,
+ C_OFFSET_DRCS = 32
+ /* 32..47: 32+subpage# GDRCS (offset/20 in PageChar) */
+ /* 48..63: 48+subpage# DRCS (offset/20 in PageChar) */
+};
+
+enum /* page function */
+{
+ FUNC_LOP = 0, /* Basic Level 1 Teletext page (LOP) */
+ FUNC_DATA, /* Data broadcasting page coded according to EN 300 708 [2] clause 4 */
+ FUNC_GPOP, /* Global Object definition page (GPOP) - (see clause 10.5.1) */
+ FUNC_POP, /* Normal Object definition page (POP) - (see clause 10.5.1) */
+ FUNC_GDRCS, /* Global DRCS downloading page (GDRCS) - (see clause 10.5.2) */
+ FUNC_DRCS, /* Normal DRCS downloading page (DRCS) - (see clause 10.5.2) */
+ FUNC_MOT, /* Magazine Organization table (MOT) - (see clause 10.6) */
+ FUNC_MIP, /* Magazine Inventory page (MIP) - (see clause 11.3) */
+ FUNC_BTT, /* Basic TOP table (BTT) } */
+ FUNC_AIT, /* Additional Information Table (AIT) } (see clause 11.2) */
+ FUNC_MPT, /* Multi-page table (MPT) } */
+ FUNC_MPTEX, /* Multi-page extension table (MPT-EX) } */
+ FUNC_TRIGGER /* Page contain trigger messages defined according to [8] */
+};
+
+enum
+{
+ NAT_DEFAULT = 0,
+ NAT_CZ = 1,
+ NAT_UK = 2,
+ NAT_ET = 3,
+ NAT_FR = 4,
+ NAT_DE = 5,
+ NAT_IT = 6,
+ NAT_LV = 7,
+ NAT_PL = 8,
+ NAT_SP = 9,
+ NAT_RO = 10,
+ NAT_SR = 11,
+ NAT_SW = 12,
+ NAT_TR = 13,
+ NAT_MAX_FROM_HEADER = 13,
+ NAT_SC = 14,
+ NAT_RB = 15,
+ NAT_UA = 16,
+ NAT_GR = 17,
+ NAT_HB = 18,
+ NAT_AR = 19
+};
+
+const unsigned char CountryConversionTable[] = { NAT_UK, NAT_DE, NAT_SW, NAT_IT, NAT_FR, NAT_SP, NAT_CZ, NAT_RO};
+const unsigned char MapTblFG[] = { 0, 0, 8, 8, 16, 16, 16 };
+const unsigned char MapTblBG[] = { 8, 16, 8, 16, 8, 16, 24 };
+const unsigned short DefaultColors[] = /* 0x0bgr */
+{
+ 0x000, 0x00f, 0x0f0, 0x0ff, 0xf00, 0xf0f, 0xff0, 0xfff,
+ 0x000, 0x007, 0x070, 0x077, 0x700, 0x707, 0x770, 0x777,
+ 0x50f, 0x07f, 0x7f0, 0xbff, 0xac0, 0x005, 0x256, 0x77c,
+ 0x333, 0x77f, 0x7f7, 0x7ff, 0xf77, 0xf7f, 0xff7, 0xddd,
+ 0x420, 0x210, 0x420, 0x000, 0x000
+};
+
+/* hamming table */
+const unsigned char dehamming[] =
+{
+ 0x01, 0xFF, 0x01, 0x01, 0xFF, 0x00, 0x01, 0xFF, 0xFF, 0x02, 0x01, 0xFF, 0x0A, 0xFF, 0xFF, 0x07,
+ 0xFF, 0x00, 0x01, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x06, 0xFF, 0xFF, 0x0B, 0xFF, 0x00, 0x03, 0xFF,
+ 0xFF, 0x0C, 0x01, 0xFF, 0x04, 0xFF, 0xFF, 0x07, 0x06, 0xFF, 0xFF, 0x07, 0xFF, 0x07, 0x07, 0x07,
+ 0x06, 0xFF, 0xFF, 0x05, 0xFF, 0x00, 0x0D, 0xFF, 0x06, 0x06, 0x06, 0xFF, 0x06, 0xFF, 0xFF, 0x07,
+ 0xFF, 0x02, 0x01, 0xFF, 0x04, 0xFF, 0xFF, 0x09, 0x02, 0x02, 0xFF, 0x02, 0xFF, 0x02, 0x03, 0xFF,
+ 0x08, 0xFF, 0xFF, 0x05, 0xFF, 0x00, 0x03, 0xFF, 0xFF, 0x02, 0x03, 0xFF, 0x03, 0xFF, 0x03, 0x03,
+ 0x04, 0xFF, 0xFF, 0x05, 0x04, 0x04, 0x04, 0xFF, 0xFF, 0x02, 0x0F, 0xFF, 0x04, 0xFF, 0xFF, 0x07,
+ 0xFF, 0x05, 0x05, 0x05, 0x04, 0xFF, 0xFF, 0x05, 0x06, 0xFF, 0xFF, 0x05, 0xFF, 0x0E, 0x03, 0xFF,
+ 0xFF, 0x0C, 0x01, 0xFF, 0x0A, 0xFF, 0xFF, 0x09, 0x0A, 0xFF, 0xFF, 0x0B, 0x0A, 0x0A, 0x0A, 0xFF,
+ 0x08, 0xFF, 0xFF, 0x0B, 0xFF, 0x00, 0x0D, 0xFF, 0xFF, 0x0B, 0x0B, 0x0B, 0x0A, 0xFF, 0xFF, 0x0B,
+ 0x0C, 0x0C, 0xFF, 0x0C, 0xFF, 0x0C, 0x0D, 0xFF, 0xFF, 0x0C, 0x0F, 0xFF, 0x0A, 0xFF, 0xFF, 0x07,
+ 0xFF, 0x0C, 0x0D, 0xFF, 0x0D, 0xFF, 0x0D, 0x0D, 0x06, 0xFF, 0xFF, 0x0B, 0xFF, 0x0E, 0x0D, 0xFF,
+ 0x08, 0xFF, 0xFF, 0x09, 0xFF, 0x09, 0x09, 0x09, 0xFF, 0x02, 0x0F, 0xFF, 0x0A, 0xFF, 0xFF, 0x09,
+ 0x08, 0x08, 0x08, 0xFF, 0x08, 0xFF, 0xFF, 0x09, 0x08, 0xFF, 0xFF, 0x0B, 0xFF, 0x0E, 0x03, 0xFF,
+ 0xFF, 0x0C, 0x0F, 0xFF, 0x04, 0xFF, 0xFF, 0x09, 0x0F, 0xFF, 0x0F, 0x0F, 0xFF, 0x0E, 0x0F, 0xFF,
+ 0x08, 0xFF, 0xFF, 0x05, 0xFF, 0x0E, 0x0D, 0xFF, 0xFF, 0x0E, 0x0F, 0xFF, 0x0E, 0x0E, 0xFF, 0x0E
+};
+
+/* odd parity table, error=0x20 (space) */
+const unsigned char deparity[] =
+{
+ ' ' , 0x01, 0x02, ' ' , 0x04, ' ' , ' ' , 0x07, 0x08, ' ' , ' ' , 0x0b, ' ' , 0x0d, 0x0e, ' ' ,
+ 0x10, ' ' , ' ' , 0x13, ' ' , 0x15, 0x16, ' ' , ' ' , 0x19, 0x1a, ' ' , 0x1c, ' ' , ' ' , 0x1f,
+ 0x20, ' ' , ' ' , 0x23, ' ' , 0x25, 0x26, ' ' , ' ' , 0x29, 0x2a, ' ' , 0x2c, ' ' , ' ' , 0x2f,
+ ' ' , 0x31, 0x32, ' ' , 0x34, ' ' , ' ' , 0x37, 0x38, ' ' , ' ' , 0x3b, ' ' , 0x3d, 0x3e, ' ' ,
+ 0x40, ' ' , ' ' , 0x43, ' ' , 0x45, 0x46, ' ' , ' ' , 0x49, 0x4a, ' ' , 0x4c, ' ' , ' ' , 0x4f,
+ ' ' , 0x51, 0x52, ' ' , 0x54, ' ' , ' ' , 0x57, 0x58, ' ' , ' ' , 0x5b, ' ' , 0x5d, 0x5e, ' ' ,
+ ' ' , 0x61, 0x62, ' ' , 0x64, ' ' , ' ' , 0x67, 0x68, ' ' , ' ' , 0x6b, ' ' , 0x6d, 0x6e, ' ' ,
+ 0x70, ' ' , ' ' , 0x73, ' ' , 0x75, 0x76, ' ' , ' ' , 0x79, 0x7a, ' ' , 0x7c, ' ' , ' ' , 0x7f,
+ 0x00, ' ' , ' ' , 0x03, ' ' , 0x05, 0x06, ' ' , ' ' , 0x09, 0x0a, ' ' , 0x0c, ' ' , ' ' , 0x0f,
+ ' ' , 0x11, 0x12, ' ' , 0x14, ' ' , ' ' , 0x17, 0x18, ' ' , ' ' , 0x1b, ' ' , 0x1d, 0x1e, ' ' ,
+ ' ' , 0x21, 0x22, ' ' , 0x24, ' ' , ' ' , 0x27, 0x28, ' ' , ' ' , 0x2b, ' ' , 0x2d, 0x2e, ' ' ,
+ 0x30, ' ' , ' ' , 0x33, ' ' , 0x35, 0x36, ' ' , ' ' , 0x39, 0x3a, ' ' , 0x3c, ' ' , ' ' , 0x3f,
+ ' ' , 0x41, 0x42, ' ' , 0x44, ' ' , ' ' , 0x47, 0x48, ' ' , ' ' , 0x4b, ' ' , 0x4d, 0x4e, ' ' ,
+ 0x50, ' ' , ' ' , 0x53, ' ' , 0x55, 0x56, ' ' , ' ' , 0x59, 0x5a, ' ' , 0x5c, ' ' , ' ' , 0x5f,
+ 0x60, ' ' , ' ' , 0x63, ' ' , 0x65, 0x66, ' ' , ' ' , 0x69, 0x6a, ' ' , 0x6c, ' ' , ' ' , 0x6f,
+ ' ' , 0x71, 0x72, ' ' , 0x74, ' ' , ' ' , 0x77, 0x78, ' ' , ' ' , 0x7b, ' ' , 0x7d, 0x7e, ' ' ,
+};
+
+/*
+ * [AleVT]
+ *
+ * This table generates the parity checks for hamm24/18 decoding.
+ * Bit 0 is for test A, 1 for B, ...
+ *
+ * Thanks to R. Gancarz for this fine table *g*
+ */
+const unsigned char hamm24par[3][256] =
+{
+ {
+ /* Parities of first byte */
+ 0, 33, 34, 3, 35, 2, 1, 32, 36, 5, 6, 39, 7, 38, 37, 4,
+ 37, 4, 7, 38, 6, 39, 36, 5, 1, 32, 35, 2, 34, 3, 0, 33,
+ 38, 7, 4, 37, 5, 36, 39, 6, 2, 35, 32, 1, 33, 0, 3, 34,
+ 3, 34, 33, 0, 32, 1, 2, 35, 39, 6, 5, 36, 4, 37, 38, 7,
+ 39, 6, 5, 36, 4, 37, 38, 7, 3, 34, 33, 0, 32, 1, 2, 35,
+ 2, 35, 32, 1, 33, 0, 3, 34, 38, 7, 4, 37, 5, 36, 39, 6,
+ 1, 32, 35, 2, 34, 3, 0, 33, 37, 4, 7, 38, 6, 39, 36, 5,
+ 36, 5, 6, 39, 7, 38, 37, 4, 0, 33, 34, 3, 35, 2, 1, 32,
+ 40, 9, 10, 43, 11, 42, 41, 8, 12, 45, 46, 15, 47, 14, 13, 44,
+ 13, 44, 47, 14, 46, 15, 12, 45, 41, 8, 11, 42, 10, 43, 40, 9,
+ 14, 47, 44, 13, 45, 12, 15, 46, 42, 11, 8, 41, 9, 40, 43, 10,
+ 43, 10, 9, 40, 8, 41, 42, 11, 15, 46, 45, 12, 44, 13, 14, 47,
+ 15, 46, 45, 12, 44, 13, 14, 47, 43, 10, 9, 40, 8, 41, 42, 11,
+ 42, 11, 8, 41, 9, 40, 43, 10, 14, 47, 44, 13, 45, 12, 15, 46,
+ 41, 8, 11, 42, 10, 43, 40, 9, 13, 44, 47, 14, 46, 15, 12, 45,
+ 12, 45, 46, 15, 47, 14, 13, 44, 40, 9, 10, 43, 11, 42, 41, 8
+ }, {
+ /* Parities of second byte */
+ 0, 41, 42, 3, 43, 2, 1, 40, 44, 5, 6, 47, 7, 46, 45, 4,
+ 45, 4, 7, 46, 6, 47, 44, 5, 1, 40, 43, 2, 42, 3, 0, 41,
+ 46, 7, 4, 45, 5, 44, 47, 6, 2, 43, 40, 1, 41, 0, 3, 42,
+ 3, 42, 41, 0, 40, 1, 2, 43, 47, 6, 5, 44, 4, 45, 46, 7,
+ 47, 6, 5, 44, 4, 45, 46, 7, 3, 42, 41, 0, 40, 1, 2, 43,
+ 2, 43, 40, 1, 41, 0, 3, 42, 46, 7, 4, 45, 5, 44, 47, 6,
+ 1, 40, 43, 2, 42, 3, 0, 41, 45, 4, 7, 46, 6, 47, 44, 5,
+ 44, 5, 6, 47, 7, 46, 45, 4, 0, 41, 42, 3, 43, 2, 1, 40,
+ 48, 25, 26, 51, 27, 50, 49, 24, 28, 53, 54, 31, 55, 30, 29, 52,
+ 29, 52, 55, 30, 54, 31, 28, 53, 49, 24, 27, 50, 26, 51, 48, 25,
+ 30, 55, 52, 29, 53, 28, 31, 54, 50, 27, 24, 49, 25, 48, 51, 26,
+ 51, 26, 25, 48, 24, 49, 50, 27, 31, 54, 53, 28, 52, 29, 30, 55,
+ 31, 54, 53, 28, 52, 29, 30, 55, 51, 26, 25, 48, 24, 49, 50, 27,
+ 50, 27, 24, 49, 25, 48, 51, 26, 30, 55, 52, 29, 53, 28, 31, 54,
+ 49, 24, 27, 50, 26, 51, 48, 25, 29, 52, 55, 30, 54, 31, 28, 53,
+ 28, 53, 54, 31, 55, 30, 29, 52, 48, 25, 26, 51, 27, 50, 49, 24
+ }, {
+ /* Parities of third byte */
+ 63, 14, 13, 60, 12, 61, 62, 15, 11, 58, 57, 8, 56, 9, 10, 59,
+ 10, 59, 56, 9, 57, 8, 11, 58, 62, 15, 12, 61, 13, 60, 63, 14,
+ 9, 56, 59, 10, 58, 11, 8, 57, 61, 12, 15, 62, 14, 63, 60, 13,
+ 60, 13, 14, 63, 15, 62, 61, 12, 8, 57, 58, 11, 59, 10, 9, 56,
+ 8, 57, 58, 11, 59, 10, 9, 56, 60, 13, 14, 63, 15, 62, 61, 12,
+ 61, 12, 15, 62, 14, 63, 60, 13, 9, 56, 59, 10, 58, 11, 8, 57,
+ 62, 15, 12, 61, 13, 60, 63, 14, 10, 59, 56, 9, 57, 8, 11, 58,
+ 11, 58, 57, 8, 56, 9, 10, 59, 63, 14, 13, 60, 12, 61, 62, 15,
+ 31, 46, 45, 28, 44, 29, 30, 47, 43, 26, 25, 40, 24, 41, 42, 27,
+ 42, 27, 24, 41, 25, 40, 43, 26, 30, 47, 44, 29, 45, 28, 31, 46,
+ 41, 24, 27, 42, 26, 43, 40, 25, 29, 44, 47, 30, 46, 31, 28, 45,
+ 28, 45, 46, 31, 47, 30, 29, 44, 40, 25, 26, 43, 27, 42, 41, 24,
+ 40, 25, 26, 43, 27, 42, 41, 24, 28, 45, 46, 31, 47, 30, 29, 44,
+ 29, 44, 47, 30, 46, 31, 28, 45, 41, 24, 27, 42, 26, 43, 40, 25,
+ 30, 47, 44, 29, 45, 28, 31, 46, 42, 27, 24, 41, 25, 40, 43, 26,
+ 43, 26, 25, 40, 24, 41, 42, 27, 31, 46, 45, 28, 44, 29, 30, 47
+ }
+};
+
+/*
+ * [AleVT]
+ *
+ * Table to extract the lower 4 bit from hamm24/18 encoded bytes
+ */
+const unsigned char hamm24val[256] =
+{
+ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1,
+ 2, 2, 2, 2, 3, 3, 3, 3, 2, 2, 2, 2, 3, 3, 3, 3,
+ 4, 4, 4, 4, 5, 5, 5, 5, 4, 4, 4, 4, 5, 5, 5, 5,
+ 6, 6, 6, 6, 7, 7, 7, 7, 6, 6, 6, 6, 7, 7, 7, 7,
+ 8, 8, 8, 8, 9, 9, 9, 9, 8, 8, 8, 8, 9, 9, 9, 9,
+ 10, 10, 10, 10, 11, 11, 11, 11, 10, 10, 10, 10, 11, 11, 11, 11,
+ 12, 12, 12, 12, 13, 13, 13, 13, 12, 12, 12, 12, 13, 13, 13, 13,
+ 14, 14, 14, 14, 15, 15, 15, 15, 14, 14, 14, 14, 15, 15, 15, 15,
+ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1,
+ 2, 2, 2, 2, 3, 3, 3, 3, 2, 2, 2, 2, 3, 3, 3, 3,
+ 4, 4, 4, 4, 5, 5, 5, 5, 4, 4, 4, 4, 5, 5, 5, 5,
+ 6, 6, 6, 6, 7, 7, 7, 7, 6, 6, 6, 6, 7, 7, 7, 7,
+ 8, 8, 8, 8, 9, 9, 9, 9, 8, 8, 8, 8, 9, 9, 9, 9,
+ 10, 10, 10, 10, 11, 11, 11, 11, 10, 10, 10, 10, 11, 11, 11, 11,
+ 12, 12, 12, 12, 13, 13, 13, 13, 12, 12, 12, 12, 13, 13, 13, 13,
+ 14, 14, 14, 14, 15, 15, 15, 15, 14, 14, 14, 14, 15, 15, 15, 15
+};
+
+const signed char hamm24err[64] =
+{
+ 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1,
+};
+
+/*
+ * [AleVT]
+ *
+ * Mapping from parity checks made by table hamm24par to faulty bit
+ * in the decoded 18 bit word.
+ */
+const unsigned int hamm24cor[64] =
+{
+ 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000,
+ 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000,
+ 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000,
+ 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000,
+ 0x00000, 0x00000, 0x00000, 0x00001, 0x00000, 0x00002, 0x00004, 0x00008,
+ 0x00000, 0x00010, 0x00020, 0x00040, 0x00080, 0x00100, 0x00200, 0x00400,
+ 0x00000, 0x00800, 0x01000, 0x02000, 0x04000, 0x08000, 0x10000, 0x20000,
+ 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000, 0x00000,
+};
+
+inline int IsDec(int i)
+{
+ return ((i & 0x00F) <= 9) && ((i & 0x0F0) <= 0x90);
+}
+
+/* struct for page attributes */
+typedef struct
+{
+ unsigned char fg :6; /* foreground color */
+ unsigned char bg :6; /* background color */
+ unsigned char charset :6; /* see enum above */
+ unsigned char doubleh :1; /* double height */
+ unsigned char doublew :1; /* double width */
+ /* ignore at Black Background Color Substitution */
+ /* black background set by New Background ($1d) instead of start-of-row default or Black Backgr. ($1c) */
+ /* or black background set by level 2.5 extensions */
+ unsigned char IgnoreAtBlackBgSubst:1;
+ unsigned char concealed:1; /* concealed information */
+ unsigned char inverted :1; /* colors inverted */
+ unsigned char flashing :5; /* flash mode */
+ unsigned char diacrit :4; /* diacritical mark */
+ unsigned char underline:1; /* Text underlined */
+ unsigned char boxwin :1; /* Text boxed/windowed */
+ unsigned char setX26 :1; /* Char is set by packet X/26 (no national subset used) */
+ unsigned char setG0G2 :7; /* G0+G2 set designation */
+} TextPageAttr_t;
+
+/* struct for (G)POP/(G)DRCS links for level 2.5, allocated at reception of p27/4 or /5, initialized with 0 after allocation */
+typedef struct
+{
+ unsigned short page; /* linked page number */
+ unsigned short subpage; /* 1 bit for each needed (1) subpage */
+ unsigned char l25:1; /* 1: page required at level 2.5 */
+ unsigned char l35:1; /* 1: page required at level 3.5 */
+ unsigned char drcs:1; /* 1: link to (G)DRCS, 0: (G)POP */
+ unsigned char local:1; /* 1: global (G*), 0: local */
+} Textp27_t;
+
+/* struct for extension data for level 2.5, allocated at reception, initialized with 0 after allocation */
+typedef struct
+{
+ unsigned char *p26[16]; /* array of pointers to max. 16 designation codes of packet 26 */
+ Textp27_t *p27; /* array of 4 structs for (G)POP/(G)DRCS links for level 2.5 */
+ unsigned short bgr[16]; /* CLUT 2+3, 2*8 colors, 0x0bgr */
+ unsigned char DefaultCharset:7; /* default G0/G2 charset + national option */
+ unsigned char LSP:1; /* 1: left side panel to be displayed */
+ unsigned char SecondCharset:7; /* second G0 charset */
+ unsigned char RSP:1; /* 1: right side panel to be displayed */
+ unsigned char DefScreenColor:5; /* default screen color (above and below lines 0..24) */
+ unsigned char ColorTableRemapping:3; /* 1: index in table of CLUTs to use */
+ unsigned char DefRowColor:5; /* default row color (left and right to lines 0..24) */
+ unsigned char BlackBgSubst:1; /* 1: substitute black background (as result of start-of-line or 1c, not 00/10+1d) */
+ unsigned char SPL25:1; /* 1: side panel required at level 2.5 */
+ unsigned char p28Received:1; /* 1: extension data valid (p28/0 received) */
+ unsigned char LSPColumns:4; /* number of columns in left side panel, 0->16, rsp=16-lsp */
+} TextExtData_t;
+
+/* struct for pageinfo, max. 16 Bytes, at beginning of each cached page buffer, initialized with 0 after allocation */
+typedef struct
+{
+ unsigned char *p24; /* pointer to lines 25+26 (packets 24+25) (2*40 bytes) for FLOF or level 2.5 data */
+ TextExtData_t *ext; /* pointer to array[16] of data for level 2.5 */
+ unsigned char boxed :1; /* p0: boxed (newsflash or subtitle) */
+ unsigned char nationalvalid :1; /* p0: national option character subset is valid (no biterror detected) */
+ unsigned char national :3; /* p0: national option character subset */
+ unsigned char function :3; /* p28/0: page function */
+} TextPageinfo_t;
+
+/* one cached page: struct for pageinfo, 24 lines page data */
+typedef struct
+{
+ TextPageinfo_t pageinfo;
+ unsigned char p0[24]; /* packet 0: center of headline */
+ unsigned char data[23*40]; /* packet 1-23 */
+} TextCachedPage_t;
+
+typedef struct
+{
+ short page;
+ short language;
+} TextSubtitle_t;
+
+typedef struct
+{
+ bool Valid;
+ std::chrono::time_point<std::chrono::steady_clock> Timestamp;
+ unsigned char PageChar[TELETEXT_PAGE_SIZE];
+ TextPageAttr_t PageAtrb[TELETEXT_PAGE_SIZE];
+} TextSubtitleCache_t;
+
+/* main data structure */
+typedef struct TextCacheStruct_t
+{
+ int CurrentPage[9];
+ int CurrentSubPage[9];
+ TextExtData_t *astP29[9];
+ TextCachedPage_t *astCachetable[0x900][0x80];
+ unsigned char SubPageTable[0x900];
+ unsigned char BasicTop[0x900];
+ short FlofPages[0x900][FLOFSIZE];
+ char ADIPTable[0x900][13];
+ int ADIP_PgMax;
+ int ADIP_Pg[10];
+ bool BTTok;
+ int CachedPages;
+ int PageReceiving;
+ int Page;
+ int SubPage;
+ bool PageUpdate;
+ int NationalSubset;
+ int NationalSubsetSecondary;
+ bool ZapSubpageManual;
+ TextSubtitle_t SubtitlePages[8];
+ unsigned char TimeString[8];
+ int vtxtpid;
+
+ /* cachetable for packets 29 (one for each magazine) */
+ /* cachetable */
+ unsigned char FullRowColor[25];
+ unsigned char FullScrColor;
+ unsigned char tAPx, tAPy; /* temporary offset to Active Position for objects */
+ short pop, gpop, drcs, gdrcs;
+ unsigned short *ColorTable;
+
+ std::string line30;
+
+ // TODO: We should get rid of this public mutex. Here are the details: https://github.com/xbmc/xbmc/pull/22226
+ CCriticalSection m_critSection;
+} TextCacheStruct_t;
+
+/* struct for all Information needed for Page Rendering */
+typedef struct
+{
+ bool PageCatching;
+ bool TranspMode;
+ bool HintMode;
+ bool ShowFlof;
+ bool Show39;
+ bool Showl25;
+ bool ShowHex;
+ int ZoomMode;
+
+ int InputCounter;
+ int ClearBBColor;
+ int Prev_100, Prev_10, Next_10, Next_100;
+ int Height;
+ int Width;
+ int FontHeight;
+ int FontWidth;
+ int FontWidth_Normal;
+ unsigned short rd0[TXT_Color_SIZECOLTABLE];
+ unsigned short gn0[TXT_Color_SIZECOLTABLE];
+ unsigned short bl0[TXT_Color_SIZECOLTABLE];
+ unsigned short tr0[TXT_Color_SIZECOLTABLE];
+ TextSubtitleCache_t *SubtitleCache[SUBTITLE_CACHESIZE];
+ unsigned char PageChar[25*40];
+ TextPageAttr_t PageAtrb[25*40];
+ TextPageinfo_t *PageInfo;
+ int PosX;
+ int PosY;
+ int nofirst;
+ unsigned char axdrcs[12+1+10+1];
+ int TTFShiftX, TTFShiftY; /* parameters for adapting to various TTF fonts */
+ bool Boxed;
+ int ScreenMode, PrevScreenMode;
+ bool DelayStarted;
+ unsigned int SubtitleDelay;
+} TextRenderInfo_t;
+
+class CDVDTeletextTools
+{
+public:
+ static void NextDec(int *i);
+ static void PrevDec(int *i);
+ static void Hex2Str(char *s, unsigned int n);
+ static signed int deh24(unsigned char *p);
+};
diff --git a/xbmc/video/VideoDatabase.cpp b/xbmc/video/VideoDatabase.cpp
new file mode 100644
index 0000000..c000d68
--- /dev/null
+++ b/xbmc/video/VideoDatabase.cpp
@@ -0,0 +1,11356 @@
+/*
+ * Copyright (C) 2016-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 "VideoDatabase.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "URL.h"
+#include "Util.h"
+#include "VideoInfoScanner.h"
+#include "XBDateTime.h"
+#include "addons/AddonManager.h"
+#include "dbwrappers/dataset.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "filesystem/PluginDirectory.h"
+#include "filesystem/StackDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/Artist.h"
+#include "playlists/SmartPlayList.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "utils/FileUtils.h"
+#include "utils/GroupUtils.h"
+#include "utils/LabelFormatter.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+#include "video/VideoDbUrl.h"
+#include "video/VideoInfoTag.h"
+#include "video/VideoLibraryQueue.h"
+#include "video/windows/GUIWindowVideoBase.h"
+
+#include <algorithm>
+#include <map>
+#include <memory>
+#include <string>
+#include <unordered_set>
+#include <vector>
+
+using namespace dbiplus;
+using namespace XFILE;
+using namespace VIDEO;
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+using namespace KODI::GUILIB;
+
+//********************************************************************************************************************************
+CVideoDatabase::CVideoDatabase(void) = default;
+
+//********************************************************************************************************************************
+CVideoDatabase::~CVideoDatabase(void) = default;
+
+//********************************************************************************************************************************
+bool CVideoDatabase::Open()
+{
+ return CDatabase::Open(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseVideo);
+}
+
+void CVideoDatabase::CreateTables()
+{
+ CLog::Log(LOGINFO, "create bookmark table");
+ m_pDS->exec("CREATE TABLE bookmark ( idBookmark integer primary key, idFile integer, timeInSeconds double, totalTimeInSeconds double, thumbNailImage text, player text, playerState text, type integer)\n");
+
+ CLog::Log(LOGINFO, "create settings table");
+ m_pDS->exec("CREATE TABLE settings ( idFile integer, Deinterlace bool,"
+ "ViewMode integer,ZoomAmount float, PixelRatio float, VerticalShift float, AudioStream integer, SubtitleStream integer,"
+ "SubtitleDelay float, SubtitlesOn bool, Brightness float, Contrast float, Gamma float,"
+ "VolumeAmplification float, AudioDelay float, ResumeTime integer,"
+ "Sharpness float, NoiseReduction float, NonLinStretch bool, PostProcess bool,"
+ "ScalingMethod integer, DeinterlaceMode integer, StereoMode integer, StereoInvert bool, VideoStream integer,"
+ "TonemapMethod integer, TonemapParam float, Orientation integer, CenterMixLevel integer)\n");
+
+ CLog::Log(LOGINFO, "create stacktimes table");
+ m_pDS->exec("CREATE TABLE stacktimes (idFile integer, times text)\n");
+
+ CLog::Log(LOGINFO, "create genre table");
+ m_pDS->exec("CREATE TABLE genre ( genre_id integer primary key, name TEXT)\n");
+ m_pDS->exec("CREATE TABLE genre_link (genre_id integer, media_id integer, media_type TEXT)");
+
+ CLog::Log(LOGINFO, "create country table");
+ m_pDS->exec("CREATE TABLE country ( country_id integer primary key, name TEXT)");
+ m_pDS->exec("CREATE TABLE country_link (country_id integer, media_id integer, media_type TEXT)");
+
+ CLog::Log(LOGINFO, "create movie table");
+ std::string columns = "CREATE TABLE movie ( idMovie integer primary key, idFile integer";
+
+ for (int i = 0; i < VIDEODB_MAX_COLUMNS; i++)
+ columns += StringUtils::Format(",c{:02} text", i);
+
+ columns += ", idSet integer, userrating integer, premiered text)";
+ m_pDS->exec(columns);
+
+ CLog::Log(LOGINFO, "create actor table");
+ m_pDS->exec("CREATE TABLE actor ( actor_id INTEGER PRIMARY KEY, name TEXT, art_urls TEXT )");
+ m_pDS->exec("CREATE TABLE actor_link(actor_id INTEGER, media_id INTEGER, media_type TEXT, role TEXT, cast_order INTEGER)");
+ m_pDS->exec("CREATE TABLE director_link(actor_id INTEGER, media_id INTEGER, media_type TEXT)");
+ m_pDS->exec("CREATE TABLE writer_link(actor_id INTEGER, media_id INTEGER, media_type TEXT)");
+
+ CLog::Log(LOGINFO, "create path table");
+ m_pDS->exec(
+ "CREATE TABLE path ( idPath integer primary key, strPath text, strContent text, strScraper "
+ "text, strHash text, scanRecursive integer, useFolderNames bool, strSettings text, noUpdate "
+ "bool, exclude bool, allAudio bool, dateAdded text, idParentPath integer)");
+
+ CLog::Log(LOGINFO, "create files table");
+ m_pDS->exec("CREATE TABLE files ( idFile integer primary key, idPath integer, strFilename text, playCount integer, lastPlayed text, dateAdded text)");
+
+ CLog::Log(LOGINFO, "create tvshow table");
+ columns = "CREATE TABLE tvshow ( idShow integer primary key";
+
+ for (int i = 0; i < VIDEODB_MAX_COLUMNS; i++)
+ columns += StringUtils::Format(",c{:02} text", i);
+
+ columns += ", userrating integer, duration INTEGER)";
+ m_pDS->exec(columns);
+
+ CLog::Log(LOGINFO, "create episode table");
+ columns = "CREATE TABLE episode ( idEpisode integer primary key, idFile integer";
+ for (int i = 0; i < VIDEODB_MAX_COLUMNS; i++)
+ {
+ std::string column;
+ if ( i == VIDEODB_ID_EPISODE_SEASON || i == VIDEODB_ID_EPISODE_EPISODE || i == VIDEODB_ID_EPISODE_BOOKMARK)
+ column = StringUtils::Format(",c{:02} varchar(24)", i);
+ else
+ column = StringUtils::Format(",c{:02} text", i);
+
+ columns += column;
+ }
+ columns += ", idShow integer, userrating integer, idSeason integer)";
+ m_pDS->exec(columns);
+
+ CLog::Log(LOGINFO, "create tvshowlinkpath table");
+ m_pDS->exec("CREATE TABLE tvshowlinkpath (idShow integer, idPath integer)\n");
+
+ CLog::Log(LOGINFO, "create movielinktvshow table");
+ m_pDS->exec("CREATE TABLE movielinktvshow ( idMovie integer, IdShow integer)\n");
+
+ CLog::Log(LOGINFO, "create studio table");
+ m_pDS->exec("CREATE TABLE studio ( studio_id integer primary key, name TEXT)\n");
+ m_pDS->exec("CREATE TABLE studio_link (studio_id integer, media_id integer, media_type TEXT)");
+
+ CLog::Log(LOGINFO, "create musicvideo table");
+ columns = "CREATE TABLE musicvideo ( idMVideo integer primary key, idFile integer";
+ for (int i = 0; i < VIDEODB_MAX_COLUMNS; i++)
+ columns += StringUtils::Format(",c{:02} text", i);
+
+ columns += ", userrating integer, premiered text)";
+ m_pDS->exec(columns);
+
+ CLog::Log(LOGINFO, "create streaminfo table");
+ m_pDS->exec("CREATE TABLE streamdetails (idFile integer, iStreamType integer, "
+ "strVideoCodec text, fVideoAspect float, iVideoWidth integer, iVideoHeight integer, "
+ "strAudioCodec text, iAudioChannels integer, strAudioLanguage text, "
+ "strSubtitleLanguage text, iVideoDuration integer, strStereoMode text, strVideoLanguage text, "
+ "strHdrType text)");
+
+ CLog::Log(LOGINFO, "create sets table");
+ m_pDS->exec("CREATE TABLE sets ( idSet integer primary key, strSet text, strOverview text)");
+
+ CLog::Log(LOGINFO, "create seasons table");
+ m_pDS->exec("CREATE TABLE seasons ( idSeason integer primary key, idShow integer, season integer, name text, userrating integer)");
+
+ CLog::Log(LOGINFO, "create art table");
+ m_pDS->exec("CREATE TABLE art(art_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, type TEXT, url TEXT)");
+
+ CLog::Log(LOGINFO, "create tag table");
+ m_pDS->exec("CREATE TABLE tag (tag_id integer primary key, name TEXT)");
+ m_pDS->exec("CREATE TABLE tag_link (tag_id integer, media_id integer, media_type TEXT)");
+
+ CLog::Log(LOGINFO, "create rating table");
+ m_pDS->exec("CREATE TABLE rating (rating_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, rating_type TEXT, rating FLOAT, votes INTEGER)");
+
+ CLog::Log(LOGINFO, "create uniqueid table");
+ m_pDS->exec("CREATE TABLE uniqueid (uniqueid_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, value TEXT, type TEXT)");
+}
+
+void CVideoDatabase::CreateLinkIndex(const char *table)
+{
+ m_pDS->exec(PrepareSQL("CREATE UNIQUE INDEX ix_%s_1 ON %s (name(255))", table, table));
+ m_pDS->exec(PrepareSQL("CREATE UNIQUE INDEX ix_%s_link_1 ON %s_link (%s_id, media_type(20), media_id)", table, table, table));
+ m_pDS->exec(PrepareSQL("CREATE UNIQUE INDEX ix_%s_link_2 ON %s_link (media_id, media_type(20), %s_id)", table, table, table));
+ m_pDS->exec(PrepareSQL("CREATE INDEX ix_%s_link_3 ON %s_link (media_type(20))", table, table));
+}
+
+void CVideoDatabase::CreateForeignLinkIndex(const char *table, const char *foreignkey)
+{
+ m_pDS->exec(PrepareSQL("CREATE UNIQUE INDEX ix_%s_link_1 ON %s_link (%s_id, media_type(20), media_id)", table, table, foreignkey));
+ m_pDS->exec(PrepareSQL("CREATE UNIQUE INDEX ix_%s_link_2 ON %s_link (media_id, media_type(20), %s_id)", table, table, foreignkey));
+ m_pDS->exec(PrepareSQL("CREATE INDEX ix_%s_link_3 ON %s_link (media_type(20))", table, table));
+}
+
+void CVideoDatabase::CreateAnalytics()
+{
+ /* indexes should be added on any columns that are used in */
+ /* a where or a join. primary key on a column is the same as a */
+ /* unique index on that column, so there is no need to add any */
+ /* index if no other columns are referred */
+
+ /* order of indexes are important, for an index to be considered all */
+ /* columns up to the column in question have to have been specified */
+ /* select * from foolink where foo_id = 1, can not take */
+ /* advantage of a index that has been created on ( bar_id, foo_id ) */
+ /* however an index on ( foo_id, bar_id ) will be considered for use */
+
+ CLog::Log(LOGINFO, "{} - creating indices", __FUNCTION__);
+ m_pDS->exec("CREATE INDEX ix_bookmark ON bookmark (idFile, type)");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_settings ON settings ( idFile )\n");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_stacktimes ON stacktimes ( idFile )\n");
+ m_pDS->exec("CREATE INDEX ix_path ON path ( strPath(255) )");
+ m_pDS->exec("CREATE INDEX ix_path2 ON path ( idParentPath )");
+ m_pDS->exec("CREATE INDEX ix_files ON files ( idPath, strFilename(255) )");
+
+ m_pDS->exec("CREATE UNIQUE INDEX ix_movie_file_1 ON movie (idFile, idMovie)");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_movie_file_2 ON movie (idMovie, idFile)");
+
+ m_pDS->exec("CREATE UNIQUE INDEX ix_tvshowlinkpath_1 ON tvshowlinkpath ( idShow, idPath )\n");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_tvshowlinkpath_2 ON tvshowlinkpath ( idPath, idShow )\n");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_movielinktvshow_1 ON movielinktvshow ( idShow, idMovie)\n");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_movielinktvshow_2 ON movielinktvshow ( idMovie, idShow)\n");
+
+ m_pDS->exec("CREATE UNIQUE INDEX ix_episode_file_1 on episode (idEpisode, idFile)");
+ m_pDS->exec("CREATE UNIQUE INDEX id_episode_file_2 on episode (idFile, idEpisode)");
+ std::string createColIndex =
+ StringUtils::Format("CREATE INDEX ix_episode_season_episode on episode (c{:02}, c{:02})",
+ VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_EPISODE_EPISODE);
+ m_pDS->exec(createColIndex);
+ createColIndex = StringUtils::Format("CREATE INDEX ix_episode_bookmark on episode (c{:02})",
+ VIDEODB_ID_EPISODE_BOOKMARK);
+ m_pDS->exec(createColIndex);
+ m_pDS->exec("CREATE INDEX ix_episode_show1 on episode(idEpisode,idShow)");
+ m_pDS->exec("CREATE INDEX ix_episode_show2 on episode(idShow,idEpisode)");
+
+ m_pDS->exec("CREATE UNIQUE INDEX ix_musicvideo_file_1 on musicvideo (idMVideo, idFile)");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_musicvideo_file_2 on musicvideo (idFile, idMVideo)");
+
+ m_pDS->exec("CREATE INDEX ixMovieBasePath ON movie ( c23(12) )");
+ m_pDS->exec("CREATE INDEX ixMusicVideoBasePath ON musicvideo ( c14(12) )");
+ m_pDS->exec("CREATE INDEX ixEpisodeBasePath ON episode ( c19(12) )");
+
+ m_pDS->exec("CREATE INDEX ix_streamdetails ON streamdetails (idFile)");
+ m_pDS->exec("CREATE INDEX ix_seasons ON seasons (idShow, season)");
+ m_pDS->exec("CREATE INDEX ix_art ON art(media_id, media_type(20), type(20))");
+
+ m_pDS->exec("CREATE INDEX ix_rating ON rating(media_id, media_type(20))");
+
+ m_pDS->exec("CREATE INDEX ix_uniqueid1 ON uniqueid(media_id, media_type(20), type(20))");
+ m_pDS->exec("CREATE INDEX ix_uniqueid2 ON uniqueid(media_type(20), value(20))");
+
+ m_pDS->exec("CREATE UNIQUE INDEX ix_actor_1 ON actor (name(255))");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_actor_link_1 ON "
+ "actor_link (actor_id, media_type(20), media_id, role(255))");
+ m_pDS->exec("CREATE INDEX ix_actor_link_2 ON "
+ "actor_link (media_id, media_type(20), actor_id)");
+ m_pDS->exec("CREATE INDEX ix_actor_link_3 ON actor_link (media_type(20))");
+
+ CreateLinkIndex("tag");
+ CreateForeignLinkIndex("director", "actor");
+ CreateForeignLinkIndex("writer", "actor");
+ CreateLinkIndex("studio");
+ CreateLinkIndex("genre");
+ CreateLinkIndex("country");
+
+ CLog::Log(LOGINFO, "{} - creating triggers", __FUNCTION__);
+ m_pDS->exec("CREATE TRIGGER delete_movie AFTER DELETE ON movie FOR EACH ROW BEGIN "
+ "DELETE FROM genre_link WHERE media_id=old.idMovie AND media_type='movie'; "
+ "DELETE FROM actor_link WHERE media_id=old.idMovie AND media_type='movie'; "
+ "DELETE FROM director_link WHERE media_id=old.idMovie AND media_type='movie'; "
+ "DELETE FROM studio_link WHERE media_id=old.idMovie AND media_type='movie'; "
+ "DELETE FROM country_link WHERE media_id=old.idMovie AND media_type='movie'; "
+ "DELETE FROM writer_link WHERE media_id=old.idMovie AND media_type='movie'; "
+ "DELETE FROM movielinktvshow WHERE idMovie=old.idMovie; "
+ "DELETE FROM art WHERE media_id=old.idMovie AND media_type='movie'; "
+ "DELETE FROM tag_link WHERE media_id=old.idMovie AND media_type='movie'; "
+ "DELETE FROM rating WHERE media_id=old.idMovie AND media_type='movie'; "
+ "DELETE FROM uniqueid WHERE media_id=old.idMovie AND media_type='movie'; "
+ "END");
+ m_pDS->exec("CREATE TRIGGER delete_tvshow AFTER DELETE ON tvshow FOR EACH ROW BEGIN "
+ "DELETE FROM actor_link WHERE media_id=old.idShow AND media_type='tvshow'; "
+ "DELETE FROM director_link WHERE media_id=old.idShow AND media_type='tvshow'; "
+ "DELETE FROM studio_link WHERE media_id=old.idShow AND media_type='tvshow'; "
+ "DELETE FROM tvshowlinkpath WHERE idShow=old.idShow; "
+ "DELETE FROM genre_link WHERE media_id=old.idShow AND media_type='tvshow'; "
+ "DELETE FROM movielinktvshow WHERE idShow=old.idShow; "
+ "DELETE FROM seasons WHERE idShow=old.idShow; "
+ "DELETE FROM art WHERE media_id=old.idShow AND media_type='tvshow'; "
+ "DELETE FROM tag_link WHERE media_id=old.idShow AND media_type='tvshow'; "
+ "DELETE FROM rating WHERE media_id=old.idShow AND media_type='tvshow'; "
+ "DELETE FROM uniqueid WHERE media_id=old.idShow AND media_type='tvshow'; "
+ "END");
+ m_pDS->exec("CREATE TRIGGER delete_musicvideo AFTER DELETE ON musicvideo FOR EACH ROW BEGIN "
+ "DELETE FROM actor_link WHERE media_id=old.idMVideo AND media_type='musicvideo'; "
+ "DELETE FROM director_link WHERE media_id=old.idMVideo AND media_type='musicvideo'; "
+ "DELETE FROM genre_link WHERE media_id=old.idMVideo AND media_type='musicvideo'; "
+ "DELETE FROM studio_link WHERE media_id=old.idMVideo AND media_type='musicvideo'; "
+ "DELETE FROM art WHERE media_id=old.idMVideo AND media_type='musicvideo'; "
+ "DELETE FROM tag_link WHERE media_id=old.idMVideo AND media_type='musicvideo'; "
+ "DELETE FROM uniqueid WHERE media_id=old.idMVideo AND media_type='musicvideo'; "
+ "END");
+ m_pDS->exec("CREATE TRIGGER delete_episode AFTER DELETE ON episode FOR EACH ROW BEGIN "
+ "DELETE FROM actor_link WHERE media_id=old.idEpisode AND media_type='episode'; "
+ "DELETE FROM director_link WHERE media_id=old.idEpisode AND media_type='episode'; "
+ "DELETE FROM writer_link WHERE media_id=old.idEpisode AND media_type='episode'; "
+ "DELETE FROM art WHERE media_id=old.idEpisode AND media_type='episode'; "
+ "DELETE FROM rating WHERE media_id=old.idEpisode AND media_type='episode'; "
+ "DELETE FROM uniqueid WHERE media_id=old.idEpisode AND media_type='episode'; "
+ "END");
+ m_pDS->exec("CREATE TRIGGER delete_season AFTER DELETE ON seasons FOR EACH ROW BEGIN "
+ "DELETE FROM art WHERE media_id=old.idSeason AND media_type='season'; "
+ "END");
+ m_pDS->exec("CREATE TRIGGER delete_set AFTER DELETE ON sets FOR EACH ROW BEGIN "
+ "DELETE FROM art WHERE media_id=old.idSet AND media_type='set'; "
+ "END");
+ m_pDS->exec("CREATE TRIGGER delete_person AFTER DELETE ON actor FOR EACH ROW BEGIN "
+ "DELETE FROM art WHERE media_id=old.actor_id AND media_type IN ('actor','artist','writer','director'); "
+ "END");
+ m_pDS->exec("CREATE TRIGGER delete_tag AFTER DELETE ON tag_link FOR EACH ROW BEGIN "
+ "DELETE FROM tag WHERE tag_id=old.tag_id AND tag_id NOT IN (SELECT DISTINCT tag_id FROM tag_link); "
+ "END");
+ m_pDS->exec("CREATE TRIGGER delete_file AFTER DELETE ON files FOR EACH ROW BEGIN "
+ "DELETE FROM bookmark WHERE idFile=old.idFile; "
+ "DELETE FROM settings WHERE idFile=old.idFile; "
+ "DELETE FROM stacktimes WHERE idFile=old.idFile; "
+ "DELETE FROM streamdetails WHERE idFile=old.idFile; "
+ "END");
+
+ CreateViews();
+}
+
+void CVideoDatabase::CreateViews()
+{
+ CLog::Log(LOGINFO, "create episode_view");
+ std::string episodeview = PrepareSQL("CREATE VIEW episode_view AS SELECT "
+ " episode.*,"
+ " files.strFileName AS strFileName,"
+ " path.strPath AS strPath,"
+ " files.playCount AS playCount,"
+ " files.lastPlayed AS lastPlayed,"
+ " files.dateAdded AS dateAdded,"
+ " tvshow.c%02d AS strTitle,"
+ " tvshow.c%02d AS genre,"
+ " tvshow.c%02d AS studio,"
+ " tvshow.c%02d AS premiered,"
+ " tvshow.c%02d AS mpaa,"
+ " bookmark.timeInSeconds AS resumeTimeInSeconds, "
+ " bookmark.totalTimeInSeconds AS totalTimeInSeconds, "
+ " bookmark.playerState AS playerState, "
+ " rating.rating AS rating, "
+ " rating.votes AS votes, "
+ " rating.rating_type AS rating_type, "
+ " uniqueid.value AS uniqueid_value, "
+ " uniqueid.type AS uniqueid_type "
+ "FROM episode"
+ " JOIN files ON"
+ " files.idFile=episode.idFile"
+ " JOIN tvshow ON"
+ " tvshow.idShow=episode.idShow"
+ " JOIN seasons ON"
+ " seasons.idSeason=episode.idSeason"
+ " JOIN path ON"
+ " files.idPath=path.idPath"
+ " LEFT JOIN bookmark ON"
+ " bookmark.idFile=episode.idFile AND bookmark.type=1"
+ " LEFT JOIN rating ON"
+ " rating.rating_id=episode.c%02d"
+ " LEFT JOIN uniqueid ON"
+ " uniqueid.uniqueid_id=episode.c%02d",
+ VIDEODB_ID_TV_TITLE, VIDEODB_ID_TV_GENRE,
+ VIDEODB_ID_TV_STUDIOS, VIDEODB_ID_TV_PREMIERED,
+ VIDEODB_ID_TV_MPAA, VIDEODB_ID_EPISODE_RATING_ID,
+ VIDEODB_ID_EPISODE_IDENT_ID);
+ m_pDS->exec(episodeview);
+
+ CLog::Log(LOGINFO, "create tvshowcounts");
+ std::string tvshowcounts = PrepareSQL("CREATE VIEW tvshowcounts AS SELECT "
+ " tvshow.idShow AS idShow,"
+ " MAX(files.lastPlayed) AS lastPlayed,"
+ " NULLIF(COUNT(episode.c12), 0) AS totalCount,"
+ " COUNT(files.playCount) AS watchedcount,"
+ " NULLIF(COUNT(DISTINCT(episode.c12)), 0) AS totalSeasons, "
+ " MAX(files.dateAdded) as dateAdded "
+ " FROM tvshow"
+ " LEFT JOIN episode ON"
+ " episode.idShow=tvshow.idShow"
+ " LEFT JOIN files ON"
+ " files.idFile=episode.idFile "
+ "GROUP BY tvshow.idShow");
+ m_pDS->exec(tvshowcounts);
+
+ CLog::Log(LOGINFO, "create tvshowlinkpath_minview");
+ // This view only exists to workaround a limitation in MySQL <5.7 which is not able to
+ // perform subqueries in joins.
+ // Also, the correct solution is to remove the path information altogether, since a
+ // TV series can always have multiple paths. It is used in the GUI at the moment, but
+ // such usage should be removed together with this view and the path columns in tvshow_view.
+ //!@todo Remove the hacky selection of a semi-random path for tvshows from the queries and UI
+ std::string tvshowlinkpathview = PrepareSQL("CREATE VIEW tvshowlinkpath_minview AS SELECT "
+ " idShow, "
+ " min(idPath) AS idPath "
+ "FROM tvshowlinkpath "
+ "GROUP BY idShow");
+ m_pDS->exec(tvshowlinkpathview);
+
+ CLog::Log(LOGINFO, "create tvshow_view");
+ std::string tvshowview = PrepareSQL("CREATE VIEW tvshow_view AS SELECT "
+ " tvshow.*,"
+ " path.idParentPath AS idParentPath,"
+ " path.strPath AS strPath,"
+ " tvshowcounts.dateAdded AS dateAdded,"
+ " lastPlayed, totalCount, watchedcount, totalSeasons, "
+ " rating.rating AS rating, "
+ " rating.votes AS votes, "
+ " rating.rating_type AS rating_type, "
+ " uniqueid.value AS uniqueid_value, "
+ " uniqueid.type AS uniqueid_type "
+ "FROM tvshow"
+ " LEFT JOIN tvshowlinkpath_minview ON "
+ " tvshowlinkpath_minview.idShow=tvshow.idShow"
+ " LEFT JOIN path ON"
+ " path.idPath=tvshowlinkpath_minview.idPath"
+ " INNER JOIN tvshowcounts ON"
+ " tvshow.idShow = tvshowcounts.idShow "
+ " LEFT JOIN rating ON"
+ " rating.rating_id=tvshow.c%02d "
+ " LEFT JOIN uniqueid ON"
+ " uniqueid.uniqueid_id=tvshow.c%02d ",
+ VIDEODB_ID_TV_RATING_ID, VIDEODB_ID_TV_IDENT_ID);
+ m_pDS->exec(tvshowview);
+
+ CLog::Log(LOGINFO, "create season_view");
+ std::string seasonview = PrepareSQL("CREATE VIEW season_view AS SELECT "
+ " seasons.idSeason AS idSeason,"
+ " seasons.idShow AS idShow,"
+ " seasons.season AS season,"
+ " seasons.name AS name,"
+ " seasons.userrating AS userrating,"
+ " tvshow_view.strPath AS strPath,"
+ " tvshow_view.c%02d AS showTitle,"
+ " tvshow_view.c%02d AS plot,"
+ " tvshow_view.c%02d AS premiered,"
+ " tvshow_view.c%02d AS genre,"
+ " tvshow_view.c%02d AS studio,"
+ " tvshow_view.c%02d AS mpaa,"
+ " count(DISTINCT episode.idEpisode) AS episodes,"
+ " count(files.playCount) AS playCount,"
+ " min(episode.c%02d) AS aired "
+ "FROM seasons"
+ " JOIN tvshow_view ON"
+ " tvshow_view.idShow = seasons.idShow"
+ " JOIN episode ON"
+ " episode.idShow = seasons.idShow AND episode.c%02d = seasons.season"
+ " JOIN files ON"
+ " files.idFile = episode.idFile "
+ "GROUP BY seasons.idSeason,"
+ " seasons.idShow,"
+ " seasons.season,"
+ " seasons.name,"
+ " seasons.userrating,"
+ " tvshow_view.strPath,"
+ " tvshow_view.c%02d,"
+ " tvshow_view.c%02d,"
+ " tvshow_view.c%02d,"
+ " tvshow_view.c%02d,"
+ " tvshow_view.c%02d,"
+ " tvshow_view.c%02d ",
+ VIDEODB_ID_TV_TITLE, VIDEODB_ID_TV_PLOT, VIDEODB_ID_TV_PREMIERED,
+ VIDEODB_ID_TV_GENRE, VIDEODB_ID_TV_STUDIOS, VIDEODB_ID_TV_MPAA,
+ VIDEODB_ID_EPISODE_AIRED, VIDEODB_ID_EPISODE_SEASON,
+ VIDEODB_ID_TV_TITLE, VIDEODB_ID_TV_PLOT, VIDEODB_ID_TV_PREMIERED,
+ VIDEODB_ID_TV_GENRE, VIDEODB_ID_TV_STUDIOS, VIDEODB_ID_TV_MPAA);
+ m_pDS->exec(seasonview);
+
+ CLog::Log(LOGINFO, "create musicvideo_view");
+ m_pDS->exec(PrepareSQL(
+ "CREATE VIEW musicvideo_view AS SELECT"
+ " musicvideo.*,"
+ " files.strFileName as strFileName,"
+ " path.strPath as strPath,"
+ " files.playCount as playCount,"
+ " files.lastPlayed as lastPlayed,"
+ " files.dateAdded as dateAdded, "
+ " bookmark.timeInSeconds AS resumeTimeInSeconds, "
+ " bookmark.totalTimeInSeconds AS totalTimeInSeconds, "
+ " bookmark.playerState AS playerState, "
+ " uniqueid.value AS uniqueid_value, "
+ " uniqueid.type AS uniqueid_type "
+ "FROM musicvideo"
+ " JOIN files ON"
+ " files.idFile=musicvideo.idFile"
+ " JOIN path ON"
+ " path.idPath=files.idPath"
+ " LEFT JOIN bookmark ON"
+ " bookmark.idFile=musicvideo.idFile AND bookmark.type=1"
+ " LEFT JOIN uniqueid ON"
+ " uniqueid.uniqueid_id=musicvideo.c%02d",
+ VIDEODB_ID_MUSICVIDEO_IDENT_ID));
+
+ CLog::Log(LOGINFO, "create movie_view");
+
+ std::string movieview = PrepareSQL("CREATE VIEW movie_view AS SELECT"
+ " movie.*,"
+ " sets.strSet AS strSet,"
+ " sets.strOverview AS strSetOverview,"
+ " files.strFileName AS strFileName,"
+ " path.strPath AS strPath,"
+ " files.playCount AS playCount,"
+ " files.lastPlayed AS lastPlayed, "
+ " files.dateAdded AS dateAdded, "
+ " bookmark.timeInSeconds AS resumeTimeInSeconds, "
+ " bookmark.totalTimeInSeconds AS totalTimeInSeconds, "
+ " bookmark.playerState AS playerState, "
+ " rating.rating AS rating, "
+ " rating.votes AS votes, "
+ " rating.rating_type AS rating_type, "
+ " uniqueid.value AS uniqueid_value, "
+ " uniqueid.type AS uniqueid_type "
+ "FROM movie"
+ " LEFT JOIN sets ON"
+ " sets.idSet = movie.idSet"
+ " JOIN files ON"
+ " files.idFile=movie.idFile"
+ " JOIN path ON"
+ " path.idPath=files.idPath"
+ " LEFT JOIN bookmark ON"
+ " bookmark.idFile=movie.idFile AND bookmark.type=1"
+ " LEFT JOIN rating ON"
+ " rating.rating_id=movie.c%02d"
+ " LEFT JOIN uniqueid ON"
+ " uniqueid.uniqueid_id=movie.c%02d",
+ VIDEODB_ID_RATING_ID, VIDEODB_ID_IDENT_ID);
+ m_pDS->exec(movieview);
+}
+
+//********************************************************************************************************************************
+int CVideoDatabase::GetPathId(const std::string& strPath)
+{
+ std::string strSQL;
+ try
+ {
+ int idPath=-1;
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string strPath1(strPath);
+ if (URIUtils::IsStack(strPath) || StringUtils::StartsWithNoCase(strPath, "rar://") || StringUtils::StartsWithNoCase(strPath, "zip://"))
+ URIUtils::GetParentPath(strPath,strPath1);
+
+ URIUtils::AddSlashAtEnd(strPath1);
+
+ strSQL=PrepareSQL("select idPath from path where strPath='%s'",strPath1.c_str());
+ m_pDS->query(strSQL);
+ if (!m_pDS->eof())
+ idPath = m_pDS->fv("path.idPath").get_asInt();
+
+ m_pDS->close();
+ return idPath;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} unable to getpath ({})", __FUNCTION__, strSQL);
+ }
+ return -1;
+}
+
+bool CVideoDatabase::GetPaths(std::set<std::string> &paths)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ paths.clear();
+
+ // grab all paths with movie content set
+ if (!m_pDS->query("select strPath,noUpdate from path"
+ " where (strContent = 'movies' or strContent = 'musicvideos')"
+ " and strPath NOT like 'multipath://%%'"
+ " order by strPath"))
+ return false;
+
+ while (!m_pDS->eof())
+ {
+ if (!m_pDS->fv("noUpdate").get_asBool())
+ paths.insert(m_pDS->fv("strPath").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ // then grab all tvshow paths
+ if (!m_pDS->query("select strPath,noUpdate from path"
+ " where ( strContent = 'tvshows'"
+ " or idPath in (select idPath from tvshowlinkpath))"
+ " and strPath NOT like 'multipath://%%'"
+ " order by strPath"))
+ return false;
+
+ while (!m_pDS->eof())
+ {
+ if (!m_pDS->fv("noUpdate").get_asBool())
+ paths.insert(m_pDS->fv("strPath").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ // finally grab all other paths holding a movie which is not a stack or a rar archive
+ // - this isnt perfect but it should do fine in most situations.
+ // reason we need it to hold a movie is stacks from different directories (cdx folders for instance)
+ // not making mistakes must take priority
+ if (!m_pDS->query("select strPath,noUpdate from path"
+ " where idPath in (select idPath from files join movie on movie.idFile=files.idFile)"
+ " and idPath NOT in (select idPath from tvshowlinkpath)"
+ " and idPath NOT in (select idPath from files where strFileName like 'video_ts.ifo')" // dvd folders get stacked to a single item in parent folder
+ " and idPath NOT in (select idPath from files where strFileName like 'index.bdmv')" // bluray folders get stacked to a single item in parent folder
+ " and strPath NOT like 'multipath://%%'"
+ " and strContent NOT in ('movies', 'tvshows', 'None')" // these have been added above
+ " order by strPath"))
+
+ return false;
+ while (!m_pDS->eof())
+ {
+ if (!m_pDS->fv("noUpdate").get_asBool())
+ paths.insert(m_pDS->fv("strPath").get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetPathsLinkedToTvShow(int idShow, std::vector<std::string> &paths)
+{
+ std::string sql;
+ try
+ {
+ sql = PrepareSQL("SELECT strPath FROM path JOIN tvshowlinkpath ON tvshowlinkpath.idPath=path.idPath WHERE idShow=%i", idShow);
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ paths.emplace_back(m_pDS->fv(0).get_asString());
+ m_pDS->next();
+ }
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} error during query: {}", __FUNCTION__, sql);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetPathsForTvShow(int idShow, std::set<int>& paths)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // add base path
+ strSQL = PrepareSQL("SELECT strPath FROM tvshow_view WHERE idShow=%i", idShow);
+ if (m_pDS->query(strSQL))
+ paths.insert(GetPathId(m_pDS->fv(0).get_asString()));
+
+ // add all other known paths
+ strSQL = PrepareSQL("SELECT DISTINCT idPath FROM files JOIN episode ON episode.idFile=files.idFile WHERE episode.idShow=%i",idShow);
+ m_pDS->query(strSQL);
+ while (!m_pDS->eof())
+ {
+ paths.insert(m_pDS->fv(0).get_asInt());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} error during query: {}", __FUNCTION__, strSQL);
+ }
+ return false;
+}
+
+int CVideoDatabase::RunQuery(const std::string &sql)
+{
+ auto start = std::chrono::steady_clock::now();
+
+ int rows = -1;
+ if (m_pDS->query(sql))
+ {
+ rows = m_pDS->num_rows();
+ if (rows == 0)
+ m_pDS->close();
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{} took {} ms for {} items query: {}", __FUNCTION__,
+ duration.count(), rows, sql);
+
+ return rows;
+}
+
+bool CVideoDatabase::GetSubPaths(const std::string &basepath, std::vector<std::pair<int, std::string>>& subpaths)
+{
+ std::string sql;
+ try
+ {
+ if (!m_pDB || !m_pDS)
+ return false;
+
+ std::string path(basepath);
+ URIUtils::AddSlashAtEnd(path);
+ sql = PrepareSQL("SELECT idPath,strPath FROM path WHERE SUBSTR(strPath,1,%i)='%s'"
+ " AND idPath NOT IN (SELECT idPath FROM files WHERE strFileName LIKE 'video_ts.ifo')"
+ " AND idPath NOT IN (SELECT idPath FROM files WHERE strFileName LIKE 'index.bdmv')"
+ , StringUtils::utf8_strlen(path.c_str()), path.c_str());
+
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ subpaths.emplace_back(m_pDS->fv(0).get_asInt(), m_pDS->fv(1).get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} error during query: {}", __FUNCTION__, sql);
+ }
+ return false;
+}
+
+int CVideoDatabase::AddPath(const std::string& strPath, const std::string &parentPath /*= "" */, const CDateTime& dateAdded /* = CDateTime() */)
+{
+ std::string strSQL;
+ try
+ {
+ int idPath = GetPathId(strPath);
+ if (idPath >= 0)
+ return idPath; // already have the path
+
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string strPath1(strPath);
+ if (URIUtils::IsStack(strPath) || StringUtils::StartsWithNoCase(strPath, "rar://") || StringUtils::StartsWithNoCase(strPath, "zip://"))
+ URIUtils::GetParentPath(strPath,strPath1);
+
+ URIUtils::AddSlashAtEnd(strPath1);
+
+ int idParentPath = GetPathId(parentPath.empty() ? URIUtils::GetParentPath(strPath1) : parentPath);
+
+ // add the path
+ if (idParentPath < 0)
+ {
+ if (dateAdded.IsValid())
+ strSQL=PrepareSQL("insert into path (idPath, strPath, dateAdded) values (NULL, '%s', '%s')", strPath1.c_str(), dateAdded.GetAsDBDateTime().c_str());
+ else
+ strSQL=PrepareSQL("insert into path (idPath, strPath) values (NULL, '%s')", strPath1.c_str());
+ }
+ else
+ {
+ if (dateAdded.IsValid())
+ strSQL = PrepareSQL("insert into path (idPath, strPath, dateAdded, idParentPath) values (NULL, '%s', '%s', %i)", strPath1.c_str(), dateAdded.GetAsDBDateTime().c_str(), idParentPath);
+ else
+ strSQL=PrepareSQL("insert into path (idPath, strPath, idParentPath) values (NULL, '%s', %i)", strPath1.c_str(), idParentPath);
+ }
+ m_pDS->exec(strSQL);
+ idPath = (int)m_pDS->lastinsertid();
+ return idPath;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} unable to addpath ({})", __FUNCTION__, strSQL);
+ }
+ return -1;
+}
+
+bool CVideoDatabase::GetPathHash(const std::string &path, std::string &hash)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL=PrepareSQL("select strHash from path where strPath='%s'", path.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ return false;
+ hash = m_pDS->fv("strHash").get_asString();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, path);
+ }
+
+ return false;
+}
+
+bool CVideoDatabase::GetSourcePath(const std::string &path, std::string &sourcePath)
+{
+ SScanSettings dummy;
+ return GetSourcePath(path, sourcePath, dummy);
+}
+
+bool CVideoDatabase::GetSourcePath(const std::string &path, std::string &sourcePath, SScanSettings& settings)
+{
+ try
+ {
+ if (path.empty() || m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ std::string strPath2;
+
+ if (URIUtils::IsMultiPath(path))
+ strPath2 = CMultiPathDirectory::GetFirstPath(path);
+ else
+ strPath2 = path;
+
+ std::string strPath1 = URIUtils::GetDirectory(strPath2);
+ int idPath = GetPathId(strPath1);
+
+ if (idPath > -1)
+ {
+ // check if the given path already is a source itself
+ std::string strSQL = PrepareSQL("SELECT path.useFolderNames, path.scanRecursive, path.noUpdate, path.exclude FROM path WHERE "
+ "path.idPath = %i AND "
+ "path.strContent IS NOT NULL AND path.strContent != '' AND "
+ "path.strScraper IS NOT NULL AND path.strScraper != ''", idPath);
+ if (m_pDS->query(strSQL) && !m_pDS->eof())
+ {
+ settings.parent_name_root = settings.parent_name = m_pDS->fv(0).get_asBool();
+ settings.recurse = m_pDS->fv(1).get_asInt();
+ settings.noupdate = m_pDS->fv(2).get_asBool();
+ settings.exclude = m_pDS->fv(3).get_asBool();
+
+ m_pDS->close();
+ sourcePath = path;
+ return true;
+ }
+ }
+
+ // look for parent paths until there is one which is a source
+ std::string strParent;
+ bool found = false;
+ while (URIUtils::GetParentPath(strPath1, strParent))
+ {
+ std::string strSQL = PrepareSQL("SELECT path.strContent, path.strScraper, path.scanRecursive, path.useFolderNames, path.noUpdate, path.exclude FROM path WHERE strPath = '%s'", strParent.c_str());
+ if (m_pDS->query(strSQL) && !m_pDS->eof())
+ {
+ std::string strContent = m_pDS->fv(0).get_asString();
+ std::string strScraper = m_pDS->fv(1).get_asString();
+ if (!strContent.empty() && !strScraper.empty())
+ {
+ settings.parent_name_root = settings.parent_name = m_pDS->fv(2).get_asBool();
+ settings.recurse = m_pDS->fv(3).get_asInt();
+ settings.noupdate = m_pDS->fv(4).get_asBool();
+ settings.exclude = m_pDS->fv(5).get_asBool();
+ found = true;
+ break;
+ }
+ }
+
+ strPath1 = strParent;
+ }
+ m_pDS->close();
+
+ if (found)
+ {
+ sourcePath = strParent;
+ return true;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+//********************************************************************************************************************************
+int CVideoDatabase::AddFile(const std::string& strFileNameAndPath,
+ const std::string& parentPath /* = "" */,
+ const CDateTime& dateAdded /* = CDateTime() */,
+ int playcount /* = 0 */,
+ const CDateTime& lastPlayed /* = CDateTime() */)
+{
+ std::string strSQL = "";
+ try
+ {
+ int idFile;
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ const auto finalDateAdded = GetDateAdded(strFileNameAndPath, dateAdded);
+
+ std::string strFileName, strPath;
+ SplitPath(strFileNameAndPath,strPath,strFileName);
+
+ int idPath = AddPath(strPath, parentPath, finalDateAdded);
+ if (idPath < 0)
+ return -1;
+
+ std::string strSQL=PrepareSQL("select idFile from files where strFileName='%s' and idPath=%i", strFileName.c_str(),idPath);
+
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ {
+ idFile = m_pDS->fv("idFile").get_asInt() ;
+ m_pDS->close();
+ return idFile;
+ }
+ m_pDS->close();
+
+ std::string strPlaycount = "NULL";
+ if (playcount > 0)
+ strPlaycount = std::to_string(playcount);
+ std::string strLastPlayed = "NULL";
+ if (lastPlayed.IsValid())
+ strLastPlayed = "'" + lastPlayed.GetAsDBDateTime() + "'";
+
+ strSQL = PrepareSQL("INSERT INTO files (idFile, idPath, strFileName, playCount, lastPlayed, dateAdded) "
+ "VALUES(NULL, %i, '%s', " + strPlaycount + ", " + strLastPlayed + ", '%s')",
+ idPath, strFileName.c_str(), finalDateAdded.GetAsDBDateTime().c_str());
+ m_pDS->exec(strSQL);
+ idFile = (int)m_pDS->lastinsertid();
+ return idFile;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} unable to addfile ({})", __FUNCTION__, strSQL);
+ }
+ return -1;
+}
+
+int CVideoDatabase::AddFile(const CFileItem& item)
+{
+ if (item.IsVideoDb() && item.HasVideoInfoTag())
+ {
+ const auto videoInfoTag = item.GetVideoInfoTag();
+ if (videoInfoTag->m_iFileId != -1)
+ return videoInfoTag->m_iFileId;
+ else
+ return AddFile(*videoInfoTag);
+ }
+ return AddFile(item.GetPath());
+}
+
+int CVideoDatabase::AddFile(const CVideoInfoTag& details, const std::string& parentPath /* = "" */)
+{
+ return AddFile(details.GetPath(), parentPath, details.m_dateAdded, details.GetPlayCount(),
+ details.m_lastPlayed);
+}
+
+void CVideoDatabase::UpdateFileDateAdded(CVideoInfoTag& details)
+{
+ if (details.GetPath().empty() || GetAndFillFileId(details) <= 0)
+ return;
+
+ CDateTime finalDateAdded;
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ finalDateAdded = GetDateAdded(details.GetPath(), details.m_dateAdded);
+
+ m_pDS->exec(PrepareSQL("UPDATE files SET dateAdded='%s' WHERE idFile=%d",
+ finalDateAdded.GetAsDBDateTime().c_str(), details.m_iFileId));
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}, {}) failed", __FUNCTION__, CURL::GetRedacted(details.GetPath()),
+ finalDateAdded.GetAsDBDateTime());
+ }
+}
+
+bool CVideoDatabase::SetPathHash(const std::string &path, const std::string &hash)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ int idPath = AddPath(path);
+ if (idPath < 0) return false;
+
+ std::string strSQL=PrepareSQL("update path set strHash='%s' where idPath=%ld", hash.c_str(), idPath);
+ m_pDS->exec(strSQL);
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}, {}) failed", __FUNCTION__, path, hash);
+ }
+
+ return false;
+}
+
+bool CVideoDatabase::LinkMovieToTvshow(int idMovie, int idShow, bool bRemove)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (bRemove) // delete link
+ {
+ std::string strSQL=PrepareSQL("delete from movielinktvshow where idMovie=%i and idShow=%i", idMovie, idShow);
+ m_pDS->exec(strSQL);
+ return true;
+ }
+
+ std::string strSQL=PrepareSQL("insert into movielinktvshow (idShow,idMovie) values (%i,%i)", idShow,idMovie);
+ m_pDS->exec(strSQL);
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}, {}) failed", __FUNCTION__, idMovie, idShow);
+ }
+
+ return false;
+}
+
+bool CVideoDatabase::IsLinkedToTvshow(int idMovie)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL=PrepareSQL("select * from movielinktvshow where idMovie=%i", idMovie);
+ m_pDS->query(strSQL);
+ if (m_pDS->eof())
+ {
+ m_pDS->close();
+ return false;
+ }
+
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idMovie);
+ }
+
+ return false;
+}
+
+bool CVideoDatabase::GetLinksToTvShow(int idMovie, std::vector<int>& ids)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL=PrepareSQL("select * from movielinktvshow where idMovie=%i", idMovie);
+ m_pDS2->query(strSQL);
+ while (!m_pDS2->eof())
+ {
+ ids.push_back(m_pDS2->fv(1).get_asInt());
+ m_pDS2->next();
+ }
+
+ m_pDS2->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idMovie);
+ }
+
+ return false;
+}
+
+
+//********************************************************************************************************************************
+int CVideoDatabase::GetFileId(const std::string& strFilenameAndPath)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+ std::string strPath, strFileName;
+ SplitPath(strFilenameAndPath,strPath,strFileName);
+
+ int idPath = GetPathId(strPath);
+ if (idPath >= 0)
+ {
+ std::string strSQL;
+ strSQL=PrepareSQL("select idFile from files where strFileName='%s' and idPath=%i", strFileName.c_str(),idPath);
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ {
+ int idFile = m_pDS->fv("files.idFile").get_asInt();
+ m_pDS->close();
+ return idFile;
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return -1;
+}
+
+int CVideoDatabase::GetFileId(const CFileItem &item)
+{
+ int fileId = -1;
+ if (item.HasVideoInfoTag())
+ fileId = GetFileId(*item.GetVideoInfoTag());
+
+ if (fileId == -1)
+ fileId = GetFileId(item.GetPath());
+
+ return fileId;
+}
+
+int CVideoDatabase::GetFileId(const CVideoInfoTag& details)
+{
+ if (details.m_iFileId > 0)
+ return details.m_iFileId;
+
+ const auto& filePath = details.GetPath();
+ if (filePath.empty())
+ return -1;
+
+ return GetFileId(filePath);
+}
+
+int CVideoDatabase::GetAndFillFileId(CVideoInfoTag& details)
+{
+ details.m_iFileId = GetFileId(details);
+ return details.m_iFileId;
+}
+
+//********************************************************************************************************************************
+int CVideoDatabase::GetMovieId(const std::string& strFilenameAndPath)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+ int idMovie = -1;
+
+ // needed for query parameters
+ int idFile = GetFileId(strFilenameAndPath);
+ int idPath=-1;
+ std::string strPath;
+ if (idFile < 0)
+ {
+ std::string strFile;
+ SplitPath(strFilenameAndPath,strPath,strFile);
+
+ // have to join movieinfo table for correct results
+ idPath = GetPathId(strPath);
+ if (idPath < 0 && strPath != strFilenameAndPath)
+ return -1;
+ }
+
+ if (idFile == -1 && strPath != strFilenameAndPath)
+ return -1;
+
+ std::string strSQL;
+ if (idFile == -1)
+ strSQL=PrepareSQL("select idMovie from movie join files on files.idFile=movie.idFile where files.idPath=%i",idPath);
+ else
+ strSQL=PrepareSQL("select idMovie from movie where idFile=%i", idFile);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{} ({}), query = {}", __FUNCTION__,
+ CURL::GetRedacted(strFilenameAndPath), strSQL);
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() > 0)
+ idMovie = m_pDS->fv("idMovie").get_asInt();
+ m_pDS->close();
+
+ return idMovie;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return -1;
+}
+
+int CVideoDatabase::GetTvShowId(const std::string& strPath)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+ int idTvShow = -1;
+
+ // have to join movieinfo table for correct results
+ int idPath = GetPathId(strPath);
+ if (idPath < 0)
+ return -1;
+
+ std::string strSQL;
+ std::string strPath1=strPath;
+ std::string strParent;
+ int iFound=0;
+
+ strSQL=PrepareSQL("select idShow from tvshowlinkpath where tvshowlinkpath.idPath=%i",idPath);
+ m_pDS->query(strSQL);
+ if (!m_pDS->eof())
+ iFound = 1;
+
+ while (iFound == 0 && URIUtils::GetParentPath(strPath1, strParent))
+ {
+ strSQL=PrepareSQL("SELECT idShow FROM path INNER JOIN tvshowlinkpath ON tvshowlinkpath.idPath=path.idPath WHERE strPath='%s'",strParent.c_str());
+ m_pDS->query(strSQL);
+ if (!m_pDS->eof())
+ {
+ int idShow = m_pDS->fv("idShow").get_asInt();
+ if (idShow != -1)
+ iFound = 2;
+ }
+ strPath1 = strParent;
+ }
+
+ if (m_pDS->num_rows() > 0)
+ idTvShow = m_pDS->fv("idShow").get_asInt();
+ m_pDS->close();
+
+ return idTvShow;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strPath);
+ }
+ return -1;
+}
+
+int CVideoDatabase::GetEpisodeId(const std::string& strFilenameAndPath, int idEpisode, int idSeason) // input value is episode/season number hint - for multiparters
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ // need this due to the nested GetEpisodeInfo query
+ std::unique_ptr<Dataset> pDS;
+ pDS.reset(m_pDB->CreateDataset());
+ if (nullptr == pDS)
+ return -1;
+
+ int idFile = GetFileId(strFilenameAndPath);
+ if (idFile < 0)
+ return -1;
+
+ std::string strSQL=PrepareSQL("select idEpisode from episode where idFile=%i", idFile);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{} ({}), query = {}", __FUNCTION__,
+ CURL::GetRedacted(strFilenameAndPath), strSQL);
+ pDS->query(strSQL);
+ if (pDS->num_rows() > 0)
+ {
+ if (idEpisode == -1)
+ idEpisode = pDS->fv("episode.idEpisode").get_asInt();
+ else // use the hint!
+ {
+ while (!pDS->eof())
+ {
+ CVideoInfoTag tag;
+ int idTmpEpisode = pDS->fv("episode.idEpisode").get_asInt();
+ GetEpisodeBasicInfo(strFilenameAndPath, tag, idTmpEpisode);
+ if (tag.m_iEpisode == idEpisode && (idSeason == -1 || tag.m_iSeason == idSeason)) {
+ // match on the episode hint, and there's no season hint or a season hint match
+ idEpisode = idTmpEpisode;
+ break;
+ }
+ pDS->next();
+ }
+ if (pDS->eof())
+ idEpisode = -1;
+ }
+ }
+ else
+ idEpisode = -1;
+
+ pDS->close();
+
+ return idEpisode;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return -1;
+}
+
+int CVideoDatabase::GetMusicVideoId(const std::string& strFilenameAndPath)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ int idFile = GetFileId(strFilenameAndPath);
+ if (idFile < 0)
+ return -1;
+
+ std::string strSQL=PrepareSQL("select idMVideo from musicvideo where idFile=%i", idFile);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{} ({}), query = {}", __FUNCTION__,
+ CURL::GetRedacted(strFilenameAndPath), strSQL);
+ m_pDS->query(strSQL);
+ int idMVideo=-1;
+ if (m_pDS->num_rows() > 0)
+ idMVideo = m_pDS->fv("idMVideo").get_asInt();
+ m_pDS->close();
+
+ return idMVideo;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return -1;
+}
+
+//********************************************************************************************************************************
+int CVideoDatabase::AddNewMovie(CVideoInfoTag& details)
+{
+ const auto filePath = details.GetPath();
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ if (details.m_iFileId <= 0)
+ {
+ details.m_iFileId = AddFile(details);
+ if (details.m_iFileId <= 0)
+ return -1;
+ }
+
+ std::string strSQL =
+ PrepareSQL("INSERT INTO movie (idMovie, idFile) VALUES (NULL, %i)", details.m_iFileId);
+ m_pDS->exec(strSQL);
+ details.m_iDbId = static_cast<int>(m_pDS->lastinsertid());
+
+ return details.m_iDbId;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
+ }
+ return -1;
+}
+
+bool CVideoDatabase::AddPathToTvShow(int idShow, const std::string &path, const std::string &parentPath, const CDateTime& dateAdded /* = CDateTime() */)
+{
+ // Check if this path is already added
+ int idPath = GetPathId(path);
+ if (idPath < 0)
+ idPath = AddPath(path, parentPath, GetDateAdded(path, dateAdded));
+
+ return ExecuteQuery(PrepareSQL("REPLACE INTO tvshowlinkpath(idShow, idPath) VALUES (%i,%i)", idShow, idPath));
+}
+
+int CVideoDatabase::AddTvShow()
+{
+ if (ExecuteQuery("INSERT INTO tvshow(idShow) VALUES(NULL)"))
+ return (int)m_pDS->lastinsertid();
+ return -1;
+}
+
+//********************************************************************************************************************************
+int CVideoDatabase::AddNewEpisode(int idShow, CVideoInfoTag& details)
+{
+ const auto filePath = details.GetPath();
+
+ try
+ {
+ if (nullptr == m_pDB || nullptr == m_pDS)
+ return -1;
+
+ if (details.m_iFileId <= 0)
+ {
+ details.m_iFileId = AddFile(details);
+ if (details.m_iFileId <= 0)
+ return -1;
+ }
+
+ std::string strSQL =
+ PrepareSQL("INSERT INTO episode (idEpisode, idFile, idShow) VALUES (NULL, %i, %i)",
+ details.m_iFileId, idShow);
+ m_pDS->exec(strSQL);
+ details.m_iDbId = static_cast<int>(m_pDS->lastinsertid());
+
+ return details.m_iDbId;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
+ }
+ return -1;
+}
+
+int CVideoDatabase::AddNewMusicVideo(CVideoInfoTag& details)
+{
+ const auto filePath = details.GetPath();
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ if (details.m_iFileId <= 0)
+ {
+ details.m_iFileId = AddFile(details);
+ if (details.m_iFileId <= 0)
+ return -1;
+ }
+
+ std::string strSQL = PrepareSQL("INSERT INTO musicvideo (idMVideo, idFile) VALUES (NULL, %i)",
+ details.m_iFileId);
+ m_pDS->exec(strSQL);
+ details.m_iDbId = static_cast<int>(m_pDS->lastinsertid());
+
+ return details.m_iDbId;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
+ }
+ return -1;
+}
+
+//********************************************************************************************************************************
+int CVideoDatabase::AddToTable(const std::string& table, const std::string& firstField, const std::string& secondField, const std::string& value)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string strSQL = PrepareSQL("select %s from %s where %s like '%s'", firstField.c_str(), table.c_str(), secondField.c_str(), value.substr(0, 255).c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ // doesn't exists, add it
+ strSQL = PrepareSQL("insert into %s (%s, %s) values(NULL, '%s')", table.c_str(), firstField.c_str(), secondField.c_str(), value.substr(0, 255).c_str());
+ m_pDS->exec(strSQL);
+ int id = (int)m_pDS->lastinsertid();
+ return id;
+ }
+ else
+ {
+ int id = m_pDS->fv(firstField.c_str()).get_asInt();
+ m_pDS->close();
+ return id;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, value);
+ }
+
+ return -1;
+}
+
+int CVideoDatabase::UpdateRatings(int mediaId, const char *mediaType, const RatingMap& values, const std::string& defaultRating)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string sql = PrepareSQL("DELETE FROM rating WHERE media_id=%i AND media_type='%s'", mediaId, mediaType);
+ m_pDS->exec(sql);
+
+ return AddRatings(mediaId, mediaType, values, defaultRating);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} unable to update ratings of ({})", __FUNCTION__, mediaType);
+ }
+ return -1;
+}
+
+int CVideoDatabase::AddRatings(int mediaId, const char *mediaType, const RatingMap& values, const std::string& defaultRating)
+{
+ int ratingid = -1;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ for (const auto& i : values)
+ {
+ int id;
+ std::string strSQL = PrepareSQL("SELECT rating_id FROM rating WHERE media_id=%i AND media_type='%s' AND rating_type = '%s'", mediaId, mediaType, i.first.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ // doesn't exists, add it
+ strSQL = PrepareSQL("INSERT INTO rating (media_id, media_type, rating_type, rating, votes) "
+ "VALUES (%i, '%s', '%s', %f, %i)",
+ mediaId, mediaType, i.first.c_str(),
+ static_cast<double>(i.second.rating), i.second.votes);
+ m_pDS->exec(strSQL);
+ id = (int)m_pDS->lastinsertid();
+ }
+ else
+ {
+ id = m_pDS->fv(0).get_asInt();
+ m_pDS->close();
+ strSQL = PrepareSQL("UPDATE rating SET rating = %f, votes = %i WHERE rating_id = %i",
+ static_cast<double>(i.second.rating), i.second.votes, id);
+ m_pDS->exec(strSQL);
+ }
+ if (i.first == defaultRating)
+ ratingid = id;
+ }
+ return ratingid;
+
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({} - {}) failed", __FUNCTION__, mediaId, mediaType);
+ }
+
+ return ratingid;
+}
+
+int CVideoDatabase::UpdateUniqueIDs(int mediaId, const char *mediaType, const CVideoInfoTag& details)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string sql = PrepareSQL("DELETE FROM uniqueid WHERE media_id=%i AND media_type='%s'", mediaId, mediaType);
+ m_pDS->exec(sql);
+
+ return AddUniqueIDs(mediaId, mediaType, details);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} unable to update unique ids of ({})", __FUNCTION__, mediaType);
+ }
+ return -1;
+}
+
+int CVideoDatabase::AddUniqueIDs(int mediaId, const char *mediaType, const CVideoInfoTag& details)
+{
+ int uniqueid = -1;
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ for (const auto& i : details.GetUniqueIDs())
+ {
+ int id;
+ std::string strSQL = PrepareSQL("SELECT uniqueid_id FROM uniqueid WHERE media_id=%i AND media_type='%s' AND type = '%s'", mediaId, mediaType, i.first.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ // doesn't exists, add it
+ strSQL = PrepareSQL("INSERT INTO uniqueid (media_id, media_type, value, type) VALUES (%i, '%s', '%s', '%s')", mediaId, mediaType, i.second.c_str(), i.first.c_str());
+ m_pDS->exec(strSQL);
+ id = (int)m_pDS->lastinsertid();
+ }
+ else
+ {
+ id = m_pDS->fv(0).get_asInt();
+ m_pDS->close();
+ strSQL = PrepareSQL("UPDATE uniqueid SET value = '%s', type = '%s' WHERE uniqueid_id = %i", i.second.c_str(), i.first.c_str(), id);
+ m_pDS->exec(strSQL);
+ }
+ if (i.first == details.GetDefaultUniqueID())
+ uniqueid = id;
+ }
+ return uniqueid;
+
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({} - {}) failed", __FUNCTION__, mediaId, mediaType);
+ }
+
+ return uniqueid;
+}
+
+int CVideoDatabase::AddSet(const std::string& strSet, const std::string& strOverview /* = "" */)
+{
+ if (strSet.empty())
+ return -1;
+
+ try
+ {
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return -1;
+
+ std::string strSQL = PrepareSQL("SELECT idSet FROM sets WHERE strSet LIKE '%s'", strSet.c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ strSQL = PrepareSQL("INSERT INTO sets (idSet, strSet, strOverview) VALUES(NULL, '%s', '%s')", strSet.c_str(), strOverview.c_str());
+ m_pDS->exec(strSQL);
+ int id = static_cast<int>(m_pDS->lastinsertid());
+ return id;
+ }
+ else
+ {
+ int id = m_pDS->fv("idSet").get_asInt();
+ m_pDS->close();
+ return id;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSet);
+ }
+
+ return -1;
+}
+
+int CVideoDatabase::AddTag(const std::string& name)
+{
+ if (name.empty())
+ return -1;
+
+ return AddToTable("tag", "tag_id", "name", name);
+}
+
+int CVideoDatabase::AddActor(const std::string& name, const std::string& thumbURLs, const std::string &thumb)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+ int idActor = -1;
+
+ // ATTENTION: the trimming of actor names should really not be done here but after the scraping / NFO-parsing
+ std::string trimmedName = name.c_str();
+ StringUtils::Trim(trimmedName);
+
+ std::string strSQL=PrepareSQL("select actor_id from actor where name like '%s'", trimmedName.substr(0, 255).c_str());
+ m_pDS->query(strSQL);
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ // doesn't exists, add it
+ strSQL=PrepareSQL("insert into actor (actor_id, name, art_urls) values(NULL, '%s', '%s')", trimmedName.substr(0,255).c_str(), thumbURLs.c_str());
+ m_pDS->exec(strSQL);
+ idActor = (int)m_pDS->lastinsertid();
+ }
+ else
+ {
+ idActor = m_pDS->fv(0).get_asInt();
+ m_pDS->close();
+ // update the thumb url's
+ if (!thumbURLs.empty())
+ {
+ strSQL=PrepareSQL("update actor set art_urls = '%s' where actor_id = %i", thumbURLs.c_str(), idActor);
+ m_pDS->exec(strSQL);
+ }
+ }
+ // add artwork
+ if (!thumb.empty())
+ SetArtForItem(idActor, "actor", "thumb", thumb);
+ return idActor;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, name);
+ }
+ return -1;
+}
+
+
+
+void CVideoDatabase::AddLinkToActor(int mediaId, const char *mediaType, int actorId, const std::string &role, int order)
+{
+ std::string sql = PrepareSQL("SELECT 1 FROM actor_link WHERE actor_id=%i AND "
+ "media_id=%i AND media_type='%s' AND role='%s'",
+ actorId, mediaId, mediaType, role.c_str());
+
+ if (GetSingleValue(sql).empty())
+ { // doesn't exists, add it
+ sql = PrepareSQL("INSERT INTO actor_link (actor_id, media_id, media_type, role, cast_order) VALUES(%i,%i,'%s','%s',%i)", actorId, mediaId, mediaType, role.c_str(), order);
+ ExecuteQuery(sql);
+ }
+}
+
+void CVideoDatabase::AddToLinkTable(int mediaId, const std::string& mediaType, const std::string& table, int valueId, const char *foreignKey)
+{
+ const char *key = foreignKey ? foreignKey : table.c_str();
+ std::string sql = PrepareSQL("SELECT 1 FROM %s_link WHERE %s_id=%i AND media_id=%i AND media_type='%s'", table.c_str(), key, valueId, mediaId, mediaType.c_str());
+
+ if (GetSingleValue(sql).empty())
+ { // doesn't exists, add it
+ sql = PrepareSQL("INSERT INTO %s_link (%s_id,media_id,media_type) VALUES(%i,%i,'%s')", table.c_str(), key, valueId, mediaId, mediaType.c_str());
+ ExecuteQuery(sql);
+ }
+}
+
+void CVideoDatabase::RemoveFromLinkTable(int mediaId, const std::string& mediaType, const std::string& table, int valueId, const char *foreignKey)
+{
+ const char *key = foreignKey ? foreignKey : table.c_str();
+ std::string sql = PrepareSQL("DELETE FROM %s_link WHERE %s_id=%i AND media_id=%i AND media_type='%s'", table.c_str(), key, valueId, mediaId, mediaType.c_str());
+
+ ExecuteQuery(sql);
+}
+
+void CVideoDatabase::AddLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values)
+{
+ for (const auto &i : values)
+ {
+ if (!i.empty())
+ {
+ int idValue = AddToTable(field, field + "_id", "name", i);
+ if (idValue > -1)
+ AddToLinkTable(mediaId, mediaType, field, idValue);
+ }
+ }
+}
+
+void CVideoDatabase::UpdateLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values)
+{
+ std::string sql = PrepareSQL("DELETE FROM %s_link WHERE media_id=%i AND media_type='%s'", field.c_str(), mediaId, mediaType.c_str());
+ m_pDS->exec(sql);
+
+ AddLinksToItem(mediaId, mediaType, field, values);
+}
+
+void CVideoDatabase::AddActorLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values)
+{
+ for (const auto &i : values)
+ {
+ if (!i.empty())
+ {
+ int idValue = AddActor(i, "");
+ if (idValue > -1)
+ AddToLinkTable(mediaId, mediaType, field, idValue, "actor");
+ }
+ }
+}
+
+void CVideoDatabase::UpdateActorLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values)
+{
+ std::string sql = PrepareSQL("DELETE FROM %s_link WHERE media_id=%i AND media_type='%s'", field.c_str(), mediaId, mediaType.c_str());
+ m_pDS->exec(sql);
+
+ AddActorLinksToItem(mediaId, mediaType, field, values);
+}
+
+//****Tags****
+void CVideoDatabase::AddTagToItem(int media_id, int tag_id, const std::string &type)
+{
+ if (type.empty())
+ return;
+
+ AddToLinkTable(media_id, type, "tag", tag_id);
+}
+
+void CVideoDatabase::RemoveTagFromItem(int media_id, int tag_id, const std::string &type)
+{
+ if (type.empty())
+ return;
+
+ RemoveFromLinkTable(media_id, type, "tag", tag_id);
+}
+
+void CVideoDatabase::RemoveTagsFromItem(int media_id, const std::string &type)
+{
+ if (type.empty())
+ return;
+
+ m_pDS2->exec(PrepareSQL("DELETE FROM tag_link WHERE media_id=%d AND media_type='%s'", media_id, type.c_str()));
+}
+
+//****Actors****
+void CVideoDatabase::AddCast(int mediaId, const char *mediaType, const std::vector< SActorInfo > &cast)
+{
+ if (cast.empty())
+ return;
+
+ int order = std::max_element(cast.begin(), cast.end())->order;
+ for (const auto &i : cast)
+ {
+ int idActor = AddActor(i.strName, i.thumbUrl.GetData(), i.thumb);
+ AddLinkToActor(mediaId, mediaType, idActor, i.strRole, i.order >= 0 ? i.order : ++order);
+ }
+}
+
+//********************************************************************************************************************************
+bool CVideoDatabase::LoadVideoInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int getDetails /* = VideoDbDetailsAll */)
+{
+ if (GetMovieInfo(strFilenameAndPath, details))
+ return true;
+ if (GetEpisodeInfo(strFilenameAndPath, details))
+ return true;
+ if (GetMusicVideoInfo(strFilenameAndPath, details))
+ return true;
+ if (GetFileInfo(strFilenameAndPath, details))
+ return true;
+
+ return false;
+}
+
+bool CVideoDatabase::HasMovieInfo(const std::string& strFilenameAndPath)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ int idMovie = GetMovieId(strFilenameAndPath);
+ return (idMovie > 0); // index of zero is also invalid
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return false;
+}
+
+bool CVideoDatabase::HasTvShowInfo(const std::string& strPath)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ int idTvShow = GetTvShowId(strPath);
+ return (idTvShow > 0); // index of zero is also invalid
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strPath);
+ }
+ return false;
+}
+
+bool CVideoDatabase::HasEpisodeInfo(const std::string& strFilenameAndPath)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ int idEpisode = GetEpisodeId(strFilenameAndPath);
+ return (idEpisode > 0); // index of zero is also invalid
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return false;
+}
+
+bool CVideoDatabase::HasMusicVideoInfo(const std::string& strFilenameAndPath)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ int idMVideo = GetMusicVideoId(strFilenameAndPath);
+ return (idMVideo > 0); // index of zero is also invalid
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return false;
+}
+
+void CVideoDatabase::DeleteDetailsForTvShow(int idTvShow)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ std::string strSQL;
+ strSQL=PrepareSQL("DELETE from genre_link WHERE media_id=%i AND media_type='tvshow'", idTvShow);
+ m_pDS->exec(strSQL);
+
+ strSQL=PrepareSQL("DELETE FROM actor_link WHERE media_id=%i AND media_type='tvshow'", idTvShow);
+ m_pDS->exec(strSQL);
+
+ strSQL=PrepareSQL("DELETE FROM director_link WHERE media_id=%i AND media_type='tvshow'", idTvShow);
+ m_pDS->exec(strSQL);
+
+ strSQL=PrepareSQL("DELETE FROM studio_link WHERE media_id=%i AND media_type='tvshow'", idTvShow);
+ m_pDS->exec(strSQL);
+
+ strSQL = PrepareSQL("DELETE FROM rating WHERE media_id=%i AND media_type='tvshow'", idTvShow);
+ m_pDS->exec(strSQL);
+
+ strSQL = PrepareSQL("DELETE FROM uniqueid WHERE media_id=%i AND media_type='tvshow'", idTvShow);
+ m_pDS->exec(strSQL);
+
+ // remove all info other than the id
+ // we do this due to the way we have the link between the file + movie tables.
+
+ std::vector<std::string> ids;
+ for (int iType = VIDEODB_ID_TV_MIN + 1; iType < VIDEODB_ID_TV_MAX; iType++)
+ ids.emplace_back(StringUtils::Format("c{:02}=NULL", iType));
+
+ strSQL = "update tvshow set ";
+ strSQL += StringUtils::Join(ids, ", ");
+ strSQL += PrepareSQL(" where idShow=%i", idTvShow);
+ m_pDS->exec(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idTvShow);
+ }
+}
+
+//********************************************************************************************************************************
+void CVideoDatabase::GetMoviesByActor(const std::string& name, CFileItemList& items)
+{
+ Filter filter;
+ filter.join = "LEFT JOIN actor_link ON actor_link.media_id=movie_view.idMovie AND actor_link.media_type='movie' "
+ "LEFT JOIN actor a ON a.actor_id=actor_link.actor_id "
+ "LEFT JOIN director_link ON director_link.media_id=movie_view.idMovie AND director_link.media_type='movie' "
+ "LEFT JOIN actor d ON d.actor_id=director_link.actor_id";
+ filter.where = PrepareSQL("a.name='%s' OR d.name='%s'", name.c_str(), name.c_str());
+ filter.group = "movie_view.idMovie";
+ GetMoviesByWhere("videodb://movies/titles/", filter, items);
+}
+
+void CVideoDatabase::GetTvShowsByActor(const std::string& name, CFileItemList& items)
+{
+ Filter filter;
+ filter.join = "LEFT JOIN actor_link ON actor_link.media_id=tvshow_view.idShow AND actor_link.media_type='tvshow' "
+ "LEFT JOIN actor a ON a.actor_id=actor_link.actor_id "
+ "LEFT JOIN director_link ON director_link.media_id=tvshow_view.idShow AND director_link.media_type='tvshow' "
+ "LEFT JOIN actor d ON d.actor_id=director_link.actor_id";
+ filter.where = PrepareSQL("a.name='%s' OR d.name='%s'", name.c_str(), name.c_str());
+ GetTvShowsByWhere("videodb://tvshows/titles/", filter, items);
+}
+
+void CVideoDatabase::GetEpisodesByActor(const std::string& name, CFileItemList& items)
+{
+ Filter filter;
+ filter.join = "LEFT JOIN actor_link ON actor_link.media_id=episode_view.idEpisode AND actor_link.media_type='episode' "
+ "LEFT JOIN actor a ON a.actor_id=actor_link.actor_id "
+ "LEFT JOIN director_link ON director_link.media_id=episode_view.idEpisode AND director_link.media_type='episode' "
+ "LEFT JOIN actor d ON d.actor_id=director_link.actor_id";
+ filter.where = PrepareSQL("a.name='%s' OR d.name='%s'", name.c_str(), name.c_str());
+ filter.group = "episode_view.idEpisode";
+ GetEpisodesByWhere("videodb://tvshows/titles/", filter, items);
+}
+
+void CVideoDatabase::GetMusicVideosByArtist(const std::string& strArtist, CFileItemList& items)
+{
+ try
+ {
+ items.Clear();
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ std::string strSQL;
+ if (strArtist.empty()) //! @todo SMARTPLAYLISTS what is this here for???
+ strSQL=PrepareSQL("select distinct * from musicvideo_view join actor_link on actor_link.media_id=musicvideo_view.idMVideo AND actor_link.media_type='musicvideo' join actor on actor.actor_id=actor_link.actor_id");
+ else // same artist OR same director
+ strSQL = PrepareSQL(
+ "select * from musicvideo_view join actor_link on "
+ "actor_link.media_id=musicvideo_view.idMVideo AND actor_link.media_type='musicvideo' "
+ "join actor on actor.actor_id=actor_link.actor_id where actor.name='%s' OR "
+ "musicvideo_view.c05='%s' GROUP BY idMVideo",
+ strArtist.c_str(), strArtist.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ CVideoInfoTag tag = GetDetailsForMusicVideo(m_pDS);
+ CFileItemPtr pItem(new CFileItem(tag));
+ pItem->SetLabel(StringUtils::Join(tag.m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator));
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strArtist);
+ }
+}
+
+//********************************************************************************************************************************
+bool CVideoDatabase::GetMovieInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idMovie /* = -1 */, int getDetails /* = VideoDbDetailsAll */)
+{
+ try
+ {
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ if (idMovie < 0)
+ idMovie = GetMovieId(strFilenameAndPath);
+ if (idMovie < 0) return false;
+
+ std::string sql = PrepareSQL("select * from movie_view where idMovie=%i", idMovie);
+ if (!m_pDS->query(sql))
+ return false;
+ details = GetDetailsForMovie(m_pDS, getDetails);
+ return !details.IsEmpty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return false;
+}
+
+//********************************************************************************************************************************
+bool CVideoDatabase::GetTvShowInfo(const std::string& strPath, CVideoInfoTag& details, int idTvShow /* = -1 */, CFileItem *item /* = NULL */, int getDetails /* = VideoDbDetailsAll */)
+{
+ try
+ {
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ if (idTvShow < 0)
+ idTvShow = GetTvShowId(strPath);
+ if (idTvShow < 0) return false;
+
+ std::string sql = PrepareSQL("SELECT * FROM tvshow_view WHERE idShow=%i GROUP BY idShow", idTvShow);
+ if (!m_pDS->query(sql))
+ return false;
+ details = GetDetailsForTvShow(m_pDS, getDetails, item);
+ return !details.IsEmpty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strPath);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetSeasonInfo(int idSeason, CVideoInfoTag& details, bool allDetails /* = true */)
+{
+ return GetSeasonInfo(idSeason, details, allDetails, nullptr);
+}
+
+bool CVideoDatabase::GetSeasonInfo(int idSeason, CVideoInfoTag& details, CFileItem* item)
+{
+ return GetSeasonInfo(idSeason, details, true, item);
+}
+
+bool CVideoDatabase::GetSeasonInfo(int idSeason,
+ CVideoInfoTag& details,
+ bool allDetails,
+ CFileItem* item)
+{
+ if (idSeason < 0)
+ return false;
+
+ try
+ {
+ if (!m_pDB || !m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL("SELECT idSeason, idShow, season, name, userrating FROM seasons WHERE idSeason=%i", idSeason);
+ if (!m_pDS->query(sql))
+ return false;
+
+ if (m_pDS->num_rows() != 1)
+ return false;
+
+ if (allDetails)
+ {
+ int idShow = m_pDS->fv(1).get_asInt();
+
+ // close the current result because we are going to query the season view for all details
+ m_pDS->close();
+
+ if (idShow < 0)
+ return false;
+
+ CFileItemList seasons;
+ if (!GetSeasonsNav(StringUtils::Format("videodb://tvshows/titles/{}/", idShow), seasons, -1,
+ -1, -1, -1, idShow, false) ||
+ seasons.Size() <= 0)
+ return false;
+
+ for (int index = 0; index < seasons.Size(); index++)
+ {
+ const CFileItemPtr season = seasons.Get(index);
+ if (season->HasVideoInfoTag() && season->GetVideoInfoTag()->m_iDbId == idSeason && season->GetVideoInfoTag()->m_iIdShow == idShow)
+ {
+ details = *season->GetVideoInfoTag();
+ if (item)
+ *item = *season;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ const int season = m_pDS->fv(2).get_asInt();
+ std::string name = m_pDS->fv(3).get_asString();
+
+ if (name.empty())
+ {
+ if (season == 0)
+ name = g_localizeStrings.Get(20381);
+ else
+ name = StringUtils::Format(g_localizeStrings.Get(20358), season);
+ }
+
+ details.m_strTitle = name;
+ if (!name.empty())
+ details.m_strSortTitle = name;
+ details.m_iSeason = season;
+ details.m_iDbId = m_pDS->fv(0).get_asInt();
+ details.m_iIdSeason = details.m_iDbId;
+ details.m_type = MediaTypeSeason;
+ details.m_iUserRating = m_pDS->fv(4).get_asInt();
+ details.m_iIdShow = m_pDS->fv(1).get_asInt();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSeason);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetEpisodeBasicInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idEpisode /* = -1 */)
+{
+ try
+ {
+ if (idEpisode < 0)
+ idEpisode = GetEpisodeId(strFilenameAndPath);
+
+ if (idEpisode < 0)
+ return false;
+
+ std::string sql = PrepareSQL("select * from episode where idEpisode=%i",idEpisode);
+ if (!m_pDS->query(sql))
+ return false;
+ details = GetBasicDetailsForEpisode(m_pDS);
+ return !details.IsEmpty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetEpisodeInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idEpisode /* = -1 */, int getDetails /* = VideoDbDetailsAll */)
+{
+ try
+ {
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ if (idEpisode < 0)
+ idEpisode = GetEpisodeId(strFilenameAndPath, details.m_iEpisode, details.m_iSeason);
+ if (idEpisode < 0) return false;
+
+ std::string sql = PrepareSQL("select * from episode_view where idEpisode=%i",idEpisode);
+ if (!m_pDS->query(sql))
+ return false;
+ details = GetDetailsForEpisode(m_pDS, getDetails);
+ return !details.IsEmpty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetMusicVideoInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idMVideo /* = -1 */, int getDetails /* = VideoDbDetailsAll */)
+{
+ try
+ {
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return false;
+
+ if (idMVideo < 0)
+ idMVideo = GetMusicVideoId(strFilenameAndPath);
+ if (idMVideo < 0) return false;
+
+ std::string sql = PrepareSQL("select * from musicvideo_view where idMVideo=%i", idMVideo);
+ if (!m_pDS->query(sql))
+ return false;
+ details = GetDetailsForMusicVideo(m_pDS, getDetails);
+ return !details.IsEmpty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetSetInfo(int idSet, CVideoInfoTag& details, CFileItem* item /* = nullptr */)
+{
+ try
+ {
+ if (idSet < 0)
+ return false;
+
+ Filter filter;
+ filter.where = PrepareSQL("sets.idSet=%d", idSet);
+ CFileItemList items;
+ if (!GetSetsByWhere("videodb://movies/sets/", filter, items) ||
+ items.Size() != 1 ||
+ !items[0]->HasVideoInfoTag())
+ return false;
+
+ details = *(items[0]->GetVideoInfoTag());
+ if (item)
+ *item = *items[0];
+ return !details.IsEmpty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSet);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetFileInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idFile /* = -1 */)
+{
+ try
+ {
+ if (idFile < 0)
+ idFile = GetFileId(strFilenameAndPath);
+ if (idFile < 0)
+ return false;
+
+ std::string sql = PrepareSQL("SELECT * FROM files "
+ "JOIN path ON path.idPath = files.idPath "
+ "LEFT JOIN bookmark ON bookmark.idFile = files.idFile AND bookmark.type = %i "
+ "WHERE files.idFile = %i", CBookmark::RESUME, idFile);
+ if (!m_pDS->query(sql))
+ return false;
+
+ details.m_iFileId = m_pDS->fv("files.idFile").get_asInt();
+ details.m_strPath = m_pDS->fv("path.strPath").get_asString();
+ std::string strFileName = m_pDS->fv("files.strFilename").get_asString();
+ ConstructPath(details.m_strFileNameAndPath, details.m_strPath, strFileName);
+ details.SetPlayCount(std::max(details.GetPlayCount(), m_pDS->fv("files.playCount").get_asInt()));
+ if (!details.m_lastPlayed.IsValid())
+ details.m_lastPlayed.SetFromDBDateTime(m_pDS->fv("files.lastPlayed").get_asString());
+ if (!details.m_dateAdded.IsValid())
+ details.m_dateAdded.SetFromDBDateTime(m_pDS->fv("files.dateAdded").get_asString());
+ if (!details.GetResumePoint().IsSet() ||
+ (!details.GetResumePoint().HasSavedPlayerState() &&
+ !m_pDS->fv("bookmark.playerState").get_asString().empty()))
+ {
+ details.SetResumePoint(m_pDS->fv("bookmark.timeInSeconds").get_asDouble(),
+ m_pDS->fv("bookmark.totalTimeInSeconds").get_asDouble(),
+ m_pDS->fv("bookmark.playerState").get_asString());
+ }
+
+ // get streamdetails
+ GetStreamDetails(details);
+
+ return !details.IsEmpty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+ return false;
+}
+
+std::string CVideoDatabase::GetValueString(const CVideoInfoTag &details, int min, int max, const SDbTableOffsets *offsets) const
+{
+ std::vector<std::string> conditions;
+ for (int i = min + 1; i < max; ++i)
+ {
+ switch (offsets[i].type)
+ {
+ case VIDEODB_TYPE_STRING:
+ conditions.emplace_back(PrepareSQL("c%02d='%s'", i, ((const std::string*)(((const char*)&details)+offsets[i].offset))->c_str()));
+ break;
+ case VIDEODB_TYPE_INT:
+ conditions.emplace_back(PrepareSQL("c%02d='%i'", i, *(const int*)(((const char*)&details)+offsets[i].offset)));
+ break;
+ case VIDEODB_TYPE_COUNT:
+ {
+ int value = *(const int*)(((const char*)&details)+offsets[i].offset);
+ if (value)
+ conditions.emplace_back(PrepareSQL("c%02d=%i", i, value));
+ else
+ conditions.emplace_back(PrepareSQL("c%02d=NULL", i));
+ }
+ break;
+ case VIDEODB_TYPE_BOOL:
+ conditions.emplace_back(PrepareSQL("c%02d='%s'", i, *(const bool*)(((const char*)&details)+offsets[i].offset)?"true":"false"));
+ break;
+ case VIDEODB_TYPE_FLOAT:
+ conditions.emplace_back(PrepareSQL(
+ "c%02d='%f'", i, *(const double*)(((const char*)&details) + offsets[i].offset)));
+ break;
+ case VIDEODB_TYPE_STRINGARRAY:
+ conditions.emplace_back(PrepareSQL("c%02d='%s'", i, StringUtils::Join(*((const std::vector<std::string>*)(((const char*)&details)+offsets[i].offset)),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator).c_str()));
+ break;
+ case VIDEODB_TYPE_DATE:
+ conditions.emplace_back(PrepareSQL("c%02d='%s'", i, ((const CDateTime*)(((const char*)&details)+offsets[i].offset))->GetAsDBDate().c_str()));
+ break;
+ case VIDEODB_TYPE_DATETIME:
+ conditions.emplace_back(PrepareSQL("c%02d='%s'", i, ((const CDateTime*)(((const char*)&details)+offsets[i].offset))->GetAsDBDateTime().c_str()));
+ break;
+ case VIDEODB_TYPE_UNUSED: // Skip the unused field to avoid populating unused data
+ continue;
+ }
+ }
+ return StringUtils::Join(conditions, ",");
+}
+
+//********************************************************************************************************************************
+int CVideoDatabase::SetDetailsForItem(CVideoInfoTag& details, const std::map<std::string, std::string> &artwork)
+{
+ return SetDetailsForItem(details.m_iDbId, details.m_type, details, artwork);
+}
+
+int CVideoDatabase::SetDetailsForItem(int id, const MediaType& mediaType, CVideoInfoTag& details, const std::map<std::string, std::string> &artwork)
+{
+ if (mediaType == MediaTypeNone)
+ return -1;
+
+ if (mediaType == MediaTypeMovie)
+ return SetDetailsForMovie(details, artwork, id);
+ else if (mediaType == MediaTypeVideoCollection)
+ return SetDetailsForMovieSet(details, artwork, id);
+ else if (mediaType == MediaTypeTvShow)
+ {
+ std::map<int, std::map<std::string, std::string> > seasonArtwork;
+ if (!UpdateDetailsForTvShow(id, details, artwork, seasonArtwork))
+ return -1;
+
+ return id;
+ }
+ else if (mediaType == MediaTypeSeason)
+ return SetDetailsForSeason(details, artwork, details.m_iIdShow, id);
+ else if (mediaType == MediaTypeEpisode)
+ return SetDetailsForEpisode(details, artwork, details.m_iIdShow, id);
+ else if (mediaType == MediaTypeMusicVideo)
+ return SetDetailsForMusicVideo(details, artwork, id);
+
+ return -1;
+}
+
+int CVideoDatabase::SetDetailsForMovie(CVideoInfoTag& details,
+ const std::map<std::string, std::string>& artwork,
+ int idMovie /* = -1 */)
+{
+ const auto filePath = details.GetPath();
+
+ try
+ {
+ BeginTransaction();
+
+ if (idMovie < 0)
+ idMovie = GetMovieId(filePath);
+
+ if (idMovie > -1)
+ DeleteMovie(idMovie, true); // true to keep the table entry
+ else
+ {
+ // only add a new movie if we don't already have a valid idMovie
+ // (DeleteMovie is called with bKeepId == true so the movie won't
+ // be removed from the movie table)
+ idMovie = AddNewMovie(details);
+ if (idMovie < 0)
+ {
+ RollbackTransaction();
+ return idMovie;
+ }
+ }
+
+ // update dateadded if it's set
+ if (details.m_dateAdded.IsValid())
+ UpdateFileDateAdded(details);
+
+ AddCast(idMovie, "movie", details.m_cast);
+ AddLinksToItem(idMovie, MediaTypeMovie, "genre", details.m_genre);
+ AddLinksToItem(idMovie, MediaTypeMovie, "studio", details.m_studio);
+ AddLinksToItem(idMovie, MediaTypeMovie, "country", details.m_country);
+ AddLinksToItem(idMovie, MediaTypeMovie, "tag", details.m_tags);
+ AddActorLinksToItem(idMovie, MediaTypeMovie, "director", details.m_director);
+ AddActorLinksToItem(idMovie, MediaTypeMovie, "writer", details.m_writingCredits);
+
+ // add ratingsu
+ details.m_iIdRating = AddRatings(idMovie, MediaTypeMovie, details.m_ratings, details.GetDefaultRating());
+
+ // add unique ids
+ details.m_iIdUniqueID = AddUniqueIDs(idMovie, MediaTypeMovie, details);
+
+ // add set...
+ int idSet = -1;
+ if (!details.m_set.title.empty())
+ {
+ idSet = AddSet(details.m_set.title, details.m_set.overview);
+ // add art if not available
+ if (!HasArtForItem(idSet, MediaTypeVideoCollection))
+ {
+ for (const auto &it : artwork)
+ {
+ if (StringUtils::StartsWith(it.first, "set."))
+ SetArtForItem(idSet, MediaTypeVideoCollection, it.first.substr(4), it.second);
+ }
+ }
+ }
+
+ if (details.HasStreamDetails())
+ SetStreamDetailsForFileId(details.m_streamDetails, GetAndFillFileId(details));
+
+ SetArtForItem(idMovie, MediaTypeMovie, artwork);
+
+ if (!details.HasUniqueID() && details.HasYear())
+ { // query DB for any movies matching online id and year
+ std::string strSQL = PrepareSQL("SELECT files.playCount, files.lastPlayed "
+ "FROM movie "
+ " INNER JOIN files "
+ " ON files.idFile=movie.idFile "
+ " JOIN uniqueid "
+ " ON movie.idMovie=uniqueid.media_id AND uniqueid.media_type='movie' AND uniqueid.value='%s'"
+ "WHERE movie.premiered LIKE '%i%%' AND movie.idMovie!=%i AND files.playCount > 0",
+ details.GetUniqueID().c_str(), details.GetYear(), idMovie);
+ m_pDS->query(strSQL);
+
+ if (!m_pDS->eof())
+ {
+ int playCount = m_pDS->fv("files.playCount").get_asInt();
+
+ CDateTime lastPlayed;
+ lastPlayed.SetFromDBDateTime(m_pDS->fv("files.lastPlayed").get_asString());
+
+ // update with playCount and lastPlayed
+ strSQL =
+ PrepareSQL("update files set playCount=%i,lastPlayed='%s' where idFile=%i", playCount,
+ lastPlayed.GetAsDBDateTime().c_str(), GetAndFillFileId(details));
+ m_pDS->exec(strSQL);
+ }
+
+ m_pDS->close();
+ }
+ // update our movie table (we know it was added already above)
+ // and insert the new row
+ std::string sql = "UPDATE movie SET " + GetValueString(details, VIDEODB_ID_MIN, VIDEODB_ID_MAX, DbMovieOffsets);
+ if (idSet > 0)
+ sql += PrepareSQL(", idSet = %i", idSet);
+ else
+ sql += ", idSet = NULL";
+ if (details.m_iUserRating > 0 && details.m_iUserRating < 11)
+ sql += PrepareSQL(", userrating = %i", details.m_iUserRating);
+ else
+ sql += ", userrating = NULL";
+ if (details.HasPremiered())
+ sql += PrepareSQL(", premiered = '%s'", details.GetPremiered().GetAsDBDate().c_str());
+ else
+ sql += PrepareSQL(", premiered = '%i'", details.GetYear());
+ sql += PrepareSQL(" where idMovie=%i", idMovie);
+ m_pDS->exec(sql);
+ CommitTransaction();
+
+ return idMovie;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
+ }
+ RollbackTransaction();
+ return -1;
+}
+
+int CVideoDatabase::UpdateDetailsForMovie(int idMovie, CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, const std::set<std::string> &updatedDetails)
+{
+ if (idMovie < 0)
+ return idMovie;
+
+ try
+ {
+ CLog::Log(LOGINFO, "{}: Starting updates for movie {}", __FUNCTION__, idMovie);
+
+ BeginTransaction();
+
+ // process the link table updates
+ if (updatedDetails.find("genre") != updatedDetails.end())
+ UpdateLinksToItem(idMovie, MediaTypeMovie, "genre", details.m_genre);
+ if (updatedDetails.find("studio") != updatedDetails.end())
+ UpdateLinksToItem(idMovie, MediaTypeMovie, "studio", details.m_studio);
+ if (updatedDetails.find("country") != updatedDetails.end())
+ UpdateLinksToItem(idMovie, MediaTypeMovie, "country", details.m_country);
+ if (updatedDetails.find("tag") != updatedDetails.end())
+ UpdateLinksToItem(idMovie, MediaTypeMovie, "tag", details.m_tags);
+ if (updatedDetails.find("director") != updatedDetails.end())
+ UpdateActorLinksToItem(idMovie, MediaTypeMovie, "director", details.m_director);
+ if (updatedDetails.find("writer") != updatedDetails.end())
+ UpdateActorLinksToItem(idMovie, MediaTypeMovie, "writer", details.m_writingCredits);
+ if (updatedDetails.find("art.altered") != updatedDetails.end())
+ SetArtForItem(idMovie, MediaTypeMovie, artwork);
+ if (updatedDetails.find("ratings") != updatedDetails.end())
+ details.m_iIdRating = UpdateRatings(idMovie, MediaTypeMovie, details.m_ratings, details.GetDefaultRating());
+ if (updatedDetails.find("uniqueid") != updatedDetails.end())
+ details.m_iIdUniqueID = UpdateUniqueIDs(idMovie, MediaTypeMovie, details);
+ if (updatedDetails.find("dateadded") != updatedDetails.end() && details.m_dateAdded.IsValid())
+ UpdateFileDateAdded(details);
+
+ // track if the set was updated
+ int idSet = 0;
+ if (updatedDetails.find("set") != updatedDetails.end())
+ { // set
+ idSet = -1;
+ if (!details.m_set.title.empty())
+ {
+ idSet = AddSet(details.m_set.title, details.m_set.overview);
+ }
+ }
+
+ if (updatedDetails.find("showlink") != updatedDetails.end())
+ {
+ // remove existing links
+ std::vector<int> tvShowIds;
+ GetLinksToTvShow(idMovie, tvShowIds);
+ for (const auto& idTVShow : tvShowIds)
+ LinkMovieToTvshow(idMovie, idTVShow, true);
+
+ // setup links to shows if the linked shows are in the db
+ for (const auto& showLink : details.m_showLink)
+ {
+ CFileItemList items;
+ GetTvShowsByName(showLink, items);
+ if (!items.IsEmpty())
+ LinkMovieToTvshow(idMovie, items[0]->GetVideoInfoTag()->m_iDbId, false);
+ else
+ CLog::Log(LOGWARNING, "{}: Failed to link movie {} to show {}", __FUNCTION__,
+ details.m_strTitle, showLink);
+ }
+ }
+
+ // and update the movie table
+ std::string sql = "UPDATE movie SET " + GetValueString(details, VIDEODB_ID_MIN, VIDEODB_ID_MAX, DbMovieOffsets);
+ if (idSet > 0)
+ sql += PrepareSQL(", idSet = %i", idSet);
+ else if (idSet < 0)
+ sql += ", idSet = NULL";
+ if (details.m_iUserRating > 0 && details.m_iUserRating < 11)
+ sql += PrepareSQL(", userrating = %i", details.m_iUserRating);
+ else
+ sql += ", userrating = NULL";
+ if (details.HasPremiered())
+ sql += PrepareSQL(", premiered = '%s'", details.GetPremiered().GetAsDBDate().c_str());
+ else
+ sql += PrepareSQL(", premiered = '%i'", details.GetYear());
+ sql += PrepareSQL(" where idMovie=%i", idMovie);
+ m_pDS->exec(sql);
+
+ CommitTransaction();
+
+ CLog::Log(LOGINFO, "{}: Finished updates for movie {}", __FUNCTION__, idMovie);
+
+ return idMovie;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idMovie);
+ }
+ RollbackTransaction();
+ return -1;
+}
+
+int CVideoDatabase::SetDetailsForMovieSet(const CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, int idSet /* = -1 */)
+{
+ if (details.m_strTitle.empty())
+ return -1;
+
+ try
+ {
+ BeginTransaction();
+ if (idSet < 0)
+ {
+ idSet = AddSet(details.m_strTitle, details.m_strPlot);
+ if (idSet < 0)
+ {
+ RollbackTransaction();
+ return -1;
+ }
+ }
+
+ SetArtForItem(idSet, MediaTypeVideoCollection, artwork);
+
+ // and insert the new row
+ std::string sql = PrepareSQL("UPDATE sets SET strSet='%s', strOverview='%s' WHERE idSet=%i", details.m_strTitle.c_str(), details.m_strPlot.c_str(), idSet);
+ m_pDS->exec(sql);
+ CommitTransaction();
+
+ return idSet;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSet);
+ }
+ RollbackTransaction();
+ return -1;
+}
+
+int CVideoDatabase::GetMatchingTvShow(const CVideoInfoTag &details)
+{
+ // first try matching on uniqueid, then on title + year
+ int id = -1;
+ if (!details.HasUniqueID())
+ id = GetDbId(PrepareSQL("SELECT idShow FROM tvshow JOIN uniqueid ON uniqueid.media_id=tvshow.idShow AND uniqueid.media_type='tvshow' WHERE uniqueid.value='%s'", details.GetUniqueID().c_str()));
+ if (id < 0)
+ id = GetDbId(PrepareSQL("SELECT idShow FROM tvshow WHERE c%02d='%s' AND c%02d='%s'", VIDEODB_ID_TV_TITLE, details.m_strTitle.c_str(), VIDEODB_ID_TV_PREMIERED, details.GetPremiered().GetAsDBDate().c_str()));
+ return id;
+}
+
+int CVideoDatabase::SetDetailsForTvShow(const std::vector<std::pair<std::string, std::string> > &paths,
+ CVideoInfoTag& details, const std::map<std::string, std::string> &artwork,
+ const std::map<int, std::map<std::string, std::string> > &seasonArt, int idTvShow /*= -1 */)
+{
+
+ /*
+ The steps are as follows.
+ 1. Check if the tvshow is found on any of the given paths. If found, we have the show id.
+ 2. Search for a matching show. If found, we have the show id.
+ 3. If we don't have the id, add a new show.
+ 4. Add the paths to the show.
+ 5. Add details for the show.
+ */
+
+ if (idTvShow < 0)
+ {
+ for (const auto &i : paths)
+ {
+ idTvShow = GetTvShowId(i.first);
+ if (idTvShow > -1)
+ break;
+ }
+ }
+ if (idTvShow < 0)
+ idTvShow = GetMatchingTvShow(details);
+ if (idTvShow < 0)
+ {
+ idTvShow = AddTvShow();
+ if (idTvShow < 0)
+ return -1;
+ }
+
+ // add any paths to the tvshow
+ for (const auto &i : paths)
+ AddPathToTvShow(idTvShow, i.first, i.second, details.m_dateAdded);
+
+ UpdateDetailsForTvShow(idTvShow, details, artwork, seasonArt);
+
+ return idTvShow;
+}
+
+bool CVideoDatabase::UpdateDetailsForTvShow(int idTvShow, CVideoInfoTag &details,
+ const std::map<std::string, std::string> &artwork, const std::map<int, std::map<std::string, std::string>> &seasonArt)
+{
+ BeginTransaction();
+
+ DeleteDetailsForTvShow(idTvShow);
+
+ AddCast(idTvShow, "tvshow", details.m_cast);
+ AddLinksToItem(idTvShow, MediaTypeTvShow, "genre", details.m_genre);
+ AddLinksToItem(idTvShow, MediaTypeTvShow, "studio", details.m_studio);
+ AddLinksToItem(idTvShow, MediaTypeTvShow, "tag", details.m_tags);
+ AddActorLinksToItem(idTvShow, MediaTypeTvShow, "director", details.m_director);
+
+ // add ratings
+ details.m_iIdRating = AddRatings(idTvShow, MediaTypeTvShow, details.m_ratings, details.GetDefaultRating());
+
+ // add unique ids
+ details.m_iIdUniqueID = AddUniqueIDs(idTvShow, MediaTypeTvShow, details);
+
+ // add "all seasons" - the rest are added in SetDetailsForEpisode
+ AddSeason(idTvShow, -1);
+
+ // add any named seasons
+ for (const auto& namedSeason : details.m_namedSeasons)
+ {
+ // make sure the named season exists
+ int seasonId = AddSeason(idTvShow, namedSeason.first, namedSeason.second);
+
+ // get any existing details for the named season
+ CVideoInfoTag season;
+ if (!GetSeasonInfo(seasonId, season, false) || season.m_strSortTitle == namedSeason.second)
+ continue;
+
+ season.SetSortTitle(namedSeason.second);
+ SetDetailsForSeason(season, std::map<std::string, std::string>(), idTvShow, seasonId);
+ }
+
+ SetArtForItem(idTvShow, MediaTypeTvShow, artwork);
+ for (const auto &i : seasonArt)
+ {
+ int idSeason = AddSeason(idTvShow, i.first);
+ if (idSeason > -1)
+ SetArtForItem(idSeason, MediaTypeSeason, i.second);
+ }
+
+ // and insert the new row
+ std::string sql = "UPDATE tvshow SET " + GetValueString(details, VIDEODB_ID_TV_MIN, VIDEODB_ID_TV_MAX, DbTvShowOffsets);
+ if (details.m_iUserRating > 0 && details.m_iUserRating < 11)
+ sql += PrepareSQL(", userrating = %i", details.m_iUserRating);
+ else
+ sql += ", userrating = NULL";
+ if (details.GetDuration() > 0)
+ sql += PrepareSQL(", duration = %i", details.GetDuration());
+ else
+ sql += ", duration = NULL";
+ sql += PrepareSQL(" WHERE idShow=%i", idTvShow);
+ if (ExecuteQuery(sql))
+ {
+ CommitTransaction();
+ return true;
+ }
+ RollbackTransaction();
+ return false;
+}
+
+int CVideoDatabase::SetDetailsForSeason(const CVideoInfoTag& details, const std::map<std::string,
+ std::string> &artwork, int idShow, int idSeason /* = -1 */)
+{
+ if (idShow < 0 || details.m_iSeason < -1)
+ return -1;
+
+ try
+ {
+ BeginTransaction();
+ if (idSeason < 0)
+ {
+ idSeason = AddSeason(idShow, details.m_iSeason);
+ if (idSeason < 0)
+ {
+ RollbackTransaction();
+ return -1;
+ }
+ }
+
+ SetArtForItem(idSeason, MediaTypeSeason, artwork);
+
+ // and insert the new row
+ std::string sql = PrepareSQL("UPDATE seasons SET season=%i", details.m_iSeason);
+ if (!details.m_strSortTitle.empty())
+ sql += PrepareSQL(", name='%s'", details.m_strSortTitle.c_str());
+ if (details.m_iUserRating > 0 && details.m_iUserRating < 11)
+ sql += PrepareSQL(", userrating = %i", details.m_iUserRating);
+ else
+ sql += ", userrating = NULL";
+ sql += PrepareSQL(" WHERE idSeason=%i", idSeason);
+ m_pDS->exec(sql.c_str());
+ CommitTransaction();
+
+ return idSeason;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSeason);
+ }
+ RollbackTransaction();
+ return -1;
+}
+
+int CVideoDatabase::SetDetailsForEpisode(CVideoInfoTag& details,
+ const std::map<std::string, std::string>& artwork,
+ int idShow,
+ int idEpisode /* = -1 */)
+{
+ const auto filePath = details.GetPath();
+
+ try
+ {
+ BeginTransaction();
+ if (idEpisode < 0)
+ idEpisode = GetEpisodeId(filePath);
+
+ if (idEpisode > 0)
+ DeleteEpisode(idEpisode, true); // true to keep the table entry
+ else
+ {
+ // only add a new episode if we don't already have a valid idEpisode
+ // (DeleteEpisode is called with bKeepId == true so the episode won't
+ // be removed from the episode table)
+ idEpisode = AddNewEpisode(idShow, details);
+ if (idEpisode < 0)
+ {
+ RollbackTransaction();
+ return -1;
+ }
+ }
+
+ // update dateadded if it's set
+ if (details.m_dateAdded.IsValid())
+ UpdateFileDateAdded(details);
+
+ AddCast(idEpisode, "episode", details.m_cast);
+ AddActorLinksToItem(idEpisode, MediaTypeEpisode, "director", details.m_director);
+ AddActorLinksToItem(idEpisode, MediaTypeEpisode, "writer", details.m_writingCredits);
+
+ // add ratings
+ details.m_iIdRating = AddRatings(idEpisode, MediaTypeEpisode, details.m_ratings, details.GetDefaultRating());
+
+ // add unique ids
+ details.m_iIdUniqueID = AddUniqueIDs(idEpisode, MediaTypeEpisode, details);
+
+ if (details.HasStreamDetails())
+ SetStreamDetailsForFileId(details.m_streamDetails, GetAndFillFileId(details));
+
+ // ensure we have this season already added
+ int idSeason = AddSeason(idShow, details.m_iSeason);
+
+ SetArtForItem(idEpisode, MediaTypeEpisode, artwork);
+
+ if (details.m_iEpisode != -1 && details.m_iSeason != -1)
+ { // query DB for any episodes matching idShow, Season and Episode
+ std::string strSQL = PrepareSQL("SELECT files.playCount, files.lastPlayed "
+ "FROM episode INNER JOIN files ON files.idFile=episode.idFile "
+ "WHERE episode.c%02d=%i AND episode.c%02d=%i AND episode.idShow=%i "
+ "AND episode.idEpisode!=%i AND files.playCount > 0",
+ VIDEODB_ID_EPISODE_SEASON, details.m_iSeason, VIDEODB_ID_EPISODE_EPISODE,
+ details.m_iEpisode, idShow, idEpisode);
+ m_pDS->query(strSQL);
+
+ if (!m_pDS->eof())
+ {
+ int playCount = m_pDS->fv("files.playCount").get_asInt();
+
+ CDateTime lastPlayed;
+ lastPlayed.SetFromDBDateTime(m_pDS->fv("files.lastPlayed").get_asString());
+
+ // update with playCount and lastPlayed
+ strSQL =
+ PrepareSQL("update files set playCount=%i,lastPlayed='%s' where idFile=%i", playCount,
+ lastPlayed.GetAsDBDateTime().c_str(), GetAndFillFileId(details));
+ m_pDS->exec(strSQL);
+ }
+
+ m_pDS->close();
+ }
+ // and insert the new row
+ std::string sql = "UPDATE episode SET " + GetValueString(details, VIDEODB_ID_EPISODE_MIN, VIDEODB_ID_EPISODE_MAX, DbEpisodeOffsets);
+ if (details.m_iUserRating > 0 && details.m_iUserRating < 11)
+ sql += PrepareSQL(", userrating = %i", details.m_iUserRating);
+ else
+ sql += ", userrating = NULL";
+ sql += PrepareSQL(", idSeason = %i", idSeason);
+ sql += PrepareSQL(" where idEpisode=%i", idEpisode);
+ m_pDS->exec(sql);
+ CommitTransaction();
+
+ return idEpisode;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
+ }
+ RollbackTransaction();
+ return -1;
+}
+
+int CVideoDatabase::GetSeasonId(int showID, int season)
+{
+ std::string sql = PrepareSQL("idShow=%i AND season=%i", showID, season);
+ std::string id = GetSingleValue("seasons", "idSeason", sql);
+ if (id.empty())
+ return -1;
+ return strtol(id.c_str(), NULL, 10);
+}
+
+int CVideoDatabase::AddSeason(int showID, int season, const std::string& name /* = "" */)
+{
+ int seasonId = GetSeasonId(showID, season);
+ if (seasonId < 0)
+ {
+ if (ExecuteQuery(PrepareSQL("INSERT INTO seasons (idShow, season, name) VALUES(%i, %i, '%s')", showID, season, name.c_str())))
+ seasonId = (int)m_pDS->lastinsertid();
+ }
+ return seasonId;
+}
+
+int CVideoDatabase::SetDetailsForMusicVideo(CVideoInfoTag& details,
+ const std::map<std::string, std::string>& artwork,
+ int idMVideo /* = -1 */)
+{
+ const auto filePath = details.GetPath();
+
+ try
+ {
+ BeginTransaction();
+
+ if (idMVideo < 0)
+ idMVideo = GetMusicVideoId(filePath);
+
+ if (idMVideo > -1)
+ DeleteMusicVideo(idMVideo, true); // Keep id
+ else
+ {
+ // only add a new musicvideo if we don't already have a valid idMVideo
+ // (DeleteMusicVideo is called with bKeepId == true so the musicvideo won't
+ // be removed from the musicvideo table)
+ idMVideo = AddNewMusicVideo(details);
+ if (idMVideo < 0)
+ {
+ RollbackTransaction();
+ return -1;
+ }
+ }
+
+ // update dateadded if it's set
+ if (details.m_dateAdded.IsValid())
+ UpdateFileDateAdded(details);
+
+ AddCast(idMVideo, MediaTypeMusicVideo, details.m_cast);
+ AddActorLinksToItem(idMVideo, MediaTypeMusicVideo, "actor", details.m_artist);
+ AddActorLinksToItem(idMVideo, MediaTypeMusicVideo, "director", details.m_director);
+ AddLinksToItem(idMVideo, MediaTypeMusicVideo, "genre", details.m_genre);
+ AddLinksToItem(idMVideo, MediaTypeMusicVideo, "studio", details.m_studio);
+ AddLinksToItem(idMVideo, MediaTypeMusicVideo, "tag", details.m_tags);
+
+ // add unique ids
+ details.m_iIdUniqueID = UpdateUniqueIDs(idMVideo, MediaTypeMusicVideo, details);
+
+ if (details.HasStreamDetails())
+ SetStreamDetailsForFileId(details.m_streamDetails, GetAndFillFileId(details));
+
+ SetArtForItem(idMVideo, MediaTypeMusicVideo, artwork);
+
+ // update our movie table (we know it was added already above)
+ // and insert the new row
+ std::string sql = "UPDATE musicvideo SET " + GetValueString(details, VIDEODB_ID_MUSICVIDEO_MIN, VIDEODB_ID_MUSICVIDEO_MAX, DbMusicVideoOffsets);
+ if (details.m_iUserRating > 0 && details.m_iUserRating < 11)
+ sql += PrepareSQL(", userrating = %i", details.m_iUserRating);
+ else
+ sql += ", userrating = NULL";
+ if (details.HasPremiered())
+ sql += PrepareSQL(", premiered = '%s'", details.GetPremiered().GetAsDBDate().c_str());
+ else
+ sql += PrepareSQL(", premiered = '%i'", details.GetYear());
+ sql += PrepareSQL(" where idMVideo=%i", idMVideo);
+ m_pDS->exec(sql);
+ CommitTransaction();
+
+ return idMVideo;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
+ }
+ RollbackTransaction();
+ return -1;
+}
+
+void CVideoDatabase::SetStreamDetailsForFile(const CStreamDetails& details, const std::string &strFileNameAndPath)
+{
+ // AddFile checks to make sure the file isn't already in the DB first
+ int idFile = AddFile(strFileNameAndPath);
+ if (idFile < 0)
+ return;
+ SetStreamDetailsForFileId(details, idFile);
+}
+
+void CVideoDatabase::SetStreamDetailsForFileId(const CStreamDetails& details, int idFile)
+{
+ if (idFile < 0)
+ return;
+
+ try
+ {
+ m_pDS->exec(PrepareSQL("DELETE FROM streamdetails WHERE idFile = %i", idFile));
+
+ for (int i=1; i<=details.GetVideoStreamCount(); i++)
+ {
+ m_pDS->exec(PrepareSQL("INSERT INTO streamdetails "
+ "(idFile, iStreamType, strVideoCodec, fVideoAspect, iVideoWidth, "
+ "iVideoHeight, iVideoDuration, strStereoMode, strVideoLanguage, "
+ "strHdrType)"
+ "VALUES (%i,%i,'%s',%f,%i,%i,%i,'%s','%s','%s')",
+ idFile, (int)CStreamDetail::VIDEO, details.GetVideoCodec(i).c_str(),
+ static_cast<double>(details.GetVideoAspect(i)),
+ details.GetVideoWidth(i), details.GetVideoHeight(i),
+ details.GetVideoDuration(i), details.GetStereoMode(i).c_str(),
+ details.GetVideoLanguage(i).c_str(),
+ details.GetVideoHdrType(i).c_str()));
+ }
+ for (int i=1; i<=details.GetAudioStreamCount(); i++)
+ {
+ m_pDS->exec(PrepareSQL("INSERT INTO streamdetails "
+ "(idFile, iStreamType, strAudioCodec, iAudioChannels, strAudioLanguage) "
+ "VALUES (%i,%i,'%s',%i,'%s')",
+ idFile, (int)CStreamDetail::AUDIO,
+ details.GetAudioCodec(i).c_str(), details.GetAudioChannels(i),
+ details.GetAudioLanguage(i).c_str()));
+ }
+ for (int i=1; i<=details.GetSubtitleStreamCount(); i++)
+ {
+ m_pDS->exec(PrepareSQL("INSERT INTO streamdetails "
+ "(idFile, iStreamType, strSubtitleLanguage) "
+ "VALUES (%i,%i,'%s')",
+ idFile, (int)CStreamDetail::SUBTITLE,
+ details.GetSubtitleLanguage(i).c_str()));
+ }
+
+ // update the runtime information, if empty
+ if (details.GetVideoDuration())
+ {
+ std::vector<std::pair<std::string, int> > tables;
+ tables.emplace_back("movie", VIDEODB_ID_RUNTIME);
+ tables.emplace_back("episode", VIDEODB_ID_EPISODE_RUNTIME);
+ tables.emplace_back("musicvideo", VIDEODB_ID_MUSICVIDEO_RUNTIME);
+ for (const auto &i : tables)
+ {
+ std::string sql = PrepareSQL("update %s set c%02d=%d where idFile=%d and c%02d=''",
+ i.first.c_str(), i.second, details.GetVideoDuration(), idFile, i.second);
+ m_pDS->exec(sql);
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idFile);
+ }
+}
+
+//********************************************************************************************************************************
+void CVideoDatabase::GetFilePathById(int idMovie, std::string& filePath, VideoDbContentType iType)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (idMovie < 0) return ;
+
+ std::string strSQL;
+ if (iType == VideoDbContentType::MOVIES)
+ strSQL=PrepareSQL("SELECT path.strPath, files.strFileName FROM path INNER JOIN files ON path.idPath=files.idPath INNER JOIN movie ON files.idFile=movie.idFile WHERE movie.idMovie=%i ORDER BY strFilename", idMovie );
+ if (iType == VideoDbContentType::EPISODES)
+ strSQL=PrepareSQL("SELECT path.strPath, files.strFileName FROM path INNER JOIN files ON path.idPath=files.idPath INNER JOIN episode ON files.idFile=episode.idFile WHERE episode.idEpisode=%i ORDER BY strFilename", idMovie );
+ if (iType == VideoDbContentType::TVSHOWS)
+ strSQL=PrepareSQL("SELECT path.strPath FROM path INNER JOIN tvshowlinkpath ON path.idPath=tvshowlinkpath.idPath WHERE tvshowlinkpath.idShow=%i", idMovie );
+ if (iType == VideoDbContentType::MUSICVIDEOS)
+ strSQL=PrepareSQL("SELECT path.strPath, files.strFileName FROM path INNER JOIN files ON path.idPath=files.idPath INNER JOIN musicvideo ON files.idFile=musicvideo.idFile WHERE musicvideo.idMVideo=%i ORDER BY strFilename", idMovie );
+
+ m_pDS->query( strSQL );
+ if (!m_pDS->eof())
+ {
+ if (iType != VideoDbContentType::TVSHOWS)
+ {
+ std::string fileName = m_pDS->fv("files.strFilename").get_asString();
+ ConstructPath(filePath,m_pDS->fv("path.strPath").get_asString(),fileName);
+ }
+ else
+ filePath = m_pDS->fv("path.strPath").get_asString();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+}
+
+//********************************************************************************************************************************
+void CVideoDatabase::GetBookMarksForFile(const std::string& strFilenameAndPath, VECBOOKMARKS& bookmarks, CBookmark::EType type /*= CBookmark::STANDARD*/, bool bAppend, long partNumber)
+{
+ try
+ {
+ if (URIUtils::IsStack(strFilenameAndPath) && CFileItem(CStackDirectory::GetFirstStackedFile(strFilenameAndPath),false).IsDiscImage())
+ {
+ CStackDirectory dir;
+ CFileItemList fileList;
+ const CURL pathToUrl(strFilenameAndPath);
+ dir.GetDirectory(pathToUrl, fileList);
+ if (!bAppend)
+ bookmarks.clear();
+ for (int i = fileList.Size() - 1; i >= 0; i--) // put the bookmarks of the highest part first in the list
+ GetBookMarksForFile(fileList[i]->GetPath(), bookmarks, type, true, (i+1));
+ }
+ else
+ {
+ int idFile = GetFileId(strFilenameAndPath);
+ if (idFile < 0) return ;
+ if (!bAppend)
+ bookmarks.erase(bookmarks.begin(), bookmarks.end());
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ std::string strSQL=PrepareSQL("select * from bookmark where idFile=%i and type=%i order by timeInSeconds", idFile, (int)type);
+ m_pDS->query( strSQL );
+ while (!m_pDS->eof())
+ {
+ CBookmark bookmark;
+ bookmark.timeInSeconds = m_pDS->fv("timeInSeconds").get_asDouble();
+ bookmark.partNumber = partNumber;
+ bookmark.totalTimeInSeconds = m_pDS->fv("totalTimeInSeconds").get_asDouble();
+ bookmark.thumbNailImage = m_pDS->fv("thumbNailImage").get_asString();
+ bookmark.playerState = m_pDS->fv("playerState").get_asString();
+ bookmark.player = m_pDS->fv("player").get_asString();
+ bookmark.type = type;
+ if (type == CBookmark::EPISODE)
+ {
+ std::string strSQL2=PrepareSQL("select c%02d, c%02d from episode where c%02d=%i order by c%02d, c%02d", VIDEODB_ID_EPISODE_EPISODE, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_EPISODE_BOOKMARK, m_pDS->fv("idBookmark").get_asInt(), VIDEODB_ID_EPISODE_SORTSEASON, VIDEODB_ID_EPISODE_SORTEPISODE);
+ m_pDS2->query(strSQL2);
+ bookmark.episodeNumber = m_pDS2->fv(0).get_asInt();
+ bookmark.seasonNumber = m_pDS2->fv(1).get_asInt();
+ m_pDS2->close();
+ }
+ bookmarks.push_back(bookmark);
+ m_pDS->next();
+ }
+ //sort(bookmarks.begin(), bookmarks.end(), SortBookmarks);
+ m_pDS->close();
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+}
+
+bool CVideoDatabase::GetResumeBookMark(const std::string& strFilenameAndPath, CBookmark &bookmark)
+{
+ VECBOOKMARKS bookmarks;
+ GetBookMarksForFile(strFilenameAndPath, bookmarks, CBookmark::RESUME);
+ if (!bookmarks.empty())
+ {
+ bookmark = bookmarks[0];
+ return true;
+ }
+ return false;
+}
+
+void CVideoDatabase::DeleteResumeBookMark(const CFileItem& item)
+{
+ if (!m_pDB || !m_pDS)
+ return;
+
+ int fileID = item.GetVideoInfoTag()->m_iFileId;
+ if (fileID < 0)
+ {
+ fileID = GetFileId(item.GetPath());
+ if (fileID < 0)
+ return;
+ }
+
+ try
+ {
+ std::string sql = PrepareSQL("delete from bookmark where idFile=%i and type=%i", fileID, CBookmark::RESUME);
+ m_pDS->exec(sql);
+
+ VideoDbContentType iType = static_cast<VideoDbContentType>(item.GetVideoContentType());
+ std::string content;
+ switch (iType)
+ {
+ case VideoDbContentType::MOVIES:
+ content = MediaTypeMovie;
+ break;
+ case VideoDbContentType::EPISODES:
+ content = MediaTypeEpisode;
+ break;
+ case VideoDbContentType::TVSHOWS:
+ content = MediaTypeTvShow;
+ break;
+ case VideoDbContentType::MUSICVIDEOS:
+ content = MediaTypeMusicVideo;
+ break;
+ default:
+ break;
+ }
+
+ if (!content.empty())
+ {
+ AnnounceUpdate(content, item.GetVideoInfoTag()->m_iDbId);
+ }
+
+ }
+ catch(...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__,
+ item.GetVideoInfoTag()->m_strFileNameAndPath);
+ }
+}
+
+void CVideoDatabase::GetEpisodesByFile(const std::string& strFilenameAndPath, std::vector<CVideoInfoTag>& episodes)
+{
+ try
+ {
+ std::string strSQL = PrepareSQL("select * from episode_view where idFile=%i order by c%02d, c%02d asc", GetFileId(strFilenameAndPath), VIDEODB_ID_EPISODE_SORTSEASON, VIDEODB_ID_EPISODE_SORTEPISODE);
+ m_pDS->query(strSQL);
+ while (!m_pDS->eof())
+ {
+ episodes.emplace_back(GetDetailsForEpisode(m_pDS));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+}
+
+//********************************************************************************************************************************
+void CVideoDatabase::AddBookMarkToFile(const std::string& strFilenameAndPath, const CBookmark &bookmark, CBookmark::EType type /*= CBookmark::STANDARD*/)
+{
+ try
+ {
+ int idFile = AddFile(strFilenameAndPath);
+ if (idFile < 0)
+ return;
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ std::string strSQL;
+ int idBookmark=-1;
+ if (type == CBookmark::RESUME) // get the same resume mark bookmark each time type
+ {
+ strSQL=PrepareSQL("select idBookmark from bookmark where idFile=%i and type=1", idFile);
+ }
+ else if (type == CBookmark::STANDARD) // get the same bookmark again, and update. not sure here as a dvd can have same time in multiple places, state will differ thou
+ {
+ /* get a bookmark within the same time as previous */
+ double mintime = bookmark.timeInSeconds - 0.5;
+ double maxtime = bookmark.timeInSeconds + 0.5;
+ strSQL=PrepareSQL("select idBookmark from bookmark where idFile=%i and type=%i and (timeInSeconds between %f and %f) and playerState='%s'", idFile, (int)type, mintime, maxtime, bookmark.playerState.c_str());
+ }
+
+ if (type != CBookmark::EPISODE)
+ {
+ // get current id
+ m_pDS->query( strSQL );
+ if (m_pDS->num_rows() != 0)
+ idBookmark = m_pDS->get_field_value("idBookmark").get_asInt();
+ m_pDS->close();
+ }
+ // update or insert depending if it existed before
+ if (idBookmark >= 0 )
+ strSQL=PrepareSQL("update bookmark set timeInSeconds = %f, totalTimeInSeconds = %f, thumbNailImage = '%s', player = '%s', playerState = '%s' where idBookmark = %i", bookmark.timeInSeconds, bookmark.totalTimeInSeconds, bookmark.thumbNailImage.c_str(), bookmark.player.c_str(), bookmark.playerState.c_str(), idBookmark);
+ else
+ strSQL=PrepareSQL("insert into bookmark (idBookmark, idFile, timeInSeconds, totalTimeInSeconds, thumbNailImage, player, playerState, type) values(NULL,%i,%f,%f,'%s','%s','%s', %i)", idFile, bookmark.timeInSeconds, bookmark.totalTimeInSeconds, bookmark.thumbNailImage.c_str(), bookmark.player.c_str(), bookmark.playerState.c_str(), (int)type);
+
+ m_pDS->exec(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+}
+
+void CVideoDatabase::ClearBookMarkOfFile(const std::string& strFilenameAndPath, CBookmark& bookmark, CBookmark::EType type /*= CBookmark::STANDARD*/)
+{
+ try
+ {
+ int idFile = GetFileId(strFilenameAndPath);
+ if (idFile < 0) return ;
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ /* a little bit ugly, we clear first bookmark that is within one second of given */
+ /* should be no problem since we never add bookmarks that are closer than that */
+ double mintime = bookmark.timeInSeconds - 0.5;
+ double maxtime = bookmark.timeInSeconds + 0.5;
+ std::string strSQL = PrepareSQL("select idBookmark from bookmark where idFile=%i and type=%i and playerState like '%s' and player like '%s' and (timeInSeconds between %f and %f)", idFile, type, bookmark.playerState.c_str(), bookmark.player.c_str(), mintime, maxtime);
+
+ m_pDS->query( strSQL );
+ if (m_pDS->num_rows() != 0)
+ {
+ int idBookmark = m_pDS->get_field_value("idBookmark").get_asInt();
+ strSQL=PrepareSQL("delete from bookmark where idBookmark=%i",idBookmark);
+ m_pDS->exec(strSQL);
+ if (type == CBookmark::EPISODE)
+ {
+ strSQL=PrepareSQL("update episode set c%02d=-1 where idFile=%i and c%02d=%i", VIDEODB_ID_EPISODE_BOOKMARK, idFile, VIDEODB_ID_EPISODE_BOOKMARK, idBookmark);
+ m_pDS->exec(strSQL);
+ }
+ }
+
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strFilenameAndPath);
+ }
+}
+
+//********************************************************************************************************************************
+void CVideoDatabase::ClearBookMarksOfFile(const std::string& strFilenameAndPath, CBookmark::EType type /*= CBookmark::STANDARD*/)
+{
+ int idFile = GetFileId(strFilenameAndPath);
+ if (idFile >= 0)
+ return ClearBookMarksOfFile(idFile, type);
+}
+
+void CVideoDatabase::ClearBookMarksOfFile(int idFile, CBookmark::EType type /*= CBookmark::STANDARD*/)
+{
+ if (idFile < 0)
+ return;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ std::string strSQL=PrepareSQL("delete from bookmark where idFile=%i and type=%i", idFile, (int)type);
+ m_pDS->exec(strSQL);
+ if (type == CBookmark::EPISODE)
+ {
+ strSQL=PrepareSQL("update episode set c%02d=-1 where idFile=%i", VIDEODB_ID_EPISODE_BOOKMARK, idFile);
+ m_pDS->exec(strSQL);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idFile);
+ }
+}
+
+
+bool CVideoDatabase::GetBookMarkForEpisode(const CVideoInfoTag& tag, CBookmark& bookmark)
+{
+ try
+ {
+ std::string strSQL = PrepareSQL("select bookmark.* from bookmark join episode on episode.c%02d=bookmark.idBookmark where episode.idEpisode=%i", VIDEODB_ID_EPISODE_BOOKMARK, tag.m_iDbId);
+ m_pDS2->query( strSQL );
+ if (!m_pDS2->eof())
+ {
+ bookmark.timeInSeconds = m_pDS2->fv("timeInSeconds").get_asDouble();
+ bookmark.totalTimeInSeconds = m_pDS2->fv("totalTimeInSeconds").get_asDouble();
+ bookmark.thumbNailImage = m_pDS2->fv("thumbNailImage").get_asString();
+ bookmark.playerState = m_pDS2->fv("playerState").get_asString();
+ bookmark.player = m_pDS2->fv("player").get_asString();
+ bookmark.type = (CBookmark::EType)m_pDS2->fv("type").get_asInt();
+ }
+ else
+ {
+ m_pDS2->close();
+ return false;
+ }
+ m_pDS2->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ return false;
+ }
+ return true;
+}
+
+void CVideoDatabase::AddBookMarkForEpisode(const CVideoInfoTag& tag, const CBookmark& bookmark)
+{
+ try
+ {
+ int idFile = GetFileId(tag.m_strFileNameAndPath);
+ // delete the current episode for the selected episode number
+ std::string strSQL = PrepareSQL("delete from bookmark where idBookmark in (select c%02d from episode where c%02d=%i and c%02d=%i and idFile=%i)", VIDEODB_ID_EPISODE_BOOKMARK, VIDEODB_ID_EPISODE_SEASON, tag.m_iSeason, VIDEODB_ID_EPISODE_EPISODE, tag.m_iEpisode, idFile);
+ m_pDS->exec(strSQL);
+
+ AddBookMarkToFile(tag.m_strFileNameAndPath, bookmark, CBookmark::EPISODE);
+ int idBookmark = (int)m_pDS->lastinsertid();
+ strSQL = PrepareSQL("update episode set c%02d=%i where c%02d=%i and c%02d=%i and idFile=%i", VIDEODB_ID_EPISODE_BOOKMARK, idBookmark, VIDEODB_ID_EPISODE_SEASON, tag.m_iSeason, VIDEODB_ID_EPISODE_EPISODE, tag.m_iEpisode, idFile);
+ m_pDS->exec(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, tag.m_iDbId);
+ }
+}
+
+void CVideoDatabase::DeleteBookMarkForEpisode(const CVideoInfoTag& tag)
+{
+ try
+ {
+ std::string strSQL = PrepareSQL("delete from bookmark where idBookmark in (select c%02d from episode where idEpisode=%i)", VIDEODB_ID_EPISODE_BOOKMARK, tag.m_iDbId);
+ m_pDS->exec(strSQL);
+ strSQL = PrepareSQL("update episode set c%02d=-1 where idEpisode=%i", VIDEODB_ID_EPISODE_BOOKMARK, tag.m_iDbId);
+ m_pDS->exec(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, tag.m_iDbId);
+ }
+}
+
+//********************************************************************************************************************************
+void CVideoDatabase::DeleteMovie(int idMovie, bool bKeepId /* = false */)
+{
+ if (idMovie < 0)
+ return;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ BeginTransaction();
+
+ int idFile = GetDbId(PrepareSQL("SELECT idFile FROM movie WHERE idMovie=%i", idMovie));
+ DeleteStreamDetails(idFile);
+
+ // keep the movie table entry, linking to tv shows, and bookmarks
+ // so we can update the data in place
+ // the ancillary tables are still purged
+ if (!bKeepId)
+ {
+ std::string path = GetSingleValue(PrepareSQL("SELECT strPath FROM path JOIN files ON files.idPath=path.idPath WHERE files.idFile=%i", idFile));
+ if (!path.empty())
+ InvalidatePathHash(path);
+
+ std::string strSQL = PrepareSQL("delete from movie where idMovie=%i", idMovie);
+ m_pDS->exec(strSQL);
+ }
+
+ //! @todo move this below CommitTransaction() once UPnP doesn't rely on this anymore
+ if (!bKeepId)
+ AnnounceRemove(MediaTypeMovie, idMovie);
+
+ CommitTransaction();
+
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ RollbackTransaction();
+ }
+}
+
+void CVideoDatabase::DeleteTvShow(const std::string& strPath)
+{
+ int idTvShow = GetTvShowId(strPath);
+ if (idTvShow >= 0)
+ DeleteTvShow(idTvShow);
+}
+
+void CVideoDatabase::DeleteTvShow(int idTvShow, bool bKeepId /* = false */)
+{
+ if (idTvShow < 0)
+ return;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ BeginTransaction();
+
+ std::set<int> paths;
+ GetPathsForTvShow(idTvShow, paths);
+
+ std::string strSQL=PrepareSQL("SELECT episode.idEpisode FROM episode WHERE episode.idShow=%i",idTvShow);
+ m_pDS2->query(strSQL);
+ while (!m_pDS2->eof())
+ {
+ DeleteEpisode(m_pDS2->fv(0).get_asInt(), bKeepId);
+ m_pDS2->next();
+ }
+
+ DeleteDetailsForTvShow(idTvShow);
+
+ strSQL=PrepareSQL("delete from seasons where idShow=%i", idTvShow);
+ m_pDS->exec(strSQL);
+
+ // keep tvshow table and movielink table so we can update data in place
+ if (!bKeepId)
+ {
+ strSQL=PrepareSQL("delete from tvshow where idShow=%i", idTvShow);
+ m_pDS->exec(strSQL);
+
+ for (const auto &i : paths)
+ {
+ std::string path = GetSingleValue(PrepareSQL("SELECT strPath FROM path WHERE idPath=%i", i));
+ if (!path.empty())
+ InvalidatePathHash(path);
+ }
+ }
+
+ //! @todo move this below CommitTransaction() once UPnP doesn't rely on this anymore
+ if (!bKeepId)
+ AnnounceRemove(MediaTypeTvShow, idTvShow);
+
+ CommitTransaction();
+
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idTvShow);
+ RollbackTransaction();
+ }
+}
+
+void CVideoDatabase::DeleteSeason(int idSeason, bool bKeepId /* = false */)
+{
+ if (idSeason < 0)
+ return;
+
+ try
+ {
+ if (m_pDB == nullptr || m_pDS == nullptr || m_pDS2 == nullptr)
+ return;
+
+ BeginTransaction();
+
+ std::string strSQL = PrepareSQL("SELECT episode.idEpisode FROM episode "
+ "JOIN seasons ON seasons.idSeason = %d AND episode.idShow = seasons.idShow AND episode.c%02d = seasons.season ",
+ idSeason, VIDEODB_ID_EPISODE_SEASON);
+ m_pDS2->query(strSQL);
+ while (!m_pDS2->eof())
+ {
+ DeleteEpisode(m_pDS2->fv(0).get_asInt(), bKeepId);
+ m_pDS2->next();
+ }
+
+ ExecuteQuery(PrepareSQL("DELETE FROM seasons WHERE idSeason = %i", idSeason));
+
+ CommitTransaction();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSeason);
+ RollbackTransaction();
+ }
+}
+
+void CVideoDatabase::DeleteEpisode(int idEpisode, bool bKeepId /* = false */)
+{
+ if (idEpisode < 0)
+ return;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ //! @todo move this below CommitTransaction() once UPnP doesn't rely on this anymore
+ if (!bKeepId)
+ AnnounceRemove(MediaTypeEpisode, idEpisode);
+
+ int idFile = GetDbId(PrepareSQL("SELECT idFile FROM episode WHERE idEpisode=%i", idEpisode));
+ DeleteStreamDetails(idFile);
+
+ // keep episode table entry and bookmarks so we can update the data in place
+ // the ancillary tables are still purged
+ if (!bKeepId)
+ {
+ std::string path = GetSingleValue(PrepareSQL("SELECT strPath FROM path JOIN files ON files.idPath=path.idPath WHERE files.idFile=%i", idFile));
+ if (!path.empty())
+ InvalidatePathHash(path);
+
+ std::string strSQL = PrepareSQL("delete from episode where idEpisode=%i", idEpisode);
+ m_pDS->exec(strSQL);
+ }
+
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idEpisode);
+ }
+}
+
+void CVideoDatabase::DeleteMusicVideo(int idMVideo, bool bKeepId /* = false */)
+{
+ if (idMVideo < 0)
+ return;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ BeginTransaction();
+
+ int idFile = GetDbId(PrepareSQL("SELECT idFile FROM musicvideo WHERE idMVideo=%i", idMVideo));
+ DeleteStreamDetails(idFile);
+
+ // keep the music video table entry and bookmarks so we can update data in place
+ // the ancillary tables are still purged
+ if (!bKeepId)
+ {
+ std::string path = GetSingleValue(PrepareSQL("SELECT strPath FROM path JOIN files ON files.idPath=path.idPath WHERE files.idFile=%i", idFile));
+ if (!path.empty())
+ InvalidatePathHash(path);
+
+ std::string strSQL = PrepareSQL("delete from musicvideo where idMVideo=%i", idMVideo);
+ m_pDS->exec(strSQL);
+ }
+
+ //! @todo move this below CommitTransaction() once UPnP doesn't rely on this anymore
+ if (!bKeepId)
+ AnnounceRemove(MediaTypeMusicVideo, idMVideo);
+
+ CommitTransaction();
+
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ RollbackTransaction();
+ }
+}
+
+int CVideoDatabase::GetDbId(const std::string &query)
+{
+ std::string result = GetSingleValue(query);
+ if (!result.empty())
+ {
+ int idDb = strtol(result.c_str(), NULL, 10);
+ if (idDb > 0)
+ return idDb;
+ }
+ return -1;
+}
+
+void CVideoDatabase::DeleteStreamDetails(int idFile)
+{
+ m_pDS->exec(PrepareSQL("DELETE FROM streamdetails WHERE idFile = %i", idFile));
+}
+
+void CVideoDatabase::DeleteSet(int idSet)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ std::string strSQL;
+ strSQL=PrepareSQL("delete from sets where idSet = %i", idSet);
+ m_pDS->exec(strSQL);
+ strSQL=PrepareSQL("update movie set idSet = null where idSet = %i", idSet);
+ m_pDS->exec(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idSet);
+ }
+}
+
+void CVideoDatabase::ClearMovieSet(int idMovie)
+{
+ SetMovieSet(idMovie, -1);
+}
+
+void CVideoDatabase::SetMovieSet(int idMovie, int idSet)
+{
+ if (idSet >= 0)
+ ExecuteQuery(PrepareSQL("update movie set idSet = %i where idMovie = %i", idSet, idMovie));
+ else
+ ExecuteQuery(PrepareSQL("update movie set idSet = null where idMovie = %i", idMovie));
+}
+
+void CVideoDatabase::DeleteTag(int idTag, VideoDbContentType mediaType)
+{
+ try
+ {
+ if (m_pDB == nullptr || m_pDS == nullptr)
+ return;
+
+ std::string type;
+ if (mediaType == VideoDbContentType::MOVIES)
+ type = MediaTypeMovie;
+ else if (mediaType == VideoDbContentType::TVSHOWS)
+ type = MediaTypeTvShow;
+ else if (mediaType == VideoDbContentType::MUSICVIDEOS)
+ type = MediaTypeMusicVideo;
+ else
+ return;
+
+ std::string strSQL = PrepareSQL("DELETE FROM tag_link WHERE tag_id = %i AND media_type = '%s'", idTag, type.c_str());
+ m_pDS->exec(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idTag);
+ }
+}
+
+void CVideoDatabase::GetDetailsFromDB(std::unique_ptr<Dataset> &pDS, int min, int max, const SDbTableOffsets *offsets, CVideoInfoTag &details, int idxOffset)
+{
+ GetDetailsFromDB(pDS->get_sql_record(), min, max, offsets, details, idxOffset);
+}
+
+void CVideoDatabase::GetDetailsFromDB(const dbiplus::sql_record* const record, int min, int max, const SDbTableOffsets *offsets, CVideoInfoTag &details, int idxOffset)
+{
+ for (int i = min + 1; i < max; i++)
+ {
+ switch (offsets[i].type)
+ {
+ case VIDEODB_TYPE_STRING:
+ *(std::string*)(((char*)&details)+offsets[i].offset) = record->at(i+idxOffset).get_asString();
+ break;
+ case VIDEODB_TYPE_INT:
+ case VIDEODB_TYPE_COUNT:
+ *(int*)(((char*)&details)+offsets[i].offset) = record->at(i+idxOffset).get_asInt();
+ break;
+ case VIDEODB_TYPE_BOOL:
+ *(bool*)(((char*)&details)+offsets[i].offset) = record->at(i+idxOffset).get_asBool();
+ break;
+ case VIDEODB_TYPE_FLOAT:
+ *(float*)(((char*)&details)+offsets[i].offset) = record->at(i+idxOffset).get_asFloat();
+ break;
+ case VIDEODB_TYPE_STRINGARRAY:
+ {
+ std::string value = record->at(i+idxOffset).get_asString();
+ if (!value.empty())
+ *(std::vector<std::string>*)(((char*)&details)+offsets[i].offset) = StringUtils::Split(value, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ break;
+ }
+ case VIDEODB_TYPE_DATE:
+ ((CDateTime*)(((char*)&details)+offsets[i].offset))->SetFromDBDate(record->at(i+idxOffset).get_asString());
+ break;
+ case VIDEODB_TYPE_DATETIME:
+ ((CDateTime*)(((char*)&details)+offsets[i].offset))->SetFromDBDateTime(record->at(i+idxOffset).get_asString());
+ break;
+ case VIDEODB_TYPE_UNUSED: // Skip the unused field to avoid populating unused data
+ continue;
+ }
+ }
+}
+
+bool CVideoDatabase::GetDetailsByTypeAndId(CFileItem& item, VideoDbContentType type, int id)
+{
+ CVideoInfoTag details;
+ details.Reset();
+
+ switch (type)
+ {
+ case VideoDbContentType::MOVIES:
+ GetMovieInfo("", details, id);
+ break;
+ case VideoDbContentType::TVSHOWS:
+ GetTvShowInfo("", details, id, &item);
+ break;
+ case VideoDbContentType::EPISODES:
+ GetEpisodeInfo("", details, id);
+ break;
+ case VideoDbContentType::MUSICVIDEOS:
+ GetMusicVideoInfo("", details, id);
+ break;
+ default:
+ return false;
+ }
+
+ item.SetFromVideoInfoTag(details);
+ return true;
+}
+
+CVideoInfoTag CVideoDatabase::GetDetailsByTypeAndId(VideoDbContentType type, int id)
+{
+ CFileItem item;
+ if (GetDetailsByTypeAndId(item, type, id))
+ return CVideoInfoTag(*item.GetVideoInfoTag());
+
+ return {};
+}
+
+bool CVideoDatabase::GetStreamDetails(CFileItem& item)
+{
+ // Note that this function (possibly) creates VideoInfoTags for items that don't have one yet!
+ int fileId = -1;
+
+ if (item.HasVideoInfoTag())
+ fileId = item.GetVideoInfoTag()->m_iFileId;
+
+ if (fileId < 0)
+ fileId = GetFileId(item);
+
+ if (fileId < 0)
+ return false;
+
+ // Have a file id, get stream details if available (creates tag either way)
+ item.GetVideoInfoTag()->m_iFileId = fileId;
+ return GetStreamDetails(*item.GetVideoInfoTag());
+}
+
+bool CVideoDatabase::GetStreamDetails(CVideoInfoTag& tag) const
+{
+ if (tag.m_iFileId < 0)
+ return false;
+
+ bool retVal = false;
+
+ CStreamDetails& details = tag.m_streamDetails;
+ details.Reset();
+
+ std::unique_ptr<Dataset> pDS(m_pDB->CreateDataset());
+ try
+ {
+ std::string strSQL = PrepareSQL("SELECT * FROM streamdetails WHERE idFile = %i", tag.m_iFileId);
+ pDS->query(strSQL);
+
+ while (!pDS->eof())
+ {
+ CStreamDetail::StreamType e = (CStreamDetail::StreamType)pDS->fv(1).get_asInt();
+ switch (e)
+ {
+ case CStreamDetail::VIDEO:
+ {
+ CStreamDetailVideo *p = new CStreamDetailVideo();
+ p->m_strCodec = pDS->fv(2).get_asString();
+ p->m_fAspect = pDS->fv(3).get_asFloat();
+ p->m_iWidth = pDS->fv(4).get_asInt();
+ p->m_iHeight = pDS->fv(5).get_asInt();
+ p->m_iDuration = pDS->fv(10).get_asInt();
+ p->m_strStereoMode = pDS->fv(11).get_asString();
+ p->m_strLanguage = pDS->fv(12).get_asString();
+ p->m_strHdrType = pDS->fv(13).get_asString();
+ details.AddStream(p);
+ retVal = true;
+ break;
+ }
+ case CStreamDetail::AUDIO:
+ {
+ CStreamDetailAudio *p = new CStreamDetailAudio();
+ p->m_strCodec = pDS->fv(6).get_asString();
+ if (pDS->fv(7).get_isNull())
+ p->m_iChannels = -1;
+ else
+ p->m_iChannels = pDS->fv(7).get_asInt();
+ p->m_strLanguage = pDS->fv(8).get_asString();
+ details.AddStream(p);
+ retVal = true;
+ break;
+ }
+ case CStreamDetail::SUBTITLE:
+ {
+ CStreamDetailSubtitle *p = new CStreamDetailSubtitle();
+ p->m_strLanguage = pDS->fv(9).get_asString();
+ details.AddStream(p);
+ retVal = true;
+ break;
+ }
+ }
+
+ pDS->next();
+ }
+
+ pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, tag.m_iFileId);
+ }
+ details.DetermineBestStreams();
+
+ if (details.GetVideoDuration() > 0)
+ tag.SetDuration(details.GetVideoDuration());
+
+ return retVal;
+}
+
+bool CVideoDatabase::GetResumePoint(CVideoInfoTag& tag)
+{
+ if (tag.m_iFileId < 0)
+ return false;
+
+ bool match = false;
+
+ try
+ {
+ if (URIUtils::IsStack(tag.m_strFileNameAndPath) && CFileItem(CStackDirectory::GetFirstStackedFile(tag.m_strFileNameAndPath),false).IsDiscImage())
+ {
+ CStackDirectory dir;
+ CFileItemList fileList;
+ const CURL pathToUrl(tag.m_strFileNameAndPath);
+ dir.GetDirectory(pathToUrl, fileList);
+ tag.SetResumePoint(CBookmark());
+ for (int i = fileList.Size() - 1; i >= 0; i--)
+ {
+ CBookmark bookmark;
+ if (GetResumeBookMark(fileList[i]->GetPath(), bookmark))
+ {
+ bookmark.partNumber = (i+1); /* store part number in here */
+ tag.SetResumePoint(bookmark);
+ match = true;
+ break;
+ }
+ }
+ }
+ else
+ {
+ std::string strSQL=PrepareSQL("select timeInSeconds, totalTimeInSeconds from bookmark where idFile=%i and type=%i order by timeInSeconds", tag.m_iFileId, CBookmark::RESUME);
+ m_pDS2->query( strSQL );
+ if (!m_pDS2->eof())
+ {
+ tag.SetResumePoint(m_pDS2->fv(0).get_asDouble(), m_pDS2->fv(1).get_asDouble(), "");
+ match = true;
+ }
+ m_pDS2->close();
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, tag.m_strFileNameAndPath);
+ }
+
+ return match;
+}
+
+CVideoInfoTag CVideoDatabase::GetDetailsForMovie(std::unique_ptr<Dataset> &pDS, int getDetails /* = VideoDbDetailsNone */)
+{
+ return GetDetailsForMovie(pDS->get_sql_record(), getDetails);
+}
+
+CVideoInfoTag CVideoDatabase::GetDetailsForMovie(const dbiplus::sql_record* const record, int getDetails /* = VideoDbDetailsNone */)
+{
+ CVideoInfoTag details;
+
+ if (record == NULL)
+ return details;
+
+ int idMovie = record->at(0).get_asInt();
+
+ GetDetailsFromDB(record, VIDEODB_ID_MIN, VIDEODB_ID_MAX, DbMovieOffsets, details);
+
+ details.m_iDbId = idMovie;
+ details.m_type = MediaTypeMovie;
+
+ details.m_set.id = record->at(VIDEODB_DETAILS_MOVIE_SET_ID).get_asInt();
+ details.m_set.title = record->at(VIDEODB_DETAILS_MOVIE_SET_NAME).get_asString();
+ details.m_set.overview = record->at(VIDEODB_DETAILS_MOVIE_SET_OVERVIEW).get_asString();
+ details.m_iFileId = record->at(VIDEODB_DETAILS_FILEID).get_asInt();
+ details.m_strPath = record->at(VIDEODB_DETAILS_MOVIE_PATH).get_asString();
+ std::string strFileName = record->at(VIDEODB_DETAILS_MOVIE_FILE).get_asString();
+ ConstructPath(details.m_strFileNameAndPath,details.m_strPath,strFileName);
+ details.SetPlayCount(record->at(VIDEODB_DETAILS_MOVIE_PLAYCOUNT).get_asInt());
+ details.m_lastPlayed.SetFromDBDateTime(record->at(VIDEODB_DETAILS_MOVIE_LASTPLAYED).get_asString());
+ details.m_dateAdded.SetFromDBDateTime(record->at(VIDEODB_DETAILS_MOVIE_DATEADDED).get_asString());
+ details.SetResumePoint(record->at(VIDEODB_DETAILS_MOVIE_RESUME_TIME).get_asInt(),
+ record->at(VIDEODB_DETAILS_MOVIE_TOTAL_TIME).get_asInt(),
+ record->at(VIDEODB_DETAILS_MOVIE_PLAYER_STATE).get_asString());
+ details.m_iUserRating = record->at(VIDEODB_DETAILS_MOVIE_USER_RATING).get_asInt();
+ details.SetRating(record->at(VIDEODB_DETAILS_MOVIE_RATING).get_asFloat(),
+ record->at(VIDEODB_DETAILS_MOVIE_VOTES).get_asInt(),
+ record->at(VIDEODB_DETAILS_MOVIE_RATING_TYPE).get_asString(), true);
+ details.SetUniqueID(record->at(VIDEODB_DETAILS_MOVIE_UNIQUEID_VALUE).get_asString(), record->at(VIDEODB_DETAILS_MOVIE_UNIQUEID_TYPE).get_asString() ,true);
+ std::string premieredString = record->at(VIDEODB_DETAILS_MOVIE_PREMIERED).get_asString();
+ if (premieredString.size() == 4)
+ details.SetYear(record->at(VIDEODB_DETAILS_MOVIE_PREMIERED).get_asInt());
+ else
+ details.SetPremieredFromDBDate(premieredString);
+
+ if (getDetails)
+ {
+ GetCast(details.m_iDbId, MediaTypeMovie, details.m_cast);
+
+ if (getDetails & VideoDbDetailsTag)
+ GetTags(details.m_iDbId, MediaTypeMovie, details.m_tags);
+
+ if (getDetails & VideoDbDetailsRating)
+ GetRatings(details.m_iDbId, MediaTypeMovie, details.m_ratings);
+
+ if (getDetails & VideoDbDetailsUniqueID)
+ GetUniqueIDs(details.m_iDbId, MediaTypeMovie, details);
+
+ if (getDetails & VideoDbDetailsShowLink)
+ {
+ // create tvshowlink string
+ std::vector<int> links;
+ GetLinksToTvShow(idMovie, links);
+ for (unsigned int i = 0; i < links.size(); ++i)
+ {
+ std::string strSQL = PrepareSQL("select c%02d from tvshow where idShow=%i",
+ VIDEODB_ID_TV_TITLE, links[i]);
+ m_pDS2->query(strSQL);
+ if (!m_pDS2->eof())
+ details.m_showLink.emplace_back(m_pDS2->fv(0).get_asString());
+ }
+ m_pDS2->close();
+ }
+
+ if (getDetails & VideoDbDetailsStream)
+ GetStreamDetails(details);
+
+ details.m_parsedDetails = getDetails;
+ }
+ return details;
+}
+
+CVideoInfoTag CVideoDatabase::GetDetailsForTvShow(std::unique_ptr<Dataset> &pDS, int getDetails /* = VideoDbDetailsNone */, CFileItem* item /* = NULL */)
+{
+ return GetDetailsForTvShow(pDS->get_sql_record(), getDetails, item);
+}
+
+CVideoInfoTag CVideoDatabase::GetDetailsForTvShow(const dbiplus::sql_record* const record, int getDetails /* = VideoDbDetailsNone */, CFileItem* item /* = NULL */)
+{
+ CVideoInfoTag details;
+
+ if (record == NULL)
+ return details;
+
+ int idTvShow = record->at(0).get_asInt();
+
+ GetDetailsFromDB(record, VIDEODB_ID_TV_MIN, VIDEODB_ID_TV_MAX, DbTvShowOffsets, details, 1);
+ details.m_bHasPremiered = details.m_premiered.IsValid();
+ details.m_iDbId = idTvShow;
+ details.m_type = MediaTypeTvShow;
+ details.m_strPath = record->at(VIDEODB_DETAILS_TVSHOW_PATH).get_asString();
+ details.m_basePath = details.m_strPath;
+ details.m_parentPathID = record->at(VIDEODB_DETAILS_TVSHOW_PARENTPATHID).get_asInt();
+ details.m_dateAdded.SetFromDBDateTime(record->at(VIDEODB_DETAILS_TVSHOW_DATEADDED).get_asString());
+ details.m_lastPlayed.SetFromDBDateTime(record->at(VIDEODB_DETAILS_TVSHOW_LASTPLAYED).get_asString());
+ details.m_iSeason = record->at(VIDEODB_DETAILS_TVSHOW_NUM_SEASONS).get_asInt();
+ details.m_iEpisode = record->at(VIDEODB_DETAILS_TVSHOW_NUM_EPISODES).get_asInt();
+ details.SetPlayCount(record->at(VIDEODB_DETAILS_TVSHOW_NUM_WATCHED).get_asInt());
+ details.m_strShowTitle = details.m_strTitle;
+ details.m_iUserRating = record->at(VIDEODB_DETAILS_TVSHOW_USER_RATING).get_asInt();
+ details.SetRating(record->at(VIDEODB_DETAILS_TVSHOW_RATING).get_asFloat(),
+ record->at(VIDEODB_DETAILS_TVSHOW_VOTES).get_asInt(),
+ record->at(VIDEODB_DETAILS_TVSHOW_RATING_TYPE).get_asString(), true);
+ details.SetUniqueID(record->at(VIDEODB_DETAILS_TVSHOW_UNIQUEID_VALUE).get_asString(), record->at(VIDEODB_DETAILS_TVSHOW_UNIQUEID_TYPE).get_asString(), true);
+ details.SetDuration(record->at(VIDEODB_DETAILS_TVSHOW_DURATION).get_asInt());
+
+ if (getDetails)
+ {
+ if (getDetails & VideoDbDetailsCast)
+ {
+ GetCast(details.m_iDbId, "tvshow", details.m_cast);
+ }
+
+ if (getDetails & VideoDbDetailsTag)
+ GetTags(details.m_iDbId, MediaTypeTvShow, details.m_tags);
+
+ if (getDetails & VideoDbDetailsRating)
+ GetRatings(details.m_iDbId, MediaTypeTvShow, details.m_ratings);
+
+ if (getDetails & VideoDbDetailsUniqueID)
+ GetUniqueIDs(details.m_iDbId, MediaTypeTvShow, details);
+
+ details.m_parsedDetails = getDetails;
+ }
+
+ if (item != NULL)
+ {
+ item->m_dateTime = details.GetPremiered();
+ item->SetProperty("totalseasons", details.m_iSeason);
+ item->SetProperty("totalepisodes", details.m_iEpisode);
+ item->SetProperty("numepisodes", details.m_iEpisode); // will be changed later to reflect watchmode setting
+ item->SetProperty("watchedepisodes", details.GetPlayCount());
+ item->SetProperty("unwatchedepisodes", details.m_iEpisode - details.GetPlayCount());
+ item->SetProperty("watchedepisodepercent",
+ details.m_iEpisode > 0 ? (details.GetPlayCount() * 100 / details.m_iEpisode)
+ : 0);
+ }
+ details.SetPlayCount((details.m_iEpisode <= details.GetPlayCount()) ? 1 : 0);
+
+ return details;
+}
+
+CVideoInfoTag CVideoDatabase::GetBasicDetailsForEpisode(std::unique_ptr<Dataset> &pDS)
+{
+ return GetBasicDetailsForEpisode(pDS->get_sql_record());
+}
+
+CVideoInfoTag CVideoDatabase::GetBasicDetailsForEpisode(const dbiplus::sql_record* const record)
+{
+ CVideoInfoTag details;
+
+ if (record == nullptr)
+ return details;
+
+ int idEpisode = record->at(0).get_asInt();
+
+ GetDetailsFromDB(record, VIDEODB_ID_EPISODE_MIN, VIDEODB_ID_EPISODE_MAX, DbEpisodeOffsets, details);
+ details.m_iDbId = idEpisode;
+ details.m_type = MediaTypeEpisode;
+ details.m_iFileId = record->at(VIDEODB_DETAILS_FILEID).get_asInt();
+ details.m_iIdShow = record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_ID).get_asInt();
+ details.m_iIdSeason = record->at(VIDEODB_DETAILS_EPISODE_SEASON_ID).get_asInt();
+ details.m_iUserRating = record->at(VIDEODB_DETAILS_EPISODE_USER_RATING).get_asInt();
+
+ return details;
+}
+
+CVideoInfoTag CVideoDatabase::GetDetailsForEpisode(std::unique_ptr<Dataset> &pDS, int getDetails /* = VideoDbDetailsNone */)
+{
+ return GetDetailsForEpisode(pDS->get_sql_record(), getDetails);
+}
+
+CVideoInfoTag CVideoDatabase::GetDetailsForEpisode(const dbiplus::sql_record* const record, int getDetails /* = VideoDbDetailsNone */)
+{
+ CVideoInfoTag details;
+
+ if (record == nullptr)
+ return details;
+
+ details = GetBasicDetailsForEpisode(record);
+
+ details.m_strPath = record->at(VIDEODB_DETAILS_EPISODE_PATH).get_asString();
+ std::string strFileName = record->at(VIDEODB_DETAILS_EPISODE_FILE).get_asString();
+ ConstructPath(details.m_strFileNameAndPath,details.m_strPath,strFileName);
+ details.SetPlayCount(record->at(VIDEODB_DETAILS_EPISODE_PLAYCOUNT).get_asInt());
+ details.m_lastPlayed.SetFromDBDateTime(record->at(VIDEODB_DETAILS_EPISODE_LASTPLAYED).get_asString());
+ details.m_dateAdded.SetFromDBDateTime(record->at(VIDEODB_DETAILS_EPISODE_DATEADDED).get_asString());
+ details.m_strMPAARating = record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_MPAA).get_asString();
+ details.m_strShowTitle = record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_NAME).get_asString();
+ details.m_genre = StringUtils::Split(record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_GENRE).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ details.m_studio = StringUtils::Split(record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_STUDIO).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ details.SetPremieredFromDBDate(record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_AIRED).get_asString());
+
+ details.SetResumePoint(record->at(VIDEODB_DETAILS_EPISODE_RESUME_TIME).get_asInt(),
+ record->at(VIDEODB_DETAILS_EPISODE_TOTAL_TIME).get_asInt(),
+ record->at(VIDEODB_DETAILS_EPISODE_PLAYER_STATE).get_asString());
+
+ details.SetRating(record->at(VIDEODB_DETAILS_EPISODE_RATING).get_asFloat(),
+ record->at(VIDEODB_DETAILS_EPISODE_VOTES).get_asInt(),
+ record->at(VIDEODB_DETAILS_EPISODE_RATING_TYPE).get_asString(), true);
+ details.SetUniqueID(record->at(VIDEODB_DETAILS_EPISODE_UNIQUEID_VALUE).get_asString(), record->at(VIDEODB_DETAILS_EPISODE_UNIQUEID_TYPE).get_asString(), true);
+
+ if (getDetails)
+ {
+ if (getDetails & VideoDbDetailsCast)
+ {
+ GetCast(details.m_iDbId, MediaTypeEpisode, details.m_cast);
+ GetCast(details.m_iIdShow, MediaTypeTvShow, details.m_cast);
+ }
+
+ if (getDetails & VideoDbDetailsRating)
+ GetRatings(details.m_iDbId, MediaTypeEpisode, details.m_ratings);
+
+ if (getDetails & VideoDbDetailsUniqueID)
+ GetUniqueIDs(details.m_iDbId, MediaTypeEpisode, details);
+
+ if (getDetails & VideoDbDetailsBookmark)
+ GetBookMarkForEpisode(details, details.m_EpBookmark);
+
+ if (getDetails & VideoDbDetailsStream)
+ GetStreamDetails(details);
+
+ details.m_parsedDetails = getDetails;
+ }
+ return details;
+}
+
+CVideoInfoTag CVideoDatabase::GetDetailsForMusicVideo(std::unique_ptr<Dataset> &pDS, int getDetails /* = VideoDbDetailsNone */)
+{
+ return GetDetailsForMusicVideo(pDS->get_sql_record(), getDetails);
+}
+
+CVideoInfoTag CVideoDatabase::GetDetailsForMusicVideo(const dbiplus::sql_record* const record, int getDetails /* = VideoDbDetailsNone */)
+{
+ CVideoInfoTag details;
+ CArtist artist;
+
+ if (record == nullptr)
+ return details;
+
+ int idMVideo = record->at(0).get_asInt();
+
+ GetDetailsFromDB(record, VIDEODB_ID_MUSICVIDEO_MIN, VIDEODB_ID_MUSICVIDEO_MAX, DbMusicVideoOffsets, details);
+ details.m_iDbId = idMVideo;
+ details.m_type = MediaTypeMusicVideo;
+
+ details.m_iFileId = record->at(VIDEODB_DETAILS_FILEID).get_asInt();
+ details.m_strPath = record->at(VIDEODB_DETAILS_MUSICVIDEO_PATH).get_asString();
+ std::string strFileName = record->at(VIDEODB_DETAILS_MUSICVIDEO_FILE).get_asString();
+ ConstructPath(details.m_strFileNameAndPath,details.m_strPath,strFileName);
+ details.SetPlayCount(record->at(VIDEODB_DETAILS_MUSICVIDEO_PLAYCOUNT).get_asInt());
+ details.m_lastPlayed.SetFromDBDateTime(record->at(VIDEODB_DETAILS_MUSICVIDEO_LASTPLAYED).get_asString());
+ details.m_dateAdded.SetFromDBDateTime(record->at(VIDEODB_DETAILS_MUSICVIDEO_DATEADDED).get_asString());
+ details.SetResumePoint(record->at(VIDEODB_DETAILS_MUSICVIDEO_RESUME_TIME).get_asInt(),
+ record->at(VIDEODB_DETAILS_MUSICVIDEO_TOTAL_TIME).get_asInt(),
+ record->at(VIDEODB_DETAILS_MUSICVIDEO_PLAYER_STATE).get_asString());
+ details.m_iUserRating = record->at(VIDEODB_DETAILS_MUSICVIDEO_USER_RATING).get_asInt();
+ details.SetUniqueID(record->at(VIDEODB_DETAILS_MUSICVIDEO_UNIQUEID_VALUE).get_asString(),
+ record->at(VIDEODB_DETAILS_MUSICVIDEO_UNIQUEID_TYPE).get_asString(), true);
+ std::string premieredString = record->at(VIDEODB_DETAILS_MUSICVIDEO_PREMIERED).get_asString();
+ if (premieredString.size() == 4)
+ details.SetYear(record->at(VIDEODB_DETAILS_MUSICVIDEO_PREMIERED).get_asInt());
+ else
+ details.SetPremieredFromDBDate(premieredString);
+
+ if (getDetails)
+ {
+ if (getDetails & VideoDbDetailsTag)
+ GetTags(details.m_iDbId, MediaTypeMusicVideo, details.m_tags);
+
+ if (getDetails & VideoDbDetailsUniqueID)
+ GetUniqueIDs(details.m_iDbId, MediaTypeMusicVideo, details);
+
+ if (getDetails & VideoDbDetailsStream)
+ GetStreamDetails(details);
+
+ if (getDetails & VideoDbDetailsAll)
+ {
+ GetCast(details.m_iDbId, "musicvideo", details.m_cast);
+ }
+
+ details.m_parsedDetails = getDetails;
+ }
+ return details;
+}
+
+void CVideoDatabase::GetCast(int media_id, const std::string &media_type, std::vector<SActorInfo> &cast)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS2)
+ return;
+
+ std::string sql = PrepareSQL("SELECT actor.name,"
+ " actor_link.role,"
+ " actor_link.cast_order,"
+ " actor.art_urls,"
+ " art.url "
+ "FROM actor_link"
+ " JOIN actor ON"
+ " actor_link.actor_id=actor.actor_id"
+ " LEFT JOIN art ON"
+ " art.media_id=actor.actor_id AND art.media_type='actor' AND art.type='thumb' "
+ "WHERE actor_link.media_id=%i AND actor_link.media_type='%s'"
+ "ORDER BY actor_link.cast_order", media_id, media_type.c_str());
+ m_pDS2->query(sql);
+ while (!m_pDS2->eof())
+ {
+ SActorInfo info;
+ info.strName = m_pDS2->fv(0).get_asString();
+ info.strRole = m_pDS2->fv(1).get_asString();
+
+ // ignore identical actors (since cast might already be prefilled)
+ if (std::none_of(cast.begin(), cast.end(), [info](const SActorInfo& actor) {
+ return actor.strName == info.strName && actor.strRole == info.strRole;
+ }))
+ {
+ info.order = m_pDS2->fv(2).get_asInt();
+ info.thumbUrl.ParseFromData(m_pDS2->fv(3).get_asString());
+ info.thumb = m_pDS2->fv(4).get_asString();
+ cast.emplace_back(std::move(info));
+ }
+
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({},{}) failed", __FUNCTION__, media_id, media_type);
+ }
+}
+
+void CVideoDatabase::GetTags(int media_id, const std::string &media_type, std::vector<std::string> &tags)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS2)
+ return;
+
+ std::string sql = PrepareSQL("SELECT tag.name FROM tag INNER JOIN tag_link ON tag_link.tag_id = tag.tag_id WHERE tag_link.media_id = %i AND tag_link.media_type = '%s' ORDER BY tag.tag_id", media_id, media_type.c_str());
+ m_pDS2->query(sql);
+ while (!m_pDS2->eof())
+ {
+ tags.emplace_back(m_pDS2->fv(0).get_asString());
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({},{}) failed", __FUNCTION__, media_id, media_type);
+ }
+}
+
+void CVideoDatabase::GetRatings(int media_id, const std::string &media_type, RatingMap &ratings)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS2)
+ return;
+
+ std::string sql = PrepareSQL("SELECT rating.rating_type, rating.rating, rating.votes FROM rating WHERE rating.media_id = %i AND rating.media_type = '%s'", media_id, media_type.c_str());
+ m_pDS2->query(sql);
+ while (!m_pDS2->eof())
+ {
+ ratings[m_pDS2->fv(0).get_asString()] = CRating(m_pDS2->fv(1).get_asFloat(), m_pDS2->fv(2).get_asInt());
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({},{}) failed", __FUNCTION__, media_id, media_type);
+ }
+}
+
+void CVideoDatabase::GetUniqueIDs(int media_id, const std::string &media_type, CVideoInfoTag& details)
+{
+ try
+ {
+ if (!m_pDB)
+ return;
+ if (!m_pDS2)
+ return;
+
+ std::string sql = PrepareSQL("SELECT type, value FROM uniqueid WHERE media_id = %i AND media_type = '%s'", media_id, media_type.c_str());
+ m_pDS2->query(sql);
+ while (!m_pDS2->eof())
+ {
+ details.SetUniqueID(m_pDS2->fv(1).get_asString(), m_pDS2->fv(0).get_asString());
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({},{}) failed", __FUNCTION__, media_id, media_type);
+ }
+}
+
+bool CVideoDatabase::GetVideoSettings(const CFileItem &item, CVideoSettings &settings)
+{
+ return GetVideoSettings(GetFileId(item), settings);
+}
+
+/// \brief GetVideoSettings() obtains any saved video settings for the current file.
+/// \retval Returns true if the settings exist, false otherwise.
+bool CVideoDatabase::GetVideoSettings(const std::string &filePath, CVideoSettings &settings)
+{
+ return GetVideoSettings(GetFileId(filePath), settings);
+}
+
+bool CVideoDatabase::GetVideoSettings(int idFile, CVideoSettings &settings)
+{
+ try
+ {
+ if (idFile < 0) return false;
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL=PrepareSQL("select * from settings where settings.idFile = '%i'", idFile);
+ m_pDS->query( strSQL );
+
+ if (m_pDS->num_rows() > 0)
+ { // get the video settings info
+ settings.m_AudioDelay = m_pDS->fv("AudioDelay").get_asFloat();
+ settings.m_AudioStream = m_pDS->fv("AudioStream").get_asInt();
+ settings.m_Brightness = m_pDS->fv("Brightness").get_asFloat();
+ settings.m_Contrast = m_pDS->fv("Contrast").get_asFloat();
+ settings.m_CustomPixelRatio = m_pDS->fv("PixelRatio").get_asFloat();
+ settings.m_CustomNonLinStretch = m_pDS->fv("NonLinStretch").get_asBool();
+ settings.m_NoiseReduction = m_pDS->fv("NoiseReduction").get_asFloat();
+ settings.m_PostProcess = m_pDS->fv("PostProcess").get_asBool();
+ settings.m_Sharpness = m_pDS->fv("Sharpness").get_asFloat();
+ settings.m_CustomZoomAmount = m_pDS->fv("ZoomAmount").get_asFloat();
+ settings.m_CustomVerticalShift = m_pDS->fv("VerticalShift").get_asFloat();
+ settings.m_Gamma = m_pDS->fv("Gamma").get_asFloat();
+ settings.m_SubtitleDelay = m_pDS->fv("SubtitleDelay").get_asFloat();
+ settings.m_SubtitleOn = m_pDS->fv("SubtitlesOn").get_asBool();
+ settings.m_SubtitleStream = m_pDS->fv("SubtitleStream").get_asInt();
+ settings.m_ViewMode = m_pDS->fv("ViewMode").get_asInt();
+ settings.m_ResumeTime = m_pDS->fv("ResumeTime").get_asInt();
+ settings.m_InterlaceMethod = (EINTERLACEMETHOD)m_pDS->fv("Deinterlace").get_asInt();
+ settings.m_VolumeAmplification = m_pDS->fv("VolumeAmplification").get_asFloat();
+ settings.m_ScalingMethod = (ESCALINGMETHOD)m_pDS->fv("ScalingMethod").get_asInt();
+ settings.m_StereoMode = m_pDS->fv("StereoMode").get_asInt();
+ settings.m_StereoInvert = m_pDS->fv("StereoInvert").get_asBool();
+ settings.m_VideoStream = m_pDS->fv("VideoStream").get_asInt();
+ settings.m_ToneMapMethod =
+ static_cast<ETONEMAPMETHOD>(m_pDS->fv("TonemapMethod").get_asInt());
+ settings.m_ToneMapParam = m_pDS->fv("TonemapParam").get_asFloat();
+ settings.m_Orientation = m_pDS->fv("Orientation").get_asInt();
+ settings.m_CenterMixLevel = m_pDS->fv("CenterMixLevel").get_asInt();
+ m_pDS->close();
+ return true;
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+void CVideoDatabase::SetVideoSettings(const CFileItem &item, const CVideoSettings &settings)
+{
+ int idFile = AddFile(item);
+ SetVideoSettings(idFile, settings);
+}
+
+/// \brief Sets the settings for a particular video file
+void CVideoDatabase::SetVideoSettings(int idFile, const CVideoSettings &setting)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+ if (idFile < 0)
+ return;
+ std::string strSQL = PrepareSQL("select * from settings where idFile=%i", idFile);
+ m_pDS->query( strSQL );
+ if (m_pDS->num_rows() > 0)
+ {
+ m_pDS->close();
+ // update the item
+ strSQL = PrepareSQL(
+ "update settings set "
+ "Deinterlace=%i,ViewMode=%i,ZoomAmount=%f,PixelRatio=%f,VerticalShift=%f,"
+ "AudioStream=%i,SubtitleStream=%i,SubtitleDelay=%f,SubtitlesOn=%i,Brightness=%f,Contrast="
+ "%f,Gamma=%f,"
+ "VolumeAmplification=%f,AudioDelay=%f,Sharpness=%f,NoiseReduction=%f,NonLinStretch=%i,"
+ "PostProcess=%i,ScalingMethod=%i,",
+ setting.m_InterlaceMethod, setting.m_ViewMode,
+ static_cast<double>(setting.m_CustomZoomAmount),
+ static_cast<double>(setting.m_CustomPixelRatio),
+ static_cast<double>(setting.m_CustomVerticalShift), setting.m_AudioStream,
+ setting.m_SubtitleStream, static_cast<double>(setting.m_SubtitleDelay),
+ setting.m_SubtitleOn, static_cast<double>(setting.m_Brightness),
+ static_cast<double>(setting.m_Contrast), static_cast<double>(setting.m_Gamma),
+ static_cast<double>(setting.m_VolumeAmplification),
+ static_cast<double>(setting.m_AudioDelay), static_cast<double>(setting.m_Sharpness),
+ static_cast<double>(setting.m_NoiseReduction), setting.m_CustomNonLinStretch,
+ setting.m_PostProcess, setting.m_ScalingMethod);
+ std::string strSQL2;
+
+ strSQL2 = PrepareSQL("ResumeTime=%i,StereoMode=%i,StereoInvert=%i,VideoStream=%i,"
+ "TonemapMethod=%i,TonemapParam=%f where idFile=%i\n",
+ setting.m_ResumeTime, setting.m_StereoMode, setting.m_StereoInvert,
+ setting.m_VideoStream, setting.m_ToneMapMethod,
+ static_cast<double>(setting.m_ToneMapParam), idFile);
+ strSQL += strSQL2;
+ m_pDS->exec(strSQL);
+ return ;
+ }
+ else
+ { // add the items
+ m_pDS->close();
+ strSQL= "INSERT INTO settings (idFile,Deinterlace,ViewMode,ZoomAmount,PixelRatio, VerticalShift, "
+ "AudioStream,SubtitleStream,SubtitleDelay,SubtitlesOn,Brightness,"
+ "Contrast,Gamma,VolumeAmplification,AudioDelay,"
+ "ResumeTime,"
+ "Sharpness,NoiseReduction,NonLinStretch,PostProcess,ScalingMethod,StereoMode,StereoInvert,VideoStream,TonemapMethod,TonemapParam,Orientation,CenterMixLevel) "
+ "VALUES ";
+ strSQL += PrepareSQL(
+ "(%i,%i,%i,%f,%f,%f,%i,%i,%f,%i,%f,%f,%f,%f,%f,%i,%f,%f,%i,%i,%i,%i,%i,%i,%i,%f,%i,%i)",
+ idFile, setting.m_InterlaceMethod, setting.m_ViewMode,
+ static_cast<double>(setting.m_CustomZoomAmount),
+ static_cast<double>(setting.m_CustomPixelRatio),
+ static_cast<double>(setting.m_CustomVerticalShift), setting.m_AudioStream,
+ setting.m_SubtitleStream, static_cast<double>(setting.m_SubtitleDelay),
+ setting.m_SubtitleOn, static_cast<double>(setting.m_Brightness),
+ static_cast<double>(setting.m_Contrast), static_cast<double>(setting.m_Gamma),
+ static_cast<double>(setting.m_VolumeAmplification),
+ static_cast<double>(setting.m_AudioDelay), setting.m_ResumeTime,
+ static_cast<double>(setting.m_Sharpness), static_cast<double>(setting.m_NoiseReduction),
+ setting.m_CustomNonLinStretch, setting.m_PostProcess, setting.m_ScalingMethod,
+ setting.m_StereoMode, setting.m_StereoInvert, setting.m_VideoStream,
+ setting.m_ToneMapMethod, static_cast<double>(setting.m_ToneMapParam),
+ setting.m_Orientation, setting.m_CenterMixLevel);
+ m_pDS->exec(strSQL);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idFile);
+ }
+}
+
+void CVideoDatabase::SetArtForItem(int mediaId, const MediaType &mediaType, const std::map<std::string, std::string> &art)
+{
+ for (const auto &i : art)
+ SetArtForItem(mediaId, mediaType, i.first, i.second);
+}
+
+void CVideoDatabase::SetArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType, const std::string &url)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ // don't set <foo>.<bar> art types - these are derivative types from parent items
+ if (artType.find('.') != std::string::npos)
+ return;
+
+ std::string sql = PrepareSQL("SELECT art_id,url FROM art WHERE media_id=%i AND media_type='%s' AND type='%s'", mediaId, mediaType.c_str(), artType.c_str());
+ m_pDS->query(sql);
+ if (!m_pDS->eof())
+ { // update
+ int artId = m_pDS->fv(0).get_asInt();
+ std::string oldUrl = m_pDS->fv(1).get_asString();
+ m_pDS->close();
+ if (oldUrl != url)
+ {
+ sql = PrepareSQL("UPDATE art SET url='%s' where art_id=%d", url.c_str(), artId);
+ m_pDS->exec(sql);
+ }
+ }
+ else
+ { // insert
+ m_pDS->close();
+ sql = PrepareSQL("INSERT INTO art(media_id, media_type, type, url) VALUES (%d, '%s', '%s', '%s')", mediaId, mediaType.c_str(), artType.c_str(), url.c_str());
+ m_pDS->exec(sql);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}, '{}', '{}', '{}') failed", __FUNCTION__, mediaId, mediaType,
+ artType, url);
+ }
+}
+
+bool CVideoDatabase::GetArtForItem(int mediaId, const MediaType &mediaType, std::map<std::string, std::string> &art)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false; // using dataset 2 as we're likely called in loops on dataset 1
+
+ std::string sql = PrepareSQL("SELECT type,url FROM art WHERE media_id=%i AND media_type='%s'", mediaId, mediaType.c_str());
+ m_pDS2->query(sql);
+ while (!m_pDS2->eof())
+ {
+ art.insert(make_pair(m_pDS2->fv(0).get_asString(), m_pDS2->fv(1).get_asString()));
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ return !art.empty();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaId);
+ }
+ return false;
+}
+
+std::string CVideoDatabase::GetArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType)
+{
+ std::string query = PrepareSQL("SELECT url FROM art WHERE media_id=%i AND media_type='%s' AND type='%s'", mediaId, mediaType.c_str(), artType.c_str());
+ return GetSingleValue(query, m_pDS2);
+}
+
+bool CVideoDatabase::RemoveArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType)
+{
+ return ExecuteQuery(PrepareSQL("DELETE FROM art WHERE media_id=%i AND media_type='%s' AND type='%s'", mediaId, mediaType.c_str(), artType.c_str()));
+}
+
+bool CVideoDatabase::RemoveArtForItem(int mediaId, const MediaType &mediaType, const std::set<std::string> &artTypes)
+{
+ bool result = true;
+ for (const auto &i : artTypes)
+ result &= RemoveArtForItem(mediaId, mediaType, i);
+
+ return result;
+}
+
+bool CVideoDatabase::HasArtForItem(int mediaId, const MediaType &mediaType)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false; // using dataset 2 as we're likely called in loops on dataset 1
+
+ std::string sql = PrepareSQL("SELECT 1 FROM art WHERE media_id=%i AND media_type='%s' LIMIT 1", mediaId, mediaType.c_str());
+ m_pDS2->query(sql);
+ bool result = !m_pDS2->eof();
+ m_pDS2->close();
+ return result;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaId);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetTvShowSeasons(int showId, std::map<int, int> &seasons)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false; // using dataset 2 as we're likely called in loops on dataset 1
+
+ // get all seasons for this show
+ std::string sql = PrepareSQL("select idSeason,season from seasons where idShow=%i", showId);
+ m_pDS2->query(sql);
+
+ seasons.clear();
+ while (!m_pDS2->eof())
+ {
+ seasons.insert(std::make_pair(m_pDS2->fv(1).get_asInt(), m_pDS2->fv(0).get_asInt()));
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, showId);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetTvShowNamedSeasons(int showId, std::map<int, std::string> &seasons)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false; // using dataset 2 as we're likely called in loops on dataset 1
+
+ // get all named seasons for this show
+ std::string sql = PrepareSQL("select season, name from seasons where season > 0 and name is not null and name <> '' and idShow = %i", showId);
+ m_pDS2->query(sql);
+
+ seasons.clear();
+ while (!m_pDS2->eof())
+ {
+ seasons.insert(std::make_pair(m_pDS2->fv(0).get_asInt(), m_pDS2->fv(1).get_asString()));
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, showId);
+ }
+ return false;
+}
+
+std::string CVideoDatabase::GetTvShowNamedSeasonById(int tvshowId, int seasonId)
+{
+ return GetSingleValue("seasons", "name",
+ PrepareSQL("season=%i AND idShow=%i", seasonId, tvshowId));
+}
+
+bool CVideoDatabase::GetTvShowSeasonArt(int showId, std::map<int, std::map<std::string, std::string> > &seasonArt)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false; // using dataset 2 as we're likely called in loops on dataset 1
+
+ std::map<int, int> seasons;
+ GetTvShowSeasons(showId, seasons);
+
+ for (const auto &i : seasons)
+ {
+ std::map<std::string, std::string> art;
+ GetArtForItem(i.second, MediaTypeSeason, art);
+ seasonArt.insert(std::make_pair(i.first,art));
+ }
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, showId);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetArtTypes(const MediaType &mediaType, std::vector<std::string> &artTypes)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL("SELECT DISTINCT type FROM art WHERE media_type='%s'", mediaType.c_str());
+ int numRows = RunQuery(sql);
+ if (numRows <= 0)
+ return numRows == 0;
+
+ while (!m_pDS->eof())
+ {
+ artTypes.emplace_back(m_pDS->fv(0).get_asString());
+ m_pDS->next();
+ }
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, mediaType);
+ }
+ return false;
+}
+
+namespace
+{
+std::vector<std::string> GetBasicItemAvailableArtTypes(int mediaId,
+ VideoDbContentType dbType,
+ CVideoDatabase& db)
+{
+ std::vector<std::string> result;
+ CVideoInfoTag tag = db.GetDetailsByTypeAndId(dbType, mediaId);
+
+ //! @todo artwork: fanart stored separately, doesn't need to be
+ tag.m_fanart.Unpack();
+ if (tag.m_fanart.GetNumFanarts() && std::find(result.cbegin(), result.cend(), "fanart") == result.cend())
+ result.emplace_back("fanart");
+
+ // all other images
+ tag.m_strPictureURL.Parse();
+ for (const auto& urlEntry : tag.m_strPictureURL.GetUrls())
+ {
+ std::string artType = urlEntry.m_aspect;
+ if (artType.empty())
+ artType = tag.m_type == MediaTypeEpisode ? "thumb" : "poster";
+ if (urlEntry.m_type == CScraperUrl::UrlType::General && // exclude season artwork for TV shows
+ !StringUtils::StartsWith(artType, "set.") && // exclude movie set artwork for movies
+ std::find(result.cbegin(), result.cend(), artType) == result.cend())
+ {
+ result.push_back(artType);
+ }
+ }
+ return result;
+}
+
+std::vector<std::string> GetSeasonAvailableArtTypes(int mediaId, CVideoDatabase& db)
+{
+ CVideoInfoTag tag;
+ db.GetSeasonInfo(mediaId, tag);
+
+ std::vector<std::string> result;
+
+ CVideoInfoTag sourceShow;
+ db.GetTvShowInfo("", sourceShow, tag.m_iIdShow);
+ sourceShow.m_strPictureURL.Parse();
+ for (const auto& urlEntry : sourceShow.m_strPictureURL.GetUrls())
+ {
+ std::string artType = urlEntry.m_aspect;
+ if (artType.empty())
+ artType = "poster";
+ if (urlEntry.m_type == CScraperUrl::UrlType::Season && urlEntry.m_season == tag.m_iSeason &&
+ std::find(result.cbegin(), result.cend(), artType) == result.cend())
+ {
+ result.push_back(artType);
+ }
+ }
+ return result;
+}
+
+std::vector<std::string> GetMovieSetAvailableArtTypes(int mediaId, CVideoDatabase& db)
+{
+ std::vector<std::string> result;
+ CFileItemList items;
+ std::string baseDir = StringUtils::Format("videodb://movies/sets/{}", mediaId);
+ if (db.GetMoviesNav(baseDir, items))
+ {
+ for (const auto& item : items)
+ {
+ CVideoInfoTag* pTag = item->GetVideoInfoTag();
+ pTag->m_strPictureURL.Parse();
+
+ for (const auto& urlEntry : pTag->m_strPictureURL.GetUrls())
+ {
+ if (!StringUtils::StartsWith(urlEntry.m_aspect, "set."))
+ continue;
+
+ std::string artType = urlEntry.m_aspect.substr(4);
+ if (std::find(result.cbegin(), result.cend(), artType) == result.cend())
+ result.push_back(artType);
+ }
+ }
+ }
+ return result;
+}
+
+std::vector<CScraperUrl::SUrlEntry> GetBasicItemAvailableArt(int mediaId,
+ VideoDbContentType dbType,
+ const std::string& artType,
+ CVideoDatabase& db)
+{
+ std::vector<CScraperUrl::SUrlEntry> result;
+ CVideoInfoTag tag = db.GetDetailsByTypeAndId(dbType, mediaId);
+
+ if (artType.empty() || artType == "fanart")
+ {
+ tag.m_fanart.Unpack();
+ for (unsigned int i = 0; i < tag.m_fanart.GetNumFanarts(); i++)
+ {
+ CScraperUrl::SUrlEntry url(tag.m_fanart.GetImageURL(i));
+ url.m_preview = tag.m_fanart.GetPreviewURL(i);
+ url.m_aspect = "fanart";
+ result.push_back(url);
+ }
+ }
+ tag.m_strPictureURL.Parse();
+ for (auto urlEntry : tag.m_strPictureURL.GetUrls())
+ {
+ if (urlEntry.m_aspect.empty())
+ urlEntry.m_aspect = tag.m_type == MediaTypeEpisode ? "thumb" : "poster";
+ if ((urlEntry.m_aspect == artType ||
+ (artType.empty() && !StringUtils::StartsWith(urlEntry.m_aspect, "set."))) &&
+ urlEntry.m_type == CScraperUrl::UrlType::General)
+ {
+ result.push_back(urlEntry);
+ }
+ }
+
+ return result;
+}
+
+std::vector<CScraperUrl::SUrlEntry> GetSeasonAvailableArt(
+ int mediaId, const std::string& artType, CVideoDatabase& db)
+{
+ CVideoInfoTag tag;
+ db.GetSeasonInfo(mediaId, tag);
+
+ std::vector<CScraperUrl::SUrlEntry> result;
+
+ CVideoInfoTag sourceShow;
+ db.GetTvShowInfo("", sourceShow, tag.m_iIdShow);
+ sourceShow.m_strPictureURL.Parse();
+ for (auto urlEntry : sourceShow.m_strPictureURL.GetUrls())
+ {
+ if (urlEntry.m_aspect.empty())
+ urlEntry.m_aspect = "poster";
+ if ((artType.empty() || urlEntry.m_aspect == artType) &&
+ urlEntry.m_type == CScraperUrl::UrlType::Season &&
+ urlEntry.m_season == tag.m_iSeason)
+ {
+ result.push_back(urlEntry);
+ }
+ }
+ return result;
+}
+
+std::vector<CScraperUrl::SUrlEntry> GetMovieSetAvailableArt(
+ int mediaId, const std::string& artType, CVideoDatabase& db)
+{
+ std::vector<CScraperUrl::SUrlEntry> result;
+ CFileItemList items;
+ std::string baseDir = StringUtils::Format("videodb://movies/sets/{}", mediaId);
+ std::unordered_set<std::string> addedURLs;
+ if (db.GetMoviesNav(baseDir, items))
+ {
+ for (const auto& item : items)
+ {
+ CVideoInfoTag* pTag = item->GetVideoInfoTag();
+ pTag->m_strPictureURL.Parse();
+
+ for (auto urlEntry : pTag->m_strPictureURL.GetUrls())
+ {
+ bool isSetArt = !artType.empty() ? urlEntry.m_aspect == "set." + artType :
+ StringUtils::StartsWith(urlEntry.m_aspect, "set.");
+ if (isSetArt && addedURLs.insert(urlEntry.m_url).second)
+ {
+ urlEntry.m_aspect = urlEntry.m_aspect.substr(4);
+ result.push_back(urlEntry);
+ }
+ }
+ }
+ }
+ return result;
+}
+
+VideoDbContentType CovertMediaTypeToContentType(const MediaType& mediaType)
+{
+ VideoDbContentType dbType{VideoDbContentType::UNKNOWN};
+ if (mediaType == MediaTypeTvShow)
+ dbType = VideoDbContentType::TVSHOWS;
+ else if (mediaType == MediaTypeMovie)
+ dbType = VideoDbContentType::MOVIES;
+ else if (mediaType == MediaTypeEpisode)
+ dbType = VideoDbContentType::EPISODES;
+ else if (mediaType == MediaTypeMusicVideo)
+ dbType = VideoDbContentType::MUSICVIDEOS;
+
+ return dbType;
+}
+} // namespace
+
+std::vector<CScraperUrl::SUrlEntry> CVideoDatabase::GetAvailableArtForItem(
+ int mediaId, const MediaType& mediaType, const std::string& artType)
+{
+ VideoDbContentType dbType = CovertMediaTypeToContentType(mediaType);
+
+ if (dbType != VideoDbContentType::UNKNOWN)
+ return GetBasicItemAvailableArt(mediaId, dbType, artType, *this);
+ if (mediaType == MediaTypeSeason)
+ return GetSeasonAvailableArt(mediaId, artType, *this);
+ if (mediaType == MediaTypeVideoCollection)
+ return GetMovieSetAvailableArt(mediaId, artType, *this);
+ return {};
+}
+
+std::vector<std::string> CVideoDatabase::GetAvailableArtTypesForItem(int mediaId,
+ const MediaType& mediaType)
+{
+ VideoDbContentType dbType = CovertMediaTypeToContentType(mediaType);
+
+ if (dbType != VideoDbContentType::UNKNOWN)
+ return GetBasicItemAvailableArtTypes(mediaId, dbType, *this);
+ if (mediaType == MediaTypeSeason)
+ return GetSeasonAvailableArtTypes(mediaId, *this);
+ if (mediaType == MediaTypeVideoCollection)
+ return GetMovieSetAvailableArtTypes(mediaId, *this);
+ return {};
+}
+
+/// \brief GetStackTimes() obtains any saved video times for the stacked file
+/// \retval Returns true if the stack times exist, false otherwise.
+bool CVideoDatabase::GetStackTimes(const std::string &filePath, std::vector<uint64_t> &times)
+{
+ try
+ {
+ // obtain the FileID (if it exists)
+ int idFile = GetFileId(filePath);
+ if (idFile < 0) return false;
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+ // ok, now obtain the settings for this file
+ std::string strSQL=PrepareSQL("select times from stacktimes where idFile=%i\n", idFile);
+ m_pDS->query( strSQL );
+ if (m_pDS->num_rows() > 0)
+ { // get the video settings info
+ uint64_t timeTotal = 0;
+ std::vector<std::string> timeString = StringUtils::Split(m_pDS->fv("times").get_asString(), ",");
+ times.clear();
+ for (const auto &i : timeString)
+ {
+ uint64_t partTime = static_cast<uint64_t>(atof(i.c_str()) * 1000.0);
+ times.push_back(partTime); // db stores in secs, convert to msecs
+ timeTotal += partTime;
+ }
+ m_pDS->close();
+ return (timeTotal > 0);
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+/// \brief Sets the stack times for a particular video file
+void CVideoDatabase::SetStackTimes(const std::string& filePath, const std::vector<uint64_t> &times)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+ int idFile = AddFile(filePath);
+ if (idFile < 0)
+ return;
+
+ // delete any existing items
+ m_pDS->exec( PrepareSQL("delete from stacktimes where idFile=%i", idFile) );
+
+ // add the items
+ std::string timeString = StringUtils::Format("{:.3f}", times[0] / 1000.0f);
+ for (unsigned int i = 1; i < times.size(); i++)
+ timeString += StringUtils::Format(",{:.3f}", times[i] / 1000.0f);
+
+ m_pDS->exec( PrepareSQL("insert into stacktimes (idFile,times) values (%i,'%s')\n", idFile, timeString.c_str()) );
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
+ }
+}
+
+void CVideoDatabase::RemoveContentForPath(const std::string& strPath, CGUIDialogProgress *progress /* = NULL */)
+{
+ if(URIUtils::IsMultiPath(strPath))
+ {
+ std::vector<std::string> paths;
+ CMultiPathDirectory::GetPaths(strPath, paths);
+
+ for(unsigned i=0;i<paths.size();i++)
+ RemoveContentForPath(paths[i], progress);
+ }
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (progress)
+ {
+ progress->SetHeading(CVariant{700});
+ progress->SetLine(0, CVariant{""});
+ progress->SetLine(1, CVariant{313});
+ progress->SetLine(2, CVariant{330});
+ progress->SetPercentage(0);
+ progress->Open();
+ progress->ShowProgressBar(true);
+ }
+ std::vector<std::pair<int, std::string> > paths;
+ GetSubPaths(strPath, paths);
+ int iCurr = 0;
+ for (const auto &i : paths)
+ {
+ bool bMvidsChecked=false;
+ if (progress)
+ {
+ progress->SetPercentage((int)((float)(iCurr++)/paths.size()*100.f));
+ progress->Progress();
+ }
+
+ const auto tvshowId = GetTvShowId(i.second);
+ if (tvshowId > 0)
+ DeleteTvShow(tvshowId);
+ else
+ {
+ std::string strSQL = PrepareSQL("select files.strFilename from files join movie on movie.idFile=files.idFile where files.idPath=%i", i.first);
+ m_pDS2->query(strSQL);
+ if (m_pDS2->eof())
+ {
+ strSQL = PrepareSQL("select files.strFilename from files join musicvideo on musicvideo.idFile=files.idFile where files.idPath=%i", i.first);
+ m_pDS2->query(strSQL);
+ bMvidsChecked = true;
+ }
+ while (!m_pDS2->eof())
+ {
+ std::string strMoviePath;
+ std::string strFileName = m_pDS2->fv("files.strFilename").get_asString();
+ ConstructPath(strMoviePath, i.second, strFileName);
+ const auto movieId = GetMovieId(strMoviePath);
+ if (movieId > 0)
+ DeleteMovie(movieId);
+ else
+ {
+ const auto musicvideoId = GetMusicVideoId(strMoviePath);
+ if (musicvideoId > 0)
+ DeleteMusicVideo(musicvideoId);
+ }
+ m_pDS2->next();
+ if (m_pDS2->eof() && !bMvidsChecked)
+ {
+ strSQL =PrepareSQL("select files.strFilename from files join musicvideo on musicvideo.idFile=files.idFile where files.idPath=%i", i.first);
+ m_pDS2->query(strSQL);
+ bMvidsChecked = true;
+ }
+ }
+ m_pDS2->close();
+ m_pDS2->exec(PrepareSQL("update path set strContent='', strScraper='', strHash='',strSettings='',useFolderNames=0,scanRecursive=0 where idPath=%i", i.first));
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strPath);
+ }
+ if (progress)
+ progress->Close();
+}
+
+void CVideoDatabase::SetScraperForPath(const std::string& filePath, const ScraperPtr& scraper, const VIDEO::SScanSettings& settings)
+{
+ // if we have a multipath, set scraper for all contained paths
+ if(URIUtils::IsMultiPath(filePath))
+ {
+ std::vector<std::string> paths;
+ CMultiPathDirectory::GetPaths(filePath, paths);
+
+ for(unsigned i=0;i<paths.size();i++)
+ SetScraperForPath(paths[i],scraper,settings);
+
+ return;
+ }
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ int idPath = AddPath(filePath);
+ if (idPath < 0)
+ return;
+
+ // Update
+ std::string strSQL;
+ if (settings.exclude)
+ { //NB See note in ::GetScraperForPath about strContent=='none'
+ strSQL = PrepareSQL(
+ "UPDATE path SET strContent='', strScraper='', scanRecursive=0, useFolderNames=0, "
+ "strSettings='', noUpdate=0, exclude=1, allAudio=%i WHERE idPath=%i",
+ settings.m_allExtAudio, idPath);
+ }
+ else if(!scraper)
+ { // catch clearing content, but not excluding
+ strSQL = PrepareSQL(
+ "UPDATE path SET strContent='', strScraper='', scanRecursive=0, useFolderNames=0, "
+ "strSettings='', noUpdate=0, exclude=0, allAudio=%i WHERE idPath=%i",
+ settings.m_allExtAudio, idPath);
+ }
+ else
+ {
+ std::string content = TranslateContent(scraper->Content());
+ strSQL = PrepareSQL(
+ "UPDATE path SET strContent='%s', strScraper='%s', scanRecursive=%i, useFolderNames=%i, "
+ "strSettings='%s', noUpdate=%i, exclude=0, allAudio=%i WHERE idPath=%i",
+ content.c_str(), scraper->ID().c_str(), settings.recurse, settings.parent_name,
+ scraper->GetPathSettings().c_str(), settings.noupdate, settings.m_allExtAudio, idPath);
+ }
+ m_pDS->exec(strSQL);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, filePath);
+ }
+}
+
+bool CVideoDatabase::ScraperInUse(const std::string &scraperID) const
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL("select count(1) from path where strScraper='%s'", scraperID.c_str());
+ if (!m_pDS->query(sql) || m_pDS->num_rows() == 0)
+ return false;
+ bool found = m_pDS->fv(0).get_asInt() > 0;
+ m_pDS->close();
+ return found;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}({}) failed", __FUNCTION__, scraperID);
+ }
+ return false;
+}
+
+class CArtItem
+{
+public:
+ CArtItem() { art_id = 0; media_id = 0; };
+ int art_id;
+ std::string art_type;
+ std::string art_url;
+ int media_id;
+ std::string media_type;
+};
+
+// used for database update to v83
+class CShowItem
+{
+public:
+ bool operator==(const CShowItem &r) const
+ {
+ return (!ident.empty() && ident == r.ident) || (title == r.title && year == r.year);
+ };
+ int id;
+ int path;
+ std::string title;
+ std::string year;
+ std::string ident;
+};
+
+// used for database update to v84
+class CShowLink
+{
+public:
+ int show;
+ int pathId;
+ std::string path;
+};
+
+void CVideoDatabase::UpdateTables(int iVersion)
+{
+ // Important: DO NOT use CREATE TABLE [...] AS SELECT [...] - it does not work on MySQL with GTID consistency enforced
+
+ if (iVersion < 76)
+ {
+ m_pDS->exec("ALTER TABLE settings ADD StereoMode integer");
+ m_pDS->exec("ALTER TABLE settings ADD StereoInvert bool");
+ }
+ if (iVersion < 77)
+ m_pDS->exec("ALTER TABLE streamdetails ADD strStereoMode text");
+
+ if (iVersion < 81)
+ { // add idParentPath to path table
+ m_pDS->exec("ALTER TABLE path ADD idParentPath integer");
+ std::map<std::string, int> paths;
+ m_pDS->query("select idPath,strPath from path");
+ while (!m_pDS->eof())
+ {
+ paths.insert(make_pair(m_pDS->fv(1).get_asString(), m_pDS->fv(0).get_asInt()));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ // run through these paths figuring out the parent path, and add to the table if found
+ for (const auto &i : paths)
+ {
+ std::string parent = URIUtils::GetParentPath(i.first);
+ auto j = paths.find(parent);
+ if (j != paths.end())
+ m_pDS->exec(PrepareSQL("UPDATE path SET idParentPath=%i WHERE idPath=%i", j->second, i.second));
+ }
+ }
+ if (iVersion < 82)
+ {
+ // drop parent path id and basePath from tvshow table
+ m_pDS->exec("UPDATE tvshow SET c16=NULL,c17=NULL");
+ }
+ if (iVersion < 83)
+ {
+ // drop duplicates in tvshow table, and update tvshowlinkpath accordingly
+ std::string sql = PrepareSQL("SELECT tvshow.idShow,idPath,c%02d,c%02d,c%02d FROM tvshow JOIN tvshowlinkpath ON tvshow.idShow = tvshowlinkpath.idShow", VIDEODB_ID_TV_TITLE, VIDEODB_ID_TV_PREMIERED, VIDEODB_ID_TV_IDENT_ID);
+ m_pDS->query(sql);
+ std::vector<CShowItem> shows;
+ while (!m_pDS->eof())
+ {
+ CShowItem show;
+ show.id = m_pDS->fv(0).get_asInt();
+ show.path = m_pDS->fv(1).get_asInt();
+ show.title = m_pDS->fv(2).get_asString();
+ show.year = m_pDS->fv(3).get_asString();
+ show.ident = m_pDS->fv(4).get_asString();
+ shows.emplace_back(std::move(show));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ if (!shows.empty())
+ {
+ for (auto i = shows.begin() + 1; i != shows.end(); ++i)
+ {
+ // has this show been found before?
+ auto j = find(shows.begin(), i, *i);
+ if (j != i)
+ { // this is a duplicate
+ // update the tvshowlinkpath table
+ m_pDS->exec(PrepareSQL("UPDATE tvshowlinkpath SET idShow = %d WHERE idShow = %d AND idPath = %d", j->id, i->id, i->path));
+ // update episodes, seasons, movie links
+ m_pDS->exec(PrepareSQL("UPDATE episode SET idShow = %d WHERE idShow = %d", j->id, i->id));
+ m_pDS->exec(PrepareSQL("UPDATE seasons SET idShow = %d WHERE idShow = %d", j->id, i->id));
+ m_pDS->exec(PrepareSQL("UPDATE movielinktvshow SET idShow = %d WHERE idShow = %d", j->id, i->id));
+ // delete tvshow
+ m_pDS->exec(PrepareSQL("DELETE FROM genrelinktvshow WHERE idShow=%i", i->id));
+ m_pDS->exec(PrepareSQL("DELETE FROM actorlinktvshow WHERE idShow=%i", i->id));
+ m_pDS->exec(PrepareSQL("DELETE FROM directorlinktvshow WHERE idShow=%i", i->id));
+ m_pDS->exec(PrepareSQL("DELETE FROM studiolinktvshow WHERE idShow=%i", i->id));
+ m_pDS->exec(PrepareSQL("DELETE FROM tvshow WHERE idShow = %d", i->id));
+ }
+ }
+ // cleanup duplicate seasons
+ m_pDS->exec("DELETE FROM seasons WHERE idSeason NOT IN (SELECT idSeason FROM (SELECT min(idSeason) as idSeason FROM seasons GROUP BY idShow,season) AS sub)");
+ }
+ }
+ if (iVersion < 84)
+ { // replace any multipaths in tvshowlinkpath table
+ m_pDS->query("SELECT idShow, tvshowlinkpath.idPath, strPath FROM tvshowlinkpath JOIN path ON tvshowlinkpath.idPath=path.idPath WHERE path.strPath LIKE 'multipath://%'");
+ std::vector<CShowLink> shows;
+ while (!m_pDS->eof())
+ {
+ CShowLink link;
+ link.show = m_pDS->fv(0).get_asInt();
+ link.pathId = m_pDS->fv(1).get_asInt();
+ link.path = m_pDS->fv(2).get_asString();
+ shows.emplace_back(std::move(link));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ // update these
+ for (auto i = shows.begin(); i != shows.end(); ++i)
+ {
+ std::vector<std::string> paths;
+ CMultiPathDirectory::GetPaths(i->path, paths);
+ for (auto j = paths.begin(); j != paths.end(); ++j)
+ {
+ int idPath = AddPath(*j, URIUtils::GetParentPath(*j));
+ /* we can't rely on REPLACE INTO here as analytics (indices) aren't online yet */
+ if (GetSingleValue(PrepareSQL("SELECT 1 FROM tvshowlinkpath WHERE idShow=%i AND idPath=%i", i->show, idPath)).empty())
+ m_pDS->exec(PrepareSQL("INSERT INTO tvshowlinkpath(idShow, idPath) VALUES(%i,%i)", i->show, idPath));
+ }
+ m_pDS->exec(PrepareSQL("DELETE FROM tvshowlinkpath WHERE idShow=%i AND idPath=%i", i->show, i->pathId));
+ }
+ }
+ if (iVersion < 85)
+ {
+ // drop multipaths from the path table - they're not needed for anything at all
+ m_pDS->exec("DELETE FROM path WHERE strPath LIKE 'multipath://%'");
+ }
+ if (iVersion < 87)
+ { // due to the tvshow merging above, there could be orphaned season or show art
+ m_pDS->exec("DELETE from art WHERE media_type='tvshow' AND NOT EXISTS (SELECT 1 FROM tvshow WHERE tvshow.idShow = art.media_id)");
+ m_pDS->exec("DELETE from art WHERE media_type='season' AND NOT EXISTS (SELECT 1 FROM seasons WHERE seasons.idSeason = art.media_id)");
+ }
+ if (iVersion < 91)
+ {
+ // create actor link table
+ m_pDS->exec("CREATE TABLE actor_link(actor_id INT, media_id INT, media_type TEXT, role TEXT, cast_order INT)");
+ m_pDS->exec("INSERT INTO actor_link(actor_id, media_id, media_type, role, cast_order) SELECT DISTINCT idActor, idMovie, 'movie', strRole, iOrder from actorlinkmovie");
+ m_pDS->exec("INSERT INTO actor_link(actor_id, media_id, media_type, role, cast_order) SELECT DISTINCT idActor, idShow, 'tvshow', strRole, iOrder from actorlinktvshow");
+ m_pDS->exec("INSERT INTO actor_link(actor_id, media_id, media_type, role, cast_order) SELECT DISTINCT idActor, idEpisode, 'episode', strRole, iOrder from actorlinkepisode");
+ m_pDS->exec("DROP TABLE IF EXISTS actorlinkmovie");
+ m_pDS->exec("DROP TABLE IF EXISTS actorlinktvshow");
+ m_pDS->exec("DROP TABLE IF EXISTS actorlinkepisode");
+ m_pDS->exec("CREATE TABLE actor(actor_id INTEGER PRIMARY KEY, name TEXT, art_urls TEXT)");
+ m_pDS->exec("INSERT INTO actor(actor_id, name, art_urls) SELECT idActor,strActor,strThumb FROM actors");
+ m_pDS->exec("DROP TABLE IF EXISTS actors");
+
+ // directors
+ m_pDS->exec("CREATE TABLE director_link(actor_id INTEGER, media_id INTEGER, media_type TEXT)");
+ m_pDS->exec("INSERT INTO director_link(actor_id, media_id, media_type) SELECT DISTINCT idDirector, idMovie, 'movie' FROM directorlinkmovie");
+ m_pDS->exec("INSERT INTO director_link(actor_id, media_id, media_type) SELECT DISTINCT idDirector, idShow, 'tvshow' FROM directorlinktvshow");
+ m_pDS->exec("INSERT INTO director_link(actor_id, media_id, media_type) SELECT DISTINCT idDirector, idEpisode, 'episode' FROM directorlinkepisode");
+ m_pDS->exec("INSERT INTO director_link(actor_id, media_id, media_type) SELECT DISTINCT idDirector, idMVideo, 'musicvideo' FROM directorlinkmusicvideo");
+ m_pDS->exec("DROP TABLE IF EXISTS directorlinkmovie");
+ m_pDS->exec("DROP TABLE IF EXISTS directorlinktvshow");
+ m_pDS->exec("DROP TABLE IF EXISTS directorlinkepisode");
+ m_pDS->exec("DROP TABLE IF EXISTS directorlinkmusicvideo");
+
+ // writers
+ m_pDS->exec("CREATE TABLE writer_link(actor_id INTEGER, media_id INTEGER, media_type TEXT)");
+ m_pDS->exec("INSERT INTO writer_link(actor_id, media_id, media_type) SELECT DISTINCT idWriter, idMovie, 'movie' FROM writerlinkmovie");
+ m_pDS->exec("INSERT INTO writer_link(actor_id, media_id, media_type) SELECT DISTINCT idWriter, idEpisode, 'episode' FROM writerlinkepisode");
+ m_pDS->exec("DROP TABLE IF EXISTS writerlinkmovie");
+ m_pDS->exec("DROP TABLE IF EXISTS writerlinkepisode");
+
+ // music artist
+ m_pDS->exec("INSERT INTO actor_link(actor_id, media_id, media_type) SELECT DISTINCT idArtist, idMVideo, 'musicvideo' FROM artistlinkmusicvideo");
+ m_pDS->exec("DROP TABLE IF EXISTS artistlinkmusicvideo");
+
+ // studios
+ m_pDS->exec("CREATE TABLE studio_link(studio_id INTEGER, media_id INTEGER, media_type TEXT)");
+ m_pDS->exec("INSERT INTO studio_link(studio_id, media_id, media_type) SELECT DISTINCT idStudio, idMovie, 'movie' FROM studiolinkmovie");
+ m_pDS->exec("INSERT INTO studio_link(studio_id, media_id, media_type) SELECT DISTINCT idStudio, idShow, 'tvshow' FROM studiolinktvshow");
+ m_pDS->exec("INSERT INTO studio_link(studio_id, media_id, media_type) SELECT DISTINCT idStudio, idMVideo, 'musicvideo' FROM studiolinkmusicvideo");
+ m_pDS->exec("DROP TABLE IF EXISTS studiolinkmovie");
+ m_pDS->exec("DROP TABLE IF EXISTS studiolinktvshow");
+ m_pDS->exec("DROP TABLE IF EXISTS studiolinkmusicvideo");
+ m_pDS->exec("CREATE TABLE studionew(studio_id INTEGER PRIMARY KEY, name TEXT)");
+ m_pDS->exec("INSERT INTO studionew(studio_id, name) SELECT idStudio,strStudio FROM studio");
+ m_pDS->exec("DROP TABLE IF EXISTS studio");
+ m_pDS->exec("ALTER TABLE studionew RENAME TO studio");
+
+ // genres
+ m_pDS->exec("CREATE TABLE genre_link(genre_id INTEGER, media_id INTEGER, media_type TEXT)");
+ m_pDS->exec("INSERT INTO genre_link(genre_id, media_id, media_type) SELECT DISTINCT idGenre, idMovie, 'movie' FROM genrelinkmovie");
+ m_pDS->exec("INSERT INTO genre_link(genre_id, media_id, media_type) SELECT DISTINCT idGenre, idShow, 'tvshow' FROM genrelinktvshow");
+ m_pDS->exec("INSERT INTO genre_link(genre_id, media_id, media_type) SELECT DISTINCT idGenre, idMVideo, 'musicvideo' FROM genrelinkmusicvideo");
+ m_pDS->exec("DROP TABLE IF EXISTS genrelinkmovie");
+ m_pDS->exec("DROP TABLE IF EXISTS genrelinktvshow");
+ m_pDS->exec("DROP TABLE IF EXISTS genrelinkmusicvideo");
+ m_pDS->exec("CREATE TABLE genrenew(genre_id INTEGER PRIMARY KEY, name TEXT)");
+ m_pDS->exec("INSERT INTO genrenew(genre_id, name) SELECT idGenre,strGenre FROM genre");
+ m_pDS->exec("DROP TABLE IF EXISTS genre");
+ m_pDS->exec("ALTER TABLE genrenew RENAME TO genre");
+
+ // country
+ m_pDS->exec("CREATE TABLE country_link(country_id INTEGER, media_id INTEGER, media_type TEXT)");
+ m_pDS->exec("INSERT INTO country_link(country_id, media_id, media_type) SELECT DISTINCT idCountry, idMovie, 'movie' FROM countrylinkmovie");
+ m_pDS->exec("DROP TABLE IF EXISTS countrylinkmovie");
+ m_pDS->exec("CREATE TABLE countrynew(country_id INTEGER PRIMARY KEY, name TEXT)");
+ m_pDS->exec("INSERT INTO countrynew(country_id, name) SELECT idCountry,strCountry FROM country");
+ m_pDS->exec("DROP TABLE IF EXISTS country");
+ m_pDS->exec("ALTER TABLE countrynew RENAME TO country");
+
+ // tags
+ m_pDS->exec("CREATE TABLE tag_link(tag_id INTEGER, media_id INTEGER, media_type TEXT)");
+ m_pDS->exec("INSERT INTO tag_link(tag_id, media_id, media_type) SELECT DISTINCT idTag, idMedia, media_type FROM taglinks");
+ m_pDS->exec("DROP TABLE IF EXISTS taglinks");
+ m_pDS->exec("CREATE TABLE tagnew(tag_id INTEGER PRIMARY KEY, name TEXT)");
+ m_pDS->exec("INSERT INTO tagnew(tag_id, name) SELECT idTag,strTag FROM tag");
+ m_pDS->exec("DROP TABLE IF EXISTS tag");
+ m_pDS->exec("ALTER TABLE tagnew RENAME TO tag");
+ }
+
+ if (iVersion < 93)
+ {
+ // cleanup main tables
+ std::string valuesSql;
+ for(int i = 0; i < VIDEODB_MAX_COLUMNS; i++)
+ {
+ valuesSql += StringUtils::Format("c{:02} = TRIM(c{:02})", i, i);
+ if (i < VIDEODB_MAX_COLUMNS - 1)
+ valuesSql += ",";
+ }
+ m_pDS->exec("UPDATE episode SET " + valuesSql);
+ m_pDS->exec("UPDATE movie SET " + valuesSql);
+ m_pDS->exec("UPDATE musicvideo SET " + valuesSql);
+ m_pDS->exec("UPDATE tvshow SET " + valuesSql);
+
+ // cleanup additional tables
+ std::map<std::string, std::vector<std::string>> additionalTablesMap = {
+ {"actor", {"actor_link", "director_link", "writer_link"}},
+ {"studio", {"studio_link"}},
+ {"genre", {"genre_link"}},
+ {"country", {"country_link"}},
+ {"tag", {"tag_link"}}
+ };
+ for (const auto& additionalTableEntry : additionalTablesMap)
+ {
+ std::string table = additionalTableEntry.first;
+ std::string tablePk = additionalTableEntry.first + "_id";
+ std::map<int, std::string> duplicatesMinMap;
+ std::map<int, std::string> duplicatesMap;
+
+ // cleanup name
+ m_pDS->exec(PrepareSQL("UPDATE %s SET name = TRIM(name)",
+ table.c_str()));
+
+ // shrink name to length 255
+ m_pDS->exec(PrepareSQL("UPDATE %s SET name = SUBSTR(name, 1, 255) WHERE LENGTH(name) > 255",
+ table.c_str()));
+
+ // fetch main entries
+ m_pDS->query(PrepareSQL("SELECT MIN(%s), name FROM %s GROUP BY name HAVING COUNT(1) > 1",
+ tablePk.c_str(), table.c_str()));
+
+ while (!m_pDS->eof())
+ {
+ duplicatesMinMap.insert(std::make_pair(m_pDS->fv(0).get_asInt(), m_pDS->fv(1).get_asString()));
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ // fetch duplicate entries
+ for (const auto& entry : duplicatesMinMap)
+ {
+ m_pDS->query(PrepareSQL("SELECT %s FROM %s WHERE name = '%s' AND %s <> %i",
+ tablePk.c_str(), table.c_str(),
+ entry.second.c_str(), tablePk.c_str(), entry.first));
+
+ std::stringstream ids;
+ while (!m_pDS->eof())
+ {
+ int id = m_pDS->fv(0).get_asInt();
+ m_pDS->next();
+
+ ids << id;
+ if (!m_pDS->eof())
+ ids << ",";
+ }
+ m_pDS->close();
+
+ duplicatesMap.insert(std::make_pair(entry.first, ids.str()));
+ }
+
+ // cleanup duplicates in link tables
+ for (const auto& subTable : additionalTableEntry.second)
+ {
+ // create indexes to speed up things
+ m_pDS->exec(PrepareSQL("CREATE INDEX ix_%s ON %s (%s)",
+ subTable.c_str(), subTable.c_str(), tablePk.c_str()));
+
+ // migrate every duplicate entry to the main entry
+ for (const auto& entry : duplicatesMap)
+ {
+ m_pDS->exec(PrepareSQL("UPDATE %s SET %s = %i WHERE %s IN (%s) ",
+ subTable.c_str(), tablePk.c_str(), entry.first,
+ tablePk.c_str(), entry.second.c_str()));
+ }
+
+ // clear all duplicates in the link tables
+ if (subTable == "actor_link")
+ {
+ // as a distinct won't work because of role and cast_order and a group by kills a
+ // low powered mysql, we de-dupe it with REPLACE INTO while using the real unique index
+ m_pDS->exec("CREATE TABLE temp_actor_link(actor_id INT, media_id INT, media_type TEXT, role TEXT, cast_order INT)");
+ m_pDS->exec("CREATE UNIQUE INDEX ix_temp_actor_link ON temp_actor_link (actor_id, media_type(20), media_id)");
+ m_pDS->exec("REPLACE INTO temp_actor_link SELECT * FROM actor_link");
+ m_pDS->exec("DROP INDEX ix_temp_actor_link ON temp_actor_link");
+ }
+ else
+ {
+ m_pDS->exec(PrepareSQL("CREATE TABLE temp_%s AS SELECT DISTINCT * FROM %s",
+ subTable.c_str(), subTable.c_str()));
+ }
+
+ m_pDS->exec(PrepareSQL("DROP TABLE IF EXISTS %s",
+ subTable.c_str()));
+
+ m_pDS->exec(PrepareSQL("ALTER TABLE temp_%s RENAME TO %s",
+ subTable.c_str(), subTable.c_str()));
+ }
+
+ // delete duplicates in main table
+ for (const auto& entry : duplicatesMap)
+ {
+ m_pDS->exec(PrepareSQL("DELETE FROM %s WHERE %s IN (%s)",
+ table.c_str(), tablePk.c_str(), entry.second.c_str()));
+ }
+ }
+ }
+
+ if (iVersion < 96)
+ {
+ m_pDS->exec("ALTER TABLE movie ADD userrating integer");
+ m_pDS->exec("ALTER TABLE episode ADD userrating integer");
+ m_pDS->exec("ALTER TABLE tvshow ADD userrating integer");
+ m_pDS->exec("ALTER TABLE musicvideo ADD userrating integer");
+ }
+
+ if (iVersion < 97)
+ m_pDS->exec("ALTER TABLE sets ADD strOverview TEXT");
+
+ if (iVersion < 98)
+ m_pDS->exec("ALTER TABLE seasons ADD name text");
+
+ if (iVersion < 99)
+ {
+ // Add idSeason to episode table, so we don't have to join via idShow and season in the future
+ m_pDS->exec("ALTER TABLE episode ADD idSeason integer");
+
+ m_pDS->query("SELECT idSeason, idShow, season FROM seasons");
+ while (!m_pDS->eof())
+ {
+ m_pDS2->exec(PrepareSQL("UPDATE episode "
+ "SET idSeason = %d "
+ "WHERE "
+ "episode.idShow = %d AND "
+ "episode.c%02d = %d",
+ m_pDS->fv(0).get_asInt(), m_pDS->fv(1).get_asInt(),
+ VIDEODB_ID_EPISODE_SEASON, m_pDS->fv(2).get_asInt()));
+
+ m_pDS->next();
+ }
+ }
+ if (iVersion < 101)
+ m_pDS->exec("ALTER TABLE seasons ADD userrating INTEGER");
+
+ if (iVersion < 102)
+ {
+ m_pDS->exec("CREATE TABLE rating (rating_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, rating_type TEXT, rating FLOAT, votes INTEGER)");
+
+ std::string sql = PrepareSQL("SELECT DISTINCT idMovie, c%02d, c%02d FROM movie", VIDEODB_ID_RATING_ID, VIDEODB_ID_VOTES);
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ m_pDS2->exec(PrepareSQL("INSERT INTO rating(media_id, media_type, rating_type, rating, "
+ "votes) VALUES (%i, 'movie', 'default', %f, %i)",
+ m_pDS->fv(0).get_asInt(),
+ strtod(m_pDS->fv(1).get_asString().c_str(), NULL),
+ StringUtils::ReturnDigits(m_pDS->fv(2).get_asString())));
+ int idRating = (int)m_pDS2->lastinsertid();
+ m_pDS2->exec(PrepareSQL("UPDATE movie SET c%02d=%i WHERE idMovie=%i", VIDEODB_ID_RATING_ID, idRating, m_pDS->fv(0).get_asInt()));
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ sql = PrepareSQL("SELECT DISTINCT idShow, c%02d, c%02d FROM tvshow", VIDEODB_ID_TV_RATING_ID, VIDEODB_ID_TV_VOTES);
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ m_pDS2->exec(PrepareSQL("INSERT INTO rating(media_id, media_type, rating_type, rating, "
+ "votes) VALUES (%i, 'tvshow', 'default', %f, %i)",
+ m_pDS->fv(0).get_asInt(),
+ strtod(m_pDS->fv(1).get_asString().c_str(), NULL),
+ StringUtils::ReturnDigits(m_pDS->fv(2).get_asString())));
+ int idRating = (int)m_pDS2->lastinsertid();
+ m_pDS2->exec(PrepareSQL("UPDATE tvshow SET c%02d=%i WHERE idShow=%i", VIDEODB_ID_TV_RATING_ID, idRating, m_pDS->fv(0).get_asInt()));
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ sql = PrepareSQL("SELECT DISTINCT idEpisode, c%02d, c%02d FROM episode", VIDEODB_ID_EPISODE_RATING_ID, VIDEODB_ID_EPISODE_VOTES);
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ m_pDS2->exec(PrepareSQL("INSERT INTO rating(media_id, media_type, rating_type, rating, "
+ "votes) VALUES (%i, 'episode', 'default', %f, %i)",
+ m_pDS->fv(0).get_asInt(),
+ strtod(m_pDS->fv(1).get_asString().c_str(), NULL),
+ StringUtils::ReturnDigits(m_pDS->fv(2).get_asString())));
+ int idRating = (int)m_pDS2->lastinsertid();
+ m_pDS2->exec(PrepareSQL("UPDATE episode SET c%02d=%i WHERE idEpisode=%i", VIDEODB_ID_EPISODE_RATING_ID, idRating, m_pDS->fv(0).get_asInt()));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+
+ if (iVersion < 103)
+ {
+ m_pDS->exec("ALTER TABLE settings ADD VideoStream integer");
+ m_pDS->exec("ALTER TABLE streamdetails ADD strVideoLanguage text");
+ }
+
+ if (iVersion < 104)
+ {
+ m_pDS->exec("ALTER TABLE tvshow ADD duration INTEGER");
+
+ std::string sql = PrepareSQL( "SELECT episode.idShow, MAX(episode.c%02d) "
+ "FROM episode "
+
+ "LEFT JOIN streamdetails "
+ "ON streamdetails.idFile = episode.idFile "
+ "AND streamdetails.iStreamType = 0 " // only grab video streams
+
+ "WHERE episode.c%02d <> streamdetails.iVideoDuration "
+ "OR streamdetails.iVideoDuration IS NULL "
+ "GROUP BY episode.idShow", VIDEODB_ID_EPISODE_RUNTIME, VIDEODB_ID_EPISODE_RUNTIME);
+
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ m_pDS2->exec(PrepareSQL("UPDATE tvshow SET duration=%i WHERE idShow=%i", m_pDS->fv(1).get_asInt(), m_pDS->fv(0).get_asInt()));
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+
+ if (iVersion < 105)
+ {
+ m_pDS->exec("ALTER TABLE movie ADD premiered TEXT");
+ m_pDS->exec(PrepareSQL("UPDATE movie SET premiered=c%02d", VIDEODB_ID_YEAR));
+ m_pDS->exec("ALTER TABLE musicvideo ADD premiered TEXT");
+ m_pDS->exec(PrepareSQL("UPDATE musicvideo SET premiered=c%02d", VIDEODB_ID_MUSICVIDEO_YEAR));
+ }
+
+ if (iVersion < 107)
+ {
+ // need this due to the nested GetScraperPath query
+ std::unique_ptr<Dataset> pDS;
+ pDS.reset(m_pDB->CreateDataset());
+ if (nullptr == pDS)
+ return;
+
+ pDS->exec("CREATE TABLE uniqueid (uniqueid_id INTEGER PRIMARY KEY, media_id INTEGER, media_type TEXT, value TEXT, type TEXT)");
+
+ for (int i = 0; i < 3; ++i)
+ {
+ std::string mediatype, columnID;
+ int columnUniqueID;
+ switch (i)
+ {
+ case (0):
+ mediatype = "movie";
+ columnID = "idMovie";
+ columnUniqueID = VIDEODB_ID_IDENT_ID;
+ break;
+ case (1):
+ mediatype = "tvshow";
+ columnID = "idShow";
+ columnUniqueID = VIDEODB_ID_TV_IDENT_ID;
+ break;
+ case (2):
+ mediatype = "episode";
+ columnID = "idEpisode";
+ columnUniqueID = VIDEODB_ID_EPISODE_IDENT_ID;
+ break;
+ default:
+ continue;
+ }
+ pDS->query(PrepareSQL("SELECT %s, c%02d FROM %s", columnID.c_str(), columnUniqueID, mediatype.c_str()));
+ while (!pDS->eof())
+ {
+ std::string uniqueid = pDS->fv(1).get_asString();
+ if (!uniqueid.empty())
+ {
+ int mediaid = pDS->fv(0).get_asInt();
+ if (StringUtils::StartsWith(uniqueid, "tt"))
+ m_pDS2->exec(PrepareSQL("INSERT INTO uniqueid(media_id, media_type, type, value) VALUES (%i, '%s', 'imdb', '%s')", mediaid, mediatype.c_str(), uniqueid.c_str()));
+ else
+ m_pDS2->exec(PrepareSQL("INSERT INTO uniqueid(media_id, media_type, type, value) VALUES (%i, '%s', 'unknown', '%s')", mediaid, mediatype.c_str(), uniqueid.c_str()));
+ m_pDS2->exec(PrepareSQL("UPDATE %s SET c%02d='%i' WHERE %s=%i", mediatype.c_str(), columnUniqueID, (int)m_pDS2->lastinsertid(), columnID.c_str(), mediaid));
+ }
+ pDS->next();
+ }
+ pDS->close();
+ }
+ }
+
+ if (iVersion < 109)
+ {
+ m_pDS->exec("ALTER TABLE settings RENAME TO settingsold");
+ m_pDS->exec("CREATE TABLE settings ( idFile integer, Deinterlace bool,"
+ "ViewMode integer,ZoomAmount float, PixelRatio float, VerticalShift float, AudioStream integer, SubtitleStream integer,"
+ "SubtitleDelay float, SubtitlesOn bool, Brightness float, Contrast float, Gamma float,"
+ "VolumeAmplification float, AudioDelay float, ResumeTime integer,"
+ "Sharpness float, NoiseReduction float, NonLinStretch bool, PostProcess bool,"
+ "ScalingMethod integer, DeinterlaceMode integer, StereoMode integer, StereoInvert bool, VideoStream integer)");
+ m_pDS->exec("INSERT INTO settings SELECT idFile, Deinterlace, ViewMode, ZoomAmount, PixelRatio, VerticalShift, AudioStream, SubtitleStream, SubtitleDelay, SubtitlesOn, Brightness, Contrast, Gamma, VolumeAmplification, AudioDelay, ResumeTime, Sharpness, NoiseReduction, NonLinStretch, PostProcess, ScalingMethod, DeinterlaceMode, StereoMode, StereoInvert, VideoStream FROM settingsold");
+ m_pDS->exec("DROP TABLE settingsold");
+ }
+
+ if (iVersion < 110)
+ {
+ m_pDS->exec("ALTER TABLE settings ADD TonemapMethod integer");
+ m_pDS->exec("ALTER TABLE settings ADD TonemapParam float");
+ }
+
+ if (iVersion < 111)
+ m_pDS->exec("ALTER TABLE settings ADD Orientation integer");
+
+ if (iVersion < 112)
+ m_pDS->exec("ALTER TABLE settings ADD CenterMixLevel integer");
+
+ if (iVersion < 113)
+ {
+ // fb9c25f5 and e5f6d204 changed the behavior of path splitting for plugin URIs (previously it would only use the root)
+ // Re-split paths for plugin files in order to maintain watched state etc.
+ m_pDS->query("SELECT files.idFile, files.strFilename, path.strPath FROM files LEFT JOIN path ON files.idPath = path.idPath WHERE files.strFilename LIKE 'plugin://%'");
+ while (!m_pDS->eof())
+ {
+ std::string path, fn;
+ SplitPath(m_pDS->fv(1).get_asString(), path, fn);
+ if (path != m_pDS->fv(2).get_asString())
+ {
+ int pathid = -1;
+ m_pDS2->query(PrepareSQL("SELECT idPath FROM path WHERE strPath='%s'", path.c_str()));
+ if (!m_pDS2->eof())
+ pathid = m_pDS2->fv(0).get_asInt();
+ m_pDS2->close();
+ if (pathid < 0)
+ {
+ std::string parent = URIUtils::GetParentPath(path);
+ int parentid = -1;
+ m_pDS2->query(PrepareSQL("SELECT idPath FROM path WHERE strPath='%s'", parent.c_str()));
+ if (!m_pDS2->eof())
+ parentid = m_pDS2->fv(0).get_asInt();
+ m_pDS2->close();
+ if (parentid < 0)
+ {
+ m_pDS2->exec(PrepareSQL("INSERT INTO path (strPath) VALUES ('%s')", parent.c_str()));
+ parentid = (int)m_pDS2->lastinsertid();
+ }
+ m_pDS2->exec(PrepareSQL("INSERT INTO path (strPath, idParentPath) VALUES ('%s', %i)", path.c_str(), parentid));
+ pathid = (int)m_pDS2->lastinsertid();
+ }
+ m_pDS2->query(PrepareSQL("SELECT idFile FROM files WHERE strFileName='%s' AND idPath=%i", fn.c_str(), pathid));
+ bool exists = !m_pDS2->eof();
+ m_pDS2->close();
+ if (exists)
+ m_pDS2->exec(PrepareSQL("DELETE FROM files WHERE idFile=%i", m_pDS->fv(0).get_asInt()));
+ else
+ m_pDS2->exec(PrepareSQL("UPDATE files SET idPath=%i WHERE idFile=%i", pathid, m_pDS->fv(0).get_asInt()));
+ }
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+
+ if (iVersion < 119)
+ m_pDS->exec("ALTER TABLE path ADD allAudio bool");
+
+ if (iVersion < 120)
+ m_pDS->exec("ALTER TABLE streamdetails ADD strHdrType text");
+
+ if (iVersion < 121)
+ {
+ // https://github.com/xbmc/xbmc/issues/21253 - Kodi picks up wrong "year" for PVR recording.
+
+ m_pDS->query("SELECT idFile, strFilename FROM files WHERE strFilename LIKE '% (1969)%.pvr' OR "
+ "strFilename LIKE '% (1601)%.pvr'");
+ while (!m_pDS->eof())
+ {
+ std::string fixedFileName = m_pDS->fv(1).get_asString();
+ size_t pos = fixedFileName.find(" (1969)");
+ if (pos == std::string::npos)
+ pos = fixedFileName.find(" (1601)");
+
+ if (pos != std::string::npos)
+ {
+ fixedFileName.erase(pos, 7);
+
+ m_pDS2->exec(PrepareSQL("UPDATE files SET strFilename='%s' WHERE idFile=%i",
+ fixedFileName.c_str(), m_pDS->fv(0).get_asInt()));
+ }
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+}
+
+int CVideoDatabase::GetSchemaVersion() const
+{
+ return 121;
+}
+
+bool CVideoDatabase::LookupByFolders(const std::string &path, bool shows)
+{
+ SScanSettings settings;
+ bool foundDirectly = false;
+ ScraperPtr scraper = GetScraperForPath(path, settings, foundDirectly);
+ if (scraper && scraper->Content() == CONTENT_TVSHOWS && !shows)
+ return false; // episodes
+ return settings.parent_name_root; // shows, movies, musicvids
+}
+
+bool CVideoDatabase::GetPlayCounts(const std::string &strPath, CFileItemList &items)
+{
+ if(URIUtils::IsMultiPath(strPath))
+ {
+ std::vector<std::string> paths;
+ CMultiPathDirectory::GetPaths(strPath, paths);
+
+ bool ret = false;
+ for(unsigned i=0;i<paths.size();i++)
+ ret |= GetPlayCounts(paths[i], items);
+
+ return ret;
+ }
+ int pathID = -1;
+ if (!URIUtils::IsPlugin(strPath))
+ {
+ pathID = GetPathId(strPath);
+ if (pathID < 0)
+ return false; // path (and thus files) aren't in the database
+ }
+
+ try
+ {
+ // error!
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string sql =
+ "SELECT"
+ " files.strFilename, files.playCount,"
+ " bookmark.timeInSeconds, bookmark.totalTimeInSeconds "
+ "FROM files"
+ " LEFT JOIN bookmark ON"
+ " files.idFile = bookmark.idFile AND bookmark.type = %i ";
+
+ if (URIUtils::IsPlugin(strPath))
+ {
+ for (auto& item : items)
+ {
+ if (!item || item->m_bIsFolder || !item->GetProperty("IsPlayable").asBoolean())
+ continue;
+
+ std::string path, filename;
+ SplitPath(item->GetPath(), path, filename);
+ m_pDS->query(PrepareSQL(sql +
+ "INNER JOIN path ON files.idPath = path.idPath "
+ "WHERE files.strFilename='%s' AND path.strPath='%s'",
+ (int)CBookmark::RESUME, filename.c_str(), path.c_str()));
+
+ if (!m_pDS->eof())
+ {
+ if (!item->GetVideoInfoTag()->IsPlayCountSet())
+ item->GetVideoInfoTag()->SetPlayCount(m_pDS->fv(1).get_asInt());
+ if (!item->GetVideoInfoTag()->GetResumePoint().IsSet())
+ item->GetVideoInfoTag()->SetResumePoint(m_pDS->fv(2).get_asInt(), m_pDS->fv(3).get_asInt(), "");
+ }
+ m_pDS->close();
+ }
+ }
+ else
+ {
+ //! @todo also test a single query for the above and below
+ sql = PrepareSQL(sql + "WHERE files.idPath=%i", (int)CBookmark::RESUME, pathID);
+
+ if (RunQuery(sql) <= 0)
+ return false;
+
+ items.SetFastLookup(true); // note: it's possibly quicker the other way around (map on db returned items)?
+ while (!m_pDS->eof())
+ {
+ std::string path;
+ ConstructPath(path, strPath, m_pDS->fv(0).get_asString());
+ CFileItemPtr item = items.Get(path);
+ if (item)
+ {
+ if (!items.IsPlugin() || !item->GetVideoInfoTag()->IsPlayCountSet())
+ item->GetVideoInfoTag()->SetPlayCount(m_pDS->fv(1).get_asInt());
+
+ if (!item->GetVideoInfoTag()->GetResumePoint().IsSet())
+ {
+ item->GetVideoInfoTag()->SetResumePoint(m_pDS->fv(2).get_asInt(), m_pDS->fv(3).get_asInt(), "");
+ }
+ }
+ m_pDS->next();
+ }
+ }
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+int CVideoDatabase::GetPlayCount(int iFileId)
+{
+ if (iFileId < 0)
+ return 0; // not in db, so not watched
+
+ try
+ {
+ // error!
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string strSQL = PrepareSQL("select playCount from files WHERE idFile=%i", iFileId);
+ int count = 0;
+ if (m_pDS->query(strSQL))
+ {
+ // there should only ever be one row returned
+ if (m_pDS->num_rows() == 1)
+ count = m_pDS->fv(0).get_asInt();
+ m_pDS->close();
+ }
+ return count;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+int CVideoDatabase::GetPlayCount(const std::string& strFilenameAndPath)
+{
+ return GetPlayCount(GetFileId(strFilenameAndPath));
+}
+
+int CVideoDatabase::GetPlayCount(const CFileItem &item)
+{
+ return GetPlayCount(GetFileId(item));
+}
+
+CDateTime CVideoDatabase::GetLastPlayed(int iFileId)
+{
+ if (iFileId < 0)
+ return {}; // not in db, so not watched
+
+ try
+ {
+ // error!
+ if (nullptr == m_pDB)
+ return {};
+ if (nullptr == m_pDS)
+ return {};
+
+ std::string strSQL = PrepareSQL("select lastPlayed from files WHERE idFile=%i", iFileId);
+ CDateTime lastPlayed;
+ if (m_pDS->query(strSQL))
+ {
+ // there should only ever be one row returned
+ if (m_pDS->num_rows() == 1)
+ lastPlayed.SetFromDBDateTime(m_pDS->fv(0).get_asString());
+ m_pDS->close();
+ }
+ return lastPlayed;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return {};
+}
+
+CDateTime CVideoDatabase::GetLastPlayed(const std::string& strFilenameAndPath)
+{
+ return GetLastPlayed(GetFileId(strFilenameAndPath));
+}
+
+void CVideoDatabase::UpdateFanart(const CFileItem& item, VideoDbContentType type)
+{
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+ if (!item.HasVideoInfoTag() || item.GetVideoInfoTag()->m_iDbId < 0) return;
+
+ std::string exec;
+ if (type == VideoDbContentType::TVSHOWS)
+ exec = PrepareSQL("UPDATE tvshow set c%02d='%s' WHERE idShow=%i", VIDEODB_ID_TV_FANART, item.GetVideoInfoTag()->m_fanart.m_xml.c_str(), item.GetVideoInfoTag()->m_iDbId);
+ else if (type == VideoDbContentType::MOVIES)
+ exec = PrepareSQL("UPDATE movie set c%02d='%s' WHERE idMovie=%i", VIDEODB_ID_FANART, item.GetVideoInfoTag()->m_fanart.m_xml.c_str(), item.GetVideoInfoTag()->m_iDbId);
+
+ try
+ {
+ m_pDS->exec(exec);
+
+ if (type == VideoDbContentType::TVSHOWS)
+ AnnounceUpdate(MediaTypeTvShow, item.GetVideoInfoTag()->m_iDbId);
+ else if (type == VideoDbContentType::MOVIES)
+ AnnounceUpdate(MediaTypeMovie, item.GetVideoInfoTag()->m_iDbId);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} - error updating fanart for {}", __FUNCTION__, item.GetPath());
+ }
+}
+
+CDateTime CVideoDatabase::SetPlayCount(const CFileItem& item, int count, const CDateTime& date)
+{
+ int id;
+ if (item.HasProperty("original_listitem_url") &&
+ URIUtils::IsPlugin(item.GetProperty("original_listitem_url").asString()))
+ {
+ CFileItem item2(item);
+ item2.SetPath(item.GetProperty("original_listitem_url").asString());
+ id = AddFile(item2);
+ }
+ else
+ id = AddFile(item);
+ if (id < 0)
+ return {};
+
+ // and mark as watched
+ try
+ {
+ const CDateTime lastPlayed(date.IsValid() ? date : CDateTime::GetCurrentDateTime());
+
+ if (nullptr == m_pDB)
+ return {};
+ if (nullptr == m_pDS)
+ return {};
+
+ std::string strSQL;
+ if (count)
+ {
+ strSQL = PrepareSQL("update files set playCount=%i,lastPlayed='%s' where idFile=%i", count,
+ lastPlayed.GetAsDBDateTime().c_str(), id);
+ }
+ else
+ {
+ if (!date.IsValid())
+ strSQL = PrepareSQL("update files set playCount=NULL,lastPlayed=NULL where idFile=%i", id);
+ else
+ strSQL = PrepareSQL("update files set playCount=NULL,lastPlayed='%s' where idFile=%i",
+ lastPlayed.GetAsDBDateTime().c_str(), id);
+ }
+
+ m_pDS->exec(strSQL);
+
+ // We only need to announce changes to video items in the library
+ if (item.HasVideoInfoTag() && item.GetVideoInfoTag()->m_iDbId > 0)
+ {
+ CVariant data;
+ if (CVideoLibraryQueue::GetInstance().IsScanningLibrary())
+ data["transaction"] = true;
+ // Only provide the "playcount" value if it has actually changed
+ if (item.GetVideoInfoTag()->GetPlayCount() != count)
+ data["playcount"] = count;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnUpdate",
+ CFileItemPtr(new CFileItem(item)), data);
+ }
+
+ return lastPlayed;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+
+ return {};
+}
+
+CDateTime CVideoDatabase::IncrementPlayCount(const CFileItem& item)
+{
+ return SetPlayCount(item, GetPlayCount(item) + 1);
+}
+
+CDateTime CVideoDatabase::UpdateLastPlayed(const CFileItem& item)
+{
+ return SetPlayCount(item, GetPlayCount(item), CDateTime::GetCurrentDateTime());
+}
+
+void CVideoDatabase::UpdateMovieTitle(int idMovie,
+ const std::string& strNewMovieTitle,
+ VideoDbContentType iType)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+ std::string content;
+ if (iType == VideoDbContentType::MOVIES)
+ {
+ CLog::Log(LOGINFO, "Changing Movie:id:{} New Title:{}", idMovie, strNewMovieTitle);
+ content = MediaTypeMovie;
+ }
+ else if (iType == VideoDbContentType::EPISODES)
+ {
+ CLog::Log(LOGINFO, "Changing Episode:id:{} New Title:{}", idMovie, strNewMovieTitle);
+ content = MediaTypeEpisode;
+ }
+ else if (iType == VideoDbContentType::TVSHOWS)
+ {
+ CLog::Log(LOGINFO, "Changing TvShow:id:{} New Title:{}", idMovie, strNewMovieTitle);
+ content = MediaTypeTvShow;
+ }
+ else if (iType == VideoDbContentType::MUSICVIDEOS)
+ {
+ CLog::Log(LOGINFO, "Changing MusicVideo:id:{} New Title:{}", idMovie, strNewMovieTitle);
+ content = MediaTypeMusicVideo;
+ }
+ else if (iType == VideoDbContentType::MOVIE_SETS)
+ {
+ CLog::Log(LOGINFO, "Changing Movie set:id:{} New Title:{}", idMovie, strNewMovieTitle);
+ std::string strSQL = PrepareSQL("UPDATE sets SET strSet='%s' WHERE idSet=%i", strNewMovieTitle.c_str(), idMovie );
+ m_pDS->exec(strSQL);
+ }
+
+ if (!content.empty())
+ {
+ SetSingleValue(iType, idMovie, FieldTitle, strNewMovieTitle);
+ AnnounceUpdate(content, idMovie);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(
+ LOGERROR,
+ "{} (int idMovie, const std::string& strNewMovieTitle) failed on MovieID:{} and Title:{}",
+ __FUNCTION__, idMovie, strNewMovieTitle);
+ }
+}
+
+bool CVideoDatabase::UpdateVideoSortTitle(int idDb,
+ const std::string& strNewSortTitle,
+ VideoDbContentType iType /* = MOVIES */)
+{
+ try
+ {
+ if (nullptr == m_pDB || nullptr == m_pDS)
+ return false;
+ if (iType != VideoDbContentType::MOVIES && iType != VideoDbContentType::TVSHOWS)
+ return false;
+
+ std::string content = MediaTypeMovie;
+ if (iType == VideoDbContentType::TVSHOWS)
+ content = MediaTypeTvShow;
+
+ if (SetSingleValue(iType, idDb, FieldSortTitle, strNewSortTitle))
+ {
+ AnnounceUpdate(content, idDb);
+ return true;
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR,
+ "{} (int idDb, const std::string& strNewSortTitle, VIDEODB_CONTENT_TYPE iType) "
+ "failed on ID: {} and Sort Title: {}",
+ __FUNCTION__, idDb, strNewSortTitle);
+ }
+
+ return false;
+}
+
+/// \brief EraseVideoSettings() Erases the videoSettings table and reconstructs it
+void CVideoDatabase::EraseVideoSettings(const CFileItem &item)
+{
+ int idFile = GetFileId(item);
+ if (idFile < 0)
+ return;
+
+ try
+ {
+ std::string sql = PrepareSQL("DELETE FROM settings WHERE idFile=%i", idFile);
+
+ CLog::Log(LOGINFO, "Deleting settings information for files {}",
+ CURL::GetRedacted(item.GetPath()));
+ m_pDS->exec(sql);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+}
+
+void CVideoDatabase::EraseAllVideoSettings()
+{
+ try
+ {
+ std::string sql = "DELETE FROM settings";
+
+ CLog::Log(LOGINFO, "Deleting all video settings");
+ m_pDS->exec(sql);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+}
+
+void CVideoDatabase::EraseAllVideoSettings(const std::string& path)
+{
+ std::string itemsToDelete;
+
+ try
+ {
+ std::string sql = PrepareSQL("SELECT files.idFile FROM files WHERE idFile IN (SELECT idFile FROM files INNER JOIN path ON path.idPath = files.idPath AND path.strPath LIKE \"%s%%\")", path.c_str());
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ std::string file = m_pDS->fv("files.idFile").get_asString() + ",";
+ itemsToDelete += file;
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ if (!itemsToDelete.empty())
+ {
+ itemsToDelete = "(" + StringUtils::TrimRight(itemsToDelete, ",") + ")";
+
+ sql = "DELETE FROM settings WHERE idFile IN " + itemsToDelete;
+ m_pDS->exec(sql);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+}
+
+bool CVideoDatabase::GetGenresNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent /* = UNKNOWN */,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ return GetNavCommon(strBaseDir, items, "genre", idContent, filter, countOnly);
+}
+
+bool CVideoDatabase::GetCountriesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent /* = UNKNOWN */,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ return GetNavCommon(strBaseDir, items, "country", idContent, filter, countOnly);
+}
+
+bool CVideoDatabase::GetStudiosNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent /* = UNKNOWN */,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ return GetNavCommon(strBaseDir, items, "studio", idContent, filter, countOnly);
+}
+
+bool CVideoDatabase::GetNavCommon(const std::string& strBaseDir,
+ CFileItemList& items,
+ const char* type,
+ VideoDbContentType idContent /* = UNKNOWN */,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ Filter extFilter = filter;
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ {
+ std::string view, view_id, media_type, extraField, extraJoin;
+ if (idContent == VideoDbContentType::MOVIES)
+ {
+ view = MediaTypeMovie;
+ view_id = "idMovie";
+ media_type = MediaTypeMovie;
+ extraField = "files.playCount";
+ }
+ else if (idContent == VideoDbContentType::TVSHOWS) //this will not get tvshows with 0 episodes
+ {
+ view = MediaTypeEpisode;
+ view_id = "idShow";
+ media_type = MediaTypeTvShow;
+ // in order to make use of FieldPlaycount in smart playlists we need an extra join
+ if (StringUtils::EqualsNoCase(type, "tag"))
+ extraJoin = PrepareSQL("JOIN tvshow_view ON tvshow_view.idShow = tag_link.media_id AND tag_link.media_type='tvshow'");
+ }
+ else if (idContent == VideoDbContentType::MUSICVIDEOS)
+ {
+ view = MediaTypeMusicVideo;
+ view_id = "idMVideo";
+ media_type = MediaTypeMusicVideo;
+ extraField = "files.playCount";
+ }
+ else
+ return false;
+
+ strSQL = "SELECT {} " + PrepareSQL("FROM %s ", type);
+ extFilter.fields = PrepareSQL("%s.%s_id, %s.name, path.strPath", type, type, type);
+ extFilter.AppendField(extraField);
+ extFilter.AppendJoin(PrepareSQL("JOIN %s_link ON %s.%s_id = %s_link.%s_id", type, type, type, type, type));
+ extFilter.AppendJoin(PrepareSQL("JOIN %s_view ON %s_link.media_id = %s_view.%s AND %s_link.media_type='%s'", view.c_str(), type, view.c_str(), view_id.c_str(), type, media_type.c_str()));
+ extFilter.AppendJoin(PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str()));
+ extFilter.AppendJoin("JOIN path ON path.idPath = files.idPath");
+ extFilter.AppendJoin(extraJoin);
+ }
+ else
+ {
+ std::string view, view_id, media_type, extraField, extraJoin;
+ if (idContent == VideoDbContentType::MOVIES)
+ {
+ view = MediaTypeMovie;
+ view_id = "idMovie";
+ media_type = MediaTypeMovie;
+ extraField = "count(1), count(files.playCount)";
+ extraJoin = PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str());
+ }
+ else if (idContent == VideoDbContentType::TVSHOWS)
+ {
+ view = MediaTypeTvShow;
+ view_id = "idShow";
+ media_type = MediaTypeTvShow;
+ }
+ else if (idContent == VideoDbContentType::MUSICVIDEOS)
+ {
+ view = MediaTypeMusicVideo;
+ view_id = "idMVideo";
+ media_type = MediaTypeMusicVideo;
+ extraField = "count(1), count(files.playCount)";
+ extraJoin = PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str());
+ }
+ else
+ return false;
+
+ strSQL = "SELECT {} " + PrepareSQL("FROM %s ", type);
+ extFilter.fields = PrepareSQL("%s.%s_id, %s.name", type, type, type);
+ extFilter.AppendField(extraField);
+ extFilter.AppendJoin(PrepareSQL("JOIN %s_link ON %s.%s_id = %s_link.%s_id", type, type, type, type, type));
+ extFilter.AppendJoin(PrepareSQL("JOIN %s_view ON %s_link.media_id = %s_view.%s AND %s_link.media_type='%s'",
+ view.c_str(), type, view.c_str(), view_id.c_str(), type, media_type.c_str()));
+ extFilter.AppendJoin(extraJoin);
+ extFilter.AppendGroup(PrepareSQL("%s.%s_id", type, type));
+ }
+
+ if (countOnly)
+ {
+ extFilter.fields = PrepareSQL("COUNT(DISTINCT %s.%s_id)", type, type);
+ extFilter.group.clear();
+ extFilter.order.clear();
+ }
+ strSQL = StringUtils::Format(strSQL, !extFilter.fields.empty() ? extFilter.fields : "*");
+
+ CVideoDbUrl videoUrl;
+ if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, videoUrl))
+ return false;
+
+ int iRowsFound = RunQuery(strSQL);
+ if (iRowsFound <= 0)
+ return iRowsFound == 0;
+
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ {
+ std::map<int, std::pair<std::string,int> > mapItems;
+ while (!m_pDS->eof())
+ {
+ int id = m_pDS->fv(0).get_asInt();
+ std::string str = m_pDS->fv(1).get_asString();
+
+ // was this already found?
+ auto it = mapItems.find(id);
+ if (it == mapItems.end())
+ {
+ // check path
+ if (g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv(2).get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ if (idContent == VideoDbContentType::MOVIES ||
+ idContent == VideoDbContentType::MUSICVIDEOS)
+ mapItems.insert(std::pair<int, std::pair<std::string,int> >(id, std::pair<std::string, int>(str,m_pDS->fv(3).get_asInt()))); //fv(3) is file.playCount
+ else if (idContent == VideoDbContentType::TVSHOWS)
+ mapItems.insert(std::pair<int, std::pair<std::string,int> >(id, std::pair<std::string,int>(str,0)));
+ }
+ }
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ for (const auto &i : mapItems)
+ {
+ CFileItemPtr pItem(new CFileItem(i.second.first));
+ pItem->GetVideoInfoTag()->m_iDbId = i.first;
+ pItem->GetVideoInfoTag()->m_type = type;
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string path = StringUtils::Format("{}/", i.first);
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ if (idContent == VideoDbContentType::MOVIES || idContent == VideoDbContentType::MUSICVIDEOS)
+ pItem->GetVideoInfoTag()->SetPlayCount(i.second.second);
+ if (!items.Contains(pItem->GetPath()))
+ {
+ pItem->SetLabelPreformatted(true);
+ items.Add(pItem);
+ }
+ }
+ }
+ else
+ {
+ while (!m_pDS->eof())
+ {
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ pItem->GetVideoInfoTag()->m_iDbId = m_pDS->fv(0).get_asInt();
+ pItem->GetVideoInfoTag()->m_type = type;
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string path = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ pItem->SetLabelPreformatted(true);
+ if (idContent == VideoDbContentType::MOVIES || idContent == VideoDbContentType::MUSICVIDEOS)
+ { // fv(3) is the number of videos watched, fv(2) is the total number. We set the playcount
+ // only if the number of videos watched is equal to the total number (i.e. every video watched)
+ pItem->GetVideoInfoTag()->SetPlayCount((m_pDS->fv(3).get_asInt() == m_pDS->fv(2).get_asInt()) ? 1 : 0);
+ }
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetTagsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent /* = UNKNOWN */,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ return GetNavCommon(strBaseDir, items, "tag", idContent, filter, countOnly);
+}
+
+bool CVideoDatabase::GetSetsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent /* = UNKNOWN */,
+ const Filter& filter /* = Filter() */,
+ bool ignoreSingleMovieSets /* = false */)
+{
+ if (idContent != VideoDbContentType::MOVIES)
+ return false;
+
+ return GetSetsByWhere(strBaseDir, filter, items, ignoreSingleMovieSets);
+}
+
+bool CVideoDatabase::GetSetsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool ignoreSingleMovieSets /* = false */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(strBaseDir))
+ return false;
+
+ Filter setFilter = filter;
+ setFilter.join += " JOIN sets ON movie_view.idSet = sets.idSet";
+ if (!setFilter.order.empty())
+ setFilter.order += ",";
+ setFilter.order += "sets.idSet";
+
+ if (!GetMoviesByWhere(strBaseDir, setFilter, items))
+ return false;
+
+ CFileItemList sets;
+ GroupAttribute groupingAttributes;
+ const CUrlOptions::UrlOptions& options = videoUrl.GetOptions();
+ auto option = options.find("ignoreSingleMovieSets");
+
+ if (option != options.end())
+ {
+ groupingAttributes =
+ option->second.asBoolean() ? GroupAttributeIgnoreSingleItems : GroupAttributeNone;
+ }
+ else
+ {
+ groupingAttributes =
+ ignoreSingleMovieSets ? GroupAttributeIgnoreSingleItems : GroupAttributeNone;
+ }
+
+ if (!GroupUtils::Group(GroupBySet, strBaseDir, items, sets, groupingAttributes))
+ return false;
+
+ items.ClearItems();
+ items.Append(sets);
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetMusicVideoAlbumsNav(const std::string& strBaseDir, CFileItemList& items, int idArtist /* = -1 */, const Filter &filter /* = Filter() */, bool countOnly /* = false */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(strBaseDir))
+ return false;
+
+ std::string strSQL = "select {} from musicvideo_view ";
+ Filter extFilter = filter;
+ extFilter.fields = PrepareSQL("musicvideo_view.c%02d, musicvideo_view.idMVideo, actor.name, "
+ "musicvideo_view.c%02d, musicvideo_view.c%02d, musicvideo_view.c%02d ",
+ VIDEODB_ID_MUSICVIDEO_ALBUM, VIDEODB_ID_MUSICVIDEO_TITLE,
+ VIDEODB_ID_MUSICVIDEO_PLOT, VIDEODB_ID_MUSICVIDEO_ARTIST);
+ extFilter.AppendJoin(
+ PrepareSQL("JOIN actor_link ON actor_link.media_id=musicvideo_view.idMVideo "));
+ extFilter.AppendJoin(PrepareSQL("JOIN actor ON actor.actor_id = actor_link.actor_id"));
+ extFilter.fields += ", path.strPath";
+ extFilter.AppendJoin("join files on files.idFile = musicvideo_view.idFile join path on path.idPath = files.idPath");
+
+ if (StringUtils::EndsWith(strBaseDir,"albums/"))
+ extFilter.AppendWhere(PrepareSQL("musicvideo_view.c%02d != ''", VIDEODB_ID_MUSICVIDEO_ALBUM));
+
+ if (idArtist > -1)
+ videoUrl.AddOption("artistid", idArtist);
+
+ extFilter.AppendGroup(PrepareSQL(" CASE WHEN musicvideo_view.c09 !='' THEN musicvideo_view.c09 "
+ "ELSE musicvideo_view.c00 END"));
+
+ if (countOnly)
+ {
+ extFilter.fields = "COUNT(1)";
+ extFilter.group.clear();
+ extFilter.order.clear();
+ }
+ strSQL = StringUtils::Format(strSQL, !extFilter.fields.empty() ? extFilter.fields : "*");
+
+ if (!BuildSQL(videoUrl.ToString(), strSQL, extFilter, strSQL, videoUrl))
+ return false;
+
+ int iRowsFound = RunQuery(strSQL);
+ /* fields returned by query are :-
+ (0) - Album title (if any)
+ (1) - idMVideo
+ (2) - Artist name
+ (3) - Music video title
+ (4) - Music video plot
+ (5) - Music Video artist
+ (6) - Path to video
+ */
+ if (iRowsFound <= 0)
+ return iRowsFound == 0;
+
+ std::string strArtist;
+ if (idArtist> -1)
+ strArtist = m_pDS->fv("actor.name").get_asString();
+
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ std::list <int> idMVideoList;
+ std::list <std::pair<std::string, std::string>> idData;
+
+ while (!m_pDS->eof())
+ {
+ bool isAlbum = true;
+ std::string strAlbum = m_pDS->fv(0).get_asString(); //Album title
+ int idMVideo = m_pDS->fv(1).get_asInt();
+ if (strAlbum.empty())
+ {
+ strAlbum = m_pDS->fv(3).get_asString(); // video title if not an album
+ isAlbum = false;
+ }
+
+ CFileItemPtr pItem(new CFileItem(strAlbum));
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string path = StringUtils::Format("{}/", idMVideo);
+ if (!isAlbum)
+ {
+ itemUrl.AddOption("albumid", idMVideo);
+ path += std::to_string(idMVideo);
+
+ strSQL = PrepareSQL(
+ "SELECT type, url FROM art WHERE media_id = %i AND media_type = 'musicvideo'",
+ idMVideo);
+ m_pDS2->query(strSQL);
+ while (!m_pDS2->eof())
+ {
+ pItem->SetArt(m_pDS2->fv(0).get_asString(), m_pDS2->fv(1).get_asString());
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+ }
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+ pItem->m_bIsFolder = isAlbum;
+ pItem->SetLabelPreformatted(true);
+
+ if (!items.Contains(pItem->GetPath()))
+ if (g_passwordManager.IsDatabasePathUnlocked(
+ m_pDS->fv("path.strPath").get_asString(),
+ *CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ pItem->GetVideoInfoTag()->m_artist.emplace_back(strArtist);
+ pItem->GetVideoInfoTag()->m_iDbId = idMVideo;
+ items.Add(pItem);
+ idMVideoList.push_back(idMVideo);
+ idData.push_back(make_pair(m_pDS->fv(0).get_asString(), m_pDS->fv(5).get_asString()));
+ }
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CVideoInfoTag details;
+
+ if (items[i]->m_bIsFolder)
+ {
+ details.SetPath(items[i]->GetPath());
+ details.m_strAlbum = idData.front().first;
+ details.m_type = MediaTypeAlbum;
+ details.m_artist.emplace_back(idData.front().second);
+ details.m_iDbId = idMVideoList.front();
+ items[i]->SetProperty("musicvideomediatype", MediaTypeAlbum);
+ items[i]->SetLabel(idData.front().first);
+ items[i]->SetFromVideoInfoTag(details);
+
+ idMVideoList.pop_front();
+ idData.pop_front();
+ continue;
+ }
+ else
+ {
+ GetMusicVideoInfo("", details, idMVideoList.front());
+ items[i]->SetFromVideoInfoTag(details);
+ idMVideoList.pop_front();
+ idData.pop_front();
+ }
+ }
+
+ if (!strArtist.empty())
+ items.SetProperty("customtitle",strArtist); // change displayed path from eg /23 to /Artist
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetWritersNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent /* = UNKNOWN */,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ return GetPeopleNav(strBaseDir, items, "writer", idContent, filter, countOnly);
+}
+
+bool CVideoDatabase::GetDirectorsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent /* = UNKNOWN */,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ return GetPeopleNav(strBaseDir, items, "director", idContent, filter, countOnly);
+}
+
+bool CVideoDatabase::GetActorsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent /* = UNKNOWN */,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ if (GetPeopleNav(strBaseDir, items, "actor", idContent, filter, countOnly))
+ { // set thumbs - ideally this should be in the normal thumb setting routines
+ for (int i = 0; i < items.Size() && !countOnly; i++)
+ {
+ CFileItemPtr pItem = items[i];
+ if (idContent == VideoDbContentType::MUSICVIDEOS)
+ pItem->SetArt("icon", "DefaultArtist.png");
+ else
+ pItem->SetArt("icon", "DefaultActor.png");
+ }
+ return true;
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetPeopleNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const char* type,
+ VideoDbContentType idContent /* = UNKNOWN */,
+ const Filter& filter /* = Filter() */,
+ bool countOnly /* = false */)
+{
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ try
+ {
+ //! @todo This routine (and probably others at this same level) use playcount as a reference to filter on at a later
+ //! point. This means that we *MUST* filter these levels as you'll get double ups. Ideally we'd allow playcount
+ //! to filter through as we normally do for tvshows to save this happening.
+ //! Also, we apply this same filtering logic to the locked or unlocked paths to prevent these from showing.
+ //! Whether or not this should happen is a tricky one - it complicates all the high level categories (everything
+ //! above titles).
+
+ // General routine that the other actor/director/writer routines call
+
+ // get primary genres for movies
+ std::string strSQL;
+ bool bMainArtistOnly = false;
+ Filter extFilter = filter;
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ {
+ std::string view, view_id, media_type, extraField, extraJoin, group;
+ if (idContent == VideoDbContentType::MOVIES)
+ {
+ view = MediaTypeMovie;
+ view_id = "idMovie";
+ media_type = MediaTypeMovie;
+ extraField = "files.playCount";
+ }
+ else if (idContent == VideoDbContentType::TVSHOWS)
+ {
+ view = MediaTypeEpisode;
+ view_id = "idShow";
+ media_type = MediaTypeTvShow;
+ extraField = "count(DISTINCT idShow)";
+ group = "actor.actor_id";
+ }
+ else if (idContent == VideoDbContentType::EPISODES)
+ {
+ view = MediaTypeEpisode;
+ view_id = "idEpisode";
+ media_type = MediaTypeEpisode;
+ extraField = "files.playCount";
+ }
+ else if (idContent == VideoDbContentType::MUSICVIDEOS)
+ {
+ bMainArtistOnly = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_VIDEOLIBRARY_SHOWPERFORMERS);
+ if (StringUtils::EndsWith(strBaseDir, "directors/"))
+ // only set this to true if getting artists and show all performers is false
+ bMainArtistOnly = false;
+ view = MediaTypeMusicVideo;
+ view_id = "idMVideo";
+ media_type = MediaTypeMusicVideo;
+ extraField = "count(1), count(files.playCount)";
+ if (bMainArtistOnly)
+ extraJoin =
+ PrepareSQL(" WHERE actor.name IN (SELECT musicvideo_view.c10 from musicvideo_view)");
+ group = "actor.actor_id";
+ }
+ else
+ return false;
+
+ strSQL = "SELECT {} FROM actor ";
+ extFilter.fields = "actor.actor_id, actor.name, actor.art_urls, path.strPath";
+ extFilter.AppendField(extraField);
+ extFilter.AppendJoin(PrepareSQL("JOIN %s_link ON actor.actor_id = %s_link.actor_id", type, type));
+ extFilter.AppendJoin(PrepareSQL("JOIN %s_view ON %s_link.media_id = %s_view.%s AND %s_link.media_type='%s'", view.c_str(), type, view.c_str(), view_id.c_str(), type, media_type.c_str()));
+ extFilter.AppendJoin(PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str()));
+ extFilter.AppendJoin("JOIN path ON path.idPath = files.idPath");
+ extFilter.AppendJoin(extraJoin);
+ extFilter.AppendGroup(group);
+ }
+ else
+ {
+ std::string view, view_id, media_type, extraField, extraJoin;
+ if (idContent == VideoDbContentType::MOVIES)
+ {
+ view = MediaTypeMovie;
+ view_id = "idMovie";
+ media_type = MediaTypeMovie;
+ extraField = "count(1), count(files.playCount)";
+ extraJoin = PrepareSQL(" JOIN files ON files.idFile=%s_view.idFile", view.c_str());
+ }
+ else if (idContent == VideoDbContentType::TVSHOWS)
+ {
+ view = MediaTypeTvShow;
+ view_id = "idShow";
+ media_type = MediaTypeTvShow;
+ extraField = "count(idShow)";
+ }
+ else if (idContent == VideoDbContentType::EPISODES)
+ {
+ view = MediaTypeEpisode;
+ view_id = "idEpisode";
+ media_type = MediaTypeEpisode;
+ extraField = "count(1), count(files.playCount)";
+ extraJoin = PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str());
+ }
+ else if (idContent == VideoDbContentType::MUSICVIDEOS)
+ {
+ bMainArtistOnly = !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_VIDEOLIBRARY_SHOWPERFORMERS);
+ if (StringUtils::EndsWith(strBaseDir, "directors/"))
+ // only set this to true if getting artists and show all performers is false
+ bMainArtistOnly = false;
+ view = MediaTypeMusicVideo;
+ view_id = "idMVideo";
+ media_type = MediaTypeMusicVideo;
+ extraField = "count(1), count(files.playCount)";
+ extraJoin = PrepareSQL("JOIN files ON files.idFile = %s_view.idFile", view.c_str());
+ if (bMainArtistOnly)
+ extraJoin =
+ extraJoin +
+ PrepareSQL(" WHERE actor.name IN (SELECT musicvideo_view.c10 from musicvideo_view)");
+ }
+ else
+ return false;
+
+ strSQL = "SELECT {} FROM actor ";
+ extFilter.fields = "actor.actor_id, actor.name, actor.art_urls";
+ extFilter.AppendField(extraField);
+ extFilter.AppendJoin(PrepareSQL("JOIN %s_link on actor.actor_id = %s_link.actor_id", type, type));
+ extFilter.AppendJoin(PrepareSQL("JOIN %s_view on %s_link.media_id = %s_view.%s AND %s_link.media_type='%s'", view.c_str(), type, view.c_str(), view_id.c_str(), type, media_type.c_str()));
+ extFilter.AppendJoin(extraJoin);
+ extFilter.AppendGroup("actor.actor_id");
+ }
+
+ if (countOnly)
+ {
+ extFilter.fields = "COUNT(1)";
+ extFilter.group.clear();
+ extFilter.order.clear();
+ }
+ strSQL = StringUtils::Format(strSQL, !extFilter.fields.empty() ? extFilter.fields : "*");
+
+ CVideoDbUrl videoUrl;
+ if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, videoUrl))
+ return false;
+
+ // run query
+ auto start = std::chrono::steady_clock::now();
+
+ if (!m_pDS->query(strSQL)) return false;
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{} - query took {} ms", __FUNCTION__, duration.count());
+
+ start = std::chrono::steady_clock::now();
+
+ int iRowsFound = m_pDS->num_rows();
+ if (iRowsFound == 0)
+ {
+ m_pDS->close();
+ return true;
+ }
+
+ if (countOnly)
+ {
+ CFileItemPtr pItem(new CFileItem());
+ pItem->SetProperty("total", iRowsFound == 1 ? m_pDS->fv(0).get_asInt() : iRowsFound);
+ items.Add(pItem);
+
+ m_pDS->close();
+ return true;
+ }
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ {
+ std::map<int, CActor> mapActors;
+
+ while (!m_pDS->eof())
+ {
+ int idActor = m_pDS->fv(0).get_asInt();
+ CActor actor;
+ actor.name = m_pDS->fv(1).get_asString();
+ actor.thumb = m_pDS->fv(2).get_asString();
+ if (idContent != VideoDbContentType::TVSHOWS &&
+ idContent != VideoDbContentType::MUSICVIDEOS)
+ {
+ actor.playcount = m_pDS->fv(3).get_asInt();
+ actor.appearances = 1;
+ }
+ else actor.appearances = m_pDS->fv(4).get_asInt();
+ auto it = mapActors.find(idActor);
+ // is this actor already known?
+ if (it == mapActors.end())
+ {
+ // check path
+ if (g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ mapActors.insert(std::pair<int, CActor>(idActor, actor));
+ }
+ else if (idContent != VideoDbContentType::TVSHOWS &&
+ idContent != VideoDbContentType::MUSICVIDEOS)
+ it->second.appearances++;
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ for (const auto &i : mapActors)
+ {
+ CFileItemPtr pItem(new CFileItem(i.second.name));
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string path = StringUtils::Format("{}/", i.first);
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder=true;
+ pItem->GetVideoInfoTag()->SetPlayCount(i.second.playcount);
+ pItem->GetVideoInfoTag()->m_strPictureURL.ParseFromData(i.second.thumb);
+ pItem->GetVideoInfoTag()->m_iDbId = i.first;
+ pItem->GetVideoInfoTag()->m_type = type;
+ pItem->GetVideoInfoTag()->m_relevance = i.second.appearances;
+ if (idContent == VideoDbContentType::MUSICVIDEOS)
+ {
+ // Get artist bio from music db later if available
+ pItem->GetVideoInfoTag()->m_artist.emplace_back(i.second.name);
+ pItem->SetProperty("musicvideomediatype", MediaTypeArtist);
+ }
+ items.Add(pItem);
+ }
+ }
+ else
+ {
+ while (!m_pDS->eof())
+ {
+ try
+ {
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string path = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder=true;
+ pItem->GetVideoInfoTag()->m_strPictureURL.ParseFromData(m_pDS->fv(2).get_asString());
+ pItem->GetVideoInfoTag()->m_iDbId = m_pDS->fv(0).get_asInt();
+ pItem->GetVideoInfoTag()->m_type = type;
+ if (idContent != VideoDbContentType::TVSHOWS)
+ {
+ // fv(4) is the number of videos watched, fv(3) is the total number. We set the playcount
+ // only if the number of videos watched is equal to the total number (i.e. every video watched)
+ pItem->GetVideoInfoTag()->SetPlayCount((m_pDS->fv(4).get_asInt() == m_pDS->fv(3).get_asInt()) ? 1 : 0);
+ }
+ pItem->GetVideoInfoTag()->m_relevance = m_pDS->fv(3).get_asInt();
+ if (idContent == VideoDbContentType::MUSICVIDEOS)
+ {
+ pItem->GetVideoInfoTag()->m_artist.emplace_back(pItem->GetLabel());
+ pItem->SetProperty("musicvideomediatype", MediaTypeArtist);
+ }
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{}: out of memory - retrieved {} items", __FUNCTION__, items.Size());
+ return items.Size() > 0;
+ }
+ }
+ m_pDS->close();
+ }
+
+ end = std::chrono::steady_clock::now();
+ duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{} item retrieval took {} ms", __FUNCTION__,
+ duration.count());
+
+ return true;
+ }
+ catch (...)
+ {
+ m_pDS->close();
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetYearsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent /* = UNKNOWN */,
+ const Filter& filter /* = Filter() */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string strSQL;
+ Filter extFilter = filter;
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ {
+ if (idContent == VideoDbContentType::MOVIES)
+ {
+ strSQL = "select movie_view.premiered, path.strPath, files.playCount from movie_view ";
+ extFilter.AppendJoin("join files on files.idFile = movie_view.idFile join path on files.idPath = path.idPath");
+ }
+ else if (idContent == VideoDbContentType::TVSHOWS)
+ {
+ strSQL = PrepareSQL("select tvshow_view.c%02d, path.strPath from tvshow_view ", VIDEODB_ID_TV_PREMIERED);
+ extFilter.AppendJoin("join episode_view on episode_view.idShow = tvshow_view.idShow join files on files.idFile = episode_view.idFile join path on files.idPath = path.idPath");
+ }
+ else if (idContent == VideoDbContentType::MUSICVIDEOS)
+ {
+ strSQL = "select musicvideo_view.premiered, path.strPath, files.playCount from musicvideo_view ";
+ extFilter.AppendJoin("join files on files.idFile = musicvideo_view.idFile join path on files.idPath = path.idPath");
+ }
+ else
+ return false;
+ }
+ else
+ {
+ std::string group;
+ if (idContent == VideoDbContentType::MOVIES)
+ {
+ strSQL = "select movie_view.premiered, count(1), count(files.playCount) from movie_view ";
+ extFilter.AppendJoin("join files on files.idFile = movie_view.idFile");
+ extFilter.AppendGroup("movie_view.premiered");
+ }
+ else if (idContent == VideoDbContentType::TVSHOWS)
+ {
+ strSQL = PrepareSQL("select distinct tvshow_view.c%02d from tvshow_view", VIDEODB_ID_TV_PREMIERED);
+ extFilter.AppendGroup(PrepareSQL("tvshow_view.c%02d", VIDEODB_ID_TV_PREMIERED));
+ }
+ else if (idContent == VideoDbContentType::MUSICVIDEOS)
+ {
+ strSQL = "select musicvideo_view.premiered, count(1), count(files.playCount) from musicvideo_view ";
+ extFilter.AppendJoin("join files on files.idFile = musicvideo_view.idFile");
+ extFilter.AppendGroup("musicvideo_view.premiered");
+ }
+ else
+ return false;
+ }
+
+ CVideoDbUrl videoUrl;
+ if (!BuildSQL(strBaseDir, strSQL, extFilter, strSQL, videoUrl))
+ return false;
+
+ int iRowsFound = RunQuery(strSQL);
+ if (iRowsFound <= 0)
+ return iRowsFound == 0;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ {
+ std::map<int, std::pair<std::string,int> > mapYears;
+ while (!m_pDS->eof())
+ {
+ int lYear = 0;
+ std::string dateString = m_pDS->fv(0).get_asString();
+ if (dateString.size() == 4)
+ lYear = m_pDS->fv(0).get_asInt();
+ else
+ {
+ CDateTime time;
+ time.SetFromDateString(dateString);
+ if (time.IsValid())
+ lYear = time.GetYear();
+ }
+ auto it = mapYears.find(lYear);
+ if (it == mapYears.end())
+ {
+ // check path
+ if (g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ std::string year = std::to_string(lYear);
+ if (idContent == VideoDbContentType::MOVIES ||
+ idContent == VideoDbContentType::MUSICVIDEOS)
+ mapYears.insert(std::pair<int, std::pair<std::string,int> >(lYear, std::pair<std::string,int>(year,m_pDS->fv(2).get_asInt())));
+ else
+ mapYears.insert(std::pair<int, std::pair<std::string,int> >(lYear, std::pair<std::string,int>(year,0)));
+ }
+ }
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ for (const auto &i : mapYears)
+ {
+ if (i.first == 0)
+ continue;
+ CFileItemPtr pItem(new CFileItem(i.second.first));
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string path = StringUtils::Format("{}/", i.first);
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder=true;
+ if (idContent == VideoDbContentType::MOVIES || idContent == VideoDbContentType::MUSICVIDEOS)
+ pItem->GetVideoInfoTag()->SetPlayCount(i.second.second);
+ items.Add(pItem);
+ }
+ }
+ else
+ {
+ while (!m_pDS->eof())
+ {
+ int lYear = 0;
+ std::string strLabel = m_pDS->fv(0).get_asString();
+ if (strLabel.size() == 4)
+ lYear = m_pDS->fv(0).get_asInt();
+ else
+ {
+ CDateTime time;
+ time.SetFromDateString(strLabel);
+ if (time.IsValid())
+ {
+ lYear = time.GetYear();
+ strLabel = std::to_string(lYear);
+ }
+ }
+ if (lYear == 0)
+ {
+ m_pDS->next();
+ continue;
+ }
+ CFileItemPtr pItem(new CFileItem(strLabel));
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string path = StringUtils::Format("{}/", lYear);
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder=true;
+ if (idContent == VideoDbContentType::MOVIES || idContent == VideoDbContentType::MUSICVIDEOS)
+ {
+ // fv(2) is the number of videos watched, fv(1) is the total number. We set the playcount
+ // only if the number of videos watched is equal to the total number (i.e. every video watched)
+ pItem->GetVideoInfoTag()->SetPlayCount((m_pDS->fv(2).get_asInt() == m_pDS->fv(1).get_asInt()) ? 1 : 0);
+ }
+
+ // take care of dupes ..
+ if (!items.Contains(pItem->GetPath()))
+ items.Add(pItem);
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetSeasonsNav(const std::string& strBaseDir, CFileItemList& items, int idActor, int idDirector, int idGenre, int idYear, int idShow, bool getLinkedMovies /* = true */)
+{
+ // parse the base path to get additional filters
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(strBaseDir))
+ return false;
+
+ if (idShow != -1)
+ videoUrl.AddOption("tvshowid", idShow);
+ if (idActor != -1)
+ videoUrl.AddOption("actorid", idActor);
+ else if (idDirector != -1)
+ videoUrl.AddOption("directorid", idDirector);
+ else if (idGenre != -1)
+ videoUrl.AddOption("genreid", idGenre);
+ else if (idYear != -1)
+ videoUrl.AddOption("year", idYear);
+
+ if (!GetSeasonsByWhere(videoUrl.ToString(), Filter(), items, false))
+ return false;
+
+ // now add any linked movies
+ if (getLinkedMovies && idShow != -1)
+ {
+ Filter movieFilter;
+ movieFilter.join = PrepareSQL("join movielinktvshow on movielinktvshow.idMovie=movie_view.idMovie");
+ movieFilter.where = PrepareSQL("movielinktvshow.idShow = %i", idShow);
+ CFileItemList movieItems;
+ GetMoviesByWhere("videodb://movies/titles/", movieFilter, movieItems);
+
+ if (movieItems.Size() > 0)
+ items.Append(movieItems);
+ }
+
+ return true;
+}
+
+bool CVideoDatabase::GetSeasonsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool appendFullShowPath /* = true */, const SortDescription &sortDescription /* = SortDescription() */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ int total = -1;
+
+ std::string strSQL = "SELECT %s FROM season_view ";
+ CVideoDbUrl videoUrl;
+ std::string strSQLExtra;
+ Filter extFilter = filter;
+ SortDescription sorting = sortDescription;
+ if (!BuildSQL(strBaseDir, strSQLExtra, extFilter, strSQLExtra, videoUrl, sorting))
+ return false;
+
+ // Apply the limiting directly here if there's no special sorting but limiting
+ if (extFilter.limit.empty() && sorting.sortBy == SortByNone &&
+ (sorting.limitStart > 0 || sorting.limitEnd > 0 ||
+ (sorting.limitStart == 0 && sorting.limitEnd == 0)))
+ {
+ total = (int)strtol(GetSingleValue(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS).c_str(), NULL, 10);
+ strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
+ }
+
+ strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : "*") + strSQLExtra;
+
+ int iRowsFound = RunQuery(strSQL);
+
+ // store the total value of items as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ if (iRowsFound <= 0)
+ return iRowsFound == 0;
+
+ std::set<std::pair<int, int>> mapSeasons;
+ while (!m_pDS->eof())
+ {
+ int id = m_pDS->fv(VIDEODB_ID_SEASON_ID).get_asInt();
+ int showId = m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_ID).get_asInt();
+ int iSeason = m_pDS->fv(VIDEODB_ID_SEASON_NUMBER).get_asInt();
+ std::string name = m_pDS->fv(VIDEODB_ID_SEASON_NAME).get_asString();
+ std::string path = m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_PATH).get_asString();
+
+ if (mapSeasons.find(std::make_pair(showId, iSeason)) == mapSeasons.end() &&
+ (m_profileManager.GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE || g_passwordManager.bMasterUser ||
+ g_passwordManager.IsDatabasePathUnlocked(path, *CMediaSourceSettings::GetInstance().GetSources("video"))))
+ {
+ mapSeasons.insert(std::make_pair(showId, iSeason));
+
+ std::string strLabel = name;
+ if (strLabel.empty())
+ {
+ if (iSeason == 0)
+ strLabel = g_localizeStrings.Get(20381);
+ else
+ strLabel = StringUtils::Format(g_localizeStrings.Get(20358), iSeason);
+ }
+ CFileItemPtr pItem(new CFileItem(strLabel));
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string strDir;
+ if (appendFullShowPath)
+ strDir += StringUtils::Format("{}/", showId);
+ strDir += StringUtils::Format("{}/", iSeason);
+ itemUrl.AppendPath(strDir);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->m_bIsFolder = true;
+ pItem->GetVideoInfoTag()->m_strTitle = strLabel;
+ if (!name.empty())
+ pItem->GetVideoInfoTag()->m_strSortTitle = name;
+ pItem->GetVideoInfoTag()->m_iSeason = iSeason;
+ pItem->GetVideoInfoTag()->m_iDbId = id;
+ pItem->GetVideoInfoTag()->m_iIdSeason = id;
+ pItem->GetVideoInfoTag()->m_type = MediaTypeSeason;
+ pItem->GetVideoInfoTag()->m_strPath = path;
+ pItem->GetVideoInfoTag()->m_strShowTitle = m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_TITLE).get_asString();
+ pItem->GetVideoInfoTag()->m_strPlot = m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_PLOT).get_asString();
+ pItem->GetVideoInfoTag()->SetPremieredFromDBDate(m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_PREMIERED).get_asString());
+ pItem->GetVideoInfoTag()->m_firstAired.SetFromDBDate(m_pDS->fv(VIDEODB_ID_SEASON_PREMIERED).get_asString());
+ pItem->GetVideoInfoTag()->m_iUserRating = m_pDS->fv(VIDEODB_ID_SEASON_USER_RATING).get_asInt();
+ // season premiered date based on first episode airdate associated to the season
+ // tvshow premiered date is used as a fallback
+ if (pItem->GetVideoInfoTag()->m_firstAired.IsValid())
+ pItem->GetVideoInfoTag()->SetPremiered(pItem->GetVideoInfoTag()->m_firstAired);
+ else if (pItem->GetVideoInfoTag()->HasPremiered())
+ pItem->GetVideoInfoTag()->SetPremiered(pItem->GetVideoInfoTag()->GetPremiered());
+ else if (pItem->GetVideoInfoTag()->HasYear())
+ pItem->GetVideoInfoTag()->SetYear(pItem->GetVideoInfoTag()->GetYear());
+ pItem->GetVideoInfoTag()->m_genre = StringUtils::Split(m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_GENRE).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ pItem->GetVideoInfoTag()->m_studio = StringUtils::Split(m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_STUDIO).get_asString(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ pItem->GetVideoInfoTag()->m_strMPAARating = m_pDS->fv(VIDEODB_ID_SEASON_TVSHOW_MPAA).get_asString();
+ pItem->GetVideoInfoTag()->m_iIdShow = showId;
+
+ const int totalEpisodes = m_pDS->fv(VIDEODB_ID_SEASON_EPISODES_TOTAL).get_asInt();
+ const int watchedEpisodes = m_pDS->fv(VIDEODB_ID_SEASON_EPISODES_WATCHED).get_asInt();
+ pItem->GetVideoInfoTag()->m_iEpisode = totalEpisodes;
+ pItem->SetProperty("totalepisodes", totalEpisodes);
+ pItem->SetProperty("numepisodes", totalEpisodes); // will be changed later to reflect watchmode setting
+ pItem->SetProperty("watchedepisodes", watchedEpisodes);
+ pItem->SetProperty("unwatchedepisodes", totalEpisodes - watchedEpisodes);
+ pItem->SetProperty("watchedepisodepercent",
+ totalEpisodes > 0 ? (watchedEpisodes * 100 / totalEpisodes) : 0);
+ if (iSeason == 0)
+ pItem->SetProperty("isspecial", true);
+ pItem->GetVideoInfoTag()->SetPlayCount((totalEpisodes == watchedEpisodes) ? 1 : 0);
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, (pItem->GetVideoInfoTag()->GetPlayCount() > 0) && (pItem->GetVideoInfoTag()->m_iEpisode > 0));
+
+ items.Add(pItem);
+ }
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetSortedVideos(const MediaType &mediaType, const std::string& strBaseDir, const SortDescription &sortDescription, CFileItemList& items, const Filter &filter /* = Filter() */)
+{
+ if (nullptr == m_pDB || nullptr == m_pDS)
+ return false;
+
+ if (mediaType != MediaTypeMovie && mediaType != MediaTypeTvShow && mediaType != MediaTypeEpisode && mediaType != MediaTypeMusicVideo)
+ return false;
+
+ SortDescription sorting = sortDescription;
+ if (sortDescription.sortBy == SortByFile || sortDescription.sortBy == SortByTitle ||
+ sortDescription.sortBy == SortBySortTitle || sortDescription.sortBy == SortByOriginalTitle ||
+ sortDescription.sortBy == SortByLabel || sortDescription.sortBy == SortByDateAdded ||
+ sortDescription.sortBy == SortByRating || sortDescription.sortBy == SortByUserRating ||
+ sortDescription.sortBy == SortByYear || sortDescription.sortBy == SortByLastPlayed ||
+ sortDescription.sortBy == SortByPlaycount)
+ sorting.sortAttributes = (SortAttribute)(sortDescription.sortAttributes | SortAttributeIgnoreFolders);
+
+ bool success = false;
+ if (mediaType == MediaTypeMovie)
+ success = GetMoviesByWhere(strBaseDir, filter, items, sorting);
+ else if (mediaType == MediaTypeTvShow)
+ success = GetTvShowsByWhere(strBaseDir, filter, items, sorting);
+ else if (mediaType == MediaTypeEpisode)
+ success = GetEpisodesByWhere(strBaseDir, filter, items, true, sorting);
+ else if (mediaType == MediaTypeMusicVideo)
+ success = GetMusicVideosByWhere(strBaseDir, filter, items, true, sorting);
+ else
+ return false;
+
+ items.SetContent(CMediaTypes::ToPlural(mediaType));
+ return success;
+}
+
+bool CVideoDatabase::GetItems(const std::string &strBaseDir, CFileItemList &items, const Filter &filter /* = Filter() */, const SortDescription &sortDescription /* = SortDescription() */)
+{
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(strBaseDir))
+ return false;
+
+ return GetItems(strBaseDir, videoUrl.GetType(), videoUrl.GetItemType(), items, filter, sortDescription);
+}
+
+bool CVideoDatabase::GetItems(const std::string &strBaseDir, const std::string &mediaType, const std::string &itemType, CFileItemList &items, const Filter &filter /* = Filter() */, const SortDescription &sortDescription /* = SortDescription() */)
+{
+ VideoDbContentType contentType;
+ if (StringUtils::EqualsNoCase(mediaType, "movies"))
+ contentType = VideoDbContentType::MOVIES;
+ else if (StringUtils::EqualsNoCase(mediaType, "tvshows"))
+ {
+ if (StringUtils::EqualsNoCase(itemType, "episodes"))
+ contentType = VideoDbContentType::EPISODES;
+ else
+ contentType = VideoDbContentType::TVSHOWS;
+ }
+ else if (StringUtils::EqualsNoCase(mediaType, "musicvideos"))
+ contentType = VideoDbContentType::MUSICVIDEOS;
+ else
+ return false;
+
+ return GetItems(strBaseDir, contentType, itemType, items, filter, sortDescription);
+}
+
+bool CVideoDatabase::GetItems(const std::string& strBaseDir,
+ VideoDbContentType mediaType,
+ const std::string& itemType,
+ CFileItemList& items,
+ const Filter& filter /* = Filter() */,
+ const SortDescription& sortDescription /* = SortDescription() */)
+{
+ if (StringUtils::EqualsNoCase(itemType, "movies") &&
+ (mediaType == VideoDbContentType::MOVIES || mediaType == VideoDbContentType::MOVIE_SETS))
+ return GetMoviesByWhere(strBaseDir, filter, items, sortDescription);
+ else if (StringUtils::EqualsNoCase(itemType, "tvshows") &&
+ mediaType == VideoDbContentType::TVSHOWS)
+ {
+ Filter extFilter = filter;
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->
+ GetBool(CSettings::SETTING_VIDEOLIBRARY_SHOWEMPTYTVSHOWS))
+ extFilter.AppendWhere("totalCount IS NOT NULL AND totalCount > 0");
+ return GetTvShowsByWhere(strBaseDir, extFilter, items, sortDescription);
+ }
+ else if (StringUtils::EqualsNoCase(itemType, "musicvideos") &&
+ mediaType == VideoDbContentType::MUSICVIDEOS)
+ return GetMusicVideosByWhere(strBaseDir, filter, items, true, sortDescription);
+ else if (StringUtils::EqualsNoCase(itemType, "episodes") &&
+ mediaType == VideoDbContentType::EPISODES)
+ return GetEpisodesByWhere(strBaseDir, filter, items, true, sortDescription);
+ else if (StringUtils::EqualsNoCase(itemType, "seasons") &&
+ mediaType == VideoDbContentType::TVSHOWS)
+ return GetSeasonsNav(strBaseDir, items);
+ else if (StringUtils::EqualsNoCase(itemType, "genres"))
+ return GetGenresNav(strBaseDir, items, mediaType, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "years"))
+ return GetYearsNav(strBaseDir, items, mediaType, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "actors"))
+ return GetActorsNav(strBaseDir, items, mediaType, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "directors"))
+ return GetDirectorsNav(strBaseDir, items, mediaType, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "writers"))
+ return GetWritersNav(strBaseDir, items, mediaType, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "studios"))
+ return GetStudiosNav(strBaseDir, items, mediaType, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "sets"))
+ return GetSetsNav(strBaseDir, items, mediaType, filter, !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_GROUPSINGLEITEMSETS));
+ else if (StringUtils::EqualsNoCase(itemType, "countries"))
+ return GetCountriesNav(strBaseDir, items, mediaType, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "tags"))
+ return GetTagsNav(strBaseDir, items, mediaType, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "artists") &&
+ mediaType == VideoDbContentType::MUSICVIDEOS)
+ return GetActorsNav(strBaseDir, items, mediaType, filter);
+ else if (StringUtils::EqualsNoCase(itemType, "albums") &&
+ mediaType == VideoDbContentType::MUSICVIDEOS)
+ return GetMusicVideoAlbumsNav(strBaseDir, items, -1, filter);
+
+ return false;
+}
+
+std::string CVideoDatabase::GetItemById(const std::string &itemType, int id)
+{
+ if (StringUtils::EqualsNoCase(itemType, "genres"))
+ return GetGenreById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "years"))
+ return std::to_string(id);
+ else if (StringUtils::EqualsNoCase(itemType, "actors") ||
+ StringUtils::EqualsNoCase(itemType, "directors") ||
+ StringUtils::EqualsNoCase(itemType, "artists"))
+ return GetPersonById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "studios"))
+ return GetStudioById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "sets"))
+ return GetSetById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "countries"))
+ return GetCountryById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "tags"))
+ return GetTagById(id);
+ else if (StringUtils::EqualsNoCase(itemType, "albums"))
+ return GetMusicVideoAlbumById(id);
+
+ return "";
+}
+
+bool CVideoDatabase::GetMoviesNav(const std::string& strBaseDir, CFileItemList& items,
+ int idGenre /* = -1 */, int idYear /* = -1 */, int idActor /* = -1 */, int idDirector /* = -1 */,
+ int idStudio /* = -1 */, int idCountry /* = -1 */, int idSet /* = -1 */, int idTag /* = -1 */,
+ const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */)
+{
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(strBaseDir))
+ return false;
+
+ if (idGenre > 0)
+ videoUrl.AddOption("genreid", idGenre);
+ else if (idCountry > 0)
+ videoUrl.AddOption("countryid", idCountry);
+ else if (idStudio > 0)
+ videoUrl.AddOption("studioid", idStudio);
+ else if (idDirector > 0)
+ videoUrl.AddOption("directorid", idDirector);
+ else if (idYear > 0)
+ videoUrl.AddOption("year", idYear);
+ else if (idActor > 0)
+ videoUrl.AddOption("actorid", idActor);
+ else if (idSet > 0)
+ videoUrl.AddOption("setid", idSet);
+ else if (idTag > 0)
+ videoUrl.AddOption("tagid", idTag);
+
+ Filter filter;
+ return GetMoviesByWhere(videoUrl.ToString(), filter, items, sortDescription, getDetails);
+}
+
+bool CVideoDatabase::GetMoviesByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ // parse the base path to get additional filters
+ CVideoDbUrl videoUrl;
+ Filter extFilter = filter;
+ SortDescription sorting = sortDescription;
+ if (!videoUrl.FromString(strBaseDir) || !GetFilter(videoUrl, extFilter, sorting))
+ return false;
+
+ int total = -1;
+
+ std::string strSQL = "select %s from movie_view ";
+ std::string strSQLExtra;
+ if (!CDatabase::BuildSQL(strSQLExtra, extFilter, strSQLExtra))
+ return false;
+
+ // Apply the limiting directly here if there's no special sorting but limiting
+ if (extFilter.limit.empty() && sorting.sortBy == SortByNone &&
+ (sorting.limitStart > 0 || sorting.limitEnd > 0 ||
+ (sorting.limitStart == 0 && sorting.limitEnd == 0)))
+ {
+ total = (int)strtol(GetSingleValue(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS).c_str(), NULL, 10);
+ strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
+ }
+
+ strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : "*") + strSQLExtra;
+
+ int iRowsFound = RunQuery(strSQL);
+
+ // store the total value of items as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ if (iRowsFound <= 0)
+ return iRowsFound == 0;
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+
+ if (!SortUtils::SortFromDataset(sortDescription, MediaTypeMovie, m_pDS, results))
+ return false;
+
+ // get data from returned rows
+ items.Reserve(results.size());
+ const query_data &data = m_pDS->get_result_set().records;
+ for (const auto &i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ CVideoInfoTag movie = GetDetailsForMovie(record, getDetails);
+ if (m_profileManager.GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE ||
+ g_passwordManager.bMasterUser ||
+ g_passwordManager.IsDatabasePathUnlocked(movie.m_strPath, *CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ CFileItemPtr pItem(new CFileItem(movie));
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string path = std::to_string(movie.m_iDbId);
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+ pItem->SetDynPath(movie.m_strFileNameAndPath);
+
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED,movie.GetPlayCount() > 0);
+ items.Add(pItem);
+ }
+ }
+
+ // cleanup
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetTvShowsNav(const std::string& strBaseDir, CFileItemList& items,
+ int idGenre /* = -1 */, int idYear /* = -1 */, int idActor /* = -1 */, int idDirector /* = -1 */, int idStudio /* = -1 */, int idTag /* = -1 */,
+ const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */)
+{
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(strBaseDir))
+ return false;
+
+ if (idGenre != -1)
+ videoUrl.AddOption("genreid", idGenre);
+ else if (idStudio != -1)
+ videoUrl.AddOption("studioid", idStudio);
+ else if (idDirector != -1)
+ videoUrl.AddOption("directorid", idDirector);
+ else if (idYear != -1)
+ videoUrl.AddOption("year", idYear);
+ else if (idActor != -1)
+ videoUrl.AddOption("actorid", idActor);
+ else if (idTag != -1)
+ videoUrl.AddOption("tagid", idTag);
+
+ Filter filter;
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_SHOWEMPTYTVSHOWS))
+ filter.AppendWhere("totalCount IS NOT NULL AND totalCount > 0");
+ return GetTvShowsByWhere(videoUrl.ToString(), filter, items, sortDescription, getDetails);
+}
+
+bool CVideoDatabase::GetTvShowsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ int total = -1;
+
+ std::string strSQL = "SELECT %s FROM tvshow_view ";
+ CVideoDbUrl videoUrl;
+ std::string strSQLExtra;
+ Filter extFilter = filter;
+ SortDescription sorting = sortDescription;
+ if (!BuildSQL(strBaseDir, strSQLExtra, extFilter, strSQLExtra, videoUrl, sorting))
+ return false;
+
+ // Apply the limiting directly here if there's no special sorting but limiting
+ if (extFilter.limit.empty() && sorting.sortBy == SortByNone &&
+ (sorting.limitStart > 0 || sorting.limitEnd > 0 ||
+ (sorting.limitStart == 0 && sorting.limitEnd == 0)))
+ {
+ total = (int)strtol(GetSingleValue(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS).c_str(), NULL, 10);
+ strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
+ }
+
+ strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : "*") + strSQLExtra;
+
+ int iRowsFound = RunQuery(strSQL);
+
+ // store the total value of items as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ if (iRowsFound <= 0)
+ return iRowsFound == 0;
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+ if (!SortUtils::SortFromDataset(sorting, MediaTypeTvShow, m_pDS, results))
+ return false;
+
+ // get data from returned rows
+ items.Reserve(results.size());
+ const query_data &data = m_pDS->get_result_set().records;
+ for (const auto &i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ CFileItemPtr pItem(new CFileItem());
+ CVideoInfoTag movie = GetDetailsForTvShow(record, getDetails, pItem.get());
+ if (m_profileManager.GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE ||
+ g_passwordManager.bMasterUser ||
+ g_passwordManager.IsDatabasePathUnlocked(movie.m_strPath, *CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ pItem->SetFromVideoInfoTag(movie);
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string path = StringUtils::Format("{}/", record->at(0).get_asInt());
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, (pItem->GetVideoInfoTag()->GetPlayCount() > 0) && (pItem->GetVideoInfoTag()->m_iEpisode > 0));
+ items.Add(pItem);
+ }
+ }
+
+ // cleanup
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetEpisodesNav(const std::string& strBaseDir, CFileItemList& items, int idGenre, int idYear, int idActor, int idDirector, int idShow, int idSeason, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */)
+{
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(strBaseDir))
+ return false;
+
+ if (idShow != -1)
+ {
+ videoUrl.AddOption("tvshowid", idShow);
+ if (idSeason >= 0)
+ videoUrl.AddOption("season", idSeason);
+
+ if (idGenre != -1)
+ videoUrl.AddOption("genreid", idGenre);
+ else if (idYear !=-1)
+ videoUrl.AddOption("year", idYear);
+ else if (idActor != -1)
+ videoUrl.AddOption("actorid", idActor);
+ }
+ else if (idYear != -1)
+ videoUrl.AddOption("year", idYear);
+
+ if (idDirector != -1)
+ videoUrl.AddOption("directorid", idDirector);
+
+ Filter filter;
+ bool ret = GetEpisodesByWhere(videoUrl.ToString(), filter, items, false, sortDescription, getDetails);
+
+ if (idSeason == -1 && idShow != -1)
+ { // add any linked movies
+ Filter movieFilter;
+ movieFilter.join = PrepareSQL("join movielinktvshow on movielinktvshow.idMovie=movie_view.idMovie");
+ movieFilter.where = PrepareSQL("movielinktvshow.idShow = %i", idShow);
+ CFileItemList movieItems;
+ GetMoviesByWhere("videodb://movies/titles/", movieFilter, movieItems);
+
+ if (movieItems.Size() > 0)
+ items.Append(movieItems);
+ }
+
+ return ret;
+}
+
+bool CVideoDatabase::GetEpisodesByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool appendFullShowPath /* = true */, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ int total = -1;
+
+ std::string strSQL = "select %s from episode_view ";
+ CVideoDbUrl videoUrl;
+ std::string strSQLExtra;
+ Filter extFilter = filter;
+ SortDescription sorting = sortDescription;
+ if (!BuildSQL(strBaseDir, strSQLExtra, extFilter, strSQLExtra, videoUrl, sorting))
+ return false;
+
+ // Apply the limiting directly here if there's no special sorting but limiting
+ if (extFilter.limit.empty() && sorting.sortBy == SortByNone &&
+ (sorting.limitStart > 0 || sorting.limitEnd > 0 ||
+ (sorting.limitStart == 0 && sorting.limitEnd == 0)))
+ {
+ total = (int)strtol(GetSingleValue(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS).c_str(), NULL, 10);
+ strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
+ }
+
+ strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : "*") + strSQLExtra;
+
+ int iRowsFound = RunQuery(strSQL);
+
+ // store the total value of items as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ if (iRowsFound <= 0)
+ return iRowsFound == 0;
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+ if (!SortUtils::SortFromDataset(sorting, MediaTypeEpisode, m_pDS, results))
+ return false;
+
+ // get data from returned rows
+ items.Reserve(results.size());
+ CLabelFormatter formatter("%H. %T", "");
+
+ const query_data &data = m_pDS->get_result_set().records;
+ for (const auto &i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ CVideoInfoTag episode = GetDetailsForEpisode(record, getDetails);
+ if (m_profileManager.GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE ||
+ g_passwordManager.bMasterUser ||
+ g_passwordManager.IsDatabasePathUnlocked(episode.m_strPath, *CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ CFileItemPtr pItem(new CFileItem(episode));
+ formatter.FormatLabel(pItem.get());
+
+ int idEpisode = record->at(0).get_asInt();
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string path;
+ if (appendFullShowPath && videoUrl.GetItemType() != "episodes")
+ path = StringUtils::Format("{}/{}/{}",
+ record->at(VIDEODB_DETAILS_EPISODE_TVSHOW_ID).get_asInt(),
+ episode.m_iSeason, idEpisode);
+ else
+ path = std::to_string(idEpisode);
+ itemUrl.AppendPath(path);
+ pItem->SetPath(itemUrl.ToString());
+ pItem->SetDynPath(episode.m_strFileNameAndPath);
+
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, episode.GetPlayCount() > 0);
+ pItem->m_dateTime = episode.m_firstAired;
+ items.Add(pItem);
+ }
+ }
+
+ // cleanup
+ m_pDS->close();
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+bool CVideoDatabase::GetMusicVideosNav(const std::string& strBaseDir, CFileItemList& items, int idGenre, int idYear, int idArtist, int idDirector, int idStudio, int idAlbum, int idTag /* = -1 */, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */)
+{
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(strBaseDir))
+ return false;
+
+ if (idGenre != -1)
+ videoUrl.AddOption("genreid", idGenre);
+ else if (idStudio != -1)
+ videoUrl.AddOption("studioid", idStudio);
+ else if (idDirector != -1)
+ videoUrl.AddOption("directorid", idDirector);
+ else if (idYear !=-1)
+ videoUrl.AddOption("year", idYear);
+ else if (idArtist != -1)
+ videoUrl.AddOption("artistid", idArtist);
+ else if (idTag != -1)
+ videoUrl.AddOption("tagid", idTag);
+ if (idAlbum != -1)
+ videoUrl.AddOption("albumid", idAlbum);
+
+ Filter filter;
+ return GetMusicVideosByWhere(videoUrl.ToString(), filter, items, true, sortDescription, getDetails);
+}
+
+bool CVideoDatabase::GetRecentlyAddedMoviesNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit /* = 0 */, int getDetails /* = VideoDbDetailsNone */)
+{
+ Filter filter;
+ filter.order = "dateAdded desc, idMovie desc";
+ filter.limit = PrepareSQL("%u", limit ? limit : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iVideoLibraryRecentlyAddedItems);
+ return GetMoviesByWhere(strBaseDir, filter, items, SortDescription(), getDetails);
+}
+
+bool CVideoDatabase::GetRecentlyAddedEpisodesNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit /* = 0 */, int getDetails /* = VideoDbDetailsNone */)
+{
+ Filter filter;
+ filter.order = "dateAdded desc, idEpisode desc";
+ filter.limit = PrepareSQL("%u", limit ? limit : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iVideoLibraryRecentlyAddedItems);
+ return GetEpisodesByWhere(strBaseDir, filter, items, false, SortDescription(), getDetails);
+}
+
+bool CVideoDatabase::GetRecentlyAddedMusicVideosNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit /* = 0 */, int getDetails /* = VideoDbDetailsNone */)
+{
+ Filter filter;
+ filter.order = "dateAdded desc, idMVideo desc";
+ filter.limit = PrepareSQL("%u", limit ? limit : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iVideoLibraryRecentlyAddedItems);
+ return GetMusicVideosByWhere(strBaseDir, filter, items, true, SortDescription(), getDetails);
+}
+
+bool CVideoDatabase::GetInProgressTvShowsNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit /* = 0 */, int getDetails /* = VideoDbDetailsNone */)
+{
+ Filter filter;
+ filter.order = PrepareSQL("c%02d", VIDEODB_ID_TV_TITLE);
+ filter.where = "watchedCount != 0 AND totalCount != watchedCount";
+ return GetTvShowsByWhere(strBaseDir, filter, items, SortDescription(), getDetails);
+}
+
+std::string CVideoDatabase::GetGenreById(int id)
+{
+ return GetSingleValue("genre", "name", PrepareSQL("genre_id=%i", id));
+}
+
+std::string CVideoDatabase::GetCountryById(int id)
+{
+ return GetSingleValue("country", "name", PrepareSQL("country_id=%i", id));
+}
+
+std::string CVideoDatabase::GetSetById(int id)
+{
+ return GetSingleValue("sets", "strSet", PrepareSQL("idSet=%i", id));
+}
+
+std::string CVideoDatabase::GetTagById(int id)
+{
+ return GetSingleValue("tag", "name", PrepareSQL("tag_id = %i", id));
+}
+
+std::string CVideoDatabase::GetPersonById(int id)
+{
+ return GetSingleValue("actor", "name", PrepareSQL("actor_id=%i", id));
+}
+
+std::string CVideoDatabase::GetStudioById(int id)
+{
+ return GetSingleValue("studio", "name", PrepareSQL("studio_id=%i", id));
+}
+
+std::string CVideoDatabase::GetTvShowTitleById(int id)
+{
+ return GetSingleValue("tvshow", PrepareSQL("c%02d", VIDEODB_ID_TV_TITLE), PrepareSQL("idShow=%i", id));
+}
+
+std::string CVideoDatabase::GetMusicVideoAlbumById(int id)
+{
+ return GetSingleValue("musicvideo", PrepareSQL("c%02d", VIDEODB_ID_MUSICVIDEO_ALBUM), PrepareSQL("idMVideo=%i", id));
+}
+
+bool CVideoDatabase::HasSets() const
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ m_pDS->query("SELECT movie_view.idSet,COUNT(1) AS c FROM movie_view "
+ "JOIN sets ON sets.idSet = movie_view.idSet "
+ "GROUP BY movie_view.idSet HAVING c>1");
+
+ bool bResult = (m_pDS->num_rows() > 0);
+ m_pDS->close();
+ return bResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+int CVideoDatabase::GetTvShowForEpisode(int idEpisode)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS2)
+ return false;
+
+ // make sure we use m_pDS2, as this is called in loops using m_pDS
+ std::string strSQL=PrepareSQL("select idShow from episode where idEpisode=%i", idEpisode);
+ m_pDS2->query( strSQL );
+
+ int result=-1;
+ if (!m_pDS2->eof())
+ result=m_pDS2->fv(0).get_asInt();
+ m_pDS2->close();
+
+ return result;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, idEpisode);
+ }
+ return false;
+}
+
+int CVideoDatabase::GetSeasonForEpisode(int idEpisode)
+{
+ char column[5];
+ sprintf(column, "c%0d", VIDEODB_ID_EPISODE_SEASON);
+ std::string id = GetSingleValue("episode", column, PrepareSQL("idEpisode=%i", idEpisode));
+ if (id.empty())
+ return -1;
+ return atoi(id.c_str());
+}
+
+bool CVideoDatabase::HasContent()
+{
+ return (HasContent(VideoDbContentType::MOVIES) || HasContent(VideoDbContentType::TVSHOWS) ||
+ HasContent(VideoDbContentType::MUSICVIDEOS));
+}
+
+bool CVideoDatabase::HasContent(VideoDbContentType type)
+{
+ bool result = false;
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string sql;
+ if (type == VideoDbContentType::MOVIES)
+ sql = "select count(1) from movie";
+ else if (type == VideoDbContentType::TVSHOWS)
+ sql = "select count(1) from tvshow";
+ else if (type == VideoDbContentType::MUSICVIDEOS)
+ sql = "select count(1) from musicvideo";
+ m_pDS->query( sql );
+
+ if (!m_pDS->eof())
+ result = (m_pDS->fv(0).get_asInt() > 0);
+
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return result;
+}
+
+ScraperPtr CVideoDatabase::GetScraperForPath( const std::string& strPath )
+{
+ SScanSettings settings;
+ return GetScraperForPath(strPath, settings);
+}
+
+ScraperPtr CVideoDatabase::GetScraperForPath(const std::string& strPath, SScanSettings& settings)
+{
+ bool dummy;
+ return GetScraperForPath(strPath, settings, dummy);
+}
+
+ScraperPtr CVideoDatabase::GetScraperForPath(const std::string& strPath, SScanSettings& settings, bool& foundDirectly)
+{
+ foundDirectly = false;
+ try
+ {
+ if (strPath.empty() || !m_pDB || !m_pDS)
+ return ScraperPtr();
+
+ ScraperPtr scraper;
+ std::string strPath2;
+
+ if (URIUtils::IsMultiPath(strPath))
+ strPath2 = CMultiPathDirectory::GetFirstPath(strPath);
+ else
+ strPath2 = strPath;
+
+ std::string strPath1 = URIUtils::GetDirectory(strPath2);
+ int idPath = GetPathId(strPath1);
+
+ if (idPath > -1)
+ {
+ std::string strSQL = PrepareSQL(
+ "SELECT path.strContent, path.strScraper, path.scanRecursive, path.useFolderNames, "
+ "path.strSettings, path.noUpdate, path.exclude, path.allAudio FROM path WHERE idPath=%i",
+ idPath);
+ m_pDS->query( strSQL );
+ }
+
+ int iFound = 1;
+ CONTENT_TYPE content = CONTENT_NONE;
+ if (!m_pDS->eof())
+ { // path is stored in db
+
+ settings.m_allExtAudio = m_pDS->fv("path.allAudio").get_asBool();
+
+ if (m_pDS->fv("path.exclude").get_asBool())
+ {
+ settings.exclude = true;
+ m_pDS->close();
+ return ScraperPtr();
+ }
+ settings.exclude = false;
+
+ // try and ascertain scraper for this path
+ std::string strcontent = m_pDS->fv("path.strContent").get_asString();
+ StringUtils::ToLower(strcontent);
+ content = TranslateContent(strcontent);
+
+ //FIXME paths stored should not have empty strContent
+ //assert(content != CONTENT_NONE);
+ std::string scraperID = m_pDS->fv("path.strScraper").get_asString();
+
+ AddonPtr addon;
+ if (!scraperID.empty() &&
+ CServiceBroker::GetAddonMgr().GetAddon(scraperID, addon, ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ scraper = std::dynamic_pointer_cast<CScraper>(addon);
+ if (!scraper)
+ return ScraperPtr();
+
+ // store this path's content & settings
+ scraper->SetPathSettings(content, m_pDS->fv("path.strSettings").get_asString());
+ settings.parent_name = m_pDS->fv("path.useFolderNames").get_asBool();
+ settings.recurse = m_pDS->fv("path.scanRecursive").get_asInt();
+ settings.noupdate = m_pDS->fv("path.noUpdate").get_asBool();
+ }
+ }
+
+ if (content == CONTENT_NONE)
+ { // this path is not saved in db
+ // we must drill up until a scraper is configured
+ std::string strParent;
+ while (URIUtils::GetParentPath(strPath1, strParent))
+ {
+ iFound++;
+
+ std::string strSQL =
+ PrepareSQL("SELECT path.strContent, path.strScraper, path.scanRecursive, "
+ "path.useFolderNames, path.strSettings, path.noUpdate, path.exclude, "
+ "path.allAudio FROM path WHERE strPath='%s'",
+ strParent.c_str());
+ m_pDS->query(strSQL);
+
+ CONTENT_TYPE content = CONTENT_NONE;
+ if (!m_pDS->eof())
+ {
+ settings.m_allExtAudio = m_pDS->fv("path.allAudio").get_asBool();
+ std::string strcontent = m_pDS->fv("path.strContent").get_asString();
+ StringUtils::ToLower(strcontent);
+ if (m_pDS->fv("path.exclude").get_asBool())
+ {
+ settings.exclude = true;
+ scraper.reset();
+ m_pDS->close();
+ break;
+ }
+
+ content = TranslateContent(strcontent);
+
+ AddonPtr addon;
+ if (content != CONTENT_NONE &&
+ CServiceBroker::GetAddonMgr().GetAddon(m_pDS->fv("path.strScraper").get_asString(),
+ addon, ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ scraper = std::dynamic_pointer_cast<CScraper>(addon);
+ scraper->SetPathSettings(content, m_pDS->fv("path.strSettings").get_asString());
+ settings.parent_name = m_pDS->fv("path.useFolderNames").get_asBool();
+ settings.recurse = m_pDS->fv("path.scanRecursive").get_asInt();
+ settings.noupdate = m_pDS->fv("path.noUpdate").get_asBool();
+ settings.exclude = false;
+ break;
+ }
+ }
+ strPath1 = strParent;
+ }
+ }
+ m_pDS->close();
+
+ if (!scraper || scraper->Content() == CONTENT_NONE)
+ return ScraperPtr();
+
+ if (scraper->Content() == CONTENT_TVSHOWS)
+ {
+ settings.recurse = 0;
+ if(settings.parent_name) // single show
+ {
+ settings.parent_name_root = settings.parent_name = (iFound == 1);
+ }
+ else // show root
+ {
+ settings.parent_name_root = settings.parent_name = (iFound == 2);
+ }
+ }
+ else if (scraper->Content() == CONTENT_MOVIES)
+ {
+ settings.recurse = settings.recurse - (iFound-1);
+ settings.parent_name_root = settings.parent_name && (!settings.recurse || iFound > 1);
+ }
+ else if (scraper->Content() == CONTENT_MUSICVIDEOS)
+ {
+ settings.recurse = settings.recurse - (iFound-1);
+ settings.parent_name_root = settings.parent_name && (!settings.recurse || iFound > 1);
+ }
+ else
+ {
+ iFound = 0;
+ return ScraperPtr();
+ }
+ foundDirectly = (iFound == 1);
+ return scraper;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return ScraperPtr();
+}
+
+bool CVideoDatabase::GetUseAllExternalAudioForVideo(const std::string& videoPath)
+{
+ // Find longest configured source path for given video path
+ std::string strSQL = PrepareSQL("SELECT allAudio FROM path WHERE allAudio IS NOT NULL AND "
+ "instr('%s', strPath) = 1 ORDER BY length(strPath) DESC LIMIT 1",
+ videoPath.c_str());
+ m_pDS->query(strSQL);
+
+ if (!m_pDS->eof())
+ return m_pDS->fv("allAudio").get_asBool();
+
+ return false;
+}
+
+std::string CVideoDatabase::GetContentForPath(const std::string& strPath)
+{
+ SScanSettings settings;
+ bool foundDirectly = false;
+ ScraperPtr scraper = GetScraperForPath(strPath, settings, foundDirectly);
+ if (scraper)
+ {
+ if (scraper->Content() == CONTENT_TVSHOWS)
+ {
+ // check for episodes or seasons. Assumptions are:
+ // 1. if episodes are in the path then we're in episodes.
+ // 2. if no episodes are found, and content was set directly on this path, then we're in shows.
+ // 3. if no episodes are found, and content was not set directly on this path, we're in seasons (assumes tvshows/seasons/episodes)
+ std::string sql = "SELECT COUNT(*) FROM episode_view ";
+
+ if (foundDirectly)
+ sql += PrepareSQL("WHERE strPath = '%s'", strPath.c_str());
+ else
+ sql += PrepareSQL("WHERE strPath LIKE '%s%%'", strPath.c_str());
+
+ m_pDS->query( sql );
+ if (m_pDS->num_rows() && m_pDS->fv(0).get_asInt() > 0)
+ return "episodes";
+ return foundDirectly ? "tvshows" : "seasons";
+ }
+ return TranslateContent(scraper->Content());
+ }
+ return "";
+}
+
+void CVideoDatabase::GetMovieGenresByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL=PrepareSQL("SELECT genre.genre_id, genre.name, path.strPath FROM genre INNER JOIN genre_link ON genre_link.genre_id = genre.genre_id INNER JOIN movie ON (genre_link.media_type='movie' = genre_link.media_id=movie.idMovie) INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON path.idPath=files.idPath WHERE genre.name LIKE '%%%s%%'",strSearch.c_str());
+ else
+ strSQL=PrepareSQL("SELECT DISTINCT genre.genre_id, genre.name FROM genre INNER JOIN genre_link ON genre_link.genre_id=genre.genre_id WHERE genre_link.media_type='movie' AND name LIKE '%%%s%%'", strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),
+ *CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ pItem->SetPath("videodb://movies/genres/"+ strDir);
+ pItem->m_bIsFolder=true;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetMovieCountriesByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL=PrepareSQL("SELECT country.country_id, country.name, path.strPath FROM country INNER JOIN country_link ON country_link.country_id=country.country_id INNER JOIN movie ON country_link.media_id=movie.idMovie INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON path.idPath=files.idPath WHERE country_link.media_type='movie' AND country.name LIKE '%%%s%%'", strSearch.c_str());
+ else
+ strSQL=PrepareSQL("SELECT DISTINCT country.country_id, country.name FROM country INNER JOIN country_link ON country_link.country_id=country.country_id WHERE country_link.media_type='movie' AND name like '%%%s%%'", strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),
+ *CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ pItem->SetPath("videodb://movies/genres/"+ strDir);
+ pItem->m_bIsFolder=true;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetTvShowGenresByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL=PrepareSQL("SELECT genre.genre_id, genre.name, path.strPath FROM genre INNER JOIN genre_link ON genre_link.genre_id=genre.genre_id INNER JOIN tvshow ON genre_link.media_id=tvshow.idShow INNER JOIN tvshowlinkpath ON tvshowlinkpath.idShow=tvshow.idShow INNER JOIN path ON path.idPath=tvshowlinkpath.idPath WHERE genre_link.media_type='tvshow' AND genre.name LIKE '%%%s%%'", strSearch.c_str());
+ else
+ strSQL=PrepareSQL("SELECT DISTINCT genre.genre_id, genre.name FROM genre INNER JOIN genre_link ON genre_link.genre_id=genre.genre_id WHERE genre_link.media_type='tvshow' AND name LIKE '%%%s%%'", strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ pItem->SetPath("videodb://tvshows/genres/"+ strDir);
+ pItem->m_bIsFolder=true;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetMovieActorsByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL=PrepareSQL("SELECT actor.actor_id, actor.name, path.strPath FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN movie ON actor_link.media_id=movie.idMovie INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON path.idPath=files.idPath WHERE actor_link.media_type='movie' AND actor.name LIKE '%%%s%%'", strSearch.c_str());
+ else
+ strSQL=PrepareSQL("SELECT DISTINCT actor.actor_id, actor.name FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN movie ON actor_link.media_id=movie.idMovie WHERE actor_link.media_type='movie' AND actor.name LIKE '%%%s%%'", strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ pItem->SetPath("videodb://movies/actors/"+ strDir);
+ pItem->m_bIsFolder=true;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetTvShowsActorsByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL=PrepareSQL("SELECT actor.actor_id, actor.name, path.strPath FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN tvshow ON actor_link.media_id=tvshow.idShow INNER JOIN tvshowlinkpath ON tvshowlinkpath.idPath=tvshow.idShow INNER JOIN path ON path.idPath=tvshowlinkpath.idPath WHERE actor_link.media_type='tvshow' AND actor.name LIKE '%%%s%%'", strSearch.c_str());
+ else
+ strSQL=PrepareSQL("SELECT DISTINCT actor.actor_id, actor.name FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN tvshow ON actor_link.media_id=tvshow.idShow WHERE actor_link.media_type='tvshow' AND actor.name LIKE '%%%s%%'",strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ pItem->SetPath("videodb://tvshows/actors/"+ strDir);
+ pItem->m_bIsFolder=true;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetMusicVideoArtistsByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ std::string strLike;
+ if (!strSearch.empty())
+ strLike = "and actor.name like '%%%s%%'";
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL=PrepareSQL("SELECT actor.actor_id, actor.name, path.strPath FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN musicvideo ON actor_link.media_id=musicvideo.idMVideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE actor_link.media_type='musicvideo' "+strLike, strSearch.c_str());
+ else
+ strSQL=PrepareSQL("SELECT DISTINCT actor.actor_id, actor.name from actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id WHERE actor_link.media_type='musicvideo' "+strLike,strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ pItem->SetPath("videodb://musicvideos/artists/"+ strDir);
+ pItem->m_bIsFolder=true;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetMusicVideoGenresByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL=PrepareSQL("SELECT genre.genre_id, genre.name, path.strPath FROM genre INNER JOIN genre_link ON genre_link.genre_id=genre.genre_id INNER JOIN musicvideo ON genre_link.media_id=musicvideo.idMVideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE genre_link.media_type='musicvideo' AND genre.name LIKE '%%%s%%'", strSearch.c_str());
+ else
+ strSQL=PrepareSQL("SELECT DISTINCT genre.genre_id, genre.name FROM genre INNER JOIN genre_link ON genre_link.genre_id=genre.genre_id WHERE genre_link.media_type='musicvideo' AND genre.name LIKE '%%%s%%'", strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ pItem->SetPath("videodb://musicvideos/genres/"+ strDir);
+ pItem->m_bIsFolder=true;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetMusicVideoAlbumsByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ strSQL = StringUtils::Format("SELECT DISTINCT"
+ " musicvideo.c{:02},"
+ " musicvideo.idMVideo,"
+ " path.strPath"
+ " FROM"
+ " musicvideo"
+ " JOIN files ON"
+ " files.idFile=musicvideo.idFile"
+ " JOIN path ON"
+ " path.idPath=files.idPath",
+ VIDEODB_ID_MUSICVIDEO_ALBUM);
+ if (!strSearch.empty())
+ strSQL += PrepareSQL(" WHERE musicvideo.c%02d like '%%%s%%'",VIDEODB_ID_MUSICVIDEO_ALBUM, strSearch.c_str());
+
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_pDS->fv(0).get_asString().empty())
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv(2).get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(0).get_asString()));
+ std::string strDir = std::to_string(m_pDS->fv(1).get_asInt());
+ pItem->SetPath("videodb://musicvideos/titles/"+ strDir);
+ pItem->m_bIsFolder=false;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetMusicVideosByAlbum(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL = PrepareSQL("SELECT musicvideo.idMVideo, musicvideo.c%02d,musicvideo.c%02d, path.strPath FROM musicvideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE musicvideo.c%02d LIKE '%%%s%%'", VIDEODB_ID_MUSICVIDEO_ALBUM, VIDEODB_ID_MUSICVIDEO_TITLE, VIDEODB_ID_MUSICVIDEO_ALBUM, strSearch.c_str());
+ else
+ strSQL = PrepareSQL("select musicvideo.idMVideo,musicvideo.c%02d,musicvideo.c%02d from musicvideo where musicvideo.c%02d like '%%%s%%'",VIDEODB_ID_MUSICVIDEO_ALBUM,VIDEODB_ID_MUSICVIDEO_TITLE,VIDEODB_ID_MUSICVIDEO_ALBUM,strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()+" - "+m_pDS->fv(2).get_asString()));
+ std::string strDir =
+ StringUtils::Format("3/2/{}", m_pDS->fv("musicvideo.idMVideo").get_asInt());
+
+ pItem->SetPath("videodb://"+ strDir);
+ pItem->m_bIsFolder=false;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+bool CVideoDatabase::GetMusicVideosByWhere(const std::string &baseDir, const Filter &filter, CFileItemList &items, bool checkLocks /*= true*/, const SortDescription &sortDescription /* = SortDescription() */, int getDetails /* = VideoDbDetailsNone */)
+{
+ try
+ {
+
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ int total = -1;
+
+ std::string strSQL = "select %s from musicvideo_view ";
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(baseDir))
+ return false;
+
+ std::string strSQLExtra;
+ const CUrlOptions::UrlOptions& options = videoUrl.GetOptions();
+ std::string strArtist;
+ int idArtist = -1;
+ // If we have an artistid then get the artist name and use that to fix up the path displayed in
+ // the GUI (musicvideos/artist-name instead of musicvideos/artistid)
+ auto option = options.find("artistid");
+ if (option != options.end())
+ {
+ idArtist = option->second.asInteger();
+ strArtist = GetSingleValue(
+ PrepareSQL("SELECT name FROM actor where actor_id = '%i'", idArtist), m_pDS)
+ .c_str();
+ }
+ Filter extFilter = filter;
+ SortDescription sorting = sortDescription;
+ if (!BuildSQL(baseDir, strSQLExtra, extFilter, strSQLExtra, videoUrl, sorting))
+ return false;
+
+ // Apply the limiting directly here if there's no special sorting but limiting
+ if (extFilter.limit.empty() && sorting.sortBy == SortByNone &&
+ (sorting.limitStart > 0 || sorting.limitEnd > 0 ||
+ (sorting.limitStart == 0 && sorting.limitEnd == 0)))
+ {
+ total = (int)strtol(GetSingleValue(PrepareSQL(strSQL, "COUNT(1)") + strSQLExtra, m_pDS).c_str(), NULL, 10);
+ strSQLExtra += DatabaseUtils::BuildLimitClause(sorting.limitEnd, sorting.limitStart);
+ }
+
+ strSQL = PrepareSQL(strSQL, !extFilter.fields.empty() ? extFilter.fields.c_str() : "*") + strSQLExtra;
+
+ int iRowsFound = RunQuery(strSQL);
+
+ // store the total value of items as a property
+ if (total < iRowsFound)
+ total = iRowsFound;
+ items.SetProperty("total", total);
+
+ if (iRowsFound <= 0)
+ return iRowsFound == 0;
+
+ DatabaseResults results;
+ results.reserve(iRowsFound);
+ if (!SortUtils::SortFromDataset(sorting, MediaTypeMusicVideo, m_pDS, results))
+ return false;
+
+ // get data from returned rows
+ items.Reserve(results.size());
+ // get songs from returned subtable
+ const query_data &data = m_pDS->get_result_set().records;
+ for (const auto &i : results)
+ {
+ unsigned int targetRow = (unsigned int)i.at(FieldRow).asInteger();
+ const dbiplus::sql_record* const record = data.at(targetRow);
+
+ CVideoInfoTag musicvideo = GetDetailsForMusicVideo(record, getDetails);
+ if (!checkLocks || m_profileManager.GetMasterProfile().getLockMode() == LOCK_MODE_EVERYONE || g_passwordManager.bMasterUser ||
+ g_passwordManager.IsDatabasePathUnlocked(musicvideo.m_strPath, *CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ CFileItemPtr item(new CFileItem(musicvideo));
+
+ CVideoDbUrl itemUrl = videoUrl;
+ std::string path = std::to_string(record->at(0).get_asInt());
+ itemUrl.AppendPath(path);
+ item->SetPath(itemUrl.ToString());
+
+ item->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED, musicvideo.GetPlayCount() > 0);
+ items.Add(item);
+ }
+ }
+
+ // cleanup
+ m_pDS->close();
+ if (!strArtist.empty())
+ items.SetProperty("customtitle", strArtist);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return false;
+}
+
+unsigned int CVideoDatabase::GetRandomMusicVideoIDs(const std::string& strWhere, std::vector<std::pair<int,int> > &songIDs)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return 0;
+ if (nullptr == m_pDS)
+ return 0;
+
+ std::string strSQL = "select distinct idMVideo from musicvideo_view";
+ if (!strWhere.empty())
+ strSQL += " where " + strWhere;
+ strSQL += PrepareSQL(" ORDER BY RANDOM()");
+
+ if (!m_pDS->query(strSQL)) return 0;
+ songIDs.clear();
+ if (m_pDS->num_rows() == 0)
+ {
+ m_pDS->close();
+ return 0;
+ }
+ songIDs.reserve(m_pDS->num_rows());
+ while (!m_pDS->eof())
+ {
+ songIDs.push_back(std::make_pair<int,int>(2,m_pDS->fv(0).get_asInt()));
+ m_pDS->next();
+ } // cleanup
+ m_pDS->close();
+ return songIDs.size();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strWhere);
+ }
+ return 0;
+}
+
+int CVideoDatabase::GetMatchingMusicVideo(const std::string& strArtist, const std::string& strAlbum, const std::string& strTitle)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return -1;
+ if (nullptr == m_pDS)
+ return -1;
+
+ std::string strSQL;
+ if (strAlbum.empty() && strTitle.empty())
+ { // we want to return matching artists only
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL=PrepareSQL("SELECT DISTINCT actor.actor_id, path.strPath FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN musicvideo ON actor_link.media_id=musicvideo.idMVideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE actor_link.media_type='musicvideo' AND actor.name like '%s'", strArtist.c_str());
+ else
+ strSQL=PrepareSQL("SELECT DISTINCT actor.actor_id FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id WHERE actor_link.media_type='musicvideo' AND actor.name LIKE '%s'", strArtist.c_str());
+ }
+ else
+ { // we want to return the matching musicvideo
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL = PrepareSQL("SELECT musicvideo.idMVideo FROM actor INNER JOIN actor_link ON actor_link.actor_id=actor.actor_id INNER JOIN musicvideo ON actor_link.media_id=musicvideo.idMVideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE actor_link.media_type='musicvideo' AND musicvideo.c%02d LIKE '%s' AND musicvideo.c%02d LIKE '%s' AND actor.name LIKE '%s'", VIDEODB_ID_MUSICVIDEO_ALBUM, strAlbum.c_str(), VIDEODB_ID_MUSICVIDEO_TITLE, strTitle.c_str(), strArtist.c_str());
+ else
+ strSQL = PrepareSQL("select musicvideo.idMVideo from musicvideo join actor_link on actor_link.media_id=musicvideo.idMVideo AND actor_link.media_type='musicvideo' join actor on actor.actor_id=actor_link.actor_id where musicvideo.c%02d like '%s' and musicvideo.c%02d like '%s' and actor.name like '%s'",VIDEODB_ID_MUSICVIDEO_ALBUM,strAlbum.c_str(),VIDEODB_ID_MUSICVIDEO_TITLE,strTitle.c_str(),strArtist.c_str());
+ }
+ m_pDS->query( strSQL );
+
+ if (m_pDS->eof())
+ return -1;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->close();
+ return -1;
+ }
+
+ int lResult = m_pDS->fv(0).get_asInt();
+ m_pDS->close();
+ return lResult;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ return -1;
+}
+
+void CVideoDatabase::GetMoviesByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL = PrepareSQL("SELECT movie.idMovie, movie.c%02d, path.strPath, movie.idSet FROM movie "
+ "INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON "
+ "path.idPath=files.idPath "
+ "WHERE movie.c%02d LIKE '%%%s%%' OR movie.c%02d LIKE '%%%s%%'",
+ VIDEODB_ID_TITLE, VIDEODB_ID_TITLE, strSearch.c_str(),
+ VIDEODB_ID_ORIGINALTITLE, strSearch.c_str());
+ else
+ strSQL = PrepareSQL("SELECT movie.idMovie,movie.c%02d, movie.idSet FROM movie WHERE "
+ "movie.c%02d like '%%%s%%' OR movie.c%02d LIKE '%%%s%%'",
+ VIDEODB_ID_TITLE, VIDEODB_ID_TITLE, strSearch.c_str(),
+ VIDEODB_ID_ORIGINALTITLE, strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ int movieId = m_pDS->fv("movie.idMovie").get_asInt();
+ int setId = m_pDS->fv("movie.idSet").get_asInt();
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ std::string path;
+ if (setId <= 0 || !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_GROUPMOVIESETS))
+ path = StringUtils::Format("videodb://movies/titles/{}", movieId);
+ else
+ path = StringUtils::Format("videodb://movies/sets/{}/{}", setId, movieId);
+ pItem->SetPath(path);
+ pItem->m_bIsFolder=false;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetTvShowsByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL = PrepareSQL("SELECT tvshow.idShow, tvshow.c%02d, path.strPath FROM tvshow INNER JOIN tvshowlinkpath ON tvshowlinkpath.idShow=tvshow.idShow INNER JOIN path ON path.idPath=tvshowlinkpath.idPath WHERE tvshow.c%02d LIKE '%%%s%%'", VIDEODB_ID_TV_TITLE, VIDEODB_ID_TV_TITLE, strSearch.c_str());
+ else
+ strSQL = PrepareSQL("select tvshow.idShow,tvshow.c%02d from tvshow where tvshow.c%02d like '%%%s%%'",VIDEODB_ID_TV_TITLE,VIDEODB_ID_TV_TITLE,strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ std::string strDir =
+ StringUtils::Format("tvshows/titles/{}/", m_pDS->fv("tvshow.idShow").get_asInt());
+
+ pItem->SetPath("videodb://"+ strDir);
+ pItem->m_bIsFolder=true;
+ pItem->GetVideoInfoTag()->m_iDbId = m_pDS->fv("tvshow.idShow").get_asInt();
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetEpisodesByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL = PrepareSQL("SELECT episode.idEpisode, episode.c%02d, episode.c%02d, episode.idShow, tvshow.c%02d, path.strPath FROM episode INNER JOIN tvshow ON tvshow.idShow=episode.idShow INNER JOIN files ON files.idFile=episode.idFile INNER JOIN path ON path.idPath=files.idPath WHERE episode.c%02d LIKE '%%%s%%'", VIDEODB_ID_EPISODE_TITLE, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_TV_TITLE, VIDEODB_ID_EPISODE_TITLE, strSearch.c_str());
+ else
+ strSQL = PrepareSQL("SELECT episode.idEpisode, episode.c%02d, episode.c%02d, episode.idShow, tvshow.c%02d FROM episode INNER JOIN tvshow ON tvshow.idShow=episode.idShow WHERE episode.c%02d like '%%%s%%'", VIDEODB_ID_EPISODE_TITLE, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_TV_TITLE, VIDEODB_ID_EPISODE_TITLE, strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()+" ("+m_pDS->fv(4).get_asString()+")"));
+ std::string path = StringUtils::Format("videodb://tvshows/titles/{}/{}/{}",
+ m_pDS->fv("episode.idShow").get_asInt(),
+ m_pDS->fv(2).get_asInt(), m_pDS->fv(0).get_asInt());
+ pItem->SetPath(path);
+ pItem->m_bIsFolder=false;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetMusicVideosByName(const std::string& strSearch, CFileItemList& items)
+{
+// Alternative searching - not quite as fast though due to
+// retrieving all information
+// Filter filter(PrepareSQL("c%02d like '%s%%' or c%02d like '%% %s%%'", VIDEODB_ID_MUSICVIDEO_TITLE, strSearch.c_str(), VIDEODB_ID_MUSICVIDEO_TITLE, strSearch.c_str()));
+// GetMusicVideosByWhere("videodb://musicvideos/titles/", filter, items);
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL = PrepareSQL("SELECT musicvideo.idMVideo, musicvideo.c%02d, path.strPath FROM musicvideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE musicvideo.c%02d LIKE '%%%s%%'", VIDEODB_ID_MUSICVIDEO_TITLE, VIDEODB_ID_MUSICVIDEO_TITLE, strSearch.c_str());
+ else
+ strSQL = PrepareSQL("select musicvideo.idMVideo,musicvideo.c%02d from musicvideo where musicvideo.c%02d like '%%%s%%'",VIDEODB_ID_MUSICVIDEO_TITLE,VIDEODB_ID_MUSICVIDEO_TITLE,strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ std::string strDir =
+ StringUtils::Format("3/2/{}", m_pDS->fv("musicvideo.idMVideo").get_asInt());
+
+ pItem->SetPath("videodb://"+ strDir);
+ pItem->m_bIsFolder=false;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetEpisodesByPlot(const std::string& strSearch, CFileItemList& items)
+{
+// Alternative searching - not quite as fast though due to
+// retrieving all information
+// Filter filter;
+// filter.where = PrepareSQL("c%02d like '%s%%' or c%02d like '%% %s%%'", VIDEODB_ID_EPISODE_PLOT, strSearch.c_str(), VIDEODB_ID_EPISODE_PLOT, strSearch.c_str());
+// filter.where += PrepareSQL("or c%02d like '%s%%' or c%02d like '%% %s%%'", VIDEODB_ID_EPISODE_TITLE, strSearch.c_str(), VIDEODB_ID_EPISODE_TITLE, strSearch.c_str());
+// GetEpisodesByWhere("videodb://tvshows/titles/", filter, items);
+// return;
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL = PrepareSQL("SELECT episode.idEpisode, episode.c%02d, episode.c%02d, episode.idShow, tvshow.c%02d, path.strPath FROM episode INNER JOIN tvshow ON tvshow.idShow=episode.idShow INNER JOIN files ON files.idFile=episode.idFile INNER JOIN path ON path.idPath=files.idPath WHERE episode.c%02d LIKE '%%%s%%'", VIDEODB_ID_EPISODE_TITLE, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_TV_TITLE, VIDEODB_ID_EPISODE_PLOT, strSearch.c_str());
+ else
+ strSQL = PrepareSQL("SELECT episode.idEpisode, episode.c%02d, episode.c%02d, episode.idShow, tvshow.c%02d FROM episode INNER JOIN tvshow ON tvshow.idShow=episode.idShow WHERE episode.c%02d LIKE '%%%s%%'", VIDEODB_ID_EPISODE_TITLE, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_TV_TITLE, VIDEODB_ID_EPISODE_PLOT, strSearch.c_str());
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()+" ("+m_pDS->fv(4).get_asString()+")"));
+ std::string path = StringUtils::Format("videodb://tvshows/titles/{}/{}/{}",
+ m_pDS->fv("episode.idShow").get_asInt(),
+ m_pDS->fv(2).get_asInt(), m_pDS->fv(0).get_asInt());
+ pItem->SetPath(path);
+ pItem->m_bIsFolder=false;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetMoviesByPlot(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL = PrepareSQL("select movie.idMovie, movie.c%02d, path.strPath FROM movie INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON path.idPath=files.idPath WHERE movie.c%02d LIKE '%%%s%%' OR movie.c%02d LIKE '%%%s%%' OR movie.c%02d LIKE '%%%s%%'", VIDEODB_ID_TITLE,VIDEODB_ID_PLOT, strSearch.c_str(), VIDEODB_ID_PLOTOUTLINE, strSearch.c_str(), VIDEODB_ID_TAGLINE,strSearch.c_str());
+ else
+ strSQL = PrepareSQL("SELECT movie.idMovie, movie.c%02d FROM movie WHERE (movie.c%02d LIKE '%%%s%%' OR movie.c%02d LIKE '%%%s%%' OR movie.c%02d LIKE '%%%s%%')", VIDEODB_ID_TITLE, VIDEODB_ID_PLOT, strSearch.c_str(), VIDEODB_ID_PLOTOUTLINE, strSearch.c_str(), VIDEODB_ID_TAGLINE, strSearch.c_str());
+
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv(2).get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+ std::string path =
+ StringUtils::Format("videodb://movies/titles/{}", m_pDS->fv(0).get_asInt());
+ pItem->SetPath(path);
+ pItem->m_bIsFolder=false;
+
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetMovieDirectorsByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name, path.strPath FROM movie INNER JOIN director_link ON (director_link.media_id=movie.idMovie AND director_link.media_type='movie') INNER JOIN actor ON actor.actor_id=director_link.actor_id INNER JOIN files ON files.idFile=movie.idFile INNER JOIN path ON path.idPath=files.idPath WHERE actor.name LIKE '%%%s%%'", strSearch.c_str());
+ else
+ strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name FROM actor INNER JOIN director_link ON director_link.actor_id=actor.actor_id INNER JOIN movie ON director_link.media_id=movie.idMovie WHERE director_link.media_type='movie' AND actor.name like '%%%s%%'", strSearch.c_str());
+
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+
+ pItem->SetPath("videodb://movies/directors/"+ strDir);
+ pItem->m_bIsFolder=true;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetTvShowsDirectorsByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name, path.strPath FROM actor INNER JOIN director_link ON director_link.actor_id=actor.actor_id INNER JOIN tvshow ON director_link.media_id=tvshow.idShow INNER JOIN tvshowlinkpath ON tvshowlinkpath.idShow=tvshow.idShow INNER JOIN path ON path.idPath=tvshowlinkpath.idPath WHERE director_link.media_type='tvshow' AND actor.name LIKE '%%%s%%'", strSearch.c_str());
+ else
+ strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name FROM actor INNER JOIN director_link ON director_link.actor_id=actor.actor_id INNER JOIN tvshow ON director_link.media_id=tvshow.idShow WHERE director_link.media_type='tvshow' AND actor.name LIKE '%%%s%%'", strSearch.c_str());
+
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+
+ pItem->SetPath("videodb://tvshows/directors/"+ strDir);
+ pItem->m_bIsFolder=true;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::GetMusicVideoDirectorsByName(const std::string& strSearch, CFileItemList& items)
+{
+ std::string strSQL;
+
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name, path.strPath FROM actor INNER JOIN director_link ON director_link.actor_id=actor.actor_id INNER JOIN musicvideo ON director_link.media_id=musicvideo.idMVideo INNER JOIN files ON files.idFile=musicvideo.idFile INNER JOIN path ON path.idPath=files.idPath WHERE director_link.media_type='musicvideo' AND actor.name LIKE '%%%s%%'", strSearch.c_str());
+ else
+ strSQL = PrepareSQL("SELECT DISTINCT director_link.actor_id, actor.name FROM actor INNER JOIN director_link ON director_link.actor_id=actor.actor_id INNER JOIN musicvideo ON director_link.media_id=musicvideo.idMVideo WHERE director_link.media_type='musicvideo' AND actor.name LIKE '%%%s%%'", strSearch.c_str());
+
+ m_pDS->query( strSQL );
+
+ while (!m_pDS->eof())
+ {
+ if (m_profileManager.GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE && !g_passwordManager.bMasterUser)
+ if (!g_passwordManager.IsDatabasePathUnlocked(m_pDS->fv("path.strPath").get_asString(),*CMediaSourceSettings::GetInstance().GetSources("video")))
+ {
+ m_pDS->next();
+ continue;
+ }
+
+ std::string strDir = StringUtils::Format("{}/", m_pDS->fv(0).get_asInt());
+ CFileItemPtr pItem(new CFileItem(m_pDS->fv(1).get_asString()));
+
+ pItem->SetPath("videodb://musicvideos/albums/"+ strDir);
+ pItem->m_bIsFolder=true;
+ items.Add(pItem);
+ m_pDS->next();
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+}
+
+void CVideoDatabase::CleanDatabase(CGUIDialogProgressBarHandle* handle,
+ const std::set<int>& paths,
+ bool showProgress)
+{
+ CGUIDialogProgress* progress = NULL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+ if (nullptr == m_pDS2)
+ return;
+
+ auto start = std::chrono::steady_clock::now();
+ CLog::Log(LOGINFO, "{}: Starting videodatabase cleanup ..", __FUNCTION__);
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
+ "OnCleanStarted");
+
+ if (handle)
+ {
+ handle->SetTitle(g_localizeStrings.Get(700));
+ handle->SetText("");
+ }
+ else if (showProgress)
+ {
+ progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(
+ WINDOW_DIALOG_PROGRESS);
+ if (progress)
+ {
+ progress->SetHeading(CVariant{700});
+ progress->SetLine(0, CVariant{""});
+ progress->SetLine(1, CVariant{313});
+ progress->SetLine(2, CVariant{330});
+ progress->SetPercentage(0);
+ progress->Open();
+ progress->ShowProgressBar(true);
+ }
+ }
+
+ BeginTransaction();
+
+ // find all the files
+ std::string sql = "SELECT files.idFile, files.strFileName, path.strPath FROM files "
+ "INNER JOIN path ON path.idPath=files.idPath";
+ if (!paths.empty())
+ {
+ std::string strPaths;
+ for (const auto& i : paths)
+ strPaths += StringUtils::Format(",{}", i);
+ sql += PrepareSQL(" AND path.idPath IN (%s)", strPaths.substr(1).c_str());
+ }
+
+ // For directory caching to work properly, we need to sort the files by path
+ sql += " ORDER BY path.strPath";
+
+ m_pDS2->query(sql);
+ if (m_pDS2->num_rows() > 0)
+ {
+ std::string filesToTestForDelete;
+ VECSOURCES videoSources(*CMediaSourceSettings::GetInstance().GetSources("video"));
+ CServiceBroker::GetMediaManager().GetRemovableDrives(videoSources);
+
+ int total = m_pDS2->num_rows();
+ int current = 0;
+ std::string lastDir;
+ bool gotDir = true;
+
+ while (!m_pDS2->eof())
+ {
+ std::string path = m_pDS2->fv("path.strPath").get_asString();
+ std::string fileName = m_pDS2->fv("files.strFileName").get_asString();
+ std::string fullPath;
+ ConstructPath(fullPath, path, fileName);
+
+ // get the first stacked file
+ if (URIUtils::IsStack(fullPath))
+ fullPath = CStackDirectory::GetFirstStackedFile(fullPath);
+
+ // get the actual archive path
+ if (URIUtils::IsInArchive(fullPath))
+ fullPath = CURL(fullPath).GetHostName();
+
+ bool del = true;
+ if (URIUtils::IsPlugin(fullPath))
+ {
+ SScanSettings settings;
+ bool foundDirectly = false;
+ ScraperPtr scraper = GetScraperForPath(fullPath, settings, foundDirectly);
+ if (scraper &&
+ CPluginDirectory::CheckExists(TranslateContent(scraper->Content()), fullPath))
+ del = false;
+ }
+ else
+ {
+ // Only consider keeping this file if not optical and belonging to a (matching) source
+ bool bIsSource;
+ if (!URIUtils::IsOnDVD(fullPath) &&
+ CUtil::GetMatchingSource(fullPath, videoSources, bIsSource) >= 0)
+ {
+ const std::string pathDir = URIUtils::GetDirectory(fullPath);
+
+ // Cache file's directory in case it's different from the previous file
+ if (lastDir != pathDir)
+ {
+ lastDir = pathDir;
+ CFileItemList items; // Dummy list
+ gotDir = CDirectory::GetDirectory(pathDir, items, "",
+ DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
+ }
+
+ // Keep existing files
+ if (gotDir && CFile::Exists(fullPath, true))
+ del = false;
+ }
+ }
+ if (del)
+ filesToTestForDelete += m_pDS2->fv("files.idFile").get_asString() + ",";
+
+ if (handle == NULL && progress != NULL)
+ {
+ int percentage = current * 100 / total;
+ if (percentage > progress->GetPercentage())
+ {
+ progress->SetPercentage(percentage);
+ progress->Progress();
+ }
+ if (progress->IsCanceled())
+ {
+ progress->Close();
+ m_pDS2->close();
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
+ "OnCleanFinished");
+ return;
+ }
+ }
+ else if (handle != NULL)
+ handle->SetPercentage(current * 100 / (float)total);
+
+ m_pDS2->next();
+ current++;
+ }
+ m_pDS2->close();
+
+ std::string filesToDelete;
+
+ // Add any files that don't have a valid idPath entry to the filesToDelete list.
+ m_pDS->query("SELECT files.idFile FROM files WHERE NOT EXISTS (SELECT 1 FROM path "
+ "WHERE path.idPath = files.idPath)");
+ while (!m_pDS->eof())
+ {
+ std::string file = m_pDS->fv("files.idFile").get_asString() + ",";
+ filesToTestForDelete += file;
+ filesToDelete += file;
+
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ std::map<int, bool> pathsDeleteDecisions;
+ std::vector<int> movieIDs;
+ std::vector<int> tvshowIDs;
+ std::vector<int> episodeIDs;
+ std::vector<int> musicVideoIDs;
+
+ if (!filesToTestForDelete.empty())
+ {
+ StringUtils::TrimRight(filesToTestForDelete, ",");
+
+ movieIDs = CleanMediaType(MediaTypeMovie, filesToTestForDelete, pathsDeleteDecisions,
+ filesToDelete, !showProgress);
+ episodeIDs = CleanMediaType(MediaTypeEpisode, filesToTestForDelete, pathsDeleteDecisions,
+ filesToDelete, !showProgress);
+ musicVideoIDs = CleanMediaType(MediaTypeMusicVideo, filesToTestForDelete,
+ pathsDeleteDecisions, filesToDelete, !showProgress);
+ }
+
+ if (progress != NULL)
+ {
+ progress->SetPercentage(100);
+ progress->Progress();
+ }
+
+ if (!filesToDelete.empty())
+ {
+ filesToDelete = "(" + StringUtils::TrimRight(filesToDelete, ",") + ")";
+
+ // Clean hashes of all paths that files are deleted from
+ // Otherwise there is a mismatch between the path contents and the hash in the
+ // database, leading to potentially missed items on re-scan (if deleted files are
+ // later re-added to a source)
+ CLog::LogFC(LOGDEBUG, LOGDATABASE, "Cleaning path hashes");
+ m_pDS->query("SELECT DISTINCT strPath FROM path JOIN files ON files.idPath=path.idPath "
+ "WHERE files.idFile IN " +
+ filesToDelete);
+ int pathHashCount = m_pDS->num_rows();
+ while (!m_pDS->eof())
+ {
+ InvalidatePathHash(m_pDS->fv("strPath").get_asString());
+ m_pDS->next();
+ }
+ CLog::LogFC(LOGDEBUG, LOGDATABASE, "Cleaned {} path hashes", pathHashCount);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning files table", __FUNCTION__);
+ sql = "DELETE FROM files WHERE idFile IN " + filesToDelete;
+ m_pDS->exec(sql);
+ }
+
+ if (!movieIDs.empty())
+ {
+ std::string moviesToDelete;
+ for (const auto& i : movieIDs)
+ moviesToDelete += StringUtils::Format("{},", i);
+ moviesToDelete = "(" + StringUtils::TrimRight(moviesToDelete, ",") + ")";
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning movie table", __FUNCTION__);
+ sql = "DELETE FROM movie WHERE idMovie IN " + moviesToDelete;
+ m_pDS->exec(sql);
+ }
+
+ if (!episodeIDs.empty())
+ {
+ std::string episodesToDelete;
+ for (const auto& i : episodeIDs)
+ episodesToDelete += StringUtils::Format("{},", i);
+ episodesToDelete = "(" + StringUtils::TrimRight(episodesToDelete, ",") + ")";
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning episode table", __FUNCTION__);
+ sql = "DELETE FROM episode WHERE idEpisode IN " + episodesToDelete;
+ m_pDS->exec(sql);
+ }
+
+ CLog::Log(LOGDEBUG, LOGDATABASE,
+ "{}: Cleaning paths that don't exist and have content set...", __FUNCTION__);
+ sql = "SELECT path.idPath, path.strPath, path.idParentPath FROM path "
+ "WHERE NOT ((strContent IS NULL OR strContent = '') "
+ "AND (strSettings IS NULL OR strSettings = '') "
+ "AND (strHash IS NULL OR strHash = '') "
+ "AND (exclude IS NULL OR exclude != 1))";
+ m_pDS2->query(sql);
+ std::string strIds;
+ while (!m_pDS2->eof())
+ {
+ auto pathsDeleteDecision = pathsDeleteDecisions.find(m_pDS2->fv(0).get_asInt());
+ // Check if we have a decision for the parent path
+ auto pathsDeleteDecisionByParent = pathsDeleteDecisions.find(m_pDS2->fv(2).get_asInt());
+ std::string path = m_pDS2->fv(1).get_asString();
+
+ bool exists = false;
+ if (URIUtils::IsPlugin(path))
+ {
+ SScanSettings settings;
+ bool foundDirectly = false;
+ ScraperPtr scraper = GetScraperForPath(path, settings, foundDirectly);
+ if (scraper && CPluginDirectory::CheckExists(TranslateContent(scraper->Content()), path))
+ exists = true;
+ }
+ else
+ exists = CDirectory::Exists(path, false);
+
+ if (((pathsDeleteDecision != pathsDeleteDecisions.end() && pathsDeleteDecision->second) ||
+ (pathsDeleteDecision == pathsDeleteDecisions.end() && !exists)) &&
+ ((pathsDeleteDecisionByParent != pathsDeleteDecisions.end() &&
+ pathsDeleteDecisionByParent->second) ||
+ (pathsDeleteDecisionByParent == pathsDeleteDecisions.end())))
+ strIds += m_pDS2->fv(0).get_asString() + ",";
+
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+
+ if (!strIds.empty())
+ {
+ sql = PrepareSQL("DELETE FROM path WHERE idPath IN (%s)",
+ StringUtils::TrimRight(strIds, ",").c_str());
+ m_pDS->exec(sql);
+ sql = "DELETE FROM tvshowlinkpath "
+ "WHERE NOT EXISTS (SELECT 1 FROM path WHERE path.idPath = tvshowlinkpath.idPath)";
+ m_pDS->exec(sql);
+ }
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning tvshow table", __FUNCTION__);
+
+ std::string tvshowsToDelete;
+ sql = "SELECT idShow FROM tvshow "
+ "WHERE NOT EXISTS (SELECT 1 FROM tvshowlinkpath WHERE tvshowlinkpath.idShow = "
+ "tvshow.idShow)";
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ tvshowIDs.push_back(m_pDS->fv(0).get_asInt());
+ tvshowsToDelete += m_pDS->fv(0).get_asString() + ",";
+ m_pDS->next();
+ }
+ m_pDS->close();
+ if (!tvshowsToDelete.empty())
+ {
+ sql = "DELETE FROM tvshow WHERE idShow IN (" +
+ StringUtils::TrimRight(tvshowsToDelete, ",") + ")";
+ m_pDS->exec(sql);
+ }
+
+ if (!musicVideoIDs.empty())
+ {
+ std::string musicVideosToDelete;
+ for (const auto& i : musicVideoIDs)
+ musicVideosToDelete += StringUtils::Format("{},", i);
+ musicVideosToDelete = "(" + StringUtils::TrimRight(musicVideosToDelete, ",") + ")";
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning musicvideo table", __FUNCTION__);
+ sql = "DELETE FROM musicvideo WHERE idMVideo IN " + musicVideosToDelete;
+ m_pDS->exec(sql);
+ }
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning path table", __FUNCTION__);
+ sql = StringUtils::Format(
+ "DELETE FROM path "
+ "WHERE (strContent IS NULL OR strContent = '') "
+ "AND (strSettings IS NULL OR strSettings = '') "
+ "AND (strHash IS NULL OR strHash = '') "
+ "AND (exclude IS NULL OR exclude != 1) "
+ "AND (idParentPath IS NULL OR NOT EXISTS (SELECT 1 FROM (SELECT idPath FROM path) as "
+ "parentPath WHERE parentPath.idPath = path.idParentPath)) " // MySQL only fix (#5007)
+ "AND NOT EXISTS (SELECT 1 FROM files WHERE files.idPath = path.idPath) "
+ "AND NOT EXISTS (SELECT 1 FROM tvshowlinkpath WHERE tvshowlinkpath.idPath = path.idPath) "
+ "AND NOT EXISTS (SELECT 1 FROM movie WHERE movie.c{:02} = path.idPath) "
+ "AND NOT EXISTS (SELECT 1 FROM episode WHERE episode.c{:02} = path.idPath) "
+ "AND NOT EXISTS (SELECT 1 FROM musicvideo WHERE musicvideo.c{:02} = path.idPath)",
+ VIDEODB_ID_PARENTPATHID, VIDEODB_ID_EPISODE_PARENTPATHID,
+ VIDEODB_ID_MUSICVIDEO_PARENTPATHID);
+ m_pDS->exec(sql);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning genre table", __FUNCTION__);
+ sql =
+ "DELETE FROM genre "
+ "WHERE NOT EXISTS (SELECT 1 FROM genre_link WHERE genre_link.genre_id = genre.genre_id)";
+ m_pDS->exec(sql);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning country table", __FUNCTION__);
+ sql = "DELETE FROM country WHERE NOT EXISTS (SELECT 1 FROM country_link WHERE "
+ "country_link.country_id = country.country_id)";
+ m_pDS->exec(sql);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning actor table of actors, directors and writers",
+ __FUNCTION__);
+ sql =
+ "DELETE FROM actor "
+ "WHERE NOT EXISTS (SELECT 1 FROM actor_link WHERE actor_link.actor_id = actor.actor_id) "
+ "AND NOT EXISTS (SELECT 1 FROM director_link WHERE director_link.actor_id = "
+ "actor.actor_id) "
+ "AND NOT EXISTS (SELECT 1 FROM writer_link WHERE writer_link.actor_id = actor.actor_id)";
+ m_pDS->exec(sql);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning studio table", __FUNCTION__);
+ sql = "DELETE FROM studio "
+ "WHERE NOT EXISTS (SELECT 1 FROM studio_link WHERE studio_link.studio_id = "
+ "studio.studio_id)";
+ m_pDS->exec(sql);
+
+ CLog::Log(LOGDEBUG, LOGDATABASE, "{}: Cleaning set table", __FUNCTION__);
+ sql = "DELETE FROM sets "
+ "WHERE NOT EXISTS (SELECT 1 FROM movie WHERE movie.idSet = sets.idSet)";
+ m_pDS->exec(sql);
+
+ CommitTransaction();
+
+ if (handle)
+ handle->SetTitle(g_localizeStrings.Get(331));
+
+ Compress(false);
+
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGINFO, "{}: Cleaning videodatabase done. Operation took {} ms", __FUNCTION__,
+ duration.count());
+
+ for (const auto& i : movieIDs)
+ AnnounceRemove(MediaTypeMovie, i, true);
+
+ for (const auto& i : episodeIDs)
+ AnnounceRemove(MediaTypeEpisode, i, true);
+
+ for (const auto& i : tvshowIDs)
+ AnnounceRemove(MediaTypeTvShow, i, true);
+
+ for (const auto& i : musicVideoIDs)
+ AnnounceRemove(MediaTypeMusicVideo, i, true);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ RollbackTransaction();
+ }
+ if (progress)
+ progress->Close();
+
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnCleanFinished");
+}
+
+std::vector<int> CVideoDatabase::CleanMediaType(const std::string &mediaType, const std::string &cleanableFileIDs,
+ std::map<int, bool> &pathsDeleteDecisions, std::string &deletedFileIDs, bool silent)
+{
+ std::vector<int> cleanedIDs;
+ if (mediaType.empty() || cleanableFileIDs.empty())
+ return cleanedIDs;
+
+ const std::string& table = mediaType;
+ std::string idField;
+ std::string parentPathIdField;
+ bool isEpisode = false;
+ if (mediaType == MediaTypeMovie)
+ {
+ idField = "idMovie";
+ parentPathIdField = StringUtils::Format("{}.c{:02}", table, VIDEODB_ID_PARENTPATHID);
+ }
+ else if (mediaType == MediaTypeEpisode)
+ {
+ idField = "idEpisode";
+ parentPathIdField = "showPath.idParentPath";
+ isEpisode = true;
+ }
+ else if (mediaType == MediaTypeMusicVideo)
+ {
+ idField = "idMVideo";
+ parentPathIdField = StringUtils::Format("{}.c{:02}", table, VIDEODB_ID_MUSICVIDEO_PARENTPATHID);
+ }
+ else
+ return cleanedIDs;
+
+ // now grab them media items
+ std::string sql = PrepareSQL("SELECT %s.%s, %s.idFile, path.idPath, parentPath.strPath FROM %s "
+ "JOIN files ON files.idFile = %s.idFile "
+ "JOIN path ON path.idPath = files.idPath ",
+ table.c_str(), idField.c_str(), table.c_str(), table.c_str(),
+ table.c_str());
+
+ if (isEpisode)
+ sql += "JOIN tvshowlinkpath ON tvshowlinkpath.idShow = episode.idShow JOIN path AS showPath ON showPath.idPath=tvshowlinkpath.idPath ";
+
+ sql += PrepareSQL("LEFT JOIN path as parentPath ON parentPath.idPath = %s "
+ "WHERE %s.idFile IN (%s)",
+ parentPathIdField.c_str(),
+ table.c_str(), cleanableFileIDs.c_str());
+
+ VECSOURCES videoSources(*CMediaSourceSettings::GetInstance().GetSources("video"));
+ CServiceBroker::GetMediaManager().GetRemovableDrives(videoSources);
+
+ // map of parent path ID to boolean pair (if not exists and user choice)
+ std::map<int, std::pair<bool, bool> > sourcePathsDeleteDecisions;
+ m_pDS2->query(sql);
+ while (!m_pDS2->eof())
+ {
+ bool del = true;
+ if (m_pDS2->fv(3).get_isNull() == false)
+ {
+ std::string parentPath = m_pDS2->fv(3).get_asString();
+
+ // try to find the source path the parent path belongs to
+ SScanSettings scanSettings;
+ std::string sourcePath;
+ GetSourcePath(parentPath, sourcePath, scanSettings);
+
+ bool bIsSourceName;
+ bool sourceNotFound = (CUtil::GetMatchingSource(parentPath, videoSources, bIsSourceName) < 0);
+
+ if (sourceNotFound && sourcePath.empty())
+ sourcePath = parentPath;
+
+ int sourcePathID = GetPathId(sourcePath);
+ auto sourcePathsDeleteDecision = sourcePathsDeleteDecisions.find(sourcePathID);
+ if (sourcePathsDeleteDecision == sourcePathsDeleteDecisions.end())
+ {
+ bool sourcePathNotExists = (sourceNotFound || !CDirectory::Exists(sourcePath, false));
+ // if the parent path exists, the file will be deleted without asking
+ // if the parent path doesn't exist or does not belong to a valid media source,
+ // ask the user whether to remove all items it contained
+ if (sourcePathNotExists)
+ {
+ // in silent mode assume that the files are just temporarily missing
+ if (silent)
+ del = false;
+ else
+ {
+ CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (pDialog != NULL)
+ {
+ CURL sourceUrl(sourcePath);
+ pDialog->SetHeading(CVariant{15012});
+ pDialog->SetText(CVariant{StringUtils::Format(g_localizeStrings.Get(15013),
+ sourceUrl.GetWithoutUserDetails())});
+ pDialog->SetChoice(0, CVariant{15015});
+ pDialog->SetChoice(1, CVariant{15014});
+ pDialog->Open();
+
+ del = !pDialog->IsConfirmed();
+ }
+ }
+ }
+
+ sourcePathsDeleteDecisions.insert(std::make_pair(sourcePathID, std::make_pair(sourcePathNotExists, del)));
+ pathsDeleteDecisions.insert(std::make_pair(sourcePathID, sourcePathNotExists && del));
+ }
+ // the only reason not to delete the file is if the parent path doesn't
+ // exist and the user decided to delete all the items it contained
+ else if (sourcePathsDeleteDecision->second.first &&
+ !sourcePathsDeleteDecision->second.second)
+ del = false;
+
+ if (scanSettings.parent_name)
+ pathsDeleteDecisions.insert(std::make_pair(m_pDS2->fv(2).get_asInt(), del));
+ }
+
+ if (del)
+ {
+ deletedFileIDs += m_pDS2->fv(1).get_asString() + ",";
+ cleanedIDs.push_back(m_pDS2->fv(0).get_asInt());
+ }
+
+ m_pDS2->next();
+ }
+ m_pDS2->close();
+
+ return cleanedIDs;
+}
+
+void CVideoDatabase::DumpToDummyFiles(const std::string &path)
+{
+ // get all tvshows
+ CFileItemList items;
+ GetTvShowsByWhere("videodb://tvshows/titles/", CDatabase::Filter(), items);
+ std::string showPath = URIUtils::AddFileToFolder(path, "shows");
+ CDirectory::Create(showPath);
+ for (int i = 0; i < items.Size(); i++)
+ {
+ // create a folder in this directory
+ std::string showName = CUtil::MakeLegalFileName(items[i]->GetVideoInfoTag()->m_strShowTitle);
+ std::string TVFolder = URIUtils::AddFileToFolder(showPath, showName);
+ if (CDirectory::Create(TVFolder))
+ { // right - grab the episodes and dump them as well
+ CFileItemList episodes;
+ Filter filter(PrepareSQL("idShow=%i", items[i]->GetVideoInfoTag()->m_iDbId));
+ GetEpisodesByWhere("videodb://tvshows/titles/", filter, episodes);
+ for (int i = 0; i < episodes.Size(); i++)
+ {
+ CVideoInfoTag *tag = episodes[i]->GetVideoInfoTag();
+ std::string episode =
+ StringUtils::Format("{}.s{:02}e{:02}.avi", showName, tag->m_iSeason, tag->m_iEpisode);
+ // and make a file
+ std::string episodePath = URIUtils::AddFileToFolder(TVFolder, episode);
+ CFile file;
+ if (file.OpenForWrite(episodePath))
+ file.Close();
+ }
+ }
+ }
+ // get all movies
+ items.Clear();
+ GetMoviesByWhere("videodb://movies/titles/", CDatabase::Filter(), items);
+ std::string moviePath = URIUtils::AddFileToFolder(path, "movies");
+ CDirectory::Create(moviePath);
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CVideoInfoTag *tag = items[i]->GetVideoInfoTag();
+ std::string movie = StringUtils::Format("{}.avi", tag->m_strTitle);
+ CFile file;
+ if (file.OpenForWrite(URIUtils::AddFileToFolder(moviePath, movie)))
+ file.Close();
+ }
+}
+
+void CVideoDatabase::ExportToXML(const std::string &path, bool singleFile /* = true */, bool images /* = false */, bool actorThumbs /* false */, bool overwrite /*=false*/)
+{
+ int iFailCount = 0;
+ CGUIDialogProgress *progress=NULL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+ if (nullptr == m_pDS2)
+ return;
+
+ // create a 3rd dataset as well as GetEpisodeDetails() etc. uses m_pDS2, and we need to do 3 nested queries on tv shows
+ std::unique_ptr<Dataset> pDS;
+ pDS.reset(m_pDB->CreateDataset());
+ if (nullptr == pDS)
+ return;
+
+ std::unique_ptr<Dataset> pDS2;
+ pDS2.reset(m_pDB->CreateDataset());
+ if (nullptr == pDS2)
+ return;
+
+ // if we're exporting to a single folder, we export thumbs as well
+ std::string exportRoot = URIUtils::AddFileToFolder(path, "kodi_videodb_" + CDateTime::GetCurrentDateTime().GetAsDBDate());
+ std::string xmlFile = URIUtils::AddFileToFolder(exportRoot, "videodb.xml");
+ std::string actorsDir = URIUtils::AddFileToFolder(exportRoot, "actors");
+ std::string moviesDir = URIUtils::AddFileToFolder(exportRoot, "movies");
+ std::string movieSetsDir = URIUtils::AddFileToFolder(exportRoot, "moviesets");
+ std::string musicvideosDir = URIUtils::AddFileToFolder(exportRoot, "musicvideos");
+ std::string tvshowsDir = URIUtils::AddFileToFolder(exportRoot, "tvshows");
+ if (singleFile)
+ {
+ images = true;
+ overwrite = false;
+ actorThumbs = true;
+ CDirectory::Remove(exportRoot);
+ CDirectory::Create(exportRoot);
+ CDirectory::Create(actorsDir);
+ CDirectory::Create(moviesDir);
+ CDirectory::Create(movieSetsDir);
+ CDirectory::Create(musicvideosDir);
+ CDirectory::Create(tvshowsDir);
+ }
+
+ progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ // find all movies
+ std::string sql = "select * from movie_view";
+
+ m_pDS->query(sql);
+
+ if (progress)
+ {
+ progress->SetHeading(CVariant{647});
+ progress->SetLine(0, CVariant{650});
+ progress->SetLine(1, CVariant{""});
+ progress->SetLine(2, CVariant{""});
+ progress->SetPercentage(0);
+ progress->Open();
+ progress->ShowProgressBar(true);
+ }
+
+ int total = m_pDS->num_rows();
+ int current = 0;
+
+ // create our xml document
+ CXBMCTinyXML xmlDoc;
+ TiXmlDeclaration decl("1.0", "UTF-8", "yes");
+ xmlDoc.InsertEndChild(decl);
+ TiXmlNode *pMain = NULL;
+ if (!singleFile)
+ pMain = &xmlDoc;
+ else
+ {
+ TiXmlElement xmlMainElement("videodb");
+ pMain = xmlDoc.InsertEndChild(xmlMainElement);
+ XMLUtils::SetInt(pMain,"version", GetExportVersion());
+ }
+
+ while (!m_pDS->eof())
+ {
+ CVideoInfoTag movie = GetDetailsForMovie(m_pDS, VideoDbDetailsAll);
+ // strip paths to make them relative
+ if (StringUtils::StartsWith(movie.m_strTrailer, movie.m_strPath))
+ movie.m_strTrailer = movie.m_strTrailer.substr(movie.m_strPath.size());
+ std::map<std::string, std::string> artwork;
+ if (GetArtForItem(movie.m_iDbId, movie.m_type, artwork) && singleFile)
+ {
+ TiXmlElement additionalNode("art");
+ for (const auto &i : artwork)
+ XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second);
+ movie.Save(pMain, "movie", true, &additionalNode);
+ }
+ else
+ movie.Save(pMain, "movie", singleFile);
+
+ // reset old skip state
+ bool bSkip = false;
+
+ if (progress)
+ {
+ progress->SetLine(1, CVariant{movie.m_strTitle});
+ progress->SetPercentage(current * 100 / total);
+ progress->Progress();
+ if (progress->IsCanceled())
+ {
+ progress->Close();
+ m_pDS->close();
+ return;
+ }
+ }
+
+ CFileItem item(movie.m_strFileNameAndPath,false);
+ if (!singleFile && CUtil::SupportsWriteFileOperations(movie.m_strFileNameAndPath))
+ {
+ if (!item.Exists(false))
+ {
+ CLog::Log(LOGINFO, "{} - Not exporting item {} as it does not exist", __FUNCTION__,
+ movie.m_strFileNameAndPath);
+ bSkip = true;
+ }
+ else
+ {
+ std::string nfoFile(URIUtils::ReplaceExtension(item.GetTBNFile(), ".nfo"));
+
+ if (item.IsOpticalMediaFile())
+ {
+ nfoFile = URIUtils::AddFileToFolder(
+ URIUtils::GetParentPath(nfoFile),
+ URIUtils::GetFileName(nfoFile));
+ }
+
+ if (overwrite || !CFile::Exists(nfoFile, false))
+ {
+ if(!xmlDoc.SaveFile(nfoFile))
+ {
+ CLog::Log(LOGERROR, "{}: Movie nfo export failed! ('{}')", __FUNCTION__, nfoFile);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(20302),
+ CURL::GetRedacted(nfoFile));
+ iFailCount++;
+ }
+ }
+ }
+ }
+ if (!singleFile)
+ {
+ xmlDoc.Clear();
+ TiXmlDeclaration decl("1.0", "UTF-8", "yes");
+ xmlDoc.InsertEndChild(decl);
+ }
+
+ if (images && !bSkip)
+ {
+ if (singleFile)
+ {
+ std::string strFileName(movie.m_strTitle);
+ if (movie.HasYear())
+ strFileName += StringUtils::Format("_{}", movie.GetYear());
+ item.SetPath(GetSafeFile(moviesDir, strFileName) + ".avi");
+ }
+ for (const auto &i : artwork)
+ {
+ std::string savedThumb = item.GetLocalArt(i.first, false);
+ CServiceBroker::GetTextureCache()->Export(i.second, savedThumb, overwrite);
+ }
+ if (actorThumbs)
+ ExportActorThumbs(actorsDir, movie, !singleFile, overwrite);
+ }
+ m_pDS->next();
+ current++;
+ }
+ m_pDS->close();
+
+ if (!singleFile)
+ movieSetsDir = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER);
+ if (images && !movieSetsDir.empty())
+ {
+ // find all movie sets
+ sql = "select idSet, strSet from sets";
+
+ m_pDS->query(sql);
+
+ total = m_pDS->num_rows();
+ current = 0;
+
+ while (!m_pDS->eof())
+ {
+ std::string title = m_pDS->fv("strSet").get_asString();
+
+ if (progress)
+ {
+ progress->SetLine(1, CVariant{title});
+ progress->SetPercentage(current * 100 / total);
+ progress->Progress();
+ if (progress->IsCanceled())
+ {
+ progress->Close();
+ m_pDS->close();
+ return;
+ }
+ }
+
+ std::string itemPath = URIUtils::AddFileToFolder(movieSetsDir,
+ CUtil::MakeLegalFileName(title, LEGAL_WIN32_COMPAT));
+ if (CDirectory::Exists(itemPath) || CDirectory::Create(itemPath))
+ {
+ std::map<std::string, std::string> artwork;
+ GetArtForItem(m_pDS->fv("idSet").get_asInt(), MediaTypeVideoCollection, artwork);
+ for (const auto& art : artwork)
+ {
+ std::string savedThumb = URIUtils::AddFileToFolder(itemPath, art.first);
+ CServiceBroker::GetTextureCache()->Export(art.second, savedThumb, overwrite);
+ }
+ }
+ else
+ CLog::Log(
+ LOGDEBUG,
+ "CVideoDatabase::{} - Not exporting movie set '{}' as could not create folder '{}'",
+ __FUNCTION__, title, itemPath);
+ m_pDS->next();
+ current++;
+ }
+ m_pDS->close();
+ }
+
+ // find all musicvideos
+ sql = "select * from musicvideo_view";
+
+ m_pDS->query(sql);
+
+ total = m_pDS->num_rows();
+ current = 0;
+
+ while (!m_pDS->eof())
+ {
+ CVideoInfoTag movie = GetDetailsForMusicVideo(m_pDS, VideoDbDetailsAll);
+ std::map<std::string, std::string> artwork;
+ if (GetArtForItem(movie.m_iDbId, movie.m_type, artwork) && singleFile)
+ {
+ TiXmlElement additionalNode("art");
+ for (const auto &i : artwork)
+ XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second);
+ movie.Save(pMain, "musicvideo", true, &additionalNode);
+ }
+ else
+ movie.Save(pMain, "musicvideo", singleFile);
+
+ // reset old skip state
+ bool bSkip = false;
+
+ if (progress)
+ {
+ progress->SetLine(1, CVariant{movie.m_strTitle});
+ progress->SetPercentage(current * 100 / total);
+ progress->Progress();
+ if (progress->IsCanceled())
+ {
+ progress->Close();
+ m_pDS->close();
+ return;
+ }
+ }
+
+ CFileItem item(movie.m_strFileNameAndPath,false);
+ if (!singleFile && CUtil::SupportsWriteFileOperations(movie.m_strFileNameAndPath))
+ {
+ if (!item.Exists(false))
+ {
+ CLog::Log(LOGINFO, "{} - Not exporting item {} as it does not exist", __FUNCTION__,
+ movie.m_strFileNameAndPath);
+ bSkip = true;
+ }
+ else
+ {
+ std::string nfoFile(URIUtils::ReplaceExtension(item.GetTBNFile(), ".nfo"));
+
+ if (overwrite || !CFile::Exists(nfoFile, false))
+ {
+ if(!xmlDoc.SaveFile(nfoFile))
+ {
+ CLog::Log(LOGERROR, "{}: Musicvideo nfo export failed! ('{}')", __FUNCTION__,
+ nfoFile);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(20302),
+ CURL::GetRedacted(nfoFile));
+ iFailCount++;
+ }
+ }
+ }
+ }
+ if (!singleFile)
+ {
+ xmlDoc.Clear();
+ TiXmlDeclaration decl("1.0", "UTF-8", "yes");
+ xmlDoc.InsertEndChild(decl);
+ }
+ if (images && !bSkip)
+ {
+ if (singleFile)
+ {
+ std::string strFileName(StringUtils::Join(movie.m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator) + "." + movie.m_strTitle);
+ if (movie.HasYear())
+ strFileName += StringUtils::Format("_{}", movie.GetYear());
+ item.SetPath(GetSafeFile(musicvideosDir, strFileName) + ".avi");
+ }
+ for (const auto &i : artwork)
+ {
+ std::string savedThumb = item.GetLocalArt(i.first, false);
+ CServiceBroker::GetTextureCache()->Export(i.second, savedThumb, overwrite);
+ }
+ }
+ m_pDS->next();
+ current++;
+ }
+ m_pDS->close();
+
+ // repeat for all tvshows
+ sql = "SELECT * FROM tvshow_view";
+ m_pDS->query(sql);
+
+ total = m_pDS->num_rows();
+ current = 0;
+
+ while (!m_pDS->eof())
+ {
+ CVideoInfoTag tvshow = GetDetailsForTvShow(m_pDS, VideoDbDetailsAll);
+ GetTvShowNamedSeasons(tvshow.m_iDbId, tvshow.m_namedSeasons);
+
+ std::map<int, std::map<std::string, std::string> > seasonArt;
+ GetTvShowSeasonArt(tvshow.m_iDbId, seasonArt);
+
+ std::map<std::string, std::string> artwork;
+ if (GetArtForItem(tvshow.m_iDbId, tvshow.m_type, artwork) && singleFile)
+ {
+ TiXmlElement additionalNode("art");
+ for (const auto &i : artwork)
+ XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second);
+ for (const auto &i : seasonArt)
+ {
+ TiXmlElement seasonNode("season");
+ seasonNode.SetAttribute("num", i.first);
+ for (const auto &j : i.second)
+ XMLUtils::SetString(&seasonNode, j.first.c_str(), j.second);
+ additionalNode.InsertEndChild(seasonNode);
+ }
+ tvshow.Save(pMain, "tvshow", true, &additionalNode);
+ }
+ else
+ tvshow.Save(pMain, "tvshow", singleFile);
+
+ // reset old skip state
+ bool bSkip = false;
+
+ if (progress)
+ {
+ progress->SetLine(1, CVariant{tvshow.m_strTitle});
+ progress->SetPercentage(current * 100 / total);
+ progress->Progress();
+ if (progress->IsCanceled())
+ {
+ progress->Close();
+ m_pDS->close();
+ return;
+ }
+ }
+
+ CFileItem item(tvshow.m_strPath, true);
+ if (!singleFile && CUtil::SupportsWriteFileOperations(tvshow.m_strPath))
+ {
+ if (!item.Exists(false))
+ {
+ CLog::Log(LOGINFO, "{} - Not exporting item {} as it does not exist", __FUNCTION__,
+ tvshow.m_strPath);
+ bSkip = true;
+ }
+ else
+ {
+ std::string nfoFile = URIUtils::AddFileToFolder(tvshow.m_strPath, "tvshow.nfo");
+
+ if (overwrite || !CFile::Exists(nfoFile, false))
+ {
+ if(!xmlDoc.SaveFile(nfoFile))
+ {
+ CLog::Log(LOGERROR, "{}: TVShow nfo export failed! ('{}')", __FUNCTION__, nfoFile);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(20302),
+ CURL::GetRedacted(nfoFile));
+ iFailCount++;
+ }
+ }
+ }
+ }
+ if (!singleFile)
+ {
+ xmlDoc.Clear();
+ TiXmlDeclaration decl("1.0", "UTF-8", "yes");
+ xmlDoc.InsertEndChild(decl);
+ }
+ if (images && !bSkip)
+ {
+ if (singleFile)
+ item.SetPath(GetSafeFile(tvshowsDir, tvshow.m_strTitle));
+
+ for (const auto &i : artwork)
+ {
+ std::string savedThumb = item.GetLocalArt(i.first, true);
+ CServiceBroker::GetTextureCache()->Export(i.second, savedThumb, overwrite);
+ }
+
+ if (actorThumbs)
+ ExportActorThumbs(actorsDir, tvshow, !singleFile, overwrite);
+
+ // export season thumbs
+ for (const auto &i : seasonArt)
+ {
+ std::string seasonThumb;
+ if (i.first == -1)
+ seasonThumb = "season-all";
+ else if (i.first == 0)
+ seasonThumb = "season-specials";
+ else
+ seasonThumb = StringUtils::Format("season{:02}", i.first);
+ for (const auto &j : i.second)
+ {
+ std::string savedThumb(item.GetLocalArt(seasonThumb + "-" + j.first, true));
+ if (!i.second.empty())
+ CServiceBroker::GetTextureCache()->Export(j.second, savedThumb, overwrite);
+ }
+ }
+ }
+
+ // now save the episodes from this show
+ sql = PrepareSQL("select * from episode_view where idShow=%i order by strFileName, idEpisode",tvshow.m_iDbId);
+ pDS->query(sql);
+ std::string showDir(item.GetPath());
+
+ while (!pDS->eof())
+ {
+ CVideoInfoTag episode = GetDetailsForEpisode(pDS, VideoDbDetailsAll);
+ std::map<std::string, std::string> artwork;
+ if (GetArtForItem(episode.m_iDbId, MediaTypeEpisode, artwork) && singleFile)
+ {
+ TiXmlElement additionalNode("art");
+ for (const auto &i : artwork)
+ XMLUtils::SetString(&additionalNode, i.first.c_str(), i.second);
+ episode.Save(pMain->LastChild(), "episodedetails", true, &additionalNode);
+ }
+ else if (singleFile)
+ episode.Save(pMain->LastChild(), "episodedetails", singleFile);
+ else
+ episode.Save(pMain, "episodedetails", singleFile);
+ pDS->next();
+ // multi-episode files need dumping to the same XML
+ while (!singleFile && !pDS->eof() &&
+ episode.m_iFileId == pDS->fv("idFile").get_asInt())
+ {
+ episode = GetDetailsForEpisode(pDS, VideoDbDetailsAll);
+ episode.Save(pMain, "episodedetails", singleFile);
+ pDS->next();
+ }
+
+ // reset old skip state
+ bool bSkip = false;
+
+ CFileItem item(episode.m_strFileNameAndPath, false);
+ if (!singleFile && CUtil::SupportsWriteFileOperations(episode.m_strFileNameAndPath))
+ {
+ if (!item.Exists(false))
+ {
+ CLog::Log(LOGINFO, "{} - Not exporting item {} as it does not exist", __FUNCTION__,
+ episode.m_strFileNameAndPath);
+ bSkip = true;
+ }
+ else
+ {
+ std::string nfoFile(URIUtils::ReplaceExtension(item.GetTBNFile(), ".nfo"));
+
+ if (overwrite || !CFile::Exists(nfoFile, false))
+ {
+ if(!xmlDoc.SaveFile(nfoFile))
+ {
+ CLog::Log(LOGERROR, "{}: Episode nfo export failed! ('{}')", __FUNCTION__, nfoFile);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error,
+ g_localizeStrings.Get(20302),
+ CURL::GetRedacted(nfoFile));
+ iFailCount++;
+ }
+ }
+ }
+ }
+ if (!singleFile)
+ {
+ xmlDoc.Clear();
+ TiXmlDeclaration decl("1.0", "UTF-8", "yes");
+ xmlDoc.InsertEndChild(decl);
+ }
+
+ if (images && !bSkip)
+ {
+ if (singleFile)
+ {
+ std::string epName =
+ StringUtils::Format("s{:02}e{:02}.avi", episode.m_iSeason, episode.m_iEpisode);
+ item.SetPath(URIUtils::AddFileToFolder(showDir, epName));
+ }
+ for (const auto &i : artwork)
+ {
+ std::string savedThumb = item.GetLocalArt(i.first, false);
+ CServiceBroker::GetTextureCache()->Export(i.second, savedThumb, overwrite);
+ }
+ if (actorThumbs)
+ ExportActorThumbs(actorsDir, episode, !singleFile, overwrite);
+ }
+ }
+ pDS->close();
+ m_pDS->next();
+ current++;
+ }
+ m_pDS->close();
+
+ if (!singleFile && progress)
+ {
+ progress->SetPercentage(100);
+ progress->Progress();
+ }
+
+ if (singleFile)
+ {
+ // now dump path info
+ std::set<std::string> paths;
+ GetPaths(paths);
+ TiXmlElement xmlPathElement("paths");
+ TiXmlNode *pPaths = pMain->InsertEndChild(xmlPathElement);
+ for (const auto &i : paths)
+ {
+ bool foundDirectly = false;
+ SScanSettings settings;
+ ScraperPtr info = GetScraperForPath(i, settings, foundDirectly);
+ if (info && foundDirectly)
+ {
+ TiXmlElement xmlPathElement2("path");
+ TiXmlNode *pPath = pPaths->InsertEndChild(xmlPathElement2);
+ XMLUtils::SetString(pPath,"url", i);
+ XMLUtils::SetInt(pPath,"scanrecursive", settings.recurse);
+ XMLUtils::SetBoolean(pPath,"usefoldernames", settings.parent_name);
+ XMLUtils::SetString(pPath,"content", TranslateContent(info->Content()));
+ XMLUtils::SetString(pPath,"scraperpath", info->ID());
+ }
+ }
+ xmlDoc.SaveFile(xmlFile);
+ }
+ CVariant data;
+ if (singleFile)
+ {
+ data["root"] = exportRoot;
+ data["file"] = xmlFile;
+ if (iFailCount > 0)
+ data["failcount"] = iFailCount;
+ }
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnExport",
+ data);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ iFailCount++;
+ }
+
+ if (progress)
+ progress->Close();
+
+ if (iFailCount > 0)
+ HELPERS::ShowOKDialogText(
+ CVariant{647}, CVariant{StringUtils::Format(g_localizeStrings.Get(15011), iFailCount)});
+}
+
+void CVideoDatabase::ExportActorThumbs(const std::string &strDir, const CVideoInfoTag &tag, bool singleFiles, bool overwrite /*=false*/)
+{
+ std::string strPath(strDir);
+ if (singleFiles)
+ {
+ strPath = URIUtils::AddFileToFolder(tag.m_strPath, ".actors");
+ if (!CDirectory::Exists(strPath))
+ {
+ CDirectory::Create(strPath);
+ CFile::SetHidden(strPath, true);
+ }
+ }
+
+ for (const auto &i : tag.m_cast)
+ {
+ CFileItem item;
+ item.SetLabel(i.strName);
+ if (!i.thumb.empty())
+ {
+ std::string thumbFile(GetSafeFile(strPath, i.strName));
+ CServiceBroker::GetTextureCache()->Export(i.thumb, thumbFile, overwrite);
+ }
+ }
+}
+
+void CVideoDatabase::ImportFromXML(const std::string &path)
+{
+ CGUIDialogProgress *progress=NULL;
+ try
+ {
+ if (nullptr == m_pDB)
+ return;
+ if (nullptr == m_pDS)
+ return;
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(URIUtils::AddFileToFolder(path, "videodb.xml")))
+ return;
+
+ TiXmlElement *root = xmlDoc.RootElement();
+ if (!root) return;
+
+ progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (progress)
+ {
+ progress->SetHeading(CVariant{648});
+ progress->SetLine(0, CVariant{649});
+ progress->SetLine(1, CVariant{330});
+ progress->SetLine(2, CVariant{""});
+ progress->SetPercentage(0);
+ progress->Open();
+ progress->ShowProgressBar(true);
+ }
+
+ int iVersion = 0;
+ XMLUtils::GetInt(root, "version", iVersion);
+
+ CLog::Log(LOGINFO, "{}: Starting import (export version = {})", __FUNCTION__, iVersion);
+
+ TiXmlElement *movie = root->FirstChildElement();
+ int current = 0;
+ int total = 0;
+ // first count the number of items...
+ while (movie)
+ {
+ if (StringUtils::CompareNoCase(movie->Value(), MediaTypeMovie, 5) == 0 ||
+ StringUtils::CompareNoCase(movie->Value(), MediaTypeTvShow, 6) == 0 ||
+ StringUtils::CompareNoCase(movie->Value(), MediaTypeMusicVideo, 10) == 0)
+ total++;
+ movie = movie->NextSiblingElement();
+ }
+
+ std::string actorsDir(URIUtils::AddFileToFolder(path, "actors"));
+ std::string moviesDir(URIUtils::AddFileToFolder(path, "movies"));
+ std::string movieSetsDir(URIUtils::AddFileToFolder(path, "moviesets"));
+ std::string musicvideosDir(URIUtils::AddFileToFolder(path, "musicvideos"));
+ std::string tvshowsDir(URIUtils::AddFileToFolder(path, "tvshows"));
+ CVideoInfoScanner scanner;
+ // add paths first (so we have scraper settings available)
+ TiXmlElement *path = root->FirstChildElement("paths");
+ path = path->FirstChildElement();
+ while (path)
+ {
+ std::string strPath;
+ if (XMLUtils::GetString(path,"url",strPath) && !strPath.empty())
+ AddPath(strPath);
+
+ std::string content;
+ if (XMLUtils::GetString(path,"content", content) && !content.empty())
+ { // check the scraper exists, if so store the path
+ AddonPtr addon;
+ std::string id;
+ XMLUtils::GetString(path,"scraperpath",id);
+ if (CServiceBroker::GetAddonMgr().GetAddon(id, addon, ADDON::OnlyEnabled::CHOICE_YES))
+ {
+ SScanSettings settings;
+ ScraperPtr scraper = std::dynamic_pointer_cast<CScraper>(addon);
+ // FIXME: scraper settings are not exported?
+ scraper->SetPathSettings(TranslateContent(content), "");
+ XMLUtils::GetInt(path,"scanrecursive",settings.recurse);
+ XMLUtils::GetBoolean(path,"usefoldernames",settings.parent_name);
+ SetScraperForPath(strPath,scraper,settings);
+ }
+ }
+ path = path->NextSiblingElement();
+ }
+ movie = root->FirstChildElement();
+ while (movie)
+ {
+ CVideoInfoTag info;
+ if (StringUtils::CompareNoCase(movie->Value(), MediaTypeMovie, 5) == 0)
+ {
+ info.Load(movie);
+ CFileItem item(info);
+ bool useFolders = info.m_basePath.empty() ? LookupByFolders(item.GetPath()) : false;
+ std::string filename = info.m_strTitle;
+ if (info.HasYear())
+ filename += StringUtils::Format("_{}", info.GetYear());
+ CFileItem artItem(item);
+ artItem.SetPath(GetSafeFile(moviesDir, filename) + ".avi");
+ scanner.GetArtwork(&artItem, CONTENT_MOVIES, useFolders, true, actorsDir);
+ item.SetArt(artItem.GetArt());
+ if (!item.GetVideoInfoTag()->m_set.title.empty())
+ {
+ std::string setPath = URIUtils::AddFileToFolder(movieSetsDir,
+ CUtil::MakeLegalFileName(item.GetVideoInfoTag()->m_set.title, LEGAL_WIN32_COMPAT));
+ if (CDirectory::Exists(setPath))
+ {
+ CGUIListItem::ArtMap setArt;
+ CFileItem artItem(setPath, true);
+ for (const auto& artType : CVideoThumbLoader::GetArtTypes(MediaTypeVideoCollection))
+ {
+ std::string artPath = CVideoThumbLoader::GetLocalArt(artItem, artType, true);
+ if (!artPath.empty())
+ {
+ setArt[artType] = artPath;
+ }
+ }
+ item.AppendArt(setArt, "set");
+ }
+ }
+ scanner.AddVideo(&item, CONTENT_MOVIES, useFolders, true, NULL, true);
+ current++;
+ }
+ else if (StringUtils::CompareNoCase(movie->Value(), MediaTypeMusicVideo, 10) == 0)
+ {
+ info.Load(movie);
+ CFileItem item(info);
+ bool useFolders = info.m_basePath.empty() ? LookupByFolders(item.GetPath()) : false;
+ std::string filename = StringUtils::Join(info.m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator) + "." + info.m_strTitle;
+ if (info.HasYear())
+ filename += StringUtils::Format("_{}", info.GetYear());
+ CFileItem artItem(item);
+ artItem.SetPath(GetSafeFile(musicvideosDir, filename) + ".avi");
+ scanner.GetArtwork(&artItem, CONTENT_MUSICVIDEOS, useFolders, true, actorsDir);
+ item.SetArt(artItem.GetArt());
+ scanner.AddVideo(&item, CONTENT_MUSICVIDEOS, useFolders, true, NULL, true);
+ current++;
+ }
+ else if (StringUtils::CompareNoCase(movie->Value(), MediaTypeTvShow, 6) == 0)
+ {
+ // load the TV show in. NOTE: This deletes all episodes under the TV Show, which may not be
+ // what we desire. It may make better sense to only delete (or even better, update) the show information
+ info.Load(movie);
+ URIUtils::AddSlashAtEnd(info.m_strPath);
+ DeleteTvShow(info.m_strPath);
+ CFileItem showItem(info);
+ bool useFolders = info.m_basePath.empty() ? LookupByFolders(showItem.GetPath(), true) : false;
+ CFileItem artItem(showItem);
+ std::string artPath(GetSafeFile(tvshowsDir, info.m_strTitle));
+ artItem.SetPath(artPath);
+ scanner.GetArtwork(&artItem, CONTENT_TVSHOWS, useFolders, true, actorsDir);
+ showItem.SetArt(artItem.GetArt());
+ int showID = scanner.AddVideo(&showItem, CONTENT_TVSHOWS, useFolders, true, NULL, true);
+ // season artwork
+ std::map<int, std::map<std::string, std::string> > seasonArt;
+ artItem.GetVideoInfoTag()->m_strPath = artPath;
+ scanner.GetSeasonThumbs(*artItem.GetVideoInfoTag(), seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), true);
+ for (const auto &i : seasonArt)
+ {
+ int seasonID = AddSeason(showID, i.first);
+ SetArtForItem(seasonID, MediaTypeSeason, i.second);
+ }
+ current++;
+ // now load the episodes
+ TiXmlElement *episode = movie->FirstChildElement("episodedetails");
+ while (episode)
+ {
+ // no need to delete the episode info, due to the above deletion
+ CVideoInfoTag info;
+ info.Load(episode);
+ CFileItem item(info);
+ std::string filename =
+ StringUtils::Format("s{:02}e{:02}.avi", info.m_iSeason, info.m_iEpisode);
+ CFileItem artItem(item);
+ artItem.SetPath(GetSafeFile(artPath, filename));
+ scanner.GetArtwork(&artItem, CONTENT_TVSHOWS, useFolders, true, actorsDir);
+ item.SetArt(artItem.GetArt());
+ scanner.AddVideo(&item,CONTENT_TVSHOWS, false, false, showItem.GetVideoInfoTag(), true);
+ episode = episode->NextSiblingElement("episodedetails");
+ }
+ }
+ movie = movie->NextSiblingElement();
+ if (progress && total)
+ {
+ progress->SetPercentage(current * 100 / total);
+ progress->SetLine(2, CVariant{info.m_strTitle});
+ progress->Progress();
+ if (progress->IsCanceled())
+ {
+ progress->Close();
+ return;
+ }
+ }
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+ if (progress)
+ progress->Close();
+}
+
+bool CVideoDatabase::ImportArtFromXML(const TiXmlNode *node, std::map<std::string, std::string> &artwork)
+{
+ if (!node) return false;
+ const TiXmlNode *art = node->FirstChild();
+ while (art && art->FirstChild())
+ {
+ artwork.insert(make_pair(art->ValueStr(), art->FirstChild()->ValueStr()));
+ art = art->NextSibling();
+ }
+ return !artwork.empty();
+}
+
+void CVideoDatabase::ConstructPath(std::string& strDest, const std::string& strPath, const std::string& strFileName)
+{
+ if (URIUtils::IsStack(strFileName) ||
+ URIUtils::IsInArchive(strFileName) || URIUtils::IsPlugin(strPath))
+ strDest = strFileName;
+ else
+ strDest = URIUtils::AddFileToFolder(strPath, strFileName);
+}
+
+void CVideoDatabase::SplitPath(const std::string& strFileNameAndPath, std::string& strPath, std::string& strFileName)
+{
+ if (URIUtils::IsStack(strFileNameAndPath) || StringUtils::StartsWithNoCase(strFileNameAndPath, "rar://") || StringUtils::StartsWithNoCase(strFileNameAndPath, "zip://"))
+ {
+ URIUtils::GetParentPath(strFileNameAndPath,strPath);
+ strFileName = strFileNameAndPath;
+ }
+ else if (URIUtils::IsPlugin(strFileNameAndPath))
+ {
+ CURL url(strFileNameAndPath);
+ strPath = url.GetOptions().empty() ? url.GetWithoutFilename() : url.GetWithoutOptions();
+ strFileName = strFileNameAndPath;
+ }
+ else
+ {
+ URIUtils::Split(strFileNameAndPath, strPath, strFileName);
+ // Keep protocol options as part of the path
+ if (URIUtils::IsURL(strFileNameAndPath))
+ {
+ CURL url(strFileNameAndPath);
+ if (!url.GetProtocolOptions().empty())
+ strPath += "|" + url.GetProtocolOptions();
+ }
+ }
+}
+
+void CVideoDatabase::InvalidatePathHash(const std::string& strPath)
+{
+ SScanSettings settings;
+ bool foundDirectly;
+ ScraperPtr info = GetScraperForPath(strPath,settings,foundDirectly);
+ SetPathHash(strPath,"");
+ if (!info)
+ return;
+ if (info->Content() == CONTENT_TVSHOWS || (info->Content() == CONTENT_MOVIES && !foundDirectly)) // if we scan by folder name we need to invalidate parent as well
+ {
+ if (info->Content() == CONTENT_TVSHOWS || settings.parent_name_root)
+ {
+ std::string strParent;
+ if (URIUtils::GetParentPath(strPath, strParent) && (!URIUtils::IsPlugin(strPath) || !CURL(strParent).GetHostName().empty()))
+ SetPathHash(strParent, "");
+ }
+ }
+}
+
+bool CVideoDatabase::CommitTransaction()
+{
+ if (CDatabase::CommitTransaction())
+ { // number of items in the db has likely changed, so recalculate
+ GUIINFO::CLibraryGUIInfo& guiInfo = CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider();
+ guiInfo.SetLibraryBool(LIBRARY_HAS_MOVIES, HasContent(VideoDbContentType::MOVIES));
+ guiInfo.SetLibraryBool(LIBRARY_HAS_TVSHOWS, HasContent(VideoDbContentType::TVSHOWS));
+ guiInfo.SetLibraryBool(LIBRARY_HAS_MUSICVIDEOS, HasContent(VideoDbContentType::MUSICVIDEOS));
+ return true;
+ }
+ return false;
+}
+
+bool CVideoDatabase::SetSingleValue(VideoDbContentType type,
+ int dbId,
+ int dbField,
+ const std::string& strValue)
+{
+ std::string strSQL;
+ try
+ {
+ if (nullptr == m_pDB || nullptr == m_pDS)
+ return false;
+
+ std::string strTable, strField;
+ if (type == VideoDbContentType::MOVIES)
+ {
+ strTable = "movie";
+ strField = "idMovie";
+ }
+ else if (type == VideoDbContentType::TVSHOWS)
+ {
+ strTable = "tvshow";
+ strField = "idShow";
+ }
+ else if (type == VideoDbContentType::EPISODES)
+ {
+ strTable = "episode";
+ strField = "idEpisode";
+ }
+ else if (type == VideoDbContentType::MUSICVIDEOS)
+ {
+ strTable = "musicvideo";
+ strField = "idMVideo";
+ }
+
+ if (strTable.empty())
+ return false;
+
+ return SetSingleValue(strTable, StringUtils::Format("c{:02}", dbField), strValue, strField,
+ dbId);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, strSQL);
+ }
+ return false;
+}
+
+bool CVideoDatabase::SetSingleValue(VideoDbContentType type,
+ int dbId,
+ Field dbField,
+ const std::string& strValue)
+{
+ MediaType mediaType = DatabaseUtils::MediaTypeFromVideoContentType(type);
+ if (mediaType == MediaTypeNone)
+ return false;
+
+ int dbFieldIndex = DatabaseUtils::GetField(dbField, mediaType);
+ if (dbFieldIndex < 0)
+ return false;
+
+ return SetSingleValue(type, dbId, dbFieldIndex, strValue);
+}
+
+bool CVideoDatabase::SetSingleValue(const std::string &table, const std::string &fieldName, const std::string &strValue,
+ const std::string &conditionName /* = "" */, int conditionValue /* = -1 */)
+{
+ if (table.empty() || fieldName.empty())
+ return false;
+
+ std::string sql;
+ try
+ {
+ if (nullptr == m_pDB || nullptr == m_pDS)
+ return false;
+
+ sql = PrepareSQL("UPDATE %s SET %s='%s'", table.c_str(), fieldName.c_str(), strValue.c_str());
+ if (!conditionName.empty())
+ sql += PrepareSQL(" WHERE %s=%u", conditionName.c_str(), conditionValue);
+ if (m_pDS->exec(sql) == 0)
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}) failed", __FUNCTION__, sql);
+ }
+ return false;
+}
+
+std::string CVideoDatabase::GetSafeFile(const std::string &dir, const std::string &name) const
+{
+ std::string safeThumb(name);
+ StringUtils::Replace(safeThumb, ' ', '_');
+ return URIUtils::AddFileToFolder(dir, CUtil::MakeLegalFileName(safeThumb));
+}
+
+void CVideoDatabase::AnnounceRemove(const std::string& content, int id, bool scanning /* = false */)
+{
+ CVariant data;
+ data["type"] = content;
+ data["id"] = id;
+ if (scanning)
+ data["transaction"] = true;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnRemove", data);
+}
+
+void CVideoDatabase::AnnounceUpdate(const std::string& content, int id)
+{
+ CVariant data;
+ data["type"] = content;
+ data["id"] = id;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnUpdate", data);
+}
+
+bool CVideoDatabase::GetItemsForPath(const std::string &content, const std::string &strPath, CFileItemList &items)
+{
+ const std::string& path(strPath);
+
+ if(URIUtils::IsMultiPath(path))
+ {
+ std::vector<std::string> paths;
+ CMultiPathDirectory::GetPaths(path, paths);
+
+ for(unsigned i=0;i<paths.size();i++)
+ GetItemsForPath(content, paths[i], items);
+
+ return items.Size() > 0;
+ }
+
+ int pathID = GetPathId(path);
+ if (pathID < 0)
+ return false;
+
+ if (content == "movies")
+ {
+ Filter filter(PrepareSQL("c%02d=%d", VIDEODB_ID_PARENTPATHID, pathID));
+ GetMoviesByWhere("videodb://movies/titles/", filter, items);
+ }
+ else if (content == "episodes")
+ {
+ Filter filter(PrepareSQL("c%02d=%d", VIDEODB_ID_EPISODE_PARENTPATHID, pathID));
+ GetEpisodesByWhere("videodb://tvshows/titles/", filter, items);
+ }
+ else if (content == "tvshows")
+ {
+ Filter filter(PrepareSQL("idParentPath=%d", pathID));
+ GetTvShowsByWhere("videodb://tvshows/titles/", filter, items);
+ }
+ else if (content == "musicvideos")
+ {
+ Filter filter(PrepareSQL("c%02d=%d", VIDEODB_ID_MUSICVIDEO_PARENTPATHID, pathID));
+ GetMusicVideosByWhere("videodb://musicvideos/titles/", filter, items);
+ }
+ for (int i = 0; i < items.Size(); i++)
+ items[i]->SetPath(items[i]->GetVideoInfoTag()->m_basePath);
+ return items.Size() > 0;
+}
+
+void CVideoDatabase::AppendIdLinkFilter(const char* field, const char *table, const MediaType& mediaType, const char *view, const char *viewKey, const CUrlOptions::UrlOptions& options, Filter &filter)
+{
+ auto option = options.find((std::string)field + "id");
+ if (option == options.end())
+ return;
+
+ filter.AppendJoin(PrepareSQL("JOIN %s_link ON %s_link.media_id=%s_view.%s AND %s_link.media_type='%s'", field, field, view, viewKey, field, mediaType.c_str()));
+ filter.AppendWhere(PrepareSQL("%s_link.%s_id = %i", field, table, (int)option->second.asInteger()));
+}
+
+void CVideoDatabase::AppendLinkFilter(const char* field, const char *table, const MediaType& mediaType, const char *view, const char *viewKey, const CUrlOptions::UrlOptions& options, Filter &filter)
+{
+ auto option = options.find(field);
+ if (option == options.end())
+ return;
+
+ filter.AppendJoin(PrepareSQL("JOIN %s_link ON %s_link.media_id=%s_view.%s AND %s_link.media_type='%s'", field, field, view, viewKey, field, mediaType.c_str()));
+ filter.AppendJoin(PrepareSQL("JOIN %s ON %s.%s_id=%s_link.%s_id", table, table, field, table, field));
+ filter.AppendWhere(PrepareSQL("%s.name like '%s'", table, option->second.asString().c_str()));
+}
+
+bool CVideoDatabase::GetFilter(CDbUrl &videoUrl, Filter &filter, SortDescription &sorting)
+{
+ if (!videoUrl.IsValid())
+ return false;
+
+ std::string type = videoUrl.GetType();
+ std::string itemType = ((const CVideoDbUrl &)videoUrl).GetItemType();
+ const CUrlOptions::UrlOptions& options = videoUrl.GetOptions();
+
+ if (type == "movies")
+ {
+ AppendIdLinkFilter("genre", "genre", "movie", "movie", "idMovie", options, filter);
+ AppendLinkFilter("genre", "genre", "movie", "movie", "idMovie", options, filter);
+
+ AppendIdLinkFilter("country", "country", "movie", "movie", "idMovie", options, filter);
+ AppendLinkFilter("country", "country", "movie", "movie", "idMovie", options, filter);
+
+ AppendIdLinkFilter("studio", "studio", "movie", "movie", "idMovie", options, filter);
+ AppendLinkFilter("studio", "studio", "movie", "movie", "idMovie", options, filter);
+
+ AppendIdLinkFilter("director", "actor", "movie", "movie", "idMovie", options, filter);
+ AppendLinkFilter("director", "actor", "movie", "movie", "idMovie", options, filter);
+
+ auto option = options.find("year");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("movie_view.premiered like '%i%%'", (int)option->second.asInteger()));
+
+ AppendIdLinkFilter("actor", "actor", "movie", "movie", "idMovie", options, filter);
+ AppendLinkFilter("actor", "actor", "movie", "movie", "idMovie", options, filter);
+
+ option = options.find("setid");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("movie_view.idSet = %i", (int)option->second.asInteger()));
+
+ option = options.find("set");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("movie_view.strSet LIKE '%s'", option->second.asString().c_str()));
+
+ AppendIdLinkFilter("tag", "tag", "movie", "movie", "idMovie", options, filter);
+ AppendLinkFilter("tag", "tag", "movie", "movie", "idMovie", options, filter);
+ }
+ else if (type == "tvshows")
+ {
+ if (itemType == "tvshows")
+ {
+ AppendIdLinkFilter("genre", "genre", "tvshow", "tvshow", "idShow", options, filter);
+ AppendLinkFilter("genre", "genre", "tvshow", "tvshow", "idShow", options, filter);
+
+ AppendIdLinkFilter("studio", "studio", "tvshow", "tvshow", "idShow", options, filter);
+ AppendLinkFilter("studio", "studio", "tvshow", "tvshow", "idShow", options, filter);
+
+ AppendIdLinkFilter("director", "actor", "tvshow", "tvshow", "idShow", options, filter);
+
+ auto option = options.find("year");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("tvshow_view.c%02d like '%%%i%%'", VIDEODB_ID_TV_PREMIERED, (int)option->second.asInteger()));
+
+ AppendIdLinkFilter("actor", "actor", "tvshow", "tvshow", "idShow", options, filter);
+ AppendLinkFilter("actor", "actor", "tvshow", "tvshow", "idShow", options, filter);
+
+ AppendIdLinkFilter("tag", "tag", "tvshow", "tvshow", "idShow", options, filter);
+ AppendLinkFilter("tag", "tag", "tvshow", "tvshow", "idShow", options, filter);
+ }
+ else if (itemType == "seasons")
+ {
+ auto option = options.find("tvshowid");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("season_view.idShow = %i", (int)option->second.asInteger()));
+
+ AppendIdLinkFilter("genre", "genre", "tvshow", "season", "idShow", options, filter);
+
+ AppendIdLinkFilter("director", "actor", "tvshow", "season", "idShow", options, filter);
+
+ option = options.find("year");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("season_view.premiered like '%%%i%%'", (int)option->second.asInteger()));
+
+ AppendIdLinkFilter("actor", "actor", "tvshow", "season", "idShow", options, filter);
+ }
+ else if (itemType == "episodes")
+ {
+ int idShow = -1;
+ auto option = options.find("tvshowid");
+ if (option != options.end())
+ idShow = (int)option->second.asInteger();
+
+ int season = -1;
+ option = options.find("season");
+ if (option != options.end())
+ season = (int)option->second.asInteger();
+
+ if (idShow > -1)
+ {
+ bool condition = false;
+
+ AppendIdLinkFilter("genre", "genre", "tvshow", "episode", "idShow", options, filter);
+ AppendLinkFilter("genre", "genre", "tvshow", "episode", "idShow", options, filter);
+
+ AppendIdLinkFilter("director", "actor", "tvshow", "episode", "idShow", options, filter);
+ AppendLinkFilter("director", "actor", "tvshow", "episode", "idShow", options, filter);
+
+ option = options.find("year");
+ if (option != options.end())
+ {
+ condition = true;
+ filter.AppendWhere(PrepareSQL("episode_view.idShow = %i and episode_view.premiered like '%%%i%%'", idShow, (int)option->second.asInteger()));
+ }
+
+ AppendIdLinkFilter("actor", "actor", "tvshow", "episode", "idShow", options, filter);
+ AppendLinkFilter("actor", "actor", "tvshow", "episode", "idShow", options, filter);
+
+ if (!condition)
+ filter.AppendWhere(PrepareSQL("episode_view.idShow = %i", idShow));
+
+ if (season > -1)
+ {
+ if (season == 0) // season = 0 indicates a special - we grab all specials here (see below)
+ filter.AppendWhere(PrepareSQL("episode_view.c%02d = %i", VIDEODB_ID_EPISODE_SEASON, season));
+ else
+ filter.AppendWhere(PrepareSQL("(episode_view.c%02d = %i or (episode_view.c%02d = 0 and (episode_view.c%02d = 0 or episode_view.c%02d = %i)))",
+ VIDEODB_ID_EPISODE_SEASON, season, VIDEODB_ID_EPISODE_SEASON, VIDEODB_ID_EPISODE_SORTSEASON, VIDEODB_ID_EPISODE_SORTSEASON, season));
+ }
+ }
+ else
+ {
+ option = options.find("year");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("episode_view.premiered like '%%%i%%'", (int)option->second.asInteger()));
+
+ AppendIdLinkFilter("director", "actor", "episode", "episode", "idEpisode", options, filter);
+ AppendLinkFilter("director", "actor", "episode", "episode", "idEpisode", options, filter);
+ }
+ }
+ }
+ else if (type == "musicvideos")
+ {
+ AppendIdLinkFilter("genre", "genre", "musicvideo", "musicvideo", "idMVideo", options, filter);
+ AppendLinkFilter("genre", "genre", "musicvideo", "musicvideo", "idMVideo", options, filter);
+
+ AppendIdLinkFilter("studio", "studio", "musicvideo", "musicvideo", "idMVideo", options, filter);
+ AppendLinkFilter("studio", "studio", "musicvideo", "musicvideo", "idMVideo", options, filter);
+
+ AppendIdLinkFilter("director", "actor", "musicvideo", "musicvideo", "idMVideo", options, filter);
+ AppendLinkFilter("director", "actor", "musicvideo", "musicvideo", "idMVideo", options, filter);
+
+ auto option = options.find("year");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("musicvideo_view.premiered like '%i%%'", (int)option->second.asInteger()));
+
+ option = options.find("artistid");
+ if (option != options.end())
+ {
+ if (itemType != "albums")
+ filter.AppendJoin(PrepareSQL("JOIN actor_link ON actor_link.media_id=musicvideo_view.idMVideo AND actor_link.media_type='musicvideo'"));
+ filter.AppendWhere(PrepareSQL("actor_link.actor_id = %i", (int)option->second.asInteger()));
+ }
+
+ option = options.find("artist");
+ if (option != options.end())
+ {
+ if (itemType != "albums")
+ {
+ filter.AppendJoin(PrepareSQL("JOIN actor_link ON actor_link.media_id=musicvideo_view.idMVideo AND actor_link.media_type='musicvideo'"));
+ filter.AppendJoin(PrepareSQL("JOIN actor ON actor.actor_id=actor_link.actor_id"));
+ }
+ filter.AppendWhere(PrepareSQL("actor.name LIKE '%s'", option->second.asString().c_str()));
+ }
+
+ option = options.find("albumid");
+ if (option != options.end())
+ filter.AppendWhere(PrepareSQL("musicvideo_view.c%02d = (select c%02d from musicvideo where idMVideo = %i)", VIDEODB_ID_MUSICVIDEO_ALBUM, VIDEODB_ID_MUSICVIDEO_ALBUM, (int)option->second.asInteger()));
+
+ AppendIdLinkFilter("tag", "tag", "musicvideo", "musicvideo", "idMVideo", options, filter);
+ AppendLinkFilter("tag", "tag", "musicvideo", "musicvideo", "idMVideo", options, filter);
+ }
+ else
+ return false;
+
+ auto option = options.find("xsp");
+ if (option != options.end())
+ {
+ CSmartPlaylist xsp;
+ if (!xsp.LoadFromJson(option->second.asString()))
+ return false;
+
+ // check if the filter playlist matches the item type
+ if (xsp.GetType() == itemType ||
+ (xsp.GetGroup() == itemType && !xsp.IsGroupMixed()) ||
+ // handle episode listings with videodb://tvshows/titles/ which get the rest
+ // of the path (season and episodeid) appended later
+ (xsp.GetType() == "episodes" && itemType == "tvshows"))
+ {
+ std::set<std::string> playlists;
+ filter.AppendWhere(xsp.GetWhereClause(*this, playlists));
+
+ if (xsp.GetLimit() > 0)
+ sorting.limitEnd = xsp.GetLimit();
+ if (xsp.GetOrder() != SortByNone)
+ sorting.sortBy = xsp.GetOrder();
+ if (xsp.GetOrderDirection() != SortOrderNone)
+ sorting.sortOrder = xsp.GetOrderDirection();
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING))
+ sorting.sortAttributes = SortAttributeIgnoreArticle;
+ }
+ }
+
+ option = options.find("filter");
+ if (option != options.end())
+ {
+ CSmartPlaylist xspFilter;
+ if (!xspFilter.LoadFromJson(option->second.asString()))
+ return false;
+
+ // check if the filter playlist matches the item type
+ if (xspFilter.GetType() == itemType)
+ {
+ std::set<std::string> playlists;
+ filter.AppendWhere(xspFilter.GetWhereClause(*this, playlists));
+ }
+ // remove the filter if it doesn't match the item type
+ else
+ videoUrl.RemoveOption("filter");
+ }
+
+ return true;
+}
+
+bool CVideoDatabase::SetVideoUserRating(int dbId, int rating, const MediaType& mediaType)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ if (mediaType == MediaTypeNone)
+ return false;
+
+ std::string sql;
+ if (mediaType == MediaTypeMovie)
+ sql = PrepareSQL("UPDATE movie SET userrating=%i WHERE idMovie = %i", rating, dbId);
+ else if (mediaType == MediaTypeEpisode)
+ sql = PrepareSQL("UPDATE episode SET userrating=%i WHERE idEpisode = %i", rating, dbId);
+ else if (mediaType == MediaTypeMusicVideo)
+ sql = PrepareSQL("UPDATE musicvideo SET userrating=%i WHERE idMVideo = %i", rating, dbId);
+ else if (mediaType == MediaTypeTvShow)
+ sql = PrepareSQL("UPDATE tvshow SET userrating=%i WHERE idShow = %i", rating, dbId);
+ else if (mediaType == MediaTypeSeason)
+ sql = PrepareSQL("UPDATE seasons SET userrating=%i WHERE idSeason = %i", rating, dbId);
+
+ m_pDS->exec(sql);
+ return true;
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} ({}, {}, {}) failed", __FUNCTION__, dbId, mediaType, rating);
+ }
+ return false;
+}
+
+CDateTime CVideoDatabase::GetDateAdded(const std::string& filename,
+ CDateTime dateAdded /* = CDateTime() */)
+{
+ if (!dateAdded.IsValid())
+ {
+ // suppress warnings if we have plugin source
+ if (!URIUtils::IsPlugin(filename))
+ {
+ const auto dateAddedSetting =
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iVideoLibraryDateAdded;
+
+ // 1 prefer using the files mtime (if it's valid) and
+ // only use the file's ctime if mtime isn't valid
+ if (dateAddedSetting == 1)
+ dateAdded = CFileUtils::GetModificationDate(filename, false);
+ // 2 use the newer datetime of the file's mtime and ctime
+ else if (dateAddedSetting == 2)
+ dateAdded = CFileUtils::GetModificationDate(filename, true);
+ }
+
+ // 0 use the current datetime if non of the above match or one returns an invalid datetime
+ if (!dateAdded.IsValid())
+ dateAdded = CDateTime::GetCurrentDateTime();
+ }
+
+ return dateAdded;
+}
+
+void CVideoDatabase::EraseAllForPath(const std::string& path)
+{
+ try
+ {
+ std::string itemsToDelete;
+ std::string sql =
+ PrepareSQL("SELECT files.idFile FROM files WHERE idFile IN (SELECT idFile FROM files INNER "
+ "JOIN path ON path.idPath = files.idPath AND path.strPath LIKE \"%s%%\")",
+ path.c_str());
+
+ m_pDS->query(sql);
+ while (!m_pDS->eof())
+ {
+ std::string file = m_pDS->fv("files.idFile").get_asString() + ",";
+ itemsToDelete += file;
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ sql = PrepareSQL("DELETE FROM path WHERE strPath LIKE \"%s%%\"", path.c_str());
+ m_pDS->exec(sql);
+
+ if (!itemsToDelete.empty())
+ {
+ itemsToDelete = "(" + StringUtils::TrimRight(itemsToDelete, ",") + ")";
+
+ sql = "DELETE FROM files WHERE idFile IN " + itemsToDelete;
+ m_pDS->exec(sql);
+
+ sql = "DELETE FROM settings WHERE idFile IN " + itemsToDelete;
+ m_pDS->exec(sql);
+
+ sql = "DELETE FROM bookmark WHERE idFile IN " + itemsToDelete;
+ m_pDS->exec(sql);
+
+ sql = "DELETE FROM streamdetails WHERE idFile IN " + itemsToDelete;
+ m_pDS->exec(sql);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed", __FUNCTION__);
+ }
+}
diff --git a/xbmc/video/VideoDatabase.h b/xbmc/video/VideoDatabase.h
new file mode 100644
index 0000000..af04052
--- /dev/null
+++ b/xbmc/video/VideoDatabase.h
@@ -0,0 +1,1154 @@
+/*
+ * Copyright (C) 2016-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 "Bookmark.h"
+#include "VideoInfoTag.h"
+#include "addons/Scraper.h"
+#include "dbwrappers/Database.h"
+#include "utils/SortUtils.h"
+#include "utils/UrlOptions.h"
+
+#include <memory>
+#include <set>
+#include <utility>
+#include <vector>
+
+class CFileItem;
+class CFileItemList;
+class CVideoSettings;
+class CGUIDialogProgress;
+class CGUIDialogProgressBarHandle;
+
+namespace dbiplus
+{
+ class field_value;
+ typedef std::vector<field_value> sql_record;
+}
+
+#ifndef my_offsetof
+#ifndef TARGET_POSIX
+#define my_offsetof(TYPE, MEMBER) offsetof(TYPE, MEMBER)
+#else
+/*
+ Custom version of standard offsetof() macro which can be used to get
+ offsets of members in class for non-POD types (according to the current
+ version of C++ standard offsetof() macro can't be used in such cases and
+ attempt to do so causes warnings to be emitted, OTOH in many cases it is
+ still OK to assume that all instances of the class has the same offsets
+ for the same members).
+ */
+#define my_offsetof(TYPE, MEMBER) \
+ ((size_t)((char *)&(((TYPE *)0x10)->MEMBER) - (char*)0x10))
+#endif
+#endif
+
+typedef std::vector<CVideoInfoTag> VECMOVIES;
+
+namespace VIDEO
+{
+ class IVideoInfoScannerObserver;
+ struct SScanSettings;
+}
+
+enum VideoDbDetails
+{
+ VideoDbDetailsNone = 0x00,
+ VideoDbDetailsRating = 0x01,
+ VideoDbDetailsTag = 0x02,
+ VideoDbDetailsShowLink = 0x04,
+ VideoDbDetailsStream = 0x08,
+ VideoDbDetailsCast = 0x10,
+ VideoDbDetailsBookmark = 0x20,
+ VideoDbDetailsUniqueID = 0x40,
+ VideoDbDetailsAll = 0xFF
+} ;
+
+// these defines are based on how many columns we have and which column certain data is going to be in
+// when we do GetDetailsForMovie()
+#define VIDEODB_MAX_COLUMNS 24
+#define VIDEODB_DETAILS_FILEID 1
+
+#define VIDEODB_DETAILS_MOVIE_SET_ID VIDEODB_MAX_COLUMNS + 2
+#define VIDEODB_DETAILS_MOVIE_USER_RATING VIDEODB_MAX_COLUMNS + 3
+#define VIDEODB_DETAILS_MOVIE_PREMIERED VIDEODB_MAX_COLUMNS + 4
+#define VIDEODB_DETAILS_MOVIE_SET_NAME VIDEODB_MAX_COLUMNS + 5
+#define VIDEODB_DETAILS_MOVIE_SET_OVERVIEW VIDEODB_MAX_COLUMNS + 6
+#define VIDEODB_DETAILS_MOVIE_FILE VIDEODB_MAX_COLUMNS + 7
+#define VIDEODB_DETAILS_MOVIE_PATH VIDEODB_MAX_COLUMNS + 8
+#define VIDEODB_DETAILS_MOVIE_PLAYCOUNT VIDEODB_MAX_COLUMNS + 9
+#define VIDEODB_DETAILS_MOVIE_LASTPLAYED VIDEODB_MAX_COLUMNS + 10
+#define VIDEODB_DETAILS_MOVIE_DATEADDED VIDEODB_MAX_COLUMNS + 11
+#define VIDEODB_DETAILS_MOVIE_RESUME_TIME VIDEODB_MAX_COLUMNS + 12
+#define VIDEODB_DETAILS_MOVIE_TOTAL_TIME VIDEODB_MAX_COLUMNS + 13
+#define VIDEODB_DETAILS_MOVIE_PLAYER_STATE VIDEODB_MAX_COLUMNS + 14
+#define VIDEODB_DETAILS_MOVIE_RATING VIDEODB_MAX_COLUMNS + 15
+#define VIDEODB_DETAILS_MOVIE_VOTES VIDEODB_MAX_COLUMNS + 16
+#define VIDEODB_DETAILS_MOVIE_RATING_TYPE VIDEODB_MAX_COLUMNS + 17
+#define VIDEODB_DETAILS_MOVIE_UNIQUEID_VALUE VIDEODB_MAX_COLUMNS + 18
+#define VIDEODB_DETAILS_MOVIE_UNIQUEID_TYPE VIDEODB_MAX_COLUMNS + 19
+
+#define VIDEODB_DETAILS_EPISODE_TVSHOW_ID VIDEODB_MAX_COLUMNS + 2
+#define VIDEODB_DETAILS_EPISODE_USER_RATING VIDEODB_MAX_COLUMNS + 3
+#define VIDEODB_DETAILS_EPISODE_SEASON_ID VIDEODB_MAX_COLUMNS + 4
+#define VIDEODB_DETAILS_EPISODE_FILE VIDEODB_MAX_COLUMNS + 5
+#define VIDEODB_DETAILS_EPISODE_PATH VIDEODB_MAX_COLUMNS + 6
+#define VIDEODB_DETAILS_EPISODE_PLAYCOUNT VIDEODB_MAX_COLUMNS + 7
+#define VIDEODB_DETAILS_EPISODE_LASTPLAYED VIDEODB_MAX_COLUMNS + 8
+#define VIDEODB_DETAILS_EPISODE_DATEADDED VIDEODB_MAX_COLUMNS + 9
+#define VIDEODB_DETAILS_EPISODE_TVSHOW_NAME VIDEODB_MAX_COLUMNS + 10
+#define VIDEODB_DETAILS_EPISODE_TVSHOW_GENRE VIDEODB_MAX_COLUMNS + 11
+#define VIDEODB_DETAILS_EPISODE_TVSHOW_STUDIO VIDEODB_MAX_COLUMNS + 12
+#define VIDEODB_DETAILS_EPISODE_TVSHOW_AIRED VIDEODB_MAX_COLUMNS + 13
+#define VIDEODB_DETAILS_EPISODE_TVSHOW_MPAA VIDEODB_MAX_COLUMNS + 14
+#define VIDEODB_DETAILS_EPISODE_RESUME_TIME VIDEODB_MAX_COLUMNS + 15
+#define VIDEODB_DETAILS_EPISODE_TOTAL_TIME VIDEODB_MAX_COLUMNS + 16
+#define VIDEODB_DETAILS_EPISODE_PLAYER_STATE VIDEODB_MAX_COLUMNS + 17
+#define VIDEODB_DETAILS_EPISODE_RATING VIDEODB_MAX_COLUMNS + 18
+#define VIDEODB_DETAILS_EPISODE_VOTES VIDEODB_MAX_COLUMNS + 19
+#define VIDEODB_DETAILS_EPISODE_RATING_TYPE VIDEODB_MAX_COLUMNS + 20
+#define VIDEODB_DETAILS_EPISODE_UNIQUEID_VALUE VIDEODB_MAX_COLUMNS + 21
+#define VIDEODB_DETAILS_EPISODE_UNIQUEID_TYPE VIDEODB_MAX_COLUMNS + 22
+
+#define VIDEODB_DETAILS_TVSHOW_USER_RATING VIDEODB_MAX_COLUMNS + 1
+#define VIDEODB_DETAILS_TVSHOW_DURATION VIDEODB_MAX_COLUMNS + 2
+#define VIDEODB_DETAILS_TVSHOW_PARENTPATHID VIDEODB_MAX_COLUMNS + 3
+#define VIDEODB_DETAILS_TVSHOW_PATH VIDEODB_MAX_COLUMNS + 4
+#define VIDEODB_DETAILS_TVSHOW_DATEADDED VIDEODB_MAX_COLUMNS + 5
+#define VIDEODB_DETAILS_TVSHOW_LASTPLAYED VIDEODB_MAX_COLUMNS + 6
+#define VIDEODB_DETAILS_TVSHOW_NUM_EPISODES VIDEODB_MAX_COLUMNS + 7
+#define VIDEODB_DETAILS_TVSHOW_NUM_WATCHED VIDEODB_MAX_COLUMNS + 8
+#define VIDEODB_DETAILS_TVSHOW_NUM_SEASONS VIDEODB_MAX_COLUMNS + 9
+#define VIDEODB_DETAILS_TVSHOW_RATING VIDEODB_MAX_COLUMNS + 10
+#define VIDEODB_DETAILS_TVSHOW_VOTES VIDEODB_MAX_COLUMNS + 11
+#define VIDEODB_DETAILS_TVSHOW_RATING_TYPE VIDEODB_MAX_COLUMNS + 12
+#define VIDEODB_DETAILS_TVSHOW_UNIQUEID_VALUE VIDEODB_MAX_COLUMNS + 13
+#define VIDEODB_DETAILS_TVSHOW_UNIQUEID_TYPE VIDEODB_MAX_COLUMNS + 14
+
+#define VIDEODB_DETAILS_MUSICVIDEO_USER_RATING VIDEODB_MAX_COLUMNS + 2
+#define VIDEODB_DETAILS_MUSICVIDEO_PREMIERED VIDEODB_MAX_COLUMNS + 3
+#define VIDEODB_DETAILS_MUSICVIDEO_FILE VIDEODB_MAX_COLUMNS + 4
+#define VIDEODB_DETAILS_MUSICVIDEO_PATH VIDEODB_MAX_COLUMNS + 5
+#define VIDEODB_DETAILS_MUSICVIDEO_PLAYCOUNT VIDEODB_MAX_COLUMNS + 6
+#define VIDEODB_DETAILS_MUSICVIDEO_LASTPLAYED VIDEODB_MAX_COLUMNS + 7
+#define VIDEODB_DETAILS_MUSICVIDEO_DATEADDED VIDEODB_MAX_COLUMNS + 8
+#define VIDEODB_DETAILS_MUSICVIDEO_RESUME_TIME VIDEODB_MAX_COLUMNS + 9
+#define VIDEODB_DETAILS_MUSICVIDEO_TOTAL_TIME VIDEODB_MAX_COLUMNS + 10
+#define VIDEODB_DETAILS_MUSICVIDEO_PLAYER_STATE VIDEODB_MAX_COLUMNS + 11
+#define VIDEODB_DETAILS_MUSICVIDEO_UNIQUEID_VALUE VIDEODB_MAX_COLUMNS + 12
+#define VIDEODB_DETAILS_MUSICVIDEO_UNIQUEID_TYPE VIDEODB_MAX_COLUMNS + 13
+
+#define VIDEODB_TYPE_UNUSED 0
+#define VIDEODB_TYPE_STRING 1
+#define VIDEODB_TYPE_INT 2
+#define VIDEODB_TYPE_FLOAT 3
+#define VIDEODB_TYPE_BOOL 4
+#define VIDEODB_TYPE_COUNT 5
+#define VIDEODB_TYPE_STRINGARRAY 6
+#define VIDEODB_TYPE_DATE 7
+#define VIDEODB_TYPE_DATETIME 8
+
+enum class VideoDbContentType
+{
+ UNKNOWN = -1,
+ MOVIES = 1,
+ TVSHOWS = 2,
+ MUSICVIDEOS = 3,
+ EPISODES = 4,
+ MOVIE_SETS = 5,
+ MUSICALBUMS = 6
+};
+
+typedef enum // this enum MUST match the offset struct further down!! and make sure to keep min and max at -1 and sizeof(offsets)
+{
+ VIDEODB_ID_MIN = -1,
+ VIDEODB_ID_TITLE = 0,
+ VIDEODB_ID_PLOT = 1,
+ VIDEODB_ID_PLOTOUTLINE = 2,
+ VIDEODB_ID_TAGLINE = 3,
+ VIDEODB_ID_VOTES = 4, // unused
+ VIDEODB_ID_RATING_ID = 5,
+ VIDEODB_ID_CREDITS = 6,
+ VIDEODB_ID_YEAR = 7, // unused
+ VIDEODB_ID_THUMBURL = 8,
+ VIDEODB_ID_IDENT_ID = 9,
+ VIDEODB_ID_SORTTITLE = 10,
+ VIDEODB_ID_RUNTIME = 11,
+ VIDEODB_ID_MPAA = 12,
+ VIDEODB_ID_TOP250 = 13,
+ VIDEODB_ID_GENRE = 14,
+ VIDEODB_ID_DIRECTOR = 15,
+ VIDEODB_ID_ORIGINALTITLE = 16,
+ VIDEODB_ID_THUMBURL_SPOOF = 17,
+ VIDEODB_ID_STUDIOS = 18,
+ VIDEODB_ID_TRAILER = 19,
+ VIDEODB_ID_FANART = 20,
+ VIDEODB_ID_COUNTRY = 21,
+ VIDEODB_ID_BASEPATH = 22,
+ VIDEODB_ID_PARENTPATHID = 23,
+ VIDEODB_ID_MAX
+} VIDEODB_IDS;
+
+const struct SDbTableOffsets
+{
+ int type;
+ size_t offset;
+} DbMovieOffsets[] =
+{
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTitle) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPlot) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPlotOutline) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTagLine) },
+ { VIDEODB_TYPE_UNUSED, 0 }, // unused
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdRating) },
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_writingCredits) },
+ { VIDEODB_TYPE_UNUSED, 0 }, // unused
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPictureURL.m_data) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdUniqueID) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strSortTitle) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_duration) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strMPAARating) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iTop250) },
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_genre) },
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_director) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strOriginalTitle) },
+ { VIDEODB_TYPE_UNUSED, 0 }, // unused
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_studio) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTrailer) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_fanart.m_xml) },
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_country) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_basePath) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_parentPathID) }
+};
+
+typedef enum // this enum MUST match the offset struct further down!! and make sure to keep min and max at -1 and sizeof(offsets)
+{
+ VIDEODB_ID_TV_MIN = -1,
+ VIDEODB_ID_TV_TITLE = 0,
+ VIDEODB_ID_TV_PLOT = 1,
+ VIDEODB_ID_TV_STATUS = 2,
+ VIDEODB_ID_TV_VOTES = 3, // unused
+ VIDEODB_ID_TV_RATING_ID = 4,
+ VIDEODB_ID_TV_PREMIERED = 5,
+ VIDEODB_ID_TV_THUMBURL = 6,
+ VIDEODB_ID_TV_THUMBURL_SPOOF = 7,
+ VIDEODB_ID_TV_GENRE = 8,
+ VIDEODB_ID_TV_ORIGINALTITLE = 9,
+ VIDEODB_ID_TV_EPISODEGUIDE = 10,
+ VIDEODB_ID_TV_FANART = 11,
+ VIDEODB_ID_TV_IDENT_ID = 12,
+ VIDEODB_ID_TV_MPAA = 13,
+ VIDEODB_ID_TV_STUDIOS = 14,
+ VIDEODB_ID_TV_SORTTITLE = 15,
+ VIDEODB_ID_TV_TRAILER = 16,
+ VIDEODB_ID_TV_MAX
+} VIDEODB_TV_IDS;
+
+const struct SDbTableOffsets DbTvShowOffsets[] =
+{
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTitle) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPlot) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strStatus) },
+ { VIDEODB_TYPE_UNUSED, 0 }, //unused
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdRating) },
+ { VIDEODB_TYPE_DATE, my_offsetof(CVideoInfoTag,m_premiered) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPictureURL.m_data) },
+ { VIDEODB_TYPE_UNUSED, 0 }, // unused
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_genre) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strOriginalTitle)},
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strEpisodeGuide)},
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_fanart.m_xml)},
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdUniqueID)},
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strMPAARating)},
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_studio)},
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strSortTitle)},
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTrailer)}
+};
+
+//! @todo is this comment valid for seasons? There is no offset structure or am I wrong?
+typedef enum // this enum MUST match the offset struct further down!! and make sure to keep min and max at -1 and sizeof(offsets)
+{
+ VIDEODB_ID_SEASON_MIN = -1,
+ VIDEODB_ID_SEASON_ID = 0,
+ VIDEODB_ID_SEASON_TVSHOW_ID = 1,
+ VIDEODB_ID_SEASON_NUMBER = 2,
+ VIDEODB_ID_SEASON_NAME = 3,
+ VIDEODB_ID_SEASON_USER_RATING = 4,
+ VIDEODB_ID_SEASON_TVSHOW_PATH = 5,
+ VIDEODB_ID_SEASON_TVSHOW_TITLE = 6,
+ VIDEODB_ID_SEASON_TVSHOW_PLOT = 7,
+ VIDEODB_ID_SEASON_TVSHOW_PREMIERED = 8,
+ VIDEODB_ID_SEASON_TVSHOW_GENRE = 9,
+ VIDEODB_ID_SEASON_TVSHOW_STUDIO = 10,
+ VIDEODB_ID_SEASON_TVSHOW_MPAA = 11,
+ VIDEODB_ID_SEASON_EPISODES_TOTAL = 12,
+ VIDEODB_ID_SEASON_EPISODES_WATCHED = 13,
+ VIDEODB_ID_SEASON_PREMIERED = 14,
+ VIDEODB_ID_SEASON_MAX
+} VIDEODB_SEASON_IDS;
+
+typedef enum // this enum MUST match the offset struct further down!! and make sure to keep min and max at -1 and sizeof(offsets)
+{
+ VIDEODB_ID_EPISODE_MIN = -1,
+ VIDEODB_ID_EPISODE_TITLE = 0,
+ VIDEODB_ID_EPISODE_PLOT = 1,
+ VIDEODB_ID_EPISODE_VOTES = 2, // unused
+ VIDEODB_ID_EPISODE_RATING_ID = 3,
+ VIDEODB_ID_EPISODE_CREDITS = 4,
+ VIDEODB_ID_EPISODE_AIRED = 5,
+ VIDEODB_ID_EPISODE_THUMBURL = 6,
+ VIDEODB_ID_EPISODE_THUMBURL_SPOOF = 7,
+ VIDEODB_ID_EPISODE_PLAYCOUNT = 8, // unused - feel free to repurpose
+ VIDEODB_ID_EPISODE_RUNTIME = 9,
+ VIDEODB_ID_EPISODE_DIRECTOR = 10,
+ VIDEODB_ID_EPISODE_PRODUCTIONCODE = 11,
+ VIDEODB_ID_EPISODE_SEASON = 12,
+ VIDEODB_ID_EPISODE_EPISODE = 13,
+ VIDEODB_ID_EPISODE_ORIGINALTITLE = 14,
+ VIDEODB_ID_EPISODE_SORTSEASON = 15,
+ VIDEODB_ID_EPISODE_SORTEPISODE = 16,
+ VIDEODB_ID_EPISODE_BOOKMARK = 17,
+ VIDEODB_ID_EPISODE_BASEPATH = 18,
+ VIDEODB_ID_EPISODE_PARENTPATHID = 19,
+ VIDEODB_ID_EPISODE_IDENT_ID = 20,
+ VIDEODB_ID_EPISODE_MAX
+} VIDEODB_EPISODE_IDS;
+
+const struct SDbTableOffsets DbEpisodeOffsets[] =
+{
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strTitle) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPlot) },
+ { VIDEODB_TYPE_UNUSED, 0 }, // unused
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdRating) },
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_writingCredits) },
+ { VIDEODB_TYPE_DATE, my_offsetof(CVideoInfoTag,m_firstAired) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPictureURL.m_data) },
+ { VIDEODB_TYPE_UNUSED, 0 }, // unused
+ { VIDEODB_TYPE_UNUSED, 0 }, // unused
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_duration) },
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_director) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strProductionCode) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iSeason) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iEpisode) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strOriginalTitle)},
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iSpecialSortSeason) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iSpecialSortEpisode) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iBookmarkId) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_basePath) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_parentPathID) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdUniqueID) }
+};
+
+typedef enum // this enum MUST match the offset struct further down!! and make sure to keep min and max at -1 and sizeof(offsets)
+{
+ VIDEODB_ID_MUSICVIDEO_MIN = -1,
+ VIDEODB_ID_MUSICVIDEO_TITLE = 0,
+ VIDEODB_ID_MUSICVIDEO_THUMBURL = 1,
+ VIDEODB_ID_MUSICVIDEO_THUMBURL_SPOOF = 2,
+ VIDEODB_ID_MUSICVIDEO_PLAYCOUNT = 3, // unused - feel free to repurpose
+ VIDEODB_ID_MUSICVIDEO_RUNTIME = 4,
+ VIDEODB_ID_MUSICVIDEO_DIRECTOR = 5,
+ VIDEODB_ID_MUSICVIDEO_STUDIOS = 6,
+ VIDEODB_ID_MUSICVIDEO_YEAR = 7, // unused
+ VIDEODB_ID_MUSICVIDEO_PLOT = 8,
+ VIDEODB_ID_MUSICVIDEO_ALBUM = 9,
+ VIDEODB_ID_MUSICVIDEO_ARTIST = 10,
+ VIDEODB_ID_MUSICVIDEO_GENRE = 11,
+ VIDEODB_ID_MUSICVIDEO_TRACK = 12,
+ VIDEODB_ID_MUSICVIDEO_BASEPATH = 13,
+ VIDEODB_ID_MUSICVIDEO_PARENTPATHID = 14,
+ VIDEODB_ID_MUSICVIDEO_IDENT_ID = 15,
+ VIDEODB_ID_MUSICVIDEO_MAX
+} VIDEODB_MUSICVIDEO_IDS;
+
+const struct SDbTableOffsets DbMusicVideoOffsets[] =
+{
+ { VIDEODB_TYPE_STRING, my_offsetof(class CVideoInfoTag,m_strTitle) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPictureURL.m_data) },
+ { VIDEODB_TYPE_UNUSED, 0 }, // unused
+ { VIDEODB_TYPE_UNUSED, 0 }, // unused
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_duration) },
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_director) },
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_studio) },
+ { VIDEODB_TYPE_UNUSED, 0 }, // unused
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strPlot) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_strAlbum) },
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_artist) },
+ { VIDEODB_TYPE_STRINGARRAY, my_offsetof(CVideoInfoTag,m_genre) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iTrack) },
+ { VIDEODB_TYPE_STRING, my_offsetof(CVideoInfoTag,m_basePath) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_parentPathID) },
+ { VIDEODB_TYPE_INT, my_offsetof(CVideoInfoTag,m_iIdUniqueID)}
+};
+
+#define COMPARE_PERCENTAGE 0.90f // 90%
+#define COMPARE_PERCENTAGE_MIN 0.50f // 50%
+
+class CVideoDatabase : public CDatabase
+{
+public:
+
+ class CActor // used for actor retrieval for non-master users
+ {
+ public:
+ std::string name;
+ std::string thumb;
+ int playcount;
+ int appearances;
+ };
+
+ class CSeason // used for season retrieval for non-master users
+ {
+ public:
+ std::string path;
+ std::vector<std::string> genre;
+ int numEpisodes;
+ int numWatched;
+ int id;
+ };
+
+ class CSetInfo
+ {
+ public:
+ std::string name;
+ VECMOVIES movies;
+ DatabaseResults results;
+ };
+
+ CVideoDatabase(void);
+ ~CVideoDatabase(void) override;
+
+ bool Open() override;
+ bool CommitTransaction() override;
+
+ int AddNewEpisode(int idShow, CVideoInfoTag& details);
+
+ // editing functions
+ /*! \brief Set the playcount of an item, update last played time
+ Sets the playcount and last played date to a given value
+ \param item CFileItem to set the playcount for
+ \param count The playcount to set.
+ \param date The date the file was last viewed (does not denote the video was watched to completion).
+ If empty we use current datetime (if count > 0) or never viewed (if count = 0).
+ \return on success, the new last played time set, invalid datetime otherwise.
+ \sa GetPlayCount, IncrementPlayCount, UpdateLastPlayed
+ */
+ CDateTime SetPlayCount(const CFileItem& item, int count, const CDateTime& date = CDateTime());
+
+ /*! \brief Increment the playcount of an item
+ Increments the playcount and updates the last played date
+ \param item CFileItem to increment the playcount for
+ \return on success, the new last played time set, invalid datetime otherwise.
+ \sa GetPlayCount, SetPlayCount, GetPlayCounts
+ */
+ CDateTime IncrementPlayCount(const CFileItem& item);
+
+ /*! \brief Get the playcount of an item
+ \param item CFileItem to get the playcount for
+ \return the playcount of the item, or -1 on error
+ \sa SetPlayCount, IncrementPlayCount, GetPlayCounts
+ */
+ int GetPlayCount(const CFileItem &item);
+
+ /*! \brief Get the playcount of a filename and path
+ \param strFilenameAndPath filename and path to get the playcount for
+ \return the playcount of the item, or -1 on error
+ \sa SetPlayCount, IncrementPlayCount, GetPlayCounts
+ */
+ int GetPlayCount(const std::string& strFilenameAndPath);
+
+ /*! \brief Get the last played time of a filename and path
+ \param strFilenameAndPath filename and path to get the last played time for
+ \return the last played time of the item, or an invalid CDateTime on error
+ \sa UpdateLastPlayed
+ */
+ CDateTime GetLastPlayed(const std::string& strFilenameAndPath);
+
+ /*! \brief Update the last played time of an item
+ Updates the last played date
+ \param item CFileItem to update the last played time for
+ \return on success, the last played time set, invalid datetime otherwise.
+ \sa GetPlayCount, SetPlayCount, IncrementPlayCount, GetPlayCounts
+ */
+ CDateTime UpdateLastPlayed(const CFileItem& item);
+
+ /*! \brief Get the playcount and resume point of a list of items
+ Note that if the resume point is already set on an item, it won't be overridden.
+ \param path the path to fetch videos from
+ \param items CFileItemList to fetch the playcounts for
+ \sa GetPlayCount, SetPlayCount, IncrementPlayCount
+ */
+ bool GetPlayCounts(const std::string &path, CFileItemList &items);
+
+ void UpdateMovieTitle(int idMovie,
+ const std::string& strNewMovieTitle,
+ VideoDbContentType iType = VideoDbContentType::MOVIES);
+ bool UpdateVideoSortTitle(int idDb,
+ const std::string& strNewSortTitle,
+ VideoDbContentType iType = VideoDbContentType::MOVIES);
+
+ bool HasMovieInfo(const std::string& strFilenameAndPath);
+ bool HasTvShowInfo(const std::string& strFilenameAndPath);
+ bool HasEpisodeInfo(const std::string& strFilenameAndPath);
+ bool HasMusicVideoInfo(const std::string& strFilenameAndPath);
+
+ void GetFilePathById(int idMovie, std::string& filePath, VideoDbContentType iType);
+ std::string GetGenreById(int id);
+ std::string GetCountryById(int id);
+ std::string GetSetById(int id);
+ std::string GetTagById(int id);
+ std::string GetPersonById(int id);
+ std::string GetStudioById(int id);
+ std::string GetTvShowTitleById(int id);
+ std::string GetMusicVideoAlbumById(int id);
+ int GetTvShowForEpisode(int idEpisode);
+ int GetSeasonForEpisode(int idEpisode);
+
+ bool LoadVideoInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int getDetails = VideoDbDetailsAll);
+ bool GetMovieInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idMovie = -1, int getDetails = VideoDbDetailsAll);
+ bool GetTvShowInfo(const std::string& strPath, CVideoInfoTag& details, int idTvShow = -1, CFileItem* item = NULL, int getDetails = VideoDbDetailsAll);
+ bool GetSeasonInfo(int idSeason, CVideoInfoTag& details, CFileItem* item);
+ bool GetSeasonInfo(int idSeason, CVideoInfoTag& details, bool allDetails = true);
+ bool GetEpisodeBasicInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idEpisode = -1);
+ bool GetEpisodeInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idEpisode = -1, int getDetails = VideoDbDetailsAll);
+ bool GetMusicVideoInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idMVideo = -1, int getDetails = VideoDbDetailsAll);
+ bool GetSetInfo(int idSet, CVideoInfoTag& details, CFileItem* item = nullptr);
+ bool GetFileInfo(const std::string& strFilenameAndPath, CVideoInfoTag& details, int idFile = -1);
+
+ int GetPathId(const std::string& strPath);
+ int GetTvShowId(const std::string& strPath);
+ int GetEpisodeId(const std::string& strFilenameAndPath, int idEpisode=-1, int idSeason=-1); // idEpisode, idSeason are used for multipart episodes as hints
+ int GetSeasonId(int idShow, int season);
+
+ void GetEpisodesByFile(const std::string& strFilenameAndPath, std::vector<CVideoInfoTag>& episodes);
+
+ int SetDetailsForItem(CVideoInfoTag& details, const std::map<std::string, std::string> &artwork);
+ int SetDetailsForItem(int id, const MediaType& mediaType, CVideoInfoTag& details, const std::map<std::string, std::string> &artwork);
+
+ int SetDetailsForMovie(CVideoInfoTag& details,
+ const std::map<std::string, std::string>& artwork,
+ int idMovie = -1);
+ int SetDetailsForMovieSet(const CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, int idSet = -1);
+
+ /*! \brief add a tvshow to the library, setting metadata detail
+ First checks for whether this TV Show is already in the database (based on idTvShow, or via GetMatchingTvShow)
+ and if present adds the paths to the show. If not present, we add a new show and set the show metadata.
+ \param paths a vector<string,string> list of the path(s) and parent path(s) for the show.
+ \param details a CVideoInfoTag filled with the metadata for the show.
+ \param artwork the artwork map for the show.
+ \param seasonArt the artwork map for seasons.
+ \param idTvShow the database id of the tvshow if known (defaults to -1)
+ \return the id of the tvshow.
+ */
+ int SetDetailsForTvShow(const std::vector< std::pair<std::string, std::string> > &paths, CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, const std::map<int, std::map<std::string, std::string> > &seasonArt, int idTvShow = -1);
+ bool UpdateDetailsForTvShow(int idTvShow, CVideoInfoTag &details, const std::map<std::string, std::string> &artwork, const std::map<int, std::map<std::string, std::string> > &seasonArt);
+ int SetDetailsForSeason(const CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, int idShow, int idSeason = -1);
+ int SetDetailsForEpisode(CVideoInfoTag& details,
+ const std::map<std::string, std::string>& artwork,
+ int idShow,
+ int idEpisode = -1);
+ int SetDetailsForMusicVideo(CVideoInfoTag& details,
+ const std::map<std::string, std::string>& artwork,
+ int idMVideo = -1);
+ void SetStreamDetailsForFile(const CStreamDetails& details, const std::string &strFileNameAndPath);
+ void SetStreamDetailsForFileId(const CStreamDetails& details, int idFile);
+
+ bool SetSingleValue(VideoDbContentType type, int dbId, int dbField, const std::string& strValue);
+ bool SetSingleValue(VideoDbContentType type,
+ int dbId,
+ Field dbField,
+ const std::string& strValue);
+ bool SetSingleValue(const std::string &table, const std::string &fieldName, const std::string &strValue,
+ const std::string &conditionName = "", int conditionValue = -1);
+
+ int UpdateDetailsForMovie(int idMovie, CVideoInfoTag& details, const std::map<std::string, std::string> &artwork, const std::set<std::string> &updatedDetails);
+
+ void DeleteMovie(int idMovie, bool bKeepId = false);
+ void DeleteTvShow(int idTvShow, bool bKeepId = false);
+ void DeleteTvShow(const std::string& strPath);
+ void DeleteSeason(int idSeason, bool bKeepId = false);
+ void DeleteEpisode(int idEpisode, bool bKeepId = false);
+ void DeleteMusicVideo(int idMusicVideo, bool bKeepId = false);
+ void DeleteDetailsForTvShow(int idTvShow);
+ void DeleteStreamDetails(int idFile);
+ void RemoveContentForPath(const std::string& strPath,CGUIDialogProgress *progress = NULL);
+ void UpdateFanart(const CFileItem& item, VideoDbContentType type);
+ void DeleteSet(int idSet);
+ void DeleteTag(int idTag, VideoDbContentType mediaType);
+
+ /*! \brief Get video settings for the specified file id
+ \param idFile file id to get the settings for
+ \return true if video settings found, false otherwise
+ \sa SetVideoSettings
+ */
+ bool GetVideoSettings(int idFile, CVideoSettings &settings);
+
+ /*! \brief Get video settings for the specified file item
+ \param item item to get the settings for
+ \return true if video settings found, false otherwise
+ \sa SetVideoSettings
+ */
+ bool GetVideoSettings(const CFileItem &item, CVideoSettings &settings);
+
+ /*! \brief Get video settings for the specified file path
+ \param filePath filepath to get the settings for
+ \return true if video settings found, false otherwise
+ \sa SetVideoSettings
+ */
+ bool GetVideoSettings(const std::string &filePath, CVideoSettings &settings);
+
+ /*! \brief Set video settings for the specified file path
+ \param fileItem to set the settings for
+ \sa GetVideoSettings
+ */
+ void SetVideoSettings(const CFileItem &item, const CVideoSettings &settings);
+
+ /*! \brief Set video settings for the specified file path
+ \param fileId to set the settings for
+ \sa GetVideoSettings
+ */
+ void SetVideoSettings(int idFile, const CVideoSettings &settings);
+
+ /**
+ * Erases video settings for file item
+ * @param fileitem
+ */
+ void EraseVideoSettings(const CFileItem &item);
+
+ /**
+ * Erases all video settings
+ */
+ void EraseAllVideoSettings();
+
+ /**
+ * Erases video settings for files starting with path
+ * @param path pattern
+ */
+ void EraseAllVideoSettings(const std::string& path);
+
+ /**
+ * Erases all entries for files starting with path, including the files and path entries
+ * @param path pattern
+ */
+ void EraseAllForPath(const std::string& path);
+
+ bool GetStackTimes(const std::string &filePath, std::vector<uint64_t> &times);
+ void SetStackTimes(const std::string &filePath, const std::vector<uint64_t> &times);
+
+ void GetBookMarksForFile(const std::string& strFilenameAndPath, VECBOOKMARKS& bookmarks, CBookmark::EType type = CBookmark::STANDARD, bool bAppend=false, long partNumber=0);
+ void AddBookMarkToFile(const std::string& strFilenameAndPath, const CBookmark &bookmark, CBookmark::EType type = CBookmark::STANDARD);
+ bool GetResumeBookMark(const std::string& strFilenameAndPath, CBookmark &bookmark);
+ void DeleteResumeBookMark(const CFileItem& item);
+ void ClearBookMarkOfFile(const std::string& strFilenameAndPath, CBookmark& bookmark, CBookmark::EType type = CBookmark::STANDARD);
+ void ClearBookMarksOfFile(const std::string& strFilenameAndPath, CBookmark::EType type = CBookmark::STANDARD);
+ void ClearBookMarksOfFile(int idFile, CBookmark::EType type = CBookmark::STANDARD);
+ bool GetBookMarkForEpisode(const CVideoInfoTag& tag, CBookmark& bookmark);
+ void AddBookMarkForEpisode(const CVideoInfoTag& tag, const CBookmark& bookmark);
+ void DeleteBookMarkForEpisode(const CVideoInfoTag& tag);
+ bool GetResumePoint(CVideoInfoTag& tag);
+ bool GetStreamDetails(CFileItem& item);
+ bool GetStreamDetails(CVideoInfoTag& tag) const;
+ bool GetDetailsByTypeAndId(CFileItem& item, VideoDbContentType type, int id);
+ CVideoInfoTag GetDetailsByTypeAndId(VideoDbContentType type, int id);
+
+ // scraper settings
+ void SetScraperForPath(const std::string& filePath, const ADDON::ScraperPtr& info, const VIDEO::SScanSettings& settings);
+ ADDON::ScraperPtr GetScraperForPath(const std::string& strPath);
+ ADDON::ScraperPtr GetScraperForPath(const std::string& strPath, VIDEO::SScanSettings& settings);
+
+ /*! \brief Retrieve the scraper and settings we should use for the specified path
+ If the scraper is not set on this particular path, we'll recursively check parent folders.
+ \param strPath path to start searching in.
+ \param settings [out] scan settings for this folder.
+ \param foundDirectly [out] true if a scraper was found directly for strPath, false if it was in a parent path.
+ \return A ScraperPtr containing the scraper information. Returns NULL if a trivial (Content == CONTENT_NONE)
+ scraper or no scraper is found.
+ */
+ ADDON::ScraperPtr GetScraperForPath(const std::string& strPath, VIDEO::SScanSettings& settings, bool& foundDirectly);
+
+ /*! \brief Retrieve the content type of videos in the given path
+ If content is set on the folder, we return the given content type, except in the case of tvshows,
+ where we first check for whether we have episodes directly in the path (thus return episodes) or whether
+ we've found a scraper directly (shows). Any folders inbetween are treated as seasons (regardless of whether
+ they actually are seasons). Note that any subfolders in movies will be treated as movies.
+ \param strPath path to start searching in.
+ \return A content type string for the current path.
+ */
+ std::string GetContentForPath(const std::string& strPath);
+
+ /*! \brief Get videos of the given content type from the given path
+ \param content the content type to fetch.
+ \param path the path to fetch videos from.
+ \param items the returned items
+ \return true if items are found, false otherwise.
+ */
+ bool GetItemsForPath(const std::string &content, const std::string &path, CFileItemList &items);
+
+ /*! \brief Check whether a given scraper is in use.
+ \param scraperID the scraper to check for.
+ \return true if the scraper is in use, false otherwise.
+ */
+ bool ScraperInUse(const std::string &scraperID) const;
+
+ // scanning hashes and paths scanned
+ bool SetPathHash(const std::string &path, const std::string &hash);
+ bool GetPathHash(const std::string &path, std::string &hash);
+ bool GetPaths(std::set<std::string> &paths);
+ bool GetPathsForTvShow(int idShow, std::set<int>& paths);
+
+ /*! \brief return the paths linked to a tvshow.
+ \param idShow the id of the tvshow.
+ \param paths [out] the list of paths associated with the show.
+ \return true on success, false on failure.
+ */
+ bool GetPathsLinkedToTvShow(int idShow, std::vector<std::string> &paths);
+
+ /*! \brief retrieve subpaths of a given path. Assumes a hierarchical folder structure
+ \param basepath the root path to retrieve subpaths for
+ \param subpaths the returned subpaths
+ \return true if we successfully retrieve subpaths (may be zero), false on error
+ */
+ bool GetSubPaths(const std::string& basepath, std::vector< std::pair<int, std::string> >& subpaths);
+
+ bool GetSourcePath(const std::string &path, std::string &sourcePath);
+ bool GetSourcePath(const std::string &path, std::string &sourcePath, VIDEO::SScanSettings& settings);
+
+ // for music + musicvideo linkups - if no album and title given it will return the artist id, else the id of the matching video
+ int GetMatchingMusicVideo(const std::string& strArtist, const std::string& strAlbum = "", const std::string& strTitle = "");
+
+ // searching functions
+ void GetMoviesByActor(const std::string& strActor, CFileItemList& items);
+ void GetTvShowsByActor(const std::string& strActor, CFileItemList& items);
+ void GetEpisodesByActor(const std::string& strActor, CFileItemList& items);
+
+ void GetMusicVideosByArtist(const std::string& strArtist, CFileItemList& items);
+ void GetMusicVideosByAlbum(const std::string& strAlbum, CFileItemList& items);
+
+ void GetMovieGenresByName(const std::string& strSearch, CFileItemList& items);
+ void GetTvShowGenresByName(const std::string& strSearch, CFileItemList& items);
+ void GetMusicVideoGenresByName(const std::string& strSearch, CFileItemList& items);
+
+ void GetMovieCountriesByName(const std::string& strSearch, CFileItemList& items);
+
+ void GetMusicVideoAlbumsByName(const std::string& strSearch, CFileItemList& items);
+
+ void GetMovieActorsByName(const std::string& strSearch, CFileItemList& items);
+ void GetTvShowsActorsByName(const std::string& strSearch, CFileItemList& items);
+ void GetMusicVideoArtistsByName(const std::string& strSearch, CFileItemList& items);
+
+ void GetMovieDirectorsByName(const std::string& strSearch, CFileItemList& items);
+ void GetTvShowsDirectorsByName(const std::string& strSearch, CFileItemList& items);
+ void GetMusicVideoDirectorsByName(const std::string& strSearch, CFileItemList& items);
+
+ void GetMoviesByName(const std::string& strSearch, CFileItemList& items);
+ void GetTvShowsByName(const std::string& strSearch, CFileItemList& items);
+ void GetEpisodesByName(const std::string& strSearch, CFileItemList& items);
+ void GetMusicVideosByName(const std::string& strSearch, CFileItemList& items);
+
+ void GetEpisodesByPlot(const std::string& strSearch, CFileItemList& items);
+ void GetMoviesByPlot(const std::string& strSearch, CFileItemList& items);
+
+ bool LinkMovieToTvshow(int idMovie, int idShow, bool bRemove);
+ bool IsLinkedToTvshow(int idMovie);
+ bool GetLinksToTvShow(int idMovie, std::vector<int>& ids);
+
+ // general browsing
+ bool GetGenresNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetCountriesNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetStudiosNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetYearsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN,
+ const Filter& filter = Filter());
+ bool GetActorsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetDirectorsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetWritersNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetSetsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN,
+ const Filter& filter = Filter(),
+ bool ignoreSingleMovieSets = false);
+ bool GetTagsNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetMusicVideoAlbumsNav(const std::string& strBaseDir, CFileItemList& items, int idArtist, const Filter &filter = Filter(), bool countOnly = false);
+
+ bool GetMoviesNav(const std::string& strBaseDir, CFileItemList& items, int idGenre=-1, int idYear=-1, int idActor=-1, int idDirector=-1, int idStudio=-1, int idCountry=-1, int idSet=-1, int idTag=-1, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone);
+ bool GetTvShowsNav(const std::string& strBaseDir, CFileItemList& items, int idGenre=-1, int idYear=-1, int idActor=-1, int idDirector=-1, int idStudio=-1, int idTag=-1, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone);
+ bool GetSeasonsNav(const std::string& strBaseDir, CFileItemList& items, int idActor=-1, int idDirector=-1, int idGenre=-1, int idYear=-1, int idShow=-1, bool getLinkedMovies = true);
+ bool GetEpisodesNav(const std::string& strBaseDir, CFileItemList& items, int idGenre=-1, int idYear=-1, int idActor=-1, int idDirector=-1, int idShow=-1, int idSeason=-1, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone);
+ bool GetMusicVideosNav(const std::string& strBaseDir, CFileItemList& items, int idGenre=-1, int idYear=-1, int idArtist=-1, int idDirector=-1, int idStudio=-1, int idAlbum=-1, int idTag=-1, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone);
+
+ bool GetRecentlyAddedMoviesNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit=0, int getDetails = VideoDbDetailsNone);
+ bool GetRecentlyAddedEpisodesNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit=0, int getDetails = VideoDbDetailsNone);
+ bool GetRecentlyAddedMusicVideosNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit=0, int getDetails = VideoDbDetailsNone);
+ bool GetInProgressTvShowsNav(const std::string& strBaseDir, CFileItemList& items, unsigned int limit=0, int getDetails = VideoDbDetailsNone);
+
+ bool HasContent();
+ bool HasContent(VideoDbContentType type);
+ bool HasSets() const;
+
+ void CleanDatabase(CGUIDialogProgressBarHandle* handle = NULL, const std::set<int>& paths = std::set<int>(), bool showProgress = true);
+
+ /*! \brief Add a file to the database, if necessary
+ If the file is already in the database, we simply return its id.
+ \param url - full path of the file to add.
+ \param parentPath the parent path of the path to add. If empty, URIUtils::GetParentPath() will determine the path.
+ \param dateAdded datetime when the file was added to the filesystem/database
+ \param playcount the playcount of the file to add.
+ \param lastPlayed the date and time when the file to add was last played.
+ \return id of the file, -1 if it could not be added.
+ */
+ int AddFile(const std::string& url,
+ const std::string& parentPath = "",
+ const CDateTime& dateAdded = CDateTime(),
+ int playcount = 0,
+ const CDateTime& lastPlayed = CDateTime());
+
+ /*! \brief Add a file to the database, if necessary
+ Works for both videodb:// items and normal fileitems
+ \param item CFileItem to add.
+ \return id of the file, -1 if it could not be added.
+ */
+ int AddFile(const CFileItem& item);
+
+ /*! \brief Add a file to the database, if necessary
+ Works for both videodb:// items and normal fileitems
+ \param url full path of the file to add.
+ \param details details of the item to add.
+ \return id of the file, -1 if it could not be added.
+ */
+ int AddFile(const CVideoInfoTag& details, const std::string& parentPath = "");
+
+ /*! \brief Add a path to the database, if necessary
+ If the path is already in the database, we simply return its id.
+ \param strPath the path to add
+ \param parentPath the parent path of the path to add. If empty, URIUtils::GetParentPath() will determine the path.
+ \param dateAdded datetime when the path was added to the filesystem/database
+ \return id of the file, -1 if it could not be added.
+ */
+ int AddPath(const std::string& strPath, const std::string &parentPath = "", const CDateTime& dateAdded = CDateTime());
+
+ /*! \brief Updates the dateAdded field in the files table for the file
+ with the given idFile and the given path based on the files modification date
+ \param details details of the video file
+ */
+ void UpdateFileDateAdded(CVideoInfoTag& details);
+
+ void ExportToXML(const std::string &path, bool singleFile = true, bool images=false, bool actorThumbs=false, bool overwrite=false);
+ void ExportActorThumbs(const std::string &path, const CVideoInfoTag& tag, bool singleFiles, bool overwrite=false);
+ void ImportFromXML(const std::string &path);
+ void DumpToDummyFiles(const std::string &path);
+ bool ImportArtFromXML(const TiXmlNode *node, std::map<std::string, std::string> &artwork);
+
+ // smart playlists and main retrieval work in these functions
+ bool GetMoviesByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone);
+ bool GetSetsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool ignoreSingleMovieSets = false);
+ bool GetTvShowsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone);
+ bool GetSeasonsByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool appendFullShowPath = true, const SortDescription &sortDescription = SortDescription());
+ bool GetEpisodesByWhere(const std::string& strBaseDir, const Filter &filter, CFileItemList& items, bool appendFullShowPath = true, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone);
+ bool GetMusicVideosByWhere(const std::string &baseDir, const Filter &filter, CFileItemList& items, bool checkLocks = true, const SortDescription &sortDescription = SortDescription(), int getDetails = VideoDbDetailsNone);
+
+ // retrieve sorted and limited items
+ bool GetSortedVideos(const MediaType &mediaType, const std::string& strBaseDir, const SortDescription &sortDescription, CFileItemList& items, const Filter &filter = Filter());
+
+ // retrieve a list of items
+ bool GetItems(const std::string &strBaseDir, CFileItemList &items, const Filter &filter = Filter(), const SortDescription &sortDescription = SortDescription());
+ bool GetItems(const std::string &strBaseDir, const std::string &mediaType, const std::string &itemType, CFileItemList &items, const Filter &filter = Filter(), const SortDescription &sortDescription = SortDescription());
+ bool GetItems(const std::string& strBaseDir,
+ VideoDbContentType mediaType,
+ const std::string& itemType,
+ CFileItemList& items,
+ const Filter& filter = Filter(),
+ const SortDescription& sortDescription = SortDescription());
+ std::string GetItemById(const std::string &itemType, int id);
+
+ // partymode
+ /*! \brief Gets music video IDs in random order that match the where clause
+ \param strWhere the SQL where clause to apply in the query
+ \param songIDs a vector of <2, id> pairs suited to party mode use
+ \return count of music video IDs found.
+ */
+ unsigned int GetRandomMusicVideoIDs(const std::string& strWhere, std::vector<std::pair<int, int> > &songIDs);
+
+ static void VideoContentTypeToString(VideoDbContentType type, std::string& out)
+ {
+ switch (type)
+ {
+ case VideoDbContentType::MOVIES:
+ out = MediaTypeMovie;
+ break;
+ case VideoDbContentType::TVSHOWS:
+ out = MediaTypeTvShow;
+ break;
+ case VideoDbContentType::EPISODES:
+ out = MediaTypeEpisode;
+ break;
+ case VideoDbContentType::MUSICVIDEOS:
+ out = MediaTypeMusicVideo;
+ break;
+ default:
+ break;
+ }
+ }
+
+ void SetArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType, const std::string &url);
+ void SetArtForItem(int mediaId, const MediaType &mediaType, const std::map<std::string, std::string> &art);
+ bool GetArtForItem(int mediaId, const MediaType &mediaType, std::map<std::string, std::string> &art);
+ std::string GetArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType);
+ bool HasArtForItem(int mediaId, const MediaType &mediaType);
+ bool RemoveArtForItem(int mediaId, const MediaType &mediaType, const std::string &artType);
+ bool RemoveArtForItem(int mediaId, const MediaType &mediaType, const std::set<std::string> &artTypes);
+ bool GetTvShowSeasons(int showId, std::map<int, int> &seasons);
+ bool GetTvShowNamedSeasons(int showId, std::map<int, std::string> &seasons);
+
+ /*!
+ * \brief Get the custom named season.
+ * \param tvshowId The tv show id relative to the season.
+ * \param seasonId The season id for which to search the named title.
+ * \return The named title if found, otherwise empty.
+ */
+ std::string GetTvShowNamedSeasonById(int tvshowId, int seasonId);
+
+ bool GetTvShowSeasonArt(int mediaId, std::map<int, std::map<std::string, std::string> > &seasonArt);
+ bool GetArtTypes(const MediaType &mediaType, std::vector<std::string> &artTypes);
+
+ /*! \brief Fetch the distinct types of available-but-unassigned art held in the
+ database for a specific media item.
+ \param mediaId the id in the media table.
+ \param mediaType the type of media, which corresponds to the table the item resides in.
+ \return the types of art e.g. "thumb", "fanart", etc.
+ */
+ std::vector<std::string> GetAvailableArtTypesForItem(int mediaId, const MediaType& mediaType);
+
+ /*! \brief Fetch the list of available-but-unassigned art URLs held in the
+ database for a specific media item and art type.
+ \param mediaId the id in the media table.
+ \param mediaType corresponds to the table the item resides in.
+ \param artType e.g. "thumb", "fanart", etc.
+ \return list of URLs
+ */
+ std::vector<CScraperUrl::SUrlEntry> GetAvailableArtForItem(
+ int mediaId, const MediaType& mediaType, const std::string& artType);
+
+ int AddTag(const std::string &tag);
+ void AddTagToItem(int idItem, int idTag, const std::string &type);
+ void RemoveTagFromItem(int idItem, int idTag, const std::string &type);
+ void RemoveTagsFromItem(int idItem, const std::string &type);
+
+ bool GetFilter(CDbUrl &videoUrl, Filter &filter, SortDescription &sorting) override;
+
+ /*! \brief Will check if the season exists and if that is not the case add it to the database.
+ \param showID The id of the show in question.
+ \param season The season number we want to add.
+ \return The dbId of the season.
+ */
+ int AddSeason(int showID, int season, const std::string& name = "");
+ int AddSet(const std::string& strSet, const std::string& strOverview = "");
+ void ClearMovieSet(int idMovie);
+ void SetMovieSet(int idMovie, int idSet);
+ bool SetVideoUserRating(int dbId, int rating, const MediaType& mediaType);
+ bool GetUseAllExternalAudioForVideo(const std::string& videoPath);
+
+protected:
+ int AddNewMovie(CVideoInfoTag& details);
+ int AddNewMusicVideo(CVideoInfoTag& details);
+
+ int GetMovieId(const std::string& strFilenameAndPath);
+ int GetMusicVideoId(const std::string& strFilenameAndPath);
+
+ /*! \brief Get the id of this fileitem
+ Works for both videodb:// items and normal fileitems
+ \param item CFileItem to grab the fileid of
+ \return id of the file, -1 if it is not in the db.
+ */
+ int GetFileId(const CFileItem &item);
+ int GetFileId(const CVideoInfoTag& details);
+
+ /*! \brief Get the id of the file of this item and store it in the item
+ \param details CVideoInfoTag for which to get and store the id of the file
+ \return id of the file, -1 if it is not in the db.
+ */
+ int GetAndFillFileId(CVideoInfoTag& details);
+
+ /*! \brief Get the id of a file from path
+ \param url full path to the file
+ \return id of the file, -1 if it is not in the db.
+ */
+ int GetFileId(const std::string& url);
+
+ int AddToTable(const std::string& table, const std::string& firstField, const std::string& secondField, const std::string& value);
+ int UpdateRatings(int mediaId, const char *mediaType, const RatingMap& values, const std::string& defaultRating);
+ int AddRatings(int mediaId, const char *mediaType, const RatingMap& values, const std::string& defaultRating);
+ int UpdateUniqueIDs(int mediaId, const char *mediaType, const CVideoInfoTag& details);
+ int AddUniqueIDs(int mediaId, const char *mediaType, const CVideoInfoTag& details);
+ int AddActor(const std::string& strActor, const std::string& thumbURL, const std::string &thumb = "");
+
+ int AddTvShow();
+
+ /*! \brief Adds a path to the tvshow link table.
+ \param idShow the id of the show.
+ \param path the path to add.
+ \param parentPath the parent path of the path to add.
+ \param dateAdded date/time when the path was added
+ \return true if successfully added, false otherwise.
+ */
+ bool AddPathToTvShow(int idShow, const std::string &path, const std::string &parentPath, const CDateTime& dateAdded = CDateTime());
+
+ /*! \brief Check whether a show is already in the library.
+ Matches on unique identifier or matching title and premiered date.
+ \param show the details of the show to check for.
+ \return the show id if found, else -1.
+ */
+ int GetMatchingTvShow(const CVideoInfoTag &show);
+
+ // link functions - these two do all the work
+ void AddLinkToActor(int mediaId, const char *mediaType, int actorId, const std::string &role, int order);
+ void AddToLinkTable(int mediaId, const std::string& mediaType, const std::string& table, int valueId, const char *foreignKey = NULL);
+ void RemoveFromLinkTable(int mediaId, const std::string& mediaType, const std::string& table, int valueId, const char *foreignKey = NULL);
+
+ void AddLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values);
+ void UpdateLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values);
+ void AddActorLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values);
+ void UpdateActorLinksToItem(int mediaId, const std::string& mediaType, const std::string& field, const std::vector<std::string>& values);
+
+ void AddCast(int mediaId, const char *mediaType, const std::vector<SActorInfo> &cast);
+
+ CVideoInfoTag GetDetailsForMovie(std::unique_ptr<dbiplus::Dataset> &pDS, int getDetails = VideoDbDetailsNone);
+ CVideoInfoTag GetDetailsForMovie(const dbiplus::sql_record* const record, int getDetails = VideoDbDetailsNone);
+ CVideoInfoTag GetDetailsForTvShow(std::unique_ptr<dbiplus::Dataset> &pDS, int getDetails = VideoDbDetailsNone, CFileItem* item = NULL);
+ CVideoInfoTag GetDetailsForTvShow(const dbiplus::sql_record* const record, int getDetails = VideoDbDetailsNone, CFileItem* item = NULL);
+ CVideoInfoTag GetBasicDetailsForEpisode(std::unique_ptr<dbiplus::Dataset> &pDS);
+ CVideoInfoTag GetBasicDetailsForEpisode(const dbiplus::sql_record* const record);
+ CVideoInfoTag GetDetailsForEpisode(std::unique_ptr<dbiplus::Dataset> &pDS, int getDetails = VideoDbDetailsNone);
+ CVideoInfoTag GetDetailsForEpisode(const dbiplus::sql_record* const record, int getDetails = VideoDbDetailsNone);
+ CVideoInfoTag GetDetailsForMusicVideo(std::unique_ptr<dbiplus::Dataset> &pDS, int getDetails = VideoDbDetailsNone);
+ CVideoInfoTag GetDetailsForMusicVideo(const dbiplus::sql_record* const record, int getDetails = VideoDbDetailsNone);
+ bool GetPeopleNav(const std::string& strBaseDir,
+ CFileItemList& items,
+ const char* type,
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ bool GetNavCommon(const std::string& strBaseDir,
+ CFileItemList& items,
+ const char* type,
+ VideoDbContentType idContent = VideoDbContentType::UNKNOWN,
+ const Filter& filter = Filter(),
+ bool countOnly = false);
+ void GetCast(int media_id, const std::string &media_type, std::vector<SActorInfo> &cast);
+ void GetTags(int media_id, const std::string &media_type, std::vector<std::string> &tags);
+ void GetRatings(int media_id, const std::string &media_type, RatingMap &ratings);
+ void GetUniqueIDs(int media_id, const std::string &media_type, CVideoInfoTag& details);
+
+ void GetDetailsFromDB(std::unique_ptr<dbiplus::Dataset> &pDS, int min, int max, const SDbTableOffsets *offsets, CVideoInfoTag &details, int idxOffset = 2);
+ void GetDetailsFromDB(const dbiplus::sql_record* const record, int min, int max, const SDbTableOffsets *offsets, CVideoInfoTag &details, int idxOffset = 2);
+ std::string GetValueString(const CVideoInfoTag &details, int min, int max, const SDbTableOffsets *offsets) const;
+
+private:
+ void CreateTables() override;
+ void CreateAnalytics() override;
+ void UpdateTables(int version) override;
+ void CreateLinkIndex(const char *table);
+ void CreateForeignLinkIndex(const char *table, const char *foreignkey);
+
+ /*! \brief (Re)Create the generic database views for movies, tvshows,
+ episodes and music videos
+ */
+ virtual void CreateViews();
+
+ /*! \brief Helper to get a database id given a query.
+ Returns an integer, -1 if not found, and greater than 0 if found.
+ \param query the SQL that will retrieve a database id.
+ \return -1 if not found, else a valid database id (i.e. > 0)
+ */
+ int GetDbId(const std::string &query);
+
+ /*! \brief Run a query on the main dataset and return the number of rows
+ If no rows are found we close the dataset and return 0.
+ \param sql the sql query to run
+ \return the number of rows, -1 for an error.
+ */
+ int RunQuery(const std::string &sql);
+
+ void AppendIdLinkFilter(const char* field, const char *table, const MediaType& mediaType, const char *view, const char *viewKey, const CUrlOptions::UrlOptions& options, Filter &filter);
+ void AppendLinkFilter(const char* field, const char *table, const MediaType& mediaType, const char *view, const char *viewKey, const CUrlOptions::UrlOptions& options, Filter &filter);
+
+ /*! \brief Determine whether the path is using lookup using folders
+ \param path the path to check
+ \param shows whether this path is from a tvshow (defaults to false)
+ */
+ bool LookupByFolders(const std::string &path, bool shows = false);
+
+ /*! \brief Get the playcount for a file id
+ \param iFileId file id to get the playcount for
+ \return the playcount of the item, or -1 on error
+ \sa SetPlayCount, IncrementPlayCount, GetPlayCounts
+ */
+ int GetPlayCount(int iFileId);
+
+ /*! \brief Get the last played time of a filename and path
+ \param iFileId file id to get the playcount for
+ \return the last played time of the item, or an invalid CDateTime on error
+ \sa UpdateLastPlayed
+ */
+ CDateTime GetLastPlayed(int iFileId);
+
+ bool GetSeasonInfo(int idSeason, CVideoInfoTag& details, bool allDetails, CFileItem* item);
+
+ int GetMinSchemaVersion() const override { return 75; }
+ int GetSchemaVersion() const override;
+ virtual int GetExportVersion() const { return 1; }
+ const char* GetBaseDBName() const override { return "MyVideos"; }
+
+ void ConstructPath(std::string& strDest, const std::string& strPath, const std::string& strFileName);
+ void SplitPath(const std::string& strFileNameAndPath, std::string& strPath, std::string& strFileName);
+ void InvalidatePathHash(const std::string& strPath);
+
+ /*! \brief Get a safe filename from a given string
+ \param dir directory to use for the file
+ \param name movie, show name, or actor to get a safe filename for
+ \return safe filename based on this title
+ */
+ std::string GetSafeFile(const std::string &dir, const std::string &name) const;
+
+ std::vector<int> CleanMediaType(const std::string &mediaType, const std::string &cleanableFileIDs,
+ std::map<int, bool> &pathsDeleteDecisions, std::string &deletedFileIDs, bool silent);
+
+ static void AnnounceRemove(const std::string& content, int id, bool scanning = false);
+ static void AnnounceUpdate(const std::string& content, int id);
+
+ static CDateTime GetDateAdded(const std::string& filename, CDateTime dateAdded = CDateTime());
+};
diff --git a/xbmc/video/VideoDbUrl.cpp b/xbmc/video/VideoDbUrl.cpp
new file mode 100644
index 0000000..0bb899c
--- /dev/null
+++ b/xbmc/video/VideoDbUrl.cpp
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2016-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 "VideoDbUrl.h"
+
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "playlists/SmartPlayList.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+
+using namespace XFILE;
+
+CVideoDbUrl::CVideoDbUrl()
+ : CDbUrl()
+{ }
+
+CVideoDbUrl::~CVideoDbUrl() = default;
+
+bool CVideoDbUrl::parse()
+{
+ // the URL must start with videodb://
+ if (!m_url.IsProtocol("videodb") || m_url.GetFileName().empty())
+ return false;
+
+ std::string path = m_url.Get();
+ VIDEODATABASEDIRECTORY::NODE_TYPE dirType = CVideoDatabaseDirectory::GetDirectoryType(path);
+ VIDEODATABASEDIRECTORY::NODE_TYPE childType = CVideoDatabaseDirectory::GetDirectoryChildType(path);
+
+ switch (dirType)
+ {
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_MOVIES_OVERVIEW:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_MOVIES:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MOVIES:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_SETS:
+ m_type = "movies";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TVSHOWS_OVERVIEW:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_TVSHOWS:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_SEASONS:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_EPISODES:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_EPISODES:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_INPROGRESS_TVSHOWS:
+ m_type = "tvshows";
+ break;
+
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_OVERVIEW:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MUSICVIDEOS:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_ALBUM:
+ m_type = "musicvideos";
+
+ default:
+ break;
+ }
+
+ switch (childType)
+ {
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_MOVIES_OVERVIEW:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MOVIES:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_MOVIES:
+ m_type = "movies";
+ m_itemType = "movies";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TVSHOWS_OVERVIEW:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_TVSHOWS:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_INPROGRESS_TVSHOWS:
+ m_type = "tvshows";
+ m_itemType = "tvshows";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_SEASONS:
+ m_type = "tvshows";
+ m_itemType = "seasons";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_EPISODES:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_EPISODES:
+ m_type = "tvshows";
+ m_itemType = "episodes";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_OVERVIEW:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TITLE_MUSICVIDEOS:
+ m_type = "musicvideos";
+ m_itemType = "musicvideos";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_GENRE:
+ m_itemType = "genres";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_ACTOR:
+ m_itemType = "actors";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_YEAR:
+ m_itemType = "years";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_DIRECTOR:
+ m_itemType = "directors";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_STUDIO:
+ m_itemType = "studios";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_COUNTRY:
+ m_itemType = "countries";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_SETS:
+ m_itemType = "sets";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_ALBUM:
+ m_type = "musicvideos";
+ m_itemType = "albums";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_TAGS:
+ m_itemType = "tags";
+ break;
+
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_ROOT:
+ case VIDEODATABASEDIRECTORY::NODE_TYPE_OVERVIEW:
+ default:
+ return false;
+ }
+
+ if (m_type.empty() || m_itemType.empty())
+ return false;
+
+ // parse query params
+ VIDEODATABASEDIRECTORY::CQueryParams queryParams;
+ if (!CVideoDatabaseDirectory::GetQueryParams(path, queryParams))
+ return false;
+
+ // retrieve and parse all options
+ AddOptions(m_url.GetOptions());
+
+ // add options based on the QueryParams
+ if (queryParams.GetActorId() != -1)
+ {
+ std::string optionName = "actorid";
+ if (m_type == "musicvideos")
+ optionName = "artistid";
+
+ AddOption(optionName, (int)queryParams.GetActorId());
+ }
+ if (queryParams.GetAlbumId() != -1)
+ AddOption("albumid", (int)queryParams.GetAlbumId());
+ if (queryParams.GetCountryId() != -1)
+ AddOption("countryid", (int)queryParams.GetCountryId());
+ if (queryParams.GetDirectorId() != -1)
+ AddOption("directorid", (int)queryParams.GetDirectorId());
+ if (queryParams.GetEpisodeId() != -1)
+ AddOption("episodeid", (int)queryParams.GetEpisodeId());
+ if (queryParams.GetGenreId() != -1)
+ AddOption("genreid", (int)queryParams.GetGenreId());
+ if (queryParams.GetMovieId() != -1)
+ AddOption("movieid", (int)queryParams.GetMovieId());
+ if (queryParams.GetMVideoId() != -1)
+ AddOption("musicvideoid", (int)queryParams.GetMVideoId());
+ if (queryParams.GetSeason() != -1 && queryParams.GetSeason() >= -2)
+ AddOption("season", (int)queryParams.GetSeason());
+ if (queryParams.GetSetId() != -1)
+ AddOption("setid", (int)queryParams.GetSetId());
+ if (queryParams.GetStudioId() != -1)
+ AddOption("studioid", (int)queryParams.GetStudioId());
+ if (queryParams.GetTvShowId() != -1)
+ AddOption("tvshowid", (int)queryParams.GetTvShowId());
+ if (queryParams.GetYear() != -1)
+ AddOption("year", (int)queryParams.GetYear());
+
+ return true;
+}
+
+bool CVideoDbUrl::validateOption(const std::string &key, const CVariant &value)
+{
+ if (!CDbUrl::validateOption(key, value))
+ return false;
+
+ // if the value is empty it will remove the option which is ok
+ // otherwise we only care about the "filter" option here
+ if (value.empty() || !StringUtils::EqualsNoCase(key, "filter"))
+ return true;
+
+ if (!value.isString())
+ return false;
+
+ CSmartPlaylist xspFilter;
+ if (!xspFilter.LoadFromJson(value.asString()))
+ return false;
+
+ // check if the filter playlist matches the item type
+ return (xspFilter.GetType() == m_itemType ||
+ (xspFilter.GetType() == "movies" && m_itemType == "sets"));
+}
diff --git a/xbmc/video/VideoDbUrl.h b/xbmc/video/VideoDbUrl.h
new file mode 100644
index 0000000..41d6649
--- /dev/null
+++ b/xbmc/video/VideoDbUrl.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "DbUrl.h"
+
+class CVariant;
+
+class CVideoDbUrl : public CDbUrl
+{
+public:
+ CVideoDbUrl();
+ ~CVideoDbUrl() override;
+
+ const std::string& GetItemType() const { return m_itemType; }
+
+protected:
+ bool parse() override;
+ bool validateOption(const std::string &key, const CVariant &value) override;
+
+private:
+ std::string m_itemType;
+};
diff --git a/xbmc/video/VideoInfoDownloader.cpp b/xbmc/video/VideoInfoDownloader.cpp
new file mode 100644
index 0000000..b47f25c
--- /dev/null
+++ b/xbmc/video/VideoInfoDownloader.cpp
@@ -0,0 +1,263 @@
+/*
+ * 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 "VideoInfoDownloader.h"
+
+#include "dialogs/GUIDialogProgress.h"
+#include "filesystem/CurlFile.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace VIDEO;
+using namespace KODI::MESSAGING;
+using namespace std::chrono_literals;
+
+#ifndef __GNUC__
+#pragma warning (disable:4018)
+#endif
+
+CVideoInfoDownloader::CVideoInfoDownloader(const ADDON::ScraperPtr &scraper) :
+ CThread("VideoInfoDownloader"), m_state(DO_NOTHING), m_found(0), m_info(scraper)
+{
+ m_http = new XFILE::CCurlFile;
+}
+
+CVideoInfoDownloader::~CVideoInfoDownloader()
+{
+ delete m_http;
+}
+
+// return value: 0 = we failed, -1 = we failed and reported an error, 1 = success
+int CVideoInfoDownloader::InternalFindMovie(const std::string &movieTitle, int movieYear,
+ MOVIELIST& movielist,
+ bool cleanChars /* = true */)
+{
+ try
+ {
+ movielist = m_info->FindMovie(*m_http, movieTitle, movieYear, cleanChars);
+ }
+ catch (const ADDON::CScraperError &sce)
+ {
+ ShowErrorDialog(sce);
+ return sce.FAborted() ? 0 : -1;
+ }
+ return 1; // success
+}
+
+void CVideoInfoDownloader::ShowErrorDialog(const ADDON::CScraperError &sce)
+{
+ if (!sce.Title().empty())
+ HELPERS::ShowOKDialogText(CVariant{ sce.Title() }, CVariant{ sce.Message() });
+}
+
+// threaded functions
+void CVideoInfoDownloader::Process()
+{
+ // note here that we're calling our external functions but we're calling them with
+ // no progress bar set, so they're effectively calling our internal functions directly.
+ m_found = 0;
+ if (m_state == FIND_MOVIE)
+ {
+ if (!(m_found=FindMovie(m_movieTitle, m_movieYear, m_movieList)))
+ CLog::Log(LOGERROR, "{}: Error looking up item {} ({})", __FUNCTION__, m_movieTitle,
+ m_movieYear);
+ m_state = DO_NOTHING;
+ return;
+ }
+
+ if (!m_url.HasUrls())
+ {
+ // empty url when it's not supposed to be..
+ // this might happen if the previously scraped item was removed from the site (see ticket #10537)
+ CLog::Log(LOGERROR, "{}: Error getting details for {} ({}) due to an empty url", __FUNCTION__,
+ m_movieTitle, m_movieYear);
+ }
+ else if (m_state == GET_DETAILS)
+ {
+ if (!GetDetails(m_url, m_movieDetails))
+ CLog::Log(LOGERROR, "{}: Error getting details from {}", __FUNCTION__,
+ m_url.GetFirstThumbUrl());
+ }
+ else if (m_state == GET_EPISODE_DETAILS)
+ {
+ if (!GetEpisodeDetails(m_url, m_movieDetails))
+ CLog::Log(LOGERROR, "{}: Error getting episode details from {}", __FUNCTION__,
+ m_url.GetFirstThumbUrl());
+ }
+ else if (m_state == GET_EPISODE_LIST)
+ {
+ if (!GetEpisodeList(m_url, m_episode))
+ CLog::Log(LOGERROR, "{}: Error getting episode list from {}", __FUNCTION__,
+ m_url.GetFirstThumbUrl());
+ }
+ m_found = 1;
+ m_state = DO_NOTHING;
+}
+
+int CVideoInfoDownloader::FindMovie(const std::string &movieTitle, int movieYear,
+ MOVIELIST& movieList,
+ CGUIDialogProgress *pProgress /* = NULL */)
+{
+ //CLog::Log(LOGDEBUG,"CVideoInfoDownloader::FindMovie({})", strMovie);
+
+ if (pProgress)
+ { // threaded version
+ m_state = FIND_MOVIE;
+ m_movieTitle = movieTitle;
+ m_movieYear = movieYear;
+ m_found = 0;
+ if (IsRunning())
+ StopThread();
+ Create();
+ while (m_state != DO_NOTHING)
+ {
+ pProgress->Progress();
+ if (pProgress->IsCanceled())
+ {
+ CloseThread();
+ return 0;
+ }
+ CThread::Sleep(1ms);
+ }
+ // transfer to our movielist
+ m_movieList.swap(movieList);
+ int found=m_found;
+ CloseThread();
+ return found;
+ }
+
+ // unthreaded
+ int success = InternalFindMovie(movieTitle, movieYear, movieList);
+ // NOTE: this might be improved by rescraping if the match quality isn't high?
+ if (success == 1 && movieList.empty())
+ { // no results. try without cleaning chars like '.' and '_'
+ success = InternalFindMovie(movieTitle, movieYear, movieList, false);
+ }
+ return success;
+}
+
+bool CVideoInfoDownloader::GetArtwork(CVideoInfoTag &details)
+{
+ return m_info->GetArtwork(*m_http, details);
+}
+
+bool CVideoInfoDownloader::GetDetails(const CScraperUrl &url,
+ CVideoInfoTag &movieDetails,
+ CGUIDialogProgress *pProgress /* = NULL */)
+{
+ //CLog::Log(LOGDEBUG,"CVideoInfoDownloader::GetDetails({})", url.m_strURL);
+ m_url = url;
+ m_movieDetails = movieDetails;
+
+ // fill in the defaults
+ movieDetails.Reset();
+ if (pProgress)
+ { // threaded version
+ m_state = GET_DETAILS;
+ m_found = 0;
+ if (IsRunning())
+ StopThread();
+ Create();
+ while (!m_found)
+ {
+ pProgress->Progress();
+ if (pProgress->IsCanceled())
+ {
+ CloseThread();
+ return false;
+ }
+ CThread::Sleep(1ms);
+ }
+ movieDetails = m_movieDetails;
+ CloseThread();
+ return true;
+ }
+ else // unthreaded
+ return m_info->GetVideoDetails(*m_http, url, true/*fMovie*/, movieDetails);
+}
+
+bool CVideoInfoDownloader::GetEpisodeDetails(const CScraperUrl &url,
+ CVideoInfoTag &movieDetails,
+ CGUIDialogProgress *pProgress /* = NULL */)
+{
+ //CLog::Log(LOGDEBUG,"CVideoInfoDownloader::GetDetails({})", url.m_strURL);
+ m_url = url;
+ m_movieDetails = movieDetails;
+
+ // fill in the defaults
+ movieDetails.Reset();
+ if (pProgress)
+ { // threaded version
+ m_state = GET_EPISODE_DETAILS;
+ m_found = 0;
+ if (IsRunning())
+ StopThread();
+ Create();
+ while (!m_found)
+ {
+ pProgress->Progress();
+ if (pProgress->IsCanceled())
+ {
+ CloseThread();
+ return false;
+ }
+ CThread::Sleep(1ms);
+ }
+ movieDetails = m_movieDetails;
+ CloseThread();
+ return true;
+ }
+ else // unthreaded
+ return m_info->GetVideoDetails(*m_http, url, false/*fMovie*/, movieDetails);
+}
+
+bool CVideoInfoDownloader::GetEpisodeList(const CScraperUrl& url,
+ EPISODELIST& movieDetails,
+ CGUIDialogProgress *pProgress /* = NULL */)
+{
+ //CLog::Log(LOGDEBUG,"CVideoInfoDownloader::GetDetails({})", url.m_strURL);
+ m_url = url;
+ m_episode = movieDetails;
+
+ // fill in the defaults
+ movieDetails.clear();
+ if (pProgress)
+ { // threaded version
+ m_state = GET_EPISODE_LIST;
+ m_found = 0;
+ if (IsRunning())
+ StopThread();
+ Create();
+ while (!m_found)
+ {
+ pProgress->Progress();
+ if (pProgress->IsCanceled())
+ {
+ CloseThread();
+ return false;
+ }
+ CThread::Sleep(1ms);
+ }
+ movieDetails = m_episode;
+ CloseThread();
+ return true;
+ }
+ else // unthreaded
+ return !(movieDetails = m_info->GetEpisodeList(*m_http, url)).empty();
+}
+
+void CVideoInfoDownloader::CloseThread()
+{
+ m_http->Cancel();
+ StopThread();
+ m_http->Reset();
+ m_state = DO_NOTHING;
+ m_found = 0;
+}
+
diff --git a/xbmc/video/VideoInfoDownloader.h b/xbmc/video/VideoInfoDownloader.h
new file mode 100644
index 0000000..5a0f893
--- /dev/null
+++ b/xbmc/video/VideoInfoDownloader.h
@@ -0,0 +1,87 @@
+/*
+ * 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 "Episode.h"
+#include "VideoInfoTag.h"
+#include "addons/Scraper.h"
+#include "threads/Thread.h"
+
+#include <string>
+#include <vector>
+
+// forward declarations
+class CXBMCTinyXML;
+class CGUIDialogProgress;
+
+namespace ADDON
+{
+class CScraperError;
+}
+namespace XFILE
+{
+class CurlFile;
+}
+
+typedef std::vector<CScraperUrl> MOVIELIST;
+
+class CVideoInfoDownloader : public CThread
+{
+public:
+ explicit CVideoInfoDownloader(const ADDON::ScraperPtr &scraper);
+ ~CVideoInfoDownloader() override;
+
+ // threaded lookup functions
+
+ /*! \brief Do a search for matching media items (possibly asynchronously) with our scraper
+ \param movieTitle title of the media item to look for
+ \param movieYear year of the media item to look for (-1 if not known)
+ \param movielist [out] list of results to fill. May be empty on success.
+ \param pProgress progress bar to update as we go. If NULL we run on thread, if non-NULL we run off thread.
+ \return 1 on success, -1 on a scraper-specific error, 0 on some other error
+ */
+ int FindMovie(const std::string& movieTitle, int movieYear, MOVIELIST& movielist, CGUIDialogProgress *pProgress = NULL);
+
+ /*! \brief Fetch art URLs for an item with our scraper
+ \param details the video info tag structure to fill with art.
+ \return true on success, false on failure.
+ */
+ bool GetArtwork(CVideoInfoTag &details);
+
+ bool GetDetails(const CScraperUrl& url, CVideoInfoTag &movieDetails, CGUIDialogProgress *pProgress = NULL);
+ bool GetEpisodeDetails(const CScraperUrl& url, CVideoInfoTag &movieDetails, CGUIDialogProgress *pProgress = NULL);
+ bool GetEpisodeList(const CScraperUrl& url, VIDEO::EPISODELIST& details, CGUIDialogProgress *pProgress = NULL);
+
+ static void ShowErrorDialog(const ADDON::CScraperError &sce);
+
+protected:
+ enum LOOKUP_STATE { DO_NOTHING = 0,
+ FIND_MOVIE = 1,
+ GET_DETAILS = 2,
+ GET_EPISODE_LIST = 3,
+ GET_EPISODE_DETAILS = 4 };
+
+ XFILE::CCurlFile* m_http;
+ std::string m_movieTitle;
+ int m_movieYear;
+ MOVIELIST m_movieList;
+ CVideoInfoTag m_movieDetails;
+ CScraperUrl m_url;
+ VIDEO::EPISODELIST m_episode;
+ LOOKUP_STATE m_state;
+ int m_found;
+ ADDON::ScraperPtr m_info;
+
+ // threaded stuff
+ void Process() override;
+ void CloseThread();
+
+ int InternalFindMovie(const std::string& movieTitle, int movieYear, MOVIELIST& movielist, bool cleanChars = true);
+};
+
diff --git a/xbmc/video/VideoInfoScanner.cpp b/xbmc/video/VideoInfoScanner.cpp
new file mode 100644
index 0000000..92fa5c1
--- /dev/null
+++ b/xbmc/video/VideoInfoScanner.cpp
@@ -0,0 +1,2247 @@
+/*
+ * 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 "VideoInfoScanner.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIUserMessages.h"
+#include "NfoFile.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "URL.h"
+#include "Util.h"
+#include "VideoInfoDownloader.h"
+#include "cores/VideoPlayer/DVDFileInfo.h"
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "events/EventLog.h"
+#include "events/MediaLibraryEvent.h"
+#include "filesystem/Directory.h"
+#include "filesystem/DirectoryCache.h"
+#include "filesystem/File.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "filesystem/PluginDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "tags/VideoInfoTagLoaderFactory.h"
+#include "utils/Digest.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/RegExp.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoThumbLoader.h"
+
+#include <algorithm>
+#include <utility>
+
+using namespace XFILE;
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+using KODI::UTILITY::CDigest;
+
+namespace VIDEO
+{
+
+ CVideoInfoScanner::CVideoInfoScanner()
+ {
+ m_bStop = false;
+ m_scanAll = false;
+ }
+
+ CVideoInfoScanner::~CVideoInfoScanner()
+ = default;
+
+ void CVideoInfoScanner::Process()
+ {
+ m_bStop = false;
+
+ try
+ {
+ if (m_showDialog && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_BACKGROUNDUPDATE))
+ {
+ CGUIDialogExtendedProgressBar* dialog =
+ CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>(WINDOW_DIALOG_EXT_PROGRESS);
+ if (dialog)
+ m_handle = dialog->GetHandle(g_localizeStrings.Get(314));
+ }
+
+ // check if we only need to perform a cleaning
+ if (m_bClean && m_pathsToScan.empty())
+ {
+ std::set<int> paths;
+ m_database.CleanDatabase(m_handle, paths, false);
+
+ if (m_handle)
+ m_handle->MarkFinished();
+ m_handle = NULL;
+
+ m_bRunning = false;
+
+ return;
+ }
+
+ auto start = std::chrono::steady_clock::now();
+
+ m_database.Open();
+
+ m_bCanInterrupt = true;
+
+ CLog::Log(LOGINFO, "VideoInfoScanner: Starting scan ..");
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
+ "OnScanStarted");
+
+ // Database operations should not be canceled
+ // using Interrupt() while scanning as it could
+ // result in unexpected behaviour.
+ m_bCanInterrupt = false;
+
+ bool bCancelled = false;
+ while (!bCancelled && !m_pathsToScan.empty())
+ {
+ /*
+ * A copy of the directory path is used because the path supplied is
+ * immediately removed from the m_pathsToScan set in DoScan(). If the
+ * reference points to the entry in the set a null reference error
+ * occurs.
+ */
+ std::string directory = *m_pathsToScan.begin();
+ if (m_bStop)
+ {
+ bCancelled = true;
+ }
+ else if (!CDirectory::Exists(directory))
+ {
+ /*
+ * Note that this will skip clean (if m_bClean is enabled) if the directory really
+ * doesn't exist rather than a NAS being switched off. A manual clean from settings
+ * will still pick up and remove it though.
+ */
+ CLog::Log(LOGWARNING, "{} directory '{}' does not exist - skipping scan{}.", __FUNCTION__,
+ CURL::GetRedacted(directory), m_bClean ? " and clean" : "");
+ m_pathsToScan.erase(m_pathsToScan.begin());
+ }
+ else if (!DoScan(directory))
+ bCancelled = true;
+ }
+
+ if (!bCancelled)
+ {
+ if (m_bClean)
+ m_database.CleanDatabase(m_handle, m_pathsToClean, false);
+ else
+ {
+ if (m_handle)
+ m_handle->SetTitle(g_localizeStrings.Get(331));
+ m_database.Compress(false);
+ }
+ }
+
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetLibraryInfoProvider().ResetLibraryBools();
+ m_database.Close();
+
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ CLog::Log(LOGINFO, "VideoInfoScanner: Finished scan. Scanning for video info took {} ms",
+ duration.count());
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "VideoInfoScanner: Exception while scanning.");
+ }
+
+ m_bRunning = false;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary,
+ "OnScanFinished");
+
+ if (m_handle)
+ m_handle->MarkFinished();
+ m_handle = NULL;
+ }
+
+ void CVideoInfoScanner::Start(const std::string& strDirectory, bool scanAll)
+ {
+ m_strStartDir = strDirectory;
+ m_scanAll = scanAll;
+ m_pathsToScan.clear();
+ m_pathsToClean.clear();
+
+ m_database.Open();
+ if (strDirectory.empty())
+ { // scan all paths in the database. We do this by scanning all paths in the db, and crossing them off the list as
+ // we go.
+ m_database.GetPaths(m_pathsToScan);
+ }
+ else
+ { // scan all the paths of this subtree that is in the database
+ std::vector<std::string> rootDirs;
+ if (URIUtils::IsMultiPath(strDirectory))
+ CMultiPathDirectory::GetPaths(strDirectory, rootDirs);
+ else
+ rootDirs.push_back(strDirectory);
+
+ for (std::vector<std::string>::const_iterator it = rootDirs.begin(); it < rootDirs.end(); ++it)
+ {
+ m_pathsToScan.insert(*it);
+ std::vector<std::pair<int, std::string>> subpaths;
+ m_database.GetSubPaths(*it, subpaths);
+ for (std::vector<std::pair<int, std::string>>::iterator it = subpaths.begin(); it < subpaths.end(); ++it)
+ m_pathsToScan.insert(it->second);
+ }
+ }
+ m_database.Close();
+ m_bClean = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryCleanOnUpdate;
+
+ m_bRunning = true;
+ Process();
+ }
+
+ void CVideoInfoScanner::Stop()
+ {
+ if (m_bCanInterrupt)
+ m_database.Interrupt();
+
+ m_bStop = true;
+ }
+
+ static void OnDirectoryScanned(const std::string& strDirectory)
+ {
+ CGUIMessage msg(GUI_MSG_DIRECTORY_SCANNED, 0, 0, 0);
+ msg.SetStringParam(strDirectory);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+
+ bool CVideoInfoScanner::DoScan(const std::string& strDirectory)
+ {
+ if (m_handle)
+ {
+ m_handle->SetText(g_localizeStrings.Get(20415));
+ }
+
+ /*
+ * Remove this path from the list we're processing. This must be done prior to
+ * the check for file or folder exclusion to prevent an infinite while loop
+ * in Process().
+ */
+ std::set<std::string>::iterator it = m_pathsToScan.find(strDirectory);
+ if (it != m_pathsToScan.end())
+ m_pathsToScan.erase(it);
+
+ // load subfolder
+ CFileItemList items;
+ bool foundDirectly = false;
+ bool bSkip = false;
+
+ SScanSettings settings;
+ ScraperPtr info = m_database.GetScraperForPath(strDirectory, settings, foundDirectly);
+ CONTENT_TYPE content = info ? info->Content() : CONTENT_NONE;
+
+ // exclude folders that match our exclude regexps
+ const std::vector<std::string> &regexps = content == CONTENT_TVSHOWS ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps
+ : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps;
+
+ if (CUtil::ExcludeFileOrFolder(strDirectory, regexps))
+ return true;
+
+ if (HasNoMedia(strDirectory))
+ return true;
+
+ bool ignoreFolder = !m_scanAll && settings.noupdate;
+ if (content == CONTENT_NONE || ignoreFolder)
+ return true;
+
+ if (URIUtils::IsPlugin(strDirectory) && !CPluginDirectory::IsMediaLibraryScanningAllowed(TranslateContent(content), strDirectory))
+ {
+ CLog::Log(
+ LOGINFO,
+ "VideoInfoScanner: Plugin '{}' does not support media library scanning for '{}' content",
+ CURL::GetRedacted(strDirectory), TranslateContent(content));
+ return true;
+ }
+
+ std::string hash, dbHash;
+ if (content == CONTENT_MOVIES ||content == CONTENT_MUSICVIDEOS)
+ {
+ if (m_handle)
+ {
+ int str = content == CONTENT_MOVIES ? 20317:20318;
+ m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(str), info->Name()));
+ }
+
+ std::string fastHash;
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash && !URIUtils::IsPlugin(strDirectory))
+ fastHash = GetFastHash(strDirectory, regexps);
+
+ if (m_database.GetPathHash(strDirectory, dbHash) && !fastHash.empty() && StringUtils::EqualsNoCase(fastHash, dbHash))
+ { // fast hashes match - no need to process anything
+ hash = fastHash;
+ }
+ else
+ { // need to fetch the folder
+ CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
+ DIR_FLAG_DEFAULTS);
+ // do not consider inner folders with .nomedia
+ items.erase(std::remove_if(items.begin(), items.end(),
+ [this](const CFileItemPtr& item) {
+ return item->m_bIsFolder && HasNoMedia(item->GetPath());
+ }),
+ items.end());
+ items.Stack();
+
+ // check whether to re-use previously computed fast hash
+ if (!CanFastHash(items, regexps) || fastHash.empty())
+ GetPathHash(items, hash);
+ else
+ hash = fastHash;
+ }
+
+ if (StringUtils::EqualsNoCase(hash, dbHash))
+ { // hash matches - skipping
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '{}' due to no change{}",
+ CURL::GetRedacted(strDirectory), !fastHash.empty() ? " (fasthash)" : "");
+ bSkip = true;
+ }
+ else if (hash.empty())
+ { // directory empty or non-existent - add to clean list and skip
+ CLog::Log(LOGDEBUG,
+ "VideoInfoScanner: Skipping dir '{}' as it's empty or doesn't exist - adding to "
+ "clean list",
+ CURL::GetRedacted(strDirectory));
+ if (m_bClean)
+ m_pathsToClean.insert(m_database.GetPathId(strDirectory));
+ bSkip = true;
+ }
+ else if (dbHash.empty())
+ { // new folder - scan
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '{}' as not in the database",
+ CURL::GetRedacted(strDirectory));
+ }
+ else
+ { // hash changed - rescan
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})",
+ CURL::GetRedacted(strDirectory), dbHash, hash);
+ }
+ }
+ else if (content == CONTENT_TVSHOWS)
+ {
+ if (m_handle)
+ m_handle->SetTitle(StringUtils::Format(g_localizeStrings.Get(20319), info->Name()));
+
+ if (foundDirectly && !settings.parent_name_root)
+ {
+ CDirectory::GetDirectory(strDirectory, items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
+ DIR_FLAG_DEFAULTS);
+ items.SetPath(strDirectory);
+ GetPathHash(items, hash);
+ bSkip = true;
+ if (!m_database.GetPathHash(strDirectory, dbHash) || !StringUtils::EqualsNoCase(dbHash, hash))
+ bSkip = false;
+ else
+ items.Clear();
+ }
+ else
+ {
+ CFileItemPtr item(new CFileItem(URIUtils::GetFileName(strDirectory)));
+ item->SetPath(strDirectory);
+ item->m_bIsFolder = true;
+ items.Add(item);
+ items.SetPath(URIUtils::GetParentPath(item->GetPath()));
+ }
+ }
+
+ if (!bSkip)
+ {
+ if (RetrieveVideoInfo(items, settings.parent_name_root, content))
+ {
+ if (!m_bStop && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
+ {
+ m_database.SetPathHash(strDirectory, hash);
+ if (m_bClean)
+ m_pathsToClean.insert(m_database.GetPathId(strDirectory));
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Finished adding information from dir {}",
+ CURL::GetRedacted(strDirectory));
+ }
+ }
+ else
+ {
+ if (m_bClean)
+ m_pathsToClean.insert(m_database.GetPathId(strDirectory));
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: No (new) information was found in dir {}",
+ CURL::GetRedacted(strDirectory));
+ }
+ }
+ else if (!StringUtils::EqualsNoCase(hash, dbHash) && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
+ { // update the hash either way - we may have changed the hash to a fast version
+ m_database.SetPathHash(strDirectory, hash);
+ }
+
+ if (m_handle)
+ OnDirectoryScanned(strDirectory);
+
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr pItem = items[i];
+
+ if (m_bStop)
+ break;
+
+ // if we have a directory item (non-playlist) we then recurse into that folder
+ // do not recurse for tv shows - we have already looked recursively for episodes
+ if (pItem->m_bIsFolder && !pItem->IsParentFolder() && !pItem->IsPlayList() && settings.recurse > 0 && content != CONTENT_TVSHOWS)
+ {
+ if (!DoScan(pItem->GetPath()))
+ {
+ m_bStop = true;
+ }
+ }
+ }
+ return !m_bStop;
+ }
+
+ bool CVideoInfoScanner::RetrieveVideoInfo(CFileItemList& items, bool bDirNames, CONTENT_TYPE content, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress)
+ {
+ if (pDlgProgress)
+ {
+ if (items.Size() > 1 || (items[0]->m_bIsFolder && fetchEpisodes))
+ {
+ pDlgProgress->ShowProgressBar(true);
+ pDlgProgress->SetPercentage(0);
+ }
+ else
+ pDlgProgress->ShowProgressBar(false);
+
+ pDlgProgress->Progress();
+ }
+
+ m_database.Open();
+
+ bool FoundSomeInfo = false;
+ std::vector<int> seenPaths;
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr pItem = items[i];
+
+ // we do this since we may have a override per dir
+ ScraperPtr info2 = m_database.GetScraperForPath(pItem->m_bIsFolder ? pItem->GetPath() : items.GetPath());
+ if (!info2) // skip
+ continue;
+
+ // Discard all .nomedia folders
+ if (pItem->m_bIsFolder && HasNoMedia(pItem->GetPath()))
+ continue;
+
+ // Discard all exclude files defined by regExExclude
+ if (CUtil::ExcludeFileOrFolder(pItem->GetPath(), (content == CONTENT_TVSHOWS) ? CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps
+ : CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps))
+ continue;
+
+ if (info2->Content() == CONTENT_MOVIES || info2->Content() == CONTENT_MUSICVIDEOS)
+ {
+ if (m_handle)
+ m_handle->SetPercentage(i*100.f/items.Size());
+ }
+
+ // clear our scraper cache
+ info2->ClearCache();
+
+ INFO_RET ret = INFO_CANCELLED;
+ if (info2->Content() == CONTENT_TVSHOWS)
+ ret = RetrieveInfoForTvShow(pItem.get(), bDirNames, info2, useLocal, pURL, fetchEpisodes, pDlgProgress);
+ else if (info2->Content() == CONTENT_MOVIES)
+ ret = RetrieveInfoForMovie(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
+ else if (info2->Content() == CONTENT_MUSICVIDEOS)
+ ret = RetrieveInfoForMusicVideo(pItem.get(), bDirNames, info2, useLocal, pURL, pDlgProgress);
+ else
+ {
+ CLog::Log(LOGERROR, "VideoInfoScanner: Unknown content type {} ({})", info2->Content(),
+ CURL::GetRedacted(pItem->GetPath()));
+ FoundSomeInfo = false;
+ break;
+ }
+ if (ret == INFO_CANCELLED || ret == INFO_ERROR)
+ {
+ CLog::Log(LOGWARNING,
+ "VideoInfoScanner: Error {} occurred while retrieving"
+ "information for {}.",
+ ret, CURL::GetRedacted(pItem->GetPath()));
+ FoundSomeInfo = false;
+ break;
+ }
+ if (ret == INFO_ADDED || ret == INFO_HAVE_ALREADY)
+ FoundSomeInfo = true;
+ else if (ret == INFO_NOT_FOUND)
+ {
+ CLog::Log(LOGWARNING,
+ "No information found for item '{}', it won't be added to the library.",
+ CURL::GetRedacted(pItem->GetPath()));
+
+ MediaType mediaType = MediaTypeMovie;
+ if (info2->Content() == CONTENT_TVSHOWS)
+ mediaType = MediaTypeTvShow;
+ else if (info2->Content() == CONTENT_MUSICVIDEOS)
+ mediaType = MediaTypeMusicVideo;
+
+ auto eventLog = CServiceBroker::GetEventLog();
+ if (eventLog)
+ eventLog->Add(EventPtr(new CMediaLibraryEvent(
+ mediaType, pItem->GetPath(), 24145,
+ StringUtils::Format(g_localizeStrings.Get(24147), mediaType,
+ URIUtils::GetFileName(pItem->GetPath())),
+ pItem->GetArt("thumb"), CURL::GetRedacted(pItem->GetPath()), EventLevel::Warning)));
+ }
+
+ pURL = NULL;
+
+ // Keep track of directories we've seen
+ if (m_bClean && pItem->m_bIsFolder)
+ seenPaths.push_back(m_database.GetPathId(pItem->GetPath()));
+ }
+
+ if (content == CONTENT_TVSHOWS && ! seenPaths.empty())
+ {
+ std::vector<std::pair<int, std::string>> libPaths;
+ m_database.GetSubPaths(items.GetPath(), libPaths);
+ for (std::vector<std::pair<int, std::string> >::iterator i = libPaths.begin(); i < libPaths.end(); ++i)
+ {
+ if (find(seenPaths.begin(), seenPaths.end(), i->first) == seenPaths.end())
+ m_pathsToClean.insert(i->first);
+ }
+ }
+ if(pDlgProgress)
+ pDlgProgress->ShowProgressBar(false);
+
+ m_database.Close();
+ return FoundSomeInfo;
+ }
+
+ CInfoScanner::INFO_RET
+ CVideoInfoScanner::RetrieveInfoForTvShow(CFileItem *pItem,
+ bool bDirNames,
+ ScraperPtr &info2,
+ bool useLocal,
+ CScraperUrl* pURL,
+ bool fetchEpisodes,
+ CGUIDialogProgress* pDlgProgress)
+ {
+ long idTvShow = -1;
+ std::string strPath = pItem->GetPath();
+ if (pItem->m_bIsFolder)
+ idTvShow = m_database.GetTvShowId(strPath);
+ else if (pItem->IsPlugin() && pItem->HasVideoInfoTag() && pItem->GetVideoInfoTag()->m_iIdShow >= 0)
+ {
+ // for plugin source we cannot get idTvShow from episode path with URIUtils::GetDirectory() in all cases
+ // so use m_iIdShow from video info tag if possible
+ idTvShow = pItem->GetVideoInfoTag()->m_iIdShow;
+ CVideoInfoTag showInfo;
+ if (m_database.GetTvShowInfo(std::string(), showInfo, idTvShow, nullptr, 0))
+ strPath = showInfo.GetPath();
+ }
+ else
+ {
+ strPath = URIUtils::GetDirectory(strPath);
+ idTvShow = m_database.GetTvShowId(strPath);
+ }
+ if (idTvShow > -1 && (fetchEpisodes || !pItem->m_bIsFolder))
+ {
+ INFO_RET ret = RetrieveInfoForEpisodes(pItem, idTvShow, info2, useLocal, pDlgProgress);
+ if (ret == INFO_ADDED)
+ m_database.SetPathHash(strPath, pItem->GetProperty("hash").asString());
+ return ret;
+ }
+
+ if (ProgressCancelled(pDlgProgress, pItem->m_bIsFolder ? 20353 : 20361, pItem->GetLabel()))
+ return INFO_CANCELLED;
+
+ if (m_handle)
+ m_handle->SetText(pItem->GetMovieName(bDirNames));
+
+ CInfoScanner::INFO_TYPE result=CInfoScanner::NO_NFO;
+ CScraperUrl scrUrl;
+ // handle .nfo files
+ std::unique_ptr<IVideoInfoTagLoader> loader;
+ if (useLocal)
+ {
+ loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
+ if (loader)
+ {
+ pItem->GetVideoInfoTag()->Reset();
+ result = loader->Load(*pItem->GetVideoInfoTag(), false);
+ }
+ }
+
+ if (result == CInfoScanner::FULL_NFO)
+ {
+
+ long lResult = AddVideo(pItem, info2->Content(), bDirNames, useLocal);
+ if (lResult < 0)
+ return INFO_ERROR;
+ if (fetchEpisodes)
+ {
+ INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
+ if (ret == INFO_ADDED)
+ m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
+ return ret;
+ }
+ return INFO_ADDED;
+ }
+ if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
+ {
+ scrUrl = loader->ScraperUrl();
+ pURL = &scrUrl;
+ }
+
+ CScraperUrl url;
+ int retVal = 0;
+ std::string movieTitle = pItem->GetMovieName(bDirNames);
+ int movieYear = -1; // hint that movie title was not found
+ if (result == CInfoScanner::TITLE_NFO)
+ {
+ CVideoInfoTag* tag = pItem->GetVideoInfoTag();
+ movieTitle = tag->GetTitle();
+ movieYear = tag->GetYear(); // movieYear is expected to be >= 0
+ }
+ if (pURL && pURL->HasUrls())
+ url = *pURL;
+ else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
+ return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
+
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
+ url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
+
+ long lResult = -1;
+ if (GetDetails(pItem, url, info2,
+ (result == CInfoScanner::COMBINED_NFO ||
+ result == CInfoScanner::OVERRIDE_NFO) ? loader.get() : nullptr,
+ pDlgProgress))
+ {
+ if ((lResult = AddVideo(pItem, info2->Content(), false, useLocal)) < 0)
+ return INFO_ERROR;
+ }
+ if (fetchEpisodes)
+ {
+ INFO_RET ret = RetrieveInfoForEpisodes(pItem, lResult, info2, useLocal, pDlgProgress);
+ if (ret == INFO_ADDED)
+ m_database.SetPathHash(pItem->GetPath(), pItem->GetProperty("hash").asString());
+ }
+ return INFO_ADDED;
+ }
+
+ CInfoScanner::INFO_RET
+ CVideoInfoScanner::RetrieveInfoForMovie(CFileItem *pItem,
+ bool bDirNames,
+ ScraperPtr &info2,
+ bool useLocal,
+ CScraperUrl* pURL,
+ CGUIDialogProgress* pDlgProgress)
+ {
+ if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
+ (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
+ return INFO_NOT_NEEDED;
+
+ if (ProgressCancelled(pDlgProgress, 198, pItem->GetLabel()))
+ return INFO_CANCELLED;
+
+ if (m_database.HasMovieInfo(pItem->GetDynPath()))
+ return INFO_HAVE_ALREADY;
+
+ if (m_handle)
+ m_handle->SetText(pItem->GetMovieName(bDirNames));
+
+ CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
+ CScraperUrl scrUrl;
+ // handle .nfo files
+ std::unique_ptr<IVideoInfoTagLoader> loader;
+ if (useLocal)
+ {
+ loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
+ if (loader)
+ {
+ pItem->GetVideoInfoTag()->Reset();
+ result = loader->Load(*pItem->GetVideoInfoTag(), false);
+ }
+ }
+ if (result == CInfoScanner::FULL_NFO)
+ {
+ if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
+ return INFO_ERROR;
+ return INFO_ADDED;
+ }
+ if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
+ {
+ scrUrl = loader->ScraperUrl();
+ pURL = &scrUrl;
+ }
+
+ CScraperUrl url;
+ int retVal = 0;
+ std::string movieTitle = pItem->GetMovieName(bDirNames);
+ int movieYear = -1; // hint that movie title was not found
+ if (result == CInfoScanner::TITLE_NFO)
+ {
+ CVideoInfoTag* tag = pItem->GetVideoInfoTag();
+ movieTitle = tag->GetTitle();
+ movieYear = tag->GetYear(); // movieYear is expected to be >= 0
+ }
+ if (pURL && pURL->HasUrls())
+ url = *pURL;
+ else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
+ return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
+
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
+ url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
+
+ if (GetDetails(pItem, url, info2,
+ (result == CInfoScanner::COMBINED_NFO ||
+ result == CInfoScanner::OVERRIDE_NFO) ? loader.get() : nullptr,
+ pDlgProgress))
+ {
+ if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
+ return INFO_ERROR;
+ return INFO_ADDED;
+ }
+ //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled
+ return INFO_NOT_FOUND;
+ }
+
+ CInfoScanner::INFO_RET
+ CVideoInfoScanner::RetrieveInfoForMusicVideo(CFileItem *pItem,
+ bool bDirNames,
+ ScraperPtr &info2,
+ bool useLocal,
+ CScraperUrl* pURL,
+ CGUIDialogProgress* pDlgProgress)
+ {
+ if (pItem->m_bIsFolder || !pItem->IsVideo() || pItem->IsNFO() ||
+ (pItem->IsPlayList() && !URIUtils::HasExtension(pItem->GetPath(), ".strm")))
+ return INFO_NOT_NEEDED;
+
+ if (ProgressCancelled(pDlgProgress, 20394, pItem->GetLabel()))
+ return INFO_CANCELLED;
+
+ if (m_database.HasMusicVideoInfo(pItem->GetPath()))
+ return INFO_HAVE_ALREADY;
+
+ if (m_handle)
+ m_handle->SetText(pItem->GetMovieName(bDirNames));
+
+ CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
+ CScraperUrl scrUrl;
+ // handle .nfo files
+ std::unique_ptr<IVideoInfoTagLoader> loader;
+ if (useLocal)
+ {
+ loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*pItem, info2, bDirNames));
+ if (loader)
+ {
+ pItem->GetVideoInfoTag()->Reset();
+ result = loader->Load(*pItem->GetVideoInfoTag(), false);
+ }
+ }
+ if (result == CInfoScanner::FULL_NFO)
+ {
+ if (AddVideo(pItem, info2->Content(), bDirNames, true) < 0)
+ return INFO_ERROR;
+ return INFO_ADDED;
+ }
+ if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
+ {
+ scrUrl = loader->ScraperUrl();
+ pURL = &scrUrl;
+ }
+
+ CScraperUrl url;
+ int retVal = 0;
+ std::string movieTitle = pItem->GetMovieName(bDirNames);
+ int movieYear = -1; // hint that movie title was not found
+ if (result == CInfoScanner::TITLE_NFO)
+ {
+ CVideoInfoTag* tag = pItem->GetVideoInfoTag();
+ movieTitle = tag->GetTitle();
+ movieYear = tag->GetYear(); // movieYear is expected to be >= 0
+ }
+ if (pURL && pURL->HasUrls())
+ url = *pURL;
+ else if ((retVal = FindVideo(movieTitle, movieYear, info2, url, pDlgProgress)) <= 0)
+ return retVal < 0 ? INFO_CANCELLED : INFO_NOT_FOUND;
+
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Fetching url '{}' using {} scraper (content: '{}')",
+ url.GetFirstThumbUrl(), info2->Name(), TranslateContent(info2->Content()));
+
+ if (GetDetails(pItem, url, info2,
+ (result == CInfoScanner::COMBINED_NFO ||
+ result == CInfoScanner::OVERRIDE_NFO) ? loader.get() : nullptr,
+ pDlgProgress))
+ {
+ if (AddVideo(pItem, info2->Content(), bDirNames, useLocal) < 0)
+ return INFO_ERROR;
+ return INFO_ADDED;
+ }
+ //! @todo This is not strictly correct as we could fail to download information here or error, or be cancelled
+ return INFO_NOT_FOUND;
+ }
+
+ CInfoScanner::INFO_RET
+ CVideoInfoScanner::RetrieveInfoForEpisodes(CFileItem *item,
+ long showID,
+ const ADDON::ScraperPtr &scraper,
+ bool useLocal,
+ CGUIDialogProgress *progress)
+ {
+ // enumerate episodes
+ EPISODELIST files;
+ if (!EnumerateSeriesFolder(item, files))
+ return INFO_HAVE_ALREADY;
+ if (files.empty()) // no update or no files
+ return INFO_NOT_NEEDED;
+
+ if (m_bStop || (progress && progress->IsCanceled()))
+ return INFO_CANCELLED;
+
+ CVideoInfoTag showInfo;
+ m_database.GetTvShowInfo("", showInfo, showID);
+ INFO_RET ret = OnProcessSeriesFolder(files, scraper, useLocal, showInfo, progress);
+
+ if (ret == INFO_ADDED)
+ {
+ std::map<int, std::map<std::string, std::string>> seasonArt;
+ m_database.GetTvShowSeasonArt(showID, seasonArt);
+
+ bool updateSeasonArt = false;
+ for (std::map<int, std::map<std::string, std::string>>::const_iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
+ {
+ if (i->second.empty())
+ {
+ updateSeasonArt = true;
+ break;
+ }
+ }
+
+ if (updateSeasonArt)
+ {
+ if (!item->IsPlugin() || scraper->ID() != "metadata.local")
+ {
+ CVideoInfoDownloader loader(scraper);
+ loader.GetArtwork(showInfo);
+ }
+ GetSeasonThumbs(showInfo, seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), useLocal && !item->IsPlugin());
+ for (std::map<int, std::map<std::string, std::string> >::const_iterator i = seasonArt.begin(); i != seasonArt.end(); ++i)
+ {
+ int seasonID = m_database.AddSeason(showID, i->first);
+ m_database.SetArtForItem(seasonID, MediaTypeSeason, i->second);
+ }
+ }
+ }
+ return ret;
+ }
+
+ bool CVideoInfoScanner::EnumerateSeriesFolder(CFileItem* item, EPISODELIST& episodeList)
+ {
+ CFileItemList items;
+ const std::vector<std::string> &regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowExcludeFromScanRegExps;
+
+ bool bSkip = false;
+
+ if (item->m_bIsFolder)
+ {
+ /*
+ * Note: DoScan() will not remove this path as it's not recursing for tvshows.
+ * Remove this path from the list we're processing in order to avoid hitting
+ * it twice in the main loop.
+ */
+ std::set<std::string>::iterator it = m_pathsToScan.find(item->GetPath());
+ if (it != m_pathsToScan.end())
+ m_pathsToScan.erase(it);
+
+ std::string hash, dbHash;
+ bool allowEmptyHash = false;
+ if (item->IsPlugin())
+ {
+ // if plugin has already calculated a hash for directory contents - use it
+ // in this case we don't need to get directory listing from plugin for hash checking
+ if (item->HasProperty("hash"))
+ {
+ hash = item->GetProperty("hash").asString();
+ allowEmptyHash = true;
+ }
+ }
+ else if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash)
+ hash = GetRecursiveFastHash(item->GetPath(), regexps);
+
+ if (m_database.GetPathHash(item->GetPath(), dbHash) && (allowEmptyHash || !hash.empty()) && StringUtils::EqualsNoCase(dbHash, hash))
+ {
+ // fast hashes match - no need to process anything
+ bSkip = true;
+ }
+
+ // fast hash cannot be computed or we need to rescan. fetch the listing.
+ if (!bSkip)
+ {
+ int flags = DIR_FLAG_DEFAULTS;
+ if (!hash.empty())
+ flags |= DIR_FLAG_NO_FILE_INFO;
+
+ CUtil::GetRecursiveListing(item->GetPath(), items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(), flags);
+
+ // fast hash failed - compute slow one
+ if (hash.empty())
+ {
+ GetPathHash(items, hash);
+ if (StringUtils::EqualsNoCase(dbHash, hash))
+ {
+ // slow hashes match - no need to process anything
+ bSkip = true;
+ }
+ }
+ }
+
+ if (bSkip)
+ {
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Skipping dir '{}' due to no change",
+ CURL::GetRedacted(item->GetPath()));
+ // update our dialog with our progress
+ if (m_handle)
+ OnDirectoryScanned(item->GetPath());
+ return false;
+ }
+
+ if (dbHash.empty())
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Scanning dir '{}' as not in the database",
+ CURL::GetRedacted(item->GetPath()));
+ else
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Rescanning dir '{}' due to change ({} != {})",
+ CURL::GetRedacted(item->GetPath()), dbHash, hash);
+
+ if (m_bClean)
+ {
+ m_pathsToClean.insert(m_database.GetPathId(item->GetPath()));
+ m_database.GetPathsForTvShow(m_database.GetTvShowId(item->GetPath()), m_pathsToClean);
+ }
+ item->SetProperty("hash", hash);
+ }
+ else
+ {
+ CFileItemPtr newItem(new CFileItem(*item));
+ items.Add(newItem);
+ }
+
+ /*
+ stack down any dvd folders
+ need to sort using the full path since this is a collapsed recursive listing of all subdirs
+ video_ts.ifo files should sort at the top of a dvd folder in ascending order
+
+ /foo/bar/video_ts.ifo
+ /foo/bar/vts_x_y.ifo
+ /foo/bar/vts_x_y.vob
+ */
+
+ // since we're doing this now anyway, should other items be stacked?
+ items.Sort(SortByPath, SortOrderAscending);
+ int x = 0;
+ while (x < items.Size())
+ {
+ if (items[x]->m_bIsFolder)
+ {
+ x++;
+ continue;
+ }
+
+ std::string strPathX, strFileX;
+ URIUtils::Split(items[x]->GetPath(), strPathX, strFileX);
+ //CLog::Log(LOGDEBUG,"{}:{}:{}", x, strPathX, strFileX);
+
+ const int y = x + 1;
+ if (StringUtils::EqualsNoCase(strFileX, "VIDEO_TS.IFO"))
+ {
+ while (y < items.Size())
+ {
+ std::string strPathY, strFileY;
+ URIUtils::Split(items[y]->GetPath(), strPathY, strFileY);
+ //CLog::Log(LOGDEBUG," {}:{}:{}", y, strPathY, strFileY);
+
+ if (StringUtils::EqualsNoCase(strPathY, strPathX))
+ /*
+ remove everything sorted below the video_ts.ifo file in the same path.
+ understandably this wont stack correctly if there are other files in the the dvd folder.
+ this should be unlikely and thus is being ignored for now but we can monitor the
+ where the path changes and potentially remove the items above the video_ts.ifo file.
+ */
+ items.Remove(y);
+ else
+ break;
+ }
+ }
+ x++;
+ }
+
+ // enumerate
+ for (int i=0;i<items.Size();++i)
+ {
+ if (items[i]->m_bIsFolder)
+ continue;
+ std::string strPath = URIUtils::GetDirectory(items[i]->GetPath());
+ URIUtils::RemoveSlashAtEnd(strPath); // want no slash for the test that follows
+
+ if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strPath), "sample"))
+ continue;
+
+ // Discard all exclude files defined by regExExcludes
+ if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps))
+ continue;
+
+ /*
+ * Check if the media source has already set the season and episode or original air date in
+ * the VideoInfoTag. If it has, do not try to parse any of them from the file path to avoid
+ * any false positive matches.
+ */
+ if (ProcessItemByVideoInfoTag(items[i].get(), episodeList))
+ continue;
+
+ if (!EnumerateEpisodeItem(items[i].get(), episodeList))
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Could not enumerate file {}", CURL::GetRedacted(items[i]->GetPath()));
+ }
+ return true;
+ }
+
+ bool CVideoInfoScanner::ProcessItemByVideoInfoTag(const CFileItem *item, EPISODELIST &episodeList)
+ {
+ if (!item->HasVideoInfoTag())
+ return false;
+
+ const CVideoInfoTag* tag = item->GetVideoInfoTag();
+ bool isValid = false;
+ /*
+ * First check the season and episode number. This takes precedence over the original air
+ * date and episode title. Must be a valid season and episode number combination.
+ */
+ if (tag->m_iSeason > -1 && tag->m_iEpisode > 0)
+ isValid = true;
+
+ // episode 0 with non-zero season is valid! (e.g. prequel episode)
+ if (item->IsPlugin() && tag->m_iSeason > 0 && tag->m_iEpisode >= 0)
+ isValid = true;
+
+ if (isValid)
+ {
+ EPISODE episode;
+ episode.strPath = item->GetPath();
+ episode.iSeason = tag->m_iSeason;
+ episode.iEpisode = tag->m_iEpisode;
+ episode.isFolder = false;
+ // save full item for plugin source
+ if (item->IsPlugin())
+ episode.item = std::make_shared<CFileItem>(*item);
+ episodeList.push_back(episode);
+ CLog::Log(LOGDEBUG, "{} - found match for: {}. Season {}, Episode {}", __FUNCTION__,
+ CURL::GetRedacted(episode.strPath), episode.iSeason, episode.iEpisode);
+ return true;
+ }
+
+ /*
+ * Next preference is the first aired date. If it exists use that for matching the TV Show
+ * information. Also set the title in case there are multiple matches for the first aired date.
+ */
+ if (tag->m_firstAired.IsValid())
+ {
+ EPISODE episode;
+ episode.strPath = item->GetPath();
+ episode.strTitle = tag->m_strTitle;
+ episode.isFolder = false;
+ /*
+ * Set season and episode to -1 to indicate to use the aired date.
+ */
+ episode.iSeason = -1;
+ episode.iEpisode = -1;
+ /*
+ * The first aired date string must be parseable.
+ */
+ episode.cDate = item->GetVideoInfoTag()->m_firstAired;
+ episodeList.push_back(episode);
+ CLog::Log(LOGDEBUG, "{} - found match for: '{}', firstAired: '{}' = '{}', title: '{}'",
+ __FUNCTION__, CURL::GetRedacted(episode.strPath),
+ tag->m_firstAired.GetAsDBDateTime(), episode.cDate.GetAsLocalizedDate(),
+ episode.strTitle);
+ return true;
+ }
+
+ /*
+ * Next preference is the episode title. If it exists use that for matching the TV Show
+ * information.
+ */
+ if (!tag->m_strTitle.empty())
+ {
+ EPISODE episode;
+ episode.strPath = item->GetPath();
+ episode.strTitle = tag->m_strTitle;
+ episode.isFolder = false;
+ /*
+ * Set season and episode to -1 to indicate to use the title.
+ */
+ episode.iSeason = -1;
+ episode.iEpisode = -1;
+ episodeList.push_back(episode);
+ CLog::Log(LOGDEBUG, "{} - found match for: '{}', title: '{}'", __FUNCTION__,
+ CURL::GetRedacted(episode.strPath), episode.strTitle);
+ return true;
+ }
+
+ /*
+ * There is no further episode information available if both the season and episode number have
+ * been set to 0. Return the match as true so no further matching is attempted, but don't add it
+ * to the episode list.
+ */
+ if (tag->m_iSeason == 0 && tag->m_iEpisode == 0)
+ {
+ CLog::Log(LOGDEBUG,
+ "{} - found exclusion match for: {}. Both Season and Episode are 0. Item will be "
+ "ignored for scanning.",
+ __FUNCTION__, CURL::GetRedacted(item->GetPath()));
+ return true;
+ }
+
+ return false;
+ }
+
+ bool CVideoInfoScanner::EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList)
+ {
+ SETTINGS_TVSHOWLIST expression = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowEnumRegExps;
+
+ std::string strLabel;
+
+ // remove path to main file if it's a bd or dvd folder to regex the right (folder) name
+ if (item->IsOpticalMediaFile())
+ {
+ strLabel = item->GetLocalMetadataPath();
+ URIUtils::RemoveSlashAtEnd(strLabel);
+ }
+ else
+ strLabel = item->GetPath();
+
+ // URLDecode in case an episode is on a http/https/dav/davs:// source and URL-encoded like foo%201x01%20bar.avi
+ strLabel = CURL::Decode(CURL::GetRedacted(strLabel));
+
+ for (unsigned int i=0;i<expression.size();++i)
+ {
+ CRegExp reg(true, CRegExp::autoUtf8);
+ if (!reg.RegComp(expression[i].regexp))
+ continue;
+
+ int regexppos, regexp2pos;
+ //CLog::Log(LOGDEBUG,"running expression {} on {}",expression[i].regexp,strLabel);
+ if ((regexppos = reg.RegFind(strLabel.c_str())) < 0)
+ continue;
+
+ EPISODE episode;
+ episode.strPath = item->GetPath();
+ episode.iSeason = -1;
+ episode.iEpisode = -1;
+ episode.cDate.SetValid(false);
+ episode.isFolder = false;
+
+ bool byDate = expression[i].byDate ? true : false;
+ bool byTitle = expression[i].byTitle;
+ int defaultSeason = expression[i].defaultSeason;
+
+ if (byDate)
+ {
+ if (!GetAirDateFromRegExp(reg, episode))
+ continue;
+
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Found date based match {} ({}) [{}]",
+ CURL::GetRedacted(episode.strPath), episode.cDate.GetAsLocalizedDate(),
+ expression[i].regexp);
+ }
+ else if (byTitle)
+ {
+ if (!GetEpisodeTitleFromRegExp(reg, episode))
+ continue;
+
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Found title based match {} ({}) [{}]",
+ CURL::GetRedacted(episode.strPath), episode.strTitle, expression[i].regexp);
+ }
+ else
+ {
+ if (!GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason))
+ continue;
+
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Found episode match {} (s{}e{}) [{}]",
+ CURL::GetRedacted(episode.strPath), episode.iSeason, episode.iEpisode,
+ expression[i].regexp);
+ }
+
+ // Grab the remainder from first regexp run
+ // as second run might modify or empty it.
+ std::string remainder(reg.GetMatch(3));
+
+ /*
+ * Check if the files base path is a dedicated folder that contains
+ * only this single episode. If season and episode match with the
+ * actual media file, we set episode.isFolder to true.
+ */
+ std::string strBasePath = item->GetBaseMoviePath(true);
+ URIUtils::RemoveSlashAtEnd(strBasePath);
+ strBasePath = URIUtils::GetFileName(strBasePath);
+
+ if (reg.RegFind(strBasePath.c_str()) > -1)
+ {
+ EPISODE parent;
+ if (byDate)
+ {
+ GetAirDateFromRegExp(reg, parent);
+ if (episode.cDate == parent.cDate)
+ episode.isFolder = true;
+ }
+ else
+ {
+ GetEpisodeAndSeasonFromRegExp(reg, parent, defaultSeason);
+ if (episode.iSeason == parent.iSeason && episode.iEpisode == parent.iEpisode)
+ episode.isFolder = true;
+ }
+ }
+
+ // add what we found by now
+ episodeList.push_back(episode);
+
+ CRegExp reg2(true, CRegExp::autoUtf8);
+ // check the remainder of the string for any further episodes.
+ if (!byDate && reg2.RegComp(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_tvshowMultiPartEnumRegExp))
+ {
+ int offset = 0;
+
+ // we want "long circuit" OR below so that both offsets are evaluated
+ while (static_cast<int>((regexp2pos = reg2.RegFind(remainder.c_str() + offset)) > -1) |
+ static_cast<int>((regexppos = reg.RegFind(remainder.c_str() + offset)) > -1))
+ {
+ if (((regexppos <= regexp2pos) && regexppos != -1) ||
+ (regexppos >= 0 && regexp2pos == -1))
+ {
+ GetEpisodeAndSeasonFromRegExp(reg, episode, defaultSeason);
+
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new season {}, multipart episode {} [{}]",
+ episode.iSeason, episode.iEpisode,
+ CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_tvshowMultiPartEnumRegExp);
+
+ episodeList.push_back(episode);
+ remainder = reg.GetMatch(3);
+ offset = 0;
+ }
+ else if (((regexp2pos < regexppos) && regexp2pos != -1) ||
+ (regexp2pos >= 0 && regexppos == -1))
+ {
+ episode.iEpisode = atoi(reg2.GetMatch(1).c_str());
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding multipart episode {} [{}]",
+ episode.iEpisode,
+ CServiceBroker::GetSettingsComponent()
+ ->GetAdvancedSettings()
+ ->m_tvshowMultiPartEnumRegExp);
+ episodeList.push_back(episode);
+ offset += regexp2pos + reg2.GetFindLen();
+ }
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ bool CVideoInfoScanner::GetEpisodeAndSeasonFromRegExp(CRegExp &reg, EPISODE &episodeInfo, int defaultSeason)
+ {
+ std::string season(reg.GetMatch(1));
+ std::string episode(reg.GetMatch(2));
+
+ if (!season.empty() || !episode.empty())
+ {
+ char* endptr = NULL;
+ if (season.empty() && !episode.empty())
+ { // no season specified -> assume defaultSeason
+ episodeInfo.iSeason = defaultSeason;
+ if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(episode.c_str())) == -1)
+ episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
+ }
+ else if (!season.empty() && episode.empty())
+ { // no episode specification -> assume defaultSeason
+ episodeInfo.iSeason = defaultSeason;
+ if ((episodeInfo.iEpisode = CUtil::TranslateRomanNumeral(season.c_str())) == -1)
+ episodeInfo.iEpisode = atoi(season.c_str());
+ }
+ else
+ { // season and episode specified
+ episodeInfo.iSeason = atoi(season.c_str());
+ episodeInfo.iEpisode = strtol(episode.c_str(), &endptr, 10);
+ }
+ if (endptr)
+ {
+ if (isalpha(*endptr))
+ episodeInfo.iSubepisode = *endptr - (islower(*endptr) ? 'a' : 'A') + 1;
+ else if (*endptr == '.')
+ episodeInfo.iSubepisode = atoi(endptr+1);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ bool CVideoInfoScanner::GetAirDateFromRegExp(CRegExp &reg, EPISODE &episodeInfo)
+ {
+ std::string param1(reg.GetMatch(1));
+ std::string param2(reg.GetMatch(2));
+ std::string param3(reg.GetMatch(3));
+
+ if (!param1.empty() && !param2.empty() && !param3.empty())
+ {
+ // regular expression by date
+ int len1 = param1.size();
+ int len2 = param2.size();
+ int len3 = param3.size();
+
+ if (len1==4 && len2==2 && len3==2)
+ {
+ // yyyy mm dd format
+ episodeInfo.cDate.SetDate(atoi(param1.c_str()), atoi(param2.c_str()), atoi(param3.c_str()));
+ }
+ else if (len1==2 && len2==2 && len3==4)
+ {
+ // mm dd yyyy format
+ episodeInfo.cDate.SetDate(atoi(param3.c_str()), atoi(param1.c_str()), atoi(param2.c_str()));
+ }
+ }
+ return episodeInfo.cDate.IsValid();
+ }
+
+ bool CVideoInfoScanner::GetEpisodeTitleFromRegExp(CRegExp& reg, EPISODE& episodeInfo)
+ {
+ std::string param1(reg.GetMatch(1));
+
+ if (!param1.empty())
+ {
+ episodeInfo.strTitle = param1;
+ return true;
+ }
+ return false;
+ }
+
+ long CVideoInfoScanner::AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder /* = false */, bool useLocal /* = true */, const CVideoInfoTag *showInfo /* = NULL */, bool libraryImport /* = false */)
+ {
+ // ensure our database is open (this can get called via other classes)
+ if (!m_database.Open())
+ return -1;
+
+ if (!libraryImport)
+ GetArtwork(pItem, content, videoFolder, useLocal && !pItem->IsPlugin(), showInfo ? showInfo->m_strPath : "");
+
+ // ensure the art map isn't completely empty by specifying an empty thumb
+ std::map<std::string, std::string> art = pItem->GetArt();
+ if (art.empty())
+ art["thumb"] = "";
+
+ CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
+ if (movieDetails.m_basePath.empty())
+ movieDetails.m_basePath = pItem->GetBaseMoviePath(videoFolder);
+ movieDetails.m_parentPathID = m_database.AddPath(URIUtils::GetParentPath(movieDetails.m_basePath));
+
+ movieDetails.m_strFileNameAndPath = pItem->GetPath();
+
+ if (pItem->m_bIsFolder)
+ movieDetails.m_strPath = pItem->GetPath();
+
+ std::string strTitle(movieDetails.m_strTitle);
+
+ if (showInfo && content == CONTENT_TVSHOWS)
+ {
+ strTitle = StringUtils::Format("{} - {}x{} - {}", showInfo->m_strTitle,
+ movieDetails.m_iSeason, movieDetails.m_iEpisode, strTitle);
+ }
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS) &&
+ CDVDFileInfo::GetFileStreamDetails(pItem))
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Extracted filestream details from video file {}",
+ CURL::GetRedacted(pItem->GetPath()));
+
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Adding new item to {}:{}", TranslateContent(content), CURL::GetRedacted(pItem->GetPath()));
+ long lResult = -1;
+
+ if (content == CONTENT_MOVIES)
+ {
+ // find local trailer first
+ std::string strTrailer = pItem->FindTrailer();
+ if (!strTrailer.empty())
+ movieDetails.m_strTrailer = strTrailer;
+
+ lResult = m_database.SetDetailsForMovie(movieDetails, art);
+ movieDetails.m_iDbId = lResult;
+ movieDetails.m_type = MediaTypeMovie;
+
+ // setup links to shows if the linked shows are in the db
+ for (unsigned int i=0; i < movieDetails.m_showLink.size(); ++i)
+ {
+ CFileItemList items;
+ m_database.GetTvShowsByName(movieDetails.m_showLink[i], items);
+ if (items.Size())
+ m_database.LinkMovieToTvshow(lResult, items[0]->GetVideoInfoTag()->m_iDbId, false);
+ else
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Failed to link movie {} to show {}",
+ movieDetails.m_strTitle, movieDetails.m_showLink[i]);
+ }
+ }
+ else if (content == CONTENT_TVSHOWS)
+ {
+ if (pItem->m_bIsFolder)
+ {
+ /*
+ multipaths are not stored in the database, so in the case we have one,
+ we split the paths, and compute the parent paths in each case.
+ */
+ std::vector<std::string> multipath;
+ if (!URIUtils::IsMultiPath(pItem->GetPath()) || !CMultiPathDirectory::GetPaths(pItem->GetPath(), multipath))
+ multipath.push_back(pItem->GetPath());
+ std::vector<std::pair<std::string, std::string> > paths;
+ for (std::vector<std::string>::const_iterator i = multipath.begin(); i != multipath.end(); ++i)
+ paths.emplace_back(*i, URIUtils::GetParentPath(*i));
+
+ std::map<int, std::map<std::string, std::string> > seasonArt;
+
+ if (!libraryImport)
+ GetSeasonThumbs(movieDetails, seasonArt, CVideoThumbLoader::GetArtTypes(MediaTypeSeason), useLocal && !pItem->IsPlugin());
+
+ lResult = m_database.SetDetailsForTvShow(paths, movieDetails, art, seasonArt);
+ movieDetails.m_iDbId = lResult;
+ movieDetails.m_type = MediaTypeTvShow;
+ }
+ else
+ {
+ // we add episode then set details, as otherwise set details will delete the
+ // episode then add, which breaks multi-episode files.
+ int idShow = showInfo ? showInfo->m_iDbId : -1;
+ int idEpisode = m_database.AddNewEpisode(idShow, movieDetails);
+ lResult = m_database.SetDetailsForEpisode(movieDetails, art, idShow, idEpisode);
+ movieDetails.m_iDbId = lResult;
+ movieDetails.m_type = MediaTypeEpisode;
+ movieDetails.m_strShowTitle = showInfo ? showInfo->m_strTitle : "";
+ if (movieDetails.m_EpBookmark.timeInSeconds > 0)
+ {
+ movieDetails.m_strFileNameAndPath = pItem->GetPath();
+ movieDetails.m_EpBookmark.seasonNumber = movieDetails.m_iSeason;
+ movieDetails.m_EpBookmark.episodeNumber = movieDetails.m_iEpisode;
+ m_database.AddBookMarkForEpisode(movieDetails, movieDetails.m_EpBookmark);
+ }
+ }
+ }
+ else if (content == CONTENT_MUSICVIDEOS)
+ {
+ lResult = m_database.SetDetailsForMusicVideo(movieDetails, art);
+ movieDetails.m_iDbId = lResult;
+ movieDetails.m_type = MediaTypeMusicVideo;
+ }
+
+ if (!pItem->m_bIsFolder)
+ {
+ const auto advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+ if ((libraryImport || advancedSettings->m_bVideoLibraryImportWatchedState) &&
+ (movieDetails.IsPlayCountSet() || movieDetails.m_lastPlayed.IsValid()))
+ m_database.SetPlayCount(*pItem, movieDetails.GetPlayCount(), movieDetails.m_lastPlayed);
+
+ if ((libraryImport || advancedSettings->m_bVideoLibraryImportResumePoint) &&
+ movieDetails.GetResumePoint().IsSet())
+ m_database.AddBookMarkToFile(pItem->GetPath(), movieDetails.GetResumePoint(), CBookmark::RESUME);
+ }
+
+ m_database.Close();
+
+ CFileItemPtr itemCopy = CFileItemPtr(new CFileItem(*pItem));
+ CVariant data;
+ data["added"] = true;
+ if (m_bRunning)
+ data["transaction"] = true;
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::VideoLibrary, "OnUpdate",
+ itemCopy, data);
+ return lResult;
+ }
+
+ std::string ContentToMediaType(CONTENT_TYPE content, bool folder)
+ {
+ switch (content)
+ {
+ case CONTENT_MOVIES:
+ return MediaTypeMovie;
+ case CONTENT_MUSICVIDEOS:
+ return MediaTypeMusicVideo;
+ case CONTENT_TVSHOWS:
+ return folder ? MediaTypeTvShow : MediaTypeEpisode;
+ default:
+ return "";
+ }
+ }
+
+ std::string CVideoInfoScanner::GetArtTypeFromSize(unsigned int width, unsigned int height)
+ {
+ std::string type = "thumb";
+ if (width*5 < height*4)
+ type = "poster";
+ else if (width*1 > height*4)
+ type = "banner";
+ return type;
+ }
+
+ std::string CVideoInfoScanner::GetMovieSetInfoFolder(const std::string& setTitle)
+ {
+ if (setTitle.empty())
+ return "";
+ std::string path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER);
+ if (path.empty())
+ return "";
+ path = URIUtils::AddFileToFolder(path, CUtil::MakeLegalFileName(setTitle, LEGAL_WIN32_COMPAT));
+ URIUtils::AddSlashAtEnd(path);
+ CLog::Log(LOGDEBUG,
+ "VideoInfoScanner: Looking for local artwork for movie set '{}' in folder '{}'",
+ setTitle,
+ CURL::GetRedacted(path));
+ return CDirectory::Exists(path) ? path : "";
+ }
+
+ void CVideoInfoScanner::AddLocalItemArtwork(CGUIListItem::ArtMap& itemArt,
+ const std::vector<std::string>& wantedArtTypes, const std::string& itemPath,
+ bool addAll, bool exactName)
+ {
+ std::string path = URIUtils::GetDirectory(itemPath);
+ if (path.empty())
+ return;
+
+ CFileItemList availableArtFiles;
+ CDirectory::GetDirectory(path, availableArtFiles,
+ CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
+ DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
+
+ std::string baseFilename = URIUtils::GetFileName(itemPath);
+ if (!baseFilename.empty())
+ {
+ URIUtils::RemoveExtension(baseFilename);
+ baseFilename.append("-");
+ }
+
+ for (const auto& artFile : availableArtFiles)
+ {
+ std::string candidate = URIUtils::GetFileName(artFile->GetPath());
+
+ bool matchesFilename =
+ !baseFilename.empty() && StringUtils::StartsWith(candidate, baseFilename);
+ if (!baseFilename.empty() && !matchesFilename)
+ continue;
+
+ if (matchesFilename)
+ candidate.erase(0, baseFilename.length());
+ URIUtils::RemoveExtension(candidate);
+ StringUtils::ToLower(candidate);
+
+ // move 'folder' to thumb / poster / banner based on aspect ratio
+ // if such artwork doesn't already exist
+ if (!matchesFilename && StringUtils::EqualsNoCase(candidate, "folder") &&
+ !CVideoThumbLoader::IsArtTypeInWhitelist("folder", wantedArtTypes, exactName))
+ {
+ // cache the image to determine sizing
+ CTextureDetails details;
+ if (CServiceBroker::GetTextureCache()->CacheImage(artFile->GetPath(), details))
+ {
+ candidate = GetArtTypeFromSize(details.width, details.height);
+ if (itemArt.find(candidate) != itemArt.end())
+ continue;
+ }
+ }
+
+ if ((addAll && CVideoThumbLoader::IsValidArtType(candidate)) ||
+ CVideoThumbLoader::IsArtTypeInWhitelist(candidate, wantedArtTypes, exactName))
+ {
+ itemArt[candidate] = artFile->GetPath();
+ }
+ }
+ }
+
+ void CVideoInfoScanner::GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir, bool useLocal, const std::string &actorArtPath)
+ {
+ int artLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL);
+ if (artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE)
+ return;
+
+ CVideoInfoTag &movieDetails = *pItem->GetVideoInfoTag();
+ movieDetails.m_fanart.Unpack();
+ movieDetails.m_strPictureURL.Parse();
+
+ CGUIListItem::ArtMap art = pItem->GetArt();
+
+ // get and cache thumb images
+ std::string mediaType = ContentToMediaType(content, pItem->m_bIsFolder);
+ std::vector<std::string> artTypes = CVideoThumbLoader::GetArtTypes(mediaType);
+ bool moviePartOfSet = content == CONTENT_MOVIES && !movieDetails.m_set.title.empty();
+ std::vector<std::string> movieSetArtTypes;
+ if (moviePartOfSet)
+ {
+ movieSetArtTypes = CVideoThumbLoader::GetArtTypes(MediaTypeVideoCollection);
+ for (const std::string& artType : movieSetArtTypes)
+ artTypes.push_back("set." + artType);
+ }
+ bool addAll = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL;
+ bool exactName = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC;
+ // find local art
+ if (useLocal)
+ {
+ if (!pItem->SkipLocalArt())
+ {
+ if (bApplyToDir && (content == CONTENT_MOVIES || content == CONTENT_MUSICVIDEOS))
+ {
+ std::string filename = pItem->GetLocalArtBaseFilename();
+ std::string directory = URIUtils::GetDirectory(filename);
+ if (filename != directory)
+ AddLocalItemArtwork(art, artTypes, directory, addAll, exactName);
+ }
+ AddLocalItemArtwork(art, artTypes, pItem->GetLocalArtBaseFilename(), addAll, exactName);
+ }
+
+ if (moviePartOfSet)
+ {
+ std::string movieSetInfoPath = GetMovieSetInfoFolder(movieDetails.m_set.title);
+ if (!movieSetInfoPath.empty())
+ {
+ CGUIListItem::ArtMap movieSetArt;
+ AddLocalItemArtwork(movieSetArt, movieSetArtTypes, movieSetInfoPath, addAll, exactName);
+ for (const auto& artItem : movieSetArt)
+ {
+ art["set." + artItem.first] = artItem.second;
+ }
+ }
+ }
+ }
+
+ // find embedded art
+ if (pItem->HasVideoInfoTag() && !pItem->GetVideoInfoTag()->m_coverArt.empty())
+ {
+ for (auto& it : pItem->GetVideoInfoTag()->m_coverArt)
+ {
+ if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(it.m_type, artTypes, exactName)) &&
+ art.find(it.m_type) == art.end())
+ {
+ std::string thumb = CTextureUtils::GetWrappedImageURL(pItem->GetPath(),
+ "video_" + it.m_type);
+ art.insert(std::make_pair(it.m_type, thumb));
+ }
+ }
+ }
+
+ // add online fanart (treated separately due to it being stored in m_fanart)
+ if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist("fanart", artTypes, exactName)) &&
+ art.find("fanart") == art.end())
+ {
+ std::string fanart = pItem->GetVideoInfoTag()->m_fanart.GetImageURL();
+ if (!fanart.empty())
+ art.insert(std::make_pair("fanart", fanart));
+ }
+
+ // add online art
+ for (const auto& url : pItem->GetVideoInfoTag()->m_strPictureURL.GetUrls())
+ {
+ if (url.m_type != CScraperUrl::UrlType::General)
+ continue;
+ std::string aspect = url.m_aspect;
+ if (aspect.empty())
+ // Backward compatibility with Kodi 11 Eden NFO files
+ aspect = mediaType == MediaTypeEpisode ? "thumb" : "poster";
+
+ if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(aspect, artTypes, exactName)) &&
+ art.find(aspect) == art.end())
+ {
+ std::string image = GetImage(url, pItem->GetPath());
+ if (!image.empty())
+ art.insert(std::make_pair(aspect, image));
+ }
+ }
+
+ for (const auto& artType : artTypes)
+ {
+ if (art.find(artType) != art.end())
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(art[artType]);
+ }
+
+ pItem->SetArt(art);
+
+ // parent folder to apply the thumb to and to search for local actor thumbs
+ std::string parentDir = URIUtils::GetBasePath(pItem->GetPath());
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_ACTORTHUMBS))
+ FetchActorThumbs(movieDetails.m_cast, actorArtPath.empty() ? parentDir : actorArtPath);
+ if (bApplyToDir)
+ ApplyThumbToFolder(parentDir, art["thumb"]);
+ }
+
+ std::string CVideoInfoScanner::GetImage(const CScraperUrl::SUrlEntry &image, const std::string& itemPath)
+ {
+ std::string thumb = CScraperUrl::GetThumbUrl(image);
+ if (!thumb.empty() && thumb.find('/') == std::string::npos &&
+ thumb.find('\\') == std::string::npos)
+ {
+ std::string strPath = URIUtils::GetDirectory(itemPath);
+ thumb = URIUtils::AddFileToFolder(strPath, thumb);
+ }
+ return thumb;
+ }
+
+ CInfoScanner::INFO_RET
+ CVideoInfoScanner::OnProcessSeriesFolder(EPISODELIST& files,
+ const ADDON::ScraperPtr &scraper,
+ bool useLocal,
+ const CVideoInfoTag& showInfo,
+ CGUIDialogProgress* pDlgProgress /* = NULL */)
+ {
+ if (pDlgProgress)
+ {
+ pDlgProgress->SetLine(1, CVariant{showInfo.m_strTitle});
+ pDlgProgress->SetLine(2, CVariant{20361});
+ pDlgProgress->SetPercentage(0);
+ pDlgProgress->ShowProgressBar(true);
+ pDlgProgress->Progress();
+ }
+
+ EPISODELIST episodes;
+ bool hasEpisodeGuide = false;
+
+ int iMax = files.size();
+ int iCurr = 1;
+ for (EPISODELIST::iterator file = files.begin(); file != files.end(); ++file)
+ {
+ if (pDlgProgress)
+ {
+ pDlgProgress->SetLine(2, CVariant{20361});
+ pDlgProgress->SetPercentage((int)((float)(iCurr++)/iMax*100));
+ pDlgProgress->Progress();
+ }
+ if (m_handle)
+ m_handle->SetPercentage(100.f*iCurr++/iMax);
+
+ if ((pDlgProgress && pDlgProgress->IsCanceled()) || m_bStop)
+ return INFO_CANCELLED;
+
+ if (m_database.GetEpisodeId(file->strPath, file->iEpisode, file->iSeason) > -1)
+ {
+ if (m_handle)
+ m_handle->SetText(g_localizeStrings.Get(20415));
+ continue;
+ }
+
+ CFileItem item;
+ if (file->item)
+ item = *file->item;
+ else
+ {
+ item.SetPath(file->strPath);
+ item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
+ }
+
+ // handle .nfo files
+ CInfoScanner::INFO_TYPE result=CInfoScanner::NO_NFO;
+ CScraperUrl scrUrl;
+ const ScraperPtr& info(scraper);
+ std::unique_ptr<IVideoInfoTagLoader> loader;
+ if (useLocal)
+ {
+ loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(item, info, false));
+ if (loader)
+ {
+ // no reset here on purpose
+ result = loader->Load(*item.GetVideoInfoTag(), false);
+ }
+ }
+ if (result == CInfoScanner::FULL_NFO)
+ {
+ // override with episode and season number from file if available
+ if (file->iEpisode > -1)
+ {
+ item.GetVideoInfoTag()->m_iEpisode = file->iEpisode;
+ item.GetVideoInfoTag()->m_iSeason = file->iSeason;
+ }
+ if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, true, &showInfo) < 0)
+ return INFO_ERROR;
+ continue;
+ }
+
+ if (!hasEpisodeGuide)
+ {
+ // fetch episode guide
+ if (!showInfo.m_strEpisodeGuide.empty())
+ {
+ CScraperUrl url;
+ url.ParseAndAppendUrlsFromEpisodeGuide(showInfo.m_strEpisodeGuide);
+
+ if (pDlgProgress)
+ {
+ pDlgProgress->SetLine(2, CVariant{20354});
+ pDlgProgress->Progress();
+ }
+
+ CVideoInfoDownloader imdb(scraper);
+ if (!imdb.GetEpisodeList(url, episodes))
+ return INFO_NOT_FOUND;
+
+ hasEpisodeGuide = true;
+ }
+ }
+
+ if (episodes.empty())
+ {
+ CLog::Log(LOGERROR,
+ "VideoInfoScanner: Asked to lookup episode {}"
+ " online, but we have no episode guide. Check your tvshow.nfo and make"
+ " sure the <episodeguide> tag is in place.",
+ CURL::GetRedacted(file->strPath));
+ continue;
+ }
+
+ EPISODE key(file->iSeason, file->iEpisode, file->iSubepisode);
+ EPISODE backupkey(file->iSeason, file->iEpisode, 0);
+ bool bFound = false;
+ EPISODELIST::iterator guide = episodes.begin();
+ EPISODELIST matches;
+
+ for (; guide != episodes.end(); ++guide )
+ {
+ if ((file->iEpisode!=-1) && (file->iSeason!=-1))
+ {
+ if (key==*guide)
+ {
+ bFound = true;
+ break;
+ }
+ else if ((file->iSubepisode!=0) && (backupkey==*guide))
+ {
+ matches.push_back(*guide);
+ continue;
+ }
+ }
+ if (file->cDate.IsValid() && guide->cDate.IsValid() && file->cDate==guide->cDate)
+ {
+ matches.push_back(*guide);
+ continue;
+ }
+ if (!guide->cScraperUrl.GetTitle().empty() &&
+ StringUtils::EqualsNoCase(guide->cScraperUrl.GetTitle(), file->strTitle))
+ {
+ bFound = true;
+ break;
+ }
+ if (!guide->strTitle.empty() && StringUtils::EqualsNoCase(guide->strTitle, file->strTitle))
+ {
+ bFound = true;
+ break;
+ }
+ }
+
+ if (!bFound)
+ {
+ /*
+ * If there is only one match or there are matches but no title to compare with to help
+ * identify the best match, then pick the first match as the best possible candidate.
+ *
+ * Otherwise, use the title to further refine the best match.
+ */
+ if (matches.size() == 1 || (file->strTitle.empty() && matches.size() > 1))
+ {
+ guide = matches.begin();
+ bFound = true;
+ }
+ else if (!file->strTitle.empty())
+ {
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: analyzing parsed title '{}'", file->strTitle);
+ double minscore = 0; // Default minimum score is 0 to find whatever is the best match.
+
+ EPISODELIST *candidates;
+ if (matches.empty()) // No matches found using earlier criteria. Use fuzzy match on titles across all episodes.
+ {
+ minscore = 0.8; // 80% should ensure a good match.
+ candidates = &episodes;
+ }
+ else // Multiple matches found. Use fuzzy match on the title with already matched episodes to pick the best.
+ candidates = &matches;
+
+ std::vector<std::string> titles;
+ for (guide = candidates->begin(); guide != candidates->end(); ++guide)
+ {
+ auto title = guide->cScraperUrl.GetTitle();
+ if (title.empty())
+ {
+ title = guide->strTitle;
+ }
+ StringUtils::ToLower(title);
+ guide->cScraperUrl.SetTitle(title);
+ titles.push_back(title);
+ }
+
+ double matchscore;
+ std::string loweredTitle(file->strTitle);
+ StringUtils::ToLower(loweredTitle);
+ int index = StringUtils::FindBestMatch(loweredTitle, titles, matchscore);
+ if (index >= 0 && matchscore >= minscore)
+ {
+ guide = candidates->begin() + index;
+ bFound = true;
+ CLog::Log(LOGDEBUG,
+ "{} fuzzy title match for show: '{}', title: '{}', match: '{}', score: {:f} "
+ ">= {:f}",
+ __FUNCTION__, showInfo.m_strTitle, file->strTitle, titles[index], matchscore,
+ minscore);
+ }
+ }
+ }
+
+ if (bFound)
+ {
+ CVideoInfoDownloader imdb(scraper);
+ CFileItem item;
+ item.SetPath(file->strPath);
+ if (!imdb.GetEpisodeDetails(guide->cScraperUrl, *item.GetVideoInfoTag(), pDlgProgress))
+ return INFO_NOT_FOUND; //! @todo should we just skip to the next episode?
+
+ // Only set season/epnum from filename when it is not already set by a scraper
+ if (item.GetVideoInfoTag()->m_iSeason == -1)
+ item.GetVideoInfoTag()->m_iSeason = guide->iSeason;
+ if (item.GetVideoInfoTag()->m_iEpisode == -1)
+ item.GetVideoInfoTag()->m_iEpisode = guide->iEpisode;
+
+ if (AddVideo(&item, CONTENT_TVSHOWS, file->isFolder, useLocal, &showInfo) < 0)
+ return INFO_ERROR;
+ }
+ else
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "{} - no match for show: '{}', season: {}, episode: {}.{}, airdate: '{}', title: '{}'",
+ __FUNCTION__, showInfo.m_strTitle, file->iSeason, file->iEpisode, file->iSubepisode,
+ file->cDate.GetAsLocalizedDate(), file->strTitle);
+ }
+ }
+ return INFO_ADDED;
+ }
+
+ bool CVideoInfoScanner::GetDetails(CFileItem *pItem, CScraperUrl &url,
+ const ScraperPtr& scraper,
+ IVideoInfoTagLoader* loader,
+ CGUIDialogProgress* pDialog /* = NULL */)
+ {
+ CVideoInfoTag movieDetails;
+
+ if (m_handle && !url.GetTitle().empty())
+ m_handle->SetText(url.GetTitle());
+
+ CVideoInfoDownloader imdb(scraper);
+ bool ret = imdb.GetDetails(url, movieDetails, pDialog);
+
+ if (ret)
+ {
+ if (loader)
+ loader->Load(movieDetails, true);
+
+ if (m_handle && url.GetTitle().empty())
+ m_handle->SetText(movieDetails.m_strTitle);
+
+ if (pDialog)
+ {
+ pDialog->SetLine(1, CVariant{movieDetails.m_strTitle});
+ pDialog->Progress();
+ }
+
+ *pItem->GetVideoInfoTag() = movieDetails;
+ return true;
+ }
+ return false; // no info found, or cancelled
+ }
+
+ void CVideoInfoScanner::ApplyThumbToFolder(const std::string &folder, const std::string &imdbThumb)
+ {
+ // copy icon to folder also;
+ if (!imdbThumb.empty())
+ {
+ CFileItem folderItem(folder, true);
+ CThumbLoader loader;
+ loader.SetCachedImage(folderItem, "thumb", imdbThumb);
+ }
+ }
+
+ int CVideoInfoScanner::GetPathHash(const CFileItemList &items, std::string &hash)
+ {
+ // Create a hash based on the filenames, filesize and filedate. Also count the number of files
+ if (0 == items.Size()) return 0;
+ CDigest digest{CDigest::Type::MD5};
+ int count = 0;
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ const CFileItemPtr pItem = items[i];
+ digest.Update(pItem->GetPath());
+ if (pItem->IsPlugin())
+ {
+ // allow plugin to calculate hash itself using strings rather than binary data for size and date
+ // according to ListItem.setInfo() documentation date format should be "d.m.Y"
+ if (pItem->m_dwSize)
+ digest.Update(std::to_string(pItem->m_dwSize));
+ if (pItem->m_dateTime.IsValid())
+ digest.Update(StringUtils::Format("{:02}.{:02}.{:04}", pItem->m_dateTime.GetDay(),
+ pItem->m_dateTime.GetMonth(),
+ pItem->m_dateTime.GetYear()));
+ }
+ else
+ {
+ digest.Update(&pItem->m_dwSize, sizeof(pItem->m_dwSize));
+ KODI::TIME::FileTime time = pItem->m_dateTime;
+ digest.Update(&time, sizeof(KODI::TIME::FileTime));
+ }
+ if (pItem->IsVideo() && !pItem->IsPlayList() && !pItem->IsNFO())
+ count++;
+ }
+ hash = digest.Finalize();
+ return count;
+ }
+
+ bool CVideoInfoScanner::CanFastHash(const CFileItemList &items, const std::vector<std::string> &excludes) const
+ {
+ if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryUseFastHash || items.IsPlugin())
+ return false;
+
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ if (items[i]->m_bIsFolder && !CUtil::ExcludeFileOrFolder(items[i]->GetPath(), excludes))
+ return false;
+ }
+ return true;
+ }
+
+ std::string CVideoInfoScanner::GetFastHash(const std::string &directory,
+ const std::vector<std::string> &excludes) const
+ {
+ CDigest digest{CDigest::Type::MD5};
+
+ if (excludes.size())
+ digest.Update(StringUtils::Join(excludes, "|"));
+
+ struct __stat64 buffer;
+ if (XFILE::CFile::Stat(directory, &buffer) == 0)
+ {
+ int64_t time = buffer.st_mtime;
+ if (!time)
+ time = buffer.st_ctime;
+ if (time)
+ {
+ digest.Update((unsigned char *)&time, sizeof(time));
+ return digest.Finalize();
+ }
+ }
+ return "";
+ }
+
+ std::string CVideoInfoScanner::GetRecursiveFastHash(const std::string &directory,
+ const std::vector<std::string> &excludes) const
+ {
+ CFileItemList items;
+ items.Add(CFileItemPtr(new CFileItem(directory, true)));
+ CUtil::GetRecursiveDirsListing(directory, items, DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO);
+
+ CDigest digest{CDigest::Type::MD5};
+
+ if (excludes.size())
+ digest.Update(StringUtils::Join(excludes, "|"));
+
+ int64_t time = 0;
+ for (int i=0; i < items.Size(); ++i)
+ {
+ int64_t stat_time = 0;
+ struct __stat64 buffer;
+ if (XFILE::CFile::Stat(items[i]->GetPath(), &buffer) == 0)
+ {
+ //! @todo some filesystems may return the mtime/ctime inline, in which case this is
+ //! unnecessarily expensive. Consider supporting Stat() in our directory cache?
+ stat_time = buffer.st_mtime ? buffer.st_mtime : buffer.st_ctime;
+ time += stat_time;
+ }
+
+ if (!stat_time)
+ return "";
+ }
+
+ if (time)
+ {
+ digest.Update((unsigned char *)&time, sizeof(time));
+ return digest.Finalize();
+ }
+ return "";
+ }
+
+ void CVideoInfoScanner::GetSeasonThumbs(const CVideoInfoTag &show,
+ std::map<int, std::map<std::string, std::string>> &seasonArt, const std::vector<std::string> &artTypes, bool useLocal)
+ {
+ int artLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->
+ GetInt(CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL);
+ bool addAll = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_ALL;
+ bool exactName = artLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_BASIC;
+ if (useLocal)
+ {
+ // find the maximum number of seasons we have local thumbs for
+ int maxSeasons = 0;
+ CFileItemList items;
+ std::string extensions = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions();
+ if (!show.m_strPath.empty())
+ {
+ CDirectory::GetDirectory(show.m_strPath, items, extensions,
+ DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE |
+ DIR_FLAG_NO_FILE_INFO);
+ }
+ extensions.erase(std::remove(extensions.begin(), extensions.end(), '.'), extensions.end());
+ CRegExp reg;
+ if (items.Size() && reg.RegComp("season([0-9]+)(-[a-z0-9]+)?\\.(" + extensions + ")"))
+ {
+ for (const auto& item : items)
+ {
+ std::string name = URIUtils::GetFileName(item->GetPath());
+ if (reg.RegFind(name) > -1)
+ {
+ int season = atoi(reg.GetMatch(1).c_str());
+ if (season > maxSeasons)
+ maxSeasons = season;
+ }
+ }
+ }
+ for (int season = -1; season <= maxSeasons; season++)
+ {
+ // skip if we already have some art
+ std::map<int, std::map<std::string, std::string>>::const_iterator it = seasonArt.find(season);
+ if (it != seasonArt.end() && !it->second.empty())
+ continue;
+
+ std::map<std::string, std::string> art;
+ std::string basePath;
+ if (season == -1)
+ basePath = "season-all";
+ else if (season == 0)
+ basePath = "season-specials";
+ else
+ basePath = StringUtils::Format("season{:02}", season);
+
+ AddLocalItemArtwork(art, artTypes,
+ URIUtils::AddFileToFolder(show.m_strPath, basePath),
+ addAll, exactName);
+
+ seasonArt[season] = art;
+ }
+ }
+ // add online art
+ for (const auto& url : show.m_strPictureURL.GetUrls())
+ {
+ if (url.m_type != CScraperUrl::UrlType::Season)
+ continue;
+ std::string aspect = url.m_aspect;
+ if (aspect.empty())
+ aspect = "thumb";
+ std::map<std::string, std::string>& art = seasonArt[url.m_season];
+ if ((addAll || CVideoThumbLoader::IsArtTypeInWhitelist(aspect, artTypes, exactName)) &&
+ art.find(aspect) == art.end())
+ {
+ std::string image = CScraperUrl::GetThumbUrl(url);
+ if (!image.empty())
+ art.insert(std::make_pair(aspect, image));
+ }
+ }
+ }
+
+ void CVideoInfoScanner::FetchActorThumbs(std::vector<SActorInfo>& actors, const std::string& strPath)
+ {
+ CFileItemList items;
+ // don't try to fetch anything local with plugin source
+ if (!URIUtils::IsPlugin(strPath))
+ {
+ std::string actorsDir = URIUtils::AddFileToFolder(strPath, ".actors");
+ if (CDirectory::Exists(actorsDir))
+ CDirectory::GetDirectory(actorsDir, items, ".png|.jpg|.tbn", DIR_FLAG_NO_FILE_DIRS |
+ DIR_FLAG_NO_FILE_INFO);
+ }
+ for (std::vector<SActorInfo>::iterator i = actors.begin(); i != actors.end(); ++i)
+ {
+ if (i->thumb.empty())
+ {
+ std::string thumbFile = i->strName;
+ StringUtils::Replace(thumbFile, ' ', '_');
+ for (int j = 0; j < items.Size(); j++)
+ {
+ std::string compare = URIUtils::GetFileName(items[j]->GetPath());
+ URIUtils::RemoveExtension(compare);
+ if (!items[j]->m_bIsFolder && compare == thumbFile)
+ {
+ i->thumb = items[j]->GetPath();
+ break;
+ }
+ }
+ if (i->thumb.empty() && !i->thumbUrl.GetFirstUrlByType().m_url.empty())
+ i->thumb = CScraperUrl::GetThumbUrl(i->thumbUrl.GetFirstUrlByType());
+ if (!i->thumb.empty())
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(i->thumb);
+ }
+ }
+ }
+
+ bool CVideoInfoScanner::DownloadFailed(CGUIDialogProgress* pDialog)
+ {
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoScannerIgnoreErrors)
+ return true;
+
+ if (pDialog)
+ {
+ HELPERS::ShowOKDialogText(CVariant{20448}, CVariant{20449});
+ return false;
+ }
+ return HELPERS::ShowYesNoDialogText(CVariant{20448}, CVariant{20450}) ==
+ DialogResponse::CHOICE_YES;
+ }
+
+ bool CVideoInfoScanner::ProgressCancelled(CGUIDialogProgress* progress, int heading, const std::string &line1)
+ {
+ if (progress)
+ {
+ progress->SetHeading(CVariant{heading});
+ progress->SetLine(0, CVariant{line1});
+ progress->SetLine(2, CVariant{""});
+ progress->Progress();
+ return progress->IsCanceled();
+ }
+ return m_bStop;
+ }
+
+ int CVideoInfoScanner::FindVideo(const std::string &title, int year, const ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress)
+ {
+ MOVIELIST movielist;
+ CVideoInfoDownloader imdb(scraper);
+ int returncode = imdb.FindMovie(title, year, movielist, progress);
+ if (returncode < 0 || (returncode == 0 && (m_bStop || !DownloadFailed(progress))))
+ { // scraper reported an error, or we had an error and user wants to cancel the scan
+ m_bStop = true;
+ return -1; // cancelled
+ }
+ if (returncode > 0 && movielist.size())
+ {
+ url = movielist[0];
+ return 1; // found a movie
+ }
+ return 0; // didn't find anything
+ }
+
+}
diff --git a/xbmc/video/VideoInfoScanner.h b/xbmc/video/VideoInfoScanner.h
new file mode 100644
index 0000000..3bd2314
--- /dev/null
+++ b/xbmc/video/VideoInfoScanner.h
@@ -0,0 +1,260 @@
+/*
+ * 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 "InfoScanner.h"
+#include "VideoDatabase.h"
+#include "addons/Scraper.h"
+#include "guilib/GUIListItem.h"
+
+#include <set>
+#include <string>
+#include <vector>
+
+class CRegExp;
+class CFileItem;
+class CFileItemList;
+
+namespace VIDEO
+{
+ class IVideoInfoTagLoader;
+
+ typedef struct SScanSettings
+ {
+ SScanSettings()
+ {
+ parent_name = false;
+ parent_name_root = false;
+ noupdate = false;
+ exclude = false;
+ m_allExtAudio = false;
+ recurse = 1;
+ }
+ bool parent_name; /* use the parent dirname as name of lookup */
+ bool parent_name_root; /* use the name of directory where scan started as name for files in that dir */
+ int recurse; /* recurse into sub folders (indicate levels) */
+ bool noupdate; /* exclude from update library function */
+ bool exclude; /* exclude this path from scraping */
+ bool m_allExtAudio; /* treat all audio files in video directory as external tracks */
+ } SScanSettings;
+
+ class CVideoInfoScanner : public CInfoScanner
+ {
+ public:
+ CVideoInfoScanner();
+ ~CVideoInfoScanner() override;
+
+ /*! \brief Scan a folder using the background scanner
+ \param strDirectory path to scan
+ \param scanAll whether to scan everything not already scanned (regardless of whether the user normally doesn't want a folder scanned.) Defaults to false.
+ */
+ void Start(const std::string& strDirectory, bool scanAll = false);
+ void Stop();
+
+ /*! \brief Add an item to the database.
+ \param pItem item to add to the database.
+ \param content content type of the item.
+ \param videoFolder whether the video is represented by a folder (single movie per folder). Defaults to false.
+ \param useLocal whether to use local information for artwork etc.
+ \param showInfo pointer to CVideoInfoTag details for the show if this is an episode. Defaults to NULL.
+ \param libraryImport Whether this call belongs to a full library import or not. Defaults to false.
+ \return database id of the added item, or -1 on failure.
+ */
+ long AddVideo(CFileItem *pItem, const CONTENT_TYPE &content, bool videoFolder = false, bool useLocal = true, const CVideoInfoTag *showInfo = NULL, bool libraryImport = false);
+
+ /*! \brief Retrieve information for a list of items and add them to the database.
+ \param items list of items to retrieve info for.
+ \param bDirNames whether we should use folder or file names for lookups.
+ \param content type of content to retrieve.
+ \param useLocal should local data (.nfo and art) be used. Defaults to true.
+ \param pURL an optional URL to use to retrieve online info. Defaults to NULL.
+ \param fetchEpisodes whether we are fetching episodes with shows. Defaults to true.
+ \param pDlgProgress progress dialog to update and check for cancellation during processing. Defaults to NULL.
+ \return true if we successfully found information for some items, false otherwise
+ */
+ bool RetrieveVideoInfo(CFileItemList& items, bool bDirNames, CONTENT_TYPE content, bool useLocal = true, CScraperUrl *pURL = NULL, bool fetchEpisodes = true, CGUIDialogProgress* pDlgProgress = NULL);
+
+ static void ApplyThumbToFolder(const std::string &folder, const std::string &imdbThumb);
+ static bool DownloadFailed(CGUIDialogProgress* pDlgProgress);
+
+ /*! \brief Retrieve any artwork associated with an item
+ \param pItem item to find artwork for.
+ \param content content type of the item.
+ \param bApplyToDir whether we should apply any thumbs to a folder. Defaults to false.
+ \param useLocal whether we should use local thumbs. Defaults to true.
+ \param actorArtPath the path to search for actor thumbs. Defaults to empty.
+ */
+ void GetArtwork(CFileItem *pItem, const CONTENT_TYPE &content, bool bApplyToDir=false, bool useLocal=true, const std::string &actorArtPath = "");
+
+ /*! \brief Get season thumbs for a tvshow.
+ All seasons (regardless of whether the user has episodes) are added to the art map.
+ \param show tvshow info tag
+ \param art artwork map to which season thumbs are added.
+ \param useLocal whether to use local thumbs, defaults to true
+ */
+ static void GetSeasonThumbs(const CVideoInfoTag &show, std::map<int, std::map<std::string, std::string> > &art, const std::vector<std::string> &artTypes, bool useLocal = true);
+ static std::string GetImage(const CScraperUrl::SUrlEntry &image, const std::string& itemPath);
+
+ bool EnumerateEpisodeItem(const CFileItem *item, EPISODELIST& episodeList);
+
+ static std::string GetMovieSetInfoFolder(const std::string& setTitle);
+
+ protected:
+ virtual void Process();
+ bool DoScan(const std::string& strDirectory) override;
+
+ INFO_RET RetrieveInfoForTvShow(CFileItem *pItem, bool bDirNames, ADDON::ScraperPtr &scraper, bool useLocal, CScraperUrl* pURL, bool fetchEpisodes, CGUIDialogProgress* pDlgProgress);
+ INFO_RET RetrieveInfoForMovie(CFileItem *pItem, bool bDirNames, ADDON::ScraperPtr &scraper, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress);
+ INFO_RET RetrieveInfoForMusicVideo(CFileItem *pItem, bool bDirNames, ADDON::ScraperPtr &scraper, bool useLocal, CScraperUrl* pURL, CGUIDialogProgress* pDlgProgress);
+ INFO_RET RetrieveInfoForEpisodes(CFileItem *item, long showID, const ADDON::ScraperPtr &scraper, bool useLocal, CGUIDialogProgress *progress = NULL);
+
+ /*! \brief Update the progress bar with the heading and line and check for cancellation
+ \param progress CGUIDialogProgress bar
+ \param heading string id of heading
+ \param line1 string to set for the first line
+ \return true if the user has cancelled the scanner, false otherwise
+ */
+ bool ProgressCancelled(CGUIDialogProgress* progress, int heading, const std::string &line1);
+
+ /*! \brief Find a url for the given video using the given scraper
+ \param title title of the video to lookup
+ \param year year of the video to lookup
+ \param scraper scraper to use for the lookup
+ \param url [out] returned url from the scraper
+ \param progress CGUIDialogProgress bar
+ \return >0 on success, <0 on failure (cancellation), and 0 on no info found
+ */
+ int FindVideo(const std::string &title, int year, const ADDON::ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress);
+
+ /*! \brief Find a url for the given video using the given scraper
+ \param item the video to lookup
+ \param scraper scraper to use for the lookup
+ \param url [out] returned url from the scraper
+ \param progress CGUIDialogProgress bar
+ \return >0 on success, <0 on failure (cancellation), and 0 on no info found
+ */
+ int FindVideoUsingTag(CFileItem& item, const ADDON::ScraperPtr &scraper, CScraperUrl &url, CGUIDialogProgress *progress);
+
+ /*! \brief Retrieve detailed information for an item from an online source, optionally supplemented with local data
+ @todo sort out some better return codes.
+ \param pItem item to retrieve online details for.
+ \param url URL to use to retrieve online details.
+ \param scraper Scraper that handles parsing the online data.
+ \param nfoFile if set, we override the online data with the locally supplied data. Defaults to NULL.
+ \param pDialog progress dialog to update and check for cancellation during processing. Defaults to NULL.
+ \return true if information is found, false if an error occurred, the lookup was cancelled, or no information was found.
+ */
+ bool GetDetails(CFileItem *pItem, CScraperUrl &url,
+ const ADDON::ScraperPtr &scraper,
+ VIDEO::IVideoInfoTagLoader* nfoFile = nullptr,
+ CGUIDialogProgress* pDialog = nullptr);
+
+ /*! \brief Extract episode and season numbers from a processed regexp
+ \param reg Regular expression object with at least 2 matches
+ \param episodeInfo Episode information to fill in.
+ \param defaultSeason Season to use if not found in reg.
+ \return true on success (2 matches), false on failure (fewer than 2 matches)
+ */
+ bool GetEpisodeAndSeasonFromRegExp(CRegExp &reg, EPISODE &episodeInfo, int defaultSeason);
+
+ /*! \brief Extract episode air-date from a processed regexp
+ \param reg Regular expression object with at least 3 matches
+ \param episodeInfo Episode information to fill in.
+ \return true on success (3 matches), false on failure (fewer than 3 matches)
+ */
+ bool GetAirDateFromRegExp(CRegExp &reg, EPISODE &episodeInfo);
+
+ /*! \brief Extract episode title from a processed regexp
+ \param reg Regular expression object with at least 1 match
+ \param episodeInfo Episode information to fill in.
+ \return true on success (1 match), false on failure (no matches)
+ */
+ bool GetEpisodeTitleFromRegExp(CRegExp& reg, EPISODE& episodeInfo);
+
+ /*! \brief Fetch thumbs for actors
+ Updates each actor with their thumb (local or online)
+ \param actors - vector of SActorInfo
+ \param strPath - path on filesystem to look for local thumbs
+ */
+ void FetchActorThumbs(std::vector<SActorInfo>& actors, const std::string& strPath);
+
+ static int GetPathHash(const CFileItemList &items, std::string &hash);
+
+ /*! \brief Retrieve a "fast" hash of the given directory (if available)
+ Performs a stat() on the directory, and uses modified time to create a "fast"
+ hash of the folder. If no modified time is available, the create time is used,
+ and if neither are available, an empty hash is returned.
+ In case exclude from scan expressions are present, the string array will be appended
+ to the md5 hash to ensure we're doing a re-scan whenever the user modifies those.
+ \param directory folder to hash
+ \param excludes string array of exclude expressions
+ \return the md5 hash of the folder"
+ */
+ std::string GetFastHash(const std::string &directory, const std::vector<std::string> &excludes) const;
+
+ /*! \brief Retrieve a "fast" hash of the given directory recursively (if available)
+ Performs a stat() on the directory, and uses modified time to create a "fast"
+ hash of each folder. If no modified time is available, the create time is used,
+ and if neither are available, an empty hash is returned.
+ In case exclude from scan expressions are present, the string array will be appended
+ to the md5 hash to ensure we're doing a re-scan whenever the user modifies those.
+ \param directory folder to hash (recursively)
+ \param excludes string array of exclude expressions
+ \return the md5 hash of the folder
+ */
+ std::string GetRecursiveFastHash(const std::string &directory, const std::vector<std::string> &excludes) const;
+
+ /*! \brief Decide whether a folder listing could use the "fast" hash
+ Fast hashing can be done whenever the folder contains no scannable subfolders, as the
+ fast hash technique uses modified time to determine when folder content changes, which
+ is generally not propagated up the directory tree.
+ \param items the directory listing
+ \param excludes string array of exclude expressions
+ \return true if this directory listing can be fast hashed, false otherwise
+ */
+ bool CanFastHash(const CFileItemList &items, const std::vector<std::string> &excludes) const;
+
+ /*! \brief Process a series folder, filling in episode details and adding them to the database.
+ @todo Ideally we would return INFO_HAVE_ALREADY if we don't have to update any episodes
+ and we should return INFO_NOT_FOUND only if no information is found for any of
+ the episodes. INFO_ADDED then indicates we've added one or more episodes.
+ \param files the episode files to process.
+ \param scraper scraper to use for finding online info
+ \param showInfo information for the show.
+ \param pDlgProcess progress dialog to update during processing. Defaults to NULL.
+ \return INFO_ERROR on failure, INFO_CANCELLED on cancellation,
+ INFO_NOT_FOUND if an episode isn't found, or INFO_ADDED if all episodes are added.
+ */
+ INFO_RET OnProcessSeriesFolder(EPISODELIST& files, const ADDON::ScraperPtr &scraper, bool useLocal, const CVideoInfoTag& showInfo, CGUIDialogProgress* pDlgProgress = NULL);
+
+ bool EnumerateSeriesFolder(CFileItem* item, EPISODELIST& episodeList);
+ bool ProcessItemByVideoInfoTag(const CFileItem *item, EPISODELIST &episodeList);
+
+ bool m_bStop;
+ bool m_scanAll;
+ std::string m_strStartDir;
+ CVideoDatabase m_database;
+ std::set<std::string> m_pathsToCount;
+ std::set<int> m_pathsToClean;
+
+ private:
+ static void AddLocalItemArtwork(CGUIListItem::ArtMap& itemArt,
+ const std::vector<std::string>& wantedArtTypes, const std::string& itemPath,
+ bool addAll, bool exactName);
+
+ /*! \brief Retrieve the art type for an image from the given size.
+ \param width the width of the image.
+ \param height the height of the image.
+ \return "poster" if the aspect ratio is at most 4:5, "banner" if the aspect ratio
+ is at least 1:4, "thumb" otherwise.
+ */
+ static std::string GetArtTypeFromSize(unsigned int width, unsigned int height);
+ };
+}
+
diff --git a/xbmc/video/VideoInfoTag.cpp b/xbmc/video/VideoInfoTag.cpp
new file mode 100644
index 0000000..811dd06
--- /dev/null
+++ b/xbmc/video/VideoInfoTag.cpp
@@ -0,0 +1,1791 @@
+/*
+ * 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 "VideoInfoTag.h"
+
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Archive.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <sstream>
+#include <string>
+#include <vector>
+
+void CVideoInfoTag::Reset()
+{
+ m_director.clear();
+ m_writingCredits.clear();
+ m_genre.clear();
+ m_country.clear();
+ m_strTagLine.clear();
+ m_strPlotOutline.clear();
+ m_strPlot.clear();
+ m_strPictureURL.Clear();
+ m_strTitle.clear();
+ m_strShowTitle.clear();
+ m_strOriginalTitle.clear();
+ m_strSortTitle.clear();
+ m_cast.clear();
+ m_set.title.clear();
+ m_set.id = -1;
+ m_set.overview.clear();
+ m_tags.clear();
+ m_strFile.clear();
+ m_strPath.clear();
+ m_strMPAARating.clear();
+ m_strFileNameAndPath.clear();
+ m_premiered.Reset();
+ m_bHasPremiered = false;
+ m_strStatus.clear();
+ m_strProductionCode.clear();
+ m_firstAired.Reset();
+ m_studio.clear();
+ m_strAlbum.clear();
+ m_artist.clear();
+ m_strTrailer.clear();
+ m_iTop250 = 0;
+ m_year = -1;
+ m_iSeason = -1;
+ m_iEpisode = -1;
+ m_iIdUniqueID = -1;
+ m_uniqueIDs.clear();
+ m_strDefaultUniqueID = "unknown";
+ m_iSpecialSortSeason = -1;
+ m_iSpecialSortEpisode = -1;
+ m_strDefaultRating = "default";
+ m_iIdRating = -1;
+ m_ratings.clear();
+ m_iUserRating = 0;
+ m_iDbId = -1;
+ m_iFileId = -1;
+ m_iBookmarkId = -1;
+ m_iTrack = -1;
+ m_fanart.m_xml.clear();
+ m_duration = 0;
+ m_lastPlayed.Reset();
+ m_showLink.clear();
+ m_namedSeasons.clear();
+ m_streamDetails.Reset();
+ m_playCount = PLAYCOUNT_NOT_SET;
+ m_EpBookmark.Reset();
+ m_EpBookmark.type = CBookmark::EPISODE;
+ m_basePath.clear();
+ m_parentPathID = -1;
+ m_resumePoint.Reset();
+ m_resumePoint.type = CBookmark::RESUME;
+ m_iIdShow = -1;
+ m_iIdSeason = -1;
+ m_dateAdded.Reset();
+ m_type.clear();
+ m_relevance = -1;
+ m_parsedDetails = 0;
+ m_coverArt.clear();
+}
+
+bool CVideoInfoTag::Save(TiXmlNode *node, const std::string &tag, bool savePathInfo, const TiXmlElement *additionalNode)
+{
+ if (!node) return false;
+
+ // we start with a <tag> tag
+ TiXmlElement movieElement(tag.c_str());
+ TiXmlNode *movie = node->InsertEndChild(movieElement);
+
+ if (!movie) return false;
+
+ XMLUtils::SetString(movie, "title", m_strTitle);
+ if (!m_strOriginalTitle.empty())
+ XMLUtils::SetString(movie, "originaltitle", m_strOriginalTitle);
+ if (!m_strShowTitle.empty())
+ XMLUtils::SetString(movie, "showtitle", m_strShowTitle);
+ if (!m_strSortTitle.empty())
+ XMLUtils::SetString(movie, "sorttitle", m_strSortTitle);
+ if (!m_ratings.empty())
+ {
+ TiXmlElement ratings("ratings");
+ for (const auto& it : m_ratings)
+ {
+ TiXmlElement rating("rating");
+ rating.SetAttribute("name", it.first.c_str());
+ XMLUtils::SetFloat(&rating, "value", it.second.rating);
+ XMLUtils::SetInt(&rating, "votes", it.second.votes);
+ rating.SetAttribute("max", 10);
+ if (it.first == m_strDefaultRating)
+ rating.SetAttribute("default", "true");
+ ratings.InsertEndChild(rating);
+ }
+ movie->InsertEndChild(ratings);
+ }
+ XMLUtils::SetInt(movie, "userrating", m_iUserRating);
+
+ if (m_EpBookmark.timeInSeconds > 0)
+ {
+ TiXmlElement epbookmark("episodebookmark");
+ XMLUtils::SetDouble(&epbookmark, "position", m_EpBookmark.timeInSeconds);
+ if (!m_EpBookmark.playerState.empty())
+ {
+ TiXmlElement playerstate("playerstate");
+ CXBMCTinyXML doc;
+ doc.Parse(m_EpBookmark.playerState);
+ playerstate.InsertEndChild(*doc.RootElement());
+ epbookmark.InsertEndChild(playerstate);
+ }
+ movie->InsertEndChild(epbookmark);
+ }
+
+ XMLUtils::SetInt(movie, "top250", m_iTop250);
+ if (tag == "episodedetails" || tag == "tvshow")
+ {
+ XMLUtils::SetInt(movie, "season", m_iSeason);
+ XMLUtils::SetInt(movie, "episode", m_iEpisode);
+ XMLUtils::SetInt(movie, "displayseason",m_iSpecialSortSeason);
+ XMLUtils::SetInt(movie, "displayepisode",m_iSpecialSortEpisode);
+ }
+ if (tag == "musicvideo")
+ {
+ XMLUtils::SetInt(movie, "track", m_iTrack);
+ XMLUtils::SetString(movie, "album", m_strAlbum);
+ }
+ XMLUtils::SetString(movie, "outline", m_strPlotOutline);
+ XMLUtils::SetString(movie, "plot", m_strPlot);
+ XMLUtils::SetString(movie, "tagline", m_strTagLine);
+ XMLUtils::SetInt(movie, "runtime", GetDuration() / 60);
+ if (m_strPictureURL.HasData())
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(m_strPictureURL.GetData());
+ const TiXmlNode* thumb = doc.FirstChild("thumb");
+ while (thumb)
+ {
+ movie->InsertEndChild(*thumb);
+ thumb = thumb->NextSibling("thumb");
+ }
+ }
+ if (m_fanart.m_xml.size())
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(m_fanart.m_xml);
+ movie->InsertEndChild(*doc.RootElement());
+ }
+ XMLUtils::SetString(movie, "mpaa", m_strMPAARating);
+ XMLUtils::SetInt(movie, "playcount", GetPlayCount());
+ XMLUtils::SetDate(movie, "lastplayed", m_lastPlayed);
+ if (savePathInfo)
+ {
+ XMLUtils::SetString(movie, "file", m_strFile);
+ XMLUtils::SetString(movie, "path", m_strPath);
+ XMLUtils::SetString(movie, "filenameandpath", m_strFileNameAndPath);
+ XMLUtils::SetString(movie, "basepath", m_basePath);
+ }
+ if (!m_strEpisodeGuide.empty())
+ {
+ CXBMCTinyXML doc;
+ doc.Parse(m_strEpisodeGuide);
+ if (doc.RootElement())
+ movie->InsertEndChild(*doc.RootElement());
+ else
+ XMLUtils::SetString(movie, "episodeguide", m_strEpisodeGuide);
+ }
+
+ XMLUtils::SetString(movie, "id", GetUniqueID());
+ for (const auto& uniqueid : m_uniqueIDs)
+ {
+ TiXmlElement uniqueID("uniqueid");
+ uniqueID.SetAttribute("type", uniqueid.first);
+ if (uniqueid.first == m_strDefaultUniqueID)
+ uniqueID.SetAttribute("default", "true");
+ TiXmlText value(uniqueid.second);
+ uniqueID.InsertEndChild(value);
+
+ movie->InsertEndChild(uniqueID);
+ }
+ XMLUtils::SetStringArray(movie, "genre", m_genre);
+ XMLUtils::SetStringArray(movie, "country", m_country);
+ if (!m_set.title.empty())
+ {
+ TiXmlElement set("set");
+ XMLUtils::SetString(&set, "name", m_set.title);
+ if (!m_set.overview.empty())
+ XMLUtils::SetString(&set, "overview", m_set.overview);
+ movie->InsertEndChild(set);
+ }
+ XMLUtils::SetStringArray(movie, "tag", m_tags);
+ XMLUtils::SetStringArray(movie, "credits", m_writingCredits);
+ XMLUtils::SetStringArray(movie, "director", m_director);
+ if (HasPremiered())
+ XMLUtils::SetDate(movie, "premiered", m_premiered);
+ if (HasYear())
+ XMLUtils::SetInt(movie, "year", GetYear());
+ XMLUtils::SetString(movie, "status", m_strStatus);
+ XMLUtils::SetString(movie, "code", m_strProductionCode);
+ XMLUtils::SetDate(movie, "aired", m_firstAired);
+ XMLUtils::SetStringArray(movie, "studio", m_studio);
+ XMLUtils::SetString(movie, "trailer", m_strTrailer);
+
+ if (m_streamDetails.HasItems())
+ {
+ // it goes fileinfo/streamdetails/[video|audio|subtitle]
+ TiXmlElement fileinfo("fileinfo");
+ TiXmlElement streamdetails("streamdetails");
+ for (int iStream=1; iStream<=m_streamDetails.GetVideoStreamCount(); iStream++)
+ {
+ TiXmlElement stream("video");
+ XMLUtils::SetString(&stream, "codec", m_streamDetails.GetVideoCodec(iStream));
+ XMLUtils::SetFloat(&stream, "aspect", m_streamDetails.GetVideoAspect(iStream));
+ XMLUtils::SetInt(&stream, "width", m_streamDetails.GetVideoWidth(iStream));
+ XMLUtils::SetInt(&stream, "height", m_streamDetails.GetVideoHeight(iStream));
+ XMLUtils::SetInt(&stream, "durationinseconds", m_streamDetails.GetVideoDuration(iStream));
+ XMLUtils::SetString(&stream, "stereomode", m_streamDetails.GetStereoMode(iStream));
+ XMLUtils::SetString(&stream, "hdrtype", m_streamDetails.GetVideoHdrType(iStream));
+ streamdetails.InsertEndChild(stream);
+ }
+ for (int iStream=1; iStream<=m_streamDetails.GetAudioStreamCount(); iStream++)
+ {
+ TiXmlElement stream("audio");
+ XMLUtils::SetString(&stream, "codec", m_streamDetails.GetAudioCodec(iStream));
+ XMLUtils::SetString(&stream, "language", m_streamDetails.GetAudioLanguage(iStream));
+ XMLUtils::SetInt(&stream, "channels", m_streamDetails.GetAudioChannels(iStream));
+ streamdetails.InsertEndChild(stream);
+ }
+ for (int iStream=1; iStream<=m_streamDetails.GetSubtitleStreamCount(); iStream++)
+ {
+ TiXmlElement stream("subtitle");
+ XMLUtils::SetString(&stream, "language", m_streamDetails.GetSubtitleLanguage(iStream));
+ streamdetails.InsertEndChild(stream);
+ }
+ fileinfo.InsertEndChild(streamdetails);
+ movie->InsertEndChild(fileinfo);
+ } /* if has stream details */
+
+ // cast
+ for (iCast it = m_cast.begin(); it != m_cast.end(); ++it)
+ {
+ // add a <actor> tag
+ TiXmlElement cast("actor");
+ TiXmlNode *node = movie->InsertEndChild(cast);
+ XMLUtils::SetString(node, "name", it->strName);
+ XMLUtils::SetString(node, "role", it->strRole);
+ XMLUtils::SetInt(node, "order", it->order);
+ XMLUtils::SetString(node, "thumb", it->thumbUrl.GetFirstUrlByType().m_url);
+ }
+ XMLUtils::SetStringArray(movie, "artist", m_artist);
+ XMLUtils::SetStringArray(movie, "showlink", m_showLink);
+
+ for (const auto& namedSeason : m_namedSeasons)
+ {
+ TiXmlElement season("namedseason");
+ season.SetAttribute("number", namedSeason.first);
+ TiXmlText value(namedSeason.second);
+ season.InsertEndChild(value);
+ movie->InsertEndChild(season);
+ }
+
+ TiXmlElement resume("resume");
+ XMLUtils::SetDouble(&resume, "position", m_resumePoint.timeInSeconds);
+ XMLUtils::SetDouble(&resume, "total", m_resumePoint.totalTimeInSeconds);
+ if (!m_resumePoint.playerState.empty())
+ {
+ TiXmlElement playerstate("playerstate");
+ CXBMCTinyXML doc;
+ doc.Parse(m_resumePoint.playerState);
+ playerstate.InsertEndChild(*doc.RootElement());
+ resume.InsertEndChild(playerstate);
+ }
+ movie->InsertEndChild(resume);
+
+ XMLUtils::SetDateTime(movie, "dateadded", m_dateAdded);
+
+ if (additionalNode)
+ movie->InsertEndChild(*additionalNode);
+
+ return true;
+}
+
+bool CVideoInfoTag::Load(const TiXmlElement *element, bool append, bool prioritise)
+{
+ if (!element)
+ return false;
+ if (!append)
+ Reset();
+ ParseNative(element, prioritise);
+ return true;
+}
+
+void CVideoInfoTag::Merge(CVideoInfoTag& other)
+{
+ if (!other.m_director.empty())
+ m_director = other.m_director;
+ if (!other.m_writingCredits.empty())
+ m_writingCredits = other.m_writingCredits;
+ if (!other.m_genre.empty())
+ m_genre = other.m_genre;
+ if (!other.m_country.empty())
+ m_country = other.m_country;
+ if (!other.m_strTagLine.empty())
+ m_strTagLine = other.m_strTagLine;
+ if (!other.m_strPlotOutline.empty())
+ m_strPlotOutline = other.m_strPlotOutline;
+ if (!other.m_strPlot.empty())
+ m_strPlot = other.m_strPlot;
+ if (other.m_strPictureURL.HasData())
+ m_strPictureURL = other.m_strPictureURL;
+ if (!other.m_strTitle.empty())
+ m_strTitle = other.m_strTitle;
+ if (!other.m_strShowTitle.empty())
+ m_strShowTitle = other.m_strShowTitle;
+ if (!other.m_strOriginalTitle.empty())
+ m_strOriginalTitle = other.m_strOriginalTitle;
+ if (!other.m_strSortTitle.empty())
+ m_strSortTitle = other.m_strSortTitle;
+ if (other.m_cast.size())
+ m_cast = other.m_cast;
+
+ if (!other.m_set.title.empty())
+ m_set.title = other.m_set.title;
+ if (other.m_set.id)
+ m_set.id = other.m_set.id;
+ if (!other.m_set.overview.empty())
+ m_set.overview = other.m_set.overview;
+ if (!other.m_tags.empty())
+ m_tags = other.m_tags;
+
+ if (!other.m_strFile.empty())
+ m_strFile = other.m_strFile;
+ if (!other.m_strPath.empty())
+ m_strPath = other.m_strPath;
+
+ if (!other.m_strMPAARating.empty())
+ m_strMPAARating = other.m_strMPAARating;
+ if (!other.m_strFileNameAndPath.empty())
+ m_strFileNameAndPath = other.m_strFileNameAndPath;
+
+ if (other.m_premiered.IsValid())
+ SetPremiered(other.GetPremiered());
+
+ if (!other.m_strStatus.empty())
+ m_strStatus = other.m_strStatus;
+ if (!other.m_strProductionCode.empty())
+ m_strProductionCode = other.m_strProductionCode;
+
+ if (other.m_firstAired.IsValid())
+ m_firstAired = other.m_firstAired;
+ if (!other.m_studio.empty())
+ m_studio = other.m_studio;
+ if (!other.m_strAlbum.empty())
+ m_strAlbum = other.m_strAlbum;
+ if (!other.m_artist.empty())
+ m_artist = other.m_artist;
+ if (!other.m_strTrailer.empty())
+ m_strTrailer = other.m_strTrailer;
+ if (other.m_iTop250)
+ m_iTop250 = other.m_iTop250;
+ if (other.m_iSeason != -1)
+ m_iSeason = other.m_iSeason;
+ if (other.m_iEpisode != -1)
+ m_iEpisode = other.m_iEpisode;
+
+ if (other.m_iIdUniqueID != -1)
+ m_iIdUniqueID = other.m_iIdUniqueID;
+ if (other.m_uniqueIDs.size())
+ {
+ m_uniqueIDs = other.m_uniqueIDs;
+ m_strDefaultUniqueID = other.m_strDefaultUniqueID;
+ };
+ if (other.m_iSpecialSortSeason != -1)
+ m_iSpecialSortSeason = other.m_iSpecialSortSeason;
+ if (other.m_iSpecialSortEpisode != -1)
+ m_iSpecialSortEpisode = other.m_iSpecialSortEpisode;
+
+ if (!other.m_ratings.empty())
+ {
+ m_ratings = other.m_ratings;
+ m_strDefaultRating = other.m_strDefaultRating;
+ };
+ if (other.m_iIdRating != -1)
+ m_iIdRating = other.m_iIdRating;
+ if (other.m_iUserRating)
+ m_iUserRating = other.m_iUserRating;
+
+ if (other.m_iDbId != -1)
+ m_iDbId = other.m_iDbId;
+ if (other.m_iFileId != -1)
+ m_iFileId = other.m_iFileId;
+ if (other.m_iBookmarkId != -1)
+ m_iBookmarkId = other.m_iBookmarkId;
+ if (other.m_iTrack != -1)
+ m_iTrack = other.m_iTrack;
+
+ if (other.m_fanart.GetNumFanarts())
+ m_fanart = other.m_fanart;
+
+ if (other.m_duration)
+ m_duration = other.m_duration;
+ if (other.m_lastPlayed.IsValid())
+ m_lastPlayed = other.m_lastPlayed;
+
+ if (!other.m_showLink.empty())
+ m_showLink = other.m_showLink;
+ if (other.m_namedSeasons.size())
+ m_namedSeasons = other.m_namedSeasons;
+ if (other.m_streamDetails.HasItems())
+ m_streamDetails = other.m_streamDetails;
+ if (other.IsPlayCountSet())
+ SetPlayCount(other.GetPlayCount());
+
+ if (other.m_EpBookmark.IsSet())
+ m_EpBookmark = other.m_EpBookmark;
+
+ if (!other.m_basePath.empty())
+ m_basePath = other.m_basePath;
+ if (other.m_parentPathID != -1)
+ m_parentPathID = other.m_parentPathID;
+ if (other.GetResumePoint().IsSet())
+ SetResumePoint(other.GetResumePoint());
+ if (other.m_iIdShow != -1)
+ m_iIdShow = other.m_iIdShow;
+ if (other.m_iIdSeason != -1)
+ m_iIdSeason = other.m_iIdSeason;
+
+ if (other.m_dateAdded.IsValid())
+ m_dateAdded = other.m_dateAdded;
+
+ if (!other.m_type.empty())
+ m_type = other.m_type;
+
+ if (other.m_relevance != -1)
+ m_relevance = other.m_relevance;
+ if (other.m_parsedDetails)
+ m_parsedDetails = other.m_parsedDetails;
+ if (other.m_coverArt.size())
+ m_coverArt = other.m_coverArt;
+ if (other.m_year != -1)
+ m_year = other.m_year;
+}
+
+void CVideoInfoTag::Archive(CArchive& ar)
+{
+ if (ar.IsStoring())
+ {
+ ar << m_director;
+ ar << m_writingCredits;
+ ar << m_genre;
+ ar << m_country;
+ ar << m_strTagLine;
+ ar << m_strPlotOutline;
+ ar << m_strPlot;
+ ar << m_strPictureURL.GetData();
+ ar << m_fanart.m_xml;
+ ar << m_strTitle;
+ ar << m_strSortTitle;
+ ar << m_studio;
+ ar << m_strTrailer;
+ ar << (int)m_cast.size();
+ for (unsigned int i=0;i<m_cast.size();++i)
+ {
+ ar << m_cast[i].strName;
+ ar << m_cast[i].strRole;
+ ar << m_cast[i].order;
+ ar << m_cast[i].thumb;
+ ar << m_cast[i].thumbUrl.GetData();
+ }
+
+ ar << m_set.title;
+ ar << m_set.id;
+ ar << m_set.overview;
+ ar << m_tags;
+ ar << m_duration;
+ ar << m_strFile;
+ ar << m_strPath;
+ ar << m_strMPAARating;
+ ar << m_strFileNameAndPath;
+ ar << m_strOriginalTitle;
+ ar << m_strEpisodeGuide;
+ ar << m_premiered;
+ ar << m_bHasPremiered;
+ ar << m_strStatus;
+ ar << m_strProductionCode;
+ ar << m_firstAired;
+ ar << m_strShowTitle;
+ ar << m_strAlbum;
+ ar << m_artist;
+ ar << GetPlayCount();
+ ar << m_lastPlayed;
+ ar << m_iTop250;
+ ar << m_iSeason;
+ ar << m_iEpisode;
+ ar << (int)m_uniqueIDs.size();
+ for (const auto& i : m_uniqueIDs)
+ {
+ ar << i.first;
+ ar << (i.first == m_strDefaultUniqueID);
+ ar << i.second;
+ }
+ ar << (int)m_ratings.size();
+ for (const auto& i : m_ratings)
+ {
+ ar << i.first;
+ ar << (i.first == m_strDefaultRating);
+ ar << i.second.rating;
+ ar << i.second.votes;
+ }
+ ar << m_iUserRating;
+ ar << m_iDbId;
+ ar << m_iFileId;
+ ar << m_iSpecialSortSeason;
+ ar << m_iSpecialSortEpisode;
+ ar << m_iBookmarkId;
+ ar << m_iTrack;
+ ar << dynamic_cast<IArchivable&>(m_streamDetails);
+ ar << m_showLink;
+ ar << static_cast<int>(m_namedSeasons.size());
+ for (const auto& namedSeason : m_namedSeasons)
+ {
+ ar << namedSeason.first;
+ ar << namedSeason.second;
+ }
+ ar << m_EpBookmark.playerState;
+ ar << m_EpBookmark.timeInSeconds;
+ ar << m_basePath;
+ ar << m_parentPathID;
+ ar << m_resumePoint.timeInSeconds;
+ ar << m_resumePoint.totalTimeInSeconds;
+ ar << m_resumePoint.playerState;
+ ar << m_iIdShow;
+ ar << m_dateAdded.GetAsDBDateTime();
+ ar << m_type;
+ ar << m_iIdSeason;
+ ar << m_coverArt.size();
+ for (auto& it : m_coverArt)
+ ar << it;
+ }
+ else
+ {
+ ar >> m_director;
+ ar >> m_writingCredits;
+ ar >> m_genre;
+ ar >> m_country;
+ ar >> m_strTagLine;
+ ar >> m_strPlotOutline;
+ ar >> m_strPlot;
+ std::string data;
+ ar >> data;
+ m_strPictureURL.SetData(data);
+ ar >> m_fanart.m_xml;
+ ar >> m_strTitle;
+ ar >> m_strSortTitle;
+ ar >> m_studio;
+ ar >> m_strTrailer;
+ int iCastSize;
+ ar >> iCastSize;
+ m_cast.reserve(iCastSize);
+ for (int i=0;i<iCastSize;++i)
+ {
+ SActorInfo info;
+ ar >> info.strName;
+ ar >> info.strRole;
+ ar >> info.order;
+ ar >> info.thumb;
+ std::string strXml;
+ ar >> strXml;
+ info.thumbUrl.ParseFromData(strXml);
+ m_cast.push_back(info);
+ }
+
+ ar >> m_set.title;
+ ar >> m_set.id;
+ ar >> m_set.overview;
+ ar >> m_tags;
+ ar >> m_duration;
+ ar >> m_strFile;
+ ar >> m_strPath;
+ ar >> m_strMPAARating;
+ ar >> m_strFileNameAndPath;
+ ar >> m_strOriginalTitle;
+ ar >> m_strEpisodeGuide;
+ ar >> m_premiered;
+ ar >> m_bHasPremiered;
+ ar >> m_strStatus;
+ ar >> m_strProductionCode;
+ ar >> m_firstAired;
+ ar >> m_strShowTitle;
+ ar >> m_strAlbum;
+ ar >> m_artist;
+ ar >> m_playCount;
+ ar >> m_lastPlayed;
+ ar >> m_iTop250;
+ ar >> m_iSeason;
+ ar >> m_iEpisode;
+ int iUniqueIDSize;
+ ar >> iUniqueIDSize;
+ for (int i = 0; i < iUniqueIDSize; ++i)
+ {
+ std::string value;
+ std::string name;
+ bool defaultUniqueID;
+ ar >> name;
+ ar >> defaultUniqueID;
+ ar >> value;
+ SetUniqueID(value, name);
+ if (defaultUniqueID)
+ m_strDefaultUniqueID = name;
+ }
+ int iRatingSize;
+ ar >> iRatingSize;
+ for (int i = 0; i < iRatingSize; ++i)
+ {
+ CRating rating;
+ std::string name;
+ bool defaultRating;
+ ar >> name;
+ ar >> defaultRating;
+ ar >> rating.rating;
+ ar >> rating.votes;
+ SetRating(rating, name);
+ if (defaultRating)
+ m_strDefaultRating = name;
+ }
+ ar >> m_iUserRating;
+ ar >> m_iDbId;
+ ar >> m_iFileId;
+ ar >> m_iSpecialSortSeason;
+ ar >> m_iSpecialSortEpisode;
+ ar >> m_iBookmarkId;
+ ar >> m_iTrack;
+ ar >> dynamic_cast<IArchivable&>(m_streamDetails);
+ ar >> m_showLink;
+
+ int namedSeasonSize;
+ ar >> namedSeasonSize;
+ for (int i = 0; i < namedSeasonSize; ++i)
+ {
+ int seasonNumber;
+ ar >> seasonNumber;
+ std::string seasonName;
+ ar >> seasonName;
+ m_namedSeasons.insert(std::make_pair(seasonNumber, seasonName));
+ }
+ ar >> m_EpBookmark.playerState;
+ ar >> m_EpBookmark.timeInSeconds;
+ ar >> m_basePath;
+ ar >> m_parentPathID;
+ ar >> m_resumePoint.timeInSeconds;
+ ar >> m_resumePoint.totalTimeInSeconds;
+ ar >> m_resumePoint.playerState;
+ ar >> m_iIdShow;
+
+ std::string dateAdded;
+ ar >> dateAdded;
+ m_dateAdded.SetFromDBDateTime(dateAdded);
+ ar >> m_type;
+ ar >> m_iIdSeason;
+ size_t size;
+ ar >> size;
+ m_coverArt.resize(size);
+ for (size_t i = 0; i < size; ++i)
+ ar >> m_coverArt[i];
+ }
+}
+
+void CVideoInfoTag::Serialize(CVariant& value) const
+{
+ value["director"] = m_director;
+ value["writer"] = m_writingCredits;
+ value["genre"] = m_genre;
+ value["country"] = m_country;
+ value["tagline"] = m_strTagLine;
+ value["plotoutline"] = m_strPlotOutline;
+ value["plot"] = m_strPlot;
+ value["title"] = m_strTitle;
+ value["votes"] = std::to_string(GetRating().votes);
+ value["studio"] = m_studio;
+ value["trailer"] = m_strTrailer;
+ value["cast"] = CVariant(CVariant::VariantTypeArray);
+ for (unsigned int i = 0; i < m_cast.size(); ++i)
+ {
+ CVariant actor;
+ actor["name"] = m_cast[i].strName;
+ actor["role"] = m_cast[i].strRole;
+ actor["order"] = m_cast[i].order;
+ if (!m_cast[i].thumb.empty())
+ actor["thumbnail"] = CTextureUtils::GetWrappedImageURL(m_cast[i].thumb);
+ value["cast"].push_back(actor);
+ }
+ value["set"] = m_set.title;
+ value["setid"] = m_set.id;
+ value["setoverview"] = m_set.overview;
+ value["tag"] = m_tags;
+ value["runtime"] = GetDuration();
+ value["file"] = m_strFile;
+ value["path"] = m_strPath;
+ value["imdbnumber"] = GetUniqueID();
+ value["mpaa"] = m_strMPAARating;
+ value["filenameandpath"] = m_strFileNameAndPath;
+ value["originaltitle"] = m_strOriginalTitle;
+ value["sorttitle"] = m_strSortTitle;
+ value["episodeguide"] = m_strEpisodeGuide;
+ value["premiered"] = m_premiered.IsValid() ? m_premiered.GetAsDBDate() : StringUtils::Empty;
+ value["status"] = m_strStatus;
+ value["productioncode"] = m_strProductionCode;
+ value["firstaired"] = m_firstAired.IsValid() ? m_firstAired.GetAsDBDate() : StringUtils::Empty;
+ value["showtitle"] = m_strShowTitle;
+ value["album"] = m_strAlbum;
+ value["artist"] = m_artist;
+ value["playcount"] = GetPlayCount();
+ value["lastplayed"] = m_lastPlayed.IsValid() ? m_lastPlayed.GetAsDBDateTime() : StringUtils::Empty;
+ value["top250"] = m_iTop250;
+ value["year"] = GetYear();
+ value["season"] = m_iSeason;
+ value["episode"] = m_iEpisode;
+ for (const auto& i : m_uniqueIDs)
+ value["uniqueid"][i.first] = i.second;
+
+ value["rating"] = GetRating().rating;
+ CVariant ratings = CVariant(CVariant::VariantTypeObject);
+ for (const auto& i : m_ratings)
+ {
+ CVariant rating;
+ rating["rating"] = i.second.rating;
+ rating["votes"] = i.second.votes;
+ rating["default"] = i.first == m_strDefaultRating;
+
+ ratings[i.first] = rating;
+ }
+ value["ratings"] = ratings;
+ value["userrating"] = m_iUserRating;
+ value["dbid"] = m_iDbId;
+ value["fileid"] = m_iFileId;
+ value["track"] = m_iTrack;
+ value["showlink"] = m_showLink;
+ m_streamDetails.Serialize(value["streamdetails"]);
+ CVariant resume = CVariant(CVariant::VariantTypeObject);
+ resume["position"] = m_resumePoint.timeInSeconds;
+ resume["total"] = m_resumePoint.totalTimeInSeconds;
+ value["resume"] = resume;
+ value["tvshowid"] = m_iIdShow;
+ value["dateadded"] = m_dateAdded.IsValid() ? m_dateAdded.GetAsDBDateTime() : StringUtils::Empty;
+ value["type"] = m_type;
+ value["seasonid"] = m_iIdSeason;
+ value["specialsortseason"] = m_iSpecialSortSeason;
+ value["specialsortepisode"] = m_iSpecialSortEpisode;
+}
+
+void CVideoInfoTag::ToSortable(SortItem& sortable, Field field) const
+{
+ switch (field)
+ {
+ case FieldDirector: sortable[FieldDirector] = m_director; break;
+ case FieldWriter: sortable[FieldWriter] = m_writingCredits; break;
+ case FieldGenre: sortable[FieldGenre] = m_genre; break;
+ case FieldCountry: sortable[FieldCountry] = m_country; break;
+ case FieldTagline: sortable[FieldTagline] = m_strTagLine; break;
+ case FieldPlotOutline: sortable[FieldPlotOutline] = m_strPlotOutline; break;
+ case FieldPlot: sortable[FieldPlot] = m_strPlot; break;
+ case FieldTitle:
+ {
+ // make sure not to overwrite an existing title with an empty one
+ std::string title = m_strTitle;
+ if (!title.empty() || sortable.find(FieldTitle) == sortable.end())
+ sortable[FieldTitle] = title;
+ break;
+ }
+ case FieldVotes: sortable[FieldVotes] = GetRating().votes; break;
+ case FieldStudio: sortable[FieldStudio] = m_studio; break;
+ case FieldTrailer: sortable[FieldTrailer] = m_strTrailer; break;
+ case FieldSet: sortable[FieldSet] = m_set.title; break;
+ case FieldTime: sortable[FieldTime] = GetDuration(); break;
+ case FieldFilename: sortable[FieldFilename] = m_strFile; break;
+ case FieldMPAA: sortable[FieldMPAA] = m_strMPAARating; break;
+ case FieldPath:
+ {
+ // make sure not to overwrite an existing path with an empty one
+ std::string path = GetPath();
+ if (!path.empty() || sortable.find(FieldPath) == sortable.end())
+ sortable[FieldPath] = path;
+ break;
+ }
+ case FieldSortTitle:
+ {
+ // seasons with a custom name/title need special handling as they should be sorted by season number
+ if (m_type == MediaTypeSeason && !m_strSortTitle.empty())
+ sortable[FieldSortTitle] = StringUtils::Format(g_localizeStrings.Get(20358), m_iSeason);
+ else
+ sortable[FieldSortTitle] = m_strSortTitle;
+ break;
+ }
+ case FieldOriginalTitle:
+ {
+ // seasons with a custom name/title need special handling as they should be sorted by season number
+ if (m_type == MediaTypeSeason && !m_strOriginalTitle.empty())
+ sortable[FieldOriginalTitle] =
+ StringUtils::Format(g_localizeStrings.Get(20358).c_str(), m_iSeason);
+ else
+ sortable[FieldOriginalTitle] = m_strOriginalTitle;
+ break;
+ }
+ case FieldTvShowStatus: sortable[FieldTvShowStatus] = m_strStatus; break;
+ case FieldProductionCode: sortable[FieldProductionCode] = m_strProductionCode; break;
+ case FieldAirDate: sortable[FieldAirDate] = m_firstAired.IsValid() ? m_firstAired.GetAsDBDate() : (m_premiered.IsValid() ? m_premiered.GetAsDBDate() : StringUtils::Empty); break;
+ case FieldTvShowTitle: sortable[FieldTvShowTitle] = m_strShowTitle; break;
+ case FieldAlbum: sortable[FieldAlbum] = m_strAlbum; break;
+ case FieldArtist: sortable[FieldArtist] = m_artist; break;
+ case FieldPlaycount: sortable[FieldPlaycount] = GetPlayCount(); break;
+ case FieldLastPlayed: sortable[FieldLastPlayed] = m_lastPlayed.IsValid() ? m_lastPlayed.GetAsDBDateTime() : StringUtils::Empty; break;
+ case FieldTop250: sortable[FieldTop250] = m_iTop250; break;
+ case FieldYear: sortable[FieldYear] = GetYear(); break;
+ case FieldSeason: sortable[FieldSeason] = m_iSeason; break;
+ case FieldEpisodeNumber: sortable[FieldEpisodeNumber] = m_iEpisode; break;
+ case FieldNumberOfEpisodes: sortable[FieldNumberOfEpisodes] = m_iEpisode; break;
+ case FieldNumberOfWatchedEpisodes: sortable[FieldNumberOfWatchedEpisodes] = m_iEpisode; break;
+ case FieldEpisodeNumberSpecialSort: sortable[FieldEpisodeNumberSpecialSort] = m_iSpecialSortEpisode; break;
+ case FieldSeasonSpecialSort: sortable[FieldSeasonSpecialSort] = m_iSpecialSortSeason; break;
+ case FieldRating: sortable[FieldRating] = GetRating().rating; break;
+ case FieldUserRating: sortable[FieldUserRating] = m_iUserRating; break;
+ case FieldId: sortable[FieldId] = m_iDbId; break;
+ case FieldTrackNumber: sortable[FieldTrackNumber] = m_iTrack; break;
+ case FieldTag: sortable[FieldTag] = m_tags; break;
+
+ case FieldVideoResolution: sortable[FieldVideoResolution] = m_streamDetails.GetVideoHeight(); break;
+ case FieldVideoAspectRatio: sortable[FieldVideoAspectRatio] = m_streamDetails.GetVideoAspect(); break;
+ case FieldVideoCodec: sortable[FieldVideoCodec] = m_streamDetails.GetVideoCodec(); break;
+ case FieldStereoMode: sortable[FieldStereoMode] = m_streamDetails.GetStereoMode(); break;
+
+ case FieldAudioChannels: sortable[FieldAudioChannels] = m_streamDetails.GetAudioChannels(); break;
+ case FieldAudioCodec: sortable[FieldAudioCodec] = m_streamDetails.GetAudioCodec(); break;
+ case FieldAudioLanguage: sortable[FieldAudioLanguage] = m_streamDetails.GetAudioLanguage(); break;
+
+ case FieldSubtitleLanguage: sortable[FieldSubtitleLanguage] = m_streamDetails.GetSubtitleLanguage(); break;
+
+ case FieldInProgress: sortable[FieldInProgress] = m_resumePoint.IsPartWay(); break;
+ case FieldDateAdded: sortable[FieldDateAdded] = m_dateAdded.IsValid() ? m_dateAdded.GetAsDBDateTime() : StringUtils::Empty; break;
+ case FieldMediaType: sortable[FieldMediaType] = m_type; break;
+ case FieldRelevance: sortable[FieldRelevance] = m_relevance; break;
+ default: break;
+ }
+}
+
+const CRating CVideoInfoTag::GetRating(std::string type) const
+{
+ if (type.empty())
+ type = m_strDefaultRating;
+
+ const auto& rating = m_ratings.find(type);
+ if (rating == m_ratings.end())
+ return CRating();
+
+ return rating->second;
+}
+
+const std::string& CVideoInfoTag::GetDefaultRating() const
+{
+ return m_strDefaultRating;
+}
+
+bool CVideoInfoTag::HasYear() const
+{
+ return m_year > 0 || m_firstAired.IsValid() || m_premiered.IsValid();
+}
+
+int CVideoInfoTag::GetYear() const
+{
+ if (m_year > 0)
+ return m_year;
+ if (m_firstAired.IsValid())
+ return GetFirstAired().GetYear();
+ if (m_premiered.IsValid())
+ return GetPremiered().GetYear();
+ return 0;
+}
+
+bool CVideoInfoTag::HasPremiered() const
+{
+ return m_bHasPremiered;
+}
+
+const CDateTime& CVideoInfoTag::GetPremiered() const
+{
+ return m_premiered;
+}
+
+const CDateTime& CVideoInfoTag::GetFirstAired() const
+{
+ return m_firstAired;
+}
+
+const std::string CVideoInfoTag::GetUniqueID(std::string type) const
+{
+ if (type.empty())
+ type = m_strDefaultUniqueID;
+
+ const auto& uniqueid = m_uniqueIDs.find(type);
+ if (uniqueid == m_uniqueIDs.end())
+ return "";
+
+ return uniqueid->second;
+}
+
+const std::map<std::string, std::string>& CVideoInfoTag::GetUniqueIDs() const
+{
+ return m_uniqueIDs;
+}
+
+const std::string& CVideoInfoTag::GetDefaultUniqueID() const
+{
+ return m_strDefaultUniqueID;
+}
+
+bool CVideoInfoTag::HasUniqueID() const
+{
+ return !m_uniqueIDs.empty();
+}
+
+const std::string CVideoInfoTag::GetCast(bool bIncludeRole /*= false*/) const
+{
+ std::string strLabel;
+ for (iCast it = m_cast.begin(); it != m_cast.end(); ++it)
+ {
+ std::string character;
+ if (it->strRole.empty() || !bIncludeRole)
+ character = StringUtils::Format("{}\n", it->strName);
+ else
+ character =
+ StringUtils::Format("{} {} {}\n", it->strName, g_localizeStrings.Get(20347), it->strRole);
+ strLabel += character;
+ }
+ return StringUtils::TrimRight(strLabel, "\n");
+}
+
+void CVideoInfoTag::ParseNative(const TiXmlElement* movie, bool prioritise)
+{
+ std::string value;
+ float fValue;
+
+ if (XMLUtils::GetString(movie, "title", value))
+ SetTitle(value);
+
+ if (XMLUtils::GetString(movie, "originaltitle", value))
+ SetOriginalTitle(value);
+
+ if (XMLUtils::GetString(movie, "showtitle", value))
+ SetShowTitle(value);
+
+ if (XMLUtils::GetString(movie, "sorttitle", value))
+ SetSortTitle(value);
+
+ const TiXmlElement* node = movie->FirstChildElement("ratings");
+ if (node)
+ {
+ for (const TiXmlElement* child = node->FirstChildElement("rating"); child != nullptr; child = child->NextSiblingElement("rating"))
+ {
+ CRating r;
+ std::string name;
+ if (child->QueryStringAttribute("name", &name) != TIXML_SUCCESS)
+ name = "default";
+ XMLUtils::GetFloat(child, "value", r.rating);
+ if (XMLUtils::GetString(child, "votes", value))
+ r.votes = StringUtils::ReturnDigits(value);
+ int max_value = 10;
+ if ((child->QueryIntAttribute("max", &max_value) == TIXML_SUCCESS) && max_value >= 1)
+ r.rating = r.rating / max_value * 10; // Normalise the Movie Rating to between 1 and 10
+ SetRating(r, name);
+ bool isDefault = false;
+ // guard against assert in tinyxml
+ const char* rAtt = child->Attribute("default", static_cast<int*>(nullptr));
+ if (rAtt && strlen(rAtt) != 0 &&
+ (child->QueryBoolAttribute("default", &isDefault) == TIXML_SUCCESS) && isDefault)
+ m_strDefaultRating = name;
+ }
+ }
+ else if (XMLUtils::GetFloat(movie, "rating", fValue))
+ {
+ CRating r(fValue, 0);
+ if (XMLUtils::GetString(movie, "votes", value))
+ r.votes = StringUtils::ReturnDigits(value);
+ int max_value = 10;
+ const TiXmlElement* rElement = movie->FirstChildElement("rating");
+ if (rElement && (rElement->QueryIntAttribute("max", &max_value) == TIXML_SUCCESS) && max_value >= 1)
+ r.rating = r.rating / max_value * 10; // Normalise the Movie Rating to between 1 and 10
+ SetRating(r, "default");
+ m_strDefaultRating = "default";
+ }
+ XMLUtils::GetInt(movie, "userrating", m_iUserRating);
+
+ const TiXmlElement *epbookmark = movie->FirstChildElement("episodebookmark");
+ if (epbookmark)
+ {
+ XMLUtils::GetDouble(epbookmark, "position", m_EpBookmark.timeInSeconds);
+ const TiXmlElement *playerstate = epbookmark->FirstChildElement("playerstate");
+ if (playerstate)
+ {
+ const TiXmlElement *value = playerstate->FirstChildElement();
+ if (value)
+ m_EpBookmark.playerState << *value;
+ }
+ }
+ else
+ XMLUtils::GetDouble(movie, "epbookmark", m_EpBookmark.timeInSeconds);
+
+ int max_value = 10;
+ const TiXmlElement* urElement = movie->FirstChildElement("userrating");
+ if (urElement && (urElement->QueryIntAttribute("max", &max_value) == TIXML_SUCCESS) && max_value >= 1)
+ m_iUserRating = m_iUserRating / max_value * 10; // Normalise the user Movie Rating to between 1 and 10
+ XMLUtils::GetInt(movie, "top250", m_iTop250);
+ XMLUtils::GetInt(movie, "season", m_iSeason);
+ XMLUtils::GetInt(movie, "episode", m_iEpisode);
+ XMLUtils::GetInt(movie, "track", m_iTrack);
+
+ XMLUtils::GetInt(movie, "displayseason", m_iSpecialSortSeason);
+ XMLUtils::GetInt(movie, "displayepisode", m_iSpecialSortEpisode);
+ int after=0;
+ XMLUtils::GetInt(movie, "displayafterseason",after);
+ if (after > 0)
+ {
+ m_iSpecialSortSeason = after;
+ m_iSpecialSortEpisode = 0x1000; // should be more than any realistic episode number
+ }
+
+ if (XMLUtils::GetString(movie, "outline", value))
+ SetPlotOutline(value);
+
+ if (XMLUtils::GetString(movie, "plot", value))
+ SetPlot(value);
+
+ if (XMLUtils::GetString(movie, "tagline", value))
+ SetTagLine(value);
+
+
+ if (XMLUtils::GetString(movie, "runtime", value) && !value.empty())
+ m_duration = GetDurationFromMinuteString(StringUtils::Trim(value));
+
+ if (XMLUtils::GetString(movie, "mpaa", value))
+ SetMPAARating(value);
+
+ XMLUtils::GetInt(movie, "playcount", m_playCount);
+ XMLUtils::GetDate(movie, "lastplayed", m_lastPlayed);
+
+ if (XMLUtils::GetString(movie, "file", value))
+ SetFile(value);
+
+ if (XMLUtils::GetString(movie, "path", value))
+ SetPath(value);
+
+ const TiXmlElement* uniqueid = movie->FirstChildElement("uniqueid");
+ if (uniqueid == nullptr)
+ {
+ if (XMLUtils::GetString(movie, "id", value))
+ SetUniqueID(value);
+ }
+ else
+ {
+ for (; uniqueid != nullptr; uniqueid = uniqueid->NextSiblingElement("uniqueid"))
+ {
+ if (uniqueid->FirstChild())
+ {
+ if (uniqueid->QueryStringAttribute("type", &value) == TIXML_SUCCESS)
+ SetUniqueID(uniqueid->FirstChild()->ValueStr(), value);
+ else
+ SetUniqueID(uniqueid->FirstChild()->ValueStr());
+ bool isDefault;
+ if (m_strDefaultUniqueID == "unknown" &&
+ (uniqueid->QueryBoolAttribute("default", &isDefault) == TIXML_SUCCESS) && isDefault)
+ {
+ m_strDefaultUniqueID = value;
+ }
+ }
+ }
+ }
+
+ if (XMLUtils::GetString(movie, "filenameandpath", value))
+ SetFileNameAndPath(value);
+
+ if (XMLUtils::GetDate(movie, "premiered", m_premiered))
+ {
+ m_bHasPremiered = true;
+ }
+ else
+ {
+ int year;
+ if (XMLUtils::GetInt(movie, "year", year))
+ SetYear(year);
+ }
+
+ if (XMLUtils::GetString(movie, "status", value))
+ SetStatus(value);
+
+ if (XMLUtils::GetString(movie, "code", value))
+ SetProductionCode(value);
+
+ XMLUtils::GetDate(movie, "aired", m_firstAired);
+
+ if (XMLUtils::GetString(movie, "album", value))
+ SetAlbum(value);
+
+ if (XMLUtils::GetString(movie, "trailer", value))
+ SetTrailer(value);
+
+ if (XMLUtils::GetString(movie, "basepath", value))
+ SetBasePath(value);
+
+ // make sure the picture URLs have been parsed
+ m_strPictureURL.Parse();
+ size_t iThumbCount = m_strPictureURL.GetUrls().size();
+ std::string xmlAdd = m_strPictureURL.GetData();
+
+ const TiXmlElement* thumb = movie->FirstChildElement("thumb");
+ while (thumb)
+ {
+ m_strPictureURL.ParseAndAppendUrl(thumb);
+ if (prioritise)
+ {
+ std::string temp;
+ temp << *thumb;
+ xmlAdd = temp+xmlAdd;
+ }
+ thumb = thumb->NextSiblingElement("thumb");
+ }
+
+ // prioritise thumbs from nfos
+ if (prioritise && iThumbCount && iThumbCount != m_strPictureURL.GetUrls().size())
+ {
+ auto thumbUrls = m_strPictureURL.GetUrls();
+ rotate(thumbUrls.begin(), thumbUrls.begin() + iThumbCount, thumbUrls.end());
+ m_strPictureURL.SetUrls(thumbUrls);
+ m_strPictureURL.SetData(xmlAdd);
+ }
+
+ const std::string itemSeparator = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator;
+
+ std::vector<std::string> genres(m_genre);
+ if (XMLUtils::GetStringArray(movie, "genre", genres, prioritise, itemSeparator))
+ SetGenre(genres);
+
+ std::vector<std::string> country(m_country);
+ if (XMLUtils::GetStringArray(movie, "country", country, prioritise, itemSeparator))
+ SetCountry(country);
+
+ std::vector<std::string> credits(m_writingCredits);
+ if (XMLUtils::GetStringArray(movie, "credits", credits, prioritise, itemSeparator))
+ SetWritingCredits(credits);
+
+ std::vector<std::string> director(m_director);
+ if (XMLUtils::GetStringArray(movie, "director", director, prioritise, itemSeparator))
+ SetDirector(director);
+
+ std::vector<std::string> showLink(m_showLink);
+ if (XMLUtils::GetStringArray(movie, "showlink", showLink, prioritise, itemSeparator))
+ SetShowLink(showLink);
+
+ const TiXmlElement* namedSeason = movie->FirstChildElement("namedseason");
+ while (namedSeason != nullptr)
+ {
+ if (namedSeason->FirstChild() != nullptr)
+ {
+ int seasonNumber;
+ std::string seasonName = namedSeason->FirstChild()->ValueStr();
+ if (!seasonName.empty() &&
+ namedSeason->Attribute("number", &seasonNumber) != nullptr)
+ m_namedSeasons.insert(std::make_pair(seasonNumber, seasonName));
+ }
+
+ namedSeason = namedSeason->NextSiblingElement("namedseason");
+ }
+
+ // cast
+ node = movie->FirstChildElement("actor");
+ if (node && node->FirstChild() && prioritise)
+ m_cast.clear();
+ while (node)
+ {
+ const TiXmlNode *actor = node->FirstChild("name");
+ if (actor && actor->FirstChild())
+ {
+ SActorInfo info;
+ info.strName = actor->FirstChild()->Value();
+
+ if (XMLUtils::GetString(node, "role", value))
+ info.strRole = StringUtils::Trim(value);
+
+ XMLUtils::GetInt(node, "order", info.order);
+ const TiXmlElement* thumb = node->FirstChildElement("thumb");
+ while (thumb)
+ {
+ info.thumbUrl.ParseAndAppendUrl(thumb);
+ thumb = thumb->NextSiblingElement("thumb");
+ }
+ const char* clear=node->Attribute("clear");
+ if (clear && StringUtils::CompareNoCase(clear, "true"))
+ m_cast.clear();
+ m_cast.push_back(info);
+ }
+ node = node->NextSiblingElement("actor");
+ }
+
+ // Pre-Jarvis NFO file:
+ // <set>A set</set>
+ if (XMLUtils::GetString(movie, "set", value))
+ SetSet(value);
+ // Jarvis+:
+ // <set><name>A set</name><overview>A set with a number of movies...</overview></set>
+ node = movie->FirstChildElement("set");
+ if (node)
+ {
+ // No name, no set
+ if (XMLUtils::GetString(node, "name", value))
+ {
+ SetSet(value);
+ if (XMLUtils::GetString(node, "overview", value))
+ SetSetOverview(value);
+ }
+ }
+
+ std::vector<std::string> tags(m_tags);
+ if (XMLUtils::GetStringArray(movie, "tag", tags, prioritise, itemSeparator))
+ SetTags(tags);
+
+ std::vector<std::string> studio(m_studio);
+ if (XMLUtils::GetStringArray(movie, "studio", studio, prioritise, itemSeparator))
+ SetStudio(studio);
+
+ // artists
+ std::vector<std::string> artist(m_artist);
+ node = movie->FirstChildElement("artist");
+ if (node && node->FirstChild() && prioritise)
+ artist.clear();
+ while (node)
+ {
+ const TiXmlNode* pNode = node->FirstChild("name");
+ const char* pValue=NULL;
+ if (pNode && pNode->FirstChild())
+ pValue = pNode->FirstChild()->Value();
+ else if (node->FirstChild())
+ pValue = node->FirstChild()->Value();
+ if (pValue)
+ {
+ const char* clear=node->Attribute("clear");
+ if (clear && StringUtils::CompareNoCase(clear, "true") == 0)
+ artist.clear();
+ std::vector<std::string> newArtists = StringUtils::Split(pValue, itemSeparator);
+ artist.insert(artist.end(), newArtists.begin(), newArtists.end());
+ }
+ node = node->NextSiblingElement("artist");
+ }
+ SetArtist(artist);
+
+ node = movie->FirstChildElement("fileinfo");
+ if (node)
+ {
+ // Try to pull from fileinfo/streamdetails/[video|audio|subtitle]
+ const TiXmlNode *nodeStreamDetails = node->FirstChild("streamdetails");
+ if (nodeStreamDetails)
+ {
+ const TiXmlNode *nodeDetail = NULL;
+ while ((nodeDetail = nodeStreamDetails->IterateChildren("audio", nodeDetail)))
+ {
+ CStreamDetailAudio *p = new CStreamDetailAudio();
+ if (XMLUtils::GetString(nodeDetail, "codec", value))
+ p->m_strCodec = StringUtils::Trim(value);
+
+ if (XMLUtils::GetString(nodeDetail, "language", value))
+ p->m_strLanguage = StringUtils::Trim(value);
+
+ XMLUtils::GetInt(nodeDetail, "channels", p->m_iChannels);
+ StringUtils::ToLower(p->m_strCodec);
+ StringUtils::ToLower(p->m_strLanguage);
+ m_streamDetails.AddStream(p);
+ }
+ nodeDetail = NULL;
+ while ((nodeDetail = nodeStreamDetails->IterateChildren("video", nodeDetail)))
+ {
+ CStreamDetailVideo *p = new CStreamDetailVideo();
+ if (XMLUtils::GetString(nodeDetail, "codec", value))
+ p->m_strCodec = StringUtils::Trim(value);
+
+ XMLUtils::GetFloat(nodeDetail, "aspect", p->m_fAspect);
+ XMLUtils::GetInt(nodeDetail, "width", p->m_iWidth);
+ XMLUtils::GetInt(nodeDetail, "height", p->m_iHeight);
+ XMLUtils::GetInt(nodeDetail, "durationinseconds", p->m_iDuration);
+ if (XMLUtils::GetString(nodeDetail, "stereomode", value))
+ p->m_strStereoMode = StringUtils::Trim(value);
+ if (XMLUtils::GetString(nodeDetail, "language", value))
+ p->m_strLanguage = StringUtils::Trim(value);
+ if (XMLUtils::GetString(nodeDetail, "hdrtype", value))
+ p->m_strHdrType = StringUtils::Trim(value);
+
+ StringUtils::ToLower(p->m_strCodec);
+ StringUtils::ToLower(p->m_strStereoMode);
+ StringUtils::ToLower(p->m_strLanguage);
+ StringUtils::ToLower(p->m_strHdrType);
+ m_streamDetails.AddStream(p);
+ }
+ nodeDetail = NULL;
+ while ((nodeDetail = nodeStreamDetails->IterateChildren("subtitle", nodeDetail)))
+ {
+ CStreamDetailSubtitle *p = new CStreamDetailSubtitle();
+ if (XMLUtils::GetString(nodeDetail, "language", value))
+ p->m_strLanguage = StringUtils::Trim(value);
+ StringUtils::ToLower(p->m_strLanguage);
+ m_streamDetails.AddStream(p);
+ }
+ }
+ m_streamDetails.DetermineBestStreams();
+ } /* if fileinfo */
+
+ if (m_strEpisodeGuide.empty())
+ {
+ const TiXmlElement* epguide = movie->FirstChildElement("episodeguide");
+ if (epguide)
+ {
+ // DEPRECIATE ME - support for old XML-encoded <episodeguide> blocks.
+ if (epguide->FirstChild() &&
+ StringUtils::CompareNoCase("<episodeguide", epguide->FirstChild()->Value(), 13) == 0)
+ {
+ m_strEpisodeGuide = epguide->FirstChild()->Value();
+ }
+ else
+ {
+ std::stringstream stream;
+ stream << *epguide;
+ m_strEpisodeGuide = stream.str();
+ }
+ }
+ }
+
+ // fanart
+ const TiXmlElement *fanart = movie->FirstChildElement("fanart");
+ if (fanart)
+ {
+ // we prioritise mixed-mode nfo's with fanart set
+ if (prioritise)
+ {
+ std::string temp;
+ temp << *fanart;
+ m_fanart.m_xml = temp+m_fanart.m_xml;
+ }
+ else
+ m_fanart.m_xml << *fanart;
+ m_fanart.Unpack();
+ }
+
+ // resumePoint
+ const TiXmlNode *resume = movie->FirstChild("resume");
+ if (resume)
+ {
+ XMLUtils::GetDouble(resume, "position", m_resumePoint.timeInSeconds);
+ XMLUtils::GetDouble(resume, "total", m_resumePoint.totalTimeInSeconds);
+ const TiXmlElement *playerstate = resume->FirstChildElement("playerstate");
+ if (playerstate)
+ {
+ const TiXmlElement *value = playerstate->FirstChildElement();
+ if (value)
+ m_resumePoint.playerState << *value;
+ }
+ }
+
+ XMLUtils::GetDateTime(movie, "dateadded", m_dateAdded);
+}
+
+bool CVideoInfoTag::HasStreamDetails() const
+{
+ return m_streamDetails.HasItems();
+}
+
+bool CVideoInfoTag::IsEmpty() const
+{
+ return (m_strTitle.empty() &&
+ m_strFile.empty() &&
+ m_strPath.empty());
+}
+
+void CVideoInfoTag::SetDuration(int duration)
+{
+ m_duration = duration;
+}
+
+unsigned int CVideoInfoTag::GetDuration() const
+{
+ /*
+ Prefer the duration from the stream if it isn't too
+ small (60%) compared to the duration from the tag.
+ */
+ unsigned int duration = m_streamDetails.GetVideoDuration();
+ if (duration > m_duration * 0.6)
+ return duration;
+
+ return m_duration;
+}
+
+unsigned int CVideoInfoTag::GetStaticDuration() const
+{
+ return m_duration;
+}
+
+unsigned int CVideoInfoTag::GetDurationFromMinuteString(const std::string &runtime)
+{
+ unsigned int duration = (unsigned int)str2uint64(runtime);
+ if (!duration)
+ { // failed for some reason, or zero
+ duration = strtoul(runtime.c_str(), NULL, 10);
+ CLog::Log(LOGWARNING, "{} <runtime> should be in minutes. Interpreting '{}' as {} minutes",
+ __FUNCTION__, runtime, duration);
+ }
+ return duration*60;
+}
+
+void CVideoInfoTag::SetBasePath(std::string basePath)
+{
+ m_basePath = Trim(std::move(basePath));
+}
+
+void CVideoInfoTag::SetDirector(std::vector<std::string> director)
+{
+ m_director = Trim(std::move(director));
+}
+
+void CVideoInfoTag::SetWritingCredits(std::vector<std::string> writingCredits)
+{
+ m_writingCredits = Trim(std::move(writingCredits));
+}
+
+void CVideoInfoTag::SetGenre(std::vector<std::string> genre)
+{
+ m_genre = Trim(std::move(genre));
+}
+
+void CVideoInfoTag::SetCountry(std::vector<std::string> country)
+{
+ m_country = Trim(std::move(country));
+}
+
+void CVideoInfoTag::SetTagLine(std::string tagLine)
+{
+ m_strTagLine = Trim(std::move(tagLine));
+}
+
+void CVideoInfoTag::SetPlotOutline(std::string plotOutline)
+{
+ m_strPlotOutline = Trim(std::move(plotOutline));
+}
+
+void CVideoInfoTag::SetTrailer(std::string trailer)
+{
+ m_strTrailer = Trim(std::move(trailer));
+}
+
+void CVideoInfoTag::SetPlot(std::string plot)
+{
+ m_strPlot = Trim(std::move(plot));
+}
+
+void CVideoInfoTag::SetTitle(std::string title)
+{
+ m_strTitle = Trim(std::move(title));
+}
+
+std::string const &CVideoInfoTag::GetTitle()
+{
+ return m_strTitle;
+}
+
+void CVideoInfoTag::SetSortTitle(std::string sortTitle)
+{
+ m_strSortTitle = Trim(std::move(sortTitle));
+}
+
+void CVideoInfoTag::SetPictureURL(CScraperUrl &pictureURL)
+{
+ m_strPictureURL = pictureURL;
+}
+
+void CVideoInfoTag::SetRating(float rating, int votes, const std::string& type /* = "" */, bool def /* = false */)
+{
+ SetRating(CRating(rating, votes), type, def);
+}
+
+void CVideoInfoTag::SetRating(CRating rating, const std::string& type /* = "" */, bool def /* = false */)
+{
+ if (rating.rating <= 0 || rating.rating > 10)
+ return;
+
+ if (type.empty())
+ m_ratings[m_strDefaultRating] = rating;
+ else
+ {
+ if (def || m_ratings.empty())
+ m_strDefaultRating = type;
+ m_ratings[type] = rating;
+ }
+}
+
+void CVideoInfoTag::SetRating(float rating, const std::string& type /* = "" */, bool def /* = false */)
+{
+ if (rating <= 0 || rating > 10)
+ return;
+
+ if (type.empty())
+ m_ratings[m_strDefaultRating].rating = rating;
+ else
+ {
+ if (def || m_ratings.empty())
+ m_strDefaultRating = type;
+ m_ratings[type].rating = rating;
+ }
+}
+
+void CVideoInfoTag::RemoveRating(const std::string& type)
+{
+ if (m_ratings.find(type) != m_ratings.end())
+ {
+ m_ratings.erase(type);
+ if (m_strDefaultRating == type && !m_ratings.empty())
+ m_strDefaultRating = m_ratings.begin()->first;
+ }
+}
+
+void CVideoInfoTag::SetRatings(RatingMap ratings, const std::string& defaultRating /* = "" */)
+{
+ m_ratings = std::move(ratings);
+
+ if (!defaultRating.empty() && m_ratings.find(defaultRating) != m_ratings.end())
+ m_strDefaultRating = defaultRating;
+}
+
+void CVideoInfoTag::SetVotes(int votes, const std::string& type /* = "" */)
+{
+ if (type.empty())
+ m_ratings[m_strDefaultRating].votes = votes;
+ else
+ m_ratings[type].votes = votes;
+}
+
+void CVideoInfoTag::SetPremiered(const CDateTime& premiered)
+{
+ m_premiered = premiered;
+ m_bHasPremiered = premiered.IsValid();
+}
+
+void CVideoInfoTag::SetPremieredFromDBDate(const std::string& premieredString)
+{
+ CDateTime premiered;
+ premiered.SetFromDBDate(premieredString);
+ SetPremiered(premiered);
+}
+
+void CVideoInfoTag::SetYear(int year)
+{
+ if (year <= 0)
+ return;
+
+ m_year = year;
+}
+
+void CVideoInfoTag::SetArtist(std::vector<std::string> artist)
+{
+ m_artist = Trim(std::move(artist));
+}
+
+void CVideoInfoTag::SetUniqueIDs(std::map<std::string, std::string> uniqueIDs)
+{
+ for (const auto& uniqueid : uniqueIDs)
+ {
+ if (uniqueid.first.empty())
+ uniqueIDs.erase(uniqueid.first);
+ }
+ if (uniqueIDs.find(m_strDefaultUniqueID) == uniqueIDs.end())
+ {
+ const auto defaultUniqueId = GetUniqueID();
+ if (!defaultUniqueId.empty())
+ uniqueIDs[m_strDefaultUniqueID] = defaultUniqueId;
+ }
+ m_uniqueIDs = std::move(uniqueIDs);
+}
+
+void CVideoInfoTag::SetSet(std::string set)
+{
+ m_set.title = Trim(std::move(set));
+}
+
+void CVideoInfoTag::SetSetOverview(std::string setOverview)
+{
+ m_set.overview = Trim(std::move(setOverview));
+}
+
+void CVideoInfoTag::SetTags(std::vector<std::string> tags)
+{
+ m_tags = Trim(std::move(tags));
+}
+
+void CVideoInfoTag::SetFile(std::string file)
+{
+ m_strFile = Trim(std::move(file));
+}
+
+void CVideoInfoTag::SetPath(std::string path)
+{
+ m_strPath = Trim(std::move(path));
+}
+
+void CVideoInfoTag::SetMPAARating(std::string mpaaRating)
+{
+ m_strMPAARating = Trim(std::move(mpaaRating));
+}
+
+void CVideoInfoTag::SetFileNameAndPath(std::string fileNameAndPath)
+{
+ m_strFileNameAndPath = Trim(std::move(fileNameAndPath));
+}
+
+void CVideoInfoTag::SetOriginalTitle(std::string originalTitle)
+{
+ m_strOriginalTitle = Trim(std::move(originalTitle));
+}
+
+void CVideoInfoTag::SetEpisodeGuide(std::string episodeGuide)
+{
+ if (StringUtils::StartsWith(episodeGuide, "<episodeguide"))
+ m_strEpisodeGuide = Trim(std::move(episodeGuide));
+ else
+ m_strEpisodeGuide =
+ StringUtils::Format("<episodeguide>{}</episodeguide>", Trim(std::move(episodeGuide)));
+}
+
+void CVideoInfoTag::SetStatus(std::string status)
+{
+ m_strStatus = Trim(std::move(status));
+}
+
+void CVideoInfoTag::SetProductionCode(std::string productionCode)
+{
+ m_strProductionCode = Trim(std::move(productionCode));
+}
+
+void CVideoInfoTag::SetShowTitle(std::string showTitle)
+{
+ m_strShowTitle = Trim(std::move(showTitle));
+}
+
+void CVideoInfoTag::SetStudio(std::vector<std::string> studio)
+{
+ m_studio = Trim(std::move(studio));
+}
+
+void CVideoInfoTag::SetAlbum(std::string album)
+{
+ m_strAlbum = Trim(std::move(album));
+}
+
+void CVideoInfoTag::SetShowLink(std::vector<std::string> showLink)
+{
+ m_showLink = Trim(std::move(showLink));
+}
+
+void CVideoInfoTag::SetUniqueID(const std::string& uniqueid, const std::string& type /* = "" */, bool isDefaultID /* = false */)
+{
+ if (uniqueid.empty())
+ return;
+
+ if (type.empty())
+ m_uniqueIDs[m_strDefaultUniqueID] = uniqueid;
+ else
+ {
+ m_uniqueIDs[type] = uniqueid;
+ if (isDefaultID)
+ m_strDefaultUniqueID = type;
+ }
+}
+
+void CVideoInfoTag::RemoveUniqueID(const std::string& type)
+{
+ if (m_uniqueIDs.find(type) != m_uniqueIDs.end())
+ m_uniqueIDs.erase(type);
+}
+
+void CVideoInfoTag::SetNamedSeasons(std::map<int, std::string> namedSeasons)
+{
+ m_namedSeasons = std::move(namedSeasons);
+}
+
+void CVideoInfoTag::SetUserrating(int userrating)
+{
+ //This value needs to be between 0-10 - 0 will unset the userrating
+ userrating = std::max(userrating, 0);
+ userrating = std::min(userrating, 10);
+
+ m_iUserRating = userrating;
+}
+
+std::string CVideoInfoTag::Trim(std::string &&value)
+{
+ return StringUtils::Trim(value);
+}
+
+std::vector<std::string> CVideoInfoTag::Trim(std::vector<std::string>&& items)
+{
+ std::for_each(items.begin(), items.end(), [](std::string &str){
+ str = StringUtils::Trim(str);
+ });
+ return std::move(items);
+}
+
+int CVideoInfoTag::GetPlayCount() const
+{
+ return IsPlayCountSet() ? m_playCount : 0;
+}
+
+bool CVideoInfoTag::SetPlayCount(int count)
+{
+ m_playCount = count;
+ return true;
+}
+
+bool CVideoInfoTag::IncrementPlayCount()
+{
+ if (!IsPlayCountSet())
+ m_playCount = 0;
+
+ m_playCount++;
+ return true;
+}
+
+void CVideoInfoTag::ResetPlayCount()
+{
+ m_playCount = PLAYCOUNT_NOT_SET;
+}
+
+bool CVideoInfoTag::IsPlayCountSet() const
+{
+ return m_playCount != PLAYCOUNT_NOT_SET;
+}
+
+CBookmark CVideoInfoTag::GetResumePoint() const
+{
+ return m_resumePoint;
+}
+
+bool CVideoInfoTag::SetResumePoint(const CBookmark &resumePoint)
+{
+ m_resumePoint = resumePoint;
+ return true;
+}
+
+bool CVideoInfoTag::SetResumePoint(double timeInSeconds, double totalTimeInSeconds, const std::string &playerState)
+{
+ CBookmark resumePoint;
+ resumePoint.timeInSeconds = timeInSeconds;
+ resumePoint.totalTimeInSeconds = totalTimeInSeconds;
+ resumePoint.playerState = playerState;
+ resumePoint.type = CBookmark::RESUME;
+
+ m_resumePoint = resumePoint;
+ return true;
+}
diff --git a/xbmc/video/VideoInfoTag.h b/xbmc/video/VideoInfoTag.h
new file mode 100644
index 0000000..ffbdb15
--- /dev/null
+++ b/xbmc/video/VideoInfoTag.h
@@ -0,0 +1,307 @@
+/*
+ * 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 "XBDateTime.h"
+#include "utils/EmbeddedArt.h"
+#include "utils/Fanart.h"
+#include "utils/ISortable.h"
+#include "utils/ScraperUrl.h"
+#include "utils/StreamDetails.h"
+#include "video/Bookmark.h"
+
+#include <string>
+#include <vector>
+
+class CArchive;
+class TiXmlNode;
+class TiXmlElement;
+class CVariant;
+
+struct SActorInfo
+{
+ bool operator<(const SActorInfo &right) const
+ {
+ return order < right.order;
+ }
+ std::string strName;
+ std::string strRole;
+ CScraperUrl thumbUrl;
+ std::string thumb;
+ int order = -1;
+};
+
+class CRating
+{
+public:
+ CRating() = default;
+ explicit CRating(float r): rating(r) {}
+ CRating(float r, int v): rating(r), votes(v) {}
+ float rating = 0.0f;
+ int votes = 0;
+};
+typedef std::map<std::string, CRating> RatingMap;
+
+class CVideoInfoTag : public IArchivable, public ISerializable, public ISortable
+{
+public:
+ CVideoInfoTag() { Reset(); }
+ virtual ~CVideoInfoTag() = default;
+ void Reset();
+ /* \brief Load information to a videoinfotag from an XML element
+ There are three types of tags supported:
+ 1. Single-value tags, such as <title>. These are set if available, else are left untouched.
+ 2. Additive tags, such as <set> or <genre>. These are appended to or replaced (if available) based on the value
+ of the prioritise parameter. In addition, a clear attribute is available in the XML to clear the current value prior
+ to appending.
+ 3. Image tags such as <thumb> and <fanart>. If the prioritise value is specified, any additional values are prepended
+ to the existing values.
+
+ \param element the root XML element to parse.
+ \param append whether information should be added to the existing tag, or whether it should be reset first.
+ \param prioritise if appending, whether additive tags should be prioritised (i.e. replace or prepend) over existing values. Defaults to false.
+
+ \sa ParseNative
+ */
+ bool Load(const TiXmlElement *element, bool append = false, bool prioritise = false);
+ bool Save(TiXmlNode *node, const std::string &tag, bool savePathInfo = true, const TiXmlElement *additionalNode = NULL);
+ void Merge(CVideoInfoTag& other);
+ void Archive(CArchive& ar) override;
+ void Serialize(CVariant& value) const override;
+ void ToSortable(SortItem& sortable, Field field) const override;
+ const CRating GetRating(std::string type = "") const;
+ const std::string& GetDefaultRating() const;
+ const std::string GetUniqueID(std::string type = "") const;
+ const std::map<std::string, std::string>& GetUniqueIDs() const;
+ const std::string& GetDefaultUniqueID() const;
+ bool HasUniqueID() const;
+ virtual bool HasYear() const;
+ virtual int GetYear() const;
+ bool HasPremiered() const;
+ const CDateTime& GetPremiered() const;
+ const CDateTime& GetFirstAired() const;
+ const std::string GetCast(bool bIncludeRole = false) const;
+ bool HasStreamDetails() const;
+ bool IsEmpty() const;
+
+ const std::string& GetPath() const
+ {
+ if (m_strFileNameAndPath.empty())
+ return m_strPath;
+ return m_strFileNameAndPath;
+ };
+
+ /*! \brief set the duration in seconds
+ \param duration the duration to set
+ */
+ void SetDuration(int duration);
+
+ /*! \brief retrieve the duration in seconds.
+ Prefers the duration from stream details if available.
+ */
+ unsigned int GetDuration() const;
+
+ /*! \brief retrieve the duration in seconds.
+ Ignores the duration from stream details even if available.
+ */
+ unsigned int GetStaticDuration() const;
+
+ /*! \brief get the duration in seconds from a minute string
+ \param runtime the runtime string from a scraper or similar
+ \return the time in seconds, if decipherable.
+ */
+ static unsigned int GetDurationFromMinuteString(const std::string &runtime);
+
+ void SetBasePath(std::string basePath);
+ void SetDirector(std::vector<std::string> director);
+ void SetWritingCredits(std::vector<std::string> writingCredits);
+ void SetGenre(std::vector<std::string> genre);
+ void SetCountry(std::vector<std::string> country);
+ void SetTagLine(std::string tagLine);
+ void SetPlotOutline(std::string plotOutline);
+ void SetTrailer(std::string trailer);
+ void SetPlot(std::string plot);
+ std::string const &GetTitle();
+ void SetTitle(std::string title);
+ void SetSortTitle(std::string sortTitle);
+ void SetPictureURL(CScraperUrl &pictureURL);
+ void SetRating(float rating, int votes, const std::string& type = "", bool def = false);
+ void SetRating(CRating rating, const std::string& type = "", bool def = false);
+ void SetRating(float rating, const std::string& type = "", bool def = false);
+ void RemoveRating(const std::string& type);
+ void SetRatings(RatingMap ratings, const std::string& defaultRating = "");
+ void SetVotes(int votes, const std::string& type = "");
+ void SetUniqueIDs(std::map<std::string, std::string> uniqueIDs);
+ void SetPremiered(const CDateTime& premiered);
+ void SetPremieredFromDBDate(const std::string& premieredString);
+ virtual void SetYear(int year);
+ void SetArtist(std::vector<std::string> artist);
+ void SetSet(std::string set);
+ void SetSetOverview(std::string setOverview);
+ void SetTags(std::vector<std::string> tags);
+ void SetFile(std::string file);
+ void SetPath(std::string path);
+ void SetMPAARating(std::string mpaaRating);
+ void SetFileNameAndPath(std::string fileNameAndPath);
+ void SetOriginalTitle(std::string originalTitle);
+ void SetEpisodeGuide(std::string episodeGuide);
+ void SetStatus(std::string status);
+ void SetProductionCode(std::string productionCode);
+ void SetShowTitle(std::string showTitle);
+ void SetStudio(std::vector<std::string> studio);
+ void SetAlbum(std::string album);
+ void SetShowLink(std::vector<std::string> showLink);
+ void SetUniqueID(const std::string& uniqueid, const std::string& type = "", bool def = false);
+ void RemoveUniqueID(const std::string& type);
+ void SetNamedSeasons(std::map<int, std::string> namedSeasons);
+ void SetUserrating(int userrating);
+
+ /*!
+ * @brief Get this videos's play count.
+ * @return the play count.
+ */
+ virtual int GetPlayCount() const;
+
+ /*!
+ * @brief Set this videos's play count.
+ * @param count play count.
+ * @return True if play count was set successfully, false otherwise.
+ */
+ virtual bool SetPlayCount(int count);
+
+ /*!
+ * @brief Increment this videos's play count.
+ * @return True if play count was increased successfully, false otherwise.
+ */
+ virtual bool IncrementPlayCount();
+
+ /*!
+ * @brief Reset playcount
+ */
+ virtual void ResetPlayCount();
+
+ /*!
+ * @brief Check if the playcount is set
+ * @return True if play count value is set
+ */
+ virtual bool IsPlayCountSet() const;
+
+ /*!
+ * @brief Get this videos's resume point.
+ * @return the resume point.
+ */
+ virtual CBookmark GetResumePoint() const;
+
+ /*!
+ * @brief Set this videos's resume point.
+ * @param resumePoint resume point.
+ * @return True if resume point was set successfully, false otherwise.
+ */
+ virtual bool SetResumePoint(const CBookmark &resumePoint);
+
+ /*!
+ * @brief Set this videos's resume point.
+ * @param timeInSeconds the time of the resume point
+ * @param totalTimeInSeconds the total time of the video
+ * @param playerState the player state
+ * @return True if resume point was set successfully, false otherwise.
+ */
+ virtual bool SetResumePoint(double timeInSeconds, double totalTimeInSeconds, const std::string &playerState);
+
+ std::string m_basePath; // the base path of the video, for folder-based lookups
+ int m_parentPathID; // the parent path id where the base path of the video lies
+ std::vector<std::string> m_director;
+ std::vector<std::string> m_writingCredits;
+ std::vector<std::string> m_genre;
+ std::vector<std::string> m_country;
+ std::string m_strTagLine;
+ std::string m_strPlotOutline;
+ std::string m_strTrailer;
+ std::string m_strPlot;
+ CScraperUrl m_strPictureURL;
+ std::string m_strTitle;
+ std::string m_strSortTitle;
+ std::vector<std::string> m_artist;
+ std::vector< SActorInfo > m_cast;
+ typedef std::vector< SActorInfo >::const_iterator iCast;
+ struct SetInfo //!< Struct holding information about a movie set
+ {
+ std::string title; //!< Title of the movie set
+ int id; //!< ID of movie set in database
+ std::string overview; //!< Overview/description of the movie set
+ };
+ SetInfo m_set; //!< Assigned movie set
+ std::vector<std::string> m_tags;
+ std::string m_strFile;
+ std::string m_strPath;
+ std::string m_strMPAARating;
+ std::string m_strFileNameAndPath;
+ std::string m_strOriginalTitle;
+ std::string m_strEpisodeGuide;
+ CDateTime m_premiered;
+ bool m_bHasPremiered;
+ std::string m_strStatus;
+ std::string m_strProductionCode;
+ CDateTime m_firstAired;
+ std::string m_strShowTitle;
+ std::vector<std::string> m_studio;
+ std::string m_strAlbum;
+ CDateTime m_lastPlayed;
+ std::vector<std::string> m_showLink;
+ std::map<int, std::string> m_namedSeasons;
+ int m_iTop250;
+ int m_year;
+ int m_iSeason;
+ int m_iEpisode;
+ int m_iIdUniqueID;
+ int m_iDbId;
+ int m_iFileId;
+ int m_iSpecialSortSeason;
+ int m_iSpecialSortEpisode;
+ int m_iTrack;
+ RatingMap m_ratings;
+ int m_iIdRating;
+ int m_iUserRating;
+ CBookmark m_EpBookmark;
+ int m_iBookmarkId;
+ int m_iIdShow;
+ int m_iIdSeason;
+ CFanart m_fanart;
+ CStreamDetails m_streamDetails;
+ CDateTime m_dateAdded;
+ MediaType m_type;
+ int m_relevance; // Used for actors' number of appearances
+ int m_parsedDetails;
+ std::vector<EmbeddedArtInfo> m_coverArt; ///< art information
+
+ // TODO: cannot be private, because of 'struct SDbTableOffsets'
+ unsigned int m_duration; ///< duration in seconds
+
+private:
+ /* \brief Parse our native XML format for video info.
+ See Load for a description of the available tag types.
+
+ \param element the root XML element to parse.
+ \param prioritise whether additive tags should be replaced (or prepended) by the content of the tags, or appended to.
+ \sa Load
+ */
+ void ParseNative(const TiXmlElement* element, bool prioritise);
+
+ std::string m_strDefaultRating;
+ std::string m_strDefaultUniqueID;
+ std::map<std::string, std::string> m_uniqueIDs;
+ std::string Trim(std::string &&value);
+ std::vector<std::string> Trim(std::vector<std::string> &&items);
+
+ int m_playCount;
+ CBookmark m_resumePoint;
+ static const int PLAYCOUNT_NOT_SET = -1;
+};
+
+typedef std::vector<CVideoInfoTag> VECMOVIES;
diff --git a/xbmc/video/VideoLibraryQueue.cpp b/xbmc/video/VideoLibraryQueue.cpp
new file mode 100644
index 0000000..65e65bb
--- /dev/null
+++ b/xbmc/video/VideoLibraryQueue.cpp
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2014-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 "VideoLibraryQueue.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "video/jobs/VideoLibraryCleaningJob.h"
+#include "video/jobs/VideoLibraryJob.h"
+#include "video/jobs/VideoLibraryMarkWatchedJob.h"
+#include "video/jobs/VideoLibraryRefreshingJob.h"
+#include "video/jobs/VideoLibraryResetResumePointJob.h"
+#include "video/jobs/VideoLibraryScanningJob.h"
+
+#include <mutex>
+#include <utility>
+
+CVideoLibraryQueue::CVideoLibraryQueue()
+ : CJobQueue(false, 1, CJob::PRIORITY_LOW),
+ m_jobs()
+{ }
+
+CVideoLibraryQueue::~CVideoLibraryQueue()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ m_jobs.clear();
+}
+
+CVideoLibraryQueue& CVideoLibraryQueue::GetInstance()
+{
+ static CVideoLibraryQueue s_instance;
+ return s_instance;
+}
+
+void CVideoLibraryQueue::ScanLibrary(const std::string& directory, bool scanAll /* = false */ , bool showProgress /* = true */)
+{
+ AddJob(new CVideoLibraryScanningJob(directory, scanAll, showProgress));
+}
+
+bool CVideoLibraryQueue::IsScanningLibrary() const
+{
+ // check if the library is being cleaned synchronously
+ if (m_cleaning)
+ return true;
+
+ // check if the library is being scanned asynchronously
+ VideoLibraryJobMap::const_iterator scanningJobs = m_jobs.find("VideoLibraryScanningJob");
+ if (scanningJobs != m_jobs.end() && !scanningJobs->second.empty())
+ return true;
+
+ // check if the library is being cleaned asynchronously
+ VideoLibraryJobMap::const_iterator cleaningJobs = m_jobs.find("VideoLibraryCleaningJob");
+ if (cleaningJobs != m_jobs.end() && !cleaningJobs->second.empty())
+ return true;
+
+ return false;
+}
+
+void CVideoLibraryQueue::StopLibraryScanning()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ VideoLibraryJobMap::const_iterator scanningJobs = m_jobs.find("VideoLibraryScanningJob");
+ if (scanningJobs == m_jobs.end())
+ return;
+
+ // get a copy of the scanning jobs because CancelJob() will modify m_scanningJobs
+ VideoLibraryJobs tmpScanningJobs(scanningJobs->second.begin(), scanningJobs->second.end());
+
+ // cancel all scanning jobs
+ for (VideoLibraryJobs::const_iterator job = tmpScanningJobs.begin(); job != tmpScanningJobs.end(); ++job)
+ CancelJob(*job);
+ Refresh();
+}
+
+bool CVideoLibraryQueue::CleanLibrary(const std::set<int>& paths /* = std::set<int>() */,
+ bool asynchronous /* = true */,
+ CGUIDialogProgressBarHandle* progressBar /* = NULL */)
+{
+ CVideoLibraryCleaningJob* cleaningJob = new CVideoLibraryCleaningJob(paths, progressBar);
+
+ if (asynchronous)
+ AddJob(cleaningJob);
+ else
+ {
+ // we can't perform a modal library cleaning if other jobs are running
+ if (IsRunning())
+ return false;
+
+ m_modal = true;
+ m_cleaning = true;
+ cleaningJob->DoWork();
+
+ delete cleaningJob;
+ m_cleaning = false;
+ m_modal = false;
+ Refresh();
+ }
+
+ return true;
+}
+
+bool CVideoLibraryQueue::CleanLibraryModal(const std::set<int>& paths /* = std::set<int>() */)
+{
+ // we can't perform a modal library cleaning if other jobs are running
+ if (IsRunning())
+ return false;
+
+ m_modal = true;
+ m_cleaning = true;
+ CVideoLibraryCleaningJob cleaningJob(paths, true);
+ cleaningJob.DoWork();
+ m_cleaning = false;
+ m_modal = false;
+ Refresh();
+
+ return true;
+}
+
+void CVideoLibraryQueue::RefreshItem(std::shared_ptr<CFileItem> item,
+ bool ignoreNfo /* = false */,
+ bool forceRefresh /* = true */,
+ bool refreshAll /* = false */,
+ const std::string& searchTitle /* = "" */)
+{
+ AddJob(new CVideoLibraryRefreshingJob(std::move(item), forceRefresh, refreshAll, ignoreNfo,
+ searchTitle));
+}
+
+bool CVideoLibraryQueue::RefreshItemModal(std::shared_ptr<CFileItem> item,
+ bool forceRefresh /* = true */,
+ bool refreshAll /* = false */)
+{
+ // we can't perform a modal item refresh if other jobs are running
+ if (IsRunning())
+ return false;
+
+ m_modal = true;
+ CVideoLibraryRefreshingJob refreshingJob(std::move(item), forceRefresh, refreshAll);
+
+ bool result = refreshingJob.DoModal();
+ m_modal = false;
+
+ return result;
+}
+
+void CVideoLibraryQueue::MarkAsWatched(const std::shared_ptr<CFileItem>& item, bool watched)
+{
+ if (item == NULL)
+ return;
+
+ AddJob(new CVideoLibraryMarkWatchedJob(item, watched));
+}
+
+void CVideoLibraryQueue::ResetResumePoint(const std::shared_ptr<CFileItem>& item)
+{
+ if (item == nullptr)
+ return;
+
+ AddJob(new CVideoLibraryResetResumePointJob(item));
+}
+
+void CVideoLibraryQueue::AddJob(CVideoLibraryJob *job)
+{
+ if (job == NULL)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ if (!CJobQueue::AddJob(job))
+ return;
+
+ // add the job to our list of queued/running jobs
+ std::string jobType = job->GetType();
+ VideoLibraryJobMap::iterator jobsIt = m_jobs.find(jobType);
+ if (jobsIt == m_jobs.end())
+ {
+ VideoLibraryJobs jobs;
+ jobs.insert(job);
+ m_jobs.insert(std::make_pair(jobType, jobs));
+ }
+ else
+ jobsIt->second.insert(job);
+}
+
+void CVideoLibraryQueue::CancelJob(CVideoLibraryJob *job)
+{
+ if (job == NULL)
+ return;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // remember the job type needed later because the job might be deleted
+ // in the call to CJobQueue::CancelJob()
+ std::string jobType;
+ if (job->GetType() != NULL)
+ jobType = job->GetType();
+
+ // check if the job supports cancellation and cancel it
+ if (job->CanBeCancelled())
+ job->Cancel();
+
+ // remove the job from the job queue
+ CJobQueue::CancelJob(job);
+
+ // remove the job from our list of queued/running jobs
+ VideoLibraryJobMap::iterator jobsIt = m_jobs.find(jobType);
+ if (jobsIt != m_jobs.end())
+ jobsIt->second.erase(job);
+}
+
+void CVideoLibraryQueue::CancelAllJobs()
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ CJobQueue::CancelJobs();
+
+ // remove all scanning jobs
+ m_jobs.clear();
+}
+
+bool CVideoLibraryQueue::IsRunning() const
+{
+ return CJobQueue::IsProcessing() || m_modal;
+}
+
+void CVideoLibraryQueue::Refresh()
+{
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CVideoLibraryQueue::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ if (success)
+ {
+ if (QueueEmpty())
+ Refresh();
+ }
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // remove the job from our list of queued/running jobs
+ VideoLibraryJobMap::iterator jobsIt = m_jobs.find(job->GetType());
+ if (jobsIt != m_jobs.end())
+ jobsIt->second.erase(static_cast<CVideoLibraryJob*>(job));
+ }
+
+ return CJobQueue::OnJobComplete(jobID, success, job);
+}
diff --git a/xbmc/video/VideoLibraryQueue.h b/xbmc/video/VideoLibraryQueue.h
new file mode 100644
index 0000000..875dc5d
--- /dev/null
+++ b/xbmc/video/VideoLibraryQueue.h
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2014-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 "utils/JobManager.h"
+
+#include <map>
+#include <memory>
+#include <set>
+
+class CFileItem;
+class CGUIDialogProgressBarHandle;
+class CVideoLibraryJob;
+
+/*!
+ \brief Queue for video library jobs.
+
+ The queue can only process a single job at any time and every job will be
+ executed at the lowest priority.
+ */
+class CVideoLibraryQueue : protected CJobQueue
+{
+public:
+ ~CVideoLibraryQueue() override;
+
+ /*!
+ \brief Gets the singleton instance of the video library queue.
+ */
+ static CVideoLibraryQueue& GetInstance();
+
+ /*!
+ \brief Enqueue a library scan job.
+
+ \param[in] directory Directory to scan
+ \param[in] scanAll Ignore exclude setting for items. Defaults to false
+ \param[in] showProgress Whether or not to show a progress dialog. Defaults to true
+ */
+ void ScanLibrary(const std::string& directory, bool scanAll = false, bool showProgress = true);
+
+ /*!
+ \brief Check if a library scan is in progress.
+
+ \return True if a scan is in progress, false otherwise
+ */
+ bool IsScanningLibrary() const;
+
+ /*!
+ \brief Stop and dequeue all scanning jobs.
+ */
+ void StopLibraryScanning();
+
+ /*!
+ \brief Enqueue a library cleaning job.
+
+ \param[in] paths Set with database IDs of paths to be cleaned
+ \param[in] asynchronous Run the clean job asynchronously. Defaults to true
+ \param[in] progressBar Progress bar to update in GUI. Defaults to NULL (no progress bar to update)
+ \return True if the video library cleaning job has started, false otherwise
+ */
+ bool CleanLibrary(const std::set<int>& paths = std::set<int>(),
+ bool asynchronous = true,
+ CGUIDialogProgressBarHandle* progressBar = NULL);
+
+ /*!
+ \brief Executes a library cleaning with a modal dialog.
+
+ \param[in] paths Set with database IDs of paths to be cleaned
+ \return True if the video library cleaning job has started, false otherwise
+ */
+ bool CleanLibraryModal(const std::set<int>& paths = std::set<int>());
+
+ /*!
+ \brief Enqueues a job to refresh the details of the given item.
+
+ \param[inout] item Video item to be refreshed
+ \param[in] ignoreNfo Whether or not to ignore local NFO files
+ \param[in] forceRefresh Whether to force a complete refresh (including NFO or internet lookup)
+ \param[in] refreshAll Whether to refresh all sub-items (in case of a tvshow)
+ \param[in] searchTitle Title to use for the search (instead of determining it from the item's filename/path)
+ */
+ void RefreshItem(std::shared_ptr<CFileItem> item,
+ bool ignoreNfo = false,
+ bool forceRefresh = true,
+ bool refreshAll = false,
+ const std::string& searchTitle = "");
+
+ /*!
+ \brief Refreshes the details of the given item with a modal dialog.
+
+ \param[inout] item Video item to be refreshed
+ \param[in] forceRefresh Whether to force a complete refresh (including NFO or internet lookup)
+ \param[in] refreshAll Whether to refresh all sub-items (in case of a tvshow)
+ \return True if the item has been successfully refreshed, false otherwise.
+ */
+ bool RefreshItemModal(std::shared_ptr<CFileItem> item,
+ bool forceRefresh = true,
+ bool refreshAll = false);
+
+ /*!
+ \brief Queue a watched status update job.
+
+ \param[in] item Item to update watched status for
+ \param[in] watched New watched status
+ */
+ void MarkAsWatched(const std::shared_ptr<CFileItem>& item, bool watched);
+
+ /*!
+ \brief Queue a reset resume point job.
+
+ \param[in] item Item to reset the resume point for
+ */
+ void ResetResumePoint(const std::shared_ptr<CFileItem>& item);
+
+ /*!
+ \brief Adds the given job to the queue.
+
+ \param[in] job Video library job to be queued.
+ */
+ void AddJob(CVideoLibraryJob *job);
+
+ /*!
+ \brief Cancels the given job and removes it from the queue.
+
+ \param[in] job Video library job to be canceled and removed from the queue.
+ */
+ void CancelJob(CVideoLibraryJob *job);
+
+ /*!
+ \brief Cancels all running and queued jobs.
+ */
+ void CancelAllJobs();
+
+ /*!
+ \brief Whether any jobs are running or not.
+ */
+ bool IsRunning() const;
+
+protected:
+ // implementation of IJobCallback
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ /*!
+ \brief Notifies all to refresh the current listings.
+ */
+ void Refresh();
+
+private:
+ CVideoLibraryQueue();
+ CVideoLibraryQueue(const CVideoLibraryQueue&) = delete;
+ CVideoLibraryQueue const& operator=(CVideoLibraryQueue const&) = delete;
+
+ typedef std::set<CVideoLibraryJob*> VideoLibraryJobs;
+ typedef std::map<std::string, VideoLibraryJobs> VideoLibraryJobMap;
+ VideoLibraryJobMap m_jobs;
+ CCriticalSection m_critical;
+
+ bool m_modal = false;
+ bool m_cleaning = false;
+};
diff --git a/xbmc/video/VideoThumbLoader.cpp b/xbmc/video/VideoThumbLoader.cpp
new file mode 100644
index 0000000..31ebe3e
--- /dev/null
+++ b/xbmc/video/VideoThumbLoader.cpp
@@ -0,0 +1,803 @@
+/*
+ * 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 "VideoThumbLoader.h"
+
+#include "FileItem.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "URL.h"
+#include "cores/VideoPlayer/DVDFileInfo.h"
+#include "cores/VideoSettings.h"
+#include "filesystem/Directory.h"
+#include "filesystem/DirectoryCache.h"
+#include "filesystem/StackDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/StereoscopicsManager.h"
+#include "music/MusicDatabase.h"
+#include "music/tags/MusicInfoTag.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingUtils.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/EmbeddedArt.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoInfoTag.h"
+#include "video/tags/VideoInfoTagLoaderFactory.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <utility>
+
+using namespace XFILE;
+using namespace VIDEO;
+
+CThumbExtractor::CThumbExtractor(const CFileItem& item,
+ const std::string& listpath,
+ bool thumb,
+ const std::string& target,
+ int64_t pos,
+ bool fillStreamDetails)
+ : m_target(target), m_listpath(listpath), m_item(item)
+{
+ m_thumb = thumb;
+ m_pos = pos;
+ m_fillStreamDetails = fillStreamDetails;
+
+ if (item.IsVideoDb() && item.HasVideoInfoTag())
+ m_item.SetPath(item.GetVideoInfoTag()->m_strFileNameAndPath);
+
+ if (m_item.IsStack())
+ m_item.SetPath(CStackDirectory::GetFirstStackedFile(m_item.GetPath()));
+}
+
+CThumbExtractor::~CThumbExtractor() = default;
+
+bool CThumbExtractor::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(),GetType()) == 0)
+ {
+ const CThumbExtractor* jobExtract = dynamic_cast<const CThumbExtractor*>(job);
+ if (jobExtract && jobExtract->m_listpath == m_listpath
+ && jobExtract->m_target == m_target)
+ return true;
+ }
+ return false;
+}
+
+bool CThumbExtractor::DoWork()
+{
+ if (m_item.IsLiveTV()
+ // Due to a pvr addon api design flaw (no support for multiple concurrent streams
+ // per addon instance), pvr recording thumbnail extraction does not work (reliably).
+ || URIUtils::IsPVRRecording(m_item.GetDynPath())
+ || URIUtils::IsUPnP(m_item.GetPath())
+ || URIUtils::IsBluray(m_item.GetPath())
+ || URIUtils::IsPlugin(m_item.GetDynPath()) // plugin path not fully resolved
+ || m_item.IsBDFile()
+ || m_item.IsDVD()
+ || m_item.IsDiscImage()
+ || m_item.IsDVDFile(false, true)
+ || m_item.IsInternetStream()
+ || m_item.IsDiscStub()
+ || m_item.IsPlayList())
+ return false;
+
+ // For HTTP/FTP we only allow extraction when on a LAN
+ if (URIUtils::IsRemote(m_item.GetPath()) &&
+ !URIUtils::IsOnLAN(m_item.GetPath()) &&
+ (URIUtils::IsFTP(m_item.GetPath()) ||
+ URIUtils::IsHTTP(m_item.GetPath())))
+ return false;
+
+ bool result=false;
+ if (m_thumb)
+ {
+ CLog::Log(LOGDEBUG, "{} - trying to extract thumb from video file {}", __FUNCTION__,
+ CURL::GetRedacted(m_item.GetPath()));
+ // construct the thumb cache file
+ CTextureDetails details;
+ details.file = CTextureCache::GetCacheFile(m_target) + ".jpg";
+ result = CDVDFileInfo::ExtractThumb(m_item, details, m_fillStreamDetails ? &m_item.GetVideoInfoTag()->m_streamDetails : nullptr, m_pos);
+ if (result)
+ {
+ CServiceBroker::GetTextureCache()->AddCachedTexture(m_target, details);
+ m_item.SetProperty("HasAutoThumb", true);
+ m_item.SetProperty("AutoThumbImage", m_target);
+ m_item.SetArt("thumb", m_target);
+
+ CVideoInfoTag* info = m_item.GetVideoInfoTag();
+ if (info->m_iDbId > 0 && !info->m_type.empty())
+ {
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ db.SetArtForItem(info->m_iDbId, info->m_type, "thumb", m_item.GetArt("thumb"));
+ db.Close();
+ }
+ }
+ }
+ }
+ else if (!m_item.IsPlugin() &&
+ (!m_item.HasVideoInfoTag() ||
+ !m_item.GetVideoInfoTag()->HasStreamDetails()))
+ {
+ // No tag or no details set, so extract them
+ CLog::Log(LOGDEBUG, "{} - trying to extract filestream details from video file {}",
+ __FUNCTION__, CURL::GetRedacted(m_item.GetPath()));
+ result = CDVDFileInfo::GetFileStreamDetails(&m_item);
+ }
+
+ if (result)
+ {
+ CVideoInfoTag* info = m_item.GetVideoInfoTag();
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ if (URIUtils::IsStack(m_listpath))
+ {
+ // Don't know the total time of the stack, so set duration to zero to avoid confusion
+ info->m_streamDetails.SetVideoDuration(0, 0);
+
+ // Restore original stack path
+ m_item.SetPath(m_listpath);
+ }
+
+ db.BeginTransaction();
+
+ if (info->m_iFileId < 0)
+ db.SetStreamDetailsForFile(info->m_streamDetails, !info->m_strFileNameAndPath.empty() ? info->m_strFileNameAndPath : m_item.GetPath());
+ else
+ db.SetStreamDetailsForFileId(info->m_streamDetails, info->m_iFileId);
+
+ // overwrite the runtime value if the one from streamdetails is available
+ if (info->m_iDbId > 0
+ && info->GetStaticDuration() != info->GetDuration())
+ {
+ info->SetDuration(info->GetDuration());
+
+ // store the updated information in the database
+ db.SetDetailsForItem(info->m_iDbId, info->m_type, *info, m_item.GetArt());
+ }
+
+ db.CommitTransaction();
+ db.Close();
+ }
+ return true;
+ }
+
+ return false;
+}
+
+CVideoThumbLoader::CVideoThumbLoader() :
+ CThumbLoader(), CJobQueue(true, 1, CJob::PRIORITY_LOW_PAUSABLE)
+{
+ m_videoDatabase = new CVideoDatabase();
+}
+
+CVideoThumbLoader::~CVideoThumbLoader()
+{
+ StopThread();
+ delete m_videoDatabase;
+}
+
+void CVideoThumbLoader::OnLoaderStart()
+{
+ m_videoDatabase->Open();
+ m_artCache.clear();
+ CThumbLoader::OnLoaderStart();
+}
+
+void CVideoThumbLoader::OnLoaderFinish()
+{
+ m_videoDatabase->Close();
+ m_artCache.clear();
+ CThumbLoader::OnLoaderFinish();
+}
+
+static void SetupRarOptions(CFileItem& item, const std::string& path)
+{
+ std::string path2(path);
+ if (item.IsVideoDb() && item.HasVideoInfoTag())
+ path2 = item.GetVideoInfoTag()->m_strFileNameAndPath;
+ CURL url(path2);
+ std::string opts = url.GetOptions();
+ if (opts.find("flags") != std::string::npos)
+ return;
+ if (opts.size())
+ opts += "&flags=8";
+ else
+ opts = "?flags=8";
+ url.SetOptions(opts);
+ if (item.IsVideoDb() && item.HasVideoInfoTag())
+ item.GetVideoInfoTag()->m_strFileNameAndPath = url.Get();
+ else
+ item.SetPath(url.Get());
+ g_directoryCache.ClearDirectory(url.GetWithoutFilename());
+}
+
+namespace
+{
+std::vector<std::string> GetSettingListAsString(const std::string& settingID)
+{
+ std::vector<CVariant> values =
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(settingID);
+ std::vector<std::string> result;
+ std::transform(values.begin(), values.end(), std::back_inserter(result),
+ [](const CVariant& s) { return s.asString(); });
+ return result;
+}
+
+const std::map<std::string, std::vector<std::string>> artTypeDefaults = {
+ {MediaTypeEpisode, {"thumb"}},
+ {MediaTypeTvShow, {"poster", "fanart", "banner"}},
+ {MediaTypeSeason, {"poster", "fanart", "banner"}},
+ {MediaTypeMovie, {"poster", "fanart"}},
+ {MediaTypeVideoCollection, {"poster", "fanart"}},
+ {MediaTypeMusicVideo, {"poster", "fanart"}},
+ {MediaTypeNone, { "poster", "fanart", "banner", "thumb" }},
+};
+
+const std::vector<std::string> artTypeDefaultsFallback = {};
+
+const std::vector<std::string>& GetArtTypeDefault(const std::string& mediaType)
+{
+ auto defaults = artTypeDefaults.find(mediaType);
+ if (defaults != artTypeDefaults.end())
+ return defaults->second;
+ return artTypeDefaultsFallback;
+}
+
+const std::map<std::string, std::string> artTypeSettings = {
+ {MediaTypeEpisode, CSettings::SETTING_VIDEOLIBRARY_EPISODEART_WHITELIST},
+ {MediaTypeTvShow, CSettings::SETTING_VIDEOLIBRARY_TVSHOWART_WHITELIST},
+ {MediaTypeSeason, CSettings::SETTING_VIDEOLIBRARY_TVSHOWART_WHITELIST},
+ {MediaTypeMovie, CSettings::SETTING_VIDEOLIBRARY_MOVIEART_WHITELIST},
+ {MediaTypeVideoCollection, CSettings::SETTING_VIDEOLIBRARY_MOVIEART_WHITELIST},
+ {MediaTypeMusicVideo, CSettings::SETTING_VIDEOLIBRARY_MUSICVIDEOART_WHITELIST},
+};
+} // namespace
+
+std::vector<std::string> CVideoThumbLoader::GetArtTypes(const std::string &type)
+{
+ int artworkLevel = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL);
+ if (artworkLevel == CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE)
+ {
+ return {};
+ }
+
+ std::vector<std::string> result = GetArtTypeDefault(type);
+ if (artworkLevel != CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_CUSTOM)
+ {
+ return result;
+ }
+
+ auto settings = artTypeSettings.find(type);
+ if (settings == artTypeSettings.end())
+ return result;
+
+ for (auto& artType : GetSettingListAsString(settings->second))
+ {
+ if (find(result.begin(), result.end(), artType) == result.end())
+ result.push_back(artType);
+ }
+
+ return result;
+}
+
+bool CVideoThumbLoader::IsValidArtType(const std::string& potentialArtType)
+{
+ return !potentialArtType.empty() && potentialArtType.length() <= 25 &&
+ std::find_if_not(
+ potentialArtType.begin(), potentialArtType.end(),
+ StringUtils::isasciialphanum
+ ) == potentialArtType.end();
+}
+
+bool CVideoThumbLoader::IsArtTypeInWhitelist(const std::string& artType, const std::vector<std::string>& whitelist, bool exact)
+{
+ // whitelist contains art "families", 'fanart' also matches 'fanart1', 'fanart2', and so on
+ std::string compareArtType = artType;
+ if (!exact)
+ StringUtils::TrimRight(compareArtType, "0123456789");
+
+ return std::find(whitelist.begin(), whitelist.end(), compareArtType) != whitelist.end();
+}
+
+/**
+ * Look for a thumbnail for pItem. If one does not exist, look for an autogenerated
+ * thumbnail. If that does not exist, attempt to autogenerate one. Finally, check
+ * for the existence of fanart and set properties accordingly.
+ * @return: true if pItem has been modified
+ */
+bool CVideoThumbLoader::LoadItem(CFileItem* pItem)
+{
+ bool result = LoadItemCached(pItem);
+ result |= LoadItemLookup(pItem);
+
+ return result;
+}
+
+bool CVideoThumbLoader::LoadItemCached(CFileItem* pItem)
+{
+ if (pItem->m_bIsShareOrDrive
+ || pItem->IsParentFolder())
+ return false;
+
+ m_videoDatabase->Open();
+
+ if (!pItem->HasVideoInfoTag() || !pItem->GetVideoInfoTag()->HasStreamDetails()) // no stream details
+ {
+ if ((pItem->HasVideoInfoTag() && pItem->GetVideoInfoTag()->m_iFileId >= 0) // file (or maybe folder) is in the database
+ || (!pItem->m_bIsFolder && pItem->IsVideo())) // Some other video file for which we haven't yet got any database details
+ {
+ if (m_videoDatabase->GetStreamDetails(*pItem))
+ pItem->SetInvalid();
+ }
+ }
+
+ // video db items normally have info in the database
+ if (pItem->HasVideoInfoTag() && !pItem->GetProperty("libraryartfilled").asBoolean())
+ {
+ FillLibraryArt(*pItem);
+
+ if (!pItem->GetVideoInfoTag()->m_type.empty() &&
+ pItem->GetVideoInfoTag()->m_type != MediaTypeMovie &&
+ pItem->GetVideoInfoTag()->m_type != MediaTypeTvShow &&
+ pItem->GetVideoInfoTag()->m_type != MediaTypeEpisode &&
+ pItem->GetVideoInfoTag()->m_type != MediaTypeMusicVideo)
+ {
+ m_videoDatabase->Close();
+ return true; // nothing else to be done
+ }
+ }
+
+ // if we have no art, look for it all
+ std::map<std::string, std::string> artwork = pItem->GetArt();
+ if (artwork.empty())
+ {
+ std::vector<std::string> artTypes = GetArtTypes(pItem->HasVideoInfoTag() ? pItem->GetVideoInfoTag()->m_type : "");
+ if (find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end())
+ artTypes.emplace_back("thumb"); // always look for "thumb" art for files
+ for (std::vector<std::string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
+ {
+ std::string type = *i;
+ std::string art = GetCachedImage(*pItem, type);
+ if (!art.empty())
+ artwork.insert(std::make_pair(type, art));
+ }
+ pItem->AppendArt(artwork);
+ }
+
+ m_videoDatabase->Close();
+
+ return true;
+}
+
+bool CVideoThumbLoader::LoadItemLookup(CFileItem* pItem)
+{
+ if (pItem->m_bIsShareOrDrive || pItem->IsParentFolder() || pItem->GetPath() == "add")
+ return false;
+
+ if (pItem->HasVideoInfoTag() &&
+ !pItem->GetVideoInfoTag()->m_type.empty() &&
+ pItem->GetVideoInfoTag()->m_type != MediaTypeMovie &&
+ pItem->GetVideoInfoTag()->m_type != MediaTypeTvShow &&
+ pItem->GetVideoInfoTag()->m_type != MediaTypeEpisode &&
+ pItem->GetVideoInfoTag()->m_type != MediaTypeMusicVideo)
+ return false; // Nothing to do here
+
+ DetectAndAddMissingItemData(*pItem);
+
+ m_videoDatabase->Open();
+
+ std::map<std::string, std::string> artwork = pItem->GetArt();
+ std::vector<std::string> artTypes = GetArtTypes(pItem->HasVideoInfoTag() ? pItem->GetVideoInfoTag()->m_type : "");
+ if (find(artTypes.begin(), artTypes.end(), "thumb") == artTypes.end())
+ artTypes.emplace_back("thumb"); // always look for "thumb" art for files
+ for (std::vector<std::string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
+ {
+ std::string type = *i;
+ if (!pItem->HasArt(type))
+ {
+ std::string art = GetLocalArt(*pItem, type, type=="fanart");
+ if (!art.empty()) // cache it
+ {
+ SetCachedImage(*pItem, type, art);
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(art);
+ artwork.insert(std::make_pair(type, art));
+ }
+ else
+ {
+ // If nothing was found, try embedded art
+ if (pItem->HasVideoInfoTag() && !pItem->GetVideoInfoTag()->m_coverArt.empty())
+ {
+ for (auto& it : pItem->GetVideoInfoTag()->m_coverArt)
+ {
+ if (it.m_type == type)
+ {
+ art = CTextureUtils::GetWrappedImageURL(pItem->GetPath(), "video_" + type);
+ artwork.insert(std::make_pair(type, art));
+ }
+ }
+ }
+ }
+ }
+ }
+ pItem->AppendArt(artwork);
+
+ // We can only extract flags/thumbs for file-like items
+ if (!pItem->m_bIsFolder && pItem->IsVideo())
+ {
+ // An auto-generated thumb may have been cached on a different device - check we have it here
+ std::string url = pItem->GetArt("thumb");
+ if (StringUtils::StartsWith(url, "image://video@") &&
+ !CServiceBroker::GetTextureCache()->HasCachedImage(url))
+ pItem->SetArt("thumb", "");
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (!pItem->HasArt("thumb"))
+ {
+ // create unique thumb for auto generated thumbs
+ std::string thumbURL = GetEmbeddedThumbURL(*pItem);
+ if (CServiceBroker::GetTextureCache()->HasCachedImage(thumbURL))
+ {
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumbURL);
+ pItem->SetProperty("HasAutoThumb", true);
+ pItem->SetProperty("AutoThumbImage", thumbURL);
+ pItem->SetArt("thumb", thumbURL);
+
+ if (pItem->HasVideoInfoTag())
+ {
+ // Item has cached autogen image but no art entry. Save it to db.
+ CVideoInfoTag* info = pItem->GetVideoInfoTag();
+ if (info->m_iDbId > 0 && !info->m_type.empty())
+ m_videoDatabase->SetArtForItem(info->m_iDbId, info->m_type, "thumb", thumbURL);
+ }
+ }
+ else if (settings->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTTHUMB) &&
+ settings->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS) &&
+ settings->GetInt(CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL) !=
+ CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE)
+ {
+ CFileItem item(*pItem);
+ std::string path(item.GetPath());
+ if (URIUtils::IsInRAR(item.GetPath()))
+ SetupRarOptions(item,path);
+
+ CThumbExtractor* extract = new CThumbExtractor(item, path, true, thumbURL);
+ AddJob(extract);
+
+ m_videoDatabase->Close();
+ return true;
+ }
+ }
+
+ // flag extraction
+ if (settings->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS) &&
+ (!pItem->HasVideoInfoTag() ||
+ !pItem->GetVideoInfoTag()->HasStreamDetails() ) )
+ {
+ CFileItem item(*pItem);
+ std::string path(item.GetPath());
+ if (URIUtils::IsInRAR(item.GetPath()))
+ SetupRarOptions(item,path);
+ CThumbExtractor* extract = new CThumbExtractor(item,path,false);
+ AddJob(extract);
+ }
+ }
+
+ m_videoDatabase->Close();
+ return true;
+}
+
+bool CVideoThumbLoader::FillLibraryArt(CFileItem &item)
+{
+ CVideoInfoTag &tag = *item.GetVideoInfoTag();
+ std::map<std::string, std::string> artwork;
+ // Video item can be an album - either a
+ // a) search result with full details including music library album id, or
+ // b) musicvideo album that needs matching to a music album, storing id as well as fetch art.
+ if (tag.m_type == MediaTypeAlbum)
+ {
+ int idAlbum = -1;
+ if (item.HasMusicInfoTag()) // Album is a search result
+ idAlbum = item.GetMusicInfoTag()->GetAlbumId();
+ CMusicDatabase database;
+ database.Open();
+ if (idAlbum < 0 && !tag.m_strAlbum.empty() &&
+ item.GetProperty("musicvideomediatype") == MediaTypeAlbum)
+ {
+ // Musicvideo album - try to match album in music db on artist(s) and album name.
+ // Get review if available and save the matching music library album id.
+ std::string strArtist = StringUtils::Join(
+ tag.m_artist,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator);
+ std::string strReview;
+ if (database.GetMatchingMusicVideoAlbum(
+ tag.m_strAlbum, strArtist, idAlbum, strReview))
+ {
+ item.SetProperty("album_musicid", idAlbum);
+ item.SetProperty("album_description", strReview);
+ }
+ }
+ // Get album art only (not related artist art)
+ if (database.GetArtForItem(idAlbum, MediaTypeAlbum, artwork))
+ item.SetArt(artwork);
+ database.Close();
+ }
+ else if (tag.m_type == "actor" && !tag.m_artist.empty() &&
+ item.GetProperty("musicvideomediatype") == MediaTypeArtist)
+ {
+ // Try to match artist in music db on name, get bio if available and fetch artist art
+ // Save the matching music library artist id.
+ CMusicDatabase database;
+ database.Open();
+ CArtist artist;
+ int idArtist = database.GetArtistByName(tag.m_artist[0]);
+ if (idArtist > 0)
+ {
+ database.GetArtist(idArtist, artist);
+ tag.m_strPlot = artist.strBiography;
+ item.SetProperty("artist_musicid", idArtist);
+ }
+ if (database.GetArtForItem(idArtist, MediaTypeArtist, artwork))
+ item.SetArt(artwork);
+ database.Close();
+ }
+
+ if (tag.m_iDbId > -1 && !tag.m_type.empty())
+ {
+ m_videoDatabase->Open();
+ if (m_videoDatabase->GetArtForItem(tag.m_iDbId, tag.m_type, artwork))
+ item.AppendArt(artwork);
+ else if (tag.m_type == "actor" && !tag.m_artist.empty() &&
+ item.GetProperty("musicvideomediatype") != MediaTypeArtist)
+ {
+ // Fallback to music library for actors without art
+ //! @todo Is m_artist set other than musicvideo? Remove this fallback if not.
+ CMusicDatabase database;
+ database.Open();
+ int idArtist = database.GetArtistByName(item.GetLabel());
+ if (database.GetArtForItem(idArtist, MediaTypeArtist, artwork))
+ item.SetArt(artwork);
+ database.Close();
+ }
+
+ if (tag.m_type == MediaTypeEpisode || tag.m_type == MediaTypeSeason)
+ {
+ // For episodes and seasons, we want to set fanart for that of the show
+ if (!item.HasArt("tvshow.fanart") && tag.m_iIdShow >= 0)
+ {
+ const ArtMap& artmap = GetArtFromCache(MediaTypeTvShow, tag.m_iIdShow);
+ if (!artmap.empty())
+ {
+ item.AppendArt(artmap, MediaTypeTvShow);
+ item.SetArtFallback("fanart", "tvshow.fanart");
+ item.SetArtFallback("tvshow.thumb", "tvshow.poster");
+ }
+ }
+
+ if (tag.m_type == MediaTypeEpisode && !item.HasArt("season.poster") && tag.m_iSeason > -1)
+ {
+ const ArtMap& artmap = GetArtFromCache(MediaTypeSeason, tag.m_iIdSeason);
+ if (!artmap.empty())
+ item.AppendArt(artmap, MediaTypeSeason);
+ }
+ }
+ else if (tag.m_type == MediaTypeMovie && tag.m_set.id >= 0 && !item.HasArt("set.fanart"))
+ {
+ const ArtMap& artmap = GetArtFromCache(MediaTypeVideoCollection, tag.m_set.id);
+ if (!artmap.empty())
+ item.AppendArt(artmap, MediaTypeVideoCollection);
+ }
+ m_videoDatabase->Close();
+ }
+ item.SetProperty("libraryartfilled", true);
+ return !item.GetArt().empty();
+}
+
+bool CVideoThumbLoader::FillThumb(CFileItem &item)
+{
+ if (item.HasArt("thumb"))
+ return true;
+ std::string thumb = GetCachedImage(item, "thumb");
+ if (thumb.empty())
+ {
+ thumb = GetLocalArt(item, "thumb");
+ if (!thumb.empty())
+ SetCachedImage(item, "thumb", thumb);
+ }
+ if (!thumb.empty())
+ item.SetArt("thumb", thumb);
+ else
+ {
+ // If nothing was found, try embedded art
+ if (item.HasVideoInfoTag() && !item.GetVideoInfoTag()->m_coverArt.empty())
+ {
+ for (auto& it : item.GetVideoInfoTag()->m_coverArt)
+ {
+ if (it.m_type == "thumb")
+ {
+ thumb = CTextureUtils::GetWrappedImageURL(item.GetPath(), "video_" + it.m_type);
+ item.SetArt(it.m_type, thumb);
+ }
+ }
+ }
+ }
+
+ return !thumb.empty();
+}
+
+std::string CVideoThumbLoader::GetLocalArt(const CFileItem &item, const std::string &type, bool checkFolder)
+{
+ if (item.SkipLocalArt())
+ return "";
+
+ /* Cache directory for (sub) folders with Curl("streamed") filesystems. We need to do this
+ else entering (new) directories from the app thread becomes much slower. This
+ is caused by the fact that Curl Stat/Exist() is really slow and that the
+ thumbloader thread accesses the streamed filesystem at the same time as the
+ app thread and the latter has to wait for it.
+ */
+ if (item.m_bIsFolder &&
+ (item.IsStreamedFilesystem() ||
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_cacheBufferMode ==
+ CACHE_BUFFER_MODE_ALL))
+ {
+ CFileItemList items; // Dummy list
+ CDirectory::GetDirectory(item.GetPath(), items, "", DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
+ }
+
+ std::string art;
+ if (!type.empty())
+ {
+ art = item.FindLocalArt(type + ".jpg", checkFolder);
+ if (art.empty())
+ art = item.FindLocalArt(type + ".png", checkFolder);
+ }
+ if (art.empty() && (type.empty() || type == "thumb"))
+ { // backward compatibility
+ art = item.FindLocalArt("", false);
+ if (art.empty() && (checkFolder || (item.m_bIsFolder && !item.IsFileFolder()) || item.IsOpticalMediaFile()))
+ { // try movie.tbn
+ art = item.FindLocalArt("movie.tbn", true);
+ if (art.empty()) // try folder.jpg
+ art = item.FindLocalArt("folder.jpg", true);
+ }
+ }
+
+ return art;
+}
+
+std::string CVideoThumbLoader::GetEmbeddedThumbURL(const CFileItem &item)
+{
+ std::string path(item.GetPath());
+ if (item.IsVideoDb() && item.HasVideoInfoTag())
+ path = item.GetVideoInfoTag()->m_strFileNameAndPath;
+ if (URIUtils::IsStack(path))
+ path = CStackDirectory::GetFirstStackedFile(path);
+
+ return CTextureUtils::GetWrappedImageURL(path, "video");
+}
+
+bool CVideoThumbLoader::GetEmbeddedThumb(const std::string& path,
+ const std::string& type, EmbeddedArt& art)
+{
+ CFileItem item(path, false);
+ std::unique_ptr<IVideoInfoTagLoader> pLoader;
+ pLoader.reset(CVideoInfoTagLoaderFactory::CreateLoader(item,ADDON::ScraperPtr(),false));
+ CVideoInfoTag tag;
+ std::vector<EmbeddedArt> artv;
+ if (pLoader)
+ pLoader->Load(tag, false, &artv);
+
+ for (const EmbeddedArt& it : artv)
+ {
+ if (it.m_type == type)
+ {
+ art = it;
+ break;
+ }
+ }
+
+ return !art.Empty();
+}
+
+void CVideoThumbLoader::OnJobComplete(unsigned int jobID, bool success, CJob* job)
+{
+ if (success)
+ {
+ CThumbExtractor* loader = static_cast<CThumbExtractor*>(job);
+ loader->m_item.SetPath(loader->m_listpath);
+
+ if (m_pObserver)
+ m_pObserver->OnItemLoaded(&loader->m_item);
+ CFileItemPtr pItem(new CFileItem(loader->m_item));
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, pItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+ CJobQueue::OnJobComplete(jobID, success, job);
+}
+
+void CVideoThumbLoader::DetectAndAddMissingItemData(CFileItem &item)
+{
+ if (item.m_bIsFolder) return;
+
+ if (item.HasVideoInfoTag())
+ {
+ CStreamDetails& details = item.GetVideoInfoTag()->m_streamDetails;
+
+ // add audio language properties
+ for (int i = 1; i <= details.GetAudioStreamCount(); i++)
+ {
+ std::string index = std::to_string(i);
+ item.SetProperty("AudioChannels." + index, details.GetAudioChannels(i));
+ item.SetProperty("AudioCodec." + index, details.GetAudioCodec(i).c_str());
+ item.SetProperty("AudioLanguage." + index, details.GetAudioLanguage(i).c_str());
+ }
+
+ // add subtitle language properties
+ for (int i = 1; i <= details.GetSubtitleStreamCount(); i++)
+ {
+ std::string index = std::to_string(i);
+ item.SetProperty("SubtitleLanguage." + index, details.GetSubtitleLanguage(i).c_str());
+ }
+ }
+
+ const CStereoscopicsManager &stereoscopicsManager = CServiceBroker::GetGUI()->GetStereoscopicsManager();
+
+ std::string stereoMode;
+
+ // detect stereomode for videos
+ if (item.HasVideoInfoTag())
+ stereoMode = item.GetVideoInfoTag()->m_streamDetails.GetStereoMode();
+
+ if (stereoMode.empty())
+ {
+ std::string path = item.GetPath();
+ if (item.IsVideoDb() && item.HasVideoInfoTag())
+ path = item.GetVideoInfoTag()->GetPath();
+
+ // check for custom stereomode setting in video settings
+ CVideoSettings itemVideoSettings;
+ m_videoDatabase->Open();
+ if (m_videoDatabase->GetVideoSettings(item, itemVideoSettings) && itemVideoSettings.m_StereoMode != RENDER_STEREO_MODE_OFF)
+ {
+ stereoMode = CStereoscopicsManager::ConvertGuiStereoModeToString(static_cast<RENDER_STEREO_MODE>(itemVideoSettings.m_StereoMode));
+ }
+ m_videoDatabase->Close();
+
+ // still empty, try grabbing from filename
+ //! @todo in case of too many false positives due to using the full path, extract the filename only using string utils
+ if (stereoMode.empty())
+ stereoMode = stereoscopicsManager.DetectStereoModeByString(path);
+ }
+
+ if (!stereoMode.empty())
+ item.SetProperty("stereomode", CStereoscopicsManager::NormalizeStereoMode(stereoMode));
+}
+
+const ArtMap& CVideoThumbLoader::GetArtFromCache(const std::string &mediaType, const int id)
+{
+ std::pair<MediaType, int> key = std::make_pair(mediaType, id);
+ auto it = m_artCache.find(key);
+ if (it == m_artCache.end())
+ {
+ ArtMap newart;
+ m_videoDatabase->GetArtForItem(id, mediaType, newart);
+ it = m_artCache.insert(std::make_pair(key, std::move(newart))).first;
+ }
+ return it->second;
+}
diff --git a/xbmc/video/VideoThumbLoader.h b/xbmc/video/VideoThumbLoader.h
new file mode 100644
index 0000000..3c2d9f0
--- /dev/null
+++ b/xbmc/video/VideoThumbLoader.h
@@ -0,0 +1,135 @@
+/*
+ * 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 "FileItem.h"
+#include "ThumbLoader.h"
+#include "utils/JobManager.h"
+
+#include <map>
+#include <vector>
+
+class CStreamDetails;
+class CVideoDatabase;
+class EmbeddedArt;
+
+using ArtMap = std::map<std::string, std::string>;
+using ArtCache = std::map<std::pair<MediaType, int>, ArtMap>;
+
+/*!
+ \ingroup thumbs,jobs
+ \brief Thumb extractor job class
+
+ Used by the CVideoThumbLoader to perform asynchronous generation of thumbs
+
+ \sa CVideoThumbLoader and CJob
+ */
+class CThumbExtractor : public CJob
+{
+public:
+ CThumbExtractor(const CFileItem& item, const std::string& listpath, bool thumb, const std::string& strTarget="", int64_t pos = -1, bool fillStreamDetails = true);
+ ~CThumbExtractor() override;
+
+ /*!
+ \brief Work function that extracts thumb.
+ */
+ bool DoWork() override;
+
+ const char* GetType() const override
+ {
+ return kJobTypeMediaFlags;
+ }
+
+ bool operator==(const CJob* job) const override;
+
+ std::string m_target; ///< thumbpath
+ std::string m_listpath; ///< path used in fileitem list
+ CFileItem m_item;
+ bool m_thumb; ///< extract thumb?
+ int64_t m_pos; ///< position to extract thumb from
+ bool m_fillStreamDetails; ///< fill in stream details?
+};
+
+class CVideoThumbLoader : public CThumbLoader, public CJobQueue
+{
+public:
+ CVideoThumbLoader();
+ ~CVideoThumbLoader() override;
+
+ void OnLoaderStart() override;
+ void OnLoaderFinish() override;
+
+ bool LoadItem(CFileItem* pItem) override;
+ bool LoadItemCached(CFileItem* pItem) override;
+ bool LoadItemLookup(CFileItem* pItem) override;
+
+ /*! \brief Fill the thumb of a video item
+ First uses a cached thumb from a previous run, then checks for a local thumb
+ and caches it for the next run
+ \param item the CFileItem object to fill
+ \return true if we fill the thumb, false otherwise
+ */
+ virtual bool FillThumb(CFileItem &item);
+
+ /*! \brief Find a particular art type for a given item, optionally checking at the folder level
+ \param item the CFileItem to search.
+ \param type the type of art to look for.
+ \param checkFolder whether to also check the folder level for files. Defaults to false.
+ \return the art file (if found), else empty.
+ */
+ static std::string GetLocalArt(const CFileItem &item, const std::string &type, bool checkFolder = false);
+
+ /*! \brief return the available art types for a given media type
+ \param type the type of media.
+ \return a vector of art types.
+ \sa GetLocalArt
+ */
+ static std::vector<std::string> GetArtTypes(const std::string &type);
+
+ static bool IsValidArtType(const std::string& potentialArtType);
+
+ static bool IsArtTypeInWhitelist(const std::string& artType, const std::vector<std::string>& whitelist, bool exact);
+
+ /*! \brief helper function to retrieve a thumb URL for embedded video thumbs
+ \param item a video CFileItem.
+ \return a URL for the embedded thumb.
+ */
+ static std::string GetEmbeddedThumbURL(const CFileItem &item);
+
+ /*! \brief helper function to fill the art for a video library item
+ \param item a video CFileItem
+ \return true if we fill art, false otherwise
+ */
+ bool FillLibraryArt(CFileItem &item) override;
+
+ /*!
+ \brief Callback from CThumbExtractor on completion of a generated image
+
+ Performs the callbacks and updates the GUI.
+
+ \sa CImageLoader, IJobCallback
+ */
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ static bool GetEmbeddedThumb(const std::string& path,
+ const std::string& type,
+ EmbeddedArt& art);
+
+protected:
+ CVideoDatabase *m_videoDatabase;
+ ArtCache m_artCache;
+
+ /*! \brief Tries to detect missing data/info from a file and adds those
+ \param item The CFileItem to process
+ \return void
+ */
+ void DetectAndAddMissingItemData(CFileItem &item);
+
+ const ArtMap& GetArtFromCache(const std::string &mediaType, const int id);
+};
diff --git a/xbmc/video/VideoUtils.cpp b/xbmc/video/VideoUtils.cpp
new file mode 100644
index 0000000..44609aa
--- /dev/null
+++ b/xbmc/video/VideoUtils.cpp
@@ -0,0 +1,702 @@
+/*
+ * Copyright (C) 2022 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 "VideoUtils.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "PartyModeManager.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "filesystem/Directory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/IRunnable.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoInfoTag.h"
+#include "view/GUIViewState.h"
+
+namespace
+{
+class CAsyncGetItemsForPlaylist : public IRunnable
+{
+public:
+ CAsyncGetItemsForPlaylist(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems)
+ : m_item(item),
+ m_resume(item->GetStartOffset() == STARTOFFSET_RESUME),
+ m_queuedItems(queuedItems)
+ {
+ }
+
+ ~CAsyncGetItemsForPlaylist() override = default;
+
+ void Run() override
+ {
+ // fast lookup is needed here
+ m_queuedItems.SetFastLookup(true);
+
+ GetItemsForPlaylist(m_item);
+ }
+
+private:
+ void GetItemsForPlaylist(const std::shared_ptr<CFileItem>& item);
+
+ const std::shared_ptr<CFileItem> m_item;
+ const bool m_resume{false};
+ CFileItemList& m_queuedItems;
+};
+
+SortDescription GetSortDescription(const CGUIViewState& state, const CFileItemList& items)
+{
+ SortDescription sortDescDate;
+
+ auto sortDescriptions = state.GetSortDescriptions();
+ for (auto& sortDescription : sortDescriptions)
+ {
+ if (sortDescription.sortBy == SortByEpisodeNumber)
+ {
+ // check whether at least one item has actually an episode number set
+ for (const auto& item : items)
+ {
+ if (item->HasVideoInfoTag() && item->GetVideoInfoTag()->m_iEpisode > 0)
+ {
+ // first choice for folders containig episodes
+ sortDescription.sortOrder = SortOrderAscending;
+ return sortDescription;
+ }
+ }
+ continue;
+ }
+ else if (sortDescription.sortBy == SortByYear)
+ {
+ // check whether at least one item has actually a year set
+ for (const auto& item : items)
+ {
+ if (item->HasVideoInfoTag() && item->GetVideoInfoTag()->HasYear())
+ {
+ // first choice for folders containing movies
+ sortDescription.sortOrder = SortOrderAscending;
+ return sortDescription;
+ }
+ }
+ }
+ else if (sortDescription.sortBy == SortByDate)
+ {
+ // check whether at least one item has actually a valid date set
+ for (const auto& item : items)
+ {
+ if (item->m_dateTime.IsValid())
+ {
+ // fallback, if neither ByEpisode nor ByYear is available
+ sortDescDate = sortDescription;
+ sortDescDate.sortOrder = SortOrderAscending;
+ break; // leave items loop. we can still find ByEpisode or ByYear. so, no return here.
+ }
+ }
+ }
+ }
+
+ if (sortDescDate.sortBy != SortByNone)
+ return sortDescDate;
+ else
+ return state.GetSortMethod(); // last resort
+}
+
+void CAsyncGetItemsForPlaylist::GetItemsForPlaylist(const std::shared_ptr<CFileItem>& item)
+{
+ if (item->IsParentFolder() || !item->CanQueue() || item->IsRAR() || item->IsZIP())
+ return;
+
+ if (item->m_bIsFolder)
+ {
+ // check if it's a folder with dvd or bluray files, then just add the relevant file
+ const std::string mediapath = item->GetOpticalMediaPath();
+ if (!mediapath.empty())
+ {
+ m_queuedItems.Add(std::make_shared<CFileItem>(mediapath, false));
+ return;
+ }
+
+ // Check if we add a locked share
+ if (!item->IsPVR() && item->m_bIsShareOrDrive)
+ {
+ if (!g_passwordManager.IsItemUnlocked(item.get(), "video"))
+ return;
+ }
+
+ CFileItemList items;
+ XFILE::CDirectory::GetDirectory(item->GetPath(), items, "", XFILE::DIR_FLAG_DEFAULTS);
+
+ int viewStateWindowId = WINDOW_VIDEO_NAV;
+ if (URIUtils::IsPVRRadioRecordingFileOrFolder(item->GetPath()))
+ viewStateWindowId = WINDOW_RADIO_RECORDINGS;
+ else if (URIUtils::IsPVRTVRecordingFileOrFolder(item->GetPath()))
+ viewStateWindowId = WINDOW_TV_RECORDINGS;
+
+ const std::unique_ptr<CGUIViewState> state(
+ CGUIViewState::GetViewState(viewStateWindowId, items));
+ if (state)
+ {
+ LABEL_MASKS labelMasks;
+ state->GetSortMethodLabelMasks(labelMasks);
+
+ const CLabelFormatter fileFormatter(labelMasks.m_strLabelFile, labelMasks.m_strLabel2File);
+ const CLabelFormatter folderFormatter(labelMasks.m_strLabelFolder,
+ labelMasks.m_strLabel2Folder);
+ for (const auto& i : items)
+ {
+ if (i->IsLabelPreformatted())
+ continue;
+
+ if (i->m_bIsFolder)
+ folderFormatter.FormatLabels(i.get());
+ else
+ fileFormatter.FormatLabels(i.get());
+ }
+
+ SortDescription sortDesc;
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == viewStateWindowId)
+ sortDesc = state->GetSortMethod();
+ else
+ sortDesc = GetSortDescription(*state, items);
+
+ if (sortDesc.sortBy == SortByLabel)
+ items.ClearSortState();
+
+ items.Sort(sortDesc);
+ }
+
+ if (items.GetContent().empty() && !items.IsVideoDb() && !items.IsVirtualDirectoryRoot() &&
+ !items.IsSourcesPath() && !items.IsLibraryFolder())
+ {
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ std::string content = db.GetContentForPath(items.GetPath());
+ if (content.empty() && !items.IsPlugin())
+ content = "files";
+
+ items.SetContent(content);
+ }
+ }
+
+ if (m_resume)
+ {
+ // put last played item at the begin of the playlist; add start offsets for videos
+ std::shared_ptr<CFileItem> lastPlayedItem;
+ CDateTime lastPlayed;
+ for (const auto& i : items)
+ {
+ if (!i->HasVideoInfoTag())
+ continue;
+
+ const auto videoTag = i->GetVideoInfoTag();
+
+ const CBookmark& bookmark = videoTag->GetResumePoint();
+ if (bookmark.IsSet())
+ {
+ i->SetStartOffset(CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds));
+
+ const CDateTime& currLastPlayed = videoTag->m_lastPlayed;
+ if (currLastPlayed.IsValid() && (!lastPlayed.IsValid() || (lastPlayed < currLastPlayed)))
+ {
+ lastPlayedItem = i;
+ lastPlayed = currLastPlayed;
+ }
+ }
+ }
+
+ if (lastPlayedItem)
+ {
+ items.Remove(lastPlayedItem.get());
+ items.AddFront(lastPlayedItem, 0);
+ }
+ }
+
+ int watchedMode;
+ if (m_resume)
+ watchedMode = WatchedModeUnwatched;
+ else
+ watchedMode = CMediaSettings::GetInstance().GetWatchedMode(items.GetContent());
+
+ const bool unwatchedOnly = watchedMode == WatchedModeUnwatched;
+ const bool watchedOnly = watchedMode == WatchedModeWatched;
+ bool fetchedPlayCounts = false;
+ for (const auto& i : items)
+ {
+ if (i->m_bIsFolder)
+ {
+ std::string path = i->GetPath();
+ URIUtils::RemoveSlashAtEnd(path);
+ if (StringUtils::EndsWithNoCase(path, "sample")) // skip sample folders
+ continue;
+ }
+ else
+ {
+ if (!fetchedPlayCounts &&
+ (!i->HasVideoInfoTag() || !i->GetVideoInfoTag()->IsPlayCountSet()))
+ {
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ fetchedPlayCounts = true;
+ db.GetPlayCounts(items.GetPath(), items);
+ }
+ }
+ if (i->HasVideoInfoTag() && i->GetVideoInfoTag()->IsPlayCountSet())
+ {
+ const int playCount = i->GetVideoInfoTag()->GetPlayCount();
+ if ((unwatchedOnly && playCount > 0) || (watchedOnly && playCount <= 0))
+ continue;
+ }
+ }
+ GetItemsForPlaylist(i);
+ }
+ }
+ else if (item->IsPlayList())
+ {
+ const std::unique_ptr<PLAYLIST::CPlayList> playList(PLAYLIST::CPlayListFactory::Create(*item));
+ if (!playList)
+ {
+ CLog::LogF(LOGERROR, "Failed to create playlist {}", item->GetPath());
+ return;
+ }
+
+ if (!playList->Load(item->GetPath()))
+ {
+ CLog::LogF(LOGERROR, "Failed to load playlist {}", item->GetPath());
+ return;
+ }
+
+ for (int i = 0; i < playList->size(); ++i)
+ {
+ GetItemsForPlaylist((*playList)[i]);
+ }
+ }
+ else if (item->IsInternetStream())
+ {
+ // just queue the internet stream, it will be expanded on play
+ m_queuedItems.Add(item);
+ }
+ else if (item->IsPlugin() && item->GetProperty("isplayable").asBoolean())
+ {
+ // a playable python files
+ m_queuedItems.Add(item);
+ }
+ else if (item->IsVideoDb())
+ {
+ // this case is needed unless we allow IsVideo() to return true for videodb items,
+ // but then we have issues with playlists of videodb items
+ const auto itemCopy = std::make_shared<CFileItem>(*item->GetVideoInfoTag());
+ itemCopy->SetStartOffset(item->GetStartOffset());
+ m_queuedItems.Add(itemCopy);
+ }
+ else if (!item->IsNFO() && item->IsVideo())
+ {
+ m_queuedItems.Add(item);
+ }
+}
+
+} // unnamed namespace
+
+namespace VIDEO_UTILS
+{
+void PlayItem(const std::shared_ptr<CFileItem>& itemIn)
+{
+ auto item = itemIn;
+
+ // Allow queuing of unqueueable items
+ // when we try to queue them directly
+ if (!itemIn->CanQueue())
+ {
+ // make a copy to not alter the original item
+ item = std::make_shared<CFileItem>(*itemIn);
+ item->SetCanQueue(true);
+ }
+
+ if (item->m_bIsFolder && !item->IsPlugin())
+ {
+ // recursively add items to list
+ CFileItemList queuedItems;
+ GetItemsForPlayList(item, queuedItems);
+
+ auto& player = CServiceBroker::GetPlaylistPlayer();
+ player.ClearPlaylist(PLAYLIST::TYPE_VIDEO);
+ player.Reset();
+ player.Add(PLAYLIST::TYPE_VIDEO, queuedItems);
+ player.SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ player.Play();
+ }
+ else if (item->HasVideoInfoTag())
+ {
+ // single item, play it
+ CServiceBroker::GetPlaylistPlayer().Play(item, "");
+ }
+}
+
+void QueueItem(const std::shared_ptr<CFileItem>& itemIn, QueuePosition pos)
+{
+ auto item = itemIn;
+
+ // Allow queuing of unqueueable items
+ // when we try to queue them directly
+ if (!itemIn->CanQueue())
+ {
+ // make a copy to not alter the original item
+ item = std::make_shared<CFileItem>(*itemIn);
+ item->SetCanQueue(true);
+ }
+
+ auto& player = CServiceBroker::GetPlaylistPlayer();
+ const auto& components = CServiceBroker::GetAppComponents();
+
+ // Determine the proper list to queue this element
+ PLAYLIST::Id playlistId = player.GetCurrentPlaylist();
+ if (playlistId == PLAYLIST::TYPE_NONE)
+ playlistId = components.GetComponent<CApplicationPlayer>()->GetPreferredPlaylist();
+
+ if (playlistId == PLAYLIST::TYPE_NONE)
+ playlistId = PLAYLIST::TYPE_VIDEO;
+
+ CFileItemList queuedItems;
+ GetItemsForPlayList(item, queuedItems);
+
+ // if party mode, add items but DONT start playing
+ if (g_partyModeManager.IsEnabled(PARTYMODECONTEXT_VIDEO))
+ {
+ g_partyModeManager.AddUserSongs(queuedItems, false);
+ return;
+ }
+
+ if (pos == QueuePosition::POSITION_BEGIN &&
+ components.GetComponent<CApplicationPlayer>()->IsPlaying())
+ player.Insert(playlistId, queuedItems, player.GetCurrentSong() + 1);
+ else
+ player.Add(playlistId, queuedItems);
+
+ player.SetCurrentPlaylist(playlistId);
+
+ // Note: video does not auto play on queue like music
+}
+
+bool GetItemsForPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems)
+{
+ CAsyncGetItemsForPlaylist getItems(item, queuedItems);
+ return CGUIDialogBusy::Wait(&getItems,
+ 500, // 500ms before busy dialog appears
+ true); // can be cancelled
+}
+
+bool IsItemPlayable(const CFileItem& item)
+{
+ if (item.IsParentFolder())
+ return false;
+
+ if (item.IsDeleted())
+ return false;
+
+ // Include all PVR recordings and recordings folders
+ if (URIUtils::IsPVRRecordingFileOrFolder(item.GetPath()))
+ return true;
+
+ // Include Live TV
+ if (!item.m_bIsFolder && (item.IsLiveTV() || item.IsEPG()))
+ return true;
+
+ // Exclude all music library items
+ if (item.IsMusicDb() || StringUtils::StartsWithNoCase(item.GetPath(), "library://music/"))
+ return false;
+
+ // Exclude other components
+ if (item.IsPlugin() || item.IsScript() || item.IsAddonsPath())
+ return false;
+
+ // Exclude unwanted windows
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_PLAYLIST)
+ return false;
+
+ // Exclude special items
+ if (StringUtils::StartsWithNoCase(item.GetPath(), "newsmartplaylist://") ||
+ StringUtils::StartsWithNoCase(item.GetPath(), "newplaylist://") ||
+ StringUtils::StartsWithNoCase(item.GetPath(), "newtag://"))
+ return false;
+
+ // Include playlists located at one of the possible video/mixed playlist locations
+ if (item.IsPlayList())
+ {
+ if (StringUtils::StartsWithNoCase(item.GetMimeType(), "video/"))
+ return true;
+
+ if (StringUtils::StartsWithNoCase(item.GetPath(), "special://videoplaylists/") ||
+ StringUtils::StartsWithNoCase(item.GetPath(), "special://profile/playlists/video/") ||
+ StringUtils::StartsWithNoCase(item.GetPath(), "special://profile/playlists/mixed/"))
+ return true;
+
+ // Has user changed default playlists location and the list is located there?
+ const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ std::string path = settings->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH);
+ StringUtils::TrimRight(path, "/");
+ if (StringUtils::StartsWith(item.GetPath(), StringUtils::Format("{}/video/", path)) ||
+ StringUtils::StartsWith(item.GetPath(), StringUtils::Format("{}/mixed/", path)))
+ return true;
+
+ if (!item.m_bIsFolder)
+ {
+ // Unknown location. Type cannot be determined for non-folder items.
+ return false;
+ }
+ }
+
+ if (item.m_bIsFolder &&
+ (item.IsVideoDb() || StringUtils::StartsWithNoCase(item.GetPath(), "library://video/")))
+ {
+ // Exclude top level nodes - eg can't play 'genres' just a specific genre etc
+ const XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE node =
+ XFILE::CVideoDatabaseDirectory::GetDirectoryParentType(item.GetPath());
+ if (node == XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_OVERVIEW ||
+ node == XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_MOVIES_OVERVIEW ||
+ node == XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_TVSHOWS_OVERVIEW ||
+ node == XFILE::VIDEODATABASEDIRECTORY::NODE_TYPE_MUSICVIDEOS_OVERVIEW)
+ return false;
+
+ return true;
+ }
+
+ if (item.HasVideoInfoTag() && item.CanQueue())
+ {
+ return true;
+ }
+ else if ((!item.m_bIsFolder && item.IsVideo()) || item.IsDVD() || item.IsCDDA())
+ {
+ return true;
+ }
+ else if (item.m_bIsFolder)
+ {
+ // Not a video-specific folder (like file:// or nfs://). Allow play if context is Video window.
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_NAV &&
+ item.GetPath() != "add") // Exclude "Add video source" item
+ return true;
+ }
+
+ return false;
+}
+
+namespace
+{
+bool HasInProgressVideo(const std::string& path, CVideoDatabase& db)
+{
+ //! @todo this function is really very expensive and should be optimized (at db level).
+
+ CFileItemList items;
+ CUtil::GetRecursiveListing(path, items, {}, XFILE::DIR_FLAG_DEFAULTS);
+
+ if (items.IsEmpty())
+ return false;
+
+ for (const auto& item : items)
+ {
+ const auto videoTag = item->GetVideoInfoTag();
+ if (!item->HasVideoInfoTag())
+ continue;
+
+ if (videoTag->GetPlayCount() > 0)
+ continue;
+
+ // get resume point
+ CBookmark bookmark(videoTag->GetResumePoint());
+ if (!bookmark.IsSet() && db.GetResumeBookMark(videoTag->m_strFileNameAndPath, bookmark))
+ videoTag->SetResumePoint(bookmark);
+
+ if (bookmark.IsSet())
+ return true;
+ }
+
+ return false;
+}
+
+ResumeInformation GetFolderItemResumeInformation(const CFileItem& item)
+{
+ if (!item.m_bIsFolder)
+ return {};
+
+ bool hasInProgressVideo = false;
+
+ CFileItem folderItem(item);
+ if ((!folderItem.HasProperty("watchedepisodes") || // season/show
+ (folderItem.GetProperty("watchedepisodes").asInteger() == 0)) &&
+ (!folderItem.HasProperty("watched") || // movie set
+ (folderItem.GetProperty("watched").asInteger() == 0)))
+ {
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ if (!folderItem.HasProperty("watchedepisodes") && !folderItem.HasProperty("watched"))
+ {
+ XFILE::VIDEODATABASEDIRECTORY::CQueryParams params;
+ XFILE::VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(item.GetPath(), params);
+
+ if (params.GetTvShowId() >= 0)
+ {
+ if (params.GetSeason() >= 0)
+ {
+ const int idSeason = db.GetSeasonId(static_cast<int>(params.GetTvShowId()),
+ static_cast<int>(params.GetSeason()));
+ if (idSeason >= 0)
+ {
+ CVideoInfoTag details;
+ db.GetSeasonInfo(idSeason, details, &folderItem);
+ }
+ }
+ else
+ {
+ CVideoInfoTag details;
+ db.GetTvShowInfo(item.GetPath(), details, static_cast<int>(params.GetTvShowId()),
+ &folderItem);
+ }
+ }
+ else if (params.GetSetId() >= 0)
+ {
+ CVideoInfoTag details;
+ db.GetSetInfo(static_cast<int>(params.GetSetId()), details, &folderItem);
+ }
+ }
+
+ // no episodes/movies watched completely, but there could be some or more we have
+ // started watching
+ if ((folderItem.HasProperty("watchedepisodes") && // season/show
+ folderItem.GetProperty("watchedepisodes").asInteger() == 0) ||
+ (folderItem.HasProperty("watched") && // movie set
+ folderItem.GetProperty("watched").asInteger() == 0))
+ hasInProgressVideo = HasInProgressVideo(item.GetPath(), db);
+
+ db.Close();
+ }
+ }
+
+ if (hasInProgressVideo ||
+ (folderItem.GetProperty("watchedepisodes").asInteger() > 0 &&
+ folderItem.GetProperty("unwatchedepisodes").asInteger() > 0) ||
+ (folderItem.GetProperty("watched").asInteger() > 0 &&
+ folderItem.GetProperty("unwatched").asInteger() > 0))
+ {
+ ResumeInformation resumeInfo;
+ resumeInfo.isResumable = true;
+ return resumeInfo;
+ }
+ return {};
+}
+
+ResumeInformation GetNonFolderItemResumeInformation(const CFileItem& item)
+{
+ if (!item.IsResumable())
+ return {};
+
+ // do not resume Live TV and 'deleted' items (e.g. trashed pvr recordings)
+ if (item.IsLiveTV() || item.IsDeleted())
+ return {};
+
+ ResumeInformation resumeInfo;
+
+ if (item.GetCurrentResumeTimeAndPartNumber(resumeInfo.startOffset, resumeInfo.partNumber))
+ {
+ if (resumeInfo.startOffset > 0)
+ {
+ resumeInfo.startOffset = CUtil::ConvertSecsToMilliSecs(resumeInfo.startOffset);
+ resumeInfo.isResumable = true;
+ }
+ }
+ else
+ {
+ // Obtain the resume bookmark from video db...
+
+ CVideoDatabase db;
+ if (!db.Open())
+ {
+ CLog::LogF(LOGERROR, "Cannot open VideoDatabase");
+ return {};
+ }
+
+ std::string path = item.GetPath();
+ if (item.IsVideoDb() || item.IsDVD())
+ {
+ if (item.HasVideoInfoTag())
+ {
+ path = item.GetVideoInfoTag()->m_strFileNameAndPath;
+ }
+ else if (item.IsVideoDb())
+ {
+ // Obtain path+filename from video db
+ XFILE::VIDEODATABASEDIRECTORY::CQueryParams params;
+ XFILE::VIDEODATABASEDIRECTORY::CDirectoryNode::GetDatabaseInfo(item.GetPath(), params);
+
+ long id = -1;
+ VideoDbContentType content_type;
+ if ((id = params.GetMovieId()) >= 0)
+ content_type = VideoDbContentType::MOVIES;
+ else if ((id = params.GetEpisodeId()) >= 0)
+ content_type = VideoDbContentType::EPISODES;
+ else if ((id = params.GetMVideoId()) >= 0)
+ content_type = VideoDbContentType::MUSICVIDEOS;
+ else
+ {
+ CLog::LogF(LOGERROR, "Cannot obtain video content type");
+ db.Close();
+ return {};
+ }
+
+ db.GetFilePathById(static_cast<int>(id), path, content_type);
+ }
+ else
+ {
+ // DVD
+ CLog::LogF(LOGERROR, "Cannot obtain bookmark for DVD");
+ db.Close();
+ return {};
+ }
+ }
+
+ CBookmark bookmark;
+ db.GetResumeBookMark(path, bookmark);
+ db.Close();
+
+ if (bookmark.IsSet())
+ {
+ resumeInfo.isResumable = bookmark.IsPartWay();
+ resumeInfo.startOffset = CUtil::ConvertSecsToMilliSecs(bookmark.timeInSeconds);
+ resumeInfo.partNumber = static_cast<int>(bookmark.partNumber);
+ }
+ }
+ return resumeInfo;
+}
+
+} // unnamed namespace
+
+ResumeInformation GetItemResumeInformation(const CFileItem& item)
+{
+ ResumeInformation info = GetNonFolderItemResumeInformation(item);
+ if (info.isResumable)
+ return info;
+
+ return GetFolderItemResumeInformation(item);
+}
+
+} // namespace VIDEO_UTILS
diff --git a/xbmc/video/VideoUtils.h b/xbmc/video/VideoUtils.h
new file mode 100644
index 0000000..d82df47
--- /dev/null
+++ b/xbmc/video/VideoUtils.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022 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 <memory>
+
+class CFileItem;
+class CFileItemList;
+
+namespace VIDEO_UTILS
+{
+/*! \brief Start playback of the given item. If the item is a folder, build a playlist with
+ all items contained in the folder and start playback of the playlist. If item is a single video
+ item, start playback directly, without adding it to the video playlist first.
+ \param item [in] the item to play
+ */
+void PlayItem(const std::shared_ptr<CFileItem>& item);
+
+enum class QueuePosition
+{
+ POSITION_BEGIN, // place at begin of queue, before other items
+ POSITION_END, // place at end of queue, after other items
+};
+
+/*! \brief Queue the given item in the currently active playlist. If no playlist is active,
+ put the item into the video playlist.
+ \param item [in] the item to queue
+ \param pos [in] whether to place the item and the begin or the end of the queue
+ */
+void QueueItem(const std::shared_ptr<CFileItem>& item, QueuePosition pos);
+
+/*! \brief For a given item, get the items to put in a playlist. If the item is a folder, all
++ subitems will be added recursively to the returned item list. If the item is a playlist, the
++ playlist will be loaded and contained items will be added to the returned item list. Shows a
++ busy dialog if action takes certain amount of time to give the user visual feedback.
++ \param item [in] the item to add to the playlist
++ \param queuedItems [out] the items that can be put in a play list
++ \return true on success, false otherwise
++ */
+bool GetItemsForPlayList(const std::shared_ptr<CFileItem>& item, CFileItemList& queuedItems);
+
+/*!
+ \brief Check whether the given item can be played by the app playlist player as one or more videos.
+ \param item The item to check
+ \return True if playable, false otherwise.
+ */
+bool IsItemPlayable(const CFileItem& item);
+
+struct ResumeInformation
+{
+ bool isResumable{false}; // the playback of the item can be resumed
+ int64_t startOffset{0}; // a start offset
+ int partNumber{0}; // a part number
+};
+
+/*!
+ \brief Check whether playback of the given item can be resumed, get detailed information.
+ \param item The item to retrieve information for
+ \return The resume information.
+ */
+ResumeInformation GetItemResumeInformation(const CFileItem& item);
+
+} // namespace VIDEO_UTILS
diff --git a/xbmc/video/ViewModeSettings.cpp b/xbmc/video/ViewModeSettings.cpp
new file mode 100644
index 0000000..6c8a919
--- /dev/null
+++ b/xbmc/video/ViewModeSettings.cpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2016-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 "ViewModeSettings.h"
+
+#include "cores/VideoSettings.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/lib/SettingDefinitions.h"
+
+struct ViewModeProperties
+{
+ int stringIndex;
+ int viewMode;
+ bool hideFromQuickCycle = false;
+ bool hideFromList = false;
+};
+
+#define HIDE_ITEM true
+
+/** The list of all the view modes along with their properties
+ */
+static const ViewModeProperties viewModes[] =
+{
+ { 630, ViewModeNormal },
+ { 631, ViewModeZoom },
+ { 39008, ViewModeZoom120Width },
+ { 39009, ViewModeZoom110Width },
+ { 632, ViewModeStretch4x3 },
+ { 633, ViewModeWideZoom },
+ { 634, ViewModeStretch16x9 },
+ { 644, ViewModeStretch16x9Nonlin, HIDE_ITEM, HIDE_ITEM },
+ { 635, ViewModeOriginal },
+ { 636, ViewModeCustom, HIDE_ITEM }
+};
+
+#define NUMBER_OF_VIEW_MODES (sizeof(viewModes) / sizeof(viewModes[0]))
+
+/** Gets the index of a view mode
+ *
+ * @param viewMode The view mode
+ * @return The index of the view mode in the viewModes array
+ */
+static int GetViewModeIndex(int viewMode)
+{
+ size_t i;
+
+ // Find the current view mode
+ for (i = 0; i < NUMBER_OF_VIEW_MODES; i++)
+ {
+ if (viewModes[i].viewMode == viewMode)
+ return i;
+ }
+
+ return 0; // An invalid view mode will always return the first view mode
+}
+
+/** Gets the next view mode for quick cycling through the modes
+ *
+ * @param viewMode The current view mode
+ * @return The next view mode
+ */
+int CViewModeSettings::GetNextQuickCycleViewMode(int viewMode)
+{
+ // Find the next quick cycle view mode
+ for (size_t i = GetViewModeIndex(viewMode) + 1; i < NUMBER_OF_VIEW_MODES; i++)
+ {
+ if (!viewModes[i].hideFromQuickCycle)
+ return viewModes[i].viewMode;
+ }
+
+ return ViewModeNormal;
+}
+
+/** Gets the string index for the view mode
+ *
+ * @param viewMode The current view mode
+ * @return The string index
+ */
+int CViewModeSettings::GetViewModeStringIndex(int viewMode)
+{
+ return viewModes[GetViewModeIndex(viewMode)].stringIndex;
+}
+
+/** Fills the list with all visible view modes
+ */
+void CViewModeSettings::ViewModesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ // Add all appropriate view modes to the list control
+ for (const auto &item : viewModes)
+ {
+ if (!item.hideFromList)
+ list.emplace_back(g_localizeStrings.Get(item.stringIndex), item.viewMode);
+ }
+}
diff --git a/xbmc/video/ViewModeSettings.h b/xbmc/video/ViewModeSettings.h
new file mode 100644
index 0000000..b43ed85
--- /dev/null
+++ b/xbmc/video/ViewModeSettings.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016-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/Setting.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+struct IntegerSettingOption;
+
+class CViewModeSettings
+{
+private:
+ CViewModeSettings();
+ ~CViewModeSettings() = default;
+
+public:
+ /** Gets the next view mode for quick cycling through the modes
+ *
+ * @param viewMode The current view mode
+ * @return The next view mode
+ */
+ static int GetNextQuickCycleViewMode(int viewMode);
+
+ /** Gets the string index for the view mode
+ *
+ * @param viewMode The current view mode
+ * @return The string index
+ */
+ static int GetViewModeStringIndex(int viewMode);
+
+ /** Fills the list with all visible view modes
+ */
+ static void ViewModesFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+};
diff --git a/xbmc/video/dialogs/CMakeLists.txt b/xbmc/video/dialogs/CMakeLists.txt
new file mode 100644
index 0000000..b2ec11f
--- /dev/null
+++ b/xbmc/video/dialogs/CMakeLists.txt
@@ -0,0 +1,26 @@
+set(SOURCES GUIDialogAudioSettings.cpp
+ GUIDialogFullScreenInfo.cpp
+ GUIDialogSubtitles.cpp
+ GUIDialogSubtitleSettings.cpp
+ GUIDialogTeletext.cpp
+ GUIDialogVideoBookmarks.cpp
+ GUIDialogVideoInfo.cpp
+ GUIDialogVideoOSD.cpp
+ GUIDialogVideoSettings.cpp)
+
+set(HEADERS GUIDialogAudioSettings.h
+ GUIDialogFullScreenInfo.h
+ GUIDialogSubtitles.h
+ GUIDialogSubtitleSettings.h
+ GUIDialogTeletext.h
+ GUIDialogVideoBookmarks.h
+ GUIDialogVideoInfo.h
+ GUIDialogVideoOSD.h
+ GUIDialogVideoSettings.h)
+
+if(OPENGL_FOUND OR CORE_SYSTEM_NAME STREQUAL windows OR CORE_SYSTEM_NAME STREQUAL windowsstore)
+ list(APPEND SOURCES GUIDialogCMSSettings.cpp)
+ list(APPEND HEADERS GUIDialogCMSSettings.h)
+endif()
+
+core_add_library(video_dialogs)
diff --git a/xbmc/video/dialogs/GUIDialogAudioSettings.cpp b/xbmc/video/dialogs/GUIDialogAudioSettings.cpp
new file mode 100644
index 0000000..83b888b
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogAudioSettings.cpp
@@ -0,0 +1,437 @@
+/*
+ * 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 "GUIDialogAudioSettings.h"
+
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "application/ApplicationVolumeHandling.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "cores/IPlayer.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <string>
+#include <vector>
+
+#define SETTING_AUDIO_VOLUME "audio.volume"
+#define SETTING_AUDIO_VOLUME_AMPLIFICATION "audio.volumeamplification"
+#define SETTING_AUDIO_CENTERMIXLEVEL "audio.centermixlevel"
+#define SETTING_AUDIO_DELAY "audio.delay"
+#define SETTING_AUDIO_STREAM "audio.stream"
+#define SETTING_AUDIO_PASSTHROUGH "audio.digitalanalog"
+#define SETTING_AUDIO_MAKE_DEFAULT "audio.makedefault"
+
+CGUIDialogAudioSettings::CGUIDialogAudioSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_AUDIO_OSD_SETTINGS, "DialogSettings.xml")
+{ }
+
+CGUIDialogAudioSettings::~CGUIDialogAudioSettings() = default;
+
+void CGUIDialogAudioSettings::FrameMove()
+{
+ // update the volume setting if necessary
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ float newVolume = appVolume->GetVolumeRatio();
+ if (newVolume != m_volume)
+ GetSettingsManager()->SetNumber(SETTING_AUDIO_VOLUME, static_cast<double>(newVolume));
+
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->HasPlayer())
+ {
+ const CVideoSettings videoSettings = appPlayer->GetVideoSettings();
+
+ // these settings can change on the fly
+ //! @todo (needs special handling): m_settingsManager->SetInt(SETTING_AUDIO_STREAM, g_application.GetAppPlayer().GetAudioStream());
+ GetSettingsManager()->SetNumber(SETTING_AUDIO_DELAY,
+ static_cast<double>(videoSettings.m_AudioDelay));
+ GetSettingsManager()->SetBool(SETTING_AUDIO_PASSTHROUGH, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH));
+ }
+
+ CGUIDialogSettingsManualBase::FrameMove();
+}
+
+std::string CGUIDialogAudioSettings::FormatDelay(float value, float interval)
+{
+ if (fabs(value) < 0.5f * interval)
+ return StringUtils::Format(g_localizeStrings.Get(22003), 0.0);
+ if (value < 0)
+ return StringUtils::Format(g_localizeStrings.Get(22004), fabs(value));
+
+ return StringUtils::Format(g_localizeStrings.Get(22005), value);
+}
+
+std::string CGUIDialogAudioSettings::FormatDecibel(float value)
+{
+ return StringUtils::Format(g_localizeStrings.Get(14054), value);
+}
+
+std::string CGUIDialogAudioSettings::FormatPercentAsDecibel(float value)
+{
+ return StringUtils::Format(g_localizeStrings.Get(14054), CAEUtil::PercentToGain(value));
+}
+
+void CGUIDialogAudioSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_AUDIO_VOLUME)
+ {
+ m_volume = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue());
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ appVolume->SetVolume(m_volume, false); // false - value is not in percent
+ }
+ else if (settingId == SETTING_AUDIO_VOLUME_AMPLIFICATION)
+ {
+ float value = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue());
+ appPlayer->SetDynamicRangeCompression((long)(value * 100));
+ }
+ else if (settingId == SETTING_AUDIO_CENTERMIXLEVEL)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_CenterMixLevel = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_AUDIO_DELAY)
+ {
+ float value = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue());
+ appPlayer->SetAVDelay(value);
+ }
+ else if (settingId == SETTING_AUDIO_STREAM)
+ {
+ m_audioStream = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ // only change the audio stream if a different one has been asked for
+ if (appPlayer->GetAudioStream() != m_audioStream)
+ {
+ appPlayer->SetAudioStream(m_audioStream); // Set the audio stream to the one selected
+ }
+ }
+ else if (settingId == SETTING_AUDIO_PASSTHROUGH)
+ {
+ m_passthrough = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetBool(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH, m_passthrough);
+ }
+}
+
+void CGUIDialogAudioSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_AUDIO_MAKE_DEFAULT)
+ Save();
+}
+
+bool CGUIDialogAudioSettings::Save()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (!g_passwordManager.CheckSettingLevelLock(SettingLevel::Expert) &&
+ profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE)
+ return true;
+
+ // prompt user if they are sure
+ if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{12376}, CVariant{12377}))
+ return true;
+
+ // reset the settings
+ CVideoDatabase db;
+ if (!db.Open())
+ return true;
+
+ db.EraseAllVideoSettings();
+ db.Close();
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ CMediaSettings::GetInstance().GetDefaultVideoSettings() = appPlayer->GetVideoSettings();
+ CMediaSettings::GetInstance().GetDefaultVideoSettings().m_AudioStream = -1;
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ return true;
+}
+
+void CGUIDialogAudioSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ SetHeading(13396);
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_OKAY_BUTTON);
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 15067);
+}
+
+void CGUIDialogAudioSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("audiosubtitlesettings", -1);
+ if (category == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogAudioSettings: unable to setup settings");
+ return;
+ }
+
+ // get all necessary setting groups
+ const std::shared_ptr<CSettingGroup> groupAudio = AddGroup(category);
+ if (groupAudio == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogAudioSettings: unable to setup settings");
+ return;
+ }
+ const std::shared_ptr<CSettingGroup> groupSubtitles = AddGroup(category);
+ if (groupSubtitles == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogAudioSettings: unable to setup settings");
+ return;
+ }
+ const std::shared_ptr<CSettingGroup> groupSaveAsDefault = AddGroup(category);
+ if (groupSaveAsDefault == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogAudioSettings: unable to setup settings");
+ return;
+ }
+
+ bool usePopup = g_SkinInfo->HasSkinFile("DialogSlider.xml");
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ const CVideoSettings videoSettings = appPlayer->GetVideoSettings();
+ if (appPlayer->HasPlayer())
+ {
+ appPlayer->GetAudioCapabilities(m_audioCaps);
+ }
+
+ // register IsPlayingPassthrough condition
+ GetSettingsManager()->AddDynamicCondition("IsPlayingPassthrough", IsPlayingPassthrough);
+
+ CSettingDependency dependencyAudioOutputPassthroughDisabled(SettingDependencyType::Enable, GetSettingsManager());
+ dependencyAudioOutputPassthroughDisabled.Or()
+ ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_AUDIO_PASSTHROUGH, "false", SettingDependencyOperator::Equals, false, GetSettingsManager())))
+ ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition("IsPlayingPassthrough", "", "", true, GetSettingsManager())));
+ SettingDependencies depsAudioOutputPassthroughDisabled;
+ depsAudioOutputPassthroughDisabled.push_back(dependencyAudioOutputPassthroughDisabled);
+
+ // audio settings
+ // audio volume setting
+ const auto appVolume = components.GetComponent<CApplicationVolumeHandling>();
+ m_volume = appVolume->GetVolumeRatio();
+ std::shared_ptr<CSettingNumber> settingAudioVolume =
+ AddSlider(groupAudio, SETTING_AUDIO_VOLUME, 13376, SettingLevel::Basic, m_volume, 14054,
+ CApplicationVolumeHandling::VOLUME_MINIMUM,
+ CApplicationVolumeHandling::VOLUME_MAXIMUM / 100.0f,
+ CApplicationVolumeHandling::VOLUME_MAXIMUM);
+ settingAudioVolume->SetDependencies(depsAudioOutputPassthroughDisabled);
+ std::static_pointer_cast<CSettingControlSlider>(settingAudioVolume->GetControl())->SetFormatter(SettingFormatterPercentAsDecibel);
+
+ // audio volume amplification setting
+ if (SupportsAudioFeature(IPC_AUD_AMP))
+ {
+ std::shared_ptr<CSettingNumber> settingAudioVolumeAmplification = AddSlider(groupAudio, SETTING_AUDIO_VOLUME_AMPLIFICATION, 660, SettingLevel::Basic, videoSettings.m_VolumeAmplification, 14054, VOLUME_DRC_MINIMUM * 0.01f, (VOLUME_DRC_MAXIMUM - VOLUME_DRC_MINIMUM) / 6000.0f, VOLUME_DRC_MAXIMUM * 0.01f);
+ settingAudioVolumeAmplification->SetDependencies(depsAudioOutputPassthroughDisabled);
+ }
+
+ // downmix: center mix level
+ {
+ AddSlider(groupAudio, SETTING_AUDIO_CENTERMIXLEVEL, 39112, SettingLevel::Basic,
+ videoSettings.m_CenterMixLevel, 14050, -10, 1, 30,
+ -1, false, false, true, 39113);
+ }
+
+ // audio delay setting
+ if (SupportsAudioFeature(IPC_AUD_OFFSET))
+ {
+ std::shared_ptr<CSettingNumber> settingAudioDelay = AddSlider(
+ groupAudio, SETTING_AUDIO_DELAY, 297, SettingLevel::Basic, videoSettings.m_AudioDelay, 0,
+ -CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAudioDelayRange,
+ AUDIO_DELAY_STEP,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAudioDelayRange, 297,
+ usePopup);
+ std::static_pointer_cast<CSettingControlSlider>(settingAudioDelay->GetControl())->SetFormatter(SettingFormatterDelay);
+ }
+
+ // audio stream setting
+ if (SupportsAudioFeature(IPC_AUD_SELECT_STREAM))
+ AddAudioStreams(groupAudio, SETTING_AUDIO_STREAM);
+
+ // audio digital/analog setting
+ if (SupportsAudioFeature(IPC_AUD_SELECT_OUTPUT))
+ {
+ m_passthrough = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_AUDIOOUTPUT_PASSTHROUGH);
+ AddToggle(groupAudio, SETTING_AUDIO_PASSTHROUGH, 348, SettingLevel::Basic, m_passthrough);
+ }
+
+ // subtitle stream setting
+ AddButton(groupSaveAsDefault, SETTING_AUDIO_MAKE_DEFAULT, 12376, SettingLevel::Basic);
+}
+
+bool CGUIDialogAudioSettings::SupportsAudioFeature(int feature)
+{
+ for (Features::iterator itr = m_audioCaps.begin(); itr != m_audioCaps.end(); ++itr)
+ {
+ if (*itr == feature || *itr == IPC_AUD_ALL)
+ return true;
+ }
+
+ return false;
+}
+
+void CGUIDialogAudioSettings::AddAudioStreams(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& settingId)
+{
+ if (group == NULL || settingId.empty())
+ return;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ m_audioStream = appPlayer->GetAudioStream();
+ if (m_audioStream < 0)
+ m_audioStream = 0;
+
+ AddList(group, settingId, 460, SettingLevel::Basic, m_audioStream, AudioStreamsOptionFiller, 460);
+}
+
+bool CGUIDialogAudioSettings::IsPlayingPassthrough(const std::string& condition,
+ const std::string& value,
+ const SettingConstPtr& setting,
+ void* data)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ return appPlayer->IsPassthrough();
+}
+
+void CGUIDialogAudioSettings::AudioStreamsOptionFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ int audioStreamCount = appPlayer->GetAudioStreamCount();
+
+ std::string strFormat = "{:s} - {:s} - {:d} " + g_localizeStrings.Get(10127);
+ std::string strUnknown = "[" + g_localizeStrings.Get(13205) + "]";
+
+ // cycle through each audio stream and add it to our list control
+ for (int i = 0; i < audioStreamCount; ++i)
+ {
+ std::string strItem;
+ std::string strLanguage;
+
+ AudioStreamInfo info;
+ appPlayer->GetAudioStreamInfo(i, info);
+
+ if (!g_LangCodeExpander.Lookup(info.language, strLanguage))
+ strLanguage = strUnknown;
+
+ if (info.name.length() == 0)
+ info.name = strUnknown;
+
+ strItem = StringUtils::Format(strFormat, strLanguage, info.name, info.channels);
+
+ strItem += FormatFlags(info.flags);
+ strItem += StringUtils::Format(" ({}/{})", i + 1, audioStreamCount);
+ list.emplace_back(strItem, i);
+ }
+
+ if (list.empty())
+ {
+ list.emplace_back(g_localizeStrings.Get(231), -1);
+ current = -1;
+ }
+}
+
+std::string CGUIDialogAudioSettings::SettingFormatterDelay(
+ const std::shared_ptr<const CSettingControlSlider>& control,
+ const CVariant& value,
+ const CVariant& minimum,
+ const CVariant& step,
+ const CVariant& maximum)
+{
+ if (!value.isDouble())
+ return "";
+
+ float fValue = value.asFloat();
+ float fStep = step.asFloat();
+
+ if (fabs(fValue) < 0.5f * fStep)
+ return StringUtils::Format(g_localizeStrings.Get(22003), 0.0);
+ if (fValue < 0)
+ return StringUtils::Format(g_localizeStrings.Get(22004), fabs(fValue));
+
+ return StringUtils::Format(g_localizeStrings.Get(22005), fValue);
+}
+
+std::string CGUIDialogAudioSettings::SettingFormatterPercentAsDecibel(
+ const std::shared_ptr<const CSettingControlSlider>& control,
+ const CVariant& value,
+ const CVariant& minimum,
+ const CVariant& step,
+ const CVariant& maximum)
+{
+ if (control == NULL || !value.isDouble())
+ return "";
+
+ std::string formatString = control->GetFormatString();
+ if (control->GetFormatLabel() > -1)
+ formatString = g_localizeStrings.Get(control->GetFormatLabel());
+
+ return StringUtils::Format(formatString, CAEUtil::PercentToGain(value.asFloat()));
+}
+
+std::string CGUIDialogAudioSettings::FormatFlags(StreamFlags flags)
+{
+ std::vector<std::string> localizedFlags;
+ if (flags & StreamFlags::FLAG_DEFAULT)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39105));
+ if (flags & StreamFlags::FLAG_FORCED)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39106));
+ if (flags & StreamFlags::FLAG_HEARING_IMPAIRED)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39107));
+ if (flags & StreamFlags::FLAG_VISUAL_IMPAIRED)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39108));
+ if (flags & StreamFlags::FLAG_ORIGINAL)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39111));
+
+ std::string formated = StringUtils::Join(localizedFlags, ", ");
+
+ if (!formated.empty())
+ formated = StringUtils::Format(" [{}]", formated);
+
+ return formated;
+}
diff --git a/xbmc/video/dialogs/GUIDialogAudioSettings.h b/xbmc/video/dialogs/GUIDialogAudioSettings.h
new file mode 100644
index 0000000..de69b77
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogAudioSettings.h
@@ -0,0 +1,82 @@
+/*
+ * 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/VideoPlayer/Interface/StreamInfo.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+class CVariant;
+struct IntegerSettingOption;
+
+class CGUIDialogAudioSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogAudioSettings();
+ ~CGUIDialogAudioSettings() override;
+
+ // specialization of CGUIWindow
+ void FrameMove() override;
+
+ static std::string FormatDelay(float value, float interval);
+ static std::string FormatDecibel(float value);
+ static std::string FormatPercentAsDecibel(float value);
+
+protected:
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+ bool SupportsAudioFeature(int feature);
+
+ void AddAudioStreams(const std::shared_ptr<CSettingGroup>& group, const std::string& settingId);
+
+ static bool IsPlayingPassthrough(const std::string& condition,
+ const std::string& value,
+ const std::shared_ptr<const CSetting>& setting,
+ void* data);
+
+ static void AudioStreamsOptionFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ static std::string SettingFormatterDelay(
+ const std::shared_ptr<const CSettingControlSlider>& control,
+ const CVariant& value,
+ const CVariant& minimum,
+ const CVariant& step,
+ const CVariant& maximum);
+ static std::string SettingFormatterPercentAsDecibel(
+ const std::shared_ptr<const CSettingControlSlider>& control,
+ const CVariant& value,
+ const CVariant& minimum,
+ const CVariant& step,
+ const CVariant& maximum);
+
+ float m_volume;
+ int m_audioStream;
+ bool m_passthrough = false;
+
+ typedef std::vector<int> Features;
+ Features m_audioCaps;
+private:
+ static std::string FormatFlags(StreamFlags flags);
+};
diff --git a/xbmc/video/dialogs/GUIDialogCMSSettings.cpp b/xbmc/video/dialogs/GUIDialogCMSSettings.cpp
new file mode 100644
index 0000000..ac1a7ad
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogCMSSettings.cpp
@@ -0,0 +1,233 @@
+/*
+ * 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 "GUIDialogCMSSettings.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "cores/VideoPlayer/VideoRenderers/ColorManager.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderManager.h"
+#include "filesystem/Directory.h"
+#include "guilib/GUIWindowManager.h"
+#include "profiles/ProfileManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <vector>
+
+#define SETTING_VIDEO_CMSENABLE "videoscreen.cmsenabled"
+#define SETTING_VIDEO_CMSMODE "videoscreen.cmsmode"
+#define SETTING_VIDEO_CMS3DLUT "videoscreen.cms3dlut"
+#define SETTING_VIDEO_CMSWHITEPOINT "videoscreen.cmswhitepoint"
+#define SETTING_VIDEO_CMSPRIMARIES "videoscreen.cmsprimaries"
+#define SETTING_VIDEO_CMSGAMMAMODE "videoscreen.cmsgammamode"
+#define SETTING_VIDEO_CMSGAMMA "videoscreen.cmsgamma"
+#define SETTING_VIDEO_CMSLUTSIZE "videoscreen.cmslutsize"
+
+CGUIDialogCMSSettings::CGUIDialogCMSSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_CMS_OSD_SETTINGS, "DialogSettings.xml")
+{ }
+
+CGUIDialogCMSSettings::~CGUIDialogCMSSettings() = default;
+
+void CGUIDialogCMSSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ SetHeading(36560);
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_OKAY_BUTTON);
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 15067);
+}
+
+void CGUIDialogCMSSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("cms", -1);
+ if (category == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogCMSSettings: unable to setup settings");
+ return;
+ }
+
+ // get all necessary setting groups
+ const std::shared_ptr<CSettingGroup> groupColorManagement = AddGroup(category);
+ if (groupColorManagement == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogCMSSettings: unable to setup settings");
+ return;
+ }
+
+ bool usePopup = g_SkinInfo->HasSkinFile("DialogSlider.xml");
+
+ TranslatableIntegerSettingOptions entries;
+
+ // create "depsCmsEnabled" for settings depending on CMS being enabled
+ CSettingDependency dependencyCmsEnabled(SettingDependencyType::Enable, GetSettingsManager());
+ dependencyCmsEnabled.Or()
+ ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_VIDEO_CMSENABLE, "true", SettingDependencyOperator::Equals, false, GetSettingsManager())));
+ SettingDependencies depsCmsEnabled;
+ depsCmsEnabled.push_back(dependencyCmsEnabled);
+
+ // create "depsCms3dlut" for 3dlut settings
+ CSettingDependency dependencyCms3dlut(SettingDependencyType::Visible, GetSettingsManager());
+ dependencyCms3dlut.And()
+ ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_VIDEO_CMSMODE, std::to_string(CMS_MODE_3DLUT), SettingDependencyOperator::Equals, false, GetSettingsManager())));
+ SettingDependencies depsCms3dlut;
+ depsCms3dlut.push_back(dependencyCmsEnabled);
+ depsCms3dlut.push_back(dependencyCms3dlut);
+
+ // create "depsCmsIcc" for display settings with icc profile
+ CSettingDependency dependencyCmsIcc(SettingDependencyType::Visible, GetSettingsManager());
+ dependencyCmsIcc.And()
+ ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_VIDEO_CMSMODE, std::to_string(CMS_MODE_PROFILE), SettingDependencyOperator::Equals, false, GetSettingsManager())));
+ SettingDependencies depsCmsIcc;
+ depsCmsIcc.push_back(dependencyCmsEnabled);
+ depsCmsIcc.push_back(dependencyCmsIcc);
+
+ // create "depsCmsGamma" for effective gamma adjustment (not available with bt.1886)
+ CSettingDependency dependencyCmsGamma(SettingDependencyType::Visible, GetSettingsManager());
+ dependencyCmsGamma.And()
+ ->Add(CSettingDependencyConditionPtr(new CSettingDependencyCondition(SETTING_VIDEO_CMSGAMMAMODE, std::to_string(CMS_TRC_BT1886), SettingDependencyOperator::Equals, true, GetSettingsManager())));
+ SettingDependencies depsCmsGamma;
+ depsCmsGamma.push_back(dependencyCmsEnabled);
+ depsCmsGamma.push_back(dependencyCmsIcc);
+ depsCmsGamma.push_back(dependencyCmsGamma);
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ // color management settings
+ AddToggle(groupColorManagement, SETTING_VIDEO_CMSENABLE, 36560, SettingLevel::Basic, settings->GetBool(SETTING_VIDEO_CMSENABLE));
+
+ int currentMode = settings->GetInt(SETTING_VIDEO_CMSMODE);
+ entries.clear();
+ // entries.push_back(TranslatableIntegerSettingOption(16039, CMS_MODE_OFF)); // FIXME: get from CMS class
+ entries.push_back(TranslatableIntegerSettingOption(36580, CMS_MODE_3DLUT));
+#ifdef HAVE_LCMS2
+ entries.push_back(TranslatableIntegerSettingOption(36581, CMS_MODE_PROFILE));
+#endif
+ std::shared_ptr<CSettingInt> settingCmsMode = AddSpinner(groupColorManagement, SETTING_VIDEO_CMSMODE, 36562, SettingLevel::Basic, currentMode, entries);
+ settingCmsMode->SetDependencies(depsCmsEnabled);
+
+ std::string current3dLUT = settings->GetString(SETTING_VIDEO_CMS3DLUT);
+ std::shared_ptr<CSettingString> settingCms3dlut = AddList(groupColorManagement, SETTING_VIDEO_CMS3DLUT, 36564, SettingLevel::Basic, current3dLUT, Cms3dLutsFiller, 36564);
+ settingCms3dlut->SetDependencies(depsCms3dlut);
+
+ // display settings
+ int currentWhitepoint = settings->GetInt(SETTING_VIDEO_CMSWHITEPOINT);
+ entries.clear();
+ entries.push_back(TranslatableIntegerSettingOption(36586, CMS_WHITEPOINT_D65));
+ entries.push_back(TranslatableIntegerSettingOption(36587, CMS_WHITEPOINT_D93));
+ std::shared_ptr<CSettingInt> settingCmsWhitepoint = AddSpinner(groupColorManagement, SETTING_VIDEO_CMSWHITEPOINT, 36568, SettingLevel::Basic, currentWhitepoint, entries);
+ settingCmsWhitepoint->SetDependencies(depsCmsIcc);
+
+ int currentPrimaries = settings->GetInt(SETTING_VIDEO_CMSPRIMARIES);
+ entries.clear();
+ entries.push_back(TranslatableIntegerSettingOption(36588, CMS_PRIMARIES_AUTO));
+ entries.push_back(TranslatableIntegerSettingOption(36589, CMS_PRIMARIES_BT709));
+ entries.push_back(TranslatableIntegerSettingOption(36579, CMS_PRIMARIES_BT2020));
+ entries.push_back(TranslatableIntegerSettingOption(36590, CMS_PRIMARIES_170M));
+ entries.push_back(TranslatableIntegerSettingOption(36591, CMS_PRIMARIES_BT470M));
+ entries.push_back(TranslatableIntegerSettingOption(36592, CMS_PRIMARIES_BT470BG));
+ entries.push_back(TranslatableIntegerSettingOption(36593, CMS_PRIMARIES_240M));
+ std::shared_ptr<CSettingInt> settingCmsPrimaries = AddSpinner(groupColorManagement, SETTING_VIDEO_CMSPRIMARIES, 36570, SettingLevel::Basic, currentPrimaries, entries);
+ settingCmsPrimaries->SetDependencies(depsCmsIcc);
+
+ int currentGammaMode = settings->GetInt(SETTING_VIDEO_CMSGAMMAMODE);
+ entries.clear();
+ entries.push_back(TranslatableIntegerSettingOption(36582, CMS_TRC_BT1886));
+ entries.push_back(TranslatableIntegerSettingOption(36583, CMS_TRC_INPUT_OFFSET));
+ entries.push_back(TranslatableIntegerSettingOption(36584, CMS_TRC_OUTPUT_OFFSET));
+ entries.push_back(TranslatableIntegerSettingOption(36585, CMS_TRC_ABSOLUTE));
+ std::shared_ptr<CSettingInt> settingCmsGammaMode = AddSpinner(groupColorManagement, SETTING_VIDEO_CMSGAMMAMODE, 36572, SettingLevel::Basic, currentGammaMode, entries);
+ settingCmsGammaMode->SetDependencies(depsCmsIcc);
+
+ float currentGamma = settings->GetInt(SETTING_VIDEO_CMSGAMMA)/100.0f;
+ if (currentGamma == 0.0f)
+ currentGamma = 2.20f;
+ std::shared_ptr<CSettingNumber> settingCmsGamma = AddSlider(groupColorManagement, SETTING_VIDEO_CMSGAMMA, 36574, SettingLevel::Basic, currentGamma, 36597, 1.6, 0.05, 2.8, 36574, usePopup);
+ settingCmsGamma->SetDependencies(depsCmsGamma);
+
+ int currentLutSize = settings->GetInt(SETTING_VIDEO_CMSLUTSIZE);
+ entries.clear();
+ entries.push_back(TranslatableIntegerSettingOption(36594, 4));
+ entries.push_back(TranslatableIntegerSettingOption(36595, 6));
+ entries.push_back(TranslatableIntegerSettingOption(36596, 8));
+ std::shared_ptr<CSettingInt> settingCmsLutSize = AddSpinner(groupColorManagement, SETTING_VIDEO_CMSLUTSIZE, 36576, SettingLevel::Basic, currentLutSize, entries);
+ settingCmsLutSize->SetDependencies(depsCmsIcc);
+}
+
+void CGUIDialogCMSSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_VIDEO_CMSENABLE)
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetBool(SETTING_VIDEO_CMSENABLE, (std::static_pointer_cast<const CSettingBool>(setting)->GetValue()));
+ else if (settingId == SETTING_VIDEO_CMSMODE)
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSMODE, std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ else if (settingId == SETTING_VIDEO_CMS3DLUT)
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(SETTING_VIDEO_CMS3DLUT, std::static_pointer_cast<const CSettingString>(setting)->GetValue());
+ else if (settingId == SETTING_VIDEO_CMSWHITEPOINT)
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSWHITEPOINT, std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ else if (settingId == SETTING_VIDEO_CMSPRIMARIES)
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSPRIMARIES, std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ else if (settingId == SETTING_VIDEO_CMSGAMMAMODE)
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSGAMMAMODE, std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ else if (settingId == SETTING_VIDEO_CMSGAMMA)
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSGAMMA, static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue())*100);
+ else if (settingId == SETTING_VIDEO_CMSLUTSIZE)
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetInt(SETTING_VIDEO_CMSLUTSIZE, std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+}
+
+bool CGUIDialogCMSSettings::OnBack(int actionID)
+{
+ Save();
+ return CGUIDialogSettingsBase::OnBack(actionID);
+}
+
+bool CGUIDialogCMSSettings::Save()
+{
+ CLog::Log(LOGINFO, "CGUIDialogCMSSettings: Save() called");
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ return true;
+}
+
+void CGUIDialogCMSSettings::Cms3dLutsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ // get 3dLut directory from settings
+ CFileItemList items;
+
+ // list .3dlut files
+ std::string current3dlut = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(SETTING_VIDEO_CMS3DLUT);
+ if (!current3dlut.empty())
+ current3dlut = URIUtils::GetDirectory(current3dlut);
+ XFILE::CDirectory::GetDirectory(current3dlut, items, ".3dlut", XFILE::DIR_FLAG_DEFAULTS);
+
+ for (int i = 0; i < items.Size(); i++)
+ {
+ list.emplace_back(items[i]->GetLabel(), items[i]->GetPath());
+ }
+}
diff --git a/xbmc/video/dialogs/GUIDialogCMSSettings.h b/xbmc/video/dialogs/GUIDialogCMSSettings.h
new file mode 100644
index 0000000..e718b29
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogCMSSettings.h
@@ -0,0 +1,39 @@
+/*
+ * 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 "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+struct StringSettingOption;
+
+class CGUIDialogCMSSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogCMSSettings();
+ ~CGUIDialogCMSSettings() override;
+
+protected:
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool OnBack(int actionID) override;
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+private:
+ static void Cms3dLutsFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data);
+};
diff --git a/xbmc/video/dialogs/GUIDialogFullScreenInfo.cpp b/xbmc/video/dialogs/GUIDialogFullScreenInfo.cpp
new file mode 100644
index 0000000..70ebafe
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogFullScreenInfo.cpp
@@ -0,0 +1,30 @@
+/*
+ * 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 "GUIDialogFullScreenInfo.h"
+
+#include "input/Key.h"
+
+CGUIDialogFullScreenInfo::CGUIDialogFullScreenInfo(void)
+ : CGUIDialog(WINDOW_DIALOG_FULLSCREEN_INFO, "DialogFullScreenInfo.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogFullScreenInfo::~CGUIDialogFullScreenInfo(void) = default;
+
+bool CGUIDialogFullScreenInfo::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_SHOW_INFO)
+ {
+ Close();
+ return true;
+ }
+ return CGUIDialog::OnAction(action);
+}
+
diff --git a/xbmc/video/dialogs/GUIDialogFullScreenInfo.h b/xbmc/video/dialogs/GUIDialogFullScreenInfo.h
new file mode 100644
index 0000000..ca5b5e8
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogFullScreenInfo.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIDialogFullScreenInfo :
+ public CGUIDialog
+{
+public:
+ CGUIDialogFullScreenInfo(void);
+ ~CGUIDialogFullScreenInfo(void) override;
+ bool OnAction(const CAction &action) override;
+};
+
diff --git a/xbmc/video/dialogs/GUIDialogSubtitleSettings.cpp b/xbmc/video/dialogs/GUIDialogSubtitleSettings.cpp
new file mode 100644
index 0000000..24f2618
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogSubtitleSettings.cpp
@@ -0,0 +1,426 @@
+/*
+ * 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 "GUIDialogSubtitleSettings.h"
+
+#include "FileItem.h"
+#include "GUIDialogSubtitles.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "addons/Skin.h"
+#include "addons/VFSEntry.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/IPlayer.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/FileUtils.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <string>
+#include <vector>
+
+#define SETTING_SUBTITLE_ENABLE "subtitles.enable"
+#define SETTING_SUBTITLE_DELAY "subtitles.delay"
+#define SETTING_SUBTITLE_STREAM "subtitles.stream"
+#define SETTING_SUBTITLE_BROWSER "subtitles.browser"
+#define SETTING_SUBTITLE_SEARCH "subtitles.search"
+#define SETTING_MAKE_DEFAULT "audio.makedefault"
+
+CGUIDialogSubtitleSettings::CGUIDialogSubtitleSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_SUBTITLE_OSD_SETTINGS, "DialogSettings.xml")
+{ }
+
+CGUIDialogSubtitleSettings::~CGUIDialogSubtitleSettings() = default;
+
+void CGUIDialogSubtitleSettings::FrameMove()
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->HasPlayer())
+ {
+ const CVideoSettings videoSettings = appPlayer->GetVideoSettings();
+
+ // these settings can change on the fly
+ //! @todo m_settingsManager->SetBool(SETTING_SUBTITLE_ENABLE, g_application.GetAppPlayer().GetSubtitleVisible());
+ // \-> Unless subtitle visibility can change on the fly, while Dialog is up, this code should be removed.
+ GetSettingsManager()->SetNumber(SETTING_SUBTITLE_DELAY,
+ static_cast<double>(videoSettings.m_SubtitleDelay));
+ //! @todo (needs special handling): m_settingsManager->SetInt(SETTING_SUBTITLE_STREAM, g_application.GetAppPlayer().GetSubtitle());
+ }
+
+ CGUIDialogSettingsManualBase::FrameMove();
+}
+
+bool CGUIDialogSubtitleSettings::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_SUBTITLE_DOWNLOADED)
+ {
+ Close();
+ }
+ return CGUIDialogSettingsManualBase::OnMessage(message);
+}
+
+void CGUIDialogSubtitleSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_SUBTITLE_ENABLE)
+ {
+ bool value = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ if (value)
+ {
+ // Ensure that we use/store the subtitle stream the user currently sees in the dialog.
+ appPlayer->SetSubtitle(m_subtitleStream);
+ }
+ appPlayer->SetSubtitleVisible(value);
+ }
+ else if (settingId == SETTING_SUBTITLE_DELAY)
+ {
+ float value = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue());
+ appPlayer->SetSubTitleDelay(value);
+ }
+ else if (settingId == SETTING_SUBTITLE_STREAM)
+ {
+ m_subtitleStream = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ appPlayer->SetSubtitle(m_subtitleStream);
+ }
+}
+
+std::string CGUIDialogSubtitleSettings::BrowseForSubtitle()
+{
+ std::string extras;
+ for (const auto& vfsAddon : CServiceBroker::GetVFSAddonCache().GetAddonInstances())
+ {
+ if (vfsAddon->ID() == "vfs.rar" || vfsAddon->ID() == "vfs.libarchive")
+ extras += '|' + vfsAddon->GetExtensions();
+ }
+
+ std::string strPath;
+ if (URIUtils::IsInRAR(g_application.CurrentFileItem().GetPath()) || URIUtils::IsInZIP(g_application.CurrentFileItem().GetPath()))
+ {
+ strPath = CURL(g_application.CurrentFileItem().GetPath()).GetHostName();
+ }
+ else if (!URIUtils::IsPlugin(g_application.CurrentFileItem().GetPath()))
+ {
+ strPath = g_application.CurrentFileItem().GetPath();
+ }
+
+ std::string strMask =
+ ".utf|.utf8|.utf-8|.sub|.srt|.smi|.rt|.txt|.ssa|.aqt|.jss|.ass|.vtt|.idx|.zip|.sup";
+
+ if (g_application.GetCurrentPlayer() == "VideoPlayer")
+ strMask = ".srt|.zip|.ifo|.smi|.sub|.idx|.ass|.ssa|.vtt|.txt|.sup";
+
+ strMask += extras;
+
+ VECSOURCES shares(*CMediaSourceSettings::GetInstance().GetSources("video"));
+ if (CMediaSettings::GetInstance().GetAdditionalSubtitleDirectoryChecked() != -1 && !CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_CUSTOMPATH).empty())
+ {
+ CMediaSource share;
+ std::vector<std::string> paths;
+ if (!strPath.empty())
+ {
+ paths.push_back(URIUtils::GetDirectory(strPath));
+ }
+ paths.push_back(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_CUSTOMPATH));
+ share.FromNameAndPaths("video",g_localizeStrings.Get(21367),paths);
+ shares.push_back(share);
+ strPath = share.strPath;
+ URIUtils::AddSlashAtEnd(strPath);
+ }
+
+ if (CGUIDialogFileBrowser::ShowAndGetFile(shares, strMask, g_localizeStrings.Get(293), strPath, false, true)) // "subtitles"
+ {
+ if (URIUtils::HasExtension(strPath, ".sub"))
+ {
+ if (CFileUtils::Exists(URIUtils::ReplaceExtension(strPath, ".idx")))
+ strPath = URIUtils::ReplaceExtension(strPath, ".idx");
+ }
+
+ return strPath;
+ }
+
+ return "";
+}
+
+void CGUIDialogSubtitleSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingAction(setting);
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_SUBTITLE_BROWSER)
+ {
+ std::string strPath = BrowseForSubtitle();
+ if (!strPath.empty())
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->AddSubtitle(strPath);
+ Close();
+ }
+ }
+ else if (settingId == SETTING_SUBTITLE_SEARCH)
+ {
+ auto dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSubtitles>(WINDOW_DIALOG_SUBTITLES);
+ if (dialog)
+ {
+ dialog->Open();
+ m_subtitleStreamSetting->UpdateDynamicOptions();
+ }
+ }
+ else if (settingId == SETTING_MAKE_DEFAULT)
+ Save();
+}
+
+bool CGUIDialogSubtitleSettings::Save()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (!g_passwordManager.CheckSettingLevelLock(SettingLevel::Expert) &&
+ profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE)
+ return true;
+
+ // prompt user if they are sure
+ if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{12376}, CVariant{12377}))
+ return true;
+
+ // reset the settings
+ CVideoDatabase db;
+ if (!db.Open())
+ return true;
+
+ db.EraseAllVideoSettings();
+ db.Close();
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ CMediaSettings::GetInstance().GetDefaultVideoSettings() = appPlayer->GetVideoSettings();
+ CMediaSettings::GetInstance().GetDefaultVideoSettings().m_SubtitleStream = -1;
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ return true;
+}
+
+void CGUIDialogSubtitleSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ SetHeading(24133);
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_OKAY_BUTTON);
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 15067);
+}
+
+void CGUIDialogSubtitleSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("audiosubtitlesettings", -1);
+ if (category == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogSubtitleSettings: unable to setup settings");
+ return;
+ }
+
+ // get all necessary setting groups
+ const std::shared_ptr<CSettingGroup> groupAudio = AddGroup(category);
+ if (groupAudio == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogSubtitleSettings: unable to setup settings");
+ return;
+ }
+ const std::shared_ptr<CSettingGroup> groupSubtitles = AddGroup(category);
+ if (groupSubtitles == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogSubtitleSettings: unable to setup settings");
+ return;
+ }
+ const std::shared_ptr<CSettingGroup> groupSaveAsDefault = AddGroup(category);
+ if (groupSaveAsDefault == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogSubtitleSettings: unable to setup settings");
+ return;
+ }
+
+ bool usePopup = g_SkinInfo->HasSkinFile("DialogSlider.xml");
+
+ const CVideoSettings videoSettings = appPlayer->GetVideoSettings();
+
+ if (appPlayer->HasPlayer())
+ {
+ appPlayer->GetSubtitleCapabilities(m_subtitleCapabilities);
+ }
+
+ // subtitle settings
+ m_subtitleVisible = appPlayer->GetSubtitleVisible();
+
+ // subtitle enabled setting
+ AddToggle(groupSubtitles, SETTING_SUBTITLE_ENABLE, 13397, SettingLevel::Basic, m_subtitleVisible);
+
+ // subtitle delay setting
+ if (SupportsSubtitleFeature(IPC_SUBS_OFFSET))
+ {
+ std::shared_ptr<CSettingNumber> settingSubtitleDelay = AddSlider(groupSubtitles, SETTING_SUBTITLE_DELAY, 22006, SettingLevel::Basic, videoSettings.m_SubtitleDelay, 0, -CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoSubsDelayRange, 0.1f, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoSubsDelayRange, 22006, usePopup);
+ std::static_pointer_cast<CSettingControlSlider>(settingSubtitleDelay->GetControl())->SetFormatter(SettingFormatterDelay);
+ }
+
+ // subtitle stream setting
+ if (SupportsSubtitleFeature(IPC_SUBS_SELECT))
+ AddSubtitleStreams(groupSubtitles, SETTING_SUBTITLE_STREAM);
+
+ // subtitle browser setting
+ if (SupportsSubtitleFeature(IPC_SUBS_EXTERNAL))
+ AddButton(groupSubtitles, SETTING_SUBTITLE_BROWSER, 13250, SettingLevel::Basic);
+
+ AddButton(groupSubtitles, SETTING_SUBTITLE_SEARCH, 24134, SettingLevel::Basic);
+
+ // subtitle stream setting
+ AddButton(groupSaveAsDefault, SETTING_MAKE_DEFAULT, 12376, SettingLevel::Basic);
+}
+
+bool CGUIDialogSubtitleSettings::SupportsSubtitleFeature(int feature)
+{
+ for (auto item : m_subtitleCapabilities)
+ {
+ if (item == feature || item == IPC_SUBS_ALL)
+ return true;
+ }
+ return false;
+}
+
+void CGUIDialogSubtitleSettings::AddSubtitleStreams(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& settingId)
+{
+ if (group == NULL || settingId.empty())
+ return;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ m_subtitleStream = appPlayer->GetSubtitle();
+ if (m_subtitleStream < 0)
+ m_subtitleStream = 0;
+
+ m_subtitleStreamSetting = AddList(group, settingId, 462, SettingLevel::Basic, m_subtitleStream, SubtitleStreamsOptionFiller, 462);
+}
+
+void CGUIDialogSubtitleSettings::SubtitleStreamsOptionFiller(
+ const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ int subtitleStreamCount = appPlayer->GetSubtitleCount();
+
+ // cycle through each subtitle and add it to our entry list
+ for (int i = 0; i < subtitleStreamCount; ++i)
+ {
+ SubtitleStreamInfo info;
+ appPlayer->GetSubtitleStreamInfo(i, info);
+
+ std::string strItem;
+ std::string strLanguage;
+
+ if (!g_LangCodeExpander.Lookup(info.language, strLanguage))
+ strLanguage = g_localizeStrings.Get(13205); // Unknown
+
+ if (info.name.length() == 0)
+ strItem = strLanguage;
+ else
+ strItem = StringUtils::Format("{} - {}", strLanguage, info.name);
+
+ strItem += FormatFlags(info.flags);
+ strItem += StringUtils::Format(" ({}/{})", i + 1, subtitleStreamCount);
+
+ list.emplace_back(strItem, i);
+ }
+
+ // no subtitle streams - just add a "None" entry
+ if (list.empty())
+ {
+ list.emplace_back(g_localizeStrings.Get(231), -1);
+ current = -1;
+ }
+}
+
+std::string CGUIDialogSubtitleSettings::SettingFormatterDelay(
+ const std::shared_ptr<const CSettingControlSlider>& control,
+ const CVariant& value,
+ const CVariant& minimum,
+ const CVariant& step,
+ const CVariant& maximum)
+{
+ if (!value.isDouble())
+ return "";
+
+ float fValue = value.asFloat();
+ float fStep = step.asFloat();
+
+ if (fabs(fValue) < 0.5f * fStep)
+ return StringUtils::Format(g_localizeStrings.Get(22003), 0.0);
+ if (fValue < 0)
+ return StringUtils::Format(g_localizeStrings.Get(22004), fabs(fValue));
+
+ return StringUtils::Format(g_localizeStrings.Get(22005), fValue);
+}
+
+std::string CGUIDialogSubtitleSettings::FormatFlags(StreamFlags flags)
+{
+ std::vector<std::string> localizedFlags;
+ if (flags & StreamFlags::FLAG_DEFAULT)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39105));
+ if (flags & StreamFlags::FLAG_FORCED)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39106));
+ if (flags & StreamFlags::FLAG_HEARING_IMPAIRED)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39107));
+ if (flags & StreamFlags::FLAG_VISUAL_IMPAIRED)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39108));
+
+ std::string formated = StringUtils::Join(localizedFlags, ", ");
+
+ if (!formated.empty())
+ formated = StringUtils::Format(" [{}]", formated);
+
+ return formated;
+}
diff --git a/xbmc/video/dialogs/GUIDialogSubtitleSettings.h b/xbmc/video/dialogs/GUIDialogSubtitleSettings.h
new file mode 100644
index 0000000..65216ed
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogSubtitleSettings.h
@@ -0,0 +1,70 @@
+/*
+ * 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/VideoPlayer/Interface/StreamInfo.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+class CVariant;
+struct IntegerSettingOption;
+
+class CGUIDialogSubtitleSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogSubtitleSettings();
+ ~CGUIDialogSubtitleSettings() override;
+ bool OnMessage(CGUIMessage& message) override;
+
+ // specialization of CGUIWindow
+ void FrameMove() override;
+
+ static std::string BrowseForSubtitle();
+
+protected:
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+private:
+ bool SupportsSubtitleFeature(int feature);
+
+ void AddSubtitleStreams(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& settingId);
+
+ int m_subtitleStream;
+ bool m_subtitleVisible;
+ std::shared_ptr<CSettingInt> m_subtitleStreamSetting;
+
+ std::vector<int> m_subtitleCapabilities;
+ static std::string FormatFlags(StreamFlags flags);
+
+ static void SubtitleStreamsOptionFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ static std::string SettingFormatterDelay(
+ const std::shared_ptr<const CSettingControlSlider>& control,
+ const CVariant& value,
+ const CVariant& minimum,
+ const CVariant& step,
+ const CVariant& maximum);
+};
diff --git a/xbmc/video/dialogs/GUIDialogSubtitles.cpp b/xbmc/video/dialogs/GUIDialogSubtitles.cpp
new file mode 100644
index 0000000..600c6da
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogSubtitles.cpp
@@ -0,0 +1,703 @@
+/*
+ * 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 "GUIDialogSubtitles.h"
+
+#include "FileItem.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonInfo.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/IPlayer.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "filesystem/AddonsDirectory.h"
+#include "filesystem/Directory.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "filesystem/StackDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/ActionIDs.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/JobManager.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+
+#include <mutex>
+
+using namespace ADDON;
+using namespace XFILE;
+
+namespace
+{
+constexpr int CONTROL_NAMELABEL = 100;
+constexpr int CONTROL_NAMELOGO = 110;
+constexpr int CONTROL_SUBLIST = 120;
+constexpr int CONTROL_SUBSEXIST = 130;
+constexpr int CONTROL_SUBSTATUS = 140;
+constexpr int CONTROL_SERVICELIST = 150;
+constexpr int CONTROL_MANUALSEARCH = 160;
+
+enum class SUBTITLE_SERVICE_CONTEXT_BUTTONS
+{
+ ADDON_SETTINGS,
+ ADDON_DISABLE
+};
+} // namespace
+
+/*! \brief simple job to retrieve a directory and store a string (language)
+ */
+class CSubtitlesJob: public CJob
+{
+public:
+ CSubtitlesJob(const CURL &url, const std::string &language) : m_url(url), m_language(language)
+ {
+ m_items = new CFileItemList;
+ }
+ ~CSubtitlesJob() override
+ {
+ delete m_items;
+ }
+ bool DoWork() override
+ {
+ CDirectory::GetDirectory(m_url.Get(), *m_items, "", DIR_FLAG_DEFAULTS);
+ return true;
+ }
+ bool operator==(const CJob *job) const override
+ {
+ if (strcmp(job->GetType(),GetType()) == 0)
+ {
+ const CSubtitlesJob* rjob = dynamic_cast<const CSubtitlesJob*>(job);
+ if (rjob)
+ {
+ return m_url.Get() == rjob->m_url.Get() &&
+ m_language == rjob->m_language;
+ }
+ }
+ return false;
+ }
+ const CFileItemList *GetItems() const { return m_items; }
+ const CURL &GetURL() const { return m_url; }
+ const std::string &GetLanguage() const { return m_language; }
+private:
+ CURL m_url;
+ CFileItemList *m_items;
+ std::string m_language;
+};
+
+CGUIDialogSubtitles::CGUIDialogSubtitles(void)
+ : CGUIDialog(WINDOW_DIALOG_SUBTITLES, "DialogSubtitles.xml")
+ , m_subtitles(new CFileItemList)
+ , m_serviceItems(new CFileItemList)
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogSubtitles::~CGUIDialogSubtitles(void)
+{
+ CancelJobs();
+ delete m_subtitles;
+ delete m_serviceItems;
+}
+
+bool CGUIDialogSubtitles::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_CLICKED)
+ {
+ int iControl = message.GetSenderId();
+ bool selectAction = (message.GetParam1() == ACTION_SELECT_ITEM ||
+ message.GetParam1() == ACTION_MOUSE_LEFT_CLICK);
+
+ bool contextMenuAction = (message.GetParam1() == ACTION_CONTEXT_MENU ||
+ message.GetParam1() == ACTION_MOUSE_RIGHT_CLICK);
+
+ if (selectAction && iControl == CONTROL_SUBLIST)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_SUBLIST);
+ OnMessage(msg);
+
+ int item = msg.GetParam1();
+ if (item >= 0 && item < m_subtitles->Size())
+ Download(*m_subtitles->Get(item));
+ return true;
+ }
+ else if (selectAction && iControl == CONTROL_SERVICELIST)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_SERVICELIST);
+ OnMessage(msg);
+
+ int item = msg.GetParam1();
+ if (item >= 0 && item < m_serviceItems->Size())
+ {
+ SetService(m_serviceItems->Get(item)->GetProperty("Addon.ID").asString());
+ Search();
+ }
+ return true;
+ }
+ else if (contextMenuAction && iControl == CONTROL_SERVICELIST)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_SERVICELIST);
+ OnMessage(msg);
+
+ const int itemIdx = msg.GetParam1();
+ if (itemIdx >= 0 && itemIdx < m_serviceItems->Size())
+ {
+ OnSubtitleServiceContextMenu(itemIdx);
+ }
+ }
+ else if (iControl == CONTROL_MANUALSEARCH)
+ {
+ //manual search
+ if (CGUIKeyboardFactory::ShowAndGetInput(m_strManualSearch, CVariant{g_localizeStrings.Get(24121)}, true))
+ {
+ Search(m_strManualSearch);
+ return true;
+ }
+ }
+ }
+ else if (message.GetMessage() == GUI_MSG_WINDOW_DEINIT)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ // Resume the video if the user has requested it
+ if (appPlayer->IsPaused() && m_pausedOnRun)
+ appPlayer->Pause();
+
+ CGUIDialog::OnMessage(message);
+
+ ClearSubtitles();
+ ClearServices();
+ return true;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogSubtitles::OnInitWindow()
+{
+ // Pause the video if the user has requested it
+ m_pausedOnRun = false;
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_SUBTITLES_PAUSEONSEARCH) &&
+ !appPlayer->IsPaused())
+ {
+ appPlayer->Pause();
+ m_pausedOnRun = true;
+ }
+
+ FillServices();
+ CGUIWindow::OnInitWindow();
+ Search();
+}
+
+void CGUIDialogSubtitles::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_bInvalidated)
+ {
+ // take copies of our variables to ensure we don't hold the lock for long.
+ std::string status;
+ CFileItemList subs;
+ {
+ std::unique_lock<CCriticalSection> lock(m_critsection);
+ status = m_status;
+ subs.Assign(*m_subtitles);
+ }
+ SET_CONTROL_LABEL(CONTROL_SUBSTATUS, status);
+
+ if (m_updateSubsList)
+ {
+ CGUIMessage message(GUI_MSG_LABEL_BIND, GetID(), CONTROL_SUBLIST, 0, 0, &subs);
+ OnMessage(message);
+ if (!subs.IsEmpty())
+ {
+ // focus subtitles list
+ CGUIMessage msg(GUI_MSG_SETFOCUS, GetID(), CONTROL_SUBLIST);
+ OnMessage(msg);
+ }
+ m_updateSubsList = false;
+ }
+
+ int control = GetFocusedControlID();
+ // nothing has focus
+ if (!control)
+ {
+ CGUIMessage msg(GUI_MSG_SETFOCUS, GetID(), m_subtitles->IsEmpty() ?
+ CONTROL_SERVICELIST : CONTROL_SUBLIST);
+ OnMessage(msg);
+ }
+ // subs list is focused but we have no subs
+ else if (control == CONTROL_SUBLIST && m_subtitles->IsEmpty())
+ {
+ CGUIMessage msg(GUI_MSG_SETFOCUS, GetID(), CONTROL_SERVICELIST);
+ OnMessage(msg);
+ }
+ }
+ CGUIDialog::Process(currentTime, dirtyregions);
+}
+
+void CGUIDialogSubtitles::FillServices()
+{
+ ClearServices();
+
+ VECADDONS addons;
+ CServiceBroker::GetAddonMgr().GetAddons(addons, AddonType::SUBTITLE_MODULE);
+
+ if (addons.empty())
+ {
+ UpdateStatus(NO_SERVICES);
+ return;
+ }
+
+ std::string defaultService;
+ const CFileItem &item = g_application.CurrentUnstackedItem();
+ if (item.GetVideoContentType() == VideoDbContentType::TVSHOWS ||
+ item.GetVideoContentType() == VideoDbContentType::EPISODES)
+ // Set default service for tv shows
+ defaultService = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_TV);
+ else
+ // Set default service for filemode and movies
+ defaultService = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_MOVIE);
+
+ std::string service = addons.front()->ID();
+ for (VECADDONS::const_iterator addonIt = addons.begin(); addonIt != addons.end(); ++addonIt)
+ {
+ CFileItemPtr item(CAddonsDirectory::FileItemFromAddon(*addonIt, "plugin://" + (*addonIt)->ID(), false));
+ m_serviceItems->Add(item);
+ if ((*addonIt)->ID() == defaultService)
+ service = (*addonIt)->ID();
+ }
+
+ // Bind our services to the UI
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_SERVICELIST, 0, 0, m_serviceItems);
+ OnMessage(msg);
+
+ SetService(service);
+}
+
+bool CGUIDialogSubtitles::SetService(const std::string &service)
+{
+ if (service != m_currentService)
+ {
+ m_currentService = service;
+ CLog::Log(LOGDEBUG, "New Service [{}] ", m_currentService);
+
+ CFileItemPtr currentService = GetService();
+ // highlight this item in the skin
+ for (int i = 0; i < m_serviceItems->Size(); i++)
+ {
+ CFileItemPtr pItem = m_serviceItems->Get(i);
+ pItem->Select(pItem == currentService);
+ }
+
+ SET_CONTROL_LABEL(CONTROL_NAMELABEL, currentService->GetLabel());
+
+ if (currentService->HasAddonInfo())
+ {
+ std::string icon = URIUtils::AddFileToFolder(currentService->GetAddonInfo()->Path(), "logo.png");
+ SET_CONTROL_FILENAME(CONTROL_NAMELOGO, icon);
+ }
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->GetSubtitleCount() == 0)
+ SET_CONTROL_HIDDEN(CONTROL_SUBSEXIST);
+ else
+ SET_CONTROL_VISIBLE(CONTROL_SUBSEXIST);
+
+ return true;
+ }
+ return false;
+}
+
+const CFileItemPtr CGUIDialogSubtitles::GetService() const
+{
+ for (int i = 0; i < m_serviceItems->Size(); i++)
+ {
+ if (m_serviceItems->Get(i)->GetProperty("Addon.ID") == m_currentService)
+ return m_serviceItems->Get(i);
+ }
+ return CFileItemPtr();
+}
+
+void CGUIDialogSubtitles::Search(const std::string &search/*=""*/)
+{
+ if (m_currentService.empty())
+ return; // no services available
+
+ UpdateStatus(SEARCHING);
+ ClearSubtitles();
+
+ CURL url("plugin://" + m_currentService + "/");
+ if (!search.empty())
+ {
+ url.SetOption("action", "manualsearch");
+ url.SetOption("searchstring", search);
+ }
+ else
+ url.SetOption("action", "search");
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ SettingConstPtr setting = settings->GetSetting(CSettings::SETTING_SUBTITLES_LANGUAGES);
+ if (setting)
+ url.SetOption("languages", setting->ToString());
+
+ // Check for stacking
+ if (g_application.CurrentFileItem().IsStack())
+ url.SetOption("stack", "1");
+
+ std::string preferredLanguage = settings->GetString(CSettings::SETTING_LOCALE_SUBTITLELANGUAGE);
+
+ if (StringUtils::EqualsNoCase(preferredLanguage, "original"))
+ {
+ AudioStreamInfo info;
+ std::string strLanguage;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->GetAudioStreamInfo(CURRENT_STREAM, info);
+
+ if (!g_LangCodeExpander.Lookup(info.language, strLanguage))
+ strLanguage = "Unknown";
+
+ preferredLanguage = strLanguage;
+ }
+ else if (StringUtils::EqualsNoCase(preferredLanguage, "default"))
+ preferredLanguage = g_langInfo.GetEnglishLanguageName();
+
+ url.SetOption("preferredlanguage", preferredLanguage);
+
+ AddJob(new CSubtitlesJob(url, ""));
+}
+
+void CGUIDialogSubtitles::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ const CURL &url = static_cast<CSubtitlesJob*>(job)->GetURL();
+ const CFileItemList *items = static_cast<CSubtitlesJob*>(job)->GetItems();
+ const std::string &language = static_cast<CSubtitlesJob*>(job)->GetLanguage();
+ if (url.GetOption("action") == "search" || url.GetOption("action") == "manualsearch")
+ OnSearchComplete(items);
+ else
+ OnDownloadComplete(items, language);
+ CJobQueue::OnJobComplete(jobID, success, job);
+}
+
+void CGUIDialogSubtitles::OnSearchComplete(const CFileItemList *items)
+{
+ std::unique_lock<CCriticalSection> lock(m_critsection);
+ m_subtitles->Assign(*items);
+ UpdateStatus(SEARCH_COMPLETE);
+ m_updateSubsList = true;
+ MarkDirtyRegion();
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!items->IsEmpty() && appPlayer->GetSubtitleCount() == 0 &&
+ m_LastAutoDownloaded != g_application.CurrentFile() &&
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_SUBTITLES_DOWNLOADFIRST))
+ {
+ CFileItemPtr item = items->Get(0);
+ CLog::Log(LOGDEBUG, "{} - Automatically download first subtitle: {}", __FUNCTION__,
+ item->GetLabel2());
+ m_LastAutoDownloaded = g_application.CurrentFile();
+ Download(*item);
+ }
+
+ SetInvalid();
+}
+
+void CGUIDialogSubtitles::OnSubtitleServiceContextMenu(int itemIdx)
+{
+ const auto service = m_serviceItems->Get(itemIdx);
+
+ CContextButtons buttons;
+ // Subtitle addon settings
+ buttons.Add(static_cast<int>(SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_SETTINGS),
+ g_localizeStrings.Get(21417));
+ // Disable addon
+ buttons.Add(static_cast<int>(SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_DISABLE),
+ g_localizeStrings.Get(24021));
+
+ auto idx = static_cast<SUBTITLE_SERVICE_CONTEXT_BUTTONS>(CGUIDialogContextMenu::Show(buttons));
+ switch (idx)
+ {
+ case SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_SETTINGS:
+ {
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(service->GetProperty("Addon.ID").asString(), addon,
+ AddonType::SUBTITLE_MODULE,
+ OnlyEnabled::CHOICE_YES))
+ {
+ CGUIDialogAddonSettings::ShowForAddon(addon);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "{} - Could not open settings for addon: {}", __FUNCTION__,
+ service->GetProperty("Addon.ID").asString());
+ }
+ break;
+ }
+ case SUBTITLE_SERVICE_CONTEXT_BUTTONS::ADDON_DISABLE:
+ {
+ CServiceBroker::GetAddonMgr().DisableAddon(service->GetProperty("Addon.ID").asString(),
+ AddonDisabledReason::USER);
+ const bool currentActiveServiceWasDisabled =
+ m_currentService == service->GetProperty("Addon.ID").asString();
+ FillServices();
+ // restart search if the current active service was disabled
+ if (currentActiveServiceWasDisabled && !m_serviceItems->IsEmpty())
+ {
+ Search();
+ }
+ // if no more services are available make sure the subtitle list is cleaned up
+ else if (m_serviceItems->IsEmpty())
+ {
+ ClearSubtitles();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void CGUIDialogSubtitles::UpdateStatus(STATUS status)
+{
+ std::unique_lock<CCriticalSection> lock(m_critsection);
+ std::string label;
+ switch (status)
+ {
+ case NO_SERVICES:
+ label = g_localizeStrings.Get(24114);
+ break;
+ case SEARCHING:
+ label = g_localizeStrings.Get(24107);
+ break;
+ case SEARCH_COMPLETE:
+ if (!m_subtitles->IsEmpty())
+ label = StringUtils::Format(g_localizeStrings.Get(24108), m_subtitles->Size());
+ else
+ label = g_localizeStrings.Get(24109);
+ break;
+ case DOWNLOADING:
+ label = g_localizeStrings.Get(24110);
+ break;
+ default:
+ break;
+ }
+ if (label != m_status)
+ {
+ m_status = label;
+ SetInvalid();
+ }
+}
+
+void CGUIDialogSubtitles::Download(const CFileItem &subtitle)
+{
+ UpdateStatus(DOWNLOADING);
+
+ // subtitle URL should be of the form plugin://<addonid>/?param=foo&param=bar
+ // we just append (if not already present) the action=download parameter.
+ CURL url(subtitle.GetURL());
+ if (url.GetOption("action").empty())
+ url.SetOption("action", "download");
+
+ AddJob(new CSubtitlesJob(url, subtitle.GetLabel()));
+}
+
+void CGUIDialogSubtitles::OnDownloadComplete(const CFileItemList *items, const std::string &language)
+{
+ if (items->IsEmpty())
+ {
+ CFileItemPtr service = GetService();
+ if (service)
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, service->GetLabel(), g_localizeStrings.Get(24113));
+ UpdateStatus(SEARCH_COMPLETE);
+ return;
+ }
+
+ SUBTITLE_STORAGEMODE storageMode = (SUBTITLE_STORAGEMODE) CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SUBTITLES_STORAGEMODE);
+
+ // Get (unstacked) path
+ std::string strCurrentFile = g_application.CurrentUnstackedItem().GetDynPath();
+
+ std::string strDownloadPath = "special://temp";
+ std::string strDestPath;
+ std::vector<std::string> vecFiles;
+
+ std::string strCurrentFilePath;
+ if (StringUtils::StartsWith(strCurrentFilePath, "http://"))
+ {
+ strCurrentFile = "TempSubtitle";
+ vecFiles.push_back(strCurrentFile);
+ }
+ else
+ {
+ std::string subPath = CSpecialProtocol::TranslatePath("special://subtitles");
+ if (!subPath.empty())
+ strDownloadPath = subPath;
+
+ /** Get item's folder for sub storage, special case for RAR/ZIP items
+ * @todo We need some way to avoid special casing this all over the place
+ * for rar/zip (perhaps modify GetDirectory?)
+ */
+ if (URIUtils::IsInRAR(strCurrentFile) || URIUtils::IsInZIP(strCurrentFile))
+ strCurrentFilePath = URIUtils::GetDirectory(CURL(strCurrentFile).GetHostName());
+ else
+ strCurrentFilePath = URIUtils::GetDirectory(strCurrentFile);
+
+ // Handle stacks
+ if (g_application.CurrentFileItem().IsStack() && items->Size() > 1)
+ {
+ CStackDirectory::GetPaths(g_application.CurrentFileItem().GetPath(), vecFiles);
+ // Make sure (stack) size is the same as the size of the items handed to us, else fallback to single item
+ if (items->Size() != (int) vecFiles.size())
+ {
+ vecFiles.clear();
+ vecFiles.push_back(strCurrentFile);
+ }
+ }
+ else
+ {
+ vecFiles.push_back(strCurrentFile);
+ }
+
+ if (storageMode == SUBTITLE_STORAGEMODE_MOVIEPATH &&
+ CUtil::SupportsWriteFileOperations(strCurrentFilePath))
+ {
+ strDestPath = strCurrentFilePath;
+ }
+ }
+
+ // Use fallback?
+ if (strDestPath.empty())
+ strDestPath = strDownloadPath;
+
+ // Extract the language and appropriate extension
+ std::string strSubLang;
+ g_LangCodeExpander.ConvertToISO6391(language, strSubLang);
+
+ // Iterate over all items to transfer
+ for (unsigned int i = 0; i < vecFiles.size() && i < (unsigned int) items->Size(); i++)
+ {
+ std::string strUrl = items->Get(i)->GetPath();
+ std::string strFileName = URIUtils::GetFileName(vecFiles[i]);
+ URIUtils::RemoveExtension(strFileName);
+
+ // construct subtitle path
+ std::string strSubExt = URIUtils::GetExtension(strUrl);
+ std::string strSubName = StringUtils::Format("{}.{}{}", strFileName, strSubLang, strSubExt);
+
+ // Handle URL encoding:
+ std::string strDownloadFile = URIUtils::ChangeBasePath(strCurrentFilePath, strSubName, strDownloadPath);
+ std::string strDestFile = strDownloadFile;
+
+ if (!CFile::Copy(strUrl, strDownloadFile))
+ {
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, strSubName, g_localizeStrings.Get(24113));
+ CLog::Log(LOGERROR, "{} - Saving of subtitle {} to {} failed", __FUNCTION__, strUrl,
+ strDownloadFile);
+ }
+ else
+ {
+ if (strDestPath != strDownloadPath)
+ {
+ // Handle URL encoding:
+ std::string strTryDestFile = URIUtils::ChangeBasePath(strCurrentFilePath, strSubName, strDestPath);
+
+ /* Copy the file from temp to our final destination, if that fails fallback to download path
+ * (ie. special://subtitles or use special://temp). Note that after the first item strDownloadPath equals strDestpath
+ * so that all remaining items (including the .idx below) are copied directly to their final destination and thus all
+ * items end up in the same folder
+ */
+ CLog::Log(LOGDEBUG, "{} - Saving subtitle {} to {}", __FUNCTION__, strDownloadFile,
+ strTryDestFile);
+ if (CFile::Copy(strDownloadFile, strTryDestFile))
+ {
+ CFile::Delete(strDownloadFile);
+ strDestFile = strTryDestFile;
+ strDownloadPath = strDestPath; // Update download path so all the other items get directly downloaded to our final destination
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "{} - Saving of subtitle {} to {} failed. Falling back to {}",
+ __FUNCTION__, strDownloadFile, strTryDestFile, strDownloadPath);
+ strDestPath = strDownloadPath; // Copy failed, use fallback for the rest of the items
+ }
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "{} - Saved subtitle {} to {}", __FUNCTION__, strUrl, strDownloadFile);
+ }
+
+ // for ".sub" subtitles we check if ".idx" counterpart exists and copy that as well
+ if (StringUtils::EqualsNoCase(strSubExt, ".sub"))
+ {
+ strUrl = URIUtils::ReplaceExtension(strUrl, ".idx");
+ if(CFile::Exists(strUrl))
+ {
+ std::string strSubNameIdx = StringUtils::Format("{}.{}.idx", strFileName, strSubLang);
+ // Handle URL encoding:
+ strDestFile = URIUtils::ChangeBasePath(strCurrentFilePath, strSubNameIdx, strDestPath);
+ CFile::Copy(strUrl, strDestFile);
+ }
+ }
+
+ // Set sub for currently playing (stack) item
+ if (vecFiles[i] == strCurrentFile)
+ SetSubtitles(strDestFile);
+ }
+ }
+
+ // Notify window manager that a subtitle was downloaded
+ CGUIMessage msg(GUI_MSG_SUBTITLE_DOWNLOADED, 0, 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+
+ // Close the window
+ Close();
+}
+
+void CGUIDialogSubtitles::ClearSubtitles()
+{
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_SUBLIST);
+ OnMessage(msg);
+ std::unique_lock<CCriticalSection> lock(m_critsection);
+ m_subtitles->Clear();
+}
+
+void CGUIDialogSubtitles::ClearServices()
+{
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_SERVICELIST);
+ OnMessage(msg);
+ m_serviceItems->Clear();
+ m_currentService.clear();
+}
+
+void CGUIDialogSubtitles::SetSubtitles(const std::string &subtitle)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->AddSubtitle(subtitle);
+}
diff --git a/xbmc/video/dialogs/GUIDialogSubtitles.h b/xbmc/video/dialogs/GUIDialogSubtitles.h
new file mode 100644
index 0000000..043a613
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogSubtitles.h
@@ -0,0 +1,72 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "threads/CriticalSection.h"
+#include "utils/JobManager.h"
+
+#include <string>
+
+enum SUBTITLE_STORAGEMODE
+{
+ SUBTITLE_STORAGEMODE_MOVIEPATH = 0,
+ SUBTITLE_STORAGEMODE_CUSTOMPATH
+};
+
+class CFileItem;
+class CFileItemList;
+
+class CGUIDialogSubtitles : public CGUIDialog, CJobQueue
+{
+public:
+ CGUIDialogSubtitles(void);
+ ~CGUIDialogSubtitles(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void OnInitWindow() override;
+
+protected:
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ bool SetService(const std::string &service);
+ const CFileItemPtr GetService() const;
+ void FillServices();
+ void ClearServices();
+ void ClearSubtitles();
+
+ enum STATUS { NO_SERVICES = 0, SEARCHING, SEARCH_COMPLETE, DOWNLOADING };
+ void UpdateStatus(STATUS status);
+
+ void Search(const std::string &search="");
+ void OnSearchComplete(const CFileItemList *items);
+
+ void Download(const CFileItem &subtitle);
+ void OnDownloadComplete(const CFileItemList *items, const std::string &language);
+
+
+ /*!
+ \brief Called when the context menu is requested on a subtitle service
+ present on the list of installed subtitle addons
+ \param itemIdx the index of the selected subtitle service on the list
+ */
+ void OnSubtitleServiceContextMenu(int itemIdx);
+
+ void SetSubtitles(const std::string &subtitle);
+
+ CCriticalSection m_critsection;
+ CFileItemList* m_subtitles;
+ CFileItemList* m_serviceItems;
+ std::string m_currentService;
+ std::string m_status;
+ std::string m_strManualSearch;
+ bool m_pausedOnRun = false;
+ bool m_updateSubsList = false; ///< true if we need to update our subs list
+ std::string m_LastAutoDownloaded; ///< Last video file path which automatically downloaded subtitle
+};
diff --git a/xbmc/video/dialogs/GUIDialogTeletext.cpp b/xbmc/video/dialogs/GUIDialogTeletext.cpp
new file mode 100644
index 0000000..01e86cb
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogTeletext.cpp
@@ -0,0 +1,197 @@
+/*
+ * 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 "GUIDialogTeletext.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUITexture.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/Texture.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/ColorUtils.h"
+#include "utils/log.h"
+
+static int teletextFadeAmount = 0;
+
+CGUIDialogTeletext::CGUIDialogTeletext()
+ : CGUIDialog(WINDOW_DIALOG_OSD_TELETEXT, ""), m_pTxtTexture(nullptr)
+{
+ m_renderOrder = RENDER_ORDER_DIALOG_TELETEXT;
+}
+
+CGUIDialogTeletext::~CGUIDialogTeletext() = default;
+
+bool CGUIDialogTeletext::OnAction(const CAction& action)
+{
+ if (m_TextDecoder.HandleAction(action))
+ {
+ MarkDirtyRegion();
+ return true;
+ }
+
+ return CGUIDialog::OnAction(action);
+}
+
+bool CGUIDialogTeletext::OnBack(int actionID)
+{
+ m_bClose = true;
+ MarkDirtyRegion();
+ return true;
+}
+
+bool CGUIDialogTeletext::OnMessage(CGUIMessage& message)
+{
+ if (message.GetMessage() == GUI_MSG_WINDOW_INIT)
+ {
+ /* Do not open if no teletext is available */
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->HasTeletextCache())
+ {
+ Close();
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(23049), "", 1500, false);
+ return true;
+ }
+ }
+ else if (message.GetMessage() == GUI_MSG_NOTIFY_ALL)
+ {
+ if (message.GetParam1() == GUI_MSG_WINDOW_RESIZE)
+ {
+ SetCoordinates();
+ }
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogTeletext::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ CGUIDialog::Process(currentTime, dirtyregions);
+ m_renderRegion = m_vertCoords;
+}
+
+void CGUIDialogTeletext::Render()
+{
+ // Do not render if we have no texture
+ if (!m_pTxtTexture)
+ {
+ CLog::Log(LOGERROR, "CGUITeletextBox::Render called without texture");
+ return;
+ }
+
+ m_TextDecoder.RenderPage();
+
+ if (!m_bClose)
+ {
+ if (teletextFadeAmount < 100)
+ {
+ teletextFadeAmount = std::min(100, teletextFadeAmount + 5);
+ MarkDirtyRegion();
+ }
+ }
+ else
+ {
+ if (teletextFadeAmount > 0)
+ {
+ teletextFadeAmount = std::max(0, teletextFadeAmount - 10);
+ MarkDirtyRegion();
+ }
+
+ if (teletextFadeAmount == 0)
+ Close();
+ }
+
+ unsigned char* textureBuffer = (unsigned char*)m_TextDecoder.GetTextureBuffer();
+ if (!m_bClose && m_TextDecoder.NeedRendering() && textureBuffer)
+ {
+ m_pTxtTexture->Update(m_TextDecoder.GetWidth(), m_TextDecoder.GetHeight(), m_TextDecoder.GetWidth()*4, XB_FMT_A8R8G8B8, textureBuffer, false);
+ m_TextDecoder.RenderingDone();
+ MarkDirtyRegion();
+ }
+
+ UTILS::COLOR::Color color =
+ (static_cast<UTILS::COLOR::Color>(teletextFadeAmount * 2.55f) & 0xff) << 24 | 0xFFFFFF;
+ CGUITexture::DrawQuad(m_vertCoords, color, m_pTxtTexture.get());
+
+ CGUIDialog::Render();
+}
+
+void CGUIDialogTeletext::OnInitWindow()
+{
+ teletextFadeAmount = 0;
+ m_bClose = false;
+ m_windowLoaded = true;
+
+ SetCoordinates();
+
+ if (!m_TextDecoder.InitDecoder())
+ {
+ CLog::Log(LOGERROR, "{}: failed to init teletext decoder", __FUNCTION__);
+ Close();
+ }
+
+ m_pTxtTexture =
+ CTexture::CreateTexture(m_TextDecoder.GetWidth(), m_TextDecoder.GetHeight(), XB_FMT_A8R8G8B8);
+ if (!m_pTxtTexture)
+ {
+ CLog::Log(LOGERROR, "{}: failed to create texture", __FUNCTION__);
+ Close();
+ }
+
+ CGUIDialog::OnInitWindow();
+}
+
+void CGUIDialogTeletext::OnDeinitWindow(int nextWindowID)
+{
+ m_windowLoaded = false;
+ m_TextDecoder.EndDecoder();
+
+ m_pTxtTexture.reset();
+
+ CGUIDialog::OnDeinitWindow(nextWindowID);
+}
+
+void CGUIDialogTeletext::SetCoordinates()
+{
+ float left, right, top, bottom;
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetScalingResolution(m_coordsRes, m_needsScaling);
+
+ left = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord(0, 0);
+ right = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalXCoord((float)m_coordsRes.iWidth, 0);
+ top = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(0, 0);
+ bottom = CServiceBroker::GetWinSystem()->GetGfxContext().ScaleFinalYCoord(0, (float)m_coordsRes.iHeight);
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOPLAYER_TELETEXTSCALE))
+ {
+ /* Fixed aspect ratio to 4:3 for teletext */
+ float width = right - left;
+ float height = bottom - top;
+ if (width / 4 > height / 3)
+ {
+ left = (width - height * 4 / 3) / 2;
+ right = width - left;
+ }
+ else
+ {
+ top = (height - width * 3 / 4) / 2;
+ bottom = height - top;
+ }
+ }
+
+ m_vertCoords.SetRect(left,
+ top,
+ right,
+ bottom);
+
+ MarkDirtyRegion();
+}
diff --git a/xbmc/video/dialogs/GUIDialogTeletext.h b/xbmc/video/dialogs/GUIDialogTeletext.h
new file mode 100644
index 0000000..4a60ec0
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogTeletext.h
@@ -0,0 +1,39 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "video/Teletext.h"
+
+#include <memory>
+
+class CTexture;
+
+class CGUIDialogTeletext : public CGUIDialog
+{
+public:
+ CGUIDialogTeletext(void);
+ ~CGUIDialogTeletext(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override;
+ bool OnBack(int actionID) override;
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ void OnInitWindow() override;
+ void OnDeinitWindow(int nextWindowID) override;
+
+protected:
+ bool m_bClose; /* Close sendet, needed for fade out */
+ std::unique_ptr<CTexture> m_pTxtTexture; /* Texture info class to render to screen */
+ CRect m_vertCoords; /* Coordinates of teletext field on screen */
+ CTeletextDecoder m_TextDecoder; /* Decoding class for teletext code */
+
+private:
+ void SetCoordinates();
+};
diff --git a/xbmc/video/dialogs/GUIDialogVideoBookmarks.cpp b/xbmc/video/dialogs/GUIDialogVideoBookmarks.cpp
new file mode 100644
index 0000000..f2669d3
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogVideoBookmarks.cpp
@@ -0,0 +1,590 @@
+/*
+ * 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 "GUIDialogVideoBookmarks.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "Util.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "pictures/Picture.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/Crc32.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoThumbLoader.h"
+#include "view/ViewState.h"
+
+#include <mutex>
+#include <string>
+#include <vector>
+
+#define BOOKMARK_THUMB_WIDTH CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes
+
+#define CONTROL_ADD_BOOKMARK 2
+#define CONTROL_CLEAR_BOOKMARKS 3
+#define CONTROL_ADD_EPISODE_BOOKMARK 4
+
+#define CONTROL_THUMBS 11
+
+CGUIDialogVideoBookmarks::CGUIDialogVideoBookmarks()
+ : CGUIDialog(WINDOW_DIALOG_VIDEO_BOOKMARKS, "VideoOSDBookmarks.xml"),
+ CJobQueue(false, 1, CJob::PRIORITY_NORMAL)
+{
+ m_vecItems = new CFileItemList;
+ m_loadType = LOAD_EVERY_TIME;
+ m_jobsStarted = 0;
+}
+
+CGUIDialogVideoBookmarks::~CGUIDialogVideoBookmarks()
+{
+ delete m_vecItems;
+}
+
+bool CGUIDialogVideoBookmarks::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ Clear();
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ // don't init this dialog if we don't playback a file
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlaying())
+ return false;
+
+ CGUIWindow::OnMessage(message);
+ Update();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_ADD_BOOKMARK)
+ {
+ AddBookmark();
+ Update();
+ }
+ else if (iControl == CONTROL_CLEAR_BOOKMARKS)
+ {
+ ClearBookmarks();
+ }
+ else if (iControl == CONTROL_ADD_EPISODE_BOOKMARK)
+ {
+ AddEpisodeBookmark();
+ Update();
+ }
+ else if (m_viewControl.HasControl(iControl)) // list/thumb control
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ int iAction = message.GetParam1();
+ if (iAction == ACTION_DELETE_ITEM)
+ {
+ Delete(iItem);
+ }
+ else if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK)
+ {
+ GotoBookmark(iItem);
+ }
+ }
+ }
+ break;
+ case GUI_MSG_SETFOCUS:
+ {
+ if (m_viewControl.HasControl(message.GetControlId()) && m_viewControl.GetCurrentControl() != message.GetControlId())
+ {
+ m_viewControl.SetFocused();
+ return true;
+ }
+ }
+ break;
+ case GUI_MSG_REFRESH_LIST:
+ {
+ switch (message.GetParam1())
+ {
+ case 0:
+ OnRefreshList();
+ break;
+ case 1:
+ UpdateItem(message.GetParam2());
+ break;
+ default:
+ break;
+ }
+ }
+ break;
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+bool CGUIDialogVideoBookmarks::OnAction(const CAction &action)
+{
+ switch(action.GetID())
+ {
+ case ACTION_CONTEXT_MENU:
+ case ACTION_MOUSE_RIGHT_CLICK:
+ {
+ OnPopupMenu(m_viewControl.GetSelectedItem());
+ return true;
+ }
+ }
+ return CGUIDialog::OnAction(action);
+}
+
+
+void CGUIDialogVideoBookmarks::OnPopupMenu(int item)
+{
+ if (item < 0 || item >= (int) m_bookmarks.size())
+ return;
+
+ // highlight the item
+ (*m_vecItems)[item]->Select(true);
+
+ CContextButtons choices;
+ choices.Add(1, (m_bookmarks[item].type == CBookmark::EPISODE ? 20405 : 20404)); // "Remove episode bookmark" or "Remove bookmark"
+
+ int button = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+
+ // unhighlight the item
+ (*m_vecItems)[item]->Select(false);
+
+ if (button == 1)
+ Delete(item);
+}
+
+void CGUIDialogVideoBookmarks::Delete(int item)
+{
+ if ( item>=0 && (unsigned)item < m_bookmarks.size() )
+ {
+ CVideoDatabase videoDatabase;
+ videoDatabase.Open();
+ std::string path(g_application.CurrentFile());
+ if (g_application.CurrentFileItem().HasProperty("original_listitem_url") &&
+ !URIUtils::IsVideoDb(g_application.CurrentFileItem().GetProperty("original_listitem_url").asString()))
+ path = g_application.CurrentFileItem().GetProperty("original_listitem_url").asString();
+ videoDatabase.ClearBookMarkOfFile(path, m_bookmarks[item], m_bookmarks[item].type);
+ videoDatabase.Close();
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ }
+ Update();
+}
+
+void CGUIDialogVideoBookmarks::UpdateItem(unsigned int chapterIdx)
+{
+ std::unique_lock<CCriticalSection> lock(m_refreshSection);
+
+ int itemPos = 0;
+ for (const auto& item : *m_vecItems)
+ {
+ if (chapterIdx == item->GetProperty("chapter").asInteger())
+ break;
+ itemPos++;
+ }
+
+ if (itemPos < m_vecItems->Size())
+ {
+ std::string time = StringUtils::Format("chapter://{}/{}", m_filePath, chapterIdx);
+ std::string cachefile = CServiceBroker::GetTextureCache()->GetCachedPath(
+ CServiceBroker::GetTextureCache()->GetCacheFile(time) + ".jpg");
+ if (CFileUtils::Exists(cachefile))
+ {
+ (*m_vecItems)[itemPos]->SetArt("thumb", cachefile);
+ }
+ }
+}
+
+void CGUIDialogVideoBookmarks::OnRefreshList()
+{
+ m_bookmarks.clear();
+ std::vector<CFileItemPtr> items;
+
+ // open the d/b and retrieve the bookmarks for the current movie
+ m_filePath = g_application.CurrentFile();
+ if (g_application.CurrentFileItem().HasProperty("original_listitem_url") &&
+ !URIUtils::IsVideoDb(g_application.CurrentFileItem().GetProperty("original_listitem_url").asString()))
+ m_filePath = g_application.CurrentFileItem().GetProperty("original_listitem_url").asString();
+
+ CVideoDatabase videoDatabase;
+ videoDatabase.Open();
+ videoDatabase.GetBookMarksForFile(m_filePath, m_bookmarks);
+ videoDatabase.GetBookMarksForFile(m_filePath, m_bookmarks, CBookmark::EPISODE, true);
+ videoDatabase.Close();
+
+ std::unique_lock<CCriticalSection> lock(m_refreshSection);
+ m_vecItems->Clear();
+
+ // cycle through each stored bookmark and add it to our list control
+ for (unsigned int i = 0; i < m_bookmarks.size(); ++i)
+ {
+ std::string bookmarkTime;
+ if (m_bookmarks[i].type == CBookmark::EPISODE)
+ bookmarkTime = StringUtils::Format("{} {} {} {}", g_localizeStrings.Get(20373),
+ m_bookmarks[i].seasonNumber, g_localizeStrings.Get(20359),
+ m_bookmarks[i].episodeNumber);
+ else
+ bookmarkTime = StringUtils::SecondsToTimeString((long)m_bookmarks[i].timeInSeconds, TIME_FORMAT_HH_MM_SS);
+
+ CFileItemPtr item(new CFileItem(StringUtils::Format(g_localizeStrings.Get(299), i + 1)));
+ item->SetLabel2(bookmarkTime);
+ item->SetArt("thumb", m_bookmarks[i].thumbNailImage);
+ item->SetProperty("resumepoint", m_bookmarks[i].timeInSeconds);
+ item->SetProperty("playerstate", m_bookmarks[i].playerState);
+ item->SetProperty("isbookmark", "true");
+ items.push_back(item);
+ }
+
+ // add chapters if around
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ for (int i = 1; i <= appPlayer->GetChapterCount(); ++i)
+ {
+ std::string chapterName;
+ appPlayer->GetChapterName(chapterName, i);
+
+ int64_t pos = appPlayer->GetChapterPos(i);
+ std::string time = StringUtils::SecondsToTimeString((long) pos, TIME_FORMAT_HH_MM_SS);
+
+ if (chapterName.empty() ||
+ StringUtils::StartsWithNoCase(chapterName, time) ||
+ StringUtils::IsNaturalNumber(chapterName))
+ chapterName = StringUtils::Format(g_localizeStrings.Get(25010), i);
+
+ CFileItemPtr item(new CFileItem(chapterName));
+ item->SetLabel2(time);
+
+ std::string chapterPath = StringUtils::Format("chapter://{}/{}", m_filePath, i);
+ std::string cachefile = CServiceBroker::GetTextureCache()->GetCachedPath(
+ CServiceBroker::GetTextureCache()->GetCacheFile(chapterPath) + ".jpg");
+ if (CFileUtils::Exists(cachefile))
+ item->SetArt("thumb", cachefile);
+ else if (i > m_jobsStarted && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTCHAPTERTHUMBS))
+ {
+ CFileItem item(m_filePath, false);
+ CJob* job = new CThumbExtractor(item, m_filePath, true, chapterPath, pos * 1000, false);
+ AddJob(job);
+ m_mapJobsChapter[job] = i;
+ m_jobsStarted++;
+ }
+
+ item->SetProperty("chapter", i);
+ item->SetProperty("resumepoint", static_cast<double>(pos));
+ item->SetProperty("ischapter", "true");
+ items.push_back(item);
+ }
+
+ // sort items by resume point
+ std::sort(items.begin(), items.end(), [](const CFileItemPtr &item1, const CFileItemPtr &item2) {
+ return item1->GetProperty("resumepoint").asDouble() < item2->GetProperty("resumepoint").asDouble();
+ });
+
+ // add items to file list and mark the proper item as selected if the current playtime is above
+ int selectedItemIndex = 0;
+ double playTime = g_application.GetTime();
+ for (auto& item : items)
+ {
+ m_vecItems->Add(item);
+ if (playTime >= item->GetProperty("resumepoint").asDouble())
+ selectedItemIndex = m_vecItems->Size() - 1;
+ }
+
+ m_viewControl.SetItems(*m_vecItems);
+ m_viewControl.SetSelectedItem(selectedItemIndex);
+}
+
+void CGUIDialogVideoBookmarks::Update()
+{
+ CVideoDatabase videoDatabase;
+ videoDatabase.Open();
+
+ if (g_application.CurrentFileItem().HasVideoInfoTag() && g_application.CurrentFileItem().GetVideoInfoTag()->m_iEpisode > -1)
+ {
+ std::vector<CVideoInfoTag> episodes;
+ videoDatabase.GetEpisodesByFile(g_application.CurrentFile(),episodes);
+ if (episodes.size() > 1)
+ {
+ CONTROL_ENABLE(CONTROL_ADD_EPISODE_BOOKMARK);
+ }
+ else
+ {
+ CONTROL_DISABLE(CONTROL_ADD_EPISODE_BOOKMARK);
+ }
+ }
+ else
+ {
+ CONTROL_DISABLE(CONTROL_ADD_EPISODE_BOOKMARK);
+ }
+
+
+ m_viewControl.SetCurrentView(DEFAULT_VIEW_ICONS);
+
+ // empty the list ready for population
+ Clear();
+
+ OnRefreshList();
+
+ videoDatabase.Close();
+}
+
+void CGUIDialogVideoBookmarks::Clear()
+{
+ m_viewControl.Clear();
+ m_vecItems->Clear();
+}
+
+void CGUIDialogVideoBookmarks::GotoBookmark(int item)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (item < 0 || item >= m_vecItems->Size() || !appPlayer->HasPlayer())
+ return;
+
+ CFileItemPtr fileItem = m_vecItems->Get(item);
+ int chapter = static_cast<int>(fileItem->GetProperty("chapter").asInteger());
+ if (chapter <= 0)
+ {
+ appPlayer->SetPlayerState(fileItem->GetProperty("playerstate").asString());
+ g_application.SeekTime(fileItem->GetProperty("resumepoint").asDouble());
+ }
+ else
+ appPlayer->SeekChapter(chapter);
+
+ Close();
+}
+
+void CGUIDialogVideoBookmarks::ClearBookmarks()
+{
+ CVideoDatabase videoDatabase;
+ videoDatabase.Open();
+ std::string path = g_application.CurrentFile();
+ if (g_application.CurrentFileItem().HasProperty("original_listitem_url") &&
+ !URIUtils::IsVideoDb(g_application.CurrentFileItem().GetProperty("original_listitem_url").asString()))
+ path = g_application.CurrentFileItem().GetProperty("original_listitem_url").asString();
+ videoDatabase.ClearBookMarksOfFile(path, CBookmark::STANDARD);
+ videoDatabase.ClearBookMarksOfFile(path, CBookmark::RESUME);
+ videoDatabase.ClearBookMarksOfFile(path, CBookmark::EPISODE);
+ videoDatabase.Close();
+ Update();
+}
+
+bool CGUIDialogVideoBookmarks::AddBookmark(CVideoInfoTag* tag)
+{
+ CVideoDatabase videoDatabase;
+ CBookmark bookmark;
+ bookmark.timeInSeconds = (int)g_application.GetTime();
+ bookmark.totalTimeInSeconds = (int)g_application.GetTotalTime();
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ if (appPlayer->HasPlayer())
+ bookmark.playerState = appPlayer->GetPlayerState();
+ else
+ bookmark.playerState.clear();
+
+ bookmark.player = g_application.GetCurrentPlayer();
+
+ // create the thumbnail image
+ float aspectRatio = appPlayer->GetRenderAspectRatio();
+ int width = BOOKMARK_THUMB_WIDTH;
+ int height = (int)(BOOKMARK_THUMB_WIDTH / aspectRatio);
+ if (height > (int)BOOKMARK_THUMB_WIDTH)
+ {
+ height = BOOKMARK_THUMB_WIDTH;
+ width = (int)(BOOKMARK_THUMB_WIDTH * aspectRatio);
+ }
+
+
+ uint8_t *pixels = (uint8_t*)malloc(height * width * 4);
+ unsigned int captureId = appPlayer->RenderCaptureAlloc();
+
+ appPlayer->RenderCapture(captureId, width, height, CAPTUREFLAG_IMMEDIATELY);
+ bool hasImage = appPlayer->RenderCaptureGetPixels(captureId, 1000, pixels, height * width * 4);
+
+ if (hasImage)
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ auto crc = Crc32::ComputeFromLowerCase(g_application.CurrentFile());
+ bookmark.thumbNailImage =
+ StringUtils::Format("{:08x}_{}.jpg", crc, (int)bookmark.timeInSeconds);
+ bookmark.thumbNailImage = URIUtils::AddFileToFolder(profileManager->GetBookmarksThumbFolder(), bookmark.thumbNailImage);
+
+ if (!CPicture::CreateThumbnailFromSurface(pixels, width, height, width * 4,
+ bookmark.thumbNailImage))
+ {
+ bookmark.thumbNailImage.clear();
+ }
+ else
+ CLog::Log(LOGERROR,"CGUIDialogVideoBookmarks: failed to create thumbnail");
+
+ appPlayer->RenderCaptureRelease(captureId);
+ }
+ else
+ CLog::Log(LOGERROR,"CGUIDialogVideoBookmarks: failed to create thumbnail 2");
+
+ free(pixels);
+
+ videoDatabase.Open();
+ if (tag)
+ videoDatabase.AddBookMarkForEpisode(*tag, bookmark);
+ else
+ {
+ std::string path = g_application.CurrentFile();
+ if (g_application.CurrentFileItem().HasProperty("original_listitem_url") &&
+ !URIUtils::IsVideoDb(g_application.CurrentFileItem().GetProperty("original_listitem_url").asString()))
+ path = g_application.CurrentFileItem().GetProperty("original_listitem_url").asString();
+ videoDatabase.AddBookMarkToFile(path, bookmark, CBookmark::STANDARD);
+ }
+ videoDatabase.Close();
+ return true;
+}
+
+void CGUIDialogVideoBookmarks::OnWindowLoaded()
+{
+ CGUIDialog::OnWindowLoaded();
+ m_viewControl.Reset();
+ m_viewControl.SetParentWindow(GetID());
+ m_viewControl.AddView(GetControl(CONTROL_THUMBS));
+ m_jobsStarted = 0;
+ m_mapJobsChapter.clear();
+ m_vecItems->Clear();
+}
+
+void CGUIDialogVideoBookmarks::OnWindowUnload()
+{
+ //stop running thumb extraction jobs
+ CancelJobs();
+ m_mapJobsChapter.clear();
+ m_vecItems->Clear();
+ CGUIDialog::OnWindowUnload();
+ m_viewControl.Reset();
+}
+
+CGUIControl *CGUIDialogVideoBookmarks::GetFirstFocusableControl(int id)
+{
+ if (m_viewControl.HasControl(id))
+ id = m_viewControl.GetCurrentControl();
+ return CGUIWindow::GetFirstFocusableControl(id);
+}
+
+bool CGUIDialogVideoBookmarks::AddEpisodeBookmark()
+{
+ std::vector<CVideoInfoTag> episodes;
+ CVideoDatabase videoDatabase;
+ videoDatabase.Open();
+ videoDatabase.GetEpisodesByFile(g_application.CurrentFile(), episodes);
+ videoDatabase.Close();
+ if (!episodes.empty())
+ {
+ CContextButtons choices;
+ for (unsigned int i=0; i < episodes.size(); ++i)
+ {
+ std::string strButton =
+ StringUtils::Format("{} {}, {} {}", g_localizeStrings.Get(20373), episodes[i].m_iSeason,
+ g_localizeStrings.Get(20359), episodes[i].m_iEpisode);
+ choices.Add(i, strButton);
+ }
+
+ int pressed = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (pressed >= 0)
+ {
+ AddBookmark(&episodes[pressed]);
+ return true;
+ }
+ }
+ return false;
+}
+
+
+
+bool CGUIDialogVideoBookmarks::OnAddBookmark()
+{
+ if (!g_application.CurrentFileItem().IsVideo())
+ return false;
+
+ if (CGUIDialogVideoBookmarks::AddBookmark())
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_REFRESH_LIST, 0, WINDOW_DIALOG_VIDEO_BOOKMARKS);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info,
+ g_localizeStrings.Get(298), // "Bookmarks"
+ g_localizeStrings.Get(21362));// "Bookmark created"
+ return true;
+ }
+ return false;
+}
+
+bool CGUIDialogVideoBookmarks::OnAddEpisodeBookmark()
+{
+ bool bReturn = false;
+ if (g_application.CurrentFileItem().HasVideoInfoTag() && g_application.CurrentFileItem().GetVideoInfoTag()->m_iEpisode > -1)
+ {
+ CVideoDatabase videoDatabase;
+ videoDatabase.Open();
+ std::vector<CVideoInfoTag> episodes;
+ videoDatabase.GetEpisodesByFile(g_application.CurrentFile(),episodes);
+ if (episodes.size() > 1)
+ {
+ bReturn = CGUIDialogVideoBookmarks::AddEpisodeBookmark();
+ if(bReturn)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_REFRESH_LIST, 0, WINDOW_DIALOG_VIDEO_BOOKMARKS);
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info,
+ g_localizeStrings.Get(298), // "Bookmarks"
+ g_localizeStrings.Get(21363));// "Episode Bookmark created"
+
+ }
+ }
+ videoDatabase.Close();
+ }
+ return bReturn;
+}
+
+void CGUIDialogVideoBookmarks::OnJobComplete(unsigned int jobID,
+ bool success, CJob* job)
+{
+ if (success && IsActive())
+ {
+ MAPJOBSCHAPS::iterator iter = m_mapJobsChapter.find(job);
+ if (iter != m_mapJobsChapter.end())
+ {
+ unsigned int chapterIdx = (*iter).second;
+ CGUIMessage m(GUI_MSG_REFRESH_LIST, GetID(), 0, 1, chapterIdx);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(m);
+ m_mapJobsChapter.erase(iter);
+ }
+ }
+ CJobQueue::OnJobComplete(jobID, success, job);
+}
diff --git a/xbmc/video/dialogs/GUIDialogVideoBookmarks.h b/xbmc/video/dialogs/GUIDialogVideoBookmarks.h
new file mode 100644
index 0000000..801afcd
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogVideoBookmarks.h
@@ -0,0 +1,76 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "utils/JobManager.h"
+#include "video/VideoDatabase.h"
+#include "view/GUIViewControl.h"
+
+class CFileItemList;
+
+class CGUIDialogVideoBookmarks : public CGUIDialog, public CJobQueue
+{
+ typedef std::map<CJob*, unsigned int> MAPJOBSCHAPS;
+
+public:
+ CGUIDialogVideoBookmarks(void);
+ ~CGUIDialogVideoBookmarks(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ bool OnAction(const CAction &action) override;
+
+ /*!
+ \brief Creates a bookmark of the currently playing video file.
+
+ NOTE: sends a GUI_MSG_REFRESH_LIST message to DialogVideoBookmark on success
+ \return True if creation of bookmark was successful
+ \sa OnAddEpisodeBookmark
+ */
+ static bool OnAddBookmark();
+
+ /*!
+ \brief Creates an episode bookmark of the currently playing file
+
+ An episode bookmark specifies the end/beginning of episodes on files like: S01E01E02
+ Fails if the current video isn't a multi-episode file
+ NOTE: sends a GUI_MSG_REFRESH_LIST message to DialogVideoBookmark on success
+ \return True, if bookmark was successfully created
+ \sa OnAddBookmark
+ **/
+ static bool OnAddEpisodeBookmark();
+
+
+ void Update();
+protected:
+ void GotoBookmark(int iItem);
+ void ClearBookmarks();
+ static bool AddEpisodeBookmark();
+ static bool AddBookmark(CVideoInfoTag *tag=NULL);
+ void Delete(int item);
+ void Clear();
+ void OnRefreshList();
+ void OnPopupMenu(int item);
+ CGUIControl *GetFirstFocusableControl(int id) override;
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob* job) override;
+
+ CFileItemList* m_vecItems;
+ CGUIViewControl m_viewControl;
+ VECBOOKMARKS m_bookmarks;
+
+private:
+ void UpdateItem(unsigned int chapterIdx);
+
+ int m_jobsStarted;
+ std::string m_filePath;
+ CCriticalSection m_refreshSection;
+ MAPJOBSCHAPS m_mapJobsChapter;
+};
diff --git a/xbmc/video/dialogs/GUIDialogVideoInfo.cpp b/xbmc/video/dialogs/GUIDialogVideoInfo.cpp
new file mode 100644
index 0000000..240d672
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogVideoInfo.cpp
@@ -0,0 +1,2398 @@
+/*
+ * 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 "GUIDialogVideoInfo.h"
+
+#include "ContextMenuManager.h"
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "TextureCache.h"
+#include "Util.h"
+#include "dialogs/GUIDialogFileBrowser.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/Directory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "filesystem/VideoDatabaseDirectory/QueryParams.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIImage.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindow.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicDatabase.h"
+#include "music/dialogs/GUIDialogMusicInfo.h"
+#include "playlists/PlayListTypes.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/SettingUtils.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "storage/MediaManager.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "video/VideoDbUrl.h"
+#include "video/VideoInfoScanner.h"
+#include "video/VideoInfoTag.h"
+#include "video/VideoLibraryQueue.h"
+#include "video/VideoThumbLoader.h"
+#include "video/tags/VideoTagLoaderFFmpeg.h"
+#include "video/windows/GUIWindowVideoNav.h"
+
+#include <iterator>
+#include <string>
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+using namespace XFILE;
+using namespace KODI::MESSAGING;
+
+#define CONTROL_IMAGE 3
+#define CONTROL_TEXTAREA 4
+#define CONTROL_BTN_TRACKS 5
+#define CONTROL_BTN_REFRESH 6
+#define CONTROL_BTN_USERRATING 7
+#define CONTROL_BTN_PLAY 8
+#define CONTROL_BTN_RESUME 9
+#define CONTROL_BTN_GET_THUMB 10
+#define CONTROL_BTN_PLAY_TRAILER 11
+#define CONTROL_BTN_GET_FANART 12
+#define CONTROL_BTN_DIRECTOR 13
+
+#define CONTROL_LIST 50
+
+// predicate used by sorting and set_difference
+bool compFileItemsByDbId(const CFileItemPtr& lhs, const CFileItemPtr& rhs)
+{
+ return lhs->HasVideoInfoTag() && rhs->HasVideoInfoTag() && lhs->GetVideoInfoTag()->m_iDbId < rhs->GetVideoInfoTag()->m_iDbId;
+}
+
+CGUIDialogVideoInfo::CGUIDialogVideoInfo(void)
+ : CGUIDialog(WINDOW_DIALOG_VIDEO_INFO, "DialogVideoInfo.xml"),
+ m_movieItem(new CFileItem),
+ m_castList(new CFileItemList)
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogVideoInfo::~CGUIDialogVideoInfo(void)
+{
+ delete m_castList;
+}
+
+bool CGUIDialogVideoInfo::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ ClearCastList();
+
+ if (m_startUserrating != m_movieItem->GetVideoInfoTag()->m_iUserRating)
+ {
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ m_hasUpdatedUserrating = true;
+ db.SetVideoUserRating(m_movieItem->GetVideoInfoTag()->m_iDbId, m_movieItem->GetVideoInfoTag()->m_iUserRating, m_movieItem->GetVideoInfoTag()->m_type);
+ db.Close();
+ }
+ }
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTN_REFRESH)
+ {
+ if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeTvShow)
+ {
+ bool bCanceled=false;
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{20377}, CVariant{20378}, bCanceled, CVariant{ "" }, CVariant{ "" }, CGUIDialogYesNo::NO_TIMEOUT))
+ {
+ m_bRefreshAll = true;
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ db.SetPathHash(m_movieItem->GetVideoInfoTag()->m_strPath,"");
+ db.Close();
+ }
+ }
+ else
+ m_bRefreshAll = false;
+
+ if (bCanceled)
+ return false;
+ }
+ m_bRefresh = true;
+ Close();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_TRACKS)
+ {
+ m_bViewReview = !m_bViewReview;
+ Update();
+ }
+ else if (iControl == CONTROL_BTN_PLAY)
+ {
+ Play();
+ }
+ else if (iControl == CONTROL_BTN_USERRATING)
+ {
+ OnSetUserrating();
+ }
+ else if (iControl == CONTROL_BTN_RESUME)
+ {
+ Play(true);
+ }
+ else if (iControl == CONTROL_BTN_GET_THUMB)
+ {
+ OnGetArt();
+ }
+ else if (iControl == CONTROL_BTN_PLAY_TRAILER)
+ {
+ PlayTrailer();
+ }
+ else if (iControl == CONTROL_BTN_GET_FANART)
+ {
+ OnGetFanart();
+ }
+ else if (iControl == CONTROL_BTN_DIRECTOR)
+ {
+ auto directors = m_movieItem->GetVideoInfoTag()->m_director;
+ if (directors.size() == 0)
+ return true;
+ if (directors.size() == 1)
+ OnSearch(directors[0]);
+ else
+ {
+ auto pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (pDlgSelect)
+ {
+ pDlgSelect->Reset();
+ pDlgSelect->SetHeading(CVariant{22080});
+ for (const auto &director: directors)
+ pDlgSelect->Add(director);
+ pDlgSelect->Open();
+
+ int iItem = pDlgSelect->GetSelectedItem();
+ if (iItem < 0)
+ return true;
+ OnSearch(directors[iItem]);
+ }
+ }
+ }
+ else if (iControl == CONTROL_LIST)
+ {
+ int iAction = message.GetParam1();
+ if (ACTION_SELECT_ITEM == iAction || ACTION_MOUSE_LEFT_CLICK == iAction)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl);
+ OnMessage(msg);
+ int iItem = msg.GetParam1();
+ if (iItem < 0 || iItem >= m_castList->Size())
+ break;
+ std::string strItem = m_castList->Get(iItem)->GetLabel();
+ if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeVideoCollection)
+ {
+ SetMovie(m_castList->Get(iItem).get());
+ Close();
+ Open();
+ }
+ else
+ OnSearch(strItem);
+ }
+ }
+ }
+ break;
+ case GUI_MSG_NOTIFY_ALL:
+ {
+ if (IsActive() && message.GetParam1() == GUI_MSG_UPDATE_ITEM && message.GetItem())
+ {
+ CFileItemPtr item = std::static_pointer_cast<CFileItem>(message.GetItem());
+ if (item && m_movieItem->IsPath(item->GetPath()))
+ { // Just copy over the stream details and the thumb if we don't already have one
+ if (!m_movieItem->HasArt("thumb"))
+ m_movieItem->SetArt("thumb", item->GetArt("thumb"));
+ m_movieItem->GetVideoInfoTag()->m_streamDetails = item->GetVideoInfoTag()->m_streamDetails;
+ }
+ return true;
+ }
+ }
+ }
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIDialogVideoInfo::OnInitWindow()
+{
+ m_bRefresh = false;
+ m_bRefreshAll = true;
+ m_hasUpdatedThumb = false;
+ m_hasUpdatedUserrating = false;
+ m_bViewReview = true;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ const std::string uniqueId = m_movieItem->GetProperty("xxuniqueid").asString();
+ if (uniqueId.empty() || !StringUtils::StartsWithNoCase(uniqueId.c_str(), "xx"))
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_REFRESH,
+ (profileManager->GetCurrentProfile().canWriteDatabases() ||
+ g_passwordManager.bMasterUser));
+ else
+ CONTROL_DISABLE(CONTROL_BTN_REFRESH);
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_GET_THUMB,
+ (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser) &&
+ !StringUtils::StartsWithNoCase(m_movieItem->GetVideoInfoTag()->
+ GetUniqueID().c_str(), "plugin"));
+ // Disable video user rating button for plugins and sets as they don't have tables to save this
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_USERRATING, !m_movieItem->IsPlugin() && m_movieItem->GetVideoInfoTag()->m_type != MediaTypeVideoCollection);
+
+ VideoDbContentType type = m_movieItem->GetVideoContentType();
+ if (type == VideoDbContentType::TVSHOWS || type == VideoDbContentType::MOVIES)
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BTN_GET_FANART, (profileManager->
+ GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser) &&
+ !StringUtils::StartsWithNoCase(m_movieItem->GetVideoInfoTag()->
+ GetUniqueID().c_str(), "plugin"));
+ else
+ CONTROL_DISABLE(CONTROL_BTN_GET_FANART);
+
+ Update();
+
+ CGUIDialog::OnInitWindow();
+}
+
+bool CGUIDialogVideoInfo::OnAction(const CAction &action)
+{
+ int userrating = m_movieItem->GetVideoInfoTag()->m_iUserRating;
+ if (action.GetID() == ACTION_INCREASE_RATING)
+ {
+ SetUserrating(userrating + 1);
+ return true;
+ }
+ else if (action.GetID() == ACTION_DECREASE_RATING)
+ {
+ SetUserrating(userrating - 1);
+ return true;
+ }
+ else if (action.GetID() == ACTION_SHOW_INFO)
+ {
+ Close();
+ return true;
+ }
+ return CGUIDialog::OnAction(action);
+}
+
+void CGUIDialogVideoInfo::SetUserrating(int userrating) const
+{
+ userrating = std::max(userrating, 0);
+ userrating = std::min(userrating, 10);
+ if (userrating != m_movieItem->GetVideoInfoTag()->m_iUserRating)
+ {
+ m_movieItem->GetVideoInfoTag()->SetUserrating(userrating);
+
+ // send a message to all windows to tell them to update the fileitem (eg playlistplayer, media windows)
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, m_movieItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+}
+
+void CGUIDialogVideoInfo::SetMovie(const CFileItem *item)
+{
+ *m_movieItem = *item;
+
+ // setup cast list
+ ClearCastList();
+
+ // When the scraper throws an error, the video tag can be null here
+ if (!item->HasVideoInfoTag())
+ return;
+
+ MediaType type = item->GetVideoInfoTag()->m_type;
+
+ m_startUserrating = m_movieItem->GetVideoInfoTag()->m_iUserRating;
+
+ if (type == MediaTypeMusicVideo)
+ { // music video
+ CMusicDatabase database;
+ database.Open();
+ const std::vector<std::string> &artists = m_movieItem->GetVideoInfoTag()->m_artist;
+ for (std::vector<std::string>::const_iterator it = artists.begin(); it != artists.end(); ++it)
+ {
+ int idArtist = database.GetArtistByName(*it);
+ std::string thumb = database.GetArtForItem(idArtist, MediaTypeArtist, "thumb");
+ CFileItemPtr item(new CFileItem(*it));
+ if (!thumb.empty())
+ item->SetArt("thumb", thumb);
+ item->SetArt("icon", "DefaultArtist.png");
+ item->SetLabel2(g_localizeStrings.Get(29904));
+ m_castList->Add(item);
+ }
+ // get performers in the music video (added as actors)
+ for (CVideoInfoTag::iCast it = m_movieItem->GetVideoInfoTag()->m_cast.begin();
+ it != m_movieItem->GetVideoInfoTag()->m_cast.end(); ++it)
+ {
+ // Check to see if we have already added this performer as the artist and skip adding if so
+ auto haveArtist = std::find(std::begin(artists), std::end(artists), it->strName);
+ if (haveArtist == artists.end()) // artist or performer not already in the list
+ {
+ CFileItemPtr item(new CFileItem(it->strName));
+ if (!it->thumb.empty())
+ item->SetArt("thumb", it->thumb);
+ else if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_VIDEOLIBRARY_ACTORTHUMBS))
+ { // backward compatibility
+ std::string thumb = CScraperUrl::GetThumbUrl(it->thumbUrl.GetFirstUrlByType());
+ if (!thumb.empty())
+ {
+ item->SetArt("thumb", thumb);
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumb);
+ }
+ }
+ item->SetArt("icon", "DefaultActor.png");
+ item->SetLabel(it->strName);
+ item->SetLabel2(it->strRole);
+ m_castList->Add(item);
+ }
+ }
+ }
+ else if (type == MediaTypeVideoCollection)
+ {
+ CVideoDatabase database;
+ database.Open();
+ database.GetMoviesNav(m_movieItem->GetPath(), *m_castList, -1, -1, -1, -1, -1, -1,
+ m_movieItem->GetVideoInfoTag()->m_set.id, -1,
+ SortDescription(), VideoDbDetailsAll);
+ m_castList->Sort(SortBySortTitle, SortOrderDescending);
+ CVideoThumbLoader loader;
+ for (auto& item : *m_castList)
+ loader.LoadItem(item.get());
+ }
+ else
+ { // movie/show/episode
+ for (CVideoInfoTag::iCast it = m_movieItem->GetVideoInfoTag()->m_cast.begin(); it != m_movieItem->GetVideoInfoTag()->m_cast.end(); ++it)
+ {
+ CFileItemPtr item(new CFileItem(it->strName));
+ if (!it->thumb.empty())
+ item->SetArt("thumb", it->thumb);
+ else
+ {
+ const std::shared_ptr<CSettings> settings =
+ CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (settings->GetInt(CSettings::SETTING_VIDEOLIBRARY_ARTWORK_LEVEL) !=
+ CSettings::VIDEOLIBRARY_ARTWORK_LEVEL_NONE &&
+ settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_ACTORTHUMBS))
+ { // backward compatibility
+ std::string thumb = CScraperUrl::GetThumbUrl(it->thumbUrl.GetFirstUrlByType());
+ if (!thumb.empty())
+ {
+ item->SetArt("thumb", thumb);
+ CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumb);
+ }
+ }
+ }
+ item->SetArt("icon", "DefaultActor.png");
+ item->SetLabel(it->strName);
+ item->SetLabel2(it->strRole);
+ m_castList->Add(item);
+ }
+ }
+
+ if (type == MediaTypeMovie)
+ {
+ // local trailers should always override non-local, so check
+ // for a local one if the registered trailer is online
+ if (m_movieItem->GetVideoInfoTag()->m_strTrailer.empty() ||
+ URIUtils::IsInternetStream(m_movieItem->GetVideoInfoTag()->m_strTrailer))
+ {
+ std::string localTrailer = m_movieItem->FindTrailer();
+ if (!localTrailer.empty())
+ {
+ m_movieItem->GetVideoInfoTag()->m_strTrailer = localTrailer;
+ CVideoDatabase database;
+ if (database.Open())
+ {
+ database.SetSingleValue(VideoDbContentType::MOVIES, VIDEODB_ID_TRAILER,
+ m_movieItem->GetVideoInfoTag()->m_iDbId,
+ m_movieItem->GetVideoInfoTag()->m_strTrailer);
+ database.Close();
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ }
+ }
+ }
+ }
+
+ m_castList->SetContent(CMediaTypes::ToPlural(type));
+
+ CVideoThumbLoader loader;
+ loader.LoadItem(m_movieItem.get());
+}
+
+void CGUIDialogVideoInfo::Update()
+{
+ // setup plot text area
+ std::shared_ptr<CSettingList> setting(std::dynamic_pointer_cast<CSettingList>(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(CSettings::SETTING_VIDEOLIBRARY_SHOWUNWATCHEDPLOTS)));
+ std::string strTmp = m_movieItem->GetVideoInfoTag()->m_strPlot;
+ if (m_movieItem->GetVideoInfoTag()->m_type != MediaTypeTvShow)
+ if (m_movieItem->GetVideoInfoTag()->GetPlayCount() == 0 && setting &&
+ ((m_movieItem->GetVideoInfoTag()->m_type == MediaTypeMovie &&
+ !CSettingUtils::FindIntInList(setting,
+ CSettings::VIDEOLIBRARY_PLOTS_SHOW_UNWATCHED_MOVIES)) ||
+ (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeEpisode &&
+ !CSettingUtils::FindIntInList(
+ setting, CSettings::VIDEOLIBRARY_PLOTS_SHOW_UNWATCHED_TVSHOWEPISODES))))
+ strTmp = g_localizeStrings.Get(20370);
+
+ StringUtils::Trim(strTmp);
+ SetLabel(CONTROL_TEXTAREA, strTmp);
+
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_LIST, 0, 0, m_castList);
+ OnMessage(msg);
+
+ if (GetControl(CONTROL_BTN_TRACKS)) // if no CONTROL_BTN_TRACKS found - allow skinner full visibility control over CONTROL_TEXTAREA and CONTROL_LIST
+ {
+ if (m_bViewReview)
+ {
+ if (!m_movieItem->GetVideoInfoTag()->m_artist.empty())
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_TRACKS, 133);
+ }
+ else if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeVideoCollection)
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_TRACKS, 20342);
+ }
+ else
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_TRACKS, 206);
+ }
+
+ SET_CONTROL_HIDDEN(CONTROL_LIST);
+ SET_CONTROL_VISIBLE(CONTROL_TEXTAREA);
+ }
+ else
+ {
+ SET_CONTROL_LABEL(CONTROL_BTN_TRACKS, 207);
+
+ SET_CONTROL_HIDDEN(CONTROL_TEXTAREA);
+ SET_CONTROL_VISIBLE(CONTROL_LIST);
+ }
+ }
+
+ // Check for resumability
+ if (m_movieItem->GetVideoInfoTag()->GetResumePoint().timeInSeconds > 0.0)
+ CONTROL_ENABLE(CONTROL_BTN_RESUME);
+ else
+ CONTROL_DISABLE(CONTROL_BTN_RESUME);
+
+ CONTROL_ENABLE(CONTROL_BTN_PLAY);
+
+ // update the thumbnail
+ CGUIControl* pControl = GetControl(CONTROL_IMAGE);
+ if (pControl)
+ {
+ CGUIImage* pImageControl = static_cast<CGUIImage*>(pControl);
+ pImageControl->FreeResources();
+ pImageControl->SetFileName(m_movieItem->GetArt("thumb"));
+ }
+ // tell our GUI to completely reload all controls (as some of them
+ // are likely to have had this image in use so will need refreshing)
+ if (m_hasUpdatedThumb)
+ {
+ CGUIMessage reload(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_REFRESH_THUMBS);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(reload);
+ }
+}
+
+bool CGUIDialogVideoInfo::NeedRefresh() const
+{
+ return m_bRefresh;
+}
+
+bool CGUIDialogVideoInfo::RefreshAll() const
+{
+ return m_bRefreshAll;
+}
+
+void CGUIDialogVideoInfo::OnSearch(std::string& strSearch)
+{
+ CGUIDialogProgress *progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (progress)
+ {
+ progress->SetHeading(CVariant{194});
+ progress->SetLine(0, CVariant{strSearch});
+ progress->SetLine(1, CVariant{""});
+ progress->SetLine(2, CVariant{""});
+ progress->Open();
+ progress->Progress();
+ }
+ CFileItemList items;
+ DoSearch(strSearch, items);
+
+ if (progress)
+ progress->Close();
+
+ if (items.Size())
+ {
+ CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (pDlgSelect)
+ {
+ pDlgSelect->Reset();
+ pDlgSelect->SetHeading(CVariant{283});
+
+ CVideoThumbLoader loader;
+ for (int i = 0; i < items.Size(); i++)
+ {
+ if (items[i]->HasVideoInfoTag() &&
+ items[i]->GetVideoInfoTag()->GetPlayCount() > 0)
+ items[i]->SetLabel2(g_localizeStrings.Get(16102));
+
+ loader.LoadItem(items[i].get());
+ pDlgSelect->Add(*items[i]);
+ }
+
+ pDlgSelect->SetUseDetails(true);
+ pDlgSelect->Open();
+
+ int iItem = pDlgSelect->GetSelectedItem();
+ if (iItem < 0)
+ return;
+
+ OnSearchItemFound(items[iItem].get());
+ }
+ }
+ else
+ {
+ HELPERS::ShowOKDialogText(CVariant{194}, CVariant{284});
+ }
+}
+
+void CGUIDialogVideoInfo::DoSearch(std::string& strSearch, CFileItemList& items) const
+{
+ CVideoDatabase db;
+ if (!db.Open())
+ return;
+
+ CFileItemList movies;
+ db.GetMoviesByActor(strSearch, movies);
+ for (int i = 0; i < movies.Size(); ++i)
+ {
+ std::string label = movies[i]->GetVideoInfoTag()->m_strTitle;
+ if (movies[i]->GetVideoInfoTag()->HasYear())
+ label += StringUtils::Format(" ({})", movies[i]->GetVideoInfoTag()->GetYear());
+ movies[i]->SetLabel(label);
+ }
+ CGUIWindowVideoBase::AppendAndClearSearchItems(movies, "[" + g_localizeStrings.Get(20338) + "] ", items);
+
+ db.GetTvShowsByActor(strSearch, movies);
+ for (int i = 0; i < movies.Size(); ++i)
+ {
+ std::string label = movies[i]->GetVideoInfoTag()->m_strShowTitle;
+ if (movies[i]->GetVideoInfoTag()->HasYear())
+ label += StringUtils::Format(" ({})", movies[i]->GetVideoInfoTag()->GetYear());
+ movies[i]->SetLabel(label);
+ }
+ CGUIWindowVideoBase::AppendAndClearSearchItems(movies, "[" + g_localizeStrings.Get(20364) + "] ", items);
+
+ db.GetEpisodesByActor(strSearch, movies);
+ for (int i = 0; i < movies.Size(); ++i)
+ {
+ std::string label = movies[i]->GetVideoInfoTag()->m_strTitle + " (" + movies[i]->GetVideoInfoTag()->m_strShowTitle + ")";
+ movies[i]->SetLabel(label);
+ }
+ CGUIWindowVideoBase::AppendAndClearSearchItems(movies, "[" + g_localizeStrings.Get(20359) + "] ", items);
+
+ db.GetMusicVideosByArtist(strSearch, movies);
+ for (int i = 0; i < movies.Size(); ++i)
+ {
+ std::string label = StringUtils::Join(movies[i]->GetVideoInfoTag()->m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator) + " - " + movies[i]->GetVideoInfoTag()->m_strTitle;
+ if (movies[i]->GetVideoInfoTag()->HasYear())
+ label += StringUtils::Format(" ({})", movies[i]->GetVideoInfoTag()->GetYear());
+ movies[i]->SetLabel(label);
+ }
+ CGUIWindowVideoBase::AppendAndClearSearchItems(movies, "[" + g_localizeStrings.Get(20391) + "] ", items);
+ db.Close();
+
+ // Search for music albums by artist with name matching search string
+ CMusicDatabase music_database;
+ if (!music_database.Open())
+ return;
+
+ if (music_database.SearchAlbumsByArtistName(strSearch, movies))
+ {
+ for (int i = 0; i < movies.Size(); ++i)
+ {
+ // Set type so that video thumbloader handles album art
+ movies[i]->GetVideoInfoTag()->m_type = MediaTypeAlbum;
+ }
+ CGUIWindowVideoBase::AppendAndClearSearchItems(
+ movies, "[" + g_localizeStrings.Get(36918) + "] ", items);
+ }
+ music_database.Close();
+}
+
+void CGUIDialogVideoInfo::OnSearchItemFound(const CFileItem* pItem)
+{
+ VideoDbContentType type = pItem->GetVideoContentType();
+
+ CVideoDatabase db;
+ if (!db.Open())
+ return;
+
+ CVideoInfoTag movieDetails;
+ if (type == VideoDbContentType::MOVIES)
+ db.GetMovieInfo(pItem->GetPath(), movieDetails, pItem->GetVideoInfoTag()->m_iDbId);
+ if (type == VideoDbContentType::EPISODES)
+ db.GetEpisodeInfo(pItem->GetPath(), movieDetails, pItem->GetVideoInfoTag()->m_iDbId);
+ if (type == VideoDbContentType::TVSHOWS)
+ db.GetTvShowInfo(pItem->GetPath(), movieDetails, pItem->GetVideoInfoTag()->m_iDbId);
+ if (type == VideoDbContentType::MUSICVIDEOS)
+ db.GetMusicVideoInfo(pItem->GetPath(), movieDetails, pItem->GetVideoInfoTag()->m_iDbId);
+ db.Close();
+ if (type == VideoDbContentType::MUSICALBUMS)
+ {
+ Close();
+ CGUIDialogMusicInfo::ShowFor(const_cast<CFileItem*>(pItem));
+ return; // No video info to refresh so just close the window and go back to the fileitem list
+ }
+
+ CFileItem item(*pItem);
+ *item.GetVideoInfoTag() = movieDetails;
+ SetMovie(&item);
+ // refresh our window entirely
+ Close();
+ Open();
+}
+
+void CGUIDialogVideoInfo::ClearCastList()
+{
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), CONTROL_LIST);
+ OnMessage(msg);
+ m_castList->Clear();
+}
+
+void CGUIDialogVideoInfo::Play(bool resume)
+{
+ if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeTvShow)
+ {
+ std::string strPath;
+ if (m_movieItem->IsPlugin())
+ {
+ strPath = m_movieItem->GetPath();
+ Close();
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_NAV)
+ {
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, CServiceBroker::GetGUI()->
+ GetWindowManager().GetActiveWindow(), 0, GUI_MSG_UPDATE, 0);
+ message.SetStringParam(strPath);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
+ }
+ else
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_NAV,strPath);
+ }
+ else
+ {
+ strPath = StringUtils::Format("videodb://tvshows/titles/{}/",
+ m_movieItem->GetVideoInfoTag()->m_iDbId);
+ Close();
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_NAV,strPath);
+ }
+ return;
+ }
+
+ if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeVideoCollection)
+ {
+ std::string strPath = StringUtils::Format("videodb://movies/sets/{}/?setid={}",
+ m_movieItem->GetVideoInfoTag()->m_iDbId,
+ m_movieItem->GetVideoInfoTag()->m_iDbId);
+ Close();
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_NAV, strPath);
+ return;
+ }
+
+ CGUIWindowVideoNav* pWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowVideoNav>(WINDOW_VIDEO_NAV);
+ if (pWindow)
+ {
+ // close our dialog
+ Close(true);
+ if (resume)
+ m_movieItem->SetStartOffset(STARTOFFSET_RESUME);
+ else if (!CGUIWindowVideoBase::ShowResumeMenu(*m_movieItem))
+ {
+ // The Resume dialog was closed without any choice
+ Open();
+ return;
+ }
+ m_movieItem->SetProperty("playlist_type_hint", PLAYLIST::TYPE_VIDEO);
+
+ pWindow->PlayMovie(m_movieItem.get());
+ }
+}
+
+namespace
+{
+// Add art types required in Kodi and configured by the user
+void AddHardCodedAndExtendedArtTypes(std::vector<std::string>& artTypes, const CVideoInfoTag& tag)
+{
+ for (const auto& artType : CVideoThumbLoader::GetArtTypes(tag.m_type))
+ {
+ if (find(artTypes.cbegin(), artTypes.cend(), artType) == artTypes.cend())
+ artTypes.emplace_back(artType);
+ }
+}
+
+// Add art types currently assigned to the media item
+void AddCurrentArtTypes(std::vector<std::string>& artTypes, const CVideoInfoTag& tag,
+ CVideoDatabase& db)
+{
+ std::map<std::string, std::string> currentArt;
+ db.GetArtForItem(tag.m_iDbId, tag.m_type, currentArt);
+ for (const auto& art : currentArt)
+ {
+ if (!art.second.empty() && find(artTypes.cbegin(), artTypes.cend(), art.first) == artTypes.cend())
+ artTypes.push_back(art.first);
+ }
+}
+
+// Add art types that exist for other media items of the same type
+void AddMediaTypeArtTypes(std::vector<std::string>& artTypes, const CVideoInfoTag& tag,
+ CVideoDatabase& db)
+{
+ std::vector<std::string> dbArtTypes;
+ db.GetArtTypes(tag.m_type, dbArtTypes);
+ for (const auto& artType : dbArtTypes)
+ {
+ if (find(artTypes.cbegin(), artTypes.cend(), artType) == artTypes.cend())
+ artTypes.push_back(artType);
+ }
+}
+
+// Add art types from available but unassigned artwork for this media item
+void AddAvailableArtTypes(std::vector<std::string>& artTypes, const CVideoInfoTag& tag,
+ CVideoDatabase& db)
+{
+ for (const auto& artType : db.GetAvailableArtTypesForItem(tag.m_iDbId, tag.m_type))
+ {
+ if (find(artTypes.cbegin(), artTypes.cend(), artType) == artTypes.cend())
+ artTypes.push_back(artType);
+ }
+}
+
+std::vector<std::string> GetArtTypesList(const CVideoInfoTag& tag)
+{
+ CVideoDatabase db;
+ db.Open();
+
+ std::vector<std::string> artTypes;
+
+ AddHardCodedAndExtendedArtTypes(artTypes, tag);
+ AddCurrentArtTypes(artTypes, tag, db);
+ AddMediaTypeArtTypes(artTypes, tag, db);
+ AddAvailableArtTypes(artTypes, tag, db);
+
+ db.Close();
+ return artTypes;
+}
+}
+
+std::string CGUIDialogVideoInfo::ChooseArtType(const CFileItem &videoItem)
+{
+ // prompt for choice
+ CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (!dialog || !videoItem.HasVideoInfoTag())
+ return "";
+
+ CFileItemList items;
+ dialog->SetHeading(CVariant{13511});
+ dialog->Reset();
+ dialog->SetUseDetails(true);
+ dialog->EnableButton(true, 13516);
+
+ std::vector<std::string> artTypes = GetArtTypesList(*videoItem.GetVideoInfoTag());
+
+ for (std::vector<std::string>::const_iterator i = artTypes.begin(); i != artTypes.end(); ++i)
+ {
+ const std::string& type = *i;
+ CFileItemPtr item(new CFileItem(type, false));
+ if (type == "banner")
+ item->SetLabel(g_localizeStrings.Get(20020));
+ else if (type == "fanart")
+ item->SetLabel(g_localizeStrings.Get(20445));
+ else if (type == "poster")
+ item->SetLabel(g_localizeStrings.Get(20021));
+ else if (type == "thumb")
+ item->SetLabel(g_localizeStrings.Get(21371));
+ else
+ item->SetLabel(type);
+ item->SetProperty("type", type);
+ if (videoItem.HasArt(type))
+ item->SetArt("thumb", videoItem.GetArt(type));
+ items.Add(item);
+ }
+
+ dialog->SetItems(items);
+ dialog->Open();
+
+ if (dialog->IsButtonPressed())
+ {
+ // Get the new artwork name
+ std::string strArtworkName;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strArtworkName, CVariant{g_localizeStrings.Get(13516)}, false))
+ return "";
+
+ return strArtworkName;
+ }
+
+ return dialog->GetSelectedFileItem()->GetProperty("type").asString();
+}
+
+void CGUIDialogVideoInfo::OnGetArt()
+{
+ if (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeVideoCollection)
+ {
+ ManageVideoItemArtwork(m_movieItem, m_movieItem->GetVideoInfoTag()->m_type);
+ return;
+ }
+ std::string type = ChooseArtType(*m_movieItem);
+ if (type.empty())
+ return; // cancelled
+
+ //! @todo this can be removed once these are unified.
+ if (type == "fanart")
+ OnGetFanart();
+ else
+ {
+ CFileItemList items;
+
+ // Current thumb
+ if (m_movieItem->HasArt(type))
+ {
+ CFileItemPtr item(new CFileItem("thumb://Current", false));
+ item->SetArt("thumb", m_movieItem->GetArt(type));
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13512));
+ items.Add(item);
+ }
+ else if ((type == "poster" || type == "banner") && m_movieItem->HasArt("thumb"))
+ { // add the 'thumb' type in
+ CFileItemPtr item(new CFileItem("thumb://Thumb", false));
+ item->SetArt("thumb", m_movieItem->GetArt("thumb"));
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13512));
+ items.Add(item);
+ }
+
+ std::string embeddedArt;
+ if (URIUtils::HasExtension(m_movieItem->GetVideoInfoTag()->m_strFileNameAndPath, ".mkv"))
+ {
+ CFileItem item(m_movieItem->GetVideoInfoTag()->m_strFileNameAndPath, false);
+ CVideoTagLoaderFFmpeg loader(item, nullptr, false);
+ CVideoInfoTag tag;
+ loader.Load(tag, false, nullptr);
+ for (const auto& it : tag.m_coverArt)
+ {
+ if (it.m_type == type)
+ {
+ CFileItemPtr itemF(new CFileItem("thumb://Embedded", false));
+ embeddedArt = CTextureUtils::GetWrappedImageURL(item.GetPath(), "video_" + type);
+ itemF->SetArt("thumb", embeddedArt);
+ itemF->SetLabel(g_localizeStrings.Get(13519));
+ items.Add(itemF);
+ }
+ }
+ }
+
+ // Grab the thumbnails from the web
+ m_movieItem->GetVideoInfoTag()->m_strPictureURL.Parse();
+ std::vector<std::string> thumbs;
+ int season = (m_movieItem->GetVideoInfoTag()->m_type == MediaTypeSeason) ? m_movieItem->GetVideoInfoTag()->m_iSeason : -1;
+ m_movieItem->GetVideoInfoTag()->m_strPictureURL.GetThumbUrls(thumbs, type, season);
+
+ for (unsigned int i = 0; i < thumbs.size(); ++i)
+ {
+ std::string strItemPath = StringUtils::Format("thumb://Remote{}", i);
+ CFileItemPtr item(new CFileItem(strItemPath, false));
+ item->SetArt("thumb", thumbs[i]);
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13513));
+
+ //! @todo Do we need to clear the cached image?
+ // CServiceBroker::GetTextureCache()->ClearCachedImage(thumb);
+ items.Add(item);
+ }
+
+ std::string localThumb = CVideoThumbLoader::GetLocalArt(*m_movieItem, type);
+ if (!localThumb.empty())
+ {
+ CFileItemPtr item(new CFileItem("thumb://Local", false));
+ item->SetArt("thumb", localThumb);
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13514));
+ items.Add(item);
+ }
+ else
+ { // no local thumb exists, so we are just using the IMDb thumb or cached thumb
+ // which is probably the IMDb thumb. These could be wrong, so allow the user
+ // to delete the incorrect thumb
+ CFileItemPtr item(new CFileItem("thumb://None", false));
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13515));
+ items.Add(item);
+ }
+
+ std::string result;
+ VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("video"));
+ AddItemPathToFileBrowserSources(sources, *m_movieItem);
+ CServiceBroker::GetMediaManager().GetLocalDrives(sources);
+ if (CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(13511), result) &&
+ result != "thumb://Current") // user didn't choose the one they have
+ {
+ std::string newThumb;
+ if (StringUtils::StartsWith(result, "thumb://Remote"))
+ {
+ int number = atoi(result.substr(14).c_str());
+ newThumb = thumbs[number];
+ }
+ else if (result == "thumb://Thumb")
+ newThumb = m_movieItem->GetArt("thumb");
+ else if (result == "thumb://Local")
+ newThumb = localThumb;
+ else if (result == "thumb://Embedded")
+ newThumb = embeddedArt;
+ else if (CFileUtils::Exists(result))
+ newThumb = result;
+ else // none
+ newThumb.clear();
+
+ // update thumb in the database
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ db.SetArtForItem(m_movieItem->GetVideoInfoTag()->m_iDbId, m_movieItem->GetVideoInfoTag()->m_type, type, newThumb);
+ db.Close();
+ }
+ CUtil::DeleteVideoDatabaseDirectoryCache(); // to get them new thumbs to show
+ m_movieItem->SetArt(type, newThumb);
+ if (m_movieItem->HasProperty("set_folder_thumb"))
+ { // have a folder thumb to set as well
+ VIDEO::CVideoInfoScanner::ApplyThumbToFolder(m_movieItem->GetProperty("set_folder_thumb").asString(), newThumb);
+ }
+ m_hasUpdatedThumb = true;
+ }
+ }
+
+ // Update our screen
+ Update();
+
+ // re-open the art selection dialog as we come back from
+ // the image selection dialog
+ OnGetArt();
+}
+
+// Allow user to select a Fanart
+void CGUIDialogVideoInfo::OnGetFanart()
+{
+ CFileItemList items;
+
+ // Ensure the fanart is unpacked
+ m_movieItem->GetVideoInfoTag()->m_fanart.Unpack();
+
+ if (m_movieItem->HasArt("fanart"))
+ {
+ CFileItemPtr itemCurrent(new CFileItem("fanart://Current",false));
+ itemCurrent->SetArt("thumb", m_movieItem->GetArt("fanart"));
+ itemCurrent->SetArt("icon", "DefaultPicture.png");
+ itemCurrent->SetLabel(g_localizeStrings.Get(20440));
+ items.Add(itemCurrent);
+ }
+
+ std::string embeddedArt;
+ if (URIUtils::HasExtension(m_movieItem->GetVideoInfoTag()->m_strFileNameAndPath, ".mkv"))
+ {
+ CFileItem item(m_movieItem->GetVideoInfoTag()->m_strFileNameAndPath, false);
+ CVideoTagLoaderFFmpeg loader(item, nullptr, false);
+ CVideoInfoTag tag;
+ loader.Load(tag, false, nullptr);
+ for (const auto& it : tag.m_coverArt)
+ {
+ if (it.m_type == "fanart")
+ {
+ CFileItemPtr itemF(new CFileItem("fanart://Embedded", false));
+ embeddedArt = CTextureUtils::GetWrappedImageURL(item.GetPath(), "video_fanart");
+ itemF->SetArt("thumb", embeddedArt);
+ itemF->SetLabel(g_localizeStrings.Get(13520));
+ items.Add(itemF);
+ }
+ }
+ }
+
+ // Grab the thumbnails from the web
+ for (unsigned int i = 0; i < m_movieItem->GetVideoInfoTag()->m_fanart.GetNumFanarts(); i++)
+ {
+ if (URIUtils::IsProtocol(m_movieItem->GetVideoInfoTag()->m_fanart.GetPreviewURL(i), "image"))
+ continue;
+ std::string strItemPath = StringUtils::Format("fanart://Remote{}", i);
+ CFileItemPtr item(new CFileItem(strItemPath, false));
+ std::string thumb = m_movieItem->GetVideoInfoTag()->m_fanart.GetPreviewURL(i);
+ item->SetArt("thumb", CTextureUtils::GetWrappedThumbURL(thumb));
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(20441));
+
+ //! @todo Do we need to clear the cached image?
+ // CServiceBroker::GetTextureCache()->ClearCachedImage(thumb);
+ items.Add(item);
+ }
+
+ CFileItem item(*m_movieItem->GetVideoInfoTag());
+ std::string strLocal = item.GetLocalFanart();
+ if (!strLocal.empty())
+ {
+ CFileItemPtr itemLocal(new CFileItem("fanart://Local",false));
+ itemLocal->SetArt("thumb", strLocal);
+ itemLocal->SetArt("icon", "DefaultPicture.png");
+ itemLocal->SetLabel(g_localizeStrings.Get(20438));
+
+ //! @todo Do we need to clear the cached image?
+ CServiceBroker::GetTextureCache()->ClearCachedImage(strLocal);
+ items.Add(itemLocal);
+ }
+ else
+ {
+ CFileItemPtr itemNone(new CFileItem("fanart://None", false));
+ itemNone->SetArt("icon", "DefaultPicture.png");
+ itemNone->SetLabel(g_localizeStrings.Get(20439));
+ items.Add(itemNone);
+ }
+
+ std::string result;
+ VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("video"));
+ AddItemPathToFileBrowserSources(sources, item);
+ CServiceBroker::GetMediaManager().GetLocalDrives(sources);
+ bool flip=false;
+ if (!CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(20437), result, &flip, 20445) ||
+ StringUtils::EqualsNoCase(result, "fanart://Current"))
+ return; // user cancelled
+
+ if (StringUtils::EqualsNoCase(result, "fanart://Local"))
+ result = strLocal;
+
+ if (StringUtils::EqualsNoCase(result, "fanart://Embedded"))
+ {
+ unsigned int current = m_movieItem->GetVideoInfoTag()->m_fanart.GetNumFanarts();
+ int found = -1;
+ for (size_t i = 0; i < current; ++i)
+ if (URIUtils::IsProtocol(m_movieItem->GetVideoInfoTag()->m_fanart.GetImageURL(), "image"))
+ found = i;
+ if (found != -1)
+ {
+ m_movieItem->GetVideoInfoTag()->m_fanart.AddFanart(embeddedArt, "", "");
+ found = current;
+ }
+
+ m_movieItem->GetVideoInfoTag()->m_fanart.SetPrimaryFanart(found);
+
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ db.UpdateFanart(*m_movieItem, m_movieItem->GetVideoContentType());
+ db.Close();
+ }
+ result = embeddedArt;
+ }
+
+ if (StringUtils::StartsWith(result, "fanart://Remote"))
+ {
+ int iFanart = atoi(result.substr(15).c_str());
+ // set new primary fanart, and update our database accordingly
+ m_movieItem->GetVideoInfoTag()->m_fanart.SetPrimaryFanart(iFanart);
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ db.UpdateFanart(*m_movieItem, m_movieItem->GetVideoContentType());
+ db.Close();
+ }
+ result = m_movieItem->GetVideoInfoTag()->m_fanart.GetImageURL();
+ }
+ else if (StringUtils::EqualsNoCase(result, "fanart://None") || !CFileUtils::Exists(result))
+ result.clear();
+
+ // set the fanart image
+ if (flip && !result.empty())
+ result = CTextureUtils::GetWrappedImageURL(result, "", "flipped");
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ db.SetArtForItem(m_movieItem->GetVideoInfoTag()->m_iDbId, m_movieItem->GetVideoInfoTag()->m_type, "fanart", result);
+ db.Close();
+ }
+
+ CUtil::DeleteVideoDatabaseDirectoryCache(); // to get them new thumbs to show
+ m_movieItem->SetArt("fanart", result);
+ m_hasUpdatedThumb = true;
+
+ // Update our screen
+ Update();
+}
+
+void CGUIDialogVideoInfo::OnSetUserrating() const
+{
+ CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (dialog)
+ {
+ dialog->SetHeading(CVariant{ 38023 });
+ dialog->Add(g_localizeStrings.Get(38022));
+ for (int i = 1; i <= 10; i++)
+ dialog->Add(StringUtils::Format("{}: {}", g_localizeStrings.Get(563), i));
+
+ dialog->SetSelected(m_movieItem->GetVideoInfoTag()->m_iUserRating);
+
+ dialog->Open();
+
+ int iItem = dialog->GetSelectedItem();
+ if (iItem < 0)
+ return;
+
+ SetUserrating(iItem);
+ }
+}
+
+void CGUIDialogVideoInfo::PlayTrailer()
+{
+ Close(true);
+ CGUIMessage msg(GUI_MSG_PLAY_TRAILER, 0, 0, 0, 0, m_movieItem);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
+
+void CGUIDialogVideoInfo::SetLabel(int iControl, const std::string &strLabel)
+{
+ if (strLabel.empty())
+ {
+ SET_CONTROL_LABEL(iControl, 416); // "Not available"
+ }
+ else
+ {
+ SET_CONTROL_LABEL(iControl, strLabel);
+ }
+}
+
+std::string CGUIDialogVideoInfo::GetThumbnail() const
+{
+ return m_movieItem->GetArt("thumb");
+}
+
+namespace
+{
+std::string GetItemPathForBrowserSource(const CFileItem& item)
+{
+ if (!item.HasVideoInfoTag())
+ return "";
+
+ std::string itemDir = item.GetVideoInfoTag()->m_basePath;
+ //season
+ if (itemDir.empty())
+ itemDir = item.GetVideoInfoTag()->GetPath();
+
+ CFileItem itemTmp(itemDir, false);
+ if (itemTmp.IsVideo())
+ itemDir = URIUtils::GetParentPath(itemDir);
+
+ return itemDir;
+}
+
+void AddItemPathStringToFileBrowserSources(VECSOURCES& sources,
+ const std::string& itemDir, const std::string& label)
+{
+ if (!itemDir.empty() && CDirectory::Exists(itemDir))
+ {
+ CMediaSource itemSource;
+ itemSource.strName = label;
+ itemSource.strPath = itemDir;
+ sources.push_back(itemSource);
+ }
+}
+} // namespace
+
+void CGUIDialogVideoInfo::AddItemPathToFileBrowserSources(VECSOURCES& sources,
+ const CFileItem& item)
+{
+ std::string itemDir = GetItemPathForBrowserSource(item);
+ AddItemPathStringToFileBrowserSources(sources, itemDir, g_localizeStrings.Get(36041));
+}
+
+int CGUIDialogVideoInfo::ManageVideoItem(const std::shared_ptr<CFileItem>& item)
+{
+ if (item == nullptr || !item->IsVideoDb() || !item->HasVideoInfoTag() || item->GetVideoInfoTag()->m_iDbId < 0)
+ return -1;
+
+ CVideoDatabase database;
+ if (!database.Open())
+ return -1;
+
+ const std::string &type = item->GetVideoInfoTag()->m_type;
+ int dbId = item->GetVideoInfoTag()->m_iDbId;
+
+ CContextButtons buttons;
+ if (type == MediaTypeMovie || type == MediaTypeVideoCollection ||
+ type == MediaTypeTvShow || type == MediaTypeEpisode ||
+ (type == MediaTypeSeason && item->GetVideoInfoTag()->m_iSeason > 0) || // seasons without "all seasons" and "specials"
+ type == MediaTypeMusicVideo)
+ buttons.Add(CONTEXT_BUTTON_EDIT, 16105);
+
+ if (type == MediaTypeMovie || type == MediaTypeTvShow)
+ buttons.Add(CONTEXT_BUTTON_EDIT_SORTTITLE, 16107);
+
+ if (type == MediaTypeMovie)
+ {
+ // only show link/unlink if there are tvshows available
+ if (database.HasContent(VideoDbContentType::TVSHOWS))
+ {
+ buttons.Add(CONTEXT_BUTTON_LINK_MOVIE, 20384);
+ if (database.IsLinkedToTvshow(dbId))
+ buttons.Add(CONTEXT_BUTTON_UNLINK_MOVIE, 20385);
+ }
+
+ // set or change movie set the movie belongs to
+ buttons.Add(CONTEXT_BUTTON_SET_MOVIESET, 20465);
+ }
+
+ if (type == MediaTypeEpisode &&
+ item->GetVideoInfoTag()->m_iBookmarkId > 0)
+ buttons.Add(CONTEXT_BUTTON_UNLINK_BOOKMARK, 20405);
+
+ // movie sets
+ if (item->m_bIsFolder && type == MediaTypeVideoCollection)
+ {
+ buttons.Add(CONTEXT_BUTTON_SET_MOVIESET_ART, 13511);
+ buttons.Add(CONTEXT_BUTTON_MOVIESET_ADD_REMOVE_ITEMS, 20465);
+ }
+
+ // seasons
+ if (item->m_bIsFolder && type == MediaTypeSeason)
+ buttons.Add(CONTEXT_BUTTON_SET_SEASON_ART, 13511);
+
+ // tags
+ if (item->m_bIsFolder && type == "tag")
+ {
+ CVideoDbUrl videoUrl;
+ if (videoUrl.FromString(item->GetPath()))
+ {
+ const std::string &mediaType = videoUrl.GetItemType();
+
+ buttons.Add(
+ CONTEXT_BUTTON_TAGS_ADD_ITEMS,
+ StringUtils::Format(g_localizeStrings.Get(20460), GetLocalizedVideoType(mediaType)));
+ buttons.Add(CONTEXT_BUTTON_TAGS_REMOVE_ITEMS, StringUtils::Format(g_localizeStrings.Get(20461).c_str(), GetLocalizedVideoType(mediaType).c_str()));
+ }
+ }
+
+ if (type != MediaTypeSeason)
+ buttons.Add(CONTEXT_BUTTON_DELETE, 646);
+
+ //temporary workaround until the context menu ids are removed
+ const int addonItemOffset = 10000;
+
+ auto addonItems = CServiceBroker::GetContextMenuManager().GetAddonItems(*item, CContextMenuManager::MANAGE);
+ for (size_t i = 0; i < addonItems.size(); ++i)
+ buttons.Add(addonItemOffset + i, addonItems[i]->GetLabel(*item));
+
+ bool result = false;
+ int button = CGUIDialogContextMenu::ShowAndGetChoice(buttons);
+ if (button >= 0)
+ {
+ switch (static_cast<CONTEXT_BUTTON>(button))
+ {
+ case CONTEXT_BUTTON_EDIT:
+ result = UpdateVideoItemTitle(item);
+ break;
+
+ case CONTEXT_BUTTON_EDIT_SORTTITLE:
+ result = UpdateVideoItemSortTitle(item);
+ break;
+
+ case CONTEXT_BUTTON_LINK_MOVIE:
+ result = LinkMovieToTvShow(item, false, database);
+ break;
+
+ case CONTEXT_BUTTON_UNLINK_MOVIE:
+ result = LinkMovieToTvShow(item, true, database);
+ break;
+
+ case CONTEXT_BUTTON_SET_MOVIESET:
+ {
+ CFileItemPtr selectedSet;
+ if (GetSetForMovie(item.get(), selectedSet))
+ result = SetMovieSet(item.get(), selectedSet.get());
+ break;
+ }
+
+ case CONTEXT_BUTTON_UNLINK_BOOKMARK:
+ database.DeleteBookMarkForEpisode(*item->GetVideoInfoTag());
+ result = true;
+ break;
+
+ case CONTEXT_BUTTON_DELETE:
+ result = DeleteVideoItem(item);
+ break;
+
+ case CONTEXT_BUTTON_SET_MOVIESET_ART:
+ case CONTEXT_BUTTON_SET_SEASON_ART:
+ result = ManageVideoItemArtwork(item, type);
+ break;
+
+ case CONTEXT_BUTTON_MOVIESET_ADD_REMOVE_ITEMS:
+ result = ManageMovieSets(item);
+ break;
+
+ case CONTEXT_BUTTON_TAGS_ADD_ITEMS:
+ result = AddItemsToTag(item);
+ break;
+
+ case CONTEXT_BUTTON_TAGS_REMOVE_ITEMS:
+ result = RemoveItemsFromTag(item);
+ break;
+
+ default:
+ if (button >= addonItemOffset)
+ result = CONTEXTMENU::LoopFrom(*addonItems[button - addonItemOffset], item);
+ break;
+ }
+ }
+
+ database.Close();
+
+ if (result)
+ return button;
+
+ return -1;
+}
+
+//Add change a title's name
+bool CGUIDialogVideoInfo::UpdateVideoItemTitle(const std::shared_ptr<CFileItem>& pItem)
+{
+ if (pItem == nullptr || !pItem->HasVideoInfoTag())
+ return false;
+
+ // dont allow update while scanning
+ if (CVideoLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{14057});
+ return false;
+ }
+
+ CVideoDatabase database;
+ if (!database.Open())
+ return false;
+
+ int iDbId = pItem->GetVideoInfoTag()->m_iDbId;
+ MediaType mediaType = pItem->GetVideoInfoTag()->m_type;
+
+ CVideoInfoTag detail;
+ std::string title;
+ if (mediaType == MediaTypeMovie)
+ {
+ database.GetMovieInfo("", detail, iDbId, VideoDbDetailsNone);
+ title = detail.m_strTitle;
+ }
+ else if (mediaType == MediaTypeVideoCollection)
+ {
+ database.GetSetInfo(iDbId, detail);
+ title = detail.m_strTitle;
+ }
+ else if (mediaType == MediaTypeEpisode)
+ {
+ database.GetEpisodeInfo(pItem->GetPath(), detail, iDbId, VideoDbDetailsNone);
+ title = detail.m_strTitle;
+ }
+ else if (mediaType == MediaTypeSeason)
+ {
+ database.GetSeasonInfo(iDbId, detail);
+ title = detail.m_strSortTitle.empty() ? detail.m_strTitle : detail.m_strSortTitle;
+ }
+ else if (mediaType == MediaTypeTvShow)
+ {
+ database.GetTvShowInfo(pItem->GetVideoInfoTag()->m_strFileNameAndPath, detail, iDbId, 0, VideoDbDetailsNone);
+ title = detail.m_strTitle;
+ }
+ else if (mediaType == MediaTypeMusicVideo)
+ {
+ database.GetMusicVideoInfo(pItem->GetVideoInfoTag()->m_strFileNameAndPath, detail, iDbId, VideoDbDetailsNone);
+ title = detail.m_strTitle;
+ }
+
+ // get the new title
+ if (!CGUIKeyboardFactory::ShowAndGetInput(title, CVariant{ g_localizeStrings.Get(16105) }, false))
+ return false;
+
+ if (mediaType == MediaTypeSeason)
+ {
+ detail.m_strSortTitle = title;
+ std::map<std::string, std::string> artwork;
+ database.SetDetailsForSeason(detail, artwork, detail.m_iIdShow, detail.m_iDbId);
+ }
+ else
+ {
+ detail.m_strTitle = title;
+ VideoDbContentType iType = pItem->GetVideoContentType();
+ database.UpdateMovieTitle(iDbId, detail.m_strTitle, iType);
+ }
+
+ return true;
+}
+
+bool CGUIDialogVideoInfo::CanDeleteVideoItem(const std::shared_ptr<CFileItem>& item)
+{
+ if (item == nullptr || !item->HasVideoInfoTag())
+ return false;
+
+ if (item->GetVideoInfoTag()->m_type == "tag")
+ return true;
+
+ CQueryParams params;
+ CVideoDatabaseDirectory::GetQueryParams(item->GetPath(), params);
+
+ return params.GetMovieId() != -1 ||
+ params.GetEpisodeId() != -1 ||
+ params.GetMVideoId() != -1 ||
+ params.GetSetId() != -1 ||
+ (params.GetTvShowId() != -1 && params.GetSeason() <= -1 &&
+ !CVideoDatabaseDirectory::IsAllItem(item->GetPath()));
+}
+
+bool CGUIDialogVideoInfo::DeleteVideoItemFromDatabase(const std::shared_ptr<CFileItem>& item,
+ bool unavailable /* = false */)
+{
+ if (item == nullptr || !item->HasVideoInfoTag() ||
+ !CanDeleteVideoItem(item))
+ return false;
+
+ // dont allow update while scanning
+ if (CVideoLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{14057});
+ return false;
+ }
+
+ CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+ if (pDialog == nullptr)
+ return false;
+
+ int heading = -1;
+ VideoDbContentType type = item->GetVideoContentType();
+ const std::string& subtype = item->GetVideoInfoTag()->m_type;
+ if (subtype != "tag")
+ {
+ switch (type)
+ {
+ case VideoDbContentType::MOVIES:
+ heading = 432;
+ break;
+ case VideoDbContentType::EPISODES:
+ heading = 20362;
+ break;
+ case VideoDbContentType::TVSHOWS:
+ heading = 20363;
+ break;
+ case VideoDbContentType::MUSICVIDEOS:
+ heading = 20392;
+ break;
+ case VideoDbContentType::MOVIE_SETS:
+ heading = 646;
+ break;
+ default:
+ return false;
+ }
+ }
+ else
+ {
+ heading = 10058;
+ }
+
+ pDialog->SetHeading(CVariant{heading});
+
+ if (unavailable)
+ {
+ pDialog->SetLine(0, CVariant{g_localizeStrings.Get(662)});
+ pDialog->SetLine(1, CVariant{g_localizeStrings.Get(663)});
+ }
+ else
+ {
+ pDialog->SetLine(0,
+ CVariant{StringUtils::Format(g_localizeStrings.Get(433), item->GetLabel())});
+ pDialog->SetLine(1, CVariant{""});
+ }
+ pDialog->SetLine(2, CVariant{""});
+ pDialog->Open();
+
+ if (!pDialog->IsConfirmed())
+ return false;
+
+ CVideoDatabase database;
+ database.Open();
+
+ if (item->GetVideoInfoTag()->m_iDbId < 0)
+ return false;
+
+ if (subtype == "tag")
+ {
+ database.DeleteTag(item->GetVideoInfoTag()->m_iDbId, type);
+ return true;
+ }
+
+ switch (type)
+ {
+ case VideoDbContentType::MOVIES:
+ database.DeleteMovie(item->GetVideoInfoTag()->m_iDbId);
+ break;
+ case VideoDbContentType::EPISODES:
+ database.DeleteEpisode(item->GetVideoInfoTag()->m_iDbId);
+ break;
+ case VideoDbContentType::TVSHOWS:
+ database.DeleteTvShow(item->GetVideoInfoTag()->m_iDbId);
+ break;
+ case VideoDbContentType::MUSICVIDEOS:
+ database.DeleteMusicVideo(item->GetVideoInfoTag()->m_iDbId);
+ break;
+ case VideoDbContentType::MOVIE_SETS:
+ database.DeleteSet(item->GetVideoInfoTag()->m_iDbId);
+ break;
+ default:
+ return false;
+ }
+ return true;
+}
+
+bool CGUIDialogVideoInfo::DeleteVideoItem(const std::shared_ptr<CFileItem>& item,
+ bool unavailable /* = false */)
+{
+ if (item == nullptr)
+ return false;
+
+ // delete the video item from the database
+ if (!DeleteVideoItemFromDatabase(item, unavailable))
+ return false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // check if the user is allowed to delete the actual file as well
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_ALLOWFILEDELETION) &&
+ (profileManager->GetCurrentProfile().getLockMode() == LOCK_MODE_EVERYONE ||
+ !profileManager->GetCurrentProfile().filesLocked() ||
+ g_passwordManager.IsMasterLockUnlocked(true)))
+ {
+ std::string strDeletePath = item->GetVideoInfoTag()->GetPath();
+
+ if (StringUtils::EqualsNoCase(URIUtils::GetFileName(strDeletePath), "VIDEO_TS.IFO"))
+ {
+ strDeletePath = URIUtils::GetDirectory(strDeletePath);
+ if (StringUtils::EndsWithNoCase(strDeletePath, "video_ts/"))
+ {
+ URIUtils::RemoveSlashAtEnd(strDeletePath);
+ strDeletePath = URIUtils::GetDirectory(strDeletePath);
+ }
+ }
+ if (URIUtils::HasSlashAtEnd(strDeletePath))
+ item->m_bIsFolder = true;
+
+ // check if the file/directory can be deleted
+ if (CUtil::SupportsWriteFileOperations(strDeletePath))
+ {
+ item->SetPath(strDeletePath);
+
+ // HACK: stacked files need to be treated as folders in order to be deleted
+ if (item->IsStack())
+ item->m_bIsFolder = true;
+
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui && gui->ConfirmDelete(item->GetPath()))
+ CFileUtils::DeleteItem(item);
+ }
+ }
+
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+
+ return true;
+}
+
+bool CGUIDialogVideoInfo::ManageMovieSets(const std::shared_ptr<CFileItem>& item)
+{
+ if (item == nullptr)
+ return false;
+
+ CFileItemList originalItems;
+ CFileItemList selectedItems;
+
+ if (!GetMoviesForSet(item.get(), originalItems, selectedItems) ||
+ selectedItems.Size() == 0) // need at least one item selected
+ return false;
+
+ VECFILEITEMS original = originalItems.GetList();
+ std::sort(original.begin(), original.end(), compFileItemsByDbId);
+ VECFILEITEMS selected = selectedItems.GetList();
+ std::sort(selected.begin(), selected.end(), compFileItemsByDbId);
+
+ bool refreshNeeded = false;
+ // update the "added" items
+ VECFILEITEMS addedItems;
+ set_difference(selected.begin(),selected.end(), original.begin(),original.end(), std::back_inserter(addedItems), compFileItemsByDbId);
+ for (VECFILEITEMS::const_iterator it = addedItems.begin(); it != addedItems.end(); ++it)
+ {
+ if (SetMovieSet(it->get(), item.get()))
+ refreshNeeded = true;
+ }
+
+ // update the "deleted" items
+ CFileItemPtr clearItem(new CFileItem());
+ clearItem->GetVideoInfoTag()->m_iDbId = -1; // -1 will be used to clear set
+ VECFILEITEMS deletedItems;
+ set_difference(original.begin(),original.end(), selected.begin(),selected.end(), std::back_inserter(deletedItems), compFileItemsByDbId);
+ for (VECFILEITEMS::iterator it = deletedItems.begin(); it != deletedItems.end(); ++it)
+ {
+ if (SetMovieSet(it->get(), clearItem.get()))
+ refreshNeeded = true;
+ }
+
+ return refreshNeeded;
+}
+
+bool CGUIDialogVideoInfo::GetMoviesForSet(const CFileItem *setItem, CFileItemList &originalMovies, CFileItemList &selectedMovies)
+{
+ if (setItem == nullptr || !setItem->HasVideoInfoTag())
+ return false;
+
+ CVideoDatabase videodb;
+ if (!videodb.Open())
+ return false;
+
+ std::string baseDir =
+ StringUtils::Format("videodb://movies/sets/{}", setItem->GetVideoInfoTag()->m_iDbId);
+
+ if (!CDirectory::GetDirectory(baseDir, originalMovies, "", DIR_FLAG_DEFAULTS) ||
+ originalMovies.Size() <= 0) // keep a copy of the original members of the set
+ return false;
+
+ CFileItemList listItems;
+ if (!videodb.GetSortedVideos(MediaTypeMovie, "videodb://movies", SortDescription(), listItems) || listItems.Size() <= 0)
+ return false;
+
+ CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (dialog == nullptr)
+ return false;
+
+ listItems.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+
+ dialog->Reset();
+ dialog->SetMultiSelection(true);
+ dialog->SetHeading(CVariant{g_localizeStrings.Get(20457)});
+ dialog->SetItems(listItems);
+ std::vector<int> selectedIndices;
+ for (int i = 0; i < originalMovies.Size(); i++)
+ {
+ for (int listIndex = 0; listIndex < listItems.Size(); listIndex++)
+ {
+ if (listItems.Get(listIndex)->GetVideoInfoTag()->m_iDbId == originalMovies[i]->GetVideoInfoTag()->m_iDbId)
+ {
+ selectedIndices.push_back(listIndex);
+ break;
+ }
+ }
+ }
+ dialog->SetSelected(selectedIndices);
+ dialog->EnableButton(true, 186);
+ dialog->Open();
+
+ if (dialog->IsConfirmed())
+ {
+ for (int i : dialog->GetSelectedItems())
+ selectedMovies.Add(listItems.Get(i));
+ return (selectedMovies.Size() > 0);
+ }
+ else
+ return false;
+}
+
+bool CGUIDialogVideoInfo::GetSetForMovie(const CFileItem* movieItem,
+ std::shared_ptr<CFileItem>& selectedSet)
+{
+ if (movieItem == nullptr || !movieItem->HasVideoInfoTag())
+ return false;
+
+ CVideoDatabase videodb;
+ if (!videodb.Open())
+ return false;
+
+ CFileItemList listItems;
+
+ // " ignoreSingleMovieSets=false " as an option in the url is needed here
+ // to override the gui-setting "Include sets containing a single movie"
+ // and retrieve all moviesets
+
+ std::string baseDir = "videodb://movies/sets/?ignoreSingleMovieSets=false";
+
+ if (!CDirectory::GetDirectory(baseDir, listItems, "", DIR_FLAG_DEFAULTS))
+ return false;
+ listItems.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+
+ int currentSetId = 0;
+ std::string currentSetLabel;
+
+ if (movieItem->GetVideoInfoTag()->m_set.id > currentSetId)
+ {
+ currentSetId = movieItem->GetVideoInfoTag()->m_set.id;
+ currentSetLabel = videodb.GetSetById(currentSetId);
+ }
+
+ if (currentSetId > 0)
+ {
+ // remove duplicate entry
+ for (int listIndex = 0; listIndex < listItems.Size(); listIndex++)
+ {
+ if (listItems.Get(listIndex)->GetVideoInfoTag()->m_iDbId == currentSetId)
+ {
+ listItems.Remove(listIndex);
+ break;
+ }
+ }
+ // add clear item
+ std::string strClear = StringUtils::Format(g_localizeStrings.Get(20467), currentSetLabel);
+ CFileItemPtr clearItem(new CFileItem(strClear));
+ clearItem->GetVideoInfoTag()->m_iDbId = -1; // -1 will be used to clear set
+ listItems.AddFront(clearItem, 0);
+ // add keep current set item
+ std::string strKeep = StringUtils::Format(g_localizeStrings.Get(20469), currentSetLabel);
+ CFileItemPtr keepItem(new CFileItem(strKeep));
+ keepItem->GetVideoInfoTag()->m_iDbId = currentSetId;
+ listItems.AddFront(keepItem, 1);
+ }
+
+ CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (dialog == nullptr)
+ return false;
+
+ dialog->Reset();
+ dialog->SetHeading(CVariant{g_localizeStrings.Get(20466)});
+ dialog->SetItems(listItems);
+ if (currentSetId >= 0)
+ {
+ for (int listIndex = 0; listIndex < listItems.Size(); listIndex++)
+ {
+ if (listItems.Get(listIndex)->GetVideoInfoTag()->m_iDbId == currentSetId)
+ {
+ dialog->SetSelected(listIndex);
+ break;
+ }
+ }
+ }
+ dialog->EnableButton(true, 20468); // new set via button
+ dialog->Open();
+
+ if (dialog->IsButtonPressed())
+ { // creating new set
+ std::string newSetTitle;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(newSetTitle, CVariant{g_localizeStrings.Get(20468)}, false))
+ return false;
+ int idSet = videodb.AddSet(newSetTitle);
+ std::map<std::string, std::string> movieArt, setArt;
+ if (!videodb.GetArtForItem(idSet, MediaTypeVideoCollection, setArt))
+ {
+ videodb.GetArtForItem(movieItem->GetVideoInfoTag()->m_iDbId, MediaTypeMovie, movieArt);
+ videodb.SetArtForItem(idSet, MediaTypeVideoCollection, movieArt);
+ }
+ CFileItemPtr newSet(new CFileItem(newSetTitle));
+ newSet->GetVideoInfoTag()->m_iDbId = idSet;
+ selectedSet = newSet;
+ return true;
+ }
+ else if (dialog->IsConfirmed())
+ {
+ selectedSet = dialog->GetSelectedFileItem();
+ return (selectedSet != nullptr);
+ }
+ else
+ return false;
+}
+
+bool CGUIDialogVideoInfo::SetMovieSet(const CFileItem *movieItem, const CFileItem *selectedSet)
+{
+ if (movieItem == nullptr || !movieItem->HasVideoInfoTag() ||
+ selectedSet == nullptr || !selectedSet->HasVideoInfoTag())
+ return false;
+
+ CVideoDatabase videodb;
+ if (!videodb.Open())
+ return false;
+
+ videodb.SetMovieSet(movieItem->GetVideoInfoTag()->m_iDbId, selectedSet->GetVideoInfoTag()->m_iDbId);
+ return true;
+}
+
+bool CGUIDialogVideoInfo::GetItemsForTag(const std::string &strHeading, const std::string &type, CFileItemList &items, int idTag /* = -1 */, bool showAll /* = true */)
+{
+ CVideoDatabase videodb;
+ if (!videodb.Open())
+ return false;
+
+ MediaType mediaType = MediaTypeNone;
+ std::string baseDir = "videodb://";
+ std::string idColumn;
+ if (type.compare(MediaTypeMovie) == 0)
+ {
+ mediaType = MediaTypeMovie;
+ baseDir += "movies";
+ idColumn = "idMovie";
+ }
+ else if (type.compare(MediaTypeTvShow) == 0)
+ {
+ mediaType = MediaTypeTvShow;
+ baseDir += "tvshows";
+ idColumn = "idShow";
+ }
+ else if (type.compare(MediaTypeMusicVideo) == 0)
+ {
+ mediaType = MediaTypeMusicVideo;
+ baseDir += "musicvideos";
+ idColumn = "idMVideo";
+ }
+
+ baseDir += "/titles/";
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(baseDir))
+ return false;
+
+ CVideoDatabase::Filter filter;
+ if (idTag > 0)
+ {
+ if (!showAll)
+ videoUrl.AddOption("tagid", idTag);
+ else
+ filter.where = videodb.PrepareSQL("%s_view.%s NOT IN (SELECT tag_link.media_id FROM tag_link WHERE tag_link.tag_id = %d AND tag_link.media_type = '%s')", type.c_str(), idColumn.c_str(), idTag, type.c_str());
+ }
+
+ CFileItemList listItems;
+ if (!videodb.GetSortedVideos(mediaType, videoUrl.ToString(), SortDescription(), listItems, filter) || listItems.Size() <= 0)
+ return false;
+
+ CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (dialog == nullptr)
+ return false;
+
+ listItems.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+
+ dialog->Reset();
+ dialog->SetMultiSelection(true);
+ dialog->SetHeading(CVariant{strHeading});
+ dialog->SetItems(listItems);
+ dialog->EnableButton(true, 186);
+ dialog->Open();
+
+ for (int i : dialog->GetSelectedItems())
+ items.Add(listItems.Get(i));
+ return items.Size() > 0;
+}
+
+bool CGUIDialogVideoInfo::AddItemsToTag(const std::shared_ptr<CFileItem>& tagItem)
+{
+ if (tagItem == nullptr || !tagItem->HasVideoInfoTag())
+ return false;
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(tagItem->GetPath()))
+ return false;
+
+ CVideoDatabase videodb;
+ if (!videodb.Open())
+ return true;
+
+ std::string mediaType = videoUrl.GetItemType();
+ mediaType = mediaType.substr(0, mediaType.length() - 1);
+
+ CFileItemList items;
+ std::string localizedType = GetLocalizedVideoType(mediaType);
+ std::string strLabel = StringUtils::Format(g_localizeStrings.Get(20464), localizedType);
+ if (!GetItemsForTag(strLabel, mediaType, items, tagItem->GetVideoInfoTag()->m_iDbId))
+ return true;
+
+ for (int index = 0; index < items.Size(); index++)
+ {
+ if (!items[index]->HasVideoInfoTag() || items[index]->GetVideoInfoTag()->m_iDbId <= 0)
+ continue;
+
+ videodb.AddTagToItem(items[index]->GetVideoInfoTag()->m_iDbId, tagItem->GetVideoInfoTag()->m_iDbId, mediaType);
+ }
+
+ return true;
+}
+
+bool CGUIDialogVideoInfo::RemoveItemsFromTag(const std::shared_ptr<CFileItem>& tagItem)
+{
+ if (tagItem == nullptr || !tagItem->HasVideoInfoTag())
+ return false;
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(tagItem->GetPath()))
+ return false;
+
+ CVideoDatabase videodb;
+ if (!videodb.Open())
+ return true;
+
+ std::string mediaType = videoUrl.GetItemType();
+ mediaType = mediaType.substr(0, mediaType.length() - 1);
+
+ CFileItemList items;
+ std::string localizedType = GetLocalizedVideoType(mediaType);
+ std::string strLabel = StringUtils::Format(g_localizeStrings.Get(20464), localizedType);
+ if (!GetItemsForTag(strLabel, mediaType, items, tagItem->GetVideoInfoTag()->m_iDbId, false))
+ return true;
+
+ for (int index = 0; index < items.Size(); index++)
+ {
+ if (!items[index]->HasVideoInfoTag() || items[index]->GetVideoInfoTag()->m_iDbId <= 0)
+ continue;
+
+ videodb.RemoveTagFromItem(items[index]->GetVideoInfoTag()->m_iDbId, tagItem->GetVideoInfoTag()->m_iDbId, mediaType);
+ }
+
+ return true;
+}
+
+namespace
+{
+std::string FindLocalMovieSetArtworkFile(const CFileItemPtr& item, const std::string& artType)
+{
+ std::string infoFolder = VIDEO::CVideoInfoScanner::GetMovieSetInfoFolder(item->GetLabel());
+ if (infoFolder.empty())
+ return "";
+
+ CFileItemList availableArtFiles;
+ CDirectory::GetDirectory(infoFolder, availableArtFiles,
+ CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(),
+ DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_READ_CACHE | DIR_FLAG_NO_FILE_INFO);
+ for (const auto& artFile : availableArtFiles)
+ {
+ std::string candidate = URIUtils::GetFileName(artFile->GetPath());
+ URIUtils::RemoveExtension(candidate);
+ if (StringUtils::EqualsNoCase(candidate, artType))
+ return artFile->GetPath();
+ }
+ return "";
+}
+} // namespace
+
+bool CGUIDialogVideoInfo::ManageVideoItemArtwork(const std::shared_ptr<CFileItem>& item,
+ const MediaType& type)
+{
+ if (item == nullptr || !item->HasVideoInfoTag() || type.empty())
+ return false;
+
+ CVideoDatabase videodb;
+ if (!videodb.Open())
+ return true;
+
+ // Grab the thumbnails from the web
+ CFileItemList items;
+ CFileItemPtr noneitem(new CFileItem("thumb://None", false));
+ std::string currentThumb;
+ int idArtist = -1;
+ std::string artistPath;
+ std::string artistOldPath;
+ std::string artType = "thumb";
+ if (type == MediaTypeArtist)
+ {
+ CMusicDatabase musicdb;
+ if (musicdb.Open())
+ {
+ idArtist = musicdb.GetArtistByName(item->GetLabel()); // Fails when name not unique
+ if (idArtist >= 0 )
+ {
+ // Get artist paths - possible locations for thumb - while music db open
+ musicdb.GetOldArtistPath(idArtist, artistOldPath); // Old artist path, local to music files
+ CArtist artist;
+ musicdb.GetArtist(idArtist, artist); // Need name and mbid for artist folder name
+ musicdb.GetArtistPath(artist, artistPath); // Artist path in artist info folder
+
+ currentThumb = musicdb.GetArtForItem(idArtist, MediaTypeArtist, "thumb");
+ if (currentThumb.empty())
+ currentThumb = videodb.GetArtForItem(item->GetVideoInfoTag()->m_iDbId, item->GetVideoInfoTag()->m_type, artType);
+ }
+ }
+ }
+ else if (type == "actor")
+ currentThumb = videodb.GetArtForItem(item->GetVideoInfoTag()->m_iDbId, item->GetVideoInfoTag()->m_type, artType);
+ else
+ { // SEASON, SET
+ artType = ChooseArtType(*item);
+ if (artType.empty())
+ return false;
+
+ if (artType == "fanart" && type != MediaTypeVideoCollection)
+ return OnGetFanart(item);
+
+ if (item->HasArt(artType))
+ currentThumb = item->GetArt(artType);
+ else if ((artType == "poster" || artType == "banner") && item->HasArt("thumb"))
+ currentThumb = item->GetArt("thumb");
+ }
+
+ if (!currentThumb.empty())
+ {
+ CFileItemPtr item(new CFileItem("thumb://Current", false));
+ item->SetArt("thumb", currentThumb);
+ item->SetLabel(g_localizeStrings.Get(13512));
+ items.Add(item);
+ }
+ noneitem->SetArt("icon", "DefaultFolder.png");
+ noneitem->SetLabel(g_localizeStrings.Get(13515));
+
+ bool local = false;
+ std::vector<std::string> thumbs;
+ if (type != MediaTypeArtist)
+ {
+ CVideoInfoTag tag;
+ if (type == MediaTypeSeason)
+ {
+ videodb.GetTvShowInfo("", tag, item->GetVideoInfoTag()->m_iIdShow);
+ tag.m_strPictureURL.Parse();
+ tag.m_strPictureURL.GetThumbUrls(thumbs, artType, item->GetVideoInfoTag()->m_iSeason);
+ }
+ else if (type == MediaTypeVideoCollection)
+ {
+ CFileItemList items;
+ std::string baseDir =
+ StringUtils::Format("videodb://movies/sets/{}", item->GetVideoInfoTag()->m_iDbId);
+ if (videodb.GetMoviesNav(baseDir, items))
+ {
+ for (int i=0; i < items.Size(); i++)
+ {
+ CVideoInfoTag* pTag = items[i]->GetVideoInfoTag();
+ pTag->m_strPictureURL.Parse();
+ pTag->m_strPictureURL.GetThumbUrls(thumbs, "set." + artType, -1, true);
+ }
+ }
+ }
+ else
+ {
+ tag = *item->GetVideoInfoTag();
+ tag.m_strPictureURL.Parse();
+ tag.m_strPictureURL.GetThumbUrls(thumbs, artType);
+ }
+
+ for (size_t i = 0; i < thumbs.size(); i++)
+ {
+ CFileItemPtr item(new CFileItem(StringUtils::Format("thumb://Remote{0}", i), false));
+ item->SetArt("thumb", thumbs[i]);
+ item->SetArt("icon", "DefaultPicture.png");
+ item->SetLabel(g_localizeStrings.Get(13513));
+ items.Add(item);
+
+ //! @todo Do we need to clear the cached image?
+ // CServiceBroker::GetTextureCache()->ClearCachedImage(thumbs[i]);
+ }
+
+ if (type == "actor")
+ {
+ std::string picturePath;
+ std::string strThumb = URIUtils::AddFileToFolder(picturePath, "folder.jpg");
+ if (CFileUtils::Exists(strThumb))
+ {
+ CFileItemPtr pItem(new CFileItem(strThumb,false));
+ pItem->SetLabel(g_localizeStrings.Get(13514));
+ pItem->SetArt("thumb", strThumb);
+ items.Add(pItem);
+ local = true;
+ }
+ else
+ noneitem->SetArt("icon", "DefaultActor.png");
+ }
+
+ if (type == MediaTypeVideoCollection)
+ {
+ std::string localFile = FindLocalMovieSetArtworkFile(item, artType);
+ if (!localFile.empty())
+ {
+ CFileItemPtr pItem(new CFileItem(localFile, false));
+ pItem->SetLabel(g_localizeStrings.Get(13514));
+ pItem->SetArt("thumb", localFile);
+ items.Add(pItem);
+ local = true;
+ }
+ else
+ noneitem->SetArt("icon", "DefaultVideo.png");
+ }
+ }
+ else
+ {
+ std::string strThumb;
+ bool existsThumb = false;
+ // First look for artist thumb in the primary location
+ if (!artistPath.empty())
+ {
+ strThumb = URIUtils::AddFileToFolder(artistPath, "folder.jpg");
+ existsThumb = CFileUtils::Exists(strThumb);
+ }
+ // If not there fall back local to music files (historic location for those album artists with a unique folder)
+ if (!existsThumb && !artistOldPath.empty())
+ {
+ strThumb = URIUtils::AddFileToFolder(artistOldPath, "folder.jpg");
+ existsThumb = CFileUtils::Exists(strThumb);
+ }
+
+ if (existsThumb)
+ {
+ CFileItemPtr pItem(new CFileItem(strThumb, false));
+ pItem->SetLabel(g_localizeStrings.Get(13514));
+ pItem->SetArt("thumb", strThumb);
+ items.Add(pItem);
+ local = true;
+ }
+ else
+ noneitem->SetArt("icon", "DefaultArtist.png");
+ }
+
+ if (!local)
+ items.Add(noneitem);
+
+ std::string result;
+ VECSOURCES sources=*CMediaSourceSettings::GetInstance().GetSources("video");
+ CServiceBroker::GetMediaManager().GetLocalDrives(sources);
+ if (type == MediaTypeVideoCollection)
+ {
+ AddItemPathStringToFileBrowserSources(sources,
+ VIDEO::CVideoInfoScanner::GetMovieSetInfoFolder(item->GetLabel()),
+ g_localizeStrings.Get(36041));
+ AddItemPathStringToFileBrowserSources(sources,
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_VIDEOLIBRARY_MOVIESETSFOLDER),
+ "* " + g_localizeStrings.Get(20226));
+ }
+ else
+ AddItemPathToFileBrowserSources(sources, *item);
+ if (!CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(13511), result))
+ return false; // user cancelled
+
+ if (result == "thumb://Current")
+ result = currentThumb; // user chose the one they have
+
+ // delete the thumbnail if that's what the user wants, else overwrite with the
+ // new thumbnail
+ if (result == "thumb://None")
+ result.clear();
+ else if (StringUtils::StartsWith(result, "thumb://Remote"))
+ {
+ int number = atoi(StringUtils::Mid(result, 14).c_str());
+ result = thumbs[number];
+ }
+
+ // write the selected artwork to the database
+ if (type == MediaTypeVideoCollection ||
+ type == "actor" ||
+ type == MediaTypeSeason ||
+ (type == MediaTypeArtist && idArtist < 0))
+ videodb.SetArtForItem(item->GetVideoInfoTag()->m_iDbId, item->GetVideoInfoTag()->m_type, artType, result);
+ else
+ {
+ CMusicDatabase musicdb;
+ if (musicdb.Open())
+ musicdb.SetArtForItem(idArtist, MediaTypeArtist, artType, result);
+ }
+
+ item->SetArt(artType, result);
+
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_REFRESH_THUMBS);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ return true;
+}
+
+std::string CGUIDialogVideoInfo::GetLocalizedVideoType(const std::string &strType)
+{
+ if (CMediaTypes::IsMediaType(strType, MediaTypeMovie))
+ return g_localizeStrings.Get(20342);
+ else if (CMediaTypes::IsMediaType(strType, MediaTypeTvShow))
+ return g_localizeStrings.Get(20343);
+ else if (CMediaTypes::IsMediaType(strType, MediaTypeEpisode))
+ return g_localizeStrings.Get(20359);
+ else if (CMediaTypes::IsMediaType(strType, MediaTypeMusicVideo))
+ return g_localizeStrings.Get(20391);
+
+ return "";
+}
+
+bool CGUIDialogVideoInfo::UpdateVideoItemSortTitle(const std::shared_ptr<CFileItem>& pItem)
+{
+ // dont allow update while scanning
+ if (CVideoLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{14057});
+ return false;
+ }
+
+ CVideoDatabase database;
+ if (!database.Open())
+ return false;
+
+ int iDbId = pItem->GetVideoInfoTag()->m_iDbId;
+ CVideoInfoTag detail;
+ VideoDbContentType iType = pItem->GetVideoContentType();
+ if (iType == VideoDbContentType::MOVIES)
+ database.GetMovieInfo("", detail, iDbId, VideoDbDetailsNone);
+ else if (iType == VideoDbContentType::TVSHOWS)
+ database.GetTvShowInfo(pItem->GetVideoInfoTag()->m_strFileNameAndPath, detail, iDbId, 0, VideoDbDetailsNone);
+
+ std::string currentTitle;
+ if (detail.m_strSortTitle.empty())
+ currentTitle = detail.m_strTitle;
+ else
+ currentTitle = detail.m_strSortTitle;
+
+ // get the new sort title
+ if (!CGUIKeyboardFactory::ShowAndGetInput(currentTitle, CVariant{g_localizeStrings.Get(16107)}, false))
+ return false;
+
+ return database.UpdateVideoSortTitle(iDbId, currentTitle, iType);
+}
+
+bool CGUIDialogVideoInfo::LinkMovieToTvShow(const std::shared_ptr<CFileItem>& item,
+ bool bRemove,
+ CVideoDatabase& database)
+{
+ int dbId = item->GetVideoInfoTag()->m_iDbId;
+
+ CFileItemList list;
+ if (bRemove)
+ {
+ std::vector<int> ids;
+ if (!database.GetLinksToTvShow(dbId, ids))
+ return false;
+
+ for (unsigned int i = 0; i < ids.size(); ++i)
+ {
+ CVideoInfoTag tag;
+ database.GetTvShowInfo("", tag, ids[i], 0 , VideoDbDetailsNone);
+ CFileItemPtr show(new CFileItem(tag));
+ list.Add(show);
+ }
+ }
+ else
+ {
+ database.GetTvShowsNav("videodb://tvshows/titles", list);
+
+ // remove already linked shows
+ std::vector<int> ids;
+ if (!database.GetLinksToTvShow(dbId, ids))
+ return false;
+
+ for (int i = 0; i < list.Size(); )
+ {
+ size_t j;
+ for (j = 0; j < ids.size(); ++j)
+ {
+ if (list[i]->GetVideoInfoTag()->m_iDbId == ids[j])
+ break;
+ }
+ if (j == ids.size())
+ i++;
+ else
+ list.Remove(i);
+ }
+ }
+
+ int iSelectedLabel = 0;
+ if (list.Size() > 1 || (!bRemove && !list.IsEmpty()))
+ {
+ list.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ CGUIDialogSelect* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (pDialog)
+ {
+ pDialog->Reset();
+ pDialog->SetItems(list);
+ pDialog->SetHeading(CVariant{20356});
+ pDialog->Open();
+ iSelectedLabel = pDialog->GetSelectedItem();
+ }
+ }
+
+ if (iSelectedLabel > -1 && iSelectedLabel < list.Size())
+ return database.LinkMovieToTvshow(dbId, list[iSelectedLabel]->GetVideoInfoTag()->m_iDbId, bRemove);
+
+ return false;
+}
+
+bool CGUIDialogVideoInfo::OnGetFanart(const std::shared_ptr<CFileItem>& videoItem)
+{
+ if (videoItem == nullptr || !videoItem->HasVideoInfoTag())
+ return false;
+
+ // update the db
+ CVideoDatabase videodb;
+ if (!videodb.Open())
+ return false;
+
+ CVideoThumbLoader loader;
+ CFileItem item(*videoItem);
+ loader.LoadItem(&item);
+
+ CFileItemList items;
+ if (item.HasArt("fanart"))
+ {
+ CFileItemPtr itemCurrent(new CFileItem("fanart://Current", false));
+ itemCurrent->SetArt("thumb", item.GetArt("fanart"));
+ itemCurrent->SetLabel(g_localizeStrings.Get(20440));
+ items.Add(itemCurrent);
+ }
+
+ // add the none option
+ {
+ CFileItemPtr itemNone(new CFileItem("fanart://None", false));
+ itemNone->SetArt("icon", "DefaultVideo.png");
+ itemNone->SetLabel(g_localizeStrings.Get(20439));
+ items.Add(itemNone);
+ }
+
+ std::string result;
+ VECSOURCES sources(*CMediaSourceSettings::GetInstance().GetSources("video"));
+ CServiceBroker::GetMediaManager().GetLocalDrives(sources);
+ AddItemPathToFileBrowserSources(sources, item);
+ bool flip = false;
+ if (!CGUIDialogFileBrowser::ShowAndGetImage(items, sources, g_localizeStrings.Get(20437), result, &flip, 20445) ||
+ StringUtils::EqualsNoCase(result, "fanart://Current"))
+ return false;
+
+ else if (StringUtils::EqualsNoCase(result, "fanart://None") || !CFileUtils::Exists(result))
+ result.clear();
+ if (!result.empty() && flip)
+ result = CTextureUtils::GetWrappedImageURL(result, "", "flipped");
+
+ videodb.SetArtForItem(item.GetVideoInfoTag()->m_iDbId, item.GetVideoInfoTag()->m_type, "fanart", result);
+
+ // clear view cache and reload images
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+
+ return true;
+}
+
+void CGUIDialogVideoInfo::ShowFor(const CFileItem& item)
+{
+ auto window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowVideoNav>(WINDOW_VIDEO_NAV);
+ if (window)
+ {
+ ADDON::ScraperPtr info;
+ window->OnItemInfo(item, info);
+ }
+}
diff --git a/xbmc/video/dialogs/GUIDialogVideoInfo.h b/xbmc/video/dialogs/GUIDialogVideoInfo.h
new file mode 100644
index 0000000..ac19e98
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogVideoInfo.h
@@ -0,0 +1,115 @@
+/*
+ * 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 "MediaSource.h"
+#include "guilib/GUIDialog.h"
+#include "media/MediaType.h"
+
+#include <memory>
+
+class CFileItem;
+class CFileItemList;
+class CVideoDatabase;
+
+class CGUIDialogVideoInfo :
+ public CGUIDialog
+{
+public:
+ CGUIDialogVideoInfo(void);
+ ~CGUIDialogVideoInfo(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ void SetMovie(const CFileItem *item);
+ bool NeedRefresh() const;
+ bool RefreshAll() const;
+ bool HasUpdatedThumb() const { return m_hasUpdatedThumb; }
+ bool HasUpdatedUserrating() const { return m_hasUpdatedUserrating; }
+
+ std::string GetThumbnail() const;
+ std::shared_ptr<CFileItem> GetCurrentListItem(int offset = 0) override { return m_movieItem; }
+ const CFileItemList& CurrentDirectory() const { return *m_castList; }
+ bool HasListItems() const override { return true; }
+
+ static void AddItemPathToFileBrowserSources(VECSOURCES &sources, const CFileItem &item);
+
+ static int ManageVideoItem(const std::shared_ptr<CFileItem>& item);
+ static bool UpdateVideoItemTitle(const std::shared_ptr<CFileItem>& pItem);
+ static bool CanDeleteVideoItem(const std::shared_ptr<CFileItem>& item);
+ static bool DeleteVideoItemFromDatabase(const std::shared_ptr<CFileItem>& item,
+ bool unavailable = false);
+ static bool DeleteVideoItem(const std::shared_ptr<CFileItem>& item, bool unavailable = false);
+
+ static bool ManageMovieSets(const std::shared_ptr<CFileItem>& item);
+ static bool GetMoviesForSet(const CFileItem *setItem, CFileItemList &originalMovies, CFileItemList &selectedMovies);
+ static bool GetSetForMovie(const CFileItem* movieItem, std::shared_ptr<CFileItem>& selectedSet);
+ static bool SetMovieSet(const CFileItem *movieItem, const CFileItem *selectedSet);
+
+ static bool GetItemsForTag(const std::string &strHeading, const std::string &type, CFileItemList &items, int idTag = -1, bool showAll = true);
+ static bool AddItemsToTag(const std::shared_ptr<CFileItem>& tagItem);
+ static bool RemoveItemsFromTag(const std::shared_ptr<CFileItem>& tagItem);
+
+ static bool ManageVideoItemArtwork(const std::shared_ptr<CFileItem>& item, const MediaType& type);
+
+ static std::string GetLocalizedVideoType(const std::string &strType);
+
+ static void ShowFor(const CFileItem& item);
+
+protected:
+ void OnInitWindow() override;
+ void Update();
+ void SetLabel(int iControl, const std::string& strLabel);
+ void SetUserrating(int userrating) const;
+
+ // link cast to movies
+ void ClearCastList();
+ /**
+ * \brief Search the current directory for a string got from the virtual keyboard
+ * \param strSearch The search string
+ */
+ void OnSearch(std::string& strSearch);
+ /**
+ * \brief Make the actual search for the OnSearch function.
+ * \param strSearch The search string
+ * \param items Items Found
+ */
+ void DoSearch(std::string& strSearch, CFileItemList& items) const;
+ /**
+ * \brief React on the selected search item
+ * \param pItem Search result item
+ */
+ void OnSearchItemFound(const CFileItem* pItem);
+ void Play(bool resume = false);
+ void OnGetArt();
+ void OnGetFanart();
+ void OnSetUserrating() const;
+ void PlayTrailer();
+
+ static bool UpdateVideoItemSortTitle(const std::shared_ptr<CFileItem>& pItem);
+ static bool LinkMovieToTvShow(const std::shared_ptr<CFileItem>& item,
+ bool bRemove,
+ CVideoDatabase& database);
+
+ /*! \brief Pop up a fanart chooser. Does not utilise remote URLs.
+ \param videoItem the item to choose fanart for.
+ */
+ static bool OnGetFanart(const std::shared_ptr<CFileItem>& videoItem);
+
+ std::shared_ptr<CFileItem> m_movieItem;
+ CFileItemList *m_castList;
+ bool m_bViewReview = false;
+ bool m_bRefresh = false;
+ bool m_bRefreshAll = true;
+ bool m_hasUpdatedThumb = false;
+ bool m_hasUpdatedUserrating = false;
+ int m_startUserrating = -1;
+
+private:
+ static std::string ChooseArtType(const CFileItem& item);
+};
diff --git a/xbmc/video/dialogs/GUIDialogVideoOSD.cpp b/xbmc/video/dialogs/GUIDialogVideoOSD.cpp
new file mode 100644
index 0000000..3b8009d
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogVideoOSD.cpp
@@ -0,0 +1,101 @@
+/*
+ * 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 "GUIDialogVideoOSD.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "input/InputManager.h"
+#include "input/actions/ActionIDs.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+using namespace PVR;
+
+CGUIDialogVideoOSD::CGUIDialogVideoOSD(void)
+ : CGUIDialog(WINDOW_DIALOG_VIDEO_OSD, "VideoOSD.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIDialogVideoOSD::~CGUIDialogVideoOSD(void) = default;
+
+void CGUIDialogVideoOSD::FrameMove()
+{
+ if (m_autoClosing)
+ {
+ // check for movement of mouse or a submenu open
+ if (CServiceBroker::GetInputManager().IsMouseActive()
+ || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_AUDIO_OSD_SETTINGS)
+ || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_SUBTITLE_OSD_SETTINGS)
+ || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_VIDEO_OSD_SETTINGS)
+ || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_CMS_OSD_SETTINGS)
+ || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_VIDEO_BOOKMARKS)
+ || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_PVR_OSD_CHANNELS)
+ || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_PVR_CHANNEL_GUIDE)
+ || CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_DIALOG_OSD_TELETEXT))
+ // extend show time by original value
+ SetAutoClose(m_showDuration);
+ }
+ CGUIDialog::FrameMove();
+}
+
+bool CGUIDialogVideoOSD::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_SHOW_OSD)
+ {
+ Close();
+ return true;
+ }
+
+ return CGUIDialog::OnAction(action);
+}
+
+EVENT_RESULT CGUIDialogVideoOSD::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_WHEEL_UP)
+ {
+ return g_application.OnAction(CAction(ACTION_ANALOG_SEEK_FORWARD, 0.5f)) ? EVENT_RESULT_HANDLED : EVENT_RESULT_UNHANDLED;
+ }
+ if (event.m_id == ACTION_MOUSE_WHEEL_DOWN)
+ {
+ return g_application.OnAction(CAction(ACTION_ANALOG_SEEK_BACK, 0.5f)) ? EVENT_RESULT_HANDLED : EVENT_RESULT_UNHANDLED;
+ }
+
+ return CGUIDialog::OnMouseEvent(point, event);
+}
+
+bool CGUIDialogVideoOSD::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_VIDEO_MENU_STARTED:
+ {
+ // We have gone to the DVD menu, so close the OSD.
+ Close();
+ }
+ break;
+ case GUI_MSG_WINDOW_DEINIT: // fired when OSD is hidden
+ {
+ // Remove our subdialogs if visible
+ CGUIDialog *pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_AUDIO_OSD_SETTINGS);
+ if (pDialog && pDialog->IsDialogRunning())
+ pDialog->Close(true);
+ pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_SUBTITLE_OSD_SETTINGS);
+ if (pDialog && pDialog->IsDialogRunning())
+ pDialog->Close(true);
+ }
+ break;
+ }
+ return CGUIDialog::OnMessage(message);
+}
+
diff --git a/xbmc/video/dialogs/GUIDialogVideoOSD.h b/xbmc/video/dialogs/GUIDialogVideoOSD.h
new file mode 100644
index 0000000..95cfc9f
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogVideoOSD.h
@@ -0,0 +1,25 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIDialogVideoOSD : public CGUIDialog
+{
+public:
+
+ CGUIDialogVideoOSD(void);
+ ~CGUIDialogVideoOSD(void) override;
+
+ void FrameMove() override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+protected:
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+};
diff --git a/xbmc/video/dialogs/GUIDialogVideoSettings.cpp b/xbmc/video/dialogs/GUIDialogVideoSettings.cpp
new file mode 100644
index 0000000..24d9d22
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogVideoSettings.cpp
@@ -0,0 +1,573 @@
+/*
+ * 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 "GUIDialogVideoSettings.h"
+
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "profiles/ProfileManager.h"
+#include "settings/MediaSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/LangCodeExpander.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/ViewModeSettings.h"
+
+#include <utility>
+
+#define SETTING_VIDEO_VIEW_MODE "video.viewmode"
+#define SETTING_VIDEO_ZOOM "video.zoom"
+#define SETTING_VIDEO_PIXEL_RATIO "video.pixelratio"
+#define SETTING_VIDEO_BRIGHTNESS "video.brightness"
+#define SETTING_VIDEO_CONTRAST "video.contrast"
+#define SETTING_VIDEO_GAMMA "video.gamma"
+#define SETTING_VIDEO_NONLIN_STRETCH "video.nonlinearstretch"
+#define SETTING_VIDEO_POSTPROCESS "video.postprocess"
+#define SETTING_VIDEO_VERTICAL_SHIFT "video.verticalshift"
+#define SETTING_VIDEO_TONEMAP_METHOD "video.tonemapmethod"
+#define SETTING_VIDEO_TONEMAP_PARAM "video.tonemapparam"
+#define SETTING_VIDEO_ORIENTATION "video.orientation"
+
+#define SETTING_VIDEO_VDPAU_NOISE "vdpau.noise"
+#define SETTING_VIDEO_VDPAU_SHARPNESS "vdpau.sharpness"
+
+#define SETTING_VIDEO_INTERLACEMETHOD "video.interlacemethod"
+#define SETTING_VIDEO_SCALINGMETHOD "video.scalingmethod"
+
+#define SETTING_VIDEO_STEREOSCOPICMODE "video.stereoscopicmode"
+#define SETTING_VIDEO_STEREOSCOPICINVERT "video.stereoscopicinvert"
+
+#define SETTING_VIDEO_MAKE_DEFAULT "video.save"
+#define SETTING_VIDEO_CALIBRATION "video.calibration"
+#define SETTING_VIDEO_STREAM "video.stream"
+
+CGUIDialogVideoSettings::CGUIDialogVideoSettings()
+ : CGUIDialogSettingsManualBase(WINDOW_DIALOG_VIDEO_OSD_SETTINGS, "DialogSettings.xml")
+{ }
+
+CGUIDialogVideoSettings::~CGUIDialogVideoSettings() = default;
+
+void CGUIDialogVideoSettings::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_VIDEO_INTERLACEMETHOD)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_InterlaceMethod = static_cast<EINTERLACEMETHOD>(std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_SCALINGMETHOD)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_ScalingMethod = static_cast<ESCALINGMETHOD>(std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_STREAM)
+ {
+ m_videoStream = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ // only change the video stream if a different one has been asked for
+ if (appPlayer->GetVideoStream() != m_videoStream)
+ {
+ appPlayer->SetVideoStream(m_videoStream); // Set the video stream to the one selected
+ }
+ }
+ else if (settingId == SETTING_VIDEO_VIEW_MODE)
+ {
+ int value = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ const CVideoSettings vs = appPlayer->GetVideoSettings();
+
+ appPlayer->SetRenderViewMode(value, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio,
+ vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch);
+
+ m_viewModeChanged = true;
+ GetSettingsManager()->SetNumber(SETTING_VIDEO_ZOOM, static_cast<double>(vs.m_CustomZoomAmount));
+ GetSettingsManager()->SetNumber(SETTING_VIDEO_PIXEL_RATIO,
+ static_cast<double>(vs.m_CustomPixelRatio));
+ GetSettingsManager()->SetNumber(SETTING_VIDEO_VERTICAL_SHIFT,
+ static_cast<double>(vs.m_CustomVerticalShift));
+ GetSettingsManager()->SetBool(SETTING_VIDEO_NONLIN_STRETCH, vs.m_CustomNonLinStretch);
+ m_viewModeChanged = false;
+ }
+ else if (settingId == SETTING_VIDEO_ZOOM ||
+ settingId == SETTING_VIDEO_VERTICAL_SHIFT ||
+ settingId == SETTING_VIDEO_PIXEL_RATIO ||
+ settingId == SETTING_VIDEO_NONLIN_STRETCH)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ if (settingId == SETTING_VIDEO_ZOOM)
+ vs.m_CustomZoomAmount = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue());
+ else if (settingId == SETTING_VIDEO_VERTICAL_SHIFT)
+ vs.m_CustomVerticalShift = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue());
+ else if (settingId == SETTING_VIDEO_PIXEL_RATIO)
+ vs.m_CustomPixelRatio = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue());
+ else if (settingId == SETTING_VIDEO_NONLIN_STRETCH)
+ vs.m_CustomNonLinStretch = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+
+ // try changing the view mode to custom. If it already is set to custom
+ // manually call the render manager
+ if (GetSettingsManager()->GetInt(SETTING_VIDEO_VIEW_MODE) != ViewModeCustom)
+ GetSettingsManager()->SetInt(SETTING_VIDEO_VIEW_MODE, ViewModeCustom);
+ else
+ appPlayer->SetRenderViewMode(vs.m_ViewMode, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio,
+ vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch);
+ }
+ else if (settingId == SETTING_VIDEO_POSTPROCESS)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_PostProcess = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_BRIGHTNESS)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_Brightness = static_cast<float>(std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_CONTRAST)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_Contrast = static_cast<float>(std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_GAMMA)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_Gamma = static_cast<float>(std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_VDPAU_NOISE)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_NoiseReduction = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue());
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_VDPAU_SHARPNESS)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_Sharpness = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue());
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_TONEMAP_METHOD)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_ToneMapMethod = static_cast<ETONEMAPMETHOD>(
+ std::static_pointer_cast<const CSettingInt>(setting)->GetValue());
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_TONEMAP_PARAM)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_ToneMapParam = static_cast<float>(std::static_pointer_cast<const CSettingNumber>(setting)->GetValue());
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_ORIENTATION)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_Orientation = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_STEREOSCOPICMODE)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_StereoMode = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ appPlayer->SetVideoSettings(vs);
+ }
+ else if (settingId == SETTING_VIDEO_STEREOSCOPICINVERT)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_StereoInvert = std::static_pointer_cast<const CSettingBool>(setting)->GetValue();
+ appPlayer->SetVideoSettings(vs);
+ }
+}
+
+void CGUIDialogVideoSettings::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ CGUIDialogSettingsManualBase::OnSettingChanged(setting);
+
+ const std::string &settingId = setting->GetId();
+ if (settingId == SETTING_VIDEO_CALIBRATION)
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return;
+
+ auto calibsetting = settings->GetSetting(CSettings::SETTING_VIDEOSCREEN_GUICALIBRATION);
+ if (!calibsetting)
+ {
+ CLog::Log(LOGERROR, "Failed to load setting for: {}",
+ CSettings::SETTING_VIDEOSCREEN_GUICALIBRATION);
+ return;
+ }
+
+ // launch calibration window
+ if (profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE &&
+ g_passwordManager.CheckSettingLevelLock(calibsetting->GetLevel()))
+ return;
+
+ CServiceBroker::GetGUI()->GetWindowManager().ForceActivateWindow(WINDOW_SCREEN_CALIBRATION);
+ }
+ //! @todo implement
+ else if (settingId == SETTING_VIDEO_MAKE_DEFAULT)
+ Save();
+}
+
+bool CGUIDialogVideoSettings::Save()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE &&
+ !g_passwordManager.CheckSettingLevelLock(::SettingLevel::Expert))
+ return true;
+
+ // prompt user if they are sure
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant(12376), CVariant(12377)))
+ { // reset the settings
+ CVideoDatabase db;
+ if (!db.Open())
+ return true;
+ db.EraseAllVideoSettings();
+ db.Close();
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ CMediaSettings::GetInstance().GetDefaultVideoSettings() = appPlayer->GetVideoSettings();
+ CMediaSettings::GetInstance().GetDefaultVideoSettings().m_SubtitleStream = -1;
+ CMediaSettings::GetInstance().GetDefaultVideoSettings().m_AudioStream = -1;
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ }
+
+ return true;
+}
+
+void CGUIDialogVideoSettings::SetupView()
+{
+ CGUIDialogSettingsManualBase::SetupView();
+
+ SetHeading(13395);
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_OKAY_BUTTON);
+ SET_CONTROL_HIDDEN(CONTROL_SETTINGS_CUSTOM_BUTTON);
+ SET_CONTROL_LABEL(CONTROL_SETTINGS_CANCEL_BUTTON, 15067);
+}
+
+void CGUIDialogVideoSettings::InitializeSettings()
+{
+ CGUIDialogSettingsManualBase::InitializeSettings();
+
+ const std::shared_ptr<CSettingCategory> category = AddCategory("videosettings", -1);
+ if (category == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogVideoSettings: unable to setup settings");
+ return;
+ }
+
+ // get all necessary setting groups
+ const std::shared_ptr<CSettingGroup> groupVideoStream = AddGroup(category);
+ if (groupVideoStream == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogVideoSettings: unable to setup settings");
+ return;
+ }
+ const std::shared_ptr<CSettingGroup> groupVideo = AddGroup(category);
+ if (groupVideo == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogVideoSettings: unable to setup settings");
+ return;
+ }
+ const std::shared_ptr<CSettingGroup> groupStereoscopic = AddGroup(category);
+ if (groupStereoscopic == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogVideoSettings: unable to setup settings");
+ return;
+ }
+ const std::shared_ptr<CSettingGroup> groupSaveAsDefault = AddGroup(category);
+ if (groupSaveAsDefault == NULL)
+ {
+ CLog::Log(LOGERROR, "CGUIDialogVideoSettings: unable to setup settings");
+ return;
+ }
+
+ bool usePopup = g_SkinInfo->HasSkinFile("DialogSlider.xml");
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ const CVideoSettings videoSettings = appPlayer->GetVideoSettings();
+
+ TranslatableIntegerSettingOptions entries;
+
+ entries.clear();
+ entries.push_back(TranslatableIntegerSettingOption(16039, VS_INTERLACEMETHOD_NONE));
+ entries.push_back(TranslatableIntegerSettingOption(16019, VS_INTERLACEMETHOD_AUTO));
+ entries.push_back(TranslatableIntegerSettingOption(20131, VS_INTERLACEMETHOD_RENDER_BLEND));
+ entries.push_back(TranslatableIntegerSettingOption(20129, VS_INTERLACEMETHOD_RENDER_WEAVE));
+ entries.push_back(TranslatableIntegerSettingOption(16021, VS_INTERLACEMETHOD_RENDER_BOB));
+ entries.push_back(TranslatableIntegerSettingOption(16020, VS_INTERLACEMETHOD_DEINTERLACE));
+ entries.push_back(TranslatableIntegerSettingOption(16036, VS_INTERLACEMETHOD_DEINTERLACE_HALF));
+ entries.push_back(
+ TranslatableIntegerSettingOption(16311, VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL));
+ entries.push_back(TranslatableIntegerSettingOption(16310, VS_INTERLACEMETHOD_VDPAU_TEMPORAL));
+ entries.push_back(TranslatableIntegerSettingOption(16325, VS_INTERLACEMETHOD_VDPAU_BOB));
+ entries.push_back(
+ TranslatableIntegerSettingOption(16318, VS_INTERLACEMETHOD_VDPAU_TEMPORAL_SPATIAL_HALF));
+ entries.push_back(
+ TranslatableIntegerSettingOption(16317, VS_INTERLACEMETHOD_VDPAU_TEMPORAL_HALF));
+ entries.push_back(TranslatableIntegerSettingOption(16327, VS_INTERLACEMETHOD_VAAPI_BOB));
+ entries.push_back(TranslatableIntegerSettingOption(16328, VS_INTERLACEMETHOD_VAAPI_MADI));
+ entries.push_back(TranslatableIntegerSettingOption(16329, VS_INTERLACEMETHOD_VAAPI_MACI));
+ entries.push_back(TranslatableIntegerSettingOption(16320, VS_INTERLACEMETHOD_DXVA_AUTO));
+
+ /* remove unsupported methods */
+ for (TranslatableIntegerSettingOptions::iterator it = entries.begin(); it != entries.end(); )
+ {
+ if (appPlayer->Supports(static_cast<EINTERLACEMETHOD>(it->value)))
+ ++it;
+ else
+ it = entries.erase(it);
+ }
+
+ if (!entries.empty())
+ {
+ EINTERLACEMETHOD method = videoSettings.m_InterlaceMethod;
+ if (!appPlayer->Supports(method))
+ {
+ method = appPlayer->GetDeinterlacingMethodDefault();
+ }
+ AddSpinner(groupVideo, SETTING_VIDEO_INTERLACEMETHOD, 16038, SettingLevel::Basic, static_cast<int>(method), entries);
+ }
+
+ entries.clear();
+ entries.push_back(TranslatableIntegerSettingOption(16301, VS_SCALINGMETHOD_NEAREST));
+ entries.push_back(TranslatableIntegerSettingOption(16302, VS_SCALINGMETHOD_LINEAR));
+ entries.push_back(TranslatableIntegerSettingOption(16303, VS_SCALINGMETHOD_CUBIC_B_SPLINE));
+ entries.push_back(TranslatableIntegerSettingOption(16314, VS_SCALINGMETHOD_CUBIC_MITCHELL));
+ entries.push_back(TranslatableIntegerSettingOption(16321, VS_SCALINGMETHOD_CUBIC_CATMULL));
+ entries.push_back(TranslatableIntegerSettingOption(16326, VS_SCALINGMETHOD_CUBIC_0_075));
+ entries.push_back(TranslatableIntegerSettingOption(16330, VS_SCALINGMETHOD_CUBIC_0_1));
+ entries.push_back(TranslatableIntegerSettingOption(16304, VS_SCALINGMETHOD_LANCZOS2));
+ entries.push_back(TranslatableIntegerSettingOption(16323, VS_SCALINGMETHOD_SPLINE36_FAST));
+ entries.push_back(TranslatableIntegerSettingOption(16315, VS_SCALINGMETHOD_LANCZOS3_FAST));
+ entries.push_back(TranslatableIntegerSettingOption(16322, VS_SCALINGMETHOD_SPLINE36));
+ entries.push_back(TranslatableIntegerSettingOption(16305, VS_SCALINGMETHOD_LANCZOS3));
+ entries.push_back(TranslatableIntegerSettingOption(16306, VS_SCALINGMETHOD_SINC8));
+ entries.push_back(TranslatableIntegerSettingOption(16307, VS_SCALINGMETHOD_BICUBIC_SOFTWARE));
+ entries.push_back(TranslatableIntegerSettingOption(16308, VS_SCALINGMETHOD_LANCZOS_SOFTWARE));
+ entries.push_back(TranslatableIntegerSettingOption(16309, VS_SCALINGMETHOD_SINC_SOFTWARE));
+ entries.push_back(TranslatableIntegerSettingOption(13120, VS_SCALINGMETHOD_VDPAU_HARDWARE));
+ entries.push_back(TranslatableIntegerSettingOption(16319, VS_SCALINGMETHOD_DXVA_HARDWARE));
+ entries.push_back(TranslatableIntegerSettingOption(16316, VS_SCALINGMETHOD_AUTO));
+
+ /* remove unsupported methods */
+ for(TranslatableIntegerSettingOptions::iterator it = entries.begin(); it != entries.end(); )
+ {
+ if (appPlayer->Supports(static_cast<ESCALINGMETHOD>(it->value)))
+ ++it;
+ else
+ it = entries.erase(it);
+ }
+
+ AddSpinner(groupVideo, SETTING_VIDEO_SCALINGMETHOD, 16300, SettingLevel::Basic, static_cast<int>(videoSettings.m_ScalingMethod), entries);
+
+ AddVideoStreams(groupVideoStream, SETTING_VIDEO_STREAM);
+
+ if (appPlayer->Supports(RENDERFEATURE_STRETCH) || appPlayer->Supports(RENDERFEATURE_PIXEL_RATIO))
+ {
+ AddList(groupVideo, SETTING_VIDEO_VIEW_MODE, 629, SettingLevel::Basic, videoSettings.m_ViewMode, CViewModeSettings::ViewModesFiller, 629);
+ }
+ if (appPlayer->Supports(RENDERFEATURE_ZOOM))
+ AddSlider(groupVideo, SETTING_VIDEO_ZOOM, 216, SettingLevel::Basic,
+ videoSettings.m_CustomZoomAmount, "{:2.2f}", 0.5f, 0.01f, 2.0f, 216, usePopup);
+ if (appPlayer->Supports(RENDERFEATURE_VERTICAL_SHIFT))
+ AddSlider(groupVideo, SETTING_VIDEO_VERTICAL_SHIFT, 225, SettingLevel::Basic,
+ videoSettings.m_CustomVerticalShift, "{:2.2f}", -2.0f, 0.01f, 2.0f, 225, usePopup);
+ if (appPlayer->Supports(RENDERFEATURE_PIXEL_RATIO))
+ AddSlider(groupVideo, SETTING_VIDEO_PIXEL_RATIO, 217, SettingLevel::Basic,
+ videoSettings.m_CustomPixelRatio, "{:2.2f}", 0.5f, 0.01f, 2.0f, 217, usePopup);
+
+ AddList(groupVideo, SETTING_VIDEO_ORIENTATION, 21843, SettingLevel::Basic, videoSettings.m_Orientation, CGUIDialogVideoSettings::VideoOrientationFiller, 21843);
+
+ if (appPlayer->Supports(RENDERFEATURE_POSTPROCESS))
+ AddToggle(groupVideo, SETTING_VIDEO_POSTPROCESS, 16400, SettingLevel::Basic, videoSettings.m_PostProcess);
+ if (appPlayer->Supports(RENDERFEATURE_BRIGHTNESS))
+ AddPercentageSlider(groupVideo, SETTING_VIDEO_BRIGHTNESS, 464, SettingLevel::Basic, static_cast<int>(videoSettings.m_Brightness), 14047, 1, 464, usePopup);
+ if (appPlayer->Supports(RENDERFEATURE_CONTRAST))
+ AddPercentageSlider(groupVideo, SETTING_VIDEO_CONTRAST, 465, SettingLevel::Basic, static_cast<int>(videoSettings.m_Contrast), 14047, 1, 465, usePopup);
+ if (appPlayer->Supports(RENDERFEATURE_GAMMA))
+ AddPercentageSlider(groupVideo, SETTING_VIDEO_GAMMA, 466, SettingLevel::Basic, static_cast<int>(videoSettings.m_Gamma), 14047, 1, 466, usePopup);
+ if (appPlayer->Supports(RENDERFEATURE_NOISE))
+ AddSlider(groupVideo, SETTING_VIDEO_VDPAU_NOISE, 16312, SettingLevel::Basic,
+ videoSettings.m_NoiseReduction, "{:2.2f}", 0.0f, 0.01f, 1.0f, 16312, usePopup);
+ if (appPlayer->Supports(RENDERFEATURE_SHARPNESS))
+ AddSlider(groupVideo, SETTING_VIDEO_VDPAU_SHARPNESS, 16313, SettingLevel::Basic,
+ videoSettings.m_Sharpness, "{:2.2f}", -1.0f, 0.02f, 1.0f, 16313, usePopup);
+ if (appPlayer->Supports(RENDERFEATURE_NONLINSTRETCH))
+ AddToggle(groupVideo, SETTING_VIDEO_NONLIN_STRETCH, 659, SettingLevel::Basic, videoSettings.m_CustomNonLinStretch);
+
+ // tone mapping
+ if (appPlayer->Supports(RENDERFEATURE_TONEMAP))
+ {
+ bool visible = !(CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CServiceBroker::GetWinSystem()->SETTING_WINSYSTEM_IS_HDR_DISPLAY) &&
+ CServiceBroker::GetWinSystem()->IsHDRDisplay());
+ entries.clear();
+ entries.push_back(TranslatableIntegerSettingOption(36554, VS_TONEMAPMETHOD_OFF));
+ entries.push_back(TranslatableIntegerSettingOption(36555, VS_TONEMAPMETHOD_REINHARD));
+ entries.push_back(TranslatableIntegerSettingOption(36557, VS_TONEMAPMETHOD_ACES));
+ entries.push_back(TranslatableIntegerSettingOption(36558, VS_TONEMAPMETHOD_HABLE));
+
+ AddSpinner(groupVideo, SETTING_VIDEO_TONEMAP_METHOD, 36553, SettingLevel::Basic,
+ videoSettings.m_ToneMapMethod, entries, false, visible);
+ AddSlider(groupVideo, SETTING_VIDEO_TONEMAP_PARAM, 36556, SettingLevel::Basic,
+ videoSettings.m_ToneMapParam, "{:2.2f}", 0.1f, 0.1f, 5.0f, 36556, usePopup, false,
+ visible);
+ }
+
+ // stereoscopic settings
+ entries.clear();
+ entries.push_back(TranslatableIntegerSettingOption(16316, RENDER_STEREO_MODE_OFF));
+ entries.push_back(TranslatableIntegerSettingOption(36503, RENDER_STEREO_MODE_SPLIT_HORIZONTAL));
+ entries.push_back(TranslatableIntegerSettingOption(36504, RENDER_STEREO_MODE_SPLIT_VERTICAL));
+ AddSpinner(groupStereoscopic, SETTING_VIDEO_STEREOSCOPICMODE, 36535, SettingLevel::Basic, videoSettings.m_StereoMode, entries);
+ AddToggle(groupStereoscopic, SETTING_VIDEO_STEREOSCOPICINVERT, 36536, SettingLevel::Basic, videoSettings.m_StereoInvert);
+
+ // general settings
+ AddButton(groupSaveAsDefault, SETTING_VIDEO_MAKE_DEFAULT, 12376, SettingLevel::Basic);
+ AddButton(groupSaveAsDefault, SETTING_VIDEO_CALIBRATION, 214, SettingLevel::Basic);
+}
+
+void CGUIDialogVideoSettings::AddVideoStreams(const std::shared_ptr<CSettingGroup>& group,
+ const std::string& settingId)
+{
+ if (group == NULL || settingId.empty())
+ return;
+
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ m_videoStream = appPlayer->GetVideoStream();
+ if (m_videoStream < 0)
+ m_videoStream = 0;
+
+ AddList(group, settingId, 38031, SettingLevel::Basic, m_videoStream, VideoStreamsOptionFiller, 38031);
+}
+
+void CGUIDialogVideoSettings::VideoStreamsOptionFiller(
+ const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ int videoStreamCount = appPlayer->GetVideoStreamCount();
+ // cycle through each video stream and add it to our list control
+ for (int i = 0; i < videoStreamCount; ++i)
+ {
+ std::string strItem;
+ std::string strLanguage;
+
+ VideoStreamInfo info;
+ appPlayer->GetVideoStreamInfo(i, info);
+
+ g_LangCodeExpander.Lookup(info.language, strLanguage);
+
+ if (!info.name.empty())
+ {
+ if (!strLanguage.empty())
+ strItem = StringUtils::Format("{} - {}", strLanguage, info.name);
+ else
+ strItem = info.name;
+ }
+ else if (!strLanguage.empty())
+ {
+ strItem = strLanguage;
+ }
+
+ if (info.codecName.empty())
+ strItem += StringUtils::Format(" ({}x{}", info.width, info.height);
+ else
+ strItem += StringUtils::Format(" ({}, {}x{}", info.codecName, info.width, info.height);
+
+ if (info.bitrate)
+ strItem += StringUtils::Format(", {} bps)", info.bitrate);
+ else
+ strItem += ")";
+
+ strItem += FormatFlags(info.flags);
+ strItem += StringUtils::Format(" ({}/{})", i + 1, videoStreamCount);
+ list.emplace_back(strItem, i);
+ }
+
+ if (list.empty())
+ {
+ list.emplace_back(g_localizeStrings.Get(231), -1);
+ current = -1;
+ }
+}
+
+void CGUIDialogVideoSettings::VideoOrientationFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ list.emplace_back(g_localizeStrings.Get(687), 0);
+ list.emplace_back(g_localizeStrings.Get(35229), 90);
+ list.emplace_back(g_localizeStrings.Get(35230), 180);
+ list.emplace_back(g_localizeStrings.Get(35231), 270);
+}
+
+std::string CGUIDialogVideoSettings::FormatFlags(StreamFlags flags)
+{
+ std::vector<std::string> localizedFlags;
+ if (flags & StreamFlags::FLAG_DEFAULT)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39105));
+ if (flags & StreamFlags::FLAG_FORCED)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39106));
+ if (flags & StreamFlags::FLAG_HEARING_IMPAIRED)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39107));
+ if (flags & StreamFlags::FLAG_VISUAL_IMPAIRED)
+ localizedFlags.emplace_back(g_localizeStrings.Get(39108));
+
+ std::string formated = StringUtils::Join(localizedFlags, ", ");
+
+ if (!formated.empty())
+ formated = StringUtils::Format(" [{}]", formated);
+
+ return formated;
+}
diff --git a/xbmc/video/dialogs/GUIDialogVideoSettings.h b/xbmc/video/dialogs/GUIDialogVideoSettings.h
new file mode 100644
index 0000000..5314e40
--- /dev/null
+++ b/xbmc/video/dialogs/GUIDialogVideoSettings.h
@@ -0,0 +1,55 @@
+/*
+ * 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/VideoPlayer/Interface/StreamInfo.h"
+#include "settings/dialogs/GUIDialogSettingsManualBase.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+struct IntegerSettingOption;
+
+class CGUIDialogVideoSettings : public CGUIDialogSettingsManualBase
+{
+public:
+ CGUIDialogVideoSettings();
+ ~CGUIDialogVideoSettings() override;
+
+protected:
+ // implementations of ISettingCallback
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+ void AddVideoStreams(const std::shared_ptr<CSettingGroup>& group, const std::string& settingId);
+ static void VideoStreamsOptionFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ static void VideoOrientationFiller(const std::shared_ptr<const CSetting>& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data);
+
+ static std::string FormatFlags(StreamFlags flags);
+
+ // specialization of CGUIDialogSettingsBase
+ bool AllowResettingSettings() const override { return false; }
+ bool Save() override;
+ void SetupView() override;
+
+ // specialization of CGUIDialogSettingsManualBase
+ void InitializeSettings() override;
+
+private:
+ int m_videoStream;
+ bool m_viewModeChanged = false;
+};
diff --git a/xbmc/video/jobs/CMakeLists.txt b/xbmc/video/jobs/CMakeLists.txt
new file mode 100644
index 0000000..b63d8fe
--- /dev/null
+++ b/xbmc/video/jobs/CMakeLists.txt
@@ -0,0 +1,17 @@
+set(SOURCES VideoLibraryCleaningJob.cpp
+ VideoLibraryJob.cpp
+ VideoLibraryMarkWatchedJob.cpp
+ VideoLibraryProgressJob.cpp
+ VideoLibraryRefreshingJob.cpp
+ VideoLibraryScanningJob.cpp
+ VideoLibraryResetResumePointJob.cpp)
+
+set(HEADERS VideoLibraryCleaningJob.h
+ VideoLibraryJob.h
+ VideoLibraryMarkWatchedJob.h
+ VideoLibraryProgressJob.h
+ VideoLibraryRefreshingJob.h
+ VideoLibraryScanningJob.h
+ VideoLibraryResetResumePointJob.h)
+
+core_add_library(video_jobs)
diff --git a/xbmc/video/jobs/VideoLibraryCleaningJob.cpp b/xbmc/video/jobs/VideoLibraryCleaningJob.cpp
new file mode 100644
index 0000000..7f4adfd
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryCleaningJob.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2014-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 "VideoLibraryCleaningJob.h"
+
+#include "dialogs/GUIDialogExtendedProgressBar.h"
+#include "video/VideoDatabase.h"
+
+CVideoLibraryCleaningJob::CVideoLibraryCleaningJob(const std::set<int>& paths /* = std::set<int>() */, bool showDialog /* = false */)
+ : CVideoLibraryProgressJob(NULL),
+ m_paths(paths),
+ m_showDialog(showDialog)
+{ }
+
+CVideoLibraryCleaningJob::CVideoLibraryCleaningJob(const std::set<int>& paths, CGUIDialogProgressBarHandle* progressBar)
+ : CVideoLibraryProgressJob(progressBar),
+ m_paths(paths),
+ m_showDialog(false)
+{ }
+
+CVideoLibraryCleaningJob::~CVideoLibraryCleaningJob() = default;
+
+bool CVideoLibraryCleaningJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CVideoLibraryCleaningJob* cleaningJob = dynamic_cast<const CVideoLibraryCleaningJob*>(job);
+ if (cleaningJob == NULL)
+ return false;
+
+ return m_paths == cleaningJob->m_paths &&
+ m_showDialog == cleaningJob->m_showDialog;
+}
+
+bool CVideoLibraryCleaningJob::Work(CVideoDatabase &db)
+{
+ db.CleanDatabase(GetProgressBar(), m_paths, m_showDialog);
+ return true;
+}
diff --git a/xbmc/video/jobs/VideoLibraryCleaningJob.h b/xbmc/video/jobs/VideoLibraryCleaningJob.h
new file mode 100644
index 0000000..f85682c
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryCleaningJob.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014-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 "video/jobs/VideoLibraryProgressJob.h"
+
+#include <set>
+
+class CGUIDialogProgressBarHandle;
+
+/*!
+ \brief Video library job implementation for cleaning the video library.
+*/
+class CVideoLibraryCleaningJob : public CVideoLibraryProgressJob
+{
+public:
+ /*!
+ \brief Creates a new video library cleaning job for the given paths.
+
+ \param[in] paths Set with database IDs of paths to be cleaned
+ \param[in] showDialog Whether to show a modal dialog or not
+ */
+ CVideoLibraryCleaningJob(const std::set<int>& paths = std::set<int>(), bool showDialog = false);
+
+ /*!
+ \brief Creates a new video library cleaning job for the given paths.
+
+ \param[in] paths Set with database IDs of paths to be cleaned
+ \param[in] progressBar Progress bar to be used to display the cleaning progress
+ */
+ CVideoLibraryCleaningJob(const std::set<int>& paths, CGUIDialogProgressBarHandle* progressBar);
+ ~CVideoLibraryCleaningJob() override;
+
+ // specialization of CJob
+ const char *GetType() const override { return "VideoLibraryCleaningJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ // implementation of CVideoLibraryJob
+ bool Work(CVideoDatabase &db) override;
+
+private:
+ std::set<int> m_paths;
+ bool m_showDialog;
+};
diff --git a/xbmc/video/jobs/VideoLibraryJob.cpp b/xbmc/video/jobs/VideoLibraryJob.cpp
new file mode 100644
index 0000000..efbbdb4
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryJob.cpp
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2014-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 "VideoLibraryJob.h"
+
+#include "video/VideoDatabase.h"
+
+CVideoLibraryJob::CVideoLibraryJob() = default;
+
+CVideoLibraryJob::~CVideoLibraryJob() = default;
+
+bool CVideoLibraryJob::DoWork()
+{
+ CVideoDatabase db;
+ if (!db.Open())
+ return false;
+
+ return Work(db);
+}
diff --git a/xbmc/video/jobs/VideoLibraryJob.h b/xbmc/video/jobs/VideoLibraryJob.h
new file mode 100644
index 0000000..a8f7851
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryJob.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014-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 "utils/Job.h"
+
+class CVideoDatabase;
+
+/*!
+ \brief Basic implementation/interface of a CJob which interacts with the
+ video database.
+ */
+class CVideoLibraryJob : public CJob
+{
+public:
+ ~CVideoLibraryJob() override;
+
+ /*!
+ \brief Whether the job can be cancelled or not.
+ */
+ virtual bool CanBeCancelled() const { return false; }
+
+ /*!
+ \brief Tries to cancel the running job.
+
+ \return True if the job was cancelled, false otherwise
+ */
+ virtual bool Cancel() { return false; }
+
+ // implementation of CJob
+ bool DoWork() override;
+ const char *GetType() const override { return "VideoLibraryJob"; }
+ bool operator==(const CJob* job) const override { return false; }
+
+protected:
+ CVideoLibraryJob();
+
+ /*!
+ \brief Worker method to be implemented by an actual implementation.
+
+ \param[in] db Already open video database to be used for interaction
+ \return True if the process succeeded, false otherwise
+ */
+ virtual bool Work(CVideoDatabase &db) = 0;
+};
diff --git a/xbmc/video/jobs/VideoLibraryMarkWatchedJob.cpp b/xbmc/video/jobs/VideoLibraryMarkWatchedJob.cpp
new file mode 100644
index 0000000..25381bb
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryMarkWatchedJob.cpp
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2014-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 <vector>
+
+#include "VideoLibraryMarkWatchedJob.h"
+#include "FileItem.h"
+#include "Util.h"
+#include "filesystem/Directory.h"
+#ifdef HAS_UPNP
+#include "network/upnp/UPnP.h"
+#endif
+#include "pvr/PVRManager.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "profiles/ProfileManager.h"
+#include "settings/SettingsComponent.h"
+#include "ServiceBroker.h"
+#include "utils/URIUtils.h"
+#include "video/VideoDatabase.h"
+
+CVideoLibraryMarkWatchedJob::CVideoLibraryMarkWatchedJob(const std::shared_ptr<CFileItem>& item,
+ bool mark)
+ : m_item(item), m_mark(mark)
+{ }
+
+CVideoLibraryMarkWatchedJob::~CVideoLibraryMarkWatchedJob() = default;
+
+bool CVideoLibraryMarkWatchedJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CVideoLibraryMarkWatchedJob* markJob = dynamic_cast<const CVideoLibraryMarkWatchedJob*>(job);
+ if (markJob == NULL)
+ return false;
+
+ return m_item->IsSamePath(markJob->m_item.get()) && markJob->m_mark == m_mark;
+}
+
+bool CVideoLibraryMarkWatchedJob::Work(CVideoDatabase &db)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (!profileManager->GetCurrentProfile().canWriteDatabases())
+ return false;
+
+ CFileItemList items;
+ items.Add(CFileItemPtr(new CFileItem(*m_item)));
+
+ if (m_item->m_bIsFolder)
+ CUtil::GetRecursiveListing(m_item->GetPath(), items, "", XFILE::DIR_FLAG_NO_FILE_INFO);
+
+ std::vector<CFileItemPtr> markItems;
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items.Get(i);
+ if (item->HasVideoInfoTag() && m_mark == (item->GetVideoInfoTag()->GetPlayCount() > 0))
+ continue;
+
+#ifdef HAS_UPNP
+ if (URIUtils::IsUPnP(item->GetPath()) && UPNP::CUPnP::MarkWatched(*item, m_mark))
+ continue;
+#endif
+
+ if (item->HasPVRRecordingInfoTag() &&
+ CServiceBroker::GetPVRManager().Recordings()->MarkWatched(item->GetPVRRecordingInfoTag(), m_mark))
+ {
+ CDateTime newLastPlayed;
+ if (m_mark)
+ newLastPlayed = db.IncrementPlayCount(*item);
+ else
+ newLastPlayed = db.SetPlayCount(*item, 0);
+
+ if (newLastPlayed.IsValid())
+ item->GetVideoInfoTag()->m_lastPlayed = newLastPlayed;
+
+ continue;
+ }
+
+ markItems.push_back(item);
+ }
+
+ if (markItems.empty())
+ return true;
+
+ db.BeginTransaction();
+
+ for (std::vector<CFileItemPtr>::const_iterator iter = markItems.begin(); iter != markItems.end(); ++iter)
+ {
+ const CFileItemPtr& item = *iter;
+
+ std::string path(item->GetPath());
+ if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->GetPath().empty())
+ path = item->GetVideoInfoTag()->GetPath();
+
+ // With both mark as watched and unwatched we want the resume bookmarks to be reset
+ db.ClearBookMarksOfFile(path, CBookmark::RESUME);
+
+ CDateTime newLastPlayed;
+ if (m_mark)
+ newLastPlayed = db.IncrementPlayCount(*item);
+ else
+ newLastPlayed = db.SetPlayCount(*item, 0);
+
+ if (newLastPlayed.IsValid() && item->HasVideoInfoTag())
+ item->GetVideoInfoTag()->m_lastPlayed = newLastPlayed;
+ }
+
+ db.CommitTransaction();
+ db.Close();
+
+ return true;
+}
diff --git a/xbmc/video/jobs/VideoLibraryMarkWatchedJob.h b/xbmc/video/jobs/VideoLibraryMarkWatchedJob.h
new file mode 100644
index 0000000..e683b3f
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryMarkWatchedJob.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2014-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 "video/jobs/VideoLibraryJob.h"
+
+#include <memory>
+
+class CFileItem;
+
+/*!
+ \brief Video library job implementation for marking items as watched/unwatched.
+ */
+class CVideoLibraryMarkWatchedJob : public CVideoLibraryJob
+{
+public:
+ /*!
+ \brief Creates a new video library scanning job.
+
+ \param[in] item Item to be marked as watched/unwatched
+ \param[in] mark Whether to mark the item as watched or unwatched
+ */
+ CVideoLibraryMarkWatchedJob(const std::shared_ptr<CFileItem>& item, bool mark);
+ ~CVideoLibraryMarkWatchedJob() override;
+
+ const char *GetType() const override { return "CVideoLibraryMarkWatchedJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ bool Work(CVideoDatabase &db) override;
+
+private:
+ std::shared_ptr<CFileItem> m_item;
+ bool m_mark;
+};
diff --git a/xbmc/video/jobs/VideoLibraryProgressJob.cpp b/xbmc/video/jobs/VideoLibraryProgressJob.cpp
new file mode 100644
index 0000000..0aab524
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryProgressJob.cpp
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2014-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 "VideoLibraryProgressJob.h"
+
+CVideoLibraryProgressJob::CVideoLibraryProgressJob(CGUIDialogProgressBarHandle* progressBar)
+ : CProgressJob(progressBar)
+{ }
+
+CVideoLibraryProgressJob::~CVideoLibraryProgressJob() = default;
+
+bool CVideoLibraryProgressJob::DoWork()
+{
+ bool result = CVideoLibraryJob::DoWork();
+
+ MarkFinished();
+
+ return result;
+}
diff --git a/xbmc/video/jobs/VideoLibraryProgressJob.h b/xbmc/video/jobs/VideoLibraryProgressJob.h
new file mode 100644
index 0000000..278072b
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryProgressJob.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2014-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 "utils/ProgressJob.h"
+#include "video/jobs/VideoLibraryJob.h"
+
+/*!
+ \brief Combined base implementation of a video library job with a progress bar.
+ */
+class CVideoLibraryProgressJob : public CProgressJob, public CVideoLibraryJob
+{
+public:
+ ~CVideoLibraryProgressJob() override;
+
+ // implementation of CJob
+ bool DoWork() override;
+ const char *GetType() const override { return "CVideoLibraryProgressJob"; }
+ bool operator==(const CJob* job) const override { return false; }
+
+protected:
+ explicit CVideoLibraryProgressJob(CGUIDialogProgressBarHandle* progressBar);
+};
diff --git a/xbmc/video/jobs/VideoLibraryRefreshingJob.cpp b/xbmc/video/jobs/VideoLibraryRefreshingJob.cpp
new file mode 100644
index 0000000..dbac5ed
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryRefreshingJob.cpp
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2014-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 "VideoLibraryRefreshingJob.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "TextureDatabase.h"
+#include "URL.h"
+#include "addons/Scraper.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/PluginDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "media/MediaType.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoInfoDownloader.h"
+#include "video/VideoInfoScanner.h"
+#include "video/tags/IVideoInfoTagLoader.h"
+#include "video/tags/VideoInfoTagLoaderFactory.h"
+#include "video/tags/VideoTagLoaderPlugin.h"
+
+#include <utility>
+
+using namespace KODI::MESSAGING;
+using namespace VIDEO;
+
+CVideoLibraryRefreshingJob::CVideoLibraryRefreshingJob(std::shared_ptr<CFileItem> item,
+ bool forceRefresh,
+ bool refreshAll,
+ bool ignoreNfo /* = false */,
+ const std::string& searchTitle /* = "" */)
+ : CVideoLibraryProgressJob(nullptr),
+ m_item(std::move(item)),
+ m_forceRefresh(forceRefresh),
+ m_refreshAll(refreshAll),
+ m_ignoreNfo(ignoreNfo),
+ m_searchTitle(searchTitle)
+{ }
+
+CVideoLibraryRefreshingJob::~CVideoLibraryRefreshingJob() = default;
+
+bool CVideoLibraryRefreshingJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CVideoLibraryRefreshingJob* refreshingJob = dynamic_cast<const CVideoLibraryRefreshingJob*>(job);
+ if (refreshingJob == nullptr)
+ return false;
+
+ return m_item->GetPath() == refreshingJob->m_item->GetPath();
+}
+
+bool CVideoLibraryRefreshingJob::Work(CVideoDatabase &db)
+{
+ if (m_item == nullptr)
+ return false;
+
+ // determine the scraper for the item's path
+ VIDEO::SScanSettings scanSettings;
+ ADDON::ScraperPtr scraper = db.GetScraperForPath(m_item->GetPath(), scanSettings);
+ if (scraper == nullptr)
+ return false;
+
+ if (URIUtils::IsPlugin(m_item->GetPath()) && !XFILE::CPluginDirectory::IsMediaLibraryScanningAllowed(ADDON::TranslateContent(scraper->Content()), m_item->GetPath()))
+ {
+ CLog::Log(LOGINFO,
+ "CVideoLibraryRefreshingJob: Plugin '{}' does not support media library scanning and "
+ "refreshing",
+ CURL::GetRedacted(m_item->GetPath()));
+ return false;
+ }
+
+ // copy the scraper in case we need it again
+ ADDON::ScraperPtr originalScraper(scraper);
+
+ // get the item's correct title
+ std::string itemTitle = m_searchTitle;
+ if (itemTitle.empty())
+ itemTitle = m_item->GetMovieName(scanSettings.parent_name);
+
+ CScraperUrl scraperUrl;
+ bool needsRefresh = m_forceRefresh;
+ bool hasDetails = false;
+ bool ignoreNfo = m_ignoreNfo;
+
+ // run this in a loop in case we need to refresh again
+ bool failure = false;
+ do
+ {
+ std::unique_ptr<CVideoInfoTag> pluginTag;
+ std::unique_ptr<CGUIListItem::ArtMap> pluginArt;
+
+ if (!ignoreNfo)
+ {
+ std::unique_ptr<IVideoInfoTagLoader> loader;
+ loader.reset(CVideoInfoTagLoaderFactory::CreateLoader(*m_item, scraper,
+ scanSettings.parent_name_root, m_forceRefresh));
+ // check if there's an NFO for the item
+ CInfoScanner::INFO_TYPE nfoResult = CInfoScanner::NO_NFO;
+ if (loader)
+ {
+ std::unique_ptr<CVideoInfoTag> tag(new CVideoInfoTag());
+ nfoResult = loader->Load(*tag, false);
+ if (nfoResult == CInfoScanner::FULL_NFO && m_item->IsPlugin() && scraper->ID() == "metadata.local")
+ {
+ // get video info and art from plugin source with metadata.local scraper
+ if (scraper->Content() == CONTENT_TVSHOWS && !m_item->m_bIsFolder && tag->m_iIdShow < 0)
+ // preserve show_id for episode
+ tag->m_iIdShow = m_item->GetVideoInfoTag()->m_iIdShow;
+ pluginTag = std::move(tag);
+ CVideoTagLoaderPlugin* nfo = dynamic_cast<CVideoTagLoaderPlugin*>(loader.get());
+ if (nfo && nfo->GetArt())
+ pluginArt = std::move(nfo->GetArt());
+ }
+ else if (nfoResult == CInfoScanner::URL_NFO)
+ scraperUrl = loader->ScraperUrl();
+ }
+
+ // if there's no NFO remember it in case we have to refresh again
+ if (nfoResult == CInfoScanner::ERROR_NFO)
+ ignoreNfo = true;
+ else if (nfoResult != CInfoScanner::NO_NFO)
+ hasDetails = true;
+
+ // if we are performing a forced refresh ask the user to choose between using a valid NFO and a valid scraper
+ if (needsRefresh && IsModal() && !scraper->IsNoop()
+ && nfoResult != CInfoScanner::ERROR_NFO)
+ {
+ int heading = 20159;
+ if (scraper->Content() == CONTENT_MOVIES)
+ heading = 13346;
+ else if (scraper->Content() == CONTENT_TVSHOWS)
+ heading = m_item->m_bIsFolder ? 20351 : 20352;
+ else if (scraper->Content() == CONTENT_MUSICVIDEOS)
+ heading = 20393;
+ if (CGUIDialogYesNo::ShowAndGetInput(heading, 20446))
+ {
+ hasDetails = false;
+ ignoreNfo = true;
+ scraperUrl.Clear();
+ scraper = originalScraper;
+ }
+ }
+ }
+
+ // no need to re-fetch the episode guide for episodes
+ if (scraper->Content() == CONTENT_TVSHOWS && !m_item->m_bIsFolder)
+ hasDetails = true;
+
+ // if we don't have an url or need to refresh anyway do the web search
+ if (!hasDetails && (needsRefresh || !scraperUrl.HasUrls()))
+ {
+ SetTitle(StringUtils::Format(g_localizeStrings.Get(197), scraper->Name()));
+ SetText(itemTitle);
+ SetProgress(0);
+
+ // clear any cached data from the scraper
+ scraper->ClearCache();
+
+ // create the info downloader for the scraper
+ CVideoInfoDownloader infoDownloader(scraper);
+
+ // try to find a matching item
+ MOVIELIST itemResultList;
+ int result = infoDownloader.FindMovie(itemTitle, -1, itemResultList, GetProgressDialog());
+
+ // close the progress dialog
+ MarkFinished();
+
+ if (result > 0)
+ {
+ // there are multiple matches for the item
+ if (!itemResultList.empty())
+ {
+ // choose the first match
+ if (!IsModal())
+ scraperUrl = itemResultList.at(0);
+ else
+ {
+ // ask the user what to do
+ CGUIDialogSelect* selectDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ selectDialog->Reset();
+ selectDialog->SetHeading(scraper->Content() == CONTENT_TVSHOWS ? 20356 : 196);
+ for (const auto& itemResult : itemResultList)
+ selectDialog->Add(itemResult.GetTitle());
+ selectDialog->EnableButton(true, 413); // "Manual"
+ selectDialog->Open();
+
+ // check if the user has chosen one of the results
+ int selectedItem = selectDialog->GetSelectedItem();
+ if (selectedItem >= 0)
+ scraperUrl = itemResultList.at(selectedItem);
+ // the user hasn't chosen one of the results and but has chosen to manually enter a title to use
+ else if (selectDialog->IsButtonPressed())
+ {
+ // ask the user to input a title to use
+ if (!CGUIKeyboardFactory::ShowAndGetInput(itemTitle, g_localizeStrings.Get(scraper->Content() == CONTENT_TVSHOWS ? 20357 : 16009), false))
+ return false;
+
+ // go through the whole process again
+ needsRefresh = true;
+ continue;
+ }
+ // nothing else we can do
+ else
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "CVideoLibraryRefreshingJob: user selected item '{}' with URL '{}'",
+ scraperUrl.GetTitle(), scraperUrl.GetFirstThumbUrl());
+ }
+ }
+ else if (result < 0 || !VIDEO::CVideoInfoScanner::DownloadFailed(GetProgressDialog()))
+ {
+ failure = true;
+ break;
+ }
+ }
+
+ // if the URL is still empty, check whether or not we're allowed
+ // to prompt and ask the user to input a new search title
+ if (!hasDetails && !scraperUrl.HasUrls())
+ {
+ if (IsModal())
+ {
+ // ask the user to input a title to use
+ if (!CGUIKeyboardFactory::ShowAndGetInput(itemTitle, g_localizeStrings.Get(scraper->Content() == CONTENT_TVSHOWS ? 20357 : 16009), false))
+ return false;
+
+ // go through the whole process again
+ needsRefresh = true;
+ continue;
+ }
+
+ // nothing else we can do
+ failure = true;
+ break;
+ }
+
+ // before we start downloading all the necessary information cleanup any existing artwork and hashes
+ CTextureDatabase textureDb;
+ if (textureDb.Open())
+ {
+ for (const auto& artwork : m_item->GetArt())
+ textureDb.InvalidateCachedTexture(artwork.second);
+
+ textureDb.Close();
+ }
+ m_item->ClearArt();
+
+ // put together the list of items to refresh
+ std::string path = m_item->GetPath();
+ CFileItemList items;
+ if (m_item->HasVideoInfoTag() && m_item->GetVideoInfoTag()->m_iDbId > 0)
+ {
+ // for a tvshow we need to handle all paths of it
+ std::vector<std::string> tvshowPaths;
+ if (CMediaTypes::IsMediaType(m_item->GetVideoInfoTag()->m_type, MediaTypeTvShow) && m_refreshAll &&
+ db.GetPathsLinkedToTvShow(m_item->GetVideoInfoTag()->m_iDbId, tvshowPaths))
+ {
+ for (const auto& tvshowPath : tvshowPaths)
+ {
+ CFileItemPtr tvshowItem(new CFileItem(*m_item->GetVideoInfoTag()));
+ tvshowItem->SetPath(tvshowPath);
+ items.Add(tvshowItem);
+ }
+ }
+ // otherwise just add a copy of the item
+ else
+ items.Add(CFileItemPtr(new CFileItem(*m_item->GetVideoInfoTag())));
+
+ // update the path to the real path (instead of a videodb:// one)
+ path = m_item->GetVideoInfoTag()->m_strPath;
+ }
+ else
+ items.Add(CFileItemPtr(new CFileItem(*m_item)));
+
+ // set the proper path of the list of items to lookup
+ items.SetPath(m_item->m_bIsFolder ? URIUtils::GetParentPath(path) : URIUtils::GetDirectory(path));
+
+ int headingLabel = 198;
+ if (scraper->Content() == CONTENT_TVSHOWS)
+ {
+ if (m_item->m_bIsFolder)
+ headingLabel = 20353;
+ else
+ headingLabel = 20361;
+ }
+ else if (scraper->Content() == CONTENT_MUSICVIDEOS)
+ headingLabel = 20394;
+
+ // prepare the progress dialog for downloading all the necessary information
+ SetTitle(g_localizeStrings.Get(headingLabel));
+ SetText(scraperUrl.GetTitle());
+ SetProgress(0);
+
+ // remove any existing data for the item we're going to refresh
+ if (m_item->GetVideoInfoTag()->m_iDbId > 0)
+ {
+ int dbId = m_item->GetVideoInfoTag()->m_iDbId;
+ if (scraper->Content() == CONTENT_MOVIES)
+ db.DeleteMovie(dbId);
+ else if (scraper->Content() == CONTENT_MUSICVIDEOS)
+ db.DeleteMusicVideo(dbId);
+ else if (scraper->Content() == CONTENT_TVSHOWS)
+ {
+ if (!m_item->m_bIsFolder)
+ db.DeleteEpisode(dbId);
+ else if (m_refreshAll)
+ db.DeleteTvShow(dbId);
+ else
+ db.DeleteDetailsForTvShow(dbId);
+ }
+ }
+
+ if (pluginTag || pluginArt)
+ // set video info and art from plugin source with metadata.local scraper to items
+ for (auto &i: items)
+ {
+ if (pluginTag)
+ *i->GetVideoInfoTag() = *pluginTag;
+ if (pluginArt)
+ i->SetArt(*pluginArt);
+ }
+
+ // finally download the information for the item
+ CVideoInfoScanner scanner;
+ if (!scanner.RetrieveVideoInfo(items, scanSettings.parent_name,
+ scraper->Content(), !ignoreNfo,
+ scraperUrl.HasUrls() ? &scraperUrl : nullptr,
+ m_refreshAll, GetProgressDialog()))
+ {
+ // something went wrong
+ MarkFinished();
+
+ // check if the user cancelled
+ if (!IsCancelled() && IsModal())
+ HELPERS::ShowOKDialogText(CVariant{195}, CVariant{itemTitle});
+
+ return false;
+ }
+
+ // retrieve the updated information from the database
+ if (scraper->Content() == CONTENT_MOVIES)
+ db.GetMovieInfo(m_item->GetPath(), *m_item->GetVideoInfoTag());
+ else if (scraper->Content() == CONTENT_MUSICVIDEOS)
+ db.GetMusicVideoInfo(m_item->GetPath(), *m_item->GetVideoInfoTag());
+ else if (scraper->Content() == CONTENT_TVSHOWS)
+ {
+ // update tvshow info to get updated episode numbers
+ if (m_item->m_bIsFolder)
+ db.GetTvShowInfo(m_item->GetPath(), *m_item->GetVideoInfoTag());
+ else
+ db.GetEpisodeInfo(m_item->GetPath(), *m_item->GetVideoInfoTag());
+ }
+
+ // we're finally done
+ MarkFinished();
+ break;
+ } while (needsRefresh);
+
+ if (failure && IsModal())
+ HELPERS::ShowOKDialogText(CVariant{195}, CVariant{itemTitle});
+
+ return true;
+}
diff --git a/xbmc/video/jobs/VideoLibraryRefreshingJob.h b/xbmc/video/jobs/VideoLibraryRefreshingJob.h
new file mode 100644
index 0000000..e98f4c0
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryRefreshingJob.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014-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 "video/jobs/VideoLibraryProgressJob.h"
+
+#include <memory>
+#include <string>
+
+class CFileItem;
+
+/*!
+ \brief Video library job implementation for refreshing a single item.
+*/
+class CVideoLibraryRefreshingJob : public CVideoLibraryProgressJob
+{
+public:
+ /*!
+ \brief Creates a new video library cleaning job for the given paths.
+
+ \param[inout] item Video item to be refreshed
+ \param[in] forceRefresh Whether to force a complete refresh (including NFO or internet lookup)
+ \param[in] refreshAll Whether to refresh all sub-items (in case of a tvshow)
+ \param[in] ignoreNfo Whether or not to ignore local NFO files
+ \param[in] searchTitle Title to use for the search (instead of determining it from the item's filename/path)
+ */
+ CVideoLibraryRefreshingJob(std::shared_ptr<CFileItem> item,
+ bool forceRefresh,
+ bool refreshAll,
+ bool ignoreNfo = false,
+ const std::string& searchTitle = "");
+
+ ~CVideoLibraryRefreshingJob() override;
+
+ // specialization of CJob
+ const char *GetType() const override { return "VideoLibraryRefreshingJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ // implementation of CVideoLibraryJob
+ bool Work(CVideoDatabase &db) override;
+
+private:
+ std::shared_ptr<CFileItem> m_item;
+ bool m_forceRefresh;
+ bool m_refreshAll;
+ bool m_ignoreNfo;
+ std::string m_searchTitle;
+};
diff --git a/xbmc/video/jobs/VideoLibraryResetResumePointJob.cpp b/xbmc/video/jobs/VideoLibraryResetResumePointJob.cpp
new file mode 100644
index 0000000..15b71a2
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryResetResumePointJob.cpp
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017-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 "VideoLibraryResetResumePointJob.h"
+
+#include <vector>
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "filesystem/IDirectory.h"
+#ifdef HAS_UPNP
+#include "network/upnp/UPnP.h"
+#endif
+#include "profiles/ProfileManager.h"
+#include "pvr/PVRManager.h"
+#include "pvr/recordings/PVRRecordings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+#include "video/VideoDatabase.h"
+
+CVideoLibraryResetResumePointJob::CVideoLibraryResetResumePointJob(
+ const std::shared_ptr<CFileItem>& item)
+ : m_item(item)
+{
+}
+
+bool CVideoLibraryResetResumePointJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CVideoLibraryResetResumePointJob* resetJob = dynamic_cast<const CVideoLibraryResetResumePointJob*>(job);
+ if (!resetJob)
+ return false;
+
+ return m_item->IsSamePath(resetJob->m_item.get());
+}
+
+bool CVideoLibraryResetResumePointJob::Work(CVideoDatabase &db)
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (!profileManager->GetCurrentProfile().canWriteDatabases())
+ return false;
+
+ CFileItemList items;
+ items.Add(std::make_shared<CFileItem>(*m_item));
+
+ if (m_item->m_bIsFolder)
+ CUtil::GetRecursiveListing(m_item->GetPath(), items, "", XFILE::DIR_FLAG_NO_FILE_INFO);
+
+ std::vector<CFileItemPtr> resetItems;
+ for (const auto& item : items)
+ {
+#ifdef HAS_UPNP
+ if (URIUtils::IsUPnP(item->GetPath()) && UPNP::CUPnP::SaveFileState(*item, CBookmark(), false /* updatePlayCount */))
+ continue;
+#endif
+
+ if (item->HasPVRRecordingInfoTag() &&
+ CServiceBroker::GetPVRManager().Recordings()->ResetResumePoint(item->GetPVRRecordingInfoTag()))
+ continue;
+
+ resetItems.emplace_back(item);
+ }
+
+ if (resetItems.empty())
+ return true;
+
+ db.BeginTransaction();
+
+ for (const auto& resetItem : resetItems)
+ {
+ db.DeleteResumeBookMark(*resetItem);
+ }
+
+ db.CommitTransaction();
+ db.Close();
+
+ return true;
+}
diff --git a/xbmc/video/jobs/VideoLibraryResetResumePointJob.h b/xbmc/video/jobs/VideoLibraryResetResumePointJob.h
new file mode 100644
index 0000000..1e2d69a
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryResetResumePointJob.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017-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 "video/jobs/VideoLibraryJob.h"
+
+#include <memory>
+
+/*!
+ \brief Video library job implementation for resetting a resume point.
+ */
+class CVideoLibraryResetResumePointJob : public CVideoLibraryJob
+{
+public:
+ /*!
+ \brief Creates a new job for resetting a given item's resume point.
+
+ \param[in] item Item for that the resume point shall be reset.
+ */
+ CVideoLibraryResetResumePointJob(const std::shared_ptr<CFileItem>& item);
+ ~CVideoLibraryResetResumePointJob() override = default;
+
+ const char *GetType() const override { return "CVideoLibraryResetResumePointJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ bool Work(CVideoDatabase &db) override;
+
+private:
+ std::shared_ptr<CFileItem> m_item;
+};
diff --git a/xbmc/video/jobs/VideoLibraryScanningJob.cpp b/xbmc/video/jobs/VideoLibraryScanningJob.cpp
new file mode 100644
index 0000000..f938420
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryScanningJob.cpp
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014-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 "VideoLibraryScanningJob.h"
+
+#include "video/VideoDatabase.h"
+
+CVideoLibraryScanningJob::CVideoLibraryScanningJob(const std::string& directory, bool scanAll /* = false */, bool showProgress /* = true */)
+ : m_scanner(),
+ m_directory(directory),
+ m_showProgress(showProgress),
+ m_scanAll(scanAll)
+{ }
+
+CVideoLibraryScanningJob::~CVideoLibraryScanningJob() = default;
+
+bool CVideoLibraryScanningJob::Cancel()
+{
+ if (!m_scanner.IsScanning())
+ return true;
+
+ m_scanner.Stop();
+ return true;
+}
+
+bool CVideoLibraryScanningJob::operator==(const CJob* job) const
+{
+ if (strcmp(job->GetType(), GetType()) != 0)
+ return false;
+
+ const CVideoLibraryScanningJob* scanningJob = dynamic_cast<const CVideoLibraryScanningJob*>(job);
+ if (scanningJob == NULL)
+ return false;
+
+ return m_directory == scanningJob->m_directory &&
+ m_scanAll == scanningJob->m_scanAll;
+}
+
+bool CVideoLibraryScanningJob::Work(CVideoDatabase &db)
+{
+ m_scanner.ShowDialog(m_showProgress);
+ m_scanner.Start(m_directory, m_scanAll);
+
+ return true;
+}
diff --git a/xbmc/video/jobs/VideoLibraryScanningJob.h b/xbmc/video/jobs/VideoLibraryScanningJob.h
new file mode 100644
index 0000000..3df98a7
--- /dev/null
+++ b/xbmc/video/jobs/VideoLibraryScanningJob.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2014-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 "video/VideoInfoScanner.h"
+#include "video/jobs/VideoLibraryJob.h"
+
+#include <string>
+
+/*!
+ \brief Video library job implementation for scanning items.
+
+ Uses CVideoInfoScanner for the whole filesystem scanning and can be run with
+ or without a visible progress bar.
+ */
+class CVideoLibraryScanningJob : public CVideoLibraryJob
+{
+public:
+ /*!
+ \brief Creates a new video library scanning job.
+
+ \param[in] directory Directory to be scanned for new items
+ \param[in] scanAll Whether to scan all items or not
+ \param[in] showProgress Whether to show a progress bar or not
+ */
+ CVideoLibraryScanningJob(const std::string& directory, bool scanAll = false, bool showProgress = true);
+ ~CVideoLibraryScanningJob() override;
+
+ // specialization of CVideoLibraryJob
+ bool CanBeCancelled() const override { return true; }
+ bool Cancel() override;
+
+ // specialization of CJob
+ const char *GetType() const override { return "VideoLibraryScanningJob"; }
+ bool operator==(const CJob* job) const override;
+
+protected:
+ // implementation of CVideoLibraryJob
+ bool Work(CVideoDatabase &db) override;
+
+private:
+ VIDEO::CVideoInfoScanner m_scanner;
+ std::string m_directory;
+ bool m_showProgress;
+ bool m_scanAll;
+};
diff --git a/xbmc/video/tags/CMakeLists.txt b/xbmc/video/tags/CMakeLists.txt
new file mode 100644
index 0000000..ab289f8
--- /dev/null
+++ b/xbmc/video/tags/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(SOURCES VideoInfoTagLoaderFactory.cpp
+ VideoTagLoaderFFmpeg.cpp
+ VideoTagLoaderNFO.cpp
+ VideoTagLoaderPlugin.cpp)
+
+set(HEADERS IVideoInfoTagLoader.h
+ VideoInfoTagLoaderFactory.h
+ VideoTagLoaderFFmpeg.h
+ VideoTagLoaderNFO.h
+ VideoTagLoaderPlugin.h)
+
+core_add_library(video_tags)
diff --git a/xbmc/video/tags/IVideoInfoTagLoader.h b/xbmc/video/tags/IVideoInfoTagLoader.h
new file mode 100644
index 0000000..8afc31e
--- /dev/null
+++ b/xbmc/video/tags/IVideoInfoTagLoader.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017-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 "InfoScanner.h"
+#include "addons/Scraper.h"
+
+#include <string>
+#include <utility>
+
+class CFileItem;
+class CVideoInfoTag;
+class EmbeddedArt;
+
+namespace VIDEO
+{
+
+//! \brief Base class for video tag loaders.
+class IVideoInfoTagLoader
+{
+public:
+ //! \brief Constructor
+ //! \param item The item to load info for
+ //! \param info Scraper info
+ //! \param llokInFolder True to look in folder holding file
+ IVideoInfoTagLoader(const CFileItem& item, ADDON::ScraperPtr info, bool lookInFolder)
+ : m_item(item), m_info(std::move(info))
+ {
+ }
+ virtual ~IVideoInfoTagLoader() = default;
+
+ //! \brief Returns true if we have info to provide.
+ virtual bool HasInfo() const = 0;
+
+ //! \brief Load tag from file.
+ //! \brief tag Tag to load info into
+ //! \brief prioritise True to prioritise data over existing data in tag
+ //! \returns True if tag was read, false otherwise
+ virtual CInfoScanner::INFO_TYPE Load(CVideoInfoTag& tag,
+ bool prioritise,
+ std::vector<EmbeddedArt>* art = nullptr) = 0;
+
+ //! \brief Returns url associated with obtained URL (NFO_URL et al).
+ const CScraperUrl& ScraperUrl() const { return m_url; }
+
+ //! \brief Returns current scaper info.
+ const ADDON::ScraperPtr GetAddonInfo() const { return m_info; }
+
+protected:
+ const CFileItem& m_item; //!< Reference to item to load for
+ ADDON::ScraperPtr m_info; //!< Scraper info
+ CScraperUrl m_url; //!< URL for scraper
+};
+
+}
diff --git a/xbmc/video/tags/VideoInfoTagLoaderFactory.cpp b/xbmc/video/tags/VideoInfoTagLoaderFactory.cpp
new file mode 100644
index 0000000..00ed67a
--- /dev/null
+++ b/xbmc/video/tags/VideoInfoTagLoaderFactory.cpp
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017-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 "VideoInfoTagLoaderFactory.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "VideoTagLoaderFFmpeg.h"
+#include "VideoTagLoaderNFO.h"
+#include "VideoTagLoaderPlugin.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+using namespace VIDEO;
+
+IVideoInfoTagLoader* CVideoInfoTagLoaderFactory::CreateLoader(const CFileItem& item,
+ const ADDON::ScraperPtr& info,
+ bool lookInFolder,
+ bool forceRefresh)
+{
+ if (item.IsPlugin() && info && info->ID() == "metadata.local")
+ {
+ // Direct loading from plugin source with metadata.local scraper
+ CVideoTagLoaderPlugin* plugin = new CVideoTagLoaderPlugin(item, forceRefresh);
+ if (plugin->HasInfo())
+ return plugin;
+ delete plugin;
+ }
+
+ CVideoTagLoaderNFO* nfo = new CVideoTagLoaderNFO(item, info, lookInFolder);
+ if (nfo->HasInfo())
+ return nfo;
+ delete nfo;
+
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_USETAGS) &&
+ (item.IsType(".mkv") || item.IsType(".mp4") || item.IsType(".avi") || item.IsType(".m4v")))
+ {
+ CVideoTagLoaderFFmpeg* ff = new CVideoTagLoaderFFmpeg(item, info, lookInFolder);
+ if (ff->HasInfo())
+ return ff;
+ delete ff;
+ }
+
+ return nullptr;
+}
diff --git a/xbmc/video/tags/VideoInfoTagLoaderFactory.h b/xbmc/video/tags/VideoInfoTagLoaderFactory.h
new file mode 100644
index 0000000..9b8861f
--- /dev/null
+++ b/xbmc/video/tags/VideoInfoTagLoaderFactory.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017-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 "IVideoInfoTagLoader.h"
+#include "addons/Scraper.h"
+
+class CFileItem; // forward
+
+namespace VIDEO
+{
+ class CVideoInfoTagLoaderFactory
+ {
+ public:
+ //! \brief Returns a tag loader for the given item.
+ //! \param item The item to find tag loader for
+ //! \param type Type of tag loader. In particular used for tvshows
+ static IVideoInfoTagLoader* CreateLoader(const CFileItem& item,
+ const ADDON::ScraperPtr& info,
+ bool lookInFolder,
+ bool forceRefresh = false);
+
+ protected:
+ // No instancing of this class
+ CVideoInfoTagLoaderFactory(void) = delete;
+ virtual ~CVideoInfoTagLoaderFactory() = delete;
+ };
+}
diff --git a/xbmc/video/tags/VideoTagLoaderFFmpeg.cpp b/xbmc/video/tags/VideoTagLoaderFFmpeg.cpp
new file mode 100644
index 0000000..c1b0495
--- /dev/null
+++ b/xbmc/video/tags/VideoTagLoaderFFmpeg.cpp
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2017-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 "VideoTagLoaderFFmpeg.h"
+
+#include "FileItem.h"
+#include "NfoFile.h"
+#include "addons/Scraper.h"
+#include "cores/FFmpeg.h"
+#include "filesystem/File.h"
+#include "filesystem/StackDirectory.h"
+#include "utils/StringUtils.h"
+#include "video/VideoInfoTag.h"
+
+using namespace XFILE;
+
+static int vfs_file_read(void *h, uint8_t* buf, int size)
+{
+ CFile* pFile = static_cast<CFile*>(h);
+ return pFile->Read(buf, size);
+}
+
+static int64_t vfs_file_seek(void *h, int64_t pos, int whence)
+{
+ CFile* pFile = static_cast<CFile*>(h);
+ if (whence == AVSEEK_SIZE)
+ return pFile->GetLength();
+ else
+ return pFile->Seek(pos, whence & ~AVSEEK_FORCE);
+}
+
+CVideoTagLoaderFFmpeg::CVideoTagLoaderFFmpeg(const CFileItem& item,
+ const ADDON::ScraperPtr& info,
+ bool lookInFolder)
+ : IVideoInfoTagLoader(item, info, lookInFolder)
+ , m_info(info)
+{
+ std::string filename =
+ item.IsStack() ? CStackDirectory::GetFirstStackedFile(item.GetPath()) : item.GetPath();
+
+ m_file = new CFile;
+
+ if (!m_file->Open(filename))
+ {
+ delete m_file;
+ m_file = nullptr;
+ return;
+ }
+
+ int blockSize = m_file->GetChunkSize();
+ int bufferSize = blockSize > 1 ? blockSize : 4096;
+ uint8_t* buffer = (uint8_t*)av_malloc(bufferSize);
+ m_ioctx = avio_alloc_context(buffer, bufferSize, 0,
+ m_file, vfs_file_read, nullptr,
+ vfs_file_seek);
+
+ m_fctx = avformat_alloc_context();
+ m_fctx->pb = m_ioctx;
+
+ if (m_file->IoControl(IOCTRL_SEEK_POSSIBLE, nullptr) != 1)
+ m_ioctx->seekable = 0;
+
+ AVInputFormat* iformat = nullptr;
+ av_probe_input_buffer(m_ioctx, &iformat, m_item.GetPath().c_str(), nullptr, 0, 0);
+ if (avformat_open_input(&m_fctx, m_item.GetPath().c_str(), iformat, nullptr) < 0)
+ {
+ delete m_file;
+ m_file = nullptr;
+ }
+}
+
+CVideoTagLoaderFFmpeg::~CVideoTagLoaderFFmpeg()
+{
+ if (m_fctx)
+ avformat_close_input(&m_fctx);
+ if (m_ioctx)
+ {
+ av_free(m_ioctx->buffer);
+ av_free(m_ioctx);
+ }
+ delete m_file;
+}
+
+bool CVideoTagLoaderFFmpeg::HasInfo() const
+{
+ if (!m_file)
+ return false;
+
+ for (size_t i = 0; i < m_fctx->nb_streams; ++i)
+ {
+ AVDictionaryEntry* avtag;
+ avtag = av_dict_get(m_fctx->streams[i]->metadata, "filename", nullptr, AV_DICT_IGNORE_SUFFIX);
+ if (avtag && strcmp(avtag->value,"kodi-metadata") == 0)
+ {
+ m_metadata_stream = i;
+ return true;
+ }
+ else if (avtag && strcmp(avtag->value,"kodi-override-metadata") == 0)
+ {
+ m_metadata_stream = i;
+ m_override_data = true;
+ return true;
+ }
+ }
+
+ AVDictionaryEntry* avtag = nullptr;
+ if (m_item.IsType(".mkv"))
+ {
+ avtag = av_dict_get(m_fctx->metadata, "IMDBURL", nullptr, AV_DICT_IGNORE_SUFFIX);
+ if (!avtag)
+ avtag = av_dict_get(m_fctx->metadata, "TMDBURL", nullptr, AV_DICT_IGNORE_SUFFIX);
+ if (!avtag)
+ avtag = av_dict_get(m_fctx->metadata, "TITLE", nullptr, AV_DICT_IGNORE_SUFFIX);
+ } else if (m_item.IsType(".mp4") || m_item.IsType(".avi"))
+ avtag = av_dict_get(m_fctx->metadata, "title", nullptr, AV_DICT_IGNORE_SUFFIX);
+
+ return avtag != nullptr;
+}
+
+CInfoScanner::INFO_TYPE CVideoTagLoaderFFmpeg::Load(CVideoInfoTag& tag,
+ bool, std::vector<EmbeddedArt>* art)
+{
+ if (m_item.IsType(".mkv"))
+ return LoadMKV(tag, art);
+ else if (m_item.IsType(".mp4"))
+ return LoadMP4(tag, art);
+ else if (m_item.IsType(".avi"))
+ return LoadAVI(tag, art);
+ else
+ return CInfoScanner::NO_NFO;
+
+}
+
+CInfoScanner::INFO_TYPE CVideoTagLoaderFFmpeg::LoadMKV(CVideoInfoTag& tag,
+ std::vector<EmbeddedArt>* art)
+{
+ // embedded art
+ for (size_t i = 0; i < m_fctx->nb_streams; ++i)
+ {
+ if ((m_fctx->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) == 0)
+ continue;
+ AVDictionaryEntry* avtag;
+ avtag = av_dict_get(m_fctx->streams[i]->metadata, "filename", nullptr, AV_DICT_IGNORE_SUFFIX);
+ std::string value;
+ if (avtag)
+ value = avtag->value;
+ avtag = av_dict_get(m_fctx->streams[i]->metadata, "mimetype", nullptr, AV_DICT_IGNORE_SUFFIX);
+ if (!value.empty() && avtag)
+ {
+ std::string type;
+ if (value == "fanart.png" || value == "fanart.jpg")
+ type = "fanart";
+ else if (value == "cover.png" || value == "cover.jpg")
+ type = "poster";
+ else if (value == "small_cover.png" || value == "small_cover.jpg")
+ type = "thumb";
+ if (type.empty())
+ continue;
+ size_t size = m_fctx->streams[i]->attached_pic.size;
+ if (art)
+ art->emplace_back(EmbeddedArt(m_fctx->streams[i]->attached_pic.data,
+ size, avtag->value, type));
+ else
+ tag.m_coverArt.emplace_back(EmbeddedArtInfo(size, avtag->value, type));
+ }
+ }
+
+ if (m_metadata_stream != -1)
+ {
+ CNfoFile nfo;
+ auto* data = m_fctx->streams[m_metadata_stream]->codecpar->extradata;
+ const char* content = reinterpret_cast<const char*>(data);
+ if (!m_override_data)
+ {
+ nfo.GetDetails(tag, content);
+ return CInfoScanner::FULL_NFO;
+ }
+ else
+ {
+ nfo.Create(content, m_info);
+ m_url = nfo.ScraperUrl();
+ return CInfoScanner::URL_NFO;
+ }
+ }
+
+ AVDictionaryEntry* avtag = nullptr;
+ bool hastag = false;
+ while ((avtag = av_dict_get(m_fctx->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX)))
+ {
+ if (StringUtils::CompareNoCase(avtag->key, "imdburl") == 0 ||
+ StringUtils::CompareNoCase(avtag->key, "tmdburl") == 0)
+ {
+ CNfoFile nfo;
+ nfo.Create(avtag->value, m_info);
+ m_url = nfo.ScraperUrl();
+ return CInfoScanner::URL_NFO;
+ }
+ else if (StringUtils::CompareNoCase(avtag->key, "title") == 0)
+ tag.SetTitle(avtag->value);
+ else if (StringUtils::CompareNoCase(avtag->key, "director") == 0)
+ {
+ std::vector<std::string> dirs = StringUtils::Split(avtag->value, " / ");
+ tag.SetDirector(dirs);
+ }
+ else if (StringUtils::CompareNoCase(avtag->key, "date_released") == 0)
+ tag.SetYear(atoi(avtag->value));
+ hastag = true;
+ }
+
+ return hastag ? CInfoScanner::TITLE_NFO : CInfoScanner::NO_NFO;
+}
+
+// https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
+CInfoScanner::INFO_TYPE CVideoTagLoaderFFmpeg::LoadMP4(CVideoInfoTag& tag,
+ std::vector<EmbeddedArt>* art)
+{
+ bool hasfull = false;
+ AVDictionaryEntry* avtag = nullptr;
+ // If either description or synopsis is found, assume user wants to use the tag info only
+ while ((avtag = av_dict_get(m_fctx->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX)))
+ {
+ if (strcmp(avtag->key, "title") == 0)
+ tag.SetTitle(avtag->value);
+ else if (strcmp(avtag->key, "composer") == 0)
+ tag.SetWritingCredits(StringUtils::Split(avtag->value, " / "));
+ else if (strcmp(avtag->key, "genre") == 0)
+ tag.SetGenre(StringUtils::Split(avtag->value, " / "));
+ else if (strcmp(avtag->key,"date") == 0)
+ tag.SetYear(atoi(avtag->value));
+ else if (strcmp(avtag->key, "description") == 0)
+ {
+ tag.SetPlotOutline(avtag->value);
+ hasfull = true;
+ }
+ else if (strcmp(avtag->key, "synopsis") == 0)
+ {
+ tag.SetPlot(avtag->value);
+ hasfull = true;
+ }
+ else if (strcmp(avtag->key, "track") == 0)
+ tag.m_iTrack = std::stoi(avtag->value);
+ else if (strcmp(avtag->key, "album") == 0)
+ tag.SetAlbum(avtag->value);
+ else if (strcmp(avtag->key, "artist") == 0)
+ tag.SetArtist(StringUtils::Split(avtag->value, " / "));
+ }
+
+ for (size_t i = 0; i < m_fctx->nb_streams; ++i)
+ {
+ if ((m_fctx->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) == 0)
+ continue;
+
+ size_t size = m_fctx->streams[i]->attached_pic.size;
+ const std::string type = "poster";
+ if (art)
+ art->emplace_back(EmbeddedArt(m_fctx->streams[i]->attached_pic.data,
+ size, "image/png", type));
+ else
+ tag.m_coverArt.emplace_back(EmbeddedArtInfo(size, "image/png", type));
+ }
+
+ return hasfull ? CInfoScanner::FULL_NFO : CInfoScanner::TITLE_NFO;
+}
+
+// https://wiki.multimedia.cx/index.php/FFmpeg_Metadata#AVI
+CInfoScanner::INFO_TYPE CVideoTagLoaderFFmpeg::LoadAVI(CVideoInfoTag& tag,
+ std::vector<EmbeddedArt>* art)
+{
+ AVDictionaryEntry* avtag = nullptr;
+ while ((avtag = av_dict_get(m_fctx->metadata, "", avtag, AV_DICT_IGNORE_SUFFIX)))
+ {
+ if (strcmp(avtag->key, "title") == 0)
+ tag.SetTitle(avtag->value);
+ else if (strcmp(avtag->key,"date") == 0)
+ tag.SetYear(atoi(avtag->value));
+ }
+
+ return CInfoScanner::TITLE_NFO;
+}
diff --git a/xbmc/video/tags/VideoTagLoaderFFmpeg.h b/xbmc/video/tags/VideoTagLoaderFFmpeg.h
new file mode 100644
index 0000000..f6b50df
--- /dev/null
+++ b/xbmc/video/tags/VideoTagLoaderFFmpeg.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017-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 "IVideoInfoTagLoader.h"
+
+#include <string>
+#include <vector>
+
+struct AVFormatContext;
+struct AVIOContext;
+namespace XFILE
+{
+ class CFile;
+}
+
+//! \brief Video tag loader using FFMPEG.
+class CVideoTagLoaderFFmpeg : public VIDEO::IVideoInfoTagLoader
+{
+public:
+ //! \brief Constructor.
+ CVideoTagLoaderFFmpeg(const CFileItem& item,
+ const ADDON::ScraperPtr& info,
+ bool lookInFolder);
+
+ //! \brief Destructor.
+ ~CVideoTagLoaderFFmpeg() override;
+
+ //! \brief Returns whether or not reader has info.
+ bool HasInfo() const override;
+
+ //! \brief Load "tag" from nfo file.
+ //! \brief tag Tag to load info into
+ CInfoScanner::INFO_TYPE Load(CVideoInfoTag& tag, bool,
+ std::vector<EmbeddedArt>* art = nullptr) override;
+
+ ADDON::ScraperPtr GetScraperInfo() const { return m_info; }
+
+protected:
+ ADDON::ScraperPtr m_info; //!< Passed scraper info
+ AVIOContext* m_ioctx = nullptr; //!< IO context for file
+ AVFormatContext* m_fctx = nullptr; //!< Format context for file
+ XFILE::CFile* m_file = nullptr; //!< VFS file handle for file
+ mutable int m_metadata_stream = -1; //!< Stream holding kodi metadata (mkv)
+ mutable bool m_override_data = false; //!< Data is for overriding
+
+ //! \brief Load tags from MKV file.
+ CInfoScanner::INFO_TYPE LoadMKV(CVideoInfoTag& tag, std::vector<EmbeddedArt>* art);
+
+ //! \brief Load tags from MP4 file.
+ CInfoScanner::INFO_TYPE LoadMP4(CVideoInfoTag& tag, std::vector<EmbeddedArt>* art);
+
+ //! \brief Load tags from AVI file.
+ CInfoScanner::INFO_TYPE LoadAVI(CVideoInfoTag& tag, std::vector<EmbeddedArt>* art);
+};
+
diff --git a/xbmc/video/tags/VideoTagLoaderNFO.cpp b/xbmc/video/tags/VideoTagLoaderNFO.cpp
new file mode 100644
index 0000000..f85ce39
--- /dev/null
+++ b/xbmc/video/tags/VideoTagLoaderNFO.cpp
@@ -0,0 +1,204 @@
+/*
+ * 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 "VideoTagLoaderNFO.h"
+
+#include "FileItem.h"
+#include "NfoFile.h"
+#include "URL.h"
+#include "filesystem/Directory.h"
+#include "filesystem/StackDirectory.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoInfoTag.h"
+
+#include <utility>
+
+using namespace XFILE;
+
+CVideoTagLoaderNFO::CVideoTagLoaderNFO(const CFileItem& item,
+ ADDON::ScraperPtr info,
+ bool lookInFolder)
+ : IVideoInfoTagLoader(item, std::move(info), lookInFolder)
+{
+ if (m_info && m_info->Content() == CONTENT_TVSHOWS && m_item.m_bIsFolder)
+ m_path = URIUtils::AddFileToFolder(m_item.GetPath(), "tvshow.nfo");
+ else
+ m_path = FindNFO(m_item, lookInFolder);
+}
+
+bool CVideoTagLoaderNFO::HasInfo() const
+{
+ return !m_path.empty() && CFileUtils::Exists(m_path);
+}
+
+CInfoScanner::INFO_TYPE CVideoTagLoaderNFO::Load(CVideoInfoTag& tag,
+ bool prioritise,
+ std::vector<EmbeddedArt>*)
+{
+ CNfoFile nfoReader;
+ CInfoScanner::INFO_TYPE result = CInfoScanner::NO_NFO;
+ if (m_info && m_info->Content() == CONTENT_TVSHOWS && !m_item.m_bIsFolder)
+ result = nfoReader.Create(m_path, m_info, m_item.GetVideoInfoTag()->m_iEpisode);
+ else if (m_info)
+ result = nfoReader.Create(m_path, m_info);
+
+ if (result == CInfoScanner::FULL_NFO || result == CInfoScanner::COMBINED_NFO)
+ nfoReader.GetDetails(tag, nullptr, prioritise);
+
+ if (result == CInfoScanner::URL_NFO || result == CInfoScanner::COMBINED_NFO)
+ {
+ m_url = nfoReader.ScraperUrl();
+ m_info = nfoReader.GetScraperInfo();
+ }
+
+ std::string type;
+ switch(result)
+ {
+ case CInfoScanner::COMBINED_NFO:
+ type = "mixed";
+ break;
+ case CInfoScanner::FULL_NFO:
+ type = "full";
+ break;
+ case CInfoScanner::URL_NFO:
+ type = "URL";
+ break;
+ case CInfoScanner::NO_NFO:
+ type = "";
+ break;
+ case CInfoScanner::OVERRIDE_NFO:
+ type = "override";
+ break;
+ default:
+ type = "malformed";
+ }
+ if (result != CInfoScanner::NO_NFO)
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: Found matching {} NFO file: {}", type,
+ CURL::GetRedacted(m_path));
+ else
+ CLog::Log(LOGDEBUG, "VideoInfoScanner: No NFO file found. Using title search for '{}'",
+ CURL::GetRedacted(m_item.GetPath()));
+
+ return result;
+}
+
+std::string CVideoTagLoaderNFO::FindNFO(const CFileItem& item,
+ bool movieFolder) const
+{
+ std::string nfoFile;
+ // Find a matching .nfo file
+ if (!item.m_bIsFolder)
+ {
+ if (URIUtils::IsInRAR(item.GetPath())) // we have a rarred item - we want to check outside the rars
+ {
+ CFileItem item2(item);
+ CURL url(item.GetPath());
+ std::string strPath = URIUtils::GetDirectory(url.GetHostName());
+ item2.SetPath(URIUtils::AddFileToFolder(strPath,
+ URIUtils::GetFileName(item.GetPath())));
+ return FindNFO(item2, movieFolder);
+ }
+
+ // grab the folder path
+ std::string strPath = URIUtils::GetDirectory(item.GetPath());
+
+ if (movieFolder && !item.IsStack())
+ { // looking up by folder name - movie.nfo takes priority - but not for stacked items (handled below)
+ nfoFile = URIUtils::AddFileToFolder(strPath, "movie.nfo");
+ if (CFileUtils::Exists(nfoFile))
+ return nfoFile;
+ }
+
+ // try looking for .nfo file for a stacked item
+ if (item.IsStack())
+ {
+ // first try .nfo file matching first file in stack
+ CStackDirectory dir;
+ std::string firstFile = dir.GetFirstStackedFile(item.GetPath());
+ CFileItem item2;
+ item2.SetPath(firstFile);
+ nfoFile = FindNFO(item2, movieFolder);
+ // else try .nfo file matching stacked title
+ if (nfoFile.empty())
+ {
+ std::string stackedTitlePath = dir.GetStackedTitlePath(item.GetPath());
+ item2.SetPath(stackedTitlePath);
+ nfoFile = FindNFO(item2, movieFolder);
+ }
+ }
+ else
+ {
+ // already an .nfo file?
+ if (URIUtils::HasExtension(item.GetPath(), ".nfo"))
+ nfoFile = item.GetPath();
+ // no, create .nfo file
+ else
+ nfoFile = URIUtils::ReplaceExtension(item.GetPath(), ".nfo");
+ }
+
+ // test file existence
+ if (!nfoFile.empty() && !CFileUtils::Exists(nfoFile))
+ nfoFile.clear();
+
+ if (nfoFile.empty()) // final attempt - strip off any cd1 folders
+ {
+ URIUtils::RemoveSlashAtEnd(strPath); // need no slash for the check that follows
+ CFileItem item2;
+ if (StringUtils::EndsWithNoCase(strPath, "cd1"))
+ {
+ strPath.erase(strPath.size() - 3);
+ item2.SetPath(URIUtils::AddFileToFolder(strPath, URIUtils::GetFileName(item.GetPath())));
+ return FindNFO(item2, movieFolder);
+ }
+ }
+
+ if (nfoFile.empty() && item.IsOpticalMediaFile())
+ {
+ CFileItem parentDirectory(item.GetLocalMetadataPath(), true);
+ nfoFile = FindNFO(parentDirectory, true);
+ }
+ }
+ // folders (or stacked dvds) can take any nfo file if there's a unique one
+ if (item.m_bIsFolder || item.IsOpticalMediaFile() || (movieFolder && nfoFile.empty()))
+ {
+ // see if there is a unique nfo file in this folder, and if so, use that
+ CFileItemList items;
+ CDirectory dir;
+ std::string strPath;
+ if (item.m_bIsFolder)
+ strPath = item.GetPath();
+ else
+ strPath = URIUtils::GetDirectory(item.GetPath());
+
+ if (dir.GetDirectory(strPath, items, ".nfo", DIR_FLAG_DEFAULTS) && items.Size())
+ {
+ int numNFO = -1;
+ for (int i = 0; i < items.Size(); i++)
+ {
+ if (items[i]->IsNFO())
+ {
+ if (numNFO == -1)
+ numNFO = i;
+ else
+ {
+ numNFO = -1;
+ break;
+ }
+ }
+ }
+ if (numNFO > -1)
+ return items[numNFO]->GetPath();
+ }
+ }
+
+ return nfoFile;
+}
+
diff --git a/xbmc/video/tags/VideoTagLoaderNFO.h b/xbmc/video/tags/VideoTagLoaderNFO.h
new file mode 100644
index 0000000..e6124c8
--- /dev/null
+++ b/xbmc/video/tags/VideoTagLoaderNFO.h
@@ -0,0 +1,41 @@
+/*
+ * 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 "IVideoInfoTagLoader.h"
+
+#include <string>
+#include <vector>
+
+//! \brief Video tag loader using nfo files.
+class CVideoTagLoaderNFO : public VIDEO::IVideoInfoTagLoader
+{
+public:
+ CVideoTagLoaderNFO(const CFileItem& item,
+ ADDON::ScraperPtr info,
+ bool lookInFolder);
+
+ ~CVideoTagLoaderNFO() override = default;
+
+ //! \brief Returns whether or not read has info.
+ bool HasInfo() const override;
+
+ //! \brief Load "tag" from nfo file.
+ //! \brief tag Tag to load info into
+ CInfoScanner::INFO_TYPE Load(CVideoInfoTag& tag, bool prioritise,
+ std::vector<EmbeddedArt>* = nullptr) override;
+
+protected:
+ //! \brief Find nfo file for item
+ //! \param item The item to find NFO file for
+ //! \param movieFolder If true, look for movie.nfo
+ std::string FindNFO(const CFileItem& item, bool movieFolder) const;
+
+ std::string m_path; //!< Path to nfo file
+};
diff --git a/xbmc/video/tags/VideoTagLoaderPlugin.cpp b/xbmc/video/tags/VideoTagLoaderPlugin.cpp
new file mode 100644
index 0000000..407e0ee
--- /dev/null
+++ b/xbmc/video/tags/VideoTagLoaderPlugin.cpp
@@ -0,0 +1,66 @@
+/*
+ * 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 "VideoTagLoaderPlugin.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "filesystem/PluginDirectory.h"
+
+using namespace XFILE;
+
+CVideoTagLoaderPlugin::CVideoTagLoaderPlugin(const CFileItem& item, bool forceRefresh)
+ : IVideoInfoTagLoader(item, nullptr, false), m_force_refresh(forceRefresh)
+{
+ if (forceRefresh)
+ return;
+ // Preserve CFileItem video info and art to avoid info loss between creating VideoInfoTagLoaderFactory and calling Load()
+ if (m_item.HasVideoInfoTag())
+ m_tag.reset(new CVideoInfoTag(*m_item.GetVideoInfoTag()));
+ auto& art = item.GetArt();
+ if (!art.empty())
+ m_art.reset(new CGUIListItem::ArtMap(art));
+}
+
+bool CVideoTagLoaderPlugin::HasInfo() const
+{
+ return m_tag || m_force_refresh;
+}
+
+CInfoScanner::INFO_TYPE CVideoTagLoaderPlugin::Load(CVideoInfoTag& tag, bool, std::vector<EmbeddedArt>*)
+{
+ if (m_force_refresh)
+ {
+ // In case of force refresh call our plugin with option "kodi_action=refresh_info"
+ // Plugin must do all refreshing work at specified path and return directory containing one ListItem with video tag and art
+ // We cannot obtain all info from setResolvedUrl, because CPluginDirectory::GetPluginResult doesn't copy full art
+ CURL url(m_item.GetPath());
+ url.SetOption("kodi_action", "refresh_info");
+ CPluginDirectory plugin;
+ CFileItemList items;
+ if (!plugin.GetDirectory(url, items))
+ return CInfoScanner::ERROR_NFO;
+ if (!items.IsEmpty())
+ {
+ const CFileItemPtr &item = items[0];
+ m_art.reset(new CGUIListItem::ArtMap(item->GetArt()));
+ if (item->HasVideoInfoTag())
+ {
+ tag = *item->GetVideoInfoTag();
+ return CInfoScanner::FULL_NFO;
+ }
+ }
+ }
+ else if (m_tag)
+ {
+ // Otherwise just copy CFileItem video info to tag
+ tag = *m_tag;
+ return CInfoScanner::FULL_NFO;
+ }
+ return CInfoScanner::NO_NFO;
+}
diff --git a/xbmc/video/tags/VideoTagLoaderPlugin.h b/xbmc/video/tags/VideoTagLoaderPlugin.h
new file mode 100644
index 0000000..7da92c1
--- /dev/null
+++ b/xbmc/video/tags/VideoTagLoaderPlugin.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "IVideoInfoTagLoader.h"
+#include "video/VideoInfoTag.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+//! \brief Video tag loader from plugin source.
+class CVideoTagLoaderPlugin : public VIDEO::IVideoInfoTagLoader
+{
+public:
+ CVideoTagLoaderPlugin(const CFileItem& item, bool forceRefresh);
+
+ ~CVideoTagLoaderPlugin() override = default;
+
+ //! \brief Returns whether or not read has info.
+ bool HasInfo() const override;
+
+ //! \brief Load "tag" from plugin.
+ //! \param tag Tag to load info into
+ CInfoScanner::INFO_TYPE Load(CVideoInfoTag& tag, bool prioritise,
+ std::vector<EmbeddedArt>* = nullptr) override;
+
+ inline std::unique_ptr<std::map<std::string, std::string>>& GetArt()
+ {
+ return m_art;
+ }
+protected:
+ std::unique_ptr<CVideoInfoTag> m_tag;
+ std::unique_ptr<std::map<std::string, std::string>> m_art;
+ bool m_force_refresh;
+};
diff --git a/xbmc/video/test/CMakeLists.txt b/xbmc/video/test/CMakeLists.txt
new file mode 100644
index 0000000..e03acfb
--- /dev/null
+++ b/xbmc/video/test/CMakeLists.txt
@@ -0,0 +1,4 @@
+set(SOURCES TestStacks.cpp
+ TestVideoInfoScanner.cpp)
+
+core_add_test_library(video_test)
diff --git a/xbmc/video/test/TestStacks.cpp b/xbmc/video/test/TestStacks.cpp
new file mode 100644
index 0000000..f5d92ea
--- /dev/null
+++ b/xbmc/video/test/TestStacks.cpp
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 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 "FileItem.h"
+#include "filesystem/Directory.h"
+#include "test/TestUtils.h"
+
+#include <string>
+
+#include <gtest/gtest.h>
+
+using namespace XFILE;
+
+namespace
+{
+const std::string VIDEO_EXTENSIONS = ".mpg|.mpeg|.mp4|.mkv|.mk3d|.iso";
+}
+
+class TestStacks : public ::testing::Test
+{
+protected:
+ TestStacks() = default;
+ ~TestStacks() override = default;
+};
+
+TEST_F(TestStacks, TestMovieFilesStackFilesAB)
+{
+ const std::string movieFolder =
+ XBMC_REF_FILE_PATH("xbmc/video/test/testdata/moviestack_ab/Movie-(2001)");
+ CFileItemList items;
+ CDirectory::GetDirectory(movieFolder, items, VIDEO_EXTENSIONS, DIR_FLAG_DEFAULTS);
+ // make sure items has 2 items (the two movie parts)
+ EXPECT_EQ(items.Size(), 2);
+ // stack the items and make sure we end up with a single movie
+ items.Stack();
+ EXPECT_EQ(items.Size(), 1);
+ // check the single item in the stack is a stack://
+ EXPECT_EQ(items.Get(0)->IsStack(), true);
+}
+
+TEST_F(TestStacks, TestMovieFilesStackFilesPart)
+{
+ const std::string movieFolder =
+ XBMC_REF_FILE_PATH("xbmc/video/test/testdata/moviestack_part/Movie_(2001)");
+ CFileItemList items;
+ CDirectory::GetDirectory(movieFolder, items, VIDEO_EXTENSIONS, DIR_FLAG_DEFAULTS);
+ // make sure items has 3 items (the three movie parts)
+ EXPECT_EQ(items.Size(), 3);
+ // stack the items and make sure we end up with a single movie
+ items.Stack();
+ EXPECT_EQ(items.Size(), 1);
+ // check the single item in the stack is a stack://
+ EXPECT_EQ(items.Get(0)->IsStack(), true);
+}
+
+TEST_F(TestStacks, TestMovieFilesStackDvdIso)
+{
+ const std::string movieFolder =
+ XBMC_REF_FILE_PATH("xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)");
+ CFileItemList items;
+ CDirectory::GetDirectory(movieFolder, items, VIDEO_EXTENSIONS, DIR_FLAG_DEFAULTS);
+ // make sure items has 2 items (the two dvd isos)
+ EXPECT_EQ(items.Size(), 2);
+ // stack the items and make sure we end up with a single movie
+ items.Stack();
+ EXPECT_EQ(items.Size(), 1);
+ // check the single item in the stack is a stack://
+ EXPECT_EQ(items.Get(0)->IsStack(), true);
+}
diff --git a/xbmc/video/test/TestVideoInfoScanner.cpp b/xbmc/video/test/TestVideoInfoScanner.cpp
new file mode 100644
index 0000000..981d749
--- /dev/null
+++ b/xbmc/video/test/TestVideoInfoScanner.cpp
@@ -0,0 +1,74 @@
+/*
+ * 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 "FileItem.h"
+#include "video/VideoInfoScanner.h"
+
+#include <gtest/gtest.h>
+
+using namespace VIDEO;
+using ::testing::Test;
+using ::testing::WithParamInterface;
+using ::testing::ValuesIn;
+
+typedef struct
+{
+ const char* path;
+ int season;
+ int episode[4]; // for multi-episodes
+} TestEntry;
+
+static const TestEntry TestData[] = {
+ //season+episode
+ {"foo.S02E03.mkv", 2, {3} },
+ {"foo.203.mkv", 2, {3} },
+ //episode only
+ {"foo.Ep03.mkv", 1, {3} },
+ {"foo.Ep_03.mkv", 1, {3} },
+ {"foo.Part.III.mkv", 1, {3} },
+ {"foo.Part.3.mkv", 1, {3} },
+ {"foo.E03.mkv", 1, {3} },
+ {"foo.2009.E03.mkv", 1, {3} },
+ // multi-episode
+ {"The Legend of Korra - S01E01-02 - Welcome to Republic City & A Leaf in the Wind.mkv", 1, { 1, 2 } },
+ {"foo.S01E01E02.mkv", 1, {1,2} },
+ {"foo.S01E03E04E05.mkv", 1, {3,4,5} }
+};
+
+class TestVideoInfoScanner : public Test,
+ public WithParamInterface<TestEntry>
+{
+};
+
+TEST_P(TestVideoInfoScanner, EnumerateEpisodeItem)
+{
+ const TestEntry& entry = GetParam();
+ CVideoInfoScanner scanner;
+ CFileItem item(entry.path, false);
+ EPISODELIST expected;
+ for (int i = 0; i < 3 && entry.episode[i]; i++)
+ expected.push_back(EPISODE(entry.season, entry.episode[i], 0, false));
+
+ EPISODELIST result;
+ ASSERT_TRUE(scanner.EnumerateEpisodeItem(&item, result));
+ EXPECT_EQ(expected.size(), result.size());
+ for (size_t i = 0; i < expected.size(); i++)
+ EXPECT_EQ(expected[i], result[i]);
+}
+
+INSTANTIATE_TEST_SUITE_P(VideoInfoScanner, TestVideoInfoScanner, ValuesIn(TestData));
+
+TEST(TestVideoInfoScanner, EnumerateEpisodeItemByTitle)
+{
+ CVideoInfoScanner scanner;
+ CFileItem item("/foo.special.mp4", false);
+ EPISODELIST result;
+ ASSERT_TRUE(scanner.EnumerateEpisodeItem(&item, result));
+ ASSERT_EQ(result.size(), 1);
+ ASSERT_EQ(result[0].strTitle, "foo");
+}
diff --git a/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)A.mp4 b/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)A.mp4
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)A.mp4
diff --git a/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)B.mp4 b/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)B.mp4
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xbmc/video/test/testdata/moviestack_ab/Movie-(2001)/Movie-(2001)B.mp4
diff --git a/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd1.iso b/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd1.iso
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd1.iso
diff --git a/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd2.iso b/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd2.iso
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xbmc/video/test/testdata/moviestack_dvdiso/Movie_(2001)/Movie_(2001)_dvd2.iso
diff --git a/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part1.mkv b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part1.mkv
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part1.mkv
diff --git a/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part2.mkv b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part2.mkv
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part2.mkv
diff --git a/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part3.mkv b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part3.mkv
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xbmc/video/test/testdata/moviestack_part/Movie_(2001)/Movie_(2001)_part3.mkv
diff --git a/xbmc/video/windows/CMakeLists.txt b/xbmc/video/windows/CMakeLists.txt
new file mode 100644
index 0000000..0695663
--- /dev/null
+++ b/xbmc/video/windows/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES GUIWindowFullScreen.cpp
+ GUIWindowVideoBase.cpp
+ GUIWindowVideoNav.cpp
+ GUIWindowVideoPlaylist.cpp
+ VideoFileItemListModifier.cpp)
+
+set(HEADERS GUIWindowFullScreen.h
+ GUIWindowFullScreenDefines.h
+ GUIWindowVideoBase.h
+ GUIWindowVideoNav.h
+ GUIWindowVideoPlaylist.h
+ VideoFileItemListModifier.h)
+
+core_add_library(video_windows)
diff --git a/xbmc/video/windows/GUIWindowFullScreen.cpp b/xbmc/video/windows/GUIWindowFullScreen.cpp
new file mode 100644
index 0000000..7cebc5f
--- /dev/null
+++ b/xbmc/video/windows/GUIWindowFullScreen.cpp
@@ -0,0 +1,437 @@
+/*
+ * 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 "GUIWindowFullScreen.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "GUIWindowFullScreenDefines.h"
+#include "ServiceBroker.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/IPlayer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "video/ViewModeSettings.h"
+#include "video/dialogs/GUIDialogFullScreenInfo.h"
+#include "video/dialogs/GUIDialogSubtitleSettings.h"
+#include "windowing/WinSystem.h"
+
+#include <algorithm>
+#include <stdio.h>
+#if defined(TARGET_DARWIN)
+#include "platform/posix/PosixResourceCounter.h"
+#endif
+
+using namespace KODI::GUILIB;
+using namespace KODI::MESSAGING;
+
+#if defined(TARGET_DARWIN)
+static CPosixResourceCounter m_resourceCounter;
+#endif
+
+CGUIWindowFullScreen::CGUIWindowFullScreen()
+ : CGUIWindow(WINDOW_FULLSCREEN_VIDEO, "VideoFullScreen.xml")
+{
+ m_viewModeChanged = true;
+ m_dwShowViewModeTimeout = {};
+ m_bShowCurrentTime = false;
+ m_loadType = KEEP_IN_MEMORY;
+ // audio
+ // - language
+ // - volume
+ // - stream
+
+ // video
+ // - Create Bookmark (294)
+ // - Cycle bookmarks (295)
+ // - Clear bookmarks (296)
+ // - jump to specific time
+ // - slider
+ // - av delay
+
+ // subtitles
+ // - delay
+ // - language
+
+ m_controlStats = new GUICONTROLSTATS;
+}
+
+CGUIWindowFullScreen::~CGUIWindowFullScreen(void)
+{
+ delete m_controlStats;
+}
+
+bool CGUIWindowFullScreen::OnAction(const CAction &action)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ switch (action.GetID())
+ {
+ case ACTION_SHOW_OSD:
+ ToggleOSD();
+ return true;
+
+ case ACTION_TRIGGER_OSD:
+ TriggerOSD();
+ return true;
+
+ case ACTION_MOUSE_MOVE:
+ if (action.GetAmount(2) || action.GetAmount(3))
+ {
+ if (!appPlayer->IsInMenu())
+ {
+ TriggerOSD();
+ return true;
+ }
+ }
+ break;
+
+ case ACTION_MOUSE_LEFT_CLICK:
+ if (!appPlayer->IsInMenu())
+ {
+ TriggerOSD();
+ return true;
+ }
+ break;
+
+ case ACTION_SHOW_GUI:
+ {
+ // switch back to the menu
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return true;
+ }
+ break;
+
+ case ACTION_SHOW_OSD_TIME:
+ m_bShowCurrentTime = !m_bShowCurrentTime;
+ CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().SetShowTime(m_bShowCurrentTime);
+ return true;
+ break;
+
+ case ACTION_SHOW_INFO:
+ {
+ CGUIDialogFullScreenInfo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogFullScreenInfo>(WINDOW_DIALOG_FULLSCREEN_INFO);
+ if (pDialog)
+ {
+ CFileItem item(g_application.CurrentFileItem());
+ pDialog->Open();
+ return true;
+ }
+ break;
+ }
+
+ case ACTION_ASPECT_RATIO:
+ { // toggle the aspect ratio mode (only if the info is onscreen)
+ if (m_dwShowViewModeTimeout.time_since_epoch().count() != 0)
+ {
+ CVideoSettings vs = appPlayer->GetVideoSettings();
+ vs.m_ViewMode = CViewModeSettings::GetNextQuickCycleViewMode(vs.m_ViewMode);
+ appPlayer->SetRenderViewMode(vs.m_ViewMode, vs.m_CustomZoomAmount, vs.m_CustomPixelRatio,
+ vs.m_CustomVerticalShift, vs.m_CustomNonLinStretch);
+ }
+ else
+ m_viewModeChanged = true;
+ m_dwShowViewModeTimeout = std::chrono::steady_clock::now();
+ }
+ return true;
+ break;
+ case ACTION_SHOW_PLAYLIST:
+ {
+ CFileItem item(g_application.CurrentFileItem());
+ if (item.HasPVRChannelInfoTag())
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_DIALOG_PVR_OSD_CHANNELS);
+ else if (item.HasVideoInfoTag())
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_PLAYLIST);
+ else if (item.HasMusicInfoTag())
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST);
+ }
+ return true;
+ break;
+ case ACTION_BROWSE_SUBTITLE:
+ {
+ std::string path = CGUIDialogSubtitleSettings::BrowseForSubtitle();
+ if (!path.empty())
+ appPlayer->AddSubtitle(path);
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return CGUIWindow::OnAction(action);
+}
+
+void CGUIWindowFullScreen::ClearBackground()
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsRenderingVideoLayer())
+ CServiceBroker::GetWinSystem()->GetGfxContext().Clear(0);
+}
+
+void CGUIWindowFullScreen::OnWindowLoaded()
+{
+ CGUIWindow::OnWindowLoaded();
+ // override the clear colour - we must never clear fullscreen
+ m_clearBackground = 0;
+}
+
+bool CGUIWindowFullScreen::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ // check whether we've come back here from a window during which time we've actually
+ // stopped playing videos
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (message.GetParam1() == WINDOW_INVALID && !appPlayer->IsPlayingVideo())
+ { // why are we here if nothing is playing???
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return true;
+ }
+
+ GUIINFO::CPlayerGUIInfo& guiInfo = CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider();
+ guiInfo.SetShowInfo(false);
+ m_bShowCurrentTime = false;
+
+ // switch resolution
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetFullScreenVideo(true);
+
+ // now call the base class to load our windows
+ CGUIWindow::OnMessage(message);
+
+ m_dwShowViewModeTimeout = {};
+ m_viewModeChanged = true;
+
+
+ return true;
+ }
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ // close all active modal dialogs
+ CServiceBroker::GetGUI()->GetWindowManager().CloseInternalModalDialogs(true);
+
+ CGUIWindow::OnMessage(message);
+
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetFullScreenVideo(false);
+
+ return true;
+ }
+ case GUI_MSG_SETFOCUS:
+ case GUI_MSG_LOSTFOCUS:
+ if (message.GetSenderId() != WINDOW_FULLSCREEN_VIDEO) return true;
+ break;
+ }
+
+ return CGUIWindow::OnMessage(message);
+}
+
+EVENT_RESULT CGUIWindowFullScreen::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
+{
+ if (event.m_id == ACTION_MOUSE_RIGHT_CLICK)
+ { // no control found to absorb this click - go back to GUI
+ OnAction(CAction(ACTION_SHOW_GUI));
+ return EVENT_RESULT_HANDLED;
+ }
+ if (event.m_id == ACTION_MOUSE_WHEEL_UP)
+ {
+ return g_application.OnAction(CAction(ACTION_ANALOG_SEEK_FORWARD, 0.5f)) ? EVENT_RESULT_HANDLED : EVENT_RESULT_UNHANDLED;
+ }
+ if (event.m_id == ACTION_MOUSE_WHEEL_DOWN)
+ {
+ return g_application.OnAction(CAction(ACTION_ANALOG_SEEK_BACK, 0.5f)) ? EVENT_RESULT_HANDLED : EVENT_RESULT_UNHANDLED;
+ }
+ if (event.m_id >= ACTION_GESTURE_NOTIFY && event.m_id <= ACTION_GESTURE_END) // gestures
+ return EVENT_RESULT_UNHANDLED;
+ return EVENT_RESULT_UNHANDLED;
+}
+
+void CGUIWindowFullScreen::FrameMove()
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->HasPlayer())
+ return;
+
+ //----------------------
+ // ViewMode Information
+ //----------------------
+
+ auto now = std::chrono::steady_clock::now();
+ auto duration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - m_dwShowViewModeTimeout);
+
+ if (m_dwShowViewModeTimeout.time_since_epoch().count() != 0 && duration.count() > 2500)
+ {
+ m_dwShowViewModeTimeout = {};
+ m_viewModeChanged = true;
+ }
+
+ if (m_dwShowViewModeTimeout.time_since_epoch().count() != 0)
+ {
+ RESOLUTION_INFO res = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+
+ {
+ // get the "View Mode" string
+ const std::string& strTitle = g_localizeStrings.Get(629);
+ const auto& vs = appPlayer->GetVideoSettings();
+ int sId = CViewModeSettings::GetViewModeStringIndex(vs.m_ViewMode);
+ const std::string& strMode = g_localizeStrings.Get(sId);
+ std::string strInfo = StringUtils::Format("{} : {}", strTitle, strMode);
+ CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), LABEL_ROW1);
+ msg.SetLabel(strInfo);
+ OnMessage(msg);
+ }
+ // show sizing information
+ VideoStreamInfo info;
+ appPlayer->GetVideoStreamInfo(CURRENT_STREAM, info);
+ {
+ // Splitres scaling factor
+ float xscale = (float)res.iScreenWidth / (float)res.iWidth;
+ float yscale = (float)res.iScreenHeight / (float)res.iHeight;
+
+ std::string strSizing = StringUtils::Format(
+ g_localizeStrings.Get(245), (int)info.SrcRect.Width(), (int)info.SrcRect.Height(),
+ (int)(info.DestRect.Width() * xscale), (int)(info.DestRect.Height() * yscale),
+ CDisplaySettings::GetInstance().GetZoomAmount(),
+ info.videoAspectRatio * CDisplaySettings::GetInstance().GetPixelRatio(),
+ CDisplaySettings::GetInstance().GetPixelRatio(),
+ CDisplaySettings::GetInstance().GetVerticalShift());
+ CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), LABEL_ROW2);
+ msg.SetLabel(strSizing);
+ OnMessage(msg);
+ }
+ // show resolution information
+ {
+ std::string strStatus;
+ if (CServiceBroker::GetWinSystem()->IsFullScreen())
+ strStatus = StringUtils::Format("{} {}x{}@{:.2f}Hz - {}", g_localizeStrings.Get(13287),
+ res.iScreenWidth, res.iScreenHeight, res.fRefreshRate,
+ g_localizeStrings.Get(244));
+ else
+ strStatus =
+ StringUtils::Format("{} {}x{} - {}", g_localizeStrings.Get(13287), res.iScreenWidth,
+ res.iScreenHeight, g_localizeStrings.Get(242));
+
+ CGUIMessage msg(GUI_MSG_LABEL_SET, GetID(), LABEL_ROW3);
+ msg.SetLabel(strStatus);
+ OnMessage(msg);
+ }
+ }
+
+ if (m_viewModeChanged)
+ {
+ if (m_dwShowViewModeTimeout.time_since_epoch().count() != 0)
+ {
+ SET_CONTROL_VISIBLE(LABEL_ROW1);
+ SET_CONTROL_VISIBLE(LABEL_ROW2);
+ SET_CONTROL_VISIBLE(LABEL_ROW3);
+ SET_CONTROL_VISIBLE(BLUE_BAR);
+ }
+ else
+ {
+ SET_CONTROL_HIDDEN(LABEL_ROW1);
+ SET_CONTROL_HIDDEN(LABEL_ROW2);
+ SET_CONTROL_HIDDEN(LABEL_ROW3);
+ SET_CONTROL_HIDDEN(BLUE_BAR);
+ }
+ m_viewModeChanged = false;
+ }
+}
+
+void CGUIWindowFullScreen::Process(unsigned int currentTime, CDirtyRegionList &dirtyregion)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsRenderingGuiLayer())
+ MarkDirtyRegion();
+
+ m_controlStats->Reset();
+
+ CGUIWindow::Process(currentTime, dirtyregion);
+
+ //! @todo This isn't quite optimal - ideally we'd only be dirtying up the actual video render rect
+ //! which is probably the job of the renderer as it can more easily track resizing etc.
+ m_renderRegion.SetRect(0, 0, (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight());
+}
+
+void CGUIWindowFullScreen::Render()
+{
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(), false);
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->Render(true, 255);
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(m_coordsRes, m_needsScaling);
+ CGUIWindow::Render();
+}
+
+void CGUIWindowFullScreen::RenderEx()
+{
+ CGUIWindow::RenderEx();
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(), false);
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->Render(false, 255, false);
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(m_coordsRes, m_needsScaling);
+}
+
+void CGUIWindowFullScreen::SeekChapter(int iChapter)
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ appPlayer->SeekChapter(iChapter);
+}
+
+void CGUIWindowFullScreen::ToggleOSD()
+{
+ CGUIDialog *pOSD = GetOSD();
+ if (pOSD)
+ {
+ if (pOSD->IsDialogRunning())
+ pOSD->Close();
+ else
+ pOSD->Open();
+ }
+
+ MarkDirtyRegion();
+}
+
+void CGUIWindowFullScreen::TriggerOSD()
+{
+ CGUIDialog *pOSD = GetOSD();
+ if (pOSD && !pOSD->IsDialogRunning())
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlayingGame())
+ pOSD->SetAutoClose(3000);
+ pOSD->Open();
+ }
+}
+
+bool CGUIWindowFullScreen::HasVisibleControls()
+{
+ return m_controlStats->nCountVisible > 0;
+}
+
+CGUIDialog* CGUIWindowFullScreen::GetOSD()
+{
+ return CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_VIDEO_OSD);
+}
diff --git a/xbmc/video/windows/GUIWindowFullScreen.h b/xbmc/video/windows/GUIWindowFullScreen.h
new file mode 100644
index 0000000..de4e38d
--- /dev/null
+++ b/xbmc/video/windows/GUIWindowFullScreen.h
@@ -0,0 +1,45 @@
+/*
+ * 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 "guilib/GUIWindow.h"
+
+#include <chrono>
+
+class CGUIDialog;
+
+class CGUIWindowFullScreen : public CGUIWindow
+{
+public:
+ CGUIWindowFullScreen();
+ ~CGUIWindowFullScreen(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ void ClearBackground() override;
+ void FrameMove() override;
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregion) override;
+ void Render() override;
+ void RenderEx() override;
+ void OnWindowLoaded() override;
+ bool HasVisibleControls() override;
+
+protected:
+ EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override;
+
+private:
+ void SeekChapter(int iChapter);
+ void ToggleOSD();
+ void TriggerOSD();
+ CGUIDialog *GetOSD();
+
+ bool m_viewModeChanged;
+ std::chrono::time_point<std::chrono::steady_clock> m_dwShowViewModeTimeout;
+
+ bool m_bShowCurrentTime;
+};
diff --git a/xbmc/video/windows/GUIWindowFullScreenDefines.h b/xbmc/video/windows/GUIWindowFullScreenDefines.h
new file mode 100644
index 0000000..cf67af5
--- /dev/null
+++ b/xbmc/video/windows/GUIWindowFullScreenDefines.h
@@ -0,0 +1,14 @@
+/*
+ * Copyright (C) 2017-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
+
+#define BLUE_BAR 0
+#define LABEL_ROW1 10
+#define LABEL_ROW2 11
+#define LABEL_ROW3 12
diff --git a/xbmc/video/windows/GUIWindowVideoBase.cpp b/xbmc/video/windows/GUIWindowVideoBase.cpp
new file mode 100644
index 0000000..94164ef
--- /dev/null
+++ b/xbmc/video/windows/GUIWindowVideoBase.cpp
@@ -0,0 +1,1589 @@
+/*
+ * 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 "GUIWindowVideoBase.h"
+
+#include "Autorun.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "PartyModeManager.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/gui/GUIDialogAddonInfo.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "dialogs/GUIDialogSmartPlaylistEditor.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/Directory.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "filesystem/StackDirectory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/dialogs/GUIDialogMusicInfo.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingUtils.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/dialogs/GUIDialogContentSettings.h"
+#include "settings/lib/Setting.h"
+#include "storage/MediaManager.h"
+#include "utils/FileExtensionProvider.h"
+#include "utils/FileUtils.h"
+#include "utils/GroupUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoInfoScanner.h"
+#include "video/VideoLibraryQueue.h"
+#include "video/VideoUtils.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+#include "view/GUIViewState.h"
+
+using namespace XFILE;
+using namespace VIDEODATABASEDIRECTORY;
+using namespace VIDEO;
+using namespace ADDON;
+using namespace PVR;
+using namespace KODI::MESSAGING;
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_LABELFILES 12
+
+#define CONTROL_PLAY_DVD 6
+
+#define PROPERTY_GROUP_BY "group.by"
+#define PROPERTY_GROUP_MIXED "group.mixed"
+
+static constexpr int SETTING_AUTOPLAYNEXT_MUSICVIDEOS = 0;
+static constexpr int SETTING_AUTOPLAYNEXT_EPISODES = 2;
+static constexpr int SETTING_AUTOPLAYNEXT_MOVIES = 3;
+static constexpr int SETTING_AUTOPLAYNEXT_UNCATEGORIZED = 4;
+
+CGUIWindowVideoBase::CGUIWindowVideoBase(int id, const std::string &xmlFile)
+ : CGUIMediaWindow(id, xmlFile.c_str())
+{
+ m_thumbLoader.SetObserver(this);
+ m_stackingAvailable = true;
+ m_dlgProgress = NULL;
+}
+
+CGUIWindowVideoBase::~CGUIWindowVideoBase() = default;
+
+bool CGUIWindowVideoBase::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_SCAN_ITEM)
+ return OnContextButton(m_viewControl.GetSelectedItem(),CONTEXT_BUTTON_SCAN);
+ else if (action.GetID() == ACTION_SHOW_PLAYLIST)
+ {
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO ||
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_VIDEO).size() > 0)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_VIDEO_PLAYLIST);
+ return true;
+ }
+ }
+
+ return CGUIMediaWindow::OnAction(action);
+}
+
+bool CGUIWindowVideoBase::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+ m_database.Close();
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_database.Open();
+ m_dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ return CGUIMediaWindow::OnMessage(message);
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+#if defined(HAS_DVD_DRIVE)
+ if (iControl == CONTROL_PLAY_DVD)
+ {
+ // play movie...
+ MEDIA_DETECT::CAutorun::PlayDiscAskResume(
+ CServiceBroker::GetMediaManager().TranslateDevicePath(""));
+ }
+ else
+#endif
+ if (m_viewControl.HasControl(iControl)) // list/thumb control
+ {
+ // get selected item
+ int iItem = m_viewControl.GetSelectedItem();
+ int iAction = message.GetParam1();
+
+ // iItem is checked for validity inside these routines
+ if (iAction == ACTION_QUEUE_ITEM || iAction == ACTION_MOUSE_MIDDLE_CLICK)
+ {
+ OnQueueItem(iItem);
+ return true;
+ }
+ else if (iAction == ACTION_QUEUE_ITEM_NEXT)
+ {
+ OnQueueItem(iItem, true);
+ return true;
+ }
+ else if (iAction == ACTION_SHOW_INFO)
+ {
+ return OnItemInfo(iItem);
+ }
+ else if (iAction == ACTION_PLAYER_PLAY)
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ // if playback is paused or playback speed != 1, return
+ if (appPlayer->IsPlayingVideo())
+ {
+ if (appPlayer->IsPausedPlayback())
+ return false;
+ if (appPlayer->GetPlaySpeed() != 1)
+ return false;
+ }
+
+ // not playing video, or playback speed == 1
+ return OnResumeItem(iItem);
+ }
+ else if (iAction == ACTION_DELETE_ITEM)
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // is delete allowed?
+ if (profileManager->GetCurrentProfile().canWriteDatabases())
+ {
+ // must be at the title window
+ if (GetID() == WINDOW_VIDEO_NAV)
+ OnDeleteItem(iItem);
+
+ // or be at the video playlists location
+ else if (m_vecItems->IsPath("special://videoplaylists/"))
+ OnDeleteItem(iItem);
+ else
+ return false;
+
+ return true;
+ }
+ }
+ }
+ }
+ break;
+ case GUI_MSG_SEARCH:
+ OnSearch();
+ break;
+ }
+ return CGUIMediaWindow::OnMessage(message);
+}
+
+bool CGUIWindowVideoBase::OnItemInfo(const CFileItem& fileItem, ADDON::ScraperPtr& scraper)
+{
+ if (fileItem.IsParentFolder() || fileItem.m_bIsShareOrDrive || fileItem.IsPath("add") ||
+ (fileItem.IsPlayList() && !URIUtils::HasExtension(fileItem.GetDynPath(), ".strm")))
+ return false;
+
+ CFileItem item(fileItem);
+ bool fromDB = false;
+ if ((item.IsVideoDb() && item.HasVideoInfoTag()) ||
+ (item.HasVideoInfoTag() && item.GetVideoInfoTag()->m_iDbId != -1))
+ {
+ if (item.GetVideoInfoTag()->m_type == MediaTypeSeason)
+ { // clear out the art - we're really grabbing the info on the show here
+ item.ClearArt();
+ item.GetVideoInfoTag()->m_iDbId = item.GetVideoInfoTag()->m_iIdShow;
+ }
+ item.SetPath(item.GetVideoInfoTag()->GetPath());
+ fromDB = true;
+ }
+ else
+ {
+ if (item.m_bIsFolder && scraper && scraper->Content() != CONTENT_TVSHOWS)
+ {
+ CFileItemList items;
+ CDirectory::GetDirectory(item.GetPath(), items, CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
+ DIR_FLAG_DEFAULTS);
+
+ // Check for cases 1_dir/1_dir/.../file (e.g. by packages where have a extra folder)
+ while (items.Size() == 1 && items[0]->m_bIsFolder)
+ {
+ const std::string path = items[0]->GetPath();
+ items.Clear();
+ CDirectory::GetDirectory(path, items,
+ CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(),
+ DIR_FLAG_DEFAULTS);
+ }
+
+ items.Stack();
+
+ // check for media files
+ bool bFoundFile(false);
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr item2 = items[i];
+
+ if (item2->IsVideo() && !item2->IsPlayList() &&
+ !CUtil::ExcludeFileOrFolder(item2->GetPath(), CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_moviesExcludeFromScanRegExps))
+ {
+ item.SetPath(item2->GetPath());
+ item.m_bIsFolder = false;
+ bFoundFile = true;
+ break;
+ }
+ }
+
+ // no video file in this folder
+ if (!bFoundFile)
+ {
+ HELPERS::ShowOKDialogText(CVariant{13346}, CVariant{20349});
+ return false;
+ }
+ }
+ }
+
+ // we need to also request any thumbs be applied to the folder item
+ if (fileItem.m_bIsFolder)
+ item.SetProperty("set_folder_thumb", fileItem.GetPath());
+
+ bool modified = ShowIMDB(CFileItemPtr(new CFileItem(item)), scraper, fromDB);
+ if (modified &&
+ (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_VIDEO_NAV)) // since we can be called from the music library we need this check
+ {
+ int itemNumber = m_viewControl.GetSelectedItem();
+ Refresh();
+ m_viewControl.SetSelectedItem(itemNumber);
+ }
+ return true;
+}
+
+// ShowIMDB is called as follows:
+// 1. To lookup info on a file.
+// 2. To lookup info on a folder (which may or may not contain a file)
+// 3. To lookup info just for fun (no file or folder related)
+
+// We just need the item object for this.
+// A "blank" item object is sent for 3.
+// If a folder is sent, currently it sets strFolder and bFolder
+// this is only used for setting the folder thumb, however.
+
+// Steps should be:
+
+// 1. Check database to see if we have this information already
+// 2. Else, check for a nfoFile to get the URL
+// 3. Run a loop to check for refresh
+// 4. If no URL is present do a search to get the URL
+// 4. Once we have the URL, download the details
+// 5. Once we have the details, add to the database if necessary (case 1,2)
+// and show the information.
+// 6. Check for a refresh, and if so, go to 3.
+
+bool CGUIWindowVideoBase::ShowIMDB(CFileItemPtr item, const ScraperPtr &info2, bool fromDB)
+{
+ /*
+ CLog::Log(LOGDEBUG,"CGUIWindowVideoBase::ShowIMDB");
+ CLog::Log(LOGDEBUG," strMovie = [{}]", strMovie);
+ CLog::Log(LOGDEBUG," strFile = [{}]", strFile);
+ CLog::Log(LOGDEBUG," strFolder = [{}]", strFolder);
+ CLog::Log(LOGDEBUG," bFolder = [{}]", ((int)bFolder ? "true" : "false"));
+ */
+
+ CGUIDialogProgress* pDlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ CGUIDialogVideoInfo* pDlgInfo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogVideoInfo>(WINDOW_DIALOG_VIDEO_INFO);
+
+ const ScraperPtr& info(info2); // use this as nfo might change it..
+
+ if (!pDlgProgress) return false;
+ if (!pDlgSelect) return false;
+ if (!pDlgInfo) return false;
+
+ // 1. Check for already downloaded information, and if we have it, display our dialog
+ // Return if no Refresh is needed.
+ bool bHasInfo=false;
+
+ CVideoInfoTag movieDetails;
+ if (info)
+ {
+ m_database.Open(); // since we can be called from the music library
+
+ int dbId = item->HasVideoInfoTag() ? item->GetVideoInfoTag()->m_iDbId : -1;
+ if (info->Content() == CONTENT_MOVIES)
+ {
+ bHasInfo = m_database.GetMovieInfo(item->GetPath(), movieDetails, dbId);
+ }
+ if (info->Content() == CONTENT_TVSHOWS)
+ {
+ if (item->m_bIsFolder)
+ {
+ bHasInfo = m_database.GetTvShowInfo(item->GetPath(), movieDetails, dbId);
+ }
+ else
+ {
+ bHasInfo = m_database.GetEpisodeInfo(item->GetPath(), movieDetails, dbId);
+ if (!bHasInfo)
+ {
+ // !! WORKAROUND !!
+ // As we cannot add an episode to a non-existing tvshow entry, we have to check the parent directory
+ // to see if it`s already in our video database. If it's not yet part of the database we will exit here.
+ // (Ticket #4764)
+ //
+ // NOTE: This will fail for episodes on multipath shares, as the parent path isn't what is stored in the
+ // database. Possible solutions are to store the paths in the db separately and rely on the show
+ // stacking stuff, or to modify GetTvShowId to do support multipath:// shares
+ std::string strParentDirectory;
+ URIUtils::GetParentPath(item->GetPath(), strParentDirectory);
+ if (m_database.GetTvShowId(strParentDirectory) < 0)
+ {
+ CLog::Log(LOGERROR, "{}: could not add episode [{}]. tvshow does not exist yet..",
+ __FUNCTION__, item->GetPath());
+ return false;
+ }
+ }
+ }
+ }
+ if (info->Content() == CONTENT_MUSICVIDEOS)
+ {
+ bHasInfo = m_database.GetMusicVideoInfo(item->GetPath(), movieDetails);
+ }
+ m_database.Close();
+ }
+ else if(item->HasVideoInfoTag())
+ {
+ bHasInfo = true;
+ movieDetails = *item->GetVideoInfoTag();
+ }
+
+ bool needsRefresh = false;
+ if (bHasInfo)
+ {
+ if (!info || info->Content() == CONTENT_NONE) // disable refresh button
+ item->SetProperty("xxuniqueid", "xx" + movieDetails.GetUniqueID());
+ *item->GetVideoInfoTag() = movieDetails;
+ pDlgInfo->SetMovie(item.get());
+ pDlgInfo->Open();
+ if (pDlgInfo->HasUpdatedUserrating())
+ return true;
+ needsRefresh = pDlgInfo->NeedRefresh();
+ if (!needsRefresh)
+ return pDlgInfo->HasUpdatedThumb();
+ // check if the item in the video info dialog has changed and if so, get the new item
+ else if (pDlgInfo->GetCurrentListItem() != NULL)
+ {
+ item = pDlgInfo->GetCurrentListItem();
+
+ if (item->IsVideoDb() && item->HasVideoInfoTag())
+ item->SetPath(item->GetVideoInfoTag()->GetPath());
+ }
+ }
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ // quietly return if Internet lookups are disabled
+ if (!profileManager->GetCurrentProfile().canWriteDatabases() && !g_passwordManager.bMasterUser)
+ return false;
+
+ if (!info)
+ return false;
+
+ if (CVideoLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ HELPERS::ShowOKDialogText(CVariant{13346}, CVariant{14057});
+ return false;
+ }
+
+ bool listNeedsUpdating = false;
+ // 3. Run a loop so that if we Refresh we re-run this block
+ do
+ {
+ if (!CVideoLibraryQueue::GetInstance().RefreshItemModal(item, needsRefresh, pDlgInfo->RefreshAll()))
+ return listNeedsUpdating;
+
+ // remove directory caches and reload images
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ CGUIMessage reload(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_REFRESH_THUMBS);
+ OnMessage(reload);
+
+ pDlgInfo->SetMovie(item.get());
+ pDlgInfo->Open();
+ item->SetArt("thumb", pDlgInfo->GetThumbnail());
+ needsRefresh = pDlgInfo->NeedRefresh();
+ listNeedsUpdating = true;
+ } while (needsRefresh);
+
+ return listNeedsUpdating;
+}
+
+void CGUIWindowVideoBase::OnQueueItem(int iItem, bool first)
+{
+ // don't re-queue items from playlist window
+ if (GetID() == WINDOW_VIDEO_PLAYLIST)
+ return;
+
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return;
+
+ // add item 2 playlist
+ const auto item = m_vecItems->Get(iItem);
+
+ if (item->IsRAR() || item->IsZIP())
+ return;
+
+ VIDEO_UTILS::QueueItem(item, first ? VIDEO_UTILS::QueuePosition::POSITION_BEGIN
+ : VIDEO_UTILS::QueuePosition::POSITION_END);
+
+ // select next item
+ m_viewControl.SetSelectedItem(iItem + 1);
+}
+
+bool CGUIWindowVideoBase::OnClick(int iItem, const std::string &player)
+{
+ return CGUIMediaWindow::OnClick(iItem, player);
+}
+
+bool CGUIWindowVideoBase::OnSelect(int iItem)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return false;
+
+ CFileItemPtr item = m_vecItems->Get(iItem);
+
+ std::string path = item->GetPath();
+ if (!item->m_bIsFolder && path != "add" &&
+ !StringUtils::StartsWith(path, "newsmartplaylist://") &&
+ !StringUtils::StartsWith(path, "newplaylist://") &&
+ !StringUtils::StartsWith(path, "newtag://") &&
+ !StringUtils::StartsWith(path, "script://"))
+ return OnFileAction(iItem, CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MYVIDEOS_SELECTACTION), "");
+
+ return CGUIMediaWindow::OnSelect(iItem);
+}
+
+bool CGUIWindowVideoBase::OnFileAction(int iItem, int action, const std::string& player)
+{
+ CFileItemPtr item = m_vecItems->Get(iItem);
+ if (!item)
+ {
+ return false;
+ }
+
+ // Reset the current start offset. The actual resume
+ // option is set in the switch, based on the action passed.
+ item->SetStartOffset(0);
+
+ switch (action)
+ {
+ case SELECT_ACTION_CHOOSE:
+ {
+ CContextButtons choices;
+
+ if (item->IsVideoDb())
+ {
+ std::string itemPath(item->GetPath());
+ itemPath = item->GetVideoInfoTag()->m_strFileNameAndPath;
+ if (URIUtils::IsStack(itemPath) && CFileItem(CStackDirectory::GetFirstStackedFile(itemPath),false).IsDiscImage())
+ choices.Add(SELECT_ACTION_PLAYPART, 20324); // Play Part
+ }
+
+ std::string resumeString = GetResumeString(*item);
+ if (!resumeString.empty())
+ {
+ choices.Add(SELECT_ACTION_RESUME, resumeString);
+ choices.Add(SELECT_ACTION_PLAY, 12021); // Play from beginning
+ }
+ else
+ choices.Add(SELECT_ACTION_PLAY, 208); // Play
+
+ choices.Add(SELECT_ACTION_INFO, 22081); // Info
+ choices.Add(SELECT_ACTION_MORE, 22082); // More
+ int value = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (value < 0)
+ return true;
+
+ return OnFileAction(iItem, value, player);
+ }
+ break;
+ case SELECT_ACTION_PLAY_OR_RESUME:
+ return OnResumeItem(iItem, player);
+ case SELECT_ACTION_INFO:
+ return OnItemInfo(iItem);
+ case SELECT_ACTION_MORE:
+ OnPopupMenu(iItem);
+ return true;
+ case SELECT_ACTION_RESUME:
+ item->SetStartOffset(STARTOFFSET_RESUME);
+ if (item->m_bIsFolder)
+ {
+ PlayItem(iItem, player);
+ return true;
+ }
+ break;
+ case SELECT_ACTION_PLAYPART:
+ if (!OnPlayStackPart(iItem))
+ return false;
+ break;
+ case SELECT_ACTION_QUEUE:
+ OnQueueItem(iItem);
+ return true;
+ case SELECT_ACTION_PLAY:
+ if (item->m_bIsFolder)
+ {
+ PlayItem(iItem, player);
+ return true;
+ }
+ break;
+ default:
+ break;
+ }
+ return OnClick(iItem, player);
+}
+
+bool CGUIWindowVideoBase::OnItemInfo(int iItem)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return false;
+
+ CFileItemPtr item = m_vecItems->Get(iItem);
+
+ if (item->IsPath("add") || item->IsParentFolder() ||
+ (item->IsPlayList() && !URIUtils::HasExtension(item->GetDynPath(), ".strm")))
+ return false;
+
+ if (!m_vecItems->IsPlugin() && (item->IsPlugin() || item->IsScript()))
+ return CGUIDialogAddonInfo::ShowForItem(item);
+
+ if (item->m_bIsFolder &&
+ item->IsVideoDb() &&
+ StringUtils::StartsWith(item->GetPath(), "videodb://movies/sets/"))
+ return ShowIMDB(item, nullptr, true);
+
+ ADDON::ScraperPtr scraper;
+
+ // Match visibility test of CMusicInfo::IsVisible
+ if (item->IsVideoDb() && item->HasVideoInfoTag() &&
+ (item->HasProperty("artist_musicid") || item->HasProperty("album_musicid")))
+ {
+ CGUIDialogMusicInfo::ShowFor(item.get());
+ return true;
+ }
+ if (!m_vecItems->IsPlugin() && !m_vecItems->IsRSS() && !m_vecItems->IsLiveTV())
+ {
+ std::string strDir;
+ if (item->IsVideoDb() &&
+ item->HasVideoInfoTag() &&
+ !item->GetVideoInfoTag()->m_strPath.empty())
+ {
+ strDir = item->GetVideoInfoTag()->m_strPath;
+ }
+ else
+ strDir = URIUtils::GetDirectory(item->GetPath());
+
+ SScanSettings settings;
+ bool foundDirectly = false;
+ scraper = m_database.GetScraperForPath(strDir, settings, foundDirectly);
+
+ if (!scraper &&
+ !(m_database.HasMovieInfo(item->GetDynPath()) || m_database.HasTvShowInfo(strDir) ||
+ m_database.HasEpisodeInfo(item->GetDynPath())))
+ {
+ HELPERS::ShowOKDialogText(CVariant{20176}, // Show video information
+ CVariant{19055}); // no information available
+ return false;
+ }
+
+ if (scraper && scraper->Content() == CONTENT_TVSHOWS && foundDirectly && !settings.parent_name_root) // dont lookup on root tvshow folder
+ return true;
+ }
+
+ return OnItemInfo(*item, scraper);
+}
+
+void CGUIWindowVideoBase::OnRestartItem(int iItem, const std::string &player)
+{
+ CGUIMediaWindow::OnClick(iItem, player);
+}
+
+void CGUIWindowVideoBase::LoadVideoInfo(CFileItemList& items,
+ CVideoDatabase& database,
+ bool allowReplaceLabels)
+{
+ //! @todo this could possibly be threaded as per the music info loading,
+ //! we could also cache the info
+ if (!items.GetContent().empty() && !items.IsPlugin())
+ return; // don't load for listings that have content set and weren't created from plugins
+
+ std::string content = items.GetContent();
+ // determine content only if it isn't set
+ if (content.empty())
+ {
+ content = database.GetContentForPath(items.GetPath());
+ items.SetContent((content.empty() && !items.IsPlugin()) ? "files" : content);
+ }
+
+ /*
+ If we have a matching item in the library, so we can assign the metadata to it. In addition, we can choose
+ * whether the item is stacked down (eg in the case of folders representing a single item)
+ * whether or not we assign the library's labels to the item, or leave the item as is.
+
+ As certain users (read: certain developers) don't want either of these to occur, we compromise by stacking
+ items down only if stacking is available and enabled.
+
+ Similarly, we assign the "clean" library labels to the item only if the "Replace filenames with library titles"
+ setting is enabled.
+ */
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ const bool stackItems =
+ items.GetProperty("isstacked").asBoolean() ||
+ (StackingAvailable(items) && settings->GetBool(CSettings::SETTING_MYVIDEOS_STACKVIDEOS));
+ const bool replaceLabels =
+ allowReplaceLabels && settings->GetBool(CSettings::SETTING_MYVIDEOS_REPLACELABELS);
+
+ CFileItemList dbItems;
+ /* NOTE: In the future when GetItemsForPath returns all items regardless of whether they're "in the library"
+ we won't need the fetchedPlayCounts code, and can "simply" do this directly on absence of content. */
+ bool fetchedPlayCounts = false;
+ if (!content.empty())
+ {
+ database.GetItemsForPath(content, items.GetPath(), dbItems);
+ dbItems.SetFastLookup(true);
+ }
+
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr pItem = items[i];
+ CFileItemPtr match;
+
+ if (pItem->m_bIsFolder && !pItem->IsParentFolder())
+ {
+ // we need this for enabling the right context menu entries, like mark watched / unwatched
+ pItem->SetProperty("IsVideoFolder", true);
+ }
+
+ if (!content
+ .empty()) /* optical media will be stacked down, so it's path won't match the base path */
+ {
+ std::string pathToMatch =
+ pItem->IsOpticalMediaFile() ? pItem->GetLocalMetadataPath() : pItem->GetPath();
+ if (URIUtils::IsMultiPath(pathToMatch))
+ pathToMatch = CMultiPathDirectory::GetFirstPath(pathToMatch);
+ match = dbItems.Get(pathToMatch);
+ }
+ if (match)
+ {
+ pItem->UpdateInfo(*match, replaceLabels);
+
+ if (stackItems)
+ {
+ if (match->m_bIsFolder)
+ pItem->SetPath(match->GetVideoInfoTag()->m_strPath);
+ else
+ pItem->SetPath(match->GetVideoInfoTag()->m_strFileNameAndPath);
+ // if we switch from a file to a folder item it means we really shouldn't be sorting files and
+ // folders separately
+ if (pItem->m_bIsFolder != match->m_bIsFolder)
+ {
+ items.SetSortIgnoreFolders(true);
+ pItem->m_bIsFolder = match->m_bIsFolder;
+ }
+ }
+ }
+ else
+ {
+ /* NOTE: Currently we GetPlayCounts on our items regardless of whether content is set
+ as if content is set, GetItemsForPaths doesn't return anything not in the content tables.
+ This code can be removed once the content tables are always filled */
+ if (!pItem->m_bIsFolder && !fetchedPlayCounts)
+ {
+ database.GetPlayCounts(items.GetPath(), items);
+ fetchedPlayCounts = true;
+ }
+
+ // set the watched overlay
+ if (pItem->IsVideo())
+ pItem->SetOverlayImage(CGUIListItem::ICON_OVERLAY_UNWATCHED,
+ pItem->HasVideoInfoTag() &&
+ pItem->GetVideoInfoTag()->GetPlayCount() > 0);
+ }
+ }
+}
+
+std::string CGUIWindowVideoBase::GetResumeString(const CFileItem &item)
+{
+ const VIDEO_UTILS::ResumeInformation resumeInfo = VIDEO_UTILS::GetItemResumeInformation(item);
+ if (resumeInfo.isResumable)
+ {
+ if (resumeInfo.startOffset > 0)
+ {
+ std::string resumeString = StringUtils::Format(
+ g_localizeStrings.Get(12022),
+ StringUtils::SecondsToTimeString(
+ static_cast<long>(CUtil::ConvertMilliSecsToSecsInt(resumeInfo.startOffset)),
+ TIME_FORMAT_HH_MM_SS));
+ if (resumeInfo.partNumber > 0)
+ {
+ const std::string partString =
+ StringUtils::Format(g_localizeStrings.Get(23051), resumeInfo.partNumber);
+ resumeString += " (" + partString + ")";
+ }
+ return resumeString;
+ }
+ else
+ {
+ return g_localizeStrings.Get(13362); // Continue watching
+ }
+ }
+ return {};
+}
+
+bool CGUIWindowVideoBase::ShowResumeMenu(CFileItem &item)
+{
+ if (!item.IsLiveTV())
+ {
+ std::string resumeString = GetResumeString(item);
+ if (!resumeString.empty())
+ { // prompt user whether they wish to resume
+ CContextButtons choices;
+ choices.Add(1, resumeString);
+ choices.Add(2, 12021); // Play from beginning
+ int retVal = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (retVal < 0)
+ return false; // don't do anything
+ if (retVal == 1)
+ item.SetStartOffset(STARTOFFSET_RESUME);
+ }
+ }
+ return true;
+}
+
+bool CGUIWindowVideoBase::OnResumeItem(int iItem, const std::string &player)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size()) return true;
+ CFileItemPtr item = m_vecItems->Get(iItem);
+
+ std::string resumeString = GetResumeString(*item);
+
+ if (!resumeString.empty())
+ {
+ CContextButtons choices;
+ choices.Add(SELECT_ACTION_RESUME, resumeString);
+ choices.Add(SELECT_ACTION_PLAY, 12021); // Play from beginning
+ int value = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (value < 0)
+ return true;
+ return OnFileAction(iItem, value, player);
+ }
+
+ if (item->m_bIsFolder)
+ {
+ // resuming directories isn't fully supported yet. play all of its content.
+ PlayItem(iItem, player);
+ return true;
+ }
+
+ return OnFileAction(iItem, SELECT_ACTION_PLAY, player);
+}
+
+void CGUIWindowVideoBase::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+
+ // contextual buttons
+ if (item)
+ {
+ if (!item->IsParentFolder())
+ {
+ std::string path(item->GetPath());
+ if (item->IsVideoDb() && item->HasVideoInfoTag())
+ path = item->GetVideoInfoTag()->m_strFileNameAndPath;
+
+ if (!item->IsPath("add") && !item->IsPlugin() &&
+ !item->IsScript() && !item->IsAddonsPath() && !item->IsLiveTV())
+ {
+ if (URIUtils::IsStack(path))
+ {
+ std::vector<uint64_t> times;
+ if (m_database.GetStackTimes(path,times) || CFileItem(CStackDirectory::GetFirstStackedFile(path),false).IsDiscImage())
+ buttons.Add(CONTEXT_BUTTON_PLAY_PART, 20324);
+ }
+ }
+
+ if (!item->m_bIsFolder && !(item->IsPlayList() && !CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_playlistAsFolders))
+ {
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ // get players
+ std::vector<std::string> players;
+ if (item->IsVideoDb())
+ {
+ CFileItem item2(item->GetVideoInfoTag()->m_strFileNameAndPath, false);
+ playerCoreFactory.GetPlayers(item2, players);
+ }
+ else
+ playerCoreFactory.GetPlayers(*item, players);
+
+ if (players.size() > 1)
+ buttons.Add(CONTEXT_BUTTON_PLAY_WITH, 15213);
+ }
+ if (item->IsSmartPlayList())
+ {
+ buttons.Add(CONTEXT_BUTTON_PLAY_PARTYMODE, 15216); // Play in Partymode
+ }
+
+ // if the item isn't a folder or script, is not explicitly marked as not playable,
+ // is a member of a list rather than a single item and we're not on the last element of the list,
+ // then add either 'play from here' or 'play only this' depending on default behaviour
+ if (!(item->m_bIsFolder || item->IsScript()) &&
+ (!item->HasProperty("IsPlayable") || item->GetProperty("IsPlayable").asBoolean()) &&
+ m_vecItems->Size() > 1 && itemNumber < m_vecItems->Size() - 1)
+ {
+ int settingValue = SETTING_AUTOPLAYNEXT_UNCATEGORIZED;
+
+ if (item->IsVideoDb() && item->HasVideoInfoTag())
+ {
+ const std::string mediaType = item->GetVideoInfoTag()->m_type;
+
+ if (mediaType == MediaTypeMusicVideo)
+ settingValue = SETTING_AUTOPLAYNEXT_MUSICVIDEOS;
+ else if (mediaType == MediaTypeEpisode)
+ settingValue = SETTING_AUTOPLAYNEXT_EPISODES;
+ else if (mediaType == MediaTypeMovie)
+ settingValue = SETTING_AUTOPLAYNEXT_MOVIES;
+ }
+
+ const auto setting = std::dynamic_pointer_cast<CSettingList>(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(
+ CSettings::SETTING_VIDEOPLAYER_AUTOPLAYNEXTITEM));
+
+ if (setting && CSettingUtils::FindIntInList(setting, settingValue))
+ buttons.Add(CONTEXT_BUTTON_PLAY_ONLY_THIS, 13434);
+ else
+ buttons.Add(CONTEXT_BUTTON_PLAY_AND_QUEUE, 13412);
+ }
+ if (item->IsSmartPlayList() || m_vecItems->IsSmartPlayList())
+ buttons.Add(CONTEXT_BUTTON_EDIT_SMART_PLAYLIST, 586);
+ }
+ }
+ CGUIMediaWindow::GetContextButtons(itemNumber, buttons);
+}
+
+bool CGUIWindowVideoBase::OnPlayStackPart(int iItem)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return false;
+
+ CFileItemPtr stack = m_vecItems->Get(iItem);
+ std::string path(stack->GetPath());
+ if (stack->IsVideoDb())
+ path = stack->GetVideoInfoTag()->m_strFileNameAndPath;
+
+ if (!URIUtils::IsStack(path))
+ return false;
+
+ CFileItemList parts;
+ CDirectory::GetDirectory(path, parts, "", DIR_FLAG_DEFAULTS);
+
+ for (int i = 0; i < parts.Size(); i++)
+ parts[i]->SetLabel(StringUtils::Format(g_localizeStrings.Get(23051), i + 1));
+
+ CGUIDialogSelect* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+
+ pDialog->Reset();
+ pDialog->SetHeading(CVariant{20324});
+ pDialog->SetItems(parts);
+ pDialog->Open();
+
+ if (!pDialog->IsConfirmed())
+ return false;
+
+ int selectedFile = pDialog->GetSelectedItem();
+ if (selectedFile >= 0)
+ {
+ // ISO stack
+ if (CFileItem(CStackDirectory::GetFirstStackedFile(path),false).IsDiscImage())
+ {
+ std::string resumeString = CGUIWindowVideoBase::GetResumeString(*(parts[selectedFile].get()));
+ stack->SetStartOffset(0);
+ if (!resumeString.empty())
+ {
+ CContextButtons choices;
+ choices.Add(SELECT_ACTION_RESUME, resumeString);
+ choices.Add(SELECT_ACTION_PLAY, 12021); // Play from beginning
+ int value = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (value == SELECT_ACTION_RESUME)
+ {
+ const VIDEO_UTILS::ResumeInformation resumeInfo =
+ VIDEO_UTILS::GetItemResumeInformation(*parts[selectedFile]);
+ stack->SetStartOffset(resumeInfo.startOffset);
+ stack->m_lStartPartNumber = resumeInfo.partNumber;
+ }
+ else if (value != SELECT_ACTION_PLAY)
+ return false; // if not selected PLAY, then we changed our mind so return
+ }
+ stack->m_lStartPartNumber = selectedFile + 1;
+ }
+ // regular stack
+ else
+ {
+ if (selectedFile > 0)
+ {
+ std::vector<uint64_t> times;
+ if (m_database.GetStackTimes(path,times))
+ stack->SetStartOffset(times[selectedFile - 1]);
+ }
+ else
+ stack->SetStartOffset(0);
+ }
+
+
+ }
+
+ return true;
+}
+
+bool CGUIWindowVideoBase::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+ switch (button)
+ {
+ case CONTEXT_BUTTON_SET_CONTENT:
+ {
+ OnAssignContent(item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strPath.empty() ? item->GetVideoInfoTag()->m_strPath : item->GetPath());
+ return true;
+ }
+ case CONTEXT_BUTTON_PLAY_PART:
+ {
+ if (OnPlayStackPart(itemNumber))
+ {
+ // call CGUIMediaWindow::OnClick() as otherwise autoresume will kick in
+ CGUIMediaWindow::OnClick(itemNumber);
+ return true;
+ }
+ else
+ return false;
+ }
+ case CONTEXT_BUTTON_PLAY_WITH:
+ {
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ std::vector<std::string> players;
+ if (item->IsVideoDb())
+ {
+ CFileItem item2(*item->GetVideoInfoTag());
+ playerCoreFactory.GetPlayers(item2, players);
+ }
+ else
+ playerCoreFactory.GetPlayers(*item, players);
+
+ std:: string player = playerCoreFactory.SelectPlayerDialog(players);
+ if (!player.empty())
+ {
+ // any other select actions but play or resume, resume, play or playpart
+ // don't make any sense here since the user already decided that he'd
+ // like to play the item (just with a specific player)
+ VideoSelectAction selectAction = (VideoSelectAction)CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MYVIDEOS_SELECTACTION);
+ if (selectAction != SELECT_ACTION_PLAY_OR_RESUME &&
+ selectAction != SELECT_ACTION_RESUME &&
+ selectAction != SELECT_ACTION_PLAY &&
+ selectAction != SELECT_ACTION_PLAYPART)
+ selectAction = SELECT_ACTION_PLAY_OR_RESUME;
+ return OnFileAction(itemNumber, selectAction, player);
+ }
+ return true;
+ }
+
+ case CONTEXT_BUTTON_PLAY_PARTYMODE:
+ g_partyModeManager.Enable(PARTYMODECONTEXT_VIDEO, m_vecItems->Get(itemNumber)->GetPath());
+ return true;
+
+ case CONTEXT_BUTTON_SCAN:
+ {
+ if( !item)
+ return false;
+ ADDON::ScraperPtr info;
+ SScanSettings settings;
+ GetScraperForItem(item.get(), info, settings);
+ std::string strPath = item->GetPath();
+ if (item->IsVideoDb() && (!item->m_bIsFolder || item->GetVideoInfoTag()->m_strPath.empty()))
+ return false;
+
+ if (item->IsVideoDb())
+ strPath = item->GetVideoInfoTag()->m_strPath;
+
+ if (!info || info->Content() == CONTENT_NONE)
+ return false;
+
+ if (item->m_bIsFolder)
+ {
+ OnScan(strPath, true);
+ }
+ else
+ OnItemInfo(*item, info);
+
+ return true;
+ }
+ case CONTEXT_BUTTON_DELETE:
+ OnDeleteItem(itemNumber);
+ return true;
+ case CONTEXT_BUTTON_EDIT_SMART_PLAYLIST:
+ {
+ std::string playlist = m_vecItems->Get(itemNumber)->IsSmartPlayList() ? m_vecItems->Get(itemNumber)->GetPath() : m_vecItems->GetPath(); // save path as activatewindow will destroy our items
+ if (CGUIDialogSmartPlaylistEditor::EditPlaylist(playlist, "video"))
+ Refresh(true); // need to update
+ return true;
+ }
+ case CONTEXT_BUTTON_RENAME:
+ OnRenameItem(itemNumber);
+ return true;
+ case CONTEXT_BUTTON_PLAY_AND_QUEUE:
+ return OnPlayAndQueueMedia(item);
+ case CONTEXT_BUTTON_PLAY_ONLY_THIS:
+ return OnPlayMedia(itemNumber);
+ default:
+ break;
+ }
+ return CGUIMediaWindow::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowVideoBase::OnPlayMedia(int iItem, const std::string &player)
+{
+ if ( iItem < 0 || iItem >= m_vecItems->Size() )
+ return false;
+
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+
+ // party mode
+ if (g_partyModeManager.IsEnabled(PARTYMODECONTEXT_VIDEO))
+ {
+ PLAYLIST::CPlayList playlistTemp;
+ playlistTemp.Add(pItem);
+ g_partyModeManager.AddUserSongs(playlistTemp, true);
+ return true;
+ }
+
+ // Reset Playlistplayer, playback started now does
+ // not use the playlistplayer.
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE);
+
+ CFileItem item(*pItem);
+ if (pItem->IsVideoDb())
+ {
+ item.SetPath(pItem->GetVideoInfoTag()->m_strFileNameAndPath);
+ item.SetProperty("original_listitem_url", pItem->GetPath());
+ }
+ CLog::Log(LOGDEBUG, "{} {}", __FUNCTION__, CURL::GetRedacted(item.GetPath()));
+
+ item.SetProperty("playlist_type_hint", m_guiState->GetPlaylist());
+
+ PlayMovie(&item, player);
+
+ return true;
+}
+
+bool CGUIWindowVideoBase::OnPlayAndQueueMedia(const CFileItemPtr& item, const std::string& player)
+{
+ // Get the current playlist and make sure it is not shuffled
+ PLAYLIST::Id playlistId = m_guiState->GetPlaylist();
+ if (playlistId != PLAYLIST::TYPE_NONE &&
+ CServiceBroker::GetPlaylistPlayer().IsShuffled(playlistId))
+ {
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(playlistId, false);
+ }
+
+ CFileItemPtr movieItem(new CFileItem(*item));
+
+ // Call the base method to actually queue the items
+ // and start playing the given item
+ return CGUIMediaWindow::OnPlayAndQueueMedia(movieItem, player);
+}
+
+void CGUIWindowVideoBase::PlayMovie(const CFileItem *item, const std::string &player)
+{
+ if(m_thumbLoader.IsLoading())
+ m_thumbLoader.StopAsync();
+
+ CServiceBroker::GetPlaylistPlayer().Play(std::make_shared<CFileItem>(*item), player);
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (!appPlayer->IsPlayingVideo())
+ m_thumbLoader.Load(*m_vecItems);
+}
+
+void CGUIWindowVideoBase::OnDeleteItem(int iItem)
+{
+ if ( iItem < 0 || iItem >= m_vecItems->Size())
+ return;
+
+ OnDeleteItem(m_vecItems->Get(iItem));
+
+ Refresh(true);
+ m_viewControl.SetSelectedItem(iItem);
+}
+
+void CGUIWindowVideoBase::OnDeleteItem(const CFileItemPtr& item)
+{
+ // HACK: stacked files need to be treated as folders in order to be deleted
+ if (item->IsStack())
+ item->m_bIsFolder = true;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (profileManager->GetCurrentProfile().getLockMode() != LOCK_MODE_EVERYONE &&
+ profileManager->GetCurrentProfile().filesLocked())
+ {
+ if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return;
+ }
+
+ if ((CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_ALLOWFILEDELETION) ||
+ m_vecItems->IsPath("special://videoplaylists/")) &&
+ CUtil::SupportsWriteFileOperations(item->GetPath()))
+ {
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui && gui->ConfirmDelete(item->GetPath()))
+ CFileUtils::DeleteItem(item);
+ }
+}
+
+void CGUIWindowVideoBase::LoadPlayList(const std::string& strPlayList,
+ PLAYLIST::Id playlistId /* = PLAYLIST::TYPE_VIDEO */)
+{
+ // if partymode is active, we disable it
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Disable();
+
+ // load a playlist like .m3u, .pls
+ // first get correct factory to load playlist
+ std::unique_ptr<PLAYLIST::CPlayList> pPlayList(PLAYLIST::CPlayListFactory::Create(strPlayList));
+ if (pPlayList)
+ {
+ // load it
+ if (!pPlayList->Load(strPlayList))
+ {
+ HELPERS::ShowOKDialogText(CVariant{6}, CVariant{477});
+ return; //hmmm unable to load playlist?
+ }
+ }
+
+ if (g_application.ProcessAndStartPlaylist(strPlayList, *pPlayList, playlistId))
+ {
+ if (m_guiState)
+ m_guiState->SetPlaylistDirectory("playlistvideo://");
+ }
+}
+
+void CGUIWindowVideoBase::PlayItem(int iItem, const std::string &player)
+{
+ // restrictions should be placed in the appropriate window code
+ // only call the base code if the item passes since this clears
+ // the currently playing temp playlist
+
+ const CFileItemPtr pItem = m_vecItems->Get(iItem);
+ // if its a folder, build a temp playlist
+ if (pItem->m_bIsFolder && !pItem->IsPlugin())
+ {
+ // take a copy so we can alter the queue state
+ CFileItemPtr item(new CFileItem(*m_vecItems->Get(iItem)));
+
+ // Allow queuing of unqueueable items
+ // when we try to queue them directly
+ if (!item->CanQueue())
+ item->SetCanQueue(true);
+
+ // skip ".."
+ if (item->IsParentFolder())
+ return;
+
+ // recursively add items to list
+ CFileItemList queuedItems;
+ VIDEO_UTILS::GetItemsForPlayList(item, queuedItems);
+
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().Add(PLAYLIST::TYPE_VIDEO, queuedItems);
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().Play();
+ }
+ else if (pItem->IsPlayList())
+ {
+ // load the playlist the old way
+ LoadPlayList(pItem->GetPath(), PLAYLIST::TYPE_VIDEO);
+ }
+ else
+ {
+ // single item, play it
+ OnClick(iItem, player);
+ }
+}
+
+bool CGUIWindowVideoBase::Update(const std::string &strDirectory, bool updateFilterPath /* = true */)
+{
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ if (!CGUIMediaWindow::Update(strDirectory, updateFilterPath))
+ return false;
+
+ // might already be running from GetGroupedItems
+ if (!m_thumbLoader.IsLoading())
+ m_thumbLoader.Load(*m_vecItems);
+
+ return true;
+}
+
+bool CGUIWindowVideoBase::GetDirectory(const std::string &strDirectory, CFileItemList &items)
+{
+ bool bResult = CGUIMediaWindow::GetDirectory(strDirectory, items);
+
+ // add in the "New Playlist" item if we're in the playlists folder
+ if ((items.GetPath() == "special://videoplaylists/") && !items.Contains("newplaylist://"))
+ {
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ CFileItemPtr newPlaylist(new CFileItem(profileManager->GetUserDataItem("PartyMode-Video.xsp"),false));
+ newPlaylist->SetLabel(g_localizeStrings.Get(16035));
+ newPlaylist->SetLabelPreformatted(true);
+ newPlaylist->SetArt("icon", "DefaultPartyMode.png");
+ newPlaylist->m_bIsFolder = true;
+ items.Add(newPlaylist);
+
+/* newPlaylist.reset(new CFileItem("newplaylist://", false));
+ newPlaylist->SetLabel(g_localizeStrings.Get(525));
+ newPlaylist->SetLabelPreformatted(true);
+ items.Add(newPlaylist);
+*/
+ newPlaylist.reset(new CFileItem("newsmartplaylist://video", false));
+ newPlaylist->SetLabel(g_localizeStrings.Get(21437)); // "new smart playlist..."
+ newPlaylist->SetArt("icon", "DefaultAddSource.png");
+ newPlaylist->SetLabelPreformatted(true);
+ items.Add(newPlaylist);
+ }
+
+ m_stackingAvailable = StackingAvailable(items);
+ // we may also be in a tvshow files listing
+ // (ideally this should be removed, and our stack regexps tidied up if necessary
+ // No "normal" episodes should stack, and multi-parts should be supported)
+ ADDON::ScraperPtr info = m_database.GetScraperForPath(strDirectory);
+ if (info && info->Content() == CONTENT_TVSHOWS)
+ m_stackingAvailable = false;
+
+ if (m_stackingAvailable && !items.IsStack() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_STACKVIDEOS))
+ items.Stack();
+
+ return bResult;
+}
+
+bool CGUIWindowVideoBase::StackingAvailable(const CFileItemList &items)
+{
+ CURL url(items.GetPath());
+ return !(items.IsPlugin() || items.IsAddonsPath() ||
+ items.IsRSS() || items.IsInternetStream() ||
+ items.IsVideoDb() || url.IsProtocol("playlistvideo"));
+}
+
+void CGUIWindowVideoBase::GetGroupedItems(CFileItemList &items)
+{
+ CGUIMediaWindow::GetGroupedItems(items);
+
+ std::string group;
+ bool mixed = false;
+ if (items.HasProperty(PROPERTY_GROUP_BY))
+ group = items.GetProperty(PROPERTY_GROUP_BY).asString();
+ if (items.HasProperty(PROPERTY_GROUP_MIXED))
+ mixed = items.GetProperty(PROPERTY_GROUP_MIXED).asBoolean();
+
+ // group == "none" completely suppresses any grouping
+ if (!StringUtils::EqualsNoCase(group, "none"))
+ {
+ CQueryParams params;
+ CVideoDatabaseDirectory dir;
+ dir.GetQueryParams(items.GetPath(), params);
+ VIDEODATABASEDIRECTORY::NODE_TYPE nodeType = CVideoDatabaseDirectory::GetDirectoryChildType(m_strFilterPath);
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ if (items.GetContent() == "movies" && params.GetSetId() <= 0 &&
+ nodeType == NODE_TYPE_TITLE_MOVIES &&
+ (settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_GROUPMOVIESETS) || (StringUtils::EqualsNoCase(group, "sets") && mixed)))
+ {
+ CFileItemList groupedItems;
+ GroupAttribute groupAttributes = settings->GetBool(CSettings::SETTING_VIDEOLIBRARY_GROUPSINGLEITEMSETS) ? GroupAttributeNone : GroupAttributeIgnoreSingleItems;
+ if (GroupUtils::GroupAndMix(GroupBySet, m_strFilterPath, items, groupedItems, groupAttributes))
+ {
+ items.ClearItems();
+ items.Append(groupedItems);
+ }
+ }
+ }
+
+ // reload thumbs after filtering and grouping
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ m_thumbLoader.Load(items);
+}
+
+bool CGUIWindowVideoBase::CheckFilterAdvanced(CFileItemList &items) const
+{
+ const std::string& content = items.GetContent();
+ if ((items.IsVideoDb() || CanContainFilter(m_strFilterPath)) &&
+ (StringUtils::EqualsNoCase(content, "movies") ||
+ StringUtils::EqualsNoCase(content, "tvshows") ||
+ StringUtils::EqualsNoCase(content, "episodes") ||
+ StringUtils::EqualsNoCase(content, "musicvideos")))
+ return true;
+
+ return false;
+}
+
+bool CGUIWindowVideoBase::CanContainFilter(const std::string &strDirectory) const
+{
+ return URIUtils::IsProtocol(strDirectory, "videodb://");
+}
+
+/// \brief Search the current directory for a string got from the virtual keyboard
+void CGUIWindowVideoBase::OnSearch()
+{
+ std::string strSearch;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strSearch, CVariant{g_localizeStrings.Get(16017)}, false))
+ return ;
+
+ StringUtils::ToLower(strSearch);
+ if (m_dlgProgress)
+ {
+ m_dlgProgress->SetHeading(CVariant{194});
+ m_dlgProgress->SetLine(0, CVariant{strSearch});
+ m_dlgProgress->SetLine(1, CVariant{""});
+ m_dlgProgress->SetLine(2, CVariant{""});
+ m_dlgProgress->Open();
+ m_dlgProgress->Progress();
+ }
+ CFileItemList items;
+ DoSearch(strSearch, items);
+
+ if (m_dlgProgress)
+ m_dlgProgress->Close();
+
+ if (items.Size())
+ {
+ CGUIDialogSelect* pDlgSelect = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ pDlgSelect->Reset();
+ pDlgSelect->SetHeading(CVariant{283});
+
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr pItem = items[i];
+ pDlgSelect->Add(pItem->GetLabel());
+ }
+
+ pDlgSelect->Open();
+
+ int iItem = pDlgSelect->GetSelectedItem();
+ if (iItem < 0)
+ return;
+
+ OnSearchItemFound(items[iItem].get());
+ }
+ else
+ {
+ HELPERS::ShowOKDialogText(CVariant{194}, CVariant{284});
+ }
+}
+
+/// \brief React on the selected search item
+/// \param pItem Search result item
+void CGUIWindowVideoBase::OnSearchItemFound(const CFileItem* pSelItem)
+{
+ if (pSelItem->m_bIsFolder)
+ {
+ std::string strPath = pSelItem->GetPath();
+ std::string strParentPath;
+ URIUtils::GetParentPath(strPath, strParentPath);
+
+ Update(strParentPath);
+
+ if (pSelItem->IsVideoDb() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN))
+ SetHistoryForPath("");
+ else
+ SetHistoryForPath(strParentPath);
+
+ strPath = pSelItem->GetPath();
+ CURL url(strPath);
+ if (pSelItem->IsSmb() && !URIUtils::HasSlashAtEnd(strPath))
+ strPath += "/";
+
+ for (int i = 0; i < m_vecItems->Size(); i++)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+ if (pItem->GetPath() == strPath)
+ {
+ m_viewControl.SetSelectedItem(i);
+ break;
+ }
+ }
+ }
+ else
+ {
+ std::string strPath = URIUtils::GetDirectory(pSelItem->GetPath());
+
+ Update(strPath);
+
+ if (pSelItem->IsVideoDb() && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN))
+ SetHistoryForPath("");
+ else
+ SetHistoryForPath(strPath);
+
+ for (int i = 0; i < m_vecItems->Size(); i++)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+ CURL url(pItem->GetPath());
+ if (pSelItem->IsVideoDb())
+ url.SetOptions("");
+ if (url.Get() == pSelItem->GetPath())
+ {
+ m_viewControl.SetSelectedItem(i);
+ break;
+ }
+ }
+ }
+ m_viewControl.SetFocused();
+}
+
+int CGUIWindowVideoBase::GetScraperForItem(CFileItem *item, ADDON::ScraperPtr &info, SScanSettings& settings)
+{
+ if (!item)
+ return 0;
+
+ if (m_vecItems->IsPlugin() || m_vecItems->IsRSS())
+ {
+ info.reset();
+ return 0;
+ }
+ else if(m_vecItems->IsLiveTV())
+ {
+ info.reset();
+ return 0;
+ }
+
+ bool foundDirectly = false;
+ info = m_database.GetScraperForPath(item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strPath.empty() ? item->GetVideoInfoTag()->m_strPath : item->GetPath(), settings, foundDirectly);
+ return foundDirectly ? 1 : 0;
+}
+
+void CGUIWindowVideoBase::OnScan(const std::string& strPath, bool scanAll)
+{
+ CVideoLibraryQueue::GetInstance().ScanLibrary(strPath, scanAll, true);
+}
+
+std::string CGUIWindowVideoBase::GetStartFolder(const std::string &dir)
+{
+ std::string lower(dir); StringUtils::ToLower(lower);
+ if (lower == "$playlists" || lower == "playlists")
+ return "special://videoplaylists/";
+ else if (lower == "plugins" || lower == "addons")
+ return "addons://sources/video/";
+ return CGUIMediaWindow::GetStartFolder(dir);
+}
+
+void CGUIWindowVideoBase::AppendAndClearSearchItems(CFileItemList &searchItems, const std::string &prependLabel, CFileItemList &results)
+{
+ if (!searchItems.Size())
+ return;
+
+ searchItems.Sort(SortByLabel, SortOrderAscending, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone);
+ for (int i = 0; i < searchItems.Size(); i++)
+ searchItems[i]->SetLabel(prependLabel + searchItems[i]->GetLabel());
+ results.Append(searchItems);
+
+ searchItems.Clear();
+}
+
+bool CGUIWindowVideoBase::OnUnAssignContent(const std::string &path, int header, int text)
+{
+ bool bCanceled;
+ CVideoDatabase db;
+ db.Open();
+ if (CGUIDialogYesNo::ShowAndGetInput(CVariant{header}, CVariant{text}, bCanceled, CVariant{ "" }, CVariant{ "" }, CGUIDialogYesNo::NO_TIMEOUT))
+ {
+ CGUIDialogProgress *progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ db.RemoveContentForPath(path, progress);
+ db.Close();
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+ return true;
+ }
+ else
+ {
+ if (!bCanceled)
+ {
+ ADDON::ScraperPtr info;
+ SScanSettings settings;
+ settings.exclude = true;
+ db.SetScraperForPath(path,info,settings);
+ }
+ }
+ db.Close();
+
+ return false;
+}
+
+void CGUIWindowVideoBase::OnAssignContent(const std::string &path)
+{
+ bool bScan=false;
+ CVideoDatabase db;
+ db.Open();
+
+ SScanSettings settings;
+ ADDON::ScraperPtr info = db.GetScraperForPath(path, settings);
+
+ ADDON::ScraperPtr info2(info);
+
+ if (CGUIDialogContentSettings::Show(info, settings))
+ {
+ if(settings.exclude || (!info && info2))
+ {
+ OnUnAssignContent(path, 20375, 20340);
+ }
+ else if (info != info2)
+ {
+ if (OnUnAssignContent(path, 20442, 20443))
+ bScan = true;
+ }
+ db.SetScraperForPath(path, info, settings);
+ }
+
+ if (bScan)
+ {
+ CVideoLibraryQueue::GetInstance().ScanLibrary(path, true, true);
+ }
+}
diff --git a/xbmc/video/windows/GUIWindowVideoBase.h b/xbmc/video/windows/GUIWindowVideoBase.h
new file mode 100644
index 0000000..0c5acf4
--- /dev/null
+++ b/xbmc/video/windows/GUIWindowVideoBase.h
@@ -0,0 +1,148 @@
+/*
+ * 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 "playlists/PlayListTypes.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoThumbLoader.h"
+#include "windows/GUIMediaWindow.h"
+
+enum VideoSelectAction
+{
+ SELECT_ACTION_CHOOSE = 0,
+ SELECT_ACTION_PLAY_OR_RESUME,
+ SELECT_ACTION_RESUME,
+ SELECT_ACTION_INFO,
+ SELECT_ACTION_MORE,
+ SELECT_ACTION_PLAY,
+ SELECT_ACTION_PLAYPART,
+ SELECT_ACTION_QUEUE
+};
+
+class CGUIWindowVideoBase : public CGUIMediaWindow, public IBackgroundLoaderObserver
+{
+public:
+ CGUIWindowVideoBase(int id, const std::string &xmlFile);
+ ~CGUIWindowVideoBase(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+
+ void PlayMovie(const CFileItem* item, const std::string& player = "");
+
+ /*! \brief Gets called to process the "info" action for the given file item
+ Default implementation shows a dialog containing information for the movie/episode/...
+ represented by the file item.
+ \param fileItem the item for which information is to be presented.
+ \param scraper a scraper addon instance that can be used to obtain additional information for
+ the given item
+ \return true if information was presented, false otherwise.
+ */
+ virtual bool OnItemInfo(const CFileItem& fileItem, ADDON::ScraperPtr& scraper);
+
+ /*! \brief Show the resume menu for this item (if it has a resume bookmark)
+ If a resume bookmark is found, we set the item's m_lStartOffset to STARTOFFSET_RESUME.
+ Note that we do this in favour of setting the resume point, as we need additional
+ information from the database (in particular, the playerState) when resuming some items
+ (eg ISO/VIDEO_TS).
+ \param item item to check for a resume bookmark
+ \return true if an option was chosen, false if the resume menu was cancelled.
+ */
+ static bool ShowResumeMenu(CFileItem &item);
+
+ /*! \brief Append a set of search items to a results list using a specific prepend label
+ Sorts the search items first, then appends with the given prependLabel to the results list.
+ Then empty the search item list so it can be refilled.
+ \param searchItems The search items to append.
+ \param prependLabel the label that should be prepended to all search results.
+ \param results the fileitemlist to append the search results to.
+ \sa DoSearch
+ */
+ static void AppendAndClearSearchItems(CFileItemList &searchItems, const std::string &prependLabel, CFileItemList &results);
+
+ /*! \brief Prompt the user for assigning content to a path.
+ Based on changes, we then call OnUnassignContent, update or refresh scraper information in the database
+ and optionally start a scan
+ \param path the path to assign content for
+ */
+ static void OnAssignContent(const std::string &path);
+
+ /*! \brief checks the database for a resume position and puts together a string
+ \param item selected item
+ \return string containing the resume position or an empty string if there is no resume position
+ */
+ static std::string GetResumeString(const CFileItem &item);
+
+ /*! \brief Load video information from the database for these items (public static version)
+ Useful for grabbing information for file listings, from watched status to full metadata
+ \param items the items to load information for.
+ \param database open database object to retrieve the data from
+ \param allowReplaceLabels allow label replacement if according GUI setting is enabled
+ */
+ static void LoadVideoInfo(CFileItemList& items,
+ CVideoDatabase& database,
+ bool allowReplaceLabels = true);
+
+protected:
+ void OnScan(const std::string& strPath, bool scanAll = false);
+ bool Update(const std::string &strDirectory, bool updateFilterPath = true) override;
+ bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override;
+ void OnItemLoaded(CFileItem* pItem) override {};
+ void GetGroupedItems(CFileItemList &items) override;
+
+ bool CheckFilterAdvanced(CFileItemList &items) const override;
+ bool CanContainFilter(const std::string &strDirectory) const override;
+
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ virtual void OnQueueItem(int iItem, bool first = false);
+ virtual void OnDeleteItem(const CFileItemPtr& pItem);
+ void OnDeleteItem(int iItem) override;
+ virtual void DoSearch(const std::string& strSearch, CFileItemList& items) {}
+ std::string GetStartFolder(const std::string &dir) override;
+
+ bool OnClick(int iItem, const std::string &player = "") override;
+ bool OnSelect(int iItem) override;
+ /*! \brief react to an Info action on a view item
+ \param item the selected item
+ \return true if the action is performed, false otherwise
+ */
+ bool OnItemInfo(int item);
+ /*! \brief perform a given action on a file
+ \param item the selected item
+ \param action the action to perform
+ \return true if the action is performed, false otherwise
+ */
+ bool OnFileAction(int item, int action, const std::string& player);
+
+ void OnRestartItem(int iItem, const std::string &player = "");
+ bool OnResumeItem(int iItem, const std::string &player = "");
+ void PlayItem(int iItem, const std::string &player = "");
+ bool OnPlayMedia(int iItem, const std::string &player = "") override;
+ bool OnPlayAndQueueMedia(const CFileItemPtr& item, const std::string& player = "") override;
+ using CGUIMediaWindow::LoadPlayList;
+ void LoadPlayList(const std::string& strPlayList, PLAYLIST::Id playlistId = PLAYLIST::TYPE_VIDEO);
+
+ bool ShowIMDB(CFileItemPtr item, const ADDON::ScraperPtr& content, bool fromDB);
+
+ void OnSearch();
+ void OnSearchItemFound(const CFileItem* pSelItem);
+ int GetScraperForItem(CFileItem *item, ADDON::ScraperPtr &info, VIDEO::SScanSettings& settings);
+
+ static bool OnUnAssignContent(const std::string &path, int header, int text);
+
+ static bool StackingAvailable(const CFileItemList &items);
+
+ bool OnPlayStackPart(int item);
+
+ CGUIDialogProgress* m_dlgProgress;
+ CVideoDatabase m_database;
+
+ CVideoThumbLoader m_thumbLoader;
+ bool m_stackingAvailable;
+};
diff --git a/xbmc/video/windows/GUIWindowVideoNav.cpp b/xbmc/video/windows/GUIWindowVideoNav.cpp
new file mode 100644
index 0000000..d53c105
--- /dev/null
+++ b/xbmc/video/windows/GUIWindowVideoNav.cpp
@@ -0,0 +1,1171 @@
+/*
+ * Copyright (C) 2016-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 "GUIWindowVideoNav.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "PartyModeManager.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "dialogs/GUIDialogMediaSource.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "filesystem/Directory.h"
+#include "filesystem/VideoDatabaseDirectory.h"
+#include "filesystem/VideoDatabaseFile.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "music/MusicDatabase.h"
+#include "profiles/ProfileManager.h"
+#include "pvr/recordings/PVRRecording.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/MediaSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "video/VideoDbUrl.h"
+#include "video/VideoInfoScanner.h"
+#include "video/VideoLibraryQueue.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+#include "view/GUIViewState.h"
+
+#include <utility>
+
+using namespace XFILE;
+using namespace VIDEODATABASEDIRECTORY;
+using namespace KODI::MESSAGING;
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_BTNSEARCH 8
+#define CONTROL_LABELFILES 12
+
+#define CONTROL_BTN_FILTER 19
+#define CONTROL_BTNSHOWMODE 10
+#define CONTROL_BTNSHOWALL 14
+#define CONTROL_UNLOCK 11
+
+#define CONTROL_FILTER 15
+#define CONTROL_BTNPARTYMODE 16
+#define CONTROL_LABELEMPTY 18
+
+#define CONTROL_UPDATE_LIBRARY 20
+
+CGUIWindowVideoNav::CGUIWindowVideoNav(void)
+ : CGUIWindowVideoBase(WINDOW_VIDEO_NAV, "MyVideoNav.xml")
+{
+ m_thumbLoader.SetObserver(this);
+}
+
+CGUIWindowVideoNav::~CGUIWindowVideoNav(void) = default;
+
+bool CGUIWindowVideoNav::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_TOGGLE_WATCHED)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(m_viewControl.GetSelectedItem());
+ if (pItem->IsParentFolder())
+ return false;
+
+ if (pItem && pItem->HasVideoInfoTag())
+ {
+ CVideoLibraryQueue::GetInstance().MarkAsWatched(pItem, pItem->GetVideoInfoTag()->GetPlayCount() == 0);
+ return true;
+ }
+ }
+ return CGUIWindowVideoBase::OnAction(action);
+}
+
+bool CGUIWindowVideoNav::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_RESET:
+ m_vecItems->SetPath("");
+ break;
+ case GUI_MSG_WINDOW_DEINIT:
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+ break;
+ case GUI_MSG_WINDOW_INIT:
+ {
+ /* We don't want to show Autosourced items (ie removable pendrives, memorycards) in Library mode */
+ m_rootDir.AllowNonLocalSources(false);
+
+ SetProperty("flattened", CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_FLATTEN));
+ if (message.GetNumStringParams() && StringUtils::EqualsNoCase(message.GetStringParam(0), "Files") &&
+ CMediaSourceSettings::GetInstance().GetSources("video")->empty())
+ {
+ message.SetStringParam("");
+ }
+
+ if (!CGUIWindowVideoBase::OnMessage(message))
+ return false;
+
+
+ if (message.GetStringParam(0) != "")
+ {
+ CURL url(message.GetStringParam(0));
+
+ int i = 0;
+ for (; i < m_vecItems->Size(); i++)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+
+ // skip ".."
+ if (pItem->IsParentFolder())
+ continue;
+
+ if (URIUtils::PathEquals(pItem->GetPath(), message.GetStringParam(0), true, true))
+ {
+ m_viewControl.SetSelectedItem(i);
+ i = -1;
+ if (url.GetOption("showinfo") == "true")
+ {
+ ADDON::ScraperPtr scrapper;
+ OnItemInfo(*pItem, scrapper);
+ }
+ break;
+ }
+ }
+ if (i >= m_vecItems->Size())
+ {
+ SelectFirstUnwatched();
+
+ if (url.GetOption("showinfo") == "true")
+ {
+ // We are here if the item is filtered out in the nav window
+ const std::string& path = message.GetStringParam(0);
+ CFileItem item(path, URIUtils::HasSlashAtEnd(path));
+ if (item.IsVideoDb())
+ {
+ *(item.GetVideoInfoTag()) = XFILE::CVideoDatabaseFile::GetVideoTag(CURL(item.GetPath()));
+ if (!item.GetVideoInfoTag()->IsEmpty())
+ {
+ item.SetPath(item.GetVideoInfoTag()->m_strFileNameAndPath);
+ ADDON::ScraperPtr scrapper;
+ OnItemInfo(item, scrapper);
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ // This needs to be done again, because the initialization of CGUIWindow overwrites it with default values
+ // Mostly affects cases where GUIWindowVideoNav is constructed and we're already in a show, e.g. entering from the homescreen
+ SelectFirstUnwatched();
+ }
+
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTNPARTYMODE)
+ {
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Disable();
+ else
+ {
+ if (!g_partyModeManager.Enable(PARTYMODECONTEXT_VIDEO))
+ {
+ SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE,false);
+ return false;
+ }
+
+ // Playlist directory is the root of the playlist window
+ if (m_guiState)
+ m_guiState->SetPlaylistDirectory("playlistvideo://");
+
+ return true;
+ }
+ UpdateButtons();
+ }
+
+ if (iControl == CONTROL_BTNSEARCH)
+ {
+ OnSearch();
+ }
+ else if (iControl == CONTROL_BTNSHOWMODE)
+ {
+ CMediaSettings::GetInstance().CycleWatchedMode(m_vecItems->GetContent());
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ OnFilterItems(GetProperty("filter").asString());
+ UpdateButtons();
+ return true;
+ }
+ else if (iControl == CONTROL_BTNSHOWALL)
+ {
+ if (CMediaSettings::GetInstance().GetWatchedMode(m_vecItems->GetContent()) == WatchedModeAll)
+ CMediaSettings::GetInstance().SetWatchedMode(m_vecItems->GetContent(), WatchedModeUnwatched);
+ else
+ CMediaSettings::GetInstance().SetWatchedMode(m_vecItems->GetContent(), WatchedModeAll);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ OnFilterItems(GetProperty("filter").asString());
+ UpdateButtons();
+ return true;
+ }
+ else if (iControl == CONTROL_UPDATE_LIBRARY)
+ {
+ if (!CVideoLibraryQueue::GetInstance().IsScanningLibrary())
+ OnScan("");
+ else
+ CVideoLibraryQueue::GetInstance().StopLibraryScanning();
+ return true;
+ }
+ }
+ break;
+ // update the display
+ case GUI_MSG_REFRESH_THUMBS:
+ Refresh();
+ break;
+ }
+ return CGUIWindowVideoBase::OnMessage(message);
+}
+
+SelectFirstUnwatchedItem CGUIWindowVideoNav::GetSettingSelectFirstUnwatchedItem()
+{
+ if (m_vecItems->IsVideoDb())
+ {
+ NODE_TYPE nodeType = CVideoDatabaseDirectory::GetDirectoryChildType(m_vecItems->GetPath());
+
+ if (nodeType == NODE_TYPE_SEASONS || nodeType == NODE_TYPE_EPISODES)
+ {
+ int iValue = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOLIBRARY_TVSHOWSSELECTFIRSTUNWATCHEDITEM);
+ if (iValue >= SelectFirstUnwatchedItem::NEVER && iValue <= SelectFirstUnwatchedItem::ALWAYS)
+ return (SelectFirstUnwatchedItem)iValue;
+ }
+ }
+
+ return SelectFirstUnwatchedItem::NEVER;
+}
+
+IncludeAllSeasonsAndSpecials CGUIWindowVideoNav::GetSettingIncludeAllSeasonsAndSpecials()
+{
+ int iValue = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOLIBRARY_TVSHOWSINCLUDEALLSEASONSANDSPECIALS);
+ if (iValue >= IncludeAllSeasonsAndSpecials::NEITHER && iValue <= IncludeAllSeasonsAndSpecials::SPECIALS)
+ return (IncludeAllSeasonsAndSpecials)iValue;
+
+ return IncludeAllSeasonsAndSpecials::NEITHER;
+}
+
+int CGUIWindowVideoNav::GetFirstUnwatchedItemIndex(bool includeAllSeasons, bool includeSpecials)
+{
+ int iIndex = 0;
+ int iUnwatchedSeason = INT_MAX;
+ int iUnwatchedEpisode = INT_MAX;
+ NODE_TYPE nodeType = CVideoDatabaseDirectory::GetDirectoryChildType(m_vecItems->GetPath());
+
+ // Run through the list of items and find the first unwatched season/episode
+ for (int i = 0; i < m_vecItems->Size(); ++i)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+ if (pItem->IsParentFolder() || !pItem->HasVideoInfoTag())
+ continue;
+
+ CVideoInfoTag *pTag = pItem->GetVideoInfoTag();
+
+ if ((!includeAllSeasons && pTag->m_iSeason < 0) || (!includeSpecials && pTag->m_iSeason == 0))
+ continue;
+
+ // Use the special sort values if they're available
+ int iSeason = pTag->m_iSpecialSortSeason >= 0 ? pTag->m_iSpecialSortSeason : pTag->m_iSeason;
+ int iEpisode = pTag->m_iSpecialSortEpisode >= 0 ? pTag->m_iSpecialSortEpisode : pTag->m_iEpisode;
+
+ if (nodeType == NODE_TYPE::NODE_TYPE_SEASONS)
+ {
+ // Is the season unwatched, and is its season number lower than the currently identified
+ // first unwatched season
+ if (pTag->GetPlayCount() == 0 && iSeason < iUnwatchedSeason)
+ {
+ iUnwatchedSeason = iSeason;
+ iIndex = i;
+ }
+ }
+
+ if (nodeType == NODE_TYPE::NODE_TYPE_EPISODES)
+ {
+ // Is the episode unwatched, and is its season number lower
+ // or is its episode number lower within the current season
+ if (pTag->GetPlayCount() == 0 && (iSeason < iUnwatchedSeason || (iSeason == iUnwatchedSeason && iEpisode < iUnwatchedEpisode)))
+ {
+ iUnwatchedSeason = iSeason;
+ iUnwatchedEpisode = iEpisode;
+ iIndex = i;
+ }
+ }
+ }
+
+ return iIndex;
+}
+
+bool CGUIWindowVideoNav::Update(const std::string &strDirectory, bool updateFilterPath /* = true */)
+{
+ if (!CGUIWindowVideoBase::Update(strDirectory, updateFilterPath))
+ return false;
+
+ SelectFirstUnwatched();
+
+ return true;
+}
+
+void CGUIWindowVideoNav::SelectFirstUnwatched() {
+ // Check if we should select the first unwatched item
+ SelectFirstUnwatchedItem selectFirstUnwatched = GetSettingSelectFirstUnwatchedItem();
+ if (selectFirstUnwatched != SelectFirstUnwatchedItem::NEVER)
+ {
+ bool bIsItemSelected = (m_viewControl.GetSelectedItem() > 0);
+
+ if (selectFirstUnwatched == SelectFirstUnwatchedItem::ALWAYS ||
+ (selectFirstUnwatched == SelectFirstUnwatchedItem::ON_FIRST_ENTRY && !bIsItemSelected))
+ {
+ IncludeAllSeasonsAndSpecials incAllSeasonsSpecials = GetSettingIncludeAllSeasonsAndSpecials();
+
+ bool bIncludeAllSeasons = (incAllSeasonsSpecials == IncludeAllSeasonsAndSpecials::BOTH || incAllSeasonsSpecials == IncludeAllSeasonsAndSpecials::ALL_SEASONS);
+ bool bIncludeSpecials = (incAllSeasonsSpecials == IncludeAllSeasonsAndSpecials::BOTH || incAllSeasonsSpecials == IncludeAllSeasonsAndSpecials::SPECIALS);
+
+ int iIndex = GetFirstUnwatchedItemIndex(bIncludeAllSeasons, bIncludeSpecials);
+ m_viewControl.SetSelectedItem(iIndex);
+ }
+ }
+}
+
+bool CGUIWindowVideoNav::GetDirectory(const std::string &strDirectory, CFileItemList &items)
+{
+ if (m_thumbLoader.IsLoading())
+ m_thumbLoader.StopThread();
+
+ items.ClearArt();
+ items.ClearProperties();
+
+ bool bResult = CGUIWindowVideoBase::GetDirectory(strDirectory, items);
+ if (bResult)
+ {
+ if (items.IsVideoDb())
+ {
+ XFILE::CVideoDatabaseDirectory dir;
+ CQueryParams params;
+ dir.GetQueryParams(items.GetPath(),params);
+ VIDEODATABASEDIRECTORY::NODE_TYPE node = dir.GetDirectoryChildType(items.GetPath());
+
+ int iFlatten = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOLIBRARY_FLATTENTVSHOWS);
+ int itemsSize = items.GetObjectCount();
+ int firstIndex = items.Size() - itemsSize;
+
+ // perform the flattening logic for tvshows with a single (unwatched) season (+ optional special season)
+ if (node == NODE_TYPE_SEASONS && !items.IsEmpty())
+ {
+ // check if the last item is the "All seasons" item which should be ignored for flattening
+ if (!items[items.Size() - 1]->HasVideoInfoTag() || items[items.Size() - 1]->GetVideoInfoTag()->m_iSeason < 0)
+ itemsSize -= 1;
+
+ bool bFlatten = (itemsSize == 1 && iFlatten == 1) || iFlatten == 2 || // flatten if one one season or if always flatten is enabled
+ (itemsSize == 2 && iFlatten == 1 && // flatten if one season + specials
+ (items[firstIndex]->GetVideoInfoTag()->m_iSeason == 0 || items[firstIndex + 1]->GetVideoInfoTag()->m_iSeason == 0));
+
+ if (iFlatten > 0 && !bFlatten && (WatchedMode)CMediaSettings::GetInstance().GetWatchedMode("tvshows") == WatchedModeUnwatched)
+ {
+ int count = 0;
+ for(int i = 0; i < items.Size(); i++)
+ {
+ const CFileItemPtr item = items.Get(i);
+ if (item->GetProperty("unwatchedepisodes").asInteger() != 0 && item->GetVideoInfoTag()->m_iSeason > 0)
+ count++;
+ }
+ bFlatten = (count < 2); // flatten if there is only 1 unwatched season (not counting specials)
+ }
+
+ if (bFlatten)
+ { // flatten if one season or flatten always
+ items.Clear();
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(items.GetPath()))
+ return false;
+
+ videoUrl.AppendPath("-2/");
+ return GetDirectory(videoUrl.ToString(), items);
+ }
+ }
+
+ if (node == VIDEODATABASEDIRECTORY::NODE_TYPE_EPISODES ||
+ node == NODE_TYPE_SEASONS ||
+ node == NODE_TYPE_RECENTLY_ADDED_EPISODES)
+ {
+ CLog::Log(LOGDEBUG, "WindowVideoNav::GetDirectory");
+ // grab the show thumb
+ CVideoInfoTag details;
+ m_database.GetTvShowInfo("", details, params.GetTvShowId());
+ std::map<std::string, std::string> art;
+ if (m_database.GetArtForItem(details.m_iDbId, details.m_type, art))
+ {
+ items.AppendArt(art, details.m_type);
+ items.SetArtFallback("fanart", "tvshow.fanart");
+ if (node == NODE_TYPE_SEASONS)
+ { // set an art fallback for "thumb"
+ if (items.HasArt("tvshow.poster"))
+ items.SetArtFallback("thumb", "tvshow.poster");
+ else if (items.HasArt("tvshow.banner"))
+ items.SetArtFallback("thumb", "tvshow.banner");
+ }
+ }
+
+ // Grab fanart data
+ items.SetProperty("fanart_color1", details.m_fanart.GetColor(0));
+ items.SetProperty("fanart_color2", details.m_fanart.GetColor(1));
+ items.SetProperty("fanart_color3", details.m_fanart.GetColor(2));
+
+ // save the show description (showplot)
+ items.SetProperty("showplot", details.m_strPlot);
+ items.SetProperty("showtitle", details.m_strShowTitle);
+
+ // the container folder thumb is the parent (i.e. season or show)
+ if (itemsSize && (node == NODE_TYPE_EPISODES || node == NODE_TYPE_RECENTLY_ADDED_EPISODES))
+ {
+ int seasonID = -1;
+ int seasonParam = params.GetSeason();
+
+ // grab all season art when flatten always
+ if (seasonParam == -2 && iFlatten == 2)
+ seasonParam = -1;
+
+ if (seasonParam >= -1)
+ seasonID = m_database.GetSeasonId(details.m_iDbId, seasonParam);
+ else
+ seasonID = items[firstIndex]->GetVideoInfoTag()->m_iIdSeason;
+
+ CGUIListItem::ArtMap seasonArt;
+ if (seasonID > -1 && m_database.GetArtForItem(seasonID, MediaTypeSeason, seasonArt))
+ {
+ items.AppendArt(seasonArt, MediaTypeSeason);
+ // set an art fallback for "thumb"
+ if (items.HasArt("season.poster"))
+ items.SetArtFallback("thumb", "season.poster");
+ else if (items.HasArt("season.banner"))
+ items.SetArtFallback("thumb", "season.banner");
+ }
+ }
+ }
+ else if (node == NODE_TYPE_TITLE_MOVIES ||
+ node == NODE_TYPE_RECENTLY_ADDED_MOVIES)
+ {
+ if (params.GetSetId() > 0)
+ {
+ CGUIListItem::ArtMap setArt;
+ if (m_database.GetArtForItem(params.GetSetId(), MediaTypeVideoCollection, setArt))
+ {
+ items.AppendArt(setArt, MediaTypeVideoCollection);
+ items.SetArtFallback("fanart", "set.fanart");
+ if (items.HasArt("set.poster"))
+ items.SetArtFallback("thumb", "set.poster");
+ }
+ }
+ }
+ }
+ else if (URIUtils::PathEquals(items.GetPath(), "special://videoplaylists/"))
+ items.SetContent("playlists");
+ else if (!items.IsVirtualDirectoryRoot())
+ { // load info from the database
+ std::string label;
+ if (items.GetLabel().empty() && m_rootDir.IsSource(items.GetPath(), CMediaSourceSettings::GetInstance().GetSources("video"), &label))
+ items.SetLabel(label);
+ if (!items.IsSourcesPath() && !items.IsLibraryFolder())
+ LoadVideoInfo(items, m_database);
+ }
+
+ CVideoDbUrl videoUrl;
+ if (videoUrl.FromString(items.GetPath()) && items.GetContent() == "tags" &&
+ !items.Contains("newtag://" + videoUrl.GetType()))
+ {
+ CFileItemPtr newTag(new CFileItem("newtag://" + videoUrl.GetType(), false));
+ newTag->SetLabel(g_localizeStrings.Get(20462));
+ newTag->SetLabelPreformatted(true);
+ newTag->SetSpecialSort(SortSpecialOnTop);
+ items.Add(newTag);
+ }
+ }
+ return bResult;
+}
+
+void CGUIWindowVideoNav::UpdateButtons()
+{
+ CGUIWindowVideoBase::UpdateButtons();
+
+ // Update object count
+ int iItems = m_vecItems->Size();
+ if (iItems)
+ {
+ // check for parent dir and "all" items
+ // should always be the first two items
+ for (int i = 0; i <= (iItems>=2 ? 1 : 0); i++)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+ if (pItem->IsParentFolder()) iItems--;
+ if (StringUtils::StartsWith(pItem->GetPath(), "/-1/")) iItems--;
+ }
+ // or the last item
+ if (m_vecItems->Size() > 2 &&
+ StringUtils::StartsWith(m_vecItems->Get(m_vecItems->Size()-1)->GetPath(), "/-1/"))
+ iItems--;
+ }
+ std::string items = StringUtils::Format("{} {}", iItems, g_localizeStrings.Get(127));
+ SET_CONTROL_LABEL(CONTROL_LABELFILES, items);
+
+ // set the filter label
+ std::string strLabel;
+
+ // "Playlists"
+ if (m_vecItems->IsPath("special://videoplaylists/"))
+ strLabel = g_localizeStrings.Get(136);
+ // "{Playlist Name}"
+ else if (m_vecItems->IsPlayList())
+ {
+ // get playlist name from path
+ std::string strDummy;
+ URIUtils::Split(m_vecItems->GetPath(), strDummy, strLabel);
+ }
+ else if (m_vecItems->IsPath("sources://video/"))
+ strLabel = g_localizeStrings.Get(744);
+ // everything else is from a videodb:// path
+ else if (m_vecItems->IsVideoDb())
+ {
+ CVideoDatabaseDirectory dir;
+ dir.GetLabel(m_vecItems->GetPath(), strLabel);
+ }
+ else
+ strLabel = URIUtils::GetFileName(m_vecItems->GetPath());
+
+ SET_CONTROL_LABEL(CONTROL_FILTER, strLabel);
+
+ int watchMode = CMediaSettings::GetInstance().GetWatchedMode(m_vecItems->GetContent());
+ SET_CONTROL_LABEL(CONTROL_BTNSHOWMODE, g_localizeStrings.Get(16100 + watchMode));
+
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTNSHOWALL, watchMode != WatchedModeAll);
+
+ SET_CONTROL_SELECTED(GetID(),CONTROL_BTNPARTYMODE, g_partyModeManager.IsEnabled());
+
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_UPDATE_LIBRARY, !m_vecItems->IsAddonsPath() && !m_vecItems->IsPlugin() && !m_vecItems->IsScript());
+}
+
+bool CGUIWindowVideoNav::GetFilteredItems(const std::string &filter, CFileItemList &items)
+{
+ bool listchanged = CGUIMediaWindow::GetFilteredItems(filter, items);
+ listchanged |= ApplyWatchedFilter(items);
+
+ return listchanged;
+}
+
+/// \brief Search for names, genres, artists, directors, and plots with search string \e strSearch in the
+/// \brief video databases and return the found \e items
+/// \param strSearch The search string
+/// \param items Items Found
+void CGUIWindowVideoNav::DoSearch(const std::string& strSearch, CFileItemList& items)
+{
+ CFileItemList tempItems;
+ const std::string& strGenre = g_localizeStrings.Get(515); // Genre
+ const std::string& strActor = g_localizeStrings.Get(20337); // Actor
+ const std::string& strDirector = g_localizeStrings.Get(20339); // Director
+
+ //get matching names
+ m_database.GetMoviesByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20338) + "] ", items);
+
+ m_database.GetEpisodesByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20359) + "] ", items);
+
+ m_database.GetTvShowsByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20364) + "] ", items);
+
+ m_database.GetMusicVideosByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20391) + "] ", items);
+
+ m_database.GetMusicVideosByAlbum(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(558) + "] ", items);
+
+ // get matching genres
+ m_database.GetMovieGenresByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + strGenre + " - " + g_localizeStrings.Get(20342) + "] ", items);
+
+ m_database.GetTvShowGenresByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + strGenre + " - " + g_localizeStrings.Get(20343) + "] ", items);
+
+ m_database.GetMusicVideoGenresByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + strGenre + " - " + g_localizeStrings.Get(20389) + "] ", items);
+
+ //get actors/artists
+ m_database.GetMovieActorsByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + strActor + " - " + g_localizeStrings.Get(20342) + "] ", items);
+
+ m_database.GetTvShowsActorsByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + strActor + " - " + g_localizeStrings.Get(20343) + "] ", items);
+
+ m_database.GetMusicVideoArtistsByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + strActor + " - " + g_localizeStrings.Get(20389) + "] ", items);
+
+ //directors
+ m_database.GetMovieDirectorsByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + strDirector + " - " + g_localizeStrings.Get(20342) + "] ", items);
+
+ m_database.GetTvShowsDirectorsByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + strDirector + " - " + g_localizeStrings.Get(20343) + "] ", items);
+
+ m_database.GetMusicVideoDirectorsByName(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + strDirector + " - " + g_localizeStrings.Get(20389) + "] ", items);
+
+ //plot
+ m_database.GetEpisodesByPlot(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20365) + "] ", items);
+
+ m_database.GetMoviesByPlot(strSearch, tempItems);
+ AppendAndClearSearchItems(tempItems, "[" + g_localizeStrings.Get(20323) + "] ", items);
+}
+
+void CGUIWindowVideoNav::PlayItem(int iItem)
+{
+ // unlike additemtoplaylist, we need to check the items here
+ // before calling it since the current playlist will be stopped
+ // and cleared!
+
+ // root is not allowed
+ if (m_vecItems->IsVirtualDirectoryRoot())
+ return;
+
+ CGUIWindowVideoBase::PlayItem(iItem);
+}
+
+bool CGUIWindowVideoNav::OnItemInfo(const CFileItem& fileItem, ADDON::ScraperPtr& scraper)
+{
+ if (!scraper || scraper->Content() == CONTENT_NONE)
+ {
+ m_database.Open(); // since we can be called from the music library without being inited
+ if (fileItem.IsVideoDb())
+ scraper = m_database.GetScraperForPath(fileItem.GetVideoInfoTag()->m_strPath);
+ else
+ {
+ std::string strPath,strFile;
+ URIUtils::Split(fileItem.GetPath(),strPath,strFile);
+ scraper = m_database.GetScraperForPath(strPath);
+ }
+ m_database.Close();
+ }
+ return CGUIWindowVideoBase::OnItemInfo(fileItem, scraper);
+}
+
+void CGUIWindowVideoNav::OnDeleteItem(const CFileItemPtr& pItem)
+{
+ if (m_vecItems->IsParentFolder())
+ return;
+
+ if (!m_vecItems->IsVideoDb() && !pItem->IsVideoDb())
+ {
+ if (!pItem->IsPath("newsmartplaylist://video") &&
+ !pItem->IsPath("special://videoplaylists/") &&
+ !pItem->IsPath("sources://video/") &&
+ !URIUtils::IsProtocol(pItem->GetPath(), "newtag"))
+ CGUIWindowVideoBase::OnDeleteItem(pItem);
+ }
+ else if (StringUtils::StartsWithNoCase(pItem->GetPath(), "videodb://movies/sets/") &&
+ pItem->GetPath().size() > 22 && pItem->m_bIsFolder)
+ {
+ CGUIDialogYesNo* pDialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogYesNo>(WINDOW_DIALOG_YES_NO);
+
+ if (!pDialog)
+ return;
+
+ pDialog->SetHeading(CVariant{432});
+ std::string strLabel = StringUtils::Format(g_localizeStrings.Get(433), pItem->GetLabel());
+ pDialog->SetLine(1, CVariant{std::move(strLabel)});
+ pDialog->SetLine(2, CVariant{""});
+ pDialog->Open();
+ if (pDialog->IsConfirmed())
+ {
+ CFileItemList items;
+ CDirectory::GetDirectory(pItem->GetPath(),items,"",DIR_FLAG_NO_FILE_DIRS);
+ for (int i=0;i<items.Size();++i)
+ OnDeleteItem(items[i]);
+
+ CVideoDatabaseDirectory dir;
+ CQueryParams params;
+ dir.GetQueryParams(pItem->GetPath(),params);
+ m_database.DeleteSet(params.GetSetId());
+ }
+ }
+ else if (m_vecItems->IsPath(CUtil::VideoPlaylistsLocation()) ||
+ m_vecItems->IsPath("special://videoplaylists/"))
+ {
+ pItem->m_bIsFolder = false;
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui && gui->ConfirmDelete(pItem->GetPath()))
+ CFileUtils::DeleteItem(pItem);
+ }
+ else
+ {
+ if (!CGUIDialogVideoInfo::DeleteVideoItem(pItem))
+ return;
+ }
+ int itemNumber = m_viewControl.GetSelectedItem();
+ int select = itemNumber >= m_vecItems->Size()-1 ? itemNumber-1 : itemNumber;
+ m_viewControl.SetSelectedItem(select);
+
+ CUtil::DeleteVideoDatabaseDirectoryCache();
+}
+
+void CGUIWindowVideoNav::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+
+ CGUIWindowVideoBase::GetContextButtons(itemNumber, buttons);
+
+ CVideoDatabaseDirectory dir;
+ NODE_TYPE node = dir.GetDirectoryChildType(m_vecItems->GetPath());
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (!item)
+ {
+ // nothing to do here
+ }
+ else if (m_vecItems->IsPath("sources://video/"))
+ {
+ // get the usual shares
+ CGUIDialogContextMenu::GetContextButtons("video", item, buttons);
+ if (!item->IsDVD() && item->GetPath() != "add" && !item->IsParentFolder() &&
+ (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser))
+ {
+ CVideoDatabase database;
+ database.Open();
+ ADDON::ScraperPtr info = database.GetScraperForPath(item->GetPath());
+
+ if (!item->IsLiveTV() && !item->IsAddonsPath() && !URIUtils::IsUPnP(item->GetPath()))
+ {
+ if (info && info->Content() != CONTENT_NONE)
+ {
+ buttons.Add(CONTEXT_BUTTON_SET_CONTENT, 20442);
+ buttons.Add(CONTEXT_BUTTON_SCAN, 13349);
+ }
+ else
+ buttons.Add(CONTEXT_BUTTON_SET_CONTENT, 20333);
+ }
+ }
+ }
+ else
+ {
+ // are we in the playlists location?
+ bool inPlaylists = m_vecItems->IsPath(CUtil::VideoPlaylistsLocation()) ||
+ m_vecItems->IsPath("special://videoplaylists/");
+
+ if (item->HasVideoInfoTag() && item->HasProperty("artist_musicid"))
+ buttons.Add(CONTEXT_BUTTON_GO_TO_ARTIST, 20396);
+
+ if (item->HasVideoInfoTag() && item->HasProperty("album_musicid"))
+ buttons.Add(CONTEXT_BUTTON_GO_TO_ALBUM, 20397);
+
+ if (item->HasVideoInfoTag() && !item->GetVideoInfoTag()->m_strAlbum.empty() &&
+ !item->GetVideoInfoTag()->m_artist.empty() &&
+ !item->GetVideoInfoTag()->m_strTitle.empty())
+ {
+ CMusicDatabase database;
+ database.Open();
+ if (database.GetSongByArtistAndAlbumAndTitle(StringUtils::Join(item->GetVideoInfoTag()->m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator),
+ item->GetVideoInfoTag()->m_strAlbum,
+ item->GetVideoInfoTag()->m_strTitle) > -1)
+ {
+ buttons.Add(CONTEXT_BUTTON_PLAY_OTHER, 20398);
+ }
+ }
+ if (!item->IsParentFolder())
+ {
+ ADDON::ScraperPtr info;
+ VIDEO::SScanSettings settings;
+ GetScraperForItem(item.get(), info, settings);
+
+ // can we update the database?
+ if (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser)
+ {
+ if (!CVideoLibraryQueue::GetInstance().IsScanningLibrary() && item->IsVideoDb() &&
+ item->HasVideoInfoTag() &&
+ (item->GetVideoInfoTag()->m_type == MediaTypeMovie || // movies
+ item->GetVideoInfoTag()->m_type == MediaTypeTvShow || // tvshows
+ item->GetVideoInfoTag()->m_type == MediaTypeSeason || // seasons
+ item->GetVideoInfoTag()->m_type == MediaTypeEpisode || // episodes
+ item->GetVideoInfoTag()->m_type == MediaTypeMusicVideo || // musicvideos
+ item->GetVideoInfoTag()->m_type == "tag" || // tags
+ item->GetVideoInfoTag()->m_type == MediaTypeVideoCollection)) // sets
+ {
+ buttons.Add(CONTEXT_BUTTON_EDIT, 16106);
+ }
+ if (node == NODE_TYPE_TITLE_TVSHOWS)
+ {
+ buttons.Add(CONTEXT_BUTTON_SCAN, 13349);
+ }
+
+ if (node == NODE_TYPE_ACTOR && !dir.IsAllItem(item->GetPath()) && item->m_bIsFolder)
+ {
+ if (StringUtils::StartsWithNoCase(m_vecItems->GetPath(), "videodb://musicvideos")) // mvids
+ buttons.Add(CONTEXT_BUTTON_SET_ARTIST_THUMB, 13359);
+ else
+ buttons.Add(CONTEXT_BUTTON_SET_ACTOR_THUMB, 20403);
+ }
+ }
+
+ if (!m_vecItems->IsVideoDb() && !m_vecItems->IsVirtualDirectoryRoot())
+ { // non-video db items, file operations are allowed
+ if ((CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_ALLOWFILEDELETION) &&
+ CUtil::SupportsWriteFileOperations(item->GetPath())) ||
+ (inPlaylists && URIUtils::GetFileName(item->GetPath()) != "PartyMode-Video.xsp"
+ && (item->IsPlayList() || item->IsSmartPlayList())))
+ {
+ buttons.Add(CONTEXT_BUTTON_DELETE, 117);
+ buttons.Add(CONTEXT_BUTTON_RENAME, 118);
+ }
+ // add "Set/Change content" to folders
+ if (item->m_bIsFolder && !item->IsVideoDb() && !item->IsPlayList() && !item->IsSmartPlayList() && !item->IsLibraryFolder() && !item->IsLiveTV() && !item->IsPlugin() && !item->IsAddonsPath() && !URIUtils::IsUPnP(item->GetPath()))
+ {
+ if (info && info->Content() != CONTENT_NONE)
+ buttons.Add(CONTEXT_BUTTON_SET_CONTENT, 20442);
+ else
+ buttons.Add(CONTEXT_BUTTON_SET_CONTENT, 20333);
+
+ if (info && info->Content() != CONTENT_NONE)
+ buttons.Add(CONTEXT_BUTTON_SCAN, 13349);
+ }
+ }
+
+ if ((!item->HasVideoInfoTag() || item->GetVideoInfoTag()->m_iDbId == -1) && info && info->Content() != CONTENT_NONE)
+ buttons.Add(CONTEXT_BUTTON_SCAN_TO_LIBRARY, 21845);
+
+ }
+ }
+}
+
+bool CGUIWindowVideoNav::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+ if (CGUIDialogContextMenu::OnContextButton("video", item, button))
+ {
+ if (button == CONTEXT_BUTTON_REMOVE_SOURCE && !item->IsLiveTV()
+ &&!item->IsRSS() && !URIUtils::IsUPnP(item->GetPath()))
+ {
+ // if the source has been properly removed, remove the cached source list because the list has changed
+ if (OnUnAssignContent(item->GetPath(), 20375, 20340))
+ m_vecItems->RemoveDiscCache(GetID());
+ }
+ Refresh();
+ return true;
+ }
+ switch (button)
+ {
+ case CONTEXT_BUTTON_EDIT:
+ {
+ CONTEXT_BUTTON ret = (CONTEXT_BUTTON)CGUIDialogVideoInfo::ManageVideoItem(item);
+ if (ret != CONTEXT_BUTTON_CANCELLED)
+ {
+ Refresh(true);
+ if (ret == CONTEXT_BUTTON_DELETE)
+ {
+ int select = itemNumber >= m_vecItems->Size()-1 ? itemNumber-1:itemNumber;
+ m_viewControl.SetSelectedItem(select);
+ }
+ }
+ return true;
+ }
+
+ case CONTEXT_BUTTON_SET_ACTOR_THUMB:
+ case CONTEXT_BUTTON_SET_ARTIST_THUMB:
+ {
+ std::string type = MediaTypeSeason;
+ if (button == CONTEXT_BUTTON_SET_ACTOR_THUMB)
+ type = "actor";
+ else if (button == CONTEXT_BUTTON_SET_ARTIST_THUMB)
+ type = MediaTypeArtist;
+
+ bool result = CGUIDialogVideoInfo::ManageVideoItemArtwork(m_vecItems->Get(itemNumber), type);
+ Refresh();
+
+ return result;
+ }
+ case CONTEXT_BUTTON_GO_TO_ARTIST:
+ {
+ std::string strPath;
+ strPath = StringUtils::Format("musicdb://artists/{}/",
+ item->GetProperty("artist_musicid").asInteger());
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_NAV, strPath);
+ return true;
+ }
+ case CONTEXT_BUTTON_GO_TO_ALBUM:
+ {
+ std::string strPath;
+ strPath = StringUtils::Format("musicdb://albums/{}/",
+ item->GetProperty("album_musicid").asInteger());
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_NAV, strPath);
+ return true;
+ }
+ case CONTEXT_BUTTON_PLAY_OTHER:
+ {
+ CMusicDatabase database;
+ database.Open();
+ CSong song;
+ if (database.GetSong(database.GetSongByArtistAndAlbumAndTitle(StringUtils::Join(m_vecItems->Get(itemNumber)->GetVideoInfoTag()->m_artist, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoItemSeparator),m_vecItems->Get(itemNumber)->GetVideoInfoTag()->m_strAlbum,
+ m_vecItems->Get(itemNumber)->GetVideoInfoTag()->m_strTitle),
+ song))
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0,
+ static_cast<void*>(new CFileItem(song)));
+ }
+ return true;
+ }
+ case CONTEXT_BUTTON_SCAN_TO_LIBRARY:
+ CGUIDialogVideoInfo::ShowFor(*item);
+ return true;
+
+ default:
+ break;
+
+ }
+ return CGUIWindowVideoBase::OnContextButton(itemNumber, button);
+}
+
+bool CGUIWindowVideoNav::OnAddMediaSource()
+{
+ return CGUIDialogMediaSource::ShowAndAddMediaSource("video");
+}
+
+bool CGUIWindowVideoNav::OnClick(int iItem, const std::string &player)
+{
+ CFileItemPtr item = m_vecItems->Get(iItem);
+ if (!item->m_bIsFolder && item->IsVideoDb() && !item->Exists())
+ {
+ CLog::Log(LOGDEBUG, "{} called on '{}' but file doesn't exist", __FUNCTION__, item->GetPath());
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (profileManager->GetCurrentProfile().canWriteDatabases() || g_passwordManager.bMasterUser)
+ {
+ if (!CGUIDialogVideoInfo::DeleteVideoItemFromDatabase(item, true))
+ return true;
+
+ // update list
+ Refresh(true);
+ m_viewControl.SetSelectedItem(iItem);
+ return true;
+ }
+ else
+ {
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{662});
+ return true;
+ }
+ }
+ else if (StringUtils::StartsWithNoCase(item->GetPath(), "newtag://"))
+ {
+ // dont allow update while scanning
+ if (CVideoLibraryQueue::GetInstance().IsScanningLibrary())
+ {
+ HELPERS::ShowOKDialogText(CVariant{257}, CVariant{14057});
+ return true;
+ }
+
+ //Get the new title
+ std::string strTag;
+ if (!CGUIKeyboardFactory::ShowAndGetInput(strTag, CVariant{g_localizeStrings.Get(20462)}, false))
+ return true;
+
+ CVideoDatabase videodb;
+ if (!videodb.Open())
+ return true;
+
+ // get the media type and convert from plural to singular (by removing the trailing "s")
+ std::string mediaType = item->GetPath().substr(9);
+ mediaType = mediaType.substr(0, mediaType.size() - 1);
+ std::string localizedType = CGUIDialogVideoInfo::GetLocalizedVideoType(mediaType);
+ if (localizedType.empty())
+ return true;
+
+ if (!videodb.GetSingleValue("tag", "tag.tag_id", videodb.PrepareSQL("tag.name = '%s' AND tag.tag_id IN (SELECT tag_link.tag_id FROM tag_link WHERE tag_link.media_type = '%s')", strTag.c_str(), mediaType.c_str())).empty())
+ {
+ std::string strError = StringUtils::Format(g_localizeStrings.Get(20463), strTag);
+ HELPERS::ShowOKDialogText(CVariant{20462}, CVariant{std::move(strError)});
+ return true;
+ }
+
+ int idTag = videodb.AddTag(strTag);
+ CFileItemList items;
+ std::string strLabel = StringUtils::Format(g_localizeStrings.Get(20464), localizedType);
+ if (CGUIDialogVideoInfo::GetItemsForTag(strLabel, mediaType, items, idTag))
+ {
+ for (int index = 0; index < items.Size(); index++)
+ {
+ if (!items[index]->HasVideoInfoTag() || items[index]->GetVideoInfoTag()->m_iDbId <= 0)
+ continue;
+
+ videodb.AddTagToItem(items[index]->GetVideoInfoTag()->m_iDbId, idTag, mediaType);
+ }
+ }
+
+ Refresh(true);
+ return true;
+ }
+
+ return CGUIWindowVideoBase::OnClick(iItem, player);
+}
+
+std::string CGUIWindowVideoNav::GetStartFolder(const std::string &dir)
+{
+ std::string lower(dir); StringUtils::ToLower(lower);
+ if (lower == "moviegenres")
+ return "videodb://movies/genres/";
+ else if (lower == "movietitles")
+ return "videodb://movies/titles/";
+ else if (lower == "movieyears")
+ return "videodb://movies/years/";
+ else if (lower == "movieactors")
+ return "videodb://movies/actors/";
+ else if (lower == "moviedirectors")
+ return "videodb://movies/directors/";
+ else if (lower == "moviestudios")
+ return "videodb://movies/studios/";
+ else if (lower == "moviesets")
+ return "videodb://movies/sets/";
+ else if (lower == "moviecountries")
+ return "videodb://movies/countries/";
+ else if (lower == "movietags")
+ return "videodb://movies/tags/";
+ else if (lower == "movies")
+ return "videodb://movies/";
+ else if (lower == "tvshowgenres")
+ return "videodb://tvshows/genres/";
+ else if (lower == "tvshowtitles")
+ return "videodb://tvshows/titles/";
+ else if (lower == "tvshowyears")
+ return "videodb://tvshows/years/";
+ else if (lower == "tvshowactors")
+ return "videodb://tvshows/actors/";
+ else if (lower == "tvshowstudios")
+ return "videodb://tvshows/studios/";
+ else if (lower == "tvshowtags")
+ return "videodb://tvshows/tags/";
+ else if (lower == "tvshows")
+ return "videodb://tvshows/";
+ else if (lower == "musicvideogenres")
+ return "videodb://musicvideos/genres/";
+ else if (lower == "musicvideotitles")
+ return "videodb://musicvideos/titles/";
+ else if (lower == "musicvideoyears")
+ return "videodb://musicvideos/years/";
+ else if (lower == "musicvideoartists")
+ return "videodb://musicvideos/artists/";
+ else if (lower == "musicvideoalbums")
+ return "videodb://musicvideos/albums/";
+ else if (lower == "musicvideodirectors")
+ return "videodb://musicvideos/directors/";
+ else if (lower == "musicvideostudios")
+ return "videodb://musicvideos/studios/";
+ else if (lower == "musicvideotags")
+ return "videodb://musicvideos/tags/";
+ else if (lower == "musicvideos")
+ return "videodb://musicvideos/";
+ else if (lower == "recentlyaddedmovies")
+ return "videodb://recentlyaddedmovies/";
+ else if (lower == "recentlyaddedepisodes")
+ return "videodb://recentlyaddedepisodes/";
+ else if (lower == "recentlyaddedmusicvideos")
+ return "videodb://recentlyaddedmusicvideos/";
+ else if (lower == "inprogresstvshows")
+ return "videodb://inprogresstvshows/";
+ else if (lower == "files")
+ return "sources://video/";
+ return CGUIWindowVideoBase::GetStartFolder(dir);
+}
+
+bool CGUIWindowVideoNav::ApplyWatchedFilter(CFileItemList &items)
+{
+ bool listchanged = false;
+ CVideoDatabaseDirectory dir;
+ NODE_TYPE node = dir.GetDirectoryChildType(items.GetPath());
+
+ // now filter watched items as necessary
+ bool filterWatched=false;
+ if (node == NODE_TYPE_EPISODES
+ || node == NODE_TYPE_SEASONS
+ || node == NODE_TYPE_SETS
+ || node == NODE_TYPE_TAGS
+ || node == NODE_TYPE_TITLE_MOVIES
+ || node == NODE_TYPE_TITLE_TVSHOWS
+ || node == NODE_TYPE_TITLE_MUSICVIDEOS
+ || node == NODE_TYPE_RECENTLY_ADDED_EPISODES
+ || node == NODE_TYPE_RECENTLY_ADDED_MOVIES
+ || node == NODE_TYPE_RECENTLY_ADDED_MUSICVIDEOS)
+ filterWatched = true;
+ if (!items.IsVideoDb())
+ filterWatched = true;
+ if (items.GetContent() == "tvshows" &&
+ (items.IsSmartPlayList() || items.IsLibraryFolder()))
+ node = NODE_TYPE_TITLE_TVSHOWS; // so that the check below works
+
+ int watchMode = CMediaSettings::GetInstance().GetWatchedMode(m_vecItems->GetContent());
+
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items.Get(i);
+
+ if(item->HasVideoInfoTag() && (node == NODE_TYPE_TITLE_TVSHOWS || node == NODE_TYPE_SEASONS))
+ {
+ if (watchMode == WatchedModeUnwatched)
+ item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("unwatchedepisodes").asInteger();
+ if (watchMode == WatchedModeWatched)
+ item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("watchedepisodes").asInteger();
+ if (watchMode == WatchedModeAll)
+ item->GetVideoInfoTag()->m_iEpisode = (int)item->GetProperty("totalepisodes").asInteger();
+ item->SetProperty("numepisodes", item->GetVideoInfoTag()->m_iEpisode);
+ listchanged = true;
+ }
+
+ if (filterWatched)
+ {
+ if(!item->IsParentFolder() && // Don't delete the go to parent folder
+ ((watchMode == WatchedModeWatched && item->GetVideoInfoTag()->GetPlayCount() == 0) ||
+ (watchMode == WatchedModeUnwatched && item->GetVideoInfoTag()->GetPlayCount() > 0)))
+ {
+ items.Remove(i);
+ i--;
+ listchanged = true;
+ }
+ }
+ }
+
+ // Remove the parent folder icon, if it's the only thing in the folder. This is needed for hiding seasons.
+ if (items.GetObjectCount() == 0 && items.GetFileCount() > 0 && items.Get(0)->IsParentFolder())
+ items.Remove(0);
+
+ if(node == NODE_TYPE_TITLE_TVSHOWS || node == NODE_TYPE_SEASONS)
+ {
+ // the watched filter may change the "numepisodes" property which is reflected in the TV_SHOWS and SEASONS nodes
+ // therefore, the items labels have to be refreshed, and possibly the list needs resorting as well.
+ items.ClearSortState(); // this is needed to force resorting even if sort method did not change
+ FormatAndSort(items);
+ }
+
+ return listchanged;
+}
diff --git a/xbmc/video/windows/GUIWindowVideoNav.h b/xbmc/video/windows/GUIWindowVideoNav.h
new file mode 100644
index 0000000..bdf4987
--- /dev/null
+++ b/xbmc/video/windows/GUIWindowVideoNav.h
@@ -0,0 +1,68 @@
+/*
+ * 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 "GUIWindowVideoBase.h"
+
+class CFileItemList;
+
+enum SelectFirstUnwatchedItem
+{
+ NEVER = 0,
+ ON_FIRST_ENTRY = 1,
+ ALWAYS = 2
+};
+
+enum IncludeAllSeasonsAndSpecials
+{
+ NEITHER = 0,
+ BOTH = 1,
+ ALL_SEASONS = 2,
+ SPECIALS = 3
+};
+
+class CGUIWindowVideoNav : public CGUIWindowVideoBase
+{
+public:
+
+ CGUIWindowVideoNav(void);
+ ~CGUIWindowVideoNav(void) override;
+
+ bool OnAction(const CAction &action) override;
+ bool OnMessage(CGUIMessage& message) override;
+
+ bool OnItemInfo(const CFileItem& fileItem, ADDON::ScraperPtr& info) override;
+
+protected:
+ bool ApplyWatchedFilter(CFileItemList &items);
+ bool GetFilteredItems(const std::string &filter, CFileItemList &items) override;
+
+ void OnItemLoaded(CFileItem* pItem) override {};
+
+ // override base class methods
+ bool Update(const std::string &strDirectory, bool updateFilterPath = true) override;
+ bool GetDirectory(const std::string &strDirectory, CFileItemList &items) override;
+ void UpdateButtons() override;
+ void DoSearch(const std::string& strSearch, CFileItemList& items) override;
+ virtual void PlayItem(int iItem);
+ void OnDeleteItem(const CFileItemPtr& pItem) override;
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+ bool OnAddMediaSource() override;
+ bool OnClick(int iItem, const std::string &player = "") override;
+ std::string GetStartFolder(const std::string &dir) override;
+
+ VECSOURCES m_shares;
+
+private:
+ virtual SelectFirstUnwatchedItem GetSettingSelectFirstUnwatchedItem();
+ virtual IncludeAllSeasonsAndSpecials GetSettingIncludeAllSeasonsAndSpecials();
+ virtual int GetFirstUnwatchedItemIndex(bool includeAllSeasons, bool includeSpecials);
+ void SelectFirstUnwatched();
+};
diff --git a/xbmc/video/windows/GUIWindowVideoPlaylist.cpp b/xbmc/video/windows/GUIWindowVideoPlaylist.cpp
new file mode 100644
index 0000000..b27582d
--- /dev/null
+++ b/xbmc/video/windows/GUIWindowVideoPlaylist.cpp
@@ -0,0 +1,609 @@
+/*
+ * 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 "GUIWindowVideoPlaylist.h"
+
+#include "GUIUserMessages.h"
+#include "PartyModeManager.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "dialogs/GUIDialogSmartPlaylistEditor.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "playlists/PlayListM3U.h"
+#include "settings/MediaSettings.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_LABELFILES 12
+
+#define CONTROL_BTNSHUFFLE 20
+#define CONTROL_BTNSAVE 21
+#define CONTROL_BTNCLEAR 22
+
+#define CONTROL_BTNPLAY 23
+#define CONTROL_BTNNEXT 24
+#define CONTROL_BTNPREVIOUS 25
+#define CONTROL_BTNREPEAT 26
+
+CGUIWindowVideoPlaylist::CGUIWindowVideoPlaylist()
+: CGUIWindowVideoBase(WINDOW_VIDEO_PLAYLIST, "MyPlaylist.xml")
+{
+ m_movingFrom = -1;
+}
+
+CGUIWindowVideoPlaylist::~CGUIWindowVideoPlaylist() = default;
+
+void CGUIWindowVideoPlaylist::OnPrepareFileItems(CFileItemList& items)
+{
+ CGUIWindowVideoBase::OnPrepareFileItems(items);
+
+ if (items.IsEmpty())
+ return;
+
+ if (!items.IsVideoDb() && !items.IsVirtualDirectoryRoot())
+ { // load info from the database
+ std::string label;
+ if (items.GetLabel().empty() &&
+ m_rootDir.IsSource(items.GetPath(), CMediaSourceSettings::GetInstance().GetSources("video"),
+ &label))
+ items.SetLabel(label);
+ if (!items.IsSourcesPath() && !items.IsLibraryFolder())
+ LoadVideoInfo(items, m_database);
+ }
+}
+
+bool CGUIWindowVideoPlaylist::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_PLAYLISTPLAYER_REPEAT:
+ {
+ UpdateButtons();
+ }
+ break;
+
+ case GUI_MSG_PLAYLISTPLAYER_RANDOM:
+ case GUI_MSG_PLAYLIST_CHANGED:
+ {
+ // global playlist changed outside playlist window
+ UpdateButtons();
+ Refresh(true);
+
+ if (m_viewControl.HasControl(m_iLastControl) && m_vecItems->Size() <= 0)
+ {
+ m_iLastControl = CONTROL_BTNVIEWASICONS;
+ SET_CONTROL_FOCUS(m_iLastControl, 0);
+ }
+
+ }
+ break;
+
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ m_movingFrom = -1;
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ m_vecItems->SetPath("playlistvideo://");
+
+ if (!CGUIWindowVideoBase::OnMessage(message))
+ return false;
+
+ if (m_vecItems->Size() <= 0)
+ {
+ m_iLastControl = CONTROL_BTNVIEWASICONS;
+ SET_CONTROL_FOCUS(m_iLastControl, 0);
+ }
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingVideo() &&
+ CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO)
+ {
+ int iSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ if (iSong >= 0 && iSong <= m_vecItems->Size())
+ m_viewControl.SetSelectedItem(iSong);
+ }
+
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTNSHUFFLE)
+ {
+ if (!g_partyModeManager.IsEnabled())
+ {
+ CServiceBroker::GetPlaylistPlayer().SetShuffle(
+ PLAYLIST::TYPE_VIDEO,
+ !(CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_VIDEO)));
+ CMediaSettings::GetInstance().SetVideoPlaylistShuffled(
+ CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_VIDEO));
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+ UpdateButtons();
+ Refresh();
+ }
+ }
+ else if (iControl == CONTROL_BTNSAVE)
+ {
+ SavePlayList();
+ }
+ else if (iControl == CONTROL_BTNCLEAR)
+ {
+ ClearPlayList();
+ }
+ else if (iControl == CONTROL_BTNPLAY)
+ {
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().Play(m_viewControl.GetSelectedItem(), "");
+ UpdateButtons();
+ }
+ else if (iControl == CONTROL_BTNNEXT)
+ {
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().PlayNext();
+ }
+ else if (iControl == CONTROL_BTNPREVIOUS)
+ {
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ CServiceBroker::GetPlaylistPlayer().PlayPrevious();
+ }
+ else if (iControl == CONTROL_BTNREPEAT)
+ {
+ // increment repeat state
+ PLAYLIST::RepeatState state =
+ CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_VIDEO);
+ if (state == PLAYLIST::RepeatState::NONE)
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_VIDEO,
+ PLAYLIST::RepeatState::ALL);
+ else if (state == PLAYLIST::RepeatState::ALL)
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_VIDEO,
+ PLAYLIST::RepeatState::ONE);
+ else
+ CServiceBroker::GetPlaylistPlayer().SetRepeat(PLAYLIST::TYPE_VIDEO,
+ PLAYLIST::RepeatState::NONE);
+
+ // save settings
+ CMediaSettings::GetInstance().SetVideoPlaylistRepeat(
+ CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_VIDEO) ==
+ PLAYLIST::RepeatState::ALL);
+ CServiceBroker::GetSettingsComponent()->GetSettings()->Save();
+
+ UpdateButtons();
+ }
+ else if (m_viewControl.HasControl(iControl)) // list/thumb control
+ {
+ int iAction = message.GetParam1();
+ int iItem = m_viewControl.GetSelectedItem();
+ if (iAction == ACTION_DELETE_ITEM || iAction == ACTION_MOUSE_MIDDLE_CLICK)
+ {
+ RemovePlayListItem(iItem);
+ MarkPlaying();
+ }
+ }
+ }
+ break;
+ }
+ return CGUIWindowVideoBase::OnMessage(message);
+}
+
+bool CGUIWindowVideoPlaylist::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_PARENT_DIR)
+ {
+ // Playlist has no parent dirs
+ return true;
+ }
+ if (action.GetID() == ACTION_SHOW_PLAYLIST)
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return true;
+ }
+ if ((action.GetID() == ACTION_MOVE_ITEM_UP) || (action.GetID() == ACTION_MOVE_ITEM_DOWN))
+ {
+ int iItem = -1;
+ int iFocusedControl = GetFocusedControlID();
+ if (m_viewControl.HasControl(iFocusedControl))
+ iItem = m_viewControl.GetSelectedItem();
+ OnMove(iItem, action.GetID());
+ return true;
+ }
+ return CGUIWindowVideoBase::OnAction(action);
+}
+
+bool CGUIWindowVideoPlaylist::OnBack(int actionID)
+{
+ if (actionID == ACTION_NAV_BACK)
+ return CGUIWindow::OnBack(actionID); // base class goes up a folder, but none to go up
+ return CGUIWindowVideoBase::OnBack(actionID);
+}
+
+bool CGUIWindowVideoPlaylist::MoveCurrentPlayListItem(int iItem, int iAction, bool bUpdate /* = true */)
+{
+ int iSelected = iItem;
+ int iNew = iSelected;
+ if (iAction == ACTION_MOVE_ITEM_UP)
+ iNew--;
+ else
+ iNew++;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ // is the currently playing item affected?
+ bool bFixCurrentSong = false;
+ if ((CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO) &&
+ appPlayer->IsPlayingVideo() &&
+ ((CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iSelected) ||
+ (CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iNew)))
+ bFixCurrentSong = true;
+
+ PLAYLIST::CPlayList& playlist =
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_VIDEO);
+ if (playlist.Swap(iSelected, iNew))
+ {
+ // Correct the current playing song in playlistplayer
+ if (bFixCurrentSong)
+ {
+ int iCurrentSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ if (iSelected == iCurrentSong)
+ iCurrentSong = iNew;
+ else if (iNew == iCurrentSong)
+ iCurrentSong = iSelected;
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(iCurrentSong);
+ }
+
+ if (bUpdate)
+ Refresh();
+ return true;
+ }
+
+ return false;
+}
+
+
+void CGUIWindowVideoPlaylist::ClearPlayList()
+{
+ ClearFileItems();
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(PLAYLIST::TYPE_VIDEO);
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO)
+ {
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE);
+ }
+ m_viewControl.SetItems(*m_vecItems);
+ UpdateButtons();
+ SET_CONTROL_FOCUS(CONTROL_BTNVIEWASICONS, 0);
+}
+
+void CGUIWindowVideoPlaylist::UpdateButtons()
+{
+ // Update playlist buttons
+ if (m_vecItems->Size() )
+ {
+ CONTROL_ENABLE(CONTROL_BTNCLEAR);
+ CONTROL_ENABLE(CONTROL_BTNSAVE);
+ CONTROL_ENABLE(CONTROL_BTNPLAY);
+ CONTROL_ENABLE(CONTROL_BTNSHUFFLE);
+ CONTROL_ENABLE(CONTROL_BTNREPEAT);
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingVideo() &&
+ CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO)
+ {
+ CONTROL_ENABLE(CONTROL_BTNNEXT);
+ CONTROL_ENABLE(CONTROL_BTNPREVIOUS);
+ }
+ else
+ {
+ CONTROL_DISABLE(CONTROL_BTNNEXT);
+ CONTROL_DISABLE(CONTROL_BTNPREVIOUS);
+ }
+ }
+ else
+ {
+ CONTROL_DISABLE(CONTROL_BTNCLEAR);
+ CONTROL_DISABLE(CONTROL_BTNSAVE);
+ CONTROL_DISABLE(CONTROL_BTNSHUFFLE);
+ CONTROL_DISABLE(CONTROL_BTNPLAY);
+ CONTROL_DISABLE(CONTROL_BTNNEXT);
+ CONTROL_DISABLE(CONTROL_BTNPREVIOUS);
+ CONTROL_DISABLE(CONTROL_BTNREPEAT);
+ }
+
+ CGUIMediaWindow::UpdateButtons();
+
+ // update buttons
+ CONTROL_DESELECT(CONTROL_BTNSHUFFLE);
+ if (CServiceBroker::GetPlaylistPlayer().IsShuffled(PLAYLIST::TYPE_VIDEO))
+ CONTROL_SELECT(CONTROL_BTNSHUFFLE);
+
+ // update repeat button
+ PLAYLIST::RepeatState repState =
+ CServiceBroker::GetPlaylistPlayer().GetRepeat(PLAYLIST::TYPE_VIDEO);
+ int iLocalizedString;
+ if (repState == PLAYLIST::RepeatState::NONE)
+ iLocalizedString = 595; // Repeat: Off
+ else if (repState == PLAYLIST::RepeatState::ONE)
+ iLocalizedString = 596; // Repeat: One
+ else
+ iLocalizedString = 597; // Repeat: All
+
+ SET_CONTROL_LABEL(CONTROL_BTNREPEAT, g_localizeStrings.Get(iLocalizedString));
+
+ MarkPlaying();
+}
+
+bool CGUIWindowVideoPlaylist::OnPlayMedia(int iItem, const std::string &player)
+{
+ if ( iItem < 0 || iItem >= m_vecItems->Size() ) return false;
+ if (g_partyModeManager.IsEnabled())
+ g_partyModeManager.Play(iItem);
+ else
+ {
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+ std::string strPath = pItem->GetPath();
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_VIDEO);
+ // need to update Playlist FileItem's startOffset and resumePoint based on GUIWindowVideoPlaylist FileItem
+ if (pItem->GetStartOffset() == STARTOFFSET_RESUME)
+ {
+ CFileItemPtr pPlaylistItem =
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(PLAYLIST::TYPE_VIDEO)[iItem];
+ pPlaylistItem->SetStartOffset(pItem->GetStartOffset());
+ if (pPlaylistItem->HasVideoInfoTag() && pItem->HasVideoInfoTag())
+ pPlaylistItem->GetVideoInfoTag()->SetResumePoint(pItem->GetVideoInfoTag()->GetResumePoint());
+ }
+ // now play item
+ CServiceBroker::GetPlaylistPlayer().Play(iItem, player);
+ }
+ return true;
+}
+
+void CGUIWindowVideoPlaylist::RemovePlayListItem(int iItem)
+{
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+
+ // The current playing song can't be removed
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == PLAYLIST::TYPE_VIDEO &&
+ appPlayer->IsPlayingVideo() && CServiceBroker::GetPlaylistPlayer().GetCurrentSong() == iItem)
+ return ;
+
+ CServiceBroker::GetPlaylistPlayer().Remove(PLAYLIST::TYPE_VIDEO, iItem);
+
+ Refresh();
+
+ if (m_vecItems->Size() <= 0)
+ {
+ SET_CONTROL_FOCUS(CONTROL_BTNVIEWASICONS, 0);
+ }
+ else
+ {
+ m_viewControl.SetSelectedItem(iItem - 1);
+ }
+
+ g_partyModeManager.OnSongChange();
+}
+
+/// \brief Save current playlist to playlist folder
+void CGUIWindowVideoPlaylist::SavePlayList()
+{
+ std::string strNewFileName;
+ if (CGUIKeyboardFactory::ShowAndGetInput(strNewFileName, CVariant{g_localizeStrings.Get(16012)}, false))
+ {
+ // need 2 rename it
+ strNewFileName = CUtil::MakeLegalFileName(strNewFileName);
+ strNewFileName += ".m3u";
+ std::string strPath = URIUtils::AddFileToFolder(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH),
+ "video",
+ strNewFileName);
+
+ PLAYLIST::CPlayListM3U playlist;
+ playlist.Add(*m_vecItems);
+
+ CLog::Log(LOGDEBUG, "Saving video playlist: [{}]", strPath);
+ playlist.Save(strPath);
+ }
+}
+
+void CGUIWindowVideoPlaylist::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ int itemPlaying = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ if (m_movingFrom >= 0)
+ {
+ if (itemNumber != m_movingFrom && (!g_partyModeManager.IsEnabled() || itemNumber > itemPlaying))
+ buttons.Add(CONTEXT_BUTTON_MOVE_HERE, 13252); // move item here
+ buttons.Add(CONTEXT_BUTTON_CANCEL_MOVE, 13253);
+
+ }
+ else
+ {
+ if (itemNumber > -1)
+ {
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ // check what players we have, if we have multiple display play with option
+ std::vector<std::string> players;
+ if (item->IsVideoDb())
+ {
+ CFileItem item2(item->GetVideoInfoTag()->m_strFileNameAndPath, false);
+ playerCoreFactory.GetPlayers(item2, players);
+ }
+ else
+ playerCoreFactory.GetPlayers(*item, players);
+
+ if (players.size() > 1)
+ buttons.Add(CONTEXT_BUTTON_PLAY_WITH, 15213); // Play With...
+ }
+ if (itemNumber > (g_partyModeManager.IsEnabled() ? 1 : 0))
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_UP, 13332);
+ if (itemNumber + 1 < m_vecItems->Size())
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM_DOWN, 13333);
+ if (!g_partyModeManager.IsEnabled() || itemNumber != itemPlaying)
+ buttons.Add(CONTEXT_BUTTON_MOVE_ITEM, 13251);
+
+ if (itemNumber != itemPlaying)
+ buttons.Add(CONTEXT_BUTTON_DELETE, 15015);
+ }
+ if (g_partyModeManager.IsEnabled())
+ {
+ buttons.Add(CONTEXT_BUTTON_EDIT_PARTYMODE, 21439);
+ buttons.Add(CONTEXT_BUTTON_CANCEL_PARTYMODE, 588); // cancel party mode
+ }
+}
+
+bool CGUIWindowVideoPlaylist::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ switch (button)
+ {
+ case CONTEXT_BUTTON_PLAY_WITH:
+ {
+ CFileItemPtr item;
+ if (itemNumber >= 0 && itemNumber < m_vecItems->Size())
+ item = m_vecItems->Get(itemNumber);
+ if (!item)
+ break;
+
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ std::vector<std::string> players;
+ if (item->IsVideoDb())
+ {
+ CFileItem item2(*item->GetVideoInfoTag());
+ playerCoreFactory.GetPlayers(item2, players);
+ }
+ else
+ playerCoreFactory.GetPlayers(*item, players);
+
+ std::string player = playerCoreFactory.SelectPlayerDialog(players);
+ if (!player.empty())
+ OnClick(itemNumber, player);
+ return true;
+ }
+
+ case CONTEXT_BUTTON_MOVE_ITEM:
+ m_movingFrom = itemNumber;
+ return true;
+
+ case CONTEXT_BUTTON_MOVE_HERE:
+ if (m_movingFrom >= 0)
+ MoveItem(m_movingFrom, itemNumber);
+ m_movingFrom = -1;
+ return true;
+
+ case CONTEXT_BUTTON_CANCEL_MOVE:
+ m_movingFrom = -1;
+ return true;
+
+ case CONTEXT_BUTTON_MOVE_ITEM_UP:
+ OnMove(itemNumber, ACTION_MOVE_ITEM_UP);
+ return true;
+
+ case CONTEXT_BUTTON_MOVE_ITEM_DOWN:
+ OnMove(itemNumber, ACTION_MOVE_ITEM_DOWN);
+ return true;
+
+ case CONTEXT_BUTTON_DELETE:
+ RemovePlayListItem(itemNumber);
+ return true;
+ case CONTEXT_BUTTON_CANCEL_PARTYMODE:
+ g_partyModeManager.Disable();
+ return true;
+ case CONTEXT_BUTTON_EDIT_PARTYMODE:
+ {
+ std::string playlist = "special://profile/PartyMode-Video.xsp";
+ if (CGUIDialogSmartPlaylistEditor::EditPlaylist(playlist))
+ {
+ // apply new rules
+ g_partyModeManager.Disable();
+ g_partyModeManager.Enable(PARTYMODECONTEXT_VIDEO);
+ }
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return CGUIWindowVideoBase::OnContextButton(itemNumber, button);
+}
+
+void CGUIWindowVideoPlaylist::OnMove(int iItem, int iAction)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size()) return;
+ MoveCurrentPlayListItem(iItem, iAction);
+}
+
+void CGUIWindowVideoPlaylist::MoveItem(int iStart, int iDest)
+{
+ if (iStart < 0 || iStart >= m_vecItems->Size()) return;
+ if (iDest < 0 || iDest >= m_vecItems->Size()) return;
+
+ // default to move up
+ int iAction = ACTION_MOVE_ITEM_UP;
+ int iDirection = -1;
+ // are we moving down?
+ if (iStart < iDest)
+ {
+ iAction = ACTION_MOVE_ITEM_DOWN;
+ iDirection = 1;
+ }
+
+ // keep swapping until you get to the destination or you
+ // hit the currently playing song
+ int i = iStart;
+ while (i != iDest)
+ {
+ // try to swap adjacent items
+ if (MoveCurrentPlayListItem(i, iAction, false))
+ i = i + (1 * iDirection);
+ // we hit currently playing song, so abort
+ else
+ break;
+ }
+ Refresh();
+}
+
+void CGUIWindowVideoPlaylist::MarkPlaying()
+{
+ /* // clear markings
+ for (int i = 0; i < m_vecItems->Size(); i++)
+ m_vecItems->Get(i)->Select(false);
+
+ // mark the currently playing item
+ if ((CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist() == TYPE_VIDEO) && (g_application.GetAppPlayer().IsPlayingVideo()))
+ {
+ int iSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ if (iSong >= 0 && iSong <= m_vecItems->Size())
+ m_vecItems->Get(iSong)->Select(true);
+ }*/
+}
+
diff --git a/xbmc/video/windows/GUIWindowVideoPlaylist.h b/xbmc/video/windows/GUIWindowVideoPlaylist.h
new file mode 100644
index 0000000..ea04fb0
--- /dev/null
+++ b/xbmc/video/windows/GUIWindowVideoPlaylist.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "GUIWindowVideoBase.h"
+
+class CGUIWindowVideoPlaylist : public CGUIWindowVideoBase
+{
+public:
+ CGUIWindowVideoPlaylist(void);
+ ~CGUIWindowVideoPlaylist(void) override;
+
+ void OnPrepareFileItems(CFileItemList& items) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnBack(int actionID) override;
+
+protected:
+ bool OnPlayMedia(int iItem, const std::string &player = "") override;
+ void UpdateButtons() override;
+ void MarkPlaying();
+
+ void GetContextButtons(int itemNumber, CContextButtons &buttons) override;
+ bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override;
+
+ void OnMove(int iItem, int iAction);
+
+ void ClearPlayList();
+ void RemovePlayListItem(int iItem);
+ bool MoveCurrentPlayListItem(int iItem, int iAction, bool bUpdate = true);
+ void MoveItem(int iStart, int iDest);
+
+ void SavePlayList();
+
+ int m_movingFrom;
+ VECSOURCES m_shares;
+};
diff --git a/xbmc/video/windows/VideoFileItemListModifier.cpp b/xbmc/video/windows/VideoFileItemListModifier.cpp
new file mode 100644
index 0000000..4d26af3
--- /dev/null
+++ b/xbmc/video/windows/VideoFileItemListModifier.cpp
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2016-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 "VideoFileItemListModifier.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "filesystem/VideoDatabaseDirectory/DirectoryNode.h"
+#include "guilib/LocalizeStrings.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoDbUrl.h"
+
+using namespace XFILE::VIDEODATABASEDIRECTORY;
+
+bool CVideoFileItemListModifier::CanModify(const CFileItemList &items) const
+{
+ if (items.IsVideoDb())
+ return true;
+
+ return false;
+}
+
+bool CVideoFileItemListModifier::Modify(CFileItemList &items) const
+{
+ AddQueuingFolder(items);
+ return true;
+}
+
+// Add an "* All ..." folder to the CFileItemList
+// depending on the child node
+void CVideoFileItemListModifier::AddQueuingFolder(CFileItemList& items)
+{
+ if (!items.IsVideoDb())
+ return;
+
+ auto directoryNode = CDirectoryNode::ParseURL(items.GetPath());
+
+ CFileItemPtr pItem;
+
+ // always show "all" items by default
+ if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOLIBRARY_SHOWALLITEMS))
+ return;
+
+ // no need for "all" item when only one item
+ if (items.GetObjectCount() <= 1)
+ return;
+
+ CVideoDbUrl videoUrl;
+ if (!videoUrl.FromString(directoryNode->BuildPath()))
+ return;
+
+ // hack - as the season node might return episodes
+ std::unique_ptr<CDirectoryNode> pNode(directoryNode);
+
+ switch (pNode->GetChildType())
+ {
+ case NODE_TYPE_SEASONS:
+ {
+ const std::string& strLabel = g_localizeStrings.Get(20366);
+ pItem.reset(new CFileItem(strLabel)); // "All Seasons"
+ videoUrl.AppendPath("-1/");
+ pItem->SetPath(videoUrl.ToString());
+ // set the number of watched and unwatched items accordingly
+ int watched = 0;
+ int unwatched = 0;
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items[i];
+ watched += static_cast<int>(item->GetProperty("watchedepisodes").asInteger());
+ unwatched += static_cast<int>(item->GetProperty("unwatchedepisodes").asInteger());
+ }
+ const int totalEpisodes = watched + unwatched;
+ pItem->SetProperty("totalepisodes", totalEpisodes);
+ pItem->SetProperty("numepisodes",
+ totalEpisodes); // will be changed later to reflect watchmode setting
+ pItem->SetProperty("watchedepisodes", watched);
+ pItem->SetProperty("unwatchedepisodes", unwatched);
+ pItem->SetProperty("watchedepisodepercent",
+ totalEpisodes > 0 ? watched * 100 / totalEpisodes : 0);
+
+ // @note: The items list may contain additional items that do not belong to the show.
+ // This is the case of the up directory (..) or movies linked to the tvshow.
+ // Iterate through the list till the first season type is found and the infotag can safely be copied.
+
+ if (items.Size() > 1)
+ {
+ for (int i = 1; i < items.Size(); i++)
+ {
+ if (items[i]->GetVideoInfoTag() && items[i]->GetVideoInfoTag()->m_type == MediaTypeSeason &&
+ items[i]->GetVideoInfoTag()->m_iSeason > 0)
+ {
+ *pItem->GetVideoInfoTag() = *items[i]->GetVideoInfoTag();
+ pItem->GetVideoInfoTag()->m_iSeason = -1;
+ break;
+ }
+ }
+ }
+
+ pItem->GetVideoInfoTag()->m_strTitle = strLabel;
+ pItem->GetVideoInfoTag()->m_iEpisode = watched + unwatched;
+ pItem->GetVideoInfoTag()->SetPlayCount((unwatched == 0) ? 1 : 0);
+ CVideoDatabase db;
+ if (db.Open())
+ {
+ pItem->GetVideoInfoTag()->m_iDbId = db.GetSeasonId(pItem->GetVideoInfoTag()->m_iIdShow, -1);
+ db.Close();
+ }
+ pItem->GetVideoInfoTag()->m_type = MediaTypeSeason;
+ }
+ break;
+ case NODE_TYPE_MUSICVIDEOS_ALBUM:
+ pItem.reset(new CFileItem("* " + g_localizeStrings.Get(16100))); // "* All Videos"
+ videoUrl.AppendPath("-1/");
+ pItem->SetPath(videoUrl.ToString());
+ break;
+ default:
+ break;
+ }
+
+ if (pItem)
+ {
+ pItem->m_bIsFolder = true;
+ pItem->SetSpecialSort(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bVideoLibraryAllItemsOnBottom ? SortSpecialOnBottom : SortSpecialOnTop);
+ pItem->SetCanQueue(false);
+ items.Add(pItem);
+ }
+}
diff --git a/xbmc/video/windows/VideoFileItemListModifier.h b/xbmc/video/windows/VideoFileItemListModifier.h
new file mode 100644
index 0000000..c4727f9
--- /dev/null
+++ b/xbmc/video/windows/VideoFileItemListModifier.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016-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 "IFileItemListModifier.h"
+
+class CVideoFileItemListModifier : public IFileItemListModifier
+{
+public:
+ CVideoFileItemListModifier() = default;
+ ~CVideoFileItemListModifier() override = default;
+
+ bool CanModify(const CFileItemList &items) const override;
+ bool Modify(CFileItemList &items) const override;
+
+private:
+ static void AddQueuingFolder(CFileItemList & items);
+};
diff --git a/xbmc/view/CMakeLists.txt b/xbmc/view/CMakeLists.txt
new file mode 100644
index 0000000..1370a73
--- /dev/null
+++ b/xbmc/view/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(SOURCES GUIViewControl.cpp
+ GUIViewState.cpp
+ ViewDatabase.cpp
+ ViewStateSettings.cpp)
+
+set(HEADERS GUIViewControl.h
+ GUIViewState.h
+ ViewDatabase.h
+ ViewState.h
+ ViewStateSettings.h)
+
+core_add_library(view)
diff --git a/xbmc/view/GUIViewControl.cpp b/xbmc/view/GUIViewControl.cpp
new file mode 100644
index 0000000..3d4801b
--- /dev/null
+++ b/xbmc/view/GUIViewControl.cpp
@@ -0,0 +1,354 @@
+/*
+ * 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 "GUIViewControl.h"
+
+#include "FileItem.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/IGUIContainer.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+
+#include <utility>
+
+CGUIViewControl::CGUIViewControl(void)
+{
+ m_viewAsControl = -1;
+ m_parentWindow = WINDOW_INVALID;
+ m_fileItems = nullptr;
+ Reset();
+}
+
+CGUIViewControl::~CGUIViewControl(void) = default;
+
+void CGUIViewControl::Reset()
+{
+ m_currentView = -1;
+ m_visibleViews.clear();
+ m_allViews.clear();
+}
+
+void CGUIViewControl::AddView(const CGUIControl *control)
+{
+ if (!control || !control->IsContainer()) return;
+ m_allViews.push_back(const_cast<CGUIControl*>(control));
+}
+
+void CGUIViewControl::SetViewControlID(int control)
+{
+ m_viewAsControl = control;
+}
+
+void CGUIViewControl::SetParentWindow(int window)
+{
+ m_parentWindow = window;
+}
+
+void CGUIViewControl::SetCurrentView(int viewMode, bool bRefresh /* = false */)
+{
+ // grab the previous control
+ CGUIControl *previousView = nullptr;
+ if (m_currentView >= 0 && m_currentView < (int)m_visibleViews.size())
+ previousView = m_visibleViews[m_currentView];
+
+ UpdateViewVisibility();
+
+ // viewMode is of the form TYPE << 16 | ID
+ VIEW_TYPE type = (VIEW_TYPE)(viewMode >> 16);
+ int id = viewMode & 0xffff;
+
+ // first find a view that matches this view, if possible...
+ int newView = GetView(type, id);
+ if (newView < 0) // no suitable view that matches both id and type, so try just type
+ newView = GetView(type, 0);
+ if (newView < 0 && type == VIEW_TYPE_BIG_ICON) // try icon view if they want big icon
+ newView = GetView(VIEW_TYPE_ICON, 0);
+ if (newView < 0 && type == VIEW_TYPE_BIG_INFO)
+ newView = GetView(VIEW_TYPE_INFO, 0);
+ if (newView < 0) // try a list view
+ newView = GetView(VIEW_TYPE_LIST, 0);
+ if (newView < 0) // try anything!
+ newView = GetView(VIEW_TYPE_NONE, 0);
+
+ if (newView < 0)
+ return;
+
+ m_currentView = newView;
+ CGUIControl *pNewView = m_visibleViews[m_currentView];
+
+ // make only current control visible...
+ for (ciViews view = m_allViews.begin(); view != m_allViews.end(); ++view)
+ (*view)->SetVisible(false);
+ pNewView->SetVisible(true);
+
+ if (!bRefresh && pNewView == previousView)
+ return; // no need to actually update anything (other than visibility above)
+
+ // CLog::Log(LOGDEBUG,"SetCurrentView: Oldview: {}, Newview :{}", m_currentView, viewMode);
+
+ bool hasFocus(false);
+ int item = -1;
+ if (previousView)
+ { // have an old view - let's clear it out and hide it.
+ hasFocus = previousView->HasFocus();
+ item = GetSelectedItem(previousView);
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, m_parentWindow, previousView->GetID());
+ previousView->OnMessage(msg);
+ }
+
+ // Update it with the contents
+ UpdateContents(pNewView, item);
+
+ // and focus if necessary
+ if (hasFocus)
+ {
+ CGUIMessage msg(GUI_MSG_SETFOCUS, m_parentWindow, pNewView->GetID(), 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg, m_parentWindow);
+ }
+
+ UpdateViewAsControl(static_cast<IGUIContainer*>(pNewView)->GetLabel());
+}
+
+void CGUIViewControl::SetItems(CFileItemList &items)
+{
+ // CLog::Log(LOGDEBUG,"SetItems: {}", m_currentView);
+ m_fileItems = &items;
+ // update our current view control...
+ UpdateView();
+}
+
+void CGUIViewControl::UpdateContents(const CGUIControl *control, int currentItem) const
+{
+ if (!control || !m_fileItems) return;
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, m_parentWindow, control->GetID(), currentItem, 0, m_fileItems);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg, m_parentWindow);
+}
+
+void CGUIViewControl::UpdateView()
+{
+ // CLog::Log(LOGDEBUG,"UpdateView: {}", m_currentView);
+ if (m_currentView < 0 || m_currentView >= (int)m_visibleViews.size())
+ return; // no valid current view!
+
+ CGUIControl *pControl = m_visibleViews[m_currentView];
+ // get the currently selected item
+ int item = GetSelectedItem(pControl);
+ UpdateContents(pControl, item < 0 ? 0 : item);
+}
+
+int CGUIViewControl::GetSelectedItem(const CGUIControl *control) const
+{
+ if (!control || !m_fileItems)
+ return -1;
+
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, m_parentWindow, control->GetID());
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg, m_parentWindow);
+
+ int iItem = msg.GetParam1();
+ if (iItem >= m_fileItems->Size())
+ return -1;
+
+ return iItem;
+}
+
+int CGUIViewControl::GetSelectedItem() const
+{
+ if (m_currentView < 0 || m_currentView >= (int)m_visibleViews.size())
+ return -1; // no valid current view!
+
+ return GetSelectedItem(m_visibleViews[m_currentView]);
+}
+
+std::string CGUIViewControl::GetSelectedItemPath() const
+{
+ if (m_currentView < 0 || (size_t)m_currentView >= m_visibleViews.size())
+ return "";
+
+ int selectedItem = GetSelectedItem(m_visibleViews[m_currentView]);
+ if (selectedItem > -1)
+ {
+ CFileItemPtr fileItem = m_fileItems->Get(selectedItem);
+ if (fileItem)
+ return fileItem->GetPath();
+ }
+
+ return "";
+}
+
+void CGUIViewControl::SetSelectedItem(int item)
+{
+ if (!m_fileItems || item < 0 || item >= m_fileItems->Size())
+ return;
+
+ if (m_currentView < 0 || m_currentView >= (int)m_visibleViews.size())
+ return; // no valid current view!
+
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, m_parentWindow, m_visibleViews[m_currentView]->GetID(), item);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg, m_parentWindow);
+}
+
+void CGUIViewControl::SetSelectedItem(const std::string &itemPath)
+{
+ if (!m_fileItems || itemPath.empty())
+ return;
+
+ std::string comparePath(itemPath);
+ URIUtils::RemoveSlashAtEnd(comparePath);
+
+ int item = -1;
+ for (int i = 0; i < m_fileItems->Size(); ++i)
+ {
+ std::string strPath =(*m_fileItems)[i]->GetPath();
+ URIUtils::RemoveSlashAtEnd(strPath);
+ if (strPath == comparePath)
+ {
+ item = i;
+ break;
+ }
+ }
+ SetSelectedItem(item);
+}
+
+void CGUIViewControl::SetFocused()
+{
+ if (m_currentView < 0 || m_currentView >= (int)m_visibleViews.size())
+ return; // no valid current view!
+
+ CGUIMessage msg(GUI_MSG_SETFOCUS, m_parentWindow, m_visibleViews[m_currentView]->GetID(), 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg, m_parentWindow);
+}
+
+bool CGUIViewControl::HasControl(int viewControlID) const
+{
+ // run through our controls, checking for the id
+ for (ciViews it = m_allViews.begin(); it != m_allViews.end(); ++it)
+ {
+ if ((*it)->GetID() == viewControlID)
+ return true;
+ }
+ return false;
+}
+
+int CGUIViewControl::GetCurrentControl() const
+{
+ if (m_currentView < 0 || m_currentView >= (int)m_visibleViews.size())
+ return -1; // no valid current view!
+
+ return m_visibleViews[m_currentView]->GetID();
+}
+
+// returns the number-th view's viewmode (type and id)
+int CGUIViewControl::GetViewModeNumber(int number) const
+{
+ IGUIContainer *nextView = nullptr;
+ if (number >= 0 && number < (int)m_visibleViews.size())
+ nextView = static_cast<IGUIContainer*>(m_visibleViews[number]);
+ else if (m_visibleViews.size())
+ nextView = static_cast<IGUIContainer*>(m_visibleViews[0]);
+ if (nextView)
+ return (nextView->GetType() << 16) | nextView->GetID();
+ return 0; // no view modes :(
+}
+
+// returns the amount of visible views
+int CGUIViewControl::GetViewModeCount() const
+{
+ return static_cast<int>(m_visibleViews.size());
+}
+
+int CGUIViewControl::GetViewModeByID(int id) const
+{
+ for (unsigned int i = 0; i < m_visibleViews.size(); ++i)
+ {
+ IGUIContainer *view = static_cast<IGUIContainer*>(m_visibleViews[i]);
+ if (view->GetID() == id)
+ return (view->GetType() << 16) | view->GetID();
+ }
+ return 0; // no view modes :(
+}
+
+// returns the next viewmode in the cycle
+int CGUIViewControl::GetNextViewMode(int direction) const
+{
+ if (!m_visibleViews.size())
+ return 0; // no view modes :(
+
+ int viewNumber = (m_currentView + direction) % (int)m_visibleViews.size();
+ if (viewNumber < 0) viewNumber += m_visibleViews.size();
+ IGUIContainer *nextView = static_cast<IGUIContainer*>(m_visibleViews[viewNumber]);
+ return (nextView->GetType() << 16) | nextView->GetID();
+}
+
+void CGUIViewControl::Clear()
+{
+ if (m_currentView < 0 || m_currentView >= (int)m_visibleViews.size())
+ return; // no valid current view!
+
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, m_parentWindow, m_visibleViews[m_currentView]->GetID(), 0);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg, m_parentWindow);
+}
+
+int CGUIViewControl::GetView(VIEW_TYPE type, int id) const
+{
+ for (int i = 0; i < (int)m_visibleViews.size(); i++)
+ {
+ IGUIContainer *view = static_cast<IGUIContainer*>(m_visibleViews[i]);
+ if ((type == VIEW_TYPE_NONE || type == view->GetType()) && (!id || view->GetID() == id))
+ return i;
+ }
+ return -1;
+}
+
+void CGUIViewControl::UpdateViewAsControl(const std::string &viewLabel)
+{
+ // the view as control could be a select/spin/dropdown button
+ std::vector< std::pair<std::string, int> > labels;
+ for (unsigned int i = 0; i < m_visibleViews.size(); i++)
+ {
+ IGUIContainer *view = static_cast<IGUIContainer*>(m_visibleViews[i]);
+ std::string label = StringUtils::Format(g_localizeStrings.Get(534),
+ view->GetLabel()); // View: {}
+ labels.emplace_back(std::move(label), i);
+ }
+ CGUIMessage msg(GUI_MSG_SET_LABELS, m_parentWindow, m_viewAsControl, m_currentView);
+ msg.SetPointer(&labels);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg, m_parentWindow);
+
+ // otherwise it's just a normal button
+ std::string label = StringUtils::Format(g_localizeStrings.Get(534), viewLabel); // View: {}
+ CGUIMessage msgSet(GUI_MSG_LABEL_SET, m_parentWindow, m_viewAsControl);
+ msgSet.SetLabel(label);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msgSet, m_parentWindow);
+}
+
+void CGUIViewControl::UpdateViewVisibility()
+{
+ // first reset our infomanager cache, as it's likely that the vis conditions
+ // used for views (i.e. based on contenttype) may have changed
+ CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager();
+ infoMgr.ResetCache();
+ infoMgr.GetInfoProviders().GetGUIControlsInfoProvider().ResetContainerMovingCache();
+ m_visibleViews.clear();
+ for (unsigned int i = 0; i < m_allViews.size(); i++)
+ {
+ CGUIControl *view = m_allViews[i];
+ if (view->HasVisibleCondition())
+ {
+ view->UpdateVisibility(nullptr);
+ if (view->IsVisibleFromSkin())
+ m_visibleViews.push_back(view);
+ }
+ else
+ m_visibleViews.push_back(view);
+ }
+}
diff --git a/xbmc/view/GUIViewControl.h b/xbmc/view/GUIViewControl.h
new file mode 100644
index 0000000..40a70f7
--- /dev/null
+++ b/xbmc/view/GUIViewControl.h
@@ -0,0 +1,67 @@
+/*
+ * 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 "windowing/GraphicContext.h" // for VIEW_TYPE
+
+#include <string>
+#include <vector>
+
+class CGUIControl;
+class CFileItemList;
+
+class CGUIViewControl
+{
+public:
+ CGUIViewControl();
+ virtual ~CGUIViewControl();
+
+ void Reset();
+ void SetParentWindow(int window);
+ void AddView(const CGUIControl *control);
+ void SetViewControlID(int control);
+
+ void SetCurrentView(int viewMode, bool bRefresh = false);
+
+ void SetItems(CFileItemList &items);
+
+ void SetSelectedItem(int item);
+ void SetSelectedItem(const std::string &itemPath);
+
+ int GetSelectedItem() const;
+ std::string GetSelectedItemPath() const;
+ void SetFocused();
+
+ bool HasControl(int controlID) const;
+ int GetNextViewMode(int direction = 1) const;
+ int GetViewModeNumber(int number) const;
+ int GetViewModeCount() const;
+ int GetViewModeByID(int id) const;
+
+ int GetCurrentControl() const;
+
+ void Clear();
+
+protected:
+ int GetSelectedItem(const CGUIControl *control) const;
+ void UpdateContents(const CGUIControl *control, int currentItem) const;
+ void UpdateView();
+ void UpdateViewAsControl(const std::string &viewLabel);
+ void UpdateViewVisibility();
+ int GetView(VIEW_TYPE type, int id) const;
+
+ std::vector<CGUIControl*> m_allViews;
+ std::vector<CGUIControl*> m_visibleViews;
+ typedef std::vector<CGUIControl*>::const_iterator ciViews;
+
+ CFileItemList* m_fileItems;
+ int m_viewAsControl;
+ int m_parentWindow;
+ int m_currentView;
+};
diff --git a/xbmc/view/GUIViewState.cpp b/xbmc/view/GUIViewState.cpp
new file mode 100644
index 0000000..358bb63
--- /dev/null
+++ b/xbmc/view/GUIViewState.cpp
@@ -0,0 +1,637 @@
+/*
+ * 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 "view/GUIViewState.h"
+
+#include "AutoSwitch.h"
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "ViewDatabase.h"
+#include "addons/Addon.h"
+#include "addons/AddonManager.h"
+#include "addons/PluginSource.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIViewStateAddonBrowser.h"
+#include "dialogs/GUIDialogSelect.h"
+#include "events/windows/GUIViewStateEventLog.h"
+#include "favourites/GUIViewStateFavourites.h"
+#include "games/windows/GUIViewStateWindowGames.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/TextureManager.h"
+#include "music/GUIViewStateMusic.h"
+#include "pictures/GUIViewStatePictures.h"
+#include "profiles/ProfileManager.h"
+#include "programs/GUIViewStatePrograms.h"
+#include "pvr/windows/GUIViewStatePVR.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/SettingUtils.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/URIUtils.h"
+#include "video/GUIViewStateVideo.h"
+#include "view/ViewState.h"
+
+#define PROPERTY_SORT_ORDER "sort.order"
+#define PROPERTY_SORT_ASCENDING "sort.ascending"
+
+using namespace KODI;
+using namespace ADDON;
+using namespace PVR;
+
+std::string CGUIViewState::m_strPlaylistDirectory;
+VECSOURCES CGUIViewState::m_sources;
+
+static const int SETTING_AUTOPLAYNEXT_MUSICVIDEOS = 0;
+static const int SETTING_AUTOPLAYNEXT_TVSHOWS = 1;
+static const int SETTING_AUTOPLAYNEXT_EPISODES = 2;
+static const int SETTING_AUTOPLAYNEXT_MOVIES = 3;
+static const int SETTING_AUTOPLAYNEXT_UNCATEGORIZED = 4;
+
+CGUIViewState* CGUIViewState::GetViewState(int windowId, const CFileItemList& items)
+{
+ // don't expect derived classes to clear the sources
+ m_sources.clear();
+
+ if (windowId == 0)
+ return GetViewState(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(),items);
+
+ const CURL url=items.GetURL();
+
+ if (items.IsAddonsPath())
+ return new CGUIViewStateAddonBrowser(items);
+
+ if (items.HasSortDetails())
+ return new CGUIViewStateFromItems(items);
+
+ if (url.IsProtocol("musicdb"))
+ return new CGUIViewStateMusicDatabase(items);
+
+ if (url.IsProtocol("musicsearch"))
+ return new CGUIViewStateMusicSearch(items);
+
+ if (items.IsSmartPlayList() || url.IsProtocol("upnp") ||
+ items.IsLibraryFolder())
+ {
+ if (items.GetContent() == "songs" ||
+ items.GetContent() == "albums" ||
+ items.GetContent() == "mixed")
+ return new CGUIViewStateMusicSmartPlaylist(items);
+ else if (items.GetContent() == "musicvideos")
+ return new CGUIViewStateVideoMusicVideos(items);
+ else if (items.GetContent() == "tvshows")
+ return new CGUIViewStateVideoTVShows(items);
+ else if (items.GetContent() == "episodes")
+ return new CGUIViewStateVideoEpisodes(items);
+ else if (items.GetContent() == "movies")
+ return new CGUIViewStateVideoMovies(items);
+ }
+
+ if (url.IsProtocol("library"))
+ return new CGUIViewStateLibrary(items);
+
+ if (items.IsPlayList())
+ {
+ // Playlists (like .strm) can be music or video type
+ if (windowId == WINDOW_VIDEO_NAV)
+ return new CGUIViewStateVideoPlaylist(items);
+ else
+ return new CGUIViewStateMusicPlaylist(items);
+ }
+
+ if (items.GetPath() == "special://musicplaylists/")
+ return new CGUIViewStateWindowMusicNav(items);
+
+ if (url.IsProtocol("androidapp"))
+ return new CGUIViewStateWindowPrograms(items);
+
+ if (url.IsProtocol("activities"))
+ return new CGUIViewStateEventLog(items);
+
+ if (windowId == WINDOW_MUSIC_NAV)
+ return new CGUIViewStateWindowMusicNav(items);
+
+ if (windowId == WINDOW_MUSIC_PLAYLIST)
+ return new CGUIViewStateWindowMusicPlaylist(items);
+
+ if (windowId == WINDOW_MUSIC_PLAYLIST_EDITOR)
+ return new CGUIViewStateWindowMusicNav(items);
+
+ if (windowId == WINDOW_VIDEO_NAV)
+ return new CGUIViewStateWindowVideoNav(items);
+
+ if (windowId == WINDOW_VIDEO_PLAYLIST)
+ return new CGUIViewStateWindowVideoPlaylist(items);
+
+ if (windowId == WINDOW_TV_CHANNELS)
+ return new CGUIViewStateWindowPVRChannels(windowId, items);
+
+ if (windowId == WINDOW_TV_RECORDINGS)
+ return new CGUIViewStateWindowPVRRecordings(windowId, items);
+
+ if (windowId == WINDOW_TV_GUIDE)
+ return new CGUIViewStateWindowPVRGuide(windowId, items);
+
+ if (windowId == WINDOW_TV_TIMERS)
+ return new CGUIViewStateWindowPVRTimers(windowId, items);
+
+ if (windowId == WINDOW_TV_TIMER_RULES)
+ return new CGUIViewStateWindowPVRTimers(windowId, items);
+
+ if (windowId == WINDOW_TV_SEARCH)
+ return new CGUIViewStateWindowPVRSearch(windowId, items);
+
+ if (windowId == WINDOW_RADIO_CHANNELS)
+ return new CGUIViewStateWindowPVRChannels(windowId, items);
+
+ if (windowId == WINDOW_RADIO_RECORDINGS)
+ return new CGUIViewStateWindowPVRRecordings(windowId, items);
+
+ if (windowId == WINDOW_RADIO_GUIDE)
+ return new CGUIViewStateWindowPVRGuide(windowId, items);
+
+ if (windowId == WINDOW_RADIO_TIMERS)
+ return new CGUIViewStateWindowPVRTimers(windowId, items);
+
+ if (windowId == WINDOW_RADIO_TIMER_RULES)
+ return new CGUIViewStateWindowPVRTimers(windowId, items);
+
+ if (windowId == WINDOW_RADIO_SEARCH)
+ return new CGUIViewStateWindowPVRSearch(windowId, items);
+
+ if (windowId == WINDOW_PICTURES)
+ return new CGUIViewStateWindowPictures(items);
+
+ if (windowId == WINDOW_PROGRAMS)
+ return new CGUIViewStateWindowPrograms(items);
+
+ if (windowId == WINDOW_GAMES)
+ return new GAME::CGUIViewStateWindowGames(items);
+
+ if (windowId == WINDOW_ADDON_BROWSER)
+ return new CGUIViewStateAddonBrowser(items);
+
+ if (windowId == WINDOW_EVENT_LOG)
+ return new CGUIViewStateEventLog(items);
+
+ if (windowId == WINDOW_FAVOURITES)
+ return new CGUIViewStateFavourites(items);
+
+ // Use as fallback/default
+ return new CGUIViewStateGeneral(items);
+}
+
+CGUIViewState::CGUIViewState(const CFileItemList& items) : m_items(items)
+{
+ m_currentViewAsControl = 0;
+ m_currentSortMethod = 0;
+ m_playlist = PLAYLIST::TYPE_NONE;
+}
+
+CGUIViewState::~CGUIViewState() = default;
+
+SortOrder CGUIViewState::SetNextSortOrder()
+{
+ if (GetSortOrder() == SortOrderAscending)
+ SetSortOrder(SortOrderDescending);
+ else
+ SetSortOrder(SortOrderAscending);
+
+ SaveViewState();
+
+ return GetSortOrder();
+}
+
+SortOrder CGUIViewState::GetSortOrder() const
+{
+ if (m_currentSortMethod >= 0 && m_currentSortMethod < (int)m_sortMethods.size())
+ return m_sortMethods[m_currentSortMethod].m_sortDescription.sortOrder;
+
+ return SortOrderAscending;
+}
+
+int CGUIViewState::GetSortOrderLabel() const
+{
+ if (m_currentSortMethod >= 0 && m_currentSortMethod < (int)m_sortMethods.size())
+ if (m_sortMethods[m_currentSortMethod].m_sortDescription.sortOrder == SortOrderDescending)
+ return 585;
+
+ return 584; // default sort order label 'Ascending'
+}
+
+int CGUIViewState::GetViewAsControl() const
+{
+ return m_currentViewAsControl;
+}
+
+void CGUIViewState::SetViewAsControl(int viewAsControl)
+{
+ if (viewAsControl == DEFAULT_VIEW_AUTO)
+ m_currentViewAsControl = CAutoSwitch::GetView(m_items);
+ else
+ m_currentViewAsControl = viewAsControl;
+}
+
+void CGUIViewState::SaveViewAsControl(int viewAsControl)
+{
+ SetViewAsControl(viewAsControl);
+ SaveViewState();
+}
+
+SortDescription CGUIViewState::GetSortMethod() const
+{
+ SortDescription sorting;
+ if (m_currentSortMethod >= 0 && m_currentSortMethod < (int)m_sortMethods.size())
+ sorting = m_sortMethods[m_currentSortMethod].m_sortDescription;
+
+ return sorting;
+}
+
+bool CGUIViewState::HasMultipleSortMethods() const
+{
+ return m_sortMethods.size() > 1;
+}
+
+int CGUIViewState::GetSortMethodLabel() const
+{
+ if (m_currentSortMethod >= 0 && m_currentSortMethod < (int)m_sortMethods.size())
+ return m_sortMethods[m_currentSortMethod].m_buttonLabel;
+
+ return 551; // default sort method label 'Name'
+}
+
+void CGUIViewState::GetSortMethodLabelMasks(LABEL_MASKS& masks) const
+{
+ if (m_currentSortMethod >= 0 && m_currentSortMethod < (int)m_sortMethods.size())
+ {
+ masks = m_sortMethods[m_currentSortMethod].m_labelMasks;
+ return;
+ }
+
+ masks.m_strLabelFile.clear();
+ masks.m_strLabel2File.clear();
+ masks.m_strLabelFolder.clear();
+ masks.m_strLabel2Folder.clear();
+}
+
+std::vector<SortDescription> CGUIViewState::GetSortDescriptions() const
+{
+ std::vector<SortDescription> descriptions;
+ for (const auto& desc : m_sortMethods)
+ {
+ descriptions.emplace_back(desc.m_sortDescription);
+ }
+ return descriptions;
+}
+
+void CGUIViewState::AddSortMethod(SortBy sortBy, int buttonLabel, const LABEL_MASKS &labelMasks, SortAttribute sortAttributes /* = SortAttributeNone */, SortOrder sortOrder /* = SortOrderNone */)
+{
+ AddSortMethod(sortBy, sortAttributes, buttonLabel, labelMasks, sortOrder);
+}
+
+void CGUIViewState::AddSortMethod(SortBy sortBy, SortAttribute sortAttributes, int buttonLabel, const LABEL_MASKS &labelMasks, SortOrder sortOrder /* = SortOrderNone */)
+{
+ for (size_t i = 0; i < m_sortMethods.size(); ++i)
+ if (m_sortMethods[i].m_sortDescription.sortBy == sortBy)
+ return;
+
+ // handle unspecified sort order
+ if (sortBy != SortByNone && sortOrder == SortOrderNone)
+ {
+ // the following sort methods are sorted in descending order by default
+ if (sortBy == SortByDate || sortBy == SortBySize || sortBy == SortByPlaycount ||
+ sortBy == SortByRating || sortBy == SortByProgramCount ||
+ sortBy == SortByBitrate || sortBy == SortByListeners ||
+ sortBy == SortByUserRating || sortBy == SortByLastPlayed)
+ sortOrder = SortOrderDescending;
+ else
+ sortOrder = SortOrderAscending;
+ }
+
+ GUIViewSortDetails sort;
+ sort.m_sortDescription.sortBy = sortBy;
+ sort.m_sortDescription.sortOrder = sortOrder;
+ sort.m_sortDescription.sortAttributes = sortAttributes;
+ sort.m_buttonLabel = buttonLabel;
+ sort.m_labelMasks = labelMasks;
+ m_sortMethods.push_back(sort);
+}
+
+void CGUIViewState::AddSortMethod(SortDescription sortDescription, int buttonLabel, const LABEL_MASKS &labelMasks)
+{
+ AddSortMethod(sortDescription.sortBy, sortDescription.sortAttributes, buttonLabel, labelMasks, sortDescription.sortOrder);
+}
+
+void CGUIViewState::SetCurrentSortMethod(int method)
+{
+ SortBy sortBy = (SortBy)method;
+ if (sortBy < SortByNone || sortBy > SortByLastUsed)
+ return; // invalid
+
+ SetSortMethod(sortBy);
+ SaveViewState();
+}
+
+void CGUIViewState::SetSortMethod(SortBy sortBy, SortOrder sortOrder /* = SortOrderNone */)
+{
+ for (int i = 0; i < (int)m_sortMethods.size(); ++i)
+ {
+ if (m_sortMethods[i].m_sortDescription.sortBy == sortBy)
+ {
+ m_currentSortMethod = i;
+ break;
+ }
+ }
+
+ if (sortOrder != SortOrderNone)
+ SetSortOrder(sortOrder);
+}
+
+void CGUIViewState::SetSortMethod(SortDescription sortDescription)
+{
+ return SetSortMethod(sortDescription.sortBy, sortDescription.sortOrder);
+}
+
+bool CGUIViewState::ChooseSortMethod()
+{
+
+ CGUIDialogSelect *dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>(WINDOW_DIALOG_SELECT);
+ if (!dialog)
+ return false;
+ dialog->Reset();
+ dialog->SetHeading(CVariant{ 39010 }); // Label "Sort by"
+ for (auto &sortMethod : m_sortMethods)
+ dialog->Add(g_localizeStrings.Get(sortMethod.m_buttonLabel));
+ dialog->SetSelected(m_currentSortMethod);
+ dialog->Open();
+ int newSelected = dialog->GetSelectedItem();
+ // check if selection has changed
+ if (!dialog->IsConfirmed() || newSelected < 0 || newSelected == m_currentSortMethod)
+ return false;
+
+ m_currentSortMethod = newSelected;
+ SaveViewState();
+ return true;
+}
+
+SortDescription CGUIViewState::SetNextSortMethod(int direction /* = 1 */)
+{
+ m_currentSortMethod += direction;
+
+ if (m_currentSortMethod >= (int)m_sortMethods.size())
+ m_currentSortMethod = 0;
+ if (m_currentSortMethod < 0)
+ m_currentSortMethod = m_sortMethods.size() ? (int)m_sortMethods.size() - 1 : 0;
+
+ SaveViewState();
+
+ return GetSortMethod();
+}
+
+bool CGUIViewState::HideExtensions()
+{
+ return !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWEXTENSIONS);
+}
+
+bool CGUIViewState::HideParentDirItems()
+{
+ return !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWPARENTDIRITEMS);
+}
+
+bool CGUIViewState::DisableAddSourceButtons()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (profileManager->GetCurrentProfile().canWriteSources() || g_passwordManager.bMasterUser)
+ return !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWADDSOURCEBUTTONS);
+
+ return true;
+}
+
+PLAYLIST::Id CGUIViewState::GetPlaylist() const
+{
+ return m_playlist;
+}
+
+const std::string& CGUIViewState::GetPlaylistDirectory()
+{
+ return m_strPlaylistDirectory;
+}
+
+void CGUIViewState::SetPlaylistDirectory(const std::string& strDirectory)
+{
+ m_strPlaylistDirectory = strDirectory;
+ URIUtils::RemoveSlashAtEnd(m_strPlaylistDirectory);
+}
+
+bool CGUIViewState::IsCurrentPlaylistDirectory(const std::string& strDirectory)
+{
+ if (CServiceBroker::GetPlaylistPlayer().GetCurrentPlaylist()!=GetPlaylist())
+ return false;
+
+ std::string strDir = strDirectory;
+ URIUtils::RemoveSlashAtEnd(strDir);
+
+ return m_strPlaylistDirectory == strDir;
+}
+
+bool CGUIViewState::AutoPlayNextItem()
+{
+ return false;
+}
+
+std::string CGUIViewState::GetLockType()
+{
+ return "";
+}
+
+std::string CGUIViewState::GetExtensions()
+{
+ return "";
+}
+
+VECSOURCES& CGUIViewState::GetSources()
+{
+ return m_sources;
+}
+
+void CGUIViewState::AddLiveTVSources()
+{
+ VECSOURCES *sources = CMediaSourceSettings::GetInstance().GetSources("video");
+ for (IVECSOURCES it = sources->begin(); it != sources->end(); it++)
+ {
+ if (URIUtils::IsLiveTV((*it).strPath))
+ {
+ CMediaSource source;
+ source.strPath = (*it).strPath;
+ source.strName = (*it).strName;
+ source.vecPaths = (*it).vecPaths;
+ source.m_strThumbnailImage = "";
+ source.FromNameAndPaths("video", source.strName, source.vecPaths);
+ m_sources.push_back(source);
+ }
+ }
+}
+
+void CGUIViewState::SetSortOrder(SortOrder sortOrder)
+{
+ if (sortOrder == SortOrderNone)
+ return;
+
+ if (m_currentSortMethod < 0 || m_currentSortMethod >= (int)m_sortMethods.size())
+ return;
+
+ m_sortMethods[m_currentSortMethod].m_sortDescription.sortOrder = sortOrder;
+}
+
+bool CGUIViewState::AutoPlayNextVideoItem() const
+{
+ if (GetPlaylist() != PLAYLIST::TYPE_VIDEO)
+ return false;
+
+ int settingValue(-1);
+ if (m_items.GetContent() == "musicvideos")
+ settingValue = SETTING_AUTOPLAYNEXT_MUSICVIDEOS;
+ else if (m_items.GetContent() == "tvshows")
+ settingValue = SETTING_AUTOPLAYNEXT_TVSHOWS;
+ else if (m_items.GetContent() == "episodes")
+ settingValue = SETTING_AUTOPLAYNEXT_EPISODES;
+ else if (m_items.GetContent() == "movies")
+ settingValue = SETTING_AUTOPLAYNEXT_MOVIES;
+ else
+ settingValue = SETTING_AUTOPLAYNEXT_UNCATEGORIZED;
+
+ const auto setting = std::dynamic_pointer_cast<CSettingList>(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(
+ CSettings::SETTING_VIDEOPLAYER_AUTOPLAYNEXTITEM));
+
+ return settingValue >= 0 && setting != nullptr &&
+ CSettingUtils::FindIntInList(setting, settingValue);
+}
+
+void CGUIViewState::LoadViewState(const std::string &path, int windowID)
+{ // get our view state from the db
+ CViewDatabase db;
+ if (!db.Open())
+ return;
+
+ CViewState state;
+ if (db.GetViewState(path, windowID, state, CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN)) ||
+ db.GetViewState(path, windowID, state, ""))
+ {
+ SetViewAsControl(state.m_viewMode);
+ SetSortMethod(state.m_sortDescription);
+ }
+}
+
+void CGUIViewState::SaveViewToDb(const std::string &path, int windowID, CViewState *viewState)
+{
+ CViewDatabase db;
+ if (!db.Open())
+ return;
+
+ SortDescription sorting = GetSortMethod();
+ CViewState state(m_currentViewAsControl, sorting.sortBy, sorting.sortOrder, sorting.sortAttributes);
+ if (viewState != NULL)
+ *viewState = state;
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ db.SetViewState(path, windowID, state, settings->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN));
+ db.Close();
+
+ if (viewState != NULL)
+ settings->Save();
+}
+
+void CGUIViewState::AddPlaylistOrder(const CFileItemList& items, const LABEL_MASKS& label_masks)
+{
+ SortBy sortBy = SortByPlaylistOrder;
+ int sortLabel = 559;
+ SortOrder sortOrder = SortOrderAscending;
+ if (items.HasProperty(PROPERTY_SORT_ORDER))
+ {
+ sortBy = (SortBy)items.GetProperty(PROPERTY_SORT_ORDER).asInteger();
+ if (sortBy != SortByNone)
+ {
+ sortLabel = SortUtils::GetSortLabel(sortBy);
+ sortOrder = items.GetProperty(PROPERTY_SORT_ASCENDING).asBoolean() ? SortOrderAscending : SortOrderDescending;
+ }
+ }
+
+ AddSortMethod(sortBy, sortLabel, label_masks, SortAttributeNone, sortOrder);
+ SetSortMethod(sortBy, sortOrder);
+}
+
+CGUIViewStateGeneral::CGUIViewStateGeneral(const CFileItemList& items) : CGUIViewState(items)
+{
+ AddSortMethod(SortByLabel, 551, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, size | Foldername, empty
+ SetSortMethod(SortByLabel);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+}
+
+CGUIViewStateFromItems::CGUIViewStateFromItems(const CFileItemList &items) : CGUIViewState(items)
+{
+ const std::vector<GUIViewSortDetails> &details = items.GetSortDetails();
+ for (unsigned int i = 0; i < details.size(); i++)
+ {
+ const GUIViewSortDetails& sort = details[i];
+ AddSortMethod(sort.m_sortDescription, sort.m_buttonLabel, sort.m_labelMasks);
+ }
+ //! @todo Should default sort/view mode be specified?
+ m_currentSortMethod = 0;
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ if (items.IsPlugin())
+ {
+ CURL url(items.GetPath());
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), addon, AddonType::PLUGIN,
+ OnlyEnabled::CHOICE_YES))
+ {
+ const auto plugin = std::static_pointer_cast<CPluginSource>(addon);
+ if (plugin->Provides(CPluginSource::AUDIO))
+ m_playlist = PLAYLIST::TYPE_MUSIC;
+ if (plugin->Provides(CPluginSource::VIDEO))
+ m_playlist = PLAYLIST::TYPE_VIDEO;
+ }
+ }
+
+ LoadViewState(items.GetPath(), CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+}
+
+bool CGUIViewStateFromItems::AutoPlayNextItem()
+{
+ return AutoPlayNextVideoItem();
+}
+
+void CGUIViewStateFromItems::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+}
+
+CGUIViewStateLibrary::CGUIViewStateLibrary(const CFileItemList &items) : CGUIViewState(items)
+{
+ AddSortMethod(SortByNone, 551, LABEL_MASKS("%F", "%I", "%L", "")); // Filename, Size | Foldername, empty
+ SetSortMethod(SortByNone);
+
+ SetViewAsControl(DEFAULT_VIEW_LIST);
+
+ LoadViewState(items.GetPath(), CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+}
+
+void CGUIViewStateLibrary::SaveViewState()
+{
+ SaveViewToDb(m_items.GetPath(), CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
+}
diff --git a/xbmc/view/GUIViewState.h b/xbmc/view/GUIViewState.h
new file mode 100644
index 0000000..8723081
--- /dev/null
+++ b/xbmc/view/GUIViewState.h
@@ -0,0 +1,126 @@
+/*
+ * 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 "MediaSource.h"
+#include "utils/LabelFormatter.h"
+#include "utils/SortUtils.h"
+
+#include <vector>
+
+class CViewState; // forward
+class CFileItemList;
+
+namespace PLAYLIST
+{
+using Id = int;
+} // namespace PLAYLIST
+
+class CGUIViewState
+{
+public:
+ virtual ~CGUIViewState();
+ static CGUIViewState* GetViewState(int windowId, const CFileItemList& items);
+
+ void SetViewAsControl(int viewAsControl);
+ void SaveViewAsControl(int viewAsControl);
+ int GetViewAsControl() const;
+
+ bool ChooseSortMethod();
+ SortDescription SetNextSortMethod(int direction = 1);
+ void SetCurrentSortMethod(int method);
+ SortDescription GetSortMethod() const;
+ bool HasMultipleSortMethods() const;
+ int GetSortMethodLabel() const;
+ int GetSortOrderLabel() const;
+ void GetSortMethodLabelMasks(LABEL_MASKS& masks) const;
+
+ std::vector<SortDescription> GetSortDescriptions() const;
+
+ SortOrder SetNextSortOrder();
+ SortOrder GetSortOrder() const;
+
+ virtual bool HideExtensions();
+ virtual bool HideParentDirItems();
+ virtual bool DisableAddSourceButtons();
+
+ virtual PLAYLIST::Id GetPlaylist() const;
+ const std::string& GetPlaylistDirectory();
+ void SetPlaylistDirectory(const std::string& strDirectory);
+ bool IsCurrentPlaylistDirectory(const std::string& strDirectory);
+ virtual bool AutoPlayNextItem();
+
+ virtual std::string GetLockType();
+ virtual std::string GetExtensions();
+ virtual VECSOURCES& GetSources();
+
+protected:
+ explicit CGUIViewState(const CFileItemList& items); // no direct object creation, use GetViewState()
+
+ virtual void SaveViewState() = 0;
+ virtual void SaveViewToDb(const std::string &path, int windowID, CViewState *viewState = NULL);
+ void LoadViewState(const std::string &path, int windowID);
+
+ void AddLiveTVSources();
+
+ /*! \brief Add the sort order defined in a smartplaylist
+ Defaults to SORT_METHOD_PLAYLIST_ORDER if no order is defined.
+ \param items the list of items for the view state.
+ \param label_mask the label masks for formatting items.
+ */
+ void AddPlaylistOrder(const CFileItemList& items, const LABEL_MASKS& label_masks);
+
+ void AddSortMethod(SortBy sortBy, int buttonLabel, const LABEL_MASKS &labelMasks, SortAttribute sortAttributes = SortAttributeNone, SortOrder sortOrder = SortOrderNone);
+ void AddSortMethod(SortBy sortBy, SortAttribute sortAttributes, int buttonLabel, const LABEL_MASKS &labelMasks, SortOrder sortOrder = SortOrderNone);
+ void AddSortMethod(SortDescription sortDescription, int buttonLabel, const LABEL_MASKS &labelMasks);
+ void SetSortMethod(SortBy sortBy, SortOrder sortOrder = SortOrderNone);
+ void SetSortMethod(SortDescription sortDescription);
+ void SetSortOrder(SortOrder sortOrder);
+
+ bool AutoPlayNextVideoItem() const;
+
+ const CFileItemList& m_items;
+
+ int m_currentViewAsControl;
+ PLAYLIST::Id m_playlist;
+
+ std::vector<GUIViewSortDetails> m_sortMethods;
+ int m_currentSortMethod;
+
+ static VECSOURCES m_sources;
+ static std::string m_strPlaylistDirectory;
+};
+
+class CGUIViewStateGeneral : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateGeneral(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override { }
+};
+
+class CGUIViewStateFromItems : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateFromItems(const CFileItemList& items);
+ bool AutoPlayNextItem() override;
+
+protected:
+ void SaveViewState() override;
+};
+
+class CGUIViewStateLibrary : public CGUIViewState
+{
+public:
+ explicit CGUIViewStateLibrary(const CFileItemList& items);
+
+protected:
+ void SaveViewState() override;
+};
diff --git a/xbmc/view/ViewDatabase.cpp b/xbmc/view/ViewDatabase.cpp
new file mode 100644
index 0000000..7605cfb
--- /dev/null
+++ b/xbmc/view/ViewDatabase.cpp
@@ -0,0 +1,208 @@
+/*
+ * 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 "ViewDatabase.h"
+
+#include <utility>
+
+#include "dbwrappers/dataset.h"
+#include "SortFileItem.h"
+#include "utils/LegacyPathTranslation.h"
+#include "utils/log.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "view/ViewState.h"
+
+#ifdef TARGET_POSIX
+#include "platform/posix/ConvUtils.h"
+#endif
+CViewDatabase::CViewDatabase(void) = default;
+
+CViewDatabase::~CViewDatabase(void) = default;
+
+bool CViewDatabase::Open()
+{
+ return CDatabase::Open();
+}
+
+void CViewDatabase::CreateTables()
+{
+ CLog::Log(LOGINFO, "create view table");
+ m_pDS->exec("CREATE TABLE view ("
+ "idView integer primary key,"
+ "window integer,"
+ "path text,"
+ "viewMode integer,"
+ "sortMethod integer,"
+ "sortOrder integer,"
+ "sortAttributes integer,"
+ "skin text)");
+}
+
+void CViewDatabase::CreateAnalytics()
+{
+ CLog::Log(LOGINFO, "{} - creating indices", __FUNCTION__);
+ m_pDS->exec("CREATE INDEX idxViews ON view(path)");
+ m_pDS->exec("CREATE INDEX idxViewsWindow ON view(window)");
+}
+
+void CViewDatabase::UpdateTables(int version)
+{
+ if (version < 4)
+ m_pDS->exec("alter table view add skin text");
+ if (version < 5)
+ {
+ // translate legacy videodb:// and musicdb:// paths
+ std::vector< std::pair<int, std::string> > paths;
+ if (m_pDS->query("SELECT idView, path FROM view"))
+ {
+ while (!m_pDS->eof())
+ {
+ std::string originalPath = m_pDS->fv(1).get_asString();
+ std::string path = originalPath;
+ if (StringUtils::StartsWithNoCase(path, "musicdb://"))
+ path = CLegacyPathTranslation::TranslateMusicDbPath(path);
+ else if (StringUtils::StartsWithNoCase(path, "videodb://"))
+ path = CLegacyPathTranslation::TranslateVideoDbPath(path);
+
+ if (!StringUtils::EqualsNoCase(path, originalPath))
+ paths.emplace_back(m_pDS->fv(0).get_asInt(), path);
+ m_pDS->next();
+ }
+ m_pDS->close();
+
+ for (std::vector< std::pair<int, std::string> >::const_iterator it = paths.begin(); it != paths.end(); ++it)
+ m_pDS->exec(PrepareSQL("UPDATE view SET path='%s' WHERE idView=%d", it->second.c_str(), it->first));
+ }
+ }
+ if (version < 6)
+ {
+ // convert the "path" table
+ m_pDS->exec("ALTER TABLE view RENAME TO tmp_view");
+
+ m_pDS->exec("CREATE TABLE view ("
+ "idView integer primary key,"
+ "window integer,"
+ "path text,"
+ "viewMode integer,"
+ "sortMethod integer,"
+ "sortOrder integer,"
+ "sortAttributes integer,"
+ "skin text)\n");
+
+ m_pDS->query("SELECT * FROM tmp_view");
+ while (!m_pDS->eof())
+ {
+ SortDescription sorting = SortUtils::TranslateOldSortMethod((SORT_METHOD)m_pDS->fv(4).get_asInt());
+
+ std::string sql = PrepareSQL("INSERT INTO view (idView, window, path, viewMode, sortMethod, sortOrder, sortAttributes, skin) VALUES (%i, %i, '%s', %i, %i, %i, %i, '%s')",
+ m_pDS->fv(0).get_asInt(), m_pDS->fv(1).get_asInt(), m_pDS->fv(2).get_asString().c_str(), m_pDS->fv(3).get_asInt(),
+ (int)sorting.sortBy, m_pDS->fv(5).get_asInt(), (int)sorting.sortAttributes, m_pDS->fv(6).get_asString().c_str());
+ m_pDS2->exec(sql);
+
+ m_pDS->next();
+ }
+ m_pDS->exec("DROP TABLE tmp_view");
+ }
+}
+
+bool CViewDatabase::GetViewState(const std::string &path, int window, CViewState &state, const std::string &skin)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string path1(path);
+ URIUtils::AddSlashAtEnd(path1);
+ if (path1.empty()) path1 = "root://";
+
+ std::string sql;
+ if (skin.empty())
+ sql = PrepareSQL("select * from view where window = %i and path='%s'", window, path1.c_str());
+ else
+ sql = PrepareSQL("select * from view where window = %i and path='%s' and skin='%s'", window, path1.c_str(), skin.c_str());
+ m_pDS->query(sql);
+
+ if (!m_pDS->eof())
+ { // have some information
+ state.m_viewMode = m_pDS->fv("viewMode").get_asInt();
+ state.m_sortDescription.sortBy = (SortBy)m_pDS->fv("sortMethod").get_asInt();
+ state.m_sortDescription.sortOrder = (SortOrder)m_pDS->fv("sortOrder").get_asInt();
+ state.m_sortDescription.sortAttributes = (SortAttribute)m_pDS->fv("sortAttributes").get_asInt();
+ m_pDS->close();
+ return true;
+ }
+ m_pDS->close();
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{}, failed on path '{}'", __FUNCTION__, path);
+ }
+ return false;
+}
+
+bool CViewDatabase::SetViewState(const std::string &path, int window, const CViewState &state, const std::string &skin)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string path1(path);
+ URIUtils::AddSlashAtEnd(path1);
+ if (path1.empty()) path1 = "root://";
+
+ std::string sql = PrepareSQL("select idView from view where window = %i and path='%s' and skin='%s'", window, path1.c_str(), skin.c_str());
+ m_pDS->query(sql);
+ if (!m_pDS->eof())
+ { // update the view
+ int idView = m_pDS->fv("idView").get_asInt();
+ m_pDS->close();
+ sql = PrepareSQL("update view set viewMode=%i,sortMethod=%i,sortOrder=%i,sortAttributes=%i where idView=%i",
+ state.m_viewMode, (int)state.m_sortDescription.sortBy, (int)state.m_sortDescription.sortOrder, (int)state.m_sortDescription.sortAttributes, idView);
+ m_pDS->exec(sql);
+ }
+ else
+ { // add the view
+ m_pDS->close();
+ sql = PrepareSQL("insert into view (idView, path, window, viewMode, sortMethod, sortOrder, sortAttributes, skin) values(NULL, '%s', %i, %i, %i, %i, %i, '%s')",
+ path1.c_str(), window, state.m_viewMode, (int)state.m_sortDescription.sortBy, (int)state.m_sortDescription.sortOrder, (int)state.m_sortDescription.sortAttributes, skin.c_str());
+ m_pDS->exec(sql);
+ }
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on path '{}'", __FUNCTION__, path);
+ }
+ return true;
+}
+
+bool CViewDatabase::ClearViewStates(int windowID)
+{
+ try
+ {
+ if (nullptr == m_pDB)
+ return false;
+ if (nullptr == m_pDS)
+ return false;
+
+ std::string sql = PrepareSQL("delete from view where window = %i", windowID);
+ m_pDS->exec(sql);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "{} failed on window '{}'", __FUNCTION__, windowID);
+ }
+ return true;
+}
diff --git a/xbmc/view/ViewDatabase.h b/xbmc/view/ViewDatabase.h
new file mode 100644
index 0000000..701d1f6
--- /dev/null
+++ b/xbmc/view/ViewDatabase.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "dbwrappers/Database.h"
+
+#include <string>
+
+class CViewState;
+
+class CViewDatabase : public CDatabase
+{
+public:
+ CViewDatabase();
+ ~CViewDatabase() override;
+ bool Open() override;
+
+ bool GetViewState(const std::string &path, int windowID, CViewState &state, const std::string &skin);
+ bool SetViewState(const std::string &path, int windowID, const CViewState &state, const std::string &skin);
+ bool ClearViewStates(int windowID);
+
+protected:
+ void CreateTables() override;
+ void CreateAnalytics() override;
+ void UpdateTables(int version) override;
+ int GetSchemaVersion() const override { return 6; }
+ const char *GetBaseDBName() const override { return "ViewModes"; }
+};
diff --git a/xbmc/view/ViewState.h b/xbmc/view/ViewState.h
new file mode 100644
index 0000000..1ae0737
--- /dev/null
+++ b/xbmc/view/ViewState.h
@@ -0,0 +1,40 @@
+/*
+ * 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 "utils/SortUtils.h"
+
+#define DEFAULT_VIEW_AUTO (VIEW_TYPE_AUTO << 16)
+#define DEFAULT_VIEW_LIST (VIEW_TYPE_LIST << 16)
+#define DEFAULT_VIEW_ICONS (VIEW_TYPE_ICON << 16)
+#define DEFAULT_VIEW_BIG_ICONS (VIEW_TYPE_BIG_ICON << 16)
+#define DEFAULT_VIEW_INFO (VIEW_TYPE_INFO << 16)
+#define DEFAULT_VIEW_BIG_INFO (VIEW_TYPE_BIG_INFO << 16)
+#define DEFAULT_VIEW_MAX (((VIEW_TYPE_MAX - 1) << 16) | 60)
+
+class CViewState
+{
+public:
+ CViewState(int viewMode, SortBy sortMethod, SortOrder sortOrder, SortAttribute sortAttributes = SortAttributeNone)
+ {
+ m_viewMode = viewMode;
+ m_sortDescription.sortBy = sortMethod;
+ m_sortDescription.sortOrder = sortOrder;
+ m_sortDescription.sortAttributes = sortAttributes;
+ };
+ CViewState()
+ {
+ m_viewMode = 0;
+ m_sortDescription.sortBy = SortByLabel;
+ m_sortDescription.sortOrder = SortOrderAscending;
+ };
+
+ int m_viewMode;
+ SortDescription m_sortDescription;
+};
diff --git a/xbmc/view/ViewStateSettings.cpp b/xbmc/view/ViewStateSettings.cpp
new file mode 100644
index 0000000..d883393
--- /dev/null
+++ b/xbmc/view/ViewStateSettings.cpp
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2013-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 "ViewStateSettings.h"
+
+#include "utils/SortUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <cstring>
+#include <mutex>
+#include <utility>
+
+#define XML_VIEWSTATESETTINGS "viewstates"
+#define XML_VIEWMODE "viewmode"
+#define XML_SORTMETHOD "sortmethod"
+#define XML_SORTORDER "sortorder"
+#define XML_SORTATTRIBUTES "sortattributes"
+#define XML_GENERAL "general"
+#define XML_SETTINGLEVEL "settinglevel"
+#define XML_EVENTLOG "eventlog"
+#define XML_EVENTLOG_LEVEL "level"
+#define XML_EVENTLOG_LEVEL_HIGHER "showhigherlevels"
+
+CViewStateSettings::CViewStateSettings()
+{
+ AddViewState("musicnavartists");
+ AddViewState("musicnavalbums");
+ AddViewState("musicnavsongs", DEFAULT_VIEW_LIST, SortByTrackNumber);
+ AddViewState("musiclastfm");
+ AddViewState("videonavactors");
+ AddViewState("videonavyears");
+ AddViewState("videonavgenres");
+ AddViewState("videonavtitles");
+ AddViewState("videonavepisodes", DEFAULT_VIEW_AUTO, SortByEpisodeNumber);
+ AddViewState("videonavtvshows");
+ AddViewState("videonavseasons");
+ AddViewState("videonavmusicvideos");
+
+ AddViewState("programs", DEFAULT_VIEW_AUTO);
+ AddViewState("pictures", DEFAULT_VIEW_AUTO);
+ AddViewState("videofiles", DEFAULT_VIEW_AUTO);
+ AddViewState("musicfiles", DEFAULT_VIEW_AUTO);
+ AddViewState("games", DEFAULT_VIEW_AUTO);
+
+ Clear();
+}
+
+CViewStateSettings::~CViewStateSettings()
+{
+ for (std::map<std::string, CViewState*>::const_iterator viewState = m_viewStates.begin(); viewState != m_viewStates.end(); ++viewState)
+ delete viewState->second;
+ m_viewStates.clear();
+}
+
+CViewStateSettings& CViewStateSettings::GetInstance()
+{
+ static CViewStateSettings sViewStateSettings;
+ return sViewStateSettings;
+}
+
+bool CViewStateSettings::Load(const TiXmlNode *settings)
+{
+ if (settings == NULL)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ const TiXmlNode *pElement = settings->FirstChildElement(XML_VIEWSTATESETTINGS);
+ if (pElement == NULL)
+ {
+ CLog::Log(LOGWARNING, "CViewStateSettings: no <viewstates> tag found");
+ return false;
+ }
+
+ for (std::map<std::string, CViewState*>::iterator viewState = m_viewStates.begin(); viewState != m_viewStates.end(); ++viewState)
+ {
+ const TiXmlNode* pViewState = pElement->FirstChildElement(viewState->first);
+ if (pViewState == NULL)
+ continue;
+
+ XMLUtils::GetInt(pViewState, XML_VIEWMODE, viewState->second->m_viewMode, DEFAULT_VIEW_LIST, DEFAULT_VIEW_MAX);
+
+ // keep backwards compatibility to the old sorting methods
+ if (pViewState->FirstChild(XML_SORTATTRIBUTES) == NULL)
+ {
+ int sortMethod;
+ if (XMLUtils::GetInt(pViewState, XML_SORTMETHOD, sortMethod, SORT_METHOD_NONE, SORT_METHOD_MAX))
+ viewState->second->m_sortDescription = SortUtils::TranslateOldSortMethod((SORT_METHOD)sortMethod);
+ }
+ else
+ {
+ int sortMethod;
+ if (XMLUtils::GetInt(pViewState, XML_SORTMETHOD, sortMethod, SortByNone, SortByLastUsed))
+ viewState->second->m_sortDescription.sortBy = (SortBy)sortMethod;
+ if (XMLUtils::GetInt(pViewState, XML_SORTATTRIBUTES, sortMethod, SortAttributeNone, SortAttributeIgnoreFolders))
+ viewState->second->m_sortDescription.sortAttributes = (SortAttribute)sortMethod;
+ }
+
+ int sortOrder;
+ if (XMLUtils::GetInt(pViewState, XML_SORTORDER, sortOrder, SortOrderNone, SortOrderDescending))
+ viewState->second->m_sortDescription.sortOrder = (SortOrder)sortOrder;
+ }
+
+ pElement = settings->FirstChild(XML_GENERAL);
+ if (pElement != NULL)
+ {
+ int settingLevel;
+ if (XMLUtils::GetInt(pElement, XML_SETTINGLEVEL, settingLevel, static_cast<int>(SettingLevel::Basic), static_cast<int>(SettingLevel::Expert)))
+ m_settingLevel = (SettingLevel)settingLevel;
+ else
+ m_settingLevel = SettingLevel::Standard;
+
+ const TiXmlNode* pEventLogNode = pElement->FirstChild(XML_EVENTLOG);
+ if (pEventLogNode != NULL)
+ {
+ int eventLevel;
+ if (XMLUtils::GetInt(pEventLogNode, XML_EVENTLOG_LEVEL, eventLevel, static_cast<int>(EventLevel::Basic), static_cast<int>(EventLevel::Error)))
+ m_eventLevel = (EventLevel)eventLevel;
+ else
+ m_eventLevel = EventLevel::Basic;
+
+ if (!XMLUtils::GetBoolean(pEventLogNode, XML_EVENTLOG_LEVEL_HIGHER, m_eventShowHigherLevels))
+ m_eventShowHigherLevels = true;
+ }
+ }
+
+ return true;
+}
+
+bool CViewStateSettings::Save(TiXmlNode *settings) const
+{
+ if (settings == NULL)
+ return false;
+
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ // add the <viewstates> tag
+ TiXmlElement xmlViewStateElement(XML_VIEWSTATESETTINGS);
+ TiXmlNode *pViewStateNode = settings->InsertEndChild(xmlViewStateElement);
+ if (pViewStateNode == NULL)
+ {
+ CLog::Log(LOGWARNING, "CViewStateSettings: could not create <viewstates> tag");
+ return false;
+ }
+
+ for (std::map<std::string, CViewState*>::const_iterator viewState = m_viewStates.begin(); viewState != m_viewStates.end(); ++viewState)
+ {
+ TiXmlElement newElement(viewState->first);
+ TiXmlNode *pNewNode = pViewStateNode->InsertEndChild(newElement);
+ if (pNewNode == NULL)
+ continue;
+
+ XMLUtils::SetInt(pNewNode, XML_VIEWMODE, viewState->second->m_viewMode);
+ XMLUtils::SetInt(pNewNode, XML_SORTMETHOD, (int)viewState->second->m_sortDescription.sortBy);
+ XMLUtils::SetInt(pNewNode, XML_SORTORDER, (int)viewState->second->m_sortDescription.sortOrder);
+ XMLUtils::SetInt(pNewNode, XML_SORTATTRIBUTES, (int)viewState->second->m_sortDescription.sortAttributes);
+ }
+
+ TiXmlNode *generalNode = settings->FirstChild(XML_GENERAL);
+ if (generalNode == NULL)
+ {
+ TiXmlElement generalElement(XML_GENERAL);
+ generalNode = settings->InsertEndChild(generalElement);
+ if (generalNode == NULL)
+ return false;
+ }
+
+ XMLUtils::SetInt(generalNode, XML_SETTINGLEVEL, (int)m_settingLevel);
+
+ TiXmlNode *eventLogNode = generalNode->FirstChild(XML_EVENTLOG);
+ if (eventLogNode == NULL)
+ {
+ TiXmlElement eventLogElement(XML_EVENTLOG);
+ eventLogNode = generalNode->InsertEndChild(eventLogElement);
+ if (eventLogNode == NULL)
+ return false;
+ }
+
+ XMLUtils::SetInt(eventLogNode, XML_EVENTLOG_LEVEL, (int)m_eventLevel);
+ XMLUtils::SetBoolean(eventLogNode, XML_EVENTLOG_LEVEL_HIGHER, (int)m_eventShowHigherLevels);
+
+ return true;
+}
+
+void CViewStateSettings::Clear()
+{
+ m_settingLevel = SettingLevel::Standard;
+}
+
+const CViewState* CViewStateSettings::Get(const std::string &viewState) const
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ std::map<std::string, CViewState*>::const_iterator view = m_viewStates.find(viewState);
+ if (view != m_viewStates.end())
+ return view->second;
+
+ return NULL;
+}
+
+CViewState* CViewStateSettings::Get(const std::string &viewState)
+{
+ std::unique_lock<CCriticalSection> lock(m_critical);
+ std::map<std::string, CViewState*>::iterator view = m_viewStates.find(viewState);
+ if (view != m_viewStates.end())
+ return view->second;
+
+ return NULL;
+}
+
+void CViewStateSettings::SetSettingLevel(SettingLevel settingLevel)
+{
+ if (settingLevel < SettingLevel::Basic)
+ m_settingLevel = SettingLevel::Basic;
+ if (settingLevel > SettingLevel::Expert)
+ m_settingLevel = SettingLevel::Expert;
+ else
+ m_settingLevel = settingLevel;
+}
+
+void CViewStateSettings::CycleSettingLevel()
+{
+ m_settingLevel = GetNextSettingLevel();
+}
+
+SettingLevel CViewStateSettings::GetNextSettingLevel() const
+{
+ SettingLevel level = (SettingLevel)((int)m_settingLevel + 1);
+ if (level > SettingLevel::Expert)
+ level = SettingLevel::Basic;
+ return level;
+}
+
+void CViewStateSettings::SetEventLevel(EventLevel eventLevel)
+{
+ if (eventLevel < EventLevel::Basic)
+ m_eventLevel = EventLevel::Basic;
+ if (eventLevel > EventLevel::Error)
+ m_eventLevel = EventLevel::Error;
+ else
+ m_eventLevel = eventLevel;
+}
+
+void CViewStateSettings::CycleEventLevel()
+{
+ m_eventLevel = GetNextEventLevel();
+}
+
+EventLevel CViewStateSettings::GetNextEventLevel() const
+{
+ EventLevel level = (EventLevel)((int)m_eventLevel + 1);
+ if (level > EventLevel::Error)
+ level = EventLevel::Basic;
+ return level;
+}
+
+void CViewStateSettings::AddViewState(const std::string& strTagName, int defaultView /* = DEFAULT_VIEW_LIST */, SortBy defaultSort /* = SortByLabel */)
+{
+ if (strTagName.empty() || m_viewStates.find(strTagName) != m_viewStates.end())
+ return;
+
+ CViewState *viewState = new CViewState(defaultView, defaultSort, SortOrderAscending);
+ if (viewState == NULL)
+ return;
+
+ m_viewStates.insert(make_pair(strTagName, viewState));
+}
diff --git a/xbmc/view/ViewStateSettings.h b/xbmc/view/ViewStateSettings.h
new file mode 100644
index 0000000..2a18fc9
--- /dev/null
+++ b/xbmc/view/ViewStateSettings.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2013-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 "ViewState.h"
+#include "events/IEvent.h"
+#include "settings/ISubSettings.h"
+#include "settings/lib/Setting.h"
+#include "threads/CriticalSection.h"
+#include "windowing/GraphicContext.h"
+
+#include <map>
+#include <string>
+
+class TiXmlNode;
+
+class CViewStateSettings : public ISubSettings
+{
+public:
+ static CViewStateSettings& GetInstance();
+
+ bool Load(const TiXmlNode *settings) override;
+ bool Save(TiXmlNode *settings) const override;
+ void Clear() override;
+
+ const CViewState* Get(const std::string &viewState) const;
+ CViewState* Get(const std::string &viewState);
+
+ SettingLevel GetSettingLevel() const { return m_settingLevel; }
+ void SetSettingLevel(SettingLevel settingLevel);
+ void CycleSettingLevel();
+ SettingLevel GetNextSettingLevel() const;
+
+ EventLevel GetEventLevel() const { return m_eventLevel; }
+ void SetEventLevel(EventLevel eventLevel);
+ void CycleEventLevel();
+ EventLevel GetNextEventLevel() const;
+ bool ShowHigherEventLevels() const { return m_eventShowHigherLevels; }
+ void SetShowHigherEventLevels(bool showHigherEventLevels) { m_eventShowHigherLevels = showHigherEventLevels; }
+ void ToggleShowHigherEventLevels() { m_eventShowHigherLevels = !m_eventShowHigherLevels; }
+
+protected:
+ CViewStateSettings();
+ CViewStateSettings(const CViewStateSettings&) = delete;
+ CViewStateSettings& operator=(CViewStateSettings const&) = delete;
+ ~CViewStateSettings() override;
+
+private:
+ std::map<std::string, CViewState*> m_viewStates;
+ SettingLevel m_settingLevel = SettingLevel::Standard;
+ EventLevel m_eventLevel = EventLevel::Basic;
+ bool m_eventShowHigherLevels = true;
+ mutable CCriticalSection m_critical;
+
+ void AddViewState(const std::string& strTagName, int defaultView = DEFAULT_VIEW_LIST, SortBy defaultSort = SortByLabel);
+};
diff --git a/xbmc/weather/CMakeLists.txt b/xbmc/weather/CMakeLists.txt
new file mode 100644
index 0000000..c79cab1
--- /dev/null
+++ b/xbmc/weather/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES GUIWindowWeather.cpp
+ WeatherJob.cpp
+ WeatherManager.cpp)
+
+set(HEADERS GUIWindowWeather.h
+ WeatherJob.h
+ WeatherManager.h)
+
+core_add_library(weather)
diff --git a/xbmc/weather/GUIWindowWeather.cpp b/xbmc/weather/GUIWindowWeather.cpp
new file mode 100644
index 0000000..66b93b5
--- /dev/null
+++ b/xbmc/weather/GUIWindowWeather.cpp
@@ -0,0 +1,299 @@
+/*
+ * 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 "GUIWindowWeather.h"
+
+#include "GUIUserMessages.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "guilib/WindowIDs.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "weather/WeatherManager.h"
+
+#include <utility>
+
+using namespace ADDON;
+
+#define CONTROL_BTNREFRESH 2
+#define CONTROL_SELECTLOCATION 3
+#define CONTROL_LABELUPDATED 11
+
+#define CONTROL_STATICTEMP 223
+#define CONTROL_STATICFEEL 224
+#define CONTROL_STATICUVID 225
+#define CONTROL_STATICWIND 226
+#define CONTROL_STATICDEWP 227
+#define CONTROL_STATICHUMI 228
+
+#define CONTROL_LABELD0DAY 31
+#define CONTROL_LABELD0HI 32
+#define CONTROL_LABELD0LOW 33
+#define CONTROL_LABELD0GEN 34
+#define CONTROL_IMAGED0IMG 35
+
+#define LOCALIZED_TOKEN_FIRSTID 370
+#define LOCALIZED_TOKEN_LASTID 395
+
+/*
+FIXME'S
+>strings are not centered
+*/
+
+CGUIWindowWeather::CGUIWindowWeather(void)
+ : CGUIWindow(WINDOW_WEATHER, "MyWeather.xml")
+{
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIWindowWeather::~CGUIWindowWeather(void) = default;
+
+bool CGUIWindowWeather::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTNREFRESH)
+ {
+ CServiceBroker::GetWeatherManager().Refresh(); // Refresh clicked so do a complete update
+ }
+ else if (iControl == CONTROL_SELECTLOCATION)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED,GetID(),CONTROL_SELECTLOCATION);
+ OnMessage(msg);
+
+ SetLocation(msg.GetParam1());
+ }
+ }
+ break;
+ case GUI_MSG_NOTIFY_ALL:
+ if (message.GetParam1() == GUI_MSG_WINDOW_RESET)
+ {
+ CServiceBroker::GetWeatherManager().Reset();
+ return true;
+ }
+ else if (message.GetParam1() == GUI_MSG_WEATHER_FETCHED)
+ {
+ UpdateLocations();
+ SetProperties();
+ }
+ break;
+ case GUI_MSG_ITEM_SELECT:
+ {
+ if (message.GetSenderId() == 0) //handle only message from builtin
+ {
+ SetLocation(message.GetParam1());
+ return true;
+ }
+ }
+ break;
+ case GUI_MSG_MOVE_OFFSET:
+ {
+ if (message.GetSenderId() == 0 && m_maxLocation > 0) //handle only message from builtin
+ {
+ // Clamp location between 1 and m_maxLocation
+ int v = (CServiceBroker::GetWeatherManager().GetArea() + message.GetParam1() - 1) % m_maxLocation + 1;
+ if (v < 1) v += m_maxLocation;
+ SetLocation(v);
+ return true;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ return CGUIWindow::OnMessage(message);
+}
+
+void CGUIWindowWeather::OnInitWindow()
+{
+ // call UpdateButtons() so that we start with our initial stuff already present
+ UpdateButtons();
+ UpdateLocations();
+ CGUIWindow::OnInitWindow();
+}
+
+void CGUIWindowWeather::UpdateLocations()
+{
+ if (!IsActive()) return;
+ m_maxLocation = strtol(GetProperty("Locations").asString().c_str(),0,10);
+ if (m_maxLocation < 1) return;
+
+ std::vector< std::pair<std::string, int> > labels;
+
+ unsigned int iCurWeather = CServiceBroker::GetWeatherManager().GetArea();
+
+ if (iCurWeather > m_maxLocation)
+ {
+ CServiceBroker::GetWeatherManager().SetArea(m_maxLocation);
+ iCurWeather = m_maxLocation;
+ ClearProperties();
+ CServiceBroker::GetWeatherManager().Refresh();
+ }
+
+ for (unsigned int i = 1; i <= m_maxLocation; i++)
+ {
+ std::string strLabel = CServiceBroker::GetWeatherManager().GetLocation(i);
+ if (strLabel.size() > 1) //got the location string yet?
+ {
+ size_t iPos = strLabel.rfind(", ");
+ if (iPos != std::string::npos)
+ {
+ std::string strLabel2(strLabel);
+ strLabel = strLabel2.substr(0,iPos);
+ }
+ labels.emplace_back(strLabel, i);
+ }
+ else
+ {
+ strLabel = StringUtils::Format("AreaCode {}", i);
+ labels.emplace_back(strLabel, i);
+ }
+ // in case it's a button, set the label
+ if (i == iCurWeather)
+ SET_CONTROL_LABEL(CONTROL_SELECTLOCATION,strLabel);
+ }
+
+ SET_CONTROL_LABELS(CONTROL_SELECTLOCATION, iCurWeather, &labels);
+}
+
+void CGUIWindowWeather::UpdateButtons()
+{
+ CONTROL_ENABLE(CONTROL_BTNREFRESH);
+
+ SET_CONTROL_LABEL(CONTROL_BTNREFRESH, 184); //Refresh
+
+ SET_CONTROL_LABEL(WEATHER_LABEL_LOCATION, CServiceBroker::GetWeatherManager().GetLocation(CServiceBroker::GetWeatherManager().GetArea()));
+ SET_CONTROL_LABEL(CONTROL_LABELUPDATED, CServiceBroker::GetWeatherManager().GetLastUpdateTime());
+
+ SET_CONTROL_LABEL(WEATHER_LABEL_CURRENT_COND, CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_COND));
+ SET_CONTROL_LABEL(WEATHER_LABEL_CURRENT_TEMP, CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_TEMP) + g_langInfo.GetTemperatureUnitString());
+ SET_CONTROL_LABEL(WEATHER_LABEL_CURRENT_FEEL, CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_FEEL) + g_langInfo.GetTemperatureUnitString());
+ SET_CONTROL_LABEL(WEATHER_LABEL_CURRENT_UVID, CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_UVID));
+ SET_CONTROL_LABEL(WEATHER_LABEL_CURRENT_WIND, CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_WIND));
+ SET_CONTROL_LABEL(WEATHER_LABEL_CURRENT_DEWP, CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_DEWP) + g_langInfo.GetTemperatureUnitString());
+ SET_CONTROL_LABEL(WEATHER_LABEL_CURRENT_HUMI, CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_HUMI));
+ SET_CONTROL_FILENAME(WEATHER_IMAGE_CURRENT_ICON, CServiceBroker::GetWeatherManager().GetInfo(WEATHER_IMAGE_CURRENT_ICON));
+
+ //static labels
+ SET_CONTROL_LABEL(CONTROL_STATICTEMP, 401); //Temperature
+ SET_CONTROL_LABEL(CONTROL_STATICFEEL, 402); //Feels Like
+ SET_CONTROL_LABEL(CONTROL_STATICUVID, 403); //UV Index
+ SET_CONTROL_LABEL(CONTROL_STATICWIND, 404); //Wind
+ SET_CONTROL_LABEL(CONTROL_STATICDEWP, 405); //Dew Point
+ SET_CONTROL_LABEL(CONTROL_STATICHUMI, 406); //Humidity
+
+ for (int i = 0; i < NUM_DAYS; i++)
+ {
+ SET_CONTROL_LABEL(CONTROL_LABELD0DAY + (i*10), CServiceBroker::GetWeatherManager().GetForecast(i).m_day);
+ SET_CONTROL_LABEL(CONTROL_LABELD0HI + (i*10), CServiceBroker::GetWeatherManager().GetForecast(i).m_high + g_langInfo.GetTemperatureUnitString());
+ SET_CONTROL_LABEL(CONTROL_LABELD0LOW + (i*10), CServiceBroker::GetWeatherManager().GetForecast(i).m_low + g_langInfo.GetTemperatureUnitString());
+ SET_CONTROL_LABEL(CONTROL_LABELD0GEN + (i*10), CServiceBroker::GetWeatherManager().GetForecast(i).m_overview);
+ SET_CONTROL_FILENAME(CONTROL_IMAGED0IMG + (i*10), CServiceBroker::GetWeatherManager().GetForecast(i).m_icon);
+ }
+}
+
+void CGUIWindowWeather::FrameMove()
+{
+ // update our controls
+ UpdateButtons();
+
+ CGUIWindow::FrameMove();
+}
+
+/*!
+ \brief Sets the location to the specified index and refreshes the weather
+ \param loc the location index (in the range [1..MAXLOCATION])
+ */
+void CGUIWindowWeather::SetLocation(int loc)
+{
+ if (loc < 1 || loc > (int)m_maxLocation)
+ return;
+ // Avoid a settings write if old location == new location
+ if (CServiceBroker::GetWeatherManager().GetArea() != loc)
+ {
+ ClearProperties();
+ CServiceBroker::GetWeatherManager().SetArea(loc);
+ std::string strLabel = CServiceBroker::GetWeatherManager().GetLocation(loc);
+ size_t iPos = strLabel.rfind(", ");
+ if (iPos != std::string::npos)
+ strLabel = strLabel.substr(0, iPos);
+ SET_CONTROL_LABEL(CONTROL_SELECTLOCATION, strLabel);
+ }
+ CServiceBroker::GetWeatherManager().Refresh();
+}
+
+void CGUIWindowWeather::SetProperties()
+{
+ // Current weather
+ int iCurWeather = CServiceBroker::GetWeatherManager().GetArea();
+ SetProperty("Location", CServiceBroker::GetWeatherManager().GetLocation(iCurWeather));
+ SetProperty("LocationIndex", iCurWeather);
+ SetProperty("Updated", CServiceBroker::GetWeatherManager().GetLastUpdateTime());
+ SetProperty("Current.ConditionIcon", CServiceBroker::GetWeatherManager().GetInfo(WEATHER_IMAGE_CURRENT_ICON));
+ SetProperty("Current.Condition", CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_COND));
+ SetProperty("Current.Temperature", CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_TEMP));
+ SetProperty("Current.FeelsLike", CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_FEEL));
+ SetProperty("Current.UVIndex", CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_UVID));
+ SetProperty("Current.Wind", CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_WIND));
+ SetProperty("Current.DewPoint", CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_DEWP));
+ SetProperty("Current.Humidity", CServiceBroker::GetWeatherManager().GetInfo(WEATHER_LABEL_CURRENT_HUMI));
+ // we use the icons code number for fanart as it's the safest way
+ std::string fanartcode = URIUtils::GetFileName(CServiceBroker::GetWeatherManager().GetInfo(WEATHER_IMAGE_CURRENT_ICON));
+ URIUtils::RemoveExtension(fanartcode);
+ SetProperty("Current.FanartCode", fanartcode);
+
+ // Future weather
+ std::string day;
+ for (int i = 0; i < NUM_DAYS; i++)
+ {
+ day = StringUtils::Format("Day{}.", i);
+ SetProperty(day + "Title", CServiceBroker::GetWeatherManager().GetForecast(i).m_day);
+ SetProperty(day + "HighTemp", CServiceBroker::GetWeatherManager().GetForecast(i).m_high);
+ SetProperty(day + "LowTemp", CServiceBroker::GetWeatherManager().GetForecast(i).m_low);
+ SetProperty(day + "Outlook", CServiceBroker::GetWeatherManager().GetForecast(i).m_overview);
+ SetProperty(day + "OutlookIcon", CServiceBroker::GetWeatherManager().GetForecast(i).m_icon);
+ fanartcode = URIUtils::GetFileName(CServiceBroker::GetWeatherManager().GetForecast(i).m_icon);
+ URIUtils::RemoveExtension(fanartcode);
+ SetProperty(day + "FanartCode", fanartcode);
+ }
+}
+
+void CGUIWindowWeather::ClearProperties()
+{
+ // Current weather
+ SetProperty("Location", "");
+ SetProperty("LocationIndex", "");
+ SetProperty("Updated", "");
+ SetProperty("Current.ConditionIcon", "");
+ SetProperty("Current.Condition", "");
+ SetProperty("Current.Temperature", "");
+ SetProperty("Current.FeelsLike", "");
+ SetProperty("Current.UVIndex", "");
+ SetProperty("Current.Wind", "");
+ SetProperty("Current.DewPoint", "");
+ SetProperty("Current.Humidity", "");
+ SetProperty("Current.FanartCode", "");
+
+ // Future weather
+ std::string day;
+ for (int i = 0; i < NUM_DAYS; i++)
+ {
+ day = StringUtils::Format("Day{}.", i);
+ SetProperty(day + "Title", "");
+ SetProperty(day + "HighTemp", "");
+ SetProperty(day + "LowTemp", "");
+ SetProperty(day + "Outlook", "");
+ SetProperty(day + "OutlookIcon", "");
+ SetProperty(day + "FanartCode", "");
+ }
+}
diff --git a/xbmc/weather/GUIWindowWeather.h b/xbmc/weather/GUIWindowWeather.h
new file mode 100644
index 0000000..1ae1828
--- /dev/null
+++ b/xbmc/weather/GUIWindowWeather.h
@@ -0,0 +1,31 @@
+/*
+ * 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 "guilib/GUIWindow.h"
+
+class CGUIWindowWeather : public CGUIWindow
+{
+public:
+ CGUIWindowWeather(void);
+ ~CGUIWindowWeather(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void FrameMove() override;
+
+protected:
+ void OnInitWindow() override;
+
+ void UpdateButtons();
+ void UpdateLocations();
+ void SetProperties();
+ void ClearProperties();
+ void SetLocation(int loc);
+
+ unsigned int m_maxLocation = 0;
+};
diff --git a/xbmc/weather/WeatherJob.cpp b/xbmc/weather/WeatherJob.cpp
new file mode 100644
index 0000000..17a0995
--- /dev/null
+++ b/xbmc/weather/WeatherJob.cpp
@@ -0,0 +1,243 @@
+/*
+ * 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 "WeatherJob.h"
+
+#include "GUIUserMessages.h"
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "XBDateTime.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "network/Network.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/POUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#define LOCALIZED_TOKEN_FIRSTID 370
+#define LOCALIZED_TOKEN_LASTID 395
+#define LOCALIZED_TOKEN_FIRSTID2 1350
+#define LOCALIZED_TOKEN_LASTID2 1449
+#define LOCALIZED_TOKEN_FIRSTID3 11
+#define LOCALIZED_TOKEN_LASTID3 17
+#define LOCALIZED_TOKEN_FIRSTID4 71
+#define LOCALIZED_TOKEN_LASTID4 97
+
+using namespace ADDON;
+
+using namespace std::chrono_literals;
+
+CWeatherJob::CWeatherJob(int location)
+{
+ m_location = location;
+}
+
+bool CWeatherJob::DoWork()
+{
+ // wait for the network
+ if (!CServiceBroker::GetNetwork().IsAvailable())
+ return false;
+
+ AddonPtr addon;
+ if (!CServiceBroker::GetAddonMgr().GetAddon(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_WEATHER_ADDON),
+ addon, AddonType::SCRIPT_WEATHER, OnlyEnabled::CHOICE_YES))
+ return false;
+
+ // initialize our sys.argv variables
+ std::vector<std::string> argv;
+ argv.push_back(addon->LibPath());
+
+ std::string strSetting = std::to_string(m_location);
+ argv.push_back(strSetting);
+
+ // Download our weather
+ CLog::Log(LOGINFO, "WEATHER: Downloading weather");
+ // call our script, passing the areacode
+ int scriptId = -1;
+ if ((scriptId = CScriptInvocationManager::GetInstance().ExecuteAsync(argv[0], addon, argv)) >= 0)
+ {
+ while (true)
+ {
+ if (!CScriptInvocationManager::GetInstance().IsRunning(scriptId))
+ break;
+ KODI::TIME::Sleep(100ms);
+ }
+
+ SetFromProperties();
+
+ // and send a message that we're done
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL,0,0,GUI_MSG_WEATHER_FETCHED);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+ }
+ else
+ CLog::Log(LOGERROR, "WEATHER: Weather download failed!");
+
+ return true;
+}
+
+const CWeatherInfo &CWeatherJob::GetInfo() const
+{
+ return m_info;
+}
+
+void CWeatherJob::LocalizeOverviewToken(std::string &token)
+{
+ // This routine is case-insensitive.
+ std::string strLocStr;
+ if (!token.empty())
+ {
+ ilocalizedTokens i;
+ i = m_localizedTokens.find(token);
+ if (i != m_localizedTokens.end())
+ {
+ strLocStr = g_localizeStrings.Get(i->second);
+ }
+ }
+ if (strLocStr == "")
+ strLocStr = token; //if not found, let fallback
+ token = strLocStr;
+}
+
+void CWeatherJob::LocalizeOverview(std::string &str)
+{
+ std::vector<std::string> words = StringUtils::Split(str, " ");
+ for (std::vector<std::string>::iterator i = words.begin(); i != words.end(); ++i)
+ LocalizeOverviewToken(*i);
+ str = StringUtils::Join(words, " ");
+}
+
+void CWeatherJob::FormatTemperature(std::string &text, double temp)
+{
+ CTemperature temperature = CTemperature::CreateFromCelsius(temp);
+ text = StringUtils::Format("{:.0f}", temperature.To(g_langInfo.GetTemperatureUnit()));
+}
+
+void CWeatherJob::LoadLocalizedToken()
+{
+ // We load the english strings in to get our tokens
+ std::string language = LANGUAGE_DEFAULT;
+ std::shared_ptr<CSettingString> languageSetting = std::static_pointer_cast<CSettingString>(CServiceBroker::GetSettingsComponent()->GetSettings()->GetSetting(CSettings::SETTING_LOCALE_LANGUAGE));
+ if (languageSetting != NULL)
+ language = languageSetting->GetDefault();
+
+ // Load the strings.po file
+ CPODocument PODoc;
+ if (PODoc.LoadFile(URIUtils::AddFileToFolder(CLangInfo::GetLanguagePath(language), "strings.po")))
+ {
+ int counter = 0;
+
+ while (PODoc.GetNextEntry())
+ {
+ if (PODoc.GetEntryType() != ID_FOUND)
+ continue;
+
+ uint32_t id = PODoc.GetEntryID();
+ PODoc.ParseEntry(ISSOURCELANG);
+
+ if (id > LOCALIZED_TOKEN_LASTID2) break;
+ if ((LOCALIZED_TOKEN_FIRSTID <= id && id <= LOCALIZED_TOKEN_LASTID) ||
+ (LOCALIZED_TOKEN_FIRSTID2 <= id && id <= LOCALIZED_TOKEN_LASTID2) ||
+ (LOCALIZED_TOKEN_FIRSTID3 <= id && id <= LOCALIZED_TOKEN_LASTID3) ||
+ (LOCALIZED_TOKEN_FIRSTID4 <= id && id <= LOCALIZED_TOKEN_LASTID4))
+ {
+ if (!PODoc.GetMsgid().empty())
+ {
+ m_localizedTokens.insert(make_pair(PODoc.GetMsgid(), id));
+ counter++;
+ }
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "POParser: loaded {} weather tokens", counter);
+ return;
+ }
+}
+
+std::string CWeatherJob::ConstructPath(std::string in) // copy intended
+{
+ if (in.find('/') != std::string::npos || in.find('\\') != std::string::npos)
+ return in;
+ if (in.empty() || in == "N/A")
+ in = "na.png";
+
+ return URIUtils::AddFileToFolder(ICON_ADDON_PATH, in);
+}
+
+void CWeatherJob::SetFromProperties()
+{
+ // Load in our tokens if necessary
+ if (m_localizedTokens.empty())
+ LoadLocalizedToken();
+
+ CGUIWindow* window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_WEATHER);
+ if (window)
+ {
+ CDateTime time = CDateTime::GetCurrentDateTime();
+ m_info.lastUpdateTime = time.GetAsLocalizedDateTime(false, false);
+ m_info.currentConditions = window->GetProperty("Current.Condition").asString();
+ m_info.currentIcon = ConstructPath(window->GetProperty("Current.OutlookIcon").asString());
+ LocalizeOverview(m_info.currentConditions);
+ FormatTemperature(m_info.currentTemperature,
+ strtod(window->GetProperty("Current.Temperature").asString().c_str(), nullptr));
+ FormatTemperature(m_info.currentFeelsLike,
+ strtod(window->GetProperty("Current.FeelsLike").asString().c_str(), nullptr));
+ m_info.currentUVIndex = window->GetProperty("Current.UVIndex").asString();
+ LocalizeOverview(m_info.currentUVIndex);
+ CSpeed speed = CSpeed::CreateFromKilometresPerHour(strtol(window->GetProperty("Current.Wind").asString().c_str(),0,10));
+ std::string direction = window->GetProperty("Current.WindDirection").asString();
+ if (direction == "CALM")
+ m_info.currentWind = g_localizeStrings.Get(1410);
+ else
+ {
+ LocalizeOverviewToken(direction);
+ m_info.currentWind = StringUtils::Format(g_localizeStrings.Get(434), direction,
+ (int)speed.To(g_langInfo.GetSpeedUnit()),
+ g_langInfo.GetSpeedUnitString());
+ }
+ std::string windspeed = StringUtils::Format("{} {}", (int)speed.To(g_langInfo.GetSpeedUnit()),
+ g_langInfo.GetSpeedUnitString());
+ window->SetProperty("Current.WindSpeed",windspeed);
+ FormatTemperature(m_info.currentDewPoint,
+ strtod(window->GetProperty("Current.DewPoint").asString().c_str(), nullptr));
+ if (window->GetProperty("Current.Humidity").asString().empty())
+ m_info.currentHumidity.clear();
+ else
+ m_info.currentHumidity =
+ StringUtils::Format("{}%", window->GetProperty("Current.Humidity").asString());
+ m_info.location = window->GetProperty("Current.Location").asString();
+ for (int i=0;i<NUM_DAYS;++i)
+ {
+ std::string strDay = StringUtils::Format("Day{}.Title", i);
+ m_info.forecast[i].m_day = window->GetProperty(strDay).asString();
+ LocalizeOverviewToken(m_info.forecast[i].m_day);
+ strDay = StringUtils::Format("Day{}.HighTemp", i);
+ FormatTemperature(m_info.forecast[i].m_high,
+ strtod(window->GetProperty(strDay).asString().c_str(), nullptr));
+ strDay = StringUtils::Format("Day{}.LowTemp", i);
+ FormatTemperature(m_info.forecast[i].m_low,
+ strtod(window->GetProperty(strDay).asString().c_str(), nullptr));
+ strDay = StringUtils::Format("Day{}.OutlookIcon", i);
+ m_info.forecast[i].m_icon = ConstructPath(window->GetProperty(strDay).asString());
+ strDay = StringUtils::Format("Day{}.Outlook", i);
+ m_info.forecast[i].m_overview = window->GetProperty(strDay).asString();
+ LocalizeOverview(m_info.forecast[i].m_overview);
+ }
+ }
+}
diff --git a/xbmc/weather/WeatherJob.h b/xbmc/weather/WeatherJob.h
new file mode 100644
index 0000000..989dac3
--- /dev/null
+++ b/xbmc/weather/WeatherJob.h
@@ -0,0 +1,63 @@
+/*
+ * 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 "WeatherManager.h"
+
+#include <map>
+#include <string>
+
+class CWeatherJob : public CJob
+{
+public:
+ explicit CWeatherJob(int location);
+
+ bool DoWork() override;
+
+ const CWeatherInfo &GetInfo() const;
+private:
+ static std::string ConstructPath(std::string in);
+ void LocalizeOverview(std::string &str);
+ void LocalizeOverviewToken(std::string &str);
+ void LoadLocalizedToken();
+ static int ConvertSpeed(int speed);
+
+ void SetFromProperties();
+
+ /*! \brief Formats a celsius temperature into a string based on the users locale
+ \param text the string to format
+ \param temp the temperature (in degrees celsius).
+ */
+ static void FormatTemperature(std::string &text, double temp);
+
+ struct ci_less
+ {
+ // case-independent (ci) compare_less binary function
+ struct nocase_compare
+ {
+ bool operator() (const unsigned char& c1, const unsigned char& c2) const {
+ return tolower(c1) < tolower(c2);
+ }
+ };
+ bool operator()(const std::string & s1, const std::string & s2) const {
+ return std::lexicographical_compare
+ (s1.begin(), s1.end(),
+ s2.begin(), s2.end(),
+ nocase_compare());
+ }
+ };
+
+ std::map<std::string, int, ci_less> m_localizedTokens;
+ typedef std::map<std::string, int, ci_less>::const_iterator ilocalizedTokens;
+
+ CWeatherInfo m_info;
+ int m_location;
+
+ static bool m_imagesOkay;
+};
diff --git a/xbmc/weather/WeatherManager.cpp b/xbmc/weather/WeatherManager.cpp
new file mode 100644
index 0000000..98f576f
--- /dev/null
+++ b/xbmc/weather/WeatherManager.cpp
@@ -0,0 +1,189 @@
+/*
+ * 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 "WeatherManager.h"
+
+#include "LangInfo.h"
+#include "ServiceBroker.h"
+#include "WeatherJob.h"
+#include "addons/AddonManager.h"
+#include "addons/addoninfo/AddonType.h"
+#include "addons/gui/GUIDialogAddonSettings.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+
+using namespace ADDON;
+
+CWeatherManager::CWeatherManager(void) : CInfoLoader(30 * 60 * 1000) // 30 minutes
+{
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSettingsManager()->RegisterCallback(this, {
+ CSettings::SETTING_WEATHER_ADDON,
+ CSettings::SETTING_WEATHER_ADDONSETTINGS
+ });
+
+ Reset();
+}
+
+CWeatherManager::~CWeatherManager(void)
+{
+ const auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ const std::shared_ptr<CSettings> settings = settingsComponent->GetSettings();
+ if (!settings)
+ return;
+
+ settings->GetSettingsManager()->UnregisterCallback(this);
+}
+
+std::string CWeatherManager::BusyInfo(int info) const
+{
+ if (info == WEATHER_IMAGE_CURRENT_ICON)
+ return URIUtils::AddFileToFolder(ICON_ADDON_PATH, "na.png");
+
+ return CInfoLoader::BusyInfo(info);
+}
+
+std::string CWeatherManager::TranslateInfo(int info) const
+{
+ switch (info) {
+ case WEATHER_LABEL_CURRENT_COND:
+ return m_info.currentConditions;
+ case WEATHER_IMAGE_CURRENT_ICON:
+ return m_info.currentIcon;
+ case WEATHER_LABEL_CURRENT_TEMP:
+ return m_info.currentTemperature;
+ case WEATHER_LABEL_CURRENT_FEEL:
+ return m_info.currentFeelsLike;
+ case WEATHER_LABEL_CURRENT_UVID:
+ return m_info.currentUVIndex;
+ case WEATHER_LABEL_CURRENT_WIND:
+ return m_info.currentWind;
+ case WEATHER_LABEL_CURRENT_DEWP:
+ return m_info.currentDewPoint;
+ case WEATHER_LABEL_CURRENT_HUMI:
+ return m_info.currentHumidity;
+ case WEATHER_LABEL_LOCATION:
+ return m_info.location;
+ default:
+ return "";
+ }
+}
+
+/*!
+ \brief Retrieve the city name for the specified location from the settings
+ \param iLocation the location index (can be in the range [1..MAXLOCATION])
+ \return the city name (without the accompanying region area code)
+ */
+std::string CWeatherManager::GetLocation(int iLocation)
+{
+ CGUIWindow* window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_WEATHER);
+ if (window)
+ {
+ std::string setting = StringUtils::Format("Location{}", iLocation);
+ return window->GetProperty(setting).asString();
+ }
+ return "";
+}
+
+void CWeatherManager::Reset()
+{
+ m_info.Reset();
+}
+
+bool CWeatherManager::IsFetched()
+{
+ // call GetInfo() to make sure that we actually start up
+ GetInfo(0);
+ return !m_info.lastUpdateTime.empty();
+}
+
+const ForecastDay &CWeatherManager::GetForecast(int day) const
+{
+ return m_info.forecast[day];
+}
+
+/*!
+ \brief Saves the specified location index to the settings. Call Refresh()
+ afterwards to update weather info for the new location.
+ \param iLocation the new location index (can be in the range [1..MAXLOCATION])
+ */
+void CWeatherManager::SetArea(int iLocation)
+{
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetInt(CSettings::SETTING_WEATHER_CURRENTLOCATION, iLocation);
+ settings->Save();
+}
+
+/*!
+ \brief Retrieves the current location index from the settings
+ \return the active location index (will be in the range [1..MAXLOCATION])
+ */
+int CWeatherManager::GetArea() const
+{
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_WEATHER_CURRENTLOCATION);
+}
+
+CJob *CWeatherManager::GetJob() const
+{
+ return new CWeatherJob(GetArea());
+}
+
+void CWeatherManager::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ m_info = static_cast<CWeatherJob*>(job)->GetInfo();
+ CInfoLoader::OnJobComplete(jobID, success, job);
+}
+
+void CWeatherManager::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_WEATHER_ADDON)
+ {
+ // clear "WeatherProviderLogo" property that some weather addons set
+ CGUIWindow* window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_WEATHER);
+ if (window != nullptr)
+ window->SetProperty("WeatherProviderLogo", "");
+ Refresh();
+ }
+}
+
+void CWeatherManager::OnSettingAction(const std::shared_ptr<const CSetting>& setting)
+{
+ if (setting == NULL)
+ return;
+
+ const std::string settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_WEATHER_ADDONSETTINGS)
+ {
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_WEATHER_ADDON),
+ addon, AddonType::SCRIPT_WEATHER, OnlyEnabled::CHOICE_YES) &&
+ addon != NULL)
+ { //! @todo maybe have ShowAndGetInput return a bool if settings changed, then only reset weather if true.
+ CGUIDialogAddonSettings::ShowForAddon(addon);
+ Refresh();
+ }
+ }
+}
+
diff --git a/xbmc/weather/WeatherManager.h b/xbmc/weather/WeatherManager.h
new file mode 100644
index 0000000..c729f4c
--- /dev/null
+++ b/xbmc/weather/WeatherManager.h
@@ -0,0 +1,108 @@
+/*
+ * 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 "settings/lib/ISettingCallback.h"
+#include "utils/InfoLoader.h"
+
+#include <string>
+
+#define WEATHER_LABEL_LOCATION 10
+#define WEATHER_IMAGE_CURRENT_ICON 21
+#define WEATHER_LABEL_CURRENT_COND 22
+#define WEATHER_LABEL_CURRENT_TEMP 23
+#define WEATHER_LABEL_CURRENT_FEEL 24
+#define WEATHER_LABEL_CURRENT_UVID 25
+#define WEATHER_LABEL_CURRENT_WIND 26
+#define WEATHER_LABEL_CURRENT_DEWP 27
+#define WEATHER_LABEL_CURRENT_HUMI 28
+
+static const std::string ICON_ADDON_PATH = "resource://resource.images.weathericons.default";
+
+struct ForecastDay
+{
+ std::string m_icon;
+ std::string m_overview;
+ std::string m_day;
+ std::string m_high;
+ std::string m_low;
+};
+
+#define NUM_DAYS 7
+
+class CWeatherInfo
+{
+public:
+ ForecastDay forecast[NUM_DAYS];
+
+ void Reset()
+ {
+ lastUpdateTime.clear();
+ currentIcon.clear();
+ currentConditions.clear();
+ currentTemperature.clear();
+ currentFeelsLike.clear();
+ currentWind.clear();
+ currentHumidity.clear();
+ currentUVIndex.clear();
+ currentDewPoint.clear();
+
+ for (ForecastDay& f : forecast)
+ {
+ f.m_icon.clear();
+ f.m_overview.clear();
+ f.m_day.clear();
+ f.m_high.clear();
+ f.m_low.clear();
+ }
+ };
+
+ std::string lastUpdateTime;
+ std::string location;
+ std::string currentIcon;
+ std::string currentConditions;
+ std::string currentTemperature;
+ std::string currentFeelsLike;
+ std::string currentUVIndex;
+ std::string currentWind;
+ std::string currentDewPoint;
+ std::string currentHumidity;
+ std::string busyString;
+ std::string naIcon;
+};
+
+class CWeatherManager
+: public CInfoLoader, public ISettingCallback
+{
+public:
+ CWeatherManager(void);
+ ~CWeatherManager(void) override;
+ static bool GetSearchResults(const std::string &strSearch, std::string &strResult);
+
+ std::string GetLocation(int iLocation);
+ const std::string& GetLastUpdateTime() const { return m_info.lastUpdateTime; }
+ const ForecastDay &GetForecast(int day) const;
+ bool IsFetched();
+ void Reset();
+
+ void SetArea(int iLocation);
+ int GetArea() const;
+protected:
+ CJob *GetJob() const override;
+ std::string TranslateInfo(int info) const override;
+ std::string BusyInfo(int info) const override;
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+ void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override;
+
+private:
+
+ CWeatherInfo m_info;
+};
diff --git a/xbmc/windowing/CMakeLists.txt b/xbmc/windowing/CMakeLists.txt
new file mode 100644
index 0000000..21c7611
--- /dev/null
+++ b/xbmc/windowing/CMakeLists.txt
@@ -0,0 +1,16 @@
+set(SOURCES GraphicContext.cpp
+ OSScreenSaver.cpp
+ Resolution.cpp
+ WindowSystemFactory.cpp
+ WinSystem.cpp)
+
+set(HEADERS GraphicContext.h
+ OSScreenSaver.h
+ Resolution.h
+ WinEvents.h
+ WindowSystemFactory.h
+ WinSystem.h
+ XBMC_events.h
+ VideoSync.h)
+
+core_add_library(windowing)
diff --git a/xbmc/windowing/GraphicContext.cpp b/xbmc/windowing/GraphicContext.cpp
new file mode 100644
index 0000000..423148c
--- /dev/null
+++ b/xbmc/windowing/GraphicContext.cpp
@@ -0,0 +1,1011 @@
+/*
+ * 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 "GraphicContext.h"
+
+#include "ServiceBroker.h"
+#include "WinSystem.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/TextureManager.h"
+#include "guilib/gui3d.h"
+#include "input/InputManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "rendering/RenderSystem.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/log.h"
+
+#include <cassert>
+#include <mutex>
+
+CGraphicContext::CGraphicContext() = default;
+CGraphicContext::~CGraphicContext() = default;
+
+void CGraphicContext::SetOrigin(float x, float y)
+{
+ if (!m_origins.empty())
+ m_origins.push(CPoint(x,y) + m_origins.top());
+ else
+ m_origins.push(CPoint(x,y));
+
+ AddTransform(TransformMatrix::CreateTranslation(x, y));
+}
+
+void CGraphicContext::RestoreOrigin()
+{
+ if (!m_origins.empty())
+ m_origins.pop();
+ RemoveTransform();
+}
+
+// add a new clip region, intersecting with the previous clip region.
+bool CGraphicContext::SetClipRegion(float x, float y, float w, float h)
+{ // transform from our origin
+ CPoint origin;
+ if (!m_origins.empty())
+ origin = m_origins.top();
+
+ // ok, now intersect with our old clip region
+ CRect rect(x, y, x + w, y + h);
+ rect += origin;
+ if (!m_clipRegions.empty())
+ {
+ // intersect with original clip region
+ rect.Intersect(m_clipRegions.top());
+ }
+
+ if (rect.IsEmpty())
+ return false;
+
+ m_clipRegions.push(rect);
+
+ // here we could set the hardware clipping, if applicable
+ return true;
+}
+
+void CGraphicContext::RestoreClipRegion()
+{
+ if (!m_clipRegions.empty())
+ m_clipRegions.pop();
+
+ // here we could reset the hardware clipping, if applicable
+}
+
+void CGraphicContext::ClipRect(CRect &vertex, CRect &texture, CRect *texture2)
+{
+ // this is the software clipping routine. If the graphics hardware is set to do the clipping
+ // (eg via SetClipPlane in D3D for instance) then this routine is unneeded.
+ if (!m_clipRegions.empty())
+ {
+ // take a copy of the vertex rectangle and intersect
+ // it with our clip region (moved to the same coordinate system)
+ CRect clipRegion(m_clipRegions.top());
+ if (!m_origins.empty())
+ clipRegion -= m_origins.top();
+ CRect original(vertex);
+ vertex.Intersect(clipRegion);
+ // and use the original to compute the texture coordinates
+ if (original != vertex)
+ {
+ float scaleX = texture.Width() / original.Width();
+ float scaleY = texture.Height() / original.Height();
+ texture.x1 += (vertex.x1 - original.x1) * scaleX;
+ texture.y1 += (vertex.y1 - original.y1) * scaleY;
+ texture.x2 += (vertex.x2 - original.x2) * scaleX;
+ texture.y2 += (vertex.y2 - original.y2) * scaleY;
+ if (texture2)
+ {
+ scaleX = texture2->Width() / original.Width();
+ scaleY = texture2->Height() / original.Height();
+ texture2->x1 += (vertex.x1 - original.x1) * scaleX;
+ texture2->y1 += (vertex.y1 - original.y1) * scaleY;
+ texture2->x2 += (vertex.x2 - original.x2) * scaleX;
+ texture2->y2 += (vertex.y2 - original.y2) * scaleY;
+ }
+ }
+ }
+}
+
+CRect CGraphicContext::GetClipRegion()
+{
+ if (m_clipRegions.empty())
+ return CRect(0, 0, m_iScreenWidth, m_iScreenHeight);
+ CRect clipRegion(m_clipRegions.top());
+ if (!m_origins.empty())
+ clipRegion -= m_origins.top();
+ return clipRegion;
+}
+
+void CGraphicContext::AddGUITransform()
+{
+ m_transforms.push(m_finalTransform);
+ m_finalTransform = m_guiTransform;
+}
+
+TransformMatrix CGraphicContext::AddTransform(const TransformMatrix &matrix)
+{
+ m_transforms.push(m_finalTransform);
+ m_finalTransform.matrix *= matrix;
+ return m_finalTransform.matrix;
+}
+
+void CGraphicContext::SetTransform(const TransformMatrix &matrix)
+{
+ m_transforms.push(m_finalTransform);
+ m_finalTransform.matrix = matrix;
+}
+
+void CGraphicContext::SetTransform(const TransformMatrix &matrix, float scaleX, float scaleY)
+{
+ m_transforms.push(m_finalTransform);
+ m_finalTransform.matrix = matrix;
+ m_finalTransform.scaleX = scaleX;
+ m_finalTransform.scaleY = scaleY;
+}
+
+void CGraphicContext::RemoveTransform()
+{
+ if (!m_transforms.empty())
+ {
+ m_finalTransform = m_transforms.top();
+ m_transforms.pop();
+ }
+}
+
+bool CGraphicContext::SetViewPort(float fx, float fy, float fwidth, float fheight, bool intersectPrevious /* = false */)
+{
+ // transform coordinates - we may have a rotation which changes the positioning of the
+ // minimal and maximal viewport extents. We currently go to the maximal extent.
+ float x[4], y[4];
+ x[0] = x[3] = fx;
+ x[1] = x[2] = fx + fwidth;
+ y[0] = y[1] = fy;
+ y[2] = y[3] = fy + fheight;
+ float minX = (float)m_iScreenWidth;
+ float maxX = 0;
+ float minY = (float)m_iScreenHeight;
+ float maxY = 0;
+ for (int i = 0; i < 4; i++)
+ {
+ float z = 0;
+ ScaleFinalCoords(x[i], y[i], z);
+ if (x[i] < minX) minX = x[i];
+ if (x[i] > maxX) maxX = x[i];
+ if (y[i] < minY) minY = y[i];
+ if (y[i] > maxY) maxY = y[i];
+ }
+
+ int newLeft = (int)(minX + 0.5f);
+ int newTop = (int)(minY + 0.5f);
+ int newRight = (int)(maxX + 0.5f);
+ int newBottom = (int)(maxY + 0.5f);
+ if (intersectPrevious)
+ {
+ CRect oldviewport = m_viewStack.top();
+ // do the intersection
+ int oldLeft = (int)oldviewport.x1;
+ int oldTop = (int)oldviewport.y1;
+ int oldRight = (int)oldviewport.x2;
+ int oldBottom = (int)oldviewport.y2;
+ if (newLeft >= oldRight || newTop >= oldBottom || newRight <= oldLeft || newBottom <= oldTop)
+ { // empty intersection - return false to indicate no rendering should occur
+ return false;
+ }
+ // ok, they intersect, do the intersection
+ if (newLeft < oldLeft) newLeft = oldLeft;
+ if (newTop < oldTop) newTop = oldTop;
+ if (newRight > oldRight) newRight = oldRight;
+ if (newBottom > oldBottom) newBottom = oldBottom;
+ }
+ // check range against screen size
+ if (newRight <= 0 || newBottom <= 0 ||
+ newTop >= m_iScreenHeight || newLeft >= m_iScreenWidth ||
+ newLeft >= newRight || newTop >= newBottom)
+ { // no intersection with the screen
+ return false;
+ }
+ // intersection with the screen
+ if (newLeft < 0) newLeft = 0;
+ if (newTop < 0) newTop = 0;
+ if (newRight > m_iScreenWidth) newRight = m_iScreenWidth;
+ if (newBottom > m_iScreenHeight) newBottom = m_iScreenHeight;
+
+ assert(newLeft < newRight);
+ assert(newTop < newBottom);
+
+ CRect newviewport((float)newLeft, (float)newTop, (float)newRight, (float)newBottom);
+
+ m_viewStack.push(newviewport);
+
+ newviewport = StereoCorrection(newviewport);
+ CServiceBroker::GetRenderSystem()->SetViewPort(newviewport);
+
+
+ UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top());
+ return true;
+}
+
+void CGraphicContext::RestoreViewPort()
+{
+ if (m_viewStack.size() <= 1) return;
+
+ m_viewStack.pop();
+ CRect viewport = StereoCorrection(m_viewStack.top());
+ CServiceBroker::GetRenderSystem()->SetViewPort(viewport);
+
+ UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top());
+}
+
+CPoint CGraphicContext::StereoCorrection(const CPoint &point) const
+{
+ CPoint res(point);
+
+ if(m_stereoMode == RENDER_STEREO_MODE_SPLIT_HORIZONTAL)
+ {
+ const RESOLUTION_INFO info = GetResInfo();
+
+ if(m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ res.y += info.iHeight + info.iBlanking;
+ }
+ if(m_stereoMode == RENDER_STEREO_MODE_SPLIT_VERTICAL)
+ {
+ const RESOLUTION_INFO info = GetResInfo();
+
+ if(m_stereoView == RENDER_STEREO_VIEW_RIGHT)
+ res.x += info.iWidth + info.iBlanking;
+ }
+ return res;
+}
+
+CRect CGraphicContext::StereoCorrection(const CRect &rect) const
+{
+ CRect res(StereoCorrection(rect.P1())
+ , StereoCorrection(rect.P2()));
+ return res;
+}
+
+void CGraphicContext::SetScissors(const CRect &rect)
+{
+ m_scissors = rect;
+ m_scissors.Intersect(CRect(0,0,(float)m_iScreenWidth, (float)m_iScreenHeight));
+ CServiceBroker::GetRenderSystem()->SetScissors(StereoCorrection(m_scissors));
+}
+
+const CRect &CGraphicContext::GetScissors() const
+{
+ return m_scissors;
+}
+
+void CGraphicContext::ResetScissors()
+{
+ m_scissors.SetRect(0, 0, (float)m_iScreenWidth, (float)m_iScreenHeight);
+ CServiceBroker::GetRenderSystem()->SetScissors(StereoCorrection(m_scissors));
+}
+
+const CRect CGraphicContext::GetViewWindow() const
+{
+ if (m_bCalibrating || m_bFullScreenVideo)
+ {
+ CRect rect;
+ RESOLUTION_INFO info = GetResInfo();
+ rect.x1 = (float)info.Overscan.left;
+ rect.y1 = (float)info.Overscan.top;
+ rect.x2 = (float)info.Overscan.right;
+ rect.y2 = (float)info.Overscan.bottom;
+ return rect;
+ }
+ return m_videoRect;
+}
+
+void CGraphicContext::SetViewWindow(float left, float top, float right, float bottom)
+{
+ m_videoRect.x1 = ScaleFinalXCoord(left, top);
+ m_videoRect.y1 = ScaleFinalYCoord(left, top);
+ m_videoRect.x2 = ScaleFinalXCoord(right, bottom);
+ m_videoRect.y2 = ScaleFinalYCoord(right, bottom);
+}
+
+void CGraphicContext::SetFullScreenVideo(bool bOnOff)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ m_bFullScreenVideo = bOnOff;
+
+ if (m_bFullScreenRoot)
+ {
+ bool bTriggerUpdateRes = false;
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (m_bFullScreenVideo)
+ bTriggerUpdateRes = true;
+ else
+ {
+ bool allowDesktopRes = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE) == ADJUST_REFRESHRATE_ALWAYS;
+ if (!allowDesktopRes)
+ {
+ if (appPlayer->IsPlayingVideo())
+ bTriggerUpdateRes = true;
+ }
+ }
+
+ bool allowResolutionChangeOnStop = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE) != ADJUST_REFRESHRATE_ON_START;
+ RESOLUTION targetResolutionOnStop = RES_DESKTOP;
+ if (bTriggerUpdateRes)
+ appPlayer->TriggerUpdateResolution();
+ else if (CDisplaySettings::GetInstance().GetCurrentResolution() > RES_DESKTOP)
+ {
+ targetResolutionOnStop = CDisplaySettings::GetInstance().GetCurrentResolution();
+ }
+
+ if (allowResolutionChangeOnStop && !bTriggerUpdateRes)
+ {
+ SetVideoResolution(targetResolutionOnStop, false);
+ }
+ }
+ else
+ SetVideoResolution(RES_WINDOW, false);
+}
+
+bool CGraphicContext::IsFullScreenVideo() const
+{
+ return m_bFullScreenVideo;
+}
+
+bool CGraphicContext::IsCalibrating() const
+{
+ return m_bCalibrating;
+}
+
+void CGraphicContext::SetCalibrating(bool bOnOff)
+{
+ m_bCalibrating = bOnOff;
+}
+
+bool CGraphicContext::IsValidResolution(RESOLUTION res)
+{
+ if (res >= RES_WINDOW && (size_t) res < CDisplaySettings::GetInstance().ResolutionInfoSize())
+ {
+ return true;
+ }
+
+ return false;
+}
+
+// call SetVideoResolutionInternal and ensure its done from mainthread
+void CGraphicContext::SetVideoResolution(RESOLUTION res, bool forceUpdate)
+{
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread())
+ {
+ SetVideoResolutionInternal(res, forceUpdate);
+ }
+ else
+ {
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_SETVIDEORESOLUTION, res, forceUpdate ? 1 : 0);
+ }
+}
+
+void CGraphicContext::SetVideoResolutionInternal(RESOLUTION res, bool forceUpdate)
+{
+ RESOLUTION lastRes = m_Resolution;
+
+ // If the user asked us to guess, go with desktop
+ if (!IsValidResolution(res))
+ {
+ res = RES_DESKTOP;
+ }
+
+ // If we are switching to the same resolution and same window/full-screen, no need to do anything
+ if (!forceUpdate && res == lastRes && m_bFullScreenRoot == CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen)
+ {
+ return;
+ }
+
+ if (res >= RES_DESKTOP)
+ {
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen = true;
+ m_bFullScreenRoot = true;
+ }
+ else
+ {
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen = false;
+ m_bFullScreenRoot = false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ // FIXME Wayland windowing needs some way to "deny" resolution updates since what Kodi
+ // requests might not get actually set by the compositor.
+ // So in theory, m_iScreenWidth etc. would not need to be updated at all before the
+ // change is confirmed.
+ // But other windowing code expects these variables to be already set when
+ // SetFullScreen() is called, so set them anyway and remember the old values.
+ int origScreenWidth = m_iScreenWidth;
+ int origScreenHeight = m_iScreenHeight;
+ float origFPSOverride = m_fFPSOverride;
+
+ UpdateInternalStateWithResolution(res);
+ RESOLUTION_INFO info_org = CDisplaySettings::GetInstance().GetResolutionInfo(res);
+
+ bool switched = false;
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen)
+ {
+#if defined (TARGET_DARWIN) || defined (TARGET_WINDOWS)
+ bool blankOtherDisplays = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_BLANKDISPLAYS);
+ switched = CServiceBroker::GetWinSystem()->SetFullScreen(true, info_org, blankOtherDisplays);
+#else
+ switched = CServiceBroker::GetWinSystem()->SetFullScreen(true, info_org, false);
+#endif
+ }
+ else if (lastRes >= RES_DESKTOP )
+ switched = CServiceBroker::GetWinSystem()->SetFullScreen(false, info_org, false);
+ else
+ switched = CServiceBroker::GetWinSystem()->ResizeWindow(info_org.iWidth, info_org.iHeight, -1, -1);
+
+ if (switched)
+ {
+ m_scissors.SetRect(0, 0, (float)m_iScreenWidth, (float)m_iScreenHeight);
+
+ // make sure all stereo stuff are correctly setup
+ SetStereoView(RENDER_STEREO_VIEW_OFF);
+
+ // update anyone that relies on sizing information
+ CServiceBroker::GetInputManager().SetMouseResolution(info_org.iWidth, info_org.iHeight, 1, 1);
+
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetWindowManager().SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_WINDOW_RESIZE);
+ }
+ else
+ {
+ // Reset old state
+ m_iScreenWidth = origScreenWidth;
+ m_iScreenHeight = origScreenHeight;
+ m_fFPSOverride = origFPSOverride;
+ if (IsValidResolution(lastRes))
+ {
+ m_Resolution = lastRes;
+ }
+ else
+ {
+ // FIXME Resolution has become invalid
+ // This happens e.g. when switching monitors and the new monitor has fewer
+ // resolutions than the old one. Fall back to RES_DESKTOP and hope that
+ // the real resolution is set soon.
+ // Again, must be fixed as part of a greater refactor.
+ m_Resolution = RES_DESKTOP;
+ }
+ }
+}
+
+void CGraphicContext::ApplyVideoResolution(RESOLUTION res)
+{
+ if (!IsValidResolution(res))
+ {
+ CLog::LogF(LOGWARNING, "Asked to apply invalid resolution {}, falling back to RES_DESKTOP",
+ res);
+ res = RES_DESKTOP;
+ }
+
+ if (res >= RES_DESKTOP)
+ {
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen = true;
+ m_bFullScreenRoot = true;
+ }
+ else
+ {
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen = false;
+ m_bFullScreenRoot = false;
+ }
+
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ UpdateInternalStateWithResolution(res);
+
+ m_scissors.SetRect(0, 0, (float)m_iScreenWidth, (float)m_iScreenHeight);
+
+ // make sure all stereo stuff are correctly setup
+ SetStereoView(RENDER_STEREO_VIEW_OFF);
+
+ // update anyone that relies on sizing information
+ RESOLUTION_INFO info_org = CDisplaySettings::GetInstance().GetResolutionInfo(res);
+ CServiceBroker::GetInputManager().SetMouseResolution(info_org.iWidth, info_org.iHeight, 1, 1);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_WINDOW_RESIZE);
+}
+
+void CGraphicContext::UpdateInternalStateWithResolution(RESOLUTION res)
+{
+ RESOLUTION_INFO info_mod = GetResInfo(res);
+
+ m_iScreenWidth = info_mod.iWidth;
+ m_iScreenHeight = info_mod.iHeight;
+ m_Resolution = res;
+ m_fFPSOverride = 0;
+}
+
+void CGraphicContext::ApplyModeChange(RESOLUTION res)
+{
+ ApplyVideoResolution(res);
+ CServiceBroker::GetWinSystem()->FinishModeChange(res);
+}
+
+void CGraphicContext::ApplyWindowResize(int newWidth, int newHeight)
+{
+ CServiceBroker::GetWinSystem()->SetWindowResolution(newWidth, newHeight);
+ ApplyVideoResolution(RES_WINDOW);
+ CServiceBroker::GetWinSystem()->FinishWindowResize(newWidth, newHeight);
+}
+
+RESOLUTION CGraphicContext::GetVideoResolution() const
+{
+ return m_Resolution;
+}
+
+void CGraphicContext::ResetOverscan(RESOLUTION_INFO &res)
+{
+ res.Overscan.left = 0;
+ res.Overscan.top = 0;
+ res.Overscan.right = res.iWidth;
+ res.Overscan.bottom = res.iHeight;
+}
+
+void CGraphicContext::ResetOverscan(RESOLUTION res, OVERSCAN &overscan)
+{
+ overscan.left = 0;
+ overscan.top = 0;
+
+ RESOLUTION_INFO info = GetResInfo(res);
+ overscan.right = info.iWidth;
+ overscan.bottom = info.iHeight;
+}
+
+void CGraphicContext::ResetScreenParameters(RESOLUTION res)
+{
+ RESOLUTION_INFO& info = CDisplaySettings::GetInstance().GetResolutionInfo(res);
+
+ info.iSubtitles = info.iHeight;
+ info.fPixelRatio = 1.0f;
+ info.iScreenWidth = info.iWidth;
+ info.iScreenHeight = info.iHeight;
+ ResetOverscan(res, info.Overscan);
+}
+
+void CGraphicContext::Clear(UTILS::COLOR::Color color)
+{
+ CServiceBroker::GetRenderSystem()->ClearBuffers(color);
+}
+
+void CGraphicContext::CaptureStateBlock()
+{
+ CServiceBroker::GetRenderSystem()->CaptureStateBlock();
+}
+
+void CGraphicContext::ApplyStateBlock()
+{
+ CServiceBroker::GetRenderSystem()->ApplyStateBlock();
+}
+
+const RESOLUTION_INFO CGraphicContext::GetResInfo(RESOLUTION res) const
+{
+ RESOLUTION_INFO info = CDisplaySettings::GetInstance().GetResolutionInfo(res);
+
+ if(m_stereoMode == RENDER_STEREO_MODE_SPLIT_HORIZONTAL)
+ {
+ if((info.dwFlags & D3DPRESENTFLAG_MODE3DTB) == 0)
+ {
+ info.fPixelRatio /= 2;
+ info.iBlanking = 0;
+ info.dwFlags |= D3DPRESENTFLAG_MODE3DTB;
+ }
+ info.iHeight = (info.iHeight - info.iBlanking) / 2;
+ info.Overscan.top /= 2;
+ info.Overscan.bottom = (info.Overscan.bottom - info.iBlanking) / 2;
+ info.iSubtitles = (info.iSubtitles - info.iBlanking) / 2;
+ }
+
+ if(m_stereoMode == RENDER_STEREO_MODE_SPLIT_VERTICAL)
+ {
+ if((info.dwFlags & D3DPRESENTFLAG_MODE3DSBS) == 0)
+ {
+ info.fPixelRatio *= 2;
+ info.iBlanking = 0;
+ info.dwFlags |= D3DPRESENTFLAG_MODE3DSBS;
+ }
+ info.iWidth = (info.iWidth - info.iBlanking) / 2;
+ info.Overscan.left /= 2;
+ info.Overscan.right = (info.Overscan.right - info.iBlanking) / 2;
+ }
+
+ if (res == m_Resolution && m_fFPSOverride != 0)
+ {
+ info.fRefreshRate = m_fFPSOverride;
+ }
+
+ return info;
+}
+
+void CGraphicContext::SetResInfo(RESOLUTION res, const RESOLUTION_INFO& info)
+{
+ RESOLUTION_INFO& curr = CDisplaySettings::GetInstance().GetResolutionInfo(res);
+ curr.Overscan = info.Overscan;
+ curr.iSubtitles = info.iSubtitles;
+ curr.fPixelRatio = info.fPixelRatio;
+
+ if(info.dwFlags & D3DPRESENTFLAG_MODE3DSBS)
+ {
+ curr.Overscan.right = info.Overscan.right * 2 + info.iBlanking;
+ if((curr.dwFlags & D3DPRESENTFLAG_MODE3DSBS) == 0)
+ curr.fPixelRatio /= 2.0f;
+ }
+
+ if(info.dwFlags & D3DPRESENTFLAG_MODE3DTB)
+ {
+ curr.Overscan.bottom = info.Overscan.bottom * 2 + info.iBlanking;
+ curr.iSubtitles = info.iSubtitles * 2 + info.iBlanking;
+ if((curr.dwFlags & D3DPRESENTFLAG_MODE3DTB) == 0)
+ curr.fPixelRatio *= 2.0f;
+ }
+}
+
+const RESOLUTION_INFO CGraphicContext::GetResInfo() const
+{
+ return GetResInfo(m_Resolution);
+}
+
+void CGraphicContext::GetGUIScaling(const RESOLUTION_INFO &res, float &scaleX, float &scaleY, TransformMatrix *matrix /* = NULL */)
+{
+ if (m_Resolution != RES_INVALID)
+ {
+ // calculate necessary scalings
+ RESOLUTION_INFO info = GetResInfo();
+ float fFromWidth = (float)res.iWidth;
+ float fFromHeight = (float)res.iHeight;
+ auto fToPosX = info.Overscan.left + info.guiInsets.left;
+ auto fToPosY = info.Overscan.top + info.guiInsets.top;
+ auto fToWidth = info.Overscan.right - info.guiInsets.right - fToPosX;
+ auto fToHeight = info.Overscan.bottom - info.guiInsets.bottom - fToPosY;
+
+ float fZoom = (100 + CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_LOOKANDFEEL_SKINZOOM)) * 0.01f;
+
+ fZoom -= 1.0f;
+ fToPosX -= fToWidth * fZoom * 0.5f;
+ fToWidth *= fZoom + 1.0f;
+
+ // adjust for aspect ratio as zoom is given in the vertical direction and we don't
+ // do aspect ratio corrections in the gui code
+ fZoom = fZoom / info.fPixelRatio;
+ fToPosY -= fToHeight * fZoom * 0.5f;
+ fToHeight *= fZoom + 1.0f;
+
+ scaleX = fFromWidth / fToWidth;
+ scaleY = fFromHeight / fToHeight;
+ if (matrix)
+ {
+ TransformMatrix guiScaler = TransformMatrix::CreateScaler(fToWidth / fFromWidth, fToHeight / fFromHeight, fToHeight / fFromHeight);
+ TransformMatrix guiOffset = TransformMatrix::CreateTranslation(fToPosX, fToPosY);
+ *matrix = guiOffset * guiScaler;
+ }
+ }
+ else
+ {
+ scaleX = scaleY = 1.0f;
+ if (matrix)
+ matrix->Reset();
+ }
+}
+
+void CGraphicContext::SetScalingResolution(const RESOLUTION_INFO &res, bool needsScaling)
+{
+ m_windowResolution = res;
+ if (needsScaling && m_Resolution != RES_INVALID)
+ GetGUIScaling(res, m_guiTransform.scaleX, m_guiTransform.scaleY, &m_guiTransform.matrix);
+ else
+ {
+ m_guiTransform.Reset();
+ }
+
+ // reset our origin and camera
+ while (!m_origins.empty())
+ m_origins.pop();
+ m_origins.push(CPoint(0, 0));
+ while (!m_cameras.empty())
+ m_cameras.pop();
+ m_cameras.push(CPoint(0.5f*m_iScreenWidth, 0.5f*m_iScreenHeight));
+ while (!m_stereoFactors.empty())
+ m_stereoFactors.pop();
+ m_stereoFactors.push(0.0f);
+
+ // and reset the final transform
+ m_finalTransform = m_guiTransform;
+}
+
+void CGraphicContext::SetRenderingResolution(const RESOLUTION_INFO &res, bool needsScaling)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ SetScalingResolution(res, needsScaling);
+ UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top());
+}
+
+void CGraphicContext::SetStereoView(RENDER_STEREO_VIEW view)
+{
+ m_stereoView = view;
+
+ while(!m_viewStack.empty())
+ m_viewStack.pop();
+
+ CRect viewport(0.0f, 0.0f, (float)m_iScreenWidth, (float)m_iScreenHeight);
+
+ m_viewStack.push(viewport);
+
+ viewport = StereoCorrection(viewport);
+ CServiceBroker::GetRenderSystem()->SetStereoMode(m_stereoMode, m_stereoView);
+ CServiceBroker::GetRenderSystem()->SetViewPort(viewport);
+ CServiceBroker::GetRenderSystem()->SetScissors(viewport);
+}
+
+void CGraphicContext::InvertFinalCoords(float &x, float &y) const
+{
+ m_finalTransform.matrix.InverseTransformPosition(x, y);
+}
+
+float CGraphicContext::ScaleFinalXCoord(float x, float y) const
+{
+ return m_finalTransform.matrix.TransformXCoord(x, y, 0);
+}
+
+float CGraphicContext::ScaleFinalYCoord(float x, float y) const
+{
+ return m_finalTransform.matrix.TransformYCoord(x, y, 0);
+}
+
+float CGraphicContext::ScaleFinalZCoord(float x, float y) const
+{
+ return m_finalTransform.matrix.TransformZCoord(x, y, 0);
+}
+
+void CGraphicContext::ScaleFinalCoords(float &x, float &y, float &z) const
+{
+ m_finalTransform.matrix.TransformPosition(x, y, z);
+}
+
+float CGraphicContext::GetScalingPixelRatio() const
+{
+ // assume the resolutions are different - we want to return the aspect ratio of the video resolution
+ // but only once it's been corrected for the skin -> screen coordinates scaling
+ return GetResInfo().fPixelRatio * (m_finalTransform.scaleY / m_finalTransform.scaleX);
+}
+
+void CGraphicContext::SetCameraPosition(const CPoint &camera)
+{
+ // offset the camera from our current location (this is in XML coordinates) and scale it up to
+ // the screen resolution
+ CPoint cam(camera);
+ if (!m_origins.empty())
+ cam += m_origins.top();
+
+ cam.x *= (float)m_iScreenWidth / m_windowResolution.iWidth;
+ cam.y *= (float)m_iScreenHeight / m_windowResolution.iHeight;
+
+ m_cameras.push(cam);
+ UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top());
+}
+
+void CGraphicContext::RestoreCameraPosition()
+{ // remove the top camera from the stack
+ assert(m_cameras.size());
+ m_cameras.pop();
+ UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top());
+}
+
+void CGraphicContext::SetStereoFactor(float factor)
+{
+ m_stereoFactors.push(factor);
+ UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top());
+}
+
+void CGraphicContext::RestoreStereoFactor()
+{ // remove the top factor from the stack
+ assert(m_stereoFactors.size());
+ m_stereoFactors.pop();
+ UpdateCameraPosition(m_cameras.top(), m_stereoFactors.top());
+}
+
+CRect CGraphicContext::GenerateAABB(const CRect &rect) const
+{
+// ------------------------
+// |(x1, y1) (x2, y2)|
+// | |
+// |(x3, y3) (x4, y4)|
+// ------------------------
+
+ float x1 = rect.x1, x2 = rect.x2, x3 = rect.x1, x4 = rect.x2;
+ float y1 = rect.y1, y2 = rect.y1, y3 = rect.y2, y4 = rect.y2;
+
+ float z = 0.0f;
+ ScaleFinalCoords(x1, y1, z);
+ CServiceBroker::GetRenderSystem()->Project(x1, y1, z);
+
+ z = 0.0f;
+ ScaleFinalCoords(x2, y2, z);
+ CServiceBroker::GetRenderSystem()->Project(x2, y2, z);
+
+ z = 0.0f;
+ ScaleFinalCoords(x3, y3, z);
+ CServiceBroker::GetRenderSystem()->Project(x3, y3, z);
+
+ z = 0.0f;
+ ScaleFinalCoords(x4, y4, z);
+ CServiceBroker::GetRenderSystem()->Project(x4, y4, z);
+
+ return CRect( std::min(std::min(std::min(x1, x2), x3), x4),
+ std::min(std::min(std::min(y1, y2), y3), y4),
+ std::max(std::max(std::max(x1, x2), x3), x4),
+ std::max(std::max(std::max(y1, y2), y3), y4));
+}
+
+// NOTE: This routine is currently called (twice) every time there is a <camera>
+// tag in the skin. It actually only has to be called before we render
+// something, so another option is to just save the camera coordinates
+// and then have a routine called before every draw that checks whether
+// the camera has changed, and if so, changes it. Similarly, it could set
+// the world transform at that point as well (or even combine world + view
+// to cut down on one setting)
+void CGraphicContext::UpdateCameraPosition(const CPoint &camera, const float &factor)
+{
+ float stereoFactor = 0.f;
+ if ( m_stereoMode != RENDER_STEREO_MODE_OFF
+ && m_stereoMode != RENDER_STEREO_MODE_MONO
+ && m_stereoView != RENDER_STEREO_VIEW_OFF)
+ {
+ RESOLUTION_INFO res = GetResInfo();
+ RESOLUTION_INFO desktop = GetResInfo(RES_DESKTOP);
+ float scaleRes = (static_cast<float>(res.iWidth) / static_cast<float>(desktop.iWidth));
+ float scaleX = static_cast<float>(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_LOOKANDFEEL_STEREOSTRENGTH)) * scaleRes;
+ stereoFactor = factor * (m_stereoView == RENDER_STEREO_VIEW_LEFT ? scaleX : -scaleX);
+ }
+ CServiceBroker::GetRenderSystem()->SetCameraPosition(camera, m_iScreenWidth, m_iScreenHeight, stereoFactor);
+}
+
+bool CGraphicContext::RectIsAngled(float x1, float y1, float x2, float y2) const
+{ // need only test 3 points, as they must be co-planer
+ if (m_finalTransform.matrix.TransformZCoord(x1, y1, 0)) return true;
+ if (m_finalTransform.matrix.TransformZCoord(x2, y2, 0)) return true;
+ if (m_finalTransform.matrix.TransformZCoord(x1, y2, 0)) return true;
+ return false;
+}
+
+const TransformMatrix &CGraphicContext::GetGUIMatrix() const
+{
+ return m_finalTransform.matrix;
+}
+
+float CGraphicContext::GetGUIScaleX() const
+{
+ return m_finalTransform.scaleX;
+}
+
+float CGraphicContext::GetGUIScaleY() const
+{
+ return m_finalTransform.scaleY;
+}
+
+UTILS::COLOR::Color CGraphicContext::MergeAlpha(UTILS::COLOR::Color color) const
+{
+ UTILS::COLOR::Color alpha = m_finalTransform.matrix.TransformAlpha((color >> 24) & 0xff);
+ if (alpha > 255) alpha = 255;
+ return ((alpha << 24) & 0xff000000) | (color & 0xffffff);
+}
+
+UTILS::COLOR::Color CGraphicContext::MergeColor(UTILS::COLOR::Color color) const
+{
+ return m_finalTransform.matrix.TransformColor(color);
+}
+
+int CGraphicContext::GetWidth() const
+{
+ return m_iScreenWidth;
+}
+
+int CGraphicContext::GetHeight() const
+{
+ return m_iScreenHeight;
+}
+
+float CGraphicContext::GetFPS() const
+{
+ if (m_Resolution != RES_INVALID)
+ {
+ RESOLUTION_INFO info = GetResInfo();
+ if (info.fRefreshRate > 0)
+ return info.fRefreshRate;
+ }
+ return 60.0f;
+}
+
+float CGraphicContext::GetDisplayLatency() const
+{
+ float latency = CServiceBroker::GetWinSystem()->GetDisplayLatency();
+ if (latency < 0.0f)
+ {
+ // fallback
+ latency = (CServiceBroker::GetWinSystem()->NoOfBuffers() + 1) / GetFPS() * 1000.0f;
+ }
+
+ return latency;
+}
+
+bool CGraphicContext::IsFullScreenRoot () const
+{
+ return m_bFullScreenRoot;
+}
+
+void CGraphicContext::ToggleFullScreen()
+{
+ RESOLUTION uiRes;
+
+ if (m_bFullScreenRoot)
+ {
+ uiRes = RES_WINDOW;
+ }
+ else
+ {
+ if (CDisplaySettings::GetInstance().GetCurrentResolution() > RES_DESKTOP)
+ uiRes = CDisplaySettings::GetInstance().GetCurrentResolution();
+ else
+ uiRes = RES_DESKTOP;
+ }
+
+ CDisplaySettings::GetInstance().SetCurrentResolution(uiRes, true);
+}
+
+void CGraphicContext::SetMediaDir(const std::string &strMediaDir)
+{
+ CServiceBroker::GetGUI()->GetTextureManager().SetTexturePath(strMediaDir);
+ m_strMediaDir = strMediaDir;
+}
+
+const std::string& CGraphicContext::GetMediaDir() const
+{
+ return m_strMediaDir;
+
+}
+
+void CGraphicContext::Flip(bool rendered, bool videoLayer)
+{
+ CServiceBroker::GetRenderSystem()->PresentRender(rendered, videoLayer);
+
+ if(m_stereoMode != m_nextStereoMode)
+ {
+ m_stereoMode = m_nextStereoMode;
+ SetVideoResolution(GetVideoResolution(), true);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_RENDERER_RESET);
+ }
+}
+
+void CGraphicContext::GetAllowedResolutions(std::vector<RESOLUTION> &res)
+{
+ res.clear();
+
+ res.push_back(RES_WINDOW);
+ res.push_back(RES_DESKTOP);
+ for (size_t r = (size_t) RES_CUSTOM; r < CDisplaySettings::GetInstance().ResolutionInfoSize(); r++)
+ {
+ res.push_back((RESOLUTION) r);
+ }
+}
+
+void CGraphicContext::SetFPS(float fps)
+{
+ m_fFPSOverride = fps;
+}
diff --git a/xbmc/windowing/GraphicContext.h b/xbmc/windowing/GraphicContext.h
new file mode 100644
index 0000000..9cd4f32
--- /dev/null
+++ b/xbmc/windowing/GraphicContext.h
@@ -0,0 +1,232 @@
+/*
+ * 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 "Resolution.h"
+#include "rendering/RenderSystem.h"
+#include "threads/CriticalSection.h"
+#include "utils/ColorUtils.h"
+#include "utils/Geometry.h" // for CRect/CPoint
+#include "utils/TransformMatrix.h" // for the members m_guiTransform etc.
+
+#include <map>
+#include <stack>
+#include <string>
+#include <vector>
+
+// required by clients
+#include "ServiceBroker.h"
+#include "WinSystem.h"
+
+#define D3DPRESENTFLAG_INTERLACED 1
+#define D3DPRESENTFLAG_WIDESCREEN 2
+#define D3DPRESENTFLAG_PROGRESSIVE 4
+#define D3DPRESENTFLAG_MODE3DSBS 8
+#define D3DPRESENTFLAG_MODE3DTB 16
+
+/* what types are important for mode setting */
+#define D3DPRESENTFLAG_MODEMASK ( D3DPRESENTFLAG_INTERLACED \
+ | D3DPRESENTFLAG_MODE3DSBS \
+ | D3DPRESENTFLAG_MODE3DTB )
+
+enum VIEW_TYPE { VIEW_TYPE_NONE = 0,
+ VIEW_TYPE_LIST,
+ VIEW_TYPE_ICON,
+ VIEW_TYPE_BIG_LIST,
+ VIEW_TYPE_BIG_ICON,
+ VIEW_TYPE_WIDE,
+ VIEW_TYPE_BIG_WIDE,
+ VIEW_TYPE_WRAP,
+ VIEW_TYPE_BIG_WRAP,
+ VIEW_TYPE_INFO,
+ VIEW_TYPE_BIG_INFO,
+ VIEW_TYPE_AUTO,
+ VIEW_TYPE_MAX };
+
+enum AdjustRefreshRate
+{
+ ADJUST_REFRESHRATE_OFF = 0,
+ ADJUST_REFRESHRATE_ALWAYS,
+ ADJUST_REFRESHRATE_ON_STARTSTOP,
+ ADJUST_REFRESHRATE_ON_START,
+};
+
+class CGraphicContext : public CCriticalSection
+{
+public:
+ CGraphicContext(void);
+ virtual ~CGraphicContext();
+
+ // methods related to windowing
+ float GetFPS() const;
+ void SetFPS(float fps);
+ float GetDisplayLatency() const;
+ bool IsFullScreenRoot() const;
+ void ToggleFullScreen();
+ void SetFullScreenVideo(bool bOnOff);
+ bool IsFullScreenVideo() const;
+ bool IsValidResolution(RESOLUTION res);
+ void SetVideoResolution(RESOLUTION res, bool forceUpdate);
+ void ApplyModeChange(RESOLUTION res);
+ void ApplyWindowResize(int newWidth, int newHeight);
+ RESOLUTION GetVideoResolution() const;
+ const RESOLUTION_INFO GetResInfo() const;
+ const RESOLUTION_INFO GetResInfo(RESOLUTION res) const;
+ void SetResInfo(RESOLUTION res, const RESOLUTION_INFO& info);
+
+ void Flip(bool rendered, bool videoLayer);
+
+ // gfx context interface
+ int GetWidth() const;
+ int GetHeight() const;
+ bool SetViewPort(float fx, float fy , float fwidth, float fheight, bool intersectPrevious = false);
+ void RestoreViewPort();
+ void SetScissors(const CRect &rect);
+ void ResetScissors();
+ const CRect &GetScissors() const;
+ const CRect GetViewWindow() const;
+ void SetViewWindow(float left, float top, float right, float bottom);
+ bool IsCalibrating() const;
+ void SetCalibrating(bool bOnOff);
+ void ResetOverscan(RESOLUTION res, OVERSCAN &overscan);
+ void ResetOverscan(RESOLUTION_INFO &resinfo);
+ void ResetScreenParameters(RESOLUTION res);
+ void CaptureStateBlock();
+ void ApplyStateBlock();
+ void Clear(UTILS::COLOR::Color color = 0);
+ void GetAllowedResolutions(std::vector<RESOLUTION> &res);
+
+ /* \brief Get UI scaling information from a given resolution to the screen resolution.
+ Takes account of overscan and UI zooming.
+ \param res the resolution to scale from.
+ \param scaleX [out] the scaling amount in the X direction.
+ \param scaleY [out] the scaling amount in the Y direction.
+ \param matrix [out] if non-NULL, a suitable transformation from res to screen resolution is set.
+ */
+ void GetGUIScaling(const RESOLUTION_INFO &res, float &scaleX, float &scaleY, TransformMatrix *matrix = NULL);
+ void SetRenderingResolution(const RESOLUTION_INFO &res, bool needsScaling); ///< Sets scaling up for rendering
+ void SetScalingResolution(const RESOLUTION_INFO &res, bool needsScaling); ///< Sets scaling up for skin loading etc.
+ float GetScalingPixelRatio() const;
+ void InvertFinalCoords(float &x, float &y) const;
+ float ScaleFinalXCoord(float x, float y) const;
+ float ScaleFinalYCoord(float x, float y) const;
+ float ScaleFinalZCoord(float x, float y) const;
+ void ScaleFinalCoords(float &x, float &y, float &z) const;
+ bool RectIsAngled(float x1, float y1, float x2, float y2) const;
+ const TransformMatrix &GetGUIMatrix() const;
+ float GetGUIScaleX() const;
+ float GetGUIScaleY() const;
+ UTILS::COLOR::Color MergeAlpha(UTILS::COLOR::Color color) const;
+ UTILS::COLOR::Color MergeColor(UTILS::COLOR::Color color) const;
+ void SetOrigin(float x, float y);
+ void RestoreOrigin();
+ void SetCameraPosition(const CPoint &camera);
+ void SetStereoView(RENDER_STEREO_VIEW view);
+ RENDER_STEREO_VIEW GetStereoView() { return m_stereoView; }
+ void SetStereoMode(RENDER_STEREO_MODE mode) { m_nextStereoMode = mode; }
+ RENDER_STEREO_MODE GetStereoMode() { return m_stereoMode; }
+ void RestoreCameraPosition();
+ void SetStereoFactor(float factor);
+ void RestoreStereoFactor();
+ /*! \brief Set a region in which to clip all rendering
+ Anything that is rendered after setting a clip region will be clipped so that no part renders
+ outside of the clip region. Successive calls to SetClipRegion intersect the clip region, which
+ means the clip region may eventually become an empty set. In this case SetClipRegion returns false
+ to indicate that no rendering need be performed.
+
+ This call must be matched with a RestoreClipRegion call unless SetClipRegion returns false.
+
+ Usage should be of the form:
+
+ if (SetClipRegion(x, y, w, h))
+ {
+ ...
+ perform rendering
+ ...
+ RestoreClipRegion();
+ }
+
+ \param x the left-most coordinate of the clip region
+ \param y the top-most coordinate of the clip region
+ \param w the width of the clip region
+ \param h the height of the clip region
+ \returns true if the region is set and the result is non-empty. Returns false if the resulting region is empty.
+ \sa RestoreClipRegion
+ */
+ bool SetClipRegion(float x, float y, float w, float h);
+ void RestoreClipRegion();
+ void ClipRect(CRect &vertex, CRect &texture, CRect *diffuse = NULL);
+ CRect GetClipRegion();
+ void AddGUITransform();
+ TransformMatrix AddTransform(const TransformMatrix &matrix);
+ void SetTransform(const TransformMatrix &matrix);
+ void SetTransform(const TransformMatrix &matrix, float scaleX, float scaleY);
+ void RemoveTransform();
+
+ /* modifies final coordinates according to stereo mode if needed */
+ CRect StereoCorrection(const CRect &rect) const;
+ CPoint StereoCorrection(const CPoint &point) const;
+
+ CRect GenerateAABB(const CRect &rect) const;
+
+ //@todo move those somewhere else
+ const std::string& GetMediaDir() const;
+ void SetMediaDir(const std::string& strMediaDir);
+
+protected:
+
+ void UpdateCameraPosition(const CPoint &camera, const float &factor);
+ void SetVideoResolutionInternal(RESOLUTION res, bool forceUpdate);
+ void ApplyVideoResolution(RESOLUTION res);
+ void UpdateInternalStateWithResolution(RESOLUTION res);
+
+ int m_iScreenHeight = 576;
+ int m_iScreenWidth = 720;
+ std::string m_strMediaDir;
+ CRect m_videoRect;
+ bool m_bFullScreenRoot = true;
+ bool m_bFullScreenVideo = false;
+ bool m_bCalibrating = false;
+ RESOLUTION m_Resolution = RES_INVALID;
+ float m_fFPSOverride = 0.0f;
+
+ RESOLUTION_INFO m_windowResolution;
+ std::stack<CPoint> m_cameras;
+ std::stack<CPoint> m_origins;
+ std::stack<CRect> m_clipRegions;
+ std::stack<float> m_stereoFactors;
+ std::stack<CRect> m_viewStack;
+ CRect m_scissors;
+
+ class UITransform
+ {
+ public:
+ UITransform() : matrix() {}
+ UITransform(const TransformMatrix& m, const float sX = 1.0f, const float sY = 1.0f)
+ : matrix(m), scaleX(sX), scaleY(sY)
+ {
+ }
+ void Reset()
+ {
+ matrix.Reset();
+ scaleX = scaleY = 1.0f;
+ }
+
+ TransformMatrix matrix;
+ float scaleX = 1.0f;
+ float scaleY = 1.0f;
+ };
+
+ UITransform m_guiTransform;
+ UITransform m_finalTransform;
+ std::stack<UITransform> m_transforms;
+ RENDER_STEREO_VIEW m_stereoView = RENDER_STEREO_VIEW_OFF;
+ RENDER_STEREO_MODE m_stereoMode = RENDER_STEREO_MODE_OFF;
+ RENDER_STEREO_MODE m_nextStereoMode = RENDER_STEREO_MODE_OFF;
+};
diff --git a/xbmc/windowing/OSScreenSaver.cpp b/xbmc/windowing/OSScreenSaver.cpp
new file mode 100644
index 0000000..3651314
--- /dev/null
+++ b/xbmc/windowing/OSScreenSaver.cpp
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017-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 "OSScreenSaver.h"
+
+#include "utils/log.h"
+
+using namespace KODI::WINDOWING;
+
+COSScreenSaverManager::COSScreenSaverManager(std::unique_ptr<IOSScreenSaver> impl)
+: m_impl{std::move(impl)}
+{
+}
+
+COSScreenSaverInhibitor COSScreenSaverManager::CreateInhibitor()
+{
+ COSScreenSaverInhibitor inhibitor{this};
+ if (m_inhibitionCount++ == 0)
+ {
+ // Inhibit if this was first inhibitor
+ CLog::Log(LOGDEBUG, "Inhibiting OS screen saver");
+ m_impl->Inhibit();
+ }
+ return inhibitor;
+}
+
+bool COSScreenSaverManager::IsInhibited()
+{
+ return (m_inhibitionCount > 0);
+}
+
+void COSScreenSaverManager::RemoveInhibitor()
+{
+ if (--m_inhibitionCount == 0)
+ {
+ CLog::Log(LOGDEBUG, "Uninhibiting OS screen saver");
+ // Uninhibit if this was last inhibitor
+ m_impl->Uninhibit();
+ }
+}
+
+COSScreenSaverInhibitor::COSScreenSaverInhibitor() noexcept
+: m_active{false}, m_manager{}
+{
+}
+
+COSScreenSaverInhibitor::COSScreenSaverInhibitor(COSScreenSaverManager* manager)
+: m_active{true}, m_manager{manager}
+{
+}
+
+COSScreenSaverInhibitor::COSScreenSaverInhibitor(COSScreenSaverInhibitor&& other) noexcept
+: m_active{false}, m_manager{}
+{
+ *this = std::move(other);
+}
+
+COSScreenSaverInhibitor& COSScreenSaverInhibitor::operator=(COSScreenSaverInhibitor&& other) noexcept
+{
+ Release();
+ m_active = other.m_active;
+ m_manager = other.m_manager;
+ other.m_active = false;
+ other.m_manager = nullptr;
+ return *this;
+}
+
+bool COSScreenSaverInhibitor::IsActive() const
+{
+ return m_active;
+}
+
+COSScreenSaverInhibitor::operator bool() const
+{
+ return IsActive();
+}
+
+void COSScreenSaverInhibitor::Release()
+{
+ if (m_active)
+ {
+ m_manager->RemoveInhibitor();
+ m_active = false;
+ }
+}
+
+COSScreenSaverInhibitor::~COSScreenSaverInhibitor() noexcept
+{
+ Release();
+}
+
+
+
diff --git a/xbmc/windowing/OSScreenSaver.h b/xbmc/windowing/OSScreenSaver.h
new file mode 100644
index 0000000..367d6db
--- /dev/null
+++ b/xbmc/windowing/OSScreenSaver.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017-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 <memory>
+#include <utility>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+
+class COSScreenSaverManager;
+
+/**
+ * Inhibit the OS screen saver as long as this object is alive
+ *
+ * Destroy or call \ref Release to stop this inhibitor from being active.
+ * The OS screen saver may still be inhibited as long as other inhibitors are
+ * active though.
+ *
+ * \note Make sure to release or destroy the inhibitor before the \ref
+ * COSScreenSaverManager is destroyed
+ */
+class COSScreenSaverInhibitor
+{
+public:
+ COSScreenSaverInhibitor() noexcept;
+ COSScreenSaverInhibitor(COSScreenSaverInhibitor&& other) noexcept;
+ COSScreenSaverInhibitor& operator=(COSScreenSaverInhibitor&& other) noexcept;
+ ~COSScreenSaverInhibitor() noexcept;
+ void Release();
+ bool IsActive() const;
+ operator bool() const;
+
+private:
+ friend class COSScreenSaverManager;
+ explicit COSScreenSaverInhibitor(COSScreenSaverManager* manager);
+ bool m_active;
+ COSScreenSaverManager* m_manager;
+
+ COSScreenSaverInhibitor(COSScreenSaverInhibitor const& other) = delete;
+ COSScreenSaverInhibitor& operator=(COSScreenSaverInhibitor const& other) = delete;
+};
+
+/**
+ * Interface for OS screen saver control implementations
+ */
+class IOSScreenSaver
+{
+public:
+ virtual ~IOSScreenSaver() = default;
+ /**
+ * Do not allow the OS screen saver to become active
+ *
+ * Calling this function multiple times without calling \ref Unhibit
+ * MUST NOT produce any side-effects.
+ */
+ virtual void Inhibit() = 0;
+ /**
+ * Allow the OS screen saver to become active again
+ *
+ * Calling this function multiple times or at all without calling \ref Inhibit
+ * MUST NOT produce any side-effects.
+ */
+ virtual void Uninhibit() = 0;
+};
+
+/**
+ * Dummy implementation of IOSScreenSaver
+ */
+class CDummyOSScreenSaver : public IOSScreenSaver
+{
+public:
+ void Inhibit() override {}
+ void Uninhibit() override {}
+};
+
+/**
+ * Manage the OS screen saver
+ *
+ * This class keeps track of a number of \ref COSScreenSaverInhibitor instances
+ * and keeps the OS screen saver inhibited as long as at least one of them
+ * exists and is active.
+ */
+class COSScreenSaverManager
+{
+public:
+ /**
+ * Create manager with backing OS-specific implementation
+ */
+ explicit COSScreenSaverManager(std::unique_ptr<IOSScreenSaver> impl);
+ /**
+ * Create inhibitor that prevents the OS screen saver from becoming active as
+ * long as it is alive
+ */
+ COSScreenSaverInhibitor CreateInhibitor();
+ /**
+ * Check whether the OS screen saver is currently inhibited
+ */
+ bool IsInhibited();
+
+private:
+ friend class COSScreenSaverInhibitor;
+ void RemoveInhibitor();
+
+ unsigned int m_inhibitionCount{0u};
+ std::unique_ptr<IOSScreenSaver> m_impl;
+};
+
+}
+}
diff --git a/xbmc/windowing/Resolution.cpp b/xbmc/windowing/Resolution.cpp
new file mode 100644
index 0000000..8d9af06
--- /dev/null
+++ b/xbmc/windowing/Resolution.cpp
@@ -0,0 +1,474 @@
+/*
+ * 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 "Resolution.h"
+
+#include "GraphicContext.h"
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/MathUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <cstdlib>
+#include <limits>
+
+namespace
+{
+
+const char* SETTING_VIDEOSCREEN_WHITELIST_PULLDOWN{"videoscreen.whitelistpulldown"};
+const char* SETTING_VIDEOSCREEN_WHITELIST_DOUBLEREFRESHRATE{
+ "videoscreen.whitelistdoublerefreshrate"};
+
+} // namespace
+
+EdgeInsets::EdgeInsets(float l, float t, float r, float b) : left(l), top(t), right(r), bottom(b)
+{
+}
+
+RESOLUTION_INFO::RESOLUTION_INFO(int width, int height, float aspect, const std::string &mode) :
+ strMode(mode)
+{
+ iWidth = width;
+ iHeight = height;
+ iBlanking = 0;
+ iScreenWidth = width;
+ iScreenHeight = height;
+ fPixelRatio = aspect ? ((float)width)/height / aspect : 1.0f;
+ bFullScreen = true;
+ fRefreshRate = 0;
+ dwFlags = iSubtitles = 0;
+}
+
+RESOLUTION_INFO::RESOLUTION_INFO(const RESOLUTION_INFO& res)
+ : Overscan(res.Overscan),
+ guiInsets(res.guiInsets),
+ strMode(res.strMode),
+ strOutput(res.strOutput),
+ strId(res.strId)
+{
+ bFullScreen = res.bFullScreen;
+ iWidth = res.iWidth; iHeight = res.iHeight;
+ iScreenWidth = res.iScreenWidth; iScreenHeight = res.iScreenHeight;
+ iSubtitles = res.iSubtitles; dwFlags = res.dwFlags;
+ fPixelRatio = res.fPixelRatio; fRefreshRate = res.fRefreshRate;
+ iBlanking = res.iBlanking;
+}
+
+float RESOLUTION_INFO::DisplayRatio() const
+{
+ return iWidth * fPixelRatio / iHeight;
+}
+
+RESOLUTION CResolutionUtils::ChooseBestResolution(float fps, int width, int height, bool is3D)
+{
+ RESOLUTION res = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution();
+ float weight = 0.0f;
+
+ if (!FindResolutionFromOverride(fps, width, is3D, res, weight, false)) //find a refreshrate from overrides
+ {
+ if (!FindResolutionFromOverride(fps, width, is3D, res, weight, true)) //if that fails find it from a fallback
+ {
+ FindResolutionFromWhitelist(fps, width, height, is3D, res); //find a refreshrate from whitelist
+ }
+ }
+
+ CLog::Log(LOGINFO, "Display resolution ADJUST : {} ({}) (weight: {:.3f})",
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(res).strMode, res, weight);
+ return res;
+}
+
+void CResolutionUtils::FindResolutionFromWhitelist(float fps, int width, int height, bool is3D, RESOLUTION &resolution)
+{
+ RESOLUTION_INFO curr = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(resolution);
+ CLog::Log(LOGINFO,
+ "[WHITELIST] Searching the whitelist for: width: {}, height: {}, fps: {:0.3f}, 3D: {}",
+ width, height, fps, is3D ? "true" : "false");
+
+ std::vector<CVariant> indexList = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(CSettings::SETTING_VIDEOSCREEN_WHITELIST);
+
+ bool noWhiteList = indexList.empty();
+
+ if (noWhiteList)
+ {
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] Using the default whitelist because the user whitelist is empty");
+ std::vector<RESOLUTION> candidates;
+ RESOLUTION_INFO info;
+ std::string resString;
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetAllowedResolutions(candidates);
+ for (const auto& c : candidates)
+ {
+ info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(c);
+ if (info.iScreenHeight >= curr.iScreenHeight && info.iScreenWidth >= curr.iScreenWidth &&
+ (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (curr.dwFlags & D3DPRESENTFLAG_MODEMASK))
+ {
+ // do not add half refreshrates (25, 29.97 by default) as kodi cannot cope with
+ // them on playback start. Especially interlaced content is not properly detected
+ // and this causes ugly double switching.
+ // This won't allow 25p / 30p playback on native refreshrate by default
+ if ((info.fRefreshRate > 30) || (MathUtils::FloatEquals(info.fRefreshRate, 24.0f, 0.1f)))
+ {
+ resString = CDisplaySettings::GetInstance().GetStringFromRes(c);
+ indexList.emplace_back(resString);
+ }
+ }
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "[WHITELIST] Searching for an exact resolution with an exact refresh rate");
+
+ unsigned int penalty = std::numeric_limits<unsigned int>::max();
+ bool found = false;
+
+ for (const auto& mode : indexList)
+ {
+ auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString());
+ const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i);
+
+ // allow resolutions that are exact and have the correct refresh rate
+ // allow macroblock alignment / padding errors (e.g. 1080 mod16 == 8)
+ if (((height == info.iScreenHeight && width <= info.iScreenWidth + 8) ||
+ (width == info.iScreenWidth && height <= info.iScreenHeight + 8)) &&
+ (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (curr.dwFlags & D3DPRESENTFLAG_MODEMASK) &&
+ MathUtils::FloatEquals(info.fRefreshRate, fps, 0.01f))
+ {
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] Matched an exact resolution with an exact refresh rate {} ({})",
+ info.strMode, i);
+ unsigned int pen = abs(info.iScreenHeight - height) + abs(info.iScreenWidth - width);
+ if (pen < penalty)
+ {
+ resolution = i;
+ found = true;
+ penalty = pen;
+ }
+ }
+ }
+
+ if (!found)
+ CLog::Log(LOGDEBUG, "[WHITELIST] No match for an exact resolution with an exact refresh rate");
+
+ if (noWhiteList || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ SETTING_VIDEOSCREEN_WHITELIST_DOUBLEREFRESHRATE))
+ {
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] Searching for an exact resolution with double the refresh rate");
+
+ for (const auto& mode : indexList)
+ {
+ auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString());
+ const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i);
+
+ // allow resolutions that are exact and have double the refresh rate
+ // allow macroblock alignment / padding errors (e.g. 1080 mod16 == 8)
+ if (((height == info.iScreenHeight && width <= info.iScreenWidth + 8) ||
+ (width == info.iScreenWidth && height <= info.iScreenHeight + 8)) &&
+ (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (curr.dwFlags & D3DPRESENTFLAG_MODEMASK) &&
+ MathUtils::FloatEquals(info.fRefreshRate, fps * 2, 0.01f))
+ {
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] Matched an exact resolution with double the refresh rate {} ({})",
+ info.strMode, i);
+ unsigned int pen = abs(info.iScreenHeight - height) + abs(info.iScreenWidth - width);
+ if (pen < penalty)
+ {
+ resolution = i;
+ found = true;
+ penalty = pen;
+ }
+ }
+ }
+ if (found)
+ return;
+
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] No match for an exact resolution with double the refresh rate");
+ }
+ else if (found)
+ return;
+
+ if (noWhiteList || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ SETTING_VIDEOSCREEN_WHITELIST_PULLDOWN))
+ {
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] Searching for an exact resolution with a 3:2 pulldown refresh rate");
+
+ for (const auto& mode : indexList)
+ {
+ auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString());
+ const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i);
+
+ // allow resolutions that are exact and have 2.5 times the refresh rate
+ // allow macroblock alignment / padding errors (e.g. 1080 mod16 == 8)
+ if (((height == info.iScreenHeight && width <= info.iScreenWidth + 8) ||
+ (width == info.iScreenWidth && height <= info.iScreenHeight + 8)) &&
+ (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (curr.dwFlags & D3DPRESENTFLAG_MODEMASK) &&
+ MathUtils::FloatEquals(info.fRefreshRate, fps * 2.5f, 0.01f))
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "[WHITELIST] Matched an exact resolution with a 3:2 pulldown refresh rate {} ({})",
+ info.strMode, i);
+ unsigned int pen = abs(info.iScreenHeight - height) + abs(info.iScreenWidth - width);
+ if (pen < penalty)
+ {
+ resolution = i;
+ found = true;
+ penalty = pen;
+ }
+ }
+ }
+ if (found)
+ return;
+
+ CLog::Log(LOGDEBUG, "[WHITELIST] No match for a resolution with a 3:2 pulldown refresh rate");
+ }
+
+
+ CLog::Log(LOGDEBUG, "[WHITELIST] Searching for a desktop resolution with an exact refresh rate");
+
+ const RESOLUTION_INFO desktop_info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(CDisplaySettings::GetInstance().GetCurrentResolution());
+
+ for (const auto& mode : indexList)
+ {
+ auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString());
+ const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i);
+
+ // allow resolutions that are desktop resolution but have the correct refresh rate
+ if (info.iScreenWidth == desktop_info.iScreenWidth &&
+ (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (desktop_info.dwFlags & D3DPRESENTFLAG_MODEMASK) &&
+ MathUtils::FloatEquals(info.fRefreshRate, fps, 0.01f))
+ {
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] Matched a desktop resolution with an exact refresh rate {} ({})",
+ info.strMode, i);
+ resolution = i;
+ return;
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "[WHITELIST] No match for a desktop resolution with an exact refresh rate");
+
+ if (noWhiteList || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ SETTING_VIDEOSCREEN_WHITELIST_DOUBLEREFRESHRATE))
+ {
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] Searching for a desktop resolution with double the refresh rate");
+
+ for (const auto& mode : indexList)
+ {
+ auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString());
+ const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i);
+
+ // allow resolutions that are desktop resolution but have double the refresh rate
+ if (info.iScreenWidth == desktop_info.iScreenWidth &&
+ (info.dwFlags & D3DPRESENTFLAG_MODEMASK) ==
+ (desktop_info.dwFlags & D3DPRESENTFLAG_MODEMASK) &&
+ MathUtils::FloatEquals(info.fRefreshRate, fps * 2, 0.01f))
+ {
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] Matched a desktop resolution with double the refresh rate {} ({})",
+ info.strMode, i);
+ resolution = i;
+ return;
+ }
+ }
+
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] No match for a desktop resolution with double the refresh rate");
+ }
+
+ if (noWhiteList || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ SETTING_VIDEOSCREEN_WHITELIST_PULLDOWN))
+ {
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] Searching for a desktop resolution with a 3:2 pulldown refresh rate");
+
+ for (const auto& mode : indexList)
+ {
+ auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString());
+ const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i);
+
+ // allow resolutions that are desktop resolution but have 2.5 times the refresh rate
+ if (info.iScreenWidth == desktop_info.iScreenWidth &&
+ (info.dwFlags & D3DPRESENTFLAG_MODEMASK) ==
+ (desktop_info.dwFlags & D3DPRESENTFLAG_MODEMASK) &&
+ MathUtils::FloatEquals(info.fRefreshRate, fps * 2.5f, 0.01f))
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "[WHITELIST] Matched a desktop resolution with a 3:2 pulldown refresh rate {} ({})",
+ info.strMode, i);
+ resolution = i;
+ return;
+ }
+ }
+
+ CLog::Log(LOGDEBUG,
+ "[WHITELIST] No match for a desktop resolution with a 3:2 pulldown refresh rate");
+ }
+
+ CLog::Log(LOGDEBUG, "[WHITELIST] No resolution matched");
+}
+
+bool CResolutionUtils::FindResolutionFromOverride(float fps, int width, bool is3D, RESOLUTION &resolution, float& weight, bool fallback)
+{
+ RESOLUTION_INFO curr = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(resolution);
+
+ //try to find a refreshrate from the override
+ for (int i = 0; i < (int)CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAdjustRefreshOverrides.size(); i++)
+ {
+ RefreshOverride& override = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoAdjustRefreshOverrides[i];
+
+ if (override.fallback != fallback)
+ continue;
+
+ //if we're checking for overrides, check if the fps matches
+ if (!fallback && (fps < override.fpsmin || fps > override.fpsmax))
+ continue;
+
+ for (size_t j = (int)RES_DESKTOP; j < CDisplaySettings::GetInstance().ResolutionInfoSize(); j++)
+ {
+ RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo((RESOLUTION)j);
+
+ if (info.iScreenWidth == curr.iScreenWidth &&
+ info.iScreenHeight == curr.iScreenHeight &&
+ (info.dwFlags & D3DPRESENTFLAG_MODEMASK) == (curr.dwFlags & D3DPRESENTFLAG_MODEMASK))
+ {
+ if (info.fRefreshRate <= override.refreshmax &&
+ info.fRefreshRate >= override.refreshmin)
+ {
+ resolution = (RESOLUTION)j;
+
+ if (fallback)
+ {
+ CLog::Log(
+ LOGDEBUG,
+ "Found Resolution {} ({}) from fallback (refreshmin:{:.3f} refreshmax:{:.3f})",
+ info.strMode, resolution, override.refreshmin, override.refreshmax);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG,
+ "Found Resolution {} ({}) from override of fps {:.3f} (fpsmin:{:.3f} "
+ "fpsmax:{:.3f} refreshmin:{:.3f} refreshmax:{:.3f})",
+ info.strMode, resolution, fps, override.fpsmin, override.fpsmax,
+ override.refreshmin, override.refreshmax);
+ }
+
+ weight = RefreshWeight(info.fRefreshRate, fps);
+
+ return true; //fps and refresh match with this override, use this resolution
+ }
+ }
+ }
+ }
+
+ return false; //no override found
+}
+
+//distance of refresh to the closest multiple of fps (multiple is 1 or higher), as a multiplier of fps
+float CResolutionUtils::RefreshWeight(float refresh, float fps)
+{
+ float div = refresh / fps;
+ int round = MathUtils::round_int(static_cast<double>(div));
+
+ float weight = 0.0f;
+
+ if (round < 1)
+ weight = (fps - refresh) / fps;
+ else
+ weight = fabs(div / round - 1.0f);
+
+ // punish higher refreshrates and prefer better matching
+ // e.g. 30 fps content at 60 hz is better than
+ // 30 fps at 120 hz - as we sometimes don't know if
+ // the content is interlaced at the start, only
+ // punish when refreshrate > 60 hz to not have to switch
+ // twice for 30i content
+ if (refresh > 60 && round > 1)
+ weight += round / 10000.0f;
+
+ return weight;
+}
+
+bool CResolutionUtils::HasWhitelist()
+{
+ std::vector<CVariant> indexList = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(CSettings::SETTING_VIDEOSCREEN_WHITELIST);
+ return !indexList.empty();
+}
+
+void CResolutionUtils::PrintWhitelist()
+{
+ std::string modeStr;
+ auto indexList = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_VIDEOSCREEN_WHITELIST);
+ if (!indexList.empty())
+ {
+ for (const auto& mode : indexList)
+ {
+ auto i = CDisplaySettings::GetInstance().GetResFromString(mode.asString());
+ const RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(i);
+ modeStr.append("\n" + info.strMode);
+ }
+
+ CLog::Log(LOGDEBUG, "[WHITELIST] whitelisted modes:{}", modeStr);
+ }
+}
+
+void CResolutionUtils::GetMaxAllowedResolution(unsigned int& width, unsigned int& height)
+{
+ if (!CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot())
+ return;
+
+ std::vector<RESOLUTION_INFO> resList;
+
+ auto indexList = CServiceBroker::GetSettingsComponent()->GetSettings()->GetList(
+ CSettings::SETTING_VIDEOSCREEN_WHITELIST);
+
+ unsigned int maxWidth{0};
+ unsigned int maxHeight{0};
+
+ if (!indexList.empty())
+ {
+ for (const auto& mode : indexList)
+ {
+ RESOLUTION res = CDisplaySettings::GetInstance().GetResFromString(mode.asString());
+ RESOLUTION_INFO resInfo{CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(res)};
+ if (static_cast<unsigned int>(resInfo.iScreenWidth) > maxWidth &&
+ static_cast<unsigned int>(resInfo.iScreenHeight) > maxHeight)
+ {
+ maxWidth = static_cast<unsigned int>(resInfo.iScreenWidth);
+ maxHeight = static_cast<unsigned int>(resInfo.iScreenHeight);
+ }
+ }
+ }
+ else
+ {
+ std::vector<RESOLUTION> resList;
+ CServiceBroker::GetWinSystem()->GetGfxContext().GetAllowedResolutions(resList);
+
+ for (const auto& res : resList)
+ {
+ RESOLUTION_INFO resInfo{CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(res)};
+ if (static_cast<unsigned int>(resInfo.iScreenWidth) > maxWidth &&
+ static_cast<unsigned int>(resInfo.iScreenHeight) > maxHeight)
+ {
+ maxWidth = static_cast<unsigned int>(resInfo.iScreenWidth);
+ maxHeight = static_cast<unsigned int>(resInfo.iScreenHeight);
+ }
+ }
+ }
+
+ width = maxWidth;
+ height = maxHeight;
+}
diff --git a/xbmc/windowing/Resolution.h b/xbmc/windowing/Resolution.h
new file mode 100644
index 0000000..768d459
--- /dev/null
+++ b/xbmc/windowing/Resolution.h
@@ -0,0 +1,107 @@
+/*
+ * 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 <stdint.h>
+#include <string>
+
+typedef int DisplayMode;
+#define DM_WINDOWED -1
+#define DM_FULLSCREEN 0
+
+enum RESOLUTION
+{
+ RES_INVALID = -1,
+ RES_WINDOW = 15,
+ RES_DESKTOP = 16, // Desktop resolution
+ RES_CUSTOM = 16 + 1, // First additional resolution
+};
+
+struct OVERSCAN
+{
+ int left;
+ int top;
+ int right;
+ int bottom;
+public:
+ OVERSCAN()
+ {
+ left = top = right = bottom = 0;
+ }
+ OVERSCAN(const OVERSCAN& os)
+ {
+ left = os.left; top = os.top;
+ right = os.right; bottom = os.bottom;
+ }
+ OVERSCAN& operator=(const OVERSCAN&) = default;
+
+ bool operator==(const OVERSCAN& other)
+ {
+ return left == other.left && right == other.right && top == other.top && bottom == other.bottom;
+ }
+ bool operator!=(const OVERSCAN& other)
+ {
+ return left != other.left || right != other.right || top != other.top || bottom != other.bottom;
+ }
+};
+
+struct EdgeInsets
+{
+ float left = 0.0f;
+ float top = 0.0f;
+ float right = 0.0f;
+ float bottom = 0.0f;
+
+ EdgeInsets() = default;
+ EdgeInsets(float l, float t, float r, float b);
+};
+
+struct RESOLUTION_INFO
+{
+ OVERSCAN Overscan;
+ EdgeInsets guiInsets;
+ bool bFullScreen;
+ int iWidth;
+ int iHeight;
+ int iBlanking; /**< number of pixels of padding between stereoscopic frames */
+ int iScreenWidth;
+ int iScreenHeight;
+ int iSubtitles;
+ uint32_t dwFlags;
+ float fPixelRatio;
+ float fRefreshRate;
+ std::string strMode;
+ std::string strOutput;
+ std::string strId;
+public:
+ RESOLUTION_INFO(int width = 1280, int height = 720, float aspect = 0, const std::string &mode = "");
+ float DisplayRatio() const;
+ RESOLUTION_INFO(const RESOLUTION_INFO& res);
+ RESOLUTION_INFO& operator=(const RESOLUTION_INFO&) = default;
+};
+
+class CResolutionUtils
+{
+public:
+ static RESOLUTION ChooseBestResolution(float fps, int width, int height, bool is3D);
+ static bool HasWhitelist();
+ static void PrintWhitelist();
+
+ /*!
+ * \brief Get the max allowed resolution, if fullscreen
+ * \param width [OUT] Max width resolution
+ * \param height [OUT] Max height resolution
+ */
+ static void GetMaxAllowedResolution(unsigned int& width, unsigned int& height);
+
+protected:
+ static void FindResolutionFromWhitelist(float fps, int width, int height, bool is3D, RESOLUTION &resolution);
+ static bool FindResolutionFromOverride(float fps, int width, bool is3D, RESOLUTION &resolution, float& weight, bool fallback);
+ static float RefreshWeight(float refresh, float fps);
+};
diff --git a/xbmc/windowing/VideoSync.h b/xbmc/windowing/VideoSync.h
new file mode 100644
index 0000000..7e79339
--- /dev/null
+++ b/xbmc/windowing/VideoSync.h
@@ -0,0 +1,31 @@
+/*
+ * 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 "threads/Event.h"
+
+class CVideoReferenceClock;
+typedef void (*PUPDATECLOCK)(int NrVBlanks, uint64_t time, void *clock);
+
+class CVideoSync
+{
+public:
+ explicit CVideoSync(void* clock) { m_refClock = clock; }
+ virtual ~CVideoSync() = default;
+ virtual bool Setup(PUPDATECLOCK func) = 0;
+ virtual void Run(CEvent& stop) = 0;
+ virtual void Cleanup() = 0;
+ virtual float GetFps() = 0;
+ virtual void RefreshChanged() {}
+
+protected:
+ PUPDATECLOCK UpdateClock;
+ float m_fps;
+ void *m_refClock;
+};
diff --git a/xbmc/windowing/WinEvents.h b/xbmc/windowing/WinEvents.h
new file mode 100644
index 0000000..8f58c6c
--- /dev/null
+++ b/xbmc/windowing/WinEvents.h
@@ -0,0 +1,19 @@
+/*
+ * 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 "XBMC_events.h"
+
+class IWinEvents
+{
+public:
+ virtual ~IWinEvents() = default;
+ virtual bool MessagePump() = 0;
+};
+
diff --git a/xbmc/windowing/WinSystem.cpp b/xbmc/windowing/WinSystem.cpp
new file mode 100644
index 0000000..cef6940
--- /dev/null
+++ b/xbmc/windowing/WinSystem.cpp
@@ -0,0 +1,278 @@
+/*
+ * 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 "WinSystem.h"
+
+#include "ServiceBroker.h"
+#include "guilib/DispResource.h"
+#include "powermanagement/DPMSSupport.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "windowing/GraphicContext.h"
+
+#include <mutex>
+#if HAS_GLES
+#include "guilib/GUIFontTTFGL.h"
+#endif
+
+const char* CWinSystemBase::SETTING_WINSYSTEM_IS_HDR_DISPLAY = "winsystem.ishdrdisplay";
+
+CWinSystemBase::CWinSystemBase()
+{
+ m_gfxContext.reset(new CGraphicContext());
+}
+
+CWinSystemBase::~CWinSystemBase() = default;
+
+bool CWinSystemBase::InitWindowSystem()
+{
+ UpdateResolutions();
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+
+ CResolutionUtils::PrintWhitelist();
+
+ return true;
+}
+
+bool CWinSystemBase::DestroyWindowSystem()
+{
+ m_screenSaverManager.reset();
+ return false;
+}
+
+void CWinSystemBase::UpdateDesktopResolution(RESOLUTION_INFO& newRes, const std::string &output, int width, int height, float refreshRate, uint32_t dwFlags)
+{
+ newRes.Overscan.left = 0;
+ newRes.Overscan.top = 0;
+ newRes.Overscan.right = width;
+ newRes.Overscan.bottom = height;
+ newRes.bFullScreen = true;
+ newRes.iSubtitles = height;
+ newRes.dwFlags = dwFlags;
+ newRes.fRefreshRate = refreshRate;
+ newRes.fPixelRatio = 1.0f;
+ newRes.iWidth = width;
+ newRes.iHeight = height;
+ newRes.iScreenWidth = width;
+ newRes.iScreenHeight = height;
+ newRes.strMode = StringUtils::Format("{}: {}x{}", output, width, height);
+ if (refreshRate > 1)
+ newRes.strMode += StringUtils::Format(" @ {:.2f}Hz", refreshRate);
+ if (dwFlags & D3DPRESENTFLAG_INTERLACED)
+ newRes.strMode += "i";
+ if (dwFlags & D3DPRESENTFLAG_MODE3DTB)
+ newRes.strMode += "tab";
+ if (dwFlags & D3DPRESENTFLAG_MODE3DSBS)
+ newRes.strMode += "sbs";
+ newRes.strOutput = output;
+}
+
+void CWinSystemBase::UpdateResolutions()
+{
+ // add the window res - defaults are fine.
+ RESOLUTION_INFO& window = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW);
+ window.bFullScreen = false;
+ if (window.iWidth == 0)
+ window.iWidth = 720;
+ if (window.iHeight == 0)
+ window.iHeight = 480;
+ window.iScreenWidth = window.iWidth;
+ window.iScreenHeight = window.iHeight;
+ if (window.iSubtitles == 0)
+ window.iSubtitles = window.iHeight;
+ window.fPixelRatio = 1.0f;
+ window.strMode = "Windowed";
+}
+
+void CWinSystemBase::SetWindowResolution(int width, int height)
+{
+ RESOLUTION_INFO& window = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW);
+ window.iWidth = width;
+ window.iHeight = height;
+ window.iScreenWidth = width;
+ window.iScreenHeight = height;
+ window.iSubtitles = window.iHeight;
+ CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(window);
+}
+
+static void AddResolution(std::vector<RESOLUTION_WHR> &resolutions, unsigned int addindex, float bestRefreshrate)
+{
+ RESOLUTION_INFO resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(addindex);
+ int width = resInfo.iScreenWidth;
+ int height = resInfo.iScreenHeight;
+ int flags = resInfo.dwFlags & D3DPRESENTFLAG_MODEMASK;
+ float refreshrate = resInfo.fRefreshRate;
+
+ // don't touch RES_DESKTOP
+ for (unsigned int idx = 1; idx < resolutions.size(); idx++)
+ if ( resolutions[idx].width == width
+ && resolutions[idx].height == height
+ &&(resolutions[idx].flags & D3DPRESENTFLAG_MODEMASK) == flags)
+ {
+ // check if the refresh rate of this resolution is better suited than
+ // the refresh rate of the resolution with the same width/height/interlaced
+ // property and if so replace it
+ if (bestRefreshrate > 0.0f && refreshrate == bestRefreshrate)
+ resolutions[idx].ResInfo_Index = addindex;
+
+ // no need to add the resolution again
+ return;
+ }
+
+ RESOLUTION_WHR res = {width, height, flags, (int)addindex};
+ resolutions.push_back(res);
+}
+
+static bool resSortPredicate(RESOLUTION_WHR i, RESOLUTION_WHR j)
+{
+ // note: this comparison must obey "strict weak ordering"
+ // a "!=" on the flags comparison resulted in memory corruption
+ return ( i.width < j.width
+ || (i.width == j.width && i.height < j.height)
+ || (i.width == j.width && i.height == j.height && i.flags < j.flags) );
+}
+
+std::vector<RESOLUTION_WHR> CWinSystemBase::ScreenResolutions(float refreshrate)
+{
+ std::vector<RESOLUTION_WHR> resolutions;
+
+ for (unsigned int idx = RES_CUSTOM; idx < CDisplaySettings::GetInstance().ResolutionInfoSize(); idx++)
+ {
+ RESOLUTION_INFO info = CDisplaySettings::GetInstance().GetResolutionInfo(idx);
+ AddResolution(resolutions, idx, refreshrate);
+ }
+
+ // Can't assume a sort order
+ sort(resolutions.begin(), resolutions.end(), resSortPredicate);
+
+ return resolutions;
+}
+
+static void AddRefreshRate(std::vector<REFRESHRATE> &refreshrates, unsigned int addindex)
+{
+ float RefreshRate = CDisplaySettings::GetInstance().GetResolutionInfo(addindex).fRefreshRate;
+
+ for (unsigned int idx = 0; idx < refreshrates.size(); idx++)
+ if ( refreshrates[idx].RefreshRate == RefreshRate)
+ return; // already taken care of.
+
+ REFRESHRATE rr = {RefreshRate, (int)addindex};
+ refreshrates.push_back(rr);
+}
+
+static bool rrSortPredicate(REFRESHRATE i, REFRESHRATE j)
+{
+ return (i.RefreshRate < j.RefreshRate);
+}
+
+std::vector<REFRESHRATE> CWinSystemBase::RefreshRates(int width, int height, uint32_t dwFlags)
+{
+ std::vector<REFRESHRATE> refreshrates;
+
+ for (unsigned int idx = RES_DESKTOP; idx < CDisplaySettings::GetInstance().ResolutionInfoSize(); idx++)
+ {
+ if (CDisplaySettings::GetInstance().GetResolutionInfo(idx).iScreenWidth == width &&
+ CDisplaySettings::GetInstance().GetResolutionInfo(idx).iScreenHeight == height &&
+ (CDisplaySettings::GetInstance().GetResolutionInfo(idx).dwFlags & D3DPRESENTFLAG_MODEMASK) == (dwFlags & D3DPRESENTFLAG_MODEMASK))
+ AddRefreshRate(refreshrates, idx);
+ }
+
+ // Can't assume a sort order
+ sort(refreshrates.begin(), refreshrates.end(), rrSortPredicate);
+
+ return refreshrates;
+}
+
+REFRESHRATE CWinSystemBase::DefaultRefreshRate(std::vector<REFRESHRATE> rates)
+{
+ REFRESHRATE bestmatch = rates[0];
+ float bestfitness = -1.0f;
+ float targetfps = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).fRefreshRate;
+
+ for (unsigned i = 0; i < rates.size(); i++)
+ {
+ float fitness = fabs(targetfps - rates[i].RefreshRate);
+
+ if (bestfitness <0 || fitness < bestfitness)
+ {
+ bestfitness = fitness;
+ bestmatch = rates[i];
+ if (bestfitness == 0.0f) // perfect match
+ break;
+ }
+ }
+ return bestmatch;
+}
+
+bool CWinSystemBase::UseLimitedColor()
+{
+ return false;
+}
+
+std::string CWinSystemBase::GetClipboardText(void)
+{
+ return "";
+}
+
+int CWinSystemBase::NoOfBuffers(void)
+{
+ int buffers = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_VIDEOSCREEN_NOOFBUFFERS);
+ return buffers;
+}
+
+KODI::WINDOWING::COSScreenSaverManager* CWinSystemBase::GetOSScreenSaver()
+{
+ if (!m_screenSaverManager)
+ {
+ auto impl = GetOSScreenSaverImpl();
+ if (impl)
+ {
+ m_screenSaverManager.reset(new KODI::WINDOWING::COSScreenSaverManager(std::move(impl)));
+ }
+ }
+
+ return m_screenSaverManager.get();
+}
+
+void CWinSystemBase::RegisterRenderLoop(IRenderLoop *client)
+{
+ std::unique_lock<CCriticalSection> lock(m_renderLoopSection);
+ m_renderLoopClients.push_back(client);
+}
+
+void CWinSystemBase::UnregisterRenderLoop(IRenderLoop *client)
+{
+ std::unique_lock<CCriticalSection> lock(m_renderLoopSection);
+ auto i = find(m_renderLoopClients.begin(), m_renderLoopClients.end(), client);
+ if (i != m_renderLoopClients.end())
+ m_renderLoopClients.erase(i);
+}
+
+void CWinSystemBase::DriveRenderLoop()
+{
+ MessagePump();
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_renderLoopSection);
+ for (auto i = m_renderLoopClients.begin(); i != m_renderLoopClients.end(); ++i)
+ (*i)->FrameMove();
+ }
+}
+
+CGraphicContext& CWinSystemBase::GetGfxContext()
+{
+ return *m_gfxContext;
+}
+
+std::shared_ptr<CDPMSSupport> CWinSystemBase::GetDPMSManager()
+{
+ return m_dpms;
+}
diff --git a/xbmc/windowing/WinSystem.h b/xbmc/windowing/WinSystem.h
new file mode 100644
index 0000000..cac94b0
--- /dev/null
+++ b/xbmc/windowing/WinSystem.h
@@ -0,0 +1,203 @@
+/*
+ * 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 "HDRStatus.h"
+#include "OSScreenSaver.h"
+#include "Resolution.h"
+#include "VideoSync.h"
+#include "WinEvents.h"
+#include "cores/VideoPlayer/VideoRenderers/DebugInfo.h"
+#include "guilib/DispResource.h"
+#include "utils/HDRCapabilities.h"
+
+#include <memory>
+#include <vector>
+
+struct RESOLUTION_WHR
+{
+ int width;
+ int height;
+ int flags; //< only D3DPRESENTFLAG_MODEMASK flags
+ int ResInfo_Index;
+};
+
+struct REFRESHRATE
+{
+ float RefreshRate;
+ int ResInfo_Index;
+};
+
+class CDPMSSupport;
+class CGraphicContext;
+class CRenderSystemBase;
+class IRenderLoop;
+
+struct VideoPicture;
+
+class CWinSystemBase
+{
+public:
+ CWinSystemBase();
+ virtual ~CWinSystemBase();
+
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Access render system interface
+ virtual CRenderSystemBase *GetRenderSystem() { return nullptr; }
+
+ virtual const std::string GetName() { return "platform default"; }
+
+ // windowing interfaces
+ virtual bool InitWindowSystem();
+ virtual bool DestroyWindowSystem();
+ virtual bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) = 0;
+ virtual bool DestroyWindow(){ return false; }
+ virtual bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) = 0;
+ virtual bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) = 0;
+ virtual bool DisplayHardwareScalingEnabled() { return false; }
+ virtual void UpdateDisplayHardwareScaling(const RESOLUTION_INFO& resInfo) { }
+ virtual bool MoveWindow(int topLeft, int topRight){return false;}
+ virtual void FinishModeChange(RESOLUTION res){}
+ virtual void FinishWindowResize(int newWidth, int newHeight) {ResizeWindow(newWidth, newHeight, -1, -1);}
+ virtual bool CenterWindow(){return false;}
+ virtual bool IsCreated(){ return m_bWindowCreated; }
+ virtual void NotifyAppFocusChange(bool bGaining) {}
+ virtual void NotifyAppActiveChange(bool bActivated) {}
+ virtual void ShowOSMouse(bool show) {}
+ virtual bool HasCursor(){ return true; }
+ //some platforms have api for gesture inertial scrolling - default to false and use the InertialScrollingHandler
+ virtual bool HasInertialGestures(){ return false; }
+ //does the output expect limited color range (ie 16-235)
+ virtual bool UseLimitedColor();
+ //the number of presentation buffers
+ virtual int NoOfBuffers();
+ /**
+ * Get average display latency
+ *
+ * The latency should be measured as the time between finishing the rendering
+ * of a frame, i.e. calling PresentRender, and the rendered content becoming
+ * visible on the screen.
+ *
+ * \return average display latency in seconds, or negative value if unknown
+ */
+ virtual float GetDisplayLatency() { return -1.0f; }
+ /**
+ * Get time that should be subtracted from the display latency for this frame
+ * in milliseconds
+ *
+ * Contrary to \ref GetDisplayLatency, this value is calculated ad-hoc
+ * for the frame currently being rendered and not a value that is calculated/
+ * averaged from past frames and their presentation times
+ */
+ virtual float GetFrameLatencyAdjustment() { return 0.0; }
+
+ virtual bool Minimize() { return false; }
+ virtual bool Restore() { return false; }
+ virtual bool Hide() { return false; }
+ virtual bool Show(bool raise = true) { return false; }
+
+ // videosync
+ virtual std::unique_ptr<CVideoSync> GetVideoSync(void *clock) { return nullptr; }
+
+ // notifications
+ virtual void OnMove(int x, int y) {}
+
+ // OS System screensaver
+ /**
+ * Get OS screen saver inhibit implementation if available
+ *
+ * \return OS screen saver implementation that can be used with this windowing system
+ * or nullptr if unsupported.
+ * Lifetime of the returned object will usually end with \ref DestroyWindowSystem, so
+ * do not use any more after calling that.
+ */
+ KODI::WINDOWING::COSScreenSaverManager* GetOSScreenSaver();
+
+ // resolution interfaces
+ unsigned int GetWidth() { return m_nWidth; }
+ unsigned int GetHeight() { return m_nHeight; }
+ virtual bool CanDoWindowed() { return true; }
+ bool IsFullScreen() { return m_bFullScreen; }
+ virtual void UpdateResolutions();
+ void SetWindowResolution(int width, int height);
+ std::vector<RESOLUTION_WHR> ScreenResolutions(float refreshrate);
+ std::vector<REFRESHRATE> RefreshRates(int width, int height, uint32_t dwFlags);
+ REFRESHRATE DefaultRefreshRate(std::vector<REFRESHRATE> rates);
+ virtual bool HasCalibration(const RESOLUTION_INFO& resInfo) { return true; }
+
+ // text input interface
+ virtual std::string GetClipboardText(void);
+
+ // Display event callback
+ virtual void Register(IDispResource *resource) = 0;
+ virtual void Unregister(IDispResource *resource) = 0;
+
+ // render loop
+ void RegisterRenderLoop(IRenderLoop *client);
+ void UnregisterRenderLoop(IRenderLoop *client);
+ void DriveRenderLoop();
+
+ // winsystem events
+ virtual bool MessagePump() { return false; }
+
+ // Access render system interface
+ CGraphicContext& GetGfxContext();
+
+ /**
+ * Get OS specific hardware context
+ *
+ * \return OS specific context or nullptr if OS not have
+ *
+ * \note This function is currently only related to Windows with DirectX,
+ * all other OS where use GL returns nullptr.
+ * Returned Windows class pointer is ID3D11DeviceContext1.
+ */
+ virtual void* GetHWContext() { return nullptr; }
+
+ std::shared_ptr<CDPMSSupport> GetDPMSManager();
+
+ /**
+ * @brief Set the HDR metadata. Passing nullptr as the parameter should
+ * disable HDR.
+ *
+ */
+ virtual bool SetHDR(const VideoPicture* videoPicture) { return false; }
+ virtual bool IsHDRDisplay() { return false; }
+ virtual HDR_STATUS ToggleHDR() { return HDR_STATUS::HDR_UNSUPPORTED; }
+ virtual HDR_STATUS GetOSHDRStatus() { return HDR_STATUS::HDR_UNSUPPORTED; }
+ virtual CHDRCapabilities GetDisplayHDRCapabilities() const { return {}; }
+
+ static const char* SETTING_WINSYSTEM_IS_HDR_DISPLAY;
+
+ // Gets debug info from video renderer
+ virtual DEBUG_INFO_RENDER GetDebugInfo() { return {}; }
+
+ virtual std::vector<std::string> GetConnectedOutputs() { return {}; }
+
+protected:
+ void UpdateDesktopResolution(RESOLUTION_INFO& newRes, const std::string &output, int width, int height, float refreshRate, uint32_t dwFlags);
+ virtual std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() { return nullptr; }
+
+ int m_nWidth = 0;
+ int m_nHeight = 0;
+ int m_nTop = 0;
+ int m_nLeft = 0;
+ bool m_bWindowCreated = false;
+ bool m_bFullScreen = false;
+ bool m_bBlankOtherDisplay = false;
+ float m_fRefreshRate = 0.0f;
+ std::unique_ptr<KODI::WINDOWING::COSScreenSaverManager> m_screenSaverManager;
+ CCriticalSection m_renderLoopSection;
+ std::vector<IRenderLoop*> m_renderLoopClients;
+
+ std::unique_ptr<IWinEvents> m_winEvents;
+ std::unique_ptr<CGraphicContext> m_gfxContext;
+ std::shared_ptr<CDPMSSupport> m_dpms;
+};
diff --git a/xbmc/windowing/WindowSystemFactory.cpp b/xbmc/windowing/WindowSystemFactory.cpp
new file mode 100644
index 0000000..edb6098
--- /dev/null
+++ b/xbmc/windowing/WindowSystemFactory.cpp
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2005-2020 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 "WindowSystemFactory.h"
+
+#include <algorithm>
+
+using namespace KODI::WINDOWING;
+
+std::list<std::pair<std::string, std::function<std::unique_ptr<CWinSystemBase>()>>>
+ CWindowSystemFactory::m_windowSystems;
+
+std::list<std::string> CWindowSystemFactory::GetWindowSystems()
+{
+ std::list<std::string> available;
+ for (const auto& windowSystem : m_windowSystems)
+ available.emplace_back(windowSystem.first);
+
+ return available;
+}
+
+std::unique_ptr<CWinSystemBase> CWindowSystemFactory::CreateWindowSystem(const std::string& name)
+{
+ auto windowSystem =
+ std::find_if(m_windowSystems.begin(), m_windowSystems.end(),
+ [&name](auto& windowSystem) { return windowSystem.first == name; });
+ if (windowSystem != m_windowSystems.end())
+ return windowSystem->second();
+
+ return nullptr;
+}
+
+void CWindowSystemFactory::RegisterWindowSystem(
+ const std::function<std::unique_ptr<CWinSystemBase>()>& createFunction, const std::string& name)
+{
+ m_windowSystems.emplace_back(std::make_pair(name, createFunction));
+}
diff --git a/xbmc/windowing/WindowSystemFactory.h b/xbmc/windowing/WindowSystemFactory.h
new file mode 100644
index 0000000..20e051d
--- /dev/null
+++ b/xbmc/windowing/WindowSystemFactory.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2005-2020 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 "WinSystem.h"
+
+#include <functional>
+#include <list>
+#include <map>
+#include <memory>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+
+class CWindowSystemFactory
+{
+public:
+ static std::unique_ptr<CWinSystemBase> CreateWindowSystem(const std::string& name);
+ static std::list<std::string> GetWindowSystems();
+ static void RegisterWindowSystem(
+ const std::function<std::unique_ptr<CWinSystemBase>()>& createFunction,
+ const std::string& name = "default");
+
+private:
+ static std::list<std::pair<std::string, std::function<std::unique_ptr<CWinSystemBase>()>>>
+ m_windowSystems;
+};
+
+} // namespace WINDOWING
+} // namespace KODI
diff --git a/xbmc/windowing/X11/CMakeLists.txt b/xbmc/windowing/X11/CMakeLists.txt
new file mode 100644
index 0000000..91e13d6
--- /dev/null
+++ b/xbmc/windowing/X11/CMakeLists.txt
@@ -0,0 +1,37 @@
+set(SOURCES GLContextEGL.cpp
+ GLContext.cpp
+ OptionalsReg.cpp
+ OSScreenSaverX11.cpp
+ WinEventsX11.cpp
+ WinSystemX11.cpp
+ XRandR.cpp
+ X11DPMSSupport.cpp)
+
+set(HEADERS GLContext.h
+ GLContextEGL.h
+ OptionalsReg.h
+ OSScreenSaverX11.h
+ WinEventsX11.h
+ WinSystemX11.h
+ XRandR.h
+ X11DPMSSupport.h)
+
+if(GLX_FOUND)
+ list(APPEND SOURCES GLContextGLX.cpp
+ VideoSyncGLX.cpp)
+ list(APPEND HEADERS GLContextGLX.h
+ VideoSyncGLX.h)
+endif()
+
+if(OPENGL_FOUND)
+ list(APPEND SOURCES WinSystemX11GLContext.cpp)
+ list(APPEND HEADERS WinSystemX11GLContext.h)
+ list(APPEND SOURCES VideoSyncOML.cpp)
+ list(APPEND HEADERS VideoSyncOML.h)
+endif()
+if(OPENGLES_FOUND)
+ list(APPEND SOURCES WinSystemX11GLESContext.cpp)
+ list(APPEND HEADERS WinSystemX11GLESContext.h)
+endif()
+
+core_add_library(windowing_X11)
diff --git a/xbmc/windowing/X11/GLContext.cpp b/xbmc/windowing/X11/GLContext.cpp
new file mode 100644
index 0000000..3dac508
--- /dev/null
+++ b/xbmc/windowing/X11/GLContext.cpp
@@ -0,0 +1,20 @@
+/*
+ * 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 "GLContext.h"
+
+bool CGLContext::IsExtSupported(const char* extension) const
+{
+ std::string name;
+
+ name = " ";
+ name += extension;
+ name += " ";
+
+ return m_extensions.find(name) != std::string::npos;
+}
diff --git a/xbmc/windowing/X11/GLContext.h b/xbmc/windowing/X11/GLContext.h
new file mode 100644
index 0000000..95e22ec
--- /dev/null
+++ b/xbmc/windowing/X11/GLContext.h
@@ -0,0 +1,42 @@
+/*
+ * 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 <cstdint>
+#include <string>
+
+#include <X11/Xlib.h>
+
+class CGLContext
+{
+public:
+ explicit CGLContext(Display *dpy)
+ {
+ m_dpy = dpy;
+ }
+ virtual ~CGLContext() = default;
+ virtual bool Refresh(bool force, int screen, Window glWindow, bool &newContext) = 0;
+ virtual bool CreatePB() { return false; }
+ virtual void Destroy() = 0;
+ virtual void Detach() = 0;
+ virtual void SetVSync(bool enable) = 0;
+ virtual void SwapBuffers() = 0;
+ virtual void QueryExtensions() = 0;
+ virtual uint64_t GetVblankTiming(uint64_t& msc, uint64_t& interval) { return 0; }
+ bool IsExtSupported(const char* extension) const;
+
+ std::string ExtPrefix() { return m_extPrefix; }
+ std::string m_extPrefix;
+ std::string m_extensions;
+
+ Display *m_dpy;
+
+protected:
+ bool m_omlSync = true;
+};
diff --git a/xbmc/windowing/X11/GLContextEGL.cpp b/xbmc/windowing/X11/GLContextEGL.cpp
new file mode 100644
index 0000000..66de374
--- /dev/null
+++ b/xbmc/windowing/X11/GLContextEGL.cpp
@@ -0,0 +1,542 @@
+/*
+ * 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.
+ */
+
+// always define GL_GLEXT_PROTOTYPES before include gl headers
+#if !defined(GL_GLEXT_PROTOTYPES)
+ #define GL_GLEXT_PROTOTYPES
+#endif
+
+#include "GLContextEGL.h"
+
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+
+#include <clocale>
+#include <mutex>
+
+#include <EGL/eglext.h>
+#include <unistd.h>
+
+#include "PlatformDefs.h"
+#include "system_gl.h"
+
+#define EGL_NO_CONFIG (EGLConfig)0
+
+CGLContextEGL::CGLContextEGL(Display* dpy, EGLint renderingApi)
+ : CGLContext(dpy), m_renderingApi(renderingApi)
+{
+ m_extPrefix = "EGL_";
+ m_eglConfig = EGL_NO_CONFIG;
+
+ m_eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT");
+
+ const auto settings = CServiceBroker::GetSettingsComponent();
+ if (settings)
+ {
+ m_omlSync = settings->GetAdvancedSettings()->m_omlSync;
+ }
+}
+
+CGLContextEGL::~CGLContextEGL()
+{
+ Destroy();
+}
+
+bool CGLContextEGL::Refresh(bool force, int screen, Window glWindow, bool &newContext)
+{
+ m_sync.cont = 0;
+
+ // refresh context
+ if (m_eglContext && !force)
+ {
+ if (m_eglSurface == EGL_NO_SURFACE)
+ {
+ m_eglSurface = eglCreateWindowSurface(m_eglDisplay, m_eglConfig, glWindow, NULL);
+ if (m_eglSurface == EGL_NO_SURFACE)
+ {
+ CLog::Log(LOGERROR, "failed to create EGL window surface {}", eglGetError());
+ return false;
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "CWinSystemX11::RefreshEGLContext: refreshing context");
+ eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+ eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext);
+ return true;
+ }
+
+ Destroy();
+ newContext = true;
+
+ if (m_eglGetPlatformDisplayEXT)
+ {
+ EGLint attribs[] =
+ {
+ EGL_PLATFORM_X11_SCREEN_EXT, screen,
+ EGL_NONE
+ };
+ m_eglDisplay = m_eglGetPlatformDisplayEXT(EGL_PLATFORM_X11_EXT,(EGLNativeDisplayType)m_dpy,
+ attribs);
+ }
+ else
+ m_eglDisplay = eglGetDisplay((EGLNativeDisplayType)m_dpy);
+
+ if (m_eglDisplay == EGL_NO_DISPLAY)
+ {
+ CLog::Log(LOGERROR, "failed to get egl display");
+ return false;
+ }
+ if (!eglInitialize(m_eglDisplay, NULL, NULL))
+ {
+ CLog::Log(LOGERROR, "failed to initialize egl");
+ Destroy();
+ return false;
+ }
+ if (!eglBindAPI(m_renderingApi))
+ {
+ CLog::Log(LOGERROR, "failed to bind rendering API");
+ Destroy();
+ return false;
+ }
+
+ // create context
+
+ XVisualInfo vMask;
+ XVisualInfo *vInfo = nullptr;
+ int availableVisuals = 0;
+ vMask.screen = screen;
+ XWindowAttributes winAttr;
+
+ if (!XGetWindowAttributes(m_dpy, glWindow, &winAttr))
+ {
+ CLog::Log(LOGWARNING, "Failed to get window attributes");
+ Destroy();
+ return false;
+ }
+
+ vMask.visualid = XVisualIDFromVisual(winAttr.visual);
+ vInfo = XGetVisualInfo(m_dpy, VisualScreenMask | VisualIDMask, &vMask, &availableVisuals);
+ if (!vInfo)
+ {
+ CLog::Log(LOGERROR, "Failed to get VisualInfo of visual 0x{:x}", (unsigned)vMask.visualid);
+ Destroy();
+ return false;
+ }
+
+ unsigned int visualid = static_cast<unsigned int>(vInfo->visualid);
+ m_eglConfig = GetEGLConfig(m_eglDisplay, vInfo);
+ XFree(vInfo);
+
+ if (m_eglConfig == EGL_NO_CONFIG)
+ {
+ CLog::Log(LOGERROR, "failed to get suitable eglconfig for visual 0x{:x}", visualid);
+ Destroy();
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "Using visual 0x{:x}", visualid);
+
+ m_eglSurface = eglCreateWindowSurface(m_eglDisplay, m_eglConfig, glWindow, NULL);
+ if (m_eglSurface == EGL_NO_SURFACE)
+ {
+ CLog::Log(LOGERROR, "failed to create EGL window surface {}", eglGetError());
+ Destroy();
+ return false;
+ }
+
+ EGLint contextAttributes[] =
+ {
+ EGL_CONTEXT_MAJOR_VERSION_KHR, 3,
+ EGL_CONTEXT_MINOR_VERSION_KHR, 2,
+ EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR,
+ EGL_NONE
+ };
+ m_eglContext = eglCreateContext(m_eglDisplay, m_eglConfig, EGL_NO_CONTEXT, contextAttributes);
+ if (m_eglContext == EGL_NO_CONTEXT)
+ {
+ EGLint contextAttributes[] =
+ {
+ EGL_CONTEXT_MAJOR_VERSION_KHR, 2,
+ EGL_NONE
+ };
+ m_eglContext = eglCreateContext(m_eglDisplay, m_eglConfig, EGL_NO_CONTEXT, contextAttributes);
+
+ if (m_eglContext == EGL_NO_CONTEXT)
+ {
+ CLog::Log(LOGERROR, "failed to create EGL context");
+ Destroy();
+ return false;
+ }
+
+ CLog::Log(LOGWARNING, "Failed to get an OpenGL context supporting core profile 3.2, "
+ "using legacy mode with reduced feature set");
+ }
+
+ if (!eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext))
+ {
+ CLog::Log(LOGERROR, "Failed to make context current {} {} {}", fmt::ptr(m_eglDisplay),
+ fmt::ptr(m_eglSurface), fmt::ptr(m_eglContext));
+ Destroy();
+ return false;
+ }
+
+ m_eglGetSyncValuesCHROMIUM = (PFNEGLGETSYNCVALUESCHROMIUMPROC)eglGetProcAddress("eglGetSyncValuesCHROMIUM");
+
+ m_usePB = false;
+ return true;
+}
+
+bool CGLContextEGL::CreatePB()
+{
+ const EGLint configAttribs[] =
+ {
+ EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
+ EGL_BLUE_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_RED_SIZE, 8,
+ EGL_DEPTH_SIZE, 8,
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
+ EGL_NONE
+ };
+
+ const EGLint pbufferAttribs[] =
+ {
+ EGL_WIDTH, 9,
+ EGL_HEIGHT, 9,
+ EGL_NONE,
+ };
+
+ Destroy();
+
+ if (m_eglGetPlatformDisplayEXT)
+ {
+ m_eglDisplay = m_eglGetPlatformDisplayEXT(EGL_PLATFORM_X11_EXT,(EGLNativeDisplayType)m_dpy,
+ NULL);
+ }
+ else
+ m_eglDisplay = eglGetDisplay((EGLNativeDisplayType)m_dpy);
+
+ if (m_eglDisplay == EGL_NO_DISPLAY)
+ {
+ CLog::Log(LOGERROR, "failed to get egl display");
+ return false;
+ }
+ if (!eglInitialize(m_eglDisplay, NULL, NULL))
+ {
+ CLog::Log(LOGERROR, "failed to initialize egl");
+ Destroy();
+ return false;
+ }
+ if (!eglBindAPI(m_renderingApi))
+ {
+ CLog::Log(LOGERROR, "failed to bind rendering API");
+ Destroy();
+ return false;
+ }
+
+ EGLint numConfigs;
+
+ eglChooseConfig(m_eglDisplay, configAttribs, &m_eglConfig, 1, &numConfigs);
+ m_eglSurface = eglCreatePbufferSurface(m_eglDisplay, m_eglConfig, pbufferAttribs);
+ if (m_eglSurface == EGL_NO_SURFACE)
+ {
+ CLog::Log(LOGERROR, "failed to create EGL window surface {}", eglGetError());
+ Destroy();
+ return false;
+ }
+
+ EGLint contextAttributes[] =
+ {
+ EGL_CONTEXT_MAJOR_VERSION_KHR, 3,
+ EGL_CONTEXT_MINOR_VERSION_KHR, 2,
+ EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR,
+ EGL_NONE
+ };
+ m_eglContext = eglCreateContext(m_eglDisplay, m_eglConfig, EGL_NO_CONTEXT, contextAttributes);
+ if (m_eglContext == EGL_NO_CONTEXT)
+ {
+ EGLint contextAttributes[] =
+ {
+ EGL_CONTEXT_MAJOR_VERSION_KHR, 2,
+ EGL_NONE
+ };
+ m_eglContext = eglCreateContext(m_eglDisplay, m_eglConfig, EGL_NO_CONTEXT, contextAttributes);
+
+ if (m_eglContext == EGL_NO_CONTEXT)
+ {
+ CLog::Log(LOGERROR, "failed to create EGL context");
+ Destroy();
+ return false;
+ }
+ }
+
+ if (!eglMakeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext))
+ {
+ CLog::Log(LOGERROR, "Failed to make context current {} {} {}", fmt::ptr(m_eglDisplay),
+ fmt::ptr(m_eglSurface), fmt::ptr(m_eglContext));
+ Destroy();
+ return false;
+ }
+
+ m_usePB = true;
+ return true;
+}
+
+void CGLContextEGL::Destroy()
+{
+ if (m_eglContext)
+ {
+ glFinish();
+ eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+ eglDestroyContext(m_eglDisplay, m_eglContext);
+ m_eglContext = EGL_NO_CONTEXT;
+ }
+
+ if (m_eglSurface)
+ {
+ eglDestroySurface(m_eglDisplay, m_eglSurface);
+ m_eglSurface = EGL_NO_SURFACE;
+ }
+
+ if (m_eglDisplay)
+ {
+ eglTerminate(m_eglDisplay);
+ m_eglDisplay = EGL_NO_DISPLAY;
+ }
+
+ m_eglConfig = EGL_NO_CONFIG;
+}
+
+void CGLContextEGL::Detach()
+{
+ if (m_eglContext)
+ {
+ glFinish();
+ eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+ }
+ if (m_eglSurface)
+ {
+ eglDestroySurface(m_eglDisplay, m_eglSurface);
+ m_eglSurface = EGL_NO_SURFACE;
+ }
+}
+
+bool CGLContextEGL::SuitableCheck(EGLDisplay eglDisplay, EGLConfig config)
+{
+ if (config == EGL_NO_CONFIG)
+ return false;
+
+ EGLint value;
+ if (!eglGetConfigAttrib(eglDisplay, config, EGL_RED_SIZE, &value) || value < 8)
+ return false;
+ if (!eglGetConfigAttrib(eglDisplay, config, EGL_GREEN_SIZE, &value) || value < 8)
+ return false;
+ if (!eglGetConfigAttrib(eglDisplay, config, EGL_BLUE_SIZE, &value) || value < 8)
+ return false;
+ if (!eglGetConfigAttrib(eglDisplay, config, EGL_DEPTH_SIZE, &value) || value < 24)
+ return false;
+
+ return true;
+}
+
+EGLConfig CGLContextEGL::GetEGLConfig(EGLDisplay eglDisplay, XVisualInfo *vInfo)
+{
+ EGLint numConfigs;
+
+ if (!eglGetConfigs(eglDisplay, nullptr, 0, &numConfigs))
+ {
+ CLog::Log(LOGERROR, "Failed to query number of egl configs");
+ return EGL_NO_CONFIG;
+ }
+ if (numConfigs == 0)
+ {
+ CLog::Log(LOGERROR, "No suitable egl configs found");
+ return EGL_NO_CONFIG;
+ }
+
+ EGLConfig *eglConfigs;
+ eglConfigs = (EGLConfig*)malloc(numConfigs * sizeof(EGLConfig));
+ if (!eglConfigs)
+ {
+ CLog::Log(LOGERROR, "eglConfigs malloc failed");
+ return EGL_NO_CONFIG;
+ }
+ EGLConfig eglConfig = EGL_NO_CONFIG;
+ if (!eglGetConfigs(eglDisplay, eglConfigs, numConfigs, &numConfigs))
+ {
+ CLog::Log(LOGERROR, "Failed to query egl configs");
+ goto Exit;
+ }
+ for (EGLint i = 0; i < numConfigs; ++i)
+ {
+ if (!SuitableCheck(eglDisplay, eglConfigs[i]))
+ continue;
+
+ EGLint value;
+ if (!eglGetConfigAttrib(eglDisplay, eglConfigs[i], EGL_NATIVE_VISUAL_ID, &value))
+ {
+ CLog::Log(LOGERROR, "Failed to query EGL_NATIVE_VISUAL_ID for egl config.");
+ break;
+ }
+ if (value == (EGLint)vInfo->visualid)
+ {
+ eglConfig = eglConfigs[i];
+ break;
+ }
+ }
+
+Exit:
+ free(eglConfigs);
+ return eglConfig;
+}
+
+void CGLContextEGL::SetVSync(bool enable)
+{
+ eglSwapInterval(m_eglDisplay, enable ? 1 : 0);
+}
+
+void CGLContextEGL::SwapBuffers()
+{
+ if ((m_eglDisplay == EGL_NO_DISPLAY) || (m_eglSurface == EGL_NO_SURFACE))
+ return;
+
+ if (m_usePB)
+ {
+ eglSwapBuffers(m_eglDisplay, m_eglSurface);
+ usleep(20 * 1000);
+ return;
+ }
+
+ uint64_t ust1, ust2;
+ uint64_t msc1, msc2;
+ uint64_t sbc1, sbc2;
+ struct timespec nowTs;
+ uint64_t now;
+ uint64_t cont = m_sync.cont;
+ uint64_t interval = m_sync.interval;
+
+ if (m_eglGetSyncValuesCHROMIUM)
+ {
+ m_eglGetSyncValuesCHROMIUM(m_eglDisplay, m_eglSurface, &ust1, &msc1, &sbc1);
+ }
+
+ eglSwapBuffers(m_eglDisplay, m_eglSurface);
+
+ if (!m_eglGetSyncValuesCHROMIUM)
+ return;
+
+ clock_gettime(CLOCK_MONOTONIC, &nowTs);
+ now = static_cast<uint64_t>(nowTs.tv_sec) * 1000000000ULL + nowTs.tv_nsec;
+
+ m_eglGetSyncValuesCHROMIUM(m_eglDisplay, m_eglSurface, &ust2, &msc2, &sbc2);
+
+ if ((msc1 - m_sync.msc1) > 2)
+ {
+ cont = 0;
+ }
+
+ // we want to block in SwapBuffers
+ // if a vertical retrace occurs 5 times in a row outside
+ // of this function, we take action
+ if (m_sync.cont < 5)
+ {
+ if ((msc1 - m_sync.msc1) == 2)
+ {
+ cont = 0;
+ }
+ else if ((msc1 - m_sync.msc1) == 1)
+ {
+ interval = (ust1 - m_sync.ust1) / (msc1 - m_sync.msc1);
+ cont++;
+ }
+ }
+ else if (m_sync.cont == 5 && m_omlSync)
+ {
+ CLog::Log(LOGDEBUG, "CGLContextEGL::SwapBuffers: sync check blocking");
+
+ if (msc2 == msc1)
+ {
+ // if no vertical retrace has occurred in eglSwapBuffers,
+ // sleep until next vertical retrace
+ uint64_t lastIncrement = (now / 1000 - ust2);
+ if (lastIncrement > m_sync.interval)
+ {
+ lastIncrement = m_sync.interval;
+ CLog::Log(LOGWARNING, "CGLContextEGL::SwapBuffers: last msc time greater than interval");
+ }
+ uint64_t sleeptime = m_sync.interval - lastIncrement;
+ usleep(sleeptime);
+ cont++;
+ msc2++;
+ CLog::Log(LOGDEBUG, "CGLContextEGL::SwapBuffers: sync sleep: {}", sleeptime);
+ }
+ }
+ else if ((m_sync.cont > 5) && (msc2 == m_sync.msc2))
+ {
+ // sleep until next vertical retrace
+ // this avoids blocking outside of this function
+ uint64_t lastIncrement = (now / 1000 - ust2);
+ if (lastIncrement > m_sync.interval)
+ {
+ lastIncrement = m_sync.interval;
+ CLog::Log(LOGWARNING, "CGLContextEGL::SwapBuffers: last msc time greater than interval (1)");
+ }
+ uint64_t sleeptime = m_sync.interval - lastIncrement;
+ usleep(sleeptime);
+ msc2++;
+ }
+ {
+ std::unique_lock<CCriticalSection> lock(m_syncLock);
+ m_sync.ust1 = ust1;
+ m_sync.ust2 = ust2;
+ m_sync.msc1 = msc1;
+ m_sync.msc2 = msc2;
+ m_sync.interval = interval;
+ m_sync.cont = cont;
+ }
+}
+
+uint64_t CGLContextEGL::GetVblankTiming(uint64_t &msc, uint64_t &interval)
+{
+ struct timespec nowTs;
+ uint64_t now;
+ clock_gettime(CLOCK_MONOTONIC, &nowTs);
+ now = static_cast<uint64_t>(nowTs.tv_sec) * 1000000000ULL + nowTs.tv_nsec;
+ now /= 1000;
+
+ std::unique_lock<CCriticalSection> lock(m_syncLock);
+ msc = m_sync.msc2;
+
+ interval = (m_sync.cont >= 5) ? m_sync.interval : m_sync.ust2 - m_sync.ust1;
+ if (interval == 0)
+ return 0;
+
+ if (now < m_sync.ust2)
+ {
+ return 0;
+ }
+
+ uint64_t ret = now - m_sync.ust2;
+ while (ret > interval)
+ {
+ ret -= interval;
+ msc++;
+ }
+
+ return ret;
+}
+
+void CGLContextEGL::QueryExtensions()
+{
+ std::string extensions = eglQueryString(m_eglDisplay, EGL_EXTENSIONS);
+ m_extensions = std::string(" ") + extensions + " ";
+
+ CLog::Log(LOGDEBUG, "EGL_EXTENSIONS:{}", m_extensions);
+}
diff --git a/xbmc/windowing/X11/GLContextEGL.h b/xbmc/windowing/X11/GLContextEGL.h
new file mode 100644
index 0000000..441787b
--- /dev/null
+++ b/xbmc/windowing/X11/GLContextEGL.h
@@ -0,0 +1,63 @@
+/*
+ * 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 "GLContext.h"
+#include "system_egl.h"
+#include "threads/CriticalSection.h"
+
+#include <cstdint>
+
+#include <EGL/eglext.h>
+#ifdef HAVE_EGLEXTANGLE
+#include <EGL/eglext_angle.h>
+#else
+#include <EGL/eglextchromium.h>
+#endif
+#include <X11/Xutil.h>
+
+class CGLContextEGL : public CGLContext
+{
+public:
+ explicit CGLContextEGL(Display *dpy, EGLint renderingApi);
+ ~CGLContextEGL() override;
+ bool Refresh(bool force, int screen, Window glWindow, bool &newContext) override;
+ bool CreatePB() override;
+ void Destroy() override;
+ void Detach() override;
+ void SetVSync(bool enable) override;
+ void SwapBuffers() override;
+ void QueryExtensions() override;
+ uint64_t GetVblankTiming(uint64_t &msc, uint64_t &interval) override;
+
+ EGLint m_renderingApi;
+ EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
+ EGLSurface m_eglSurface = EGL_NO_SURFACE;
+ EGLContext m_eglContext = EGL_NO_CONTEXT;
+ EGLConfig m_eglConfig;
+protected:
+ bool SuitableCheck(EGLDisplay eglDisplay, EGLConfig config);
+ EGLConfig GetEGLConfig(EGLDisplay eglDisplay, XVisualInfo *vInfo);
+ PFNEGLGETSYNCVALUESCHROMIUMPROC m_eglGetSyncValuesCHROMIUM = nullptr;
+ PFNEGLGETPLATFORMDISPLAYEXTPROC m_eglGetPlatformDisplayEXT = nullptr;
+
+ struct Sync
+ {
+ uint64_t cont = 0;
+ uint64_t ust1 = 0;
+ uint64_t ust2 = 0;
+ uint64_t msc1 = 0;
+ uint64_t msc2 = 0;
+ uint64_t interval = 0;
+ } m_sync;
+
+ CCriticalSection m_syncLock;
+
+ bool m_usePB = false;
+};
diff --git a/xbmc/windowing/X11/GLContextGLX.cpp b/xbmc/windowing/X11/GLContextGLX.cpp
new file mode 100644
index 0000000..3c31c22
--- /dev/null
+++ b/xbmc/windowing/X11/GLContextGLX.cpp
@@ -0,0 +1,293 @@
+/*
+ * 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 "GLContextGLX.h"
+
+#include "utils/log.h"
+
+#include <GL/glx.h>
+
+#include "system_gl.h"
+
+using namespace KODI::WINDOWING::X11;
+
+CGLContextGLX::CGLContextGLX(Display *dpy) : CGLContext(dpy)
+{
+ m_extPrefix = "GLX_";
+ m_vsyncMode = 0;
+}
+
+bool CGLContextGLX::Refresh(bool force, int screen, Window glWindow, bool &newContext)
+{
+ bool retVal = false;
+ m_glxWindow = glWindow;
+ m_nScreen = screen;
+
+ // refresh context
+ if (m_glxContext && !force)
+ {
+ CLog::Log(LOGDEBUG, "CWinSystemX11::RefreshGlxContext: refreshing context");
+ glXMakeCurrent(m_dpy, None, NULL);
+ glXMakeCurrent(m_dpy, glWindow, m_glxContext);
+ return true;
+ }
+
+ // create context
+
+ XVisualInfo vMask;
+ XVisualInfo *visuals;
+ XVisualInfo *vInfo = NULL;
+ int availableVisuals = 0;
+ vMask.screen = screen;
+ XWindowAttributes winAttr;
+
+ /* Assume a depth of 24 in case the below calls to XGetWindowAttributes()
+ or XGetVisualInfo() fail. That shouldn't happen unless something is
+ fatally wrong, but lets prepare for everything. */
+ vMask.depth = 24;
+
+ if (XGetWindowAttributes(m_dpy, glWindow, &winAttr))
+ {
+ vMask.visualid = XVisualIDFromVisual(winAttr.visual);
+ vInfo = XGetVisualInfo(m_dpy, VisualScreenMask | VisualIDMask, &vMask, &availableVisuals);
+ if (!vInfo)
+ CLog::Log(LOGWARNING, "Failed to get VisualInfo of visual 0x{:x}", (unsigned)vMask.visualid);
+ else if(!IsSuitableVisual(vInfo))
+ {
+ CLog::Log(LOGWARNING,
+ "Visual 0x{:x} of the window is not suitable, looking for another one...",
+ (unsigned)vInfo->visualid);
+ vMask.depth = vInfo->depth;
+ XFree(vInfo);
+ vInfo = NULL;
+ }
+ }
+ else
+ CLog::Log(LOGWARNING, "Failed to get window attributes");
+
+ /* As per glXMakeCurrent documentation, we have to use the same visual as
+ m_glWindow. Since that was not suitable for use, we try to use another
+ one with the same depth and hope that the used implementation is less
+ strict than the documentation. */
+ if (!vInfo)
+ {
+ visuals = XGetVisualInfo(m_dpy, VisualScreenMask | VisualDepthMask, &vMask, &availableVisuals);
+ for (int i = 0; i < availableVisuals; i++)
+ {
+ if (IsSuitableVisual(&visuals[i]))
+ {
+ vMask.visualid = visuals[i].visualid;
+ vInfo = XGetVisualInfo(m_dpy, VisualScreenMask | VisualIDMask, &vMask, &availableVisuals);
+ break;
+ }
+ }
+ XFree(visuals);
+ }
+
+ if (vInfo)
+ {
+ CLog::Log(LOGINFO, "Using visual 0x{:x}", (unsigned)vInfo->visualid);
+ if (m_glxContext)
+ {
+ glXMakeCurrent(m_dpy, None, NULL);
+ glXDestroyContext(m_dpy, m_glxContext);
+ XSync(m_dpy, False);
+ }
+
+ if ((m_glxContext = glXCreateContext(m_dpy, vInfo, NULL, True)))
+ {
+ // make this context current
+ glXMakeCurrent(m_dpy, glWindow, m_glxContext);
+ retVal = true;
+ newContext = true;
+ }
+ else
+ CLog::Log(LOGERROR, "GLX Error: Could not create context");
+
+ XFree(vInfo);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "GLX Error: vInfo is NULL!");
+ }
+
+ return retVal;
+}
+
+void CGLContextGLX::Destroy()
+{
+ glXMakeCurrent(m_dpy, None, NULL);
+ glXDestroyContext(m_dpy, m_glxContext);
+ m_glxContext = 0;
+}
+
+void CGLContextGLX::Detach()
+{
+ glXMakeCurrent(m_dpy, None, NULL);
+}
+
+bool CGLContextGLX::IsSuitableVisual(XVisualInfo *vInfo)
+{
+ int value;
+
+ if (glXGetConfig(m_dpy, vInfo, GLX_RGBA, &value) || !value)
+ return false;
+ if (glXGetConfig(m_dpy, vInfo, GLX_DOUBLEBUFFER, &value) || !value)
+ return false;
+ if (glXGetConfig(m_dpy, vInfo, GLX_RED_SIZE, &value) || value < 8)
+ return false;
+ if (glXGetConfig(m_dpy, vInfo, GLX_GREEN_SIZE, &value) || value < 8)
+ return false;
+ if (glXGetConfig(m_dpy, vInfo, GLX_BLUE_SIZE, &value) || value < 8)
+ return false;
+ if (glXGetConfig(m_dpy, vInfo, GLX_DEPTH_SIZE, &value) || value < 24)
+ return false;
+
+ return true;
+}
+
+void CGLContextGLX::SetVSync(bool enable)
+{
+ // turn of current setting first
+ if(m_glXSwapIntervalEXT)
+ m_glXSwapIntervalEXT(m_dpy, m_glxWindow, 0);
+ else if(m_glXSwapIntervalMESA)
+ m_glXSwapIntervalMESA(0);
+
+ m_iVSyncErrors = 0;
+
+ if(!enable)
+ return;
+
+ if (m_glXSwapIntervalEXT)
+ {
+ m_glXSwapIntervalEXT(m_dpy, m_glxWindow, 1);
+ m_vsyncMode = 6;
+ }
+ if (m_glXSwapIntervalMESA)
+ {
+ if(m_glXSwapIntervalMESA(1) == 0)
+ m_vsyncMode = 2;
+ else
+ CLog::Log(LOGWARNING, "{} - glXSwapIntervalMESA failed", __FUNCTION__);
+ }
+ if (m_glXWaitVideoSyncSGI && m_glXGetVideoSyncSGI && !m_vsyncMode)
+ {
+ unsigned int count;
+ if(m_glXGetVideoSyncSGI(&count) == 0)
+ m_vsyncMode = 3;
+ else
+ CLog::Log(LOGWARNING, "{} - glXGetVideoSyncSGI failed, glcontext probably not direct",
+ __FUNCTION__);
+ }
+}
+
+void CGLContextGLX::SwapBuffers()
+{
+ if (m_vsyncMode == 3)
+ {
+ glFinish();
+ unsigned int before = 0, after = 0;
+ if (m_glXGetVideoSyncSGI(&before) != 0)
+ CLog::Log(LOGERROR, "{} - glXGetVideoSyncSGI - Failed to get current retrace count",
+ __FUNCTION__);
+
+ glXSwapBuffers(m_dpy, m_glxWindow);
+ glFinish();
+
+ if(m_glXGetVideoSyncSGI(&after) != 0)
+ CLog::Log(LOGERROR, "{} - glXGetVideoSyncSGI - Failed to get current retrace count",
+ __FUNCTION__);
+
+ if (after == before)
+ m_iVSyncErrors = 1;
+ else
+ m_iVSyncErrors--;
+
+ if (m_iVSyncErrors > 0)
+ {
+ CLog::Log(LOGINFO, "GL: retrace count didn't change after buffer swap, switching to vsync mode 4");
+ m_iVSyncErrors = 0;
+ m_vsyncMode = 4;
+ }
+
+ if (m_iVSyncErrors < -200)
+ {
+ CLog::Log(
+ LOGINFO,
+ "GL: retrace count change for {} consecutive buffer swap, switching to vsync mode 2",
+ -m_iVSyncErrors);
+ m_iVSyncErrors = 0;
+ m_vsyncMode = 2;
+ }
+ }
+ else if (m_vsyncMode == 4)
+ {
+ glFinish();
+ unsigned int before = 0, swap = 0, after = 0;
+ if (m_glXGetVideoSyncSGI(&before) != 0)
+ CLog::Log(LOGERROR, "{} - glXGetVideoSyncSGI - Failed to get current retrace count",
+ __FUNCTION__);
+
+ if(m_glXWaitVideoSyncSGI(2, (before+1)%2, &swap) != 0)
+ CLog::Log(LOGERROR, "{} - glXWaitVideoSyncSGI - Returned error", __FUNCTION__);
+
+ glXSwapBuffers(m_dpy, m_glxWindow);
+ glFinish();
+
+ if (m_glXGetVideoSyncSGI(&after) != 0)
+ CLog::Log(LOGERROR, "{} - glXGetVideoSyncSGI - Failed to get current retrace count",
+ __FUNCTION__);
+
+ if (after == before)
+ CLog::Log(LOGERROR, "{} - glXWaitVideoSyncSGI - Woke up early", __FUNCTION__);
+
+ if (after > before + 1)
+ m_iVSyncErrors++;
+ else
+ m_iVSyncErrors = 0;
+
+ if (m_iVSyncErrors > 30)
+ {
+ CLog::Log(LOGINFO, "GL: retrace count seems to be changing due to the swapbuffers call, switching to vsync mode 3");
+ m_vsyncMode = 3;
+ m_iVSyncErrors = 0;
+ }
+ }
+ else
+ glXSwapBuffers(m_dpy, m_glxWindow);
+}
+
+void CGLContextGLX::QueryExtensions()
+{
+ m_extensions = " ";
+ m_extensions += glXQueryExtensionsString(m_dpy, m_nScreen);
+ m_extensions += " ";
+
+ CLog::Log(LOGDEBUG, "GLX_EXTENSIONS:{}", m_extensions);
+
+ if (IsExtSupported("GLX_SGI_video_sync"))
+ m_glXWaitVideoSyncSGI = (int (*)(int, int, unsigned int*))glXGetProcAddress((const GLubyte*)"glXWaitVideoSyncSGI");
+ else
+ m_glXWaitVideoSyncSGI = NULL;
+
+ if (IsExtSupported("GLX_SGI_video_sync"))
+ m_glXGetVideoSyncSGI = (int (*)(unsigned int*))glXGetProcAddress((const GLubyte*)"glXGetVideoSyncSGI");
+ else
+ m_glXGetVideoSyncSGI = NULL;
+
+ if (IsExtSupported("GLX_MESA_swap_control"))
+ m_glXSwapIntervalMESA = (int (*)(int))glXGetProcAddress((const GLubyte*)"glXSwapIntervalMESA");
+ else
+ m_glXSwapIntervalMESA = NULL;
+
+ if (IsExtSupported("GLX_EXT_swap_control"))
+ m_glXSwapIntervalEXT = (PFNGLXSWAPINTERVALEXTPROC)glXGetProcAddress((const GLubyte*)"glXSwapIntervalEXT");
+ else
+ m_glXSwapIntervalEXT = NULL;
+}
diff --git a/xbmc/windowing/X11/GLContextGLX.h b/xbmc/windowing/X11/GLContextGLX.h
new file mode 100644
index 0000000..6fd41b3
--- /dev/null
+++ b/xbmc/windowing/X11/GLContextGLX.h
@@ -0,0 +1,49 @@
+/*
+ * 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 "GLContext.h"
+
+#include <GL/glx.h>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+class CGLContextGLX : public CGLContext
+{
+public:
+ explicit CGLContextGLX(Display *dpy);
+ bool Refresh(bool force, int screen, Window glWindow, bool &newContext) override;
+ void Destroy() override;
+ void Detach() override;
+ void SetVSync(bool enable) override;
+ void SwapBuffers() override;
+ void QueryExtensions() override;
+ GLXWindow m_glxWindow = 0;
+ GLXContext m_glxContext = 0;
+
+protected:
+ bool IsSuitableVisual(XVisualInfo *vInfo);
+
+ int (*m_glXGetVideoSyncSGI)(unsigned int*);
+ int (*m_glXWaitVideoSyncSGI)(int, int, unsigned int*);
+ int (*m_glXSwapIntervalMESA)(int);
+ PFNGLXSWAPINTERVALEXTPROC m_glXSwapIntervalEXT;
+ int m_nScreen;
+ int m_iVSyncErrors;
+ int m_vsyncMode;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/X11/OSScreenSaverX11.cpp b/xbmc/windowing/X11/OSScreenSaverX11.cpp
new file mode 100644
index 0000000..3395e46
--- /dev/null
+++ b/xbmc/windowing/X11/OSScreenSaverX11.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017-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 "OSScreenSaverX11.h"
+
+#include <cassert>
+
+using namespace std::chrono_literals;
+
+COSScreenSaverX11::COSScreenSaverX11(Display* dpy)
+: m_dpy(dpy), m_screensaverResetTimer(std::bind(&COSScreenSaverX11::ResetScreenSaver, this))
+{
+ assert(m_dpy);
+}
+
+void COSScreenSaverX11::Inhibit()
+{
+ // disallow the screensaver by periodically calling XResetScreenSaver(),
+ // for some reason setting a 0 timeout with XSetScreenSaver doesn't work with gnome
+ m_screensaverResetTimer.Start(5000ms, true);
+}
+
+void COSScreenSaverX11::Uninhibit()
+{
+ m_screensaverResetTimer.Stop(true);
+}
+
+void COSScreenSaverX11::ResetScreenSaver()
+{
+ XResetScreenSaver(m_dpy);
+}
diff --git a/xbmc/windowing/X11/OSScreenSaverX11.h b/xbmc/windowing/X11/OSScreenSaverX11.h
new file mode 100644
index 0000000..60c9b45
--- /dev/null
+++ b/xbmc/windowing/X11/OSScreenSaverX11.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017-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 "../OSScreenSaver.h"
+#include "threads/Timer.h"
+
+#include <X11/Xlib.h>
+
+class COSScreenSaverX11 : public KODI::WINDOWING::IOSScreenSaver
+{
+public:
+ explicit COSScreenSaverX11(Display* dpy);
+ void Inhibit() override;
+ void Uninhibit() override;
+
+private:
+ void ResetScreenSaver();
+
+ Display* m_dpy;
+ CTimer m_screensaverResetTimer;
+};
diff --git a/xbmc/windowing/X11/OptionalsReg.cpp b/xbmc/windowing/X11/OptionalsReg.cpp
new file mode 100644
index 0000000..ecfd780
--- /dev/null
+++ b/xbmc/windowing/X11/OptionalsReg.cpp
@@ -0,0 +1,256 @@
+/*
+ * 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 "OptionalsReg.h"
+
+//-----------------------------------------------------------------------------
+// VAAPI
+//-----------------------------------------------------------------------------
+#if defined (HAVE_LIBVA)
+#include <va/va_x11.h>
+#include "cores/VideoPlayer/DVDCodecs/Video/VAAPI.h"
+#if defined(HAS_GL)
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.h"
+#endif
+#if defined(HAS_GLES)
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.h"
+#endif
+
+using namespace KODI::WINDOWING::X11;
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+class CWinSystemX11GLContext;
+
+class CVaapiProxy : public VAAPI::IVaapiWinSystem
+{
+public:
+ CVaapiProxy() = default;
+ virtual ~CVaapiProxy() = default;
+ VADisplay GetVADisplay() override { return vaGetDisplay(dpy); };
+ void *GetEGLDisplay() override { return eglDisplay; };
+
+ Display *dpy;
+ void *eglDisplay;
+};
+
+CVaapiProxy* VaapiProxyCreate()
+{
+ return new CVaapiProxy();
+}
+
+void VaapiProxyDelete(CVaapiProxy *proxy)
+{
+ delete proxy;
+}
+
+void VaapiProxyConfig(CVaapiProxy *proxy, void *dpy, void *eglDpy)
+{
+ proxy->dpy = static_cast<Display*>(dpy);
+ proxy->eglDisplay = eglDpy;
+}
+
+void VAAPIRegister(CVaapiProxy *winSystem, bool deepColor)
+{
+ VAAPI::CDecoder::Register(winSystem, deepColor);
+}
+
+#if defined(HAS_GL)
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+ EGLDisplay eglDpy = winSystem->eglDisplay;
+ VADisplay vaDpy = vaGetDisplay(winSystem->dpy);
+ CRendererVAAPIGL::Register(winSystem, vaDpy, eglDpy, general, deepColor);
+}
+#endif
+
+#if defined(HAS_GLES)
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+ EGLDisplay eglDpy = winSystem->eglDisplay;
+ VADisplay vaDpy = vaGetDisplay(winSystem->dpy);
+ CRendererVAAPIGLES::Register(winSystem, vaDpy, eglDpy, general, deepColor);
+}
+#endif
+}
+}
+}
+
+#else
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+class CVaapiProxy
+{
+};
+
+CVaapiProxy* VaapiProxyCreate()
+{
+ return nullptr;
+}
+
+void VaapiProxyDelete(CVaapiProxy *proxy)
+{
+}
+
+void VaapiProxyConfig(CVaapiProxy *proxy, void *dpy, void *eglDpy)
+{
+}
+
+void VAAPIRegister(CVaapiProxy *winSystem, bool deepColor)
+{
+}
+
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+}
+
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+}
+
+}
+}
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// GLX
+//-----------------------------------------------------------------------------
+
+#ifdef HAS_GLX
+#include <GL/glx.h>
+#include "VideoSyncGLX.h"
+#include "GLContextGLX.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+XID GLXGetWindow(void* context)
+{
+ return static_cast<CGLContextGLX*>(context)->m_glxWindow;
+}
+
+void* GLXGetContext(void* context)
+{
+ return static_cast<CGLContextGLX*>(context)->m_glxContext;
+}
+
+CGLContext* GLXContextCreate(Display *dpy)
+{
+ return new CGLContextGLX(dpy);
+}
+
+
+CVideoSync* GLXVideoSyncCreate(void* clock, CWinSystemX11GLContext& winSystem)
+{
+ return new CVideoSyncGLX(clock, winSystem);
+}
+
+}
+}
+}
+#else
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+XID GLXGetWindow(void* context)
+{
+ return 0;
+}
+
+void* GLXGetContext(void* context)
+{
+ return nullptr;
+}
+
+CGLContext* GLXContextCreate(Display *dpy)
+{
+ return nullptr;
+}
+
+CVideoSync* GLXVideoSyncCreate(void* clock, CWinSystemX11GLContext& winSystem)
+{
+ return nullptr;
+}
+
+}
+}
+}
+#endif
+
+//-----------------------------------------------------------------------------
+// VDPAU
+//-----------------------------------------------------------------------------
+
+#if defined (HAVE_LIBVDPAU)
+#include "cores/VideoPlayer/DVDCodecs/Video/VDPAU.h"
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVDPAU.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+void VDPAURegisterRender()
+{
+ CRendererVDPAU::Register();
+}
+
+void VDPAURegister()
+{
+ VDPAU::CDecoder::Register();
+}
+
+}
+}
+}
+#else
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+void VDPAURegisterRender()
+{
+
+}
+
+void VDPAURegister()
+{
+
+}
+
+}
+}
+}
+#endif
+
diff --git a/xbmc/windowing/X11/OptionalsReg.h b/xbmc/windowing/X11/OptionalsReg.h
new file mode 100644
index 0000000..2c4f15b
--- /dev/null
+++ b/xbmc/windowing/X11/OptionalsReg.h
@@ -0,0 +1,78 @@
+/*
+ * 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 <X11/Xlib.h>
+
+//-----------------------------------------------------------------------------
+// VAAPI
+//-----------------------------------------------------------------------------
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+class CVaapiProxy;
+
+CVaapiProxy* VaapiProxyCreate();
+void VaapiProxyDelete(CVaapiProxy *proxy);
+void VaapiProxyConfig(CVaapiProxy *proxy, void *dpy, void *eglDpy);
+void VAAPIRegister(CVaapiProxy *winSystem, bool deepColor);
+#if defined(HAS_GL)
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor);
+#endif
+#if defined(HAS_GLES)
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor);
+#endif
+}
+}
+}
+
+//-----------------------------------------------------------------------------
+// GLX
+//-----------------------------------------------------------------------------
+
+class CVideoSync;
+class CGLContext;
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+class CWinSystemX11GLContext;
+
+XID GLXGetWindow(void* context);
+void* GLXGetContext(void* context);
+CGLContext* GLXContextCreate(Display *dpy);
+CVideoSync* GLXVideoSyncCreate(void *clock, CWinSystemX11GLContext& winSystem);
+}
+}
+}
+
+//-----------------------------------------------------------------------------
+// VDPAU
+//-----------------------------------------------------------------------------
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+void VDPAURegisterRender();
+void VDPAURegister();
+}
+}
+}
diff --git a/xbmc/windowing/X11/VideoSyncGLX.cpp b/xbmc/windowing/X11/VideoSyncGLX.cpp
new file mode 100644
index 0000000..0e29a85
--- /dev/null
+++ b/xbmc/windowing/X11/VideoSyncGLX.cpp
@@ -0,0 +1,277 @@
+/*
+ * 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 "VideoSyncGLX.h"
+
+#include "utils/TimeUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/X11/WinSystemX11GLContext.h"
+
+#include <mutex>
+#include <sstream>
+
+#include <X11/extensions/Xrandr.h>
+
+using namespace KODI::WINDOWING::X11;
+
+using namespace std::chrono_literals;
+
+Display* CVideoSyncGLX::m_Dpy = NULL;
+
+void CVideoSyncGLX::OnLostDisplay()
+{
+ if (!m_displayLost)
+ {
+ m_displayLost = true;
+ m_lostEvent.Wait();
+ }
+}
+
+void CVideoSyncGLX::OnResetDisplay()
+{
+ m_displayReset = true;
+}
+
+bool CVideoSyncGLX::Setup(PUPDATECLOCK func)
+{
+ std::unique_lock<CCriticalSection> lock(m_winSystem.GetGfxContext());
+
+ m_glXWaitVideoSyncSGI = NULL;
+ m_glXGetVideoSyncSGI = NULL;
+ m_vInfo = NULL;
+ m_Window = 0;
+ m_Context = NULL;
+ UpdateClock = func;
+
+ int singleBufferAttributes[] = {
+ GLX_RGBA,
+ GLX_RED_SIZE, 0,
+ GLX_GREEN_SIZE, 0,
+ GLX_BLUE_SIZE, 0,
+ None
+ };
+
+ int ReturnV, SwaMask;
+ unsigned int GlxTest;
+ XSetWindowAttributes Swa;
+
+ m_vInfo = NULL;
+ m_Context = NULL;
+ m_Window = 0;
+
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: Setting up GLX");
+
+ static_cast<CWinSystemX11*>(&m_winSystem)->Register(this);
+
+ m_displayLost = false;
+ m_displayReset = false;
+ m_lostEvent.Reset();
+
+ if (!m_Dpy)
+ {
+ m_Dpy = XOpenDisplay(NULL);
+ if (!m_Dpy)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: Unable to open display");
+ return false;
+ }
+ }
+
+ if (!glXQueryExtension(m_Dpy, NULL, NULL))
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: X server does not support GLX");
+ return false;
+ }
+
+ bool ExtensionFound = false;
+ std::istringstream Extensions(glXQueryExtensionsString(m_Dpy, m_winSystem.GetScreen()));
+ std::string ExtensionStr;
+
+ while (!ExtensionFound)
+ {
+ Extensions >> ExtensionStr;
+ if (Extensions.fail())
+ break;
+
+ if (ExtensionStr == "GLX_SGI_video_sync")
+ ExtensionFound = true;
+ }
+
+ if (!ExtensionFound)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: X server does not support GLX_SGI_video_sync");
+ return false;
+ }
+
+ m_vInfo = glXChooseVisual(m_Dpy, m_winSystem.GetScreen(), singleBufferAttributes);
+ if (!m_vInfo)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXChooseVisual returned NULL");
+ return false;
+ }
+
+ Swa.border_pixel = 0;
+ Swa.event_mask = StructureNotifyMask;
+ Swa.colormap = XCreateColormap(m_Dpy, m_winSystem.GetWindow(), m_vInfo->visual, AllocNone );
+ SwaMask = CWBorderPixel | CWColormap | CWEventMask;
+
+ m_Window = XCreateWindow(m_Dpy, m_winSystem.GetWindow(), 0, 0, 256, 256, 0,
+ m_vInfo->depth, InputOutput, m_vInfo->visual, SwaMask, &Swa);
+
+ m_Context = glXCreateContext(m_Dpy, m_vInfo, NULL, True);
+ if (!m_Context)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXCreateContext returned NULL");
+ return false;
+ }
+
+ ReturnV = glXMakeCurrent(m_Dpy, m_Window, m_Context);
+ if (ReturnV != True)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXMakeCurrent returned {}", ReturnV);
+ return false;
+ }
+
+ m_glXWaitVideoSyncSGI = (int (*)(int, int, unsigned int*))glXGetProcAddress((const GLubyte*)"glXWaitVideoSyncSGI");
+ if (!m_glXWaitVideoSyncSGI)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXWaitVideoSyncSGI not found");
+ return false;
+ }
+
+ ReturnV = m_glXWaitVideoSyncSGI(2, 0, &GlxTest);
+ if (ReturnV)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXWaitVideoSyncSGI returned {}", ReturnV);
+ return false;
+ }
+
+ m_glXGetVideoSyncSGI = (int (*)(unsigned int*))glXGetProcAddress((const GLubyte*)"glXGetVideoSyncSGI");
+ if (!m_glXGetVideoSyncSGI)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXGetVideoSyncSGI not found");
+ return false;
+ }
+
+ ReturnV = m_glXGetVideoSyncSGI(&GlxTest);
+ if (ReturnV)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXGetVideoSyncSGI returned {}", ReturnV);
+ return false;
+ }
+
+ return true;
+}
+
+void CVideoSyncGLX::Run(CEvent& stopEvent)
+{
+ unsigned int PrevVblankCount;
+ unsigned int VblankCount;
+ int ReturnV;
+ bool IsReset = false;
+ int64_t Now;
+
+ //get the current vblank counter
+ m_glXGetVideoSyncSGI(&VblankCount);
+ PrevVblankCount = VblankCount;
+
+ while(!stopEvent.Signaled() && !m_displayLost && !m_displayReset)
+ {
+ //wait for the next vblank
+ ReturnV = m_glXWaitVideoSyncSGI(2, (VblankCount + 1) % 2, &VblankCount);
+ m_glXGetVideoSyncSGI(&VblankCount); //the vblank count returned by glXWaitVideoSyncSGI is not always correct
+ Now = CurrentHostCounter(); //get the timestamp of this vblank
+
+ if(ReturnV)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXWaitVideoSyncSGI returned {}", ReturnV);
+ return;
+ }
+
+ if (VblankCount > PrevVblankCount)
+ {
+ UpdateClock((int)(VblankCount - PrevVblankCount), Now, m_refClock);
+ IsReset = false;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: Vblank counter has reset");
+
+ //only try reattaching once
+ if (IsReset)
+ return;
+
+ //because of a bug in the nvidia driver, glXWaitVideoSyncSGI breaks when the vblank counter resets
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: Detaching glX context");
+ ReturnV = glXMakeCurrent(m_Dpy, None, NULL);
+ if (ReturnV != True)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXMakeCurrent returned {}", ReturnV);
+ return;
+ }
+
+ //sleep here so we don't busy spin when this constantly happens, for example when the display went to sleep
+ KODI::TIME::Sleep(1s);
+
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: Attaching glX context");
+ ReturnV = glXMakeCurrent(m_Dpy, m_Window, m_Context);
+ if (ReturnV != True)
+ {
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: glXMakeCurrent returned {}", ReturnV);
+ return;
+ }
+
+ m_glXGetVideoSyncSGI(&VblankCount);
+
+ IsReset = true;
+ }
+ PrevVblankCount = VblankCount;
+ }
+ m_lostEvent.Set();
+ while(!stopEvent.Signaled() && m_displayLost && !m_displayReset)
+ {
+ KODI::TIME::Sleep(10ms);
+ }
+}
+
+void CVideoSyncGLX::Cleanup()
+{
+ CLog::Log(LOGDEBUG, "CVideoReferenceClock: Cleaning up GLX");
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_winSystem.GetGfxContext());
+
+ if (m_vInfo)
+ {
+ XFree(m_vInfo);
+ m_vInfo = NULL;
+ }
+ if (m_Context)
+ {
+ glXMakeCurrent(m_Dpy, None, NULL);
+ glXDestroyContext(m_Dpy, m_Context);
+ m_Context = NULL;
+ }
+ if (m_Window)
+ {
+ XDestroyWindow(m_Dpy, m_Window);
+ m_Window = 0;
+ }
+ }
+
+ m_lostEvent.Set();
+ m_winSystem.Unregister(this);
+}
+
+float CVideoSyncGLX::GetFps()
+{
+ m_fps = m_winSystem.GetGfxContext().GetFPS();
+ return m_fps;
+}
diff --git a/xbmc/windowing/X11/VideoSyncGLX.h b/xbmc/windowing/X11/VideoSyncGLX.h
new file mode 100644
index 0000000..06d348e
--- /dev/null
+++ b/xbmc/windowing/X11/VideoSyncGLX.h
@@ -0,0 +1,62 @@
+/*
+ * 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 "guilib/DispResource.h"
+#include "threads/Event.h"
+#include "windowing/VideoSync.h"
+
+#include <GL/glx.h>
+#include <X11/X.h>
+#include <X11/Xlib.h>
+
+#include "system_gl.h"
+
+
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+class CWinSystemX11GLContext;
+
+class CVideoSyncGLX : public CVideoSync, IDispResource
+{
+public:
+ explicit CVideoSyncGLX(void* clock, CWinSystemX11GLContext& winSystem)
+ : CVideoSync(clock), m_winSystem(winSystem)
+ {
+ }
+ bool Setup(PUPDATECLOCK func) override;
+ void Run(CEvent& stopEvent) override;
+ void Cleanup() override;
+ float GetFps() override;
+ void OnLostDisplay() override;
+ void OnResetDisplay() override;
+
+private:
+ int (*m_glXWaitVideoSyncSGI) (int, int, unsigned int*);
+ int (*m_glXGetVideoSyncSGI) (unsigned int*);
+
+ static Display* m_Dpy;
+ CWinSystemX11GLContext &m_winSystem;
+ XVisualInfo *m_vInfo;
+ Window m_Window;
+ GLXContext m_Context;
+ volatile bool m_displayLost;
+ volatile bool m_displayReset;
+ CEvent m_lostEvent;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/X11/VideoSyncOML.cpp b/xbmc/windowing/X11/VideoSyncOML.cpp
new file mode 100644
index 0000000..2dc91fe
--- /dev/null
+++ b/xbmc/windowing/X11/VideoSyncOML.cpp
@@ -0,0 +1,83 @@
+/*
+ * 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 "VideoSyncOML.h"
+
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/X11/WinSystemX11GLContext.h"
+
+#include <unistd.h>
+
+using namespace KODI::WINDOWING::X11;
+
+bool CVideoSyncOML::Setup(PUPDATECLOCK func)
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncOML::{} - setting up OML", __FUNCTION__);
+
+ UpdateClock = func;
+
+ m_abort = false;
+
+ static_cast<CWinSystemX11*>(&m_winSystem)->Register(this);
+
+ return true;
+}
+
+void CVideoSyncOML::Run(CEvent& stopEvent)
+{
+ uint64_t interval, timeSinceVblank, msc;
+
+ timeSinceVblank = m_winSystem.GetVblankTiming(msc, interval);
+
+ while (!stopEvent.Signaled() && !m_abort)
+ {
+ if (interval == 0)
+ {
+ usleep(10000);
+ }
+ else
+ {
+ usleep(interval - timeSinceVblank + 1000);
+ }
+ uint64_t newMsc;
+ timeSinceVblank = m_winSystem.GetVblankTiming(newMsc, interval);
+
+ if (newMsc == msc)
+ {
+ newMsc++;
+ }
+ else if (newMsc < msc)
+ {
+ timeSinceVblank = interval;
+ continue;
+ }
+
+ uint64_t now = CurrentHostCounter();
+ UpdateClock(newMsc - msc, now, m_refClock);
+ msc = newMsc;
+ }
+}
+
+void CVideoSyncOML::Cleanup()
+{
+ m_winSystem.Unregister(this);
+}
+
+void CVideoSyncOML::OnResetDisplay()
+{
+ m_abort = true;
+}
+
+float CVideoSyncOML::GetFps()
+{
+ m_fps = m_winSystem.GetGfxContext().GetFPS();
+ return m_fps;
+}
+
diff --git a/xbmc/windowing/X11/VideoSyncOML.h b/xbmc/windowing/X11/VideoSyncOML.h
new file mode 100644
index 0000000..a04bd1d
--- /dev/null
+++ b/xbmc/windowing/X11/VideoSyncOML.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "guilib/DispResource.h"
+#include "windowing/VideoSync.h"
+
+#include <atomic>
+
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+class CWinSystemX11GLContext;
+
+class CVideoSyncOML : public CVideoSync, IDispResource
+{
+public:
+ explicit CVideoSyncOML(void* clock, CWinSystemX11GLContext& winSystem)
+ : CVideoSync(clock), m_winSystem(winSystem)
+ {
+ }
+ bool Setup(PUPDATECLOCK func) override;
+ void Run(CEvent& stopEvent) override;
+ void Cleanup() override;
+ float GetFps() override;
+ void OnResetDisplay() override;
+
+private:
+ std::atomic_bool m_abort;
+ CWinSystemX11GLContext &m_winSystem;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/X11/WinEventsX11.cpp b/xbmc/windowing/X11/WinEventsX11.cpp
new file mode 100644
index 0000000..faffd99
--- /dev/null
+++ b/xbmc/windowing/X11/WinEventsX11.cpp
@@ -0,0 +1,673 @@
+/*
+ * 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 "WinEventsX11.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "application/Application.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/InputManager.h"
+#include "input/mouse/MouseStat.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/CharsetConverter.h"
+#include "utils/log.h"
+#include "windowing/WinEvents.h"
+#include "windowing/X11/WinSystemX11.h"
+
+#include <stdexcept>
+
+#include <X11/XF86keysym.h>
+#include <X11/Xlib.h>
+#include <X11/extensions/Xrandr.h>
+#include <X11/keysymdef.h>
+
+using namespace KODI::WINDOWING::X11;
+
+static uint32_t SymMappingsX11[][2] =
+{
+ {XK_BackSpace, XBMCK_BACKSPACE}
+, {XK_Tab, XBMCK_TAB}
+, {XK_Clear, XBMCK_CLEAR}
+, {XK_Return, XBMCK_RETURN}
+, {XK_Pause, XBMCK_PAUSE}
+, {XK_Escape, XBMCK_ESCAPE}
+, {XK_Delete, XBMCK_DELETE}
+// multi-media keys
+, {XF86XK_Back, XBMCK_BROWSER_BACK}
+, {XF86XK_Forward, XBMCK_BROWSER_FORWARD}
+, {XF86XK_Refresh, XBMCK_BROWSER_REFRESH}
+, {XF86XK_Stop, XBMCK_BROWSER_STOP}
+, {XF86XK_Search, XBMCK_BROWSER_SEARCH}
+, {XF86XK_Favorites, XBMCK_BROWSER_FAVORITES}
+, {XF86XK_HomePage, XBMCK_BROWSER_HOME}
+, {XF86XK_AudioMute, XBMCK_VOLUME_MUTE}
+, {XF86XK_AudioLowerVolume, XBMCK_VOLUME_DOWN}
+, {XF86XK_AudioRaiseVolume, XBMCK_VOLUME_UP}
+, {XF86XK_AudioNext, XBMCK_MEDIA_NEXT_TRACK}
+, {XF86XK_AudioPrev, XBMCK_MEDIA_PREV_TRACK}
+, {XF86XK_AudioStop, XBMCK_MEDIA_STOP}
+, {XF86XK_AudioPause, XBMCK_MEDIA_PLAY_PAUSE}
+, {XF86XK_Mail, XBMCK_LAUNCH_MAIL}
+, {XF86XK_Select, XBMCK_LAUNCH_MEDIA_SELECT}
+, {XF86XK_Launch0, XBMCK_LAUNCH_APP1}
+, {XF86XK_Launch1, XBMCK_LAUNCH_APP2}
+, {XF86XK_WWW, XBMCK_LAUNCH_FILE_BROWSER}
+, {XF86XK_AudioMedia, XBMCK_LAUNCH_MEDIA_CENTER }
+ // Numeric keypad
+, {XK_KP_0, XBMCK_KP0}
+, {XK_KP_1, XBMCK_KP1}
+, {XK_KP_2, XBMCK_KP2}
+, {XK_KP_3, XBMCK_KP3}
+, {XK_KP_4, XBMCK_KP4}
+, {XK_KP_5, XBMCK_KP5}
+, {XK_KP_6, XBMCK_KP6}
+, {XK_KP_7, XBMCK_KP7}
+, {XK_KP_8, XBMCK_KP8}
+, {XK_KP_9, XBMCK_KP9}
+, {XK_KP_Separator, XBMCK_KP_PERIOD}
+, {XK_KP_Divide, XBMCK_KP_DIVIDE}
+, {XK_KP_Multiply, XBMCK_KP_MULTIPLY}
+, {XK_KP_Subtract, XBMCK_KP_MINUS}
+, {XK_KP_Add, XBMCK_KP_PLUS}
+, {XK_KP_Enter, XBMCK_KP_ENTER}
+, {XK_KP_Equal, XBMCK_KP_EQUALS}
+ // Arrows + Home/End pad
+, {XK_Up, XBMCK_UP}
+, {XK_Down, XBMCK_DOWN}
+, {XK_Right, XBMCK_RIGHT}
+, {XK_Left, XBMCK_LEFT}
+, {XK_Insert, XBMCK_INSERT}
+, {XK_Home, XBMCK_HOME}
+, {XK_End, XBMCK_END}
+, {XK_Page_Up, XBMCK_PAGEUP}
+, {XK_Page_Down, XBMCK_PAGEDOWN}
+ // Function keys
+, {XK_F1, XBMCK_F1}
+, {XK_F2, XBMCK_F2}
+, {XK_F3, XBMCK_F3}
+, {XK_F4, XBMCK_F4}
+, {XK_F5, XBMCK_F5}
+, {XK_F6, XBMCK_F6}
+, {XK_F7, XBMCK_F7}
+, {XK_F8, XBMCK_F8}
+, {XK_F9, XBMCK_F9}
+, {XK_F10, XBMCK_F10}
+, {XK_F11, XBMCK_F11}
+, {XK_F12, XBMCK_F12}
+, {XK_F13, XBMCK_F13}
+, {XK_F14, XBMCK_F14}
+, {XK_F15, XBMCK_F15}
+ // Key state modifier keys
+, {XK_Num_Lock, XBMCK_NUMLOCK}
+, {XK_Caps_Lock, XBMCK_CAPSLOCK}
+, {XK_Scroll_Lock, XBMCK_SCROLLOCK}
+, {XK_Shift_R, XBMCK_RSHIFT}
+, {XK_Shift_L, XBMCK_LSHIFT}
+, {XK_Control_R, XBMCK_RCTRL}
+, {XK_Control_L, XBMCK_LCTRL}
+, {XK_Alt_R, XBMCK_RALT}
+, {XK_Alt_L, XBMCK_LALT}
+, {XK_Meta_R, XBMCK_RMETA}
+, {XK_Meta_L, XBMCK_LMETA}
+, {XK_Super_L, XBMCK_LSUPER}
+, {XK_Super_R, XBMCK_RSUPER}
+, {XK_Mode_switch, XBMCK_MODE}
+, {XK_Multi_key, XBMCK_COMPOSE}
+ // Miscellaneous function keys
+, {XK_Help, XBMCK_HELP}
+, {XK_Print, XBMCK_PRINT}
+//, {0, XBMCK_SYSREQ}
+, {XK_Break, XBMCK_BREAK}
+, {XK_Menu, XBMCK_MENU}
+, {XF86XK_PowerOff, XBMCK_POWER}
+, {XF86XK_Sleep, XBMCK_SLEEP}
+, {XK_EcuSign, XBMCK_EURO}
+, {XK_Undo, XBMCK_UNDO}
+ /* Media keys */
+, {XF86XK_Eject, XBMCK_EJECT}
+, {XF86XK_Stop, XBMCK_STOP}
+, {XF86XK_AudioRecord, XBMCK_RECORD}
+, {XF86XK_AudioRewind, XBMCK_REWIND}
+, {XF86XK_Phone, XBMCK_PHONE}
+, {XF86XK_AudioPlay, XBMCK_PLAY}
+, {XF86XK_AudioRandomPlay, XBMCK_SHUFFLE}
+, {XF86XK_AudioForward, XBMCK_FASTFORWARD}
+};
+
+CWinEventsX11::CWinEventsX11(CWinSystemX11& winSystem) : m_winSystem(winSystem)
+{
+}
+
+CWinEventsX11::~CWinEventsX11()
+{
+ Quit();
+}
+
+bool CWinEventsX11::Init(Display *dpy, Window win)
+{
+ if (m_display)
+ return true;
+
+ m_display = dpy;
+ m_window = win;
+ m_keybuf_len = 32*sizeof(char);
+ m_keybuf = (char*)malloc(m_keybuf_len);
+ m_keymodState = 0;
+ m_wmDeleteMessage = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
+ m_structureChanged = false;
+ m_xrrEventPending = false;
+
+ // open input method
+ char *old_locale = NULL, *old_modifiers = NULL;
+ char res_name[8];
+ const char *p;
+
+ // set resource name to xbmc, not used
+ strcpy(res_name, "xbmc");
+
+ // save current locale, this should be "C"
+ p = setlocale(LC_ALL, NULL);
+ if (p)
+ {
+ old_locale = (char*)malloc(strlen(p) +1);
+ strcpy(old_locale, p);
+ }
+ p = XSetLocaleModifiers(NULL);
+ if (p)
+ {
+ old_modifiers = (char*)malloc(strlen(p) +1);
+ strcpy(old_modifiers, p);
+ }
+
+ // set users preferences and open input method
+ p = setlocale(LC_ALL, "");
+ XSetLocaleModifiers("");
+ m_xim = XOpenIM(m_display, NULL, res_name, res_name);
+
+ // restore old locale
+ if (old_locale)
+ {
+ setlocale(LC_ALL, old_locale);
+ free(old_locale);
+ }
+ if (old_modifiers)
+ {
+ XSetLocaleModifiers(old_modifiers);
+ free(old_modifiers);
+ }
+
+ m_xic = NULL;
+ if (m_xim)
+ {
+ m_xic = XCreateIC(m_xim,
+ XNClientWindow, m_window,
+ XNFocusWindow, m_window,
+ XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
+ XNResourceName, res_name,
+ XNResourceClass, res_name,
+ nullptr);
+ }
+
+ if (!m_xic)
+ CLog::Log(LOGWARNING,"CWinEventsX11::Init - no input method found");
+
+ // build Keysym lookup table
+ for (const auto& symMapping : SymMappingsX11)
+ {
+ m_symLookupTable[symMapping[0]] = symMapping[1];
+ }
+
+ // register for xrandr events
+ int iReturn;
+ XRRQueryExtension(m_display, &m_RREventBase, &iReturn);
+ int numScreens = XScreenCount(m_display);
+ for (int i = 0; i < numScreens; i++)
+ {
+ XRRSelectInput(m_display, RootWindow(m_display, i), RRScreenChangeNotifyMask | RRCrtcChangeNotifyMask | RROutputChangeNotifyMask | RROutputPropertyNotifyMask);
+ }
+
+ return true;
+}
+
+void CWinEventsX11::Quit()
+{
+ free(m_keybuf);
+ m_keybuf = nullptr;
+
+ if (m_xic)
+ {
+ XUnsetICFocus(m_xic);
+ XDestroyIC(m_xic);
+ m_xic = nullptr;
+ }
+
+ if (m_xim)
+ {
+ XCloseIM(m_xim);
+ m_xim = nullptr;
+ }
+
+ m_symLookupTable.clear();
+
+ m_display = nullptr;
+}
+
+bool CWinEventsX11::HasStructureChanged()
+{
+ if (!m_display)
+ return false;
+
+ bool ret = m_structureChanged;
+ m_structureChanged = false;
+ return ret;
+}
+
+void CWinEventsX11::SetXRRFailSafeTimer(std::chrono::milliseconds duration)
+{
+ if (!m_display)
+ return;
+
+ m_xrrFailSafeTimer.Set(duration);
+ m_xrrEventPending = true;
+}
+
+bool CWinEventsX11::MessagePump()
+{
+ if (!m_display)
+ return false;
+
+ bool ret = false;
+ XEvent xevent;
+ unsigned long serial = 0;
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+
+ while (m_display && XPending(m_display))
+ {
+ memset(&xevent, 0, sizeof (XEvent));
+ XNextEvent(m_display, &xevent);
+
+ if (m_display && (xevent.type == m_RREventBase + RRScreenChangeNotify))
+ {
+ if (xevent.xgeneric.serial == serial)
+ continue;
+
+ if (m_xrrEventPending)
+ {
+ m_winSystem.NotifyXRREvent();
+ m_xrrEventPending = false;
+ serial = xevent.xgeneric.serial;
+ }
+
+ continue;
+ }
+ else if (m_display && (xevent.type == m_RREventBase + RRNotify))
+ {
+ if (xevent.xgeneric.serial == serial)
+ continue;
+
+ XRRNotifyEvent* rrEvent = reinterpret_cast<XRRNotifyEvent*>(&xevent);
+ if (rrEvent->subtype == RRNotify_OutputChange)
+ {
+ XRROutputChangeNotifyEvent* changeEvent = reinterpret_cast<XRROutputChangeNotifyEvent*>(&xevent);
+ if (changeEvent->connection == RR_Connected ||
+ changeEvent->connection == RR_Disconnected)
+ {
+ m_winSystem.NotifyXRREvent();
+ CServiceBroker::GetActiveAE()->DeviceChange();
+ serial = xevent.xgeneric.serial;
+ }
+ }
+
+ continue;
+ }
+
+ if (XFilterEvent(&xevent, m_window))
+ continue;
+
+ switch (xevent.type)
+ {
+ case MapNotify:
+ {
+ if (appPort)
+ appPort->SetRenderGUI(true);
+ break;
+ }
+
+ case UnmapNotify:
+ {
+ if (appPort)
+ appPort->SetRenderGUI(false);
+ break;
+ }
+
+ case FocusIn:
+ {
+ if (m_xic)
+ XSetICFocus(m_xic);
+ g_application.m_AppFocused = true;
+ m_keymodState = 0;
+ if (serial == xevent.xfocus.serial)
+ break;
+ m_winSystem.NotifyAppFocusChange(g_application.m_AppFocused);
+ break;
+ }
+
+ case FocusOut:
+ {
+ if (m_xic)
+ XUnsetICFocus(m_xic);
+ g_application.m_AppFocused = false;
+ m_winSystem.NotifyAppFocusChange(g_application.m_AppFocused);
+ serial = xevent.xfocus.serial;
+ break;
+ }
+
+ case Expose:
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().MarkDirty();
+ break;
+ }
+
+ case ConfigureNotify:
+ {
+ if (xevent.xconfigure.window != m_window)
+ break;
+
+ m_structureChanged = true;
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_VIDEORESIZE;
+ newEvent.resize.w = xevent.xconfigure.width;
+ newEvent.resize.h = xevent.xconfigure.height;
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ CServiceBroker::GetGUI()->GetWindowManager().MarkDirty();
+ break;
+ }
+
+ case ClientMessage:
+ {
+ if ((unsigned int)xevent.xclient.data.l[0] == m_wmDeleteMessage)
+ if (!g_application.m_bStop)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+ break;
+ }
+
+ case KeyPress:
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_KEYDOWN;
+ KeySym xkeysym;
+
+ // fallback if we have no IM
+ if (!m_xic)
+ {
+ static XComposeStatus state;
+ char keybuf[32];
+ XLookupString(&xevent.xkey, NULL, 0, &xkeysym, NULL);
+ newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym);
+ newEvent.key.keysym.scancode = xevent.xkey.keycode;
+ if (XLookupString(&xevent.xkey, keybuf, sizeof(keybuf), NULL, &state))
+ {
+ newEvent.key.keysym.unicode = keybuf[0];
+ }
+ ret |= ProcessKey(newEvent);
+ break;
+ }
+
+ Status status;
+ int len;
+ len = Xutf8LookupString(m_xic, &xevent.xkey,
+ m_keybuf, m_keybuf_len,
+ &xkeysym, &status);
+ if (status == XBufferOverflow)
+ {
+ m_keybuf_len = len;
+ m_keybuf = (char*)realloc(m_keybuf, m_keybuf_len);
+ if (m_keybuf == nullptr)
+ throw std::runtime_error("Failed to realloc memory, insufficient memory available");
+ len = Xutf8LookupString(m_xic, &xevent.xkey,
+ m_keybuf, m_keybuf_len,
+ &xkeysym, &status);
+ }
+ switch (status)
+ {
+ case XLookupNone:
+ break;
+ case XLookupChars:
+ case XLookupBoth:
+ {
+ std::string data(m_keybuf, len);
+ std::wstring keys;
+ g_charsetConverter.utf8ToW(data, keys, false);
+
+ if (keys.length() == 0)
+ {
+ break;
+ }
+
+ for (unsigned int i = 0; i < keys.length() - 1; i++)
+ {
+ newEvent.key.keysym.sym = XBMCK_UNKNOWN;
+ newEvent.key.keysym.unicode = keys[i];
+ ret |= ProcessKey(newEvent);
+ }
+ if (keys.length() > 0)
+ {
+ newEvent.key.keysym.scancode = xevent.xkey.keycode;
+ XLookupString(&xevent.xkey, NULL, 0, &xkeysym, NULL);
+ newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym);
+ newEvent.key.keysym.unicode = keys[keys.length() - 1];
+
+ ret |= ProcessKey(newEvent);
+ }
+ break;
+ }
+
+ case XLookupKeySym:
+ {
+ newEvent.key.keysym.scancode = xevent.xkey.keycode;
+ newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym);
+ ret |= ProcessKey(newEvent);
+ break;
+ }
+
+ }// switch status
+ break;
+ } //KeyPress
+
+ case KeyRelease:
+ {
+ // if we have a queued press directly after, this is a repeat
+ if (XEventsQueued(m_display, QueuedAfterReading))
+ {
+ XEvent next_event;
+ XPeekEvent(m_display, &next_event);
+ if (next_event.type == KeyPress &&
+ next_event.xkey.window == xevent.xkey.window &&
+ next_event.xkey.keycode == xevent.xkey.keycode &&
+ (next_event.xkey.time - xevent.xkey.time < 2))
+ continue;
+ }
+
+ XBMC_Event newEvent = {};
+ KeySym xkeysym;
+ newEvent.type = XBMC_KEYUP;
+ xkeysym = XLookupKeysym(&xevent.xkey, 0);
+ newEvent.key.keysym.scancode = xevent.xkey.keycode;
+ newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym);
+ ret |= ProcessKey(newEvent);
+ break;
+ }
+
+ case EnterNotify:
+ {
+ break;
+ }
+
+ // lose mouse coverage
+ case LeaveNotify:
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ break;
+ }
+
+ case MotionNotify:
+ {
+ if (xevent.xmotion.window != m_window)
+ break;
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEMOTION;
+ newEvent.motion.x = (int16_t)xevent.xmotion.x;
+ newEvent.motion.y = (int16_t)xevent.xmotion.y;
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+
+ case ButtonPress:
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEBUTTONDOWN;
+ newEvent.button.button = (unsigned char)xevent.xbutton.button;
+ newEvent.button.x = (int16_t)xevent.xbutton.x;
+ newEvent.button.y = (int16_t)xevent.xbutton.y;
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+
+ case ButtonRelease:
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEBUTTONUP;
+ newEvent.button.button = (unsigned char)xevent.xbutton.button;
+ newEvent.button.x = (int16_t)xevent.xbutton.x;
+ newEvent.button.y = (int16_t)xevent.xbutton.y;
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+
+ default:
+ {
+ break;
+ }
+ }// switch event.type
+ }// while
+
+ if (m_display && m_xrrEventPending && m_xrrFailSafeTimer.IsTimePast())
+ {
+ CLog::Log(LOGERROR,"CWinEventsX11::MessagePump - missed XRR Events");
+ m_winSystem.NotifyXRREvent();
+ m_xrrEventPending = false;
+ }
+
+ return ret;
+}
+
+bool CWinEventsX11::ProcessKey(XBMC_Event &event)
+{
+ if (event.type == XBMC_KEYDOWN)
+ {
+ // check key modifiers
+ switch(event.key.keysym.sym)
+ {
+ case XBMCK_LSHIFT:
+ m_keymodState |= XBMCKMOD_LSHIFT;
+ break;
+ case XBMCK_RSHIFT:
+ m_keymodState |= XBMCKMOD_RSHIFT;
+ break;
+ case XBMCK_LCTRL:
+ m_keymodState |= XBMCKMOD_LCTRL;
+ break;
+ case XBMCK_RCTRL:
+ m_keymodState |= XBMCKMOD_RCTRL;
+ break;
+ case XBMCK_LALT:
+ m_keymodState |= XBMCKMOD_LALT;
+ break;
+ case XBMCK_RALT:
+ m_keymodState |= XBMCKMOD_RCTRL;
+ break;
+ case XBMCK_LMETA:
+ m_keymodState |= XBMCKMOD_LMETA;
+ break;
+ case XBMCK_RMETA:
+ m_keymodState |= XBMCKMOD_RMETA;
+ break;
+ case XBMCK_MODE:
+ m_keymodState |= XBMCKMOD_MODE;
+ break;
+ default:
+ break;
+ }
+ event.key.keysym.mod = (XBMCMod)m_keymodState;
+ }
+ else if (event.type == XBMC_KEYUP)
+ {
+ switch(event.key.keysym.sym)
+ {
+ case XBMCK_LSHIFT:
+ m_keymodState &= ~XBMCKMOD_LSHIFT;
+ break;
+ case XBMCK_RSHIFT:
+ m_keymodState &= ~XBMCKMOD_RSHIFT;
+ break;
+ case XBMCK_LCTRL:
+ m_keymodState &= ~XBMCKMOD_LCTRL;
+ break;
+ case XBMCK_RCTRL:
+ m_keymodState &= ~XBMCKMOD_RCTRL;
+ break;
+ case XBMCK_LALT:
+ m_keymodState &= ~XBMCKMOD_LALT;
+ break;
+ case XBMCK_RALT:
+ m_keymodState &= ~XBMCKMOD_RCTRL;
+ break;
+ case XBMCK_LMETA:
+ m_keymodState &= ~XBMCKMOD_LMETA;
+ break;
+ case XBMCK_RMETA:
+ m_keymodState &= ~XBMCKMOD_RMETA;
+ break;
+ case XBMCK_MODE:
+ m_keymodState &= ~XBMCKMOD_MODE;
+ break;
+ default:
+ break;
+ }
+ event.key.keysym.mod = (XBMCMod)m_keymodState;
+ }
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(event);
+ return true;
+}
+
+XBMCKey CWinEventsX11::LookupXbmcKeySym(KeySym keysym)
+{
+ // try direct mapping first
+ std::map<uint32_t, uint32_t>::iterator it;
+ it = m_symLookupTable.find(keysym);
+ if (it != m_symLookupTable.end())
+ {
+ return (XBMCKey)(it->second);
+ }
+
+ // try ascii mappings
+ if (keysym>>8 == 0x00)
+ return (XBMCKey)tolower(keysym & 0xFF);
+
+ return (XBMCKey)keysym;
+}
diff --git a/xbmc/windowing/X11/WinEventsX11.h b/xbmc/windowing/X11/WinEventsX11.h
new file mode 100644
index 0000000..b2111da
--- /dev/null
+++ b/xbmc/windowing/X11/WinEventsX11.h
@@ -0,0 +1,60 @@
+/*
+ * 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 "threads/SystemClock.h"
+#include "windowing/WinEvents.h"
+#include "windowing/X11/WinSystemX11.h"
+
+#include <clocale>
+#include <map>
+
+#include <X11/Xlib.h>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+class CWinEventsX11 : public IWinEvents
+{
+public:
+ CWinEventsX11(CWinSystemX11& winSystem);
+ ~CWinEventsX11() override;
+ bool MessagePump() override;
+ bool Init(Display *dpy, Window win);
+ void Quit();
+ bool HasStructureChanged();
+ void PendingResize(int width, int height);
+ void SetXRRFailSafeTimer(std::chrono::milliseconds duration);
+
+protected:
+ XBMCKey LookupXbmcKeySym(KeySym keysym);
+ bool ProcessKey(XBMC_Event &event);
+ Display *m_display = nullptr;
+ Window m_window = 0;
+ Atom m_wmDeleteMessage;
+ char *m_keybuf = nullptr;
+ size_t m_keybuf_len = 0;
+ XIM m_xim = nullptr;
+ XIC m_xic = nullptr;
+ std::map<uint32_t,uint32_t> m_symLookupTable;
+ int m_keymodState;
+ bool m_structureChanged;
+ int m_RREventBase;
+ XbmcThreads::EndTime<> m_xrrFailSafeTimer;
+ bool m_xrrEventPending;
+ CWinSystemX11& m_winSystem;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/X11/WinSystemX11.cpp b/xbmc/windowing/X11/WinSystemX11.cpp
new file mode 100644
index 0000000..43a8ebb
--- /dev/null
+++ b/xbmc/windowing/X11/WinSystemX11.cpp
@@ -0,0 +1,1081 @@
+/*
+ * 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 "WinSystemX11.h"
+
+#include "CompileInfo.h"
+#include "OSScreenSaverX11.h"
+#include "ServiceBroker.h"
+#include "WinEventsX11.h"
+#include "XRandR.h"
+#include "guilib/DispResource.h"
+#include "guilib/Texture.h"
+#include "input/InputManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <mutex>
+#include <string>
+#include <vector>
+
+#include <X11/Xatom.h>
+#include <X11/extensions/Xrandr.h>
+
+using namespace KODI::WINDOWING::X11;
+
+using namespace std::chrono_literals;
+
+#define EGL_NO_CONFIG (EGLConfig)0
+
+CWinSystemX11::CWinSystemX11() : CWinSystemBase()
+{
+ m_dpy = NULL;
+ m_bWasFullScreenBeforeMinimize = false;
+ m_minimized = false;
+ m_bIgnoreNextFocusMessage = false;
+ m_bIsInternalXrr = false;
+ m_delayDispReset = false;
+
+ XSetErrorHandler(XErrorHandler);
+
+ m_winEventsX11 = new CWinEventsX11(*this);
+ m_winEvents.reset(m_winEventsX11);
+}
+
+CWinSystemX11::~CWinSystemX11() = default;
+
+bool CWinSystemX11::InitWindowSystem()
+{
+ const char* env = getenv("DISPLAY");
+ if (!env)
+ {
+ CLog::Log(LOGDEBUG, "CWinSystemX11::{} - DISPLAY env not set", __FUNCTION__);
+ return false;
+ }
+
+ if ((m_dpy = XOpenDisplay(NULL)))
+ {
+ bool ret = CWinSystemBase::InitWindowSystem();
+
+ CServiceBroker::GetSettingsComponent()
+ ->GetSettings()
+ ->GetSetting(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE)
+ ->SetVisible(true);
+
+ return ret;
+ }
+ else
+ CLog::Log(LOGERROR, "X11 Error: No Display found");
+
+ return false;
+}
+
+bool CWinSystemX11::DestroyWindowSystem()
+{
+ //restore desktop resolution on exit
+ if (m_bFullScreen)
+ {
+ XOutput out;
+ XMode mode;
+ out.name = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).strOutput;
+ mode.w = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iWidth;
+ mode.h = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iHeight;
+ mode.hz = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).fRefreshRate;
+ mode.id = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).strId;
+ g_xrandr.SetMode(out, mode);
+ }
+
+ return true;
+}
+
+bool CWinSystemX11::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res)
+{
+ if(!SetFullScreen(fullScreen, res, false))
+ return false;
+
+ m_bWindowCreated = true;
+ return true;
+}
+
+bool CWinSystemX11::DestroyWindow()
+{
+ if (!m_mainWindow)
+ return true;
+
+ if (m_invisibleCursor)
+ {
+ XUndefineCursor(m_dpy, m_mainWindow);
+ XFreeCursor(m_dpy, m_invisibleCursor);
+ m_invisibleCursor = 0;
+ }
+
+ m_winEventsX11->Quit();
+
+ XUnmapWindow(m_dpy, m_mainWindow);
+ XDestroyWindow(m_dpy, m_glWindow);
+ XDestroyWindow(m_dpy, m_mainWindow);
+ m_glWindow = 0;
+ m_mainWindow = 0;
+
+ if (m_icon)
+ XFreePixmap(m_dpy, m_icon);
+
+ return true;
+}
+
+bool CWinSystemX11::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ m_userOutput = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR);
+ XOutput *out = NULL;
+ if (m_userOutput.compare("Default") != 0)
+ {
+ out = g_xrandr.GetOutput(m_userOutput);
+ if (out)
+ {
+ XMode mode = g_xrandr.GetCurrentMode(m_userOutput);
+ if (!mode.isCurrent)
+ {
+ out = NULL;
+ }
+ }
+ }
+ if (!out)
+ {
+ std::vector<XOutput> outputs = g_xrandr.GetModes();
+ if (!outputs.empty())
+ {
+ m_userOutput = outputs[0].name;
+ }
+ }
+
+ if (!SetWindow(newWidth, newHeight, false, m_userOutput))
+ {
+ return false;
+ }
+
+ m_nWidth = newWidth;
+ m_nHeight = newHeight;
+ m_bFullScreen = false;
+ m_currentOutput = m_userOutput;
+
+ return true;
+}
+
+void CWinSystemX11::FinishWindowResize(int newWidth, int newHeight)
+{
+ m_userOutput = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR);
+ XOutput *out = NULL;
+ if (m_userOutput.compare("Default") != 0)
+ {
+ out = g_xrandr.GetOutput(m_userOutput);
+ if (out)
+ {
+ XMode mode = g_xrandr.GetCurrentMode(m_userOutput);
+ if (!mode.isCurrent)
+ {
+ out = NULL;
+ }
+ }
+ }
+ if (!out)
+ {
+ std::vector<XOutput> outputs = g_xrandr.GetModes();
+ if (!outputs.empty())
+ {
+ m_userOutput = outputs[0].name;
+ }
+ }
+
+ XResizeWindow(m_dpy, m_glWindow, newWidth, newHeight);
+ UpdateCrtc();
+
+ if (m_userOutput.compare(m_currentOutput) != 0)
+ {
+ SetWindow(newWidth, newHeight, false, m_userOutput);
+ }
+
+ m_nWidth = newWidth;
+ m_nHeight = newHeight;
+}
+
+bool CWinSystemX11::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ XOutput out;
+ XMode mode;
+
+ if (fullScreen)
+ {
+ out.name = res.strOutput;
+ mode.w = res.iWidth;
+ mode.h = res.iHeight;
+ mode.hz = res.fRefreshRate;
+ mode.id = res.strId;
+ }
+ else
+ {
+ out.name = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).strOutput;
+ mode.w = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iWidth;
+ mode.h = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iHeight;
+ mode.hz = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).fRefreshRate;
+ mode.id = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).strId;
+ }
+
+ XMode currmode = g_xrandr.GetCurrentMode(out.name);
+ if (!currmode.name.empty())
+ {
+ // flip h/w when rotated
+ if (m_bIsRotated)
+ {
+ int w = mode.w;
+ mode.w = mode.h;
+ mode.h = w;
+ }
+
+ // only call xrandr if mode changes
+ if (m_mainWindow)
+ {
+ if (currmode.w != mode.w || currmode.h != mode.h ||
+ currmode.hz != mode.hz || currmode.id != mode.id)
+ {
+ CLog::Log(LOGINFO, "CWinSystemX11::SetFullScreen - calling xrandr");
+
+ // remember last position of mouse
+ Window root_return, child_return;
+ int root_x_return, root_y_return;
+ int win_x_return, win_y_return;
+ unsigned int mask_return;
+ bool isInWin = XQueryPointer(m_dpy, m_mainWindow, &root_return, &child_return,
+ &root_x_return, &root_y_return,
+ &win_x_return, &win_y_return,
+ &mask_return);
+
+ if (isInWin)
+ {
+ m_MouseX = win_x_return;
+ m_MouseY = win_y_return;
+ }
+ else
+ {
+ m_MouseX = -1;
+ m_MouseY = -1;
+ }
+
+ OnLostDevice();
+ m_bIsInternalXrr = true;
+ g_xrandr.SetMode(out, mode);
+ auto delay =
+ std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ "videoscreen.delayrefreshchange") *
+ 100);
+ if (delay > 0ms)
+ {
+ m_delayDispReset = true;
+ m_dispResetTimer.Set(delay);
+ }
+ return true;
+ }
+ }
+ }
+
+ if (!SetWindow(res.iWidth, res.iHeight, fullScreen, m_userOutput))
+ return false;
+
+ m_nWidth = res.iWidth;
+ m_nHeight = res.iHeight;
+ m_bFullScreen = fullScreen;
+ m_currentOutput = m_userOutput;
+
+ return true;
+}
+
+void CWinSystemX11::UpdateResolutions()
+{
+ CWinSystemBase::UpdateResolutions();
+
+ int numScreens = XScreenCount(m_dpy);
+ g_xrandr.SetNumScreens(numScreens);
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ bool switchOnOff = settings->GetBool(CSettings::SETTING_VIDEOSCREEN_BLANKDISPLAYS);
+ m_userOutput = settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR);
+ if (m_userOutput.compare("Default") == 0)
+ switchOnOff = false;
+
+ if(g_xrandr.Query(true, !switchOnOff))
+ {
+ XOutput *out = NULL;
+ if (m_userOutput.compare("Default") != 0)
+ {
+ out = g_xrandr.GetOutput(m_userOutput);
+ if (out)
+ {
+ XMode mode = g_xrandr.GetCurrentMode(m_userOutput);
+ if (!mode.isCurrent && !switchOnOff)
+ {
+ out = NULL;
+ }
+ }
+ }
+ if (!out)
+ {
+ m_userOutput = g_xrandr.GetModes()[0].name;
+ out = g_xrandr.GetOutput(m_userOutput);
+ }
+
+ if (switchOnOff)
+ {
+ // switch on output
+ g_xrandr.TurnOnOutput(m_userOutput);
+
+ // switch off other outputs
+ std::vector<XOutput> outputs = g_xrandr.GetModes();
+ for (size_t i=0; i<outputs.size(); i++)
+ {
+ if (StringUtils::EqualsNoCase(outputs[i].name, m_userOutput))
+ continue;
+ g_xrandr.TurnOffOutput(outputs[i].name);
+ }
+ }
+
+ XMode mode = g_xrandr.GetCurrentMode(m_userOutput);
+ if (mode.id.empty())
+ mode = g_xrandr.GetPreferredMode(m_userOutput);
+ m_bIsRotated = out->isRotated;
+ if (!m_bIsRotated)
+ UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), out->name, mode.w, mode.h, mode.hz, 0);
+ else
+ UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), out->name, mode.h, mode.w, mode.hz, 0);
+ CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).strId = mode.id;
+ }
+ else
+ {
+ m_userOutput = "No Output";
+ m_screen = DefaultScreen(m_dpy);
+ int w = DisplayWidth(m_dpy, m_screen);
+ int h = DisplayHeight(m_dpy, m_screen);
+ UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), m_userOutput, w, h, 0.0, 0);
+ }
+
+ // erase previous stored modes
+ CDisplaySettings::GetInstance().ClearCustomResolutions();
+
+ CLog::Log(LOGINFO, "Available videomodes (xrandr):");
+
+ XOutput *out = g_xrandr.GetOutput(m_userOutput);
+ if (out != NULL)
+ {
+ CLog::Log(LOGINFO, "Output '{}' has {} modes", out->name, out->modes.size());
+
+ for (auto mode : out->modes)
+ {
+ CLog::Log(LOGINFO, "ID:{} Name:{} Refresh:{:f} Width:{} Height:{}", mode.id, mode.name,
+ mode.hz, mode.w, mode.h);
+ RESOLUTION_INFO res;
+ res.dwFlags = 0;
+
+ if (mode.IsInterlaced())
+ res.dwFlags |= D3DPRESENTFLAG_INTERLACED;
+
+ if (!m_bIsRotated)
+ {
+ res.iWidth = mode.w;
+ res.iHeight = mode.h;
+ res.iScreenWidth = mode.w;
+ res.iScreenHeight = mode.h;
+ }
+ else
+ {
+ res.iWidth = mode.h;
+ res.iHeight = mode.w;
+ res.iScreenWidth = mode.h;
+ res.iScreenHeight = mode.w;
+ }
+
+ if (mode.h > 0 && mode.w > 0 && out->hmm > 0 && out->wmm > 0)
+ res.fPixelRatio = ((float)out->wmm/(float)mode.w) / (((float)out->hmm/(float)mode.h));
+ else
+ res.fPixelRatio = 1.0f;
+
+ CLog::Log(LOGINFO, "Pixel Ratio: {:f}", res.fPixelRatio);
+
+ res.strMode = StringUtils::Format("{}: {} @ {:.2f}Hz", out->name, mode.name, mode.hz);
+ res.strOutput = out->name;
+ res.strId = mode.id;
+ res.iSubtitles = mode.h;
+ res.fRefreshRate = mode.hz;
+ res.bFullScreen = true;
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res);
+ CDisplaySettings::GetInstance().AddResolutionInfo(res);
+ }
+ }
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+}
+
+bool CWinSystemX11::HasCalibration(const RESOLUTION_INFO &resInfo)
+{
+ XOutput *out = g_xrandr.GetOutput(m_currentOutput);
+
+ // keep calibrations done on a not connected output
+ if (!StringUtils::EqualsNoCase(out->name, resInfo.strOutput))
+ return true;
+
+ // keep calibrations not updated with resolution data
+ if (resInfo.iWidth == 0)
+ return true;
+
+ float fPixRatio;
+ if (resInfo.iHeight>0 && resInfo.iWidth>0 && out->hmm>0 && out->wmm>0)
+ fPixRatio = ((float)out->wmm/(float)resInfo.iWidth) / (((float)out->hmm/(float)resInfo.iHeight));
+ else
+ fPixRatio = 1.0f;
+
+ if (resInfo.Overscan.left != 0)
+ return true;
+ if (resInfo.Overscan.top != 0)
+ return true;
+ if (resInfo.Overscan.right != resInfo.iWidth)
+ return true;
+ if (resInfo.Overscan.bottom != resInfo.iHeight)
+ return true;
+ if (resInfo.fPixelRatio != fPixRatio)
+ return true;
+ if (resInfo.iSubtitles != resInfo.iHeight)
+ return true;
+
+ return false;
+}
+
+bool CWinSystemX11::UseLimitedColor()
+{
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE);
+}
+
+std::vector<std::string> CWinSystemX11::GetConnectedOutputs()
+{
+ std::vector<std::string> outputs;
+ std::vector<XOutput> outs;
+ g_xrandr.Query(true);
+ outs = g_xrandr.GetModes();
+ outputs.emplace_back("Default");
+ for(unsigned int i=0; i<outs.size(); ++i)
+ {
+ outputs.emplace_back(outs[i].name);
+ }
+
+ return outputs;
+}
+
+bool CWinSystemX11::IsCurrentOutput(const std::string& output)
+{
+ return (StringUtils::EqualsNoCase(output, "Default")) || (m_currentOutput.compare(output.c_str()) == 0);
+}
+
+void CWinSystemX11::ShowOSMouse(bool show)
+{
+ if (show)
+ XUndefineCursor(m_dpy,m_mainWindow);
+ else if (m_invisibleCursor)
+ XDefineCursor(m_dpy,m_mainWindow, m_invisibleCursor);
+}
+
+std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> CWinSystemX11::GetOSScreenSaverImpl()
+{
+ std::unique_ptr<IOSScreenSaver> ret;
+ if (m_dpy)
+ {
+ ret.reset(new COSScreenSaverX11(m_dpy));
+ }
+ return ret;
+}
+
+void CWinSystemX11::NotifyAppActiveChange(bool bActivated)
+{
+ if (bActivated && m_bWasFullScreenBeforeMinimize && !m_bFullScreen)
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN);
+
+ m_bWasFullScreenBeforeMinimize = false;
+ }
+ m_minimized = !bActivated;
+}
+
+void CWinSystemX11::NotifyAppFocusChange(bool bGaining)
+{
+ if (bGaining && m_bWasFullScreenBeforeMinimize && !m_bIgnoreNextFocusMessage &&
+ !m_bFullScreen)
+ {
+ m_bWasFullScreenBeforeMinimize = false;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN);
+ m_minimized = false;
+ }
+ if (!bGaining)
+ m_bIgnoreNextFocusMessage = false;
+}
+
+bool CWinSystemX11::Minimize()
+{
+ m_bWasFullScreenBeforeMinimize = m_bFullScreen;
+ if (m_bWasFullScreenBeforeMinimize)
+ {
+ m_bIgnoreNextFocusMessage = true;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN);
+ }
+
+ XIconifyWindow(m_dpy, m_mainWindow, m_screen);
+
+ m_minimized = true;
+ return true;
+}
+bool CWinSystemX11::Restore()
+{
+ return false;
+}
+bool CWinSystemX11::Hide()
+{
+ XUnmapWindow(m_dpy, m_mainWindow);
+ XFlush(m_dpy);
+ return true;
+}
+bool CWinSystemX11::Show(bool raise)
+{
+ XMapWindow(m_dpy, m_mainWindow);
+ XFlush(m_dpy);
+ m_minimized = false;
+ return true;
+}
+
+void CWinSystemX11::NotifyXRREvent()
+{
+ CLog::Log(LOGDEBUG, "{} - notify display reset event", __FUNCTION__);
+
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ if (!g_xrandr.Query(true))
+ {
+ CLog::Log(LOGERROR, "WinSystemX11::RefreshWindow - failed to query xrandr");
+ return;
+ }
+
+ // if external event update resolutions
+ if (!m_bIsInternalXrr)
+ {
+ UpdateResolutions();
+ }
+
+ RecreateWindow();
+}
+
+void CWinSystemX11::RecreateWindow()
+{
+ m_windowDirty = true;
+
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+
+ XOutput *out = g_xrandr.GetOutput(m_userOutput);
+ XMode mode = g_xrandr.GetCurrentMode(m_userOutput);
+
+ if (out)
+ CLog::Log(LOGDEBUG, "{} - current output: {}, mode: {}, refresh: {:.3f}", __FUNCTION__,
+ out->name, mode.id, mode.hz);
+ else
+ CLog::Log(LOGWARNING, "{} - output name not set", __FUNCTION__);
+
+ RESOLUTION_INFO res;
+ unsigned int i;
+ bool found(false);
+ for (i = RES_DESKTOP; i < CDisplaySettings::GetInstance().ResolutionInfoSize(); ++i)
+ {
+ res = CDisplaySettings::GetInstance().GetResolutionInfo(i);
+ if (StringUtils::EqualsNoCase(CDisplaySettings::GetInstance().GetResolutionInfo(i).strId, mode.id))
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ CLog::Log(LOGERROR, "CWinSystemX11::RecreateWindow - could not find resolution");
+ i = RES_DESKTOP;
+ }
+
+ if (CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot())
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution((RESOLUTION)i, true);
+ else
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(RES_WINDOW, true);
+}
+
+void CWinSystemX11::OnLostDevice()
+{
+ CLog::Log(LOGDEBUG, "{} - notify display change event", __FUNCTION__);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnLostDisplay();
+ }
+
+ m_winEventsX11->SetXRRFailSafeTimer(3s);
+}
+
+void CWinSystemX11::Register(IDispResource *resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ m_resources.push_back(resource);
+}
+
+void CWinSystemX11::Unregister(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> 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);
+}
+
+int CWinSystemX11::XErrorHandler(Display* dpy, XErrorEvent* error)
+{
+ char buf[1024];
+ XGetErrorText(error->display, error->error_code, buf, sizeof(buf));
+ CLog::Log(LOGERROR,
+ "CWinSystemX11::XErrorHandler: {}, type:{}, serial:{}, error_code:{}, request_code:{} "
+ "minor_code:{}",
+ buf, error->type, error->serial, (int)error->error_code, (int)error->request_code,
+ (int)error->minor_code);
+
+ return 0;
+}
+
+bool CWinSystemX11::SetWindow(int width, int height, bool fullscreen, const std::string &output, int *winstate)
+{
+ bool changeWindow = false;
+ bool changeSize = false;
+ float mouseX = 0.5;
+ float mouseY = 0.5;
+
+ if (!m_mainWindow)
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ }
+
+ if (m_mainWindow && ((m_bFullScreen != fullscreen) || m_currentOutput.compare(output) != 0 || m_windowDirty))
+ {
+ // set mouse to last known position
+ // we can't trust values after an xrr event
+ if (m_bIsInternalXrr && m_MouseX >= 0 && m_MouseY >= 0)
+ {
+ mouseX = (float)m_MouseX/m_nWidth;
+ mouseY = (float)m_MouseY/m_nHeight;
+ }
+ else if (!m_windowDirty)
+ {
+ Window root_return, child_return;
+ int root_x_return, root_y_return;
+ int win_x_return, win_y_return;
+ unsigned int mask_return;
+ bool isInWin = XQueryPointer(m_dpy, m_mainWindow, &root_return, &child_return,
+ &root_x_return, &root_y_return,
+ &win_x_return, &win_y_return,
+ &mask_return);
+
+ if (isInWin)
+ {
+ mouseX = (float)win_x_return/m_nWidth;
+ mouseY = (float)win_y_return/m_nHeight;
+ }
+ }
+
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ OnLostDevice();
+ DestroyWindow();
+ m_windowDirty = true;
+ }
+
+ // create main window
+ if (!m_mainWindow)
+ {
+ Colormap cmap;
+ XSetWindowAttributes swa;
+ XVisualInfo *vi;
+ int x0 = 0;
+ int y0 = 0;
+
+ XOutput *out = g_xrandr.GetOutput(output);
+ if (!out)
+ out = g_xrandr.GetOutput(m_currentOutput);
+ if (out)
+ {
+ m_screen = out->screen;
+ x0 = out->x;
+ y0 = out->y;
+ }
+
+ vi = GetVisual();
+ if (!vi)
+ {
+ CLog::Log(LOGERROR, "Failed to find matching visual");
+ return false;
+ }
+
+ cmap = XCreateColormap(m_dpy, RootWindow(m_dpy, vi->screen), vi->visual, AllocNone);
+
+ bool hasWM = HasWindowManager();
+
+ int def_vis = (vi->visual == DefaultVisual(m_dpy, vi->screen));
+ swa.override_redirect = hasWM ? False : True;
+ swa.border_pixel = fullscreen ? 0 : 5;
+ swa.background_pixel = def_vis ? BlackPixel(m_dpy, vi->screen) : 0;
+ swa.colormap = cmap;
+ swa.event_mask = FocusChangeMask | KeyPressMask | KeyReleaseMask |
+ ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
+ PropertyChangeMask | StructureNotifyMask | KeymapStateMask |
+ EnterWindowMask | LeaveWindowMask | ExposureMask;
+ unsigned long mask = CWBackPixel | CWBorderPixel | CWColormap | CWOverrideRedirect | CWEventMask;
+
+ m_mainWindow = XCreateWindow(m_dpy, RootWindow(m_dpy, vi->screen),
+ x0, y0, width, height, 0, vi->depth,
+ InputOutput, vi->visual,
+ mask, &swa);
+
+ swa.override_redirect = False;
+ swa.border_pixel = 0;
+ swa.event_mask = ExposureMask;
+ mask = CWBackPixel | CWBorderPixel | CWColormap | CWOverrideRedirect | CWColormap | CWEventMask;
+
+ m_glWindow = XCreateWindow(m_dpy, m_mainWindow,
+ 0, 0, width, height, 0, vi->depth,
+ InputOutput, vi->visual,
+ mask, &swa);
+
+ if (fullscreen && hasWM)
+ {
+ Atom fs = XInternAtom(m_dpy, "_NET_WM_STATE_FULLSCREEN", True);
+ XChangeProperty(m_dpy, m_mainWindow, XInternAtom(m_dpy, "_NET_WM_STATE", True), XA_ATOM, 32, PropModeReplace, (unsigned char *) &fs, 1);
+ // disable desktop compositing for KDE, when Kodi is in full-screen mode
+ int one = 1;
+ Atom composite = XInternAtom(m_dpy, "_KDE_NET_WM_BLOCK_COMPOSITING", True);
+ if (composite != None)
+ {
+ XChangeProperty(m_dpy, m_mainWindow, XInternAtom(m_dpy, "_KDE_NET_WM_BLOCK_COMPOSITING", True), XA_CARDINAL, 32,
+ PropModeReplace, (unsigned char*) &one, 1);
+ }
+ composite = XInternAtom(m_dpy, "_NET_WM_BYPASS_COMPOSITOR", True);
+ if (composite != None)
+ {
+ // standard way for Gnome 3
+ XChangeProperty(m_dpy, m_mainWindow, XInternAtom(m_dpy, "_NET_WM_BYPASS_COMPOSITOR", True), XA_CARDINAL, 32,
+ PropModeReplace, (unsigned char*) &one, 1);
+ }
+ }
+
+ // define invisible cursor
+ Pixmap bitmapNoData;
+ XColor black;
+ static char noData[] = { 0,0,0,0,0,0,0,0 };
+ black.red = black.green = black.blue = 0;
+
+ bitmapNoData = XCreateBitmapFromData(m_dpy, m_mainWindow, noData, 8, 8);
+ m_invisibleCursor = XCreatePixmapCursor(m_dpy, bitmapNoData, bitmapNoData,
+ &black, &black, 0, 0);
+ XFreePixmap(m_dpy, bitmapNoData);
+ XDefineCursor(m_dpy,m_mainWindow, m_invisibleCursor);
+ XFree(vi);
+
+ //init X11 events
+ m_winEventsX11->Init(m_dpy, m_mainWindow);
+
+ changeWindow = true;
+ changeSize = true;
+ }
+
+ if (!m_winEventsX11->HasStructureChanged() && ((width != m_nWidth) || (height != m_nHeight)))
+ {
+ changeSize = true;
+ }
+
+ if (changeSize || changeWindow)
+ {
+ XResizeWindow(m_dpy, m_mainWindow, width, height);
+ }
+
+ if ((width != m_nWidth) || (height != m_nHeight) || changeWindow)
+ {
+ XResizeWindow(m_dpy, m_glWindow, width, height);
+ }
+
+ if (changeWindow)
+ {
+ m_icon = None;
+ {
+ CreateIconPixmap();
+ XWMHints *wm_hints;
+ XClassHint *class_hints;
+ XTextProperty windowName, iconName;
+
+ std::string titleString = CCompileInfo::GetAppName();
+ const std::string& classString = titleString;
+ char *title = const_cast<char*>(titleString.c_str());
+
+ XStringListToTextProperty(&title, 1, &windowName);
+ XStringListToTextProperty(&title, 1, &iconName);
+
+ wm_hints = XAllocWMHints();
+ wm_hints->initial_state = NormalState;
+ wm_hints->icon_pixmap = m_icon;
+ wm_hints->flags = StateHint | IconPixmapHint;
+
+ class_hints = XAllocClassHint();
+ class_hints->res_class = const_cast<char*>(classString.c_str());
+ class_hints->res_name = const_cast<char*>(classString.c_str());
+
+ XSetWMProperties(m_dpy, m_mainWindow, &windowName, &iconName,
+ NULL, 0, NULL, wm_hints,
+ class_hints);
+ XFree(class_hints);
+ XFree(wm_hints);
+ XFree(iconName.value);
+ XFree(windowName.value);
+
+ // register interest in the delete window message
+ Atom wmDeleteMessage = XInternAtom(m_dpy, "WM_DELETE_WINDOW", False);
+ XSetWMProtocols(m_dpy, m_mainWindow, &wmDeleteMessage, 1);
+ }
+
+ // placement of window may follow mouse
+ XWarpPointer(m_dpy, None, m_mainWindow, 0, 0, 0, 0, mouseX*width, mouseY*height);
+
+ XMapRaised(m_dpy, m_glWindow);
+ XMapRaised(m_dpy, m_mainWindow);
+
+ // discard events generated by creating the window, i.e. xrr events
+ XSync(m_dpy, True);
+
+ if (winstate)
+ *winstate = 1;
+ }
+
+ UpdateCrtc();
+
+ return true;
+}
+
+bool CWinSystemX11::CreateIconPixmap()
+{
+ int depth;
+ XImage *img = NULL;
+ Visual *vis;
+ XWindowAttributes wndattribs;
+ XVisualInfo visInfo;
+ double rRatio;
+ double gRatio;
+ double bRatio;
+ int outIndex = 0;
+ unsigned int i,j;
+ unsigned char *buf;
+ uint32_t *newBuf = 0;
+ size_t numNewBufBytes;
+
+ // Get visual Info
+ XGetWindowAttributes(m_dpy, m_glWindow, &wndattribs);
+ visInfo.visualid = wndattribs.visual->visualid;
+ int nvisuals = 0;
+ XVisualInfo* visuals = XGetVisualInfo(m_dpy, VisualIDMask, &visInfo, &nvisuals);
+ if (nvisuals != 1)
+ {
+ CLog::Log(LOGERROR, "CWinSystemX11::CreateIconPixmap - could not find visual");
+ return false;
+ }
+ visInfo = visuals[0];
+ XFree(visuals);
+
+ depth = visInfo.depth;
+ vis = visInfo.visual;
+
+ if (depth < 15)
+ {
+ CLog::Log(LOGERROR, "CWinSystemX11::CreateIconPixmap - no suitable depth");
+ return false;
+ }
+
+ rRatio = vis->red_mask / 255.0;
+ gRatio = vis->green_mask / 255.0;
+ bRatio = vis->blue_mask / 255.0;
+
+ std::unique_ptr<CTexture> iconTexture =
+ CTexture::LoadFromFile("special://xbmc/media/icon256x256.png");
+
+ if (!iconTexture)
+ return false;
+
+ buf = iconTexture->GetPixels();
+
+ if (depth>=24)
+ numNewBufBytes = (4 * (iconTexture->GetWidth() * iconTexture->GetHeight()));
+ else
+ numNewBufBytes = (2 * (iconTexture->GetWidth() * iconTexture->GetHeight()));
+
+ newBuf = (uint32_t*)malloc(numNewBufBytes);
+ if (!newBuf)
+ {
+ CLog::Log(LOGERROR, "CWinSystemX11::CreateIconPixmap - malloc failed");
+ return false;
+ }
+
+ for (i=0; i<iconTexture->GetHeight();++i)
+ {
+ for (j=0; j<iconTexture->GetWidth();++j)
+ {
+ unsigned int pos = i*iconTexture->GetPitch()+j*4;
+ unsigned int r, g, b;
+ r = (buf[pos+2] * rRatio);
+ g = (buf[pos+1] * gRatio);
+ b = (buf[pos+0] * bRatio);
+ r &= vis->red_mask;
+ g &= vis->green_mask;
+ b &= vis->blue_mask;
+ newBuf[outIndex] = r | g | b;
+ ++outIndex;
+ }
+ }
+ img = XCreateImage(m_dpy, vis, depth,ZPixmap, 0, (char *)newBuf,
+ iconTexture->GetWidth(), iconTexture->GetHeight(),
+ (depth>=24)?32:16, 0);
+ if (!img)
+ {
+ CLog::Log(LOGERROR, "CWinSystemX11::CreateIconPixmap - could not create image");
+ free(newBuf);
+ return false;
+ }
+ if (!XInitImage(img))
+ {
+ CLog::Log(LOGERROR, "CWinSystemX11::CreateIconPixmap - init image failed");
+ XDestroyImage(img);
+ return false;
+ }
+
+ // set byte order
+ union
+ {
+ char c[sizeof(short)];
+ short s;
+ } order;
+ order.s = 1;
+ if ((1 == order.c[0]))
+ {
+ img->byte_order = LSBFirst;
+ }
+ else
+ {
+ img->byte_order = MSBFirst;
+ }
+
+ // create icon pixmap from image
+ m_icon = XCreatePixmap(m_dpy, m_glWindow, img->width, img->height, depth);
+ GC gc = XCreateGC(m_dpy, m_glWindow, 0, NULL);
+ XPutImage(m_dpy, m_icon, gc, img, 0, 0, 0, 0, img->width, img->height);
+ XFreeGC(m_dpy, gc);
+ XDestroyImage(img); // this also frees newBuf
+
+ return true;
+}
+
+bool CWinSystemX11::HasWindowManager()
+{
+ Window wm_check;
+ unsigned char *data;
+ int status, real_format;
+ Atom real_type, prop;
+ unsigned long items_read, items_left;
+
+ prop = XInternAtom(m_dpy, "_NET_SUPPORTING_WM_CHECK", True);
+ if (prop == None)
+ return false;
+ status = XGetWindowProperty(m_dpy, DefaultRootWindow(m_dpy), prop,
+ 0L, 1L, False, XA_WINDOW, &real_type, &real_format,
+ &items_read, &items_left, &data);
+ if(status != Success || ! items_read)
+ {
+ if(status == Success)
+ XFree(data);
+ return false;
+ }
+
+ wm_check = ((Window*)data)[0];
+ XFree(data);
+
+ status = XGetWindowProperty(m_dpy, wm_check, prop,
+ 0L, 1L, False, XA_WINDOW, &real_type, &real_format,
+ &items_read, &items_left, &data);
+
+ if(status != Success || !items_read)
+ {
+ if(status == Success)
+ XFree(data);
+ return false;
+ }
+
+ if(wm_check != ((Window*)data)[0])
+ {
+ XFree(data);
+ return false;
+ }
+
+ XFree(data);
+
+ prop = XInternAtom(m_dpy, "_NET_WM_NAME", True);
+ if (prop == None)
+ {
+ CLog::Log(LOGDEBUG,"Window Manager Name: ");
+ return true;
+ }
+
+ status = XGetWindowProperty(m_dpy, wm_check, prop,
+ 0L, (~0L), False, AnyPropertyType, &real_type, &real_format,
+ &items_read, &items_left, &data);
+
+ if(status == Success && items_read)
+ {
+ const char* s;
+
+ s = reinterpret_cast<const char*>(data);
+ CLog::Log(LOGDEBUG, "Window Manager Name: {}", s);
+ }
+ else
+ CLog::Log(LOGDEBUG,"Window Manager Name: ");
+
+ if(status == Success)
+ XFree(data);
+
+ return true;
+}
+
+void CWinSystemX11::UpdateCrtc()
+{
+ XWindowAttributes winattr;
+ int posx, posy;
+ float fps = 0.0f;
+ Window child;
+ XGetWindowAttributes(m_dpy, m_mainWindow, &winattr);
+ XTranslateCoordinates(m_dpy, m_mainWindow, RootWindow(m_dpy, m_screen), winattr.x, winattr.y,
+ &posx, &posy, &child);
+
+ m_crtc = g_xrandr.GetCrtc(posx+winattr.width/2, posy+winattr.height/2, fps);
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetFPS(fps);
+}
+
+bool CWinSystemX11::MessagePump()
+{
+ return m_winEvents->MessagePump();
+}
diff --git a/xbmc/windowing/X11/WinSystemX11.h b/xbmc/windowing/X11/WinSystemX11.h
new file mode 100644
index 0000000..f65538d
--- /dev/null
+++ b/xbmc/windowing/X11/WinSystemX11.h
@@ -0,0 +1,117 @@
+/*
+ * 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 "settings/lib/ISettingCallback.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "utils/Stopwatch.h"
+#include "windowing/WinSystem.h"
+
+#include <string>
+#include <vector>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+class IDispResource;
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+class CWinEventsX11;
+
+class CWinSystemX11 : public CWinSystemBase
+{
+public:
+ CWinSystemX11();
+ ~CWinSystemX11() override;
+
+ const std::string GetName() override { return "x11"; }
+
+ // CWinSystemBase
+ 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;
+ void FinishWindowResize(int newWidth, int newHeight) override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+ void UpdateResolutions() override;
+ void ShowOSMouse(bool show) override;
+
+ void NotifyAppActiveChange(bool bActivated) override;
+ void NotifyAppFocusChange(bool bGaining) override;
+
+ bool Minimize() override;
+ bool Restore() override;
+ bool Hide() override;
+ bool Show(bool raise = true) override;
+ void Register(IDispResource *resource) override;
+ void Unregister(IDispResource *resource) override;
+ bool HasCalibration(const RESOLUTION_INFO &resInfo) override;
+ bool UseLimitedColor() override;
+
+ std::vector<std::string> GetConnectedOutputs() override;
+
+ // Local to WinSystemX11 only
+ Display* GetDisplay() { return m_dpy; }
+ int GetScreen() { return m_screen; }
+ void NotifyXRREvent();
+ bool IsCurrentOutput(const std::string& output);
+ void RecreateWindow();
+ int GetCrtc() { return m_crtc; }
+
+ // winevents override
+ bool MessagePump() override;
+
+protected:
+ std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override;
+
+ virtual bool SetWindow(int width, int height, bool fullscreen, const std::string &output, int *winstate = NULL) = 0;
+ virtual XVisualInfo* GetVisual() = 0;
+
+ void OnLostDevice();
+
+ Window m_glWindow = 0, m_mainWindow = 0;
+ int m_screen = 0;
+ Display *m_dpy;
+ Cursor m_invisibleCursor = 0;
+ Pixmap m_icon;
+ bool m_bIsRotated;
+ bool m_bWasFullScreenBeforeMinimize;
+ bool m_minimized;
+ bool m_bIgnoreNextFocusMessage;
+ CCriticalSection m_resourceSection;
+ std::vector<IDispResource*> m_resources;
+ bool m_delayDispReset;
+ XbmcThreads::EndTime<> m_dispResetTimer;
+ std::string m_currentOutput;
+ std::string m_userOutput;
+ bool m_windowDirty;
+ bool m_bIsInternalXrr;
+ int m_MouseX, m_MouseY;
+ int m_crtc;
+ CWinEventsX11 *m_winEventsX11;
+
+private:
+ bool IsSuitableVisual(XVisualInfo *vInfo);
+ static int XErrorHandler(Display* dpy, XErrorEvent* error);
+ bool CreateIconPixmap();
+ bool HasWindowManager();
+ void UpdateCrtc();
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/X11/WinSystemX11GLContext.cpp b/xbmc/windowing/X11/WinSystemX11GLContext.cpp
new file mode 100644
index 0000000..ad27aca
--- /dev/null
+++ b/xbmc/windowing/X11/WinSystemX11GLContext.cpp
@@ -0,0 +1,358 @@
+/*
+ * 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 "WinSystemX11GLContext.h"
+
+#include "GLContextEGL.h"
+#include "OptionalsReg.h"
+#include "VideoSyncOML.h"
+#include "X11DPMSSupport.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationSkinHandling.h"
+#include "cores/RetroPlayer/process/X11/RPProcessInfoX11.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/Process/X11/ProcessInfoX11.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "guilib/DispResource.h"
+#include "rendering/gl/ScreenshotSurfaceGL.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WindowSystemFactory.h"
+
+#include <mutex>
+#include <vector>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+using namespace KODI;
+using namespace KODI::WINDOWING::X11;
+
+
+void CWinSystemX11GLContext::Register()
+{
+ KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "x11");
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemX11GLContext::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemX11GLContext>();
+}
+
+CWinSystemX11GLContext::~CWinSystemX11GLContext()
+{
+ delete m_pGLContext;
+}
+
+void CWinSystemX11GLContext::PresentRenderImpl(bool rendered)
+{
+ if (rendered)
+ m_pGLContext->SwapBuffers();
+
+ if (m_delayDispReset && m_dispResetTimer.IsTimePast())
+ {
+ m_delayDispReset = false;
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ // tell any shared resources
+ for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnResetDisplay();
+ }
+}
+
+void CWinSystemX11GLContext::SetVSyncImpl(bool enable)
+{
+ m_pGLContext->SetVSync(enable);
+}
+
+bool CWinSystemX11GLContext::IsExtSupported(const char* extension) const
+{
+ if(strncmp(extension, m_pGLContext->ExtPrefix().c_str(), 4) != 0)
+ return CRenderSystemGL::IsExtSupported(extension);
+
+ return m_pGLContext->IsExtSupported(extension);
+}
+
+XID CWinSystemX11GLContext::GetWindow() const
+{
+ return GLXGetWindow(m_pGLContext);
+}
+
+void* CWinSystemX11GLContext::GetGlxContext() const
+{
+ return GLXGetContext(m_pGLContext);
+}
+
+EGLDisplay CWinSystemX11GLContext::GetEGLDisplay() const
+{
+ return static_cast<CGLContextEGL*>(m_pGLContext)->m_eglDisplay;
+}
+
+EGLSurface CWinSystemX11GLContext::GetEGLSurface() const
+{
+ return static_cast<CGLContextEGL*>(m_pGLContext)->m_eglSurface;
+}
+
+EGLContext CWinSystemX11GLContext::GetEGLContext() const
+{
+ return static_cast<CGLContextEGL*>(m_pGLContext)->m_eglContext;
+}
+
+EGLConfig CWinSystemX11GLContext::GetEGLConfig() const
+{
+ return static_cast<CGLContextEGL*>(m_pGLContext)->m_eglConfig;
+}
+
+bool CWinSystemX11GLContext::SetWindow(int width, int height, bool fullscreen, const std::string &output, int *winstate)
+{
+ int newwin = 0;
+
+ CWinSystemX11::SetWindow(width, height, fullscreen, output, &newwin);
+ if (newwin)
+ {
+ RefreshGLContext(m_currentOutput.compare(output) != 0);
+ XSync(m_dpy, False);
+ CServiceBroker::GetWinSystem()->GetGfxContext().Clear(0);
+ CServiceBroker::GetWinSystem()->GetGfxContext().Flip(true, false);
+ ResetVSync();
+
+ m_windowDirty = false;
+ m_bIsInternalXrr = false;
+
+ if (!m_delayDispReset)
+ {
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ // tell any shared resources
+ for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnResetDisplay();
+ }
+ }
+ return true;
+}
+
+bool CWinSystemX11GLContext::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res)
+{
+ if(!CWinSystemX11::CreateNewWindow(name, fullScreen, res))
+ return false;
+
+ m_pGLContext->QueryExtensions();
+ return true;
+}
+
+bool CWinSystemX11GLContext::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ m_newGlContext = false;
+ CWinSystemX11::ResizeWindow(newWidth, newHeight, newLeft, newTop);
+ CRenderSystemGL::ResetRenderSystem(newWidth, newHeight);
+
+ if (m_newGlContext)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appSkin = components.GetComponent<CApplicationSkinHandling>();
+ appSkin->ReloadSkin();
+ }
+
+ return true;
+}
+
+void CWinSystemX11GLContext::FinishWindowResize(int newWidth, int newHeight)
+{
+ m_newGlContext = false;
+ CWinSystemX11::FinishWindowResize(newWidth, newHeight);
+ CRenderSystemGL::ResetRenderSystem(newWidth, newHeight);
+
+ if (m_newGlContext)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appSkin = components.GetComponent<CApplicationSkinHandling>();
+ appSkin->ReloadSkin();
+ }
+}
+
+bool CWinSystemX11GLContext::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ m_newGlContext = false;
+ CWinSystemX11::SetFullScreen(fullScreen, res, blankOtherDisplays);
+ CRenderSystemGL::ResetRenderSystem(res.iWidth, res.iHeight);
+
+ if (m_newGlContext)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appSkin = components.GetComponent<CApplicationSkinHandling>();
+ appSkin->ReloadSkin();
+ }
+
+ return true;
+}
+
+bool CWinSystemX11GLContext::DestroyWindowSystem()
+{
+ if (m_pGLContext)
+ m_pGLContext->Destroy();
+ return CWinSystemX11::DestroyWindowSystem();
+}
+
+bool CWinSystemX11GLContext::DestroyWindow()
+{
+ if (m_pGLContext)
+ m_pGLContext->Detach();
+ return CWinSystemX11::DestroyWindow();
+}
+
+XVisualInfo* CWinSystemX11GLContext::GetVisual()
+{
+ int count = 0;
+ XVisualInfo vTemplate;
+ XVisualInfo *visual = nullptr;
+
+ int vMask = VisualScreenMask | VisualDepthMask | VisualClassMask;
+
+ vTemplate.screen = m_screen;
+ vTemplate.depth = 24;
+ vTemplate.c_class = TrueColor;
+
+ visual = XGetVisualInfo(m_dpy, vMask, &vTemplate, &count);
+
+ if (!visual)
+ {
+ vTemplate.depth = 30;
+ visual = XGetVisualInfo(m_dpy, vMask, &vTemplate, &count);
+ }
+
+ return visual;
+}
+
+bool CWinSystemX11GLContext::RefreshGLContext(bool force)
+{
+ bool success = false;
+ if (m_pGLContext)
+ {
+ if (force)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appSkin = components.GetComponent<CApplicationSkinHandling>();
+ appSkin->UnloadSkin();
+ CRenderSystemGL::DestroyRenderSystem();
+ }
+ success = m_pGLContext->Refresh(force, m_screen, m_glWindow, m_newGlContext);
+ if (!success)
+ {
+ success = m_pGLContext->CreatePB();
+ m_newGlContext = true;
+ }
+ if (force)
+ CRenderSystemGL::InitRenderSystem();
+ return success;
+ }
+
+ m_dpms = std::make_shared<CX11DPMSSupport>();
+ VIDEOPLAYER::CProcessInfoX11::Register();
+ RETRO::CRPProcessInfoX11::Register();
+ RETRO::CRPProcessInfoX11::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL);
+ CDVDFactoryCodec::ClearHWAccels();
+ VIDEOPLAYER::CRendererFactory::ClearRenderer();
+ CLinuxRendererGL::Register();
+
+ CScreenshotSurfaceGL::Register();
+
+ std::string gpuvendor;
+ const char* vend = (const char*) glGetString(GL_VENDOR);
+ if (vend)
+ gpuvendor = vend;
+ std::transform(gpuvendor.begin(), gpuvendor.end(), gpuvendor.begin(), ::tolower);
+ bool isNvidia = (gpuvendor.compare(0, 6, "nvidia") == 0);
+ bool isIntel = (gpuvendor.compare(0, 5, "intel") == 0);
+ std::string gli = (getenv("KODI_GL_INTERFACE") != nullptr) ? getenv("KODI_GL_INTERFACE") : "";
+
+ if (gli != "GLX")
+ {
+ m_pGLContext = new CGLContextEGL(m_dpy, EGL_OPENGL_API);
+ success = m_pGLContext->Refresh(force, m_screen, m_glWindow, m_newGlContext);
+ if (success)
+ {
+ if (!isNvidia)
+ {
+ m_vaapiProxy.reset(VaapiProxyCreate());
+ VaapiProxyConfig(m_vaapiProxy.get(), GetDisplay(),
+ static_cast<CGLContextEGL*>(m_pGLContext)->m_eglDisplay);
+ bool general = false;
+ bool deepColor = false;
+ VAAPIRegisterRenderGL(m_vaapiProxy.get(), general, deepColor);
+ if (general)
+ {
+ VAAPIRegister(m_vaapiProxy.get(), deepColor);
+ return true;
+ }
+ if (isIntel || gli == "EGL")
+ return true;
+ }
+ }
+ else if (gli == "EGL_PB")
+ {
+ success = m_pGLContext->CreatePB();
+ if (success)
+ return true;
+ }
+ }
+
+ delete m_pGLContext;
+
+ // fallback for vdpau
+ m_pGLContext = GLXContextCreate(m_dpy);
+ success = m_pGLContext->Refresh(force, m_screen, m_glWindow, m_newGlContext);
+ if (success)
+ {
+ VDPAURegister();
+ VDPAURegisterRender();
+ }
+ return success;
+}
+
+std::unique_ptr<CVideoSync> CWinSystemX11GLContext::GetVideoSync(void *clock)
+{
+ std::unique_ptr<CVideoSync> pVSync;
+
+ if (dynamic_cast<CGLContextEGL*>(m_pGLContext))
+ {
+ pVSync.reset(new CVideoSyncOML(clock, *this));
+ }
+ else
+ {
+ pVSync.reset(GLXVideoSyncCreate(clock, *this));
+ }
+
+ return pVSync;
+}
+
+float CWinSystemX11GLContext::GetFrameLatencyAdjustment()
+{
+ if (m_pGLContext)
+ {
+ uint64_t msc, interval;
+ float micros = m_pGLContext->GetVblankTiming(msc, interval);
+ return micros / 1000;
+ }
+ return 0;
+}
+
+uint64_t CWinSystemX11GLContext::GetVblankTiming(uint64_t &msc, uint64_t &interval)
+{
+ if (m_pGLContext)
+ {
+ float micros = m_pGLContext->GetVblankTiming(msc, interval);
+ return micros;
+ }
+ msc = 0;
+ interval = 0;
+ return 0;
+}
+
+void CWinSystemX11GLContext::delete_CVaapiProxy::operator()(CVaapiProxy *p) const
+{
+ VaapiProxyDelete(p);
+}
diff --git a/xbmc/windowing/X11/WinSystemX11GLContext.h b/xbmc/windowing/X11/WinSystemX11GLContext.h
new file mode 100644
index 0000000..7d227b2
--- /dev/null
+++ b/xbmc/windowing/X11/WinSystemX11GLContext.h
@@ -0,0 +1,79 @@
+/*
+ * 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 "WinSystemX11.h"
+#include "rendering/gl/RenderSystemGL.h"
+#include "system_egl.h"
+
+#include <memory>
+
+class CGLContext;
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+class CVaapiProxy;
+
+class CWinSystemX11GLContext : public CWinSystemX11, public CRenderSystemGL
+{
+public:
+ CWinSystemX11GLContext() = default;
+ ~CWinSystemX11GLContext() override;
+
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystem via CWinSystemX11
+ CRenderSystemBase *GetRenderSystem() override { return this; }
+ bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override;
+ bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override;
+ void FinishWindowResize(int newWidth, int newHeight) override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+ bool DestroyWindowSystem() override;
+ bool DestroyWindow() override;
+
+ bool IsExtSupported(const char* extension) const override;
+
+ // videosync
+ std::unique_ptr<CVideoSync> GetVideoSync(void *clock) override;
+ float GetFrameLatencyAdjustment() override;
+ uint64_t GetVblankTiming(uint64_t &msc, uint64_t &interval);
+
+ XID GetWindow() const;
+ void* GetGlxContext() const;
+ EGLDisplay GetEGLDisplay() const;
+ EGLSurface GetEGLSurface() const;
+ EGLContext GetEGLContext() const;
+ EGLConfig GetEGLConfig() const;
+
+protected:
+ bool SetWindow(int width, int height, bool fullscreen, const std::string &output, int *winstate = NULL) override;
+ void PresentRenderImpl(bool rendered) override;
+ void SetVSyncImpl(bool enable) override;
+ bool RefreshGLContext(bool force);
+ XVisualInfo* GetVisual() override;
+
+ CGLContext *m_pGLContext = nullptr;
+ bool m_newGlContext;
+
+ struct delete_CVaapiProxy
+ {
+ void operator()(CVaapiProxy *p) const;
+ };
+ std::unique_ptr<CVaapiProxy, delete_CVaapiProxy> m_vaapiProxy;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/X11/WinSystemX11GLESContext.cpp b/xbmc/windowing/X11/WinSystemX11GLESContext.cpp
new file mode 100644
index 0000000..e2367d3
--- /dev/null
+++ b/xbmc/windowing/X11/WinSystemX11GLESContext.cpp
@@ -0,0 +1,293 @@
+/*
+ * 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 "WinSystemX11GLESContext.h"
+
+#include "GLContextEGL.h"
+#include "OptionalsReg.h"
+#include "X11DPMSSupport.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationSkinHandling.h"
+#include "cores/RetroPlayer/process/X11/RPProcessInfoX11.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/Process/X11/ProcessInfoX11.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "guilib/DispResource.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WindowSystemFactory.h"
+
+#include <mutex>
+
+using namespace KODI;
+using namespace KODI::WINDOWING::X11;
+
+void CWinSystemX11GLESContext::Register()
+{
+ KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "x11");
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemX11GLESContext::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemX11GLESContext>();
+}
+
+CWinSystemX11GLESContext::~CWinSystemX11GLESContext()
+{
+ delete m_pGLContext;
+}
+
+void CWinSystemX11GLESContext::PresentRenderImpl(bool rendered)
+{
+ if (rendered && m_pGLContext)
+ m_pGLContext->SwapBuffers();
+
+ if (m_delayDispReset && m_dispResetTimer.IsTimePast())
+ {
+ m_delayDispReset = false;
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ // tell any shared resources
+ for (std::vector<IDispResource*>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnResetDisplay();
+ }
+}
+
+void CWinSystemX11GLESContext::SetVSyncImpl(bool enable)
+{
+ m_pGLContext->SetVSync(enable);
+}
+
+bool CWinSystemX11GLESContext::IsExtSupported(const char* extension) const
+{
+ if (strncmp(extension, m_pGLContext->ExtPrefix().c_str(), 4) != 0)
+ return CRenderSystemGLES::IsExtSupported(extension);
+
+ return m_pGLContext->IsExtSupported(extension);
+}
+
+EGLDisplay CWinSystemX11GLESContext::GetEGLDisplay() const
+{
+ return m_pGLContext->m_eglDisplay;
+}
+
+EGLSurface CWinSystemX11GLESContext::GetEGLSurface() const
+{
+ return m_pGLContext->m_eglSurface;
+}
+
+EGLContext CWinSystemX11GLESContext::GetEGLContext() const
+{
+ return m_pGLContext->m_eglContext;
+}
+
+EGLConfig CWinSystemX11GLESContext::GetEGLConfig() const
+{
+ return m_pGLContext->m_eglConfig;
+}
+
+bool CWinSystemX11GLESContext::SetWindow(int width, int height, bool fullscreen, const std::string& output, int* winstate)
+{
+ int newwin = 0;
+
+ CWinSystemX11::SetWindow(width, height, fullscreen, output, &newwin);
+ if (newwin)
+ {
+ RefreshGLContext(m_currentOutput.compare(output) != 0);
+ XSync(m_dpy, false);
+ CServiceBroker::GetWinSystem()->GetGfxContext().Clear(0);
+ CServiceBroker::GetWinSystem()->GetGfxContext().Flip(true, false);
+ ResetVSync();
+
+ m_windowDirty = false;
+ m_bIsInternalXrr = false;
+
+ if (!m_delayDispReset)
+ {
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ // tell any shared resources
+ for (std::vector<IDispResource*>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnResetDisplay();
+ }
+ }
+ return true;
+}
+
+bool CWinSystemX11GLESContext::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res)
+{
+ CLog::Log(LOGINFO, "CWinSystemX11GLESContext::CreateNewWindow");
+ if (!CWinSystemX11::CreateNewWindow(name, fullScreen, res) || !m_pGLContext)
+ return false;
+
+ m_pGLContext->QueryExtensions();
+ return true;
+}
+
+bool CWinSystemX11GLESContext::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ m_newGlContext = false;
+ CWinSystemX11::ResizeWindow(newWidth, newHeight, newLeft, newTop);
+ CRenderSystemGLES::ResetRenderSystem(newWidth, newHeight);
+
+ if (m_newGlContext)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appSkin = components.GetComponent<CApplicationSkinHandling>();
+ appSkin->ReloadSkin();
+ }
+
+ return true;
+}
+
+void CWinSystemX11GLESContext::FinishWindowResize(int newWidth, int newHeight)
+{
+ m_newGlContext = false;
+ CWinSystemX11::FinishWindowResize(newWidth, newHeight);
+ CRenderSystemGLES::ResetRenderSystem(newWidth, newHeight);
+
+ if (m_newGlContext)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appSkin = components.GetComponent<CApplicationSkinHandling>();
+ appSkin->ReloadSkin();
+ }
+}
+
+bool CWinSystemX11GLESContext::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ m_newGlContext = false;
+ CWinSystemX11::SetFullScreen(fullScreen, res, blankOtherDisplays);
+ CRenderSystemGLES::ResetRenderSystem(res.iWidth, res.iHeight);
+
+ if (m_newGlContext)
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appSkin = components.GetComponent<CApplicationSkinHandling>();
+ appSkin->ReloadSkin();
+ }
+
+ return true;
+}
+
+bool CWinSystemX11GLESContext::DestroyWindowSystem()
+{
+ if (m_pGLContext)
+ m_pGLContext->Destroy();
+ return CWinSystemX11::DestroyWindowSystem();
+}
+
+bool CWinSystemX11GLESContext::DestroyWindow()
+{
+ if (m_pGLContext)
+ m_pGLContext->Detach();
+ return CWinSystemX11::DestroyWindow();
+}
+
+XVisualInfo* CWinSystemX11GLESContext::GetVisual()
+{
+ EGLDisplay eglDisplay;
+
+ PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT =
+ reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(eglGetProcAddress("eglGetPlatformDisplayEXT"));
+ if (eglGetPlatformDisplayEXT)
+ {
+ EGLint attribs[] =
+ {
+ EGL_PLATFORM_X11_SCREEN_EXT, m_screen,
+ EGL_NONE
+ };
+ eglDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_X11_EXT,static_cast<EGLNativeDisplayType>(m_dpy), attribs);
+ }
+ else
+ eglDisplay = eglGetDisplay(static_cast<EGLNativeDisplayType>(m_dpy));
+
+ if (eglDisplay == EGL_NO_DISPLAY)
+ {
+ CLog::Log(LOGERROR, "failed to get egl display");
+ return nullptr;
+ }
+ if (!eglInitialize(eglDisplay, nullptr, nullptr))
+ {
+ CLog::Log(LOGERROR, "failed to initialize egl display");
+ return nullptr;
+ }
+
+ GLint att[] =
+ {
+ EGL_RED_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_BLUE_SIZE, 8,
+ EGL_ALPHA_SIZE, 8,
+ EGL_BUFFER_SIZE, 32,
+ EGL_DEPTH_SIZE, 24,
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+ EGL_NONE
+ };
+ EGLint numConfigs;
+ EGLConfig eglConfig = 0;
+ if (!eglChooseConfig(eglDisplay, att, &eglConfig, 1, &numConfigs) || numConfigs == 0)
+ {
+ CLog::Log(LOGERROR, "Failed to choose a config {}", eglGetError());
+ return nullptr;
+ }
+
+ XVisualInfo x11_visual_info_template;
+ memset(&x11_visual_info_template, 0, sizeof(XVisualInfo));
+
+ if (!eglGetConfigAttrib(eglDisplay, eglConfig,
+ EGL_NATIVE_VISUAL_ID, reinterpret_cast<EGLint*>(&x11_visual_info_template.visualid)))
+ {
+ CLog::Log(LOGERROR, "Failed to query native visual id");
+ return nullptr;
+ }
+ int num_visuals;
+ XVisualInfo* visual =
+ XGetVisualInfo(m_dpy, VisualIDMask, &x11_visual_info_template, &num_visuals);
+ return visual;
+}
+
+bool CWinSystemX11GLESContext::RefreshGLContext(bool force)
+{
+ bool success = false;
+ if (m_pGLContext)
+ {
+ success = m_pGLContext->Refresh(force, m_screen, m_glWindow, m_newGlContext);
+ if (!success)
+ {
+ success = m_pGLContext->CreatePB();
+ m_newGlContext = true;
+ }
+ return success;
+ }
+
+ m_dpms = std::make_shared<CX11DPMSSupport>();
+ VIDEOPLAYER::CProcessInfoX11::Register();
+ RETRO::CRPProcessInfoX11::Register();
+ RETRO::CRPProcessInfoX11::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGLES);
+ CDVDFactoryCodec::ClearHWAccels();
+ VIDEOPLAYER::CRendererFactory::ClearRenderer();
+ CLinuxRendererGLES::Register();
+
+ std::string gli = (getenv("KODI_GL_INTERFACE") != nullptr) ? getenv("KODI_GL_INTERFACE") : "";
+
+ m_pGLContext = new CGLContextEGL(m_dpy, EGL_OPENGL_ES_API);
+ success = m_pGLContext->Refresh(force, m_screen, m_glWindow, m_newGlContext);
+ if (!success && gli == "EGL_PB")
+ {
+ success = m_pGLContext->CreatePB();
+ m_newGlContext = true;
+ }
+
+ if (!success)
+ {
+ delete m_pGLContext;
+ m_pGLContext = nullptr;
+ }
+ return success;
+}
diff --git a/xbmc/windowing/X11/WinSystemX11GLESContext.h b/xbmc/windowing/X11/WinSystemX11GLESContext.h
new file mode 100644
index 0000000..cdb1cc4
--- /dev/null
+++ b/xbmc/windowing/X11/WinSystemX11GLESContext.h
@@ -0,0 +1,62 @@
+/*
+ * 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 "EGL/egl.h"
+#include "WinSystemX11.h"
+#include "rendering/gles/RenderSystemGLES.h"
+
+class CGLContextEGL;
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace X11
+{
+
+class CWinSystemX11GLESContext : public CWinSystemX11, public CRenderSystemGLES
+{
+public:
+ CWinSystemX11GLESContext() = default;
+ virtual ~CWinSystemX11GLESContext() override;
+
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystem via CWinSystemX11
+ CRenderSystemBase* GetRenderSystem() override { return this; }
+ bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override;
+ bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override;
+ void FinishWindowResize(int newWidth, int newHeight) override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+ bool DestroyWindowSystem() override;
+ bool DestroyWindow() override;
+
+ bool IsExtSupported(const char* extension) const override;
+
+ EGLDisplay GetEGLDisplay() const;
+ EGLSurface GetEGLSurface() const;
+ EGLContext GetEGLContext() const;
+ EGLConfig GetEGLConfig() const;
+
+protected:
+ bool SetWindow(int width, int height, bool fullscreen, const std::string& output, int* winstate = nullptr) override;
+ void PresentRenderImpl(bool rendered) override;
+ void SetVSyncImpl(bool enable) override;
+ bool RefreshGLContext(bool force);
+ XVisualInfo* GetVisual() override;
+
+ CGLContextEGL* m_pGLContext = nullptr;
+ bool m_newGlContext;
+};
+
+} // namespace X11
+} // namespace WINDOWING
+} // namespace KODI
diff --git a/xbmc/windowing/X11/X11DPMSSupport.cpp b/xbmc/windowing/X11/X11DPMSSupport.cpp
new file mode 100644
index 0000000..7512bfa
--- /dev/null
+++ b/xbmc/windowing/X11/X11DPMSSupport.cpp
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2009-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 "X11DPMSSupport.h"
+
+#include "ServiceBroker.h"
+#include "utils/log.h"
+#include "windowing/X11/WinSystemX11.h"
+
+#include <X11/Xlib.h>
+#include <X11/extensions/dpms.h>
+
+using namespace KODI::WINDOWING::X11;
+
+namespace
+{
+// Mapping of PowerSavingMode to X11's mode constants.
+const CARD16 X_DPMS_MODES[] =
+{
+ DPMSModeStandby,
+ DPMSModeSuspend,
+ DPMSModeOff
+};
+}
+
+CX11DPMSSupport::CX11DPMSSupport()
+{
+ CWinSystemX11* winSystem = dynamic_cast<CWinSystemX11*>(CServiceBroker::GetWinSystem());
+ if (!winSystem)
+ return;
+
+ Display* dpy = winSystem->GetDisplay();
+ if (!dpy)
+ return;
+
+ int event_base, error_base; // we ignore these
+ if (!DPMSQueryExtension(dpy, &event_base, &error_base))
+ {
+ CLog::Log(LOGINFO, "DPMS: X11 extension not present, power-saving will not be available");
+ return;
+ }
+
+ if (!DPMSCapable(dpy))
+ {
+ CLog::Log(LOGINFO, "DPMS: display does not support power-saving");
+ return;
+ }
+
+ m_supportedModes.push_back(SUSPEND); // best compromise
+ m_supportedModes.push_back(OFF); // next best
+ m_supportedModes.push_back(STANDBY); // rather lame, < 80% power according to DPMS spec
+}
+
+bool CX11DPMSSupport::EnablePowerSaving(PowerSavingMode mode)
+{
+ CWinSystemX11* winSystem = dynamic_cast<CWinSystemX11*>(CServiceBroker::GetWinSystem());
+ if (!winSystem)
+ return false;
+
+ Display* dpy = winSystem->GetDisplay();
+ if (!dpy)
+ return false;
+
+ // This is not needed on my ATI Radeon, but the docs say that DPMSForceLevel
+ // after a DPMSDisable (from SDL) should not normally work.
+ DPMSEnable(dpy);
+ DPMSForceLevel(dpy, X_DPMS_MODES[mode]);
+ // There shouldn't be any errors if we called DPMSEnable; if they do happen,
+ // they're asynchronous and messy to detect.
+ XFlush(dpy);
+ return true;
+}
+
+bool CX11DPMSSupport::DisablePowerSaving()
+{
+ CWinSystemX11* winSystem = dynamic_cast<CWinSystemX11*>(CServiceBroker::GetWinSystem());
+ if (!winSystem)
+ return false;
+
+ Display* dpy = winSystem->GetDisplay();
+ if (!dpy)
+ return false;
+
+ DPMSForceLevel(dpy, DPMSModeOn);
+ DPMSDisable(dpy);
+ XFlush(dpy);
+
+ winSystem->RecreateWindow();
+
+ return true;
+}
diff --git a/xbmc/windowing/X11/X11DPMSSupport.h b/xbmc/windowing/X11/X11DPMSSupport.h
new file mode 100644
index 0000000..c6ce8df
--- /dev/null
+++ b/xbmc/windowing/X11/X11DPMSSupport.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2009-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 "xbmc/powermanagement/DPMSSupport.h"
+
+class CX11DPMSSupport : public CDPMSSupport
+{
+public:
+ CX11DPMSSupport();
+ ~CX11DPMSSupport() override = default;
+
+protected:
+ bool EnablePowerSaving(PowerSavingMode mode) override;
+ bool DisablePowerSaving() override;
+};
diff --git a/xbmc/windowing/X11/XRandR.cpp b/xbmc/windowing/X11/XRandR.cpp
new file mode 100644
index 0000000..ab20da3
--- /dev/null
+++ b/xbmc/windowing/X11/XRandR.cpp
@@ -0,0 +1,518 @@
+/*
+ * 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 "XRandR.h"
+
+#include "CompileInfo.h"
+#include "threads/SystemClock.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <string.h>
+
+#include <sys/wait.h>
+
+#include "PlatformDefs.h"
+
+#if defined(TARGET_FREEBSD)
+#include <sys/types.h>
+#include <sys/wait.h>
+#endif
+
+using namespace std::chrono_literals;
+
+CXRandR::CXRandR(bool query)
+{
+ m_bInit = false;
+ m_numScreens = 1;
+ if (query)
+ Query();
+}
+
+bool CXRandR::Query(bool force, bool ignoreoff)
+{
+ if (!force)
+ if (m_bInit)
+ return m_outputs.size() > 0;
+
+ m_bInit = true;
+
+ if (getenv("KODI_BIN_HOME") == NULL)
+ return false;
+
+ m_outputs.clear();
+ // query all screens
+ // we are happy if at least one screen returns results
+ bool success = false;
+ for(unsigned int screennum=0; screennum<m_numScreens; ++screennum)
+ {
+ if(Query(force, screennum, ignoreoff))
+ success = true;
+ }
+ return success;
+}
+
+bool CXRandR::Query(bool force, int screennum, bool ignoreoff)
+{
+ std::string cmd;
+ std::string appname = CCompileInfo::GetAppName();
+ StringUtils::ToLower(appname);
+ if (getenv("KODI_BIN_HOME"))
+ {
+ cmd = getenv("KODI_BIN_HOME");
+ cmd += "/" + appname + "-xrandr";
+ cmd = StringUtils::Format("{} -q --screen {}", cmd, screennum);
+ }
+
+ FILE* file = popen(cmd.c_str(),"r");
+ if (!file)
+ {
+ CLog::Log(LOGERROR, "CXRandR::Query - unable to execute xrandr tool");
+ return false;
+ }
+
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(file, TIXML_DEFAULT_ENCODING))
+ {
+ CLog::Log(LOGERROR, "CXRandR::Query - unable to open xrandr xml");
+ pclose(file);
+ return false;
+ }
+ pclose(file);
+
+ TiXmlElement *pRootElement = xmlDoc.RootElement();
+ if (atoi(pRootElement->Attribute("id")) != screennum)
+ {
+ //! @todo ERROR
+ return false;
+ }
+
+ for (TiXmlElement* output = pRootElement->FirstChildElement("output"); output; output = output->NextSiblingElement("output"))
+ {
+ XOutput xoutput;
+ xoutput.name = output->Attribute("name");
+ StringUtils::Trim(xoutput.name);
+ xoutput.isConnected = (StringUtils::CompareNoCase(output->Attribute("connected"), "true") == 0);
+ xoutput.screen = screennum;
+ xoutput.w = (output->Attribute("w") != NULL ? atoi(output->Attribute("w")) : 0);
+ xoutput.h = (output->Attribute("h") != NULL ? atoi(output->Attribute("h")) : 0);
+ xoutput.x = (output->Attribute("x") != NULL ? atoi(output->Attribute("x")) : 0);
+ xoutput.y = (output->Attribute("y") != NULL ? atoi(output->Attribute("y")) : 0);
+ xoutput.crtc = (output->Attribute("crtc") != NULL ? atoi(output->Attribute("crtc")) : 0);
+ xoutput.wmm = (output->Attribute("wmm") != NULL ? atoi(output->Attribute("wmm")) : 0);
+ xoutput.hmm = (output->Attribute("hmm") != NULL ? atoi(output->Attribute("hmm")) : 0);
+ if (output->Attribute("rotation") != NULL &&
+ (StringUtils::CompareNoCase(output->Attribute("rotation"), "left") == 0 ||
+ StringUtils::CompareNoCase(output->Attribute("rotation"), "right") == 0))
+ {
+ xoutput.isRotated = true;
+ }
+ else
+ xoutput.isRotated = false;
+
+ if (!xoutput.isConnected)
+ continue;
+
+ bool hascurrent = false;
+ for (TiXmlElement* mode = output->FirstChildElement("mode"); mode; mode = mode->NextSiblingElement("mode"))
+ {
+ XMode xmode;
+ xmode.id = mode->Attribute("id");
+ xmode.name = mode->Attribute("name");
+ xmode.hz = atof(mode->Attribute("hz"));
+ xmode.w = atoi(mode->Attribute("w"));
+ xmode.h = atoi(mode->Attribute("h"));
+ xmode.isPreferred = (StringUtils::CompareNoCase(mode->Attribute("preferred"), "true") == 0);
+ xmode.isCurrent = (StringUtils::CompareNoCase(mode->Attribute("current"), "true") == 0);
+ xoutput.modes.push_back(xmode);
+ if (xmode.isCurrent)
+ hascurrent = true;
+ }
+ if (hascurrent || !ignoreoff)
+ m_outputs.push_back(xoutput);
+ else
+ CLog::Log(LOGWARNING, "CXRandR::Query - output {} has no current mode, assuming disconnected",
+ xoutput.name);
+ }
+ return m_outputs.size() > 0;
+}
+
+bool CXRandR::TurnOffOutput(const std::string& name)
+{
+ XOutput *output = GetOutput(name);
+ if (!output)
+ return false;
+
+ std::string cmd;
+ std::string appname = CCompileInfo::GetAppName();
+ StringUtils::ToLower(appname);
+
+ if (getenv("KODI_BIN_HOME"))
+ {
+ cmd = getenv("KODI_BIN_HOME");
+ cmd += "/" + appname + "-xrandr";
+ cmd = StringUtils::Format("{} --screen {} --output {} --off", cmd, output->screen, name);
+ }
+
+ int status = system(cmd.c_str());
+ if (status == -1)
+ return false;
+
+ if (WEXITSTATUS(status) != 0)
+ return false;
+
+ return true;
+}
+
+bool CXRandR::TurnOnOutput(const std::string& name)
+{
+ XOutput *output = GetOutput(name);
+ if (!output)
+ return false;
+
+ XMode mode = GetCurrentMode(output->name);
+ if (mode.isCurrent)
+ return true;
+
+ // get preferred mode
+ for (unsigned int j = 0; j < m_outputs.size(); j++)
+ {
+ if (m_outputs[j].name == output->name)
+ {
+ for (unsigned int i = 0; i < m_outputs[j].modes.size(); i++)
+ {
+ if (m_outputs[j].modes[i].isPreferred)
+ {
+ mode = m_outputs[j].modes[i];
+ break;
+ }
+ }
+ }
+ }
+
+ if (!mode.isPreferred)
+ return false;
+
+ if (!SetMode(*output, mode))
+ return false;
+
+ XbmcThreads::EndTime<> timeout(5s);
+ while (!timeout.IsTimePast())
+ {
+ if (!Query(true))
+ return false;
+
+ output = GetOutput(name);
+ if (output && output->h > 0)
+ return true;
+
+ KODI::TIME::Sleep(200ms);
+ }
+
+ return false;
+}
+
+std::vector<XOutput> CXRandR::GetModes(void)
+{
+ Query();
+ return m_outputs;
+}
+
+void CXRandR::SaveState()
+{
+ Query(true);
+}
+
+bool CXRandR::SetMode(const XOutput& output, const XMode& mode)
+{
+ if ((output.name == "" && mode.id == ""))
+ return true;
+
+ Query();
+
+ // Make sure the output exists, if not -- complain and exit
+ bool isOutputFound = false;
+ XOutput outputFound;
+ for (size_t i = 0; i < m_outputs.size(); i++)
+ {
+ if (m_outputs[i].name == output.name)
+ {
+ isOutputFound = true;
+ outputFound = m_outputs[i];
+ }
+ }
+
+ if (!isOutputFound)
+ {
+ CLog::Log(LOGERROR,
+ "CXRandR::SetMode: asked to change resolution for non existing output: {} mode: {}",
+ output.name, mode.id);
+ return false;
+ }
+
+ // try to find the same exact mode (same id, resolution, hz)
+ bool isModeFound = false;
+ XMode modeFound;
+ for (size_t i = 0; i < outputFound.modes.size(); i++)
+ {
+ if (outputFound.modes[i].id == mode.id)
+ {
+ if (outputFound.modes[i].w == mode.w &&
+ outputFound.modes[i].h == mode.h &&
+ outputFound.modes[i].hz == mode.hz)
+ {
+ isModeFound = true;
+ modeFound = outputFound.modes[i];
+ }
+ else
+ {
+ CLog::Log(LOGERROR,
+ "CXRandR::SetMode: asked to change resolution for mode that exists but with "
+ "different w/h/hz: {} mode: {}. Searching for similar modes...",
+ output.name, mode.id);
+ break;
+ }
+ }
+ }
+
+ if (!isModeFound)
+ {
+ for (size_t i = 0; i < outputFound.modes.size(); i++)
+ {
+ if (outputFound.modes[i].w == mode.w &&
+ outputFound.modes[i].h == mode.h &&
+ outputFound.modes[i].hz == mode.hz)
+ {
+ isModeFound = true;
+ modeFound = outputFound.modes[i];
+ CLog::Log(LOGWARNING, "CXRandR::SetMode: found alternative mode (same hz): {} mode: {}.",
+ output.name, outputFound.modes[i].id);
+ }
+ }
+ }
+
+ if (!isModeFound)
+ {
+ for (size_t i = 0; i < outputFound.modes.size(); i++)
+ {
+ if (outputFound.modes[i].w == mode.w &&
+ outputFound.modes[i].h == mode.h)
+ {
+ isModeFound = true;
+ modeFound = outputFound.modes[i];
+ CLog::Log(LOGWARNING,
+ "CXRandR::SetMode: found alternative mode (different hz): {} mode: {}.",
+ output.name, outputFound.modes[i].id);
+ }
+ }
+ }
+
+ // Let's try finding a mode that is the same
+ if (!isModeFound)
+ {
+ CLog::Log(LOGERROR,
+ "CXRandR::SetMode: asked to change resolution for non existing mode: {} mode: {}",
+ output.name, mode.id);
+ return false;
+ }
+
+ m_currentOutput = outputFound.name;
+ m_currentMode = modeFound.id;
+ std::string appname = CCompileInfo::GetAppName();
+ StringUtils::ToLower(appname);
+ char cmd[255];
+
+ if (getenv("KODI_BIN_HOME"))
+ snprintf(cmd, sizeof(cmd), "%s/%s-xrandr --screen %d --output %s --mode %s",
+ getenv("KODI_BIN_HOME"),appname.c_str(),
+ outputFound.screen, outputFound.name.c_str(), modeFound.id.c_str());
+ else
+ return false;
+ CLog::Log(LOGINFO, "XRANDR: {}", cmd);
+ int status = system(cmd);
+ if (status == -1)
+ return false;
+
+ if (WEXITSTATUS(status) != 0)
+ return false;
+
+ return true;
+}
+
+XMode CXRandR::GetCurrentMode(const std::string& outputName)
+{
+ Query();
+ XMode result;
+
+ for (unsigned int j = 0; j < m_outputs.size(); j++)
+ {
+ if (m_outputs[j].name == outputName || outputName == "")
+ {
+ for (unsigned int i = 0; i < m_outputs[j].modes.size(); i++)
+ {
+ if (m_outputs[j].modes[i].isCurrent)
+ {
+ result = m_outputs[j].modes[i];
+ break;
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+XMode CXRandR::GetPreferredMode(const std::string& outputName)
+{
+ Query();
+ XMode result;
+
+ for (unsigned int j = 0; j < m_outputs.size(); j++)
+ {
+ if (m_outputs[j].name == outputName || outputName == "")
+ {
+ for (unsigned int i = 0; i < m_outputs[j].modes.size(); i++)
+ {
+ if (m_outputs[j].modes[i].isPreferred)
+ {
+ result = m_outputs[j].modes[i];
+ break;
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+void CXRandR::LoadCustomModeLinesToAllOutputs(void)
+{
+ Query();
+ CXBMCTinyXML xmlDoc;
+
+ if (!xmlDoc.LoadFile("special://xbmc/userdata/ModeLines.xml"))
+ {
+ return;
+ }
+
+ TiXmlElement *pRootElement = xmlDoc.RootElement();
+ if (StringUtils::CompareNoCase(pRootElement->Value(), "modelines") != 0)
+ {
+ //! @todo ERROR
+ return;
+ }
+
+ char cmd[255];
+ std::string name;
+ std::string strModeLine;
+
+ for (TiXmlElement* modeline = pRootElement->FirstChildElement("modeline"); modeline; modeline = modeline->NextSiblingElement("modeline"))
+ {
+ name = modeline->Attribute("label");
+ StringUtils::Trim(name);
+ strModeLine = modeline->FirstChild()->Value();
+ StringUtils::Trim(strModeLine);
+ std::string appname = CCompileInfo::GetAppName();
+ StringUtils::ToLower(appname);
+
+ if (getenv("KODI_BIN_HOME"))
+ {
+ snprintf(cmd, sizeof(cmd), "%s/%s-xrandr --newmode \"%s\" %s > /dev/null 2>&1", getenv("KODI_BIN_HOME"),
+ appname.c_str(), name.c_str(), strModeLine.c_str());
+ if (system(cmd) != 0)
+ CLog::Log(LOGERROR, "Unable to create modeline \"{}\"", name);
+ }
+
+ for (unsigned int i = 0; i < m_outputs.size(); i++)
+ {
+ if (getenv("KODI_BIN_HOME"))
+ {
+ snprintf(cmd, sizeof(cmd), "%s/%s-xrandr --addmode %s \"%s\" > /dev/null 2>&1", getenv("KODI_BIN_HOME"),
+ appname.c_str(), m_outputs[i].name.c_str(), name.c_str());
+ if (system(cmd) != 0)
+ CLog::Log(LOGERROR, "Unable to add modeline \"{}\"", name);
+ }
+ }
+ }
+}
+
+void CXRandR::SetNumScreens(unsigned int num)
+{
+ m_numScreens = num;
+ m_bInit = false;
+}
+
+bool CXRandR::IsOutputConnected(const std::string& name)
+{
+ bool result = false;
+ Query();
+
+ for (unsigned int i = 0; i < m_outputs.size(); ++i)
+ {
+ if (m_outputs[i].name == name)
+ {
+ result = true;
+ break;
+ }
+ }
+ return result;
+}
+
+XOutput* CXRandR::GetOutput(const std::string& outputName)
+{
+ XOutput *result = 0;
+ Query();
+ for (unsigned int i = 0; i < m_outputs.size(); ++i)
+ {
+ if (m_outputs[i].name == outputName)
+ {
+ result = &m_outputs[i];
+ break;
+ }
+ }
+ return result;
+}
+
+int CXRandR::GetCrtc(int x, int y, float &hz)
+{
+ int crtc = 0;
+ for (unsigned int i = 0; i < m_outputs.size(); ++i)
+ {
+ if (!m_outputs[i].isConnected)
+ continue;
+
+ if ((m_outputs[i].x <= x && (m_outputs[i].x+m_outputs[i].w) > x) &&
+ (m_outputs[i].y <= y && (m_outputs[i].y+m_outputs[i].h) > y))
+ {
+ crtc = m_outputs[i].crtc;
+ for (const auto& mode : m_outputs[i].modes)
+ {
+ if (mode.isCurrent)
+ {
+ hz = mode.hz;
+ break;
+ }
+ }
+ break;
+ }
+ }
+ return crtc;
+}
+
+CXRandR g_xrandr;
+
+/*
+ int main()
+ {
+ CXRandR r;
+ r.LoadCustomModeLinesToAllOutputs();
+ }
+*/
diff --git a/xbmc/windowing/X11/XRandR.h b/xbmc/windowing/X11/XRandR.h
new file mode 100644
index 0000000..6f5f74c
--- /dev/null
+++ b/xbmc/windowing/X11/XRandR.h
@@ -0,0 +1,109 @@
+/*
+ * 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 <map>
+#include <string>
+#include <vector>
+
+class XMode
+{
+public:
+ XMode()
+ {
+ hz=0.0f;
+ isPreferred=false;
+ isCurrent=false;
+ w=h=0;
+ }
+ bool operator==(XMode& mode) const
+ {
+ if (id != mode.id)
+ return false;
+ if (name != mode.name)
+ return false;
+ if (hz != mode.hz)
+ return false;
+ if (isPreferred != mode.isPreferred)
+ return false;
+ if (isCurrent != mode.isCurrent)
+ return false;
+ if (w != mode.w)
+ return false;
+ if (h != mode.h)
+ return false;
+ return true;
+ }
+ bool IsInterlaced()
+ {
+ return name.back() == 'i';
+ }
+ std::string id;
+ std::string name;
+ float hz;
+ bool isPreferred;
+ bool isCurrent;
+ unsigned int w;
+ unsigned int h;
+};
+
+class XOutput
+{
+public:
+ XOutput()
+ {
+ isConnected = false;
+ w = h = x = y = wmm = hmm = 0;
+ }
+ std::string name;
+ bool isConnected;
+ int screen;
+ int w;
+ int h;
+ int x;
+ int y;
+ int crtc;
+ int wmm;
+ int hmm;
+ std::vector<XMode> modes;
+ bool isRotated;
+};
+
+class CXRandR
+{
+public:
+ explicit CXRandR(bool query=false);
+ bool Query(bool force=false, bool ignoreoff=true);
+ bool Query(bool force, int screennum, bool ignoreoff=true);
+ std::vector<XOutput> GetModes(void);
+ XMode GetCurrentMode(const std::string& outputName);
+ XMode GetPreferredMode(const std::string& outputName);
+ XOutput *GetOutput(const std::string& outputName);
+ bool SetMode(const XOutput& output, const XMode& mode);
+ void LoadCustomModeLinesToAllOutputs(void);
+ void SaveState();
+ void SetNumScreens(unsigned int num);
+ bool IsOutputConnected(const std::string& name);
+ bool TurnOffOutput(const std::string& name);
+ bool TurnOnOutput(const std::string& name);
+ int GetCrtc(int x, int y, float &hz);
+ //bool Has1080i();
+ //bool Has1080p();
+ //bool Has720p();
+ //bool Has480p();
+
+private:
+ bool m_bInit;
+ std::vector<XOutput> m_outputs;
+ std::string m_currentOutput;
+ std::string m_currentMode;
+ unsigned int m_numScreens;
+};
+
+extern CXRandR g_xrandr;
diff --git a/xbmc/windowing/XBMC_events.h b/xbmc/windowing/XBMC_events.h
new file mode 100644
index 0000000..d9bf3ea
--- /dev/null
+++ b/xbmc/windowing/XBMC_events.h
@@ -0,0 +1,134 @@
+/*
+ * SDL - Simple DirectMedia Layer
+ * Copyright (C) 1997-2009 Sam Lantinga
+ * Sam Lantinga
+ * slouken@libsdl.org
+ *
+ * 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 file for SDL event handling */
+
+#include "Resolution.h"
+#include "input/XBMC_keyboard.h"
+
+/* Event enumerations */
+typedef enum
+{
+ XBMC_NOEVENT = 0, /* Unused (do not remove) */
+ XBMC_KEYDOWN, /* Keys pressed */
+ XBMC_KEYUP, /* Keys released */
+ XBMC_MOUSEMOTION, /* Mouse moved */
+ XBMC_MOUSEBUTTONDOWN, /* Mouse button pressed */
+ XBMC_MOUSEBUTTONUP, /* Mouse button released */
+ XBMC_QUIT, /* User-requested quit */
+ XBMC_VIDEORESIZE, /* User resized video mode */
+ XBMC_FULLSCREEN_UPDATE, /* Triggered by an OS event when Kodi is running in fullscreen, rescale and repositioning is required */
+ XBMC_VIDEOMOVE, /* User moved the window */
+ XBMC_MODECHANGE, /* Video mode must be changed */
+ XBMC_TOUCH,
+ XBMC_BUTTON, /* Button (remote) pressed */
+ XBMC_SETFOCUS,
+ XBMC_USEREVENT,
+
+ XBMC_MAXEVENT = 256 /* XBMC_EventType is represented as uchar */
+} XBMC_EventType;
+
+/* Keyboard event structure */
+typedef struct XBMC_KeyboardEvent {
+ XBMC_keysym keysym;
+} XBMC_KeyboardEvent;
+
+/* Mouse motion event structure */
+typedef struct XBMC_MouseMotionEvent {
+ uint16_t x, y; /* The X/Y coordinates of the mouse */
+} XBMC_MouseMotionEvent;
+
+/* Mouse button event structure */
+typedef struct XBMC_MouseButtonEvent {
+ unsigned char button; /* The mouse button index */
+ uint16_t x, y; /* The X/Y coordinates of the mouse at press time */
+} XBMC_MouseButtonEvent;
+
+/* The "window resized" event
+ When you get this event, you are responsible for setting a new video
+ mode with the new width and height.
+ */
+typedef struct XBMC_ResizeEvent {
+ int w; /* New width */
+ int h; /* New height */
+} XBMC_ResizeEvent;
+
+typedef struct XBMC_MoveEvent {
+ int x; /* New x position */
+ int y; /* New y position */
+} XBMC_MoveEvent;
+
+struct XBMC_ModeChangeEvent
+{
+ RESOLUTION res;
+};
+
+/* The "quit requested" event */
+typedef struct XBMC_QuitEvent {
+} XBMC_QuitEvent;
+
+/* A user-defined event type */
+typedef struct XBMC_UserEvent {
+ int code; /* User defined event code */
+ void *data1; /* User defined data pointer */
+ void *data2; /* User defined data pointer */
+} XBMC_UserEvent;
+
+/* Multimedia keys on keyboards / remotes are mapped to APPCOMMAND events */
+typedef struct XBMC_AppCommandEvent {
+ unsigned int action; /* One of ACTION_... */
+} XBMC_AppCommandEvent;
+
+/* Mouse motion event structure */
+typedef struct XBMC_TouchEvent {
+ int action; /* action ID */
+ float x, y; /* The X/Y coordinates of the mouse */
+ float x2, y2; /* Additional X/Y coordinates */
+ float x3, y3; /* Additional X/Y coordinates */
+ int pointers; /* number of touch pointers */
+} XBMC_TouchEvent;
+
+typedef struct XBMC_SetFocusEvent {
+ int x; /* x position */
+ int y; /* y position */
+} XBMC_SetFocusEvent;
+
+/* Button event structure */
+typedef struct XBMC_ButtonEvent
+{
+ uint32_t button;
+ uint32_t holdtime;
+} XBMC_ButtonEvent;
+
+/* General event structure */
+typedef struct XBMC_Event {
+ uint8_t type;
+ union
+ {
+ XBMC_KeyboardEvent key;
+ XBMC_MouseMotionEvent motion;
+ XBMC_MouseButtonEvent button;
+ XBMC_ResizeEvent resize;
+ XBMC_MoveEvent move;
+ XBMC_ModeChangeEvent mode;
+ XBMC_QuitEvent quit;
+ XBMC_UserEvent user;
+ XBMC_AppCommandEvent appcommand;
+ XBMC_TouchEvent touch;
+ XBMC_ButtonEvent keybutton;
+ XBMC_SetFocusEvent focus;
+ };
+} XBMC_Event;
+
diff --git a/xbmc/windowing/android/AndroidUtils.cpp b/xbmc/windowing/android/AndroidUtils.cpp
new file mode 100644
index 0000000..8c61fae
--- /dev/null
+++ b/xbmc/windowing/android/AndroidUtils.cpp
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2011-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 "AndroidUtils.h"
+
+#include "ServiceBroker.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/SettingsManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include "platform/android/activity/XBMCApp.h"
+
+#include <androidjni/MediaCodecInfo.h>
+#include <androidjni/MediaCodecList.h>
+#include <androidjni/System.h>
+#include <androidjni/SystemProperties.h>
+#include <androidjni/View.h>
+#include <androidjni/Window.h>
+#include <androidjni/WindowManager.h>
+
+static bool s_hasModeApi = false;
+static std::vector<RESOLUTION_INFO> s_res_displayModes;
+static RESOLUTION_INFO s_res_cur_displayMode;
+
+static float currentRefreshRate()
+{
+ if (s_hasModeApi)
+ return s_res_cur_displayMode.fRefreshRate;
+
+ CJNIWindow window = CXBMCApp::getWindow();
+ if (window)
+ {
+ float preferredRate = window.getAttributes().getpreferredRefreshRate();
+ if (preferredRate > 20.0f)
+ {
+ CLog::Log(LOGINFO, "CAndroidUtils: Preferred refresh rate: {:f}", preferredRate);
+ return preferredRate;
+ }
+ CJNIView view(window.getDecorView());
+ if (view)
+ {
+ CJNIDisplay display(view.getDisplay());
+ if (display)
+ {
+ float reportedRate = display.getRefreshRate();
+ if (reportedRate > 20.0f)
+ {
+ CLog::Log(LOGINFO, "CAndroidUtils: Current display refresh rate: {:f}", reportedRate);
+ return reportedRate;
+ }
+ }
+ }
+ }
+ CLog::Log(LOGDEBUG, "found no refresh rate");
+ return 60.0;
+}
+
+static void fetchDisplayModes()
+{
+ s_hasModeApi = false;
+ s_res_displayModes.clear();
+
+ CJNIDisplay display = CXBMCApp::getWindow().getDecorView().getDisplay();
+
+ if (display)
+ {
+ CJNIDisplayMode m = display.getMode();
+ if (m)
+ {
+ if (m.getPhysicalWidth() > m.getPhysicalHeight()) // Assume unusable if portrait is returned
+ {
+ s_hasModeApi = true;
+
+ CLog::Log(LOGDEBUG, "CAndroidUtils: current mode: {}: {}x{}@{:f}", m.getModeId(),
+ m.getPhysicalWidth(), m.getPhysicalHeight(), m.getRefreshRate());
+ s_res_cur_displayMode.strId = std::to_string(m.getModeId());
+ s_res_cur_displayMode.iWidth = s_res_cur_displayMode.iScreenWidth = m.getPhysicalWidth();
+ s_res_cur_displayMode.iHeight = s_res_cur_displayMode.iScreenHeight = m.getPhysicalHeight();
+ s_res_cur_displayMode.fRefreshRate = m.getRefreshRate();
+ s_res_cur_displayMode.dwFlags = D3DPRESENTFLAG_PROGRESSIVE;
+ s_res_cur_displayMode.bFullScreen = true;
+ s_res_cur_displayMode.iSubtitles = s_res_cur_displayMode.iHeight;
+ s_res_cur_displayMode.fPixelRatio = 1.0f;
+ s_res_cur_displayMode.strMode = StringUtils::Format(
+ "{}x{} @ {:.6f}{} - Full Screen", s_res_cur_displayMode.iScreenWidth,
+ s_res_cur_displayMode.iScreenHeight, s_res_cur_displayMode.fRefreshRate,
+ s_res_cur_displayMode.dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : "");
+
+ std::vector<CJNIDisplayMode> modes = display.getSupportedModes();
+ for (CJNIDisplayMode& m : modes)
+ {
+ CLog::Log(LOGDEBUG, "CAndroidUtils: available mode: {}: {}x{}@{:f}", m.getModeId(),
+ m.getPhysicalWidth(), m.getPhysicalHeight(), m.getRefreshRate());
+
+ RESOLUTION_INFO res;
+ res.strId = std::to_string(m.getModeId());
+ res.iWidth = res.iScreenWidth = m.getPhysicalWidth();
+ res.iHeight = res.iScreenHeight = m.getPhysicalHeight();
+ res.fRefreshRate = m.getRefreshRate();
+ res.dwFlags = D3DPRESENTFLAG_PROGRESSIVE;
+ res.bFullScreen = true;
+ res.iSubtitles = res.iHeight;
+ res.fPixelRatio = 1.0f;
+ res.strMode = StringUtils::Format("{}x{} @ {:.6f}{} - Full Screen", res.iScreenWidth,
+ res.iScreenHeight, res.fRefreshRate,
+ res.dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : "");
+
+ s_res_displayModes.push_back(res);
+ }
+ }
+ }
+ }
+}
+
+namespace
+{
+const std::map<int, std::string> hdrTypeMap = {
+ {CAndroidUtils::HDRTypes::HDR10, "HDR10"},
+ {CAndroidUtils::HDRTypes::HLG, "HLG"},
+ {CAndroidUtils::HDRTypes::HDR10_PLUS, "HDR10+"},
+ {CAndroidUtils::HDRTypes::DOLBY_VISION, "Dolby Vision"}};
+
+std::string HdrTypeString(int type)
+{
+ auto hdr = hdrTypeMap.find(type);
+ if (hdr != hdrTypeMap.end())
+ return hdr->second;
+
+ return "Unknown";
+}
+} // unnamed namespace
+
+const std::string CAndroidUtils::SETTING_LIMITGUI = "videoscreen.limitgui";
+
+CAndroidUtils::CAndroidUtils()
+{
+ std::string displaySize;
+ m_width = m_height = 0;
+
+ if (CJNIBase::GetSDKVersion() >= 23)
+ {
+ fetchDisplayModes();
+ for (const RESOLUTION_INFO& res : s_res_displayModes)
+ {
+ if (res.iWidth > m_width || res.iHeight > m_height)
+ {
+ m_width = res.iWidth;
+ m_height = res.iHeight;
+ }
+ }
+ }
+
+ if (!m_width || !m_height)
+ {
+ // Property available on some devices
+ displaySize = CJNISystemProperties::get("sys.display-size", "");
+ if (!displaySize.empty())
+ {
+ std::vector<std::string> aSize = StringUtils::Split(displaySize, "x");
+ if (aSize.size() == 2)
+ {
+ m_width = StringUtils::IsInteger(aSize[0]) ? atoi(aSize[0].c_str()) : 0;
+ m_height = StringUtils::IsInteger(aSize[1]) ? atoi(aSize[1].c_str()) : 0;
+ }
+ CLog::Log(LOGDEBUG, "CAndroidUtils: display-size: {}({}x{})", displaySize, m_width, m_height);
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "CAndroidUtils: maximum/current resolution: {}x{}", m_width, m_height);
+ int limit = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CAndroidUtils::SETTING_LIMITGUI);
+ switch (limit)
+ {
+ case 0: // auto
+ m_width = 0;
+ m_height = 0;
+ break;
+
+ case 9999: // unlimited
+ break;
+
+ case 720:
+ if (m_height > 720)
+ {
+ m_width = 1280;
+ m_height = 720;
+ }
+ break;
+
+ case 1080:
+ if (m_height > 1080)
+ {
+ m_width = 1920;
+ m_height = 1080;
+ }
+ break;
+ }
+ CLog::Log(LOGDEBUG, "CAndroidUtils: selected resolution: {}x{}", m_width, m_height);
+
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetSettingsManager()->RegisterCallback(
+ this, {CAndroidUtils::SETTING_LIMITGUI});
+
+ LogDisplaySupportedHdrTypes();
+}
+
+bool CAndroidUtils::GetNativeResolution(RESOLUTION_INFO* res) const
+{
+ const std::shared_ptr<CNativeWindow> nativeWindow = CXBMCApp::Get().GetNativeWindow(30000);
+ if (!nativeWindow)
+ return false;
+
+ if (!m_width || !m_height)
+ {
+ m_width = nativeWindow->GetWidth();
+ m_height = nativeWindow->GetHeight();
+ CLog::Log(LOGINFO, "CAndroidUtils: window resolution: {}x{}", m_width, m_height);
+ }
+
+ if (s_hasModeApi)
+ {
+ *res = s_res_cur_displayMode;
+ res->iWidth = m_width;
+ res->iHeight = m_height;
+ }
+ else
+ {
+ res->strId = "-1";
+ res->fRefreshRate = currentRefreshRate();
+ res->dwFlags = D3DPRESENTFLAG_PROGRESSIVE;
+ res->bFullScreen = true;
+ res->iWidth = m_width;
+ res->iHeight = m_height;
+ res->fPixelRatio = 1.0f;
+ res->iScreenWidth = res->iWidth;
+ res->iScreenHeight = res->iHeight;
+ }
+ res->iSubtitles = res->iHeight;
+ res->strMode =
+ StringUtils::Format("{}x{} @ {:.6f}{} - Full Screen", res->iScreenWidth, res->iScreenHeight,
+ res->fRefreshRate, res->dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : "");
+ CLog::Log(LOGINFO, "CAndroidUtils: Current resolution: {}x{} {}", res->iWidth, res->iHeight,
+ res->strMode);
+ return true;
+}
+
+bool CAndroidUtils::SetNativeResolution(const RESOLUTION_INFO& res)
+{
+ CLog::Log(LOGINFO, "CAndroidUtils: SetNativeResolution: {}: {}x{} {}x{}@{:f}", res.strId,
+ res.iWidth, res.iHeight, res.iScreenWidth, res.iScreenHeight, res.fRefreshRate);
+
+ if (s_hasModeApi)
+ {
+ CXBMCApp::Get().SetDisplayMode(std::atoi(res.strId.c_str()), res.fRefreshRate);
+ s_res_cur_displayMode = res;
+ }
+ else
+ CXBMCApp::Get().SetRefreshRate(res.fRefreshRate);
+
+ CXBMCApp::Get().SetBuffersGeometry(res.iWidth, res.iHeight, 0);
+
+ return true;
+}
+
+bool CAndroidUtils::ProbeResolutions(std::vector<RESOLUTION_INFO>& resolutions)
+{
+ RESOLUTION_INFO cur_res;
+ bool ret = GetNativeResolution(&cur_res);
+
+ CLog::Log(LOGDEBUG, "CAndroidUtils: ProbeResolutions: {}x{}", m_width, m_height);
+
+ if (s_hasModeApi)
+ {
+ for (RESOLUTION_INFO res : s_res_displayModes)
+ {
+ if (m_width && m_height)
+ {
+ res.iWidth = std::min(res.iWidth, m_width);
+ res.iHeight = std::min(res.iHeight, m_height);
+ res.iSubtitles = res.iHeight;
+ }
+ resolutions.push_back(res);
+ }
+ return true;
+ }
+
+ if (ret && cur_res.iWidth > 1 && cur_res.iHeight > 1)
+ {
+ std::vector<float> refreshRates;
+ CJNIWindow window = CXBMCApp::getWindow();
+ if (window)
+ {
+ CJNIView view = window.getDecorView();
+ if (view)
+ {
+ CJNIDisplay display = view.getDisplay();
+ if (display)
+ {
+ refreshRates = display.getSupportedRefreshRates();
+ }
+ }
+
+ if (!refreshRates.empty())
+ {
+ for (unsigned int i = 0; i < refreshRates.size(); i++)
+ {
+ if (refreshRates[i] < 20.0f)
+ continue;
+ cur_res.fRefreshRate = refreshRates[i];
+ cur_res.strMode = StringUtils::Format(
+ "{}x{} @ {:.6f}{} - Full Screen", cur_res.iScreenWidth, cur_res.iScreenHeight,
+ cur_res.fRefreshRate, cur_res.dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : "");
+ resolutions.push_back(cur_res);
+ }
+ }
+ }
+ if (resolutions.empty())
+ {
+ /* No valid refresh rates available, just provide the current one */
+ resolutions.push_back(cur_res);
+ }
+ return true;
+ }
+ return false;
+}
+
+bool CAndroidUtils::UpdateDisplayModes()
+{
+ if (CJNIBase::GetSDKVersion() >= 23)
+ fetchDisplayModes();
+ return true;
+}
+
+bool CAndroidUtils::IsHDRDisplay()
+{
+ CJNIWindow window = CXBMCApp::getWindow();
+ bool ret = false;
+
+ if (window)
+ {
+ CJNIView view = window.getDecorView();
+ if (view)
+ {
+ CJNIDisplay display = view.getDisplay();
+ if (display)
+ ret = display.isHdr();
+ }
+ }
+ CLog::Log(LOGDEBUG, "CAndroidUtils: IsHDRDisplay: {}", ret ? "true" : "false");
+ return ret;
+}
+
+void CAndroidUtils::OnSettingChanged(const std::shared_ptr<const CSetting>& setting)
+{
+ const std::string& settingId = setting->GetId();
+ /* Calibration (overscan / subtitles) are based on GUI size -> reset required */
+ if (settingId == CAndroidUtils::SETTING_LIMITGUI)
+ CDisplaySettings::GetInstance().ClearCalibrations();
+}
+
+std::vector<int> CAndroidUtils::GetDisplaySupportedHdrTypes()
+{
+ CJNIWindow window = CXBMCApp::getWindow();
+
+ if (window)
+ {
+ CJNIView view = window.getDecorView();
+ if (view)
+ {
+ CJNIDisplay display = view.getDisplay();
+ if (display)
+ {
+ CJNIDisplayHdrCapabilities caps = display.getHdrCapabilities();
+ if (caps)
+ return caps.getSupportedHdrTypes();
+ }
+ }
+ }
+
+ return {};
+}
+
+void CAndroidUtils::LogDisplaySupportedHdrTypes()
+{
+ const std::vector<int> hdrTypes = GetDisplaySupportedHdrTypes();
+ std::string text;
+
+ for (const int& type : hdrTypes)
+ {
+ text += " " + HdrTypeString(type);
+ }
+
+ CLog::Log(LOGDEBUG, "CAndroidUtils: Display supported HDR types:{}",
+ text.empty() ? " None" : text);
+}
+
+CHDRCapabilities CAndroidUtils::GetDisplayHDRCapabilities()
+{
+ CHDRCapabilities caps;
+ const std::vector<int> types = GetDisplaySupportedHdrTypes();
+
+ if (std::find(types.begin(), types.end(), CAndroidUtils::HDRTypes::HDR10) != types.end())
+ caps.SetHDR10();
+
+ if (std::find(types.begin(), types.end(), CAndroidUtils::HDRTypes::HLG) != types.end())
+ caps.SetHLG();
+
+ if (std::find(types.begin(), types.end(), CAndroidUtils::HDRTypes::HDR10_PLUS) != types.end())
+ caps.SetHDR10Plus();
+
+ if (std::find(types.begin(), types.end(), CAndroidUtils::HDRTypes::DOLBY_VISION) != types.end())
+ caps.SetDolbyVision();
+
+ return caps;
+}
+
+bool CAndroidUtils::SupportsMediaCodecMimeType(const std::string& mimeType)
+{
+ const std::vector<CJNIMediaCodecInfo> codecInfos =
+ CJNIMediaCodecList(CJNIMediaCodecList::REGULAR_CODECS).getCodecInfos();
+
+ for (const CJNIMediaCodecInfo& codec_info : codecInfos)
+ {
+ if (codec_info.isEncoder())
+ continue;
+
+ std::vector<std::string> types = codec_info.getSupportedTypes();
+ if (std::find(types.begin(), types.end(), mimeType) != types.end())
+ return true;
+ }
+
+ return false;
+}
diff --git a/xbmc/windowing/android/AndroidUtils.h b/xbmc/windowing/android/AndroidUtils.h
new file mode 100644
index 0000000..426baee
--- /dev/null
+++ b/xbmc/windowing/android/AndroidUtils.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011-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"
+#include "utils/HDRCapabilities.h"
+#include "windowing/Resolution.h"
+
+#include <string>
+#include <vector>
+
+#include <androidjni/Display.h>
+
+class CAndroidUtils : public ISettingCallback
+{
+public:
+ CAndroidUtils();
+ ~CAndroidUtils() override = default;
+ bool GetNativeResolution(RESOLUTION_INFO* res) const;
+ bool SetNativeResolution(const RESOLUTION_INFO& res);
+ bool ProbeResolutions(std::vector<RESOLUTION_INFO>& resolutions);
+ bool UpdateDisplayModes();
+ bool IsHDRDisplay();
+
+ // Implementation of ISettingCallback
+ static const std::string SETTING_LIMITGUI;
+ void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override;
+
+ static bool SupportsMediaCodecMimeType(const std::string& mimeType);
+
+ // Android specific HDR type mapping
+ // https://developer.android.com/reference/android/view/Display.HdrCapabilities#constants_1
+ enum HDRTypes
+ {
+ DOLBY_VISION = 1,
+ HDR10 = 2,
+ HLG = 3,
+ HDR10_PLUS = 4
+ };
+
+ static std::vector<int> GetDisplaySupportedHdrTypes();
+ static CHDRCapabilities GetDisplayHDRCapabilities();
+
+protected:
+ mutable int m_width;
+ mutable int m_height;
+
+private:
+ static void LogDisplaySupportedHdrTypes();
+};
diff --git a/xbmc/windowing/android/CMakeLists.txt b/xbmc/windowing/android/CMakeLists.txt
new file mode 100644
index 0000000..fd489d1
--- /dev/null
+++ b/xbmc/windowing/android/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(SOURCES OSScreenSaverAndroid.cpp
+ WinEventsAndroid.cpp
+ WinSystemAndroid.cpp
+ AndroidUtils.cpp
+ VideoSyncAndroid.cpp)
+
+set(HEADERS OSScreenSaverAndroid.h
+ WinEventsAndroid.h
+ WinSystemAndroid.h
+ AndroidUtils.h
+ VideoSyncAndroid.h)
+
+if(OPENGLES_FOUND)
+ list(APPEND SOURCES WinSystemAndroidGLESContext.cpp)
+ list(APPEND HEADERS WinSystemAndroidGLESContext.h)
+endif()
+
+core_add_library(windowing_android)
diff --git a/xbmc/windowing/android/OSScreenSaverAndroid.cpp b/xbmc/windowing/android/OSScreenSaverAndroid.cpp
new file mode 100644
index 0000000..aa6dfc5
--- /dev/null
+++ b/xbmc/windowing/android/OSScreenSaverAndroid.cpp
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2018 Chris Browet
+ * 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 "OSScreenSaverAndroid.h"
+
+#include "platform/android/activity/XBMCApp.h"
+
+void COSScreenSaverAndroid::Inhibit()
+{
+ CXBMCApp::Get().EnableWakeLock(true);
+}
+
+void COSScreenSaverAndroid::Uninhibit()
+{
+ CXBMCApp::Get().EnableWakeLock(false);
+}
diff --git a/xbmc/windowing/android/OSScreenSaverAndroid.h b/xbmc/windowing/android/OSScreenSaverAndroid.h
new file mode 100644
index 0000000..1b67d85
--- /dev/null
+++ b/xbmc/windowing/android/OSScreenSaverAndroid.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2018 Chris Browet
+ * 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 "../OSScreenSaver.h"
+
+class COSScreenSaverAndroid : public KODI::WINDOWING::IOSScreenSaver
+{
+public:
+ explicit COSScreenSaverAndroid() = default;
+ void Inhibit() override;
+ void Uninhibit() override;
+};
diff --git a/xbmc/windowing/android/VideoSyncAndroid.cpp b/xbmc/windowing/android/VideoSyncAndroid.cpp
new file mode 100644
index 0000000..cee6f3e
--- /dev/null
+++ b/xbmc/windowing/android/VideoSyncAndroid.cpp
@@ -0,0 +1,78 @@
+/*
+ * 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 "VideoSyncAndroid.h"
+
+#include "ServiceBroker.h"
+#include "cores/VideoPlayer/VideoReferenceClock.h"
+#include "utils/MathUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include "platform/android/activity/XBMCApp.h"
+
+
+bool CVideoSyncAndroid::Setup(PUPDATECLOCK func)
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncAndroid::{} setting up", __FUNCTION__);
+
+ //init the vblank timestamp
+ m_LastVBlankTime = CurrentHostCounter();
+ UpdateClock = func;
+ m_abortEvent.Reset();
+
+ CXBMCApp::Get().InitFrameCallback(this);
+ CServiceBroker::GetWinSystem()->Register(this);
+
+ return true;
+}
+
+void CVideoSyncAndroid::Run(CEvent& stopEvent)
+{
+ XbmcThreads::CEventGroup waitGroup{&stopEvent, &m_abortEvent};
+ waitGroup.wait();
+}
+
+void CVideoSyncAndroid::Cleanup()
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncAndroid::{} cleaning up", __FUNCTION__);
+ CXBMCApp::Get().DeinitFrameCallback();
+ CServiceBroker::GetWinSystem()->Unregister(this);
+}
+
+float CVideoSyncAndroid::GetFps()
+{
+ m_fps = CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS();
+ CLog::Log(LOGDEBUG, "CVideoSyncAndroid::{} Detected refreshrate: {:f} hertz", __FUNCTION__,
+ m_fps);
+ return m_fps;
+}
+
+void CVideoSyncAndroid::OnResetDisplay()
+{
+ m_abortEvent.Set();
+}
+
+void CVideoSyncAndroid::FrameCallback(int64_t frameTimeNanos)
+{
+ int NrVBlanks;
+ double VBlankTime;
+
+ //calculate how many vblanks happened
+ VBlankTime = (double)(frameTimeNanos - m_LastVBlankTime) / (double)CurrentHostFrequency();
+ NrVBlanks = MathUtils::round_int(VBlankTime * static_cast<double>(m_fps));
+
+ //save the timestamp of this vblank so we can calculate how many happened next time
+ m_LastVBlankTime = frameTimeNanos;
+
+ //update the vblank timestamp, update the clock and send a signal that we got a vblank
+ UpdateClock(NrVBlanks, frameTimeNanos, m_refClock);
+}
diff --git a/xbmc/windowing/android/VideoSyncAndroid.h b/xbmc/windowing/android/VideoSyncAndroid.h
new file mode 100644
index 0000000..6bf76c0
--- /dev/null
+++ b/xbmc/windowing/android/VideoSyncAndroid.h
@@ -0,0 +1,34 @@
+/*
+ * 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 CVideoSyncAndroid : public CVideoSync, IDispResource
+{
+public:
+ CVideoSyncAndroid(void *clock) : CVideoSync(clock), m_LastVBlankTime(0) {}
+
+ // CVideoSync interface
+ bool Setup(PUPDATECLOCK func) override;
+ void Run(CEvent& stop) override;
+ void Cleanup() override;
+ float GetFps() override;
+
+ // IDispResource interface
+ void OnResetDisplay() override;
+
+ // Choreographer callback
+ void FrameCallback(int64_t frameTimeNanos);
+
+private:
+ int64_t m_LastVBlankTime; //timestamp of the last vblank, used for calculating how many vblanks happened
+ CEvent m_abortEvent;
+};
diff --git a/xbmc/windowing/android/WinEventsAndroid.cpp b/xbmc/windowing/android/WinEventsAndroid.cpp
new file mode 100644
index 0000000..a384902
--- /dev/null
+++ b/xbmc/windowing/android/WinEventsAndroid.cpp
@@ -0,0 +1,191 @@
+/*
+ * 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 "WinEventsAndroid.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/InputManager.h"
+#include "input/XBMC_vkeys.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+#define ALMOST_ZERO 0.125f
+enum {
+ EVENT_STATE_TEST,
+ EVENT_STATE_HOLD,
+ EVENT_STATE_REPEAT
+};
+
+/************************************************************************/
+/************************************************************************/
+static bool different_event(XBMC_Event &curEvent, XBMC_Event &newEvent)
+{
+ // different type
+ if (curEvent.type != newEvent.type)
+ return true;
+
+ return false;
+}
+
+/************************************************************************/
+/************************************************************************/
+CWinEventsAndroid::CWinEventsAndroid()
+: CThread("CWinEventsAndroid")
+{
+ CLog::Log(LOGDEBUG, "CWinEventsAndroid::CWinEventsAndroid");
+ Create();
+}
+
+CWinEventsAndroid::~CWinEventsAndroid()
+{
+ m_bStop = true;
+ StopThread(true);
+}
+
+void CWinEventsAndroid::MessagePush(XBMC_Event *newEvent)
+{
+ std::unique_lock<CCriticalSection> lock(m_eventsCond);
+
+ m_events.push_back(*newEvent);
+}
+
+void CWinEventsAndroid::MessagePushRepeat(XBMC_Event *repeatEvent)
+{
+ std::unique_lock<CCriticalSection> lock(m_eventsCond);
+
+ std::list<XBMC_Event>::iterator itt;
+ for (itt = m_events.begin(); itt != m_events.end(); ++itt)
+ {
+ // we have events pending, if we we just
+ // repush, we might push the repeat event
+ // in back of a canceling non-active event.
+ // do not repush if pending are different event.
+ if (different_event(*itt, *repeatEvent))
+ return;
+ }
+
+ // is a repeat, push it
+ m_events.push_back(*repeatEvent);
+}
+
+bool CWinEventsAndroid::MessagePump()
+{
+ bool ret = false;
+
+ // 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;
+ {
+ std::unique_lock<CCriticalSection> lock(m_eventsCond);
+ if (m_events.empty())
+ return ret;
+ pumpEvent = m_events.front();
+ m_events.pop_front();
+ }
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ ret |= appPort->OnEvent(pumpEvent);
+
+ if (pumpEvent.type == XBMC_MOUSEBUTTONUP)
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_UNFOCUS_ALL, 0, 0, 0, 0);
+ }
+
+ return ret;
+}
+
+size_t CWinEventsAndroid::GetQueueSize()
+{
+ std::unique_lock<CCriticalSection> lock(m_eventsCond);
+ return m_events.size();
+}
+
+void CWinEventsAndroid::Process()
+{
+ uint32_t timeout = 10;
+ uint32_t holdTimeout = 500;
+ uint32_t repeatTimeout = 100;
+ uint32_t repeatDuration = 0;
+
+ XBMC_Event cur_event;
+ int state = EVENT_STATE_TEST;
+ while (!m_bStop)
+ {
+ // run a 10ms (timeout) wait cycle
+ CThread::Sleep(std::chrono::milliseconds(timeout));
+
+ std::unique_lock<CCriticalSection> lock(m_lasteventCond);
+
+ switch(state)
+ {
+ default:
+ case EVENT_STATE_TEST:
+ // non-active event, eat it
+ if (!m_lastevent.empty())
+ m_lastevent.pop();
+ break;
+
+ case EVENT_STATE_HOLD:
+ repeatDuration += timeout;
+ if (!m_lastevent.empty())
+ {
+ if (different_event(cur_event, m_lastevent.front()))
+ {
+ // different event, cycle back to test
+ state = EVENT_STATE_TEST;
+ break;
+ }
+
+ // same event, eat it
+ m_lastevent.pop();
+ }
+
+ if (repeatDuration >= holdTimeout)
+ {
+ CLog::Log(LOGDEBUG, "hold ->repeat, size({}), repeatDuration({})", m_lastevent.size(),
+ repeatDuration);
+ state = EVENT_STATE_REPEAT;
+ }
+ break;
+
+ case EVENT_STATE_REPEAT:
+ repeatDuration += timeout;
+ if (!m_lastevent.empty())
+ {
+ if (different_event(cur_event, m_lastevent.front()))
+ {
+ // different event, cycle back to test
+ state = EVENT_STATE_TEST;
+ break;
+ }
+
+ // same event, eat it
+ m_lastevent.pop();
+ }
+
+ if (repeatDuration >= holdTimeout)
+ {
+ // this is a repeat, push it
+ MessagePushRepeat(&cur_event);
+ // assuming holdTimeout > repeatTimeout,
+ // just subtract the repeatTimeout
+ // to get the next cycle time
+ repeatDuration -= repeatTimeout;
+ }
+ break;
+ }
+ }
+}
diff --git a/xbmc/windowing/android/WinEventsAndroid.h b/xbmc/windowing/android/WinEventsAndroid.h
new file mode 100644
index 0000000..62b04b5
--- /dev/null
+++ b/xbmc/windowing/android/WinEventsAndroid.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "windowing/WinEvents.h"
+
+#include <list>
+#include <queue>
+#include <string>
+#include <vector>
+
+class CWinEventsAndroid : public IWinEvents, public CThread
+{
+public:
+ CWinEventsAndroid();
+ ~CWinEventsAndroid() override;
+
+ void MessagePush(XBMC_Event *newEvent);
+ void MessagePushRepeat(XBMC_Event *repeatEvent);
+ bool MessagePump() override;
+
+private:
+ size_t GetQueueSize();
+
+ // for CThread
+ void Process() override;
+
+ CCriticalSection m_eventsCond;
+ std::list<XBMC_Event> m_events;
+
+ CCriticalSection m_lasteventCond;
+ std::queue<XBMC_Event> m_lastevent;
+};
+
diff --git a/xbmc/windowing/android/WinSystemAndroid.cpp b/xbmc/windowing/android/WinSystemAndroid.cpp
new file mode 100644
index 0000000..d8ddbb0
--- /dev/null
+++ b/xbmc/windowing/android/WinSystemAndroid.cpp
@@ -0,0 +1,329 @@
+/*
+ * 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 "WinSystemAndroid.h"
+
+#include "OSScreenSaverAndroid.h"
+#include "ServiceBroker.h"
+#include "WinEventsAndroid.h"
+#include "addons/interfaces/platform/android/System.h"
+#include "cores/RetroPlayer/process/android/RPProcessInfoAndroid.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h"
+#include "cores/VideoPlayer/DVDCodecs/Audio/DVDAudioCodecAndroidMediaCodec.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecAndroidMediaCodec.h"
+#include "cores/VideoPlayer/Process/android/ProcessInfoAndroid.h"
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodec.h"
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererMediaCodecSurface.h"
+#include "guilib/DispResource.h"
+#include "rendering/gles/ScreenshotSurfaceGLES.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "system_egl.h"
+#include "utils/HDRCapabilities.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/Resolution.h"
+
+#include "platform/android/activity/XBMCApp.h"
+#include "platform/android/media/decoderfilter/MediaCodecDecoderFilterManager.h"
+#include "platform/android/media/drm/MediaDrmCryptoSession.h"
+
+#include <float.h>
+#include <mutex>
+#include <string.h>
+
+#include <EGL/eglplatform.h>
+
+using namespace KODI;
+using namespace std::chrono_literals;
+
+CWinSystemAndroid::CWinSystemAndroid()
+{
+ m_displayWidth = 0;
+ m_displayHeight = 0;
+
+ m_stereo_mode = RENDER_STEREO_MODE_OFF;
+
+ m_dispResetTimer = new CTimer(this);
+
+ m_android = nullptr;
+
+ m_winEvents.reset(new CWinEventsAndroid());
+}
+
+CWinSystemAndroid::~CWinSystemAndroid()
+{
+ delete m_dispResetTimer;
+}
+
+bool CWinSystemAndroid::InitWindowSystem()
+{
+ m_nativeDisplay = EGL_DEFAULT_DISPLAY;
+
+ m_android = new CAndroidUtils();
+
+ m_decoderFilterManager = new(CMediaCodecDecoderFilterManager);
+ CServiceBroker::RegisterDecoderFilterManager(m_decoderFilterManager);
+
+ CDVDVideoCodecAndroidMediaCodec::Register();
+ CDVDAudioCodecAndroidMediaCodec::Register();
+
+ CLinuxRendererGLES::Register();
+ RETRO::CRPProcessInfoAndroid::Register();
+ RETRO::CRPProcessInfoAndroid::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGLES);
+ CRendererMediaCodec::Register();
+ CRendererMediaCodecSurface::Register();
+ ADDON::Interface_Android::Register();
+ DRM::CMediaDrmCryptoSession::Register();
+ VIDEOPLAYER::CProcessInfoAndroid::Register();
+
+ CScreenshotSurfaceGLES::Register();
+
+ return CWinSystemBase::InitWindowSystem();
+}
+
+bool CWinSystemAndroid::DestroyWindowSystem()
+{
+ CLog::Log(LOGINFO, "CWinSystemAndroid::{}", __FUNCTION__);
+
+ delete m_android;
+ m_android = nullptr;
+
+ CServiceBroker::RegisterDecoderFilterManager(nullptr);
+ delete m_decoderFilterManager;
+ m_decoderFilterManager = nullptr;
+
+ return true;
+}
+
+bool CWinSystemAndroid::CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res)
+{
+ RESOLUTION_INFO current_resolution;
+ current_resolution.iWidth = current_resolution.iHeight = 0;
+ RENDER_STEREO_MODE stereo_mode = CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode();
+
+ m_nWidth = res.iWidth;
+ m_nHeight = res.iHeight;
+ m_displayWidth = res.iScreenWidth;
+ m_displayHeight = res.iScreenHeight;
+ m_fRefreshRate = res.fRefreshRate;
+
+ if ((m_bWindowCreated && m_android->GetNativeResolution(&current_resolution)) &&
+ current_resolution.iWidth == res.iWidth && current_resolution.iHeight == res.iHeight &&
+ current_resolution.iScreenWidth == res.iScreenWidth && current_resolution.iScreenHeight == res.iScreenHeight &&
+ m_bFullScreen == fullScreen && current_resolution.fRefreshRate == res.fRefreshRate &&
+ (current_resolution.dwFlags & D3DPRESENTFLAG_MODEMASK) == (res.dwFlags & D3DPRESENTFLAG_MODEMASK) &&
+ m_stereo_mode == stereo_mode)
+ {
+ CLog::Log(LOGDEBUG, "CWinSystemAndroid::CreateNewWindow: No need to create a new window");
+ return true;
+ }
+
+ m_dispResetTimer->Stop();
+ m_HdmiModeTriggered = false;
+
+ m_stereo_mode = stereo_mode;
+ m_bFullScreen = fullScreen;
+
+ m_nativeWindow = CXBMCApp::Get().GetNativeWindow(2000);
+ if (!m_nativeWindow)
+ {
+ CLog::Log(LOGERROR, "CWinSystemAndroid::CreateNewWindow: failed");
+ return false;
+ }
+
+ m_android->SetNativeResolution(res);
+
+ return true;
+}
+
+bool CWinSystemAndroid::DestroyWindow()
+{
+ CLog::Log(LOGINFO, "CWinSystemAndroid::{}", __FUNCTION__);
+ m_nativeWindow.reset();
+ m_bWindowCreated = false;
+ return true;
+}
+
+void CWinSystemAndroid::UpdateResolutions()
+{
+ UpdateResolutions(true);
+}
+
+void CWinSystemAndroid::UpdateResolutions(bool bUpdateDesktopRes)
+{
+ CWinSystemBase::UpdateResolutions();
+
+ std::vector<RESOLUTION_INFO> resolutions;
+ if (!m_android->ProbeResolutions(resolutions) || resolutions.empty())
+ {
+ CLog::Log(LOGWARNING, "CWinSystemAndroid::{} failed.", __FUNCTION__);
+ }
+
+ const RESOLUTION_INFO resWindow = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW);
+
+ RESOLUTION_INFO resDesktop;
+ if (bUpdateDesktopRes)
+ {
+ // ProbeResolutions includes already all resolutions.
+ // Only get desktop resolution so we can replace Kodi's desktop res.
+ RESOLUTION_INFO curDisplay;
+ if (m_android->GetNativeResolution(&curDisplay))
+ resDesktop = curDisplay;
+ }
+ else
+ {
+ // Do not replace Kodi's desktop res, just update the data.
+ resDesktop = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP);
+ }
+
+ CDisplaySettings::GetInstance().ClearCustomResolutions();
+
+ for (auto& res : resolutions)
+ {
+ CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res);
+ CDisplaySettings::GetInstance().AddResolutionInfo(res);
+
+ if (resDesktop.iScreenWidth == res.iScreenWidth &&
+ resDesktop.iScreenHeight == res.iScreenHeight &&
+ (resDesktop.dwFlags & D3DPRESENTFLAG_MODEMASK) == (res.dwFlags & D3DPRESENTFLAG_MODEMASK) &&
+ std::fabs(resDesktop.fRefreshRate - res.fRefreshRate) < FLT_EPSILON)
+ {
+ CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP) = res;
+ }
+
+ if (resWindow.iScreenWidth == res.iScreenWidth &&
+ resWindow.iScreenHeight == res.iScreenHeight &&
+ (resWindow.dwFlags & D3DPRESENTFLAG_MODEMASK) == (res.dwFlags & D3DPRESENTFLAG_MODEMASK) &&
+ std::fabs(resWindow.fRefreshRate - res.fRefreshRate) < FLT_EPSILON)
+ {
+ CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW) = res;
+ }
+ }
+
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+}
+
+void CWinSystemAndroid::OnTimeout()
+{
+ m_HdmiModeTriggered = true;
+}
+
+void CWinSystemAndroid::InitiateModeChange()
+{
+ auto delay =
+ std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ "videoscreen.delayrefreshchange") *
+ 100);
+
+ if (delay < 2000ms)
+ delay = 2000ms;
+ m_dispResetTimer->Stop();
+ m_dispResetTimer->Start(delay);
+
+ SetHdmiState(false);
+}
+
+void CWinSystemAndroid::SetHdmiState(bool connected)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ CLog::Log(LOGDEBUG, "CWinSystemAndroid::SetHdmiState: state: {}", static_cast<int>(connected));
+
+ if (connected)
+ {
+ if (m_dispResetTimer->IsRunning())
+ {
+ // We stop the timer if OS supports HDMI_AUDIO_PLUG intent
+ // and configured delay is smaller than the time HDMI_PLUG took.
+ // Note that timer is always started with minimum of 2 seconds
+ // regardless if the configured delay is smaller
+ if (m_dispResetTimer->GetElapsedMilliseconds() >=
+ CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ "videoscreen.delayrefreshchange") *
+ 100)
+ m_dispResetTimer->Stop();
+ else
+ return;
+ }
+
+ for (auto resource : m_resources)
+ resource->OnResetDisplay();
+ m_dispResetTimer->Stop();
+ m_HdmiModeTriggered = false;
+ }
+ else
+ {
+ for (auto resource : m_resources)
+ resource->OnLostDisplay();
+ }
+}
+
+void CWinSystemAndroid::UpdateDisplayModes()
+{
+ // re-fetch display modes
+ m_android->UpdateDisplayModes();
+
+ if (m_nativeWindow)
+ {
+ // update display settings
+ UpdateResolutions(false);
+ }
+}
+
+bool CWinSystemAndroid::Hide()
+{
+ return false;
+}
+
+bool CWinSystemAndroid::Show(bool raise)
+{
+ return false;
+}
+
+void CWinSystemAndroid::Register(IDispResource *resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ m_resources.push_back(resource);
+}
+
+void CWinSystemAndroid::Unregister(IDispResource *resource)
+{
+ std::unique_lock<CCriticalSection> 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 CWinSystemAndroid::MessagePush(XBMC_Event *newEvent)
+{
+ dynamic_cast<CWinEventsAndroid&>(*m_winEvents).MessagePush(newEvent);
+}
+
+bool CWinSystemAndroid::MessagePump()
+{
+ return m_winEvents->MessagePump();
+}
+
+bool CWinSystemAndroid::IsHDRDisplay()
+{
+ return m_android->IsHDRDisplay();
+}
+
+std::unique_ptr<WINDOWING::IOSScreenSaver> CWinSystemAndroid::GetOSScreenSaverImpl()
+{
+ std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> ret(new COSScreenSaverAndroid());
+ return ret;
+}
+
+CHDRCapabilities CWinSystemAndroid::GetDisplayHDRCapabilities() const
+{
+ return CAndroidUtils::GetDisplayHDRCapabilities();
+}
diff --git a/xbmc/windowing/android/WinSystemAndroid.h b/xbmc/windowing/android/WinSystemAndroid.h
new file mode 100644
index 0000000..0e39193
--- /dev/null
+++ b/xbmc/windowing/android/WinSystemAndroid.h
@@ -0,0 +1,85 @@
+/*
+ * 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 "AndroidUtils.h"
+#include "rendering/gles/RenderSystemGLES.h"
+#include "system_egl.h"
+#include "threads/CriticalSection.h"
+#include "threads/Timer.h"
+#include "utils/HDRCapabilities.h"
+#include "windowing/WinSystem.h"
+
+#include <memory>
+
+class CDecoderFilterManager;
+class IDispResource;
+class CNativeWindow;
+
+class CWinSystemAndroid : public CWinSystemBase, public ITimerCallback
+{
+public:
+ CWinSystemAndroid();
+ ~CWinSystemAndroid() override;
+
+ bool InitWindowSystem() override;
+ bool DestroyWindowSystem() override;
+
+ bool CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res) override;
+
+ bool DestroyWindow() override;
+ void UpdateResolutions() override;
+
+ void InitiateModeChange();
+ bool IsHdmiModeTriggered() const { return m_HdmiModeTriggered; }
+ void SetHdmiState(bool connected);
+
+ void UpdateDisplayModes();
+
+ bool HasCursor() override { return false; }
+
+ bool Hide() override;
+ bool Show(bool raise = true) override;
+ void Register(IDispResource *resource) override;
+ void Unregister(IDispResource *resource) override;
+
+ void MessagePush(XBMC_Event *newEvent);
+
+ // winevents override
+ bool MessagePump() override;
+ bool IsHDRDisplay() override;
+
+ CHDRCapabilities GetDisplayHDRCapabilities() const override;
+
+protected:
+ std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override;
+ void OnTimeout() override;
+
+ CAndroidUtils *m_android;
+
+ EGLDisplay m_nativeDisplay = EGL_NO_DISPLAY;
+ std::shared_ptr<CNativeWindow> m_nativeWindow;
+
+ int m_displayWidth;
+ int m_displayHeight;
+
+ RENDER_STEREO_MODE m_stereo_mode;
+
+ CTimer *m_dispResetTimer;
+
+ CCriticalSection m_resourceSection;
+ std::vector<IDispResource*> m_resources;
+ CDecoderFilterManager *m_decoderFilterManager;
+
+private:
+ bool m_HdmiModeTriggered = false;
+ void UpdateResolutions(bool bUpdateDesktopRes);
+};
diff --git a/xbmc/windowing/android/WinSystemAndroidGLESContext.cpp b/xbmc/windowing/android/WinSystemAndroidGLESContext.cpp
new file mode 100644
index 0000000..ac562ae
--- /dev/null
+++ b/xbmc/windowing/android/WinSystemAndroidGLESContext.cpp
@@ -0,0 +1,270 @@
+/*
+ * 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 "WinSystemAndroidGLESContext.h"
+
+#include "ServiceBroker.h"
+#include "VideoSyncAndroid.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodec.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SingleLock.h"
+#include "utils/log.h"
+#include "windowing/WindowSystemFactory.h"
+
+#include "platform/android/activity/XBMCApp.h"
+
+#include <EGL/eglext.h>
+#include <unistd.h>
+
+#include "PlatformDefs.h"
+
+void CWinSystemAndroidGLESContext::Register()
+{
+ KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem);
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemAndroidGLESContext::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemAndroidGLESContext>();
+}
+
+bool CWinSystemAndroidGLESContext::InitWindowSystem()
+{
+ if (!CWinSystemAndroid::InitWindowSystem())
+ {
+ return false;
+ }
+
+ if (!m_pGLContext.CreateDisplay(m_nativeDisplay))
+ {
+ return false;
+ }
+
+ if (!m_pGLContext.InitializeDisplay(EGL_OPENGL_ES_API))
+ {
+ return false;
+ }
+
+ if (!m_pGLContext.ChooseConfig(EGL_OPENGL_ES2_BIT))
+ {
+ return false;
+ }
+
+ m_hasHDRConfig = m_pGLContext.ChooseConfig(EGL_OPENGL_ES2_BIT, 0, true);
+
+ m_hasEGL_BT2020_PQ_Colorspace_Extension =
+ CEGLUtils::HasExtension(m_pGLContext.GetEGLDisplay(), "EGL_EXT_gl_colorspace_bt2020_pq");
+ m_hasEGL_ST2086_Extension =
+ CEGLUtils::HasExtension(m_pGLContext.GetEGLDisplay(), "EGL_EXT_surface_SMPTE2086_metadata");
+
+ bool hasEGLHDRExtensions = m_hasEGL_BT2020_PQ_Colorspace_Extension && m_hasEGL_ST2086_Extension;
+
+ CLog::Log(LOGDEBUG,
+ "CWinSystemAndroidGLESContext::InitWindowSystem: HDRConfig: {}, HDRExtensions: {}",
+ static_cast<int>(m_hasHDRConfig), static_cast<int>(hasEGLHDRExtensions));
+
+ CEGLAttributesVec contextAttribs;
+ contextAttribs.Add({{EGL_CONTEXT_CLIENT_VERSION, 2}});
+
+ if (!m_pGLContext.CreateContext(contextAttribs))
+ {
+ return false;
+ }
+
+ return true;
+}
+
+bool CWinSystemAndroidGLESContext::CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res)
+{
+ m_pGLContext.DestroySurface();
+
+ if (!CWinSystemAndroid::CreateNewWindow(name, fullScreen, res))
+ {
+ return false;
+ }
+
+ if (!CreateSurface())
+ {
+ return false;
+ }
+
+ if (!m_pGLContext.BindContext())
+ {
+ return false;
+ }
+
+ return true;
+}
+
+bool CWinSystemAndroidGLESContext::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ CRenderSystemGLES::ResetRenderSystem(newWidth, newHeight);
+ return true;
+}
+
+bool CWinSystemAndroidGLESContext::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ CreateNewWindow("", fullScreen, res);
+ CRenderSystemGLES::ResetRenderSystem(res.iWidth, res.iHeight);
+ return true;
+}
+
+void CWinSystemAndroidGLESContext::SetVSyncImpl(bool enable)
+{
+ // We use Choreographer for timing
+ m_pGLContext.SetVSync(false);
+}
+
+void CWinSystemAndroidGLESContext::PresentRenderImpl(bool rendered)
+{
+ if (!m_nativeWindow)
+ {
+ usleep(10000);
+ return;
+ }
+
+ // Mode change finalization was triggered by timer
+ if (IsHdmiModeTriggered())
+ SetHdmiState(true);
+
+ // Ignore EGL_BAD_SURFACE: It seems to happen during/after mode changes, but
+ // we can't actually do anything about it
+ if (rendered && !m_pGLContext.TrySwapBuffers())
+ CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed");
+
+ CXBMCApp::Get().WaitVSync(1000);
+}
+
+float CWinSystemAndroidGLESContext::GetFrameLatencyAdjustment()
+{
+ return CXBMCApp::Get().GetFrameLatencyMs();
+}
+
+EGLDisplay CWinSystemAndroidGLESContext::GetEGLDisplay() const
+{
+ return m_pGLContext.GetEGLDisplay();
+}
+
+EGLSurface CWinSystemAndroidGLESContext::GetEGLSurface() const
+{
+ return m_pGLContext.GetEGLSurface();
+}
+
+EGLContext CWinSystemAndroidGLESContext::GetEGLContext() const
+{
+ return m_pGLContext.GetEGLContext();
+}
+
+EGLConfig CWinSystemAndroidGLESContext::GetEGLConfig() const
+{
+ return m_pGLContext.GetEGLConfig();
+}
+
+std::unique_ptr<CVideoSync> CWinSystemAndroidGLESContext::GetVideoSync(void *clock)
+{
+ std::unique_ptr<CVideoSync> pVSync(new CVideoSyncAndroid(clock));
+ return pVSync;
+}
+
+bool CWinSystemAndroidGLESContext::CreateSurface()
+{
+ if (!m_pGLContext.CreateSurface(static_cast<EGLNativeWindowType>(m_nativeWindow->m_window),
+ m_HDRColorSpace))
+ {
+ if (m_HDRColorSpace != EGL_NONE)
+ {
+ m_HDRColorSpace = EGL_NONE;
+ m_displayMetadata = nullptr;
+ m_lightMetadata = nullptr;
+ if (!m_pGLContext.CreateSurface(static_cast<EGLNativeWindowType>(m_nativeWindow->m_window)))
+ return false;
+ }
+ else
+ return false;
+ }
+
+#if EGL_EXT_surface_SMPTE2086_metadata
+ if (m_displayMetadata)
+ {
+ m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_RX_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[0][0]) * EGL_METADATA_SCALING_EXT + 0.5));
+ m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_RY_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[0][1]) * EGL_METADATA_SCALING_EXT + 0.5));
+ m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_GX_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[1][0]) * EGL_METADATA_SCALING_EXT + 0.5));
+ m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_GY_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[1][1]) * EGL_METADATA_SCALING_EXT + 0.5));
+ m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_BX_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[2][0]) * EGL_METADATA_SCALING_EXT + 0.5));
+ m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_DISPLAY_PRIMARY_BY_EXT, static_cast<int>(av_q2d(m_displayMetadata->display_primaries[2][1]) * EGL_METADATA_SCALING_EXT + 0.5));
+ m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_WHITE_POINT_X_EXT, static_cast<int>(av_q2d(m_displayMetadata->white_point[0]) * EGL_METADATA_SCALING_EXT + 0.5));
+ m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_WHITE_POINT_Y_EXT, static_cast<int>(av_q2d(m_displayMetadata->white_point[1]) * EGL_METADATA_SCALING_EXT + 0.5));
+ m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_MAX_LUMINANCE_EXT, static_cast<int>(av_q2d(m_displayMetadata->max_luminance) * EGL_METADATA_SCALING_EXT + 0.5));
+ m_pGLContext.SurfaceAttrib(EGL_SMPTE2086_MIN_LUMINANCE_EXT, static_cast<int>(av_q2d(m_displayMetadata->min_luminance) * EGL_METADATA_SCALING_EXT + 0.5));
+ }
+ if (m_lightMetadata)
+ {
+ m_pGLContext.SurfaceAttrib(EGL_CTA861_3_MAX_CONTENT_LIGHT_LEVEL_EXT, static_cast<int>(m_lightMetadata->MaxCLL * EGL_METADATA_SCALING_EXT));
+ m_pGLContext.SurfaceAttrib(EGL_CTA861_3_MAX_FRAME_AVERAGE_LEVEL_EXT, static_cast<int>(m_lightMetadata->MaxFALL * EGL_METADATA_SCALING_EXT));
+ }
+#endif
+ return true;
+}
+
+bool CWinSystemAndroidGLESContext::IsHDRDisplay()
+{
+ return m_hasHDRConfig && (m_hasEGL_BT2020_PQ_Colorspace_Extension || m_hasEGL_ST2086_Extension) &&
+ CWinSystemAndroid::IsHDRDisplay();
+}
+
+bool CWinSystemAndroidGLESContext::SetHDR(const VideoPicture* videoPicture)
+{
+ if (!CWinSystemAndroid::IsHDRDisplay() || !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(SETTING_WINSYSTEM_IS_HDR_DISPLAY))
+ return false;
+
+ EGLint HDRColorSpace = 0;
+
+#if EGL_EXT_gl_colorspace_bt2020_linear
+ if (m_hasHDRConfig && m_hasEGL_BT2020_PQ_Colorspace_Extension && m_hasEGL_ST2086_Extension)
+ {
+ HDRColorSpace = EGL_NONE;
+ if (videoPicture && videoPicture->hasDisplayMetadata)
+ {
+ switch (videoPicture->color_space)
+ {
+ case AVCOL_SPC_BT2020_NCL:
+ case AVCOL_SPC_BT2020_CL:
+ case AVCOL_SPC_BT709:
+ HDRColorSpace = EGL_GL_COLORSPACE_BT2020_PQ_EXT;
+ break;
+ default:
+ m_displayMetadata = nullptr;
+ m_lightMetadata = nullptr;
+ }
+ }
+ else
+ {
+ m_displayMetadata = nullptr;
+ m_lightMetadata = nullptr;
+ }
+
+ if (HDRColorSpace != m_HDRColorSpace)
+ {
+ CLog::Log(LOGDEBUG, "CWinSystemAndroidGLESContext::SetHDR: ColorSpace: {}", HDRColorSpace);
+
+ m_HDRColorSpace = HDRColorSpace;
+ m_displayMetadata = m_HDRColorSpace == EGL_NONE ? nullptr : std::unique_ptr<AVMasteringDisplayMetadata>(new AVMasteringDisplayMetadata(videoPicture->displayMetadata));
+ // TODO: discuss with NVIDIA why this prevent turning HDR display off
+ //m_lightMetadata = !videoPicture || m_HDRColorSpace == EGL_NONE ? nullptr : std::unique_ptr<AVContentLightMetadata>(new AVContentLightMetadata(videoPicture->lightMetadata));
+ m_pGLContext.DestroySurface();
+ CreateSurface();
+ m_pGLContext.BindContext();
+ }
+ }
+#endif
+
+ return m_HDRColorSpace == HDRColorSpace;
+}
diff --git a/xbmc/windowing/android/WinSystemAndroidGLESContext.h b/xbmc/windowing/android/WinSystemAndroidGLESContext.h
new file mode 100644
index 0000000..b04ff41
--- /dev/null
+++ b/xbmc/windowing/android/WinSystemAndroidGLESContext.h
@@ -0,0 +1,63 @@
+/*
+ * 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 "WinSystemAndroid.h"
+#include "rendering/gles/RenderSystemGLES.h"
+#include "utils/EGLUtils.h"
+#include "utils/GlobalsHandling.h"
+
+struct AVMasteringDisplayMetadata;
+struct AVContentLightMetadata;
+
+class CWinSystemAndroidGLESContext : public CWinSystemAndroid, public CRenderSystemGLES
+{
+public:
+ CWinSystemAndroidGLESContext() = default;
+ ~CWinSystemAndroidGLESContext() override = default;
+
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystemBase via CWinSystemAndroid
+ CRenderSystemBase *GetRenderSystem() override { return this; }
+ bool InitWindowSystem() override;
+ bool CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res) override;
+
+ bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+
+ std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override;
+
+ float GetFrameLatencyAdjustment() override;
+ bool IsHDRDisplay() override;
+ bool SetHDR(const VideoPicture* videoPicture) override;
+
+ EGLDisplay GetEGLDisplay() const;
+ EGLSurface GetEGLSurface() const;
+ EGLContext GetEGLContext() const;
+ EGLConfig GetEGLConfig() const;
+protected:
+ void SetVSyncImpl(bool enable) override;
+ void PresentRenderImpl(bool rendered) override;
+
+private:
+ bool CreateSurface();
+
+ CEGLContextUtils m_pGLContext;
+ bool m_hasHDRConfig = false;
+
+ std::unique_ptr<AVMasteringDisplayMetadata> m_displayMetadata;
+ std::unique_ptr<AVContentLightMetadata> m_lightMetadata;
+ EGLint m_HDRColorSpace = EGL_NONE;
+ bool m_hasEGL_ST2086_Extension = false;
+ bool m_hasEGL_BT2020_PQ_Colorspace_Extension = false;
+};
diff --git a/xbmc/windowing/gbm/CMakeLists.txt b/xbmc/windowing/gbm/CMakeLists.txt
new file mode 100644
index 0000000..254b2db
--- /dev/null
+++ b/xbmc/windowing/gbm/CMakeLists.txt
@@ -0,0 +1,26 @@
+add_subdirectory(drm)
+
+set(SOURCES OptionalsReg.cpp
+ WinSystemGbm.cpp
+ VideoSyncGbm.cpp
+ GBMUtils.cpp
+ WinSystemGbmEGLContext.cpp
+ GBMDPMSSupport.cpp)
+
+set(HEADERS OptionalsReg.h
+ WinSystemGbm.h
+ VideoSyncGbm.h
+ GBMUtils.h
+ WinSystemGbmEGLContext.h
+ GBMDPMSSupport.h)
+
+if (OPENGL_FOUND)
+ list(APPEND SOURCES WinSystemGbmGLContext.cpp)
+ list(APPEND HEADERS WinSystemGbmGLContext.h)
+endif()
+if(OPENGLES_FOUND)
+ list(APPEND SOURCES WinSystemGbmGLESContext.cpp)
+ list(APPEND HEADERS WinSystemGbmGLESContext.h)
+endif()
+
+core_add_library(windowing_gbm)
diff --git a/xbmc/windowing/gbm/GBMDPMSSupport.cpp b/xbmc/windowing/gbm/GBMDPMSSupport.cpp
new file mode 100644
index 0000000..6544587
--- /dev/null
+++ b/xbmc/windowing/gbm/GBMDPMSSupport.cpp
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2009-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 "GBMDPMSSupport.h"
+
+#include "ServiceBroker.h"
+#include "windowing/gbm/WinSystemGbm.h"
+
+using namespace KODI::WINDOWING::GBM;
+
+CGBMDPMSSupport::CGBMDPMSSupport()
+{
+ m_supportedModes.push_back(OFF);
+}
+
+bool CGBMDPMSSupport::EnablePowerSaving(PowerSavingMode mode)
+{
+ auto winSystem = dynamic_cast<CWinSystemGbm*>(CServiceBroker::GetWinSystem());
+ if (!winSystem)
+ return false;
+
+ switch (mode)
+ {
+ case OFF:
+ return winSystem->Hide();
+ default:
+ return false;
+ }
+}
+
+bool CGBMDPMSSupport::DisablePowerSaving()
+{
+ auto winSystem = dynamic_cast<CWinSystemGbm*>(CServiceBroker::GetWinSystem());
+ if (!winSystem)
+ return false;
+
+ return winSystem->Show();
+}
diff --git a/xbmc/windowing/gbm/GBMDPMSSupport.h b/xbmc/windowing/gbm/GBMDPMSSupport.h
new file mode 100644
index 0000000..f5fabf8
--- /dev/null
+++ b/xbmc/windowing/gbm/GBMDPMSSupport.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2009-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 "powermanagement/DPMSSupport.h"
+
+#include <memory>
+
+class CGBMDPMSSupport : public CDPMSSupport
+{
+public:
+ CGBMDPMSSupport();
+ ~CGBMDPMSSupport() override = default;
+
+protected:
+ bool EnablePowerSaving(PowerSavingMode mode) override;
+ bool DisablePowerSaving() override;
+};
diff --git a/xbmc/windowing/gbm/GBMUtils.cpp b/xbmc/windowing/gbm/GBMUtils.cpp
new file mode 100644
index 0000000..5267c93
--- /dev/null
+++ b/xbmc/windowing/gbm/GBMUtils.cpp
@@ -0,0 +1,107 @@
+/*
+ * 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 "GBMUtils.h"
+
+#include "utils/log.h"
+
+#include <mutex>
+
+using namespace KODI::WINDOWING::GBM;
+
+namespace
+{
+std::once_flag flag;
+}
+
+bool CGBMUtils::CreateDevice(int fd)
+{
+ auto device = gbm_create_device(fd);
+ if (!device)
+ {
+ CLog::Log(LOGERROR, "CGBMUtils::{} - failed to create device: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ m_device.reset(new CGBMDevice(device));
+
+ return true;
+}
+
+CGBMUtils::CGBMDevice::CGBMDevice(gbm_device* device) : m_device(device)
+{
+}
+
+bool CGBMUtils::CGBMDevice::CreateSurface(
+ int width, int height, uint32_t format, const uint64_t* modifiers, const int modifiers_count)
+{
+ gbm_surface* surface{nullptr};
+#if defined(HAS_GBM_MODIFIERS)
+ if (modifiers)
+ {
+ surface = gbm_surface_create_with_modifiers(m_device, width, height, format, modifiers,
+ modifiers_count);
+ }
+#endif
+ if (!surface)
+ {
+ surface = gbm_surface_create(m_device, width, height, format,
+ GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
+ }
+
+ if (!surface)
+ {
+ CLog::Log(LOGERROR, "CGBMUtils::{} - failed to create surface: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "CGBMUtils::{} - created surface with size {}x{}", __FUNCTION__, width,
+ height);
+
+ m_surface.reset(new CGBMSurface(surface));
+
+ return true;
+}
+
+CGBMUtils::CGBMDevice::CGBMSurface::CGBMSurface(gbm_surface* surface) : m_surface(surface)
+{
+}
+
+CGBMUtils::CGBMDevice::CGBMSurface::CGBMSurfaceBuffer* CGBMUtils::CGBMDevice::CGBMSurface::
+ LockFrontBuffer()
+{
+ m_buffers.emplace(std::make_unique<CGBMSurfaceBuffer>(m_surface));
+
+ if (!static_cast<bool>(gbm_surface_has_free_buffers(m_surface)))
+ {
+ /*
+ * We want to use call_once here because we want it to be logged the first time that
+ * we have to release buffers. This means that the maximum amount of buffers had been reached.
+ * For mesa this should be 4 buffers but it may vary across other implementations.
+ */
+ std::call_once(
+ flag, [this]() { CLog::Log(LOGDEBUG, "CGBMUtils - using {} buffers", m_buffers.size()); });
+
+ m_buffers.pop();
+ }
+
+ return m_buffers.back().get();
+}
+
+CGBMUtils::CGBMDevice::CGBMSurface::CGBMSurfaceBuffer::CGBMSurfaceBuffer(gbm_surface* surface)
+ : m_surface(surface), m_buffer(gbm_surface_lock_front_buffer(surface))
+{
+}
+
+CGBMUtils::CGBMDevice::CGBMSurface::CGBMSurfaceBuffer::~CGBMSurfaceBuffer()
+{
+ if (m_surface && m_buffer)
+ gbm_surface_release_buffer(m_surface, m_buffer);
+}
diff --git a/xbmc/windowing/gbm/GBMUtils.h b/xbmc/windowing/gbm/GBMUtils.h
new file mode 100644
index 0000000..291a93a
--- /dev/null
+++ b/xbmc/windowing/gbm/GBMUtils.h
@@ -0,0 +1,177 @@
+/*
+ * 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 <memory>
+#include <queue>
+
+#include <gbm.h>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+/**
+ * @brief A wrapper for gbm c classes to allow OOP and RAII.
+ *
+ */
+class CGBMUtils
+{
+public:
+ CGBMUtils(const CGBMUtils&) = delete;
+ CGBMUtils& operator=(const CGBMUtils&) = delete;
+ CGBMUtils() = default;
+ ~CGBMUtils() = default;
+
+ /**
+ * @brief Create a gbm device for allocating buffers
+ *
+ * @param fd The file descriptor for a backend device
+ * @return true The device creation succeeded
+ * @return false The device creation failed
+ */
+ bool CreateDevice(int fd);
+
+ /**
+ * @brief A wrapper for gbm_device to allow OOP and RAII
+ *
+ */
+ class CGBMDevice
+ {
+ public:
+ CGBMDevice(const CGBMDevice&) = delete;
+ CGBMDevice& operator=(const CGBMDevice&) = delete;
+ explicit CGBMDevice(gbm_device* device);
+ ~CGBMDevice() = default;
+
+ /**
+ * @brief Create a gbm surface
+ *
+ * @param width The width to use for the surface
+ * @param height The height to use for the surface
+ * @param format The format to use for the surface
+ * @param modifiers The modifiers to use for the surface
+ * @param modifiers_count The amount of modifiers in the modifiers param
+ * @return true The surface creation succeeded
+ * @return false The surface creation failed
+ */
+ bool CreateSurface(int width,
+ int height,
+ uint32_t format,
+ const uint64_t* modifiers,
+ const int modifiers_count);
+
+ /**
+ * @brief Get the underlying gbm_device
+ *
+ * @return gbm_device* A pointer to the underlying gbm_device
+ */
+ gbm_device* Get() const { return m_device; }
+
+ /**
+ * @brief A wrapper for gbm_surface to allow OOP and RAII
+ *
+ */
+ class CGBMSurface
+ {
+ public:
+ CGBMSurface(const CGBMSurface&) = delete;
+ CGBMSurface& operator=(const CGBMSurface&) = delete;
+ explicit CGBMSurface(gbm_surface* surface);
+ ~CGBMSurface() = default;
+
+ /**
+ * @brief Get the underlying gbm_surface
+ *
+ * @return gbm_surface* A pointer to the underlying gbm_surface
+ */
+ gbm_surface* Get() const { return m_surface; }
+
+ /**
+ * @brief A wrapper for gbm_bo to allow OOP and RAII
+ *
+ */
+ class CGBMSurfaceBuffer
+ {
+ public:
+ CGBMSurfaceBuffer(const CGBMSurfaceBuffer&) = delete;
+ CGBMSurfaceBuffer& operator=(const CGBMSurfaceBuffer&) = delete;
+ explicit CGBMSurfaceBuffer(gbm_surface* surface);
+ ~CGBMSurfaceBuffer();
+
+ /**
+ * @brief Get the underlying gbm_bo
+ *
+ * @return gbm_bo* A pointer to the underlying gbm_bo
+ */
+ gbm_bo* Get() const { return m_buffer; }
+
+ private:
+ gbm_surface* m_surface{nullptr};
+ gbm_bo* m_buffer{nullptr};
+ };
+
+ /**
+ * @brief Lock the surface's current front buffer.
+ *
+ * @return CGBMSurfaceBuffer* A pointer to a CGBMSurfaceBuffer object
+ */
+ CGBMSurfaceBuffer* LockFrontBuffer();
+
+ private:
+ gbm_surface* m_surface{nullptr};
+ std::queue<std::unique_ptr<CGBMSurfaceBuffer>> m_buffers;
+ };
+
+ /**
+ * @brief Get the CGBMSurface object
+ *
+ * @return CGBMSurface* A pointer to the CGBMSurface object
+ */
+ CGBMDevice::CGBMSurface* GetSurface() const { return m_surface.get(); }
+
+ private:
+ gbm_device* m_device{nullptr};
+
+ struct CGBMSurfaceDeleter
+ {
+ void operator()(CGBMSurface* p) const
+ {
+ if (p)
+ gbm_surface_destroy(p->Get());
+ }
+ };
+ std::unique_ptr<CGBMSurface, CGBMSurfaceDeleter> m_surface;
+ };
+
+ /**
+ * @brief Get the CGBMDevice object
+ *
+ * @return CGBMDevice* A pointer to the CGBMDevice object
+ */
+ CGBMUtils::CGBMDevice* GetDevice() const { return m_device.get(); }
+
+private:
+ struct CGBMDeviceDeleter
+ {
+ void operator()(CGBMDevice* p) const
+ {
+ if (p)
+ gbm_device_destroy(p->Get());
+ }
+ };
+ std::unique_ptr<CGBMDevice, CGBMDeviceDeleter> m_device;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/gbm/OptionalsReg.cpp b/xbmc/windowing/gbm/OptionalsReg.cpp
new file mode 100644
index 0000000..9f5076f
--- /dev/null
+++ b/xbmc/windowing/gbm/OptionalsReg.cpp
@@ -0,0 +1,139 @@
+/*
+ * 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 "OptionalsReg.h"
+
+//-----------------------------------------------------------------------------
+// VAAPI
+//-----------------------------------------------------------------------------
+#if defined (HAVE_LIBVA)
+#include <va/va_drm.h>
+#include "cores/VideoPlayer/DVDCodecs/Video/VAAPI.h"
+#if defined(HAS_GL)
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.h"
+#endif
+#if defined(HAS_GLES)
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.h"
+#endif
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CVaapiProxy : public VAAPI::IVaapiWinSystem
+{
+public:
+ CVaapiProxy(int fd) : m_fd(fd) {};
+ virtual ~CVaapiProxy() = default;
+ VADisplay GetVADisplay() override;
+ void *GetEGLDisplay() override { return eglDisplay; };
+
+ VADisplay vaDpy;
+ void *eglDisplay;
+
+private:
+ int m_fd{-1};
+};
+
+VADisplay CVaapiProxy::GetVADisplay()
+{
+ return vaGetDisplayDRM(m_fd);
+}
+
+CVaapiProxy* VaapiProxyCreate(int fd)
+{
+ return new CVaapiProxy(fd);
+}
+
+void VaapiProxyDelete(CVaapiProxy *proxy)
+{
+ delete proxy;
+}
+
+void VaapiProxyConfig(CVaapiProxy *proxy, void *eglDpy)
+{
+ proxy->vaDpy = proxy->GetVADisplay();
+ proxy->eglDisplay = eglDpy;
+}
+
+void VAAPIRegister(CVaapiProxy *winSystem, bool deepColor)
+{
+ VAAPI::CDecoder::Register(winSystem, deepColor);
+}
+
+#if defined(HAS_GL)
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+ CRendererVAAPIGL::Register(winSystem, winSystem->vaDpy, winSystem->eglDisplay, general,
+ deepColor);
+}
+#endif
+
+#if defined(HAS_GLES)
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+ CRendererVAAPIGLES::Register(winSystem, winSystem->vaDpy, winSystem->eglDisplay, general,
+ deepColor);
+}
+#endif
+}
+}
+}
+
+#else
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CVaapiProxy
+{
+};
+
+CVaapiProxy* VaapiProxyCreate(int fd)
+{
+ return nullptr;
+}
+
+void VaapiProxyDelete(CVaapiProxy *proxy)
+{
+}
+
+void VaapiProxyConfig(CVaapiProxy *proxy, void *eglDpy)
+{
+
+}
+
+void VAAPIRegister(CVaapiProxy *winSystem, bool deepColor)
+{
+
+}
+
+#if defined(HAS_GL)
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+
+}
+#endif
+
+#if defined(HAS_GLES)
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+}
+#endif
+}
+}
+}
+
+#endif
diff --git a/xbmc/windowing/gbm/OptionalsReg.h b/xbmc/windowing/gbm/OptionalsReg.h
new file mode 100644
index 0000000..ee459f6
--- /dev/null
+++ b/xbmc/windowing/gbm/OptionalsReg.h
@@ -0,0 +1,36 @@
+/*
+ * 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
+
+
+//-----------------------------------------------------------------------------
+// VAAPI
+//-----------------------------------------------------------------------------
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+class CVaapiProxy;
+
+CVaapiProxy* VaapiProxyCreate(int fd);
+void VaapiProxyDelete(CVaapiProxy *proxy);
+void VaapiProxyConfig(CVaapiProxy *proxy, void *eglDpy);
+void VAAPIRegister(CVaapiProxy *winSystem, bool deepColor);
+#if defined(HAS_GL)
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor);
+#endif
+#if defined(HAS_GLES)
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor);
+#endif
+}
+}
+}
diff --git a/xbmc/windowing/gbm/VideoLayerBridge.h b/xbmc/windowing/gbm/VideoLayerBridge.h
new file mode 100644
index 0000000..7070567
--- /dev/null
+++ b/xbmc/windowing/gbm/VideoLayerBridge.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 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
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CVideoLayerBridge
+{
+public:
+ virtual ~CVideoLayerBridge() = default;
+ virtual void Disable() {}
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/gbm/VideoSyncGbm.cpp b/xbmc/windowing/gbm/VideoSyncGbm.cpp
new file mode 100644
index 0000000..d113c90
--- /dev/null
+++ b/xbmc/windowing/gbm/VideoSyncGbm.cpp
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2005-2021 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 "VideoSyncGbm.h"
+
+#include "ServiceBroker.h"
+#include "threads/Thread.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+#include "windowing/gbm/WinSystemGbm.h"
+#include "xf86drm.h"
+#include "xf86drmMode.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include <unistd.h>
+
+CVideoSyncGbm::CVideoSyncGbm(void* clock)
+ : CVideoSync(clock), m_winSystem(CServiceBroker::GetWinSystem())
+{
+ if (!m_winSystem)
+ throw std::runtime_error("window system not available");
+}
+
+bool CVideoSyncGbm::Setup(PUPDATECLOCK func)
+{
+ UpdateClock = func;
+ m_abort = false;
+ m_winSystem->Register(this);
+ CLog::Log(LOGDEBUG, "CVideoSyncGbm::{} setting up", __FUNCTION__);
+
+ auto winSystemGbm = dynamic_cast<KODI::WINDOWING::GBM::CWinSystemGbm*>(m_winSystem);
+ if (!winSystemGbm)
+ {
+ CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: failed to get winSystem", __FUNCTION__);
+ return false;
+ }
+
+ auto drm = winSystemGbm->GetDrm();
+ if (!drm)
+ {
+ CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: failed to get drm", __FUNCTION__);
+ return false;
+ }
+
+ auto crtc = drm->GetCrtc();
+ if (!crtc)
+ {
+ CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: failed to get crtc", __FUNCTION__);
+ return false;
+ }
+
+ uint64_t ns = 0;
+ m_crtcId = crtc->GetCrtcId();
+ m_fd = drm->GetFileDescriptor();
+ int s = drmCrtcGetSequence(m_fd, m_crtcId, &m_sequence, &ns);
+ m_offset = CurrentHostCounter() - ns;
+ if (s != 0)
+ {
+ CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: drmCrtcGetSequence failed ({})", __FUNCTION__, s);
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CVideoSyncGbm::{}: opened (fd:{} crtc:{} seq:{} ns:{}:{})", __FUNCTION__,
+ m_fd, m_crtcId, m_sequence, ns, m_offset + ns);
+ return true;
+}
+
+void CVideoSyncGbm::Run(CEvent& stopEvent)
+{
+ /* This shouldn't be very busy and timing is important so increase priority */
+ CThread::GetCurrentThread()->SetPriority(ThreadPriority::ABOVE_NORMAL);
+
+ if (m_fd < 0)
+ {
+ CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: failed to open device ({})", __FUNCTION__, m_fd);
+ return;
+ }
+ CLog::Log(LOGDEBUG, "CVideoSyncGbm::{}: started {}", __FUNCTION__, m_fd);
+
+ while (!stopEvent.Signaled() && !m_abort)
+ {
+ uint64_t sequence = 0, ns = 0;
+ usleep(1000);
+ int s = drmCrtcGetSequence(m_fd, m_crtcId, &sequence, &ns);
+ if (s != 0)
+ {
+ CLog::Log(LOGWARNING, "CVideoSyncGbm::{}: drmCrtcGetSequence failed ({})", __FUNCTION__, s);
+ break;
+ }
+
+ if (sequence == m_sequence)
+ continue;
+
+ UpdateClock(sequence - m_sequence, m_offset + ns, m_refClock);
+ m_sequence = sequence;
+ }
+}
+
+void CVideoSyncGbm::Cleanup()
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncGbm::{}: cleaning up", __FUNCTION__);
+ m_winSystem->Unregister(this);
+}
+
+float CVideoSyncGbm::GetFps()
+{
+ m_fps = m_winSystem->GetGfxContext().GetFPS();
+ CLog::Log(LOGDEBUG, "CVideoSyncGbm::{}: fps:{}", __FUNCTION__, m_fps);
+ return m_fps;
+}
+
+void CVideoSyncGbm::OnResetDisplay()
+{
+ m_abort = true;
+}
+
+void CVideoSyncGbm::RefreshChanged()
+{
+ if (m_fps != m_winSystem->GetGfxContext().GetFPS())
+ m_abort = true;
+}
diff --git a/xbmc/windowing/gbm/VideoSyncGbm.h b/xbmc/windowing/gbm/VideoSyncGbm.h
new file mode 100644
index 0000000..610d988
--- /dev/null
+++ b/xbmc/windowing/gbm/VideoSyncGbm.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2005-2021 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"
+
+#include <atomic>
+
+class CWinSystemBase;
+
+class CVideoSyncGbm : public CVideoSync, IDispResource
+{
+public:
+ explicit CVideoSyncGbm(void* clock);
+ CVideoSyncGbm() = delete;
+ ~CVideoSyncGbm() override = default;
+
+ // CVideoSync overrides
+ bool Setup(PUPDATECLOCK func) override;
+ void Run(CEvent& stopEvent) override;
+ void Cleanup() override;
+ float GetFps() override;
+ void RefreshChanged() override;
+
+ // IDispResource overrides
+ void OnResetDisplay() override;
+
+private:
+ int m_fd = -1;
+ uint32_t m_crtcId = 0;
+ uint64_t m_sequence = 0;
+ uint64_t m_offset = 0;
+ std::atomic<bool> m_abort{false};
+
+ CWinSystemBase* m_winSystem;
+};
diff --git a/xbmc/windowing/gbm/WinSystemGbm.cpp b/xbmc/windowing/gbm/WinSystemGbm.cpp
new file mode 100644
index 0000000..4fd2da4
--- /dev/null
+++ b/xbmc/windowing/gbm/WinSystemGbm.cpp
@@ -0,0 +1,445 @@
+/*
+ * 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 "WinSystemGbm.h"
+
+#include "GBMDPMSSupport.h"
+#include "OptionalsReg.h"
+#include "ServiceBroker.h"
+#include "VideoSyncGbm.h"
+#include "cores/VideoPlayer/Buffers/VideoBufferDRMPRIME.h"
+#include "drm/DRMAtomic.h"
+#include "drm/DRMLegacy.h"
+#include "drm/OffScreenModeSetting.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <mutex>
+#include <string.h>
+
+#ifndef HAVE_HDR_OUTPUT_METADATA
+// HDR structs is copied from linux include/linux/hdmi.h
+struct hdr_metadata_infoframe
+{
+ uint8_t eotf;
+ uint8_t metadata_type;
+ struct
+ {
+ uint16_t x, y;
+ } display_primaries[3];
+ struct
+ {
+ uint16_t x, y;
+ } white_point;
+ uint16_t max_display_mastering_luminance;
+ uint16_t min_display_mastering_luminance;
+ uint16_t max_cll;
+ uint16_t max_fall;
+};
+struct hdr_output_metadata
+{
+ uint32_t metadata_type;
+ union
+ {
+ struct hdr_metadata_infoframe hdmi_metadata_type1;
+ };
+};
+#endif
+
+using namespace KODI::WINDOWING::GBM;
+
+using namespace std::chrono_literals;
+
+CWinSystemGbm::CWinSystemGbm() :
+ m_DRM(nullptr),
+ m_GBM(new CGBMUtils),
+ m_libinput(new CLibInputHandler)
+{
+ m_dpms = std::make_shared<CGBMDPMSSupport>();
+ m_libinput->Start();
+}
+
+bool CWinSystemGbm::InitWindowSystem()
+{
+ const char* x11 = getenv("DISPLAY");
+ const char* wayland = getenv("WAYLAND_DISPLAY");
+ if (x11 || wayland)
+ {
+ CLog::Log(LOGDEBUG, "CWinSystemGbm::{} - not allowed to run GBM under a window manager",
+ __FUNCTION__);
+ return false;
+ }
+
+ m_DRM = std::make_shared<CDRMAtomic>();
+
+ if (!m_DRM->InitDrm())
+ {
+ CLog::Log(LOGERROR, "CWinSystemGbm::{} - failed to initialize Atomic DRM", __FUNCTION__);
+ m_DRM.reset();
+
+ m_DRM = std::make_shared<CDRMLegacy>();
+
+ if (!m_DRM->InitDrm())
+ {
+ CLog::Log(LOGERROR, "CWinSystemGbm::{} - failed to initialize Legacy DRM", __FUNCTION__);
+ m_DRM.reset();
+
+ m_DRM = std::make_shared<COffScreenModeSetting>();
+ if (!m_DRM->InitDrm())
+ {
+ CLog::Log(LOGERROR, "CWinSystemGbm::{} - failed to initialize off screen DRM",
+ __FUNCTION__);
+ m_DRM.reset();
+ return false;
+ }
+ }
+ }
+
+ if (!m_GBM->CreateDevice(m_DRM->GetFileDescriptor()))
+ {
+ m_GBM.reset();
+ return false;
+ }
+
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return false;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return false;
+
+ auto setting = settings->GetSetting(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE);
+ if (setting)
+ setting->SetVisible(true);
+
+ setting = settings->GetSetting("videoscreen.limitguisize");
+ if (setting)
+ setting->SetVisible(true);
+
+ CLog::Log(LOGDEBUG, "CWinSystemGbm::{} - initialized DRM", __FUNCTION__);
+ return CWinSystemBase::InitWindowSystem();
+}
+
+bool CWinSystemGbm::DestroyWindowSystem()
+{
+ CLog::Log(LOGDEBUG, "CWinSystemGbm::{} - deinitialized DRM", __FUNCTION__);
+
+ m_libinput.reset();
+
+ return true;
+}
+
+void CWinSystemGbm::UpdateResolutions()
+{
+ RESOLUTION_INFO current = m_DRM->GetCurrentMode();
+
+ auto resolutions = m_DRM->GetModes();
+ if (resolutions.empty())
+ {
+ CLog::Log(LOGWARNING, "CWinSystemGbm::{} - Failed to get resolutions", __FUNCTION__);
+ }
+ else
+ {
+ CDisplaySettings::GetInstance().ClearCustomResolutions();
+
+ for (auto &res : resolutions)
+ {
+ CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res);
+ CDisplaySettings::GetInstance().AddResolutionInfo(res);
+
+ if (current.iScreenWidth == res.iScreenWidth &&
+ current.iScreenHeight == res.iScreenHeight &&
+ current.iWidth == res.iWidth &&
+ current.iHeight == res.iHeight &&
+ current.fRefreshRate == res.fRefreshRate &&
+ current.dwFlags == res.dwFlags)
+ {
+ CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP) = res;
+ }
+
+ CLog::Log(LOGINFO, "Found resolution {}x{} with {}x{}{} @ {:f} Hz", res.iWidth, res.iHeight,
+ res.iScreenWidth, res.iScreenHeight,
+ res.dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : "", res.fRefreshRate);
+ }
+ }
+
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+}
+
+bool CWinSystemGbm::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ return true;
+}
+
+bool CWinSystemGbm::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ // Notify other subsystems that we will change resolution
+ OnLostDevice();
+
+ if(!m_DRM->SetMode(res))
+ {
+ CLog::Log(LOGERROR, "CWinSystemGbm::{} - failed to set DRM mode", __FUNCTION__);
+ return false;
+ }
+
+ struct gbm_bo *bo = nullptr;
+
+ if (!std::dynamic_pointer_cast<CDRMAtomic>(m_DRM))
+ {
+ bo = m_GBM->GetDevice()->GetSurface()->LockFrontBuffer()->Get();
+ }
+
+ auto result = m_DRM->SetVideoMode(res, bo);
+
+ auto delay =
+ std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ "videoscreen.delayrefreshchange") *
+ 100);
+ if (delay > 0ms)
+ m_dispResetTimer.Set(delay);
+
+ return result;
+}
+
+bool CWinSystemGbm::DisplayHardwareScalingEnabled()
+{
+ auto drmAtomic = std::dynamic_pointer_cast<CDRMAtomic>(m_DRM);
+ if (drmAtomic && drmAtomic->DisplayHardwareScalingEnabled())
+ return true;
+
+ return false;
+}
+
+void CWinSystemGbm::UpdateDisplayHardwareScaling(const RESOLUTION_INFO& resInfo)
+{
+ if (!DisplayHardwareScalingEnabled())
+ return;
+
+ //! @todo The PR that made the res struct constant was abandoned due to drama.
+ // It should be const-corrected and changed here.
+ RESOLUTION_INFO& resMutable = const_cast<RESOLUTION_INFO&>(resInfo);
+
+ SetFullScreen(true, resMutable, false);
+}
+
+void CWinSystemGbm::FlipPage(bool rendered, bool videoLayer)
+{
+ if (m_videoLayerBridge && !videoLayer)
+ {
+ // disable video plane when video layer no longer is active
+ m_videoLayerBridge->Disable();
+ }
+
+ struct gbm_bo *bo = nullptr;
+
+ if (rendered)
+ {
+ bo = m_GBM->GetDevice()->GetSurface()->LockFrontBuffer()->Get();
+ }
+
+ m_DRM->FlipPage(bo, rendered, videoLayer);
+
+ if (m_videoLayerBridge && !videoLayer)
+ {
+ // delete video layer bridge when video layer no longer is active
+ m_videoLayerBridge.reset();
+ }
+}
+
+bool CWinSystemGbm::UseLimitedColor()
+{
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE);
+}
+
+bool CWinSystemGbm::Hide()
+{
+ bool ret = m_DRM->SetActive(false);
+ FlipPage(false, false);
+ return ret;
+}
+
+bool CWinSystemGbm::Show(bool raise)
+{
+ bool ret = m_DRM->SetActive(true);
+ FlipPage(false, false);
+ return ret;
+}
+
+void CWinSystemGbm::Register(IDispResource *resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ m_resources.push_back(resource);
+}
+
+void CWinSystemGbm::Unregister(IDispResource *resource)
+{
+ std::unique_lock<CCriticalSection> 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 CWinSystemGbm::OnLostDevice()
+{
+ CLog::Log(LOGDEBUG, "{} - notify display change event", __FUNCTION__);
+ m_dispReset = true;
+
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ for (auto resource : m_resources)
+ resource->OnLostDisplay();
+}
+
+std::unique_ptr<CVideoSync> CWinSystemGbm::GetVideoSync(void* clock)
+{
+ return std::make_unique<CVideoSyncGbm>(clock);
+}
+
+std::vector<std::string> CWinSystemGbm::GetConnectedOutputs()
+{
+ return m_DRM->GetConnectedConnectorNames();
+}
+
+bool CWinSystemGbm::SetHDR(const VideoPicture* videoPicture)
+{
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return false;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return false;
+
+ if (!settings->GetBool(SETTING_WINSYSTEM_IS_HDR_DISPLAY))
+ return false;
+
+ auto drm = std::dynamic_pointer_cast<CDRMAtomic>(m_DRM);
+ if (!drm)
+ return false;
+
+ if (!videoPicture)
+ {
+ auto connector = drm->GetConnector();
+ if (connector->SupportsProperty("HDR_OUTPUT_METADATA"))
+ {
+ drm->AddProperty(connector, "HDR_OUTPUT_METADATA", 0);
+ drm->SetActive(true);
+
+ if (m_hdr_blob_id)
+ drmModeDestroyPropertyBlob(drm->GetFileDescriptor(), m_hdr_blob_id);
+ m_hdr_blob_id = 0;
+ }
+
+ return true;
+ }
+
+ auto connector = drm->GetConnector();
+ if (connector->SupportsProperty("HDR_OUTPUT_METADATA"))
+ {
+ hdr_output_metadata hdr_metadata = {};
+
+ hdr_metadata.metadata_type = DRMPRIME::HDMI_STATIC_METADATA_TYPE1;
+ hdr_metadata.hdmi_metadata_type1.eotf = DRMPRIME::GetEOTF(*videoPicture);
+ hdr_metadata.hdmi_metadata_type1.metadata_type = DRMPRIME::HDMI_STATIC_METADATA_TYPE1;
+
+ if (m_hdr_blob_id)
+ drmModeDestroyPropertyBlob(drm->GetFileDescriptor(), m_hdr_blob_id);
+ m_hdr_blob_id = 0;
+
+ if (hdr_metadata.hdmi_metadata_type1.eotf)
+ {
+ const AVMasteringDisplayMetadata* mdmd = DRMPRIME::GetMasteringDisplayMetadata(*videoPicture);
+ if (mdmd && mdmd->has_primaries)
+ {
+ // Convert to unsigned 16-bit values in units of 0.00002,
+ // where 0x0000 represents zero and 0xC350 represents 1.0000
+ for (int i = 0; i < 3; i++)
+ {
+ hdr_metadata.hdmi_metadata_type1.display_primaries[i].x =
+ std::round(av_q2d(mdmd->display_primaries[i][0]) * 50000.0);
+ hdr_metadata.hdmi_metadata_type1.display_primaries[i].y =
+ std::round(av_q2d(mdmd->display_primaries[i][1]) * 50000.0);
+
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - display_primaries[{}].x: {}",
+ __FUNCTION__, i, hdr_metadata.hdmi_metadata_type1.display_primaries[i].x);
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - display_primaries[{}].y: {}",
+ __FUNCTION__, i, hdr_metadata.hdmi_metadata_type1.display_primaries[i].y);
+ }
+ hdr_metadata.hdmi_metadata_type1.white_point.x =
+ std::round(av_q2d(mdmd->white_point[0]) * 50000.0);
+ hdr_metadata.hdmi_metadata_type1.white_point.y =
+ std::round(av_q2d(mdmd->white_point[1]) * 50000.0);
+
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - white_point.x: {}", __FUNCTION__,
+ hdr_metadata.hdmi_metadata_type1.white_point.x);
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - white_point.y: {}", __FUNCTION__,
+ hdr_metadata.hdmi_metadata_type1.white_point.y);
+ }
+ if (mdmd && mdmd->has_luminance)
+ {
+ // Convert to unsigned 16-bit value in units of 1 cd/m2,
+ // where 0x0001 represents 1 cd/m2 and 0xFFFF represents 65535 cd/m2
+ hdr_metadata.hdmi_metadata_type1.max_display_mastering_luminance =
+ std::round(av_q2d(mdmd->max_luminance));
+
+ // Convert to unsigned 16-bit value in units of 0.0001 cd/m2,
+ // where 0x0001 represents 0.0001 cd/m2 and 0xFFFF represents 6.5535 cd/m2
+ hdr_metadata.hdmi_metadata_type1.min_display_mastering_luminance =
+ std::round(av_q2d(mdmd->min_luminance) * 10000.0);
+
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - max_display_mastering_luminance: {}",
+ __FUNCTION__, hdr_metadata.hdmi_metadata_type1.max_display_mastering_luminance);
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - min_display_mastering_luminance: {}",
+ __FUNCTION__, hdr_metadata.hdmi_metadata_type1.min_display_mastering_luminance);
+ }
+
+ const AVContentLightMetadata* clmd = DRMPRIME::GetContentLightMetadata(*videoPicture);
+ if (clmd)
+ {
+ hdr_metadata.hdmi_metadata_type1.max_cll = clmd->MaxCLL;
+ hdr_metadata.hdmi_metadata_type1.max_fall = clmd->MaxFALL;
+
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - max_cll: {}", __FUNCTION__,
+ hdr_metadata.hdmi_metadata_type1.max_cll);
+ CLog::Log(LOGDEBUG, LOGVIDEO, "CWinSystemGbm::{} - max_fall: {}", __FUNCTION__,
+ hdr_metadata.hdmi_metadata_type1.max_fall);
+ }
+
+ drmModeCreatePropertyBlob(drm->GetFileDescriptor(), &hdr_metadata, sizeof(hdr_metadata),
+ &m_hdr_blob_id);
+ }
+
+ drm->AddProperty(connector, "HDR_OUTPUT_METADATA", m_hdr_blob_id);
+ drm->SetActive(true);
+ }
+
+ return true;
+}
+
+bool CWinSystemGbm::IsHDRDisplay()
+{
+ auto drm = std::dynamic_pointer_cast<CDRMAtomic>(m_DRM);
+ if (!drm)
+ return false;
+
+ auto connector = drm->GetConnector();
+ if (!connector)
+ return false;
+
+ //! @todo: improve detection (edid?)
+ // we have no way to know if the display is actually HDR capable and we blindly set the HDR metadata
+ return connector->SupportsProperty("HDR_OUTPUT_METADATA");
+}
diff --git a/xbmc/windowing/gbm/WinSystemGbm.h b/xbmc/windowing/gbm/WinSystemGbm.h
new file mode 100644
index 0000000..b313d33
--- /dev/null
+++ b/xbmc/windowing/gbm/WinSystemGbm.h
@@ -0,0 +1,96 @@
+/*
+ * 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 "VideoLayerBridge.h"
+#include "drm/DRMUtils.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "windowing/WinSystem.h"
+
+#include "platform/linux/input/LibInputHandler.h"
+
+#include <utility>
+
+#include <gbm.h>
+
+class IDispResource;
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CWinSystemGbm : public CWinSystemBase
+{
+public:
+ CWinSystemGbm();
+ ~CWinSystemGbm() override = default;
+
+ const std::string GetName() override { return "gbm"; }
+
+ bool InitWindowSystem() override;
+ bool DestroyWindowSystem() override;
+
+ bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+ bool DisplayHardwareScalingEnabled() override;
+ void UpdateDisplayHardwareScaling(const RESOLUTION_INFO& resInfo) override;
+
+ void FlipPage(bool rendered, bool videoLayer);
+
+ bool CanDoWindowed() override { return false; }
+ void UpdateResolutions() override;
+
+ bool UseLimitedColor() override;
+
+ bool Hide() override;
+ bool Show(bool raise = true) override;
+ void Register(IDispResource* resource) override;
+ void Unregister(IDispResource* resource) override;
+
+ bool SetHDR(const VideoPicture* videoPicture) override;
+ bool IsHDRDisplay() override;
+
+ std::shared_ptr<CVideoLayerBridge> GetVideoLayerBridge() const { return m_videoLayerBridge; }
+ void RegisterVideoLayerBridge(std::shared_ptr<CVideoLayerBridge> bridge)
+ {
+ m_videoLayerBridge = std::move(bridge);
+ };
+
+ CGBMUtils::CGBMDevice* GetGBMDevice() const { return m_GBM->GetDevice(); }
+ std::shared_ptr<CDRMUtils> GetDrm() const { return m_DRM; }
+
+ std::vector<std::string> GetConnectedOutputs() override;
+
+protected:
+ void OnLostDevice();
+
+ std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override;
+
+ std::shared_ptr<CDRMUtils> m_DRM;
+ std::unique_ptr<CGBMUtils> m_GBM;
+ std::shared_ptr<CVideoLayerBridge> m_videoLayerBridge;
+
+ CCriticalSection m_resourceSection;
+ std::vector<IDispResource*> m_resources;
+
+ bool m_dispReset = false;
+ XbmcThreads::EndTime<> m_dispResetTimer;
+ std::unique_ptr<CLibInputHandler> m_libinput;
+
+private:
+ uint32_t m_hdr_blob_id = 0;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/gbm/WinSystemGbmEGLContext.cpp b/xbmc/windowing/gbm/WinSystemGbmEGLContext.cpp
new file mode 100644
index 0000000..83a5941
--- /dev/null
+++ b/xbmc/windowing/gbm/WinSystemGbmEGLContext.cpp
@@ -0,0 +1,141 @@
+/*
+ * 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 "WinSystemGbmEGLContext.h"
+
+#include "OptionalsReg.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "utils/log.h"
+
+using namespace KODI::WINDOWING::GBM;
+using namespace KODI::WINDOWING::LINUX;
+
+bool CWinSystemGbmEGLContext::InitWindowSystemEGL(EGLint renderableType, EGLint apiType)
+{
+ if (!CWinSystemGbm::InitWindowSystem())
+ {
+ return false;
+ }
+
+ if (!m_eglContext.CreatePlatformDisplay(m_GBM->GetDevice()->Get(), m_GBM->GetDevice()->Get()))
+ {
+ return false;
+ }
+
+ if (!m_eglContext.InitializeDisplay(apiType))
+ {
+ return false;
+ }
+
+ auto plane = m_DRM->GetGuiPlane();
+ uint32_t visualId = plane != nullptr ? plane->GetFormat() : DRM_FORMAT_XRGB2101010;
+
+ // prefer alpha visual id, fallback to non-alpha visual id
+ if (!m_eglContext.ChooseConfig(renderableType, CDRMUtils::FourCCWithAlpha(visualId)) &&
+ !m_eglContext.ChooseConfig(renderableType, CDRMUtils::FourCCWithoutAlpha(visualId)))
+ {
+ // fallback to 8bit format if no EGL config was found for 10bit
+ if (plane)
+ plane->SetFormat(DRM_FORMAT_XRGB8888);
+
+ visualId = plane != nullptr ? plane->GetFormat() : DRM_FORMAT_XRGB8888;
+
+ if (!m_eglContext.ChooseConfig(renderableType, CDRMUtils::FourCCWithAlpha(visualId)) &&
+ !m_eglContext.ChooseConfig(renderableType, CDRMUtils::FourCCWithoutAlpha(visualId)))
+ {
+ return false;
+ }
+ }
+
+ if (!CreateContext())
+ {
+ return false;
+ }
+
+ return true;
+}
+
+bool CWinSystemGbmEGLContext::CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res)
+{
+ //Notify other subsystems that we change resolution
+ OnLostDevice();
+
+ if (!DestroyWindow())
+ {
+ return false;
+ }
+
+ if (!m_DRM->SetMode(res))
+ {
+ CLog::Log(LOGERROR, "CWinSystemGbmEGLContext::{} - failed to set DRM mode", __FUNCTION__);
+ return false;
+ }
+
+ uint32_t format = m_eglContext.GetConfigAttrib(EGL_NATIVE_VISUAL_ID);
+
+ std::vector<uint64_t> modifiers;
+
+ auto plane = m_DRM->GetGuiPlane();
+ if (plane)
+ modifiers = plane->GetModifiersForFormat(format);
+
+ if (!m_GBM->GetDevice()->CreateSurface(res.iWidth, res.iHeight, format, modifiers.data(),
+ modifiers.size()))
+ {
+ CLog::Log(LOGERROR, "CWinSystemGbmEGLContext::{} - failed to initialize GBM", __FUNCTION__);
+ return false;
+ }
+
+ // This check + the reinterpret cast is for security reason, if the user has outdated platform header files which often is the case
+ static_assert(sizeof(EGLNativeWindowType) == sizeof(gbm_surface*), "Declaration specifier differs in size");
+
+ if (!m_eglContext.CreatePlatformSurface(
+ m_GBM->GetDevice()->GetSurface()->Get(),
+ reinterpret_cast<khronos_uintptr_t>(m_GBM->GetDevice()->GetSurface()->Get())))
+ {
+ return false;
+ }
+
+ if (!m_eglContext.BindContext())
+ {
+ return false;
+ }
+
+ m_bFullScreen = fullScreen;
+ m_nWidth = res.iWidth;
+ m_nHeight = res.iHeight;
+ m_fRefreshRate = res.fRefreshRate;
+
+ CLog::Log(LOGDEBUG, "CWinSystemGbmEGLContext::{} - initialized GBM", __FUNCTION__);
+ return true;
+}
+
+bool CWinSystemGbmEGLContext::DestroyWindow()
+{
+ m_eglContext.DestroySurface();
+
+ CLog::Log(LOGDEBUG, "CWinSystemGbmEGLContext::{} - deinitialized GBM", __FUNCTION__);
+ return true;
+}
+
+bool CWinSystemGbmEGLContext::DestroyWindowSystem()
+{
+ CDVDFactoryCodec::ClearHWAccels();
+ VIDEOPLAYER::CRendererFactory::ClearRenderer();
+ m_eglContext.Destroy();
+
+ return CWinSystemGbm::DestroyWindowSystem();
+}
+
+void CWinSystemGbmEGLContext::delete_CVaapiProxy::operator()(CVaapiProxy *p) const
+{
+ VaapiProxyDelete(p);
+}
diff --git a/xbmc/windowing/gbm/WinSystemGbmEGLContext.h b/xbmc/windowing/gbm/WinSystemGbmEGLContext.h
new file mode 100644
index 0000000..84f863d
--- /dev/null
+++ b/xbmc/windowing/gbm/WinSystemGbmEGLContext.h
@@ -0,0 +1,58 @@
+/*
+ * 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 "WinSystemGbm.h"
+#include "utils/EGLUtils.h"
+#include "windowing/linux/WinSystemEGL.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CVaapiProxy;
+
+class CWinSystemGbmEGLContext : public KODI::WINDOWING::LINUX::CWinSystemEGL, public CWinSystemGbm
+{
+public:
+ ~CWinSystemGbmEGLContext() override = default;
+
+ bool DestroyWindowSystem() override;
+ bool CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res) override;
+ bool DestroyWindow() override;
+
+protected:
+ CWinSystemGbmEGLContext(EGLenum platform, std::string const& platformExtension)
+ : CWinSystemEGL{platform, platformExtension}
+ {}
+
+ /**
+ * Inheriting classes should override InitWindowSystem() without parameters
+ * and call this function there with appropriate parameters
+ */
+ bool InitWindowSystemEGL(EGLint renderableType, EGLint apiType);
+ virtual bool CreateContext() = 0;
+
+ struct delete_CVaapiProxy
+ {
+ void operator()(CVaapiProxy *p) const;
+ };
+ std::unique_ptr<CVaapiProxy, delete_CVaapiProxy> m_vaapiProxy;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/gbm/WinSystemGbmGLContext.cpp b/xbmc/windowing/gbm/WinSystemGbmGLContext.cpp
new file mode 100644
index 0000000..e4ff49c
--- /dev/null
+++ b/xbmc/windowing/gbm/WinSystemGbmGLContext.cpp
@@ -0,0 +1,174 @@
+/*
+ * 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 "WinSystemGbmGLContext.h"
+
+#include "OptionalsReg.h"
+#include "cores/RetroPlayer/process/gbm/RPProcessInfoGbm.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "rendering/gl/ScreenshotSurfaceGL.h"
+#include "utils/BufferObjectFactory.h"
+#include "utils/DMAHeapBufferObject.h"
+#include "utils/DumbBufferObject.h"
+#include "utils/GBMBufferObject.h"
+#include "utils/UDMABufferObject.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/WindowSystemFactory.h"
+
+#include <mutex>
+
+#include <EGL/eglext.h>
+
+using namespace KODI::WINDOWING::GBM;
+
+using namespace std::chrono_literals;
+
+CWinSystemGbmGLContext::CWinSystemGbmGLContext()
+: CWinSystemGbmEGLContext(EGL_PLATFORM_GBM_MESA, "EGL_MESA_platform_gbm")
+{}
+
+void CWinSystemGbmGLContext::Register()
+{
+ CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "gbm");
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemGbmGLContext::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemGbmGLContext>();
+}
+
+bool CWinSystemGbmGLContext::InitWindowSystem()
+{
+ VIDEOPLAYER::CRendererFactory::ClearRenderer();
+ CDVDFactoryCodec::ClearHWAccels();
+ CLinuxRendererGL::Register();
+ RETRO::CRPProcessInfoGbm::Register();
+ RETRO::CRPProcessInfoGbm::RegisterRendererFactory(new RETRO::CRendererFactoryDMA);
+ RETRO::CRPProcessInfoGbm::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL);
+
+ if (!CWinSystemGbmEGLContext::InitWindowSystemEGL(EGL_OPENGL_BIT, EGL_OPENGL_API))
+ {
+ return false;
+ }
+
+ bool general, deepColor;
+ m_vaapiProxy.reset(VaapiProxyCreate(m_DRM->GetRenderNodeFileDescriptor()));
+ VaapiProxyConfig(m_vaapiProxy.get(), m_eglContext.GetEGLDisplay());
+ VAAPIRegisterRenderGL(m_vaapiProxy.get(), general, deepColor);
+
+ if (general)
+ {
+ VAAPIRegister(m_vaapiProxy.get(), deepColor);
+ }
+
+ CScreenshotSurfaceGL::Register();
+
+ CBufferObjectFactory::ClearBufferObjects();
+ CDumbBufferObject::Register();
+#if defined(HAS_GBM_BO_MAP)
+ CGBMBufferObject::Register();
+#endif
+#if defined(HAVE_LINUX_MEMFD) && defined(HAVE_LINUX_UDMABUF)
+ CUDMABufferObject::Register();
+#endif
+#if defined(HAVE_LINUX_DMA_HEAP)
+ CDMAHeapBufferObject::Register();
+#endif
+
+ return true;
+}
+
+bool CWinSystemGbmGLContext::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ if (res.iWidth != m_nWidth ||
+ res.iHeight != m_nHeight)
+ {
+ CLog::Log(LOGDEBUG, "CWinSystemGbmGLContext::{} - resolution changed, creating a new window",
+ __FUNCTION__);
+ CreateNewWindow("", fullScreen, res);
+ }
+
+ if (!m_eglContext.TrySwapBuffers())
+ {
+ CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed");
+ throw std::runtime_error("eglSwapBuffers failed");
+ }
+
+ CWinSystemGbm::SetFullScreen(fullScreen, res, blankOtherDisplays);
+ CRenderSystemGL::ResetRenderSystem(res.iWidth, res.iHeight);
+
+ return true;
+}
+
+void CWinSystemGbmGLContext::PresentRender(bool rendered, bool videoLayer)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ if (rendered || videoLayer)
+ {
+ if (rendered)
+ {
+ if (!m_eglContext.TrySwapBuffers())
+ {
+ CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed");
+ throw std::runtime_error("eglSwapBuffers failed");
+ }
+ }
+ CWinSystemGbm::FlipPage(rendered, videoLayer);
+
+ if (m_dispReset && m_dispResetTimer.IsTimePast())
+ {
+ CLog::Log(LOGDEBUG, "CWinSystemGbmGLContext::{} - Sending display reset to all clients",
+ __FUNCTION__);
+ m_dispReset = false;
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+
+ for (auto resource : m_resources)
+ resource->OnResetDisplay();
+ }
+ }
+ else
+ {
+ KODI::TIME::Sleep(10ms);
+ }
+}
+
+bool CWinSystemGbmGLContext::CreateContext()
+{
+ const EGLint glMajor = 3;
+ const EGLint glMinor = 2;
+
+ CEGLAttributesVec contextAttribs;
+ contextAttribs.Add({{EGL_CONTEXT_MAJOR_VERSION_KHR, glMajor},
+ {EGL_CONTEXT_MINOR_VERSION_KHR, glMinor},
+ {EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR}});
+
+ if (!m_eglContext.CreateContext(contextAttribs))
+ {
+ CEGLAttributesVec fallbackContextAttribs;
+ fallbackContextAttribs.Add({{EGL_CONTEXT_CLIENT_VERSION, 2}});
+
+ if (!m_eglContext.CreateContext(fallbackContextAttribs))
+ {
+ CLog::Log(LOGERROR, "EGL context creation failed");
+ return false;
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Your OpenGL drivers do not support OpenGL {}.{} core profile. Kodi will run in compatibility mode, but performance may suffer.", glMajor, glMinor);
+ }
+ }
+
+ return true;
+}
diff --git a/xbmc/windowing/gbm/WinSystemGbmGLContext.h b/xbmc/windowing/gbm/WinSystemGbmGLContext.h
new file mode 100644
index 0000000..8994ce6
--- /dev/null
+++ b/xbmc/windowing/gbm/WinSystemGbmGLContext.h
@@ -0,0 +1,48 @@
+/*
+ * 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 "WinSystemGbmEGLContext.h"
+#include "rendering/gl/RenderSystemGL.h"
+#include "utils/EGLUtils.h"
+
+#include <memory>
+
+class CVaapiProxy;
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CWinSystemGbmGLContext : public CWinSystemGbmEGLContext, public CRenderSystemGL
+{
+public:
+ CWinSystemGbmGLContext();
+ ~CWinSystemGbmGLContext() override = default;
+
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystemBase via CWinSystemGbm
+ CRenderSystemBase *GetRenderSystem() override { return this; }
+ bool InitWindowSystem() override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+ void PresentRender(bool rendered, bool videoLayer) override;
+protected:
+ void SetVSyncImpl(bool enable) override {}
+ void PresentRenderImpl(bool rendered) override {};
+ bool CreateContext() override;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/gbm/WinSystemGbmGLESContext.cpp b/xbmc/windowing/gbm/WinSystemGbmGLESContext.cpp
new file mode 100644
index 0000000..0d071c3
--- /dev/null
+++ b/xbmc/windowing/gbm/WinSystemGbmGLESContext.cpp
@@ -0,0 +1,167 @@
+/*
+ * 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 "WinSystemGbmGLESContext.h"
+
+#include "OptionalsReg.h"
+#include "cores/RetroPlayer/process/gbm/RPProcessInfoGbm.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.h"
+#include "cores/VideoPlayer/Process/gbm/ProcessInfoGBM.h"
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIME.h"
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIMEGLES.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "rendering/gles/ScreenshotSurfaceGLES.h"
+#include "utils/BufferObjectFactory.h"
+#include "utils/DMAHeapBufferObject.h"
+#include "utils/DumbBufferObject.h"
+#include "utils/GBMBufferObject.h"
+#include "utils/UDMABufferObject.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/WindowSystemFactory.h"
+
+#include <mutex>
+
+#include <gbm.h>
+
+using namespace KODI::WINDOWING::GBM;
+
+using namespace std::chrono_literals;
+
+CWinSystemGbmGLESContext::CWinSystemGbmGLESContext()
+: CWinSystemGbmEGLContext(EGL_PLATFORM_GBM_MESA, "EGL_MESA_platform_gbm")
+{}
+
+void CWinSystemGbmGLESContext::Register()
+{
+ CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "gbm");
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemGbmGLESContext::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemGbmGLESContext>();
+}
+
+bool CWinSystemGbmGLESContext::InitWindowSystem()
+{
+ VIDEOPLAYER::CRendererFactory::ClearRenderer();
+ CDVDFactoryCodec::ClearHWAccels();
+ CLinuxRendererGLES::Register();
+ RETRO::CRPProcessInfoGbm::Register();
+ RETRO::CRPProcessInfoGbm::RegisterRendererFactory(new RETRO::CRendererFactoryDMA);
+ RETRO::CRPProcessInfoGbm::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGLES);
+
+ if (!CWinSystemGbmEGLContext::InitWindowSystemEGL(EGL_OPENGL_ES2_BIT, EGL_OPENGL_ES_API))
+ {
+ return false;
+ }
+
+ bool general, deepColor;
+ m_vaapiProxy.reset(GBM::VaapiProxyCreate(m_DRM->GetRenderNodeFileDescriptor()));
+ GBM::VaapiProxyConfig(m_vaapiProxy.get(), m_eglContext.GetEGLDisplay());
+ GBM::VAAPIRegisterRenderGLES(m_vaapiProxy.get(), general, deepColor);
+
+ if (general)
+ {
+ GBM::VAAPIRegister(m_vaapiProxy.get(), deepColor);
+ }
+
+ CRendererDRMPRIMEGLES::Register();
+ CRendererDRMPRIME::Register();
+ CDVDVideoCodecDRMPRIME::Register();
+ VIDEOPLAYER::CProcessInfoGBM::Register();
+
+ CScreenshotSurfaceGLES::Register();
+
+ CBufferObjectFactory::ClearBufferObjects();
+ CDumbBufferObject::Register();
+#if defined(HAS_GBM_BO_MAP)
+ CGBMBufferObject::Register();
+#endif
+#if defined(HAVE_LINUX_MEMFD) && defined(HAVE_LINUX_UDMABUF)
+ CUDMABufferObject::Register();
+#endif
+#if defined(HAVE_LINUX_DMA_HEAP)
+ CDMAHeapBufferObject::Register();
+#endif
+
+ return true;
+}
+
+bool CWinSystemGbmGLESContext::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ if (res.iWidth != m_nWidth ||
+ res.iHeight != m_nHeight)
+ {
+ CLog::Log(LOGDEBUG, "CWinSystemGbmGLESContext::{} - resolution changed, creating a new window",
+ __FUNCTION__);
+ CreateNewWindow("", fullScreen, res);
+ }
+
+ if (!m_eglContext.TrySwapBuffers())
+ {
+ CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed");
+ throw std::runtime_error("eglSwapBuffers failed");
+ }
+
+ CWinSystemGbm::SetFullScreen(fullScreen, res, blankOtherDisplays);
+ CRenderSystemGLES::ResetRenderSystem(res.iWidth, res.iHeight);
+
+ return true;
+}
+
+void CWinSystemGbmGLESContext::PresentRender(bool rendered, bool videoLayer)
+{
+ if (!m_bRenderCreated)
+ return;
+
+ if (rendered || videoLayer)
+ {
+ if (rendered)
+ {
+ if (!m_eglContext.TrySwapBuffers())
+ {
+ CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed");
+ throw std::runtime_error("eglSwapBuffers failed");
+ }
+ }
+ CWinSystemGbm::FlipPage(rendered, videoLayer);
+
+ if (m_dispReset && m_dispResetTimer.IsTimePast())
+ {
+ CLog::Log(LOGDEBUG, "CWinSystemGbmGLESContext::{} - Sending display reset to all clients",
+ __FUNCTION__);
+ m_dispReset = false;
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+
+ for (auto resource : m_resources)
+ resource->OnResetDisplay();
+ }
+ }
+ else
+ {
+ KODI::TIME::Sleep(10ms);
+ }
+}
+
+bool CWinSystemGbmGLESContext::CreateContext()
+{
+ CEGLAttributesVec contextAttribs;
+ contextAttribs.Add({{EGL_CONTEXT_CLIENT_VERSION, 2}});
+
+ if (!m_eglContext.CreateContext(contextAttribs))
+ {
+ CLog::Log(LOGERROR, "EGL context creation failed");
+ return false;
+ }
+ return true;
+}
diff --git a/xbmc/windowing/gbm/WinSystemGbmGLESContext.h b/xbmc/windowing/gbm/WinSystemGbmGLESContext.h
new file mode 100644
index 0000000..8b9de77
--- /dev/null
+++ b/xbmc/windowing/gbm/WinSystemGbmGLESContext.h
@@ -0,0 +1,48 @@
+/*
+ * 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 "WinSystemGbmEGLContext.h"
+#include "rendering/gles/RenderSystemGLES.h"
+#include "utils/EGLUtils.h"
+
+#include <memory>
+
+class CVaapiProxy;
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CWinSystemGbmGLESContext : public CWinSystemGbmEGLContext, public CRenderSystemGLES
+{
+public:
+ CWinSystemGbmGLESContext();
+ ~CWinSystemGbmGLESContext() override = default;
+
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystemBase via CWinSystemGbm
+ CRenderSystemBase *GetRenderSystem() override { return this; }
+ bool InitWindowSystem() override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+ void PresentRender(bool rendered, bool videoLayer) override;
+protected:
+ void SetVSyncImpl(bool enable) override {}
+ void PresentRenderImpl(bool rendered) override {};
+ bool CreateContext() override;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/gbm/drm/CMakeLists.txt b/xbmc/windowing/gbm/drm/CMakeLists.txt
new file mode 100644
index 0000000..8bd07ea
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/CMakeLists.txt
@@ -0,0 +1,21 @@
+set(SOURCES DRMAtomic.cpp
+ DRMConnector.cpp
+ DRMCrtc.cpp
+ DRMEncoder.cpp
+ DRMLegacy.cpp
+ DRMObject.cpp
+ DRMPlane.cpp
+ DRMUtils.cpp
+ OffScreenModeSetting.cpp)
+
+set(HEADERS DRMAtomic.h
+ DRMConnector.h
+ DRMCrtc.h
+ DRMEncoder.h
+ DRMLegacy.h
+ DRMObject.h
+ DRMPlane.h
+ DRMUtils.h
+ OffScreenModeSetting.h)
+
+core_add_library(windowing_gbm_drm)
diff --git a/xbmc/windowing/gbm/drm/DRMAtomic.cpp b/xbmc/windowing/gbm/drm/DRMAtomic.cpp
new file mode 100644
index 0000000..5d61a69
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMAtomic.cpp
@@ -0,0 +1,344 @@
+/*
+ * 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 "DRMAtomic.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/log.h"
+
+#include <errno.h>
+#include <string.h>
+
+#include <drm_fourcc.h>
+#include <drm_mode.h>
+#include <unistd.h>
+
+using namespace KODI::WINDOWING::GBM;
+
+namespace
+{
+
+const auto SETTING_VIDEOSCREEN_HW_SCALING_FILTER = "videoscreen.hwscalingfilter";
+
+uint32_t GetScalingFactor(uint32_t srcWidth,
+ uint32_t srcHeight,
+ uint32_t destWidth,
+ uint32_t destHeight)
+{
+ uint32_t factor_W = destWidth / srcWidth;
+ uint32_t factor_H = destHeight / srcHeight;
+ if (factor_W != factor_H)
+ return (factor_W < factor_H) ? factor_W : factor_H;
+ return factor_W;
+}
+
+} // namespace
+
+bool CDRMAtomic::SetScalingFilter(CDRMObject* object, const char* name, const char* type)
+{
+ bool result;
+ uint64_t value;
+ std::tie(result, value) = m_gui_plane->GetPropertyValue(name, type);
+ if (!result)
+ return false;
+
+ if (!AddProperty(object, name, value))
+ return false;
+
+ uint32_t mar_scale_factor =
+ GetScalingFactor(m_width, m_height, m_mode->hdisplay, m_mode->vdisplay);
+ AddProperty(object, "CRTC_W", (mar_scale_factor * m_width));
+ AddProperty(object, "CRTC_H", (mar_scale_factor * m_height));
+
+ return true;
+}
+
+void CDRMAtomic::DrmAtomicCommit(int fb_id, int flags, bool rendered, bool videoLayer)
+{
+ uint32_t blob_id;
+
+ if (flags & DRM_MODE_ATOMIC_ALLOW_MODESET)
+ {
+ if (!AddProperty(m_connector, "CRTC_ID", m_crtc->GetCrtcId()))
+ return;
+
+ if (drmModeCreatePropertyBlob(m_fd, m_mode, sizeof(*m_mode), &blob_id) != 0)
+ return;
+
+ if (m_active && m_orig_crtc && m_orig_crtc->GetCrtcId() != m_crtc->GetCrtcId())
+ {
+ // if using a different CRTC than the original, disable original to avoid EINVAL
+ if (!AddProperty(m_orig_crtc, "MODE_ID", 0))
+ return;
+
+ if (!AddProperty(m_orig_crtc, "ACTIVE", 0))
+ return;
+ }
+
+ if (!AddProperty(m_crtc, "MODE_ID", blob_id))
+ return;
+
+ if (!AddProperty(m_crtc, "ACTIVE", m_active ? 1 : 0))
+ return;
+ }
+
+ if (rendered)
+ {
+ AddProperty(m_gui_plane, "FB_ID", fb_id);
+ AddProperty(m_gui_plane, "CRTC_ID", m_crtc->GetCrtcId());
+ AddProperty(m_gui_plane, "SRC_X", 0);
+ AddProperty(m_gui_plane, "SRC_Y", 0);
+ AddProperty(m_gui_plane, "SRC_W", m_width << 16);
+ AddProperty(m_gui_plane, "SRC_H", m_height << 16);
+ AddProperty(m_gui_plane, "CRTC_X", 0);
+ AddProperty(m_gui_plane, "CRTC_Y", 0);
+ //! @todo: disabled until upstream kernel changes are merged
+ // if (DisplayHardwareScalingEnabled())
+ // {
+ // SetScalingFilter(m_gui_plane, "SCALING_FILTER", "Nearest Neighbor");
+ // }
+ // else
+ {
+ AddProperty(m_gui_plane, "CRTC_W", m_mode->hdisplay);
+ AddProperty(m_gui_plane, "CRTC_H", m_mode->vdisplay);
+ }
+
+ }
+ else if (videoLayer && !CServiceBroker::GetGUI()->GetWindowManager().HasVisibleControls())
+ {
+ // disable gui plane when video layer is active and gui has no visible controls
+ AddProperty(m_gui_plane, "FB_ID", 0);
+ AddProperty(m_gui_plane, "CRTC_ID", 0);
+ }
+
+ if (CServiceBroker::GetLogging().CanLogComponent(LOGWINDOWING))
+ m_req->LogAtomicRequest();
+
+ auto ret = drmModeAtomicCommit(m_fd, m_req->Get(), flags | DRM_MODE_ATOMIC_TEST_ONLY, nullptr);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR,
+ "CDRMAtomic::{} - test commit failed: ({}) - falling back to last successful atomic "
+ "request",
+ __FUNCTION__, strerror(errno));
+
+ auto oldRequest = m_atomicRequestQueue.front().get();
+ CDRMAtomicRequest::LogAtomicDiff(m_req, oldRequest);
+ m_req = oldRequest;
+
+ // update the old atomic request with the new fb id to avoid tearing
+ if (rendered)
+ AddProperty(m_gui_plane, "FB_ID", fb_id);
+ }
+
+ ret = drmModeAtomicCommit(m_fd, m_req->Get(), flags, nullptr);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CDRMAtomic::{} - atomic commit failed: {}", __FUNCTION__,
+ strerror(errno));
+ }
+
+ if (flags & DRM_MODE_ATOMIC_ALLOW_MODESET)
+ {
+ if (drmModeDestroyPropertyBlob(m_fd, blob_id) != 0)
+ CLog::Log(LOGERROR, "CDRMAtomic::{} - failed to destroy property blob: {}", __FUNCTION__,
+ strerror(errno));
+ }
+
+ if (m_atomicRequestQueue.size() > 1)
+ m_atomicRequestQueue.pop_back();
+
+ m_atomicRequestQueue.emplace_back(std::make_unique<CDRMAtomicRequest>());
+ m_req = m_atomicRequestQueue.back().get();
+}
+
+void CDRMAtomic::FlipPage(struct gbm_bo *bo, bool rendered, bool videoLayer)
+{
+ struct drm_fb *drm_fb = nullptr;
+
+ if (rendered)
+ {
+ if (videoLayer)
+ m_gui_plane->SetFormat(CDRMUtils::FourCCWithAlpha(m_gui_plane->GetFormat()));
+ else
+ m_gui_plane->SetFormat(CDRMUtils::FourCCWithoutAlpha(m_gui_plane->GetFormat()));
+
+ drm_fb = CDRMUtils::DrmFbGetFromBo(bo);
+ if (!drm_fb)
+ {
+ CLog::Log(LOGERROR, "CDRMAtomic::{} - Failed to get a new FBO", __FUNCTION__);
+ return;
+ }
+ }
+
+ uint32_t flags = 0;
+
+ if (m_need_modeset)
+ {
+ flags |= DRM_MODE_ATOMIC_ALLOW_MODESET;
+ m_need_modeset = false;
+ CLog::Log(LOGDEBUG, "CDRMAtomic::{} - Execute modeset at next commit", __FUNCTION__);
+ }
+
+ DrmAtomicCommit(!drm_fb ? 0 : drm_fb->fb_id, flags, rendered, videoLayer);
+}
+
+bool CDRMAtomic::InitDrm()
+{
+ if (!CDRMUtils::OpenDrm(true))
+ return false;
+
+ auto ret = drmSetClientCap(m_fd, DRM_CLIENT_CAP_ATOMIC, 1);
+ if (ret)
+ {
+ CLog::Log(LOGERROR, "CDRMAtomic::{} - no atomic modesetting support: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ m_atomicRequestQueue.emplace_back(std::make_unique<CDRMAtomicRequest>());
+ m_req = m_atomicRequestQueue.back().get();
+
+ if (!CDRMUtils::InitDrm())
+ return false;
+
+ for (auto& plane : m_planes)
+ {
+ AddProperty(plane.get(), "FB_ID", 0);
+ AddProperty(plane.get(), "CRTC_ID", 0);
+ }
+
+ CLog::Log(LOGDEBUG, "CDRMAtomic::{} - initialized atomic DRM", __FUNCTION__);
+
+ //! @todo: disabled until upstream kernel changes are merged
+ // if (m_gui_plane->SupportsProperty("SCALING_FILTER"))
+ // {
+ // const std::shared_ptr<CSettings> settings =
+ // CServiceBroker::GetSettingsComponent()->GetSettings();
+ // settings->GetSetting(SETTING_VIDEOSCREEN_HW_SCALING_FILTER)->SetVisible(true);
+ // }
+
+ return true;
+}
+
+void CDRMAtomic::DestroyDrm()
+{
+ CDRMUtils::DestroyDrm();
+}
+
+bool CDRMAtomic::SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo *bo)
+{
+ m_need_modeset = true;
+
+ return true;
+}
+
+bool CDRMAtomic::SetActive(bool active)
+{
+ m_need_modeset = true;
+ m_active = active;
+
+ return true;
+}
+
+bool CDRMAtomic::AddProperty(CDRMObject* object, const char* name, uint64_t value)
+{
+ return m_req->AddProperty(object, name, value);
+}
+
+bool CDRMAtomic::DisplayHardwareScalingEnabled()
+{
+ auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ if (settings && settings->GetBool(SETTING_VIDEOSCREEN_HW_SCALING_FILTER))
+ return true;
+
+ return false;
+}
+
+CDRMAtomic::CDRMAtomicRequest::CDRMAtomicRequest() : m_atomicRequest(drmModeAtomicAlloc())
+{
+}
+
+bool CDRMAtomic::CDRMAtomicRequest::AddProperty(CDRMObject* object, const char* name, uint64_t value)
+{
+ uint32_t propertyId = object->GetPropertyId(name);
+ if (propertyId == 0)
+ return false;
+
+ int ret = drmModeAtomicAddProperty(m_atomicRequest.get(), object->GetId(), propertyId, value);
+ if (ret < 0)
+ return false;
+
+ m_atomicRequestItems[object][propertyId] = value;
+ return true;
+}
+
+void CDRMAtomic::CDRMAtomicRequest::LogAtomicDiff(CDRMAtomicRequest* current,
+ CDRMAtomicRequest* old)
+{
+ std::map<CDRMObject*, std::map<uint32_t, uint64_t>> atomicDiff;
+
+ for (const auto& object : current->m_atomicRequestItems)
+ {
+ auto sameObject = old->m_atomicRequestItems.find(object.first);
+ if (sameObject != old->m_atomicRequestItems.end())
+ {
+ std::map<uint32_t, uint64_t> propertyDiff;
+
+ std::set_difference(current->m_atomicRequestItems[object.first].begin(),
+ current->m_atomicRequestItems[object.first].end(),
+ old->m_atomicRequestItems[object.first].begin(),
+ old->m_atomicRequestItems[object.first].end(),
+ std::inserter(propertyDiff, propertyDiff.begin()));
+
+ atomicDiff[object.first] = propertyDiff;
+ }
+ else
+ {
+ atomicDiff[object.first] = current->m_atomicRequestItems[object.first];
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "CDRMAtomicRequest::{} - DRM Atomic Request Diff:", __FUNCTION__);
+
+ LogAtomicRequest(LOGERROR, atomicDiff);
+}
+
+void CDRMAtomic::CDRMAtomicRequest::LogAtomicRequest()
+{
+ CLog::Log(LOGDEBUG, "CDRMAtomicRequest::{} - DRM Atomic Request:", __FUNCTION__);
+ LogAtomicRequest(LOGDEBUG, m_atomicRequestItems);
+}
+
+void CDRMAtomic::CDRMAtomicRequest::LogAtomicRequest(
+ uint8_t logLevel, std::map<CDRMObject*, std::map<uint32_t, uint64_t>>& atomicRequestItems)
+{
+ std::string message;
+ for (const auto& object : atomicRequestItems)
+ {
+ message.append("\nObject: " + object.first->GetTypeName() +
+ "\tID: " + std::to_string(object.first->GetId()));
+ for (const auto& property : object.second)
+ message.append("\n Property: " + object.first->GetPropertyName(property.first) +
+ "\tID: " + std::to_string(property.first) +
+ "\tValue: " + std::to_string(property.second));
+ }
+
+ CLog::Log(logLevel, "{}", message);
+}
+
+void CDRMAtomic::CDRMAtomicRequest::DrmModeAtomicReqDeleter::operator()(drmModeAtomicReqPtr p) const
+{
+ drmModeAtomicFree(p);
+};
diff --git a/xbmc/windowing/gbm/drm/DRMAtomic.h b/xbmc/windowing/gbm/drm/DRMAtomic.h
new file mode 100644
index 0000000..ca2cd9a
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMAtomic.h
@@ -0,0 +1,81 @@
+/*
+ * 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 "DRMUtils.h"
+
+#include <cstdint>
+#include <deque>
+#include <map>
+#include <memory>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CDRMAtomic : public CDRMUtils
+{
+public:
+ CDRMAtomic() = default;
+ ~CDRMAtomic() override = default;
+ void FlipPage(struct gbm_bo* bo, bool rendered, bool videoLayer) override;
+ bool SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo* bo) override;
+ bool SetActive(bool active) override;
+ bool InitDrm() override;
+ void DestroyDrm() override;
+ bool AddProperty(CDRMObject* object, const char* name, uint64_t value);
+
+ bool DisplayHardwareScalingEnabled();
+
+private:
+ void DrmAtomicCommit(int fb_id, int flags, bool rendered, bool videoLayer);
+
+ bool SetScalingFilter(CDRMObject* object, const char* name, const char* type);
+
+ bool m_need_modeset;
+ bool m_active = true;
+
+ class CDRMAtomicRequest
+ {
+ public:
+ CDRMAtomicRequest();
+ ~CDRMAtomicRequest() = default;
+ CDRMAtomicRequest(const CDRMAtomicRequest& right) = delete;
+
+ drmModeAtomicReqPtr Get() const { return m_atomicRequest.get(); }
+
+ bool AddProperty(CDRMObject* object, const char* name, uint64_t value);
+ void LogAtomicRequest();
+
+ static void LogAtomicDiff(CDRMAtomicRequest* current, CDRMAtomicRequest* old);
+
+ private:
+ static void LogAtomicRequest(
+ uint8_t logLevel, std::map<CDRMObject*, std::map<uint32_t, uint64_t>>& atomicRequestItems);
+
+ std::map<CDRMObject*, std::map<uint32_t, uint64_t>> m_atomicRequestItems;
+
+ struct DrmModeAtomicReqDeleter
+ {
+ void operator()(drmModeAtomicReqPtr p) const;
+ };
+
+ std::unique_ptr<drmModeAtomicReq, DrmModeAtomicReqDeleter> m_atomicRequest;
+ };
+
+ CDRMAtomicRequest* m_req = nullptr;
+ std::deque<std::unique_ptr<CDRMAtomicRequest>> m_atomicRequestQueue;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/gbm/drm/DRMConnector.cpp b/xbmc/windowing/gbm/drm/DRMConnector.cpp
new file mode 100644
index 0000000..fc76b90
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMConnector.cpp
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2005-2020 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 "DRMConnector.h"
+
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+
+#include <map>
+
+using namespace KODI::WINDOWING::GBM;
+
+using namespace std::chrono_literals;
+
+namespace
+{
+
+std::map<int, std::string> connectorTypeNames = {
+ {DRM_MODE_CONNECTOR_Unknown, "unknown"},
+ {DRM_MODE_CONNECTOR_VGA, "VGA"},
+ {DRM_MODE_CONNECTOR_DVII, "DVI-I"},
+ {DRM_MODE_CONNECTOR_DVID, "DVI-D"},
+ {DRM_MODE_CONNECTOR_DVIA, "DVI-A"},
+ {DRM_MODE_CONNECTOR_Composite, "composite"},
+ {DRM_MODE_CONNECTOR_SVIDEO, "s-video"},
+ {DRM_MODE_CONNECTOR_LVDS, "LVDS"},
+ {DRM_MODE_CONNECTOR_Component, "component"},
+ {DRM_MODE_CONNECTOR_9PinDIN, "9-pin DIN"},
+ {DRM_MODE_CONNECTOR_DisplayPort, "DP"},
+ {DRM_MODE_CONNECTOR_HDMIA, "HDMI-A"},
+ {DRM_MODE_CONNECTOR_HDMIB, "HDMI-B"},
+ {DRM_MODE_CONNECTOR_TV, "TV"},
+ {DRM_MODE_CONNECTOR_eDP, "eDP"},
+ {DRM_MODE_CONNECTOR_VIRTUAL, "Virtual"},
+ {DRM_MODE_CONNECTOR_DSI, "DSI"},
+ {DRM_MODE_CONNECTOR_DPI, "DPI"},
+};
+
+std::map<drmModeConnection, std::string> connectorStatusNames = {
+ {DRM_MODE_CONNECTED, "connected"},
+ {DRM_MODE_DISCONNECTED, "disconnected"},
+ {DRM_MODE_UNKNOWNCONNECTION, "unknown"},
+};
+
+} // namespace
+
+CDRMConnector::CDRMConnector(int fd, uint32_t connector)
+ : CDRMObject(fd), m_connector(drmModeGetConnector(m_fd, connector))
+{
+ if (!m_connector)
+ throw std::runtime_error("drmModeGetConnector failed: " + std::string{strerror(errno)});
+
+ if (!GetProperties(m_connector->connector_id, DRM_MODE_OBJECT_CONNECTOR))
+ throw std::runtime_error("failed to get properties for connector: " +
+ std::to_string(m_connector->connector_id));
+}
+
+bool CDRMConnector::CheckConnector()
+{
+ unsigned retryCnt = 7;
+ while (!IsConnected() && retryCnt > 0)
+ {
+ CLog::Log(LOGDEBUG, "CDRMConnector::{} - connector is disconnected", __FUNCTION__);
+ retryCnt--;
+ KODI::TIME::Sleep(1s);
+
+ m_connector.reset(drmModeGetConnector(m_fd, m_connector->connector_id));
+ }
+
+ return m_connector->connection == DRM_MODE_CONNECTED;
+}
+
+std::string CDRMConnector::GetType()
+{
+ auto typeName = connectorTypeNames.find(m_connector->connector_type);
+ if (typeName == connectorTypeNames.end())
+ return connectorTypeNames[DRM_MODE_CONNECTOR_Unknown];
+
+ return typeName->second;
+}
+
+std::string CDRMConnector::GetStatus()
+{
+ auto statusName = connectorStatusNames.find(m_connector->connection);
+ if (statusName == connectorStatusNames.end())
+ return connectorStatusNames[DRM_MODE_UNKNOWNCONNECTION];
+
+ return statusName->second;
+}
+
+std::string CDRMConnector::GetName()
+{
+ return GetType() + "-" + std::to_string(m_connector->connector_type_id);
+}
diff --git a/xbmc/windowing/gbm/drm/DRMConnector.h b/xbmc/windowing/gbm/drm/DRMConnector.h
new file mode 100644
index 0000000..5a29222
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMConnector.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2005-2020 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 "DRMObject.h"
+
+#include <string>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CDRMConnector : public CDRMObject
+{
+public:
+ explicit CDRMConnector(int fd, uint32_t connector);
+ CDRMConnector(const CDRMConnector&) = delete;
+ CDRMConnector& operator=(const CDRMConnector&) = delete;
+ ~CDRMConnector() = default;
+
+ std::string GetType();
+ std::string GetStatus();
+ std::string GetName();
+
+ uint32_t GetEncoderId() const { return m_connector->encoder_id; }
+ uint32_t* GetConnectorId() const { return &m_connector->connector_id; }
+ int GetModesCount() const { return m_connector->count_modes; }
+ drmModeModeInfoPtr GetModeForIndex(int index) const { return &m_connector->modes[index]; }
+
+ bool IsConnected() { return m_connector->connection == DRM_MODE_CONNECTED; }
+ bool CheckConnector();
+
+private:
+ struct DrmModeConnectorDeleter
+ {
+ void operator()(drmModeConnector* p) { drmModeFreeConnector(p); }
+ };
+
+ std::unique_ptr<drmModeConnector, DrmModeConnectorDeleter> m_connector;
+};
+
+} // namespace GBM
+} // namespace WINDOWING
+} // namespace KODI
diff --git a/xbmc/windowing/gbm/drm/DRMCrtc.cpp b/xbmc/windowing/gbm/drm/DRMCrtc.cpp
new file mode 100644
index 0000000..426fd95
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMCrtc.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2005-2020 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 "DRMCrtc.h"
+
+#include <cstring>
+#include <errno.h>
+#include <stdexcept>
+#include <string>
+
+using namespace KODI::WINDOWING::GBM;
+
+CDRMCrtc::CDRMCrtc(int fd, uint32_t crtc) : CDRMObject(fd), m_crtc(drmModeGetCrtc(m_fd, crtc))
+{
+ if (!m_crtc)
+ throw std::runtime_error("drmModeGetCrtc failed: " + std::string{strerror(errno)});
+
+ if (!GetProperties(m_crtc->crtc_id, DRM_MODE_OBJECT_CRTC))
+ throw std::runtime_error("failed to get properties for crtc: " +
+ std::to_string(m_crtc->crtc_id));
+}
diff --git a/xbmc/windowing/gbm/drm/DRMCrtc.h b/xbmc/windowing/gbm/drm/DRMCrtc.h
new file mode 100644
index 0000000..a1aadc2
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMCrtc.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2005-2020 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 "DRMObject.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CDRMCrtc : public CDRMObject
+{
+public:
+ explicit CDRMCrtc(int fd, uint32_t crtc);
+ CDRMCrtc(const CDRMCrtc&) = delete;
+ CDRMCrtc& operator=(const CDRMCrtc&) = delete;
+ ~CDRMCrtc() = default;
+
+ uint32_t GetCrtcId() const { return m_crtc->crtc_id; }
+ uint32_t GetBufferId() const { return m_crtc->buffer_id; }
+ uint32_t GetX() const { return m_crtc->x; }
+ uint32_t GetY() const { return m_crtc->y; }
+ drmModeModeInfoPtr GetMode() const { return &m_crtc->mode; }
+ bool GetModeValid() const { return m_crtc->mode_valid != 0; }
+
+private:
+ struct DrmModeCrtcDeleter
+ {
+ void operator()(drmModeCrtc* p) { drmModeFreeCrtc(p); }
+ };
+
+ std::unique_ptr<drmModeCrtc, DrmModeCrtcDeleter> m_crtc;
+};
+
+} // namespace GBM
+} // namespace WINDOWING
+} // namespace KODI
diff --git a/xbmc/windowing/gbm/drm/DRMEncoder.cpp b/xbmc/windowing/gbm/drm/DRMEncoder.cpp
new file mode 100644
index 0000000..e4289c6
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMEncoder.cpp
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2005-2020 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 "DRMEncoder.h"
+
+#include <cstring>
+#include <errno.h>
+#include <stdexcept>
+#include <string>
+
+using namespace KODI::WINDOWING::GBM;
+
+CDRMEncoder::CDRMEncoder(int fd, uint32_t encoder)
+ : CDRMObject(fd), m_encoder(drmModeGetEncoder(m_fd, encoder))
+{
+ if (!m_encoder)
+ throw std::runtime_error("drmModeGetEncoder failed: " + std::string{strerror(errno)});
+}
diff --git a/xbmc/windowing/gbm/drm/DRMEncoder.h b/xbmc/windowing/gbm/drm/DRMEncoder.h
new file mode 100644
index 0000000..aceb276
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMEncoder.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2005-2020 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 "DRMObject.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CDRMEncoder : public CDRMObject
+{
+public:
+ explicit CDRMEncoder(int fd, uint32_t encoder);
+ CDRMEncoder(const CDRMEncoder&) = delete;
+ CDRMEncoder& operator=(const CDRMEncoder&) = delete;
+ ~CDRMEncoder() = default;
+
+ uint32_t GetEncoderId() const { return m_encoder->encoder_id; }
+ uint32_t GetCrtcId() const { return m_encoder->crtc_id; }
+ uint32_t GetPossibleCrtcs() const { return m_encoder->possible_crtcs; }
+
+private:
+ struct DrmModeEncoderDeleter
+ {
+ void operator()(drmModeEncoder* p) { drmModeFreeEncoder(p); }
+ };
+
+ std::unique_ptr<drmModeEncoder, DrmModeEncoderDeleter> m_encoder;
+};
+
+} // namespace GBM
+} // namespace WINDOWING
+} // namespace KODI
diff --git a/xbmc/windowing/gbm/drm/DRMLegacy.cpp b/xbmc/windowing/gbm/drm/DRMLegacy.cpp
new file mode 100644
index 0000000..418d067
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMLegacy.cpp
@@ -0,0 +1,141 @@
+/*
+ * 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 "DRMLegacy.h"
+
+#include "guilib/gui3d.h"
+#include "settings/Settings.h"
+#include "utils/log.h"
+
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <drm_mode.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <unistd.h>
+
+using namespace KODI::WINDOWING::GBM;
+
+static int flip_happening = 0;
+
+bool CDRMLegacy::SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo *bo)
+{
+ struct drm_fb *drm_fb = DrmFbGetFromBo(bo);
+
+ auto ret = drmModeSetCrtc(m_fd, m_crtc->GetCrtcId(), drm_fb->fb_id, 0, 0,
+ m_connector->GetConnectorId(), 1, m_mode);
+
+ if(ret < 0)
+ {
+ CLog::Log(LOGERROR, "CDRMLegacy::{} - failed to set crtc mode: {}x{}{} @ {} Hz", __FUNCTION__,
+ m_mode->hdisplay, m_mode->vdisplay,
+ m_mode->flags & DRM_MODE_FLAG_INTERLACE ? "i" : "", m_mode->vrefresh);
+
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "CDRMLegacy::{} - set crtc mode: {}x{}{} @ {} Hz", __FUNCTION__,
+ m_mode->hdisplay, m_mode->vdisplay, m_mode->flags & DRM_MODE_FLAG_INTERLACE ? "i" : "",
+ m_mode->vrefresh);
+
+ return true;
+}
+
+void CDRMLegacy::PageFlipHandler(int fd, unsigned int frame, unsigned int sec,
+ unsigned int usec, void *data)
+{
+ (void) fd, (void) frame, (void) sec, (void) usec;
+
+ int *flip_happening = static_cast<int *>(data);
+ *flip_happening = 0;
+}
+
+bool CDRMLegacy::WaitingForFlip()
+{
+ if (!flip_happening)
+ return false;
+
+ struct pollfd drm_fds =
+ {
+ m_fd,
+ POLLIN,
+ 0,
+ };
+
+ drmEventContext drm_evctx{};
+ drm_evctx.version = DRM_EVENT_CONTEXT_VERSION;
+ drm_evctx.page_flip_handler = PageFlipHandler;
+
+ while(flip_happening)
+ {
+ auto ret = poll(&drm_fds, 1, -1);
+
+ if(ret < 0)
+ return true;
+
+ if(drm_fds.revents & (POLLHUP | POLLERR))
+ return true;
+
+ if(drm_fds.revents & POLLIN)
+ drmHandleEvent(m_fd, &drm_evctx);
+ }
+
+ return false;
+}
+
+bool CDRMLegacy::QueueFlip(struct gbm_bo *bo)
+{
+ struct drm_fb *drm_fb = DrmFbGetFromBo(bo);
+
+ auto ret = drmModePageFlip(m_fd, m_crtc->GetCrtcId(), drm_fb->fb_id, DRM_MODE_PAGE_FLIP_EVENT,
+ &flip_happening);
+
+ if(ret)
+ {
+ CLog::Log(LOGDEBUG, "CDRMLegacy::{} - failed to queue DRM page flip", __FUNCTION__);
+ return false;
+ }
+
+ return true;
+}
+
+void CDRMLegacy::FlipPage(struct gbm_bo *bo, bool rendered, bool videoLayer)
+{
+ if (rendered || videoLayer)
+ {
+ flip_happening = QueueFlip(bo);
+ WaitingForFlip();
+ }
+}
+
+bool CDRMLegacy::InitDrm()
+{
+ if (!CDRMUtils::OpenDrm(true))
+ return false;
+
+ if (!CDRMUtils::InitDrm())
+ return false;
+
+ CLog::Log(LOGDEBUG, "CDRMLegacy::{} - initialized legacy DRM", __FUNCTION__);
+ return true;
+}
+
+bool CDRMLegacy::SetActive(bool active)
+{
+ if (!m_connector->SetProperty("DPMS", active ? DRM_MODE_DPMS_ON : DRM_MODE_DPMS_OFF))
+ {
+ CLog::Log(LOGDEBUG, "CDRMLegacy::{} - failed to set DPMS property", __FUNCTION__);
+ return false;
+ }
+
+ return true;
+}
diff --git a/xbmc/windowing/gbm/drm/DRMLegacy.h b/xbmc/windowing/gbm/drm/DRMLegacy.h
new file mode 100644
index 0000000..2b7ff45
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMLegacy.h
@@ -0,0 +1,39 @@
+/*
+ * 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 "DRMUtils.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CDRMLegacy : public CDRMUtils
+{
+public:
+ CDRMLegacy() = default;
+ ~CDRMLegacy() override = default;
+ void FlipPage(struct gbm_bo* bo, bool rendered, bool videoLayer) override;
+ bool SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo* bo) override;
+ bool SetActive(bool active) override;
+ bool InitDrm() override;
+
+private:
+ bool WaitingForFlip();
+ bool QueueFlip(struct gbm_bo *bo);
+ static void PageFlipHandler(int fd, unsigned int frame, unsigned int sec,
+ unsigned int usec, void *data);
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/gbm/drm/DRMObject.cpp b/xbmc/windowing/gbm/drm/DRMObject.cpp
new file mode 100644
index 0000000..599bb61
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMObject.cpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2005-2020 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 "DRMObject.h"
+
+#include "utils/log.h"
+
+#include <algorithm>
+#include <array>
+
+using namespace KODI::WINDOWING::GBM;
+
+namespace
+{
+
+constexpr std::array<std::pair<uint32_t, const char*>, 8> DrmModeObjectTypes = {
+ {{DRM_MODE_OBJECT_CRTC, "crtc"},
+ {DRM_MODE_OBJECT_CONNECTOR, "connector"},
+ {DRM_MODE_OBJECT_ENCODER, "encoder"},
+ {DRM_MODE_OBJECT_MODE, "mode"},
+ {DRM_MODE_OBJECT_PROPERTY, "property"},
+ {DRM_MODE_OBJECT_FB, "framebuffer"},
+ {DRM_MODE_OBJECT_BLOB, "blob"},
+ {DRM_MODE_OBJECT_PLANE, "plane"}}};
+}
+
+CDRMObject::CDRMObject(int fd) : m_fd(fd)
+{
+}
+
+std::string CDRMObject::GetTypeName() const
+{
+ auto name = std::find_if(DrmModeObjectTypes.begin(), DrmModeObjectTypes.end(),
+ [this](const auto& p) { return p.first == m_type; });
+ if (name != DrmModeObjectTypes.end())
+ return name->second;
+
+ return "invalid type";
+}
+
+std::string CDRMObject::GetPropertyName(uint32_t propertyId) const
+{
+ auto prop = std::find_if(m_propsInfo.begin(), m_propsInfo.end(),
+ [&propertyId](const auto& p) { return p->prop_id == propertyId; });
+ if (prop != m_propsInfo.end())
+ return prop->get()->name;
+
+ return "invalid property";
+}
+
+uint32_t CDRMObject::GetPropertyId(const std::string& name) const
+{
+ auto property = std::find_if(m_propsInfo.begin(), m_propsInfo.end(),
+ [&name](const auto& prop) { return prop->name == name; });
+
+ if (property != m_propsInfo.end())
+ return property->get()->prop_id;
+
+ return 0;
+}
+
+bool CDRMObject::GetProperties(uint32_t id, uint32_t type)
+{
+ m_props.reset(drmModeObjectGetProperties(m_fd, id, type));
+ if (!m_props)
+ return false;
+
+ m_id = id;
+ m_type = type;
+
+ for (uint32_t i = 0; i < m_props->count_props; i++)
+ m_propsInfo.emplace_back(std::unique_ptr<drmModePropertyRes, DrmModePropertyResDeleter>(
+ drmModeGetProperty(m_fd, m_props->props[i])));
+
+ return true;
+}
+
+//! @todo: improve with c++17
+std::tuple<bool, uint64_t> CDRMObject::GetPropertyValue(const std::string& name,
+ const std::string& valueName) const
+{
+ auto property = std::find_if(m_propsInfo.begin(), m_propsInfo.end(),
+ [&name](const auto& prop) { return prop->name == name; });
+
+ if (property == m_propsInfo.end())
+ return std::make_tuple(false, 0);
+
+ auto prop = property->get();
+
+ if (!static_cast<bool>(drm_property_type_is(prop, DRM_MODE_PROP_ENUM)))
+ return std::make_tuple(false, 0);
+
+ for (int j = 0; j < prop->count_enums; j++)
+ {
+ if (prop->enums[j].name != valueName)
+ continue;
+
+ return std::make_tuple(true, prop->enums[j].value);
+ }
+
+ return std::make_tuple(false, 0);
+}
+
+bool CDRMObject::SetProperty(const std::string& name, uint64_t value)
+{
+ auto property = std::find_if(m_propsInfo.begin(), m_propsInfo.end(),
+ [&name](const auto& prop) { return prop->name == name; });
+
+ if (property != m_propsInfo.end())
+ {
+ int ret = drmModeObjectSetProperty(m_fd, m_id, m_type, property->get()->prop_id, value);
+ if (ret == 0)
+ return true;
+ }
+
+ return false;
+}
+
+bool CDRMObject::SupportsProperty(const std::string& name)
+{
+ auto property = std::find_if(m_propsInfo.begin(), m_propsInfo.end(),
+ [&name](const auto& prop) { return prop->name == name; });
+
+ if (property != m_propsInfo.end())
+ return true;
+
+ return false;
+}
diff --git a/xbmc/windowing/gbm/drm/DRMObject.h b/xbmc/windowing/gbm/drm/DRMObject.h
new file mode 100644
index 0000000..e2ae326
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMObject.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2005-2020 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 <cstddef>
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include <xf86drmMode.h>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CDRMObject
+{
+public:
+ CDRMObject(const CDRMObject&) = delete;
+ CDRMObject& operator=(const CDRMObject&) = delete;
+ virtual ~CDRMObject() = default;
+
+ std::string GetTypeName() const;
+ std::string GetPropertyName(uint32_t propertyId) const;
+
+ uint32_t GetId() const { return m_id; }
+ uint32_t GetPropertyId(const std::string& name) const;
+ std::tuple<bool, uint64_t> GetPropertyValue(const std::string& name,
+ const std::string& valueName) const;
+
+ bool SetProperty(const std::string& name, uint64_t value);
+ bool SupportsProperty(const std::string& name);
+
+protected:
+ explicit CDRMObject(int fd);
+
+ bool GetProperties(uint32_t id, uint32_t type);
+
+ struct DrmModeObjectPropertiesDeleter
+ {
+ void operator()(drmModeObjectProperties* p) { drmModeFreeObjectProperties(p); }
+ };
+
+ std::unique_ptr<drmModeObjectProperties, DrmModeObjectPropertiesDeleter> m_props;
+
+ struct DrmModePropertyResDeleter
+ {
+ void operator()(drmModePropertyRes* p) { drmModeFreeProperty(p); }
+ };
+
+ std::vector<std::unique_ptr<drmModePropertyRes, DrmModePropertyResDeleter>> m_propsInfo;
+
+ int m_fd{-1};
+
+private:
+ uint32_t m_id{0};
+ uint32_t m_type{0};
+};
+
+} // namespace GBM
+} // namespace WINDOWING
+} // namespace KODI
diff --git a/xbmc/windowing/gbm/drm/DRMPlane.cpp b/xbmc/windowing/gbm/drm/DRMPlane.cpp
new file mode 100644
index 0000000..90b5660
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMPlane.cpp
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2005-2020 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 "DRMPlane.h"
+
+#include "DRMUtils.h"
+#include "utils/DRMHelpers.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+using namespace KODI::WINDOWING::GBM;
+
+CDRMPlane::CDRMPlane(int fd, uint32_t plane) : CDRMObject(fd), m_plane(drmModeGetPlane(m_fd, plane))
+{
+ if (!m_plane)
+ throw std::runtime_error("drmModeGetPlane failed: " + std::string{strerror(errno)});
+
+ if (!GetProperties(m_plane->plane_id, DRM_MODE_OBJECT_PLANE))
+ throw std::runtime_error("failed to get properties for plane: " +
+ std::to_string(m_plane->plane_id));
+}
+
+bool CDRMPlane::SupportsFormat(uint32_t format)
+{
+ for (uint32_t i = 0; i < m_plane->count_formats; i++)
+ if (m_plane->formats[i] == format)
+ return true;
+
+ return false;
+}
+
+bool CDRMPlane::SupportsFormatAndModifier(uint32_t format, uint64_t modifier)
+{
+ /*
+ * Some broadcom modifiers have parameters encoded which need to be
+ * masked out before comparing with reported modifiers.
+ */
+ if (modifier >> 56 == DRM_FORMAT_MOD_VENDOR_BROADCOM)
+ modifier = fourcc_mod_broadcom_mod(modifier);
+
+ if (modifier == DRM_FORMAT_MOD_LINEAR)
+ {
+ if (!SupportsFormat(format))
+ {
+ CLog::Log(LOGDEBUG, "CDRMPlane::{} - format not supported: {}", __FUNCTION__,
+ DRMHELPERS::FourCCToString(format));
+ return false;
+ }
+ }
+ else
+ {
+ auto formatModifiers = &m_modifiers_map[format];
+ if (formatModifiers->empty())
+ {
+ CLog::Log(LOGDEBUG, "CDRMPlane::{} - format not supported: {}", __FUNCTION__,
+ DRMHELPERS::FourCCToString(format));
+ return false;
+ }
+
+ auto formatModifier = std::find(formatModifiers->begin(), formatModifiers->end(), modifier);
+ if (formatModifier == formatModifiers->end())
+ {
+ CLog::Log(LOGDEBUG, "CDRMPlane::{} - modifier ({}) not supported for format ({})",
+ __FUNCTION__, DRMHELPERS::ModifierToString(modifier),
+ DRMHELPERS::FourCCToString(format));
+ return false;
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "CDRMPlane::{} - found plane format ({}) and modifier ({})", __FUNCTION__,
+ DRMHELPERS::FourCCToString(format), DRMHELPERS::ModifierToString(modifier));
+
+ return true;
+}
+
+void CDRMPlane::FindModifiers()
+{
+ auto property = std::find_if(m_propsInfo.begin(), m_propsInfo.end(), [](auto& prop) {
+ return StringUtils::EqualsNoCase(prop->name, "IN_FORMATS");
+ });
+
+ uint64_t blob_id = 0;
+ if (property != m_propsInfo.end())
+ blob_id = m_props->prop_values[std::distance(m_propsInfo.begin(), property)];
+
+ if (blob_id == 0)
+ return;
+
+ drmModePropertyBlobPtr blob = drmModeGetPropertyBlob(m_fd, blob_id);
+ if (!blob)
+ return;
+
+ drm_format_modifier_blob* header = static_cast<drm_format_modifier_blob*>(blob->data);
+ uint32_t* formats =
+ reinterpret_cast<uint32_t*>(reinterpret_cast<char*>(header) + header->formats_offset);
+ drm_format_modifier* mod = reinterpret_cast<drm_format_modifier*>(
+ reinterpret_cast<char*>(header) + header->modifiers_offset);
+
+ for (uint32_t i = 0; i < header->count_formats; i++)
+ {
+ std::vector<uint64_t> modifiers;
+ for (uint32_t j = 0; j < header->count_modifiers; j++)
+ {
+ if (mod[j].formats & 1ULL << i)
+ modifiers.emplace_back(mod[j].modifier);
+ }
+
+ m_modifiers_map.emplace(formats[i], modifiers);
+ }
+
+ if (blob)
+ drmModeFreePropertyBlob(blob);
+}
diff --git a/xbmc/windowing/gbm/drm/DRMPlane.h b/xbmc/windowing/gbm/drm/DRMPlane.h
new file mode 100644
index 0000000..e077fc3
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMPlane.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2005-2020 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 "DRMObject.h"
+
+#include <map>
+
+#include <drm_fourcc.h>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class CDRMPlane : public CDRMObject
+{
+public:
+ explicit CDRMPlane(int fd, uint32_t plane);
+ CDRMPlane(const CDRMPlane&) = delete;
+ CDRMPlane& operator=(const CDRMPlane&) = delete;
+ ~CDRMPlane() = default;
+
+ uint32_t GetPlaneId() const { return m_plane->plane_id; }
+ uint32_t GetPossibleCrtcs() const { return m_plane->possible_crtcs; }
+
+ void FindModifiers();
+
+ void SetFormat(const uint32_t newFormat) { m_format = newFormat; }
+ uint32_t GetFormat() const { return m_format; }
+ std::vector<uint64_t>& GetModifiersForFormat(uint32_t format) { return m_modifiers_map[format]; }
+
+ bool SupportsFormat(uint32_t format);
+ bool SupportsFormatAndModifier(uint32_t format, uint64_t modifier);
+
+private:
+ struct DrmModePlaneDeleter
+ {
+ void operator()(drmModePlane* p) { drmModeFreePlane(p); }
+ };
+
+ std::unique_ptr<drmModePlane, DrmModePlaneDeleter> m_plane;
+
+ std::map<uint32_t, std::vector<uint64_t>> m_modifiers_map;
+ uint32_t m_format{DRM_FORMAT_XRGB8888};
+};
+
+} // namespace GBM
+} // namespace WINDOWING
+} // namespace KODI
diff --git a/xbmc/windowing/gbm/drm/DRMUtils.cpp b/xbmc/windowing/gbm/drm/DRMUtils.cpp
new file mode 100644
index 0000000..6b61403
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMUtils.cpp
@@ -0,0 +1,740 @@
+/*
+ * 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 "DRMUtils.h"
+
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/DRMHelpers.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include "PlatformDefs.h"
+
+using namespace KODI::WINDOWING::GBM;
+
+namespace
+{
+const std::string SETTING_VIDEOSCREEN_LIMITGUISIZE = "videoscreen.limitguisize";
+
+void DrmFbDestroyCallback(gbm_bo* bo, void* data)
+{
+ drm_fb* fb = static_cast<drm_fb*>(data);
+
+ if (fb->fb_id > 0)
+ {
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - removing framebuffer: {}", __FUNCTION__, fb->fb_id);
+ int drm_fd = gbm_device_get_fd(gbm_bo_get_device(bo));
+ drmModeRmFB(drm_fd, fb->fb_id);
+ }
+
+ delete fb;
+}
+}
+
+CDRMUtils::~CDRMUtils()
+{
+ DestroyDrm();
+}
+
+bool CDRMUtils::SetMode(const RESOLUTION_INFO& res)
+{
+ if (!m_connector->CheckConnector())
+ return false;
+
+ m_mode = m_connector->GetModeForIndex(std::atoi(res.strId.c_str()));
+ m_width = res.iWidth;
+ m_height = res.iHeight;
+
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - found crtc mode: {}x{}{} @ {} Hz", __FUNCTION__,
+ m_mode->hdisplay, m_mode->vdisplay, m_mode->flags & DRM_MODE_FLAG_INTERLACE ? "i" : "",
+ m_mode->vrefresh);
+
+ return true;
+}
+
+drm_fb * CDRMUtils::DrmFbGetFromBo(struct gbm_bo *bo)
+{
+ {
+ struct drm_fb *fb = static_cast<drm_fb *>(gbm_bo_get_user_data(bo));
+ if(fb)
+ {
+ if (m_gui_plane->GetFormat() == fb->format)
+ return fb;
+ else
+ DrmFbDestroyCallback(bo, gbm_bo_get_user_data(bo));
+ }
+ }
+
+ struct drm_fb *fb = new drm_fb;
+ fb->bo = bo;
+ fb->format = m_gui_plane->GetFormat();
+
+ uint32_t width,
+ height,
+ handles[4] = {0},
+ strides[4] = {0},
+ offsets[4] = {0};
+
+ uint64_t modifiers[4] = {0};
+
+ width = gbm_bo_get_width(bo);
+ height = gbm_bo_get_height(bo);
+
+#if defined(HAS_GBM_MODIFIERS)
+ for (int i = 0; i < gbm_bo_get_plane_count(bo); i++)
+ {
+ handles[i] = gbm_bo_get_handle_for_plane(bo, i).u32;
+ strides[i] = gbm_bo_get_stride_for_plane(bo, i);
+ offsets[i] = gbm_bo_get_offset(bo, i);
+ modifiers[i] = gbm_bo_get_modifier(bo);
+ }
+#else
+ handles[0] = gbm_bo_get_handle(bo).u32;
+ strides[0] = gbm_bo_get_stride(bo);
+ memset(offsets, 0, 16);
+#endif
+
+ uint32_t flags = 0;
+
+ if (modifiers[0] && modifiers[0] != DRM_FORMAT_MOD_INVALID)
+ {
+ flags |= DRM_MODE_FB_MODIFIERS;
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - using modifier: {}", __FUNCTION__,
+ DRMHELPERS::ModifierToString(modifiers[0]));
+ }
+
+ int ret = drmModeAddFB2WithModifiers(m_fd,
+ width,
+ height,
+ fb->format,
+ handles,
+ strides,
+ offsets,
+ modifiers,
+ &fb->fb_id,
+ flags);
+
+ if(ret < 0)
+ {
+ ret = drmModeAddFB2(m_fd,
+ width,
+ height,
+ fb->format,
+ handles,
+ strides,
+ offsets,
+ &fb->fb_id,
+ flags);
+
+ if (ret < 0)
+ {
+ delete (fb);
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - failed to add framebuffer: {} ({})", __FUNCTION__, strerror(errno), errno);
+ return nullptr;
+ }
+ }
+
+ gbm_bo_set_user_data(bo, fb, DrmFbDestroyCallback);
+
+ return fb;
+}
+
+bool CDRMUtils::FindPreferredMode()
+{
+ if (m_mode)
+ return true;
+
+ for (int i = 0, area = 0; i < m_connector->GetModesCount(); i++)
+ {
+ drmModeModeInfo* current_mode = m_connector->GetModeForIndex(i);
+
+ if(current_mode->type & DRM_MODE_TYPE_PREFERRED)
+ {
+ m_mode = current_mode;
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - found preferred mode: {}x{}{} @ {} Hz", __FUNCTION__,
+ m_mode->hdisplay, m_mode->vdisplay,
+ m_mode->flags & DRM_MODE_FLAG_INTERLACE ? "i" : "", m_mode->vrefresh);
+ break;
+ }
+
+ auto current_area = current_mode->hdisplay * current_mode->vdisplay;
+ if (current_area > area)
+ {
+ m_mode = current_mode;
+ area = current_area;
+ }
+ }
+
+ if(!m_mode)
+ {
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - failed to find preferred mode", __FUNCTION__);
+ return false;
+ }
+
+ return true;
+}
+
+bool CDRMUtils::FindPlanes()
+{
+ for (size_t i = 0; i < m_crtcs.size(); i++)
+ {
+ if (!(m_encoder->GetPossibleCrtcs() & (1 << i)))
+ continue;
+
+ auto videoPlane = std::find_if(m_planes.begin(), m_planes.end(), [&i](auto& plane) {
+ if (plane->GetPossibleCrtcs() & (1 << i))
+ {
+ return plane->SupportsFormat(DRM_FORMAT_NV12);
+ }
+ return false;
+ });
+
+ uint32_t videoPlaneId{0};
+
+ if (videoPlane != m_planes.end())
+ videoPlaneId = videoPlane->get()->GetPlaneId();
+
+ auto guiPlane =
+ std::find_if(m_planes.begin(), m_planes.end(), [&i, &videoPlaneId](auto& plane) {
+ if (plane->GetPossibleCrtcs() & (1 << i))
+ {
+ return (plane->GetPlaneId() != videoPlaneId &&
+ (videoPlaneId == 0 || plane->SupportsFormat(DRM_FORMAT_ARGB8888)) &&
+ (plane->SupportsFormat(DRM_FORMAT_XRGB2101010) ||
+ plane->SupportsFormat(DRM_FORMAT_XRGB8888)));
+ }
+ return false;
+ });
+
+ if (videoPlane != m_planes.end() && guiPlane != m_planes.end())
+ {
+ m_crtc = m_crtcs[i].get();
+ m_video_plane = videoPlane->get();
+ m_gui_plane = guiPlane->get();
+ break;
+ }
+
+ if (guiPlane != m_planes.end())
+ {
+ if (!m_crtc && m_encoder->GetCrtcId() == m_crtcs[i]->GetCrtcId())
+ {
+ m_crtc = m_crtcs[i].get();
+ m_gui_plane = guiPlane->get();
+ m_video_plane = nullptr;
+ }
+ }
+ }
+
+ CLog::Log(LOGINFO, "CDRMUtils::{} - using crtc: {}", __FUNCTION__, m_crtc->GetCrtcId());
+
+ // video plane may not be available
+ if (m_video_plane)
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - using video plane {}", __FUNCTION__,
+ m_video_plane->GetPlaneId());
+
+ if (m_gui_plane->SupportsFormat(DRM_FORMAT_XRGB2101010))
+ {
+ m_gui_plane->SetFormat(DRM_FORMAT_XRGB2101010);
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - using 10bit gui plane {}", __FUNCTION__,
+ m_gui_plane->GetPlaneId());
+ }
+ else
+ {
+ m_gui_plane->SetFormat(DRM_FORMAT_XRGB8888);
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - using gui plane {}", __FUNCTION__,
+ m_gui_plane->GetPlaneId());
+ }
+
+ return true;
+}
+
+void CDRMUtils::PrintDrmDeviceInfo(drmDevicePtr device)
+{
+ std::string message;
+
+ // clang-format off
+ message.append(fmt::format("CDRMUtils::{} - DRM Device Info:", __FUNCTION__));
+ message.append(fmt::format("\n available_nodes: {:#04x}", device->available_nodes));
+ message.append("\n nodes:");
+
+ for (int i = 0; i < DRM_NODE_MAX; i++)
+ {
+ if (device->available_nodes & 1 << i)
+ message.append(fmt::format("\n nodes[{}]: {}", i, device->nodes[i]));
+ }
+
+ message.append(fmt::format("\n bustype: {:#04x}", device->bustype));
+
+ if (device->bustype == DRM_BUS_PCI)
+ {
+ message.append("\n pci:");
+ message.append(fmt::format("\n domain: {:#04x}", device->businfo.pci->domain));
+ message.append(fmt::format("\n bus: {:#02x}", device->businfo.pci->bus));
+ message.append(fmt::format("\n dev: {:#02x}", device->businfo.pci->dev));
+ message.append(fmt::format("\n func: {:#1}", device->businfo.pci->func));
+
+ message.append("\n deviceinfo:");
+ message.append("\n pci:");
+ message.append(fmt::format("\n vendor_id: {:#04x}", device->deviceinfo.pci->vendor_id));
+ message.append(fmt::format("\n device_id: {:#04x}", device->deviceinfo.pci->device_id));
+ message.append(fmt::format("\n subvendor_id: {:#04x}", device->deviceinfo.pci->subvendor_id));
+ message.append(fmt::format("\n subdevice_id: {:#04x}", device->deviceinfo.pci->subdevice_id));
+ }
+ else if (device->bustype == DRM_BUS_USB)
+ {
+ message.append("\n usb:");
+ message.append(fmt::format("\n bus: {:#03x}", device->businfo.usb->bus));
+ message.append(fmt::format("\n dev: {:#03x}", device->businfo.usb->dev));
+
+ message.append("\n deviceinfo:");
+ message.append("\n usb:");
+ message.append(fmt::format("\n vendor: {:#04x}", device->deviceinfo.usb->vendor));
+ message.append(fmt::format("\n product: {:#04x}", device->deviceinfo.usb->product));
+ }
+ else if (device->bustype == DRM_BUS_PLATFORM)
+ {
+ message.append("\n platform:");
+ message.append(fmt::format("\n fullname: {}", device->businfo.platform->fullname));
+ }
+ else if (device->bustype == DRM_BUS_HOST1X)
+ {
+ message.append("\n host1x:");
+ message.append(fmt::format("\n fullname: {}", device->businfo.host1x->fullname));
+ }
+ else
+ message.append("\n unhandled bus type");
+ // clang-format on
+
+ CLog::Log(LOGDEBUG, "{}", message);
+}
+
+bool CDRMUtils::OpenDrm(bool needConnector)
+{
+ int numDevices = drmGetDevices2(0, nullptr, 0);
+ if (numDevices <= 0)
+ {
+ CLog::Log(LOGERROR, "CDRMUtils::{} - no drm devices found: ({})", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - drm devices found: {}", __FUNCTION__, numDevices);
+
+ std::vector<drmDevicePtr> devices(numDevices);
+
+ int ret = drmGetDevices2(0, devices.data(), devices.size());
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CDRMUtils::{} - drmGetDevices2 return an error: ({})", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ for (const auto device : devices)
+ {
+ if (!(device->available_nodes & 1 << DRM_NODE_PRIMARY))
+ continue;
+
+ close(m_fd);
+ m_fd = open(device->nodes[DRM_NODE_PRIMARY], O_RDWR | O_CLOEXEC);
+ if (m_fd < 0)
+ continue;
+
+ if (needConnector)
+ {
+ auto resources = drmModeGetResources(m_fd);
+ if (!resources)
+ continue;
+
+ m_connectors.clear();
+ for (int i = 0; i < resources->count_connectors; i++)
+ m_connectors.emplace_back(std::make_unique<CDRMConnector>(m_fd, resources->connectors[i]));
+
+ drmModeFreeResources(resources);
+
+ if (!FindConnector())
+ continue;
+ }
+
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - opened device: {}", __FUNCTION__,
+ device->nodes[DRM_NODE_PRIMARY]);
+
+ PrintDrmDeviceInfo(device);
+
+ const char* renderPath = drmGetRenderDeviceNameFromFd(m_fd);
+
+ if (!renderPath)
+ renderPath = drmGetDeviceNameFromFd2(m_fd);
+
+ if (!renderPath)
+ renderPath = drmGetDeviceNameFromFd(m_fd);
+
+ if (renderPath)
+ {
+ m_renderDevicePath = renderPath;
+ m_renderFd = open(renderPath, O_RDWR | O_CLOEXEC);
+ if (m_renderFd != 0)
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - opened render node: {}", __FUNCTION__, renderPath);
+ }
+
+ drmFreeDevices(devices.data(), devices.size());
+ return true;
+ }
+
+ drmFreeDevices(devices.data(), devices.size());
+ return false;
+}
+
+bool CDRMUtils::InitDrm()
+{
+ if (m_fd < 0)
+ return false;
+
+ /* caps need to be set before allocating connectors, encoders, crtcs, and planes */
+ int ret = drmSetClientCap(m_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
+ if (ret)
+ {
+ CLog::Log(LOGERROR, "CDRMUtils::{} - failed to set universal planes capability: {}",
+ __FUNCTION__, strerror(errno));
+ return false;
+ }
+
+ ret = drmSetClientCap(m_fd, DRM_CLIENT_CAP_STEREO_3D, 1);
+ if (ret)
+ {
+ CLog::Log(LOGERROR, "CDRMUtils::{} - failed to set stereo 3d capability: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+#if defined(DRM_CLIENT_CAP_ASPECT_RATIO)
+ ret = drmSetClientCap(m_fd, DRM_CLIENT_CAP_ASPECT_RATIO, 0);
+ if (ret != 0)
+ CLog::Log(LOGERROR, "CDRMUtils::{} - aspect ratio capability is not supported: {}",
+ __FUNCTION__, strerror(errno));
+#endif
+
+ auto resources = drmModeGetResources(m_fd);
+ if (!resources)
+ {
+ CLog::Log(LOGERROR, "CDRMUtils::{} - failed to get drm resources: {}", __FUNCTION__, strerror(errno));
+ return false;
+ }
+
+ m_connectors.clear();
+ for (int i = 0; i < resources->count_connectors; i++)
+ m_connectors.emplace_back(std::make_unique<CDRMConnector>(m_fd, resources->connectors[i]));
+
+ m_encoders.clear();
+ for (int i = 0; i < resources->count_encoders; i++)
+ m_encoders.emplace_back(std::make_unique<CDRMEncoder>(m_fd, resources->encoders[i]));
+
+ m_crtcs.clear();
+ for (int i = 0; i < resources->count_crtcs; i++)
+ m_crtcs.emplace_back(std::make_unique<CDRMCrtc>(m_fd, resources->crtcs[i]));
+
+ drmModeFreeResources(resources);
+
+ auto planeResources = drmModeGetPlaneResources(m_fd);
+ if (!planeResources)
+ {
+ CLog::Log(LOGERROR, "CDRMUtils::{} - failed to get drm plane resources: {}", __FUNCTION__, strerror(errno));
+ return false;
+ }
+
+ m_planes.clear();
+ for (uint32_t i = 0; i < planeResources->count_planes; i++)
+ {
+ m_planes.emplace_back(std::make_unique<CDRMPlane>(m_fd, planeResources->planes[i]));
+ m_planes[i]->FindModifiers();
+ }
+
+ drmModeFreePlaneResources(planeResources);
+
+ if (!FindConnector())
+ return false;
+
+ if (!FindEncoder())
+ return false;
+
+ if (!FindCrtc())
+ return false;
+
+ if (!FindPlanes())
+ return false;
+
+ if (!FindPreferredMode())
+ return false;
+
+ ret = drmSetMaster(m_fd);
+ if (ret < 0)
+ {
+ CLog::Log(LOGDEBUG,
+ "CDRMUtils::{} - failed to set drm master, will try to authorize instead: {}",
+ __FUNCTION__, strerror(errno));
+
+ drm_magic_t magic;
+
+ ret = drmGetMagic(m_fd, &magic);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CDRMUtils::{} - failed to get drm magic: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ ret = drmAuthMagic(m_fd, magic);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "CDRMUtils::{} - failed to authorize drm magic: {}", __FUNCTION__,
+ strerror(errno));
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CDRMUtils::{} - successfully authorized drm magic", __FUNCTION__);
+ }
+
+ return true;
+}
+
+bool CDRMUtils::FindConnector()
+{
+ auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return false;
+
+ auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return false;
+
+ std::vector<std::unique_ptr<CDRMConnector>>::iterator connector;
+
+ std::string connectorName = settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR);
+ if (connectorName != "Default")
+ {
+ connector = std::find_if(m_connectors.begin(), m_connectors.end(),
+ [&connectorName](auto& connector)
+ {
+ return connector->GetEncoderId() > 0 && connector->IsConnected() &&
+ connector->GetName() == connectorName;
+ });
+ }
+
+ if (connector == m_connectors.end())
+ {
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - failed to find specified connector: {}, trying default",
+ __FUNCTION__, connectorName);
+ connectorName = "Default";
+ }
+
+ if (connectorName == "Default")
+ {
+ connector = std::find_if(m_connectors.begin(), m_connectors.end(),
+ [](auto& connector)
+ { return connector->GetEncoderId() > 0 && connector->IsConnected(); });
+ }
+
+ if (connector == m_connectors.end())
+ {
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - failed to find connected connector", __FUNCTION__);
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CDRMUtils::{} - using connector: {}", __FUNCTION__,
+ connector->get()->GetName());
+
+ m_connector = connector->get();
+ return true;
+}
+
+bool CDRMUtils::FindEncoder()
+{
+ auto encoder = std::find_if(m_encoders.begin(), m_encoders.end(), [this](auto& encoder) {
+ return encoder->GetEncoderId() == m_connector->GetEncoderId();
+ });
+
+ if (encoder == m_encoders.end())
+ {
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - failed to find encoder for connector id: {}", __FUNCTION__,
+ *m_connector->GetConnectorId());
+ return false;
+ }
+
+ CLog::Log(LOGINFO, "CDRMUtils::{} - using encoder: {}", __FUNCTION__,
+ encoder->get()->GetEncoderId());
+
+ m_encoder = encoder->get();
+ return true;
+}
+
+bool CDRMUtils::FindCrtc()
+{
+ for (size_t i = 0; i < m_crtcs.size(); i++)
+ {
+ if (m_encoder->GetPossibleCrtcs() & (1 << i))
+ {
+ if (m_crtcs[i]->GetCrtcId() == m_encoder->GetCrtcId())
+ {
+ m_orig_crtc = m_crtcs[i].get();
+ if (m_orig_crtc->GetModeValid())
+ {
+ m_mode = m_orig_crtc->GetMode();
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - original crtc mode: {}x{}{} @ {} Hz", __FUNCTION__,
+ m_mode->hdisplay, m_mode->vdisplay,
+ m_mode->flags & DRM_MODE_FLAG_INTERLACE ? "i" : "", m_mode->vrefresh);
+ }
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+bool CDRMUtils::RestoreOriginalMode()
+{
+ if(!m_orig_crtc)
+ {
+ return false;
+ }
+
+ auto ret = drmModeSetCrtc(m_fd, m_orig_crtc->GetCrtcId(), m_orig_crtc->GetBufferId(),
+ m_orig_crtc->GetX(), m_orig_crtc->GetY(), m_connector->GetConnectorId(),
+ 1, m_orig_crtc->GetMode());
+
+ if(ret)
+ {
+ CLog::Log(LOGERROR, "CDRMUtils::{} - failed to set original crtc mode", __FUNCTION__);
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "CDRMUtils::{} - set original crtc mode", __FUNCTION__);
+
+ return true;
+}
+
+void CDRMUtils::DestroyDrm()
+{
+ RestoreOriginalMode();
+
+ if (drmAuthMagic(m_fd, 0) == EINVAL)
+ drmDropMaster(m_fd);
+
+ close(m_renderFd);
+ close(m_fd);
+
+ m_connector = nullptr;
+ m_encoder = nullptr;
+ m_crtc = nullptr;
+ m_orig_crtc = nullptr;
+ m_video_plane = nullptr;
+ m_gui_plane = nullptr;
+}
+
+RESOLUTION_INFO CDRMUtils::GetResolutionInfo(drmModeModeInfoPtr mode)
+{
+ RESOLUTION_INFO res;
+ res.iScreenWidth = mode->hdisplay;
+ res.iScreenHeight = mode->vdisplay;
+ res.iWidth = res.iScreenWidth;
+ res.iHeight = res.iScreenHeight;
+
+ int limit = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ SETTING_VIDEOSCREEN_LIMITGUISIZE);
+ if (limit > 0 && res.iScreenWidth > 1920 && res.iScreenHeight > 1080)
+ {
+ switch (limit)
+ {
+ case 1: // 720p
+ res.iWidth = 1280;
+ res.iHeight = 720;
+ break;
+ case 2: // 1080p / 720p (>30hz)
+ res.iWidth = mode->vrefresh > 30 ? 1280 : 1920;
+ res.iHeight = mode->vrefresh > 30 ? 720 : 1080;
+ break;
+ case 3: // 1080p
+ res.iWidth = 1920;
+ res.iHeight = 1080;
+ break;
+ case 4: // Unlimited / 1080p (>30hz)
+ res.iWidth = mode->vrefresh > 30 ? 1920 : res.iScreenWidth;
+ res.iHeight = mode->vrefresh > 30 ? 1080 : res.iScreenHeight;
+ break;
+ }
+ }
+
+ if (mode->clock % 5 != 0)
+ res.fRefreshRate = static_cast<float>(mode->vrefresh) * (1000.0f/1001.0f);
+ else
+ res.fRefreshRate = mode->vrefresh;
+ res.iSubtitles = res.iHeight;
+ res.fPixelRatio = 1.0f;
+ res.bFullScreen = true;
+
+ if (mode->flags & DRM_MODE_FLAG_3D_MASK)
+ {
+ if (mode->flags & DRM_MODE_FLAG_3D_TOP_AND_BOTTOM)
+ res.dwFlags = D3DPRESENTFLAG_MODE3DTB;
+ else if (mode->flags & DRM_MODE_FLAG_3D_SIDE_BY_SIDE_HALF)
+ res.dwFlags = D3DPRESENTFLAG_MODE3DSBS;
+ }
+ else if (mode->flags & DRM_MODE_FLAG_INTERLACE)
+ res.dwFlags = D3DPRESENTFLAG_INTERLACED;
+ else
+ res.dwFlags = D3DPRESENTFLAG_PROGRESSIVE;
+
+ res.strMode =
+ StringUtils::Format("{}x{}{} @ {:.6f} Hz", res.iScreenWidth, res.iScreenHeight,
+ res.dwFlags & D3DPRESENTFLAG_INTERLACED ? "i" : "", res.fRefreshRate);
+ return res;
+}
+
+RESOLUTION_INFO CDRMUtils::GetCurrentMode()
+{
+ return GetResolutionInfo(m_mode);
+}
+
+std::vector<RESOLUTION_INFO> CDRMUtils::GetModes()
+{
+ std::vector<RESOLUTION_INFO> resolutions;
+ resolutions.reserve(m_connector->GetModesCount());
+
+ for (auto i = 0; i < m_connector->GetModesCount(); i++)
+ {
+ RESOLUTION_INFO res = GetResolutionInfo(m_connector->GetModeForIndex(i));
+ res.strId = std::to_string(i);
+ resolutions.push_back(res);
+ }
+
+ return resolutions;
+}
+
+std::vector<std::string> CDRMUtils::GetConnectedConnectorNames()
+{
+ std::vector<std::string> connectorNames;
+ for (const auto& connector : m_connectors)
+ {
+ if (connector->IsConnected())
+ connectorNames.emplace_back(connector->GetName());
+ }
+
+ return connectorNames;
+}
+
+uint32_t CDRMUtils::FourCCWithAlpha(uint32_t fourcc)
+{
+ return (fourcc & 0xFFFFFF00) | static_cast<uint32_t>('A');
+}
+
+uint32_t CDRMUtils::FourCCWithoutAlpha(uint32_t fourcc)
+{
+ return (fourcc & 0xFFFFFF00) | static_cast<uint32_t>('X');
+}
diff --git a/xbmc/windowing/gbm/drm/DRMUtils.h b/xbmc/windowing/gbm/drm/DRMUtils.h
new file mode 100644
index 0000000..5327e35
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/DRMUtils.h
@@ -0,0 +1,103 @@
+/*
+ * 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 "DRMConnector.h"
+#include "DRMCrtc.h"
+#include "DRMEncoder.h"
+#include "DRMPlane.h"
+#include "windowing/Resolution.h"
+#include "windowing/gbm/GBMUtils.h"
+
+#include <vector>
+
+#include <gbm.h>
+#include <xf86drm.h>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+struct drm_fb
+{
+ struct gbm_bo *bo = nullptr;
+ uint32_t fb_id;
+ uint32_t format;
+};
+
+class CDRMUtils
+{
+public:
+ CDRMUtils() = default;
+ virtual ~CDRMUtils();
+ virtual void FlipPage(struct gbm_bo* bo, bool rendered, bool videoLayer) {}
+ virtual bool SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo* bo) { return false; }
+ virtual bool SetActive(bool active) { return false; }
+ virtual bool InitDrm();
+ virtual void DestroyDrm();
+
+ int GetFileDescriptor() const { return m_fd; }
+ int GetRenderNodeFileDescriptor() const { return m_renderFd; }
+ const char* GetRenderDevicePath() const { return m_renderDevicePath; }
+ CDRMPlane* GetVideoPlane() const { return m_video_plane; }
+ CDRMPlane* GetGuiPlane() const { return m_gui_plane; }
+ CDRMCrtc* GetCrtc() const { return m_crtc; }
+ CDRMConnector* GetConnector() const { return m_connector; }
+
+ std::vector<std::string> GetConnectedConnectorNames();
+
+ virtual RESOLUTION_INFO GetCurrentMode();
+ virtual std::vector<RESOLUTION_INFO> GetModes();
+ virtual bool SetMode(const RESOLUTION_INFO& res);
+
+ static uint32_t FourCCWithAlpha(uint32_t fourcc);
+ static uint32_t FourCCWithoutAlpha(uint32_t fourcc);
+
+protected:
+ bool OpenDrm(bool needConnector);
+ drm_fb* DrmFbGetFromBo(struct gbm_bo *bo);
+
+ int m_fd;
+ CDRMConnector* m_connector{nullptr};
+ CDRMEncoder* m_encoder{nullptr};
+ CDRMCrtc* m_crtc{nullptr};
+ CDRMCrtc* m_orig_crtc{nullptr};
+ CDRMPlane* m_video_plane{nullptr};
+ CDRMPlane* m_gui_plane{nullptr};
+ drmModeModeInfo *m_mode = nullptr;
+
+ int m_width = 0;
+ int m_height = 0;
+
+ std::vector<std::unique_ptr<CDRMPlane>> m_planes;
+
+private:
+ bool FindConnector();
+ bool FindEncoder();
+ bool FindCrtc();
+ bool FindPlanes();
+ bool FindPreferredMode();
+ bool RestoreOriginalMode();
+ RESOLUTION_INFO GetResolutionInfo(drmModeModeInfoPtr mode);
+ void PrintDrmDeviceInfo(drmDevicePtr device);
+
+ int m_renderFd;
+ const char* m_renderDevicePath{nullptr};
+
+ std::vector<std::unique_ptr<CDRMConnector>> m_connectors;
+ std::vector<std::unique_ptr<CDRMEncoder>> m_encoders;
+ std::vector<std::unique_ptr<CDRMCrtc>> m_crtcs;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/gbm/drm/OffScreenModeSetting.cpp b/xbmc/windowing/gbm/drm/OffScreenModeSetting.cpp
new file mode 100644
index 0000000..dc69fdc
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/OffScreenModeSetting.cpp
@@ -0,0 +1,52 @@
+/*
+ * 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 "OffScreenModeSetting.h"
+
+#include "utils/log.h"
+
+using namespace KODI::WINDOWING::GBM;
+
+namespace
+{
+constexpr int DISPLAY_WIDTH = 1280;
+constexpr int DISPLAY_HEIGHT = 720;
+constexpr float DISPLAY_REFRESH = 50.0f;
+} // namespace
+
+bool COffScreenModeSetting::InitDrm()
+{
+ if (!CDRMUtils::OpenDrm(false))
+ return false;
+
+ CLog::Log(LOGDEBUG, "COffScreenModeSetting::{} - initialized offscreen DRM", __FUNCTION__);
+ return true;
+}
+
+std::vector<RESOLUTION_INFO> COffScreenModeSetting::GetModes()
+{
+ std::vector<RESOLUTION_INFO> resolutions;
+ resolutions.push_back(GetCurrentMode());
+ return resolutions;
+}
+
+RESOLUTION_INFO COffScreenModeSetting::GetCurrentMode()
+{
+ RESOLUTION_INFO res;
+ res.iScreenWidth = DISPLAY_WIDTH;
+ res.iWidth = DISPLAY_WIDTH;
+ res.iScreenHeight = DISPLAY_HEIGHT;
+ res.iHeight = DISPLAY_HEIGHT;
+ res.fRefreshRate = DISPLAY_REFRESH;
+ res.iSubtitles = res.iHeight;
+ res.fPixelRatio = 1.0f;
+ res.bFullScreen = true;
+ res.strId = "0";
+
+ return res;
+}
diff --git a/xbmc/windowing/gbm/drm/OffScreenModeSetting.h b/xbmc/windowing/gbm/drm/OffScreenModeSetting.h
new file mode 100644
index 0000000..4270d4e
--- /dev/null
+++ b/xbmc/windowing/gbm/drm/OffScreenModeSetting.h
@@ -0,0 +1,38 @@
+/*
+ * 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 "DRMUtils.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace GBM
+{
+
+class COffScreenModeSetting : public CDRMUtils
+{
+public:
+ COffScreenModeSetting() = default;
+ ~COffScreenModeSetting() override = default;
+ void FlipPage(struct gbm_bo *bo, bool rendered, bool videoLayer) override {}
+ bool SetVideoMode(const RESOLUTION_INFO& res, struct gbm_bo *bo) override { return false; }
+ bool SetActive(bool active) override { return false; }
+ bool InitDrm() override;
+ void DestroyDrm() override {}
+
+ RESOLUTION_INFO GetCurrentMode() override;
+ std::vector<RESOLUTION_INFO> GetModes() override;
+ bool SetMode(const RESOLUTION_INFO& res) override { return true; }
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/ios/CMakeLists.txt b/xbmc/windowing/ios/CMakeLists.txt
new file mode 100644
index 0000000..0cdd0d6
--- /dev/null
+++ b/xbmc/windowing/ios/CMakeLists.txt
@@ -0,0 +1,8 @@
+set(SOURCES WinEventsIOS.mm
+ WinSystemIOS.mm
+ VideoSyncIos.cpp)
+set(HEADERS WinEventsIOS.h
+ WinSystemIOS.h
+ VideoSyncIos.h)
+
+core_add_library(windowing_ios)
diff --git a/xbmc/windowing/ios/VideoSyncIos.cpp b/xbmc/windowing/ios/VideoSyncIos.cpp
new file mode 100644
index 0000000..a3cac13
--- /dev/null
+++ b/xbmc/windowing/ios/VideoSyncIos.cpp
@@ -0,0 +1,95 @@
+/*
+ * 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 "VideoSyncIos.h"
+
+#include "cores/VideoPlayer/VideoReferenceClock.h"
+#include "utils/MathUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/ios/WinSystemIOS.h"
+
+bool CVideoSyncIos::Setup(PUPDATECLOCK func)
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncIos::{} setting up OSX", __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 CVideoSyncIos::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 CVideoSyncIos::Cleanup()
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncIos::{} cleaning up OSX", __FUNCTION__);
+ DeinitDisplayLink();
+ m_winSystem.Unregister(this);
+}
+
+float CVideoSyncIos::GetFps()
+{
+ m_fps = CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS();
+ CLog::Log(LOGDEBUG, "CVideoSyncIos::{} Detected refreshrate: {:f} hertz", __FUNCTION__, m_fps);
+ return m_fps;
+}
+
+void CVideoSyncIos::OnResetDisplay()
+{
+ m_abortEvent.Set();
+}
+
+void CVideoSyncIos::IosVblankHandler()
+{
+ int NrVBlanks;
+ double VBlankTime;
+ int64_t nowtime = CurrentHostCounter();
+
+ //calculate how many vblanks happened
+ VBlankTime = (double)(nowtime - m_LastVBlankTime) / (double)CurrentHostFrequency();
+ NrVBlanks = MathUtils::round_int(VBlankTime * static_cast<double>(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 CVideoSyncIos::InitDisplayLink()
+{
+ bool ret = true;
+ CLog::Log(LOGDEBUG, "CVideoSyncIos: setting up displaylink");
+ if (!m_winSystem.InitDisplayLink(this))
+ {
+ CLog::Log(LOGDEBUG, "CVideoSyncIos: InitDisplayLink failed");
+ ret = false;
+ }
+ return ret;
+}
+
+void CVideoSyncIos::DeinitDisplayLink()
+{
+ m_winSystem.DeinitDisplayLink();
+}
+
diff --git a/xbmc/windowing/ios/VideoSyncIos.h b/xbmc/windowing/ios/VideoSyncIos.h
new file mode 100644
index 0000000..0828e34
--- /dev/null
+++ b/xbmc/windowing/ios/VideoSyncIos.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 CWinSystemIOS;
+
+class CVideoSyncIos : public CVideoSync, IDispResource
+{
+public:
+ CVideoSyncIos(void *clock, CWinSystemIOS &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 IosVblankHandler();
+
+private:
+ // CVideoSyncDarwin interface
+ virtual bool InitDisplayLink();
+ virtual void DeinitDisplayLink();
+
+ int64_t m_LastVBlankTime = 0; //timestamp of the last vblank, used for calculating how many vblanks happened
+ CEvent m_abortEvent;
+ CWinSystemIOS &m_winSystem;
+};
+
diff --git a/xbmc/windowing/ios/WinEventsIOS.h b/xbmc/windowing/ios/WinEventsIOS.h
new file mode 100644
index 0000000..98ec4dc
--- /dev/null
+++ b/xbmc/windowing/ios/WinEventsIOS.h
@@ -0,0 +1,20 @@
+/*
+ * 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 "windowing/WinEvents.h"
+
+class CWinEventsIOS : public IWinEvents
+{
+public:
+ bool MessagePump() override;
+private:
+ size_t GetQueueSize();
+};
+
diff --git a/xbmc/windowing/ios/WinEventsIOS.mm b/xbmc/windowing/ios/WinEventsIOS.mm
new file mode 100644
index 0000000..ec416a6
--- /dev/null
+++ b/xbmc/windowing/ios/WinEventsIOS.mm
@@ -0,0 +1,55 @@
+/*
+ * 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 "WinEventsIOS.h"
+
+#include "application/AppInboundProtocol.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/InputManager.h"
+#include "input/XBMC_vkeys.h"
+#include "threads/CriticalSection.h"
+#include "utils/log.h"
+
+#include <list>
+#include <mutex>
+
+static CCriticalSection g_inputCond;
+
+static std::list<XBMC_Event> events;
+
+bool CWinEventsIOS::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;
+ {
+ std::unique_lock<CCriticalSection> lock(g_inputCond);
+ if (events.empty())
+ return ret;
+ pumpEvent = events.front();
+ events.pop_front();
+ }
+
+ if (appPort)
+ ret = appPort->OnEvent(pumpEvent);
+ }
+ return ret;
+}
+
+size_t CWinEventsIOS::GetQueueSize()
+{
+ std::unique_lock<CCriticalSection> lock(g_inputCond);
+ return events.size();
+}
diff --git a/xbmc/windowing/ios/WinSystemIOS.h b/xbmc/windowing/ios/WinSystemIOS.h
new file mode 100644
index 0000000..8320b60
--- /dev/null
+++ b/xbmc/windowing/ios/WinSystemIOS.h
@@ -0,0 +1,97 @@
+/*
+ * 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 "windowing/WinSystem.h"
+
+#include <string>
+#include <vector>
+
+#include <CoreVideo/CVOpenGLESTextureCache.h>
+
+class IDispResource;
+class CVideoSyncIos;
+struct CADisplayLinkWrapper;
+
+class CWinSystemIOS : public CWinSystemBase, public CRenderSystemGLES
+{
+public:
+ CWinSystemIOS();
+ ~CWinSystemIOS() override;
+
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ 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;
+
+ std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override;
+
+ std::vector<std::string> GetConnectedOutputs() override;
+
+ bool InitDisplayLink(CVideoSyncIos *syncImpl);
+ void DeinitDisplayLink(void);
+ void OnAppFocusChange(bool focus);
+ bool IsBackgrounded() const { return m_bIsBackgrounded; }
+ CVEAGLContext GetEAGLContextObj();
+ void MoveToTouchscreen();
+
+ // winevents override
+ bool MessagePump() override;
+
+protected:
+ 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;
+
+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;
+ int m_internalTouchscreenResolutionWidth = -1;
+ int m_internalTouchscreenResolutionHeight = -1;
+};
+
diff --git a/xbmc/windowing/ios/WinSystemIOS.mm b/xbmc/windowing/ios/WinSystemIOS.mm
new file mode 100644
index 0000000..805c573
--- /dev/null
+++ b/xbmc/windowing/ios/WinSystemIOS.mm
@@ -0,0 +1,501 @@
+/*
+ * 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 "WinSystemIOS.h"
+
+#include "ServiceBroker.h"
+#include "VideoSyncIos.h"
+#include "WinEventsIOS.h"
+#include "cores/AudioEngine/Sinks/AESinkDARWINIOS.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 "rendering/gles/ScreenshotSurfaceGLES.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WindowSystemFactory.h"
+
+#import "platform/darwin/ios/IOSScreenManager.h"
+#import "platform/darwin/ios/XBMCController.h"
+
+#include <mutex>
+#include <vector>
+
+#import <Foundation/Foundation.h>
+#import <OpenGLES/ES2/gl.h>
+#import <OpenGLES/ES2/glext.h>
+#import <QuartzCore/CADisplayLink.h>
+#import <dlfcn.h>
+
+#define CONST_TOUCHSCREEN "Touchscreen"
+#define CONST_EXTERNAL "External"
+
+// IOSDisplayLinkCallback is declared in the lower part of the file
+@interface IOSDisplayLinkCallback : NSObject
+{
+@private CVideoSyncIos *_videoSyncImpl;
+}
+@property (nonatomic, setter=SetVideoSyncImpl:) CVideoSyncIos *_videoSyncImpl;
+- (void) runDisplayLink;
+@end
+
+using namespace KODI;
+using namespace MESSAGING;
+
+struct CADisplayLinkWrapper
+{
+ CADisplayLink* impl;
+ IOSDisplayLinkCallback *callbackClass;
+};
+
+void CWinSystemIOS::Register()
+{
+ KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem);
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemIOS::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemIOS>();
+}
+
+int CWinSystemIOS::GetDisplayIndexFromSettings()
+{
+ std::string currentScreen = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR);
+
+ int screenIdx = 0;
+ if (currentScreen == CONST_EXTERNAL)
+ {
+ if ([[UIScreen screens] count] > 1)
+ {
+ screenIdx = 1;
+ }
+ else// screen 1 is setup but not connected
+ {
+ // force internal screen
+ MoveToTouchscreen();
+ }
+ }
+
+ return screenIdx;
+}
+
+CWinSystemIOS::CWinSystemIOS() : CWinSystemBase()
+{
+ m_bIsBackgrounded = false;
+ m_pDisplayLink = new CADisplayLinkWrapper;
+ m_pDisplayLink->callbackClass = [[IOSDisplayLinkCallback alloc] init];
+ m_winEvents.reset(new CWinEventsIOS());
+
+ CAESinkDARWINIOS::Register();
+}
+
+CWinSystemIOS::~CWinSystemIOS()
+{
+ delete m_pDisplayLink;
+}
+
+bool CWinSystemIOS::InitWindowSystem()
+{
+ return CWinSystemBase::InitWindowSystem();
+}
+
+bool CWinSystemIOS::DestroyWindowSystem()
+{
+ return true;
+}
+
+bool CWinSystemIOS::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res)
+{
+ //NSLog(@"%s", __PRETTY_FUNCTION__);
+
+ if(!SetFullScreen(fullScreen, res, false))
+ return false;
+
+ [g_xbmcController setFramebuffer];
+
+ m_bWindowCreated = true;
+
+ m_eglext = " ";
+
+ const char *tmpExtensions = (const char*) glGetString(GL_EXTENSIONS);
+ if (tmpExtensions != NULL)
+ {
+ m_eglext += tmpExtensions;
+ }
+
+ m_eglext += " ";
+
+ CLog::Log(LOGDEBUG, "EGL_EXTENSIONS:{}", m_eglext);
+
+ // 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);
+ CScreenshotSurfaceGLES::Register();
+
+ return true;
+}
+
+bool CWinSystemIOS::DestroyWindow()
+{
+ return true;
+}
+
+bool CWinSystemIOS::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ //NSLog(@"%s", __PRETTY_FUNCTION__);
+
+ if (m_nWidth != newWidth || m_nHeight != newHeight)
+ {
+ m_nWidth = newWidth;
+ m_nHeight = newHeight;
+ }
+
+ CRenderSystemGLES::ResetRenderSystem(newWidth, newHeight);
+
+ return true;
+}
+
+bool CWinSystemIOS::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ //NSLog(@"%s", __PRETTY_FUNCTION__);
+
+ m_nWidth = res.iWidth;
+ m_nHeight = res.iHeight;
+ m_bFullScreen = fullScreen;
+
+ CLog::Log(LOGDEBUG, "About to switch to {} x {}", m_nWidth, m_nHeight);
+ SwitchToVideoMode(res.iWidth, res.iHeight, static_cast<double>(res.fRefreshRate));
+ CRenderSystemGLES::ResetRenderSystem(res.iWidth, res.iHeight);
+
+ return true;
+}
+
+UIScreenMode *getModeForResolution(int width, int height, unsigned int screenIdx)
+{
+ auto screen = UIScreen.screens[screenIdx];
+ for (UIScreenMode* mode in screen.availableModes)
+ {
+ //for main screen also find modes where width and height are
+ //exchanged (because of the 90°degree rotated buildinscreens)
+ auto modeSize = mode.size;
+ if ((modeSize.width == width && modeSize.height == height) ||
+ (screenIdx == 0 && modeSize.width == height && modeSize.height == width))
+ {
+ CLog::Log(LOGDEBUG, "Found matching mode: {} x {}", modeSize.width, modeSize.height);
+ return mode;
+ }
+ }
+ CLog::Log(LOGERROR,"No matching mode found!");
+ return nil;
+}
+
+bool CWinSystemIOS::SwitchToVideoMode(int width, int height, double refreshrate)
+{
+ bool ret = false;
+ int screenIdx = GetDisplayIndexFromSettings();
+
+ //get the mode to pass to the controller
+ UIScreenMode *newMode = getModeForResolution(width, height, screenIdx);
+
+ if(newMode)
+ {
+ ret = [g_xbmcController changeScreen:screenIdx withMode:newMode];
+ }
+ return ret;
+}
+
+bool CWinSystemIOS::GetScreenResolution(int* w, int* h, double* fps, int screenIdx)
+{
+ UIScreen *screen = [[UIScreen screens] objectAtIndex:screenIdx];
+ CGSize screenSize = [screen currentMode].size;
+ *w = screenSize.width;
+ *h = screenSize.height;
+ *fps = 0.0;
+
+ //if current mode is 0x0 (happens with external screens which aren't active)
+ //then use the preferred mode
+ if(*h == 0 || *w ==0)
+ {
+ UIScreenMode *firstMode = [screen preferredMode];
+ *w = firstMode.size.width;
+ *h = firstMode.size.height;
+ }
+
+ // for mainscreen use the eagl bounds from xbmcController
+ // because mainscreen is might be 90° rotate dependend on
+ // the device and eagl gives the correct values in all cases.
+ if(screenIdx == 0)
+ {
+ // at very first start up we cache the internal screen resolution
+ // because when using external screens and need to go back
+ // to internal we are not able to determine the eagl bounds
+ // before we really switched back to internal
+ // but display settings ask for the internal resolution before
+ // switching. So we give the cached values back in that case.
+ if (m_internalTouchscreenResolutionWidth == -1 &&
+ m_internalTouchscreenResolutionHeight == -1)
+ {
+ m_internalTouchscreenResolutionWidth = [g_xbmcController getScreenSize].width;
+ m_internalTouchscreenResolutionHeight = [g_xbmcController getScreenSize].height;
+ }
+
+ *w = m_internalTouchscreenResolutionWidth;
+ *h = m_internalTouchscreenResolutionHeight;
+ }
+ CLog::Log(LOGDEBUG, "Current resolution Screen: {} with {} x {}", screenIdx, *w, *h);
+ return true;
+}
+
+void CWinSystemIOS::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), screenIdx == 0 ? CONST_TOUCHSCREEN : CONST_EXTERNAL, 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 CWinSystemIOS::FillInVideoModes(int screenIdx)
+{
+ // Add full screen settings for additional monitors
+ RESOLUTION_INFO res;
+ int w, h;
+ // atm we don't get refreshrate info from iOS
+ // but this may change in the future. In that case
+ // we will adapt this code for filling some
+ // useful info into this local var :)
+ double refreshrate = 0.0;
+ //screen 0 is mainscreen - 1 has to be the external one...
+ UIScreen *aScreen = [[UIScreen screens]objectAtIndex:screenIdx];
+ //found external screen
+ for ( UIScreenMode *mode in [aScreen availableModes] )
+ {
+ w = mode.size.width;
+ h = mode.size.height;
+
+ UpdateDesktopResolution(res, screenIdx == 0 ? CONST_TOUCHSCREEN : CONST_EXTERNAL, w, h, refreshrate, 0);
+ CLog::Log(LOGINFO, "Found possible resolution for display {} with {} x {}", screenIdx, w, h);
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res);
+ CDisplaySettings::GetInstance().AddResolutionInfo(res);
+ }
+}
+
+bool CWinSystemIOS::IsExtSupported(const char* extension) const
+{
+ if(strncmp(extension, "EGL_", 4) != 0)
+ return CRenderSystemGLES::IsExtSupported(extension);
+
+ std::string name;
+
+ name = " ";
+ name += extension;
+ name += " ";
+
+ return m_eglext.find(name) != std::string::npos;
+}
+
+bool CWinSystemIOS::BeginRender()
+{
+ bool rtn;
+
+ [g_xbmcController setFramebuffer];
+
+ rtn = CRenderSystemGLES::BeginRender();
+ return rtn;
+}
+
+bool CWinSystemIOS::EndRender()
+{
+ bool rtn;
+
+ rtn = CRenderSystemGLES::EndRender();
+ return rtn;
+}
+
+void CWinSystemIOS::Register(IDispResource *resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ m_resources.push_back(resource);
+}
+
+void CWinSystemIOS::Unregister(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> 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 CWinSystemIOS::OnAppFocusChange(bool focus)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ m_bIsBackgrounded = !focus;
+ CLog::Log(LOGDEBUG, "CWinSystemIOS::OnAppFocusChange: {}", focus ? 1 : 0);
+ for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); i++)
+ (*i)->OnAppFocusChange(focus);
+}
+
+//--------------------------------------------------------------
+//-------------------DisplayLink stuff
+@implementation IOSDisplayLinkCallback
+@synthesize _videoSyncImpl;
+//--------------------------------------------------------------
+- (void) runDisplayLink
+{
+ @autoreleasepool
+ {
+ if (_videoSyncImpl != nil)
+ {
+ _videoSyncImpl->IosVblankHandler();
+ }
+ }
+}
+@end
+
+bool CWinSystemIOS::InitDisplayLink(CVideoSyncIos *syncImpl)
+{
+ //init with the appropriate display link for the
+ //used screen
+ if([[IOSScreenManager sharedInstance] isExternalScreen])
+ {
+ fprintf(stderr,"InitDisplayLink on external");
+ }
+ else
+ {
+ fprintf(stderr,"InitDisplayLink on internal");
+ }
+
+ unsigned int currentScreenIdx = [[IOSScreenManager sharedInstance] GetScreenIdx];
+ UIScreen * currentScreen = [[UIScreen screens] objectAtIndex:currentScreenIdx];
+ [m_pDisplayLink->callbackClass SetVideoSyncImpl:syncImpl];
+ m_pDisplayLink->impl = [currentScreen displayLinkWithTarget:m_pDisplayLink->callbackClass selector:@selector(runDisplayLink)];
+
+ [m_pDisplayLink->impl setPreferredFramesPerSecond:0];
+ [m_pDisplayLink->impl addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
+ return m_pDisplayLink->impl != nil;
+}
+
+void CWinSystemIOS::DeinitDisplayLink(void)
+{
+ if (m_pDisplayLink->impl)
+ {
+ [m_pDisplayLink->impl invalidate];
+ m_pDisplayLink->impl = nil;
+ [m_pDisplayLink->callbackClass SetVideoSyncImpl:nil];
+ }
+}
+//------------DisplayLink stuff end
+//--------------------------------------------------------------
+
+void CWinSystemIOS::PresentRenderImpl(bool rendered)
+{
+ //glFlush;
+ if (rendered)
+ [g_xbmcController presentFramebuffer];
+}
+
+bool CWinSystemIOS::HasCursor()
+{
+ // apple touch devices
+ return false;
+}
+
+void CWinSystemIOS::NotifyAppActiveChange(bool bActivated)
+{
+ if (bActivated && m_bWasFullScreenBeforeMinimize && !CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot())
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN);
+}
+
+bool CWinSystemIOS::Minimize()
+{
+ m_bWasFullScreenBeforeMinimize = CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot();
+ if (m_bWasFullScreenBeforeMinimize)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN);
+
+ return true;
+}
+
+bool CWinSystemIOS::Restore()
+{
+ return false;
+}
+
+bool CWinSystemIOS::Hide()
+{
+ return true;
+}
+
+bool CWinSystemIOS::Show(bool raise)
+{
+ return true;
+}
+
+CVEAGLContext CWinSystemIOS::GetEAGLContextObj()
+{
+ return [g_xbmcController getEAGLContextObj];
+}
+
+std::vector<std::string> CWinSystemIOS::GetConnectedOutputs()
+{
+ std::vector<std::string> outputs;
+ outputs.emplace_back("Default");
+ outputs.emplace_back(CONST_TOUCHSCREEN);
+ if ([[UIScreen screens] count] > 1)
+ {
+ outputs.emplace_back(CONST_EXTERNAL);
+ }
+
+ return outputs;
+}
+
+void CWinSystemIOS::MoveToTouchscreen()
+{
+ CDisplaySettings::GetInstance().SetMonitor(CONST_TOUCHSCREEN);
+}
+
+std::unique_ptr<CVideoSync> CWinSystemIOS::GetVideoSync(void *clock)
+{
+ std::unique_ptr<CVideoSync> pVSync(new CVideoSyncIos(clock, *this));
+ return pVSync;
+}
+
+bool CWinSystemIOS::MessagePump()
+{
+ return m_winEvents->MessagePump();
+}
diff --git a/xbmc/windowing/linux/CMakeLists.txt b/xbmc/windowing/linux/CMakeLists.txt
new file mode 100644
index 0000000..2a36d63
--- /dev/null
+++ b/xbmc/windowing/linux/CMakeLists.txt
@@ -0,0 +1,16 @@
+set(SOURCES "")
+set(HEADERS "")
+
+if(DBUS_FOUND)
+ list(APPEND SOURCES OSScreenSaverFreedesktop.cpp)
+ list(APPEND HEADERS OSScreenSaverFreedesktop.h)
+endif()
+
+if(EGL_FOUND)
+ list(APPEND SOURCES WinSystemEGL.cpp)
+ list(APPEND HEADERS WinSystemEGL.h)
+endif()
+
+if(SOURCES)
+ core_add_library(windowing_linux)
+endif()
diff --git a/xbmc/windowing/linux/OSScreenSaverFreedesktop.cpp b/xbmc/windowing/linux/OSScreenSaverFreedesktop.cpp
new file mode 100644
index 0000000..167930d
--- /dev/null
+++ b/xbmc/windowing/linux/OSScreenSaverFreedesktop.cpp
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017-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 "OSScreenSaverFreedesktop.h"
+
+#include "CompileInfo.h"
+#include "guilib/LocalizeStrings.h"
+#include "utils/log.h"
+
+#include "platform/linux/DBusMessage.h"
+#include "platform/linux/DBusUtil.h"
+
+using namespace KODI::WINDOWING::LINUX;
+
+namespace
+{
+const std::string SCREENSAVER_OBJECT = "/org/freedesktop/ScreenSaver";
+const std::string SCREENSAVER_INTERFACE = "org.freedesktop.ScreenSaver";
+}
+
+bool COSScreenSaverFreedesktop::IsAvailable()
+{
+ // Test by making a call to a function without side-effects
+ CDBusMessage dummyMessage(SCREENSAVER_INTERFACE, SCREENSAVER_OBJECT, SCREENSAVER_INTERFACE, "GetActive");
+ CDBusError error;
+ dummyMessage.SendSession(error);
+ // We do not care whether GetActive() is actually supported, we're just checking for the name to be there
+ // (GNOME for example does not support GetActive)
+ return !error || error.Name() == DBUS_ERROR_NOT_SUPPORTED;
+}
+
+void COSScreenSaverFreedesktop::Inhibit()
+{
+ if (m_inhibited)
+ {
+ return;
+ }
+
+ CDBusMessage inhibitMessage(SCREENSAVER_INTERFACE, SCREENSAVER_OBJECT, SCREENSAVER_INTERFACE, "Inhibit");
+ inhibitMessage.AppendArguments(std::string(CCompileInfo::GetAppName()),
+ g_localizeStrings.Get(14086));
+ if (!inhibitMessage.SendSession())
+ {
+ // DBus call failed
+ CLog::Log(LOGERROR, "Inhibiting freedesktop screen saver failed");
+ return;
+ }
+
+ if (!inhibitMessage.GetReplyArguments(&m_cookie))
+ {
+ CLog::Log(LOGERROR, "Could not retrieve cookie from org.freedesktop.ScreenSaver Inhibit response");
+ return;
+ }
+
+ m_inhibited = true;
+}
+
+void COSScreenSaverFreedesktop::Uninhibit()
+{
+ if (!m_inhibited)
+ {
+ return;
+ }
+
+ CDBusMessage uninhibitMessage(SCREENSAVER_INTERFACE, SCREENSAVER_OBJECT, SCREENSAVER_INTERFACE, "UnInhibit");
+ uninhibitMessage.AppendArgument(m_cookie);
+ if (!uninhibitMessage.SendSession())
+ {
+ CLog::Log(LOGERROR, "Uninhibiting freedesktop screen saver failed");
+ }
+
+ m_inhibited = false;
+}
diff --git a/xbmc/windowing/linux/OSScreenSaverFreedesktop.h b/xbmc/windowing/linux/OSScreenSaverFreedesktop.h
new file mode 100644
index 0000000..041e8ab
--- /dev/null
+++ b/xbmc/windowing/linux/OSScreenSaverFreedesktop.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017-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 "../OSScreenSaver.h"
+
+#include <cstdint>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace LINUX
+{
+
+// FIXME This is not really linux-specific, BSD could also have this. Better directory name?
+
+class COSScreenSaverFreedesktop : public IOSScreenSaver
+{
+public:
+ COSScreenSaverFreedesktop() = default;
+
+ static bool IsAvailable();
+ void Inhibit() override;
+ void Uninhibit() override;
+
+private:
+ bool m_inhibited{false};
+ std::uint32_t m_cookie;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/linux/WinSystemEGL.cpp b/xbmc/windowing/linux/WinSystemEGL.cpp
new file mode 100644
index 0000000..f791b91
--- /dev/null
+++ b/xbmc/windowing/linux/WinSystemEGL.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2005-2020 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 "WinSystemEGL.h"
+
+using namespace KODI::WINDOWING::LINUX;
+
+CWinSystemEGL::CWinSystemEGL(EGLenum platform, std::string const& platformExtension)
+ : m_eglContext{platform, platformExtension}
+{
+}
+
+EGLDisplay CWinSystemEGL::GetEGLDisplay() const
+{
+ return m_eglContext.GetEGLDisplay();
+}
+
+EGLSurface CWinSystemEGL::GetEGLSurface() const
+{
+ return m_eglContext.GetEGLSurface();
+}
+
+EGLContext CWinSystemEGL::GetEGLContext() const
+{
+ return m_eglContext.GetEGLContext();
+}
+
+EGLConfig CWinSystemEGL::GetEGLConfig() const
+{
+ return m_eglContext.GetEGLConfig();
+}
diff --git a/xbmc/windowing/linux/WinSystemEGL.h b/xbmc/windowing/linux/WinSystemEGL.h
new file mode 100644
index 0000000..44bb5e4
--- /dev/null
+++ b/xbmc/windowing/linux/WinSystemEGL.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2005-2020 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 "utils/EGLUtils.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace LINUX
+{
+
+class CWinSystemEGL
+{
+public:
+ CWinSystemEGL(EGLenum platform, std::string const& platformExtension);
+ ~CWinSystemEGL() = default;
+
+ EGLDisplay GetEGLDisplay() const;
+ EGLSurface GetEGLSurface() const;
+ EGLContext GetEGLContext() const;
+ EGLConfig GetEGLConfig() const;
+
+protected:
+ CEGLContextUtils m_eglContext;
+};
+
+} // namespace LINUX
+} // namespace WINDOWING
+} // namespace KODI
diff --git a/xbmc/windowing/osx/CMakeLists.txt b/xbmc/windowing/osx/CMakeLists.txt
new file mode 100644
index 0000000..433f5cf
--- /dev/null
+++ b/xbmc/windowing/osx/CMakeLists.txt
@@ -0,0 +1,17 @@
+set(SOURCES CocoaDPMSSupport.cpp
+ OSScreenSaverOSX.cpp
+ VideoSyncOsx.mm)
+set(HEADERS CocoaDPMSSupport.h
+ OSScreenSaverOSX.h
+ VideoSyncOsx.h)
+
+if(NOT SDL_FOUND)
+ list(APPEND SOURCES WinEventsOSX.mm
+ WinEventsOSXImpl.mm
+ WinSystemOSX.mm)
+ list(APPEND HEADERS WinEventsOSX.h
+ WinEventsOSXImpl.h
+ WinSystemOSX.h)
+endif()
+
+core_add_library(windowing_osx)
diff --git a/xbmc/windowing/osx/CocoaDPMSSupport.cpp b/xbmc/windowing/osx/CocoaDPMSSupport.cpp
new file mode 100644
index 0000000..bc86220
--- /dev/null
+++ b/xbmc/windowing/osx/CocoaDPMSSupport.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2009-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 "CocoaDPMSSupport.h"
+
+#include "ServiceBroker.h"
+#include "utils/log.h"
+
+#include <CoreFoundation/CFNumber.h>
+#include <IOKit/IOKitLib.h>
+
+CCocoaDPMSSupport::CCocoaDPMSSupport()
+{
+ m_supportedModes.push_back(OFF);
+ m_supportedModes.push_back(STANDBY);
+}
+
+bool CCocoaDPMSSupport::EnablePowerSaving(PowerSavingMode mode)
+{
+ // http://lists.apple.com/archives/Cocoa-dev/2007/Nov/msg00267.html
+ // This is an unsupported system call that might kernel panic on PPC boxes
+ // The reported OSX-PPC panic is unverified so we are going to enable this until
+ // we find out which OSX-PPC boxes have problems, then add detect/disable for those boxes.
+ io_registry_entry_t r = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/IOResources/IODisplayWrangler");
+ if (!r)
+ return false;
+
+ switch (mode)
+ {
+ case OFF:
+ case STANDBY:
+ return (IORegistryEntrySetCFProperty(r, CFSTR("IORequestIdle"), kCFBooleanTrue) == 0);
+ default:
+ return false;
+ }
+}
+
+bool CCocoaDPMSSupport::DisablePowerSaving()
+{
+ // http://lists.apple.com/archives/Cocoa-dev/2007/Nov/msg00267.html
+ // This is an unsupported system call that might kernel panic on PPC boxes
+ // The reported OSX-PPC panic is unverified so we are going to enable this until
+ // we find out which OSX-PPC boxes have problems, then add detect/disable for those boxes.
+ io_registry_entry_t r = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/IOResources/IODisplayWrangler");
+ if (!r)
+ return false;
+
+ // Turn display on
+ return (IORegistryEntrySetCFProperty(r, CFSTR("IORequestIdle"), kCFBooleanFalse) == 0);
+}
diff --git a/xbmc/windowing/osx/CocoaDPMSSupport.h b/xbmc/windowing/osx/CocoaDPMSSupport.h
new file mode 100644
index 0000000..6a54f42
--- /dev/null
+++ b/xbmc/windowing/osx/CocoaDPMSSupport.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2009-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 "xbmc/powermanagement/DPMSSupport.h"
+
+class CCocoaDPMSSupport : public CDPMSSupport
+{
+public:
+ CCocoaDPMSSupport();
+ ~CCocoaDPMSSupport() override = default;
+
+protected:
+ bool EnablePowerSaving(PowerSavingMode mode) override;
+ bool DisablePowerSaving() override;
+};
diff --git a/xbmc/windowing/osx/OSScreenSaverOSX.cpp b/xbmc/windowing/osx/OSScreenSaverOSX.cpp
new file mode 100644
index 0000000..019f86c
--- /dev/null
+++ b/xbmc/windowing/osx/OSScreenSaverOSX.cpp
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017-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 "OSScreenSaverOSX.h"
+
+#include <CoreFoundation/CoreFoundation.h>
+
+void COSScreenSaverOSX::Inhibit()
+{
+ // see Technical Q&A QA1340
+ if (m_assertionID == 0)
+ {
+ CFStringRef reasonForActivity= CFSTR("XBMC requested disable system screen saver");
+ IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep,
+ kIOPMAssertionLevelOn, reasonForActivity, &m_assertionID);
+ }
+}
+
+void COSScreenSaverOSX::Uninhibit()
+{
+ if (m_assertionID != 0)
+ {
+ IOPMAssertionRelease(m_assertionID);
+ m_assertionID = 0;
+ }
+} \ No newline at end of file
diff --git a/xbmc/windowing/osx/OSScreenSaverOSX.h b/xbmc/windowing/osx/OSScreenSaverOSX.h
new file mode 100644
index 0000000..c79367a
--- /dev/null
+++ b/xbmc/windowing/osx/OSScreenSaverOSX.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2017-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 "../OSScreenSaver.h"
+
+#import <IOKit/pwr_mgt/IOPMLib.h>
+
+class COSScreenSaverOSX : public KODI::WINDOWING::IOSScreenSaver
+{
+public:
+ COSScreenSaverOSX() = default;
+ void Inhibit() override;
+ void Uninhibit() override;
+
+private:
+ IOPMAssertionID m_assertionID = 0;
+};
diff --git a/xbmc/windowing/osx/OpenGL/CMakeLists.txt b/xbmc/windowing/osx/OpenGL/CMakeLists.txt
new file mode 100644
index 0000000..fd250d7
--- /dev/null
+++ b/xbmc/windowing/osx/OpenGL/CMakeLists.txt
@@ -0,0 +1,14 @@
+if(OPENGL_FOUND)
+ set(SOURCES WinSystemOSXGL.mm)
+ set(HEADERS WinSystemOSXGL.h)
+
+ if(NOT SDL_FOUND)
+ list(APPEND SOURCES OSXGLView.mm
+ OSXGLWindow.mm)
+ list(APPEND HEADERS OSXGLView.h
+ OSXGLWindow.h)
+ endif()
+
+ core_add_library(windowing_osx_opengl)
+
+endif()
diff --git a/xbmc/windowing/osx/OpenGL/OSXGLView.h b/xbmc/windowing/osx/OpenGL/OSXGLView.h
new file mode 100644
index 0000000..577642d
--- /dev/null
+++ b/xbmc/windowing/osx/OpenGL/OSXGLView.h
@@ -0,0 +1,18 @@
+#pragma once
+
+/*
+ * Copyright (C) 2021- 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 <Cocoa/Cocoa.h>
+
+@interface OSXGLView : NSOpenGLView
+
+- (id)initWithFrame:(NSRect)frameRect;
+- (NSOpenGLContext*)getGLContext;
+
+@end
diff --git a/xbmc/windowing/osx/OpenGL/OSXGLView.mm b/xbmc/windowing/osx/OpenGL/OSXGLView.mm
new file mode 100644
index 0000000..6c0a8b5
--- /dev/null
+++ b/xbmc/windowing/osx/OpenGL/OSXGLView.mm
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2021- 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 "OSXGLView.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "application/AppParamParser.h"
+#include "application/Application.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+
+#include "system_gl.h"
+
+@implementation OSXGLView
+{
+ NSOpenGLContext* m_glcontext;
+ NSOpenGLPixelFormat* m_pixFmt;
+ NSTrackingArea* m_trackingArea;
+ BOOL pause;
+}
+
+- (id)initWithFrame:(NSRect)frameRect
+{
+ NSOpenGLPixelFormatAttribute wattrs[] = {
+ NSOpenGLPFANoRecovery, NSOpenGLPFAAccelerated,
+ NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
+ NSOpenGLPFAColorSize, (NSOpenGLPixelFormatAttribute)32,
+ NSOpenGLPFAAlphaSize, (NSOpenGLPixelFormatAttribute)8,
+ NSOpenGLPFADepthSize, (NSOpenGLPixelFormatAttribute)24,
+ NSOpenGLPFADoubleBuffer, (NSOpenGLPixelFormatAttribute)0};
+
+ self = [super initWithFrame:frameRect];
+ if (self)
+ {
+ m_pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs];
+ m_glcontext = [[NSOpenGLContext alloc] initWithFormat:m_pixFmt shareContext:nil];
+ }
+
+ [self updateTrackingAreas];
+
+ GLint swapInterval = 1;
+ [m_glcontext setValues:&swapInterval forParameter:NSOpenGLCPSwapInterval];
+ [m_glcontext makeCurrentContext];
+
+ return self;
+}
+
+- (void)dealloc
+{
+ [NSOpenGLContext clearCurrentContext];
+ [m_glcontext clearDrawable];
+}
+
+- (void)drawRect:(NSRect)rect
+{
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ [m_glcontext setView:self];
+
+ // clear screen on first render
+ glClearColor(0, 0, 0, 0);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glClearColor(0, 0, 0, 0);
+
+ [m_glcontext update];
+ });
+}
+
+- (void)updateTrackingAreas
+{
+ if (m_trackingArea != nil)
+ {
+ [self removeTrackingArea:m_trackingArea];
+ }
+
+ const int opts =
+ (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways);
+ m_trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds
+ options:opts
+ owner:self
+ userInfo:nil];
+ [self addTrackingArea:m_trackingArea];
+}
+
+- (void)mouseEntered:(NSEvent*)theEvent
+{
+ [NSCursor hide];
+}
+
+- (void)mouseMoved:(NSEvent*)theEvent
+{
+}
+
+- (void)mouseExited:(NSEvent*)theEvent
+{
+ [NSCursor unhide];
+}
+
+- (NSOpenGLContext*)getGLContext
+{
+ return m_glcontext;
+}
+@end
diff --git a/xbmc/windowing/osx/OpenGL/OSXGLWindow.h b/xbmc/windowing/osx/OpenGL/OSXGLWindow.h
new file mode 100644
index 0000000..a722804
--- /dev/null
+++ b/xbmc/windowing/osx/OpenGL/OSXGLWindow.h
@@ -0,0 +1,19 @@
+#pragma once
+
+/*
+ * Copyright (C) 2021- 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 <Cocoa/Cocoa.h>
+
+@interface OSXGLWindow : NSWindow <NSWindowDelegate>
+
+@property(atomic) bool resizeState;
+
+- (id)initWithContentRect:(NSRect)box styleMask:(uint)style;
+
+@end
diff --git a/xbmc/windowing/osx/OpenGL/OSXGLWindow.mm b/xbmc/windowing/osx/OpenGL/OSXGLWindow.mm
new file mode 100644
index 0000000..8ee26fc
--- /dev/null
+++ b/xbmc/windowing/osx/OpenGL/OSXGLWindow.mm
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2021- 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 "OSXGLWindow.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "application/AppParamParser.h"
+#include "application/Application.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#import "windowing/osx/OpenGL/OSXGLView.h"
+#include "windowing/osx/WinEventsOSX.h"
+#import "windowing/osx/WinSystemOSX.h"
+
+#include "platform/darwin/osx/CocoaInterface.h"
+
+//------------------------------------------------------------------------------------------
+@implementation OSXGLWindow
+
+@synthesize resizeState = m_resizeState;
+
+- (id)initWithContentRect:(NSRect)box styleMask:(uint)style
+{
+ self = [super initWithContentRect:box styleMask:style backing:NSBackingStoreBuffered defer:YES];
+ [self setDelegate:self];
+ [self setAcceptsMouseMovedEvents:YES];
+ // autosave the window position/size
+ // Tell the controller to not cascade its windows.
+ [self.windowController setShouldCascadeWindows:NO];
+ [self setFrameAutosaveName:@"OSXGLWindowPositionHeightWidth"];
+
+ g_application.m_AppFocused = true;
+
+ return self;
+}
+
+- (void)dealloc
+{
+ [self setDelegate:nil];
+}
+
+- (BOOL)windowShouldClose:(id)sender
+{
+ if (!g_application.m_bStop)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+
+ return NO;
+}
+
+- (void)windowDidExpose:(NSNotification*)aNotification
+{
+ g_application.m_AppFocused = true;
+}
+
+- (void)windowDidMove:(NSNotification*)aNotification
+{
+ NSOpenGLContext* context = NSOpenGLContext.currentContext;
+ if (context)
+ {
+ if (context.view)
+ {
+ NSPoint window_origin = [[[context view] window] frame].origin;
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_VIDEOMOVE;
+ newEvent.move.x = window_origin.x;
+ newEvent.move.y = window_origin.y;
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ }
+ }
+}
+
+- (void)windowWillStartLiveResize:(NSNotification*)notification
+{
+ m_resizeState = true;
+}
+
+- (void)windowDidEndLiveResize:(NSNotification*)notification
+{
+ m_resizeState = false;
+ [self windowDidResize:notification];
+}
+
+- (void)windowDidResize:(NSNotification*)aNotification
+{
+ if (!m_resizeState)
+ {
+ NSRect rect = [self contentRectForFrameRect:self.frame];
+ int width = static_cast<int>(rect.size.width);
+ int height = static_cast<int>(rect.size.height);
+
+ XBMC_Event newEvent = {};
+
+ if (!CServiceBroker::GetWinSystem()->IsFullScreen())
+ {
+ RESOLUTION res_index = RES_DESKTOP;
+ if ((width == CDisplaySettings::GetInstance().GetResolutionInfo(res_index).iWidth) &&
+ (height == CDisplaySettings::GetInstance().GetResolutionInfo(res_index).iHeight))
+ return;
+
+ newEvent.type = XBMC_VIDEORESIZE;
+ }
+ else
+ {
+ // macos may trigger a resize/rescale event just after Kodi has entered fullscreen
+ // (from windowDidEndLiveResize). Kodi needs to rescale the UI - use a different event
+ // type since XBMC_VIDEORESIZE is supposed to only be used in windowed mode
+ newEvent.type = XBMC_FULLSCREEN_UPDATE;
+ newEvent.move.x = -1;
+ newEvent.move.y = -1;
+ }
+
+ newEvent.resize.w = width;
+ newEvent.resize.h = height;
+
+ // check for valid sizes cause in some cases
+ // we are hit during fullscreen transition from macos
+ // and might be technically "zero" sized
+ if (newEvent.resize.w != 0 && newEvent.resize.h != 0)
+ {
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ }
+ }
+}
+
+- (void)windowDidChangeScreen:(NSNotification*)notification
+{
+ // user has moved the window to a
+ // different screen
+ // if (CServiceBroker::GetWinSystem()->IsFullScreen())
+ // CServiceBroker::GetWinSystem()->SetMovedToOtherScreen(true);
+}
+
+- (NSSize)windowWillResize:(NSWindow*)sender toSize:(NSSize)frameSize
+{
+ return frameSize;
+}
+
+- (void)windowWillEnterFullScreen:(NSNotification*)pNotification
+{
+ CWinSystemOSX* winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem());
+ if (!winSystem)
+ return;
+
+ // if osx is the issuer of the toggle
+ // call Kodi's toggle function
+ if (!winSystem->GetFullscreenWillToggle())
+ {
+ // indicate that we are toggling
+ // flag will be reset in SetFullscreen once its
+ // called from Kodi's gui thread
+ winSystem->SetFullscreenWillToggle(true);
+
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN);
+ }
+ else
+ {
+ // in this case we are just called because
+ // of Kodi did a toggle - just reset the flag
+ // we don't need to do anything else
+ winSystem->SetFullscreenWillToggle(false);
+ }
+}
+
+- (void)windowDidExitFullScreen:(NSNotification*)pNotification
+{
+ auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem());
+ if (!winSystem)
+ return;
+
+ // if osx is the issuer of the toggle
+ // call Kodi's toggle function
+ if (!winSystem->GetFullscreenWillToggle())
+ {
+ // indicate that we are toggling
+ // flag will be reset in SetFullscreen once its
+ // called from Kodi's gui thread
+ winSystem->SetFullscreenWillToggle(true);
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN);
+ }
+ else
+ {
+ // in this case we are just called because
+ // of Kodi did a toggle - just reset the flag
+ // we don't need to do anything else
+ winSystem->SetFullscreenWillToggle(false);
+ }
+}
+
+- (NSApplicationPresentationOptions)window:(NSWindow*)window
+ willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions
+{
+ // customize our appearance when entering full screen:
+ // we don't want the dock to appear but we want the menubar to hide/show automatically
+ //
+ return (NSApplicationPresentationFullScreen | // support full screen for this window (required)
+ NSApplicationPresentationHideDock | // completely hide the dock
+ NSApplicationPresentationAutoHideMenuBar); // yes we want the menu bar to show/hide
+}
+
+- (void)windowDidMiniaturize:(NSNotification*)aNotification
+{
+ g_application.m_AppFocused = false;
+}
+
+- (void)windowDidDeminiaturize:(NSNotification*)aNotification
+{
+ g_application.m_AppFocused = true;
+}
+
+- (void)windowDidBecomeKey:(NSNotification*)aNotification
+{
+ g_application.m_AppFocused = true;
+
+ auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem());
+ if (winSystem)
+ {
+ winSystem->enableInputEvents();
+ }
+}
+
+- (void)windowDidResignKey:(NSNotification*)aNotification
+{
+ g_application.m_AppFocused = false;
+
+ auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem());
+ if (winSystem)
+ {
+ winSystem->disableInputEvents();
+ }
+}
+@end
diff --git a/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.h b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.h
new file mode 100644
index 0000000..c38e5e3
--- /dev/null
+++ b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.h
@@ -0,0 +1,35 @@
+/*
+ * 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
+
+#if defined(HAS_SDL)
+#include "windowing/osx/SDL/WinSystemOSXSDL.h"
+#else
+#include "windowing/osx/WinSystemOSX.h"
+#endif
+#include "rendering/gl/RenderSystemGL.h"
+
+class CWinSystemOSXGL : public CWinSystemOSX, public CRenderSystemGL
+{
+public:
+ CWinSystemOSXGL() = default;
+ ~CWinSystemOSXGL() override = default;
+
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystemBase via CWinSystemOSX
+ CRenderSystemBase *GetRenderSystem() override { return this; }
+ bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+
+protected:
+ void PresentRenderImpl(bool rendered) override;
+ void SetVSyncImpl(bool enable) override;
+};
diff --git a/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.mm b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.mm
new file mode 100644
index 0000000..30e44f2
--- /dev/null
+++ b/xbmc/windowing/osx/OpenGL/WinSystemOSXGL.mm
@@ -0,0 +1,79 @@
+/*
+ * 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 "WinSystemOSXGL.h"
+
+#include "guilib/Texture.h"
+#include "rendering/gl/RenderSystemGL.h"
+#include "windowing/WindowSystemFactory.h"
+
+#include <unistd.h>
+
+void CWinSystemOSXGL::Register()
+{
+ KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem);
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemOSXGL::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemOSXGL>();
+}
+
+void CWinSystemOSXGL::PresentRenderImpl(bool rendered)
+{
+ if (rendered)
+ FlushBuffer();
+
+ // FlushBuffer does not block if window is obscured
+ // in this case we need to throttle the render loop
+ if (IsObscured())
+ usleep(10000);
+
+ if (m_delayDispReset && m_dispResetTimer.IsTimePast())
+ {
+ m_delayDispReset = false;
+ AnnounceOnResetDevice();
+ }
+}
+
+void CWinSystemOSXGL::SetVSyncImpl(bool enable)
+{
+ EnableVSync(false);
+
+ if (enable)
+ {
+ EnableVSync(true);
+ }
+}
+
+bool CWinSystemOSXGL::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ CWinSystemOSX::ResizeWindow(newWidth, newHeight, newLeft, newTop);
+ CRenderSystemGL::ResetRenderSystem(newWidth, newHeight);
+
+ if (m_bVSync)
+ {
+ EnableVSync(m_bVSync);
+ }
+
+ return true;
+}
+
+bool CWinSystemOSXGL::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ CWinSystemOSX::SetFullScreen(fullScreen, res, blankOtherDisplays);
+ CRenderSystemGL::ResetRenderSystem(res.iWidth, res.iHeight);
+
+ if (m_bVSync)
+ {
+ EnableVSync(m_bVSync);
+ }
+
+ return true;
+}
+
diff --git a/xbmc/windowing/osx/SDL/CMakeLists.txt b/xbmc/windowing/osx/SDL/CMakeLists.txt
new file mode 100644
index 0000000..33e25d8
--- /dev/null
+++ b/xbmc/windowing/osx/SDL/CMakeLists.txt
@@ -0,0 +1,10 @@
+if(SDL_FOUND)
+ set(SOURCES WinEventsSDL.cpp
+ WinSystemOSXSDL.mm)
+ set(HEADERS WinEventsSDL.h
+ WinSystemOSXSDL.h)
+endif()
+
+if(SOURCES)
+ core_add_library(windowing_osx_SDL)
+endif()
diff --git a/xbmc/windowing/osx/SDL/WinEventsSDL.cpp b/xbmc/windowing/osx/SDL/WinEventsSDL.cpp
new file mode 100644
index 0000000..31b2506
--- /dev/null
+++ b/xbmc/windowing/osx/SDL/WinEventsSDL.cpp
@@ -0,0 +1,242 @@
+/*
+ * 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 "WinEventsSDL.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "application/Application.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/InputManager.h"
+#include "input/Key.h"
+#include "input/mouse/MouseStat.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/DisplaySettings.h"
+#include "windowing/WinSystem.h"
+
+#include "platform/darwin/osx/CocoaInterface.h"
+
+bool CWinEventsOSX::MessagePump()
+{
+ SDL_Event event;
+ bool ret = false;
+
+ while (SDL_PollEvent(&event))
+ {
+ switch(event.type)
+ {
+ case SDL_QUIT:
+ if (!g_application.m_bStop)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+ break;
+
+ case SDL_ACTIVEEVENT:
+ //If the window was inconified or restored
+ if( event.active.state & SDL_APPACTIVE )
+ {
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->SetRenderGUI(event.active.gain != 0);
+ CServiceBroker::GetWinSystem()->NotifyAppActiveChange(g_application.GetRenderGUI());
+ }
+ else if (event.active.state & SDL_APPINPUTFOCUS)
+ {
+ g_application.m_AppFocused = event.active.gain != 0;
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort && g_application.m_AppFocused)
+ appPort->SetRenderGUI(g_application.m_AppFocused);
+ CServiceBroker::GetWinSystem()->NotifyAppFocusChange(g_application.m_AppFocused);
+ }
+ break;
+
+ case SDL_KEYDOWN:
+ {
+ // process any platform specific shortcuts before handing off to XBMC
+ if (ProcessOSXShortcuts(event))
+ {
+ ret = true;
+ break;
+ }
+
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_KEYDOWN;
+ newEvent.key.keysym.scancode = event.key.keysym.scancode;
+ newEvent.key.keysym.sym = (XBMCKey) event.key.keysym.sym;
+ newEvent.key.keysym.unicode = event.key.keysym.unicode;
+
+ // Check if the Windows keys are down because SDL doesn't flag this.
+ uint16_t mod = event.key.keysym.mod;
+ uint8_t* keystate = SDL_GetKeyState(NULL);
+ if (keystate[SDLK_LSUPER] || keystate[SDLK_RSUPER])
+ mod |= XBMCKMOD_LSUPER;
+ newEvent.key.keysym.mod = (XBMCMod) mod;
+
+ // don't handle any more messages in the queue until we've handled keydown,
+ // if a keyup is in the queue it will reset the keypress before it is handled.
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+
+ case SDL_KEYUP:
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_KEYUP;
+ newEvent.key.keysym.scancode = event.key.keysym.scancode;
+ newEvent.key.keysym.sym = (XBMCKey) event.key.keysym.sym;
+ newEvent.key.keysym.mod =(XBMCMod) event.key.keysym.mod;
+ newEvent.key.keysym.unicode = event.key.keysym.unicode;
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+
+ case SDL_MOUSEBUTTONDOWN:
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEBUTTONDOWN;
+ newEvent.button.button = event.button.button;
+ newEvent.button.x = event.button.x;
+ newEvent.button.y = event.button.y;
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+
+ case SDL_MOUSEBUTTONUP:
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEBUTTONUP;
+ newEvent.button.button = event.button.button;
+ newEvent.button.x = event.button.x;
+ newEvent.button.y = event.button.y;
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+
+ case SDL_MOUSEMOTION:
+ {
+ if (0 == (SDL_GetAppState() & SDL_APPMOUSEFOCUS))
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ // See CApplication::ProcessSlow() for a description as to why we call Cocoa_HideMouse.
+ // this is here to restore the pointer when toggling back to window mode from fullscreen.
+ Cocoa_ShowMouse();
+ break;
+ }
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEMOTION;
+ newEvent.motion.x = event.motion.x;
+ newEvent.motion.y = event.motion.y;
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+ case SDL_VIDEORESIZE:
+ {
+ // Under newer osx versions sdl is so fucked up that it even fires resize events
+ // that exceed the screen size (maybe some HiDP incompatibility in old SDL?)
+ // ensure to ignore those events because it will mess with windowed size
+ if((event.resize.w > CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iWidth) ||
+ (event.resize.h > CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP).iHeight))
+ {
+ break;
+ }
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_VIDEORESIZE;
+ newEvent.resize.w = event.resize.w;
+ newEvent.resize.h = event.resize.h;
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ CServiceBroker::GetGUI()->GetWindowManager().MarkDirty();
+ break;
+ }
+ case SDL_USEREVENT:
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_USEREVENT;
+ newEvent.user.code = event.user.code;
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+ case SDL_VIDEOEXPOSE:
+ CServiceBroker::GetGUI()->GetWindowManager().MarkDirty();
+ break;
+ }
+ memset(&event, 0, sizeof(SDL_Event));
+ }
+
+ return ret;
+}
+
+bool CWinEventsOSX::ProcessOSXShortcuts(SDL_Event& event)
+{
+ static bool shift = false, cmd = false;
+
+ cmd = !!(SDL_GetModState() & (KMOD_LMETA | KMOD_RMETA ));
+ shift = !!(SDL_GetModState() & (KMOD_LSHIFT | KMOD_RSHIFT));
+
+ if (cmd && event.key.type == SDL_KEYDOWN)
+ {
+ char keysymbol = event.key.keysym.sym;
+
+ // if the unicode is in the ascii range
+ // use this instead for getting the real
+ // character based on the used keyboard layout
+ // see http://lists.libsdl.org/pipermail/sdl-libsdl.org/2004-May/043716.html
+ bool isControl = (event.key.keysym.mod & KMOD_CTRL) != 0;
+ if (!isControl && !(event.key.keysym.unicode & 0xff80))
+ keysymbol = event.key.keysym.unicode;
+
+ switch(keysymbol)
+ {
+ case SDLK_q: // CMD-q to quit
+ if (!g_application.m_bStop)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+ return true;
+
+ case SDLK_f: // CMD-Ctrl-f to toggle fullscreen
+ if (!isControl)
+ return false;
+ g_application.OnAction(CAction(ACTION_TOGGLE_FULLSCREEN));
+ return true;
+
+ case SDLK_s: // CMD-3 to take a screenshot
+ g_application.OnAction(CAction(ACTION_TAKE_SCREENSHOT));
+ return true;
+
+ case SDLK_h: // CMD-h to hide
+ CServiceBroker::GetWinSystem()->Hide();
+ return true;
+
+ case SDLK_m: // CMD-m to minimize
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MINIMIZE);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ return false;
+}
diff --git a/xbmc/windowing/osx/SDL/WinEventsSDL.h b/xbmc/windowing/osx/SDL/WinEventsSDL.h
new file mode 100644
index 0000000..06e6325
--- /dev/null
+++ b/xbmc/windowing/osx/SDL/WinEventsSDL.h
@@ -0,0 +1,24 @@
+/*
+ * 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 "windowing/WinEvents.h"
+
+#include <SDL/SDL_events.h>
+
+class CWinEventsOSX : public IWinEvents
+{
+public:
+ CWinEventsOSX() = default;
+ ~CWinEventsOSX() override = default;
+ bool MessagePump() override;
+
+private:
+ static bool ProcessOSXShortcuts(SDL_Event& event);
+};
diff --git a/xbmc/windowing/osx/SDL/WinSystemOSXSDL.h b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.h
new file mode 100644
index 0000000..ab19301
--- /dev/null
+++ b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.h
@@ -0,0 +1,111 @@
+/*
+ * 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 "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "threads/Timer.h"
+#include "windowing/WinSystem.h"
+
+#include <chrono>
+#include <string>
+#include <vector>
+
+typedef struct SDL_Surface SDL_Surface;
+
+class IDispResource;
+class CWinEventsOSX;
+class CWinSystemOSXImpl;
+#ifdef __OBJC__
+@class NSOpenGLContext;
+#endif
+
+class CWinSystemOSX : public CWinSystemBase, public ITimerCallback
+{
+public:
+
+ CWinSystemOSX();
+ ~CWinSystemOSX() override;
+
+ // ITimerCallback interface
+ void OnTimeout() override;
+
+ // CWinSystemBase
+ 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;
+ void NotifyAppFocusChange(bool bGaining) override;
+ void ShowOSMouse(bool show) override;
+ bool Minimize() override;
+ bool Restore() override;
+ bool Hide() override;
+ bool Show(bool raise = true) override;
+ void OnMove(int x, int y) override;
+
+ std::string GetClipboardText() override;
+
+ void Register(IDispResource *resource) override;
+ void Unregister(IDispResource *resource) override;
+
+ std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override;
+
+ std::vector<std::string> GetConnectedOutputs() override;
+
+ void WindowChangedScreen();
+
+ void AnnounceOnLostDevice();
+ void AnnounceOnResetDevice();
+ void HandleOnResetDevice();
+ void StartLostDeviceTimer();
+ void StopLostDeviceTimer();
+
+ void* GetCGLContextObj();
+#ifdef __OBJC__
+ NSOpenGLContext* GetNSOpenGLContext();
+#else
+ void* GetNSOpenGLContext();
+#endif
+
+ // winevents override
+ bool MessagePump() override;
+
+protected:
+ std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override;
+
+ void HandlePossibleRefreshrateChange();
+ void GetScreenResolution(int* w, int* h, double* fps, int screenIdx);
+ void EnableVSync(bool enable);
+ bool SwitchToVideoMode(int width, int height, double refreshrate);
+ void FillInVideoModes();
+ bool FlushBuffer(void);
+ bool IsObscured(void);
+ void StartTextInput();
+ void StopTextInput();
+
+ std::unique_ptr<CWinSystemOSXImpl> m_impl;
+ SDL_Surface* m_SDLSurface;
+ CWinEventsOSX *m_osx_events;
+ bool m_obscured;
+ std::chrono::time_point<std::chrono::steady_clock> m_obscured_timecheck;
+
+ bool m_movedToOtherScreen;
+ int m_lastDisplayNr;
+ double m_refreshRate;
+
+ CCriticalSection m_resourceSection;
+ std::vector<IDispResource*> m_resources;
+ CTimer m_lostDeviceTimer;
+ bool m_delayDispReset;
+ XbmcThreads::EndTime<> m_dispResetTimer;
+ int m_updateGLContext = 0;
+};
diff --git a/xbmc/windowing/osx/SDL/WinSystemOSXSDL.mm b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.mm
new file mode 100644
index 0000000..7a1b46b
--- /dev/null
+++ b/xbmc/windowing/osx/SDL/WinSystemOSXSDL.mm
@@ -0,0 +1,1852 @@
+/*
+ * 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 "WinSystemOSXSDL.h"
+
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Sinks/AESinkDARWINOSX.h"
+#include "cores/RetroPlayer/process/osx/RPProcessInfoOSX.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/VTB.h"
+#include "cores/VideoPlayer/Process/osx/ProcessInfoOSX.h"
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "guilib/DispResource.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/KeyboardStat.h"
+#include "messaging/ApplicationMessenger.h"
+#include "rendering/gl/ScreenshotSurfaceGL.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/log.h"
+#include "windowing/osx/CocoaDPMSSupport.h"
+#include "windowing/osx/OSScreenSaverOSX.h"
+#include "windowing/osx/SDL/WinEventsSDL.h"
+#include "windowing/osx/VideoSyncOsx.h"
+
+#include "platform/darwin/DarwinUtils.h"
+#include "platform/darwin/DictionaryUtils.h"
+#include "platform/darwin/osx/CocoaInterface.h"
+#import "platform/darwin/osx/SDL/OSXTextInputResponder.h"
+#include "platform/darwin/osx/XBMCHelper.h"
+
+#include <cstdlib>
+#include <mutex>
+#include <signal.h>
+
+#import <Cocoa/Cocoa.h>
+#import <IOKit/graphics/IOGraphicsLib.h>
+#import <IOKit/pwr_mgt/IOPMLib.h>
+#import <QuartzCore/QuartzCore.h>
+#import <SDL/SDL.h>
+
+// turn off deprecated warning spew.
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+
+using namespace KODI;
+using namespace WINDOWING;
+using namespace std::chrono_literals;
+
+//------------------------------------------------------------------------------------------
+// special object-c class for handling the NSWindowDidMoveNotification callback.
+@interface windowDidMoveNoteClass : NSObject
+{
+ void *m_userdata;
+}
++ (windowDidMoveNoteClass*) initWith: (void*) userdata;
+- (void) windowDidMoveNotification:(NSNotification*) note;
+@end
+
+@implementation windowDidMoveNoteClass
++ (windowDidMoveNoteClass*) initWith: (void*) userdata
+{
+ windowDidMoveNoteClass *windowDidMove = [windowDidMoveNoteClass new];
+ windowDidMove->m_userdata = userdata;
+ return windowDidMove;
+}
+- (void) windowDidMoveNotification:(NSNotification*) note
+{
+ CWinSystemOSX *winsys = (CWinSystemOSX*)m_userdata;
+ if (!winsys)
+ return;
+
+ NSOpenGLContext* context = [NSOpenGLContext currentContext];
+ if (context)
+ {
+ if ([context view])
+ {
+ NSPoint window_origin = [[[context view] window] frame].origin;
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_VIDEOMOVE;
+ newEvent.move.x = window_origin.x;
+ newEvent.move.y = window_origin.y;
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ }
+ }
+}
+@end
+//------------------------------------------------------------------------------------------
+// special object-c class for handling the NSWindowDidReSizeNotification callback.
+@interface windowDidReSizeNoteClass : NSObject
+{
+ void *m_userdata;
+}
++ (windowDidReSizeNoteClass*) initWith: (void*) userdata;
+- (void) windowDidReSizeNotification:(NSNotification*) note;
+@end
+@implementation windowDidReSizeNoteClass
++ (windowDidReSizeNoteClass*) initWith: (void*) userdata
+{
+ windowDidReSizeNoteClass *windowDidReSize = [windowDidReSizeNoteClass new];
+ windowDidReSize->m_userdata = userdata;
+ return windowDidReSize;
+}
+- (void) windowDidReSizeNotification:(NSNotification*) note
+{
+ CWinSystemOSX *winsys = (CWinSystemOSX*)m_userdata;
+ if (!winsys)
+ return;
+
+}
+@end
+
+//------------------------------------------------------------------------------------------
+// special object-c class for handling the NSWindowDidChangeScreenNotification callback.
+@interface windowDidChangeScreenNoteClass : NSObject
+{
+ void *m_userdata;
+}
++ (windowDidChangeScreenNoteClass*) initWith: (void*) userdata;
+- (void) windowDidChangeScreenNotification:(NSNotification*) note;
+@end
+@implementation windowDidChangeScreenNoteClass
++ (windowDidChangeScreenNoteClass*) initWith: (void*) userdata
+{
+ windowDidChangeScreenNoteClass *windowDidChangeScreen = [windowDidChangeScreenNoteClass new];
+ windowDidChangeScreen->m_userdata = userdata;
+ return windowDidChangeScreen;
+}
+- (void) windowDidChangeScreenNotification:(NSNotification*) note
+{
+ CWinSystemOSX *winsys = (CWinSystemOSX*)m_userdata;
+ if (!winsys)
+ return;
+ winsys->WindowChangedScreen();
+}
+@end
+//------------------------------------------------------------------------------------------
+
+class CWinSystemOSXImpl
+{
+public:
+ NSOpenGLContext* m_glContext;
+ static NSOpenGLContext* m_lastOwnedContext;
+
+ windowDidMoveNoteClass* m_windowDidMove;
+ windowDidReSizeNoteClass* m_windowDidReSize;
+ windowDidChangeScreenNoteClass* m_windowChangedScreen;
+};
+
+NSOpenGLContext* CWinSystemOSXImpl::m_lastOwnedContext = nil;
+
+
+#define MAX_DISPLAYS 32
+// 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 auto LOST_DEVICE_TIMEOUT_MS = 3000ms;
+static NSWindow* blankingWindows[MAX_DISPLAYS];
+
+//------------------------------------------------------------------------------------------
+CRect CGRectToCRect(CGRect cgrect)
+{
+ CRect crect = CRect(
+ cgrect.origin.x,
+ cgrect.origin.y,
+ cgrect.origin.x + cgrect.size.width,
+ cgrect.origin.y + cgrect.size.height);
+ return crect;
+}
+//---------------------------------------------------------------------------------
+void SetMenuBarVisible(bool visible)
+{
+ if(visible)
+ {
+ [[NSApplication sharedApplication]
+ setPresentationOptions: NSApplicationPresentationDefault];
+ }
+ else
+ {
+ [[NSApplication sharedApplication]
+ setPresentationOptions: NSApplicationPresentationHideMenuBar |
+ NSApplicationPresentationHideDock];
+ }
+}
+//---------------------------------------------------------------------------------
+CGDirectDisplayID GetDisplayID(int screen_index)
+{
+ CGDirectDisplayID displayArray[MAX_DISPLAYS];
+ CGDisplayCount numDisplays;
+
+ // Get the list of displays.
+ CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays);
+ if (screen_index >= 0 && screen_index < static_cast<int>(numDisplays))
+ return(displayArray[screen_index]);
+ else
+ return(displayArray[0]);
+}
+
+size_t DisplayBitsPerPixelForMode(CGDisplayModeRef mode)
+{
+ size_t bitsPerPixel = 0;
+
+ CFStringRef pixEnc = CGDisplayModeCopyPixelEncoding(mode);
+ if(CFStringCompare(pixEnc, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+ {
+ bitsPerPixel = 32;
+ }
+ else if(CFStringCompare(pixEnc, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+ {
+ bitsPerPixel = 16;
+ }
+ else if(CFStringCompare(pixEnc, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+ {
+ bitsPerPixel = 8;
+ }
+
+ CFRelease(pixEnc);
+
+ return bitsPerPixel;
+}
+
+CFArrayRef GetAllDisplayModes(CGDirectDisplayID display)
+{
+ int value = 1;
+
+ CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value);
+ if (!number)
+ {
+ CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Number!");
+ return NULL;
+ }
+
+ CFStringRef key = kCGDisplayShowDuplicateLowResolutionModes;
+ CFDictionaryRef options = CFDictionaryCreate(kCFAllocatorDefault, (const void **)&key, (const void **)&number, 1, NULL, NULL);
+ CFRelease(number);
+
+ if (!options)
+ {
+ CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Dictionary!");
+ return NULL;
+ }
+
+ CFArrayRef displayModes = CGDisplayCopyAllDisplayModes(display, options);
+ CFRelease(options);
+
+ if (!displayModes)
+ {
+ CLog::Log(LOGERROR, "GetAllDisplayModes - no displaymodes found!");
+ return NULL;
+ }
+
+ return displayModes;
+}
+
+// mimic former behavior of deprecated CGDisplayBestModeForParameters
+CGDisplayModeRef BestMatchForMode(CGDirectDisplayID display, size_t bitsPerPixel, size_t width, size_t height, boolean_t &match)
+{
+
+ // Get a copy of the current display mode
+ CGDisplayModeRef displayMode = CGDisplayCopyDisplayMode(display);
+
+ // Loop through all display modes to determine the closest match.
+ // CGDisplayBestModeForParameters is deprecated on 10.6 so we will emulate it's behavior
+ // Try to find a mode with the requested depth and equal or greater dimensions first.
+ // If no match is found, try to find a mode with greater depth and same or greater dimensions.
+ // If still no match is found, just use the current mode.
+ CFArrayRef allModes = GetAllDisplayModes(display);
+
+ for(int i = 0; i < CFArrayGetCount(allModes); i++) {
+ CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i);
+
+ if(DisplayBitsPerPixelForMode(mode) != bitsPerPixel)
+ continue;
+
+ if((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height))
+ {
+ CGDisplayModeRelease(displayMode); // release the copy we got before ...
+ displayMode = mode;
+ match = true;
+ break;
+ }
+ }
+
+ // No depth match was found
+ if(!match)
+ {
+ for(int i = 0; i < CFArrayGetCount(allModes); i++)
+ {
+ CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i);
+ if(DisplayBitsPerPixelForMode(mode) >= bitsPerPixel)
+ continue;
+
+ if((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height))
+ {
+ displayMode = mode;
+ match = true;
+ break;
+ }
+ }
+ }
+
+ CFRelease(allModes);
+
+ return displayMode;
+}
+
+CGDirectDisplayID GetDisplayIDFromScreen(NSScreen *screen)
+{
+ NSDictionary* screenInfo = [screen deviceDescription];
+ NSNumber* screenID = [screenInfo objectForKey:@"NSScreenNumber"];
+
+ return (CGDirectDisplayID)[screenID longValue];
+}
+
+int GetDisplayIndex(CGDirectDisplayID display)
+{
+ CGDirectDisplayID displayArray[MAX_DISPLAYS];
+ CGDisplayCount numDisplays;
+
+ // Get the list of displays.
+ CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays);
+ while (numDisplays > 0)
+ {
+ if (display == displayArray[--numDisplays])
+ return numDisplays;
+ }
+ return -1;
+}
+
+void BlankOtherDisplays(int screen_index)
+{
+ int i;
+ int numDisplays = [[NSScreen screens] count];
+
+ // zero out blankingWindows for debugging
+ for (i=0; i<MAX_DISPLAYS; i++)
+ {
+ blankingWindows[i] = 0;
+ }
+
+ // Blank.
+ for (i=0; i<numDisplays; i++)
+ {
+ if (i != screen_index)
+ {
+ // Get the size.
+ NSScreen* pScreen = [[NSScreen screens] objectAtIndex:i];
+ NSRect screenRect = [pScreen frame];
+
+ // Build a blanking window.
+ screenRect.origin = NSZeroPoint;
+ blankingWindows[i] = [[NSWindow alloc] initWithContentRect:screenRect
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO
+ screen:pScreen];
+
+ [blankingWindows[i] setBackgroundColor:[NSColor blackColor]];
+ [blankingWindows[i] setLevel:CGShieldingWindowLevel()];
+ [blankingWindows[i] makeKeyAndOrderFront:nil];
+ }
+ }
+}
+
+void UnblankDisplays(void)
+{
+ int numDisplays = [[NSScreen screens] count];
+ int i = 0;
+
+ for (i=0; i<numDisplays; i++)
+ {
+ if (blankingWindows[i] != 0)
+ {
+ // Get rid of the blanking windows we created.
+ [blankingWindows[i] close];
+ blankingWindows[i] = 0;
+ }
+ }
+}
+
+CGDisplayFadeReservationToken DisplayFadeToBlack(bool fade)
+{
+ // Fade to black to hide resolution-switching flicker and garbage.
+ CGDisplayFadeReservationToken fade_token = kCGDisplayFadeReservationInvalidToken;
+ if (CGAcquireDisplayFadeReservation (5, &fade_token) == kCGErrorSuccess && fade)
+ CGDisplayFade(fade_token, 0.3, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, TRUE);
+
+ return(fade_token);
+}
+
+void DisplayFadeFromBlack(CGDisplayFadeReservationToken fade_token, bool fade)
+{
+ if (fade_token != kCGDisplayFadeReservationInvalidToken)
+ {
+ if (fade)
+ CGDisplayFade(fade_token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0.0, 0.0, 0.0, FALSE);
+ CGReleaseDisplayFadeReservation(fade_token);
+ }
+}
+
+NSString* screenNameForDisplay(CGDirectDisplayID displayID)
+{
+ NSString* screenName;
+ @autoreleasepool
+ {
+ NSDictionary* deviceInfo = (__bridge_transfer NSDictionary*)IODisplayCreateInfoDictionary(
+ CGDisplayIOServicePort(displayID), kIODisplayOnlyPreferredName);
+ NSDictionary* localizedNames =
+ [deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]];
+
+ if ([localizedNames count] > 0)
+ {
+ screenName = [localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]];
+ }
+ }
+
+ if (screenName == nil)
+ {
+ screenName = [[NSString alloc] initWithFormat:@"%i", displayID];
+ }
+ else
+ {
+ // ensure screen name is unique by appending displayid
+ screenName = [screenName stringByAppendingFormat:@" (%@)", [@(displayID) stringValue]];
+ }
+
+ return screenName;
+}
+
+int GetDisplayIndex(const std::string& dispName)
+{
+ int ret = 0;
+
+ // Add full screen settings for additional monitors
+ int numDisplays = [[NSScreen screens] count];
+
+ for (int disp = 0; disp < numDisplays; disp++)
+ {
+ NSString *name = screenNameForDisplay(GetDisplayID(disp));
+ if ([name UTF8String] == dispName)
+ {
+ ret = disp;
+ break;
+ }
+ }
+
+ return ret;
+}
+
+void ShowHideNSWindow(NSWindow *wind, bool show)
+{
+ if (show)
+ [wind orderFront:nil];
+ else
+ [wind orderOut:nil];
+}
+
+static NSWindow *curtainWindow;
+void fadeInDisplay(NSScreen *theScreen, double fadeTime)
+{
+ int fadeSteps = 100;
+ double fadeInterval = (fadeTime / (double) fadeSteps);
+
+ if (curtainWindow != nil)
+ {
+ for (int step = 0; step < fadeSteps; step++)
+ {
+ double fade = 1.0 - (step * fadeInterval);
+ [curtainWindow setAlphaValue:fade];
+
+ NSDate *nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval];
+ [[NSRunLoop currentRunLoop] runUntilDate:nextDate];
+ }
+ }
+ [curtainWindow close];
+ curtainWindow = nil;
+
+ [NSCursor unhide];
+}
+
+void fadeOutDisplay(NSScreen *theScreen, double fadeTime)
+{
+ int fadeSteps = 100;
+ double fadeInterval = (fadeTime / (double) fadeSteps);
+
+ [NSCursor hide];
+
+ curtainWindow = [[NSWindow alloc]
+ initWithContentRect:[theScreen frame]
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:YES
+ screen:theScreen];
+
+ [curtainWindow setAlphaValue:0.0];
+ [curtainWindow setBackgroundColor:[NSColor blackColor]];
+ [curtainWindow setLevel:NSScreenSaverWindowLevel];
+
+ [curtainWindow makeKeyAndOrderFront:nil];
+ [curtainWindow setFrame:[curtainWindow
+ frameRectForContentRect:[theScreen frame]]
+ display:YES
+ animate:NO];
+
+ for (int step = 0; step < fadeSteps; step++)
+ {
+ double fade = step * fadeInterval;
+ [curtainWindow setAlphaValue:fade];
+
+ NSDate *nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval];
+ [[NSRunLoop currentRunLoop] runUntilDate:nextDate];
+ }
+}
+
+// try to find mode that matches the desired size, refreshrate
+// non interlaced, nonstretched, safe for hardware
+CGDisplayModeRef GetMode(int width, int height, double refreshrate, int screenIdx)
+{
+ if ( screenIdx >= (signed)[[NSScreen screens] count])
+ return NULL;
+
+ Boolean stretched;
+ Boolean interlaced;
+ Boolean safeForHardware;
+ Boolean televisionoutput;
+ int w, h, bitsperpixel;
+ double rate;
+ RESOLUTION_INFO res;
+
+ CLog::Log(LOGDEBUG, "GetMode looking for suitable mode with {} x {} @ {:f} Hz on display {}",
+ width, height, refreshrate, screenIdx);
+
+ CFArrayRef displayModes = GetAllDisplayModes(GetDisplayID(screenIdx));
+
+ if (!displayModes)
+ return NULL;
+
+ for (int i=0; i < CFArrayGetCount(displayModes); ++i)
+ {
+ CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i);
+ uint32_t flags = CGDisplayModeGetIOFlags(displayMode);
+ stretched = flags & kDisplayModeStretchedFlag ? true : false;
+ interlaced = flags & kDisplayModeInterlacedFlag ? true : false;
+ bitsperpixel = DisplayBitsPerPixelForMode(displayMode);
+ safeForHardware = flags & kDisplayModeSafetyFlags ? true : false;
+ televisionoutput = flags & kDisplayModeTelevisionFlag ? true : false;
+ w = CGDisplayModeGetWidth(displayMode);
+ h = CGDisplayModeGetHeight(displayMode);
+ rate = CGDisplayModeGetRefreshRate(displayMode);
+
+
+ if ((bitsperpixel == 32) &&
+ (safeForHardware == YES) &&
+ (stretched == NO) &&
+ (interlaced == NO) &&
+ (w == width) &&
+ (h == height) &&
+ (rate == refreshrate || rate == 0))
+ {
+ CLog::Log(LOGDEBUG, "GetMode found a match!");
+ return displayMode;
+ }
+ }
+
+ CFRelease(displayModes);
+ CLog::Log(LOGERROR, "GetMode - no match found!");
+ return NULL;
+}
+
+//---------------------------------------------------------------------------------
+static void DisplayReconfigured(CGDirectDisplayID display,
+ CGDisplayChangeSummaryFlags flags, void* userData)
+{
+ CWinSystemOSX *winsys = (CWinSystemOSX*)userData;
+ if (!winsys)
+ return;
+
+ CLog::Log(LOGDEBUG, "CWinSystemOSX::DisplayReconfigured with flags {}", flags);
+
+ // we fire the callbacks on start of configuration
+ // or when the mode set was finished
+ // or when we are called with flags == 0 (which is undocumented but seems to happen
+ // on some macs - we treat it as device reset)
+
+ // first check if we need to call OnLostDevice
+ if (flags & kCGDisplayBeginConfigurationFlag)
+ {
+ // pre/post-reconfiguration changes
+ RESOLUTION res = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution();
+ if (res == RES_INVALID)
+ return;
+
+ NSScreen* pScreen = nil;
+ unsigned int screenIdx = 0;
+
+ if ( screenIdx < [[NSScreen screens] count] )
+ {
+ pScreen = [[NSScreen screens] objectAtIndex:screenIdx];
+ }
+
+ // kCGDisplayBeginConfigurationFlag is only fired while the screen is still
+ // valid
+ if (pScreen)
+ {
+ CGDirectDisplayID xbmc_display = GetDisplayIDFromScreen(pScreen);
+ if (xbmc_display == display)
+ {
+ // we only respond to changes on the display we are running on.
+ winsys->AnnounceOnLostDevice();
+ winsys->StartLostDeviceTimer();
+ }
+ }
+ }
+ else // the else case checks if we need to call OnResetDevice
+ {
+ // we fire if kCGDisplaySetModeFlag is set or if flags == 0
+ // (which is undocumented but seems to happen
+ // on some macs - we treat it as device reset)
+ // we also don't check the screen here as we might not even have
+ // one anymore (e.x. when tv is turned off)
+ if (flags & kCGDisplaySetModeFlag || flags == 0)
+ {
+ winsys->StopLostDeviceTimer(); // no need to timeout - we've got the callback
+ winsys->HandleOnResetDevice();
+ }
+ }
+
+ if ((flags & kCGDisplayAddFlag) || (flags & kCGDisplayRemoveFlag))
+ winsys->UpdateResolutions();
+}
+
+//------------------------------------------------------------------------------
+NSOpenGLContext* CreateWindowedContext(NSOpenGLContext* shareCtx);
+void ResizeWindowInternal(int newWidth, int newHeight, int newLeft, int newTop, NSView* last_view);
+
+//------------------------------------------------------------------------------
+CWinSystemOSX::CWinSystemOSX()
+ : CWinSystemBase()
+ , m_impl{new CWinSystemOSXImpl}
+ , m_lostDeviceTimer(this)
+{
+ m_SDLSurface = NULL;
+ m_osx_events = NULL;
+ m_obscured = false;
+ m_obscured_timecheck = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000);
+ m_lastDisplayNr = -1;
+ m_movedToOtherScreen = false;
+ m_refreshRate = 0.0;
+ m_delayDispReset = false;
+
+ m_winEvents.reset(new CWinEventsOSX());
+
+ AE::CAESinkFactory::ClearSinks();
+ CAESinkDARWINOSX::Register();
+ m_dpms = std::make_shared<CCocoaDPMSSupport>();
+}
+
+CWinSystemOSX::~CWinSystemOSX() = default;
+
+void CWinSystemOSX::StartLostDeviceTimer()
+{
+ if (m_lostDeviceTimer.IsRunning())
+ m_lostDeviceTimer.Restart();
+ else
+ m_lostDeviceTimer.Start(LOST_DEVICE_TIMEOUT_MS, false);
+}
+
+void CWinSystemOSX::StopLostDeviceTimer()
+{
+ m_lostDeviceTimer.Stop();
+}
+
+void CWinSystemOSX::OnTimeout()
+{
+ HandleOnResetDevice();
+}
+
+bool CWinSystemOSX::InitWindowSystem()
+{
+ CLog::LogF(LOGINFO, "Setup SDL");
+
+ /* Clean up on exit, exit on window close and interrupt */
+ std::atexit(SDL_Quit);
+
+ if (SDL_Init(SDL_INIT_VIDEO) != 0)
+ {
+ CLog::LogF(LOGFATAL, "Unable to initialize SDL: {}", SDL_GetError());
+ return false;
+ }
+ // SDL_Init will install a handler for segfaults, restore the default handler.
+ signal(SIGSEGV, SIG_DFL);
+
+ SDL_EnableUNICODE(1);
+
+ // set repeat to 10ms to ensure repeat time < frame time
+ // so that hold times can be reliably detected
+ SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, 10);
+
+ if (!CWinSystemBase::InitWindowSystem())
+ return false;
+
+ m_osx_events = new CWinEventsOSX();
+
+ CGDisplayRegisterReconfigurationCallback(DisplayReconfigured, (void*)this);
+
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ auto windowDidMove = [windowDidMoveNoteClass initWith:this];
+ [center addObserver:windowDidMove
+ selector:@selector(windowDidMoveNotification:)
+ name:NSWindowDidMoveNotification object:nil];
+ m_impl->m_windowDidMove = windowDidMove;
+
+ auto windowDidReSize = [windowDidReSizeNoteClass initWith:this];
+ [center addObserver:windowDidReSize
+ selector:@selector(windowDidReSizeNotification:)
+ name:NSWindowDidResizeNotification object:nil];
+ m_impl->m_windowDidReSize = windowDidReSize;
+
+ auto windowDidChangeScreen = [windowDidChangeScreenNoteClass initWith:this];
+ [center addObserver:windowDidChangeScreen
+ selector:@selector(windowDidChangeScreenNotification:)
+ name:NSWindowDidChangeScreenNotification object:nil];
+ m_impl->m_windowChangedScreen = windowDidChangeScreen;
+
+ return true;
+}
+
+bool CWinSystemOSX::DestroyWindowSystem()
+{
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center removeObserver:m_impl->m_windowDidMove name:NSWindowDidMoveNotification object:nil];
+ [center removeObserver:m_impl->m_windowDidReSize name:NSWindowDidResizeNotification object:nil];
+ [center removeObserver:m_impl->m_windowChangedScreen
+ name:NSWindowDidChangeScreenNotification
+ object:nil];
+
+ CGDisplayRemoveReconfigurationCallback(DisplayReconfigured, (void*)this);
+
+ delete m_osx_events;
+ m_osx_events = NULL;
+
+ UnblankDisplays();
+ m_impl->m_glContext = nil;
+ return true;
+}
+
+bool CWinSystemOSX::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res)
+{
+ // force initial window creation to be windowed, if fullscreen, it will switch to it below
+ // fixes the white screen of death if starting fullscreen and switching to windowed.
+ RESOLUTION_INFO resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW);
+ m_nWidth = resInfo.iWidth;
+ m_nHeight = resInfo.iHeight;
+ m_bFullScreen = false;
+
+ SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
+ SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
+ SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
+ SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
+ SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
+
+ // Enable vertical sync to avoid any tearing.
+ SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, 1);
+
+ m_SDLSurface = SDL_SetVideoMode(m_nWidth, m_nHeight, 0, SDL_OPENGL | SDL_RESIZABLE);
+ if (!m_SDLSurface)
+ return false;
+
+ // the context SDL creates isn't full screen compatible, so we create new one
+ // first, find the current contect and make sure a view is attached
+ NSOpenGLContext* cur_context = [NSOpenGLContext currentContext];
+ NSView* view = [cur_context view];
+ if (!view)
+ return false;
+
+ if (CDisplaySettings::GetInstance().GetCurrentResolution() != RES_WINDOW)
+ {
+ // If we are not starting up windowed, then hide the initial SDL window
+ // so we do not see it flash before the fade-out and switch to fullscreen.
+ ShowHideNSWindow([view window], false);
+ }
+
+ // disassociate view from context
+ [cur_context clearDrawable];
+
+ // release the context
+ if (CWinSystemOSXImpl::m_lastOwnedContext == cur_context)
+ {
+ [ NSOpenGLContext clearCurrentContext ];
+ [ cur_context clearDrawable ];
+ cur_context = nil;
+ }
+
+ // create a new context
+ auto new_context = CreateWindowedContext(nil);
+ if (!new_context)
+ return false;
+
+ // associate with current view
+ [new_context setView:view];
+ [new_context makeCurrentContext];
+
+ // set the window title
+ [[[new_context view] window]
+ setTitle:[NSString stringWithFormat:@"%s Media Center", CCompileInfo::GetAppName()]];
+
+ m_impl->m_glContext = new_context;
+ CWinSystemOSXImpl::m_lastOwnedContext = new_context;
+ m_bWindowCreated = true;
+
+ // get screen refreshrate - this is needed
+ // when we startup in windowed mode and don't run through SetFullScreen
+ int dummy;
+ GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr);
+
+ // register platform dependent objects
+ CDVDFactoryCodec::ClearHWAccels();
+ VTB::CDecoder::Register();
+ VIDEOPLAYER::CRendererFactory::ClearRenderer();
+ CLinuxRendererGL::Register();
+ CRendererVTB::Register();
+ VIDEOPLAYER::CProcessInfoOSX::Register();
+ RETRO::CRPProcessInfoOSX::Register();
+ RETRO::CRPProcessInfoOSX::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL);
+ CScreenshotSurfaceGL::Register();
+
+ return true;
+}
+
+bool CWinSystemOSX::DestroyWindow()
+{
+ return true;
+}
+
+void ResizeWindowInternal(int newWidth, int newHeight, int newLeft, int newTop, NSView* last_view)
+{
+ if (last_view && [last_view window])
+ {
+ auto size = NSMakeSize(newWidth, newHeight);
+ NSWindow* lastWindow = [last_view window];
+ [lastWindow setContentSize:size];
+ [lastWindow update];
+ [last_view setFrameSize:size];
+ }
+}
+bool CWinSystemOSX::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ if (!m_impl->m_glContext)
+ return false;
+
+ NSOpenGLContext* context = [NSOpenGLContext currentContext];
+ NSView* view;
+ NSWindow* window;
+
+ view = [context view];
+
+ if (view)
+ {
+ // It seems, that in macOS 10.15 this defaults to YES, but we currently do not support
+ // Retina resolutions properly. Ensure that the view uses a 1 pixel per point framebuffer.
+ view.wantsBestResolutionOpenGLSurface = NO;
+ }
+
+ if (view && (newWidth > 0) && (newHeight > 0))
+ {
+ window = [view window];
+ if (window)
+ {
+ [window setContentSize:NSMakeSize(newWidth, newHeight)];
+ [window update];
+ [view setFrameSize:NSMakeSize(newWidth, newHeight)];
+ [context update];
+ // this is needed in case we traverse from fullscreen screen 2
+ // to windowed on screen 1 directly where in ScreenChangedNotification
+ // we don't have a window to get the current screen on
+ // in that case ResizeWindow is called at a later stage from SetFullScreen(false)
+ // and we can grab the correct display number here then
+ m_lastDisplayNr = GetDisplayIndex(GetDisplayIDFromScreen( [window screen] ));
+ }
+ }
+
+ // HACK: resize SDL's view manually so that mouse bounds are correctly updated.
+ // there are two parts to this, the internal SDL (current_video->screen) and
+ // the cocoa view ( handled in SetFullScreen).
+ SDL_SetWidthHeight(newWidth, newHeight);
+
+ [context makeCurrentContext];
+
+ m_nWidth = newWidth;
+ m_nHeight = newHeight;
+ m_impl->m_glContext = context;
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetFPS(m_refreshRate);
+
+ return true;
+}
+
+static bool needtoshowme = true;
+
+bool CWinSystemOSX::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ static NSWindow* windowedFullScreenwindow = nil;
+ static NSPoint last_window_origin;
+ static NSView* last_view = nil;
+ static NSSize last_view_size;
+ static NSPoint last_view_origin;
+ static NSInteger last_window_level = NSNormalWindowLevel;
+ bool was_fullscreen = m_bFullScreen;
+ NSOpenGLContext* cur_context;
+
+ // Fade to black to hide resolution-switching flicker and garbage.
+ CGDisplayFadeReservationToken fade_token = DisplayFadeToBlack(needtoshowme);
+
+ // If we're already fullscreen then we must be moving to a different display.
+ // or if we are still on the same display - it might be only a refreshrate/resolution
+ // change request.
+ // Recurse to reset fullscreen mode and then continue.
+ if (was_fullscreen && fullScreen)
+ {
+ needtoshowme = false;
+ ShowHideNSWindow([last_view window], needtoshowme);
+ RESOLUTION_INFO& window = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW);
+ CWinSystemOSX::SetFullScreen(false, window, blankOtherDisplays);
+ needtoshowme = true;
+ }
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ m_lastDisplayNr = GetDisplayIndex(settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR));
+ m_nWidth = res.iWidth;
+ m_nHeight = res.iHeight;
+ m_bFullScreen = fullScreen;
+
+ cur_context = [NSOpenGLContext currentContext];
+
+ //handle resolution/refreshrate switching early here
+ if (m_bFullScreen)
+ {
+ // switch videomode
+ SwitchToVideoMode(res.iWidth, res.iHeight, static_cast<double>(res.fRefreshRate));
+ }
+
+ //no context? done.
+ if (!cur_context)
+ {
+ DisplayFadeFromBlack(fade_token, needtoshowme);
+ return false;
+ }
+
+ if (windowedFullScreenwindow != nil)
+ {
+ [windowedFullScreenwindow close];
+ windowedFullScreenwindow = nil;
+ }
+
+ if (m_bFullScreen)
+ {
+ // FullScreen Mode
+ NSOpenGLContext* newContext = nil;
+
+ // Save info about the windowed context so we can restore it when returning to windowed.
+ last_view = [cur_context view];
+ last_view_size = [last_view frame].size;
+ last_view_origin = [last_view frame].origin;
+ last_window_origin = [[last_view window] frame].origin;
+ last_window_level = [[last_view window] level];
+
+ // This is Cocoa Windowed FullScreen Mode
+ // Get the screen rect of our current display
+ NSScreen* pScreen = [[NSScreen screens] objectAtIndex:m_lastDisplayNr];
+ NSRect screenRect = [pScreen frame];
+
+ // remove frame origin offset of original display
+ screenRect.origin = NSZeroPoint;
+
+ // make a new window to act as the windowedFullScreen
+ windowedFullScreenwindow = [[NSWindow alloc] initWithContentRect:screenRect
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO
+ screen:pScreen];
+ windowedFullScreenwindow.releasedWhenClosed = NO;
+
+ [windowedFullScreenwindow setBackgroundColor:[NSColor blackColor]];
+ [windowedFullScreenwindow makeKeyAndOrderFront:nil];
+
+ // make our window the same level as the rest to enable cmd+tab switching
+ [windowedFullScreenwindow setLevel:NSNormalWindowLevel];
+ // this will make our window topmost and hide all system messages
+ //[windowedFullScreenwindow setLevel:CGShieldingWindowLevel()];
+
+ // ...and the original one beneath it and on the same screen.
+ [[last_view window] setLevel:NSNormalWindowLevel-1];
+ [[last_view window] setFrameOrigin:[pScreen frame].origin];
+ // expand the mouse bounds in SDL view to fullscreen
+ [ last_view setFrameOrigin:NSMakePoint(0.0, 0.0)];
+ [ last_view setFrameSize:NSMakeSize(m_nWidth, m_nHeight) ];
+
+ NSView* blankView = [[NSView alloc] init];
+ [windowedFullScreenwindow setContentView:blankView];
+ [windowedFullScreenwindow setContentSize:NSMakeSize(m_nWidth, m_nHeight)];
+ [windowedFullScreenwindow update];
+ [blankView setFrameSize:NSMakeSize(m_nWidth, m_nHeight)];
+
+ // force SDL's window origin to be at zero:
+ // on Monterey, X coordinate becomes non-zero somewhere after this method returns
+ const auto twoSeconds = dispatch_time(DISPATCH_TIME_NOW, 2LL * NSEC_PER_SEC);
+ dispatch_after(twoSeconds, dispatch_get_main_queue(), ^{
+ for (NSWindow* w in NSApp.windows)
+ {
+ if (w == windowedFullScreenwindow)
+ continue;
+ [w setFrameOrigin:w.screen.frame.origin];
+ break;
+ }
+ });
+
+ // Obtain windowed pixel format and create a new context.
+ newContext = CreateWindowedContext(cur_context);
+ [newContext setView:blankView];
+
+ // Hide the menu bar.
+ SetMenuBarVisible(false);
+
+ // Blank other displays if requested.
+ if (blankOtherDisplays)
+ BlankOtherDisplays(m_lastDisplayNr);
+
+ // Hide the mouse.
+ [NSCursor hide];
+
+ // Release old context if we created it.
+ if (CWinSystemOSXImpl::m_lastOwnedContext == cur_context)
+ {
+ [NSOpenGLContext clearCurrentContext];
+ [cur_context clearDrawable];
+ }
+
+ // activate context
+ [newContext makeCurrentContext];
+ CWinSystemOSXImpl::m_lastOwnedContext = newContext;
+ }
+ else
+ {
+ // Windowed Mode
+ // exit fullscreen
+ [cur_context clearDrawable];
+
+ [NSCursor unhide];
+
+ // Show menubar.
+ SetMenuBarVisible(true);
+
+ // restore the windowed window level
+ [[last_view window] setLevel:last_window_level];
+
+ // Unblank.
+ // Force the unblank when returning from fullscreen, we get called with blankOtherDisplays set false.
+ //if (blankOtherDisplays)
+ UnblankDisplays();
+
+ // create our new context (sharing with the current one)
+ auto newContext = CreateWindowedContext(cur_context);
+ if (!newContext)
+ return false;
+
+ // Assign view from old context, move back to original screen.
+ [newContext setView:last_view];
+ [[last_view window] setFrameOrigin:last_window_origin];
+ // return the mouse bounds in SDL view to previous size
+ [last_view setFrameSize:last_view_size];
+ [last_view setFrameOrigin:last_view_origin];
+
+ // Release the fullscreen context.
+ if (CWinSystemOSXImpl::m_lastOwnedContext == cur_context)
+ {
+ [NSOpenGLContext clearCurrentContext];
+ [cur_context clearDrawable];
+ }
+
+ // Activate context.
+ [newContext makeCurrentContext];
+ CWinSystemOSXImpl::m_lastOwnedContext = newContext;
+ }
+
+ DisplayFadeFromBlack(fade_token, needtoshowme);
+
+ ShowHideNSWindow([last_view window], needtoshowme);
+ // need to make sure SDL tracks any window size changes
+ ResizeWindow(m_nWidth, m_nHeight, -1, -1);
+ ResizeWindowInternal(m_nWidth, m_nHeight, -1, -1, last_view);
+ // restore origin once again when going to windowed mode
+ if (!fullScreen)
+ {
+ [[last_view window] setFrameOrigin:last_window_origin];
+ }
+ HandlePossibleRefreshrateChange();
+
+ m_updateGLContext = 0;
+ return true;
+}
+
+void CWinSystemOSX::UpdateResolutions()
+{
+ CWinSystemBase::UpdateResolutions();
+
+ // Add desktop resolution
+ int w, h;
+ double fps;
+
+ int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR));
+ GetScreenResolution(&w, &h, &fps, dispIdx);
+ NSString* dispName = screenNameForDisplay(GetDisplayID(dispIdx));
+ UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP), [dispName UTF8String], 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();
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+}
+
+/*
+void* Cocoa_GL_CreateContext(void* pixFmt, void* shareCtx)
+{
+ if (!pixFmt)
+ return nil;
+
+ NSOpenGLContext* newContext = [[NSOpenGLContext alloc] initWithFormat:(NSOpenGLPixelFormat*)pixFmt
+ shareContext:(NSOpenGLContext*)shareCtx];
+
+ // snipit from SDL_cocoaopengl.m
+ //
+ // Wisdom from Apple engineer in reference to UT2003's OpenGL performance:
+ // "You are blowing a couple of the internal OpenGL function caches. This
+ // appears to be happening in the VAO case. You can tell OpenGL to up
+ // the cache size by issuing the following calls right after you create
+ // the OpenGL context. The default cache size is 16." --ryan.
+ //
+
+ #ifndef GLI_ARRAY_FUNC_CACHE_MAX
+ #define GLI_ARRAY_FUNC_CACHE_MAX 284
+ #endif
+
+ #ifndef GLI_SUBMIT_FUNC_CACHE_MAX
+ #define GLI_SUBMIT_FUNC_CACHE_MAX 280
+ #endif
+
+ {
+ long cache_max = 64;
+ CGLContextObj ctx = (CGLContextObj)[newContext CGLContextObj];
+ CGLSetParameter(ctx, (CGLContextParameter)GLI_SUBMIT_FUNC_CACHE_MAX, &cache_max);
+ CGLSetParameter(ctx, (CGLContextParameter)GLI_ARRAY_FUNC_CACHE_MAX, &cache_max);
+ }
+
+ // End Wisdom from Apple Engineer section. --ryan.
+ return newContext;
+}
+*/
+
+NSOpenGLContext* CreateWindowedContext(NSOpenGLContext* shareCtx)
+{
+ NSOpenGLPixelFormat* pixFmt;
+ if (getenv("KODI_GL_PROFILE_LEGACY"))
+ {
+ NSOpenGLPixelFormatAttribute wattrs[] = {
+ NSOpenGLPFADoubleBuffer,
+ NSOpenGLPFANoRecovery,
+ NSOpenGLPFAAccelerated,
+ NSOpenGLPFADepthSize,
+ static_cast<NSOpenGLPixelFormatAttribute>(8),
+ static_cast<NSOpenGLPixelFormatAttribute>(0)};
+ pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs];
+ }
+ else
+ {
+ NSOpenGLPixelFormatAttribute wattrs_gl3[] = {
+ NSOpenGLPFADoubleBuffer,
+ NSOpenGLPFAOpenGLProfile,
+ NSOpenGLProfileVersion3_2Core,
+ NSOpenGLPFANoRecovery,
+ NSOpenGLPFAAccelerated,
+ NSOpenGLPFADepthSize,
+ static_cast<NSOpenGLPixelFormatAttribute>(24),
+ static_cast<NSOpenGLPixelFormatAttribute>(0)};
+ pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs_gl3];
+ }
+
+ auto newContext = [[NSOpenGLContext alloc] initWithFormat:pixFmt shareContext:shareCtx];
+
+ if (!newContext)
+ {
+ // bah, try again for non-accelerated renderer
+ NSOpenGLPixelFormatAttribute wattrs2[] =
+ {
+ NSOpenGLPFADoubleBuffer,
+ NSOpenGLPFANoRecovery,
+ NSOpenGLPFADepthSize, (NSOpenGLPixelFormatAttribute)8,
+ (NSOpenGLPixelFormatAttribute)0
+ };
+ newContext = [[NSOpenGLContext alloc]
+ initWithFormat:[[NSOpenGLPixelFormat alloc] initWithAttributes:wattrs2]
+ shareContext:shareCtx];
+ }
+
+ return newContext;
+}
+
+NSOpenGLContext* CreateFullScreenContext(int screen_index, NSOpenGLContext* shareCtx)
+{
+ CGDirectDisplayID displayArray[MAX_DISPLAYS];
+ CGDisplayCount numDisplays;
+ CGDirectDisplayID displayID;
+
+ // Get the list of displays.
+ CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays);
+ displayID = displayArray[screen_index];
+
+ NSOpenGLPixelFormat* pixFmt;
+ if (getenv("KODI_GL_PROFILE_LEGACY"))
+ {
+ NSOpenGLPixelFormatAttribute fsattrs[] = {
+ NSOpenGLPFADoubleBuffer,
+ NSOpenGLPFANoRecovery,
+ NSOpenGLPFAAccelerated,
+ NSOpenGLPFADepthSize,
+ static_cast<NSOpenGLPixelFormatAttribute>(8),
+ NSOpenGLPFAScreenMask,
+ static_cast<NSOpenGLPixelFormatAttribute>(CGDisplayIDToOpenGLDisplayMask(displayID)),
+ static_cast<NSOpenGLPixelFormatAttribute>(0)};
+ pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:fsattrs];
+ }
+ else
+ {
+ NSOpenGLPixelFormatAttribute fsattrs_gl3[] = {
+ NSOpenGLPFADoubleBuffer,
+ NSOpenGLPFANoRecovery,
+ NSOpenGLPFAAccelerated,
+ NSOpenGLPFADepthSize,
+ static_cast<NSOpenGLPixelFormatAttribute>(24),
+ NSOpenGLPFAOpenGLProfile,
+ NSOpenGLProfileVersion3_2Core,
+ NSOpenGLPFAScreenMask,
+ static_cast<NSOpenGLPixelFormatAttribute>(CGDisplayIDToOpenGLDisplayMask(displayID)),
+ static_cast<NSOpenGLPixelFormatAttribute>(0)};
+ pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:fsattrs_gl3];
+ }
+
+ if (!pixFmt)
+ return nil;
+
+ auto newContext = [[NSOpenGLContext alloc] initWithFormat:pixFmt shareContext:shareCtx];
+
+ return newContext;
+}
+
+void CWinSystemOSX::GetScreenResolution(int* w, int* h, double* fps, int screenIdx)
+{
+ CGDirectDisplayID display_id = (CGDirectDisplayID)GetDisplayID(screenIdx);
+ CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display_id);
+ *w = CGDisplayModeGetWidth(mode);
+ *h = CGDisplayModeGetHeight(mode);
+ *fps = CGDisplayModeGetRefreshRate(mode);
+ CGDisplayModeRelease(mode);
+ if ((int)*fps == 0)
+ {
+ // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays.
+ *fps = 60.0;
+ }
+}
+
+void CWinSystemOSX::EnableVSync(bool enable)
+{
+ // OpenGL Flush synchronised with vertical retrace
+ GLint swapInterval = enable ? 1 : 0;
+ [[NSOpenGLContext currentContext] setValues:&swapInterval forParameter:NSOpenGLCPSwapInterval];
+}
+
+bool CWinSystemOSX::SwitchToVideoMode(int width, int height, double refreshrate)
+{
+ boolean_t match = false;
+ CGDisplayModeRef dispMode = NULL;
+
+ int screenIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR));
+
+ // Figure out the screen size. (default to main screen)
+ CGDirectDisplayID display_id = GetDisplayID(screenIdx);
+
+ // find mode that matches the desired size, refreshrate
+ // non interlaced, nonstretched, safe for hardware
+ dispMode = GetMode(width, height, refreshrate, screenIdx);
+
+ //not found - fallback to bestemdeforparameters
+ if (!dispMode)
+ {
+ dispMode = BestMatchForMode(display_id, 32, width, height, match);
+
+ if (!match)
+ dispMode = BestMatchForMode(display_id, 16, width, height, match);
+
+ // still no match? fallback to current resolution of the display which HAS to work [tm]
+ if (!match)
+ {
+ int tmpWidth;
+ int tmpHeight;
+ double tmpRefresh;
+
+ GetScreenResolution(&tmpWidth, &tmpHeight, &tmpRefresh, screenIdx);
+ dispMode = GetMode(tmpWidth, tmpHeight, tmpRefresh, screenIdx);
+
+ // no way to get a resolution set
+ if (!dispMode)
+ return false;
+ }
+
+ if (!match)
+ return false;
+ }
+
+ // switch mode and return success
+ CGDisplayCapture(display_id);
+ CGDisplayConfigRef cfg;
+ CGBeginDisplayConfiguration(&cfg);
+ CGConfigureDisplayWithDisplayMode(cfg, display_id, dispMode, nullptr);
+ CGError err = CGCompleteDisplayConfiguration(cfg, kCGConfigureForAppOnly);
+ CGDisplayRelease(display_id);
+
+ m_refreshRate = CGDisplayModeGetRefreshRate(dispMode);
+
+ Cocoa_CVDisplayLinkUpdate();
+
+ return (err == kCGErrorSuccess);
+}
+
+void CWinSystemOSX::FillInVideoModes()
+{
+ int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR));
+
+ // Add full screen settings for additional monitors
+ int numDisplays = [[NSScreen screens] count];
+
+ for (int disp = 0; disp < numDisplays; disp++)
+ {
+ Boolean stretched;
+ Boolean interlaced;
+ Boolean safeForHardware;
+ Boolean televisionoutput;
+ int w, h, bitsperpixel;
+ double refreshrate;
+ RESOLUTION_INFO res;
+
+ CFArrayRef displayModes = GetAllDisplayModes(GetDisplayID(disp));
+ NSString *dispName = screenNameForDisplay(GetDisplayID(disp));
+
+ CLog::Log(LOGINFO, "Display {} has name {}", disp, [dispName UTF8String]);
+
+ if (NULL == displayModes)
+ continue;
+
+ for (int i=0; i < CFArrayGetCount(displayModes); ++i)
+ {
+ CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i);
+
+ uint32_t flags = CGDisplayModeGetIOFlags(displayMode);
+ stretched = flags & kDisplayModeStretchedFlag ? true : false;
+ interlaced = flags & kDisplayModeInterlacedFlag ? true : false;
+ bitsperpixel = DisplayBitsPerPixelForMode(displayMode);
+ safeForHardware = flags & kDisplayModeSafetyFlags ? true : false;
+ televisionoutput = flags & kDisplayModeTelevisionFlag ? true : false;
+
+ if ((bitsperpixel == 32) &&
+ (safeForHardware == YES) &&
+ (stretched == NO) &&
+ (interlaced == NO))
+ {
+ w = CGDisplayModeGetWidth(displayMode);
+ h = CGDisplayModeGetHeight(displayMode);
+ refreshrate = CGDisplayModeGetRefreshRate(displayMode);
+ if ((int)refreshrate == 0) // LCD display?
+ {
+ // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays.
+ refreshrate = 60.0;
+ }
+ CLog::Log(LOGINFO, "Found possible resolution for display {} with {} x {} @ {:f} Hz", disp,
+ w, h, refreshrate);
+
+ // only add the resolution if it belongs to "our" screen
+ // all others are only logged above...
+ if (disp == dispIdx)
+ {
+ UpdateDesktopResolution(res, (dispName != nil) ? [dispName UTF8String] : "Unknown", w, h, refreshrate, 0);
+ CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res);
+ CDisplaySettings::GetInstance().AddResolutionInfo(res);
+ }
+ }
+ }
+ CFRelease(displayModes);
+ }
+}
+
+bool CWinSystemOSX::FlushBuffer(void)
+{
+ if (m_updateGLContext < 5)
+ {
+ [m_impl->m_glContext update];
+ m_updateGLContext++;
+ }
+
+ [m_impl->m_glContext flushBuffer];
+
+ return true;
+}
+
+bool CWinSystemOSX::IsObscured(void)
+{
+ // check once a second if we are obscured.
+ auto now_time = std::chrono::steady_clock::now();
+ if (m_obscured_timecheck > now_time)
+ return m_obscured;
+ else
+ m_obscured_timecheck = now_time + std::chrono::milliseconds(1000);
+
+ NSOpenGLContext* cur_context = [NSOpenGLContext currentContext];
+ NSView* view = [cur_context view];
+ if (!view)
+ {
+ // sanity check, we should always have a view
+ m_obscured = true;
+ return m_obscured;
+ }
+
+ NSWindow *window = [view window];
+ if (!window)
+ {
+ // sanity check, we should always have a window
+ m_obscured = true;
+ return m_obscured;
+ }
+
+ if ([window isVisible] == NO)
+ {
+ // not visible means the window is not showing.
+ // this should never really happen as we are always visible
+ // even when minimized in dock.
+ m_obscured = true;
+ return m_obscured;
+ }
+
+ // check if we are minimized (to an icon in the Dock).
+ if ([window isMiniaturized] == YES)
+ {
+ m_obscured = true;
+ return m_obscured;
+ }
+
+ // check if we are showing on the active workspace.
+ if ([window isOnActiveSpace] == NO)
+ {
+ m_obscured = true;
+ return m_obscured;
+ }
+
+ // default to false before we start parsing though the windows.
+ // if we are are obscured by any windows, then set true.
+ m_obscured = false;
+ static bool obscureLogged = false;
+
+ CGWindowListOption opts;
+ opts = kCGWindowListOptionOnScreenAboveWindow | kCGWindowListExcludeDesktopElements;
+ CFArrayRef windowIDs =CGWindowListCreate(opts, (CGWindowID)[window windowNumber]);
+
+ if (!windowIDs)
+ return m_obscured;
+
+ CFArrayRef windowDescs = CGWindowListCreateDescriptionFromArray(windowIDs);
+ if (!windowDescs)
+ {
+ CFRelease(windowIDs);
+ return m_obscured;
+ }
+
+ CGRect bounds = NSRectToCGRect([window frame]);
+ // kCGWindowBounds measures the origin as the top-left corner of the rectangle
+ // relative to the top-left corner of the screen.
+ // NSWindow’s frame property measures the origin as the bottom-left corner
+ // of the rectangle relative to the bottom-left corner of the screen.
+ // convert bounds from NSWindow to CGWindowBounds here.
+ bounds.origin.y = [[window screen] frame].size.height - bounds.origin.y - bounds.size.height;
+
+ std::vector<CRect> partialOverlaps;
+ CRect ourBounds = CGRectToCRect(bounds);
+
+ for (CFIndex idx=0; idx < CFArrayGetCount(windowDescs); idx++)
+ {
+ // walk the window list of windows that are above us and are not desktop elements
+ CFDictionaryRef windowDictionary = (CFDictionaryRef)CFArrayGetValueAtIndex(windowDescs, idx);
+
+ // skip the Dock window, it actually covers the entire screen.
+ CFStringRef ownerName = (CFStringRef)CFDictionaryGetValue(windowDictionary, kCGWindowOwnerName);
+ if (CFStringCompare(ownerName, CFSTR("Dock"), 0) == kCFCompareEqualTo)
+ continue;
+
+ // Ignore known brightness tools for dimming the screen. They claim to cover
+ // the whole XBMC window and therefore would make the framerate limiter
+ // kicking in. Unfortunately even the alpha of these windows is 1.0 so
+ // we have to check the ownerName.
+ if (CFStringCompare(ownerName, CFSTR("Shades"), 0) == kCFCompareEqualTo ||
+ CFStringCompare(ownerName, CFSTR("SmartSaver"), 0) == kCFCompareEqualTo ||
+ CFStringCompare(ownerName, CFSTR("Brightness Slider"), 0) == kCFCompareEqualTo ||
+ CFStringCompare(ownerName, CFSTR("Displaperture"), 0) == kCFCompareEqualTo ||
+ CFStringCompare(ownerName, CFSTR("Dreamweaver"), 0) == kCFCompareEqualTo ||
+ CFStringCompare(ownerName, CFSTR("Window Server"), 0) == kCFCompareEqualTo)
+ continue;
+
+ CFDictionaryRef rectDictionary = (CFDictionaryRef)CFDictionaryGetValue(windowDictionary, kCGWindowBounds);
+ if (!rectDictionary)
+ continue;
+
+ CGRect windowBounds;
+ if (CGRectMakeWithDictionaryRepresentation(rectDictionary, &windowBounds))
+ {
+ if (CGRectContainsRect(windowBounds, bounds))
+ {
+ // if the windowBounds completely encloses our bounds, we are obscured.
+ if (!obscureLogged)
+ {
+ std::string appName;
+ if (CDarwinUtils::CFStringRefToUTF8String(ownerName, appName))
+ CLog::Log(LOGDEBUG, "WinSystemOSX: Fullscreen window {} obscures Kodi!", appName);
+ obscureLogged = true;
+ }
+ m_obscured = true;
+ break;
+ }
+
+ // handle overlapping windows above us that combine
+ // to obscure by collecting any partial overlaps,
+ // then subtract them from our bounds and check
+ // for any remaining area.
+ CRect intersection = CGRectToCRect(windowBounds);
+ intersection.Intersect(ourBounds);
+ if (!intersection.IsEmpty())
+ partialOverlaps.push_back(intersection);
+ }
+ }
+
+ if (!m_obscured)
+ {
+ // if we are here we are not obscured by any fullscreen window - reset flag
+ // for allowing the logmessage above to show again if this changes.
+ if (obscureLogged)
+ obscureLogged = false;
+ std::vector<CRect> rects = ourBounds.SubtractRects(partialOverlaps);
+ // they got us covered
+ if (rects.empty())
+ m_obscured = true;
+ }
+
+ CFRelease(windowDescs);
+ CFRelease(windowIDs);
+
+ return m_obscured;
+}
+
+void CWinSystemOSX::NotifyAppFocusChange(bool bGaining)
+{
+ if (!(m_bFullScreen && bGaining))
+ return;
+ @autoreleasepool
+ {
+ // find the window
+ NSOpenGLContext* context = [NSOpenGLContext currentContext];
+ if (context)
+ {
+ NSView* view;
+
+ view = [context view];
+ if (view)
+ {
+ NSWindow* window;
+ window = [view window];
+ if (window)
+ {
+ SetMenuBarVisible(false);
+ [window orderFront:nil];
+ }
+ }
+ }
+ }
+}
+
+void CWinSystemOSX::ShowOSMouse(bool show)
+{
+ SDL_ShowCursor(show ? 1 : 0);
+}
+
+bool CWinSystemOSX::Minimize()
+{
+ @autoreleasepool
+ {
+ [[NSApplication sharedApplication] miniaturizeAll:nil];
+ }
+ return true;
+}
+
+bool CWinSystemOSX::Restore()
+{
+ @autoreleasepool
+ {
+ [[NSApplication sharedApplication] unhide:nil];
+ }
+ return true;
+}
+
+bool CWinSystemOSX::Hide()
+{
+ @autoreleasepool
+ {
+ [[NSApplication sharedApplication] hide:nil];
+ }
+ return true;
+}
+
+void CWinSystemOSX::HandlePossibleRefreshrateChange()
+{
+ static double oldRefreshRate = m_refreshRate;
+ Cocoa_CVDisplayLinkUpdate();
+ int dummy = 0;
+
+ GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr);
+
+ if (oldRefreshRate != m_refreshRate)
+ {
+ oldRefreshRate = m_refreshRate;
+ // send a message so that videoresolution (and refreshrate)
+ // is changed
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_VIDEORESIZE, m_SDLSurface->w, m_SDLSurface->h);
+ }
+}
+
+void CWinSystemOSX::OnMove(int x, int y)
+{
+ HandlePossibleRefreshrateChange();
+}
+
+std::unique_ptr<IOSScreenSaver> CWinSystemOSX::GetOSScreenSaverImpl()
+{
+ return std::unique_ptr<IOSScreenSaver> (new COSScreenSaverOSX);
+}
+
+OSXTextInputResponder *g_textInputResponder = nil;
+
+void CWinSystemOSX::StartTextInput()
+{
+ NSView *parentView = [[NSApp keyWindow] contentView];
+
+ /* We only keep one field editor per process, since only the front most
+ * window can receive text input events, so it make no sense to keep more
+ * than one copy. When we switched to another window and requesting for
+ * text input, simply remove the field editor from its superview then add
+ * it to the front most window's content view */
+ if (!g_textInputResponder) {
+ g_textInputResponder =
+ [[OSXTextInputResponder alloc] initWithFrame: NSMakeRect(0.0, 0.0, 0.0, 0.0)];
+ }
+
+ if (![[g_textInputResponder superview] isEqual: parentView])
+ {
+// DLOG(@"add fieldEdit to window contentView");
+ [g_textInputResponder removeFromSuperview];
+ [parentView addSubview: g_textInputResponder];
+ [[NSApp keyWindow] makeFirstResponder: g_textInputResponder];
+ }
+}
+void CWinSystemOSX::StopTextInput()
+{
+ if (g_textInputResponder)
+ {
+ [g_textInputResponder removeFromSuperview];
+ g_textInputResponder = nil;
+ }
+}
+
+void CWinSystemOSX::Register(IDispResource *resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ m_resources.push_back(resource);
+}
+
+void CWinSystemOSX::Unregister(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> 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);
+}
+
+bool CWinSystemOSX::Show(bool raise)
+{
+ @autoreleasepool
+ {
+ auto app = [NSApplication sharedApplication];
+ if (raise)
+ {
+ [app unhide:nil];
+ [app activateIgnoringOtherApps:YES];
+ [app arrangeInFront:nil];
+ }
+ else
+ {
+ [app unhideWithoutActivation];
+ }
+ }
+ return true;
+}
+
+void CWinSystemOSX::WindowChangedScreen()
+{
+ // user has moved the window to a
+ // different screen
+ NSOpenGLContext* context = [NSOpenGLContext currentContext];
+ m_lastDisplayNr = -1;
+
+ // if we are here the user dragged the window to a different
+ // screen and we return the screen of the window
+ if (context)
+ {
+ NSView* view;
+
+ view = [context view];
+ if (view)
+ {
+ NSWindow* window;
+ window = [view window];
+ if (window)
+ {
+ m_lastDisplayNr = GetDisplayIndex(GetDisplayIDFromScreen([window screen]));
+ }
+ }
+ }
+ if (m_lastDisplayNr == -1)
+ m_lastDisplayNr = 0;// default to main screen
+}
+
+void CWinSystemOSX::AnnounceOnLostDevice()
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ // tell any shared resources
+ CLog::Log(LOGDEBUG, "CWinSystemOSX::AnnounceOnLostDevice");
+ for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnLostDisplay();
+}
+
+void CWinSystemOSX::HandleOnResetDevice()
+{
+
+ auto delay =
+ std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ "videoscreen.delayrefreshchange") *
+ 100);
+ if (delay > 0ms)
+ {
+ m_delayDispReset = true;
+ m_dispResetTimer.Set(delay);
+ }
+ else
+ {
+ AnnounceOnResetDevice();
+ }
+}
+
+void CWinSystemOSX::AnnounceOnResetDevice()
+{
+ double currentFps = m_refreshRate;
+ int w = 0;
+ int h = 0;
+ int currentScreenIdx = m_lastDisplayNr;
+ // ensure that graphics context knows about the current refreshrate before
+ // doing the callbacks
+ GetScreenResolution(&w, &h, &currentFps, currentScreenIdx);
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetFPS(currentFps);
+
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ // tell any shared resources
+ CLog::Log(LOGDEBUG, "CWinSystemOSX::AnnounceOnResetDevice");
+ for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnResetDisplay();
+}
+
+void* CWinSystemOSX::GetCGLContextObj()
+{
+ return [m_impl->m_glContext CGLContextObj];
+}
+
+NSOpenGLContext* CWinSystemOSX::GetNSOpenGLContext()
+{
+ return m_impl->m_glContext;
+}
+
+std::string CWinSystemOSX::GetClipboardText(void)
+{
+ std::string utf8_text;
+
+ const char *szStr = Cocoa_Paste();
+ if (szStr)
+ utf8_text = szStr;
+
+ return utf8_text;
+}
+
+std::unique_ptr<CVideoSync> CWinSystemOSX::GetVideoSync(void *clock)
+{
+ std::unique_ptr<CVideoSync> pVSync(new CVideoSyncOsx(clock));
+ return pVSync;
+}
+
+bool CWinSystemOSX::MessagePump()
+{
+ return m_winEvents->MessagePump();
+}
+
+std::vector<std::string> CWinSystemOSX::GetConnectedOutputs()
+{
+ std::vector<std::string> outputs;
+ outputs.emplace_back("Default");
+
+ int numDisplays = [[NSScreen screens] count];
+
+ for (int disp = 0; disp < numDisplays; disp++)
+ {
+ NSString *dispName = screenNameForDisplay(GetDisplayID(disp));
+ outputs.emplace_back([dispName UTF8String]);
+ }
+
+ return outputs;
+}
diff --git a/xbmc/windowing/osx/VideoSyncOsx.h b/xbmc/windowing/osx/VideoSyncOsx.h
new file mode 100644
index 0000000..b3ba145
--- /dev/null
+++ b/xbmc/windowing/osx/VideoSyncOsx.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "guilib/DispResource.h"
+#include "threads/Event.h"
+#include "windowing/VideoSync.h"
+
+class CVideoSyncOsx : public CVideoSync, IDispResource
+{
+public:
+ CVideoSyncOsx(void* clock)
+ : CVideoSync(clock), m_LastVBlankTime(0), m_displayLost(false), m_displayReset(false)
+ {
+ }
+
+ // CVideoSync interface
+ bool Setup(PUPDATECLOCK func) override;
+ void Run(CEvent& stopEvent) override;
+ void Cleanup() override;
+ float GetFps() override;
+ void RefreshChanged() override;
+
+ // IDispResource interface
+ void OnLostDisplay() override;
+ void OnResetDisplay() override;
+
+ // used in the displaylink callback
+ void VblankHandler(int64_t nowtime, uint32_t timebase);
+
+private:
+ virtual bool InitDisplayLink();
+ virtual void DeinitDisplayLink();
+
+ int64_t m_LastVBlankTime; //timestamp of the last vblank, used for calculating how many vblanks happened
+ volatile bool m_displayLost;
+ volatile bool m_displayReset;
+ CEvent m_lostEvent;
+};
+
diff --git a/xbmc/windowing/osx/VideoSyncOsx.mm b/xbmc/windowing/osx/VideoSyncOsx.mm
new file mode 100644
index 0000000..d19e724
--- /dev/null
+++ b/xbmc/windowing/osx/VideoSyncOsx.mm
@@ -0,0 +1,148 @@
+/*
+ * 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 "VideoSyncOsx.h"
+
+#include "ServiceBroker.h"
+#include "utils/MathUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WinSystem.h"
+
+#include "platform/darwin/osx/CocoaInterface.h"
+
+#include <CoreVideo/CVHostTime.h>
+#include <QuartzCore/CVDisplayLink.h>
+#include <unistd.h>
+
+using namespace std::chrono_literals;
+
+bool CVideoSyncOsx::Setup(PUPDATECLOCK func)
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} setting up OSX", __FUNCTION__);
+
+ //init the vblank timestamp
+ m_LastVBlankTime = 0;
+ UpdateClock = func;
+ m_displayLost = false;
+ m_displayReset = false;
+ m_lostEvent.Reset();
+
+ CServiceBroker::GetWinSystem()->Register(this);
+
+ return true;
+}
+
+void CVideoSyncOsx::Run(CEvent& stopEvent)
+{
+ InitDisplayLink();
+
+ //because cocoa has a vblank callback, we just keep sleeping until we're asked to stop the thread
+ while(!stopEvent.Signaled() && !m_displayLost && !m_displayReset)
+ {
+ usleep(100000);
+ }
+
+ m_lostEvent.Set();
+
+ while(!stopEvent.Signaled() && m_displayLost && !m_displayReset)
+ {
+ usleep(10000);
+ }
+
+ DeinitDisplayLink();
+}
+
+void CVideoSyncOsx::Cleanup()
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} cleaning up OSX", __FUNCTION__);
+ m_lostEvent.Set();
+ m_LastVBlankTime = 0;
+ CServiceBroker::GetWinSystem()->Unregister(this);
+}
+
+float CVideoSyncOsx::GetFps()
+{
+ m_fps = CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS();
+ CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} Detected refreshrate: {:f} hertz", __FUNCTION__, m_fps);
+ return m_fps;
+}
+
+void CVideoSyncOsx::RefreshChanged()
+{
+ m_displayReset = true;
+}
+
+void CVideoSyncOsx::OnLostDisplay()
+{
+ if (!m_displayLost)
+ {
+ m_displayLost = true;
+ m_lostEvent.Wait(1000ms);
+ }
+}
+
+void CVideoSyncOsx::OnResetDisplay()
+{
+ m_displayReset = true;
+}
+
+void CVideoSyncOsx::VblankHandler(int64_t nowtime, uint32_t timebase)
+{
+ int NrVBlanks;
+ double VBlankTime;
+ int64_t Now = CurrentHostCounter();
+
+ if (m_LastVBlankTime != 0)
+ {
+ VBlankTime = (double)(nowtime - m_LastVBlankTime) / (double)timebase;
+ NrVBlanks = MathUtils::round_int(VBlankTime * static_cast<double>(m_fps));
+
+ //update the vblank timestamp, update the clock and send a signal that we got a vblank
+ UpdateClock(NrVBlanks, Now, m_refClock);
+ }
+
+ //save the timestamp of this vblank so we can calculate how many happened next time
+ m_LastVBlankTime = nowtime;
+}
+
+// Called by the Core Video Display Link whenever it's appropriate to render a frame.
+static CVReturn DisplayLinkCallBack(CVDisplayLinkRef displayLink, const CVTimeStamp* inNow, const CVTimeStamp* inOutputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext)
+{
+ @autoreleasepool
+ {
+ CVideoSyncOsx* VideoSyncOsx = reinterpret_cast<CVideoSyncOsx*>(displayLinkContext);
+
+ if (inOutputTime->flags & kCVTimeStampHostTimeValid)
+ VideoSyncOsx->VblankHandler(inOutputTime->hostTime, CVGetHostClockFrequency());
+ else
+ VideoSyncOsx->VblankHandler(CVGetCurrentHostTime(), CVGetHostClockFrequency());
+ }
+
+ return kCVReturnSuccess;
+}
+
+bool CVideoSyncOsx::InitDisplayLink()
+{
+ bool ret = true;
+ CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} setting up displaylink", __FUNCTION__);
+
+ if (!Cocoa_CVDisplayLinkCreate((void*)DisplayLinkCallBack, reinterpret_cast<void*>(this)))
+ {
+ CLog::Log(LOGDEBUG, "CVideoSyncOsx::{} Cocoa_CVDisplayLinkCreate failed", __FUNCTION__);
+ ret = false;
+ }
+ return ret;
+}
+
+void CVideoSyncOsx::DeinitDisplayLink()
+{
+ Cocoa_CVDisplayLinkRelease();
+}
+
diff --git a/xbmc/windowing/osx/WinEventsOSX.h b/xbmc/windowing/osx/WinEventsOSX.h
new file mode 100644
index 0000000..7c4644f
--- /dev/null
+++ b/xbmc/windowing/osx/WinEventsOSX.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2011-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/Thread.h"
+#include "windowing/WinEvents.h"
+#include "windowing/XBMC_events.h"
+
+#include <memory>
+
+struct CWinEventsOSXImplWrapper;
+
+class CWinEventsOSX : public IWinEvents, public CThread
+{
+public:
+ CWinEventsOSX();
+ ~CWinEventsOSX();
+
+ void MessagePush(XBMC_Event* newEvent);
+ bool MessagePump();
+ size_t GetQueueSize();
+
+ void enableInputEvents();
+ void disableInputEvents();
+
+private:
+ std::unique_ptr<CWinEventsOSXImplWrapper> m_eventsImplWrapper;
+};
diff --git a/xbmc/windowing/osx/WinEventsOSX.mm b/xbmc/windowing/osx/WinEventsOSX.mm
new file mode 100644
index 0000000..b2d07db
--- /dev/null
+++ b/xbmc/windowing/osx/WinEventsOSX.mm
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2011-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 "WinEventsOSX.h"
+
+#import "WinEventsOSXImpl.h"
+
+struct CWinEventsOSXImplWrapper
+{
+ CWinEventsOSXImpl* callbackClass;
+};
+
+CWinEventsOSX::CWinEventsOSX() : CThread("CWinEventsOSX")
+{
+ m_eventsImplWrapper = std::make_unique<CWinEventsOSXImplWrapper>();
+ m_eventsImplWrapper->callbackClass = [CWinEventsOSXImpl new];
+ Create();
+}
+
+CWinEventsOSX::~CWinEventsOSX()
+{
+ m_bStop = true;
+ StopThread(true);
+}
+
+void CWinEventsOSX::MessagePush(XBMC_Event* newEvent)
+{
+ [m_eventsImplWrapper->callbackClass MessagePush:newEvent];
+}
+
+size_t CWinEventsOSX::GetQueueSize()
+{
+ return [m_eventsImplWrapper->callbackClass GetQueueSize];
+}
+
+bool CWinEventsOSX::MessagePump()
+{
+ return [m_eventsImplWrapper->callbackClass MessagePump];
+}
+
+void CWinEventsOSX::enableInputEvents()
+{
+ return [m_eventsImplWrapper->callbackClass enableInputEvents];
+}
+
+void CWinEventsOSX::disableInputEvents()
+{
+ return [m_eventsImplWrapper->callbackClass disableInputEvents];
+}
diff --git a/xbmc/windowing/osx/WinEventsOSXImpl.h b/xbmc/windowing/osx/WinEventsOSXImpl.h
new file mode 100644
index 0000000..2dcacd4
--- /dev/null
+++ b/xbmc/windowing/osx/WinEventsOSXImpl.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011-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 "windowing/osx/WinEventsOSX.h"
+
+#import <CoreGraphics/CoreGraphics.h>
+#import <Foundation/Foundation.h>
+
+@interface CWinEventsOSXImpl : NSObject
+
+- (void)MessagePush:(XBMC_Event*)newEvent;
+- (size_t)GetQueueSize;
+- (bool)MessagePump;
+- (void)enableInputEvents;
+- (void)disableInputEvents;
+- (XBMC_Event)keyPressEvent:(CGEventRef*)event;
+
+@end
diff --git a/xbmc/windowing/osx/WinEventsOSXImpl.mm b/xbmc/windowing/osx/WinEventsOSXImpl.mm
new file mode 100644
index 0000000..48c4e37
--- /dev/null
+++ b/xbmc/windowing/osx/WinEventsOSXImpl.mm
@@ -0,0 +1,364 @@
+/*
+ * 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 "WinEventsOSXImpl.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "application/Application.h"
+#include "input/XBMC_keysym.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "input/mouse/MouseStat.h"
+#include "messaging/ApplicationMessenger.h"
+#include "threads/CriticalSection.h"
+#include "utils/log.h"
+#include "windowing/osx/WinSystemOSX.h"
+
+#include <memory>
+#include <mutex>
+#include <queue>
+
+#import <AppKit/AppKit.h>
+#import <Carbon/Carbon.h> // kvk_ANSI_ keycodes
+#import <CoreGraphics/CoreGraphics.h>
+#import <Foundation/Foundation.h>
+
+#pragma mark - objc implementation
+
+@implementation CWinEventsOSXImpl
+{
+ std::queue<XBMC_Event> events;
+ CCriticalSection m_inputlock;
+ id mLocalMonitorId;
+}
+
+#pragma mark - init
+
+- (instancetype)init
+{
+ self = [super init];
+
+ [self enableInputEvents];
+
+ return self;
+}
+
+- (void)MessagePush:(XBMC_Event*)newEvent
+{
+ std::unique_lock<CCriticalSection> lock(m_inputlock);
+ events.emplace(*newEvent);
+}
+
+- (bool)MessagePump
+{
+
+ bool ret = false;
+
+ // 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 = [self 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 = {};
+ {
+ std::unique_lock<CCriticalSection> lock(m_inputlock);
+ if (events.size() == 0)
+ return ret;
+ pumpEvent = events.front();
+ events.pop();
+ }
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ ret |= appPort->OnEvent(pumpEvent);
+ }
+ return ret;
+}
+
+- (size_t)GetQueueSize
+{
+ std::unique_lock<CCriticalSection> lock(m_inputlock);
+ return events.size();
+}
+
+- (unichar)OsxKey2XbmcKey:(unichar)character
+{
+ switch (character)
+ {
+ case kVK_ANSI_8:
+ case NSLeftArrowFunctionKey:
+ return XBMCK_LEFT;
+ case kVK_ANSI_0:
+ case NSRightArrowFunctionKey:
+ return XBMCK_RIGHT;
+ case kVK_ANSI_RightBracket:
+ case NSUpArrowFunctionKey:
+ return XBMCK_UP;
+ case kVK_ANSI_O:
+ case NSDownArrowFunctionKey:
+ return XBMCK_DOWN;
+ case NSDeleteCharacter:
+ return XBMCK_BACKSPACE;
+ default:
+ return character;
+ }
+}
+
+- (XBMCMod)OsxMod2XbmcMod:(CGEventFlags)appleModifier
+{
+ unsigned int xbmcModifier = XBMCKMOD_NONE;
+ // shift left
+ if (appleModifier & kCGEventFlagMaskAlphaShift)
+ xbmcModifier |= XBMCKMOD_LSHIFT;
+ // shift right
+ if (appleModifier & kCGEventFlagMaskShift)
+ xbmcModifier |= XBMCKMOD_RSHIFT;
+ // left ctrl
+ if (appleModifier & kCGEventFlagMaskControl)
+ xbmcModifier |= XBMCKMOD_LCTRL;
+ // left alt/option
+ if (appleModifier & kCGEventFlagMaskAlternate)
+ xbmcModifier |= XBMCKMOD_LALT;
+ // left command
+ if (appleModifier & kCGEventFlagMaskCommand)
+ xbmcModifier |= XBMCKMOD_LMETA;
+
+ return static_cast<XBMCMod>(xbmcModifier);
+}
+
+- (bool)ProcessOSXShortcuts:(XBMC_Event&)event
+{
+ const auto cmd = (event.key.keysym.mod & (XBMCKMOD_LMETA | XBMCKMOD_RMETA)) != 0;
+ if (cmd && event.type == XBMC_KEYDOWN)
+ {
+ switch (event.key.keysym.sym)
+ {
+ case XBMCK_q: // CMD-q to quit
+ if (!g_application.m_bStop)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+ return true;
+
+ case XBMCK_CTRLF: // CMD-CTRL-f to toggle fullscreen
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN);
+ return true;
+
+ case XBMCK_s: // CMD-s to take a screenshot
+ {
+ CAction* action = new CAction(ACTION_TAKE_SCREENSHOT);
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(action));
+ return true;
+ }
+ case XBMCK_h: // CMD-h to hide (but we minimize for now)
+ case XBMCK_m: // CMD-m to minimize
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MINIMIZE);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ return false;
+}
+
+- (void)enableInputEvents
+{
+ [self disableInputEvents]; // allow only one registration at a time
+
+ // clang-format off
+ // Create an event tap. We are interested in mouse and keyboard events.
+ NSEventMask eventMask =
+ NSEventMaskKeyDown | NSEventMaskKeyUp |
+ NSEventMaskLeftMouseDown | NSEventMaskLeftMouseUp |
+ NSEventMaskRightMouseDown | NSEventMaskRightMouseUp |
+ NSEventMaskOtherMouseDown | NSEventMaskOtherMouseUp |
+ NSEventMaskScrollWheel |
+ NSEventMaskLeftMouseDragged |
+ NSEventMaskRightMouseDragged |
+ NSEventMaskOtherMouseDragged |
+ NSEventMaskMouseMoved;
+ // clang-format on
+
+ mLocalMonitorId = [NSEvent addLocalMonitorForEventsMatchingMask:eventMask
+ handler:^(NSEvent* event) {
+ return [self InputEventHandler:event];
+ }];
+}
+
+- (void)disableInputEvents
+{
+ // Disable the local Monitor
+ if (mLocalMonitorId != nil)
+ [NSEvent removeMonitor:mLocalMonitorId];
+ mLocalMonitorId = nil;
+}
+
+- (NSEvent*)InputEventHandler:(NSEvent*)nsevent
+{
+ bool passEvent = true;
+ CGEventRef event = nsevent.CGEvent;
+ CGEventType type = CGEventGetType(event);
+
+ // The incoming mouse position.
+ NSPoint location = nsevent.locationInWindow;
+ if (location.x < 0 || location.y < 0)
+ return nsevent;
+
+ // cocoa world is upside down ...
+ auto winSystem = dynamic_cast<CWinSystemOSX*>(CServiceBroker::GetWinSystem());
+ if (!winSystem)
+ return nsevent;
+
+ NSRect frame = winSystem->GetWindowDimensions();
+ location.y = frame.size.height - location.y;
+
+ XBMC_Event newEvent = {};
+
+ switch (type)
+ {
+ // handle mouse events and transform them into the xbmc event world
+ case kCGEventLeftMouseUp:
+ newEvent.type = XBMC_MOUSEBUTTONUP;
+ newEvent.button.button = XBMC_BUTTON_LEFT;
+ newEvent.button.x = location.x;
+ newEvent.button.y = location.y;
+ [self MessagePush:&newEvent];
+ break;
+ case kCGEventLeftMouseDown:
+ newEvent.type = XBMC_MOUSEBUTTONDOWN;
+ newEvent.button.button = XBMC_BUTTON_LEFT;
+ newEvent.button.x = location.x;
+ newEvent.button.y = location.y;
+ [self MessagePush:&newEvent];
+ break;
+ case kCGEventRightMouseUp:
+ newEvent.type = XBMC_MOUSEBUTTONUP;
+ newEvent.button.button = XBMC_BUTTON_RIGHT;
+ newEvent.button.x = location.x;
+ newEvent.button.y = location.y;
+ [self MessagePush:&newEvent];
+ break;
+ case kCGEventRightMouseDown:
+ newEvent.type = XBMC_MOUSEBUTTONDOWN;
+ newEvent.button.button = XBMC_BUTTON_RIGHT;
+ newEvent.button.x = location.x;
+ newEvent.button.y = location.y;
+ [self MessagePush:&newEvent];
+ break;
+ case kCGEventOtherMouseUp:
+ newEvent.type = XBMC_MOUSEBUTTONUP;
+ newEvent.button.button = XBMC_BUTTON_MIDDLE;
+ newEvent.button.x = location.x;
+ newEvent.button.y = location.y;
+ [self MessagePush:&newEvent];
+ break;
+ case kCGEventOtherMouseDown:
+ newEvent.type = XBMC_MOUSEBUTTONDOWN;
+ newEvent.button.button = XBMC_BUTTON_MIDDLE;
+ newEvent.button.x = location.x;
+ newEvent.button.y = location.y;
+ [self MessagePush:&newEvent];
+ break;
+ case kCGEventMouseMoved:
+ case kCGEventLeftMouseDragged:
+ case kCGEventRightMouseDragged:
+ case kCGEventOtherMouseDragged:
+ newEvent.type = XBMC_MOUSEMOTION;
+ newEvent.motion.x = location.x;
+ newEvent.motion.y = location.y;
+ [self MessagePush:&newEvent];
+ break;
+ case kCGEventScrollWheel:
+ // very strange, real scrolls have non-zero deltaY followed by same number of events
+ // with a zero deltaY. This reverses our scroll which is WTF? anoying. Trap them out here.
+ if (nsevent.deltaY != 0.0)
+ {
+ newEvent.type = XBMC_MOUSEBUTTONDOWN;
+ newEvent.button.x = location.x;
+ newEvent.button.y = location.y;
+ newEvent.button.button =
+ CGEventGetIntegerValueField(event, kCGScrollWheelEventDeltaAxis1) > 0
+ ? XBMC_BUTTON_WHEELUP
+ : XBMC_BUTTON_WHEELDOWN;
+ [self MessagePush:&newEvent];
+
+ newEvent.type = XBMC_MOUSEBUTTONUP;
+ [self MessagePush:&newEvent];
+ }
+ break;
+
+ // handle keyboard events and transform them into the xbmc event world
+ case kCGEventKeyUp:
+ newEvent = [self keyPressEvent:&event];
+ newEvent.type = XBMC_KEYUP;
+
+ [self MessagePush:&newEvent];
+ passEvent = false;
+ break;
+ case kCGEventKeyDown:
+ newEvent = [self keyPressEvent:&event];
+ newEvent.type = XBMC_KEYDOWN;
+
+ if (![self ProcessOSXShortcuts:newEvent])
+ [self MessagePush:&newEvent];
+ passEvent = false;
+
+ break;
+ default:
+ return nsevent;
+ }
+ // We must return the event for it to be useful if not already handled
+ if (passEvent)
+ return nsevent;
+ else
+ return nullptr;
+}
+
+- (XBMC_Event)keyPressEvent:(CGEventRef*)event
+{
+ UniCharCount actualStringLength = 0;
+ // Get stringlength of event
+ CGEventKeyboardGetUnicodeString(*event, 0, &actualStringLength, nullptr);
+
+ // Create array with size of event string
+ UniChar unicodeString[actualStringLength];
+ memset(unicodeString, 0, sizeof(unicodeString));
+
+ auto keycode =
+ static_cast<CGKeyCode>(CGEventGetIntegerValueField(*event, kCGKeyboardEventKeycode));
+ CGEventKeyboardGetUnicodeString(*event, sizeof(unicodeString) / sizeof(*unicodeString),
+ &actualStringLength, unicodeString);
+
+ XBMC_Event newEvent = {};
+
+ // May be possible for actualStringLength > 1. Havent been able to replicate anything
+ // larger than 1, but keep in mind for any regressions
+ if (actualStringLength == 0)
+ {
+ return newEvent;
+ }
+ else if (actualStringLength > 1)
+ {
+ CLog::Log(LOGERROR, "CWinEventsOSXImpl::keyPressEvent - event string > 1 - size: {}",
+ static_cast<int>(actualStringLength));
+ return newEvent;
+ }
+
+ unicodeString[0] = [self OsxKey2XbmcKey:unicodeString[0]];
+
+ newEvent.key.keysym.scancode = keycode;
+ newEvent.key.keysym.sym = static_cast<XBMCKey>(unicodeString[0]);
+ newEvent.key.keysym.unicode = unicodeString[0];
+ newEvent.key.keysym.mod = [self OsxMod2XbmcMod:CGEventGetFlags(*event)];
+
+ return newEvent;
+}
+
+@end
diff --git a/xbmc/windowing/osx/WinSystemOSX.h b/xbmc/windowing/osx/WinSystemOSX.h
new file mode 100644
index 0000000..0ac08d4
--- /dev/null
+++ b/xbmc/windowing/osx/WinSystemOSX.h
@@ -0,0 +1,120 @@
+/*
+ * 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 "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "threads/Timer.h"
+#include "windowing/WinSystem.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+typedef struct _CGLContextObject* CGLContextObj;
+typedef struct CGRect NSRect;
+
+class IDispResource;
+class CWinEventsOSX;
+#ifdef __OBJC__
+@class NSWindow;
+@class OSXGLView;
+#else
+struct NSWindow;
+struct OSXGLView;
+#endif
+
+class CWinSystemOSX : public CWinSystemBase, public ITimerCallback
+{
+public:
+ CWinSystemOSX();
+ ~CWinSystemOSX() override;
+
+ // ITimerCallback interface
+ void OnTimeout() override;
+
+ // CWinSystemBase
+ 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;
+ void NotifyAppFocusChange(bool bGaining) override;
+ void ShowOSMouse(bool show) override;
+ bool Minimize() override;
+ bool Restore() override;
+ bool Hide() override;
+ bool Show(bool raise = true) override;
+ void OnMove(int x, int y) override;
+
+ void SetOcclusionState(bool occluded);
+
+ std::string GetClipboardText() override;
+
+ void Register(IDispResource* resource) override;
+ void Unregister(IDispResource* resource) override;
+
+ std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override;
+
+ void WindowChangedScreen();
+
+ void AnnounceOnLostDevice();
+ void AnnounceOnResetDevice();
+ void HandleOnResetDevice();
+ void StartLostDeviceTimer();
+ void StopLostDeviceTimer();
+
+ void SetMovedToOtherScreen(bool moved) { m_movedToOtherScreen = moved; }
+ int CheckDisplayChanging(uint32_t flags);
+ void SetFullscreenWillToggle(bool toggle) { m_fullscreenWillToggle = toggle; }
+ bool GetFullscreenWillToggle() { return m_fullscreenWillToggle; }
+
+ CGLContextObj GetCGLContextObj();
+
+ std::vector<std::string> GetConnectedOutputs() override;
+
+ // winevents override
+ bool MessagePump() override;
+
+ NSRect GetWindowDimensions();
+ void enableInputEvents();
+ void disableInputEvents();
+
+protected:
+ std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override;
+
+ void GetScreenResolution(int* w, int* h, double* fps, int screenIdx);
+ void EnableVSync(bool enable);
+ bool SwitchToVideoMode(int width, int height, double refreshrate);
+ void FillInVideoModes();
+ bool FlushBuffer();
+ bool IsObscured();
+
+ bool DestroyWindowInternal();
+
+ std::unique_ptr<CWinEventsOSX> m_winEvents;
+
+ std::string m_name;
+ bool m_obscured;
+ NSWindow* m_appWindow;
+ OSXGLView* m_glView;
+ bool m_movedToOtherScreen;
+ int m_lastDisplayNr;
+ double m_refreshRate;
+
+ CCriticalSection m_resourceSection;
+ std::vector<IDispResource*> m_resources;
+ CTimer m_lostDeviceTimer;
+ bool m_delayDispReset;
+ XbmcThreads::EndTime<> m_dispResetTimer;
+ bool m_fullscreenWillToggle;
+ CCriticalSection m_critSection;
+};
diff --git a/xbmc/windowing/osx/WinSystemOSX.mm b/xbmc/windowing/osx/WinSystemOSX.mm
new file mode 100644
index 0000000..85dd4b7
--- /dev/null
+++ b/xbmc/windowing/osx/WinSystemOSX.mm
@@ -0,0 +1,1305 @@
+/*
+ * 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 "WinSystemOSX.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Sinks/AESinkDARWINOSX.h"
+#include "cores/RetroPlayer/process/osx/RPProcessInfoOSX.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/VTB.h"
+#include "cores/VideoPlayer/Process/osx/ProcessInfoOSX.h"
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGL.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "guilib/DispResource.h"
+#include "guilib/GUIWindowManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "rendering/gl/ScreenshotSurfaceGL.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/CriticalSection.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "windowing/osx/CocoaDPMSSupport.h"
+#include "windowing/osx/OSScreenSaverOSX.h"
+#import "windowing/osx/OpenGL/OSXGLView.h"
+#import "windowing/osx/OpenGL/OSXGLWindow.h"
+#include "windowing/osx/VideoSyncOsx.h"
+#include "windowing/osx/WinEventsOSX.h"
+
+#include "platform/darwin/DarwinUtils.h"
+#include "platform/darwin/DictionaryUtils.h"
+#include "platform/darwin/osx/CocoaInterface.h"
+#include "platform/darwin/osx/powermanagement/CocoaPowerSyscall.h"
+
+#include <chrono>
+#include <cstdlib>
+#include <mutex>
+#include <signal.h>
+
+#import <Cocoa/Cocoa.h>
+#import <Foundation/Foundation.h>
+#import <IOKit/graphics/IOGraphicsLib.h>
+#import <IOKit/pwr_mgt/IOPMLib.h>
+#import <QuartzCore/QuartzCore.h>
+
+using namespace KODI;
+using namespace MESSAGING;
+using namespace WINDOWING;
+using namespace std::chrono_literals;
+
+#define MAX_DISPLAYS 32
+static NSWindow* blankingWindows[MAX_DISPLAYS];
+
+size_t DisplayBitsPerPixelForMode(CGDisplayModeRef mode)
+{
+ size_t bitsPerPixel = 0;
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+ // No replacement for CGDisplayModeCopyPixelEncoding
+ // Disable warning for now
+ CFStringRef pixEnc = CGDisplayModeCopyPixelEncoding(mode);
+#pragma GCC diagnostic pop
+
+ if (CFStringCompare(pixEnc, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive) ==
+ kCFCompareEqualTo)
+ {
+ bitsPerPixel = 32;
+ }
+ else if (CFStringCompare(pixEnc, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive) ==
+ kCFCompareEqualTo)
+ {
+ bitsPerPixel = 16;
+ }
+ else if (CFStringCompare(pixEnc, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive) ==
+ kCFCompareEqualTo)
+ {
+ bitsPerPixel = 8;
+ }
+
+ CFRelease(pixEnc);
+
+ return bitsPerPixel;
+}
+
+#pragma mark - GetScreenName
+
+NSString* screenNameForDisplay(CGDirectDisplayID displayID)
+{
+ NSString* screenName;
+ @autoreleasepool
+ {
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+ // No real replacement of CGDisplayIOServicePort
+ // Stackoverflow links to https://github.com/glfw/glfw/pull/192 as a possible replacement
+ // disable warning for now
+ NSDictionary* deviceInfo = (__bridge_transfer NSDictionary*)IODisplayCreateInfoDictionary(
+ CGDisplayIOServicePort(displayID), kIODisplayOnlyPreferredName);
+
+#pragma GCC diagnostic pop
+
+ NSDictionary* localizedNames =
+ [deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]];
+
+ if ([localizedNames count] > 0)
+ {
+ screenName = [localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]];
+ }
+ }
+
+ if (screenName == nil)
+ {
+ screenName = [[NSString alloc] initWithFormat:@"%i", displayID];
+ }
+ else
+ {
+ // ensure screen name is unique by appending displayid
+ screenName = [screenName stringByAppendingFormat:@" (%@)", [@(displayID) stringValue]];
+ }
+
+ return screenName;
+}
+
+#pragma mark - GetDisplay
+
+CGDirectDisplayID GetDisplayID(int screen_index)
+{
+ CGDirectDisplayID displayArray[MAX_DISPLAYS];
+ uint32_t numDisplays;
+
+ // Get the list of displays.
+ CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays);
+ if (screen_index >= 0 && screen_index < static_cast<int>(numDisplays))
+ return (displayArray[screen_index]);
+ else
+ return (displayArray[0]);
+}
+
+CGDirectDisplayID GetDisplayIDFromScreen(NSScreen* screen)
+{
+ NSDictionary* screenInfo = screen.deviceDescription;
+ NSNumber* screenID = [screenInfo objectForKey:@"NSScreenNumber"];
+
+ return (CGDirectDisplayID)[screenID longValue];
+}
+
+int GetDisplayIndex(CGDirectDisplayID display)
+{
+ CGDirectDisplayID displayArray[MAX_DISPLAYS];
+ CGDisplayCount numDisplays;
+
+ // Get the list of displays.
+ CGGetActiveDisplayList(MAX_DISPLAYS, displayArray, &numDisplays);
+ while (numDisplays > 0)
+ {
+ if (display == displayArray[--numDisplays])
+ return numDisplays;
+ }
+ return -1;
+}
+
+int GetDisplayIndex(const std::string& dispName)
+{
+ int ret = 0;
+
+ // Add full screen settings for additional monitors
+ int numDisplays = NSScreen.screens.count;
+
+ for (int disp = 0; disp < numDisplays; disp++)
+ {
+ NSString* name = screenNameForDisplay(GetDisplayID(disp));
+ if (name.UTF8String == dispName)
+ {
+ ret = disp;
+ break;
+ }
+ }
+
+ return ret;
+}
+
+#pragma mark - Display Modes
+
+CFArrayRef GetAllDisplayModes(CGDirectDisplayID display)
+{
+ int value = 1;
+
+ CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value);
+ if (!number)
+ {
+ CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Number!");
+ return nullptr;
+ }
+
+ CFStringRef key = kCGDisplayShowDuplicateLowResolutionModes;
+ CFDictionaryRef options = CFDictionaryCreate(kCFAllocatorDefault, (const void**)&key,
+ (const void**)&number, 1, nullptr, nullptr);
+ CFRelease(number);
+
+ if (!options)
+ {
+ CLog::Log(LOGERROR, "GetAllDisplayModes - could not create Dictionary!");
+ return nullptr;
+ }
+
+ CFArrayRef displayModes = CGDisplayCopyAllDisplayModes(display, options);
+ CFRelease(options);
+
+ if (!displayModes)
+ {
+ CLog::Log(LOGERROR, "GetAllDisplayModes - no displaymodes found!");
+ return nullptr;
+ }
+
+ return displayModes;
+}
+
+// try to find mode that matches the desired size, refreshrate
+// non interlaced, nonstretched, safe for hardware
+CGDisplayModeRef GetMode(int width, int height, double refreshrate, int screenIdx)
+{
+ if (screenIdx >= (signed)[[NSScreen screens] count])
+ return nullptr;
+
+ bool stretched;
+ bool interlaced;
+ bool safeForHardware;
+ int w, h, bitsperpixel;
+ double rate;
+ RESOLUTION_INFO res;
+
+ CLog::Log(LOGDEBUG, "GetMode looking for suitable mode with {} x {} @ {} Hz on display {}", width,
+ height, refreshrate, screenIdx);
+
+ CFArrayRef allModes = GetAllDisplayModes(GetDisplayID(screenIdx));
+
+ if (!allModes)
+ return nullptr;
+
+ for (int i = 0; i < CFArrayGetCount(allModes); ++i)
+ {
+ CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i);
+ uint32_t flags = CGDisplayModeGetIOFlags(displayMode);
+ stretched = (flags & kDisplayModeStretchedFlag) != 0;
+ interlaced = (flags & kDisplayModeInterlacedFlag) != 0;
+ bitsperpixel = DisplayBitsPerPixelForMode(displayMode);
+ safeForHardware = (flags & kDisplayModeSafetyFlags) != 0;
+ w = CGDisplayModeGetWidth(displayMode);
+ h = CGDisplayModeGetHeight(displayMode);
+ rate = CGDisplayModeGetRefreshRate(displayMode);
+
+ if ((bitsperpixel == 32) && (safeForHardware == true) && (stretched == false) &&
+ (interlaced == false) && (w == width) && (h == height) &&
+ (rate == refreshrate || rate == 0))
+ {
+ CFRelease(allModes);
+ CLog::Log(LOGDEBUG, "GetMode found a match!");
+ return CGDisplayModeRetain(displayMode);
+ }
+ }
+
+ CFRelease(allModes);
+ CLog::Log(LOGERROR, "GetMode - no match found!");
+ return nullptr;
+}
+
+// mimic former behavior of deprecated CGDisplayBestModeForParameters
+CGDisplayModeRef BestMatchForMode(CGDirectDisplayID display,
+ size_t bitsPerPixel,
+ size_t width,
+ size_t height)
+{
+ // Loop through all display modes to determine the closest match.
+ // CGDisplayBestModeForParameters is deprecated on 10.6 so we will emulate it's behavior
+ // Try to find a mode with the requested depth and equal or greater dimensions first.
+ // If no match is found, try to find a mode with greater depth and same or greater dimensions.
+ // If still no match is found, just use the current mode.
+ CFArrayRef allModes = GetAllDisplayModes(display);
+
+ if (!allModes)
+ return nullptr;
+
+ CGDisplayModeRef displayMode = nullptr;
+
+ for (int i = 0; i < CFArrayGetCount(allModes); i++)
+ {
+ CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i);
+
+ if (!mode)
+ continue;
+
+ if (DisplayBitsPerPixelForMode(mode) != bitsPerPixel)
+ continue;
+
+ if ((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height))
+ {
+ displayMode = mode;
+ break;
+ }
+ }
+
+ // No depth match was found
+ if (!displayMode)
+ {
+ for (int i = 0; i < CFArrayGetCount(allModes); i++)
+ {
+ CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i);
+
+ if (!mode)
+ continue;
+
+ if (DisplayBitsPerPixelForMode(mode) >= bitsPerPixel)
+ continue;
+
+ if ((CGDisplayModeGetWidth(mode) == width) && (CGDisplayModeGetHeight(mode) == height))
+ {
+ displayMode = mode;
+ break;
+ }
+ }
+ }
+
+ CFRelease(allModes);
+
+ return displayMode;
+}
+
+#pragma mark - Blank Displays
+
+void BlankOtherDisplays(int screen_index)
+{
+ int i;
+ int numDisplays = [[NSScreen screens] count];
+
+ // zero out blankingWindows for debugging
+ for (i = 0; i < MAX_DISPLAYS; i++)
+ {
+ blankingWindows[i] = 0;
+ }
+
+ // Blank.
+ for (i = 0; i < numDisplays; i++)
+ {
+ if (i != screen_index)
+ {
+ // Get the size.
+ NSScreen* pScreen = [NSScreen.screens objectAtIndex:i];
+ NSRect screenRect = pScreen.frame;
+
+ // Build a blanking window.
+ screenRect.origin = NSZeroPoint;
+ blankingWindows[i] = [[NSWindow alloc] initWithContentRect:screenRect
+ styleMask:NSWindowStyleMaskBorderless
+ backing:NSBackingStoreBuffered
+ defer:NO
+ screen:pScreen];
+
+ [blankingWindows[i] setBackgroundColor:NSColor.blackColor];
+ [blankingWindows[i] setLevel:CGShieldingWindowLevel()];
+ [blankingWindows[i] makeKeyAndOrderFront:nil];
+ }
+ }
+}
+
+void UnblankDisplays(void)
+{
+ for (auto i = 0; i < static_cast<int>(NSScreen.screens.count); i++)
+ {
+ if (blankingWindows[i] != 0)
+ {
+ // Get rid of the blanking windows we created.
+ [blankingWindows[i] close];
+ blankingWindows[i] = 0;
+ }
+ }
+}
+
+#pragma mark - Fade Display
+//! @Todo Look to replace Fade with CABasicAnimation
+static NSWindow* curtainWindow;
+void fadeInDisplay(NSScreen* theScreen, double fadeTime)
+{
+ int fadeSteps = 100;
+ double fadeInterval = (fadeTime / (double)fadeSteps);
+
+ if (curtainWindow != nil)
+ {
+ for (int step = 0; step < fadeSteps; step++)
+ {
+ double fade = 1.0 - (step * fadeInterval);
+ [curtainWindow setAlphaValue:fade];
+
+ NSDate* nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval];
+ [NSRunLoop.currentRunLoop runUntilDate:nextDate];
+ }
+ }
+ [curtainWindow close];
+ curtainWindow = nil;
+}
+
+void fadeOutDisplay(NSScreen* theScreen, double fadeTime)
+{
+ int fadeSteps = 100;
+ double fadeInterval = (fadeTime / (double)fadeSteps);
+
+ [NSCursor hide];
+
+ curtainWindow = [[NSWindow alloc] initWithContentRect:[theScreen frame]
+ styleMask:NSWindowStyleMaskBorderless
+ backing:NSBackingStoreBuffered
+ defer:YES
+ screen:theScreen];
+
+ [curtainWindow setAlphaValue:0.0];
+ [curtainWindow setBackgroundColor:NSColor.blackColor];
+ [curtainWindow setLevel:NSScreenSaverWindowLevel];
+
+ [curtainWindow makeKeyAndOrderFront:nil];
+ [curtainWindow setFrame:[curtainWindow frameRectForContentRect:[theScreen frame]]
+ display:YES
+ animate:NO];
+
+ for (int step = 0; step < fadeSteps; step++)
+ {
+ double fade = step * fadeInterval;
+ [curtainWindow setAlphaValue:fade];
+
+ NSDate* nextDate = [NSDate dateWithTimeIntervalSinceNow:fadeInterval];
+ [NSRunLoop.currentRunLoop runUntilDate:nextDate];
+ }
+}
+
+//---------------------------------------------------------------------------------
+static void DisplayReconfigured(CGDirectDisplayID display,
+ CGDisplayChangeSummaryFlags flags,
+ void* userData)
+{
+ CWinSystemOSX* winsys = (CWinSystemOSX*)userData;
+ if (!winsys)
+ return;
+
+ CLog::Log(LOGDEBUG, "CWinSystemOSX::DisplayReconfigured with flags {}", flags);
+
+ // we fire the callbacks on start of configuration
+ // or when the mode set was finished
+ // or when we are called with flags == 0 (which is undocumented but seems to happen
+ // on some macs - we treat it as device reset)
+
+ // first check if we need to call OnLostDevice
+ if (flags & kCGDisplayBeginConfigurationFlag)
+ {
+ // pre/post-reconfiguration changes
+ RESOLUTION res = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution();
+ if (res == RES_INVALID)
+ return;
+
+ NSScreen* pScreen = nil;
+ unsigned int screenIdx = 0;
+
+ if (screenIdx < NSScreen.screens.count)
+ {
+ pScreen = [NSScreen.screens objectAtIndex:screenIdx];
+ }
+
+ // kCGDisplayBeginConfigurationFlag is only fired while the screen is still
+ // valid
+ if (pScreen)
+ {
+ CGDirectDisplayID xbmc_display = GetDisplayIDFromScreen(pScreen);
+ if (xbmc_display == display)
+ {
+ // we only respond to changes on the display we are running on.
+ winsys->AnnounceOnLostDevice();
+ winsys->StartLostDeviceTimer();
+ }
+ }
+ }
+ else // the else case checks if we need to call OnResetDevice
+ {
+ // we fire if kCGDisplaySetModeFlag is set or if flags == 0
+ // (which is undocumented but seems to happen
+ // on some macs - we treat it as device reset)
+ // we also don't check the screen here as we might not even have
+ // one anymore (e.x. when tv is turned off)
+ if (flags & kCGDisplaySetModeFlag || flags == 0)
+ {
+ winsys->StopLostDeviceTimer(); // no need to timeout - we've got the callback
+ winsys->HandleOnResetDevice();
+ }
+ }
+}
+
+#pragma mark - CWinSystemOSX
+//------------------------------------------------------------------------------
+CWinSystemOSX::CWinSystemOSX() : CWinSystemBase(), m_lostDeviceTimer(this)
+{
+ m_appWindow = nullptr;
+ m_glView = nullptr;
+ m_obscured = false;
+ m_lastDisplayNr = -1;
+ m_movedToOtherScreen = false;
+ m_refreshRate = 0.0;
+ m_delayDispReset = false;
+
+ m_winEvents.reset(new CWinEventsOSX());
+
+ AE::CAESinkFactory::ClearSinks();
+ CAESinkDARWINOSX::Register();
+ CCocoaPowerSyscall::Register();
+ m_dpms = std::make_shared<CCocoaDPMSSupport>();
+}
+
+CWinSystemOSX::~CWinSystemOSX() = default;
+
+void CWinSystemOSX::Register(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ m_resources.push_back(resource);
+}
+
+void CWinSystemOSX::Unregister(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> 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 CWinSystemOSX::AnnounceOnLostDevice()
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ // tell any shared resources
+ CLog::LogF(LOGDEBUG, "Lost Device Announce");
+ for (std::vector<IDispResource*>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnLostDisplay();
+}
+
+void CWinSystemOSX::HandleOnResetDevice()
+{
+ auto delay =
+ std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ "videoscreen.delayrefreshchange") *
+ 100);
+ if (delay > 0ms)
+ {
+ m_delayDispReset = true;
+ m_dispResetTimer.Set(delay);
+ }
+ else
+ {
+ AnnounceOnResetDevice();
+ }
+}
+
+void CWinSystemOSX::AnnounceOnResetDevice()
+{
+ double currentFps = m_refreshRate;
+ int w = 0;
+ int h = 0;
+ int currentScreenIdx = m_lastDisplayNr;
+ // ensure that graphics context knows about the current refreshrate before
+ // doing the callbacks
+ GetScreenResolution(&w, &h, &currentFps, currentScreenIdx);
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetFPS(currentFps);
+
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ // tell any shared resources
+ CLog::LogF(LOGDEBUG, "Reset Device Announce");
+ for (std::vector<IDispResource*>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnResetDisplay();
+}
+
+#pragma mark - Timers
+
+void CWinSystemOSX::StartLostDeviceTimer()
+{
+ if (m_lostDeviceTimer.IsRunning())
+ m_lostDeviceTimer.Restart();
+ else
+ m_lostDeviceTimer.Start(3000ms, false);
+}
+
+void CWinSystemOSX::StopLostDeviceTimer()
+{
+ m_lostDeviceTimer.Stop();
+}
+
+void CWinSystemOSX::OnTimeout()
+{
+ HandleOnResetDevice();
+}
+
+#pragma mark - WindowSystem
+
+bool CWinSystemOSX::InitWindowSystem()
+{
+ if (!CWinSystemBase::InitWindowSystem())
+ return false;
+
+ CGDisplayRegisterReconfigurationCallback(DisplayReconfigured, (void*)this);
+
+ return true;
+}
+
+bool CWinSystemOSX::DestroyWindowSystem()
+{
+ CGDisplayRemoveReconfigurationCallback(DisplayReconfigured, (void*)this);
+
+ DestroyWindowInternal();
+
+ if (m_glView)
+ {
+ m_glView = nullptr;
+ }
+
+ UnblankDisplays();
+ return true;
+}
+
+bool CWinSystemOSX::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res)
+{
+ // force initial window creation to be windowed, if fullscreen, it will switch to it below
+ // fixes the white screen of death if starting fullscreen and switching to windowed.
+ RESOLUTION_INFO resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW);
+ m_nWidth = resInfo.iWidth;
+ m_nHeight = resInfo.iHeight;
+ m_bFullScreen = false;
+ m_name = name;
+
+ __block NSWindow* appWindow;
+ // because we are not main thread, delay any updates
+ // and only become keyWindow after it finishes.
+ [NSAnimationContext beginGrouping];
+ [NSAnimationContext.currentContext setCompletionHandler:^{
+ [appWindow makeKeyWindow];
+ }];
+
+ const NSUInteger windowStyleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable |
+ NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
+
+ if (m_appWindow == nullptr)
+ {
+ // create new content view
+ NSRect rect = [appWindow contentRectForFrameRect:appWindow.frame];
+
+ // create new view if we don't have one
+ if (!m_glView)
+ m_glView = [[OSXGLView alloc] initWithFrame:rect];
+
+ OSXGLView* view = (OSXGLView*)m_glView;
+
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ appWindow = [[OSXGLWindow alloc] initWithContentRect:NSMakeRect(0, 0, m_nWidth, m_nHeight)
+ styleMask:windowStyleMask];
+ NSString* title = [NSString stringWithUTF8String:m_name.c_str()];
+ appWindow.backgroundColor = NSColor.blackColor;
+ appWindow.title = title;
+
+ NSWindowCollectionBehavior behavior = appWindow.collectionBehavior;
+ behavior |= NSWindowCollectionBehaviorFullScreenPrimary;
+ [appWindow setCollectionBehavior:behavior];
+
+ // associate with current window
+ [appWindow setContentView:view];
+ });
+
+ [view.getGLContext makeCurrentContext];
+ [view.getGLContext update];
+
+ m_appWindow = appWindow;
+ m_bWindowCreated = true;
+ }
+
+ // warning, we can order front but not become
+ // key window or risk starting up with bad flicker
+ // becoming key window must happen in completion block.
+ [(NSWindow*)m_appWindow performSelectorOnMainThread:@selector(orderFront:)
+ withObject:nil
+ waitUntilDone:YES];
+
+ [NSAnimationContext endGrouping];
+
+ // get screen refreshrate - this is needed
+ // when we startup in windowed mode and don't run through SetFullScreen
+ int dummy;
+ GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr);
+
+ // register platform dependent objects
+ CDVDFactoryCodec::ClearHWAccels();
+ VTB::CDecoder::Register();
+ VIDEOPLAYER::CRendererFactory::ClearRenderer();
+ CLinuxRendererGL::Register();
+ CRendererVTB::Register();
+ VIDEOPLAYER::CProcessInfoOSX::Register();
+ RETRO::CRPProcessInfoOSX::Register();
+ RETRO::CRPProcessInfoOSX::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL);
+ CScreenshotSurfaceGL::Register();
+
+ return true;
+}
+
+bool CWinSystemOSX::DestroyWindowInternal()
+{
+ // set this 1st, we should really mutex protext m_appWindow in this class
+ m_bWindowCreated = false;
+ if (m_appWindow)
+ {
+ NSWindow* oldAppWindow = m_appWindow;
+ m_appWindow = nullptr;
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ [oldAppWindow setContentView:nil];
+ });
+ }
+
+ return true;
+}
+
+bool CWinSystemOSX::DestroyWindow()
+{
+ return true;
+}
+
+bool CWinSystemOSX::Minimize()
+{
+ @autoreleasepool
+ {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ [NSApplication.sharedApplication miniaturizeAll:nil];
+ });
+ }
+ return true;
+}
+
+bool CWinSystemOSX::Restore()
+{
+ @autoreleasepool
+ {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ [NSApplication.sharedApplication unhide:nil];
+ });
+ }
+ return true;
+}
+
+bool CWinSystemOSX::Show(bool raise)
+{
+ @autoreleasepool
+ {
+ auto app = NSApplication.sharedApplication;
+ if (raise)
+ {
+ [app unhide:nil];
+ [app activateIgnoringOtherApps:YES];
+ [app arrangeInFront:nil];
+ }
+ else
+ {
+ [app unhideWithoutActivation];
+ }
+ }
+ return true;
+}
+
+bool CWinSystemOSX::Hide()
+{
+ @autoreleasepool
+ {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ [NSApplication.sharedApplication hide:nil];
+ });
+ }
+ return true;
+}
+
+NSRect CWinSystemOSX::GetWindowDimensions()
+{
+ if (m_appWindow)
+ {
+ NSWindow* win = (NSWindow*)m_appWindow;
+ NSRect frame = win.contentView.frame;
+ return frame;
+ }
+}
+
+#pragma mark - Resize Window
+
+bool CWinSystemOSX::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ if (!m_appWindow)
+ return false;
+
+ [(OSXGLWindow*)m_appWindow setResizeState:true];
+
+ __block OSXGLView* view;
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ view = m_appWindow.contentView;
+ });
+
+ if (view)
+ {
+ // It seems, that in macOS 10.15 this defaults to YES, but we currently do not support
+ // Retina resolutions properly. Ensure that the view uses a 1 pixel per point framebuffer.
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ view.wantsBestResolutionOpenGLSurface = NO;
+ });
+ }
+
+ if (newWidth < 0)
+ {
+ newWidth = [(NSWindow*)m_appWindow minSize].width;
+ }
+
+ if (newHeight < 0)
+ {
+ newHeight = [(NSWindow*)m_appWindow minSize].height;
+ }
+
+ if (view)
+ {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ NSOpenGLContext* context = [view getGLContext];
+ NSWindow* window = m_appWindow;
+
+ NSRect pos = window.frame;
+
+ NSRect myNewContentFrame = NSMakeRect(pos.origin.x, pos.origin.y, newWidth, newHeight);
+ NSRect myNewWindowRect = [window frameRectForContentRect:myNewContentFrame];
+ [window setFrame:myNewWindowRect display:TRUE];
+
+ [context update];
+ });
+ }
+
+ m_nWidth = newWidth;
+ m_nHeight = newHeight;
+
+ [(OSXGLWindow*)m_appWindow setResizeState:false];
+
+ return true;
+}
+
+bool CWinSystemOSX::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ std::unique_lock<CCriticalSection> lock(m_critSection);
+
+ // if (m_lastDisplayNr == -1)
+ // m_lastDisplayNr = res.iScreen;
+
+ __block NSWindow* window = m_appWindow;
+
+ const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ m_lastDisplayNr = GetDisplayIndex(settings->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR));
+ m_nWidth = res.iWidth;
+ m_nHeight = res.iHeight;
+ m_bFullScreen = fullScreen;
+
+ //handle resolution/refreshrate switching early here
+ if (m_bFullScreen)
+ {
+ // switch videomode
+ SwitchToVideoMode(res.iWidth, res.iHeight, static_cast<double>(res.fRefreshRate));
+ // hide the OS mouse
+ [NSCursor hide];
+ }
+
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ [window setAllowsConcurrentViewDrawing:NO];
+ });
+
+ if (m_fullscreenWillToggle)
+ {
+ ResizeWindow(m_nWidth, m_nHeight, -1, -1);
+ m_fullscreenWillToggle = false;
+ return true;
+ }
+
+ if (m_bFullScreen)
+ {
+ // This is Cocoa Windowed FullScreen Mode
+ // Get the screen rect of our current display
+ NSScreen* pScreen = [NSScreen.screens objectAtIndex:m_lastDisplayNr];
+
+ // remove frame origin offset of original display
+ pScreen.frame.origin = NSZeroPoint;
+
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ [window.contentView setFrameSize:NSMakeSize(m_nWidth, m_nHeight)];
+ window.title = @"";
+ [window setAllowsConcurrentViewDrawing:YES];
+ });
+
+ // Blank other displays if requested.
+ if (blankOtherDisplays)
+ BlankOtherDisplays(m_lastDisplayNr);
+ }
+ else
+ {
+ // Show menubar.
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ [NSApplication.sharedApplication setPresentationOptions:NSApplicationPresentationDefault];
+ });
+
+ // Unblank.
+ // Force the unblank when returning from fullscreen, we get called with blankOtherDisplays set false.
+ //if (blankOtherDisplays)
+ UnblankDisplays();
+ }
+
+ //DisplayFadeFromBlack(fade_token, needtoshowme);
+
+ m_fullscreenWillToggle = true;
+ // toggle cocoa fullscreen mode
+ if ([m_appWindow respondsToSelector:@selector(toggleFullScreen:)])
+ [m_appWindow performSelectorOnMainThread:@selector(toggleFullScreen:)
+ withObject:nil
+ waitUntilDone:YES];
+
+ ResizeWindow(m_nWidth, m_nHeight, -1, -1);
+
+ return true;
+}
+
+#pragma mark - Resolution
+
+void CWinSystemOSX::UpdateResolutions()
+{
+ CWinSystemBase::UpdateResolutions();
+
+ // Add desktop resolution
+ int w;
+ int h;
+ double fps;
+
+ int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_VIDEOSCREEN_MONITOR));
+ GetScreenResolution(&w, &h, &fps, dispIdx);
+ NSString* dispName = screenNameForDisplay(GetDisplayID(dispIdx));
+ UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP),
+ dispName.UTF8String, 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();
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+}
+
+void CWinSystemOSX::GetScreenResolution(int* w, int* h, double* fps, int screenIdx)
+{
+ CGDirectDisplayID display_id = (CGDirectDisplayID)GetDisplayID(screenIdx);
+ CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display_id);
+ *w = CGDisplayModeGetWidth(mode);
+ *h = CGDisplayModeGetHeight(mode);
+ *fps = CGDisplayModeGetRefreshRate(mode);
+ CGDisplayModeRelease(mode);
+ if (static_cast<int>(*fps) == 0)
+ {
+ // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays.
+ *fps = 60.0;
+ }
+}
+
+#pragma mark - Video Modes
+
+bool CWinSystemOSX::SwitchToVideoMode(int width, int height, double refreshrate)
+{
+ CGDisplayModeRef dispMode = nullptr;
+
+ int screenIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_VIDEOSCREEN_MONITOR));
+
+ // Figure out the screen size. (default to main screen)
+ CGDirectDisplayID display_id = GetDisplayID(screenIdx);
+
+ // find mode that matches the desired size, refreshrate
+ // non interlaced, nonstretched, safe for hardware
+ dispMode = GetMode(width, height, refreshrate, screenIdx);
+
+ //not found - fallback to bestemdeforparameters
+ if (!dispMode)
+ {
+ dispMode = BestMatchForMode(display_id, 32, width, height);
+
+ if (!dispMode)
+ {
+ dispMode = BestMatchForMode(display_id, 16, width, height);
+
+ // still no match? fallback to current resolution of the display which HAS to work [tm]
+ if (!dispMode)
+ {
+ int currentWidth;
+ int currentHeight;
+ double currentRefresh;
+
+ GetScreenResolution(&currentWidth, &currentHeight, &currentRefresh, screenIdx);
+ dispMode = GetMode(currentWidth, currentHeight, currentRefresh, screenIdx);
+
+ // no way to get a resolution set
+ if (!dispMode)
+ return false;
+ }
+ }
+ }
+ // switch mode and return success
+ CGDisplayCapture(display_id);
+ CGDisplayConfigRef cfg;
+ CGBeginDisplayConfiguration(&cfg);
+ CGConfigureDisplayWithDisplayMode(cfg, display_id, dispMode, nullptr);
+ CGError err = CGCompleteDisplayConfiguration(cfg, kCGConfigureForAppOnly);
+ CGDisplayRelease(display_id);
+
+ m_refreshRate = CGDisplayModeGetRefreshRate(dispMode);
+
+ Cocoa_CVDisplayLinkUpdate();
+
+ return (err == kCGErrorSuccess);
+}
+
+void CWinSystemOSX::FillInVideoModes()
+{
+ int dispIdx = GetDisplayIndex(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_VIDEOSCREEN_MONITOR));
+
+ for (int disp = 0; disp < static_cast<int>(NSScreen.screens.count); disp++)
+ {
+ bool stretched;
+ bool interlaced;
+ bool safeForHardware;
+ int w, h, bitsperpixel;
+ double refreshrate;
+ RESOLUTION_INFO res;
+
+ CFArrayRef displayModes = GetAllDisplayModes(GetDisplayID(disp));
+ NSString* dispName = screenNameForDisplay(GetDisplayID(disp));
+
+ CLog::LogF(LOGINFO, "Display {} has name {}", disp, [dispName UTF8String]);
+
+ if (!displayModes)
+ continue;
+
+ for (int i = 0; i < CFArrayGetCount(displayModes); ++i)
+ {
+ CGDisplayModeRef displayMode = (CGDisplayModeRef)CFArrayGetValueAtIndex(displayModes, i);
+
+ uint32_t flags = CGDisplayModeGetIOFlags(displayMode);
+ stretched = (flags & kDisplayModeStretchedFlag) != 0;
+ interlaced = (flags & kDisplayModeInterlacedFlag) != 0;
+ bitsperpixel = DisplayBitsPerPixelForMode(displayMode);
+ safeForHardware = (flags & kDisplayModeSafetyFlags) != 0;
+
+ if ((bitsperpixel == 32) && (safeForHardware == true) && (stretched == false) &&
+ (interlaced == false))
+ {
+ w = CGDisplayModeGetWidth(displayMode);
+ h = CGDisplayModeGetHeight(displayMode);
+ refreshrate = CGDisplayModeGetRefreshRate(displayMode);
+ if (static_cast<int>(refreshrate) == 0) // LCD display?
+ {
+ // NOTE: The refresh rate will be REPORTED AS 0 for many DVI and notebook displays.
+ refreshrate = 60.0;
+ }
+ CLog::Log(LOGINFO, "Found possible resolution for display {} with {} x {} @ {} Hz", disp, w,
+ h, refreshrate);
+
+ // only add the resolution if it belongs to "our" screen
+ // all others are only logged above...
+ if (disp == dispIdx)
+ {
+ UpdateDesktopResolution(res, (dispName != nil) ? [dispName UTF8String] : "Unknown", w, h,
+ refreshrate, 0);
+ CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res);
+ CDisplaySettings::GetInstance().AddResolutionInfo(res);
+ }
+ }
+ }
+ CFRelease(displayModes);
+ }
+}
+
+#pragma mark - Occlusion
+
+bool CWinSystemOSX::IsObscured()
+{
+ if (m_obscured)
+ CLog::LogF(LOGDEBUG, "Obscured");
+ return m_obscured;
+}
+
+void CWinSystemOSX::SetOcclusionState(bool occluded)
+{
+ // m_obscured = occluded;
+ // CLog::LogF(LOGDEBUG, "{}", occluded ? "true":"false");
+}
+
+void CWinSystemOSX::NotifyAppFocusChange(bool bGaining)
+{
+ if (!(m_bFullScreen && bGaining))
+ return;
+ @autoreleasepool
+ {
+ // find the window
+ NSOpenGLContext* context = NSOpenGLContext.currentContext;
+ if (context)
+ {
+ NSView* view;
+
+ view = context.view;
+ if (view)
+ {
+ NSWindow* window;
+ window = view.window;
+ if (window)
+ {
+ [window orderFront:nil];
+ }
+ }
+ }
+ }
+}
+
+#pragma mark - Window Move
+
+void CWinSystemOSX::OnMove(int x, int y)
+{
+ static double oldRefreshRate = m_refreshRate;
+ Cocoa_CVDisplayLinkUpdate();
+
+ int dummy = 0;
+ GetScreenResolution(&dummy, &dummy, &m_refreshRate, m_lastDisplayNr);
+
+ if (oldRefreshRate != m_refreshRate)
+ {
+ oldRefreshRate = m_refreshRate;
+
+ // send a message so that videoresolution (and refreshrate) is changed
+ NSWindow* win = m_appWindow;
+ NSRect frame = win.contentView.frame;
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_VIDEORESIZE, frame.size.width,
+ frame.size.height);
+ }
+}
+
+void CWinSystemOSX::WindowChangedScreen()
+{
+ // user has moved the window to a
+ // different screen
+ NSOpenGLContext* context = [NSOpenGLContext currentContext];
+ m_lastDisplayNr = -1;
+
+ // if we are here the user dragged the window to a different
+ // screen and we return the screen of the window
+ if (context)
+ {
+ NSView* view;
+
+ view = context.view;
+ if (view)
+ {
+ NSWindow* window;
+ window = view.window;
+ if (window)
+ {
+ m_lastDisplayNr = GetDisplayIndex(GetDisplayIDFromScreen(window.screen));
+ }
+ }
+ }
+ if (m_lastDisplayNr == -1)
+ m_lastDisplayNr = 0; // default to main screen
+}
+
+CGLContextObj CWinSystemOSX::GetCGLContextObj()
+{
+ __block CGLContextObj cglcontex = nullptr;
+ if (m_appWindow)
+ {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ OSXGLView* contentView = m_appWindow.contentView;
+ cglcontex = contentView.getGLContext.CGLContextObj;
+ });
+ }
+
+ return cglcontex;
+}
+
+bool CWinSystemOSX::FlushBuffer()
+{
+ if (m_appWindow)
+ {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ OSXGLView* contentView = m_appWindow.contentView;
+ NSOpenGLContext* glcontex = contentView.getGLContext;
+ [glcontex flushBuffer];
+ });
+ }
+
+ return true;
+}
+
+#pragma mark - Vsync
+
+void CWinSystemOSX::EnableVSync(bool enable)
+{
+ // OpenGL Flush synchronised with vertical retrace
+ GLint swapInterval = enable ? 1 : 0;
+ [NSOpenGLContext.currentContext setValues:&swapInterval
+ forParameter:NSOpenGLContextParameterSwapInterval];
+}
+
+std::unique_ptr<CVideoSync> CWinSystemOSX::GetVideoSync(void* clock)
+{
+ return std::make_unique<CVideoSyncOsx>(clock);
+}
+
+std::vector<std::string> CWinSystemOSX::GetConnectedOutputs()
+{
+ std::vector<std::string> outputs;
+ outputs.push_back("Default");
+
+ int numDisplays = [[NSScreen screens] count];
+
+ for (int disp = 0; disp < numDisplays; disp++)
+ {
+ NSString* dispName = screenNameForDisplay(GetDisplayID(disp));
+ outputs.push_back(dispName.UTF8String);
+ }
+
+ return outputs;
+}
+
+#pragma mark - OSScreenSaver
+
+std::unique_ptr<IOSScreenSaver> CWinSystemOSX::GetOSScreenSaverImpl()
+{
+ return std::make_unique<COSScreenSaverOSX>();
+}
+
+#pragma mark - Input
+
+bool CWinSystemOSX::MessagePump()
+{
+ return m_winEvents->MessagePump();
+}
+
+void CWinSystemOSX::enableInputEvents()
+{
+ m_winEvents->enableInputEvents();
+}
+
+void CWinSystemOSX::disableInputEvents()
+{
+ m_winEvents->disableInputEvents();
+}
+
+std::string CWinSystemOSX::GetClipboardText()
+{
+ std::string utf8_text;
+
+ const char* szStr = Cocoa_Paste();
+ if (szStr)
+ utf8_text = szStr;
+
+ return utf8_text;
+}
+
+void CWinSystemOSX::ShowOSMouse(bool show)
+{
+}
+
+#pragma mark - Unused
+
+CGDisplayFadeReservationToken DisplayFadeToBlack(bool fade)
+{
+ // Fade to black to hide resolution-switching flicker and garbage.
+ CGDisplayFadeReservationToken fade_token = kCGDisplayFadeReservationInvalidToken;
+ if (CGAcquireDisplayFadeReservation(5, &fade_token) == kCGErrorSuccess && fade)
+ CGDisplayFade(fade_token, 0.3, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0,
+ TRUE);
+
+ return (fade_token);
+}
+
+void DisplayFadeFromBlack(CGDisplayFadeReservationToken fade_token, bool fade)
+{
+ if (fade_token != kCGDisplayFadeReservationInvalidToken)
+ {
+ if (fade)
+ CGDisplayFade(fade_token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0.0, 0.0,
+ 0.0, FALSE);
+ CGReleaseDisplayFadeReservation(fade_token);
+ }
+}
diff --git a/xbmc/windowing/tvos/CMakeLists.txt b/xbmc/windowing/tvos/CMakeLists.txt
new file mode 100644
index 0000000..d9aa9cc
--- /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 0000000..2b5d3bd
--- /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 0000000..7af5580
--- /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 0000000..c00aa60
--- /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 * static_cast<double>(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 0000000..de78226
--- /dev/null
+++ b/xbmc/windowing/tvos/VideoSyncTVos.h
@@ -0,0 +1,44 @@
+/*
+ * 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 0000000..fa30665
--- /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 0000000..67e9e54
--- /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 "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/InputManager.h"
+#include "input/XBMC_vkeys.h"
+#include "threads/CriticalSection.h"
+#include "utils/log.h"
+
+#include <list>
+#include <mutex>
+
+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)
+{
+ std::unique_lock<CCriticalSection> lock(m_eventsCond);
+
+ m_events.push_back(*newEvent);
+}
+
+size_t CWinEventsTVOS::GetQueueSize()
+{
+ std::unique_lock<CCriticalSection> 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;
+ {
+ std::unique_lock<CCriticalSection> 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 0000000..774b7e9
--- /dev/null
+++ b/xbmc/windowing/tvos/WinSystemTVOS.h
@@ -0,0 +1,107 @@
+/*
+ * 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();
+
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // 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;
+
+ std::vector<std::string> GetConnectedOutputs() override;
+
+ bool InitDisplayLink(CVideoSyncTVos* syncImpl);
+ void DeinitDisplayLink(void);
+ void OnAppFocusChange(bool focus);
+ bool IsBackgrounded() const { return m_bIsBackgrounded; }
+ CVEAGLContext GetEAGLContextObj();
+
+ // 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 0000000..bd47c6f
--- /dev/null
+++ b/xbmc/windowing/tvos/WinSystemTVOS.mm
@@ -0,0 +1,453 @@
+/*
+ * 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"
+
+#include "ServiceBroker.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 "utils/StringUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/OSScreenSaver.h"
+#include "windowing/WindowSystemFactory.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 <mutex>
+#include <vector>
+
+#import <Foundation/Foundation.h>
+#import <OpenGLES/ES2/gl.h>
+#import <OpenGLES/ES2/glext.h>
+#import <QuartzCore/CADisplayLink.h>
+
+using namespace std::chrono_literals;
+
+#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 auto LOST_DEVICE_TIMEOUT_MS{3000ms};
+
+// 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;
+};
+
+void CWinSystemTVOS::Register()
+{
+ KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem);
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemTVOS::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemTVOS>();
+}
+
+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()
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ // tell any shared resources
+ CLog::Log(LOGDEBUG, "CWinSystemTVOS::AnnounceOnLostDevice");
+ for (auto dispResource : m_resources)
+ dispResource->OnLostDisplay();
+}
+
+void CWinSystemTVOS::AnnounceOnResetDevice()
+{
+ std::unique_lock<CCriticalSection> 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);
+
+ // 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, static_cast<double>(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 shouldn't 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 = static_cast<double>([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(LOGINFO, "Found possible resolution for display {} with {} x {} RefreshRate:{} ",
+ 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)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ m_resources.push_back(resource);
+}
+
+void CWinSystemTVOS::Unregister(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> 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)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ m_bIsBackgrounded = !focus;
+ CLog::Log(LOGDEBUG, "CWinSystemTVOS::OnAppFocusChange: {}", 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())
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN);
+}
+
+bool CWinSystemTVOS::Minimize()
+{
+ m_bWasFullScreenBeforeMinimize =
+ CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot();
+ if (m_bWasFullScreenBeforeMinimize)
+ CServiceBroker::GetAppMessenger()->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];
+}
+
+std::vector<std::string> CWinSystemTVOS::GetConnectedOutputs()
+{
+ std::vector<std::string> outputs;
+ outputs.emplace_back("Default");
+ outputs.emplace_back(CONST_HDMI);
+
+ return outputs;
+}
+
+bool CWinSystemTVOS::MessagePump()
+{
+ return m_winEvents->MessagePump();
+}
diff --git a/xbmc/windowing/wayland/CMakeLists.txt b/xbmc/windowing/wayland/CMakeLists.txt
new file mode 100644
index 0000000..5628ed8
--- /dev/null
+++ b/xbmc/windowing/wayland/CMakeLists.txt
@@ -0,0 +1,67 @@
+# from ArchSetup.cmake
+set_source_files_properties(${WAYLAND_EXTRA_PROTOCOL_GENERATED_DIR}/wayland-extra-protocols.cpp
+ ${WAYLAND_EXTRA_PROTOCOL_GENERATED_DIR}/wayland-extra-protocols.hpp
+ PROPERTIES GENERATED TRUE)
+
+set(SOURCES Connection.cpp
+ OptionalsReg.cpp
+ Output.cpp
+ InputProcessorKeyboard.h
+ InputProcessorPointer.h
+ InputProcessorTouch.h
+ OSScreenSaverIdleInhibitUnstableV1.cpp
+ Registry.cpp
+ Seat.cpp
+ SeatInputProcessing.cpp
+ SeatSelection.cpp
+ ShellSurface.cpp
+ ShellSurfaceWlShell.cpp
+ ShellSurfaceXdgShell.cpp
+ ShellSurfaceXdgShellUnstableV6.cpp
+ Util.cpp
+ VideoSyncWpPresentation.cpp
+ ${WAYLAND_EXTRA_PROTOCOL_GENERATED_DIR}/wayland-extra-protocols.cpp
+ WindowDecorator.cpp
+ WinEventsWayland.cpp
+ WinSystemWayland.cpp
+ XkbcommonKeymap.cpp)
+
+set(HEADERS Connection.h
+ OptionalsReg.h
+ Output.h
+ InputProcessorKeyboard.cpp
+ InputProcessorPointer.cpp
+ InputProcessorTouch.cpp
+ OSScreenSaverIdleInhibitUnstableV1.h
+ Registry.h
+ Seat.h
+ SeatInputProcessing.h
+ SeatSelection.h
+ ShellSurface.h
+ ShellSurfaceWlShell.h
+ ShellSurfaceXdgShell.h
+ ShellSurfaceXdgShellUnstableV6.h
+ Signals.h
+ VideoSyncWpPresentation.h
+ ${WAYLAND_EXTRA_PROTOCOL_GENERATED_DIR}/wayland-extra-protocols.hpp
+ WindowDecorator.h
+ WinEventsWayland.h
+ WinSystemWayland.h
+ XkbcommonKeymap.h)
+
+if(EGL_FOUND)
+ list(APPEND SOURCES WinSystemWaylandEGLContext.cpp)
+ list(APPEND HEADERS WinSystemWaylandEGLContext.h)
+endif()
+
+if(OPENGL_FOUND)
+ list(APPEND SOURCES WinSystemWaylandEGLContextGL.cpp)
+ list(APPEND HEADERS WinSystemWaylandEGLContextGL.h)
+endif()
+if(OPENGLES_FOUND)
+ list(APPEND SOURCES WinSystemWaylandEGLContextGLES.cpp)
+ list(APPEND HEADERS WinSystemWaylandEGLContextGLES.h)
+endif()
+
+
+core_add_library(windowing_WAYLAND)
diff --git a/xbmc/windowing/wayland/Connection.cpp b/xbmc/windowing/wayland/Connection.cpp
new file mode 100644
index 0000000..9d45be6
--- /dev/null
+++ b/xbmc/windowing/wayland/Connection.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017-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 "Connection.h"
+
+#include "utils/log.h"
+
+#include <cassert>
+#include <stdexcept>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+CConnection::CConnection()
+{
+ try
+ {
+ m_display = std::make_unique<wayland::display_t>();
+ }
+ catch (const std::exception& err)
+ {
+ CLog::Log(LOGERROR, "Wayland connection error: {}", err.what());
+ }
+}
+
+bool CConnection::HasDisplay() const
+{
+ return static_cast<bool>(m_display);
+}
+
+wayland::display_t& CConnection::GetDisplay()
+{
+ assert(m_display);
+ return *m_display;
+}
diff --git a/xbmc/windowing/wayland/Connection.h b/xbmc/windowing/wayland/Connection.h
new file mode 100644
index 0000000..b1badd4
--- /dev/null
+++ b/xbmc/windowing/wayland/Connection.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017-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 <memory>
+
+#include <wayland-client.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Connection to Wayland compositor
+ */
+class CConnection
+{
+public:
+ CConnection();
+
+ bool HasDisplay() const;
+ wayland::display_t& GetDisplay();
+
+private:
+ std::unique_ptr<wayland::display_t> m_display;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/InputProcessorKeyboard.cpp b/xbmc/windowing/wayland/InputProcessorKeyboard.cpp
new file mode 100644
index 0000000..37b0240
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorKeyboard.cpp
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2017-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 "InputProcessorKeyboard.h"
+
+#include "utils/log.h"
+
+#include <cassert>
+#include <limits>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+// Offset between keyboard codes of Wayland (effectively evdev) and xkb_keycode_t
+constexpr int WL_KEYBOARD_XKB_CODE_OFFSET{8};
+}
+
+CInputProcessorKeyboard::CInputProcessorKeyboard(IInputHandlerKeyboard& handler)
+: m_handler{handler}, m_keyRepeatTimer{std::bind(&CInputProcessorKeyboard::KeyRepeatTimeout, this)}
+{
+}
+
+void CInputProcessorKeyboard::OnKeyboardKeymap(CSeat* seat, wayland::keyboard_keymap_format format, std::string const &keymap)
+{
+ if (format != wayland::keyboard_keymap_format::xkb_v1)
+ {
+ CLog::Log(LOGWARNING,
+ "Wayland compositor sent keymap in format {}, but we only understand xkbv1 - "
+ "keyboard input will not work",
+ static_cast<unsigned int>(format));
+ return;
+ }
+
+ m_keyRepeatTimer.Stop();
+
+ try
+ {
+ if (!m_xkbContext)
+ {
+ // Lazily initialize XkbcommonContext
+ m_xkbContext.reset(new CXkbcommonContext);
+ }
+
+ m_keymap = m_xkbContext->KeymapFromString(keymap);
+ }
+ catch(std::exception const& e)
+ {
+ CLog::Log(LOGERROR, "Could not parse keymap from compositor: {} - continuing without keymap",
+ e.what());
+ }
+}
+
+void CInputProcessorKeyboard::OnKeyboardEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ const wayland::array_t& keys)
+{
+ m_handler.OnKeyboardEnter();
+}
+
+void CInputProcessorKeyboard::OnKeyboardLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface)
+{
+ m_keyRepeatTimer.Stop();
+ m_handler.OnKeyboardLeave();
+}
+
+void CInputProcessorKeyboard::OnKeyboardKey(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t key, wayland::keyboard_key_state state)
+{
+ if (!m_keymap)
+ {
+ CLog::Log(LOGWARNING, "Key event for code {} without valid keymap, ignoring", key);
+ return;
+ }
+
+ ConvertAndSendKey(key, state == wayland::keyboard_key_state::pressed);
+}
+
+void CInputProcessorKeyboard::OnKeyboardModifiers(CSeat* seat, std::uint32_t serial, std::uint32_t modsDepressed, std::uint32_t modsLatched, std::uint32_t modsLocked, std::uint32_t group)
+{
+ if (!m_keymap)
+ {
+ CLog::Log(LOGWARNING, "Modifier event without valid keymap, ignoring");
+ return;
+ }
+
+ m_keyRepeatTimer.Stop();
+ m_keymap->UpdateMask(modsDepressed, modsLatched, modsLocked, group);
+}
+
+void CInputProcessorKeyboard::OnKeyboardRepeatInfo(CSeat* seat, std::int32_t rate, std::int32_t delay)
+{
+ CLog::Log(LOGDEBUG, "Key repeat rate: {} cps, delay {} ms", rate, delay);
+ // rate is in characters per second, so convert to msec interval
+ m_keyRepeatInterval = (rate != 0) ? static_cast<int> (1000.0f / rate) : 0;
+ m_keyRepeatDelay = delay;
+}
+
+void CInputProcessorKeyboard::ConvertAndSendKey(std::uint32_t scancode, bool pressed)
+{
+ std::uint32_t xkbCode{scancode + WL_KEYBOARD_XKB_CODE_OFFSET};
+ XBMCKey xbmcKey{m_keymap->XBMCKeyForKeycode(xkbCode)};
+ std::uint32_t utf32{m_keymap->UnicodeCodepointForKeycode(xkbCode)};
+
+ if (utf32 > std::numeric_limits<std::uint16_t>::max())
+ {
+ // Kodi event system only supports UTF16, so ignore the codepoint if
+ // it does not fit
+ utf32 = 0;
+ }
+ if (scancode > std::numeric_limits<unsigned char>::max())
+ {
+ // Kodi scancodes are limited to unsigned char, pretend the scancode is unknown
+ // on overflow
+ scancode = 0;
+ }
+
+ XBMC_Event event{SendKey(scancode, xbmcKey, static_cast<std::uint16_t> (utf32), pressed)};
+
+ if (pressed && m_keymap->ShouldKeycodeRepeat(xkbCode) && m_keyRepeatInterval > 0)
+ {
+ // Can't modify keyToRepeat until we're sure the thread isn't accessing it
+ m_keyRepeatTimer.Stop(true);
+ // Update/Set key
+ m_keyToRepeat = event;
+ // Start timer with initial delay
+ m_keyRepeatTimer.Start(std::chrono::milliseconds(m_keyRepeatDelay), false);
+ }
+ else
+ {
+ m_keyRepeatTimer.Stop();
+ }
+}
+
+XBMC_Event CInputProcessorKeyboard::SendKey(unsigned char scancode, XBMCKey key, std::uint16_t unicodeCodepoint, bool pressed)
+{
+ assert(m_keymap);
+
+ XBMC_Event event{};
+ event.type = pressed ? XBMC_KEYDOWN : XBMC_KEYUP;
+ event.key.keysym =
+ {
+ .scancode = scancode,
+ .sym = key,
+ .mod = m_keymap->ActiveXBMCModifiers(),
+ .unicode = unicodeCodepoint
+ };
+ m_handler.OnKeyboardEvent(event);
+ // Return created event for convenience (key repeat)
+ return event;
+}
+
+void CInputProcessorKeyboard::KeyRepeatTimeout()
+{
+ // Reset ourselves
+ m_keyRepeatTimer.RestartAsync(std::chrono::milliseconds(m_keyRepeatInterval));
+ // Simulate repeat: Key up and down
+ XBMC_Event event = m_keyToRepeat;
+ event.type = XBMC_KEYUP;
+ m_handler.OnKeyboardEvent(event);
+ event.type = XBMC_KEYDOWN;
+ m_handler.OnKeyboardEvent(event);
+}
diff --git a/xbmc/windowing/wayland/InputProcessorKeyboard.h b/xbmc/windowing/wayland/InputProcessorKeyboard.h
new file mode 100644
index 0000000..73a37ce
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorKeyboard.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017-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 "Seat.h"
+#include "XkbcommonKeymap.h"
+#include "input/XBMC_keysym.h"
+#include "threads/Timer.h"
+#include "windowing/XBMC_events.h"
+
+#include <atomic>
+#include <cstdint>
+#include <memory>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class IInputHandlerKeyboard
+{
+public:
+ virtual void OnKeyboardEnter() {}
+ virtual void OnKeyboardLeave() {}
+ virtual void OnKeyboardEvent(XBMC_Event& event) = 0;
+ virtual ~IInputHandlerKeyboard() = default;
+};
+
+class CInputProcessorKeyboard final : public IRawInputHandlerKeyboard
+{
+public:
+ CInputProcessorKeyboard(IInputHandlerKeyboard& handler);
+
+ void OnKeyboardKeymap(CSeat* seat, wayland::keyboard_keymap_format format, std::string const& keymap) override;
+ void OnKeyboardEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ const wayland::array_t& keys) override;
+ void OnKeyboardLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface) override;
+ void OnKeyboardKey(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t key, wayland::keyboard_key_state state) override;
+ void OnKeyboardModifiers(CSeat* seat, std::uint32_t serial, std::uint32_t modsDepressed, std::uint32_t modsLatched, std::uint32_t modsLocked, std::uint32_t group) override;
+ void OnKeyboardRepeatInfo(CSeat* seat, std::int32_t rate, std::int32_t delay) override;
+
+private:
+ CInputProcessorKeyboard(CInputProcessorKeyboard const& other) = delete;
+ CInputProcessorKeyboard& operator=(CInputProcessorKeyboard const& other) = delete;
+
+ void ConvertAndSendKey(std::uint32_t scancode, bool pressed);
+ XBMC_Event SendKey(unsigned char scancode, XBMCKey key, std::uint16_t unicodeCodepoint, bool pressed);
+ void KeyRepeatTimeout();
+
+ IInputHandlerKeyboard& m_handler;
+
+ std::unique_ptr<CXkbcommonContext> m_xkbContext;
+ std::unique_ptr<CXkbcommonKeymap> m_keymap;
+ // Default values are used if compositor does not send any
+ std::atomic<int> m_keyRepeatDelay{1000};
+ std::atomic<int> m_keyRepeatInterval{50};
+ // Save complete XBMC_Event so no keymap lookups which might not be thread-safe
+ // are needed in the repeat callback
+ XBMC_Event m_keyToRepeat;
+
+ CTimer m_keyRepeatTimer;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/InputProcessorPointer.cpp b/xbmc/windowing/wayland/InputProcessorPointer.cpp
new file mode 100644
index 0000000..6a2fe04
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorPointer.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2017-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 "InputProcessorPointer.h"
+
+#include "input/mouse/MouseStat.h"
+
+#include <cmath>
+
+#include <linux/input-event-codes.h>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+int WaylandToXbmcButton(std::uint32_t button)
+{
+ // Wayland button is evdev code
+ switch (button)
+ {
+ case BTN_LEFT:
+ return XBMC_BUTTON_LEFT;
+ case BTN_MIDDLE:
+ return XBMC_BUTTON_MIDDLE;
+ case BTN_RIGHT:
+ return XBMC_BUTTON_RIGHT;
+ default:
+ return -1;
+ }
+}
+
+}
+
+CInputProcessorPointer::CInputProcessorPointer(wayland::surface_t const& surface, IInputHandlerPointer& handler)
+: m_surface{surface}, m_handler{handler}
+{
+}
+
+void CInputProcessorPointer::OnPointerEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ double surfaceX,
+ double surfaceY)
+{
+ if (surface == m_surface)
+ {
+ m_pointerOnSurface = true;
+ m_handler.OnPointerEnter(seat->GetGlobalName(), serial);
+ SetMousePosFromSurface({surfaceX, surfaceY});
+ SendMouseMotion();
+ }
+}
+
+void CInputProcessorPointer::OnPointerLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface)
+{
+ if (m_pointerOnSurface)
+ {
+ m_handler.OnPointerLeave();
+ m_pointerOnSurface = false;
+ }
+}
+
+void CInputProcessorPointer::OnPointerMotion(CSeat* seat, std::uint32_t time, double surfaceX, double surfaceY)
+{
+ if (m_pointerOnSurface)
+ {
+ SetMousePosFromSurface({surfaceX, surfaceY});
+ SendMouseMotion();
+ }
+}
+
+void CInputProcessorPointer::OnPointerButton(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state)
+{
+ if (m_pointerOnSurface)
+ {
+ int xbmcButton = WaylandToXbmcButton(button);
+ if (xbmcButton < 0)
+ {
+ // Button is unmapped
+ return;
+ }
+
+ bool pressed = (state == wayland::pointer_button_state::pressed);
+ SendMouseButton(xbmcButton, pressed);
+ }
+}
+
+void CInputProcessorPointer::OnPointerAxis(CSeat* seat, std::uint32_t time, wayland::pointer_axis axis, double value)
+{
+ if (m_pointerOnSurface)
+ {
+ // For axis events we only care about the vector direction
+ // and not the scalar magnitude. Every axis event callback
+ // generates one scroll button event for XBMC
+
+ // Negative is up
+ auto xbmcButton = static_cast<unsigned char> ((value < 0.0) ? XBMC_BUTTON_WHEELUP : XBMC_BUTTON_WHEELDOWN);
+ // Simulate a single click of the wheel-equivalent "button"
+ SendMouseButton(xbmcButton, true);
+ SendMouseButton(xbmcButton, false);
+ }
+}
+
+std::uint16_t CInputProcessorPointer::ConvertMouseCoordinate(double coord) const
+{
+ return static_cast<std::uint16_t> (std::round(coord * m_coordinateScale));
+}
+
+void CInputProcessorPointer::SetMousePosFromSurface(CPointGen<double> position)
+{
+ m_pointerPosition = {ConvertMouseCoordinate(position.x), ConvertMouseCoordinate(position.y)};
+}
+
+void CInputProcessorPointer::SendMouseMotion()
+{
+ XBMC_Event event{};
+ event.type = XBMC_MOUSEMOTION;
+ event.motion = {m_pointerPosition.x, m_pointerPosition.y};
+ m_handler.OnPointerEvent(event);
+}
+
+void CInputProcessorPointer::SendMouseButton(unsigned char button, bool pressed)
+{
+ XBMC_Event event{};
+ event.type = pressed ? XBMC_MOUSEBUTTONDOWN : XBMC_MOUSEBUTTONUP;
+ event.button = {button, m_pointerPosition.x, m_pointerPosition.y};
+ m_handler.OnPointerEvent(event);
+}
diff --git a/xbmc/windowing/wayland/InputProcessorPointer.h b/xbmc/windowing/wayland/InputProcessorPointer.h
new file mode 100644
index 0000000..ce44601
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorPointer.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017-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 "Seat.h"
+#include "input/XBMC_keysym.h"
+#include "utils/Geometry.h"
+#include "windowing/XBMC_events.h"
+
+#include <cstdint>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class IInputHandlerPointer
+{
+public:
+ virtual void OnPointerEnter(std::uint32_t seatGlobalName, std::uint32_t serial) {}
+ virtual void OnPointerLeave() {}
+ virtual void OnPointerEvent(XBMC_Event& event) = 0;
+protected:
+ ~IInputHandlerPointer() = default;
+};
+
+class CInputProcessorPointer final : public IRawInputHandlerPointer
+{
+public:
+ CInputProcessorPointer(wayland::surface_t const& surface, IInputHandlerPointer& handler);
+ void SetCoordinateScale(std::int32_t scale) { m_coordinateScale = scale; }
+
+ void OnPointerEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ double surfaceX,
+ double surfaceY) override;
+ void OnPointerLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface) override;
+ void OnPointerMotion(CSeat* seat, std::uint32_t time, double surfaceX, double surfaceY) override;
+ void OnPointerButton(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state) override;
+ void OnPointerAxis(CSeat* seat, std::uint32_t time, wayland::pointer_axis axis, double value) override;
+
+private:
+ CInputProcessorPointer(CInputProcessorPointer const& other) = delete;
+ CInputProcessorPointer& operator=(CInputProcessorPointer const& other) = delete;
+
+ std::uint16_t ConvertMouseCoordinate(double coord) const;
+ void SetMousePosFromSurface(CPointGen<double> position);
+ void SendMouseMotion();
+ void SendMouseButton(unsigned char button, bool pressed);
+
+ wayland::surface_t m_surface;
+ IInputHandlerPointer& m_handler;
+
+ bool m_pointerOnSurface{};
+
+ // Pointer position in *scaled* coordinates
+ CPointGen<std::uint16_t> m_pointerPosition;
+ std::int32_t m_coordinateScale{1};
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/InputProcessorTouch.cpp b/xbmc/windowing/wayland/InputProcessorTouch.cpp
new file mode 100644
index 0000000..a664e32
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorTouch.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017-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 "InputProcessorTouch.h"
+
+#include "input/touch/generic/GenericTouchInputHandler.h"
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+CInputProcessorTouch::CInputProcessorTouch(wayland::surface_t const& surface)
+: m_surface{surface}
+{
+}
+
+void CInputProcessorTouch::OnTouchDown(CSeat* seat,
+ std::uint32_t serial,
+ std::uint32_t time,
+ const wayland::surface_t& surface,
+ std::int32_t id,
+ double x,
+ double y)
+{
+ if (surface != m_surface)
+ {
+ return;
+ }
+
+ // Find free Kodi pointer number
+ int kodiPointer{-1};
+ // Not optimal, but irrelevant for the small number of iterations
+ for (int testPointer{0}; testPointer < CGenericTouchInputHandler::MAX_POINTERS; testPointer++)
+ {
+ if (std::all_of(m_touchPoints.cbegin(), m_touchPoints.cend(),
+ [=](decltype(m_touchPoints)::value_type const& pair)
+ {
+ return (pair.second.kodiPointerNumber != testPointer);
+ }))
+ {
+ kodiPointer = testPointer;
+ break;
+ }
+ }
+
+ if (kodiPointer != -1)
+ {
+ auto it = m_touchPoints.emplace(std::piecewise_construct, std::forward_as_tuple(id), std::forward_as_tuple(time, kodiPointer, x * m_coordinateScale, y * m_coordinateScale, 0.0f)).first;
+ SendTouchPointEvent(TouchInputDown, it->second);
+ }
+}
+
+void CInputProcessorTouch::OnTouchUp(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::int32_t id)
+{
+ auto it = m_touchPoints.find(id);
+ if (it != m_touchPoints.end())
+ {
+ auto& point = it->second;
+ point.lastEventTime = time;
+ SendTouchPointEvent(TouchInputUp, point);
+ m_touchPoints.erase(it);
+ }
+}
+
+void CInputProcessorTouch::OnTouchMotion(CSeat* seat, std::uint32_t time, std::int32_t id, double x, double y)
+{
+ auto it = m_touchPoints.find(id);
+ if (it != m_touchPoints.end())
+ {
+ auto& point = it->second;
+ point.x = x * m_coordinateScale;
+ point.y = y * m_coordinateScale;
+ point.lastEventTime = time;
+ SendTouchPointEvent(TouchInputMove, point);
+ }
+}
+
+void CInputProcessorTouch::OnTouchCancel(CSeat* seat)
+{
+ AbortTouches();
+}
+
+void CInputProcessorTouch::OnTouchShape(CSeat* seat, std::int32_t id, double major, double minor)
+{
+ auto it = m_touchPoints.find(id);
+ if (it != m_touchPoints.end())
+ {
+ auto& point = it->second;
+ // Kodi only supports size without shape, so use average of both axes
+ point.size = ((major + minor) / 2.0) * m_coordinateScale;
+ UpdateTouchPoint(point);
+ }
+}
+
+CInputProcessorTouch::~CInputProcessorTouch() noexcept
+{
+ AbortTouches();
+}
+
+void CInputProcessorTouch::AbortTouches()
+{
+ // TouchInputAbort aborts for all pointers, so it does not matter which is specified
+ if (!m_touchPoints.empty())
+ {
+ SendTouchPointEvent(TouchInputAbort, m_touchPoints.begin()->second);
+ }
+ m_touchPoints.clear();
+}
+
+void CInputProcessorTouch::SendTouchPointEvent(TouchInput event, const TouchPoint& point)
+{
+ if (event == TouchInputMove)
+ {
+ for (auto const& point : m_touchPoints)
+ {
+ // Contrary to the docs, this must be called before HandleTouchInput or the
+ // position will not be updated and gesture detection will not work
+ UpdateTouchPoint(point.second);
+ }
+ }
+ CGenericTouchInputHandler::GetInstance().HandleTouchInput(event, point.x, point.y, point.lastEventTime * INT64_C(1000000), point.kodiPointerNumber, point.size);
+}
+
+void CInputProcessorTouch::UpdateTouchPoint(const TouchPoint& point)
+{
+ CGenericTouchInputHandler::GetInstance().UpdateTouchPointer(point.kodiPointerNumber, point.x, point.y, point.lastEventTime * INT64_C(1000000), point.size);
+}
diff --git a/xbmc/windowing/wayland/InputProcessorTouch.h b/xbmc/windowing/wayland/InputProcessorTouch.h
new file mode 100644
index 0000000..fa6f606
--- /dev/null
+++ b/xbmc/windowing/wayland/InputProcessorTouch.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017-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 "Seat.h"
+#include "input/touch/ITouchInputHandler.h"
+
+#include <cstdint>
+#include <map>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Touch input processor
+ *
+ * Events go directly to \ref CGenericTouchInputHandler, so no callbacks here
+ */
+class CInputProcessorTouch final : public IRawInputHandlerTouch
+{
+public:
+ CInputProcessorTouch(wayland::surface_t const& surface);
+ ~CInputProcessorTouch() noexcept;
+ void SetCoordinateScale(std::int32_t scale) { m_coordinateScale = scale; }
+
+ void OnTouchDown(CSeat* seat,
+ std::uint32_t serial,
+ std::uint32_t time,
+ const wayland::surface_t& surface,
+ std::int32_t id,
+ double x,
+ double y) override;
+ void OnTouchUp(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::int32_t id) override;
+ void OnTouchMotion(CSeat* seat, std::uint32_t time, std::int32_t id, double x, double y) override;
+ void OnTouchCancel(CSeat* seat) override;
+ void OnTouchShape(CSeat* seat, std::int32_t id, double major, double minor) override;
+
+private:
+ CInputProcessorTouch(CInputProcessorTouch const& other) = delete;
+ CInputProcessorTouch& operator=(CInputProcessorTouch const& other) = delete;
+
+ struct TouchPoint
+ {
+ std::uint32_t lastEventTime;
+ /// Pointer number passed to \ref ITouchInputHandler
+ std::int32_t kodiPointerNumber;
+ /**
+ * Last coordinates - needed for TouchInputUp events where Wayland does not
+ * send new coordinates but Kodi needs them anyway
+ */
+ float x, y, size;
+ TouchPoint(std::uint32_t initialEventTime, std::int32_t kodiPointerNumber, float x, float y, float size)
+ : lastEventTime{initialEventTime}, kodiPointerNumber{kodiPointerNumber}, x{x}, y{y}, size{size}
+ {}
+ };
+
+ void SendTouchPointEvent(TouchInput event, TouchPoint const& point);
+ void UpdateTouchPoint(TouchPoint const& point);
+ void AbortTouches();
+
+ wayland::surface_t m_surface;
+ std::int32_t m_coordinateScale{1};
+
+ /// Map of wl_touch point id to data
+ std::map<std::int32_t, TouchPoint> m_touchPoints;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.cpp b/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.cpp
new file mode 100644
index 0000000..32dbbc6
--- /dev/null
+++ b/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017-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 "OSScreenSaverIdleInhibitUnstableV1.h"
+
+#include "Registry.h"
+
+#include <cassert>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+COSScreenSaverIdleInhibitUnstableV1* COSScreenSaverIdleInhibitUnstableV1::TryCreate(CConnection& connection, wayland::surface_t const& inhibitSurface)
+{
+ wayland::zwp_idle_inhibit_manager_v1_t manager;
+ CRegistry registry{connection};
+ registry.RequestSingleton(manager, 1, 1, false);
+ registry.Bind();
+
+ if (manager)
+ {
+ return new COSScreenSaverIdleInhibitUnstableV1(manager, inhibitSurface);
+ }
+ else
+ {
+ return nullptr;
+ }
+}
+
+COSScreenSaverIdleInhibitUnstableV1::COSScreenSaverIdleInhibitUnstableV1(const wayland::zwp_idle_inhibit_manager_v1_t& manager, const wayland::surface_t& inhibitSurface)
+: m_manager{manager}, m_surface{inhibitSurface}
+{
+ assert(m_manager);
+ assert(m_surface);
+}
+
+void COSScreenSaverIdleInhibitUnstableV1::Inhibit()
+{
+ if (!m_inhibitor)
+ {
+ m_inhibitor = m_manager.create_inhibitor(m_surface);
+ }
+}
+
+void COSScreenSaverIdleInhibitUnstableV1::Uninhibit()
+{
+ m_inhibitor.proxy_release();
+}
diff --git a/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.h b/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.h
new file mode 100644
index 0000000..0303d60
--- /dev/null
+++ b/xbmc/windowing/wayland/OSScreenSaverIdleInhibitUnstableV1.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017-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 "../OSScreenSaver.h"
+#include "Connection.h"
+
+#include <wayland-extra-protocols.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class COSScreenSaverIdleInhibitUnstableV1 : public IOSScreenSaver
+{
+public:
+ COSScreenSaverIdleInhibitUnstableV1(wayland::zwp_idle_inhibit_manager_v1_t const& manager, wayland::surface_t const& inhibitSurface);
+ static COSScreenSaverIdleInhibitUnstableV1* TryCreate(CConnection& connection, wayland::surface_t const& inhibitSurface);
+ void Inhibit() override;
+ void Uninhibit() override;
+
+private:
+ wayland::zwp_idle_inhibit_manager_v1_t m_manager;
+ wayland::zwp_idle_inhibitor_v1_t m_inhibitor;
+ wayland::surface_t m_surface;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/OptionalsReg.cpp b/xbmc/windowing/wayland/OptionalsReg.cpp
new file mode 100644
index 0000000..5b456eb
--- /dev/null
+++ b/xbmc/windowing/wayland/OptionalsReg.cpp
@@ -0,0 +1,136 @@
+/*
+ * 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 "OptionalsReg.h"
+
+//-----------------------------------------------------------------------------
+// VAAPI
+//-----------------------------------------------------------------------------
+#if defined(HAVE_LIBVA) && defined(HAS_EGL)
+#include <va/va_wayland.h>
+#include "cores/VideoPlayer/DVDCodecs/Video/VAAPI.h"
+#if defined(HAS_GL)
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGL.h"
+#endif
+#if defined(HAS_GLES)
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVAAPIGLES.h"
+#endif
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CVaapiProxy : public VAAPI::IVaapiWinSystem
+{
+public:
+ CVaapiProxy() = default;
+ virtual ~CVaapiProxy() = default;
+ VADisplay GetVADisplay() override { return vaGetDisplayWl(dpy); };
+ void *GetEGLDisplay() override { return eglDisplay; };
+
+ wl_display *dpy;
+ void *eglDisplay;
+};
+
+CVaapiProxy* VaapiProxyCreate()
+{
+ return new CVaapiProxy();
+}
+
+void VaapiProxyDelete(CVaapiProxy* proxy)
+{
+ delete proxy;
+}
+
+void VaapiProxyConfig(CVaapiProxy* proxy, void* dpy, void* eglDpy)
+{
+ proxy->dpy = static_cast<wl_display*>(dpy);
+ proxy->eglDisplay = eglDpy;
+}
+
+void VAAPIRegister(CVaapiProxy* winSystem, bool deepColor)
+{
+ VAAPI::CDecoder::Register(winSystem, deepColor);
+}
+
+#if defined(HAS_GL)
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+ EGLDisplay eglDpy = winSystem->eglDisplay;
+ VADisplay vaDpy = vaGetDisplayWl(winSystem->dpy);
+ CRendererVAAPIGL::Register(winSystem, vaDpy, eglDpy, general, deepColor);
+}
+#endif
+
+#if defined(HAS_GLES)
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+ EGLDisplay eglDpy = winSystem->eglDisplay;
+ VADisplay vaDpy = vaGetDisplayWl(winSystem->dpy);
+ CRendererVAAPIGLES::Register(winSystem, vaDpy, eglDpy, general, deepColor);
+}
+#endif
+
+} // namespace WAYLAND
+} // namespace WINDOWING
+} // namespace KODI
+
+#else
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CVaapiProxy
+{
+};
+
+CVaapiProxy* VaapiProxyCreate()
+{
+ return nullptr;
+}
+
+void VaapiProxyDelete(CVaapiProxy* proxy)
+{
+}
+
+void VaapiProxyConfig(CVaapiProxy* proxy, void* dpy, void* eglDpy)
+{
+
+}
+
+void VAAPIRegister(CVaapiProxy* winSystem, bool deepColor)
+{
+
+}
+
+#if defined(HAS_GL)
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+
+}
+#endif
+
+#if defined(HAS_GLES)
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor)
+{
+}
+#endif
+
+} // namespace WAYLAND
+} // namespace WINDOWING
+} // namespace KODI
+
+#endif
+
diff --git a/xbmc/windowing/wayland/OptionalsReg.h b/xbmc/windowing/wayland/OptionalsReg.h
new file mode 100644
index 0000000..293c9c3
--- /dev/null
+++ b/xbmc/windowing/wayland/OptionalsReg.h
@@ -0,0 +1,37 @@
+/*
+ * 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
+
+
+//-----------------------------------------------------------------------------
+// VAAPI
+//-----------------------------------------------------------------------------
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+class CVaapiProxy;
+
+CVaapiProxy* VaapiProxyCreate();
+void VaapiProxyDelete(CVaapiProxy *proxy);
+void VaapiProxyConfig(CVaapiProxy *proxy, void *dpy, void *eglDpy);
+void VAAPIRegister(CVaapiProxy *winSystem, bool deepColor);
+#if defined(HAS_GL)
+void VAAPIRegisterRenderGL(CVaapiProxy* winSystem, bool& general, bool& deepColor);
+#endif
+#if defined(HAS_GLES)
+void VAAPIRegisterRenderGLES(CVaapiProxy* winSystem, bool& general, bool& deepColor);
+#endif
+
+} // namespace WAYLAND
+} // namespace WINDOWING
+} // namespace KODI
diff --git a/xbmc/windowing/wayland/Output.cpp b/xbmc/windowing/wayland/Output.cpp
new file mode 100644
index 0000000..5dd840a
--- /dev/null
+++ b/xbmc/windowing/wayland/Output.cpp
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2017-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 "Output.h"
+
+#include <cassert>
+#include <cmath>
+#include <mutex>
+#include <set>
+#include <stdexcept>
+#include <utility>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+COutput::COutput(std::uint32_t globalName,
+ wayland::output_t const& output,
+ std::function<void()> doneHandler)
+ : m_globalName{globalName}, m_output{output}, m_doneHandler{std::move(doneHandler)}
+{
+ assert(m_output);
+
+ m_output.on_geometry() = [this](std::int32_t x, std::int32_t y, std::int32_t physWidth,
+ std::int32_t physHeight, wayland::output_subpixel,
+ std::string const& make, std::string const& model,
+ const wayland::output_transform&)
+ {
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ m_position = {x, y};
+ // Some monitors report invalid (negative) values that would cause an exception
+ // with CSizeInt
+ if (physWidth < 0 || physHeight < 0)
+ m_physicalSize = {};
+ else
+ m_physicalSize = {physWidth, physHeight};
+ m_make = make;
+ m_model = model;
+ };
+ m_output.on_mode() = [this](const wayland::output_mode& flags, std::int32_t width,
+ std::int32_t height, std::int32_t refresh) {
+ // std::set.emplace returns pair of iterator to the (possibly) inserted
+ // element and boolean information whether the element was actually added
+ // which we do not need
+ auto modeIterator = m_modes.emplace(CSizeInt{width, height}, refresh).first;
+ std::unique_lock<CCriticalSection> lock(m_iteratorCriticalSection);
+ // Remember current and preferred mode
+ // Current mode is the last one that was sent with current flag set
+ if (flags & wayland::output_mode::current)
+ {
+ m_currentMode = modeIterator;
+ }
+ if (flags & wayland::output_mode::preferred)
+ {
+ m_preferredMode = modeIterator;
+ }
+ };
+ m_output.on_scale() = [this](std::int32_t scale)
+ {
+ m_scale = scale;
+ };
+
+ m_output.on_done() = [this]()
+ {
+ m_doneHandler();
+ };
+}
+
+COutput::~COutput() noexcept
+{
+ // Reset event handlers - someone might still hold a reference to the output_t,
+ // causing events to be dispatched. They should not go to a deleted class.
+ m_output.on_geometry() = nullptr;
+ m_output.on_mode() = nullptr;
+ m_output.on_done() = nullptr;
+ m_output.on_scale() = nullptr;
+}
+
+const COutput::Mode& COutput::GetCurrentMode() const
+{
+ std::unique_lock<CCriticalSection> lock(m_iteratorCriticalSection);
+ if (m_currentMode == m_modes.end())
+ {
+ throw std::runtime_error("Current mode not set");
+ }
+ return *m_currentMode;
+}
+
+const COutput::Mode& COutput::GetPreferredMode() const
+{
+ std::unique_lock<CCriticalSection> lock(m_iteratorCriticalSection);
+ if (m_preferredMode == m_modes.end())
+ {
+ throw std::runtime_error("Preferred mode not set");
+ }
+ return *m_preferredMode;
+}
+
+float COutput::GetPixelRatioForMode(const Mode& mode) const
+{
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ if (m_physicalSize.IsZero() || mode.size.IsZero())
+ {
+ return 1.0f;
+ }
+ else
+ {
+ return (
+ (static_cast<float> (m_physicalSize.Width()) / static_cast<float> (mode.size.Width()))
+ /
+ (static_cast<float> (m_physicalSize.Height()) / static_cast<float> (mode.size.Height()))
+ );
+ }
+}
+
+float COutput::GetDpiForMode(const Mode& mode) const
+{
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ if (m_physicalSize.IsZero())
+ {
+ // We really have no idea, so use a "sane" default
+ return 96.0;
+ }
+ else
+ {
+ constexpr float INCH_MM_RATIO{25.4f};
+
+ float diagonalPixels = std::sqrt(mode.size.Width() * mode.size.Width() + mode.size.Height() * mode.size.Height());
+ // physicalWidth/physicalHeight is in millimeters
+ float diagonalInches =
+ std::sqrt(static_cast<float>(m_physicalSize.Width() * m_physicalSize.Width() +
+ m_physicalSize.Height() * m_physicalSize.Height())) /
+ INCH_MM_RATIO;
+
+ return diagonalPixels / diagonalInches;
+ }
+}
+
+float COutput::GetCurrentDpi() const
+{
+ return GetDpiForMode(GetCurrentMode());
+}
diff --git a/xbmc/windowing/wayland/Output.h b/xbmc/windowing/wayland/Output.h
new file mode 100644
index 0000000..2408a6f
--- /dev/null
+++ b/xbmc/windowing/wayland/Output.h
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017-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 "utils/Geometry.h"
+
+#include <atomic>
+#include <cstdint>
+#include <mutex>
+#include <set>
+#include <tuple>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * wl_output handler that collects information from the compositor and then
+ * passes it on when everything is available
+ */
+class COutput
+{
+public:
+ COutput(std::uint32_t globalName, wayland::output_t const & output, std::function<void()> doneHandler);
+ ~COutput() noexcept;
+
+ wayland::output_t const& GetWaylandOutput() const
+ {
+ return m_output;
+ }
+ std::uint32_t GetGlobalName() const
+ {
+ return m_globalName;
+ }
+ /**
+ * Get output position in compositor coordinate space
+ * \return (x, y) tuple of output position
+ */
+ CPointInt GetPosition() const
+ {
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ return m_position;
+ }
+ /**
+ * Get output physical size in millimeters
+ * \return (width, height) tuple of output physical size in millimeters
+ */
+ CSizeInt GetPhysicalSize() const
+ {
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ return m_physicalSize;
+ }
+ std::string const& GetMake() const
+ {
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ return m_make;
+ }
+ std::string const& GetModel() const
+ {
+ std::unique_lock<CCriticalSection> lock(m_geometryCriticalSection);
+ return m_model;
+ }
+ std::int32_t GetScale() const
+ {
+ return m_scale;
+ }
+
+ struct Mode
+ {
+ CSizeInt size;
+ std::int32_t refreshMilliHz;
+ Mode(CSizeInt size, std::int32_t refreshMilliHz)
+ : size{size}, refreshMilliHz(refreshMilliHz)
+ {}
+
+ float GetRefreshInHz() const
+ {
+ return refreshMilliHz / 1000.0f;
+ }
+
+ std::tuple<std::int32_t, std::int32_t, std::int32_t> AsTuple() const
+ {
+ return std::make_tuple(size.Width(), size.Height(), refreshMilliHz);
+ }
+
+ // Comparison operator needed for std::set
+ bool operator<(const Mode& right) const
+ {
+ return AsTuple() < right.AsTuple();
+ }
+
+ bool operator==(const Mode& right) const
+ {
+ return AsTuple() == right.AsTuple();
+ }
+
+ bool operator!=(const Mode& right) const
+ {
+ return !(*this == right);
+ }
+ };
+
+ std::set<Mode> const& GetModes() const
+ {
+ return m_modes;
+ }
+ Mode const& GetCurrentMode() const;
+ Mode const& GetPreferredMode() const;
+
+ float GetPixelRatioForMode(Mode const& mode) const;
+ float GetDpiForMode(Mode const& mode) const;
+ float GetCurrentDpi() const;
+
+private:
+ COutput(COutput const& other) = delete;
+ COutput& operator=(COutput const& other) = delete;
+
+ std::uint32_t m_globalName;
+ wayland::output_t m_output;
+ std::function<void()> m_doneHandler;
+
+ mutable CCriticalSection m_geometryCriticalSection;
+ mutable CCriticalSection m_iteratorCriticalSection;
+
+ CPointInt m_position;
+ CSizeInt m_physicalSize;
+ std::string m_make, m_model;
+ std::atomic<std::int32_t> m_scale{1}; // default scale of 1 if no wl_output::scale is sent
+
+ std::set<Mode> m_modes;
+ // For std::set, insertion never invalidates existing iterators, and modes are
+ // never removed, so the usage of iterators is safe
+ std::set<Mode>::iterator m_currentMode{m_modes.end()};
+ std::set<Mode>::iterator m_preferredMode{m_modes.end()};
+};
+
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/Registry.cpp b/xbmc/windowing/wayland/Registry.cpp
new file mode 100644
index 0000000..68cd745
--- /dev/null
+++ b/xbmc/windowing/wayland/Registry.cpp
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2017-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 "Registry.h"
+
+#include "WinEventsWayland.h"
+#include "utils/log.h"
+
+#include <wayland-client-protocol.h>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+void TryBind(wayland::registry_t& registry, wayland::proxy_t& target, std::uint32_t name, std::string const& interface, std::uint32_t minVersion, std::uint32_t maxVersion, std::uint32_t offeredVersion)
+{
+ if (offeredVersion < minVersion)
+ {
+ CLog::Log(LOGWARNING,
+ "Not binding Wayland protocol {} because server has only version {} (we need at "
+ "least version {})",
+ interface, offeredVersion, minVersion);
+ }
+ else
+ {
+ // Binding below the offered version is OK
+ auto bindVersion = std::min(maxVersion, offeredVersion);
+ CLog::Log(LOGDEBUG, "Binding Wayland protocol {} version {} (server has version {})", interface,
+ bindVersion, offeredVersion);
+ registry.bind(name, target, bindVersion);
+ }
+}
+
+}
+
+CRegistry::CRegistry(CConnection& connection)
+: m_connection{connection}
+{
+}
+
+void CRegistry::RequestSingletonInternal(wayland::proxy_t& target, std::string const& interfaceName, std::uint32_t minVersion, std::uint32_t maxVersion, bool required)
+{
+ if (m_registry)
+ {
+ throw std::logic_error("Cannot request more binds from registry after binding has started");
+ }
+ m_singletonBinds.emplace(std::piecewise_construct, std::forward_as_tuple(interfaceName), std::forward_as_tuple(target, minVersion, maxVersion, required));
+}
+
+void CRegistry::RequestInternal(std::function<wayland::proxy_t()> constructor, const std::string& interfaceName, std::uint32_t minVersion, std::uint32_t maxVersion, AddHandler addHandler, RemoveHandler removeHandler)
+{
+ if (m_registry)
+ {
+ throw std::logic_error("Cannot request more binds from registry after binding has started");
+ }
+ m_binds.emplace(std::piecewise_construct, std::forward_as_tuple(interfaceName), std::forward_as_tuple(constructor, minVersion, maxVersion, addHandler, removeHandler));
+}
+
+void CRegistry::Bind()
+{
+ if (m_registry)
+ {
+ throw std::logic_error("Cannot start binding on registry twice");
+ }
+
+ // We want to block in this function until we have received the global interfaces
+ // from the compositor - no matter whether the global event pump is running
+ // or not.
+ // If it is running, we have to take special precautions not to drop events between
+ // the creation of the registry and attaching event handlers, so we create
+ // an extra queue and use that to dispatch the singleton globals. Then
+ // we switch back to the global queue for further dispatch of interfaces
+ // added/removed dynamically.
+
+ auto registryRoundtripQueue = m_connection.GetDisplay().create_queue();
+
+ auto displayProxy = m_connection.GetDisplay().proxy_create_wrapper();
+ displayProxy.set_queue(registryRoundtripQueue);
+
+ m_registry = displayProxy.get_registry();
+
+ m_registry.on_global() = [this](std::uint32_t name, const std::string& interface,
+ std::uint32_t version) {
+ {
+ auto it = m_singletonBinds.find(interface);
+ if (it != m_singletonBinds.end())
+ {
+ auto& bind = it->second;
+ auto registryProxy = m_registry.proxy_create_wrapper();
+ // Events on the bound global should always go to the main queue
+ registryProxy.set_queue(wayland::event_queue_t());
+ TryBind(registryProxy, bind.target, name, interface, bind.minVersion, bind.maxVersion, version);
+ return;
+ }
+ }
+
+ {
+ auto it = m_binds.find(interface);
+ if (it != m_binds.end())
+ {
+ auto& bind = it->second;
+ wayland::proxy_t target{bind.constructor()};
+ auto registryProxy = m_registry.proxy_create_wrapper();
+ // Events on the bound global should always go to the main queue
+ registryProxy.set_queue(wayland::event_queue_t());
+ TryBind(registryProxy, target, name, interface, bind.minVersion, bind.maxVersion, version);
+ if (target)
+ {
+ m_boundNames.emplace(name, bind);
+ bind.addHandler(name, std::move(target));
+ }
+ return;
+ }
+ }
+ };
+
+ m_registry.on_global_remove() = [this] (std::uint32_t name)
+ {
+ auto it = m_boundNames.find(name);
+ if (it != m_boundNames.end())
+ {
+ it->second.get().removeHandler(name);
+ m_boundNames.erase(it);
+ }
+ };
+
+ CLog::Log(LOGDEBUG, "Wayland connection: Waiting for global interfaces");
+ m_connection.GetDisplay().roundtrip_queue(registryRoundtripQueue);
+ CLog::Log(LOGDEBUG, "Wayland connection: Roundtrip complete");
+
+ CheckRequired();
+
+ // Now switch it to the global queue for further runtime binds
+ m_registry.set_queue(wayland::event_queue_t());
+ // Roundtrip extra queue one last time in case something got queued up there.
+ // Do it on the event thread so it does not race/run in parallel with the
+ // dispatch of newly arrived registry messages in the default queue.
+ CWinEventsWayland::RoundtripQueue(registryRoundtripQueue);
+}
+
+void CRegistry::UnbindSingletons()
+{
+ for (auto& bind : m_singletonBinds)
+ {
+ bind.second.target.proxy_release();
+ }
+}
+
+void CRegistry::CheckRequired()
+{
+ for (auto const& bind : m_singletonBinds)
+ {
+ if (bind.second.required && !bind.second.target)
+ {
+ throw std::runtime_error(std::string("Missing required ") + bind.first + " protocol");
+ }
+ }
+} \ No newline at end of file
diff --git a/xbmc/windowing/wayland/Registry.h b/xbmc/windowing/wayland/Registry.h
new file mode 100644
index 0000000..f440782
--- /dev/null
+++ b/xbmc/windowing/wayland/Registry.h
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2017-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 "Connection.h"
+
+#include <cstdint>
+#include <functional>
+#include <map>
+#include <utility>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Handle Wayland globals
+ *
+ * Request singletons (bound once) with \ref RequestSingleton, non-singletons
+ * such as wl_output with \ref Request, then call \ref Bind once.
+ *
+ * Make sure to destroy all registries before destroying the \ref CConnection.
+ */
+class CRegistry
+{
+public:
+ explicit CRegistry(CConnection& connection);
+
+ /**
+ * Request a static singleton global to be bound to a proxy
+ *
+ * You should only use this if the singleton is announced at registry bind time
+ * (not dynamically) and you do not need to catch events that are sent immediately
+ * in response to the bind. Use \ref Request in that case, even if there is
+ * ever only one instance of the object at maximum.
+ *
+ * Cannot be called after \ref Bind has been called.
+ *
+ * \param target target of waylandpp proxy type
+ * \param minVersion minimum version to bind
+ * \param maxVersion maximum version to bind
+ * \param required whether to throw an exception when the bind cannot be satisfied
+ * by the compositor - exception is thrown in \ref Bind
+ */
+ template<typename T>
+ void RequestSingleton(T& target, std::uint32_t minVersion, std::uint32_t maxVersion, bool required = true)
+ {
+ RequestSingletonInternal(target, T::interface_name, minVersion, maxVersion, required);
+ }
+ using AddHandler = std::function<void(std::uint32_t /* name */, wayland::proxy_t&& /* object */)>;
+ using RemoveHandler = std::function<void(std::uint32_t) /* name */>;
+ /**
+ * Request a callback when a dynamic global appears or disappears
+ *
+ * The callbacks may be called from the thread that calls \ref Bind or the
+ * global Wayland message pump thread during \ref Bind (but never at the same
+ * time) and only from the global thread after \ref Bind returns.
+ *
+ * Events that occur immediately upon binding are only delivered reliably
+ * if \ref Bind is called from the Wayland message pump thread.
+ *
+ * Cannot be called after \ref Bind has been called.
+ *
+ * \param minVersion minimum version to bind
+ * \param maxVersion maximum version to bind
+ * \param addHandler function that is called when a global of the requested
+ * type is added
+ * \param removeHandler function that is called when a global of the requested
+ * type is removed
+ */
+ template<typename T>
+ void Request(std::uint32_t minVersion,
+ std::uint32_t maxVersion,
+ const AddHandler& addHandler,
+ const RemoveHandler& removeHandler)
+ {
+ RequestInternal([]{ return T(); }, T::interface_name, minVersion, maxVersion, addHandler, removeHandler);
+ }
+
+ /**
+ * Create a registry object at the compositor and do an roundtrip to bind
+ * objects
+ *
+ * This function blocks until the initial roundtrip is complete. All statically
+ * requested singletons that were available will be bound then.
+ *
+ * Neither statically nor dynamically requested proxies will be bound before this
+ * function is called.
+ *
+ * May throw std::runtime_error if a required global was not found.
+ *
+ * Can only be called once for the same \ref CRegistry object.
+ */
+ void Bind();
+ /**
+ * Unbind all singletons requested with \ref RequestSingleton
+ */
+ void UnbindSingletons();
+
+private:
+ CRegistry(CRegistry const& other) = delete;
+ CRegistry& operator=(CRegistry const& other) = delete;
+
+ void RequestSingletonInternal(wayland::proxy_t& target, std::string const& interfaceName, std::uint32_t minVersion, std::uint32_t maxVersion, bool required);
+ void RequestInternal(std::function<wayland::proxy_t()> constructor, std::string const& interfaceName, std::uint32_t minVersion, std::uint32_t maxVersion, AddHandler addHandler, RemoveHandler removeHandler);
+ void CheckRequired();
+
+ CConnection& m_connection;
+ wayland::registry_t m_registry;
+
+ struct SingletonBindInfo
+ {
+ wayland::proxy_t& target;
+ // Throw exception if trying to bind below this version and required
+ std::uint32_t minVersion;
+ // Limit bind version to the minimum of this and compositor version
+ std::uint32_t maxVersion;
+ bool required;
+ SingletonBindInfo(wayland::proxy_t& target, std::uint32_t minVersion, std::uint32_t maxVersion, bool required)
+ : target{target}, minVersion{minVersion}, maxVersion{maxVersion}, required{required}
+ {}
+ };
+ std::map<std::string, SingletonBindInfo> m_singletonBinds;
+
+ struct BindInfo
+ {
+ std::function<wayland::proxy_t()> constructor;
+ std::uint32_t minVersion;
+ std::uint32_t maxVersion;
+ AddHandler addHandler;
+ RemoveHandler removeHandler;
+ BindInfo(std::function<wayland::proxy_t()> constructor,
+ std::uint32_t minVersion,
+ std::uint32_t maxVersion,
+ AddHandler addHandler,
+ RemoveHandler removeHandler)
+ : constructor{std::move(constructor)},
+ minVersion{minVersion},
+ maxVersion{maxVersion},
+ addHandler{std::move(addHandler)},
+ removeHandler{std::move(removeHandler)}
+ {}
+ };
+ std::map<std::string, BindInfo> m_binds;
+
+ std::map<std::uint32_t, std::reference_wrapper<BindInfo>> m_boundNames;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/Seat.cpp b/xbmc/windowing/wayland/Seat.cpp
new file mode 100644
index 0000000..56709b3
--- /dev/null
+++ b/xbmc/windowing/wayland/Seat.cpp
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2017-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 "Seat.h"
+
+#include "utils/log.h"
+
+#include "platform/posix/utils/FileHandle.h"
+#include "platform/posix/utils/Mmap.h"
+
+#include <cassert>
+#include <utility>
+
+#include <unistd.h>
+
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace std::placeholders;
+
+namespace
+{
+
+/**
+ * Handle change of availability of a wl_seat input capability
+ *
+ * This checks whether the capability is currently available with the wl_seat
+ * and whether it was bound to a protocol object. If there is a mismatch between
+ * these two, the protocol proxy is released if a capability was removed or bound
+ * if a capability was added.
+ *
+ * \param caps new capabilities
+ * \param cap capability to check for
+ * \param seatName human-readable name of the seat for log messages
+ * \param capName human-readable name of the capability for log messages
+ * \param proxy proxy object that should be filled with a new instance or reset
+ * \param instanceProvider function that functions as factory for the Wayland
+ * protocol instance if the capability has been added
+ */
+template<typename T, typename InstanceProviderT>
+bool HandleCapabilityChange(const wayland::seat_capability& caps,
+ const wayland::seat_capability& cap,
+ std::string const& seatName,
+ std::string const& capName,
+ T& proxy,
+ InstanceProviderT instanceProvider)
+{
+ bool hasCapability = caps & cap;
+
+ if ((!!proxy) != hasCapability)
+ {
+ // Capability changed
+
+ if (hasCapability)
+ {
+ // The capability was added
+ CLog::Log(LOGDEBUG, "Wayland seat {} gained capability {}", seatName, capName);
+ proxy = instanceProvider();
+ return true;
+ }
+ else
+ {
+ // The capability was removed
+ CLog::Log(LOGDEBUG, "Wayland seat {} lost capability {}", seatName, capName);
+ proxy.proxy_release();
+ }
+ }
+
+ return false;
+}
+
+}
+
+CSeat::CSeat(std::uint32_t globalName, wayland::seat_t const& seat, CConnection& connection)
+: m_globalName{globalName}, m_seat{seat}, m_selection{connection, seat}
+{
+ m_seat.on_name() = [this](std::string name) { m_name = std::move(name); };
+ m_seat.on_capabilities() = std::bind(&CSeat::HandleOnCapabilities, this, std::placeholders::_1);
+}
+
+CSeat::~CSeat() noexcept = default;
+
+void CSeat::AddRawInputHandlerKeyboard(KODI::WINDOWING::WAYLAND::IRawInputHandlerKeyboard *rawKeyboardHandler)
+{
+ assert(rawKeyboardHandler);
+ m_rawKeyboardHandlers.emplace(rawKeyboardHandler);
+}
+
+void CSeat::RemoveRawInputHandlerKeyboard(KODI::WINDOWING::WAYLAND::IRawInputHandlerKeyboard *rawKeyboardHandler)
+{
+ m_rawKeyboardHandlers.erase(rawKeyboardHandler);
+}
+
+void CSeat::AddRawInputHandlerPointer(IRawInputHandlerPointer* rawPointerHandler)
+{
+ assert(rawPointerHandler);
+ m_rawPointerHandlers.emplace(rawPointerHandler);
+}
+
+void CSeat::RemoveRawInputHandlerPointer(KODI::WINDOWING::WAYLAND::IRawInputHandlerPointer *rawPointerHandler)
+{
+ m_rawPointerHandlers.erase(rawPointerHandler);
+}
+
+void CSeat::AddRawInputHandlerTouch(IRawInputHandlerTouch* rawTouchHandler)
+{
+ assert(rawTouchHandler);
+ m_rawTouchHandlers.emplace(rawTouchHandler);
+}
+
+void CSeat::RemoveRawInputHandlerTouch(KODI::WINDOWING::WAYLAND::IRawInputHandlerTouch *rawTouchHandler)
+{
+ m_rawTouchHandlers.erase(rawTouchHandler);
+}
+
+void CSeat::HandleOnCapabilities(const wayland::seat_capability& caps)
+{
+ if (HandleCapabilityChange(caps, wayland::seat_capability::keyboard, GetName(), "keyboard", m_keyboard, std::bind(&wayland::seat_t::get_keyboard, m_seat)))
+ {
+ HandleKeyboardCapability();
+ }
+ if (HandleCapabilityChange(caps, wayland::seat_capability::pointer, GetName(), "pointer", m_pointer, std::bind(&wayland::seat_t::get_pointer, m_seat)))
+ {
+ HandlePointerCapability();
+ }
+ if (HandleCapabilityChange(caps, wayland::seat_capability::touch, GetName(), "touch", m_touch, std::bind(&wayland::seat_t::get_touch, m_seat)))
+ {
+ HandleTouchCapability();
+ }
+}
+
+void CSeat::SetCursor(std::uint32_t serial, wayland::surface_t const &surface, std::int32_t hotspotX, std::int32_t hotspotY)
+{
+ if (m_pointer)
+ {
+ m_pointer.set_cursor(serial, surface, hotspotX, hotspotY);
+ }
+}
+
+void CSeat::HandleKeyboardCapability()
+{
+ m_keyboard.on_keymap() = [this](wayland::keyboard_keymap_format format, int fd, std::uint32_t size)
+ {
+ KODI::UTILS::POSIX::CFileHandle fdGuard{fd};
+ KODI::UTILS::POSIX::CMmap mmap{nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0};
+ std::string keymap{static_cast<const char*> (mmap.Data()), size};
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardKeymap(this, format, keymap);
+ }
+ };
+ m_keyboard.on_enter() = [this](std::uint32_t serial, const wayland::surface_t& surface,
+ const wayland::array_t& keys) {
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardEnter(this, serial, surface, keys);
+ }
+ };
+ m_keyboard.on_leave() = [this](std::uint32_t serial, const wayland::surface_t& surface) {
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardLeave(this, serial, surface);
+ }
+ };
+ m_keyboard.on_key() = [this](std::uint32_t serial, std::uint32_t time, std::uint32_t key, wayland::keyboard_key_state state)
+ {
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardKey(this, serial, time, key, state);
+ }
+ };
+ m_keyboard.on_modifiers() = [this](std::uint32_t serial, std::uint32_t modsDepressed, std::uint32_t modsLatched, std::uint32_t modsLocked, std::uint32_t group)
+ {
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardModifiers(this, serial, modsDepressed, modsLatched, modsLocked, group);
+ }
+ };
+ m_keyboard.on_repeat_info() = [this](std::int32_t rate, std::int32_t delay)
+ {
+ for (auto handler : m_rawKeyboardHandlers)
+ {
+ handler->OnKeyboardRepeatInfo(this, rate, delay);
+ }
+ };
+}
+
+
+void CSeat::HandlePointerCapability()
+{
+ m_pointer.on_enter() = [this](std::uint32_t serial, const wayland::surface_t& surface,
+ double surfaceX, double surfaceY) {
+ for (auto handler : m_rawPointerHandlers)
+ {
+ handler->OnPointerEnter(this, serial, surface, surfaceX, surfaceY);
+ }
+ };
+ m_pointer.on_leave() = [this](std::uint32_t serial, const wayland::surface_t& surface) {
+ for (auto handler : m_rawPointerHandlers)
+ {
+ handler->OnPointerLeave(this, serial, surface);
+ }
+ };
+ m_pointer.on_motion() = [this](std::uint32_t time, double surfaceX, double surfaceY)
+ {
+ for (auto handler : m_rawPointerHandlers)
+ {
+ handler->OnPointerMotion(this, time, surfaceX, surfaceY);
+ }
+ };
+ m_pointer.on_button() = [this](std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state)
+ {
+ for (auto handler : m_rawPointerHandlers)
+ {
+ handler->OnPointerButton(this, serial, time, button, state);
+ }
+ };
+ m_pointer.on_axis() = [this](std::uint32_t time, wayland::pointer_axis axis, double value)
+ {
+ for (auto handler : m_rawPointerHandlers)
+ {
+ handler->OnPointerAxis(this, time, axis, value);
+ }
+ };
+ // Wayland groups pointer events, but right now there is no benefit in
+ // treating them in groups. The main use case for doing so seems to be
+ // multi-axis (i.e. diagonal) scrolling, but we do not support this anyway.
+ /*m_pointer.on_frame() = [this]()
+ {
+
+ };*/
+}
+
+void CSeat::HandleTouchCapability()
+{
+ m_touch.on_down() = [this](std::uint32_t serial, std::uint32_t time,
+ const wayland::surface_t& surface, std::int32_t id, double x,
+ double y) {
+ for (auto handler : m_rawTouchHandlers)
+ {
+ handler->OnTouchDown(this, serial, time, surface, id, x, y);
+ }
+ };
+ m_touch.on_up() = [this](std::uint32_t serial, std::uint32_t time, std::int32_t id)
+ {
+ for (auto handler : m_rawTouchHandlers)
+ {
+ handler->OnTouchUp(this, serial, time, id);
+ }
+ };
+ m_touch.on_motion() = [this](std::uint32_t time, std::int32_t id, double x, double y)
+ {
+ for (auto handler : m_rawTouchHandlers)
+ {
+ handler->OnTouchMotion(this, time, id, x, y);
+ }
+ };
+ m_touch.on_cancel() = [this]()
+ {
+ for (auto handler : m_rawTouchHandlers)
+ {
+ handler->OnTouchCancel(this);
+ }
+ };
+ m_touch.on_shape() = [this](std::int32_t id, double major, double minor)
+ {
+ for (auto handler : m_rawTouchHandlers)
+ {
+ handler->OnTouchShape(this, id, major, minor);
+ }
+ };
+}
diff --git a/xbmc/windowing/wayland/Seat.h b/xbmc/windowing/wayland/Seat.h
new file mode 100644
index 0000000..095f18d
--- /dev/null
+++ b/xbmc/windowing/wayland/Seat.h
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2017-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 "SeatSelection.h"
+
+#include <cstdint>
+#include <set>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CSeat;
+
+/**
+ * Handler for raw wl_keyboard events
+ *
+ * All functions are identical to wl_keyboard, except for the keymap which is
+ * retrieved from its fd and put into a string
+ */
+class IRawInputHandlerKeyboard
+{
+public:
+ virtual void OnKeyboardKeymap(CSeat* seat, wayland::keyboard_keymap_format format, std::string const& keymap) {}
+ virtual void OnKeyboardEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ const wayland::array_t& keys)
+ {
+ }
+ virtual void OnKeyboardLeave(CSeat* seat, std::uint32_t serial, const wayland::surface_t& surface)
+ {
+ }
+ virtual void OnKeyboardKey(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t key, wayland::keyboard_key_state state) {}
+ virtual void OnKeyboardModifiers(CSeat* seat, std::uint32_t serial, std::uint32_t modsDepressed, std::uint32_t modsLatched, std::uint32_t modsLocked, std::uint32_t group) {}
+ virtual void OnKeyboardRepeatInfo(CSeat* seat, std::int32_t rate, std::int32_t delay) {}
+protected:
+ ~IRawInputHandlerKeyboard() = default;
+};
+
+/**
+ * Handler for raw wl_pointer events
+ *
+ * All functions are identical to wl_pointer
+ */
+class IRawInputHandlerPointer
+{
+public:
+ virtual void OnPointerEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ double surfaceX,
+ double surfaceY)
+ {
+ }
+ virtual void OnPointerLeave(CSeat* seat, std::uint32_t serial, const wayland::surface_t& surface)
+ {
+ }
+ virtual void OnPointerMotion(CSeat* seat, std::uint32_t time, double surfaceX, double surfaceY) {}
+ virtual void OnPointerButton(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state) {}
+ virtual void OnPointerAxis(CSeat* seat, std::uint32_t time, wayland::pointer_axis axis, double value) {}
+protected:
+ ~IRawInputHandlerPointer() = default;
+};
+
+/**
+ * Handler for raw wl_touch events
+ *
+ * All functions are identical to wl_touch
+ */
+class IRawInputHandlerTouch
+{
+public:
+ virtual void OnTouchDown(CSeat* seat,
+ std::uint32_t serial,
+ std::uint32_t time,
+ const wayland::surface_t& surface,
+ std::int32_t id,
+ double x,
+ double y)
+ {
+ }
+ virtual void OnTouchUp(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::int32_t id) {}
+ virtual void OnTouchMotion(CSeat* seat, std::uint32_t time, std::int32_t id, double x, double y) {}
+ virtual void OnTouchCancel(CSeat* seat) {}
+ virtual void OnTouchShape(CSeat* seat, std::int32_t id, double major, double minor) {}
+protected:
+ ~IRawInputHandlerTouch() = default;
+};
+
+/**
+ * Handle all events and requests related to one seat (including input and selection)
+ *
+ * The primary purpose of this class is to act as entry point of Wayland events into
+ * the Kodi world and distribute them further as necessary.
+ * Input events are forwarded to (potentially multiple) handlers. As the Wayland
+ * protocol is not very specific on having multiple wl_seat/wl_pointer instances
+ * and how they interact, having one central instance and then handling everything
+ * in Kodi with multiple handlers is better than each handler having its own
+ * protocol object instance.
+ */
+class CSeat
+{
+public:
+ /**
+ * Construct seat handler
+ * \param globalName Wayland numeric global name of the seat
+ * \param seat bound seat_t instance
+ * \param connection connection for retrieving additional globals
+ */
+ CSeat(std::uint32_t globalName, wayland::seat_t const& seat, CConnection& connection);
+ ~CSeat() noexcept;
+
+ void AddRawInputHandlerKeyboard(IRawInputHandlerKeyboard* rawKeyboardHandler);
+ void RemoveRawInputHandlerKeyboard(IRawInputHandlerKeyboard* rawKeyboardHandler);
+ void AddRawInputHandlerPointer(IRawInputHandlerPointer* rawPointerHandler);
+ void RemoveRawInputHandlerPointer(IRawInputHandlerPointer* rawPointerHandler);
+ void AddRawInputHandlerTouch(IRawInputHandlerTouch* rawTouchHandler);
+ void RemoveRawInputHandlerTouch(IRawInputHandlerTouch* rawTouchHandler);
+
+ std::uint32_t GetGlobalName() const
+ {
+ return m_globalName;
+ }
+ std::string const& GetName() const
+ {
+ return m_name;
+ }
+ bool HasPointerCapability() const
+ {
+ return !!m_pointer;
+ }
+ bool HasKeyboardCapability() const
+ {
+ return !!m_keyboard;
+ }
+ bool HasTouchCapability() const
+ {
+ return !!m_touch;
+ }
+ std::string GetSelectionText() const
+ {
+ return m_selection.GetSelectionText();
+ }
+ /**
+ * Get the wl_seat underlying this seat
+ *
+ * The wl_seat should only be used when strictly necessary, e.g. when
+ * starting a move operation with shell interfaces.
+ * It may not be used to derive further wl_pointer etc. instances.
+ */
+ wayland::seat_t const& GetWlSeat()
+ {
+ return m_seat;
+ }
+
+ /**
+ * Set the cursor of the pointer of this seat
+ *
+ * Parameters are identical wo wl_pointer.set_cursor().
+ * If the seat does not currently have the pointer capability, this is a no-op.
+ */
+ void SetCursor(std::uint32_t serial, wayland::surface_t const& surface, std::int32_t hotspotX, std::int32_t hotspotY);
+
+private:
+ CSeat(CSeat const& other) = delete;
+ CSeat& operator=(CSeat const& other) = delete;
+
+ void HandleOnCapabilities(const wayland::seat_capability& caps);
+ void HandlePointerCapability();
+ void HandleKeyboardCapability();
+ void HandleTouchCapability();
+
+ std::uint32_t m_globalName;
+ std::string m_name{"<unknown>"};
+
+ wayland::seat_t m_seat;
+ wayland::pointer_t m_pointer;
+ wayland::keyboard_t m_keyboard;
+ wayland::touch_t m_touch;
+
+ std::set<IRawInputHandlerKeyboard*> m_rawKeyboardHandlers;
+ std::set<IRawInputHandlerPointer*> m_rawPointerHandlers;
+ std::set<IRawInputHandlerTouch*> m_rawTouchHandlers;
+
+ CSeatSelection m_selection;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/SeatInputProcessing.cpp b/xbmc/windowing/wayland/SeatInputProcessing.cpp
new file mode 100644
index 0000000..6430aad
--- /dev/null
+++ b/xbmc/windowing/wayland/SeatInputProcessing.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 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 "SeatInputProcessing.h"
+
+#include <cassert>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+CSeatInputProcessing::CSeatInputProcessing(wayland::surface_t const& inputSurface, IInputHandler& handler)
+: m_inputSurface{inputSurface}, m_handler{handler}
+{
+}
+
+void CSeatInputProcessing::AddSeat(CSeat* seat)
+{
+ assert(m_seats.find(seat->GetGlobalName()) == m_seats.end());
+ auto& seatState = m_seats.emplace(seat->GetGlobalName(), seat).first->second;
+
+ seatState.keyboardProcessor.reset(new CInputProcessorKeyboard(*this));
+ seat->AddRawInputHandlerKeyboard(seatState.keyboardProcessor.get());
+ seatState.pointerProcessor.reset(new CInputProcessorPointer(m_inputSurface, *this));
+ seat->AddRawInputHandlerPointer(seatState.pointerProcessor.get());
+ seatState.touchProcessor.reset(new CInputProcessorTouch(m_inputSurface));
+ seat->AddRawInputHandlerTouch(seatState.touchProcessor.get());
+}
+
+void CSeatInputProcessing::RemoveSeat(CSeat* seat)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI != m_seats.end())
+ {
+ seat->RemoveRawInputHandlerKeyboard(seatStateI->second.keyboardProcessor.get());
+ seat->RemoveRawInputHandlerPointer(seatStateI->second.pointerProcessor.get());
+ seat->RemoveRawInputHandlerTouch(seatStateI->second.touchProcessor.get());
+ m_seats.erase(seatStateI);
+ }
+}
+
+void CSeatInputProcessing::OnPointerEnter(std::uint32_t seatGlobalName, std::uint32_t serial)
+{
+ m_handler.OnSetCursor(seatGlobalName, serial);
+ m_handler.OnEnter(InputType::POINTER);
+}
+
+void CSeatInputProcessing::OnPointerLeave()
+{
+ m_handler.OnLeave(InputType::POINTER);
+}
+
+void CSeatInputProcessing::OnPointerEvent(XBMC_Event& event)
+{
+ m_handler.OnEvent(InputType::POINTER, event);
+}
+
+void CSeatInputProcessing::OnKeyboardEnter()
+{
+ m_handler.OnEnter(InputType::KEYBOARD);
+}
+
+void CSeatInputProcessing::OnKeyboardLeave()
+{
+ m_handler.OnLeave(InputType::KEYBOARD);
+}
+
+void CSeatInputProcessing::OnKeyboardEvent(XBMC_Event& event)
+{
+ m_handler.OnEvent(InputType::KEYBOARD, event);
+}
+
+void CSeatInputProcessing::SetCoordinateScale(std::int32_t scale)
+{
+ for (auto& seatPair : m_seats)
+ {
+ seatPair.second.touchProcessor->SetCoordinateScale(scale);
+ seatPair.second.pointerProcessor->SetCoordinateScale(scale);
+ }
+}
diff --git a/xbmc/windowing/wayland/SeatInputProcessing.h b/xbmc/windowing/wayland/SeatInputProcessing.h
new file mode 100644
index 0000000..ce1f0ee
--- /dev/null
+++ b/xbmc/windowing/wayland/SeatInputProcessing.h
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 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 "InputProcessorKeyboard.h"
+#include "InputProcessorPointer.h"
+#include "InputProcessorTouch.h"
+#include "Seat.h"
+
+#include <cstdint>
+#include <map>
+#include <memory>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+enum class InputType
+{
+ POINTER,
+ KEYBOARD,
+ TOUCH
+};
+
+/**
+ * Handler interface for input events from \ref CSeatInputProcessor
+ */
+class IInputHandler
+{
+public:
+ /**
+ * Handle input event
+ * \param type input device type that caused the event
+ * \param event XBMC event data
+ */
+ virtual void OnEvent(InputType type, XBMC_Event& event) {}
+ /**
+ * Handle focus enter
+ * \param type input device type for which the surface has gained the focus
+ */
+ virtual void OnEnter(InputType type) {}
+ /**
+ * Handle focus leave
+ * \param type input device type for which the surface has lost the focus
+ */
+ virtual void OnLeave(InputType type) {}
+ /**
+ * Handle request for setting the cursor
+ *
+ * When the client gains pointer focus for a surface, a cursor image must be
+ * attached to the pointer. Otherwise the previous pointer image would
+ * be used.
+ *
+ * This request is sent in addition to \ref OnEnter for \ref InputType::POINTER.
+ *
+ * \param seatGlobalName numeric Wayland global name of the seat the event occurred on
+ * \param pointer pointer instance that needs its cursor set
+ * \param serial Wayland protocol message serial that must be sent back in set_cursor
+ */
+ virtual void OnSetCursor(std::uint32_t seatGlobalName, std::uint32_t serial) {}
+
+ virtual ~IInputHandler() = default;
+};
+
+/**
+ * Receive events from all registered wl_seats and process them into Kodi events
+ *
+ * Multi-seat support is not currently implemented completely, but each seat has
+ * separate state.
+ */
+class CSeatInputProcessing final : IInputHandlerPointer, IInputHandlerKeyboard
+{
+public:
+ /**
+ * Construct a seat input processor
+ *
+ * \param inputSurface Surface that events should be processed on (all other surfaces are ignored)
+ * \param handler Mandatory handler for processed input events
+ */
+ CSeatInputProcessing(wayland::surface_t const& inputSurface, IInputHandler& handler);
+ void AddSeat(CSeat* seat);
+ void RemoveSeat(CSeat* seat);
+
+ /**
+ * Set the scale the coordinates should be interpreted at
+ *
+ * Wayland input events are always in surface coordinates, but Kodi only uses
+ * buffer coordinates internally. Use this function to set the scaling
+ * factor between the two and multiply the surface coordinates accordingly
+ * for Kodi events.
+ *
+ * \param scale new buffer-to-surface pixel ratio
+ */
+ void SetCoordinateScale(std::int32_t scale);
+
+private:
+ wayland::surface_t m_inputSurface;
+ IInputHandler& m_handler;
+
+ void OnPointerEnter(std::uint32_t seatGlobalName, std::uint32_t serial) override;
+ void OnPointerLeave() override;
+ void OnPointerEvent(XBMC_Event& event) override;
+
+ void OnKeyboardEnter() override;
+ void OnKeyboardLeave() override;
+ void OnKeyboardEvent(XBMC_Event& event) override;
+
+ struct SeatState
+ {
+ CSeat* seat;
+ std::unique_ptr<CInputProcessorKeyboard> keyboardProcessor;
+ std::unique_ptr<CInputProcessorPointer> pointerProcessor;
+ std::unique_ptr<CInputProcessorTouch> touchProcessor;
+
+ SeatState(CSeat* seat)
+ : seat{seat}
+ {}
+ };
+ std::map<std::uint32_t, SeatState> m_seats;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/SeatSelection.cpp b/xbmc/windowing/wayland/SeatSelection.cpp
new file mode 100644
index 0000000..f66555a
--- /dev/null
+++ b/xbmc/windowing/wayland/SeatSelection.cpp
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2017-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 "SeatSelection.h"
+
+#include "Connection.h"
+#include "Registry.h"
+#include "WinEventsWayland.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include "platform/posix/utils/FileHandle.h"
+
+#include <cerrno>
+#include <chrono>
+#include <cstring>
+#include <mutex>
+#include <system_error>
+#include <utility>
+
+#include <poll.h>
+#include <unistd.h>
+
+using namespace KODI::UTILS::POSIX;
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+const std::vector<std::string> MIME_TYPES_PREFERENCE =
+{
+ "text/plain;charset=utf-8",
+ "text/plain;charset=iso-8859-1",
+ "text/plain;charset=us-ascii",
+ "text/plain"
+};
+
+}
+
+CSeatSelection::CSeatSelection(CConnection& connection, wayland::seat_t const& seat)
+{
+ wayland::data_device_manager_t manager;
+ {
+ CRegistry registry{connection};
+ registry.RequestSingleton(manager, 1, 3, false);
+ registry.Bind();
+ }
+
+ if (!manager)
+ {
+ CLog::Log(LOGWARNING, "No data device manager announced by compositor, clipboard will not be available");
+ return;
+ }
+
+ m_dataDevice = manager.get_data_device(seat);
+
+ // Class is created in response to seat add events - so no events can get lost
+ m_dataDevice.on_data_offer() = [this](wayland::data_offer_t offer)
+ {
+ // We don't know yet whether this is drag-and-drop or selection, so collect
+ // MIME types in either case
+ m_currentOffer = std::move(offer);
+ m_mimeTypeOffers.clear();
+ m_currentOffer.on_offer() = [this](std::string mime)
+ {
+ m_mimeTypeOffers.push_back(std::move(mime));
+ };
+ };
+ m_dataDevice.on_selection() = [this](const wayland::data_offer_t& offer)
+ {
+ std::unique_lock<CCriticalSection> lock(m_currentSelectionMutex);
+ m_matchedMimeType.clear();
+
+ if (offer != m_currentOffer)
+ {
+ // Selection was not previously introduced by offer (could be NULL for example)
+ m_currentSelection.proxy_release();
+ }
+ else
+ {
+ m_currentSelection = offer;
+ std::string offers = StringUtils::Join(m_mimeTypeOffers, ", ");
+
+ // Match MIME type by priority: Find first preferred MIME type that is in the
+ // set of offered types
+ // Charset is not case-sensitive in MIME type spec, so match case-insensitively
+ auto mimeIt = std::find_first_of(MIME_TYPES_PREFERENCE.cbegin(), MIME_TYPES_PREFERENCE.cend(),
+ m_mimeTypeOffers.cbegin(), m_mimeTypeOffers.cend(),
+ // static_cast needed for overload resolution
+ static_cast<bool (*)(std::string const&, std::string const&)> (&StringUtils::EqualsNoCase));
+ if (mimeIt != MIME_TYPES_PREFERENCE.cend())
+ {
+ m_matchedMimeType = *mimeIt;
+ CLog::Log(LOGDEBUG, "Chose selection MIME type {} out of offered {}", m_matchedMimeType,
+ offers);
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "Could not find compatible MIME type for selection data (offered: {})",
+ offers);
+ }
+ }
+ };
+}
+
+std::string CSeatSelection::GetSelectionText() const
+{
+ std::unique_lock<CCriticalSection> lock(m_currentSelectionMutex);
+ if (!m_currentSelection || m_matchedMimeType.empty())
+ {
+ return "";
+ }
+
+ std::array<int, 2> fds;
+ if (pipe(fds.data()) != 0)
+ {
+ CLog::LogF(LOGERROR, "Could not open pipe for selection data transfer: {}",
+ std::strerror(errno));
+ return "";
+ }
+
+ CFileHandle readFd{fds[0]};
+ CFileHandle writeFd{fds[1]};
+
+ m_currentSelection.receive(m_matchedMimeType, writeFd);
+ lock.unlock();
+ // Make sure the other party gets the request as soon as possible
+ CWinEventsWayland::Flush();
+ // Fd now gets sent to the other party -> make sure our write end is closed
+ // so we get POLLHUP when the other party closes its write fd
+ writeFd.reset();
+
+ pollfd fd =
+ {
+ .fd = readFd,
+ .events = POLLIN,
+ .revents = 0
+ };
+
+ // UI will block in this function when Ctrl+V is pressed, so timeout should be
+ // rather short!
+ const std::chrono::seconds TIMEOUT{1};
+ const std::size_t MAX_SIZE{4096};
+ std::array<char, MAX_SIZE> buffer;
+
+ auto start = std::chrono::steady_clock::now();
+ std::size_t totalBytesRead{0};
+
+ do
+ {
+ auto now = std::chrono::steady_clock::now();
+ // Do not permit negative timeouts (would cause infinitely long poll)
+ auto remainingTimeout = std::max(std::chrono::milliseconds(0), std::chrono::duration_cast<std::chrono::milliseconds> (TIMEOUT - (now - start))).count();
+ // poll() for changes until poll signals POLLHUP and the remaining data was read
+ int ret{poll(&fd, 1, remainingTimeout)};
+ if (ret == 0)
+ {
+ // Timeout
+ CLog::LogF(LOGERROR, "Reading from selection data pipe timed out");
+ return "";
+ }
+ else if (ret < 0 && errno == EINTR)
+ {
+ continue;
+ }
+ else if (ret < 0)
+ {
+ throw std::system_error(errno, std::generic_category(), "Error polling selection pipe");
+ }
+ else if (fd.revents & POLLNVAL || fd.revents & POLLERR)
+ {
+ CLog::LogF(LOGERROR, "poll() indicated error on selection pipe");
+ return "";
+ }
+ else if (fd.revents & POLLIN)
+ {
+ if (totalBytesRead >= buffer.size())
+ {
+ CLog::LogF(LOGERROR, "Selection data is too big, aborting read");
+ return "";
+ }
+ ssize_t readBytes{read(fd.fd, buffer.data() + totalBytesRead, buffer.size() - totalBytesRead)};
+ if (readBytes < 0)
+ {
+ CLog::LogF(LOGERROR, "read() from selection pipe failed: {}", std::strerror(errno));
+ return "";
+ }
+ totalBytesRead += readBytes;
+ }
+ }
+ while (!(fd.revents & POLLHUP));
+
+ return std::string(buffer.data(), totalBytesRead);
+}
diff --git a/xbmc/windowing/wayland/SeatSelection.h b/xbmc/windowing/wayland/SeatSelection.h
new file mode 100644
index 0000000..ec99a0d
--- /dev/null
+++ b/xbmc/windowing/wayland/SeatSelection.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017-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 <string>
+#include <vector>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CConnection;
+
+/**
+ * Retrieve and accept selection (clipboard) offers on the data device of a seat
+ */
+class CSeatSelection
+{
+public:
+ CSeatSelection(CConnection& connection, wayland::seat_t const& seat);
+ std::string GetSelectionText() const;
+
+private:
+ wayland::data_device_t m_dataDevice;
+ wayland::data_offer_t m_currentOffer;
+ mutable wayland::data_offer_t m_currentSelection;
+
+ std::vector<std::string> m_mimeTypeOffers;
+ std::string m_matchedMimeType;
+
+ mutable CCriticalSection m_currentSelectionMutex;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/ShellSurface.cpp b/xbmc/windowing/wayland/ShellSurface.cpp
new file mode 100644
index 0000000..5836689
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurface.cpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017-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 "ShellSurface.h"
+
+#include "utils/StringUtils.h"
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+std::string IShellSurface::StateToString(StateBitset state)
+{
+ std::vector<std::string> parts;
+ if (state.test(STATE_ACTIVATED))
+ {
+ parts.emplace_back("activated");
+ }
+ if (state.test(STATE_FULLSCREEN))
+ {
+ parts.emplace_back("fullscreen");
+ }
+ if (state.test(STATE_MAXIMIZED))
+ {
+ parts.emplace_back("maximized");
+ }
+ if (state.test(STATE_RESIZING))
+ {
+ parts.emplace_back("resizing");
+ }
+ return parts.empty() ? "none" : StringUtils::Join(parts, ",");
+} \ No newline at end of file
diff --git a/xbmc/windowing/wayland/ShellSurface.h b/xbmc/windowing/wayland/ShellSurface.h
new file mode 100644
index 0000000..08690cc
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurface.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017-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 "utils/Geometry.h"
+
+#include <bitset>
+#include <cstdint>
+
+#include <wayland-client.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class IShellSurfaceHandler;
+
+/**
+ * Abstraction for shell surfaces to support multiple protocols
+ * such as wl_shell (for compatibility) and xdg_shell (for features)
+ *
+ * The interface itself is modeled after xdg_shell, so see there for the meaning
+ * of e.g. the surface states
+ */
+class IShellSurface
+{
+public:
+ // Not enum class since it must be used like a bitfield
+ enum State
+ {
+ STATE_MAXIMIZED = 0,
+ STATE_FULLSCREEN,
+ STATE_RESIZING,
+ STATE_ACTIVATED,
+ STATE_COUNT
+ };
+ using StateBitset = std::bitset<STATE_COUNT>;
+ static std::string StateToString(StateBitset state);
+
+ /**
+ * Initialize shell surface
+ *
+ * The event loop thread MUST NOT be running when this function is called.
+ * The difference to the constructor is that in this function callbacks may
+ * already be called.
+ */
+ virtual void Initialize() = 0;
+
+ virtual void SetFullScreen(wayland::output_t const& output, float refreshRate) = 0;
+ virtual void SetWindowed() = 0;
+ virtual void SetMaximized() = 0;
+ virtual void UnsetMaximized() = 0;
+ virtual void SetMinimized() = 0;
+ virtual void SetWindowGeometry(CRectInt geometry) = 0;
+
+ virtual void AckConfigure(std::uint32_t serial) = 0;
+
+ virtual void StartMove(wayland::seat_t const& seat, std::uint32_t serial) = 0;
+ virtual void StartResize(wayland::seat_t const& seat, std::uint32_t serial, wayland::shell_surface_resize edge) = 0;
+ virtual void ShowShellContextMenu(wayland::seat_t const& seat, std::uint32_t serial, CPointInt position) = 0;
+
+ virtual ~IShellSurface() = default;
+
+protected:
+ IShellSurface() noexcept = default;
+
+private:
+ IShellSurface(IShellSurface const& other) = delete;
+ IShellSurface& operator=(IShellSurface const& other) = delete;
+};
+
+class IShellSurfaceHandler
+{
+public:
+ virtual void OnConfigure(std::uint32_t serial, CSizeInt size, IShellSurface::StateBitset state) = 0;
+ virtual void OnClose() = 0;
+
+ virtual ~IShellSurfaceHandler() = default;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/ShellSurfaceWlShell.cpp b/xbmc/windowing/wayland/ShellSurfaceWlShell.cpp
new file mode 100644
index 0000000..2c91858
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceWlShell.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2017-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 "ShellSurfaceWlShell.h"
+
+#include "Registry.h"
+
+#include <cmath>
+
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace std::placeholders;
+
+CShellSurfaceWlShell::CShellSurfaceWlShell(IShellSurfaceHandler& handler,
+ CConnection& connection,
+ const wayland::surface_t& surface,
+ const std::string& title,
+ const std::string& class_)
+ : m_handler{handler}
+{
+ {
+ CRegistry registry{connection};
+ registry.RequestSingleton(m_shell, 1, 1);
+ registry.Bind();
+ }
+
+ m_shellSurface = m_shell.get_shell_surface(surface);
+
+ m_surfaceState.set(STATE_ACTIVATED);
+ m_shellSurface.set_class(class_);
+ m_shellSurface.set_title(title);
+ m_shellSurface.on_ping() = [this](std::uint32_t serial)
+ {
+ m_shellSurface.pong(serial);
+ };
+ m_shellSurface.on_configure() = [this](const wayland::shell_surface_resize&, std::int32_t width,
+ std::int32_t height) {
+ // wl_shell does not have serials
+ m_handler.OnConfigure(0, {width, height}, m_surfaceState);
+ };
+}
+
+void CShellSurfaceWlShell::AckConfigure(std::uint32_t)
+{
+}
+
+void CShellSurfaceWlShell::Initialize()
+{
+ // Nothing to do here - constructor already handles it
+ // This is not a problem since the constructor is guaranteed not to call
+ // handler functions since the event loop is not running.
+}
+
+void CShellSurfaceWlShell::SetFullScreen(const wayland::output_t& output, float refreshRate)
+{
+ m_shellSurface.set_fullscreen(wayland::shell_surface_fullscreen_method::driver, std::round(refreshRate * 1000.0f), output);
+ m_surfaceState.set(STATE_FULLSCREEN);
+}
+
+void CShellSurfaceWlShell::SetWindowed()
+{
+ m_shellSurface.set_toplevel();
+ m_surfaceState.reset(STATE_FULLSCREEN);
+}
+
+void CShellSurfaceWlShell::SetMaximized()
+{
+ m_shellSurface.set_maximized(wayland::output_t());
+ m_surfaceState.set(STATE_MAXIMIZED);
+}
+
+void CShellSurfaceWlShell::UnsetMaximized()
+{
+ m_surfaceState.reset(STATE_MAXIMIZED);
+}
+
+void CShellSurfaceWlShell::SetMinimized()
+{
+}
+
+void CShellSurfaceWlShell::SetWindowGeometry(CRectInt)
+{
+}
+
+void CShellSurfaceWlShell::StartMove(const wayland::seat_t& seat, std::uint32_t serial)
+{
+ m_shellSurface.move(seat, serial);
+}
+
+void CShellSurfaceWlShell::StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge)
+{
+ m_shellSurface.resize(seat, serial, edge);
+}
+
+void CShellSurfaceWlShell::ShowShellContextMenu(const wayland::seat_t&, std::uint32_t, CPointInt)
+{
+}
diff --git a/xbmc/windowing/wayland/ShellSurfaceWlShell.h b/xbmc/windowing/wayland/ShellSurfaceWlShell.h
new file mode 100644
index 0000000..8b60d60
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceWlShell.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017-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 "Connection.h"
+#include "ShellSurface.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CShellSurfaceWlShell : public IShellSurface
+{
+public:
+ /**
+ * Construct wl_shell_surface for given surface
+ *
+ * \parma handler shell surface handler
+ * \param connection connection global
+ * \param surface surface to make shell surface for
+ * \param title title of the surfae
+ * \param class_ class of the surface, which should match the name of the
+ * .desktop file of the application
+ */
+ CShellSurfaceWlShell(IShellSurfaceHandler& handler,
+ CConnection& connection,
+ wayland::surface_t const& surface,
+ const std::string& title,
+ const std::string& class_);
+
+ void Initialize() override;
+
+ void SetFullScreen(wayland::output_t const& output, float refreshRate) override;
+ void SetWindowed() override;
+ void SetMaximized() override;
+ void UnsetMaximized() override;
+ void SetMinimized() override;
+ void SetWindowGeometry(CRectInt geometry) override;
+ void AckConfigure(std::uint32_t serial) override;
+
+ void StartMove(const wayland::seat_t& seat, std::uint32_t serial) override;
+ void StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge) override;
+ void ShowShellContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position) override;
+
+private:
+ IShellSurfaceHandler& m_handler;
+ wayland::shell_t m_shell;
+ wayland::shell_surface_t m_shellSurface;
+ StateBitset m_surfaceState;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/ShellSurfaceXdgShell.cpp b/xbmc/windowing/wayland/ShellSurfaceXdgShell.cpp
new file mode 100644
index 0000000..e92d33c
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceXdgShell.cpp
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2017-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 "ShellSurfaceXdgShell.h"
+
+#include "Registry.h"
+#include "messaging/ApplicationMessenger.h"
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+IShellSurface::State ConvertStateFlag(wayland::xdg_toplevel_state flag)
+{
+ switch(flag)
+ {
+ case wayland::xdg_toplevel_state::activated:
+ return IShellSurface::STATE_ACTIVATED;
+ case wayland::xdg_toplevel_state::fullscreen:
+ return IShellSurface::STATE_FULLSCREEN;
+ case wayland::xdg_toplevel_state::maximized:
+ return IShellSurface::STATE_MAXIMIZED;
+ case wayland::xdg_toplevel_state::resizing:
+ return IShellSurface::STATE_RESIZING;
+ default:
+ throw std::runtime_error(std::string("Unknown xdg_toplevel state flag ") + std::to_string(static_cast<std::underlying_type<decltype(flag)>::type> (flag)));
+ }
+}
+
+}
+
+CShellSurfaceXdgShell* CShellSurfaceXdgShell::TryCreate(IShellSurfaceHandler& handler, CConnection& connection, const wayland::surface_t& surface, std::string const& title, std::string const& class_)
+{
+ wayland::xdg_wm_base_t shell;
+ CRegistry registry{connection};
+ registry.RequestSingleton(shell, 1, 1, false);
+ registry.Bind();
+
+ if (shell)
+ {
+ return new CShellSurfaceXdgShell(handler, connection.GetDisplay(), shell, surface, title, class_);
+ }
+ else
+ {
+ return nullptr;
+ }
+}
+
+CShellSurfaceXdgShell::CShellSurfaceXdgShell(IShellSurfaceHandler& handler, wayland::display_t& display, const wayland::xdg_wm_base_t& shell, const wayland::surface_t& surface, std::string const& title, std::string const& app_id)
+: m_handler{handler}, m_display{display}, m_shell{shell}, m_surface{surface}, m_xdgSurface{m_shell.get_xdg_surface(m_surface)}, m_xdgToplevel{m_xdgSurface.get_toplevel()}
+{
+ m_shell.on_ping() = [this](std::uint32_t serial)
+ {
+ m_shell.pong(serial);
+ };
+ m_xdgSurface.on_configure() = [this](std::uint32_t serial)
+ {
+ m_handler.OnConfigure(serial, m_configuredSize, m_configuredState);
+ };
+ m_xdgToplevel.on_close() = [this]()
+ {
+ m_handler.OnClose();
+ };
+ m_xdgToplevel.on_configure() = [this](std::int32_t width, std::int32_t height,
+ const std::vector<wayland::xdg_toplevel_state>& states) {
+ m_configuredSize.Set(width, height);
+ m_configuredState.reset();
+ for (auto state : states)
+ {
+ m_configuredState.set(ConvertStateFlag(state));
+ }
+ };
+ m_xdgToplevel.set_app_id(app_id);
+ m_xdgToplevel.set_title(title);
+ // Set sensible minimum size
+ m_xdgToplevel.set_min_size(300, 200);
+}
+
+void CShellSurfaceXdgShell::Initialize()
+{
+ // Commit surface to confirm role
+ // Don't do it in constructor since SetFullScreen might be called before
+ m_surface.commit();
+ // Make sure we get the initial configure before continuing
+ m_display.roundtrip();
+}
+
+void CShellSurfaceXdgShell::AckConfigure(std::uint32_t serial)
+{
+ m_xdgSurface.ack_configure(serial);
+}
+
+CShellSurfaceXdgShell::~CShellSurfaceXdgShell() noexcept
+{
+ // xdg_shell is picky: must destroy toplevel role before surface
+ m_xdgToplevel.proxy_release();
+ m_xdgSurface.proxy_release();
+}
+
+void CShellSurfaceXdgShell::SetFullScreen(const wayland::output_t& output, float)
+{
+ // xdg_shell does not support refresh rate setting at the moment
+ m_xdgToplevel.set_fullscreen(output);
+}
+
+void CShellSurfaceXdgShell::SetWindowed()
+{
+ m_xdgToplevel.unset_fullscreen();
+}
+
+void CShellSurfaceXdgShell::SetMaximized()
+{
+ m_xdgToplevel.set_maximized();
+}
+
+void CShellSurfaceXdgShell::UnsetMaximized()
+{
+ m_xdgToplevel.unset_maximized();
+}
+
+void CShellSurfaceXdgShell::SetMinimized()
+{
+ m_xdgToplevel.set_minimized();
+}
+
+void CShellSurfaceXdgShell::SetWindowGeometry(CRectInt geometry)
+{
+ m_xdgSurface.set_window_geometry(geometry.x1, geometry.y1, geometry.Width(), geometry.Height());
+}
+
+void CShellSurfaceXdgShell::StartMove(const wayland::seat_t& seat, std::uint32_t serial)
+{
+ m_xdgToplevel.move(seat, serial);
+}
+
+void CShellSurfaceXdgShell::StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge)
+{
+ // wl_shell shell_surface_resize is identical to xdg_shell resize_edge
+ m_xdgToplevel.resize(seat, serial, static_cast<std::uint32_t> (edge));
+}
+
+void CShellSurfaceXdgShell::ShowShellContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position)
+{
+ m_xdgToplevel.show_window_menu(seat, serial, position.x, position.y);
+}
diff --git a/xbmc/windowing/wayland/ShellSurfaceXdgShell.h b/xbmc/windowing/wayland/ShellSurfaceXdgShell.h
new file mode 100644
index 0000000..718b572
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceXdgShell.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017-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 "Connection.h"
+#include "ShellSurface.h"
+
+#include <wayland-extra-protocols.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Shell surface implementation for stable xdg_shell
+ */
+class CShellSurfaceXdgShell : public IShellSurface
+{
+public:
+ /**
+ * Construct xdg_shell toplevel object for given surface
+ *
+ * \param handler the shell surface handler
+ * \param display the wl_display global (for initial roundtrip)
+ * \param shell the xdg_wm_base global
+ * \param surface surface to make shell surface for
+ * \param title title of the surfae
+ * \param class_ class of the surface, which should match the name of the
+ * .desktop file of the application
+ */
+ CShellSurfaceXdgShell(IShellSurfaceHandler& handler, wayland::display_t& display, wayland::xdg_wm_base_t const& shell, wayland::surface_t const& surface, std::string const& title, std::string const& class_);
+ ~CShellSurfaceXdgShell() noexcept override;
+
+ static CShellSurfaceXdgShell* TryCreate(IShellSurfaceHandler& handler, CConnection& connection, wayland::surface_t const& surface, std::string const& title, std::string const& class_);
+
+ void Initialize() override;
+
+ void SetFullScreen(wayland::output_t const& output, float refreshRate) override;
+ void SetWindowed() override;
+ void SetMaximized() override;
+ void UnsetMaximized() override;
+ void SetMinimized() override;
+ void SetWindowGeometry(CRectInt geometry) override;
+ void AckConfigure(std::uint32_t serial) override;
+
+ void StartMove(const wayland::seat_t& seat, std::uint32_t serial) override;
+ void StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge) override;
+ void ShowShellContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position) override;
+
+private:
+ IShellSurfaceHandler& m_handler;
+ wayland::display_t& m_display;
+ wayland::xdg_wm_base_t m_shell;
+ wayland::surface_t m_surface;
+ wayland::xdg_surface_t m_xdgSurface;
+ wayland::xdg_toplevel_t m_xdgToplevel;
+
+ CSizeInt m_configuredSize;
+ StateBitset m_configuredState;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp
new file mode 100644
index 0000000..ca68b98
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.cpp
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017-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 "ShellSurfaceXdgShellUnstableV6.h"
+
+#include "Registry.h"
+#include "messaging/ApplicationMessenger.h"
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+IShellSurface::State ConvertStateFlag(wayland::zxdg_toplevel_v6_state flag)
+{
+ switch(flag)
+ {
+ case wayland::zxdg_toplevel_v6_state::activated:
+ return IShellSurface::STATE_ACTIVATED;
+ case wayland::zxdg_toplevel_v6_state::fullscreen:
+ return IShellSurface::STATE_FULLSCREEN;
+ case wayland::zxdg_toplevel_v6_state::maximized:
+ return IShellSurface::STATE_MAXIMIZED;
+ case wayland::zxdg_toplevel_v6_state::resizing:
+ return IShellSurface::STATE_RESIZING;
+ default:
+ throw std::runtime_error(std::string("Unknown xdg_toplevel state flag ") + std::to_string(static_cast<std::underlying_type<decltype(flag)>::type> (flag)));
+ }
+}
+
+}
+
+CShellSurfaceXdgShellUnstableV6* CShellSurfaceXdgShellUnstableV6::TryCreate(IShellSurfaceHandler& handler, CConnection& connection, const wayland::surface_t& surface, std::string const& title, std::string const& class_)
+{
+ wayland::zxdg_shell_v6_t shell;
+ CRegistry registry{connection};
+ registry.RequestSingleton(shell, 1, 1, false);
+ registry.Bind();
+
+ if (shell)
+ {
+ return new CShellSurfaceXdgShellUnstableV6(handler, connection.GetDisplay(), shell, surface, title, class_);
+ }
+ else
+ {
+ return nullptr;
+ }
+}
+
+CShellSurfaceXdgShellUnstableV6::CShellSurfaceXdgShellUnstableV6(IShellSurfaceHandler& handler, wayland::display_t& display, const wayland::zxdg_shell_v6_t& shell, const wayland::surface_t& surface, std::string const& title, std::string const& app_id)
+: m_handler{handler}, m_display{display}, m_shell{shell}, m_surface{surface}, m_xdgSurface{m_shell.get_xdg_surface(m_surface)}, m_xdgToplevel{m_xdgSurface.get_toplevel()}
+{
+ m_shell.on_ping() = [this](std::uint32_t serial)
+ {
+ m_shell.pong(serial);
+ };
+ m_xdgSurface.on_configure() = [this](std::uint32_t serial)
+ {
+ m_handler.OnConfigure(serial, m_configuredSize, m_configuredState);
+ };
+ m_xdgToplevel.on_close() = [this]()
+ {
+ m_handler.OnClose();
+ };
+ m_xdgToplevel.on_configure() =
+ [this](std::int32_t width, std::int32_t height,
+ const std::vector<wayland::zxdg_toplevel_v6_state>& states) {
+ m_configuredSize.Set(width, height);
+ m_configuredState.reset();
+ for (auto state : states)
+ {
+ m_configuredState.set(ConvertStateFlag(state));
+ }
+ };
+ m_xdgToplevel.set_app_id(app_id);
+ m_xdgToplevel.set_title(title);
+ // Set sensible minimum size
+ m_xdgToplevel.set_min_size(300, 200);
+}
+
+void CShellSurfaceXdgShellUnstableV6::Initialize()
+{
+ // Commit surface to confirm role
+ // Don't do it in constructor since SetFullScreen might be called before
+ m_surface.commit();
+ // Make sure we get the initial configure before continuing
+ m_display.roundtrip();
+}
+
+void CShellSurfaceXdgShellUnstableV6::AckConfigure(std::uint32_t serial)
+{
+ m_xdgSurface.ack_configure(serial);
+}
+
+CShellSurfaceXdgShellUnstableV6::~CShellSurfaceXdgShellUnstableV6() noexcept
+{
+ // xdg_shell is picky: must destroy toplevel role before surface
+ m_xdgToplevel.proxy_release();
+ m_xdgSurface.proxy_release();
+}
+
+void CShellSurfaceXdgShellUnstableV6::SetFullScreen(const wayland::output_t& output, float)
+{
+ // xdg_shell does not support refresh rate setting at the moment
+ m_xdgToplevel.set_fullscreen(output);
+}
+
+void CShellSurfaceXdgShellUnstableV6::SetWindowed()
+{
+ m_xdgToplevel.unset_fullscreen();
+}
+
+void CShellSurfaceXdgShellUnstableV6::SetMaximized()
+{
+ m_xdgToplevel.set_maximized();
+}
+
+void CShellSurfaceXdgShellUnstableV6::UnsetMaximized()
+{
+ m_xdgToplevel.unset_maximized();
+}
+
+void CShellSurfaceXdgShellUnstableV6::SetMinimized()
+{
+ m_xdgToplevel.set_minimized();
+}
+
+void CShellSurfaceXdgShellUnstableV6::SetWindowGeometry(CRectInt geometry)
+{
+ m_xdgSurface.set_window_geometry(geometry.x1, geometry.y1, geometry.Width(), geometry.Height());
+}
+
+void CShellSurfaceXdgShellUnstableV6::StartMove(const wayland::seat_t& seat, std::uint32_t serial)
+{
+ m_xdgToplevel.move(seat, serial);
+}
+
+void CShellSurfaceXdgShellUnstableV6::StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge)
+{
+ // wl_shell shell_surface_resize is identical to xdg_shell resize_edge
+ m_xdgToplevel.resize(seat, serial, static_cast<std::uint32_t> (edge));
+}
+
+void CShellSurfaceXdgShellUnstableV6::ShowShellContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position)
+{
+ m_xdgToplevel.show_window_menu(seat, serial, position.x, position.y);
+} \ No newline at end of file
diff --git a/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h
new file mode 100644
index 0000000..d84f4a5
--- /dev/null
+++ b/xbmc/windowing/wayland/ShellSurfaceXdgShellUnstableV6.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017-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 "Connection.h"
+#include "ShellSurface.h"
+
+#include <wayland-extra-protocols.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Shell surface implementation for unstable xdg_shell in version 6
+ *
+ * xdg_shell was accepted as a stable protocol in wayland-protocols, which
+ * means this class is deprecated and can be safely removed once the relevant
+ * compositors have made the switch.
+ */
+class CShellSurfaceXdgShellUnstableV6 : public IShellSurface
+{
+public:
+ /**
+ * Construct xdg_shell toplevel object for given surface
+ *
+ * \param handler the shell surface handler
+ * \param display the wl_display global (for initial roundtrip)
+ * \param shell the zxdg_shell_v6 global
+ * \param surface surface to make shell surface for
+ * \param title title of the surfae
+ * \param class_ class of the surface, which should match the name of the
+ * .desktop file of the application
+ */
+ CShellSurfaceXdgShellUnstableV6(IShellSurfaceHandler& handler, wayland::display_t& display, wayland::zxdg_shell_v6_t const& shell, wayland::surface_t const& surface, std::string const& title, std::string const& class_);
+ ~CShellSurfaceXdgShellUnstableV6() noexcept override;
+
+ static CShellSurfaceXdgShellUnstableV6* TryCreate(IShellSurfaceHandler& handler, CConnection& connection, wayland::surface_t const& surface, std::string const& title, std::string const& class_);
+
+ void Initialize() override;
+
+ void SetFullScreen(wayland::output_t const& output, float refreshRate) override;
+ void SetWindowed() override;
+ void SetMaximized() override;
+ void UnsetMaximized() override;
+ void SetMinimized() override;
+ void SetWindowGeometry(CRectInt geometry) override;
+ void AckConfigure(std::uint32_t serial) override;
+
+ void StartMove(const wayland::seat_t& seat, std::uint32_t serial) override;
+ void StartResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge) override;
+ void ShowShellContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position) override;
+
+private:
+ IShellSurfaceHandler& m_handler;
+ wayland::display_t& m_display;
+ wayland::zxdg_shell_v6_t m_shell;
+ wayland::surface_t m_surface;
+ wayland::zxdg_surface_v6_t m_xdgSurface;
+ wayland::zxdg_toplevel_v6_t m_xdgToplevel;
+
+ CSizeInt m_configuredSize;
+ StateBitset m_configuredState;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/Signals.h b/xbmc/windowing/wayland/Signals.h
new file mode 100644
index 0000000..59a1fdf
--- /dev/null
+++ b/xbmc/windowing/wayland/Signals.h
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2017-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 <map>
+#include <memory>
+#include <mutex>
+
+namespace KODI
+{
+
+using RegistrationIdentifierType = int;
+
+class ISignalHandlerData
+{
+protected:
+ ~ISignalHandlerData() = default;
+
+public:
+ virtual void Unregister(RegistrationIdentifierType id) = 0;
+};
+
+class CSignalRegistration
+{
+ std::weak_ptr<ISignalHandlerData> m_list;
+ RegistrationIdentifierType m_registration;
+
+ template<typename ManagedT>
+ friend class CSignalHandlerList;
+
+ CSignalRegistration(std::shared_ptr<ISignalHandlerData> const& list, RegistrationIdentifierType registration)
+ : m_list{list}, m_registration{registration}
+ {
+ }
+
+ CSignalRegistration(CSignalRegistration const& other) = delete;
+ CSignalRegistration& operator=(CSignalRegistration const& other) = delete;
+
+public:
+ CSignalRegistration() noexcept = default;
+
+ CSignalRegistration(CSignalRegistration&& other) noexcept
+ {
+ *this = std::move(other);
+ }
+
+ inline CSignalRegistration& operator=(CSignalRegistration&& other) noexcept
+ {
+ Unregister();
+ std::swap(m_list, other.m_list);
+ m_registration = other.m_registration;
+ return *this;
+ }
+
+ ~CSignalRegistration() noexcept
+ {
+ Unregister();
+ }
+
+ inline void Unregister()
+ {
+ if (auto list = m_list.lock())
+ {
+ list->Unregister(m_registration);
+ list.reset();
+ }
+ }
+};
+
+template<typename ManagedT>
+class CSignalHandlerList
+{
+ /**
+ * Internal storage for handler list
+ *
+ * Extra struct so memory handling with shared_ptr and weak_ptr can be done
+ * on this level
+ */
+ struct Data final : public ISignalHandlerData
+ {
+ CCriticalSection m_handlerCriticalSection;
+ std::map<RegistrationIdentifierType, ManagedT> m_handlers;
+
+ void Unregister(RegistrationIdentifierType id) override
+ {
+ std::unique_lock<CCriticalSection> lock(m_handlerCriticalSection);
+ m_handlers.erase(id);
+ }
+ };
+
+ std::shared_ptr<Data> m_data;
+ RegistrationIdentifierType m_lastRegistrationId{};
+
+ CSignalHandlerList(CSignalHandlerList const& other) = delete;
+ CSignalHandlerList& operator=(CSignalHandlerList const& other) = delete;
+
+public:
+ CSignalHandlerList()
+ : m_data{new Data}
+ {}
+
+ CSignalRegistration Register(ManagedT const& handler)
+ {
+ std::unique_lock<CCriticalSection> lock(m_data->m_handlerCriticalSection);
+ bool inserted{false};
+ while(!inserted)
+ {
+ inserted = m_data->m_handlers.emplace(++m_lastRegistrationId, handler).second;
+ }
+ return {m_data, m_lastRegistrationId};
+ }
+
+ /**
+ * Invoke all registered signal handlers with the provided arguments
+ * when the signal type is a std::function or otherwise implements
+ * operator()
+ */
+ template<typename... ArgsT>
+ void Invoke(ArgsT&&... args)
+ {
+ std::unique_lock<CCriticalSection> lock(m_data->m_handlerCriticalSection);
+ for (auto const& handler : *this)
+ {
+ handler.second(std::forward<ArgsT>(args)...);
+ }
+ }
+
+ auto begin() const { return m_data->m_handlers.cbegin(); }
+
+ auto end() const { return m_data->m_handlers.cend(); }
+
+ /**
+ * Get critical section for accessing the handler list
+ * \note You must lock this yourself if you iterate through the handler
+ * list manually without using \ref Invoke or similar.
+ */
+ CCriticalSection const& CriticalSection() const
+ {
+ return m_data->m_handlerCriticalSection;
+ }
+};
+
+}
diff --git a/xbmc/windowing/wayland/Util.cpp b/xbmc/windowing/wayland/Util.cpp
new file mode 100644
index 0000000..707da11
--- /dev/null
+++ b/xbmc/windowing/wayland/Util.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017-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 "Util.h"
+
+#include <map>
+#include <string>
+
+#include <wayland-cursor.hpp>
+
+namespace
+{
+/*
+ * List from gdkcursor-wayland.c
+ *
+ * GDK - The GIMP Drawing Kit
+ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * See LICENSES/README.md for more information.
+ */
+static std::map<std::string, std::string> CursorFallbackNameMap =
+{
+ { "default", "left_ptr" },
+ { "help", "question_arrow" },
+ { "context-menu", "left_ptr" },
+ { "pointer", "hand" },
+ { "progress", "left_ptr_watch" },
+ { "wait", "watch" },
+ { "cell", "crosshair" },
+ { "crosshair", "cross" },
+ { "text", "xterm" },
+ { "vertical-text","xterm" },
+ { "alias", "dnd-link" },
+ { "copy", "dnd-copy" },
+ { "move", "dnd-move" },
+ { "no-drop", "dnd-none" },
+ { "dnd-ask", "dnd-copy" }, // not CSS, but we want to guarantee it anyway
+ { "not-allowed", "crossed_circle" },
+ { "grab", "hand2" },
+ { "grabbing", "hand2" },
+ { "all-scroll", "left_ptr" },
+ { "col-resize", "h_double_arrow" },
+ { "row-resize", "v_double_arrow" },
+ { "n-resize", "top_side" },
+ { "e-resize", "right_side" },
+ { "s-resize", "bottom_side" },
+ { "w-resize", "left_side" },
+ { "ne-resize", "top_right_corner" },
+ { "nw-resize", "top_left_corner" },
+ { "se-resize", "bottom_right_corner" },
+ { "sw-resize", "bottom_left_corner" },
+ { "ew-resize", "h_double_arrow" },
+ { "ns-resize", "v_double_arrow" },
+ { "nesw-resize", "fd_double_arrow" },
+ { "nwse-resize", "bd_double_arrow" },
+ { "zoom-in", "left_ptr" },
+ { "zoom-out", "left_ptr" }
+};
+
+}
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+wayland::cursor_t CCursorUtil::LoadFromTheme(wayland::cursor_theme_t const& theme, std::string const& name)
+{
+ try
+ {
+ return theme.get_cursor(name);
+ }
+ catch (std::exception const&)
+ {
+ auto i = CursorFallbackNameMap.find(name);
+ if (i == CursorFallbackNameMap.end())
+ {
+ throw;
+ }
+ else
+ {
+ return theme.get_cursor(i->second);
+ }
+ }
+}
diff --git a/xbmc/windowing/wayland/Util.h b/xbmc/windowing/wayland/Util.h
new file mode 100644
index 0000000..ab13e29
--- /dev/null
+++ b/xbmc/windowing/wayland/Util.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017-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 <cstdint>
+#include <string>
+
+#include <wayland-client.hpp>
+#include <wayland-cursor.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+struct WaylandCPtrCompare
+{
+ bool operator()(wayland::proxy_t const& p1, wayland::proxy_t const& p2) const
+ {
+ return reinterpret_cast<std::uintptr_t>(p1.c_ptr()) < reinterpret_cast<std::uintptr_t>(p2.c_ptr());
+ }
+};
+
+class CCursorUtil
+{
+public:
+ /**
+ * Load a cursor from a theme with automatic fallback
+ *
+ * Modern cursor themes use CSS names for the cursors as defined in
+ * the XDG cursor-spec (draft at the moment), but older themes
+ * might still use the cursor names that were popular with X11.
+ * This function tries to load a cursor by the given CSS name and
+ * automatically falls back to the corresponding X11 name if the
+ * load fails.
+ *
+ * \param theme cursor theme to load from
+ * \param name CSS cursor name to load
+ * \return requested cursor
+ */
+ static wayland::cursor_t LoadFromTheme(wayland::cursor_theme_t const& theme, std::string const& name);
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/VideoSyncWpPresentation.cpp b/xbmc/windowing/wayland/VideoSyncWpPresentation.cpp
new file mode 100644
index 0000000..e050a2d
--- /dev/null
+++ b/xbmc/windowing/wayland/VideoSyncWpPresentation.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017-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 "VideoSyncWpPresentation.h"
+
+#include "settings/AdvancedSettings.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/wayland/WinSystemWayland.h"
+
+#include <cinttypes>
+#include <functional>
+
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace std::placeholders;
+
+CVideoSyncWpPresentation::CVideoSyncWpPresentation(void* clock, CWinSystemWayland& winSystem)
+: CVideoSync(clock), m_winSystem(winSystem)
+{
+}
+
+bool CVideoSyncWpPresentation::Setup(PUPDATECLOCK func)
+{
+ UpdateClock = func;
+ m_stopEvent.Reset();
+ m_fps = m_winSystem.GetSyncOutputRefreshRate();
+
+ return true;
+}
+
+void CVideoSyncWpPresentation::Run(CEvent& stopEvent)
+{
+ m_presentationHandler = m_winSystem.RegisterOnPresentationFeedback(std::bind(&CVideoSyncWpPresentation::HandlePresentation, this, _1, _2, _3, _4, _5));
+
+ XbmcThreads::CEventGroup waitGroup{&stopEvent, &m_stopEvent};
+ waitGroup.wait();
+
+ m_presentationHandler.Unregister();
+}
+
+void CVideoSyncWpPresentation::Cleanup()
+{
+}
+
+float CVideoSyncWpPresentation::GetFps()
+{
+ return m_fps;
+}
+
+void CVideoSyncWpPresentation::HandlePresentation(timespec tv, std::uint32_t refresh, std::uint32_t syncOutputID, float syncOutputRefreshRate, std::uint64_t msc)
+{
+ auto mscDiff = msc - m_lastMsc;
+
+ CLog::Log(LOGDEBUG, LOGAVTIMING,
+ "VideoSyncWpPresentation: tv {}.{:09} s next refresh in +{} ns (fps {:f}) sync output "
+ "id {} fps {:f} msc {} mscdiff {}",
+ static_cast<std::uint64_t>(tv.tv_sec), static_cast<std::uint64_t>(tv.tv_nsec), refresh,
+ 1.0e9 / refresh, syncOutputID, syncOutputRefreshRate, msc, mscDiff);
+
+ if (m_fps != syncOutputRefreshRate || (m_syncOutputID != 0 && m_syncOutputID != syncOutputID))
+ {
+ // Restart if fps changes or sync output changes (which means that the msc jumps)
+ CLog::Log(LOGDEBUG, "fps or sync output changed, restarting Wayland video sync");
+ m_stopEvent.Set();
+ }
+ m_syncOutputID = syncOutputID;
+
+ if (m_lastMsc == 0)
+ {
+ // If this is the first time or MSC is not supported, assume we moved one frame
+ mscDiff = 1;
+ }
+ m_lastMsc = msc;
+
+ // FIXME use timespec instead of currenthostcounter()? Possibly difficult
+ // due to different clock base
+ UpdateClock(mscDiff, CurrentHostCounter(), m_refClock);
+}
diff --git a/xbmc/windowing/wayland/VideoSyncWpPresentation.h b/xbmc/windowing/wayland/VideoSyncWpPresentation.h
new file mode 100644
index 0000000..9d05550
--- /dev/null
+++ b/xbmc/windowing/wayland/VideoSyncWpPresentation.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017-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 "Signals.h"
+#include "windowing/VideoSync.h"
+
+#include <cstdint>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CWinSystemWayland;
+
+class CVideoSyncWpPresentation : public CVideoSync
+{
+public:
+ explicit CVideoSyncWpPresentation(void* clock, CWinSystemWayland& winSystem);
+
+ float GetFps() override;
+ bool Setup(PUPDATECLOCK func) override;
+ void Run(CEvent& stop) override;
+ void Cleanup() override;
+
+private:
+ void HandlePresentation(timespec tv, std::uint32_t refresh, std::uint32_t syncOutputID, float syncOutputRefreshRate, std::uint64_t msc);
+
+ CEvent m_stopEvent;
+ CSignalRegistration m_presentationHandler;
+ std::uint64_t m_lastMsc{};
+ std::uint32_t m_syncOutputID{};
+ CWinSystemWayland &m_winSystem;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WinEventsWayland.cpp b/xbmc/windowing/wayland/WinEventsWayland.cpp
new file mode 100644
index 0000000..4345cf0
--- /dev/null
+++ b/xbmc/windowing/wayland/WinEventsWayland.cpp
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2017-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 "WinEventsWayland.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "threads/CriticalSection.h"
+#include "threads/Thread.h"
+#include "utils/log.h"
+
+#include "platform/posix/utils/FileHandle.h"
+
+#include <exception>
+#include <memory>
+#include <mutex>
+#include <system_error>
+
+#include <sys/poll.h>
+#include <unistd.h>
+#include <wayland-client.hpp>
+
+using namespace KODI::UTILS::POSIX;
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+/**
+ * Thread for processing Wayland events
+ *
+ * While not strictly needed, reading from the Wayland display file descriptor
+ * and dispatching the resulting events is done in an extra thread here.
+ * Sometime in the future, MessagePump() might be gone and then the
+ * transition will be easier since this extra thread is already here.
+ */
+class CWinEventsWaylandThread : CThread
+{
+ wayland::display_t& m_display;
+ // Pipe used for cancelling poll() on shutdown
+ CFileHandle m_pipeRead;
+ CFileHandle m_pipeWrite;
+
+ CCriticalSection m_roundtripQueueMutex;
+ std::atomic<wayland::event_queue_t*> m_roundtripQueue{nullptr};
+ CEvent m_roundtripQueueEvent;
+
+public:
+ CWinEventsWaylandThread(wayland::display_t& display)
+ : CThread("Wayland message pump"), m_display{display}
+ {
+ std::array<int, 2> fds;
+ if (pipe(fds.data()) < 0)
+ {
+ throw std::system_error(errno, std::generic_category(), "Error creating pipe for Wayland message pump cancellation");
+ }
+ m_pipeRead.attach(fds[0]);
+ m_pipeWrite.attach(fds[1]);
+ Create();
+ }
+
+ ~CWinEventsWaylandThread() override
+ {
+ Stop();
+ // Wait for roundtrip invocation to finish
+ std::unique_lock<CCriticalSection> lock(m_roundtripQueueMutex);
+ }
+
+ void Stop()
+ {
+ CLog::Log(LOGDEBUG, "Stopping Wayland message pump");
+ // Set m_bStop
+ StopThread(false);
+ InterruptPoll();
+ // Now wait for actual exit
+ StopThread(true);
+ }
+
+ void RoundtripQueue(wayland::event_queue_t const& queue)
+ {
+ wayland::event_queue_t queueCopy{queue};
+
+ // Serialize invocations of this function - it's used very rarely and usually
+ // not in parallel anyway, and doing it avoids lots of complications
+ std::unique_lock<CCriticalSection> lock(m_roundtripQueueMutex);
+
+ m_roundtripQueueEvent.Reset();
+ // We can just set the value here since there is no other writer in parallel
+ m_roundtripQueue.store(&queueCopy);
+ // Dispatching can happen now
+
+ // Make sure we don't wait for an event to happen on the socket
+ InterruptPoll();
+
+ if (m_bStop)
+ return;
+
+ m_roundtripQueueEvent.Wait();
+ }
+
+ wayland::display_t& GetDisplay()
+ {
+ return m_display;
+ }
+
+private:
+ void InterruptPoll()
+ {
+ char c = 0;
+ if (write(m_pipeWrite, &c, 1) != 1)
+ throw std::runtime_error("Failed to write to wayland message pipe");
+ }
+
+ void Process() override
+ {
+ try
+ {
+ std::array<pollfd, 2> pollFds;
+ pollfd& waylandPoll = pollFds[0];
+ pollfd& cancelPoll = pollFds[1];
+ // Wayland filedescriptor
+ waylandPoll.fd = m_display.get_fd();
+ waylandPoll.events = POLLIN;
+ waylandPoll.revents = 0;
+ // Read end of the cancellation pipe
+ cancelPoll.fd = m_pipeRead;
+ cancelPoll.events = POLLIN;
+ cancelPoll.revents = 0;
+
+ CLog::Log(LOGDEBUG, "Starting Wayland message pump");
+
+ // Run until cancelled or error
+ while (!m_bStop)
+ {
+ // dispatch() provides no way to cancel a blocked read from the socket
+ // wl_display_disconnect would just close the socket, leading to problems
+ // with the poll() that dispatch() uses internally - so we have to implement
+ // cancellation ourselves here
+
+ // Acquire global read intent
+ wayland::read_intent readIntent = m_display.obtain_read_intent();
+ m_display.flush();
+
+ if (poll(pollFds.data(), pollFds.size(), -1) < 0)
+ {
+ if (errno == EINTR)
+ {
+ continue;
+ }
+ else
+ {
+ throw std::system_error(errno, std::generic_category(), "Error polling on Wayland socket");
+ }
+ }
+
+ if (cancelPoll.revents & POLLERR || cancelPoll.revents & POLLHUP || cancelPoll.revents & POLLNVAL)
+ {
+ throw std::runtime_error("poll() signalled error condition on poll interruption socket");
+ }
+
+ if (waylandPoll.revents & POLLERR || waylandPoll.revents & POLLHUP || waylandPoll.revents & POLLNVAL)
+ {
+ throw std::runtime_error("poll() signalled error condition on Wayland socket");
+ }
+
+ // Read events and release intent; this does not block
+ readIntent.read();
+ // Dispatch default event queue
+ m_display.dispatch_pending();
+
+ if (auto* roundtripQueue = m_roundtripQueue.exchange(nullptr))
+ {
+ m_display.roundtrip_queue(*roundtripQueue);
+ m_roundtripQueueEvent.Set();
+ }
+ if (cancelPoll.revents & POLLIN)
+ {
+ // Read away the char so we don't get another notification
+ // Indepentent from m_roundtripQueue so there are no races
+ char c;
+ if (read(m_pipeRead, &c, 1) != 1)
+ throw std::runtime_error("Error reading from wayland message pipe");
+ }
+ }
+
+ CLog::Log(LOGDEBUG, "Wayland message pump stopped");
+ }
+ catch (std::exception const& e)
+ {
+ // FIXME CThread::OnException is very badly named and should probably go away
+ // FIXME Thread exception handling is seriously broken:
+ // Exceptions will be swallowed and do not terminate the program.
+ // Even XbmcCommons::UncheckedException which claims to be there for just this
+ // purpose does not cause termination, the log message will just be slightly different.
+
+ // But here, going on would be meaningless, so do a hard exit
+ CLog::Log(LOGFATAL, "Exception in Wayland message pump, exiting: {}", e.what());
+ std::terminate();
+ }
+
+ // Wake up if someone is still waiting for roundtrip, won't happen anytime soon...
+ m_roundtripQueueEvent.Set();
+ }
+};
+
+std::unique_ptr<CWinEventsWaylandThread> g_WlMessagePump{nullptr};
+
+}
+
+void CWinEventsWayland::SetDisplay(wayland::display_t* display)
+{
+ if (display && !g_WlMessagePump)
+ {
+ // Start message processing as soon as we have a display
+ g_WlMessagePump.reset(new CWinEventsWaylandThread(*display));
+ }
+ else if (g_WlMessagePump)
+ {
+ // Stop if display is set to nullptr
+ g_WlMessagePump.reset();
+ }
+}
+
+void CWinEventsWayland::Flush()
+{
+ if (g_WlMessagePump)
+ {
+ g_WlMessagePump->GetDisplay().flush();
+ }
+}
+
+void CWinEventsWayland::RoundtripQueue(const wayland::event_queue_t& queue)
+{
+ if (g_WlMessagePump)
+ {
+ g_WlMessagePump->RoundtripQueue(queue);
+ }
+}
+
+bool CWinEventsWayland::MessagePump()
+{
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ // Forward any events that may have been pushed to our queue
+ while (true)
+ {
+ XBMC_Event event;
+ {
+ // Scoped lock for reentrancy
+ std::unique_lock<CCriticalSection> lock(m_queueMutex);
+
+ if (m_queue.empty())
+ {
+ break;
+ }
+
+ // First get event and remove it from the queue, then pass it on - be aware that this
+ // function must be reentrant
+ event = m_queue.front();
+ m_queue.pop();
+ }
+
+ if (appPort)
+ appPort->OnEvent(event);
+ }
+
+ return true;
+}
+
+void CWinEventsWayland::MessagePush(XBMC_Event* ev)
+{
+ std::unique_lock<CCriticalSection> lock(m_queueMutex);
+ m_queue.emplace(*ev);
+}
diff --git a/xbmc/windowing/wayland/WinEventsWayland.h b/xbmc/windowing/wayland/WinEventsWayland.h
new file mode 100644
index 0000000..9390956
--- /dev/null
+++ b/xbmc/windowing/wayland/WinEventsWayland.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017-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 "../WinEvents.h"
+#include "threads/CriticalSection.h"
+
+#include <queue>
+
+namespace wayland
+{
+class event_queue_t;
+class display_t;
+}
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CWinEventsWayland : public IWinEvents
+{
+public:
+ bool MessagePump() override;
+ void MessagePush(XBMC_Event* ev);
+ /// Write buffered messages to the compositor
+ static void Flush();
+ /// Do a roundtrip on the specified queue from the event processing thread
+ static void RoundtripQueue(wayland::event_queue_t const& queue);
+
+private:
+ friend class CWinSystemWayland;
+ static void SetDisplay(wayland::display_t* display);
+
+ CCriticalSection m_queueMutex;
+ std::queue<XBMC_Event> m_queue;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WinSystemWayland.cpp b/xbmc/windowing/wayland/WinSystemWayland.cpp
new file mode 100644
index 0000000..9733339
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWayland.cpp
@@ -0,0 +1,1568 @@
+/*
+ * Copyright (C) 2017-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 "WinSystemWayland.h"
+
+#include "CompileInfo.h"
+#include "Connection.h"
+#include "OSScreenSaverIdleInhibitUnstableV1.h"
+#include "OptionalsReg.h"
+#include "Registry.h"
+#include "ServiceBroker.h"
+#include "ShellSurfaceWlShell.h"
+#include "ShellSurfaceXdgShell.h"
+#include "ShellSurfaceXdgShellUnstableV6.h"
+#include "Util.h"
+#include "VideoSyncWpPresentation.h"
+#include "WinEventsWayland.h"
+#include "WindowDecorator.h"
+#include "application/Application.h"
+#include "cores/RetroPlayer/process/wayland/RPProcessInfoWayland.h"
+#include "cores/VideoPlayer/Process/wayland/ProcessInfoWayland.h"
+#include "guilib/DispResource.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/InputManager.h"
+#include "input/touch/generic/GenericTouchActionHandler.h"
+#include "input/touch/generic/GenericTouchInputHandler.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "utils/ActorProtocol.h"
+#include "utils/MathUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/linux/OSScreenSaverFreedesktop.h"
+
+#include "platform/linux/TimeUtils.h"
+
+#include <algorithm>
+#include <limits>
+#include <mutex>
+#include <numeric>
+
+#if defined(HAS_DBUS)
+# include "windowing/linux/OSScreenSaverFreedesktop.h"
+#endif
+
+using namespace KODI::WINDOWING;
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace std::placeholders;
+using namespace std::chrono_literals;
+
+namespace
+{
+
+RESOLUTION FindMatchingCustomResolution(CSizeInt size, float refreshRate)
+{
+ for (size_t res{RES_DESKTOP}; res < CDisplaySettings::GetInstance().ResolutionInfoSize(); ++res)
+ {
+ auto const& resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(res);
+ if (resInfo.iWidth == size.Width() && resInfo.iHeight == size.Height() && MathUtils::FloatEquals(resInfo.fRefreshRate, refreshRate, 0.0005f))
+ {
+ return static_cast<RESOLUTION> (res);
+ }
+ }
+ return RES_INVALID;
+}
+
+struct OutputScaleComparer
+{
+ bool operator()(std::shared_ptr<COutput> const& output1, std::shared_ptr<COutput> const& output2)
+ {
+ return output1->GetScale() < output2->GetScale();
+ }
+};
+
+struct OutputCurrentRefreshRateComparer
+{
+ bool operator()(std::shared_ptr<COutput> const& output1, std::shared_ptr<COutput> const& output2)
+ {
+ return output1->GetCurrentMode().refreshMilliHz < output2->GetCurrentMode().refreshMilliHz;
+ }
+};
+
+/// Scope guard for Actor::Message
+class MessageHandle : public KODI::UTILS::CScopeGuard<Actor::Message*, nullptr, void(Actor::Message*)>
+{
+public:
+ MessageHandle() : CScopeGuard{std::bind(&Actor::Message::Release, std::placeholders::_1), nullptr} {}
+ explicit MessageHandle(Actor::Message* message) : CScopeGuard{std::bind(&Actor::Message::Release, std::placeholders::_1), message} {}
+ Actor::Message* Get() { return static_cast<Actor::Message*> (*this); }
+};
+
+/**
+ * Protocol for communication between Wayland event thread and main thread
+ *
+ * Many messages received from the Wayland compositor must be processed at a
+ * defined time between frame rendering, such as resolution switches. Thus
+ * they are pushed to the main thread for processing.
+ *
+ * The protocol is strictly uni-directional from event to main thread at the moment,
+ * so \ref Actor::Protocol is mainly used as an event queue.
+ */
+namespace WinSystemWaylandProtocol
+{
+
+enum OutMessage
+{
+ CONFIGURE,
+ OUTPUT_HOTPLUG,
+ BUFFER_SCALE
+};
+
+struct MsgConfigure
+{
+ std::uint32_t serial;
+ CSizeInt surfaceSize;
+ IShellSurface::StateBitset state;
+};
+
+struct MsgBufferScale
+{
+ int scale;
+};
+
+};
+
+}
+
+CWinSystemWayland::CWinSystemWayland()
+: CWinSystemBase{}, m_protocol{"WinSystemWaylandInternal"}
+{
+ m_winEvents.reset(new CWinEventsWayland());
+}
+
+CWinSystemWayland::~CWinSystemWayland() noexcept
+{
+ DestroyWindowSystem();
+}
+
+bool CWinSystemWayland::InitWindowSystem()
+{
+ const char* env = getenv("WAYLAND_DISPLAY");
+ if (!env)
+ {
+ CLog::Log(LOGDEBUG, "CWinSystemWayland::{} - WAYLAND_DISPLAY env not set", __FUNCTION__);
+ return false;
+ }
+
+ wayland::set_log_handler([](const std::string& message)
+ { CLog::Log(LOGWARNING, "wayland-client log message: {}", message); });
+
+ CLog::LogF(LOGINFO, "Connecting to Wayland server");
+ m_connection = std::make_unique<CConnection>();
+ if (!m_connection->HasDisplay())
+ return false;
+
+ VIDEOPLAYER::CProcessInfoWayland::Register();
+ RETRO::CRPProcessInfoWayland::Register();
+
+ m_registry.reset(new CRegistry{*m_connection});
+
+ m_registry->RequestSingleton(m_compositor, 1, 4);
+ m_registry->RequestSingleton(m_shm, 1, 1);
+ m_registry->RequestSingleton(m_presentation, 1, 1, false);
+ // version 2 adds done() -> required
+ // version 3 adds destructor -> optional
+ m_registry->Request<wayland::output_t>(2, 3, std::bind(&CWinSystemWayland::OnOutputAdded, this, _1, _2), std::bind(&CWinSystemWayland::OnOutputRemoved, this, _1));
+
+ m_registry->Bind();
+
+ if (m_presentation)
+ {
+ m_presentation.on_clock_id() = [this](std::uint32_t clockId)
+ {
+ CLog::Log(LOGINFO, "Wayland presentation clock: {}", clockId);
+ m_presentationClock = static_cast<clockid_t> (clockId);
+ };
+ }
+
+ // Do another roundtrip to get initial wl_output information
+ m_connection->GetDisplay().roundtrip();
+ if (m_outputs.empty())
+ {
+ throw std::runtime_error("No outputs received from compositor");
+ }
+
+ // Event loop is started in CreateWindow
+
+ // pointer is by default not on this window, will be immediately rectified
+ // by the enter() events if it is
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ // Always use the generic touch action handler
+ CGenericTouchInputHandler::GetInstance().RegisterHandler(&CGenericTouchActionHandler::GetInstance());
+
+ CServiceBroker::GetSettingsComponent()
+ ->GetSettings()
+ ->GetSetting(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE)
+ ->SetVisible(true);
+
+ return CWinSystemBase::InitWindowSystem();
+}
+
+bool CWinSystemWayland::DestroyWindowSystem()
+{
+ DestroyWindow();
+ // wl_display_disconnect frees all proxy objects, so we have to make sure
+ // all stuff is gone on the C++ side before that
+ m_cursorSurface = wayland::surface_t{};
+ m_cursorBuffer = wayland::buffer_t{};
+ m_cursorImage = wayland::cursor_image_t{};
+ m_cursorTheme = wayland::cursor_theme_t{};
+ m_outputsInPreparation.clear();
+ m_outputs.clear();
+ m_frameCallback = wayland::callback_t{};
+ m_screenSaverManager.reset();
+
+ m_seatInputProcessing.reset();
+
+ if (m_registry)
+ {
+ m_registry->UnbindSingletons();
+ }
+ m_registry.reset();
+ m_connection.reset();
+
+ CGenericTouchInputHandler::GetInstance().UnregisterHandler();
+
+ return CWinSystemBase::DestroyWindowSystem();
+}
+
+bool CWinSystemWayland::CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res)
+{
+ CLog::LogF(LOGINFO, "Starting {} size {}x{}", fullScreen ? "full screen" : "windowed", res.iWidth,
+ res.iHeight);
+
+ m_surface = m_compositor.create_surface();
+ m_surface.on_enter() = [this](const wayland::output_t& wloutput) {
+ if (auto output = FindOutputByWaylandOutput(wloutput))
+ {
+ CLog::Log(LOGDEBUG, "Entering output \"{}\" with scale {} and {:.3f} dpi",
+ UserFriendlyOutputName(output), output->GetScale(), output->GetCurrentDpi());
+ std::unique_lock<CCriticalSection> lock(m_surfaceOutputsMutex);
+ m_surfaceOutputs.emplace(output);
+ lock.unlock();
+ UpdateBufferScale();
+ UpdateTouchDpi();
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Entering output that was not configured yet, ignoring");
+ }
+ };
+ m_surface.on_leave() = [this](const wayland::output_t& wloutput) {
+ if (auto output = FindOutputByWaylandOutput(wloutput))
+ {
+ CLog::Log(LOGDEBUG, "Leaving output \"{}\" with scale {}", UserFriendlyOutputName(output),
+ output->GetScale());
+ std::unique_lock<CCriticalSection> lock(m_surfaceOutputsMutex);
+ m_surfaceOutputs.erase(output);
+ lock.unlock();
+ UpdateBufferScale();
+ UpdateTouchDpi();
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Leaving output that was not configured yet, ignoring");
+ }
+ };
+
+ m_windowDecorator.reset(new CWindowDecorator(*this, *m_connection, m_surface));
+
+ m_seatInputProcessing.reset(new CSeatInputProcessing(m_surface, *this));
+ m_seatRegistry.reset(new CRegistry{*m_connection});
+ // version 2 adds name event -> optional
+ // version 4 adds wl_keyboard repeat_info -> optional
+ // version 5 adds discrete axis events in wl_pointer -> unused
+ m_seatRegistry->Request<wayland::seat_t>(1, 5, std::bind(&CWinSystemWayland::OnSeatAdded, this, _1, _2), std::bind(&CWinSystemWayland::OnSeatRemoved, this, _1));
+ m_seatRegistry->Bind();
+
+ if (m_seats.empty())
+ {
+ CLog::Log(LOGWARNING, "Wayland compositor did not announce a wl_seat - you will not have any input devices for the time being");
+ }
+
+ if (fullScreen)
+ {
+ m_shellSurfaceState.set(IShellSurface::STATE_FULLSCREEN);
+ }
+ // Assume we're active on startup until someone tells us otherwise
+ m_shellSurfaceState.set(IShellSurface::STATE_ACTIVATED);
+ // Try with this resolution if compositor does not say otherwise
+ UpdateSizeVariables({res.iWidth, res.iHeight}, m_scale, m_shellSurfaceState, false);
+
+ // Use AppName as the desktop file name. This is required to lookup the app icon of the same name.
+ m_shellSurface.reset(CShellSurfaceXdgShell::TryCreate(*this, *m_connection, m_surface, name,
+ std::string(CCompileInfo::GetAppName())));
+ if (!m_shellSurface)
+ {
+ m_shellSurface.reset(CShellSurfaceXdgShellUnstableV6::TryCreate(
+ *this, *m_connection, m_surface, name, std::string(CCompileInfo::GetAppName())));
+ }
+ if (!m_shellSurface)
+ {
+ CLog::LogF(LOGWARNING, "Compositor does not support xdg_shell protocol (stable or unstable v6) - falling back to wl_shell, not all features might work");
+ m_shellSurface.reset(new CShellSurfaceWlShell(*this, *m_connection, m_surface, name,
+ std::string(CCompileInfo::GetAppName())));
+ }
+
+ if (fullScreen)
+ {
+ // Try to start on correct monitor and with correct buffer scale
+ auto output = FindOutputByUserFriendlyName(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR));
+ auto wlOutput = output ? output->GetWaylandOutput() : wayland::output_t{};
+ m_lastSetOutput = wlOutput;
+ m_shellSurface->SetFullScreen(wlOutput, res.fRefreshRate);
+ if (output && m_surface.can_set_buffer_scale())
+ {
+ m_scale = output->GetScale();
+ ApplyBufferScale();
+ }
+ }
+
+ // Just remember initial width/height for context creation in OnConfigure
+ // This is used for sizing the EGLSurface
+ m_shellSurfaceInitializing = true;
+ m_shellSurface->Initialize();
+ m_shellSurfaceInitializing = false;
+
+ // Apply window decorations if necessary
+ m_windowDecorator->SetState(m_configuredSize, m_scale, m_shellSurfaceState);
+
+ // Set initial opaque region and window geometry
+ ApplyOpaqueRegion();
+ ApplyWindowGeometry();
+
+ // Update resolution with real size as it could have changed due to configure()
+ UpdateDesktopResolution(res, res.strOutput, m_bufferSize.Width(), m_bufferSize.Height(), res.fRefreshRate, 0);
+ res.bFullScreen = fullScreen;
+
+ // Now start processing events
+ //
+ // There are two stages to the event handling:
+ // * Initialization (which ends here): Everything runs synchronously and init
+ // code that needs events processed must call roundtrip().
+ // This is done for simplicity because it is a lot easier than to make
+ // everything event-based and thread-safe everywhere in the startup code,
+ // which is also not really necessary.
+ // * Runtime (which starts here): Every object creation from now on
+ // needs to take great care to be thread-safe:
+ // Since the event pump is always running now, there is a tiny window between
+ // creating an object and attaching the C++ event handlers during which
+ // events can get queued and dispatched for the object but the handlers have
+ // not been set yet. Consequently, the events would get lost.
+ // However, this does not apply to objects that are created in response to
+ // compositor events. Since the callbacks are called from the event processing
+ // thread and ran strictly sequentially, no other events are dispatched during
+ // the runtime of a callback. Luckily this applies to global binding like
+ // wl_output and wl_seat and thus to most if not all runtime object creation
+ // cases we have to support.
+ // There is another problem when Wayland objects are destructed from the main
+ // thread: An event handler could be running in parallel, resulting in certain
+ // doom. So objects should only be deleted in response to compositor events, too.
+ // They might be hiding behind class member variables, so be wary.
+ // Note that this does not apply to global teardown since the event pump is
+ // stopped then.
+ CWinEventsWayland::SetDisplay(&m_connection->GetDisplay());
+
+ return true;
+}
+
+bool CWinSystemWayland::DestroyWindow()
+{
+ // Make sure no more events get processed when we kill the instances
+ CWinEventsWayland::SetDisplay(nullptr);
+
+ m_shellSurface.reset();
+ // waylandpp automatically calls wl_surface_destroy when the last reference is removed
+ m_surface = wayland::surface_t();
+ m_windowDecorator.reset();
+ m_seats.clear();
+ m_lastSetOutput.proxy_release();
+ m_surfaceOutputs.clear();
+ m_surfaceSubmissions.clear();
+ m_seatRegistry.reset();
+
+ return true;
+}
+
+bool CWinSystemWayland::CanDoWindowed()
+{
+ return true;
+}
+
+std::vector<std::string> CWinSystemWayland::GetConnectedOutputs()
+{
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ std::vector<std::string> outputs;
+ std::transform(m_outputs.cbegin(), m_outputs.cend(), std::back_inserter(outputs),
+ [this](decltype(m_outputs)::value_type const& pair)
+ { return UserFriendlyOutputName(pair.second); });
+
+ return outputs;
+}
+
+bool CWinSystemWayland::UseLimitedColor()
+{
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE);
+}
+
+void CWinSystemWayland::UpdateResolutions()
+{
+ CWinSystemBase::UpdateResolutions();
+
+ CDisplaySettings::GetInstance().ClearCustomResolutions();
+
+ // Mimic X11:
+ // Only show resolutions for the currently selected output
+ std::string userOutput = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR);
+
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+
+ if (m_outputs.empty())
+ {
+ // *Usually* this should not happen - just give up
+ return;
+ }
+
+ auto output = FindOutputByUserFriendlyName(userOutput);
+ if (!output && m_lastSetOutput)
+ {
+ // Fallback to current output
+ output = FindOutputByWaylandOutput(m_lastSetOutput);
+ }
+ if (!output)
+ {
+ // Well just use the first one
+ output = m_outputs.begin()->second;
+ }
+
+ std::string outputName = UserFriendlyOutputName(output);
+
+ auto const& modes = output->GetModes();
+ auto const& currentMode = output->GetCurrentMode();
+ auto physicalSize = output->GetPhysicalSize();
+ CLog::LogF(LOGINFO,
+ "User wanted output \"{}\", we now have \"{}\" size {}x{} mm with {} mode(s):",
+ userOutput, outputName, physicalSize.Width(), physicalSize.Height(), modes.size());
+
+ for (auto const& mode : modes)
+ {
+ bool isCurrent = (mode == currentMode);
+ float pixelRatio = output->GetPixelRatioForMode(mode);
+ CLog::LogF(LOGINFO, "- {}x{} @{:.3f} Hz pixel ratio {:.3f}{}", mode.size.Width(),
+ mode.size.Height(), mode.refreshMilliHz / 1000.0f, pixelRatio,
+ isCurrent ? " current" : "");
+
+ RESOLUTION_INFO res;
+ UpdateDesktopResolution(res, outputName, mode.size.Width(), mode.size.Height(), mode.GetRefreshInHz(), 0);
+ res.fPixelRatio = pixelRatio;
+
+ if (isCurrent)
+ {
+ CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP) = res;
+ }
+ else
+ {
+ CDisplaySettings::GetInstance().AddResolutionInfo(res);
+ }
+ }
+
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+}
+
+std::shared_ptr<COutput> CWinSystemWayland::FindOutputByUserFriendlyName(const std::string& name)
+{
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ auto outputIt = std::find_if(m_outputs.begin(), m_outputs.end(),
+ [this, &name](decltype(m_outputs)::value_type const& entry)
+ {
+ return (name == UserFriendlyOutputName(entry.second));
+ });
+
+ return (outputIt == m_outputs.end() ? nullptr : outputIt->second);
+}
+
+std::shared_ptr<COutput> CWinSystemWayland::FindOutputByWaylandOutput(wayland::output_t const& output)
+{
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ auto outputIt = std::find_if(m_outputs.begin(), m_outputs.end(),
+ [&output](decltype(m_outputs)::value_type const& entry)
+ {
+ return (output == entry.second->GetWaylandOutput());
+ });
+
+ return (outputIt == m_outputs.end() ? nullptr : outputIt->second);
+}
+
+/**
+ * Change resolution and window state on Kodi request
+ *
+ * This function is used for updating resolution when Kodi initiates a resolution
+ * change, such as when changing between full screen and windowed mode or when
+ * selecting a different monitor or resolution in the settings.
+ *
+ * Size updates originating from compositor events (such as configure or buffer
+ * scale changes) should not use this function, but \ref SetResolutionInternal
+ * instead.
+ *
+ * \param fullScreen whether to go full screen or windowed
+ * \param res resolution to set
+ * \return whether the requested resolution was actually set - is false e.g.
+ * when already in full screen mode since the application cannot
+ * set the size then
+ */
+bool CWinSystemWayland::SetResolutionExternal(bool fullScreen, RESOLUTION_INFO const& res)
+{
+ // In fullscreen modes, we never change the surface size on Kodi's request,
+ // but only when the compositor tells us to. At least xdg_shell specifies
+ // that with state fullscreen the dimensions given in configure() must
+ // always be observed.
+ // This does mean that the compositor has no way of knowing which resolution
+ // we would (in theory) want. Since no compositor implements dynamic resolution
+ // switching at the moment, this is not a problem. If it is some day implemented
+ // in compositors, this code must be changed to match the behavior that is
+ // expected then anyway.
+
+ // We can honor the Kodi-requested size only if we are not bound by configure rules,
+ // which applies for maximized and fullscreen states.
+ // Also, setting an unconfigured size when just going fullscreen makes no sense.
+ // Give precedence to the size we have still pending, if any.
+ bool mustHonorSize{m_waitingForApply || m_shellSurfaceState.test(IShellSurface::STATE_MAXIMIZED) || m_shellSurfaceState.test(IShellSurface::STATE_FULLSCREEN) || fullScreen};
+
+ CLog::LogF(LOGINFO, "Kodi asked to switch mode to {}x{} @{:.3f} Hz on output \"{}\" {}",
+ res.iWidth, res.iHeight, res.fRefreshRate, res.strOutput,
+ fullScreen ? "full screen" : "windowed");
+
+ if (fullScreen)
+ {
+ // Try to match output
+ auto output = FindOutputByUserFriendlyName(res.strOutput);
+ auto wlOutput = output ? output->GetWaylandOutput() : wayland::output_t{};
+ if (!m_shellSurfaceState.test(IShellSurface::STATE_FULLSCREEN) || (m_lastSetOutput != wlOutput))
+ {
+ // Remember the output we set last so we don't set it again until we
+ // either go windowed or were on a different output
+ m_lastSetOutput = wlOutput;
+
+ if (output)
+ {
+ CLog::LogF(LOGDEBUG, "Resolved output \"{}\" to bound Wayland global {}", res.strOutput,
+ output->GetGlobalName());
+ }
+ else
+ {
+ CLog::LogF(LOGINFO,
+ "Could not match output \"{}\" to a currently available Wayland output, falling "
+ "back to default output",
+ res.strOutput);
+ }
+
+ CLog::LogF(LOGDEBUG, "Setting full-screen with refresh rate {:.3f}", res.fRefreshRate);
+ m_shellSurface->SetFullScreen(wlOutput, res.fRefreshRate);
+ }
+ else
+ {
+ CLog::LogF(LOGDEBUG, "Not setting full screen: already full screen on requested output");
+ }
+ }
+ else
+ {
+ if (m_shellSurfaceState.test(IShellSurface::STATE_FULLSCREEN))
+ {
+ CLog::LogF(LOGDEBUG, "Setting windowed");
+ m_shellSurface->SetWindowed();
+ }
+ else
+ {
+ CLog::LogF(LOGDEBUG, "Not setting windowed: already windowed");
+ }
+ }
+
+ // Set Kodi-provided size only if we are free to choose any size, otherwise
+ // wait for the compositor configure
+ if (!mustHonorSize)
+ {
+ CLog::LogF(LOGDEBUG, "Directly setting windowed size {}x{} on Kodi request", res.iWidth,
+ res.iHeight);
+ // Kodi is directly setting window size, apply
+ auto updateResult = UpdateSizeVariables({res.iWidth, res.iHeight}, m_scale, m_shellSurfaceState, false);
+ ApplySizeUpdate(updateResult);
+ }
+
+ bool wasInitialSetFullScreen{m_isInitialSetFullScreen};
+ m_isInitialSetFullScreen = false;
+
+ // Need to return true
+ // * when this SetFullScreen() call was free to change the context size (and possibly did so)
+ // * on first SetFullScreen so GraphicsContext gets resolution
+ // Otherwise, Kodi must keep the old resolution.
+ return !mustHonorSize || wasInitialSetFullScreen;
+}
+
+bool CWinSystemWayland::ResizeWindow(int, int, int, int)
+{
+ // CGraphicContext is "smart" and calls ResizeWindow or SetFullScreen depending
+ // on some state like whether we were already fullscreen. But actually the processing
+ // here is always identical, so we are using a common function to handle both.
+ const auto& res = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW);
+ // The newWidth/newHeight parameters are taken from RES_WINDOW anyway, so we can just
+ // ignore them
+ return SetResolutionExternal(false, res);
+}
+
+bool CWinSystemWayland::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool)
+{
+ return SetResolutionExternal(fullScreen, res);
+}
+
+void CWinSystemWayland::ApplySizeUpdate(SizeUpdateInformation update)
+{
+ if (update.bufferScaleChanged)
+ {
+ // Buffer scale must also match egl size configuration
+ ApplyBufferScale();
+ }
+ if (update.surfaceSizeChanged)
+ {
+ // Update opaque region here so size always matches the configured egl surface
+ ApplyOpaqueRegion();
+ }
+ if (update.configuredSizeChanged)
+ {
+ // Update window decoration state
+ m_windowDecorator->SetState(m_configuredSize, m_scale, m_shellSurfaceState);
+ ApplyWindowGeometry();
+ }
+ // Set always, because of initialization order GL context has to keep track of
+ // whether the size changed. If we skip based on update.bufferSizeChanged here,
+ // GL context will never get its initial size set.
+ SetContextSize(m_bufferSize);
+}
+
+void CWinSystemWayland::ApplyOpaqueRegion()
+{
+ // Mark everything opaque so the compositor can render it faster
+ CLog::LogF(LOGDEBUG, "Setting opaque region size {}x{}", m_surfaceSize.Width(),
+ m_surfaceSize.Height());
+ wayland::region_t opaqueRegion{m_compositor.create_region()};
+ opaqueRegion.add(0, 0, m_surfaceSize.Width(), m_surfaceSize.Height());
+ m_surface.set_opaque_region(opaqueRegion);
+}
+
+void CWinSystemWayland::ApplyWindowGeometry()
+{
+ m_shellSurface->SetWindowGeometry(m_windowDecorator->GetWindowGeometry());
+}
+
+void CWinSystemWayland::ProcessMessages()
+{
+ if (m_waitingForApply)
+ {
+ // Do not put multiple size updates into the pipeline, this would only make
+ // it more complicated without any real benefit. Wait until the size was reconfigured,
+ // then process events again.
+ return;
+ }
+
+ Actor::Message* message{};
+ MessageHandle lastConfigureMessage;
+ int skippedConfigures{-1};
+ int newScale{m_scale};
+
+ while (m_protocol.ReceiveOutMessage(&message))
+ {
+ MessageHandle guard{message};
+ switch (message->signal)
+ {
+ case WinSystemWaylandProtocol::CONFIGURE:
+ // Do not directly process configures, get the last one queued:
+ // While resizing, the compositor will usually send a configure event
+ // each time the mouse moves without any throttling (i.e. multiple times
+ // per rendered frame).
+ // Going through all those and applying them would waste a lot of time when
+ // we already know that the size is not final and will change again anyway.
+ skippedConfigures++;
+ lastConfigureMessage = std::move(guard);
+ break;
+ case WinSystemWaylandProtocol::OUTPUT_HOTPLUG:
+ {
+ CLog::LogF(LOGDEBUG, "Output hotplug, re-reading resolutions");
+ UpdateResolutions();
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ auto const& desktopRes = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP);
+ auto output = FindOutputByUserFriendlyName(desktopRes.strOutput);
+ auto const& wlOutput = output->GetWaylandOutput();
+ // Maybe the output that was added was the one we should be on?
+ if (m_bFullScreen && m_lastSetOutput != wlOutput)
+ {
+ CLog::LogF(LOGDEBUG, "Output hotplug resulted in monitor set in settings appearing, switching");
+ // Switch to this output
+ m_lastSetOutput = wlOutput;
+ m_shellSurface->SetFullScreen(wlOutput, desktopRes.fRefreshRate);
+ // SetOutput will result in a configure that updates the actual context size
+ }
+ }
+ break;
+ case WinSystemWaylandProtocol::BUFFER_SCALE:
+ // Never update buffer scale if not possible to set it
+ if (m_surface.can_set_buffer_scale())
+ {
+ newScale = (reinterpret_cast<WinSystemWaylandProtocol::MsgBufferScale*> (message->data))->scale;
+ }
+ break;
+ }
+ }
+
+ if (lastConfigureMessage)
+ {
+ if (skippedConfigures > 0)
+ {
+ CLog::LogF(LOGDEBUG, "Skipped {} configures", skippedConfigures);
+ }
+ // Wayland will tell us here the size of the surface that was actually created,
+ // which might be different from what we expected e.g. when fullscreening
+ // on an output we chose - the compositor might have decided to use a different
+ // output for example
+ // It is very important that the EGL native module and the rendering system use the
+ // Wayland-announced size for rendering or corrupted graphics output will result.
+ auto configure = reinterpret_cast<WinSystemWaylandProtocol::MsgConfigure*> (lastConfigureMessage.Get()->data);
+ CLog::LogF(LOGDEBUG, "Configure serial {}: size {}x{} state {}", configure->serial,
+ configure->surfaceSize.Width(), configure->surfaceSize.Height(),
+ IShellSurface::StateToString(configure->state));
+
+
+ CSizeInt size = configure->surfaceSize;
+ bool sizeIncludesDecoration = true;
+
+ if (size.IsZero())
+ {
+ if (configure->state.test(IShellSurface::STATE_FULLSCREEN))
+ {
+ // Do not change current size - UpdateWithConfiguredSize must be called regardless in case
+ // scale or something else changed
+ size = m_configuredSize;
+ }
+ else
+ {
+ // Compositor has no preference and we're windowed
+ // -> adopt windowed size that Kodi wants
+ auto const& windowed = CDisplaySettings::GetInstance().GetResolutionInfo(RES_WINDOW);
+ // Kodi resolution is buffer size, but SetResolutionInternal expects
+ // surface size, so divide by m_scale
+ size = CSizeInt{windowed.iWidth, windowed.iHeight} / newScale;
+ CLog::LogF(LOGDEBUG, "Adapting Kodi windowed size {}x{}", size.Width(), size.Height());
+ sizeIncludesDecoration = false;
+ }
+ }
+
+ SetResolutionInternal(size, newScale, configure->state, sizeIncludesDecoration, true, configure->serial);
+ }
+ // If we were also configured, scale is already taken care of. But it could
+ // also be a scale change without configure, so apply that.
+ else if (m_scale != newScale)
+ {
+ SetResolutionInternal(m_configuredSize, newScale, m_shellSurfaceState, true, false);
+ }
+}
+
+void CWinSystemWayland::ApplyShellSurfaceState(IShellSurface::StateBitset state)
+{
+ m_windowDecorator->SetState(m_configuredSize, m_scale, state);
+ m_shellSurfaceState = state;
+}
+
+void CWinSystemWayland::OnConfigure(std::uint32_t serial, CSizeInt size, IShellSurface::StateBitset state)
+{
+ if (m_shellSurfaceInitializing)
+ {
+ CLog::LogF(LOGDEBUG, "Initial configure serial {}: size {}x{} state {}", serial, size.Width(),
+ size.Height(), IShellSurface::StateToString(state));
+ m_shellSurfaceState = state;
+ if (!size.IsZero())
+ {
+ UpdateSizeVariables(size, m_scale, m_shellSurfaceState, true);
+ }
+ AckConfigure(serial);
+ }
+ else
+ {
+ WinSystemWaylandProtocol::MsgConfigure msg{serial, size, state};
+ m_protocol.SendOutMessage(WinSystemWaylandProtocol::CONFIGURE, &msg, sizeof(msg));
+ }
+}
+
+void CWinSystemWayland::AckConfigure(std::uint32_t serial)
+{
+ // Send ack if we have a new serial number or this is the first time
+ // this function is called
+ if (serial != m_lastAckedSerial || !m_firstSerialAcked)
+ {
+ CLog::LogF(LOGDEBUG, "Acking serial {}", serial);
+ m_shellSurface->AckConfigure(serial);
+ m_lastAckedSerial = serial;
+ m_firstSerialAcked = true;
+ }
+}
+
+/**
+ * Recalculate sizes from given parameters, apply them and update Kodi CDisplaySettings
+ * resolution if necessary
+ *
+ * This function should be called when events internal to the windowing system
+ * such as a compositor configure lead to a size change.
+ *
+ * Call only from main thread.
+ *
+ * \param size configured size, can be zero if compositor does not have a preference
+ * \param scale new buffer scale
+ * \param sizeIncludesDecoration whether size includes the size of the window decorations if present
+ */
+void CWinSystemWayland::SetResolutionInternal(CSizeInt size, std::int32_t scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration, bool mustAck, std::uint32_t configureSerial)
+{
+ // This should never be called while a size set is pending
+ assert(!m_waitingForApply);
+
+ bool fullScreen{state.test(IShellSurface::STATE_FULLSCREEN)};
+ auto sizes = CalculateSizes(size, scale, state, sizeIncludesDecoration);
+
+ CLog::LogF(LOGDEBUG, "Set size for serial {}: {}x{} {} decoration at scale {} state {}",
+ configureSerial, size.Width(), size.Height(),
+ sizeIncludesDecoration ? "including" : "excluding", scale,
+ IShellSurface::StateToString(state));
+
+ // Get actual frame rate from monitor, take highest frame rate if multiple
+ float refreshRate{m_fRefreshRate};
+ {
+ std::unique_lock<CCriticalSection> lock(m_surfaceOutputsMutex);
+ auto maxRefreshIt = std::max_element(m_surfaceOutputs.cbegin(), m_surfaceOutputs.cend(), OutputCurrentRefreshRateComparer());
+ if (maxRefreshIt != m_surfaceOutputs.cend())
+ {
+ refreshRate = (*maxRefreshIt)->GetCurrentMode().GetRefreshInHz();
+ CLog::LogF(LOGDEBUG, "Resolved actual (maximum) refresh rate to {:.3f} Hz on output \"{}\"",
+ refreshRate, UserFriendlyOutputName(*maxRefreshIt));
+ }
+ }
+
+ m_next.mustBeAcked = mustAck;
+ m_next.configureSerial = configureSerial;
+ m_next.configuredSize = sizes.configuredSize;
+ m_next.scale = scale;
+ m_next.shellSurfaceState = state;
+
+ // Check if any parameters of the Kodi resolution configuration changed
+ if (refreshRate != m_fRefreshRate || sizes.bufferSize != m_bufferSize || m_bFullScreen != fullScreen)
+ {
+ if (!fullScreen)
+ {
+ if (m_bFullScreen)
+ {
+ XBMC_Event msg{};
+ msg.type = XBMC_MODECHANGE;
+ msg.mode.res = RES_WINDOW;
+ SetWindowResolution(sizes.bufferSize.Width(), sizes.bufferSize.Height());
+ // FIXME
+ dynamic_cast<CWinEventsWayland&>(*m_winEvents).MessagePush(&msg);
+ m_waitingForApply = true;
+ CLog::LogF(LOGDEBUG, "Queued change to windowed mode size {}x{}", sizes.bufferSize.Width(),
+ sizes.bufferSize.Height());
+ }
+ else
+ {
+ XBMC_Event msg{};
+ msg.type = XBMC_VIDEORESIZE;
+ msg.resize = {sizes.bufferSize.Width(), sizes.bufferSize.Height()};
+ // FIXME
+ dynamic_cast<CWinEventsWayland&>(*m_winEvents).MessagePush(&msg);
+ m_waitingForApply = true;
+ CLog::LogF(LOGDEBUG, "Queued change to windowed buffer size {}x{}",
+ sizes.bufferSize.Width(), sizes.bufferSize.Height());
+ }
+ }
+ else
+ {
+ // Find matching Kodi resolution member
+ RESOLUTION res{FindMatchingCustomResolution(sizes.bufferSize, refreshRate)};
+ if (res == RES_INVALID)
+ {
+ // Add new resolution if none found
+ RESOLUTION_INFO newResInfo;
+ // we just assume the compositor put us on the right output
+ UpdateDesktopResolution(newResInfo, CDisplaySettings::GetInstance().GetCurrentResolutionInfo().strOutput, sizes.bufferSize.Width(), sizes.bufferSize.Height(), refreshRate, 0);
+ CDisplaySettings::GetInstance().AddResolutionInfo(newResInfo);
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+ res = static_cast<RESOLUTION> (CDisplaySettings::GetInstance().ResolutionInfoSize() - 1);
+ }
+
+ XBMC_Event msg{};
+ msg.type = XBMC_MODECHANGE;
+ msg.mode.res = res;
+ // FIXME
+ dynamic_cast<CWinEventsWayland&>(*m_winEvents).MessagePush(&msg);
+ m_waitingForApply = true;
+ CLog::LogF(LOGDEBUG, "Queued change to resolution {} surface size {}x{} scale {} state {}",
+ res, sizes.surfaceSize.Width(), sizes.surfaceSize.Height(), scale,
+ IShellSurface::StateToString(state));
+ }
+ }
+ else
+ {
+ // Apply directly, Kodi resolution does not change
+ ApplyNextState();
+ }
+}
+
+void CWinSystemWayland::FinishModeChange(RESOLUTION res)
+{
+ const auto& resInfo = CDisplaySettings::GetInstance().GetResolutionInfo(res);
+
+ ApplyNextState();
+
+ m_fRefreshRate = resInfo.fRefreshRate;
+ m_bFullScreen = resInfo.bFullScreen;
+ m_waitingForApply = false;
+}
+
+void CWinSystemWayland::FinishWindowResize(int, int)
+{
+ ApplyNextState();
+ m_waitingForApply = false;
+}
+
+void CWinSystemWayland::ApplyNextState()
+{
+ CLog::LogF(LOGDEBUG, "Applying next state: serial {} configured size {}x{} scale {} state {}",
+ m_next.configureSerial, m_next.configuredSize.Width(), m_next.configuredSize.Height(),
+ m_next.scale, IShellSurface::StateToString(m_next.shellSurfaceState));
+
+ ApplyShellSurfaceState(m_next.shellSurfaceState);
+ auto updateResult = UpdateSizeVariables(m_next.configuredSize, m_next.scale, m_next.shellSurfaceState, true);
+ ApplySizeUpdate(updateResult);
+
+ if (m_next.mustBeAcked)
+ {
+ AckConfigure(m_next.configureSerial);
+ }
+}
+
+CWinSystemWayland::Sizes CWinSystemWayland::CalculateSizes(CSizeInt size, int scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration)
+{
+ Sizes result;
+
+ // Clamp to a sensible range
+ constexpr int MIN_WIDTH{300};
+ constexpr int MIN_HEIGHT{200};
+ if (size.Width() < MIN_WIDTH)
+ {
+ CLog::LogF(LOGWARNING, "Width {} is very small, clamping to {}", size.Width(), MIN_WIDTH);
+ size.SetWidth(MIN_WIDTH);
+ }
+ if (size.Height() < MIN_HEIGHT)
+ {
+ CLog::LogF(LOGWARNING, "Height {} is very small, clamping to {}", size.Height(), MIN_HEIGHT);
+ size.SetHeight(MIN_HEIGHT);
+ }
+
+ // Depending on whether the size has decorations included (i.e. comes from the
+ // compositor or from Kodi), we need to calculate differently
+ if (sizeIncludesDecoration)
+ {
+ result.configuredSize = size;
+ result.surfaceSize = m_windowDecorator->CalculateMainSurfaceSize(size, state);
+ }
+ else
+ {
+ result.surfaceSize = size;
+ result.configuredSize = m_windowDecorator->CalculateFullSurfaceSize(size, state);
+ }
+
+ result.bufferSize = result.surfaceSize * scale;
+
+ return result;
+}
+
+
+/**
+ * Calculate internal resolution from surface size and set variables
+ *
+ * \param next surface size
+ * \param scale new buffer scale
+ * \param state window state to determine whether decorations are enabled at all
+ * \param sizeIncludesDecoration if true, given size includes potential window decorations
+ * \return whether main buffer (not surface) size changed
+ */
+CWinSystemWayland::SizeUpdateInformation CWinSystemWayland::UpdateSizeVariables(CSizeInt size, int scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration)
+{
+ CLog::LogF(LOGDEBUG, "Set size {}x{} scale {} {} decorations with state {}", size.Width(),
+ size.Height(), scale, sizeIncludesDecoration ? "including" : "excluding",
+ IShellSurface::StateToString(state));
+
+ auto oldSurfaceSize = m_surfaceSize;
+ auto oldBufferSize = m_bufferSize;
+ auto oldConfiguredSize = m_configuredSize;
+ auto oldBufferScale = m_scale;
+
+ m_scale = scale;
+ auto sizes = CalculateSizes(size, scale, state, sizeIncludesDecoration);
+ m_surfaceSize = sizes.surfaceSize;
+ m_bufferSize = sizes.bufferSize;
+ m_configuredSize = sizes.configuredSize;
+
+ SizeUpdateInformation changes{m_surfaceSize != oldSurfaceSize, m_bufferSize != oldBufferSize, m_configuredSize != oldConfiguredSize, m_scale != oldBufferScale};
+
+ if (changes.surfaceSizeChanged)
+ {
+ CLog::LogF(LOGINFO, "Surface size changed: {}x{} -> {}x{}", oldSurfaceSize.Width(),
+ oldSurfaceSize.Height(), m_surfaceSize.Width(), m_surfaceSize.Height());
+ }
+ if (changes.bufferSizeChanged)
+ {
+ CLog::LogF(LOGINFO, "Buffer size changed: {}x{} -> {}x{}", oldBufferSize.Width(),
+ oldBufferSize.Height(), m_bufferSize.Width(), m_bufferSize.Height());
+ }
+ if (changes.configuredSizeChanged)
+ {
+ CLog::LogF(LOGINFO, "Configured size changed: {}x{} -> {}x{}", oldConfiguredSize.Width(),
+ oldConfiguredSize.Height(), m_configuredSize.Width(), m_configuredSize.Height());
+ }
+ if (changes.bufferScaleChanged)
+ {
+ CLog::LogF(LOGINFO, "Buffer scale changed: {} -> {}", oldBufferScale, m_scale);
+ }
+
+ return changes;
+}
+
+std::string CWinSystemWayland::UserFriendlyOutputName(std::shared_ptr<COutput> const& output)
+{
+ std::vector<std::string> parts;
+ if (!output->GetMake().empty())
+ {
+ parts.emplace_back(output->GetMake());
+ }
+ if (!output->GetModel().empty())
+ {
+ parts.emplace_back(output->GetModel());
+ }
+ if (parts.empty())
+ {
+ // Fallback to "unknown" if no name received from compositor
+ parts.emplace_back(g_localizeStrings.Get(13205));
+ }
+
+ // Add position
+ auto pos = output->GetPosition();
+ if (pos.x != 0 || pos.y != 0)
+ {
+ parts.emplace_back(StringUtils::Format("@{}x{}", pos.x, pos.y));
+ }
+
+ return StringUtils::Join(parts, " ");
+}
+
+bool CWinSystemWayland::Minimize()
+{
+ m_shellSurface->SetMinimized();
+ return true;
+}
+
+bool CWinSystemWayland::HasCursor()
+{
+ std::unique_lock<CCriticalSection> lock(m_seatsMutex);
+ return std::any_of(m_seats.cbegin(), m_seats.cend(),
+ [](decltype(m_seats)::value_type const& entry)
+ {
+ return entry.second.HasPointerCapability();
+ });
+}
+
+void CWinSystemWayland::ShowOSMouse(bool show)
+{
+ m_osCursorVisible = show;
+}
+
+void CWinSystemWayland::LoadDefaultCursor()
+{
+ if (!m_cursorSurface)
+ {
+ // Load default cursor theme and default cursor
+ // Size of 24px is what most themes seem to have
+ m_cursorTheme = wayland::cursor_theme_t("", 24, m_shm);
+ wayland::cursor_t cursor;
+ try
+ {
+ cursor = CCursorUtil::LoadFromTheme(m_cursorTheme, "default");
+ }
+ catch (std::exception const& e)
+ {
+ CLog::Log(LOGWARNING, "Could not load default cursor from theme, continuing without OS cursor");
+ }
+ // Just use the first image, do not handle animation
+ m_cursorImage = cursor.image(0);
+ m_cursorBuffer = m_cursorImage.get_buffer();
+ m_cursorSurface = m_compositor.create_surface();
+ }
+ // Attach buffer to a surface - it seems that the compositor may change
+ // the cursor surface when the pointer leaves our surface, so we reattach the
+ // buffer each time
+ m_cursorSurface.attach(m_cursorBuffer, 0, 0);
+ m_cursorSurface.damage(0, 0, m_cursorImage.width(), m_cursorImage.height());
+ m_cursorSurface.commit();
+}
+
+void CWinSystemWayland::Register(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_dispResourcesMutex);
+ m_dispResources.emplace(resource);
+}
+
+void CWinSystemWayland::Unregister(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_dispResourcesMutex);
+ m_dispResources.erase(resource);
+}
+
+void CWinSystemWayland::OnSeatAdded(std::uint32_t name, wayland::proxy_t&& proxy)
+{
+ std::unique_lock<CCriticalSection> lock(m_seatsMutex);
+
+ wayland::seat_t seat(proxy);
+ auto newSeatEmplace = m_seats.emplace(std::piecewise_construct,
+ std::forward_as_tuple(name),
+ std::forward_as_tuple(name, seat, *m_connection));
+
+ auto& seatInst = newSeatEmplace.first->second;
+ m_seatInputProcessing->AddSeat(&seatInst);
+ m_windowDecorator->AddSeat(&seatInst);
+}
+
+void CWinSystemWayland::OnSeatRemoved(std::uint32_t name)
+{
+ std::unique_lock<CCriticalSection> lock(m_seatsMutex);
+
+ auto seatI = m_seats.find(name);
+ if (seatI != m_seats.end())
+ {
+ m_seatInputProcessing->RemoveSeat(&seatI->second);
+ m_windowDecorator->RemoveSeat(&seatI->second);
+ m_seats.erase(name);
+ }
+}
+
+void CWinSystemWayland::OnOutputAdded(std::uint32_t name, wayland::proxy_t&& proxy)
+{
+ wayland::output_t output(proxy);
+ // This is not accessed from multiple threads
+ m_outputsInPreparation.emplace(name, std::make_shared<COutput>(name, output, std::bind(&CWinSystemWayland::OnOutputDone, this, name)));
+}
+
+void CWinSystemWayland::OnOutputDone(std::uint32_t name)
+{
+ auto it = m_outputsInPreparation.find(name);
+ if (it != m_outputsInPreparation.end())
+ {
+ // This output was added for the first time - done is also sent when
+ // output parameters change later
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ // Move from m_outputsInPreparation to m_outputs
+ m_outputs.emplace(std::move(*it));
+ m_outputsInPreparation.erase(it);
+ }
+
+ m_protocol.SendOutMessage(WinSystemWaylandProtocol::OUTPUT_HOTPLUG);
+ }
+
+ UpdateBufferScale();
+}
+
+void CWinSystemWayland::OnOutputRemoved(std::uint32_t name)
+{
+ m_outputsInPreparation.erase(name);
+
+ std::unique_lock<CCriticalSection> lock(m_outputsMutex);
+ if (m_outputs.erase(name) != 0)
+ {
+ // Theoretically, the compositor should automatically put us on another
+ // (visible and connected) output if the output we were on is lost,
+ // so there is nothing in particular to do here
+ }
+}
+
+void CWinSystemWayland::SendFocusChange(bool focus)
+{
+ g_application.m_AppFocused = focus;
+ std::unique_lock<CCriticalSection> lock(m_dispResourcesMutex);
+ for (auto dispResource : m_dispResources)
+ {
+ dispResource->OnAppFocusChange(focus);
+ }
+}
+
+void CWinSystemWayland::OnEnter(InputType type)
+{
+ // Couple to keyboard focus
+ if (type == InputType::KEYBOARD)
+ {
+ SendFocusChange(true);
+ }
+ if (type == InputType::POINTER)
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(true);
+ }
+}
+
+void CWinSystemWayland::OnLeave(InputType type)
+{
+ // Couple to keyboard focus
+ if (type == InputType::KEYBOARD)
+ {
+ SendFocusChange(false);
+ }
+ if (type == InputType::POINTER)
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ }
+}
+
+void CWinSystemWayland::OnEvent(InputType type, XBMC_Event& event)
+{
+ // FIXME
+ dynamic_cast<CWinEventsWayland&>(*m_winEvents).MessagePush(&event);
+}
+
+void CWinSystemWayland::OnSetCursor(std::uint32_t seatGlobalName, std::uint32_t serial)
+{
+ auto seatI = m_seats.find(seatGlobalName);
+ if (seatI == m_seats.end())
+ {
+ return;
+ }
+
+ if (m_osCursorVisible)
+ {
+ LoadDefaultCursor();
+ if (m_cursorSurface) // Cursor loading could have failed
+ {
+ seatI->second.SetCursor(serial, m_cursorSurface, m_cursorImage.hotspot_x(), m_cursorImage.hotspot_y());
+ }
+ }
+ else
+ {
+ seatI->second.SetCursor(serial, wayland::surface_t{}, 0, 0);
+ }
+}
+
+void CWinSystemWayland::UpdateBufferScale()
+{
+ // Adjust our surface size to the output with the biggest scale in order
+ // to get the best quality
+ auto const maxBufferScaleIt = std::max_element(m_surfaceOutputs.cbegin(), m_surfaceOutputs.cend(), OutputScaleComparer());
+ if (maxBufferScaleIt != m_surfaceOutputs.cend())
+ {
+ WinSystemWaylandProtocol::MsgBufferScale msg{(*maxBufferScaleIt)->GetScale()};
+ m_protocol.SendOutMessage(WinSystemWaylandProtocol::BUFFER_SCALE, &msg, sizeof(msg));
+ }
+}
+
+void CWinSystemWayland::ApplyBufferScale()
+{
+ CLog::LogF(LOGINFO, "Setting Wayland buffer scale to {}", m_scale);
+ m_surface.set_buffer_scale(m_scale);
+ m_windowDecorator->SetState(m_configuredSize, m_scale, m_shellSurfaceState);
+ m_seatInputProcessing->SetCoordinateScale(m_scale);
+}
+
+void CWinSystemWayland::UpdateTouchDpi()
+{
+ // If we have multiple outputs with wildly different DPI, this is really just
+ // guesswork to get a halfway reasonable value. min/max would probably also be OK.
+ float dpiSum = std::accumulate(m_surfaceOutputs.cbegin(), m_surfaceOutputs.cend(), 0.0f,
+ [](float acc, std::shared_ptr<COutput> const& output)
+ {
+ return acc + output->GetCurrentDpi();
+ });
+ float dpi = dpiSum / m_surfaceOutputs.size();
+ CLog::LogF(LOGDEBUG, "Computed average dpi of {:.3f} for touch handler", dpi);
+ CGenericTouchInputHandler::GetInstance().SetScreenDPI(dpi);
+}
+
+CWinSystemWayland::SurfaceSubmission::SurfaceSubmission(timespec const& submissionTime, wayland::presentation_feedback_t const& feedback)
+: submissionTime{submissionTime}, feedback{feedback}
+{
+}
+
+timespec CWinSystemWayland::GetPresentationClockTime()
+{
+ timespec time;
+ if (clock_gettime(m_presentationClock, &time) != 0)
+ {
+ throw std::system_error(errno, std::generic_category(), "Error getting time from Wayland presentation clock with clock_gettime");
+ }
+ return time;
+}
+
+void CWinSystemWayland::PrepareFramePresentation()
+{
+ // Continuously measure display latency (i.e. time between when the frame was rendered
+ // and when it becomes visible to the user) to correct AV sync
+ if (m_presentation)
+ {
+ auto tStart = GetPresentationClockTime();
+ // wp_presentation_feedback creation is coupled to the surface's commit().
+ // eglSwapBuffers() (which will be called after this) will call commit().
+ // This creates a new Wayland protocol object in the main thread, but this
+ // will not result in a race since the corresponding events are never sent
+ // before commit() on the surface, which only occurs afterwards.
+ auto feedback = m_presentation.feedback(m_surface);
+ // Save feedback objects in list so they don't get destroyed upon exit of this function
+ // Hand iterator to lambdas so they do not hold a (then circular) reference
+ // to the actual object
+ decltype(m_surfaceSubmissions)::iterator iter;
+ {
+ std::unique_lock<CCriticalSection> lock(m_surfaceSubmissionsMutex);
+ iter = m_surfaceSubmissions.emplace(m_surfaceSubmissions.end(), tStart, feedback);
+ }
+
+ feedback.on_sync_output() = [this](const wayland::output_t& wloutput) {
+ m_syncOutputID = wloutput.get_id();
+ auto output = FindOutputByWaylandOutput(wloutput);
+ if (output)
+ {
+ m_syncOutputRefreshRate = output->GetCurrentMode().GetRefreshInHz();
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Could not find Wayland output that is supposedly the sync output");
+ }
+ };
+ feedback.on_presented() = [this, iter](std::uint32_t tvSecHi, std::uint32_t tvSecLo,
+ std::uint32_t tvNsec, std::uint32_t refresh,
+ std::uint32_t seqHi, std::uint32_t seqLo,
+ const wayland::presentation_feedback_kind& flags) {
+ timespec tv = { .tv_sec = static_cast<std::time_t> ((static_cast<std::uint64_t>(tvSecHi) << 32) + tvSecLo), .tv_nsec = static_cast<long>(tvNsec) };
+ std::int64_t latency{KODI::LINUX::TimespecDifference(iter->submissionTime, tv)};
+ std::uint64_t msc{(static_cast<std::uint64_t>(seqHi) << 32) + seqLo};
+ m_presentationFeedbackHandlers.Invoke(tv, refresh, m_syncOutputID, m_syncOutputRefreshRate, msc);
+
+ iter->latency = latency / 1e9f; // nanoseconds to seconds
+ float adjust{};
+ {
+ std::unique_lock<CCriticalSection> lock(m_surfaceSubmissionsMutex);
+ if (m_surfaceSubmissions.size() > LATENCY_MOVING_AVERAGE_SIZE)
+ {
+ adjust = - m_surfaceSubmissions.front().latency / LATENCY_MOVING_AVERAGE_SIZE;
+ m_surfaceSubmissions.pop_front();
+ }
+ }
+ m_latencyMovingAverage = m_latencyMovingAverage + iter->latency / LATENCY_MOVING_AVERAGE_SIZE + adjust;
+
+ CLog::Log(LOGDEBUG, LOGAVTIMING, "Presentation feedback: {} ns -> moving average {:f} s",
+ latency, static_cast<double>(m_latencyMovingAverage));
+ };
+ feedback.on_discarded() = [this,iter]()
+ {
+ CLog::Log(LOGDEBUG, "Presentation: Frame was discarded by compositor");
+ std::unique_lock<CCriticalSection> lock(m_surfaceSubmissionsMutex);
+ m_surfaceSubmissions.erase(iter);
+ };
+ }
+
+ // Now wait for the frame callback that tells us that it is a good time to start drawing
+ //
+ // To sum up, we:
+ // 1. wait until a frame() drawing hint from the compositor arrives,
+ // 2. request a new frame() hint for the next presentation
+ // 2. then commit the backbuffer to the surface and immediately
+ // return, i.e. drawing can start again
+ // This means that rendering is optimized for maximum time available for
+ // our repaint and reliable timing rather than latency. With weston, latency
+ // will usually be on the order of two frames plus a few milliseconds.
+ // The frame timings become irregular though when nothing is rendered because
+ // kodi then sleeps for a fixed time without swapping buffers. This makes us
+ // immediately attach the next buffer because the frame callback has already arrived when
+ // this function is called and step 1. above is skipped. As we render with full
+ // FPS during video playback anyway and the timing is otherwise not relevant,
+ // this should not be a problem.
+ if (m_frameCallback)
+ {
+ // If the window is e.g. minimized, chances are that we will *never* get frame
+ // callbacks from the compositor for optimization reasons.
+ // Still, the app should remain functional, which means that we can't
+ // just block forever here - if the render thread is blocked, Kodi will not
+ // function normally. It would also be impossible to close the application
+ // while it is minimized (since the wait needs to be interrupted for that).
+ // -> Use Wait with timeout here so we can maintain a reasonable frame rate
+ // even when the window is not visible and we do not get any frame callbacks.
+ if (m_frameCallbackEvent.Wait(50ms))
+ {
+ // Only reset frame callback object a callback was received so a
+ // new one is not requested continuously
+ m_frameCallback = {};
+ m_frameCallbackEvent.Reset();
+ }
+ }
+
+ if (!m_frameCallback)
+ {
+ // Get frame callback event for checking in the next call to this function
+ m_frameCallback = m_surface.frame();
+ m_frameCallback.on_done() = [this](std::uint32_t)
+ {
+ m_frameCallbackEvent.Set();
+ };
+ }
+}
+
+void CWinSystemWayland::FinishFramePresentation()
+{
+ ProcessMessages();
+
+ m_frameStartTime = std::chrono::steady_clock::now();
+}
+
+float CWinSystemWayland::GetFrameLatencyAdjustment()
+{
+ const auto now = std::chrono::steady_clock::now();
+ const std::chrono::duration<float, std::milli> duration = now - m_frameStartTime;
+ return duration.count();
+}
+
+float CWinSystemWayland::GetDisplayLatency()
+{
+ if (m_presentation)
+ {
+ return m_latencyMovingAverage * 1000.0f;
+ }
+ else
+ {
+ return CWinSystemBase::GetDisplayLatency();
+ }
+}
+
+float CWinSystemWayland::GetSyncOutputRefreshRate()
+{
+ return m_syncOutputRefreshRate;
+}
+
+KODI::CSignalRegistration CWinSystemWayland::RegisterOnPresentationFeedback(
+ const PresentationFeedbackHandler& handler)
+{
+ return m_presentationFeedbackHandlers.Register(handler);
+}
+
+std::unique_ptr<CVideoSync> CWinSystemWayland::GetVideoSync(void* clock)
+{
+ if (m_surface && m_presentation)
+ {
+ CLog::LogF(LOGINFO, "Using presentation protocol for video sync");
+ return std::unique_ptr<CVideoSync>(new CVideoSyncWpPresentation(clock, *this));
+ }
+ else
+ {
+ CLog::LogF(LOGINFO, "No supported method for video sync found");
+ return nullptr;
+ }
+}
+
+std::unique_ptr<IOSScreenSaver> CWinSystemWayland::GetOSScreenSaverImpl()
+{
+ if (m_surface)
+ {
+ std::unique_ptr<IOSScreenSaver> ptr;
+ ptr.reset(COSScreenSaverIdleInhibitUnstableV1::TryCreate(*m_connection, m_surface));
+ if (ptr)
+ {
+ CLog::LogF(LOGINFO, "Using idle-inhibit-unstable-v1 protocol for screen saver inhibition");
+ return ptr;
+ }
+ }
+
+#if defined(HAS_DBUS)
+ if (KODI::WINDOWING::LINUX::COSScreenSaverFreedesktop::IsAvailable())
+ {
+ CLog::LogF(LOGINFO, "Using freedesktop.org DBus interface for screen saver inhibition");
+ return std::unique_ptr<IOSScreenSaver>(new KODI::WINDOWING::LINUX::COSScreenSaverFreedesktop);
+ }
+#endif
+
+ CLog::LogF(LOGINFO, "No supported method for screen saver inhibition found");
+ return std::unique_ptr<IOSScreenSaver>(new CDummyOSScreenSaver);
+}
+
+std::string CWinSystemWayland::GetClipboardText()
+{
+ std::unique_lock<CCriticalSection> lock(m_seatsMutex);
+ // Get text of first seat with non-empty selection
+ // Actually, the value of the seat that received the Ctrl+V keypress should be used,
+ // but this would need a workaround or proper multi-seat support in Kodi - it's
+ // probably just not that relevant in practice
+ for (auto const& seat : m_seats)
+ {
+ auto text = seat.second.GetSelectionText();
+ if (text != "")
+ {
+ return text;
+ }
+ }
+ return "";
+}
+
+void CWinSystemWayland::OnWindowMove(const wayland::seat_t& seat, std::uint32_t serial)
+{
+ m_shellSurface->StartMove(seat, serial);
+}
+
+void CWinSystemWayland::OnWindowResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge)
+{
+ m_shellSurface->StartResize(seat, serial, edge);
+}
+
+void CWinSystemWayland::OnWindowShowContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position)
+{
+ m_shellSurface->ShowShellContextMenu(seat, serial, position);
+}
+
+void CWinSystemWayland::OnWindowClose()
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+}
+
+void CWinSystemWayland::OnWindowMinimize()
+{
+ m_shellSurface->SetMinimized();
+}
+
+void CWinSystemWayland::OnWindowMaximize()
+{
+ if (m_shellSurfaceState.test(IShellSurface::STATE_MAXIMIZED))
+ {
+ m_shellSurface->UnsetMaximized();
+ }
+ else
+ {
+ m_shellSurface->SetMaximized();
+ }
+}
+
+void CWinSystemWayland::OnClose()
+{
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+}
+
+bool CWinSystemWayland::MessagePump()
+{
+ return m_winEvents->MessagePump();
+}
diff --git a/xbmc/windowing/wayland/WinSystemWayland.h b/xbmc/windowing/wayland/WinSystemWayland.h
new file mode 100644
index 0000000..514b8d6
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWayland.h
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2017-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 "Connection.h"
+#include "Output.h"
+#include "Seat.h"
+#include "SeatInputProcessing.h"
+#include "ShellSurface.h"
+#include "Signals.h"
+#include "WindowDecorationHandler.h"
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "utils/ActorProtocol.h"
+#include "windowing/WinSystem.h"
+
+#include <atomic>
+#include <chrono>
+#include <ctime>
+#include <list>
+#include <map>
+#include <set>
+#include <time.h>
+
+#include <wayland-client.hpp>
+#include <wayland-cursor.hpp>
+#include <wayland-extra-protocols.hpp>
+
+class IDispResource;
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CRegistry;
+class CWindowDecorator;
+
+class CWinSystemWayland : public CWinSystemBase, IInputHandler, IWindowDecorationHandler, IShellSurfaceHandler
+{
+public:
+ CWinSystemWayland();
+ ~CWinSystemWayland() noexcept override;
+
+ const std::string GetName() override { return "wayland"; }
+
+ 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 FinishModeChange(RESOLUTION res) override;
+ void FinishWindowResize(int newWidth, int newHeight) override;
+
+ bool UseLimitedColor() override;
+
+ void UpdateResolutions() override;
+
+ bool CanDoWindowed() override;
+ bool Minimize() override;
+
+ bool HasCursor() override;
+ void ShowOSMouse(bool show) override;
+
+ std::string GetClipboardText() override;
+
+ float GetSyncOutputRefreshRate();
+ float GetDisplayLatency() override;
+ float GetFrameLatencyAdjustment() override;
+ std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override;
+
+ void Register(IDispResource* resource) override;
+ void Unregister(IDispResource* resource) override;
+
+ using PresentationFeedbackHandler = std::function<void(timespec /* tv */, std::uint32_t /* refresh */, std::uint32_t /* sync output id */, float /* sync output fps */, std::uint64_t /* msc */)>;
+ CSignalRegistration RegisterOnPresentationFeedback(const PresentationFeedbackHandler& handler);
+
+ std::vector<std::string> GetConnectedOutputs() override;
+
+ // winevents override
+ bool MessagePump() override;
+
+protected:
+ std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override;
+ CSizeInt GetBufferSize() const
+ {
+ return m_bufferSize;
+ }
+ std::unique_ptr<CConnection> const& GetConnection()
+ {
+ return m_connection;
+ }
+ wayland::surface_t GetMainSurface()
+ {
+ return m_surface;
+ }
+
+ void PrepareFramePresentation();
+ void FinishFramePresentation();
+ virtual void SetContextSize(CSizeInt size) = 0;
+
+private:
+ // IInputHandler
+ void OnEnter(InputType type) override;
+ void OnLeave(InputType type) override;
+ void OnEvent(InputType type, XBMC_Event& event) override;
+ void OnSetCursor(std::uint32_t seatGlobalName, std::uint32_t serial) override;
+
+ // IWindowDecorationHandler
+ void OnWindowMove(const wayland::seat_t& seat, std::uint32_t serial) override;
+ void OnWindowResize(const wayland::seat_t& seat, std::uint32_t serial, wayland::shell_surface_resize edge) override;
+ void OnWindowShowContextMenu(const wayland::seat_t& seat, std::uint32_t serial, CPointInt position) override;
+ void OnWindowClose() override;
+ void OnWindowMaximize() override;
+ void OnWindowMinimize() override;
+
+ // IShellSurfaceHandler
+ void OnConfigure(std::uint32_t serial, CSizeInt size, IShellSurface::StateBitset state) override;
+ void OnClose() override;
+
+ // Registry handlers
+ void OnSeatAdded(std::uint32_t name, wayland::proxy_t&& seat);
+ void OnSeatRemoved(std::uint32_t name);
+ void OnOutputAdded(std::uint32_t name, wayland::proxy_t&& output);
+ void OnOutputRemoved(std::uint32_t name);
+
+ void LoadDefaultCursor();
+ void SendFocusChange(bool focus);
+ bool SetResolutionExternal(bool fullScreen, RESOLUTION_INFO const& res);
+ void SetResolutionInternal(CSizeInt size, int scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration, bool mustAck = false, std::uint32_t configureSerial = 0u);
+ struct Sizes
+ {
+ CSizeInt surfaceSize;
+ CSizeInt bufferSize;
+ CSizeInt configuredSize;
+ };
+ Sizes CalculateSizes(CSizeInt size, int scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration);
+ struct SizeUpdateInformation
+ {
+ bool surfaceSizeChanged : 1;
+ bool bufferSizeChanged : 1;
+ bool configuredSizeChanged : 1;
+ bool bufferScaleChanged : 1;
+ };
+ SizeUpdateInformation UpdateSizeVariables(CSizeInt size, int scale, IShellSurface::StateBitset state, bool sizeIncludesDecoration);
+ void ApplySizeUpdate(SizeUpdateInformation update);
+ void ApplyNextState();
+
+ std::string UserFriendlyOutputName(std::shared_ptr<COutput> const& output);
+ std::shared_ptr<COutput> FindOutputByUserFriendlyName(std::string const& name);
+ std::shared_ptr<COutput> FindOutputByWaylandOutput(wayland::output_t const& output);
+
+ // Called when wl_output::done is received for an output, i.e. associated
+ // information like modes is available
+ void OnOutputDone(std::uint32_t name);
+ void UpdateBufferScale();
+ void ApplyBufferScale();
+ void ApplyOpaqueRegion();
+ void ApplyWindowGeometry();
+ void UpdateTouchDpi();
+ void ApplyShellSurfaceState(IShellSurface::StateBitset state);
+
+ void ProcessMessages();
+ void AckConfigure(std::uint32_t serial);
+
+ timespec GetPresentationClockTime();
+
+ // Globals
+ // -------
+ std::unique_ptr<CConnection> m_connection;
+ std::unique_ptr<CRegistry> m_registry;
+ /**
+ * Registry used exclusively for wayland::seat_t
+ *
+ * Use extra registry because seats can only be registered after the surface
+ * has been created
+ */
+ std::unique_ptr<CRegistry> m_seatRegistry;
+ wayland::compositor_t m_compositor;
+ wayland::shm_t m_shm;
+ wayland::presentation_t m_presentation;
+
+ std::unique_ptr<IShellSurface> m_shellSurface;
+
+ // Frame callback handling
+ // -----------------------
+ wayland::callback_t m_frameCallback;
+ CEvent m_frameCallbackEvent;
+
+ // Seat handling
+ // -------------
+ std::map<std::uint32_t, CSeat> m_seats;
+ CCriticalSection m_seatsMutex;
+ std::unique_ptr<CSeatInputProcessing> m_seatInputProcessing;
+ std::map<std::uint32_t, std::shared_ptr<COutput>> m_outputs;
+ /// outputs that did not receive their done event yet
+ std::map<std::uint32_t, std::shared_ptr<COutput>> m_outputsInPreparation;
+ CCriticalSection m_outputsMutex;
+
+ // Windowed mode
+ // -------------
+ std::unique_ptr<CWindowDecorator> m_windowDecorator;
+
+ // Cursor
+ // ------
+ bool m_osCursorVisible{true};
+ wayland::cursor_theme_t m_cursorTheme;
+ wayland::buffer_t m_cursorBuffer;
+ wayland::cursor_image_t m_cursorImage;
+ wayland::surface_t m_cursorSurface;
+
+ // Presentation feedback
+ // ---------------------
+ clockid_t m_presentationClock;
+ struct SurfaceSubmission
+ {
+ timespec submissionTime;
+ float latency;
+ wayland::presentation_feedback_t feedback;
+ SurfaceSubmission(timespec const& submissionTime, wayland::presentation_feedback_t const& feedback);
+ };
+ std::list<SurfaceSubmission> m_surfaceSubmissions;
+ CCriticalSection m_surfaceSubmissionsMutex;
+ /// Protocol object ID of the sync output returned by wp_presentation
+ std::uint32_t m_syncOutputID;
+ /// Refresh rate of sync output returned by wp_presentation
+ std::atomic<float> m_syncOutputRefreshRate{0.0f};
+ static constexpr int LATENCY_MOVING_AVERAGE_SIZE{30};
+ std::atomic<float> m_latencyMovingAverage;
+ CSignalHandlerList<PresentationFeedbackHandler> m_presentationFeedbackHandlers;
+
+ std::chrono::steady_clock::time_point m_frameStartTime{};
+
+ // IDispResource
+ // -------------
+ std::set<IDispResource*> m_dispResources;
+ CCriticalSection m_dispResourcesMutex;
+
+ // Surface state
+ // -------------
+ wayland::surface_t m_surface;
+ wayland::output_t m_lastSetOutput;
+ /// Set of outputs that show some part of our main surface as indicated by
+ /// compositor
+ std::set<std::shared_ptr<COutput>> m_surfaceOutputs;
+ CCriticalSection m_surfaceOutputsMutex;
+ /// Size of our surface in "surface coordinates" (i.e. without scaling applied)
+ /// and without window decorations
+ CSizeInt m_surfaceSize;
+ /// Size of the buffer that should back the surface (i.e. with scaling applied)
+ CSizeInt m_bufferSize;
+ /// Size of the whole window including window decorations as given by configure
+ CSizeInt m_configuredSize;
+ /// Scale in use for main surface buffer
+ int m_scale{1};
+ /// Shell surface state last acked
+ IShellSurface::StateBitset m_shellSurfaceState;
+ /// Whether the shell surface is waiting for initial configure
+ bool m_shellSurfaceInitializing{false};
+ struct
+ {
+ bool mustBeAcked{false};
+ std::uint32_t configureSerial{};
+ CSizeInt configuredSize;
+ int scale{1};
+ IShellSurface::StateBitset shellSurfaceState;
+ } m_next;
+ bool m_waitingForApply{false};
+
+ // Internal communication
+ // ----------------------
+ /// Protocol for communicating events to the main thread
+ Actor::Protocol m_protocol;
+
+ // Configure state
+ // ---------------
+ bool m_firstSerialAcked{false};
+ std::uint32_t m_lastAckedSerial{0u};
+ /// Whether this is the first call to SetFullScreen
+ bool m_isInitialSetFullScreen{true};
+};
+
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContext.cpp b/xbmc/windowing/wayland/WinSystemWaylandEGLContext.cpp
new file mode 100644
index 0000000..a32cd14
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContext.cpp
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2017-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 "WinSystemWaylandEGLContext.h"
+
+#include "Connection.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <EGL/eglext.h>
+
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace KODI::WINDOWING::LINUX;
+
+CWinSystemWaylandEGLContext::CWinSystemWaylandEGLContext()
+ : CWinSystemEGL{EGL_PLATFORM_WAYLAND_EXT, "EGL_EXT_platform_wayland"}
+{}
+
+bool CWinSystemWaylandEGLContext::InitWindowSystemEGL(EGLint renderableType, EGLint apiType)
+{
+ VIDEOPLAYER::CRendererFactory::ClearRenderer();
+ CDVDFactoryCodec::ClearHWAccels();
+
+ if (!CWinSystemWayland::InitWindowSystem())
+ {
+ return false;
+ }
+
+ if (!m_eglContext.CreatePlatformDisplay(GetConnection()->GetDisplay(), GetConnection()->GetDisplay()))
+ {
+ return false;
+ }
+
+ if (!m_eglContext.InitializeDisplay(apiType))
+ {
+ return false;
+ }
+
+ if (!m_eglContext.ChooseConfig(renderableType))
+ {
+ return false;
+ }
+
+ return true;
+}
+
+bool CWinSystemWaylandEGLContext::CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res)
+{
+ if (!CWinSystemWayland::CreateNewWindow(name, fullScreen, res))
+ {
+ return false;
+ }
+
+ if (!CreateContext())
+ {
+ return false;
+ }
+
+ m_nativeWindow = wayland::egl_window_t{GetMainSurface(), GetBufferSize().Width(), GetBufferSize().Height()};
+
+ // CWinSystemWayland::CreateNewWindow sets internal m_bufferSize
+ // to the resolution that should be used for the initial surface size
+ // - the compositor might want something other than the resolution given
+ if (!m_eglContext.CreatePlatformSurface(
+ m_nativeWindow.c_ptr(), reinterpret_cast<khronos_uintptr_t>(m_nativeWindow.c_ptr())))
+ {
+ return false;
+ }
+
+ if (!m_eglContext.BindContext())
+ {
+ return false;
+ }
+
+ // Never enable the vsync of the EGL implementation, we handle that ourselves
+ // in WinSystemWayland
+ m_eglContext.SetVSync(false);
+
+ return true;
+}
+
+bool CWinSystemWaylandEGLContext::DestroyWindow()
+{
+ m_eglContext.DestroySurface();
+ m_nativeWindow = {};
+
+ return CWinSystemWayland::DestroyWindow();
+}
+
+bool CWinSystemWaylandEGLContext::DestroyWindowSystem()
+{
+ m_eglContext.Destroy();
+
+ return CWinSystemWayland::DestroyWindowSystem();
+}
+
+CSizeInt CWinSystemWaylandEGLContext::GetNativeWindowAttachedSize()
+{
+ int width, height;
+ m_nativeWindow.get_attached_size(width, height);
+ return {width, height};
+}
+
+void CWinSystemWaylandEGLContext::SetContextSize(CSizeInt size)
+{
+ // Change EGL surface size if necessary
+ if (GetNativeWindowAttachedSize() != size)
+ {
+ CLog::LogF(LOGDEBUG, "Updating egl_window size to {}x{}", size.Width(), size.Height());
+ m_nativeWindow.resize(size.Width(), size.Height(), 0, 0);
+ }
+}
+
+void CWinSystemWaylandEGLContext::PresentFrame(bool rendered)
+{
+ PrepareFramePresentation();
+
+ if (rendered)
+ {
+ if (!m_eglContext.TrySwapBuffers())
+ {
+ // For now we just hard fail if this fails
+ // Theoretically, EGL_CONTEXT_LOST could be handled, but it needs to be checked
+ // whether egl implementations actually use it (mesa does not)
+ CEGLUtils::Log(LOGERROR, "eglSwapBuffers failed");
+ throw std::runtime_error("eglSwapBuffers failed");
+ }
+ // eglSwapBuffers() (hopefully) calls commit on the surface and flushes
+ // ... well mesa does anyway
+ }
+ else
+ {
+ // For presentation feedback: Get notification of the next vblank even
+ // when contents did not change
+ GetMainSurface().commit();
+ // Make sure it reaches the compositor
+ GetConnection()->GetDisplay().flush();
+ }
+
+ FinishFramePresentation();
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContext.h b/xbmc/windowing/wayland/WinSystemWaylandEGLContext.h
new file mode 100644
index 0000000..097b3e7
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContext.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017-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 "WinSystemWayland.h"
+#include "utils/EGLUtils.h"
+#include "windowing/linux/WinSystemEGL.h"
+
+#include <wayland-egl.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CWinSystemWaylandEGLContext : public KODI::WINDOWING::LINUX::CWinSystemEGL,
+ public CWinSystemWayland
+{
+public:
+ CWinSystemWaylandEGLContext();
+ ~CWinSystemWaylandEGLContext() override = default;
+
+ bool CreateNewWindow(const std::string& name,
+ bool fullScreen,
+ RESOLUTION_INFO& res) override;
+ bool DestroyWindow() override;
+ bool DestroyWindowSystem() override;
+
+protected:
+ /**
+ * Inheriting classes should override InitWindowSystem() without parameters
+ * and call this function there with appropriate parameters
+ */
+ bool InitWindowSystemEGL(EGLint renderableType, EGLint apiType);
+
+ CSizeInt GetNativeWindowAttachedSize();
+ void PresentFrame(bool rendered);
+ void SetContextSize(CSizeInt size) override;
+
+ virtual bool CreateContext() = 0;
+
+ wayland::egl_window_t m_nativeWindow;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.cpp b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.cpp
new file mode 100644
index 0000000..6e5a3b8
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.cpp
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2017-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 "WinSystemWaylandEGLContextGL.h"
+
+#include "OptionalsReg.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGL.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGL.h"
+#include "rendering/gl/ScreenshotSurfaceGL.h"
+#include "utils/BufferObjectFactory.h"
+#include "utils/DMAHeapBufferObject.h"
+#include "utils/UDMABufferObject.h"
+#include "utils/log.h"
+#include "windowing/WindowSystemFactory.h"
+
+#include <EGL/eglext.h>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+void CWinSystemWaylandEGLContextGL::Register()
+{
+ CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "wayland");
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemWaylandEGLContextGL::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemWaylandEGLContextGL>();
+}
+
+bool CWinSystemWaylandEGLContextGL::InitWindowSystem()
+{
+ if (!CWinSystemWaylandEGLContext::InitWindowSystemEGL(EGL_OPENGL_BIT, EGL_OPENGL_API))
+ {
+ return false;
+ }
+
+ CLinuxRendererGL::Register();
+ RETRO::CRPProcessInfo::RegisterRendererFactory(new RETRO::CRendererFactoryDMA);
+ RETRO::CRPProcessInfo::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGL);
+
+ bool general, deepColor;
+ m_vaapiProxy.reset(WAYLAND::VaapiProxyCreate());
+ WAYLAND::VaapiProxyConfig(m_vaapiProxy.get(), GetConnection()->GetDisplay(),
+ m_eglContext.GetEGLDisplay());
+ WAYLAND::VAAPIRegisterRenderGL(m_vaapiProxy.get(), general, deepColor);
+ if (general)
+ {
+ WAYLAND::VAAPIRegister(m_vaapiProxy.get(), deepColor);
+ }
+
+ CBufferObjectFactory::ClearBufferObjects();
+#if defined(HAVE_LINUX_MEMFD) && defined(HAVE_LINUX_UDMABUF)
+ CUDMABufferObject::Register();
+#endif
+#if defined(HAVE_LINUX_DMA_HEAP)
+ CDMAHeapBufferObject::Register();
+#endif
+
+ CScreenshotSurfaceGL::Register();
+
+ return true;
+}
+
+bool CWinSystemWaylandEGLContextGL::CreateContext()
+{
+ const EGLint glMajor = 3;
+ const EGLint glMinor = 2;
+
+ CEGLAttributesVec contextAttribs;
+ contextAttribs.Add({{EGL_CONTEXT_MAJOR_VERSION_KHR, glMajor},
+ {EGL_CONTEXT_MINOR_VERSION_KHR, glMinor},
+ {EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR}});
+
+ if (!m_eglContext.CreateContext(contextAttribs))
+ {
+ CEGLAttributesVec fallbackContextAttribs;
+ fallbackContextAttribs.Add({{EGL_CONTEXT_CLIENT_VERSION, 2}});
+
+ if (!m_eglContext.CreateContext(fallbackContextAttribs))
+ {
+ CLog::Log(LOGERROR, "EGL context creation failed");
+ return false;
+ }
+ else
+ {
+ CLog::Log(LOGWARNING, "Your OpenGL drivers do not support OpenGL {}.{} core profile. Kodi will run in compatibility mode, but performance may suffer.", glMajor, glMinor);
+ }
+ }
+
+ return true;
+}
+
+void CWinSystemWaylandEGLContextGL::SetContextSize(CSizeInt size)
+{
+ CWinSystemWaylandEGLContext::SetContextSize(size);
+
+ // Propagate changed dimensions to render system if necessary
+ if (CRenderSystemGL::m_width != size.Width() || CRenderSystemGL::m_height != size.Height())
+ {
+ CLog::LogF(LOGDEBUG, "Resetting render system to {}x{}", size.Width(), size.Height());
+ CRenderSystemGL::ResetRenderSystem(size.Width(), size.Height());
+ }
+}
+
+void CWinSystemWaylandEGLContextGL::SetVSyncImpl(bool enable)
+{
+ // Unsupported
+}
+
+void CWinSystemWaylandEGLContextGL::PresentRenderImpl(bool rendered)
+{
+ PresentFrame(rendered);
+}
+
+void CWinSystemWaylandEGLContextGL::delete_CVaapiProxy::operator()(CVaapiProxy *p) const
+{
+ WAYLAND::VaapiProxyDelete(p);
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.h b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.h
new file mode 100644
index 0000000..540a7b8
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGL.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017-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 "WinSystemWaylandEGLContext.h"
+#include "rendering/gl/RenderSystemGL.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CVaapiProxy;
+
+class CWinSystemWaylandEGLContextGL : public CWinSystemWaylandEGLContext, public CRenderSystemGL
+{
+public:
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystemBase via CWinSystemWaylandEGLContext
+ CRenderSystemBase *GetRenderSystem() override { return this; }
+ bool InitWindowSystem() override;
+
+protected:
+ bool CreateContext() override;
+ void SetContextSize(CSizeInt size) override;
+ void SetVSyncImpl(bool enable) override;
+ void PresentRenderImpl(bool rendered) override;
+ struct delete_CVaapiProxy
+ {
+ void operator()(CVaapiProxy *p) const;
+ };
+ std::unique_ptr<CVaapiProxy, delete_CVaapiProxy> m_vaapiProxy;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.cpp b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.cpp
new file mode 100644
index 0000000..d7283a7
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2017-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 "WinSystemWaylandEGLContextGLES.h"
+
+#include "OptionalsReg.h"
+#include "cores/RetroPlayer/process/RPProcessInfo.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererDMA.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/DVDVideoCodecDRMPRIME.h"
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererDRMPRIMEGLES.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "rendering/gles/ScreenshotSurfaceGLES.h"
+#include "utils/BufferObjectFactory.h"
+#include "utils/DMAHeapBufferObject.h"
+#include "utils/UDMABufferObject.h"
+#include "utils/log.h"
+#include "windowing/WindowSystemFactory.h"
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+void CWinSystemWaylandEGLContextGLES::Register()
+{
+ CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem, "wayland");
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemWaylandEGLContextGLES::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemWaylandEGLContextGLES>();
+}
+
+bool CWinSystemWaylandEGLContextGLES::InitWindowSystem()
+{
+ if (!CWinSystemWaylandEGLContext::InitWindowSystemEGL(EGL_OPENGL_ES2_BIT, EGL_OPENGL_ES_API))
+ {
+ return false;
+ }
+
+ CLinuxRendererGLES::Register();
+
+ CDVDVideoCodecDRMPRIME::Register();
+ CRendererDRMPRIMEGLES::Register();
+
+ RETRO::CRPProcessInfo::RegisterRendererFactory(new RETRO::CRendererFactoryDMA);
+ RETRO::CRPProcessInfo::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGLES);
+
+ bool general, deepColor;
+ m_vaapiProxy.reset(WAYLAND::VaapiProxyCreate());
+ WAYLAND::VaapiProxyConfig(m_vaapiProxy.get(), GetConnection()->GetDisplay(),
+ m_eglContext.GetEGLDisplay());
+ WAYLAND::VAAPIRegisterRenderGLES(m_vaapiProxy.get(), general, deepColor);
+ if (general)
+ {
+ WAYLAND::VAAPIRegister(m_vaapiProxy.get(), deepColor);
+ }
+
+ CBufferObjectFactory::ClearBufferObjects();
+#if defined(HAVE_LINUX_MEMFD) && defined(HAVE_LINUX_UDMABUF)
+ CUDMABufferObject::Register();
+#endif
+#if defined(HAVE_LINUX_DMA_HEAP)
+ CDMAHeapBufferObject::Register();
+#endif
+
+ CScreenshotSurfaceGLES::Register();
+
+ return true;
+}
+
+bool CWinSystemWaylandEGLContextGLES::CreateContext()
+{
+ CEGLAttributesVec contextAttribs;
+ contextAttribs.Add({{EGL_CONTEXT_CLIENT_VERSION, 2}});
+
+ if (!m_eglContext.CreateContext(contextAttribs))
+ {
+ CLog::Log(LOGERROR, "EGL context creation failed");
+ return false;
+ }
+ return true;
+}
+
+void CWinSystemWaylandEGLContextGLES::SetContextSize(CSizeInt size)
+{
+ CWinSystemWaylandEGLContext::SetContextSize(size);
+
+ // Propagate changed dimensions to render system if necessary
+ if (CRenderSystemGLES::m_width != size.Width() || CRenderSystemGLES::m_height != size.Height())
+ {
+ CLog::LogF(LOGDEBUG, "Resetting render system to {}x{}", size.Width(), size.Height());
+ CRenderSystemGLES::ResetRenderSystem(size.Width(), size.Height());
+ }
+}
+
+void CWinSystemWaylandEGLContextGLES::SetVSyncImpl(bool enable)
+{
+ // Unsupported
+}
+
+void CWinSystemWaylandEGLContextGLES::PresentRenderImpl(bool rendered)
+{
+ PresentFrame(rendered);
+}
+
+void CWinSystemWaylandEGLContextGLES::delete_CVaapiProxy::operator()(CVaapiProxy *p) const
+{
+ WAYLAND::VaapiProxyDelete(p);
+}
diff --git a/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.h b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.h
new file mode 100644
index 0000000..0a3c71c
--- /dev/null
+++ b/xbmc/windowing/wayland/WinSystemWaylandEGLContextGLES.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017-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 "WinSystemWaylandEGLContext.h"
+#include "rendering/gles/RenderSystemGLES.h"
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+class CVaapiProxy;
+
+class CWinSystemWaylandEGLContextGLES : public CWinSystemWaylandEGLContext, public CRenderSystemGLES
+{
+public:
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystemBase via CWinSystemWaylandEGLContext
+ CRenderSystemBase *GetRenderSystem() override { return this; }
+ bool InitWindowSystem() override;
+
+protected:
+ bool CreateContext() override;
+ void SetContextSize(CSizeInt size) override;
+ void SetVSyncImpl(bool enable) override;
+ void PresentRenderImpl(bool rendered) override;
+ struct delete_CVaapiProxy
+ {
+ void operator()(CVaapiProxy *p) const;
+ };
+ std::unique_ptr<CVaapiProxy, delete_CVaapiProxy> m_vaapiProxy;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WindowDecorationHandler.h b/xbmc/windowing/wayland/WindowDecorationHandler.h
new file mode 100644
index 0000000..b02f39b
--- /dev/null
+++ b/xbmc/windowing/wayland/WindowDecorationHandler.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017-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 <cstdint>
+
+#include <wayland-client-protocol.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * Handler for reacting to events originating in window decorations, such as
+ * moving the window by clicking and dragging
+ */
+class IWindowDecorationHandler
+{
+public:
+ virtual void OnWindowMove(wayland::seat_t const& seat, std::uint32_t serial) = 0;
+ virtual void OnWindowResize(wayland::seat_t const& seat, std::uint32_t serial, wayland::shell_surface_resize edge) = 0;
+ virtual void OnWindowShowContextMenu(wayland::seat_t const& seat, std::uint32_t serial, CPointInt position) = 0;
+ virtual void OnWindowMinimize() = 0;
+ virtual void OnWindowMaximize() = 0;
+ virtual void OnWindowClose() = 0;
+
+ virtual ~IWindowDecorationHandler() = default;
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/WindowDecorator.cpp b/xbmc/windowing/wayland/WindowDecorator.cpp
new file mode 100644
index 0000000..9f61481
--- /dev/null
+++ b/xbmc/windowing/wayland/WindowDecorator.cpp
@@ -0,0 +1,1043 @@
+/*
+ * Copyright (C) 2017-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 "WindowDecorator.h"
+
+#include "Util.h"
+#include "utils/EndianSwap.h"
+#include "utils/log.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cmath>
+#include <mutex>
+#include <vector>
+
+#include <linux/input-event-codes.h>
+
+using namespace KODI::UTILS::POSIX;
+using namespace KODI::WINDOWING::WAYLAND;
+using namespace std::placeholders;
+
+namespace
+{
+
+/// Bytes per pixel in shm storage
+constexpr int BYTES_PER_PIXEL{4};
+/// Width of the visible border around the whole window
+constexpr int VISIBLE_BORDER_WIDTH{5};
+/// Width of the invisible border around the whole window for easier resizing
+constexpr int RESIZE_BORDER_WIDTH{10};
+/// Total width of the border around the window
+constexpr int BORDER_WIDTH{VISIBLE_BORDER_WIDTH + RESIZE_BORDER_WIDTH};
+/// Height of the top bar
+constexpr int TOP_BAR_HEIGHT{33};
+/// Maximum distance from the window corner to consider position valid for resize
+constexpr int RESIZE_MAX_CORNER_DISTANCE{BORDER_WIDTH};
+/// Distance of buttons from edges of the top bar
+constexpr int BUTTONS_EDGE_DISTANCE{6};
+/// Distance from button inner edge to button content
+constexpr int BUTTON_INNER_SEPARATION{4};
+/// Button size
+constexpr int BUTTON_SIZE{21};
+
+constexpr std::uint32_t TRANSPARENT{0x00000000u};
+constexpr std::uint32_t BORDER_COLOR{0xFF000000u};
+constexpr std::uint32_t BUTTON_COLOR_ACTIVE{0xFFFFFFFFu};
+constexpr std::uint32_t BUTTON_COLOR_INACTIVE{0xFF777777u};
+constexpr std::uint32_t BUTTON_HOVER_COLOR{0xFF555555u};
+
+static_assert(BUTTON_SIZE <= TOP_BAR_HEIGHT - BUTTONS_EDGE_DISTANCE * 2, "Buttons must fit in top bar");
+
+/*
+ * Decorations consist of four surfaces, one for each edge of the window. It would
+ * also be possible to position one single large surface behind the main surface
+ * instead, but that would waste a lot of memory on big/high-density screens.
+ *
+ * The four surfaces are laid out as follows: Top and bottom surfaces go over the
+ * whole width of the main surface plus the left and right borders.
+ * Left and right surfaces only go over the height of the main surface without
+ * the top and bottom borders.
+ *
+ * ---------------------------------------------
+ * | TOP |
+ * ---------------------------------------------
+ * | | | |
+ * | L | | R |
+ * | E | | I |
+ * | F | Main surface | G |
+ * | T | | H |
+ * | | | T |
+ * | | | |
+ * ---------------------------------------------
+ * | BOTTOM |
+ * ---------------------------------------------
+ */
+
+
+CRectInt SurfaceGeometry(SurfaceIndex type, CSizeInt mainSurfaceSize)
+{
+ // Coordinates are relative to main surface
+ switch (type)
+ {
+ case SURFACE_TOP:
+ return {
+ CPointInt{-BORDER_WIDTH, -(BORDER_WIDTH + TOP_BAR_HEIGHT)},
+ CSizeInt{mainSurfaceSize.Width() + 2 * BORDER_WIDTH, TOP_BAR_HEIGHT + BORDER_WIDTH}
+ };
+ case SURFACE_RIGHT:
+ return {
+ CPointInt{mainSurfaceSize.Width(), 0},
+ CSizeInt{BORDER_WIDTH, mainSurfaceSize.Height()}
+ };
+ case SURFACE_BOTTOM:
+ return {
+ CPointInt{-BORDER_WIDTH, mainSurfaceSize.Height()},
+ CSizeInt{mainSurfaceSize.Width() + 2 * BORDER_WIDTH, BORDER_WIDTH}
+ };
+ case SURFACE_LEFT:
+ return {
+ CPointInt{-BORDER_WIDTH, 0},
+ CSizeInt{BORDER_WIDTH, mainSurfaceSize.Height()}
+ };
+ default:
+ throw std::logic_error("Invalid surface type");
+ }
+}
+
+CRectInt SurfaceOpaqueRegion(SurfaceIndex type, CSizeInt mainSurfaceSize)
+{
+ // Coordinates are relative to main surface
+ auto size = SurfaceGeometry(type, mainSurfaceSize).ToSize();
+ switch (type)
+ {
+ case SURFACE_TOP:
+ return {
+ CPointInt{RESIZE_BORDER_WIDTH, RESIZE_BORDER_WIDTH},
+ size - CSizeInt{RESIZE_BORDER_WIDTH * 2, RESIZE_BORDER_WIDTH}
+ };
+ case SURFACE_RIGHT:
+ return {
+ CPointInt{},
+ size - CSizeInt{RESIZE_BORDER_WIDTH, 0}
+ };
+ case SURFACE_BOTTOM:
+ return {
+ CPointInt{RESIZE_BORDER_WIDTH, 0},
+ size - CSizeInt{RESIZE_BORDER_WIDTH * 2, RESIZE_BORDER_WIDTH}
+ };
+ case SURFACE_LEFT:
+ return {
+ CPointInt{RESIZE_BORDER_WIDTH, 0},
+ size - CSizeInt{RESIZE_BORDER_WIDTH, 0}
+ };
+ default:
+ throw std::logic_error("Invalid surface type");
+ }
+}
+
+CRectInt SurfaceWindowRegion(SurfaceIndex type, CSizeInt mainSurfaceSize)
+{
+ return SurfaceOpaqueRegion(type, mainSurfaceSize);
+}
+
+/**
+ * Full size of decorations to be added to the main surface size
+ */
+CSizeInt DecorationSize()
+{
+ return {2 * VISIBLE_BORDER_WIDTH, 2 * VISIBLE_BORDER_WIDTH + TOP_BAR_HEIGHT};
+}
+
+std::size_t MemoryBytesForSize(CSizeInt windowSurfaceSize, int scale)
+{
+ std::size_t size{};
+
+ for (auto surface : { SURFACE_TOP, SURFACE_RIGHT, SURFACE_BOTTOM, SURFACE_LEFT })
+ {
+ size += SurfaceGeometry(surface, windowSurfaceSize).Area();
+ }
+
+ size *= scale * scale;
+
+ size *= BYTES_PER_PIXEL;
+
+ return size;
+}
+
+std::size_t PositionInBuffer(CWindowDecorator::Buffer& buffer, CPointInt position)
+{
+ if (position.x < 0 || position.y < 0)
+ {
+ throw std::invalid_argument("Position out of bounds");
+ }
+ std::size_t offset{static_cast<std::size_t> (buffer.size.Width() * position.y + position.x)};
+ if (offset * BYTES_PER_PIXEL >= buffer.dataSize)
+ {
+ throw std::invalid_argument("Position out of bounds");
+ }
+ return offset;
+}
+
+void DrawHorizontalLine(CWindowDecorator::Buffer& buffer, std::uint32_t color, CPointInt position, int length)
+{
+ auto offsetStart = PositionInBuffer(buffer, position);
+ auto offsetEnd = PositionInBuffer(buffer, position + CPointInt{length - 1, 0});
+ if (offsetEnd < offsetStart)
+ {
+ throw std::invalid_argument("Invalid drawing coordinates");
+ }
+ std::fill(buffer.RgbaBuffer() + offsetStart, buffer.RgbaBuffer() + offsetEnd + 1, Endian_SwapLE32(color));
+}
+
+void DrawLineWithStride(CWindowDecorator::Buffer& buffer, std::uint32_t color, CPointInt position, int length, int stride)
+{
+ auto offsetStart = PositionInBuffer(buffer, position);
+ auto offsetEnd = offsetStart + stride * (length - 1);
+ if (offsetEnd * BYTES_PER_PIXEL >= buffer.dataSize)
+ {
+ throw std::invalid_argument("Position out of bounds");
+ }
+ if (offsetEnd < offsetStart)
+ {
+ throw std::invalid_argument("Invalid drawing coordinates");
+ }
+ auto* memory = buffer.RgbaBuffer();
+ for (std::size_t offset = offsetStart; offset <= offsetEnd; offset += stride)
+ {
+ *(memory + offset) = Endian_SwapLE32(color);
+ }
+}
+
+void DrawVerticalLine(CWindowDecorator::Buffer& buffer, std::uint32_t color, CPointInt position, int length)
+{
+ DrawLineWithStride(buffer, color, position, length, buffer.size.Width());
+}
+
+/**
+ * Draw rectangle inside the specified buffer coordinates
+ */
+void DrawRectangle(CWindowDecorator::Buffer& buffer, std::uint32_t color, CRectInt rect)
+{
+ DrawHorizontalLine(buffer, color, rect.P1(), rect.Width());
+ DrawVerticalLine(buffer, color, rect.P1(), rect.Height());
+ DrawHorizontalLine(buffer, color, rect.P1() + CPointInt{1, rect.Height() - 1}, rect.Width() - 1);
+ DrawVerticalLine(buffer, color, rect.P1() + CPointInt{rect.Width() - 1, 1}, rect.Height() - 1);
+}
+
+void FillRectangle(CWindowDecorator::Buffer& buffer, std::uint32_t color, CRectInt rect)
+{
+ for (int y{rect.y1}; y <= rect.y2; y++)
+ {
+ DrawHorizontalLine(buffer, color, {rect.x1, y}, rect.Width() + 1);
+ }
+}
+
+void DrawHorizontalLine(CWindowDecorator::Surface& surface, std::uint32_t color, CPointInt position, int length)
+{
+ for (int i{0}; i < surface.scale; i++)
+ {
+ DrawHorizontalLine(surface.buffer, color, position * surface.scale + CPointInt{0, i}, length * surface.scale);
+ }
+}
+
+void DrawAngledLine(CWindowDecorator::Surface& surface, std::uint32_t color, CPointInt position, int length, int stride)
+{
+ for (int i{0}; i < surface.scale; i++)
+ {
+ DrawLineWithStride(surface.buffer, color, position * surface.scale + CPointInt{i, 0}, length * surface.scale, surface.buffer.size.Width() + stride);
+ }
+}
+
+void DrawVerticalLine(CWindowDecorator::Surface& surface, std::uint32_t color, CPointInt position, int length)
+{
+ DrawAngledLine(surface, color, position, length, 0);
+}
+
+/**
+ * Draw rectangle inside the specified surface coordinates
+ */
+void DrawRectangle(CWindowDecorator::Surface& surface, std::uint32_t color, CRectInt rect)
+{
+ for (int i{0}; i < surface.scale; i++)
+ {
+ DrawRectangle(surface.buffer, color, {rect.P1() * surface.scale + CPointInt{i, i}, (rect.P2() + CPointInt{1, 1}) * surface.scale - CPointInt{i, i} - CPointInt{1, 1}});
+ }
+}
+
+void FillRectangle(CWindowDecorator::Surface& surface, std::uint32_t color, CRectInt rect)
+{
+ FillRectangle(surface.buffer, color, {rect.P1() * surface.scale, (rect.P2() + CPointInt{1, 1}) * surface.scale - CPointInt{1, 1}});
+}
+
+void DrawButton(CWindowDecorator::Surface& surface, std::uint32_t lineColor, CRectInt rect, bool hover)
+{
+ if (hover)
+ {
+ FillRectangle(surface, BUTTON_HOVER_COLOR, rect);
+ }
+ DrawRectangle(surface, lineColor, rect);
+}
+
+wayland::shell_surface_resize ResizeEdgeForPosition(SurfaceIndex surface, CSizeInt surfaceSize, CPointInt position)
+{
+ switch (surface)
+ {
+ case SURFACE_TOP:
+ if (position.y <= RESIZE_MAX_CORNER_DISTANCE)
+ {
+ if (position.x <= RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::top_left;
+ }
+ else if (position.x >= surfaceSize.Width() - RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::top_right;
+ }
+ else
+ {
+ return wayland::shell_surface_resize::top;
+ }
+ }
+ else
+ {
+ if (position.x <= RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::left;
+ }
+ else if (position.x >= surfaceSize.Width() - RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::right;
+ }
+ else
+ {
+ // Inside title bar, not resizing
+ return wayland::shell_surface_resize::none;
+ }
+ }
+ case SURFACE_RIGHT:
+ if (position.y >= surfaceSize.Height() - RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::bottom_right;
+ }
+ else
+ {
+ return wayland::shell_surface_resize::right;
+ }
+ case SURFACE_BOTTOM:
+ if (position.x <= RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::bottom_left;
+ }
+ else if (position.x >= surfaceSize.Width() - RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::bottom_right;
+ }
+ else
+ {
+ return wayland::shell_surface_resize::bottom;
+ }
+ case SURFACE_LEFT:
+ if (position.y >= surfaceSize.Height() - RESIZE_MAX_CORNER_DISTANCE)
+ {
+ return wayland::shell_surface_resize::bottom_left;
+ }
+ else
+ {
+ return wayland::shell_surface_resize::left;
+ }
+
+ default:
+ return wayland::shell_surface_resize::none;
+ }
+}
+
+/**
+ * Get name for resize cursor according to xdg cursor-spec
+ */
+std::string CursorForResizeEdge(wayland::shell_surface_resize edge)
+{
+ if (edge == wayland::shell_surface_resize::top)
+ return "n-resize";
+ else if (edge == wayland::shell_surface_resize::bottom)
+ return "s-resize";
+ else if (edge == wayland::shell_surface_resize::left)
+ return "w-resize";
+ else if (edge == wayland::shell_surface_resize::top_left)
+ return "nw-resize";
+ else if (edge == wayland::shell_surface_resize::bottom_left)
+ return "sw-resize";
+ else if (edge == wayland::shell_surface_resize::right)
+ return "e-resize";
+ else if (edge == wayland::shell_surface_resize::top_right)
+ return "ne-resize";
+ else if (edge == wayland::shell_surface_resize::bottom_right)
+ return "se-resize";
+ else
+ return "";
+}
+
+template<typename T, typename InstanceProviderT>
+bool HandleCapabilityChange(const wayland::seat_capability& caps,
+ const wayland::seat_capability& cap,
+ T& proxy,
+ InstanceProviderT instanceProvider)
+{
+ bool hasCapability = caps & cap;
+
+ if ((!!proxy) != hasCapability)
+ {
+ // Capability changed
+
+ if (hasCapability)
+ {
+ // The capability was added
+ proxy = instanceProvider();
+ return true;
+ }
+ else
+ {
+ // The capability was removed
+ proxy.proxy_release();
+ }
+ }
+
+ return false;
+}
+
+}
+
+CWindowDecorator::CWindowDecorator(IWindowDecorationHandler& handler, CConnection& connection, wayland::surface_t const& mainSurface)
+: m_handler{handler}, m_registry{connection}, m_mainSurface{mainSurface}, m_buttonColor{BUTTON_COLOR_ACTIVE}
+{
+ static_assert(std::tuple_size<decltype(m_borderSurfaces)>::value == SURFACE_COUNT, "SURFACE_COUNT must match surfaces array size");
+
+ m_registry.RequestSingleton(m_compositor, 1, 4);
+ m_registry.RequestSingleton(m_subcompositor, 1, 1, false);
+ m_registry.RequestSingleton(m_shm, 1, 1);
+
+ m_registry.Bind();
+}
+
+void CWindowDecorator::AddSeat(CSeat* seat)
+{
+ m_seats.emplace(std::piecewise_construct, std::forward_as_tuple(seat->GetGlobalName()), std::forward_as_tuple(seat));
+ seat->AddRawInputHandlerTouch(this);
+ seat->AddRawInputHandlerPointer(this);
+}
+
+void CWindowDecorator::RemoveSeat(CSeat* seat)
+{
+ seat->RemoveRawInputHandlerTouch(this);
+ seat->RemoveRawInputHandlerPointer(this);
+ m_seats.erase(seat->GetGlobalName());
+ UpdateButtonHoverState();
+}
+
+void CWindowDecorator::OnPointerEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ double surfaceX,
+ double surfaceY)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI == m_seats.end())
+ {
+ return;
+ }
+ auto& seatState = seatStateI->second;
+ // Reset first so we ignore events for surfaces we don't handle
+ seatState.currentSurface = SURFACE_COUNT;
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ for (std::size_t i{0}; i < m_borderSurfaces.size(); i++)
+ {
+ if (m_borderSurfaces[i].surface.wlSurface == surface)
+ {
+ seatState.pointerEnterSerial = serial;
+ seatState.currentSurface = static_cast<SurfaceIndex> (i);
+ seatState.pointerPosition = {static_cast<float> (surfaceX), static_cast<float> (surfaceY)};
+ UpdateSeatCursor(seatState);
+ UpdateButtonHoverState();
+ break;
+ }
+ }
+}
+
+void CWindowDecorator::OnPointerLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI == m_seats.end())
+ {
+ return;
+ }
+ auto& seatState = seatStateI->second;
+ seatState.currentSurface = SURFACE_COUNT;
+ // Recreate cursor surface on reenter
+ seatState.cursorName.clear();
+ seatState.cursor.proxy_release();
+ UpdateButtonHoverState();
+}
+
+void CWindowDecorator::OnPointerMotion(CSeat* seat, std::uint32_t time, double surfaceX, double surfaceY)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI == m_seats.end())
+ {
+ return;
+ }
+ auto& seatState = seatStateI->second;
+ if (seatState.currentSurface != SURFACE_COUNT)
+ {
+ seatState.pointerPosition = {static_cast<float> (surfaceX), static_cast<float> (surfaceY)};
+ UpdateSeatCursor(seatState);
+ UpdateButtonHoverState();
+ }
+}
+
+void CWindowDecorator::OnPointerButton(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI == m_seats.end())
+ {
+ return;
+ }
+ const auto& seatState = seatStateI->second;
+ if (seatState.currentSurface != SURFACE_COUNT && state == wayland::pointer_button_state::pressed)
+ {
+ HandleSeatClick(seatState, seatState.currentSurface, serial, button, seatState.pointerPosition);
+ }
+}
+
+void CWindowDecorator::OnTouchDown(CSeat* seat,
+ std::uint32_t serial,
+ std::uint32_t time,
+ const wayland::surface_t& surface,
+ std::int32_t id,
+ double x,
+ double y)
+{
+ auto seatStateI = m_seats.find(seat->GetGlobalName());
+ if (seatStateI == m_seats.end())
+ {
+ return;
+ }
+ auto& seatState = seatStateI->second;
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ for (std::size_t i{0}; i < m_borderSurfaces.size(); i++)
+ {
+ if (m_borderSurfaces[i].surface.wlSurface == surface)
+ {
+ HandleSeatClick(seatState, static_cast<SurfaceIndex> (i), serial, BTN_LEFT, {static_cast<float> (x), static_cast<float> (y)});
+ }
+ }
+}
+
+void CWindowDecorator::UpdateSeatCursor(SeatState& seatState)
+{
+ if (seatState.currentSurface == SURFACE_COUNT)
+ {
+ // Don't set anything if not on any surface
+ return;
+ }
+
+ LoadCursorTheme();
+
+ std::string cursorName{"default"};
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ auto resizeEdge = ResizeEdgeForPosition(seatState.currentSurface, SurfaceGeometry(seatState.currentSurface, m_mainSurfaceSize).ToSize(), CPointInt{seatState.pointerPosition});
+ if (resizeEdge != wayland::shell_surface_resize::none)
+ {
+ cursorName = CursorForResizeEdge(resizeEdge);
+ }
+ }
+
+ if (cursorName == seatState.cursorName)
+ {
+ // Don't reload cursor all the time when nothing is changing
+ return;
+ }
+ seatState.cursorName = cursorName;
+
+ wayland::cursor_t cursor;
+ try
+ {
+ cursor = CCursorUtil::LoadFromTheme(m_cursorTheme, cursorName);
+ }
+ catch (std::exception const& e)
+ {
+ CLog::LogF(LOGERROR, "Could not get required cursor {} from cursor theme: {}", cursorName,
+ e.what());
+ return;
+ }
+ auto cursorImage = cursor.image(0);
+
+ if (!seatState.cursor)
+ {
+ seatState.cursor = m_compositor.create_surface();
+ }
+ int calcScale{seatState.cursor.can_set_buffer_scale() ? m_scale : 1};
+
+ seatState.seat->SetCursor(seatState.pointerEnterSerial, seatState.cursor, cursorImage.hotspot_x() / calcScale, cursorImage.hotspot_y() / calcScale);
+ seatState.cursor.attach(cursorImage.get_buffer(), 0, 0);
+ seatState.cursor.damage(0, 0, cursorImage.width() / calcScale, cursorImage.height() / calcScale);
+ if (seatState.cursor.can_set_buffer_scale())
+ {
+ seatState.cursor.set_buffer_scale(m_scale);
+ }
+ seatState.cursor.commit();
+}
+
+void CWindowDecorator::UpdateButtonHoverState()
+{
+ std::vector<CPoint> pointerPositions;
+
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ for (auto const& seatPair : m_seats)
+ {
+ auto const& seat = seatPair.second;
+ if (seat.currentSurface == SURFACE_TOP)
+ {
+ pointerPositions.emplace_back(seat.pointerPosition);
+ }
+ }
+
+ bool changed{false};
+ for (auto& button : m_buttons)
+ {
+ bool wasHovered{button.hovered};
+ button.hovered = std::any_of(pointerPositions.cbegin(), pointerPositions.cend(), [&](CPoint point) { return button.position.PtInRect(CPointInt{point}); });
+ changed = changed || (button.hovered != wasHovered);
+ }
+
+ if (changed)
+ {
+ // Repaint!
+ Reset(false);
+ }
+}
+
+void CWindowDecorator::HandleSeatClick(SeatState const& seatState, SurfaceIndex surface, std::uint32_t serial, std::uint32_t button, CPoint position)
+{
+ switch (button)
+ {
+ case BTN_LEFT:
+ {
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ auto resizeEdge = ResizeEdgeForPosition(surface, SurfaceGeometry(surface, m_mainSurfaceSize).ToSize(), CPointInt{position});
+ if (resizeEdge == wayland::shell_surface_resize::none)
+ {
+ for (auto const& button : m_buttons)
+ {
+ if (button.position.PtInRect(CPointInt{position}))
+ {
+ button.onClick();
+ return;
+ }
+ }
+
+ m_handler.OnWindowMove(seatState.seat->GetWlSeat(), serial);
+ }
+ else
+ {
+ m_handler.OnWindowResize(seatState.seat->GetWlSeat(), serial, resizeEdge);
+ }
+ }
+ break;
+ case BTN_RIGHT:
+ if (surface == SURFACE_TOP)
+ {
+ m_handler.OnWindowShowContextMenu(seatState.seat->GetWlSeat(), serial, CPointInt{position} - CPointInt{BORDER_WIDTH, BORDER_WIDTH + TOP_BAR_HEIGHT});
+ }
+ break;
+ }
+}
+
+CWindowDecorator::BorderSurface CWindowDecorator::MakeBorderSurface()
+{
+ Surface surface;
+ surface.wlSurface = m_compositor.create_surface();
+ auto subsurface = m_subcompositor.get_subsurface(surface.wlSurface, m_mainSurface);
+
+ CWindowDecorator::BorderSurface boarderSurface;
+ boarderSurface.surface = surface;
+ boarderSurface.subsurface = subsurface;
+
+ return boarderSurface;
+}
+
+bool CWindowDecorator::IsDecorationActive() const
+{
+ return StateHasWindowDecorations(m_windowState);
+}
+
+bool CWindowDecorator::StateHasWindowDecorations(IShellSurface::StateBitset state) const
+{
+ // No decorations possible if subcompositor not available
+ return m_subcompositor && !state.test(IShellSurface::STATE_FULLSCREEN);
+}
+
+CSizeInt CWindowDecorator::CalculateMainSurfaceSize(CSizeInt size, IShellSurface::StateBitset state) const
+{
+ if (StateHasWindowDecorations(state))
+ {
+ // Subtract decorations
+ return size - DecorationSize();
+ }
+ else
+ {
+ // Fullscreen -> no decorations
+ return size;
+ }
+}
+
+CSizeInt CWindowDecorator::CalculateFullSurfaceSize(CSizeInt size, IShellSurface::StateBitset state) const
+{
+ if (StateHasWindowDecorations(state))
+ {
+ // Add decorations
+ return size + DecorationSize();
+ }
+ else
+ {
+ // Fullscreen -> no decorations
+ return size;
+ }
+}
+
+void CWindowDecorator::SetState(CSizeInt size, int scale, IShellSurface::StateBitset state)
+{
+ CSizeInt mainSurfaceSize{CalculateMainSurfaceSize(size, state)};
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ if (mainSurfaceSize == m_mainSurfaceSize && scale == m_scale && state == m_windowState)
+ {
+ return;
+ }
+
+ bool wasDecorations{IsDecorationActive()};
+ m_windowState = state;
+
+ m_buttonColor = m_windowState.test(IShellSurface::STATE_ACTIVATED) ? BUTTON_COLOR_ACTIVE : BUTTON_COLOR_INACTIVE;
+
+ CLog::Log(LOGDEBUG,
+ "CWindowDecorator::SetState: Setting full surface size {}x{} scale {} (main surface "
+ "size {}x{}), decorations active: {}",
+ size.Width(), size.Height(), scale, mainSurfaceSize.Width(), mainSurfaceSize.Height(),
+ IsDecorationActive());
+
+ if (mainSurfaceSize != m_mainSurfaceSize || scale != m_scale || wasDecorations != IsDecorationActive())
+ {
+ if (scale != m_scale)
+ {
+ // Reload cursor theme
+ CLog::Log(LOGDEBUG, "CWindowDecorator::SetState: Buffer scale changed, reloading cursor theme");
+ m_cursorTheme = wayland::cursor_theme_t{};
+ for (auto& seat : m_seats)
+ {
+ UpdateSeatCursor(seat.second);
+ }
+ }
+
+ m_mainSurfaceSize = mainSurfaceSize;
+ m_scale = scale;
+ CLog::Log(LOGDEBUG, "CWindowDecorator::SetState: Resetting decorations");
+ Reset(true);
+ }
+ else if (IsDecorationActive())
+ {
+ CLog::Log(LOGDEBUG, "CWindowDecorator::SetState: Repainting decorations");
+ // Only state differs, no reallocation needed
+ Reset(false);
+ }
+}
+
+void CWindowDecorator::Reset(bool reallocate)
+{
+ // The complete reset operation should be seen as one atomic update to the
+ // internal state, otherwise buffer/surface state might be mismatched
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+
+ if (reallocate)
+ {
+ ResetButtons();
+ ResetSurfaces();
+ ResetShm();
+ if (IsDecorationActive())
+ {
+ AllocateBuffers();
+ ReattachSubsurfaces();
+ PositionButtons();
+ }
+ }
+
+ if (IsDecorationActive())
+ {
+ Repaint();
+ }
+}
+
+void CWindowDecorator::ResetButtons()
+{
+ if (IsDecorationActive())
+ {
+ if (m_buttons.empty())
+ {
+ // Minimize
+ m_buttons.emplace_back();
+ Button& minimize = m_buttons.back();
+ minimize.draw = [this](Surface& surface, CRectInt position, bool hover)
+ {
+ DrawButton(surface, m_buttonColor, position, hover);
+ DrawHorizontalLine(surface, m_buttonColor, position.P1() + CPointInt{BUTTON_INNER_SEPARATION, position.Height() - BUTTON_INNER_SEPARATION - 1}, position.Width() - 2 * BUTTON_INNER_SEPARATION);
+ };
+ minimize.onClick = [this] { m_handler.OnWindowMinimize(); };
+
+ // Maximize
+ m_buttons.emplace_back();
+ Button& maximize = m_buttons.back();
+ maximize.draw = [this](Surface& surface, CRectInt position, bool hover)
+ {
+ DrawButton(surface, m_buttonColor, position, hover);
+ DrawRectangle(surface, m_buttonColor, {position.P1() + CPointInt{BUTTON_INNER_SEPARATION, BUTTON_INNER_SEPARATION}, position.P2() - CPointInt{BUTTON_INNER_SEPARATION, BUTTON_INNER_SEPARATION}});
+ DrawHorizontalLine(surface, m_buttonColor, position.P1() + CPointInt{BUTTON_INNER_SEPARATION, BUTTON_INNER_SEPARATION + 1}, position.Width() - 2 * BUTTON_INNER_SEPARATION);
+ };
+ maximize.onClick = [this] { m_handler.OnWindowMaximize(); };
+
+ // Close
+ m_buttons.emplace_back();
+ Button& close = m_buttons.back();
+ close.draw = [this](Surface& surface, CRectInt position, bool hover)
+ {
+ DrawButton(surface, m_buttonColor, position, hover);
+ auto height = position.Height() - 2 * BUTTON_INNER_SEPARATION;
+ DrawAngledLine(surface, m_buttonColor, position.P1() + CPointInt{BUTTON_INNER_SEPARATION, BUTTON_INNER_SEPARATION}, height, 1);
+ DrawAngledLine(surface, m_buttonColor, position.P1() + CPointInt{position.Width() - BUTTON_INNER_SEPARATION - 1, BUTTON_INNER_SEPARATION}, height, -1);
+ };
+ close.onClick = [this] { m_handler.OnWindowClose(); };
+ }
+ }
+ else
+ {
+ m_buttons.clear();
+ }
+}
+
+void CWindowDecorator::ResetSurfaces()
+{
+ if (IsDecorationActive())
+ {
+ if (!m_borderSurfaces.front().surface.wlSurface)
+ {
+ std::generate(m_borderSurfaces.begin(), m_borderSurfaces.end(), std::bind(&CWindowDecorator::MakeBorderSurface, this));
+ }
+ }
+ else
+ {
+ for (auto& borderSurface : m_borderSurfaces)
+ {
+ auto& wlSurface = borderSurface.surface.wlSurface;
+ if (wlSurface)
+ {
+ // Destroying the surface would cause some flicker because it takes effect
+ // immediately, before the next commit on the main surface - just make it
+ // invisible by attaching a NULL buffer
+ wlSurface.attach(wayland::buffer_t{}, 0, 0);
+ wlSurface.set_opaque_region(wayland::region_t{});
+ wlSurface.commit();
+ }
+ }
+ }
+}
+
+void CWindowDecorator::ReattachSubsurfaces()
+{
+ for (auto& surface : m_borderSurfaces)
+ {
+ surface.subsurface.set_position(surface.geometry.x1, surface.geometry.y1);
+ }
+}
+
+CRectInt CWindowDecorator::GetWindowGeometry() const
+{
+ CRectInt geometry{{0, 0}, m_mainSurfaceSize};
+
+ if (IsDecorationActive())
+ {
+ for (auto const& surface : m_borderSurfaces)
+ {
+ geometry.Union(surface.windowRect + surface.geometry.P1());
+ }
+ }
+
+ return geometry;
+}
+
+void CWindowDecorator::ResetShm()
+{
+ if (IsDecorationActive())
+ {
+ m_memory.reset(new CSharedMemory(MemoryBytesForSize(m_mainSurfaceSize, m_scale)));
+ m_memoryAllocatedSize = 0;
+ m_shmPool = m_shm.create_pool(m_memory->Fd(), m_memory->Size());
+ }
+ else
+ {
+ m_memory.reset();
+ m_shmPool.proxy_release();
+ }
+
+ for (auto& borderSurface : m_borderSurfaces)
+ {
+ // Buffers are invalid now, reset
+ borderSurface.surface.buffer = Buffer{};
+ }
+}
+
+void CWindowDecorator::PositionButtons()
+{
+ CPointInt position{m_borderSurfaces[SURFACE_TOP].surface.size.Width() - BORDER_WIDTH, BORDER_WIDTH + BUTTONS_EDGE_DISTANCE};
+ for (auto iter = m_buttons.rbegin(); iter != m_buttons.rend(); iter++)
+ {
+ position.x -= (BUTTONS_EDGE_DISTANCE + BUTTON_SIZE);
+ // Clamp if not enough space
+ position.x = std::max(0, position.x);
+
+ iter->position = CRectInt{position, position + CPointInt{BUTTON_SIZE, BUTTON_SIZE}};
+ }
+}
+
+CWindowDecorator::Buffer CWindowDecorator::GetBuffer(CSizeInt size)
+{
+ // We ignore tearing on the decorations for now.
+ // We can always implement a clever buffer management scheme later... :-)
+
+ auto totalSize{size.Area() * BYTES_PER_PIXEL};
+ if (m_memory->Size() < m_memoryAllocatedSize + totalSize)
+ {
+ // We miscalculated something
+ throw std::logic_error("Remaining SHM pool size is too small for requested buffer");
+ }
+ // argb8888 support is mandatory
+ auto buffer = m_shmPool.create_buffer(m_memoryAllocatedSize, size.Width(), size.Height(), size.Width() * BYTES_PER_PIXEL, wayland::shm_format::argb8888);
+
+ void* data{static_cast<std::uint8_t*> (m_memory->Data()) + m_memoryAllocatedSize};
+ m_memoryAllocatedSize += totalSize;
+
+ return { data, static_cast<std::size_t> (totalSize), size, std::move(buffer) };
+}
+
+void CWindowDecorator::AllocateBuffers()
+{
+ for (std::size_t i{0}; i < m_borderSurfaces.size(); i++)
+ {
+ auto& borderSurface = m_borderSurfaces[i];
+ if (!borderSurface.surface.buffer.data)
+ {
+ borderSurface.geometry = SurfaceGeometry(static_cast<SurfaceIndex> (i), m_mainSurfaceSize);
+ borderSurface.windowRect = SurfaceWindowRegion(static_cast<SurfaceIndex> (i), m_mainSurfaceSize);
+ borderSurface.surface.buffer = GetBuffer(borderSurface.geometry.ToSize() * m_scale);
+ borderSurface.surface.scale = m_scale;
+ borderSurface.surface.size = borderSurface.geometry.ToSize();
+ auto opaqueRegionGeometry = SurfaceOpaqueRegion(static_cast<SurfaceIndex> (i), m_mainSurfaceSize);
+ auto region = m_compositor.create_region();
+ region.add(opaqueRegionGeometry.x1, opaqueRegionGeometry.y1, opaqueRegionGeometry.Width(), opaqueRegionGeometry.Height());
+ borderSurface.surface.wlSurface.set_opaque_region(region);
+ if (borderSurface.surface.wlSurface.can_set_buffer_scale())
+ {
+ borderSurface.surface.wlSurface.set_buffer_scale(m_scale);
+ }
+ }
+ }
+}
+
+void CWindowDecorator::Repaint()
+{
+ // Fill transparent (outer) and color (inner)
+ for (auto& borderSurface : m_borderSurfaces)
+ {
+ std::fill_n(static_cast<std::uint32_t*> (borderSurface.surface.buffer.data), borderSurface.surface.buffer.size.Area(), Endian_SwapLE32(TRANSPARENT));
+ FillRectangle(borderSurface.surface, BORDER_COLOR, borderSurface.windowRect - CSizeInt{1, 1});
+ }
+ auto& topSurface = m_borderSurfaces[SURFACE_TOP].surface;
+ auto innerBorderColor = m_buttonColor;
+ // Draw rectangle
+ DrawHorizontalLine(topSurface, innerBorderColor, {BORDER_WIDTH - 1, BORDER_WIDTH - 1}, topSurface.size.Width() - 2 * BORDER_WIDTH + 2);
+ DrawVerticalLine(topSurface, innerBorderColor, {BORDER_WIDTH - 1, BORDER_WIDTH - 1}, topSurface.size.Height() - BORDER_WIDTH + 1);
+ DrawVerticalLine(topSurface, innerBorderColor, {topSurface.size.Width() - BORDER_WIDTH, BORDER_WIDTH - 1}, topSurface.size.Height() - BORDER_WIDTH + 1);
+ DrawVerticalLine(m_borderSurfaces[SURFACE_LEFT].surface, innerBorderColor, {BORDER_WIDTH - 1, 0}, m_borderSurfaces[SURFACE_LEFT].surface.size.Height());
+ DrawVerticalLine(m_borderSurfaces[SURFACE_RIGHT].surface, innerBorderColor, {0, 0}, m_borderSurfaces[SURFACE_RIGHT].surface.size.Height());
+ DrawHorizontalLine(m_borderSurfaces[SURFACE_BOTTOM].surface, innerBorderColor, {BORDER_WIDTH - 1, 0}, m_borderSurfaces[SURFACE_BOTTOM].surface.size.Width() - 2 * BORDER_WIDTH + 2);
+ // Draw white line into top bar as separator
+ DrawHorizontalLine(topSurface, innerBorderColor, {BORDER_WIDTH - 1, topSurface.size.Height() - 1}, topSurface.size.Width() - 2 * BORDER_WIDTH + 2);
+ // Draw buttons
+ for (auto& button : m_buttons)
+ {
+ button.draw(topSurface, button.position, button.hovered);
+ }
+
+ // Finally make everything visible
+ CommitAllBuffers();
+}
+
+void CWindowDecorator::CommitAllBuffers()
+{
+ std::unique_lock<CCriticalSection> lock(m_pendingBuffersMutex);
+
+ for (auto& borderSurface : m_borderSurfaces)
+ {
+ auto& wlSurface = borderSurface.surface.wlSurface;
+ auto& wlBuffer = borderSurface.surface.buffer.wlBuffer;
+ // Keep buffers in list so they are kept alive even when the Buffer gets
+ // recreated on size change
+ auto emplaceResult = m_pendingBuffers.emplace(wlBuffer);
+ if (emplaceResult.second)
+ {
+ // Buffer was not pending already
+ auto wlBufferC = reinterpret_cast<wl_buffer*> (wlBuffer.c_ptr());
+ // We can refer to the buffer neither by iterator (might be invalidated) nor by
+ // capturing the C++ instance in the lambda (would create a reference loop and
+ // never allow the object to be freed), so use the raw pointer for now
+ wlBuffer.on_release() = [this, wlBufferC]()
+ {
+ std::unique_lock<CCriticalSection> lock(m_pendingBuffersMutex);
+ // Construct a dummy object for searching the set
+ wayland::buffer_t findDummy(wlBufferC, wayland::proxy_t::wrapper_type::foreign);
+ auto iter = m_pendingBuffers.find(findDummy);
+ if (iter == m_pendingBuffers.end())
+ {
+ throw std::logic_error("Cannot release buffer that is not pending");
+ }
+
+ // Do not erase again until buffer is reattached (should not happen anyway, just to be safe)
+ // const_cast is OK since changing the function pointer does not affect
+ // the key in the set
+ const_cast<wayland::buffer_t&>(*iter).on_release() = nullptr;
+ m_pendingBuffers.erase(iter);
+ };
+ }
+
+ wlSurface.attach(wlBuffer, 0, 0);
+ wlSurface.damage(0, 0, borderSurface.surface.size.Width(), borderSurface.surface.size.Height());
+ wlSurface.commit();
+ }
+}
+
+void CWindowDecorator::LoadCursorTheme()
+{
+ std::unique_lock<CCriticalSection> lock(m_mutex);
+ if (!m_cursorTheme)
+ {
+ // Load default cursor theme
+ // Base size of 24px is what most cursor themes seem to have
+ m_cursorTheme = wayland::cursor_theme_t("", 24 * m_scale, m_shm);
+ }
+}
diff --git a/xbmc/windowing/wayland/WindowDecorator.h b/xbmc/windowing/wayland/WindowDecorator.h
new file mode 100644
index 0000000..23db237
--- /dev/null
+++ b/xbmc/windowing/wayland/WindowDecorator.h
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2017-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 "Connection.h"
+#include "Registry.h"
+#include "Seat.h"
+#include "ShellSurface.h"
+#include "Util.h"
+#include "WindowDecorationHandler.h"
+#include "threads/CriticalSection.h"
+#include "utils/Geometry.h"
+
+#include "platform/posix/utils/SharedMemory.h"
+
+#include <array>
+#include <memory>
+#include <set>
+
+#include <wayland-client-protocol.hpp>
+#include <wayland-cursor.hpp>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+enum SurfaceIndex
+{
+ SURFACE_TOP = 0,
+ SURFACE_RIGHT,
+ SURFACE_BOTTOM,
+ SURFACE_LEFT,
+ SURFACE_COUNT
+};
+
+/**
+ * Paint decorations around windows with subcompositing
+ *
+ * With Wayland, applications are responsible for drawing borders on their windows
+ * themselves (client-side decorations). To keep the impact on the overall architecture
+ * low, the Wayland platform implementation uses this very simple renderer to
+ * build ugly but fully functional decorations around the Kodi window. Since Kodi as a
+ * media center is usually not used in windowed mode anyway (except maybe for
+ * development), the impact of the visually not-so-pleasing decorations should
+ * be limited. Nice decorations would require more effort and external libraries for
+ * proper 2D drawing.
+ *
+ * If more platforms decide that client-side decorations would be a good idea to
+ * implement, the decorations could also be integrated with the Kodi skin system.
+ * Then this class could be removed.
+ *
+ * The decorations are positioned around the main surface automatically.
+ */
+class CWindowDecorator final : IRawInputHandlerTouch, IRawInputHandlerPointer
+{
+public:
+ /**
+ * Construct window decorator
+ * \param handler handler for window decoration events
+ * \param connection connection to get Wayland globals
+ * \param mainSurface main surface that decorations are constructed around
+ * \param windowSize full size of the window including decorations
+ * \param scale scale to use for buffers
+ * \param state surface state for adjusting decoration appearance
+ */
+ CWindowDecorator(IWindowDecorationHandler& handler, CConnection& connection, wayland::surface_t const& mainSurface);
+
+ /**
+ * Set decoration state and size by providing full surface size including decorations
+ *
+ * Calculates size of the main surface from size of all surfaces combined (including
+ * window decorations) by subtracting the decoration size
+ *
+ * Decorations will be disabled if state includes STATE_FULLSCREEN
+ *
+ * Call only from main thread
+ */
+ void SetState(CSizeInt size, int scale, IShellSurface::StateBitset state);
+ /**
+ * Get calculated size of main surface
+ */
+ CSizeInt GetMainSurfaceSize() const
+ {
+ return m_mainSurfaceSize;
+ }
+ /**
+ * Get full geometry of the window, including decorations if active
+ */
+ CRectInt GetWindowGeometry() const;
+ /**
+ * Calculate size of main surface given the size of the full area
+ * including decorations and a state
+ */
+ CSizeInt CalculateMainSurfaceSize(CSizeInt size, IShellSurface::StateBitset state) const;
+ /**
+ * Calculate size of full surface including decorations given the size of the
+ * main surface and a state
+ */
+ CSizeInt CalculateFullSurfaceSize(CSizeInt mainSurfaceSize, IShellSurface::StateBitset state) const;
+
+ bool IsDecorationActive() const;
+
+ void AddSeat(CSeat* seat);
+ void RemoveSeat(CSeat* seat);
+
+ struct Buffer
+ {
+ void* data{};
+ std::size_t dataSize{};
+ CSizeInt size{};
+ wayland::buffer_t wlBuffer;
+
+ Buffer() noexcept {}
+
+ Buffer(void* data, std::size_t dataSize, CSizeInt size, wayland::buffer_t&& buffer)
+ : data{data}, dataSize{dataSize}, size{size}, wlBuffer{std::move(buffer)}
+ {}
+
+ std::uint32_t* RgbaBuffer()
+ {
+ return static_cast<std::uint32_t*> (data);
+ }
+ };
+
+ struct Surface
+ {
+ wayland::surface_t wlSurface;
+ Buffer buffer;
+ CSizeInt size;
+ int scale{1};
+ };
+
+private:
+ CWindowDecorator(CWindowDecorator const& other) = delete;
+ CWindowDecorator& operator=(CWindowDecorator const& other) = delete;
+
+ // IRawInputHandlerTouch
+ void OnTouchDown(CSeat* seat,
+ std::uint32_t serial,
+ std::uint32_t time,
+ const wayland::surface_t& surface,
+ std::int32_t id,
+ double x,
+ double y) override;
+ // IRawInputHandlerPointer
+ void OnPointerEnter(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface,
+ double surfaceX,
+ double surfaceY) override;
+ void OnPointerLeave(CSeat* seat,
+ std::uint32_t serial,
+ const wayland::surface_t& surface) override;
+ void OnPointerMotion(CSeat* seat, std::uint32_t time, double surfaceX, double surfaceY) override;
+ void OnPointerButton(CSeat* seat, std::uint32_t serial, std::uint32_t time, std::uint32_t button, wayland::pointer_button_state state) override;
+
+ void Reset(bool reallocate);
+
+ // These functions should not be called directly as they may leave internal
+ // structures in an inconsistent state when called individually - only call
+ // Reset().
+ void ResetButtons();
+ void ResetSurfaces();
+ void ResetShm();
+ void ReattachSubsurfaces();
+ void PositionButtons();
+ void AllocateBuffers();
+ void Repaint();
+ void CommitAllBuffers();
+
+ bool StateHasWindowDecorations(IShellSurface::StateBitset state) const;
+
+ Buffer GetBuffer(CSizeInt size);
+
+ IWindowDecorationHandler& m_handler;
+
+ CSizeInt m_mainSurfaceSize;
+ int m_scale{1};
+ IShellSurface::StateBitset m_windowState;
+
+ CRegistry m_registry;
+ wayland::shm_t m_shm;
+ wayland::shm_pool_t m_shmPool;
+ wayland::compositor_t m_compositor;
+ wayland::subcompositor_t m_subcompositor;
+ wayland::surface_t m_mainSurface;
+
+ std::unique_ptr<KODI::UTILS::POSIX::CSharedMemory> m_memory;
+ std::size_t m_memoryAllocatedSize{};
+
+ struct BorderSurface
+ {
+ Surface surface;
+ wayland::subsurface_t subsurface;
+ CRectInt geometry;
+ /// Region of the surface that should count as being part of the window
+ CRectInt windowRect;
+ };
+ BorderSurface MakeBorderSurface();
+
+ /**
+ * Mutex for all surface/button state that is accessed from the event pump thread via seat events
+ * and the main thread:
+ * m_surfaces, m_buttons, m_windowSize, m_cursorTheme
+ *
+ * If size etc. is changing, mutex should be locked for the entire duration of the
+ * change until the internal state (surface, button size etc.) is consistent again.
+ */
+ CCriticalSection m_mutex;
+
+ std::array<BorderSurface, 4> m_borderSurfaces;
+
+ std::set<wayland::buffer_t, WaylandCPtrCompare> m_pendingBuffers;
+ CCriticalSection m_pendingBuffersMutex;
+
+ struct SeatState
+ {
+ CSeat* seat;
+ SurfaceIndex currentSurface{SURFACE_COUNT};
+ CPoint pointerPosition;
+
+ std::uint32_t pointerEnterSerial{};
+ std::string cursorName;
+ wayland::surface_t cursor;
+
+ explicit SeatState(CSeat* seat)
+ : seat{seat}
+ {}
+ };
+ std::map<std::uint32_t, SeatState> m_seats;
+
+ struct Button
+ {
+ CRectInt position;
+ bool hovered{};
+ std::function<void(Surface&, CRectInt, bool /* hover */)> draw;
+ std::function<void()> onClick;
+ };
+ std::vector<Button> m_buttons;
+
+ wayland::cursor_theme_t m_cursorTheme;
+ std::uint32_t m_buttonColor;
+
+ void LoadCursorTheme();
+
+ void UpdateSeatCursor(SeatState& seatState);
+ void UpdateButtonHoverState();
+ void HandleSeatClick(SeatState const& seatState, SurfaceIndex surface, std::uint32_t serial, std::uint32_t button, CPoint position);
+};
+
+}
+}
+}
diff --git a/xbmc/windowing/wayland/XkbcommonKeymap.cpp b/xbmc/windowing/wayland/XkbcommonKeymap.cpp
new file mode 100644
index 0000000..eb27cc9
--- /dev/null
+++ b/xbmc/windowing/wayland/XkbcommonKeymap.cpp
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2017-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 "XkbcommonKeymap.h"
+
+#include "Util.h"
+#include "utils/log.h"
+
+#include <iostream>
+#include <sstream>
+#include <stdexcept>
+#include <vector>
+
+using namespace KODI::WINDOWING::WAYLAND;
+
+namespace
+{
+
+struct ModifierNameXBMCMapping
+{
+ const char* name;
+ XBMCMod xbmc;
+};
+
+static const std::vector<ModifierNameXBMCMapping> ModifierNameXBMCMappings = {
+ { XKB_MOD_NAME_CTRL, XBMCKMOD_LCTRL },
+ { XKB_MOD_NAME_SHIFT, XBMCKMOD_LSHIFT },
+ { XKB_MOD_NAME_LOGO, XBMCKMOD_LSUPER },
+ { XKB_MOD_NAME_ALT, XBMCKMOD_LALT },
+ { "Meta", XBMCKMOD_LMETA },
+ { "RControl", XBMCKMOD_RCTRL },
+ { "RShift", XBMCKMOD_RSHIFT },
+ { "Hyper", XBMCKMOD_RSUPER },
+ { "AltGr", XBMCKMOD_RALT },
+ { XKB_LED_NAME_CAPS, XBMCKMOD_CAPS },
+ { XKB_LED_NAME_NUM, XBMCKMOD_NUM },
+ { XKB_LED_NAME_SCROLL, XBMCKMOD_MODE }
+};
+
+static const std::map<xkb_keycode_t, XBMCKey> XkbKeycodeXBMCMappings = {
+ // Function keys before start of ASCII printable character range
+ { XKB_KEY_BackSpace, XBMCK_BACKSPACE },
+ { XKB_KEY_Tab, XBMCK_TAB },
+ { XKB_KEY_Clear, XBMCK_CLEAR },
+ { XKB_KEY_Return, XBMCK_RETURN },
+ { XKB_KEY_Pause, XBMCK_PAUSE },
+ { XKB_KEY_Escape, XBMCK_ESCAPE },
+
+ // ASCII printable range - not included here
+
+ // Function keys after end of ASCII printable character range
+ { XKB_KEY_Delete, XBMCK_DELETE },
+
+ // Multimedia keys
+ { XKB_KEY_XF86Back, XBMCK_BROWSER_BACK },
+ { XKB_KEY_XF86Forward, XBMCK_BROWSER_FORWARD },
+ { XKB_KEY_XF86Refresh, XBMCK_BROWSER_REFRESH },
+ { XKB_KEY_XF86Stop, XBMCK_BROWSER_STOP },
+ { XKB_KEY_XF86Search, XBMCK_BROWSER_SEARCH },
+ // XKB_KEY_XF86Favorites could be XBMCK_BROWSER_FAVORITES or XBMCK_FAVORITES,
+ // XBMCK_FAVORITES was chosen here because it is more general
+ { XKB_KEY_XF86HomePage, XBMCK_BROWSER_HOME },
+ { XKB_KEY_XF86AudioMute, XBMCK_VOLUME_MUTE },
+ { XKB_KEY_XF86AudioLowerVolume, XBMCK_VOLUME_DOWN },
+ { XKB_KEY_XF86AudioRaiseVolume, XBMCK_VOLUME_UP },
+ { XKB_KEY_XF86AudioNext, XBMCK_MEDIA_NEXT_TRACK },
+ { XKB_KEY_XF86AudioPrev, XBMCK_MEDIA_PREV_TRACK },
+ { XKB_KEY_XF86AudioStop, XBMCK_MEDIA_STOP },
+ { XKB_KEY_XF86AudioPause, XBMCK_MEDIA_PLAY_PAUSE },
+ { XKB_KEY_XF86Mail, XBMCK_LAUNCH_MAIL },
+ { XKB_KEY_XF86Select, XBMCK_LAUNCH_MEDIA_SELECT },
+ { XKB_KEY_XF86Launch0, XBMCK_LAUNCH_APP1 },
+ { XKB_KEY_XF86Launch1, XBMCK_LAUNCH_APP2 },
+ { XKB_KEY_XF86WWW, XBMCK_LAUNCH_FILE_BROWSER },
+ { XKB_KEY_XF86AudioMedia, XBMCK_LAUNCH_MEDIA_CENTER },
+ { XKB_KEY_XF86AudioRewind, XBMCK_MEDIA_REWIND },
+ { XKB_KEY_XF86AudioForward, XBMCK_MEDIA_FASTFORWARD },
+
+ // Numeric keypad
+ { XKB_KEY_KP_0, XBMCK_KP0 },
+ { XKB_KEY_KP_1, XBMCK_KP1 },
+ { XKB_KEY_KP_2, XBMCK_KP2 },
+ { XKB_KEY_KP_3, XBMCK_KP3 },
+ { XKB_KEY_KP_4, XBMCK_KP4 },
+ { XKB_KEY_KP_5, XBMCK_KP5 },
+ { XKB_KEY_KP_6, XBMCK_KP6 },
+ { XKB_KEY_KP_7, XBMCK_KP7 },
+ { XKB_KEY_KP_8, XBMCK_KP8 },
+ { XKB_KEY_KP_9, XBMCK_KP9 },
+ { XKB_KEY_KP_Decimal, XBMCK_KP_PERIOD },
+ { XKB_KEY_KP_Divide, XBMCK_KP_DIVIDE },
+ { XKB_KEY_KP_Multiply, XBMCK_KP_MULTIPLY },
+ { XKB_KEY_KP_Subtract, XBMCK_KP_MINUS },
+ { XKB_KEY_KP_Add, XBMCK_KP_PLUS },
+ { XKB_KEY_KP_Enter, XBMCK_KP_ENTER },
+ { XKB_KEY_KP_Equal, XBMCK_KP_EQUALS },
+
+ // Arrows + Home/End pad
+ { XKB_KEY_Up, XBMCK_UP },
+ { XKB_KEY_Down, XBMCK_DOWN },
+ { XKB_KEY_Right, XBMCK_RIGHT },
+ { XKB_KEY_Left, XBMCK_LEFT },
+ { XKB_KEY_Insert, XBMCK_INSERT },
+ { XKB_KEY_Home, XBMCK_HOME },
+ { XKB_KEY_End, XBMCK_END },
+ { XKB_KEY_Page_Up, XBMCK_PAGEUP },
+ { XKB_KEY_Page_Down, XBMCK_PAGEDOWN },
+
+ // Function keys
+ { XKB_KEY_F1, XBMCK_F1 },
+ { XKB_KEY_F2, XBMCK_F2 },
+ { XKB_KEY_F3, XBMCK_F3 },
+ { XKB_KEY_F4, XBMCK_F4 },
+ { XKB_KEY_F5, XBMCK_F5 },
+ { XKB_KEY_F6, XBMCK_F6 },
+ { XKB_KEY_F7, XBMCK_F7 },
+ { XKB_KEY_F8, XBMCK_F8 },
+ { XKB_KEY_F9, XBMCK_F9 },
+ { XKB_KEY_F10, XBMCK_F10 },
+ { XKB_KEY_F11, XBMCK_F11 },
+ { XKB_KEY_F12, XBMCK_F12 },
+ { XKB_KEY_F13, XBMCK_F13 },
+ { XKB_KEY_F14, XBMCK_F14 },
+ { XKB_KEY_F15, XBMCK_F15 },
+
+ // Key state modifier keys
+ { XKB_KEY_Num_Lock, XBMCK_NUMLOCK },
+ { XKB_KEY_Caps_Lock, XBMCK_CAPSLOCK },
+ { XKB_KEY_Scroll_Lock, XBMCK_SCROLLOCK },
+ { XKB_KEY_Shift_R, XBMCK_RSHIFT },
+ { XKB_KEY_Shift_L, XBMCK_LSHIFT },
+ { XKB_KEY_Control_R, XBMCK_RCTRL },
+ { XKB_KEY_Control_L, XBMCK_LCTRL },
+ { XKB_KEY_Alt_R, XBMCK_RALT },
+ { XKB_KEY_Alt_L, XBMCK_LALT },
+ { XKB_KEY_Meta_R, XBMCK_RMETA },
+ { XKB_KEY_Meta_L, XBMCK_LMETA },
+ { XKB_KEY_Super_R, XBMCK_RSUPER },
+ { XKB_KEY_Super_L, XBMCK_LSUPER },
+ // XKB does not have XBMCK_MODE/"Alt Gr" - probably equal to XKB_KEY_Alt_R
+ { XKB_KEY_Multi_key, XBMCK_COMPOSE },
+
+ // Miscellaneous function keys
+ { XKB_KEY_Help, XBMCK_HELP },
+ { XKB_KEY_Print, XBMCK_PRINT },
+ // Unmapped: XBMCK_SYSREQ
+ { XKB_KEY_Break, XBMCK_BREAK },
+ { XKB_KEY_Menu, XBMCK_MENU },
+ { XKB_KEY_XF86PowerOff, XBMCK_POWER },
+ { XKB_KEY_EcuSign, XBMCK_EURO },
+ { XKB_KEY_Undo, XBMCK_UNDO },
+ { XKB_KEY_XF86Sleep, XBMCK_SLEEP },
+ // Unmapped: XBMCK_GUIDE, XBMCK_SETTINGS, XBMCK_INFO
+ { XKB_KEY_XF86Red, XBMCK_RED },
+ { XKB_KEY_XF86Green, XBMCK_GREEN },
+ { XKB_KEY_XF86Yellow, XBMCK_YELLOW },
+ { XKB_KEY_XF86Blue, XBMCK_BLUE },
+ // Unmapped: XBMCK_ZOOM, XBMCK_TEXT
+ { XKB_KEY_XF86Favorites, XBMCK_FAVORITES },
+ { XKB_KEY_XF86HomePage, XBMCK_HOMEPAGE },
+ // Unmapped: XBMCK_CONFIG, XBMCK_EPG
+
+ // Media keys
+ { XKB_KEY_XF86Eject, XBMCK_EJECT },
+ // XBMCK_STOP clashes with XBMCK_MEDIA_STOP
+ { XKB_KEY_XF86AudioRecord, XBMCK_RECORD },
+ // XBMCK_REWIND clashes with XBMCK_MEDIA_REWIND
+ { XKB_KEY_XF86Phone, XBMCK_PHONE },
+ { XKB_KEY_XF86AudioPlay, XBMCK_PLAY },
+ { XKB_KEY_XF86AudioRandomPlay, XBMCK_SHUFFLE }
+ // XBMCK_FASTFORWARD clashes with XBMCK_MEDIA_FASTFORWARD
+};
+
+}
+
+CXkbcommonContext::CXkbcommonContext(xkb_context_flags flags)
+: m_context{xkb_context_new(flags), XkbContextDeleter()}
+{
+ if (!m_context)
+ {
+ throw std::runtime_error("Failed to create xkb context");
+ }
+}
+
+void CXkbcommonContext::XkbContextDeleter::operator()(xkb_context* ctx) const
+{
+ xkb_context_unref(ctx);
+}
+
+std::unique_ptr<CXkbcommonKeymap> CXkbcommonContext::KeymapFromString(std::string const& keymap)
+{
+ std::unique_ptr<xkb_keymap, CXkbcommonKeymap::XkbKeymapDeleter> xkbKeymap{xkb_keymap_new_from_string(m_context.get(), keymap.c_str(), XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS), CXkbcommonKeymap::XkbKeymapDeleter()};
+
+ if (!xkbKeymap)
+ {
+ throw std::runtime_error("Failed to compile keymap");
+ }
+
+ return std::unique_ptr<CXkbcommonKeymap>{new CXkbcommonKeymap(std::move(xkbKeymap))};
+}
+
+std::unique_ptr<CXkbcommonKeymap> CXkbcommonContext::KeymapFromNames(const std::string& rules, const std::string& model, const std::string& layout, const std::string& variant, const std::string& options)
+{
+ xkb_rule_names names = {
+ rules.c_str(),
+ model.c_str(),
+ layout.c_str(),
+ variant.c_str(),
+ options.c_str()
+ };
+
+ std::unique_ptr<xkb_keymap, CXkbcommonKeymap::XkbKeymapDeleter> keymap{xkb_keymap_new_from_names(m_context.get(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS), CXkbcommonKeymap::XkbKeymapDeleter()};
+
+ if (!keymap)
+ {
+ throw std::runtime_error("Failed to compile keymap");
+ }
+
+ return std::unique_ptr<CXkbcommonKeymap>{new CXkbcommonKeymap(std::move(keymap))};
+}
+
+std::unique_ptr<xkb_state, CXkbcommonKeymap::XkbStateDeleter> CXkbcommonKeymap::CreateXkbStateFromKeymap(xkb_keymap* keymap)
+{
+ std::unique_ptr<xkb_state, XkbStateDeleter> state{xkb_state_new(keymap), XkbStateDeleter()};
+
+ if (!state)
+ {
+ throw std::runtime_error("Failed to create keyboard state");
+ }
+
+ return state;
+}
+
+CXkbcommonKeymap::CXkbcommonKeymap(std::unique_ptr<xkb_keymap, XkbKeymapDeleter> keymap)
+: m_keymap{std::move(keymap)}, m_state{CreateXkbStateFromKeymap(m_keymap.get())}
+{
+ // Lookup modifier indices and create new map - this is more efficient
+ // than looking the modifiers up by name each time
+ for (auto const& nameMapping : ModifierNameXBMCMappings)
+ {
+ xkb_mod_index_t index = xkb_keymap_mod_get_index(m_keymap.get(), nameMapping.name);
+ if (index != XKB_MOD_INVALID)
+ {
+ m_modifierMappings.emplace_back(index, nameMapping.xbmc);
+ }
+ }
+}
+
+void CXkbcommonKeymap::XkbStateDeleter::operator()(xkb_state* state) const
+{
+ xkb_state_unref(state);
+}
+
+void CXkbcommonKeymap::XkbKeymapDeleter::operator()(xkb_keymap* keymap) const
+{
+ xkb_keymap_unref(keymap);
+}
+
+xkb_keysym_t CXkbcommonKeymap::KeysymForKeycode(xkb_keycode_t code) const
+{
+ return xkb_state_key_get_one_sym(m_state.get(), code);
+}
+
+xkb_mod_mask_t CXkbcommonKeymap::CurrentModifiers() const
+{
+ return xkb_state_serialize_mods(m_state.get(), XKB_STATE_MODS_EFFECTIVE);
+}
+
+void CXkbcommonKeymap::UpdateMask(xkb_mod_mask_t depressed, xkb_mod_mask_t latched, xkb_mod_mask_t locked, xkb_mod_mask_t group)
+{
+ xkb_state_update_mask(m_state.get(), depressed, latched, locked, 0, 0, group);
+}
+
+XBMCMod CXkbcommonKeymap::ActiveXBMCModifiers() const
+{
+ xkb_mod_mask_t mask(CurrentModifiers());
+ XBMCMod xbmcModifiers = XBMCKMOD_NONE;
+
+ for (auto const& mapping : m_modifierMappings)
+ {
+ if (mask & (1 << mapping.xkb))
+ {
+ xbmcModifiers = static_cast<XBMCMod> (xbmcModifiers | mapping.xbmc);
+ }
+ }
+
+ return xbmcModifiers;
+}
+
+XBMCKey CXkbcommonKeymap::XBMCKeyForKeysym(xkb_keysym_t sym)
+{
+ if (sym >= 'A' && sym <= 'Z')
+ {
+ // Uppercase ASCII characters must be lowercased as XBMCKey is modifier-invariant
+ return static_cast<XBMCKey> (sym + 'a' - 'A');
+ }
+ else if (sym >= 0x20 /* ASCII space */ && sym <= 0x7E /* ASCII tilde */)
+ {
+ // Rest of ASCII printable character range is code-compatible
+ return static_cast<XBMCKey> (sym);
+ }
+
+ // Try mapping
+ auto mapping = XkbKeycodeXBMCMappings.find(sym);
+ if (mapping != XkbKeycodeXBMCMappings.end())
+ {
+ return mapping->second;
+ }
+ else
+ {
+ return XBMCK_UNKNOWN;
+ }
+}
+
+XBMCKey CXkbcommonKeymap::XBMCKeyForKeycode(xkb_keycode_t code) const
+{
+ return XBMCKeyForKeysym(KeysymForKeycode(code));
+}
+
+std::uint32_t CXkbcommonKeymap::UnicodeCodepointForKeycode(xkb_keycode_t code) const
+{
+ return xkb_state_key_get_utf32(m_state.get(), code);
+}
+
+bool CXkbcommonKeymap::ShouldKeycodeRepeat(xkb_keycode_t code) const
+{
+ return xkb_keymap_key_repeats(m_keymap.get(), code);
+}
diff --git a/xbmc/windowing/wayland/XkbcommonKeymap.h b/xbmc/windowing/wayland/XkbcommonKeymap.h
new file mode 100644
index 0000000..7718b02
--- /dev/null
+++ b/xbmc/windowing/wayland/XkbcommonKeymap.h
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2017-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 "input/XBMC_keysym.h"
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include <xkbcommon/xkbcommon.h>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WAYLAND
+{
+
+/**
+ * A wrapper class around an xkbcommon keymap and state tracker.
+ *
+ * This class knows about some common modifier combinations and keeps
+ * track of the currently pressed keys and modifiers. It also has
+ * some utility functions to transform hardware keycodes into
+ * a common representation.
+ *
+ * Since this class is keeping track of all the pressed and depressed
+ * modifiers, IT MUST ALWAYS BE KEPT UP TO DATE WITH ANY NEWLY
+ * PRESSED MODIFIERS. Undefined behaviour will result if it is not
+ * kept up to date.
+ *
+ * Instances can be easily created from keymap strings with \ref CXkbcommonContext
+ */
+class CXkbcommonKeymap
+{
+public:
+ struct XkbKeymapDeleter
+ {
+ void operator()(xkb_keymap* keymap) const;
+ };
+
+ /**
+ * Construct for known xkb_keymap
+ */
+ explicit CXkbcommonKeymap(std::unique_ptr<xkb_keymap, XkbKeymapDeleter> keymap);
+
+ /**
+ * Get xkb keysym for keycode - only a single keysym is supported
+ */
+ xkb_keysym_t KeysymForKeycode(xkb_keycode_t code) const;
+ /**
+ * Updates the currently depressed, latched, locked and group
+ * modifiers for a keyboard being tracked.
+ *
+ * This function must be called whenever modifiers change, or the state will
+ * be wrong and keysym translation will be off.
+ */
+ void UpdateMask(xkb_mod_mask_t depressed,
+ xkb_mod_mask_t latched,
+ xkb_mod_mask_t locked,
+ xkb_mod_mask_t group);
+ /**
+ * Gets the currently depressed, latched and locked modifiers
+ * for the keyboard
+ */
+ xkb_mod_mask_t CurrentModifiers() const;
+ /**
+ * Get XBMCKey for provided keycode
+ */
+ XBMCKey XBMCKeyForKeycode(xkb_keycode_t code) const;
+ /**
+ * \ref CurrentModifiers with XBMC flags
+ */
+ XBMCMod ActiveXBMCModifiers() const;
+ /**
+ * Get Unicode codepoint/UTF32 code for provided keycode
+ */
+ std::uint32_t UnicodeCodepointForKeycode(xkb_keycode_t code) const;
+ /**
+ * Check whether a given keycode should have key repeat
+ */
+ bool ShouldKeycodeRepeat(xkb_keycode_t code) const;
+
+ static XBMCKey XBMCKeyForKeysym(xkb_keysym_t sym);
+
+private:
+ struct XkbStateDeleter
+ {
+ void operator()(xkb_state* state) const;
+ };
+ static std::unique_ptr<xkb_state, XkbStateDeleter> CreateXkbStateFromKeymap(xkb_keymap* keymap);
+
+ std::unique_ptr<xkb_keymap, XkbKeymapDeleter> m_keymap;
+ std::unique_ptr<xkb_state, XkbStateDeleter> m_state;
+
+ struct ModifierMapping
+ {
+ xkb_mod_index_t xkb;
+ XBMCMod xbmc;
+ ModifierMapping(xkb_mod_index_t xkb, XBMCMod xbmc)
+ : xkb{xkb}, xbmc{xbmc}
+ {}
+ };
+ std::vector<ModifierMapping> m_modifierMappings;
+};
+
+class CXkbcommonContext
+{
+public:
+ explicit CXkbcommonContext(xkb_context_flags flags = XKB_CONTEXT_NO_FLAGS);
+
+ /**
+ * Opens a shared memory region and parses the data in it to an
+ * xkbcommon keymap.
+ *
+ * This function does not own the file descriptor. It must not be closed
+ * from this function.
+ */
+ std::unique_ptr<CXkbcommonKeymap> KeymapFromString(std::string const& keymap);
+ std::unique_ptr<CXkbcommonKeymap> KeymapFromNames(const std::string &rules, const std::string &model, const std::string &layout, const std::string &variant, const std::string &options);
+
+private:
+ struct XkbContextDeleter
+ {
+ void operator()(xkb_context* ctx) const;
+ };
+ std::unique_ptr<xkb_context, XkbContextDeleter> m_context;
+};
+
+
+}
+}
+}
diff --git a/xbmc/windowing/win10/CMakeLists.txt b/xbmc/windowing/win10/CMakeLists.txt
new file mode 100644
index 0000000..7e41c17
--- /dev/null
+++ b/xbmc/windowing/win10/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(SOURCES WinEventsWin10.cpp
+ WinSystemWin10.cpp
+ WinSystemWin10DX.cpp
+ ../windows/VideoSyncD3D.cpp)
+
+set(HEADERS WinEventsWin10.h
+ WinSystemWin10.h
+ WinSystemWin10DX.h
+ ../windows/VideoSyncD3D.h
+ ../windows/WinKeyMap.h)
+
+core_add_library(windowing_windowsstore)
diff --git a/xbmc/windowing/win10/WinEventsWin10.cpp b/xbmc/windowing/win10/WinEventsWin10.cpp
new file mode 100644
index 0000000..35da911
--- /dev/null
+++ b/xbmc/windowing/win10/WinEventsWin10.cpp
@@ -0,0 +1,658 @@
+/*
+ * 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 "WinEventsWin10.h"
+
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "application/Application.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "input/mouse/MouseStat.h"
+#include "input/touch/generic/GenericTouchInputHandler.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "rendering/dx/DeviceResources.h"
+#include "rendering/dx/RenderContext.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SystemInfo.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "windowing/windows/WinKeyMap.h"
+
+#include "platform/win10/input/RemoteControlXbox.h"
+
+#include <winrt/Windows.Devices.Input.h>
+
+namespace winrt
+{
+ using namespace Windows::Foundation;
+}
+using namespace winrt::Windows::ApplicationModel::Core;
+using namespace winrt::Windows::Devices::Input;
+using namespace winrt::Windows::Graphics::Display;
+using namespace winrt::Windows::Media;
+using namespace winrt::Windows::System;
+using namespace winrt::Windows::UI::Core;
+using namespace winrt::Windows::UI::Input;
+using namespace winrt::Windows::UI::ViewManagement;
+
+using namespace PERIPHERALS;
+
+static winrt::Point GetScreenPoint(winrt::Point point)
+{
+ auto dpi = DX::DeviceResources::Get()->GetDpi();
+ return winrt::Point(DX::ConvertDipsToPixels(point.X, dpi), DX::ConvertDipsToPixels(point.Y, dpi));
+}
+
+CWinEventsWin10::CWinEventsWin10() = default;
+CWinEventsWin10::~CWinEventsWin10() = default;
+
+void CWinEventsWin10::InitOSKeymap(void)
+{
+ KODI::WINDOWING::WINDOWS::DIB_InitOSKeymap();
+}
+
+void CWinEventsWin10::MessagePush(XBMC_Event *newEvent)
+{
+ // push input events in the queue they may init modal dialog which init
+ // deeper message loop and call the deeper MessagePump from there.
+ if ( newEvent->type == XBMC_KEYDOWN
+ || newEvent->type == XBMC_KEYUP
+ || newEvent->type == XBMC_MOUSEMOTION
+ || newEvent->type == XBMC_MOUSEBUTTONDOWN
+ || newEvent->type == XBMC_MOUSEBUTTONUP
+ || newEvent->type == XBMC_TOUCH)
+ {
+ m_events.push(*newEvent);
+ }
+ else
+ {
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(*newEvent);
+ }
+}
+
+bool CWinEventsWin10::MessagePump()
+{
+ bool ret = false;
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+
+ // processes all pending events and exits immediately
+ CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
+
+ XBMC_Event pumpEvent;
+ while (m_events.try_pop(pumpEvent))
+ {
+ if (appPort)
+ ret |= appPort->OnEvent(pumpEvent);
+
+ if (pumpEvent.type == XBMC_MOUSEBUTTONUP)
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(GUI_MSG_UNFOCUS_ALL, 0, 0, 0, 0);
+ }
+ return ret;
+}
+
+size_t CWinEventsWin10::GetQueueSize()
+{
+ return m_events.unsafe_size();
+}
+
+void CWinEventsWin10::InitEventHandlers(const CoreWindow& window)
+{
+ CWinEventsWin10::InitOSKeymap();
+
+ //window->SetPointerCapture();
+
+ // window
+ window.SizeChanged({ this, &CWinEventsWin10::OnWindowSizeChanged });
+ window.ResizeStarted({ this, &CWinEventsWin10::OnWindowResizeStarted });
+ window.ResizeCompleted({ this, &CWinEventsWin10::OnWindowResizeCompleted });
+ window.Closed({ this, &CWinEventsWin10::OnWindowClosed});
+ window.VisibilityChanged(CWinEventsWin10::OnVisibilityChanged);
+ window.Activated(CWinEventsWin10::OnWindowActivationChanged);
+ // mouse, touch and pen
+ window.PointerPressed({ this, &CWinEventsWin10::OnPointerPressed });
+ window.PointerMoved({ this, &CWinEventsWin10::OnPointerMoved });
+ window.PointerReleased({ this, &CWinEventsWin10::OnPointerReleased });
+ window.PointerExited({ this, &CWinEventsWin10::OnPointerExited });
+ window.PointerWheelChanged({ this, &CWinEventsWin10::OnPointerWheelChanged });
+ // keyboard
+ window.Dispatcher().AcceleratorKeyActivated({ this, &CWinEventsWin10::OnAcceleratorKeyActivated });
+ // display
+ DisplayInformation currentDisplayInformation = DisplayInformation::GetForCurrentView();
+ currentDisplayInformation.DpiChanged(CWinEventsWin10::OnDpiChanged);
+ currentDisplayInformation.OrientationChanged(CWinEventsWin10::OnOrientationChanged);
+ DisplayInformation::DisplayContentsInvalidated(CWinEventsWin10::OnDisplayContentsInvalidated);
+ // system
+ SystemNavigationManager sysNavManager = SystemNavigationManager::GetForCurrentView();
+ sysNavManager.BackRequested(CWinEventsWin10::OnBackRequested);
+
+ // requirement for backgroup playback
+ m_smtc = SystemMediaTransportControls::GetForCurrentView();
+ if (m_smtc)
+ {
+ m_smtc.IsPlayEnabled(true);
+ m_smtc.IsPauseEnabled(true);
+ m_smtc.IsStopEnabled(true);
+ m_smtc.IsRecordEnabled(true);
+ m_smtc.IsNextEnabled(true);
+ m_smtc.IsPreviousEnabled(true);
+ m_smtc.IsFastForwardEnabled(true);
+ m_smtc.IsRewindEnabled(true);
+ m_smtc.IsChannelUpEnabled(true);
+ m_smtc.IsChannelDownEnabled(true);
+ if (CSysInfo::GetWindowsDeviceFamily() != CSysInfo::WindowsDeviceFamily::Xbox)
+ {
+ m_smtc.ButtonPressed(CWinEventsWin10::OnSystemMediaButtonPressed);
+ }
+ m_smtc.IsEnabled(true);;
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+ }
+ if (CSysInfo::GetWindowsDeviceFamily() == CSysInfo::WindowsDeviceFamily::Xbox)
+ {
+ m_remote = std::make_unique<CRemoteControlXbox>();
+ m_remote->Initialize();
+ }
+}
+
+void CWinEventsWin10::UpdateWindowSize()
+{
+ auto size = DX::DeviceResources::Get()->GetOutputSize();
+
+ CLog::Log(LOGDEBUG, __FUNCTION__ ": window resize event {:f} x {:f} (as:{})", size.Width,
+ size.Height,
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen ? "true"
+ : "false");
+
+ auto appView = ApplicationView::GetForCurrentView();
+ appView.SetDesiredBoundsMode(ApplicationViewBoundsMode::UseCoreWindow);
+
+ // seems app has lost FS mode it may occurs if an user use core window's button
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen && !appView.IsFullScreenMode())
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_fullScreen = false;
+
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_VIDEORESIZE;
+ newEvent.resize.w = size.Width;
+ newEvent.resize.h = size.Height;
+ if (g_application.GetRenderGUI() && !DX::Windowing()->IsAlteringWindow() && newEvent.resize.w > 0 && newEvent.resize.h > 0)
+ MessagePush(&newEvent);
+}
+
+void CWinEventsWin10::OnResize(float width, float height)
+{
+ CLog::Log(LOGDEBUG, __FUNCTION__": window size changed.");
+ m_logicalWidth = width;
+ m_logicalHeight = height;
+ m_bResized = true;
+
+ if (m_sizeChanging)
+ return;
+
+ HandleWindowSizeChanged();
+}
+
+// Window event handlers.
+void CWinEventsWin10::OnWindowSizeChanged(const CoreWindow&, const WindowSizeChangedEventArgs& args)
+{
+ OnResize(args.Size().Width, args.Size().Height);
+}
+
+void CWinEventsWin10::OnWindowResizeStarted(const CoreWindow& sender, const winrt::IInspectable&)
+{
+ CLog::Log(LOGDEBUG, __FUNCTION__": window resize started.");
+ m_logicalPosX = sender.Bounds().X;
+ m_logicalPosY = sender.Bounds().Y;
+ m_sizeChanging = true;
+}
+
+void CWinEventsWin10::OnWindowResizeCompleted(const CoreWindow& sender, const winrt::IInspectable&)
+{
+ CLog::Log(LOGDEBUG, __FUNCTION__": window resize completed.");
+ m_sizeChanging = false;
+
+ if (m_logicalPosX != sender.Bounds().X || m_logicalPosY != sender.Bounds().Y)
+ m_bMoved = true;
+
+ HandleWindowSizeChanged();
+}
+
+void CWinEventsWin10::HandleWindowSizeChanged()
+{
+ CLog::Log(LOGDEBUG, __FUNCTION__": window size/move handled.");
+ if (m_bMoved)
+ {
+ // it will get position from CoreWindow
+ DX::Windowing()->OnMove(0, 0);
+ }
+ if (m_bResized)
+ {
+ DX::Windowing()->OnResize(m_logicalWidth, m_logicalHeight);
+ UpdateWindowSize();
+ }
+ m_bResized = false;
+ m_bMoved = false;
+}
+
+void CWinEventsWin10::OnVisibilityChanged(const CoreWindow& sender, const VisibilityChangedEventArgs& args)
+{
+ bool active = g_application.GetRenderGUI();
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->SetRenderGUI(args.Visible());
+
+ if (g_application.GetRenderGUI() != active)
+ DX::Windowing()->NotifyAppActiveChange(g_application.GetRenderGUI());
+ CLog::Log(LOGDEBUG, __FUNCTION__ ": window is {}",
+ g_application.GetRenderGUI() ? "shown" : "hidden");
+}
+
+void CWinEventsWin10::OnWindowActivationChanged(const CoreWindow& sender, const WindowActivatedEventArgs& args)
+{
+ bool active = g_application.GetRenderGUI();
+ if (args.WindowActivationState() == CoreWindowActivationState::Deactivated)
+ {
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->SetRenderGUI(DX::Windowing()->WindowedMode());
+ }
+ else if (args.WindowActivationState() == CoreWindowActivationState::PointerActivated
+ || args.WindowActivationState() == CoreWindowActivationState::CodeActivated)
+ {
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->SetRenderGUI(true);
+ }
+ if (g_application.GetRenderGUI() != active)
+ DX::Windowing()->NotifyAppActiveChange(g_application.GetRenderGUI());
+ CLog::Log(LOGDEBUG, __FUNCTION__ ": window is {}",
+ g_application.GetRenderGUI() ? "active" : "inactive");
+}
+
+void CWinEventsWin10::OnWindowClosed(const CoreWindow& sender, const CoreWindowEventArgs& args)
+{
+ // send quit command to the application if it's still running
+ if (!g_application.m_bStop)
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_QUIT;
+ MessagePush(&newEvent);
+ }
+}
+
+void CWinEventsWin10::OnPointerPressed(const CoreWindow&, const PointerEventArgs& args)
+{
+ XBMC_Event newEvent = {};
+
+ PointerPoint point = args.CurrentPoint();
+ auto position = GetScreenPoint(point.Position());
+
+ if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Touch)
+ {
+ CGenericTouchInputHandler::GetInstance().HandleTouchInput(TouchInputDown, position.X, position.Y, point.Timestamp(), 0, 10);
+ return;
+ }
+ else
+ {
+ newEvent.type = XBMC_MOUSEBUTTONDOWN;
+ newEvent.button.x = position.X;
+ newEvent.button.y = position.Y;
+ if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Mouse)
+ {
+ if (point.Properties().IsLeftButtonPressed())
+ newEvent.button.button = XBMC_BUTTON_LEFT;
+ else if (point.Properties().IsMiddleButtonPressed())
+ newEvent.button.button = XBMC_BUTTON_MIDDLE;
+ else if (point.Properties().IsRightButtonPressed())
+ newEvent.button.button = XBMC_BUTTON_RIGHT;
+ }
+ else if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Pen)
+ {
+ // pen
+ // TODO
+ }
+ }
+ MessagePush(&newEvent);
+}
+
+void CWinEventsWin10::OnPointerMoved(const CoreWindow&, const PointerEventArgs& args)
+{
+ PointerPoint point = args.CurrentPoint();
+ auto position = GetScreenPoint(point.Position());
+
+ if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Touch)
+ {
+ if (point.IsInContact())
+ {
+ CGenericTouchInputHandler::GetInstance().UpdateTouchPointer(0, position.X, position.Y, point.Timestamp(), 10.f);
+ CGenericTouchInputHandler::GetInstance().HandleTouchInput(TouchInputMove, position.X, position.Y, point.Timestamp(), 0, 10.f);
+ }
+ return;
+ }
+
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEMOTION;
+ newEvent.motion.x = position.X;
+ newEvent.motion.y = position.Y;
+ MessagePush(&newEvent);
+}
+
+void CWinEventsWin10::OnPointerReleased(const CoreWindow&, const PointerEventArgs& args)
+{
+ PointerPoint point = args.CurrentPoint();
+ auto position = GetScreenPoint(point.Position());
+
+ if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Touch)
+ {
+ CGenericTouchInputHandler::GetInstance().HandleTouchInput(TouchInputUp, position.X, position.Y, point.Timestamp(), 0, 10);
+ return;
+ }
+
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEBUTTONUP;
+ newEvent.button.x = position.X;
+ newEvent.button.y = position.Y;
+
+ if (point.Properties().PointerUpdateKind() == PointerUpdateKind::LeftButtonReleased)
+ newEvent.button.button = XBMC_BUTTON_LEFT;
+ else if (point.Properties().PointerUpdateKind() == PointerUpdateKind::MiddleButtonReleased)
+ newEvent.button.button = XBMC_BUTTON_MIDDLE;
+ else if (point.Properties().PointerUpdateKind() == PointerUpdateKind::RightButtonReleased)
+ newEvent.button.button = XBMC_BUTTON_RIGHT;
+
+ MessagePush(&newEvent);
+}
+
+void CWinEventsWin10::OnPointerExited(const CoreWindow&, const PointerEventArgs& args)
+{
+ const PointerPoint& point = args.CurrentPoint();
+ auto position = GetScreenPoint(point.Position());
+
+ if (point.PointerDevice().PointerDeviceType() == PointerDeviceType::Touch)
+ {
+ CGenericTouchInputHandler::GetInstance().HandleTouchInput(TouchInputAbort, position.X, position.Y, point.Timestamp(), 0, 10);
+ }
+}
+
+void CWinEventsWin10::OnPointerWheelChanged(const CoreWindow&, const PointerEventArgs& args)
+{
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEBUTTONDOWN;
+ newEvent.button.x = args.CurrentPoint().Position().X;
+ newEvent.button.y = args.CurrentPoint().Position().Y;
+ newEvent.button.button = args.CurrentPoint().Properties().MouseWheelDelta() > 0 ? XBMC_BUTTON_WHEELUP : XBMC_BUTTON_WHEELDOWN;
+ MessagePush(&newEvent);
+ newEvent.type = XBMC_MOUSEBUTTONUP;
+ MessagePush(&newEvent);
+}
+
+void CWinEventsWin10::Kodi_KeyEvent(unsigned int vkey, unsigned scancode, unsigned keycode, bool isDown)
+{
+ using State = CoreVirtualKeyStates;
+
+ XBMC_keysym keysym = {};
+ keysym.scancode = scancode;
+ keysym.sym = KODI::WINDOWING::WINDOWS::VK_keymap[vkey];
+ keysym.unicode = keycode;
+
+ auto window = CoreWindow::GetForCurrentThread();
+
+ uint16_t mod = (uint16_t)XBMCKMOD_NONE;
+ // If left control and right alt are down this usually means that AltGr is down
+ if ((window.GetKeyState(VirtualKey::LeftControl) & State::Down) == State::Down
+ && (window.GetKeyState(VirtualKey::RightMenu) & State::Down) == State::Down)
+ {
+ mod |= XBMCKMOD_MODE;
+ mod |= XBMCKMOD_MODE;
+ }
+ else
+ {
+ if ((window.GetKeyState(VirtualKey::LeftControl) & State::Down) == State::Down)
+ mod |= XBMCKMOD_LCTRL;
+ if ((window.GetKeyState(VirtualKey::RightMenu) & State::Down) == State::Down)
+ mod |= XBMCKMOD_RALT;
+ }
+
+ // Check the remaining modifiers
+ if ((window.GetKeyState(VirtualKey::LeftShift) & State::Down) == State::Down)
+ mod |= XBMCKMOD_LSHIFT;
+ if ((window.GetKeyState(VirtualKey::RightShift) & State::Down) == State::Down)
+ mod |= XBMCKMOD_RSHIFT;
+ if ((window.GetKeyState(VirtualKey::RightControl) & State::Down) == State::Down)
+ mod |= XBMCKMOD_RCTRL;
+ if ((window.GetKeyState(VirtualKey::LeftMenu) & State::Down) == State::Down)
+ mod |= XBMCKMOD_LALT;
+ if ((window.GetKeyState(VirtualKey::LeftWindows) & State::Down) == State::Down)
+ mod |= XBMCKMOD_LSUPER;
+ if ((window.GetKeyState(VirtualKey::RightWindows) & State::Down) == State::Down)
+ mod |= XBMCKMOD_LSUPER;
+
+ keysym.mod = static_cast<XBMCMod>(mod);
+
+ XBMC_Event newEvent = {};
+ newEvent.type = isDown ? XBMC_KEYDOWN : XBMC_KEYUP;
+ newEvent.key.keysym = keysym;
+ MessagePush(&newEvent);
+}
+
+void CWinEventsWin10::OnAcceleratorKeyActivated(const CoreDispatcher&, const AcceleratorKeyEventArgs& args)
+{
+ static auto lockedState = CoreVirtualKeyStates::Locked;
+ static VirtualKey keyStore = VirtualKey::None;
+
+ // skip if device is remote control
+ if (m_remote && m_remote->IsRemoteDevice(args.DeviceId().c_str()))
+ return;
+
+ bool isDown = false;
+ unsigned keyCode = 0;
+ unsigned vk = static_cast<unsigned>(args.VirtualKey());
+
+ auto window = CoreWindow::GetForCurrentThread();
+ bool numLockLocked = ((window.GetKeyState(VirtualKey::NumberKeyLock) & lockedState) == lockedState);
+
+ switch (args.EventType())
+ {
+ case CoreAcceleratorKeyEventType::KeyDown:
+ case CoreAcceleratorKeyEventType::SystemKeyDown:
+ {
+ if ( (vk == 0x08) // VK_BACK
+ || (vk == 0x09) // VK_TAB
+ || (vk == 0x0C) // VK_CLEAR
+ || (vk == 0x0D) // VK_RETURN
+ || (vk == 0x1B) // VK_ESCAPE
+ || (vk == 0x20) // VK_SPACE
+ || (vk >= 0x30 && vk <= 0x39) // numeric keys
+ || (vk >= 0x41 && vk <= 0x5A) // alphabetic keys
+ || (vk >= 0x60 && vk <= 0x69 && numLockLocked) // keypad numeric (if numlock is on)
+ || (vk >= 0x6A && vk <= 0x6F) // keypad keys except numeric
+ || (vk >= 0x92 && vk <= 0x96) // OEM specific
+ || (vk >= 0xBA && vk <= 0xC0) // OEM specific
+ || (vk >= 0xDB && vk <= 0xDF) // OEM specific
+ || (vk >= 0xE1 && vk <= 0xF5 && vk != 0xE5 && vk != 0xE7 && vk != 0xE8) // OEM specific
+ )
+ {
+ // store this for character events, because VirtualKey is key code on character event.
+ keyStore = args.VirtualKey();
+ return;
+ }
+ isDown = true;
+ break;
+ }
+ case CoreAcceleratorKeyEventType::KeyUp:
+ case CoreAcceleratorKeyEventType::SystemKeyUp:
+ break;
+ case CoreAcceleratorKeyEventType::Character:
+ case CoreAcceleratorKeyEventType::SystemCharacter:
+ case CoreAcceleratorKeyEventType::UnicodeCharacter:
+ case CoreAcceleratorKeyEventType::DeadCharacter:
+ case CoreAcceleratorKeyEventType::SystemDeadCharacter:
+ {
+ // VirtualKey is KeyCode
+ keyCode = static_cast<unsigned>(args.VirtualKey());
+ // rewrite vk with stored value
+ vk = static_cast<unsigned>(keyStore);
+ // reset stored value
+ keyStore = VirtualKey::None;
+ isDown = true;
+ }
+ default:
+ break;
+ }
+
+ Kodi_KeyEvent(vk, args.KeyStatus().ScanCode, keyCode, isDown);
+ args.Handled(true);
+}
+
+// DisplayInformation event handlers.
+void CWinEventsWin10::OnDpiChanged(const DisplayInformation& sender, const winrt::IInspectable&)
+{
+ // Note: The value for LogicalDpi retrieved here may not match the effective DPI of the app
+ // if it is being scaled for high resolution devices. Once the DPI is set on DeviceResources,
+ // you should always retrieve it using the GetDpi method.
+ // See DeviceResources.cpp for more details.
+ //critical_section::scoped_lock lock(m_deviceResources->GetCriticalSection());
+ RECT resizeRect = { 0,0,0,0 };
+ DX::Windowing()->DPIChanged(sender.LogicalDpi(), resizeRect);
+ CGenericTouchInputHandler::GetInstance().SetScreenDPI(DX::DisplayMetrics::Dpi100);
+}
+
+void CWinEventsWin10::OnOrientationChanged(const DisplayInformation&, const winrt::IInspectable&)
+{
+ //critical_section::scoped_lock lock(m_deviceResources->GetCriticalSection());
+ //m_deviceResources->SetCurrentOrientation(sender->CurrentOrientation);
+
+ //auto size = DX::DeviceResources::Get()->GetOutputSize();
+ //UpdateWindowSize(size.Width, size.Height);
+}
+
+void CWinEventsWin10::OnDisplayContentsInvalidated(const DisplayInformation&, const winrt::IInspectable&)
+{
+ CLog::Log(LOGDEBUG, __FUNCTION__": onevent.");
+ DX::DeviceResources::Get()->ValidateDevice();
+}
+
+void CWinEventsWin10::OnBackRequested(const winrt::IInspectable&, const BackRequestedEventArgs& args)
+{
+ // handle this only on windows mobile
+ if (CSysInfo::GetWindowsDeviceFamily() == CSysInfo::WindowsDeviceFamily::Mobile)
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(ACTION_NAV_BACK)));
+ }
+ args.Handled(true);
+}
+
+void CWinEventsWin10::OnSystemMediaButtonPressed(const SystemMediaTransportControls&, const SystemMediaTransportControlsButtonPressedEventArgs& args)
+{
+ int action = ACTION_NONE;
+ switch (args.Button())
+ {
+ case SystemMediaTransportControlsButton::ChannelDown:
+ action = ACTION_CHANNEL_DOWN;
+ break;
+ case SystemMediaTransportControlsButton::ChannelUp:
+ action = ACTION_CHANNEL_UP;
+ break;
+ case SystemMediaTransportControlsButton::FastForward:
+ action = ACTION_PLAYER_FORWARD;
+ break;
+ case SystemMediaTransportControlsButton::Rewind:
+ action = ACTION_PLAYER_REWIND;
+ break;
+ case SystemMediaTransportControlsButton::Next:
+ action = ACTION_NEXT_ITEM;
+ break;
+ case SystemMediaTransportControlsButton::Previous:
+ action = ACTION_PREV_ITEM;
+ break;
+ case SystemMediaTransportControlsButton::Pause:
+ case SystemMediaTransportControlsButton::Play:
+ action = ACTION_PLAYER_PLAYPAUSE;
+ break;
+ case SystemMediaTransportControlsButton::Stop:
+ action = ACTION_STOP;
+ break;
+ case SystemMediaTransportControlsButton::Record:
+ action = ACTION_RECORD;
+ break;
+ default:
+ break;
+ }
+ if (action != ACTION_NONE)
+ {
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(action)));
+ }
+}
+
+void CWinEventsWin10::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ if (flag & ANNOUNCEMENT::Player)
+ {
+ double speed = 1.0;
+ if (data.isMember("player") && data["player"].isMember("speed"))
+ speed = data["player"]["speed"].asDouble(1.0);
+
+ bool changed = false;
+ MediaPlaybackStatus status = MediaPlaybackStatus::Changing;
+
+ if (message == "OnPlay" || message == "OnResume")
+ {
+ changed = true;
+ status = MediaPlaybackStatus::Playing;
+ }
+ else if (message == "OnStop")
+ {
+ changed = true;
+ status = MediaPlaybackStatus::Stopped;
+ }
+ else if (message == "OnPause")
+ {
+ changed = true;
+ status = MediaPlaybackStatus::Paused;
+ }
+ else if (message == "OnSpeedChanged")
+ {
+ changed = true;
+ status = speed != 0.0 ? MediaPlaybackStatus::Playing : MediaPlaybackStatus::Paused;
+ }
+
+ if (changed)
+ {
+ try
+ {
+ auto dispatcher = CoreApplication::MainView().Dispatcher();
+ if (dispatcher)
+ {
+ dispatcher.RunAsync(CoreDispatcherPriority::Normal, DispatchedHandler([status, speed]
+ {
+ auto smtc = SystemMediaTransportControls::GetForCurrentView();
+ if (!smtc)
+ return;
+
+ smtc.PlaybackStatus(status);
+ smtc.PlaybackRate(speed);
+ }));
+ }
+ }
+ catch (const winrt::hresult_error&)
+ {
+ }
+ }
+ }
+}
diff --git a/xbmc/windowing/win10/WinEventsWin10.h b/xbmc/windowing/win10/WinEventsWin10.h
new file mode 100644
index 0000000..c4bd345
--- /dev/null
+++ b/xbmc/windowing/win10/WinEventsWin10.h
@@ -0,0 +1,83 @@
+/*
+ * 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 "interfaces/IAnnouncer.h"
+#include "windowing/WinEvents.h"
+
+#include <concurrent_queue.h>
+#include <winrt/Windows.Media.h>
+
+class CRemoteControlXbox;
+
+class CWinEventsWin10 : public IWinEvents
+ , public ANNOUNCEMENT::IAnnouncer
+{
+public:
+ CWinEventsWin10();
+ virtual ~CWinEventsWin10();
+
+ void MessagePush(XBMC_Event *newEvent);
+ bool MessagePump() override;
+ virtual size_t GetQueueSize();
+
+ // initialization
+ void InitEventHandlers(const winrt::Windows::UI::Core::CoreWindow&);
+ static void InitOSKeymap(void);
+
+ // Window event handlers.
+ void OnWindowSizeChanged(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::WindowSizeChangedEventArgs&);
+ void OnWindowResizeStarted(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::Foundation::IInspectable&);
+ void OnWindowResizeCompleted(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::Foundation::IInspectable&);
+ void OnWindowClosed(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::CoreWindowEventArgs&);
+ static void OnWindowActivationChanged(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::WindowActivatedEventArgs&);
+ static void OnVisibilityChanged(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::VisibilityChangedEventArgs&);
+ // touch mouse and pen
+ void OnPointerPressed(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::PointerEventArgs&);
+ void OnPointerMoved(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::PointerEventArgs&);
+ void OnPointerReleased(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::PointerEventArgs&);
+ void OnPointerExited(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::PointerEventArgs&);
+ void OnPointerWheelChanged(const winrt::Windows::UI::Core::CoreWindow&, const winrt::Windows::UI::Core::PointerEventArgs&);
+ // keyboard
+ void OnAcceleratorKeyActivated(const winrt::Windows::UI::Core::CoreDispatcher&, const winrt::Windows::UI::Core::AcceleratorKeyEventArgs&);
+
+ // DisplayInformation event handlers.
+ static void OnDpiChanged(const winrt::Windows::Graphics::Display::DisplayInformation&, const winrt::Windows::Foundation::IInspectable&);
+ static void OnOrientationChanged(const winrt::Windows::Graphics::Display::DisplayInformation&, const winrt::Windows::Foundation::IInspectable&);
+ static void OnDisplayContentsInvalidated(const winrt::Windows::Graphics::Display::DisplayInformation&, const winrt::Windows::Foundation::IInspectable&);
+ // system
+ static void OnBackRequested(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::UI::Core::BackRequestedEventArgs&);
+ // system media handlers
+ static void OnSystemMediaButtonPressed(const winrt::Windows::Media::SystemMediaTransportControls&
+ , const winrt::Windows::Media::SystemMediaTransportControlsButtonPressedEventArgs&);
+ // IAnnouncer overrides
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+private:
+ friend class CWinSystemWin10;
+
+ void OnResize(float width, float height);
+ void UpdateWindowSize();
+ void Kodi_KeyEvent(unsigned int vkey, unsigned scancode, unsigned keycode, bool isDown);
+ void HandleWindowSizeChanged();
+
+ Concurrency::concurrent_queue<XBMC_Event> m_events;
+ winrt::Windows::Media::SystemMediaTransportControls m_smtc{ nullptr };
+ bool m_bResized{ false };
+ bool m_bMoved{ false };
+ bool m_sizeChanging{ false };
+ float m_logicalWidth{ 0 };
+ float m_logicalHeight{ 0 };
+ float m_logicalPosX{ 0 };
+ float m_logicalPosY{ 0 };
+ std::unique_ptr<CRemoteControlXbox> m_remote;
+};
diff --git a/xbmc/windowing/win10/WinSystemWin10.cpp b/xbmc/windowing/win10/WinSystemWin10.cpp
new file mode 100644
index 0000000..6de3f78
--- /dev/null
+++ b/xbmc/windowing/win10/WinSystemWin10.cpp
@@ -0,0 +1,660 @@
+/*
+ * 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 "WinSystemWin10.h"
+
+#include "ServiceBroker.h"
+#include "WinEventsWin10.h"
+#include "application/Application.h"
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Sinks/AESinkWASAPI.h"
+#include "cores/AudioEngine/Sinks/AESinkXAudio.h"
+#include "rendering/dx/DirectXHelper.h"
+#include "rendering/dx/RenderContext.h"
+#include "rendering/dx/ScreenshotSurfaceWindows.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SystemInfo.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/windows/VideoSyncD3D.h"
+
+#include "platform/win10/AsyncHelpers.h"
+#include "platform/win32/CharsetConverter.h"
+
+#include <mutex>
+
+#pragma pack(push,8)
+
+#include <tpcshrd.h>
+#include <ppltasks.h>
+#include <winrt/Windows.ApplicationModel.DataTransfer.h>
+#include <winrt/Windows.Foundation.Metadata.h>
+#include <winrt/Windows.Graphics.Display.h>
+#include <winrt/Windows.Graphics.Display.Core.h>
+
+using namespace winrt::Windows::ApplicationModel::DataTransfer;
+using namespace winrt::Windows::Foundation::Metadata;
+using namespace winrt::Windows::Graphics::Display;
+using namespace winrt::Windows::Graphics::Display::Core;
+using namespace winrt::Windows::UI::Core;
+using namespace winrt::Windows::UI::ViewManagement;
+
+using namespace std::chrono_literals;
+
+CWinSystemWin10::CWinSystemWin10()
+ : CWinSystemBase()
+ , m_ValidWindowedPosition(false)
+ , m_IsAlteringWindow(false)
+ , m_delayDispReset(false)
+ , m_state(WINDOW_STATE_WINDOWED)
+ , m_fullscreenState(WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW)
+ , m_windowState(WINDOW_WINDOW_STATE_WINDOWED)
+ , m_inFocus(false)
+ , m_bMinimized(false)
+{
+ m_winEvents.reset(new CWinEventsWin10());
+
+ AE::CAESinkFactory::ClearSinks();
+ CAESinkXAudio::Register();
+ CAESinkWASAPI::Register();
+ CScreenshotSurfaceWindows::Register();
+}
+
+CWinSystemWin10::~CWinSystemWin10()
+{
+};
+
+bool CWinSystemWin10::InitWindowSystem()
+{
+ m_coreWindow = CoreWindow::GetForCurrentThread();
+ dynamic_cast<CWinEventsWin10&>(*m_winEvents).InitEventHandlers(m_coreWindow);
+
+ if (!CWinSystemBase::InitWindowSystem())
+ return false;
+
+ if (m_displays.empty())
+ {
+ CLog::Log(LOGERROR, "{} - no suitable monitor found, aborting...", __FUNCTION__);
+ return false;
+ }
+
+ return true;
+}
+
+bool CWinSystemWin10::DestroyWindowSystem()
+{
+ m_bWindowCreated = false;
+ RestoreDesktopResolution();
+ return true;
+}
+
+bool CWinSystemWin10::CanDoWindowed()
+{
+ return CSysInfo::GetWindowsDeviceFamily() == CSysInfo::Desktop;
+}
+
+bool CWinSystemWin10::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res)
+{
+ UpdateStates(fullScreen);
+ // initialize the state
+ WINDOW_STATE state = GetState(fullScreen);
+
+ m_nWidth = res.iWidth;
+ m_nHeight = res.iHeight;
+ m_bFullScreen = fullScreen;
+ m_fRefreshRate = res.fRefreshRate;
+ m_inFocus = true;
+ m_bWindowCreated = true;
+ m_state = state;
+
+ m_coreWindow.Activate();
+
+ AdjustWindow();
+ // dispatch all events currently pending in the queue to show window's content
+ // and hide UWP splash, without this the Kodi's splash will not be shown
+ m_coreWindow.Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
+
+ return true;
+}
+
+bool CWinSystemWin10::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ m_nWidth = newWidth;
+ m_nHeight = newHeight;
+
+ if (newLeft > 0)
+ m_nLeft = newLeft;
+
+ if (newTop > 0)
+ m_nTop = newTop;
+
+ AdjustWindow();
+
+ return true;
+}
+
+void CWinSystemWin10::FinishWindowResize(int newWidth, int newHeight)
+{
+ m_nWidth = newWidth;
+ m_nHeight = newHeight;
+
+ float dpi = DX::DeviceResources::Get()->GetDpi();
+ int dipsWidth = round(DX::ConvertPixelsToDips(m_nWidth, dpi));
+ int dipsHeight = round(DX::ConvertPixelsToDips(m_nHeight, dpi));
+
+ ApplicationView::PreferredLaunchViewSize(winrt::Windows::Foundation::Size(dipsWidth, dipsHeight));
+ ApplicationView::PreferredLaunchWindowingMode(ApplicationViewWindowingMode::PreferredLaunchViewSize);
+}
+
+void CWinSystemWin10::AdjustWindow()
+{
+ CLog::Log(LOGDEBUG, __FUNCTION__": adjusting window if required.");
+
+ auto appView = ApplicationView::GetForCurrentView();
+ bool isInFullscreen = appView.IsFullScreenMode();
+
+ if (m_state == WINDOW_STATE_FULLSCREEN_WINDOW || m_state == WINDOW_STATE_FULLSCREEN)
+ {
+ if (!isInFullscreen)
+ {
+ if (appView.TryEnterFullScreenMode())
+ ApplicationView::PreferredLaunchWindowingMode(ApplicationViewWindowingMode::FullScreen);
+ }
+ }
+ else // m_state == WINDOW_STATE_WINDOWED
+ {
+ if (isInFullscreen)
+ {
+ appView.ExitFullScreenMode();
+ }
+
+ int viewWidth = appView.VisibleBounds().Width;
+ int viewHeight = appView.VisibleBounds().Height;
+
+ float dpi = DX::DeviceResources::Get()->GetDpi();
+ int dipsWidth = round(DX::ConvertPixelsToDips(m_nWidth, dpi));
+ int dipsHeight = round(DX::ConvertPixelsToDips(m_nHeight, dpi));
+
+ if (viewHeight != dipsHeight || viewWidth != dipsWidth)
+ {
+ if (!appView.TryResizeView(winrt::Windows::Foundation::Size(dipsWidth, dipsHeight)))
+ {
+ CLog::LogF(LOGDEBUG, __FUNCTION__, "resizing ApplicationView failed.");
+ }
+ }
+
+ ApplicationView::PreferredLaunchViewSize(winrt::Windows::Foundation::Size(dipsWidth, dipsHeight));
+ ApplicationView::PreferredLaunchWindowingMode(ApplicationViewWindowingMode::PreferredLaunchViewSize);
+ }
+}
+
+bool CWinSystemWin10::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ CWinSystemWin10::UpdateStates(fullScreen);
+ WINDOW_STATE state = GetState(fullScreen);
+
+ CLog::Log(LOGDEBUG, "{} ({}) with size {}x{}, refresh {:f}{}", __FUNCTION__,
+ window_state_names[state], res.iWidth, res.iHeight, res.fRefreshRate,
+ (res.dwFlags & D3DPRESENTFLAG_INTERLACED) ? "i" : "");
+
+ bool forceChange = false; // resolution/display is changed but window state isn't changed
+ bool stereoChange = IsStereoEnabled() != (CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_HARDWAREBASED);
+
+ if ( m_nWidth != res.iWidth || m_nHeight != res.iHeight || m_fRefreshRate != res.fRefreshRate ||
+ stereoChange || m_bFirstResChange)
+ {
+ forceChange = true;
+ }
+
+ if (state == m_state && !forceChange)
+ return true;
+
+ // entering to stereo mode, limit resolution to 1080p@23.976
+ if (stereoChange && !IsStereoEnabled() && res.iWidth > 1280)
+ {
+ res = CDisplaySettings::GetInstance().GetResolutionInfo(CResolutionUtils::ChooseBestResolution(24.f / 1.001f, 1920, 1080, true));
+ }
+
+ if (m_state == WINDOW_STATE_WINDOWED)
+ {
+ if (m_coreWindow)
+ {
+ m_nLeft = m_coreWindow.Bounds().X;
+ m_nTop = m_coreWindow.Bounds().Y;
+ m_ValidWindowedPosition = true;
+ }
+ }
+
+ m_IsAlteringWindow = true;
+ ReleaseBackBuffer();
+
+ m_bFirstResChange = false;
+ m_bFullScreen = fullScreen;
+ m_nWidth = res.iWidth;
+ m_nHeight = res.iHeight;
+ m_bBlankOtherDisplay = blankOtherDisplays;
+ m_fRefreshRate = res.fRefreshRate;
+
+ if (state == WINDOW_STATE_FULLSCREEN)
+ {
+ // isn't allowed in UWP
+ }
+ else if (m_state == WINDOW_STATE_FULLSCREEN || m_state == WINDOW_STATE_FULLSCREEN_WINDOW) // we're in fullscreen state now
+ {
+ if (state == WINDOW_STATE_WINDOWED) // go to a windowed state
+ {
+ // need to restore resolution if it was changed to not native
+ // because we do not support resolution change in windowed mode
+ RestoreDesktopResolution();
+ }
+ else if (state == WINDOW_STATE_FULLSCREEN_WINDOW) // enter fullscreen window instead
+ {
+ ChangeResolution(res, stereoChange);
+ }
+
+ m_state = state;
+ AdjustWindow();
+ }
+ else // we're in windowed state now
+ {
+ if (state == WINDOW_STATE_FULLSCREEN_WINDOW)
+ {
+ ChangeResolution(res, stereoChange);
+
+ m_state = state;
+ AdjustWindow();
+ }
+ }
+
+ CreateBackBuffer();
+ m_IsAlteringWindow = false;
+ return true;
+}
+
+bool CWinSystemWin10::DPIChanged(WORD dpi, RECT windowRect) const
+{
+ (void)dpi;
+ return true;
+}
+
+void CWinSystemWin10::RestoreDesktopResolution()
+{
+ CLog::Log(LOGDEBUG, __FUNCTION__": restoring default desktop resolution");
+ ChangeResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP));
+}
+
+const MONITOR_DETAILS* CWinSystemWin10::GetDefaultMonitor() const
+{
+ if (m_displays.empty())
+ return nullptr;
+
+ return &m_displays.front();
+}
+
+bool CWinSystemWin10::ChangeResolution(const RESOLUTION_INFO& res, bool forceChange /*= false*/)
+{
+ const MONITOR_DETAILS* details = GetDefaultMonitor();
+
+ if (!details)
+ return false;
+
+ if (ApiInformation::IsTypePresent(L"Windows.Graphics.Display.Core.HdmiDisplayInformation"))
+ {
+ bool changed = false;
+ auto hdmiInfo = HdmiDisplayInformation::GetForCurrentView();
+ if (hdmiInfo != nullptr)
+ {
+ // default mode not in list of supported display modes
+ if (res.iScreenWidth == details->ScreenWidth && res.iScreenHeight == details->ScreenHeight
+ && fabs(res.fRefreshRate - details->RefreshRate) <= 0.00001)
+ {
+ Wait(hdmiInfo.SetDefaultDisplayModeAsync());
+ changed = true;
+ }
+ else
+ {
+ bool needStereo = CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_HARDWAREBASED;
+ auto hdmiModes = hdmiInfo.GetSupportedDisplayModes();
+
+ HdmiDisplayMode selected = nullptr;
+ for (const auto& mode : hdmiModes)
+ {
+ if (res.iScreenWidth == mode.ResolutionWidthInRawPixels() && res.iScreenHeight == mode.ResolutionHeightInRawPixels()
+ && fabs(res.fRefreshRate - mode.RefreshRate()) <= 0.00001)
+ {
+ selected = mode;
+ if (needStereo == mode.StereoEnabled())
+ break;
+ }
+ }
+
+ if (selected != nullptr)
+ {
+ changed = Wait(hdmiInfo.RequestSetCurrentDisplayModeAsync(selected));
+ }
+ }
+ }
+
+ // changing display mode doesn't fire CoreWindow::SizeChanged event
+ if (changed && m_bWindowCreated)
+ {
+ // dispatch all events currently pending in the queue to change window's content
+ m_coreWindow.Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
+
+ float dpi = DisplayInformation::GetForCurrentView().LogicalDpi();
+ float dipsW = DX::ConvertPixelsToDips(m_nWidth, dpi);
+ float dipsH = DX::ConvertPixelsToDips(m_nHeight, dpi);
+
+ dynamic_cast<CWinEventsWin10&>(*m_winEvents).OnResize(dipsW, dipsH);
+ }
+ return changed;
+ }
+
+ CLog::LogF(LOGDEBUG, "Not supported.");
+ return false;
+}
+
+void CWinSystemWin10::UpdateResolutions()
+{
+ m_displays.clear();
+
+ CWinSystemBase::UpdateResolutions();
+ GetConnectedDisplays(m_displays);
+
+ const MONITOR_DETAILS* details = GetDefaultMonitor();
+ if (!details)
+ return;
+
+ float refreshRate;
+ int w = details->ScreenWidth;
+ int h = details->ScreenHeight;
+ uint32_t dwFlags = details->Interlaced ? D3DPRESENTFLAG_INTERLACED : 0;;
+
+ if (details->RefreshRate == 59 || details->RefreshRate == 29 || details->RefreshRate == 23)
+ refreshRate = static_cast<float>(details->RefreshRate + 1) / 1.001f;
+ else
+ refreshRate = static_cast<float>(details->RefreshRate);
+
+ RESOLUTION_INFO& primary_info = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP);
+ UpdateDesktopResolution(primary_info, "Default", w, h, refreshRate, dwFlags);
+ CLog::Log(LOGINFO, "Primary mode: {}", primary_info.strMode);
+
+ // erase previous stored modes
+ CDisplaySettings::GetInstance().ClearCustomResolutions();
+
+ if (ApiInformation::IsTypePresent(L"Windows.Graphics.Display.Core.HdmiDisplayInformation"))
+ {
+ auto hdmiInfo = HdmiDisplayInformation::GetForCurrentView();
+ if (hdmiInfo != nullptr)
+ {
+ auto hdmiModes = hdmiInfo.GetSupportedDisplayModes();
+ for (const auto& mode : hdmiModes)
+ {
+ RESOLUTION_INFO res;
+ res.iWidth = mode.ResolutionWidthInRawPixels();
+ res.iHeight = mode.ResolutionHeightInRawPixels();
+ res.bFullScreen = true;
+ res.dwFlags = 0;
+ res.fRefreshRate = mode.RefreshRate();
+ res.fPixelRatio = 1.0f;
+ res.iScreenWidth = res.iWidth;
+ res.iScreenHeight = res.iHeight;
+ res.iSubtitles = res.iHeight;
+ res.strMode = StringUtils::Format("Default: {}x{} @ {:.2f}Hz", res.iWidth, res.iHeight,
+ res.fRefreshRate);
+ GetGfxContext().ResetOverscan(res);
+
+ if (AddResolution(res))
+ CLog::Log(LOGINFO, "Additional mode: {} {}", res.strMode,
+ mode.Is2086MetadataSupported() ? "(HDR)" : "");
+ }
+ }
+ }
+
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+}
+
+bool CWinSystemWin10::AddResolution(const RESOLUTION_INFO &res)
+{
+ for (unsigned int i = RES_CUSTOM; i < CDisplaySettings::GetInstance().ResolutionInfoSize(); i++)
+ {
+ RESOLUTION_INFO& info = CDisplaySettings::GetInstance().GetResolutionInfo(i);
+ if ( info.iWidth == res.iWidth
+ && info.iHeight == res.iHeight
+ && info.iScreenWidth == res.iScreenWidth
+ && info.iScreenHeight == res.iScreenHeight
+ && info.fRefreshRate == res.fRefreshRate
+ && info.dwFlags == res.dwFlags)
+ return false; // already have this resolution
+ }
+
+ CDisplaySettings::GetInstance().AddResolutionInfo(res);
+ return true;
+}
+
+void CWinSystemWin10::GetConnectedDisplays(std::vector<MONITOR_DETAILS>& outputs)
+{
+ auto dispatcher = m_coreWindow.Dispatcher();
+ DispatchedHandler handler([&]()
+ {
+ MONITOR_DETAILS md = {};
+
+ auto displayInfo = DisplayInformation::GetForCurrentView();
+ bool flipResolution = false;
+ switch (displayInfo.NativeOrientation())
+ {
+ case DisplayOrientations::Landscape:
+ switch (displayInfo.CurrentOrientation())
+ {
+ case DisplayOrientations::Portrait:
+ case DisplayOrientations::PortraitFlipped:
+ flipResolution = true;
+ break;
+ }
+ break;
+ case DisplayOrientations::Portrait:
+ switch (displayInfo.CurrentOrientation())
+ {
+ case DisplayOrientations::Landscape:
+ case DisplayOrientations::LandscapeFlipped:
+ flipResolution = true;
+ break;
+ }
+ break;
+ }
+ md.ScreenWidth = flipResolution ? displayInfo.ScreenHeightInRawPixels() : displayInfo.ScreenWidthInRawPixels();
+ md.ScreenHeight = flipResolution ? displayInfo.ScreenWidthInRawPixels() : displayInfo.ScreenHeightInRawPixels();
+
+ if (ApiInformation::IsTypePresent(L"Windows.Graphics.Display.Core.HdmiDisplayInformation"))
+ {
+ auto hdmiInfo = HdmiDisplayInformation::GetForCurrentView();
+ if (hdmiInfo != nullptr)
+ {
+ auto currentMode = hdmiInfo.GetCurrentDisplayMode();
+ // On Xbox, 4K resolutions only are reported by HdmiDisplayInformation API
+ // so ScreenHeight & ScreenWidth are updated with info provided here
+ md.ScreenHeight = currentMode.ResolutionHeightInRawPixels();
+ md.ScreenWidth = currentMode.ResolutionWidthInRawPixels();
+ md.RefreshRate = currentMode.RefreshRate();
+ md.Bpp = currentMode.BitsPerPixel();
+ }
+ else
+ {
+ md.RefreshRate = 60.0;
+ md.Bpp = 24;
+ }
+ }
+ else
+ {
+ // note that refresh rate information is not available on Win10 UWP
+ md.RefreshRate = 60.0;
+ md.Bpp = 24;
+ }
+ md.Interlaced = false;
+
+ outputs.push_back(md);
+ });
+
+ if (dispatcher.HasThreadAccess())
+ handler();
+ else
+ Wait(dispatcher.RunAsync(CoreDispatcherPriority::High, handler));
+}
+
+void CWinSystemWin10::ShowOSMouse(bool show)
+{
+ if (!m_coreWindow)
+ return;
+
+ DispatchedHandler handler([this, show]()
+ {
+ CoreCursor cursor = nullptr;
+ if (show)
+ cursor = CoreCursor(CoreCursorType::Arrow, 1);
+ m_coreWindow.PointerCursor(cursor);
+ });
+
+ if (m_coreWindow.Dispatcher().HasThreadAccess())
+ handler();
+ else
+ m_coreWindow.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, handler);
+}
+
+bool CWinSystemWin10::Minimize()
+{
+ CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__);
+ return true;
+}
+bool CWinSystemWin10::Restore()
+{
+ CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__);
+ return true;
+}
+bool CWinSystemWin10::Hide()
+{
+ CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__);
+ return true;
+}
+bool CWinSystemWin10::Show(bool raise)
+{
+ CLog::Log(LOGDEBUG, "{} is not implemented", __FUNCTION__);
+ return true;
+}
+
+void CWinSystemWin10::Register(IDispResource *resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ m_resources.push_back(resource);
+}
+
+void CWinSystemWin10::Unregister(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> 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 CWinSystemWin10::OnDisplayLost()
+{
+ CLog::Log(LOGDEBUG, "{} - notify display lost event", __FUNCTION__);
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnLostDisplay();
+ }
+}
+
+void CWinSystemWin10::OnDisplayReset()
+{
+ if (!m_delayDispReset)
+ {
+ CLog::Log(LOGDEBUG, "{} - notify display reset event", __FUNCTION__);
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnResetDisplay();
+ }
+}
+
+void CWinSystemWin10::OnDisplayBack()
+{
+ auto delay =
+ std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ "videoscreen.delayrefreshchange") *
+ 100);
+ if (delay > 0ms)
+ {
+ m_delayDispReset = true;
+ m_dispResetTimer.Set(delay);
+ }
+ OnDisplayReset();
+}
+
+void CWinSystemWin10::ResolutionChanged()
+{
+ OnDisplayLost();
+ OnDisplayBack();
+}
+
+std::unique_ptr<CVideoSync> CWinSystemWin10::GetVideoSync(void *clock)
+{
+ std::unique_ptr<CVideoSync> pVSync(new CVideoSyncD3D(clock));
+ return pVSync;
+}
+
+std::string CWinSystemWin10::GetClipboardText()
+{
+ std::wstring unicode_text;
+
+ auto contentView = Clipboard::GetContent();
+ if (contentView.Contains(StandardDataFormats::Text()))
+ {
+ auto text = Wait(contentView.GetTextAsync());
+ unicode_text.append(text.c_str());
+ }
+
+ return KODI::PLATFORM::WINDOWS::FromW(unicode_text);
+}
+
+bool CWinSystemWin10::UseLimitedColor()
+{
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE);
+}
+
+void CWinSystemWin10::NotifyAppFocusChange(bool bGaining)
+{
+ m_inFocus = bGaining;
+}
+
+void CWinSystemWin10::UpdateStates(bool fullScreen)
+{
+ m_fullscreenState = WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW; // currently only this allowed
+ m_windowState = WINDOW_WINDOW_STATE_WINDOWED; // currently only this allowed
+}
+
+WINDOW_STATE CWinSystemWin10::GetState(bool fullScreen) const
+{
+ return static_cast<WINDOW_STATE>(fullScreen ? m_fullscreenState : m_windowState);
+}
+
+bool CWinSystemWin10::MessagePump()
+{
+ return m_winEvents->MessagePump();
+}
+
+int CWinSystemWin10::GetGuiSdrPeakLuminance() const
+{
+ const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ return settings->GetInt(CSettings::SETTING_VIDEOSCREEN_GUISDRPEAKLUMINANCE);
+}
+
+#pragma pack(pop)
diff --git a/xbmc/windowing/win10/WinSystemWin10.h b/xbmc/windowing/win10/WinSystemWin10.h
new file mode 100644
index 0000000..0f2b41e
--- /dev/null
+++ b/xbmc/windowing/win10/WinSystemWin10.h
@@ -0,0 +1,156 @@
+/*
+ * 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 "guilib/DispResource.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "windowing/WinSystem.h"
+
+#include <vector>
+
+#pragma pack(push)
+#pragma pack(8)
+
+/* Controls the way the window appears and behaves. */
+enum WINDOW_STATE
+{
+ WINDOW_STATE_FULLSCREEN = 1, // Exclusive fullscreen
+ WINDOW_STATE_FULLSCREEN_WINDOW, // Non-exclusive fullscreen window
+ WINDOW_STATE_WINDOWED, //Movable window with border
+ WINDOW_STATE_BORDERLESS //Non-movable window with no border
+};
+
+static const char* window_state_names[] =
+{
+ "unknown",
+ "true fullscreen",
+ "windowed fullscreen",
+ "windowed",
+ "borderless"
+};
+
+/* WINDOW_STATE restricted to fullscreen modes. */
+enum WINDOW_FULLSCREEN_STATE
+{
+ WINDOW_FULLSCREEN_STATE_FULLSCREEN = WINDOW_STATE_FULLSCREEN,
+ WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW = WINDOW_STATE_FULLSCREEN_WINDOW
+};
+
+/* WINDOW_STATE restricted to windowed modes. */
+enum WINDOW_WINDOW_STATE
+{
+ WINDOW_WINDOW_STATE_WINDOWED = WINDOW_STATE_WINDOWED,
+ WINDOW_WINDOW_STATE_BORDERLESS = WINDOW_STATE_BORDERLESS
+};
+
+struct MONITOR_DETAILS
+{
+ // Windows desktop info
+ int ScreenWidth;
+ int ScreenHeight;
+ float RefreshRate;
+ int Bpp;
+ bool Interlaced;
+};
+
+class CWinSystemWin10 : public CWinSystemBase
+{
+public:
+ CWinSystemWin10();
+ virtual ~CWinSystemWin10();
+
+ // CWinSystemBase overrides
+ bool InitWindowSystem() override;
+ bool DestroyWindowSystem() override;
+ bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override;
+ void FinishWindowResize(int newWidth, int newHeight) override;
+ void UpdateResolutions() override;
+ void NotifyAppFocusChange(bool bGaining) override;
+ void ShowOSMouse(bool show) override;
+ bool HasInertialGestures() override { return true; }//if win32 has touchscreen - it uses the win32 gesture api for inertial scrolling
+ bool Minimize() override;
+ bool Restore() override;
+ bool Hide() override;
+ bool Show(bool raise = true) override;
+ std::string GetClipboardText() override;
+ bool UseLimitedColor() override;
+
+ // videosync
+ std::unique_ptr<CVideoSync> GetVideoSync(void *clock) override;
+
+ bool WindowedMode() const { return m_state != WINDOW_STATE_FULLSCREEN; }
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+
+ // CWinSystemWin10
+ bool IsAlteringWindow() const { return m_IsAlteringWindow; }
+ void SetAlteringWindow(bool altering) { m_IsAlteringWindow = altering; }
+ bool IsTogglingHDR() const { return false; }
+ void SetTogglingHDR(bool toggling) {}
+ virtual bool DPIChanged(WORD dpi, RECT windowRect) const;
+ bool IsMinimized() const { return m_bMinimized; }
+ void SetMinimized(bool minimized) { m_bMinimized = minimized; }
+ int GetGuiSdrPeakLuminance() const;
+
+ bool CanDoWindowed() override;
+
+ // winevents override
+ bool MessagePump() override;
+
+protected:
+ bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override = 0;
+ virtual void UpdateStates(bool fullScreen);
+ WINDOW_STATE GetState(bool fullScreen) const;
+ virtual void SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res) = 0;
+ virtual void ReleaseBackBuffer() = 0;
+ virtual void CreateBackBuffer() = 0;
+ virtual void ResizeDeviceBuffers() = 0;
+ virtual bool IsStereoEnabled() = 0;
+ virtual void AdjustWindow();
+
+ virtual void Register(IDispResource *resource);
+ virtual void Unregister(IDispResource *resource);
+
+ bool ChangeResolution(const RESOLUTION_INFO& res, bool forceChange = false);
+ const MONITOR_DETAILS* GetDefaultMonitor() const;
+ void RestoreDesktopResolution();
+ void GetConnectedDisplays(std::vector<MONITOR_DETAILS>& outputs);
+
+ /*!
+ \brief Adds a resolution to the list of resolutions if we don't already have it
+ \param res resolution to add.
+ */
+ static bool AddResolution(const RESOLUTION_INFO &res);
+
+ void OnDisplayLost();
+ void OnDisplayReset();
+ void OnDisplayBack();
+ void ResolutionChanged();
+
+ std::vector<MONITOR_DETAILS> m_displays;
+ bool m_ValidWindowedPosition;
+ bool m_IsAlteringWindow;
+
+ CCriticalSection m_resourceSection;
+ std::vector<IDispResource*> m_resources;
+ bool m_delayDispReset;
+ XbmcThreads::EndTime<> m_dispResetTimer;
+
+ WINDOW_STATE m_state; // the state of the window
+ WINDOW_FULLSCREEN_STATE m_fullscreenState; // the state of the window when in fullscreen
+ WINDOW_WINDOW_STATE m_windowState; // the state of the window when in windowed
+ bool m_inFocus;
+ bool m_bMinimized;
+ bool m_bFirstResChange = true;
+
+ winrt::Windows::UI::Core::CoreWindow m_coreWindow = nullptr;
+};
+
+#pragma pack(pop)
+
diff --git a/xbmc/windowing/win10/WinSystemWin10DX.cpp b/xbmc/windowing/win10/WinSystemWin10DX.cpp
new file mode 100644
index 0000000..c3d2fca
--- /dev/null
+++ b/xbmc/windowing/win10/WinSystemWin10DX.cpp
@@ -0,0 +1,205 @@
+/*
+ * 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 "WinSystemWin10DX.h"
+
+#include "input/touch/generic/GenericTouchActionHandler.h"
+#include "input/touch/generic/GenericTouchInputHandler.h"
+#include "rendering/dx/DirectXHelper.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/WindowSystemFactory.h"
+
+#include "platform/win32/WIN32Util.h"
+
+using namespace std::chrono_literals;
+
+void CWinSystemWin10DX::Register()
+{
+ KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem);
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemWin10DX::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemWin10DX>();
+}
+
+CWinSystemWin10DX::CWinSystemWin10DX() : CRenderSystemDX()
+{
+}
+
+CWinSystemWin10DX::~CWinSystemWin10DX()
+{
+}
+
+void CWinSystemWin10DX::PresentRenderImpl(bool rendered)
+{
+ if (rendered)
+ m_deviceResources->Present();
+
+ if (m_delayDispReset && m_dispResetTimer.IsTimePast())
+ {
+ m_delayDispReset = false;
+ OnDisplayReset();
+ }
+
+ if (!rendered)
+ KODI::TIME::Sleep(40ms);
+}
+
+bool CWinSystemWin10DX::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res)
+{
+ const MONITOR_DETAILS* monitor = GetDefaultMonitor();
+ if (!monitor)
+ return false;
+
+ m_deviceResources = DX::DeviceResources::Get();
+ m_deviceResources->SetWindow(m_coreWindow);
+
+ if (CWinSystemWin10::CreateNewWindow(name, fullScreen, res) && m_deviceResources->HasValidDevice())
+ {
+ CGenericTouchInputHandler::GetInstance().RegisterHandler(&CGenericTouchActionHandler::GetInstance());
+ CGenericTouchInputHandler::GetInstance().SetScreenDPI(m_deviceResources->GetDpi());
+ return true;
+ }
+ return false;
+}
+
+bool CWinSystemWin10DX::DestroyRenderSystem()
+{
+ CRenderSystemDX::DestroyRenderSystem();
+
+ m_deviceResources->Release();
+ m_deviceResources.reset();
+ return true;
+}
+
+void CWinSystemWin10DX::ShowSplash(const std::string & message)
+{
+ CRenderSystemBase::ShowSplash(message);
+
+ // this will prevent killing the app by watchdog timeout during loading
+ if (m_coreWindow != nullptr)
+ m_coreWindow.Dispatcher().ProcessEvents(winrt::Windows::UI::Core::CoreProcessEventsOption::ProcessAllIfPresent);
+}
+
+void CWinSystemWin10DX::SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res)
+{
+ m_deviceResources->SetFullScreen(fullScreen, res);
+}
+
+bool CWinSystemWin10DX::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ CWinSystemWin10::ResizeWindow(newWidth, newHeight, newLeft, newTop);
+ CRenderSystemDX::OnResize();
+
+ return true;
+}
+
+void CWinSystemWin10DX::OnMove(int x, int y)
+{
+ m_deviceResources->SetWindowPos(m_coreWindow.Bounds());
+}
+
+bool CWinSystemWin10DX::DPIChanged(WORD dpi, RECT windowRect) const
+{
+ m_deviceResources->SetDpi(dpi);
+ if (!IsAlteringWindow())
+ return CWinSystemWin10::DPIChanged(dpi, windowRect);
+
+ return true;
+}
+
+void CWinSystemWin10DX::ReleaseBackBuffer()
+{
+ m_deviceResources->ReleaseBackBuffer();
+}
+
+void CWinSystemWin10DX::CreateBackBuffer()
+{
+ m_deviceResources->CreateBackBuffer();
+}
+
+void CWinSystemWin10DX::ResizeDeviceBuffers()
+{
+ m_deviceResources->ResizeBuffers();
+}
+
+bool CWinSystemWin10DX::IsStereoEnabled()
+{
+ return m_deviceResources->IsStereoEnabled();
+}
+
+void CWinSystemWin10DX::OnResize(int width, int height)
+{
+ if (!m_deviceResources)
+ return;
+
+ if (!m_IsAlteringWindow)
+ ReleaseBackBuffer();
+
+ m_deviceResources->SetLogicalSize(width, height);
+
+ if (!m_IsAlteringWindow)
+ CreateBackBuffer();
+}
+
+bool CWinSystemWin10DX::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ bool const result = CWinSystemWin10::SetFullScreen(fullScreen, res, blankOtherDisplays);
+ CRenderSystemDX::OnResize();
+ return result;
+}
+
+void CWinSystemWin10DX::UninitHooks()
+{
+}
+
+void CWinSystemWin10DX::InitHooks(IDXGIOutput* pOutput)
+{
+}
+
+bool CWinSystemWin10DX::IsHDRDisplay()
+{
+ return false; // use tone mapping by default on Xbox
+}
+
+HDR_STATUS CWinSystemWin10DX::GetOSHDRStatus()
+{
+ return CWIN32Util::GetWindowsHDRStatus();
+}
+
+HDR_STATUS CWinSystemWin10DX::ToggleHDR()
+{
+ return m_deviceResources->ToggleHDR();
+}
+
+bool CWinSystemWin10DX::IsHDROutput() const
+{
+ return m_deviceResources->IsHDROutput();
+}
+
+bool CWinSystemWin10DX::IsTransferPQ() const
+{
+ return m_deviceResources->IsTransferPQ();
+}
+
+void CWinSystemWin10DX::SetHdrMetaData(DXGI_HDR_METADATA_HDR10& hdr10) const
+{
+ m_deviceResources->SetHdrMetaData(hdr10);
+}
+
+void CWinSystemWin10DX::SetHdrColorSpace(const DXGI_COLOR_SPACE_TYPE colorSpace) const
+{
+ m_deviceResources->SetHdrColorSpace(colorSpace);
+}
+
+DEBUG_INFO_RENDER CWinSystemWin10DX::GetDebugInfo()
+{
+ return m_deviceResources->GetDebugInfo();
+}
diff --git a/xbmc/windowing/win10/WinSystemWin10DX.h b/xbmc/windowing/win10/WinSystemWin10DX.h
new file mode 100644
index 0000000..84ad660
--- /dev/null
+++ b/xbmc/windowing/win10/WinSystemWin10DX.h
@@ -0,0 +1,90 @@
+/*
+ * 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 "HDRStatus.h"
+#include "WinSystemWin10.h"
+#include "rendering/dx/RenderSystemDX.h"
+
+class CWinSystemWin10DX : public CWinSystemWin10, public CRenderSystemDX
+{
+public:
+ CWinSystemWin10DX();
+ ~CWinSystemWin10DX();
+
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystemBase via CWinSystemWin10
+ CRenderSystemBase *GetRenderSystem() override { return this; }
+ bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override;
+ bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+ void PresentRenderImpl(bool rendered) override;
+ bool DPIChanged(WORD dpi, RECT windowRect) const override;
+ bool DestroyRenderSystem() override;
+ void* GetHWContext() override { return m_deviceResources->GetD3DContext(); }
+
+ void UninitHooks();
+ void InitHooks(IDXGIOutput* pOutput);
+
+ void OnMove(int x, int y) override;
+ void OnResize(int width, int height);
+ winrt::Windows::Foundation::Size GetOutputSize() const { return m_deviceResources->GetOutputSize(); }
+ void TrimDevice() const { m_deviceResources->Trim(); }
+
+ /*!
+ \brief Register as a dependent of the DirectX Render System
+ Resources should call this on construction if they're dependent on the Render System
+ for survival. Any resources that registers will get callbacks on loss and reset of
+ device. In addition, callbacks for destruction and creation of the device are also called,
+ where any resources dependent on the DirectX device should be destroyed and recreated.
+ \sa Unregister, ID3DResource
+ */
+ void Register(ID3DResource *resource) const
+ {
+ m_deviceResources->Register(resource);
+ };
+ /*!
+ \brief Unregister as a dependent of the DirectX Render System
+ Resources should call this on destruction if they're a dependent on the Render System
+ \sa Register, ID3DResource
+ */
+ void Unregister(ID3DResource *resource) const
+ {
+ m_deviceResources->Unregister(resource);
+ };
+
+ void Register(IDispResource* resource) override { CWinSystemWin10::Register(resource); }
+ void Unregister(IDispResource* resource) override { CWinSystemWin10::Unregister(resource); }
+
+ void ShowSplash(const std::string& message) override;
+
+ // HDR OS/display override
+ bool IsHDRDisplay() override;
+ HDR_STATUS ToggleHDR() override;
+ HDR_STATUS GetOSHDRStatus() override;
+
+ // HDR support
+ bool IsHDROutput() const;
+ bool IsTransferPQ() const;
+ void SetHdrMetaData(DXGI_HDR_METADATA_HDR10& hdr10) const;
+ void SetHdrColorSpace(const DXGI_COLOR_SPACE_TYPE colorSpace) const;
+
+ // Get debug info from swapchain
+ DEBUG_INFO_RENDER GetDebugInfo() override;
+
+protected:
+ void SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res) override;
+ void ReleaseBackBuffer() override;
+ void CreateBackBuffer() override;
+ void ResizeDeviceBuffers() override;
+ bool IsStereoEnabled() override;
+};
+
diff --git a/xbmc/windowing/windows/CMakeLists.txt b/xbmc/windowing/windows/CMakeLists.txt
new file mode 100644
index 0000000..2b84cee
--- /dev/null
+++ b/xbmc/windowing/windows/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(SOURCES VideoSyncD3D.cpp
+ Win32DPMSSupport.cpp
+ WinEventsWin32.cpp
+ WinSystemWin32.cpp
+ WinSystemWin32DX.cpp)
+
+set(HEADERS VideoSyncD3D.h
+ Win32DPMSSupport.h
+ WinEventsWin32.h
+ WinSystemWin32.h
+ WinSystemWin32DX.h
+ WinKeyMap.h)
+
+core_add_library(windowing_windows)
diff --git a/xbmc/windowing/windows/VideoSyncD3D.cpp b/xbmc/windowing/windows/VideoSyncD3D.cpp
new file mode 100644
index 0000000..d95374f
--- /dev/null
+++ b/xbmc/windowing/windows/VideoSyncD3D.cpp
@@ -0,0 +1,137 @@
+/*
+ * 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 "VideoSyncD3D.h"
+
+#include "Utils/MathUtils.h"
+#include "Utils/TimeUtils.h"
+#include "rendering/dx/DeviceResources.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/StringUtils.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+void CVideoSyncD3D::OnLostDisplay()
+{
+ if (!m_displayLost)
+ {
+ m_displayLost = true;
+ m_lostEvent.Wait();
+ }
+}
+
+void CVideoSyncD3D::OnResetDisplay()
+{
+ m_displayReset = true;
+}
+
+void CVideoSyncD3D::RefreshChanged()
+{
+ m_displayReset = true;
+}
+
+bool CVideoSyncD3D::Setup(PUPDATECLOCK func)
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncD3D: Setting up Direct3d");
+ std::unique_lock<CCriticalSection> lock(CServiceBroker::GetWinSystem()->GetGfxContext());
+ DX::Windowing()->Register(this);
+ m_displayLost = false;
+ m_displayReset = false;
+ m_lostEvent.Reset();
+ UpdateClock = func;
+
+ // we need a high priority thread to get accurate timing
+ if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL))
+ CLog::Log(LOGDEBUG, "CVideoSyncD3D: SetThreadPriority failed");
+
+ return true;
+}
+
+void CVideoSyncD3D::Run(CEvent& stopEvent)
+{
+ int64_t Now;
+ int64_t LastVBlankTime;
+ int NrVBlanks;
+ double VBlankTime;
+ int64_t systemFrequency = CurrentHostFrequency();
+
+ // init the vblanktime
+ Now = CurrentHostCounter();
+ LastVBlankTime = Now;
+ m_lastUpdateTime = Now - systemFrequency;
+ while (!stopEvent.Signaled() && !m_displayLost && !m_displayReset)
+ {
+ // sleep until vblank
+ Microsoft::WRL::ComPtr<IDXGIOutput> pOutput;
+ DX::DeviceResources::Get()->GetOutput(&pOutput);
+ HRESULT hr = pOutput->WaitForVBlank();
+
+ // calculate how many vblanks happened
+ Now = CurrentHostCounter();
+ VBlankTime = (double)(Now - LastVBlankTime) / (double)systemFrequency;
+ NrVBlanks = MathUtils::round_int(VBlankTime * m_fps);
+
+ // update the vblank timestamp, update the clock and send a signal that we got a vblank
+ UpdateClock(NrVBlanks, Now, m_refClock);
+
+ // save the timestamp of this vblank so we can calculate how many vblanks happened next time
+ LastVBlankTime = Now;
+
+ if ((Now - m_lastUpdateTime) >= systemFrequency)
+ {
+ float fps = m_fps;
+ if (fps != GetFps())
+ break;
+ }
+
+ // because we had a vblank, sleep until half the refreshrate period because i think WaitForVBlank block any rendering stuf
+ // without sleeping we have freeze rendering
+ int SleepTime = (int)((LastVBlankTime + (systemFrequency / MathUtils::round_int(m_fps) / 2) - Now) * 1000 / systemFrequency);
+ if (SleepTime > 50)
+ SleepTime = 50; //failsafe
+ if (SleepTime > 0)
+ ::Sleep(SleepTime);
+ }
+
+ m_lostEvent.Set();
+ while (!stopEvent.Signaled() && m_displayLost && !m_displayReset)
+ {
+ KODI::TIME::Sleep(10ms);
+ }
+}
+
+void CVideoSyncD3D::Cleanup()
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncD3D: Cleaning up Direct3d");
+
+ m_lostEvent.Set();
+ DX::Windowing()->Unregister(this);
+}
+
+float CVideoSyncD3D::GetFps()
+{
+ DXGI_MODE_DESC DisplayMode = {};
+ DX::DeviceResources::Get()->GetDisplayMode(&DisplayMode);
+
+ m_fps = (DisplayMode.RefreshRate.Denominator != 0) ? (float)DisplayMode.RefreshRate.Numerator / (float)DisplayMode.RefreshRate.Denominator : 0.0f;
+
+ if (m_fps == 0.0)
+ m_fps = 60.0f;
+
+ if (DX::Windowing()->Interlaced())
+ {
+ m_fps *= 2;
+ }
+ return m_fps;
+}
+
diff --git a/xbmc/windowing/windows/VideoSyncD3D.h b/xbmc/windowing/windows/VideoSyncD3D.h
new file mode 100644
index 0000000..e241650
--- /dev/null
+++ b/xbmc/windowing/windows/VideoSyncD3D.h
@@ -0,0 +1,37 @@
+/*
+ * 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 "guilib/DispResource.h"
+#include "threads/Event.h"
+#include "windowing/VideoSync.h"
+
+class CVideoSyncD3D : public CVideoSync, IDispResource
+{
+public:
+ CVideoSyncD3D(void* clock)
+ : CVideoSync(clock), m_displayLost(false), m_displayReset(false), m_lastUpdateTime(0)
+ {
+ }
+ bool Setup(PUPDATECLOCK func) override;
+ void Run(CEvent& stopEvent) override;
+ void Cleanup() override;
+ float GetFps() override;
+ void RefreshChanged() override;
+ // IDispResource overrides
+ void OnLostDisplay() override;
+ void OnResetDisplay() override;
+
+private:
+ volatile bool m_displayLost;
+ volatile bool m_displayReset;
+ CEvent m_lostEvent;
+ int64_t m_lastUpdateTime;
+};
+
diff --git a/xbmc/windowing/windows/Win32DPMSSupport.cpp b/xbmc/windowing/windows/Win32DPMSSupport.cpp
new file mode 100644
index 0000000..99adc10
--- /dev/null
+++ b/xbmc/windowing/windows/Win32DPMSSupport.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2009-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 "Win32DPMSSupport.h"
+
+#include "ServiceBroker.h"
+#include "rendering/dx/RenderContext.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/windows/WinSystemWin32.h"
+
+CWin32DPMSSupport::CWin32DPMSSupport()
+{
+ m_supportedModes.push_back(OFF);
+ m_supportedModes.push_back(STANDBY);
+}
+
+bool CWin32DPMSSupport::EnablePowerSaving(PowerSavingMode mode)
+{
+ auto winSystem = dynamic_cast<CWinSystemWin32*>(CServiceBroker::GetWinSystem());
+ if (!winSystem)
+ return false;
+
+ if (!winSystem->GetGfxContext().IsFullScreenRoot())
+ {
+ CLog::Log(LOGDEBUG, "DPMS: not in fullscreen, power saving disabled");
+ return false;
+ }
+
+ switch (mode)
+ {
+ case OFF:
+ // Turn off display
+ return SendMessage(DX::Windowing()->GetHwnd(), WM_SYSCOMMAND, SC_MONITORPOWER, static_cast<LPARAM>(2)) == 0;
+ case STANDBY:
+ // Set display to low power
+ return SendMessage(DX::Windowing()->GetHwnd(), WM_SYSCOMMAND, SC_MONITORPOWER, static_cast<LPARAM>(1)) == 0;
+ default:
+ return true;
+ }
+}
+
+bool CWin32DPMSSupport::DisablePowerSaving()
+{
+ // Turn display on
+ return SendMessage(DX::Windowing()->GetHwnd(), WM_SYSCOMMAND, SC_MONITORPOWER, static_cast<LPARAM>(-1)) == 0;
+}
diff --git a/xbmc/windowing/windows/Win32DPMSSupport.h b/xbmc/windowing/windows/Win32DPMSSupport.h
new file mode 100644
index 0000000..75c192f
--- /dev/null
+++ b/xbmc/windowing/windows/Win32DPMSSupport.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2009-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 "xbmc/powermanagement/DPMSSupport.h"
+
+class CWin32DPMSSupport : public CDPMSSupport
+{
+public:
+ CWin32DPMSSupport();
+ ~CWin32DPMSSupport() = default;
+
+protected:
+ bool EnablePowerSaving(PowerSavingMode mode) override;
+ bool DisablePowerSaving() override;
+};
diff --git a/xbmc/windowing/windows/WinEventsWin32.cpp b/xbmc/windowing/windows/WinEventsWin32.cpp
new file mode 100644
index 0000000..7b4cba3
--- /dev/null
+++ b/xbmc/windowing/windows/WinEventsWin32.cpp
@@ -0,0 +1,1074 @@
+/*
+ * 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.
+ */
+
+#ifndef _USE_MATH_DEFINES
+#define _USE_MATH_DEFINES
+#endif
+#include "WinEventsWin32.h"
+
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "WinKeyMap.h"
+#include "application/AppInboundProtocol.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIControl.h" // for EVENT_RESULT
+#include "guilib/GUIWindowManager.h"
+#include "input/InputManager.h"
+#include "input/mouse/MouseStat.h"
+#include "input/touch/generic/GenericTouchActionHandler.h"
+#include "input/touch/generic/GenericTouchSwipeDetector.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/Zeroconf.h"
+#include "network/ZeroconfBrowser.h"
+#include "peripherals/Peripherals.h"
+#include "rendering/dx/RenderContext.h"
+#include "storage/MediaManager.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include "platform/win32/CharsetConverter.h"
+#include "platform/win32/WIN32Util.h"
+#include "platform/win32/powermanagement/Win32PowerSyscall.h"
+#include "platform/win32/storage/Win32StorageProvider.h"
+
+#include <array>
+#include <math.h>
+
+#include <Shlobj.h>
+#include <dbt.h>
+
+HWND g_hWnd = nullptr;
+
+#ifndef LODWORD
+#define LODWORD(longval) ((DWORD)((DWORDLONG)(longval)))
+#endif
+
+#define ROTATE_ANGLE_DEGREE(arg) GID_ROTATE_ANGLE_FROM_ARGUMENT(LODWORD(arg)) * 180 / M_PI
+
+#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp))
+#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp))
+
+/* Masks for processing the windows KEYDOWN and KEYUP messages */
+#define REPEATED_KEYMASK (1<<30)
+#define EXTENDED_KEYMASK (1<<24)
+#define EXTKEYPAD(keypad) ((scancode & 0x100)?(mvke):(keypad))
+
+static GUID USB_HID_GUID = { 0x4D1E55B2, 0xF16F, 0x11CF, { 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } };
+
+uint32_t g_uQueryCancelAutoPlay = 0;
+bool g_sizeMoveSizing = false;
+bool g_sizeMoveMoving = false;
+int g_sizeMoveWidth = 0;
+int g_sizeMoveHight = 0;
+int g_sizeMoveX = -10000;
+int g_sizeMoveY = -10000;
+
+int XBMC_TranslateUNICODE = 1;
+
+int CWinEventsWin32::m_originalZoomDistance = 0;
+Pointer CWinEventsWin32::m_touchPointer;
+CGenericTouchSwipeDetector* CWinEventsWin32::m_touchSwipeDetector = nullptr;
+
+// register to receive SD card events (insert/remove)
+// seen at http://www.codeproject.com/Messages/2897423/Re-No-message-triggered-on-SD-card-insertion-remov.aspx
+#define WM_MEDIA_CHANGE (WM_USER + 666)
+SHChangeNotifyEntry shcne;
+
+static int XBMC_MapVirtualKey(int scancode, WPARAM vkey)
+{
+ int mvke = MapVirtualKeyEx(scancode & 0xFF, 1, nullptr);
+
+ switch (vkey)
+ { /* These are always correct */
+ case VK_DIVIDE:
+ case VK_MULTIPLY:
+ case VK_SUBTRACT:
+ case VK_ADD:
+ case VK_LWIN:
+ case VK_RWIN:
+ case VK_APPS:
+ /* These are already handled */
+ case VK_LCONTROL:
+ case VK_RCONTROL:
+ case VK_LSHIFT:
+ case VK_RSHIFT:
+ case VK_LMENU:
+ case VK_RMENU:
+ case VK_SNAPSHOT:
+ case VK_PAUSE:
+ /* Multimedia keys are already handled */
+ case VK_BROWSER_BACK:
+ case VK_BROWSER_FORWARD:
+ case VK_BROWSER_REFRESH:
+ case VK_BROWSER_STOP:
+ case VK_BROWSER_SEARCH:
+ case VK_BROWSER_FAVORITES:
+ case VK_BROWSER_HOME:
+ case VK_VOLUME_MUTE:
+ case VK_VOLUME_DOWN:
+ case VK_VOLUME_UP:
+ case VK_MEDIA_NEXT_TRACK:
+ case VK_MEDIA_PREV_TRACK:
+ case VK_MEDIA_STOP:
+ case VK_MEDIA_PLAY_PAUSE:
+ case VK_LAUNCH_MAIL:
+ case VK_LAUNCH_MEDIA_SELECT:
+ case VK_LAUNCH_APP1:
+ case VK_LAUNCH_APP2:
+ return static_cast<int>(vkey);
+ default:;
+ }
+ switch (mvke)
+ { /* Distinguish between keypad and extended keys */
+ case VK_INSERT: return EXTKEYPAD(VK_NUMPAD0);
+ case VK_DELETE: return EXTKEYPAD(VK_DECIMAL);
+ case VK_END: return EXTKEYPAD(VK_NUMPAD1);
+ case VK_DOWN: return EXTKEYPAD(VK_NUMPAD2);
+ case VK_NEXT: return EXTKEYPAD(VK_NUMPAD3);
+ case VK_LEFT: return EXTKEYPAD(VK_NUMPAD4);
+ case VK_CLEAR: return EXTKEYPAD(VK_NUMPAD5);
+ case VK_RIGHT: return EXTKEYPAD(VK_NUMPAD6);
+ case VK_HOME: return EXTKEYPAD(VK_NUMPAD7);
+ case VK_UP: return EXTKEYPAD(VK_NUMPAD8);
+ case VK_PRIOR: return EXTKEYPAD(VK_NUMPAD9);
+ default:;
+ }
+ return mvke ? mvke : static_cast<int>(vkey);
+}
+
+
+static XBMC_keysym *TranslateKey(WPARAM vkey, UINT scancode, XBMC_keysym *keysym, int pressed)
+{
+ using namespace KODI::WINDOWING::WINDOWS;
+
+ uint8_t keystate[256];
+
+ /* Set the keysym information */
+ keysym->scancode = static_cast<unsigned char>(scancode);
+ keysym->unicode = 0;
+
+ if ((vkey == VK_RETURN) && (scancode & 0x100))
+ {
+ /* No VK_ code for the keypad enter key */
+ keysym->sym = XBMCK_KP_ENTER;
+ }
+ else
+ {
+ keysym->sym = VK_keymap[XBMC_MapVirtualKey(scancode, vkey)];
+ }
+
+ // Attempt to convert the keypress to a UNICODE character
+ GetKeyboardState(keystate);
+
+ if (pressed && XBMC_TranslateUNICODE)
+ {
+ std::array<uint16_t, 2> wchars;
+
+ /* Numlock isn't taken into account in ToUnicode,
+ * so we handle it as a special case here */
+ if ((keystate[VK_NUMLOCK] & 1) && vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)
+ {
+ keysym->unicode = static_cast<uint16_t>(vkey - VK_NUMPAD0 + '0');
+ }
+ else if (ToUnicode(static_cast<UINT>(vkey), scancode, keystate,
+ reinterpret_cast<LPWSTR>(wchars.data()), wchars.size(), 0) > 0)
+ {
+ keysym->unicode = wchars[0];
+ }
+ }
+
+ // Set the modifier bitmap
+
+ uint16_t mod = static_cast<uint16_t>(XBMCKMOD_NONE);
+
+ // If left control and right alt are down this usually means that
+ // AltGr is down
+ if ((keystate[VK_LCONTROL] & 0x80) && (keystate[VK_RMENU] & 0x80))
+ {
+ mod |= XBMCKMOD_MODE;
+ }
+ else
+ {
+ if (keystate[VK_LCONTROL] & 0x80) mod |= XBMCKMOD_LCTRL;
+ if (keystate[VK_RMENU] & 0x80) mod |= XBMCKMOD_RALT;
+ }
+
+ // Check the remaining modifiers
+ if (keystate[VK_LSHIFT] & 0x80) mod |= XBMCKMOD_LSHIFT;
+ if (keystate[VK_RSHIFT] & 0x80) mod |= XBMCKMOD_RSHIFT;
+ if (keystate[VK_RCONTROL] & 0x80) mod |= XBMCKMOD_RCTRL;
+ if (keystate[VK_LMENU] & 0x80) mod |= XBMCKMOD_LALT;
+ if (keystate[VK_LWIN] & 0x80) mod |= XBMCKMOD_LSUPER;
+ if (keystate[VK_RWIN] & 0x80) mod |= XBMCKMOD_LSUPER;
+ keysym->mod = static_cast<XBMCMod>(mod);
+
+ // Return the updated keysym
+ return(keysym);
+}
+
+
+bool CWinEventsWin32::MessagePump()
+{
+ MSG msg;
+ while( PeekMessage( &msg, nullptr, 0U, 0U, PM_REMOVE ) )
+ {
+ TranslateMessage( &msg );
+ DispatchMessage( &msg );
+ }
+ return true;
+}
+
+LRESULT CALLBACK CWinEventsWin32::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ using KODI::PLATFORM::WINDOWS::FromW;
+
+ XBMC_Event newEvent = {};
+ static HDEVNOTIFY hDeviceNotify;
+
+#if 0
+ if (uMsg == WM_NCCREATE)
+ {
+ // if available, enable DPI scaling of non-client portion of window (title bar, etc.)
+ if (g_Windowing.PtrEnableNonClientDpiScaling != NULL)
+ {
+ g_Windowing.PtrEnableNonClientDpiScaling(hWnd);
+ }
+ return DefWindowProc(hWnd, uMsg, wParam, lParam);
+ }
+#endif
+
+ if (uMsg == WM_CREATE)
+ {
+ g_hWnd = hWnd;
+ // need to set windows handle before WM_SIZE processing
+ DX::Windowing()->SetWindow(hWnd);
+
+ KODI::WINDOWING::WINDOWS::DIB_InitOSKeymap();
+
+ g_uQueryCancelAutoPlay = RegisterWindowMessage(TEXT("QueryCancelAutoPlay"));
+ shcne.pidl = nullptr;
+ shcne.fRecursive = TRUE;
+ long fEvents = SHCNE_DRIVEADD | SHCNE_DRIVEREMOVED | SHCNE_MEDIAREMOVED | SHCNE_MEDIAINSERTED;
+ SHChangeNotifyRegister(hWnd, SHCNRF_ShellLevel | SHCNRF_NewDelivery, fEvents, WM_MEDIA_CHANGE, 1, &shcne);
+ RegisterDeviceInterfaceToHwnd(USB_HID_GUID, hWnd, &hDeviceNotify);
+ return 0;
+ }
+
+ if (uMsg == WM_DESTROY)
+ g_hWnd = nullptr;
+
+ if(g_uQueryCancelAutoPlay != 0 && uMsg == g_uQueryCancelAutoPlay)
+ return S_FALSE;
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+
+ switch (uMsg)
+ {
+ case WM_CLOSE:
+ case WM_QUIT:
+ case WM_DESTROY:
+ if (hDeviceNotify)
+ {
+ if (UnregisterDeviceNotification(hDeviceNotify))
+ hDeviceNotify = nullptr;
+ else
+ CLog::LogF(LOGINFO, "UnregisterDeviceNotification failed ({})", GetLastError());
+ }
+ newEvent.type = XBMC_QUIT;
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ break;
+ case WM_SHOWWINDOW:
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ bool active = appPower->GetRenderGUI();
+ if (appPort)
+ appPort->SetRenderGUI(wParam != 0);
+ if (appPower->GetRenderGUI() != active)
+ DX::Windowing()->NotifyAppActiveChange(appPower->GetRenderGUI());
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "WM_SHOWWINDOW -> window is {}",
+ wParam != 0 ? "shown" : "hidden");
+ }
+ break;
+ case WM_ACTIVATE:
+ {
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "WM_ACTIVATE -> window is {}",
+ LOWORD(wParam) != WA_INACTIVE ? "active" : "inactive");
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ bool active = appPower->GetRenderGUI();
+ if (HIWORD(wParam))
+ {
+ if (appPort)
+ appPort->SetRenderGUI(false);
+ }
+ else
+ {
+ WINDOWPLACEMENT lpwndpl;
+ lpwndpl.length = sizeof(lpwndpl);
+ if (LOWORD(wParam) != WA_INACTIVE)
+ {
+ if (GetWindowPlacement(hWnd, &lpwndpl))
+ {
+ if (appPort)
+ appPort->SetRenderGUI(lpwndpl.showCmd != SW_HIDE);
+ }
+ }
+ else
+ {
+
+ }
+ }
+ if (appPower->GetRenderGUI() != active)
+ DX::Windowing()->NotifyAppActiveChange(appPower->GetRenderGUI());
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "window is {}",
+ appPower->GetRenderGUI() ? "active" : "inactive");
+ }
+ break;
+ case WM_SETFOCUS:
+ case WM_KILLFOCUS:
+ g_application.m_AppFocused = uMsg == WM_SETFOCUS;
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "window focus {}",
+ g_application.m_AppFocused ? "set" : "lost");
+
+ DX::Windowing()->NotifyAppFocusChange(g_application.m_AppFocused);
+ if (uMsg == WM_KILLFOCUS)
+ {
+ std::string procfile;
+ if (CWIN32Util::GetFocussedProcess(procfile))
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "Focus switched to process {}", procfile);
+ }
+ break;
+ /* needs to be reviewed after frodo. we reset the system idle timer
+ and the display timer directly now (see m_screenSaverTimer).
+ case WM_SYSCOMMAND:
+ switch( wParam&0xFFF0 )
+ {
+ case SC_MONITORPOWER:
+ if (g_application.GetAppPlayer().IsPlaying() || g_application.GetAppPlayer().IsPausedPlayback())
+ return 0;
+ else if(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_POWERMANAGEMENT_DISPLAYSOFF) == 0)
+ return 0;
+ break;
+ case SC_SCREENSAVE:
+ return 0;
+ }
+ break;*/
+ case WM_SYSKEYDOWN:
+ switch (wParam)
+ {
+ case VK_F4: //alt-f4, default event quit.
+ return(DefWindowProc(hWnd, uMsg, wParam, lParam));
+ case VK_RETURN: //alt-return
+ if ((lParam & REPEATED_KEYMASK) == 0)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_TOGGLEFULLSCREEN);
+ return 0;
+ default:;
+ }
+ //deliberate fallthrough
+ case WM_KEYDOWN:
+ {
+ switch (wParam)
+ {
+ case VK_CONTROL:
+ if ( lParam & EXTENDED_KEYMASK )
+ wParam = VK_RCONTROL;
+ else
+ wParam = VK_LCONTROL;
+ break;
+ case VK_SHIFT:
+ /* EXTENDED trick doesn't work here */
+ if (GetKeyState(VK_LSHIFT) & 0x8000)
+ wParam = VK_LSHIFT;
+ else if (GetKeyState(VK_RSHIFT) & 0x8000)
+ wParam = VK_RSHIFT;
+ break;
+ case VK_MENU:
+ if ( lParam & EXTENDED_KEYMASK )
+ wParam = VK_RMENU;
+ else
+ wParam = VK_LMENU;
+ break;
+ default:;
+ }
+ XBMC_keysym keysym;
+ TranslateKey(wParam, HIWORD(lParam), &keysym, 1);
+
+ newEvent.type = XBMC_KEYDOWN;
+ newEvent.key.keysym = keysym;
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ }
+ return(0);
+
+ case WM_SYSKEYUP:
+ case WM_KEYUP:
+ {
+ switch (wParam)
+ {
+ case VK_CONTROL:
+ if ( lParam&EXTENDED_KEYMASK )
+ wParam = VK_RCONTROL;
+ else
+ wParam = VK_LCONTROL;
+ break;
+ case VK_SHIFT:
+ {
+ uint32_t scanCodeL = MapVirtualKey(VK_LSHIFT, MAPVK_VK_TO_VSC);
+ uint32_t scanCodeR = MapVirtualKey(VK_RSHIFT, MAPVK_VK_TO_VSC);
+ uint32_t keyCode = static_cast<uint32_t>((lParam & 0xFF0000) >> 16);
+ if (keyCode == scanCodeL)
+ wParam = VK_LSHIFT;
+ else if (keyCode == scanCodeR)
+ wParam = VK_RSHIFT;
+ }
+ break;
+ case VK_MENU:
+ if ( lParam&EXTENDED_KEYMASK )
+ wParam = VK_RMENU;
+ else
+ wParam = VK_LMENU;
+ break;
+ default:;
+ }
+ XBMC_keysym keysym;
+ TranslateKey(wParam, HIWORD(lParam), &keysym, 1);
+
+ if (wParam == VK_SNAPSHOT)
+ newEvent.type = XBMC_KEYDOWN;
+ else
+ newEvent.type = XBMC_KEYUP;
+ newEvent.key.keysym = keysym;
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ }
+ return(0);
+ case WM_APPCOMMAND: // MULTIMEDIA keys are mapped to APPCOMMANDS
+ {
+ const unsigned int appcmd = GET_APPCOMMAND_LPARAM(lParam);
+
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "APPCOMMAND {}", appcmd);
+
+ // Reset the screen saver
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ appPower->ResetScreenSaver();
+
+ // If we were currently in the screen saver wake up and don't process the
+ // appcommand
+ if (appPower->WakeUpScreenSaverAndDPMS())
+ return true;
+
+ // Retrieve the action associated with this appcommand from the mapping table
+ CKey key(appcmd | KEY_APPCOMMAND, 0U);
+ int iWin = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog();
+
+ CAction appcmdaction = CServiceBroker::GetInputManager().GetAction(iWin, key);
+ if (appcmdaction.GetID())
+ {
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "appcommand {}, action {}", appcmd,
+ appcmdaction.GetName());
+ CServiceBroker::GetInputManager().QueueAction(appcmdaction);
+ return true;
+ }
+
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "unknown appcommand {}", appcmd);
+ return DefWindowProc(hWnd, uMsg, wParam, lParam);
+ }
+ case WM_GESTURENOTIFY:
+ {
+ OnGestureNotify(hWnd, lParam);
+ return DefWindowProc(hWnd, WM_GESTURENOTIFY, wParam, lParam);
+ }
+ case WM_GESTURE:
+ {
+ OnGesture(hWnd, lParam);
+ return 0;
+ }
+ case WM_SYSCHAR:
+ if (wParam == VK_RETURN) //stop system beep on alt-return
+ return 0;
+ break;
+ case WM_SETCURSOR:
+ if (HTCLIENT != LOWORD(lParam))
+ DX::Windowing()->ShowOSMouse(true);
+ break;
+ case WM_MOUSEMOVE:
+ newEvent.type = XBMC_MOUSEMOTION;
+ newEvent.motion.x = GET_X_LPARAM(lParam);
+ newEvent.motion.y = GET_Y_LPARAM(lParam);
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ return(0);
+ case WM_LBUTTONDOWN:
+ case WM_MBUTTONDOWN:
+ case WM_RBUTTONDOWN:
+ newEvent.type = XBMC_MOUSEBUTTONDOWN;
+ newEvent.button.x = GET_X_LPARAM(lParam);
+ newEvent.button.y = GET_Y_LPARAM(lParam);
+ newEvent.button.button = 0;
+ if (uMsg == WM_LBUTTONDOWN) newEvent.button.button = XBMC_BUTTON_LEFT;
+ else if (uMsg == WM_MBUTTONDOWN) newEvent.button.button = XBMC_BUTTON_MIDDLE;
+ else if (uMsg == WM_RBUTTONDOWN) newEvent.button.button = XBMC_BUTTON_RIGHT;
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ return(0);
+ case WM_LBUTTONUP:
+ case WM_MBUTTONUP:
+ case WM_RBUTTONUP:
+ newEvent.type = XBMC_MOUSEBUTTONUP;
+ newEvent.button.x = GET_X_LPARAM(lParam);
+ newEvent.button.y = GET_Y_LPARAM(lParam);
+ newEvent.button.button = 0;
+ if (uMsg == WM_LBUTTONUP) newEvent.button.button = XBMC_BUTTON_LEFT;
+ else if (uMsg == WM_MBUTTONUP) newEvent.button.button = XBMC_BUTTON_MIDDLE;
+ else if (uMsg == WM_RBUTTONUP) newEvent.button.button = XBMC_BUTTON_RIGHT;
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ return(0);
+ case WM_MOUSEWHEEL:
+ {
+ // SDL, which our events system is based off, sends a MOUSEBUTTONDOWN message
+ // followed by a MOUSEBUTTONUP message. As this is a momentary event, we just
+ // react on the MOUSEBUTTONUP message, resetting the state after processing.
+ newEvent.type = XBMC_MOUSEBUTTONDOWN;
+ // the coordinates in WM_MOUSEWHEEL are screen, not client coordinates
+ POINT point;
+ point.x = GET_X_LPARAM(lParam);
+ point.y = GET_Y_LPARAM(lParam);
+ WindowFromScreenCoords(hWnd, &point);
+ newEvent.button.x = static_cast<uint16_t>(point.x);
+ newEvent.button.y = static_cast<uint16_t>(point.y);
+ newEvent.button.button = GET_Y_LPARAM(wParam) > 0 ? XBMC_BUTTON_WHEELUP : XBMC_BUTTON_WHEELDOWN;
+ if (appPort)
+ {
+ appPort->OnEvent(newEvent);
+ newEvent.type = XBMC_MOUSEBUTTONUP;
+ appPort->OnEvent(newEvent);
+ }
+ }
+ return(0);
+ case WM_DPICHANGED:
+ // This message tells the program that most of its window is on a
+ // monitor with a new DPI. The wParam contains the new DPI, and the
+ // lParam contains a rect which defines the window rectangle scaled
+ // the new DPI.
+ {
+ // get the suggested size of the window on the new display with a different DPI
+ uint16_t dpi = HIWORD(wParam);
+ RECT rc = *reinterpret_cast<RECT*>(lParam);
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "dpi changed event -> {} ({}, {}, {}, {})", dpi, rc.left,
+ rc.top, rc.right, rc.bottom);
+ DX::Windowing()->DPIChanged(dpi, rc);
+ return(0);
+ }
+ case WM_DISPLAYCHANGE:
+ {
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "display change event");
+ if (DX::Windowing()->IsTogglingHDR() || DX::Windowing()->IsAlteringWindow())
+ return (0);
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (appPower->GetRenderGUI() && GET_X_LPARAM(lParam) > 0 && GET_Y_LPARAM(lParam) > 0)
+ {
+ DX::Windowing()->UpdateResolutions();
+ }
+ return(0);
+ }
+ case WM_ENTERSIZEMOVE:
+ {
+ DX::Windowing()->SetSizeMoveMode(true);
+ }
+ return(0);
+ case WM_EXITSIZEMOVE:
+ {
+ DX::Windowing()->SetSizeMoveMode(false);
+ if (g_sizeMoveMoving)
+ {
+ g_sizeMoveMoving = false;
+ newEvent.type = XBMC_VIDEOMOVE;
+ newEvent.move.x = g_sizeMoveX;
+ newEvent.move.y = g_sizeMoveY;
+
+ // tell the device about new position
+ DX::Windowing()->OnMove(newEvent.move.x, newEvent.move.y);
+ // tell the application about new position
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (appPower->GetRenderGUI() && !DX::Windowing()->IsAlteringWindow())
+ {
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ }
+ }
+ if (g_sizeMoveSizing)
+ {
+ g_sizeMoveSizing = false;
+ newEvent.type = XBMC_VIDEORESIZE;
+ newEvent.resize.w = g_sizeMoveWidth;
+ newEvent.resize.h = g_sizeMoveHight;
+
+ // tell the device about new size
+ DX::Windowing()->OnResize(newEvent.resize.w, newEvent.resize.h);
+ // tell the application about new size
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (appPower->GetRenderGUI() && !DX::Windowing()->IsAlteringWindow() &&
+ newEvent.resize.w > 0 && newEvent.resize.h > 0)
+ {
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ }
+ }
+ }
+ return(0);
+ case WM_SIZE:
+ if (wParam == SIZE_MINIMIZED)
+ {
+ if (!DX::Windowing()->IsMinimized())
+ {
+ DX::Windowing()->SetMinimized(true);
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (appPower->GetRenderGUI())
+ {
+ if (appPort)
+ appPort->SetRenderGUI(false);
+ }
+ }
+ }
+ else if (DX::Windowing()->IsMinimized())
+ {
+ DX::Windowing()->SetMinimized(false);
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (!appPower->GetRenderGUI())
+ {
+ if (appPort)
+ appPort->SetRenderGUI(true);
+ }
+ }
+ else
+ {
+ g_sizeMoveWidth = GET_X_LPARAM(lParam);
+ g_sizeMoveHight = GET_Y_LPARAM(lParam);
+ if (DX::Windowing()->IsInSizeMoveMode())
+ {
+ // If an user is dragging the resize bars, we don't resize
+ // the buffers and don't rise XBMC_VIDEORESIZE here because
+ // as the user continuously resize the window, a lot of WM_SIZE
+ // messages are sent to the proc, and it'd be pointless (and slow)
+ // to resize for each WM_SIZE message received from dragging.
+ // So instead, we reset after the user is done resizing the
+ // window and releases the resize bars, which ends with WM_EXITSIZEMOVE.
+ g_sizeMoveSizing = true;
+ }
+ else
+ {
+ // API call such as SetWindowPos or SwapChain->SetFullscreenState
+ newEvent.type = XBMC_VIDEORESIZE;
+ newEvent.resize.w = g_sizeMoveWidth;
+ newEvent.resize.h = g_sizeMoveHight;
+
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "window resize event {} x {}", newEvent.resize.w,
+ newEvent.resize.h);
+ // tell device about new size
+ DX::Windowing()->OnResize(newEvent.resize.w, newEvent.resize.h);
+ // tell application about size changes
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (appPower->GetRenderGUI() && !DX::Windowing()->IsAlteringWindow() &&
+ newEvent.resize.w > 0 && newEvent.resize.h > 0)
+ {
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ }
+ }
+ }
+ return(0);
+ case WM_MOVE:
+ {
+ g_sizeMoveX = GET_X_LPARAM(lParam);
+ g_sizeMoveY = GET_Y_LPARAM(lParam);
+ if (DX::Windowing()->IsInSizeMoveMode())
+ {
+ // the same as WM_SIZE
+ g_sizeMoveMoving = true;
+ }
+ else
+ {
+ newEvent.type = XBMC_VIDEOMOVE;
+ newEvent.move.x = g_sizeMoveX;
+ newEvent.move.y = g_sizeMoveY;
+
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "window move event");
+
+ // tell the device about new position
+ DX::Windowing()->OnMove(newEvent.move.x, newEvent.move.y);
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (appPower->GetRenderGUI() && !DX::Windowing()->IsAlteringWindow())
+ {
+ if (appPort)
+ appPort->OnEvent(newEvent);
+ }
+ }
+ }
+ return(0);
+ case WM_MEDIA_CHANGE:
+ {
+ // This event detects media changes of usb, sd card and optical media.
+ // It only works if the explorer.exe process is started. Because this
+ // isn't the case for all setups we use WM_DEVICECHANGE for usb and
+ // optical media because this event is also triggered without the
+ // explorer process. Since WM_DEVICECHANGE doesn't detect sd card changes
+ // we still use this event only for sd.
+ long lEvent;
+ PIDLIST_ABSOLUTE *ppidl;
+ HANDLE hLock = SHChangeNotification_Lock(reinterpret_cast<HANDLE>(wParam), static_cast<DWORD>(lParam), &ppidl, &lEvent);
+
+ if (hLock)
+ {
+ wchar_t drivePath[MAX_PATH+1];
+ if (!SHGetPathFromIDList(ppidl[0], drivePath))
+ break;
+
+ switch(lEvent)
+ {
+ case SHCNE_DRIVEADD:
+ case SHCNE_MEDIAINSERTED:
+ if (GetDriveType(drivePath) != DRIVE_CDROM)
+ {
+ CLog::LogF(LOGDEBUG, "Drive {} Media has arrived.", FromW(drivePath));
+ CWin32StorageProvider::SetEvent();
+ }
+ break;
+
+ case SHCNE_DRIVEREMOVED:
+ case SHCNE_MEDIAREMOVED:
+ if (GetDriveType(drivePath) != DRIVE_CDROM)
+ {
+ CLog::LogF(LOGDEBUG, "Drive {} Media was removed.", FromW(drivePath));
+ CWin32StorageProvider::SetEvent();
+ }
+ break;
+ default:;
+ }
+ SHChangeNotification_Unlock(hLock);
+ }
+ break;
+ }
+ case WM_POWERBROADCAST:
+ if (wParam==PBT_APMSUSPEND)
+ {
+ CLog::Log(LOGDEBUG,"WM_POWERBROADCAST: PBT_APMSUSPEND event was sent");
+ CWin32PowerSyscall::SetOnSuspend();
+ }
+ else if(wParam==PBT_APMRESUMEAUTOMATIC)
+ {
+ CLog::Log(LOGDEBUG,"WM_POWERBROADCAST: PBT_APMRESUMEAUTOMATIC event was sent");
+ CWin32PowerSyscall::SetOnResume();
+ }
+ break;
+ case WM_DEVICECHANGE:
+ {
+ switch(wParam)
+ {
+ case DBT_DEVNODES_CHANGED:
+ CServiceBroker::GetPeripherals().TriggerDeviceScan(PERIPHERALS::PERIPHERAL_BUS_USB);
+ break;
+ case DBT_DEVICEARRIVAL:
+ case DBT_DEVICEREMOVECOMPLETE:
+ if (((_DEV_BROADCAST_HEADER*) lParam)->dbcd_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
+ {
+ CServiceBroker::GetPeripherals().TriggerDeviceScan(PERIPHERALS::PERIPHERAL_BUS_USB);
+ }
+ // check if an usb or optical media was inserted or removed
+ if (((_DEV_BROADCAST_HEADER*) lParam)->dbcd_devicetype == DBT_DEVTYP_VOLUME)
+ {
+ PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)((_DEV_BROADCAST_HEADER*) lParam);
+ // optical medium
+ if (lpdbv -> dbcv_flags & DBTF_MEDIA)
+ {
+ std::string strdrive = StringUtils::Format(
+ "{}:", CWIN32Util::FirstDriveFromMask(lpdbv->dbcv_unitmask));
+ if(wParam == DBT_DEVICEARRIVAL)
+ {
+ CLog::LogF(LOGDEBUG, "Drive {} Media has arrived.", strdrive);
+ CServiceBroker::GetJobManager()->AddJob(new CDetectDisc(strdrive, true), nullptr);
+ }
+ else
+ {
+ CLog::LogF(LOGDEBUG, "Drive {} Media was removed.", strdrive);
+ CMediaSource share;
+ share.strPath = strdrive;
+ share.strName = share.strPath;
+ CServiceBroker::GetMediaManager().RemoveAutoSource(share);
+ }
+ }
+ else
+ CWin32StorageProvider::SetEvent();
+ }
+ default:;
+ }
+ break;
+ }
+ case WM_PAINT:
+ {
+ //some other app has painted over our window, mark everything as dirty
+ CGUIComponent* component = CServiceBroker::GetGUI();
+ if (component)
+ component->GetWindowManager().MarkDirty();
+ break;
+ }
+ case BONJOUR_EVENT:
+ CZeroconf::GetInstance()->ProcessResults();
+ break;
+ case BONJOUR_BROWSER_EVENT:
+ CZeroconfBrowser::GetInstance()->ProcessResults();
+ break;
+ case TRAY_ICON_NOTIFY:
+ {
+ switch (LOWORD(lParam))
+ {
+ case WM_LBUTTONDBLCLK:
+ {
+ DX::Windowing()->SetMinimized(false);
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (!appPower->GetRenderGUI())
+ {
+ if (appPort)
+ appPort->SetRenderGUI(true);
+ }
+ break;
+ }
+ }
+ break;
+ }
+ case WM_TIMER:
+ {
+ if (wParam == ID_TIMER_HDR)
+ {
+ CLog::LogFC(LOGDEBUG, LOGWINDOWING, "finish toggling HDR event");
+ DX::Windowing()->SetTogglingHDR(false);
+ KillTimer(hWnd, wParam);
+ }
+ break;
+ }
+ case WM_INITMENU:
+ {
+ HMENU hm = GetSystemMenu(hWnd, FALSE);
+ if (hm)
+ {
+ if (DX::Windowing()->IsFullScreen())
+ EnableMenuItem(hm, SC_MOVE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
+ else
+ EnableMenuItem(hm, SC_MOVE, MF_BYCOMMAND | MF_ENABLED);
+ }
+ break;
+ }
+ default:;
+ }
+ return(DefWindowProc(hWnd, uMsg, wParam, lParam));
+}
+
+void CWinEventsWin32::RegisterDeviceInterfaceToHwnd(GUID InterfaceClassGuid, HWND hWnd, HDEVNOTIFY *hDeviceNotify)
+{
+ DEV_BROADCAST_DEVICEINTERFACE NotificationFilter = {};
+
+ NotificationFilter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
+ NotificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
+ NotificationFilter.dbcc_classguid = InterfaceClassGuid;
+
+ *hDeviceNotify = RegisterDeviceNotification(
+ hWnd, // events recipient
+ &NotificationFilter, // type of device
+ DEVICE_NOTIFY_WINDOW_HANDLE // type of recipient handle
+ );
+}
+
+void CWinEventsWin32::WindowFromScreenCoords(HWND hWnd, POINT *point)
+{
+ if (!point) return;
+ RECT clientRect;
+ GetClientRect(hWnd, &clientRect);
+ POINT windowPos;
+ windowPos.x = clientRect.left;
+ windowPos.y = clientRect.top;
+ ClientToScreen(hWnd, &windowPos);
+ point->x -= windowPos.x;
+ point->y -= windowPos.y;
+}
+
+void CWinEventsWin32::OnGestureNotify(HWND hWnd, LPARAM lParam)
+{
+ // convert to window coordinates
+ PGESTURENOTIFYSTRUCT gn = reinterpret_cast<PGESTURENOTIFYSTRUCT>(lParam);
+ POINT point = { gn->ptsLocation.x, gn->ptsLocation.y };
+ WindowFromScreenCoords(hWnd, &point);
+
+ // by default we only want twofingertap and pressandtap gestures
+ // the other gestures are enabled best on supported gestures
+ GESTURECONFIG gc[] = {{ GID_ZOOM, 0, GC_ZOOM},
+ { GID_ROTATE, 0, GC_ROTATE},
+ { GID_PAN, 0, GC_PAN},
+ { GID_TWOFINGERTAP, GC_TWOFINGERTAP, GC_TWOFINGERTAP },
+ { GID_PRESSANDTAP, GC_PRESSANDTAP, GC_PRESSANDTAP }};
+
+ // send a message to see if a control wants any
+ int gestures;
+ if ((gestures = CGenericTouchActionHandler::GetInstance().QuerySupportedGestures(static_cast<float>(point.x), static_cast<float>(point.y))) != EVENT_RESULT_UNHANDLED)
+ {
+ if (gestures & EVENT_RESULT_ZOOM)
+ gc[0].dwWant |= GC_ZOOM;
+ if (gestures & EVENT_RESULT_ROTATE)
+ gc[1].dwWant |= GC_ROTATE;
+ if (gestures & EVENT_RESULT_PAN_VERTICAL)
+ gc[2].dwWant |= GC_PAN | GC_PAN_WITH_SINGLE_FINGER_VERTICALLY | GC_PAN_WITH_GUTTER | GC_PAN_WITH_INERTIA;
+ if (gestures & EVENT_RESULT_PAN_VERTICAL_WITHOUT_INERTIA)
+ gc[2].dwWant |= GC_PAN | GC_PAN_WITH_SINGLE_FINGER_VERTICALLY;
+ if (gestures & EVENT_RESULT_PAN_HORIZONTAL)
+ gc[2].dwWant |= GC_PAN | GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY | GC_PAN_WITH_GUTTER | GC_PAN_WITH_INERTIA;
+ if (gestures & EVENT_RESULT_PAN_HORIZONTAL_WITHOUT_INERTIA)
+ gc[2].dwWant |= GC_PAN | GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY;
+ if (gestures & EVENT_RESULT_SWIPE)
+ {
+ gc[2].dwWant |= GC_PAN | GC_PAN_WITH_SINGLE_FINGER_VERTICALLY | GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY | GC_PAN_WITH_GUTTER;
+
+ // create a new touch swipe detector
+ m_touchSwipeDetector = new CGenericTouchSwipeDetector(&CGenericTouchActionHandler::GetInstance(), 160.0f);
+ }
+
+ gc[0].dwBlock = gc[0].dwWant ^ 0x01;
+ gc[1].dwBlock = gc[1].dwWant ^ 0x01;
+ gc[2].dwBlock = gc[2].dwWant ^ 0x1F;
+ }
+ if (DX::Windowing()->PtrSetGestureConfig)
+ DX::Windowing()->PtrSetGestureConfig(hWnd, 0, 5, gc, sizeof(GESTURECONFIG));
+}
+
+void CWinEventsWin32::OnGesture(HWND hWnd, LPARAM lParam)
+{
+ if (!DX::Windowing()->PtrGetGestureInfo)
+ return;
+
+ GESTUREINFO gi = {};
+ gi.cbSize = sizeof(gi);
+ DX::Windowing()->PtrGetGestureInfo(reinterpret_cast<HGESTUREINFO>(lParam), &gi);
+
+ // convert to window coordinates
+ POINT point = { gi.ptsLocation.x, gi.ptsLocation.y };
+ WindowFromScreenCoords(hWnd, &point);
+
+ if (gi.dwID == GID_BEGIN)
+ m_touchPointer.reset();
+
+ // if there's a "current" touch from a previous event, copy it to "last"
+ if (m_touchPointer.current.valid())
+ m_touchPointer.last = m_touchPointer.current;
+
+ // set the "current" touch
+ m_touchPointer.current.x = static_cast<float>(point.x);
+ m_touchPointer.current.y = static_cast<float>(point.y);
+ m_touchPointer.current.time = time(nullptr);
+
+ switch (gi.dwID)
+ {
+ case GID_BEGIN:
+ {
+ // set the "down" touch
+ m_touchPointer.down = m_touchPointer.current;
+ m_originalZoomDistance = 0;
+
+ CGenericTouchActionHandler::GetInstance().OnTouchGestureStart(static_cast<float>(point.x), static_cast<float>(point.y));
+ }
+ break;
+
+ case GID_END:
+ CGenericTouchActionHandler::GetInstance().OnTouchGestureEnd(static_cast<float>(point.x), static_cast<float>(point.y), 0.0f, 0.0f, 0.0f, 0.0f);
+ break;
+
+ case GID_PAN:
+ {
+ if (!m_touchPointer.moving)
+ m_touchPointer.moving = true;
+
+ // calculate the velocity of the pan gesture
+ float velocityX, velocityY;
+ m_touchPointer.velocity(velocityX, velocityY);
+
+ CGenericTouchActionHandler::GetInstance().OnTouchGesturePan(m_touchPointer.current.x, m_touchPointer.current.y,
+ m_touchPointer.current.x - m_touchPointer.last.x, m_touchPointer.current.y - m_touchPointer.last.y,
+ velocityX, velocityY);
+
+ if (m_touchSwipeDetector != nullptr)
+ {
+ if (gi.dwFlags & GF_BEGIN)
+ {
+ m_touchPointer.down = m_touchPointer.current;
+ m_touchSwipeDetector->OnTouchDown(0, m_touchPointer);
+ }
+ else if (gi.dwFlags & GF_END)
+ {
+ m_touchSwipeDetector->OnTouchUp(0, m_touchPointer);
+
+ delete m_touchSwipeDetector;
+ m_touchSwipeDetector = nullptr;
+ }
+ else
+ m_touchSwipeDetector->OnTouchMove(0, m_touchPointer);
+ }
+ }
+ break;
+
+ case GID_ROTATE:
+ {
+ if (gi.dwFlags == GF_BEGIN)
+ break;
+
+ CGenericTouchActionHandler::GetInstance().OnRotate(static_cast<float>(point.x), static_cast<float>(point.y),
+ -static_cast<float>(ROTATE_ANGLE_DEGREE(gi.ullArguments)));
+ }
+ break;
+
+ case GID_ZOOM:
+ {
+ if (gi.dwFlags == GF_BEGIN)
+ {
+ m_originalZoomDistance = static_cast<int>(LODWORD(gi.ullArguments));
+ break;
+ }
+
+ // avoid division by 0
+ if (m_originalZoomDistance == 0)
+ break;
+
+ CGenericTouchActionHandler::GetInstance().OnZoomPinch(static_cast<float>(point.x), static_cast<float>(point.y),
+ static_cast<float>(LODWORD(gi.ullArguments)) / static_cast<float>(m_originalZoomDistance));
+ }
+ break;
+
+ case GID_TWOFINGERTAP:
+ CGenericTouchActionHandler::GetInstance().OnTap(static_cast<float>(point.x), static_cast<float>(point.y), 2);
+ break;
+
+ case GID_PRESSANDTAP:
+ default:
+ // You have encountered an unknown gesture
+ break;
+ }
+ if(DX::Windowing()->PtrCloseGestureInfoHandle)
+ DX::Windowing()->PtrCloseGestureInfoHandle(reinterpret_cast<HGESTUREINFO>(lParam));
+}
diff --git a/xbmc/windowing/windows/WinEventsWin32.h b/xbmc/windowing/windows/WinEventsWin32.h
new file mode 100644
index 0000000..466c755
--- /dev/null
+++ b/xbmc/windowing/windows/WinEventsWin32.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "input/touch/TouchTypes.h"
+#include "windowing/WinEvents.h"
+
+class CGenericTouchSwipeDetector;
+
+class CWinEventsWin32 : public IWinEvents
+{
+public:
+ bool MessagePump() override;
+ static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+
+private:
+ static void RegisterDeviceInterfaceToHwnd(GUID InterfaceClassGuid, HWND hWnd, HDEVNOTIFY *hDeviceNotify);
+ static void WindowFromScreenCoords(HWND hWnd, POINT *point);
+ static void OnGestureNotify(HWND hWnd, LPARAM lParam);
+ static void OnGesture(HWND hWnd, LPARAM lParam);
+
+ static int m_originalZoomDistance;
+ static Pointer m_touchPointer;
+ static CGenericTouchSwipeDetector *m_touchSwipeDetector;
+};
+
diff --git a/xbmc/windowing/windows/WinKeyMap.h b/xbmc/windowing/windows/WinKeyMap.h
new file mode 100644
index 0000000..c163747
--- /dev/null
+++ b/xbmc/windowing/windows/WinKeyMap.h
@@ -0,0 +1,181 @@
+/*
+ * 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 "ServiceBroker.h"
+#include "Util.h"
+#include "input/XBMC_keysym.h"
+#include "input/XBMC_vkeys.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+
+#include <array>
+
+namespace KODI
+{
+namespace WINDOWING
+{
+namespace WINDOWS
+{
+
+static std::array<XBMCKey, XBMCK_LAST> VK_keymap;
+
+static void DIB_InitOSKeymap()
+{
+ /* Map the VK keysyms */
+ VK_keymap.fill(XBMCK_UNKNOWN);
+
+ VK_keymap[VK_BACK] = XBMCK_BACKSPACE;
+ VK_keymap[VK_TAB] = XBMCK_TAB;
+ VK_keymap[VK_CLEAR] = XBMCK_CLEAR;
+ VK_keymap[VK_RETURN] = XBMCK_RETURN;
+ VK_keymap[VK_PAUSE] = XBMCK_PAUSE;
+ VK_keymap[VK_ESCAPE] = XBMCK_ESCAPE;
+ VK_keymap[VK_SPACE] = XBMCK_SPACE;
+ VK_keymap[VK_APOSTROPHE] = XBMCK_QUOTE;
+ VK_keymap[VK_COMMA] = XBMCK_COMMA;
+ VK_keymap[VK_MINUS] = XBMCK_MINUS;
+ VK_keymap[VK_PERIOD] = XBMCK_PERIOD;
+ VK_keymap[VK_SLASH] = XBMCK_SLASH;
+ VK_keymap[VK_0] = XBMCK_0;
+ VK_keymap[VK_1] = XBMCK_1;
+ VK_keymap[VK_2] = XBMCK_2;
+ VK_keymap[VK_3] = XBMCK_3;
+ VK_keymap[VK_4] = XBMCK_4;
+ VK_keymap[VK_5] = XBMCK_5;
+ VK_keymap[VK_6] = XBMCK_6;
+ VK_keymap[VK_7] = XBMCK_7;
+ VK_keymap[VK_8] = XBMCK_8;
+ VK_keymap[VK_9] = XBMCK_9;
+ VK_keymap[VK_SEMICOLON] = XBMCK_SEMICOLON;
+ VK_keymap[VK_EQUALS] = XBMCK_EQUALS;
+ VK_keymap[VK_LBRACKET] = XBMCK_LEFTBRACKET;
+ VK_keymap[VK_BACKSLASH] = XBMCK_BACKSLASH;
+ VK_keymap[VK_OEM_102] = XBMCK_BACKSLASH;
+ VK_keymap[VK_RBRACKET] = XBMCK_RIGHTBRACKET;
+ VK_keymap[VK_GRAVE] = XBMCK_BACKQUOTE;
+ VK_keymap[VK_BACKTICK] = XBMCK_BACKQUOTE;
+ VK_keymap[VK_A] = XBMCK_a;
+ VK_keymap[VK_B] = XBMCK_b;
+ VK_keymap[VK_C] = XBMCK_c;
+ VK_keymap[VK_D] = XBMCK_d;
+ VK_keymap[VK_E] = XBMCK_e;
+ VK_keymap[VK_F] = XBMCK_f;
+ VK_keymap[VK_G] = XBMCK_g;
+ VK_keymap[VK_H] = XBMCK_h;
+ VK_keymap[VK_I] = XBMCK_i;
+ VK_keymap[VK_J] = XBMCK_j;
+ VK_keymap[VK_K] = XBMCK_k;
+ VK_keymap[VK_L] = XBMCK_l;
+ VK_keymap[VK_M] = XBMCK_m;
+ VK_keymap[VK_N] = XBMCK_n;
+ VK_keymap[VK_O] = XBMCK_o;
+ VK_keymap[VK_P] = XBMCK_p;
+ VK_keymap[VK_Q] = XBMCK_q;
+ VK_keymap[VK_R] = XBMCK_r;
+ VK_keymap[VK_S] = XBMCK_s;
+ VK_keymap[VK_T] = XBMCK_t;
+ VK_keymap[VK_U] = XBMCK_u;
+ VK_keymap[VK_V] = XBMCK_v;
+ VK_keymap[VK_W] = XBMCK_w;
+ VK_keymap[VK_X] = XBMCK_x;
+ VK_keymap[VK_Y] = XBMCK_y;
+ VK_keymap[VK_Z] = XBMCK_z;
+ VK_keymap[VK_DELETE] = XBMCK_DELETE;
+
+ VK_keymap[VK_NUMPAD0] = XBMCK_KP0;
+ VK_keymap[VK_NUMPAD1] = XBMCK_KP1;
+ VK_keymap[VK_NUMPAD2] = XBMCK_KP2;
+ VK_keymap[VK_NUMPAD3] = XBMCK_KP3;
+ VK_keymap[VK_NUMPAD4] = XBMCK_KP4;
+ VK_keymap[VK_NUMPAD5] = XBMCK_KP5;
+ VK_keymap[VK_NUMPAD6] = XBMCK_KP6;
+ VK_keymap[VK_NUMPAD7] = XBMCK_KP7;
+ VK_keymap[VK_NUMPAD8] = XBMCK_KP8;
+ VK_keymap[VK_NUMPAD9] = XBMCK_KP9;
+ VK_keymap[VK_DECIMAL] = XBMCK_KP_PERIOD;
+ VK_keymap[VK_DIVIDE] = XBMCK_KP_DIVIDE;
+ VK_keymap[VK_MULTIPLY] = XBMCK_KP_MULTIPLY;
+ VK_keymap[VK_SUBTRACT] = XBMCK_KP_MINUS;
+ VK_keymap[VK_ADD] = XBMCK_KP_PLUS;
+
+ VK_keymap[VK_UP] = XBMCK_UP;
+ VK_keymap[VK_DOWN] = XBMCK_DOWN;
+ VK_keymap[VK_RIGHT] = XBMCK_RIGHT;
+ VK_keymap[VK_LEFT] = XBMCK_LEFT;
+ VK_keymap[VK_INSERT] = XBMCK_INSERT;
+ VK_keymap[VK_HOME] = XBMCK_HOME;
+ VK_keymap[VK_END] = XBMCK_END;
+ VK_keymap[VK_PRIOR] = XBMCK_PAGEUP;
+ VK_keymap[VK_NEXT] = XBMCK_PAGEDOWN;
+
+ VK_keymap[VK_F1] = XBMCK_F1;
+ VK_keymap[VK_F2] = XBMCK_F2;
+ VK_keymap[VK_F3] = XBMCK_F3;
+ VK_keymap[VK_F4] = XBMCK_F4;
+ VK_keymap[VK_F5] = XBMCK_F5;
+ VK_keymap[VK_F6] = XBMCK_F6;
+ VK_keymap[VK_F7] = XBMCK_F7;
+ VK_keymap[VK_F8] = XBMCK_F8;
+ VK_keymap[VK_F9] = XBMCK_F9;
+ VK_keymap[VK_F10] = XBMCK_F10;
+ VK_keymap[VK_F11] = XBMCK_F11;
+ VK_keymap[VK_F12] = XBMCK_F12;
+ VK_keymap[VK_F13] = XBMCK_F13;
+ VK_keymap[VK_F14] = XBMCK_F14;
+ VK_keymap[VK_F15] = XBMCK_F15;
+
+ VK_keymap[VK_NUMLOCK] = XBMCK_NUMLOCK;
+ VK_keymap[VK_CAPITAL] = XBMCK_CAPSLOCK;
+ VK_keymap[VK_SCROLL] = XBMCK_SCROLLOCK;
+ VK_keymap[VK_RSHIFT] = XBMCK_RSHIFT;
+ VK_keymap[VK_LSHIFT] = XBMCK_LSHIFT;
+ VK_keymap[VK_RCONTROL] = XBMCK_RCTRL;
+ VK_keymap[VK_LCONTROL] = XBMCK_LCTRL;
+ VK_keymap[VK_RMENU] = XBMCK_RALT;
+ VK_keymap[VK_LMENU] = XBMCK_LALT;
+ VK_keymap[VK_RWIN] = XBMCK_RSUPER;
+ VK_keymap[VK_LWIN] = XBMCK_LSUPER;
+
+ VK_keymap[VK_HELP] = XBMCK_HELP;
+#ifdef VK_PRINT
+ VK_keymap[VK_PRINT] = XBMCK_PRINT;
+#endif
+ VK_keymap[VK_SNAPSHOT] = XBMCK_PRINT;
+ VK_keymap[VK_CANCEL] = XBMCK_BREAK;
+ VK_keymap[VK_APPS] = XBMCK_MENU;
+
+ // Only include the multimedia keys if they have been enabled in the
+ // advanced settings
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_enableMultimediaKeys)
+ {
+ VK_keymap[VK_BROWSER_BACK] = XBMCK_BROWSER_BACK;
+ VK_keymap[VK_BROWSER_FORWARD] = XBMCK_BROWSER_FORWARD;
+ VK_keymap[VK_BROWSER_REFRESH] = XBMCK_BROWSER_REFRESH;
+ VK_keymap[VK_BROWSER_STOP] = XBMCK_BROWSER_STOP;
+ VK_keymap[VK_BROWSER_SEARCH] = XBMCK_BROWSER_SEARCH;
+ VK_keymap[VK_BROWSER_FAVORITES] = XBMCK_BROWSER_FAVORITES;
+ VK_keymap[VK_BROWSER_HOME] = XBMCK_BROWSER_HOME;
+ VK_keymap[VK_VOLUME_MUTE] = XBMCK_VOLUME_MUTE;
+ VK_keymap[VK_VOLUME_DOWN] = XBMCK_VOLUME_DOWN;
+ VK_keymap[VK_VOLUME_UP] = XBMCK_VOLUME_UP;
+ VK_keymap[VK_MEDIA_NEXT_TRACK] = XBMCK_MEDIA_NEXT_TRACK;
+ VK_keymap[VK_MEDIA_PREV_TRACK] = XBMCK_MEDIA_PREV_TRACK;
+ VK_keymap[VK_MEDIA_STOP] = XBMCK_MEDIA_STOP;
+ VK_keymap[VK_MEDIA_PLAY_PAUSE] = XBMCK_MEDIA_PLAY_PAUSE;
+ VK_keymap[VK_LAUNCH_MAIL] = XBMCK_LAUNCH_MAIL;
+ VK_keymap[VK_LAUNCH_MEDIA_SELECT] = XBMCK_LAUNCH_MEDIA_SELECT;
+ VK_keymap[VK_LAUNCH_APP1] = XBMCK_LAUNCH_APP1;
+ VK_keymap[VK_LAUNCH_APP2] = XBMCK_LAUNCH_APP2;
+ }
+}
+
+} // namespace WINDOWS
+} // namespace WINDOWING
+} // namespace KODI
diff --git a/xbmc/windowing/windows/WinSystemWin32.cpp b/xbmc/windowing/windows/WinSystemWin32.cpp
new file mode 100644
index 0000000..9394b4b
--- /dev/null
+++ b/xbmc/windowing/windows/WinSystemWin32.cpp
@@ -0,0 +1,1368 @@
+/*
+ * 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 "WinSystemWin32.h"
+
+#include "ServiceBroker.h"
+#include "VideoSyncD3D.h"
+#include "WIN32Util.h"
+#include "WinEventsWin32.h"
+#include "application/Application.h"
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Sinks/AESinkDirectSound.h"
+#include "cores/AudioEngine/Sinks/AESinkWASAPI.h"
+#include "filesystem/File.h"
+#include "filesystem/SpecialProtocol.h"
+#include "messaging/ApplicationMessenger.h"
+#include "platform/Environment.h"
+#include "rendering/dx/ScreenshotSurfaceWindows.h"
+#include "resource.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/windows/Win32DPMSSupport.h"
+
+#include "platform/win32/CharsetConverter.h"
+#include "platform/win32/input/IRServerSuite.h"
+
+#include <algorithm>
+#include <mutex>
+
+#include <tpcshrd.h>
+
+using namespace std::chrono_literals;
+
+const char* CWinSystemWin32::SETTING_WINDOW_TOP = "window.top";
+const char* CWinSystemWin32::SETTING_WINDOW_LEFT = "window.left";
+
+CWinSystemWin32::CWinSystemWin32()
+ : CWinSystemBase()
+ , PtrGetGestureInfo(nullptr)
+ , PtrSetGestureConfig(nullptr)
+ , PtrCloseGestureInfoHandle(nullptr)
+ , PtrEnableNonClientDpiScaling(nullptr)
+ , m_hWnd(nullptr)
+ , m_hMonitor(nullptr)
+ , m_hInstance(nullptr)
+ , m_hIcon(nullptr)
+ , m_ValidWindowedPosition(false)
+ , m_IsAlteringWindow(false)
+ , m_delayDispReset(false)
+ , m_state(WINDOW_STATE_WINDOWED)
+ , m_fullscreenState(WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW)
+ , m_windowState(WINDOW_WINDOW_STATE_WINDOWED)
+ , m_windowStyle(WINDOWED_STYLE)
+ , m_windowExStyle(WINDOWED_EX_STYLE)
+ , m_inFocus(false)
+ , m_bMinimized(false)
+{
+ std::string cacert = CEnvironment::getenv("SSL_CERT_FILE");
+ if (cacert.empty() || !XFILE::CFile::Exists(cacert))
+ {
+ cacert = CSpecialProtocol::TranslatePath("special://xbmc/system/certs/cacert.pem");
+ if (XFILE::CFile::Exists(cacert))
+ CEnvironment::setenv("SSL_CERT_FILE", cacert.c_str(), 1);
+ }
+
+ m_winEvents.reset(new CWinEventsWin32());
+ AE::CAESinkFactory::ClearSinks();
+ CAESinkDirectSound::Register();
+ CAESinkWASAPI::Register();
+ CScreenshotSurfaceWindows::Register();
+
+ if (CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bScanIRServer)
+ {
+ m_irss.reset(new CIRServerSuite());
+ m_irss->Initialize();
+ }
+ m_dpms = std::make_shared<CWin32DPMSSupport>();
+}
+
+CWinSystemWin32::~CWinSystemWin32()
+{
+ if (m_hIcon)
+ {
+ DestroyIcon(m_hIcon);
+ m_hIcon = nullptr;
+ }
+};
+
+bool CWinSystemWin32::InitWindowSystem()
+{
+ if(!CWinSystemBase::InitWindowSystem())
+ return false;
+
+ return true;
+}
+
+bool CWinSystemWin32::DestroyWindowSystem()
+{
+ if (m_hMonitor)
+ {
+ MONITOR_DETAILS* details = GetDisplayDetails(m_hMonitor);
+ if (details)
+ RestoreDesktopResolution(details);
+ }
+ return true;
+}
+
+bool CWinSystemWin32::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res)
+{
+ using KODI::PLATFORM::WINDOWS::ToW;
+ auto nameW = ToW(name);
+
+ m_hInstance = static_cast<HINSTANCE>(GetModuleHandle(nullptr));
+ if(m_hInstance == nullptr)
+ CLog::LogF(LOGDEBUG, " GetModuleHandle failed with {}", GetLastError());
+
+ // Load Win32 procs if available
+ HMODULE hUser32 = GetModuleHandle(L"user32");
+ if (hUser32)
+ {
+ PtrGetGestureInfo = reinterpret_cast<pGetGestureInfo>(GetProcAddress(hUser32, "GetGestureInfo"));
+ PtrSetGestureConfig = reinterpret_cast<pSetGestureConfig>(GetProcAddress(hUser32, "SetGestureConfig"));
+ PtrCloseGestureInfoHandle = reinterpret_cast<pCloseGestureInfoHandle>(GetProcAddress(hUser32, "CloseGestureInfoHandle"));
+ // if available, enable automatic DPI scaling of the non-client area portions of the window.
+ PtrEnableNonClientDpiScaling = reinterpret_cast<pEnableNonClientDpiScaling>(GetProcAddress(hUser32, "EnableNonClientDpiScaling"));
+ }
+
+ UpdateStates(fullScreen);
+ // initialize the state
+ WINDOW_STATE state = GetState(fullScreen);
+
+ m_nWidth = res.iWidth;
+ m_nHeight = res.iHeight;
+ m_bFullScreen = fullScreen;
+ m_fRefreshRate = res.fRefreshRate;
+
+ m_hIcon = LoadIcon(m_hInstance, MAKEINTRESOURCE(IDI_MAIN_ICON));
+
+ // Register the windows class
+ WNDCLASSEX wndClass = {};
+ wndClass.cbSize = sizeof(wndClass);
+ wndClass.style = CS_HREDRAW | CS_VREDRAW;
+ wndClass.lpfnWndProc = CWinEventsWin32::WndProc;
+ wndClass.cbClsExtra = 0;
+ wndClass.cbWndExtra = 0;
+ wndClass.hInstance = m_hInstance;
+ wndClass.hIcon = m_hIcon;
+ wndClass.hCursor = LoadCursor(nullptr, IDC_ARROW );
+ wndClass.hbrBackground = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
+ wndClass.lpszMenuName = nullptr;
+ wndClass.lpszClassName = nameW.c_str();
+
+ if( !RegisterClassExW( &wndClass ) )
+ {
+ CLog::LogF(LOGERROR, " RegisterClassExW failed with {}", GetLastError());
+ return false;
+ }
+
+ // put the window at desired display
+ RECT screenRect = ScreenRect(m_hMonitor);
+ m_nLeft = screenRect.left;
+ m_nTop = screenRect.top;
+
+ if (state == WINDOW_STATE_WINDOWED)
+ {
+ const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ const int top = settings->GetInt(SETTING_WINDOW_TOP);
+ const int left = settings->GetInt(SETTING_WINDOW_LEFT);
+ const RECT vsRect = GetVirtualScreenRect();
+
+ // we check that window is inside of virtual screen rect (sum of all monitors)
+ // top 0 left 0 is a special position that centers the window on the screen
+ if ((top != 0 || left != 0) && top >= vsRect.top && top + m_nHeight <= vsRect.bottom &&
+ left >= vsRect.left && left + m_nWidth <= vsRect.right)
+ {
+ // restore previous window position
+ m_nLeft = left;
+ m_nTop = top;
+ }
+ else
+ {
+ // Windowed mode: position and size in settings and most places in Kodi
+ // are for the client part of the window.
+ RECT rcWorkArea = GetScreenWorkArea(m_hMonitor);
+
+ int workAreaWidth = rcWorkArea.right - rcWorkArea.left;
+ int workAreaHeight = rcWorkArea.bottom - rcWorkArea.top;
+
+ RECT rcNcArea = GetNcAreaOffsets(m_windowStyle, false, m_windowExStyle);
+ int maxClientWidth = (rcWorkArea.right - rcNcArea.right) - (rcWorkArea.left - rcNcArea.left);
+ int maxClientHeight = (rcWorkArea.bottom - rcNcArea.bottom) - (rcWorkArea.top - rcNcArea.top);
+
+ m_nWidth = std::min(m_nWidth, maxClientWidth);
+ m_nHeight = std::min(m_nHeight, maxClientHeight);
+ CWinSystemBase::SetWindowResolution(m_nWidth, m_nHeight);
+
+ // center window on desktop
+ m_nLeft = rcWorkArea.left - rcNcArea.left + (maxClientWidth - m_nWidth) / 2;
+ m_nTop = rcWorkArea.top - rcNcArea.top + (maxClientHeight - m_nHeight) / 2;
+ }
+ m_ValidWindowedPosition = true;
+ }
+
+ HWND hWnd = CreateWindowExW(
+ m_windowExStyle,
+ nameW.c_str(),
+ nameW.c_str(),
+ m_windowStyle,
+ m_nLeft,
+ m_nTop,
+ m_nWidth,
+ m_nHeight,
+ nullptr,
+ nullptr,
+ m_hInstance,
+ nullptr
+ );
+
+ if( hWnd == nullptr )
+ {
+ CLog::LogF(LOGERROR, " CreateWindow failed with {}", GetLastError());
+ return false;
+ }
+
+ m_inFocus = true;
+
+ DWORD dwHwndTabletProperty =
+ TABLET_DISABLE_PENBARRELFEEDBACK | // disables UI feedback on pen button down (circle)
+ TABLET_DISABLE_FLICKS; // disables pen flicks (back, forward, drag down, drag up)
+
+ SetProp(hWnd, MICROSOFT_TABLETPENSERVICE_PROPERTY, &dwHwndTabletProperty);
+
+ m_hWnd = hWnd;
+ m_bWindowCreated = true;
+
+ CreateBlankWindows();
+
+ m_state = state;
+ AdjustWindow(true);
+
+ // Show the window
+ ShowWindow( m_hWnd, SW_SHOWDEFAULT );
+ UpdateWindow( m_hWnd );
+
+ // Configure the tray icon.
+ m_trayIcon.cbSize = sizeof(m_trayIcon);
+ m_trayIcon.hWnd = m_hWnd;
+ m_trayIcon.hIcon = m_hIcon;
+ wcsncpy(m_trayIcon.szTip, nameW.c_str(), sizeof(m_trayIcon.szTip) / sizeof(WCHAR));
+ m_trayIcon.uCallbackMessage = TRAY_ICON_NOTIFY;
+ m_trayIcon.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE;
+
+ return true;
+}
+
+bool CWinSystemWin32::CreateBlankWindows()
+{
+ WNDCLASSEX wcex;
+
+ wcex.cbSize = sizeof(WNDCLASSEX);
+ wcex.style= CS_HREDRAW | CS_VREDRAW;
+ wcex.lpfnWndProc= DefWindowProc;
+ wcex.cbClsExtra= 0;
+ wcex.cbWndExtra= 0;
+ wcex.hInstance= nullptr;
+ wcex.hIcon= nullptr;
+ wcex.hCursor= nullptr;
+ wcex.hbrBackground= static_cast<HBRUSH>(CreateSolidBrush(RGB(0, 0, 0)));
+ wcex.lpszMenuName= nullptr;
+ wcex.lpszClassName= L"BlankWindowClass";
+ wcex.hIconSm= nullptr;
+
+ // Now we can go ahead and register our new window class
+ if(!RegisterClassEx(&wcex))
+ {
+ CLog::LogF(LOGERROR, "RegisterClass failed with {}", GetLastError());
+ return false;
+ }
+
+ // We need as many blank windows as there are screens (minus 1)
+ for (size_t i = 0; i < m_displays.size() - 1; i++)
+ {
+ HWND hBlankWindow = CreateWindowEx(WS_EX_TOPMOST, L"BlankWindowClass", L"", WS_POPUP | WS_DISABLED,
+ CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, nullptr, nullptr);
+
+ if (hBlankWindow == nullptr)
+ {
+ CLog::LogF(LOGERROR, "CreateWindowEx failed with {}", GetLastError());
+ return false;
+ }
+
+ m_hBlankWindows.push_back(hBlankWindow);
+ }
+
+ return true;
+}
+
+bool CWinSystemWin32::BlankNonActiveMonitors(bool bBlank)
+{
+ if (m_hBlankWindows.empty())
+ return false;
+
+ if (bBlank == false)
+ {
+ for (unsigned int i=0; i < m_hBlankWindows.size(); i++)
+ ShowWindow(m_hBlankWindows[i], SW_HIDE);
+ return true;
+ }
+
+ // Move a blank window in front of every display, except the current display.
+ for (size_t i = 0, j = 0; i < m_displays.size(); ++i)
+ {
+ MONITOR_DETAILS& details = m_displays[i];
+ if (details.hMonitor == m_hMonitor)
+ continue;
+
+ RECT rBounds = ScreenRect(details.hMonitor);
+ // move and resize the window
+ SetWindowPos(m_hBlankWindows[j], nullptr, rBounds.left, rBounds.top,
+ rBounds.right - rBounds.left, rBounds.bottom - rBounds.top,
+ SWP_NOACTIVATE);
+
+ ShowWindow(m_hBlankWindows[j], SW_SHOW | SW_SHOWNOACTIVATE);
+ j++;
+ }
+
+ if (m_hWnd)
+ SetForegroundWindow(m_hWnd);
+
+ return true;
+}
+
+bool CWinSystemWin32::CenterWindow()
+{
+ RESOLUTION_INFO DesktopRes = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP);
+
+ m_nLeft = (DesktopRes.iWidth / 2) - (m_nWidth / 2);
+ m_nTop = (DesktopRes.iHeight / 2) - (m_nHeight / 2);
+
+ RECT rc;
+ rc.left = m_nLeft;
+ rc.top = m_nTop;
+ rc.right = rc.left + m_nWidth;
+ rc.bottom = rc.top + m_nHeight;
+ AdjustWindowRect( &rc, WS_OVERLAPPEDWINDOW, false );
+
+ SetWindowPos(m_hWnd, nullptr, rc.left, rc.top, 0, 0, SWP_NOSIZE);
+
+ return true;
+}
+
+bool CWinSystemWin32::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ m_nWidth = newWidth;
+ m_nHeight = newHeight;
+
+ if (newLeft > 0)
+ m_nLeft = newLeft;
+
+ if (newTop > 0)
+ m_nTop = newTop;
+
+ AdjustWindow();
+
+ return true;
+}
+
+void CWinSystemWin32::FinishWindowResize(int newWidth, int newHeight)
+{
+ m_nWidth = newWidth;
+ m_nHeight = newHeight;
+}
+
+void CWinSystemWin32::AdjustWindow(bool forceResize)
+{
+ CLog::LogF(LOGDEBUG, "adjusting window if required.");
+
+ HWND windowAfter;
+ RECT rc;
+
+ if (m_state == WINDOW_STATE_FULLSCREEN_WINDOW || m_state == WINDOW_STATE_FULLSCREEN)
+ {
+ windowAfter = HWND_TOP;
+ rc = ScreenRect(m_hMonitor);
+ }
+ else // m_state == WINDOW_STATE_WINDOWED
+ {
+ windowAfter = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_alwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST;
+
+ if (!m_ValidWindowedPosition)
+ {
+ const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ const int top = settings->GetInt(SETTING_WINDOW_TOP);
+ const int left = settings->GetInt(SETTING_WINDOW_LEFT);
+ const RECT vsRect = GetVirtualScreenRect();
+
+ // we check that window is inside of virtual screen rect (sum of all monitors)
+ // top 0 left 0 is a special position that centers the window on the screen
+ if ((top != 0 || left != 0) && top >= vsRect.top && top + m_nHeight <= vsRect.bottom &&
+ left >= vsRect.left && left + m_nWidth <= vsRect.right)
+ {
+ // restore previous window position
+ m_nTop = top;
+ m_nLeft = left;
+ }
+ else
+ {
+ // Windowed mode: position and size in settings and most places in Kodi
+ // are for the client part of the window.
+ RECT rcWorkArea = GetScreenWorkArea(m_hMonitor);
+
+ int workAreaWidth = rcWorkArea.right - rcWorkArea.left;
+ int workAreaHeight = rcWorkArea.bottom - rcWorkArea.top;
+
+ RECT rcNcArea = GetNcAreaOffsets(m_windowStyle, false, m_windowExStyle);
+ int maxClientWidth =
+ (rcWorkArea.right - rcNcArea.right) - (rcWorkArea.left - rcNcArea.left);
+ int maxClientHeight =
+ (rcWorkArea.bottom - rcNcArea.bottom) - (rcWorkArea.top - rcNcArea.top);
+
+ m_nWidth = std::min(m_nWidth, maxClientWidth);
+ m_nHeight = std::min(m_nHeight, maxClientHeight);
+ CWinSystemBase::SetWindowResolution(m_nWidth, m_nHeight);
+
+ // center window on desktop
+ m_nLeft = rcWorkArea.left - rcNcArea.left + (maxClientWidth - m_nWidth) / 2;
+ m_nTop = rcWorkArea.top - rcNcArea.top + (maxClientHeight - m_nHeight) / 2;
+ }
+ m_ValidWindowedPosition = true;
+ }
+
+ rc.left = m_nLeft;
+ rc.right = m_nLeft + m_nWidth;
+ rc.top = m_nTop;
+ rc.bottom = m_nTop + m_nHeight;
+
+ HMONITOR hMon = MonitorFromRect(&rc, MONITOR_DEFAULTTONULL);
+ HMONITOR hMon2 = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTOPRIMARY);
+
+ if (!m_ValidWindowedPosition || hMon == nullptr || hMon != hMon2)
+ {
+ // centering window at desktop
+ RECT newScreenRect = ScreenRect(hMon2);
+ rc.left = m_nLeft = newScreenRect.left + ((newScreenRect.right - newScreenRect.left) / 2) - (m_nWidth / 2);
+ rc.top = m_nTop = newScreenRect.top + ((newScreenRect.bottom - newScreenRect.top) / 2) - (m_nHeight / 2);
+ rc.right = m_nLeft + m_nWidth;
+ rc.bottom = m_nTop + m_nHeight;
+ m_ValidWindowedPosition = true;
+ }
+ AdjustWindowRectEx(&rc, m_windowStyle, false, m_windowExStyle);
+ }
+
+ WINDOWINFO wi;
+ wi.cbSize = sizeof(WINDOWINFO);
+ if (!GetWindowInfo(m_hWnd, &wi))
+ {
+ CLog::LogF(LOGERROR, "GetWindowInfo failed with {}", GetLastError());
+ return;
+ }
+ RECT wr = wi.rcWindow;
+
+ if ( wr.bottom - wr.top == rc.bottom - rc.top
+ && wr.right - wr.left == rc.right - rc.left
+ && (wi.dwStyle & WS_CAPTION) == (m_windowStyle & WS_CAPTION)
+ && !forceResize)
+ {
+ return;
+ }
+
+ //Sets the window style
+ SetLastError(0);
+ SetWindowLongPtr( m_hWnd, GWL_STYLE, m_windowStyle );
+
+ //Sets the window ex style
+ SetLastError(0);
+ SetWindowLongPtr( m_hWnd, GWL_EXSTYLE, m_windowExStyle );
+
+ // resize window
+ CLog::LogF(LOGDEBUG, "resizing due to size change ({},{},{},{}{})->({},{},{},{}{})", wr.left,
+ wr.top, wr.right, wr.bottom, (wi.dwStyle & WS_CAPTION) ? "" : " fullscreen", rc.left,
+ rc.top, rc.right, rc.bottom, (m_windowStyle & WS_CAPTION) ? "" : " fullscreen");
+ SetWindowPos(
+ m_hWnd,
+ windowAfter,
+ rc.left,
+ rc.top,
+ rc.right - rc.left,
+ rc.bottom - rc.top,
+ SWP_SHOWWINDOW | SWP_DRAWFRAME
+ );
+}
+
+void CWinSystemWin32::CenterCursor() const
+{
+ RECT rect;
+ POINT point = {};
+
+ //Gets the client rect, then translates it to screen coordinates
+ //so that SetCursorPos isn't called with relative x and y values
+ GetClientRect(m_hWnd, &rect);
+ ClientToScreen(m_hWnd, &point);
+
+ rect.left += point.x;
+ rect.right += point.x;
+ rect.top += point.y;
+ rect.bottom += point.y;
+
+ int x = rect.left + (rect.right - rect.left) / 2;
+ int y = rect.top + (rect.bottom - rect.top) / 2;
+
+ SetCursorPos(x, y);
+}
+
+bool CWinSystemWin32::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ CWinSystemWin32::UpdateStates(fullScreen);
+ WINDOW_STATE state = GetState(fullScreen);
+
+ CLog::LogF(LOGDEBUG, "({}) with size {}x{}, refresh {:f}{}", window_state_names[state],
+ res.iWidth, res.iHeight, res.fRefreshRate,
+ (res.dwFlags & D3DPRESENTFLAG_INTERLACED) ? "i" : "");
+
+ // oldMonitor may be NULL if it's powered off or not available due windows settings
+ MONITOR_DETAILS* oldMonitor = GetDisplayDetails(m_hMonitor);
+ MONITOR_DETAILS* newMonitor = GetDisplayDetails(res.strOutput);
+
+ bool forceChange = false; // resolution/display is changed but window state isn't changed
+ bool changeScreen = false; // display is changed
+ bool stereoChange = IsStereoEnabled() != (CServiceBroker::GetWinSystem()->GetGfxContext().GetStereoMode() == RENDER_STEREO_MODE_HARDWAREBASED);
+
+ if (m_nWidth != res.iWidth || m_nHeight != res.iHeight || m_fRefreshRate != res.fRefreshRate ||
+ !oldMonitor || oldMonitor->hMonitor != newMonitor->hMonitor || stereoChange ||
+ m_bFirstResChange)
+ {
+ if (!oldMonitor || oldMonitor->hMonitor != newMonitor->hMonitor)
+ changeScreen = true;
+ forceChange = true;
+ }
+
+ if (state == m_state && !forceChange)
+ {
+ m_bBlankOtherDisplay = blankOtherDisplays;
+ BlankNonActiveMonitors(m_bBlankOtherDisplay);
+ return true;
+ }
+
+ // entering to stereo mode, limit resolution to 1080p@23.976
+ if (stereoChange && !IsStereoEnabled() && res.iWidth > 1280)
+ {
+ res = CDisplaySettings::GetInstance().GetResolutionInfo(
+ CResolutionUtils::ChooseBestResolution(24.f / 1.001f, 1920, 1080, true));
+ }
+
+ if (m_state == WINDOW_STATE_WINDOWED)
+ {
+ WINDOWINFO wi = {};
+ wi.cbSize = sizeof(WINDOWINFO);
+ if (GetWindowInfo(m_hWnd, &wi) && wi.rcClient.top > 0)
+ {
+ m_nLeft = wi.rcClient.left;
+ m_nTop = wi.rcClient.top;
+ m_ValidWindowedPosition = true;
+ }
+ }
+
+ m_IsAlteringWindow = true;
+ ReleaseBackBuffer();
+
+ if (changeScreen)
+ {
+ // before we changing display we have to leave exclusive mode on "old" display
+ if (m_state == WINDOW_STATE_FULLSCREEN)
+ SetDeviceFullScreen(false, res);
+
+ // restoring native resolution on "old" display
+ RestoreDesktopResolution(oldMonitor);
+
+ // notify about screen change (it may require recreate rendering device)
+ m_fRefreshRate = res.fRefreshRate; // use desired refresh for driver hook
+ OnScreenChange(newMonitor->hMonitor);
+ }
+
+ m_bFirstResChange = false;
+ m_bFullScreen = fullScreen;
+ m_hMonitor = newMonitor->hMonitor;
+ m_nWidth = res.iWidth;
+ m_nHeight = res.iHeight;
+ m_bBlankOtherDisplay = blankOtherDisplays;
+ m_fRefreshRate = res.fRefreshRate;
+
+ if (state == WINDOW_STATE_FULLSCREEN)
+ {
+ SetForegroundWindowInternal(m_hWnd);
+
+ m_state = state;
+ AdjustWindow(changeScreen);
+
+ // enter in exclusive mode, this will change resolution if we already in
+ SetDeviceFullScreen(true, res);
+ }
+ else if (m_state == WINDOW_STATE_FULLSCREEN || m_state == WINDOW_STATE_FULLSCREEN_WINDOW) // we're in fullscreen state now
+ {
+ // guess we are leaving exclusive mode, this will not an effect if we already not in
+ SetDeviceFullScreen(false, res);
+
+ if (state == WINDOW_STATE_WINDOWED) // go to a windowed state
+ {
+ // need to restore resolution if it was changed to not native
+ // because we do not support resolution change in windowed mode
+ RestoreDesktopResolution(newMonitor);
+ }
+ else if (state == WINDOW_STATE_FULLSCREEN_WINDOW) // enter fullscreen window instead
+ {
+ ChangeResolution(res, stereoChange);
+ }
+
+ m_state = state;
+ AdjustWindow(changeScreen);
+ }
+ else // we're in windowed state now
+ {
+ if (state == WINDOW_STATE_FULLSCREEN_WINDOW)
+ {
+ ChangeResolution(res, stereoChange);
+
+ m_state = state;
+ AdjustWindow(changeScreen);
+ }
+ }
+
+ if (changeScreen)
+ CenterCursor();
+
+ CreateBackBuffer();
+
+ BlankNonActiveMonitors(m_bBlankOtherDisplay);
+ m_IsAlteringWindow = false;
+
+ return true;
+}
+
+bool CWinSystemWin32::DPIChanged(WORD dpi, RECT windowRect) const
+{
+ (void)dpi;
+ RECT resizeRect = windowRect;
+ HMONITOR hMon = MonitorFromRect(&resizeRect, MONITOR_DEFAULTTONULL);
+ if (hMon == nullptr)
+ {
+ hMon = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTOPRIMARY);
+ }
+
+ if (hMon)
+ {
+ MONITORINFOEX monitorInfo;
+ monitorInfo.cbSize = sizeof(MONITORINFOEX);
+ GetMonitorInfoW(hMon, &monitorInfo);
+
+ if (m_state == WINDOW_STATE_FULLSCREEN_WINDOW ||
+ m_state == WINDOW_STATE_FULLSCREEN)
+ {
+ resizeRect = monitorInfo.rcMonitor; // the whole screen
+ }
+ else
+ {
+ RECT wr = monitorInfo.rcWork; // it excludes task bar
+ long wrWidth = wr.right - wr.left;
+ long wrHeight = wr.bottom - wr.top;
+ long resizeWidth = resizeRect.right - resizeRect.left;
+ long resizeHeight = resizeRect.bottom - resizeRect.top;
+
+ if (resizeWidth > wrWidth)
+ {
+ resizeRect.right = resizeRect.left + wrWidth;
+ }
+
+ // make sure suggested windows size is not taller or wider than working area of new monitor (considers the toolbar)
+ if (resizeHeight > wrHeight)
+ {
+ resizeRect.bottom = resizeRect.top + wrHeight;
+ }
+ }
+ }
+
+ // resize the window to the suggested size. Will generate a WM_SIZE event
+ SetWindowPos(m_hWnd,
+ nullptr,
+ resizeRect.left,
+ resizeRect.top,
+ resizeRect.right - resizeRect.left,
+ resizeRect.bottom - resizeRect.top,
+ SWP_NOZORDER | SWP_NOACTIVATE);
+
+ return true;
+}
+
+void CWinSystemWin32::SetMinimized(bool minimized)
+{
+ const auto advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
+
+ if (advancedSettings->m_minimizeToTray)
+ {
+ if (minimized)
+ {
+ Shell_NotifyIcon(NIM_ADD, &m_trayIcon);
+ ShowWindow(m_hWnd, SW_HIDE);
+ }
+ else
+ {
+ Shell_NotifyIcon(NIM_DELETE, &m_trayIcon);
+ ShowWindow(m_hWnd, SW_RESTORE);
+ }
+ }
+
+ m_bMinimized = minimized;
+}
+
+std::vector<std::string> CWinSystemWin32::GetConnectedOutputs()
+{
+ std::vector<std::string> outputs;
+
+ for (auto& display : m_displays)
+ {
+ outputs.emplace_back(KODI::PLATFORM::WINDOWS::FromW(display.MonitorNameW));
+ }
+
+ return outputs;
+}
+
+void CWinSystemWin32::RestoreDesktopResolution(MONITOR_DETAILS* details)
+{
+ if (!details)
+ return;
+
+ RESOLUTION_INFO info;
+ info.iWidth = details->ScreenWidth;
+ info.iHeight = details->ScreenHeight;
+ if ((details->RefreshRate + 1) % 24 == 0 || (details->RefreshRate + 1) % 30 == 0)
+ info.fRefreshRate = static_cast<float>(details->RefreshRate + 1) / 1.001f;
+ else
+ info.fRefreshRate = static_cast<float>(details->RefreshRate);
+ info.strOutput = KODI::PLATFORM::WINDOWS::FromW(details->DeviceNameW);
+ info.dwFlags = details->Interlaced ? D3DPRESENTFLAG_INTERLACED : 0;
+
+ CLog::LogF(LOGDEBUG, "restoring desktop resolution for '{}'.", KODI::PLATFORM::WINDOWS::FromW(details->MonitorNameW));
+ ChangeResolution(info);
+}
+
+MONITOR_DETAILS* CWinSystemWin32::GetDisplayDetails(const std::string& name)
+{
+ using KODI::PLATFORM::WINDOWS::ToW;
+
+ if (!name.empty() && name != "Default")
+ {
+ std::wstring nameW = ToW(name);
+ auto it = std::find_if(m_displays.begin(), m_displays.end(), [&nameW](MONITOR_DETAILS& m)
+ {
+ if (nameW[0] == '\\') // name is device name
+ return m.DeviceNameW == nameW;
+ return m.MonitorNameW == nameW;
+ });
+ if (it != m_displays.end())
+ return &(*it);
+ }
+
+ // fallback to primary
+ auto it = std::find_if(m_displays.begin(), m_displays.end(), [](MONITOR_DETAILS& m)
+ {
+ return m.IsPrimary;
+ });
+ if (it != m_displays.end())
+ return &(*it);
+
+ // nothing found
+ return nullptr;
+}
+
+MONITOR_DETAILS* CWinSystemWin32::GetDisplayDetails(HMONITOR handle)
+{
+ auto it = std::find_if(m_displays.begin(), m_displays.end(), [&handle](MONITOR_DETAILS& m)
+ {
+ return m.hMonitor == handle;
+ });
+ if (it != m_displays.end())
+ return &(*it);
+
+ return nullptr;
+}
+
+RECT CWinSystemWin32::ScreenRect(HMONITOR handle)
+{
+ const MONITOR_DETAILS* details = GetDisplayDetails(handle);
+ if (!details)
+ {
+ CLog::LogF(LOGERROR, "no monitor found for handle");
+ return RECT();
+ }
+
+ DEVMODEW sDevMode = {};
+ sDevMode.dmSize = sizeof(sDevMode);
+ if(!EnumDisplaySettingsW(details->DeviceNameW.c_str(), ENUM_CURRENT_SETTINGS, &sDevMode))
+ CLog::LogF(LOGERROR, " EnumDisplaySettings failed with {}", GetLastError());
+
+ RECT rc;
+ rc.left = sDevMode.dmPosition.x;
+ rc.right = sDevMode.dmPosition.x + sDevMode.dmPelsWidth;
+ rc.top = sDevMode.dmPosition.y;
+ rc.bottom = sDevMode.dmPosition.y + sDevMode.dmPelsHeight;
+
+ return rc;
+}
+
+void CWinSystemWin32::GetConnectedDisplays(std::vector<MONITOR_DETAILS>& outputs)
+{
+ using KODI::PLATFORM::WINDOWS::FromW;
+
+ const POINT ptZero = { 0, 0 };
+ HMONITOR hmPrimary = MonitorFromPoint(ptZero, MONITOR_DEFAULTTOPRIMARY);
+
+ DISPLAY_DEVICEW ddAdapter = {};
+ ddAdapter.cb = sizeof(ddAdapter);
+
+ for (DWORD adapter = 0; EnumDisplayDevicesW(nullptr, adapter, &ddAdapter, 0); ++adapter)
+ {
+ // Exclude displays that are not part of the windows desktop. Using them is too different: no windows,
+ // direct access with GDI CreateDC() or DirectDraw for example. So it may be possible to play video, but GUI?
+ if ((ddAdapter.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER)
+ || !(ddAdapter.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP))
+ continue;
+
+ DISPLAY_DEVICEW ddMon = {};
+ ddMon.cb = sizeof(ddMon);
+ bool foundScreen = false;
+
+ DWORD screen = 0;
+ // Just look for the first active output, we're actually only interested in the information at the adapter level.
+ for (; EnumDisplayDevicesW(ddAdapter.DeviceName, screen, &ddMon, 0); ++screen)
+ {
+ if (ddMon.StateFlags & (DISPLAY_DEVICE_ACTIVE | DISPLAY_DEVICE_ATTACHED))
+ {
+ foundScreen = true;
+ break;
+ }
+ }
+
+ // Remoting returns no screens. Handle with a dummy screen.
+ if (!foundScreen && screen == 0)
+ {
+ lstrcpyW(ddMon.DeviceString, L"Dummy Monitor"); // safe: large static array
+ foundScreen = true;
+ }
+
+ if (foundScreen)
+ {
+ // get information about the display's current position and display mode
+ DEVMODEW dm = {};
+ dm.dmSize = sizeof(dm);
+ if (EnumDisplaySettingsExW(ddAdapter.DeviceName, ENUM_CURRENT_SETTINGS, &dm, 0) == FALSE)
+ EnumDisplaySettingsExW(ddAdapter.DeviceName, ENUM_REGISTRY_SETTINGS, &dm, 0);
+
+ POINT pt = { dm.dmPosition.x, dm.dmPosition.y };
+ HMONITOR hm = MonitorFromPoint(pt, MONITOR_DEFAULTTONULL);
+
+ MONITOR_DETAILS md = {};
+ uint8_t num = 1;
+ do
+ {
+ // `Monitor #N`
+ md.MonitorNameW = std::wstring(ddMon.DeviceString) + L" #" + std::to_wstring(num++);
+ }
+ while(std::any_of(outputs.begin(), outputs.end(), [&](MONITOR_DETAILS& m) { return m.MonitorNameW == md.MonitorNameW; }));
+
+ md.CardNameW = ddAdapter.DeviceString;
+ md.DeviceNameW = ddAdapter.DeviceName;
+ md.ScreenWidth = dm.dmPelsWidth;
+ md.ScreenHeight = dm.dmPelsHeight;
+ md.hMonitor = hm;
+ md.RefreshRate = dm.dmDisplayFrequency;
+ md.Bpp = dm.dmBitsPerPel;
+ md.Interlaced = (dm.dmDisplayFlags & DM_INTERLACED) ? true : false;
+
+ MONITORINFO mi = {};
+ mi.cbSize = sizeof(mi);
+ if (GetMonitorInfoW(hm, &mi) && (mi.dwFlags & MONITORINFOF_PRIMARY))
+ md.IsPrimary = true;
+
+ outputs.push_back(md);
+ }
+ }
+}
+
+bool CWinSystemWin32::ChangeResolution(const RESOLUTION_INFO& res, bool forceChange /*= false*/)
+{
+ using KODI::PLATFORM::WINDOWS::ToW;
+ std::wstring outputW = ToW(res.strOutput);
+
+ DEVMODEW sDevMode = {};
+ sDevMode.dmSize = sizeof(sDevMode);
+
+ // If we can't read the current resolution or any detail of the resolution is different than res
+ if (!EnumDisplaySettingsW(outputW.c_str(), ENUM_CURRENT_SETTINGS, &sDevMode) ||
+ sDevMode.dmPelsWidth != res.iWidth || sDevMode.dmPelsHeight != res.iHeight ||
+ sDevMode.dmDisplayFrequency != static_cast<int>(res.fRefreshRate) ||
+ ((sDevMode.dmDisplayFlags & DM_INTERLACED) && !(res.dwFlags & D3DPRESENTFLAG_INTERLACED)) ||
+ (!(sDevMode.dmDisplayFlags & DM_INTERLACED) && (res.dwFlags & D3DPRESENTFLAG_INTERLACED))
+ || forceChange)
+ {
+ ZeroMemory(&sDevMode, sizeof(sDevMode));
+ sDevMode.dmSize = sizeof(sDevMode);
+ sDevMode.dmDriverExtra = 0;
+ sDevMode.dmPelsWidth = res.iWidth;
+ sDevMode.dmPelsHeight = res.iHeight;
+ sDevMode.dmDisplayFrequency = static_cast<int>(res.fRefreshRate);
+ sDevMode.dmDisplayFlags = (res.dwFlags & D3DPRESENTFLAG_INTERLACED) ? DM_INTERLACED : 0;
+ sDevMode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
+
+ LONG rc;
+ bool bResChanged = false;
+
+ // Windows 8 refresh rate workaround for 24.0, 48.0 and 60.0 Hz
+ if ( CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin8)
+ && (res.fRefreshRate == 24.0 || res.fRefreshRate == 48.0 || res.fRefreshRate == 60.0))
+ {
+ CLog::LogF(LOGDEBUG, "Using Windows 8+ workaround for refresh rate {} Hz",
+ static_cast<int>(res.fRefreshRate));
+
+ // Get current resolution stored in registry
+ DEVMODEW sDevModeRegistry = {};
+ sDevModeRegistry.dmSize = sizeof(sDevModeRegistry);
+ if (EnumDisplaySettingsW(outputW.c_str(), ENUM_REGISTRY_SETTINGS, &sDevModeRegistry))
+ {
+ // Set requested mode in registry without actually changing resolution
+ rc = ChangeDisplaySettingsExW(outputW.c_str(), &sDevMode, nullptr, CDS_UPDATEREGISTRY | CDS_NORESET, nullptr);
+ if (rc == DISP_CHANGE_SUCCESSFUL)
+ {
+ // Change resolution based on registry setting
+ rc = ChangeDisplaySettingsExW(outputW.c_str(), nullptr, nullptr, CDS_FULLSCREEN, nullptr);
+ if (rc == DISP_CHANGE_SUCCESSFUL)
+ bResChanged = true;
+ else
+ CLog::LogF(
+ LOGERROR,
+ "ChangeDisplaySettingsEx (W8+ change resolution) failed with {}, using fallback",
+ rc);
+
+ // Restore registry with original values
+ sDevModeRegistry.dmSize = sizeof(sDevModeRegistry);
+ sDevModeRegistry.dmDriverExtra = 0;
+ sDevModeRegistry.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
+ rc = ChangeDisplaySettingsExW(outputW.c_str(), &sDevModeRegistry, nullptr, CDS_UPDATEREGISTRY | CDS_NORESET, nullptr);
+ if (rc != DISP_CHANGE_SUCCESSFUL)
+ CLog::LogF(LOGERROR, "ChangeDisplaySettingsEx (W8+ restore registry) failed with {}",
+ rc);
+ }
+ else
+ CLog::LogF(LOGERROR,
+ "ChangeDisplaySettingsEx (W8+ set registry) failed with {}, using fallback",
+ rc);
+ }
+ else
+ CLog::LogF(LOGERROR, "Unable to retrieve registry settings for Windows 8+ workaround, using fallback");
+ }
+
+ // Standard resolution change/fallback for Windows 8+ workaround
+ if (!bResChanged)
+ {
+ // CDS_FULLSCREEN is for temporary fullscreen mode and prevents icons and windows from moving
+ // to fit within the new dimensions of the desktop
+ rc = ChangeDisplaySettingsExW(outputW.c_str(), &sDevMode, nullptr, CDS_FULLSCREEN, nullptr);
+ if (rc == DISP_CHANGE_SUCCESSFUL)
+ bResChanged = true;
+ else
+ CLog::LogF(LOGERROR, "ChangeDisplaySettingsEx failed with {}", rc);
+ }
+
+ if (bResChanged)
+ ResolutionChanged();
+
+ return bResChanged;
+ }
+
+ // nothing to do, return success
+ return true;
+}
+
+void CWinSystemWin32::UpdateResolutions()
+{
+ using KODI::PLATFORM::WINDOWS::FromW;
+
+ m_displays.clear();
+
+ CWinSystemBase::UpdateResolutions();
+ GetConnectedDisplays(m_displays);
+
+ MONITOR_DETAILS* details = GetDisplayDetails(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR));
+ if (!details)
+ return;
+
+ float refreshRate;
+ int w = details->ScreenWidth;
+ int h = details->ScreenHeight;
+ if ((details->RefreshRate + 1) % 24 == 0 || (details->RefreshRate + 1) % 30 == 0)
+ refreshRate = static_cast<float>(details->RefreshRate + 1) / 1.001f;
+ else
+ refreshRate = static_cast<float>(details->RefreshRate);
+
+ std::string strOutput = FromW(details->DeviceNameW);
+ std::string monitorName = FromW(details->MonitorNameW);
+
+ uint32_t dwFlags = details->Interlaced ? D3DPRESENTFLAG_INTERLACED : 0;
+
+ RESOLUTION_INFO& info = CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP);
+ UpdateDesktopResolution(info, monitorName, w, h, refreshRate, dwFlags);
+ info.strOutput = strOutput;
+
+ CLog::Log(LOGINFO, "Primary mode: {}", info.strMode);
+
+ // erase previous stored modes
+ CDisplaySettings::GetInstance().ClearCustomResolutions();
+
+ for(int mode = 0;; mode++)
+ {
+ DEVMODEW devmode = {};
+ devmode.dmSize = sizeof(devmode);
+ if(EnumDisplaySettingsW(details->DeviceNameW.c_str(), mode, &devmode) == 0)
+ break;
+ if(devmode.dmBitsPerPel != 32)
+ continue;
+
+ float refresh;
+ if ((devmode.dmDisplayFrequency + 1) % 24 == 0 || (devmode.dmDisplayFrequency + 1) % 30 == 0)
+ refresh = static_cast<float>(devmode.dmDisplayFrequency + 1) / 1.001f;
+ else
+ refresh = static_cast<float>(devmode.dmDisplayFrequency);
+ dwFlags = (devmode.dmDisplayFlags & DM_INTERLACED) ? D3DPRESENTFLAG_INTERLACED : 0;
+
+ RESOLUTION_INFO res;
+ res.iWidth = devmode.dmPelsWidth;
+ res.iHeight = devmode.dmPelsHeight;
+ res.bFullScreen = true;
+ res.dwFlags = dwFlags;
+ res.fRefreshRate = refresh;
+ res.fPixelRatio = 1.0f;
+ res.iScreenWidth = res.iWidth;
+ res.iScreenHeight = res.iHeight;
+ res.iSubtitles = res.iHeight;
+ res.strMode = StringUtils::Format("{}: {}x{} @ {:.2f}Hz", monitorName, res.iWidth, res.iHeight,
+ res.fRefreshRate);
+ GetGfxContext().ResetOverscan(res);
+ res.strOutput = strOutput;
+
+ if (AddResolution(res))
+ CLog::Log(LOGINFO, "Additional mode: {}", res.strMode);
+ }
+
+ CDisplaySettings::GetInstance().ApplyCalibrations();
+}
+
+bool CWinSystemWin32::AddResolution(const RESOLUTION_INFO &res)
+{
+ for (unsigned int i = RES_CUSTOM; i < CDisplaySettings::GetInstance().ResolutionInfoSize(); i++)
+ {
+ RESOLUTION_INFO& info = CDisplaySettings::GetInstance().GetResolutionInfo(i);
+ if (info.iWidth == res.iWidth
+ && info.iHeight == res.iHeight
+ && info.iScreenWidth == res.iScreenWidth
+ && info.iScreenHeight == res.iScreenHeight
+ && info.fRefreshRate == res.fRefreshRate
+ && info.dwFlags == res.dwFlags)
+ return false; // already have this resolution
+ }
+
+ CDisplaySettings::GetInstance().AddResolutionInfo(res);
+ return true;
+}
+
+void CWinSystemWin32::ShowOSMouse(bool show)
+{
+ static int counter = 0;
+ if ((counter < 0 && show) || (counter >= 0 && !show))
+ counter = ShowCursor(show);
+}
+
+bool CWinSystemWin32::Minimize()
+{
+ ShowWindow(m_hWnd, SW_MINIMIZE);
+ return true;
+}
+bool CWinSystemWin32::Restore()
+{
+ ShowWindow(m_hWnd, SW_RESTORE);
+ return true;
+}
+bool CWinSystemWin32::Hide()
+{
+ ShowWindow(m_hWnd, SW_HIDE);
+ return true;
+}
+bool CWinSystemWin32::Show(bool raise)
+{
+ HWND windowAfter = HWND_BOTTOM;
+ if (raise)
+ {
+ if (m_bFullScreen)
+ windowAfter = HWND_TOP;
+ else
+ windowAfter = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_alwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST;
+ }
+
+ SetWindowPos(m_hWnd, windowAfter, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_ASYNCWINDOWPOS);
+ UpdateWindow(m_hWnd);
+
+ if (raise)
+ {
+ SetForegroundWindow(m_hWnd);
+ SetFocus(m_hWnd);
+ }
+ return true;
+}
+
+void CWinSystemWin32::Register(IDispResource *resource)
+{
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ m_resources.push_back(resource);
+}
+
+void CWinSystemWin32::Unregister(IDispResource* resource)
+{
+ std::unique_lock<CCriticalSection> 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 CWinSystemWin32::OnDisplayLost()
+{
+ CLog::LogF(LOGDEBUG, "notify display lost event");
+
+ {
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnLostDisplay();
+ }
+}
+
+void CWinSystemWin32::OnDisplayReset()
+{
+ if (!m_delayDispReset)
+ {
+ CLog::LogF(LOGDEBUG, "notify display reset event");
+ std::unique_lock<CCriticalSection> lock(m_resourceSection);
+ for (std::vector<IDispResource *>::iterator i = m_resources.begin(); i != m_resources.end(); ++i)
+ (*i)->OnResetDisplay();
+ }
+}
+
+void CWinSystemWin32::OnDisplayBack()
+{
+ auto delay =
+ std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ "videoscreen.delayrefreshchange") *
+ 100);
+ if (delay > 0ms)
+ {
+ m_delayDispReset = true;
+ m_dispResetTimer.Set(delay);
+ }
+ OnDisplayReset();
+}
+
+void CWinSystemWin32::ResolutionChanged()
+{
+ OnDisplayLost();
+ OnDisplayBack();
+}
+
+void CWinSystemWin32::SetForegroundWindowInternal(HWND hWnd)
+{
+ if (!IsWindow(hWnd)) return;
+
+ // if the window isn't focused, bring it to front or SetFullScreen will fail
+ BYTE keyState[256] = {};
+ // to unlock SetForegroundWindow we need to imitate Alt pressing
+ if (GetKeyboardState(reinterpret_cast<LPBYTE>(&keyState)) && !(keyState[VK_MENU] & 0x80))
+ keybd_event(VK_MENU, 0, KEYEVENTF_EXTENDEDKEY | 0, 0);
+
+ BOOL res = SetForegroundWindow(hWnd);
+
+ if (GetKeyboardState(reinterpret_cast<LPBYTE>(&keyState)) && !(keyState[VK_MENU] & 0x80))
+ keybd_event(VK_MENU, 0, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0);
+
+ if (!res)
+ {
+ //relation time of SetForegroundWindow lock
+ DWORD lockTimeOut = 0;
+ HWND hCurrWnd = GetForegroundWindow();
+ DWORD dwThisTID = GetCurrentThreadId(),
+ dwCurrTID = GetWindowThreadProcessId(hCurrWnd, nullptr);
+
+ // we need to bypass some limitations from Microsoft
+ if (dwThisTID != dwCurrTID)
+ {
+ AttachThreadInput(dwThisTID, dwCurrTID, TRUE);
+ SystemParametersInfo(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, &lockTimeOut, 0);
+ SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, nullptr, SPIF_SENDWININICHANGE | SPIF_UPDATEINIFILE);
+ AllowSetForegroundWindow(ASFW_ANY);
+ }
+
+ SetForegroundWindow(hWnd);
+
+ if (dwThisTID != dwCurrTID)
+ {
+ SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, &lockTimeOut, SPIF_SENDWININICHANGE | SPIF_UPDATEINIFILE);
+ AttachThreadInput(dwThisTID, dwCurrTID, FALSE);
+ }
+ }
+}
+
+std::unique_ptr<CVideoSync> CWinSystemWin32::GetVideoSync(void *clock)
+{
+ std::unique_ptr<CVideoSync> pVSync(new CVideoSyncD3D(clock));
+ return pVSync;
+}
+
+std::string CWinSystemWin32::GetClipboardText()
+{
+ std::wstring unicode_text;
+ std::string utf8_text;
+
+ if (OpenClipboard(nullptr))
+ {
+ HGLOBAL hglb = GetClipboardData(CF_UNICODETEXT);
+ if (hglb != nullptr)
+ {
+ LPWSTR lpwstr = static_cast<LPWSTR>(GlobalLock(hglb));
+ if (lpwstr != nullptr)
+ {
+ unicode_text = lpwstr;
+ GlobalUnlock(hglb);
+ }
+ }
+ CloseClipboard();
+ }
+
+ return KODI::PLATFORM::WINDOWS::FromW(unicode_text);
+}
+
+bool CWinSystemWin32::UseLimitedColor()
+{
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_LIMITEDRANGE);
+}
+
+void CWinSystemWin32::NotifyAppFocusChange(bool bGaining)
+{
+ if (m_state == WINDOW_STATE_FULLSCREEN && !m_IsAlteringWindow)
+ {
+ m_IsAlteringWindow = true;
+ ReleaseBackBuffer();
+
+ if (bGaining)
+ SetWindowPos(m_hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOREDRAW);
+
+ RESOLUTION_INFO res = {};
+ const RESOLUTION resolution = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution();
+ if (bGaining && resolution > RES_INVALID)
+ res = CDisplaySettings::GetInstance().GetResolutionInfo(resolution);
+
+ SetDeviceFullScreen(bGaining, res);
+
+ if (!bGaining)
+ SetWindowPos(m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOREDRAW);
+
+ CreateBackBuffer();
+ m_IsAlteringWindow = false;
+ }
+ m_inFocus = bGaining;
+}
+
+void CWinSystemWin32::UpdateStates(bool fullScreen)
+{
+ m_fullscreenState = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_VIDEOSCREEN_FAKEFULLSCREEN)
+ ? WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW
+ : WINDOW_FULLSCREEN_STATE_FULLSCREEN;
+ m_windowState = WINDOW_WINDOW_STATE_WINDOWED; // currently only this allowed
+
+ // set the appropriate window style
+ if (fullScreen)
+ {
+ m_windowStyle = FULLSCREEN_WINDOW_STYLE;
+ m_windowExStyle = FULLSCREEN_WINDOW_EX_STYLE;
+ }
+ else
+ {
+ m_windowStyle = WINDOWED_STYLE;
+ m_windowExStyle = WINDOWED_EX_STYLE;
+ }
+}
+
+WINDOW_STATE CWinSystemWin32::GetState(bool fullScreen) const
+{
+ return static_cast<WINDOW_STATE>(fullScreen ? m_fullscreenState : m_windowState);
+}
+
+bool CWinSystemWin32::MessagePump()
+{
+ return m_winEvents->MessagePump();
+}
+
+void CWinSystemWin32::SetTogglingHDR(bool toggling)
+{
+ if (toggling)
+ SetTimer(m_hWnd, ID_TIMER_HDR, 6000U, nullptr);
+
+ m_IsTogglingHDR = toggling;
+}
+
+RECT CWinSystemWin32::GetVirtualScreenRect()
+{
+ RECT rect = {};
+ rect.left = GetSystemMetrics(SM_XVIRTUALSCREEN);
+ rect.right = GetSystemMetrics(SM_CXVIRTUALSCREEN) + rect.left;
+ rect.top = GetSystemMetrics(SM_YVIRTUALSCREEN);
+ rect.bottom = GetSystemMetrics(SM_CYVIRTUALSCREEN) + rect.top;
+
+ return rect;
+}
+
+int CWinSystemWin32::GetGuiSdrPeakLuminance() const
+{
+ const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+
+ return settings->GetInt(CSettings::SETTING_VIDEOSCREEN_GUISDRPEAKLUMINANCE);
+}
+
+RECT CWinSystemWin32::GetScreenWorkArea(HMONITOR handle) const
+{
+ MONITORINFO monitorInfo{};
+ monitorInfo.cbSize = sizeof(MONITORINFO);
+ if (!GetMonitorInfoW(handle, &monitorInfo))
+ {
+ CLog::LogF(LOGERROR, "GetMonitorInfoW failed with {}", GetLastError());
+ return RECT();
+ }
+ return monitorInfo.rcWork;
+}
+
+RECT CWinSystemWin32::GetNcAreaOffsets(DWORD dwStyle, BOOL bMenu, DWORD dwExStyle) const
+{
+ RECT rcNcArea{};
+ SetRectEmpty(&rcNcArea);
+
+ if (!AdjustWindowRectEx(&rcNcArea, dwStyle, false, dwExStyle))
+ {
+ CLog::LogF(LOGERROR, "AdjustWindowRectEx failed with {}", GetLastError());
+ return RECT();
+ }
+ return rcNcArea;
+}
diff --git a/xbmc/windowing/windows/WinSystemWin32.h b/xbmc/windowing/windows/WinSystemWin32.h
new file mode 100644
index 0000000..0573295
--- /dev/null
+++ b/xbmc/windowing/windows/WinSystemWin32.h
@@ -0,0 +1,215 @@
+/*
+ * 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 "guilib/DispResource.h"
+#include "threads/CriticalSection.h"
+#include "threads/SystemClock.h"
+#include "windowing/WinSystem.h"
+
+#include <shellapi.h>
+#include <vector>
+
+static const DWORD WINDOWED_STYLE = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN;
+static const DWORD WINDOWED_EX_STYLE = NULL;
+static const DWORD FULLSCREEN_WINDOW_STYLE = WS_POPUP | WS_SYSMENU | WS_CLIPCHILDREN;
+static const DWORD FULLSCREEN_WINDOW_EX_STYLE = WS_EX_APPWINDOW;
+static const UINT ID_TIMER_HDR = 34U;
+
+/* Controls the way the window appears and behaves. */
+enum WINDOW_STATE
+{
+ WINDOW_STATE_FULLSCREEN = 1, // Exclusive fullscreen
+ WINDOW_STATE_FULLSCREEN_WINDOW, // Non-exclusive fullscreen window
+ WINDOW_STATE_WINDOWED, //Movable window with border
+ WINDOW_STATE_BORDERLESS //Non-movable window with no border
+};
+
+static const char* window_state_names[] =
+{
+ "unknown",
+ "true fullscreen",
+ "windowed fullscreen",
+ "windowed",
+ "borderless"
+};
+
+/* WINDOW_STATE restricted to fullscreen modes. */
+enum WINDOW_FULLSCREEN_STATE
+{
+ WINDOW_FULLSCREEN_STATE_FULLSCREEN = WINDOW_STATE_FULLSCREEN,
+ WINDOW_FULLSCREEN_STATE_FULLSCREEN_WINDOW = WINDOW_STATE_FULLSCREEN_WINDOW
+};
+
+/* WINDOW_STATE restricted to windowed modes. */
+enum WINDOW_WINDOW_STATE
+{
+ WINDOW_WINDOW_STATE_WINDOWED = WINDOW_STATE_WINDOWED,
+ WINDOW_WINDOW_STATE_BORDERLESS = WINDOW_STATE_BORDERLESS
+};
+
+struct MONITOR_DETAILS
+{
+ int ScreenWidth;
+ int ScreenHeight;
+ int RefreshRate;
+ int Bpp;
+ int DisplayId;
+ bool Interlaced;
+ bool IsPrimary;
+
+ HMONITOR hMonitor;
+ std::wstring MonitorNameW;
+ std::wstring CardNameW;
+ std::wstring DeviceNameW;
+};
+
+class CIRServerSuite;
+
+class CWinSystemWin32 : public CWinSystemBase
+{
+public:
+ CWinSystemWin32();
+ virtual ~CWinSystemWin32();
+
+ // CWinSystemBase overrides
+ bool InitWindowSystem() override;
+ bool DestroyWindowSystem() override;
+ bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override;
+ void FinishWindowResize(int newWidth, int newHeight) override;
+ void UpdateResolutions() override;
+ bool CenterWindow() override;
+ virtual void NotifyAppFocusChange(bool bGaining) override;
+ void ShowOSMouse(bool show) override;
+ bool HasInertialGestures() override { return true; }//if win32 has touchscreen - it uses the win32 gesture api for inertial scrolling
+ bool Minimize() override;
+ bool Restore() override;
+ bool Hide() override;
+ bool Show(bool raise = true) override;
+ std::string GetClipboardText() override;
+ bool UseLimitedColor() override;
+
+ // videosync
+ std::unique_ptr<CVideoSync> GetVideoSync(void *clock) override;
+
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+
+ std::vector<std::string> GetConnectedOutputs() override;
+
+ // CWinSystemWin32
+ HWND GetHwnd() const { return m_hWnd; }
+ bool IsAlteringWindow() const { return m_IsAlteringWindow; }
+ void SetAlteringWindow(bool altering) { m_IsAlteringWindow = altering; }
+ bool IsTogglingHDR() const { return m_IsTogglingHDR; }
+ void SetTogglingHDR(bool toggling);
+ virtual bool DPIChanged(WORD dpi, RECT windowRect) const;
+ bool IsMinimized() const { return m_bMinimized; }
+ void SetMinimized(bool minimized);
+ int GetGuiSdrPeakLuminance() const;
+
+ // touchscreen support
+ typedef BOOL(WINAPI *pGetGestureInfo)(HGESTUREINFO, PGESTUREINFO);
+ typedef BOOL(WINAPI *pSetGestureConfig)(HWND, DWORD, UINT, PGESTURECONFIG, UINT);
+ typedef BOOL(WINAPI *pCloseGestureInfoHandle)(HGESTUREINFO);
+ typedef BOOL(WINAPI *pEnableNonClientDpiScaling)(HWND);
+ pGetGestureInfo PtrGetGestureInfo;
+ pSetGestureConfig PtrSetGestureConfig;
+ pCloseGestureInfoHandle PtrCloseGestureInfoHandle;
+ pEnableNonClientDpiScaling PtrEnableNonClientDpiScaling;
+
+ void SetSizeMoveMode(bool mode) { m_bSizeMoveEnabled = mode; }
+ bool IsInSizeMoveMode() const { return m_bSizeMoveEnabled; }
+
+ // winevents override
+ bool MessagePump() override;
+
+protected:
+ bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override = 0;
+ virtual void UpdateStates(bool fullScreen);
+ WINDOW_STATE GetState(bool fullScreen) const;
+ virtual void SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res) = 0;
+ virtual void ReleaseBackBuffer() = 0;
+ virtual void CreateBackBuffer() = 0;
+ virtual void ResizeDeviceBuffers() = 0;
+ virtual bool IsStereoEnabled() = 0;
+ virtual void OnScreenChange(HMONITOR monitor) = 0;
+ virtual void AdjustWindow(bool forceResize = false);
+ void CenterCursor() const;
+
+ virtual void Register(IDispResource *resource);
+ virtual void Unregister(IDispResource *resource);
+
+ virtual bool ChangeResolution(const RESOLUTION_INFO& res, bool forceChange = false);
+ virtual bool CreateBlankWindows();
+ virtual bool BlankNonActiveMonitors(bool bBlank);
+ MONITOR_DETAILS* GetDisplayDetails(const std::string& name);
+ MONITOR_DETAILS* GetDisplayDetails(HMONITOR handle);
+ void RestoreDesktopResolution(MONITOR_DETAILS* details);
+ RECT ScreenRect(HMONITOR handle);
+ void GetConnectedDisplays(std::vector<MONITOR_DETAILS>& outputs);
+
+
+ /*!
+ \brief Adds a resolution to the list of resolutions if we don't already have it
+ \param res resolution to add.
+ */
+ static bool AddResolution(const RESOLUTION_INFO &res);
+
+ void OnDisplayLost();
+ void OnDisplayReset();
+ void OnDisplayBack();
+ void ResolutionChanged();
+ static void SetForegroundWindowInternal(HWND hWnd);
+ static RECT GetVirtualScreenRect();
+ /*!
+ * Retrieve the work area of the screen (exclude task bar and other occlusions)
+ */
+ RECT GetScreenWorkArea(HMONITOR handle) const;
+ /*!
+ * Retrieve size of the title bar and borders
+ * Add to coordinates to convert client coordinates to window coordinates
+ * Substract from coordinates to convert from window coordinates to client coordinates
+ */
+ RECT GetNcAreaOffsets(DWORD dwStyle, BOOL bMenu, DWORD dwExStyle) const;
+
+ HWND m_hWnd;
+ HMONITOR m_hMonitor;
+
+ std::vector<HWND> m_hBlankWindows;
+ HINSTANCE m_hInstance;
+ HICON m_hIcon;
+ bool m_ValidWindowedPosition;
+ bool m_IsAlteringWindow;
+ bool m_IsTogglingHDR{false};
+
+ CCriticalSection m_resourceSection;
+ std::vector<IDispResource*> m_resources;
+ bool m_delayDispReset;
+ XbmcThreads::EndTime<> m_dispResetTimer;
+
+ WINDOW_STATE m_state; // the state of the window
+ WINDOW_FULLSCREEN_STATE m_fullscreenState; // the state of the window when in fullscreen
+ WINDOW_WINDOW_STATE m_windowState; // the state of the window when in windowed
+ DWORD m_windowStyle; // the style of the window
+ DWORD m_windowExStyle; // the ex style of the window
+ bool m_inFocus;
+ bool m_bMinimized;
+ bool m_bSizeMoveEnabled = false;
+ bool m_bFirstResChange = true;
+ std::unique_ptr<CIRServerSuite> m_irss;
+ std::vector<MONITOR_DETAILS> m_displays;
+
+ NOTIFYICONDATA m_trayIcon = {};
+
+ static const char* SETTING_WINDOW_TOP;
+ static const char* SETTING_WINDOW_LEFT;
+};
+
+extern HWND g_hWnd;
+
diff --git a/xbmc/windowing/windows/WinSystemWin32DX.cpp b/xbmc/windowing/windows/WinSystemWin32DX.cpp
new file mode 100644
index 0000000..6d568fc
--- /dev/null
+++ b/xbmc/windowing/windows/WinSystemWin32DX.cpp
@@ -0,0 +1,447 @@
+/*
+ * 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 "WinSystemWin32DX.h"
+
+#include "commons/ilog.h"
+#include "rendering/dx/RenderContext.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/SystemInfo.h"
+#include "utils/XTimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/WindowSystemFactory.h"
+
+#include "platform/win32/CharsetConverter.h"
+#include "platform/win32/WIN32Util.h"
+
+#ifndef _M_X64
+#include "utils/SystemInfo.h"
+#endif
+#if _DEBUG
+#pragma comment(lib, "detoursd.lib")
+#else
+#pragma comment(lib, "detours.lib")
+#endif
+#pragma comment(lib, "dxgi.lib")
+#include <windows.h>
+#include <winnt.h>
+#include <winternl.h>
+#pragma warning(disable: 4091)
+#include <d3d10umddi.h>
+#pragma warning(default: 4091)
+#include <detours.h>
+
+using KODI::PLATFORM::WINDOWS::FromW;
+
+using namespace std::chrono_literals;
+
+// User Mode Driver hooks definitions
+void APIENTRY HookCreateResource(D3D10DDI_HDEVICE hDevice, const D3D10DDIARG_CREATERESOURCE* pResource, D3D10DDI_HRESOURCE hResource, D3D10DDI_HRTRESOURCE hRtResource);
+HRESULT APIENTRY HookCreateDevice(D3D10DDI_HADAPTER hAdapter, D3D10DDIARG_CREATEDEVICE* pCreateData);
+HRESULT APIENTRY HookOpenAdapter10_2(D3D10DDIARG_OPENADAPTER *pOpenData);
+static PFND3D10DDI_OPENADAPTER s_fnOpenAdapter10_2{ nullptr };
+static PFND3D10DDI_CREATEDEVICE s_fnCreateDeviceOrig{ nullptr };
+static PFND3D10DDI_CREATERESOURCE s_fnCreateResourceOrig{ nullptr };
+
+void CWinSystemWin32DX::Register()
+{
+ KODI::WINDOWING::CWindowSystemFactory::RegisterWindowSystem(CreateWinSystem);
+}
+
+std::unique_ptr<CWinSystemBase> CWinSystemWin32DX::CreateWinSystem()
+{
+ return std::make_unique<CWinSystemWin32DX>();
+}
+
+CWinSystemWin32DX::CWinSystemWin32DX() : CRenderSystemDX()
+ , m_hDriverModule(nullptr)
+{
+}
+
+CWinSystemWin32DX::~CWinSystemWin32DX()
+{
+}
+
+void CWinSystemWin32DX::PresentRenderImpl(bool rendered)
+{
+ if (rendered)
+ m_deviceResources->Present();
+
+ if (m_delayDispReset && m_dispResetTimer.IsTimePast())
+ {
+ m_delayDispReset = false;
+ OnDisplayReset();
+ }
+
+ if (!rendered)
+ KODI::TIME::Sleep(40ms);
+}
+
+bool CWinSystemWin32DX::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res)
+{
+ const MONITOR_DETAILS* monitor = GetDisplayDetails(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_VIDEOSCREEN_MONITOR));
+ if (!monitor)
+ return false;
+
+ m_hMonitor = monitor->hMonitor;
+ m_deviceResources = DX::DeviceResources::Get();
+ // setting monitor before creating window for proper hooking into a driver
+ m_deviceResources->SetMonitor(m_hMonitor);
+
+ return CWinSystemWin32::CreateNewWindow(name, fullScreen, res) && m_deviceResources->HasValidDevice();
+}
+
+void CWinSystemWin32DX::SetWindow(HWND hWnd) const
+{
+ m_deviceResources->SetWindow(hWnd);
+}
+
+bool CWinSystemWin32DX::DestroyRenderSystem()
+{
+ CRenderSystemDX::DestroyRenderSystem();
+
+ m_deviceResources->Release();
+ m_deviceResources.reset();
+ return true;
+}
+
+void CWinSystemWin32DX::SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res)
+{
+ if (m_deviceResources->SetFullScreen(fullScreen, res))
+ {
+ ResolutionChanged();
+ }
+}
+
+bool CWinSystemWin32DX::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ CWinSystemWin32::ResizeWindow(newWidth, newHeight, newLeft, newTop);
+ CRenderSystemDX::OnResize();
+
+ return true;
+}
+
+void CWinSystemWin32DX::OnMove(int x, int y)
+{
+ // do not handle moving at window creation because MonitorFromWindow
+ // returns default system monitor in case of m_hWnd is null
+ if (!m_hWnd)
+ return;
+
+ HMONITOR newMonitor = MonitorFromWindow(m_hWnd, MONITOR_DEFAULTTONEAREST);
+ if (newMonitor != m_hMonitor)
+ {
+ MONITOR_DETAILS* details = GetDisplayDetails(newMonitor);
+
+ if (!details)
+ return;
+
+ CDisplaySettings::GetInstance().SetMonitor(KODI::PLATFORM::WINDOWS::FromW(details->MonitorNameW));
+ m_deviceResources->SetMonitor(newMonitor);
+ m_hMonitor = newMonitor;
+ }
+
+ // Save window position if not fullscreen
+ if (!IsFullScreen() && (m_nLeft != x || m_nTop != y))
+ {
+ m_nLeft = x;
+ m_nTop = y;
+ const auto settings = CServiceBroker::GetSettingsComponent()->GetSettings();
+ settings->SetInt(SETTING_WINDOW_LEFT, x);
+ settings->SetInt(SETTING_WINDOW_TOP, y);
+ settings->Save();
+ }
+}
+
+bool CWinSystemWin32DX::DPIChanged(WORD dpi, RECT windowRect) const
+{
+ // on Win10 FCU the OS keeps window size exactly the same size as it was
+ if (CSysInfo::IsWindowsVersionAtLeast(CSysInfo::WindowsVersionWin10_1709))
+ return true;
+
+ m_deviceResources->SetDpi(dpi);
+ if (!IsAlteringWindow())
+ return __super::DPIChanged(dpi, windowRect);
+
+ return true;
+}
+
+void CWinSystemWin32DX::ReleaseBackBuffer()
+{
+ m_deviceResources->ReleaseBackBuffer();
+}
+
+void CWinSystemWin32DX::CreateBackBuffer()
+{
+ m_deviceResources->CreateBackBuffer();
+}
+
+void CWinSystemWin32DX::ResizeDeviceBuffers()
+{
+ m_deviceResources->ResizeBuffers();
+}
+
+bool CWinSystemWin32DX::IsStereoEnabled()
+{
+ return m_deviceResources->IsStereoEnabled();
+}
+
+void CWinSystemWin32DX::OnScreenChange(HMONITOR monitor)
+{
+ m_deviceResources->SetMonitor(monitor);
+}
+
+bool CWinSystemWin32DX::ChangeResolution(const RESOLUTION_INFO &res, bool forceChange)
+{
+ bool changed = CWinSystemWin32::ChangeResolution(res, forceChange);
+ // this is a try to fix FCU issue after changing resolution
+ if (m_deviceResources && changed)
+ m_deviceResources->ResizeBuffers();
+ return changed;
+}
+
+void CWinSystemWin32DX::OnResize(int width, int height)
+{
+ if (!m_IsAlteringWindow)
+ ReleaseBackBuffer();
+
+ m_deviceResources->SetLogicalSize(static_cast<float>(width), static_cast<float>(height));
+
+ if (!m_IsAlteringWindow)
+ CreateBackBuffer();
+}
+
+bool CWinSystemWin32DX::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ bool const result = CWinSystemWin32::SetFullScreen(fullScreen, res, blankOtherDisplays);
+ CRenderSystemDX::OnResize();
+ return result;
+}
+
+void CWinSystemWin32DX::UninitHooks()
+{
+ // uninstall
+ if (!s_fnOpenAdapter10_2)
+ return;
+
+ DetourTransactionBegin();
+ DetourUpdateThread(GetCurrentThread());
+ DetourDetach(reinterpret_cast<void**>(&s_fnOpenAdapter10_2), HookOpenAdapter10_2);
+ DetourTransactionCommit();
+ if (m_hDriverModule)
+ {
+ FreeLibrary(m_hDriverModule);
+ m_hDriverModule = nullptr;
+ }
+}
+
+void CWinSystemWin32DX::InitHooks(IDXGIOutput* pOutput)
+{
+ DXGI_OUTPUT_DESC outputDesc;
+ if (!pOutput || FAILED(pOutput->GetDesc(&outputDesc)))
+ return;
+
+ DISPLAY_DEVICEW displayDevice;
+ displayDevice.cb = sizeof(DISPLAY_DEVICEW);
+ DWORD adapter = 0;
+ bool deviceFound = false;
+
+ // delete exiting hooks.
+ if (s_fnOpenAdapter10_2)
+ UninitHooks();
+
+ // enum devices to find matched
+ while (EnumDisplayDevicesW(nullptr, adapter, &displayDevice, 0))
+ {
+ if (wcscmp(displayDevice.DeviceName, outputDesc.DeviceName) == 0)
+ {
+ deviceFound = true;
+ break;
+ }
+ adapter++;
+ }
+ if (!deviceFound)
+ return;
+
+ CLog::LogF(LOGDEBUG, "Hooking into UserModeDriver on device {}. ",
+ FromW(displayDevice.DeviceKey));
+ const wchar_t* keyName =
+#ifndef _M_X64
+ // on x64 system and x32 build use UserModeDriverNameWow key
+ CSysInfo::GetKernelBitness() == 64 ? keyName = L"UserModeDriverNameWow" :
+#endif // !_WIN64
+ L"UserModeDriverName";
+
+ DWORD dwType = REG_MULTI_SZ;
+ HKEY hKey = nullptr;
+ wchar_t value[1024];
+ DWORD valueLength = sizeof(value);
+ LSTATUS lstat;
+
+ // to void \Registry\Machine at the beginning, we use shifted pointer at 18
+ if (ERROR_SUCCESS == (lstat = RegOpenKeyExW(HKEY_LOCAL_MACHINE, displayDevice.DeviceKey + 18, 0, KEY_READ, &hKey))
+ && ERROR_SUCCESS == (lstat = RegQueryValueExW(hKey, keyName, nullptr, &dwType, (LPBYTE)&value, &valueLength)))
+ {
+ // 1. registry value has a list of drivers for each API with the following format: dx9\0dx10\0dx11\0dx12\0\0
+ // 2. we split the value by \0
+ std::vector<std::wstring> drivers;
+ const wchar_t* pValue = value;
+ while (*pValue)
+ {
+ drivers.push_back(std::wstring(pValue));
+ pValue += drivers.back().size() + 1;
+ }
+ // no entries in the registry
+ if (drivers.empty())
+ return;
+ // 3. we take only first three values (dx12 driver isn't needed if it exists ofc)
+ if (drivers.size() > 3)
+ drivers = std::vector<std::wstring>(drivers.begin(), drivers.begin() + 3);
+ // 4. and then iterate with reverse order to start iterate with the best candidate for d3d11 driver
+ for (auto it = drivers.rbegin(); it != drivers.rend(); ++it)
+ {
+ m_hDriverModule = LoadLibraryW(it->c_str());
+ if (m_hDriverModule != nullptr)
+ {
+ s_fnOpenAdapter10_2 = reinterpret_cast<PFND3D10DDI_OPENADAPTER>(GetProcAddress(m_hDriverModule, "OpenAdapter10_2"));
+ if (s_fnOpenAdapter10_2 != nullptr)
+ {
+ DetourTransactionBegin();
+ DetourUpdateThread(GetCurrentThread());
+ DetourAttach(reinterpret_cast<void**>(&s_fnOpenAdapter10_2), HookOpenAdapter10_2);
+ if (NO_ERROR == DetourTransactionCommit())
+ // install and activate hook into a driver
+ {
+ CLog::LogF(LOGDEBUG, "D3D11 hook installed and activated.");
+ break;
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, __FUNCTION__": Unable to install and activate D3D11 hook.");
+ s_fnOpenAdapter10_2 = nullptr;
+ FreeLibrary(m_hDriverModule);
+ m_hDriverModule = nullptr;
+ }
+ }
+ }
+ }
+ }
+
+ if (lstat != ERROR_SUCCESS)
+ CLog::LogF(LOGDEBUG, "error open registry key with error {}.", lstat);
+
+ if (hKey != nullptr)
+ RegCloseKey(hKey);
+}
+
+void CWinSystemWin32DX::FixRefreshRateIfNecessary(const D3D10DDIARG_CREATERESOURCE* pResource) const
+{
+ if (pResource && pResource->pPrimaryDesc)
+ {
+ float refreshRate = RATIONAL_TO_FLOAT(pResource->pPrimaryDesc->ModeDesc.RefreshRate);
+ if (refreshRate > 10.0f && refreshRate < 300.0f)
+ {
+ // interlaced
+ if (pResource->pPrimaryDesc->ModeDesc.ScanlineOrdering > DXGI_DDI_MODE_SCANLINE_ORDER_PROGRESSIVE)
+ refreshRate /= 2;
+
+ uint32_t refreshNum, refreshDen;
+ DX::GetRefreshRatio(static_cast<uint32_t>(floor(m_fRefreshRate)), &refreshNum, &refreshDen);
+ float diff = fabs(refreshRate - static_cast<float>(refreshNum) / static_cast<float>(refreshDen)) / refreshRate;
+ CLog::LogF(LOGDEBUG,
+ "refreshRate: {:0.4f}, desired: {:0.4f}, deviation: {:.5f}, fixRequired: {}, {}",
+ refreshRate, m_fRefreshRate, diff, (diff > 0.0005 && diff < 0.1) ? "yes" : "no",
+ pResource->pPrimaryDesc->Flags);
+ if (diff > 0.0005 && diff < 0.1)
+ {
+ pResource->pPrimaryDesc->ModeDesc.RefreshRate.Numerator = refreshNum;
+ pResource->pPrimaryDesc->ModeDesc.RefreshRate.Denominator = refreshDen;
+ if (pResource->pPrimaryDesc->ModeDesc.ScanlineOrdering > DXGI_DDI_MODE_SCANLINE_ORDER_PROGRESSIVE)
+ pResource->pPrimaryDesc->ModeDesc.RefreshRate.Numerator *= 2;
+ CLog::LogF(LOGDEBUG, "refreshRate fix applied -> {:0.3f}",
+ RATIONAL_TO_FLOAT(pResource->pPrimaryDesc->ModeDesc.RefreshRate));
+ }
+ }
+ }
+}
+
+void APIENTRY HookCreateResource(D3D10DDI_HDEVICE hDevice, const D3D10DDIARG_CREATERESOURCE* pResource, D3D10DDI_HRESOURCE hResource, D3D10DDI_HRTRESOURCE hRtResource)
+{
+ if (pResource && pResource->pPrimaryDesc)
+ {
+ DX::Windowing()->FixRefreshRateIfNecessary(pResource);
+ }
+ s_fnCreateResourceOrig(hDevice, pResource, hResource, hRtResource);
+}
+
+HRESULT APIENTRY HookCreateDevice(D3D10DDI_HADAPTER hAdapter, D3D10DDIARG_CREATEDEVICE* pCreateData)
+{
+ HRESULT hr = s_fnCreateDeviceOrig(hAdapter, pCreateData);
+ if (pCreateData->pDeviceFuncs->pfnCreateResource)
+ {
+ CLog::LogF(LOGDEBUG, "hook into pCreateData->pDeviceFuncs->pfnCreateResource");
+ s_fnCreateResourceOrig = pCreateData->pDeviceFuncs->pfnCreateResource;
+ pCreateData->pDeviceFuncs->pfnCreateResource = HookCreateResource;
+ }
+ return hr;
+}
+
+HRESULT APIENTRY HookOpenAdapter10_2(D3D10DDIARG_OPENADAPTER *pOpenData)
+{
+ HRESULT hr = s_fnOpenAdapter10_2(pOpenData);
+ if (pOpenData->pAdapterFuncs->pfnCreateDevice)
+ {
+ CLog::LogF(LOGDEBUG, "hook into pOpenData->pAdapterFuncs->pfnCreateDevice");
+ s_fnCreateDeviceOrig = pOpenData->pAdapterFuncs->pfnCreateDevice;
+ pOpenData->pAdapterFuncs->pfnCreateDevice = HookCreateDevice;
+ }
+ return hr;
+}
+
+bool CWinSystemWin32DX::IsHDRDisplay()
+{
+ return (CWIN32Util::GetWindowsHDRStatus() != HDR_STATUS::HDR_UNSUPPORTED);
+}
+
+HDR_STATUS CWinSystemWin32DX::GetOSHDRStatus()
+{
+ return CWIN32Util::GetWindowsHDRStatus();
+}
+
+HDR_STATUS CWinSystemWin32DX::ToggleHDR()
+{
+ return m_deviceResources->ToggleHDR();
+}
+
+bool CWinSystemWin32DX::IsHDROutput() const
+{
+ return m_deviceResources->IsHDROutput();
+}
+
+bool CWinSystemWin32DX::IsTransferPQ() const
+{
+ return m_deviceResources->IsTransferPQ();
+}
+
+void CWinSystemWin32DX::SetHdrMetaData(DXGI_HDR_METADATA_HDR10& hdr10) const
+{
+ m_deviceResources->SetHdrMetaData(hdr10);
+}
+
+void CWinSystemWin32DX::SetHdrColorSpace(const DXGI_COLOR_SPACE_TYPE colorSpace) const
+{
+ m_deviceResources->SetHdrColorSpace(colorSpace);
+}
+
+DEBUG_INFO_RENDER CWinSystemWin32DX::GetDebugInfo()
+{
+ return m_deviceResources->GetDebugInfo();
+}
diff --git a/xbmc/windowing/windows/WinSystemWin32DX.h b/xbmc/windowing/windows/WinSystemWin32DX.h
new file mode 100644
index 0000000..d3673de
--- /dev/null
+++ b/xbmc/windowing/windows/WinSystemWin32DX.h
@@ -0,0 +1,96 @@
+/*
+ * 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 "HDRStatus.h"
+#include "rendering/dx/RenderSystemDX.h"
+#include "windowing/windows/WinSystemWin32.h"
+
+struct D3D10DDIARG_CREATERESOURCE;
+
+class CWinSystemWin32DX : public CWinSystemWin32, public CRenderSystemDX
+{
+ friend interface DX::IDeviceNotify;
+public:
+ CWinSystemWin32DX();
+ ~CWinSystemWin32DX();
+
+ static void Register();
+ static std::unique_ptr<CWinSystemBase> CreateWinSystem();
+
+ // Implementation of CWinSystemBase via CWinSystemWin32
+ CRenderSystemBase *GetRenderSystem() override { return this; }
+ bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override;
+ bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+ void PresentRenderImpl(bool rendered) override;
+ bool DPIChanged(WORD dpi, RECT windowRect) const override;
+ void SetWindow(HWND hWnd) const;
+ bool DestroyRenderSystem() override;
+ void* GetHWContext() override { return m_deviceResources->GetD3DContext(); }
+
+ void UninitHooks();
+ void InitHooks(IDXGIOutput* pOutput);
+
+ void OnMove(int x, int y) override;
+ void OnResize(int width, int height);
+
+ /*!
+ \brief Register as a dependent of the DirectX Render System
+ Resources should call this on construction if they're dependent on the Render System
+ for survival. Any resources that registers will get callbacks on loss and reset of
+ device. In addition, callbacks for destruction and creation of the device are also called,
+ where any resources dependent on the DirectX device should be destroyed and recreated.
+ \sa Unregister, ID3DResource
+ */
+ void Register(ID3DResource *resource) const
+ {
+ m_deviceResources->Register(resource);
+ };
+ /*!
+ \brief Unregister as a dependent of the DirectX Render System
+ Resources should call this on destruction if they're a dependent on the Render System
+ \sa Register, ID3DResource
+ */
+ void Unregister(ID3DResource *resource) const
+ {
+ m_deviceResources->Unregister(resource);
+ };
+
+ void Register(IDispResource* resource) override { CWinSystemWin32::Register(resource); }
+ void Unregister(IDispResource* resource) override { CWinSystemWin32::Unregister(resource); }
+
+ void FixRefreshRateIfNecessary(const D3D10DDIARG_CREATERESOURCE* pResource) const;
+
+ // HDR OS/display override
+ bool IsHDRDisplay() override;
+ HDR_STATUS ToggleHDR() override;
+ HDR_STATUS GetOSHDRStatus() override;
+
+ // HDR support
+ bool IsHDROutput() const;
+ bool IsTransferPQ() const;
+ void SetHdrMetaData(DXGI_HDR_METADATA_HDR10& hdr10) const;
+ void SetHdrColorSpace(const DXGI_COLOR_SPACE_TYPE colorSpace) const;
+
+ // Get debug info from swapchain
+ DEBUG_INFO_RENDER GetDebugInfo() override;
+
+protected:
+ void SetDeviceFullScreen(bool fullScreen, RESOLUTION_INFO& res) override;
+ void ReleaseBackBuffer() override;
+ void CreateBackBuffer() override;
+ void ResizeDeviceBuffers() override;
+ bool IsStereoEnabled() override;
+ void OnScreenChange(HMONITOR monitor) override;
+ bool ChangeResolution(const RESOLUTION_INFO& res, bool forceChange = false) override;
+
+ HMODULE m_hDriverModule;
+};
+
diff --git a/xbmc/windows/CMakeLists.txt b/xbmc/windows/CMakeLists.txt
new file mode 100644
index 0000000..2fe465f
--- /dev/null
+++ b/xbmc/windows/CMakeLists.txt
@@ -0,0 +1,25 @@
+set(SOURCES GUIMediaWindow.cpp
+ GUIWindowDebugInfo.cpp
+ GUIWindowFileManager.cpp
+ GUIWindowHome.cpp
+ GUIWindowLoginScreen.cpp
+ GUIWindowPointer.cpp
+ GUIWindowScreensaver.cpp
+ GUIWindowScreensaverDim.cpp
+ GUIWindowSplash.cpp
+ GUIWindowStartup.cpp
+ GUIWindowSystemInfo.cpp)
+
+set(HEADERS GUIMediaWindow.h
+ GUIWindowDebugInfo.h
+ GUIWindowFileManager.h
+ GUIWindowHome.h
+ GUIWindowLoginScreen.h
+ GUIWindowPointer.h
+ GUIWindowScreensaver.h
+ GUIWindowScreensaverDim.h
+ GUIWindowSplash.h
+ GUIWindowStartup.h
+ GUIWindowSystemInfo.h)
+
+core_add_library(windows)
diff --git a/xbmc/windows/GUIMediaWindow.cpp b/xbmc/windows/GUIMediaWindow.cpp
new file mode 100644
index 0000000..b55cb17
--- /dev/null
+++ b/xbmc/windows/GUIMediaWindow.cpp
@@ -0,0 +1,2314 @@
+/*
+ * 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 "GUIMediaWindow.h"
+
+#include "ContextMenuManager.h"
+#include "FileItem.h"
+#include "FileItemListModification.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "PartyModeManager.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "addons/AddonManager.h"
+#include "addons/PluginSource.h"
+#include "addons/addoninfo/AddonType.h"
+#include "application/Application.h"
+#include "messaging/ApplicationMessenger.h"
+#if defined(TARGET_ANDROID)
+#include "platform/android/activity/XBMCApp.h"
+#endif
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "dialogs/GUIDialogMediaFilter.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogSmartPlaylistEditor.h"
+#include "filesystem/FileDirectoryFactory.h"
+#include "filesystem/MultiPathDirectory.h"
+#include "filesystem/PluginDirectory.h"
+#include "filesystem/SmartPlaylistDirectory.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIEditControl.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "network/Network.h"
+#include "playlists/PlayList.h"
+#include "profiles/ProfileManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "threads/IRunnable.h"
+#include "utils/FileUtils.h"
+#include "utils/LabelFormatter.h"
+#include "utils/SortUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+#include "view/GUIViewState.h"
+
+#include <inttypes.h>
+
+#define CONTROL_BTNVIEWASICONS 2
+#define CONTROL_BTNSORTBY 3
+#define CONTROL_BTNSORTASC 4
+#define CONTROL_BTN_FILTER 19
+
+#define CONTROL_LABELFILES 12
+
+#define PROPERTY_PATH_DB "path.db"
+#define PROPERTY_SORT_ORDER "sort.order"
+#define PROPERTY_SORT_ASCENDING "sort.ascending"
+
+#define PLUGIN_REFRESH_DELAY 200
+
+using namespace ADDON;
+using namespace KODI::MESSAGING;
+using namespace std::chrono_literals;
+
+namespace
+{
+class CGetDirectoryItems : public IRunnable
+{
+public:
+ CGetDirectoryItems(XFILE::CVirtualDirectory &dir, CURL &url, CFileItemList &items, bool useDir)
+ : m_dir(dir), m_url(url), m_items(items), m_useDir(useDir)
+ {
+ }
+
+ void Run() override
+ {
+ m_result = m_dir.GetDirectory(m_url, m_items, m_useDir, true);
+ }
+
+ void Cancel() override
+ {
+ m_dir.CancelDirectory();
+ }
+
+ bool m_result = false;
+
+protected:
+ XFILE::CVirtualDirectory &m_dir;
+ CURL m_url;
+ CFileItemList &m_items;
+ bool m_useDir;
+};
+}
+
+CGUIMediaWindow::CGUIMediaWindow(int id, const char *xmlFile)
+ : CGUIWindow(id, xmlFile)
+{
+ m_loadType = KEEP_IN_MEMORY;
+ m_vecItems = new CFileItemList;
+ m_unfilteredItems = new CFileItemList;
+ m_vecItems->SetPath("?");
+ m_iLastControl = -1;
+ m_canFilterAdvanced = false;
+
+ m_guiState.reset(CGUIViewState::GetViewState(GetID(), *m_vecItems));
+}
+
+CGUIMediaWindow::~CGUIMediaWindow()
+{
+ delete m_vecItems;
+ delete m_unfilteredItems;
+}
+
+bool CGUIMediaWindow::Load(TiXmlElement *pRootElement)
+{
+ bool retVal = CGUIWindow::Load(pRootElement);
+
+ if (!retVal)
+ return false;
+
+ // configure our view control
+ m_viewControl.Reset();
+ m_viewControl.SetParentWindow(GetID());
+ TiXmlElement *element = pRootElement->FirstChildElement("views");
+ if (element && element->FirstChild())
+ { // format is <views>50,29,51,95</views>
+ const std::string &allViews = element->FirstChild()->ValueStr();
+ std::vector<std::string> views = StringUtils::Split(allViews, ",");
+ for (std::vector<std::string>::const_iterator i = views.begin(); i != views.end(); ++i)
+ {
+ int controlID = atol(i->c_str());
+ CGUIControl *control = GetControl(controlID);
+ if (control && control->IsContainer())
+ m_viewControl.AddView(control);
+ }
+ }
+ m_viewControl.SetViewControlID(CONTROL_BTNVIEWASICONS);
+
+ return true;
+}
+
+void CGUIMediaWindow::OnWindowLoaded()
+{
+ SendMessage(GUI_MSG_SET_TYPE, CONTROL_BTN_FILTER, CGUIEditControl::INPUT_TYPE_FILTER);
+ CGUIWindow::OnWindowLoaded();
+ SetupShares();
+}
+
+void CGUIMediaWindow::OnWindowUnload()
+{
+ CGUIWindow::OnWindowUnload();
+ m_viewControl.Reset();
+}
+
+CFileItemPtr CGUIMediaWindow::GetCurrentListItem(int offset)
+{
+ int item = m_viewControl.GetSelectedItem();
+ if (!m_vecItems->Size() || item < 0)
+ return CFileItemPtr();
+ item = (item + offset) % m_vecItems->Size();
+ if (item < 0) item += m_vecItems->Size();
+ return m_vecItems->Get(item);
+}
+
+bool CGUIMediaWindow::OnAction(const CAction &action)
+{
+ if (action.GetID() == ACTION_PARENT_DIR)
+ {
+ GoParentFolder();
+ return true;
+ }
+
+ if (CGUIWindow::OnAction(action))
+ return true;
+
+ if (action.GetID() == ACTION_FILTER)
+ return Filter();
+
+ // live filtering
+ if (action.GetID() == ACTION_FILTER_CLEAR)
+ {
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, GetID(), 0, GUI_MSG_FILTER_ITEMS);
+ message.SetStringParam("");
+ OnMessage(message);
+ return true;
+ }
+
+ if (action.GetID() == ACTION_BACKSPACE)
+ {
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, GetID(), 0, GUI_MSG_FILTER_ITEMS, 2); // 2 for delete
+ OnMessage(message);
+ return true;
+ }
+
+ if (action.GetID() >= ACTION_FILTER_SMS2 && action.GetID() <= ACTION_FILTER_SMS9)
+ {
+ std::string filter = std::to_string(action.GetID() - ACTION_FILTER_SMS2 + 2);
+ CGUIMessage message(GUI_MSG_NOTIFY_ALL, GetID(), 0, GUI_MSG_FILTER_ITEMS, 1); // 1 for append
+ message.SetStringParam(filter);
+ OnMessage(message);
+ return true;
+ }
+
+ return false;
+}
+
+bool CGUIMediaWindow::OnBack(int actionID)
+{
+ CancelUpdateItems();
+
+ CURL filterUrl(m_strFilterPath);
+ if (actionID == ACTION_NAV_BACK &&
+ !m_vecItems->IsVirtualDirectoryRoot() &&
+ !URIUtils::PathEquals(m_vecItems->GetPath(), GetRootPath(), true) &&
+ (!URIUtils::PathEquals(m_vecItems->GetPath(), m_startDirectory, true) || (m_canFilterAdvanced && filterUrl.HasOption("filter"))))
+ {
+ if (GoParentFolder())
+ return true;
+ }
+ return CGUIWindow::OnBack(actionID);
+}
+
+bool CGUIMediaWindow::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ CancelUpdateItems();
+
+ m_iLastControl = GetFocusedControlID();
+ CGUIWindow::OnMessage(message);
+
+ // get rid of any active filtering
+ if (m_canFilterAdvanced)
+ {
+ m_canFilterAdvanced = false;
+ m_filter.Reset();
+ }
+ m_strFilterPath.clear();
+
+ // Call ClearFileItems() after our window has finished doing any WindowClose
+ // animations
+ ClearFileItems();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BTNVIEWASICONS)
+ {
+ // view as control could be a select button
+ int viewMode = 0;
+ const CGUIControl *control = GetControl(CONTROL_BTNVIEWASICONS);
+ if (control && control->GetControlType() != CGUIControl::GUICONTROL_BUTTON)
+ {
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_BTNVIEWASICONS);
+ OnMessage(msg);
+ viewMode = m_viewControl.GetViewModeNumber(msg.GetParam1());
+ }
+ else
+ viewMode = m_viewControl.GetNextViewMode();
+
+ if (m_guiState)
+ m_guiState->SaveViewAsControl(viewMode);
+
+ UpdateButtons();
+ return true;
+ }
+ else if (iControl == CONTROL_BTNSORTASC) // sort asc
+ {
+ if (m_guiState)
+ m_guiState->SetNextSortOrder();
+ UpdateFileList();
+ return true;
+ }
+ else if (iControl == CONTROL_BTNSORTBY) // sort by
+ {
+ if (m_guiState.get() && m_guiState->ChooseSortMethod())
+ UpdateFileList();
+ return true;
+ }
+ else if (iControl == CONTROL_BTN_FILTER)
+ return Filter(false);
+ else if (m_viewControl.HasControl(iControl)) // list/thumb control
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ int iAction = message.GetParam1();
+ if (iItem < 0) break;
+ if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK)
+ {
+ OnSelect(iItem);
+ }
+ else if (iAction == ACTION_CONTEXT_MENU || iAction == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ OnPopupMenu(iItem);
+ return true;
+ }
+ }
+ }
+ break;
+
+ case GUI_MSG_SETFOCUS:
+ {
+ if (m_viewControl.HasControl(message.GetControlId()) && m_viewControl.GetCurrentControl() != message.GetControlId())
+ {
+ m_viewControl.SetFocused();
+ return true;
+ }
+ }
+ break;
+
+ case GUI_MSG_NOTIFY_ALL:
+ { // Message is received even if this window is inactive
+ if (message.GetParam1() == GUI_MSG_WINDOW_RESET)
+ {
+ m_vecItems->SetPath("?");
+ return true;
+ }
+ else if ( message.GetParam1() == GUI_MSG_REFRESH_THUMBS )
+ {
+ for (int i = 0; i < m_vecItems->Size(); i++)
+ m_vecItems->Get(i)->FreeMemory(true);
+ break; // the window will take care of any info images
+ }
+ else if (message.GetParam1() == GUI_MSG_REMOVED_MEDIA)
+ {
+ if ((m_vecItems->IsVirtualDirectoryRoot() ||
+ m_vecItems->IsSourcesPath()) && IsActive())
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ Refresh();
+ m_viewControl.SetSelectedItem(iItem);
+ }
+ else if (m_vecItems->IsRemovable())
+ { // check that we have this removable share still
+ if (!m_rootDir.IsInSource(m_vecItems->GetPath()))
+ { // don't have this share any more
+ if (IsActive()) Update("");
+ else
+ {
+ m_history.ClearPathHistory();
+ m_vecItems->SetPath("");
+ }
+ }
+ }
+
+ return true;
+ }
+ else if (message.GetParam1()==GUI_MSG_UPDATE_SOURCES)
+ { // State of the sources changed, so update our view
+ if ((m_vecItems->IsVirtualDirectoryRoot() ||
+ m_vecItems->IsSourcesPath()) && IsActive())
+ {
+ if (m_vecItemsUpdating)
+ {
+ CLog::Log(LOGWARNING, "CGUIMediaWindow::OnMessage - updating in progress");
+ return true;
+ }
+ CUpdateGuard ug(m_vecItemsUpdating);
+ int iItem = m_viewControl.GetSelectedItem();
+ Refresh(true);
+ m_viewControl.SetSelectedItem(iItem);
+ }
+ return true;
+ }
+ else if (message.GetParam1()==GUI_MSG_UPDATE && IsActive())
+ {
+ if (m_vecItemsUpdating)
+ {
+ CLog::Log(LOGWARNING, "CGUIMediaWindow::OnMessage - updating in progress");
+ return true;
+ }
+ CUpdateGuard ug(m_vecItemsUpdating);
+ if (message.GetNumStringParams())
+ {
+ if (message.GetParam2()) // param2 is used for resetting the history
+ SetHistoryForPath(message.GetStringParam());
+
+ CFileItemList list(message.GetStringParam());
+ list.RemoveDiscCache(GetID());
+ Update(message.GetStringParam());
+ }
+ else
+ Refresh(true); // refresh the listing
+ }
+ else if (message.GetParam1()==GUI_MSG_UPDATE_ITEM && message.GetItem())
+ {
+ int flag = message.GetParam2();
+ CFileItemPtr newItem = std::static_pointer_cast<CFileItem>(message.GetItem());
+
+ if (IsActive() || (flag & GUI_MSG_FLAG_FORCE_UPDATE))
+ {
+ m_vecItems->UpdateItem(newItem.get());
+
+ if (flag & GUI_MSG_FLAG_UPDATE_LIST)
+ { // need the list updated as well
+ UpdateFileList();
+ }
+ }
+ else if (newItem)
+ { // need to remove the disc cache
+ CFileItemList items;
+ items.SetPath(URIUtils::GetDirectory(newItem->GetPath()));
+ if (newItem->HasProperty("cachefilename"))
+ {
+ // Use stored cache file name
+ std::string crcfile = newItem->GetProperty("cachefilename").asString();
+ items.RemoveDiscCacheCRC(crcfile);
+ }
+ else
+ // No stored cache file name, attempt using truncated item path as list path
+ items.RemoveDiscCache(GetID());
+ }
+ }
+ else if (message.GetParam1()==GUI_MSG_UPDATE_PATH)
+ {
+ if (IsActive())
+ {
+ if((message.GetStringParam() == m_vecItems->GetPath()) ||
+ (m_vecItems->IsMultiPath() && XFILE::CMultiPathDirectory::HasPath(m_vecItems->GetPath(), message.GetStringParam())))
+ Refresh();
+ }
+ }
+ else if (message.GetParam1() == GUI_MSG_FILTER_ITEMS && IsActive())
+ {
+ std::string filter = GetProperty("filter").asString();
+ // check if this is meant for advanced filtering
+ if (message.GetParam2() != 10)
+ {
+ if (message.GetParam2() == 1) // append
+ filter += message.GetStringParam();
+ else if (message.GetParam2() == 2)
+ { // delete
+ if (filter.size())
+ filter.erase(filter.size() - 1);
+ }
+ else
+ filter = message.GetStringParam();
+ }
+ OnFilterItems(filter);
+ UpdateButtons();
+ return true;
+ }
+ else
+ return CGUIWindow::OnMessage(message);
+
+ return true;
+ }
+ break;
+ case GUI_MSG_PLAYBACK_STARTED:
+ case GUI_MSG_PLAYBACK_ENDED:
+ case GUI_MSG_PLAYBACK_STOPPED:
+ case GUI_MSG_PLAYLIST_CHANGED:
+ case GUI_MSG_PLAYLISTPLAYER_STOPPED:
+ case GUI_MSG_PLAYLISTPLAYER_STARTED:
+ case GUI_MSG_PLAYLISTPLAYER_CHANGED:
+ { // send a notify all to all controls on this window
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, GetID(), 0, GUI_MSG_REFRESH_LIST);
+ OnMessage(msg);
+ break;
+ }
+ case GUI_MSG_CHANGE_VIEW_MODE:
+ {
+ int viewMode = 0;
+ if (message.GetParam1()) // we have an id
+ viewMode = m_viewControl.GetViewModeByID(message.GetParam1());
+ else if (message.GetParam2())
+ viewMode = m_viewControl.GetNextViewMode(message.GetParam2());
+
+ if (m_guiState)
+ m_guiState->SaveViewAsControl(viewMode);
+ UpdateButtons();
+ return true;
+ }
+ break;
+ case GUI_MSG_CHANGE_SORT_METHOD:
+ {
+ if (m_guiState)
+ {
+ if (message.GetParam1())
+ m_guiState->SetCurrentSortMethod(message.GetParam1());
+ else if (message.GetParam2())
+ m_guiState->SetNextSortMethod(message.GetParam2());
+ }
+ UpdateFileList();
+ return true;
+ }
+ break;
+ case GUI_MSG_CHANGE_SORT_DIRECTION:
+ {
+ if (m_guiState)
+ m_guiState->SetNextSortOrder();
+ UpdateFileList();
+ return true;
+ }
+ break;
+ case GUI_MSG_WINDOW_INIT:
+ {
+ if (m_vecItems->GetPath() == "?")
+ m_vecItems->SetPath("");
+
+ std::string dir = message.GetStringParam(0);
+ const std::string& ret = message.GetStringParam(1);
+ const std::string& swap = message.GetStringParam(message.GetNumStringParams() - 1);
+ const bool returning = StringUtils::EqualsNoCase(ret, "return");
+ const bool replacing = StringUtils::EqualsNoCase(swap, "replace");
+
+ if (!dir.empty())
+ {
+ // ensure our directory is valid
+ dir = GetStartFolder(dir);
+ bool resetHistory = false;
+ if (!returning || !URIUtils::PathEquals(dir, m_startDirectory, true))
+ { // we're not returning to the same path, so set our directory to the requested path
+ m_vecItems->SetPath(dir);
+ resetHistory = true;
+ }
+ else if (m_vecItems->GetPath().empty() && URIUtils::PathEquals(dir, m_startDirectory, true))
+ m_vecItems->SetPath(dir);
+
+ // check for network up
+ if (URIUtils::IsRemote(m_vecItems->GetPath()) && !WaitForNetwork())
+ {
+ m_vecItems->SetPath("");
+ resetHistory = true;
+ }
+ if (resetHistory)
+ {
+ m_vecItems->RemoveDiscCache(GetID());
+ // only compute the history for the provided path if "return" is not defined
+ // (otherwise the root level for the path will be added by default to the path history
+ // and we won't be able to move back to the path we came from)
+ if (!returning)
+ SetHistoryForPath(m_vecItems->GetPath());
+ }
+ }
+ if (message.GetParam1() != WINDOW_INVALID)
+ {
+ // if this is the first time to this window - make sure we set the root path
+ // if "return" is defined make sure we set the startDirectory to the directory we are
+ // moving to (so that we can move back to where we were onBack). If we are activating
+ // the same window but with a different path, do nothing - we are simply adding to the
+ // window history. Note that if the window is just being replaced, the start directory
+ // also needs to be set as the manager has just popped the previous window.
+ if (message.GetParam1() != message.GetParam2() || replacing)
+ m_startDirectory = returning ? dir : GetRootPath();
+ }
+ if (message.GetParam2() == PLUGIN_REFRESH_DELAY)
+ {
+ Refresh();
+ SetInitialVisibility();
+ RestoreControlStates();
+ SetInitialVisibility();
+ return true;
+ }
+ }
+ break;
+ }
+
+ return CGUIWindow::OnMessage(message);
+}
+
+/*!
+ * \brief Updates the states
+ *
+ * This updates the states (enable, disable, visible...) of the controls defined
+ * by this window.
+ *
+ * \note Override this function in a derived class to add new controls
+ */
+void CGUIMediaWindow::UpdateButtons()
+{
+ if (m_guiState)
+ {
+ // Update sorting controls
+ if (m_guiState->GetSortOrder() == SortOrderNone)
+ {
+ CONTROL_DISABLE(CONTROL_BTNSORTASC);
+ }
+ else
+ {
+ CONTROL_ENABLE(CONTROL_BTNSORTASC);
+ SET_CONTROL_SELECTED(GetID(), CONTROL_BTNSORTASC, m_guiState->GetSortOrder() != SortOrderAscending);
+ }
+
+ // Update list/thumb control
+ m_viewControl.SetCurrentView(m_guiState->GetViewAsControl());
+
+ // Update sort by button
+ if (!m_guiState->HasMultipleSortMethods())
+ CONTROL_DISABLE(CONTROL_BTNSORTBY);
+ else
+ CONTROL_ENABLE(CONTROL_BTNSORTBY);
+
+ std::string sortLabel = StringUtils::Format(
+ g_localizeStrings.Get(550), g_localizeStrings.Get(m_guiState->GetSortMethodLabel()));
+ SET_CONTROL_LABEL(CONTROL_BTNSORTBY, sortLabel);
+ }
+
+ std::string items =
+ StringUtils::Format("{} {}", m_vecItems->GetObjectCount(), g_localizeStrings.Get(127));
+ SET_CONTROL_LABEL(CONTROL_LABELFILES, items);
+
+ SET_CONTROL_LABEL2(CONTROL_BTN_FILTER, GetProperty("filter").asString());
+}
+
+void CGUIMediaWindow::ClearFileItems()
+{
+ m_viewControl.Clear();
+ m_vecItems->Clear();
+ m_unfilteredItems->Clear();
+}
+
+/*!
+ * \brief Sort file items
+ *
+ * This sorts file items based on the sort method and sort order provided by
+ * guiViewState.
+ */
+void CGUIMediaWindow::SortItems(CFileItemList &items)
+{
+ std::unique_ptr<CGUIViewState> guiState(CGUIViewState::GetViewState(GetID(), items));
+
+ if (guiState)
+ {
+ SortDescription sorting = guiState->GetSortMethod();
+ sorting.sortOrder = guiState->GetSortOrder();
+ // If the sort method is "sort by playlist" and we have a specific
+ // sort order available we can use the specified sort order to do the sorting
+ // We do this as the new SortBy methods are a superset of the SORT_METHOD methods, thus
+ // not all are available. This may be removed once SORT_METHOD_* have been replaced by
+ // SortBy.
+ if ((sorting.sortBy == SortByPlaylistOrder) && items.HasProperty(PROPERTY_SORT_ORDER))
+ {
+ SortBy sortBy = (SortBy)items.GetProperty(PROPERTY_SORT_ORDER).asInteger();
+ if (sortBy != SortByNone && sortBy != SortByPlaylistOrder && sortBy != SortByProgramCount)
+ {
+ sorting.sortBy = sortBy;
+ sorting.sortOrder = items.GetProperty(PROPERTY_SORT_ASCENDING).asBoolean() ? SortOrderAscending : SortOrderDescending;
+ sorting.sortAttributes = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_IGNORETHEWHENSORTING) ? SortAttributeIgnoreArticle : SortAttributeNone;
+
+ // if the sort order is descending, we need to switch the original sort order, as we assume
+ // in CGUIViewState::AddPlaylistOrder that SortByPlaylistOrder is ascending.
+ if (guiState->GetSortOrder() == SortOrderDescending)
+ sorting.sortOrder = sorting.sortOrder == SortOrderDescending ? SortOrderAscending : SortOrderDescending;
+ }
+ }
+
+ items.Sort(sorting);
+ }
+}
+
+/*!
+ * \brief Formats item labels
+ *
+ * This is based on the formatting provided by guiViewState.
+ */
+void CGUIMediaWindow::FormatItemLabels(CFileItemList &items, const LABEL_MASKS &labelMasks)
+{
+ CLabelFormatter fileFormatter(labelMasks.m_strLabelFile, labelMasks.m_strLabel2File);
+ CLabelFormatter folderFormatter(labelMasks.m_strLabelFolder, labelMasks.m_strLabel2Folder);
+ for (int i=0; i<items.Size(); ++i)
+ {
+ CFileItemPtr pItem=items[i];
+
+ if (pItem->IsLabelPreformatted())
+ continue;
+
+ if (pItem->m_bIsFolder)
+ folderFormatter.FormatLabels(pItem.get());
+ else
+ fileFormatter.FormatLabels(pItem.get());
+ }
+
+ if (items.GetSortMethod() == SortByLabel)
+ items.ClearSortState();
+}
+
+/*!
+ * \brief Format and sort file items
+ *
+ * Prepares and adds the fileitems to list/thumb panel
+ */
+void CGUIMediaWindow::FormatAndSort(CFileItemList &items)
+{
+ std::unique_ptr<CGUIViewState> viewState(CGUIViewState::GetViewState(GetID(), items));
+
+ if (viewState)
+ {
+ LABEL_MASKS labelMasks;
+ viewState->GetSortMethodLabelMasks(labelMasks);
+ FormatItemLabels(items, labelMasks);
+
+ items.Sort(viewState->GetSortMethod().sortBy, viewState->GetSortOrder(), viewState->GetSortMethod().sortAttributes);
+ }
+}
+
+/*!
+ * \brief Overwrite to fill fileitems from a source
+ *
+ * \param[in] strDirectory Path to read
+ * \param[out] items Fill with items specified in \e strDirectory
+ * \return false if given directory not present
+ */
+bool CGUIMediaWindow::GetDirectory(const std::string &strDirectory, CFileItemList &items)
+{
+ CURL pathToUrl(strDirectory);
+
+ std::string strParentPath = m_history.GetParentPath();
+
+ CLog::Log(LOGDEBUG, "CGUIMediaWindow::GetDirectory ({})", CURL::GetRedacted(strDirectory));
+ CLog::Log(LOGDEBUG, " ParentPath = [{}]", CURL::GetRedacted(strParentPath));
+
+ if (pathToUrl.IsProtocol("plugin") && !pathToUrl.GetHostName().empty())
+ CServiceBroker::GetAddonMgr().UpdateLastUsed(pathToUrl.GetHostName());
+
+ // see if we can load a previously cached folder
+ CFileItemList cachedItems(strDirectory);
+ if (!strDirectory.empty() && cachedItems.Load(GetID()))
+ {
+ items.Assign(cachedItems);
+ }
+ else
+ {
+ auto start = std::chrono::steady_clock::now();
+
+ if (strDirectory.empty())
+ SetupShares();
+
+ CFileItemList dirItems;
+ if (!GetDirectoryItems(pathToUrl, dirItems, UseFileDirectories()))
+ return false;
+
+ // assign fetched directory items
+ items.Assign(dirItems);
+
+ // took over a second, and not normally cached, so cache it
+ auto end = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+
+ if (duration.count() > 1000 && items.CacheToDiscIfSlow())
+ items.Save(GetID());
+
+ // if these items should replace the current listing, then pop it off the top
+ if (items.GetReplaceListing())
+ m_history.RemoveParentPath();
+ }
+
+ // update the view state's reference to the current items
+ m_guiState.reset(CGUIViewState::GetViewState(GetID(), items));
+
+ bool bHideParent = false;
+
+ if (m_guiState && m_guiState->HideParentDirItems())
+ bHideParent = true;
+ if (items.GetPath() == GetRootPath())
+ bHideParent = true;
+
+ if (!bHideParent)
+ {
+ CFileItemPtr pItem(new CFileItem(".."));
+ pItem->SetPath(strParentPath);
+ pItem->m_bIsFolder = true;
+ pItem->m_bIsShareOrDrive = false;
+ items.AddFront(pItem, 0);
+ }
+
+ int iWindow = GetID();
+ std::vector<std::string> regexps;
+
+ //! @todo Do we want to limit the directories we apply the video ones to?
+ if (iWindow == WINDOW_VIDEO_NAV)
+ regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_videoExcludeFromListingRegExps;
+ if (iWindow == WINDOW_MUSIC_NAV)
+ regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_audioExcludeFromListingRegExps;
+ if (iWindow == WINDOW_PICTURES)
+ regexps = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_pictureExcludeFromListingRegExps;
+
+ if (regexps.size())
+ {
+ for (int i=0; i < items.Size();)
+ {
+ if (CUtil::ExcludeFileOrFolder(items[i]->GetPath(), regexps))
+ items.Remove(i);
+ else
+ i++;
+ }
+ }
+
+ // clear the filter
+ SetProperty("filter", "");
+ m_canFilterAdvanced = false;
+ m_filter.Reset();
+ return true;
+}
+
+bool CGUIMediaWindow::Update(const std::string &strDirectory, bool updateFilterPath /* = true */)
+{
+ //! @todo OnInitWindow calls Update() before window path has been set properly.
+ if (strDirectory == "?")
+ return false;
+
+ // The path to load. Empty string is used in various places to denote root, so translate to the
+ // real root path first
+ const std::string path = strDirectory.empty() ? GetRootPath() : strDirectory;
+
+ // stores the selected item in history
+ SaveSelectedItemInHistory();
+
+ const std::string previousPath = m_vecItems->GetPath();
+
+ // check if the path contains a filter and temporarily remove it
+ // so that the retrieved list of items is unfiltered
+ std::string pathNoFilter = path;
+ if (CanContainFilter(pathNoFilter) && CURL(pathNoFilter).HasOption("filter"))
+ pathNoFilter = RemoveParameterFromPath(pathNoFilter, "filter");
+
+ if (!GetDirectory(pathNoFilter, *m_vecItems))
+ {
+ CLog::Log(LOGERROR, "CGUIMediaWindow::GetDirectory({}) failed", CURL(path).GetRedacted());
+
+ if (URIUtils::PathEquals(path, GetRootPath()))
+ return false; // Nothing to fallback to
+
+ // Try to return to the previous directory, if not the same
+ // else fallback to root
+ if (URIUtils::PathEquals(path, previousPath) || !Update(m_history.RemoveParentPath()))
+ Update(""); // Fallback to root
+
+ // Return false to be able to eg. show
+ // an error message.
+ return false;
+ }
+
+ if (m_vecItems->GetLabel().empty())
+ {
+ // Removable sources
+ VECSOURCES removables;
+ CServiceBroker::GetMediaManager().GetRemovableDrives(removables);
+ for (const auto& s : removables)
+ {
+ if (URIUtils::CompareWithoutSlashAtEnd(s.strPath, m_vecItems->GetPath()))
+ {
+ m_vecItems->SetLabel(s.strName);
+ break;
+ }
+ }
+ }
+
+ if (m_vecItems->GetLabel().empty())
+ m_vecItems->SetLabel(CUtil::GetTitleFromPath(m_vecItems->GetPath(), true));
+
+ // check the given path for filter data
+ UpdateFilterPath(path, *m_vecItems, updateFilterPath);
+
+ // if we're getting the root source listing
+ // make sure the path history is clean
+ if (URIUtils::PathEquals(path, GetRootPath()))
+ m_history.ClearPathHistory();
+
+ int iWindow = GetID();
+ int showLabel = 0;
+ if (URIUtils::PathEquals(path, GetRootPath()))
+ {
+ if (iWindow == WINDOW_PICTURES)
+ showLabel = 997;
+ else if (iWindow == WINDOW_FILES)
+ showLabel = 1026;
+ else if (iWindow == WINDOW_GAMES)
+ showLabel = 35250; // "Add games..."
+ }
+ if (m_vecItems->IsPath("sources://video/"))
+ showLabel = 999;
+ else if (m_vecItems->IsPath("sources://music/"))
+ showLabel = 998;
+ else if (m_vecItems->IsPath("sources://pictures/"))
+ showLabel = 997;
+ else if (m_vecItems->IsPath("sources://files/"))
+ showLabel = 1026;
+ else if (m_vecItems->IsPath("sources://games/"))
+ showLabel = 35250; // "Add games..."
+ // Add 'Add source ' item
+ if (showLabel && (m_vecItems->Size() == 0 || !m_guiState->DisableAddSourceButtons()) &&
+ iWindow != WINDOW_MUSIC_PLAYLIST_EDITOR)
+ {
+ const std::string& strLabel = g_localizeStrings.Get(showLabel);
+ CFileItemPtr pItem(new CFileItem(strLabel));
+ pItem->SetPath("add");
+ pItem->SetArt("icon", "DefaultAddSource.png");
+ pItem->SetLabel(strLabel);
+ pItem->SetLabelPreformatted(true);
+ pItem->m_bIsFolder = true;
+ pItem->SetSpecialSort(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_addSourceOnTop ?
+ SortSpecialOnTop : SortSpecialOnBottom);
+ m_vecItems->Add(pItem);
+ }
+ m_iLastControl = GetFocusedControlID();
+
+ // Check whether to enabled advanced filtering based on the content type
+ m_canFilterAdvanced = CheckFilterAdvanced(*m_vecItems);
+ if (m_canFilterAdvanced)
+ m_filter.SetType(m_vecItems->GetContent());
+
+ // Ask the derived class if it wants to load additional info
+ // for the fileitems like media info or additional
+ // filtering on the items, setting thumbs.
+ OnPrepareFileItems(*m_vecItems);
+
+ m_vecItems->FillInDefaultIcons();
+
+ // remember the original (untouched) list of items (for filtering etc)
+ m_unfilteredItems->Assign(*m_vecItems);
+
+ // Cache the list of items if possible
+ OnCacheFileItems(*m_vecItems);
+
+ // Filter and group the items if necessary
+ OnFilterItems(GetProperty("filter").asString());
+ UpdateButtons();
+
+ // Restore selected item from history
+ RestoreSelectedItemFromHistory();
+
+ m_history.AddPath(m_vecItems->GetPath(), m_strFilterPath);
+
+ //m_history.DumpPathHistory();
+
+ return true;
+}
+
+bool CGUIMediaWindow::Refresh(bool clearCache /* = false */)
+{
+ std::string strCurrentDirectory = m_vecItems->GetPath();
+ if (strCurrentDirectory == "?")
+ return false;
+
+ if (clearCache)
+ m_vecItems->RemoveDiscCache(GetID());
+
+ bool ret = true;
+
+ // get the original number of items
+ if (!Update(strCurrentDirectory, false))
+ {
+ ret = false;
+ }
+
+ return ret;
+}
+
+/*!
+ * \brief On prepare file items
+ *
+ * This function will be called by Update() before the labels of the fileitems
+ * are formatted.
+ *
+ * \note Override this function to set custom thumbs or load additional media
+ * info.
+ *
+ * It's used to load tag info for music.
+ */
+void CGUIMediaWindow::OnPrepareFileItems(CFileItemList &items)
+{
+ CFileItemListModification::GetInstance().Modify(items);
+}
+
+/*!
+ * \brief On cache file items
+ *
+ * This function will be called by Update() before
+ * any additional formatting, filtering or sorting is applied.
+ *
+ * \note Override this function to define a custom caching behaviour.
+ */
+void CGUIMediaWindow::OnCacheFileItems(CFileItemList &items)
+{
+ // Should these items be saved to the hdd
+ if (items.CacheToDiscAlways() && !IsFiltered())
+ items.Save(GetID());
+}
+
+/*!
+ * \brief On click
+ *
+ * With this function you can react on a users click in the list/thumb panel.
+ * It returns true, if the click is handled.
+ * This function calls OnPlayMedia()
+ */
+bool CGUIMediaWindow::OnClick(int iItem, const std::string &player)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return true;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+
+ if (pItem->IsParentFolder())
+ {
+ GoParentFolder();
+ return true;
+ }
+
+ if (pItem->GetPath() == "add" || pItem->GetPath() == "sources://add/") // 'add source button' in empty root
+ {
+ if (profileManager->IsMasterProfile())
+ {
+ if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return false;
+ }
+ else if (!profileManager->GetCurrentProfile().canWriteSources() && !g_passwordManager.IsProfileLockUnlocked())
+ return false;
+
+ if (OnAddMediaSource())
+ Refresh(true);
+
+ return true;
+ }
+
+ if (!pItem->m_bIsFolder && pItem->IsFileFolder(EFILEFOLDER_MASK_ONCLICK))
+ {
+ XFILE::IFileDirectory *pFileDirectory = nullptr;
+ pFileDirectory = XFILE::CFileDirectoryFactory::Create(pItem->GetURL(), pItem.get(), "");
+ if(pFileDirectory)
+ pItem->m_bIsFolder = true;
+ else if(pItem->m_bIsFolder)
+ pItem->m_bIsFolder = false;
+ delete pFileDirectory;
+ }
+
+ if (pItem->IsScript())
+ {
+ // execute the script
+ CURL url(pItem->GetPath());
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), addon, AddonType::SCRIPT,
+ OnlyEnabled::CHOICE_YES))
+ {
+ if (!CScriptInvocationManager::GetInstance().Stop(addon->LibPath()))
+ {
+ CServiceBroker::GetAddonMgr().UpdateLastUsed(addon->ID());
+ CScriptInvocationManager::GetInstance().ExecuteAsync(addon->LibPath(), addon);
+ }
+ return true;
+ }
+ }
+
+ if (pItem->m_bIsFolder)
+ {
+ if ( pItem->m_bIsShareOrDrive )
+ {
+ const std::string& strLockType=m_guiState->GetLockType();
+ if (profileManager->GetMasterProfile().getLockMode() != LOCK_MODE_EVERYONE)
+ if (!strLockType.empty() && !g_passwordManager.IsItemUnlocked(pItem.get(), strLockType))
+ return true;
+
+ if (!HaveDiscOrConnection(pItem->GetPath(), pItem->m_iDriveType))
+ return true;
+ }
+
+ // check for the partymode playlist items - they may not exist yet
+ if ((pItem->GetPath() == profileManager->GetUserDataItem("PartyMode.xsp")) ||
+ (pItem->GetPath() == profileManager->GetUserDataItem("PartyMode-Video.xsp")))
+ {
+ // party mode playlist item - if it doesn't exist, prompt for user to define it
+ if (!CFileUtils::Exists(pItem->GetPath()))
+ {
+ m_vecItems->RemoveDiscCache(GetID());
+ if (CGUIDialogSmartPlaylistEditor::EditPlaylist(pItem->GetPath()))
+ Refresh();
+ return true;
+ }
+ }
+
+ // remove the directory cache if the folder is not normally cached
+ CFileItemList items(pItem->GetPath());
+ if (!items.AlwaysCache())
+ items.RemoveDiscCache(GetID());
+
+ // if we have a filtered list, we need to add the filtered
+ // path to be able to come back to the filtered view
+ std::string strCurrentDirectory = m_vecItems->GetPath();
+ if (m_canFilterAdvanced && !m_filter.IsEmpty() &&
+ !URIUtils::PathEquals(m_strFilterPath, strCurrentDirectory))
+ {
+ m_history.RemoveParentPath();
+ m_history.AddPath(strCurrentDirectory, m_strFilterPath);
+ }
+
+ if (m_vecItemsUpdating)
+ {
+ CLog::Log(LOGWARNING, "CGUIMediaWindow::OnClick - updating in progress");
+ return true;
+ }
+ CUpdateGuard ug(m_vecItemsUpdating);
+
+ CFileItem directory(*pItem);
+ if (!Update(directory.GetPath()))
+ ShowShareErrorMessage(&directory);
+
+ return true;
+ }
+ else if (pItem->IsPlugin() && !pItem->GetProperty("isplayable").asBoolean())
+ {
+ bool resume = pItem->GetStartOffset() == STARTOFFSET_RESUME;
+ return XFILE::CPluginDirectory::RunScriptWithParams(pItem->GetPath(), resume);
+ }
+#if defined(TARGET_ANDROID)
+ else if (pItem->IsAndroidApp())
+ {
+ std::string appName = URIUtils::GetFileName(pItem->GetPath());
+ CLog::Log(LOGDEBUG, "CGUIMediaWindow::OnClick Trying to run: {}", appName);
+ return CXBMCApp::StartActivity(appName);
+ }
+#endif
+ else
+ {
+ SaveSelectedItemInHistory();
+
+ if (pItem->GetPath() == "newplaylist://")
+ {
+ m_vecItems->RemoveDiscCache(GetID());
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_MUSIC_PLAYLIST_EDITOR,"newplaylist://");
+ return true;
+ }
+ else if (StringUtils::StartsWithNoCase(pItem->GetPath(), "newsmartplaylist://"))
+ {
+ m_vecItems->RemoveDiscCache(GetID());
+ if (CGUIDialogSmartPlaylistEditor::NewPlaylist(pItem->GetPath().substr(19)))
+ Refresh();
+ return true;
+ }
+
+ bool autoplay = m_guiState.get() && m_guiState->AutoPlayNextItem();
+
+ if (m_vecItems->IsPlugin())
+ {
+ CURL url(m_vecItems->GetPath());
+ AddonPtr addon;
+ if (CServiceBroker::GetAddonMgr().GetAddon(url.GetHostName(), addon, OnlyEnabled::CHOICE_YES))
+ {
+ const auto plugin = std::dynamic_pointer_cast<CPluginSource>(addon);
+ if (plugin && plugin->Provides(CPluginSource::AUDIO))
+ {
+ CFileItemList items;
+ std::unique_ptr<CGUIViewState> state(CGUIViewState::GetViewState(GetID(), items));
+ autoplay = state.get() && state->AutoPlayNextItem();
+ }
+ }
+ }
+
+ if (autoplay && !g_partyModeManager.IsEnabled())
+ {
+ return OnPlayAndQueueMedia(pItem, player);
+ }
+ else
+ {
+ return OnPlayMedia(iItem, player);
+ }
+ }
+
+ return false;
+}
+
+bool CGUIMediaWindow::OnSelect(int item)
+{
+ return OnClick(item);
+}
+
+/*!
+ * \brief Check disc or connection present
+ *
+ * Checks if there is a disc in the dvd drive and whether the
+ * network is connected or not.
+ */
+bool CGUIMediaWindow::HaveDiscOrConnection(const std::string& strPath, int iDriveType)
+{
+ if (iDriveType==CMediaSource::SOURCE_TYPE_DVD)
+ {
+ if (!CServiceBroker::GetMediaManager().IsDiscInDrive(strPath))
+ {
+ HELPERS::ShowOKDialogText(CVariant{218}, CVariant{219});
+ return false;
+ }
+ }
+ else if (iDriveType==CMediaSource::SOURCE_TYPE_REMOTE)
+ {
+ //! @todo Handle not connected to a remote share
+ if (!CServiceBroker::GetNetwork().IsConnected())
+ {
+ HELPERS::ShowOKDialogText(CVariant{220}, CVariant{221});
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/*!
+ * \brief Shows a standard error message for a given pItem.
+ */
+void CGUIMediaWindow::ShowShareErrorMessage(CFileItem* pItem) const
+{
+ if (!pItem->m_bIsShareOrDrive)
+ return;
+
+ int idMessageText = 0;
+ CURL url(pItem->GetPath());
+
+ if (url.IsProtocol("smb") && url.GetHostName().empty()) // smb workgroup
+ idMessageText = 15303; // Workgroup not found
+ else if (pItem->m_iDriveType == CMediaSource::SOURCE_TYPE_REMOTE || URIUtils::IsRemote(pItem->GetPath()))
+ idMessageText = 15301; // Could not connect to network server
+ else
+ idMessageText = 15300; // Path not found or invalid
+
+ HELPERS::ShowOKDialogText(CVariant{220}, CVariant{idMessageText});
+}
+
+/*!
+ * \brief Go one directory up on list items
+ *
+ * The function goes up one level in the directory tree
+ */
+bool CGUIMediaWindow::GoParentFolder()
+{
+ if (m_vecItems->IsVirtualDirectoryRoot())
+ return false;
+
+ if (URIUtils::PathEquals(m_vecItems->GetPath(), GetRootPath()))
+ return false;
+
+ //m_history.DumpPathHistory();
+
+ const std::string currentPath = m_vecItems->GetPath();
+ std::string parentPath = m_history.GetParentPath();
+ // Check if a) the current folder is on the stack more than once, (parent is
+ // often same as current), OR
+ // b) the parent is an xml file (happens when ActivateWindow() called with
+ // a node file) and so current path is the result of expanding the xml.
+ // Keep going until there's nothing left or they dont match anymore.
+ while (!parentPath.empty() &&
+ (URIUtils::PathEquals(parentPath, currentPath, true) ||
+ StringUtils::EndsWith(parentPath, ".xml/") || StringUtils::EndsWith(parentPath, ".xml")))
+ {
+ m_history.RemoveParentPath();
+ parentPath = m_history.GetParentPath();
+ }
+
+ // remove the current filter but only if the parent
+ // item doesn't have a filter as well
+ CURL filterUrl(m_strFilterPath);
+ if (filterUrl.HasOption("filter"))
+ {
+ CURL parentUrl(m_history.GetParentPath(true));
+ if (!parentUrl.HasOption("filter"))
+ {
+ // we need to overwrite m_strFilterPath because
+ // Refresh() will set updateFilterPath to false
+ m_strFilterPath.clear();
+ Refresh();
+ return true;
+ }
+ }
+
+ // pop directory path from the stack
+ m_strFilterPath = m_history.GetParentPath(true);
+ m_history.RemoveParentPath();
+
+ if (!Update(parentPath, false))
+ return false;
+
+ // No items to show so go another level up
+ if (!m_vecItems->GetPath().empty() && (m_filter.IsEmpty() ? m_vecItems->Size() : m_unfilteredItems->Size()) <= 0)
+ {
+ CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, g_localizeStrings.Get(2080), g_localizeStrings.Get(2081));
+ return GoParentFolder();
+ }
+ return true;
+}
+
+void CGUIMediaWindow::SaveSelectedItemInHistory()
+{
+ int iItem = m_viewControl.GetSelectedItem();
+ std::string strSelectedItem;
+ if (iItem >= 0 && iItem < m_vecItems->Size())
+ {
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+ GetDirectoryHistoryString(pItem.get(), strSelectedItem);
+ }
+
+ m_history.SetSelectedItem(strSelectedItem, m_vecItems->GetPath());
+}
+
+void CGUIMediaWindow::RestoreSelectedItemFromHistory()
+{
+ std::string strSelectedItem = m_history.GetSelectedItem(m_vecItems->GetPath());
+
+ if (!strSelectedItem.empty())
+ {
+ for (int i = 0; i < m_vecItems->Size(); ++i)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+ std::string strHistory;
+ GetDirectoryHistoryString(pItem.get(), strHistory);
+ // set selected item if equals with history
+ if (strHistory == strSelectedItem)
+ {
+ m_viewControl.SetSelectedItem(i);
+ return;
+ }
+ }
+ }
+
+ // if we haven't found the selected item, select the first item
+ m_viewControl.SetSelectedItem(0);
+}
+
+/*!
+ * \brief Get history string for given file item
+ *
+ * \note Override the function to change the default behavior on how
+ * a selected item history should look like
+ */
+void CGUIMediaWindow::GetDirectoryHistoryString(const CFileItem* pItem, std::string& strHistoryString) const
+{
+ if (pItem->m_bIsShareOrDrive)
+ {
+ // We are in the virtual directory
+
+ // History string of the DVD drive
+ // must be handled separately
+ if (pItem->m_iDriveType == CMediaSource::SOURCE_TYPE_DVD)
+ {
+ // Remove disc label from item label
+ // and use as history string, m_strPath
+ // can change for new discs
+ std::string strLabel = pItem->GetLabel();
+ size_t nPosOpen = strLabel.find('(');
+ size_t nPosClose = strLabel.rfind(')');
+ if (nPosOpen != std::string::npos &&
+ nPosClose != std::string::npos &&
+ nPosClose > nPosOpen)
+ {
+ strLabel.erase(nPosOpen + 1, (nPosClose) - (nPosOpen + 1));
+ strHistoryString = strLabel;
+ }
+ else
+ strHistoryString = strLabel;
+ }
+ else
+ {
+ // Other items in virtual directory
+ std::string strPath = pItem->GetPath();
+ URIUtils::RemoveSlashAtEnd(strPath);
+
+ strHistoryString = pItem->GetLabel() + strPath;
+ }
+ }
+ else if (pItem->GetEndOffset() > pItem->GetStartOffset() &&
+ pItem->GetStartOffset() != STARTOFFSET_RESUME)
+ {
+ // Could be a cue item, all items of a cue share the same filename
+ // so add the offsets to build the history string
+ strHistoryString = StringUtils::Format("{}{}", pItem->GetStartOffset(), pItem->GetEndOffset());
+ strHistoryString += pItem->GetPath();
+ }
+ else
+ {
+ // Normal directory items
+ strHistoryString = pItem->GetPath();
+ }
+
+ // remove any filter
+ if (CanContainFilter(strHistoryString))
+ strHistoryString = RemoveParameterFromPath(strHistoryString, "filter");
+
+ URIUtils::RemoveSlashAtEnd(strHistoryString);
+ StringUtils::ToLower(strHistoryString);
+}
+
+/*!
+ * \brief Set history for path
+ *
+ * Call this function to create a directory history for the
+ * path given by strDirectory.
+ */
+void CGUIMediaWindow::SetHistoryForPath(const std::string& strDirectory)
+{
+ // Make sure our shares are configured
+ SetupShares();
+ if (!strDirectory.empty())
+ {
+ // Build the directory history for default path
+ std::string strPath, strParentPath;
+ strPath = strDirectory;
+ URIUtils::RemoveSlashAtEnd(strPath);
+
+ CFileItemList items;
+ CURL url;
+ GetDirectoryItems(url, items, UseFileDirectories());
+
+ m_history.ClearPathHistory();
+
+ bool originalPath = true;
+ while (URIUtils::GetParentPath(strPath, strParentPath))
+ {
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr pItem = items[i];
+ std::string path(pItem->GetPath());
+ URIUtils::RemoveSlashAtEnd(path);
+ if (URIUtils::PathEquals(path, strPath))
+ {
+ std::string strHistory;
+ GetDirectoryHistoryString(pItem.get(), strHistory);
+ m_history.SetSelectedItem(strHistory, "");
+ URIUtils::AddSlashAtEnd(strPath);
+ m_history.AddPathFront(strPath);
+ m_history.AddPathFront("");
+
+ //m_history.DumpPathHistory();
+ return ;
+ }
+ }
+
+ if (URIUtils::IsVideoDb(strPath))
+ {
+ CURL url(strParentPath);
+ url.SetOptions(""); // clear any URL options from recreated parent path
+ strParentPath = url.Get();
+ }
+
+ // set the original path exactly as it was passed in
+ if (URIUtils::PathEquals(strPath, strDirectory, true))
+ strPath = strDirectory;
+ else
+ URIUtils::AddSlashAtEnd(strPath);
+
+ m_history.AddPathFront(strPath, originalPath ? m_strFilterPath : "");
+ m_history.SetSelectedItem(strPath, strParentPath);
+ originalPath = false;
+ strPath = strParentPath;
+ URIUtils::RemoveSlashAtEnd(strPath);
+ }
+ }
+ else
+ m_history.ClearPathHistory();
+
+ //m_history.DumpPathHistory();
+}
+
+/*!
+ * \brief On media play
+ *
+ * \note Override if you want to change the default behavior, what is done
+ * when the user clicks on a file.
+ *
+ * This function is called by OnClick()
+ */
+bool CGUIMediaWindow::OnPlayMedia(int iItem, const std::string &player)
+{
+ // Reset Playlistplayer, playback started now does
+ // not use the playlistplayer.
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(PLAYLIST::TYPE_NONE);
+ CFileItemPtr pItem=m_vecItems->Get(iItem);
+
+ CLog::Log(LOGDEBUG, "{} {}", __FUNCTION__, CURL::GetRedacted(pItem->GetPath()));
+
+ bool bResult = false;
+ if (pItem->IsInternetStream() || pItem->IsPlayList())
+ bResult = g_application.PlayMedia(*pItem, player, m_guiState->GetPlaylist());
+ else
+ bResult = g_application.PlayFile(*pItem, player);
+
+ if (pItem->GetStartOffset() == STARTOFFSET_RESUME)
+ pItem->SetStartOffset(0);
+
+ return bResult;
+}
+
+/*!
+ * \brief On play and media queue
+ *
+ * \note Override if you want to change the default behavior of what is done
+ * when the user clicks on a file in a "folder" with similar files.
+ *
+ * This function is called by OnClick()
+ */
+bool CGUIMediaWindow::OnPlayAndQueueMedia(const CFileItemPtr& item, const std::string& player)
+{
+ //play and add current directory to temporary playlist
+ PLAYLIST::Id playlistId = m_guiState->GetPlaylist();
+ if (playlistId != PLAYLIST::TYPE_NONE)
+ {
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(playlistId);
+ CServiceBroker::GetPlaylistPlayer().Reset();
+ int mediaToPlay = 0;
+
+ // first try to find mainDVD file (VIDEO_TS.IFO).
+ // If we find this we should not allow to queue VOB files
+ std::string mainDVD;
+ for (int i = 0; i < m_vecItems->Size(); i++)
+ {
+ std::string path = URIUtils::GetFileName(m_vecItems->Get(i)->GetDynPath());
+ if (StringUtils::EqualsNoCase(path, "VIDEO_TS.IFO"))
+ {
+ mainDVD = path;
+ break;
+ }
+ }
+
+ // now queue...
+ for ( int i = 0; i < m_vecItems->Size(); i++ )
+ {
+ CFileItemPtr nItem = m_vecItems->Get(i);
+
+ if (nItem->m_bIsFolder)
+ continue;
+
+ if (!nItem->IsZIP() && !nItem->IsRAR() && (!nItem->IsDVDFile() || (URIUtils::GetFileName(nItem->GetDynPath()) == mainDVD)))
+ CServiceBroker::GetPlaylistPlayer().Add(playlistId, nItem);
+
+ if (item->IsSamePath(nItem.get()))
+ { // item that was clicked
+ mediaToPlay = CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId).size() - 1;
+ }
+ }
+
+ // Save current window and directory to know where the selected item was
+ if (m_guiState)
+ m_guiState->SetPlaylistDirectory(m_vecItems->GetPath());
+
+ // figure out where we start playback
+ if (CServiceBroker::GetPlaylistPlayer().IsShuffled(playlistId))
+ {
+ int iIndex =
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId).FindOrder(mediaToPlay);
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId).Swap(0, iIndex);
+ mediaToPlay = 0;
+ }
+
+ // play
+ CServiceBroker::GetPlaylistPlayer().SetCurrentPlaylist(playlistId);
+ CServiceBroker::GetPlaylistPlayer().Play(mediaToPlay, player);
+ }
+ return true;
+}
+
+/*!
+ * \brief Update file list
+ *
+ * Synchronize the fileitems with the playlistplayer
+ * also recreates the playlist of the playlistplayer based
+ * on the fileitems of the window
+ */
+void CGUIMediaWindow::UpdateFileList()
+{
+ int nItem = m_viewControl.GetSelectedItem();
+ std::string strSelected;
+ if (nItem >= 0)
+ strSelected = m_vecItems->Get(nItem)->GetPath();
+
+ FormatAndSort(*m_vecItems);
+ UpdateButtons();
+
+ m_viewControl.SetItems(*m_vecItems);
+ m_viewControl.SetSelectedItem(strSelected);
+
+ // set the currently playing item as selected, if its in this directory
+ if (m_guiState.get() && m_guiState->IsCurrentPlaylistDirectory(m_vecItems->GetPath()))
+ {
+ PLAYLIST::Id playlistId = m_guiState->GetPlaylist();
+ int nSong = CServiceBroker::GetPlaylistPlayer().GetCurrentSong();
+ CFileItem playlistItem;
+ if (nSong > -1 && playlistId != PLAYLIST::TYPE_NONE)
+ playlistItem = *CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId)[nSong];
+
+ CServiceBroker::GetPlaylistPlayer().ClearPlaylist(playlistId);
+ CServiceBroker::GetPlaylistPlayer().Reset();
+
+ for (int i = 0; i < m_vecItems->Size(); i++)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(i);
+ if (pItem->m_bIsFolder)
+ continue;
+
+ if (!pItem->IsPlayList() && !pItem->IsZIP() && !pItem->IsRAR())
+ CServiceBroker::GetPlaylistPlayer().Add(playlistId, pItem);
+
+ if (pItem->GetPath() == playlistItem.GetPath() &&
+ pItem->GetStartOffset() == playlistItem.GetStartOffset())
+ CServiceBroker::GetPlaylistPlayer().SetCurrentSong(
+ CServiceBroker::GetPlaylistPlayer().GetPlaylist(playlistId).size() - 1);
+ }
+ }
+}
+
+void CGUIMediaWindow::OnDeleteItem(int iItem)
+{
+ if ( iItem < 0 || iItem >= m_vecItems->Size()) return;
+ CFileItemPtr item = m_vecItems->Get(iItem);
+
+ if (item->IsPlayList())
+ item->m_bIsFolder = false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (profileManager->GetCurrentProfile().getLockMode() != LOCK_MODE_EVERYONE && profileManager->GetCurrentProfile().filesLocked())
+ {
+ if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return;
+ }
+
+ CGUIComponent *gui = CServiceBroker::GetGUI();
+ if (gui && gui->ConfirmDelete(item->GetPath()))
+ {
+ if (!CFileUtils::DeleteItem(item))
+ return;
+ }
+ else
+ return;
+
+ Refresh(true);
+ m_viewControl.SetSelectedItem(iItem);
+}
+
+void CGUIMediaWindow::OnRenameItem(int iItem)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ if (profileManager->GetCurrentProfile().getLockMode() != LOCK_MODE_EVERYONE && profileManager->GetCurrentProfile().filesLocked())
+ {
+ if (!g_passwordManager.IsMasterLockUnlocked(true))
+ return;
+ }
+
+ if (!CFileUtils::RenameFile(m_vecItems->Get(iItem)->GetPath()))
+ return;
+
+ Refresh(true);
+ m_viewControl.SetSelectedItem(iItem);
+}
+
+void CGUIMediaWindow::OnInitWindow()
+{
+ // initial fetch is done unthreaded to ensure the items are setup prior to skin animations kicking off
+ m_backgroundLoad = false;
+
+ // the start directory may change during Refresh
+ bool updateStartDirectory = URIUtils::PathEquals(m_vecItems->GetPath(), m_startDirectory, true);
+
+ // we have python scripts hooked in everywhere :(
+ // those scripts may open windows and we can't open a window
+ // while opening this one.
+ // for plugin sources delay call to Refresh
+ if (!URIUtils::IsPlugin(m_vecItems->GetPath()))
+ {
+ Refresh();
+ }
+ else
+ {
+ CGUIMessage msg(GUI_MSG_WINDOW_INIT, 0, 0, WINDOW_INVALID, PLUGIN_REFRESH_DELAY);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg, GetID());
+ }
+
+ if (updateStartDirectory)
+ {
+ // reset the start directory to the path of the items
+ m_startDirectory = m_vecItems->GetPath();
+
+ // reset the history based on the path of the items
+ SetHistoryForPath(m_startDirectory);
+ }
+
+ m_backgroundLoad = true;
+
+ CGUIWindow::OnInitWindow();
+}
+
+void CGUIMediaWindow::SaveControlStates()
+{
+ CGUIWindow::SaveControlStates();
+ SaveSelectedItemInHistory();
+}
+
+void CGUIMediaWindow::RestoreControlStates()
+{
+ CGUIWindow::RestoreControlStates();
+ RestoreSelectedItemFromHistory();
+}
+
+CGUIControl *CGUIMediaWindow::GetFirstFocusableControl(int id)
+{
+ if (m_viewControl.HasControl(id))
+ id = m_viewControl.GetCurrentControl();
+ return CGUIWindow::GetFirstFocusableControl(id);
+}
+
+void CGUIMediaWindow::SetupShares()
+{
+ // Setup shares and filemasks for this window
+ CFileItemList items;
+ CGUIViewState* viewState=CGUIViewState::GetViewState(GetID(), items);
+ if (viewState)
+ {
+ m_rootDir.SetMask(viewState->GetExtensions());
+ m_rootDir.SetSources(viewState->GetSources());
+ delete viewState;
+ }
+}
+
+bool CGUIMediaWindow::OnPopupMenu(int itemIdx)
+{
+ auto InRange = [](size_t i, std::pair<size_t, size_t> range){ return i >= range.first && i < range.second; };
+
+ if (itemIdx < 0 || itemIdx >= m_vecItems->Size())
+ return false;
+
+ auto item = m_vecItems->Get(itemIdx);
+ if (!item)
+ return false;
+
+ CContextButtons buttons;
+
+ //Add items from plugin
+ {
+ int i = 0;
+ while (item->HasProperty(StringUtils::Format("contextmenulabel({})", i)))
+ {
+ buttons.emplace_back(
+ ~buttons.size(),
+ item->GetProperty(StringUtils::Format("contextmenulabel({})", i)).asString());
+ ++i;
+ }
+ }
+ auto pluginMenuRange = std::make_pair(static_cast<size_t>(0), buttons.size());
+
+ //Add the global menu
+ auto globalMenu = CServiceBroker::GetContextMenuManager().GetItems(*item, CContextMenuManager::MAIN);
+ auto globalMenuRange = std::make_pair(buttons.size(), buttons.size() + globalMenu.size());
+ for (const auto& menu : globalMenu)
+ buttons.emplace_back(~buttons.size(), menu->GetLabel(*item));
+
+ //Add legacy items from windows
+ auto buttonsSize = buttons.size();
+ GetContextButtons(itemIdx, buttons);
+ auto windowMenuRange = std::make_pair(buttonsSize, buttons.size());
+
+ //Add addon menus
+ auto addonMenu = CServiceBroker::GetContextMenuManager().GetAddonItems(*item, CContextMenuManager::MAIN);
+ auto addonMenuRange = std::make_pair(buttons.size(), buttons.size() + addonMenu.size());
+ for (const auto& menu : addonMenu)
+ buttons.emplace_back(~buttons.size(), menu->GetLabel(*item));
+
+ if (buttons.empty())
+ return true;
+
+ int idx = CGUIDialogContextMenu::Show(buttons);
+ if (idx < 0 || idx >= static_cast<int>(buttons.size()))
+ return false;
+
+ if (InRange(static_cast<size_t>(idx), pluginMenuRange))
+ {
+ bool saveVal = m_backgroundLoad;
+ m_backgroundLoad = false;
+ CServiceBroker::GetAppMessenger()->SendMsg(
+ TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ item->GetProperty(StringUtils::Format("contextmenuaction({})", idx - pluginMenuRange.first))
+ .asString());
+ m_backgroundLoad = saveVal;
+ return true;
+ }
+
+ if (InRange(idx, windowMenuRange))
+ return OnContextButton(itemIdx, static_cast<CONTEXT_BUTTON>(buttons[idx].first));
+
+ if (InRange(idx, globalMenuRange))
+ return CONTEXTMENU::LoopFrom(*globalMenu[idx - globalMenuRange.first], item);
+
+ return CONTEXTMENU::LoopFrom(*addonMenu[idx - addonMenuRange.first], item);
+}
+
+void CGUIMediaWindow::GetContextButtons(int itemNumber, CContextButtons &buttons)
+{
+ CFileItemPtr item = (itemNumber >= 0 && itemNumber < m_vecItems->Size()) ? m_vecItems->Get(itemNumber) : CFileItemPtr();
+
+ if (!item || item->IsParentFolder())
+ return;
+
+ if (item->IsFileFolder(EFILEFOLDER_MASK_ONBROWSE))
+ buttons.Add(CONTEXT_BUTTON_BROWSE_INTO, 37015);
+
+}
+
+bool CGUIMediaWindow::OnContextButton(int itemNumber, CONTEXT_BUTTON button)
+{
+ switch (button)
+ {
+ case CONTEXT_BUTTON_BROWSE_INTO:
+ {
+ CFileItemPtr item = m_vecItems->Get(itemNumber);
+ Update(item->GetPath());
+ return true;
+ }
+ default:
+ break;
+ }
+ return false;
+}
+
+const CGUIViewState *CGUIMediaWindow::GetViewState() const
+{
+ return m_guiState.get();
+}
+
+const CFileItemList& CGUIMediaWindow::CurrentDirectory() const
+{
+ return *m_vecItems;
+}
+
+bool CGUIMediaWindow::WaitForNetwork() const
+{
+ if (CServiceBroker::GetNetwork().IsAvailable())
+ return true;
+
+ CGUIDialogProgress *progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (!progress)
+ return true;
+
+ CURL url(m_vecItems->GetPath());
+ progress->SetHeading(CVariant{1040}); // Loading Directory
+ progress->SetLine(1, CVariant{url.GetWithoutUserDetails()});
+ progress->ShowProgressBar(false);
+ progress->Open();
+ while (!CServiceBroker::GetNetwork().IsAvailable())
+ {
+ progress->Progress();
+ if (progress->IsCanceled())
+ {
+ progress->Close();
+ return false;
+ }
+ }
+ progress->Close();
+ return true;
+}
+
+void CGUIMediaWindow::UpdateFilterPath(const std::string &strDirectory, const CFileItemList &items, bool updateFilterPath)
+{
+ bool canfilter = CanContainFilter(strDirectory);
+
+ std::string filter;
+ CURL url(strDirectory);
+ if (canfilter && url.HasOption("filter"))
+ filter = url.GetOption("filter");
+
+ // only set the filter path if it hasn't been marked
+ // as preset or if it's empty
+ if (updateFilterPath || m_strFilterPath.empty())
+ {
+ if (items.HasProperty(PROPERTY_PATH_DB))
+ m_strFilterPath = items.GetProperty(PROPERTY_PATH_DB).asString();
+ else
+ m_strFilterPath = items.GetPath();
+ }
+
+ // maybe the filter path can contain a filter
+ if (!canfilter && CanContainFilter(m_strFilterPath))
+ canfilter = true;
+
+ // check if the filter path contains a filter
+ CURL filterPathUrl(m_strFilterPath);
+ if (canfilter && filter.empty())
+ {
+ if (filterPathUrl.HasOption("filter"))
+ filter = filterPathUrl.GetOption("filter");
+ }
+
+ // check if there is a filter and re-apply it
+ if (canfilter && !filter.empty())
+ {
+ if (!m_filter.LoadFromJson(filter))
+ {
+ CLog::Log(LOGWARNING,
+ "CGUIMediaWindow::UpdateFilterPath(): unable to load existing filter ({})", filter);
+ m_filter.Reset();
+ m_strFilterPath = m_vecItems->GetPath();
+ }
+ else
+ {
+ // add the filter to the filter path
+ filterPathUrl.SetOption("filter", filter);
+ m_strFilterPath = filterPathUrl.Get();
+ }
+ }
+}
+
+void CGUIMediaWindow::OnFilterItems(const std::string &filter)
+{
+ m_viewControl.Clear();
+
+ CFileItemList items;
+ items.Copy(*m_vecItems, false); // use the original path - it'll likely be relied on for other things later.
+ items.Append(*m_unfilteredItems);
+ bool filtered = GetFilteredItems(filter, items);
+
+ m_vecItems->ClearItems();
+ // we need to clear the sort state and re-sort the items
+ m_vecItems->ClearSortState();
+ m_vecItems->Append(items);
+
+ // if the filter has changed, get the new filter path
+ if (filtered && m_canFilterAdvanced)
+ {
+ if (items.HasProperty(PROPERTY_PATH_DB))
+ m_strFilterPath = items.GetProperty(PROPERTY_PATH_DB).asString();
+ // only set m_strFilterPath if it hasn't been set before
+ // otherwise we might overwrite it with a non-filter path
+ // in case GetFilteredItems() returns true even though no
+ // db-based filter (e.g. watched filter) has been applied
+ else if (m_strFilterPath.empty())
+ m_strFilterPath = items.GetPath();
+ }
+
+ GetGroupedItems(*m_vecItems);
+ FormatAndSort(*m_vecItems);
+
+ CFileItemPtr currentItem;
+ std::string currentItemPath;
+ int item = m_viewControl.GetSelectedItem();
+ if (item >= 0 && item < m_vecItems->Size())
+ {
+ currentItem = m_vecItems->Get(item);
+ currentItemPath = currentItem->GetPath();
+ }
+
+ // get the "filter" option
+ std::string filterOption;
+ CURL filterUrl(m_strFilterPath);
+ if (filterUrl.HasOption("filter"))
+ filterOption = filterUrl.GetOption("filter");
+
+ // apply the "filter" option to any folder item so that
+ // the filter can be passed down to the sub-directory
+ for (int index = 0; index < m_vecItems->Size(); index++)
+ {
+ CFileItemPtr pItem = m_vecItems->Get(index);
+ // if the item is a folder we need to copy the path of
+ // the filtered item to be able to keep the applied filters
+ if (pItem->m_bIsFolder)
+ {
+ CURL itemUrl(pItem->GetPath());
+ if (!filterOption.empty())
+ itemUrl.SetOption("filter", filterOption);
+ else
+ itemUrl.RemoveOption("filter");
+ pItem->SetPath(itemUrl.Get());
+ }
+ }
+
+ SetProperty("filter", filter);
+ if (filtered && m_canFilterAdvanced)
+ {
+ // to be able to select the same item as before we need to adjust
+ // the path of the item i.e. add or remove the "filter=" URL option
+ // but that's only necessary for folder items
+ if (currentItem.get() && currentItem->m_bIsFolder)
+ {
+ CURL curUrl(currentItemPath), newUrl(m_strFilterPath);
+ if (newUrl.HasOption("filter"))
+ curUrl.SetOption("filter", newUrl.GetOption("filter"));
+ else if (curUrl.HasOption("filter"))
+ curUrl.RemoveOption("filter");
+
+ currentItemPath = curUrl.Get();
+ }
+ }
+
+ // The idea here is to ensure we have something to focus if our file list
+ // is empty. As such, this check MUST be last and ignore the hide parent
+ // fileitems settings.
+ if (m_vecItems->IsEmpty())
+ {
+ CFileItemPtr pItem(new CFileItem(".."));
+ pItem->SetPath(m_history.GetParentPath());
+ pItem->m_bIsFolder = true;
+ pItem->m_bIsShareOrDrive = false;
+ m_vecItems->AddFront(pItem, 0);
+ }
+
+ // and update our view control + buttons
+ m_viewControl.SetItems(*m_vecItems);
+ m_viewControl.SetSelectedItem(currentItemPath);
+}
+
+bool CGUIMediaWindow::GetFilteredItems(const std::string &filter, CFileItemList &items)
+{
+ bool result = false;
+ if (m_canFilterAdvanced)
+ result = GetAdvanceFilteredItems(items);
+
+ std::string trimmedFilter(filter);
+ StringUtils::TrimLeft(trimmedFilter);
+ StringUtils::ToLower(trimmedFilter);
+
+ if (trimmedFilter.empty())
+ return result;
+
+ CFileItemList filteredItems(items.GetPath()); // use the original path - it'll likely be relied on for other things later.
+ bool numericMatch = StringUtils::IsNaturalNumber(trimmedFilter);
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items.Get(i);
+ if (item->IsParentFolder())
+ {
+ filteredItems.Add(item);
+ continue;
+ }
+ //! @todo Need to update this to get all labels, ideally out of the displayed info (ie from m_layout and m_focusedLayout)
+ //! though that isn't practical. Perhaps a better idea would be to just grab the info that we should filter on based on
+ //! where we are in the library tree.
+ //! Another idea is tying the filter string to the current level of the tree, so that going deeper disables the filter,
+ //! but it's re-enabled on the way back out.
+ std::string match;
+ /* if (item->GetFocusedLayout())
+ match = item->GetFocusedLayout()->GetAllText();
+ else if (item->GetLayout())
+ match = item->GetLayout()->GetAllText();
+ else*/
+ match = item->GetLabel(); // Filter label only for now
+
+ if (numericMatch)
+ StringUtils::WordToDigits(match);
+
+ size_t pos = StringUtils::FindWords(match.c_str(), trimmedFilter.c_str());
+ if (pos != std::string::npos)
+ filteredItems.Add(item);
+ }
+
+ items.ClearItems();
+ items.Append(filteredItems);
+
+ return items.GetObjectCount() > 0;
+}
+
+bool CGUIMediaWindow::GetAdvanceFilteredItems(CFileItemList &items)
+{
+ // don't run the advanced filter if the filter is empty
+ // and there hasn't been a filter applied before which
+ // would have to be removed
+ CURL url(m_strFilterPath);
+ if (m_filter.IsEmpty() && !url.HasOption("filter"))
+ return false;
+
+ CFileItemList resultItems;
+ XFILE::CSmartPlaylistDirectory::GetDirectory(m_filter, resultItems, m_strFilterPath, true);
+
+ // put together a lookup map for faster path comparison
+ std::map<std::string, CFileItemPtr> lookup;
+ for (int j = 0; j < resultItems.Size(); j++)
+ {
+ std::string itemPath = CURL(resultItems[j]->GetPath()).GetWithoutOptions();
+ StringUtils::ToLower(itemPath);
+
+ lookup[itemPath] = resultItems[j];
+ }
+
+ // loop through all the original items and find
+ // those which are still part of the filter
+ CFileItemList filteredItems;
+ for (int i = 0; i < items.Size(); i++)
+ {
+ CFileItemPtr item = items.Get(i);
+ if (item->IsParentFolder())
+ {
+ filteredItems.Add(item);
+ continue;
+ }
+
+ // check if the item is part of the resultItems list
+ // by comparing their paths (but ignoring any special
+ // options because they differ from filter to filter)
+ std::string path = CURL(item->GetPath()).GetWithoutOptions();
+ StringUtils::ToLower(path);
+
+ std::map<std::string, CFileItemPtr>::iterator itItem = lookup.find(path);
+ if (itItem != lookup.end())
+ {
+ // add the item to the list of filtered items
+ filteredItems.Add(item);
+
+ // remove the item from the lists
+ resultItems.Remove(itItem->second.get());
+ lookup.erase(itItem);
+ }
+ }
+
+ if (resultItems.Size() > 0)
+ CLog::Log(LOGWARNING, "CGUIMediaWindow::GetAdvanceFilteredItems(): {} unknown items",
+ resultItems.Size());
+
+ items.ClearItems();
+ items.Append(filteredItems);
+ items.SetPath(resultItems.GetPath());
+ if (resultItems.HasProperty(PROPERTY_PATH_DB))
+ items.SetProperty(PROPERTY_PATH_DB, resultItems.GetProperty(PROPERTY_PATH_DB));
+ return true;
+}
+
+bool CGUIMediaWindow::IsFiltered()
+{
+ return (!m_canFilterAdvanced && !GetProperty("filter").empty()) ||
+ (m_canFilterAdvanced && !m_filter.IsEmpty());
+}
+
+bool CGUIMediaWindow::IsSameStartFolder(const std::string &dir)
+{
+ const std::string startFolder = GetStartFolder(dir);
+ return URIUtils::PathHasParent(m_vecItems->GetPath(), startFolder);
+}
+
+bool CGUIMediaWindow::Filter(bool advanced /* = true */)
+{
+ // basic filtering
+ if (!m_canFilterAdvanced || !advanced)
+ {
+ const CGUIControl *btnFilter = GetControl(CONTROL_BTN_FILTER);
+ if (btnFilter && btnFilter->GetControlType() == CGUIControl::GUICONTROL_EDIT)
+ { // filter updated
+ CGUIMessage selected(GUI_MSG_ITEM_SELECTED, GetID(), CONTROL_BTN_FILTER);
+ OnMessage(selected);
+ OnFilterItems(selected.GetLabel());
+ UpdateButtons();
+ return true;
+ }
+ if (GetProperty("filter").empty())
+ {
+ std::string filter = GetProperty("filter").asString();
+ CGUIKeyboardFactory::ShowAndGetFilter(filter, false);
+ SetProperty("filter", filter);
+ }
+ else
+ {
+ OnFilterItems("");
+ UpdateButtons();
+ }
+ }
+ // advanced filtering
+ else
+ CGUIDialogMediaFilter::ShowAndEditMediaFilter(m_strFilterPath, m_filter);
+
+ return true;
+}
+
+std::string CGUIMediaWindow::GetStartFolder(const std::string &dir)
+{
+ if (StringUtils::EqualsNoCase(dir, "$root") ||
+ StringUtils::EqualsNoCase(dir, "root"))
+ return "";
+
+ // Let plugins handle their own urls themselves
+ if (StringUtils::StartsWith(dir, "plugin://"))
+ return dir;
+
+//! @todo This ifdef block probably belongs somewhere else. Move it to a better place!
+#if defined(TARGET_ANDROID)
+ // Hack for Android items (numbered id's) on the leanback screen
+ std::string path;
+ std::string fileName;
+ URIUtils::Split(dir, path, fileName);
+ URIUtils::RemoveExtension(fileName);
+ if (StringUtils::IsInteger(fileName))
+ return path;
+#endif
+
+ return dir;
+}
+
+std::string CGUIMediaWindow::RemoveParameterFromPath(const std::string &strDirectory, const std::string &strParameter)
+{
+ CURL url(strDirectory);
+ if (url.HasOption(strParameter))
+ {
+ url.RemoveOption(strParameter);
+ return url.Get();
+ }
+
+ return strDirectory;
+}
+
+bool CGUIMediaWindow::ProcessRenderLoop(bool renderOnly)
+{
+ return CServiceBroker::GetGUI()->GetWindowManager().ProcessRenderLoop(renderOnly);
+}
+
+bool CGUIMediaWindow::GetDirectoryItems(CURL &url, CFileItemList &items, bool useDir)
+{
+ if (m_backgroundLoad)
+ {
+ bool ret = true;
+ CGetDirectoryItems getItems(m_rootDir, url, items, useDir);
+
+ if (!WaitGetDirectoryItems(getItems))
+ {
+ // cancelled
+ ret = false;
+ }
+ else if (!getItems.m_result)
+ {
+ if (CServiceBroker::GetAppMessenger()->IsProcessThread() && m_rootDir.GetDirImpl() &&
+ !m_rootDir.GetDirImpl()->ProcessRequirements())
+ {
+ ret = false;
+ }
+ else if (!WaitGetDirectoryItems(getItems) || !getItems.m_result)
+ {
+ ret = false;
+ }
+ }
+
+ m_updateJobActive = false;
+ m_rootDir.ReleaseDirImpl();
+ return ret;
+ }
+ else
+ {
+ return m_rootDir.GetDirectory(url, items, useDir, false);
+ }
+}
+
+bool CGUIMediaWindow::WaitGetDirectoryItems(CGetDirectoryItems &items)
+{
+ bool ret = true;
+ CGUIDialogBusy* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogBusy>(WINDOW_DIALOG_BUSY);
+ if (dialog && !dialog->IsDialogRunning())
+ {
+ if (!CGUIDialogBusy::Wait(&items, 100, true))
+ {
+ // cancelled
+ ret = false;
+ }
+ }
+ else
+ {
+ m_updateJobActive = true;
+ m_updateAborted = false;
+ m_updateEvent.Reset();
+ CServiceBroker::GetJobManager()->Submit(
+ [&]() {
+ items.Run();
+ m_updateEvent.Set();
+ },
+ nullptr, CJob::PRIORITY_NORMAL);
+
+ // Loop until either the job ended or update canceled via CGUIMediaWindow::CancelUpdateItems.
+ while (!m_updateAborted && !m_updateEvent.Wait(1ms))
+ {
+ if (!ProcessRenderLoop(false))
+ break;
+ }
+
+ if (m_updateAborted)
+ {
+ CLog::LogF(LOGDEBUG, "Get directory items job was canceled.");
+ ret = false;
+ }
+ else if (!items.m_result)
+ {
+ CLog::LogF(LOGDEBUG, "Get directory items job was unsuccessful.");
+ ret = false;
+ }
+ }
+ return ret;
+}
+
+void CGUIMediaWindow::CancelUpdateItems()
+{
+ if (m_updateJobActive)
+ {
+ m_rootDir.CancelDirectory();
+ m_updateAborted = true;
+ if (!m_updateEvent.Wait(5000ms))
+ {
+ CLog::Log(LOGERROR, "CGUIMediaWindow::CancelUpdateItems - error cancel update");
+ }
+ m_updateJobActive = false;
+ }
+}
diff --git a/xbmc/windows/GUIMediaWindow.h b/xbmc/windows/GUIMediaWindow.h
new file mode 100644
index 0000000..1fc1dfc
--- /dev/null
+++ b/xbmc/windows/GUIMediaWindow.h
@@ -0,0 +1,227 @@
+/*
+ * 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 "dialogs/GUIDialogContextMenu.h"
+#include "filesystem/DirectoryHistory.h"
+#include "filesystem/VirtualDirectory.h"
+#include "guilib/GUIWindow.h"
+#include "playlists/SmartPlayList.h"
+#include "view/GUIViewControl.h"
+
+#include <atomic>
+
+class CFileItemList;
+class CGUIViewState;
+namespace
+{
+class CGetDirectoryItems;
+}
+
+// base class for all media windows
+class CGUIMediaWindow : public CGUIWindow
+{
+public:
+ CGUIMediaWindow(int id, const char *xmlFile);
+ ~CGUIMediaWindow(void) override;
+
+ // specializations of CGUIControl
+ bool OnAction(const CAction &action) override;
+ bool OnBack(int actionID) override;
+ bool OnMessage(CGUIMessage& message) override;
+
+ // specializations of CGUIWindow
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ void OnInitWindow() override;
+ bool IsMediaWindow() const override { return true; }
+ int GetViewContainerID() const override { return m_viewControl.GetCurrentControl(); }
+ int GetViewCount() const override { return m_viewControl.GetViewModeCount(); }
+ bool HasListItems() const override { return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+
+ // custom methods
+ virtual bool CanFilterAdvanced() { return m_canFilterAdvanced; }
+ virtual bool IsFiltered();
+ virtual bool IsSameStartFolder(const std::string &dir);
+
+ virtual std::string GetRootPath() const { return ""; }
+
+ const CFileItemList &CurrentDirectory() const;
+ const CGUIViewState *GetViewState() const;
+ virtual bool UseFileDirectories() { return true; }
+
+protected:
+ // specializations of CGUIControlGroup
+ CGUIControl *GetFirstFocusableControl(int id) override;
+
+ bool Load(TiXmlElement *pRootElement) override;
+
+ // custom methods
+ virtual void SetupShares();
+ virtual bool GoParentFolder();
+ virtual bool OnClick(int iItem, const std::string &player = "");
+
+ /* \brief React to a "Select" action on an item in a view.
+ \param item selected item.
+ \return true if the action is handled, false otherwise.
+ */
+ virtual bool OnSelect(int item);
+ virtual bool OnPopupMenu(int iItem);
+
+ virtual void GetContextButtons(int itemNumber, CContextButtons &buttons);
+ virtual bool OnContextButton(int itemNumber, CONTEXT_BUTTON button);
+ virtual bool OnAddMediaSource() { return false; }
+
+ virtual void FormatItemLabels(CFileItemList &items, const LABEL_MASKS &labelMasks);
+ virtual void UpdateButtons();
+ void SaveControlStates() override;
+ void RestoreControlStates() override;
+
+ virtual bool GetDirectory(const std::string &strDirectory, CFileItemList &items);
+ /*! \brief Retrieves the items from the given path and updates the list
+ \param strDirectory The path to the directory to get the items from
+ \param updateFilterPath Whether to update the filter path in m_strFilterPath or not
+ \return true if the list was successfully updated otherwise false
+ \sa GetDirectory
+ \sa m_vecItems
+ \sa m_strFilterPath
+ */
+ virtual bool Update(const std::string &strDirectory, bool updateFilterPath = true);
+ /*! \brief Refreshes the current list by retrieving the lists's path
+ \return true if the list was successfully refreshed otherwise false
+ \sa Update
+ \sa GetDirectory
+ */
+ virtual bool Refresh(bool clearCache = false);
+
+ virtual void FormatAndSort(CFileItemList &items);
+ virtual void OnPrepareFileItems(CFileItemList &items);
+ virtual void OnCacheFileItems(CFileItemList &items);
+ virtual void GetGroupedItems(CFileItemList &items) { }
+
+ void ClearFileItems();
+ virtual void SortItems(CFileItemList &items);
+
+ /*! \brief Check if the given list can be advance filtered or not
+ \param items List of items to check
+ \return true if the list can be advance filtered otherwise false
+ */
+ virtual bool CheckFilterAdvanced(CFileItemList &items) const { return false; }
+ /*! \brief Check if the given path can contain a "filter" parameter
+ \param strDirectory Path to check
+ \return true if the given path can contain a "filter" parameter otherwise false
+ */
+ virtual bool CanContainFilter(const std::string &strDirectory) const { return false; }
+ virtual void UpdateFilterPath(const std::string &strDirector, const CFileItemList &items, bool updateFilterPath);
+ virtual bool Filter(bool advanced = true);
+
+ /* \brief Called on response to a GUI_MSG_FILTER_ITEMS message
+ Filters the current list with the given filter using FilterItems()
+ \param filter the filter to use.
+ \sa FilterItems
+ */
+ void OnFilterItems(const std::string &filter);
+
+ /* \brief Retrieve the filtered item list
+ \param filter filter to apply
+ \param items CFileItemList to filter
+ \sa OnFilterItems
+ */
+ virtual bool GetFilteredItems(const std::string &filter, CFileItemList &items);
+
+ /* \brief Retrieve the advance filtered item list
+ \param items CFileItemList to filter
+ \param hasNewItems Whether the filtered item list contains new items
+ which were not present in the original list
+ \sa GetFilteredItems
+ */
+ virtual bool GetAdvanceFilteredItems(CFileItemList &items);
+
+ // check for a disc or connection
+ virtual bool HaveDiscOrConnection(const std::string& strPath, int iDriveType);
+ void ShowShareErrorMessage(CFileItem* pItem) const;
+
+ void SaveSelectedItemInHistory();
+ void RestoreSelectedItemFromHistory();
+ void GetDirectoryHistoryString(const CFileItem* pItem, std::string& strHistoryString) const;
+ void SetHistoryForPath(const std::string& strDirectory);
+ virtual void LoadPlayList(const std::string& strFileName) {}
+ virtual bool OnPlayMedia(int iItem, const std::string &player = "");
+ virtual bool OnPlayAndQueueMedia(const CFileItemPtr& item, const std::string& player = "");
+ void UpdateFileList();
+ virtual void OnDeleteItem(int iItem);
+ void OnRenameItem(int iItem);
+ bool WaitForNetwork() const;
+ bool GetDirectoryItems(CURL &url, CFileItemList &items, bool useDir);
+ bool WaitGetDirectoryItems(CGetDirectoryItems &items);
+ void CancelUpdateItems();
+
+ /*! \brief Translate the folder to start in from the given quick path
+ \param url the folder the user wants
+ \return the resulting path */
+ virtual std::string GetStartFolder(const std::string &url);
+
+ /*! \brief Utility method to remove the given parameter from a path/URL
+ \param strDirectory Path/URL from which to remove the given parameter
+ \param strParameter Parameter to remove from the given path/URL
+ \return Path/URL without the given parameter
+ */
+ static std::string RemoveParameterFromPath(const std::string &strDirectory, const std::string &strParameter);
+
+ bool ProcessRenderLoop(bool renderOnly);
+
+ XFILE::CVirtualDirectory m_rootDir;
+ CGUIViewControl m_viewControl;
+
+ // current path and history
+ CFileItemList* m_vecItems;
+ CFileItemList* m_unfilteredItems; ///< \brief items prior to filtering using FilterItems()
+ CDirectoryHistory m_history;
+ std::unique_ptr<CGUIViewState> m_guiState;
+ std::atomic_bool m_vecItemsUpdating = {false};
+ class CUpdateGuard
+ {
+ public:
+ CUpdateGuard(std::atomic_bool &update) : m_update(update)
+ {
+ m_update = true;
+ }
+ ~CUpdateGuard()
+ {
+ m_update = false;
+ }
+ protected:
+ std::atomic_bool &m_update;
+ };
+ CEvent m_updateEvent;
+ std::atomic_bool m_updateAborted = {false};
+ std::atomic_bool m_updateJobActive = {false};
+
+ // save control state on window exit
+ int m_iLastControl;
+ std::string m_startDirectory;
+
+ CSmartPlaylist m_filter;
+ bool m_canFilterAdvanced;
+ /*! \brief Contains the path used for filtering (including any active filter)
+
+ When Update() is called with a path to e.g. a smartplaylist or
+ a library node filter, that "original" path will be stored in
+ m_vecItems->m_strPath. But the path used by XBMC to retrieve
+ those items from the database (Videodb:// or musicdb://)
+ is stored in this member variable to still have access to it
+ because it is used for filtering by appending the currently active
+ filter as a "filter" parameter to the filter path/URL.
+
+ \sa Update
+ */
+ std::string m_strFilterPath;
+ bool m_backgroundLoad = false;
+};
diff --git a/xbmc/windows/GUIWindowDebugInfo.cpp b/xbmc/windows/GUIWindowDebugInfo.cpp
new file mode 100644
index 0000000..764e6b0
--- /dev/null
+++ b/xbmc/windows/GUIWindowDebugInfo.cpp
@@ -0,0 +1,185 @@
+/*
+ * 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 "GUIWindowDebugInfo.h"
+
+#include "CompileInfo.h"
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIControlFactory.h"
+#include "guilib/GUIControlProfiler.h"
+#include "guilib/GUIFontManager.h"
+#include "guilib/GUITextLayout.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/WindowTranslator.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/CPUInfo.h"
+#include "utils/MemUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <inttypes.h>
+
+CGUIWindowDebugInfo::CGUIWindowDebugInfo(void)
+ : CGUIDialog(WINDOW_DEBUG_INFO, "", DialogModalityType::MODELESS)
+{
+ m_needsScaling = false;
+ m_layout = nullptr;
+ m_renderOrder = RENDER_ORDER_WINDOW_DEBUG;
+}
+
+CGUIWindowDebugInfo::~CGUIWindowDebugInfo(void) = default;
+
+void CGUIWindowDebugInfo::UpdateVisibility()
+{
+ if (LOG_LEVEL_DEBUG_FREEMEM <= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel || g_SkinInfo->IsDebugging())
+ Open();
+ else
+ Close();
+}
+
+bool CGUIWindowDebugInfo::OnMessage(CGUIMessage &message)
+{
+ if (message.GetMessage() == GUI_MSG_WINDOW_DEINIT)
+ {
+ delete m_layout;
+ m_layout = nullptr;
+ }
+ else if (message.GetMessage() == GUI_MSG_REFRESH_TIMER)
+ MarkDirtyRegion();
+
+ return CGUIDialog::OnMessage(message);
+}
+
+void CGUIWindowDebugInfo::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(), false);
+
+ CServiceBroker::GetCPUInfo()->GetUsedPercentage(); // must call it to recalculate pct values
+
+ static int yShift = 20;
+ static int xShift = 40;
+ static unsigned int lastShift = time(nullptr);
+ time_t now = time(nullptr);
+ if (now - lastShift > 10)
+ {
+ yShift *= -1;
+ if (now % 5 == 0)
+ xShift *= -1;
+ lastShift = now;
+ MarkDirtyRegion();
+ }
+
+ if (!m_layout)
+ {
+ CGUIFont *font13 = g_fontManager.GetDefaultFont();
+ CGUIFont *font13border = g_fontManager.GetDefaultFont(true);
+ if (font13)
+ m_layout = new CGUITextLayout(font13, true, 0, font13border);
+ }
+ if (!m_layout)
+ return;
+
+ std::string info;
+ if (LOG_LEVEL_DEBUG_FREEMEM <= CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel)
+ {
+ KODI::MEMORY::MemoryStatus stat;
+ KODI::MEMORY::GetMemoryStatus(&stat);
+ std::string profiling = CGUIControlProfiler::IsRunning() ? " (profiling)" : "";
+ std::string strCores;
+ if (CServiceBroker::GetCPUInfo()->SupportsCPUUsage())
+ strCores = CServiceBroker::GetCPUInfo()->GetCoresUsageString();
+ else
+ strCores = "N/A";
+ std::string lcAppName = CCompileInfo::GetAppName();
+ StringUtils::ToLower(lcAppName);
+#if !defined(TARGET_POSIX)
+ info = StringUtils::Format("LOG: {}{}.log\nMEM: {}/{} KB - FPS: {:2.1f} fps\nCPU: {}{}",
+ CSpecialProtocol::TranslatePath("special://logpath"), lcAppName,
+ stat.availPhys / 1024, stat.totalPhys / 1024,
+ CServiceBroker::GetGUI()
+ ->GetInfoManager()
+ .GetInfoProviders()
+ .GetSystemInfoProvider()
+ .GetFPS(),
+ strCores, profiling);
+#else
+ double dCPU = m_resourceCounter.GetCPUUsage();
+ std::string ucAppName = lcAppName;
+ StringUtils::ToUpper(ucAppName);
+ info = StringUtils::Format("LOG: {}{}.log\n"
+ "MEM: {}/{} KB - FPS: {:2.1f} fps\n"
+ "CPU: {} (CPU-{} {:4.2f}%{})",
+ CSpecialProtocol::TranslatePath("special://logpath"), lcAppName,
+ stat.availPhys / 1024, stat.totalPhys / 1024,
+ CServiceBroker::GetGUI()
+ ->GetInfoManager()
+ .GetInfoProviders()
+ .GetSystemInfoProvider()
+ .GetFPS(),
+ strCores, ucAppName, dCPU, profiling);
+#endif
+ }
+
+ // render the skin debug info
+ if (g_SkinInfo->IsDebugging())
+ {
+ if (!info.empty())
+ info += "\n";
+ CGUIWindow *window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog());
+ CGUIWindow *pointer = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_DIALOG_POINTER);
+ CPoint point;
+ if (pointer)
+ point = CPoint(pointer->GetXPosition(), pointer->GetYPosition());
+ if (window)
+ {
+ std::string windowName = CWindowTranslator::TranslateWindow(window->GetID());
+ if (!windowName.empty())
+ windowName += " (" + window->GetProperty("xmlfile").asString() + ")";
+ else
+ windowName = window->GetProperty("xmlfile").asString();
+ info += "Window: " + windowName + "\n";
+ // transform the mouse coordinates to this window's coordinates
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetScalingResolution(window->GetCoordsRes(), true);
+ point.x *= CServiceBroker::GetWinSystem()->GetGfxContext().GetGUIScaleX();
+ point.y *= CServiceBroker::GetWinSystem()->GetGfxContext().GetGUIScaleY();
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(), false);
+ }
+ info += StringUtils::Format("Mouse: ({},{}) ", static_cast<int>(point.x),
+ static_cast<int>(point.y));
+ if (window)
+ {
+ CGUIControl *control = window->GetFocusedControl();
+ if (control)
+ info += StringUtils::Format(
+ "Focused: {} ({})", control->GetID(),
+ CGUIControlFactory::TranslateControlType(control->GetControlType()));
+ }
+ }
+
+ float w, h;
+ if (m_layout->Update(info))
+ MarkDirtyRegion();
+ m_layout->GetTextExtent(w, h);
+
+ float x = xShift + 0.04f * CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth();
+ float y = yShift + 0.04f * CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight();
+ m_renderRegion.SetRect(x, y, x+w, y+h);
+}
+
+void CGUIWindowDebugInfo::Render()
+{
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(), false);
+ if (m_layout)
+ m_layout->RenderOutline(m_renderRegion.x1, m_renderRegion.y1, 0xffffffff, 0xff000000, 0, 0);
+}
diff --git a/xbmc/windows/GUIWindowDebugInfo.h b/xbmc/windows/GUIWindowDebugInfo.h
new file mode 100644
index 0000000..b29405c
--- /dev/null
+++ b/xbmc/windows/GUIWindowDebugInfo.h
@@ -0,0 +1,34 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#ifdef TARGET_POSIX
+#include "platform/posix/PosixResourceCounter.h"
+#endif
+
+class CGUITextLayout;
+
+class CGUIWindowDebugInfo :
+ public CGUIDialog
+{
+public:
+ CGUIWindowDebugInfo();
+ ~CGUIWindowDebugInfo() override;
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+ bool OnMessage(CGUIMessage &message) override;
+protected:
+ void UpdateVisibility() override;
+private:
+ CGUITextLayout *m_layout;
+#ifdef TARGET_POSIX
+ CPosixResourceCounter m_resourceCounter;
+#endif
+};
diff --git a/xbmc/windows/GUIWindowFileManager.cpp b/xbmc/windows/GUIWindowFileManager.cpp
new file mode 100644
index 0000000..4d3f399
--- /dev/null
+++ b/xbmc/windows/GUIWindowFileManager.cpp
@@ -0,0 +1,1331 @@
+/*
+ * 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 "GUIWindowFileManager.h"
+
+#include "Autorun.h"
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "PlayListPlayer.h"
+#include "ServiceBroker.h"
+#include "URL.h"
+#include "Util.h"
+#include "application/Application.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "cores/playercorefactory/PlayerCoreFactory.h"
+#include "dialogs/GUIDialogBusy.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "dialogs/GUIDialogMediaSource.h"
+#include "dialogs/GUIDialogProgress.h"
+#include "dialogs/GUIDialogTextViewer.h"
+#include "dialogs/GUIDialogYesNo.h"
+#include "favourites/FavouritesService.h"
+#include "filesystem/Directory.h"
+#include "filesystem/FileDirectoryFactory.h"
+#include "filesystem/ZipManager.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIKeyboardFactory.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/InputManager.h"
+#include "interfaces/generic/ScriptInvocationManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "network/Network.h"
+#include "pictures/GUIWindowSlideShow.h"
+#include "platform/Filesystem.h"
+#include "playlists/PlayList.h"
+#include "playlists/PlayListFactory.h"
+#include "settings/MediaSourceSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "storage/MediaManager.h"
+#include "threads/IRunnable.h"
+#include "utils/FileOperationJob.h"
+#include "utils/FileUtils.h"
+#include "utils/JobManager.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+using namespace XFILE;
+using namespace KODI::MESSAGING;
+
+#define CONTROL_BTNSELECTALL 1
+#define CONTROL_BTNFAVOURITES 2
+#define CONTROL_BTNPLAYWITH 3
+#define CONTROL_BTNRENAME 4
+#define CONTROL_BTNDELETE 5
+#define CONTROL_BTNCOPY 6
+#define CONTROL_BTNMOVE 7
+#define CONTROL_BTNNEWFOLDER 8
+#define CONTROL_BTNCALCSIZE 9
+#define CONTROL_BTNSWITCHMEDIA 11
+#define CONTROL_BTNCANCELJOB 12
+#define CONTROL_BTNVIEW 13
+
+
+#define CONTROL_NUMFILES_LEFT 12
+#define CONTROL_NUMFILES_RIGHT 13
+
+#define CONTROL_LEFT_LIST 20
+#define CONTROL_RIGHT_LIST 21
+
+#define CONTROL_CURRENTDIRLABEL_LEFT 101
+#define CONTROL_CURRENTDIRLABEL_RIGHT 102
+
+namespace
+{
+class CGetDirectoryItems : public IRunnable
+{
+public:
+ CGetDirectoryItems(XFILE::CVirtualDirectory &dir, CURL &url, CFileItemList &items)
+ : m_result(false), m_dir(dir), m_url(url), m_items(items)
+ {
+ }
+ void Run() override
+ {
+ m_result = m_dir.GetDirectory(m_url, m_items, false, false);
+ }
+ void Cancel() override
+ {
+ m_dir.CancelDirectory();
+ }
+ bool m_result;
+protected:
+ XFILE::CVirtualDirectory &m_dir;
+ CURL m_url;
+ CFileItemList &m_items;
+};
+}
+
+CGUIWindowFileManager::CGUIWindowFileManager(void)
+ : CGUIWindow(WINDOW_FILES, "FileManager.xml"),
+ CJobQueue(false,2)
+{
+ m_Directory[0] = new CFileItem;
+ m_Directory[1] = new CFileItem;
+ m_vecItems[0] = new CFileItemList;
+ m_vecItems[1] = new CFileItemList;
+ m_Directory[0]->SetPath("?");
+ m_Directory[1]->SetPath("?");
+ m_Directory[0]->m_bIsFolder = true;
+ m_Directory[1]->m_bIsFolder = true;
+ bCheckShareConnectivity = true;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIWindowFileManager::~CGUIWindowFileManager(void)
+{
+ delete m_Directory[0];
+ delete m_Directory[1];
+ delete m_vecItems[0];
+ delete m_vecItems[1];
+}
+
+bool CGUIWindowFileManager::OnAction(const CAction &action)
+{
+ int list = GetFocusedList();
+ if (list >= 0 && list <= 1)
+ {
+ int item;
+
+ // the non-contextual menu can be called at any time
+ if (action.GetID() == ACTION_CONTEXT_MENU && m_vecItems[list]->Size() == 0)
+ {
+ OnPopupMenu(list,-1, false);
+ return true;
+ }
+ if (action.GetID() == ACTION_DELETE_ITEM)
+ {
+ if (CanDelete(list))
+ {
+ bool bDeselect = SelectItem(list, item);
+ OnDelete(list);
+ if (bDeselect) m_vecItems[list]->Get(item)->Select(false);
+ }
+ return true;
+ }
+ if (action.GetID() == ACTION_COPY_ITEM)
+ {
+ if (CanCopy(list))
+ {
+ bool bDeselect = SelectItem(list, item);
+ OnCopy(list);
+ if (bDeselect) m_vecItems[list]->Get(item)->Select(false);
+ }
+ return true;
+ }
+ if (action.GetID() == ACTION_MOVE_ITEM)
+ {
+ if (CanMove(list))
+ {
+ bool bDeselect = SelectItem(list, item);
+ OnMove(list);
+ if (bDeselect) m_vecItems[list]->Get(item)->Select(false);
+ }
+ return true;
+ }
+ if (action.GetID() == ACTION_RENAME_ITEM)
+ {
+ if (CanRename(list))
+ {
+ bool bDeselect = SelectItem(list, item);
+ OnRename(list);
+ if (bDeselect) m_vecItems[list]->Get(item)->Select(false);
+ }
+ return true;
+ }
+ if (action.GetID() == ACTION_PARENT_DIR)
+ {
+ GoParentFolder(list);
+ return true;
+ }
+ if (action.GetID() == ACTION_PLAYER_PLAY)
+ {
+#ifdef HAS_DVD_DRIVE
+ if (m_vecItems[list]->Get(GetSelectedItem(list))->IsDVD())
+ return MEDIA_DETECT::CAutorun::PlayDiscAskResume(m_vecItems[list]->Get(GetSelectedItem(list))->GetPath());
+#endif
+ }
+ }
+ return CGUIWindow::OnAction(action);
+}
+
+bool CGUIWindowFileManager::OnBack(int actionID)
+{
+ int list = GetFocusedList();
+ if (list >= 0 && list <= 1 && actionID == ACTION_NAV_BACK && !m_vecItems[list]->IsVirtualDirectoryRoot())
+ {
+ GoParentFolder(list);
+ return true;
+ }
+ return CGUIWindow::OnBack(actionID);
+}
+
+bool CGUIWindowFileManager::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_NOTIFY_ALL:
+ { // Message is received even if window is inactive
+ if (message.GetParam1() == GUI_MSG_WINDOW_RESET)
+ {
+ m_Directory[0]->SetPath("?");
+ m_Directory[1]->SetPath("?");
+ m_Directory[0]->m_bIsFolder = true;
+ m_Directory[1]->m_bIsFolder = true;
+ return true;
+ }
+
+ // handle removable media
+ if (message.GetParam1() == GUI_MSG_REMOVED_MEDIA)
+ {
+ for (int i = 0; i < 2; i++)
+ {
+ if (m_Directory[i]->IsVirtualDirectoryRoot() && IsActive())
+ {
+ int iItem = GetSelectedItem(i);
+ Update(i, m_Directory[i]->GetPath());
+ CONTROL_SELECT_ITEM(CONTROL_LEFT_LIST + i, iItem);
+ }
+ else if (m_Directory[i]->IsRemovable() && !m_rootDir.IsInSource(m_Directory[i]->GetPath()))
+ { //
+ if (IsActive())
+ Update(i, "");
+ else
+ m_Directory[i]->SetPath("");
+ }
+ }
+ return true;
+ }
+ else if (message.GetParam1()==GUI_MSG_UPDATE_SOURCES)
+ { // State of the sources changed, so update our view
+ for (int i = 0; i < 2; i++)
+ {
+ if (m_Directory[i]->IsVirtualDirectoryRoot() && IsActive())
+ {
+ int iItem = GetSelectedItem(i);
+ Update(i, m_Directory[i]->GetPath());
+ CONTROL_SELECT_ITEM(CONTROL_LEFT_LIST + i, iItem);
+ }
+ }
+ return true;
+ }
+ else if (message.GetParam1()==GUI_MSG_UPDATE && IsActive())
+ {
+ Refresh();
+ return true;
+ }
+ }
+ break;
+ case GUI_MSG_PLAYBACK_STARTED:
+ case GUI_MSG_PLAYBACK_ENDED:
+ case GUI_MSG_PLAYBACK_STOPPED:
+ case GUI_MSG_PLAYLIST_CHANGED:
+ case GUI_MSG_PLAYLISTPLAYER_STOPPED:
+ case GUI_MSG_PLAYLISTPLAYER_STARTED:
+ case GUI_MSG_PLAYLISTPLAYER_CHANGED:
+ { // send a notify all to all controls on this window
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, GetID(), 0, GUI_MSG_REFRESH_LIST);
+ OnMessage(msg);
+ break;
+ }
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ CGUIWindow::OnMessage(message);
+ ClearFileItems(0);
+ ClearFileItems(1);
+ return true;
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ SetInitialPath(message.GetStringParam());
+ message.SetStringParam("");
+
+ return CGUIWindow::OnMessage(message);
+ }
+ break;
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+
+ if (iControl == CONTROL_LEFT_LIST || iControl == CONTROL_RIGHT_LIST) // list/thumb control
+ {
+ // get selected item
+ int list = iControl - CONTROL_LEFT_LIST;
+ int iItem = GetSelectedItem(list);
+ int iAction = message.GetParam1();
+
+ // iItem is checked for validity inside these routines
+ if (iAction == ACTION_HIGHLIGHT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK)
+ {
+ OnMark(list, iItem);
+ if (!CServiceBroker::GetInputManager().IsMouseActive())
+ {
+ //move to next item
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), iControl, iItem + 1);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+ }
+ }
+ else if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_DOUBLE_CLICK)
+ {
+ OnClick(list, iItem);
+ }
+ else if (iAction == ACTION_CONTEXT_MENU || iAction == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ OnPopupMenu(list, iItem);
+ }
+ }
+ }
+ break;
+ // prevent touch/gesture unfocussing ..
+ case GUI_MSG_GESTURE_NOTIFY:
+ case GUI_MSG_UNFOCUS_ALL:
+ return true;
+ }
+ return CGUIWindow::OnMessage(message);
+}
+
+void CGUIWindowFileManager::OnSort(int iList)
+{
+ using namespace KODI::PLATFORM::FILESYSTEM;
+ // always sort the list by label in ascending order
+ for (int i = 0; i < m_vecItems[iList]->Size(); i++)
+ {
+ CFileItemPtr pItem = m_vecItems[iList]->Get(i);
+ if (pItem->m_bIsFolder && (!pItem->m_dwSize || pItem->IsPath("add")))
+ pItem->SetLabel2("");
+ else
+ pItem->SetFileSizeLabel();
+
+ // Set free space on disc
+ if (pItem->m_bIsShareOrDrive)
+ {
+ if (pItem->IsHD())
+ {
+ std::error_code ec;
+ auto freeSpace = space(pItem->GetPath(), ec);
+ if (ec.value() == 0)
+ {
+ pItem->m_dwSize = freeSpace.free;
+ pItem->SetFileSizeLabel();
+ }
+ }
+ else if (pItem->IsDVD() && CServiceBroker::GetMediaManager().IsDiscInDrive())
+ {
+ std::error_code ec;
+ auto freeSpace = space(pItem->GetPath(), ec);
+ if (ec.value() == 0)
+ {
+ pItem->m_dwSize = freeSpace.capacity;
+ pItem->SetFileSizeLabel();
+ }
+ }
+ } // if (pItem->m_bIsShareOrDrive)
+
+ }
+
+ m_vecItems[iList]->Sort(SortByLabel, SortOrderAscending);
+}
+
+void CGUIWindowFileManager::ClearFileItems(int iList)
+{
+ CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), iList + CONTROL_LEFT_LIST);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+
+ m_vecItems[iList]->Clear(); // will clean up everything
+}
+
+void CGUIWindowFileManager::UpdateButtons()
+{
+ // update our current directory labels
+ std::string strDir = CURL(m_Directory[0]->GetPath()).GetWithoutUserDetails();
+ if (strDir.empty())
+ {
+ SET_CONTROL_LABEL(CONTROL_CURRENTDIRLABEL_LEFT,g_localizeStrings.Get(20108));
+ }
+ else
+ {
+ SET_CONTROL_LABEL(CONTROL_CURRENTDIRLABEL_LEFT, strDir);
+ }
+ strDir = CURL(m_Directory[1]->GetPath()).GetWithoutUserDetails();
+ if (strDir.empty())
+ {
+ SET_CONTROL_LABEL(CONTROL_CURRENTDIRLABEL_RIGHT,g_localizeStrings.Get(20108));
+ }
+ else
+ {
+ SET_CONTROL_LABEL(CONTROL_CURRENTDIRLABEL_RIGHT, strDir);
+ }
+
+ // update the number of items in each list
+ UpdateItemCounts();
+}
+
+void CGUIWindowFileManager::UpdateItemCounts()
+{
+ for (int i = 0; i < 2; i++)
+ {
+ unsigned int selectedCount = 0;
+ unsigned int totalCount = 0;
+ int64_t selectedSize = 0;
+ for (int j = 0; j < m_vecItems[i]->Size(); j++)
+ {
+ CFileItemPtr item = m_vecItems[i]->Get(j);
+ if (item->IsParentFolder()) continue;
+ if (item->IsSelected())
+ {
+ selectedCount++;
+ selectedSize += item->m_dwSize;
+ }
+ totalCount++;
+ }
+ std::string items;
+ if (selectedCount > 0)
+ items =
+ StringUtils::Format("{}/{} {} ({})", selectedCount, totalCount,
+ g_localizeStrings.Get(127), StringUtils::SizeToString(selectedSize));
+ else
+ items = StringUtils::Format("{} {}", totalCount, g_localizeStrings.Get(127));
+ SET_CONTROL_LABEL(CONTROL_NUMFILES_LEFT + i, items);
+ }
+}
+
+bool CGUIWindowFileManager::Update(int iList, const std::string &strDirectory)
+{
+ if (m_updating)
+ {
+ CLog::Log(LOGWARNING, "CGUIWindowFileManager::Update - updating in progress");
+ return true;
+ }
+ CUpdateGuard ug(m_updating);
+
+ // get selected item
+ int iItem = GetSelectedItem(iList);
+ std::string strSelectedItem = "";
+
+ if (iItem >= 0 && iItem < m_vecItems[iList]->Size())
+ {
+ CFileItemPtr pItem = m_vecItems[iList]->Get(iItem);
+ if (!pItem->IsParentFolder())
+ {
+ GetDirectoryHistoryString(pItem.get(), strSelectedItem);
+ m_history[iList].SetSelectedItem(strSelectedItem, m_Directory[iList]->GetPath());
+ }
+ }
+
+ std::string strOldDirectory=m_Directory[iList]->GetPath();
+ m_Directory[iList]->SetPath(strDirectory);
+
+ CFileItemList items;
+ if (!GetDirectory(iList, m_Directory[iList]->GetPath(), items))
+ {
+ if (strDirectory != strOldDirectory && GetDirectory(iList, strOldDirectory, items))
+ m_Directory[iList]->SetPath(strOldDirectory); // Fallback to old (previous) path)
+ else
+ Update(iList, ""); // Fallback to root
+
+ return false;
+ }
+
+ m_history[iList].SetSelectedItem(strSelectedItem, strOldDirectory);
+
+ ClearFileItems(iList);
+
+ m_vecItems[iList]->Append(items);
+ m_vecItems[iList]->SetPath(items.GetPath());
+
+ std::string strParentPath;
+ URIUtils::GetParentPath(strDirectory, strParentPath);
+ if (strDirectory.empty() && (m_vecItems[iList]->Size() == 0 || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWADDSOURCEBUTTONS)))
+ { // add 'add source button'
+ const std::string& strLabel = g_localizeStrings.Get(1026);
+ CFileItemPtr pItem(new CFileItem(strLabel));
+ pItem->SetPath("add");
+ pItem->SetArt("icon", "DefaultAddSource.png");
+ pItem->SetLabel(strLabel);
+ pItem->SetLabelPreformatted(true);
+ pItem->m_bIsFolder = true;
+ pItem->SetSpecialSort(SortSpecialOnBottom);
+ m_vecItems[iList]->Add(pItem);
+ }
+ else if (items.IsEmpty() || CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWPARENTDIRITEMS))
+ {
+ CFileItemPtr pItem(new CFileItem(".."));
+ pItem->SetPath(m_rootDir.IsSource(strDirectory) ? "" : strParentPath);
+ pItem->m_bIsFolder = true;
+ pItem->m_bIsShareOrDrive = false;
+ m_vecItems[iList]->AddFront(pItem, 0);
+ }
+
+ m_strParentPath[iList] = (m_rootDir.IsSource(strDirectory) ? "" : strParentPath);
+
+ if (strDirectory.empty())
+ {
+ CFileItemPtr pItem(new CFileItem("special://profile/", true));
+ pItem->SetLabel(g_localizeStrings.Get(20070));
+ pItem->SetArt("thumb", "DefaultFolder.png");
+ pItem->SetLabelPreformatted(true);
+ m_vecItems[iList]->Add(pItem);
+
+ #ifdef TARGET_DARWIN_EMBEDDED
+ CFileItemPtr iItem(new CFileItem("special://envhome/Documents/Inbox", true));
+ iItem->SetLabel("Inbox");
+ iItem->SetArt("thumb", "DefaultFolder.png");
+ iItem->SetLabelPreformatted(true);
+ m_vecItems[iList]->Add(iItem);
+ #endif
+ }
+
+ // if we have a .tbn file, use itself as the thumb
+ for (int i = 0; i < m_vecItems[iList]->Size(); i++)
+ {
+ CFileItemPtr pItem = m_vecItems[iList]->Get(i);
+ if (pItem->IsHD() &&
+ URIUtils::HasExtension(pItem->GetPath(), ".tbn"))
+ {
+ pItem->SetArt("thumb", pItem->GetPath());
+ }
+ }
+ m_vecItems[iList]->FillInDefaultIcons();
+
+ OnSort(iList);
+ UpdateButtons();
+
+ int item = 0;
+ strSelectedItem = m_history[iList].GetSelectedItem(m_Directory[iList]->GetPath());
+ for (int i = 0; i < m_vecItems[iList]->Size(); ++i)
+ {
+ CFileItemPtr pItem = m_vecItems[iList]->Get(i);
+ std::string strHistory;
+ GetDirectoryHistoryString(pItem.get(), strHistory);
+ if (strHistory == strSelectedItem)
+ {
+ item = i;
+ break;
+ }
+ }
+ UpdateControl(iList, item);
+ return true;
+}
+
+
+void CGUIWindowFileManager::OnClick(int iList, int iItem)
+{
+ if ( iList < 0 || iList >= 2) return ;
+ if ( iItem < 0 || iItem >= m_vecItems[iList]->Size() ) return ;
+
+ CFileItemPtr pItem = m_vecItems[iList]->Get(iItem);
+ if (pItem->GetPath() == "add" && pItem->GetLabel() == g_localizeStrings.Get(1026)) // 'add source button' in empty root
+ {
+ if (CGUIDialogMediaSource::ShowAndAddMediaSource("files"))
+ {
+ m_rootDir.SetSources(*CMediaSourceSettings::GetInstance().GetSources("files"));
+ Update(0,m_Directory[0]->GetPath());
+ Update(1,m_Directory[1]->GetPath());
+ }
+ return;
+ }
+
+ if (!pItem->m_bIsFolder && pItem->IsFileFolder(EFILEFOLDER_MASK_ALL))
+ {
+ XFILE::IFileDirectory *pFileDirectory = NULL;
+ pFileDirectory = XFILE::CFileDirectoryFactory::Create(pItem->GetURL(), pItem.get(), "");
+ if(pFileDirectory)
+ pItem->m_bIsFolder = true;
+ else if(pItem->m_bIsFolder)
+ pItem->m_bIsFolder = false;
+ delete pFileDirectory;
+ }
+
+ if (pItem->m_bIsFolder)
+ {
+ // save path + drive type because of the possible refresh
+ std::string strPath = pItem->GetPath();
+ int iDriveType = pItem->m_iDriveType;
+ if ( pItem->m_bIsShareOrDrive )
+ {
+ if ( !g_passwordManager.IsItemUnlocked( pItem.get(), "files" ) )
+ {
+ Refresh();
+ return ;
+ }
+
+ if ( !HaveDiscOrConnection( strPath, iDriveType ) )
+ return ;
+ }
+ if (!Update(iList, strPath))
+ ShowShareErrorMessage(pItem.get());
+ }
+ else if (pItem->IsZIP() || pItem->IsCBZ()) // mount zip archive
+ {
+ CURL pathToUrl = URIUtils::CreateArchivePath("zip", pItem->GetURL(), "");
+ Update(iList, pathToUrl.Get());
+ }
+ else if (pItem->IsRAR() || pItem->IsCBR())
+ {
+ CURL pathToUrl = URIUtils::CreateArchivePath("rar", pItem->GetURL(), "");
+ Update(iList, pathToUrl.Get());
+ }
+ else
+ {
+ OnStart(pItem.get(), "");
+ return ;
+ }
+ // UpdateButtons();
+}
+
+//! @todo 2.0: Can this be removed, or should we run without the "special" file directories while
+// in filemanager view.
+void CGUIWindowFileManager::OnStart(CFileItem *pItem, const std::string &player)
+{
+ // start playlists from file manager
+ if (pItem->IsPlayList())
+ {
+ const std::string& strPlayList = pItem->GetPath();
+ std::unique_ptr<PLAYLIST::CPlayList> pPlayList(PLAYLIST::CPlayListFactory::Create(strPlayList));
+ if (nullptr != pPlayList)
+ {
+ if (!pPlayList->Load(strPlayList))
+ {
+ HELPERS::ShowOKDialogText(CVariant{6}, CVariant{477});
+ return;
+ }
+ }
+ g_application.ProcessAndStartPlaylist(strPlayList, *pPlayList, PLAYLIST::TYPE_MUSIC);
+ return;
+ }
+ if (pItem->IsAudio() || pItem->IsVideo())
+ {
+ CServiceBroker::GetPlaylistPlayer().Play(std::make_shared<CFileItem>(*pItem), player);
+ return;
+ }
+ if (pItem->IsGame())
+ {
+ g_application.PlayFile(*pItem, player);
+ return ;
+ }
+#ifdef HAS_PYTHON
+ if (pItem->IsPythonScript())
+ {
+ CScriptInvocationManager::GetInstance().ExecuteAsync(pItem->GetPath());
+ return ;
+ }
+#endif
+ if (pItem->IsPicture())
+ {
+ CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW);
+ if (!pSlideShow)
+ return ;
+
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlayingVideo())
+ g_application.StopPlaying();
+
+ pSlideShow->Reset();
+ pSlideShow->Add(pItem);
+ pSlideShow->Select(pItem->GetPath());
+
+ CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SLIDESHOW);
+ return;
+ }
+ if (pItem->IsType(".txt") || pItem->IsType(".xml"))
+ CGUIDialogTextViewer::ShowForFile(pItem->GetPath(), true);
+}
+
+bool CGUIWindowFileManager::HaveDiscOrConnection( std::string& strPath, int iDriveType )
+{
+ if ( iDriveType == CMediaSource::SOURCE_TYPE_DVD )
+ {
+ if (!CServiceBroker::GetMediaManager().IsDiscInDrive(strPath))
+ {
+ HELPERS::ShowOKDialogText(CVariant{218}, CVariant{219});
+ int iList = GetFocusedList();
+ int iItem = GetSelectedItem(iList);
+ Update(iList, "");
+ CONTROL_SELECT_ITEM(iList + CONTROL_LEFT_LIST, iItem);
+ return false;
+ }
+ }
+ else if ( iDriveType == CMediaSource::SOURCE_TYPE_REMOTE )
+ {
+ //! @todo Handle not connected to a remote share
+ if (!CServiceBroker::GetNetwork().IsConnected())
+ {
+ HELPERS::ShowOKDialogText(CVariant{220}, CVariant{221});
+ return false;
+ }
+ }
+ else
+ return true;
+ return true;
+}
+
+void CGUIWindowFileManager::UpdateControl(int iList, int item)
+{
+ CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), iList + CONTROL_LEFT_LIST, item, 0, m_vecItems[iList]);
+ CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg);
+}
+
+void CGUIWindowFileManager::OnMark(int iList, int iItem)
+{
+ CFileItemPtr pItem = m_vecItems[iList]->Get(iItem);
+
+ if (!pItem->m_bIsShareOrDrive)
+ {
+ if (!pItem->IsParentFolder())
+ {
+ // MARK file
+ pItem->Select(!pItem->IsSelected());
+ }
+ }
+
+ UpdateItemCounts();
+ // UpdateButtons();
+}
+
+void CGUIWindowFileManager::OnCopy(int iList)
+{
+ if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{120}, CVariant{123}))
+ return;
+
+ AddJob(new CFileOperationJob(CFileOperationJob::ActionCopy,
+ *m_vecItems[iList],
+ m_Directory[1 - iList]->GetPath(),
+ true, 16201, 16202));
+}
+
+void CGUIWindowFileManager::OnMove(int iList)
+{
+ if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{121}, CVariant{124}))
+ return;
+
+ AddJob(new CFileOperationJob(CFileOperationJob::ActionMove,
+ *m_vecItems[iList],
+ m_Directory[1 - iList]->GetPath(),
+ true, 16203, 16204));
+}
+
+void CGUIWindowFileManager::OnDelete(int iList)
+{
+ if (!CGUIDialogYesNo::ShowAndGetInput(CVariant{122}, CVariant{125}))
+ return;
+
+ AddJob(new CFileOperationJob(CFileOperationJob::ActionDelete,
+ *m_vecItems[iList],
+ m_Directory[iList]->GetPath(),
+ true, 16205, 16206));
+}
+
+void CGUIWindowFileManager::OnRename(int iList)
+{
+ std::string strFile;
+ for (int i = 0; i < m_vecItems[iList]->Size();++i)
+ {
+ CFileItemPtr pItem = m_vecItems[iList]->Get(i);
+ if (pItem->IsSelected())
+ {
+ strFile = pItem->GetPath();
+ break;
+ }
+ }
+
+ CFileUtils::RenameFile(strFile);
+
+ Refresh(iList);
+}
+
+void CGUIWindowFileManager::OnSelectAll(int iList)
+{
+ for (int i = 0; i < m_vecItems[iList]->Size();++i)
+ {
+ CFileItemPtr pItem = m_vecItems[iList]->Get(i);
+ if (!pItem->IsParentFolder())
+ {
+ pItem->Select(true);
+ }
+ }
+}
+
+void CGUIWindowFileManager::OnNewFolder(int iList)
+{
+ std::string strNewFolder = "";
+ if (CGUIKeyboardFactory::ShowAndGetInput(strNewFolder, CVariant{g_localizeStrings.Get(16014)}, false))
+ {
+ std::string strNewPath = m_Directory[iList]->GetPath();
+ URIUtils::AddSlashAtEnd(strNewPath);
+ strNewPath += strNewFolder;
+ CDirectory::Create(strNewPath);
+ Refresh(iList);
+
+ // select the new folder
+ for (int i=0; i<m_vecItems[iList]->Size(); ++i)
+ {
+ CFileItemPtr pItem=m_vecItems[iList]->Get(i);
+ std::string strPath=pItem->GetPath();
+ URIUtils::RemoveSlashAtEnd(strPath);
+ if (strPath==strNewPath)
+ {
+ CONTROL_SELECT_ITEM(iList + CONTROL_LEFT_LIST, i);
+ break;
+ }
+ }
+ }
+}
+
+void CGUIWindowFileManager::Refresh(int iList)
+{
+ int nSel = GetSelectedItem(iList);
+ // update the list views
+ Update(iList, m_Directory[iList]->GetPath());
+
+ while (nSel > m_vecItems[iList]->Size())
+ nSel--;
+
+ CONTROL_SELECT_ITEM(iList + CONTROL_LEFT_LIST, nSel);
+}
+
+
+void CGUIWindowFileManager::Refresh()
+{
+ int iList = GetFocusedList();
+ int nSel = GetSelectedItem(iList);
+ // update the list views
+ Update(0, m_Directory[0]->GetPath());
+ Update(1, m_Directory[1]->GetPath());
+
+ while (nSel > m_vecItems[iList]->Size())
+ nSel--;
+
+ CONTROL_SELECT_ITEM(iList + CONTROL_LEFT_LIST, nSel);
+}
+
+int CGUIWindowFileManager::GetSelectedItem(int iControl)
+{
+ if (iControl < 0 || iControl > 1 || m_vecItems[iControl]->IsEmpty())
+ return -1;
+ CGUIMessage msg(GUI_MSG_ITEM_SELECTED, GetID(), iControl + CONTROL_LEFT_LIST);
+ if (OnMessage(msg))
+ return msg.GetParam1();
+ return -1;
+}
+
+void CGUIWindowFileManager::GoParentFolder(int iList)
+{
+ CURL url(m_Directory[iList]->GetPath());
+ if (url.IsProtocol("rar") || url.IsProtocol("zip"))
+ {
+ // check for step-below, if, unmount rar
+ if (url.GetFileName().empty())
+ if (url.IsProtocol("zip"))
+ g_ZipManager.release(m_Directory[iList]->GetPath()); // release resources
+ }
+
+ std::string strPath(m_strParentPath[iList]), strOldPath(m_Directory[iList]->GetPath());
+ Update(iList, strPath);
+}
+
+/// \brief Build a directory history string
+/// \param pItem Item to build the history string from
+/// \param strHistoryString History string build as return value
+void CGUIWindowFileManager::GetDirectoryHistoryString(const CFileItem* pItem, std::string& strHistoryString)
+{
+ if (pItem->m_bIsShareOrDrive)
+ {
+ // We are in the virtual directory
+
+ // History string of the DVD drive
+ // must be handled separately
+ if (pItem->m_iDriveType == CMediaSource::SOURCE_TYPE_DVD)
+ {
+ // Remove disc label from item label
+ // and use as history string, m_strPath
+ // can change for new discs
+ std::string strLabel = pItem->GetLabel();
+ size_t nPosOpen = strLabel.find('(');
+ size_t nPosClose = strLabel.rfind(')');
+ if (nPosOpen != std::string::npos &&
+ nPosClose != std::string::npos &&
+ nPosClose > nPosOpen)
+ {
+ strLabel.erase(nPosOpen + 1, (nPosClose) - (nPosOpen + 1));
+ strHistoryString = strLabel;
+ }
+ else
+ strHistoryString = strLabel;
+ }
+ else
+ {
+ // Other items in virtual directory
+ strHistoryString = pItem->GetLabel() + pItem->GetPath();
+ URIUtils::RemoveSlashAtEnd(strHistoryString);
+ }
+ }
+ else
+ {
+ // Normal directory items
+ strHistoryString = pItem->GetPath();
+ URIUtils::RemoveSlashAtEnd(strHistoryString);
+ }
+}
+
+bool CGUIWindowFileManager::GetDirectory(int iList, const std::string &strDirectory, CFileItemList &items)
+{
+ CURL pathToUrl(strDirectory);
+
+ CGetDirectoryItems getItems(m_rootDir, pathToUrl, items);
+ if (!CGUIDialogBusy::Wait(&getItems, 100, true))
+ {
+ return false;
+ }
+ return getItems.m_result;
+}
+
+bool CGUIWindowFileManager::CanRename(int iList)
+{
+ //! @todo Renaming of shares (requires writing to sources.xml)
+ //! this might be able to be done via the webserver code stuff...
+ if (m_Directory[iList]->IsVirtualDirectoryRoot()) return false;
+ if (m_Directory[iList]->IsReadOnly()) return false;
+
+ return true;
+}
+
+bool CGUIWindowFileManager::CanCopy(int iList)
+{
+ // can't copy if the destination is not writeable, or if the source is a share!
+ //! @todo Perhaps if the source is removeable media (DVD/CD etc.) we could
+ //! put ripping/backup in here.
+ if (!CUtil::SupportsReadFileOperations(m_Directory[iList]->GetPath())) return false;
+ if (m_Directory[iList]->IsVirtualDirectoryRoot()) return false;
+ if (m_Directory[1 - iList]->IsVirtualDirectoryRoot()) return false;
+ if (m_Directory[iList]->IsVirtualDirectoryRoot()) return false;
+ if (m_Directory[1 -iList]->IsReadOnly()) return false;
+ return true;
+}
+
+bool CGUIWindowFileManager::CanMove(int iList)
+{
+ // can't move if the destination is not writeable, or if the source is a share or not writeable!
+ if (m_Directory[0]->IsVirtualDirectoryRoot() || m_Directory[0]->IsReadOnly()) return false;
+ if (m_Directory[1]->IsVirtualDirectoryRoot() || m_Directory[1]->IsReadOnly()) return false;
+ return true;
+}
+
+bool CGUIWindowFileManager::CanDelete(int iList)
+{
+ if (m_Directory[iList]->IsVirtualDirectoryRoot()) return false;
+ if (m_Directory[iList]->IsReadOnly()) return false;
+ return true;
+}
+
+bool CGUIWindowFileManager::CanNewFolder(int iList)
+{
+ if (m_Directory[iList]->IsVirtualDirectoryRoot()) return false;
+ if (m_Directory[iList]->IsReadOnly()) return false;
+ return true;
+}
+
+int CGUIWindowFileManager::NumSelected(int iList)
+{
+ int iSelectedItems = 0;
+ for (int iItem = 0; iItem < m_vecItems[iList]->Size(); ++iItem)
+ {
+ if (m_vecItems[iList]->Get(iItem)->IsSelected()) iSelectedItems++;
+ }
+ return iSelectedItems;
+}
+
+int CGUIWindowFileManager::GetFocusedList() const
+{
+ return GetFocusedControlID() - CONTROL_LEFT_LIST;
+}
+
+void CGUIWindowFileManager::OnPopupMenu(int list, int item, bool bContextDriven /* = true */)
+{
+ if (list < 0 || list >= 2) return ;
+ bool bDeselect = SelectItem(list, item);
+ // calculate the position for our menu
+ float posX = 200;
+ float posY = 100;
+ const CGUIControl *pList = GetControl(CONTROL_LEFT_LIST + list);
+ if (pList)
+ {
+ posX = pList->GetXPosition() + pList->GetWidth() / 2;
+ posY = pList->GetYPosition() + pList->GetHeight() / 2;
+ }
+
+ CFileItemPtr pItem = m_vecItems[list]->Get(item);
+ if (!pItem.get())
+ return;
+
+ if (m_Directory[list]->IsVirtualDirectoryRoot())
+ {
+ if (item < 0)
+ { //! @todo We should add the option here for shares to be added if there aren't any
+ return ;
+ }
+
+ // and do the popup menu
+ if (CGUIDialogContextMenu::SourcesMenu("files", pItem, posX, posY))
+ {
+ m_rootDir.SetSources(*CMediaSourceSettings::GetInstance().GetSources("files"));
+ if (m_Directory[1 - list]->IsVirtualDirectoryRoot())
+ Refresh();
+ else
+ Refresh(list);
+ return ;
+ }
+ pItem->Select(false);
+ return ;
+ }
+
+ const CPlayerCoreFactory &playerCoreFactory = CServiceBroker::GetPlayerCoreFactory();
+
+ // popup the context menu
+
+ bool showEntry = false;
+ if (item >= m_vecItems[list]->Size()) item = -1;
+ if (item >= 0)
+ showEntry=(!pItem->IsParentFolder() || (pItem->IsParentFolder() && m_vecItems[list]->GetSelectedCount()>0));
+
+ // determine available players
+ std::vector<std::string>players;
+ playerCoreFactory.GetPlayers(*pItem, players);
+
+ // add the needed buttons
+ CContextButtons choices;
+ if (item >= 0)
+ {
+ //The ".." item is not selectable. Take that into account when figuring out if all items are selected
+ int notSelectable = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWPARENTDIRITEMS) ? 1 : 0;
+ if (NumSelected(list) < m_vecItems[list]->Size() - notSelectable)
+ choices.Add(CONTROL_BTNSELECTALL, 188); // SelectAll
+ if (!pItem->IsParentFolder())
+ choices.Add(CONTROL_BTNFAVOURITES, CServiceBroker::GetFavouritesService().IsFavourited(*pItem.get(), GetID()) ? 14077 : 14076); // Add/Remove Favourite
+ if (players.size() > 1)
+ choices.Add(CONTROL_BTNPLAYWITH, 15213);
+ if (CanRename(list) && !pItem->IsParentFolder())
+ choices.Add(CONTROL_BTNRENAME, 118);
+ if (CanDelete(list) && showEntry)
+ choices.Add(CONTROL_BTNDELETE, 117);
+ if (CanCopy(list) && showEntry)
+ choices.Add(CONTROL_BTNCOPY, 115);
+ if (CanMove(list) && showEntry)
+ choices.Add(CONTROL_BTNMOVE, 116);
+ }
+ if (CanNewFolder(list))
+ choices.Add(CONTROL_BTNNEWFOLDER, 20309);
+ if (item >= 0 && pItem->m_bIsFolder && !pItem->IsParentFolder())
+ choices.Add(CONTROL_BTNCALCSIZE, 13393);
+ choices.Add(CONTROL_BTNSWITCHMEDIA, 523);
+ if (CServiceBroker::GetJobManager()->IsProcessing("filemanager"))
+ choices.Add(CONTROL_BTNCANCELJOB, 167);
+
+ if (!pItem->m_bIsFolder)
+ choices.Add(CONTROL_BTNVIEW, 39104);
+
+ int btnid = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (btnid == CONTROL_BTNSELECTALL)
+ {
+ OnSelectAll(list);
+ bDeselect=false;
+ }
+ if (btnid == CONTROL_BTNFAVOURITES)
+ {
+ CServiceBroker::GetFavouritesService().AddOrRemove(*pItem.get(), GetID());
+ return;
+ }
+ if (btnid == CONTROL_BTNPLAYWITH)
+ {
+ std::vector<std::string>players;
+ playerCoreFactory.GetPlayers(*pItem, players);
+ std::string player = playerCoreFactory.SelectPlayerDialog(players);
+ if (!player.empty())
+ OnStart(pItem.get(), player);
+ }
+ if (btnid == CONTROL_BTNRENAME)
+ OnRename(list);
+ if (btnid == CONTROL_BTNDELETE)
+ OnDelete(list);
+ if (btnid == CONTROL_BTNCOPY)
+ OnCopy(list);
+ if (btnid == CONTROL_BTNMOVE)
+ OnMove(list);
+ if (btnid == CONTROL_BTNNEWFOLDER)
+ OnNewFolder(list);
+ if (btnid == CONTROL_BTNCALCSIZE)
+ {
+ // setup the progress dialog, and show it
+ CGUIDialogProgress *progress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS);
+ if (progress)
+ {
+ progress->SetHeading(CVariant{13394});
+ for (int i=0; i < 3; i++)
+ progress->SetLine(i, CVariant{""});
+ progress->Open();
+ }
+
+ // Calculate folder size for each selected item
+ for (int i=0; i<m_vecItems[list]->Size(); ++i)
+ {
+ CFileItemPtr pItem2=m_vecItems[list]->Get(i);
+ if (pItem2->m_bIsFolder && pItem2->IsSelected())
+ {
+ int64_t folderSize = CalculateFolderSize(pItem2->GetPath(), progress);
+ if (folderSize >= 0)
+ {
+ pItem2->m_dwSize = folderSize;
+ if (folderSize == 0)
+ pItem2->SetLabel2(StringUtils::SizeToString(folderSize));
+ else
+ pItem2->SetFileSizeLabel();
+ }
+ }
+ }
+ if (progress)
+ progress->Close();
+ }
+ if (btnid == CONTROL_BTNSWITCHMEDIA)
+ {
+ CGUIDialogContextMenu::SwitchMedia("files", m_vecItems[list]->GetPath());
+ return;
+ }
+ if (btnid == CONTROL_BTNCANCELJOB)
+ CancelJobs();
+ if (btnid == CONTROL_BTNVIEW)
+ CGUIDialogTextViewer::ShowForFile(pItem->GetPath(), true);
+
+ if (bDeselect && item >= 0 && item < m_vecItems[list]->Size())
+ { // deselect item as we didn't do anything
+ pItem->Select(false);
+ }
+}
+
+// Highlights the item in the list under the cursor
+// returns true if we should deselect the item, false otherwise
+bool CGUIWindowFileManager::SelectItem(int list, int &item)
+{
+ // get the currently selected item in the list
+ item = GetSelectedItem(list);
+
+ // select the item if we need to
+ if (item > -1 && !NumSelected(list) && !m_vecItems[list]->Get(item)->IsParentFolder())
+ {
+ m_vecItems[list]->Get(item)->Select(true);
+ return true;
+ }
+ return false;
+}
+
+// recursively calculates the selected folder size
+int64_t CGUIWindowFileManager::CalculateFolderSize(const std::string &strDirectory, CGUIDialogProgress *pProgress)
+{
+ const CURL pathToUrl(strDirectory);
+ if (pProgress)
+ { // update our progress control
+ pProgress->Progress();
+ pProgress->SetLine(1, strDirectory);
+ if (pProgress->IsCanceled())
+ return -1;
+ }
+ // start by calculating the size of the files in this folder...
+ int64_t totalSize = 0;
+ CFileItemList items;
+ CVirtualDirectory rootDir;
+ rootDir.SetSources(*CMediaSourceSettings::GetInstance().GetSources("files"));
+ rootDir.GetDirectory(pathToUrl, items, false, false);
+ for (int i=0; i < items.Size(); i++)
+ {
+ if (items[i]->m_bIsFolder && !items[i]->IsParentFolder()) // folder
+ {
+ int64_t folderSize = CalculateFolderSize(items[i]->GetPath(), pProgress);
+ if (folderSize < 0) return -1;
+ totalSize += folderSize;
+ }
+ else // file
+ totalSize += items[i]->m_dwSize;
+ }
+ return totalSize;
+}
+
+void CGUIWindowFileManager::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ if(!success)
+ {
+ CFileOperationJob* fileJob = static_cast<CFileOperationJob*>(job);
+ HELPERS::ShowOKDialogLines(CVariant{fileJob->GetHeading()},
+ CVariant{fileJob->GetLine()}, CVariant{16200}, CVariant{0});
+ }
+
+ if (IsActive())
+ {
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, GetID(), 0, GUI_MSG_UPDATE);
+ CServiceBroker::GetAppMessenger()->SendGUIMessage(msg, GetID(), false);
+ }
+
+ CJobQueue::OnJobComplete(jobID, success, job);
+}
+
+void CGUIWindowFileManager::ShowShareErrorMessage(CFileItem* pItem)
+{
+ int idMessageText = 0;
+ CURL url(pItem->GetPath());
+
+ if (url.IsProtocol("smb") && url.GetHostName().empty()) // smb workgroup
+ idMessageText = 15303; // Workgroup not found
+ else if (pItem->m_iDriveType == CMediaSource::SOURCE_TYPE_REMOTE || URIUtils::IsRemote(pItem->GetPath()))
+ idMessageText = 15301; // Could not connect to network server
+ else
+ idMessageText = 15300; // Path not found or invalid
+
+ HELPERS::ShowOKDialogText(CVariant{220}, CVariant{idMessageText});
+}
+
+void CGUIWindowFileManager::OnInitWindow()
+{
+ bool bResult0 = Update(0, m_Directory[0]->GetPath());
+ bool bResult1 = Update(1, m_Directory[1]->GetPath());
+
+ CGUIWindow::OnInitWindow();
+
+ if (!bCheckShareConnectivity)
+ {
+ bCheckShareConnectivity = true; //reset
+ CFileItem pItem(strCheckSharePath, true);
+ ShowShareErrorMessage(&pItem); //show the error message after window is loaded!
+ Update(0,""); // reset view to root
+ }
+ else if (!bResult0)
+ {
+ ShowShareErrorMessage(m_Directory[0]); //show the error message after window is loaded!
+ Update(0, ""); // reset view to root
+ }
+
+ if (!bResult1)
+ {
+ ShowShareErrorMessage(m_Directory[1]); //show the error message after window is loaded!
+ Update(1, ""); // reset view to root
+ }
+}
+
+void CGUIWindowFileManager::SetInitialPath(const std::string &path)
+{
+ // check for a passed destination path
+ std::string strDestination = path;
+ m_rootDir.SetSources(*CMediaSourceSettings::GetInstance().GetSources("files"));
+ if (!strDestination.empty())
+ {
+ CLog::Log(LOGINFO, "Attempting to quickpath to: {}", strDestination);
+ }
+ // otherwise, is this the first time accessing this window?
+ else if (m_Directory[0]->GetPath() == "?")
+ {
+ m_Directory[0]->SetPath(strDestination = CMediaSourceSettings::GetInstance().GetDefaultSource("files"));
+ CLog::Log(LOGINFO, "Attempting to default to: {}", strDestination);
+ }
+ // try to open the destination path
+ if (!strDestination.empty())
+ {
+ // open root
+ if (StringUtils::EqualsNoCase(strDestination, "$ROOT"))
+ {
+ m_Directory[0]->SetPath("");
+ CLog::Log(LOGINFO, " Success! Opening root listing.");
+ }
+ else
+ {
+ // default parameters if the jump fails
+ m_Directory[0]->SetPath("");
+
+ bool bIsSourceName = false;
+ VECSOURCES shares;
+ m_rootDir.GetSources(shares);
+ int iIndex = CUtil::GetMatchingSource(strDestination, shares, bIsSourceName);
+ if (iIndex > -1
+#if defined(TARGET_DARWIN_EMBEDDED)
+ || URIUtils::PathHasParent(strDestination, "special://envhome/Documents/Inbox/")
+#endif
+ || URIUtils::PathHasParent(strDestination, "special://profile/"))
+ {
+ // set current directory to matching share
+ std::string path;
+ if (bIsSourceName && iIndex < (int)shares.size())
+ path = shares[iIndex].strPath;
+ else
+ path = strDestination;
+ URIUtils::RemoveSlashAtEnd(path);
+ m_Directory[0]->SetPath(path);
+ CLog::Log(LOGINFO, " Success! Opened destination path: {}", strDestination);
+
+ // outside call: check the share for connectivity
+ bCheckShareConnectivity = Update(0, m_Directory[0]->GetPath());
+ if(!bCheckShareConnectivity)
+ strCheckSharePath = m_Directory[0]->GetPath();
+ }
+ else
+ {
+ CLog::Log(LOGERROR, " Failed! Destination parameter ({}) does not match a valid share!",
+ strDestination);
+ }
+ }
+ }
+
+ if (m_Directory[1]->GetPath() == "?") m_Directory[1]->SetPath("");
+}
+
+const CFileItem& CGUIWindowFileManager::CurrentDirectory(int indx) const
+{
+ return *m_Directory[indx];
+}
diff --git a/xbmc/windows/GUIWindowFileManager.h b/xbmc/windows/GUIWindowFileManager.h
new file mode 100644
index 0000000..425e96e
--- /dev/null
+++ b/xbmc/windows/GUIWindowFileManager.h
@@ -0,0 +1,105 @@
+/*
+ * 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 "filesystem/DirectoryHistory.h"
+#include "filesystem/VirtualDirectory.h"
+#include "guilib/GUIWindow.h"
+#include "utils/JobManager.h"
+
+#include <atomic>
+#include <string>
+#include <vector>
+
+class CFileItem;
+class CFileItemList;
+class CGUIDialogProgress;
+
+class CGUIWindowFileManager :
+ public CGUIWindow,
+ public CJobQueue
+{
+public:
+
+ CGUIWindowFileManager(void);
+ ~CGUIWindowFileManager(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnBack(int actionID) override;
+ const CFileItem &CurrentDirectory(int indx) const;
+
+ static int64_t CalculateFolderSize(const std::string &strDirectory, CGUIDialogProgress *pProgress = NULL);
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+protected:
+ void OnInitWindow() override;
+ void SetInitialPath(const std::string &path);
+ void GoParentFolder(int iList);
+ void UpdateControl(int iList, int item);
+ bool Update(int iList, const std::string &strDirectory); //???
+ void OnStart(CFileItem *pItem, const std::string &player);
+ bool SelectItem(int iList, int &item);
+ void ClearFileItems(int iList);
+ void OnClick(int iList, int iItem);
+ void OnMark(int iList, int iItem);
+ void OnSort(int iList);
+ void UpdateButtons();
+ void OnCopy(int iList);
+ void OnMove(int iList);
+ void OnDelete(int iList);
+ void OnRename(int iList);
+ void OnSelectAll(int iList);
+ void OnNewFolder(int iList);
+ void Refresh();
+ void Refresh(int iList);
+ int GetSelectedItem(int iList);
+ bool HaveDiscOrConnection( std::string& strPath, int iDriveType );
+ void GetDirectoryHistoryString(const CFileItem* pItem, std::string& strHistoryString);
+ bool GetDirectory(int iList, const std::string &strDirectory, CFileItemList &items);
+ int NumSelected(int iList);
+ int GetFocusedList() const;
+ // functions to check for actions that we can perform
+ bool CanRename(int iList);
+ bool CanCopy(int iList);
+ bool CanMove(int iList);
+ bool CanDelete(int iList);
+ bool CanNewFolder(int iList);
+ void OnPopupMenu(int iList, int iItem, bool bContextDriven = true);
+ void ShowShareErrorMessage(CFileItem* pItem);
+ void UpdateItemCounts();
+
+ //
+ bool bCheckShareConnectivity;
+ std::string strCheckSharePath;
+
+ XFILE::CVirtualDirectory m_rootDir;
+ CFileItemList* m_vecItems[2];
+ typedef std::vector <CFileItem*> ::iterator ivecItems;
+ CFileItem* m_Directory[2];
+ std::string m_strParentPath[2];
+ CDirectoryHistory m_history[2];
+
+ int m_errorHeading, m_errorLine;
+private:
+ std::atomic_bool m_updating = {false};
+ class CUpdateGuard
+ {
+ public:
+ CUpdateGuard(std::atomic_bool &update) : m_update(update)
+ {
+ m_update = true;
+ }
+ ~CUpdateGuard()
+ {
+ m_update = false;
+ }
+ private:
+ std::atomic_bool &m_update;
+ };
+};
diff --git a/xbmc/windows/GUIWindowHome.cpp b/xbmc/windows/GUIWindowHome.cpp
new file mode 100644
index 0000000..abc6cfc
--- /dev/null
+++ b/xbmc/windows/GUIWindowHome.cpp
@@ -0,0 +1,183 @@
+/*
+ * 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 "GUIWindowHome.h"
+
+#include "ServiceBroker.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPlayer.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "input/actions/Action.h"
+#include "input/actions/ActionIDs.h"
+#include "interfaces/AnnouncementManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/JobManager.h"
+#include "utils/RecentlyAddedJob.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "utils/log.h"
+
+#include <mutex>
+
+CGUIWindowHome::CGUIWindowHome(void) : CGUIWindow(WINDOW_HOME, "Home.xml")
+{
+ m_updateRA = (Audio | Video | Totals);
+ m_loadType = KEEP_IN_MEMORY;
+
+ CServiceBroker::GetAnnouncementManager()->AddAnnouncer(this);
+}
+
+CGUIWindowHome::~CGUIWindowHome(void)
+{
+ CServiceBroker::GetAnnouncementManager()->RemoveAnnouncer(this);
+}
+
+bool CGUIWindowHome::OnAction(const CAction &action)
+{
+ static unsigned int min_hold_time = 1000;
+ if (action.GetID() == ACTION_NAV_BACK && action.GetHoldTime() < min_hold_time)
+ {
+ const auto& components = CServiceBroker::GetAppComponents();
+ const auto appPlayer = components.GetComponent<CApplicationPlayer>();
+ if (appPlayer->IsPlaying())
+ {
+ CGUIComponent* gui = CServiceBroker::GetGUI();
+ if (gui)
+ gui->GetWindowManager().SwitchToFullScreen();
+
+ return true;
+ }
+ }
+ return CGUIWindow::OnAction(action);
+}
+
+void CGUIWindowHome::OnInitWindow()
+{
+ // for shared databases (ie mysql) always force an update on return to home
+ // this is a temporary solution until remote announcements can be delivered
+ if (StringUtils::EqualsNoCase(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseVideo.type, "mysql") ||
+ StringUtils::EqualsNoCase(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_databaseMusic.type, "mysql") )
+ m_updateRA = (Audio | Video | Totals);
+ AddRecentlyAddedJobs( m_updateRA );
+
+ CGUIWindow::OnInitWindow();
+}
+
+void CGUIWindowHome::Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data)
+{
+ int ra_flag = 0;
+
+ CLog::Log(LOGDEBUG, LOGANNOUNCE, "GOT ANNOUNCEMENT, type: {}, from {}, message {}",
+ AnnouncementFlagToString(flag), sender, message);
+
+ // we are only interested in library changes
+ if ((flag & (ANNOUNCEMENT::VideoLibrary | ANNOUNCEMENT::AudioLibrary)) == 0)
+ return;
+
+ if (data.isMember("transaction") && data["transaction"].asBoolean())
+ return;
+
+ if (message == "OnScanStarted" || message == "OnCleanStarted")
+ return;
+
+ bool onUpdate = message == "OnUpdate";
+ // always update Totals except on an OnUpdate with no playcount update
+ if (!onUpdate || data.isMember("playcount"))
+ ra_flag |= Totals;
+
+ // always update the full list except on an OnUpdate
+ if (!onUpdate)
+ {
+ if (flag & ANNOUNCEMENT::VideoLibrary)
+ ra_flag |= Video;
+ else if (flag & ANNOUNCEMENT::AudioLibrary)
+ ra_flag |= Audio;
+ }
+
+ CGUIMessage reload(GUI_MSG_NOTIFY_ALL, GetID(), 0, GUI_MSG_REFRESH_THUMBS, ra_flag);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(reload, GetID());
+}
+
+void CGUIWindowHome::AddRecentlyAddedJobs(int flag)
+{
+ bool getAJob = false;
+
+ // this block checks to see if another one is running
+ // and keeps track of the flag
+ {
+ std::unique_lock<CCriticalSection> lockMe(*this);
+ if (!m_recentlyAddedRunning)
+ {
+ getAJob = true;
+
+ flag |= m_cumulativeUpdateFlag; // add the flags from previous calls to AddRecentlyAddedJobs
+
+ m_cumulativeUpdateFlag = 0; // now taken care of in flag.
+ // reset this since we're going to execute a job
+
+ // we're about to add one so set the indicator
+ if (flag)
+ m_recentlyAddedRunning = true; // this will happen in the if clause below
+ }
+ else
+ // since we're going to skip a job, mark that one came in and ...
+ m_cumulativeUpdateFlag |= flag; // this will be used later
+ }
+
+ if (flag && getAJob)
+ CServiceBroker::GetJobManager()->AddJob(new CRecentlyAddedJob(flag), this);
+
+ m_updateRA = 0;
+}
+
+void CGUIWindowHome::OnJobComplete(unsigned int jobID, bool success, CJob *job)
+{
+ int flag = 0;
+
+ {
+ std::unique_lock<CCriticalSection> lockMe(*this);
+
+ // the job is finished.
+ // did one come in the meantime?
+ flag = m_cumulativeUpdateFlag;
+ m_recentlyAddedRunning = false; /// we're done.
+ }
+
+ if (flag)
+ AddRecentlyAddedJobs(0 /* the flag will be set inside AddRecentlyAddedJobs via m_cumulativeUpdateFlag */ );
+}
+
+
+bool CGUIWindowHome::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_NOTIFY_ALL:
+ if (message.GetParam1() == GUI_MSG_WINDOW_RESET || message.GetParam1() == GUI_MSG_REFRESH_THUMBS)
+ {
+ int updateRA = (message.GetSenderId() == GetID()) ? message.GetParam2() : (Video | Audio | Totals);
+
+ if (IsActive())
+ AddRecentlyAddedJobs(updateRA);
+ else
+ m_updateRA |= updateRA;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return CGUIWindow::OnMessage(message);
+}
diff --git a/xbmc/windows/GUIWindowHome.h b/xbmc/windows/GUIWindowHome.h
new file mode 100644
index 0000000..08e0066
--- /dev/null
+++ b/xbmc/windows/GUIWindowHome.h
@@ -0,0 +1,41 @@
+/*
+ * 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 "guilib/GUIWindow.h"
+#include "interfaces/IAnnouncer.h"
+#include "utils/Job.h"
+
+class CVariant;
+
+class CGUIWindowHome :
+ public CGUIWindow,
+ public ANNOUNCEMENT::IAnnouncer,
+ public IJobCallback
+{
+public:
+ CGUIWindowHome(void);
+ ~CGUIWindowHome(void) override;
+ void OnInitWindow() override;
+ void Announce(ANNOUNCEMENT::AnnouncementFlag flag,
+ const std::string& sender,
+ const std::string& message,
+ const CVariant& data) override;
+
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+
+ void OnJobComplete(unsigned int jobID, bool success, CJob *job) override;
+private:
+ int m_updateRA; // flag for which recently added items needs to be queried
+ void AddRecentlyAddedJobs(int flag);
+
+ bool m_recentlyAddedRunning = false;
+ int m_cumulativeUpdateFlag = 0;
+};
diff --git a/xbmc/windows/GUIWindowLoginScreen.cpp b/xbmc/windows/GUIWindowLoginScreen.cpp
new file mode 100644
index 0000000..b00c2ae
--- /dev/null
+++ b/xbmc/windows/GUIWindowLoginScreen.cpp
@@ -0,0 +1,269 @@
+/*
+ * 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 "GUIWindowLoginScreen.h"
+
+#include "FileItem.h"
+#include "GUIPassword.h"
+#include "ServiceBroker.h"
+#include "addons/Skin.h"
+#include "dialogs/GUIDialogContextMenu.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "input/Key.h"
+#include "interfaces/builtins/Builtins.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogOKHelper.h"
+#include "profiles/Profile.h"
+#include "profiles/ProfileManager.h"
+#include "profiles/dialogs/GUIDialogProfileSettings.h"
+#include "pvr/PVRManager.h"
+#include "pvr/guilib/PVRGUIActionsPowerManagement.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/Variant.h"
+#include "view/ViewState.h"
+
+using namespace KODI::MESSAGING;
+
+#define CONTROL_BIG_LIST 52
+#define CONTROL_LABEL_HEADER 2
+#define CONTROL_LABEL_SELECTED_PROFILE 3
+
+CGUIWindowLoginScreen::CGUIWindowLoginScreen(void)
+ : CGUIWindow(WINDOW_LOGIN_SCREEN, "LoginScreen.xml")
+{
+ watch.StartZero();
+ m_vecItems = new CFileItemList;
+ m_iSelectedItem = -1;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIWindowLoginScreen::~CGUIWindowLoginScreen(void)
+{
+ delete m_vecItems;
+}
+
+bool CGUIWindowLoginScreen::OnMessage(CGUIMessage& message)
+{
+ switch ( message.GetMessage() )
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ m_vecItems->Clear();
+ }
+ break;
+
+ case GUI_MSG_CLICKED:
+ {
+ int iControl = message.GetSenderId();
+ if (iControl == CONTROL_BIG_LIST)
+ {
+ int iAction = message.GetParam1();
+
+ // iItem is checked for validity inside these routines
+ if (iAction == ACTION_CONTEXT_MENU || iAction == ACTION_MOUSE_RIGHT_CLICK)
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ bool bResult = OnPopupMenu(m_viewControl.GetSelectedItem());
+ if (bResult)
+ {
+ Update();
+ CGUIMessage msg(GUI_MSG_ITEM_SELECT,GetID(),CONTROL_BIG_LIST,iItem);
+ OnMessage(msg);
+ }
+
+ return bResult;
+ }
+ else if (iAction == ACTION_SELECT_ITEM || iAction == ACTION_MOUSE_LEFT_CLICK)
+ {
+ int iItem = m_viewControl.GetSelectedItem();
+ bool bCanceled;
+ bool bOkay = g_passwordManager.IsProfileLockUnlocked(iItem, bCanceled);
+
+ if (bOkay)
+ {
+ if (iItem >= 0)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_LOADPROFILE, iItem);
+ }
+ else
+ {
+ if (!bCanceled && iItem != 0)
+ HELPERS::ShowOKDialogText(CVariant{20068}, CVariant{20117});
+ }
+ }
+ }
+ }
+ break;
+ case GUI_MSG_SETFOCUS:
+ {
+ if (m_viewControl.HasControl(message.GetControlId()) && m_viewControl.GetCurrentControl() != message.GetControlId())
+ {
+ m_viewControl.SetFocused();
+ return true;
+ }
+ }
+ default:
+ break;
+
+ }
+
+ return CGUIWindow::OnMessage(message);
+}
+
+bool CGUIWindowLoginScreen::OnAction(const CAction &action)
+{
+ // don't allow built in actions to act here except shutdown related ones.
+ // this forces only navigation type actions to be performed.
+ if (action.GetID() == ACTION_BUILT_IN_FUNCTION)
+ {
+ std::string actionName = action.GetName();
+ StringUtils::ToLower(actionName);
+ if ((actionName.find("shutdown") != std::string::npos) &&
+ CServiceBroker::GetPVRManager().Get<PVR::GUI::PowerManagement>().CanSystemPowerdown())
+ CBuiltins::GetInstance().Execute(action.GetName());
+ return true;
+ }
+ return CGUIWindow::OnAction(action);
+}
+
+bool CGUIWindowLoginScreen::OnBack(int actionID)
+{
+ // no escape from the login window
+ return false;
+}
+
+void CGUIWindowLoginScreen::FrameMove()
+{
+ if (GetFocusedControlID() == CONTROL_BIG_LIST && !CServiceBroker::GetGUI()->GetWindowManager().HasModalDialog(true))
+ {
+ if (m_viewControl.HasControl(CONTROL_BIG_LIST))
+ m_iSelectedItem = m_viewControl.GetSelectedItem();
+ }
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ std::string strLabel = StringUtils::Format(g_localizeStrings.Get(20114), m_iSelectedItem + 1,
+ profileManager->GetNumberOfProfiles());
+ SET_CONTROL_LABEL(CONTROL_LABEL_SELECTED_PROFILE,strLabel);
+ CGUIWindow::FrameMove();
+}
+
+void CGUIWindowLoginScreen::OnInitWindow()
+{
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ m_iSelectedItem = static_cast<int>(profileManager->GetLastUsedProfileIndex());
+
+ // Update list/thumb control
+ m_viewControl.SetCurrentView(DEFAULT_VIEW_LIST);
+ Update();
+ m_viewControl.SetFocused();
+ SET_CONTROL_LABEL(CONTROL_LABEL_HEADER,g_localizeStrings.Get(20115));
+ SET_CONTROL_VISIBLE(CONTROL_BIG_LIST);
+
+ CGUIWindow::OnInitWindow();
+}
+
+void CGUIWindowLoginScreen::OnWindowLoaded()
+{
+ CGUIWindow::OnWindowLoaded();
+ m_viewControl.Reset();
+ m_viewControl.SetParentWindow(GetID());
+ m_viewControl.AddView(GetControl(CONTROL_BIG_LIST));
+}
+
+void CGUIWindowLoginScreen::OnWindowUnload()
+{
+ CGUIWindow::OnWindowUnload();
+ m_viewControl.Reset();
+}
+
+void CGUIWindowLoginScreen::Update()
+{
+ m_vecItems->Clear();
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ for (unsigned int i = 0; i < profileManager->GetNumberOfProfiles(); ++i)
+ {
+ const CProfile *profile = profileManager->GetProfile(i);
+
+ CFileItemPtr item(new CFileItem(profile->getName()));
+
+ std::string strLabel;
+ if (profile->getDate().empty())
+ strLabel = g_localizeStrings.Get(20113);
+ else
+ strLabel = StringUtils::Format(g_localizeStrings.Get(20112), profile->getDate());
+
+ item->SetLabel2(strLabel);
+ item->SetArt("thumb", profile->getThumb());
+ if (profile->getThumb().empty())
+ item->SetArt("thumb", "DefaultUser.png");
+ item->SetLabelPreformatted(true);
+
+ m_vecItems->Add(item);
+ }
+ m_viewControl.SetItems(*m_vecItems);
+ m_viewControl.SetSelectedItem(m_iSelectedItem);
+}
+
+bool CGUIWindowLoginScreen::OnPopupMenu(int iItem)
+{
+ if (iItem < 0 || iItem >= m_vecItems->Size())
+ return false;
+
+ const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();
+
+ CFileItemPtr pItem = m_vecItems->Get(iItem);
+ bool bSelect = pItem->IsSelected();
+
+ // mark the item
+ pItem->Select(true);
+
+ CContextButtons choices;
+ choices.Add(1, 20067);
+
+ if (iItem == 0 && g_passwordManager.iMasterLockRetriesLeft == 0)
+ choices.Add(2, 12334);
+
+ int choice = CGUIDialogContextMenu::ShowAndGetChoice(choices);
+ if (choice == 2)
+ {
+ if (g_passwordManager.CheckLock(profileManager->GetMasterProfile().getLockMode(), profileManager->GetMasterProfile().getLockCode(), 20075))
+ g_passwordManager.iMasterLockRetriesLeft = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_MASTERLOCK_MAXRETRIES);
+ else // be inconvenient
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_SHUTDOWN);
+
+ return true;
+ }
+
+ // Edit the profile after checking if the correct master lock password was given.
+ if (choice == 1 && g_passwordManager.IsMasterLockUnlocked(true))
+ CGUIDialogProfileSettings::ShowForProfile(m_viewControl.GetSelectedItem());
+
+ //NOTE: this can potentially (de)select the wrong item if the filelisting has changed because of an action above.
+ if (iItem < static_cast<int>(profileManager->GetNumberOfProfiles()))
+ m_vecItems->Get(iItem)->Select(bSelect);
+
+ return false;
+}
+
+CFileItemPtr CGUIWindowLoginScreen::GetCurrentListItem(int offset)
+{
+ int item = m_viewControl.GetSelectedItem();
+ if (item < 0 || !m_vecItems->Size()) return CFileItemPtr();
+
+ item = (item + offset) % m_vecItems->Size();
+ if (item < 0) item += m_vecItems->Size();
+ return m_vecItems->Get(item);
+}
diff --git a/xbmc/windows/GUIWindowLoginScreen.h b/xbmc/windows/GUIWindowLoginScreen.h
new file mode 100644
index 0000000..500bee7
--- /dev/null
+++ b/xbmc/windows/GUIWindowLoginScreen.h
@@ -0,0 +1,43 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+#include "utils/Stopwatch.h"
+#include "view/GUIViewControl.h"
+
+class CFileItemList;
+
+class CGUIWindowLoginScreen : public CGUIWindow
+{
+public:
+ CGUIWindowLoginScreen(void);
+ ~CGUIWindowLoginScreen(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction &action) override;
+ bool OnBack(int actionID) override;
+ void FrameMove() override;
+ bool HasListItems() const override { return true; }
+ CFileItemPtr GetCurrentListItem(int offset = 0) override;
+ int GetViewContainerID() const override { return m_viewControl.GetCurrentControl(); }
+
+protected:
+ void OnInitWindow() override;
+ void OnWindowLoaded() override;
+ void OnWindowUnload() override;
+ void Update();
+ void SetLabel(int iControl, const std::string& strLabel);
+
+ bool OnPopupMenu(int iItem);
+ CGUIViewControl m_viewControl;
+ CFileItemList* m_vecItems;
+
+ int m_iSelectedItem;
+ CStopWatch watch;
+};
diff --git a/xbmc/windows/GUIWindowPointer.cpp b/xbmc/windows/GUIWindowPointer.cpp
new file mode 100644
index 0000000..d9e1bda
--- /dev/null
+++ b/xbmc/windows/GUIWindowPointer.cpp
@@ -0,0 +1,82 @@
+/*
+ * 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 "GUIWindowPointer.h"
+
+#include "ServiceBroker.h"
+#include "input/InputManager.h"
+#include "input/mouse/MouseStat.h"
+#include "windowing/WinSystem.h"
+
+#define ID_POINTER 10
+
+CGUIWindowPointer::CGUIWindowPointer(void)
+ : CGUIDialog(WINDOW_DIALOG_POINTER, "Pointer.xml", DialogModalityType::MODELESS)
+{
+ m_pointer = 0;
+ m_loadType = LOAD_ON_GUI_INIT;
+ m_needsScaling = false;
+ m_active = false;
+ m_renderOrder = RENDER_ORDER_WINDOW_POINTER;
+}
+
+CGUIWindowPointer::~CGUIWindowPointer(void) = default;
+
+void CGUIWindowPointer::SetPointer(int pointer)
+{
+ if (m_pointer == pointer) return;
+ // set the new pointer visible
+ CGUIControl *pControl = GetControl(pointer);
+ if (pControl)
+ {
+ pControl->SetVisible(true);
+ // disable the old pointer
+ pControl = GetControl(m_pointer);
+ if (pControl) pControl->SetVisible(false);
+ // set pointer to the new one
+ m_pointer = pointer;
+ }
+}
+
+void CGUIWindowPointer::UpdateVisibility()
+{
+ if(CServiceBroker::GetWinSystem()->HasCursor())
+ {
+ if (CServiceBroker::GetInputManager().IsMouseActive())
+ Open();
+ else
+ Close();
+ }
+}
+
+void CGUIWindowPointer::OnWindowLoaded()
+{ // set all our pointer images invisible
+ for (iControls i = m_children.begin();i != m_children.end(); ++i)
+ {
+ CGUIControl* pControl = *i;
+ pControl->SetVisible(false);
+ }
+ CGUIWindow::OnWindowLoaded();
+ DynamicResourceAlloc(false);
+ m_pointer = 0;
+ m_renderOrder = RENDER_ORDER_WINDOW_POINTER;
+}
+
+void CGUIWindowPointer::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ bool active = CServiceBroker::GetInputManager().IsMouseActive();
+ if (active != m_active)
+ {
+ MarkDirtyRegion();
+ m_active = active;
+ }
+ MousePosition pos = CServiceBroker::GetInputManager().GetMousePosition();
+ SetPosition((float)pos.x, (float)pos.y);
+ SetPointer(CServiceBroker::GetInputManager().GetMouseState());
+ return CGUIWindow::Process(currentTime, dirtyregions);
+}
diff --git a/xbmc/windows/GUIWindowPointer.h b/xbmc/windows/GUIWindowPointer.h
new file mode 100644
index 0000000..476f0a2
--- /dev/null
+++ b/xbmc/windows/GUIWindowPointer.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIWindowPointer :
+ public CGUIDialog
+{
+public:
+ CGUIWindowPointer(void);
+ ~CGUIWindowPointer(void) override;
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+protected:
+ void SetPointer(int pointer);
+ void OnWindowLoaded() override;
+ void UpdateVisibility() override;
+private:
+ int m_pointer;
+ bool m_active;
+};
diff --git a/xbmc/windows/GUIWindowScreensaver.cpp b/xbmc/windows/GUIWindowScreensaver.cpp
new file mode 100644
index 0000000..76eaa96
--- /dev/null
+++ b/xbmc/windows/GUIWindowScreensaver.cpp
@@ -0,0 +1,106 @@
+/*
+ * 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 "GUIWindowScreensaver.h"
+
+#include "GUIPassword.h"
+#include "GUIUserMessages.h"
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/ScreenSaver.h"
+#include "addons/addoninfo/AddonType.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+
+CGUIWindowScreensaver::CGUIWindowScreensaver() : CGUIWindow(WINDOW_SCREENSAVER, "")
+{
+}
+
+void CGUIWindowScreensaver::Process(unsigned int currentTime, CDirtyRegionList& regions)
+{
+ MarkDirtyRegion();
+ CGUIWindow::Process(currentTime, regions);
+ const auto& context = CServiceBroker::GetWinSystem()->GetGfxContext();
+ m_renderRegion.SetRect(0, 0, static_cast<float>(context.GetWidth()),
+ static_cast<float>(context.GetHeight()));
+}
+
+void CGUIWindowScreensaver::Render()
+{
+ if (m_addon)
+ {
+ auto& context = CServiceBroker::GetWinSystem()->GetGfxContext();
+
+ context.CaptureStateBlock();
+ m_addon->Render();
+ context.ApplyStateBlock();
+ return;
+ }
+
+ CGUIWindow::Render();
+}
+
+// called when the mouse is moved/clicked etc. etc.
+EVENT_RESULT CGUIWindowScreensaver::OnMouseEvent(const CPoint& point, const CMouseEvent& event)
+{
+ CServiceBroker::GetGUI()->GetWindowManager().PreviousWindow();
+ return EVENT_RESULT_HANDLED;
+}
+
+bool CGUIWindowScreensaver::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ if (m_addon)
+ {
+ m_addon->Stop();
+ m_addon.reset();
+ }
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().ApplyStateBlock();
+ }
+ break;
+
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CGUIWindow::OnMessage(message);
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().CaptureStateBlock();
+
+ const std::string addon = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(
+ CSettings::SETTING_SCREENSAVER_MODE);
+ const ADDON::AddonInfoPtr addonBase =
+ CServiceBroker::GetAddonMgr().GetAddonInfo(addon, ADDON::AddonType::SCREENSAVER);
+ if (!addonBase)
+ return false;
+ m_addon = std::make_unique<KODI::ADDONS::CScreenSaver>(addonBase);
+ return m_addon->Start();
+ }
+
+ case GUI_MSG_CHECK_LOCK:
+ {
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (!g_passwordManager.IsProfileLockUnlocked())
+ {
+ appPower->SetScreenSaverLockFailed();
+ return false;
+ }
+ appPower->SetScreenSaverUnlocked();
+ return true;
+ }
+ }
+
+ return CGUIWindow::OnMessage(message);
+}
diff --git a/xbmc/windows/GUIWindowScreensaver.h b/xbmc/windows/GUIWindowScreensaver.h
new file mode 100644
index 0000000..903efde
--- /dev/null
+++ b/xbmc/windows/GUIWindowScreensaver.h
@@ -0,0 +1,42 @@
+/*
+ * 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 "guilib/GUIWindow.h"
+
+#include <memory>
+
+namespace KODI
+{
+namespace ADDONS
+{
+class CScreenSaver;
+} // namespace ADDONS
+} // namespace KODI
+
+class CGUIWindowScreensaver : public CGUIWindow
+{
+public:
+ CGUIWindowScreensaver();
+
+ bool OnMessage(CGUIMessage& message) override;
+ bool OnAction(const CAction& action) override
+ {
+ // We're just a screen saver, nothing to do here
+ return false;
+ }
+ void Render() override;
+ void Process(unsigned int currentTime, CDirtyRegionList& regions) override;
+
+protected:
+ EVENT_RESULT OnMouseEvent(const CPoint& point, const CMouseEvent& event) override;
+
+private:
+ std::unique_ptr<KODI::ADDONS::CScreenSaver> m_addon;
+};
diff --git a/xbmc/windows/GUIWindowScreensaverDim.cpp b/xbmc/windows/GUIWindowScreensaverDim.cpp
new file mode 100644
index 0000000..f617fba
--- /dev/null
+++ b/xbmc/windows/GUIWindowScreensaverDim.cpp
@@ -0,0 +1,77 @@
+/*
+ * 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 "GUIWindowScreensaverDim.h"
+
+#include "ServiceBroker.h"
+#include "addons/AddonManager.h"
+#include "addons/IAddon.h"
+#include "addons/addoninfo/AddonType.h"
+#include "application/ApplicationComponents.h"
+#include "application/ApplicationPowerHandling.h"
+#include "guilib/GUITexture.h"
+#include "utils/ColorUtils.h"
+#include "windowing/GraphicContext.h"
+
+CGUIWindowScreensaverDim::CGUIWindowScreensaverDim(void)
+ : CGUIDialog(WINDOW_SCREENSAVER_DIM, "", DialogModalityType::MODELESS)
+{
+ m_needsScaling = false;
+ m_animations.push_back(CAnimation::CreateFader(0, 100, 0, 1000, ANIM_TYPE_WINDOW_OPEN));
+ m_animations.push_back(CAnimation::CreateFader(100, 0, 0, 1000, ANIM_TYPE_WINDOW_CLOSE));
+ m_renderOrder = RENDER_ORDER_WINDOW_SCREENSAVER;
+}
+
+void CGUIWindowScreensaverDim::UpdateVisibility()
+{
+ auto& components = CServiceBroker::GetAppComponents();
+ const auto appPower = components.GetComponent<CApplicationPowerHandling>();
+ if (appPower->IsInScreenSaver())
+ {
+ if (m_visible)
+ return;
+
+ std::string usedId = appPower->ScreensaverIdInUse();
+ if (usedId == "screensaver.xbmc.builtin.dim" ||
+ usedId == "screensaver.xbmc.builtin.black")
+ {
+ m_visible = true;
+ ADDON::AddonPtr info;
+ bool success = CServiceBroker::GetAddonMgr().GetAddon(
+ usedId, info, ADDON::AddonType::SCREENSAVER, ADDON::OnlyEnabled::CHOICE_YES);
+ if (success && info && !info->GetSetting("level").empty())
+ m_newDimLevel = 100.0f - (float)atof(info->GetSetting("level").c_str());
+ else
+ m_newDimLevel = 100.0f;
+ Open();
+ }
+ }
+ else if (m_visible)
+ {
+ m_visible = false;
+ Close();
+ }
+}
+
+void CGUIWindowScreensaverDim::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
+{
+ if (m_newDimLevel != m_dimLevel && !IsAnimating(ANIM_TYPE_WINDOW_CLOSE))
+ m_dimLevel = m_newDimLevel;
+ CGUIDialog::Process(currentTime, dirtyregions);
+ m_renderRegion.SetRect(0, 0, (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight());
+}
+
+void CGUIWindowScreensaverDim::Render()
+{
+ // draw a translucent black quad - fading is handled by the window animation
+ UTILS::COLOR::Color color = (static_cast<UTILS::COLOR::Color>(m_dimLevel * 2.55f) & 0xff) << 24;
+ color = CServiceBroker::GetWinSystem()->GetGfxContext().MergeAlpha(color);
+ CRect rect(0, 0, (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight());
+ CGUITexture::DrawQuad(rect, color);
+ CGUIDialog::Render();
+}
diff --git a/xbmc/windows/GUIWindowScreensaverDim.h b/xbmc/windows/GUIWindowScreensaverDim.h
new file mode 100644
index 0000000..c4d5a78
--- /dev/null
+++ b/xbmc/windows/GUIWindowScreensaverDim.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "guilib/GUIDialog.h"
+
+class CGUIWindowScreensaverDim : public CGUIDialog
+{
+public:
+ CGUIWindowScreensaverDim();
+
+ void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) override;
+ void Render() override;
+
+protected:
+ void UpdateVisibility() override;
+
+private:
+ float m_dimLevel = 100.0f;
+ float m_newDimLevel = 100.0f;
+ bool m_visible = false;
+};
diff --git a/xbmc/windows/GUIWindowSplash.cpp b/xbmc/windows/GUIWindowSplash.cpp
new file mode 100644
index 0000000..dadd61e
--- /dev/null
+++ b/xbmc/windows/GUIWindowSplash.cpp
@@ -0,0 +1,45 @@
+/*
+ * 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 "GUIWindowSplash.h"
+
+#include "Util.h"
+#include "guilib/GUIImage.h"
+#include "guilib/GUIWindowManager.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+
+CGUIWindowSplash::CGUIWindowSplash(void) : CGUIWindow(WINDOW_SPLASH, ""), m_image(nullptr)
+{
+ m_loadType = LOAD_ON_GUI_INIT;
+}
+
+CGUIWindowSplash::~CGUIWindowSplash(void) = default;
+
+void CGUIWindowSplash::OnInitWindow()
+{
+ if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_splashImage)
+ return;
+
+ m_image = std::unique_ptr<CGUIImage>(new CGUIImage(0, 0, 0, 0, CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight(), CTextureInfo(CUtil::GetSplashPath())));
+ m_image->SetAspectRatio(CAspectRatio::AR_SCALE);
+}
+
+void CGUIWindowSplash::Render()
+{
+ CServiceBroker::GetWinSystem()->GetGfxContext().SetRenderingResolution(CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(), true);
+
+ if (!m_image)
+ return;
+
+ m_image->SetWidth(CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth());
+ m_image->SetHeight(CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight());
+ m_image->AllocResources();
+ m_image->Render();
+ m_image->FreeResources();
+}
diff --git a/xbmc/windows/GUIWindowSplash.h b/xbmc/windows/GUIWindowSplash.h
new file mode 100644
index 0000000..f5f54ee
--- /dev/null
+++ b/xbmc/windows/GUIWindowSplash.h
@@ -0,0 +1,29 @@
+/*
+ * 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/GUIWindow.h"
+
+#include <memory>
+
+class CGUITextLayout;
+class CGUIImage;
+
+class CGUIWindowSplash : public CGUIWindow
+{
+public:
+ CGUIWindowSplash(void);
+ ~CGUIWindowSplash(void) override;
+ bool OnAction(const CAction& action) override { return false; }
+ void Render() override;
+protected:
+ void OnInitWindow() override;
+private:
+ std::unique_ptr<CGUIImage> m_image;
+};
diff --git a/xbmc/windows/GUIWindowStartup.cpp b/xbmc/windows/GUIWindowStartup.cpp
new file mode 100644
index 0000000..ffa3d79
--- /dev/null
+++ b/xbmc/windows/GUIWindowStartup.cpp
@@ -0,0 +1,38 @@
+/*
+ * 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 "GUIWindowStartup.h"
+
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/WindowIDs.h"
+#include "input/Key.h"
+
+CGUIWindowStartup::CGUIWindowStartup(void)
+ : CGUIWindow(WINDOW_STARTUP_ANIM, "Startup.xml")
+{
+}
+
+CGUIWindowStartup::~CGUIWindowStartup(void) = default;
+
+bool CGUIWindowStartup::OnAction(const CAction &action)
+{
+ if (action.IsMouse())
+ return true;
+ return CGUIWindow::OnAction(action);
+}
+
+void CGUIWindowStartup::OnDeinitWindow(int nextWindowID)
+{
+ CGUIWindow::OnDeinitWindow(nextWindowID);
+
+ // let everyone know that the user interface is now ready for usage
+ CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UI_READY);
+ CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
+}
diff --git a/xbmc/windows/GUIWindowStartup.h b/xbmc/windows/GUIWindowStartup.h
new file mode 100644
index 0000000..c2e35ad
--- /dev/null
+++ b/xbmc/windows/GUIWindowStartup.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "guilib/GUIWindow.h"
+
+class CGUIWindowStartup :
+ public CGUIWindow
+{
+public:
+ CGUIWindowStartup(void);
+ ~CGUIWindowStartup(void) override;
+ bool OnAction(const CAction &action) override;
+
+ // specialization of CGUIWindow
+ void OnDeinitWindow(int nextWindowID) override;
+};
diff --git a/xbmc/windows/GUIWindowSystemInfo.cpp b/xbmc/windows/GUIWindowSystemInfo.cpp
new file mode 100644
index 0000000..157b7b8
--- /dev/null
+++ b/xbmc/windows/GUIWindowSystemInfo.cpp
@@ -0,0 +1,260 @@
+/*
+ * 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 "GUIWindowSystemInfo.h"
+
+#include "GUIInfoManager.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIMessage.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "guilib/guiinfo/GUIInfoLabels.h"
+#include "pvr/PVRManager.h"
+#include "storage/MediaManager.h"
+#include "utils/CPUInfo.h"
+#include "utils/StringUtils.h"
+#include "utils/SystemInfo.h"
+
+#define CONTROL_TB_POLICY 30
+#define CONTROL_BT_STORAGE 94
+#define CONTROL_BT_DEFAULT 95
+#define CONTROL_BT_NETWORK 96
+#define CONTROL_BT_VIDEO 97
+#define CONTROL_BT_HARDWARE 98
+#define CONTROL_BT_PVR 99
+#define CONTROL_BT_POLICY 100
+
+#define CONTROL_START CONTROL_BT_STORAGE
+#define CONTROL_END CONTROL_BT_POLICY
+
+CGUIWindowSystemInfo::CGUIWindowSystemInfo(void) :
+ CGUIWindow(WINDOW_SYSTEM_INFORMATION, "SettingsSystemInfo.xml")
+{
+ m_section = CONTROL_BT_DEFAULT;
+ m_loadType = KEEP_IN_MEMORY;
+}
+
+CGUIWindowSystemInfo::~CGUIWindowSystemInfo(void) = default;
+
+bool CGUIWindowSystemInfo::OnMessage(CGUIMessage& message)
+{
+ switch (message.GetMessage())
+ {
+ case GUI_MSG_WINDOW_INIT:
+ {
+ CGUIWindow::OnMessage(message);
+ SET_CONTROL_LABEL(52, CSysInfo::GetAppName() + " " + CSysInfo::GetVersion());
+ SET_CONTROL_LABEL(53, CSysInfo::GetBuildDate());
+ CONTROL_ENABLE_ON_CONDITION(CONTROL_BT_PVR, CServiceBroker::GetPVRManager().IsStarted());
+ return true;
+ }
+ break;
+
+ case GUI_MSG_WINDOW_DEINIT:
+ {
+ CGUIWindow::OnMessage(message);
+ m_diskUsage.clear();
+ return true;
+ }
+ break;
+
+ case GUI_MSG_FOCUSED:
+ {
+ CGUIWindow::OnMessage(message);
+ int focusedControl = GetFocusedControlID();
+ if (m_section != focusedControl && focusedControl >= CONTROL_START && focusedControl <= CONTROL_END)
+ {
+ ResetLabels();
+ m_section = focusedControl;
+ }
+ if (m_section >= CONTROL_BT_STORAGE && m_section <= CONTROL_BT_PVR)
+ SET_CONTROL_HIDDEN(CONTROL_TB_POLICY);
+ else if (m_section == CONTROL_BT_POLICY)
+ {
+ SET_CONTROL_LABEL(CONTROL_TB_POLICY, CServiceBroker::GetGUI()->GetInfoManager().GetLabel(
+ SYSTEM_PRIVACY_POLICY, INFO::DEFAULT_CONTEXT));
+ SET_CONTROL_VISIBLE(CONTROL_TB_POLICY);
+ }
+ return true;
+ }
+ break;
+ }
+ return CGUIWindow::OnMessage(message);
+}
+
+void CGUIWindowSystemInfo::FrameMove()
+{
+ int i = 2;
+ if (m_section == CONTROL_BT_DEFAULT)
+ {
+ SET_CONTROL_LABEL(40, g_localizeStrings.Get(20154));
+ SetControlLabel(i++, "{}: {}", 158, SYSTEM_FREE_MEMORY);
+ SetControlLabel(i++, "{}: {}", 150, NETWORK_IP_ADDRESS);
+ SetControlLabel(i++, "{} {}", 13287, SYSTEM_SCREEN_RESOLUTION);
+ SetControlLabel(i++, "{} {}", 13283, SYSTEM_OS_VERSION_INFO);
+ SetControlLabel(i++, "{}: {}", 12390, SYSTEM_UPTIME);
+ SetControlLabel(i++, "{}: {}", 12394, SYSTEM_TOTALUPTIME);
+ SetControlLabel(i++, "{}: {}", 12395, SYSTEM_BATTERY_LEVEL);
+ }
+
+ else if (m_section == CONTROL_BT_STORAGE)
+ {
+ SET_CONTROL_LABEL(40, g_localizeStrings.Get(20155));
+ if (m_diskUsage.empty())
+ m_diskUsage = CServiceBroker::GetMediaManager().GetDiskUsage();
+
+ for (size_t d = 0; d < m_diskUsage.size(); d++)
+ {
+ SET_CONTROL_LABEL(i++, m_diskUsage[d]);
+ }
+ }
+
+ else if (m_section == CONTROL_BT_NETWORK)
+ {
+ SET_CONTROL_LABEL(40,g_localizeStrings.Get(20158));
+ SET_CONTROL_LABEL(i++, CServiceBroker::GetGUI()->GetInfoManager().GetLabel(
+ NETWORK_LINK_STATE, INFO::DEFAULT_CONTEXT));
+ SetControlLabel(i++, "{}: {}", 149, NETWORK_MAC_ADDRESS);
+ SetControlLabel(i++, "{}: {}", 150, NETWORK_IP_ADDRESS);
+ SetControlLabel(i++, "{}: {}", 13159, NETWORK_SUBNET_MASK);
+ SetControlLabel(i++, "{}: {}", 13160, NETWORK_GATEWAY_ADDRESS);
+ SetControlLabel(i++, "{}: {}", 13161, NETWORK_DNS1_ADDRESS);
+ SetControlLabel(i++, "{}: {}", 20307, NETWORK_DNS2_ADDRESS);
+ SetControlLabel(i++, "{} {}", 13295, SYSTEM_INTERNET_STATE);
+ }
+
+ else if (m_section == CONTROL_BT_VIDEO)
+ {
+ SET_CONTROL_LABEL(40,g_localizeStrings.Get(20159));
+ SET_CONTROL_LABEL(i++, CServiceBroker::GetGUI()->GetInfoManager().GetLabel(
+ SYSTEM_VIDEO_ENCODER_INFO, INFO::DEFAULT_CONTEXT));
+ SetControlLabel(i++, "{} {}", 13287, SYSTEM_SCREEN_RESOLUTION);
+
+ auto renderingSystem = CServiceBroker::GetRenderSystem();
+ if (renderingSystem)
+ {
+ static std::string vendor = renderingSystem->GetRenderVendor();
+ if (!vendor.empty())
+ SET_CONTROL_LABEL(i++, StringUtils::Format("{} {}", g_localizeStrings.Get(22007), vendor));
+
+#if defined(HAS_DX)
+ int renderVersionLabel = 22024;
+#else
+ int renderVersionLabel = 22009;
+#endif
+ static std::string version = renderingSystem->GetRenderVersionString();
+ if (!version.empty())
+ SET_CONTROL_LABEL(
+ i++, StringUtils::Format("{} {}", g_localizeStrings.Get(renderVersionLabel), version));
+ }
+
+ auto windowSystem = CServiceBroker::GetWinSystem();
+ if (windowSystem)
+ {
+ static std::string platform = windowSystem->GetName();
+ if (platform != "platform default")
+ SET_CONTROL_LABEL(i++,
+ StringUtils::Format("{} {}", g_localizeStrings.Get(39153), platform));
+ }
+
+ SetControlLabel(i++, "{} {}", 22010, SYSTEM_GPU_TEMPERATURE);
+
+ const std::string hdrTypes = CServiceBroker::GetGUI()->GetInfoManager().GetLabel(
+ SYSTEM_SUPPORTED_HDR_TYPES, INFO::DEFAULT_CONTEXT);
+ SET_CONTROL_LABEL(
+ i++, StringUtils::Format("{}: {}", g_localizeStrings.Get(39174),
+ hdrTypes.empty() ? g_localizeStrings.Get(231) : hdrTypes));
+ }
+
+ else if (m_section == CONTROL_BT_HARDWARE)
+ {
+ SET_CONTROL_LABEL(40,g_localizeStrings.Get(20160));
+
+ auto cpuInfo = CServiceBroker::GetCPUInfo();
+ if (cpuInfo)
+ {
+ static std::string model = cpuInfo->GetCPUModel();
+ if (!model.empty())
+ SET_CONTROL_LABEL(i++, "CPU: " + model);
+
+ static std::string mips = cpuInfo->GetCPUBogoMips();
+ if (!mips.empty())
+ SET_CONTROL_LABEL(i++, "BogoMips: " + mips);
+
+ static std::string soc = cpuInfo->GetCPUSoC();
+ if (!soc.empty())
+ SET_CONTROL_LABEL(i++, "SoC: " + soc);
+
+ static std::string hardware = cpuInfo->GetCPUHardware();
+ if (!hardware.empty())
+ SET_CONTROL_LABEL(i++, "Hardware: " + hardware);
+
+ static std::string revision = cpuInfo->GetCPURevision();
+ if (!revision.empty())
+ SET_CONTROL_LABEL(i++, "Revision: " + revision);
+
+ static std::string serial = cpuInfo->GetCPUSerial();
+ if (!serial.empty())
+ SET_CONTROL_LABEL(i++, "Serial: " + serial);
+
+ // temperature can't really be conditional because of localization units
+ SetControlLabel(i++, "{} {}", 22011, SYSTEM_CPU_TEMPERATURE);
+
+ // we can check if the cpufrequency is not 0 (default if not implemented)
+ // but we have to call through CGUIInfoManager -> CSystemGUIInfo -> CSysInfo
+ // to limit the frequency of updates
+ static float cpuFreq = cpuInfo->GetCPUFrequency();
+ if (cpuFreq > 0)
+ SetControlLabel(i++, "{} {}", 13284, SYSTEM_CPUFREQUENCY);
+ }
+ }
+
+ else if (m_section == CONTROL_BT_PVR)
+ {
+ SET_CONTROL_LABEL(40, g_localizeStrings.Get(19166));
+ int i = 2;
+
+ SetControlLabel(i++, "{}: {}", 19120, PVR_BACKEND_NUMBER);
+ i++; // empty line
+ SetControlLabel(i++, "{}: {}", 19012, PVR_BACKEND_NAME);
+ SetControlLabel(i++, "{}: {}", 19114, PVR_BACKEND_VERSION);
+ SetControlLabel(i++, "{}: {}", 19115, PVR_BACKEND_HOST);
+ SetControlLabel(i++, "{}: {}", 19116, PVR_BACKEND_DISKSPACE);
+ SetControlLabel(i++, "{}: {}", 19334, PVR_BACKEND_PROVIDERS);
+ SetControlLabel(i++, "{}: {}", 19042, PVR_BACKEND_CHANNEL_GROUPS);
+ SetControlLabel(i++, "{}: {}", 19019, PVR_BACKEND_CHANNELS);
+ SetControlLabel(i++, "{}: {}", 19163, PVR_BACKEND_RECORDINGS);
+ SetControlLabel(i++, "{}: {}", 19168,
+ PVR_BACKEND_DELETED_RECORDINGS); // Deleted and recoverable recordings
+ SetControlLabel(i++, "{}: {}", 19025, PVR_BACKEND_TIMERS);
+ }
+
+ else if (m_section == CONTROL_BT_POLICY)
+ {
+ SET_CONTROL_LABEL(40, g_localizeStrings.Get(12389));
+ }
+ CGUIWindow::FrameMove();
+}
+
+void CGUIWindowSystemInfo::ResetLabels()
+{
+ for (int i = 2; i < 13; i++)
+ {
+ SET_CONTROL_LABEL(i, "");
+ }
+ SET_CONTROL_LABEL(CONTROL_TB_POLICY, "");
+}
+
+void CGUIWindowSystemInfo::SetControlLabel(int id, const char *format, int label, int info)
+{
+ std::string tmpStr = StringUtils::Format(
+ format, g_localizeStrings.Get(label),
+ CServiceBroker::GetGUI()->GetInfoManager().GetLabel(info, INFO::DEFAULT_CONTEXT));
+ SET_CONTROL_LABEL(id, tmpStr);
+}
diff --git a/xbmc/windows/GUIWindowSystemInfo.h b/xbmc/windows/GUIWindowSystemInfo.h
new file mode 100644
index 0000000..e2fc50a
--- /dev/null
+++ b/xbmc/windows/GUIWindowSystemInfo.h
@@ -0,0 +1,29 @@
+/*
+ * 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 "guilib/GUIWindow.h"
+
+#include <string>
+#include <vector>
+
+class CGUIWindowSystemInfo : public CGUIWindow
+{
+public:
+ CGUIWindowSystemInfo(void);
+ ~CGUIWindowSystemInfo(void) override;
+ bool OnMessage(CGUIMessage& message) override;
+ void FrameMove() override;
+private:
+ int m_section;
+ void ResetLabels();
+ void SetControlLabel(int id, const char *format, int label, int info);
+ std::vector<std::string> m_diskUsage;
+};
+